From 3412d57b5e7359156cdaf021624a1ff454aacf88 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 26 Jun 2021 23:23:32 -0400 Subject: [PATCH 001/435] Sentinel: fix for #1783 (#1784) This should help the flakiness in this test, it's a race described in #1783, but in the test not the functionality. --- src/StackExchange.Redis/ResultProcessor.cs | 10 ++++-- .../StackExchange.Redis.Tests/SentinelBase.cs | 35 +++++++------------ .../SentinelFailover.cs | 19 ++++++++-- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 9f8dd5c51..6f3c9b0f5 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -2278,8 +2278,14 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var returnArray = result.ToArray[], StringPairInterleavedProcessor>( (in RawResult rawInnerArray, in StringPairInterleavedProcessor proc) => { - proc.TryParse(rawInnerArray, out KeyValuePair[] kvpArray); - return kvpArray; + if (proc.TryParse(rawInnerArray, out KeyValuePair[] kvpArray)) + { + return kvpArray; + } + else + { + throw new ArgumentOutOfRangeException(nameof(rawInnerArray), $"Error processing {message.CommandAndKey}, could not decode array '{rawInnerArray}'"); + } }, innerProcessor); SetResult(message, returnArray); diff --git a/tests/StackExchange.Redis.Tests/SentinelBase.cs b/tests/StackExchange.Redis.Tests/SentinelBase.cs index b9d5a3aa1..c46e9691f 100644 --- a/tests/StackExchange.Redis.Tests/SentinelBase.cs +++ b/tests/StackExchange.Redis.Tests/SentinelBase.cs @@ -39,10 +39,14 @@ public async Task InitializeAsync() for (var i = 0; i < 150; i++) { - await Task.Delay(20).ForAwait(); - if (Conn.IsConnected && Conn.GetSentinelMasterConnection(options, Writer).IsConnected) + await Task.Delay(100).ForAwait(); + if (Conn.IsConnected) { - break; + using var checkConn = Conn.GetSentinelMasterConnection(options, Writer); + if (checkConn.IsConnected) + { + break; + } } } Assert.True(Conn.IsConnected); @@ -64,22 +68,6 @@ protected class IpComparer : IEqualityComparer public int GetHashCode(string obj) => obj.GetHashCode(); } - protected async Task DoFailoverAsync() - { - await WaitForReadyAsync(); - - // capture current replica - var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); - - Log("Starting failover..."); - var sw = Stopwatch.StartNew(); - SentinelServerA.SentinelFailover(ServiceName); - - // wait until the replica becomes the master - await WaitForReadyAsync(expectedMaster: replicas[0]); - Log($"Time to failover: {sw.Elapsed}"); - } - protected async Task WaitForReadyAsync(EndPoint expectedMaster = null, bool waitForReplication = false, TimeSpan? duration = null) { duration ??= TimeSpan.FromSeconds(30); @@ -109,12 +97,15 @@ protected async Task WaitForReadyAsync(EndPoint expectedMaster = null, bool wait throw new RedisException($"Master was expected to be {expectedMaster}"); Log($"Master is {master}"); - var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); - var checkConn = Conn.GetSentinelMasterConnection(ServiceOptions); + using var checkConn = Conn.GetSentinelMasterConnection(ServiceOptions); await WaitForRoleAsync(checkConn.GetServer(master), "master", duration.Value.Subtract(sw.Elapsed)).ForAwait(); + + var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); if (replicas.Length > 0) { + await Task.Delay(1000).ForAwait(); + replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); await WaitForRoleAsync(checkConn.GetServer(replicas[0]), "slave", duration.Value.Subtract(sw.Elapsed)).ForAwait(); } @@ -145,7 +136,7 @@ protected async Task WaitForRoleAsync(IServer server, string role, TimeSpan? dur // ignore } - await Task.Delay(1000).ForAwait(); + await Task.Delay(500).ForAwait(); } throw new RedisException($"Timeout waiting for server ({server.EndPoint}) to have expected role (\"{role}\") assigned"); diff --git a/tests/StackExchange.Redis.Tests/SentinelFailover.cs b/tests/StackExchange.Redis.Tests/SentinelFailover.cs index 975fc4c98..3063f0448 100644 --- a/tests/StackExchange.Redis.Tests/SentinelFailover.cs +++ b/tests/StackExchange.Redis.Tests/SentinelFailover.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Xunit; @@ -45,13 +46,26 @@ public async Task ManagedMasterConnectionEndToEndWithFailoverTest() var value = await db.StringGetAsync(key); Assert.Equal(expected, value); + Log("Waiting for first replication check..."); // force read from replica, replication has some lag await WaitForReplicationAsync(servers.First()).ForAwait(); value = await db.StringGetAsync(key, CommandFlags.DemandReplica); Assert.Equal(expected, value); + + Log("Waiting for ready pre-failover..."); + await WaitForReadyAsync(); - // forces and verifies failover - await DoFailoverAsync(); + // capture current replica + var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); + + Log("Starting failover..."); + var sw = Stopwatch.StartNew(); + SentinelServerA.SentinelFailover(ServiceName); + + // wait until the replica becomes the master + Log("Waiting for ready post-failover..."); + await WaitForReadyAsync(expectedMaster: replicas[0]); + Log($"Time to failover: {sw.Elapsed}"); endpoints = conn.GetEndPoints(); Assert.Equal(2, endpoints.Length); @@ -70,6 +84,7 @@ public async Task ManagedMasterConnectionEndToEndWithFailoverTest() value = await db.StringGetAsync(key); Assert.Equal(expected, value); + Log("Waiting for second replication check..."); // force read from replica, replication has some lag await WaitForReplicationAsync(newMaster).ForAwait(); value = await db.StringGetAsync(key, CommandFlags.DemandReplica); From 50a832d102fcb7cfae39bccda2417c01057a554a Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 26 Jun 2021 23:24:02 -0400 Subject: [PATCH 002/435] Fix benchmarks output --- .gitignore | 3 ++- tests/BasicTest/Program.cs | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3d1821b36..c0024fb1f 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ t8.shakespeare.txt launchSettings.json *.vsp *.diagsession -TestResults/ \ No newline at end of file +TestResults/ +BenchmarkDotNet.Artifacts/ diff --git a/tests/BasicTest/Program.cs b/tests/BasicTest/Program.cs index e14a4a67d..342d33d2d 100644 --- a/tests/BasicTest/Program.cs +++ b/tests/BasicTest/Program.cs @@ -31,7 +31,6 @@ public CustomConfig() AddValidator(JitOptimizationsValidator.FailOnError); AddJob(Configure(Job.Default.WithRuntime(ClrRuntime.Net472))); - AddJob(Configure(Job.Default.WithRuntime(CoreRuntime.Core31))); AddJob(Configure(Job.Default.WithRuntime(CoreRuntime.Core50))); } } From 119f6fa0b35367b3d2bfa074b7f6106a7ed2ce16 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 26 Jun 2021 23:24:25 -0400 Subject: [PATCH 003/435] Fix CheckFailureRecovered test race Debug only - doesn't run in CI --- tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs b/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs index 1f280a95b..8fe8d47e6 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs @@ -174,7 +174,7 @@ public async Task CheckFailureRecovered() { try { - using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true)) + using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, log: Writer)) { await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); void innerScenario() @@ -186,7 +186,9 @@ void innerScenario() server.SimulateConnectionFailure(); - Assert.Equal(ConnectionFailureType.SocketFailure, ((RedisConnectionException)muxer.GetServerSnapshot()[0].LastException).FailureType); + var lastFailure = ((RedisConnectionException)muxer.GetServerSnapshot()[0].LastException).FailureType; + // Depending on heartbat races, the last exception will be a socket failure or an internal (follow-up) failure + Assert.Contains(lastFailure, new[] { ConnectionFailureType.SocketFailure, ConnectionFailureType.InternalFailure }); // should reconnect within 1 keepalive interval muxer.AllowConnect = true; From a49bd65789b3326b99dd12013b03012aea1c08f9 Mon Sep 17 00:00:00 2001 From: Alexander Satov Date: Sun, 27 Jun 2021 08:35:22 +0500 Subject: [PATCH 004/435] OnManagedConnectionFailed thread/memory leak fix (#1710) Hi. I faced a problem with 'ConnectionMultiplexer': when it loses connection with Redis cluster, 'OnManagedConnectionFailed' event handler creates timer, which tries to 'SwitchMaster'. Timer ticks every second, but there is PLINQ query in 'GetConfiguredMasterForService' method, that leads to constantly threads starting (threads start faster than they exit). And if disconnect time is long enough, I get thread/memory leak in service. PR contains hotfix of a problem with timer (timer starts next iteration only after previous iteration complete). But maybe you should take a closer look at PLINQ queries (they seem superfluous). Also, I commented `TextWriter.Null` creation in 'SwitchMaster', because it seems like useless memory allocations (you handle nullable LogProxy). --- .../ConnectionMultiplexer.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 2ddb20e41..53d8a847d 100755 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -2477,11 +2477,9 @@ internal void OnManagedConnectionRestored(object sender, ConnectionFailedEventAr { ConnectionMultiplexer connection = (ConnectionMultiplexer)sender; - if (connection.sentinelMasterReconnectTimer != null) - { - connection.sentinelMasterReconnectTimer.Dispose(); - connection.sentinelMasterReconnectTimer = null; - } + var oldTimer = Interlocked.Exchange(ref connection.sentinelMasterReconnectTimer, null); + oldTimer?.Dispose(); + try { @@ -2524,7 +2522,7 @@ internal void OnManagedConnectionFailed(object sender, ConnectionFailedEventArgs // or if we miss the published master change if (connection.sentinelMasterReconnectTimer == null) { - connection.sentinelMasterReconnectTimer = new Timer((_) => + connection.sentinelMasterReconnectTimer = new Timer(_ => { try { @@ -2533,9 +2531,12 @@ internal void OnManagedConnectionFailed(object sender, ConnectionFailedEventArgs } catch (Exception) { - } - }, null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1)); + finally + { + connection.sentinelMasterReconnectTimer?.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan); + } + }, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan); } } @@ -2730,6 +2731,8 @@ public void Dispose() GC.SuppressFinalize(this); Close(!_isDisposed); sentinelConnection?.Dispose(); + var oldTimer = Interlocked.Exchange(ref sentinelMasterReconnectTimer, null); + oldTimer?.Dispose(); } internal Task ExecuteAsyncImpl(Message message, ResultProcessor processor, object state, ServerEndPoint server) From 5324b15881f6a154d9de16577b8511aa569a2594 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 26 Jun 2021 23:40:28 -0400 Subject: [PATCH 005/435] Add #1710 to release notes --- docs/ReleaseNotes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index f6ef64307..e8691acb3 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,5 +1,9 @@ # Release Notes +## Unreleased + +- Sentinel potential memory leak fix in OnManagedConnectionFailed handler (#1710 via alexSatov) + ## 2.2.50 - performance optimization for PING accuracy (#1714 via eduardobr) From 51d2946b7f1499605d90b95a00bcb8f776ff0b49 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sun, 27 Jun 2021 13:22:45 -0400 Subject: [PATCH 006/435] Code cleanup: Phase 1 (#1785) Tidying up for newer C# versions and removing old cruft: - Removing unneeded pragmas (e.g. transitive obsoletes) - Switch expressions - Expression body usages, where it's cleaner with new switch expressions - Statics where useful to remove virtual calls - A handful of pattern matching usages where we were repeat casting - `StringBuilder` optimizations for .NET Core (single char appends) - Suppression fixes for contention between frameworks - Suppression additions for dupe enum values (that are there for compat) - Fix messages/arguments for many argument exceptions - Re-enables the GC test (was the only skipped one - no longer needed) - Misc compiler warning fixes - Fixes KestrelRedisServer's conflict with `System.Runtime.CompilerServices.Unsafe` These are broken up by commit for an easier review, but didn't want to make 12 PRs for some maintenance catch-up. Note: this does not fix all informational message, including a few suspects for actual issues and some that are the way we structure `Dispose() => Dispose(true)` in a few files. Given those may change functionality, will do a follow-up PR there. This is meant to get all the noise gone and general cleanup without any significant impact anywhere. --- .editorconfig | 8 + .../Aggregation/AggregationRequest.cs | 2 +- src/NRediSearch/Client.cs | 4 +- src/NRediSearch/Extensions.cs | 17 +- src/NRediSearch/QueryBuilder/GeoValue.cs | 8 +- src/NRediSearch/QueryBuilder/QueryNode.cs | 4 +- src/NRediSearch/QueryBuilder/RangeValue.cs | 8 +- src/NRediSearch/QueryBuilder/ValueNode.cs | 8 +- src/NRediSearch/Schema.cs | 17 +- src/NRediSearch/Suggestion.cs | 1 + src/StackExchange.Redis/CommandBytes.cs | 2 +- src/StackExchange.Redis/CommandMap.cs | 2 +- src/StackExchange.Redis/Condition.cs | 84 +++------ .../ConfigurationOptions.cs | 16 +- .../ConnectionMultiplexer.cs | 32 ++-- src/StackExchange.Redis/EndPointCollection.cs | 2 +- src/StackExchange.Redis/Enums/ClientFlags.cs | 1 + src/StackExchange.Redis/Enums/ClientType.cs | 1 + src/StackExchange.Redis/Enums/CommandFlags.cs | 1 + .../Enums/ReplicationChangeOptions.cs | 1 + src/StackExchange.Redis/ExceptionFactory.cs | 4 +- src/StackExchange.Redis/ExponentialRetry.cs | 6 +- src/StackExchange.Redis/GeoEntry.cs | 18 +- .../KeyspaceIsolation/WrapperBase.cs | 2 +- src/StackExchange.Redis/Message.cs | 72 ++++---- src/StackExchange.Redis/PhysicalBridge.cs | 26 +-- src/StackExchange.Redis/PhysicalConnection.cs | 14 +- src/StackExchange.Redis/RawResult.cs | 10 +- src/StackExchange.Redis/RedisBase.cs | 6 +- src/StackExchange.Redis/RedisBatch.cs | 4 +- src/StackExchange.Redis/RedisChannel.cs | 17 +- src/StackExchange.Redis/RedisDatabase.cs | 58 +++--- src/StackExchange.Redis/RedisKey.cs | 14 +- src/StackExchange.Redis/RedisLiterals.cs | 17 +- src/StackExchange.Redis/RedisServer.cs | 53 ++---- src/StackExchange.Redis/RedisSubscriber.cs | 3 +- src/StackExchange.Redis/RedisTransaction.cs | 7 +- src/StackExchange.Redis/RedisValue.cs | 168 ++++++++---------- src/StackExchange.Redis/ResultBox.cs | 2 + src/StackExchange.Redis/ResultProcessor.cs | 20 ++- .../ScriptParameterMapper.cs | 22 +-- src/StackExchange.Redis/ServerEndPoint.cs | 19 +- .../ServerSelectionStrategy.cs | 10 +- src/StackExchange.Redis/StreamPosition.cs | 14 +- tests/BasicTest/Program.cs | 1 - .../ClientTests/AggregationBuilderTests.cs | 2 +- .../ClientTests/ClientTest.cs | 105 ++++++----- tests/NRediSearch.Test/ExampleUsage.cs | 2 +- tests/StackExchange.Redis.Tests/Cluster.cs | 2 +- tests/StackExchange.Redis.Tests/Failover.cs | 2 +- .../GarbageCollectionTests.cs | 3 +- .../GlobalSuppressions.cs | 4 - .../Helpers/redis-sharp.cs | 18 +- tests/StackExchange.Redis.Tests/Locking.cs | 19 +- tests/StackExchange.Redis.Tests/MassiveOps.cs | 2 +- tests/StackExchange.Redis.Tests/Migrate.cs | 4 +- .../RedisValueEquivalency.cs | 22 +-- tests/StackExchange.Redis.Tests/Scripting.cs | 6 +- tests/StackExchange.Redis.Tests/TestBase.cs | 2 - .../TransactionWrapperTests.cs | 7 +- .../KestrelRedisServer.csproj | 1 + .../MemoryCacheRedisServer.cs | 1 - toys/StackExchange.Redis.Server/RespServer.cs | 6 +- 63 files changed, 439 insertions(+), 575 deletions(-) mode change 100755 => 100644 src/StackExchange.Redis/ConnectionMultiplexer.cs diff --git a/.editorconfig b/.editorconfig index 162be69f5..70eae8240 100644 --- a/.editorconfig +++ b/.editorconfig @@ -48,6 +48,9 @@ dotnet_style_explicit_tuple_names = true:suggestion # Ignore silly if statements dotnet_style_prefer_conditional_expression_over_return = false:none +# Don't warn on things that actually need suppressing +dotnet_remove_unnecessary_suppression_exclusions = CA1009,CA1063,CA1069,CA1416,CA1816,CA1822,CA2202,CS0618,IDE0060,IDE0062,RCS1047,RCS1085,RCS1090,RCS1194,RCS1231 + # CSharp code style settings: [*.cs] # Prefer method-like constructs to have a expression-body @@ -66,6 +69,11 @@ csharp_style_pattern_matching_over_as_with_null_check = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion +csharp_prefer_simple_using_statement = true:silent + +# Disable range operator suggestions +csharp_style_prefer_range_operator = false:none +csharp_style_prefer_index_operator = false:none # Newline settings csharp_new_line_before_open_brace = all diff --git a/src/NRediSearch/Aggregation/AggregationRequest.cs b/src/NRediSearch/Aggregation/AggregationRequest.cs index e5ad26572..b1cde1a22 100644 --- a/src/NRediSearch/Aggregation/AggregationRequest.cs +++ b/src/NRediSearch/Aggregation/AggregationRequest.cs @@ -94,7 +94,7 @@ public AggregationRequest GroupBy(IList fields, IList reducers) return this; } - public AggregationRequest GroupBy(String field, params Reducer[] reducers) + public AggregationRequest GroupBy(string field, params Reducer[] reducers) { return GroupBy(new string[] { field }, reducers); } diff --git a/src/NRediSearch/Client.cs b/src/NRediSearch/Client.cs index 9b5861388..effd08e34 100644 --- a/src/NRediSearch/Client.cs +++ b/src/NRediSearch/Client.cs @@ -134,7 +134,7 @@ public sealed class ConfiguredIndexOptions public static IndexOptions Default => new IndexOptions(); private IndexOptions _options; - private IndexDefinition _definition; + private readonly IndexDefinition _definition; private string[] _stopwords; public ConfiguredIndexOptions(IndexOptions options = IndexOptions.Default) @@ -1276,7 +1276,7 @@ public async Task GetDocumentsAsync(params string[] docIds) { if (docIds.Length == 0) { - return new Document[] { }; + return Array.Empty(); } var args = new List diff --git a/src/NRediSearch/Extensions.cs b/src/NRediSearch/Extensions.cs index e8770a287..90b7ad63f 100644 --- a/src/NRediSearch/Extensions.cs +++ b/src/NRediSearch/Extensions.cs @@ -23,16 +23,13 @@ internal static string AsRedisString(this double value, bool forceDecimal = fals return value.ToString(forceDecimal ? "#.0" : "G17", NumberFormatInfo.InvariantInfo); } } - internal static string AsRedisString(this GeoUnit value) + internal static string AsRedisString(this GeoUnit value) => value switch { - switch (value) - { - case GeoUnit.Feet: return "ft"; - case GeoUnit.Kilometers: return "km"; - case GeoUnit.Meters: return "m"; - case GeoUnit.Miles: return "mi"; - default: throw new InvalidOperationException($"Unknown unit: {value}"); - } - } + GeoUnit.Feet => "ft", + GeoUnit.Kilometers => "km", + GeoUnit.Meters => "m", + GeoUnit.Miles => "mi", + _ => throw new InvalidOperationException($"Unknown unit: {value}"), + }; } } diff --git a/src/NRediSearch/QueryBuilder/GeoValue.cs b/src/NRediSearch/QueryBuilder/GeoValue.cs index 655b919a9..3b51f46de 100644 --- a/src/NRediSearch/QueryBuilder/GeoValue.cs +++ b/src/NRediSearch/QueryBuilder/GeoValue.cs @@ -21,11 +21,11 @@ public GeoValue(double lon, double lat, double radius, GeoUnit unit) public override string ToString() { return new StringBuilder("[") - .Append(_lon.AsRedisString(true)).Append(" ") - .Append(_lat.AsRedisString(true)).Append(" ") - .Append(_radius.AsRedisString(true)).Append(" ") + .Append(_lon.AsRedisString(true)).Append(' ') + .Append(_lat.AsRedisString(true)).Append(' ') + .Append(_radius.AsRedisString(true)).Append(' ') .Append(_unit.AsRedisString()) - .Append("]").ToString(); + .Append(']').ToString(); } public override bool IsCombinable() => false; diff --git a/src/NRediSearch/QueryBuilder/QueryNode.cs b/src/NRediSearch/QueryBuilder/QueryNode.cs index 6883c0eee..b75e93115 100644 --- a/src/NRediSearch/QueryBuilder/QueryNode.cs +++ b/src/NRediSearch/QueryBuilder/QueryNode.cs @@ -80,7 +80,7 @@ public virtual string ToString(ParenMode mode) if (ShouldUseParens(mode)) { - sb.Append("("); + sb.Append('('); } var sj = new StringJoiner(sb, GetJoinString()); foreach (var n in children) @@ -89,7 +89,7 @@ public virtual string ToString(ParenMode mode) } if (ShouldUseParens(mode)) { - sb.Append(")"); + sb.Append(')'); } return sb.ToString(); } diff --git a/src/NRediSearch/QueryBuilder/RangeValue.cs b/src/NRediSearch/QueryBuilder/RangeValue.cs index f53fb7ab4..8fa6f105d 100644 --- a/src/NRediSearch/QueryBuilder/RangeValue.cs +++ b/src/NRediSearch/QueryBuilder/RangeValue.cs @@ -15,7 +15,7 @@ private static void AppendNum(StringBuilder sb, double n, bool inclusive) { if (!inclusive) { - sb.Append("("); + sb.Append('('); } sb.Append(n.AsRedisString(true)); } @@ -23,11 +23,11 @@ private static void AppendNum(StringBuilder sb, double n, bool inclusive) public override string ToString() { StringBuilder sb = new StringBuilder(); - sb.Append("["); + sb.Append('['); AppendNum(sb, from, inclusiveMin); - sb.Append(" "); + sb.Append(' '); AppendNum(sb, to, inclusiveMax); - sb.Append("]"); + sb.Append(']'); return sb.ToString(); } diff --git a/src/NRediSearch/QueryBuilder/ValueNode.cs b/src/NRediSearch/QueryBuilder/ValueNode.cs index c17044053..15e751d83 100644 --- a/src/NRediSearch/QueryBuilder/ValueNode.cs +++ b/src/NRediSearch/QueryBuilder/ValueNode.cs @@ -40,7 +40,7 @@ private string ToStringCombinable(ParenMode mode) StringBuilder sb = new StringBuilder(FormatField()); if (_values.Length > 1 || mode == ParenMode.Always) { - sb.Append("("); + sb.Append('('); } var sj = new StringJoiner(sb, _joinString); foreach (var v in _values) @@ -49,7 +49,7 @@ private string ToStringCombinable(ParenMode mode) } if (_values.Length > 1 || mode == ParenMode.Always) { - sb.Append(")"); + sb.Append(')'); } return sb.ToString(); } @@ -64,7 +64,7 @@ private string ToStringDefault(ParenMode mode) var sb = new StringBuilder(); if (useParen) { - sb.Append("("); + sb.Append('('); } var sj = new StringJoiner(sb, _joinString); foreach (var v in _values) @@ -73,7 +73,7 @@ private string ToStringDefault(ParenMode mode) } if (useParen) { - sb.Append(")"); + sb.Append(')'); } return sb.ToString(); } diff --git a/src/NRediSearch/Schema.cs b/src/NRediSearch/Schema.cs index cd2e4ae36..ee9561aec 100644 --- a/src/NRediSearch/Schema.cs +++ b/src/NRediSearch/Schema.cs @@ -36,17 +36,14 @@ internal Field(string name, FieldType type, bool sortable, bool noIndex = false) internal virtual void SerializeRedisArgs(List args) { - static object GetForRedis(FieldType type) + static object GetForRedis(FieldType type) => type switch { - switch (type) - { - case FieldType.FullText: return "TEXT".Literal(); - case FieldType.Geo: return "GEO".Literal(); - case FieldType.Numeric: return "NUMERIC".Literal(); - case FieldType.Tag: return "TAG".Literal(); - default: throw new ArgumentOutOfRangeException(nameof(type)); - } - } + FieldType.FullText => "TEXT".Literal(), + FieldType.Geo => "GEO".Literal(), + FieldType.Numeric => "NUMERIC".Literal(), + FieldType.Tag => "TAG".Literal(), + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; args.Add(Name); args.Add(GetForRedis(Type)); if (Sortable) { args.Add("SORTABLE".Literal()); } diff --git a/src/NRediSearch/Suggestion.cs b/src/NRediSearch/Suggestion.cs index 3e907b735..e92d4d56b 100644 --- a/src/NRediSearch/Suggestion.cs +++ b/src/NRediSearch/Suggestion.cs @@ -91,6 +91,7 @@ public SuggestionBuilder Payload(string payload) public Suggestion Build() => Build(false); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0071:Simplify interpolation", Justification = "Allocations (string.Concat vs. string.Format)")] internal Suggestion Build(bool fromServer) { bool isStringMissing = _string == null; diff --git a/src/StackExchange.Redis/CommandBytes.cs b/src/StackExchange.Redis/CommandBytes.cs index 7579bc1cc..67f8e10d5 100644 --- a/src/StackExchange.Redis/CommandBytes.cs +++ b/src/StackExchange.Redis/CommandBytes.cs @@ -136,7 +136,7 @@ public unsafe CommandBytes(ReadOnlySpan value) public unsafe CommandBytes(in ReadOnlySequence value) { - if (value.Length > MaxLength) throw new ArgumentOutOfRangeException("Maximum command length exceeed"); + if (value.Length > MaxLength) throw new ArgumentOutOfRangeException(nameof(value), "Maximum command length exceeed"); int len = unchecked((int)value.Length); _0 = _1 = _2 = _3 = 0L; fixed (ulong* uPtr = &_0) diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index 78bd5fbfe..0bfd6de3e 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -130,7 +130,7 @@ public static CommandMap Create(HashSet commands, bool available = true) { if (Enum.TryParse(command, true, out RedisCommand parsed)) { - (exclusions ?? (exclusions = new HashSet())).Add(parsed); + (exclusions ??= new HashSet()).Add(parsed); } } } diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs index d21f9bc58..b775cde02 100644 --- a/src/StackExchange.Redis/Condition.cs +++ b/src/StackExchange.Redis/Condition.cs @@ -357,6 +357,7 @@ public static Message CreateMessage(Condition condition, int db, CommandFlags fl return new ConditionMessage(condition, db, flags, command, key, value, value1); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0071:Simplify interpolation", Justification = "Allocations (string.Concat vs. string.Format)")] protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog($"condition '{message.CommandAndKey}' got '{result.ToString()}'"); @@ -426,7 +427,7 @@ internal override Condition MapKeys(Func map) public ExistsCondition(in RedisKey key, RedisType type, in RedisValue expectedValue, bool expectedResult) { - if (key.IsNull) throw new ArgumentException("key"); + if (key.IsNull) throw new ArgumentNullException(nameof(key)); this.key = key; this.type = type; this.expectedValue = expectedValue; @@ -438,23 +439,13 @@ public ExistsCondition(in RedisKey key, RedisType type, in RedisValue expectedVa } else { - switch (type) + cmd = type switch { - case RedisType.Hash: - cmd = RedisCommand.HEXISTS; - break; - - case RedisType.Set: - cmd = RedisCommand.SISMEMBER; - break; - - case RedisType.SortedSet: - cmd = RedisCommand.ZSCORE; - break; - - default: - throw new ArgumentException(nameof(type)); - } + RedisType.Hash => RedisCommand.HEXISTS, + RedisType.Set => RedisCommand.SISMEMBER, + RedisType.SortedSet => RedisCommand.ZSCORE, + _ => throw new ArgumentException($"Type {type} is not recognized", nameof(type)), + }; } } @@ -516,25 +507,18 @@ internal override Condition MapKeys(Func map) public EqualsCondition(in RedisKey key, RedisType type, in RedisValue memberName, bool expectedEqual, in RedisValue expectedValue) { - if (key.IsNull) throw new ArgumentException("key"); + if (key.IsNull) throw new ArgumentNullException(nameof(key)); this.key = key; this.memberName = memberName; this.expectedEqual = expectedEqual; this.expectedValue = expectedValue; this.type = type; - switch (type) + cmd = type switch { - case RedisType.Hash: - cmd = memberName.IsNull ? RedisCommand.GET : RedisCommand.HGET; - break; - - case RedisType.SortedSet: - cmd = RedisCommand.ZSCORE; - break; - - default: - throw new ArgumentException(nameof(type)); - } + RedisType.Hash => memberName.IsNull ? RedisCommand.GET : RedisCommand.HGET, + RedisType.SortedSet => RedisCommand.ZSCORE, + _ => throw new ArgumentException($"Unknown type: {type}", nameof(type)), + }; } public override string ToString() @@ -610,7 +594,7 @@ internal override Condition MapKeys(Func map) private readonly RedisKey key; public ListCondition(in RedisKey key, long index, bool expectedResult, in RedisValue? expectedValue) { - if (key.IsNull) throw new ArgumentException(nameof(key)); + if (key.IsNull) throw new ArgumentNullException(nameof(key)); this.key = key; this.index = index; this.expectedResult = expectedResult; @@ -680,36 +664,20 @@ internal override Condition MapKeys(Func map) public LengthCondition(in RedisKey key, RedisType type, int compareToResult, long expectedLength) { - if (key.IsNull) throw new ArgumentException(nameof(key)); + if (key.IsNull) throw new ArgumentNullException(nameof(key)); this.key = key; this.compareToResult = compareToResult; this.expectedLength = expectedLength; this.type = type; - switch (type) + cmd = type switch { - case RedisType.Hash: - cmd = RedisCommand.HLEN; - break; - - case RedisType.Set: - cmd = RedisCommand.SCARD; - break; - - case RedisType.List: - cmd = RedisCommand.LLEN; - break; - - case RedisType.SortedSet: - cmd = RedisCommand.ZCARD; - break; - - case RedisType.String: - cmd = RedisCommand.STRLEN; - break; - - default: - throw new ArgumentException(nameof(type)); - } + RedisType.Hash => RedisCommand.HLEN, + RedisType.Set => RedisCommand.SCARD, + RedisType.List => RedisCommand.LLEN, + RedisType.SortedSet => RedisCommand.ZCARD, + RedisType.String => RedisCommand.STRLEN, + _ => throw new ArgumentException($"Type {type} isn't recognized", nameof(type)), + }; } public override string ToString() @@ -774,7 +742,7 @@ internal override Condition MapKeys(Func map) public SortedSetRangeLengthCondition(in RedisKey key, RedisValue min, RedisValue max, int compareToResult, long expectedLength) { - if (key.IsNull) throw new ArgumentException(nameof(key)); + if (key.IsNull) throw new ArgumentNullException(nameof(key)); this.key = key; this.min = min; this.max = max; @@ -844,7 +812,7 @@ public SortedSetScoreCondition(in RedisKey key, in RedisValue sortedSetScore, bo { if (key.IsNull) { - throw new ArgumentException("key"); + throw new ArgumentNullException(nameof(key)); } this.key = key; diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index b50b0a777..c6023d008 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -250,13 +250,11 @@ public CommandMap CommandMap get { if (commandMap != null) return commandMap; - switch (Proxy) + return Proxy switch { - case Proxy.Twemproxy: - return CommandMap.Twemproxy; - default: - return CommandMap.Default; - } + Proxy.Twemproxy => CommandMap.Twemproxy, + _ => CommandMap.Default, + }; } set { @@ -306,9 +304,7 @@ public int ConnectTimeout /// /// Specifies the time in seconds at which connections should be pinged to ensure validity /// -#pragma warning disable RCS1128 public int KeepAlive { get { return keepAlive.GetValueOrDefault(-1); } set { keepAlive = value; } } -#pragma warning restore RCS1128 // Use coalesce expression. /// /// The user to use to authenticate with the server. @@ -383,9 +379,7 @@ public bool PreserveAsyncOrder /// /// Specifies the time in milliseconds that the system should allow for synchronous operations (defaults to 5 seconds) /// -#pragma warning disable RCS1128 public int SyncTimeout { get { return syncTimeout.GetValueOrDefault(5000); } set { syncTimeout = value; } } -#pragma warning restore RCS1128 /// /// Tie-breaker used to choose between masters (must match the endpoint exactly) @@ -405,9 +399,7 @@ public bool PreserveAsyncOrder /// /// Check configuration every n seconds (every minute by default) /// -#pragma warning disable RCS1128 public int ConfigCheckSeconds { get { return configCheckSeconds.GetValueOrDefault(60); } set { configCheckSeconds = value; } } -#pragma warning restore RCS1128 /// /// Parse the configuration from a comma-delimited configuration string diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs old mode 100755 new mode 100644 index 53d8a847d..a4de5e997 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -709,7 +709,7 @@ private static bool AllComplete(Task[] tasks) return true; } - private async Task WaitAllIgnoreErrorsAsync(string name, Task[] tasks, int timeoutMilliseconds, LogProxy log, [CallerMemberName] string caller = null, [CallerLineNumber] int callerLineNumber = 0) + private static async Task WaitAllIgnoreErrorsAsync(string name, Task[] tasks, int timeoutMilliseconds, LogProxy log, [CallerMemberName] string caller = null, [CallerLineNumber] int callerLineNumber = 0) { if (tasks == null) throw new ArgumentNullException(nameof(tasks)); if (tasks.Length == 0) @@ -2011,11 +2011,11 @@ private void ResetAllNonConnected() } } -#pragma warning disable IDE0060 + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Used - it's a partial")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Partial - may use instance data")] partial void OnTraceLog(LogProxy log, [CallerMemberName] string caller = null); -#pragma warning restore IDE0060 - private async Task NominatePreferredMaster(LogProxy log, ServerEndPoint[] servers, bool useTieBreakers, Task[] tieBreakers, List masters, int timeoutMs) + private static async Task NominatePreferredMaster(LogProxy log, ServerEndPoint[] servers, bool useTieBreakers, Task[] tieBreakers, List masters, int timeoutMs) { Dictionary uniques = null; if (useTieBreakers) @@ -2123,7 +2123,7 @@ private async Task NominatePreferredMaster(LogProxy log, ServerE return masters[0]; } - private ServerEndPoint SelectServerByElection(ServerEndPoint[] servers, string endpoint, LogProxy log) + private static ServerEndPoint SelectServerByElection(ServerEndPoint[] servers, string endpoint, LogProxy log) { if (servers == null || string.IsNullOrWhiteSpace(endpoint)) return null; for (int i = 0; i < servers.Length; i++) @@ -2249,10 +2249,8 @@ private ValueTask TryPushMessageToBridgeAsync(Message message, R => PrepareToPushMessageToBridge(message, processor, resultBox, ref server) ? server.TryWriteAsync(message) : new ValueTask(WriteResult.NoConnectionAvailable); [Obsolete("prefer async")] -#pragma warning disable CS0618 private WriteResult TryPushMessageToBridgeSync(Message message, ResultProcessor processor, IResultBox resultBox, ref ServerEndPoint server) => PrepareToPushMessageToBridge(message, processor, resultBox, ref server) ? server.TryWriteSync(message) : WriteResult.NoConnectionAvailable; -#pragma warning restore CS0618 /// /// See Object.ToString() @@ -2780,21 +2778,15 @@ private static async Task ExecuteAsyncImpl_Awaited(ConnectionMultiplexer @ return tcs == null ? default(T) : await tcs.Task.ForAwait(); } - internal Exception GetException(WriteResult result, Message message, ServerEndPoint server) + internal Exception GetException(WriteResult result, Message message, ServerEndPoint server) => result switch { - switch (result) - { - case WriteResult.Success: return null; - case WriteResult.NoConnectionAvailable: - return ExceptionFactory.NoConnectionAvailable(this, message, server); - case WriteResult.TimeoutBeforeWrite: - return ExceptionFactory.Timeout(this, "The timeout was reached before the message could be written to the output buffer, and it was not sent", message, server, result); - case WriteResult.WriteFailure: - default: - return ExceptionFactory.ConnectionFailure(IncludeDetailInExceptions, ConnectionFailureType.ProtocolFailure, "An unknown error occurred when writing the message", server); - } - } + WriteResult.Success => null, + WriteResult.NoConnectionAvailable => ExceptionFactory.NoConnectionAvailable(this, message, server), + WriteResult.TimeoutBeforeWrite => ExceptionFactory.Timeout(this, "The timeout was reached before the message could be written to the output buffer, and it was not sent", message, server, result), + _ => ExceptionFactory.ConnectionFailure(IncludeDetailInExceptions, ConnectionFailureType.ProtocolFailure, "An unknown error occurred when writing the message", server), + }; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Intentional observation")] internal static void ThrowFailed(TaskCompletionSource source, Exception unthrownException) { try diff --git a/src/StackExchange.Redis/EndPointCollection.cs b/src/StackExchange.Redis/EndPointCollection.cs index 99bd065a7..506436260 100644 --- a/src/StackExchange.Redis/EndPointCollection.cs +++ b/src/StackExchange.Redis/EndPointCollection.cs @@ -41,7 +41,7 @@ public EndPointCollection(IList endpoints) : base(endpoints) {} public void Add(string hostAndPort) { var endpoint = Format.TryParseEndPoint(hostAndPort); - if (endpoint == null) throw new ArgumentException(); + if (endpoint == null) throw new ArgumentException($"Could not parse host and port from '{hostAndPort}'", nameof(hostAndPort)); Add(endpoint); } diff --git a/src/StackExchange.Redis/Enums/ClientFlags.cs b/src/StackExchange.Redis/Enums/ClientFlags.cs index cf87a2b3a..a652f61a4 100644 --- a/src/StackExchange.Redis/Enums/ClientFlags.cs +++ b/src/StackExchange.Redis/Enums/ClientFlags.cs @@ -18,6 +18,7 @@ namespace StackExchange.Redis /// N: no specific flag set /// [Flags] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "Compatibility")] public enum ClientFlags : long { /// diff --git a/src/StackExchange.Redis/Enums/ClientType.cs b/src/StackExchange.Redis/Enums/ClientType.cs index 2bf8a1c1e..d7bff3f25 100644 --- a/src/StackExchange.Redis/Enums/ClientType.cs +++ b/src/StackExchange.Redis/Enums/ClientType.cs @@ -6,6 +6,7 @@ namespace StackExchange.Redis /// /// The class of the connection /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "Compatibility")] public enum ClientType { /// diff --git a/src/StackExchange.Redis/Enums/CommandFlags.cs b/src/StackExchange.Redis/Enums/CommandFlags.cs index dffbad6f1..286e19cd6 100644 --- a/src/StackExchange.Redis/Enums/CommandFlags.cs +++ b/src/StackExchange.Redis/Enums/CommandFlags.cs @@ -7,6 +7,7 @@ namespace StackExchange.Redis /// Behaviour markers associated with a given command /// [Flags] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "Compatibility")] public enum CommandFlags { /// diff --git a/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs b/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs index 63412c594..4a76c0157 100644 --- a/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs +++ b/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs @@ -7,6 +7,7 @@ namespace StackExchange.Redis /// Additional operations to perform when making a server a master /// [Flags] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "Compatibility")] public enum ReplicationChangeOptions { /// diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 0885a33e7..661ce29b8 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -270,7 +270,7 @@ internal static Exception Timeout(ConnectionMultiplexer multiplexer, string base sb.Append(" (Please take a look at this article for some common client-side issues that can cause timeouts: "); sb.Append(timeoutHelpLink); - sb.Append(")"); + sb.Append(')'); var ex = new RedisTimeoutException(sb.ToString(), message?.Status ?? CommandStatus.Unknown) { @@ -397,7 +397,7 @@ internal static Exception UnableToConnect(ConnectionMultiplexer muxer, string fa if (muxer.AuthSuspect) sb.Append(" There was an authentication failure; check that passwords (or client certificates) are configured correctly."); else if (muxer.RawConfig.AbortOnConnectFail) sb.Append(" Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code."); } - if (!string.IsNullOrWhiteSpace(failureMessage)) sb.Append(" ").Append(failureMessage.Trim()); + if (!string.IsNullOrWhiteSpace(failureMessage)) sb.Append(' ').Append(failureMessage.Trim()); return new RedisConnectionException(ConnectionFailureType.UnableToConnect, sb.ToString()); } diff --git a/src/StackExchange.Redis/ExponentialRetry.cs b/src/StackExchange.Redis/ExponentialRetry.cs index 594ffaf20..ddda9c702 100644 --- a/src/StackExchange.Redis/ExponentialRetry.cs +++ b/src/StackExchange.Redis/ExponentialRetry.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace StackExchange.Redis { @@ -38,7 +38,7 @@ public bool ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSince { var exponential = (int)Math.Min(maxDeltaBackOffMilliseconds, deltaBackOffMilliseconds * Math.Pow(1.1, currentRetryCount)); int random; - r = r ?? new Random(); + r ??= new Random(); random = r.Next((int)deltaBackOffMilliseconds, exponential); return timeElapsedMillisecondsSinceLastRetry >= random; //exponential backoff with deltaBackOff of 5000ms @@ -53,4 +53,4 @@ public bool ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSince //5000 127738 } } -} \ No newline at end of file +} diff --git a/src/StackExchange.Redis/GeoEntry.cs b/src/StackExchange.Redis/GeoEntry.cs index bf97404d2..eee732e92 100644 --- a/src/StackExchange.Redis/GeoEntry.cs +++ b/src/StackExchange.Redis/GeoEntry.cs @@ -82,18 +82,14 @@ public GeoRadiusResult(in RedisValue member, double? distance, long? hash, GeoPo /// public readonly struct GeoPosition : IEquatable { - internal static string GetRedisUnit(GeoUnit unit) + internal static string GetRedisUnit(GeoUnit unit) => unit switch { - switch (unit) - { - case GeoUnit.Meters: return "m"; - case GeoUnit.Kilometers: return "km"; - case GeoUnit.Miles: return "mi"; - case GeoUnit.Feet: return "ft"; - default: - throw new ArgumentOutOfRangeException(nameof(unit)); - } - } + GeoUnit.Meters => "m", + GeoUnit.Kilometers => "km", + GeoUnit.Miles => "mi", + GeoUnit.Feet => "ft", + _ => throw new ArgumentOutOfRangeException(nameof(unit)), + }; /// /// The Latitude of the GeoPosition diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index ea33ef43f..d4b124f86 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -1042,7 +1042,7 @@ protected RedisChannel ToInner(RedisChannel outer) protected Func GetMapFunction() { // create as a delegate when first required, then re-use - return mapFunction ?? (mapFunction = new Func(ToInner)); + return mapFunction ??= new Func(ToInner); } } } diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 574557bfb..78e931ce6 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -59,6 +59,7 @@ internal abstract class Message : ICompletable internal PhysicalConnection.WriteStatus ConnectionWriteState { get; private set; } #endif [Conditional("DEBUG")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "DEBUG uses instance data")] internal void SetBacklogState(int position, PhysicalConnection physical) { #if DEBUG @@ -481,55 +482,46 @@ internal bool ResultBoxIsAsync } } - internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, RedisKey[] keys) + internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, RedisKey[] keys) => keys.Length switch { - switch (keys.Length) - { - case 0: return new CommandKeyMessage(db, flags, command, key); - case 1: return new CommandKeyKeyMessage(db, flags, command, key, keys[0]); - case 2: return new CommandKeyKeyKeyMessage(db, flags, command, key, keys[0], keys[1]); - default: return new CommandKeyKeysMessage(db, flags, command, key, keys); - } - } + 0 => new CommandKeyMessage(db, flags, command, key), + 1 => new CommandKeyKeyMessage(db, flags, command, key, keys[0]), + 2 => new CommandKeyKeyKeyMessage(db, flags, command, key, keys[0], keys[1]), + _ => new CommandKeyKeysMessage(db, flags, command, key, keys), + }; - internal static Message Create(int db, CommandFlags flags, RedisCommand command, IList keys) + internal static Message Create(int db, CommandFlags flags, RedisCommand command, IList keys) => keys.Count switch { - switch (keys.Count) - { - case 0: return new CommandMessage(db, flags, command); - case 1: return new CommandKeyMessage(db, flags, command, keys[0]); - case 2: return new CommandKeyKeyMessage(db, flags, command, keys[0], keys[1]); - case 3: return new CommandKeyKeyKeyMessage(db, flags, command, keys[0], keys[1], keys[2]); - default: return new CommandKeysMessage(db, flags, command, (keys as RedisKey[]) ?? keys.ToArray()); - } - } + 0 => new CommandMessage(db, flags, command), + 1 => new CommandKeyMessage(db, flags, command, keys[0]), + 2 => new CommandKeyKeyMessage(db, flags, command, keys[0], keys[1]), + 3 => new CommandKeyKeyKeyMessage(db, flags, command, keys[0], keys[1], keys[2]), + _ => new CommandKeysMessage(db, flags, command, (keys as RedisKey[]) ?? keys.ToArray()), + }; - internal static Message Create(int db, CommandFlags flags, RedisCommand command, IList values) + internal static Message Create(int db, CommandFlags flags, RedisCommand command, IList values) => values.Count switch { - switch (values.Count) - { - case 0: return new CommandMessage(db, flags, command); - case 1: return new CommandValueMessage(db, flags, command, values[0]); - case 2: return new CommandValueValueMessage(db, flags, command, values[0], values[1]); - case 3: return new CommandValueValueValueMessage(db, flags, command, values[0], values[1], values[2]); - // no 4; not worth adding - case 5: return new CommandValueValueValueValueValueMessage(db, flags, command, values[0], values[1], values[2], values[3], values[4]); - default: return new CommandValuesMessage(db, flags, command, (values as RedisValue[]) ?? values.ToArray()); - } - } + 0 => new CommandMessage(db, flags, command), + 1 => new CommandValueMessage(db, flags, command, values[0]), + 2 => new CommandValueValueMessage(db, flags, command, values[0], values[1]), + 3 => new CommandValueValueValueMessage(db, flags, command, values[0], values[1], values[2]), + // no 4; not worth adding + 5 => new CommandValueValueValueValueValueMessage(db, flags, command, values[0], values[1], values[2], values[3], values[4]), + _ => new CommandValuesMessage(db, flags, command, (values as RedisValue[]) ?? values.ToArray()), + }; internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, RedisValue[] values) { if (values == null) throw new ArgumentNullException(nameof(values)); - switch (values.Length) - { - case 0: return new CommandKeyMessage(db, flags, command, key); - case 1: return new CommandKeyValueMessage(db, flags, command, key, values[0]); - case 2: return new CommandKeyValueValueMessage(db, flags, command, key, values[0], values[1]); - case 3: return new CommandKeyValueValueValueMessage(db, flags, command, key, values[0], values[1], values[2]); - case 4: return new CommandKeyValueValueValueValueMessage(db, flags, command, key, values[0], values[1], values[2], values[3]); - default: return new CommandKeyValuesMessage(db, flags, command, key, values); - } + return values.Length switch + { + 0 => new CommandKeyMessage(db, flags, command, key), + 1 => new CommandKeyValueMessage(db, flags, command, key, values[0]), + 2 => new CommandKeyValueValueMessage(db, flags, command, key, values[0], values[1]), + 3 => new CommandKeyValueValueValueMessage(db, flags, command, key, values[0], values[1], values[2]), + 4 => new CommandKeyValueValueValueValueMessage(db, flags, command, key, values[0], values[1], values[2], values[3]), + _ => new CommandKeyValuesMessage(db, flags, command, key, values), + }; } internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, RedisValue[] values, in RedisKey key1) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 6036c561a..652b79719 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -165,10 +165,7 @@ public WriteResult TryWriteSync(Message message, bool isReplica) var physical = this.physical; if (physical == null) return FailDueToNoConnection(message); - -#pragma warning disable CS0618 var result = WriteMessageTakingWriteLockSync(physical, message); -#pragma warning restore CS0618 LogNonPreferred(message.Flags, isReplica); return result; } @@ -195,17 +192,17 @@ internal void AppendProfile(StringBuilder sb) } clone[ProfileLogSamples] = Interlocked.Read(ref operationCount); Array.Sort(clone); - sb.Append(" ").Append(clone[0]); + sb.Append(' ').Append(clone[0]); for (int i = 1; i < clone.Length; i++) { if (clone[i] != clone[i - 1]) { - sb.Append("+").Append(clone[i] - clone[i - 1]); + sb.Append('+').Append(clone[i] - clone[i - 1]); } } if (clone[0] != clone[ProfileLogSamples]) { - sb.Append("=").Append(clone[ProfileLogSamples]); + sb.Append('=').Append(clone[ProfileLogSamples]); } double rate = (clone[ProfileLogSamples] - clone[0]) / ProfileLogSeconds; sb.Append(" (").Append(rate.ToString("N2")).Append(" ops/s; spans ").Append(ProfileLogSeconds).Append("s)"); @@ -282,7 +279,7 @@ private async Task ExecuteSubscriptionLoop() // pushes items that have been enqu } internal bool TryEnqueueBackgroundSubscriptionWrite(in PendingSubscriptionState state) - => isDisposed ? false : (_subscriptionBackgroundQueue ?? GetSubscriptionQueue()).Writer.TryWrite(state); + => !isDisposed && (_subscriptionBackgroundQueue ?? GetSubscriptionQueue()).Writer.TryWrite(state); internal void GetOutstandingCount(out int inst, out int qs, out long @in, out int qu, out bool aw, out long toRead, out long toWrite, out BacklogStatus bs, out PhysicalConnection.ReadStatus rs, out PhysicalConnection.WriteStatus ws) @@ -584,9 +581,7 @@ internal void OnHeartbeat(bool ifConnectedOnly) internal void RemovePhysical(PhysicalConnection connection) { -#pragma warning disable 0420 Interlocked.CompareExchange(ref physical, null, connection); -#pragma warning restore 0420 } [Conditional("VERBOSE")] @@ -646,10 +641,10 @@ private WriteResult WriteMessageInsideLock(PhysicalConnection physical, Message { physical.SetWriting(); var messageIsSent = false; - if (message is IMultiMessage) + if (message is IMultiMessage multiMessage) { SelectDatabaseInsideWriteLock(physical, message); // need to switch database *before* the transaction - foreach (var subCommand in ((IMultiMessage)message).GetMessages(physical)) + foreach (var subCommand in multiMessage.GetMessages(physical)) { result = WriteMessageToServerInsideWriteLock(physical, subCommand); if (result != WriteResult.Success) @@ -722,9 +717,7 @@ internal WriteResult WriteMessageTakingWriteLockSync(PhysicalConnection physical if (result == WriteResult.Success) { -#pragma warning disable CS0618 result = physical.FlushSync(false, TimeoutMilliseconds); -#pragma warning restore CS0618 } physical.SetIdle(); @@ -783,8 +776,7 @@ private void CheckBacklogForTimeouts() // check the head of the backlog queue, c // Because peeking at the backlog, checking message and then dequeueing, is not thread-safe, we do have to use // a lock here, for mutual exclusion of backlog DEQUEUERS. Unfortunately. // But we reduce contention by only locking if we see something that looks timed out. - Message message; - while (_backlog.TryPeek(out message)) + while (_backlog.TryPeek(out Message message)) { if (message.IsInternalCall) break; // don't stomp these (not that they should have the async timeout flag, but...) if (!message.HasAsyncTimedOut(now, timeout, out var _)) break; // not a timeout - we can stop looking @@ -1114,9 +1106,7 @@ private void UnmarkActiveMessage(Message message) private State ChangeState(State newState) { -#pragma warning disable 0420 var oldState = (State)Interlocked.Exchange(ref state, (int)newState); -#pragma warning restore 0420 if (oldState != newState) { Multiplexer.Trace(ConnectionType + " state changed from " + oldState + " to " + newState); @@ -1126,9 +1116,7 @@ private State ChangeState(State newState) private bool ChangeState(State oldState, State newState) { -#pragma warning disable 0420 bool result = Interlocked.CompareExchange(ref state, (int)newState, (int)oldState) == (int)oldState; -#pragma warning restore 0420 if (result) { Multiplexer.Trace(ConnectionType + " state changed from " + oldState + " to " + newState); diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index ebb80e977..21e0a10ff 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -340,11 +340,11 @@ public void RecordConnectionFailed(ConnectionFailureType failureType, Exception exMessage.Append(" (").Append(sc.ShutdownKind); if (sc.SocketError != SocketError.Success) { - exMessage.Append("/").Append(sc.SocketError); + exMessage.Append('/').Append(sc.SocketError); } if (sc.BytesRead == 0) exMessage.Append(", 0-read"); if (sc.BytesSent == 0) exMessage.Append(", 0-sent"); - exMessage.Append(", last-recv: ").Append(sc.LastReceived).Append(")"); + exMessage.Append(", last-recv: ").Append(sc.LastReceived).Append(')'); } else if (pipe is IMeasuredDuplexPipe mdp) { @@ -365,8 +365,8 @@ void add(string lk, string sk, string v) { if (bridge != null) { - exMessage.Append(" on ").Append(Format.ToString(bridge.ServerEndPoint?.EndPoint)).Append("/").Append(connectionType) - .Append(", ").Append(_writeStatus).Append("/").Append(_readStatus) + exMessage.Append(" on ").Append(Format.ToString(bridge.ServerEndPoint?.EndPoint)).Append('/').Append(connectionType) + .Append(", ").Append(_writeStatus).Append('/').Append(_readStatus) .Append(", last: ").Append(bridge.LastCommand); data.Add(Tuple.Create("FailureType", failureType.ToString())); @@ -846,6 +846,7 @@ internal static int WriteRaw(Span span, long value, bool withLengthPrefix return WriteCrlf(span, offset); } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "DEBUG uses instance data")] private async ValueTask FlushAsync_Awaited(PhysicalConnection connection, ValueTask flush, bool throwOnFailure #if DEBUG , int startFlush, long flushBytes @@ -871,6 +872,7 @@ private async ValueTask FlushAsync_Awaited(PhysicalConnection conne CancellationTokenSource _reusableFlushSyncTokenSource; [Obsolete("this is an anti-pattern; work to reduce reliance on this is in progress")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0062:Make local function 'static'", Justification = "DEBUG uses instance data")] internal WriteResult FlushSync(bool throwOnFailure, int millisecondsTimeout) { var cts = _reusableFlushSyncTokenSource ??= new CancellationTokenSource(); @@ -1250,7 +1252,7 @@ internal long GetSocketBytes(out long readCount, out long writeCount) return VolatileSocket?.Available ?? -1; } - private RemoteCertificateValidationCallback GetAmbientIssuerCertificateCallback() + private static RemoteCertificateValidationCallback GetAmbientIssuerCertificateCallback() { try { @@ -1761,9 +1763,7 @@ private static RawResult ParseInlineProtocol(Arena arena, in RawResul if (!line.HasValue) return RawResult.Nil; // incomplete line int count = 0; -#pragma warning disable IDE0059 foreach (var _ in line.GetInlineTokenizer()) count++; -#pragma warning restore IDE0059 var block = arena.Allocate(count); var iter = block.GetEnumerator(); diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index e2f9e44ef..d138d4e37 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -255,12 +255,12 @@ internal byte[] GetBlob() internal bool GetBoolean() { if (Payload.Length != 1) throw new InvalidCastException(); - switch (Payload.First.Span[0]) + return Payload.First.Span[0] switch { - case (byte)'1': return true; - case (byte)'0': return false; - default: throw new InvalidCastException(); - } + (byte)'1' => true, + (byte)'0' => false, + _ => throw new InvalidCastException(), + }; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/StackExchange.Redis/RedisBase.cs b/src/StackExchange.Redis/RedisBase.cs index c2aad6300..9d42d05a3 100644 --- a/src/StackExchange.Redis/RedisBase.cs +++ b/src/StackExchange.Redis/RedisBase.cs @@ -61,7 +61,7 @@ internal virtual RedisFeatures GetFeatures(in RedisKey key, CommandFlags flags, return new RedisFeatures(version); } - protected void WhenAlwaysOrExists(When when) + protected static void WhenAlwaysOrExists(When when) { switch (when) { @@ -73,7 +73,7 @@ protected void WhenAlwaysOrExists(When when) } } - protected void WhenAlwaysOrExistsOrNotExists(When when) + protected static void WhenAlwaysOrExistsOrNotExists(When when) { switch (when) { @@ -86,7 +86,7 @@ protected void WhenAlwaysOrExistsOrNotExists(When when) } } - protected void WhenAlwaysOrNotExists(When when) + protected static void WhenAlwaysOrNotExists(When when) { switch (when) { diff --git a/src/StackExchange.Redis/RedisBatch.cs b/src/StackExchange.Redis/RedisBatch.cs index d364dfa18..6f4d70700 100644 --- a/src/StackExchange.Redis/RedisBatch.cs +++ b/src/StackExchange.Redis/RedisBatch.cs @@ -82,7 +82,7 @@ internal override Task ExecuteAsync(Message message, ResultProcessor pr } // store it - (pending ?? (pending = new List())).Add(message); + (pending ??= new List()).Add(message); return task; } @@ -91,7 +91,7 @@ internal override T ExecuteSync(Message message, ResultProcessor processor throw new NotSupportedException("ExecuteSync cannot be used inside a batch"); } - private void FailNoServer(List messages) + private static void FailNoServer(List messages) { if (messages == null) return; foreach(var msg in messages) diff --git a/src/StackExchange.Redis/RedisChannel.cs b/src/StackExchange.Redis/RedisChannel.cs index b830e6f27..85bcf1a49 100644 --- a/src/StackExchange.Redis/RedisChannel.cs +++ b/src/StackExchange.Redis/RedisChannel.cs @@ -38,18 +38,13 @@ private RedisChannel(byte[] value, bool isPatternBased) IsPatternBased = isPatternBased; } - private static bool DeterminePatternBased(byte[] value, PatternMode mode) + private static bool DeterminePatternBased(byte[] value, PatternMode mode) => mode switch { - switch (mode) - { - case PatternMode.Auto: - return value != null && Array.IndexOf(value, (byte)'*') >= 0; - case PatternMode.Literal: return false; - case PatternMode.Pattern: return true; - default: - throw new ArgumentOutOfRangeException(nameof(mode)); - } - } + PatternMode.Auto => value != null && Array.IndexOf(value, (byte)'*') >= 0, + PatternMode.Literal => false, + PatternMode.Pattern => true, + _ => throw new ArgumentOutOfRangeException(nameof(mode)), + }; /// /// Indicate whether two channel names are not equal diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 223a23313..be3b5d991 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -351,7 +351,7 @@ public Task> HashGetLeaseAsync(RedisKey key, RedisValue hashField, C public Task HashGetAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) { if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); - if (hashFields.Length == 0) return CompletedTask.FromResult(new RedisValue[0], asyncState); + if (hashFields.Length == 0) return CompletedTask.FromResult(Array.Empty(), asyncState); var msg = Message.Create(Database, flags, RedisCommand.HMGET, key, hashFields); return ExecuteAsync(msg, ResultProcessor.RedisValueArray); } @@ -746,7 +746,7 @@ public KeyMigrateCommandMessage(int db, RedisKey key, EndPoint toServer, int toD : base(db, flags, RedisCommand.MIGRATE, key) { if (toServer == null) throw new ArgumentNullException(nameof(toServer)); - if (!Format.TryGetHostPort(toServer, out string toHost, out int toPort)) throw new ArgumentException("toServer"); + if (!Format.TryGetHostPort(toServer, out string toHost, out int toPort)) throw new ArgumentException($"Couldn't get host and port from {toServer}", nameof(toServer)); this.toHost = toHost; this.toPort = toPort; if (toDatabase < 0) throw new ArgumentOutOfRangeException(nameof(toDatabase)); @@ -2711,7 +2711,7 @@ private ITransaction GetLockReleaseTransaction(RedisKey key, RedisValue value) return tran; } - private RedisValue GetLexRange(RedisValue value, Exclude exclude, bool isStart) + private static RedisValue GetLexRange(RedisValue value, Exclude exclude, bool isStart) { if (value.IsNull) { @@ -2831,7 +2831,7 @@ private Message GetMultiStreamReadMessage(StreamPosition[] streamPositions, int? return Message.Create(Database, flags, RedisCommand.XREAD, values); } - private RedisValue GetRange(double value, Exclude exclude, bool isStart) + private static RedisValue GetRange(double value, Exclude exclude, bool isStart) { if (isStart) { @@ -2853,13 +2853,13 @@ private Message GetRestoreMessage(RedisKey key, byte[] value, TimeSpan? expiry, private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double score, When when, CommandFlags flags) { WhenAlwaysOrExistsOrNotExists(when); - switch (when) + return when switch { - case When.Always: return Message.Create(Database, flags, RedisCommand.ZADD, key, score, member); - case When.NotExists: return Message.Create(Database, flags, RedisCommand.ZADD, key, RedisLiterals.NX, score, member); - case When.Exists: return Message.Create(Database, flags, RedisCommand.ZADD, key, RedisLiterals.XX, score, member); - default: throw new ArgumentOutOfRangeException(nameof(when)); - } + When.Always => Message.Create(Database, flags, RedisCommand.ZADD, key, score, member), + When.NotExists => Message.Create(Database, flags, RedisCommand.ZADD, key, RedisLiterals.NX, score, member), + When.Exists => Message.Create(Database, flags, RedisCommand.ZADD, key, RedisLiterals.XX, score, member), + _ => throw new ArgumentOutOfRangeException(nameof(when)), + }; } private Message GetSortedSetAddMessage(RedisKey key, SortedSetEntry[] values, When when, CommandFlags flags) @@ -2977,13 +2977,12 @@ private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long private Message GetSortedSetCombineAndStoreCommandMessage(SetOperation operation, RedisKey destination, RedisKey[] keys, double[] weights, Aggregate aggregate, CommandFlags flags) { - RedisCommand command; - switch (operation) + var command = operation switch { - case SetOperation.Intersect: command = RedisCommand.ZINTERSTORE; break; - case SetOperation.Union: command = RedisCommand.ZUNIONSTORE; break; - default: throw new ArgumentOutOfRangeException(nameof(operation)); - } + SetOperation.Intersect => RedisCommand.ZINTERSTORE, + SetOperation.Union => RedisCommand.ZUNIONSTORE, + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; if (keys == null) throw new ArgumentNullException(nameof(keys)); List values = null; @@ -3484,13 +3483,13 @@ private Message GetStringSetMessage(RedisKey key, RedisValue value, TimeSpan? ex } } - switch (when) + return when switch { - case When.Always: return Message.Create(Database, flags, RedisCommand.PSETEX, key, milliseconds, value); - case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX); - case When.NotExists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.NX); - } - throw new NotSupportedException(); + When.Always => Message.Create(Database, flags, RedisCommand.PSETEX, key, milliseconds, value), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX), + When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.NX), + _ => throw new NotSupportedException(), + }; } private Message IncrMessage(RedisKey key, long value, CommandFlags flags) @@ -3511,16 +3510,13 @@ private Message IncrMessage(RedisKey key, long value, CommandFlags flags) } } - private RedisCommand SetOperationCommand(SetOperation operation, bool store) + private static RedisCommand SetOperationCommand(SetOperation operation, bool store) => operation switch { - switch (operation) - { - case SetOperation.Difference: return store ? RedisCommand.SDIFFSTORE : RedisCommand.SDIFF; - case SetOperation.Intersect: return store ? RedisCommand.SINTERSTORE : RedisCommand.SINTER; - case SetOperation.Union: return store ? RedisCommand.SUNIONSTORE : RedisCommand.SUNION; - default: throw new ArgumentOutOfRangeException(nameof(operation)); - } - } + SetOperation.Difference => store ? RedisCommand.SDIFFSTORE : RedisCommand.SDIFF, + SetOperation.Intersect => store ? RedisCommand.SINTERSTORE : RedisCommand.SINTER, + SetOperation.Union => store ? RedisCommand.SUNIONSTORE : RedisCommand.SUNION, + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; private CursorEnumerable TryScan(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags, RedisCommand command, ResultProcessor.ScanResult> processor, out ServerEndPoint server) { diff --git a/src/StackExchange.Redis/RedisKey.cs b/src/StackExchange.Redis/RedisKey.cs index cd409caa9..725871c72 100644 --- a/src/StackExchange.Redis/RedisKey.cs +++ b/src/StackExchange.Redis/RedisKey.cs @@ -137,8 +137,8 @@ private static bool CompositeEquals(byte[] keyPrefix0, object keyValue0, byte[] if (keyValue0 == keyValue1) return true; // ref equal if (keyValue0 == null || keyValue1 == null) return false; // null vs non-null - if (keyValue0 is string && keyValue1 is string) return ((string)keyValue0) == ((string)keyValue1); - if (keyValue0 is byte[] && keyValue1 is byte[]) return RedisValue.Equals((byte[])keyValue0, (byte[])keyValue1); + if (keyValue0 is string keyString1 && keyValue1 is string keyString2) return keyString1 == keyString2; + if (keyValue0 is byte[] keyBytes1 && keyValue1 is byte[] keyBytes2) return RedisValue.Equals(keyBytes1, keyBytes2); } return RedisValue.Equals(ConcatenateBytes(keyPrefix0, keyValue0, null), ConcatenateBytes(keyPrefix1, keyValue1, null)); @@ -162,7 +162,7 @@ public override int GetHashCode() internal RedisValue AsRedisValue() { - if (KeyPrefix == null && KeyValue is string) return (string)KeyValue; + if (KeyPrefix == null && KeyValue is string keyString) return keyString; return (byte[])this; } @@ -207,7 +207,7 @@ public static implicit operator string(RedisKey key) { if (key.KeyValue == null) return null; - if (key.KeyValue is string) return (string)key.KeyValue; + if (key.KeyValue is string keyString) return keyString; arr = (byte[])key.KeyValue; } @@ -231,7 +231,7 @@ public static implicit operator string(RedisKey key) /// /// The first to add. /// The second to add. - [Obsolete] + [Obsolete("Prefer WithPrefix")] public static RedisKey operator +(RedisKey x, RedisKey y) { return new RedisKey(ConcatenateBytes(x.KeyPrefix, x.KeyValue, y.KeyPrefix), y.KeyValue); @@ -260,8 +260,8 @@ internal static byte[] ConcatenateBytes(byte[] a, object b, byte[] c) } int aLen = a?.Length ?? 0, - bLen = b == null ? 0 : (b is string - ? Encoding.UTF8.GetByteCount((string)b) + bLen = b == null ? 0 : (b is string bString + ? Encoding.UTF8.GetByteCount(bString) : ((byte[])b).Length), cLen = c?.Length ?? 0; diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index d0806bd9c..bb38b73f4 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -141,16 +141,13 @@ public static readonly RedisValue timeout = "timeout", yes = "yes"; - internal static RedisValue Get(Bitwise operation) + internal static RedisValue Get(Bitwise operation) => operation switch { - switch (operation) - { - case Bitwise.And: return AND; - case Bitwise.Or: return OR; - case Bitwise.Xor: return XOR; - case Bitwise.Not: return NOT; - default: throw new ArgumentOutOfRangeException(nameof(operation)); - } - } + Bitwise.And => AND, + Bitwise.Or => OR, + Bitwise.Xor => XOR, + Bitwise.Not => NOT, + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; } } diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 271562106..649e07923 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -426,22 +426,13 @@ public Task ScriptLoadAsync(LuaScript script, CommandFlags flag public void Shutdown(ShutdownMode shutdownMode = ShutdownMode.Default, CommandFlags flags = CommandFlags.None) { - Message msg; - switch (shutdownMode) + Message msg = shutdownMode switch { - case ShutdownMode.Default: - msg = Message.Create(-1, flags, RedisCommand.SHUTDOWN); - break; - case ShutdownMode.Always: - msg = Message.Create(-1, flags, RedisCommand.SHUTDOWN, RedisLiterals.SAVE); - break; - case ShutdownMode.Never: - msg = Message.Create(-1, flags, RedisCommand.SHUTDOWN, RedisLiterals.NOSAVE); - break; - default: - throw new ArgumentOutOfRangeException(nameof(shutdownMode)); - } - + ShutdownMode.Default => Message.Create(-1, flags, RedisCommand.SHUTDOWN), + ShutdownMode.Always => Message.Create(-1, flags, RedisCommand.SHUTDOWN, RedisLiterals.SAVE), + ShutdownMode.Never => Message.Create(-1, flags, RedisCommand.SHUTDOWN, RedisLiterals.NOSAVE), + _ => throw new ArgumentOutOfRangeException(nameof(shutdownMode)), + }; try { ExecuteSync(msg, ResultProcessor.DemandOK); @@ -665,7 +656,7 @@ public Task ReplicaOfAsync(EndPoint master, CommandFlags flags = CommandFlags.No return ExecuteAsync(msg, ResultProcessor.DemandOK); } - private void FixFlags(Message message, ServerEndPoint server) + private static void FixFlags(Message message, ServerEndPoint server) { // since the server is specified explicitly, we don't want defaults // to make the "non-preferred-endpoint" counters look artificially @@ -681,31 +672,25 @@ private void FixFlags(Message message, ServerEndPoint server) } } - private Message GetSaveMessage(SaveType type, CommandFlags flags = CommandFlags.None) + private static Message GetSaveMessage(SaveType type, CommandFlags flags = CommandFlags.None) => type switch { - switch (type) - { - case SaveType.BackgroundRewriteAppendOnlyFile: return Message.Create(-1, flags, RedisCommand.BGREWRITEAOF); - case SaveType.BackgroundSave: return Message.Create(-1, flags, RedisCommand.BGSAVE); + SaveType.BackgroundRewriteAppendOnlyFile => Message.Create(-1, flags, RedisCommand.BGREWRITEAOF), + SaveType.BackgroundSave => Message.Create(-1, flags, RedisCommand.BGSAVE), #pragma warning disable 0618 - case SaveType.ForegroundSave: return Message.Create(-1, flags, RedisCommand.SAVE); + SaveType.ForegroundSave => Message.Create(-1, flags, RedisCommand.SAVE), #pragma warning restore 0618 - default: throw new ArgumentOutOfRangeException(nameof(type)); - } - } + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; - private ResultProcessor GetSaveResultProcessor(SaveType type) + private static ResultProcessor GetSaveResultProcessor(SaveType type) => type switch { - switch (type) - { - case SaveType.BackgroundRewriteAppendOnlyFile: return ResultProcessor.DemandOK; - case SaveType.BackgroundSave: return ResultProcessor.BackgroundSaveStarted; + SaveType.BackgroundRewriteAppendOnlyFile => ResultProcessor.DemandOK, + SaveType.BackgroundSave => ResultProcessor.BackgroundSaveStarted, #pragma warning disable 0618 - case SaveType.ForegroundSave: return ResultProcessor.DemandOK; + SaveType.ForegroundSave => ResultProcessor.DemandOK, #pragma warning restore 0618 - default: throw new ArgumentOutOfRangeException(nameof(type)); - } - } + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; private static class ScriptHash { diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index 40684f105..8884dc263 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -98,10 +98,9 @@ internal void OnMessage(in RedisChannel subscription, in RedisChannel channel, i { ICompletable completable = null; ChannelMessageQueue queues = null; - Subscription sub; lock (subscriptions) { - if (subscriptions.TryGetValue(subscription, out sub)) + if (subscriptions.TryGetValue(subscription, out Subscription sub)) { completable = sub.ForInvoke(channel, payload, out queues); } diff --git a/src/StackExchange.Redis/RedisTransaction.cs b/src/StackExchange.Redis/RedisTransaction.cs index 08dd03fc2..8bbee7d52 100644 --- a/src/StackExchange.Redis/RedisTransaction.cs +++ b/src/StackExchange.Redis/RedisTransaction.cs @@ -87,7 +87,7 @@ internal override Task ExecuteAsync(Message message, ResultProcessor pr // (there is no task for the inner command) lock (SyncLock) { - (_pending ?? (_pending = new List())).Add(queued); + (_pending ??= new List()).Add(queued); switch (message.Command) { @@ -225,13 +225,13 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) for (int i = 0; i < conditions.Length; i++) { int newSlot = conditions[i].Condition.GetHashSlot(serverSelectionStrategy); - slot = serverSelectionStrategy.CombineSlot(slot, newSlot); + slot = ServerSelectionStrategy.CombineSlot(slot, newSlot); if (slot == ServerSelectionStrategy.MultipleSlots) return slot; } for (int i = 0; i < InnerOperations.Length; i++) { int newSlot = InnerOperations[i].Wrapped.GetHashSlot(serverSelectionStrategy); - slot = serverSelectionStrategy.CombineSlot(slot, newSlot); + slot = ServerSelectionStrategy.CombineSlot(slot, newSlot); if (slot == ServerSelectionStrategy.MultipleSlots) return slot; } return slot; @@ -456,7 +456,6 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog($"got {result} for {message.CommandAndKey}"); if (message is TransactionMessage tran) { - var bridge = connection.BridgeCouldBeNull; var wrapped = tran.InnerOperations; switch (result.Type) { diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index fa339b951..9ad8c1e14 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -88,7 +88,7 @@ public object Box() public static RedisValue Unbox(object value) { var val = TryParse(value, out var valid); - if (!valid) throw new ArgumentException(nameof(value)); + if (!valid) throw new ArgumentException("Could not parse value", nameof(value)); return val; } @@ -340,16 +340,13 @@ internal StorageType Type /// /// Get the size of this value in bytes /// - public long Length() + public long Length() => Type switch { - switch (Type) - { - case StorageType.Null: return 0; - case StorageType.Raw: return _memory.Length; - case StorageType.String: return Encoding.UTF8.GetByteCount((string)_objectOrSentinel); - default: throw new InvalidOperationException("Unable to compute length of type: " + Type); - } - } + StorageType.Null => 0, + StorageType.Raw => _memory.Length, + StorageType.String => Encoding.UTF8.GetByteCount((string)_objectOrSentinel), + _ => throw new InvalidOperationException("Unable to compute length of type: " + Type), + }; /// /// Compare against a RedisValue for relative order @@ -579,15 +576,12 @@ public static implicit operator RedisValue(byte[] value) /// Converts a to a . /// /// The to convert. - public static explicit operator bool(RedisValue value) + public static explicit operator bool(RedisValue value) => (long)value switch { - switch ((long)value) - { - case 0: return false; - case 1: return true; - default: throw new InvalidCastException(); - } - } + 0 => false, + 1 => true, + _ => throw new InvalidCastException(), + }; /// /// Converts a to a . @@ -603,16 +597,13 @@ public static explicit operator int(RedisValue value) public static explicit operator long(RedisValue value) { value = value.Simplify(); - switch (value.Type) + return value.Type switch { - case StorageType.Null: - return 0; // in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") - case StorageType.Int64: - return value.OverlappedValueInt64; - case StorageType.UInt64: - return checked((long)value.OverlappedValueUInt64); // this will throw since unsigned is always 64-bit - } - throw new InvalidCastException($"Unable to cast from {value.Type} to long: '{value}'"); + StorageType.Null => 0,// in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") + StorageType.Int64 => value.OverlappedValueInt64, + StorageType.UInt64 => checked((long)value.OverlappedValueUInt64),// this will throw since unsigned is always 64-bit + _ => throw new InvalidCastException($"Unable to cast from {value.Type} to long: '{value}'"), + }; } /// @@ -623,16 +614,13 @@ public static explicit operator long(RedisValue value) public static explicit operator uint(RedisValue value) { value = value.Simplify(); - switch (value.Type) + return value.Type switch { - case StorageType.Null: - return 0; // in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") - case StorageType.Int64: - return checked((uint)value.OverlappedValueInt64); - case StorageType.UInt64: - return checked((uint)value.OverlappedValueUInt64); - } - throw new InvalidCastException($"Unable to cast from {value.Type} to uint: '{value}'"); + StorageType.Null => 0,// in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") + StorageType.Int64 => checked((uint)value.OverlappedValueInt64), + StorageType.UInt64 => checked((uint)value.OverlappedValueUInt64), + _ => throw new InvalidCastException($"Unable to cast from {value.Type} to uint: '{value}'"), + }; } /// @@ -643,16 +631,13 @@ public static explicit operator uint(RedisValue value) public static explicit operator ulong(RedisValue value) { value = value.Simplify(); - switch (value.Type) + return value.Type switch { - case StorageType.Null: - return 0; // in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") - case StorageType.Int64: - return checked((ulong)value.OverlappedValueInt64); // throw if negative - case StorageType.UInt64: - return value.OverlappedValueUInt64; - } - throw new InvalidCastException($"Unable to cast from {value.Type} to ulong: '{value}'"); + StorageType.Null => 0,// in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") + StorageType.Int64 => checked((ulong)value.OverlappedValueInt64),// throw if negative + StorageType.UInt64 => value.OverlappedValueUInt64, + _ => throw new InvalidCastException($"Unable to cast from {value.Type} to ulong: '{value}'"), + }; } /// @@ -662,18 +647,14 @@ public static explicit operator ulong(RedisValue value) public static explicit operator double(RedisValue value) { value = value.Simplify(); - switch (value.Type) + return value.Type switch { - case StorageType.Null: - return 0; // in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") - case StorageType.Int64: - return value.OverlappedValueInt64; - case StorageType.UInt64: - return value.OverlappedValueUInt64; - case StorageType.Double: - return value.OverlappedValueDouble; - } - throw new InvalidCastException($"Unable to cast from {value.Type} to double: '{value}'"); + StorageType.Null => 0,// in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") + StorageType.Int64 => value.OverlappedValueInt64, + StorageType.UInt64 => value.OverlappedValueUInt64, + StorageType.Double => value.OverlappedValueDouble, + _ => throw new InvalidCastException($"Unable to cast from {value.Type} to double: '{value}'"), + }; } /// @@ -683,18 +664,14 @@ public static explicit operator double(RedisValue value) public static explicit operator decimal(RedisValue value) { value = value.Simplify(); - switch (value.Type) + return value.Type switch { - case StorageType.Null: - return 0; // in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") - case StorageType.Int64: - return value.OverlappedValueInt64; - case StorageType.UInt64: - return value.OverlappedValueUInt64; - case StorageType.Double: - return (decimal)value.OverlappedValueDouble; - } - throw new InvalidCastException($"Unable to cast from {value.Type} to decimal: '{value}'"); + StorageType.Null => 0,// in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") + StorageType.Int64 => value.OverlappedValueInt64, + StorageType.UInt64 => value.OverlappedValueUInt64, + StorageType.Double => (decimal)value.OverlappedValueDouble, + _ => throw new InvalidCastException($"Unable to cast from {value.Type} to decimal: '{value}'"), + }; } /// @@ -704,18 +681,14 @@ public static explicit operator decimal(RedisValue value) public static explicit operator float(RedisValue value) { value = value.Simplify(); - switch (value.Type) + return value.Type switch { - case StorageType.Null: - return 0; // in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") - case StorageType.Int64: - return value.OverlappedValueInt64; - case StorageType.UInt64: - return value.OverlappedValueUInt64; - case StorageType.Double: - return (float)value.OverlappedValueDouble; - } - throw new InvalidCastException($"Unable to cast from {value.Type} to double: '{value}'"); + StorageType.Null => 0,// in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") + StorageType.Int64 => value.OverlappedValueInt64, + StorageType.UInt64 => value.OverlappedValueUInt64, + StorageType.Double => (float)value.OverlappedValueDouble, + _ => throw new InvalidCastException($"Unable to cast from {value.Type} to double: '{value}'"), + }; } private static bool TryParseDouble(ReadOnlySpan blob, out double value) @@ -909,27 +882,26 @@ object IConvertible.ToType(Type conversionType, IFormatProvider provider) if (conversionType == typeof(byte[])) return (byte[])this; if (conversionType == typeof(ReadOnlyMemory)) return (ReadOnlyMemory)this; if (conversionType == typeof(RedisValue)) return this; - switch (System.Type.GetTypeCode(conversionType)) + return System.Type.GetTypeCode(conversionType) switch { - case TypeCode.Boolean: return (bool)this; - case TypeCode.Byte: return checked((byte)(uint)this); - case TypeCode.Char: return checked((char)(uint)this); - case TypeCode.DateTime: return DateTime.Parse((string)this, provider); - case TypeCode.Decimal: return (decimal)this; - case TypeCode.Double: return (double)this; - case TypeCode.Int16: return (short)this; - case TypeCode.Int32: return (int)this; - case TypeCode.Int64: return (long)this; - case TypeCode.SByte: return (sbyte)this; - case TypeCode.Single: return (float)this; - case TypeCode.String: return (string)this; - case TypeCode.UInt16: return checked((ushort)(uint)this); - case TypeCode.UInt32: return (uint)this; - case TypeCode.UInt64: return (ulong)this; - case TypeCode.Object: return this; - default: - throw new NotSupportedException(); - } + TypeCode.Boolean => (bool)this, + TypeCode.Byte => checked((byte)(uint)this), + TypeCode.Char => checked((char)(uint)this), + TypeCode.DateTime => DateTime.Parse((string)this, provider), + TypeCode.Decimal => (decimal)this, + TypeCode.Double => (double)this, + TypeCode.Int16 => (short)this, + TypeCode.Int32 => (int)this, + TypeCode.Int64 => (long)this, + TypeCode.SByte => (sbyte)this, + TypeCode.Single => (float)this, + TypeCode.String => (string)this, + TypeCode.UInt16 => checked((ushort)(uint)this), + TypeCode.UInt32 => (uint)this, + TypeCode.UInt64 => (ulong)this, + TypeCode.Object => this, + _ => throw new NotSupportedException(), + }; } ushort IConvertible.ToUInt16(IFormatProvider provider) => checked((ushort)(uint)this); diff --git a/src/StackExchange.Redis/ResultBox.cs b/src/StackExchange.Redis/ResultBox.cs index 4fe34f114..53650ea89 100644 --- a/src/StackExchange.Redis/ResultBox.cs +++ b/src/StackExchange.Redis/ResultBox.cs @@ -116,6 +116,8 @@ void IResultBox.ActivateContinuations() else ActivateContinuationsImpl(); } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Intentional observation")] private void ActivateContinuationsImpl() { var val = _value; diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 6f3c9b0f5..dd25cfc8d 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -147,6 +147,7 @@ public static readonly TimeSpanProcessor public static readonly HashEntryArrayProcessor HashEntryArray = new HashEntryArrayProcessor(); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Conditionally run on instance")] public void ConnectionFail(Message message, ConnectionFailureType fail, Exception innerException, string annotation) { PhysicalConnection.IdentifyFailureType(innerException, ref fail); @@ -160,17 +161,17 @@ public void ConnectionFail(Message message, ConnectionFailureType fail, Exceptio SetException(message, ex); } - public void ConnectionFail(Message message, ConnectionFailureType fail, string errorMessage) + public static void ConnectionFail(Message message, ConnectionFailureType fail, string errorMessage) { SetException(message, new RedisConnectionException(fail, errorMessage)); } - public void ServerFail(Message message, string errorMessage) + public static void ServerFail(Message message, string errorMessage) { SetException(message, new RedisServerException(errorMessage)); } - public void SetException(Message message, Exception ex) + public static void SetException(Message message, Exception ex) { var box = message?.ResultBox; box?.SetException(ex); @@ -512,7 +513,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal sealed class SortedSetEntryProcessor : ResultProcessor { - public bool TryParse(in RawResult result, out SortedSetEntry? entry) + public static bool TryParse(in RawResult result, out SortedSetEntry? entry) { switch (result.Type) { @@ -1315,7 +1316,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { case ResultType.MultiBulk: var typed = result.ToArray( - (in RawResult item, in GeoRadiusOptions options) => Parse(item, options), this.options); + (in RawResult item, in GeoRadiusOptions radiusOptions) => Parse(item, radiusOptions), options); SetResult(message, typed); return true; } @@ -1968,7 +1969,7 @@ internal abstract class StreamProcessorBase : ResultProcessor { // For command response formats see https://redis.io/topics/streams-intro. - protected StreamEntry ParseRedisStreamEntry(in RawResult item) + protected static StreamEntry ParseRedisStreamEntry(in RawResult item) { if (item.IsNull || item.Type != ResultType.MultiBulk) { @@ -1990,10 +1991,10 @@ protected StreamEntry[] ParseRedisStreamEntries(in RawResult result) } return result.GetItems().ToArray( - (in RawResult item, in StreamProcessorBase obj) => obj.ParseRedisStreamEntry(item), this); + (in RawResult item, in StreamProcessorBase obj) => ParseRedisStreamEntry(item), this); } - protected NameValueEntry[] ParseStreamEntryValues(in RawResult result) + protected static NameValueEntry[] ParseStreamEntryValues(in RawResult result) { // The XRANGE, XREVRANGE, XREAD commands return stream entries // in the following format. The name/value pairs are interleaved @@ -2098,6 +2099,7 @@ public override bool SetResult(PhysicalConnection connection, Message message, i return final; } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0071:Simplify interpolation", Justification = "Allocations (string.Concat vs. string.Format)")] protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { bool happy; @@ -2300,7 +2302,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal abstract class ResultProcessor : ResultProcessor { - protected void SetResult(Message message, T value) + protected static void SetResult(Message message, T value) { if (message == null) return; var box = message.ResultBox as IResultBox; diff --git a/src/StackExchange.Redis/ScriptParameterMapper.cs b/src/StackExchange.Redis/ScriptParameterMapper.cs index e6a556943..4bc1d44dd 100644 --- a/src/StackExchange.Redis/ScriptParameterMapper.cs +++ b/src/StackExchange.Redis/ScriptParameterMapper.cs @@ -75,7 +75,7 @@ private static string MakeOrdinalScriptWithoutKeys(string rawScript, string[] ar { ret.Append("ARGV["); ret.Append(argIx + 1); - ret.Append("]"); + ret.Append(']'); } else { @@ -175,7 +175,7 @@ public static bool IsValidParameterHash(Type t, LuaScript script, out string mis return false; } - var memberType = member is FieldInfo ? ((FieldInfo)member).FieldType : ((PropertyInfo)member).PropertyType; + var memberType = member is FieldInfo memberFieldInfo ? memberFieldInfo.FieldType : ((PropertyInfo)member).PropertyType; if (!ConvertableTypes.Contains(memberType)) { missingMember = null; @@ -209,18 +209,12 @@ public static bool IsValidParameterHash(Type t, LuaScript script, out string mis { if (!IsValidParameterHash(t, script, out _, out _)) throw new Exception("Shouldn't be possible"); - Expression GetMember(Expression root, MemberInfo member) + static Expression GetMember(Expression root, MemberInfo member) => member.MemberType switch { - switch (member.MemberType) - { - case MemberTypes.Property: - return Expression.Property(root, (PropertyInfo)member); - case MemberTypes.Field: - return Expression.Field(root, (FieldInfo)member); - default: - throw new ArgumentException(nameof(member)); - } - } + MemberTypes.Property => Expression.Property(root, (PropertyInfo)member), + MemberTypes.Field => Expression.Field(root, (FieldInfo)member), + _ => throw new ArgumentException($"Member type '{member.MemberType}' isn't recognized", nameof(member)), + }; var keys = new List(); var args = new List(); @@ -229,7 +223,7 @@ Expression GetMember(Expression root, MemberInfo member) var argName = script.Arguments[i]; var member = t.GetMember(argName).SingleOrDefault(m => m is PropertyInfo || m is FieldInfo); - var memberType = member is FieldInfo ? ((FieldInfo)member).FieldType : ((PropertyInfo)member).PropertyType; + var memberType = member is FieldInfo memberFieldInfo ? memberFieldInfo.FieldType : ((PropertyInfo)member).PropertyType; if (memberType == typeof(RedisKey)) { diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 58bcb5c3e..8a080f0b3 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -201,14 +201,12 @@ public void Dispose() public PhysicalBridge GetBridge(ConnectionType type, bool create = true, LogProxy log = null) { if (isDisposed) return null; - switch (type) + return type switch { - case ConnectionType.Interactive: - return interactive ?? (create ? interactive = CreateBridge(ConnectionType.Interactive, log) : null); - case ConnectionType.Subscription: - return subscription ?? (create ? subscription = CreateBridge(ConnectionType.Subscription, log) : null); - } - return null; + ConnectionType.Interactive => interactive ?? (create ? interactive = CreateBridge(ConnectionType.Interactive, log) : null), + ConnectionType.Subscription => subscription ?? (create ? subscription = CreateBridge(ConnectionType.Subscription, log) : null), + _ => null, + }; } public PhysicalBridge GetBridge(RedisCommand command, bool create = true) @@ -242,6 +240,7 @@ public void SetClusterConfiguration(ClusterConfiguration configuration) } } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0071:Simplify interpolation", Justification = "Allocations (string.Concat vs. string.Format)")] public void UpdateNodeRelations(ClusterConfiguration configuration) { var thisNode = configuration.Nodes.FirstOrDefault(x => x.EndPoint.Equals(EndPoint)); @@ -258,7 +257,7 @@ public void UpdateNodeRelations(ClusterConfiguration configuration) } else if (node.ParentNodeId == thisNode.NodeId) { - (replicas ?? (replicas = new List())).Add(Multiplexer.GetServerEndPoint(node.EndPoint)); + (replicas ??= new List()).Add(Multiplexer.GetServerEndPoint(node.EndPoint)); } } Master = master; @@ -648,7 +647,7 @@ private void ResetExponentiallyReplicationCheck() { if (ConfigCheckSeconds < Multiplexer.RawConfig.ConfigCheckSeconds) { - r = r ?? new Random(); + r ??= new Random(); var newExponentialConfigCheck = ConfigCheckSeconds * 2; var jitter = r.Next(ConfigCheckSeconds + 1, newExponentialConfigCheck); ConfigCheckSeconds = Math.Min(jitter, Multiplexer.RawConfig.ConfigCheckSeconds); @@ -724,9 +723,7 @@ internal void WriteDirectFireAndForgetSync(Message message, ResultProcessor RedisClusterSlotCount; + internal static int TotalSlots => RedisClusterSlotCount; /// /// Computes the hash-slot that would be used by the given key @@ -187,7 +185,7 @@ public bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isM } } - internal int CombineSlot(int oldSlot, int newSlot) + internal static int CombineSlot(int oldSlot, int newSlot) { if (oldSlot == MultipleSlots || newSlot == NoSlot) return oldSlot; if (oldSlot == NoSlot) return newSlot; @@ -234,7 +232,7 @@ private ServerEndPoint Any(RedisCommand command, CommandFlags flags) return multiplexer.AnyConnected(ServerType, (uint)Interlocked.Increment(ref anyStartOffset), command, flags); } - private ServerEndPoint FindMaster(ServerEndPoint endpoint, RedisCommand command) + private static ServerEndPoint FindMaster(ServerEndPoint endpoint, RedisCommand command) { int max = 5; do @@ -246,7 +244,7 @@ private ServerEndPoint FindMaster(ServerEndPoint endpoint, RedisCommand command) return null; } - private ServerEndPoint FindReplica(ServerEndPoint endpoint, RedisCommand command, bool allowDisconnected = false) + private static ServerEndPoint FindReplica(ServerEndPoint endpoint, RedisCommand command, bool allowDisconnected = false) { if (endpoint.IsReplica && endpoint.IsSelectable(command, allowDisconnected)) return endpoint; diff --git a/src/StackExchange.Redis/StreamPosition.cs b/src/StackExchange.Redis/StreamPosition.cs index 53b954226..979735d35 100644 --- a/src/StackExchange.Redis/StreamPosition.cs +++ b/src/StackExchange.Redis/StreamPosition.cs @@ -42,14 +42,14 @@ internal static RedisValue Resolve(RedisValue value, RedisCommand command) { if (value == NewMessages) { - switch (command) + return command switch { - case RedisCommand.XREAD: throw new InvalidOperationException("StreamPosition.NewMessages cannot be used with StreamRead."); - case RedisCommand.XREADGROUP: return StreamConstants.UndeliveredMessages; - case RedisCommand.XGROUP: return StreamConstants.NewMessages; - default: // new is only valid for the above - throw new ArgumentException($"Unsupported command in StreamPosition.Resolve: {command}.", nameof(command)); - } + RedisCommand.XREAD => throw new InvalidOperationException("StreamPosition.NewMessages cannot be used with StreamRead."), + RedisCommand.XREADGROUP => StreamConstants.UndeliveredMessages, + RedisCommand.XGROUP => StreamConstants.NewMessages, + // new is only valid for the above + _ => throw new ArgumentException($"Unsupported command in StreamPosition.Resolve: {command}.", nameof(command)), + }; } else if (value == StreamPosition.Beginning) { switch(command) diff --git a/tests/BasicTest/Program.cs b/tests/BasicTest/Program.cs index 342d33d2d..9f22880bd 100644 --- a/tests/BasicTest/Program.cs +++ b/tests/BasicTest/Program.cs @@ -212,7 +212,6 @@ public async Task HashGetAllAsync_FAF() } } } -#pragma warning disable CS1591 [Config(typeof(SlowConfig))] public class Issue898 : IDisposable diff --git a/tests/NRediSearch.Test/ClientTests/AggregationBuilderTests.cs b/tests/NRediSearch.Test/ClientTests/AggregationBuilderTests.cs index b9ad926f7..8d2419a35 100644 --- a/tests/NRediSearch.Test/ClientTests/AggregationBuilderTests.cs +++ b/tests/NRediSearch.Test/ClientTests/AggregationBuilderTests.cs @@ -168,7 +168,7 @@ public async Task TestCursor() } catch (RedisException) { } - AggregationBuilder r2 = new AggregationBuilder() + _ = new AggregationBuilder() .GroupBy("@name", Reducers.Sum("@count").As("sum")) .SortBy(10, SortedField.Descending("@sum")) .Cursor(1, 1000); diff --git a/tests/NRediSearch.Test/ClientTests/ClientTest.cs b/tests/NRediSearch.Test/ClientTests/ClientTest.cs index 60134a473..629874ee9 100644 --- a/tests/NRediSearch.Test/ClientTests/ClientTest.cs +++ b/tests/NRediSearch.Test/ClientTests/ClientTest.cs @@ -371,9 +371,11 @@ public void TestAlterAdd() Assert.True(cl.AlterIndex(new TagField("tags", ","), new TextField("name", 0.5))); for (int i = 0; i < 100; i++) { - var fields2 = new Dictionary(); - fields2.Add("name", $"name{i}"); - fields2.Add("tags", $"tagA,tagB,tag{i}"); + var fields2 = new Dictionary + { + { "name", $"name{i}" }, + { "tags", $"tagA,tagB,tag{i}" } + }; Assert.True(cl.UpdateDocument($"doc{i}", fields2, 1.0)); } SearchResult res2 = cl.Search(new Query("@tags:{tagA}")); @@ -739,22 +741,26 @@ public void TestGetTagField() Output.WriteLine("Initial search: " + search.TotalResults); Assert.Equal(0, search.TotalResults); - var fields1 = new Dictionary(); - fields1.Add("title", "hello world"); - fields1.Add("category", "red"); - Assert.True(cl.AddDocument("foo", fields1)); - var fields2 = new Dictionary(); - fields2.Add("title", "hello world"); - fields2.Add("category", "blue"); - Assert.True(cl.AddDocument("bar", fields2)); - var fields3 = new Dictionary(); - fields3.Add("title", "hello world"); - fields3.Add("category", "green,yellow"); - Assert.True(cl.AddDocument("baz", fields3)); - var fields4 = new Dictionary(); - fields4.Add("title", "hello world"); - fields4.Add("category", "orange;purple"); - Assert.True(cl.AddDocument("qux", fields4)); + Assert.True(cl.AddDocument("foo", new Dictionary + { + { "title", "hello world" }, + { "category", "red" } + })); + Assert.True(cl.AddDocument("bar", new Dictionary + { + { "title", "hello world" }, + { "category", "blue" } + })); + Assert.True(cl.AddDocument("baz", new Dictionary + { + { "title", "hello world" }, + { "category", "green,yellow" } + })); + Assert.True(cl.AddDocument("qux", new Dictionary + { + { "title", "hello world" }, + { "category", "orange;purple" } + })); Assert.Equal(1, cl.Search(new Query("@category:{red}")).TotalResults); Assert.Equal(1, cl.Search(new Query("@category:{blue}")).TotalResults); @@ -781,22 +787,26 @@ public void TestGetTagFieldWithNonDefaultSeparator() .AddTagField("category", ";"); Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - var fields1 = new Dictionary(); - fields1.Add("title", "hello world"); - fields1.Add("category", "red"); - Assert.True(cl.AddDocument("foo", fields1)); - var fields2 = new Dictionary(); - fields2.Add("title", "hello world"); - fields2.Add("category", "blue"); - Assert.True(cl.AddDocument("bar", fields2)); - var fields3 = new Dictionary(); - fields3.Add("title", "hello world"); - fields3.Add("category", "green;yellow"); - Assert.True(cl.AddDocument("baz", fields3)); - var fields4 = new Dictionary(); - fields4.Add("title", "hello world"); - fields4.Add("category", "orange,purple"); - Assert.True(cl.AddDocument("qux", fields4)); + Assert.True(cl.AddDocument("foo", new Dictionary + { + { "title", "hello world" }, + { "category", "red" } + })); + Assert.True(cl.AddDocument("bar", new Dictionary + { + { "title", "hello world" }, + { "category", "blue" } + })); + Assert.True(cl.AddDocument("baz", new Dictionary + { + { "title", "hello world" }, + { "category", "green;yellow" } + })); + Assert.True(cl.AddDocument("qux", new Dictionary + { + { "title", "hello world" }, + { "category", "orange,purple" } + })); Assert.Equal(1, cl.Search(new Query("@category:{red}")).TotalResults); Assert.Equal(1, cl.Search(new Query("@category:{blue}")).TotalResults); @@ -816,9 +826,11 @@ public void TestMultiDocuments() Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - var fields = new Dictionary(); - fields.Add("title", "hello world"); - fields.Add("body", "lorem ipsum"); + var fields = new Dictionary + { + { "title", "hello world" }, + { "body", "lorem ipsum" } + }; var results = cl.AddDocuments(new Document("doc1", fields), new Document("doc2", fields), new Document("doc3", fields)); @@ -841,10 +853,11 @@ public void TestReturnFields() Schema sc = new Schema().AddTextField("field1", 1.0).AddTextField("field2", 1.0); Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - - var doc = new Dictionary(); - doc.Add("field1", "value1"); - doc.Add("field2", "value2"); + var doc = new Dictionary + { + { "field1", "value1" }, + { "field2", "value2" } + }; // Store it Assert.True(cl.AddDocument("doc", doc)); @@ -862,9 +875,11 @@ public void TestInKeys() Schema sc = new Schema().AddTextField("field1", 1.0).AddTextField("field2", 1.0); Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - var doc = new Dictionary(); - doc.Add("field1", "value"); - doc.Add("field2", "not"); + var doc = new Dictionary + { + { "field1", "value" }, + { "field2", "not" } + }; // Store it Assert.True(cl.AddDocument("doc1", doc)); diff --git a/tests/NRediSearch.Test/ExampleUsage.cs b/tests/NRediSearch.Test/ExampleUsage.cs index ad1ae7270..0860887c0 100644 --- a/tests/NRediSearch.Test/ExampleUsage.cs +++ b/tests/NRediSearch.Test/ExampleUsage.cs @@ -24,7 +24,7 @@ public void BasicUsage() .AddTextField("body", 1.0) .AddNumericField("price"); - bool result = false; + bool result; try { result = client.CreateIndex(sc, new ConfiguredIndexOptions()); diff --git a/tests/StackExchange.Redis.Tests/Cluster.cs b/tests/StackExchange.Redis.Tests/Cluster.cs index d0ef5cd88..d3a1adfcf 100644 --- a/tests/StackExchange.Redis.Tests/Cluster.cs +++ b/tests/StackExchange.Redis.Tests/Cluster.cs @@ -163,7 +163,7 @@ public void TestIdentity() [Fact] public void IntentionalWrongServer() { - string StringGet(IServer server, RedisKey key, CommandFlags flags = CommandFlags.None) + static string StringGet(IServer server, RedisKey key, CommandFlags flags = CommandFlags.None) => (string)server.Execute("GET", new object[] { key }, flags); using (var conn = Create()) diff --git a/tests/StackExchange.Redis.Tests/Failover.cs b/tests/StackExchange.Redis.Tests/Failover.cs index 4923c65ef..ef0cb6899 100644 --- a/tests/StackExchange.Redis.Tests/Failover.cs +++ b/tests/StackExchange.Redis.Tests/Failover.cs @@ -206,7 +206,7 @@ public async Task DereplicateGoesToPrimary() [Fact] public async Task SubscriptionsSurviveMasterSwitchAsync() { - void TopologyFail() => Skip.Inconclusive("Replication tolopogy change failed...and that's both inconsistent and not what we're testing."); + static void TopologyFail() => Skip.Inconclusive("Replication tolopogy change failed...and that's both inconsistent and not what we're testing."); if (RunningInCI) { diff --git a/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs b/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs index c9af996d1..142b77c91 100644 --- a/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs +++ b/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs @@ -1,5 +1,4 @@ using System; -using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -20,7 +19,7 @@ private static void ForceGC() } } - [Fact(Skip = "needs investigation on netcoreapp3.1")] + [Fact] public async Task MuxerIsCollected() { #if DEBUG diff --git a/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs b/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs index 1862e28f2..5eb06cc54 100644 --- a/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs +++ b/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs @@ -5,11 +5,7 @@ // a specific target and scoped to a namespace, type, member, etc. [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionFailedErrors.SSLCertificateValidationError(System.Boolean)")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.PreserveOrder.Execute(System.Boolean)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.PubSub.ExplicitPublishMode")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.PubSub.TestBasicPubSubFireAndForget(System.Boolean)")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.PubSub.TestPatternPubSub(System.Boolean)")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.PubSub.TestBasicPubSub(System.Boolean,System.String,System.Boolean)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSL.ConnectToSSLServer(System.Boolean,System.Boolean)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSL.ShowCertFailures(StackExchange.Redis.Tests.Helpers.TextWriterOutputHelper)~System.Net.Security.RemoteCertificateValidationCallback")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionShutdown.ShutdownRaisesConnectionFailedAndRestore")] diff --git a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs index c17adf77e..eafd4360f 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs @@ -501,18 +501,14 @@ public KeyType TypeOf(string key) { if (key == null) throw new ArgumentNullException(nameof(key)); - switch (SendExpectString("TYPE {0}\r\n", key)) + return SendExpectString("TYPE {0}\r\n", key) switch { - case "none": - return KeyType.None; - case "string": - return KeyType.String; - case "set": - return KeyType.Set; - case "list": - return KeyType.List; - } - throw new ResponseException("Invalid value"); + "none" => KeyType.None, + "string" => KeyType.String, + "set" => KeyType.Set, + "list" => KeyType.List, + _ => throw new ResponseException("Invalid value"), + }; } public string RandomKey() diff --git a/tests/StackExchange.Redis.Tests/Locking.cs b/tests/StackExchange.Redis.Tests/Locking.cs index 65b412737..7089a6972 100644 --- a/tests/StackExchange.Redis.Tests/Locking.cs +++ b/tests/StackExchange.Redis.Tests/Locking.cs @@ -103,20 +103,13 @@ private void TestLockOpCountByVersion(IConnectionMultiplexer conn, int expectedO // note we get a ping from GetCounters } - private IConnectionMultiplexer Create(TestMode mode) + private IConnectionMultiplexer Create(TestMode mode) => mode switch { - switch (mode) - { - case TestMode.MultiExec: - return Create(); - case TestMode.NoMultiExec: - return Create(disabledCommands: new[] { "multi", "exec" }); - case TestMode.Twemproxy: - return Create(proxy: Proxy.Twemproxy); - default: - throw new NotSupportedException(mode.ToString()); - } - } + TestMode.MultiExec => Create(), + TestMode.NoMultiExec => Create(disabledCommands: new[] { "multi", "exec" }), + TestMode.Twemproxy => Create(proxy: Proxy.Twemproxy), + _ => throw new NotSupportedException(mode.ToString()), + }; [Theory, MemberData(nameof(TestModes))] public async Task TakeLockAndExtend(TestMode mode) diff --git a/tests/StackExchange.Redis.Tests/MassiveOps.cs b/tests/StackExchange.Redis.Tests/MassiveOps.cs index a0d6a4fa0..6f5d1e26e 100644 --- a/tests/StackExchange.Redis.Tests/MassiveOps.cs +++ b/tests/StackExchange.Redis.Tests/MassiveOps.cs @@ -39,7 +39,7 @@ public async Task MassiveBulkOpsAsync(bool withContinuation) RedisKey key = Me(); var conn = muxer.GetDatabase(); await conn.PingAsync().ForAwait(); - void nonTrivial(Task _) + static void nonTrivial(Task _) { Thread.SpinWait(5); } diff --git a/tests/StackExchange.Redis.Tests/Migrate.cs b/tests/StackExchange.Redis.Tests/Migrate.cs index 3aa797fc8..91bd373e3 100644 --- a/tests/StackExchange.Redis.Tests/Migrate.cs +++ b/tests/StackExchange.Redis.Tests/Migrate.cs @@ -1,6 +1,4 @@ -#pragma warning disable RCS1090 // Call 'ConfigureAwait(false)'. - -using System; +using System; using System.Linq; using System.Threading.Tasks; using Xunit; diff --git a/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs b/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs index 834104a78..5d9da3b16 100644 --- a/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs +++ b/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs @@ -12,7 +12,7 @@ public class RedisValueEquivalency [Fact] public void Int32_Matrix() { - void Check(RedisValue known, RedisValue test) + static void Check(RedisValue known, RedisValue test) { KeysAndValues.CheckSame(known, test); if (known.IsNull) @@ -51,7 +51,7 @@ void Check(RedisValue known, RedisValue test) [Fact] public void Int64_Matrix() { - void Check(RedisValue known, RedisValue test) + static void Check(RedisValue known, RedisValue test) { KeysAndValues.CheckSame(known, test); if (known.IsNull) @@ -90,7 +90,7 @@ void Check(RedisValue known, RedisValue test) [Fact] public void Double_Matrix() { - void Check(RedisValue known, RedisValue test) + static void Check(RedisValue known, RedisValue test) { KeysAndValues.CheckSame(known, test); if (known.IsNull) @@ -213,9 +213,9 @@ public void TryParseInt64() Assert.True(((RedisValue)123.0).TryParse(out l)); Assert.Equal(123, l); - Assert.False(((RedisValue)"abc").TryParse(out l)); - Assert.False(((RedisValue)"123.1").TryParse(out l)); - Assert.False(((RedisValue)123.1).TryParse(out l)); + Assert.False(((RedisValue)"abc").TryParse(out long _)); + Assert.False(((RedisValue)"123.1").TryParse(out long _)); + Assert.False(((RedisValue)123.1).TryParse(out long _)); } [Fact] @@ -227,7 +227,7 @@ public void TryParseInt32() Assert.True(((RedisValue)123.0).TryParse(out i)); Assert.Equal(123, i); - Assert.False(((RedisValue)(int.MaxValue + 123L)).TryParse(out i)); + Assert.False(((RedisValue)(int.MaxValue + 123L)).TryParse(out int _)); Assert.True(((RedisValue)"123").TryParse(out i)); Assert.Equal(123, i); @@ -241,9 +241,9 @@ public void TryParseInt32() Assert.True(((RedisValue)123.0).TryParse(out i)); Assert.Equal(123, i); - Assert.False(((RedisValue)"abc").TryParse(out i)); - Assert.False(((RedisValue)"123.1").TryParse(out i)); - Assert.False(((RedisValue)123.1).TryParse(out i)); + Assert.False(((RedisValue)"abc").TryParse(out int _)); + Assert.False(((RedisValue)"123.1").TryParse(out int _)); + Assert.False(((RedisValue)123.1).TryParse(out int _)); } [Fact] @@ -276,7 +276,7 @@ public void TryParseDouble() Assert.True(((RedisValue)"123.1").TryParse(out d)); Assert.Equal(123.1, d); - Assert.False(((RedisValue)"abc").TryParse(out d)); + Assert.False(((RedisValue)"abc").TryParse(out double _)); } } } diff --git a/tests/StackExchange.Redis.Tests/Scripting.cs b/tests/StackExchange.Redis.Tests/Scripting.cs index 84b911659..787f4f4f3 100644 --- a/tests/StackExchange.Redis.Tests/Scripting.cs +++ b/tests/StackExchange.Redis.Tests/Scripting.cs @@ -801,7 +801,7 @@ public void PurgeLuaScriptCache() Assert.False(ReferenceEquals(first, shouldBeNew)); } - private static void _PurgeLuaScriptOnFinalize(string script) + private static void PurgeLuaScriptOnFinalizeImpl(string script) { var first = LuaScript.Prepare(script); var fromCache = LuaScript.Prepare(script); @@ -818,12 +818,12 @@ public void PurgeLuaScriptOnFinalize() // This has to be a separate method to guarantee that the created LuaScript objects go out of scope, // and are thus available to be GC'd - _PurgeLuaScriptOnFinalize(Script); + PurgeLuaScriptOnFinalizeImpl(Script); CollectGarbage(); Assert.Equal(0, LuaScript.GetCachedScriptCount()); - var shouldBeNew = LuaScript.Prepare(Script); + LuaScript.Prepare(Script); Assert.Equal(1, LuaScript.GetCachedScriptCount()); } diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 3ca698425..e38008e99 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -417,9 +417,7 @@ void callback() for (int i = 0; i < threads; i++) { var thd = threadArr[i]; -#pragma warning disable SYSLIB0006 // yes, we know if (thd.IsAlive) thd.Abort(); -#pragma warning restore SYSLIB0006 // yes, we know } throw new TimeoutException(); } diff --git a/tests/StackExchange.Redis.Tests/TransactionWrapperTests.cs b/tests/StackExchange.Redis.Tests/TransactionWrapperTests.cs index c776e2f47..0b76e309a 100644 --- a/tests/StackExchange.Redis.Tests/TransactionWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/TransactionWrapperTests.cs @@ -1,11 +1,11 @@ using System.Text; +using System.Threading.Tasks; using Moq; using StackExchange.Redis.KeyspaceIsolation; using Xunit; namespace StackExchange.Redis.Tests { -#pragma warning disable RCS1047 // Non-asynchronous method name should not end with 'Async'. [Collection(nameof(MoqDependentCollection))] public sealed class TransactionWrapperTests { @@ -117,9 +117,9 @@ public void AddCondition_SortedSetScoreCountNotExists() } [Fact] - public void ExecuteAsync() + public async Task ExecuteAsync() { - wrapper.ExecuteAsync(CommandFlags.None); + await wrapper.ExecuteAsync(CommandFlags.None); mock.Verify(_ => _.ExecuteAsync(CommandFlags.None), Times.Once()); } @@ -130,5 +130,4 @@ public void Execute() mock.Verify(_ => _.Execute(CommandFlags.None), Times.Once()); } } -#pragma warning restore RCS1047 // Non-asynchronous method name should not end with 'Async'. } diff --git a/toys/KestrelRedisServer/KestrelRedisServer.csproj b/toys/KestrelRedisServer/KestrelRedisServer.csproj index 911116f68..bf3c1a3f1 100644 --- a/toys/KestrelRedisServer/KestrelRedisServer.csproj +++ b/toys/KestrelRedisServer/KestrelRedisServer.csproj @@ -9,6 +9,7 @@ + diff --git a/toys/StackExchange.Redis.Server/MemoryCacheRedisServer.cs b/toys/StackExchange.Redis.Server/MemoryCacheRedisServer.cs index ab56c3792..f7c100afe 100644 --- a/toys/StackExchange.Redis.Server/MemoryCacheRedisServer.cs +++ b/toys/StackExchange.Redis.Server/MemoryCacheRedisServer.cs @@ -42,7 +42,6 @@ protected override bool Exists(int database, RedisKey key) protected override IEnumerable Keys(int database, RedisKey pattern) { - string s = pattern; foreach (var pair in _cache) { if (IsMatch(pattern, pair.Key)) yield return pair.Key; diff --git a/toys/StackExchange.Redis.Server/RespServer.cs b/toys/StackExchange.Redis.Server/RespServer.cs index 3edd3f9f3..f8111647c 100644 --- a/toys/StackExchange.Redis.Server/RespServer.cs +++ b/toys/StackExchange.Redis.Server/RespServer.cs @@ -32,7 +32,7 @@ protected RespServer(TextWriter output = null) private static Dictionary BuildCommands(RespServer server) { - RedisCommandAttribute CheckSignatureAndGetAttribute(MethodInfo method) + static RedisCommandAttribute CheckSignatureAndGetAttribute(MethodInfo method) { if (method.ReturnType != typeof(TypedRedisValue)) return null; var p = method.GetParameters(); @@ -307,7 +307,7 @@ public void Log(string message) public static async ValueTask WriteResponseAsync(RedisClient client, PipeWriter output, TypedRedisValue value) { - void WritePrefix(PipeWriter ooutput, char pprefix) + static void WritePrefix(PipeWriter ooutput, char pprefix) { var span = ooutput.GetSpan(1); span[0] = (byte)pprefix; @@ -385,7 +385,7 @@ private static bool TryParseRequest(Arena arena, ref ReadOnlySequence public ValueTask TryProcessRequestAsync(ref ReadOnlySequence buffer, RedisClient client, PipeWriter output) { - async ValueTask Awaited(ValueTask wwrite, TypedRedisValue rresponse) + static async ValueTask Awaited(ValueTask wwrite, TypedRedisValue rresponse) { await wwrite; rresponse.Recycle(); From f38c742ca0dbf9c926134072bdc86c82cafc4a9b Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 28 Jun 2021 07:46:42 -0400 Subject: [PATCH 007/435] Tests: better logging (#1788) The Sentinel issue appears to be server-side, it's waiting 10 seconds to issue the REPLICAOF after a failover. This seems to be a Redis issue I haven't narrowed down yet, but increasing logging here to help tests in general. --- tests/StackExchange.Redis.Tests/SentinelFailover.cs | 13 +++++++++++++ .../SharedConnectionFixture.cs | 3 ++- tests/StackExchange.Redis.Tests/TestBase.cs | 8 ++++---- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/SentinelFailover.cs b/tests/StackExchange.Redis.Tests/SentinelFailover.cs index 3063f0448..9b5b5932c 100644 --- a/tests/StackExchange.Redis.Tests/SentinelFailover.cs +++ b/tests/StackExchange.Redis.Tests/SentinelFailover.cs @@ -20,6 +20,10 @@ public async Task ManagedMasterConnectionEndToEndWithFailoverTest() conn.ConfigurationChanged += (s, e) => { Log($"Configuration changed: {e.EndPoint}"); }; + var sub = conn.GetSubscriber(); + sub.Subscribe("*", (channel, message) => { + Log($"Sub: {channel}, message:{message}"); + }); var db = conn.GetDatabase(); await db.PingAsync(); @@ -62,6 +66,15 @@ public async Task ManagedMasterConnectionEndToEndWithFailoverTest() var sw = Stopwatch.StartNew(); SentinelServerA.SentinelFailover(ServiceName); + // There's no point in doing much for 10 seconds - this is a built-in delay of how Sentinel works. + // The actual completion invoking the replication of the former master is handled via + // https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L4799-L4808 + // ...which is invoked by INFO polls every 10 seconds (https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L81) + // ...which is calling https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L2666 + // However, the quicker iteration on INFO during an o_down does not apply here: https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L3089-L3104 + // So...we're waiting 10 seconds, no matter what. Might as well just idle to be more stable. + await Task.Delay(TimeSpan.FromSeconds(10)); + // wait until the replica becomes the master Log("Waiting for ready post-failover..."); await WaitForReadyAsync(expectedMaster: replicas[0]); diff --git a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs index b832262a2..bf22489dd 100644 --- a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs @@ -326,7 +326,8 @@ public void Teardown(TextWriter output) } //Assert.True(false, $"There were {privateFailCount} private ambient exceptions."); } - TestBase.Log(output, $"Service Counts: (Scheduler) Queue: {SocketManager.Shared?.SchedulerPool?.TotalServicedByQueue.ToString()}, Pool: {SocketManager.Shared?.SchedulerPool?.TotalServicedByPool.ToString()}"); + var pool = SocketManager.Shared?.SchedulerPool; + TestBase.Log(output, $"Service Counts: (Scheduler) By Queue: {pool?.TotalServicedByQueue.ToString()}, By Pool: {pool?.TotalServicedByPool.ToString()}, Workers: {pool?.WorkerCount.ToString()}, Available: {pool?.AvailableCount.ToString()}"); } } diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index e38008e99..a24d6948a 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -361,10 +361,10 @@ public static ConnectionMultiplexer CreateDefault( } public static string Me([CallerFilePath] string filePath = null, [CallerMemberName] string caller = null) => -#if NET462 - "net462-" -#elif NETCOREAPP2_1 - "netcoreapp2.1-" +#if NET472 + "net472-" +#elif NETCOREAPP3_1 + "netcoreapp3.1-" #else "unknown-" #endif From dcc5e7dfa7d24a9c044fd23c3661e9632b2120f3 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 28 Jun 2021 07:46:59 -0400 Subject: [PATCH 008/435] Dispose/Finalizers: Cleanup (#1787) Making analyzers happy, but teensy perf so why not. --- src/StackExchange.Redis/SocketManager.cs | 16 ++++++++-------- tests/BasicTest/Program.cs | 7 ++++++- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/StackExchange.Redis/SocketManager.cs b/src/StackExchange.Redis/SocketManager.cs index f7ea567ea..1be2eb9bc 100644 --- a/src/StackExchange.Redis/SocketManager.cs +++ b/src/StackExchange.Redis/SocketManager.cs @@ -187,9 +187,14 @@ private enum CallbackOperation /// /// Releases all resources associated with this instance /// - public void Dispose() => Dispose(true); + public void Dispose() + { + DisposeRefs(); + GC.SuppressFinalize(this); + OnDispose(); + } - private void Dispose(bool disposing) + private void DisposeRefs() { // note: the scheduler *can't* be collected by itself - there will // be threads, and those threads will be rooting the DedicatedThreadPool; @@ -197,17 +202,12 @@ private void Dispose(bool disposing) var tmp = SchedulerPool; Scheduler = PipeScheduler.ThreadPool; try { tmp?.Dispose(); } catch { } - if (disposing) - { - GC.SuppressFinalize(this); - OnDispose(); - } } /// /// Releases *appropriate* resources associated with this instance /// - ~SocketManager() => Dispose(false); + ~SocketManager() => DisposeRefs(); internal static Socket CreateSocket(EndPoint endpoint) { diff --git a/tests/BasicTest/Program.cs b/tests/BasicTest/Program.cs index 9f22880bd..c324fb59e 100644 --- a/tests/BasicTest/Program.cs +++ b/tests/BasicTest/Program.cs @@ -82,6 +82,7 @@ void IDisposable.Dispose() mgr = null; db = null; connection = null; + GC.SuppressFinalize(this); } private const int COUNT = 50; @@ -219,7 +220,11 @@ public class Issue898 : IDisposable private readonly ConnectionMultiplexer mux; private readonly IDatabase db; - public void Dispose() => mux?.Dispose(); + public void Dispose() + { + mux?.Dispose(); + GC.SuppressFinalize(this); + } public Issue898() { mux = ConnectionMultiplexer.Connect("127.0.0.1:6379"); From 14778950580de5e5a6b787bc95cc24e59e617a6f Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 28 Jun 2021 07:57:33 -0400 Subject: [PATCH 009/435] Add == & != operators for ChannelMessaege and RedisFeatures (#1786) Best practice cleanup, since we're overriding `.Equals()` Co-authored-by: Marc Gravell --- src/StackExchange.Redis/ChannelMessageQueue.cs | 10 ++++++++++ src/StackExchange.Redis/RedisFeatures.cs | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/StackExchange.Redis/ChannelMessageQueue.cs b/src/StackExchange.Redis/ChannelMessageQueue.cs index 280e4fc57..5f68714d2 100644 --- a/src/StackExchange.Redis/ChannelMessageQueue.cs +++ b/src/StackExchange.Redis/ChannelMessageQueue.cs @@ -48,6 +48,16 @@ internal ChannelMessage(ChannelMessageQueue queue, in RedisChannel channel, in R /// The value that was broadcast /// public RedisValue Message { get; } + + /// + /// Checks if 2 messages are .Equal() + /// + public static bool operator ==(ChannelMessage left, ChannelMessage right) => left.Equals(right); + + /// + /// Checks if 2 messages are not .Equal() + /// + public static bool operator !=(ChannelMessage left, ChannelMessage right) => !left.Equals(right); } /// diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 37251e93f..a8d60a9d3 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -259,9 +259,20 @@ orderby prop.Name /// Returns the hash code for this instance. /// A 32-bit signed integer that is the hash code for this instance. public override int GetHashCode() => Version.GetHashCode(); + /// Indicates whether this instance and a specified object are equal. /// true if and this instance are the same type and represent the same value; otherwise, false. /// The object to compare with the current instance. public override bool Equals(object obj) => obj is RedisFeatures f && f.Version == Version; + + /// + /// Checks if 2 RedisFeatures are .Equal() + /// + public static bool operator ==(RedisFeatures left, RedisFeatures right) => left.Equals(right); + + /// + /// Checks if 2 RedisFeatures are not .Equal() + /// + public static bool operator !=(RedisFeatures left, RedisFeatures right) => !left.Equals(right); } } From 3978c5e305360e2f23d0b532b73f22ffddbd6941 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 30 Jun 2021 14:31:38 +0100 Subject: [PATCH 010/435] prevent GetOutstandingCount from throwing (#1792) * prevent GetOutstandingCount from throwing * add release notes * Update docs/ReleaseNotes.md Co-authored-by: Nick Craver Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ServerEndPoint.cs | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index e8691acb3..22eafcf84 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -3,6 +3,7 @@ ## Unreleased - Sentinel potential memory leak fix in OnManagedConnectionFailed handler (#1710 via alexSatov) +- fix issue where `GetOutstandingCount` could obscure underlying faults by faulting itself (#1792 via mgravell) ## 2.2.50 diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 8a080f0b3..e3f180ab1 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -437,19 +437,20 @@ internal ServerCounters GetCounters() internal void GetOutstandingCount(RedisCommand command, out int inst, out int qs, out long @in, out int qu, out bool aw, out long toRead, out long toWrite, out BacklogStatus bs, out PhysicalConnection.ReadStatus rs, out PhysicalConnection.WriteStatus ws) { - var bridge = GetBridge(command, false); - if (bridge == null) + inst = qs = qu = 0; + @in = toRead = toWrite = 0; + aw = false; + bs = BacklogStatus.Inactive; + rs = PhysicalConnection.ReadStatus.NA; + ws = PhysicalConnection.WriteStatus.NA; + try { - inst = qs = qu = 0; - @in = toRead = toWrite = 0; - aw = false; - bs = BacklogStatus.Inactive; - rs = PhysicalConnection.ReadStatus.NA; - ws = PhysicalConnection.WriteStatus.NA; + var bridge = GetBridge(command, false); + bridge?.GetOutstandingCount(out inst, out qs, out @in, out qu, out aw, out toRead, out toWrite, out bs, out rs, out ws); } - else - { - bridge.GetOutstandingCount(out inst, out qs, out @in, out qu, out aw, out toRead, out toWrite, out bs, out rs, out ws); + catch (Exception ex) + { // only needs to be best efforts + System.Diagnostics.Debug.WriteLine(ex.Message); } } From cd6cafac1fdb1020a06f36cd9ca36cc9955e964f Mon Sep 17 00:00:00 2001 From: Tim Lovell-Smith Date: Thu, 1 Jul 2021 00:30:56 -0700 Subject: [PATCH 011/435] reapply the fix to #1719. (#1779) Fixes "messages get sent out-of-order when a backlog is created by another thread --- src/StackExchange.Redis/PhysicalBridge.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 652b79719..ea3883c0a 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -696,6 +696,14 @@ internal WriteResult WriteMessageTakingWriteLockSync(PhysicalConnection physical Trace("Writing: " + message); message.SetEnqueued(physical); // this also records the read/write stats at this point + // AVOID REORDERING MESSAGES + // Prefer to add it to the backlog if this thread can see that there might already be a message backlog. + // We do this before attempting to take the writelock, because we won't actually write, we'll just let the backlog get processed in due course + if (PushToBacklog(message, onlyIfExists: true)) + { + return WriteResult.Success; // queued counts as success + } + LockToken token = default; try { @@ -970,11 +978,20 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect Trace("Writing: " + message); message.SetEnqueued(physical); // this also records the read/write stats at this point + // AVOID REORDERING MESSAGES + // Prefer to add it to the backlog if this thread can see that there might already be a message backlog. + // We do this before attempting to take the writelock, because we won't actually write, we'll just let the backlog get processed in due course + if (PushToBacklog(message, onlyIfExists: true)) + { + return new ValueTask(WriteResult.Success); // queued counts as success + } + bool releaseLock = true; // fine to default to true, as it doesn't matter until token is a "success" int lockTaken = 0; LockToken token = default; try { + // try to acquire it synchronously // note: timeout is specified in mutex-constructor token = _singleWriterMutex.TryWait(options: WaitOptions.NoDelay); From fe3a9928a2df996f692d6a6f6b37f41b6c360e18 Mon Sep 17 00:00:00 2001 From: mgravell Date: Thu, 1 Jul 2021 08:33:15 +0100 Subject: [PATCH 012/435] release notes --- docs/ReleaseNotes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 22eafcf84..760e80605 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -4,6 +4,7 @@ - Sentinel potential memory leak fix in OnManagedConnectionFailed handler (#1710 via alexSatov) - fix issue where `GetOutstandingCount` could obscure underlying faults by faulting itself (#1792 via mgravell) +- fix issue #1719 with backlog messages becoming reordered (#1779 via TimLovellSmith) ## 2.2.50 From cc5de19ae19d0370175603f3282d91fecde62ad2 Mon Sep 17 00:00:00 2001 From: mgravell Date: Thu, 1 Jul 2021 09:14:11 +0100 Subject: [PATCH 013/435] release notes 2.2.62 --- docs/ReleaseNotes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 760e80605..b51ea4354 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.2.62 + - Sentinel potential memory leak fix in OnManagedConnectionFailed handler (#1710 via alexSatov) - fix issue where `GetOutstandingCount` could obscure underlying faults by faulting itself (#1792 via mgravell) - fix issue #1719 with backlog messages becoming reordered (#1779 via TimLovellSmith) From ae4d29a84378a10a349ea76cd3fdddc2d0d36e62 Mon Sep 17 00:00:00 2001 From: Philo Date: Thu, 1 Jul 2021 19:54:58 -0700 Subject: [PATCH 014/435] Add .NET version and timestamp on default logger rows (#1796) Prefix each entry with a timestamp that looks like: ``` 18:18:47.9261: Connecting (async) on .NET Core 3.1.16 18:18:47.9263: cachename.redis.cache.windows.net:6380/Interactive: Connecting... 18:18:47.9463: cachename.redis.cache.windows.net:6380: BeginConnectAsync 18:18:47.9483: 1 unique nodes specified ``` --- docs/ReleaseNotes.md | 1 + .../ConnectionMultiplexer.cs | 7 ++- tests/StackExchange.Redis.Tests/Cluster.cs | 51 +++++++------------ .../Helpers/TextWriterOutputHelper.cs | 8 ++- .../StackExchange.Redis.Tests/MultiMaster.cs | 6 +-- tests/StackExchange.Redis.Tests/TestBase.cs | 2 +- 6 files changed, 34 insertions(+), 41 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b51ea4354..79e602c5b 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,6 +1,7 @@ # Release Notes ## Unreleased +- logging additions (.NET Version and timestamps) for better debugging (#1796 via philon-msft) ## 2.2.62 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index a4de5e997..a37f1ea2a 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -14,6 +14,7 @@ using StackExchange.Redis.Profiling; using Pipelines.Sockets.Unofficial; using System.ComponentModel; +using System.Runtime.InteropServices; namespace StackExchange.Redis { @@ -852,6 +853,8 @@ private static async Task ConnectImplAsync(ConfigurationO { try { + log?.WriteLine($"Connecting (async) on {RuntimeInformation.FrameworkDescription}"); + muxer = CreateMultiplexer(configuration, logProxy, out connectHandler); killMe = muxer; Interlocked.Increment(ref muxer._connectAttemptCount); @@ -965,7 +968,7 @@ public void WriteLine(string message = null) { lock (SyncLock) { - _log?.WriteLine(message); + _log?.WriteLine($"{DateTime.UtcNow:HH:mm:ss.ffff}: {message}"); } } } @@ -1141,6 +1144,8 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat { try { + log?.WriteLine($"Connecting (sync) on {RuntimeInformation.FrameworkDescription}"); + muxer = CreateMultiplexer(configuration, logProxy, out connectHandler); killMe = muxer; Interlocked.Increment(ref muxer._connectAttemptCount); diff --git a/tests/StackExchange.Redis.Tests/Cluster.cs b/tests/StackExchange.Redis.Tests/Cluster.cs index d3a1adfcf..a7af400da 100644 --- a/tests/StackExchange.Redis.Tests/Cluster.cs +++ b/tests/StackExchange.Redis.Tests/Cluster.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Net; +using System.Text; using System.Threading; using System.Threading.Tasks; using StackExchange.Redis.Profiling; @@ -39,35 +40,24 @@ public void ExportConfiguration() [Fact] public void ConnectUsesSingleSocket() { - using (var sw = new StringWriter()) + for (int i = 0; i < 5; i++) { - try + using (var muxer = Create(failMessage: i + ": ", log: Writer)) { - for (int i = 0; i < 5; i++) + foreach (var ep in muxer.GetEndPoints()) { - using (var muxer = Create(failMessage: i + ": ", log: sw)) - { - foreach (var ep in muxer.GetEndPoints()) - { - var srv = muxer.GetServer(ep); - var counters = srv.GetCounters(); - Log($"{i}; interactive, {ep}, count: {counters.Interactive.SocketCount}"); - Log($"{i}; subscription, {ep}, count: {counters.Subscription.SocketCount}"); - } - foreach (var ep in muxer.GetEndPoints()) - { - var srv = muxer.GetServer(ep); - var counters = srv.GetCounters(); - Assert.Equal(1, counters.Interactive.SocketCount); - Assert.Equal(1, counters.Subscription.SocketCount); - } - } + var srv = muxer.GetServer(ep); + var counters = srv.GetCounters(); + Log($"{i}; interactive, {ep}, count: {counters.Interactive.SocketCount}"); + Log($"{i}; subscription, {ep}, count: {counters.Subscription.SocketCount}"); + } + foreach (var ep in muxer.GetEndPoints()) + { + var srv = muxer.GetServer(ep); + var counters = srv.GetCounters(); + Assert.Equal(1, counters.Interactive.SocketCount); + Assert.Equal(1, counters.Subscription.SocketCount); } - } - finally - { - // Connection info goes at the end... - Log(sw.ToString()); } } } @@ -96,18 +86,13 @@ private void PrintEndpoints(EndPoint[] endpoints) public void Connect() { var expectedPorts = new HashSet(Enumerable.Range(TestConfig.Current.ClusterStartPort, TestConfig.Current.ClusterServerCount)); - using (var sw = new StringWriter()) - using (var muxer = Create(log: sw)) + using (var muxer = Create(log: Writer)) { var endpoints = muxer.GetEndPoints(); if (TestConfig.Current.ClusterServerCount != endpoints.Length) { PrintEndpoints(endpoints); } - else - { - Log(sw.ToString()); - } Assert.Equal(TestConfig.Current.ClusterServerCount, endpoints.Length); int masters = 0, replicas = 0; @@ -461,8 +446,7 @@ public void SScan() [Fact] public void GetConfig() { - using (var sw = new StringWriter()) - using (var muxer = Create(allowAdmin: true, log: sw)) + using (var muxer = Create(allowAdmin: true, log: Writer)) { var endpoints = muxer.GetEndPoints(); var server = muxer.GetServer(endpoints[0]); @@ -478,7 +462,6 @@ public void GetConfig() { Log(node.ToString()); } - Log(sw.ToString()); Assert.Equal(TestConfig.Current.ClusterServerCount, endpoints.Length); Assert.Equal(TestConfig.Current.ClusterServerCount, nodes.Nodes.Count); diff --git a/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs b/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs index 8067147f5..7d89a187a 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs @@ -24,8 +24,12 @@ public override void WriteLine(string value) { try { - base.Write(TestBase.Time()); - base.Write(": "); + // Prevent double timestamps + if (value.Length < "HH:mm:ss.ffff:".Length || value["HH:mm:ss.ffff:".Length - 1] != ':') + { + base.Write(TestBase.Time()); + base.Write(": "); + } base.WriteLine(value); } catch (Exception ex) diff --git a/tests/StackExchange.Redis.Tests/MultiMaster.cs b/tests/StackExchange.Redis.Tests/MultiMaster.cs index 12823f054..3fbbbc330 100644 --- a/tests/StackExchange.Redis.Tests/MultiMaster.cs +++ b/tests/StackExchange.Redis.Tests/MultiMaster.cs @@ -32,10 +32,10 @@ public void CannotFlushReplica() [Fact] public void TestMultiNoTieBreak() { - using (var log = new StringWriter()) - using (Create(log: log, tieBreaker: "")) + var log = new StringBuilder(); + Writer.EchoTo(log); + using (Create(log: Writer, tieBreaker: "")) { - Log(log.ToString()); Assert.Contains("Choosing master arbitrarily", log.ToString()); } } diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index a24d6948a..c31a72178 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -119,7 +119,7 @@ static TestBase() Console.WriteLine(" GC LOH Mode: " + GCSettings.LargeObjectHeapCompactionMode); Console.WriteLine(" GC Latency Mode: " + GCSettings.LatencyMode); } - internal static string Time() => DateTime.UtcNow.ToString("HH:mm:ss.fff"); + internal static string Time() => DateTime.UtcNow.ToString("HH:mm:ss.ffff"); protected void OnConnectionFailed(object sender, ConnectionFailedEventArgs e) { Interlocked.Increment(ref privateFailCount); From 30565ff269b0b465fc10f66de94d4bba16319806 Mon Sep 17 00:00:00 2001 From: Chriz76 <48691511+Chriz76@users.noreply.github.com> Date: Wed, 21 Jul 2021 13:39:42 +0200 Subject: [PATCH 015/435] Script transmission in the scripting doku (#1812) See https://github.com/StackExchange/StackExchange.Redis/issues/1371 and https://github.com/StackExchange/StackExchange.Redis/issues/1246 --- docs/Scripting.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Scripting.md b/docs/Scripting.md index e04b2d19a..5f4e38bbd 100644 --- a/docs/Scripting.md +++ b/docs/Scripting.md @@ -36,8 +36,9 @@ Any object that exposes field or property members with the same name as @-prefix - RedisKey - RedisValue +StackExchange.Redis handles Lua script caching internally. It automatically transmits the Lua script to redis on the first call to 'ScriptEvaluate'. For further calls of the same script only the hash with [`EVALSHA`](http://redis.io/commands/evalsha) is used. -To avoid retransmitting the Lua script to redis each time it is evaluated, `LuaScript` objects can be converted into `LoadedLuaScript`s via `LuaScript.Load(IServer)`. +For more control of the Lua script transmission to redis, `LuaScript` objects can be converted into `LoadedLuaScript`s via `LuaScript.Load(IServer)`. `LoadedLuaScripts` are evaluated with the [`EVALSHA`](http://redis.io/commands/evalsha), and referred to by hash. An example use of `LoadedLuaScript`: From c0153ce33a55bcbd2c0ade61b09b8e5c98cfcdb5 Mon Sep 17 00:00:00 2001 From: Daniel Chandler Date: Wed, 21 Jul 2021 21:46:39 +1000 Subject: [PATCH 016/435] Stream Length Transaction Conditions (#1807) Adding some new condition overloads for XLEN, useful for deleting empty streams in a safe method. It's a very simple change, one line to the length condition implementation plus the overloads. Tests have been added and pass, though I don't have the infrastructure to run the failover/cluster/etc tests. I can include the .trx file if that's important. I would have liked to add more conditions, like one that calls XPENDING to examine the number of pending messages, but that would need a new condition implementation with different messages and more complicated code to examine the results. Maybe I'll do it in another pull request, if I find the time. --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Condition.cs | 22 +++++ .../StackExchange.Redis.Tests/Transactions.cs | 80 +++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 79e602c5b..ecf15459b 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -2,6 +2,7 @@ ## Unreleased - logging additions (.NET Version and timestamps) for better debugging (#1796 via philon-msft) +- add: `Condition` API (transactions) now supports `StreamLengthEqual` and variants (#1807 via AlphaGremlin) ## 2.2.62 diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs index b775cde02..c154ed0f8 100644 --- a/src/StackExchange.Redis/Condition.cs +++ b/src/StackExchange.Redis/Condition.cs @@ -332,6 +332,27 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value) /// The number of members which sorted set must not have. public static Condition SortedSetScoreNotExists(RedisKey key, RedisValue score, RedisValue count) => new SortedSetScoreCondition(key, score, false, count); + /// + /// Enforces that the given stream length is a certain value + /// + /// The key of the stream to check. + /// The length the stream must have. + public static Condition StreamLengthEqual(RedisKey key, long length) => new LengthCondition(key, RedisType.Stream, 0, length); + + /// + /// Enforces that the given stream length is less than a certain value + /// + /// The key of the stream to check. + /// The length the stream must be less than. + public static Condition StreamLengthLessThan(RedisKey key, long length) => new LengthCondition(key, RedisType.Stream, 1, length); + + /// + /// Enforces that the given stream length is greater than a certain value + /// + /// The key of the stream to check. + /// The length the stream must be greater than. + public static Condition StreamLengthGreaterThan(RedisKey key, long length) => new LengthCondition(key, RedisType.Stream, -1, length); + #pragma warning restore RCS1231 internal abstract void CheckCommands(CommandMap commandMap); @@ -675,6 +696,7 @@ public LengthCondition(in RedisKey key, RedisType type, int compareToResult, lon RedisType.Set => RedisCommand.SCARD, RedisType.List => RedisCommand.LLEN, RedisType.SortedSet => RedisCommand.ZCARD, + RedisType.Stream => RedisCommand.XLEN, RedisType.String => RedisCommand.STRLEN, _ => throw new ArgumentException($"Type {type} isn't recognized", nameof(type)), }; diff --git a/tests/StackExchange.Redis.Tests/Transactions.cs b/tests/StackExchange.Redis.Tests/Transactions.cs index 018b384c0..7e83282c5 100644 --- a/tests/StackExchange.Redis.Tests/Transactions.cs +++ b/tests/StackExchange.Redis.Tests/Transactions.cs @@ -1078,6 +1078,86 @@ public async Task BasicTranWithListLengthCondition(string value, ComparisonType } } + [Theory] + [InlineData("five", ComparisonType.Equal, 5L, false)] + [InlineData("four", ComparisonType.Equal, 4L, true)] + [InlineData("three", ComparisonType.Equal, 3L, false)] + [InlineData("", ComparisonType.Equal, 2L, false)] + [InlineData("", ComparisonType.Equal, 0L, true)] + + [InlineData("five", ComparisonType.LessThan, 5L, true)] + [InlineData("four", ComparisonType.LessThan, 4L, false)] + [InlineData("three", ComparisonType.LessThan, 3L, false)] + [InlineData("", ComparisonType.LessThan, 2L, true)] + [InlineData("", ComparisonType.LessThan, 0L, false)] + + [InlineData("five", ComparisonType.GreaterThan, 5L, false)] + [InlineData("four", ComparisonType.GreaterThan, 4L, false)] + [InlineData("three", ComparisonType.GreaterThan, 3L, true)] + [InlineData("", ComparisonType.GreaterThan, 2L, false)] + [InlineData("", ComparisonType.GreaterThan, 0L, false)] + public async Task BasicTranWithStreamLengthCondition(string value, ComparisonType type, long length, bool expectTranResult) + { + using (var muxer = Create()) + { + Skip.IfMissingFeature(muxer, nameof(RedisFeatures.Streams), r => r.Streams); + + RedisKey key = Me(), key2 = Me() + "2"; + var db = muxer.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + + var expectSuccess = false; + Condition condition = null; + var valueLength = value?.Length ?? 0; + switch (type) + { + case ComparisonType.Equal: + expectSuccess = valueLength == length; + condition = Condition.StreamLengthEqual(key2, length); + break; + case ComparisonType.GreaterThan: + expectSuccess = valueLength > length; + condition = Condition.StreamLengthGreaterThan(key2, length); + break; + case ComparisonType.LessThan: + expectSuccess = valueLength < length; + condition = Condition.StreamLengthLessThan(key2, length); + break; + } + RedisValue fieldName = "Test"; + for (var i = 0; i < valueLength; i++) + { + db.StreamAdd(key2, fieldName, i, flags: CommandFlags.FireAndForget); + } + Assert.False(db.KeyExists(key)); + Assert.Equal(valueLength, db.StreamLength(key2)); + + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(condition); + var push = tran.StringSetAsync(key, "any value"); + var exec = tran.ExecuteAsync(); + var get = db.StringLength(key); + + Assert.Equal(expectTranResult, await exec); + + if (expectSuccess) + { + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.True(await push); // eq: push + Assert.Equal("any value".Length, get); // eq: get + } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push + Assert.Equal(0, get); // neq: get + } + } + } + [Fact] public async Task BasicTran() { From 56ac9a21174684f30b8aa333f8069fdca77ed4e7 Mon Sep 17 00:00:00 2001 From: Philo Date: Mon, 26 Jul 2021 05:13:22 -0700 Subject: [PATCH 017/435] Add integration points for ConfigurationOptions customizers (#1809) Enables the use of extensions that customize ConfigurationOptions settings to tune behavior or add new capabilities. As proposed in #1801 Adds: - New overloads of the ConnectionMultiplexer.Connect() and ConnectAsync() methods, that take an additional Action parameter to customize the ConfigurationOptions - A new ConfigurationOptions.Apply() method to apply the same type of customizer directly to a ConfigurationOptions instance, before connecting Co-authored-by: Nick Craver --- .../ConfigurationOptions.cs | 11 ++++ .../ConnectionMultiplexer.cs | 61 +++++++++++-------- tests/StackExchange.Redis.Tests/Config.cs | 30 +++++++++ 3 files changed, 78 insertions(+), 24 deletions(-) diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index c6023d008..ad343e598 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -472,6 +472,17 @@ public ConfigurationOptions Clone() return options; } + /// + /// Apply settings to configure this instance of , e.g. for a specific scenario. + /// + /// An action that will update the properties of this instance. + /// This instance, with any changes made. + public ConfigurationOptions Apply(Action configure) + { + configure?.Invoke(this); + return this; + } + /// /// Resolve the default port for any endpoints that did not have a port explicitly specified /// diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index a37f1ea2a..a68499d4d 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -834,14 +834,35 @@ internal ServerEndPoint AnyConnected(ServerType serverType, uint startOffset, Re internal bool IsDisposed => _isDisposed; /// - /// Create a new ConnectionMultiplexer instance + /// Create a new ConnectionMultiplexer instance. /// /// The string configuration to use for this multiplexer. /// The to log to. - public static Task ConnectAsync(string configuration, TextWriter log = null) + public static Task ConnectAsync(string configuration, TextWriter log = null) => + ConnectAsync(ConfigurationOptions.Parse(configuration), log); + + /// + /// Create a new ConnectionMultiplexer instance. + /// + /// The string configuration to use for this multiplexer. + /// Action to further modify the parsed configuration options. + /// The to log to. + public static Task ConnectAsync(string configuration, Action configure, TextWriter log = null) => + ConnectAsync(ConfigurationOptions.Parse(configuration).Apply(configure), log); + + /// + /// Create a new ConnectionMultiplexer instance. + /// + /// The configuration options to use for this multiplexer. + /// The to log to. + public static Task ConnectAsync(ConfigurationOptions configuration, TextWriter log = null) { SocketConnection.AssertDependencies(); - return ConnectAsync(ConfigurationOptions.Parse(configuration), log); + + if (IsSentinel(configuration)) + return SentinelMasterConnectAsync(configuration, log); + + return ConnectImplAsync(PrepareConfig(configuration), log); } private static async Task ConnectImplAsync(ConfigurationOptions configuration, TextWriter log = null) @@ -881,21 +902,6 @@ private static async Task ConnectImplAsync(ConfigurationO } } - /// - /// Create a new ConnectionMultiplexer instance - /// - /// The configuration options to use for this multiplexer. - /// The to log to. - public static Task ConnectAsync(ConfigurationOptions configuration, TextWriter log = null) - { - SocketConnection.AssertDependencies(); - - if (IsSentinel(configuration)) - return SentinelMasterConnectAsync(configuration, log); - - return ConnectImplAsync(PrepareConfig(configuration), log); - } - private static bool IsSentinel(ConfigurationOptions configuration) { return !string.IsNullOrEmpty(configuration?.ServiceName); @@ -1009,17 +1015,24 @@ private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions conf } /// - /// Create a new ConnectionMultiplexer instance + /// Create a new ConnectionMultiplexer instance. /// /// The string configuration to use for this multiplexer. /// The to log to. - public static ConnectionMultiplexer Connect(string configuration, TextWriter log = null) - { - return Connect(ConfigurationOptions.Parse(configuration), log); - } + public static ConnectionMultiplexer Connect(string configuration, TextWriter log = null) => + Connect(ConfigurationOptions.Parse(configuration), log); + + /// + /// Create a new ConnectionMultiplexer instance. + /// + /// The string configuration to use for this multiplexer. + /// Action to further modify the parsed configuration options. + /// The to log to. + public static ConnectionMultiplexer Connect(string configuration, Action configure, TextWriter log = null) => + Connect(ConfigurationOptions.Parse(configuration).Apply(configure), log); /// - /// Create a new ConnectionMultiplexer instance + /// Create a new ConnectionMultiplexer instance. /// /// The configuration options to use for this multiplexer. /// The to log to. diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index a6f34a9f3..553f3d6f7 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -491,5 +491,35 @@ public void ConfigStringInvalidOptionErrorGiveMeaningfulMessages() Assert.StartsWith("Keyword 'flibble' is not supported.", ex.Message); // param name gets concatenated sometimes Assert.Equal("flibble", ex.ParamName); } + + + [Fact] + public void NullApply() + { + var options = ConfigurationOptions.Parse("127.0.0.1,name=FooApply"); + Assert.Equal("FooApply", options.ClientName); + + // Doesn't go boom + var result = options.Apply(null); + Assert.Equal("FooApply", options.ClientName); + Assert.Equal(result, options); + } + + [Fact] + public void Apply() + { + var options = ConfigurationOptions.Parse("127.0.0.1,name=FooApply"); + Assert.Equal("FooApply", options.ClientName); + + var randomName = Guid.NewGuid().ToString(); + var result = options.Apply(options => + { + options.ClientName = randomName; + }); + + Assert.Equal(randomName, options.ClientName); + Assert.Equal(randomName, result.ClientName); + Assert.Equal(result, options); + } } } From c3d66f8cdef462b08437362722758810459bc5b3 Mon Sep 17 00:00:00 2001 From: Avital Fine <79420960+AvitalFineRedis@users.noreply.github.com> Date: Wed, 18 Aug 2021 15:28:49 +0300 Subject: [PATCH 018/435] Support on json index (#1808) * add As to fields * add On Json API to client * add fieldName class * API * syntax * test json index definition (ON JSON) * test create with fields name (as) * test return with field names (return as) * remove redundant using * Update FieldName.cs --- src/NRediSearch/Client.cs | 42 ++++---- src/NRediSearch/Document.cs | 8 +- src/NRediSearch/FieldName.cs | 47 +++++++++ src/NRediSearch/Query.cs | 46 ++++++--- src/NRediSearch/Schema.cs | 94 +++++++++++++++++- .../ClientTests/ClientTest.cs | 98 ++++++++++++++++++- tests/NRediSearch.Test/QueryTest.cs | 11 +++ 7 files changed, 306 insertions(+), 40 deletions(-) create mode 100644 src/NRediSearch/FieldName.cs diff --git a/src/NRediSearch/Client.cs b/src/NRediSearch/Client.cs index effd08e34..c405f213d 100644 --- a/src/NRediSearch/Client.cs +++ b/src/NRediSearch/Client.cs @@ -55,13 +55,14 @@ public sealed class IndexDefinition public enum IndexType { /// - /// Used to indicates that the index should follow the keys of type Hash changes + /// Used to indicate that the index should follow the keys of type Hash changes /// - Hash + Hash, + Json } - internal readonly IndexType _type = IndexType.Hash; - internal readonly bool _async; + internal readonly IndexType _type; + internal readonly bool _async; internal readonly string[] _prefixes; internal readonly string _filter; internal readonly string _languageField; @@ -71,9 +72,10 @@ public enum IndexType internal readonly string _payloadField; public IndexDefinition(bool async = false, string[] prefixes = null, - string filter = null, string languageField = null, string language = null, - string scoreFiled = null, double score = 1.0, string payloadField = null) + string filter = null, string languageField = null, string language = null, + string scoreFiled = null, double score = 1.0, string payloadField = null, IndexType type = IndexType.Hash) { + _type = type; _async = async; _prefixes = prefixes; _filter = filter; @@ -92,36 +94,36 @@ internal void SerializeRedisArgs(List args) { args.Add("ASYNC".Literal()); } - if (_prefixes?.Length > 0) + if (_prefixes?.Length > 0) { args.Add("PREFIX".Literal()); args.Add(_prefixes.Length.ToString()); args.AddRange(_prefixes); } - if (_filter != null) + if (_filter != null) { args.Add("FILTER".Literal()); args.Add(_filter); - } + } if (_languageField != null) { args.Add("LANGUAGE_FIELD".Literal()); - args.Add(_languageField); - } + args.Add(_languageField); + } if (_language != null) { args.Add("LANGUAGE".Literal()); - args.Add(_language); - } + args.Add(_language); + } if (_scoreFiled != null) { args.Add("SCORE_FIELD".Literal()); - args.Add(_scoreFiled); - } + args.Add(_scoreFiled); + } if (_score != 1.0) { args.Add("SCORE".Literal()); - args.Add(_score.ToString()); + args.Add(_score.ToString()); } if (_payloadField != null) { args.Add("PAYLOAD_FIELD".Literal()); - args.Add(_payloadField); + args.Add(_payloadField); } } @@ -142,7 +144,7 @@ public ConfiguredIndexOptions(IndexOptions options = IndexOptions.Default) _options = options; } - public ConfiguredIndexOptions(IndexDefinition definition, IndexOptions options = IndexOptions.Default) + public ConfiguredIndexOptions(IndexDefinition definition, IndexOptions options = IndexOptions.Default) : this(options) { _definition = definition; @@ -687,7 +689,7 @@ public async Task DeleteDocumentAsync(string docId, bool deleteDocument = } /// - /// Delete multiple documents from an index. + /// Delete multiple documents from an index. /// /// if true also deletes the actual document ifs it is in the index /// the document ids to delete @@ -705,7 +707,7 @@ public bool[] DeleteDocuments(bool deleteDocuments, params string[] docIds) } /// - /// Delete multiple documents from an index. + /// Delete multiple documents from an index. /// /// if true also deletes the actual document ifs it is in the index /// the document ids to delete diff --git a/src/NRediSearch/Document.cs b/src/NRediSearch/Document.cs index f7118e572..c60359808 100644 --- a/src/NRediSearch/Document.cs +++ b/src/NRediSearch/Document.cs @@ -37,7 +37,13 @@ public static Document Load(string id, double score, byte[] payload, RedisValue[ { for (int i = 0; i < fields.Length; i += 2) { - ret[(string)fields[i]] = fields[i + 1]; + string fieldName = (string)fields[i]; + if (fieldName == "$") { + ret["json"] = fields[i + 1]; + } + else { + ret[fieldName] = fields[i + 1]; + } } } return ret; diff --git a/src/NRediSearch/FieldName.cs b/src/NRediSearch/FieldName.cs new file mode 100644 index 000000000..4d1c5bd0c --- /dev/null +++ b/src/NRediSearch/FieldName.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; + +namespace NRediSearch +{ + public class FieldName { + private readonly string name; + private string attribute; + + public FieldName(string name) : this(name, null) { + + } + + public FieldName(string name, string attribute) { + this.name = name; + this.attribute = attribute; + } + + public int AddCommandArguments(List args) { + args.Add(name); + if (attribute == null) { + return 1; + } + + args.Add("AS".Literal()); + args.Add(attribute); + return 3; + } + + public static FieldName Of(string name) { + return new FieldName(name); + } + + public FieldName As(string attribute) { + this.attribute = attribute; + return this; + } + + public static FieldName[] convert(params string[] names) { + if (names == null) return null; + FieldName[] fields = new FieldName[names.Length]; + for (int i = 0; i < names.Length; i++) + fields[i] = FieldName.Of(names[i]); + + return fields; + } + } +} diff --git a/src/NRediSearch/Query.cs b/src/NRediSearch/Query.cs index 6f9c9fdff..4e5e35e51 100644 --- a/src/NRediSearch/Query.cs +++ b/src/NRediSearch/Query.cs @@ -12,7 +12,7 @@ namespace NRediSearch public sealed class Query { /// - /// Filter represents a filtering rules in a query + /// Filter represents a filtering rules in a query /// public abstract class Filter { @@ -63,7 +63,7 @@ static RedisValue FormatNum(double num, bool exclude) } /// - /// GeoFilter encapsulates a radius filter on a geographical indexed fields + /// GeoFilter encapsulates a radius filter on a geographical indexed fields /// public class GeoFilter : Filter { @@ -102,17 +102,17 @@ public Paging(int offset, int count) } /// - /// The query's filter list. We only support AND operation on all those filters + /// The query's filter list. We only support AND operation on all those filters /// internal readonly List _filters = new List(); /// - /// The textual part of the query + /// The textual part of the query /// public string QueryString { get; } /// - /// The sorting parameters + /// The sorting parameters /// internal Paging _paging = new Paging(0, 10); @@ -151,6 +151,7 @@ public Paging(int offset, int count) internal string[] _fields = null; internal string[] _keys = null; internal string[] _returnFields = null; + internal FieldName[] _returnFieldsNames = null; /// /// Set the query payload to be evaluated by the scoring function /// @@ -230,6 +231,17 @@ internal void SerializeRedisArgs(List args) args.Add(_returnFields.Length.Boxed()); args.AddRange(_returnFields); } + else if (_returnFieldsNames?.Length > 0) + { + args.Add("RETURN".Literal()); + int returnCountIndex = args.Count; + int returnCount = 0; + foreach (FieldName fn in _returnFieldsNames) { + returnCount += fn.AddCommandArguments(args); + } + + args.Insert(returnCountIndex, returnCount); + } if (SortBy != null) { @@ -329,17 +341,6 @@ internal void SerializeRedisArgs(List args) args.Add(key); } } - - if (_returnFields != null && _returnFields.Length > 0) - { - args.Add("RETURN".Literal()); - args.Add(_returnFields.Length.Boxed()); - - foreach (var returnField in _returnFields) - { - args.Add(returnField); - } - } } /// @@ -395,6 +396,19 @@ public Query LimitKeys(params string[] keys) public Query ReturnFields(params string[] fields) { _returnFields = fields; + _returnFieldsNames = null; + return this; + } + + /// + /// Result's projection - the fields to return by the query + /// + /// field a list of TEXT fields in the schemas + /// the query object itself + public Query ReturnFields(params FieldName[] fields) + { + _returnFields = null; + _returnFieldsNames = fields; return this; } diff --git a/src/NRediSearch/Schema.cs b/src/NRediSearch/Schema.cs index ee9561aec..53bf8e70d 100644 --- a/src/NRediSearch/Schema.cs +++ b/src/NRediSearch/Schema.cs @@ -21,14 +21,21 @@ public enum FieldType public class Field { + public FieldName FieldName { get; } public string Name { get; } public FieldType Type { get; } public bool Sortable { get; } public bool NoIndex { get; } internal Field(string name, FieldType type, bool sortable, bool noIndex = false) + : this(FieldName.Of(name), type, sortable, noIndex) { Name = name; + } + + internal Field(FieldName name, FieldType type, bool sortable, bool noIndex = false) + { + FieldName = name; Type = type; Sortable = sortable; NoIndex = noIndex; @@ -44,7 +51,7 @@ internal virtual void SerializeRedisArgs(List args) FieldType.Tag => "TAG".Literal(), _ => throw new ArgumentOutOfRangeException(nameof(type)), }; - args.Add(Name); + FieldName.AddCommandArguments(args); args.Add(GetForRedis(Type)); if (Sortable) { args.Add("SORTABLE".Literal()); } if (NoIndex) { args.Add("NOINDEX".Literal()); } @@ -56,7 +63,15 @@ public class TextField : Field public double Weight { get; } public bool NoStem { get; } - public TextField(string name, double weight = 1.0, bool sortable = false, bool noStem = false, bool noIndex = false) : base(name, FieldType.FullText, sortable, noIndex) + public TextField(string name, double weight = 1.0, bool sortable = false, bool noStem = false, bool noIndex = false) + : base(name, FieldType.FullText, sortable, noIndex) + { + Weight = weight; + NoStem = noStem; + } + + public TextField(FieldName name, double weight = 1.0, bool sortable = false, bool noStem = false, bool noIndex = false) + : base(name, FieldType.FullText, sortable, noIndex) { Weight = weight; NoStem = noStem; @@ -99,6 +114,18 @@ public Schema AddTextField(string name, double weight = 1.0) return this; } + /// + /// Add a text field to the schema with a given weight. + /// + /// The field's name. + /// Its weight, a positive floating point number. + /// The object. + public Schema AddTextField(FieldName name, double weight = 1.0) + { + Fields.Add(new TextField(name, weight)); + return this; + } + /// /// Add a text field that can be sorted on. /// @@ -111,6 +138,18 @@ public Schema AddSortableTextField(string name, double weight = 1.0) return this; } + /// + /// Add a text field that can be sorted on. + /// + /// The field's name. + /// Its weight, a positive floating point number. + /// The object. + public Schema AddSortableTextField(FieldName name, double weight = 1.0) + { + Fields.Add(new TextField(name, weight, true)); + return this; + } + /// /// Add a numeric field to the schema. /// @@ -122,6 +161,17 @@ public Schema AddGeoField(string name) return this; } + /// + /// Add a numeric field to the schema. + /// + /// The field's name. + /// The object. + public Schema AddGeoField(FieldName name) + { + Fields.Add(new Field(name, FieldType.Geo, false)); + return this; + } + /// /// Add a numeric field to the schema. /// @@ -133,6 +183,17 @@ public Schema AddNumericField(string name) return this; } + /// + /// Add a numeric field to the schema. + /// + /// The field's name. + /// The object. + public Schema AddNumericField(FieldName name) + { + Fields.Add(new Field(name, FieldType.Numeric, false)); + return this; + } + /// /// Add a numeric field that can be sorted on. /// @@ -144,14 +205,31 @@ public Schema AddSortableNumericField(string name) return this; } + /// + /// Add a numeric field that can be sorted on. + /// + /// The field's name. + /// The object. + public Schema AddSortableNumericField(FieldName name) + { + Fields.Add(new Field(name, FieldType.Numeric, true)); + return this; + } + public class TagField : Field { public string Separator { get; } + internal TagField(string name, string separator = ",") : base(name, FieldType.Tag, false) { Separator = separator; } + internal TagField(FieldName name, string separator = ",") : base(name, FieldType.Tag, false) + { + Separator = separator; + } + internal override void SerializeRedisArgs(List args) { base.SerializeRedisArgs(args); @@ -174,5 +252,17 @@ public Schema AddTagField(string name, string separator = ",") Fields.Add(new TagField(name, separator)); return this; } + + /// + /// Add a TAG field. + /// + /// The field's name. + /// The tag separator. + /// The object. + public Schema AddTagField(FieldName name, string separator = ",") + { + Fields.Add(new TagField(name, separator)); + return this; + } } } diff --git a/tests/NRediSearch.Test/ClientTests/ClientTest.cs b/tests/NRediSearch.Test/ClientTests/ClientTest.cs index 629874ee9..afb23d366 100644 --- a/tests/NRediSearch.Test/ClientTests/ClientTest.cs +++ b/tests/NRediSearch.Test/ClientTests/ClientTest.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Collections.Generic; using System.Text; using StackExchange.Redis; using Xunit; @@ -892,5 +893,100 @@ public void TestInKeys() Assert.Equal("value", res.Documents[0]["field1"]); Assert.Null((string)res.Documents[0]["value"]); } + + [Fact] + public void TestWithFieldNames() + { + Client cl = GetClient(); + IndexDefinition defenition = new IndexDefinition(prefixes: new string[] {"student:", "pupil:"}); + Schema sc = new Schema().AddTextField(FieldName.Of("first").As("given")).AddTextField(FieldName.Of("last")); + Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions(defenition))); + + var docsIds = new string[] {"student:111", "pupil:222", "student:333", "teacher:333"}; + var docsData = new Dictionary[] { + new Dictionary { + { "first", "Joen" }, + { "last", "Ko" }, + { "age", "20" } + }, + new Dictionary { + { "first", "Joe" }, + { "last", "Dod" }, + { "age", "18" } + }, + new Dictionary { + { "first", "El" }, + { "last", "Mark" }, + { "age", "17" } + }, + new Dictionary { + { "first", "Pat" }, + { "last", "Rod" }, + { "age", "20" } + } + }; + + for (int i = 0; i < docsIds.Length; i++) { + Assert.True(cl.AddDocument(docsIds[i], docsData[i])); + } + + // Query + SearchResult noFilters = cl.Search(new Query("*")); + Assert.Equal(3, noFilters.TotalResults); + Assert.Equal("student:111", noFilters.Documents[0].Id); + Assert.Equal("pupil:222", noFilters.Documents[1].Id); + Assert.Equal("student:333", noFilters.Documents[2].Id); + + SearchResult asOriginal = cl.Search(new Query("@first:Jo*")); + Assert.Equal(0, asOriginal.TotalResults); + + SearchResult asAttribute = cl.Search(new Query("@given:Jo*")); + Assert.Equal(2, asAttribute.TotalResults); + Assert.Equal("student:111", noFilters.Documents[0].Id); + Assert.Equal("pupil:222", noFilters.Documents[1].Id); + + SearchResult nonAttribute = cl.Search(new Query("@last:Rod")); + Assert.Equal(0, nonAttribute.TotalResults); + } + + [Fact] + public void TestReturnWithFieldNames(){ + Client cl = GetClient(); + Schema sc = new Schema().AddTextField("a").AddTextField("b").AddTextField("c"); + Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); + + var doc = new Dictionary + { + { "a", "value1" }, + { "b", "value2" }, + { "c", "value3" } + }; + Assert.True(cl.AddDocument("doc", doc)); + + // Query + SearchResult res = cl.Search(new Query("*").ReturnFields(FieldName.Of("b").As("d"), FieldName.Of("a"))); + Assert.Equal(1, res.TotalResults); + Assert.Equal("doc", res.Documents[0].Id); + Assert.Equal("value1", res.Documents[0]["a"]); + Assert.Equal("value2", res.Documents[0]["d"]); + } + + [Fact] + public void TestJsonIndex() + { + Client cl = GetClient(); + IndexDefinition defenition = new IndexDefinition(prefixes: new string[] {"king:"} ,type: IndexDefinition.IndexType.Json); + Schema sc = new Schema().AddTextField("$.name"); + Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions(defenition))); + + Db.Execute("JSON.SET", "king:1", ".", "{\"name\": \"henry\"}"); + Db.Execute("JSON.SET", "king:2", ".", "{\"name\": \"james\"}"); + + // Query + SearchResult res = cl.Search(new Query("henry")); + Assert.Equal(1, res.TotalResults); + Assert.Equal("king:1", res.Documents[0].Id); + Assert.Equal("{\"name\":\"henry\"}", res.Documents[0]["json"]); + } } } diff --git a/tests/NRediSearch.Test/QueryTest.cs b/tests/NRediSearch.Test/QueryTest.cs index 53e033202..59ed64b6b 100644 --- a/tests/NRediSearch.Test/QueryTest.cs +++ b/tests/NRediSearch.Test/QueryTest.cs @@ -130,6 +130,17 @@ public void ReturnFields() Assert.Equal(2, query._returnFields.Length); } + + [Fact] + public void ReturnFieldNames() + { + var query = GetQuery(); + + Assert.Null(query._returnFieldsNames); + Assert.Same(query, query.ReturnFields(FieldName.Of("foo").As("bar"), FieldName.Of("foofoo"))); + Assert.Equal(2, query._returnFieldsNames.Length); + } + [Fact] public void HighlightFields() { From 25a0ba802272832155b194ac2915bcb19abfce3e Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 18 Aug 2021 08:32:46 -0400 Subject: [PATCH 019/435] Socket available bytes check: add a disposal guard (#1836) This prevents a disposal race from killing us in connection phases and such, protected against in the counters above in Unofficial (see https://github.com/mgravell/Pipelines.Sockets.Unofficial/blob/19b141f30fe2485dd79993be0ac87a9fc6b34b74/src/Pipelines.Sockets.Unofficial/SocketConnection.cs#L235) but not here - adding the same protection guard. Stack trace of such an error: ``` ----- Inner Stack Trace ----- at System.Runtime.InteropServices.SafeHandle.DangerousAddRef(Boolean& success) at System.StubHelpers.StubHelpers.SafeHandleAddRef(SafeHandle pHandle, Boolean& success) at Interop.Winsock.ioctlsocket(SafeSocketHandle socketHandle, Int32 cmd, Int32& argp) at System.Net.Sockets.SocketPal.GetAvailable(SafeSocketHandle handle, Int32& available) at System.Net.Sockets.Socket.get_Available() at StackExchange.Redis.PhysicalConnection.GetSocketBytes(Int64& readCount, Int64& writeCount) in /_/src/StackExchange.Redis/PhysicalConnection.cs:line 1250 at StackExchange.Redis.PhysicalBridge.GetOutstandingCount(Int32& inst, Int32& qs, Int64& in, Int32& qu, Boolean& aw, Int64& toRead, Int64& toWrite, BacklogStatus& bs, ReadStatus& rs, WriteStatus& ws) in /_/src/StackExchange.Redis/PhysicalBridge.cs:line 304 at StackExchange.Redis.ConnectionMultiplexer.ReconfigureAsync(Boolean first, Boolean reconfigureAll, LogProxy log, EndPoint blame, String cause, Boolean publishReconfigure, CommandFlags publishReconfigureFlags) in /_/src/StackExchange.Redis/ConnectionMultiplexer.cs:line 1748 ``` --- src/StackExchange.Redis/PhysicalConnection.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 21e0a10ff..bbbccf4e3 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -1249,7 +1249,15 @@ internal long GetSocketBytes(out long readCount, out long writeCount) return counters.BytesAvailableOnSocket; } readCount = writeCount = -1; - return VolatileSocket?.Available ?? -1; + try + { + return VolatileSocket?.Available ?? -1; + } + catch + { + // If this fails, we're likely in a race disposal situation and do not want to blow sky high here. + return -1; + } } private static RemoteCertificateValidationCallback GetAmbientIssuerCertificateCallback() From 89812faf3fe19cda7ed085238b28adae58931dd1 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 18 Aug 2021 13:44:56 +0100 Subject: [PATCH 020/435] fix breaking changes in NRediSearch (#1837) * fox .ctor breal (avoid MME) fix "filed" typo (except for .ctor break) * update release notes --- docs/ReleaseNotes.md | 2 ++ src/NRediSearch/Client.cs | 16 +++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index ecf15459b..8adc582bb 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,6 +1,8 @@ # Release Notes ## Unreleased +- NRediSearch: Support on json index (#1808 via AvitalFineRedis) +- fix potential errors getting socket bytes (#1836 via NickCraver) - logging additions (.NET Version and timestamps) for better debugging (#1796 via philon-msft) - add: `Condition` API (transactions) now supports `StreamLengthEqual` and variants (#1807 via AlphaGremlin) diff --git a/src/NRediSearch/Client.cs b/src/NRediSearch/Client.cs index c405f213d..26d106b65 100644 --- a/src/NRediSearch/Client.cs +++ b/src/NRediSearch/Client.cs @@ -67,13 +67,19 @@ public enum IndexType internal readonly string _filter; internal readonly string _languageField; internal readonly string _language; - internal readonly string _scoreFiled; + internal readonly string _scoreField; internal readonly double _score; internal readonly string _payloadField; + // this .ctor is left here to avoid MME in existing code (i.e. back-compat) + public IndexDefinition(bool async, string[] prefixes, + string filter, string languageField, string language, + string scoreFiled, double score, string payloadField) + : this(async, prefixes, filter, languageField, language, scoreFiled, score, payloadField, IndexType.Hash) + { } public IndexDefinition(bool async = false, string[] prefixes = null, string filter = null, string languageField = null, string language = null, - string scoreFiled = null, double score = 1.0, string payloadField = null, IndexType type = IndexType.Hash) + string scoreField = null, double score = 1.0, string payloadField = null, IndexType type = IndexType.Hash) { _type = type; _async = async; @@ -81,7 +87,7 @@ public IndexDefinition(bool async = false, string[] prefixes = null, _filter = filter; _languageField = languageField; _language = language; - _scoreFiled = scoreFiled; + _scoreField = scoreField; _score = score; _payloadField = payloadField; } @@ -113,9 +119,9 @@ internal void SerializeRedisArgs(List args) args.Add("LANGUAGE".Literal()); args.Add(_language); } - if (_scoreFiled != null) { + if (_scoreField != null) { args.Add("SCORE_FIELD".Literal()); - args.Add(_scoreFiled); + args.Add(_scoreField); } if (_score != 1.0) { args.Add("SCORE".Literal()); From 04126aa633974f6ba0cca2ccc6abc6bda37e82dc Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 24 Aug 2021 20:17:03 +0100 Subject: [PATCH 021/435] Add a backlog status that tracks non-activated backlogs (#1845) Part of #1847, giving better debug information when this happens. --- src/StackExchange.Redis/PhysicalBridge.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index ea3883c0a..64cf9cff1 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -765,10 +765,11 @@ private void StartBacklogProcessor() { if (Interlocked.CompareExchange(ref _backlogProcessorIsRunning, 1, 0) == 0) { - + #if DEBUG _backlogProcessorRequestedTime = Environment.TickCount; #endif + _backlogStatus = BacklogStatus.Activating; Task.Run(ProcessBacklogAsync); } } @@ -811,6 +812,7 @@ private void CheckBacklogForTimeouts() // check the head of the backlog queue, c internal enum BacklogStatus : byte { Inactive, + Activating, Starting, Started, CheckingForWork, From 7e5c219354d5e039604bf066ec0b54754b8addb5 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 2 Sep 2021 13:42:27 +0100 Subject: [PATCH 022/435] Use a dedicated thread, rather than the thread-pool, for the backlog processor (#1854) Pain point in the last release was a spiral of death triggered (or at least: exacerbated) by the backlog processor --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/PhysicalBridge.cs | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 8adc582bb..6cdaeca51 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -5,6 +5,7 @@ - fix potential errors getting socket bytes (#1836 via NickCraver) - logging additions (.NET Version and timestamps) for better debugging (#1796 via philon-msft) - add: `Condition` API (transactions) now supports `StreamLengthEqual` and variants (#1807 via AlphaGremlin) +- fix potential task/thread exhaustion from the backlog processor (#1854 via mgravell) ## 2.2.62 diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 64cf9cff1..561bd6e9b 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -770,7 +770,17 @@ private void StartBacklogProcessor() _backlogProcessorRequestedTime = Environment.TickCount; #endif _backlogStatus = BacklogStatus.Activating; - Task.Run(ProcessBacklogAsync); + + // start the backlog processor; this is a bit unorthadox, as you would *expect* this to just + // be Task.Run; that would work fine when healthy, but when we're falling on our face, it is + // easy to get into a thread-pool-starvation "spiral of death" if we rely on the thread-pool + // to unblock the thread-pool when there could be sync-over-async callers. Note that in reality, + // the initial "enough" of the back-log processor is typically sync, which means that the thread + // we start is actually useful, despite thinking "but that will just go async and back to the pool" + var thread = new Thread(s => ((PhysicalBridge)s).ProcessBacklogAsync().RedisFireAndForget()); + thread.IsBackground = true; // don't keep process alive (also: act like the thread-pool used to) + thread.Name = "redisbacklog"; // help anyone looking at thread-dumps + thread.Start(this); } } #if DEBUG From 7509952cbfce9e5b1c6b8d73f5b3f05e95444a71 Mon Sep 17 00:00:00 2001 From: Chad <1760475+felickz@users.noreply.github.com> Date: Thu, 23 Sep 2021 13:16:18 -0400 Subject: [PATCH 023/435] ExponentialRetry - Comment typos (#1867) Fixing comments --- src/StackExchange.Redis/ExponentialRetry.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/ExponentialRetry.cs b/src/StackExchange.Redis/ExponentialRetry.cs index ddda9c702..cb51491ca 100644 --- a/src/StackExchange.Redis/ExponentialRetry.cs +++ b/src/StackExchange.Redis/ExponentialRetry.cs @@ -22,7 +22,7 @@ public ExponentialRetry(int deltaBackOffMilliseconds) : this(deltaBackOffMillise /// Initializes a new instance using the specified back off interval. /// /// time in milliseconds for the back-off interval between retries - /// time in milliseconds for the maximum value that the back-off interval can exponentailly grow upto + /// time in milliseconds for the maximum value that the back-off interval can exponentially grow up to public ExponentialRetry(int deltaBackOffMilliseconds, int maxDeltaBackOffMilliseconds) { this.deltaBackOffMilliseconds = deltaBackOffMilliseconds; From 8775036dcdcc86b16f5b3f84957eeaf802aebf2b Mon Sep 17 00:00:00 2001 From: Stephen Lorello <42971704+slorello89@users.noreply.github.com> Date: Fri, 1 Oct 2021 09:31:23 -0400 Subject: [PATCH 024/435] Adding sortable tag field support & UNF support (#1862) * Adding sortable tag field support * UNF * move unf to base class * Check that UNF can't be given to non-sortable filed * Check that UNF can't be given to non-sortable filed * enforcing correct ordering of UNF & Sortable options * incorporating Marc's suggestions Co-authored-by: AvitalFineRedis --- docs/ReleaseNotes.md | 1 + src/NRediSearch/Schema.cs | 92 ++++++++++++++++--- .../ClientTests/ClientTest.cs | 82 ++++++++++++++++- 3 files changed, 161 insertions(+), 14 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 6cdaeca51..97d4f1d45 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -2,6 +2,7 @@ ## Unreleased - NRediSearch: Support on json index (#1808 via AvitalFineRedis) +- NRediSearch: Support sortable TagFields and unNormalizedForm for Tag & Text Fields (#1862 via slorello89 & AvitalFineRedis) - fix potential errors getting socket bytes (#1836 via NickCraver) - logging additions (.NET Version and timestamps) for better debugging (#1796 via philon-msft) - add: `Condition` API (transactions) now supports `StreamLengthEqual` and variants (#1807 via AlphaGremlin) diff --git a/src/NRediSearch/Schema.cs b/src/NRediSearch/Schema.cs index 53bf8e70d..63d734674 100644 --- a/src/NRediSearch/Schema.cs +++ b/src/NRediSearch/Schema.cs @@ -26,19 +26,24 @@ public class Field public FieldType Type { get; } public bool Sortable { get; } public bool NoIndex { get; } + public bool Unf { get; } - internal Field(string name, FieldType type, bool sortable, bool noIndex = false) - : this(FieldName.Of(name), type, sortable, noIndex) + internal Field(string name, FieldType type, bool sortable, bool noIndex = false, bool unf = false) + : this(FieldName.Of(name), type, sortable, noIndex, unf) { Name = name; } - internal Field(FieldName name, FieldType type, bool sortable, bool noIndex = false) + internal Field(FieldName name, FieldType type, bool sortable, bool noIndex = false, bool unf = false) { FieldName = name; Type = type; Sortable = sortable; NoIndex = noIndex; + if (unf && !sortable){ + throw new ArgumentException("UNF can't be applied on a non-sortable field."); + } + Unf = unf; } internal virtual void SerializeRedisArgs(List args) @@ -54,6 +59,7 @@ internal virtual void SerializeRedisArgs(List args) FieldName.AddCommandArguments(args); args.Add(GetForRedis(Type)); if (Sortable) { args.Add("SORTABLE".Literal()); } + if (Unf) args.Add("UNF".Literal()); if (NoIndex) { args.Add("NOINDEX".Literal()); } } } @@ -63,15 +69,21 @@ public class TextField : Field public double Weight { get; } public bool NoStem { get; } - public TextField(string name, double weight = 1.0, bool sortable = false, bool noStem = false, bool noIndex = false) - : base(name, FieldType.FullText, sortable, noIndex) + public TextField(string name, double weight, bool sortable, bool noStem, bool noIndex) + : this(name, weight, sortable, noStem, noIndex, false) { } + + public TextField(string name, double weight = 1.0, bool sortable = false, bool noStem = false, bool noIndex = false, bool unNormalizedForm = false) + : base(name, FieldType.FullText, sortable, noIndex, unNormalizedForm) { Weight = weight; NoStem = noStem; } - public TextField(FieldName name, double weight = 1.0, bool sortable = false, bool noStem = false, bool noIndex = false) - : base(name, FieldType.FullText, sortable, noIndex) + public TextField(FieldName name, double weight, bool sortable, bool noStem, bool noIndex) + : this(name, weight, sortable, noStem, noIndex, false) { } + + public TextField(FieldName name, double weight = 1.0, bool sortable = false, bool noStem = false, bool noIndex = false, bool unNormalizedForm = false) + : base(name, FieldType.FullText, sortable, noIndex, unNormalizedForm) { Weight = weight; NoStem = noStem; @@ -131,10 +143,12 @@ public Schema AddTextField(FieldName name, double weight = 1.0) /// /// The field's name. /// Its weight, a positive floating point number. + /// Set this to true to prevent the indexer from sorting on the normalized form. + /// Normalied form is the field sent to lower case with all diaretics removed /// The object. - public Schema AddSortableTextField(string name, double weight = 1.0) + public Schema AddSortableTextField(string name, double weight = 1.0, bool unf = false) { - Fields.Add(new TextField(name, weight, true)); + Fields.Add(new TextField(name, weight, true, unNormalizedForm: unf)); return this; } @@ -144,12 +158,30 @@ public Schema AddSortableTextField(string name, double weight = 1.0) /// The field's name. /// Its weight, a positive floating point number. /// The object. - public Schema AddSortableTextField(FieldName name, double weight = 1.0) + public Schema AddSortableTextField(string name, double weight) => AddSortableTextField(name, weight, false); + + /// + /// Add a text field that can be sorted on. + /// + /// The field's name. + /// Its weight, a positive floating point number. + /// Set this to true to prevent the indexer from sorting on the normalized form. + /// Normalied form is the field sent to lower case with all diaretics removed + /// The object. + public Schema AddSortableTextField(FieldName name, double weight = 1.0, bool unNormalizedForm = false) { - Fields.Add(new TextField(name, weight, true)); + Fields.Add(new TextField(name, weight, true, unNormalizedForm: unNormalizedForm)); return this; } + /// + /// Add a text field that can be sorted on. + /// + /// The field's name. + /// Its weight, a positive floating point number. + /// The object. + public Schema AddSortableTextField(FieldName name, double weight) => AddSortableTextField(name, weight, false); + /// /// Add a numeric field to the schema. /// @@ -220,12 +252,14 @@ public class TagField : Field { public string Separator { get; } - internal TagField(string name, string separator = ",") : base(name, FieldType.Tag, false) + internal TagField(string name, string separator = ",", bool sortable = false, bool unNormalizedForm = false) + : base(name, FieldType.Tag, sortable, unf: unNormalizedForm) { Separator = separator; } - internal TagField(FieldName name, string separator = ",") : base(name, FieldType.Tag, false) + internal TagField(FieldName name, string separator = ",", bool sortable = false, bool unNormalizedForm = false) + : base(name, FieldType.Tag, sortable, unf: unNormalizedForm) { Separator = separator; } @@ -235,8 +269,12 @@ internal override void SerializeRedisArgs(List args) base.SerializeRedisArgs(args); if (Separator != ",") { + if (Sortable) args.Remove("SORTABLE"); + if (Unf) args.Remove("UNF"); args.Add("SEPARATOR".Literal()); args.Add(Separator); + if (Sortable) args.Add("SORTABLE".Literal()); + if (Unf) args.Add("UNF".Literal()); } } } @@ -264,5 +302,33 @@ public Schema AddTagField(FieldName name, string separator = ",") Fields.Add(new TagField(name, separator)); return this; } + + /// + /// Add a sortable TAG field. + /// + /// The field's name. + /// The tag separator. + /// Set this to true to prevent the indexer from sorting on the normalized form. + /// Normalied form is the field sent to lower case with all diaretics removed + /// The object. + public Schema AddSortableTagField(string name, string separator = ",", bool unNormalizedForm = false) + { + Fields.Add(new TagField(name, separator, sortable: true, unNormalizedForm: unNormalizedForm)); + return this; + } + + /// + /// Add a sortable TAG field. + /// + /// The field's name. + /// The tag separator. + /// Set this to true to prevent the indexer from sorting on the normalized form. + /// Normalied form is the field sent to lower case with all diaretics removed + /// The object. + public Schema AddSortableTagField(FieldName name, string separator = ",", bool unNormalizedForm = false) + { + Fields.Add(new TagField(name, separator, sortable: true, unNormalizedForm: unNormalizedForm)); + return this; + } } } diff --git a/tests/NRediSearch.Test/ClientTests/ClientTest.cs b/tests/NRediSearch.Test/ClientTests/ClientTest.cs index afb23d366..aa45297d9 100644 --- a/tests/NRediSearch.Test/ClientTests/ClientTest.cs +++ b/tests/NRediSearch.Test/ClientTests/ClientTest.cs @@ -1,9 +1,11 @@ using System.Reflection.Metadata; using System.Collections.Generic; using System.Text; +using System; using StackExchange.Redis; using Xunit; using Xunit.Abstractions; +using NRediSearch.Aggregation; using static NRediSearch.Client; using static NRediSearch.Schema; using static NRediSearch.SuggestionOptions; @@ -819,11 +821,89 @@ public void TestGetTagFieldWithNonDefaultSeparator() Assert.Equal(4, cl.Search(new Query("hello")).TotalResults); } + [Fact] + public void TestGetSortableTagField() + { + Client cl = GetClient(); + Schema sc = new Schema() + .AddTextField("title", 1.0) + .AddSortableTagField("category", ";"); + + Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); + Assert.True(cl.AddDocument("foo", new Dictionary + { + { "title", "hello world" }, + { "category", "red" } + })); + Assert.True(cl.AddDocument("bar", new Dictionary + { + { "title", "hello world" }, + { "category", "blue" } + })); + Assert.True(cl.AddDocument("baz", new Dictionary + { + { "title", "hello world" }, + { "category", "green;yellow" } + })); + Assert.True(cl.AddDocument("qux", new Dictionary + { + { "title", "hello world" }, + { "category", "orange,purple" } + })); + + var res = cl.Search(new Query("*") { SortBy = "category", SortAscending = false }); + Assert.Equal("red", res.Documents[0]["category"]); + Assert.Equal("orange,purple", res.Documents[1]["category"]); + Assert.Equal("green;yellow", res.Documents[2]["category"]); + Assert.Equal("blue", res.Documents[3]["category"]); + + Assert.Equal(1, cl.Search(new Query("@category:{red}")).TotalResults); + Assert.Equal(1, cl.Search(new Query("@category:{blue}")).TotalResults); + Assert.Equal(1, cl.Search(new Query("hello @category:{red}")).TotalResults); + Assert.Equal(1, cl.Search(new Query("hello @category:{blue}")).TotalResults); + Assert.Equal(1, cl.Search(new Query("hello @category:{yellow}")).TotalResults); + Assert.Equal(0, cl.Search(new Query("@category:{purple}")).TotalResults); + Assert.Equal(1, cl.Search(new Query("@category:{orange\\,purple}")).TotalResults); + Assert.Equal(4, cl.Search(new Query("hello")).TotalResults); + } + + [Fact] + public void TestGetTagFieldUnf() { + // Add version check + + Client cl = GetClient(); + + // Check that UNF can't be given to non-sortable filed + try { + var temp = new Schema().AddField(new TextField("non-sortable-unf", 1.0, sortable: false, unNormalizedForm: true)); + Assert.True(false); + } catch (ArgumentException) { + Assert.True(true); + } + + Schema sc = new Schema().AddSortableTextField("txt").AddSortableTextField("txt_unf", unf: true). + AddSortableTagField("tag").AddSortableTagField("tag_unf", unNormalizedForm: true); + Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); + Db.Execute("HSET", "doc1", "txt", "FOO", "txt_unf", "FOO", "tag", "FOO", "tag_unf", "FOO"); + + AggregationBuilder r = new AggregationBuilder() + .GroupBy(new List {"@txt", "@txt_unf", "@tag", "@tag_unf"}, new List {}); + + AggregationResult res = cl.Aggregate(r); + var results = res.GetResults()[0]; + Assert.NotNull(results); + Assert.Equal(4, results.Count); + Assert.Equal("foo", results["txt"]); + Assert.Equal("FOO", results["txt_unf"]); + Assert.Equal("foo", results["tag"]); + Assert.Equal("FOO", results["tag_unf"]); + } + [Fact] public void TestMultiDocuments() { Client cl = GetClient(); - Schema sc = new Schema().AddTextField("title", 1.0).AddTextField("body", 1.0); + Schema sc = new Schema().AddTextField("title").AddTextField("body"); Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); From 001dad9814b01bc2b383e3d611573d43046bc8de Mon Sep 17 00:00:00 2001 From: Avital Fine <79420960+AvitalFineRedis@users.noreply.github.com> Date: Tue, 5 Oct 2021 20:37:11 +0200 Subject: [PATCH 025/435] support NOHL SKIPINITIALSCAN, MAXTEXTFIELDS and TEMPORARY (#1841) Adds latest NRediSearch index APIs and fixes test suite against older modules. Co-authored-by: Nick Craver --- src/NRediSearch/Client.cs | 94 ++++++++++++++++++- .../ClientTests/ClientTest.cs | 92 +++++++++++++++++- 2 files changed, 179 insertions(+), 7 deletions(-) diff --git a/src/NRediSearch/Client.cs b/src/NRediSearch/Client.cs index 26d106b65..87d0d0d39 100644 --- a/src/NRediSearch/Client.cs +++ b/src/NRediSearch/Client.cs @@ -32,7 +32,7 @@ public enum IndexOptions /// /// The default indexing options - use term offsets, keep fields flags, keep term frequencies /// - Default = UseTermOffsets | KeepFieldFlags | KeepTermFrequencies, + Default = KeepFieldFlags | KeepTermFrequencies, /// /// If set, we keep an index of the top entries per term, allowing extremely fast single word queries /// regardless of index size, at the cost of more memory @@ -47,7 +47,20 @@ public enum IndexOptions /// If set, we keep an index of the top entries per term, allowing extremely fast single word queries /// regardless of index size, at the cost of more memory /// - KeepTermFrequencies = 16 + KeepTermFrequencies = 16, + /// + /// If set, we do not scan and index. + /// + SkipInitialScan = 32, + /// + /// Disable highlighting support. If set, we do not store corresponding byte offsets for term positions. + /// Also implied by UseTermOffsets. + /// + NoHighlight = 64, + /// + /// Increases maximum number of text fields (default is 32 fields) + /// + MaxTextFields = 128, } public sealed class IndexDefinition @@ -132,7 +145,6 @@ internal void SerializeRedisArgs(List args) args.Add(_payloadField); } } - } public sealed class ConfiguredIndexOptions @@ -144,6 +156,7 @@ public sealed class ConfiguredIndexOptions private IndexOptions _options; private readonly IndexDefinition _definition; private string[] _stopwords; + private long _temporaryTimestamp; public ConfiguredIndexOptions(IndexOptions options = IndexOptions.Default) { @@ -157,7 +170,7 @@ public ConfiguredIndexOptions(IndexDefinition definition, IndexOptions options = } /// - /// Set a custom stopword list. + /// Set a custom stopword list. These words will be ignored during indexing and search time. /// /// The new stopwords to use. public ConfiguredIndexOptions SetStopwords(params string[] stopwords) @@ -168,6 +181,9 @@ public ConfiguredIndexOptions SetStopwords(params string[] stopwords) return this; } + /// + /// Disable the stopwords list. + /// public ConfiguredIndexOptions SetNoStopwords() { _options |= IndexOptions.DisableStopWords; @@ -175,8 +191,64 @@ public ConfiguredIndexOptions SetNoStopwords() return this; } + /// + /// Disable highlight support. + /// + public ConfiguredIndexOptions SetNoHighlight() + { + _options |= IndexOptions.NoHighlight; + + return this; + } + + /// + /// Disable initial index scans. + /// + public ConfiguredIndexOptions SetSkipInitialScan() + { + _options |= IndexOptions.SkipInitialScan; + + return this; + } + + /// + /// Increases maximum text fields (past 32). + /// + public ConfiguredIndexOptions SetMaxTextFields() + { + _options |= IndexOptions.MaxTextFields; + + return this; + } + + /// + /// Disable term offsets (saves memory but disables exact matches). + /// + public ConfiguredIndexOptions SetUseTermOffsets() + { + _options |= IndexOptions.UseTermOffsets; + + return this; + } + + /// + /// Set a lightweight temporary index which will expire after the specified period of inactivity. + /// The internal idle timer is reset whenever the index is searched or added to. + /// + /// The time to expire in seconds. + public ConfiguredIndexOptions SetTemporaryTime(long time) + { + _temporaryTimestamp = time; + return this; + } + internal void SerializeRedisArgs(List args) { + if (_temporaryTimestamp != 0) + { + args.Add("TEMPORARY".Literal()); + args.Add(_temporaryTimestamp); + } SerializeRedisArgs(_options, args, _definition); if (_stopwords?.Length > 0) { @@ -189,10 +261,18 @@ internal void SerializeRedisArgs(List args) internal static void SerializeRedisArgs(IndexOptions options, List args, IndexDefinition definition) { definition?.SerializeRedisArgs(args); - if ((options & IndexOptions.UseTermOffsets) == 0) + if ((options & IndexOptions.MaxTextFields) == IndexOptions.MaxTextFields) + { + args.Add("MAXTEXTFIELDS".Literal()); + } + if ((options & IndexOptions.UseTermOffsets) == IndexOptions.UseTermOffsets) { args.Add("NOOFFSETS".Literal()); } + if ((options & IndexOptions.NoHighlight) == IndexOptions.NoHighlight) + { + args.Add("NOHL".Literal()); + } if ((options & IndexOptions.KeepFieldFlags) == 0) { args.Add("NOFIELDS".Literal()); @@ -206,6 +286,10 @@ internal static void SerializeRedisArgs(IndexOptions options, List args, args.Add("STOPWORDS".Literal()); args.Add(0.Boxed()); } + if ((options & IndexOptions.SkipInitialScan) == IndexOptions.SkipInitialScan) + { + args.Add("SKIPINITIALSCAN".Literal()); + } } } diff --git a/tests/NRediSearch.Test/ClientTests/ClientTest.cs b/tests/NRediSearch.Test/ClientTests/ClientTest.cs index aa45297d9..cc082a9cf 100644 --- a/tests/NRediSearch.Test/ClientTests/ClientTest.cs +++ b/tests/NRediSearch.Test/ClientTests/ClientTest.cs @@ -1,4 +1,4 @@ -using System.Reflection.Metadata; +using System.Threading; using System.Collections.Generic; using System.Text; using System; @@ -10,12 +10,26 @@ using static NRediSearch.Schema; using static NRediSearch.SuggestionOptions; + namespace NRediSearch.Test.ClientTests { public class ClientTest : RediSearchTestBase { public ClientTest(ITestOutputHelper output) : base(output) { } + private long getModuleSearchVersion() { + Client cl = GetClient(); + var modules = (RedisResult[])Db.Execute("MODULE", "LIST"); + long version = 0; + foreach (var module in modules) { + var result = (RedisResult[])module; + if (result[1].ToString() == ("search")) { + version = (long)result[3]; + } + } + return version; + } + [Fact] public void Search() { @@ -152,6 +166,67 @@ public void TestStopwords() Assert.Equal(1, cl.Search(new Query("to be or not to be")).TotalResults); } + [Fact] + public void TestSkipInitialIndex() + { + Db.HashSet("doc1", "foo", "bar"); + var query = new Query("@foo:bar"); + var sc = new Schema().AddTextField("foo"); + + var client1 = new Client("idx1", Db); + Assert.True(client1.CreateIndex(sc, new ConfiguredIndexOptions())); + Assert.Equal(1, client1.Search(query).TotalResults); + + var client2 = new Client("idx2", Db); + Assert.True(client2.CreateIndex(sc, new ConfiguredIndexOptions().SetSkipInitialScan())); + Assert.Equal(0, client2.Search(query).TotalResults); + } + + [Fact] + public void TestSummarizationDisabled() + { + Client cl = GetClient(); + + Schema sc = new Schema().AddTextField("body"); + + Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions().SetUseTermOffsets())); + var fields = new Dictionary + { + { "body", "hello world" } + }; + Assert.True(cl.AddDocument("doc1", fields)); + + var ex = Assert.Throws(() => cl.Search(new Query("hello").SummarizeFields("body"))); + Assert.Equal("Cannot use highlight/summarize because NOOFSETS was specified at index level", ex.Message); + + cl = GetClient(); + Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions().SetNoHighlight())); + Assert.True(cl.AddDocument("doc2", fields)); + Assert.Throws(() => cl.Search(new Query("hello").SummarizeFields("body"))); + } + + [Fact] + public void TestExpire() + { + var cl = new Client("idx", Db); + Schema sc = new Schema().AddTextField("title"); + + Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions().SetTemporaryTime(4).SetMaxTextFields())); + long ttl = (long) Db.Execute("FT.DEBUG", "TTL", "idx"); + while (ttl > 2) { + ttl = (long) Db.Execute("FT.DEBUG", "TTL", "idx"); + Thread.Sleep(10); + } + + var fields = new Dictionary + { + { "title", "hello world foo bar to be or not to be" } + }; + Assert.True(cl.AddDocument("doc1", fields)); + ttl = (long) Db.Execute("FT.DEBUG", "TTL", "idx"); + Assert.True(ttl > 2); + } + [Fact] public void TestGeoFilter() { @@ -977,6 +1052,10 @@ public void TestInKeys() [Fact] public void TestWithFieldNames() { + if (getModuleSearchVersion() <= 20200) { + return; + } + Client cl = GetClient(); IndexDefinition defenition = new IndexDefinition(prefixes: new string[] {"student:", "pupil:"}); Schema sc = new Schema().AddTextField(FieldName.Of("first").As("given")).AddTextField(FieldName.Of("last")); @@ -1030,7 +1109,12 @@ public void TestWithFieldNames() } [Fact] - public void TestReturnWithFieldNames(){ + public void TestReturnWithFieldNames() + { + if (getModuleSearchVersion() <= 20200) { + return; + } + Client cl = GetClient(); Schema sc = new Schema().AddTextField("a").AddTextField("b").AddTextField("c"); Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); @@ -1054,6 +1138,10 @@ public void TestReturnWithFieldNames(){ [Fact] public void TestJsonIndex() { + if (getModuleSearchVersion() <= 20200) { + return; + } + Client cl = GetClient(); IndexDefinition defenition = new IndexDefinition(prefixes: new string[] {"king:"} ,type: IndexDefinition.IndexType.Json); Schema sc = new Schema().AddTextField("$.name"); From 755a6975befa6bdaf239a2cde52aa4ef23229373 Mon Sep 17 00:00:00 2001 From: jjfmarket Date: Tue, 5 Oct 2021 11:57:24 -0700 Subject: [PATCH 026/435] Add support for count argument to ListLeftPop, ListLeftPopAsync, ListRightPop, and ListRightPopAsync (#1850) Add support for count argument to ListLeftPop, ListLeftPopAsync, ListRightPop, and ListRightPopAsync Optional Count argument was added in Redis 6.2 Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + .../Interfaces/IDatabase.cs | 22 ++++++++++++++ .../Interfaces/IDatabaseAsync.cs | 22 ++++++++++++++ .../KeyspaceIsolation/DatabaseWrapper.cs | 10 +++++++ .../KeyspaceIsolation/WrapperBase.cs | 10 +++++++ src/StackExchange.Redis/RedisDatabase.cs | 30 +++++++++++++++++-- .../DatabaseWrapperTests.cs | 14 +++++++++ .../WrapperBaseTests.cs | 14 +++++++++ 8 files changed, 120 insertions(+), 3 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 97d4f1d45..a12a5b801 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,6 +6,7 @@ - fix potential errors getting socket bytes (#1836 via NickCraver) - logging additions (.NET Version and timestamps) for better debugging (#1796 via philon-msft) - add: `Condition` API (transactions) now supports `StreamLengthEqual` and variants (#1807 via AlphaGremlin) +- Add support for count argument to `ListLeftPop`, `ListLeftPopAsync`, `ListRightPop`, and `ListRightPopAsync` (#1850 via jjfmarket) - fix potential task/thread exhaustion from the backlog processor (#1854 via mgravell) ## 2.2.62 diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index a4d14ab2f..31e4e3bc9 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -639,6 +639,17 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/lpop RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns count elements from the tail of the list stored at key. + /// If there are less elements in the list than count, removes and returns all the elements in the list. + /// + /// The key of the list. + /// The number of items to remove. + /// The flags to use for this operation. + /// Array of values that were popped, or nil if the key doesn't exist. + /// https://redis.io/commands/lpop + RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// /// Insert the specified value at the head of the list stored at key. If key does not exist, it is created as empty list before performing the push operations. /// @@ -719,6 +730,17 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/rpop RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns count elements from the head of the list stored at key. + /// If there are less elements in the list than count, removes and returns all the elements in the list. + /// + /// The key of the list. + /// tThe number of items to remove. + /// The flags to use for this operation. + /// Array of values that were popped, or nil if the key doesn't exist. + /// https://redis.io/commands/rpop + RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// /// Atomically returns and removes the last element (tail) of the list stored at source, and pushes the element at the first element (head) of the list stored at destination. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 496722667..79d0d6ff0 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -616,6 +616,17 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/lpop Task ListLeftPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns count elements from the head of the list stored at key. + /// If the list contains less than count elements, removes and returns the number of elements in the list + /// + /// The key of the list. + /// The number of elements to remove + /// The flags to use for this operation. + /// Array of values that were popped, or nil if the key doesn't exist + /// https://redis.io/commands/lpop + Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// /// Insert the specified value at the head of the list stored at key. If key does not exist, it is created as empty list before performing the push operations. /// @@ -696,6 +707,17 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/rpop Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns count elements from the end the list stored at key. + /// If the list contains less than count elements, removes and returns the number of elements in the list + /// + /// The key of the list. + /// The number of elements to pop + /// The flags to use for this operation. + /// Array of values that were popped, or nil if the key doesn't exist + /// https://redis.io/commands/rpop + Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// /// Atomically returns and removes the last element (tail) of the list stored at source, and pushes the element at the first element (head) of the list stored at destination. /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index e6f6f506a..88585d3ef 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -301,6 +301,11 @@ public RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.No return Inner.ListLeftPop(ToInner(key), flags); } + public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + return Inner.ListLeftPop(ToInner(key), count, flags); + } + public long ListLeftPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) { return Inner.ListLeftPush(ToInner(key), values, flags); @@ -336,6 +341,11 @@ public RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.N return Inner.ListRightPop(ToInner(key), flags); } + public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + return Inner.ListRightPop(ToInner(key), count, flags); + } + public RedisValue ListRightPopLeftPush(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) { return Inner.ListRightPopLeftPush(ToInner(source), ToInner(destination), flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index d4b124f86..fc5f9abeb 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -286,6 +286,11 @@ public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = Comm return Inner.ListLeftPopAsync(ToInner(key), flags); } + public Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + return Inner.ListLeftPopAsync(ToInner(key), count, flags); + } + public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) { return Inner.ListLeftPushAsync(ToInner(key), values, flags); @@ -321,6 +326,11 @@ public Task ListRightPopAsync(RedisKey key, CommandFlags flags = Com return Inner.ListRightPopAsync(ToInner(key), flags); } + public Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + return Inner.ListRightPopAsync(ToInner(key), count, flags); + } + public Task ListRightPopLeftPushAsync(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) { return Inner.ListRightPopLeftPushAsync(ToInner(source), ToInner(destination), flags); diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index be3b5d991..825b511b1 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -922,12 +922,24 @@ public RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.No return ExecuteSync(msg, ResultProcessor.RedisValue); } + public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.LPOP, key, count); + return ExecuteSync(msg, ResultProcessor.RedisValueArray); + } + public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.LPOP, key); return ExecuteAsync(msg, ResultProcessor.RedisValue); } + public Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.LPOP, key, count); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + } + public long ListLeftPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) { WhenAlwaysOrExists(when); @@ -1016,12 +1028,24 @@ public RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.N return ExecuteSync(msg, ResultProcessor.RedisValue); } + public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.RPOP, key, count); + return ExecuteSync(msg, ResultProcessor.RedisValueArray); + } + public Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.RPOP, key); return ExecuteAsync(msg, ResultProcessor.RedisValue); } + public Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.RPOP, key, count); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + } + public RedisValue ListRightPopLeftPush(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.RPOPLPUSH, source, destination); @@ -1933,7 +1957,7 @@ public bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisV key, groupName, position, - true, + true, flags); } @@ -2251,7 +2275,7 @@ public Task StreamReadGroupAsync(RedisKey key, RedisValue groupNa false, flags); } - + public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None) { var actualPosition = position ?? StreamPosition.NewMessages; @@ -2815,7 +2839,7 @@ private Message GetMultiStreamReadMessage(StreamPosition[] streamPositions, int? * [7] = id1 * [8] = id2 * [9] = id3 - * + * * */ var pairCount = streamPositions.Length; diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index f9b539394..1455e0603 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -369,6 +369,13 @@ public void ListLeftPop() mock.Verify(_ => _.ListLeftPop("prefix:key", CommandFlags.None)); } + [Fact] + public void ListLeftPop_1() + { + wrapper.ListLeftPop("key", 123, CommandFlags.None); + mock.Verify(_ => _.ListLeftPop("prefix:key", 123, CommandFlags.None)); + } + [Fact] public void ListLeftPush_1() { @@ -420,6 +427,13 @@ public void ListRightPop() mock.Verify(_ => _.ListRightPop("prefix:key", CommandFlags.None)); } + [Fact] + public void ListRightPop_1() + { + wrapper.ListRightPop("key", 123, CommandFlags.None); + mock.Verify(_ => _.ListRightPop("prefix:key", 123, CommandFlags.None)); + } + [Fact] public void ListRightPopLeftPush() { diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index 3f1ee9f28..286fb8c5a 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -330,6 +330,13 @@ public void ListLeftPopAsync() mock.Verify(_ => _.ListLeftPopAsync("prefix:key", CommandFlags.None)); } + [Fact] + public void ListLeftPopAsync_1() + { + wrapper.ListLeftPopAsync("key", 123, CommandFlags.None); + mock.Verify(_ => _.ListLeftPopAsync("prefix:key", 123, CommandFlags.None)); + } + [Fact] public void ListLeftPushAsync_1() { @@ -381,6 +388,13 @@ public void ListRightPopAsync() mock.Verify(_ => _.ListRightPopAsync("prefix:key", CommandFlags.None)); } + [Fact] + public void ListRightPopAsync_1() + { + wrapper.ListRightPopAsync("key", 123, CommandFlags.None); + mock.Verify(_ => _.ListRightPopAsync("prefix:key", 123, CommandFlags.None)); + } + [Fact] public void ListRightPopLeftPushAsync() { From 082c69b4b72008a5958597490c470e8b8b39b298 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Wed, 6 Oct 2021 03:35:37 +0800 Subject: [PATCH 027/435] Add support for GETDEL command (#1840) Fixes #1729 Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 1 + .../Interfaces/IDatabase.cs | 9 ++++ .../Interfaces/IDatabaseAsync.cs | 9 ++++ .../KeyspaceIsolation/DatabaseWrapper.cs | 5 ++ .../KeyspaceIsolation/WrapperBase.cs | 5 ++ src/StackExchange.Redis/Message.cs | 1 + src/StackExchange.Redis/RedisDatabase.cs | 12 +++++ src/StackExchange.Redis/RedisFeatures.cs | 8 ++- .../DatabaseWrapperTests.cs | 7 +++ tests/StackExchange.Redis.Tests/Strings.cs | 50 +++++++++++++++++++ .../WrapperBaseTests.cs | 7 +++ 12 files changed, 114 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index a12a5b801..bedd1b630 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,7 @@ - add: `Condition` API (transactions) now supports `StreamLengthEqual` and variants (#1807 via AlphaGremlin) - Add support for count argument to `ListLeftPop`, `ListLeftPopAsync`, `ListRightPop`, and `ListRightPopAsync` (#1850 via jjfmarket) - fix potential task/thread exhaustion from the backlog processor (#1854 via mgravell) +- add `StringGetDelete`/`StringGetDeleteAsync` API for Redis `GETDEL` command(#1840 via WeihanLi) ## 2.2.62 diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 3f9eeab45..decb0f003 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -49,6 +49,7 @@ internal enum RedisCommand GET, GETBIT, + GETDEL, GETRANGE, GETSET, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 31e4e3bc9..bb0f56b47 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1981,6 +1981,15 @@ IEnumerable SortedSetScan(RedisKey key, /// https://redis.io/commands/getset RedisValue StringGetSet(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); + /// + /// Get the value of key and delete the key. If the key does not exist the special value nil is returned. An error is returned if the value stored at key is not a string, because GET only handles string values. + /// + /// The key of the string. + /// The flags to use for this operation. + /// The value of key, or nil when key does not exist. + /// https://redis.io/commands/getdelete + RedisValue StringGetDelete(RedisKey key, CommandFlags flags = CommandFlags.None); + /// /// Get the value of key. If the key does not exist the special value nil is returned. An error is returned if the value stored at key is not a string, because GET only handles string values. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 79d0d6ff0..b5600c97a 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1931,6 +1931,15 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// https://redis.io/commands/getset Task StringGetSetAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); + /// + /// Get the value of key and delete the key. If the key does not exist the special value nil is returned. An error is returned if the value stored at key is not a string, because GET only handles string values. + /// + /// The key of the string. + /// The flags to use for this operation. + /// The value of key, or nil when key does not exist. + /// https://redis.io/commands/getdelete + Task StringGetDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// /// Get the value of key. If the key does not exist the special value nil is returned. An error is returned if the value stored at key is not a string, because GET only handles string values. /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 88585d3ef..2a6842c49 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -841,6 +841,11 @@ public RedisValue StringGetSet(RedisKey key, RedisValue value, CommandFlags flag return Inner.StringGetSet(ToInner(key), value, flags); } + public RedisValue StringGetDelete(RedisKey key, CommandFlags flags = CommandFlags.None) + { + return Inner.StringGetDelete(ToInner(key), flags); + } + public RedisValueWithExpiry StringGetWithExpiry(RedisKey key, CommandFlags flags = CommandFlags.None) { return Inner.StringGetWithExpiry(ToInner(key), flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index fc5f9abeb..7c0e7e436 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -832,6 +832,11 @@ public Task StringGetSetAsync(RedisKey key, RedisValue value, Comman return Inner.StringGetSetAsync(ToInner(key), value, flags); } + public Task StringGetDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + return Inner.StringGetDeleteAsync(ToInner(key), flags); + } + public Task StringGetWithExpiryAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { return Inner.StringGetWithExpiryAsync(ToInner(key), flags); diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 78e931ce6..c8fdf54f8 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -359,6 +359,7 @@ public static bool IsMasterOnly(RedisCommand command) case RedisCommand.EXPIREAT: case RedisCommand.FLUSHALL: case RedisCommand.FLUSHDB: + case RedisCommand.GETDEL: case RedisCommand.GETSET: case RedisCommand.HDEL: case RedisCommand.HINCRBY: diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 825b511b1..5fec7027d 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -2503,6 +2503,18 @@ public Task StringGetSetAsync(RedisKey key, RedisValue value, Comman return ExecuteAsync(msg, ResultProcessor.RedisValue); } + public RedisValue StringGetDelete(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.GETDEL, key); + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + + public Task StringGetDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.GETDEL, key); + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + public RedisValueWithExpiry StringGetWithExpiry(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = GetStringGetWithExpiryMessage(key, flags, out ResultProcessor processor, out ServerEndPoint server); diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index a8d60a9d3..2d61f9b67 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -35,7 +35,8 @@ public readonly struct RedisFeatures v3_2_1 = new Version(3, 2, 1), v4_0_0 = new Version(4, 0, 0), v4_9_1 = new Version(4, 9, 1), // 5.0 RC1 is version 4.9.1; // 5.0 RC1 is version 4.9.1 - v5_0_0 = new Version(5, 0, 0); + v5_0_0 = new Version(5, 0, 0), + v6_2_0 = new Version(6, 2, 0); private readonly Version version; @@ -68,6 +69,11 @@ public RedisFeatures(Version version) /// public bool ExpireOverwrite => Version >= v2_1_3; + /// + /// Is GETDEL available? + /// + public bool GetDelete => Version >= v6_2_0; + /// /// Is HSTRLEN available? /// diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 1455e0603..fbce955c8 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -1118,6 +1118,13 @@ public void StringGetSet() mock.Verify(_ => _.StringGetSet("prefix:key", "value", CommandFlags.None)); } + [Fact] + public void StringGetDelete() + { + wrapper.StringGetDelete("key", CommandFlags.None); + mock.Verify(_ => _.StringGetDelete("prefix:key", CommandFlags.None)); + } + [Fact] public void StringGetWithExpiry() { diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index 678e2b544..fff5fdee7 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -106,6 +106,56 @@ public async Task GetLeaseAsStream() } } + [Fact] + public void GetDelete() + { + using (var muxer = Create()) + { + Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetDelete), r => r.GetDelete); + + var conn = muxer.GetDatabase(); + var prefix = Me(); + conn.KeyDelete(prefix + "1", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "2", CommandFlags.FireAndForget); + conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); + + Assert.True(conn.KeyExists(prefix + "1")); + Assert.False(conn.KeyExists(prefix + "2")); + + var s0 = conn.StringGetDelete(prefix + "1"); + var s2 = conn.StringGetDelete(prefix + "2"); + + Assert.False(conn.KeyExists(prefix + "1")); + Assert.Equal("abc", s0); + Assert.Equal(RedisValue.Null, s2); + } + } + + [Fact] + public async Task GetDeleteAsync() + { + using (var muxer = Create()) + { + Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetDelete), r => r.GetDelete); + + var conn = muxer.GetDatabase(); + var prefix = Me(); + conn.KeyDelete(prefix + "1", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "2", CommandFlags.FireAndForget); + conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); + + Assert.True(conn.KeyExists(prefix + "1")); + Assert.False(conn.KeyExists(prefix + "2")); + + var s0 = conn.StringGetDeleteAsync(prefix + "1"); + var s2 = conn.StringGetDeleteAsync(prefix + "2"); + + Assert.False(conn.KeyExists(prefix + "1")); + Assert.Equal("abc", await s0); + Assert.Equal(RedisValue.Null, await s2); + } + } + [Fact] public async Task SetNotExists() { diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index 286fb8c5a..98e29cfd6 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -1051,6 +1051,13 @@ public void StringGetSetAsync() mock.Verify(_ => _.StringGetSetAsync("prefix:key", "value", CommandFlags.None)); } + [Fact] + public void StringGetDeleteAsync() + { + wrapper.StringGetDeleteAsync("key", CommandFlags.None); + mock.Verify(_ => _.StringGetDeleteAsync("prefix:key", CommandFlags.None)); + } + [Fact] public void StringGetWithExpiryAsync() { From ef1178e2ebf1433bbd1c7bafb59f1cc5c1e7704d Mon Sep 17 00:00:00 2001 From: Michelle Date: Thu, 7 Oct 2021 14:12:34 +0100 Subject: [PATCH 028/435] Add support for listening to Azure Maintenance Events (#1876) Adding an automatic subscription to the AzureRedisEvents pubsub channel for Azure caches. This channel notifies clients of upcoming maintenance and failover events. By exposing these events, users will be able to know about maintenance ahead of time, and can implement their own logic (e.g. diverting traffic from the cache to another database for the duration of the maintenance event) in response, with the goal of minimizing downtime and disrupted connections. We also automatically refresh our view of the topology of the cluster in response to certain events. Here are some of the possible notifications: ``` // Indicates that a maintenance event is scheduled. May be several minutes from now NodeMaintenanceScheduled, // This event gets fired ~20s before maintenance begins NodeMaintenanceStarting, // This event gets fired when maintenance is imminent (<5s) NodeMaintenanceStart, // Indicates that the node maintenance operation is over NodeMaintenanceEnded, // Indicates that a replica has been promoted to primary NodeMaintenanceFailover ``` Co-authored-by: Michelle Soedal Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + .../ConfigurationOptions.cs | 25 ++- .../ConnectionMultiplexer.cs | 30 ++- src/StackExchange.Redis/Format.cs | 6 +- .../Maintenance/AzureMaintenanceEvent.cs | 194 ++++++++++++++++++ .../Maintenance/AzureNotificationType.cs | 38 ++++ .../Maintenance/ServerMaintenanceEvent.cs | 52 +++++ .../AzureMaintenanceEventTests.cs | 52 +++++ tests/StackExchange.Redis.Tests/PubSub.cs | 33 +++ 9 files changed, 414 insertions(+), 17 deletions(-) create mode 100644 src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs create mode 100644 src/StackExchange.Redis/Maintenance/AzureNotificationType.cs create mode 100644 src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs create mode 100644 tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index bedd1b630..74b1356c4 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,7 @@ - add: `Condition` API (transactions) now supports `StreamLengthEqual` and variants (#1807 via AlphaGremlin) - Add support for count argument to `ListLeftPop`, `ListLeftPopAsync`, `ListRightPop`, and `ListRightPopAsync` (#1850 via jjfmarket) - fix potential task/thread exhaustion from the backlog processor (#1854 via mgravell) +- add support for listening to Azure Maintenance Events (#1876 via amsoedal) - add `StringGetDelete`/`StringGetDeleteAsync` API for Redis `GETDEL` command(#1840 via WeihanLi) ## 2.2.62 diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index ad343e598..44486acbc 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -790,22 +790,29 @@ private void DoParse(string configuration, bool ignoreUnknown) // Microsoft Azure team wants abortConnect=false by default private bool GetDefaultAbortOnConnectFailSetting() => !IsAzureEndpoint(); - private bool IsAzureEndpoint() + /// + /// List of domains known to be Azure Redis, so we can light up some helpful functionality + /// for minimizing downtime during maintenance events and such. + /// + private static readonly List azureRedisDomains = new List { + ".redis.cache.windows.net", + ".redis.cache.chinacloudapi.cn", + ".redis.cache.usgovcloudapi.net", + ".redis.cache.cloudapi.de", + ".redisenterprise.cache.azure.net", + }; + + internal bool IsAzureEndpoint() { foreach (var ep in EndPoints) { if (ep is DnsEndPoint dnsEp) { - int firstDot = dnsEp.Host.IndexOf('.'); - if (firstDot >= 0) + foreach (var host in azureRedisDomains) { - switch (dnsEp.Host.Substring(firstDot).ToLowerInvariant()) + if (dnsEp.Host.EndsWith(host, StringComparison.InvariantCultureIgnoreCase)) { - case ".redis.cache.windows.net": - case ".redis.cache.chinacloudapi.cn": - case ".redis.cache.usgovcloudapi.net": - case ".redis.cache.cloudapi.de": - return true; + return true; } } } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index a68499d4d..58e6252d3 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1,20 +1,21 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.IO; +using System.IO.Compression; using System.Linq; using System.Net; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Reflection; -using System.IO.Compression; -using System.Runtime.CompilerServices; -using StackExchange.Redis.Profiling; using Pipelines.Sockets.Unofficial; -using System.ComponentModel; -using System.Runtime.InteropServices; +using StackExchange.Redis.Maintenance; +using StackExchange.Redis.Profiling; namespace StackExchange.Redis { @@ -142,6 +143,7 @@ public ServerCounters GetCounters() public string ClientName => RawConfig.ClientName ?? GetDefaultClientName(); private static string defaultClientName; + private static string GetDefaultClientName() { return defaultClientName ??= TryGetAzureRoleInstanceIdNoThrow() @@ -571,6 +573,11 @@ private static void WriteNormalizingLineEndings(string source, StreamWriter writ /// public event EventHandler ConfigurationChangedBroadcast; + /// + /// Raised when server indicates a maintenance event is going to happen. + /// + public event EventHandler ServerMaintenanceEvent; + /// /// Gets the synchronous timeout associated with the connections /// @@ -591,6 +598,9 @@ public EndPoint[] GetEndPoints(bool configuredOnly = false) return _serverSnapshot.GetEndPoints(); } + internal void InvokeServerMaintenanceEvent(ServerMaintenanceEvent e) + => ServerMaintenanceEvent?.Invoke(this, e); + internal bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isMoved) { return ServerSelectionStrategy.TryResend(hashSlot, message, endpoint, isMoved); @@ -892,6 +902,9 @@ private static async Task ConnectImplAsync(ConfigurationO // Initialize the Sentinel handlers muxer.InitializeSentinel(logProxy); } + + await Maintenance.ServerMaintenanceEvent.AddListenersAsync(muxer, logProxy).ForAwait(); + return muxer; } finally @@ -1187,6 +1200,9 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat // Initialize the Sentinel handlers muxer.InitializeSentinel(logProxy); } + + Maintenance.ServerMaintenanceEvent.AddListenersAsync(muxer, logProxy).Wait(muxer.SyncConnectTimeout(true)); + return muxer; } finally @@ -2863,7 +2879,7 @@ internal T ExecuteSyncImpl(Message message, ResultProcessor processor, Ser } } // snapshot these so that we can recycle the box - var val = source.GetResult(out var ex, canRecycle: true); // now that we aren't locking it... + var val = source.GetResult(out var ex, canRecycle: true); // now that we aren't locking it... if (ex != null) throw ex; Trace(message + " received " + val); return val; diff --git a/src/StackExchange.Redis/Format.cs b/src/StackExchange.Redis/Format.cs index f9407894e..be0e294ae 100644 --- a/src/StackExchange.Redis/Format.cs +++ b/src/StackExchange.Redis/Format.cs @@ -4,13 +4,17 @@ using System.Globalization; using System.Net; using System.Net.Sockets; -using System.Runtime.InteropServices; using System.Text; namespace StackExchange.Redis { internal static class Format { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + public static int ParseInt32(ReadOnlySpan s) => int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo); + public static bool TryParseInt32(ReadOnlySpan s, out int value) => int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out value); +#endif + public static int ParseInt32(string s) => int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo); public static long ParseInt64(string s) => long.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo); diff --git a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs new file mode 100644 index 000000000..cd4ad62c8 --- /dev/null +++ b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs @@ -0,0 +1,194 @@ +using System; +using System.Globalization; +using System.Net; +using System.Threading.Tasks; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +using System.Buffers.Text; +#endif + +namespace StackExchange.Redis.Maintenance +{ + /// + /// Azure node maintenance event. For more information, please see: https://aka.ms/redis/maintenanceevents + /// + public sealed class AzureMaintenanceEvent : ServerMaintenanceEvent + { + private const string PubSubChannelName = "AzureRedisEvents"; + + internal AzureMaintenanceEvent(string azureEvent) + { + if (azureEvent == null) + { + return; + } + + // The message consists of key-value pairs delimited by pipes. For example, a message might look like: + // NotificationType|NodeMaintenanceStarting|StartTimeUtc|2021-09-23T12:34:19|IsReplica|False|IpAddress|13.67.42.199|SSLPort|15001|NonSSLPort|13001 + var message = azureEvent.AsSpan(); + try + { + while (message.Length > 0) + { + if (message[0] == '|') + { + message = message.Slice(1); + continue; + } + + // Grab the next pair + var nextDelimiter = message.IndexOf('|'); + if (nextDelimiter < 0) + { + // The rest of the message is not a key-value pair and is therefore malformed. Stop processing it. + break; + } + + if (nextDelimiter == message.Length - 1) + { + // The message is missing the value for this key-value pair. It is malformed so we stop processing it. + break; + } + + var key = message.Slice(0, nextDelimiter); + message = message.Slice(key.Length + 1); + + var valueEnd = message.IndexOf('|'); + var value = valueEnd > -1 ? message.Slice(0, valueEnd) : message; + message = message.Slice(value.Length); + + if (key.Length > 0 && value.Length > 0) + { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER + switch (key) + { + case var _ when key.SequenceEqual(nameof(NotificationType).AsSpan()): + NotificationTypeString = value.ToString(); + NotificationType = ParseNotificationType(NotificationTypeString); + break; + case var _ when key.SequenceEqual("StartTimeInUTC".AsSpan()) && DateTime.TryParseExact(value, "s", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTime startTime): + StartTimeUtc = DateTime.SpecifyKind(startTime, DateTimeKind.Utc); + break; + case var _ when key.SequenceEqual(nameof(IsReplica).AsSpan()) && bool.TryParse(value, out var isReplica): + IsReplica = isReplica; + break; + case var _ when key.SequenceEqual(nameof(IPAddress).AsSpan()) && IPAddress.TryParse(value, out var ipAddress): + IPAddress = ipAddress; + break; + case var _ when key.SequenceEqual("SSLPort".AsSpan()) && Format.TryParseInt32(value, out var port): + SslPort = port; + break; + case var _ when key.SequenceEqual("NonSSLPort".AsSpan()) && Format.TryParseInt32(value, out var nonsslport): + NonSslPort = nonsslport; + break; + default: + break; + } +#else + switch (key) + { + case var _ when key.SequenceEqual(nameof(NotificationType).AsSpan()): + NotificationTypeString = value.ToString(); + NotificationType = ParseNotificationType(NotificationTypeString); + break; + case var _ when key.SequenceEqual("StartTimeInUTC".AsSpan()) && DateTime.TryParseExact(value.ToString(), "s", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTime startTime): + StartTimeUtc = DateTime.SpecifyKind(startTime, DateTimeKind.Utc); + break; + case var _ when key.SequenceEqual(nameof(IsReplica).AsSpan()) && bool.TryParse(value.ToString(), out var isReplica): + IsReplica = isReplica; + break; + case var _ when key.SequenceEqual(nameof(IPAddress).AsSpan()) && IPAddress.TryParse(value.ToString(), out var ipAddress): + IPAddress = ipAddress; + break; + case var _ when key.SequenceEqual("SSLPort".AsSpan()) && Format.TryParseInt32(value.ToString(), out var port): + SslPort = port; + break; + case var _ when key.SequenceEqual("NonSSLPort".AsSpan()) && Format.TryParseInt32(value.ToString(), out var nonsslport): + NonSslPort = nonsslport; + break; + default: + break; + } +#endif + } + } + } + catch + { + // TODO: Append to rolling debug log when it's present + } + } + + internal async static Task AddListenerAsync(ConnectionMultiplexer multiplexer, ConnectionMultiplexer.LogProxy logProxy) + { + try + { + var sub = multiplexer.GetSubscriber(); + if (sub == null) + { + logProxy?.WriteLine("Failed to GetSubscriber for AzureRedisEvents"); + return; + } + + await sub.SubscribeAsync(PubSubChannelName, async (channel, message) => + { + var newMessage = new AzureMaintenanceEvent(message); + multiplexer.InvokeServerMaintenanceEvent(newMessage); + + switch (newMessage.NotificationType) + { + case AzureNotificationType.NodeMaintenanceEnded: + case AzureNotificationType.NodeMaintenanceFailoverComplete: + await multiplexer.ReconfigureAsync(first: false, reconfigureAll: true, log: logProxy, blame: null, cause: $"Azure Event: {newMessage.NotificationType}").ForAwait(); + break; + } + }).ForAwait(); + } + catch (Exception e) + { + logProxy?.WriteLine($"Encountered exception: {e}"); + } + } + + /// + /// Indicates the type of event (raw string form). + /// + public string NotificationTypeString { get; } + + /// + /// The parsed version of for easier consumption. + /// + public AzureNotificationType NotificationType { get; } + + /// + /// Indicates if the event is for a replica node. + /// + public bool IsReplica { get; } + + /// + /// IPAddress of the node event is intended for. + /// + public IPAddress IPAddress { get; } + + /// + /// SSL Port. + /// + public int SslPort { get; } + + /// + /// Non-SSL port. + /// + public int NonSslPort { get; } + + private AzureNotificationType ParseNotificationType(string typeString) => typeString switch + { + "NodeMaintenanceScheduled" => AzureNotificationType.NodeMaintenanceScheduled, + "NodeMaintenanceStarting" => AzureNotificationType.NodeMaintenanceStarting, + "NodeMaintenanceStart" => AzureNotificationType.NodeMaintenanceStart, + "NodeMaintenanceEnded" => AzureNotificationType.NodeMaintenanceEnded, + // This is temporary until server changes go into effect - to be removed in later versions + "NodeMaintenanceFailover" => AzureNotificationType.NodeMaintenanceFailoverComplete, + "NodeMaintenanceFailoverComplete" => AzureNotificationType.NodeMaintenanceFailoverComplete, + _ => AzureNotificationType.Unknown, + }; + } +} diff --git a/src/StackExchange.Redis/Maintenance/AzureNotificationType.cs b/src/StackExchange.Redis/Maintenance/AzureNotificationType.cs new file mode 100644 index 000000000..06ed65e5b --- /dev/null +++ b/src/StackExchange.Redis/Maintenance/AzureNotificationType.cs @@ -0,0 +1,38 @@ +namespace StackExchange.Redis.Maintenance +{ + /// + /// The types of notifications that Azure is sending for events happening. + /// + public enum AzureNotificationType + { + /// + /// Unrecognized event type, likely needs a library update to recognize new events. + /// + Unknown, + + /// + /// Indicates that a maintenance event is scheduled. May be several minutes from now. + /// + NodeMaintenanceScheduled, + + /// + /// This event gets fired ~20s before maintenance begins. + /// + NodeMaintenanceStarting, + + /// + /// This event gets fired when maintenance is imminent (<5s). + /// + NodeMaintenanceStart, + + /// + /// Indicates that the node maintenance operation is over. + /// + NodeMaintenanceEnded, + + /// + /// Indicates that a replica has been promoted to primary. + /// + NodeMaintenanceFailoverComplete, + } +} diff --git a/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs new file mode 100644 index 000000000..46e9dcd46 --- /dev/null +++ b/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading.Tasks; +using static StackExchange.Redis.ConnectionMultiplexer; + +namespace StackExchange.Redis.Maintenance +{ + /// + /// Base class for all server maintenance events + /// + public class ServerMaintenanceEvent + { + internal ServerMaintenanceEvent() + { + ReceivedTimeUtc = DateTime.UtcNow; + } + + internal async static Task AddListenersAsync(ConnectionMultiplexer muxer, LogProxy logProxy) + { + if (!muxer.CommandMap.IsAvailable(RedisCommand.SUBSCRIBE)) + { + return; + } + + if (muxer.RawConfig.IsAzureEndpoint()) + { + await AzureMaintenanceEvent.AddListenerAsync(muxer, logProxy).ForAwait(); + } + // Other providers could be added here later + } + + /// + /// Raw message received from the server. + /// + public string RawMessage { get; protected set; } + + /// + /// The time the event was received. If we know when the event is expected to start will be populated. + /// + public DateTime ReceivedTimeUtc { get; } + + /// + /// Indicates the expected start time of the event. + /// + public DateTime? StartTimeUtc { get; protected set; } + + /// + /// Returns a string representing the maintenance event with all of its properties. + /// + public override string ToString() + => RawMessage; + } +} diff --git a/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs b/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs new file mode 100644 index 000000000..7ca5fea1a --- /dev/null +++ b/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs @@ -0,0 +1,52 @@ +using System; +using System.Globalization; +using System.Net; +using StackExchange.Redis.Maintenance; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests +{ + public class AzureMaintenanceEventTests : TestBase + { + public AzureMaintenanceEventTests(ITestOutputHelper output) : base(output) + { + } + + [Theory] + [InlineData("NotificationType|NodeMaintenanceStarting|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress||SSLPort|15001|NonSSLPort|13001", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, null, 15001, 13001)] + [InlineData("NotificationType|NodeMaintenanceFailover|StartTimeInUTC||IsReplica|False|IPAddress||SSLPort|15001|NonSSLPort|13001", AzureNotificationType.NodeMaintenanceFailoverComplete, null, false, null, 15001, 13001)] + [InlineData("NotificationType|NodeMaintenanceFailover|StartTimeInUTC||IsReplica|True|IPAddress||SSLPort|15001|NonSSLPort|13001", AzureNotificationType.NodeMaintenanceFailoverComplete, null, true, null, 15001, 13001)] + [InlineData("NotificationType|NodeMaintenanceStarting|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|j|IPAddress||SSLPort|char|NonSSLPort|char", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, null, 0, 0)] + [InlineData("NotificationType|NodeMaintenanceStarting|somejunkkey|somejunkvalue|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress||SSLPort|15999|NonSSLPort|139991", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, null, 15999, 139991)] + [InlineData("NotificationType|NodeMaintenanceStarting|somejunkkey|somejunkvalue|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress|127.0.0.1|SSLPort|15999|NonSSLPort|139991", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, "127.0.0.1", 15999, 139991)] + [InlineData("NotificationTypeNodeMaintenanceStartingsomejunkkeysomejunkvalueStartTimeInUTC2021-03-02T23:26:57IsReplicaFalseIPAddress127.0.0.1SSLPort15999NonSSLPort139991", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("NotificationType|", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("NotificationType|NodeMaintenanceStarting1", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("1|2|3", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("StartTimeInUTC|", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("IsReplica|", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("SSLPort|", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("NonSSLPort |", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("StartTimeInUTC|thisisthestart", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData(null, AzureNotificationType.Unknown, null, false, null, 0, 0)] + public void TestAzureMaintenanceEventStrings(string message, AzureNotificationType expectedEventType, string expectedStart, bool expectedIsReplica, string expectedIP, int expectedSSLPort, int expectedNonSSLPort) + { + DateTime? expectedStartTimeUtc = null; + if (expectedStart != null && DateTime.TryParseExact(expectedStart, "s", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTime startTimeUtc)) + { + expectedStartTimeUtc = DateTime.SpecifyKind(startTimeUtc, DateTimeKind.Utc); + } + IPAddress.TryParse(expectedIP, out IPAddress expectedIPAddress); + + var azureMaintenance = new AzureMaintenanceEvent(message); + + Assert.Equal(expectedEventType, azureMaintenance.NotificationType); + Assert.Equal(expectedStartTimeUtc, azureMaintenance.StartTimeUtc); + Assert.Equal(expectedIsReplica, azureMaintenance.IsReplica); + Assert.Equal(expectedIPAddress, azureMaintenance.IPAddress); + Assert.Equal(expectedSSLPort, azureMaintenance.SslPort); + Assert.Equal(expectedNonSSLPort, azureMaintenance.NonSslPort); + } + } +} diff --git a/tests/StackExchange.Redis.Tests/PubSub.cs b/tests/StackExchange.Redis.Tests/PubSub.cs index ad0e9060c..edefb781e 100644 --- a/tests/StackExchange.Redis.Tests/PubSub.cs +++ b/tests/StackExchange.Redis.Tests/PubSub.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; +using StackExchange.Redis.Maintenance; using Xunit; using Xunit.Abstractions; // ReSharper disable AccessToModifiedClosure @@ -707,6 +708,38 @@ public async Task TestSubscribeUnsubscribeAndSubscribeAgain() } } + [Fact] + public async Task AzureRedisEventsAutomaticSubscribe() + { + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); + + bool didUpdate = false; + var options = new ConfigurationOptions() + { + EndPoints = { TestConfig.Current.AzureCacheServer }, + Password = TestConfig.Current.AzureCachePassword, + Ssl = true + }; + + using (var connection = await ConnectionMultiplexer.ConnectAsync(options)) + { + connection.ServerMaintenanceEvent += (object sender, ServerMaintenanceEvent e) => + { + if (e is AzureMaintenanceEvent) + { + didUpdate = true; + } + }; + + var pubSub = connection.GetSubscriber(); + await pubSub.PublishAsync("AzureRedisEvents", "HI"); + await Task.Delay(100); + + Assert.True(didUpdate); + } + } + #if DEBUG [Fact] public async Task SubscriptionsSurviveConnectionFailureAsync() From c7f6b472527d97831eb5500e2684ae3ab58534ac Mon Sep 17 00:00:00 2001 From: James Powell Date: Thu, 14 Oct 2021 06:01:24 -0400 Subject: [PATCH 029/435] Updated the Configuration.md to verbalize ECHO permission require (#1888) * Updated the Configuration.md to verbalize ECHO permission require My ACL user was never granted the ECHO permission since my logic never had a use case for it. When I first configured StackExchange.Redis, I kept getting a connection exception, but I have a nodejs implementation that uses the node-redis npm package to connect to my server just fine. I looked through your logs and source code and found what I was missing. "...Error: NOPERM this user has no permissions to run the 'echo' command or its subcommand [serverName]:6379: OnConnectedAsync completed (Disconnected)..." * Added information of ECHO command dependency For Redis v6+, the ECHO command is needed for ACL users to confirm service connectivity. --- docs/Configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Configuration.md b/docs/Configuration.md index f73122608..7e35a96d0 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1,6 +1,7 @@ Configuration === +When connecting to Redis version 6 or above with an ACL configured, your ACL user needs to at least have permissions to run the ECHO command. We run this command to verify that we have a valid connection to the Redis service. Because there are lots of different ways to configure redis, StackExchange.Redis offers a rich configuration model, which is invoked when calling `Connect` (or `ConnectAsync`): ```csharp From b5ec0806a861ab793e7228cd5ebfe69158aab986 Mon Sep 17 00:00:00 2001 From: mgravell Date: Thu, 14 Oct 2021 11:08:18 +0100 Subject: [PATCH 030/435] release notes --- docs/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 74b1356c4..5521a940d 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,6 +1,9 @@ # Release Notes ## Unreleased + +## 2.2.79 + - NRediSearch: Support on json index (#1808 via AvitalFineRedis) - NRediSearch: Support sortable TagFields and unNormalizedForm for Tag & Text Fields (#1862 via slorello89 & AvitalFineRedis) - fix potential errors getting socket bytes (#1836 via NickCraver) From f0e485daa85e5bc2dfc1ff7eebb77c4d6a072696 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 19 Oct 2021 12:42:56 -0400 Subject: [PATCH 031/435] SimulateConnectionFailure enhancements (#1885) Part of #1864. Extends current SimulateConnectionFailure with the a failure type enum that triggers an explicit pipe failure. Previous we had races here but this addition proactively kills the pipe(s) with a specific exception. --- src/StackExchange.Redis/Interfaces/IServer.cs | 3 ++- src/StackExchange.Redis/PhysicalBridge.cs | 4 +-- src/StackExchange.Redis/PhysicalConnection.cs | 27 +++++++++++++++++++ src/StackExchange.Redis/RedisServer.cs | 2 +- src/StackExchange.Redis/ServerEndPoint.cs | 6 ++--- .../SimulatedFailureType.cs | 17 ++++++++++++ tests/StackExchange.Redis.Tests/AsyncTests.cs | 2 +- tests/StackExchange.Redis.Tests/BasicOps.cs | 4 +-- .../ConnectFailTimeout.cs | 2 +- .../ConnectingFailDetection.cs | 20 +++++++------- .../ConnectionFailedErrors.cs | 4 +-- .../ConnectionShutdown.cs | 2 +- .../ExceptionFactoryTests.cs | 4 +-- .../Issues/SO24807536.cs | 4 +-- tests/StackExchange.Redis.Tests/PubSub.cs | 7 +++-- 15 files changed, 75 insertions(+), 33 deletions(-) create mode 100644 src/StackExchange.Redis/SimulatedFailureType.cs diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 07c2615c9..792807268 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -1054,6 +1054,7 @@ internal static class IServerExtensions /// For testing only: Break the connection without mercy or thought /// /// The server to simulate failure on. - public static void SimulateConnectionFailure(this IServer server) => (server as RedisServer)?.SimulateConnectionFailure(); + /// The type of failure(s) to simulate. + public static void SimulateConnectionFailure(this IServer server, SimulatedFailureType failureType) => (server as RedisServer)?.SimulateConnectionFailure(failureType); } } diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 561bd6e9b..4fe86900d 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -1347,13 +1347,13 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne /// /// For testing only /// - internal void SimulateConnectionFailure() + internal void SimulateConnectionFailure(SimulatedFailureType failureType) { if (!Multiplexer.RawConfig.AllowAdmin) { throw ExceptionFactory.AdminModeNotEnabled(Multiplexer.IncludeDetailInExceptions, RedisCommand.DEBUG, null, ServerEndPoint); // close enough } - physical?.RecordConnectionFailed(ConnectionFailureType.SocketFailure); + physical?.SimulateConnectionFailure(failureType); } internal RedisCommand? GetActiveMessage() => Volatile.Read(ref _activeMessage)?.Command; diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index bbbccf4e3..9c2672eaa 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -290,6 +290,33 @@ public Task FlushAsync() return Task.CompletedTask; } + internal void SimulateConnectionFailure(SimulatedFailureType failureType) + { + if (connectionType == ConnectionType.Interactive) + { + if (failureType.HasFlag(SimulatedFailureType.InteractiveInbound)) + { + _ioPipe?.Input.Complete(new Exception("Simulating interactive input failure")); + } + if (failureType.HasFlag(SimulatedFailureType.InteractiveOutbound)) + { + _ioPipe?.Output.Complete(new Exception("Simulating interactive output failure")); + } + } + else if (connectionType == ConnectionType.Subscription) + { + if (failureType.HasFlag(SimulatedFailureType.SubscriptionInbound)) + { + _ioPipe?.Input.Complete(new Exception("Simulating subscription input failure")); + } + if (failureType.HasFlag(SimulatedFailureType.SubscriptionOutbound)) + { + _ioPipe?.Output.Complete(new Exception("Simulating subscription output failure")); + } + } + RecordConnectionFailed(ConnectionFailureType.SocketFailure); + } + public void RecordConnectionFailed(ConnectionFailureType failureType, Exception innerException = null, [CallerMemberName] string origin = null, bool isInitialConnect = false, IDuplexPipe connectingPipe = null ) diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 649e07923..8913aef74 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -924,7 +924,7 @@ public Task ExecuteAsync(string command, ICollection args, /// /// For testing only /// - internal void SimulateConnectionFailure() => server.SimulateConnectionFailure(); + internal void SimulateConnectionFailure(SimulatedFailureType failureType) => server.SimulateConnectionFailure(failureType); public Task LatencyDoctorAsync(CommandFlags flags = CommandFlags.None) { diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index e3f180ab1..f6050d4e5 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -905,10 +905,10 @@ private void SetConfig(ref T field, T value, [CallerMemberName] string caller /// /// For testing only /// - internal void SimulateConnectionFailure() + internal void SimulateConnectionFailure(SimulatedFailureType failureType) { - interactive?.SimulateConnectionFailure(); - subscription?.SimulateConnectionFailure(); + interactive?.SimulateConnectionFailure(failureType); + subscription?.SimulateConnectionFailure(failureType); } } } diff --git a/src/StackExchange.Redis/SimulatedFailureType.cs b/src/StackExchange.Redis/SimulatedFailureType.cs new file mode 100644 index 000000000..0084746a7 --- /dev/null +++ b/src/StackExchange.Redis/SimulatedFailureType.cs @@ -0,0 +1,17 @@ +using System; + +namespace StackExchange.Redis +{ + [Flags] + internal enum SimulatedFailureType + { + None = 0, + InteractiveInbound = 1 << 0, + InteractiveOutbound = 1 << 1, + SubscriptionInbound = 1 << 2, + SubscriptionOutbound = 1 << 3, + AllInbound = InteractiveInbound | SubscriptionInbound, + AllOutbound = InteractiveOutbound | SubscriptionOutbound, + All = AllInbound | AllOutbound, + } +} diff --git a/tests/StackExchange.Redis.Tests/AsyncTests.cs b/tests/StackExchange.Redis.Tests/AsyncTests.cs index 5367ef0b6..5ee26f815 100644 --- a/tests/StackExchange.Redis.Tests/AsyncTests.cs +++ b/tests/StackExchange.Redis.Tests/AsyncTests.cs @@ -33,7 +33,7 @@ public void AsyncTasksReportFailureIfServerUnavailable() Assert.True(conn.Wait(b)); conn.AllowConnect = false; - server.SimulateConnectionFailure(); + server.SimulateConnectionFailure(SimulatedFailureType.All); var c = db.SetAddAsync(key, "c"); Assert.True(c.IsFaulted, "faulted"); diff --git a/tests/StackExchange.Redis.Tests/BasicOps.cs b/tests/StackExchange.Redis.Tests/BasicOps.cs index 6a5d352d8..7a1662d99 100644 --- a/tests/StackExchange.Redis.Tests/BasicOps.cs +++ b/tests/StackExchange.Redis.Tests/BasicOps.cs @@ -286,14 +286,14 @@ public void GetWithExpiryWrongTypeSync() public async Task TestSevered() { SetExpectedAmbientFailureCount(2); - using (var muxer = Create(allowAdmin: true)) + using (var muxer = Create(allowAdmin: true, shared: false)) { var db = muxer.GetDatabase(); string key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, key, flags: CommandFlags.FireAndForget); var server = GetServer(muxer); - server.SimulateConnectionFailure(); + server.SimulateConnectionFailure(SimulatedFailureType.All); var watch = Stopwatch.StartNew(); await UntilCondition(TimeSpan.FromSeconds(10), () => server.IsConnected); watch.Stop(); diff --git a/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs b/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs index 6ca4ef63c..c52082d12 100644 --- a/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs +++ b/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs @@ -28,7 +28,7 @@ void innerScenario() // No need to delay, we're going to try a disconnected connection immediately so it'll fail... conn.IgnoreConnect = true; Log("simulating failure"); - server.SimulateConnectionFailure(); + server.SimulateConnectionFailure(SimulatedFailureType.All); Log("simulated failure"); conn.IgnoreConnect = false; Log("pinging - expect failure"); diff --git a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs index c78a1c4df..d1020e6e3 100644 --- a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs +++ b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs @@ -12,13 +12,12 @@ public ConnectingFailDetection(ITestOutputHelper output) : base (output) { } protected override string GetConfiguration() => TestConfig.Current.MasterServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; -#if DEBUG [Fact] public async Task FastNoticesFailOnConnectingSyncCompletion() { try { - using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true)) + using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false)) { var conn = muxer.GetDatabase(); conn.Ping(); @@ -29,12 +28,12 @@ public async Task FastNoticesFailOnConnectingSyncCompletion() muxer.AllowConnect = false; // muxer.IsConnected is true of *any* are connected, simulate failure for all cases. - server.SimulateConnectionFailure(); + server.SimulateConnectionFailure(SimulatedFailureType.All); Assert.False(server.IsConnected); Assert.True(server2.IsConnected); Assert.True(muxer.IsConnected); - server2.SimulateConnectionFailure(); + server2.SimulateConnectionFailure(SimulatedFailureType.All); Assert.False(server.IsConnected); Assert.False(server2.IsConnected); Assert.False(muxer.IsConnected); @@ -42,7 +41,7 @@ public async Task FastNoticesFailOnConnectingSyncCompletion() // should reconnect within 1 keepalive interval muxer.AllowConnect = true; Log("Waiting for reconnect"); - await Task.Delay(2000).ForAwait(); + await UntilCondition(TimeSpan.FromSeconds(2), () => muxer.IsConnected).ForAwait(); Assert.True(muxer.IsConnected); } @@ -58,7 +57,7 @@ public async Task FastNoticesFailOnConnectingAsyncCompletion() { try { - using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true)) + using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false)) { var conn = muxer.GetDatabase(); conn.Ping(); @@ -69,12 +68,12 @@ public async Task FastNoticesFailOnConnectingAsyncCompletion() muxer.AllowConnect = false; // muxer.IsConnected is true of *any* are connected, simulate failure for all cases. - server.SimulateConnectionFailure(); + server.SimulateConnectionFailure(SimulatedFailureType.All); Assert.False(server.IsConnected); Assert.True(server2.IsConnected); Assert.True(muxer.IsConnected); - server2.SimulateConnectionFailure(); + server2.SimulateConnectionFailure(SimulatedFailureType.All); Assert.False(server.IsConnected); Assert.False(server2.IsConnected); Assert.False(muxer.IsConnected); @@ -82,7 +81,7 @@ public async Task FastNoticesFailOnConnectingAsyncCompletion() // should reconnect within 1 keepalive interval muxer.AllowConnect = true; Log("Waiting for reconnect"); - await Task.Delay(2000).ForAwait(); + await UntilCondition(TimeSpan.FromSeconds(2), () => muxer.IsConnected).ForAwait(); Assert.True(muxer.IsConnected); } @@ -115,7 +114,7 @@ public async Task Issue922_ReconnectRaised() Assert.Equal(0, Volatile.Read(ref restoreCount)); var server = muxer.GetServer(TestConfig.Current.MasterServerAndPort); - server.SimulateConnectionFailure(); + server.SimulateConnectionFailure(SimulatedFailureType.All); await UntilCondition(TimeSpan.FromSeconds(10), () => Volatile.Read(ref failCount) + Volatile.Read(ref restoreCount) == 4); // interactive+subscriber = 2 @@ -123,7 +122,6 @@ public async Task Issue922_ReconnectRaised() Assert.Equal(2, Volatile.Read(ref restoreCount)); } } -#endif [Fact] public void ConnectsWhenBeginConnectCompletesSynchronously() diff --git a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs b/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs index 8fe8d47e6..cd3521788 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs @@ -174,7 +174,7 @@ public async Task CheckFailureRecovered() { try { - using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, log: Writer)) + using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, log: Writer, shared: false)) { await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); void innerScenario() @@ -184,7 +184,7 @@ void innerScenario() muxer.AllowConnect = false; - server.SimulateConnectionFailure(); + server.SimulateConnectionFailure(SimulatedFailureType.All); var lastFailure = ((RedisConnectionException)muxer.GetServerSnapshot()[0].LastException).FailureType; // Depending on heartbat races, the last exception will be a socket failure or an internal (follow-up) failure diff --git a/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs b/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs index ee7dc0b5c..a4e720772 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs @@ -38,7 +38,7 @@ public async Task ShutdownRaisesConnectionFailedAndRestore() var server = conn.GetServer(TestConfig.Current.MasterServer, TestConfig.Current.MasterPort); SetExpectedAmbientFailureCount(2); - server.SimulateConnectionFailure(); + server.SimulateConnectionFailure(SimulatedFailureType.All); db.Ping(CommandFlags.FireAndForget); await Task.Delay(250).ForAwait(); diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index fcce9bcca..25606a8c2 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -40,7 +40,7 @@ public void MultipleEndpointsThrowConnectionException() foreach (var endpoint in muxer.GetEndPoints()) { - muxer.GetServer(endpoint).SimulateConnectionFailure(); + muxer.GetServer(endpoint).SimulateConnectionFailure(SimulatedFailureType.All); } var ex = ExceptionFactory.NoConnectionAvailable(muxer as ConnectionMultiplexer, null, null); @@ -68,7 +68,7 @@ public void ServerTakesPrecendenceOverSnapshot() muxer.GetDatabase(); muxer.AllowConnect = false; - muxer.GetServer(muxer.GetEndPoints()[0]).SimulateConnectionFailure(); + muxer.GetServer(muxer.GetEndPoints()[0]).SimulateConnectionFailure(SimulatedFailureType.All); var ex = ExceptionFactory.NoConnectionAvailable(muxer as ConnectionMultiplexer, null, muxer.GetServerSnapshot()[0]); Assert.IsType(ex); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs b/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs index 54c6483c9..c795927eb 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs @@ -20,7 +20,7 @@ public async Task Exec() // setup some data cache.KeyDelete(key, CommandFlags.FireAndForget); cache.HashSet(key, "full", "some value", flags: CommandFlags.FireAndForget); - cache.KeyExpire(key, TimeSpan.FromSeconds(1), CommandFlags.FireAndForget); + cache.KeyExpire(key, TimeSpan.FromSeconds(2), CommandFlags.FireAndForget); // test while exists var keyExists = cache.KeyExists(key); @@ -31,7 +31,7 @@ public async Task Exec() Assert.Equal("some value", fullWait.Result); // wait for expiry - await Task.Delay(2000).ForAwait(); + await UntilCondition(TimeSpan.FromSeconds(10), () => !cache.KeyExists(key)).ForAwait(); // test once expired keyExists = cache.KeyExists(key); diff --git a/tests/StackExchange.Redis.Tests/PubSub.cs b/tests/StackExchange.Redis.Tests/PubSub.cs index edefb781e..e4d22798d 100644 --- a/tests/StackExchange.Redis.Tests/PubSub.cs +++ b/tests/StackExchange.Redis.Tests/PubSub.cs @@ -740,11 +740,10 @@ public async Task AzureRedisEventsAutomaticSubscribe() } } -#if DEBUG [Fact] public async Task SubscriptionsSurviveConnectionFailureAsync() { - using (var muxer = Create(allowAdmin: true)) + using (var muxer = Create(allowAdmin: true, shared: false)) { RedisChannel channel = Me(); var sub = muxer.GetSubscriber(); @@ -753,6 +752,7 @@ await sub.SubscribeAsync(channel, delegate { Interlocked.Increment(ref counter); }).ConfigureAwait(false); + await Task.Delay(200).ConfigureAwait(false); await sub.PublishAsync(channel, "abc").ConfigureAwait(false); sub.Ping(); await Task.Delay(200).ConfigureAwait(false); @@ -760,7 +760,7 @@ await sub.SubscribeAsync(channel, delegate var server = GetServer(muxer); Assert.Equal(1, server.GetCounters().Subscription.SocketCount); - server.SimulateConnectionFailure(); + server.SimulateConnectionFailure(SimulatedFailureType.All); SetExpectedAmbientFailureCount(2); await Task.Delay(200).ConfigureAwait(false); sub.Ping(); @@ -771,6 +771,5 @@ await sub.SubscribeAsync(channel, delegate Assert.Equal(2, Thread.VolatileRead(ref counter)); } } -#endif } } From 8298d4c152d46aecb8720e9e515ba24c1ef995f5 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 21 Oct 2021 09:54:19 -0400 Subject: [PATCH 032/435] NRediSearch split (#1890) PR to complete split of NRediSearch library into a sibling repo, now located at StackExchange/NRediSearch --- .devcontainer/TestConfig.json | 1 - .devcontainer/docker-compose.yml | 6 +- .github/workflows/CI.yml | 33 - README.md | 1 - StackExchange.Redis.sln | 24 +- src/NRediSearch/AddOptions.cs | 66 - .../Aggregation/AggregationBuilder.cs | 150 -- .../Aggregation/AggregationRequest.cs | 168 -- src/NRediSearch/Aggregation/Group.cs | 50 - src/NRediSearch/Aggregation/Limit.cs | 25 - .../Aggregation/Reducers/Reducer.cs | 51 - .../Aggregation/Reducers/Reducers.cs | 103 -- src/NRediSearch/Aggregation/Row.cs | 24 - src/NRediSearch/Aggregation/SortedField.cs | 23 - src/NRediSearch/AggregationResult.cs | 47 - src/NRediSearch/AssemblyInfo.cs | 4 - src/NRediSearch/Client.cs | 1497 ----------------- src/NRediSearch/Document.cs | 89 - src/NRediSearch/Extensions.cs | 35 - src/NRediSearch/FieldName.cs | 47 - src/NRediSearch/InfoResult.cs | 127 -- src/NRediSearch/Literals.cs | 50 - src/NRediSearch/NRediSearch.csproj | 11 - src/NRediSearch/Query.cs | 519 ------ src/NRediSearch/QueryBuilder/DisjunctNode.cs | 25 - .../QueryBuilder/DisjunctUnionNode.cs | 14 - src/NRediSearch/QueryBuilder/GeoValue.cs | 33 - src/NRediSearch/QueryBuilder/IntersectNode.cs | 12 - src/NRediSearch/QueryBuilder/Node.cs | 31 - src/NRediSearch/QueryBuilder/OptionalNode.cs | 25 - src/NRediSearch/QueryBuilder/QueryBuilder.cs | 123 -- src/NRediSearch/QueryBuilder/QueryNode.cs | 99 -- src/NRediSearch/QueryBuilder/RangeValue.cs | 51 - src/NRediSearch/QueryBuilder/StringJoiner.cs | 25 - src/NRediSearch/QueryBuilder/UnionNode.cs | 9 - src/NRediSearch/QueryBuilder/Value.cs | 9 - src/NRediSearch/QueryBuilder/ValueNode.cs | 93 - src/NRediSearch/QueryBuilder/Values.cs | 56 - src/NRediSearch/Schema.cs | 334 ---- src/NRediSearch/SearchResult.cs | 100 -- src/NRediSearch/Suggestion.cs | 109 -- src/NRediSearch/SuggestionOptions.cs | 107 -- src/StackExchange.Redis/AssemblyInfoHack.cs | 1 - tests/NRediSearch.Test/AssemblyInfo.cs | 15 - tests/NRediSearch.Test/Attributes.cs | 7 - .../ClientTests/AggregationBuilderTests.cs | 186 -- .../ClientTests/AggregationTest.cs | 54 - .../ClientTests/ClientTest.cs | 1160 ------------- tests/NRediSearch.Test/ExampleUsage.cs | 191 --- tests/NRediSearch.Test/Issues/Issue940.cs | 17 - .../NRediSearch.Test/NRediSearch.Test.csproj | 14 - .../QueryBuilder/BuilderTest.cs | 140 -- tests/NRediSearch.Test/QueryTest.cs | 200 --- tests/NRediSearch.Test/RediSearchTestBase.cs | 165 -- tests/RedisConfigs/docker-compose.yml | 4 - .../Helpers/TestConfig.cs | 4 - 56 files changed, 3 insertions(+), 6561 deletions(-) delete mode 100644 src/NRediSearch/AddOptions.cs delete mode 100644 src/NRediSearch/Aggregation/AggregationBuilder.cs delete mode 100644 src/NRediSearch/Aggregation/AggregationRequest.cs delete mode 100644 src/NRediSearch/Aggregation/Group.cs delete mode 100644 src/NRediSearch/Aggregation/Limit.cs delete mode 100644 src/NRediSearch/Aggregation/Reducers/Reducer.cs delete mode 100644 src/NRediSearch/Aggregation/Reducers/Reducers.cs delete mode 100644 src/NRediSearch/Aggregation/Row.cs delete mode 100644 src/NRediSearch/Aggregation/SortedField.cs delete mode 100644 src/NRediSearch/AggregationResult.cs delete mode 100644 src/NRediSearch/AssemblyInfo.cs delete mode 100644 src/NRediSearch/Client.cs delete mode 100644 src/NRediSearch/Document.cs delete mode 100644 src/NRediSearch/Extensions.cs delete mode 100644 src/NRediSearch/FieldName.cs delete mode 100644 src/NRediSearch/InfoResult.cs delete mode 100644 src/NRediSearch/Literals.cs delete mode 100644 src/NRediSearch/NRediSearch.csproj delete mode 100644 src/NRediSearch/Query.cs delete mode 100644 src/NRediSearch/QueryBuilder/DisjunctNode.cs delete mode 100644 src/NRediSearch/QueryBuilder/DisjunctUnionNode.cs delete mode 100644 src/NRediSearch/QueryBuilder/GeoValue.cs delete mode 100644 src/NRediSearch/QueryBuilder/IntersectNode.cs delete mode 100644 src/NRediSearch/QueryBuilder/Node.cs delete mode 100644 src/NRediSearch/QueryBuilder/OptionalNode.cs delete mode 100644 src/NRediSearch/QueryBuilder/QueryBuilder.cs delete mode 100644 src/NRediSearch/QueryBuilder/QueryNode.cs delete mode 100644 src/NRediSearch/QueryBuilder/RangeValue.cs delete mode 100644 src/NRediSearch/QueryBuilder/StringJoiner.cs delete mode 100644 src/NRediSearch/QueryBuilder/UnionNode.cs delete mode 100644 src/NRediSearch/QueryBuilder/Value.cs delete mode 100644 src/NRediSearch/QueryBuilder/ValueNode.cs delete mode 100644 src/NRediSearch/QueryBuilder/Values.cs delete mode 100644 src/NRediSearch/Schema.cs delete mode 100644 src/NRediSearch/SearchResult.cs delete mode 100644 src/NRediSearch/Suggestion.cs delete mode 100644 src/NRediSearch/SuggestionOptions.cs delete mode 100644 tests/NRediSearch.Test/AssemblyInfo.cs delete mode 100644 tests/NRediSearch.Test/Attributes.cs delete mode 100644 tests/NRediSearch.Test/ClientTests/AggregationBuilderTests.cs delete mode 100644 tests/NRediSearch.Test/ClientTests/AggregationTest.cs delete mode 100644 tests/NRediSearch.Test/ClientTests/ClientTest.cs delete mode 100644 tests/NRediSearch.Test/ExampleUsage.cs delete mode 100644 tests/NRediSearch.Test/Issues/Issue940.cs delete mode 100644 tests/NRediSearch.Test/NRediSearch.Test.csproj delete mode 100644 tests/NRediSearch.Test/QueryBuilder/BuilderTest.cs delete mode 100644 tests/NRediSearch.Test/QueryTest.cs delete mode 100644 tests/NRediSearch.Test/RediSearchTestBase.cs diff --git a/.devcontainer/TestConfig.json b/.devcontainer/TestConfig.json index 54ff589ce..2ec990b9f 100644 --- a/.devcontainer/TestConfig.json +++ b/.devcontainer/TestConfig.json @@ -4,7 +4,6 @@ "SecureServer": "redis", "FailoverMasterServer": "redis", "FailoverReplicaServer": "redis", - "RediSearchServer": "redisearch", "IPv4Server": "redis", "RemoteServer": "redis", "SentinelServer": "redis", diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 2b458776f..a801d6f1e 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -12,16 +12,12 @@ services: - ./TestConfig.json:/workspace/tests/StackExchange.Redis.Tests/TestConfig.json:ro depends_on: - redis - - redisearch links: - "redis:redis" - - "redisearch:redisearch" command: /bin/sh -c "while sleep 1000; do :; done" redis: build: context: ../tests/RedisConfigs dockerfile: Dockerfile sysctls : - net.core.somaxconn: '511' - redisearch: - image: redislabs/redisearch:latest \ No newline at end of file + net.core.somaxconn: '511' \ No newline at end of file diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d3a01e707..5dacdc4d2 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -41,39 +41,6 @@ jobs: - name: .NET Lib Pack run: dotnet pack src/StackExchange.Redis/StackExchange.Redis.csproj --no-build -c Release /p:Packing=true /p:PackageOutputPath=%CD%\.nupkgs /p:CI=true - nredisearch: - name: NRediSearch (Ubuntu) - runs-on: ubuntu-latest - services: - redisearch: - image: redislabs/redisearch:latest - ports: - - 6385:6379 - steps: - - name: Checkout code - uses: actions/checkout@v1 - - name: Setup .NET Core 3.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '3.1.x' - - name: Setup .NET 5.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '5.0.x' - - name: .NET Build - run: dotnet build Build.csproj -c Release /p:CI=true - - name: NRedisSearch.Tests - run: dotnet test tests/NRediSearch.Test/NRediSearch.Test.csproj -c Release --logger trx --results-directory ./test-results/ /p:CI=true - - uses: dorny/test-reporter@v1 - continue-on-error: true - if: success() || failure() - with: - name: NRedisSearch.Tests - Results - path: 'test-results/*.trx' - reporter: dotnet-trx - - name: .NET Lib Pack - run: dotnet pack src/NRediSearch/NRediSearch.csproj --no-build -c Release /p:Packing=true /p:PackageOutputPath=%CD%\.nupkgs /p:CI=true - windows: name: StackExchange.Redis (Windows Server 2019) runs-on: windows-2019 diff --git a/README.md b/README.md index 6d1263678..732579c1b 100644 --- a/README.md +++ b/README.md @@ -14,4 +14,3 @@ MyGet Pre-release feed: https://www.myget.org/gallery/stackoverflow | Package | NuGet Stable | NuGet Pre-release | Downloads | MyGet | | ------- | ------------ | ----------------- | --------- | ----- | | [StackExchange.Redis](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/dt/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | -| [NRediSearch](https://www.nuget.org/packages/NRediSearch/) | [![NRediSearch](https://img.shields.io/nuget/v/NRediSearch.svg)](https://www.nuget.org/packages/NRediSearch/) | [![NRediSearch](https://img.shields.io/nuget/vpre/NRediSearch.svg)](https://www.nuget.org/packages/NRediSearch/) | [![NRediSearch](https://img.shields.io/nuget/dt/NRediSearch.svg)](https://www.nuget.org/packages/NRediSearch/) | [![NRediSearch MyGet](https://img.shields.io/myget/stackoverflow/vpre/NRediSearch.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/NRediSearch) | diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index ca575d8c9..d33bf7db0 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28531.58 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31808.319 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3AD17044-6BFF-4750-9AC2-2CA466375F2A}" ProjectSection(SolutionItems) = preProject @@ -41,10 +41,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackExchange.Redis.Tests", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicTest", "tests\BasicTest\BasicTest.csproj", "{939FA5F7-16AA-4847-812B-6EBC3748A86D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NRediSearch", "src\NRediSearch\NRediSearch.csproj", "{71455B07-E628-4F3A-9FFF-9EC63071F78E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NRediSearch.Test", "tests\NRediSearch.Test\NRediSearch.Test.csproj", "{94D233F5-2400-4542-98B9-BA72005C57DC}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sentinel", "Sentinel", "{36255A0A-89EC-43C8-A642-F4C1ACAEF5BC}" ProjectSection(SolutionItems) = preProject tests\RedisConfigs\Sentinel\redis-7010.conf = tests\RedisConfigs\Sentinel\redis-7010.conf @@ -137,12 +133,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{A9F81D tests\RedisConfigs\Docker\supervisord.conf = tests\RedisConfigs\Docker\supervisord.conf EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RediSearch", "RediSearch", "{3FA2A7C6-DA16-4DEF-ACE0-34573A4AD430}" - ProjectSection(SolutionItems) = preProject - tests\RedisConfigs\RediSearch\redisearch-6385.conf = tests\RedisConfigs\RediSearch\redisearch-6385.conf - tests\RedisConfigs\RediSearch\redisearch.md = tests\RedisConfigs\RediSearch\redisearch.md - EndProjectSection -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -161,14 +151,6 @@ Global {939FA5F7-16AA-4847-812B-6EBC3748A86D}.Debug|Any CPU.Build.0 = Debug|Any CPU {939FA5F7-16AA-4847-812B-6EBC3748A86D}.Release|Any CPU.ActiveCfg = Release|Any CPU {939FA5F7-16AA-4847-812B-6EBC3748A86D}.Release|Any CPU.Build.0 = Release|Any CPU - {71455B07-E628-4F3A-9FFF-9EC63071F78E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {71455B07-E628-4F3A-9FFF-9EC63071F78E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {71455B07-E628-4F3A-9FFF-9EC63071F78E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {71455B07-E628-4F3A-9FFF-9EC63071F78E}.Release|Any CPU.Build.0 = Release|Any CPU - {94D233F5-2400-4542-98B9-BA72005C57DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {94D233F5-2400-4542-98B9-BA72005C57DC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {94D233F5-2400-4542-98B9-BA72005C57DC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {94D233F5-2400-4542-98B9-BA72005C57DC}.Release|Any CPU.Build.0 = Release|Any CPU {8FDB623D-779B-4A84-BC6B-75106E41D8A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8FDB623D-779B-4A84-BC6B-75106E41D8A4}.Debug|Any CPU.Build.0 = Debug|Any CPU {8FDB623D-779B-4A84-BC6B-75106E41D8A4}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -202,8 +184,6 @@ Global {EF84877F-59BE-41BE-9013-E765AF0BB72E} = {00CA0876-DA9F-44E8-B0DC-A88716BF347A} {3B8BD8F1-8BFC-4D8C-B4DA-25FFAF3D1DBE} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} {939FA5F7-16AA-4847-812B-6EBC3748A86D} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} - {71455B07-E628-4F3A-9FFF-9EC63071F78E} = {00CA0876-DA9F-44E8-B0DC-A88716BF347A} - {94D233F5-2400-4542-98B9-BA72005C57DC} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} {36255A0A-89EC-43C8-A642-F4C1ACAEF5BC} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05} {A3B4B972-5BD2-4D90-981F-7E51E350E628} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05} {38BDEEED-7BEB-4B1F-9CE0-256D63F9C502} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05} diff --git a/src/NRediSearch/AddOptions.cs b/src/NRediSearch/AddOptions.cs deleted file mode 100644 index bd3fb9d20..000000000 --- a/src/NRediSearch/AddOptions.cs +++ /dev/null @@ -1,66 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -namespace NRediSearch -{ - public sealed class AddOptions - { - public enum ReplacementPolicy - { - /// - /// The default mode. This will cause the add operation to fail if the document already exists - /// - None, - /// - /// Replace/reindex the entire document. This has the effect of atomically deleting the previous - /// document and replacing it with the context of the new document. Fields in the old document which - /// are not present in the new document are lost - /// - Full, - /// - /// Only reindex/replace fields that are updated in the command. Fields in the old document which are - /// not present in the new document are preserved.Fields that are present in both are overwritten by - /// the new document - /// - Partial, - } - - public string Language { get; set; } - public bool NoSave { get; set; } - public ReplacementPolicy ReplacePolicy { get; set; } - - /// - /// Create a new DocumentOptions object. Methods can later be chained via a builder-like pattern - /// - public AddOptions() { } - - /// - /// Set the indexing language - /// - /// Set the indexing language - public AddOptions SetLanguage(string language) - { - Language = language; - return this; - } - /// - /// Whether document's contents should not be stored in the database. - /// - /// if enabled, the document is not stored on the server. This saves disk/memory space on the - /// server but prevents retrieving the document itself. - public AddOptions SetNoSave(bool enabled) - { - NoSave = enabled; - return this; - } - - /// - /// Indicate the behavior for the existing document. - /// - /// One of the replacement modes. - public AddOptions SetReplacementPolicy(ReplacementPolicy mode) - { - ReplacePolicy = mode; - return this; - } - } -} diff --git a/src/NRediSearch/Aggregation/AggregationBuilder.cs b/src/NRediSearch/Aggregation/AggregationBuilder.cs deleted file mode 100644 index f3dc4b47f..000000000 --- a/src/NRediSearch/Aggregation/AggregationBuilder.cs +++ /dev/null @@ -1,150 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ -using System.Collections.Generic; -using System.Linq; -using NRediSearch.Aggregation.Reducers; - -namespace NRediSearch.Aggregation -{ - public sealed class AggregationBuilder - { - private readonly List _args = new List(); - - public bool IsWithCursor { get; private set; } - - internal string GetArgsString() => string.Join(" ", _args); - - public AggregationBuilder(string query = "*") => _args.Add(query); - - public AggregationBuilder Load(params string[] fields) - { - AddCommandArguments(_args, "LOAD", fields); - - return this; - } - - public AggregationBuilder Limit(int offset, int count) - { - var limit = new Limit(offset, count); - - limit.SerializeRedisArgs(_args); - - return this; - } - - public AggregationBuilder Limit(int count) => Limit(0, count); - - public AggregationBuilder SortBy(params SortedField[] fields) - { - _args.Add("SORTBY"); - _args.Add(fields.Length * 2); - - foreach (var field in fields) - { - _args.Add(field.Field); - _args.Add(field.OrderAsArg()); - } - - return this; - } - - public AggregationBuilder SortBy(int max, params SortedField[] fields) - { - SortBy(fields); - - if (max > 0) - { - _args.Add("MAX"); - _args.Add(max); - } - - return this; - } - - public AggregationBuilder SortByAscending(string field) => SortBy(SortedField.Ascending(field)); - - public AggregationBuilder SortByDescending(string field) => SortBy(SortedField.Descending(field)); - - public AggregationBuilder Apply(string projection, string alias) - { - _args.Add("APPLY"); - _args.Add(projection); - _args.Add("AS"); - _args.Add(alias); - - return this; - } - - public AggregationBuilder GroupBy(IReadOnlyCollection fields, IReadOnlyCollection reducers) - { - var group = new Group(fields.ToArray()); - - foreach (var r in reducers) - { - group.Reduce(r); - } - - GroupBy(group); - - return this; - } - - public AggregationBuilder GroupBy(string field, params Reducer[] reducers) => GroupBy(new[] { field }, reducers); - - public AggregationBuilder GroupBy(Group group) - { - _args.Add("GROUPBY"); - - group.SerializeRedisArgs(_args); - - return this; - } - - public AggregationBuilder Filter(string expression) - { - _args.Add("FILTER"); - _args.Add(expression); - - return this; - } - - public AggregationBuilder Cursor(int count, long maxIdle) - { - IsWithCursor = true; - - if (count > 0) - { - _args.Add("WITHCURSOR"); - _args.Add("COUNT"); - _args.Add(count); - - if (maxIdle < long.MaxValue && maxIdle >= 0) - { - _args.Add("MAXIDLE"); - _args.Add(maxIdle); - } - } - - return this; - } - - internal void SerializeRedisArgs(List args) - { - foreach (var arg in _args) - { - args.Add(arg); - } - } - - private static void AddCommandLength(List list, string command, int length) - { - list.Add(command); - list.Add(length); - } - - private static void AddCommandArguments(List destination, string command, IReadOnlyCollection source) - { - AddCommandLength(destination, command, source.Count); - destination.AddRange(source); - } - } -} diff --git a/src/NRediSearch/Aggregation/AggregationRequest.cs b/src/NRediSearch/Aggregation/AggregationRequest.cs deleted file mode 100644 index b1cde1a22..000000000 --- a/src/NRediSearch/Aggregation/AggregationRequest.cs +++ /dev/null @@ -1,168 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System; -using System.Collections.Generic; -using NRediSearch.Aggregation.Reducers; -using StackExchange.Redis; - -namespace NRediSearch.Aggregation -{ - public class AggregationRequest - { - private readonly string _query; - private readonly List _load = new List(); - private readonly List _groups = new List(); - private readonly List _sortby = new List(); - private readonly Dictionary _projections = new Dictionary(); - - private Limit _limit = new Limit(0, 0); - private int _sortByMax = 0; - public AggregationRequest(string query) - { - _query = query; - } - public AggregationRequest() : this("*") { } - - public AggregationRequest Load(string field) - { - _load.Add(field); - return this; - } - public AggregationRequest Load(params string[] fields) - { - _load.AddRange(fields); - return this; - } - - public AggregationRequest Limit(int offset, int count) - { - var limit = new Limit(offset, count); - if (_groups.Count == 0) - { - _limit = limit; - } - else - { - _groups[_groups.Count - 1].Limit(limit); - } - return this; - } - - public AggregationRequest Limit(int count) => Limit(0, count); - - public AggregationRequest SortBy(SortedField field) - { - _sortby.Add(field); - return this; - } - public AggregationRequest SortBy(params SortedField[] fields) - { - _sortby.AddRange(fields); - return this; - } - public AggregationRequest SortBy(IList fields, int max) - { - _sortby.AddRange(fields); - _sortByMax = max; - return this; - } - public AggregationRequest SortBy(SortedField field, int max) - { - _sortby.Add(field); - _sortByMax = max; - return this; - } - - public AggregationRequest SortBy(string field, Order order) => SortBy(new SortedField(field, order)); - public AggregationRequest SortByAscending(string field) => SortBy(field, Order.Ascending); - public AggregationRequest SortByDescending(string field) => SortBy(field, Order.Descending); - - public AggregationRequest Apply(string projection, string alias) - { - _projections.Add(alias, projection); - return this; - } - - public AggregationRequest GroupBy(IList fields, IList reducers) - { - Group g = new Group(fields); - foreach (var r in reducers) - { - g.Reduce(r); - } - _groups.Add(g); - return this; - } - - public AggregationRequest GroupBy(string field, params Reducer[] reducers) - { - return GroupBy(new string[] { field }, reducers); - } - - public AggregationRequest GroupBy(Group group) - { - _groups.Add(group); - return this; - } - - private static void AddCmdLen(List list, string cmd, int len) - { - list.Add(cmd.Literal()); - list.Add(len); - } - private static void AddCmdArgs(List dst, string cmd, IList src) - { - AddCmdLen(dst, cmd, src.Count); - foreach (var obj in src) - dst.Add(obj); - } - - internal void SerializeRedisArgs(List args) - { - args.Add(_query); - - if (_load.Count != 0) - { - AddCmdArgs(args, "LOAD", _load); - } - - if (_groups.Count != 0) - { - foreach (var group in _groups) - { - args.Add("GROUPBY".Literal()); - group.SerializeRedisArgs(args); - } - } - - if (_projections.Count != 0) - { - args.Add("APPLY".Literal()); - foreach (var e in _projections) - { - args.Add(e.Value); - args.Add("AS".Literal()); - args.Add(e.Key); - } - } - - if (_sortby.Count != 0) - { - args.Add("SORTBY".Literal()); - args.Add((_sortby.Count * 2).Boxed()); - foreach (var field in _sortby) - { - args.Add(field.Field); - args.Add(field.OrderAsArg()); - } - if (_sortByMax > 0) - { - args.Add("MAX".Literal()); - args.Add(_sortByMax.Boxed()); - } - } - - _limit.SerializeRedisArgs(args); - } - } -} diff --git a/src/NRediSearch/Aggregation/Group.cs b/src/NRediSearch/Aggregation/Group.cs deleted file mode 100644 index 31e29230f..000000000 --- a/src/NRediSearch/Aggregation/Group.cs +++ /dev/null @@ -1,50 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System.Collections.Generic; -using NRediSearch.Aggregation.Reducers; - -namespace NRediSearch.Aggregation -{ - public sealed class Group - { - private readonly IList _reducers = new List(); - private readonly IList _fields; - private Limit _limit = new Limit(0, 0); - - public Group(params string[] fields) => _fields = fields; - - public Group(IList fields) => _fields = fields; - - internal Group Limit(Limit limit) - { - _limit = limit; - return this; - } - - internal Group Reduce(Reducer r) - { - _reducers.Add(r); - return this; - } - - internal void SerializeRedisArgs(List args) - { - args.Add(_fields.Count.Boxed()); - foreach (var field in _fields) - args.Add(field); - foreach (var r in _reducers) - { - args.Add("REDUCE".Literal()); - args.Add(r.Name.Literal()); - r.SerializeRedisArgs(args); - var alias = r.Alias; - if (!string.IsNullOrEmpty(alias)) - { - args.Add("AS".Literal()); - args.Add(alias); - } - } - _limit.SerializeRedisArgs(args); - } - } -} diff --git a/src/NRediSearch/Aggregation/Limit.cs b/src/NRediSearch/Aggregation/Limit.cs deleted file mode 100644 index 05649d3de..000000000 --- a/src/NRediSearch/Aggregation/Limit.cs +++ /dev/null @@ -1,25 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System.Collections.Generic; - -namespace NRediSearch.Aggregation -{ - internal readonly struct Limit - { - private readonly int _offset, _count; - - public Limit(int offset, int count) - { - _offset = offset; - _count = count; - } - - internal void SerializeRedisArgs(List args) - { - if (_count == 0) return; - args.Add("LIMIT".Literal()); - args.Add(_offset.Boxed()); - args.Add(_count.Boxed()); - } - } -} diff --git a/src/NRediSearch/Aggregation/Reducers/Reducer.cs b/src/NRediSearch/Aggregation/Reducers/Reducer.cs deleted file mode 100644 index 559604c8b..000000000 --- a/src/NRediSearch/Aggregation/Reducers/Reducer.cs +++ /dev/null @@ -1,51 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System; -using System.Collections.Generic; - -namespace NRediSearch.Aggregation.Reducers -{ - // This class is normally received via one of the subclasses or via Reducers - public abstract class Reducer - { - public override string ToString() => Name; - private readonly string _field; - - internal Reducer(string field) => _field = field; - - /// - /// The name of the reducer - /// - public abstract string Name { get; } - - public string Alias { get; set; } - - public Reducer As(string alias) - { - Alias = alias; - return this; - } - public Reducer SetAliasAsField() - { - if (string.IsNullOrEmpty(_field)) throw new InvalidOperationException("Cannot set to field name since no field exists"); - return As(_field); - } - - protected virtual int GetOwnArgsCount() => _field == null ? 0 : 1; - protected virtual void AddOwnArgs(List args) - { - if (_field != null) args.Add(_field); - } - - internal void SerializeRedisArgs(List args) - { - int count = GetOwnArgsCount(); - args.Add(count.Boxed()); - int before = args.Count; - AddOwnArgs(args); - int after = args.Count; - if (count != (after - before)) - throw new InvalidOperationException($"Reducer '{ToString()}' incorrectly reported the arg-count as {count}, but added {after - before}"); - } - } -} diff --git a/src/NRediSearch/Aggregation/Reducers/Reducers.cs b/src/NRediSearch/Aggregation/Reducers/Reducers.cs deleted file mode 100644 index 457f97dd3..000000000 --- a/src/NRediSearch/Aggregation/Reducers/Reducers.cs +++ /dev/null @@ -1,103 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System.Collections.Generic; - -namespace NRediSearch.Aggregation.Reducers -{ - public static class Reducers - { - public static Reducer Count() => CountReducer.Instance; - private sealed class CountReducer : Reducer - { - internal static readonly Reducer Instance = new CountReducer(); - private CountReducer() : base(null) { } - public override string Name => "COUNT"; - } - - private sealed class SingleFieldReducer : Reducer - { - public override string Name { get; } - - internal SingleFieldReducer(string name, string field) : base(field) - { - Name = name; - } - } - - public static Reducer CountDistinct(string field) => new SingleFieldReducer("COUNT_DISTINCT", field); - - public static Reducer CountDistinctish(string field) => new SingleFieldReducer("COUNT_DISTINCTISH", field); - - public static Reducer Sum(string field) => new SingleFieldReducer("SUM", field); - - public static Reducer Min(string field) => new SingleFieldReducer("MIN", field); - - public static Reducer Max(string field) => new SingleFieldReducer("MAX", field); - - public static Reducer Avg(string field) => new SingleFieldReducer("AVG", field); - - public static Reducer StdDev(string field) => new SingleFieldReducer("STDDEV", field); - - public static Reducer Quantile(string field, double percentile) => new QuantileReducer(field, percentile); - - private sealed class QuantileReducer : Reducer - { - private readonly double _percentile; - public QuantileReducer(string field, double percentile) : base(field) - { - _percentile = percentile; - } - protected override int GetOwnArgsCount() => base.GetOwnArgsCount() + 1; - protected override void AddOwnArgs(List args) - { - base.AddOwnArgs(args); - args.Add(_percentile); - } - public override string Name => "QUANTILE"; - } - public static Reducer FirstValue(string field, SortedField sortBy) => new FirstValueReducer(field, sortBy); - private sealed class FirstValueReducer : Reducer - { - private readonly SortedField? _sortBy; - public FirstValueReducer(string field, SortedField? sortBy) : base(field) - { - _sortBy = sortBy; - } - public override string Name => "FIRST_VALUE"; - - protected override int GetOwnArgsCount() => base.GetOwnArgsCount() + (_sortBy.HasValue ? 3 : 0); - protected override void AddOwnArgs(List args) - { - base.AddOwnArgs(args); - if (_sortBy != null) - { - var sortBy = _sortBy.GetValueOrDefault(); - args.Add("BY".Literal()); - args.Add(sortBy.Field); - args.Add(sortBy.OrderAsArg()); - } - } - } - public static Reducer FirstValue(string field) => new FirstValueReducer(field, null); - - public static Reducer ToList(string field) => new SingleFieldReducer("TOLIST", field); - - public static Reducer RandomSample(string field, int size) => new RandomSampleReducer(field, size); - - private sealed class RandomSampleReducer : Reducer - { - private readonly int _size; - public RandomSampleReducer(string field, int size) : base(field) - { - _size = size; - } - public override string Name => "RANDOM_SAMPLE"; - protected override int GetOwnArgsCount() => base.GetOwnArgsCount() + 1; - protected override void AddOwnArgs(List args) - { - base.AddOwnArgs(args); - args.Add(_size.Boxed()); - } - } - } -} diff --git a/src/NRediSearch/Aggregation/Row.cs b/src/NRediSearch/Aggregation/Row.cs deleted file mode 100644 index a2adec522..000000000 --- a/src/NRediSearch/Aggregation/Row.cs +++ /dev/null @@ -1,24 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System.Collections.Generic; -using StackExchange.Redis; - -namespace NRediSearch.Aggregation -{ - public readonly struct Row - { - private readonly Dictionary _fields; - - internal Row(Dictionary fields) - { - _fields = fields; - } - - public bool ContainsKey(string key) => _fields.ContainsKey(key); - public RedisValue this[string key] => _fields.TryGetValue(key, out var result) ? result : RedisValue.Null; - - public string GetString(string key) => _fields.TryGetValue(key, out var result) ? (string)result : default; - public long GetInt64(string key) => _fields.TryGetValue(key, out var result) ? (long)result : default; - public double GetDouble(string key) => _fields.TryGetValue(key, out var result) ? (double)result : default; - } -} diff --git a/src/NRediSearch/Aggregation/SortedField.cs b/src/NRediSearch/Aggregation/SortedField.cs deleted file mode 100644 index 84705c0f8..000000000 --- a/src/NRediSearch/Aggregation/SortedField.cs +++ /dev/null @@ -1,23 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using StackExchange.Redis; - -namespace NRediSearch.Aggregation -{ - public readonly struct SortedField - { - public SortedField(string field, Order order) - { - Field = field; - Order = order; - } - - public string Field { get; } - public Order Order { get; } - - internal object OrderAsArg() => (Order == Order.Ascending ? "ASC" : "DESC").Literal(); - - public static SortedField Ascending(string field) => new SortedField(field, Order.Ascending); - public static SortedField Descending(string field) => new SortedField(field, Order.Descending); - } -} diff --git a/src/NRediSearch/AggregationResult.cs b/src/NRediSearch/AggregationResult.cs deleted file mode 100644 index 2f757c3fc..000000000 --- a/src/NRediSearch/AggregationResult.cs +++ /dev/null @@ -1,47 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System.Collections.Generic; -using NRediSearch.Aggregation; -using StackExchange.Redis; - -namespace NRediSearch -{ - public sealed class AggregationResult - { - private readonly Dictionary[] _results; - - internal AggregationResult(RedisResult result, long cursorId = -1) - { - var arr = (RedisResult[])result; - - _results = new Dictionary[arr.Length - 1]; - for (int i = 1; i < arr.Length; i++) - { - var raw = (RedisResult[])arr[i]; - var cur = new Dictionary(); - for (int j = 0; j < raw.Length;) - { - var key = (string)raw[j++]; - var val = raw[j++]; - if (val.Type != ResultType.MultiBulk) - cur.Add(key, (RedisValue)val); - } - _results[i - 1] = cur; - } - - CursorId = cursorId; - } - public IReadOnlyList> GetResults() => _results; - - public Dictionary this[int index] - => index >= _results.Length ? null : _results[index]; - - public Row? GetRow(int index) - { - if (index >= _results.Length) return null; - return new Row(_results[index]); - } - - public long CursorId { get; } - } -} diff --git a/src/NRediSearch/AssemblyInfo.cs b/src/NRediSearch/AssemblyInfo.cs deleted file mode 100644 index c9fcce461..000000000 --- a/src/NRediSearch/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("NRediSearch.Test, PublicKey=00240000048000009400000006020000002400005253413100040000010001007791a689e9d8950b44a9a8886baad2ea180e7a8a854f158c9b98345ca5009cdd2362c84f368f1c3658c132b3c0f74e44ff16aeb2e5b353b6e0fe02f923a050470caeac2bde47a2238a9c7125ed7dab14f486a5a64558df96640933b9f2b6db188fc4a820f96dce963b662fa8864adbff38e5b4542343f162ecdc6dad16912fff")] diff --git a/src/NRediSearch/Client.cs b/src/NRediSearch/Client.cs deleted file mode 100644 index 87d0d0d39..000000000 --- a/src/NRediSearch/Client.cs +++ /dev/null @@ -1,1497 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using NRediSearch.Aggregation; -using StackExchange.Redis; -using static NRediSearch.Schema; -using static NRediSearch.SuggestionOptions; - -namespace NRediSearch -{ - public sealed class Client - { - [Flags] - public enum IndexOptions - { - /// - /// All options disabled - /// - None = 0, - /// - /// Set this to tell the index not to save term offset vectors. This reduces memory consumption but does not - /// allow performing exact matches, and reduces overall relevance of multi-term queries - /// - UseTermOffsets = 1, - /// - /// If set (default), we keep flags per index record telling us what fields the term appeared on, - /// and allowing us to filter results by field - /// - KeepFieldFlags = 2, - /// - /// The default indexing options - use term offsets, keep fields flags, keep term frequencies - /// - Default = KeepFieldFlags | KeepTermFrequencies, - /// - /// If set, we keep an index of the top entries per term, allowing extremely fast single word queries - /// regardless of index size, at the cost of more memory - /// - [Obsolete("'NOSCOREIDX' was removed from RediSearch.", true)] - UseScoreIndexes = 4, - /// - /// If set, we will disable the Stop-Words completely - /// - DisableStopWords = 8, - /// - /// If set, we keep an index of the top entries per term, allowing extremely fast single word queries - /// regardless of index size, at the cost of more memory - /// - KeepTermFrequencies = 16, - /// - /// If set, we do not scan and index. - /// - SkipInitialScan = 32, - /// - /// Disable highlighting support. If set, we do not store corresponding byte offsets for term positions. - /// Also implied by UseTermOffsets. - /// - NoHighlight = 64, - /// - /// Increases maximum number of text fields (default is 32 fields) - /// - MaxTextFields = 128, - } - - public sealed class IndexDefinition - { - public enum IndexType - { - /// - /// Used to indicate that the index should follow the keys of type Hash changes - /// - Hash, - Json - } - - internal readonly IndexType _type; - internal readonly bool _async; - internal readonly string[] _prefixes; - internal readonly string _filter; - internal readonly string _languageField; - internal readonly string _language; - internal readonly string _scoreField; - internal readonly double _score; - internal readonly string _payloadField; - - // this .ctor is left here to avoid MME in existing code (i.e. back-compat) - public IndexDefinition(bool async, string[] prefixes, - string filter, string languageField, string language, - string scoreFiled, double score, string payloadField) - : this(async, prefixes, filter, languageField, language, scoreFiled, score, payloadField, IndexType.Hash) - { } - public IndexDefinition(bool async = false, string[] prefixes = null, - string filter = null, string languageField = null, string language = null, - string scoreField = null, double score = 1.0, string payloadField = null, IndexType type = IndexType.Hash) - { - _type = type; - _async = async; - _prefixes = prefixes; - _filter = filter; - _languageField = languageField; - _language = language; - _scoreField = scoreField; - _score = score; - _payloadField = payloadField; - } - - internal void SerializeRedisArgs(List args) - { - args.Add("ON".Literal()); - args.Add(_type.ToString("g")); - if (_async) - { - args.Add("ASYNC".Literal()); - } - if (_prefixes?.Length > 0) - { - args.Add("PREFIX".Literal()); - args.Add(_prefixes.Length.ToString()); - args.AddRange(_prefixes); - } - if (_filter != null) - { - args.Add("FILTER".Literal()); - args.Add(_filter); - } - if (_languageField != null) { - args.Add("LANGUAGE_FIELD".Literal()); - args.Add(_languageField); - } - if (_language != null) { - args.Add("LANGUAGE".Literal()); - args.Add(_language); - } - if (_scoreField != null) { - args.Add("SCORE_FIELD".Literal()); - args.Add(_scoreField); - } - if (_score != 1.0) { - args.Add("SCORE".Literal()); - args.Add(_score.ToString()); - } - if (_payloadField != null) { - args.Add("PAYLOAD_FIELD".Literal()); - args.Add(_payloadField); - } - } - } - - public sealed class ConfiguredIndexOptions - { - // This news up a enum which results in the 0 equivalent. - // It's not used in the library and I'm guessing this isn't intentional. - public static IndexOptions Default => new IndexOptions(); - - private IndexOptions _options; - private readonly IndexDefinition _definition; - private string[] _stopwords; - private long _temporaryTimestamp; - - public ConfiguredIndexOptions(IndexOptions options = IndexOptions.Default) - { - _options = options; - } - - public ConfiguredIndexOptions(IndexDefinition definition, IndexOptions options = IndexOptions.Default) - : this(options) - { - _definition = definition; - } - - /// - /// Set a custom stopword list. These words will be ignored during indexing and search time. - /// - /// The new stopwords to use. - public ConfiguredIndexOptions SetStopwords(params string[] stopwords) - { - _stopwords = stopwords ?? throw new ArgumentNullException(nameof(stopwords)); - if (stopwords.Length == 0) _options |= IndexOptions.DisableStopWords; - else _options &= ~IndexOptions.DisableStopWords; - return this; - } - - /// - /// Disable the stopwords list. - /// - public ConfiguredIndexOptions SetNoStopwords() - { - _options |= IndexOptions.DisableStopWords; - - return this; - } - - /// - /// Disable highlight support. - /// - public ConfiguredIndexOptions SetNoHighlight() - { - _options |= IndexOptions.NoHighlight; - - return this; - } - - /// - /// Disable initial index scans. - /// - public ConfiguredIndexOptions SetSkipInitialScan() - { - _options |= IndexOptions.SkipInitialScan; - - return this; - } - - /// - /// Increases maximum text fields (past 32). - /// - public ConfiguredIndexOptions SetMaxTextFields() - { - _options |= IndexOptions.MaxTextFields; - - return this; - } - - /// - /// Disable term offsets (saves memory but disables exact matches). - /// - public ConfiguredIndexOptions SetUseTermOffsets() - { - _options |= IndexOptions.UseTermOffsets; - - return this; - } - - /// - /// Set a lightweight temporary index which will expire after the specified period of inactivity. - /// The internal idle timer is reset whenever the index is searched or added to. - /// - /// The time to expire in seconds. - public ConfiguredIndexOptions SetTemporaryTime(long time) - { - _temporaryTimestamp = time; - return this; - } - - internal void SerializeRedisArgs(List args) - { - if (_temporaryTimestamp != 0) - { - args.Add("TEMPORARY".Literal()); - args.Add(_temporaryTimestamp); - } - SerializeRedisArgs(_options, args, _definition); - if (_stopwords?.Length > 0) - { - args.Add("STOPWORDS".Literal()); - args.Add(_stopwords.Length.Boxed()); - args.AddRange(_stopwords); - } - } - - internal static void SerializeRedisArgs(IndexOptions options, List args, IndexDefinition definition) - { - definition?.SerializeRedisArgs(args); - if ((options & IndexOptions.MaxTextFields) == IndexOptions.MaxTextFields) - { - args.Add("MAXTEXTFIELDS".Literal()); - } - if ((options & IndexOptions.UseTermOffsets) == IndexOptions.UseTermOffsets) - { - args.Add("NOOFFSETS".Literal()); - } - if ((options & IndexOptions.NoHighlight) == IndexOptions.NoHighlight) - { - args.Add("NOHL".Literal()); - } - if ((options & IndexOptions.KeepFieldFlags) == 0) - { - args.Add("NOFIELDS".Literal()); - } - if ((options & IndexOptions.KeepTermFrequencies) == 0) - { - args.Add("NOFREQS".Literal()); - } - if ((options & IndexOptions.DisableStopWords) == IndexOptions.DisableStopWords) - { - args.Add("STOPWORDS".Literal()); - args.Add(0.Boxed()); - } - if ((options & IndexOptions.SkipInitialScan) == IndexOptions.SkipInitialScan) - { - args.Add("SKIPINITIALSCAN".Literal()); - } - } - } - - private readonly IDatabaseAsync _db; - private IDatabase DbSync - => (_db as IDatabase) ?? throw new InvalidOperationException("Synchronous operations are not available on this database instance"); - - private readonly object _boxedIndexName; - public RedisKey IndexName => (RedisKey)_boxedIndexName; - public Client(RedisKey indexName, IDatabaseAsync db) - { - _db = db ?? throw new ArgumentNullException(nameof(db)); - _boxedIndexName = indexName; // only box once, not per-command - } - - public Client(RedisKey indexName, IDatabase db) : this(indexName, (IDatabaseAsync)db) { } - - /// - /// Create the index definition in redis - /// - /// a schema definition - /// index option flags - /// true if successful - public bool CreateIndex(Schema schema, ConfiguredIndexOptions options) - { - var args = new List - { - _boxedIndexName - }; - options.SerializeRedisArgs(args); - args.Add("SCHEMA".Literal()); - - foreach (var f in schema.Fields) - { - f.SerializeRedisArgs(args); - } - - return (string)DbSync.Execute("FT.CREATE", args) == "OK"; - } - - /// - /// Create the index definition in redis - /// - /// a schema definition - /// index option flags - /// true if successful - public async Task CreateIndexAsync(Schema schema, ConfiguredIndexOptions options) - { - var args = new List - { - _boxedIndexName - }; - options.SerializeRedisArgs(args); - args.Add("SCHEMA".Literal()); - - foreach (var f in schema.Fields) - { - f.SerializeRedisArgs(args); - } - - return (string)await _db.ExecuteAsync("FT.CREATE", args).ConfigureAwait(false) == "OK"; - } - - /// - /// Alter index add fields - /// - /// list of fields - /// `true` is successful - public bool AlterIndex(params Field[] fields) - { - var args = new List - { - _boxedIndexName, - "SCHEMA".Literal(), - "ADD".Literal() - }; - - foreach (var field in fields) - { - field.SerializeRedisArgs(args); - } - - return (string)DbSync.Execute("FT.ALTER", args) == "OK"; - } - - /// - /// Alter index add fields - /// - /// list of fields - /// `true` is successful - public async Task AlterIndexAsync(params Field[] fields) - { - var args = new List - { - _boxedIndexName, - "SCHEMA".Literal(), - "ADD".Literal() - }; - - foreach (var field in fields) - { - field.SerializeRedisArgs(args); - } - - return (string)(await _db.ExecuteAsync("FT.ALTER", args).ConfigureAwait(false)) == "OK"; - } - - /// - /// Search the index - /// - /// a object with the query string and optional parameters - /// a object with the results - public SearchResult Search(Query q) - { - var args = new List - { - _boxedIndexName - }; - q.SerializeRedisArgs(args); - - var resp = (RedisResult[])DbSync.Execute("FT.SEARCH", args); - return new SearchResult(resp, !q.NoContent, q.WithScores, q.WithPayloads, q.ExplainScore); - } - - /// - /// Search the index - /// - /// a object with the query string and optional parameters - /// a object with the results - public async Task SearchAsync(Query q) - { - var args = new List - { - _boxedIndexName - }; - q.SerializeRedisArgs(args); - - var resp = (RedisResult[])await _db.ExecuteAsync("FT.SEARCH", args).ConfigureAwait(false); - return new SearchResult(resp, !q.NoContent, q.WithScores, q.WithPayloads, q.ExplainScore); - } - - /// - /// Return Distinct Values in a TAG field - /// - /// TAG field name - /// List of TAG field values - public RedisValue[] TagVals(string fieldName) => - (RedisValue[])DbSync.Execute("FT.TAGVALS", _boxedIndexName, fieldName); - - /// - /// Return Distinct Values in a TAG field - /// - /// TAG field name - /// List of TAG field values - public async Task TagValsAsync(string fieldName) => - (RedisValue[])await _db.ExecuteAsync("FT.TAGVALS", _boxedIndexName, fieldName).ConfigureAwait(false); - - /// - /// Add a single document to the query - /// - /// the id of the document. It cannot belong to a document already in the index unless replace is set - /// the document's score, floating point number between 0 and 1 - /// a map of the document's fields - /// if set, we only index the document and do not save its contents. This allows fetching just doc ids - /// if set, and the document already exists, we reindex and update it - /// if set, we can save a payload in the index to be retrieved or evaluated by scoring functions on the server - public bool AddDocument(string docId, Dictionary fields, double score = 1.0, bool noSave = false, bool replace = false, byte[] payload = null) - { - var args = BuildAddDocumentArgs(docId, fields, score, noSave, replace, payload); - return (string)DbSync.Execute("FT.ADD", args) == "OK"; - } - - /// - /// Add a single document to the query - /// - /// the id of the document. It cannot belong to a document already in the index unless replace is set - /// the document's score, floating point number between 0 and 1 - /// a map of the document's fields - /// if set, we only index the document and do not save its contents. This allows fetching just doc ids - /// if set, and the document already exists, we reindex and update it - /// if set, we can save a payload in the index to be retrieved or evaluated by scoring functions on the server - /// true if the operation succeeded, false otherwise - public async Task AddDocumentAsync(string docId, Dictionary fields, double score = 1.0, bool noSave = false, bool replace = false, byte[] payload = null) - { - var args = BuildAddDocumentArgs(docId, fields, score, noSave, replace, payload); - - try - { - return (string)await _db.ExecuteAsync("FT.ADD", args).ConfigureAwait(false) == "OK"; - } - catch (RedisServerException ex) when (ex.Message == "Document already in index") - { - return false; - } - } - - /// - /// Add a document to the index - /// - /// The document to add - /// Options for the operation - /// true if the operation succeeded, false otherwise - public bool AddDocument(Document doc, AddOptions options = null) - { - var args = BuildAddDocumentArgs(doc.Id, doc._properties, doc.Score, options?.NoSave ?? false, options?.ReplacePolicy ?? AddOptions.ReplacementPolicy.None, doc.Payload, options?.Language); - - try - { - return (string)DbSync.Execute("FT.ADD", args) == "OK"; - } - catch (RedisServerException ex) when (ex.Message == "Document already in index" || ex.Message == "Document already exists") - { - return false; - } - - } - - /// - /// Add a document to the index - /// - /// The document to add - /// Options for the operation - /// true if the operation succeeded, false otherwise. Note that if the operation fails, an exception will be thrown - public async Task AddDocumentAsync(Document doc, AddOptions options = null) - { - var args = BuildAddDocumentArgs(doc.Id, doc._properties, doc.Score, options?.NoSave ?? false, options?.ReplacePolicy ?? AddOptions.ReplacementPolicy.None, doc.Payload, options?.Language); - return (string)await _db.ExecuteAsync("FT.ADD", args).ConfigureAwait(false) == "OK"; - } - - /// - /// Add a batch of documents to the index. - /// - /// The documents to add - /// `true` on success for each document - public bool[] AddDocuments(params Document[] documents) => - AddDocuments(new AddOptions(), documents); - - /// - /// Add a batch of documents to the index - /// - /// Options for the operation - /// The documents to add - /// `true` on success for each document - public bool[] AddDocuments(AddOptions options, params Document[] documents) - { - var result = new bool[documents.Length]; - - for (var i = 0; i < documents.Length; i++) - { - result[i] = AddDocument(documents[i], options); - } - - return result; - } - - /// - /// Add a batch of documents to the index. - /// - /// The documents to add - /// `true` on success for each document - public Task AddDocumentsAsync(params Document[] documents) => - AddDocumentsAsync(new AddOptions(), documents); - - /// - /// Add a batch of documents to the index - /// - /// Options for the operation - /// The documents to add - /// `true` on success for each document - public async Task AddDocumentsAsync(AddOptions options, params Document[] documents) - { - var result = new bool[documents.Length]; - - for (var i = 0; i < documents.Length; i++) - { - result[i] = await AddDocumentAsync(documents[i], options); - } - - return result; - } - - private List BuildAddDocumentArgs(string docId, Dictionary fields, double score, bool noSave, bool replace, byte[] payload) - => BuildAddDocumentArgs(docId, fields, score, noSave, replace ? AddOptions.ReplacementPolicy.Full : AddOptions.ReplacementPolicy.None, payload, null); - private List BuildAddDocumentArgs(string docId, Dictionary fields, double score, bool noSave, AddOptions.ReplacementPolicy replacementPolicy, byte[] payload, string language) - { - var args = new List { _boxedIndexName, docId, score }; - if (noSave) - { - args.Add("NOSAVE".Literal()); - } - if (replacementPolicy != AddOptions.ReplacementPolicy.None) - { - args.Add("REPLACE".Literal()); - if (replacementPolicy == AddOptions.ReplacementPolicy.Partial) - { - args.Add("PARTIAL".Literal()); - } - } - if (!string.IsNullOrWhiteSpace(language)) - { - args.Add("LANGUAGE".Literal()); - args.Add(language); - } - - if (payload != null) - { - args.Add("PAYLOAD".Literal()); - args.Add(payload); - } - - args.Add("FIELDS".Literal()); - foreach (var ent in fields) - { - args.Add(ent.Key); - args.Add(ent.Value); - } - return args; - } - - /// - /// Convenience method for calling AddDocument with replace=true. - /// - /// The ID of the document to replce. - /// The document fields. - /// The new score. - /// The new payload. - public bool ReplaceDocument(string docId, Dictionary fields, double score = 1.0, byte[] payload = null) - => AddDocument(docId, fields, score, false, true, payload); - - /// - /// Convenience method for calling AddDocumentAsync with replace=true. - /// - /// The ID of the document to replce. - /// The document fields. - /// The new score. - /// The new payload. - public Task ReplaceDocumentAsync(string docId, Dictionary fields, double score = 1.0, byte[] payload = null) - => AddDocumentAsync(docId, fields, score, false, true, payload); - - /// - /// Index a document already in redis as a HASH key. - /// [Deprecated] Use IDatabase.HashSet instead. - /// - /// the id of the document in redis. This must match an existing, unindexed HASH key - /// the document's index score, between 0 and 1 - /// if set, and the document already exists, we reindex and update it - /// true on success - [Obsolete("Use IDatabase.HashSet instead.")] - public bool AddHash(string docId, double score, bool replace) => AddHash((RedisKey)docId, score, replace); - - /// - /// Index a document already in redis as a HASH key. - /// [Deprecated] Use IDatabase.HashSet instead. - /// - /// the id of the document in redis. This must match an existing, unindexed HASH key - /// the document's index score, between 0 and 1 - /// if set, and the document already exists, we reindex and update it - /// true on success - [Obsolete("Use IDatabase.HashSet instead.")] - public bool AddHash(RedisKey docId, double score, bool replace) - { - var args = new List { _boxedIndexName, docId, score }; - if (replace) - { - args.Add("REPLACE".Literal()); - } - return (string)DbSync.Execute("FT.ADDHASH", args) == "OK"; - } - - /// - /// Index a document already in redis as a HASH key. - /// [Deprecated] Use IDatabase.HashSet instead. - /// - /// the id of the document in redis. This must match an existing, unindexed HASH key - /// the document's index score, between 0 and 1 - /// if set, and the document already exists, we reindex and update it - /// true on success - [Obsolete("Use IDatabase.HashSet instead.")] - public Task AddHashAsync(string docId, double score, bool replace) => AddHashAsync((RedisKey)docId, score, replace); - - /// - /// Index a document already in redis as a HASH key. - /// [Deprecated] Use IDatabase.HashSet instead. - /// - /// the id of the document in redis. This must match an existing, unindexed HASH key - /// the document's index score, between 0 and 1 - /// if set, and the document already exists, we reindex and update it - /// true on success - [Obsolete("Use IDatabase.HashSet instead.")] - public async Task AddHashAsync(RedisKey docId, double score, bool replace) - { - var args = new List { _boxedIndexName, docId, score }; - if (replace) - { - args.Add("REPLACE".Literal()); - } - return (string)await _db.ExecuteAsync("FT.ADDHASH", args).ConfigureAwait(false) == "OK"; - } - - /// - /// Get the index info, including memory consumption and other statistics - /// - /// a map of key/value pairs - public Dictionary GetInfo() => - ParseGetInfo(DbSync.Execute("FT.INFO", _boxedIndexName)); - - /// - /// Get the index info, including memory consumption and other statistics - /// - /// a map of key/value pairs - public async Task> GetInfoAsync() => - ParseGetInfo(await _db.ExecuteAsync("FT.INFO", _boxedIndexName).ConfigureAwait(false)); - - private static Dictionary ParseGetInfo(RedisResult value) - { - var res = (RedisResult[])value; - var info = new Dictionary(); - for (int i = 0; i < res.Length; i += 2) - { - var val = res[i + 1]; - if (val.Type != ResultType.MultiBulk) - { - info.Add((string)res[i], (RedisValue)val); - } - } - return info; - } - - /// - /// Get the index info, including memory consumption and other statistics. - /// - /// An `InfoResult` object with parsed values from the FT.INFO command. - public InfoResult GetInfoParsed() => - new InfoResult(DbSync.Execute("FT.INFO", _boxedIndexName)); - - - - /// - /// Get the index info, including memory consumption and other statistics. - /// - /// An `InfoResult` object with parsed values from the FT.INFO command. - public async Task GetInfoParsedAsync() => - new InfoResult(await _db.ExecuteAsync("FT.INFO", _boxedIndexName).ConfigureAwait(false)); - - /// - /// Delete a document from the index. - /// - /// the document's id - /// if true also deletes the actual document if it is in the index - /// true if it has been deleted, false if it did not exist - public bool DeleteDocument(string docId, bool deleteDocument = false) - { - var args = new List - { - _boxedIndexName, - docId - }; - - if (deleteDocument) - { - args.Add("DD".Literal()); - } - - return (long)DbSync.Execute("FT.DEL", args) == 1; - } - - /// - /// Delete a document from the index. - /// - /// the document's id - /// the document's id - /// true if it has been deleted, false if it did not exist - public async Task DeleteDocumentAsync(string docId, bool deleteDocument = false) - { - var args = new List - { - _boxedIndexName, - docId - }; - - if (deleteDocument) - { - args.Add("DD".Literal()); - } - - return (long)await _db.ExecuteAsync("FT.DEL", args).ConfigureAwait(false) == 1; - } - - /// - /// Delete multiple documents from an index. - /// - /// if true also deletes the actual document ifs it is in the index - /// the document ids to delete - /// true on success for each document if it has been deleted, false if it did not exist - public bool[] DeleteDocuments(bool deleteDocuments, params string[] docIds) - { - var result = new bool[docIds.Length]; - - for (var i = 0; i < docIds.Length; i++) - { - result[i] = DeleteDocument(docIds[i], deleteDocuments); - } - - return result; - } - - /// - /// Delete multiple documents from an index. - /// - /// if true also deletes the actual document ifs it is in the index - /// the document ids to delete - /// true on success for each document if it has been deleted, false if it did not exist - public async Task DeleteDocumentsAsync(bool deleteDocuments, params string[] docIds) - { - var result = new bool[docIds.Length]; - - for (var i = 0; i < docIds.Length; i++) - { - result[i] = await DeleteDocumentAsync(docIds[i], deleteDocuments); - } - - return result; - } - - /// - /// Drop the index and all associated keys, including documents - /// - /// true on success - public bool DropIndex() - { - return (string)DbSync.Execute("FT.DROP", _boxedIndexName) == "OK"; - } - /// - /// Drop the index and all associated keys, including documents - /// - /// true on success - public async Task DropIndexAsync() - { - return (string)await _db.ExecuteAsync("FT.DROP", _boxedIndexName).ConfigureAwait(false) == "OK"; - } - - /// - /// [Deprecated] Optimize memory consumption of the index by removing extra saved capacity. This does not affect speed - /// - [Obsolete("Index optimizations are done by the internal garbage collector in the background.")] - public long OptimizeIndex() - { - return default; - } - - /// - /// [Deprecated] Optimize memory consumption of the index by removing extra saved capacity. This does not affect speed - /// - [Obsolete("Index optimizations are done by the internal garbage collector in the background.")] - public Task OptimizeIndexAsync() - { - return Task.FromResult(default(long)); - } - - /// - /// Get the size of an autoc-complete suggestion dictionary - /// - public long CountSuggestions() - => (long)DbSync.Execute("FT.SUGLEN", _boxedIndexName); - - /// - /// Get the size of an autoc-complete suggestion dictionary - /// - public async Task CountSuggestionsAsync() - => (long)await _db.ExecuteAsync("FT.SUGLEN", _boxedIndexName).ConfigureAwait(false); - - /// - /// Add a suggestion string to an auto-complete suggestion dictionary. This is disconnected from the index definitions, and leaves creating and updating suggestino dictionaries to the user. - /// - /// the Suggestion to be added - /// if set, we increment the existing entry of the suggestion by the given score, instead of replacing the score. This is useful for updating the dictionary based on user queries in real time - /// the current size of the suggestion dictionary. - public long AddSuggestion(Suggestion suggestion, bool increment = false) - { - var args = new List - { - _boxedIndexName, - suggestion.String, - suggestion.Score - }; - - if (increment) - { - args.Add("INCR".Literal()); - } - - if (suggestion.Payload != null) - { - args.Add("PAYLOAD".Literal()); - args.Add(suggestion.Payload); - } - - return (long)DbSync.Execute("FT.SUGADD", args); - } - - /// - /// Add a suggestion string to an auto-complete suggestion dictionary. This is disconnected from the index definitions, and leaves creating and updating suggestino dictionaries to the user. - /// - /// the Suggestion to be added - /// if set, we increment the existing entry of the suggestion by the given score, instead of replacing the score. This is useful for updating the dictionary based on user queries in real time - /// the current size of the suggestion dictionary. - public async Task AddSuggestionAsync(Suggestion suggestion, bool increment = false) - { - var args = new List - { - _boxedIndexName, - suggestion.String, - suggestion.Score - }; - - if (increment) - { - args.Add("INCR".Literal()); - } - - if (suggestion.Payload != null) - { - args.Add("PAYLOAD".Literal()); - args.Add(suggestion.Payload); - } - - return (long)await _db.ExecuteAsync("FT.SUGADD", args).ConfigureAwait(false); - } - - /// - /// Delete a string from a suggestion index. - /// - /// the string to delete - public bool DeleteSuggestion(string value) - => (long)DbSync.Execute("FT.SUGDEL", _boxedIndexName, value) == 1; - - /// - /// Delete a string from a suggestion index. - /// - /// the string to delete - public async Task DeleteSuggestionAsync(string value) - => (long)await _db.ExecuteAsync("FT.SUGDEL", _boxedIndexName, value).ConfigureAwait(false) == 1; - - /// - /// Get completion suggestions for a prefix - /// - /// the prefix to complete on - /// if set,we do a fuzzy prefix search, including prefixes at levenshtein distance of 1 from the prefix sent - /// If set, we limit the results to a maximum of num. (Note: The default is 5, and the number cannot be greater than 10). - /// a list of the top suggestions matching the prefix - public string[] GetSuggestions(string prefix, bool fuzzy = false, int max = 5) - { - var optionsBuilder = SuggestionOptions.Builder.Max(max); - - if (fuzzy) - { - optionsBuilder.Fuzzy(); - } - - var suggestions = GetSuggestions(prefix, optionsBuilder.Build()); - - var result = new string[suggestions.Length]; - - for (var i = 0; i < suggestions.Length; i++) - { - result[i] = suggestions[i].String; - } - - return result; - } - - /// - /// Get completion suggestions for a prefix - /// - /// the prefix to complete on - /// the options on what you need returned and other usage - /// a list of the top suggestions matching the prefix - public Suggestion[] GetSuggestions(string prefix, SuggestionOptions options) - { - var args = new List - { - _boxedIndexName, - prefix, - "MAX".Literal(), - options.Max.Boxed() - }; - - if (options.Fuzzy) - { - args.Add("FUZZY".Literal()); - } - - if (options.With != WithOptions.None) - { - args.AddRange(options.GetFlags()); - } - - var results = (RedisResult[])DbSync.Execute("FT.SUGGET", args); - - if (options.With == WithOptions.None) - { - return GetSuggestionsNoOptions(results); - } - - if (options.GetIsPayloadAndScores()) - { - return GetSuggestionsWithPayloadAndScores(results); - } - - if (options.GetIsPayload()) - { - return GetSuggestionsWithPayload(results); - } - - if (options.GetIsScores()) - { - return GetSuggestionsWithScores(results); - } - - return default; - } - - /// - /// Get completion suggestions for a prefix - /// - /// the prefix to complete on - /// if set,we do a fuzzy prefix search, including prefixes at levenshtein distance of 1 from the prefix sent - /// If set, we limit the results to a maximum of num. (Note: The default is 5, and the number cannot be greater than 10). - /// a list of the top suggestions matching the prefix - public async Task GetSuggestionsAsync(string prefix, bool fuzzy = false, int max = 5) - { - var optionsBuilder = SuggestionOptions.Builder.Max(max); - - if (fuzzy) - { - optionsBuilder.Fuzzy(); - } - - var suggestions = await GetSuggestionsAsync(prefix, optionsBuilder.Build()); - - var result = new string[suggestions.Length]; - - for(var i = 0; i < suggestions.Length; i++) - { - result[i] = suggestions[i].String; - } - - return result; - } - - - /// - /// Get completion suggestions for a prefix - /// - /// the prefix to complete on - /// the options on what you need returned and other usage - /// a list of the top suggestions matching the prefix - public async Task GetSuggestionsAsync(string prefix, SuggestionOptions options) - { - var args = new List - { - _boxedIndexName, - prefix, - "MAX".Literal(), - options.Max.Boxed() - }; - - if (options.Fuzzy) - { - args.Add("FUZZY".Literal()); - } - - if (options.With != WithOptions.None) - { - args.AddRange(options.GetFlags()); - } - - var results = (RedisResult[])await _db.ExecuteAsync("FT.SUGGET", args).ConfigureAwait(false); - - if (options.With == WithOptions.None) - { - return GetSuggestionsNoOptions(results); - } - - if (options.GetIsPayloadAndScores()) - { - return GetSuggestionsWithPayloadAndScores(results); - } - - if (options.GetIsPayload()) - { - return GetSuggestionsWithPayload(results); - } - - if (options.GetIsScores()) - { - return GetSuggestionsWithScores(results); - } - - return default; - } - - /// - /// Perform an aggregate query - /// - /// The query to watch - [Obsolete("Use `Aggregate` method that takes an `AggregationBuilder`.")] - public AggregationResult Aggregate(AggregationRequest query) - { - var args = new List - { - _boxedIndexName - }; - query.SerializeRedisArgs(args); - - var resp = DbSync.Execute("FT.AGGREGATE", args); - - return new AggregationResult(resp); - } - - /// - /// Perform an aggregate query - /// - /// The query to watch - [Obsolete("Use `AggregateAsync` method that takes an `AggregationBuilder`.")] - public async Task AggregateAsync(AggregationRequest query) - { - var args = new List - { - _boxedIndexName - }; - query.SerializeRedisArgs(args); - - var resp = await _db.ExecuteAsync("FT.AGGREGATE", args).ConfigureAwait(false); - - return new AggregationResult(resp); - } - - /// - /// Perform an aggregate query - /// - /// The query to watch - public AggregationResult Aggregate(AggregationBuilder query) - { - var args = new List - { - _boxedIndexName - }; - - query.SerializeRedisArgs(args); - - var resp = DbSync.Execute("FT.AGGREGATE", args); - - if (query.IsWithCursor) - { - var respArray = (RedisResult[])resp; - - return new AggregationResult(respArray[0], (long)respArray[1]); - } - else - { - return new AggregationResult(resp); - } - } - - /// - /// Perform an aggregate query - /// - /// The query to watch - public async Task AggregateAsync(AggregationBuilder query) - { - var args = new List - { - _boxedIndexName - }; - - query.SerializeRedisArgs(args); - - var resp = await _db.ExecuteAsync("FT.AGGREGATE", args).ConfigureAwait(false); - - if (query.IsWithCursor) - { - var respArray = (RedisResult[])resp; - - return new AggregationResult(respArray[0], (long)respArray[1]); - } - else - { - return new AggregationResult(resp); - } - } - - /// - /// Read from an existing aggregate cursor. - /// - /// The cursor's ID. - /// Limit the amount of returned results. - /// A AggregationResult object with the results - public AggregationResult CursorRead(long cursorId, int count = -1) - { - var args = new List - { - "READ", - _boxedIndexName, - cursorId - - }; - - if (count > -1) - { - args.Add("COUNT"); - args.Add(count); - } - - RedisResult[] resp = (RedisResult[])DbSync.Execute("FT.CURSOR", args); - - return new AggregationResult(resp[0], (long)resp[1]); - } - - /// - /// Read from an existing aggregate cursor. - /// - /// The cursor's ID. - /// Limit the amount of returned results. - /// A AggregationResult object with the results - public async Task CursorReadAsync(long cursorId, int count) - { - var args = new List - { - "READ", - _boxedIndexName, - cursorId - - }; - - if (count > -1) - { - args.Add("COUNT"); - args.Add(count); - } - - RedisResult[] resp = (RedisResult[])(await _db.ExecuteAsync("FT.CURSOR", args).ConfigureAwait(false)); - - return new AggregationResult(resp[0], (long)resp[1]); - } - - /// - /// Delete a cursor from the index. - /// - /// The cursor's ID. - /// `true` if it has been deleted, `false` if it did not exist. - public bool CursorDelete(long cursorId) - { - var args = new List - { - "DEL", - _boxedIndexName, - cursorId - }; - - return (string)DbSync.Execute("FT.CURSOR", args) == "OK"; - } - - /// - /// Delete a cursor from the index. - /// - /// The cursor's ID. - /// `true` if it has been deleted, `false` if it did not exist. - public async Task CursorDeleteAsync(long cursorId) - { - var args = new List - { - "DEL", - _boxedIndexName, - cursorId - }; - - return (string)(await _db.ExecuteAsync("FT.CURSOR", args).ConfigureAwait(false)) == "OK"; - } - - /// - /// Generate an explanatory textual query tree for this query string - /// - /// The query to explain - /// A string describing this query - public string Explain(Query q) - { - var args = new List - { - _boxedIndexName - }; - q.SerializeRedisArgs(args); - return (string)DbSync.Execute("FT.EXPLAIN", args); - } - - /// - /// Generate an explanatory textual query tree for this query string - /// - /// The query to explain - /// A string describing this query - public async Task ExplainAsync(Query q) - { - var args = new List - { - _boxedIndexName - }; - q.SerializeRedisArgs(args); - return (string)await _db.ExecuteAsync("FT.EXPLAIN", args).ConfigureAwait(false); - } - - /// - /// Get a document from the index. - /// - /// The document ID to retrieve. - /// The document as stored in the index. If the document does not exist, null is returned. - public Document GetDocument(string docId) - => Document.Parse(docId, DbSync.Execute("FT.GET", _boxedIndexName, docId)); - - /// - /// Get a document from the index. - /// - /// The document ID to retrieve. - /// The document as stored in the index. If the document does not exist, null is returned. - public async Task GetDocumentAsync(string docId) - => Document.Parse(docId, await _db.ExecuteAsync("FT.GET", _boxedIndexName, docId).ConfigureAwait(false)); - - /// - /// Gets a series of documents from the index. - /// - /// The document IDs to retrieve. - /// The documents stored in the index. If the document does not exist, null is returned in the list. - public Document[] GetDocuments(params string[] docIds) - { - if (docIds.Length == 0) - { - return Array.Empty(); - } - - var args = new List - { - _boxedIndexName - }; - - foreach (var docId in docIds) - { - args.Add(docId); - } - - var queryResults = (RedisResult[])DbSync.Execute("FT.MGET", args); - - var result = new Document[docIds.Length]; - - for (var i = 0; i < docIds.Length; i++) - { - var queryResult = queryResults[i]; - - if (queryResult.IsNull) - { - result[i] = null; - } - else - { - result[i] = Document.Parse(docIds[i], queryResult); - } - } - - return result; - } - - /// - /// Gets a series of documents from the index. - /// - /// The document IDs to retrieve. - /// The documents stored in the index. If the document does not exist, null is returned in the list. - public async Task GetDocumentsAsync(params string[] docIds) - { - if (docIds.Length == 0) - { - return Array.Empty(); - } - - var args = new List - { - _boxedIndexName - }; - - foreach (var docId in docIds) - { - args.Add(docId); - } - - var queryResults = (RedisResult[])await _db.ExecuteAsync("FT.MGET", args).ConfigureAwait(false); - - var result = new Document[docIds.Length]; - - for (var i = 0; i < docIds.Length; i++) - { - var queryResult = queryResults[i]; - - if (queryResult.IsNull) - { - result[i] = null; - } - else - { - result[i] = Document.Parse(docIds[i], queryResult); - } - } - - return result; - } - - /// - /// Replace specific fields in a document. Unlike #replaceDocument(), fields not present in the field list - /// are not erased, but retained. This avoids reindexing the entire document if the new values are not - /// indexed (though a reindex will happen). - /// - /// The ID of the document. - /// The fields and values to update. - /// The new score of the document. - public bool UpdateDocument(string docId, Dictionary fields, double score = 1.0) - { - var args = BuildAddDocumentArgs(docId, fields, score, false, AddOptions.ReplacementPolicy.Partial, null, null); - return (string)DbSync.Execute("FT.ADD", args) == "OK"; - } - - /// - /// Replace specific fields in a document. Unlike #replaceDocument(), fields not present in the field list - /// are not erased, but retained. This avoids reindexing the entire document if the new values are not - /// indexed (though a reindex will happen - /// - /// The ID of the document. - /// The fields and values to update. - /// The new score of the document. - public async Task UpdateDocumentAsync(string docId, Dictionary fields, double score = 1.0) - { - var args = BuildAddDocumentArgs(docId, fields, score, false, AddOptions.ReplacementPolicy.Partial, null, null); - return (string)await _db.ExecuteAsync("FT.ADD", args).ConfigureAwait(false) == "OK"; - } - - private static Suggestion[] GetSuggestionsNoOptions(RedisResult[] results) - { - var suggestions = new Suggestion[results.Length]; - - for (var i = 0; i < results.Length; i++) - { - suggestions[i] = Suggestion.Builder.String((string)results[i]).Build(true); - } - - return suggestions; - } - - private static Suggestion[] GetSuggestionsWithPayloadAndScores(RedisResult[] results) - { - var suggestions = new Suggestion[results.Length / 3]; - - for (var i = 3; i <= results.Length; i += 3) - { - var suggestion = Suggestion.Builder; - - suggestion.String((string)results[i - 3]); - suggestion.Score((double)results[i - 2]); - suggestion.Payload((string)results[i - 1]); - - suggestions[(i / 3) - 1] = suggestion.Build(true); - } - - return suggestions; - } - - private static Suggestion[] GetSuggestionsWithPayload(RedisResult[] results) - { - var suggestions = new Suggestion[results.Length / 2]; - - for (var i = 2; i <= results.Length; i += 2) - { - var suggestion = Suggestion.Builder; - - suggestion.String((string)results[i - 2]); - suggestion.Payload((string)results[i - 1]); - - suggestions[(i / 2) - 1] = suggestion.Build(true); - } - - return suggestions; - } - - private static Suggestion[] GetSuggestionsWithScores(RedisResult[] results) - { - var suggestions = new Suggestion[results.Length / 2]; - - for (var i = 2; i <= results.Length; i += 2) - { - var suggestion = Suggestion.Builder; - - suggestion.String((string)results[i - 2]); - suggestion.Score((double)results[i - 1]); - - suggestions[(i / 2) - 1] = suggestion.Build(true); - } - - return suggestions; - } - } -} diff --git a/src/NRediSearch/Document.cs b/src/NRediSearch/Document.cs deleted file mode 100644 index c60359808..000000000 --- a/src/NRediSearch/Document.cs +++ /dev/null @@ -1,89 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System.Collections.Generic; -using StackExchange.Redis; - -namespace NRediSearch -{ - /// - /// Document represents a single indexed document or entity in the engine - /// - public class Document - { - public string Id { get; } - public double Score { get; } - public byte[] Payload { get; } - public string[] ScoreExplained { get; private set; } - internal readonly Dictionary _properties; - public Document(string id, double score, byte[] payload) : this(id, null, score, payload) { } - public Document(string id) : this(id, null, 1.0, null) { } - - public Document(string id, Dictionary fields, double score = 1.0) : this(id, fields, score, null) { } - - public Document(string id, Dictionary fields, double score, byte[] payload) - { - Id = id; - _properties = fields ?? new Dictionary(); - Score = score; - Payload = payload; - } - - public IEnumerable> GetProperties() => _properties; - - public static Document Load(string id, double score, byte[] payload, RedisValue[] fields) - { - Document ret = new Document(id, score, payload); - if (fields != null) - { - for (int i = 0; i < fields.Length; i += 2) - { - string fieldName = (string)fields[i]; - if (fieldName == "$") { - ret["json"] = fields[i + 1]; - } - else { - ret[fieldName] = fields[i + 1]; - } - } - } - return ret; - } - - public static Document Load(string id, double score, byte[] payload, RedisValue[] fields, string[] scoreExplained) - { - Document ret = Document.Load(id, score, payload, fields); - if (scoreExplained != null) - { - ret.ScoreExplained = scoreExplained; - } - return ret; - } - - public RedisValue this[string key] - { - get { return _properties.TryGetValue(key, out var val) ? val : default(RedisValue); } - internal set { _properties[key] = value; } - } - - public bool HasProperty(string key) => _properties.ContainsKey(key); - - internal static Document Parse(string docId, RedisResult result) - { - if (result == null || result.IsNull) return null; - var arr = (RedisResult[])result; - var doc = new Document(docId); - - for(int i = 0; i < arr.Length; ) - { - doc[(string)arr[i++]] = (RedisValue)arr[i++]; - } - return doc; - } - - public Document Set(string field, RedisValue value) - { - this[field] = value; - return this; - } - } -} diff --git a/src/NRediSearch/Extensions.cs b/src/NRediSearch/Extensions.cs deleted file mode 100644 index 90b7ad63f..000000000 --- a/src/NRediSearch/Extensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System; -using System.Globalization; -using StackExchange.Redis; - -namespace NRediSearch -{ - public static class Extensions - { - internal static string AsRedisString(this double value, bool forceDecimal = false) - { - if (double.IsNegativeInfinity(value)) - { - return "-inf"; - } - else if (double.IsPositiveInfinity(value)) - { - return "inf"; - } - else - { - return value.ToString(forceDecimal ? "#.0" : "G17", NumberFormatInfo.InvariantInfo); - } - } - internal static string AsRedisString(this GeoUnit value) => value switch - { - GeoUnit.Feet => "ft", - GeoUnit.Kilometers => "km", - GeoUnit.Meters => "m", - GeoUnit.Miles => "mi", - _ => throw new InvalidOperationException($"Unknown unit: {value}"), - }; - } -} diff --git a/src/NRediSearch/FieldName.cs b/src/NRediSearch/FieldName.cs deleted file mode 100644 index 4d1c5bd0c..000000000 --- a/src/NRediSearch/FieldName.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Generic; - -namespace NRediSearch -{ - public class FieldName { - private readonly string name; - private string attribute; - - public FieldName(string name) : this(name, null) { - - } - - public FieldName(string name, string attribute) { - this.name = name; - this.attribute = attribute; - } - - public int AddCommandArguments(List args) { - args.Add(name); - if (attribute == null) { - return 1; - } - - args.Add("AS".Literal()); - args.Add(attribute); - return 3; - } - - public static FieldName Of(string name) { - return new FieldName(name); - } - - public FieldName As(string attribute) { - this.attribute = attribute; - return this; - } - - public static FieldName[] convert(params string[] names) { - if (names == null) return null; - FieldName[] fields = new FieldName[names.Length]; - for (int i = 0; i < names.Length; i++) - fields[i] = FieldName.Of(names[i]); - - return fields; - } - } -} diff --git a/src/NRediSearch/InfoResult.cs b/src/NRediSearch/InfoResult.cs deleted file mode 100644 index 8798a813d..000000000 --- a/src/NRediSearch/InfoResult.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System.Collections.Generic; -using StackExchange.Redis; - -namespace NRediSearch -{ - public class InfoResult - { - private readonly Dictionary _all = new Dictionary(); - - public string IndexName => GetString("index_name"); - - public Dictionary Fields => GetRedisResultsDictionary("fields"); - - public long NumDocs => GetLong("num_docs"); - - public long NumTerms => GetLong("num_terms"); - - public long NumRecords => GetLong("num_records"); - - public double InvertedSzMebibytes => GetDouble("inverted_sz_mb"); - - public double InvertedCapMebibytes => GetDouble("inverted_cap_mb"); - - public double InvertedCapOvh => GetDouble("inverted_cap_ovh"); - - public double OffsetVectorsSzMebibytes => GetDouble("offset_vectors_sz_mb"); - - public double SkipIndexSizeMebibytes => GetDouble("skip_index_size_mb"); - - public double ScoreIndexSizeMebibytes => GetDouble("score_index_size_mb"); - - public double RecordsPerDocAvg => GetDouble("records_per_doc_avg"); - - public double BytesPerRecordAvg => GetDouble("bytes_per_record_avg"); - - public double OffsetsPerTermAvg => GetDouble("offsets_per_term_avg"); - - public double OffsetBitsPerRecordAvg => GetDouble("offset_bits_per_record_avg"); - - public string MaxDocId => GetString("max_doc_id"); - - public double DocTableSizeMebibytes => GetDouble("doc_table_size_mb"); - - public double SortableValueSizeMebibytes => GetDouble("sortable_value_size_mb"); - - public double KeyTableSizeMebibytes => GetDouble("key_table_size_mb"); - - public Dictionary GcStats => GetRedisResultDictionary("gc_stats"); - - public Dictionary CursorStats => GetRedisResultDictionary("cursor_stats"); - - public InfoResult(RedisResult result) - { - var results = (RedisResult[])result; - - for (var i = 0; i < results.Length; i += 2) - { - var key = (string)results[i]; - var value = results[i + 1]; - - _all.Add(key, value); - } - } - - private string GetString(string key) => _all.TryGetValue(key, out var value) ? (string)value : default; - - private long GetLong(string key) => _all.TryGetValue(key, out var value) ? (long)value : default; - - private double GetDouble(string key) - { - if (_all.TryGetValue(key, out var value)) - { - if ((string)value == "-nan") - { - return default; - } - else - { - return (double)value; - } - } - else - { - return default; - } - } - - private Dictionary GetRedisResultDictionary(string key) - { - if (_all.TryGetValue(key, out var value)) - { - var values = (RedisResult[])value; - var result = new Dictionary(); - - for (var ii = 0; ii < values.Length; ii += 2) - { - result.Add((string)values[ii], values[ii + 1]); - } - - return result; - } - else - { - return default; - } - } - - private Dictionary GetRedisResultsDictionary(string key) - { - if (_all.TryGetValue(key, out var value)) - { - var result = new Dictionary(); - - foreach (RedisResult[] fv in (RedisResult[])value) - { - result.Add((string)fv[0], fv); - } - - return result; - } - else - { - return default; - } - } - } -} diff --git a/src/NRediSearch/Literals.cs b/src/NRediSearch/Literals.cs deleted file mode 100644 index 82e8000a3..000000000 --- a/src/NRediSearch/Literals.cs +++ /dev/null @@ -1,50 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using StackExchange.Redis; -using System.Collections; -using System.Linq; - -namespace NRediSearch -{ - /// - /// Cache to ensure we encode and box literals once only - /// - internal static class Literals - { - private static readonly Hashtable _boxed = new Hashtable(); - private static readonly object _null = RedisValue.Null; - /// - /// Obtain a lazily-cached pre-encoded and boxed representation of a string - /// - /// The value to get a literal representation for. - /// This should only be used for fixed values, not user data (the cache is never reclaimed, so it will be a memory leak) - public static object Literal(this string value) - { - if (value == null) return _null; - - object boxed = _boxed[value]; - if (boxed == null) - { - lock (_boxed) - { - boxed = _boxed[value]; - if (boxed == null) - { - boxed = (RedisValue)value; - _boxed.Add(value, boxed); - } - } - } - return boxed; - } - - private const int BOXED_MIN = -1, BOXED_MAX = 20; - private static readonly object[] s_Boxed = Enumerable.Range(BOXED_MIN, BOXED_MAX - BOXED_MIN).Select(i => (object)i).ToArray(); - - /// - /// Obtain a pre-boxed integer if possible, else box the inbound value - /// - /// The value to get a pre-boxed integer for. - public static object Boxed(this int value) => value >= BOXED_MIN && value < BOXED_MAX ? s_Boxed[value - BOXED_MIN] : value; - } -} diff --git a/src/NRediSearch/NRediSearch.csproj b/src/NRediSearch/NRediSearch.csproj deleted file mode 100644 index 553e41612..000000000 --- a/src/NRediSearch/NRediSearch.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - netstandard2.0;netcoreapp3.1;net5.0 - false - Redis;Search;Modules;RediSearch - true - - - - - \ No newline at end of file diff --git a/src/NRediSearch/Query.cs b/src/NRediSearch/Query.cs deleted file mode 100644 index 4e5e35e51..000000000 --- a/src/NRediSearch/Query.cs +++ /dev/null @@ -1,519 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System.Collections.Generic; -using System.Globalization; -using StackExchange.Redis; - -namespace NRediSearch -{ - /// - /// Query represents query parameters and filters to load results from the engine - /// - public sealed class Query - { - /// - /// Filter represents a filtering rules in a query - /// - public abstract class Filter - { - public string Property { get; } - - internal abstract void SerializeRedisArgs(List args); - - internal Filter(string property) - { - Property = property; - } - } - - /// - /// NumericFilter wraps a range filter on a numeric field. It can be inclusive or exclusive - /// - public class NumericFilter : Filter - { - private readonly double min, max; - private readonly bool exclusiveMin, exclusiveMax; - - public NumericFilter(string property, double min, bool exclusiveMin, double max, bool exclusiveMax) : base(property) - { - this.min = min; - this.max = max; - this.exclusiveMax = exclusiveMax; - this.exclusiveMin = exclusiveMin; - } - - public NumericFilter(string property, double min, double max) : this(property, min, false, max, false) { } - - internal override void SerializeRedisArgs(List args) - { - static RedisValue FormatNum(double num, bool exclude) - { - if (!exclude || double.IsInfinity(num)) - { - return (RedisValue)num; // can use directly - } - // need to add leading bracket - return "(" + num.ToString("G17", NumberFormatInfo.InvariantInfo); - } - args.Add("FILTER".Literal()); - args.Add(Property); - args.Add(FormatNum(min, exclusiveMin)); - args.Add(FormatNum(max, exclusiveMax)); - } - } - - /// - /// GeoFilter encapsulates a radius filter on a geographical indexed fields - /// - public class GeoFilter : Filter - { - private readonly double lon, lat, radius; - private readonly GeoUnit unit; - - public GeoFilter(string property, double lon, double lat, double radius, GeoUnit unit) : base(property) - { - this.lon = lon; - this.lat = lat; - this.radius = radius; - this.unit = unit; - } - - internal override void SerializeRedisArgs(List args) - { - args.Add("GEOFILTER".Literal()); - args.Add(Property); - args.Add(lon); - args.Add(lat); - args.Add(radius); - args.Add(unit.AsRedisString().Literal()); - } - } - - internal readonly struct Paging - { - public int Offset { get; } - public int Count { get; } - - public Paging(int offset, int count) - { - Offset = offset; - Count = count; - } - } - - /// - /// The query's filter list. We only support AND operation on all those filters - /// - internal readonly List _filters = new List(); - - /// - /// The textual part of the query - /// - public string QueryString { get; } - - /// - /// The sorting parameters - /// - internal Paging _paging = new Paging(0, 10); - - /// - /// Set the query to verbatim mode, disabling stemming and query expansion - /// - public bool Verbatim { get; set; } - /// - /// Set the query not to return the contents of documents, and rather just return the ids - /// - public bool NoContent { get; set; } - /// - /// Set the query not to filter for stopwords. In general this should not be used - /// - public bool NoStopwords { get; set; } - /// - /// Set the query to return a factored score for each results. This is useful to merge results from multiple queries. - /// - public bool WithScores { get; set; } - /// - /// Set the query to return object payloads, if any were given - /// - public bool WithPayloads { get; set; } - - /// - /// Set the query language, for stemming purposes; see http://redisearch.io for documentation on languages and stemming - /// - public string Language { get; set; } - - /// - /// Set the query scoring. see https://oss.redislabs.com/redisearch/Scoring.html for documentation - /// - public string Scoring { get; set; } - public bool ExplainScore { get; set; } - - internal string[] _fields = null; - internal string[] _keys = null; - internal string[] _returnFields = null; - internal FieldName[] _returnFieldsNames = null; - /// - /// Set the query payload to be evaluated by the scoring function - /// - public byte[] Payload { get; set; } - - /// - /// Set the query parameter to sort by - /// - public string SortBy { get; set; } - - /// - /// Set the query parameter to sort by ASC by default - /// - public bool SortAscending { get; set; } = true; - - // highlight and summarize - internal bool _wantsHighlight = false, _wantsSummarize = false; - internal string[] _highlightFields = null; - internal string[] _summarizeFields = null; - internal HighlightTags? _highlightTags = null; - internal string _summarizeSeparator = null; - internal int _summarizeNumFragments = -1, _summarizeFragmentLen = -1; - - /// - /// Create a new index - /// - /// The query string to use for this query. - public Query(string queryString) - { - QueryString = queryString; - } - - internal void SerializeRedisArgs(List args) - { - args.Add(QueryString); - - if (Verbatim) - { - args.Add("VERBATIM".Literal()); - } - if (NoContent) - { - args.Add("NOCONTENT".Literal()); - } - if (NoStopwords) - { - args.Add("NOSTOPWORDS".Literal()); - } - if (WithScores) - { - args.Add("WITHSCORES".Literal()); - } - if (WithPayloads) - { - args.Add("WITHPAYLOADS".Literal()); - } - if (Language != null) - { - args.Add("LANGUAGE".Literal()); - args.Add(Language); - } - if (_fields?.Length > 0) - { - args.Add("INFIELDS".Literal()); - args.Add(_fields.Length.Boxed()); - args.AddRange(_fields); - } - if (_keys?.Length > 0) - { - args.Add("INKEYS".Literal()); - args.Add(_keys.Length.Boxed()); - args.AddRange(_keys); - } - if (_returnFields?.Length > 0) - { - args.Add("RETURN".Literal()); - args.Add(_returnFields.Length.Boxed()); - args.AddRange(_returnFields); - } - else if (_returnFieldsNames?.Length > 0) - { - args.Add("RETURN".Literal()); - int returnCountIndex = args.Count; - int returnCount = 0; - foreach (FieldName fn in _returnFieldsNames) { - returnCount += fn.AddCommandArguments(args); - } - - args.Insert(returnCountIndex, returnCount); - } - - if (SortBy != null) - { - args.Add("SORTBY".Literal()); - args.Add(SortBy); - args.Add((SortAscending ? "ASC" : "DESC").Literal()); - } - - if (Scoring != null) - { - args.Add("SCORER".Literal()); - args.Add(Scoring); - - if (ExplainScore) - { - args.Add("EXPLAINSCORE".Literal()); - } - } - - if (Payload != null) - { - args.Add("PAYLOAD".Literal()); - args.Add(Payload); - } - - if (_paging.Offset != 0 || _paging.Count != 10) - { - args.Add("LIMIT".Literal()); - args.Add(_paging.Offset.Boxed()); - args.Add(_paging.Count.Boxed()); - } - - if (_filters?.Count > 0) - { - foreach (var f in _filters) - { - f.SerializeRedisArgs(args); - } - } - - if (_wantsHighlight) - { - args.Add("HIGHLIGHT".Literal()); - if (_highlightFields != null) - { - args.Add("FIELDS".Literal()); - args.Add(_highlightFields.Length.Boxed()); - foreach (var s in _highlightFields) - { - args.Add(s); - } - } - if (_highlightTags != null) - { - args.Add("TAGS".Literal()); - var tags = _highlightTags.GetValueOrDefault(); - args.Add(tags.Open); - args.Add(tags.Close); - } - } - if (_wantsSummarize) - { - args.Add("SUMMARIZE".Literal()); - if (_summarizeFields != null) - { - args.Add("FIELDS".Literal()); - args.Add(_summarizeFields.Length.Boxed()); - foreach (var s in _summarizeFields) - { - args.Add(s); - } - } - if (_summarizeNumFragments != -1) - { - args.Add("FRAGS".Literal()); - args.Add(_summarizeNumFragments.Boxed()); - } - if (_summarizeFragmentLen != -1) - { - args.Add("LEN".Literal()); - args.Add(_summarizeFragmentLen.Boxed()); - } - if (_summarizeSeparator != null) - { - args.Add("SEPARATOR".Literal()); - args.Add(_summarizeSeparator); - } - } - - if (_keys != null && _keys.Length > 0) - { - args.Add("INKEYS".Literal()); - args.Add(_keys.Length.Boxed()); - - foreach (var key in _keys) - { - args.Add(key); - } - } - } - - /// - /// Limit the results to a certain offset and limit - /// - /// the first result to show, zero based indexing - /// how many results we want to show - /// the query itself, for builder-style syntax - public Query Limit(int offset, int count) - { - _paging = new Paging(offset, count); - return this; - } - - /// - /// Add a filter to the query's filter list - /// - /// either a numeric or geo filter object - /// the query itself - public Query AddFilter(Filter f) - { - _filters.Add(f); - return this; - } - - /// - /// Limit the query to results that are limited to a specific set of fields - /// - /// a list of TEXT fields in the schemas - /// the query object itself - public Query LimitFields(params string[] fields) - { - _fields = fields; - return this; - } - - /// - /// Limit the query to results that are limited to a specific set of keys - /// - /// a list of the TEXT fields in the schemas - /// the query object itself - public Query LimitKeys(params string[] keys) - { - _keys = keys; - return this; - } - - /// - /// Result's projection - the fields to return by the query - /// - /// fields a list of TEXT fields in the schemas - /// the query object itself - public Query ReturnFields(params string[] fields) - { - _returnFields = fields; - _returnFieldsNames = null; - return this; - } - - /// - /// Result's projection - the fields to return by the query - /// - /// field a list of TEXT fields in the schemas - /// the query object itself - public Query ReturnFields(params FieldName[] fields) - { - _returnFields = null; - _returnFieldsNames = fields; - return this; - } - - public readonly struct HighlightTags - { - public HighlightTags(string open, string close) - { - Open = open; - Close = close; - } - public string Open { get; } - public string Close { get; } - } - - public Query HighlightFields(HighlightTags tags, params string[] fields) => HighlightFieldsImpl(tags, fields); - public Query HighlightFields(params string[] fields) => HighlightFieldsImpl(null, fields); - private Query HighlightFieldsImpl(HighlightTags? tags, string[] fields) - { - if (fields == null || fields.Length > 0) - { - _highlightFields = fields; - } - _highlightTags = tags; - _wantsHighlight = true; - return this; - } - - public Query SummarizeFields(int contextLen, int fragmentCount, string separator, params string[] fields) - { - if (fields == null || fields.Length > 0) - { - _summarizeFields = fields; - } - _summarizeFragmentLen = contextLen; - _summarizeNumFragments = fragmentCount; - _summarizeSeparator = separator; - _wantsSummarize = true; - return this; - } - - public Query SummarizeFields(params string[] fields) => SummarizeFields(-1, -1, null, fields); - - /// - /// Set the query to be sorted by a sortable field defined in the schema - /// - /// the sorting field's name - /// if set to true, the sorting order is ascending, else descending - /// the query object itself - public Query SetSortBy(string field, bool ascending = true) - { - SortBy = field; - SortAscending = ascending; - return this; - } - - public Query SetWithScores(bool value = true) - { - WithScores = value; - return this; - } - - public Query SetNoContent(bool value = true) - { - NoContent = value; - return this; - } - - public Query SetVerbatim(bool value = true) - { - Verbatim = value; - return this; - } - - public Query SetNoStopwords(bool value = true) - { - NoStopwords = value; - return this; - } - public Query SetLanguage(string language) - { - Language = language; - return this; - } - - /// - /// RediSearch comes with a few very basic scoring functions to evaluate document relevance. They are all based on document scores and term frequency. - /// This is regardless of the ability to use sortable fields. - /// Scoring functions are specified by adding the SCORER {scorer_name} argument to a search query. - /// If you prefer a custom scoring function, it is possible to add more functions using the Extension API. - /// These are the pre-bundled scoring functions available in RediSearch and how they work.Each function is mentioned by registered name, - /// that can be passed as a SCORER argument in FT.SEARCH - /// Pre-bundled scoring: - /// - TFIDF (default) (https://oss.redislabs.com/redisearch/Scoring.html#tfidf_default) - /// - TFIDF.DOCNORM (https://oss.redislabs.com/redisearch/Scoring.html#tfidfdocnorm) - /// - BM25 (https://oss.redislabs.com/redisearch/Scoring.html#bm25) - /// - DISMAX (https://oss.redislabs.com/redisearch/Scoring.html#dismax) - /// - DOCSCORE (https://oss.redislabs.com/redisearch/Scoring.html#docscore) - /// - HAMMING (https://oss.redislabs.com/redisearch/Scoring.html#hamming) - /// - /// - /// - public Query SetScoring(string scoring) - { - Scoring = scoring; - return this; - } - } -} diff --git a/src/NRediSearch/QueryBuilder/DisjunctNode.cs b/src/NRediSearch/QueryBuilder/DisjunctNode.cs deleted file mode 100644 index fb00b2e87..000000000 --- a/src/NRediSearch/QueryBuilder/DisjunctNode.cs +++ /dev/null @@ -1,25 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -namespace NRediSearch.QueryBuilder -{ - /// - /// A disjunct node. evaluates to true if any of its children are false. Conversely, this node evaluates to false - /// only iff all of its children are true, making it the exact inverse of IntersectNode - /// - /// DisjunctUnionNode which evalutes to true if all its children are false. - public class DisjunctNode : IntersectNode - { - public override string ToString(ParenMode mode) - { - var ret = base.ToString(ParenMode.Never); - if (ShouldUseParens(mode)) - { - return "-(" + ret + ")"; - } - else - { - return "-" + ret; - } - } - } -} diff --git a/src/NRediSearch/QueryBuilder/DisjunctUnionNode.cs b/src/NRediSearch/QueryBuilder/DisjunctUnionNode.cs deleted file mode 100644 index bc162b812..000000000 --- a/src/NRediSearch/QueryBuilder/DisjunctUnionNode.cs +++ /dev/null @@ -1,14 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -namespace NRediSearch.QueryBuilder -{ - /// - /// A disjunct union node is the inverse of a UnionNode. It evaluates to true only iff all its - /// children are false. Conversely, it evaluates to false if any of its children are true. - /// - /// see DisjunctNode which evaluates to true if any of its children are false. - public class DisjunctUnionNode : DisjunctNode - { - protected override string GetJoinString() => "|"; - } -} diff --git a/src/NRediSearch/QueryBuilder/GeoValue.cs b/src/NRediSearch/QueryBuilder/GeoValue.cs deleted file mode 100644 index 3b51f46de..000000000 --- a/src/NRediSearch/QueryBuilder/GeoValue.cs +++ /dev/null @@ -1,33 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System.Text; -using StackExchange.Redis; - -namespace NRediSearch.QueryBuilder -{ - public class GeoValue : Value - { - private readonly GeoUnit _unit; - private readonly double _lon, _lat, _radius; - - public GeoValue(double lon, double lat, double radius, GeoUnit unit) - { - _lon = lon; - _lat = lat; - _radius = radius; - _unit = unit; - } - - public override string ToString() - { - return new StringBuilder("[") - .Append(_lon.AsRedisString(true)).Append(' ') - .Append(_lat.AsRedisString(true)).Append(' ') - .Append(_radius.AsRedisString(true)).Append(' ') - .Append(_unit.AsRedisString()) - .Append(']').ToString(); - } - - public override bool IsCombinable() => false; - } -} diff --git a/src/NRediSearch/QueryBuilder/IntersectNode.cs b/src/NRediSearch/QueryBuilder/IntersectNode.cs deleted file mode 100644 index 2f9e678ce..000000000 --- a/src/NRediSearch/QueryBuilder/IntersectNode.cs +++ /dev/null @@ -1,12 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -namespace NRediSearch.QueryBuilder -{ - /// - /// The intersection node evaluates to true if any of its children are true. - /// - public class IntersectNode : QueryNode - { - protected override string GetJoinString() => " "; - } -} diff --git a/src/NRediSearch/QueryBuilder/Node.cs b/src/NRediSearch/QueryBuilder/Node.cs deleted file mode 100644 index 81a5936ce..000000000 --- a/src/NRediSearch/QueryBuilder/Node.cs +++ /dev/null @@ -1,31 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -namespace NRediSearch.QueryBuilder -{ - public enum ParenMode - { - /// - /// Always encapsulate - /// - Always, - /// - /// Never encapsulate. Note that this may be ignored if parentheses are semantically required (e.g. - ///
@foo:(val1|val2)
. However something like
@foo:v1 @bar:v2
need not be parenthesized. - ///
- Never, - /// - /// Determine encapsulation based on number of children. If the node only has one child, it is not - /// parenthesized, if it has more than one child, it is parenthesized - /// - Default, - } - public interface INode - { - /// - /// Returns the string form of this node. - /// - /// Whether the string should be encapsulated in parentheses
(...)
- /// The string query. - string ToString(ParenMode mode); - } -} diff --git a/src/NRediSearch/QueryBuilder/OptionalNode.cs b/src/NRediSearch/QueryBuilder/OptionalNode.cs deleted file mode 100644 index 4d05d4daf..000000000 --- a/src/NRediSearch/QueryBuilder/OptionalNode.cs +++ /dev/null @@ -1,25 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -namespace NRediSearch.QueryBuilder -{ - /// - /// The optional node affects scoring and ordering. If it evaluates to true, the result is ranked - /// higher. It is helpful to combine it with a UnionNode to rank a document higher if it meets - /// one of several criteria. - /// - public class OptionalNode : IntersectNode - { - public override string ToString(ParenMode mode) - { - var ret = base.ToString(ParenMode.Never); - if (ShouldUseParens(mode)) - { - return "~(" + ret + ")"; - } - else - { - return "~" + ret; - } - } - } -} diff --git a/src/NRediSearch/QueryBuilder/QueryBuilder.cs b/src/NRediSearch/QueryBuilder/QueryBuilder.cs deleted file mode 100644 index aa94f274d..000000000 --- a/src/NRediSearch/QueryBuilder/QueryBuilder.cs +++ /dev/null @@ -1,123 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -namespace NRediSearch.QueryBuilder -{ - /// - /// - /// This class contains methods to construct query nodes. These query nodes can be added to parent query - /// nodes (building a chain) or used as the root query node. - /// - /// You can use
using static
for these helper methods.
- ///
- public static class QueryBuilder - { - public static QueryNode Intersect() => new IntersectNode(); - - /// - /// Create a new intersection node with child nodes. An intersection node is true if all its children - /// are also true - /// - /// sub-condition to add - /// The node - public static QueryNode Intersect(params INode[] n) => Intersect().Add(n); - - /// - /// Create a new intersection node with a field-value pair. - /// - /// The field that should contain this value. If this value is empty, then any field will be checked. - /// Value to check for. The node will be true only if the field (or any field) contains *all* of the values. - /// The query node. - public static QueryNode Intersect(string field, params Value[] values) => Intersect().Add(field, values); - - /// - /// Helper method to create a new intersection node with a string value. - /// - /// The field to check. If left null or empty, all fields will be checked. - /// The value to check. - /// The query node. - public static QueryNode Intersect(string field, string stringValue) => Intersect(field, Values.Value(stringValue)); - - public static QueryNode Union() => new UnionNode(); - - /// - /// Create a union node. Union nodes evaluate to true if any of its children are true. - /// - /// Child node. - /// The union node. - public static QueryNode Union(params INode[] n) => Union().Add(n); - - /// - /// Create a union node which can match an one or more values. - /// - /// Field to check. If empty, all fields are checked. - /// Values to search for. The node evaluates to true if matches any of the values. - /// The union node. - public static QueryNode Union(string field, params Value[] values) => Union().Add(field, values); - - /// - /// Convenience method to match one or more strings. This is equivalent to . - /// - /// Field to match. - /// Strings to check for. - /// The union node. - public static QueryNode Union(string field, params string[] values) => Union(field, Values.Value(values)); - - public static QueryNode Disjunct() => new DisjunctNode(); - - /// - /// Create a disjunct node. Disjunct nodes are true iff any of its children are not true. - /// Conversely, this node evaluates to false if all its children are true. - /// - /// Child nodes to add. - /// The disjunct node. - public static QueryNode Disjunct(params INode[] n) => Disjunct().Add(n); - - /// - /// Create a disjunct node using one or more values. The node will evaluate to true iff the field does not - /// match any of the values. - /// - /// Field to check for (empty or null for any field). - /// The values to check for. - /// The disjunct node. - public static QueryNode Disjunct(string field, params Value[] values) => Disjunct().Add(field, values); - - /// - /// Create a disjunct node using one or more values. The node will evaluate to true iff the field does not - /// match any of the values. - /// - /// Field to check for (empty or null for any field). - /// The values to check for. - /// The disjunct node. - public static QueryNode Disjunct(string field, params string[] values) => Disjunct(field, Values.Value(values)); - - public static QueryNode DisjunctUnion() => new DisjunctUnionNode(); - - /// - /// Create a disjunct union node. This node evaluates to true if all of its children are not true. - /// Conversely, this node evaluates as false if any of its children are true. - /// - /// The nodes to union. - /// The node. - public static QueryNode DisjunctUnion(params INode[] n) => DisjunctUnion().Add(n); - - public static QueryNode DisjunctUnion(string field, params Value[] values) => DisjunctUnion().Add(field, values); - - public static QueryNode DisjunctUnion(string field, params string[] values) => DisjunctUnion(field, Values.Value(values)); - - /// - /// Creates a new . - /// - /// The new . - public static QueryNode Optional() => new OptionalNode(); - - /// - /// Create an optional node. Optional nodes do not affect which results are returned but they influence - /// ordering and scoring. - /// - /// The nodes to evaluate as optional. - /// The new node. - public static QueryNode Optional(params INode[] n) => Optional().Add(n); - - public static QueryNode Optional(string field, params Value[] values) => Optional().Add(field, values); - } -} diff --git a/src/NRediSearch/QueryBuilder/QueryNode.cs b/src/NRediSearch/QueryBuilder/QueryNode.cs deleted file mode 100644 index b75e93115..000000000 --- a/src/NRediSearch/QueryBuilder/QueryNode.cs +++ /dev/null @@ -1,99 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NRediSearch.QueryBuilder -{ - public abstract class QueryNode : INode - { - private readonly List children = new List(); - - protected abstract string GetJoinString(); - - /** - * Add a match criteria to this node - * @param field The field to check. If null or empty, then any field is checked - * @param values Values to check for. - * @return The current node, for chaining. - */ - public QueryNode Add(string field, params Value[] values) - { - children.Add(new ValueNode(field, GetJoinString(), values)); - return this; - } - - /** - * Convenience method to add a list of string values - * @param field Field to check for - * @param values One or more string values. - * @return The current node, for chaining. - */ - public QueryNode Add(string field, params string[] values) - { - children.Add(new ValueNode(field, GetJoinString(), values)); - return this; - } - - /** - * Add a list of values from a collection - * @param field The field to check - * @param values Collection of values to match - * @return The current node for chaining. - */ - public QueryNode Add(string field, IList values) - { - return Add(field, values.ToArray()); - } - - /** - * Add children nodes to this node. - * @param nodes Children nodes to add - * @return The current node, for chaining. - */ - public QueryNode Add(params INode[] nodes) - { - children.AddRange(nodes); - return this; - } - - protected bool ShouldUseParens(ParenMode mode) - { - if (mode == ParenMode.Always) - { - return true; - } - else if (mode == ParenMode.Never) - { - return false; - } - else - { - return children.Count > 1; - } - } - - public virtual string ToString(ParenMode mode) - { - StringBuilder sb = new StringBuilder(); - - if (ShouldUseParens(mode)) - { - sb.Append('('); - } - var sj = new StringJoiner(sb, GetJoinString()); - foreach (var n in children) - { - sj.Add(n.ToString(mode)); - } - if (ShouldUseParens(mode)) - { - sb.Append(')'); - } - return sb.ToString(); - } - - public override string ToString() => ToString(ParenMode.Default); - } -} diff --git a/src/NRediSearch/QueryBuilder/RangeValue.cs b/src/NRediSearch/QueryBuilder/RangeValue.cs deleted file mode 100644 index 8fa6f105d..000000000 --- a/src/NRediSearch/QueryBuilder/RangeValue.cs +++ /dev/null @@ -1,51 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System.Text; - -namespace NRediSearch.QueryBuilder -{ - public sealed class RangeValue : Value - { - private readonly double from, to; - private bool inclusiveMin = true, inclusiveMax = true; - - public override bool IsCombinable() => false; - - private static void AppendNum(StringBuilder sb, double n, bool inclusive) - { - if (!inclusive) - { - sb.Append('('); - } - sb.Append(n.AsRedisString(true)); - } - - public override string ToString() - { - StringBuilder sb = new StringBuilder(); - sb.Append('['); - AppendNum(sb, from, inclusiveMin); - sb.Append(' '); - AppendNum(sb, to, inclusiveMax); - sb.Append(']'); - return sb.ToString(); - } - - public RangeValue(double from, double to) - { - this.from = from; - this.to = to; - } - - public RangeValue InclusiveMin(bool val) - { - inclusiveMin = val; - return this; - } - public RangeValue InclusiveMax(bool val) - { - inclusiveMax = val; - return this; - } - } -} diff --git a/src/NRediSearch/QueryBuilder/StringJoiner.cs b/src/NRediSearch/QueryBuilder/StringJoiner.cs deleted file mode 100644 index c8b3cf2b4..000000000 --- a/src/NRediSearch/QueryBuilder/StringJoiner.cs +++ /dev/null @@ -1,25 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System.Text; - -namespace NRediSearch.QueryBuilder -{ - internal ref struct StringJoiner // this is to replace a Java feature cleanly - { - private readonly StringBuilder _sb; - private readonly string _delimiter; - private bool _isFirst; - public StringJoiner(StringBuilder sb, string delimiter) - { - _sb = sb; - _delimiter = delimiter; - _isFirst = true; - } - public void Add(string value) - { - if (_isFirst) _isFirst = false; - else _sb.Append(_delimiter); - _sb.Append(value); - } - } -} diff --git a/src/NRediSearch/QueryBuilder/UnionNode.cs b/src/NRediSearch/QueryBuilder/UnionNode.cs deleted file mode 100644 index 33a409b67..000000000 --- a/src/NRediSearch/QueryBuilder/UnionNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -namespace NRediSearch.QueryBuilder -{ - public class UnionNode : QueryNode - { - protected override string GetJoinString() => "|"; - } -} diff --git a/src/NRediSearch/QueryBuilder/Value.cs b/src/NRediSearch/QueryBuilder/Value.cs deleted file mode 100644 index e934804e6..000000000 --- a/src/NRediSearch/QueryBuilder/Value.cs +++ /dev/null @@ -1,9 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -namespace NRediSearch.QueryBuilder -{ - public abstract class Value - { - public virtual bool IsCombinable() => false; - } -} diff --git a/src/NRediSearch/QueryBuilder/ValueNode.cs b/src/NRediSearch/QueryBuilder/ValueNode.cs deleted file mode 100644 index 15e751d83..000000000 --- a/src/NRediSearch/QueryBuilder/ValueNode.cs +++ /dev/null @@ -1,93 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System.Text; - -namespace NRediSearch.QueryBuilder -{ - public class ValueNode : INode - { - private readonly Value[] _values; - private readonly string _field, _joinString; - - public ValueNode(string field, string joinstr, params Value[] values) - { - _field = field; - _values = values; - _joinString = joinstr; - } - - private static Value[] FromStrings(string[] values) - { - Value[] objs = new Value[values.Length]; - for (int i = 0; i < values.Length; i++) - { - objs[i] = Values.Value(values[i]); - } - return objs; - } - - public ValueNode(string field, string joinstr, params string[] values) - : this(field, joinstr, FromStrings(values)) { } - - private string FormatField() - { - if (string.IsNullOrWhiteSpace(_field)) return ""; - return "@" + _field + ":"; - } - - private string ToStringCombinable(ParenMode mode) - { - StringBuilder sb = new StringBuilder(FormatField()); - if (_values.Length > 1 || mode == ParenMode.Always) - { - sb.Append('('); - } - var sj = new StringJoiner(sb, _joinString); - foreach (var v in _values) - { - sj.Add(v.ToString()); - } - if (_values.Length > 1 || mode == ParenMode.Always) - { - sb.Append(')'); - } - return sb.ToString(); - } - - private string ToStringDefault(ParenMode mode) - { - bool useParen = mode == ParenMode.Always; - if (!useParen) - { - useParen = mode != ParenMode.Never && _values.Length > 1; - } - var sb = new StringBuilder(); - if (useParen) - { - sb.Append('('); - } - var sj = new StringJoiner(sb, _joinString); - foreach (var v in _values) - { - sj.Add(FormatField() + v); - } - if (useParen) - { - sb.Append(')'); - } - return sb.ToString(); - } - - public string ToString(ParenMode mode) - { - if (_values[0].IsCombinable()) - { - return ToStringCombinable(mode); - } - else - { - return ToStringDefault(mode); - } - } - } -} diff --git a/src/NRediSearch/QueryBuilder/Values.cs b/src/NRediSearch/QueryBuilder/Values.cs deleted file mode 100644 index caceb4db2..000000000 --- a/src/NRediSearch/QueryBuilder/Values.cs +++ /dev/null @@ -1,56 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System; - -namespace NRediSearch.QueryBuilder -{ - public static class Values - { - private abstract class ScalableValue : Value - { - public override bool IsCombinable() => true; - } - private sealed class ValueValue : ScalableValue - { - private readonly string s; - public ValueValue(string s) - { - this.s = s; - } - public override string ToString() => s; - } - public static Value Value(string s) => new ValueValue(s); - - internal static Value[] Value(string[] s) => Array.ConvertAll(s, _ => Value(_)); - - public static RangeValue Between(double from, double to) => new RangeValue(from, to); - - public static RangeValue Between(int from, int to) => new RangeValue((double)from, (double)to); - - public static RangeValue Equal(double d) => new RangeValue(d, d); - - public static RangeValue Equal(int i) => Equal((double)i); - - public static RangeValue LessThan(double d) => new RangeValue(double.NegativeInfinity, d).InclusiveMax(false); - - public static RangeValue GreaterThan(double d) => new RangeValue(d, double.PositiveInfinity).InclusiveMin(false); - public static RangeValue LessThanOrEqual(double d) => LessThan(d).InclusiveMax(true); - - public static RangeValue GreaterThanOrEqual(double d) => GreaterThan(d).InclusiveMin(true); - - public static Value Tags(params string[] tags) - { - if (tags.Length == 0) - { - throw new ArgumentException("Must have at least one tag", nameof(tags)); - } - return new TagValue("{" + string.Join(" | ", tags) + "}"); - } - private sealed class TagValue : Value - { - private readonly string s; - public TagValue(string s) { this.s = s; } - public override string ToString() => s; - } - } -} diff --git a/src/NRediSearch/Schema.cs b/src/NRediSearch/Schema.cs deleted file mode 100644 index 63d734674..000000000 --- a/src/NRediSearch/Schema.cs +++ /dev/null @@ -1,334 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using System; -using System.Collections.Generic; - -namespace NRediSearch -{ - /// - /// Schema abstracts the schema definition when creating an index. - /// Documents can contain fields not mentioned in the schema, but the index will only index pre-defined fields - /// - public sealed class Schema - { - public enum FieldType - { - FullText, - Geo, - Numeric, - Tag - } - - public class Field - { - public FieldName FieldName { get; } - public string Name { get; } - public FieldType Type { get; } - public bool Sortable { get; } - public bool NoIndex { get; } - public bool Unf { get; } - - internal Field(string name, FieldType type, bool sortable, bool noIndex = false, bool unf = false) - : this(FieldName.Of(name), type, sortable, noIndex, unf) - { - Name = name; - } - - internal Field(FieldName name, FieldType type, bool sortable, bool noIndex = false, bool unf = false) - { - FieldName = name; - Type = type; - Sortable = sortable; - NoIndex = noIndex; - if (unf && !sortable){ - throw new ArgumentException("UNF can't be applied on a non-sortable field."); - } - Unf = unf; - } - - internal virtual void SerializeRedisArgs(List args) - { - static object GetForRedis(FieldType type) => type switch - { - FieldType.FullText => "TEXT".Literal(), - FieldType.Geo => "GEO".Literal(), - FieldType.Numeric => "NUMERIC".Literal(), - FieldType.Tag => "TAG".Literal(), - _ => throw new ArgumentOutOfRangeException(nameof(type)), - }; - FieldName.AddCommandArguments(args); - args.Add(GetForRedis(Type)); - if (Sortable) { args.Add("SORTABLE".Literal()); } - if (Unf) args.Add("UNF".Literal()); - if (NoIndex) { args.Add("NOINDEX".Literal()); } - } - } - - public class TextField : Field - { - public double Weight { get; } - public bool NoStem { get; } - - public TextField(string name, double weight, bool sortable, bool noStem, bool noIndex) - : this(name, weight, sortable, noStem, noIndex, false) { } - - public TextField(string name, double weight = 1.0, bool sortable = false, bool noStem = false, bool noIndex = false, bool unNormalizedForm = false) - : base(name, FieldType.FullText, sortable, noIndex, unNormalizedForm) - { - Weight = weight; - NoStem = noStem; - } - - public TextField(FieldName name, double weight, bool sortable, bool noStem, bool noIndex) - : this(name, weight, sortable, noStem, noIndex, false) { } - - public TextField(FieldName name, double weight = 1.0, bool sortable = false, bool noStem = false, bool noIndex = false, bool unNormalizedForm = false) - : base(name, FieldType.FullText, sortable, noIndex, unNormalizedForm) - { - Weight = weight; - NoStem = noStem; - } - - internal override void SerializeRedisArgs(List args) - { - base.SerializeRedisArgs(args); - if (Weight != 1.0) - { - args.Add("WEIGHT".Literal()); - args.Add(Weight); - } - if (NoStem) args.Add("NOSTEM".Literal()); - } - } - - public List Fields { get; } = new List(); - - /// - /// Add a field to the schema. - /// - /// The to add. - /// The object. - public Schema AddField(Field field) - { - Fields.Add(field ?? throw new ArgumentNullException(nameof(field))); - return this; - } - - /// - /// Add a text field to the schema with a given weight. - /// - /// The field's name. - /// Its weight, a positive floating point number. - /// The object. - public Schema AddTextField(string name, double weight = 1.0) - { - Fields.Add(new TextField(name, weight)); - return this; - } - - /// - /// Add a text field to the schema with a given weight. - /// - /// The field's name. - /// Its weight, a positive floating point number. - /// The object. - public Schema AddTextField(FieldName name, double weight = 1.0) - { - Fields.Add(new TextField(name, weight)); - return this; - } - - /// - /// Add a text field that can be sorted on. - /// - /// The field's name. - /// Its weight, a positive floating point number. - /// Set this to true to prevent the indexer from sorting on the normalized form. - /// Normalied form is the field sent to lower case with all diaretics removed - /// The object. - public Schema AddSortableTextField(string name, double weight = 1.0, bool unf = false) - { - Fields.Add(new TextField(name, weight, true, unNormalizedForm: unf)); - return this; - } - - /// - /// Add a text field that can be sorted on. - /// - /// The field's name. - /// Its weight, a positive floating point number. - /// The object. - public Schema AddSortableTextField(string name, double weight) => AddSortableTextField(name, weight, false); - - /// - /// Add a text field that can be sorted on. - /// - /// The field's name. - /// Its weight, a positive floating point number. - /// Set this to true to prevent the indexer from sorting on the normalized form. - /// Normalied form is the field sent to lower case with all diaretics removed - /// The object. - public Schema AddSortableTextField(FieldName name, double weight = 1.0, bool unNormalizedForm = false) - { - Fields.Add(new TextField(name, weight, true, unNormalizedForm: unNormalizedForm)); - return this; - } - - /// - /// Add a text field that can be sorted on. - /// - /// The field's name. - /// Its weight, a positive floating point number. - /// The object. - public Schema AddSortableTextField(FieldName name, double weight) => AddSortableTextField(name, weight, false); - - /// - /// Add a numeric field to the schema. - /// - /// The field's name. - /// The object. - public Schema AddGeoField(string name) - { - Fields.Add(new Field(name, FieldType.Geo, false)); - return this; - } - - /// - /// Add a numeric field to the schema. - /// - /// The field's name. - /// The object. - public Schema AddGeoField(FieldName name) - { - Fields.Add(new Field(name, FieldType.Geo, false)); - return this; - } - - /// - /// Add a numeric field to the schema. - /// - /// The field's name. - /// The object. - public Schema AddNumericField(string name) - { - Fields.Add(new Field(name, FieldType.Numeric, false)); - return this; - } - - /// - /// Add a numeric field to the schema. - /// - /// The field's name. - /// The object. - public Schema AddNumericField(FieldName name) - { - Fields.Add(new Field(name, FieldType.Numeric, false)); - return this; - } - - /// - /// Add a numeric field that can be sorted on. - /// - /// The field's name. - /// The object. - public Schema AddSortableNumericField(string name) - { - Fields.Add(new Field(name, FieldType.Numeric, true)); - return this; - } - - /// - /// Add a numeric field that can be sorted on. - /// - /// The field's name. - /// The object. - public Schema AddSortableNumericField(FieldName name) - { - Fields.Add(new Field(name, FieldType.Numeric, true)); - return this; - } - - public class TagField : Field - { - public string Separator { get; } - - internal TagField(string name, string separator = ",", bool sortable = false, bool unNormalizedForm = false) - : base(name, FieldType.Tag, sortable, unf: unNormalizedForm) - { - Separator = separator; - } - - internal TagField(FieldName name, string separator = ",", bool sortable = false, bool unNormalizedForm = false) - : base(name, FieldType.Tag, sortable, unf: unNormalizedForm) - { - Separator = separator; - } - - internal override void SerializeRedisArgs(List args) - { - base.SerializeRedisArgs(args); - if (Separator != ",") - { - if (Sortable) args.Remove("SORTABLE"); - if (Unf) args.Remove("UNF"); - args.Add("SEPARATOR".Literal()); - args.Add(Separator); - if (Sortable) args.Add("SORTABLE".Literal()); - if (Unf) args.Add("UNF".Literal()); - } - } - } - - /// - /// Add a TAG field. - /// - /// The field's name. - /// The tag separator. - /// The object. - public Schema AddTagField(string name, string separator = ",") - { - Fields.Add(new TagField(name, separator)); - return this; - } - - /// - /// Add a TAG field. - /// - /// The field's name. - /// The tag separator. - /// The object. - public Schema AddTagField(FieldName name, string separator = ",") - { - Fields.Add(new TagField(name, separator)); - return this; - } - - /// - /// Add a sortable TAG field. - /// - /// The field's name. - /// The tag separator. - /// Set this to true to prevent the indexer from sorting on the normalized form. - /// Normalied form is the field sent to lower case with all diaretics removed - /// The object. - public Schema AddSortableTagField(string name, string separator = ",", bool unNormalizedForm = false) - { - Fields.Add(new TagField(name, separator, sortable: true, unNormalizedForm: unNormalizedForm)); - return this; - } - - /// - /// Add a sortable TAG field. - /// - /// The field's name. - /// The tag separator. - /// Set this to true to prevent the indexer from sorting on the normalized form. - /// Normalied form is the field sent to lower case with all diaretics removed - /// The object. - public Schema AddSortableTagField(FieldName name, string separator = ",", bool unNormalizedForm = false) - { - Fields.Add(new TagField(name, separator, sortable: true, unNormalizedForm: unNormalizedForm)); - return this; - } - } -} diff --git a/src/NRediSearch/SearchResult.cs b/src/NRediSearch/SearchResult.cs deleted file mode 100644 index a55defdd1..000000000 --- a/src/NRediSearch/SearchResult.cs +++ /dev/null @@ -1,100 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ - -using StackExchange.Redis; -using System.Collections.Generic; -using System.Linq; - -namespace NRediSearch -{ - /// - /// SearchResult encapsulates the returned result from a search query. - /// It contains publically accessible fields for the total number of results, and an array of - /// objects conatining the actual returned documents. - /// - public class SearchResult - { - public long TotalResults { get; } - public List Documents { get; } - - internal SearchResult(RedisResult[] resp, bool hasContent, bool hasScores, bool hasPayloads, bool shouldExplainScore) - { - // Calculate the step distance to walk over the results. - // The order of results is id, score (if withScore), payLoad (if hasPayloads), fields - int step = 1; - int scoreOffset = 0; - int contentOffset = 1; - int payloadOffset = 0; - if (hasScores) - { - step++; - scoreOffset = 1; - contentOffset++; - - } - if (hasContent) - { - step++; - if (hasPayloads) - { - payloadOffset = scoreOffset + 1; - step++; - contentOffset++; - } - } - - // the first element is always the number of results - TotalResults = (long)resp[0]; - var docs = new List((resp.Length - 1) / step); - Documents = docs; - for (int i = 1; i < resp.Length; i += step) - { - var id = (string)resp[i]; - double score = 1.0; - byte[] payload = null; - RedisValue[] fields = null; - string[] scoreExplained = null; - if (hasScores) - { - if (shouldExplainScore) - { - var scoreResult = (RedisResult[])resp[i + scoreOffset]; - score = (double) scoreResult[0]; - var redisResultsScoreExplained = (RedisResult[]) scoreResult[1]; - scoreExplained = FlatRedisResultArray(redisResultsScoreExplained).ToArray(); - } - else - { - score = (double)resp[i + scoreOffset]; - } - } - if (hasPayloads) - { - payload = (byte[])resp[i + payloadOffset]; - } - - if (hasContent) - { - fields = (RedisValue[])resp[i + contentOffset]; - } - - docs.Add(Document.Load(id, score, payload, fields, scoreExplained)); - } - } - - static IEnumerable FlatRedisResultArray(RedisResult[] collection) - { - foreach (var o in collection) - { - if (o.Type == ResultType.MultiBulk) - { - foreach (string t in FlatRedisResultArray((RedisResult[])o)) - yield return t; - } - else - { - yield return o.ToString(); - } - } - } - } -} diff --git a/src/NRediSearch/Suggestion.cs b/src/NRediSearch/Suggestion.cs deleted file mode 100644 index e92d4d56b..000000000 --- a/src/NRediSearch/Suggestion.cs +++ /dev/null @@ -1,109 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ -using System; -using StackExchange.Redis; - -namespace NRediSearch -{ - public sealed class Suggestion - { - public string String { get; } - public double Score { get; } - public string Payload { get; } - - private Suggestion(SuggestionBuilder builder) - { - String = builder._string; - Score = builder._score; - Payload = builder._payload; - } - - public override bool Equals(object obj) - { - if (this == obj) - { - return true; - } - - if(!(obj is Suggestion that)) - { - return false; - } - - return Score == that.Score && String == that.String && Payload == that.Payload; - } - - public override int GetHashCode() - { - unchecked - { - int hash = 17; - - hash = hash * 31 + String.GetHashCode(); - hash = hash * 31 + Score.GetHashCode(); - hash = hash * 31 + Payload.GetHashCode(); - - return hash; - } - } - - public override string ToString() => - $"Suggestion{{string='{String}', score={Score}, payload='{Payload}'}}"; - - public SuggestionBuilder ToBuilder() => new SuggestionBuilder(this); - - public static SuggestionBuilder Builder => new SuggestionBuilder(); - - public sealed class SuggestionBuilder - { - internal string _string; - internal double _score = 1.0; - internal string _payload; - - public SuggestionBuilder() { } - - public SuggestionBuilder(Suggestion suggestion) - { - _string = suggestion.String; - _score = suggestion.Score; - _payload = suggestion.Payload; - } - - public SuggestionBuilder String(string @string) - { - _string = @string; - - return this; - } - - public SuggestionBuilder Score(double score) - { - _score = score; - - return this; - } - - public SuggestionBuilder Payload(string payload) - { - _payload = payload; - - return this; - } - - public Suggestion Build() => Build(false); - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0071:Simplify interpolation", Justification = "Allocations (string.Concat vs. string.Format)")] - internal Suggestion Build(bool fromServer) - { - bool isStringMissing = _string == null; - bool isScoreOutOfRange = !fromServer && (_score < 0.0 || _score > 1.0); - - if (isStringMissing || isScoreOutOfRange) - { - throw new RedisCommandException($"Missing required fields: {(isStringMissing ? "string" : string.Empty)} {(isScoreOutOfRange ? "score not within range" : string.Empty)}: {_score.ToString()}"); - } - - return new Suggestion(this); - } - } - } -} diff --git a/src/NRediSearch/SuggestionOptions.cs b/src/NRediSearch/SuggestionOptions.cs deleted file mode 100644 index d7c30365a..000000000 --- a/src/NRediSearch/SuggestionOptions.cs +++ /dev/null @@ -1,107 +0,0 @@ -// .NET port of https://github.com/RedisLabs/JRediSearch/ -using System; - -namespace NRediSearch -{ - public class SuggestionOptions - { - private readonly object WITHPAYLOADS_FLAG = "WITHPAYLOADS".Literal(); - private readonly object WITHSCORES_FLAG = "WITHSCORES".Literal(); - - public SuggestionOptions(SuggestionOptionsBuilder builder) - { - With = builder._with; - Fuzzy = builder._fuzzy; - Max = builder._max; - } - - public static SuggestionOptionsBuilder Builder => new SuggestionOptionsBuilder(); - - public WithOptions With { get; } - - public bool Fuzzy { get; } - - public int Max { get; } = 5; - - public object[] GetFlags() - { - if (HasOption(WithOptions.PayloadsAndScores)) - { - return new[] { WITHPAYLOADS_FLAG, WITHSCORES_FLAG }; - } - - if (HasOption(WithOptions.Payloads)) - { - return new[] { WITHPAYLOADS_FLAG }; - } - - if (HasOption(WithOptions.Scores)) - { - return new[] { WITHSCORES_FLAG }; - } - - return default; - } - - public SuggestionOptionsBuilder ToBuilder() => new SuggestionOptionsBuilder(this); - - internal bool GetIsPayloadAndScores() => HasOption(WithOptions.PayloadsAndScores); - - internal bool GetIsPayload() => HasOption(WithOptions.Payloads); - - internal bool GetIsScores() => HasOption(WithOptions.Scores); - - [Flags] - public enum WithOptions - { - None = 0, - Payloads = 1, - Scores = 2, - PayloadsAndScores = Payloads | Scores - } - - internal bool HasOption(WithOptions option) => (With & option) != 0; - - public sealed class SuggestionOptionsBuilder - { - internal WithOptions _with; - internal bool _fuzzy; - internal int _max = 5; - - public SuggestionOptionsBuilder() { } - - public SuggestionOptionsBuilder(SuggestionOptions options) - { - _with = options.With; - _fuzzy = options.Fuzzy; - _max = options.Max; - } - - public SuggestionOptionsBuilder Fuzzy() - { - _fuzzy = true; - - return this; - } - - public SuggestionOptionsBuilder Max(int max) - { - _max = max; - - return this; - } - - public SuggestionOptionsBuilder With(WithOptions with) - { - _with = with; - - return this; - } - - public SuggestionOptions Build() - { - return new SuggestionOptions(this); - } - } - } -} diff --git a/src/StackExchange.Redis/AssemblyInfoHack.cs b/src/StackExchange.Redis/AssemblyInfoHack.cs index 2936ed56c..ec7037b0f 100644 --- a/src/StackExchange.Redis/AssemblyInfoHack.cs +++ b/src/StackExchange.Redis/AssemblyInfoHack.cs @@ -5,6 +5,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("StackExchange.Redis.Server, PublicKey=00240000048000009400000006020000002400005253413100040000010001007791a689e9d8950b44a9a8886baad2ea180e7a8a854f158c9b98345ca5009cdd2362c84f368f1c3658c132b3c0f74e44ff16aeb2e5b353b6e0fe02f923a050470caeac2bde47a2238a9c7125ed7dab14f486a5a64558df96640933b9f2b6db188fc4a820f96dce963b662fa8864adbff38e5b4542343f162ecdc6dad16912fff")] [assembly: InternalsVisibleTo("StackExchange.Redis.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001007791a689e9d8950b44a9a8886baad2ea180e7a8a854f158c9b98345ca5009cdd2362c84f368f1c3658c132b3c0f74e44ff16aeb2e5b353b6e0fe02f923a050470caeac2bde47a2238a9c7125ed7dab14f486a5a64558df96640933b9f2b6db188fc4a820f96dce963b662fa8864adbff38e5b4542343f162ecdc6dad16912fff")] -[assembly: InternalsVisibleTo("NRediSearch.Test, PublicKey=00240000048000009400000006020000002400005253413100040000010001007791a689e9d8950b44a9a8886baad2ea180e7a8a854f158c9b98345ca5009cdd2362c84f368f1c3658c132b3c0f74e44ff16aeb2e5b353b6e0fe02f923a050470caeac2bde47a2238a9c7125ed7dab14f486a5a64558df96640933b9f2b6db188fc4a820f96dce963b662fa8864adbff38e5b4542343f162ecdc6dad16912fff")] [assembly: CLSCompliant(true)] diff --git a/tests/NRediSearch.Test/AssemblyInfo.cs b/tests/NRediSearch.Test/AssemblyInfo.cs deleted file mode 100644 index 6a1cf5931..000000000 --- a/tests/NRediSearch.Test/AssemblyInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using Xunit; - -[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)] - -namespace NRediSearch.Test -{ - - public class AssemblyInfo - { - public AssemblyInfo() - { - } - } -} diff --git a/tests/NRediSearch.Test/Attributes.cs b/tests/NRediSearch.Test/Attributes.cs deleted file mode 100644 index 4354b5ba8..000000000 --- a/tests/NRediSearch.Test/Attributes.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NRediSearch.Test -{ - // This is only to make the namespace more-local and not need a using at the top of every test file that's easy to forget - public class FactAttribute : StackExchange.Redis.Tests.FactAttribute { } - - public class TheoryAttribute : StackExchange.Redis.Tests.TheoryAttribute { } -} diff --git a/tests/NRediSearch.Test/ClientTests/AggregationBuilderTests.cs b/tests/NRediSearch.Test/ClientTests/AggregationBuilderTests.cs deleted file mode 100644 index 8d2419a35..000000000 --- a/tests/NRediSearch.Test/ClientTests/AggregationBuilderTests.cs +++ /dev/null @@ -1,186 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using NRediSearch.Aggregation; -using NRediSearch.Aggregation.Reducers; -using StackExchange.Redis; -using Xunit; -using Xunit.Abstractions; -using static NRediSearch.Client; - -namespace NRediSearch.Test.ClientTests -{ - public class AggregationBuilderTests : RediSearchTestBase - { - public AggregationBuilderTests(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public void TestAggregations() - { - /* - 127.0.0.1:6379> FT.CREATE test_index SCHEMA name TEXT SORTABLE count NUMERIC SORTABLE - OK - 127.0.0.1:6379> FT.ADD test_index data1 1.0 FIELDS name abc count 10 - OK - 127.0.0.1:6379> FT.ADD test_index data2 1.0 FIELDS name def count 5 - OK - 127.0.0.1:6379> FT.ADD test_index data3 1.0 FIELDS name def count 25 - */ - - Client cl = GetClient(); - Schema sc = new Schema(); - - sc.AddSortableTextField("name", 1.0); - sc.AddSortableNumericField("count"); - cl.CreateIndex(sc, new ConfiguredIndexOptions()); - cl.AddDocument(new Document("data1").Set("name", "abc").Set("count", 10)); - cl.AddDocument(new Document("data2").Set("name", "def").Set("count", 5)); - cl.AddDocument(new Document("data3").Set("name", "def").Set("count", 25)); - - AggregationBuilder r = new AggregationBuilder() - .GroupBy("@name", Reducers.Sum("@count").As("sum")) - .SortBy(10, SortedField.Descending("@sum")); - - // actual search - AggregationResult res = cl.Aggregate(r); - Row? r1 = res.GetRow(0); - Assert.NotNull(r1); - Assert.Equal("def", r1.Value.GetString("name")); - Assert.Equal(30, r1.Value.GetInt64("sum")); - Assert.Equal(30.0, r1.Value.GetDouble("sum")); - - Assert.Equal(0L, r1.Value.GetInt64("nosuchcol")); - Assert.Equal(0.0, r1.Value.GetDouble("nosuchcol")); - Assert.Null(r1.Value.GetString("nosuchcol")); - - Row? r2 = res.GetRow(1); - - Assert.NotNull(r2); - Assert.Equal("abc", r2.Value.GetString("name")); - Assert.Equal(10L, r2.Value.GetInt64("sum")); - } - - [Fact] - public void TestApplyAndFilterAggregations() - { - /* - 127.0.0.1:6379> FT.CREATE test_index SCHEMA name TEXT SORTABLE subj1 NUMERIC SORTABLE subj2 NUMERIC SORTABLE - OK - 127.0.0.1:6379> FT.ADD test_index data1 1.0 FIELDS name abc subj1 20 subj2 70 - OK - 127.0.0.1:6379> FT.ADD test_index data2 1.0 FIELDS name def subj1 60 subj2 40 - OK - 127.0.0.1:6379> FT.ADD test_index data3 1.0 FIELDS name ghi subj1 50 subj2 80 - OK - 127.0.0.1:6379> FT.ADD test_index data1 1.0 FIELDS name abc subj1 30 subj2 20 - OK - 127.0.0.1:6379> FT.ADD test_index data2 1.0 FIELDS name def subj1 65 subj2 45 - OK - 127.0.0.1:6379> FT.ADD test_index data3 1.0 FIELDS name ghi subj1 70 subj2 70 - OK - */ - - Client cl = GetClient(); - Schema sc = new Schema(); - - sc.AddSortableTextField("name", 1.0); - sc.AddSortableNumericField("subj1"); - sc.AddSortableNumericField("subj2"); - cl.CreateIndex(sc, new ConfiguredIndexOptions()); - cl.AddDocument(new Document("data1").Set("name", "abc").Set("subj1", 20).Set("subj2", 70)); - cl.AddDocument(new Document("data2").Set("name", "def").Set("subj1", 60).Set("subj2", 40)); - cl.AddDocument(new Document("data3").Set("name", "ghi").Set("subj1", 50).Set("subj2", 80)); - cl.AddDocument(new Document("data4").Set("name", "abc").Set("subj1", 30).Set("subj2", 20)); - cl.AddDocument(new Document("data5").Set("name", "def").Set("subj1", 65).Set("subj2", 45)); - cl.AddDocument(new Document("data6").Set("name", "ghi").Set("subj1", 70).Set("subj2", 70)); - - AggregationBuilder r = new AggregationBuilder().Apply("(@subj1+@subj2)/2", "attemptavg") - .GroupBy("@name", Reducers.Avg("@attemptavg").As("avgscore")) - .Filter("@avgscore>=50") - .SortBy(10, SortedField.Ascending("@name")); - - // actual search - AggregationResult res = cl.Aggregate(r); - Row? r1 = res.GetRow(0); - Assert.NotNull(r1); - Assert.Equal("def", r1.Value.GetString("name")); - Assert.Equal(52.5, r1.Value.GetDouble("avgscore")); - - Row? r2 = res.GetRow(1); - Assert.NotNull(r2); - Assert.Equal("ghi", r2.Value.GetString("name")); - Assert.Equal(67.5, r2.Value.GetDouble("avgscore")); - } - - [Fact] - public async Task TestCursor() - { - /* - 127.0.0.1:6379> FT.CREATE test_index SCHEMA name TEXT SORTABLE count NUMERIC SORTABLE - OK - 127.0.0.1:6379> FT.ADD test_index data1 1.0 FIELDS name abc count 10 - OK - 127.0.0.1:6379> FT.ADD test_index data2 1.0 FIELDS name def count 5 - OK - 127.0.0.1:6379> FT.ADD test_index data3 1.0 FIELDS name def count 25 - */ - - Client cl = GetClient(); - Schema sc = new Schema(); - sc.AddSortableTextField("name", 1.0); - sc.AddSortableNumericField("count"); - cl.CreateIndex(sc, new ConfiguredIndexOptions()); - cl.AddDocument(new Document("data1").Set("name", "abc").Set("count", 10)); - cl.AddDocument(new Document("data2").Set("name", "def").Set("count", 5)); - cl.AddDocument(new Document("data3").Set("name", "def").Set("count", 25)); - - AggregationBuilder r = new AggregationBuilder() - .GroupBy("@name", Reducers.Sum("@count").As("sum")) - .SortBy(10, SortedField.Descending("@sum")) - .Cursor(1, 3000); - - // actual search - AggregationResult res = cl.Aggregate(r); - Row? row = res.GetRow(0); - Assert.NotNull(row); - Assert.Equal("def", row.Value.GetString("name")); - Assert.Equal(30, row.Value.GetInt64("sum")); - Assert.Equal(30.0, row.Value.GetDouble("sum")); - - Assert.Equal(0L, row.Value.GetInt64("nosuchcol")); - Assert.Equal(0.0, row.Value.GetDouble("nosuchcol")); - Assert.Null(row.Value.GetString("nosuchcol")); - - res = cl.CursorRead(res.CursorId, 1); - Row? row2 = res.GetRow(0); - - Assert.NotNull(row2); - Assert.Equal("abc", row2.Value.GetString("name")); - Assert.Equal(10, row2.Value.GetInt64("sum")); - - Assert.True(cl.CursorDelete(res.CursorId)); - - try - { - cl.CursorRead(res.CursorId, 1); - Assert.True(false); - } - catch (RedisException) { } - - _ = new AggregationBuilder() - .GroupBy("@name", Reducers.Sum("@count").As("sum")) - .SortBy(10, SortedField.Descending("@sum")) - .Cursor(1, 1000); - - await Task.Delay(1000).ForAwait(); - - try - { - cl.CursorRead(res.CursorId, 1); - Assert.True(false); - } - catch (RedisException) { } - } - } -} diff --git a/tests/NRediSearch.Test/ClientTests/AggregationTest.cs b/tests/NRediSearch.Test/ClientTests/AggregationTest.cs deleted file mode 100644 index 84c629c19..000000000 --- a/tests/NRediSearch.Test/ClientTests/AggregationTest.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using NRediSearch.Aggregation; -using NRediSearch.Aggregation.Reducers; -using Xunit; -using Xunit.Abstractions; -using static NRediSearch.Client; - -namespace NRediSearch.Test.ClientTests -{ - public class AggregationTest : RediSearchTestBase - { - public AggregationTest(ITestOutputHelper output) : base(output) { } - - [Fact] - [Obsolete] - public void TestAggregations() - { - /* - 127.0.0.1:6379> FT.CREATE test_index SCHEMA name TEXT SORTABLE count NUMERIC SORTABLE - OK - 127.0.0.1:6379> FT.ADD test_index data1 1.0 FIELDS name abc count 10 - OK - 127.0.0.1:6379> FT.ADD test_index data2 1.0 FIELDS name def count 5 - OK - 127.0.0.1:6379> FT.ADD test_index data3 1.0 FIELDS name def count 25 - */ - - Client cl = GetClient(); - Schema sc = new Schema(); - sc.AddSortableTextField("name", 1.0); - sc.AddSortableNumericField("count"); - cl.CreateIndex(sc, new ConfiguredIndexOptions()); - cl.AddDocument(new Document("data1").Set("name", "abc").Set("count", 10)); - cl.AddDocument(new Document("data2").Set("name", "def").Set("count", 5)); - cl.AddDocument(new Document("data3").Set("name", "def").Set("count", 25)); - - AggregationRequest r = new AggregationRequest() - .GroupBy("@name", Reducers.Sum("@count").As("sum")) - .SortBy(SortedField.Descending("@sum"), 10); - - // actual search - AggregationResult res = cl.Aggregate(r); - var r1 = res.GetRow(0); - Assert.NotNull(r1); - Assert.Equal("def", r1.Value.GetString("name")); - Assert.Equal(30, r1.Value.GetInt64("sum")); - - var r2 = res.GetRow(1); - Assert.NotNull(r2); - Assert.Equal("abc", r2.Value.GetString("name")); - Assert.Equal(10, r2.Value.GetInt64("sum")); - } - } -} diff --git a/tests/NRediSearch.Test/ClientTests/ClientTest.cs b/tests/NRediSearch.Test/ClientTests/ClientTest.cs deleted file mode 100644 index cc082a9cf..000000000 --- a/tests/NRediSearch.Test/ClientTests/ClientTest.cs +++ /dev/null @@ -1,1160 +0,0 @@ -using System.Threading; -using System.Collections.Generic; -using System.Text; -using System; -using StackExchange.Redis; -using Xunit; -using Xunit.Abstractions; -using NRediSearch.Aggregation; -using static NRediSearch.Client; -using static NRediSearch.Schema; -using static NRediSearch.SuggestionOptions; - - -namespace NRediSearch.Test.ClientTests -{ - public class ClientTest : RediSearchTestBase - { - public ClientTest(ITestOutputHelper output) : base(output) { } - - private long getModuleSearchVersion() { - Client cl = GetClient(); - var modules = (RedisResult[])Db.Execute("MODULE", "LIST"); - long version = 0; - foreach (var module in modules) { - var result = (RedisResult[])module; - if (result[1].ToString() == ("search")) { - version = (long)result[3]; - } - } - return version; - } - - [Fact] - public void Search() - { - Client cl = GetClient(); - - Schema sc = new Schema().AddTextField("title", 1.0).AddTextField("body", 1.0); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - var fields = new Dictionary - { - { "title", "hello world" }, - { "body", "lorem ipsum" } - }; - for (int i = 0; i < 100; i++) - { - Assert.True(cl.AddDocument($"doc{i}", fields, (double)i / 100.0)); - } - - SearchResult res = cl.Search(new Query("hello world") { WithScores = true }.Limit(0, 5)); - Assert.Equal(100, res.TotalResults); - Assert.Equal(5, res.Documents.Count); - foreach (var d in res.Documents) - { - Assert.StartsWith("doc", d.Id); - Assert.True(d.Score < 100); - //System.out.println(d); - } - - Assert.True(cl.DeleteDocument("doc0")); - Assert.False(cl.DeleteDocument("doc0")); - - res = cl.Search(new Query("hello world")); - Assert.Equal(99, res.TotalResults); - - Assert.True(cl.DropIndex()); - - var ex = Assert.Throws(() => cl.Search(new Query("hello world"))); - Output.WriteLine("Exception: " + ex.Message); - Assert.True(IsMissingIndexException(ex)); - } - - [Fact] - public void TestNumericFilter() - { - Client cl = GetClient(); - - Schema sc = new Schema().AddTextField("title", 1.0).AddNumericField("price"); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - - for (int i = 0; i < 100; i++) - { - var fields = new Dictionary - { - { "title", "hello world" }, - { "price", i } - }; - Assert.True(cl.AddDocument($"doc{i}", fields)); - } - - SearchResult res = cl.Search(new Query("hello world"). - AddFilter(new Query.NumericFilter("price", 0, 49))); - Assert.Equal(50, res.TotalResults); - Assert.Equal(10, res.Documents.Count); - foreach (var d in res.Documents) - { - long price = (long)d["price"]; - Assert.True(price >= 0); - Assert.True(price <= 49); - } - - res = cl.Search(new Query("hello world"). - AddFilter(new Query.NumericFilter("price", 0, true, 49, true))); - Assert.Equal(48, res.TotalResults); - Assert.Equal(10, res.Documents.Count); - foreach (var d in res.Documents) - { - long price = (long)d["price"]; - Assert.True(price > 0); - Assert.True(price < 49); - } - res = cl.Search(new Query("hello world"). - AddFilter(new Query.NumericFilter("price", 50, 100))); - Assert.Equal(50, res.TotalResults); - Assert.Equal(10, res.Documents.Count); - foreach (var d in res.Documents) - { - long price = (long)d["price"]; - Assert.True(price >= 50); - Assert.True(price <= 100); - } - - res = cl.Search(new Query("hello world"). - AddFilter(new Query.NumericFilter("price", 20, double.PositiveInfinity))); - Assert.Equal(80, res.TotalResults); - Assert.Equal(10, res.Documents.Count); - - res = cl.Search(new Query("hello world"). - AddFilter(new Query.NumericFilter("price", double.NegativeInfinity, 10))); - Assert.Equal(11, res.TotalResults); - Assert.Equal(10, res.Documents.Count); - } - - [Fact] - public void TestStopwords() - { - Client cl = GetClient(); - - Schema sc = new Schema().AddTextField("title", 1.0); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions().SetStopwords("foo", "bar", "baz"))); - - var fields = new Dictionary - { - { "title", "hello world foo bar" } - }; - Assert.True(cl.AddDocument("doc1", fields)); - SearchResult res = cl.Search(new Query("hello world")); - Assert.Equal(1, res.TotalResults); - res = cl.Search(new Query("foo bar")); - Assert.Equal(0, res.TotalResults); - - Reset(cl); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions().SetNoStopwords())); - fields = new Dictionary - { - { "title", "hello world foo bar to be or not to be" } - }; - Assert.True(cl.AddDocument("doc1", fields)); - - Assert.Equal(1, cl.Search(new Query("hello world")).TotalResults); - Assert.Equal(1, cl.Search(new Query("foo bar")).TotalResults); - Assert.Equal(1, cl.Search(new Query("to be or not to be")).TotalResults); - } - - [Fact] - public void TestSkipInitialIndex() - { - Db.HashSet("doc1", "foo", "bar"); - var query = new Query("@foo:bar"); - var sc = new Schema().AddTextField("foo"); - - var client1 = new Client("idx1", Db); - Assert.True(client1.CreateIndex(sc, new ConfiguredIndexOptions())); - Assert.Equal(1, client1.Search(query).TotalResults); - - var client2 = new Client("idx2", Db); - Assert.True(client2.CreateIndex(sc, new ConfiguredIndexOptions().SetSkipInitialScan())); - Assert.Equal(0, client2.Search(query).TotalResults); - } - - [Fact] - public void TestSummarizationDisabled() - { - Client cl = GetClient(); - - Schema sc = new Schema().AddTextField("body"); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions().SetUseTermOffsets())); - var fields = new Dictionary - { - { "body", "hello world" } - }; - Assert.True(cl.AddDocument("doc1", fields)); - - var ex = Assert.Throws(() => cl.Search(new Query("hello").SummarizeFields("body"))); - Assert.Equal("Cannot use highlight/summarize because NOOFSETS was specified at index level", ex.Message); - - cl = GetClient(); - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions().SetNoHighlight())); - Assert.True(cl.AddDocument("doc2", fields)); - Assert.Throws(() => cl.Search(new Query("hello").SummarizeFields("body"))); - } - - [Fact] - public void TestExpire() - { - var cl = new Client("idx", Db); - Schema sc = new Schema().AddTextField("title"); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions().SetTemporaryTime(4).SetMaxTextFields())); - long ttl = (long) Db.Execute("FT.DEBUG", "TTL", "idx"); - while (ttl > 2) { - ttl = (long) Db.Execute("FT.DEBUG", "TTL", "idx"); - Thread.Sleep(10); - } - - var fields = new Dictionary - { - { "title", "hello world foo bar to be or not to be" } - }; - Assert.True(cl.AddDocument("doc1", fields)); - ttl = (long) Db.Execute("FT.DEBUG", "TTL", "idx"); - Assert.True(ttl > 2); - } - - [Fact] - public void TestGeoFilter() - { - Client cl = GetClient(); - - Schema sc = new Schema().AddTextField("title", 1.0).AddGeoField("loc"); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - var fields = new Dictionary - { - { "title", "hello world" }, - { "loc", "-0.441,51.458" } - }; - Assert.True(cl.AddDocument("doc1", fields)); - - fields["loc"] = "-0.1,51.2"; - Assert.True(cl.AddDocument("doc2", fields)); - - SearchResult res = cl.Search(new Query("hello world"). - AddFilter( - new Query.GeoFilter("loc", -0.44, 51.45, - 10, GeoUnit.Kilometers) - )); - - Assert.Equal(1, res.TotalResults); - res = cl.Search(new Query("hello world"). - AddFilter( - new Query.GeoFilter("loc", -0.44, 51.45, - 100, GeoUnit.Kilometers) - )); - Assert.Equal(2, res.TotalResults); - } - - [Fact] - public void TestPayloads() - { - Client cl = GetClient(); - - Schema sc = new Schema().AddTextField("title", 1.0); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - - var fields = new Dictionary - { - { "title", "hello world" } - }; - const string payload = "foo bar"; - Assert.True(cl.AddDocument("doc1", fields, 1.0, false, false, Encoding.UTF8.GetBytes(payload))); - SearchResult res = cl.Search(new Query("hello world") { WithPayloads = true }); - Assert.Equal(1, res.TotalResults); - Assert.Single(res.Documents); - - Assert.Equal(payload, Encoding.UTF8.GetString(res.Documents[0].Payload)); - } - - [Fact] - public void TestQueryFlags() - { - Client cl = GetClient(); - - Schema sc = new Schema().AddTextField("title", 1.0); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - var fields = new Dictionary(); - - for (int i = 0; i < 100; i++) - { - fields["title"] = i % 2 == 1 ? "hello worlds" : "hello world"; - Assert.True(cl.AddDocument($"doc{i}", fields, (double)i / 100.0)); - } - - Query q = new Query("hello").SetWithScores(); - SearchResult res = cl.Search(q); - - Assert.Equal(100, res.TotalResults); - Assert.Equal(10, res.Documents.Count); - - foreach (var d in res.Documents) - { - Assert.StartsWith("doc", d.Id); - Assert.True(d.Score != 1.0); - Assert.StartsWith("hello world", d["title"]); - } - - q = new Query("hello").SetNoContent(); - res = cl.Search(q); - foreach (var d in res.Documents) - { - Assert.StartsWith("doc", d.Id); - Assert.True(d.Score == 1.0); - Assert.True(d["title"].IsNull); - } - - // test verbatim vs. stemming - res = cl.Search(new Query("hello worlds")); - Assert.Equal(100, res.TotalResults); - res = cl.Search(new Query("hello worlds").SetVerbatim()); - Assert.Equal(50, res.TotalResults); - - res = cl.Search(new Query("hello a world").SetVerbatim()); - Assert.Equal(50, res.TotalResults); - res = cl.Search(new Query("hello a worlds").SetVerbatim()); - Assert.Equal(50, res.TotalResults); - res = cl.Search(new Query("hello a world").SetVerbatim().SetNoStopwords()); - Assert.Equal(0, res.TotalResults); - } - - [Fact] - public void TestSortQueryFlags() - { - Client cl = GetClient(); - Schema sc = new Schema().AddSortableTextField("title", 1.0); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - var fields = new Dictionary - { - ["title"] = "b title" - }; - cl.AddDocument("doc1", fields, 1.0, false, true, null); - - fields["title"] = "a title"; - cl.AddDocument("doc2", fields, 1.0, false, true, null); - - fields["title"] = "c title"; - cl.AddDocument("doc3", fields, 1.0, false, true, null); - - Query q = new Query("title").SetSortBy("title", true); - SearchResult res = cl.Search(q); - - Assert.Equal(3, res.TotalResults); - Document doc1 = res.Documents[0]; - Assert.Equal("a title", doc1["title"]); - - doc1 = res.Documents[1]; - Assert.Equal("b title", doc1["title"]); - - doc1 = res.Documents[2]; - Assert.Equal("c title", doc1["title"]); - } - - [Fact] - public void TestIndexDefinition() - { - Client cl = GetClient(); - Schema sc = new Schema().AddTextField("title", 1.0); - ConfiguredIndexOptions options = new ConfiguredIndexOptions( - new IndexDefinition( prefixes: new string[]{cl.IndexName})); - Assert.True(cl.CreateIndex(sc, options)); - - RedisKey hashKey = (string)cl.IndexName + ":foo"; - Db.KeyDelete(hashKey); - Db.HashSet(hashKey, "title", "hello world"); - - try - { -#pragma warning disable 0618 - Assert.True(cl.AddHash(hashKey, 1, false)); -#pragma warning restore 0618 - } - catch (RedisServerException e) - { - Assert.StartsWith("ERR unknown command `FT.ADDHASH`", e.Message); - return; // Starting from RediSearch 2.0 this command is not supported anymore - } - SearchResult res = cl.Search(new Query("hello world").SetVerbatim()); - Assert.Equal(1, res.TotalResults); - Assert.Equal(hashKey, res.Documents[0].Id); - } - - [Fact] - public void TestDrop() - { - Client cl = GetClient(); - - Schema sc = new Schema().AddTextField("title", 1.0); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - var fields = new Dictionary - { - { "title", "hello world" } - }; - for (int i = 0; i < 100; i++) - { - Assert.True(cl.AddDocument($"doc{i}", fields)); - } - - SearchResult res = cl.Search(new Query("hello world")); - Assert.Equal(100, res.TotalResults); - - var key = (string)Db.KeyRandom(); - Output.WriteLine("Found key: " + key); - Assert.NotNull(key); - - Reset(cl); - - var indexExists = Db.KeyExists(cl.IndexName); - Assert.False(indexExists); - } - - [Fact] - public void TestAlterAdd() - { - Client cl = GetClient(); - - Schema sc = new Schema().AddTextField("title", 1.0); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - var fields = new Dictionary - { - { "title", "hello world" } - }; - for (int i = 0; i < 100; i++) - { - Assert.True(cl.AddDocument($"doc{i}", fields)); - } - - SearchResult res = cl.Search(new Query("hello world")); - Assert.Equal(100, res.TotalResults); - - Assert.True(cl.AlterIndex(new TagField("tags", ","), new TextField("name", 0.5))); - for (int i = 0; i < 100; i++) - { - var fields2 = new Dictionary - { - { "name", $"name{i}" }, - { "tags", $"tagA,tagB,tag{i}" } - }; - Assert.True(cl.UpdateDocument($"doc{i}", fields2, 1.0)); - } - SearchResult res2 = cl.Search(new Query("@tags:{tagA}")); - Assert.Equal(100, res2.TotalResults); - - var info = cl.GetInfoParsed(); - Assert.Equal(cl.IndexName, info.IndexName); - - Assert.True(info.Fields.ContainsKey("tags")); - Assert.Equal("TAG", (string)info.Fields["tags"][2]); - } - - [Fact] - public void TestNoStem() - { - Client cl = GetClient(); - - Schema sc = new Schema().AddTextField("stemmed", 1.0).AddField(new TextField("notStemmed", 1.0, false, true)); - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - - var doc = new Dictionary - { - { "stemmed", "located" }, - { "notStemmed", "located" } - }; - // Store it - Assert.True(cl.AddDocument("doc", doc)); - - // Query - SearchResult res = cl.Search(new Query("@stemmed:location")); - Assert.Equal(1, res.TotalResults); - - res = cl.Search(new Query("@notStemmed:location")); - Assert.Equal(0, res.TotalResults); - } - - [Fact] - public void TestInfoParsed() - { - Client cl = GetClient(); - - Schema sc = new Schema().AddTextField("title", 1.0); - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - - var info = cl.GetInfoParsed(); - Assert.Equal(cl.IndexName, info.IndexName); - } - - [Fact] - public void TestInfo() - { - Client cl = GetClient(); - - Schema sc = new Schema().AddTextField("title", 1.0); - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - - var info = cl.GetInfo(); - Assert.Equal(cl.IndexName, info["index_name"]); - } - - [Fact] - public void TestNoIndex() - { - Client cl = GetClient(); - - Schema sc = new Schema() - .AddField(new TextField("f1", 1.0, true, false, true)) - .AddField(new TextField("f2", 1.0)); - cl.CreateIndex(sc, new ConfiguredIndexOptions()); - - var mm = new Dictionary - { - { "f1", "MarkZZ" }, - { "f2", "MarkZZ" } - }; - cl.AddDocument("doc1", mm); - - mm.Clear(); - mm.Add("f1", "MarkAA"); - mm.Add("f2", "MarkBB"); - cl.AddDocument("doc2", mm); - - SearchResult res = cl.Search(new Query("@f1:Mark*")); - Assert.Equal(0, res.TotalResults); - - res = cl.Search(new Query("@f2:Mark*")); - Assert.Equal(2, res.TotalResults); - - res = cl.Search(new Query("@f2:Mark*").SetSortBy("f1", false)); - Assert.Equal(2, res.TotalResults); - - Assert.Equal("doc1", res.Documents[0].Id); - - res = cl.Search(new Query("@f2:Mark*").SetSortBy("f1", true)); - Assert.Equal("doc2", res.Documents[0].Id); - } - - [Fact] - public void TestReplacePartial() - { - Client cl = GetClient(); - - Schema sc = new Schema() - .AddTextField("f1", 1.0) - .AddTextField("f2", 1.0) - .AddTextField("f3", 1.0); - cl.CreateIndex(sc, new ConfiguredIndexOptions()); - - var mm = new Dictionary - { - { "f1", "f1_val" }, - { "f2", "f2_val" } - }; - - cl.AddDocument("doc1", mm); - cl.AddDocument("doc2", mm); - - mm.Clear(); - mm.Add("f3", "f3_val"); - - cl.UpdateDocument("doc1", mm, 1.0); - cl.ReplaceDocument("doc2", mm, 1.0); - - // Search for f3 value. All documents should have it. - SearchResult res = cl.Search(new Query("@f3:f3_Val")); - Assert.Equal(2, res.TotalResults); - - res = cl.Search(new Query("@f3:f3_val @f2:f2_val @f1:f1_val")); - Assert.Equal(1, res.TotalResults); - } - - [Fact] - public void TestExplain() - { - Client cl = GetClient(); - - Schema sc = new Schema() - .AddTextField("f1", 1.0) - .AddTextField("f2", 1.0) - .AddTextField("f3", 1.0); - cl.CreateIndex(sc, new ConfiguredIndexOptions()); - - var res = cl.Explain(new Query("@f3:f3_val @f2:f2_val @f1:f1_val")); - Assert.NotNull(res); - Assert.False(res.Length == 0); - Output.WriteLine(res); - } - - [Fact] - public void TestHighlightSummarize() - { - Client cl = GetClient(); - Schema sc = new Schema().AddTextField("text", 1.0); - cl.CreateIndex(sc, new ConfiguredIndexOptions()); - - var doc = new Dictionary - { - { "text", "Redis is often referred as a data structures server. What this means is that Redis provides access to mutable data structures via a set of commands, which are sent using a server-client model with TCP sockets and a simple protocol. So different processes can query and modify the same data structures in a shared way" } - }; - // Add a document - cl.AddDocument("foo", doc, 1.0); - Query q = new Query("data").HighlightFields().SummarizeFields(); - SearchResult res = cl.Search(q); - - Assert.Equal("is often referred as a data structures server. What this means is that Redis provides... What this means is that Redis provides access to mutable data structures via a set of commands, which are sent using a... So different processes can query and modify the same data structures in a shared... ", - res.Documents[0]["text"]); - - q = new Query("data").HighlightFields(new Query.HighlightTags("", "")).SummarizeFields(); - res = cl.Search(q); - - Assert.Equal("is often referred as a data structures server. What this means is that Redis provides... What this means is that Redis provides access to mutable data structures via a set of commands, which are sent using a... So different processes can query and modify the same data structures in a shared... ", - res.Documents[0]["text"]); - } - - [Fact] - public void TestLanguage() - { - Client cl = GetClient(); - Schema sc = new Schema().AddTextField("text", 1.0); - cl.CreateIndex(sc, new ConfiguredIndexOptions()); - - Document d = new Document("doc1").Set("text", "hello"); - AddOptions options = new AddOptions().SetLanguage("spanish"); - Assert.True(cl.AddDocument(d, options)); - - options.SetLanguage("ybreski"); - cl.DeleteDocument(d.Id); - - var ex = Assert.Throws(() => cl.AddDocument(d, options)); - Assert.Equal("Unsupported language", ex.Message, ignoreCase: true); - } - - [Fact] - public void TestDropMissing() - { - Client cl = GetClient(); - var ex = Assert.Throws(() => cl.DropIndex()); - Assert.True(IsMissingIndexException(ex)); - } - - [Fact] - public void TestGet() - { - Client cl = GetClient(); - cl.CreateIndex(new Schema().AddTextField("txt1", 1.0), new ConfiguredIndexOptions()); - cl.AddDocument(new Document("doc1").Set("txt1", "Hello World!"), new AddOptions()); - Document d = cl.GetDocument("doc1"); - Assert.NotNull(d); - Assert.Equal("Hello World!", d["txt1"]); - - // Get something that does not exist. Shouldn't explode - Assert.Null(cl.GetDocument("nonexist")); - } - - [Fact] - public void TestMGet() - { - Client cl = GetClient(); - - cl.CreateIndex(new Schema().AddTextField("txt1", 1.0), new ConfiguredIndexOptions()); - cl.AddDocument(new Document("doc1").Set("txt1", "Hello World!1"), new AddOptions()); - cl.AddDocument(new Document("doc2").Set("txt1", "Hello World!2"), new AddOptions()); - cl.AddDocument(new Document("doc3").Set("txt1", "Hello World!3"), new AddOptions()); - - var docs = cl.GetDocuments(); - Assert.Empty(docs); - - docs = cl.GetDocuments("doc1", "doc3", "doc4"); - Assert.Equal(3, docs.Length); - Assert.Equal("Hello World!1", docs[0]["txt1"]); - Assert.Equal("Hello World!3", docs[1]["txt1"]); - Assert.Null(docs[2]); - } - - [Fact] - public void TestAddSuggestionGetSuggestionFuzzy() - { - Client cl = GetClient(); - Suggestion suggestion = Suggestion.Builder.String("TOPIC OF WORDS").Score(1).Build(); - // test can add a suggestion string - Assert.True(cl.AddSuggestion(suggestion, true) > 0, $"{suggestion} insert should of returned at least 1"); - // test that the partial part of that string will be returned using fuzzy - - //Assert.Equal(suggestion.ToString() + " suppose to be returned", suggestion, cl.GetSuggestion(suggestion.String.Substring(0, 3), SuggestionOptions.GetBuilder().Build()).get(0)); - Assert.Equal(suggestion.ToString(), cl.GetSuggestions(suggestion.String.Substring(0, 3), SuggestionOptions.Builder.Build())[0].ToString()); - } - - [Fact] - public void TestAddSuggestionGetSuggestion() - { - Client cl = GetClient(); - Suggestion suggestion = Suggestion.Builder.String("ANOTHER_WORD").Score(1).Build(); - Suggestion noMatch = Suggestion.Builder.String("_WORD MISSED").Score(1).Build(); - - Assert.True(cl.AddSuggestion(suggestion, false) > 0, $"{suggestion} should of inserted at least 1"); - Assert.True(cl.AddSuggestion(noMatch, false) > 0, $"{noMatch} should of inserted at least 1"); - - // test that with a partial part of that string will have the entire word returned SuggestionOptions.builder().build() - Assert.Single(cl.GetSuggestions(suggestion.String.Substring(0, 3), SuggestionOptions.Builder.Fuzzy().Build())); - - // turn off fuzzy start at second word no hit - Assert.Empty(cl.GetSuggestions(noMatch.String.Substring(1, 6), SuggestionOptions.Builder.Build())); - // my attempt to trigger the fuzzy by 1 character - Assert.Single(cl.GetSuggestions(noMatch.String.Substring(1, 6), SuggestionOptions.Builder.Fuzzy().Build())); - } - - [Fact] - public void TestAddSuggestionGetSuggestionPayloadScores() - { - Client cl = GetClient(); - - Suggestion suggestion = Suggestion.Builder.String("COUNT_ME TOO").Payload("PAYLOADS ROCK ").Score(0.2).Build(); - Assert.True(cl.AddSuggestion(suggestion, false) > 0, $"{suggestion} insert should of at least returned 1"); - Assert.True(cl.AddSuggestion(suggestion.ToBuilder().String("COUNT").Payload("My PAYLOAD is better").Build(), false) > 1, "Count single added should return more than 1"); - Assert.True(cl.AddSuggestion(suggestion.ToBuilder().String("COUNT_ANOTHER").Score(1).Payload(null).Build(), false) > 1, "Count single added should return more than 1"); - - Suggestion noScoreOrPayload = Suggestion.Builder.String("COUNT NO PAYLOAD OR COUNT").Build(); - Assert.True(cl.AddSuggestion(noScoreOrPayload, true) > 1, "Count single added should return more than 1"); - - var payloads = cl.GetSuggestions(suggestion.String.Substring(0, 3), SuggestionOptions.Builder.With(WithOptions.PayloadsAndScores).Build()); - Assert.Equal(4, payloads.Length); - Assert.True(payloads[2].Payload.Length > 0); - Assert.True(payloads[1].Score < .299, "Actual score: " + payloads[1].Score); - } - - [Fact] - public void TestAddSuggestionGetSuggestionPayload() - { - Client cl = GetClient(); - cl.AddSuggestion(Suggestion.Builder.String("COUNT_ME TOO").Payload("PAYLOADS ROCK ").Build(), false); - cl.AddSuggestion(Suggestion.Builder.String("COUNT").Payload("ANOTHER PAYLOAD ").Build(), false); - cl.AddSuggestion(Suggestion.Builder.String("COUNTNO PAYLOAD OR COUNT").Build(), false); - - // test that with a partial part of that string will have the entire word returned - var payloads = cl.GetSuggestions("COU", SuggestionOptions.Builder.Max(3).Fuzzy().With(WithOptions.Payloads).Build()); - Assert.Equal(3, payloads.Length); - } - - [Fact] - public void TestGetSuggestionNoPayloadTwoOnly() - { - Client cl = GetClient(); - - cl.AddSuggestion(Suggestion.Builder.String("DIFF_WORD").Score(0.4).Payload("PAYLOADS ROCK ").Build(), false); - cl.AddSuggestion(Suggestion.Builder.String("DIFF wording").Score(0.5).Payload("ANOTHER PAYLOAD ").Build(), false); - cl.AddSuggestion(Suggestion.Builder.String("DIFFERENT").Score(0.7).Payload("I am a payload").Build(), false); - - var payloads = cl.GetSuggestions("DIF", SuggestionOptions.Builder.Max(2).Build()); - Assert.Equal(2, payloads.Length); - - var three = cl.GetSuggestions("DIF", SuggestionOptions.Builder.Max(3).Build()); - Assert.Equal(3, three.Length); - } - - [Fact] - public void TestGetSuggestionsAsStringArray() - { - Client cl = GetClient(); - - cl.AddSuggestion(Suggestion.Builder.String("DIFF_WORD").Score(0.4).Payload("PAYLOADS ROCK ").Build(), false); - cl.AddSuggestion(Suggestion.Builder.String("DIFF wording").Score(0.5).Payload("ANOTHER PAYLOAD ").Build(), false); - cl.AddSuggestion(Suggestion.Builder.String("DIFFERENT").Score(0.7).Payload("I am a payload").Build(), false); - - var payloads = cl.GetSuggestions("DIF", max: 2); - Assert.Equal(2, payloads.Length); - - var three = cl.GetSuggestions("DIF", max: 3); - Assert.Equal(3, three.Length); - } - - [Fact] - public void TestGetSuggestionWithScore() - { - Client cl = GetClient(); - - cl.AddSuggestion(Suggestion.Builder.String("DIFF_WORD").Score(0.4).Payload("PAYLOADS ROCK ").Build(), true); - var list = cl.GetSuggestions("DIF", SuggestionOptions.Builder.Max(2).With(WithOptions.Scores).Build()); - Assert.True(list[0].Score <= .2, "Actual score: " + list[0].Score); - } - - [Fact] - public void TestGetSuggestionAllNoHit() - { - Client cl = GetClient(); - - cl.AddSuggestion(Suggestion.Builder.String("NO WORD").Score(0.4).Build(), false); - - var none = cl.GetSuggestions("DIF", SuggestionOptions.Builder.Max(3).With(WithOptions.Scores).Build()); - Assert.Empty(none); - } - - [Fact] - public void TestGetTagField() - { - Client cl = GetClient(); - Schema sc = new Schema() - .AddTextField("title", 1.0) - .AddTagField("category"); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - - var search = cl.Search(new Query("hello")); - Output.WriteLine("Initial search: " + search.TotalResults); - Assert.Equal(0, search.TotalResults); - - Assert.True(cl.AddDocument("foo", new Dictionary - { - { "title", "hello world" }, - { "category", "red" } - })); - Assert.True(cl.AddDocument("bar", new Dictionary - { - { "title", "hello world" }, - { "category", "blue" } - })); - Assert.True(cl.AddDocument("baz", new Dictionary - { - { "title", "hello world" }, - { "category", "green,yellow" } - })); - Assert.True(cl.AddDocument("qux", new Dictionary - { - { "title", "hello world" }, - { "category", "orange;purple" } - })); - - Assert.Equal(1, cl.Search(new Query("@category:{red}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("@category:{blue}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("hello @category:{red}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("hello @category:{blue}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("@category:{yellow}")).TotalResults); - Assert.Equal(0, cl.Search(new Query("@category:{purple}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("@category:{orange\\;purple}")).TotalResults); - search = cl.Search(new Query("hello")); - Output.WriteLine("Post-search: " + search.TotalResults); - foreach (var doc in search.Documents) - { - Output.WriteLine("Found: " + doc.Id); - } - Assert.Equal(4, search.TotalResults); - } - - [Fact] - public void TestGetTagFieldWithNonDefaultSeparator() - { - Client cl = GetClient(); - Schema sc = new Schema() - .AddTextField("title", 1.0) - .AddTagField("category", ";"); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - Assert.True(cl.AddDocument("foo", new Dictionary - { - { "title", "hello world" }, - { "category", "red" } - })); - Assert.True(cl.AddDocument("bar", new Dictionary - { - { "title", "hello world" }, - { "category", "blue" } - })); - Assert.True(cl.AddDocument("baz", new Dictionary - { - { "title", "hello world" }, - { "category", "green;yellow" } - })); - Assert.True(cl.AddDocument("qux", new Dictionary - { - { "title", "hello world" }, - { "category", "orange,purple" } - })); - - Assert.Equal(1, cl.Search(new Query("@category:{red}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("@category:{blue}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("hello @category:{red}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("hello @category:{blue}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("hello @category:{yellow}")).TotalResults); - Assert.Equal(0, cl.Search(new Query("@category:{purple}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("@category:{orange\\,purple}")).TotalResults); - Assert.Equal(4, cl.Search(new Query("hello")).TotalResults); - } - - [Fact] - public void TestGetSortableTagField() - { - Client cl = GetClient(); - Schema sc = new Schema() - .AddTextField("title", 1.0) - .AddSortableTagField("category", ";"); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - Assert.True(cl.AddDocument("foo", new Dictionary - { - { "title", "hello world" }, - { "category", "red" } - })); - Assert.True(cl.AddDocument("bar", new Dictionary - { - { "title", "hello world" }, - { "category", "blue" } - })); - Assert.True(cl.AddDocument("baz", new Dictionary - { - { "title", "hello world" }, - { "category", "green;yellow" } - })); - Assert.True(cl.AddDocument("qux", new Dictionary - { - { "title", "hello world" }, - { "category", "orange,purple" } - })); - - var res = cl.Search(new Query("*") { SortBy = "category", SortAscending = false }); - Assert.Equal("red", res.Documents[0]["category"]); - Assert.Equal("orange,purple", res.Documents[1]["category"]); - Assert.Equal("green;yellow", res.Documents[2]["category"]); - Assert.Equal("blue", res.Documents[3]["category"]); - - Assert.Equal(1, cl.Search(new Query("@category:{red}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("@category:{blue}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("hello @category:{red}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("hello @category:{blue}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("hello @category:{yellow}")).TotalResults); - Assert.Equal(0, cl.Search(new Query("@category:{purple}")).TotalResults); - Assert.Equal(1, cl.Search(new Query("@category:{orange\\,purple}")).TotalResults); - Assert.Equal(4, cl.Search(new Query("hello")).TotalResults); - } - - [Fact] - public void TestGetTagFieldUnf() { - // Add version check - - Client cl = GetClient(); - - // Check that UNF can't be given to non-sortable filed - try { - var temp = new Schema().AddField(new TextField("non-sortable-unf", 1.0, sortable: false, unNormalizedForm: true)); - Assert.True(false); - } catch (ArgumentException) { - Assert.True(true); - } - - Schema sc = new Schema().AddSortableTextField("txt").AddSortableTextField("txt_unf", unf: true). - AddSortableTagField("tag").AddSortableTagField("tag_unf", unNormalizedForm: true); - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - Db.Execute("HSET", "doc1", "txt", "FOO", "txt_unf", "FOO", "tag", "FOO", "tag_unf", "FOO"); - - AggregationBuilder r = new AggregationBuilder() - .GroupBy(new List {"@txt", "@txt_unf", "@tag", "@tag_unf"}, new List {}); - - AggregationResult res = cl.Aggregate(r); - var results = res.GetResults()[0]; - Assert.NotNull(results); - Assert.Equal(4, results.Count); - Assert.Equal("foo", results["txt"]); - Assert.Equal("FOO", results["txt_unf"]); - Assert.Equal("foo", results["tag"]); - Assert.Equal("FOO", results["tag_unf"]); - } - - [Fact] - public void TestMultiDocuments() - { - Client cl = GetClient(); - Schema sc = new Schema().AddTextField("title").AddTextField("body"); - - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - - var fields = new Dictionary - { - { "title", "hello world" }, - { "body", "lorem ipsum" } - }; - - var results = cl.AddDocuments(new Document("doc1", fields), new Document("doc2", fields), new Document("doc3", fields)); - - Assert.Equal(new[] { true, true, true }, results); - - Assert.Equal(3, cl.Search(new Query("hello world")).TotalResults); - - results = cl.AddDocuments(new Document("doc4", fields), new Document("doc2", fields), new Document("doc5", fields)); - Assert.Equal(new[] { true, false, true }, results); - - results = cl.DeleteDocuments(true, "doc1", "doc2", "doc36"); - Assert.Equal(new[] { true, true, false }, results); - } - - [Fact] - public void TestReturnFields() - { - Client cl = GetClient(); - - Schema sc = new Schema().AddTextField("field1", 1.0).AddTextField("field2", 1.0); - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - - var doc = new Dictionary - { - { "field1", "value1" }, - { "field2", "value2" } - }; - // Store it - Assert.True(cl.AddDocument("doc", doc)); - - // Query - SearchResult res = cl.Search(new Query("*").ReturnFields("field1")); - Assert.Equal(1, res.TotalResults); - Assert.Equal("value1", res.Documents[0]["field1"]); - Assert.Null((string)res.Documents[0]["field2"]); - } - - [Fact] - public void TestInKeys() - { - Client cl = GetClient(); - Schema sc = new Schema().AddTextField("field1", 1.0).AddTextField("field2", 1.0); - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - - var doc = new Dictionary - { - { "field1", "value" }, - { "field2", "not" } - }; - - // Store it - Assert.True(cl.AddDocument("doc1", doc)); - Assert.True(cl.AddDocument("doc2", doc)); - - // Query - SearchResult res = cl.Search(new Query("value").LimitKeys("doc1")); - Assert.Equal(1, res.TotalResults); - Assert.Equal("doc1", res.Documents[0].Id); - Assert.Equal("value", res.Documents[0]["field1"]); - Assert.Null((string)res.Documents[0]["value"]); - } - - [Fact] - public void TestWithFieldNames() - { - if (getModuleSearchVersion() <= 20200) { - return; - } - - Client cl = GetClient(); - IndexDefinition defenition = new IndexDefinition(prefixes: new string[] {"student:", "pupil:"}); - Schema sc = new Schema().AddTextField(FieldName.Of("first").As("given")).AddTextField(FieldName.Of("last")); - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions(defenition))); - - var docsIds = new string[] {"student:111", "pupil:222", "student:333", "teacher:333"}; - var docsData = new Dictionary[] { - new Dictionary { - { "first", "Joen" }, - { "last", "Ko" }, - { "age", "20" } - }, - new Dictionary { - { "first", "Joe" }, - { "last", "Dod" }, - { "age", "18" } - }, - new Dictionary { - { "first", "El" }, - { "last", "Mark" }, - { "age", "17" } - }, - new Dictionary { - { "first", "Pat" }, - { "last", "Rod" }, - { "age", "20" } - } - }; - - for (int i = 0; i < docsIds.Length; i++) { - Assert.True(cl.AddDocument(docsIds[i], docsData[i])); - } - - // Query - SearchResult noFilters = cl.Search(new Query("*")); - Assert.Equal(3, noFilters.TotalResults); - Assert.Equal("student:111", noFilters.Documents[0].Id); - Assert.Equal("pupil:222", noFilters.Documents[1].Id); - Assert.Equal("student:333", noFilters.Documents[2].Id); - - SearchResult asOriginal = cl.Search(new Query("@first:Jo*")); - Assert.Equal(0, asOriginal.TotalResults); - - SearchResult asAttribute = cl.Search(new Query("@given:Jo*")); - Assert.Equal(2, asAttribute.TotalResults); - Assert.Equal("student:111", noFilters.Documents[0].Id); - Assert.Equal("pupil:222", noFilters.Documents[1].Id); - - SearchResult nonAttribute = cl.Search(new Query("@last:Rod")); - Assert.Equal(0, nonAttribute.TotalResults); - } - - [Fact] - public void TestReturnWithFieldNames() - { - if (getModuleSearchVersion() <= 20200) { - return; - } - - Client cl = GetClient(); - Schema sc = new Schema().AddTextField("a").AddTextField("b").AddTextField("c"); - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions())); - - var doc = new Dictionary - { - { "a", "value1" }, - { "b", "value2" }, - { "c", "value3" } - }; - Assert.True(cl.AddDocument("doc", doc)); - - // Query - SearchResult res = cl.Search(new Query("*").ReturnFields(FieldName.Of("b").As("d"), FieldName.Of("a"))); - Assert.Equal(1, res.TotalResults); - Assert.Equal("doc", res.Documents[0].Id); - Assert.Equal("value1", res.Documents[0]["a"]); - Assert.Equal("value2", res.Documents[0]["d"]); - } - - [Fact] - public void TestJsonIndex() - { - if (getModuleSearchVersion() <= 20200) { - return; - } - - Client cl = GetClient(); - IndexDefinition defenition = new IndexDefinition(prefixes: new string[] {"king:"} ,type: IndexDefinition.IndexType.Json); - Schema sc = new Schema().AddTextField("$.name"); - Assert.True(cl.CreateIndex(sc, new ConfiguredIndexOptions(defenition))); - - Db.Execute("JSON.SET", "king:1", ".", "{\"name\": \"henry\"}"); - Db.Execute("JSON.SET", "king:2", ".", "{\"name\": \"james\"}"); - - // Query - SearchResult res = cl.Search(new Query("henry")); - Assert.Equal(1, res.TotalResults); - Assert.Equal("king:1", res.Documents[0].Id); - Assert.Equal("{\"name\":\"henry\"}", res.Documents[0]["json"]); - } - } -} diff --git a/tests/NRediSearch.Test/ExampleUsage.cs b/tests/NRediSearch.Test/ExampleUsage.cs deleted file mode 100644 index 0860887c0..000000000 --- a/tests/NRediSearch.Test/ExampleUsage.cs +++ /dev/null @@ -1,191 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using StackExchange.Redis; -using Xunit; -using Xunit.Abstractions; -using static NRediSearch.Client; - -namespace NRediSearch.Test -{ - public class ExampleUsage : RediSearchTestBase - { - public ExampleUsage(ITestOutputHelper output) : base(output) { } - - [Fact] - public void BasicUsage() - { - var client = GetClient(); - - try { client.DropIndex(); } catch { /* Intentionally ignored */ } // reset DB - - // Defining a schema for an index and creating it: - var sc = new Schema() - .AddTextField("title", 5.0) - .AddTextField("body", 1.0) - .AddNumericField("price"); - - bool result; - try - { - result = client.CreateIndex(sc, new ConfiguredIndexOptions()); - } - catch (RedisServerException ex) - { - // TODO: Convert to Skip - if (ex.Message == "ERR unknown command 'FT.CREATE'") - { - Output.WriteLine(ex.Message); - Output.WriteLine("Module not installed, aborting"); - } - throw; - } - - Assert.True(result); - - // note: using java API equivalent here; it would be nice to - // use meta-programming / reflection instead in .NET - - // Adding documents to the index: - var fields = new Dictionary - { - ["title"] = "hello world", - ["body"] = "lorem ipsum", - ["price"] = 1337 - }; - - Assert.True(client.AddDocument("doc1", fields)); - - // Creating a complex query - var q = new Query("hello world") - .AddFilter(new Query.NumericFilter("price", 1300, 1350)) - .Limit(0, 5); - - // actual search - var res = client.Search(q); - - Assert.Equal(1, res.TotalResults); - var item = res.Documents.Single(); - Assert.Equal("doc1", item.Id); - - Assert.True(item.HasProperty("title")); - Assert.True(item.HasProperty("body")); - Assert.True(item.HasProperty("price")); - Assert.False(item.HasProperty("blap")); - - Assert.Equal("hello world", item["title"]); - Assert.Equal("lorem ipsum", item["body"]); - Assert.Equal(1337, (int)item["price"]); - } - - [Fact] - public void BasicScoringUsage() - { - var client = GetClient(); - - try { client.DropIndex(); } catch { /* Intentionally ignored */ } // reset DB - - CreateSchema(client); - - var term = "petit*"; - - var query = new Query(term); - query.Limit(0, 10); - query.WithScores = true; - - var searchResult = client.Search(query); - - var docResult = searchResult.Documents.FirstOrDefault(); - - Assert.Equal(1, searchResult.TotalResults); - Assert.NotEqual(0, docResult.Score); - Assert.Equal("1", docResult.Id); - Assert.Null(docResult.ScoreExplained); - } - - [Fact] - public void BasicScoringUsageWithExplainScore() - { - var client = GetClient(); - - try { client.DropIndex(); } catch { /* Intentionally ignored */ } // reset DB - - CreateSchema(client); - - var term = "petit*"; - - var query = new Query(term); - query.Limit(0, 10); - query.WithScores = true; - query.Scoring = "TFIDF"; - query.ExplainScore = true; - - var searchResult = client.Search(query); - - var docResult = searchResult.Documents.FirstOrDefault(); - - Assert.Equal(1, searchResult.TotalResults); - Assert.NotEqual(0, docResult.Score); - Assert.Equal("1", docResult.Id); - Assert.NotEmpty(docResult.ScoreExplained); - Assert.Equal("Final TFIDF : words TFIDF 1.00 * document score 1.00 / norm 2 / slop 1", docResult.ScoreExplained[0]); - Assert.Equal("(Weight 1.00 * total children TFIDF 1.00)", docResult.ScoreExplained[1]); - Assert.Equal("(TFIDF 1.00 = Weight 1.00 * TF 1 * IDF 1.00)", docResult.ScoreExplained[2]); - } - - [Fact] - public void BasicScoringUsageWithExplainScoreDifferentScorer() - { - var client = GetClient(); - - try { client.DropIndex(); } catch { /* Intentionally ignored */ } // reset DB - - CreateSchema(client); - - var term = "petit*"; - - var query = new Query(term); - query.Limit(0, 10); - query.WithScores = true; - query.Scoring = "TFIDF.DOCNORM"; - query.ExplainScore = true; - - var searchResult = client.Search(query); - - var docResult = searchResult.Documents.FirstOrDefault(); - - Assert.Equal(1, searchResult.TotalResults); - Assert.NotEqual(0, docResult.Score); - Assert.Equal("1", docResult.Id); - Assert.NotEmpty(docResult.ScoreExplained); - Assert.Equal("Final TFIDF : words TFIDF 1.00 * document score 1.00 / norm 20 / slop 1", docResult.ScoreExplained[0]); - Assert.Equal("(Weight 1.00 * total children TFIDF 1.00)", docResult.ScoreExplained[1]); - Assert.Equal("(TFIDF 1.00 = Weight 1.00 * TF 1 * IDF 1.00)", docResult.ScoreExplained[2]); - } - - private void CreateSchema(Client client) - { - var schema = new Schema(); - - schema - .AddSortableTextField("title") - .AddTextField("country") - .AddTextField("author") - .AddTextField("aka") - .AddTagField("language"); - - client.CreateIndex(schema, new ConfiguredIndexOptions()); - - var doc = new Document("1"); - - doc - .Set("title", "Le Petit Prince") - .Set("country", "France") - .Set("author", "Antoine de Saint-Exupéry") - .Set("language", "fr_FR") - .Set("aka", "The Little Prince, El Principito"); - - client.AddDocument(doc); - } - } -} - diff --git a/tests/NRediSearch.Test/Issues/Issue940.cs b/tests/NRediSearch.Test/Issues/Issue940.cs deleted file mode 100644 index df68f67a4..000000000 --- a/tests/NRediSearch.Test/Issues/Issue940.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Xunit; - -namespace NRediSearch.Test -{ - public class Issue940 - { - [Fact] - public void Paging_Boxing() - { - for(int i = -20; i < 100; i++) - { - var boxed = i.Boxed(); - Assert.Equal(i, (int)boxed); - } - } - } -} diff --git a/tests/NRediSearch.Test/NRediSearch.Test.csproj b/tests/NRediSearch.Test/NRediSearch.Test.csproj deleted file mode 100644 index 50512a200..000000000 --- a/tests/NRediSearch.Test/NRediSearch.Test.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - Library - netcoreapp3.1 - true - true - - - - - - - - \ No newline at end of file diff --git a/tests/NRediSearch.Test/QueryBuilder/BuilderTest.cs b/tests/NRediSearch.Test/QueryBuilder/BuilderTest.cs deleted file mode 100644 index cd15c136b..000000000 --- a/tests/NRediSearch.Test/QueryBuilder/BuilderTest.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using NRediSearch.Aggregation; -using NRediSearch.QueryBuilder; -using StackExchange.Redis; -using Xunit; -using Xunit.Abstractions; -using static NRediSearch.Aggregation.Reducers.Reducers; -using static NRediSearch.Aggregation.SortedField; -using static NRediSearch.QueryBuilder.QueryBuilder; -using static NRediSearch.QueryBuilder.Values; - -namespace NRediSearch.Test.QueryBuilder -{ - public class BuilderTest : RediSearchTestBase - { - public BuilderTest(ITestOutputHelper output) : base(output) { } - - [Fact] - public void TestTag() - { - Value v = Tags("foo"); - Assert.Equal("{foo}", v.ToString()); - v = Tags("foo", "bar"); - Assert.Equal("{foo | bar}", v.ToString()); - } - - [Fact] - public void TestEmptyTag() - { - Assert.Throws(() => Tags()); - } - - [Fact] - public void TestRange() - { - Value v = Between(1, 10); - Assert.Equal("[1.0 10.0]", v.ToString()); - v = Between(1, 10).InclusiveMax(false); - Assert.Equal("[1.0 (10.0]", v.ToString()); - v = Between(1, 10).InclusiveMin(false); - Assert.Equal("[(1.0 10.0]", v.ToString()); - - // le, gt, etc. - Assert.Equal("[42.0 42.0]", Equal(42).ToString()); - Assert.Equal("[-inf (42.0]", LessThan(42).ToString()); - Assert.Equal("[-inf 42.0]", LessThanOrEqual(42).ToString()); - Assert.Equal("[(42.0 inf]", GreaterThan(42).ToString()); - Assert.Equal("[42.0 inf]", GreaterThanOrEqual(42).ToString()); - - // string value - Assert.Equal("s", Value("s").ToString()); - - // Geo value - Assert.Equal("[1.0 2.0 3.0 km]", - new GeoValue(1.0, 2.0, 3.0, GeoUnit.Kilometers).ToString()); - } - - [Fact] - public void TestIntersectionBasic() - { - INode n = Intersect().Add("name", "mark"); - Assert.Equal("@name:mark", n.ToString()); - - n = Intersect().Add("name", "mark", "dvir"); - Assert.Equal("@name:(mark dvir)", n.ToString()); - } - - [Fact] - public void TestIntersectionNested() - { - INode n = Intersect(). - Add(Union("name", Value("mark"), Value("dvir"))). - Add("time", Between(100, 200)). - Add(Disjunct("created", LessThan(1000))); - Assert.Equal("(@name:(mark|dvir) @time:[100.0 200.0] -@created:[-inf (1000.0])", n.ToString()); - } - - private static string GetArgsString(AggregationRequest request) - { - var args = new List(); - request.SerializeRedisArgs(args); - return string.Join(" ", args); - } - - [Fact] - public void TestAggregation() - { - Assert.Equal("*", GetArgsString(new AggregationRequest())); - AggregationRequest r = new AggregationRequest(). - GroupBy("@actor", Count().As("cnt")). - SortBy(Descending("@cnt")); - Assert.Equal("* GROUPBY 1 @actor REDUCE COUNT 0 AS cnt SORTBY 2 @cnt DESC", GetArgsString(r)); - - r = new AggregationRequest().GroupBy("@brand", - Quantile("@price", 0.50).As("q50"), - Quantile("@price", 0.90).As("q90"), - Quantile("@price", 0.95).As("q95"), - Avg("@price"), - Count().As("count")). - SortByDescending("@count"). - Limit(10); - Assert.Equal("* GROUPBY 1 @brand REDUCE QUANTILE 2 @price 0.5 AS q50 REDUCE QUANTILE 2 @price 0.9 AS q90 REDUCE QUANTILE 2 @price 0.95 AS q95 REDUCE AVG 1 @price REDUCE COUNT 0 AS count LIMIT 0 10 SORTBY 2 @count DESC", - GetArgsString(r)); - } - - [Fact] - public void TestAggregationBuilder() - { - Assert.Equal("*", new AggregationBuilder().GetArgsString()); - - AggregationBuilder r1 = new AggregationBuilder() - .GroupBy("@actor", Count().As("cnt")) - .SortBy(Descending("@cnt")); - - Assert.Equal("* GROUPBY 1 @actor REDUCE COUNT 0 AS cnt SORTBY 2 @cnt DESC", r1.GetArgsString()); - - Group group = new Group("@brand") - .Reduce(Quantile("@price", 0.50).As("q50")) - .Reduce(Quantile("@price", 0.90).As("q90")) - .Reduce(Quantile("@price", 0.95).As("q95")) - .Reduce(Avg("@price")) - .Reduce(Count().As("count")); - AggregationBuilder r2 = new AggregationBuilder() - .GroupBy(group) - .Limit(10) - .SortByDescending("@count"); - - Assert.Equal("* GROUPBY 1 @brand REDUCE QUANTILE 2 @price 0.5 AS q50 REDUCE QUANTILE 2 @price 0.9 AS q90 REDUCE QUANTILE 2 @price 0.95 AS q95 REDUCE AVG 1 @price REDUCE COUNT 0 AS count LIMIT 0 10 SORTBY 2 @count DESC", - r2.GetArgsString()); - - AggregationBuilder r3 = new AggregationBuilder() - .Load("@count") - .Apply("@count%1000", "thousands") - .SortBy(Descending("@count")) - .Limit(0, 2); - Assert.Equal("* LOAD 1 @count APPLY @count%1000 AS thousands SORTBY 2 @count DESC LIMIT 0 2", r3.GetArgsString()); - } - } -} diff --git a/tests/NRediSearch.Test/QueryTest.cs b/tests/NRediSearch.Test/QueryTest.cs deleted file mode 100644 index 59ed64b6b..000000000 --- a/tests/NRediSearch.Test/QueryTest.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System.Collections.Generic; -using Xunit; - -namespace NRediSearch.Test -{ - public class QueryTest - { - public static Query GetQuery() => new Query("hello world"); - - [Fact] - public void GetNoContent() - { - var query = GetQuery(); - Assert.False(query.NoContent); - Assert.Same(query, query.SetNoContent()); - Assert.True(query.NoContent); - } - - [Fact] - public void GetWithScores() - { - var query = GetQuery(); - Assert.False(query.WithScores); - Assert.Same(query, query.SetWithScores()); - Assert.True(query.WithScores); - } - - [Fact] - public void SerializeRedisArgs() - { - var query = new Query("hello world") - { - NoContent = true, - Language = "", - NoStopwords = true, - Verbatim = true, - WithPayloads = true, - WithScores = true, - Scoring = "TFIDF.DOCNORM", - ExplainScore = true - }; - - var args = new List(); - query.SerializeRedisArgs(args); - - Assert.Equal(11, args.Count); - Assert.Equal(query.QueryString, (string)args[0]); - Assert.Contains("NOCONTENT".Literal(), args); - Assert.Contains("NOSTOPWORDS".Literal(), args); - Assert.Contains("VERBATIM".Literal(), args); - Assert.Contains("WITHPAYLOADS".Literal(), args); - Assert.Contains("WITHSCORES".Literal(), args); - Assert.Contains("LANGUAGE".Literal(), args); - Assert.Contains("", args); - Assert.Contains("SCORER".Literal(), args); - Assert.Contains("TFIDF.DOCNORM", args); - Assert.Contains("EXPLAINSCORE".Literal(), args); - - var languageIndex = args.IndexOf("LANGUAGE".Literal()); - Assert.Equal("", args[languageIndex + 1]); - - var scoringIndex = args.IndexOf("SCORER".Literal()); - Assert.Equal("TFIDF.DOCNORM", args[scoringIndex + 1]); - } - - [Fact] - public void Limit() - { - var query = GetQuery(); - Assert.Equal(0, query._paging.Offset); - Assert.Equal(10, query._paging.Count); - Assert.Same(query, query.Limit(1, 30)); - Assert.Equal(1, query._paging.Offset); - Assert.Equal(30, query._paging.Count); - } - - [Fact] - public void AddFilter() - { - var query = GetQuery(); - Assert.Empty(query._filters); - Query.NumericFilter f = new Query.NumericFilter("foo", 0, 100); - Assert.Same(query, query.AddFilter(f)); - Assert.Same(f, query._filters[0]); - } - - [Fact] - public void SetVerbatim() - { - var query = GetQuery(); - Assert.False(query.Verbatim); - Assert.Same(query, query.SetVerbatim()); - Assert.True(query.Verbatim); - } - - [Fact] - public void SetNoStopwords() - { - var query = GetQuery(); - Assert.False(query.NoStopwords); - Assert.Same(query, query.SetNoStopwords()); - Assert.True(query.NoStopwords); - } - - [Fact] - public void SetLanguage() - { - var query = GetQuery(); - Assert.Null(query.Language); - Assert.Same(query, query.SetLanguage("chinese")); - Assert.Equal("chinese", query.Language); - } - - [Fact] - public void LimitFields() - { - var query = GetQuery(); - Assert.Null(query._fields); - Assert.Same(query, query.LimitFields("foo", "bar")); - Assert.Equal(2, query._fields.Length); - } - - [Fact] - public void ReturnFields() - { - var query = GetQuery(); - - Assert.Null(query._returnFields); - Assert.Same(query, query.ReturnFields("foo", "bar")); - Assert.Equal(2, query._returnFields.Length); - } - - - [Fact] - public void ReturnFieldNames() - { - var query = GetQuery(); - - Assert.Null(query._returnFieldsNames); - Assert.Same(query, query.ReturnFields(FieldName.Of("foo").As("bar"), FieldName.Of("foofoo"))); - Assert.Equal(2, query._returnFieldsNames.Length); - } - - [Fact] - public void HighlightFields() - { - var query = GetQuery(); - Assert.False(query._wantsHighlight); - Assert.Null(query._highlightFields); - - query = new Query("Hello"); - Assert.Same(query, query.HighlightFields("foo", "bar")); - Assert.Equal(2, query._highlightFields.Length); - Assert.Null(query._highlightTags); - Assert.True(query._wantsHighlight); - - query = new Query("Hello").HighlightFields(); - Assert.Null(query._highlightFields); - Assert.Null(query._highlightTags); - Assert.True(query._wantsHighlight); - - Assert.Same(query, query.HighlightFields(new Query.HighlightTags("", ""))); - Assert.Null(query._highlightFields); - Assert.NotNull(query._highlightTags); - Assert.Equal("", query._highlightTags.Value.Open); - Assert.Equal("", query._highlightTags.Value.Close); - } - - [Fact] - public void SummarizeFields() - { - var query = GetQuery(); - Assert.False(query._wantsSummarize); - Assert.Null(query._summarizeFields); - - query = new Query("Hello"); - Assert.Equal(query, query.SummarizeFields()); - Assert.True(query._wantsSummarize); - Assert.Null(query._summarizeFields); - Assert.Equal(-1, query._summarizeFragmentLen); - Assert.Equal(-1, query._summarizeNumFragments); - - query = new Query("Hello"); - Assert.Equal(query, query.SummarizeFields("someField")); - Assert.True(query._wantsSummarize); - Assert.Single(query._summarizeFields); - Assert.Equal(-1, query._summarizeFragmentLen); - Assert.Equal(-1, query._summarizeNumFragments); - } - - [Fact] - public void SetScoring() - { - var query = GetQuery(); - Assert.Null(query.Scoring); - Assert.Same(query, query.SetScoring("TFIDF.DOCNORM")); - Assert.Equal("TFIDF.DOCNORM", query.Scoring); - } - } -} diff --git a/tests/NRediSearch.Test/RediSearchTestBase.cs b/tests/NRediSearch.Test/RediSearchTestBase.cs deleted file mode 100644 index ede002f1e..000000000 --- a/tests/NRediSearch.Test/RediSearchTestBase.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using StackExchange.Redis; -using StackExchange.Redis.Tests; -using Xunit; -using Xunit.Abstractions; - -namespace NRediSearch.Test -{ - [Collection(nameof(NonParallelCollection))] - public abstract class RediSearchTestBase : IDisposable - { - protected readonly ITestOutputHelper Output; - protected RediSearchTestBase(ITestOutputHelper output) - { - muxer = GetWithFT(output); - Output = output; - Db = muxer.GetDatabase(); - var server = muxer.GetServer(muxer.GetEndPoints()[0]); - server.FlushDatabase(); - } - private ConnectionMultiplexer muxer; - protected IDatabase Db { get; private set; } - - public void Dispose() - { - muxer?.Dispose(); - muxer = null; - Db = null; - } - - protected Client GetClient([CallerFilePath] string filePath = null, [CallerMemberName] string caller = null) - { - // Remove all that extra pathing - var offset = filePath?.IndexOf("NRediSearch.Test"); - if (offset > -1) - { - filePath = filePath.Substring(offset.Value + "NRediSearch.Test".Length + 1); - } - - var indexName = $"{filePath}:{caller}"; - Output.WriteLine("Using Index: " + indexName); - var exists = Db.KeyExists("idx:" + indexName); - Output.WriteLine("Key existed: " + exists); - - var client = new Client(indexName, Db); - var wasReset = Reset(client); - Output.WriteLine("Index was reset?: " + wasReset); - return client; - } - - protected bool Reset(Client client) - { - Output.WriteLine("Resetting index"); - try - { - var result = client.DropIndex(); // tests create them - Output.WriteLine(" Result: " + result); - return result; - } - catch (RedisServerException ex) - { - if (string.Equals("Unknown Index name", ex.Message, StringComparison.InvariantCultureIgnoreCase)) - { - Output.WriteLine(" Unknown index name"); - return true; - } - if (string.Equals("no such index", ex.Message, StringComparison.InvariantCultureIgnoreCase)) - { - Output.WriteLine(" No such index"); - return true; - } - else - { - throw; - } - } - } - - private static bool instanceMissing; - - internal static ConnectionMultiplexer GetWithFT(ITestOutputHelper output) - { - var options = new ConfigurationOptions - { - EndPoints = { TestConfig.Current.RediSearchServerAndPort }, - AllowAdmin = true, - ConnectTimeout = 2000, - SyncTimeout = 15000, - }; - static void InstanceMissing() => Skip.Inconclusive("NRedisSearch instance available at " + TestConfig.Current.RediSearchServerAndPort); - // Don't timeout every single test - optimization - if (instanceMissing) - { - InstanceMissing(); - } - - ConnectionMultiplexer conn = null; - try - { - conn = ConnectionMultiplexer.Connect(options); - conn.MessageFaulted += (msg, ex, origin) => output.WriteLine($"Faulted from '{origin}': '{msg}' - '{(ex == null ? "(null)" : ex.Message)}'"); - conn.Connecting += (e, t) => output.WriteLine($"Connecting to {Format.ToString(e)} as {t}"); - conn.Closing += complete => output.WriteLine(complete ? "Closed" : "Closing..."); - } - catch (RedisConnectionException) - { - instanceMissing = true; - InstanceMissing(); - } - - // If say we're on a 3.x Redis server...bomb out. - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Module), r => r.Module); - - var server = conn.GetServer(TestConfig.Current.RediSearchServerAndPort); - var arr = (RedisResult[])server.Execute("module", "list"); - bool found = false; - foreach (var module in arr) - { - var parsed = Parse(module); - if (parsed.TryGetValue("name", out var val) && (val == "ft" || val == "search")) - { - found = true; - if (parsed.TryGetValue("ver", out val)) - output?.WriteLine($"Version: {val}"); - break; - } - } - - if (!found) - { - output?.WriteLine("Module not found."); - throw new RedisException("NRedisSearch module missing on " + TestConfig.Current.RediSearchServerAndPort); - } - return conn; - } - - private static Dictionary Parse(RedisResult module) - { - var data = new Dictionary(); - var lines = (RedisResult[])module; - for (int i = 0; i < lines.Length;) - { - var key = (string)lines[i++]; - var value = (RedisValue)lines[i++]; - data[key] = value; - } - return data; - } - - protected bool IsMissingIndexException(Exception ex) - { - if (ex.Message == null) - { - return false; - } - return ex.Message.Contains("Unknown Index name", StringComparison.InvariantCultureIgnoreCase) - || ex.Message.Contains("no such index", StringComparison.InvariantCultureIgnoreCase); - } - } - - [CollectionDefinition(nameof(NonParallelCollection), DisableParallelization = true)] - public class NonParallelCollection { } -} diff --git a/tests/RedisConfigs/docker-compose.yml b/tests/RedisConfigs/docker-compose.yml index 79dcabdf3..6475e6664 100644 --- a/tests/RedisConfigs/docker-compose.yml +++ b/tests/RedisConfigs/docker-compose.yml @@ -1,10 +1,6 @@ version: '2.5' services: - redisearch: - image: redislabs/redisearch:latest - ports: - - 6385:6379 redis: build: context: . diff --git a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs index ec6c21ee4..07cbc6e58 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs @@ -69,10 +69,6 @@ public class Config public int FailoverReplicaPort { get; set; } = 6383; public string FailoverReplicaServerAndPort => FailoverReplicaServer + ":" + FailoverReplicaPort.ToString(); - public string RediSearchServer { get; set; } = "127.0.0.1"; - public int RediSearchPort { get; set; } = 6385; - public string RediSearchServerAndPort => RediSearchServer + ":" + RediSearchPort.ToString(); - public string IPv4Server { get; set; } = "127.0.0.1"; public int IPv4Port { get; set; } = 6379; public string IPv6Server { get; set; } = "::1"; From 9d18eb04abf534678e238b1e4d50184517e5fac3 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 2 Nov 2021 07:49:40 -0400 Subject: [PATCH 033/435] Brings toys up to date to eliminate netcoreapp2.1 errors. (#1899) Fixing build bits, quick PR. --- toys/KestrelRedisServer/KestrelRedisServer.csproj | 5 +---- toys/KestrelRedisServer/Program.cs | 5 +++-- toys/KestrelRedisServer/Startup.cs | 5 +++-- toys/TestConsole/TestConsole.csproj | 2 +- toys/TestConsoleBaseline/TestConsoleBaseline.csproj | 2 +- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/toys/KestrelRedisServer/KestrelRedisServer.csproj b/toys/KestrelRedisServer/KestrelRedisServer.csproj index bf3c1a3f1..5f0d6287d 100644 --- a/toys/KestrelRedisServer/KestrelRedisServer.csproj +++ b/toys/KestrelRedisServer/KestrelRedisServer.csproj @@ -1,15 +1,12 @@  - netcoreapp2.1 + net5.0 $(NoWarn);CS1591 - - - diff --git a/toys/KestrelRedisServer/Program.cs b/toys/KestrelRedisServer/Program.cs index c022f9480..3695b3b95 100644 --- a/toys/KestrelRedisServer/Program.cs +++ b/toys/KestrelRedisServer/Program.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; namespace KestrelRedisServer { @@ -16,7 +15,9 @@ public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseKestrel(options => { - options.ApplicationSchedulingMode = SchedulingMode.Inline; + // Moved to SocketTransportOptions.UnsafePreferInlineScheduling = true; + //options.ApplicationSchedulingMode = SchedulingMode.Inline; + // HTTP 5000 options.ListenLocalhost(5000); diff --git a/toys/KestrelRedisServer/Startup.cs b/toys/KestrelRedisServer/Startup.cs index ab3a52382..8d7d43f38 100644 --- a/toys/KestrelRedisServer/Startup.cs +++ b/toys/KestrelRedisServer/Startup.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using StackExchange.Redis.Server; namespace KestrelRedisServer @@ -19,7 +20,7 @@ public void ConfigureServices(IServiceCollection services) public void Dispose() => _server.Dispose(); // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime lifetime) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime) { _server.Shutdown.ContinueWith((t, s) => { @@ -27,7 +28,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplica { // if the resp server is shutdown by a client: stop the kestrel server too if (t.Result == RespServer.ShutdownReason.ClientInitiated) { - ((IApplicationLifetime)s).StopApplication(); + ((IHostApplicationLifetime)s).StopApplication(); } } catch { /* Don't go boom on shutdown */ } diff --git a/toys/TestConsole/TestConsole.csproj b/toys/TestConsole/TestConsole.csproj index 76826ee4e..0361fe4d6 100644 --- a/toys/TestConsole/TestConsole.csproj +++ b/toys/TestConsole/TestConsole.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1;net472 + net50;net472 SEV2 true diff --git a/toys/TestConsoleBaseline/TestConsoleBaseline.csproj b/toys/TestConsoleBaseline/TestConsoleBaseline.csproj index 66fa8a8be..a9d0162e7 100644 --- a/toys/TestConsoleBaseline/TestConsoleBaseline.csproj +++ b/toys/TestConsoleBaseline/TestConsoleBaseline.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1;net461;net462;net47;net472 + net50;net461;net462;net47;net472 From 27339cdb96e445226e874c223af989a32aed02a5 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 2 Nov 2021 07:51:43 -0400 Subject: [PATCH 034/435] Bridge & Connection stats: cleanup (#1898) We got a little heavy with stats over the years and it's cumbersome to add anything here (e.g. another `out` parameter). Instead of adding yet more stats in upcoming bits as-is, I decided to take a stab at simplifying this with `readonly struct` passes. We're not _super_ concerned with efficiency in the exception path but hey, why not. This should simplify maintenance/additions and clarify what each property is. Note that we have some static defaults here because `default` does _not_ run property initializers where a `new()` does. Includes a bump to C# 9 to get all the struct features in play. I tried C# 10 but the build agents didn't eat breakfast and were rather peeved I suggested it. --- Directory.Build.props | 2 +- .../ConnectionMultiplexer.cs | 5 +- src/StackExchange.Redis/ExceptionFactory.cs | 34 ++++--- src/StackExchange.Redis/Hacks.cs | 12 +++ src/StackExchange.Redis/PhysicalBridge.cs | 58 +++++++---- src/StackExchange.Redis/PhysicalConnection.cs | 97 ++++++++++++++++--- src/StackExchange.Redis/ServerEndPoint.cs | 16 +-- 7 files changed, 161 insertions(+), 63 deletions(-) create mode 100644 src/StackExchange.Redis/Hacks.cs diff --git a/Directory.Build.props b/Directory.Build.props index d43bc25dd..6227de316 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,7 +15,7 @@ https://github.com/StackExchange/StackExchange.Redis/ MIT - 8.0 + 9.0 git https://github.com/StackExchange/StackExchange.Redis/ diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 58e6252d3..85803df2c 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1779,8 +1779,9 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP { var server = servers[i]; var task = available[i]; - server.GetOutstandingCount(RedisCommand.PING, out int inst, out int qs, out long @in, out int qu, out bool aw, out long toRead, out long toWrite, out var bs, out var rs, out var ws); - log?.WriteLine($" Server[{i}] ({Format.ToString(server)}) Status: {task.Status} (inst: {inst}, qs: {qs}, in: {@in}, qu: {qu}, aw: {aw}, in-pipe: {toRead}, out-pipe: {toWrite}, bw: {bs}, rs: {rs}. ws: {ws})"); + var bs = server.GetBridgeStatus(RedisCommand.PING); + + log?.WriteLine($" Server[{i}] ({Format.ToString(server)}) Status: {task.Status} (inst: {bs.MessagesSinceLastHeartbeat}, qs: {bs.Connection.MessagesSentAwaitingResponse}, in: {bs.Connection.BytesAvailableOnSocket}, qu: {bs.MessagesSinceLastHeartbeat}, aw: {bs.IsWriterActive}, in-pipe: {bs.Connection.BytesInReadPipe}, out-pipe: {bs.Connection.BytesInWritePipe}, bw: {bs.BacklogStatus}, rs: {bs.Connection.ReadStatus}. ws: {bs.Connection.WriteStatus})"); } } diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 661ce29b8..4cc274d24 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -312,27 +312,31 @@ ServerEndPoint server // Add server data, if we have it if (server != null && message != null) { - server.GetOutstandingCount(message.Command, out int inst, out int qs, out long @in, out int qu, out bool aw, out long toRead, out long toWrite, out var bs, out var rs, out var ws); - switch (rs) + var bs = server.GetBridgeStatus(message.Command); + + switch (bs.Connection.ReadStatus) { case PhysicalConnection.ReadStatus.CompletePendingMessageAsync: case PhysicalConnection.ReadStatus.CompletePendingMessageSync: sb.Append(" ** possible thread-theft indicated; see https://stackexchange.github.io/StackExchange.Redis/ThreadTheft ** "); break; } - Add(data, sb, "OpsSinceLastHeartbeat", "inst", inst.ToString()); - Add(data, sb, "Queue-Awaiting-Write", "qu", qu.ToString()); - Add(data, sb, "Queue-Awaiting-Response", "qs", qs.ToString()); - Add(data, sb, "Active-Writer", "aw", aw.ToString()); - if (qu != 0) Add(data, sb, "Backlog-Writer", "bw", bs.ToString()); - if (rs != PhysicalConnection.ReadStatus.NA) Add(data, sb, "Read-State", "rs", rs.ToString()); - if (ws != PhysicalConnection.WriteStatus.NA) Add(data, sb, "Write-State", "ws", ws.ToString()); - - if (@in >= 0) Add(data, sb, "Inbound-Bytes", "in", @in.ToString()); - if (toRead >= 0) Add(data, sb, "Inbound-Pipe-Bytes", "in-pipe", toRead.ToString()); - if (toWrite >= 0) Add(data, sb, "Outbound-Pipe-Bytes", "out-pipe", toWrite.ToString()); - - if (multiplexer.StormLogThreshold >= 0 && qs >= multiplexer.StormLogThreshold && Interlocked.CompareExchange(ref multiplexer.haveStormLog, 1, 0) == 0) + Add(data, sb, "OpsSinceLastHeartbeat", "inst", bs.MessagesSinceLastHeartbeat.ToString()); + Add(data, sb, "Queue-Awaiting-Write", "qu", bs.BacklogMessagesPending.ToString()); + Add(data, sb, "Queue-Awaiting-Response", "qs", bs.Connection.MessagesSentAwaitingResponse.ToString()); + Add(data, sb, "Active-Writer", "aw", bs.IsWriterActive.ToString()); + if (bs.BacklogMessagesPending != 0) + { + Add(data, sb, "Backlog-Writer", "bw", bs.BacklogStatus.ToString()); + } + if (bs.Connection.ReadStatus != PhysicalConnection.ReadStatus.NA) Add(data, sb, "Read-State", "rs", bs.Connection.ReadStatus.ToString()); + if (bs.Connection.WriteStatus != PhysicalConnection.WriteStatus.NA) Add(data, sb, "Write-State", "ws", bs.Connection.WriteStatus.ToString()); + + if (bs.Connection.BytesAvailableOnSocket >= 0) Add(data, sb, "Inbound-Bytes", "in", bs.Connection.BytesAvailableOnSocket.ToString()); + if (bs.Connection.BytesInReadPipe >= 0) Add(data, sb, "Inbound-Pipe-Bytes", "in-pipe", bs.Connection.BytesInReadPipe.ToString()); + if (bs.Connection.BytesInWritePipe >= 0) Add(data, sb, "Outbound-Pipe-Bytes", "out-pipe", bs.Connection.BytesInWritePipe.ToString()); + + if (multiplexer.StormLogThreshold >= 0 && bs.Connection.MessagesSentAwaitingResponse >= multiplexer.StormLogThreshold && Interlocked.CompareExchange(ref multiplexer.haveStormLog, 1, 0) == 0) { var log = server.GetStormLog(message.Command); if (string.IsNullOrWhiteSpace(log)) Interlocked.Exchange(ref multiplexer.haveStormLog, 0); diff --git a/src/StackExchange.Redis/Hacks.cs b/src/StackExchange.Redis/Hacks.cs new file mode 100644 index 000000000..411a796d5 --- /dev/null +++ b/src/StackExchange.Redis/Hacks.cs @@ -0,0 +1,12 @@ +#if !NET5_0_OR_GREATER + +// To support { get; init; } properties +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit { } +} + +#endif diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 4fe86900d..22260abee 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -281,30 +281,46 @@ private async Task ExecuteSubscriptionLoop() // pushes items that have been enqu internal bool TryEnqueueBackgroundSubscriptionWrite(in PendingSubscriptionState state) => !isDisposed && (_subscriptionBackgroundQueue ?? GetSubscriptionQueue()).Writer.TryWrite(state); - internal void GetOutstandingCount(out int inst, out int qs, out long @in, out int qu, out bool aw, out long toRead, out long toWrite, - out BacklogStatus bs, out PhysicalConnection.ReadStatus rs, out PhysicalConnection.WriteStatus ws) + internal readonly struct BridgeStatus { - inst = (int)(Interlocked.Read(ref operationCount) - Interlocked.Read(ref profileLastLog)); - qu = _backlog.Count; - aw = !_singleWriterMutex.IsAvailable; - bs = _backlogStatus; - var tmp = physical; - if (tmp == null) - { - qs = 0; - toRead = toWrite = @in = -1; - rs = PhysicalConnection.ReadStatus.NA; - ws = PhysicalConnection.WriteStatus.NA; - } - else - { - qs = tmp.GetSentAwaitingResponseCount(); - @in = tmp.GetSocketBytes(out toRead, out toWrite); - rs = tmp.GetReadStatus(); - ws = tmp.GetWriteStatus(); - } + /// + /// Number of messages sent since the last heartbeat was processed. + /// + public int MessagesSinceLastHeartbeat { get; init; } + /// + /// Whether the pipe writer is currently active. + /// + public bool IsWriterActive { get; init; } + + /// + /// Total number of backlog messages that are in the retry backlog. + /// + public int BacklogMessagesPending { get; init; } + /// + /// Status of the currently processing backlog, if any. + /// + public BacklogStatus BacklogStatus { get; init; } + + /// + /// Status for the underlying . + /// + public PhysicalConnection.ConnectionStatus Connection { get; init; } + + /// + /// The default bridge stats, notable *not* the same as default since initializers don't run. + /// + public static BridgeStatus Zero { get; } = new() { Connection = PhysicalConnection.ConnectionStatus.Zero }; } + internal BridgeStatus GetStatus() => new() + { + MessagesSinceLastHeartbeat = (int)(Interlocked.Read(ref operationCount) - Interlocked.Read(ref profileLastLog)), + IsWriterActive = !_singleWriterMutex.IsAvailable, + BacklogMessagesPending = _backlog.Count, + BacklogStatus = _backlogStatus, + Connection = physical?.GetStatus() ?? PhysicalConnection.ConnectionStatus.Default, + }; + internal string GetStormLog() { var sb = new StringBuilder("Storm log for ").Append(Format.ToString(ServerEndPoint.EndPoint)).Append(" / ").Append(ConnectionType) diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 9c2672eaa..7b0096edc 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -330,7 +330,7 @@ public void RecordConnectionFailed(ConnectionFailureType failureType, Exception // stop anything new coming in... bridge?.Trace("Failed: " + failureType); - long @in = -1, @toRead = -1, @toWrite = -1; + ConnectionStatus connStatus = ConnectionStatus.Default; PhysicalBridge.State oldState = PhysicalBridge.State.Disconnected; bool isCurrent = false; bridge?.OnDisconnected(failureType, this, out isCurrent, out oldState); @@ -338,7 +338,7 @@ public void RecordConnectionFailed(ConnectionFailureType failureType, Exception { try { - @in = GetSocketBytes(out toRead, out toWrite); + connStatus = GetStatus(); } catch { /* best effort only */ } } @@ -408,9 +408,9 @@ void add(string lk, string sk, string v) add("Keep-Alive", "keep-alive", bridge.ServerEndPoint?.WriteEverySeconds + "s"); add("Previous-Physical-State", "state", oldState.ToString()); add("Manager", "mgr", bridge.Multiplexer.SocketManager?.GetState()); - if (@in >= 0) add("Inbound-Bytes", "in", @in.ToString()); - if (toRead >= 0) add("Inbound-Pipe-Bytes", "in-pipe", toRead.ToString()); - if (toWrite >= 0) add("Outbound-Pipe-Bytes", "out-pipe", toWrite.ToString()); + if (connStatus.BytesAvailableOnSocket >= 0) add("Inbound-Bytes", "in", connStatus.BytesAvailableOnSocket.ToString()); + if (connStatus.BytesInReadPipe >= 0) add("Inbound-Pipe-Bytes", "in-pipe", connStatus.BytesInReadPipe.ToString()); + if (connStatus.BytesInWritePipe >= 0) add("Outbound-Pipe-Bytes", "out-pipe", connStatus.BytesInWritePipe.ToString()); add("Last-Heartbeat", "last-heartbeat", (lastBeat == 0 ? "never" : ((unchecked(now - lastBeat) / 1000) + "s ago")) + (BridgeCouldBeNull.IsBeating ? " (mid-beat)" : "")); var mbeat = bridge.Multiplexer.LastHeartbeatSecondsAgo; @@ -1266,25 +1266,96 @@ internal static void WriteInteger(PipeWriter writer, long value) writer.Advance(bytes); } - internal long GetSocketBytes(out long readCount, out long writeCount) + internal readonly struct ConnectionStatus + { + /// + /// Number of messages sent outbound, but we don't yet have a response for. + /// + public int MessagesSentAwaitingResponse { get; init; } + + /// + /// Bytes available on the socket, not yet read into the pipe. + /// + public long BytesAvailableOnSocket { get; init; } + /// + /// Bytes read from the socket, pending in the reader pipe. + /// + public long BytesInReadPipe { get; init; } + /// + /// Bytes in the writer pipe, waiting to be written to the socket. + /// + public long BytesInWritePipe { get; init; } + + /// + /// The inbound pipe reader status. + /// + public ReadStatus ReadStatus { get; init; } + /// + /// The outbound pipe writer status. + /// + public WriteStatus WriteStatus { get; init; } + + /// + /// The default connection stats, notable *not* the same as default since initializers don't run. + /// + public static ConnectionStatus Default { get; } = new() + { + BytesAvailableOnSocket = -1, + BytesInReadPipe = -1, + BytesInWritePipe = -1, + ReadStatus = ReadStatus.NA, + WriteStatus = WriteStatus.NA, + }; + + /// + /// The zeroed connection stats, which we want to display as zero for default exception cases. + /// + public static ConnectionStatus Zero { get; } = new() + { + BytesAvailableOnSocket = 0, + BytesInReadPipe = 0, + BytesInWritePipe = 0, + ReadStatus = ReadStatus.NA, + WriteStatus = WriteStatus.NA, + }; + } + + public ConnectionStatus GetStatus() { if (_ioPipe is SocketConnection conn) { var counters = conn.GetCounters(); - readCount = counters.BytesWaitingToBeRead; - writeCount = counters.BytesWaitingToBeSent; - return counters.BytesAvailableOnSocket; + return new ConnectionStatus() + { + MessagesSentAwaitingResponse = GetSentAwaitingResponseCount(), + BytesAvailableOnSocket = counters.BytesAvailableOnSocket, + BytesInReadPipe = counters.BytesWaitingToBeRead, + BytesInWritePipe = counters.BytesWaitingToBeSent, + ReadStatus = _readStatus, + WriteStatus = _writeStatus, + }; } - readCount = writeCount = -1; + + // Fall back to bytes waiting on the socket if we can + int fallbackBytesAvailable; try - { - return VolatileSocket?.Available ?? -1; + { + fallbackBytesAvailable = VolatileSocket?.Available ?? -1; } catch { // If this fails, we're likely in a race disposal situation and do not want to blow sky high here. - return -1; + fallbackBytesAvailable = -1; } + + return new ConnectionStatus() + { + BytesAvailableOnSocket = fallbackBytesAvailable, + BytesInReadPipe = -1, + BytesInWritePipe = -1, + ReadStatus = _readStatus, + WriteStatus = _writeStatus, + }; } private static RemoteCertificateValidationCallback GetAmbientIssuerCertificateCallback() diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index f6050d4e5..a76f5ca3e 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -434,24 +434,18 @@ internal ServerCounters GetCounters() return counters; } - internal void GetOutstandingCount(RedisCommand command, out int inst, out int qs, out long @in, out int qu, out bool aw, out long toRead, out long toWrite, - out BacklogStatus bs, out PhysicalConnection.ReadStatus rs, out PhysicalConnection.WriteStatus ws) - { - inst = qs = qu = 0; - @in = toRead = toWrite = 0; - aw = false; - bs = BacklogStatus.Inactive; - rs = PhysicalConnection.ReadStatus.NA; - ws = PhysicalConnection.WriteStatus.NA; + internal BridgeStatus GetBridgeStatus(RedisCommand command) + { try { - var bridge = GetBridge(command, false); - bridge?.GetOutstandingCount(out inst, out qs, out @in, out qu, out aw, out toRead, out toWrite, out bs, out rs, out ws); + return GetBridge(command, false)?.GetStatus() ?? BridgeStatus.Zero; } catch (Exception ex) { // only needs to be best efforts System.Diagnostics.Debug.WriteLine(ex.Message); } + + return BridgeStatus.Zero; } internal string GetProfile() From ef392e9194d976b2fce9b33693dd3cc0b0aae1a0 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 3 Nov 2021 21:08:19 -0400 Subject: [PATCH 035/435] Maintenance events: add NodeMaintenanceScaleComplete (#1902) Recognize the upcoming `NodeMaintenanceScaleComplete` event to handle Redis cluster scaling (either direction). --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs | 2 ++ src/StackExchange.Redis/Maintenance/AzureNotificationType.cs | 5 +++++ .../StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs | 1 + 4 files changed, 9 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 5521a940d..90db06c14 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,6 +1,7 @@ # Release Notes ## Unreleased +- Add support for NodeMaintenanceScaleComplete event (handles Redis cluster scaling) (#1902 via NickCraver) ## 2.2.79 diff --git a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs index cd4ad62c8..bf8b620c5 100644 --- a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs @@ -138,6 +138,7 @@ await sub.SubscribeAsync(PubSubChannelName, async (channel, message) => { case AzureNotificationType.NodeMaintenanceEnded: case AzureNotificationType.NodeMaintenanceFailoverComplete: + case AzureNotificationType.NodeMaintenanceScaleComplete: await multiplexer.ReconfigureAsync(first: false, reconfigureAll: true, log: logProxy, blame: null, cause: $"Azure Event: {newMessage.NotificationType}").ForAwait(); break; } @@ -188,6 +189,7 @@ await sub.SubscribeAsync(PubSubChannelName, async (channel, message) => // This is temporary until server changes go into effect - to be removed in later versions "NodeMaintenanceFailover" => AzureNotificationType.NodeMaintenanceFailoverComplete, "NodeMaintenanceFailoverComplete" => AzureNotificationType.NodeMaintenanceFailoverComplete, + "NodeMaintenanceScaleComplete" => AzureNotificationType.NodeMaintenanceScaleComplete, _ => AzureNotificationType.Unknown, }; } diff --git a/src/StackExchange.Redis/Maintenance/AzureNotificationType.cs b/src/StackExchange.Redis/Maintenance/AzureNotificationType.cs index 06ed65e5b..13237f914 100644 --- a/src/StackExchange.Redis/Maintenance/AzureNotificationType.cs +++ b/src/StackExchange.Redis/Maintenance/AzureNotificationType.cs @@ -34,5 +34,10 @@ public enum AzureNotificationType /// Indicates that a replica has been promoted to primary. /// NodeMaintenanceFailoverComplete, + + /// + /// Indicates that a scale event (adding or removing nodes) has completed for a cluster. + /// + NodeMaintenanceScaleComplete, } } diff --git a/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs b/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs index 7ca5fea1a..bd4b966f9 100644 --- a/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs +++ b/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs @@ -20,6 +20,7 @@ public AzureMaintenanceEventTests(ITestOutputHelper output) : base(output) [InlineData("NotificationType|NodeMaintenanceStarting|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|j|IPAddress||SSLPort|char|NonSSLPort|char", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, null, 0, 0)] [InlineData("NotificationType|NodeMaintenanceStarting|somejunkkey|somejunkvalue|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress||SSLPort|15999|NonSSLPort|139991", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, null, 15999, 139991)] [InlineData("NotificationType|NodeMaintenanceStarting|somejunkkey|somejunkvalue|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress|127.0.0.1|SSLPort|15999|NonSSLPort|139991", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, "127.0.0.1", 15999, 139991)] + [InlineData("NotificationType|NodeMaintenanceScaleComplete|somejunkkey|somejunkvalue|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress|127.0.0.1|SSLPort|15999|NonSSLPort|139991", AzureNotificationType.NodeMaintenanceScaleComplete, "2021-03-02T23:26:57", false, "127.0.0.1", 15999, 139991)] [InlineData("NotificationTypeNodeMaintenanceStartingsomejunkkeysomejunkvalueStartTimeInUTC2021-03-02T23:26:57IsReplicaFalseIPAddress127.0.0.1SSLPort15999NonSSLPort139991", AzureNotificationType.Unknown, null, false, null, 0, 0)] [InlineData("NotificationType|", AzureNotificationType.Unknown, null, false, null, 0, 0)] [InlineData("NotificationType|NodeMaintenanceStarting1", AzureNotificationType.Unknown, null, false, null, 0, 0)] From ccb5f42029f3b276cebc35ab8c87f93a38487137 Mon Sep 17 00:00:00 2001 From: Michelle Date: Thu, 4 Nov 2021 01:38:20 +0000 Subject: [PATCH 036/435] Adding doc for ServerMaintenanceEvent (#1894) Documentation for the feature introduced here: #1876 --- docs/ServerMaintenanceEvent.md | 67 ++++++++++++++++++++++++++++++++++ docs/index.md | 1 + 2 files changed, 68 insertions(+) create mode 100644 docs/ServerMaintenanceEvent.md diff --git a/docs/ServerMaintenanceEvent.md b/docs/ServerMaintenanceEvent.md new file mode 100644 index 000000000..dbc3528ad --- /dev/null +++ b/docs/ServerMaintenanceEvent.md @@ -0,0 +1,67 @@ +# Introducing ServerMaintenanceEvents + +StackExchange.Redis now automatically subscribes to notifications about upcoming maintenance from supported Redis providers. The ServerMaintenanceEvent on the ConnectionMultiplexer raises events in response to notifications about server maintenance, and application code can subscribe to the event to handle connection drops more gracefully during these maintenance operations. + +If you are a Redis vendor and want to integrate support for ServerMaintenanceEvents into StackExchange.Redis, we recommend opening an issue so we can discuss the details. + +## Types of events + +Azure Cache for Redis currently sends the following notifications: +* `NodeMaintenanceScheduled`: Indicates that a maintenance event is scheduled. Can be 10-15 minutes in advance. +* `NodeMaintenanceStarting`: This event gets fired ~20s before maintenance begins +* `NodeMaintenanceStart`: This event gets fired when maintenance is imminent (<5s) +* `NodeMaintenanceFailoverComplete`: Indicates that a replica has been promoted to primary +* `NodeMaintenanceEnded`: Indicates that the node maintenance operation is over + +## Sample code + +The library will automatically subscribe to the pub/sub channel to receive notifications from the server, if one exists. For Azure Redis caches, this is the 'AzureRedisEvents' channel. To plug in your maintenance handling logic, you can pass in an event handler via the `ServerMaintenanceEvent` event on your `ConnectionMultiplexer`. For example: + +``` +multiplexer.ServerMaintenanceEvent += (object sender, ServerMaintenanceEvent e) => +{ + if (e is AzureMaintenanceEvent azureEvent && azureEvent.NotificationType == AzureNotificationType.NodeMaintenanceStart) + { + // Take whatever action is appropriate for your application to handle the maintenance operation gracefully. + // This might mean writing a log entry, redirecting traffic away from the impacted Redis server, or + // something entirely different. + } +}; +``` +You can see the schema for the `AzureMaintenanceEvent` class [here](https://github.com/StackExchange/StackExchange.Redis/blob/main/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs). Note that the library automatically sets the `ReceivedTimeUtc` timestamp when the event is received, so if you see in your logs that `ReceivedTimeUtc` is after `StartTimeUtc`, this may indicate that your connections are under high load. + +## Walking through a sample maintenance event + +1. App is connected to Redis and everything is working fine. +2. Current Time: [16:21:39] -> `NodeMaintenanceScheduled` event is raised, with a `StartTimeUtc` of 16:35:57 (about 14 minutes from current time). + * Note: the start time for this event is an approximation, because we will start getting ready for the update proactively and the node may become unavailable up to 3 minutes sooner. We recommend listening for `NodeMaintenanceStarting` and `NodeMaintenanceStart` for the highest level of accuracy (these are only likely to differ by a few seconds at most). +3. Current Time: [16:34:26] -> `NodeMaintenanceStarting` message is received, and `StartTimeUtc` is 16:34:46, about 20 seconds from the current time. +4. Current Time: [16:34:46] -> `NodeMaintenanceStart` message is received, so we know the node maintenance is about to happen. We break the circuit and stop sending new operations to the Redis connection. (Note: the appropriate action for your application may be different.) StackExchange.Redis will automatically refresh its view of the overall server topology. +5. Current Time: [16:34:47] -> The connection is closed by the Redis server. +6. Current Time: [16:34:56] -> `NodeMaintenanceFailoverComplete` message is received. This tells us that the replica node has promoted itself to primary, so the other node can go offline for maintenance. +7. Current Time [16:34:56] -> The connection to the Redis server is restored. It is safe to send commands again to the connection and all commands will succeed. +8. Current Time [16:37:48] -> `NodeMaintenanceEnded` message is received, with a `StartTimeUtc` of 16:37:48. Nothing to do here if you are talking to the load balancer endpoint (port 6380 or 6379). For clustered servers, you can resume sending readonly workloads to the replica(s). + +## Azure Cache for Redis Maintenance Event details + +#### NodeMaintenanceScheduled event + +`NodeMaintenanceScheduled` events are raised for maintenance scheduled by Azure, up to 15 minutes in advance. This event will not get fired for user-initiated reboots. + +#### NodeMaintenanceStarting event + +`NodeMaintenanceStarting` events are raised ~20 seconds ahead of upcoming maintenance. This means that one of the primary or replica nodes will be going down for maintenance. + +It's important to understand that this does *not* mean downtime if you are using a Standard/Premier SKU cache. If the replica is targeted for maintenance, disruptions should be minimal. If the primary node is the one going down for maintenance, a failover will occur, which will close existing connections going through the load balancer port (6380/6379) or directly to the node (15000/15001). You may want to pause sending write commands until the replica node has assumed the primary role and the failover is complete. + +#### NodeMaintenanceStart event + +`NodeMaintenanceStart` events are raised when maintenance is imminent (within seconds). These messages do not include a `StartTimeUtc` because they are fired immediately before maintenance occurs. + +#### NodeMaintenanceFailoverComplete event + +`NodeMaintenanceFailoverComplete` events are raised when a replica has promoted itself to primary. These events do not include a `StartTimeUtc` because the action has already occurred. + +#### NodeMaintenanceEnded event + +`NodeMaintenanceEnded` events are raised to indicate that the maintenance operation has completed and that the replica is once again available. You do *NOT* need to wait for this event to use the load balancer endpoint, as it is available throughout. However, we included this for logging purposes and for customers who use the replica endpoint in clusters for read workloads. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 2fb22443c..5a12d24d2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -38,6 +38,7 @@ Documentation - [Transactions](Transactions) - how atomic transactions work in redis - [Events](Events) - the events available for logging / information purposes - [Pub/Sub Message Order](PubSubOrder) - advice on sequential and concurrent processing +- [ServerMaintenanceEvent](ServerMaintenanceEvent) - how to listen and prepare for hosted server maintenance (e.g. Azure Cache for Redis) - [Streams](Streams) - how to use the Stream data type - [Where are `KEYS` / `SCAN` / `FLUSH*`?](KeysScan) - how to use server-based commands - [Profiling](Profiling) - profiling interfaces, as well as how to profile in an `async` world From 05dc252f816192494032d85a5b011b018e4fcd33 Mon Sep 17 00:00:00 2001 From: Lorenzo Date: Thu, 4 Nov 2021 17:40:41 -0700 Subject: [PATCH 037/435] Change default reconnection policy to Exponential (#1896) Change default reconnection policy to Exponential Update the Exponential policy to take the base as parameter to allow for different growth rates Description: To speed up reconnections, and also avoid reconnection storms, we are changing the default reconnection policy to ExponentialRetry and reducing the deltaBackOff from the previous default of 5s to 2.5s. a This allows more frequent retries early on, when a reconnection is more probable, and less frequent retries later on when it has already been impossible for some time. Another plus for the ExponentialRetry is the additional randomness added to the intervals so that we avoid reconnection storms. Here is a comparison of the two retry policies. ![image](https://user-images.githubusercontent.com/18540925/140260525-dfde3f60-2980-46e9-a0ce-1ac174f2fc80.png) ![image](https://user-images.githubusercontent.com/18540925/140260534-d137d303-e1f8-437e-a4ca-1292b9e89a4f.png) And here is a real world example: ![image](https://user-images.githubusercontent.com/18540925/140263336-cb6b12d7-92ac-4f00-a27c-00fea2d36eaa.png) The randomized exponential (orange) is within the the minimum delta (yellow) and the maximum exponential (gray). The retry happens when the timeout (blue) is greater than the retry limit (orange). --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ConfigurationOptions.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 90db06c14..0d86d98e0 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,6 +1,7 @@ # Release Notes ## Unreleased +- Connection backoff default is now exponential instead of linear (#1896 via lolodi) - Add support for NodeMaintenanceScaleComplete event (handles Redis cluster scaling) (#1902 via NickCraver) ## 2.2.79 diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 44486acbc..190d2fee3 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -334,7 +334,7 @@ public bool PreserveAsyncOrder /// /// The retry policy to be used for connection reconnects /// - public IReconnectRetryPolicy ReconnectRetryPolicy { get { return reconnectRetryPolicy ??= new LinearRetry(ConnectTimeout); } set { reconnectRetryPolicy = value; } } + public IReconnectRetryPolicy ReconnectRetryPolicy { get { return reconnectRetryPolicy ??= new ExponentialRetry(ConnectTimeout/2); } set { reconnectRetryPolicy = value; } } /// /// Indicates whether endpoints should be resolved via DNS before connecting. From 3b8e97ad8e8dd0055405b66cc4fd1df2141c4845 Mon Sep 17 00:00:00 2001 From: Philo Date: Thu, 11 Nov 2021 14:43:35 -0800 Subject: [PATCH 038/435] Update Release notes for 2.2.88 (#1908) --- docs/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 0d86d98e0..747485736 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,6 +1,9 @@ # Release Notes ## Unreleased + +## 2.2.88 + - Connection backoff default is now exponential instead of linear (#1896 via lolodi) - Add support for NodeMaintenanceScaleComplete event (handles Redis cluster scaling) (#1902 via NickCraver) From 7e5c81aa3daa8ea651e6c6097c0ae5fbc5d9e747 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Thu, 2 Dec 2021 17:40:19 +0800 Subject: [PATCH 039/435] Makes StreamEntry constructor public (#1923) * Makes StreamEntry constructor public * Update release notes --- docs/ReleaseNotes.md | 2 ++ src/StackExchange.Redis/StreamEntry.cs | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 747485736..0f86c51c8 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -2,6 +2,8 @@ ## Unreleased +- Makes `StreamEntry` constructor public for better unit test experience (#1923 via WeihanLi) + ## 2.2.88 - Connection backoff default is now exponential instead of linear (#1896 via lolodi) diff --git a/src/StackExchange.Redis/StreamEntry.cs b/src/StackExchange.Redis/StreamEntry.cs index 5611af808..b9cf50176 100644 --- a/src/StackExchange.Redis/StreamEntry.cs +++ b/src/StackExchange.Redis/StreamEntry.cs @@ -7,7 +7,10 @@ namespace StackExchange.Redis /// public readonly struct StreamEntry { - internal StreamEntry(RedisValue id, NameValueEntry[] values) + /// + /// Creates an stream entry + /// + public StreamEntry(RedisValue id, NameValueEntry[] values) { Id = id; Values = values; From d527b0ef3e8003944454366301ad96debcd764c5 Mon Sep 17 00:00:00 2001 From: Rich Kalasky Date: Sun, 5 Dec 2021 08:25:03 -0600 Subject: [PATCH 040/435] Update Server.md (#1915) Adding GCP offering to list of cloud providers --- docs/Server.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Server.md b/docs/Server.md index e236b1f5f..b8fcdff56 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -25,4 +25,5 @@ If you don't want to run your own redis servers, multiple commercial cloud offer - RedisLabs - Azure Redis Cache -- AWS ElastiCache for Redis \ No newline at end of file +- AWS ElastiCache for Redis +- GCP Memorystore for Redis From f3052db96b071aa4bd88010ef2716d4d6f0329c7 Mon Sep 17 00:00:00 2001 From: testfirstcoder Date: Sun, 5 Dec 2021 15:25:27 +0100 Subject: [PATCH 041/435] Change result type/value in the xml tag of the method documentation (#1914) Change result type/value in the documentation for the method `HashSet`: `1` => `True` and `0` => `False` --- src/StackExchange.Redis/Interfaces/IDatabase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index bb0f56b47..6a8e8a46e 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -353,7 +353,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The value to set. /// Which conditions under which to set the field value (defaults to always). /// The flags to use for this operation. - /// 1 if field is a new field in the hash and value was set. 0 if field already exists in the hash and the value was updated. + /// True if field is a new field in the hash and value was set. False if field already exists in the hash and the value was updated. /// https://redis.io/commands/hset /// https://redis.io/commands/hsetnx bool HashSet(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); From 68b978a22566b516fff2031079adc7632c06e936 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sun, 12 Dec 2021 14:35:04 +0000 Subject: [PATCH 042/435] investigate SnapshotPosition error (#1927) --- src/StackExchange.Redis/BufferReader.cs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/BufferReader.cs b/src/StackExchange.Redis/BufferReader.cs index 952a70d0e..4478eab07 100644 --- a/src/StackExchange.Redis/BufferReader.cs +++ b/src/StackExchange.Redis/BufferReader.cs @@ -114,10 +114,32 @@ private SequencePosition SnapshotPosition() var delta = consumed - _lastSnapshotBytes; if (delta == 0) return _lastSnapshotPosition; - var pos = _buffer.GetPosition(delta, _lastSnapshotPosition); + SequencePosition pos; + try + { + pos = _buffer.GetPosition(delta, _lastSnapshotPosition); + } + catch (ArgumentOutOfRangeException ex) + { + ThrowSnapshotFailure(delta, ex); + throw; // should never be reached + } _lastSnapshotBytes = consumed; return _lastSnapshotPosition = pos; } + private void ThrowSnapshotFailure(long delta, Exception innerException) + { + long length; + try + { + length = _buffer.Length; + } + catch + { + length = -1; + } + throw new ArgumentOutOfRangeException($"Error calculating {nameof(SnapshotPosition)}: {TotalConsumed} of {length}, {_lastSnapshotBytes}+{delta}", innerException); + } public ReadOnlySequence ConsumeAsBuffer(int count) { if (!TryConsumeAsBuffer(count, out var buffer)) throw new EndOfStreamException(); From a55c8beaa67b9adae31f2607c66da8e81b53842c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 13 Dec 2021 07:56:22 +0000 Subject: [PATCH 043/435] fix #1926 integer overflow in BufferReader (make total consumed 64-bit) (#1928) * fix #1926 integer overflow in BufferReader (make total consumed 64-bit) * remove temp logging code re #1926 --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/BufferReader.cs | 43 +++++++------------------ 2 files changed, 12 insertions(+), 32 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 0f86c51c8..424905b89 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -3,6 +3,7 @@ ## Unreleased - Makes `StreamEntry` constructor public for better unit test experience (#1923 via WeihanLi) +- Fix integer overflow error (issue #1926) with 2GiB+ result payloads ## 2.2.88 diff --git a/src/StackExchange.Redis/BufferReader.cs b/src/StackExchange.Redis/BufferReader.cs index 4478eab07..659ab1b75 100644 --- a/src/StackExchange.Redis/BufferReader.cs +++ b/src/StackExchange.Redis/BufferReader.cs @@ -12,15 +12,16 @@ internal enum ConsumeResult } internal ref struct BufferReader { + private long _totalConsumed; + public int OffsetThisSpan { get; private set; } + public int RemainingThisSpan { get; private set; } + private ReadOnlySequence.Enumerator _iterator; private ReadOnlySpan _current; public ReadOnlySpan OversizedSpan => _current; public ReadOnlySpan SlicedSpan => _current.Slice(OffsetThisSpan, RemainingThisSpan); - public int OffsetThisSpan { get; private set; } - private int TotalConsumed { get; set; } // hide this; callers should use the snapshot-aware methods instead - public int RemainingThisSpan { get; private set; } public bool IsEmpty => RemainingThisSpan == 0; @@ -49,7 +50,7 @@ public BufferReader(ReadOnlySequence buffer) _lastSnapshotBytes = 0; _iterator = buffer.GetEnumerator(); _current = default; - OffsetThisSpan = RemainingThisSpan = TotalConsumed = 0; + _totalConsumed = OffsetThisSpan = RemainingThisSpan = 0; FetchNextSegment(); } @@ -87,7 +88,7 @@ public bool TryConsume(int count) if (count <= available) { // consume part of this span - TotalConsumed += count; + _totalConsumed += count; RemainingThisSpan -= count; OffsetThisSpan += count; @@ -96,7 +97,7 @@ public bool TryConsume(int count) } // consume all of this span - TotalConsumed += available; + _totalConsumed += available; count -= available; } while (FetchNextSegment()); return false; @@ -110,36 +111,14 @@ public bool TryConsume(int count) // to avoid having to use buffer.Slice on huge ranges private SequencePosition SnapshotPosition() { - var consumed = TotalConsumed; - var delta = consumed - _lastSnapshotBytes; + var delta = _totalConsumed - _lastSnapshotBytes; if (delta == 0) return _lastSnapshotPosition; - SequencePosition pos; - try - { - pos = _buffer.GetPosition(delta, _lastSnapshotPosition); - } - catch (ArgumentOutOfRangeException ex) - { - ThrowSnapshotFailure(delta, ex); - throw; // should never be reached - } - _lastSnapshotBytes = consumed; + var pos = _buffer.GetPosition(delta, _lastSnapshotPosition); + _lastSnapshotBytes = _totalConsumed; return _lastSnapshotPosition = pos; } - private void ThrowSnapshotFailure(long delta, Exception innerException) - { - long length; - try - { - length = _buffer.Length; - } - catch - { - length = -1; - } - throw new ArgumentOutOfRangeException($"Error calculating {nameof(SnapshotPosition)}: {TotalConsumed} of {length}, {_lastSnapshotBytes}+{delta}", innerException); - } + public ReadOnlySequence ConsumeAsBuffer(int count) { if (!TryConsumeAsBuffer(count, out var buffer)) throw new EndOfStreamException(); From fd795154fbc5efa61dd1da14b233196a8accaf88 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 14 Dec 2021 12:36:43 -0500 Subject: [PATCH 044/435] Default versions: assume v2.8 as a minimum (SCAN over KEYS) and 4.0+ in Azure (#1929) This more reflects reality today and if we cannot detect a server version (e.g. config has INFO disabled as in #1926) then we'll do smarter things like use SCAN over KEYS. --- docs/ReleaseNotes.md | 3 ++- src/StackExchange.Redis/ConfigurationOptions.cs | 2 +- tests/StackExchange.Redis.Tests/Config.cs | 15 +++++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 424905b89..fda2564aa 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -3,7 +3,8 @@ ## Unreleased - Makes `StreamEntry` constructor public for better unit test experience (#1923 via WeihanLi) -- Fix integer overflow error (issue #1926) with 2GiB+ result payloads +- Fix integer overflow error (issue #1926) with 2GiB+ result payloads (#1928 via mgravell) +- Update assumed redis versions to v2.8 or v4.0 in the Azure case (#1929 via NickCraver) ## 2.2.88 diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 190d2fee3..07465a318 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -288,7 +288,7 @@ public int ConnectTimeout /// /// The server version to assume /// - public Version DefaultVersion { get { return defaultVersion ?? (IsAzureEndpoint() ? RedisFeatures.v3_0_0 : RedisFeatures.v2_0_0); } set { defaultVersion = value; } } + public Version DefaultVersion { get { return defaultVersion ?? (IsAzureEndpoint() ? RedisFeatures.v4_0_0 : RedisFeatures.v2_8_0); } set { defaultVersion = value; } } /// /// The endpoints defined for this configuration diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index 553f3d6f7..201af69c9 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -14,6 +14,9 @@ namespace StackExchange.Redis.Tests { public class Config : TestBase { + public Version DefaultVersion = new (2, 8, 0); + public Version DefaultAzureVersion = new (4, 0, 0); + public Config(ITestOutputHelper output) : base(output) { } [Fact] @@ -63,7 +66,7 @@ public void SslProtocols_InvalidValue() public void ConfigurationOptionsDefaultForAzure() { var options = ConfigurationOptions.Parse("contoso.redis.cache.windows.net"); - Assert.True(options.DefaultVersion.Equals(new Version(3, 0, 0))); + Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); Assert.False(options.AbortOnConnectFail); } @@ -80,7 +83,7 @@ public void ConfigurationOptionsDefaultForAzureChina() { // added a few upper case chars to validate comparison var options = ConfigurationOptions.Parse("contoso.REDIS.CACHE.chinacloudapi.cn"); - Assert.True(options.DefaultVersion.Equals(new Version(3, 0, 0))); + Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); Assert.False(options.AbortOnConnectFail); } @@ -88,7 +91,7 @@ public void ConfigurationOptionsDefaultForAzureChina() public void ConfigurationOptionsDefaultForAzureGermany() { var options = ConfigurationOptions.Parse("contoso.redis.cache.cloudapi.de"); - Assert.True(options.DefaultVersion.Equals(new Version(3, 0, 0))); + Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); Assert.False(options.AbortOnConnectFail); } @@ -96,7 +99,7 @@ public void ConfigurationOptionsDefaultForAzureGermany() public void ConfigurationOptionsDefaultForAzureUSGov() { var options = ConfigurationOptions.Parse("contoso.redis.cache.usgovcloudapi.net"); - Assert.True(options.DefaultVersion.Equals(new Version(3, 0, 0))); + Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); Assert.False(options.AbortOnConnectFail); } @@ -104,7 +107,7 @@ public void ConfigurationOptionsDefaultForAzureUSGov() public void ConfigurationOptionsDefaultForNonAzure() { var options = ConfigurationOptions.Parse("redis.contoso.com"); - Assert.True(options.DefaultVersion.Equals(new Version(2, 0, 0))); + Assert.True(options.DefaultVersion.Equals(DefaultVersion)); Assert.True(options.AbortOnConnectFail); } @@ -112,7 +115,7 @@ public void ConfigurationOptionsDefaultForNonAzure() public void ConfigurationOptionsDefaultWhenNoEndpointsSpecifiedYet() { var options = new ConfigurationOptions(); - Assert.True(options.DefaultVersion.Equals(new Version(2, 0, 0))); + Assert.True(options.DefaultVersion.Equals(DefaultVersion)); Assert.True(options.AbortOnConnectFail); } From 1384bd8cf953ab09b62b0c980250392cff45fa9d Mon Sep 17 00:00:00 2001 From: Martin Potter <675256+martinpotter@users.noreply.github.com> Date: Fri, 31 Dec 2021 07:24:55 -0800 Subject: [PATCH 045/435] Initialize ScriptEvalMessage with the correct command (#1930) Currently the constructor for ScriptEvalMessage that takes a SHA1 hash, and sends the `EVALSHA` command, initializes the base Message class's command to `RedisCommand.EVAL` instead of `RedisCommand.EVALSHA`. --- src/StackExchange.Redis/RedisDatabase.cs | 2 +- tests/StackExchange.Redis.Tests/Profiling.cs | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 5fec7027d..1656eb785 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -3807,7 +3807,7 @@ public ScriptEvalMessage(int db, CommandFlags flags, string script, RedisKey[] k } public ScriptEvalMessage(int db, CommandFlags flags, byte[] hash, RedisKey[] keys, RedisValue[] values) - : this(db, flags, RedisCommand.EVAL, null, hash, keys, values) + : this(db, flags, RedisCommand.EVALSHA, null, hash, keys, values) { if (hash == null) throw new ArgumentNullException(nameof(hash)); if (hash.Length != ResultProcessor.ScriptLoadProcessor.Sha1HashLength) throw new ArgumentOutOfRangeException(nameof(hash), "Invalid hash length"); diff --git a/tests/StackExchange.Redis.Tests/Profiling.cs b/tests/StackExchange.Redis.Tests/Profiling.cs index da69e5069..568302fc1 100644 --- a/tests/StackExchange.Redis.Tests/Profiling.cs +++ b/tests/StackExchange.Redis.Tests/Profiling.cs @@ -20,6 +20,9 @@ public void Simple() { using (var conn = Create()) { + var server = conn.GetServer(TestConfig.Current.MasterServerAndPort); + var script = LuaScript.Prepare("return redis.call('get', @key)"); + var loaded = script.Load(server); var key = Me(); var session = new ProfilingSession(); @@ -29,8 +32,10 @@ public void Simple() var dbId = TestConfig.GetDedicatedDB(); var db = conn.GetDatabase(dbId); db.StringSet(key, "world"); - var result = db.ScriptEvaluate(LuaScript.Prepare("return redis.call('get', @key)"), new { key = (RedisKey)key }); + var result = db.ScriptEvaluate(script, new { key = (RedisKey)key }); Assert.Equal("world", result.AsString()); + var loadedResult = db.ScriptEvaluate(loaded, new { key = (RedisKey)key }); + Assert.Equal("world", loadedResult.AsString()); var val = db.StringGet(key); Assert.Equal("world", val); var s = (string)db.Execute("ECHO", "fii"); @@ -44,7 +49,7 @@ public void Simple() } var all = string.Join(",", cmds.Select(x => x.Command)); - Assert.Equal("SET,EVAL,GET,ECHO", all); + Assert.Equal("SET,EVAL,EVALSHA,GET,ECHO", all); Log("Checking for SET"); var set = cmds.SingleOrDefault(cmd => cmd.Command == "SET"); Assert.NotNull(set); @@ -54,14 +59,18 @@ public void Simple() Log("Checking for EVAL"); var eval = cmds.SingleOrDefault(cmd => cmd.Command == "EVAL"); Assert.NotNull(eval); + Log("Checking for EVALSHA"); + var evalSha = cmds.SingleOrDefault(cmd => cmd.Command == "EVALSHA"); + Assert.NotNull(evalSha); Log("Checking for ECHO"); var echo = cmds.SingleOrDefault(cmd => cmd.Command == "ECHO"); Assert.NotNull(echo); - Assert.Equal(4, cmds.Count()); + Assert.Equal(5, cmds.Count()); Assert.True(set.CommandCreated <= eval.CommandCreated); - Assert.True(eval.CommandCreated <= get.CommandCreated); + Assert.True(eval.CommandCreated <= evalSha.CommandCreated); + Assert.True(evalSha.CommandCreated <= get.CommandCreated); AssertProfiledCommandValues(set, conn, dbId); @@ -69,6 +78,8 @@ public void Simple() AssertProfiledCommandValues(eval, conn, dbId); + AssertProfiledCommandValues(evalSha, conn, dbId); + AssertProfiledCommandValues(echo, conn, dbId); } } From 9adac77975f642eec3d1bfa1b46140f114540a92 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 31 Dec 2021 10:33:12 -0500 Subject: [PATCH 046/435] Add #1930 to release notes --- docs/ReleaseNotes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index fda2564aa..c3b401a47 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -5,6 +5,7 @@ - Makes `StreamEntry` constructor public for better unit test experience (#1923 via WeihanLi) - Fix integer overflow error (issue #1926) with 2GiB+ result payloads (#1928 via mgravell) - Update assumed redis versions to v2.8 or v4.0 in the Azure case (#1929 via NickCraver) +- Fix profiler showing `EVAL` instead `EVALSHA` (#1930 via martinpotter) ## 2.2.88 From 04bd5c576c534187df84fbdd82a11e4d0f0bb9d0 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sun, 2 Jan 2022 15:27:29 -0500 Subject: [PATCH 047/435] Tests: ensure we're not referencing ValueTuple (#1935) Adding a sanity check to ensure this doesn't regress as long as we support .NET Full Framework. Thanks @ltrzesniewski! --- .../StackExchange.Redis.Tests/SanityChecks.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/StackExchange.Redis.Tests/SanityChecks.cs diff --git a/tests/StackExchange.Redis.Tests/SanityChecks.cs b/tests/StackExchange.Redis.Tests/SanityChecks.cs new file mode 100644 index 000000000..55571fa2d --- /dev/null +++ b/tests/StackExchange.Redis.Tests/SanityChecks.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using System.Reflection.PortableExecutable; +using System.Reflection.Metadata; +using Xunit; + +namespace StackExchange.Redis.Tests +{ + public sealed class SanityChecks + { + /// + /// Ensure we don't reference System.ValueTuple as it causes issues with .NET Full Framework + /// + /// + /// Modified from https://github.com/ltrzesniewski/InlineIL.Fody/blob/137e8b57f78b08cdc3abdaaf50ac01af50c58759/src/InlineIL.Tests/AssemblyTests.cs#L14 + /// Thanks Lucas Trzesniewski! + /// + [Fact] + public void ValueTupleNotReferenced() + { + using var fileStream = File.OpenRead(typeof(RedisValue).Assembly.Location); + using var peReader = new PEReader(fileStream); + var metadataReader = peReader.GetMetadataReader(); + + foreach (var typeRefHandle in metadataReader.TypeReferences) + { + var typeRef = metadataReader.GetTypeReference(typeRefHandle); + if (metadataReader.GetString(typeRef.Namespace) == typeof(ValueTuple).Namespace) + { + var typeName = metadataReader.GetString(typeRef.Name); + Assert.DoesNotContain(nameof(ValueTuple), typeName); + } + } + } + } +} From 9efe9ad6ac085df4170e434cf565161ba45dccd1 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 3 Jan 2022 14:54:51 -0500 Subject: [PATCH 048/435] Tests: handle clock skew vs. test containers (#1937) I hit an oddity in the Docker setup locally today where the clock drifted ~10 seconds and these tests were failing because they make an inherent assumption of local clock == server clock. This aims to remove that assumption from the test suite and treat server time as accurate or account for the skew when checking TTLs. --- tests/StackExchange.Redis.Tests/Expiry.cs | 7 +++++++ tests/StackExchange.Redis.Tests/Issues/SO25113323.cs | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/StackExchange.Redis.Tests/Expiry.cs b/tests/StackExchange.Redis.Tests/Expiry.cs index 29cd199e2..dc2835ccc 100644 --- a/tests/StackExchange.Redis.Tests/Expiry.cs +++ b/tests/StackExchange.Redis.Tests/Expiry.cs @@ -63,6 +63,9 @@ public async Task TestBasicExpiryDateTime(bool disablePTimes, bool utc) conn.KeyDelete(key, CommandFlags.FireAndForget); var now = utc ? DateTime.UtcNow : DateTime.Now; + var serverTime = GetServer(muxer).Time(); + var offset = DateTime.UtcNow - serverTime; + Log("Now: {0}", now); conn.StringSet(key, "new value", flags: CommandFlags.FireAndForget); var a = conn.KeyTimeToLiveAsync(key); @@ -79,6 +82,10 @@ public async Task TestBasicExpiryDateTime(bool disablePTimes, bool utc) Assert.Null(await a); var time = await b; + + // Adjust for server time offset, if any when checking expectations + time += offset; + Assert.NotNull(time); Log("Time: {0}, Expected: {1}-{2}", time, TimeSpan.FromMinutes(59), TimeSpan.FromMinutes(60)); Assert.True(time >= TimeSpan.FromMinutes(59)); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO25113323.cs b/tests/StackExchange.Redis.Tests/Issues/SO25113323.cs index 1afc1ab69..fb72d2007 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO25113323.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO25113323.cs @@ -23,7 +23,8 @@ public async Task SetExpirationToPassed() await Task.Delay(2000).ForAwait(); // When - var expiresOn = DateTime.UtcNow.AddSeconds(-2); + var serverTime = GetServer(conn).Time(); + var expiresOn = serverTime.AddSeconds(-2); var firstResult = cache.KeyExpire(key, expiresOn, CommandFlags.PreferMaster); var secondResult = cache.KeyExpire(key, expiresOn, CommandFlags.PreferMaster); From 28ba01700d912cfe907c55b6468751a2daf4a361 Mon Sep 17 00:00:00 2001 From: Omar Ansari Date: Mon, 3 Jan 2022 14:59:54 -0500 Subject: [PATCH 049/435] Update streams readme for consistency (#1936) There is inconsistency between the usage of `events_stream` and `event_stream` in the documentation for Getting Started with Streams. If a user copies the sample code from the first part of the page to add to the stream and then the consumer group related code from the latter part of the page to consume, they may not notice the difference in the name of the stream and wonder why they are not receiving messages. --- docs/Streams.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/Streams.md b/docs/Streams.md index c7f278d17..4378a528d 100644 --- a/docs/Streams.md +++ b/docs/Streams.md @@ -12,7 +12,7 @@ Use the following to add a simple message with a single name/value pair to a str ```csharp var db = redis.GetDatabase(); -var messageId = db.StreamAdd("event_stream", "foo_name", "bar_value"); +var messageId = db.StreamAdd("events_stream", "foo_name", "bar_value"); // messageId = 1518951480106-0 ``` @@ -34,7 +34,7 @@ var messageId = db.StreamAdd("sensor_stream", values); You also have the option to override the auto-generated message ID by passing your own ID to the `StreamAdd` method. Other optional parameters allow you to trim the stream's length. ```csharp -db.StreamAdd("event_stream", "foo_name", "bar_value", messageId: "0-1", maxLength: 100); +db.StreamAdd("events_stream", "foo_name", "bar_value", messageId: "0-1", maxLength: 100); ``` Reading from Streams @@ -43,7 +43,7 @@ Reading from Streams Reading from a stream is done by using either the `StreamRead` or `StreamRange` methods. ```csharp -var messages = db.StreamRead("event_stream", "0-0"); +var messages = db.StreamRead("events_stream", "0-0"); ``` The code above will read all messages from the ID `"0-0"` to the end of the stream. You have the option to limit the number of messages returned by using the optional `count` parameter. @@ -53,7 +53,7 @@ The `StreamRead` method also allows you to read from multiple streams at once: ```csharp var streams = db.StreamRead(new StreamPosition[] { - new StreamPosition("event_stream", "0-0"), + new StreamPosition("events_stream", "0-0"), new StreamPosition("score_stream", "0-0") }); @@ -66,13 +66,13 @@ You can limit the number of messages returned per stream by using the `countPerS The `StreamRange` method allows you to return a range of entries within a stream. ```csharp -var messages = db.StreamRange("event_stream", minId: "-", maxId: "+"); +var messages = db.StreamRange("events_stream", minId: "-", maxId: "+"); ``` The `"-"` and `"+"` special characters indicate the smallest and greatest IDs possible. These values are the default values that will be used if no value is passed for the respective parameter. You also have the option to read the stream in reverse by using the `messageOrder` parameter. The `StreamRange` method also provides the ability to limit the number of entries returned by using the `count` parameter. ```csharp -var messages = db.StreamRange("event_stream", +var messages = db.StreamRange("events_stream", minId: "0-0", maxId: "+", count: 100, @@ -85,7 +85,7 @@ Stream Information The `StreamInfo` method provides the ability to read basic information about a stream: its first and last entry, the stream's length, the number of consumer groups, etc. This information can be used to process a stream in a more efficient manner. ```csharp -var info = db.StreamInfo("event_stream"); +var info = db.StreamInfo("events_stream"); Console.WriteLine(info.Length); Console.WriteLine(info.FirstEntry.Id); From 098f35619a95f54722a8e7695b3b14c85f6eaeec Mon Sep 17 00:00:00 2001 From: Jody Donetti Date: Tue, 4 Jan 2022 14:11:41 +0100 Subject: [PATCH 050/435] Minor typo (#1941) --- src/StackExchange.Redis/RedisValue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index 9ad8c1e14..e78411a00 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -1033,7 +1033,7 @@ public bool TryParse(out double val) /// /// Create a RedisValue from a MemoryStream; it will *attempt* to use the internal buffer - /// directly, but if this isn't possibly it will fallback to ToArray + /// directly, but if this isn't possible it will fallback to ToArray /// /// The to create a value from. public static RedisValue CreateFrom(MemoryStream stream) From 15c752e1d2e1cc437359c1c203eafc2970db8a16 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 5 Jan 2022 11:24:06 -0500 Subject: [PATCH 051/435] General code cleanup (#1940) Nothing specific, just reducing the noise a little. No functional changes intended. --- .editorconfig | 5 +- StackExchange.Redis.sln | 2 +- src/StackExchange.Redis/ClientInfo.cs | 2 +- src/StackExchange.Redis/CommandTrace.cs | 2 +- src/StackExchange.Redis/Condition.cs | 64 +++++++++---------- .../ConfigurationOptions.cs | 18 ++---- .../ConnectionMultiplexer.cs | 7 +- src/StackExchange.Redis/CursorEnumerable.cs | 9 ++- src/StackExchange.Redis/Enums/CommandFlags.cs | 2 - .../Enums/CommandStatus.cs | 6 +- src/StackExchange.Redis/Enums/When.cs | 6 +- src/StackExchange.Redis/ExponentialRetry.cs | 2 +- src/StackExchange.Redis/ExtensionMethods.cs | 36 +++++------ .../Interfaces/IDatabaseAsync.cs | 1 - .../KeyspaceIsolation/DatabaseExtension.cs | 2 +- .../KeyspaceIsolation/WrapperBase.cs | 1 - src/StackExchange.Redis/LuaScript.cs | 4 +- src/StackExchange.Redis/PhysicalBridge.cs | 10 ++- src/StackExchange.Redis/PhysicalConnection.cs | 6 +- .../Profiling/IProfiledCommand.cs | 2 +- src/StackExchange.Redis/RedisDatabase.cs | 1 - src/StackExchange.Redis/RedisServer.cs | 1 - src/StackExchange.Redis/RedisSubscriber.cs | 2 + src/StackExchange.Redis/RedisValue.cs | 1 - src/StackExchange.Redis/ResultProcessor.cs | 4 +- .../ScriptParameterMapper.cs | 2 +- src/StackExchange.Redis/ServerEndPoint.cs | 5 +- src/StackExchange.Redis/StreamConstants.cs | 2 +- .../StreamPendingMessageInfo.cs | 2 +- tests/StackExchange.Redis.Tests/BoxUnbox.cs | 2 +- tests/StackExchange.Redis.Tests/Config.cs | 1 - .../DatabaseWrapperTests.cs | 1 - tests/StackExchange.Redis.Tests/Failover.cs | 1 - tests/StackExchange.Redis.Tests/Hashes.cs | 1 - .../Issues/Issue1101.cs | 3 +- tests/StackExchange.Redis.Tests/Latency.cs | 1 - .../PubSubCommand.cs | 2 +- tests/StackExchange.Redis.Tests/SSL.cs | 2 +- tests/StackExchange.Redis.Tests/Scripting.cs | 17 +++-- .../SentinelFailover.cs | 2 +- tests/StackExchange.Redis.Tests/Streams.cs | 2 - 41 files changed, 107 insertions(+), 135 deletions(-) diff --git a/.editorconfig b/.editorconfig index 70eae8240..9322d8fec 100644 --- a/.editorconfig +++ b/.editorconfig @@ -87,4 +87,7 @@ csharp_new_line_before_members_in_anonymous_types = true csharp_space_after_keywords_in_control_flow_statements = true:suggestion # Language settings -csharp_prefer_simple_default_expression = false:none \ No newline at end of file +csharp_prefer_simple_default_expression = false:none + +# RCS1229: Use async/await when necessary. +dotnet_diagnostic.RCS1229.severity = none \ No newline at end of file diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index d33bf7db0..d1a585738 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -89,6 +89,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KestrelRedisServer", "toys\ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}" ProjectSection(SolutionItems) = preProject + tests\.editorconfig = tests\.editorconfig tests\Directory.Build.props = tests\Directory.Build.props tests\Directory.Build.targets = tests\Directory.Build.targets EndProjectSection @@ -195,7 +196,6 @@ Global {153A10E4-E668-41AD-9E0F-6785CE7EED66} = {3AD17044-6BFF-4750-9AC2-2CA466375F2A} {D58114AE-4998-4647-AFCA-9353D20495AE} = {E25031D3-5C64-430D-B86F-697B66816FD8} {A9F81DA3-DA82-423E-A5DD-B11C37548E06} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05} - {3FA2A7C6-DA16-4DEF-ACE0-34573A4AD430} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {193AA352-6748-47C1-A5FC-C9AA6B5F000B} diff --git a/src/StackExchange.Redis/ClientInfo.cs b/src/StackExchange.Redis/ClientInfo.cs index fdee896fa..91a1cf3c3 100644 --- a/src/StackExchange.Redis/ClientInfo.cs +++ b/src/StackExchange.Redis/ClientInfo.cs @@ -172,7 +172,7 @@ internal static ClientInfo[] Parse(string input) AddFlag(ref flags, value, ClientFlags.Unblocked, 'u'); AddFlag(ref flags, value, ClientFlags.UnixDomainSocket, 'U'); AddFlag(ref flags, value, ClientFlags.Transaction, 'x'); - + client.Flags = flags; break; case "id": client.Id = Format.ParseInt64(value); break; diff --git a/src/StackExchange.Redis/CommandTrace.cs b/src/StackExchange.Redis/CommandTrace.cs index de3cd1849..7d366c350 100644 --- a/src/StackExchange.Redis/CommandTrace.cs +++ b/src/StackExchange.Redis/CommandTrace.cs @@ -4,7 +4,7 @@ namespace StackExchange.Redis { /// /// Represents the information known about long-running commands - /// + /// public sealed class CommandTrace { internal static readonly ResultProcessor Processor = new CommandTraceProcessor(); diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs index c154ed0f8..050e623ac 100644 --- a/src/StackExchange.Redis/Condition.cs +++ b/src/StackExchange.Redis/Condition.cs @@ -6,7 +6,7 @@ namespace StackExchange.Redis { /// - /// Describes a pre-condition used in a redis transaction + /// Describes a pre-condition used in a redis transaction. /// public abstract class Condition { @@ -69,13 +69,13 @@ public static Condition HashNotExists(RedisKey key, RedisValue hashField) public static Condition KeyExists(RedisKey key) => new ExistsCondition(key, RedisType.None, RedisValue.Null, true); /// - /// Enforces that the given key must not exist + /// Enforces that the given key must not exist. /// /// The key that must not exist. public static Condition KeyNotExists(RedisKey key) => new ExistsCondition(key, RedisType.None, RedisValue.Null, false); /// - /// Enforces that the given list index must have the specified value + /// Enforces that the given list index must have the specified value. /// /// The key of the list to check. /// The position in the list to check. @@ -83,14 +83,14 @@ public static Condition HashNotExists(RedisKey key, RedisValue hashField) public static Condition ListIndexEqual(RedisKey key, long index, RedisValue value) => new ListCondition(key, index, true, value); /// - /// Enforces that the given list index must exist + /// Enforces that the given list index must exist. /// /// The key of the list to check. /// The position in the list that must exist. public static Condition ListIndexExists(RedisKey key, long index) => new ListCondition(key, index, true, null); /// - /// Enforces that the given list index must not have the specified value + /// Enforces that the given list index must not have the specified value. /// /// The key of the list to check. /// The position in the list to check. @@ -98,14 +98,14 @@ public static Condition HashNotExists(RedisKey key, RedisValue hashField) public static Condition ListIndexNotEqual(RedisKey key, long index, RedisValue value) => new ListCondition(key, index, false, value); /// - /// Enforces that the given list index must not exist + /// Enforces that the given list index must not exist. /// /// The key of the list to check. /// The position in the list that must not exist. public static Condition ListIndexNotExists(RedisKey key, long index) => new ListCondition(key, index, false, null); /// - /// Enforces that the given key must have the specified value + /// Enforces that the given key must have the specified value. /// /// The key to check. /// The value that must match. @@ -116,7 +116,7 @@ public static Condition StringEqual(RedisKey key, RedisValue value) } /// - /// Enforces that the given key must not have the specified value + /// Enforces that the given key must not have the specified value. /// /// The key to check. /// The value that must not match. @@ -127,112 +127,112 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value) } /// - /// Enforces that the given hash length is a certain value + /// Enforces that the given hash length is a certain value. /// /// The key of the hash to check. /// The length the hash must have. public static Condition HashLengthEqual(RedisKey key, long length) => new LengthCondition(key, RedisType.Hash, 0, length); /// - /// Enforces that the given hash length is less than a certain value + /// Enforces that the given hash length is less than a certain value. /// /// The key of the hash to check. /// The length the hash must be less than. public static Condition HashLengthLessThan(RedisKey key, long length) => new LengthCondition(key, RedisType.Hash, 1, length); /// - /// Enforces that the given hash length is greater than a certain value + /// Enforces that the given hash length is greater than a certain value. /// /// The key of the hash to check. /// The length the hash must be greater than. public static Condition HashLengthGreaterThan(RedisKey key, long length) => new LengthCondition(key, RedisType.Hash, -1, length); /// - /// Enforces that the given string length is a certain value + /// Enforces that the given string length is a certain value. /// /// The key of the string to check. /// The length the string must be equal to. public static Condition StringLengthEqual(RedisKey key, long length) => new LengthCondition(key, RedisType.String, 0, length); /// - /// Enforces that the given string length is less than a certain value + /// Enforces that the given string length is less than a certain value. /// /// The key of the string to check. /// The length the string must be less than. public static Condition StringLengthLessThan(RedisKey key, long length) => new LengthCondition(key, RedisType.String, 1, length); /// - /// Enforces that the given string length is greater than a certain value + /// Enforces that the given string length is greater than a certain value. /// /// The key of the string to check. /// The length the string must be greater than. public static Condition StringLengthGreaterThan(RedisKey key, long length) => new LengthCondition(key, RedisType.String, -1, length); /// - /// Enforces that the given list length is a certain value + /// Enforces that the given list length is a certain value. /// /// The key of the list to check. /// The length the list must be equal to. public static Condition ListLengthEqual(RedisKey key, long length) => new LengthCondition(key, RedisType.List, 0, length); /// - /// Enforces that the given list length is less than a certain value + /// Enforces that the given list length is less than a certain value. /// /// The key of the list to check. /// The length the list must be less than. public static Condition ListLengthLessThan(RedisKey key, long length) => new LengthCondition(key, RedisType.List, 1, length); /// - /// Enforces that the given list length is greater than a certain value + /// Enforces that the given list length is greater than a certain value. /// /// The key of the list to check. /// The length the list must be greater than. public static Condition ListLengthGreaterThan(RedisKey key, long length) => new LengthCondition(key, RedisType.List, -1, length); /// - /// Enforces that the given set cardinality is a certain value + /// Enforces that the given set cardinality is a certain value. /// /// The key of the set to check. /// The length the set must be equal to. public static Condition SetLengthEqual(RedisKey key, long length) => new LengthCondition(key, RedisType.Set, 0, length); /// - /// Enforces that the given set cardinality is less than a certain value + /// Enforces that the given set cardinality is less than a certain value. /// /// The key of the set to check. /// The length the set must be less than. public static Condition SetLengthLessThan(RedisKey key, long length) => new LengthCondition(key, RedisType.Set, 1, length); /// - /// Enforces that the given set cardinality is greater than a certain value + /// Enforces that the given set cardinality is greater than a certain value. /// /// The key of the set to check. /// The length the set must be greater than. public static Condition SetLengthGreaterThan(RedisKey key, long length) => new LengthCondition(key, RedisType.Set, -1, length); /// - /// Enforces that the given set contains a certain member + /// Enforces that the given set contains a certain member. /// /// The key of the set to check. /// The member the set must contain. public static Condition SetContains(RedisKey key, RedisValue member) => new ExistsCondition(key, RedisType.Set, member, true); /// - /// Enforces that the given set does not contain a certain member + /// Enforces that the given set does not contain a certain member. /// /// The key of the set to check. /// The member the set must not contain. public static Condition SetNotContains(RedisKey key, RedisValue member) => new ExistsCondition(key, RedisType.Set, member, false); /// - /// Enforces that the given sorted set cardinality is a certain value + /// Enforces that the given sorted set cardinality is a certain value. /// /// The key of the sorted set to check. /// The length the sorted set must be equal to. public static Condition SortedSetLengthEqual(RedisKey key, long length) => new LengthCondition(key, RedisType.SortedSet, 0, length); /// - /// Enforces that the given sorted set contains a certain number of members with scores in the given range + /// Enforces that the given sorted set contains a certain number of members with scores in the given range. /// /// The key of the sorted set to check. /// The length the sorted set must be equal to. @@ -248,7 +248,7 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value) public static Condition SortedSetLengthLessThan(RedisKey key, long length) => new LengthCondition(key, RedisType.SortedSet, 1, length); /// - /// Enforces that the given sorted set contains less than a certain number of members with scores in the given range + /// Enforces that the given sorted set contains less than a certain number of members with scores in the given range. /// /// The key of the sorted set to check. /// The length the sorted set must be equal to. @@ -257,14 +257,14 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value) public static Condition SortedSetLengthLessThan(RedisKey key, long length, double min = double.NegativeInfinity, double max = double.PositiveInfinity) => new SortedSetRangeLengthCondition(key, min, max, 1, length); /// - /// Enforces that the given sorted set cardinality is greater than a certain value + /// Enforces that the given sorted set cardinality is greater than a certain value. /// /// The key of the sorted set to check. /// The length the sorted set must be greater than. public static Condition SortedSetLengthGreaterThan(RedisKey key, long length) => new LengthCondition(key, RedisType.SortedSet, -1, length); /// - /// Enforces that the given sorted set contains more than a certain number of members with scores in the given range + /// Enforces that the given sorted set contains more than a certain number of members with scores in the given range. /// /// The key of the sorted set to check. /// The length the sorted set must be equal to. @@ -273,14 +273,14 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value) public static Condition SortedSetLengthGreaterThan(RedisKey key, long length, double min = double.NegativeInfinity, double max = double.PositiveInfinity) => new SortedSetRangeLengthCondition(key, min, max, -1, length); /// - /// Enforces that the given sorted set contains a certain member + /// Enforces that the given sorted set contains a certain member. /// /// The key of the sorted set to check. /// The member the sorted set must contain. public static Condition SortedSetContains(RedisKey key, RedisValue member) => new ExistsCondition(key, RedisType.SortedSet, member, true); /// - /// Enforces that the given sorted set does not contain a certain member + /// Enforces that the given sorted set does not contain a certain member. /// /// The key of the sorted set to check. /// The member the sorted set must not contain. @@ -333,21 +333,21 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value) public static Condition SortedSetScoreNotExists(RedisKey key, RedisValue score, RedisValue count) => new SortedSetScoreCondition(key, score, false, count); /// - /// Enforces that the given stream length is a certain value + /// Enforces that the given stream length is a certain value. /// /// The key of the stream to check. /// The length the stream must have. public static Condition StreamLengthEqual(RedisKey key, long length) => new LengthCondition(key, RedisType.Stream, 0, length); /// - /// Enforces that the given stream length is less than a certain value + /// Enforces that the given stream length is less than a certain value. /// /// The key of the stream to check. /// The length the stream must be less than. public static Condition StreamLengthLessThan(RedisKey key, long length) => new LengthCondition(key, RedisType.Stream, 1, length); /// - /// Enforces that the given stream length is greater than a certain value + /// Enforces that the given stream length is greater than a certain value. /// /// The key of the stream to check. /// The length the stream must be greater than. diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 07465a318..abc12b579 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -92,7 +92,6 @@ internal const string WriteBuffer = "writeBuffer", CheckCertificateRevocation = "checkCertificateRevocation"; - private static readonly Dictionary normalizedOptions = new[] { AbortOnConnectFail, @@ -247,19 +246,12 @@ private static bool CheckTrustedIssuer(X509Certificate2 certificateToValidate, X /// public CommandMap CommandMap { - get + get => commandMap ?? Proxy switch { - if (commandMap != null) return commandMap; - return Proxy switch - { - Proxy.Twemproxy => CommandMap.Twemproxy, - _ => CommandMap.Default, - }; - } - set - { - commandMap = value ?? throw new ArgumentNullException(nameof(value)); - } + Proxy.Twemproxy => CommandMap.Twemproxy, + _ => CommandMap.Default, + }; + set => commandMap = value ?? throw new ArgumentNullException(nameof(value)); } /// diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 85803df2c..f265b8e93 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -70,7 +70,6 @@ public static bool GetFeatureFlag(string flag) internal static bool PreventThreadTheft => (s_featureFlags & FeatureFlags.PreventThreadTheft) != 0; - private static TaskFactory _factory = null; #if DEBUG @@ -1794,7 +1793,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP // After we've successfully connected (and authenticated), kickoff tie breakers if needed if (useTieBreakers) { - log?.WriteLine($"Election: Gathering tie-breakers..."); + log?.WriteLine("Election: Gathering tie-breakers..."); for (int i = 0; i < available.Length; i++) { var server = servers[i]; @@ -1868,7 +1867,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP case ServerType.Cluster: server.ClearUnselectable(UnselectableFlags.ServerType); if (server.IsReplica) - { + { server.ClearUnselectable(UnselectableFlags.RedundantMaster); } else @@ -2026,7 +2025,6 @@ private async Task GetEndpointsFromClusterNodes(ServerEndPoi { serverEndpoint.UpdateNodeRelations(clusterConfig); } - } return clusterEndpoints; } @@ -2515,7 +2513,6 @@ internal void OnManagedConnectionRestored(object sender, ConnectionFailedEventAr try { - // Run a switch to make sure we have update-to-date // information about which master we should connect to SwitchMaster(e.EndPoint, connection); diff --git a/src/StackExchange.Redis/CursorEnumerable.cs b/src/StackExchange.Redis/CursorEnumerable.cs index fbe170e51..34acce1d8 100644 --- a/src/StackExchange.Redis/CursorEnumerable.cs +++ b/src/StackExchange.Redis/CursorEnumerable.cs @@ -129,7 +129,6 @@ public ValueTask DisposeAsync() Dispose(); return default; } - object IEnumerator.Current => _pageOversized[_pageOffset]; @@ -185,7 +184,7 @@ private void ProcessReply(in ScanResult result, bool isInitial) /// public bool MoveNext() => SimpleNext() || SlowNextSync(); - bool SlowNextSync() + private bool SlowNextSync() { var pending = SlowNextAsync(); if (pending.IsCompletedSuccessfully) return pending.Result; @@ -285,7 +284,7 @@ private async ValueTask AwaitedNextAsync(bool isInitial) return false; } - static void Recycle(ref T[] array, ref bool isPooled) + private static void Recycle(ref T[] array, ref bool isPooled) { var tmp = array; array = null; @@ -333,10 +332,10 @@ int IScanningCursor.PageOffset internal static CursorEnumerable From(RedisBase redis, ServerEndPoint server, Task pending, int pageOffset) => new SingleBlockEnumerable(redis, server, pending, pageOffset); - class SingleBlockEnumerable : CursorEnumerable + private class SingleBlockEnumerable : CursorEnumerable { private readonly Task _pending; - public SingleBlockEnumerable(RedisBase redis, ServerEndPoint server, + public SingleBlockEnumerable(RedisBase redis, ServerEndPoint server, Task pending, int pageOffset) : base(redis, server, 0, int.MaxValue, 0, pageOffset, default) { _pending = pending; diff --git a/src/StackExchange.Redis/Enums/CommandFlags.cs b/src/StackExchange.Redis/Enums/CommandFlags.cs index 286e19cd6..f0a670d76 100644 --- a/src/StackExchange.Redis/Enums/CommandFlags.cs +++ b/src/StackExchange.Redis/Enums/CommandFlags.cs @@ -37,8 +37,6 @@ public enum CommandFlags /// DemandMaster = 4, - - /// /// This operation should be performed on the replica if it is available, but will be performed on /// a master if no replicas are available. Suitable for read operations only. diff --git a/src/StackExchange.Redis/Enums/CommandStatus.cs b/src/StackExchange.Redis/Enums/CommandStatus.cs index 550e2e8aa..bc35845ae 100644 --- a/src/StackExchange.Redis/Enums/CommandStatus.cs +++ b/src/StackExchange.Redis/Enums/CommandStatus.cs @@ -6,15 +6,15 @@ public enum CommandStatus { /// - /// command status unknown + /// command status unknown. /// Unknown, /// - /// ConnectionMultiplexer has not yet started writing this command to redis + /// ConnectionMultiplexer has not yet started writing this command to redis. /// WaitingToBeSent, /// - /// command has been sent to Redis + /// command has been sent to Redis. /// Sent, } diff --git a/src/StackExchange.Redis/Enums/When.cs b/src/StackExchange.Redis/Enums/When.cs index d72deba29..ed931298a 100644 --- a/src/StackExchange.Redis/Enums/When.cs +++ b/src/StackExchange.Redis/Enums/When.cs @@ -6,15 +6,15 @@ public enum When { /// - /// The operation should occur whether or not there is an existing value + /// The operation should occur whether or not there is an existing value. /// Always, /// - /// The operation should only occur when there is an existing value + /// The operation should only occur when there is an existing value. /// Exists, /// - /// The operation should only occur when there is not an existing value + /// The operation should only occur when there is not an existing value. /// NotExists } diff --git a/src/StackExchange.Redis/ExponentialRetry.cs b/src/StackExchange.Redis/ExponentialRetry.cs index cb51491ca..31a909605 100644 --- a/src/StackExchange.Redis/ExponentialRetry.cs +++ b/src/StackExchange.Redis/ExponentialRetry.cs @@ -3,7 +3,7 @@ namespace StackExchange.Redis { /// - /// Represents a retry policy that performs retries, using a randomized exponential back off scheme to determine the interval between retries. + /// Represents a retry policy that performs retries, using a randomized exponential back off scheme to determine the interval between retries. /// public class ExponentialRetry : IReconnectRetryPolicy { diff --git a/src/StackExchange.Redis/ExtensionMethods.cs b/src/StackExchange.Redis/ExtensionMethods.cs index a9b7fd14b..1109f9ea0 100644 --- a/src/StackExchange.Redis/ExtensionMethods.cs +++ b/src/StackExchange.Redis/ExtensionMethods.cs @@ -16,7 +16,7 @@ namespace StackExchange.Redis public static class ExtensionMethods { /// - /// Create a dictionary from an array of HashEntry values + /// Create a dictionary from an array of HashEntry values. /// /// The entry to convert to a dictionary. public static Dictionary ToStringDictionary(this HashEntry[] hash) @@ -31,7 +31,7 @@ public static Dictionary ToStringDictionary(this HashEntry[] hash return result; } /// - /// Create a dictionary from an array of HashEntry values + /// Create a dictionary from an array of HashEntry values. /// /// The entry to convert to a dictionary. public static Dictionary ToDictionary(this HashEntry[] hash) @@ -47,7 +47,7 @@ public static Dictionary ToDictionary(this HashEntry[] h } /// - /// Create a dictionary from an array of SortedSetEntry values + /// Create a dictionary from an array of SortedSetEntry values. /// /// The set entries to convert to a dictionary. public static Dictionary ToStringDictionary(this SortedSetEntry[] sortedSet) @@ -63,7 +63,7 @@ public static Dictionary ToStringDictionary(this SortedSetEntry[ } /// - /// Create a dictionary from an array of SortedSetEntry values + /// Create a dictionary from an array of SortedSetEntry values. /// /// The set entries to convert to a dictionary. public static Dictionary ToDictionary(this SortedSetEntry[] sortedSet) @@ -79,7 +79,7 @@ public static Dictionary ToDictionary(this SortedSetEntry[] } /// - /// Create a dictionary from an array of key/value pairs + /// Create a dictionary from an array of key/value pairs. /// /// The pairs to convert to a dictionary. public static Dictionary ToStringDictionary(this KeyValuePair[] pairs) @@ -95,7 +95,7 @@ public static Dictionary ToStringDictionary(this KeyValuePair - /// Create a dictionary from an array of key/value pairs + /// Create a dictionary from an array of key/value pairs. /// /// The pairs to convert to a dictionary. public static Dictionary ToDictionary(this KeyValuePair[] pairs) @@ -111,7 +111,7 @@ public static Dictionary ToDictionary(this KeyValuePair - /// Create a dictionary from an array of string pairs + /// Create a dictionary from an array of string pairs. /// /// The pairs to convert to a dictionary. public static Dictionary ToDictionary(this KeyValuePair[] pairs) @@ -129,7 +129,7 @@ public static Dictionary ToDictionary(this KeyValuePair /// Create an array of RedisValues from an array of strings. /// - /// The string array to convert to RedisValues + /// The string array to convert to RedisValues. public static RedisValue[] ToRedisValueArray(this string[] values) { if (values == null) return null; @@ -138,7 +138,7 @@ public static RedisValue[] ToRedisValueArray(this string[] values) } /// - /// Create an array of strings from an array of values + /// Create an array of strings from an array of values. /// /// The values to convert to an array. public static string[] ToStringArray(this RedisValue[] values) @@ -167,10 +167,10 @@ private static void AuthenticateAsClientUsingDefaultProtocols(SslStream ssl, str } /// - /// Represent a byte-Lease as a read-only Stream + /// Represent a byte-Lease as a read-only Stream. /// - /// The lease upon which to base the stream - /// If true, disposing the stream also disposes the lease + /// The lease upon which to base the stream. + /// If true, disposing the stream also disposes the lease. public static Stream AsStream(this Lease bytes, bool ownsLease = true) { if (bytes == null) return null; // GIGO @@ -180,10 +180,10 @@ public static Stream AsStream(this Lease bytes, bool ownsLease = true) } /// - /// Decode a byte-Lease as a String, optionally specifying the encoding (UTF-8 if omitted) + /// Decode a byte-Lease as a String, optionally specifying the encoding (UTF-8 if omitted). /// - /// The bytes to decode - /// The encoding to use + /// The bytes to decode. + /// The encoding to use. public static string DecodeString(this Lease bytes, Encoding encoding = null) { if (bytes == null) return null; @@ -194,10 +194,10 @@ public static string DecodeString(this Lease bytes, Encoding encoding = nu } /// - /// Decode a byte-Lease as a String, optionally specifying the encoding (UTF-8 if omitted) + /// Decode a byte-Lease as a String, optionally specifying the encoding (UTF-8 if omitted). /// - /// The bytes to decode - /// The encoding to use + /// The bytes to decode. + /// The encoding to use. public static Lease DecodeLease(this Lease bytes, Encoding encoding = null) { if (bytes == null) return null; diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index b5600c97a..63f5ad644 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -2042,6 +2042,5 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The number of keys that were touched. /// https://redis.io/commands/touch Task KeyTouchAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); - } } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs index 3eb4f0678..077f8daad 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs @@ -26,7 +26,7 @@ public static class DatabaseExtensions /// /// The following methods are not supported in a key space isolated database and /// will throw an when invoked: - /// + /// /// /// /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 7c0e7e436..81d8f9b6b 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -882,7 +882,6 @@ public Task PingAsync(CommandFlags flags = CommandFlags.None) return Inner.PingAsync(flags); } - public Task KeyTouchAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) { return Inner.KeyTouchAsync(ToInner(keys), flags); diff --git a/src/StackExchange.Redis/LuaScript.cs b/src/StackExchange.Redis/LuaScript.cs index a88340b95..bcf3c673f 100644 --- a/src/StackExchange.Redis/LuaScript.cs +++ b/src/StackExchange.Redis/LuaScript.cs @@ -12,7 +12,7 @@ namespace StackExchange.Redis /// Public fields and properties of the passed in object are treated as parameters. /// /// - /// Parameters of type RedisKey are sent to Redis as KEY (https://redis.io/commands/eval) in addition to arguments, + /// Parameters of type RedisKey are sent to Redis as KEY (https://redis.io/commands/eval) in addition to arguments, /// so as to play nicely with Redis Cluster. /// /// All members of this class are thread safe. @@ -218,7 +218,7 @@ public async Task LoadAsync(IServer server, CommandFlags flags /// Public fields and properties of the passed in object are treated as parameters. /// /// - /// Parameters of type RedisKey are sent to Redis as KEY (https://redis.io/commands/eval) in addition to arguments, + /// Parameters of type RedisKey are sent to Redis as KEY (https://redis.io/commands/eval) in addition to arguments, /// so as to play nicely with Redis Cluster. /// /// All members of this class are thread safe. diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 22260abee..f14cf7b10 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -261,6 +261,7 @@ private async Task ExecuteSubscriptionLoop() // pushes items that have been enqu { try { + // Treat these commands as background/handshake and do not allow queueing to backlog if ((await TryWriteAsync(next.Message, next.IsReplica).ForAwait()) != WriteResult.Success) { next.Abort(); @@ -307,7 +308,7 @@ internal readonly struct BridgeStatus public PhysicalConnection.ConnectionStatus Connection { get; init; } /// - /// The default bridge stats, notable *not* the same as default since initializers don't run. + /// The default bridge stats, notable *not* the same as default since initializers don't run. /// public static BridgeStatus Zero { get; } = new() { Connection = PhysicalConnection.ConnectionStatus.Zero }; } @@ -753,7 +754,6 @@ internal WriteResult WriteMessageTakingWriteLockSync(PhysicalConnection physical UnmarkActiveMessage(message); token.Dispose(); } - } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1019,7 +1019,6 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect LockToken token = default; try { - // try to acquire it synchronously // note: timeout is specified in mutex-constructor token = _singleWriterMutex.TryWait(options: WaitOptions.NoDelay); @@ -1054,7 +1053,7 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect result = flush.Result; // we know it was completed, this is fine } - + physical.SetIdle(); return new ValueTask(result); @@ -1079,7 +1078,6 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect #if DEBUG private void RecordLockDuration(int lockTaken) { - var lockDuration = unchecked(Environment.TickCount - lockTaken); if (lockDuration > _maxLockDuration) _maxLockDuration = lockDuration; } @@ -1102,7 +1100,7 @@ private async ValueTask WriteMessageTakingWriteLockAsync_Awaited(Va { result = await physical.FlushAsync(false).ForAwait(); } - + physical.SetIdle(); #if DEBUG diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 7b0096edc..f15940e84 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -180,7 +180,7 @@ internal async Task BeginConnectAsync(LogProxy log) } } } - catch (NotImplementedException ex) when (!(endpoint is IPEndPoint)) + catch (NotImplementedException ex) when (endpoint is not IPEndPoint) { throw new InvalidOperationException("BeginConnect failed with NotImplementedException; consider using IP endpoints, or enable ResolveDns in the configuration", ex); } @@ -779,7 +779,6 @@ internal static void WriteCrlf(PipeWriter writer) writer.Advance(2); } - internal static int WriteRaw(Span span, long value, bool withLengthPrefix = false, int offset = 0) { if (value >= 0 && value <= 9) @@ -967,7 +966,6 @@ internal ValueTask FlushAsync(bool throwOnFailure, CancellationToke #if DEBUG private void RecordEndFlush(int start, long bytes) { - var end = Environment.TickCount; int taken = unchecked(end - start); if (taken > _maxFlushTime) @@ -1339,7 +1337,7 @@ public ConnectionStatus GetStatus() // Fall back to bytes waiting on the socket if we can int fallbackBytesAvailable; try - { + { fallbackBytesAvailable = VolatileSocket?.Available ?? -1; } catch diff --git a/src/StackExchange.Redis/Profiling/IProfiledCommand.cs b/src/StackExchange.Redis/Profiling/IProfiledCommand.cs index c89b039cc..d088d4ab5 100644 --- a/src/StackExchange.Redis/Profiling/IProfiledCommand.cs +++ b/src/StackExchange.Redis/Profiling/IProfiledCommand.cs @@ -85,7 +85,7 @@ public interface IProfiledCommand /// /// If RetransmissionOf is not null, this property will be set to either Ask or Moved to indicate /// what sort of response triggered the retransmission. - /// + /// /// This can be useful for determining the root cause of extra commands. /// RetransmissionReasonType? RetransmissionReason { get; } diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 1656eb785..e8b54c2b1 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -450,7 +450,6 @@ public long HashStringLength(RedisKey key, RedisValue hashField, CommandFlags fl return ExecuteSync(msg, ResultProcessor.Int64); } - public Task HashSetAsync(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) { WhenAlwaysOrNotExists(when); diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 8913aef74..05e5b7af6 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -953,7 +953,6 @@ private static Message LatencyResetCommand(string[] eventNames, CommandFlags fla for (int i = 0; i < eventNames.Length; i++) arr[i + 1] = eventNames[i]; return Message.Create(-1, flags, RedisCommand.LATENCY, arr); - } } public Task LatencyResetAsync(string[] eventNames = null, CommandFlags flags = CommandFlags.None) diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index 8884dc263..22b9664b7 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -218,6 +218,7 @@ public bool Remove(Action handler, ChannelMessageQueue return _handlers == null & _queues == null; } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "RCS1210:Return completed task instead of returning null.", Justification = "Intentional for efficient success check")] public Task SubscribeToServer(ConnectionMultiplexer multiplexer, in RedisChannel channel, CommandFlags flags, object asyncState, bool internalCall) { var selected = multiplexer.SelectServer(RedisCommand.SUBSCRIBE, flags, default(RedisKey)); @@ -245,6 +246,7 @@ public Task SubscribeToServer(ConnectionMultiplexer multiplexer, in RedisChannel } } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "RCS1210:Return completed task instead of returning null.", Justification = "Intentional for efficient success check")] public Task UnsubscribeFromServer(in RedisChannel channel, CommandFlags flags, object asyncState, bool internalCall) { var oldOwner = Interlocked.Exchange(ref owner, null); diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index e78411a00..88b2a241b 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -309,7 +309,6 @@ internal static unsafe int GetHashCode(ReadOnlyMemory memory) return acc; } } - internal void AssertNotNull() { diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index dd25cfc8d..af3a91dad 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -1427,7 +1427,7 @@ private static Role ParseMaster(in Sequence items) return null; } } - } + } return new Role.Master(offset, replicas); } @@ -2267,7 +2267,7 @@ private sealed class SentinelArrayOfArraysProcessor : ResultProcessor /// - /// The created Func takes a RedisKey, which will be prefixed to all keys (and arguments of type RedisKey) for + /// The created Func takes a RedisKey, which will be prefixed to all keys (and arguments of type RedisKey) for /// keyspace isolation. /// /// diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index a76f5ca3e..afb67349a 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -570,7 +570,7 @@ private static async Task OnEstablishingAsyncAwaited(PhysicalConnection connecti connection.RecordConnectionFailed(ConnectionFailureType.InternalFailure, ex); } } - + internal void OnFullyEstablished(PhysicalConnection connection, string source) { try @@ -587,7 +587,7 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) CompletePendingConnectionMonitors(source); } - Multiplexer.OnConnectionRestored(EndPoint, bridge.ConnectionType, connection?.ToString()); + Multiplexer.OnConnectionRestored(EndPoint, bridge.ConnectionType, connection?.ToString()); } } catch (Exception ex) @@ -631,7 +631,6 @@ internal bool CheckInfoReplication() [ThreadStatic] private static Random r; - // Forces frequent replication check starting from 1 second up to max ConfigCheckSeconds with an exponential increment internal void ForceExponentialBackoffReplicationCheck() { diff --git a/src/StackExchange.Redis/StreamConstants.cs b/src/StackExchange.Redis/StreamConstants.cs index 23addbf3f..409653c72 100644 --- a/src/StackExchange.Redis/StreamConstants.cs +++ b/src/StackExchange.Redis/StreamConstants.cs @@ -62,7 +62,7 @@ internal static class StreamConstants internal static readonly RedisValue MaxLen = "MAXLEN"; internal static readonly RedisValue MkStream = "MKSTREAM"; - + internal static readonly RedisValue NoAck = "NOACK"; internal static readonly RedisValue Stream = "STREAM"; diff --git a/src/StackExchange.Redis/StreamPendingMessageInfo.cs b/src/StackExchange.Redis/StreamPendingMessageInfo.cs index b1c87f296..84241116d 100644 --- a/src/StackExchange.Redis/StreamPendingMessageInfo.cs +++ b/src/StackExchange.Redis/StreamPendingMessageInfo.cs @@ -2,7 +2,7 @@ namespace StackExchange.Redis { /// - /// Describes properties of a pending message. A pending message is one that has + /// Describes properties of a pending message. A pending message is one that has /// been received by a consumer but has not yet been acknowledged. /// public readonly struct StreamPendingMessageInfo diff --git a/tests/StackExchange.Redis.Tests/BoxUnbox.cs b/tests/StackExchange.Redis.Tests/BoxUnbox.cs index c6e012372..8f0c31064 100644 --- a/tests/StackExchange.Redis.Tests/BoxUnbox.cs +++ b/tests/StackExchange.Redis.Tests/BoxUnbox.cs @@ -35,7 +35,7 @@ public void ReturnInternedBoxesForCommonValues(RedisValue value, bool expectSame AssertEqualGiveOrTakeNaN(value, RedisValue.Unbox(y)); } - static void AssertEqualGiveOrTakeNaN(RedisValue expected, RedisValue actual) + private static void AssertEqualGiveOrTakeNaN(RedisValue expected, RedisValue actual) { if (expected.Type == RedisValue.StorageType.Double && actual.Type == expected.Type) { diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index 201af69c9..fdac155fd 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -495,7 +495,6 @@ public void ConfigStringInvalidOptionErrorGiveMeaningfulMessages() Assert.Equal("flibble", ex.ParamName); } - [Fact] public void NullApply() { diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index fbce955c8..eac2aa3f5 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -9,7 +9,6 @@ namespace StackExchange.Redis.Tests { - [CollectionDefinition(nameof(MoqDependentCollection), DisableParallelization = true)] public class MoqDependentCollection { } diff --git a/tests/StackExchange.Redis.Tests/Failover.cs b/tests/StackExchange.Redis.Tests/Failover.cs index ef0cb6899..1aa26e237 100644 --- a/tests/StackExchange.Redis.Tests/Failover.cs +++ b/tests/StackExchange.Redis.Tests/Failover.cs @@ -110,7 +110,6 @@ public async Task ConfigVerifyReceiveConfigChangeBroadcast() } } - [Fact] public async Task DereplicateGoesToPrimary() { diff --git a/tests/StackExchange.Redis.Tests/Hashes.cs b/tests/StackExchange.Redis.Tests/Hashes.cs index f1c37741c..415146f65 100644 --- a/tests/StackExchange.Redis.Tests/Hashes.cs +++ b/tests/StackExchange.Redis.Tests/Hashes.cs @@ -84,7 +84,6 @@ public async Task ScanAsync() count++; } Assert.Equal(200, count); - } } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs b/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs index 9c1247b7f..aebf76648 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs @@ -12,7 +12,7 @@ public class Issue1101 : TestBase { public Issue1101(ITestOutputHelper output) : base(output) { } - static void AssertCounts(ISubscriber pubsub, in RedisChannel channel, + private static void AssertCounts(ISubscriber pubsub, in RedisChannel channel, bool has, int handlers, int queues) { var aHas = ((RedisSubscriber)pubsub).GetSubscriberCounts(channel, out var ah, out var aq); @@ -79,7 +79,6 @@ public async Task ExecuteWithUnsubscribeViaChannel() Assert.True(second.Completion.IsCompleted, "completed"); AssertCounts(pubsub, name, false, 0, 0); - subs = muxer.GetServer(muxer.GetEndPoints().Single()).SubscriptionSubscriberCount(name); Assert.Equal(0, subs); Assert.True(first.Completion.IsCompleted, "completed"); diff --git a/tests/StackExchange.Redis.Tests/Latency.cs b/tests/StackExchange.Redis.Tests/Latency.cs index 89f377f07..d3eafcf5c 100644 --- a/tests/StackExchange.Redis.Tests/Latency.cs +++ b/tests/StackExchange.Redis.Tests/Latency.cs @@ -7,7 +7,6 @@ namespace StackExchange.Redis.Tests [Collection(SharedConnectionFixture.Key)] public class Latency : TestBase { - public Latency(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] diff --git a/tests/StackExchange.Redis.Tests/PubSubCommand.cs b/tests/StackExchange.Redis.Tests/PubSubCommand.cs index d763dbb4c..45d17a921 100644 --- a/tests/StackExchange.Redis.Tests/PubSubCommand.cs +++ b/tests/StackExchange.Redis.Tests/PubSubCommand.cs @@ -58,7 +58,7 @@ public async Task SubscriberCountAsync() } } } - static class Util + internal static class Util { public static async Task WithTimeout(this Task task, int timeoutMs, [CallerMemberName] string caller = null, [CallerLineNumber] int line = 0) diff --git a/tests/StackExchange.Redis.Tests/SSL.cs b/tests/StackExchange.Redis.Tests/SSL.cs index 634e9b5cc..e8b8e5a23 100644 --- a/tests/StackExchange.Redis.Tests/SSL.cs +++ b/tests/StackExchange.Redis.Tests/SSL.cs @@ -464,7 +464,7 @@ public void SSLParseViaConfig_Issue883_ConfigString() [Fact] public void ConfigObject_Issue1407_ToStringIncludesSslProtocols() { - var sslProtocols = SslProtocols.Tls12 | SslProtocols.Tls; + const SslProtocols sslProtocols = SslProtocols.Tls12 | SslProtocols.Tls; var sourceOptions = new ConfigurationOptions { AbortOnConnectFail = false, diff --git a/tests/StackExchange.Redis.Tests/Scripting.cs b/tests/StackExchange.Redis.Tests/Scripting.cs index 787f4f4f3..62b93f60f 100644 --- a/tests/StackExchange.Redis.Tests/Scripting.cs +++ b/tests/StackExchange.Redis.Tests/Scripting.cs @@ -634,7 +634,6 @@ public void SimpleRawScriptEvaluate() } } - [Fact] public void LuaScriptWithKeys() { @@ -900,7 +899,7 @@ public void LuaScriptWithWrappedDatabase() { Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); var db = conn.GetDatabase(); - var wrappedDb = DatabaseExtensions.WithKeyPrefix(db, "prefix-"); + var wrappedDb = db.WithKeyPrefix("prefix-"); var key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -926,7 +925,7 @@ public async Task AsyncLuaScriptWithWrappedDatabase() { Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); var db = conn.GetDatabase(); - var wrappedDb = DatabaseExtensions.WithKeyPrefix(db, "prefix-"); + var wrappedDb = db.WithKeyPrefix("prefix-"); var key = Me(); await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); @@ -952,7 +951,7 @@ public void LoadedLuaScriptWithWrappedDatabase() { Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); var db = conn.GetDatabase(); - var wrappedDb = DatabaseExtensions.WithKeyPrefix(db, "prefix2-"); + var wrappedDb = db.WithKeyPrefix("prefix2-"); var key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -979,7 +978,7 @@ public async Task AsyncLoadedLuaScriptWithWrappedDatabase() { Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); var db = conn.GetDatabase(); - var wrappedDb = DatabaseExtensions.WithKeyPrefix(db, "prefix2-"); + var wrappedDb = db.WithKeyPrefix("prefix2-"); var key = Me(); await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); @@ -1047,10 +1046,10 @@ public void ScriptWithKeyPrefixCompare() { var p = conn.GetDatabase().WithKeyPrefix("prefix/"); var args = new { k = (RedisKey)"key", s = "str", v = 123 }; - LuaScript lua = LuaScript.Prepare(@"return {@k, @s, @v}"); + LuaScript lua = LuaScript.Prepare("return {@k, @s, @v}"); var viaArgs = (RedisValue[])p.ScriptEvaluate(lua, args); - var viaArr = (RedisValue[])p.ScriptEvaluate(@"return {KEYS[1], ARGV[1], ARGV[2]}", new[] { args.k }, new RedisValue[] { args.s, args.v }); + var viaArr = (RedisValue[])p.ScriptEvaluate("return {KEYS[1], ARGV[1], ARGV[2]}", new[] { args.k }, new RedisValue[] { args.s, args.v }); Assert.Equal(string.Join(",", viaArr), string.Join(",", viaArgs)); } } @@ -1060,7 +1059,7 @@ public void ScriptWithKeyPrefixCompare() [Fact] public void RedisResultUnderstandsNullArrayNull() => TestNullArray(null); - static void TestNullArray(RedisResult value) + private static void TestNullArray(RedisResult value) { Assert.True(value == null || value.IsNull); @@ -1081,7 +1080,7 @@ static void TestNullArray(RedisResult value) [Fact] public void RedisResultUnderstandsNullValue() => TestNullValue(RedisResult.Create(RedisValue.Null, ResultType.None)); - static void TestNullValue(RedisResult value) + private static void TestNullValue(RedisResult value) { Assert.True(value == null || value.IsNull); diff --git a/tests/StackExchange.Redis.Tests/SentinelFailover.cs b/tests/StackExchange.Redis.Tests/SentinelFailover.cs index 9b5b5932c..0f1bb72cc 100644 --- a/tests/StackExchange.Redis.Tests/SentinelFailover.cs +++ b/tests/StackExchange.Redis.Tests/SentinelFailover.cs @@ -55,7 +55,7 @@ public async Task ManagedMasterConnectionEndToEndWithFailoverTest() await WaitForReplicationAsync(servers.First()).ForAwait(); value = await db.StringGetAsync(key, CommandFlags.DemandReplica); Assert.Equal(expected, value); - + Log("Waiting for ready pre-failover..."); await WaitForReadyAsync(); diff --git a/tests/StackExchange.Redis.Tests/Streams.cs b/tests/StackExchange.Redis.Tests/Streams.cs index 22a0fa944..b81949b55 100644 --- a/tests/StackExchange.Redis.Tests/Streams.cs +++ b/tests/StackExchange.Redis.Tests/Streams.cs @@ -1794,7 +1794,6 @@ public void StreamReadGroupMultiStreamWithNoAckShowsNoPendingMessages() private RedisKey GetUniqueKey(string type) => $"{type}_stream_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; - [Fact] public async Task StreamReadIndexerUsage() { @@ -1821,6 +1820,5 @@ await db.StreamAddAsync(streamName, new[] { Assert.Equal("test", (string)obj.name); } } - } } From 35d3e9c7c578ebf3e22a032ddbf2b66c6435bb34 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 5 Jan 2022 12:00:05 -0500 Subject: [PATCH 052/435] Tiebreakers: move into the handshake (#1931) Currently the way we handshake is to get everything configured, wait for a tracer to complete, and then issue the tiebreakers to all servers if they are in play. This complicates a few things with respect to timings, duplication, and write paths being a one-off for tie breakers, which I tripped on hard in #1912. In this, we instead move the tie breaker fetch as part of AutoConfigure as a fire-and-forget-process-the-result-later setup with a dedicated processor. This all happens before the tracer fires moving us to the next connection phase (added comments) so we should be safe. It should reduce both complexity and overall connection times proportional to endpoint latency (since we wait for completion right now). What needs adding here is tests with us disabling commands like INFO, GET, etc. and ensuring things still behave as we want. In the overall, the tie breaker is slightly less isolated but _should_ be happening in the same order and with the same exception if any - no net result change is intended there with respect to how we do or don't error along the way. But we never want a connection to fail _because of a tiebreaker_ and I think that warrants a few tests: - [x] Disable `INFO` and see if we can connect - [x] Disable `GET` and see if we can connect - [x] Store some invalid TieBreaker and see if we can connect (e.g. make it a hash instead of a string) --- docs/ReleaseNotes.md | 1 + .../ConnectionMultiplexer.cs | 71 ++++------------- src/StackExchange.Redis/ResultProcessor.cs | 28 +++++++ src/StackExchange.Redis/ServerEndPoint.cs | 16 ++++ .../ConnectCustomConfig.cs | 77 +++++++++++++++++++ 5 files changed, 138 insertions(+), 55 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index c3b401a47..96f86de19 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,6 +6,7 @@ - Fix integer overflow error (issue #1926) with 2GiB+ result payloads (#1928 via mgravell) - Update assumed redis versions to v2.8 or v4.0 in the Azure case (#1929 via NickCraver) - Fix profiler showing `EVAL` instead `EVALSHA` (#1930 via martinpotter) +- Moved tiebreaker fetching in connections into the handshake phase (streamline + simplification) (#1931 via NickCraver) ## 2.2.88 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index f265b8e93..055ea3d12 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1718,20 +1718,16 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP } int standaloneCount = 0, clusterCount = 0, sentinelCount = 0; var endpoints = RawConfig.EndPoints; - log?.WriteLine($"{endpoints.Count} unique nodes specified"); + bool useTieBreakers = !string.IsNullOrWhiteSpace(RawConfig.TieBreaker); + log?.WriteLine($"{endpoints.Count} unique nodes specified ({(useTieBreakers ? "with" : "without")} tiebreaker)"); if (endpoints.Count == 0) { throw new InvalidOperationException("No nodes to consider"); } -#pragma warning disable CS0618 - const CommandFlags flags = CommandFlags.NoRedirect | CommandFlags.HighPriority; -#pragma warning restore CS0618 List masters = new List(endpoints.Count); - bool useTieBreakers = !string.IsNullOrWhiteSpace(RawConfig.TieBreaker); ServerEndPoint[] servers = null; - Task[] tieBreakers = null; bool encounteredConnectedClusterServer = false; Stopwatch watch = null; @@ -1747,7 +1743,6 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP if (endpoints == null) break; var available = new Task[endpoints.Count]; - tieBreakers = useTieBreakers ? new Task[endpoints.Count] : null; servers = new ServerEndPoint[available.Length]; RedisKey tieBreakerKey = useTieBreakers ? (RedisKey)RawConfig.TieBreaker : default(RedisKey); @@ -1790,22 +1785,6 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP log?.WriteLine($"{Format.ToString(server.EndPoint)}: Endpoint is {server.ConnectionState}"); } - // After we've successfully connected (and authenticated), kickoff tie breakers if needed - if (useTieBreakers) - { - log?.WriteLine("Election: Gathering tie-breakers..."); - for (int i = 0; i < available.Length; i++) - { - var server = servers[i]; - - log?.WriteLine($"{Format.ToString(server.EndPoint)}: Requesting tie-break (Key=\"{RawConfig.TieBreaker}\")..."); - Message msg = Message.Create(0, flags, RedisCommand.GET, tieBreakerKey); - msg.SetInternalCall(); - msg = LoggingMessage.Create(log, msg); - tieBreakers[i] = server.WriteDirectAsync(msg, ResultProcessor.String); - } - } - EndPointCollection updatedClusterEndpointCollection = null; for (int i = 0; i < available.Length; i++) { @@ -1919,7 +1898,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP ServerSelectionStrategy.ServerType = ServerType.Standalone; } - var preferred = await NominatePreferredMaster(log, servers, useTieBreakers, tieBreakers, masters, timeoutMs: RawConfig.ConnectTimeout - checked((int)watch.ElapsedMilliseconds)).ObserveErrors().ForAwait(); + var preferred = NominatePreferredMaster(log, servers, useTieBreakers, masters); foreach (var master in masters) { if (master == preferred || master.IsReplica) @@ -2048,44 +2027,26 @@ private void ResetAllNonConnected() [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Partial - may use instance data")] partial void OnTraceLog(LogProxy log, [CallerMemberName] string caller = null); - private static async Task NominatePreferredMaster(LogProxy log, ServerEndPoint[] servers, bool useTieBreakers, Task[] tieBreakers, List masters, int timeoutMs) + private static ServerEndPoint NominatePreferredMaster(LogProxy log, ServerEndPoint[] servers, bool useTieBreakers, List masters) { Dictionary uniques = null; if (useTieBreakers) { // count the votes uniques = new Dictionary(StringComparer.OrdinalIgnoreCase); - log?.WriteLine("Waiting for tiebreakers..."); - await WaitAllIgnoreErrorsAsync("tiebreaker", tieBreakers, Math.Max(timeoutMs, 200), log).ForAwait(); - for (int i = 0; i < tieBreakers.Length; i++) + for (int i = 0; i < servers.Length; i++) { - var ep = servers[i].EndPoint; - var status = tieBreakers[i].Status; - switch (status) + var server = servers[i]; + string serverResult = server.TieBreakerResult; + + if (string.IsNullOrWhiteSpace(serverResult)) { - case TaskStatus.RanToCompletion: - string s = tieBreakers[i].Result; - if (string.IsNullOrWhiteSpace(s)) - { - log?.WriteLine($"Election: {Format.ToString(ep)} had no tiebreaker set"); - } - else - { - log?.WriteLine($"Election: {Format.ToString(ep)} nominates: {s}"); - if (!uniques.TryGetValue(s, out int count)) count = 0; - uniques[s] = count + 1; - } - break; - case TaskStatus.Faulted: - log?.WriteLine($"Election: {Format.ToString(ep)} failed to nominate ({status})"); - foreach (var ex in tieBreakers[i].Exception.InnerExceptions) - { - if (ex.Message.StartsWith("MOVED ") || ex.Message.StartsWith("ASK ")) continue; - log?.WriteLine("> " + ex.Message); - } - break; - default: - log?.WriteLine($"Election: {Format.ToString(ep)} failed to nominate ({status})"); - break; + log?.WriteLine($"Election: {Format.ToString(server)} had no tiebreaker set"); + } + else + { + log?.WriteLine($"Election: {Format.ToString(server)} nominates: {serverResult}"); + if (!uniques.TryGetValue(serverResult, out int count)) count = 0; + uniques[serverResult] = count + 1; } } } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index af3a91dad..d17a1902a 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -121,6 +121,7 @@ public static readonly StreamPendingMessagesProcessor public static readonly ResultProcessor String = new StringProcessor(), + TieBreaker = new TieBreakerProcessor(), ClusterNodesRaw = new ClusterNodesRawProcessor(); #region Sentinel @@ -2068,6 +2069,32 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + private sealed class TieBreakerProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Type) + { + case ResultType.SimpleString: + case ResultType.BulkString: + var tieBreaker = result.GetString(); + SetResult(message, tieBreaker); + + try + { + if (connection.BridgeCouldBeNull?.ServerEndPoint is ServerEndPoint endpoint) + { + endpoint.TieBreakerResult = tieBreaker; + } + } + catch { } + + return true; + } + return false; + } + } + private class TracerProcessor : ResultProcessor { private readonly bool establishConnection; @@ -2146,6 +2173,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { if (establishConnection) { + // This is what ultimately brings us to complete a connection, by advancing the state forward from a successful tracer after connection. connection.BridgeCouldBeNull?.OnFullyEstablished(connection, $"From command: {message.Command}"); } SetResult(message, happy); diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index afb67349a..f41e360a3 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -375,6 +375,17 @@ internal async Task AutoConfigureAsync(PhysicalConnection connection, LogProxy l msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.ClusterNodes).ForAwait(); } + // If we are going to fetch a tie breaker, do so last and we'll get it in before the tracer fires completing the connection + // But if GETs are disabled on this, do not fail the connection - we just don't get tiebreaker benefits + if (!string.IsNullOrEmpty(Multiplexer.RawConfig.TieBreaker) && Multiplexer.RawConfig.CommandMap.IsAvailable(RedisCommand.GET)) + { + RedisKey tieBreakerKey = Multiplexer.RawConfig.TieBreaker; + log?.WriteLine($"{Format.ToString(EndPoint)}: Requesting tie-break (Key=\"{tieBreakerKey}\")..."); + msg = Message.Create(0, flags, RedisCommand.GET, tieBreakerKey); + msg.SetInternalCall(); + msg = LoggingMessage.Create(log, msg); + await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.TieBreaker).ForAwait(); + } } private int _nextReplicaOffset; @@ -608,6 +619,11 @@ public EndPoint MasterEndPoint set { SetConfig(ref masterEndPoint, value); } } + /// + /// Result of the latest tie breaker (from the last reconfigure). + /// + internal string TieBreakerResult { get; set; } + internal bool CheckInfoReplication() { lastInfoReplicationCheckTicks = Environment.TickCount; diff --git a/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs b/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs new file mode 100644 index 000000000..2bed55caa --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs @@ -0,0 +1,77 @@ +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests +{ + public class ConnectCustomConfig : TestBase + { + public ConnectCustomConfig(ITestOutputHelper output) : base (output) { } + + // So we're triggering tiebreakers here + protected override string GetConfiguration() => TestConfig.Current.MasterServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + + [Theory] + [InlineData("config")] + [InlineData("info")] + [InlineData("get")] + [InlineData("config,get")] + [InlineData("info,get")] + [InlineData("config,info,get")] + public void DisabledCommandsStillConnect(string disabledCommands) + { + using var muxer = Create(allowAdmin: true, disabledCommands: disabledCommands.Split(','), log: Writer); + + var db = muxer.GetDatabase(); + db.Ping(); + Assert.True(db.IsConnected(default(RedisKey))); + } + + [Fact] + public void TieBreakerIntact() + { + using var muxer = Create(allowAdmin: true, log: Writer) as ConnectionMultiplexer; + + var tiebreaker = muxer.GetDatabase().StringGet(muxer.RawConfig.TieBreaker); + Log($"Tiebreaker: {tiebreaker}"); + + var snapshot = muxer.GetServerSnapshot(); + foreach (var server in snapshot) + { + Assert.Equal(tiebreaker, server.TieBreakerResult); + } + } + + [Fact] + public void TieBreakerSkips() + { + using var muxer = Create(allowAdmin: true, disabledCommands: new[] { "get" }, log: Writer) as ConnectionMultiplexer; + Assert.Throws(() => muxer.GetDatabase().StringGet(muxer.RawConfig.TieBreaker)); + + var snapshot = muxer.GetServerSnapshot(); + foreach (var server in snapshot) + { + Assert.True(server.IsConnected); + Assert.Null(server.TieBreakerResult); + } + } + + [Fact] + public void TiebreakerIncorrectType() + { + var tiebreakerKey = Me(); + using var fubarMuxer = Create(allowAdmin: true, log: Writer); + // Store something nonsensical in the tiebreaker key: + fubarMuxer.GetDatabase().HashSet(tiebreakerKey, "foo", "bar"); + + // Ensure the next connection getting an invalid type still connects + using var muxer = Create(allowAdmin: true, tieBreaker: tiebreakerKey, log: Writer); + + var db = muxer.GetDatabase(); + db.Ping(); + Assert.True(db.IsConnected(default(RedisKey))); + + var ex = Assert.Throws(() => db.StringGet(tiebreakerKey)); + Assert.Contains("WRONGTYPE", ex.Message); + } + } +} From 239e34591df21af2b329bef902592fb6e9608945 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 5 Jan 2022 12:48:32 -0500 Subject: [PATCH 053/435] v2.5 work: .NET 6.0: add build defaulting to the thread pool (#1939) This adds a `net6.0` build (and test run) that uses the default thread pool for pipe scheduling rather than our scheduler. Namely due to 2 major changes since this was introduced: 1. Thread theft has long since been resolved (never an issue in .NET Core+) 2. Sync-over-async is an app problem, but should be much better in .NET 6.0+ which has changes specifically for this in thread pool growth (see https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/#threading) Due to this combo of changes, we want to see if this behaves much better in the next alpha. The advantages are we move from 10 threads by default shared between connections to no dedicated threads for the scheduler. It has the following benefits: 1. While these threads were mostly idle (waiting for data), people saw them in profiler traces and attribute them as working/CPU-consuming though that's not what a profiler is really saying. 2. The default of 10 threads is a best guess, but the world varies widely. Some users are deploying many connections on 20-100 core VMs and it's a bad default limiting them (by default - not if configured). On the other end of the spectrum, a _lot_ of people run small 1-4 core VMs or containers and the default size is bigger than needed. Instead of the above tradeoffs meant to unblock users, want to simplify, let the main thread pool scale, and hope the default is a net win for most or all users. If a consumer application has a _ton_ of sync-over-async (e.g. 100,000 tasks queued suddenly), this may be a net negative change for them - that's why we want to alpha and see how this behaves with workloads we may not have predicted. --- .github/workflows/CI.yml | 20 ++++++++--------- appveyor.yml | 4 +++- src/StackExchange.Redis/CommandTrace.cs | 4 ++-- .../ConnectionMultiplexer.ReaderWriter.cs | 22 +++++++++++++++++-- .../StackExchange.Redis.csproj | 2 +- tests/StackExchange.Redis.Tests/Config.cs | 2 +- tests/StackExchange.Redis.Tests/Migrate.cs | 2 +- tests/StackExchange.Redis.Tests/PubSub.cs | 2 +- .../StackExchange.Redis.Tests.csproj | 2 +- tests/StackExchange.Redis.Tests/TestBase.cs | 11 +++------- version.json | 2 +- 11 files changed, 43 insertions(+), 30 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5dacdc4d2..d8687ed74 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -16,14 +16,13 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v1 - - name: Setup .NET Core 3.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '3.1.x' - - name: Setup .NET 5.x + - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: '5.0.x' + dotnet-version: | + 3.1.x + 5.0.x + 6.0.x - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true - name: Start Redis Services (docker-compose) @@ -50,11 +49,10 @@ jobs: - name: Setup .NET Core 3.x uses: actions/setup-dotnet@v1 with: - dotnet-version: '3.1.x' - - name: Setup .NET 5.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '5.0.x' + dotnet-version: | + 3.1.x + 5.0.x + 6.0.x - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true - name: Start Redis Services (v3.0.503) diff --git a/appveyor.yml b/appveyor.yml index 2cbf38d46..51d5f56a4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,7 +6,9 @@ init: install: - cmd: >- - choco install dotnet-sdk --version 5.0.100 + choco install dotnet-sdk --version 5.0.404 + + choco install dotnet-sdk --version 6.0.101 cd tests\RedisConfigs\3.0.503 diff --git a/src/StackExchange.Redis/CommandTrace.cs b/src/StackExchange.Redis/CommandTrace.cs index 7d366c350..956b7a098 100644 --- a/src/StackExchange.Redis/CommandTrace.cs +++ b/src/StackExchange.Redis/CommandTrace.cs @@ -50,7 +50,7 @@ public string GetHelpUrl() const string BaseUrl = "https://redis.io/commands/"; - string encoded0 = Uri.EscapeUriString(((string)Arguments[0]).ToLowerInvariant()); + string encoded0 = Uri.EscapeDataString(((string)Arguments[0]).ToLowerInvariant()); if (Arguments.Length > 1) { @@ -62,7 +62,7 @@ public string GetHelpUrl() case "config": case "debug": case "pubsub": - string encoded1 = Uri.EscapeUriString(((string)Arguments[1]).ToLowerInvariant()); + string encoded1 = Uri.EscapeDataString(((string)Arguments[1]).ToLowerInvariant()); return BaseUrl + encoded0 + "-" + encoded1; } } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs b/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs index f0e9fa6b1..6a298b4f0 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs @@ -1,4 +1,6 @@ -namespace StackExchange.Redis +using System; + +namespace StackExchange.Redis { public partial class ConnectionMultiplexer { @@ -6,7 +8,7 @@ public partial class ConnectionMultiplexer partial void OnCreateReaderWriter(ConfigurationOptions configuration) { - SocketManager = configuration.SocketManager ?? SocketManager.Shared; + SocketManager = configuration.SocketManager ?? GetDefaultSocketManager(); } partial void OnCloseReaderWriter() @@ -14,5 +16,21 @@ partial void OnCloseReaderWriter() SocketManager = null; } partial void OnWriterCreated(); + + /// + /// .NET 6.0+ has changes to sync-over-async stalls in the .NET primary thread pool + /// If we're in that environment, by default remove the overhead of our own threadpool + /// This will eliminate some context-switching overhead and better-size threads on both large + /// and small environments, from 16 core machines to single core VMs where the default 10 threads + /// isn't an ideal situation. + /// + internal static SocketManager GetDefaultSocketManager() + { +#if NET6_0_OR_GREATER + return SocketManager.ThreadPool; +#else + return SocketManager.Shared; +#endif + } } } diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index a571adf1d..9f2fbb9cd 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -1,7 +1,7 @@  - net461;netstandard2.0;net472;netcoreapp3.1;net5.0 + net461;netstandard2.0;net472;netcoreapp3.1;net5.0;net6.0 High performance Redis client, incorporating both synchronous and asynchronous usage. StackExchange.Redis StackExchange.Redis diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index fdac155fd..a2d0d7034 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -457,7 +457,7 @@ public void DefaultThreadPoolManagerIsDetected() EndPoints = { { IPAddress.Loopback, 6379 } }, }; using var muxer = ConnectionMultiplexer.Connect(config); - Assert.Same(SocketManager.Shared.Scheduler, muxer.SocketManager.Scheduler); + Assert.Same(ConnectionMultiplexer.GetDefaultSocketManager().Scheduler, muxer.SocketManager.Scheduler); } [Theory] diff --git a/tests/StackExchange.Redis.Tests/Migrate.cs b/tests/StackExchange.Redis.Tests/Migrate.cs index 91bd373e3..73711129d 100644 --- a/tests/StackExchange.Redis.Tests/Migrate.cs +++ b/tests/StackExchange.Redis.Tests/Migrate.cs @@ -10,7 +10,7 @@ public class Migrate : TestBase { public Migrate(ITestOutputHelper output) : base (output) { } - [Fact] + [FactLongRunning] public async Task Basic() { var fromConfig = new ConfigurationOptions { EndPoints = { { TestConfig.Current.SecureServer, TestConfig.Current.SecurePort } }, Password = TestConfig.Current.SecurePassword, AllowAdmin = true }; diff --git a/tests/StackExchange.Redis.Tests/PubSub.cs b/tests/StackExchange.Redis.Tests/PubSub.cs index e4d22798d..0e4131913 100644 --- a/tests/StackExchange.Redis.Tests/PubSub.cs +++ b/tests/StackExchange.Redis.Tests/PubSub.cs @@ -643,7 +643,7 @@ public async Task Issue38() } } - internal static Task AllowReasonableTimeToPublishAndProcess() => Task.Delay(100); + internal static Task AllowReasonableTimeToPublishAndProcess() => Task.Delay(500); [Fact] public async Task TestPartialSubscriberGetMessage() diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index 184407fd9..5cf895e50 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -1,6 +1,6 @@  - net472;netcoreapp3.1 + net472;netcoreapp3.1;net6.0 StackExchange.Redis.Tests true true diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index c31a72178..21bf84f55 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -361,14 +361,7 @@ public static ConnectionMultiplexer CreateDefault( } public static string Me([CallerFilePath] string filePath = null, [CallerMemberName] string caller = null) => -#if NET472 - "net472-" -#elif NETCOREAPP3_1 - "netcoreapp3.1-" -#else - "unknown-" -#endif - + Path.GetFileNameWithoutExtension(filePath) + "-" + caller; + Environment.Version.ToString() + Path.GetFileNameWithoutExtension(filePath) + "-" + caller; protected static TimeSpan RunConcurrent(Action work, int threads, int timeout = 10000, [CallerMemberName] string caller = null) { @@ -417,7 +410,9 @@ void callback() for (int i = 0; i < threads; i++) { var thd = threadArr[i]; +#if !NET6_0_OR_GREATER if (thd.IsAlive) thd.Abort(); +#endif } throw new TimeoutException(); } diff --git a/version.json b/version.json index 4d664c308..8191d532a 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "2.2", + "version": "2.5-prerelease", "versionHeightOffset": -1, "assemblyVersion": "2.0", "publicReleaseRefSpec": [ From b4b1b56dde0c10f13b955c4f15dcbc3692fc52ca Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sun, 9 Jan 2022 22:24:42 -0500 Subject: [PATCH 054/435] Add test for #1578 (#1943) Can't repro this locally, adding to test suite. --- tests/StackExchange.Redis.Tests/Strings.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index fff5fdee7..30099cd22 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -166,12 +167,16 @@ public async Task SetNotExists() conn.KeyDelete(prefix + "1", CommandFlags.FireAndForget); conn.KeyDelete(prefix + "2", CommandFlags.FireAndForget); conn.KeyDelete(prefix + "3", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "4", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "5", CommandFlags.FireAndForget); conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); var x0 = conn.StringSetAsync(prefix + "1", "def", when: When.NotExists); var x1 = conn.StringSetAsync(prefix + "1", Encode("def"), when: When.NotExists); var x2 = conn.StringSetAsync(prefix + "2", "def", when: When.NotExists); var x3 = conn.StringSetAsync(prefix + "3", Encode("def"), when: When.NotExists); + var x4 = conn.StringSetAsync(prefix + "4", "def", expiry: TimeSpan.FromSeconds(4), when: When.NotExists); + var x5 = conn.StringSetAsync(prefix + "5", "def", expiry: TimeSpan.FromMilliseconds(4001), when: When.NotExists); var s0 = conn.StringGetAsync(prefix + "1"); var s2 = conn.StringGetAsync(prefix + "2"); @@ -181,6 +186,8 @@ public async Task SetNotExists() Assert.False(await x1); Assert.True(await x2); Assert.True(await x3); + Assert.True(await x4); + Assert.True(await x5); Assert.Equal("abc", await s0); Assert.Equal("def", await s2); Assert.Equal("def", await s3); From 9164304b3d3ed5b799cb5e970de553950034799a Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 10 Jan 2022 08:30:38 -0500 Subject: [PATCH 055/435] Changes for #1912 (#1945) Reducing diff from #1912 on bits we can simplify & merge in sooner. --- .../ConnectToUnexistingHost.cs | 8 +-- .../ConnectingFailDetection.cs | 17 +++-- .../ExceptionFactoryTests.cs | 4 +- tests/StackExchange.Redis.Tests/TestBase.cs | 72 ++++++++++++++----- 4 files changed, 73 insertions(+), 28 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs b/tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs index e2c454a5c..03757d918 100644 --- a/tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs +++ b/tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs @@ -48,7 +48,7 @@ void innerScenario() { var ex = Assert.Throws(() => { - using (ConnectionMultiplexer.Connect(TestConfig.Current.MasterServer + ":6500,connectTimeout=1000", Writer)) { } + using (ConnectionMultiplexer.Connect(TestConfig.Current.MasterServer + ":6500,connectTimeout=1000,connectRetry=0", Writer)) { } }); Log(ex.ToString()); } @@ -59,7 +59,7 @@ public async Task CanNotOpenNonsenseConnection_DNS() { var ex = await Assert.ThrowsAsync(async () => { - using (await ConnectionMultiplexer.ConnectAsync($"doesnot.exist.ds.{Guid.NewGuid():N}.com:6500,connectTimeout=1000", Writer).ForAwait()) { } + using (await ConnectionMultiplexer.ConnectAsync($"doesnot.exist.ds.{Guid.NewGuid():N}.com:6500,connectTimeout=1000,connectRetry=0", Writer).ForAwait()) { } }).ForAwait(); Log(ex.ToString()); } @@ -70,7 +70,7 @@ public async Task CreateDisconnectedNonsenseConnection_IP() await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); void innerScenario() { - using (var conn = ConnectionMultiplexer.Connect(TestConfig.Current.MasterServer + ":6500,abortConnect=false,connectTimeout=1000", Writer)) + using (var conn = ConnectionMultiplexer.Connect(TestConfig.Current.MasterServer + ":6500,abortConnect=false,connectTimeout=1000,connectRetry=0", Writer)) { Assert.False(conn.GetServer(conn.GetEndPoints().Single()).IsConnected); Assert.False(conn.GetDatabase().IsConnected(default(RedisKey))); @@ -84,7 +84,7 @@ public async Task CreateDisconnectedNonsenseConnection_DNS() await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); void innerScenario() { - using (var conn = ConnectionMultiplexer.Connect($"doesnot.exist.ds.{Guid.NewGuid():N}.com:6500,abortConnect=false,connectTimeout=1000", Writer)) + using (var conn = ConnectionMultiplexer.Connect($"doesnot.exist.ds.{Guid.NewGuid():N}.com:6500,abortConnect=false,connectTimeout=1000,connectRetry=0", Writer)) { Assert.False(conn.GetServer(conn.GetEndPoints().Single()).IsConnected); Assert.False(conn.GetDatabase().IsConnected(default(RedisKey))); diff --git a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs index d1020e6e3..7b33df000 100644 --- a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs +++ b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs @@ -99,22 +99,31 @@ public async Task Issue922_ReconnectRaised() config.AbortOnConnectFail = true; config.KeepAlive = 10; config.SyncTimeout = 1000; + config.AsyncTimeout = 1000; config.ReconnectRetryPolicy = new ExponentialRetry(5000); config.AllowAdmin = true; int failCount = 0, restoreCount = 0; - using (var muxer = ConnectionMultiplexer.Connect(config)) + using (var muxer = ConnectionMultiplexer.Connect(config, log: Writer)) { - muxer.ConnectionFailed += delegate { Interlocked.Increment(ref failCount); }; - muxer.ConnectionRestored += delegate { Interlocked.Increment(ref restoreCount); }; + muxer.ConnectionFailed += (s, e) => + { + Interlocked.Increment(ref failCount); + Log($"Connection Failed ({e.ConnectionType},{e.FailureType}): {e.Exception}"); + }; + muxer.ConnectionRestored += (s, e) => + { + Interlocked.Increment(ref restoreCount); + Log($"Connection Failed ({e.ConnectionType},{e.FailureType}): {e.Exception}"); + }; muxer.GetDatabase(); Assert.Equal(0, Volatile.Read(ref failCount)); Assert.Equal(0, Volatile.Read(ref restoreCount)); var server = muxer.GetServer(TestConfig.Current.MasterServerAndPort); - server.SimulateConnectionFailure(SimulatedFailureType.All); + server.SimulateConnectionFailure(SimulatedFailureType.InteractiveInbound | SimulatedFailureType.InteractiveOutbound); await UntilCondition(TimeSpan.FromSeconds(10), () => Volatile.Read(ref failCount) + Volatile.Read(ref restoreCount) == 4); // interactive+subscriber = 2 diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 25606a8c2..63c6ff2dc 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -160,12 +160,12 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple if (abortOnConnect) { options.EndPoints.Add(TestConfig.Current.MasterServerAndPort); - muxer = ConnectionMultiplexer.Connect(options); + muxer = ConnectionMultiplexer.Connect(options, Writer); } else { options.EndPoints.Add($"doesnot.exist.{Guid.NewGuid():N}:6379"); - muxer = ConnectionMultiplexer.Connect(options); + muxer = ConnectionMultiplexer.Connect(options, Writer); } using (muxer) diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 21bf84f55..d1d2ef408 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -119,6 +119,7 @@ static TestBase() Console.WriteLine(" GC LOH Mode: " + GCSettings.LargeObjectHeapCompactionMode); Console.WriteLine(" GC Latency Mode: " + GCSettings.LatencyMode); } + internal static string Time() => DateTime.UtcNow.ToString("HH:mm:ss.ffff"); protected void OnConnectionFailed(object sender, ConnectionFailedEventArgs e) { @@ -223,13 +224,25 @@ protected IServer GetAnyMaster(IConnectionMultiplexer muxer) } internal virtual IInternalConnectionMultiplexer Create( - string clientName = null, int? syncTimeout = null, bool? allowAdmin = null, int? keepAlive = null, - int? connectTimeout = null, string password = null, string tieBreaker = null, TextWriter log = null, - bool fail = true, string[] disabledCommands = null, string[] enabledCommands = null, - bool checkConnect = true, string failMessage = null, - string channelPrefix = null, Proxy? proxy = null, - string configuration = null, bool logTransactionData = true, - bool shared = true, int? defaultDatabase = null, + string clientName = null, + int? syncTimeout = null, + bool? allowAdmin = null, + int? keepAlive = null, + int? connectTimeout = null, + string password = null, + string tieBreaker = null, + TextWriter log = null, + bool fail = true, + string[] disabledCommands = null, + string[] enabledCommands = null, + bool checkConnect = true, + string failMessage = null, + string channelPrefix = null, + Proxy? proxy = null, + string configuration = null, + bool logTransactionData = true, + bool shared = true, + int? defaultDatabase = null, [CallerMemberName] string caller = null) { if (Output == null) @@ -237,8 +250,20 @@ internal virtual IInternalConnectionMultiplexer Create( Assert.True(false, "Failure: Be sure to call the TestBase constuctor like this: BasicOpsTests(ITestOutputHelper output) : base(output) { }"); } - if (shared && _fixture != null && _fixture.IsEnabled && enabledCommands == null && disabledCommands == null && fail && channelPrefix == null && proxy == null - && configuration == null && password == null && tieBreaker == null && defaultDatabase == null && (allowAdmin == null || allowAdmin == true) && expectedFailCount == 0) + // Share a connection if instructed to and we can - many specifics mean no sharing + if (shared + && _fixture != null && _fixture.IsEnabled + && enabledCommands == null + && disabledCommands == null + && fail + && channelPrefix == null + && proxy == null + && configuration == null + && password == null + && tieBreaker == null + && defaultDatabase == null + && (allowAdmin == null || allowAdmin == true) + && expectedFailCount == 0) { configuration = GetConfiguration(); if (configuration == _fixture.Configuration) @@ -255,7 +280,8 @@ internal virtual IInternalConnectionMultiplexer Create( checkConnect, failMessage, channelPrefix, proxy, configuration ?? GetConfiguration(), - logTransactionData, defaultDatabase, caller); + logTransactionData, defaultDatabase, + caller); muxer.InternalError += OnInternalError; muxer.ConnectionFailed += OnConnectionFailed; return muxer; @@ -263,18 +289,28 @@ internal virtual IInternalConnectionMultiplexer Create( public static ConnectionMultiplexer CreateDefault( TextWriter output, - string clientName = null, int? syncTimeout = null, bool? allowAdmin = null, int? keepAlive = null, - int? connectTimeout = null, string password = null, string tieBreaker = null, TextWriter log = null, - bool fail = true, string[] disabledCommands = null, string[] enabledCommands = null, - bool checkConnect = true, string failMessage = null, - string channelPrefix = null, Proxy? proxy = null, - string configuration = null, bool logTransactionData = true, + string clientName = null, + int? syncTimeout = null, + bool? allowAdmin = null, + int? keepAlive = null, + int? connectTimeout = null, + string password = null, + string tieBreaker = null, + TextWriter log = null, + bool fail = true, + string[] disabledCommands = null, + string[] enabledCommands = null, + bool checkConnect = true, + string failMessage = null, + string channelPrefix = null, + Proxy? proxy = null, + string configuration = null, + bool logTransactionData = true, int? defaultDatabase = null, - [CallerMemberName] string caller = null) { StringWriter localLog = null; - if(log == null) + if (log == null) { log = localLog = new StringWriter(); } From 51272b3fd2cad0de9aa3b862bc1d79fcdd601f75 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 10 Jan 2022 10:36:08 -0500 Subject: [PATCH 056/435] Tests: failure simulation and time fixes (#1946) I got time backwards and the simulation was raising events in a race for a connection it wasn't, e.g. both interactive directions would still raise an event on the subscriber because I'm a dummy. --- src/StackExchange.Redis/PhysicalConnection.cs | 10 +++++++++- tests/StackExchange.Redis.Tests/Expiry.cs | 5 +++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index f15940e84..e5d65f059 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -292,15 +292,18 @@ public Task FlushAsync() internal void SimulateConnectionFailure(SimulatedFailureType failureType) { + var raiseFailed = false; if (connectionType == ConnectionType.Interactive) { if (failureType.HasFlag(SimulatedFailureType.InteractiveInbound)) { _ioPipe?.Input.Complete(new Exception("Simulating interactive input failure")); + raiseFailed = true; } if (failureType.HasFlag(SimulatedFailureType.InteractiveOutbound)) { _ioPipe?.Output.Complete(new Exception("Simulating interactive output failure")); + raiseFailed = true; } } else if (connectionType == ConnectionType.Subscription) @@ -308,13 +311,18 @@ internal void SimulateConnectionFailure(SimulatedFailureType failureType) if (failureType.HasFlag(SimulatedFailureType.SubscriptionInbound)) { _ioPipe?.Input.Complete(new Exception("Simulating subscription input failure")); + raiseFailed = true; } if (failureType.HasFlag(SimulatedFailureType.SubscriptionOutbound)) { _ioPipe?.Output.Complete(new Exception("Simulating subscription output failure")); + raiseFailed = true; } } - RecordConnectionFailed(ConnectionFailureType.SocketFailure); + if (raiseFailed) + { + RecordConnectionFailed(ConnectionFailureType.SocketFailure); + } } public void RecordConnectionFailed(ConnectionFailureType failureType, Exception innerException = null, [CallerMemberName] string origin = null, diff --git a/tests/StackExchange.Redis.Tests/Expiry.cs b/tests/StackExchange.Redis.Tests/Expiry.cs index dc2835ccc..afa21d917 100644 --- a/tests/StackExchange.Redis.Tests/Expiry.cs +++ b/tests/StackExchange.Redis.Tests/Expiry.cs @@ -64,9 +64,10 @@ public async Task TestBasicExpiryDateTime(bool disablePTimes, bool utc) var now = utc ? DateTime.UtcNow : DateTime.Now; var serverTime = GetServer(muxer).Time(); + Log("Server time: {0}", serverTime); var offset = DateTime.UtcNow - serverTime; - Log("Now: {0}", now); + Log("Now (local time): {0}", now); conn.StringSet(key, "new value", flags: CommandFlags.FireAndForget); var a = conn.KeyTimeToLiveAsync(key); conn.KeyExpire(key, now.AddHours(1), CommandFlags.FireAndForget); @@ -84,7 +85,7 @@ public async Task TestBasicExpiryDateTime(bool disablePTimes, bool utc) var time = await b; // Adjust for server time offset, if any when checking expectations - time += offset; + time -= offset; Assert.NotNull(time); Log("Time: {0}, Expected: {1}-{2}", time, TimeSpan.FromMinutes(59), TimeSpan.FromMinutes(60)); From 7c965cfc3fd1dc0b86ad70e63cb038b709c2e99d Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 18 Jan 2022 08:31:30 -0500 Subject: [PATCH 057/435] Fix: Issue992 & ExceptionFactory tests (#1952) This won't be fully accurate until #1947 fixed the PING routing, but getting a test fix into main ahead of time. --- .../ConnectingFailDetection.cs | 12 ++++++++---- .../ExceptionFactoryTests.cs | 2 +- tests/StackExchange.Redis.Tests/Issues/SO24807536.cs | 2 +- tests/StackExchange.Redis.Tests/Locking.cs | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs index 7b33df000..2aa994807 100644 --- a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs +++ b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs @@ -123,12 +123,16 @@ public async Task Issue922_ReconnectRaised() Assert.Equal(0, Volatile.Read(ref restoreCount)); var server = muxer.GetServer(TestConfig.Current.MasterServerAndPort); - server.SimulateConnectionFailure(SimulatedFailureType.InteractiveInbound | SimulatedFailureType.InteractiveOutbound); + server.SimulateConnectionFailure(SimulatedFailureType.All); + + await UntilCondition(TimeSpan.FromSeconds(10), () => Volatile.Read(ref failCount) >= 2 && Volatile.Read(ref restoreCount) >= 2); - await UntilCondition(TimeSpan.FromSeconds(10), () => Volatile.Read(ref failCount) + Volatile.Read(ref restoreCount) == 4); // interactive+subscriber = 2 - Assert.Equal(2, Volatile.Read(ref failCount)); - Assert.Equal(2, Volatile.Read(ref restoreCount)); + var failCountSnapshot = Volatile.Read(ref failCount); + Assert.True(failCountSnapshot >= 2, $"failCount {failCountSnapshot} >= 2"); + + var restoreCountSnapshot = Volatile.Read(ref restoreCount); + Assert.True(restoreCountSnapshot >= 2, $"restoreCount ({restoreCountSnapshot}) >= 2"); } } diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 63c6ff2dc..494c688d6 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -151,7 +151,7 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple var options = new ConfigurationOptions() { AbortOnConnectFail = abortOnConnect, - ConnectTimeout = 500, + ConnectTimeout = 1000, SyncTimeout = 500, KeepAlive = 5000 }; diff --git a/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs b/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs index c795927eb..66b16ce7d 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs @@ -20,7 +20,7 @@ public async Task Exec() // setup some data cache.KeyDelete(key, CommandFlags.FireAndForget); cache.HashSet(key, "full", "some value", flags: CommandFlags.FireAndForget); - cache.KeyExpire(key, TimeSpan.FromSeconds(2), CommandFlags.FireAndForget); + cache.KeyExpire(key, TimeSpan.FromSeconds(4), CommandFlags.FireAndForget); // test while exists var keyExists = cache.KeyExists(key); diff --git a/tests/StackExchange.Redis.Tests/Locking.cs b/tests/StackExchange.Redis.Tests/Locking.cs index 7089a6972..9053d2956 100644 --- a/tests/StackExchange.Redis.Tests/Locking.cs +++ b/tests/StackExchange.Redis.Tests/Locking.cs @@ -69,7 +69,7 @@ void cb(object obj) [Fact] public void TestOpCountByVersionLocal_UpLevel() { - using (var conn = Create()) + using (var conn = Create(shared: false)) { TestLockOpCountByVersion(conn, 1, false); TestLockOpCountByVersion(conn, 1, true); @@ -99,7 +99,7 @@ private void TestLockOpCountByVersion(IConnectionMultiplexer conn, int expectedO Assert.Equal(!existFirst, taken); Assert.Equal(expectedVal, valAfter); - Assert.Equal(expectedOps, countAfter - countBefore); + Assert.True(expectedOps >= countAfter - countBefore, $"{expectedOps} >= ({countAfter} - {countBefore})"); // note we get a ping from GetCounters } From c923435de2b3a54d97ca1b2d72145ea69b86a08a Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 18 Jan 2022 08:34:07 -0500 Subject: [PATCH 058/435] Backlog: use a Task over Thread in .NET 6.0 (#1950) In #1854 we switched to a `Thread` here to prevent the pile-up case, however under concurrent load this incurs a 10-11% penalty given backlogs aren't all that uncommon (due to the lock contention case). In .NET 6.0+, let's use the thread pool growth semantics to handle the stall case and use the cheaper `Task.Run()` approach. Running the benchmarks from #1164 I was experimenting with on lock perf, I noticed a large perf sink in thread creation as a result of lock contention. For that benchmark as an example (high concurrent load) we're talking about a ~93 second -> ~84 second run reduction and similar wins in concurrent load tests. --- src/StackExchange.Redis/PhysicalBridge.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index f14cf7b10..57cfa8483 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -776,27 +776,34 @@ private bool PushToBacklog(Message message, bool onlyIfExists) StartBacklogProcessor(); return true; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void StartBacklogProcessor() { if (Interlocked.CompareExchange(ref _backlogProcessorIsRunning, 1, 0) == 0) { - #if DEBUG _backlogProcessorRequestedTime = Environment.TickCount; #endif _backlogStatus = BacklogStatus.Activating; - // start the backlog processor; this is a bit unorthadox, as you would *expect* this to just +#if NET6_0_OR_GREATER + // In .NET 6, use the thread pool stall semantics to our advantage and use a lighter-weight Task + Task.Run(ProcessBacklogAsync); +#else + // Start the backlog processor; this is a bit unorthodox, as you would *expect* this to just // be Task.Run; that would work fine when healthy, but when we're falling on our face, it is // easy to get into a thread-pool-starvation "spiral of death" if we rely on the thread-pool // to unblock the thread-pool when there could be sync-over-async callers. Note that in reality, // the initial "enough" of the back-log processor is typically sync, which means that the thread // we start is actually useful, despite thinking "but that will just go async and back to the pool" - var thread = new Thread(s => ((PhysicalBridge)s).ProcessBacklogAsync().RedisFireAndForget()); - thread.IsBackground = true; // don't keep process alive (also: act like the thread-pool used to) - thread.Name = "redisbacklog"; // help anyone looking at thread-dumps + var thread = new Thread(s => ((PhysicalBridge)s).ProcessBacklogAsync().RedisFireAndForget()) + { + IsBackground = true, // don't keep process alive (also: act like the thread-pool used to) + Name = "StackExchange.Redis Backlog", // help anyone looking at thread-dumps + }; thread.Start(this); +#endif } } #if DEBUG From a93ae86a7dc45d0c82c9d29c4d1a6a3ef0c84d5f Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 18 Jan 2022 08:34:51 -0500 Subject: [PATCH 059/435] General code/comment cleanup (#1951) Getting some of this in between other PRs I keep seeing over and over. --- .../ChannelMessageQueue.cs | 2 +- .../ClusterConfiguration.cs | 4 +- src/StackExchange.Redis/CommandBytes.cs | 7 +- src/StackExchange.Redis/Condition.cs | 125 ++++------- .../ConfigurationOptions.cs | 165 +++++++++++---- .../ConnectionMultiplexer.cs | 19 +- src/StackExchange.Redis/CursorEnumerable.cs | 23 +- src/StackExchange.Redis/EndPointCollection.cs | 34 ++- src/StackExchange.Redis/Exceptions.cs | 20 +- src/StackExchange.Redis/ExtensionMethods.cs | 3 +- src/StackExchange.Redis/Format.cs | 44 ++-- src/StackExchange.Redis/GeoEntry.cs | 78 +++---- src/StackExchange.Redis/HashEntry.cs | 18 +- .../HashSlotMovedEventArgs.cs | 8 +- .../InternalErrorEventArgs.cs | 10 +- src/StackExchange.Redis/Lease.cs | 22 +- src/StackExchange.Redis/LinearRetry.cs | 18 +- src/StackExchange.Redis/LuaScript.cs | 8 +- .../Maintenance/ServerMaintenanceEvent.cs | 3 +- src/StackExchange.Redis/Message.cs | 196 +++++++----------- src/StackExchange.Redis/NameValueEntry.cs | 4 +- src/StackExchange.Redis/PerfCounterHelper.cs | 14 +- src/StackExchange.Redis/PhysicalBridge.cs | 43 ++-- src/StackExchange.Redis/PhysicalConnection.cs | 125 ++++++----- .../Profiling/ProfiledCommand.cs | 9 +- src/StackExchange.Redis/RawResult.cs | 34 +-- src/StackExchange.Redis/RedisChannel.cs | 50 +---- .../RedisErrorEventArgs.cs | 6 +- src/StackExchange.Redis/RedisFeatures.cs | 4 +- src/StackExchange.Redis/RedisKey.cs | 36 ++-- src/StackExchange.Redis/RedisResult.cs | 14 +- src/StackExchange.Redis/RedisStream.cs | 2 +- src/StackExchange.Redis/RedisTransaction.cs | 13 -- src/StackExchange.Redis/RedisValue.cs | 29 +-- .../RedisValueWithExpiry.cs | 8 +- src/StackExchange.Redis/ResultProcessor.cs | 2 +- src/StackExchange.Redis/ServerCounters.cs | 13 +- src/StackExchange.Redis/SocketManager.cs | 28 +-- src/StackExchange.Redis/SortedSetEntry.cs | 34 +-- src/StackExchange.Redis/StreamConsumerInfo.cs | 2 +- src/StackExchange.Redis/StreamEntry.cs | 4 +- src/StackExchange.Redis/StreamGroupInfo.cs | 4 +- src/StackExchange.Redis/StreamInfo.cs | 4 +- src/StackExchange.Redis/StreamPosition.cs | 5 +- src/StackExchange.Redis/TaskSource.cs | 4 +- .../ConnectingFailDetection.cs | 2 +- tests/StackExchange.Redis.Tests/TestBase.cs | 3 +- 47 files changed, 631 insertions(+), 672 deletions(-) diff --git a/src/StackExchange.Redis/ChannelMessageQueue.cs b/src/StackExchange.Redis/ChannelMessageQueue.cs index 5f68714d2..2f01bce59 100644 --- a/src/StackExchange.Redis/ChannelMessageQueue.cs +++ b/src/StackExchange.Redis/ChannelMessageQueue.cs @@ -11,7 +11,7 @@ namespace StackExchange.Redis /// public readonly struct ChannelMessage { - private readonly ChannelMessageQueue _queue; // this is *smaller* than storing a RedisChannel for the subsribed channel + private readonly ChannelMessageQueue _queue; // this is *smaller* than storing a RedisChannel for the subscribed channel /// /// See Object.ToString /// diff --git a/src/StackExchange.Redis/ClusterConfiguration.cs b/src/StackExchange.Redis/ClusterConfiguration.cs index 819697f25..6a63b93b6 100644 --- a/src/StackExchange.Redis/ClusterConfiguration.cs +++ b/src/StackExchange.Redis/ClusterConfiguration.cs @@ -155,7 +155,7 @@ private static bool TryParseInt16(string s, int offset, int count, out short val /// public sealed class ClusterConfiguration { - private readonly Dictionary nodeLookup = new Dictionary(); + private readonly Dictionary nodeLookup = new(); private readonly ServerSelectionStrategy serverSelectionStrategy; internal ClusterConfiguration(ServerSelectionStrategy serverSelectionStrategy, string nodes, EndPoint origin) @@ -268,7 +268,7 @@ public ClusterNode GetBySlot(int slot) /// public sealed class ClusterNode : IEquatable, IComparable, IComparable { - private static readonly ClusterNode Dummy = new ClusterNode(); + private static readonly ClusterNode Dummy = new(); private readonly ClusterConfiguration configuration; diff --git a/src/StackExchange.Redis/CommandBytes.cs b/src/StackExchange.Redis/CommandBytes.cs index 67f8e10d5..d3eab23c6 100644 --- a/src/StackExchange.Redis/CommandBytes.cs +++ b/src/StackExchange.Redis/CommandBytes.cs @@ -88,6 +88,7 @@ public unsafe void CopyTo(Span target) new Span(bPtr + 1, *bPtr).CopyTo(target); } } + public unsafe byte this[int index] { get @@ -124,7 +125,7 @@ public unsafe CommandBytes(string value) public unsafe CommandBytes(ReadOnlySpan value) #pragma warning restore RCS1231 // Make parameter ref read-only. { - if (value.Length > MaxLength) throw new ArgumentOutOfRangeException("Maximum command length exceeed: " + value.Length + " bytes"); + if (value.Length > MaxLength) throw new ArgumentOutOfRangeException("Maximum command length exceeded: " + value.Length + " bytes"); _0 = _1 = _2 = _3 = 0L; fixed (ulong* uPtr = &_0) { @@ -136,7 +137,7 @@ public unsafe CommandBytes(ReadOnlySpan value) public unsafe CommandBytes(in ReadOnlySequence value) { - if (value.Length > MaxLength) throw new ArgumentOutOfRangeException(nameof(value), "Maximum command length exceeed"); + if (value.Length > MaxLength) throw new ArgumentOutOfRangeException(nameof(value), "Maximum command length exceeded"); int len = unchecked((int)value.Length); _0 = _1 = _2 = _3 = 0L; fixed (ulong* uPtr = &_0) @@ -164,7 +165,7 @@ private unsafe int UpperCasify(int len, byte* bPtr) const ulong HighBits = 0x8080808080808080; if (((_0 | _1 | _2 | _3) & HighBits) == 0) { - // no unicode; use ASCII bit bricks + // no Unicode; use ASCII bit bricks for (int i = 0; i < len; i++) { *bPtr = ToUpperInvariantAscii(*bPtr++); diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs index 050e623ac..b2230fbde 100644 --- a/src/StackExchange.Redis/Condition.cs +++ b/src/StackExchange.Redis/Condition.cs @@ -364,19 +364,15 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value) internal sealed class ConditionProcessor : ResultProcessor { - public static readonly ConditionProcessor Default = new ConditionProcessor(); + public static readonly ConditionProcessor Default = new(); #pragma warning disable RCS1231 // Make parameter ref read-only. - public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, RedisValue value = default(RedisValue)) + public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, RedisValue value = default(RedisValue)) => + new ConditionMessage(condition, db, flags, command, key, value); #pragma warning restore RCS1231 // Make parameter ref read-only. - { - return new ConditionMessage(condition, db, flags, command, key, value); - } - public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, in RedisValue value1) - { - return new ConditionMessage(condition, db, flags, command, key, value, value1); - } + public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, in RedisValue value1) => + new ConditionMessage(condition, db, flags, command, key, value, value1); [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0071:Simplify interpolation", Justification = "Allocations (string.Concat vs. string.Format)")] protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) @@ -441,10 +437,8 @@ internal class ExistsCondition : Condition private readonly RedisType type; private readonly RedisCommand cmd; - internal override Condition MapKeys(Func map) - { - return new ExistsCondition(map(key), type, expectedValue, expectedResult); - } + internal override Condition MapKeys(Func map) => + new ExistsCondition(map(key), type, expectedValue, expectedResult); public ExistsCondition(in RedisKey key, RedisType type, in RedisValue expectedValue, bool expectedResult) { @@ -470,11 +464,9 @@ public ExistsCondition(in RedisKey key, RedisType type, in RedisValue expectedVa } } - public override string ToString() - { - return (expectedValue.IsNull ? key.ToString() : ((string)key) + " " + type + " > " + expectedValue) + public override string ToString() => + (expectedValue.IsNull ? key.ToString() : ((string)key) + " " + type + " > " + expectedValue) + (expectedResult ? " exists" : " does not exists"); - } internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(cmd); @@ -515,10 +507,8 @@ internal override bool TryValidate(in RawResult result, out bool value) internal class EqualsCondition : Condition { - internal override Condition MapKeys(Func map) - { - return new EqualsCondition(map(key), type, memberName, expectedEqual, expectedValue); - } + internal override Condition MapKeys(Func map) => + new EqualsCondition(map(key), type, memberName, expectedEqual, expectedValue); private readonly bool expectedEqual; private readonly RedisValue memberName, expectedValue; @@ -542,12 +532,10 @@ public EqualsCondition(in RedisKey key, RedisType type, in RedisValue memberName }; } - public override string ToString() - { - return (memberName.IsNull ? key.ToString() : ((string)key) + " " + type + " > " + memberName) + public override string ToString() => + (memberName.IsNull ? key.ToString() : ((string)key) + " " + type + " > " + memberName) + (expectedEqual ? " == " : " != ") + expectedValue; - } internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(cmd); @@ -560,10 +548,7 @@ internal sealed override IEnumerable CreateMessages(int db, IResultBox yield return message; } - internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) - { - return serverSelectionStrategy.HashSlot(key); - } + internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key); internal override bool TryValidate(in RawResult result, out bool value) { @@ -604,10 +589,8 @@ internal override bool TryValidate(in RawResult result, out bool value) internal class ListCondition : Condition { - internal override Condition MapKeys(Func map) - { - return new ListCondition(map(key), index, expectedResult, expectedValue); - } + internal override Condition MapKeys(Func map) => + new ListCondition(map(key), index, expectedResult, expectedValue); private readonly bool expectedResult; private readonly long index; @@ -622,16 +605,11 @@ public ListCondition(in RedisKey key, long index, bool expectedResult, in RedisV this.expectedValue = expectedValue; } - public override string ToString() - { - return ((string)key) + "[" + index.ToString() + "]" + public override string ToString() => + ((string)key) + "[" + index.ToString() + "]" + (expectedValue.HasValue ? (expectedResult ? " == " : " != ") + expectedValue.Value : (expectedResult ? " exists" : " does not exist")); - } - internal override void CheckCommands(CommandMap commandMap) - { - commandMap.AssertAvailable(RedisCommand.LINDEX); - } + internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.LINDEX); internal sealed override IEnumerable CreateMessages(int db, IResultBox resultBox) { @@ -672,10 +650,8 @@ internal override bool TryValidate(in RawResult result, out bool value) internal class LengthCondition : Condition { - internal override Condition MapKeys(Func map) - { - return new LengthCondition(map(key), type, compareToResult, expectedLength); - } + internal override Condition MapKeys(Func map) => + new LengthCondition(map(key), type, compareToResult, expectedLength); private readonly int compareToResult; private readonly long expectedLength; @@ -702,20 +678,11 @@ public LengthCondition(in RedisKey key, RedisType type, int compareToResult, lon }; } - public override string ToString() - { - return ((string)key) + " " + type + " length" + GetComparisonString() + expectedLength; - } + public override string ToString() => ((string)key) + " " + type + " length" + GetComparisonString() + expectedLength; - private string GetComparisonString() - { - return compareToResult == 0 ? " == " : (compareToResult < 0 ? " > " : " < "); - } + private string GetComparisonString() => compareToResult == 0 ? " == " : (compareToResult < 0 ? " > " : " < "); - internal override void CheckCommands(CommandMap commandMap) - { - commandMap.AssertAvailable(cmd); - } + internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(cmd); internal sealed override IEnumerable CreateMessages(int db, IResultBox resultBox) { @@ -726,10 +693,7 @@ internal sealed override IEnumerable CreateMessages(int db, IResultBox yield return message; } - internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) - { - return serverSelectionStrategy.HashSlot(key); - } + internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key); internal override bool TryValidate(in RawResult result, out bool value) { @@ -751,10 +715,8 @@ internal override bool TryValidate(in RawResult result, out bool value) internal class SortedSetRangeLengthCondition : Condition { - internal override Condition MapKeys(Func map) - { - return new SortedSetRangeLengthCondition(map(key), min, max, compareToResult, expectedLength); - } + internal override Condition MapKeys(Func map) => + new SortedSetRangeLengthCondition(map(key), min, max, compareToResult, expectedLength); private readonly RedisValue min; private readonly RedisValue max; @@ -772,20 +734,12 @@ public SortedSetRangeLengthCondition(in RedisKey key, RedisValue min, RedisValue this.expectedLength = expectedLength; } - public override string ToString() - { - return ((string)key) + " " + RedisType.SortedSet + " range[" + min + ", " + max + "] length" + GetComparisonString() + expectedLength; - } + public override string ToString() => + ((string)key) + " " + RedisType.SortedSet + " range[" + min + ", " + max + "] length" + GetComparisonString() + expectedLength; - private string GetComparisonString() - { - return compareToResult == 0 ? " == " : (compareToResult < 0 ? " > " : " < "); - } + private string GetComparisonString() => compareToResult == 0 ? " == " : (compareToResult < 0 ? " > " : " < "); - internal override void CheckCommands(CommandMap commandMap) - { - commandMap.AssertAvailable(RedisCommand.ZCOUNT); - } + internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.ZCOUNT); internal sealed override IEnumerable CreateMessages(int db, IResultBox resultBox) { @@ -796,10 +750,7 @@ internal sealed override IEnumerable CreateMessages(int db, IResultBox yield return message; } - internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) - { - return serverSelectionStrategy.HashSlot(key); - } + internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key); internal override bool TryValidate(in RawResult result, out bool value) { @@ -821,10 +772,8 @@ internal override bool TryValidate(in RawResult result, out bool value) internal class SortedSetScoreCondition : Condition { - internal override Condition MapKeys(Func map) - { - return new SortedSetScoreCondition(map(key), sortedSetScore, expectedEqual, expectedValue); - } + internal override Condition MapKeys(Func map) => + new SortedSetScoreCondition(map(key), sortedSetScore, expectedEqual, expectedValue); private readonly bool expectedEqual; private readonly RedisValue sortedSetScore, expectedValue; @@ -843,10 +792,8 @@ public SortedSetScoreCondition(in RedisKey key, in RedisValue sortedSetScore, bo this.expectedValue = expectedValue; } - public override string ToString() - { - return key.ToString() + (expectedEqual ? " contains " : " not contains ") + expectedValue + " members with score: " + sortedSetScore; - } + public override string ToString() => + key.ToString() + (expectedEqual ? " contains " : " not contains ") + expectedValue + " members with score: " + sortedSetScore; internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.ZCOUNT); diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index abc12b579..d733353cc 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -163,17 +163,29 @@ public static string TryNormalize(string value) /// /// Gets or sets whether connect/configuration timeouts should be explicitly notified via a TimeoutException /// - public bool AbortOnConnectFail { get { return abortOnConnectFail ?? GetDefaultAbortOnConnectFailSetting(); } set { abortOnConnectFail = value; } } + public bool AbortOnConnectFail + { + get => abortOnConnectFail ?? GetDefaultAbortOnConnectFailSetting(); + set => abortOnConnectFail = value; + } /// /// Indicates whether admin operations should be allowed /// - public bool AllowAdmin { get { return allowAdmin.GetValueOrDefault(); } set { allowAdmin = value; } } + public bool AllowAdmin + { + get => allowAdmin.GetValueOrDefault(); + set => allowAdmin = value; + } /// /// Specifies the time in milliseconds that the system should allow for asynchronous operations (defaults to SyncTimeout) /// - public int AsyncTimeout { get { return asyncTimeout ?? SyncTimeout; } set { asyncTimeout = value; } } + public int AsyncTimeout + { + get => asyncTimeout ?? SyncTimeout; + set => asyncTimeout = value; + } /// /// Indicates whether the connection should be encrypted @@ -181,7 +193,11 @@ public static string TryNormalize(string value) [Obsolete("Please use .Ssl instead of .UseSsl"), Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - public bool UseSsl { get { return Ssl; } set { Ssl = value; } } + public bool UseSsl + { + get => Ssl; + set => Ssl = value; + } /// /// Automatically encodes and decodes channels @@ -191,7 +207,11 @@ public static string TryNormalize(string value) /// /// A Boolean value that specifies whether the certificate revocation list is checked during authentication. /// - public bool CheckCertificateRevocation { get { return checkCertificateRevocation ?? true; } set { checkCertificateRevocation = value; } } + public bool CheckCertificateRevocation + { + get => checkCertificateRevocation ?? true; + set => checkCertificateRevocation = value; + } /// /// Create a certificate validation check that checks against the supplied issuer even if not known by the machine @@ -239,7 +259,11 @@ private static bool CheckTrustedIssuer(X509Certificate2 certificateToValidate, X /// /// The number of times to repeat the initial connect cycle if no servers respond promptly /// - public int ConnectRetry { get { return connectRetry ?? 3; } set { connectRetry = value; } } + public int ConnectRetry + { + get => connectRetry ?? 3; + set => connectRetry = value; + } /// /// The command-map associated with this configuration @@ -257,19 +281,19 @@ public CommandMap CommandMap /// /// Channel to use for broadcasting and listening for configuration change notification /// - public string ConfigurationChannel { get { return configChannel ?? DefaultConfigurationChannel; } set { configChannel = value; } } + public string ConfigurationChannel + { + get => configChannel ?? DefaultConfigurationChannel; + set => configChannel = value; + } /// /// Specifies the time in milliseconds that should be allowed for connection (defaults to 5 seconds unless SyncTimeout is higher) /// public int ConnectTimeout { - get - { - if (connectTimeout.HasValue) return connectTimeout.GetValueOrDefault(); - return Math.Max(5000, SyncTimeout); - } - set { connectTimeout = value; } + get => connectTimeout ?? Math.Max(5000, SyncTimeout); + set => connectTimeout = value; } /// @@ -280,7 +304,11 @@ public int ConnectTimeout /// /// The server version to assume /// - public Version DefaultVersion { get { return defaultVersion ?? (IsAzureEndpoint() ? RedisFeatures.v4_0_0 : RedisFeatures.v2_8_0); } set { defaultVersion = value; } } + public Version DefaultVersion + { + get => defaultVersion ?? (IsAzureEndpoint() ? RedisFeatures.v4_0_0 : RedisFeatures.v2_8_0); + set => defaultVersion = value; + } /// /// The endpoints defined for this configuration @@ -290,13 +318,21 @@ public int ConnectTimeout /// /// Use ThreadPriority.AboveNormal for SocketManager reader and writer threads (true by default). If false, ThreadPriority.Normal will be used. /// - public bool HighPrioritySocketThreads { get { return highPrioritySocketThreads ?? true; } set { highPrioritySocketThreads = value; } } + public bool HighPrioritySocketThreads + { + get => highPrioritySocketThreads ?? true; + set => highPrioritySocketThreads = value; + } // Use coalesce expression. /// /// Specifies the time in seconds at which connections should be pinged to ensure validity /// - public int KeepAlive { get { return keepAlive.GetValueOrDefault(-1); } set { keepAlive = value; } } + public int KeepAlive + { + get => keepAlive.GetValueOrDefault(-1); + set => keepAlive = value; + } /// /// The user to use to authenticate with the server. @@ -314,33 +350,49 @@ public int ConnectTimeout [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue), false)] public bool PreserveAsyncOrder { - get { return false; } + get => false; set { } } /// /// Type of proxy to use (if any); for example Proxy.Twemproxy. /// - public Proxy Proxy { get { return proxy.GetValueOrDefault(); } set { proxy = value; } } + public Proxy Proxy + { + get => proxy.GetValueOrDefault(); + set => proxy = value; + } /// - /// The retry policy to be used for connection reconnects + /// The retry policy to be used for connection reconnects. /// - public IReconnectRetryPolicy ReconnectRetryPolicy { get { return reconnectRetryPolicy ??= new ExponentialRetry(ConnectTimeout/2); } set { reconnectRetryPolicy = value; } } + public IReconnectRetryPolicy ReconnectRetryPolicy + { + get => reconnectRetryPolicy ??= new ExponentialRetry(ConnectTimeout / 2); + set => reconnectRetryPolicy = value; + } /// /// Indicates whether endpoints should be resolved via DNS before connecting. /// If enabled the ConnectionMultiplexer will not re-resolve DNS /// when attempting to re-connect after a connection failure. /// - public bool ResolveDns { get { return resolveDns.GetValueOrDefault(); } set { resolveDns = value; } } + public bool ResolveDns + { + get => resolveDns.GetValueOrDefault(); + set => resolveDns = value; + } /// /// Specifies the time in milliseconds that the system should allow for responses before concluding that the socket is unhealthy /// (defaults to SyncTimeout) /// [Obsolete("This setting no longer has any effect, and should not be used")] - public int ResponseTimeout { get { return 0; } set { } } + public int ResponseTimeout + { + get => 0; + set { } + } /// /// The service name used to resolve a service via sentinel. @@ -356,42 +408,75 @@ public bool PreserveAsyncOrder /// /// Indicates whether the connection should be encrypted /// - public bool Ssl { get { return ssl.GetValueOrDefault(); } set { ssl = value; } } + public bool Ssl + { + get => ssl.GetValueOrDefault(); + set => ssl = value; + } /// /// The target-host to use when validating SSL certificate; setting a value here enables SSL mode /// - public string SslHost { get { return sslHost ?? InferSslHostFromEndpoints(); } set { sslHost = value; } } + public string SslHost + { + get => sslHost ?? InferSslHostFromEndpoints(); + set => sslHost = value; + } /// - /// Configures which Ssl/TLS protocols should be allowed. If not set, defaults are chosen by the .NET framework. + /// Configures which SSL/TLS protocols should be allowed. If not set, defaults are chosen by the .NET framework. /// public SslProtocols? SslProtocols { get; set; } /// /// Specifies the time in milliseconds that the system should allow for synchronous operations (defaults to 5 seconds) /// - public int SyncTimeout { get { return syncTimeout.GetValueOrDefault(5000); } set { syncTimeout = value; } } + public int SyncTimeout + { + get => syncTimeout.GetValueOrDefault(5000); + set => syncTimeout = value; + } /// /// Tie-breaker used to choose between masters (must match the endpoint exactly) /// - public string TieBreaker { get { return tieBreaker ?? DefaultTieBreaker; } set { tieBreaker = value; } } + public string TieBreaker + { + get => tieBreaker ?? DefaultTieBreaker; + set => tieBreaker = value; + } + /// /// The size of the output buffer to use /// [Obsolete("This setting no longer has any effect, and should not be used")] - public int WriteBuffer { get { return 0; } set { } } + public int WriteBuffer + { + get => 0; + set { } + } - internal LocalCertificateSelectionCallback CertificateSelectionCallback { get { return CertificateSelection; } private set { CertificateSelection = value; } } + internal LocalCertificateSelectionCallback CertificateSelectionCallback + { + get => CertificateSelection; + private set => CertificateSelection = value; + } // these just rip out the underlying handlers, bypassing the event accessors - needed when creating the SSL stream - internal RemoteCertificateValidationCallback CertificateValidationCallback { get { return CertificateValidation; } private set { CertificateValidation = value; } } + internal RemoteCertificateValidationCallback CertificateValidationCallback + { + get => CertificateValidation; + private set => CertificateValidation = value; + } /// /// Check configuration every n seconds (every minute by default) /// - public int ConfigCheckSeconds { get { return configCheckSeconds.GetValueOrDefault(60); } set { configCheckSeconds = value; } } + public int ConfigCheckSeconds + { + get => configCheckSeconds.GetValueOrDefault(60); + set => configCheckSeconds = value; + } /// /// Parse the configuration from a comma-delimited configuration string @@ -460,7 +545,9 @@ public ConfigurationOptions Clone() checkCertificateRevocation = checkCertificateRevocation, }; foreach (var item in EndPoints) + { options.EndPoints.Add(item); + } return options; } @@ -478,10 +565,7 @@ public ConfigurationOptions Apply(Action configure) /// /// Resolve the default port for any endpoints that did not have a port explicitly specified /// - public void SetDefaultPorts() - { - EndPoints.SetDefaultPorts(Ssl ? 6380 : 6379); - } + public void SetDefaultPorts() => EndPoints.SetDefaultPorts(Ssl ? 6380 : 6379); /// /// Sets default config settings required for sentinel usage @@ -548,7 +632,13 @@ public string ToString(bool includePassword) internal bool HasDnsEndPoints() { - foreach (var endpoint in EndPoints) if (endpoint is DnsEndPoint) return true; + foreach (var endpoint in EndPoints) + { + if (endpoint is DnsEndPoint) + { + return true; + } + } return false; } @@ -786,7 +876,8 @@ private void DoParse(string configuration, bool ignoreUnknown) /// List of domains known to be Azure Redis, so we can light up some helpful functionality /// for minimizing downtime during maintenance events and such. /// - private static readonly List azureRedisDomains = new List { + private static readonly List azureRedisDomains = new() + { ".redis.cache.windows.net", ".redis.cache.chinacloudapi.cn", ".redis.cache.usgovcloudapi.net", diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 055ea3d12..0cc31e76b 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -152,7 +152,7 @@ private static string GetDefaultClientName() } /// - /// Tries to get the Roleinstance Id if Microsoft.WindowsAzure.ServiceRuntime is loaded. + /// Tries to get the RoleInstance Id if Microsoft.WindowsAzure.ServiceRuntime is loaded. /// In case of any failure, swallows the exception and returns null /// internal static string TryGetAzureRoleInstanceIdNoThrow() @@ -464,7 +464,7 @@ internal void MakeMaster(ServerEndPoint server, ReplicationChangeOptions options // Try and broadcast the fact a change happened to all members // We want everyone possible to pick it up. // We broadcast before *and after* the change to remote members, so that they don't go without detecting a change happened. - // This eliminates the race of pub/sub *then* re-slaving happening, since a method both preceeds and follows. + // This eliminates the race of pub/sub *then* re-slaving happening, since a method both precedes and follows. void Broadcast(ReadOnlySpan serverNodes) { if ((options & ReplicationChangeOptions.Broadcast) != 0 && ConfigurationChangedChannel != null @@ -1672,7 +1672,7 @@ private void ActivateAllServers(LogProxy log) internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogProxy log, EndPoint blame, string cause, bool publishReconfigure = false, CommandFlags publishReconfigureFlags = CommandFlags.None) { if (_isDisposed) throw new ObjectDisposedException(ToString()); - bool showStats = log is object; + bool showStats = log is not null; bool ranThisCall = false; try @@ -2190,7 +2190,8 @@ private bool PrepareToPushMessageToBridge(Message message, ResultProcessor message.SetSource(processor, resultBox); if (server == null) - { // infer a server automatically + { + // Infer a server automatically server = SelectServer(message); } else // a server was specified; do we trust their choice, though? @@ -2233,7 +2234,7 @@ private bool PrepareToPushMessageToBridge(Message message, ResultProcessor } } - Trace("Queueing on server: " + message); + Trace("Queuing on server: " + message); return true; } Trace("No server or server unavailable - aborting: " + message); @@ -2827,7 +2828,7 @@ internal T ExecuteSyncImpl(Message message, ResultProcessor processor, Ser if (Monitor.Wait(source, TimeoutMilliseconds)) { - Trace("Timeley response to " + message); + Trace("Timely response to " + message); } else { @@ -2866,10 +2867,8 @@ internal T ExecuteSyncImpl(Message message, ResultProcessor processor, Ser /// /// Obtains the log of unusual busy patterns /// - public string GetStormLog() - { - return Volatile.Read(ref stormLogSnapshot); - } + public string GetStormLog() => Volatile.Read(ref stormLogSnapshot); + /// /// Resets the log of unusual busy patterns /// diff --git a/src/StackExchange.Redis/CursorEnumerable.cs b/src/StackExchange.Redis/CursorEnumerable.cs index 34acce1d8..8e42b918e 100644 --- a/src/StackExchange.Redis/CursorEnumerable.cs +++ b/src/StackExchange.Redis/CursorEnumerable.cs @@ -168,7 +168,8 @@ private void ProcessReply(in ScanResult result, bool isInitial) _isPooled = result.IsPooled; _pageCount = result.Count; if (_nextCursor == RedisBase.CursorUtils.Origin) - { // eof + { + // EOF _pending = null; _pendingMessage = null; } @@ -289,7 +290,9 @@ private static void Recycle(ref T[] array, ref bool isPooled) var tmp = array; array = null; if (tmp != null && tmp.Length != 0 && isPooled) + { ArrayPool.Shared.Return(tmp); + } isPooled = false; } @@ -298,7 +301,10 @@ private static void Recycle(ref T[] array, ref bool isPooled) /// public void Reset() { - if (_state == State.Disposed) throw new ObjectDisposedException(GetType().Name); + if (_state == State.Disposed) + { + throw new ObjectDisposedException(GetType().Name); + } _nextCursor = _currentCursor = parent.initialCursor; _pageOffset = parent.initialOffset; // don't -1 here; this makes it look "right" before incremented _state = State.Initial; @@ -317,17 +323,14 @@ public void Reset() int IScanningCursor.PageOffset => _pageOffset; } - long IScanningCursor.Cursor // this may fail on cluster-proxy; I'm OK with this for now - { - get { var tmp = activeCursor; return tmp?.Cursor ?? (long)initialCursor; } - } + /// + /// This may fail on cluster-proxy; I'm OK with this for now + /// + long IScanningCursor.Cursor => activeCursor?.Cursor ?? (long)initialCursor; int IScanningCursor.PageSize => pageSize; - int IScanningCursor.PageOffset - { - get { var tmp = activeCursor; return tmp?.PageOffset ?? initialOffset; } - } + int IScanningCursor.PageOffset => activeCursor?.PageOffset ?? initialOffset; internal static CursorEnumerable From(RedisBase redis, ServerEndPoint server, Task pending, int pageOffset) => new SingleBlockEnumerable(redis, server, pending, pageOffset); diff --git a/src/StackExchange.Redis/EndPointCollection.cs b/src/StackExchange.Redis/EndPointCollection.cs index 506436260..cf9fb6fc0 100644 --- a/src/StackExchange.Redis/EndPointCollection.cs +++ b/src/StackExchange.Redis/EndPointCollection.cs @@ -41,7 +41,10 @@ public EndPointCollection(IList endpoints) : base(endpoints) {} public void Add(string hostAndPort) { var endpoint = Format.TryParseEndPoint(hostAndPort); - if (endpoint == null) throw new ArgumentException($"Could not parse host and port from '{hostAndPort}'", nameof(hostAndPort)); + if (endpoint == null) + { + throw new ArgumentException($"Could not parse host and port from '{hostAndPort}'", nameof(hostAndPort)); + } Add(endpoint); } @@ -66,7 +69,10 @@ public void Add(string hostAndPort) /// True if the endpoint was added or false if not. public bool TryAdd(EndPoint endpoint) { - if (endpoint == null) throw new ArgumentNullException(nameof(endpoint)); + if (endpoint == null) + { + throw new ArgumentNullException(nameof(endpoint)); + } if (!Contains(endpoint)) { @@ -86,8 +92,15 @@ public bool TryAdd(EndPoint endpoint) /// The item to insert at . protected override void InsertItem(int index, EndPoint item) { - if (item == null) throw new ArgumentNullException(nameof(item)); - if (Contains(item)) throw new ArgumentException("EndPoints must be unique", nameof(item)); + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + if (Contains(item)) + { + throw new ArgumentException("EndPoints must be unique", nameof(item)); + } + base.InsertItem(index, item); } /// @@ -97,17 +110,24 @@ protected override void InsertItem(int index, EndPoint item) /// The item to replace the existing endpoint at . protected override void SetItem(int index, EndPoint item) { - if (item == null) throw new ArgumentNullException(nameof(item)); + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } int existingIndex; try { existingIndex = IndexOf(item); - } catch(NullReferenceException) + } + catch (NullReferenceException) { // mono has a nasty bug in DnsEndPoint.Equals; if they do bad things here: sorry, I can't help existingIndex = -1; } - if (existingIndex >= 0 && existingIndex != index) throw new ArgumentException("EndPoints must be unique", nameof(item)); + if (existingIndex >= 0 && existingIndex != index) + { + throw new ArgumentException("EndPoints must be unique", nameof(item)); + } base.SetItem(index, item); } diff --git a/src/StackExchange.Redis/Exceptions.cs b/src/StackExchange.Redis/Exceptions.cs index 2e1787883..aaacf4950 100644 --- a/src/StackExchange.Redis/Exceptions.cs +++ b/src/StackExchange.Redis/Exceptions.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis { /// - /// Indicates that a command was illegal and was not sent to the server + /// Indicates that a command was illegal and was not sent to the server. /// [Serializable] public sealed partial class RedisCommandException : Exception @@ -43,7 +43,7 @@ public RedisTimeoutException(string message, CommandStatus commandStatus) : base } /// - /// status of the command while communicating with Redis + /// status of the command while communicating with Redis. /// public CommandStatus Commandstatus { get; } @@ -52,7 +52,7 @@ private RedisTimeoutException(SerializationInfo info, StreamingContext ctx) : ba Commandstatus = (CommandStatus)info.GetValue("commandStatus", typeof(CommandStatus)); } /// - /// Serialization implementation; not intended for general usage + /// Serialization implementation; not intended for general usage. /// /// Serialization info. /// Serialization context. @@ -64,7 +64,7 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont } /// - /// Indicates a connection fault when communicating with redis + /// Indicates a connection fault when communicating with redis. /// [Serializable] public sealed partial class RedisConnectionException : RedisException @@ -98,12 +98,12 @@ public RedisConnectionException(ConnectionFailureType failureType, string messag } /// - /// The type of connection failure + /// The type of connection failure. /// public ConnectionFailureType FailureType { get; } /// - /// status of the command while communicating with Redis + /// Status of the command while communicating with Redis. /// public CommandStatus CommandStatus { get; } @@ -113,7 +113,7 @@ private RedisConnectionException(SerializationInfo info, StreamingContext ctx) : CommandStatus = (CommandStatus)info.GetValue("commandStatus", typeof(CommandStatus)); } /// - /// Serialization implementation; not intended for general usage + /// Serialization implementation; not intended for general usage. /// /// Serialization info. /// Serialization context. @@ -126,7 +126,7 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont } /// - /// Indicates an issue communicating with redis + /// Indicates an issue communicating with redis. /// [Serializable] public partial class RedisException : Exception @@ -145,7 +145,7 @@ public RedisException(string message) : base(message) { } public RedisException(string message, Exception innerException) : base(message, innerException) { } /// - /// Deserialization constructor; not intended for general usage + /// Deserialization constructor; not intended for general usage. /// /// Serialization info. /// Serialization context. @@ -153,7 +153,7 @@ protected RedisException(SerializationInfo info, StreamingContext ctx) : base(in } /// - /// Indicates an exception raised by a redis server + /// Indicates an exception raised by a redis server. /// [Serializable] public sealed partial class RedisServerException : RedisException diff --git a/src/StackExchange.Redis/ExtensionMethods.cs b/src/StackExchange.Redis/ExtensionMethods.cs index 1109f9ea0..ed3424169 100644 --- a/src/StackExchange.Redis/ExtensionMethods.cs +++ b/src/StackExchange.Redis/ExtensionMethods.cs @@ -30,6 +30,7 @@ public static Dictionary ToStringDictionary(this HashEntry[] hash } return result; } + /// /// Create a dictionary from an array of HashEntry values. /// @@ -235,7 +236,7 @@ protected override void Dispose(bool disposing) // Could not load file or assembly 'System.Numerics.Vectors, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' // or one of its dependencies.The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040) // - // also; note that the nuget tools *do not* reliably (or even occasionally) produce the correct + // also; note that the NuGet tools *do not* reliably (or even occasionally) produce the correct // assembly-binding-redirect entries to fix this up, so; it would present an unreasonable support burden // otherwise. And yes, I've tried explicitly referencing System.Numerics.Vectors in the manifest to // force it... nothing. Nada. diff --git a/src/StackExchange.Redis/Format.cs b/src/StackExchange.Redis/Format.cs index be0e294ae..60b9f6e79 100644 --- a/src/StackExchange.Redis/Format.cs +++ b/src/StackExchange.Redis/Format.cs @@ -3,8 +3,10 @@ using System.Buffers.Text; using System.Globalization; using System.Net; -using System.Net.Sockets; using System.Text; +#if UNIX_SOCKET +using System.Net.Sockets; +#endif namespace StackExchange.Redis { @@ -39,10 +41,8 @@ public static bool TryParseBoolean(string s, out bool value) return false; } - public static bool TryParseInt32(string s, out int value) - { - return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out value); - } + public static bool TryParseInt32(string s, out int value) => + int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out value); internal static EndPoint ParseEndPoint(string host, int port) { @@ -100,18 +100,13 @@ internal static string ToString(EndPoint endpoint) } } - internal static string ToStringHostOnly(EndPoint endpoint) - { - if (endpoint is DnsEndPoint dns) - { - return dns.Host; - } - if (endpoint is IPEndPoint ip) + internal static string ToStringHostOnly(EndPoint endpoint) => + endpoint switch { - return ip.Address.ToString(); - } - return ""; - } + DnsEndPoint dns => dns.Host, + IPEndPoint ip => ip.Address.ToString(), + _ => "" + }; internal static bool TryGetHostPort(EndPoint endpoint, out string host, out int port) { @@ -161,14 +156,14 @@ internal static bool TryParseDouble(string s, out double value) return double.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out value); } - internal static bool TryParseUInt64(string s, out ulong value) - => ulong.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out value); + internal static bool TryParseUInt64(string s, out ulong value) => + ulong.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out value); - internal static bool TryParseUInt64(ReadOnlySpan s, out ulong value) - => Utf8Parser.TryParse(s, out value, out int bytes, standardFormat: 'D') & bytes == s.Length; + internal static bool TryParseUInt64(ReadOnlySpan s, out ulong value) => + Utf8Parser.TryParse(s, out value, out int bytes, standardFormat: 'D') & bytes == s.Length; - internal static bool TryParseInt64(ReadOnlySpan s, out long value) - => Utf8Parser.TryParse(s, out value, out int bytes, standardFormat: 'D') & bytes == s.Length; + internal static bool TryParseInt64(ReadOnlySpan s, out long value) => + Utf8Parser.TryParse(s, out value, out int bytes, standardFormat: 'D') & bytes == s.Length; internal static bool CouldBeInteger(string s) { @@ -193,8 +188,8 @@ internal static bool CouldBeInteger(ReadOnlySpan s) return true; } - internal static bool TryParseInt64(string s, out long value) - => long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out value); + internal static bool TryParseInt64(string s, out long value) => + long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out value); internal static bool TryParseDouble(ReadOnlySpan s, out double value) { @@ -322,6 +317,7 @@ internal static string GetString(ReadOnlySequence buffer) ArrayPool.Shared.Return(arr); return s; } + internal static unsafe string GetString(ReadOnlySpan span) { if (span.IsEmpty) return ""; diff --git a/src/StackExchange.Redis/GeoEntry.cs b/src/StackExchange.Redis/GeoEntry.cs index eee732e92..af3b9f646 100644 --- a/src/StackExchange.Redis/GeoEntry.cs +++ b/src/StackExchange.Redis/GeoEntry.cs @@ -9,7 +9,7 @@ namespace StackExchange.Redis public enum GeoRadiusOptions { /// - /// No Options + /// No Options. /// None = 0, /// @@ -21,11 +21,11 @@ public enum GeoRadiusOptions /// WithDistance = 2, /// - /// Redis will return the geo hash value as an integer. (This is the score in the sorted set) + /// Redis will return the geo hash value as an integer. (This is the score in the sorted set). /// WithGeoHash = 4, /// - /// Populates the commonly used values from the entry (the integer hash is not returned as it is not commonly useful) + /// Populates the commonly used values from the entry (the integer hash is not returned as it is not commonly useful). /// Default = WithCoordinates | GeoRadiusOptions.WithDistance } @@ -36,7 +36,7 @@ public enum GeoRadiusOptions public readonly struct GeoRadiusResult { /// - /// Indicate the member being represented + /// Indicate the member being represented. /// public override string ToString() => Member.ToString(); @@ -51,7 +51,7 @@ public readonly struct GeoRadiusResult public double? Distance { get; } /// - /// The hash value of the matched member as an integer. (The key in the sorted set) + /// The hash value of the matched member as an integer. (The key in the sorted set). /// /// Note that this is not the same as the hash returned from GeoHash public long? Hash { get; } @@ -62,12 +62,12 @@ public readonly struct GeoRadiusResult public GeoPosition? Position { get; } /// - /// Returns a new GeoRadiusResult + /// Returns a new GeoRadiusResult. /// /// The value from the result. - /// Tthe distance from the result. + /// The distance from the result. /// The hash of the result. - /// The geo position of the result. + /// The GeoPosition of the result. public GeoRadiusResult(in RedisValue member, double? distance, long? hash, GeoPosition? position) { Member = member; @@ -78,7 +78,7 @@ public GeoRadiusResult(in RedisValue member, double? distance, long? hash, GeoPo } /// - /// Describes the longitude and latitude of a GeoEntry + /// Describes the longitude and latitude of a GeoEntry. /// public readonly struct GeoPosition : IEquatable { @@ -92,20 +92,18 @@ public GeoRadiusResult(in RedisValue member, double? distance, long? hash, GeoPo }; /// - /// The Latitude of the GeoPosition + /// The Latitude of the GeoPosition. /// public double Latitude { get; } /// - /// The Logitude of the GeoPosition + /// The Longitude of the GeoPosition. /// public double Longitude { get; } /// - /// Creates a new GeoPosition + /// Creates a new GeoPosition. /// - /// - /// public GeoPosition(double longitude, double latitude) { Longitude = longitude; @@ -113,13 +111,13 @@ public GeoPosition(double longitude, double latitude) } /// - /// See Object.ToString() + /// See . /// public override string ToString() => string.Format("{0} {1}", Longitude, Latitude); /// - /// See Object.GetHashCode() - /// Diagonals not an issue in the case of lat/long + /// See . + /// Diagonals not an issue in the case of lat/long. /// /// /// Diagonals are not an issue in the case of lat/long. @@ -127,116 +125,104 @@ public GeoPosition(double longitude, double latitude) public override int GetHashCode() => Longitude.GetHashCode() ^ Latitude.GetHashCode(); /// - /// Compares two values for equality + /// Compares two values for equality. /// /// The to compare to. public override bool Equals(object obj) => obj is GeoPosition gpObj && Equals(gpObj); /// - /// Compares two values for equality + /// Compares two values for equality. /// /// The to compare to. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public bool Equals(GeoPosition other) => this == other; -#pragma warning restore RCS1231 // Make parameter ref read-only. /// - /// Compares two values for equality + /// Compares two values for equality. /// /// The first position to compare. /// The second position to compare. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static bool operator ==(GeoPosition x, GeoPosition y) => x.Longitude == y.Longitude && x.Latitude == y.Latitude; -#pragma warning restore RCS1231 // Make parameter ref read-only. /// - /// Compares two values for non-equality + /// Compares two values for non-equality. /// /// The first position to compare. /// The second position to compare. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static bool operator !=(GeoPosition x, GeoPosition y) => x.Longitude != y.Longitude || x.Latitude != y.Latitude; -#pragma warning restore RCS1231 // Make parameter ref read-only. } /// - /// Describes a GeoEntry element with the corresponding value - /// GeoEntries are stored in redis as SortedSetEntries + /// Describes a GeoEntry element with the corresponding value. + /// GeoEntries are stored in redis as SortedSetEntries. /// public readonly struct GeoEntry : IEquatable { /// - /// The name of the geo entry + /// The name of the GeoEntry. /// public RedisValue Member { get; } /// - /// Describes the longitude and latitude of a GeoEntry + /// Describes the longitude and latitude of a GeoEntry. /// public GeoPosition Position { get; } /// - /// Initializes a GeoEntry value + /// Initializes a GeoEntry value. /// /// The longitude position to use. /// The latitude position to use. /// The value to store for this position. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public GeoEntry(double longitude, double latitude, RedisValue member) -#pragma warning restore RCS1231 // Make parameter ref read-only. { Member = member; Position = new GeoPosition(longitude, latitude); } /// - /// The longitude of the geo entry + /// The longitude of the GeoEntry. /// public double Longitude => Position.Longitude; /// - /// The latitude of the geo entry + /// The latitude of the GeoEntry. /// public double Latitude => Position.Latitude; /// - /// See Object.ToString() + /// See . /// public override string ToString() => $"({Longitude},{Latitude})={Member}"; /// - /// See Object.GetHashCode() + /// See . /// public override int GetHashCode() => Position.GetHashCode() ^ Member.GetHashCode(); /// - /// Compares two values for equality + /// Compares two values for equality. /// /// The to compare to. public override bool Equals(object obj) => obj is GeoEntry geObj && Equals(geObj); /// - /// Compares two values for equality + /// Compares two values for equality. /// /// The to compare to. public bool Equals(GeoEntry other) => this == other; /// - /// Compares two values for equality + /// Compares two values for equality. /// /// The first entry to compare. /// The second entry to compare. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static bool operator ==(GeoEntry x, GeoEntry y) => x.Position == y.Position && x.Member == y.Member; -#pragma warning restore RCS1231 // Make parameter ref read-only. /// - /// Compares two values for non-equality + /// Compares two values for non-equality. /// /// The first entry to compare. /// The second entry to compare. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static bool operator !=(GeoEntry x, GeoEntry y) => x.Position != y.Position || x.Member != y.Member; -#pragma warning restore RCS1231 // Make parameter ref read-only. } } diff --git a/src/StackExchange.Redis/HashEntry.cs b/src/StackExchange.Redis/HashEntry.cs index f3ad4bc3d..984e8f678 100644 --- a/src/StackExchange.Redis/HashEntry.cs +++ b/src/StackExchange.Redis/HashEntry.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis { /// - /// Describes a hash-field (a name/value pair) + /// Describes a hash-field (a name/value pair). /// public readonly struct HashEntry : IEquatable { @@ -23,21 +23,21 @@ public HashEntry(RedisValue name, RedisValue value) } /// - /// The name of the hash field + /// The name of the hash field. /// public RedisValue Name => name; /// - /// The value of the hash field + /// The value of the hash field. /// public RedisValue Value => value; /// - /// The name of the hash field + /// The name of the hash field. /// [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Please use Name", false)] - public RedisValue Key { get { return name; } } + public RedisValue Key => name; /// /// Converts to a key/value pair @@ -54,12 +54,12 @@ public static implicit operator HashEntry(KeyValuePair v new HashEntry(value.Key, value.Value); /// - /// See Object.ToString() + /// See . /// public override string ToString() => name + ": " + value; /// - /// See Object.GetHashCode() + /// See . /// public override int GetHashCode() => name.GetHashCode() ^ value.GetHashCode(); @@ -76,14 +76,14 @@ public static implicit operator HashEntry(KeyValuePair v public bool Equals(HashEntry other) => name == other.name && value == other.value; /// - /// Compares two values for equality + /// Compares two values for equality. /// /// The first to compare. /// The second to compare. public static bool operator ==(HashEntry x, HashEntry y) => x.name == y.name && x.value == y.value; /// - /// Compares two values for non-equality + /// Compares two values for non-equality. /// /// The first to compare. /// The second to compare. diff --git a/src/StackExchange.Redis/HashSlotMovedEventArgs.cs b/src/StackExchange.Redis/HashSlotMovedEventArgs.cs index 14b30a6cb..47b7ee0af 100644 --- a/src/StackExchange.Redis/HashSlotMovedEventArgs.cs +++ b/src/StackExchange.Redis/HashSlotMovedEventArgs.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis { /// - /// Contains information about individual hash-slot relocations + /// Contains information about individual hash-slot relocations. /// public class HashSlotMovedEventArgs : EventArgs, ICompletable { @@ -13,17 +13,17 @@ public class HashSlotMovedEventArgs : EventArgs, ICompletable private readonly EventHandler handler; /// - /// The hash-slot that was relocated + /// The hash-slot that was relocated. /// public int HashSlot { get; } /// - /// The old endpoint for this hash-slot (if known) + /// The old endpoint for this hash-slot (if known). /// public EndPoint OldEndPoint { get; } /// - /// The new endpoint for this hash-slot (if known) + /// The new endpoint for this hash-slot (if known). /// public EndPoint NewEndPoint { get; } diff --git a/src/StackExchange.Redis/InternalErrorEventArgs.cs b/src/StackExchange.Redis/InternalErrorEventArgs.cs index 9c613ed79..0d0767c2d 100644 --- a/src/StackExchange.Redis/InternalErrorEventArgs.cs +++ b/src/StackExchange.Redis/InternalErrorEventArgs.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis { /// - /// Describes internal errors (mainly intended for debugging) + /// Describes internal errors (mainly intended for debugging). /// public class InternalErrorEventArgs : EventArgs, ICompletable { @@ -35,22 +35,22 @@ public InternalErrorEventArgs(object sender, EndPoint endpoint, ConnectionType c } /// - /// Gets the connection-type of the failing connection + /// Gets the connection-type of the failing connection. /// public ConnectionType ConnectionType { get; } /// - /// Gets the failing server-endpoint (this can be null) + /// Gets the failing server-endpoint (this can be null). /// public EndPoint EndPoint { get; } /// - /// Gets the exception if available (this can be null) + /// Gets the exception if available (this can be null). /// public Exception Exception { get; } /// - /// The underlying origin of the error + /// The underlying origin of the error. /// public string Origin { get; } diff --git a/src/StackExchange.Redis/Lease.cs b/src/StackExchange.Redis/Lease.cs index f3f93625a..dc0869019 100644 --- a/src/StackExchange.Redis/Lease.cs +++ b/src/StackExchange.Redis/Lease.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis { /// - /// A sized region of contiguous memory backed by a memory pool; disposing the lease returns the memory to the pool + /// A sized region of contiguous memory backed by a memory pool; disposing the lease returns the memory to the pool. /// - /// The type of data being leased + /// The type of data being leased. public sealed class Lease : IMemoryOwner { /// @@ -19,15 +19,15 @@ public sealed class Lease : IMemoryOwner private T[] _arr; /// - /// The length of the lease + /// The length of the lease. /// public int Length { get; } /// - /// Create a new lease + /// Create a new lease. /// - /// The size required - /// Whether to erase the memory + /// The size required. + /// Whether to erase the memory. public static Lease Create(int length, bool clear = true) { if (length == 0) return Empty; @@ -43,7 +43,7 @@ private Lease(T[] arr, int length) } /// - /// Release all resources owned by the lease + /// Release all resources owned by the lease. /// public void Dispose() { @@ -53,8 +53,10 @@ public void Dispose() if (arr != null) ArrayPool.Shared.Return(arr); } } + [MethodImpl(MethodImplOptions.NoInlining)] private static T[] ThrowDisposed() => throw new ObjectDisposedException(nameof(Lease)); + private T[] Array { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -62,17 +64,17 @@ private T[] Array } /// - /// The data as a Memory + /// The data as a . /// public Memory Memory => new Memory(Array, 0, Length); /// - /// The data as a Span + /// The data as a . /// public Span Span => new Span(Array, 0, Length); /// - /// The data as an ArraySegment + /// The data as an . /// public ArraySegment ArraySegment => new ArraySegment(Array, 0, Length); } diff --git a/src/StackExchange.Redis/LinearRetry.cs b/src/StackExchange.Redis/LinearRetry.cs index 872654c92..809eaff15 100644 --- a/src/StackExchange.Redis/LinearRetry.cs +++ b/src/StackExchange.Redis/LinearRetry.cs @@ -1,7 +1,7 @@ -namespace StackExchange.Redis +namespace StackExchange.Redis { /// - /// Represents a retry policy that performs retries at a fixed interval. The retries are performed upto a maximum allowed time. + /// Represents a retry policy that performs retries at a fixed interval. The retries are performed up to a maximum allowed time. /// public class LinearRetry : IReconnectRetryPolicy { @@ -10,7 +10,7 @@ public class LinearRetry : IReconnectRetryPolicy /// /// Initializes a new instance using the specified maximum retry elapsed time allowed. /// - /// maximum elapsed time in milliseconds to be allowed for it to perform retries + /// maximum elapsed time in milliseconds to be allowed for it to perform retries. public LinearRetry(int maxRetryElapsedTimeAllowedMilliseconds) { this.maxRetryElapsedTimeAllowedMilliseconds = maxRetryElapsedTimeAllowedMilliseconds; @@ -19,11 +19,9 @@ public LinearRetry(int maxRetryElapsedTimeAllowedMilliseconds) /// /// This method is called by the ConnectionMultiplexer to determine if a reconnect operation can be retried now. /// - /// The number of times reconnect retries have already been made by the ConnectionMultiplexer while it was in the connecting state - /// Total elapsed time in milliseconds since the last reconnect retry was made - public bool ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSinceLastRetry) - { - return timeElapsedMillisecondsSinceLastRetry >= maxRetryElapsedTimeAllowedMilliseconds; - } + /// The number of times reconnect retries have already been made by the ConnectionMultiplexer while it was in the connecting state. + /// Total elapsed time in milliseconds since the last reconnect retry was made. + public bool ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSinceLastRetry) => + timeElapsedMillisecondsSinceLastRetry >= maxRetryElapsedTimeAllowedMilliseconds; } -} \ No newline at end of file +} diff --git a/src/StackExchange.Redis/LuaScript.cs b/src/StackExchange.Redis/LuaScript.cs index bcf3c673f..8e332461e 100644 --- a/src/StackExchange.Redis/LuaScript.cs +++ b/src/StackExchange.Redis/LuaScript.cs @@ -21,7 +21,7 @@ public sealed class LuaScript { // Since the mapping of "script text" -> LuaScript doesn't depend on any particular details of // the redis connection itself, this cache is global. - private static readonly ConcurrentDictionary Cache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary Cache = new(); /// /// The original Lua script that was used to create this. @@ -34,7 +34,9 @@ public sealed class LuaScript /// public string ExecutableScript { get; } - // Arguments are in the order they have to passed to the script in + /// + /// Arguments are in the order they have to passed to the script in. + /// internal string[] Arguments { get; } private bool HasArguments => Arguments?.Length > 0; @@ -167,7 +169,7 @@ public Task EvaluateAsync(IDatabaseAsync db, object ps = null, Redi /// Loads this LuaScript into the given IServer so it can be run with it's SHA1 hash, instead of /// passing the full script on each Evaluate or EvaluateAsync call. /// - /// Note: the FireAndForget command flag cannot be set + /// Note: the FireAndForget command flag cannot be set. /// /// The server to load the script on. /// The command flags to use. diff --git a/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs index 46e9dcd46..20246eacb 100644 --- a/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs @@ -46,7 +46,6 @@ internal async static Task AddListenersAsync(ConnectionMultiplexer muxer, LogPro /// /// Returns a string representing the maintenance event with all of its properties. /// - public override string ToString() - => RawMessage; + public override string ToString() => RawMessage; } } diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index c8fdf54f8..86336d5c6 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -130,7 +130,7 @@ protected Message(int db, CommandFlags flags, RedisCommand command) if (masterOnly) SetMasterOnly(); CreatedDateTime = DateTime.UtcNow; - CreatedTimestamp = System.Diagnostics.Stopwatch.GetTimestamp(); + CreatedTimestamp = Stopwatch.GetTimestamp(); Status = CommandStatus.WaitingToBeSent; } @@ -167,7 +167,7 @@ internal void PrepareToResend(ServerEndPoint resendTo, bool isMoved) performance = null; CreatedDateTime = DateTime.UtcNow; - CreatedTimestamp = System.Diagnostics.Stopwatch.GetTimestamp(); + CreatedTimestamp = Stopwatch.GetTimestamp(); performance = ProfiledCommand.NewAttachedToSameContext(oldPerformance, resendTo, isMoved); performance.SetMessage(this); Status = CommandStatus.WaitingToBeSent; @@ -216,10 +216,7 @@ public bool IsAdmin internal bool IsScriptUnavailable => (Flags & ScriptUnavailableFlag) != 0; - internal void SetScriptUnavailable() - { - Flags |= ScriptUnavailableFlag; - } + internal void SetScriptUnavailable() => Flags |= ScriptUnavailableFlag; public bool IsFireAndForget => (Flags & CommandFlags.FireAndForget) != 0; public bool IsInternalCall => (Flags & InternalCallFlag) != 0; @@ -235,64 +232,46 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command) return new CommandMessage(db, flags, command); } - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key) - { - return new CommandKeyMessage(db, flags, command, key); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key) => + new CommandKeyMessage(db, flags, command, key); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, in RedisKey key1) - { - return new CommandKeyKeyMessage(db, flags, command, key0, key1); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, in RedisKey key1) => + new CommandKeyKeyMessage(db, flags, command, key0, key1); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, in RedisKey key1, in RedisValue value) - { - return new CommandKeyKeyValueMessage(db, flags, command, key0, key1, value); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, in RedisKey key1, in RedisValue value) => + new CommandKeyKeyValueMessage(db, flags, command, key0, key1, value); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, in RedisKey key1, in RedisKey key2) - { - return new CommandKeyKeyKeyMessage(db, flags, command, key0, key1, key2); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, in RedisKey key1, in RedisKey key2) => + new CommandKeyKeyKeyMessage(db, flags, command, key0, key1, key2); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value) - { - return new CommandValueMessage(db, flags, command, value); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value) => + new CommandValueMessage(db, flags, command, value); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value) - { - return new CommandKeyValueMessage(db, flags, command, key, value); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value) => + new CommandKeyValueMessage(db, flags, command, key, value); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisChannel channel) - { - return new CommandChannelMessage(db, flags, command, channel); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisChannel channel) => + new CommandChannelMessage(db, flags, command, channel); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisChannel channel, in RedisValue value) - { - return new CommandChannelValueMessage(db, flags, command, channel, value); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisChannel channel, in RedisValue value) => + new CommandChannelValueMessage(db, flags, command, channel, value); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value, in RedisChannel channel) - { - return new CommandValueChannelMessage(db, flags, command, value, channel); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value, in RedisChannel channel) => + new CommandValueChannelMessage(db, flags, command, value, channel); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1) - { - return new CommandKeyValueValueMessage(db, flags, command, key, value0, value1); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1) => + new CommandKeyValueValueMessage(db, flags, command, key, value0, value1); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2) - { - return new CommandKeyValueValueValueMessage(db, flags, command, key, value0, value1, value2); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2) => + new CommandKeyValueValueValueMessage(db, flags, command, key, value0, value1, value2); public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, GeoEntry[] values) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(values); +#else if (values == null) throw new ArgumentNullException(nameof(values)); +#endif if (values.Length == 0) { throw new ArgumentOutOfRangeException(nameof(values)); @@ -300,7 +279,7 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i if (values.Length == 1) { var value = values[0]; - return Message.Create(db, flags, command, key, value.Longitude, value.Latitude, value.Member); + return Create(db, flags, command, key, value.Longitude, value.Latitude, value.Member); } var arr = new RedisValue[3 * values.Length]; int index = 0; @@ -313,35 +292,23 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i return new CommandKeyValuesMessage(db, flags, command, key, arr); } - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3) - { - return new CommandKeyValueValueValueValueMessage(db, flags, command, key, value0, value1, value2, value3); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3) => + new CommandKeyValueValueValueValueMessage(db, flags, command, key, value0, value1, value2, value3); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value0, in RedisValue value1) - { - return new CommandValueValueMessage(db, flags, command, value0, value1); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value0, in RedisValue value1) => + new CommandValueValueMessage(db, flags, command, value0, value1); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value, in RedisKey key) - { - return new CommandValueKeyMessage(db, flags, command, value, key); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value, in RedisKey key) => + new CommandValueKeyMessage(db, flags, command, value, key); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value0, in RedisValue value1, in RedisValue value2) - { - return new CommandValueValueValueMessage(db, flags, command, value0, value1, value2); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value0, in RedisValue value1, in RedisValue value2) => + new CommandValueValueValueMessage(db, flags, command, value0, value1, value2); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) - { - return new CommandValueValueValueValueValueMessage(db, flags, command, value0, value1, value2, value3, value4); - } + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) => + new CommandValueValueValueValueValueMessage(db, flags, command, value0, value1, value2, value3, value4); - public static Message CreateInSlot(int db, int slot, CommandFlags flags, RedisCommand command, RedisValue[] values) - { - return new CommandSlotValuesMessage(db, slot, flags, command, values); - } + public static Message CreateInSlot(int db, int slot, CommandFlags flags, RedisCommand command, RedisValue[] values) => + new CommandSlotValuesMessage(db, slot, flags, command, values); public static bool IsMasterOnly(RedisCommand command) { @@ -431,14 +398,14 @@ public virtual void AppendStormLog(StringBuilder sb) sb.Append(CommandAndKey); } - public virtual int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) { return ServerSelectionStrategy.NoSlot; } - public bool IsMasterOnly() - { - // note that the constructor runs the switch statement above, so - // this will alread be true for master-only commands, even if the - // user specified PreferMaster etc - return GetMasterReplicaFlags(Flags) == CommandFlags.DemandMaster; - } + public virtual int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => ServerSelectionStrategy.NoSlot; + + /// + /// Note that the constructor runs the switch statement above, so + /// this will already be true for master-only commands, even if the + /// user specified PreferMaster etc + /// + public bool IsMasterOnly() => GetMasterReplicaFlags(Flags) == CommandFlags.DemandMaster; /// /// This does a few important things: @@ -449,15 +416,10 @@ public bool IsMasterOnly() /// handshake messages, as they bypass the queue completely) /// 3: it disables non-pref logging, as it is usually server-targeted /// - public void SetInternalCall() - { - Flags |= InternalCallFlag; - } + public void SetInternalCall() => Flags |= InternalCallFlag; - public override string ToString() - { - return $"[{Db}]:{CommandAndKey} ({resultProcessor?.GetType().Name ?? "(n/a)"})"; - } + public override string ToString() => + $"[{Db}]:{CommandAndKey} ({resultProcessor?.GetType().Name ?? "(n/a)"})"; public void SetResponseReceived() => performance?.SetResponseReceived(); @@ -474,14 +436,7 @@ public void Complete() currBox?.ActivateContinuations(); } - internal bool ResultBoxIsAsync - { - get - { - var currBox = Volatile.Read(ref resultBox); - return currBox != null && currBox.IsAsync; - } - } + internal bool ResultBoxIsAsync => Volatile.Read(ref resultBox)?.IsAsync == true; internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, RedisKey[] keys) => keys.Length switch { @@ -513,7 +468,11 @@ internal bool ResultBoxIsAsync internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, RedisValue[] values) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(values); +#else if (values == null) throw new ArgumentNullException(nameof(values)); +#endif return values.Length switch { 0 => new CommandKeyMessage(db, flags, command, key), @@ -527,7 +486,11 @@ internal static Message Create(int db, CommandFlags flags, RedisCommand command, internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, RedisValue[] values, in RedisKey key1) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(values); +#else if (values == null) throw new ArgumentNullException(nameof(values)); +#endif return new CommandKeyValuesKeyMessage(db, flags, command, key0, values, key1); } @@ -657,13 +620,15 @@ internal void SetEnqueued(PhysicalConnection connection) internal void TryGetHeadMessages(out Message now, out Message next) { - var connection = _enqueuedTo; now = next = null; - if (connection != null) connection.GetHeadMessages(out now, out next); + _enqueuedTo?.GetHeadMessages(out now, out next); } - internal bool TryGetPhysicalState(out PhysicalConnection.WriteStatus ws, out PhysicalConnection.ReadStatus rs, - out long sentDelta, out long receivedDelta) + internal bool TryGetPhysicalState( + out PhysicalConnection.WriteStatus ws, + out PhysicalConnection.ReadStatus rs, + out long sentDelta, + out long receivedDelta) { var connection = _enqueuedTo; sentDelta = receivedDelta = -1; @@ -730,20 +695,13 @@ internal void SetAsking(bool value) else Flags &= ~AskingFlag; // and the bits taketh away } - internal void SetNoRedirect() - { - Flags |= CommandFlags.NoRedirect; - } + internal void SetNoRedirect() => Flags |= CommandFlags.NoRedirect; - internal void SetPreferMaster() - { + internal void SetPreferMaster() => Flags = (Flags & ~MaskMasterServerPreference) | CommandFlags.PreferMaster; - } - internal void SetPreferReplica() - { + internal void SetPreferReplica() => Flags = (Flags & ~MaskMasterServerPreference) | CommandFlags.PreferReplica; - } internal void SetSource(ResultProcessor resultProcessor, IResultBox resultBox) { // note order here reversed to prevent overload resolution errors @@ -767,7 +725,7 @@ internal void WriteTo(PhysicalConnection physical) { WriteImpl(physical); } - catch (Exception ex) when (!(ex is RedisCommandException)) // these have specific meaning; don't wrap + catch (Exception ex) when (ex is not RedisCommandException) // these have specific meaning; don't wrap { physical?.OnInternalError(ex); Fail(ConnectionFailureType.InternalFailure, ex, null); @@ -799,10 +757,7 @@ protected CommandKeyBase(int db, CommandFlags flags, RedisCommand command, in Re public override string CommandAndKey => Command + " " + (string)Key; - public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) - { - return serverSelectionStrategy.HashSlot(Key); - } + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(Key); } private sealed class CommandChannelMessage : CommandChannelBase @@ -1177,10 +1132,7 @@ public CommandSlotValuesMessage(int db, int slot, CommandFlags flags, RedisComma this.values = values; } - public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) - { - return slot; - } + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => slot; protected override void WriteImpl(PhysicalConnection physical) { diff --git a/src/StackExchange.Redis/NameValueEntry.cs b/src/StackExchange.Redis/NameValueEntry.cs index 7b2cee98e..20332e7bf 100644 --- a/src/StackExchange.Redis/NameValueEntry.cs +++ b/src/StackExchange.Redis/NameValueEntry.cs @@ -68,14 +68,14 @@ public static implicit operator NameValueEntry(KeyValuePair name == other.name && value == other.value; /// - /// Compares two values for equality + /// Compares two values for equality. /// /// The first to compare. /// The second to compare. public static bool operator ==(NameValueEntry x, NameValueEntry y) => x.name == y.name && x.value == y.value; /// - /// Compares two values for non-equality + /// Compares two values for non-equality. /// /// The first to compare. /// The second to compare. diff --git a/src/StackExchange.Redis/PerfCounterHelper.cs b/src/StackExchange.Redis/PerfCounterHelper.cs index e4f608d76..ea1969f1e 100644 --- a/src/StackExchange.Redis/PerfCounterHelper.cs +++ b/src/StackExchange.Redis/PerfCounterHelper.cs @@ -1,17 +1,20 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Threading; namespace StackExchange.Redis { -#pragma warning disable CA1416 // windows only APIs; we've guarded against that internal static class PerfCounterHelper { - private static readonly object staticLock = new object(); + private static readonly object staticLock = new(); private static volatile PerformanceCounter _cpu; private static volatile bool _disabled = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); +#if NET5_0_OR_GREATER + [SupportedOSPlatform("Windows")] +#endif public static bool TryGetSystemCPU(out float value) { value = -1; @@ -58,12 +61,10 @@ internal static string GetThreadPoolAndCPUSummary(bool includePerformanceCounter return $"IOCP: {iocp}, WORKER: {worker}, Local-CPU: {cpu}"; } - internal static string GetSystemCpuPercent() - { - return TryGetSystemCPU(out float systemCPU) + internal static string GetSystemCpuPercent() => + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && TryGetSystemCPU(out float systemCPU) ? Math.Round(systemCPU, 2) + "%" : "unavailable"; - } internal static int GetThreadPoolStats(out string iocp, out string worker) { @@ -79,5 +80,4 @@ internal static int GetThreadPoolStats(out string iocp, out string worker) return busyWorkerThreads; } } -#pragma warning restore CA1416 // windows only APIs; we've guarded against that } diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 57cfa8483..65edb99b4 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -79,14 +79,7 @@ public enum State : byte public ServerEndPoint ServerEndPoint { get; } - public long SubscriptionCount - { - get - { - var tmp = physical; - return tmp == null ? 0 : physical.SubscriptionCount; - } - } + public long SubscriptionCount => physical?.SubscriptionCount ?? 0; internal State ConnectionState => (State)state; internal bool IsBeating => Interlocked.CompareExchange(ref beating, 0, 0) == 1; @@ -372,7 +365,7 @@ internal void KeepAlive() msg.SetInternalCall(); Multiplexer.Trace("Enqueue: " + msg); Multiplexer.OnInfoMessage($"heartbeat ({physical?.LastWriteSecondsAgo}s >= {ServerEndPoint?.WriteEverySeconds}s, {physical?.GetSentAwaitingResponseCount()} waiting) '{msg.CommandAndKey}' on '{PhysicalName}' (v{features.Version})"); - physical?.UpdateLastWriteTime(); // pre-emptively + physical?.UpdateLastWriteTime(); // preemptively #pragma warning disable CS0618 var result = TryWriteSync(msg, ServerEndPoint.IsReplica); #pragma warning restore CS0618 @@ -470,6 +463,7 @@ private void AbandonPendingBacklog(Exception ex) next.SetExceptionAndComplete(ex, this); } } + internal void OnFullyEstablished(PhysicalConnection connection, string source) { Trace("OnFullyEstablished"); @@ -627,7 +621,8 @@ internal bool TryEnqueue(List messages, bool isReplica) var physical = this.physical; if (physical == null) return false; foreach (var message in messages) - { // deliberately not taking a single lock here; we don't care if + { + // deliberately not taking a single lock here; we don't care if // other threads manage to interleave - in fact, it would be desirable // (to avoid a batch monopolising the connection) #pragma warning disable CS0618 @@ -648,7 +643,7 @@ private WriteResult WriteMessageInsideLock(PhysicalConnection physical, Message var existingMessage = Interlocked.CompareExchange(ref _activeMessage, message, null); if (existingMessage != null) { - Multiplexer?.OnInfoMessage($"reentrant call to WriteMessageTakingWriteLock for {message.CommandAndKey}, {existingMessage.CommandAndKey} is still active"); + Multiplexer?.OnInfoMessage($"Reentrant call to WriteMessageTakingWriteLock for {message.CommandAndKey}, {existingMessage.CommandAndKey} is still active"); return WriteResult.NoConnectionAvailable; } #if DEBUG @@ -657,9 +652,9 @@ private WriteResult WriteMessageInsideLock(PhysicalConnection physical, Message #endif { physical.SetWriting(); - var messageIsSent = false; if (message is IMultiMessage multiMessage) { + var messageIsSent = false; SelectDatabaseInsideWriteLock(physical, message); // need to switch database *before* the transaction foreach (var subCommand in multiMessage.GetMessages(physical)) { @@ -810,12 +805,16 @@ private void StartBacklogProcessor() private volatile int _backlogProcessorRequestedTime; #endif - private void CheckBacklogForTimeouts() // check the head of the backlog queue, consuming anything that looks dead + /// + /// Crawls from the head of the backlog queue, consuming anything that should have timed out + /// and pruning it accordingly (these messages will get timeout exceptions). + /// + private void CheckBacklogForTimeouts() { var now = Environment.TickCount; var timeout = TimeoutMilliseconds; - // Because peeking at the backlog, checking message and then dequeueing, is not thread-safe, we do have to use + // Because peeking at the backlog, checking message and then dequeuing, is not thread-safe, we do have to use // a lock here, for mutual exclusion of backlog DEQUEUERS. Unfortunately. // But we reduce contention by only locking if we see something that looks timed out. while (_backlog.TryPeek(out Message message)) @@ -842,6 +841,7 @@ private void CheckBacklogForTimeouts() // check the head of the backlog queue, c message.SetExceptionAndComplete(ex, this); } } + internal enum BacklogStatus : byte { Inactive, @@ -859,6 +859,7 @@ internal enum BacklogStatus : byte SettingIdle, Faulted, } + private volatile BacklogStatus _backlogStatus; private async Task ProcessBacklogAsync() { @@ -885,6 +886,7 @@ private async Task ProcessBacklogAsync() #endif } _backlogStatus = BacklogStatus.Started; + #if DEBUG int acquiredTime = Environment.TickCount; var msToGetLock = unchecked(acquiredTime - tryToAcquireTime); @@ -994,7 +996,7 @@ private WriteResult TimedOutBeforeWrite(Message message) /// /// This writes a message to the output stream /// - /// The phsyical connection to write to. + /// The physical connection to write to. /// The message to be written. internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnection physical, Message message) { @@ -1065,7 +1067,10 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect return new ValueTask(result); } - catch (Exception ex) { return new ValueTask(HandleWriteException(message, ex)); } + catch (Exception ex) + { + return new ValueTask(HandleWriteException(message, ex)); + } finally { if (token.Success) @@ -1082,6 +1087,7 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect } } } + #if DEBUG private void RecordLockDuration(int lockTaken) { @@ -1249,7 +1255,10 @@ private void SelectDatabaseInsideWriteLock(PhysicalConnection connection, Messag private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection connection, Message message) { - if (message == null) return WriteResult.Success; // for some definition of success + if (message == null) + { + return WriteResult.Success; // for some definition of success + } bool isQueued = false; try diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index e5d65f059..e2cdc9f97 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -325,8 +325,12 @@ internal void SimulateConnectionFailure(SimulatedFailureType failureType) } } - public void RecordConnectionFailed(ConnectionFailureType failureType, Exception innerException = null, [CallerMemberName] string origin = null, - bool isInitialConnect = false, IDuplexPipe connectingPipe = null + public void RecordConnectionFailed( + ConnectionFailureType failureType, + Exception innerException = null, + [CallerMemberName] string origin = null, + bool isInitialConnect = false, + IDuplexPipe connectingPipe = null ) { Exception outerException = innerException; @@ -432,9 +436,7 @@ void add(string lk, string sk, string v) add("Version", "v", ExceptionFactory.GetLibVersion()); - outerException = innerException == null - ? new RedisConnectionException(failureType, exMessage.ToString()) - : new RedisConnectionException(failureType, exMessage.ToString(), innerException); + outerException = new RedisConnectionException(failureType, exMessage.ToString(), innerException); foreach (var kv in data) { @@ -501,11 +503,18 @@ internal static void IdentifyFailureType(Exception exception, ref ConnectionFail { if (exception != null && failureType == ConnectionFailureType.InternalFailure) { - if (exception is AggregateException) exception = exception.InnerException ?? exception; - if (exception is AuthenticationException) failureType = ConnectionFailureType.AuthenticationFailure; - else if (exception is EndOfStreamException) failureType = ConnectionFailureType.SocketClosed; - else if (exception is SocketException || exception is IOException) failureType = ConnectionFailureType.SocketFailure; - else if (exception is ObjectDisposedException) failureType = ConnectionFailureType.SocketClosed; + if (exception is AggregateException) + { + exception = exception.InnerException ?? exception; + } + + failureType = exception switch + { + AuthenticationException => ConnectionFailureType.AuthenticationFailure, + EndOfStreamException or ObjectDisposedException => ConnectionFailureType.SocketClosed, + SocketException or IOException => ConnectionFailureType.SocketFailure, + _ => failureType + }; } } @@ -528,8 +537,7 @@ internal void GetCounters(ConnectionCounters counters) internal Message GetReadModeCommand(bool isMasterOnly) { - var serverEndpoint = BridgeCouldBeNull?.ServerEndPoint; - if (serverEndpoint != null && serverEndpoint.RequiresReadMode) + if (BridgeCouldBeNull?.ServerEndPoint?.RequiresReadMode == true) { ReadMode requiredReadMode = isMasterOnly ? ReadMode.ReadWrite : ReadMode.ReadOnly; if (requiredReadMode != currentReadMode) @@ -543,7 +551,8 @@ internal Message GetReadModeCommand(bool isMasterOnly) } } else if (currentReadMode == ReadMode.ReadOnly) - { // we don't need it (because we're not a cluster, or not a replica), + { + // we don't need it (because we're not a cluster, or not a replica), // but we are in read-only mode; switch to read-write currentReadMode = ReadMode.ReadWrite; return ReusableReadWriteCommand; @@ -553,44 +562,47 @@ internal Message GetReadModeCommand(bool isMasterOnly) internal Message GetSelectDatabaseCommand(int targetDatabase, Message message) { - if (targetDatabase < 0) return null; - if (targetDatabase != currentDatabase) + if (targetDatabase < 0 || targetDatabase == currentDatabase) { - var serverEndpoint = BridgeCouldBeNull?.ServerEndPoint; - if (serverEndpoint == null) return null; - int available = serverEndpoint.Databases; - - if (!serverEndpoint.HasDatabases) // only db0 is available on cluster/twemproxy - { - if (targetDatabase != 0) - { // should never see this, since the API doesn't allow it; thus not too worried about ExceptionFactory - throw new RedisCommandException("Multiple databases are not supported on this server; cannot switch to database: " + targetDatabase); - } - return null; - } + return null; + } - if (message.Command == RedisCommand.SELECT) - { - // this could come from an EVAL/EVALSHA inside a transaction, for example; we'll accept it - BridgeCouldBeNull?.Trace("Switching database: " + targetDatabase); - currentDatabase = targetDatabase; - return null; - } + if (BridgeCouldBeNull?.ServerEndPoint is not ServerEndPoint serverEndpoint) + { + return null; + } + int available = serverEndpoint.Databases; - if (TransactionActive) - {// should never see this, since the API doesn't allow it; thus not too worried about ExceptionFactory - throw new RedisCommandException("Multiple databases inside a transaction are not currently supported: " + targetDatabase); + if (!serverEndpoint.HasDatabases) // only db0 is available on cluster/twemproxy + { + if (targetDatabase != 0) + { // should never see this, since the API doesn't allow it; thus not too worried about ExceptionFactory + throw new RedisCommandException("Multiple databases are not supported on this server; cannot switch to database: " + targetDatabase); } + return null; + } - if (available != 0 && targetDatabase >= available) // we positively know it is out of range - { - throw ExceptionFactory.DatabaseOutfRange(IncludeDetailInExceptions, targetDatabase, message, serverEndpoint); - } + if (message.Command == RedisCommand.SELECT) + { + // this could come from an EVAL/EVALSHA inside a transaction, for example; we'll accept it BridgeCouldBeNull?.Trace("Switching database: " + targetDatabase); currentDatabase = targetDatabase; - return GetSelectDatabaseCommand(targetDatabase); + return null; } - return null; + + if (TransactionActive) + { + // should never see this, since the API doesn't allow it; thus not too worried about ExceptionFactory + throw new RedisCommandException("Multiple databases inside a transaction are not currently supported: " + targetDatabase); + } + + if (available != 0 && targetDatabase >= available) // we positively know it is out of range + { + throw ExceptionFactory.DatabaseOutfRange(IncludeDetailInExceptions, targetDatabase, message, serverEndpoint); + } + BridgeCouldBeNull?.Trace("Switching database: " + targetDatabase); + currentDatabase = targetDatabase; + return GetSelectDatabaseCommand(targetDatabase); } internal static Message GetSelectDatabaseCommand(int targetDatabase) @@ -631,11 +643,8 @@ internal void OnBridgeHeartbeat() lock (_writtenAwaitingResponse) { - if (_writtenAwaitingResponse.Count != 0) + if (_writtenAwaitingResponse.Count != 0 && BridgeCouldBeNull is PhysicalBridge bridge) { - var bridge = BridgeCouldBeNull; - if (bridge == null) return; - var server = bridge?.ServerEndPoint; var timeout = bridge.Multiplexer.AsyncTimeoutMilliseconds; foreach (var msg in _writtenAwaitingResponse) @@ -650,8 +659,8 @@ internal void OnBridgeHeartbeat() msg.SetExceptionAndComplete(timeoutEx, bridge); // tell the message that it is doomed bridge.Multiplexer.OnAsyncTimeout(); } - // note: it is important that we **do not** remove the message unless we're tearing down the socket; that - // would disrupt the chain for MatchResult; we just pre-emptively abort the message from the caller's + // Note: it is important that we **do not** remove the message unless we're tearing down the socket; that + // would disrupt the chain for MatchResult; we just preemptively abort the message from the caller's // perspective, and set a flag on the message so we don't keep doing it } } @@ -660,15 +669,15 @@ internal void OnBridgeHeartbeat() internal void OnInternalError(Exception exception, [CallerMemberName] string origin = null) { - var bridge = BridgeCouldBeNull; - if (bridge != null) + if (BridgeCouldBeNull is PhysicalBridge bridge) { bridge.Multiplexer.OnInternalError(exception, bridge.ServerEndPoint.EndPoint, connectionType, origin); } } internal void SetUnknownDatabase() - { // forces next db-specific command to issue a select + { + // forces next db-specific command to issue a select currentDatabase = -1; } @@ -1301,6 +1310,9 @@ internal readonly struct ConnectionStatus /// public WriteStatus WriteStatus { get; init; } + public override string ToString() => + $"SentAwaitingResponse: {MessagesSentAwaitingResponse}, AvailableOnSocket: {BytesAvailableOnSocket} byte(s), InReadPipe: {BytesInReadPipe} byte(s), InWritePipe: {BytesInWritePipe} byte(s), ReadStatus: {ReadStatus}, WriteStatus: {WriteStatus}"; + /// /// The default connection stats, notable *not* the same as default since initializers don't run. /// @@ -1537,9 +1549,18 @@ private void MatchResult(in RawResult result) _readStatus = ReadStatus.DequeueResult; lock (_writtenAwaitingResponse) { +#if NET5_0_OR_GREATER + if (!_writtenAwaitingResponse.TryDequeue(out msg)) + { + throw new InvalidOperationException("Received response with no message waiting: " + result.ToString()); + }; +#else if (_writtenAwaitingResponse.Count == 0) + { throw new InvalidOperationException("Received response with no message waiting: " + result.ToString()); + } msg = _writtenAwaitingResponse.Dequeue(); +#endif } _activeMessage = msg; diff --git a/src/StackExchange.Redis/Profiling/ProfiledCommand.cs b/src/StackExchange.Redis/Profiling/ProfiledCommand.cs index 7d6a8fcfe..5f4ff899f 100644 --- a/src/StackExchange.Redis/Profiling/ProfiledCommand.cs +++ b/src/StackExchange.Redis/Profiling/ProfiledCommand.cs @@ -116,10 +116,8 @@ public void SetCompleted() } } - public override string ToString() - { - return - $@"EndPoint = {EndPoint} + public override string ToString() => +$@"EndPoint = {EndPoint} Db = {Db} Command = {Command} CommandCreated = {CommandCreated:u} @@ -129,7 +127,6 @@ public override string ToString() ResponseToCompletion = {ResponseToCompletion} ElapsedTime = {ElapsedTime} Flags = {Flags} -RetransmissionOf = ({RetransmissionOf})"; - } +RetransmissionOf = ({RetransmissionOf?.ToString() ?? "nothing"})"; } } diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index d138d4e37..58f40361d 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -17,7 +17,7 @@ internal readonly struct RawResult internal static readonly RawResult NullMultiBulk = new RawResult(default(Sequence), isNull: true); internal static readonly RawResult EmptyMultiBulk = new RawResult(default(Sequence), isNull: false); internal static readonly RawResult Nil = default; - // note: can't use Memory here - struct recursion breaks runtimr + // Note: can't use Memory here - struct recursion breaks runtime private readonly Sequence _items; private readonly ResultType _type; @@ -59,19 +59,13 @@ public override string ToString() { if (IsNull) return "(null)"; - switch (Type) + return Type switch { - case ResultType.SimpleString: - case ResultType.Integer: - case ResultType.Error: - return $"{Type}: {GetString()}"; - case ResultType.BulkString: - return $"{Type}: {Payload.Length} bytes"; - case ResultType.MultiBulk: - return $"{Type}: {ItemsCount} items"; - default: - return $"(unknown: {Type})"; - } + ResultType.SimpleString or ResultType.Integer or ResultType.Error => $"{Type}: {GetString()}", + ResultType.BulkString => $"{Type}: {Payload.Length} bytes", + ResultType.MultiBulk => $"{Type}: {ItemsCount} items", + _ => $"(unknown: {Type})", + }; } public Tokenizer GetInlineTokenizer() => new Tokenizer(Payload); @@ -145,17 +139,11 @@ internal RedisChannel AsRedisChannel(byte[] channelPrefix, RedisChannel.PatternM } } - internal RedisKey AsRedisKey() + internal RedisKey AsRedisKey() => Type switch { - switch (Type) - { - case ResultType.SimpleString: - case ResultType.BulkString: - return (RedisKey)GetBlob(); - default: - throw new InvalidCastException("Cannot convert to RedisKey: " + Type); - } - } + ResultType.SimpleString or ResultType.BulkString => (RedisKey)GetBlob(), + _ => throw new InvalidCastException("Cannot convert to RedisKey: " + Type), + }; internal RedisValue AsRedisValue() { diff --git a/src/StackExchange.Redis/RedisChannel.cs b/src/StackExchange.Redis/RedisChannel.cs index 85bcf1a49..14fa956cf 100644 --- a/src/StackExchange.Redis/RedisChannel.cs +++ b/src/StackExchange.Redis/RedisChannel.cs @@ -51,114 +51,85 @@ private RedisChannel(byte[] value, bool isPatternBased) /// /// The first to compare. /// The second to compare. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static bool operator !=(RedisChannel x, RedisChannel y) => !(x == y); -#pragma warning restore RCS1231 // Make parameter ref read-only. /// /// Indicate whether two channel names are not equal /// /// The first to compare. /// The second to compare. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static bool operator !=(string x, RedisChannel y) => !(x == y); -#pragma warning restore RCS1231 // Make parameter ref read-only. /// /// Indicate whether two channel names are not equal /// /// The first to compare. /// The second to compare. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static bool operator !=(byte[] x, RedisChannel y) => !(x == y); -#pragma warning restore RCS1231 // Make parameter ref read-only. /// /// Indicate whether two channel names are not equal /// /// The first to compare. /// The second to compare. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static bool operator !=(RedisChannel x, string y) => !(x == y); -#pragma warning restore RCS1231 // Make parameter ref read-only. /// /// Indicate whether two channel names are not equal /// /// The first to compare. /// The second to compare. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static bool operator !=(RedisChannel x, byte[] y) => !(x == y); -#pragma warning restore RCS1231 // Make parameter ref read-only. /// /// Indicate whether two channel names are equal /// /// The first to compare. /// The second to compare. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static bool operator ==(RedisChannel x, RedisChannel y) => x.IsPatternBased == y.IsPatternBased && RedisValue.Equals(x.Value, y.Value); -#pragma warning restore RCS1231 // Make parameter ref read-only. /// /// Indicate whether two channel names are equal /// /// The first to compare. /// The second to compare. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static bool operator ==(string x, RedisChannel y) => RedisValue.Equals(x == null ? null : Encoding.UTF8.GetBytes(x), y.Value); -#pragma warning restore RCS1231 // Make parameter ref read-only. /// /// Indicate whether two channel names are equal /// /// The first to compare. /// The second to compare. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static bool operator ==(byte[] x, RedisChannel y) => RedisValue.Equals(x, y.Value); -#pragma warning restore RCS1231 // Make parameter ref read-only. /// /// Indicate whether two channel names are equal /// /// The first to compare. /// The second to compare. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static bool operator ==(RedisChannel x, string y) => RedisValue.Equals(x.Value, y == null ? null : Encoding.UTF8.GetBytes(y)); -#pragma warning restore RCS1231 // Make parameter ref read-only. /// /// Indicate whether two channel names are equal /// /// The first to compare. /// The second to compare. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static bool operator ==(RedisChannel x, byte[] y) => RedisValue.Equals(x.Value, y); -#pragma warning restore RCS1231 // Make parameter ref read-only. /// /// See Object.Equals /// /// The to compare to. - public override bool Equals(object obj) + public override bool Equals(object obj) => obj switch { - if (obj is RedisChannel rcObj) - { - return RedisValue.Equals(Value, (rcObj).Value); - } - if (obj is string sObj) - { - return RedisValue.Equals(Value, Encoding.UTF8.GetBytes(sObj)); - } - if (obj is byte[] bObj) - { - return RedisValue.Equals(Value, bObj); - } - return false; - } + RedisChannel rcObj => RedisValue.Equals(Value, rcObj.Value), + string sObj => RedisValue.Equals(Value, Encoding.UTF8.GetBytes(sObj)), + byte[] bObj => RedisValue.Equals(Value, bObj), + _ => false + }; /// /// Indicate whether two channel names are equal @@ -174,10 +145,7 @@ public override bool Equals(object obj) /// /// Obtains a string representation of the channel name /// - public override string ToString() - { - return ((string)this) ?? "(null)"; - } + public override string ToString() => ((string)this) ?? "(null)"; internal static bool AssertStarts(byte[] value, byte[] expected) { @@ -238,17 +206,13 @@ public static implicit operator RedisChannel(byte[] key) /// Obtain the channel name as a . /// /// The channel to get a byte[] from. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static implicit operator byte[] (RedisChannel key) => key.Value; -#pragma warning restore RCS1231 // Make parameter ref read-only. /// /// Obtain the channel name as a . /// /// The channel to get a string from. -#pragma warning disable RCS1231 // Make parameter ref read-only. - public API public static implicit operator string (RedisChannel key) -#pragma warning restore RCS1231 // Make parameter ref read-only. { var arr = key.Value; if (arr == null) return null; diff --git a/src/StackExchange.Redis/RedisErrorEventArgs.cs b/src/StackExchange.Redis/RedisErrorEventArgs.cs index 4b34b7a5f..3722f8df4 100644 --- a/src/StackExchange.Redis/RedisErrorEventArgs.cs +++ b/src/StackExchange.Redis/RedisErrorEventArgs.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis { /// - /// Notification of errors from the redis server + /// Notification of errors from the redis server. /// public class RedisErrorEventArgs : EventArgs, ICompletable { @@ -33,12 +33,12 @@ public RedisErrorEventArgs(object sender, EndPoint endpoint, string message) } /// - /// The origin of the message + /// The origin of the message. /// public EndPoint EndPoint { get; } /// - /// The message from the server + /// The message from the server. /// public string Message { get; } diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 2d61f9b67..bb5479380 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -272,12 +272,12 @@ orderby prop.Name public override bool Equals(object obj) => obj is RedisFeatures f && f.Version == Version; /// - /// Checks if 2 RedisFeatures are .Equal() + /// Checks if 2 RedisFeatures are .Equal(). /// public static bool operator ==(RedisFeatures left, RedisFeatures right) => left.Equals(right); /// - /// Checks if 2 RedisFeatures are not .Equal() + /// Checks if 2 RedisFeatures are not .Equal(). /// public static bool operator !=(RedisFeatures left, RedisFeatures right) => !left.Equals(right); } diff --git a/src/StackExchange.Redis/RedisKey.cs b/src/StackExchange.Redis/RedisKey.cs index 725871c72..4b20cdd38 100644 --- a/src/StackExchange.Redis/RedisKey.cs +++ b/src/StackExchange.Redis/RedisKey.cs @@ -38,77 +38,77 @@ internal bool IsEmpty internal object KeyValue { get; } /// - /// Indicate whether two keys are not equal + /// Indicate whether two keys are not equal. /// /// The first to compare. /// The second to compare. public static bool operator !=(RedisKey x, RedisKey y) => !(x == y); /// - /// Indicate whether two keys are not equal + /// Indicate whether two keys are not equal. /// /// The first to compare. /// The second to compare. public static bool operator !=(string x, RedisKey y) => !(x == y); /// - /// Indicate whether two keys are not equal + /// Indicate whether two keys are not equal. /// /// The first to compare. /// The second to compare. public static bool operator !=(byte[] x, RedisKey y) => !(x == y); /// - /// Indicate whether two keys are not equal + /// Indicate whether two keys are not equal. /// /// The first to compare. /// The second to compare. public static bool operator !=(RedisKey x, string y) => !(x == y); /// - /// Indicate whether two keys are not equal + /// Indicate whether two keys are not equal. /// /// The first to compare. /// The second to compare. public static bool operator !=(RedisKey x, byte[] y) => !(x == y); /// - /// Indicate whether two keys are equal + /// Indicate whether two keys are equal. /// /// The first to compare. /// The second to compare. public static bool operator ==(RedisKey x, RedisKey y) => CompositeEquals(x.KeyPrefix, x.KeyValue, y.KeyPrefix, y.KeyValue); /// - /// Indicate whether two keys are equal + /// Indicate whether two keys are equal. /// /// The first to compare. /// The second to compare. public static bool operator ==(string x, RedisKey y) => CompositeEquals(null, x, y.KeyPrefix, y.KeyValue); /// - /// Indicate whether two keys are equal + /// Indicate whether two keys are equal. /// /// The first to compare. /// The second to compare. public static bool operator ==(byte[] x, RedisKey y) => CompositeEquals(null, x, y.KeyPrefix, y.KeyValue); /// - /// Indicate whether two keys are equal + /// Indicate whether two keys are equal. /// /// The first to compare. /// The second to compare. public static bool operator ==(RedisKey x, string y) => CompositeEquals(x.KeyPrefix, x.KeyValue, null, y); /// - /// Indicate whether two keys are equal + /// Indicate whether two keys are equal. /// /// The first to compare. /// The second to compare. public static bool operator ==(RedisKey x, byte[] y) => CompositeEquals(x.KeyPrefix, x.KeyValue, null, y); /// - /// See Object.Equals + /// See . /// /// The to compare to. public override bool Equals(object obj) @@ -125,7 +125,7 @@ public override bool Equals(object obj) } /// - /// Indicate whether two keys are equal + /// Indicate whether two keys are equal. /// /// The to compare to. public bool Equals(RedisKey other) => CompositeEquals(KeyPrefix, KeyValue, other.KeyPrefix, other.KeyValue); @@ -145,7 +145,7 @@ private static bool CompositeEquals(byte[] keyPrefix0, object keyValue0, byte[] } /// - /// See Object.GetHashCode + /// See . /// public override int GetHashCode() { @@ -156,7 +156,7 @@ public override int GetHashCode() } /// - /// Obtains a string representation of the key + /// Obtains a string representation of the key. /// public override string ToString() => ((string)this) ?? "(null)"; @@ -227,7 +227,7 @@ public static implicit operator string(RedisKey key) } /// - /// Concatenate two keys + /// Concatenate two keys. /// /// The first to add. /// The second to add. @@ -285,8 +285,7 @@ internal static byte[] ConcatenateBytes(byte[] a, object b, byte[] c) /// /// Prepends p to this RedisKey, returning a new RedisKey. /// - /// Avoids some allocations if possible, repeated Prepend/Appends make - /// it less possible. + /// Avoids some allocations if possible, repeated Prepend/Appends make it less possible. /// /// /// The prefix to prepend. @@ -295,8 +294,7 @@ internal static byte[] ConcatenateBytes(byte[] a, object b, byte[] c) /// /// Appends p to this RedisKey, returning a new RedisKey. /// - /// Avoids some allocations if possible, repeated Prepend/Appends make - /// it less possible. + /// Avoids some allocations if possible, repeated Prepend/Appends make it less possible. /// /// /// The suffix to append. diff --git a/src/StackExchange.Redis/RedisResult.cs b/src/StackExchange.Redis/RedisResult.cs index 5ac1735e2..0b62aef4b 100644 --- a/src/StackExchange.Redis/RedisResult.cs +++ b/src/StackExchange.Redis/RedisResult.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis { /// - /// Represents a general-purpose result from redis, that may be cast into various anticipated types + /// Represents a general-purpose result from redis, that may be cast into various anticipated types. /// public abstract class RedisResult { @@ -35,12 +35,12 @@ public static RedisResult Create(RedisResult[] values) => values == null ? NullArray : values.Length == 0 ? EmptyArray : new ArrayRedisResult(values); /// - /// An empty array result + /// An empty array result. /// internal static RedisResult EmptyArray { get; } = new ArrayRedisResult(Array.Empty()); /// - /// A null array result + /// A null array result. /// internal static RedisResult NullArray { get; } = new ArrayRedisResult(null); @@ -84,12 +84,12 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul } /// - /// Indicate the type of result that was received from redis + /// Indicate the type of result that was received from redis. /// public abstract ResultType Type { get; } /// - /// Indicates whether this result was a null result + /// Indicates whether this result was a null result. /// public abstract bool IsNull { get; } @@ -218,9 +218,9 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul public static explicit operator RedisResult[](RedisResult result) => result?.AsRedisResultArray(); /// - /// Interprets a multi-bulk result with successive key/name values as a dictionary keyed by name + /// Interprets a multi-bulk result with successive key/name values as a dictionary keyed by name. /// - /// The key comparator to use, or by default + /// The key comparator to use, or by default. public Dictionary ToDictionary(IEqualityComparer comparer = null) { var arr = AsRedisResultArray(); diff --git a/src/StackExchange.Redis/RedisStream.cs b/src/StackExchange.Redis/RedisStream.cs index 6dc6ac1e5..eb3c97967 100644 --- a/src/StackExchange.Redis/RedisStream.cs +++ b/src/StackExchange.Redis/RedisStream.cs @@ -17,7 +17,7 @@ internal RedisStream(RedisKey key, StreamEntry[] entries) public RedisKey Key { get; } /// - /// An arry of entries contained within the stream. + /// An array of entries contained within the stream. /// public StreamEntry[] Entries { get; } } diff --git a/src/StackExchange.Redis/RedisTransaction.cs b/src/StackExchange.Redis/RedisTransaction.cs index 8bbee7d52..cec65c311 100644 --- a/src/StackExchange.Redis/RedisTransaction.cs +++ b/src/StackExchange.Redis/RedisTransaction.cs @@ -535,17 +535,4 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } } - //internal class RedisDatabaseTransaction : RedisCoreTransaction, ITransaction - //{ - // public IRedisDatabaseAsync Pending { get { return this; } } - - // bool ITransaction.Execute(CommandFlags flags) - // { - // return ExecuteTransaction(flags); - // } - // Task ITransaction.ExecuteAsync(CommandFlags flags) - // { - // return ExecuteTransactionAsync(flags); - // } - //} } diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index 88b2a241b..cb6a90fa9 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -47,10 +47,10 @@ public RedisValue(string value) : this(0, default, value) { } internal long DirectOverlappedBits64 => _overlappedBits64; #pragma warning restore RCS1085 // Use auto-implemented property. - private readonly static object Sentinel_SignedInteger = new object(); - private readonly static object Sentinel_UnsignedInteger = new object(); - private readonly static object Sentinel_Raw = new object(); - private readonly static object Sentinel_Double = new object(); + private readonly static object Sentinel_SignedInteger = new(); + private readonly static object Sentinel_UnsignedInteger = new(); + private readonly static object Sentinel_Raw = new(); + private readonly static object Sentinel_Double = new(); /// /// Obtain this value as an object - to be used alongside Unbox @@ -238,21 +238,14 @@ public override bool Equals(object obj) private static int GetHashCode(RedisValue x) { x = x.Simplify(); - switch (x.Type) + return x.Type switch { - case StorageType.Null: - return -1; - case StorageType.Double: - return x.OverlappedValueDouble.GetHashCode(); - case StorageType.Int64: - case StorageType.UInt64: - return x._overlappedBits64.GetHashCode(); - case StorageType.Raw: - return ((string)x).GetHashCode(); // to match equality - case StorageType.String: - default: - return x._objectOrSentinel.GetHashCode(); - } + StorageType.Null => -1, + StorageType.Double => x.OverlappedValueDouble.GetHashCode(), + StorageType.Int64 or StorageType.UInt64 => x._overlappedBits64.GetHashCode(), + StorageType.Raw => ((string)x).GetHashCode(),// to match equality + _ => x._objectOrSentinel.GetHashCode(), + }; } /// diff --git a/src/StackExchange.Redis/RedisValueWithExpiry.cs b/src/StackExchange.Redis/RedisValueWithExpiry.cs index 340e3365b..d2ebbeb56 100644 --- a/src/StackExchange.Redis/RedisValueWithExpiry.cs +++ b/src/StackExchange.Redis/RedisValueWithExpiry.cs @@ -3,12 +3,12 @@ namespace StackExchange.Redis { /// - /// Describes a value/expiry pair + /// Describes a value/expiry pair. /// public readonly struct RedisValueWithExpiry { /// - /// Creates a from a and a + /// Creates a from a and a . /// public RedisValueWithExpiry(RedisValue value, TimeSpan? expiry) { @@ -17,12 +17,12 @@ public RedisValueWithExpiry(RedisValue value, TimeSpan? expiry) } /// - /// The expiry of this record + /// The expiry of this record. /// public TimeSpan? Expiry { get; } /// - /// The value of this record + /// The value of this record. /// public RedisValue Value { get; } } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index d17a1902a..0fc523316 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -196,7 +196,7 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in var server = bridge.ServerEndPoint; bool log = !message.IsInternalCall; bool isMoved = result.StartsWith(CommonReplies.MOVED); - bool wasNoRedirect = ( message.Flags & CommandFlags.NoRedirect ) != 0; + bool wasNoRedirect = (message.Flags & CommandFlags.NoRedirect) != 0; string err = string.Empty; bool unableToConnectError = false; if (isMoved || result.StartsWith(CommonReplies.ASK)) diff --git a/src/StackExchange.Redis/ServerCounters.cs b/src/StackExchange.Redis/ServerCounters.cs index 96b4bbb60..f3f29cde2 100644 --- a/src/StackExchange.Redis/ServerCounters.cs +++ b/src/StackExchange.Redis/ServerCounters.cs @@ -4,7 +4,7 @@ namespace StackExchange.Redis { /// - /// Illustrates the queues associates with this server + /// Illustrates the queues associates with this server. /// public class ServerCounters { @@ -21,26 +21,27 @@ public ServerCounters(EndPoint endpoint) } /// - /// The endpoint to which this data relates (this can be null if the data represents all servers) + /// The endpoint to which this data relates (this can be null if the data represents all servers). /// public EndPoint EndPoint { get; } /// - /// Counters associated with the interactive (non pub-sub) connection + /// Counters associated with the interactive (non pub-sub) connection. /// public ConnectionCounters Interactive { get; } /// - /// Counters associated with other ambient activity + /// Counters associated with other ambient activity. /// public ConnectionCounters Other { get; } /// - /// Counters associated with the subscription (pub-sub) connection + /// Counters associated with the subscription (pub-sub) connection. /// public ConnectionCounters Subscription { get; } + /// - /// Indicates the total number of outstanding items against this server + /// Indicates the total number of outstanding items against this server. /// public long TotalOutstanding => Interactive.TotalOutstanding + Subscription.TotalOutstanding + Other.TotalOutstanding; diff --git a/src/StackExchange.Redis/SocketManager.cs b/src/StackExchange.Redis/SocketManager.cs index 1be2eb9bc..2478f725a 100644 --- a/src/StackExchange.Redis/SocketManager.cs +++ b/src/StackExchange.Redis/SocketManager.cs @@ -15,19 +15,19 @@ namespace StackExchange.Redis public sealed partial class SocketManager : IDisposable { /// - /// Gets the name of this SocketManager instance + /// Gets the name of this SocketManager instance. /// public string Name { get; } /// - /// Creates a new instance + /// Creates a new instance. /// /// The name for this . public SocketManager(string name) : this(name, DEFAULT_WORKERS, SocketManagerOptions.None) { } /// - /// Creates a new instance + /// Creates a new instance. /// /// The name for this . /// Whether this should use high priority sockets. @@ -35,7 +35,7 @@ public SocketManager(string name, bool useHighPrioritySocketThreads) : this(name, DEFAULT_WORKERS, UseHighPrioritySocketThreads(useHighPrioritySocketThreads)) { } /// - /// Creates a new (optionally named) instance + /// Creates a new (optionally named) instance. /// /// The name for this . /// the number of dedicated workers for this . @@ -47,13 +47,13 @@ private static SocketManagerOptions UseHighPrioritySocketThreads(bool value) => value ? SocketManagerOptions.UseHighPrioritySocketThreads : SocketManagerOptions.None; /// - /// Additional options for configuring the socket manager + /// Additional options for configuring the socket manager. /// [Flags] public enum SocketManagerOptions { /// - /// No additional options + /// No additional options. /// None = 0, /// @@ -67,7 +67,7 @@ public enum SocketManagerOptions } /// - /// Creates a new (optionally named) instance + /// Creates a new (optionally named) instance. /// /// The name for this . /// the number of dedicated workers for this . @@ -118,7 +118,7 @@ public SocketManager(string name = null, int workerCount = 0, SocketManagerOptio } /// - /// Default / shared socket manager using a dedicated thread-pool + /// Default / shared socket manager using a dedicated thread-pool. /// public static SocketManager Shared { @@ -139,7 +139,7 @@ public static SocketManager Shared } /// - /// Shared socket manager using the main thread-pool + /// Shared socket manager using the main thread-pool. /// public static SocketManager ThreadPool { @@ -159,7 +159,9 @@ public static SocketManager ThreadPool } } - /// Returns a string that represents the current object. + /// + /// Returns a string that represents the current object. + /// /// A string that represents the current object. public override string ToString() { @@ -181,11 +183,11 @@ public override string ToString() private enum CallbackOperation { Read, - Error + Error, } /// - /// Releases all resources associated with this instance + /// Releases all resources associated with this instance. /// public void Dispose() { @@ -205,7 +207,7 @@ private void DisposeRefs() } /// - /// Releases *appropriate* resources associated with this instance + /// Releases *appropriate* resources associated with this instance. /// ~SocketManager() => DisposeRefs(); diff --git a/src/StackExchange.Redis/SortedSetEntry.cs b/src/StackExchange.Redis/SortedSetEntry.cs index 21151639b..4bd09899b 100644 --- a/src/StackExchange.Redis/SortedSetEntry.cs +++ b/src/StackExchange.Redis/SortedSetEntry.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis { /// - /// Describes a sorted-set element with the corresponding value + /// Describes a sorted-set element with the corresponding value. /// public readonly struct SortedSetEntry : IEquatable, IComparable, IComparable { @@ -24,84 +24,84 @@ public SortedSetEntry(RedisValue element, double score) } /// - /// The unique element stored in the sorted set + /// The unique element stored in the sorted set. /// public RedisValue Element => element; /// - /// The score against the element + /// The score against the element. /// public double Score => score; /// - /// The score against the element + /// The score against the element. /// [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Please use Score", false)] - public double Value { get { return score; } } + public double Value => score; /// - /// The unique element stored in the sorted set + /// The unique element stored in the sorted set. /// [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Please use Element", false)] - public RedisValue Key { get { return element; } } + public RedisValue Key => element; /// - /// Converts to a key/value pair + /// Converts to a key/value pair. /// /// The to get a for. public static implicit operator KeyValuePair(SortedSetEntry value) => new KeyValuePair(value.element, value.score); /// - /// Converts from a key/value pair + /// Converts from a key/value pair. /// /// The to get a for. public static implicit operator SortedSetEntry(KeyValuePair value) => new SortedSetEntry(value.Key, value.Value); /// - /// See Object.ToString() + /// See . /// public override string ToString() => element + ": " + score; /// - /// See Object.GetHashCode() + /// See . /// public override int GetHashCode() => element.GetHashCode() ^ score.GetHashCode(); /// - /// Compares two values for equality + /// Compares two values for equality. /// /// The to compare to. public override bool Equals(object obj) => obj is SortedSetEntry ssObj && Equals(ssObj); /// - /// Compares two values for equality + /// Compares two values for equality. /// /// The to compare to. public bool Equals(SortedSetEntry other) => score == other.score && element == other.element; /// - /// Compares two values by score + /// Compares two values by score. /// /// The to compare to. public int CompareTo(SortedSetEntry other) => score.CompareTo(other.score); /// - /// Compares two values by score + /// Compares two values by score. /// /// The to compare to. public int CompareTo(object obj) => obj is SortedSetEntry ssObj ? CompareTo(ssObj) : -1; /// - /// Compares two values for equality + /// Compares two values for equality. /// /// The first to compare. /// The second to compare. public static bool operator ==(SortedSetEntry x, SortedSetEntry y) => x.score == y.score && x.element == y.element; /// - /// Compares two values for non-equality + /// Compares two values for non-equality. /// /// The first to compare. /// The second to compare. diff --git a/src/StackExchange.Redis/StreamConsumerInfo.cs b/src/StackExchange.Redis/StreamConsumerInfo.cs index 49b50ef7f..a23e27bd0 100644 --- a/src/StackExchange.Redis/StreamConsumerInfo.cs +++ b/src/StackExchange.Redis/StreamConsumerInfo.cs @@ -2,7 +2,7 @@ namespace StackExchange.Redis { /// - /// Describes a consumer within a consumer group, retrieved using the XINFO CONSUMERS command. + /// Describes a consumer within a consumer group, retrieved using the XINFO CONSUMERS command. . /// public readonly struct StreamConsumerInfo { diff --git a/src/StackExchange.Redis/StreamEntry.cs b/src/StackExchange.Redis/StreamEntry.cs index b9cf50176..fb9238da3 100644 --- a/src/StackExchange.Redis/StreamEntry.cs +++ b/src/StackExchange.Redis/StreamEntry.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis public readonly struct StreamEntry { /// - /// Creates an stream entry + /// Creates an stream entry. /// public StreamEntry(RedisValue id, NameValueEntry[] values) { @@ -32,7 +32,7 @@ public StreamEntry(RedisValue id, NameValueEntry[] values) public NameValueEntry[] Values { get; } /// - /// Search for a specific field by name, returning the value + /// Search for a specific field by name, returning the value. /// public RedisValue this[RedisValue fieldName] { diff --git a/src/StackExchange.Redis/StreamGroupInfo.cs b/src/StackExchange.Redis/StreamGroupInfo.cs index 93a35cceb..d854a0039 100644 --- a/src/StackExchange.Redis/StreamGroupInfo.cs +++ b/src/StackExchange.Redis/StreamGroupInfo.cs @@ -2,7 +2,7 @@ namespace StackExchange.Redis { /// - /// Describes a consumer group retrieved using the XINFO GROUPS command. + /// Describes a consumer group retrieved using the XINFO GROUPS command. . /// public readonly struct StreamGroupInfo { @@ -31,7 +31,7 @@ internal StreamGroupInfo(string name, int consumerCount, int pendingMessageCount public int PendingMessageCount { get; } /// - /// The Id of the last message delivered to the group + /// The Id of the last message delivered to the group. /// public string LastDeliveredId { get; } } diff --git a/src/StackExchange.Redis/StreamInfo.cs b/src/StackExchange.Redis/StreamInfo.cs index 318224279..77c0dcf86 100644 --- a/src/StackExchange.Redis/StreamInfo.cs +++ b/src/StackExchange.Redis/StreamInfo.cs @@ -2,7 +2,7 @@ namespace StackExchange.Redis { /// - /// Describes stream information retrieved using the XINFO STREAM command. + /// Describes stream information retrieved using the XINFO STREAM command. . /// public readonly struct StreamInfo { @@ -55,7 +55,7 @@ internal StreamInfo( public StreamEntry LastEntry { get; } /// - /// The last generated id + /// The last generated id. /// public RedisValue LastGeneratedId { get; } } diff --git a/src/StackExchange.Redis/StreamPosition.cs b/src/StackExchange.Redis/StreamPosition.cs index 979735d35..b58b77304 100644 --- a/src/StackExchange.Redis/StreamPosition.cs +++ b/src/StackExchange.Redis/StreamPosition.cs @@ -50,9 +50,10 @@ internal static RedisValue Resolve(RedisValue value, RedisCommand command) // new is only valid for the above _ => throw new ArgumentException($"Unsupported command in StreamPosition.Resolve: {command}.", nameof(command)), }; - } else if (value == StreamPosition.Beginning) + } + else if (value == StreamPosition.Beginning) { - switch(command) + switch (command) { case RedisCommand.XREAD: case RedisCommand.XREADGROUP: diff --git a/src/StackExchange.Redis/TaskSource.cs b/src/StackExchange.Redis/TaskSource.cs index c1cd6fff0..5df86af89 100644 --- a/src/StackExchange.Redis/TaskSource.cs +++ b/src/StackExchange.Redis/TaskSource.cs @@ -5,11 +5,11 @@ namespace StackExchange.Redis internal static class TaskSource { /// - /// Create a new TaskCompletion source + /// Create a new TaskCompletion source. /// /// The type for the created . /// The state for the created . - /// The options to apply to the task + /// The options to apply to the task. public static TaskCompletionSource Create(object asyncState, TaskCreationOptions options = TaskCreationOptions.None) => new TaskCompletionSource(asyncState, options); } diff --git a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs index 2aa994807..9d9c88f8c 100644 --- a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs +++ b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs @@ -115,7 +115,7 @@ public async Task Issue922_ReconnectRaised() muxer.ConnectionRestored += (s, e) => { Interlocked.Increment(ref restoreCount); - Log($"Connection Failed ({e.ConnectionType},{e.FailureType}): {e.Exception}"); + Log($"Connection Restored ({e.ConnectionType},{e.FailureType}): {e.Exception}"); }; muxer.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index d1d2ef408..a74521acb 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -195,7 +195,8 @@ public void Teardown() } Skip.Inconclusive($"There were {privateFailCount} private and {sharedFailCount.Value} ambient exceptions; expected {expectedFailCount}."); } - Log($"Service Counts: (Scheduler) Queue: {SocketManager.Shared?.SchedulerPool?.TotalServicedByQueue.ToString()}, Pool: {SocketManager.Shared?.SchedulerPool?.TotalServicedByPool.ToString()}"); + var pool = SocketManager.Shared?.SchedulerPool; + Log($"Service Counts: (Scheduler) Queue: {pool?.TotalServicedByQueue.ToString()}, Pool: {pool?.TotalServicedByPool.ToString()}, Workers: {pool?.WorkerCount.ToString()}, Available: {pool?.AvailableCount.ToString()}"); } protected IServer GetServer(IConnectionMultiplexer muxer) From 745f75f9494dc3a07d84d44efc2f475fed08a86d Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 18 Jan 2022 09:07:51 -0500 Subject: [PATCH 060/435] Update libs: pulling in the disposed Arena fix from Pipelines.Unofficial (#1944) Pulling this in separately from #1912 as a minimal PR. --- Directory.Build.targets | 4 ++-- docs/ReleaseNotes.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index ac2529fe4..b52c8705b 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -13,7 +13,7 @@ - + @@ -25,6 +25,6 @@ - + \ No newline at end of file diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 96f86de19..d3d2a4224 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,6 +7,7 @@ - Update assumed redis versions to v2.8 or v4.0 in the Azure case (#1929 via NickCraver) - Fix profiler showing `EVAL` instead `EVALSHA` (#1930 via martinpotter) - Moved tiebreaker fetching in connections into the handshake phase (streamline + simplification) (#1931 via NickCraver) +- Fixed potential disposed object usage around Arenas (pulling in [Piplines.Sockets.Unofficial#63](https://github.com/mgravell/Pipelines.Sockets.Unofficial/pull/63) by MarcGravell) ## 2.2.88 From 92fdfa3b256d70b77ab4eee6fac93c70b2f413cd Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 18 Jan 2022 11:53:57 -0500 Subject: [PATCH 061/435] Support latest ClientFlags (#1953) This adds the Redis 6.0+ flags from #1912 --- src/StackExchange.Redis/ClientInfo.cs | 7 +++++++ src/StackExchange.Redis/Enums/ClientFlags.cs | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/StackExchange.Redis/ClientInfo.cs b/src/StackExchange.Redis/ClientInfo.cs index 91a1cf3c3..92620bcc7 100644 --- a/src/StackExchange.Redis/ClientInfo.cs +++ b/src/StackExchange.Redis/ClientInfo.cs @@ -47,6 +47,9 @@ public sealed class ClientInfo /// S: the client is a normal replica server /// U: the client is connected via a Unix domain socket /// x: the client is in a MULTI/EXEC context + /// t: the client enabled keys tracking in order to perform client side caching + /// R: the client tracking target client is invalid + /// B: the client enabled broadcast tracking mode /// public string FlagsRaw { get; private set; } @@ -173,6 +176,10 @@ internal static ClientInfo[] Parse(string input) AddFlag(ref flags, value, ClientFlags.UnixDomainSocket, 'U'); AddFlag(ref flags, value, ClientFlags.Transaction, 'x'); + AddFlag(ref flags, value, ClientFlags.KeysTracking, 't'); + AddFlag(ref flags, value, ClientFlags.TrackingTargetInvalid, 'R'); + AddFlag(ref flags, value, ClientFlags.BroadcastTracking, 'B'); + client.Flags = flags; break; case "id": client.Id = Format.ParseInt64(value); break; diff --git a/src/StackExchange.Redis/Enums/ClientFlags.cs b/src/StackExchange.Redis/Enums/ClientFlags.cs index a652f61a4..559a13799 100644 --- a/src/StackExchange.Redis/Enums/ClientFlags.cs +++ b/src/StackExchange.Redis/Enums/ClientFlags.cs @@ -85,5 +85,17 @@ public enum ClientFlags : long /// the client is connected via a Unix domain socket /// UnixDomainSocket = 2048, + /// + /// the client enabled keys tracking in order to perform client side caching + /// + KeysTracking = 4096, + /// + /// the client tracking target client is invalid + /// + TrackingTargetInvalid = 8192, + /// + /// the client enabled broadcast tracking mode + /// + BroadcastTracking = 16384, } } From 11943b3e3172cf58f44a603d3118768724a3c3b7 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 19 Jan 2022 08:51:25 -0500 Subject: [PATCH 062/435] Writer: switch back to SemaphoreSlim (#1949) Since Semaphore slim has been fixed on all the platforms we're building for these days, this tests moving back. Getting some test run comparison data, but all synthetic benchmarks are looking good. See https://github.com/mgravell/Pipelines.Sockets.Unofficial/issues/64 for details Here's the main contention test metrics from those benchmarks: | Method | Runtime | Mean | Error | StdDev | Allocated | |------------------------------ |----------- |-------------:|-----------:|-----------:|----------:| | SemaphoreSlim_Sync | .NET 6.0 | 18.246 ns | 0.2540 ns | 0.2375 ns | - | | MutexSlim_Sync | .NET 6.0 | 22.292 ns | 0.1948 ns | 0.1627 ns | - | | SemaphoreSlim_Sync | .NET 4.7.2 | 65.291 ns | 0.5218 ns | 0.4357 ns | - | | MutexSlim_Sync | .NET 4.7.2 | 43.145 ns | 0.3944 ns | 0.3689 ns | - | | | | | | | | | SemaphoreSlim_Async | .NET 6.0 | 20.920 ns | 0.2461 ns | 0.2302 ns | - | | MutexSlim_Async | .NET 6.0 | 42.810 ns | 0.4583 ns | 0.4287 ns | - | | SemaphoreSlim_Async | .NET 4.7.2 | 57.513 ns | 0.5600 ns | 0.5238 ns | - | | MutexSlim_Async | .NET 4.7.2 | 76.444 ns | 0.3811 ns | 0.3379 ns | - | | | | | | | | | SemaphoreSlim_Async_HotPath | .NET 6.0 | 15.182 ns | 0.1708 ns | 0.1598 ns | - | | MutexSlim_Async_HotPath | .NET 6.0 | 29.913 ns | 0.5884 ns | 0.6776 ns | - | | SemaphoreSlim_Async_HotPath | .NET 4.7.2 | 50.912 ns | 0.8782 ns | 0.8215 ns | - | | MutexSlim_Async_HotPath | .NET 4.7.2 | 55.409 ns | 0.7513 ns | 0.6660 ns | - | | | | | | | | | SemaphoreSlim_ConcurrentAsync | .NET 6.0 | 2,084.316 ns | 4.5909 ns | 4.0698 ns | 547 B | | MutexSlim_ConcurrentAsync | .NET 6.0 | 2,120.714 ns | 28.5866 ns | 26.7399 ns | 125 B | | SemaphoreSlim_ConcurrentAsync | .NET 4.7.2 | 3,812.444 ns | 42.4014 ns | 37.5877 ns | 1,449 B | | MutexSlim_ConcurrentAsync | .NET 4.7.2 | 2,883.994 ns | 46.5535 ns | 41.2685 ns | 284 B | We don't have high contention tests, but sanity checking against our test suite (where we don't expect this to matter much): Main branch: ![main test speed](https://user-images.githubusercontent.com/454813/149633288-0b1fb4ac-44f8-4151-92e1-610b678610d2.png) PR branch: ![pr test speed](https://user-images.githubusercontent.com/454813/149633275-0cc20e3a-ba6b-49b6-bfed-563ed8e343d0.png) We could scope this back to .NET 6.0+ only, but the code's a lot more `#ifdef` and complicated (because `LockTokens` aren't a thing - it's just a bool "did I get it?")...thoughts? --- src/StackExchange.Redis/PhysicalBridge.cs | 170 ++++++++++++++++++---- 1 file changed, 141 insertions(+), 29 deletions(-) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 65edb99b4..0e25bd189 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -7,10 +7,13 @@ using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; -using Pipelines.Sockets.Unofficial.Threading; -using static Pipelines.Sockets.Unofficial.Threading.MutexSlim; using static StackExchange.Redis.ConnectionMultiplexer; using PendingSubscriptionState = global::StackExchange.Redis.ConnectionMultiplexer.Subscription.PendingSubscriptionState; +#if !NETCOREAPP +using Pipelines.Sockets.Unofficial.Threading; +using static Pipelines.Sockets.Unofficial.Threading.MutexSlim; +#endif + namespace StackExchange.Redis { @@ -46,6 +49,12 @@ internal sealed class PhysicalBridge : IDisposable private volatile int state = (int)State.Disconnected; +#if NETCOREAPP + private readonly SemaphoreSlim _singleWriterMutex = new(1,1); +#else + private readonly MutexSlim _singleWriterMutex; +#endif + internal string PhysicalName => physical?.ToString(); public PhysicalBridge(ServerEndPoint serverEndPoint, ConnectionType type, int timeoutMilliseconds) { @@ -54,7 +63,9 @@ public PhysicalBridge(ServerEndPoint serverEndPoint, ConnectionType type, int ti Multiplexer = serverEndPoint.Multiplexer; Name = Format.ToString(serverEndPoint.EndPoint) + "/" + ConnectionType.ToString(); TimeoutMilliseconds = timeoutMilliseconds; +#if !NETCOREAPP _singleWriterMutex = new MutexSlim(timeoutMilliseconds: timeoutMilliseconds); +#endif } private readonly int TimeoutMilliseconds; @@ -309,7 +320,11 @@ internal readonly struct BridgeStatus internal BridgeStatus GetStatus() => new() { MessagesSinceLastHeartbeat = (int)(Interlocked.Read(ref operationCount) - Interlocked.Read(ref profileLastLog)), +#if NETCOREAPP + IsWriterActive = _singleWriterMutex.CurrentCount == 0, +#else IsWriterActive = !_singleWriterMutex.IsAvailable, +#endif BacklogMessagesPending = _backlog.Count, BacklogStatus = _backlogStatus, Connection = physical?.GetStatus() ?? PhysicalConnection.ConnectionStatus.Default, @@ -633,8 +648,6 @@ internal bool TryEnqueue(List messages, bool isReplica) return true; } - private readonly MutexSlim _singleWriterMutex; - private Message _activeMessage; private WriteResult WriteMessageInsideLock(PhysicalConnection physical, Message message) @@ -716,11 +729,20 @@ internal WriteResult WriteMessageTakingWriteLockSync(PhysicalConnection physical return WriteResult.Success; // queued counts as success } +#if NETCOREAPP + bool gotLock = false; +#else LockToken token = default; +#endif try { +#if NETCOREAPP + gotLock = _singleWriterMutex.Wait(0); + if (!gotLock) +#else token = _singleWriterMutex.TryWait(WaitOptions.NoDelay); if (!token.Success) +#endif { // we can't get it *instantaneously*; is there // perhaps a backlog and active backlog processor? @@ -729,8 +751,13 @@ internal WriteResult WriteMessageTakingWriteLockSync(PhysicalConnection physical // no backlog... try to wait with the timeout; // if we *still* can't get it: that counts as // an actual timeout +#if NETCOREAPP + gotLock = _singleWriterMutex.Wait(TimeoutMilliseconds); + if (!gotLock) return TimedOutBeforeWrite(message); +#else token = _singleWriterMutex.TryWait(); if (!token.Success) return TimedOutBeforeWrite(message); +#endif } var result = WriteMessageInsideLock(physical, message); @@ -747,7 +774,14 @@ internal WriteResult WriteMessageTakingWriteLockSync(PhysicalConnection physical finally { UnmarkActiveMessage(message); +#if NETCOREAPP + if (gotLock) + { + _singleWriterMutex.Release(); + } +#else token.Dispose(); +#endif } } @@ -863,7 +897,11 @@ internal enum BacklogStatus : byte private volatile BacklogStatus _backlogStatus; private async Task ProcessBacklogAsync() { +#if NETCOREAPP + bool gotLock = false; +#else LockToken token = default; +#endif try { #if DEBUG @@ -878,8 +916,13 @@ private async Task ProcessBacklogAsync() if (_backlog.IsEmpty) return; // nothing to do // try and get the lock; if unsuccessful, retry +#if NETCOREAPP + gotLock = await _singleWriterMutex.WaitAsync(TimeoutMilliseconds).ConfigureAwait(false); + if (gotLock) break; // got the lock; now go do something with it +#else token = await _singleWriterMutex.TryWaitAsync().ConfigureAwait(false); if (token.Success) break; // got the lock; now go do something with it +#endif #if DEBUG failureCount++; @@ -962,8 +1005,15 @@ private async Task ProcessBacklogAsync() _backlogStatus = BacklogStatus.Faulted; } finally - { + { +#if NETCOREAPP + if (gotLock) + { + _singleWriterMutex.Release(); + } +#else token.Dispose(); +#endif // Do this in finally block, so that thread aborts can't convince us the backlog processor is running forever if (Interlocked.CompareExchange(ref _backlogProcessorIsRunning, 0, 1) != 1) @@ -994,7 +1044,7 @@ private WriteResult TimedOutBeforeWrite(Message message) } /// - /// This writes a message to the output stream + /// This writes a message to the output stream. /// /// The physical connection to write to. /// The message to be written. @@ -1025,13 +1075,22 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect bool releaseLock = true; // fine to default to true, as it doesn't matter until token is a "success" int lockTaken = 0; +#if NETCOREAPP + bool gotLock = false; +#else LockToken token = default; +#endif try { // try to acquire it synchronously // note: timeout is specified in mutex-constructor +#if NETCOREAPP + gotLock = _singleWriterMutex.Wait(0); + if (!gotLock) +#else token = _singleWriterMutex.TryWait(options: WaitOptions.NoDelay); if (!token.Success) +#endif { // we can't get it *instantaneously*; is there // perhaps a backlog and active backlog processor? @@ -1041,11 +1100,19 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect // no backlog... try to wait with the timeout; // if we *still* can't get it: that counts as // an actual timeout +#if NETCOREAPP + var pending = _singleWriterMutex.WaitAsync(TimeoutMilliseconds); + if (pending.Status != TaskStatus.RanToCompletion) return WriteMessageTakingWriteLockAsync_Awaited(pending, physical, message); + + gotLock = pending.Result; // fine since we know we got a result + if (!gotLock) return new ValueTask(TimedOutBeforeWrite(message)); +#else var pending = _singleWriterMutex.TryWaitAsync(options: WaitOptions.DisableAsyncContext); if (!pending.IsCompletedSuccessfully) return WriteMessageTakingWriteLockAsync_Awaited(pending, physical, message); token = pending.Result; // fine since we know we got a result if (!token.Success) return new ValueTask(TimedOutBeforeWrite(message)); +#endif } lockTaken = Environment.TickCount; @@ -1057,7 +1124,11 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect if (!flush.IsCompletedSuccessfully) { releaseLock = false; // so we don't release prematurely +#if NETCOREAPP + return CompleteWriteAndReleaseLockAsync(flush, message, lockTaken); +#else return CompleteWriteAndReleaseLockAsync(token, flush, message, lockTaken); +#endif } result = flush.Result; // we know it was completed, this is fine @@ -1073,7 +1144,11 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect } finally { +#if NETCOREAPP + if (gotLock) +#else if (token.Success) +#endif { UnmarkActiveMessage(message); @@ -1082,7 +1157,11 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect #if DEBUG RecordLockDuration(lockTaken); #endif +#if NETCOREAPP + _singleWriterMutex.Release(); +#else token.Dispose(); +#endif } } } @@ -1097,30 +1176,42 @@ private void RecordLockDuration(int lockTaken) volatile int _maxLockDuration = -1; #endif - private async ValueTask WriteMessageTakingWriteLockAsync_Awaited(ValueTask pending, PhysicalConnection physical, Message message) + private async ValueTask WriteMessageTakingWriteLockAsync_Awaited( +#if NETCOREAPP + Task pending, +#else + ValueTask pending, +#endif + PhysicalConnection physical, Message message) { +#if NETCOREAPP + bool gotLock = false; +#endif + try { - using (var token = await pending.ForAwait()) - { - if (!token.Success) return TimedOutBeforeWrite(message); +#if NETCOREAPP + gotLock = await pending.ForAwait(); + if (!gotLock) return TimedOutBeforeWrite(message); +#else + using var token = await pending.ForAwait(); +#endif #if DEBUG - int lockTaken = Environment.TickCount; + int lockTaken = Environment.TickCount; #endif - var result = WriteMessageInsideLock(physical, message); + var result = WriteMessageInsideLock(physical, message); - if (result == WriteResult.Success) - { - result = await physical.FlushAsync(false).ForAwait(); - } + if (result == WriteResult.Success) + { + result = await physical.FlushAsync(false).ForAwait(); + } - physical.SetIdle(); + physical.SetIdle(); #if DEBUG - RecordLockDuration(lockTaken); + RecordLockDuration(lockTaken); #endif - return result; - } + return result; } catch (Exception ex) { @@ -1129,22 +1220,43 @@ private async ValueTask WriteMessageTakingWriteLockAsync_Awaited(Va finally { UnmarkActiveMessage(message); +#if NETCOREAPP + if (gotLock) + { + _singleWriterMutex.Release(); + } +#endif } } - private async ValueTask CompleteWriteAndReleaseLockAsync(LockToken lockToken, ValueTask flush, Message message, int lockTaken) + private async ValueTask CompleteWriteAndReleaseLockAsync( +#if !NETCOREAPP + LockToken lockToken, +#endif + ValueTask flush, + Message message, + int lockTaken) { +#if !NETCOREAPP using (lockToken) +#endif + try + { + var result = await flush.ForAwait(); + physical.SetIdle(); + return result; + } + catch (Exception ex) + { + return HandleWriteException(message, ex); + } + finally { - try - { - var result = await flush.ForAwait(); - physical.SetIdle(); - return result; - } - catch (Exception ex) { return HandleWriteException(message, ex); } #if DEBUG - finally { RecordLockDuration(lockTaken); } + RecordLockDuration(lockTaken); +#endif +#if NETCOREAPP + _singleWriterMutex.Release(); #endif } } From d8fbc966c5ebb0e1b645bcf7a1de343d3e9311e5 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 19 Jan 2022 15:04:59 -0500 Subject: [PATCH 063/435] AppVeyor: only build main branch (#1957) Eliminate the double builds on branches before a PR is open --- appveyor.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 51d5f56a4..7387352eb 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -50,6 +50,10 @@ install: Start-Service redis-* } +branches: + only: + - main + skip_branch_with_pr: true skip_tags: true skip_commits: From 76853f6343fc43531472098f2fb558ed342eaff3 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 20 Jan 2022 11:18:35 -0500 Subject: [PATCH 064/435] Debug: lots of pruning (#1956) In DEBUG we had lots of additional logging around locks and flushes, but the reality is these are in a slower unoptimized state and heavily influencing the results especially of lock contention themselves. This removes a lot of the debug code we had when first building this around timings. --- src/StackExchange.Redis/ExceptionFactory.cs | 4 - src/StackExchange.Redis/Message.cs | 18 --- src/StackExchange.Redis/PhysicalBridge.cs | 119 ++++-------------- src/StackExchange.Redis/PhysicalConnection.cs | 51 +------- 4 files changed, 28 insertions(+), 164 deletions(-) diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 4cc274d24..ca35a16f6 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -244,10 +244,6 @@ internal static Exception Timeout(ConnectionMultiplexer multiplexer, string base Add(data, sb, "Timeout", "timeout", Format.ToString(multiplexer.TimeoutMilliseconds)); try { -#if DEBUG - if (message.QueuePosition >= 0) Add(data, sb, "QueuePosition", null, message.QueuePosition.ToString()); // the position the item was when added to the queue - if ((int)message.ConnectionWriteState >= 0) Add(data, sb, "WriteState", null, message.ConnectionWriteState.ToString()); // what the physical was doing when it was added to the queue -#endif if (message != null && message.TryGetPhysicalState(out var ws, out var rs, out var sentDelta, out var receivedDelta)) { Add(data, sb, "Write-State", null, ws.ToString()); diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 86336d5c6..89d1dac1d 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -54,20 +54,6 @@ internal abstract class Message : ICompletable { public readonly int Db; -#if DEBUG - internal int QueuePosition { get; private set; } - internal PhysicalConnection.WriteStatus ConnectionWriteState { get; private set; } -#endif - [Conditional("DEBUG")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "DEBUG uses instance data")] - internal void SetBacklogState(int position, PhysicalConnection physical) - { -#if DEBUG - QueuePosition = position; - ConnectionWriteState = physical?.GetWriteStatus() ?? PhysicalConnection.WriteStatus.NA; -#endif - } - internal const CommandFlags InternalCallFlag = (CommandFlags)128; protected RedisCommand command; @@ -601,10 +587,6 @@ internal bool TrySetResult(T value) internal void SetEnqueued(PhysicalConnection connection) { -#if DEBUG - QueuePosition = -1; - ConnectionWriteState = PhysicalConnection.WriteStatus.NA; -#endif SetWriteTime(); performance?.SetEnqueued(); _enqueuedTo = connection; diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 0e25bd189..a9f5b5b22 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -139,7 +139,6 @@ private WriteResult QueueOrFailMessage(Message message) // you can go in the queue, but we won't be starting // a worker, because the handshake has not completed message.SetEnqueued(null); - message.SetBacklogState(_backlog.Count, null); _backlog.Enqueue(message); return WriteResult.Success; // we'll take it... } @@ -659,61 +658,40 @@ private WriteResult WriteMessageInsideLock(PhysicalConnection physical, Message Multiplexer?.OnInfoMessage($"Reentrant call to WriteMessageTakingWriteLock for {message.CommandAndKey}, {existingMessage.CommandAndKey} is still active"); return WriteResult.NoConnectionAvailable; } -#if DEBUG - int startWriteTime = Environment.TickCount; - try -#endif + + physical.SetWriting(); + if (message is IMultiMessage multiMessage) { - physical.SetWriting(); - if (message is IMultiMessage multiMessage) + var messageIsSent = false; + SelectDatabaseInsideWriteLock(physical, message); // need to switch database *before* the transaction + foreach (var subCommand in multiMessage.GetMessages(physical)) { - var messageIsSent = false; - SelectDatabaseInsideWriteLock(physical, message); // need to switch database *before* the transaction - foreach (var subCommand in multiMessage.GetMessages(physical)) + result = WriteMessageToServerInsideWriteLock(physical, subCommand); + if (result != WriteResult.Success) { - result = WriteMessageToServerInsideWriteLock(physical, subCommand); - if (result != WriteResult.Success) - { - // we screwed up; abort; note that WriteMessageToServer already - // killed the underlying connection - Trace("Unable to write to server"); - message.Fail(ConnectionFailureType.ProtocolFailure, null, "failure before write: " + result.ToString()); - message.Complete(); - return result; - } - //The parent message (next) may be returned from GetMessages - //and should not be marked as sent again below - messageIsSent = messageIsSent || subCommand == message; + // we screwed up; abort; note that WriteMessageToServer already + // killed the underlying connection + Trace("Unable to write to server"); + message.Fail(ConnectionFailureType.ProtocolFailure, null, "failure before write: " + result.ToString()); + message.Complete(); + return result; } - if (!messageIsSent) - { - message.SetRequestSent(); // well, it was attempted, at least... - } - - return WriteResult.Success; + //The parent message (next) may be returned from GetMessages + //and should not be marked as sent again below + messageIsSent = messageIsSent || subCommand == message; } - else + if (!messageIsSent) { - return WriteMessageToServerInsideWriteLock(physical, message); + message.SetRequestSent(); // well, it was attempted, at least... } + + return WriteResult.Success; } -#if DEBUG - finally + else { - int endWriteTime = Environment.TickCount; - int writeDuration = unchecked(endWriteTime - startWriteTime); - if (writeDuration > _maxWriteTime) - { - _maxWriteTime = writeDuration; - _maxWriteCommand = message?.Command ?? default; - } + return WriteMessageToServerInsideWriteLock(physical, message); } -#endif } -#if DEBUG - private volatile int _maxWriteTime = -1; - private RedisCommand _maxWriteCommand; -#endif [Obsolete("prefer async")] internal WriteResult WriteMessageTakingWriteLockSync(PhysicalConnection physical, Message message) @@ -796,7 +774,6 @@ private bool PushToBacklog(Message message, bool onlyIfExists) int count = _backlog.Count; - message.SetBacklogState(count, physical); _backlog.Enqueue(message); // The correct way to decide to start backlog process is not based on previously empty @@ -811,9 +788,6 @@ private void StartBacklogProcessor() { if (Interlocked.CompareExchange(ref _backlogProcessorIsRunning, 1, 0) == 0) { -#if DEBUG - _backlogProcessorRequestedTime = Environment.TickCount; -#endif _backlogStatus = BacklogStatus.Activating; #if NET6_0_OR_GREATER @@ -835,9 +809,6 @@ private void StartBacklogProcessor() #endif } } -#if DEBUG - private volatile int _backlogProcessorRequestedTime; -#endif /// /// Crawls from the head of the backlog queue, consuming anything that should have timed out @@ -904,11 +875,6 @@ private async Task ProcessBacklogAsync() #endif try { -#if DEBUG - int tryToAcquireTime = Environment.TickCount; - var msToStartWorker = unchecked(tryToAcquireTime - _backlogProcessorRequestedTime); - int failureCount = 0; -#endif _backlogStatus = BacklogStatus.Starting; while (true) { @@ -923,18 +889,9 @@ private async Task ProcessBacklogAsync() token = await _singleWriterMutex.TryWaitAsync().ConfigureAwait(false); if (token.Success) break; // got the lock; now go do something with it #endif - -#if DEBUG - failureCount++; -#endif } _backlogStatus = BacklogStatus.Started; -#if DEBUG - int acquiredTime = Environment.TickCount; - var msToGetLock = unchecked(acquiredTime - tryToAcquireTime); -#endif - // so now we are the writer; write some things! Message message; var timeout = TimeoutMilliseconds; @@ -955,15 +912,6 @@ private async Task ProcessBacklogAsync() { _backlogStatus = BacklogStatus.RecordingTimeout; var ex = Multiplexer.GetException(WriteResult.TimeoutBeforeWrite, message, ServerEndPoint); -#if DEBUG // additional tracking - ex.Data["Redis-BacklogStartDelay"] = msToStartWorker; - ex.Data["Redis-BacklogGetLockDelay"] = msToGetLock; - if (failureCount != 0) ex.Data["Redis-BacklogFailCount"] = failureCount; - if (_maxWriteTime >= 0) ex.Data["Redis-MaxWrite"] = _maxWriteTime.ToString() + "ms, " + _maxWriteCommand.ToString(); - var maxFlush = physical?.MaxFlushTime ?? -1; - if (maxFlush >= 0) ex.Data["Redis-MaxFlush"] = maxFlush.ToString() + "ms, " + (physical?.MaxFlushBytes ?? -1).ToString(); - if (_maxLockDuration >= 0) ex.Data["Redis-MaxLockDuration"] = _maxLockDuration; -#endif message.SetExceptionAndComplete(ex, this); } else @@ -1154,9 +1102,6 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect if (releaseLock) { -#if DEBUG - RecordLockDuration(lockTaken); -#endif #if NETCOREAPP _singleWriterMutex.Release(); #else @@ -1167,15 +1112,6 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect } } -#if DEBUG - private void RecordLockDuration(int lockTaken) - { - var lockDuration = unchecked(Environment.TickCount - lockTaken); - if (lockDuration > _maxLockDuration) _maxLockDuration = lockDuration; - } - volatile int _maxLockDuration = -1; -#endif - private async ValueTask WriteMessageTakingWriteLockAsync_Awaited( #if NETCOREAPP Task pending, @@ -1195,9 +1131,6 @@ private async ValueTask WriteMessageTakingWriteLockAsync_Awaited( if (!gotLock) return TimedOutBeforeWrite(message); #else using var token = await pending.ForAwait(); -#endif -#if DEBUG - int lockTaken = Environment.TickCount; #endif var result = WriteMessageInsideLock(physical, message); @@ -1208,9 +1141,6 @@ private async ValueTask WriteMessageTakingWriteLockAsync_Awaited( physical.SetIdle(); -#if DEBUG - RecordLockDuration(lockTaken); -#endif return result; } catch (Exception ex) @@ -1252,9 +1182,6 @@ private async ValueTask CompleteWriteAndReleaseLockAsync( } finally { -#if DEBUG - RecordLockDuration(lockTaken); -#endif #if NETCOREAPP _singleWriterMutex.Release(); #endif diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index e2cdc9f97..2924368a4 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -890,18 +890,11 @@ internal static int WriteRaw(Span span, long value, bool withLengthPrefix } [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "DEBUG uses instance data")] - private async ValueTask FlushAsync_Awaited(PhysicalConnection connection, ValueTask flush, bool throwOnFailure -#if DEBUG - , int startFlush, long flushBytes -#endif - ) + private async ValueTask FlushAsync_Awaited(PhysicalConnection connection, ValueTask flush, bool throwOnFailure) { try { await flush.ForAwait(); -#if DEBUG - RecordEndFlush(startFlush, flushBytes); -#endif connection._writeStatus = WriteStatus.Flushed; connection.UpdateLastWriteTime(); return WriteResult.Success; @@ -943,9 +936,6 @@ internal WriteResult FlushSync(bool throwOnFailure, int millisecondsTimeout) void ThrowTimeout() { -#if DEBUG - if (millisecondsTimeout > _maxFlushTime) _maxFlushTime = millisecondsTimeout; // a fair bet even if we didn't measure -#endif throw new TimeoutException("timeout while synchronously flushing"); } } @@ -956,20 +946,8 @@ internal ValueTask FlushAsync(bool throwOnFailure, CancellationToke try { _writeStatus = WriteStatus.Flushing; -#if DEBUG - int startFlush = Environment.TickCount; - long flushBytes = -1; - if (_ioPipe is SocketConnection sc) flushBytes = sc.GetCounters().BytesWaitingToBeSent; -#endif var flush = tmp.FlushAsync(cancellationToken); - if (!flush.IsCompletedSuccessfully) return FlushAsync_Awaited(this, flush, throwOnFailure -#if DEBUG - , startFlush, flushBytes -#endif - ); -#if DEBUG - RecordEndFlush(startFlush, flushBytes); -#endif + if (!flush.IsCompletedSuccessfully) return FlushAsync_Awaited(this, flush, throwOnFailure); _writeStatus = WriteStatus.Flushed; UpdateLastWriteTime(); return new ValueTask(WriteResult.Success); @@ -980,24 +958,8 @@ internal ValueTask FlushAsync(bool throwOnFailure, CancellationToke return new ValueTask(WriteResult.WriteFailure); } } -#if DEBUG - private void RecordEndFlush(int start, long bytes) - { - var end = Environment.TickCount; - int taken = unchecked(end - start); - if (taken > _maxFlushTime) - { - _maxFlushTime = taken; - if (bytes >= 0) _maxFlushBytes = bytes; - } - } - private volatile int _maxFlushTime = -1; - private long _maxFlushBytes = -1; - internal int MaxFlushTime => _maxFlushTime; - internal long MaxFlushBytes => _maxFlushBytes; -#endif - private static readonly ReadOnlyMemory NullBulkString = Encoding.ASCII.GetBytes("$-1\r\n"), EmptyBulkString = Encoding.ASCII.GetBytes("$0\r\n\r\n"); + private static readonly ReadOnlyMemory NullBulkString = Encoding.ASCII.GetBytes("$-1\r\n"), EmptyBulkString = Encoding.ASCII.GetBytes("$0\r\n\r\n"); private static void WriteUnifiedBlob(PipeWriter writer, byte[] value) { @@ -1676,12 +1638,9 @@ private async Task ReadFromPipe() } } - private static readonly ArenaOptions s_arenaOptions = new ArenaOptions( -#if DEBUG - blockSizeBytes: Unsafe.SizeOf() * 8 // force an absurdly small page size to trigger bugs -#endif - ); + private static readonly ArenaOptions s_arenaOptions = new ArenaOptions(); private readonly Arena _arena = new Arena(s_arenaOptions); + private int ProcessBuffer(ref ReadOnlySequence buffer) { int messageCount = 0; From cc08b6fc3c2a2efb8e461ad1839f7723d29ddbbf Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 22 Jan 2022 15:14:14 -0500 Subject: [PATCH 065/435] Pub/Sub: default to 3.0, fix PING, fix server selection in cluster, and cleanup (#1958) In prep for changes to how we handle subscriptions internally, this does several things: - Upgrades default Redis server assumption to 3.x - Routes PING on Subscription keepalives over the subscription bridge appropriately - Fixes cluster sharding from default(RedisKey) to shared logic for RedisChannel as well (both in byte[] form) - General code cleanup in the area (getting a lot of DEBUG/VERBOSE noise into isolated files) --- .../ConfigurationOptions.cs | 2 +- .../ConnectionMultiplexer.Threading.cs | 50 +++++++++ .../ConnectionMultiplexer.Verbose.cs | 59 ++++++++++ .../ConnectionMultiplexer.cs | 45 ++------ src/StackExchange.Redis/Enums/CommandFlags.cs | 2 + src/StackExchange.Redis/ExceptionFactory.cs | 4 +- .../Interfaces/ISubscriber.cs | 8 +- src/StackExchange.Redis/Message.cs | 12 +- src/StackExchange.Redis/PhysicalBridge.cs | 1 + src/StackExchange.Redis/RedisBatch.cs | 2 +- src/StackExchange.Redis/RedisSubscriber.cs | 90 ++------------- src/StackExchange.Redis/ServerEndPoint.cs | 56 +++++++--- .../ServerSelectionStrategy.cs | 25 ++++- tests/StackExchange.Redis.Tests/AsyncTests.cs | 2 +- tests/StackExchange.Redis.Tests/Config.cs | 2 +- .../ConnectFailTimeout.cs | 2 +- .../ConnectingFailDetection.cs | 6 +- .../ConnectionShutdown.cs | 2 +- tests/StackExchange.Redis.Tests/PubSub.cs | 103 +++++++++++++++--- tests/StackExchange.Redis.Tests/TestBase.cs | 5 + 20 files changed, 311 insertions(+), 167 deletions(-) create mode 100644 src/StackExchange.Redis/ConnectionMultiplexer.Threading.cs create mode 100644 src/StackExchange.Redis/ConnectionMultiplexer.Verbose.cs diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index d733353cc..138096120 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -306,7 +306,7 @@ public int ConnectTimeout /// public Version DefaultVersion { - get => defaultVersion ?? (IsAzureEndpoint() ? RedisFeatures.v4_0_0 : RedisFeatures.v2_8_0); + get => defaultVersion ?? (IsAzureEndpoint() ? RedisFeatures.v4_0_0 : RedisFeatures.v3_0_0); set => defaultVersion = value; } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Threading.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Threading.cs new file mode 100644 index 000000000..f23d010cc --- /dev/null +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Threading.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading; +using Pipelines.Sockets.Unofficial; + +namespace StackExchange.Redis +{ + public partial class ConnectionMultiplexer + { + private static readonly WaitCallback s_CompleteAsWorker = s => ((ICompletable)s).TryComplete(true); + internal static void CompleteAsWorker(ICompletable completable) + { + if (completable != null) + { + ThreadPool.QueueUserWorkItem(s_CompleteAsWorker, completable); + } + } + + internal static bool TryCompleteHandler(EventHandler handler, object sender, T args, bool isAsync) where T : EventArgs, ICompletable + { + if (handler == null) return true; + if (isAsync) + { + if (handler.IsSingle()) + { + try + { + handler(sender, args); + } + catch { } + } + else + { + foreach (EventHandler sub in handler.AsEnumerable()) + { + try + { + sub(sender, args); + } + catch { } + } + } + return true; + } + else + { + return false; + } + } + } +} diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Verbose.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Verbose.cs new file mode 100644 index 000000000..7a61096b6 --- /dev/null +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Verbose.cs @@ -0,0 +1,59 @@ +using System; +using System.Diagnostics; +using System.Net; +using System.Runtime.CompilerServices; + +namespace StackExchange.Redis +{ + public partial class ConnectionMultiplexer + { + internal event Action MessageFaulted; + internal event Action Closing; + internal event Action PreTransactionExec, TransactionLog, InfoMessage; + internal event Action Connecting; + internal event Action Resurrecting; + + partial void OnTrace(string message, string category); + static partial void OnTraceWithoutContext(string message, string category); + + [Conditional("VERBOSE")] + internal void Trace(string message, [CallerMemberName] string category = null) => OnTrace(message, category); + + [Conditional("VERBOSE")] + internal void Trace(bool condition, string message, [CallerMemberName] string category = null) + { + if (condition) OnTrace(message, category); + } + + [Conditional("VERBOSE")] + internal static void TraceWithoutContext(string message, [CallerMemberName] string category = null) => OnTraceWithoutContext(message, category); + + [Conditional("VERBOSE")] + internal static void TraceWithoutContext(bool condition, string message, [CallerMemberName] string category = null) + { + if (condition) OnTraceWithoutContext(message, category); + } + + [Conditional("VERBOSE")] + internal void OnMessageFaulted(Message msg, Exception fault, [CallerMemberName] string origin = default, [CallerFilePath] string path = default, [CallerLineNumber] int lineNumber = default) => + MessageFaulted?.Invoke(msg?.CommandAndKey, fault, $"{origin} ({path}#{lineNumber})"); + + [Conditional("VERBOSE")] + internal void OnInfoMessage(string message) => InfoMessage?.Invoke(message); + + [Conditional("VERBOSE")] + internal void OnClosing(bool complete) => Closing?.Invoke(complete); + + [Conditional("VERBOSE")] + internal void OnConnecting(EndPoint endpoint, ConnectionType connectionType) => Connecting?.Invoke(endpoint, connectionType); + + [Conditional("VERBOSE")] + internal void OnResurrecting(EndPoint endpoint, ConnectionType connectionType) => Resurrecting.Invoke(endpoint, connectionType); + + [Conditional("VERBOSE")] + internal void OnPreTransactionExec(Message message) => PreTransactionExec?.Invoke(message.CommandAndKey); + + [Conditional("VERBOSE")] + internal void OnTransactionLog(string message) => TransactionLog?.Invoke(message); + } +} diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 0cc31e76b..6e42daaed 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1504,33 +1504,6 @@ public IServer GetServer(EndPoint endpoint, object asyncState = null) return new RedisServer(this, server, asyncState); } - [Conditional("VERBOSE")] - internal void Trace(string message, [CallerMemberName] string category = null) - { - OnTrace(message, category); - } - - [Conditional("VERBOSE")] - internal void Trace(bool condition, string message, [CallerMemberName] string category = null) - { - if (condition) OnTrace(message, category); - } - - partial void OnTrace(string message, string category); - static partial void OnTraceWithoutContext(string message, string category); - - [Conditional("VERBOSE")] - internal static void TraceWithoutContext(string message, [CallerMemberName] string category = null) - { - OnTraceWithoutContext(message, category); - } - - [Conditional("VERBOSE")] - internal static void TraceWithoutContext(bool condition, string message, [CallerMemberName] string category = null) - { - if (condition) OnTraceWithoutContext(message, category); - } - /// /// The number of operations that have been performed on all connections /// @@ -1773,7 +1746,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP { var server = servers[i]; var task = available[i]; - var bs = server.GetBridgeStatus(RedisCommand.PING); + var bs = server.GetBridgeStatus(ConnectionType.Interactive); log?.WriteLine($" Server[{i}] ({Format.ToString(server)}) Status: {task.Status} (inst: {bs.MessagesSinceLastHeartbeat}, qs: {bs.Connection.MessagesSentAwaitingResponse}, in: {bs.Connection.BytesAvailableOnSocket}, qu: {bs.MessagesSinceLastHeartbeat}, aw: {bs.IsWriterActive}, in-pipe: {bs.Connection.BytesInReadPipe}, out-pipe: {bs.Connection.BytesInWritePipe}, bw: {bs.BacklogStatus}, rs: {bs.Connection.ReadStatus}. ws: {bs.Connection.WriteStatus})"); } @@ -2174,16 +2147,14 @@ internal void UpdateClusterRange(ClusterConfiguration configuration) private IDisposable pulse; - internal ServerEndPoint SelectServer(Message message) - { - if (message == null) return null; - return ServerSelectionStrategy.Select(message); - } + internal ServerEndPoint SelectServer(Message message) => + message == null ? null : ServerSelectionStrategy.Select(message); - internal ServerEndPoint SelectServer(RedisCommand command, CommandFlags flags, in RedisKey key) - { - return ServerSelectionStrategy.Select(command, key, flags); - } + internal ServerEndPoint SelectServer(RedisCommand command, CommandFlags flags, in RedisKey key) => + ServerSelectionStrategy.Select(command, key, flags); + + internal ServerEndPoint SelectServer(RedisCommand command, CommandFlags flags, in RedisChannel channel) => + ServerSelectionStrategy.Select(command, channel, flags); private bool PrepareToPushMessageToBridge(Message message, ResultProcessor processor, IResultBox resultBox, ref ServerEndPoint server) { diff --git a/src/StackExchange.Redis/Enums/CommandFlags.cs b/src/StackExchange.Redis/Enums/CommandFlags.cs index f0a670d76..c1efc65c1 100644 --- a/src/StackExchange.Redis/Enums/CommandFlags.cs +++ b/src/StackExchange.Redis/Enums/CommandFlags.cs @@ -82,5 +82,7 @@ public enum CommandFlags NoScriptCache = 512, // 1024: used for timed-out; never user-specified, so not visible on the public API + + // 2048: Use subscription connection type; never user-specified, so not visible on the public API } } diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index ca35a16f6..d4a6e0527 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -308,7 +308,7 @@ ServerEndPoint server // Add server data, if we have it if (server != null && message != null) { - var bs = server.GetBridgeStatus(message.Command); + var bs = server.GetBridgeStatus(message.IsForSubscriptionBridge ? ConnectionType.Subscription: ConnectionType.Interactive); switch (bs.Connection.ReadStatus) { @@ -334,7 +334,7 @@ ServerEndPoint server if (multiplexer.StormLogThreshold >= 0 && bs.Connection.MessagesSentAwaitingResponse >= multiplexer.StormLogThreshold && Interlocked.CompareExchange(ref multiplexer.haveStormLog, 1, 0) == 0) { - var log = server.GetStormLog(message.Command); + var log = server.GetStormLog(message); if (string.IsNullOrWhiteSpace(log)) Interlocked.Exchange(ref multiplexer.haveStormLog, 0); else Interlocked.Exchange(ref multiplexer.stormLogSnapshot, log); } diff --git a/src/StackExchange.Redis/Interfaces/ISubscriber.cs b/src/StackExchange.Redis/Interfaces/ISubscriber.cs index b479a8d8d..11e985a0b 100644 --- a/src/StackExchange.Redis/Interfaces/ISubscriber.cs +++ b/src/StackExchange.Redis/Interfaces/ISubscriber.cs @@ -100,8 +100,8 @@ public interface ISubscriber : IRedis EndPoint SubscribedEndpoint(RedisChannel channel); /// - /// Unsubscribe from a specified message channel; note; if no handler is specified, the subscription is cancelled regardless - /// of the subscribers; if a handler is specified, the subscription is only cancelled if this handler is the + /// Unsubscribe from a specified message channel; note; if no handler is specified, the subscription is canceled regardless + /// of the subscribers; if a handler is specified, the subscription is only canceled if this handler is the /// last handler remaining against the channel /// /// The channel that was subscribed to. @@ -128,8 +128,8 @@ public interface ISubscriber : IRedis Task UnsubscribeAllAsync(CommandFlags flags = CommandFlags.None); /// - /// Unsubscribe from a specified message channel; note; if no handler is specified, the subscription is cancelled regardless - /// of the subscribers; if a handler is specified, the subscription is only cancelled if this handler is the + /// Unsubscribe from a specified message channel; note; if no handler is specified, the subscription is canceled regardless + /// of the subscribers; if a handler is specified, the subscription is only canceled if this handler is the /// last handler remaining against the channel /// /// The channel that was subscribed to. diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 89d1dac1d..a23c5cf20 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -60,7 +60,8 @@ internal abstract class Message : ICompletable private const CommandFlags AskingFlag = (CommandFlags)32, ScriptUnavailableFlag = (CommandFlags)256, - NeedsAsyncTimeoutCheckFlag = (CommandFlags)1024; + NeedsAsyncTimeoutCheckFlag = (CommandFlags)1024, + DemandSubscriptionConnection = (CommandFlags)2048; private const CommandFlags MaskMasterServerPreference = CommandFlags.DemandMaster | CommandFlags.DemandReplica @@ -652,6 +653,15 @@ internal void SetWriteTime() private int _writeTickCount; public int GetWriteTime() => Volatile.Read(ref _writeTickCount); + /// + /// Gets if this command should be sent over the subscription bridge. + /// + internal bool IsForSubscriptionBridge => (Flags & DemandSubscriptionConnection) != 0; + /// + /// Sends this command to the subscription connection rather than the interactive. + /// + internal void SetForSubscriptionBridge() => Flags |= DemandSubscriptionConnection; + private void SetNeedsTimeoutCheck() => Flags |= NeedsAsyncTimeoutCheckFlag; internal bool HasAsyncTimedOut(int now, int timeoutMilliseconds, out int millisecondsTaken) { diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index a9f5b5b22..36578f653 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -363,6 +363,7 @@ internal void KeepAlive() if (commandMap.IsAvailable(RedisCommand.PING) && features.PingOnSubscriber) { msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.PING); + msg.SetForSubscriptionBridge(); msg.SetSource(ResultProcessor.Tracer, null); } else if (commandMap.IsAvailable(RedisCommand.UNSUBSCRIBE)) diff --git a/src/StackExchange.Redis/RedisBatch.cs b/src/StackExchange.Redis/RedisBatch.cs index 6f4d70700..7abe234c5 100644 --- a/src/StackExchange.Redis/RedisBatch.cs +++ b/src/StackExchange.Redis/RedisBatch.cs @@ -30,7 +30,7 @@ public void Execute() FailNoServer(snapshot); throw ExceptionFactory.NoConnectionAvailable(multiplexer, message, server); } - var bridge = server.GetBridge(message.Command); + var bridge = server.GetBridge(message); if (bridge == null) { FailNoServer(snapshot); diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index 22b9664b7..9b2b9c18d 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Net; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Pipelines.Sockets.Unofficial; @@ -13,34 +11,11 @@ public partial class ConnectionMultiplexer { private readonly Dictionary subscriptions = new Dictionary(); - internal static void CompleteAsWorker(ICompletable completable) + internal int GetSubscriptionsCount() { - if (completable != null) ThreadPool.QueueUserWorkItem(s_CompleteAsWorker, completable); - } - - private static readonly WaitCallback s_CompleteAsWorker = s => ((ICompletable)s).TryComplete(true); - - internal static bool TryCompleteHandler(EventHandler handler, object sender, T args, bool isAsync) where T : EventArgs, ICompletable - { - if (handler == null) return true; - if (isAsync) - { - if (handler.IsSingle()) - { - try { handler(sender, args); } catch { } - } - else - { - foreach (EventHandler sub in handler.AsEnumerable()) - { - try { sub(sender, args); } catch { } - } - } - return true; - } - else + lock (subscriptions) { - return false; + return subscriptions.Count; } } @@ -106,7 +81,7 @@ internal void OnMessage(in RedisChannel subscription, in RedisChannel channel, i } } if (queues != null) ChannelMessageQueue.WriteAll(ref queues, channel, payload); - if (completable != null && !completable.TryComplete(false)) ConnectionMultiplexer.CompleteAsWorker(completable); + if (completable != null && !completable.TryComplete(false)) CompleteAsWorker(completable); } internal Task RemoveAllSubscriptions(CommandFlags flags, object asyncState) @@ -169,7 +144,7 @@ internal void ResendSubscriptions(ServerEndPoint server) var server = GetSubscribedServer(channel); if (server != null) return server.IsConnected; - server = SelectServer(RedisCommand.SUBSCRIBE, CommandFlags.DemandMaster, default(RedisKey)); + server = SelectServer(RedisCommand.SUBSCRIBE, CommandFlags.DemandMaster, channel); return server?.IsConnected == true; } @@ -221,7 +196,7 @@ public bool Remove(Action handler, ChannelMessageQueue [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "RCS1210:Return completed task instead of returning null.", Justification = "Intentional for efficient success check")] public Task SubscribeToServer(ConnectionMultiplexer multiplexer, in RedisChannel channel, CommandFlags flags, object asyncState, bool internalCall) { - var selected = multiplexer.SelectServer(RedisCommand.SUBSCRIBE, flags, default(RedisKey)); + var selected = multiplexer.SelectServer(RedisCommand.SUBSCRIBE, flags, channel); var bridge = selected?.GetBridge(ConnectionType.Subscription, true); if (bridge == null) return null; @@ -348,51 +323,6 @@ internal void GetSubscriberCounts(out int handlers, out int queues) } } } - - internal string GetConnectionName(EndPoint endPoint, ConnectionType connectionType) - => GetServerEndPoint(endPoint)?.GetBridge(connectionType, false)?.PhysicalName; - - internal event Action MessageFaulted; - internal event Action Closing; - internal event Action PreTransactionExec, TransactionLog, InfoMessage; - internal event Action Connecting; - internal event Action Resurrecting; - - [Conditional("VERBOSE")] - internal void OnMessageFaulted(Message msg, Exception fault, [CallerMemberName] string origin = default, [CallerFilePath] string path = default, [CallerLineNumber] int lineNumber = default) - { - MessageFaulted?.Invoke(msg?.CommandAndKey, fault, $"{origin} ({path}#{lineNumber})"); - } - [Conditional("VERBOSE")] - internal void OnInfoMessage(string message) - { - InfoMessage?.Invoke(message); - } - [Conditional("VERBOSE")] - internal void OnClosing(bool complete) - { - Closing?.Invoke(complete); - } - [Conditional("VERBOSE")] - internal void OnConnecting(EndPoint endpoint, ConnectionType connectionType) - { - Connecting?.Invoke(endpoint, connectionType); - } - [Conditional("VERBOSE")] - internal void OnResurrecting(EndPoint endpoint, ConnectionType connectionType) - { - Resurrecting.Invoke(endpoint, connectionType); - } - [Conditional("VERBOSE")] - internal void OnPreTransactionExec(Message message) - { - PreTransactionExec?.Invoke(message.CommandAndKey); - } - [Conditional("VERBOSE")] - internal void OnTransactionLog(string message) - { - TransactionLog?.Invoke(message); - } } internal sealed class RedisSubscriber : RedisBase, ISubscriber @@ -442,16 +372,20 @@ private Message CreatePingMessage(CommandFlags flags, out ServerEndPoint server) catch { } } + Message msg; if (usePing) { - return ResultProcessor.TimingProcessor.CreateMessage(-1, flags, RedisCommand.PING); + msg = ResultProcessor.TimingProcessor.CreateMessage(-1, flags, RedisCommand.PING); } else { // can't use regular PING, but we can unsubscribe from something random that we weren't even subscribed to... RedisValue channel = multiplexer.UniqueId; - return ResultProcessor.TimingProcessor.CreateMessage(-1, flags, RedisCommand.UNSUBSCRIBE, channel); + msg = ResultProcessor.TimingProcessor.CreateMessage(-1, flags, RedisCommand.UNSUBSCRIBE, channel); } + // Ensure the ping is sent over the intended subscriver connection, which wouldn't happen in GetBridge() by default with PING; + msg.SetForSubscriptionBridge(); + return msg; } public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index f41e360a3..ab2889e65 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -74,6 +74,8 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) public bool IsConnected => interactive?.IsConnected == true; + public bool IsSubscriberConnected => subscription?.IsConnected == true; + public bool IsConnecting => interactive?.IsConnecting == true; private readonly List> _pendingConnectionMonitors = new List>(); @@ -92,7 +94,7 @@ async Task IfConnectedAsync(LogProxy log, bool sendTracerIfConnected, bo } if (sendTracerIfConnected) { - await SendTracer(log).ForAwait(); + await SendTracerAsync(log).ForAwait(); } log?.WriteLine($"{Format.ToString(this)}: OnConnectedAsync already connected end"); return "Already connected"; @@ -209,6 +211,28 @@ public PhysicalBridge GetBridge(ConnectionType type, bool create = true, LogProx }; } + public PhysicalBridge GetBridge(Message message, bool create = true) + { + if (isDisposed) return null; + + // Subscription commands go to a specific bridge - so we need to set that up. + // There are other commands we need to send to the right connection (e.g. subscriber PING with an explicit SetForSubscriptionBridge call), + // but these always go subscriber. + switch (message.Command) + { + case RedisCommand.SUBSCRIBE: + case RedisCommand.UNSUBSCRIBE: + case RedisCommand.PSUBSCRIBE: + case RedisCommand.PUNSUBSCRIBE: + message.SetForSubscriptionBridge(); + break; + } + + return message.IsForSubscriptionBridge + ? subscription ?? (create ? subscription = CreateBridge(ConnectionType.Subscription, null) : null) + : interactive ?? (create ? interactive = CreateBridge(ConnectionType.Interactive, null) : null); + } + public PhysicalBridge GetBridge(RedisCommand command, bool create = true) { if (isDisposed) return null; @@ -281,9 +305,9 @@ public void SetUnselectable(UnselectableFlags flags) public override string ToString() => Format.ToString(EndPoint); [Obsolete("prefer async")] - public WriteResult TryWriteSync(Message message) => GetBridge(message.Command)?.TryWriteSync(message, isReplica) ?? WriteResult.NoConnectionAvailable; + public WriteResult TryWriteSync(Message message) => GetBridge(message)?.TryWriteSync(message, isReplica) ?? WriteResult.NoConnectionAvailable; - public ValueTask TryWriteAsync(Message message) => GetBridge(message.Command)?.TryWriteAsync(message, isReplica) ?? new ValueTask(WriteResult.NoConnectionAvailable); + public ValueTask TryWriteAsync(Message message) => GetBridge(message)?.TryWriteAsync(message, isReplica) ?? new ValueTask(WriteResult.NoConnectionAvailable); internal void Activate(ConnectionType type, LogProxy log) { @@ -445,11 +469,11 @@ internal ServerCounters GetCounters() return counters; } - internal BridgeStatus GetBridgeStatus(RedisCommand command) + internal BridgeStatus GetBridgeStatus(ConnectionType connectionType) { try { - return GetBridge(command, false)?.GetStatus() ?? BridgeStatus.Zero; + return GetBridge(connectionType, false)?.GetStatus() ?? BridgeStatus.Zero; } catch (Exception ex) { // only needs to be best efforts @@ -484,9 +508,9 @@ internal byte[] GetScriptHash(string script, RedisCommand command) return found; } - internal string GetStormLog(RedisCommand command) + internal string GetStormLog(Message message) { - var bridge = GetBridge(command); + var bridge = GetBridge(message); return bridge?.GetStormLog(); } @@ -686,7 +710,7 @@ internal void OnHeartbeat() } } - internal Task WriteDirectAsync(Message message, ResultProcessor processor, object asyncState = null, PhysicalBridge bridge = null) + internal Task WriteDirectAsync(Message message, ResultProcessor processor, PhysicalBridge bridge = null) { static async Task Awaited(ServerEndPoint @this, Message message, ValueTask write, TaskCompletionSource tcs) { @@ -699,9 +723,9 @@ static async Task Awaited(ServerEndPoint @this, Message message, ValueTask.Create(out var tcs, asyncState); + var source = TaskResultBox.Create(out var tcs, null); message.SetSource(processor, source); - if (bridge == null) bridge = GetBridge(message.Command); + if (bridge == null) bridge = GetBridge(message); WriteResult result; if (bridge == null) @@ -733,7 +757,7 @@ internal void WriteDirectFireAndForgetSync(Message message, ResultProcessor SendTracer(LogProxy log = null) + internal Task SendTracerAsync(LogProxy log = null) { var msg = GetTracerMessage(false); msg = LoggingMessage.Create(log, msg); @@ -783,6 +807,9 @@ internal string Summary() return sb.ToString(); } + /// + /// Write the message directly to the pipe or fail...will not queue. + /// internal ValueTask WriteDirectOrQueueFireAndForgetAsync(PhysicalConnection connection, Message message, ResultProcessor processor) { static async ValueTask Awaited(ValueTask l_result) => await l_result.ForAwait(); @@ -794,7 +821,7 @@ internal ValueTask WriteDirectOrQueueFireAndForgetAsync(PhysicalConnection co if (connection == null) { Multiplexer.Trace($"{Format.ToString(this)}: Enqueue (async): " + message); - result = GetBridge(message.Command).TryWriteAsync(message, isReplica); + result = GetBridge(message).TryWriteAsync(message, isReplica); } else { @@ -886,7 +913,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy log) log?.WriteLine($"{Format.ToString(this)}: Sending critical tracer (handshake): {tracer.CommandAndKey}"); await WriteDirectOrQueueFireAndForgetAsync(connection, tracer, ResultProcessor.EstablishConnection).ForAwait(); - // note: this **must** be the last thing on the subscription handshake, because after this + // Note: this **must** be the last thing on the subscription handshake, because after this // we will be in subscriber mode: regular commands cannot be sent if (connType == ConnectionType.Subscription) { @@ -894,6 +921,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy log) if (configChannel != null) { msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.SUBSCRIBE, (RedisChannel)configChannel); + // Note: this is NOT internal, we want it to queue in a backlog for sending when ready if necessary await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.TrackSubscriptions).ForAwait(); } } diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index ac9e664ca..cb4bb954c 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -58,19 +58,26 @@ public ServerSelectionStrategy(ConnectionMultiplexer multiplexer) internal static int TotalSlots => RedisClusterSlotCount; /// - /// Computes the hash-slot that would be used by the given key + /// Computes the hash-slot that would be used by the given key. /// /// The to determine a slot ID for. public int HashSlot(in RedisKey key) - => ServerType == ServerType.Standalone ? NoSlot : GetClusterSlot(key); + => ServerType == ServerType.Standalone || key.IsNull ? NoSlot : GetClusterSlot((byte[])key); - private static unsafe int GetClusterSlot(in RedisKey key) + /// + /// Computes the hash-slot that would be used by the given channel. + /// + /// The to determine a slot ID for. + public int HashSlot(in RedisChannel channel) + => ServerType == ServerType.Standalone || channel.IsNull ? NoSlot : GetClusterSlot((byte[])channel); + + /// + /// HASH_SLOT = CRC16(key) mod 16384 + /// + private static unsafe int GetClusterSlot(byte[] blob) { - //HASH_SLOT = CRC16(key) mod 16384 - if (key.IsNull) return NoSlot; unchecked { - var blob = (byte[])key; fixed (byte* ptr = blob) { fixed (ushort* crc16tab = s_crc16tab) @@ -116,6 +123,12 @@ public ServerEndPoint Select(RedisCommand command, in RedisKey key, CommandFlags return Select(slot, command, flags); } + public ServerEndPoint Select(RedisCommand command, in RedisChannel channel, CommandFlags flags) + { + int slot = ServerType == ServerType.Cluster ? HashSlot(channel) : NoSlot; + return Select(slot, command, flags); + } + public bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isMoved) { try diff --git a/tests/StackExchange.Redis.Tests/AsyncTests.cs b/tests/StackExchange.Redis.Tests/AsyncTests.cs index 5ee26f815..4dd36670b 100644 --- a/tests/StackExchange.Redis.Tests/AsyncTests.cs +++ b/tests/StackExchange.Redis.Tests/AsyncTests.cs @@ -19,7 +19,7 @@ public void AsyncTasksReportFailureIfServerUnavailable() { SetExpectedAmbientFailureCount(-1); // this will get messy - using (var conn = Create(allowAdmin: true)) + using (var conn = Create(allowAdmin: true, shared: false)) { var server = conn.GetServer(TestConfig.Current.MasterServer, TestConfig.Current.MasterPort); diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index a2d0d7034..2a5cf6625 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -14,7 +14,7 @@ namespace StackExchange.Redis.Tests { public class Config : TestBase { - public Version DefaultVersion = new (2, 8, 0); + public Version DefaultVersion = new (3, 0, 0); public Version DefaultAzureVersion = new (4, 0, 0); public Config(ITestOutputHelper output) : base(output) { } diff --git a/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs b/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs index c52082d12..73af84fa4 100644 --- a/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs +++ b/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs @@ -13,7 +13,7 @@ public ConnectFailTimeout(ITestOutputHelper output) : base (output) { } public async Task NoticesConnectFail() { SetExpectedAmbientFailureCount(-1); - using (var conn = Create(allowAdmin: true)) + using (var conn = Create(allowAdmin: true, shared: false)) { var server = conn.GetServer(conn.GetEndPoints()[0]); diff --git a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs index 9d9c88f8c..fb0b84d21 100644 --- a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs +++ b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs @@ -105,17 +105,17 @@ public async Task Issue922_ReconnectRaised() int failCount = 0, restoreCount = 0; - using (var muxer = ConnectionMultiplexer.Connect(config, log: Writer)) + using (var muxer = ConnectionMultiplexer.Connect(config)) { muxer.ConnectionFailed += (s, e) => { Interlocked.Increment(ref failCount); - Log($"Connection Failed ({e.ConnectionType},{e.FailureType}): {e.Exception}"); + Log($"Connection Failed ({e.ConnectionType}, {e.FailureType}): {e.Exception}"); }; muxer.ConnectionRestored += (s, e) => { Interlocked.Increment(ref restoreCount); - Log($"Connection Restored ({e.ConnectionType},{e.FailureType}): {e.Exception}"); + Log($"Connection Restored ({e.ConnectionType}, {e.FailureType})"); }; muxer.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs b/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs index a4e720772..d75054ca4 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs @@ -14,7 +14,7 @@ public ConnectionShutdown(ITestOutputHelper output) : base(output) { } [Fact(Skip = "Unfriendly")] public async Task ShutdownRaisesConnectionFailedAndRestore() { - using (var conn = Create(allowAdmin: true)) + using (var conn = Create(allowAdmin: true, shared: false)) { int failed = 0, restored = 0; Stopwatch watch = Stopwatch.StartNew(); diff --git a/tests/StackExchange.Redis.Tests/PubSub.cs b/tests/StackExchange.Redis.Tests/PubSub.cs index 0e4131913..2ca92a430 100644 --- a/tests/StackExchange.Redis.Tests/PubSub.cs +++ b/tests/StackExchange.Redis.Tests/PubSub.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using StackExchange.Redis.Maintenance; +using StackExchange.Redis.Profiling; using Xunit; using Xunit.Abstractions; // ReSharper disable AccessToModifiedClosure @@ -55,7 +57,7 @@ await UntilCondition(TimeSpan.FromSeconds(10), [InlineData("Foo:", true, "f")] public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string breaker) { - using (var muxer = Create(channelPrefix: channelPrefix)) + using (var muxer = Create(channelPrefix: channelPrefix, log: Writer)) { var pub = GetAnyMaster(muxer); var sub = muxer.GetSubscriber(); @@ -91,6 +93,7 @@ public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string br await PingAsync(muxer, pub, sub, 3).ForAwait(); + await UntilCondition(TimeSpan.FromSeconds(5), () => received.Count == 1); lock (received) { Assert.Single(received); @@ -124,7 +127,7 @@ public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string br [Fact] public async Task TestBasicPubSubFireAndForget() { - using (var muxer = Create()) + using (var muxer = Create(log: Writer)) { var pub = GetAnyMaster(muxer); var sub = muxer.GetSubscriber(); @@ -155,6 +158,7 @@ public async Task TestBasicPubSubFireAndForget() var count = sub.Publish(key, "def", CommandFlags.FireAndForget); await PingAsync(muxer, pub, sub).ForAwait(); + await UntilCondition(TimeSpan.FromSeconds(5), () => received.Count == 1); lock (received) { Assert.Single(received); @@ -182,9 +186,7 @@ private static async Task PingAsync(IConnectionMultiplexer muxer, IServer pub, I // way to prove that is to use TPL objects var t1 = sub.PingAsync(); var t2 = pub.PingAsync(); - await Task.Delay(100).ForAwait(); // especially useful when testing any-order mode - - if (!Task.WaitAll(new[] { t1, t2 }, muxer.TimeoutMilliseconds * 2)) throw new TimeoutException(); + await Task.WhenAll(t1, t2).ForAwait(); } } @@ -220,6 +222,7 @@ public async Task TestPatternPubSub() var count = sub.Publish("abc", "def"); await PingAsync(muxer, pub, sub).ForAwait(); + await UntilCondition(TimeSpan.FromSeconds(5), () => received.Count == 1); lock (received) { Assert.Single(received); @@ -348,7 +351,7 @@ await sub.SubscribeAsync(channel, (_, val) => [Fact] public async Task PubSubGetAllCorrectOrder() { - using (var muxer = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000)) + using (var muxer = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { var sub = muxer.GetSubscriber(); RedisChannel channel = Me(); @@ -743,8 +746,10 @@ public async Task AzureRedisEventsAutomaticSubscribe() [Fact] public async Task SubscriptionsSurviveConnectionFailureAsync() { - using (var muxer = Create(allowAdmin: true, shared: false)) + var session = new ProfilingSession(); + using (var muxer = Create(allowAdmin: true, shared: false, syncTimeout: 1000) as ConnectionMultiplexer) { + muxer.RegisterProfiler(() => session); RedisChannel channel = Me(); var sub = muxer.GetSubscriber(); int counter = 0; @@ -752,23 +757,89 @@ await sub.SubscribeAsync(channel, delegate { Interlocked.Increment(ref counter); }).ConfigureAwait(false); + + var profile1 = session.FinishProfiling(); + foreach (var command in profile1) + { + Log($"{command.EndPoint}: {command}"); + } + // We shouldn't see the initial connection here + Assert.Equal(0, profile1.Count(p => p.Command == nameof(RedisCommand.SUBSCRIBE))); + + Assert.Equal(1, muxer.GetSubscriptionsCount()); + await Task.Delay(200).ConfigureAwait(false); + await sub.PublishAsync(channel, "abc").ConfigureAwait(false); sub.Ping(); await Task.Delay(200).ConfigureAwait(false); - Assert.Equal(1, Thread.VolatileRead(ref counter)); + + var counter1 = Thread.VolatileRead(ref counter); + Log($"Expecting 1 messsage, got {counter1}"); + Assert.Equal(1, counter1); + var server = GetServer(muxer); - Assert.Equal(1, server.GetCounters().Subscription.SocketCount); + var socketCount = server.GetCounters().Subscription.SocketCount; + Log($"Expecting 1 socket, got {socketCount}"); + Assert.Equal(1, socketCount); + + // We might fail both connections or just the primary in the time period + SetExpectedAmbientFailureCount(-1); + // Make sure we fail all the way + muxer.AllowConnect = false; + Log("Failing connection"); + // Fail all connections server.SimulateConnectionFailure(SimulatedFailureType.All); - SetExpectedAmbientFailureCount(2); - await Task.Delay(200).ConfigureAwait(false); - sub.Ping(); - Assert.Equal(2, server.GetCounters().Subscription.SocketCount); - await sub.PublishAsync(channel, "abc").ConfigureAwait(false); - await Task.Delay(200).ConfigureAwait(false); + // Trigger failure + Assert.Throws(() => sub.Ping()); + Assert.False(sub.IsConnected(channel)); + + // Now reconnect... + muxer.AllowConnect = true; + Log("Waiting on reconnect"); + // Wait until we're reconnected + await UntilCondition(TimeSpan.FromSeconds(10), () => sub.IsConnected(channel)); + Log("Reconnected"); + // Ensure we're reconnected + Assert.True(sub.IsConnected(channel)); + + // And time to resubscribe... + await Task.Delay(1000).ConfigureAwait(false); + + // Ensure we've sent the subscribe command after reconnecting + var profile2 = session.FinishProfiling(); + foreach (var command in profile2) + { + Log($"{command.EndPoint}: {command}"); + } + //Assert.Equal(1, profile2.Count(p => p.Command == nameof(RedisCommand.SUBSCRIBE))); + + Log($"Issuing ping after reconnected"); sub.Ping(); - Assert.Equal(2, Thread.VolatileRead(ref counter)); + Assert.Equal(1, muxer.GetSubscriptionsCount()); + + Log("Publishing"); + var published = await sub.PublishAsync(channel, "abc").ConfigureAwait(false); + + Log($"Published to {published} subscriber(s)."); + Assert.Equal(1, published); + + // Give it a few seconds to get our messages + Log("Waiting for 2 messages"); + await UntilCondition(TimeSpan.FromSeconds(5), () => Thread.VolatileRead(ref counter) == 2); + + var counter2 = Thread.VolatileRead(ref counter); + Log($"Expecting 2 messsages, got {counter2}"); + Assert.Equal(2, counter2); + + // Log all commands at the end + Log("All commands since connecting:"); + var profile3 = session.FinishProfiling(); + foreach (var command in profile3) + { + Log($"{command.EndPoint}: {command}"); + } } } } diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index a74521acb..0cca4e5b7 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -128,6 +128,7 @@ protected void OnConnectionFailed(object sender, ConnectionFailedEventArgs e) { privateExceptions.Add($"{Time()}: Connection failed ({e.FailureType}): {EndPointCollection.ToString(e.EndPoint)}/{e.ConnectionType}: {e.Exception}"); } + Log($"Connection Failed ({e.ConnectionType},{e.FailureType}): {e.Exception}"); } protected void OnInternalError(object sender, InternalErrorEventArgs e) @@ -285,6 +286,10 @@ internal virtual IInternalConnectionMultiplexer Create( caller); muxer.InternalError += OnInternalError; muxer.ConnectionFailed += OnConnectionFailed; + muxer.ConnectionRestored += (s, e) => + { + Log($"Connection Restored ({e.ConnectionType},{e.FailureType}): {e.Exception}"); + }; return muxer; } From 194b12fc012aaad2caa7e78c81ff76929be7455f Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 24 Jan 2022 20:05:10 -0500 Subject: [PATCH 066/435] Builds: remove .NET 5.0 installer (#1963) We're testing netcoreapp3.1 and net6 now, so no need for the 5.x runtime. --- .github/workflows/CI.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d8687ed74..32c118f8c 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -21,7 +21,6 @@ jobs: with: dotnet-version: | 3.1.x - 5.0.x 6.0.x - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true From b81c5da3a9721d32aeb46e0247e331049037cbec Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 25 Jan 2022 07:59:35 -0500 Subject: [PATCH 067/435] Exceptions: Add threadpool stats in .NET Core+ (#1964) I swear I already did this somewhere but it got lost, this adds additional thread pool stats exposed in .NET Core 3.1+. We'll get for example ThreadPool.PendingWorkItemCount on exception messages to better indicate app contention. --- docs/ReleaseNotes.md | 1 + docs/Timeouts.md | 1 + src/StackExchange.Redis/ConnectionMultiplexer.cs | 6 +++++- src/StackExchange.Redis/ExceptionFactory.cs | 6 +++++- src/StackExchange.Redis/PerfCounterHelper.cs | 13 ++++++++++--- .../ExceptionFactoryTests.cs | 5 +++++ 6 files changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index d3d2a4224..3cd8125b3 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,7 @@ - Fix profiler showing `EVAL` instead `EVALSHA` (#1930 via martinpotter) - Moved tiebreaker fetching in connections into the handshake phase (streamline + simplification) (#1931 via NickCraver) - Fixed potential disposed object usage around Arenas (pulling in [Piplines.Sockets.Unofficial#63](https://github.com/mgravell/Pipelines.Sockets.Unofficial/pull/63) by MarcGravell) +- Adds thread pool work item stats to exception messages to help diagnose contention (#1964 via NickCraver) ## 2.2.88 diff --git a/docs/Timeouts.md b/docs/Timeouts.md index d608b06e1..8f78d3db8 100644 --- a/docs/Timeouts.md +++ b/docs/Timeouts.md @@ -94,6 +94,7 @@ By default Redis Timeout exception(s) includes useful information, which can hel |mgr | 8 of 10 available|Redis Internal Dedicated Thread Pool State| |IOCP | IOCP: (Busy=0,Free=500,Min=248,Max=500)| Runtime Global Thread Pool IO Threads. | |WORKER | WORKER: (Busy=170,Free=330,Min=248,Max=500)| Runtime Global Thread Pool Worker Threads.| +|POOL | POOL: (Threads=8,QueuedItems=0,CompletedItems=42)| Thread Pool Work Item Stats.| |v | Redis Version: version |Current redis version you are currently using in your application.| |active | Message-Current: {string} |Included in exception message when `IncludeDetailInExceptions=True` on multiplexer| |next | Message-Next: {string} |When `IncludeDetailInExceptions=True` on multiplexer, it might include command and key, otherwise only command.| diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 6e42daaed..cdf6d4db7 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -702,8 +702,12 @@ private static void LogWithThreadPoolStats(LogProxy log, string message, out int { var sb = new StringBuilder(); sb.Append(message); - busyWorkerCount = PerfCounterHelper.GetThreadPoolStats(out string iocp, out string worker); + busyWorkerCount = PerfCounterHelper.GetThreadPoolStats(out string iocp, out string worker, out string workItems); sb.Append(", IOCP: ").Append(iocp).Append(", WORKER: ").Append(worker); + if (workItems != null) + { + sb.Append(", POOL: ").Append(workItems); + } log?.WriteLine(sb.ToString()); } } diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index d4a6e0527..3f711ed0f 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -353,9 +353,13 @@ ServerEndPoint server Add(data, sb, "Key-HashSlot", "PerfCounterHelperkeyHashSlot", message.GetHashSlot(multiplexer.ServerSelectionStrategy).ToString()); } } - int busyWorkerCount = PerfCounterHelper.GetThreadPoolStats(out string iocp, out string worker); + int busyWorkerCount = PerfCounterHelper.GetThreadPoolStats(out string iocp, out string worker, out string workItems); Add(data, sb, "ThreadPool-IO-Completion", "IOCP", iocp); Add(data, sb, "ThreadPool-Workers", "WORKER", worker); + if (workItems != null) + { + Add(data, sb, "ThreadPool-Items", "POOL", workItems); + } data.Add(Tuple.Create("Busy-Workers", busyWorkerCount.ToString())); if (multiplexer.IncludePerformanceCountersInExceptions) diff --git a/src/StackExchange.Redis/PerfCounterHelper.cs b/src/StackExchange.Redis/PerfCounterHelper.cs index ea1969f1e..876e66bc5 100644 --- a/src/StackExchange.Redis/PerfCounterHelper.cs +++ b/src/StackExchange.Redis/PerfCounterHelper.cs @@ -56,9 +56,9 @@ public static bool TryGetSystemCPU(out float value) internal static string GetThreadPoolAndCPUSummary(bool includePerformanceCounters) { - GetThreadPoolStats(out string iocp, out string worker); + GetThreadPoolStats(out string iocp, out string worker, out string workItems); var cpu = includePerformanceCounters ? GetSystemCpuPercent() : "n/a"; - return $"IOCP: {iocp}, WORKER: {worker}, Local-CPU: {cpu}"; + return $"IOCP: {iocp}, WORKER: {worker}, POOL: {workItems ?? "n/a"}, Local-CPU: {cpu}"; } internal static string GetSystemCpuPercent() => @@ -66,7 +66,7 @@ internal static string GetSystemCpuPercent() => ? Math.Round(systemCPU, 2) + "%" : "unavailable"; - internal static int GetThreadPoolStats(out string iocp, out string worker) + internal static int GetThreadPoolStats(out string iocp, out string worker, out string workItems) { ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxIoThreads); ThreadPool.GetAvailableThreads(out int freeWorkerThreads, out int freeIoThreads); @@ -77,6 +77,13 @@ internal static int GetThreadPoolStats(out string iocp, out string worker) iocp = $"(Busy={busyIoThreads},Free={freeIoThreads},Min={minIoThreads},Max={maxIoThreads})"; worker = $"(Busy={busyWorkerThreads},Free={freeWorkerThreads},Min={minWorkerThreads},Max={maxWorkerThreads})"; + +#if NETCOREAPP + workItems = $"(Threads={ThreadPool.ThreadCount},QueuedItems={ThreadPool.PendingWorkItemCount},CompletedItems={ThreadPool.CompletedWorkItemCount})"; +#else + workItems = null; +#endif + return busyWorkerThreads; } } diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 494c688d6..437dd44e0 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -123,6 +123,11 @@ public void TimeoutException() Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); Assert.Contains("mc: 1/1/0", ex.Message); Assert.Contains("serverEndpoint: " + server.EndPoint, ex.Message); + Assert.Contains("IOCP: ", ex.Message); + Assert.Contains("WORKER: ", ex.Message); +#if NETCOREAPP + Assert.Contains("POOL: ", ex.Message); +#endif Assert.DoesNotContain("Unspecified/", ex.Message); Assert.EndsWith(" (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)", ex.Message); Assert.Null(ex.InnerException); From d9b3c58cd5814785aad4264b31e4a5dab9fdfed0 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 4 Feb 2022 10:31:46 -0500 Subject: [PATCH 068/435] Pub/Sub fixes for subscribe/re-subscribe (#1947) We're working on pub/sub - breaking it out explicitly from #1912. This relates to several issues and in general handling resubscriptions on reconnect. Issues: #1110, #1586, #1830 #1835 There are a few things in play we're investigating: - [x] Subscription heartbeat not going over the subscription connection (due to `PING` and `GetBridge`) - [x] Subscriptions not reconnecting at all (or potentially doing to and unsubscribing according to some issues) - [x] Subscriptions always going to a single cluster node (due to `default(RedisKey)`) Overall this set of changes: - Completely restructures how RedisSubscriber works - No more `PendingSubscriptionState` (`Subscription` has the needed bits to reconnect) - Cleaner method topology (in `RedisSubscriber`, rather than `Subscriber`, `RedisSubscriber`, and `ConnectionMultiplexer`) - By placing these on `RedisSubscriber`, we can cleanly use `ExecuteSync/Async` bits, get proper profiling, etc. - Proper sync/async split (rather than `Wait()` in sync paths) - Changes how subscriptions work - The `Subscription` object is added to the `ConnectionMultiplexer` tracking immediately, but the command itself actually goes to the server and back (unless FireAndForget) before returning for proper ordering like other commands. - No more `Task.Run()` loop - we now ensure reconnects as part of the handshake - Subscriptions are marked as not having a server the moment a disconnect is fired - Question: Should we have a throttle around this for massive numbers of connections, or async it? - Changes how connecting works - The connection completion handler will now fire when the _second_ bridge/connection completes, this means we won't have `interactive` connected but `subscription` in an unknown state - both are connected before we fire the handler meaning the moment we come back from connect, subscriptions are in business. - Moves to a `ConcurrentDictionary` since we only need limited locking around this and we only have it once per multiplexer. - TODO: This needs eyes, we could shift it - implementation changed along the way where this isn't a critical detail - Fixes the `TrackSubscriptionsProcessor` - this was never setting the result but didn't notice in 8 years because downstream code never cared. - Note: each `Subscription` has a processor instance (with minimal state) because when the subscription command comes back _then_ we need to decide if it successfully registered (if it didn't, we need to maintain it has no successful server) - `ConnectionMultiplexer` grew a `DefaultSubscriber` for running some commands without lots of method duplication, e.g. ensuring servers are connected. - Overrides `GetHashSlot` on `CommandChannelBase` with the new `RedisChannel`-based methods so that operates correctly Not directly related changes which helped here: - Better profiler helpers for tests and profiler logging in them - Re-enables a few `PubSub` tests that were unreliable before...but correctly so. TODO: I'd like to add a few more test scenarios here: - [x] Simple Subscribe/Publish/await Until/check pattern to ensure back-to-back subscribe/publish works well - [x] Cluster connection failure and subscriptions moving to another node To consider: - [x] Subscription await loop from EnsureSubscriptionsAsync and connection impact on large reconnect situations - In a reconnect case, this is background and only the nodes affected have any latency...but still. - [ ] TODOs in code around variadic commands, e.g. re-subscribing with far fewer commands by using `SUBSCRIBE ...` - In cluster, we'd have to batch per slot...or just go for the first available node - ...but if we go for the first available node, the semantics of `IsConnected` are slightly off in the not connected (`CurrentServer is null`) case, because we'd say we're connected to where it _would_ go even though that'd be non-deterministic without hashslot batching. I think this is really minor and shouldn't affect our decision. - [x] `ConcurrentDictionary` vs. returning to locks around a `Dictionary` - ...but if we have to lock on firing consumption of handlers anyway, concurrency overhead is probably a wash. --- .github/workflows/CI.yml | 1 - docs/ReleaseNotes.md | 5 + .../ConnectionMultiplexer.cs | 9 +- .../Interfaces/IDatabase.cs | 5 +- .../Interfaces/IDatabaseAsync.cs | 5 +- .../Interfaces/ISubscriber.cs | 10 +- src/StackExchange.Redis/Message.cs | 4 +- src/StackExchange.Redis/PhysicalBridge.cs | 67 --- .../Profiling/ProfiledCommand.cs | 23 +- src/StackExchange.Redis/RedisSubscriber.cs | 554 ++++++++++-------- src/StackExchange.Redis/ResultProcessor.cs | 14 +- src/StackExchange.Redis/ServerEndPoint.cs | 12 +- .../ServerSelectionStrategy.cs | 2 +- .../SimulatedFailureType.cs | 5 + tests/RedisConfigs/Dockerfile | 2 +- tests/RedisConfigs/docker-compose.yml | 2 +- tests/StackExchange.Redis.Tests/Cluster.cs | 10 +- .../Helpers/TestConfig.cs | 2 + .../Helpers/TextWriterOutputHelper.cs | 23 +- .../Issues/Issue1101.cs | 8 +- tests/StackExchange.Redis.Tests/Locking.cs | 2 +- tests/StackExchange.Redis.Tests/PubSub.cs | 135 +++-- .../PubSubMultiserver.cs | 186 ++++++ tests/StackExchange.Redis.Tests/Sentinel.cs | 8 +- tests/StackExchange.Redis.Tests/TestBase.cs | 11 + .../TestExtensions.cs | 15 + 26 files changed, 694 insertions(+), 426 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/PubSubMultiserver.cs create mode 100644 tests/StackExchange.Redis.Tests/TestExtensions.cs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 32c118f8c..6d2c0e8d6 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -50,7 +50,6 @@ jobs: with: dotnet-version: | 3.1.x - 5.0.x 6.0.x - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 3cd8125b3..1dadce43a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,11 @@ - Moved tiebreaker fetching in connections into the handshake phase (streamline + simplification) (#1931 via NickCraver) - Fixed potential disposed object usage around Arenas (pulling in [Piplines.Sockets.Unofficial#63](https://github.com/mgravell/Pipelines.Sockets.Unofficial/pull/63) by MarcGravell) - Adds thread pool work item stats to exception messages to help diagnose contention (#1964 via NickCraver) +- Overhauls pub/sub implementation for correctness (#1947 via NickCraver) + - Fixes a race in subscribing right after connected + - Fixes a race in subscribing immediately before a publish + - Fixes subscription routing on clusters (spreading instead of choosing 1 node) + - More correctly reconnects subscriptions on connection failures, including to other endpoints ## 2.2.88 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index cdf6d4db7..13eebbee3 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1898,14 +1898,15 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP } if (!first) { - long subscriptionChanges = ValidateSubscriptions(); + // Calling the sync path here because it's all fire and forget + long subscriptionChanges = EnsureSubscriptions(CommandFlags.FireAndForget); if (subscriptionChanges == 0) { log?.WriteLine("No subscription changes necessary"); } else { - log?.WriteLine($"Subscriptions reconfigured: {subscriptionChanges}"); + log?.WriteLine($"Subscriptions attempting reconnect: {subscriptionChanges}"); } } if (showStats) @@ -2325,7 +2326,7 @@ internal void InitializeSentinel(LogProxy logProxy) } } } - }); + }, CommandFlags.FireAndForget); } // If we lose connection to a sentinel server, @@ -2344,7 +2345,7 @@ internal void InitializeSentinel(LogProxy logProxy) { string[] messageParts = ((string)message).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); UpdateSentinelAddressList(messageParts[0]); - }); + }, CommandFlags.FireAndForget); } } diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 6a8e8a46e..3a9cc3e4e 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -851,7 +851,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The channel to publish to. /// The message to send. /// The flags to use for this operation. - /// The number of clients that received the message. + /// + /// The number of clients that received the message *on the destination server*, + /// note that this doesn't mean much in a cluster as clients can get the message through other nodes. + /// /// https://redis.io/commands/publish long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 63f5ad644..9626945a0 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -828,7 +828,10 @@ public interface IDatabaseAsync : IRedisAsync /// The channel to publish to. /// The message to send. /// The flags to use for this operation. - /// The number of clients that received the message. + /// + /// The number of clients that received the message *on the destination server*, + /// note that this doesn't mean much in a cluster as clients can get the message through other nodes. + /// /// https://redis.io/commands/publish Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/Interfaces/ISubscriber.cs b/src/StackExchange.Redis/Interfaces/ISubscriber.cs index 11e985a0b..f26ea7a11 100644 --- a/src/StackExchange.Redis/Interfaces/ISubscriber.cs +++ b/src/StackExchange.Redis/Interfaces/ISubscriber.cs @@ -38,7 +38,10 @@ public interface ISubscriber : IRedis /// The channel to publish to. /// The message to publish. /// The command flags to use. - /// the number of clients that received the message. + /// + /// The number of clients that received the message *on the destination server*, + /// note that this doesn't mean much in a cluster as clients can get the message through other nodes. + /// /// https://redis.io/commands/publish long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None); @@ -48,7 +51,10 @@ public interface ISubscriber : IRedis /// The channel to publish to. /// The message to publish. /// The command flags to use. - /// the number of clients that received the message. + /// + /// The number of clients that received the message *on the destination server*, + /// note that this doesn't mean much in a cluster as clients can get the message through other nodes. + /// /// https://redis.io/commands/publish Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index a23c5cf20..708e4600f 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -589,7 +589,7 @@ internal bool TrySetResult(T value) internal void SetEnqueued(PhysicalConnection connection) { SetWriteTime(); - performance?.SetEnqueued(); + performance?.SetEnqueued(connection?.BridgeCouldBeNull?.ConnectionType); _enqueuedTo = connection; if (connection == null) { @@ -735,6 +735,8 @@ protected CommandChannelBase(int db, CommandFlags flags, RedisCommand command, i } public override string CommandAndKey => Command + " " + Channel; + + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(Channel); } internal abstract class CommandKeyBase : Message diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 36578f653..1a88cc8ea 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -8,7 +8,6 @@ using System.Threading.Channels; using System.Threading.Tasks; using static StackExchange.Redis.ConnectionMultiplexer; -using PendingSubscriptionState = global::StackExchange.Redis.ConnectionMultiplexer.Subscription.PendingSubscriptionState; #if !NETCOREAPP using Pipelines.Sockets.Unofficial.Threading; using static Pipelines.Sockets.Unofficial.Threading.MutexSlim; @@ -102,7 +101,6 @@ public enum State : byte public void Dispose() { isDisposed = true; - ShutdownSubscriptionQueue(); using (var tmp = physical) { physical = null; @@ -220,71 +218,6 @@ internal void GetCounters(ConnectionCounters counters) physical?.GetCounters(counters); } - private Channel _subscriptionBackgroundQueue; - private static readonly UnboundedChannelOptions s_subscriptionQueueOptions = new UnboundedChannelOptions - { - AllowSynchronousContinuations = false, // we do *not* want the async work to end up on the caller's thread - SingleReader = true, // only one reader will be started per channel - SingleWriter = true, // writes will be synchronized, because order matters - }; - - private Channel GetSubscriptionQueue() - { - var queue = _subscriptionBackgroundQueue; - if (queue == null) - { - queue = Channel.CreateUnbounded(s_subscriptionQueueOptions); - var existing = Interlocked.CompareExchange(ref _subscriptionBackgroundQueue, queue, null); - - if (existing != null) return existing; // we didn't win, but that's fine - - // we won (_subqueue is now queue) - // this means we have a new channel without a reader; let's fix that! - Task.Run(() => ExecuteSubscriptionLoop()); - } - return queue; - } - - private void ShutdownSubscriptionQueue() - { - try - { - Interlocked.CompareExchange(ref _subscriptionBackgroundQueue, null, null)?.Writer.TryComplete(); - } - catch { } - } - - private async Task ExecuteSubscriptionLoop() // pushes items that have been enqueued over the bridge - { - // note: this will execute on the default pool rather than our dedicated pool; I'm... OK with this - var queue = _subscriptionBackgroundQueue ?? Interlocked.CompareExchange(ref _subscriptionBackgroundQueue, null, null); // just to be sure we can read it! - try - { - while (await queue.Reader.WaitToReadAsync().ForAwait() && queue.Reader.TryRead(out var next)) - { - try - { - // Treat these commands as background/handshake and do not allow queueing to backlog - if ((await TryWriteAsync(next.Message, next.IsReplica).ForAwait()) != WriteResult.Success) - { - next.Abort(); - } - } - catch (Exception ex) - { - next.Fail(ex); - } - } - } - catch (Exception ex) - { - Multiplexer.OnInternalError(ex, ServerEndPoint?.EndPoint, ConnectionType); - } - } - - internal bool TryEnqueueBackgroundSubscriptionWrite(in PendingSubscriptionState state) - => !isDisposed && (_subscriptionBackgroundQueue ?? GetSubscriptionQueue()).Writer.TryWrite(state); - internal readonly struct BridgeStatus { /// diff --git a/src/StackExchange.Redis/Profiling/ProfiledCommand.cs b/src/StackExchange.Redis/Profiling/ProfiledCommand.cs index 5f4ff899f..4c1d366f9 100644 --- a/src/StackExchange.Redis/Profiling/ProfiledCommand.cs +++ b/src/StackExchange.Redis/Profiling/ProfiledCommand.cs @@ -55,6 +55,7 @@ private static TimeSpan GetElapsedTime(long timestampDelta) private long RequestSentTimeStamp; private long ResponseReceivedTimeStamp; private long CompletedTimeStamp; + private ConnectionType? ConnectionType; private readonly ProfilingSession PushToWhenFinished; @@ -86,7 +87,11 @@ public void SetMessage(Message msg) MessageCreatedTimeStamp = msg.CreatedTimestamp; } - public void SetEnqueued() => SetTimestamp(ref EnqueuedTimeStamp); + public void SetEnqueued(ConnectionType? connType) + { + SetTimestamp(ref EnqueuedTimeStamp); + ConnectionType = connType; + } public void SetRequestSent() => SetTimestamp(ref RequestSentTimeStamp); @@ -117,16 +122,10 @@ public void SetCompleted() } public override string ToString() => -$@"EndPoint = {EndPoint} -Db = {Db} -Command = {Command} -CommandCreated = {CommandCreated:u} -CreationToEnqueued = {CreationToEnqueued} -EnqueuedToSending = {EnqueuedToSending} -SentToResponse = {SentToResponse} -ResponseToCompletion = {ResponseToCompletion} -ElapsedTime = {ElapsedTime} -Flags = {Flags} -RetransmissionOf = ({RetransmissionOf?.ToString() ?? "nothing"})"; +$@"{Command} (DB: {Db}, Flags: {Flags}) + EndPoint = {EndPoint} ({ConnectionType}) + Created = {CommandCreated:HH:mm:ss.ffff} + ElapsedTime = {ElapsedTime.TotalMilliseconds} ms (CreationToEnqueued: {CreationToEnqueued.TotalMilliseconds} ms, EnqueuedToSending: {EnqueuedToSending.TotalMilliseconds} ms, SentToResponse: {SentToResponse.TotalMilliseconds} ms, ResponseToCompletion = {ResponseToCompletion.TotalMilliseconds} ms){(RetransmissionOf != null ? @" + RetransmissionOf = " + RetransmissionOf : "")}"; } } diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index 9b2b9c18d..5ca22dfb5 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -1,32 +1,52 @@ using System; -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Threading; using System.Threading.Tasks; using Pipelines.Sockets.Unofficial; +using static StackExchange.Redis.ConnectionMultiplexer; namespace StackExchange.Redis { public partial class ConnectionMultiplexer { - private readonly Dictionary subscriptions = new Dictionary(); + private RedisSubscriber _defaultSubscriber; + private RedisSubscriber DefaultSubscriber => _defaultSubscriber ??= new RedisSubscriber(this, null); - internal int GetSubscriptionsCount() + private readonly ConcurrentDictionary subscriptions = new(); + + internal ConcurrentDictionary GetSubscriptions() => subscriptions; + internal int GetSubscriptionsCount() => subscriptions.Count; + + internal Subscription GetOrAddSubscription(in RedisChannel channel, CommandFlags flags) { lock (subscriptions) { - return subscriptions.Count; + if (!subscriptions.TryGetValue(channel, out var sub)) + { + sub = new Subscription(flags); + subscriptions.TryAdd(channel, sub); + } + return sub; } } - - internal bool GetSubscriberCounts(in RedisChannel channel, out int handlers, out int queues) + internal bool TryGetSubscription(in RedisChannel channel, out Subscription sub) => subscriptions.TryGetValue(channel, out sub); + internal bool TryRemoveSubscription(in RedisChannel channel, out Subscription sub) { - Subscription sub; lock (subscriptions) { - if (!subscriptions.TryGetValue(channel, out sub)) sub = null; + return subscriptions.TryRemove(channel, out sub); } - if (sub != null) + } + + /// + /// Gets the subscriber counts for a channel. + /// + /// True if there's a subscription registered at all. + internal bool GetSubscriberCounts(in RedisChannel channel, out int handlers, out int queues) + { + if (subscriptions.TryGetValue(channel, out var sub)) { sub.GetSubscriberCounts(out handlers, out queues); return true; @@ -35,273 +55,167 @@ internal bool GetSubscriberCounts(in RedisChannel channel, out int handlers, out return false; } - internal Task AddSubscription(in RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags, object asyncState) - { - Task task = null; - if (handler != null | queue != null) - { - lock (subscriptions) - { - if (!subscriptions.TryGetValue(channel, out Subscription sub)) - { - sub = new Subscription(); - subscriptions.Add(channel, sub); - task = sub.SubscribeToServer(this, channel, flags, asyncState, false); - } - sub.Add(handler, queue); - } - } - return task ?? CompletedTask.Default(asyncState); - } - + /// + /// Gets which server, if any, there's a registered subscription to for this channel. + /// + /// + /// This may be null if there is a subscription, but we don't have a connected server at the moment. + /// This behavior is fine but IsConnected checks, but is a subtle difference in . + /// internal ServerEndPoint GetSubscribedServer(in RedisChannel channel) { - if (!channel.IsNullOrEmpty) + if (!channel.IsNullOrEmpty && subscriptions.TryGetValue(channel, out Subscription sub)) { - lock (subscriptions) - { - if (subscriptions.TryGetValue(channel, out Subscription sub)) - { - return sub.GetOwner(); - } - } + return sub.GetCurrentServer(); } return null; } + /// + /// Handler that executes whenever a message comes in, this doles out messages to any registered handlers. + /// internal void OnMessage(in RedisChannel subscription, in RedisChannel channel, in RedisValue payload) { ICompletable completable = null; ChannelMessageQueue queues = null; - lock (subscriptions) + if (subscriptions.TryGetValue(subscription, out Subscription sub)) { - if (subscriptions.TryGetValue(subscription, out Subscription sub)) - { - completable = sub.ForInvoke(channel, payload, out queues); - } + completable = sub.ForInvoke(channel, payload, out queues); } - if (queues != null) ChannelMessageQueue.WriteAll(ref queues, channel, payload); - if (completable != null && !completable.TryComplete(false)) CompleteAsWorker(completable); - } - - internal Task RemoveAllSubscriptions(CommandFlags flags, object asyncState) - { - Task last = null; - lock (subscriptions) + if (queues != null) { - foreach (var pair in subscriptions) - { - pair.Value.MarkCompleted(); - var task = pair.Value.UnsubscribeFromServer(pair.Key, flags, asyncState, false); - if (task != null) last = task; - } - subscriptions.Clear(); + ChannelMessageQueue.WriteAll(ref queues, channel, payload); + } + if (completable != null && !completable.TryComplete(false)) + { + CompleteAsWorker(completable); } - return last ?? CompletedTask.Default(asyncState); } - internal Task RemoveSubscription(in RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags, object asyncState) + /// + /// Updates all subscriptions re-evaluating their state. + /// This clears the current server if it's not connected, prepping them to reconnect. + /// + internal void UpdateSubscriptions() { - Task task = null; - lock (subscriptions) + foreach (var pair in subscriptions) { - if (subscriptions.TryGetValue(channel, out Subscription sub)) - { - bool remove; - if (handler == null & queue == null) // blanket wipe - { - sub.MarkCompleted(); - remove = true; - } - else - { - remove = sub.Remove(handler, queue); - } - if (remove) - { - subscriptions.Remove(channel); - task = sub.UnsubscribeFromServer(channel, flags, asyncState, false); - } - } + pair.Value.UpdateServer(); } - return task ?? CompletedTask.Default(asyncState); } - internal void ResendSubscriptions(ServerEndPoint server) + /// + /// Ensures all subscriptions are connected to a server, if possible. + /// + /// The count of subscriptions attempting to reconnect (same as the count currently not connected). + internal long EnsureSubscriptions(CommandFlags flags = CommandFlags.None) { - if (server == null) return; - lock (subscriptions) + // TODO: Subscribe with variadic commands to reduce round trips + long count = 0; + foreach (var pair in subscriptions) { - foreach (var pair in subscriptions) + if (!pair.Value.IsConnected) { - pair.Value.Resubscribe(pair.Key, server); + count++; + DefaultSubscriber.EnsureSubscribedToServer(pair.Value, pair.Key, flags, true); } } + return count; } - internal bool SubscriberConnected(in RedisChannel channel = default(RedisChannel)) + internal enum SubscriptionAction { - var server = GetSubscribedServer(channel); - if (server != null) return server.IsConnected; - - server = SelectServer(RedisCommand.SUBSCRIBE, CommandFlags.DemandMaster, channel); - return server?.IsConnected == true; - } - - internal long ValidateSubscriptions() - { - lock (subscriptions) - { - long count = 0; - foreach (var pair in subscriptions) - { - if (pair.Value.Validate(this, pair.Key)) count++; - } - return count; - } + Subscribe, + Unsubscribe } + /// + /// This is the record of a single subscription to a redis server. + /// It's the singular channel (which may or may not be a pattern), to one or more handlers. + /// We subscriber to a redis server once (for all messages) and execute 1-many handlers when a message arrives. + /// internal sealed class Subscription { private Action _handlers; private ChannelMessageQueue _queues; - private ServerEndPoint owner; + private ServerEndPoint CurrentServer; + public CommandFlags Flags { get; } + public ResultProcessor.TrackSubscriptionsProcessor Processor { get; } - public void Add(Action handler, ChannelMessageQueue queue) - { - if (handler != null) _handlers += handler; - if (queue != null) ChannelMessageQueue.Combine(ref _queues, queue); - } + /// + /// Whether the we have is connected. + /// Since we clear on a disconnect, this should stay correct. + /// + internal bool IsConnected => CurrentServer?.IsSubscriberConnected == true; - public ICompletable ForInvoke(in RedisChannel channel, in RedisValue message, out ChannelMessageQueue queues) + public Subscription(CommandFlags flags) { - var handlers = _handlers; - queues = Volatile.Read(ref _queues); - return handlers == null ? null : new MessageCompletable(channel, message, handlers); + Flags = flags; + Processor = new ResultProcessor.TrackSubscriptionsProcessor(this); } - internal void MarkCompleted() - { - _handlers = null; - ChannelMessageQueue.MarkAllCompleted(ref _queues); - } - - public bool Remove(Action handler, ChannelMessageQueue queue) + /// + /// Gets the configured (P)SUBSCRIBE or (P)UNSUBSCRIBE for an action. + /// + internal Message GetMessage(RedisChannel channel, SubscriptionAction action, CommandFlags flags, bool internalCall) { - if (handler != null) _handlers -= handler; - if (queue != null) ChannelMessageQueue.Remove(ref _queues, queue); - return _handlers == null & _queues == null; + var isPattern = channel.IsPatternBased; + var command = action switch + { + SubscriptionAction.Subscribe when isPattern => RedisCommand.PSUBSCRIBE, + SubscriptionAction.Unsubscribe when isPattern => RedisCommand.PUNSUBSCRIBE, + + SubscriptionAction.Subscribe when !isPattern => RedisCommand.SUBSCRIBE, + SubscriptionAction.Unsubscribe when !isPattern => RedisCommand.UNSUBSCRIBE, + _ => throw new ArgumentOutOfRangeException("This would be an impressive boolean feat"), + }; + + // TODO: Consider flags here - we need to pass Fire and Forget, but don't want to intermingle Primary/Replica + var msg = Message.Create(-1, Flags | flags, command, channel); + msg.SetForSubscriptionBridge(); + if (internalCall) + { + msg.SetInternalCall(); + } + return msg; } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "RCS1210:Return completed task instead of returning null.", Justification = "Intentional for efficient success check")] - public Task SubscribeToServer(ConnectionMultiplexer multiplexer, in RedisChannel channel, CommandFlags flags, object asyncState, bool internalCall) + public void Add(Action handler, ChannelMessageQueue queue) { - var selected = multiplexer.SelectServer(RedisCommand.SUBSCRIBE, flags, channel); - var bridge = selected?.GetBridge(ConnectionType.Subscription, true); - if (bridge == null) return null; - - // note: check we can create the message validly *before* we swap the owner over (Interlocked) - var state = PendingSubscriptionState.Create(channel, this, flags, true, internalCall, asyncState, selected.IsReplica); - - if (Interlocked.CompareExchange(ref owner, selected, null) != null) return null; - try + if (handler != null) { - if (!bridge.TryEnqueueBackgroundSubscriptionWrite(state)) - { - state.Abort(); - return null; - } - return state.Task; + _handlers += handler; } - catch + if (queue != null) { - // clear the owner if it is still us - Interlocked.CompareExchange(ref owner, null, selected); - throw; + ChannelMessageQueue.Combine(ref _queues, queue); } } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "RCS1210:Return completed task instead of returning null.", Justification = "Intentional for efficient success check")] - public Task UnsubscribeFromServer(in RedisChannel channel, CommandFlags flags, object asyncState, bool internalCall) + public bool Remove(Action handler, ChannelMessageQueue queue) { - var oldOwner = Interlocked.Exchange(ref owner, null); - var bridge = oldOwner?.GetBridge(ConnectionType.Subscription, false); - if (bridge == null) return null; - - var state = PendingSubscriptionState.Create(channel, this, flags, false, internalCall, asyncState, oldOwner.IsReplica); - - if (!bridge.TryEnqueueBackgroundSubscriptionWrite(state)) + if (handler != null) { - state.Abort(); - return null; + _handlers -= handler; } - return state.Task; - } - - internal readonly struct PendingSubscriptionState - { - public override string ToString() => Message.ToString(); - public Subscription Subscription { get; } - public Message Message { get; } - public bool IsReplica { get; } - public Task Task => _taskSource.Task; - private readonly TaskCompletionSource _taskSource; - - public static PendingSubscriptionState Create(RedisChannel channel, Subscription subscription, CommandFlags flags, bool subscribe, bool internalCall, object asyncState, bool isReplica) - => new PendingSubscriptionState(asyncState, channel, subscription, flags, subscribe, internalCall, isReplica); - - public void Abort() => _taskSource.TrySetCanceled(); - public void Fail(Exception ex) => _taskSource.TrySetException(ex); - - private PendingSubscriptionState(object asyncState, RedisChannel channel, Subscription subscription, CommandFlags flags, bool subscribe, bool internalCall, bool isReplica) + if (queue != null) { - var cmd = subscribe - ? (channel.IsPatternBased ? RedisCommand.PSUBSCRIBE : RedisCommand.SUBSCRIBE) - : (channel.IsPatternBased ? RedisCommand.PUNSUBSCRIBE : RedisCommand.UNSUBSCRIBE); - var msg = Message.Create(-1, flags, cmd, channel); - if (internalCall) msg.SetInternalCall(); - - var source = TaskResultBox.Create(out _taskSource, asyncState); - msg.SetSource(ResultProcessor.TrackSubscriptions, source); - - Subscription = subscription; - Message = msg; - IsReplica = isReplica; + ChannelMessageQueue.Remove(ref _queues, queue); } + return _handlers == null & _queues == null; } - internal ServerEndPoint GetOwner() => Volatile.Read(ref owner); - - internal void Resubscribe(in RedisChannel channel, ServerEndPoint server) + public ICompletable ForInvoke(in RedisChannel channel, in RedisValue message, out ChannelMessageQueue queues) { - if (server != null && Interlocked.CompareExchange(ref owner, server, server) == server) - { - var cmd = channel.IsPatternBased ? RedisCommand.PSUBSCRIBE : RedisCommand.SUBSCRIBE; - var msg = Message.Create(-1, CommandFlags.FireAndForget, cmd, channel); - msg.SetInternalCall(); -#pragma warning disable CS0618 - server.WriteDirectFireAndForgetSync(msg, ResultProcessor.TrackSubscriptions); -#pragma warning restore CS0618 - } + var handlers = _handlers; + queues = Volatile.Read(ref _queues); + return handlers == null ? null : new MessageCompletable(channel, message, handlers); } - internal bool Validate(ConnectionMultiplexer multiplexer, in RedisChannel channel) + internal void MarkCompleted() { - bool changed = false; - var oldOwner = Volatile.Read(ref owner); - if (oldOwner != null && !oldOwner.IsSelectable(RedisCommand.PSUBSCRIBE)) - { - if (UnsubscribeFromServer(channel, CommandFlags.FireAndForget, null, true) != null) - { - changed = true; - } - oldOwner = null; - } - if (oldOwner == null && SubscribeToServer(multiplexer, channel, CommandFlags.FireAndForget, null, true) != null) - { - changed = true; - } - return changed; + _handlers = null; + ChannelMessageQueue.MarkAllCompleted(ref _queues); } internal void GetSubscriberCounts(out int handlers, out int queues) @@ -322,9 +236,30 @@ internal void GetSubscriberCounts(out int handlers, out int queues) foreach (var sub in tmp.AsEnumerable()) { handlers++; } } } + + internal ServerEndPoint GetCurrentServer() => Volatile.Read(ref CurrentServer); + internal void SetCurrentServer(ServerEndPoint server) => CurrentServer = server; + + /// + /// Evaluates state and if we're not currently connected, clears the server reference. + /// + internal void UpdateServer() + { + if (!IsConnected) + { + CurrentServer = null; + } + } } } + /// + /// A wrapper for subscription actions. + /// + /// + /// By having most functionality here and state on , we can + /// use the baseline execution methods to take the normal message paths. + /// internal sealed class RedisSubscriber : RedisBase, ISubscriber { internal RedisSubscriber(ConnectionMultiplexer multiplexer, object asyncState) : base(multiplexer, asyncState) @@ -345,30 +280,34 @@ public Task IdentifyEndpointAsync(RedisChannel channel, CommandFlags f return ExecuteAsync(msg, ResultProcessor.ConnectionIdentity); } + /// + /// This is *could* we be connected, as in "what's the theoretical endpoint for this channel?", + /// rather than if we're actually connected and actually listening on that channel. + /// public bool IsConnected(RedisChannel channel = default(RedisChannel)) { - return multiplexer.SubscriberConnected(channel); + var server = multiplexer.GetSubscribedServer(channel) ?? multiplexer.SelectServer(RedisCommand.SUBSCRIBE, CommandFlags.DemandMaster, channel); + return server?.IsConnected == true && server.IsSubscriberConnected; } public override TimeSpan Ping(CommandFlags flags = CommandFlags.None) { - var msg = CreatePingMessage(flags, out var server); - return ExecuteSync(msg, ResultProcessor.ResponseTimer, server); + var msg = CreatePingMessage(flags); + return ExecuteSync(msg, ResultProcessor.ResponseTimer); } public override Task PingAsync(CommandFlags flags = CommandFlags.None) { - var msg = CreatePingMessage(flags, out var server); - return ExecuteAsync(msg, ResultProcessor.ResponseTimer, server); + var msg = CreatePingMessage(flags); + return ExecuteAsync(msg, ResultProcessor.ResponseTimer); } - private Message CreatePingMessage(CommandFlags flags, out ServerEndPoint server) + private Message CreatePingMessage(CommandFlags flags) { bool usePing = false; - server = null; if (multiplexer.CommandMap.IsAvailable(RedisCommand.PING)) { - try { usePing = GetFeatures(default, flags, out server).PingOnSubscriber; } + try { usePing = GetFeatures(default, flags, out _).PingOnSubscriber; } catch { } } @@ -383,21 +322,29 @@ private Message CreatePingMessage(CommandFlags flags, out ServerEndPoint server) RedisValue channel = multiplexer.UniqueId; msg = ResultProcessor.TimingProcessor.CreateMessage(-1, flags, RedisCommand.UNSUBSCRIBE, channel); } - // Ensure the ping is sent over the intended subscriver connection, which wouldn't happen in GetBridge() by default with PING; + // Ensure the ping is sent over the intended subscriber connection, which wouldn't happen in GetBridge() by default with PING; msg.SetForSubscriptionBridge(); return msg; } + private void ThrowIfNull(in RedisChannel channel) + { + if (channel.IsNullOrEmpty) + { + throw new ArgumentNullException(nameof(channel)); + } + } + public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { - if (channel.IsNullOrEmpty) throw new ArgumentNullException(nameof(channel)); + ThrowIfNull(channel); var msg = Message.Create(-1, flags, RedisCommand.PUBLISH, channel, message); return ExecuteSync(msg, ResultProcessor.Int64); } public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { - if (channel.IsNullOrEmpty) throw new ArgumentNullException(nameof(channel)); + ThrowIfNull(channel); var msg = Message.Create(-1, flags, RedisCommand.PUBLISH, channel, message); return ExecuteAsync(msg, ResultProcessor.Int64); } @@ -405,12 +352,6 @@ public Task PublishAsync(RedisChannel channel, RedisValue message, Command void ISubscriber.Subscribe(RedisChannel channel, Action handler, CommandFlags flags) => Subscribe(channel, handler, null, flags); - public void Subscribe(RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags) - { - var task = SubscribeAsync(channel, handler, queue, flags); - if ((flags & CommandFlags.FireAndForget) == 0) Wait(task); - } - public ChannelMessageQueue Subscribe(RedisChannel channel, CommandFlags flags = CommandFlags.None) { var queue = new ChannelMessageQueue(channel, this); @@ -418,17 +359,30 @@ public ChannelMessageQueue Subscribe(RedisChannel channel, CommandFlags flags = return queue; } - Task ISubscriber.SubscribeAsync(RedisChannel channel, Action handler, CommandFlags flags) - => SubscribeAsync(channel, handler, null, flags); + public bool Subscribe(RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags) + { + ThrowIfNull(channel); + if (handler == null && queue == null) { return true; } + + var sub = multiplexer.GetOrAddSubscription(channel, flags); + sub.Add(handler, queue); + return EnsureSubscribedToServer(sub, channel, flags, false); + } - public Task SubscribeAsync(in RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags) + internal bool EnsureSubscribedToServer(Subscription sub, RedisChannel channel, CommandFlags flags, bool internalCall) { - if (channel.IsNullOrEmpty) throw new ArgumentNullException(nameof(channel)); - return multiplexer.AddSubscription(channel, handler, queue, flags, asyncState); + if (sub.IsConnected) { return true; } + + // TODO: Cleanup old hangers here? + + sub.SetCurrentServer(null); // we're not appropriately connected, so blank it out for eligible reconnection + var message = sub.GetMessage(channel, SubscriptionAction.Subscribe, flags, internalCall); + var selected = multiplexer.SelectServer(message); + return multiplexer.ExecuteSyncImpl(message, sub.Processor, selected); } - internal bool GetSubscriberCounts(in RedisChannel channel, out int handlers, out int queues) - => multiplexer.GetSubscriberCounts(channel, out handlers, out queues); + Task ISubscriber.SubscribeAsync(RedisChannel channel, Action handler, CommandFlags flags) + => SubscribeAsync(channel, handler, null, flags); public async Task SubscribeAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None) { @@ -437,37 +391,129 @@ public async Task SubscribeAsync(RedisChannel channel, Comm return queue; } - public EndPoint SubscribedEndpoint(RedisChannel channel) + public Task SubscribeAsync(RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags) { - var server = multiplexer.GetSubscribedServer(channel); - return server?.EndPoint; + ThrowIfNull(channel); + if (handler == null && queue == null) { return CompletedTask.Default(null); } + + var sub = multiplexer.GetOrAddSubscription(channel, flags); + sub.Add(handler, queue); + return EnsureSubscribedToServerAsync(sub, channel, flags, false); } - void ISubscriber.Unsubscribe(RedisChannel channel, Action handler, CommandFlags flags) - => Unsubscribe(channel, handler, null, flags); - public void Unsubscribe(in RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags) + public Task EnsureSubscribedToServerAsync(Subscription sub, RedisChannel channel, CommandFlags flags, bool internalCall) { - var task = UnsubscribeAsync(channel, handler, queue, flags); - if ((flags & CommandFlags.FireAndForget) == 0) Wait(task); + if (sub.IsConnected) { return CompletedTask.Default(null); } + + // TODO: Cleanup old hangers here? + + sub.SetCurrentServer(null); // we're not appropriately connected, so blank it out for eligible reconnection + var message = sub.GetMessage(channel, SubscriptionAction.Subscribe, flags, internalCall); + var selected = multiplexer.SelectServer(message); + return ExecuteAsync(message, sub.Processor, selected); } - public void UnsubscribeAll(CommandFlags flags = CommandFlags.None) + public EndPoint SubscribedEndpoint(RedisChannel channel) => multiplexer.GetSubscribedServer(channel)?.EndPoint; + + void ISubscriber.Unsubscribe(RedisChannel channel, Action handler, CommandFlags flags) + => Unsubscribe(channel, handler, null, flags); + + [SuppressMessage("Style", "IDE0075:Simplify conditional expression", Justification = "The suggestion sucks.")] + public bool Unsubscribe(in RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags) { - var task = UnsubscribeAllAsync(flags); - if ((flags & CommandFlags.FireAndForget) == 0) Wait(task); + ThrowIfNull(channel); + // Unregister the subscription handler/queue, and if that returns true (last handler removed), also disconnect from the server + return UnregisterSubscription(channel, handler, queue, out var sub) + ? UnsubscribeFromServer(sub, channel, flags, false) + : true; } - public Task UnsubscribeAllAsync(CommandFlags flags = CommandFlags.None) + private bool UnsubscribeFromServer(Subscription sub, RedisChannel channel, CommandFlags flags, bool internalCall) { - return multiplexer.RemoveAllSubscriptions(flags, asyncState); + if (sub.GetCurrentServer() is ServerEndPoint oldOwner) + { + var message = sub.GetMessage(channel, SubscriptionAction.Unsubscribe, flags, internalCall); + return multiplexer.ExecuteSyncImpl(message, sub.Processor, oldOwner); + } + return false; } Task ISubscriber.UnsubscribeAsync(RedisChannel channel, Action handler, CommandFlags flags) => UnsubscribeAsync(channel, handler, null, flags); - public Task UnsubscribeAsync(in RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags) + + public Task UnsubscribeAsync(in RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags) + { + ThrowIfNull(channel); + // Unregister the subscription handler/queue, and if that returns true (last handler removed), also disconnect from the server + return UnregisterSubscription(channel, handler, queue, out var sub) + ? UnsubscribeFromServerAsync(sub, channel, flags, asyncState, false) + : CompletedTask.Default(asyncState); + } + + private Task UnsubscribeFromServerAsync(Subscription sub, RedisChannel channel, CommandFlags flags, object asyncState, bool internalCall) { - if (channel.IsNullOrEmpty) throw new ArgumentNullException(nameof(channel)); - return multiplexer.RemoveSubscription(channel, handler, queue, flags, asyncState); + if (sub.GetCurrentServer() is ServerEndPoint oldOwner) + { + var message = sub.GetMessage(channel, SubscriptionAction.Unsubscribe, flags, internalCall); + return multiplexer.ExecuteAsyncImpl(message, sub.Processor, asyncState, oldOwner); + } + return CompletedTask.FromResult(true, asyncState); + } + + /// + /// Unregisters a handler or queue and returns if we should remove it from the server. + /// + /// True if we should remove the subscription from the server, false otherwise. + private bool UnregisterSubscription(in RedisChannel channel, Action handler, ChannelMessageQueue queue, out Subscription sub) + { + ThrowIfNull(channel); + if (multiplexer.TryGetSubscription(channel, out sub)) + { + if (handler == null & queue == null) + { + // This was a blanket wipe, so clear it completely + sub.MarkCompleted(); + multiplexer.TryRemoveSubscription(channel, out _); + return true; + } + else if (sub.Remove(handler, queue)) + { + // Or this was the last handler and/or queue, which also means unsubscribe + multiplexer.TryRemoveSubscription(channel, out _); + return true; + } + } + return false; + } + + public void UnsubscribeAll(CommandFlags flags = CommandFlags.None) + { + // TODO: Unsubscribe variadic commands to reduce round trips + var subs = multiplexer.GetSubscriptions(); + foreach (var pair in subs) + { + if (subs.TryRemove(pair.Key, out var sub)) + { + sub.MarkCompleted(); + UnsubscribeFromServer(sub, pair.Key, flags, false); + } + } + } + + public Task UnsubscribeAllAsync(CommandFlags flags = CommandFlags.None) + { + // TODO: Unsubscribe variadic commands to reduce round trips + Task last = null; + var subs = multiplexer.GetSubscriptions(); + foreach (var pair in subs) + { + if (subs.TryRemove(pair.Key, out var sub)) + { + sub.MarkCompleted(); + last = UnsubscribeFromServerAsync(sub, pair.Key, flags, asyncState, false); + } + } + return last ?? CompletedTask.Default(asyncState); } } } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 0fc523316..805c50f2f 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -18,7 +18,7 @@ public static readonly ResultProcessor DemandPONG = new ExpectBasicStringProcessor(CommonReplies.PONG), DemandZeroOrOne = new DemandZeroOrOneProcessor(), AutoConfigure = new AutoConfigureProcessor(), - TrackSubscriptions = new TrackSubscriptionsProcessor(), + TrackSubscriptions = new TrackSubscriptionsProcessor(null), Tracer = new TracerProcessor(false), EstablishConnection = new TracerProcessor(true), BackgroundSaveStarted = new ExpectBasicStringProcessor(CommonReplies.backgroundSavingStarted_trimmed, startsWith: true); @@ -392,6 +392,9 @@ protected override void WriteImpl(PhysicalConnection physical) public sealed class TrackSubscriptionsProcessor : ResultProcessor { + private ConnectionMultiplexer.Subscription Subscription { get; } + public TrackSubscriptionsProcessor(ConnectionMultiplexer.Subscription sub) => Subscription = sub; + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { if (result.Type == ResultType.MultiBulk) @@ -400,9 +403,18 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (items.Length >= 3 && items[2].TryGetInt64(out long count)) { connection.SubscriptionCount = count; + SetResult(message, true); + + var newServer = message.Command switch + { + RedisCommand.SUBSCRIBE or RedisCommand.PSUBSCRIBE => connection.BridgeCouldBeNull?.ServerEndPoint, + _ => null + }; + Subscription?.SetCurrentServer(newServer); return true; } } + SetResult(message, false); return false; } } diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index ab2889e65..18f68f36b 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -575,6 +575,10 @@ internal void OnDisconnected(PhysicalBridge bridge) { CompletePendingConnectionMonitors("Disconnected"); } + else if (bridge == subscription) + { + Multiplexer.UpdateSubscriptions(); + } } internal Task OnEstablishingAsync(PhysicalConnection connection, LogProxy log) @@ -615,10 +619,14 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) { if (bridge == subscription) { - Multiplexer.ResendSubscriptions(this); + // Note: this MUST be fire and forget, because we might be in the middle of a Sync processing + // TracerProcessor which is executing this line inside a SetResultCore(). + // Since we're issuing commands inside a SetResult path in a message, we'd create a deadlock by waiting. + Multiplexer.EnsureSubscriptions(CommandFlags.FireAndForget); } - else if (bridge == interactive) + if (IsConnected && IsSubscriberConnected) { + // Only connect on the second leg - we can accomplish this by checking both CompletePendingConnectionMonitors(source); } diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index cb4bb954c..7578936f2 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -288,7 +288,7 @@ private ServerEndPoint[] MapForMutation() private ServerEndPoint Select(int slot, RedisCommand command, CommandFlags flags) { - flags = Message.GetMasterReplicaFlags(flags); // only intersted in master/replica preferences + flags = Message.GetMasterReplicaFlags(flags); // only interested in master/replica preferences ServerEndPoint[] arr; if (slot == NoSlot || (arr = map) == null) return Any(command, flags); diff --git a/src/StackExchange.Redis/SimulatedFailureType.cs b/src/StackExchange.Redis/SimulatedFailureType.cs index 0084746a7..80fca095c 100644 --- a/src/StackExchange.Redis/SimulatedFailureType.cs +++ b/src/StackExchange.Redis/SimulatedFailureType.cs @@ -10,8 +10,13 @@ internal enum SimulatedFailureType InteractiveOutbound = 1 << 1, SubscriptionInbound = 1 << 2, SubscriptionOutbound = 1 << 3, + AllInbound = InteractiveInbound | SubscriptionInbound, AllOutbound = InteractiveOutbound | SubscriptionOutbound, + + AllInteractive = InteractiveInbound | InteractiveOutbound, + AllSubscription = SubscriptionInbound | SubscriptionOutbound, + All = AllInbound | AllOutbound, } } diff --git a/tests/RedisConfigs/Dockerfile b/tests/RedisConfigs/Dockerfile index 969497b7d..047da2975 100644 --- a/tests/RedisConfigs/Dockerfile +++ b/tests/RedisConfigs/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:5 +FROM redis:6.2.6 COPY Basic /data/Basic/ COPY Failover /data/Failover/ diff --git a/tests/RedisConfigs/docker-compose.yml b/tests/RedisConfigs/docker-compose.yml index 6475e6664..cb3dd099c 100644 --- a/tests/RedisConfigs/docker-compose.yml +++ b/tests/RedisConfigs/docker-compose.yml @@ -1,4 +1,4 @@ -version: '2.5' +version: '2.6' services: redis: diff --git a/tests/StackExchange.Redis.Tests/Cluster.cs b/tests/StackExchange.Redis.Tests/Cluster.cs index a7af400da..6e6e6e7c3 100644 --- a/tests/StackExchange.Redis.Tests/Cluster.cs +++ b/tests/StackExchange.Redis.Tests/Cluster.cs @@ -3,7 +3,6 @@ using System.IO; using System.Linq; using System.Net; -using System.Text; using System.Threading; using System.Threading.Tasks; using StackExchange.Redis.Profiling; @@ -15,14 +14,7 @@ namespace StackExchange.Redis.Tests public class Cluster : TestBase { public Cluster(ITestOutputHelper output) : base (output) { } - - protected override string GetConfiguration() - { - var server = TestConfig.Current.ClusterServer; - return string.Join(",", - Enumerable.Range(TestConfig.Current.ClusterStartPort, TestConfig.Current.ClusterServerCount).Select(port => server + ":" + port) - ) + ",connectTimeout=10000"; - } + protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; [Fact] public void ExportConfiguration() diff --git a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs index 07cbc6e58..a11afa18c 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs @@ -2,6 +2,7 @@ using System; using Newtonsoft.Json; using System.Threading; +using System.Linq; namespace StackExchange.Redis.Tests { @@ -87,6 +88,7 @@ public class Config public string ClusterServer { get; set; } = "127.0.0.1"; public int ClusterStartPort { get; set; } = 7000; public int ClusterServerCount { get; set; } = 6; + public string ClusterServersAndPorts => string.Join(",", Enumerable.Range(ClusterStartPort, ClusterServerCount).Select(port => ClusterServer + ":" + port)); public string SslServer { get; set; } public int SslPort { get; set; } diff --git a/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs b/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs index 7d89a187a..bdcb7b55f 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs @@ -20,6 +20,20 @@ public TextWriterOutputHelper(ITestOutputHelper outputHelper, bool echoToConsole public void EchoTo(StringBuilder sb) => Echo = sb; + public void WriteLineNoTime(string value) + { + try + { + base.WriteLine(value); + } + catch (Exception ex) + { + Console.Write("Attempted to write: "); + Console.WriteLine(value); + Console.WriteLine(ex); + } + } + public override void WriteLine(string value) { try @@ -68,7 +82,14 @@ protected override void Dispose(bool disposing) private void FlushBuffer() { var text = Buffer.ToString(); - Output.WriteLine(text); + try + { + Output.WriteLine(text); + } + catch (InvalidOperationException) + { + // Thrown when writing from a handler after a test has ended - just bail in this case + } Echo?.AppendLine(text); if (ToConsole) { diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs b/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs index aebf76648..c81a6e004 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs @@ -15,7 +15,7 @@ public Issue1101(ITestOutputHelper output) : base(output) { } private static void AssertCounts(ISubscriber pubsub, in RedisChannel channel, bool has, int handlers, int queues) { - var aHas = ((RedisSubscriber)pubsub).GetSubscriberCounts(channel, out var ah, out var aq); + var aHas = (pubsub.Multiplexer as ConnectionMultiplexer).GetSubscriberCounts(channel, out var ah, out var aq); Assert.Equal(has, aHas); Assert.Equal(handlers, ah); Assert.Equal(queues, aq); @@ -23,7 +23,7 @@ private static void AssertCounts(ISubscriber pubsub, in RedisChannel channel, [Fact] public async Task ExecuteWithUnsubscribeViaChannel() { - using (var muxer = Create()) + using (var muxer = Create(log: Writer)) { RedisChannel name = Me(); var pubsub = muxer.GetSubscriber(); @@ -89,7 +89,7 @@ public async Task ExecuteWithUnsubscribeViaChannel() [Fact] public async Task ExecuteWithUnsubscribeViaSubscriber() { - using (var muxer = Create()) + using (var muxer = Create(shared: false, log: Writer)) { RedisChannel name = Me(); var pubsub = muxer.GetSubscriber(); @@ -141,7 +141,7 @@ public async Task ExecuteWithUnsubscribeViaSubscriber() [Fact] public async Task ExecuteWithUnsubscribeViaClearAll() { - using (var muxer = Create()) + using (var muxer = Create(log: Writer)) { RedisChannel name = Me(); var pubsub = muxer.GetSubscriber(); diff --git a/tests/StackExchange.Redis.Tests/Locking.cs b/tests/StackExchange.Redis.Tests/Locking.cs index 9053d2956..760b1b65f 100644 --- a/tests/StackExchange.Redis.Tests/Locking.cs +++ b/tests/StackExchange.Redis.Tests/Locking.cs @@ -99,8 +99,8 @@ private void TestLockOpCountByVersion(IConnectionMultiplexer conn, int expectedO Assert.Equal(!existFirst, taken); Assert.Equal(expectedVal, valAfter); - Assert.True(expectedOps >= countAfter - countBefore, $"{expectedOps} >= ({countAfter} - {countBefore})"); // note we get a ping from GetCounters + Assert.True(countAfter - countBefore >= expectedOps, $"({countAfter} - {countBefore}) >= {expectedOps}"); } private IConnectionMultiplexer Create(TestMode mode) => mode switch diff --git a/tests/StackExchange.Redis.Tests/PubSub.cs b/tests/StackExchange.Redis.Tests/PubSub.cs index 2ca92a430..2c4d6ef80 100644 --- a/tests/StackExchange.Redis.Tests/PubSub.cs +++ b/tests/StackExchange.Redis.Tests/PubSub.cs @@ -1,13 +1,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Text; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using StackExchange.Redis.Maintenance; -using StackExchange.Redis.Profiling; using Xunit; using Xunit.Abstractions; // ReSharper disable AccessToModifiedClosure @@ -22,7 +20,7 @@ public PubSub(ITestOutputHelper output, SharedConnectionFixture fixture) : base( [Fact] public async Task ExplicitPublishMode() { - using (var mx = Create(channelPrefix: "foo:")) + using (var mx = Create(channelPrefix: "foo:", log: Writer)) { var pub = mx.GetSubscriber(); int a = 0, b = 0, c = 0, d = 0; @@ -31,7 +29,6 @@ public async Task ExplicitPublishMode() pub.Subscribe(new RedisChannel("ab*d", RedisChannel.PatternMode.Auto), (x, y) => Interlocked.Increment(ref c)); pub.Subscribe("abc*", (x, y) => Interlocked.Increment(ref d)); - await Task.Delay(1000).ForAwait(); pub.Publish("abcd", "efg"); await UntilCondition(TimeSpan.FromSeconds(10), () => Thread.VolatileRead(ref b) == 1 @@ -57,12 +54,12 @@ await UntilCondition(TimeSpan.FromSeconds(10), [InlineData("Foo:", true, "f")] public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string breaker) { - using (var muxer = Create(channelPrefix: channelPrefix, log: Writer)) + using (var muxer = Create(channelPrefix: channelPrefix, shared: false, log: Writer)) { var pub = GetAnyMaster(muxer); var sub = muxer.GetSubscriber(); - await PingAsync(muxer, pub, sub).ForAwait(); - HashSet received = new HashSet(); + await PingAsync(pub, sub).ForAwait(); + HashSet received = new(); int secondHandler = 0; string subChannel = (wildCard ? "a*c" : "abc") + breaker; string pubChannel = "abc" + breaker; @@ -91,35 +88,44 @@ public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string br Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); var count = sub.Publish(pubChannel, "def"); - await PingAsync(muxer, pub, sub, 3).ForAwait(); + await PingAsync(pub, sub, 3).ForAwait(); await UntilCondition(TimeSpan.FromSeconds(5), () => received.Count == 1); lock (received) { Assert.Single(received); } + // Give handler firing a moment + await UntilCondition(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); // unsubscribe from first; should still see second sub.Unsubscribe(subChannel, handler1); count = sub.Publish(pubChannel, "ghi"); - await PingAsync(muxer, pub, sub).ForAwait(); + await PingAsync(pub, sub).ForAwait(); lock (received) { Assert.Single(received); } - Assert.Equal(2, Thread.VolatileRead(ref secondHandler)); + + await UntilCondition(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 2); + + var secondHandlerCount = Thread.VolatileRead(ref secondHandler); + Log("Expecting 2 from second handler, got: " + secondHandlerCount); + Assert.Equal(2, secondHandlerCount); Assert.Equal(1, count); // unsubscribe from second; should see nothing this time sub.Unsubscribe(subChannel, handler2); count = sub.Publish(pubChannel, "ghi"); - await PingAsync(muxer, pub, sub).ForAwait(); + await PingAsync(pub, sub).ForAwait(); lock (received) { Assert.Single(received); } - Assert.Equal(2, Thread.VolatileRead(ref secondHandler)); + secondHandlerCount = Thread.VolatileRead(ref secondHandler); + Log("Expecting 2 from second handler, got: " + secondHandlerCount); + Assert.Equal(2, secondHandlerCount); Assert.Equal(0, count); } } @@ -127,15 +133,16 @@ public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string br [Fact] public async Task TestBasicPubSubFireAndForget() { - using (var muxer = Create(log: Writer)) + using (var muxer = Create(shared: false, log: Writer)) { + var profiler = muxer.AddProfiler(); var pub = GetAnyMaster(muxer); var sub = muxer.GetSubscriber(); RedisChannel key = Me() + Guid.NewGuid(); - HashSet received = new HashSet(); + HashSet received = new(); int secondHandler = 0; - await PingAsync(muxer, pub, sub).ForAwait(); + await PingAsync(pub, sub).ForAwait(); sub.Subscribe(key, (channel, payload) => { lock (received) @@ -148,17 +155,20 @@ public async Task TestBasicPubSubFireAndForget() }, CommandFlags.FireAndForget); sub.Subscribe(key, (_, __) => Interlocked.Increment(ref secondHandler), CommandFlags.FireAndForget); + Log(profiler); lock (received) { Assert.Empty(received); } Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); - await PingAsync(muxer, pub, sub).ForAwait(); + await PingAsync(pub, sub).ForAwait(); var count = sub.Publish(key, "def", CommandFlags.FireAndForget); - await PingAsync(muxer, pub, sub).ForAwait(); + await PingAsync(pub, sub).ForAwait(); await UntilCondition(TimeSpan.FromSeconds(5), () => received.Count == 1); + Log(profiler); + lock (received) { Assert.Single(received); @@ -168,8 +178,8 @@ public async Task TestBasicPubSubFireAndForget() sub.Unsubscribe(key); count = sub.Publish(key, "ghi", CommandFlags.FireAndForget); - await PingAsync(muxer, pub, sub).ForAwait(); - + await PingAsync(pub, sub).ForAwait(); + Log(profiler); lock (received) { Assert.Single(received); @@ -178,27 +188,30 @@ public async Task TestBasicPubSubFireAndForget() } } - private static async Task PingAsync(IConnectionMultiplexer muxer, IServer pub, ISubscriber sub, int times = 1) + private async Task PingAsync(IServer pub, ISubscriber sub, int times = 1) { while (times-- > 0) { // both use async because we want to drain the completion managers, and the only // way to prove that is to use TPL objects - var t1 = sub.PingAsync(); - var t2 = pub.PingAsync(); - await Task.WhenAll(t1, t2).ForAwait(); + var subTask = sub.PingAsync(); + var pubTask = pub.PingAsync(); + await Task.WhenAll(subTask, pubTask).ForAwait(); + + Log($"Sub PING time: {subTask.Result.TotalMilliseconds} ms"); + Log($"Pub PING time: {pubTask.Result.TotalMilliseconds} ms"); } } [Fact] public async Task TestPatternPubSub() { - using (var muxer = Create()) + using (var muxer = Create(shared: false, log: Writer)) { var pub = GetAnyMaster(muxer); var sub = muxer.GetSubscriber(); - HashSet received = new HashSet(); + HashSet received = new(); int secondHandler = 0; sub.Subscribe("a*c", (channel, payload) => { @@ -218,27 +231,29 @@ public async Task TestPatternPubSub() } Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); - await PingAsync(muxer, pub, sub).ForAwait(); + await PingAsync(pub, sub).ForAwait(); var count = sub.Publish("abc", "def"); - await PingAsync(muxer, pub, sub).ForAwait(); + await PingAsync(pub, sub).ForAwait(); await UntilCondition(TimeSpan.FromSeconds(5), () => received.Count == 1); lock (received) { Assert.Single(received); } + + // Give reception a bit, the handler could be delayed under load + await UntilCondition(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); sub.Unsubscribe("a*c"); count = sub.Publish("abc", "ghi"); - await PingAsync(muxer, pub, sub).ForAwait(); + await PingAsync(pub, sub).ForAwait(); lock (received) { Assert.Single(received); } - Assert.Equal(0, count); } } @@ -300,16 +315,17 @@ private void TestMassivePublish(ISubscriber conn, string channel, string caption Assert.True(withFAF.ElapsedMilliseconds < withAsync.ElapsedMilliseconds + 3000, caption); } - [FactLongRunning] + [Fact] public async Task PubSubGetAllAnyOrder() { - using (var muxer = Create(syncTimeout: 20000)) + using (var muxer = Create(syncTimeout: 20000, shared: false, log: Writer)) { var sub = muxer.GetSubscriber(); RedisChannel channel = Me(); const int count = 1000; var syncLock = new object(); + Assert.True(sub.IsConnected()); var data = new HashSet(); await sub.SubscribeAsync(channel, (_, val) => { @@ -424,7 +440,7 @@ await Assert.ThrowsAsync(async delegate [Fact] public async Task PubSubGetAllCorrectOrder_OnMessage_Sync() { - using (var muxer = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000)) + using (var muxer = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { var sub = muxer.GetSubscriber(); RedisChannel channel = Me(); @@ -493,7 +509,7 @@ await Assert.ThrowsAsync(async delegate [Fact] public async Task PubSubGetAllCorrectOrder_OnMessage_Async() { - using (var muxer = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000)) + using (var muxer = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { var sub = muxer.GetSubscriber(); RedisChannel channel = Me(); @@ -564,8 +580,8 @@ await Assert.ThrowsAsync(async delegate public async Task TestPublishWithSubscribers() { var channel = Me(); - using (var muxerA = Create(shared: false)) - using (var muxerB = Create(shared: false)) + using (var muxerA = Create(shared: false, log: Writer)) + using (var muxerB = Create(shared: false, log: Writer)) using (var conn = Create()) { var listenA = muxerA.GetSubscriber(); @@ -588,8 +604,8 @@ public async Task TestPublishWithSubscribers() public async Task TestMultipleSubscribersGetMessage() { var channel = Me(); - using (var muxerA = Create(shared: false)) - using (var muxerB = Create(shared: false)) + using (var muxerA = Create(shared: false, log: Writer)) + using (var muxerB = Create(shared: false, log: Writer)) using (var conn = Create()) { var listenA = muxerA.GetSubscriber(); @@ -619,7 +635,7 @@ public async Task TestMultipleSubscribersGetMessage() public async Task Issue38() { // https://code.google.com/p/booksleeve/issues/detail?id=38 - using (var pub = Create()) + using (var pub = Create(log: Writer)) { var sub = pub.GetSubscriber(); int count = 0; @@ -746,25 +762,19 @@ public async Task AzureRedisEventsAutomaticSubscribe() [Fact] public async Task SubscriptionsSurviveConnectionFailureAsync() { - var session = new ProfilingSession(); - using (var muxer = Create(allowAdmin: true, shared: false, syncTimeout: 1000) as ConnectionMultiplexer) + using (var muxer = Create(allowAdmin: true, shared: false, log: Writer, syncTimeout: 1000) as ConnectionMultiplexer) { - muxer.RegisterProfiler(() => session); + var profiler = muxer.AddProfiler(); RedisChannel channel = Me(); var sub = muxer.GetSubscriber(); int counter = 0; + Assert.True(sub.IsConnected()); await sub.SubscribeAsync(channel, delegate { Interlocked.Increment(ref counter); }).ConfigureAwait(false); - var profile1 = session.FinishProfiling(); - foreach (var command in profile1) - { - Log($"{command.EndPoint}: {command}"); - } - // We shouldn't see the initial connection here - Assert.Equal(0, profile1.Count(p => p.Command == nameof(RedisCommand.SUBSCRIBE))); + var profile1 = Log(profiler); Assert.Equal(1, muxer.GetSubscriptionsCount()); @@ -775,7 +785,7 @@ await sub.SubscribeAsync(channel, delegate await Task.Delay(200).ConfigureAwait(false); var counter1 = Thread.VolatileRead(ref counter); - Log($"Expecting 1 messsage, got {counter1}"); + Log($"Expecting 1 message, got {counter1}"); Assert.Equal(1, counter1); var server = GetServer(muxer); @@ -804,20 +814,23 @@ await sub.SubscribeAsync(channel, delegate // Ensure we're reconnected Assert.True(sub.IsConnected(channel)); - // And time to resubscribe... - await Task.Delay(1000).ConfigureAwait(false); - // Ensure we've sent the subscribe command after reconnecting - var profile2 = session.FinishProfiling(); - foreach (var command in profile2) - { - Log($"{command.EndPoint}: {command}"); - } + var profile2 = Log(profiler); //Assert.Equal(1, profile2.Count(p => p.Command == nameof(RedisCommand.SUBSCRIBE))); - Log($"Issuing ping after reconnected"); + Log("Issuing ping after reconnected"); sub.Ping(); - Assert.Equal(1, muxer.GetSubscriptionsCount()); + + var muxerSubCount = muxer.GetSubscriptionsCount(); + Log($"Muxer thinks we have {muxerSubCount} subscriber(s)."); + Assert.Equal(1, muxerSubCount); + + var muxerSubs = muxer.GetSubscriptions(); + foreach (var pair in muxerSubs) + { + var muxerSub = pair.Value; + Log($" Muxer Sub: {pair.Key}: (EndPoint: {muxerSub.GetCurrentServer()}, Connected: {muxerSub.IsConnected})"); + } Log("Publishing"); var published = await sub.PublishAsync(channel, "abc").ConfigureAwait(false); @@ -830,12 +843,12 @@ await sub.SubscribeAsync(channel, delegate await UntilCondition(TimeSpan.FromSeconds(5), () => Thread.VolatileRead(ref counter) == 2); var counter2 = Thread.VolatileRead(ref counter); - Log($"Expecting 2 messsages, got {counter2}"); + Log($"Expecting 2 messages, got {counter2}"); Assert.Equal(2, counter2); // Log all commands at the end Log("All commands since connecting:"); - var profile3 = session.FinishProfiling(); + var profile3 = profiler.FinishProfiling(); foreach (var command in profile3) { Log($"{command.EndPoint}: {command}"); diff --git a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs new file mode 100644 index 000000000..1a667970e --- /dev/null +++ b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs @@ -0,0 +1,186 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests +{ + [Collection(SharedConnectionFixture.Key)] + public class PubSubMultiserver : TestBase + { + public PubSubMultiserver(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; + + [Fact] + public void ChannelSharding() + { + using var muxer = Create(channelPrefix: Me()) as ConnectionMultiplexer; + + var defaultSlot = muxer.ServerSelectionStrategy.HashSlot(default(RedisChannel)); + var slot1 = muxer.ServerSelectionStrategy.HashSlot((RedisChannel)"hey"); + var slot2 = muxer.ServerSelectionStrategy.HashSlot((RedisChannel)"hey2"); + + Assert.NotEqual(defaultSlot, slot1); + Assert.NotEqual(ServerSelectionStrategy.NoSlot, slot1); + Assert.NotEqual(slot1, slot2); + } + + [Fact] + public async Task ClusterNodeSubscriptionFailover() + { + Log("Connecting..."); + using var muxer = Create(allowAdmin: true) as ConnectionMultiplexer; + var sub = muxer.GetSubscriber(); + var channel = (RedisChannel)Me(); + + var count = 0; + Log("Subscribing..."); + await sub.SubscribeAsync(channel, (channel, val) => + { + Interlocked.Increment(ref count); + Log("Message: " + val); + }); + Assert.True(sub.IsConnected(channel)); + + Log("Publishing (1)..."); + Assert.Equal(0, count); + var publishedTo = await sub.PublishAsync(channel, "message1"); + // Client -> Redis -> Client -> handler takes just a moment + await UntilCondition(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); + Assert.Equal(1, count); + Log($" Published (1) to {publishedTo} subscriber(s)."); + Assert.Equal(1, publishedTo); + + var endpoint = sub.SubscribedEndpoint(channel); + var subscribedServer = muxer.GetServer(endpoint); + var subscribedServerEndpoint = muxer.GetServerEndPoint(endpoint); + + Assert.True(subscribedServer.IsConnected, "subscribedServer.IsConnected"); + Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); + Assert.True(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + + Assert.True(muxer.TryGetSubscription(channel, out var subscription)); + var initialServer = subscription.GetCurrentServer(); + Assert.NotNull(initialServer); + Assert.True(initialServer.IsConnected); + Log($"Connected to: " + initialServer); + + muxer.AllowConnect = false; + subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.AllSubscription); + + Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); + Assert.False(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + + await UntilCondition(TimeSpan.FromSeconds(5), () => subscription.IsConnected); + Assert.True(subscription.IsConnected); + + var newServer = subscription.GetCurrentServer(); + Assert.NotNull(newServer); + Assert.NotEqual(newServer, initialServer); + Log($"Now connected to: " + newServer); + + count = 0; + Log("Publishing (2)..."); + Assert.Equal(0, count); + publishedTo = await sub.PublishAsync(channel, "message2"); + // Client -> Redis -> Client -> handler takes just a moment + await UntilCondition(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); + Assert.Equal(1, count); + Log($" Published (2) to {publishedTo} subscriber(s)."); + + ClearAmbientFailures(); + } + + [Theory] + [InlineData(CommandFlags.PreferMaster, true)] + [InlineData(CommandFlags.PreferReplica, true)] + [InlineData(CommandFlags.DemandMaster, false)] + [InlineData(CommandFlags.DemandReplica, false)] + public async Task PrimaryReplicaSubscriptionFailover(CommandFlags flags, bool expectSuccess) + { + var config = TestConfig.Current.MasterServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + Log("Connecting..."); + using var muxer = Create(configuration: config, shared: false, allowAdmin: true) as ConnectionMultiplexer; + var sub = muxer.GetSubscriber(); + var channel = (RedisChannel)Me(); + + var count = 0; + Log("Subscribing..."); + await sub.SubscribeAsync(channel, (channel, val) => + { + Interlocked.Increment(ref count); + Log("Message: " + val); + }, flags); + Assert.True(sub.IsConnected(channel)); + + Log("Publishing (1)..."); + Assert.Equal(0, count); + var publishedTo = await sub.PublishAsync(channel, "message1"); + // Client -> Redis -> Client -> handler takes just a moment + await UntilCondition(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); + Assert.Equal(1, count); + Log($" Published (1) to {publishedTo} subscriber(s)."); + + var endpoint = sub.SubscribedEndpoint(channel); + var subscribedServer = muxer.GetServer(endpoint); + var subscribedServerEndpoint = muxer.GetServerEndPoint(endpoint); + + Assert.True(subscribedServer.IsConnected, "subscribedServer.IsConnected"); + Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); + Assert.True(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + + Assert.True(muxer.TryGetSubscription(channel, out var subscription)); + var initialServer = subscription.GetCurrentServer(); + Assert.NotNull(initialServer); + Assert.True(initialServer.IsConnected); + Log($"Connected to: " + initialServer); + + muxer.AllowConnect = false; + subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.AllSubscription); + + Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); + Assert.False(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + + if (expectSuccess) + { + await UntilCondition(TimeSpan.FromSeconds(5), () => subscription.IsConnected); + Assert.True(subscription.IsConnected); + + var newServer = subscription.GetCurrentServer(); + Assert.NotNull(newServer); + Assert.NotEqual(newServer, initialServer); + Log($"Now connected to: " + newServer); + } + else + { + // This subscription shouldn't be able to reconnect by flags (demanding an unavailable server) + await UntilCondition(TimeSpan.FromSeconds(2), () => subscription.IsConnected); + Assert.False(subscription.IsConnected); + Log("Unable to reconnect (as expected)"); + + // Allow connecting back to the original + muxer.AllowConnect = true; + await UntilCondition(TimeSpan.FromSeconds(2), () => subscription.IsConnected); + Assert.True(subscription.IsConnected); + + var newServer = subscription.GetCurrentServer(); + Assert.NotNull(newServer); + Assert.Equal(newServer, initialServer); + Log($"Now connected to: " + newServer); + } + + + count = 0; + Log("Publishing (2)..."); + Assert.Equal(0, count); + publishedTo = await sub.PublishAsync(channel, "message2"); + // Client -> Redis -> Client -> handler takes just a moment + await UntilCondition(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); + Assert.Equal(1, count); + Log($" Published (2) to {publishedTo} subscriber(s)."); + + ClearAmbientFailures(); + } + } +} diff --git a/tests/StackExchange.Redis.Tests/Sentinel.cs b/tests/StackExchange.Redis.Tests/Sentinel.cs index 332c37877..fbe4e05e9 100644 --- a/tests/StackExchange.Redis.Tests/Sentinel.cs +++ b/tests/StackExchange.Redis.Tests/Sentinel.cs @@ -359,8 +359,11 @@ public async Task SentinelMastersAsyncTest() } [Fact] - public void SentinelReplicasTest() + public async Task SentinelReplicasTest() { + // Give previous test run a moment to reset when multi-framework failover is in play. + await UntilCondition(TimeSpan.FromSeconds(5), () => SentinelServerA.SentinelReplicas(ServiceName).Length > 0); + var replicaConfigs = SentinelServerA.SentinelReplicas(ServiceName); Assert.True(replicaConfigs.Length > 0, "Has replicaConfigs"); Assert.True(replicaConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); @@ -378,6 +381,9 @@ public void SentinelReplicasTest() [Fact] public async Task SentinelReplicasAsyncTest() { + // Give previous test run a moment to reset when multi-framework failover is in play. + await UntilCondition(TimeSpan.FromSeconds(5), () => SentinelServerA.SentinelReplicas(ServiceName).Length > 0); + var replicaConfigs = await SentinelServerA.SentinelReplicasAsync(ServiceName).ForAwait(); Assert.True(replicaConfigs.Length > 0, "Has replicaConfigs"); Assert.True(replicaConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 0cca4e5b7..dd04fa9da 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using StackExchange.Redis.Profiling; using StackExchange.Redis.Tests.Helpers; using Xunit; using Xunit.Abstractions; @@ -76,6 +77,16 @@ protected void Log(string message, params object[] args) } } + protected ProfiledCommandEnumerable Log(ProfilingSession session) + { + var profile = session.FinishProfiling(); + foreach (var command in profile) + { + Writer.WriteLineNoTime(command.ToString()); + } + return profile; + } + protected void CollectGarbage() { GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); diff --git a/tests/StackExchange.Redis.Tests/TestExtensions.cs b/tests/StackExchange.Redis.Tests/TestExtensions.cs new file mode 100644 index 000000000..b4c9707fd --- /dev/null +++ b/tests/StackExchange.Redis.Tests/TestExtensions.cs @@ -0,0 +1,15 @@ +using System; +using StackExchange.Redis.Profiling; + +namespace StackExchange.Redis.Tests +{ + public static class TestExtensions + { + public static ProfilingSession AddProfiler(this IConnectionMultiplexer mutex) + { + var session = new ProfilingSession(); + mutex.RegisterProfiler(() => session); + return session; + } + } +} From 090331e7137a806eac24ebeb6193dc55d51287e8 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sun, 6 Feb 2022 10:39:42 -0500 Subject: [PATCH 069/435] Migrates links to https:// everywhere possible and misc doc tweaks (#1975) - Code formatting - http:// -> https:// - Generally updating MDSN links that moved - Updates moved SSDB site - Adding Timeouts link (resolves #1942) Note: touches the license file but only to update to https:// URLs. --- LICENSE | 8 ++-- docs/Basics.md | 27 +++++------ docs/ExecSync.md | 2 +- docs/KeysValues.md | 6 +-- docs/PipelinesMultiplexers.md | 8 ++-- docs/ReleaseNotes.md | 2 +- docs/Scripting.md | 45 +++++++++---------- docs/ServerMaintenanceEvent.md | 2 +- docs/ThreadTheft.md | 5 +-- docs/Transactions.md | 12 ++--- docs/index.md | 7 +-- src/StackExchange.Redis/CommandMap.cs | 5 +-- .../3.0.503/redis.windows-service.conf | 6 +-- tests/RedisConfigs/3.0.503/redis.windows.conf | 6 +-- 14 files changed, 68 insertions(+), 73 deletions(-) diff --git a/LICENSE b/LICENSE index e32afb858..db4620c99 100644 --- a/LICENSE +++ b/LICENSE @@ -24,11 +24,11 @@ SOFTWARE. Third Party Licenses: -The Redis project (http://redis.io/) is independent of this client library, and +The Redis project (https://redis.io/) is independent of this client library, and is licensed separately under the three clause BSD license. The full license -information can be viewed here: http://redis.io/topics/license +information can be viewed here: https://redis.io/topics/license -This tool makes use of the "redis-doc" library from http://redis.io/documentation +This tool makes use of the "redis-doc" library from https://redis.io/documentation in the intellisense comments, which is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license; full details are available here: @@ -43,5 +43,5 @@ This tool is not used in the release binaries. The development solution uses the BookSleeve package from nuget (https://code.google.com/p/booksleeve/) by Marc Gravell. This is licensed under the Apache 2.0 license; full details are available here: -http://www.apache.org/licenses/LICENSE-2.0 +https://www.apache.org/licenses/LICENSE-2.0 This tool is not used in the release binaries. \ No newline at end of file diff --git a/docs/Basics.md b/docs/Basics.md index 860658458..eddcb98a7 100644 --- a/docs/Basics.md +++ b/docs/Basics.md @@ -23,7 +23,7 @@ If it finds both nodes are masters, a tie-breaker key can optionally be specifie Once you have a `ConnectionMultiplexer`, there are 3 main things you might want to do: - access a redis database (note that in the case of a cluster, a single logical database may be spread over multiple nodes) -- make use of the [pub/sub](http://redis.io/topics/pubsub) features of redis +- make use of the [pub/sub](https://redis.io/topics/pubsub) features of redis - access an individual server for maintenance / monitoring purposes Using a redis database @@ -43,7 +43,7 @@ object asyncState = ... IDatabase db = redis.GetDatabase(databaseNumber, asyncState); ``` -Once you have the `IDatabase`, it is simply a case of using the [redis API](http://redis.io/commands). Note that all methods have both synchronous and asynchronous implementations. In line with Microsoft's naming guidance, the asynchronous methods all end `...Async(...)`, and are fully `await`-able etc. +Once you have the `IDatabase`, it is simply a case of using the [redis API](https://redis.io/commands). Note that all methods have both synchronous and asynchronous implementations. In line with Microsoft's naming guidance, the asynchronous methods all end `...Async(...)`, and are fully `await`-able etc. The simplest operation would be to store and retrieve a value: @@ -55,7 +55,7 @@ string value = db.StringGet("mykey"); Console.WriteLine(value); // writes: "abcdefg" ``` -Note that the `String...` prefix here denotes the [String redis type](http://redis.io/topics/data-types), and is largely separate to the [.NET String type][3], although both can store text data. However, redis allows raw binary data for both keys and values - the usage is identical: +Note that the `String...` prefix here denotes the [String redis type](https://redis.io/topics/data-types), and is largely separate to the [.NET String type][3], although both can store text data. However, redis allows raw binary data for both keys and values - the usage is identical: ```csharp byte[] key = ..., value = ...; @@ -64,18 +64,18 @@ db.StringSet(key, value); byte[] value = db.StringGet(key); ``` -The entire range of [redis database commands](http://redis.io/commands) covering all redis data types is available for use. +The entire range of [redis database commands](https://redis.io/commands) covering all redis data types is available for use. Using redis pub/sub ---- -Another common use of redis is as a [pub/sub message](http://redis.io/topics/pubsub) distribution tool; this is also simple, and in the event of connection failure, the `ConnectionMultiplexer` will handle all the details of re-subscribing to the requested channels. +Another common use of redis is as a [pub/sub message](https://redis.io/topics/pubsub) distribution tool; this is also simple, and in the event of connection failure, the `ConnectionMultiplexer` will handle all the details of re-subscribing to the requested channels. ```csharp ISubscriber sub = redis.GetSubscriber(); ``` -Again, the object returned from `GetSubscriber` is a cheap pass-thru object that does not need to be stored. The pub/sub API has no concept of databases, but as before we can optionally provide an async-state. Note that all subscriptions are global: they are not scoped to the lifetime of the `ISubscriber` instance. The pub/sub features in redis use named "channels"; channels do not need to be defined in advance on the server (an interesting use here is things like per-user notification channels, which is what drives parts of the realtime updates on [Stack Overflow](http://stackoverflow.com)). As is common in .NET, subscriptions take the form of callback delegates which accept the channel-name and the message: +Again, the object returned from `GetSubscriber` is a cheap pass-thru object that does not need to be stored. The pub/sub API has no concept of databases, but as before we can optionally provide an async-state. Note that all subscriptions are global: they are not scoped to the lifetime of the `ISubscriber` instance. The pub/sub features in redis use named "channels"; channels do not need to be defined in advance on the server (an interesting use here is things like per-user notification channels, which is what drives parts of the realtime updates on [Stack Overflow](https://stackoverflow.com)). As is common in .NET, subscriptions take the form of callback delegates which accept the channel-name and the message: ```csharp sub.Subscribe("messages", (channel, message) => { @@ -118,13 +118,13 @@ For maintenance purposes, it is sometimes necessary to issue server-specific com IServer server = redis.GetServer("localhost", 6379); ``` -The `GetServer` method will accept an [`EndPoint`](http://msdn.microsoft.com/en-us/library/system.net.endpoint(v=vs.110).aspx) or the name/value pair that uniquely identify the server. As before, the object returned from `GetServer` is a cheap pass-thru object that does not need to be stored, and async-state can be optionally specified. Note that the set of available endpoints is also available: +The `GetServer` method will accept an [`EndPoint`](https://docs.microsoft.com/en-us/dotnet/api/system.net.endpoint) or the name/value pair that uniquely identify the server. As before, the object returned from `GetServer` is a cheap pass-thru object that does not need to be stored, and async-state can be optionally specified. Note that the set of available endpoints is also available: ```csharp EndPoint[] endpoints = redis.GetEndPoints(); ``` -From the `IServer` instance, the [Server commands](http://redis.io/commands#server) are available; for example: +From the `IServer` instance, the [Server commands](https://redis.io/commands#server) are available; for example: ```csharp DateTime lastSave = server.LastSave(); @@ -139,7 +139,7 @@ There are 3 primary usage mechanisms with StackExchange.Redis: - Synchronous - where the operation completes before the methods returns to the caller (note that while this may block the caller, it absolutely **does not** block other threads: the key idea in StackExchange.Redis is that it aggressively shares the connection between concurrent callers) - Asynchronous - where the operation completes some time in the future, and a `Task` or `Task` is returned immediately, which can later: - be `.Wait()`ed (blocking the current thread until the response is available) - - have a continuation callback added ([`ContinueWith`](http://msdn.microsoft.com/en-us/library/system.threading.tasks.task.continuewith(v=vs.110).aspx) in the TPL) + - have a continuation callback added ([`ContinueWith`](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.continuewith) in the TPL) - be *awaited* (which is a language-level feature that simplifies the latter, while also continuing immediately if the reply is already known) - Fire-and-Forget - where you really aren't interested in the reply, and are happy to continue irrespective of the response @@ -161,9 +161,6 @@ The fire-and-forget usage is accessed by the optional `CommandFlags flags` param db.StringIncrement(pageKey, flags: CommandFlags.FireAndForget); ``` - - - - [1]: http://msdn.microsoft.com/en-us/library/dd460717%28v=vs.110%29.aspx - [2]: http://msdn.microsoft.com/en-us/library/system.threading.tasks.task.asyncstate(v=vs.110).aspx - [3]: http://msdn.microsoft.com/en-us/library/system.string(v=vs.110).aspx + [1]: https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-parallel-library-tpl + [2]: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.asyncstate + [3]: https://docs.microsoft.com/en-us/dotnet/api/system.string diff --git a/docs/ExecSync.md b/docs/ExecSync.md index 2b3409fa2..e4a09ec95 100644 --- a/docs/ExecSync.md +++ b/docs/ExecSync.md @@ -1,5 +1,5 @@ The Dangers of Synchronous Continuations === -Once, there was more content here; then [a suitably evil workaround was found](http://stackoverflow.com/a/22588431/23354). This page is not +Once, there was more content here; then [a suitably evil workaround was found](https://stackoverflow.com/a/22588431/23354). This page is not listed in the index, but remains for your curiosity. \ No newline at end of file diff --git a/docs/KeysValues.md b/docs/KeysValues.md index 0860ef37c..0a414ce21 100644 --- a/docs/KeysValues.md +++ b/docs/KeysValues.md @@ -1,9 +1,9 @@ Keys, Values and Channels === -In dealing with redis, there is quite an important distinction between *keys* and *everything else*. A key is the unique name of a piece of data (which could be a String, a List, Hash, or any of the other [redis data types](http://redis.io/topics/data-types)) within a database. Keys are never interpreted as... well, anything: they are simply inert names. Further - when dealing with clustered or sharded systems, it is the key that defines the node (or nodes if there are replicas) that contain this data - so keys are crucial for routing commands. +In dealing with redis, there is quite an important distinction between *keys* and *everything else*. A key is the unique name of a piece of data (which could be a String, a List, Hash, or any of the other [redis data types](https://redis.io/topics/data-types)) within a database. Keys are never interpreted as... well, anything: they are simply inert names. Further - when dealing with clustered or sharded systems, it is the key that defines the node (or nodes if there are replicas) that contain this data - so keys are crucial for routing commands. -This contrasts with *values*; values are the *things that you store* against keys - either individually (for String data) or as groups. Values do not affect command routing (caveat: except for [the `SORT` command](http://redis.io/commands/sort) when `BY` or `GET` is specified, but that is *really* complicated to explain). Likewise, values are often *interpreted* by redis for the purposes of an operation: +This contrasts with *values*; values are the *things that you store* against keys - either individually (for String data) or as groups. Values do not affect command routing (caveat: except for [the `SORT` command](https://redis.io/commands/sort) when `BY` or `GET` is specified, but that is *really* complicated to explain). Likewise, values are often *interpreted* by redis for the purposes of an operation: - `incr` (and the various similar commands) interpret String values as numeric data - sorting can interpret values using either numeric or unicode rules @@ -90,7 +90,7 @@ Channel names for pub/sub are represented by the `RedisChannel` type; this is la Scripting --- -[Lua scripting in redis](http://redis.io/commands/EVAL) has two notable features: +[Lua scripting in redis](https://redis.io/commands/EVAL) has two notable features: - the inputs must keep keys and values separate (which inside the script become `KEYS` and `ARGV`, respectively) - the return format is not defined in advance: it is specific to your script diff --git a/docs/PipelinesMultiplexers.md b/docs/PipelinesMultiplexers.md index aa47b2a50..b1711531f 100644 --- a/docs/PipelinesMultiplexers.md +++ b/docs/PipelinesMultiplexers.md @@ -69,7 +69,7 @@ Multiplexing Pipelining is all well and good, but often any single block of code only wants a single value (or maybe wants to perform a few operations, but which depend on each-other). This means that we still have the problem that we spend most of our time waiting for data to transfer between client and server. Now consider a busy application, perhaps a web-server. Such applications are generally inherently concurrent, so if you have 20 parallel application requests all requiring data, you might think of spinning up 20 connections, or you could synchronize access to a single connection (which would mean the last caller would need to wait for the latency of all the other 19 before it even got started). Or as a compromise, perhaps a pool of 5 connections which are leased - no matter how you are doing it, there is going to be a lot of waiting. **StackExchange.Redis does not do this**; instead, it does a *lot* of work for you to make effective use of all this idle time by *multiplexing* a single connection. When used concurrently by different callers, it **automatically pipelines the separate requests**, so regardless of whether the requests use blocking or asynchronous access, the work is all pipelined. So we could have 10 or 20 of our "get a and b" scenario from earlier (from different application requests), and they would all get onto the connection as soon as possible. Essentially, it fills the `waiting` time with work from other callers. -For this reason, the only redis features that StackExchange.Redis does not offer (and *will not ever offer*) are the "blocking pops" ([BLPOP](http://redis.io/commands/blpop), [BRPOP](http://redis.io/commands/brpop) and [BRPOPLPUSH](http://redis.io/commands/brpoplpush)) - because this would allow a single caller to stall the entire multiplexer, blocking all other callers. The only other time that StackExchange.Redis needs to hold work is when verifying pre-conditions for a transaction, which is why StackExchange.Redis encapsulates such conditions into internally managed `Condition` instances. [Read more about transactions here](Transactions). If you feel you want "blocking pops", then I strongly suggest you consider pub/sub instead: +For this reason, the only redis features that StackExchange.Redis does not offer (and *will not ever offer*) are the "blocking pops" ([BLPOP](https://redis.io/commands/blpop), [BRPOP](https://redis.io/commands/brpop) and [BRPOPLPUSH](https://redis.io/commands/brpoplpush)) - because this would allow a single caller to stall the entire multiplexer, blocking all other callers. The only other time that StackExchange.Redis needs to hold work is when verifying pre-conditions for a transaction, which is why StackExchange.Redis encapsulates such conditions into internally managed `Condition` instances. [Read more about transactions here](Transactions). If you feel you want "blocking pops", then I strongly suggest you consider pub/sub instead: ```csharp sub.Subscribe(channel, delegate { @@ -105,6 +105,6 @@ if (value == null) { return value; ``` - [1]: http://msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx - [2]: http://msdn.microsoft.com/en-us/library/system.threading.tasks.task(v=vs.110).aspx - [3]: http://msdn.microsoft.com/en-us/library/dd321424(v=vs.110).aspx + [1]: https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-parallel-library-tpl + [2]: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task + [3]: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1 diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 1dadce43a..64b26cac4 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -241,7 +241,7 @@ plans to release `1.2.7`. - add: track message status in exceptions (#576) - add: `GetDatabase()` optimization for DB 0 and low numbered databases: `IDatabase` instance is retained and recycled (as long as no `asyncState` is provided) - improved connection retry policy (#510, #572) -- add `Execute`/`ExecuteAsync` API to support "modules"; [more info](http://blog.marcgravell.com/2017/04/stackexchangeredis-and-redis-40-modules.html) +- add `Execute`/`ExecuteAsync` API to support "modules"; [more info](https://blog.marcgravell.com/2017/04/stackexchangeredis-and-redis-40-modules.html) - fix: timeout link fixed re /docs change (below) - [`NRediSearch`](https://www.nuget.org/packages/NRediSearch/) added as exploration into "modules" diff --git a/docs/Scripting.md b/docs/Scripting.md index 5f4e38bbd..56af7afc1 100644 --- a/docs/Scripting.md +++ b/docs/Scripting.md @@ -1,24 +1,23 @@ Scripting === -Basic [Lua scripting](http://redis.io/commands/EVAL) is supported by the `IServer.ScriptLoad(Async)`, `IServer.ScriptExists(Async)`, `IServer.ScriptFlush(Async)`, `IDatabase.ScriptEvaluate`, and `IDatabaseAsync.ScriptEvaluateAsync` methods. +Basic [Lua scripting](https://redis.io/commands/EVAL) is supported by the `IServer.ScriptLoad(Async)`, `IServer.ScriptExists(Async)`, `IServer.ScriptFlush(Async)`, `IDatabase.ScriptEvaluate`, and `IDatabaseAsync.ScriptEvaluateAsync` methods. These methods expose the basic commands necessary to submit and execute Lua scripts to redis. -More sophisticated scripting is available through the `LuaScript` class. The `LuaScript` class makes it simpler to prepare and submit parameters along with a script, as well as allowing you to use -cleaner variables names. +More sophisticated scripting is available through the `LuaScript` class. The `LuaScript` class makes it simpler to prepare and submit parameters along with a script, as well as allowing you to use cleaner variables names. An example use of the `LuaScript`: -``` - const string Script = "redis.call('set', @key, @value)"; +```csharp +const string Script = "redis.call('set', @key, @value)"; - using (ConnectionMultiplexer conn = /* init code */) - { - var db = conn.GetDatabase(0); +using (ConnectionMultiplexer conn = /* init code */) +{ + var db = conn.GetDatabase(0); - var prepared = LuaScript.Prepare(Script); - db.ScriptEvaluate(prepared, new { key = (RedisKey)"mykey", value = 123 }); - } + var prepared = LuaScript.Prepare(Script); + db.ScriptEvaluate(prepared, new { key = (RedisKey)"mykey", value = 123 }); +} ``` The `LuaScript` class rewrites variables in scripts of the form `@myVar` into the appropriate `ARGV[someIndex]` required by redis. If the @@ -36,25 +35,25 @@ Any object that exposes field or property members with the same name as @-prefix - RedisKey - RedisValue -StackExchange.Redis handles Lua script caching internally. It automatically transmits the Lua script to redis on the first call to 'ScriptEvaluate'. For further calls of the same script only the hash with [`EVALSHA`](http://redis.io/commands/evalsha) is used. +StackExchange.Redis handles Lua script caching internally. It automatically transmits the Lua script to redis on the first call to 'ScriptEvaluate'. For further calls of the same script only the hash with [`EVALSHA`](https://redis.io/commands/evalsha) is used. For more control of the Lua script transmission to redis, `LuaScript` objects can be converted into `LoadedLuaScript`s via `LuaScript.Load(IServer)`. -`LoadedLuaScripts` are evaluated with the [`EVALSHA`](http://redis.io/commands/evalsha), and referred to by hash. +`LoadedLuaScripts` are evaluated with the [`EVALSHA`](https://redis.io/commands/evalsha), and referred to by hash. An example use of `LoadedLuaScript`: -``` - const string Script = "redis.call('set', @key, @value)"; +```csharp +const string Script = "redis.call('set', @key, @value)"; - using (ConnectionMultiplexer conn = /* init code */) - { - var db = conn.GetDatabase(0); - var server = conn.GetServer(/* appropriate parameters*/); +using (ConnectionMultiplexer conn = /* init code */) +{ + var db = conn.GetDatabase(0); + var server = conn.GetServer(/* appropriate parameters*/); - var prepared = LuaScript.Prepare(Script); - var loaded = prepared.Load(server); - loaded.Evaluate(db, new { key = (RedisKey)"mykey", value = 123 }); - } + var prepared = LuaScript.Prepare(Script); + var loaded = prepared.Load(server); + loaded.Evaluate(db, new { key = (RedisKey)"mykey", value = 123 }); +} ``` All methods on both `LuaScript` and `LoadedLuaScript` have Async alternatives, and expose the actual script submitted to redis as the `ExecutableScript` property. diff --git a/docs/ServerMaintenanceEvent.md b/docs/ServerMaintenanceEvent.md index dbc3528ad..2f4ba1c29 100644 --- a/docs/ServerMaintenanceEvent.md +++ b/docs/ServerMaintenanceEvent.md @@ -17,7 +17,7 @@ Azure Cache for Redis currently sends the following notifications: The library will automatically subscribe to the pub/sub channel to receive notifications from the server, if one exists. For Azure Redis caches, this is the 'AzureRedisEvents' channel. To plug in your maintenance handling logic, you can pass in an event handler via the `ServerMaintenanceEvent` event on your `ConnectionMultiplexer`. For example: -``` +```csharp multiplexer.ServerMaintenanceEvent += (object sender, ServerMaintenanceEvent e) => { if (e is AzureMaintenanceEvent azureEvent && azureEvent.NotificationType == AzureNotificationType.NodeMaintenanceStart) diff --git a/docs/ThreadTheft.md b/docs/ThreadTheft.md index 339fac554..6f6581bf2 100644 --- a/docs/ThreadTheft.md +++ b/docs/ThreadTheft.md @@ -3,7 +3,7 @@ If you're here because you followed a link in an exception and you just want your code to work, the short version is: try adding the following *early on* in your application startup: -``` c# +```csharp ConnectionMultiplexer.SetFeatureFlag("preventthreadtheft", true); ``` @@ -40,14 +40,13 @@ be an asynchronous dispatch API. But... not all implementations are equal. Some in particular of `LegacyAspNetSynchronizationContext`, which is what you get if you configure ASP.NET with: - ``` xml ``` or -``` +```xml ``` diff --git a/docs/Transactions.md b/docs/Transactions.md index 8752b26ea..4d8deca27 100644 --- a/docs/Transactions.md +++ b/docs/Transactions.md @@ -1,7 +1,7 @@ Transactions in Redis ===================== -Transactions in Redis are not like transactions in, say a SQL database. The [full documentation is here](http://redis.io/topics/transactions), +Transactions in Redis are not like transactions in, say a SQL database. The [full documentation is here](https://redis.io/topics/transactions), but to paraphrase: A transaction in redis consists of a block of commands placed between `MULTI` and `EXEC` (or `DISCARD` for rollback). Once a `MULTI` @@ -41,14 +41,14 @@ you *can* do is: `WATCH` a key, check data from that key in the normal way, then If, when you check the data, you discover that you don't actually need the transaction, you can use `UNWATCH` to forget all the watched keys. Note that watched keys are also reset during `EXEC` and `DISCARD`. So *at the Redis layer*, this is conceptually: -``` +```lua WATCH {custKey} HEXISTS {custKey} "UniqueId" -(check the reply, then either:) +-- (check the reply, then either:) MULTI HSET {custKey} "UniqueId" {newId} EXEC -(or, if we find there was already an unique-id:) +-- (or, if we find there was already an unique-id:) UNWATCH ``` @@ -98,13 +98,13 @@ bool wasSet = db.HashSet(custKey, "UniqueID", newId, When.NotExists); Lua --- -You should also keep in mind that Redis 2.6 and above [support Lua scripting](http://redis.io/commands/EVAL), a versatile tool for performing multiple operations as a single atomic unit at the server. +You should also keep in mind that Redis 2.6 and above [support Lua scripting](https://redis.io/commands/EVAL), a versatile tool for performing multiple operations as a single atomic unit at the server. Since no other connections are serviced during a Lua script it behaves much like a transaction, but without the complexity of `MULTI` / `EXEC` etc. This also avoids issues such as bandwidth and latency between the caller and the server, but the trade-off is that it monopolises the server for the duration of the script. At the Redis layer (and assuming `HSETNX` did not exist) this could be implemented as: -``` +```lua EVAL "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end" 1 {custKey} {newId} ``` diff --git a/docs/index.md b/docs/index.md index 5a12d24d2..a57c3134f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,8 +6,8 @@ StackExchange.Redis ## Overview StackExchange.Redis is a high performance general purpose redis client for .NET languages (C#, etc.). It is the logical successor to [BookSleeve](https://code.google.com/archive/p/booksleeve/), -and is the client developed-by (and used-by) [Stack Exchange](http://stackexchange.com/) for busy sites like [Stack Overflow](http://stackoverflow.com/). For the full reasons -why this library was created (i.e. "What about BookSleeve?") [please see here](http://marcgravell.blogspot.com/2014/03/so-i-went-and-wrote-another-redis-client.html). +and is the client developed-by (and used-by) [Stack Exchange](https://stackexchange.com/) for busy sites like [Stack Overflow](https://stackoverflow.com/). For the full reasons +why this library was created (i.e. "What about BookSleeve?") [please see here](https://marcgravell.blogspot.com/2014/03/so-i-went-and-wrote-another-redis-client.html). Features -- @@ -44,6 +44,7 @@ Documentation - [Profiling](Profiling) - profiling interfaces, as well as how to profile in an `async` world - [Scripting](Scripting) - running Lua scripts with convenient named parameter replacement - [Testing](Testing) - running the `StackExchange.Redis.Tests` suite to validate changes +- [Timeouts](Timeouts) - guidance on dealing with timeout problems - [Thread Theft](ThreadTheft) - guidance on avoiding TPL threading problems Questions and Contributions @@ -51,5 +52,5 @@ Questions and Contributions If you think you have found a bug or have a feature request, please [report an issue][2], or if appropriate: submit a pull request. If you have a question, feel free to [contact me](https://github.com/mgravell). - [1]: http://msdn.microsoft.com/en-us/library/dd460717%28v=vs.110%29.aspx + [1]: https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-parallel-library-tpl [2]: https://github.com/StackExchange/StackExchange.Redis/issues?state=open diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index 0bfd6de3e..ae0ea8eaf 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -54,11 +54,10 @@ internal CommandMap(CommandBytes[] map) }); /// - /// The commands available to http://www.ideawu.com/ssdb/ + /// The commands available to https://ssdb.io/ /// - /// http://www.ideawu.com/ssdb/docs/redis-to-ssdb.html + /// https://ssdb.io/docs/redis-to-ssdb.html public static CommandMap SSDB { get; } = Create(new HashSet { - // see http://www.ideawu.com/ssdb/docs/redis-to-ssdb.html "ping", "get", "set", "del", "incr", "incrby", "mget", "mset", "keys", "getset", "setnx", "hget", "hset", "hdel", "hincrby", "hkeys", "hvals", "hmget", "hmset", "hlen", diff --git a/tests/RedisConfigs/3.0.503/redis.windows-service.conf b/tests/RedisConfigs/3.0.503/redis.windows-service.conf index 16f41ee5a..ed44371a3 100644 --- a/tests/RedisConfigs/3.0.503/redis.windows-service.conf +++ b/tests/RedisConfigs/3.0.503/redis.windows-service.conf @@ -516,7 +516,7 @@ slave-priority 100 # If the AOF is enabled on startup Redis will load the AOF, that is the file # with the better durability guarantees. # -# Please check http://redis.io/topics/persistence for more information. +# Please check https://redis.io/topics/persistence for more information. appendonly no @@ -738,7 +738,7 @@ lua-time-limit 5000 # cluster-require-full-coverage yes # In order to setup your cluster make sure to read the documentation -# available at http://redis.io web site. +# available at https://redis.io web site. ################################## SLOW LOG ################################### @@ -788,7 +788,7 @@ latency-monitor-threshold 0 ############################# Event notification ############################## # Redis can notify Pub/Sub clients about events happening in the key space. -# This feature is documented at http://redis.io/topics/notifications +# This feature is documented at https://redis.io/topics/notifications # # For instance if keyspace events notification is enabled, and a client # performs a DEL operation on key "foo" stored in the Database 0, two diff --git a/tests/RedisConfigs/3.0.503/redis.windows.conf b/tests/RedisConfigs/3.0.503/redis.windows.conf index 21915cce1..c07a7e9ab 100644 --- a/tests/RedisConfigs/3.0.503/redis.windows.conf +++ b/tests/RedisConfigs/3.0.503/redis.windows.conf @@ -516,7 +516,7 @@ slave-priority 100 # If the AOF is enabled on startup Redis will load the AOF, that is the file # with the better durability guarantees. # -# Please check http://redis.io/topics/persistence for more information. +# Please check https://redis.io/topics/persistence for more information. appendonly no @@ -738,7 +738,7 @@ lua-time-limit 5000 # cluster-require-full-coverage yes # In order to setup your cluster make sure to read the documentation -# available at http://redis.io web site. +# available at https://redis.io web site. ################################## SLOW LOG ################################### @@ -788,7 +788,7 @@ latency-monitor-threshold 0 ############################# Event notification ############################## # Redis can notify Pub/Sub clients about events happening in the key space. -# This feature is documented at http://redis.io/topics/notifications +# This feature is documented at https://redis.io/topics/notifications # # For instance if keyspace events notification is enabled, and a client # performs a DEL operation on key "foo" stored in the Database 0, two From 7dda23a036892a53cba6781a413ee0efb2d304fd Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 7 Feb 2022 11:27:52 -0500 Subject: [PATCH 070/435] v2.5 work: BacklogPolicy (#1912) This is a work in progress of the Backlog Policy bits to help address #1864. The overall change here is to make the message pathway backlog commands when an endpoint is down. It does not hand off to another viable endpoint in this iteration (future plans there). If the policy is set to `FailFast`, we should get the old behavior not queueing anything. In the new path: - Handshake commands go straight to the IO pipe (the only way this would fail is if we lost a pipe _during a handshake_, in which case things are ill and let's backoff until the next heartbeat anyway). - If connected and there is no backlog queue, commands go into the pipe - If disconnected or there is a backlog queue, non-internal and non-handshake commands go into the backlog, so that they are played to the pipe in order TODO List: - [x] Implement simplified backlog queue (3 queues -> 1, no handoffs) - [x] Implement backlog policy and config - [x] Add initial testing - [x] Find intermittent async hang in local test runner (test: `QueuesAndFlushesAfterReconnecting`) - this may be boneheaded or a fundamental flaw - [x] Add test for sync commands (e.g. `.Ping()`) and that they recover appropriately Co-authored-by: mgravell --- .github/workflows/CI.yml | 28 +- appveyor.yml | 2 - src/StackExchange.Redis/BacklogPolicy.cs | 43 ++ .../ConfigurationOptions.cs | 12 + .../ConnectionMultiplexer.cs | 14 +- src/StackExchange.Redis/Enums/CommandFlags.cs | 2 +- src/StackExchange.Redis/Message.cs | 37 +- src/StackExchange.Redis/PhysicalBridge.cs | 360 ++++++++++------ src/StackExchange.Redis/PhysicalConnection.cs | 8 +- src/StackExchange.Redis/RedisServer.cs | 25 +- src/StackExchange.Redis/ServerEndPoint.cs | 14 +- .../ServerSelectionStrategy.cs | 30 +- tests/StackExchange.Redis.Tests/AsyncTests.cs | 2 +- .../StackExchange.Redis.Tests/BacklogTests.cs | 402 ++++++++++++++++++ .../ConnectFailTimeout.cs | 2 +- .../ConnectingFailDetection.cs | 3 +- .../ConnectionFailedErrors.cs | 1 + .../ExceptionFactoryTests.cs | 3 +- tests/StackExchange.Redis.Tests/PubSub.cs | 7 +- .../PubSubMultiserver.cs | 2 +- tests/StackExchange.Redis.Tests/Secure.cs | 1 + .../SharedConnectionFixture.cs | 16 +- .../StackExchange.Redis.Tests.csproj | 2 +- tests/StackExchange.Redis.Tests/TestBase.cs | 7 +- 24 files changed, 812 insertions(+), 211 deletions(-) create mode 100644 src/StackExchange.Redis/BacklogPolicy.cs create mode 100644 tests/StackExchange.Redis.Tests/BacklogTests.cs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6d2c0e8d6..44bd09eb6 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,4 +1,4 @@ -name: CI Builds +name: CI on: pull_request: @@ -16,11 +16,10 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v1 - - name: Setup .NET Core + - name: Install .NET SDK uses: actions/setup-dotnet@v1 with: dotnet-version: | - 3.1.x 6.0.x - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true @@ -33,24 +32,25 @@ jobs: continue-on-error: true if: success() || failure() with: - name: StackExchange.Redis.Tests (Ubuntu) - Results + name: Test Results - Ubuntu path: 'test-results/*.trx' reporter: dotnet-trx - name: .NET Lib Pack run: dotnet pack src/StackExchange.Redis/StackExchange.Redis.csproj --no-build -c Release /p:Packing=true /p:PackageOutputPath=%CD%\.nupkgs /p:CI=true windows: - name: StackExchange.Redis (Windows Server 2019) - runs-on: windows-2019 + name: StackExchange.Redis (Windows Server 2022) + runs-on: windows-2022 + env: + NUGET_CERT_REVOCATION_MODE: offline # Disabling signing because of massive perf hit, see https://github.com/NuGet/Home/issues/11548 steps: - name: Checkout code uses: actions/checkout@v1 - - name: Setup .NET Core 3.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: | - 3.1.x - 6.0.x + # - name: Install .NET SDK + # uses: actions/setup-dotnet@v1 + # with: + # dotnet-version: | + # 6.0.x - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true - name: Start Redis Services (v3.0.503) @@ -79,6 +79,6 @@ jobs: continue-on-error: true if: success() || failure() with: - name: StackExchange.Redis.Tests (Windows Server 2019) - Results + name: Tests Results - Windows Server 2022 path: 'test-results/*.trx' - reporter: dotnet-trx + reporter: dotnet-trx diff --git a/appveyor.yml b/appveyor.yml index 7387352eb..a2107f48c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,8 +6,6 @@ init: install: - cmd: >- - choco install dotnet-sdk --version 5.0.404 - choco install dotnet-sdk --version 6.0.101 cd tests\RedisConfigs\3.0.503 diff --git a/src/StackExchange.Redis/BacklogPolicy.cs b/src/StackExchange.Redis/BacklogPolicy.cs new file mode 100644 index 000000000..4fb9e67c7 --- /dev/null +++ b/src/StackExchange.Redis/BacklogPolicy.cs @@ -0,0 +1,43 @@ +namespace StackExchange.Redis +{ + /// + /// The backlog policy to use for commands. This policy comes into effect when a connection is unhealthy or unavailable. + /// The policy can choose to backlog commands and wait to try them (within their timeout) against a connection when it comes up, + /// or it could choose to fail fast and throw ASAP. Different apps desire different behaviors with backpressure and how to handle + /// large amounts of load, so this is configurable to optimize the happy path but avoid spiral-of-death queue scenarios for others. + /// + public sealed class BacklogPolicy + { + /// + /// Backlog behavior matching StackExchange.Redis's 2.x line, failing fast and not attempting to queue + /// and retry when a connection is available again. + /// + public static BacklogPolicy FailFast { get; } = new() + { + QueueWhileDisconnected = false, + AbortPendingOnConnectionFailure = true, + }; + + /// + /// Default backlog policy which will allow commands to be issues against an endpoint and queue up. + /// Commands are still subject to their async timeout (which serves as a queue size check). + /// + public static BacklogPolicy Default { get; } = new() + { + QueueWhileDisconnected = true, + AbortPendingOnConnectionFailure = false, + }; + + /// + /// Whether to queue commands while disconnected. + /// True means queue for attempts up until their timeout. + /// False means to fail ASAP and queue nothing. + /// + public bool QueueWhileDisconnected { get; init; } + + /// + /// Whether to immediately abandon (with an exception) all pending commands when a connection goes unhealthy. + /// + public bool AbortPendingOnConnectionFailure { get; init; } + } +} diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 138096120..6914abc1a 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -146,6 +146,8 @@ public static string TryNormalize(string value) private IReconnectRetryPolicy reconnectRetryPolicy; + private BacklogPolicy backlogPolicy; + /// /// A LocalCertificateSelectionCallback delegate responsible for selecting the certificate used for authentication; note /// that this cannot be specified in the configuration-string. @@ -372,6 +374,15 @@ public IReconnectRetryPolicy ReconnectRetryPolicy set => reconnectRetryPolicy = value; } + /// + /// The backlog policy to be used for commands when a connection is unhealthy. + /// + public BacklogPolicy BacklogPolicy + { + get => backlogPolicy ?? BacklogPolicy.Default; + set => backlogPolicy = value; + } + /// /// Indicates whether endpoints should be resolved via DNS before connecting. /// If enabled the ConnectionMultiplexer will not re-resolve DNS @@ -541,6 +552,7 @@ public ConfigurationOptions Clone() responseTimeout = responseTimeout, DefaultDatabase = DefaultDatabase, ReconnectRetryPolicy = reconnectRetryPolicy, + BacklogPolicy = backlogPolicy, SslProtocols = SslProtocols, checkCertificateRevocation = checkCertificateRevocation, }; diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 13eebbee3..12959c51e 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -804,7 +804,7 @@ internal void OnHashSlotMoved(int hashSlot, EndPoint old, EndPoint @new) /// The key to get a hash slot ID for. public int HashSlot(RedisKey key) => ServerSelectionStrategy.HashSlot(key); - internal ServerEndPoint AnyConnected(ServerType serverType, uint startOffset, RedisCommand command, CommandFlags flags) + internal ServerEndPoint AnyServer(ServerType serverType, uint startOffset, RedisCommand command, CommandFlags flags, bool allowDisconnected) { var tmp = GetServerSnapshot(); int len = tmp.Length; @@ -812,7 +812,7 @@ internal ServerEndPoint AnyConnected(ServerType serverType, uint startOffset, Re for (int i = 0; i < len; i++) { var server = tmp[(int)(((uint)i + startOffset) % len)]; - if (server != null && server.ServerType == serverType && server.IsSelectable(command)) + if (server != null && server.ServerType == serverType && server.IsSelectable(command, allowDisconnected)) { if (server.IsReplica) { @@ -2169,6 +2169,12 @@ private bool PrepareToPushMessageToBridge(Message message, ResultProcessor { // Infer a server automatically server = SelectServer(message); + + // If we didn't find one successfully, and we're allowed, queue for any viable server + if (server == null && message != null && RawConfig.BacklogPolicy.QueueWhileDisconnected) + { + server = ServerSelectionStrategy.Select(message, allowDisconnected: true); + } } else // a server was specified; do we trust their choice, though? { @@ -2186,7 +2192,9 @@ private bool PrepareToPushMessageToBridge(Message message, ResultProcessor } break; } - if (!server.IsConnected) + + // If we're not allowed to queue while disconnected, we'll bomb out below. + if (!server.IsConnected && !RawConfig.BacklogPolicy.QueueWhileDisconnected) { // well, that's no use! server = null; diff --git a/src/StackExchange.Redis/Enums/CommandFlags.cs b/src/StackExchange.Redis/Enums/CommandFlags.cs index c1efc65c1..119fe22bb 100644 --- a/src/StackExchange.Redis/Enums/CommandFlags.cs +++ b/src/StackExchange.Redis/Enums/CommandFlags.cs @@ -81,7 +81,7 @@ public enum CommandFlags /// NoScriptCache = 512, - // 1024: used for timed-out; never user-specified, so not visible on the public API + // 1024: Removed - was used for async timeout checks; never user-specified, so not visible on the public API // 2048: Use subscription connection type; never user-specified, so not visible on the public API } diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 708e4600f..84bbcd6d0 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading; -using System.Threading.Tasks; using StackExchange.Redis.Profiling; using static StackExchange.Redis.ConnectionMultiplexer; @@ -60,7 +58,6 @@ internal abstract class Message : ICompletable private const CommandFlags AskingFlag = (CommandFlags)32, ScriptUnavailableFlag = (CommandFlags)256, - NeedsAsyncTimeoutCheckFlag = (CommandFlags)1024, DemandSubscriptionConnection = (CommandFlags)2048; private const CommandFlags MaskMasterServerPreference = CommandFlags.DemandMaster @@ -645,10 +642,7 @@ internal void SetRequestSent() [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void SetWriteTime() { - if ((Flags & NeedsAsyncTimeoutCheckFlag) != 0) - { - _writeTickCount = Environment.TickCount; // note this might be reset if we resend a message, cluster-moved etc; I'm OK with that - } + _writeTickCount = Environment.TickCount; // note this might be reset if we resend a message, cluster-moved etc; I'm OK with that } private int _writeTickCount; public int GetWriteTime() => Volatile.Read(ref _writeTickCount); @@ -662,21 +656,17 @@ internal void SetWriteTime() /// internal void SetForSubscriptionBridge() => Flags |= DemandSubscriptionConnection; - private void SetNeedsTimeoutCheck() => Flags |= NeedsAsyncTimeoutCheckFlag; - internal bool HasAsyncTimedOut(int now, int timeoutMilliseconds, out int millisecondsTaken) + /// + /// Checks if this message has violated the provided timeout. + /// Whether it's a sync operation in a .Wait() or in the backlog queue or written/pending asynchronously, we need to timeout everything. + /// ...or we get indefinite Task hangs for completions. + /// + internal bool HasTimedOut(int now, int timeoutMilliseconds, out int millisecondsTaken) { - if ((Flags & NeedsAsyncTimeoutCheckFlag) != 0) - { - millisecondsTaken = unchecked(now - _writeTickCount); // note: we can't just check "if sent < cutoff" because of wrap-aro - if (millisecondsTaken >= timeoutMilliseconds) - { - Flags &= ~NeedsAsyncTimeoutCheckFlag; // note: we don't remove it from the queue - still might need to marry it up; but: it is toast - return true; - } - } - else + millisecondsTaken = unchecked(now - _writeTickCount); // note: we can't just check "if sent < cutoff" because of wrap-around + if (millisecondsTaken >= timeoutMilliseconds) { - millisecondsTaken = default; + return true; } return false; } @@ -695,16 +685,17 @@ internal void SetPreferMaster() => internal void SetPreferReplica() => Flags = (Flags & ~MaskMasterServerPreference) | CommandFlags.PreferReplica; + /// + /// Note order here reversed to prevent overload resolution errors + /// internal void SetSource(ResultProcessor resultProcessor, IResultBox resultBox) - { // note order here reversed to prevent overload resolution errors - if (resultBox != null && resultBox.IsAsync) SetNeedsTimeoutCheck(); + { this.resultBox = resultBox; this.resultProcessor = resultProcessor; } internal void SetSource(IResultBox resultBox, ResultProcessor resultProcessor) { - if (resultBox != null && resultBox.IsAsync) SetNeedsTimeoutCheck(); this.resultBox = resultBox; this.resultProcessor = resultProcessor; } diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 1a88cc8ea..025702fa7 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -13,7 +13,6 @@ using static Pipelines.Sockets.Unofficial.Threading.MutexSlim; #endif - namespace StackExchange.Redis { internal sealed class PhysicalBridge : IDisposable @@ -28,8 +27,20 @@ internal sealed class PhysicalBridge : IDisposable private readonly long[] profileLog = new long[ProfileLogSamples]; - private readonly ConcurrentQueue _backlog = new ConcurrentQueue(); + /// + /// We have 1 queue in play on this bridge. + /// We're bypassing the queue for handshake events that go straight to the socket. + /// Everything else that's not an internal call goes into the queue if there is a queue. + /// + /// In a later release we want to remove per-server events from this queue completely and shunt queued messages + /// to another capable primary connection if one is available to process them faster (order is already hosed). + /// For now, simplicity in: queue it all, replay or timeout it all. + /// + private readonly ConcurrentQueue _backlog = new(); + private bool BacklogHasItems => !_backlog.IsEmpty; private int _backlogProcessorIsRunning = 0; + private int _backlogCurrentEnqueued = 0; + private long _backlogTotalEnqueued = 0; private int activeWriters = 0; private int beating; @@ -132,22 +143,25 @@ public void ReportNextFailure() private WriteResult QueueOrFailMessage(Message message) { - if (message.IsInternalCall && message.Command != RedisCommand.QUIT) - { - // you can go in the queue, but we won't be starting - // a worker, because the handshake has not completed - message.SetEnqueued(null); - _backlog.Enqueue(message); - return WriteResult.Success; // we'll take it... - } - else + // If it's an internal call that's not a QUIT + // or we're allowed to queue in general, then queue + if (message.IsInternalCall || Multiplexer.RawConfig.BacklogPolicy.QueueWhileDisconnected) { - // sorry, we're just not ready for you yet; - message.Cancel(); - Multiplexer?.OnMessageFaulted(message, null); - message.Complete(); - return WriteResult.NoConnectionAvailable; + // Let's just never ever queue a QUIT message + if (message.Command != RedisCommand.QUIT) + { + message.SetEnqueued(null); + BacklogEnqueue(message, null); + // Note: we don't start a worker on each message here + return WriteResult.Success; // Successfully queued, so indicate success + } } + + // Anything else goes in the bin - we're just not ready for you yet + message.Cancel(); + Multiplexer?.OnMessageFaulted(message, null); + message.Complete(); + return WriteResult.NoConnectionAvailable; } private WriteResult FailDueToNoConnection(Message message) @@ -165,21 +179,45 @@ public WriteResult TryWriteSync(Message message, bool isReplica) if (!IsConnected) return QueueOrFailMessage(message); var physical = this.physical; - if (physical == null) return FailDueToNoConnection(message); + if (physical == null) + { + // If we're not connected yet and supposed to, queue it up + if (Multiplexer.RawConfig.BacklogPolicy.QueueWhileDisconnected) + { + if (TryPushToBacklog(message, onlyIfExists: false)) + { + message.SetEnqueued(null); + return WriteResult.Success; + } + } + return FailDueToNoConnection(message); + } var result = WriteMessageTakingWriteLockSync(physical, message); LogNonPreferred(message.Flags, isReplica); return result; } - public ValueTask TryWriteAsync(Message message, bool isReplica) + public ValueTask TryWriteAsync(Message message, bool isReplica, bool bypassBacklog = false) { if (isDisposed) throw new ObjectDisposedException(Name); - if (!IsConnected) return new ValueTask(QueueOrFailMessage(message)); + if (!IsConnected && !bypassBacklog) return new ValueTask(QueueOrFailMessage(message)); var physical = this.physical; - if (physical == null) return new ValueTask(FailDueToNoConnection(message)); + if (physical == null) + { + // If we're not connected yet and supposed to, queue it up + if (!bypassBacklog && Multiplexer.RawConfig.BacklogPolicy.QueueWhileDisconnected) + { + if (TryPushToBacklog(message, onlyIfExists: false)) + { + message.SetEnqueued(null); + return new ValueTask(WriteResult.Success); + } + } + return new ValueTask(FailDueToNoConnection(message)); + } - var result = WriteMessageTakingWriteLockAsync(physical, message); + var result = WriteMessageTakingWriteLockAsync(physical, message, bypassBacklog: bypassBacklog); LogNonPreferred(message.Flags, isReplica); return result; } @@ -230,13 +268,22 @@ internal readonly struct BridgeStatus public bool IsWriterActive { get; init; } /// - /// Total number of backlog messages that are in the retry backlog. + /// Status of the currently processing backlog, if any. + /// + public BacklogStatus BacklogStatus { get; init; } + + /// + /// The number of messages that are in the backlog queue (waiting to be sent when the connection is healthy again). /// public int BacklogMessagesPending { get; init; } /// - /// Status of the currently processing backlog, if any. + /// The number of messages that are in the backlog queue (waiting to be sent when the connection is healthy again). /// - public BacklogStatus BacklogStatus { get; init; } + public int BacklogMessagesPendingCounter { get; init; } + /// + /// The number of messages ever added to the backlog queue in the life of this connection. + /// + public long TotalBacklogMessagesQueued { get; init; } /// /// Status for the underlying . @@ -247,6 +294,9 @@ internal readonly struct BridgeStatus /// The default bridge stats, notable *not* the same as default since initializers don't run. /// public static BridgeStatus Zero { get; } = new() { Connection = PhysicalConnection.ConnectionStatus.Zero }; + + public override string ToString() => + $"MessagesSinceLastHeartbeat: {MessagesSinceLastHeartbeat}, Writer: {(IsWriterActive ? "Active" : "Inactive")}, BacklogStatus: {BacklogStatus}, BacklogMessagesPending: (Queue: {BacklogMessagesPending}, Counter: {BacklogMessagesPendingCounter}), TotalBacklogMessagesQueued: {TotalBacklogMessagesQueued}, Connection: ({Connection})"; } internal BridgeStatus GetStatus() => new() @@ -258,7 +308,9 @@ internal readonly struct BridgeStatus IsWriterActive = !_singleWriterMutex.IsAvailable, #endif BacklogMessagesPending = _backlog.Count, + BacklogMessagesPendingCounter = Volatile.Read(ref _backlogCurrentEnqueued), BacklogStatus = _backlogStatus, + TotalBacklogMessagesQueued = _backlogTotalEnqueued, Connection = physical?.GetStatus() ?? PhysicalConnection.ConnectionStatus.Default, }; @@ -357,7 +409,12 @@ internal void ResetNonConnected() internal void OnConnectionFailed(PhysicalConnection connection, ConnectionFailureType failureType, Exception innerException) { Trace($"OnConnectionFailed: {connection}"); - AbandonPendingBacklog(innerException); + // If we're configured to, fail all pending backlogged messages + if (Multiplexer.RawConfig.BacklogPolicy?.AbortPendingOnConnectionFailure == true) + { + AbandonPendingBacklog(innerException); + } + if (reportNextFailure) { LastException = innerException; @@ -405,7 +462,7 @@ internal void OnDisconnected(ConnectionFailureType failureType, PhysicalConnecti private void AbandonPendingBacklog(Exception ex) { - while (_backlog.TryDequeue(out Message next)) + while (BacklogTryDequeue(out Message next)) { Multiplexer?.OnMessageFaulted(next, ex); next.SetExceptionAndComplete(ex, this); @@ -424,8 +481,10 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) ServerEndPoint.OnFullyEstablished(connection, source); // do we have pending system things to do? - bool createWorker = !_backlog.IsEmpty; - if (createWorker) StartBacklogProcessor(); + if (BacklogHasItems) + { + StartBacklogProcessor(); + } if (ConnectionType == ConnectionType.Interactive) ServerEndPoint.CheckInfoReplication(); } @@ -443,7 +502,14 @@ internal void OnHeartbeat(bool ifConnectedOnly) bool runThisTime = false; try { - CheckBacklogForTimeouts(); + if (BacklogHasItems) + { + // If we have a backlog, kickoff the processing + // This will first timeout any messages that have sat too long and either: + // A: Abort if we're still not connected yet (we should be in this path) + // or B: Process the backlog and send those messages through the pipe + StartBacklogProcessor(); + } runThisTime = !isDisposed && Interlocked.CompareExchange(ref beating, 1, 0) == 0; if (!runThisTime) return; @@ -538,16 +604,11 @@ internal void OnHeartbeat(bool ifConnectedOnly) } } - internal void RemovePhysical(PhysicalConnection connection) - { + internal void RemovePhysical(PhysicalConnection connection) => Interlocked.CompareExchange(ref physical, null, connection); - } [Conditional("VERBOSE")] - internal void Trace(string message) - { - Multiplexer.Trace(message, ToString()); - } + internal void Trace(string message) => Multiplexer.Trace(message, ToString()); [Conditional("VERBOSE")] internal void Trace(bool condition, string message) @@ -635,8 +696,8 @@ internal WriteResult WriteMessageTakingWriteLockSync(PhysicalConnection physical // AVOID REORDERING MESSAGES // Prefer to add it to the backlog if this thread can see that there might already be a message backlog. - // We do this before attempting to take the writelock, because we won't actually write, we'll just let the backlog get processed in due course - if (PushToBacklog(message, onlyIfExists: true)) + // We do this before attempting to take the write lock, because we won't actually write, we'll just let the backlog get processed in due course + if (TryPushToBacklog(message, onlyIfExists: true)) { return WriteResult.Success; // queued counts as success } @@ -656,9 +717,11 @@ internal WriteResult WriteMessageTakingWriteLockSync(PhysicalConnection physical if (!token.Success) #endif { - // we can't get it *instantaneously*; is there - // perhaps a backlog and active backlog processor? - if (PushToBacklog(message, onlyIfExists: true)) return WriteResult.Success; // queued counts as success + // If we can't get it *instantaneously*, pass it to the backlog for throughput + if (TryPushToBacklog(message, onlyIfExists: false)) + { + return WriteResult.Success; // queued counts as success + } // no backlog... try to wait with the timeout; // if we *still* can't get it: that counts as @@ -698,17 +761,29 @@ internal WriteResult WriteMessageTakingWriteLockSync(PhysicalConnection physical } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool PushToBacklog(Message message, bool onlyIfExists) + private bool TryPushToBacklog(Message message, bool onlyIfExists, bool bypassBacklog = false) { - // Note, for deciding emptyness for whether to push onlyIfExists, and start worker, + // In the handshake case: send the command directly through. + // If we're disconnected *in the middle of a handshake*, we've bombed a brand new socket and failing, + // backing off, and retrying next heartbeat is best anyway. + // + // Internal calls also shouldn't queue - try immediately. If these aren't errors (most aren't), we + // won't alert the user. + if (bypassBacklog || message.IsInternalCall) + { + return false; + } + + // Note, for deciding emptiness for whether to push onlyIfExists, and start worker, // we only need care if WE are able to // see the queue when its empty. Not whether anyone else sees it as empty. // So strong synchronization is not required. - if (_backlog.IsEmpty & onlyIfExists) return false; + if (onlyIfExists && Volatile.Read(ref _backlogCurrentEnqueued) == 0) + { + return false; + } - - int count = _backlog.Count; - _backlog.Enqueue(message); + BacklogEnqueue(message, physical); // The correct way to decide to start backlog process is not based on previously empty // but based on a) not empty now (we enqueued!) and b) no backlog processor already running. @@ -717,6 +792,24 @@ private bool PushToBacklog(Message message, bool onlyIfExists) return true; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void BacklogEnqueue(Message message, PhysicalConnection physical) + { + _backlog.Enqueue(message); + Interlocked.Increment(ref _backlogTotalEnqueued); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool BacklogTryDequeue(out Message message) + { + if (_backlog.TryDequeue(out message)) + { + Interlocked.Decrement(ref _backlogCurrentEnqueued); + return true; + } + return false; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void StartBacklogProcessor() { @@ -758,17 +851,17 @@ private void CheckBacklogForTimeouts() // But we reduce contention by only locking if we see something that looks timed out. while (_backlog.TryPeek(out Message message)) { - if (message.IsInternalCall) break; // don't stomp these (not that they should have the async timeout flag, but...) - if (!message.HasAsyncTimedOut(now, timeout, out var _)) break; // not a timeout - we can stop looking + // See if the message has pass our async timeout threshold + // or has otherwise been completed (e.g. a sync wait timed out) which would have cleared the ResultBox + if (!message.HasTimedOut(now, timeout, out var _) || message.ResultBox == null) break; // not a timeout - we can stop looking lock (_backlog) { - // peek again since we didn't have lock before... + // Peek again since we didn't have lock before... // and rerun the exact same checks as above, note that it may be a different message now if (!_backlog.TryPeek(out message)) break; - if (message.IsInternalCall) break; - if (!message.HasAsyncTimedOut(now, timeout, out var _)) break; + if (!message.HasTimedOut(now, timeout, out var _) && message.ResultBox != null) break; - if (!_backlog.TryDequeue(out var message2) || (message != message2)) // consume it for real + if (!BacklogTryDequeue(out var message2) || (message != message2)) // consume it for real { throw new RedisException("Thread safety bug detected! A queue message disappeared while we had the backlog lock"); } @@ -789,6 +882,7 @@ internal enum BacklogStatus : byte Started, CheckingForWork, CheckingForTimeout, + CheckingForTimeoutComplete, RecordingTimeout, WritingMessage, Flushing, @@ -800,8 +894,53 @@ internal enum BacklogStatus : byte } private volatile BacklogStatus _backlogStatus; + /// + /// Process the backlog(s) in play if any. + /// This means flushing commands to an available/active connection (if any) or spinning until timeout if not. + /// private async Task ProcessBacklogAsync() { + _backlogStatus = BacklogStatus.Starting; + try + { + if (!_backlog.IsEmpty) + { + // TODO: vNext handoff this backlog to another primary ("can handle everything") connection + // and remove any per-server commands. This means we need to track a bit of whether something + // was server-endpoint-specific in PrepareToPushMessageToBridge (was the server ref null or not) + await ProcessBridgeBacklogAsync(); // Needs handoff + } + } + catch + { + _backlogStatus = BacklogStatus.Faulted; + } + finally + { + // Do this in finally block, so that thread aborts can't convince us the backlog processor is running forever + if (Interlocked.CompareExchange(ref _backlogProcessorIsRunning, 0, 1) != 1) + { + throw new RedisException("Bug detection, couldn't indicate shutdown of backlog processor"); + } + + // Now that nobody is processing the backlog, we should consider starting a new backlog processor + // in case a new message came in after we ended this loop. + if (BacklogHasItems) + { + // Check for faults mainly to prevent unlimited tasks spawning in a fault scenario + // This won't cause a StackOverflowException due to the Task.Run() handoff + if (_backlogStatus != BacklogStatus.Faulted) + { + StartBacklogProcessor(); + } + } + } + } + + private async Task ProcessBridgeBacklogAsync() + { + // Importantly: don't assume we have a physical connection here + // We are very likely to hit a state where it's not re-established or even referenced here #if NETCOREAPP bool gotLock = false; #else @@ -810,7 +949,14 @@ private async Task ProcessBacklogAsync() try { _backlogStatus = BacklogStatus.Starting; - while (true) + + // First eliminate any messages that have timed out already. + _backlogStatus = BacklogStatus.CheckingForTimeout; + CheckBacklogForTimeouts(); + _backlogStatus = BacklogStatus.CheckingForTimeoutComplete; + + // For the rest of the backlog, if we're not connected there's no point - abort out + while (IsConnected) { // check whether the backlog is empty *before* even trying to get the lock if (_backlog.IsEmpty) return; // nothing to do @@ -826,46 +972,38 @@ private async Task ProcessBacklogAsync() } _backlogStatus = BacklogStatus.Started; - // so now we are the writer; write some things! - Message message; - var timeout = TimeoutMilliseconds; - while(true) + // Only execute if we're connected. + // Timeouts are handled above, so we're exclusively into backlog items eligible to write at this point. + // If we can't write them, abort and wait for the next heartbeat or activation to try this again. + while (IsConnected && physical?.HasOutputPipe == true) { + Message message; _backlogStatus = BacklogStatus.CheckingForWork; - // We need to lock _backlog when dequeueing because of - // races with timeout processing logic + lock (_backlog) { - if (!_backlog.TryDequeue(out message)) break; // all done + // Note that we're actively taking it off the queue here, not peeking + // If there's nothing left in queue, we're done. + if (!BacklogTryDequeue(out message)) break; } try { - _backlogStatus = BacklogStatus.CheckingForTimeout; - if (message.HasAsyncTimedOut(Environment.TickCount, timeout, out var _)) + _backlogStatus = BacklogStatus.WritingMessage; + var result = WriteMessageInsideLock(physical, message); + + if (result == WriteResult.Success) { - _backlogStatus = BacklogStatus.RecordingTimeout; - var ex = Multiplexer.GetException(WriteResult.TimeoutBeforeWrite, message, ServerEndPoint); - message.SetExceptionAndComplete(ex, this); + _backlogStatus = BacklogStatus.Flushing; + result = await physical.FlushAsync(false).ConfigureAwait(false); } - else - { - _backlogStatus = BacklogStatus.WritingMessage; - var result = WriteMessageInsideLock(physical, message); - if (result == WriteResult.Success) - { - _backlogStatus = BacklogStatus.Flushing; - result = await physical.FlushAsync(false).ConfigureAwait(false); - } - - _backlogStatus = BacklogStatus.MarkingInactive; - if (result != WriteResult.Success) - { - _backlogStatus = BacklogStatus.RecordingWriteFailure; - var ex = Multiplexer.GetException(result, message, ServerEndPoint); - HandleWriteException(message, ex); - } + _backlogStatus = BacklogStatus.MarkingInactive; + if (result != WriteResult.Success) + { + _backlogStatus = BacklogStatus.RecordingWriteFailure; + var ex = Multiplexer.GetException(result, message, ServerEndPoint); + HandleWriteException(message, ex); } } catch (Exception ex) @@ -879,13 +1017,9 @@ private async Task ProcessBacklogAsync() } } _backlogStatus = BacklogStatus.SettingIdle; - physical.SetIdle(); + physical?.SetIdle(); _backlogStatus = BacklogStatus.Inactive; } - catch - { - _backlogStatus = BacklogStatus.Faulted; - } finally { #if NETCOREAPP @@ -896,24 +1030,6 @@ private async Task ProcessBacklogAsync() #else token.Dispose(); #endif - - // Do this in finally block, so that thread aborts can't convince us the backlog processor is running forever - if (Interlocked.CompareExchange(ref _backlogProcessorIsRunning, 0, 1) != 1) - { - throw new RedisException("Bug detection, couldn't indicate shutdown of backlog processor"); - } - - // Now that nobody is processing the backlog, we should consider starting a new backlog processor - // in case a new message came in after we ended this loop. - if (!_backlog.IsEmpty) - { - // Check for faults mainly to prevent unlimited tasks spawning in a fault scenario - // - it isn't StackOverflowException due to the Task.Run() - if (_backlogStatus != BacklogStatus.Faulted) - { - StartBacklogProcessor(); - } - } } } @@ -930,27 +1046,16 @@ private WriteResult TimedOutBeforeWrite(Message message) /// /// The physical connection to write to. /// The message to be written. - internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnection physical, Message message) + /// Whether this message should bypass the backlog, going straight to the pipe or failing. + internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnection physical, Message message, bool bypassBacklog = false) { - /* design decision/choice; the code works fine either way, but if this is - * set to *true*, then when we can't take the writer-lock *right away*, - * we push the message to the backlog (starting a worker if needed) - * - * otherwise, we go for a TryWaitAsync and rely on the await machinery - * - * "true" seems to give faster times *when under heavy contention*, based on profiling - * but it involves the backlog concept; "false" works well under low contention, and - * makes more use of async - */ - const bool ALWAYS_USE_BACKLOG_IF_CANNOT_GET_SYNC_LOCK = true; - Trace("Writing: " + message); message.SetEnqueued(physical); // this also records the read/write stats at this point // AVOID REORDERING MESSAGES // Prefer to add it to the backlog if this thread can see that there might already be a message backlog. // We do this before attempting to take the writelock, because we won't actually write, we'll just let the backlog get processed in due course - if (PushToBacklog(message, onlyIfExists: true)) + if (TryPushToBacklog(message, onlyIfExists: true, bypassBacklog: bypassBacklog)) { return new ValueTask(WriteResult.Success); // queued counts as success } @@ -965,19 +1070,20 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect try { // try to acquire it synchronously - // note: timeout is specified in mutex-constructor #if NETCOREAPP gotLock = _singleWriterMutex.Wait(0); if (!gotLock) #else + // note: timeout is specified in mutex-constructor token = _singleWriterMutex.TryWait(options: WaitOptions.NoDelay); if (!token.Success) #endif { - // we can't get it *instantaneously*; is there - // perhaps a backlog and active backlog processor? - if (PushToBacklog(message, onlyIfExists: !ALWAYS_USE_BACKLOG_IF_CANNOT_GET_SYNC_LOCK)) + // If we can't get it *instantaneously*, pass it to the backlog for throughput + if (TryPushToBacklog(message, onlyIfExists: false, bypassBacklog: bypassBacklog)) + { return new ValueTask(WriteResult.Success); // queued counts as success + } // no backlog... try to wait with the timeout; // if we *still* can't get it: that counts as @@ -996,10 +1102,10 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect if (!token.Success) return new ValueTask(TimedOutBeforeWrite(message)); #endif } +#if DEBUG lockTaken = Environment.TickCount; - +#endif var result = WriteMessageInsideLock(physical, message); - if (result == WriteResult.Success) { var flush = physical.FlushAsync(false); @@ -1013,7 +1119,7 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect #endif } - result = flush.Result; // we know it was completed, this is fine + result = flush.Result; // .Result: we know it was completed, so this is fine } physical.SetIdle(); diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 2924368a4..5e7e0da93 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -10,7 +10,6 @@ using System.Net.Security; using System.Net.Sockets; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -68,6 +67,7 @@ internal void GetBytes(out long sent, out long received) } private IDuplexPipe _ioPipe; + internal bool HasOutputPipe => _ioPipe?.Output != null; private Socket _socket; private Socket VolatileSocket => Volatile.Read(ref _socket); @@ -649,7 +649,9 @@ internal void OnBridgeHeartbeat() var timeout = bridge.Multiplexer.AsyncTimeoutMilliseconds; foreach (var msg in _writtenAwaitingResponse) { - if (msg.HasAsyncTimedOut(now, timeout, out var elapsed)) + // We only handle async timeouts here, synchronous timeouts are handled upstream. + // Those sync timeouts happen in ConnectionMultiplexer.ExecuteSyncImpl() via Monitor.Wait. + if (msg.ResultBoxIsAsync && msg.HasTimedOut(now, timeout, out var elapsed)) { bool haveDeltas = msg.TryGetPhysicalState(out _, out _, out long sentDelta, out var receivedDelta) && sentDelta >= 0 && receivedDelta >= 0; var timeoutEx = ExceptionFactory.Timeout(bridge.Multiplexer, haveDeltas @@ -1557,7 +1559,7 @@ private void OnDebugAbort() var bridge = BridgeCouldBeNull; if (bridge == null || !bridge.Multiplexer.AllowConnect) { - throw new RedisConnectionException(ConnectionFailureType.InternalFailure, "debugging"); + throw new RedisConnectionException(ConnectionFailureType.InternalFailure, "Aborting (AllowConnect: False)"); } } diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 05e5b7af6..fc60c48f6 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -572,7 +572,8 @@ internal static Message CreateReplicaOfMessage(ServerEndPoint sendMessageTo, End } internal override Task ExecuteAsync(Message message, ResultProcessor processor, ServerEndPoint server = null) - { // inject our expected server automatically + { + // inject our expected server automatically if (server == null) server = this.server; FixFlags(message, server); if (!server.IsConnected) @@ -580,22 +581,32 @@ internal override Task ExecuteAsync(Message message, ResultProcessor pr if (message == null) return CompletedTask.Default(asyncState); if (message.IsFireAndForget) return CompletedTask.Default(null); // F+F explicitly does not get async-state - // no need to deny exec-sync here; will be complete before they see if - var tcs = TaskSource.Create(asyncState); - ConnectionMultiplexer.ThrowFailed(tcs, ExceptionFactory.NoConnectionAvailable(multiplexer, message, server)); - return tcs.Task; + // After the "don't care" cases above, if we can't queue then it's time to error - otherwise call through to queueing. + if (!multiplexer.RawConfig.BacklogPolicy.QueueWhileDisconnected) + { + // no need to deny exec-sync here; will be complete before they see if + var tcs = TaskSource.Create(asyncState); + ConnectionMultiplexer.ThrowFailed(tcs, ExceptionFactory.NoConnectionAvailable(multiplexer, message, server)); + return tcs.Task; + } } return base.ExecuteAsync(message, processor, server); } internal override T ExecuteSync(Message message, ResultProcessor processor, ServerEndPoint server = null) - { // inject our expected server automatically + { + // inject our expected server automatically if (server == null) server = this.server; FixFlags(message, server); if (!server.IsConnected) { if (message == null || message.IsFireAndForget) return default(T); - throw ExceptionFactory.NoConnectionAvailable(multiplexer, message, server); + + // After the "don't care" cases above, if we can't queue then it's time to error - otherwise call through to queueing. + if (!multiplexer.RawConfig.BacklogPolicy.QueueWhileDisconnected) + { + throw ExceptionFactory.NoConnectionAvailable(multiplexer, message, server); + } } return base.ExecuteSync(message, processor, server); } diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 18f68f36b..a93fb438f 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -73,7 +73,6 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) public bool HasDatabases => serverType == ServerType.Standalone; public bool IsConnected => interactive?.IsConnected == true; - public bool IsSubscriberConnected => subscription?.IsConnected == true; public bool IsConnecting => interactive?.IsConnecting == true; @@ -142,7 +141,7 @@ internal PhysicalBridge.State ConnectionState get { var tmp = interactive; - return tmp.ConnectionState; + return tmp?.ConnectionState ?? State.Disconnected; } } @@ -553,7 +552,11 @@ internal Message GetTracerMessage(bool assertIdentity) internal bool IsSelectable(RedisCommand command, bool allowDisconnected = false) { - var bridge = unselectableReasons == 0 ? GetBridge(command, false) : null; + // Until we've connected at least once, we're going too have a DidNotRespond unselectable reason present + var bridge = unselectableReasons == 0 || (allowDisconnected && unselectableReasons == UnselectableFlags.DidNotRespond) + ? GetBridge(command, false) + : null; + return bridge != null && (allowDisconnected || bridge.IsConnected); } @@ -617,6 +620,9 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) var bridge = connection?.BridgeCouldBeNull; if (bridge != null) { + // Clear the unselectable flag ASAP since we are open for business + ClearUnselectable(UnselectableFlags.DidNotRespond); + if (bridge == subscription) { // Note: this MUST be fire and forget, because we might be in the middle of a Sync processing @@ -841,7 +847,7 @@ internal ValueTask WriteDirectOrQueueFireAndForgetAsync(PhysicalConnection co } else { - result = bridge.WriteMessageTakingWriteLockAsync(connection, message); + result = bridge.WriteMessageTakingWriteLockAsync(connection, message, bypassBacklog: true); } } diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index 7578936f2..7919d4615 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -100,7 +100,7 @@ private static unsafe int GetClusterSlot(byte[] blob) } } - public ServerEndPoint Select(Message message) + public ServerEndPoint Select(Message message, bool allowDisconnected = false) { if (message == null) throw new ArgumentNullException(nameof(message)); int slot = NoSlot; @@ -114,19 +114,19 @@ public ServerEndPoint Select(Message message) if (slot == MultipleSlots) throw ExceptionFactory.MultiSlot(multiplexer.IncludeDetailInExceptions, message); break; } - return Select(slot, message.Command, message.Flags); + return Select(slot, message.Command, message.Flags, allowDisconnected); } - public ServerEndPoint Select(RedisCommand command, in RedisKey key, CommandFlags flags) + public ServerEndPoint Select(RedisCommand command, in RedisKey key, CommandFlags flags, bool allowDisconnected = false) { int slot = ServerType == ServerType.Cluster ? HashSlot(key) : NoSlot; - return Select(slot, command, flags); + return Select(slot, command, flags, allowDisconnected); } - public ServerEndPoint Select(RedisCommand command, in RedisChannel channel, CommandFlags flags) + public ServerEndPoint Select(RedisCommand command, in RedisChannel channel, CommandFlags flags, bool allowDisconnected = false) { int slot = ServerType == ServerType.Cluster ? HashSlot(channel) : NoSlot; - return Select(slot, command, flags); + return Select(slot, command, flags, allowDisconnected); } public bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isMoved) @@ -240,10 +240,8 @@ private static unsafe int IndexOf(byte* ptr, byte value, int start, int end) return -1; } - private ServerEndPoint Any(RedisCommand command, CommandFlags flags) - { - return multiplexer.AnyConnected(ServerType, (uint)Interlocked.Increment(ref anyStartOffset), command, flags); - } + private ServerEndPoint Any(RedisCommand command, CommandFlags flags, bool allowDisconnected) => + multiplexer.AnyServer(ServerType, (uint)Interlocked.Increment(ref anyStartOffset), command, flags, allowDisconnected); private static ServerEndPoint FindMaster(ServerEndPoint endpoint, RedisCommand command) { @@ -286,12 +284,12 @@ private ServerEndPoint[] MapForMutation() return arr; } - private ServerEndPoint Select(int slot, RedisCommand command, CommandFlags flags) + private ServerEndPoint Select(int slot, RedisCommand command, CommandFlags flags, bool allowDisconnected) { flags = Message.GetMasterReplicaFlags(flags); // only interested in master/replica preferences ServerEndPoint[] arr; - if (slot == NoSlot || (arr = map) == null) return Any(command, flags); + if (slot == NoSlot || (arr = map) == null) return Any(command, flags, allowDisconnected); ServerEndPoint endpoint = arr[slot], testing; // but: ^^^ is the MASTER slots; if we want a replica, we need to do some thinking @@ -301,21 +299,21 @@ private ServerEndPoint Select(int slot, RedisCommand command, CommandFlags flags switch (flags) { case CommandFlags.DemandReplica: - return FindReplica(endpoint, command) ?? Any(command, flags); + return FindReplica(endpoint, command) ?? Any(command, flags, allowDisconnected); case CommandFlags.PreferReplica: testing = FindReplica(endpoint, command); if (testing != null) return testing; break; case CommandFlags.DemandMaster: - return FindMaster(endpoint, command) ?? Any(command, flags); + return FindMaster(endpoint, command) ?? Any(command, flags, allowDisconnected); case CommandFlags.PreferMaster: testing = FindMaster(endpoint, command); if (testing != null) return testing; break; } - if (endpoint.IsSelectable(command)) return endpoint; + if (endpoint.IsSelectable(command, allowDisconnected)) return endpoint; } - return Any(command, flags); + return Any(command, flags, allowDisconnected); } } } diff --git a/tests/StackExchange.Redis.Tests/AsyncTests.cs b/tests/StackExchange.Redis.Tests/AsyncTests.cs index 4dd36670b..1ea26e76e 100644 --- a/tests/StackExchange.Redis.Tests/AsyncTests.cs +++ b/tests/StackExchange.Redis.Tests/AsyncTests.cs @@ -19,7 +19,7 @@ public void AsyncTasksReportFailureIfServerUnavailable() { SetExpectedAmbientFailureCount(-1); // this will get messy - using (var conn = Create(allowAdmin: true, shared: false)) + using (var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast)) { var server = conn.GetServer(TestConfig.Current.MasterServer, TestConfig.Current.MasterPort); diff --git a/tests/StackExchange.Redis.Tests/BacklogTests.cs b/tests/StackExchange.Redis.Tests/BacklogTests.cs new file mode 100644 index 000000000..990d15d61 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/BacklogTests.cs @@ -0,0 +1,402 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests +{ + public class BacklogTests : TestBase + { + public BacklogTests(ITestOutputHelper output) : base (output) { } + + protected override string GetConfiguration() => TestConfig.Current.MasterServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + + [Fact] + public async Task FailFast() + { + void PrintSnapshot(ConnectionMultiplexer muxer) + { + Writer.WriteLine("Snapshot summary:"); + foreach (var server in muxer.GetServerSnapshot()) + { + Writer.WriteLine($" {server.EndPoint}: "); + Writer.WriteLine($" Type: {server.ServerType}"); + Writer.WriteLine($" IsConnected: {server.IsConnected}"); + Writer.WriteLine($" IsConnecting: {server.IsConnecting}"); + Writer.WriteLine($" IsSelectable(allowDisconnected: true): {server.IsSelectable(RedisCommand.PING, true)}"); + Writer.WriteLine($" IsSelectable(allowDisconnected: false): {server.IsSelectable(RedisCommand.PING, false)}"); + Writer.WriteLine($" UnselectableFlags: {server.GetUnselectableFlags()}"); + var bridge = server.GetBridge(RedisCommand.PING, create: false); + Writer.WriteLine($" GetBridge: {bridge}"); + Writer.WriteLine($" IsConnected: {bridge.IsConnected}"); + Writer.WriteLine($" ConnectionState: {bridge.ConnectionState}"); + } + } + + try + { + // Ensuring the FailFast policy errors immediate with no connection available exceptions + var options = new ConfigurationOptions() + { + BacklogPolicy = BacklogPolicy.FailFast, + AbortOnConnectFail = false, + ConnectTimeout = 1000, + ConnectRetry = 2, + SyncTimeout = 10000, + KeepAlive = 10000, + AsyncTimeout = 5000, + AllowAdmin = true, + }; + options.EndPoints.Add(TestConfig.Current.MasterServerAndPort); + + using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); + + var db = muxer.GetDatabase(); + Writer.WriteLine("Test: Initial (connected) ping"); + await db.PingAsync(); + + var server = muxer.GetServerSnapshot()[0]; + var stats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal + + // Fail the connection + Writer.WriteLine("Test: Simulating failure"); + muxer.AllowConnect = false; + server.SimulateConnectionFailure(SimulatedFailureType.All); + Assert.False(muxer.IsConnected); + + // Queue up some commands + Writer.WriteLine("Test: Disconnected pings"); + await Assert.ThrowsAsync(() => db.PingAsync()); + + var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.False(muxer.IsConnected); + Assert.Equal(0, disconnectedStats.BacklogMessagesPending); + + Writer.WriteLine("Test: Allowing reconnect"); + muxer.AllowConnect = true; + Writer.WriteLine("Test: Awaiting reconnect"); + await UntilCondition(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); + + Writer.WriteLine("Test: Reconnecting"); + Assert.True(muxer.IsConnected); + Assert.True(server.IsConnected); + var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, reconnectedStats.BacklogMessagesPending); + + _ = db.PingAsync(); + _ = db.PingAsync(); + var lastPing = db.PingAsync(); + + // For debug, print out the snapshot and server states + PrintSnapshot(muxer); + + Assert.NotNull(muxer.SelectServer(Message.Create(-1, CommandFlags.None, RedisCommand.PING))); + + // We should see none queued + Assert.Equal(0, stats.BacklogMessagesPending); + await lastPing; + } + finally + { + ClearAmbientFailures(); + } + } + + [Fact] + public async Task QueuesAndFlushesAfterReconnectingAsync() + { + try + { + var options = new ConfigurationOptions() + { + BacklogPolicy = BacklogPolicy.Default, + AbortOnConnectFail = false, + ConnectTimeout = 1000, + ConnectRetry = 2, + SyncTimeout = 10000, + KeepAlive = 10000, + AsyncTimeout = 5000, + AllowAdmin = true, + SocketManager = SocketManager.ThreadPool, + }; + options.EndPoints.Add(TestConfig.Current.MasterServerAndPort); + + using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); + muxer.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); + muxer.InternalError += (s, e) => Log($"Internal Error {e.EndPoint}: {e.Exception.Message}"); + muxer.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); + muxer.ConnectionRestored += (s, a) => Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); + + var db = muxer.GetDatabase(); + Writer.WriteLine("Test: Initial (connected) ping"); + await db.PingAsync(); + + var server = muxer.GetServerSnapshot()[0]; + var stats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal + + // Fail the connection + Writer.WriteLine("Test: Simulating failure"); + muxer.AllowConnect = false; + server.SimulateConnectionFailure(SimulatedFailureType.All); + Assert.False(muxer.IsConnected); + + // Queue up some commands + Writer.WriteLine("Test: Disconnected pings"); + var ignoredA = db.PingAsync(); + var ignoredB = db.PingAsync(); + var lastPing = db.PingAsync(); + + // TODO: Add specific server call + + var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.False(muxer.IsConnected); + Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); + + Writer.WriteLine("Test: Allowing reconnect"); + muxer.AllowConnect = true; + Writer.WriteLine("Test: Awaiting reconnect"); + await UntilCondition(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); + + Writer.WriteLine("Test: Checking reconnected 1"); + Assert.True(muxer.IsConnected); + + Writer.WriteLine("Test: ignoredA Status: " + ignoredA.Status); + Writer.WriteLine("Test: ignoredB Status: " + ignoredB.Status); + Writer.WriteLine("Test: lastPing Status: " + lastPing.Status); + var afterConnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Writer.WriteLine($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); + + Writer.WriteLine("Test: Awaiting lastPing 1"); + await lastPing; + + Writer.WriteLine("Test: Checking reconnected 2"); + Assert.True(muxer.IsConnected); + var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, reconnectedStats.BacklogMessagesPending); + + Writer.WriteLine("Test: Pinging again..."); + _ = db.PingAsync(); + _ = db.PingAsync(); + Writer.WriteLine("Test: Last Ping issued"); + lastPing = db.PingAsync(); + + // We should see none queued + Writer.WriteLine("Test: BacklogMessagesPending check"); + Assert.Equal(0, stats.BacklogMessagesPending); + Writer.WriteLine("Test: Awaiting lastPing 2"); + await lastPing; + Writer.WriteLine("Test: Done"); + } + finally + { + ClearAmbientFailures(); + } + } + + [Fact] + public async Task QueuesAndFlushesAfterReconnecting() + { + try + { + var options = new ConfigurationOptions() + { + BacklogPolicy = BacklogPolicy.Default, + AbortOnConnectFail = false, + ConnectTimeout = 1000, + ConnectRetry = 2, + SyncTimeout = 10000, + KeepAlive = 10000, + AsyncTimeout = 5000, + AllowAdmin = true, + SocketManager = SocketManager.ThreadPool, + }; + options.EndPoints.Add(TestConfig.Current.MasterServerAndPort); + + using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); + muxer.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); + muxer.InternalError += (s, e) => Log($"Internal Error {e.EndPoint}: {e.Exception.Message}"); + muxer.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); + muxer.ConnectionRestored += (s, a) => Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); + + var db = muxer.GetDatabase(); + Writer.WriteLine("Test: Initial (connected) ping"); + await db.PingAsync(); + + var server = muxer.GetServerSnapshot()[0]; + var stats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal + + // Fail the connection + Writer.WriteLine("Test: Simulating failure"); + muxer.AllowConnect = false; + server.SimulateConnectionFailure(SimulatedFailureType.All); + Assert.False(muxer.IsConnected); + + // Queue up some commands + Writer.WriteLine("Test: Disconnected pings"); + + Task[] pings = new Task[3]; + pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(1)); + pings[1] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(2)); + pings[2] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(3)); + void disconnectedPings(int id) + { + // No need to delay, we're going to try a disconnected connection immediately so it'll fail... + Log($"Pinging (disconnected - {id})"); + var result = db.Ping(); + Log($"Pinging (disconnected - {id}) - result: " + result); + } + Writer.WriteLine("Test: Disconnected pings issued"); + + Assert.False(muxer.IsConnected); + // Give the tasks time to queue + await UntilCondition(TimeSpan.FromSeconds(5), () => server.GetBridgeStatus(ConnectionType.Interactive).BacklogMessagesPending >= 3); + + var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Log($"Test Stats: (BacklogMessagesPending: {disconnectedStats.BacklogMessagesPending}, TotalBacklogMessagesQueued: {disconnectedStats.TotalBacklogMessagesQueued})"); + Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); + + Writer.WriteLine("Test: Allowing reconnect"); + muxer.AllowConnect = true; + Writer.WriteLine("Test: Awaiting reconnect"); + await UntilCondition(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); + + Writer.WriteLine("Test: Checking reconnected 1"); + Assert.True(muxer.IsConnected); + + var afterConnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Writer.WriteLine($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); + + Writer.WriteLine("Test: Awaiting 3 pings"); + await Task.WhenAll(pings); + + Writer.WriteLine("Test: Checking reconnected 2"); + Assert.True(muxer.IsConnected); + var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, reconnectedStats.BacklogMessagesPending); + + Writer.WriteLine("Test: Pinging again..."); + pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(4)); + pings[1] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(5)); + pings[2] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(6)); + Writer.WriteLine("Test: Last Ping queued"); + + // We should see none queued + Writer.WriteLine("Test: BacklogMessagesPending check"); + Assert.Equal(0, stats.BacklogMessagesPending); + Writer.WriteLine("Test: Awaiting 3 more pings"); + await Task.WhenAll(pings); + Writer.WriteLine("Test: Done"); + } + finally + { + ClearAmbientFailures(); + } + } + + [Fact] + public async Task QueuesAndFlushesAfterReconnectingClusterAsync() + { + try + { + var options = ConfigurationOptions.Parse(TestConfig.Current.ClusterServersAndPorts); + options.BacklogPolicy = BacklogPolicy.Default; + options.AbortOnConnectFail = false; + options.ConnectTimeout = 1000; + options.ConnectRetry = 2; + options.SyncTimeout = 10000; + options.KeepAlive = 10000; + options.AsyncTimeout = 5000; + options.AllowAdmin = true; + options.SocketManager = SocketManager.ThreadPool; + + using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); + muxer.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); + muxer.InternalError += (s, e) => Log($"Internal Error {e.EndPoint}: {e.Exception.Message}"); + muxer.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); + muxer.ConnectionRestored += (s, a) => Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); + + var db = muxer.GetDatabase(); + Writer.WriteLine("Test: Initial (connected) ping"); + await db.PingAsync(); + + RedisKey meKey = Me(); + var getMsg = Message.Create(0, CommandFlags.None, RedisCommand.GET, meKey); + + var server = muxer.SelectServer(getMsg); // Get the server specifically for this message's hash slot + var stats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal + + static Task PingAsync(ServerEndPoint server, CommandFlags flags = CommandFlags.None) + { + var message = ResultProcessor.TimingProcessor.CreateMessage(-1, flags, RedisCommand.PING); + + server.Multiplexer.CheckMessage(message); + return server.Multiplexer.ExecuteAsyncImpl(message, ResultProcessor.ResponseTimer, null, server); + } + + // Fail the connection + Writer.WriteLine("Test: Simulating failure"); + muxer.AllowConnect = false; + server.SimulateConnectionFailure(SimulatedFailureType.All); + Assert.False(server.IsConnected); // Server isn't connected + Assert.True(muxer.IsConnected); // ...but the multiplexer is + + // Queue up some commands + Writer.WriteLine("Test: Disconnected pings"); + var ignoredA = PingAsync(server); + var ignoredB = PingAsync(server); + var lastPing = PingAsync(server); + + var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.False(server.IsConnected); + Assert.True(muxer.IsConnected); + Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); + + Writer.WriteLine("Test: Allowing reconnect"); + muxer.AllowConnect = true; + Writer.WriteLine("Test: Awaiting reconnect"); + await UntilCondition(TimeSpan.FromSeconds(3), () => server.IsConnected).ForAwait(); + + Writer.WriteLine("Test: Checking reconnected 1"); + Assert.True(server.IsConnected); + Assert.True(muxer.IsConnected); + + Writer.WriteLine("Test: ignoredA Status: " + ignoredA.Status); + Writer.WriteLine("Test: ignoredB Status: " + ignoredB.Status); + Writer.WriteLine("Test: lastPing Status: " + lastPing.Status); + var afterConnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Writer.WriteLine($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); + + Writer.WriteLine("Test: Awaiting lastPing 1"); + await lastPing; + + Writer.WriteLine("Test: Checking reconnected 2"); + Assert.True(server.IsConnected); + Assert.True(muxer.IsConnected); + var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, reconnectedStats.BacklogMessagesPending); + + Writer.WriteLine("Test: Pinging again..."); + _ = PingAsync(server); + _ = PingAsync(server); + Writer.WriteLine("Test: Last Ping issued"); + lastPing = PingAsync(server); ; + + // We should see none queued + Writer.WriteLine("Test: BacklogMessagesPending check"); + Assert.Equal(0, stats.BacklogMessagesPending); + Writer.WriteLine("Test: Awaiting lastPing 2"); + await lastPing; + Writer.WriteLine("Test: Done"); + } + finally + { + ClearAmbientFailures(); + } + } + } +} diff --git a/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs b/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs index 73af84fa4..6a9d2a399 100644 --- a/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs +++ b/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs @@ -13,7 +13,7 @@ public ConnectFailTimeout(ITestOutputHelper output) : base (output) { } public async Task NoticesConnectFail() { SetExpectedAmbientFailureCount(-1); - using (var conn = Create(allowAdmin: true, shared: false)) + using (var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast)) { var server = conn.GetServer(conn.GetEndPoints()[0]); diff --git a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs index fb0b84d21..606745a08 100644 --- a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs +++ b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs @@ -97,11 +97,12 @@ public async Task Issue922_ReconnectRaised() { var config = ConfigurationOptions.Parse(TestConfig.Current.MasterServerAndPort); config.AbortOnConnectFail = true; - config.KeepAlive = 10; + config.KeepAlive = 1; config.SyncTimeout = 1000; config.AsyncTimeout = 1000; config.ReconnectRetryPolicy = new ExponentialRetry(5000); config.AllowAdmin = true; + config.BacklogPolicy = BacklogPolicy.FailFast; int failCount = 0, restoreCount = 0; diff --git a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs b/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs index cd3521788..94623e4fd 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs @@ -105,6 +105,7 @@ void innerScenario() options.Password = ""; options.AbortOnConnectFail = false; options.ConnectTimeout = 1000; + options.BacklogPolicy = BacklogPolicy.FailFast; var outer = Assert.Throws(() => { using (var muxer = ConnectionMultiplexer.Connect(options)) diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 437dd44e0..31585159f 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -63,7 +63,7 @@ public void ServerTakesPrecendenceOverSnapshot() { try { - using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false)) + using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast)) { muxer.GetDatabase(); muxer.AllowConnect = false; @@ -156,6 +156,7 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple var options = new ConfigurationOptions() { AbortOnConnectFail = abortOnConnect, + BacklogPolicy = BacklogPolicy.FailFast, ConnectTimeout = 1000, SyncTimeout = 500, KeepAlive = 5000 diff --git a/tests/StackExchange.Redis.Tests/PubSub.cs b/tests/StackExchange.Redis.Tests/PubSub.cs index 2c4d6ef80..2dce49c5b 100644 --- a/tests/StackExchange.Redis.Tests/PubSub.cs +++ b/tests/StackExchange.Redis.Tests/PubSub.cs @@ -539,6 +539,9 @@ public async Task PubSubGetAllCorrectOrder_OnMessage_Async() }); await sub.PingAsync().ForAwait(); + // Give a delay between subscriptions and when we try to publish to be safe + await Task.Delay(1000).ForAwait(); + lock (syncLock) { for (int i = 0; i < count; i++) @@ -801,8 +804,8 @@ await sub.SubscribeAsync(channel, delegate Log("Failing connection"); // Fail all connections server.SimulateConnectionFailure(SimulatedFailureType.All); - // Trigger failure - Assert.Throws(() => sub.Ping()); + // Trigger failure (RedisTimeoutException because of backlog behavior) + Assert.Throws(() => sub.Ping()); Assert.False(sub.IsConnected(channel)); // Now reconnect... diff --git a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs index 1a667970e..e3f9590b3 100644 --- a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs +++ b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs @@ -103,7 +103,7 @@ public async Task PrimaryReplicaSubscriptionFailover(CommandFlags flags, bool ex Log("Connecting..."); using var muxer = Create(configuration: config, shared: false, allowAdmin: true) as ConnectionMultiplexer; var sub = muxer.GetSubscriber(); - var channel = (RedisChannel)Me(); + var channel = (RedisChannel)(Me() + flags.ToString()); // Individual channel per case to not overlap publishers var count = 0; Log("Subscribing..."); diff --git a/tests/StackExchange.Redis.Tests/Secure.cs b/tests/StackExchange.Redis.Tests/Secure.cs index 2e7d70929..79cba81b8 100644 --- a/tests/StackExchange.Redis.Tests/Secure.cs +++ b/tests/StackExchange.Redis.Tests/Secure.cs @@ -65,6 +65,7 @@ public async Task ConnectWithWrongPassword(string password) var config = ConfigurationOptions.Parse(GetConfiguration()); config.Password = password; config.ConnectRetry = 0; // we don't want to retry on closed sockets in this case. + config.BacklogPolicy = BacklogPolicy.FailFast; var ex = await Assert.ThrowsAsync(async () => { diff --git a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs index bf22489dd..a91047be1 100644 --- a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs @@ -326,8 +326,20 @@ public void Teardown(TextWriter output) } //Assert.True(false, $"There were {privateFailCount} private ambient exceptions."); } - var pool = SocketManager.Shared?.SchedulerPool; - TestBase.Log(output, $"Service Counts: (Scheduler) By Queue: {pool?.TotalServicedByQueue.ToString()}, By Pool: {pool?.TotalServicedByPool.ToString()}, Workers: {pool?.WorkerCount.ToString()}, Available: {pool?.AvailableCount.ToString()}"); + + if (_actualConnection != null) + { + TestBase.Log(output, "Connection Counts: " + _actualConnection.GetCounters().ToString()); + foreach (var ep in _actualConnection.GetServerSnapshot()) + { + var interactive = ep.GetBridge(ConnectionType.Interactive); + TestBase.Log(output, $" {Format.ToString(interactive)}: " + interactive.GetStatus()); + + var subscription = ep.GetBridge(ConnectionType.Subscription); + TestBase.Log(output, $" {Format.ToString(subscription)}: " + subscription.GetStatus()); + } + + } } } diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index 5cf895e50..454e97982 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -1,6 +1,6 @@  - net472;netcoreapp3.1;net6.0 + net472;net6.0 StackExchange.Redis.Tests true true diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index dd04fa9da..60636db54 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -256,6 +256,7 @@ internal virtual IInternalConnectionMultiplexer Create( bool logTransactionData = true, bool shared = true, int? defaultDatabase = null, + BacklogPolicy backlogPolicy = null, [CallerMemberName] string caller = null) { if (Output == null) @@ -276,7 +277,8 @@ internal virtual IInternalConnectionMultiplexer Create( && tieBreaker == null && defaultDatabase == null && (allowAdmin == null || allowAdmin == true) - && expectedFailCount == 0) + && expectedFailCount == 0 + && backlogPolicy == null) { configuration = GetConfiguration(); if (configuration == _fixture.Configuration) @@ -294,6 +296,7 @@ internal virtual IInternalConnectionMultiplexer Create( channelPrefix, proxy, configuration ?? GetConfiguration(), logTransactionData, defaultDatabase, + backlogPolicy, caller); muxer.InternalError += OnInternalError; muxer.ConnectionFailed += OnConnectionFailed; @@ -324,6 +327,7 @@ public static ConnectionMultiplexer CreateDefault( string configuration = null, bool logTransactionData = true, int? defaultDatabase = null, + BacklogPolicy backlogPolicy = null, [CallerMemberName] string caller = null) { StringWriter localLog = null; @@ -359,6 +363,7 @@ public static ConnectionMultiplexer CreateDefault( if (connectTimeout != null) config.ConnectTimeout = connectTimeout.Value; if (proxy != null) config.Proxy = proxy.Value; if (defaultDatabase != null) config.DefaultDatabase = defaultDatabase.Value; + if (backlogPolicy != null) config.BacklogPolicy = backlogPolicy; var watch = Stopwatch.StartNew(); var task = ConnectionMultiplexer.ConnectAsync(config, log); if (!task.Wait(config.ConnectTimeout >= (int.MaxValue / 2) ? int.MaxValue : config.ConnectTimeout * 2)) From a05fe6af8cc557a41ece2b29e63e827d506220d6 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 8 Feb 2022 10:36:09 -0500 Subject: [PATCH 071/435] Fix for #1236 (#1978) Not enabling this test because it hoses things, but have manually tested local - this now handles the response properly on a `BGREWRITEAOF`. --- src/StackExchange.Redis/RedisLiterals.cs | 1 + src/StackExchange.Redis/RedisServer.cs | 2 +- src/StackExchange.Redis/ResultProcessor.cs | 3 ++- tests/StackExchange.Redis.Tests/Issues/BgSaveResponse.cs | 6 ++++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index bb38b73f4..79950a2a1 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -9,6 +9,7 @@ public static readonly CommandBytes ASK = "ASK ", authFail_trimmed = CommandBytes.TrimToFit("ERR operation not permitted"), backgroundSavingStarted_trimmed = CommandBytes.TrimToFit("Background saving started"), + backgroundSavingAOFStarted_trimmed = CommandBytes.TrimToFit("Background append only file rewriting started"), databases = "databases", loading = "LOADING ", MOVED = "MOVED ", diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index fc60c48f6..026124f1c 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -695,7 +695,7 @@ private static void FixFlags(Message message, ServerEndPoint server) private static ResultProcessor GetSaveResultProcessor(SaveType type) => type switch { - SaveType.BackgroundRewriteAppendOnlyFile => ResultProcessor.DemandOK, + SaveType.BackgroundRewriteAppendOnlyFile => ResultProcessor.BackgroundSaveAOFStarted, SaveType.BackgroundSave => ResultProcessor.BackgroundSaveStarted, #pragma warning disable 0618 SaveType.ForegroundSave => ResultProcessor.DemandOK, diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 805c50f2f..d8c9efd18 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -21,7 +21,8 @@ public static readonly ResultProcessor TrackSubscriptions = new TrackSubscriptionsProcessor(null), Tracer = new TracerProcessor(false), EstablishConnection = new TracerProcessor(true), - BackgroundSaveStarted = new ExpectBasicStringProcessor(CommonReplies.backgroundSavingStarted_trimmed, startsWith: true); + BackgroundSaveStarted = new ExpectBasicStringProcessor(CommonReplies.backgroundSavingStarted_trimmed, startsWith: true), + BackgroundSaveAOFStarted = new ExpectBasicStringProcessor(CommonReplies.backgroundSavingAOFStarted_trimmed, startsWith: true); public static readonly ResultProcessor ByteArray = new ByteArrayProcessor(), diff --git a/tests/StackExchange.Redis.Tests/Issues/BgSaveResponse.cs b/tests/StackExchange.Redis.Tests/Issues/BgSaveResponse.cs index 76de9f55f..e6983f561 100644 --- a/tests/StackExchange.Redis.Tests/Issues/BgSaveResponse.cs +++ b/tests/StackExchange.Redis.Tests/Issues/BgSaveResponse.cs @@ -1,4 +1,5 @@ -using Xunit; +using System.Threading.Tasks; +using Xunit; using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues @@ -10,12 +11,13 @@ public BgSaveResponse(ITestOutputHelper output) : base (output) { } [Theory (Skip = "We don't need to test this, and it really screws local testing hard.")] [InlineData(SaveType.BackgroundSave)] [InlineData(SaveType.BackgroundRewriteAppendOnlyFile)] - public void ShouldntThrowException(SaveType saveType) + public async Task ShouldntThrowException(SaveType saveType) { using (var conn = Create(null, null, true)) { var Server = GetServer(conn); Server.Save(saveType); + await Task.Delay(1000); } } } From 953824f1dbf044d4f338ee7ad80d738ff7b877b0 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 8 Feb 2022 10:39:47 -0500 Subject: [PATCH 072/435] API Additions/Deprecations: MakePrimaryAsync/ReplicaOfAsync (#1969) Note: this is based on #1912, so let's wait until that's in. Changeset overall: - Add `MakePrimaryAsync` (deprecate `MakeMaster`) - This yanks the code and does an evil .Wait() - better ideas? - Add `ReplicaOfAsync` (deprecate `ReplicaOf`) - Remove the last usages of `CommandFlags.HighPriority` - Remove `ServerEndPoint.WriteDirectFireAndForgetSync` path Co-authored-by: mgravell --- .../ConnectionMultiplexer.cs | 54 ++++++----- src/StackExchange.Redis/Enums/CommandFlags.cs | 2 +- src/StackExchange.Redis/Interfaces/IServer.cs | 15 +++- src/StackExchange.Redis/RedisServer.cs | 89 ++++++++++++++----- src/StackExchange.Redis/ServerEndPoint.cs | 22 ++--- tests/StackExchange.Redis.Tests/Failover.cs | 84 ++++++++--------- 6 files changed, 153 insertions(+), 113 deletions(-) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 12959c51e..1616a5d14 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -384,7 +384,7 @@ public void ExportConfiguration(Stream destination, ExportOptions options = Expo } } - internal void MakeMaster(ServerEndPoint server, ReplicationChangeOptions options, LogProxy log) + internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOptions options, LogProxy log) { var cmd = server.GetFeatures().ReplicaCommands ? RedisCommand.REPLICAOF : RedisCommand.SLAVEOF; CommandMap.AssertAvailable(cmd); @@ -395,15 +395,13 @@ internal void MakeMaster(ServerEndPoint server, ReplicationChangeOptions options var srv = new RedisServer(this, server, null); if (!srv.IsConnected) throw ExceptionFactory.NoConnectionAvailable(this, null, server, GetServerSnapshot(), command: cmd); -#pragma warning disable CS0618 - const CommandFlags flags = CommandFlags.NoRedirect | CommandFlags.HighPriority; -#pragma warning restore CS0618 + const CommandFlags flags = CommandFlags.NoRedirect; Message msg; log?.WriteLine($"Checking {Format.ToString(srv.EndPoint)} is available..."); try { - srv.Ping(flags); // if it isn't happy, we're not happy + await srv.PingAsync(flags); // if it isn't happy, we're not happy } catch (Exception ex) { @@ -411,7 +409,7 @@ internal void MakeMaster(ServerEndPoint server, ReplicationChangeOptions options throw; } - var nodes = GetServerSnapshot(); + var nodes = GetServerSnapshot().ToArray(); // Have to array because async/await RedisValue newMaster = Format.ToString(server.EndPoint); RedisKey tieBreakerKey = default(RedisKey); @@ -423,12 +421,14 @@ internal void MakeMaster(ServerEndPoint server, ReplicationChangeOptions options foreach (var node in nodes) { - if (!node.IsConnected) continue; + if (!node.IsConnected || node.IsReplica) continue; log?.WriteLine($"Attempting to set tie-breaker on {Format.ToString(node.EndPoint)}..."); - msg = Message.Create(0, flags, RedisCommand.SET, tieBreakerKey, newMaster); -#pragma warning disable CS0618 - node.WriteDirectFireAndForgetSync(msg, ResultProcessor.DemandOK); -#pragma warning restore CS0618 + msg = Message.Create(0, flags | CommandFlags.FireAndForget, RedisCommand.SET, tieBreakerKey, newMaster); + try + { + await node.WriteDirectAsync(msg, ResultProcessor.DemandOK); + } + catch { } } } @@ -436,7 +436,7 @@ internal void MakeMaster(ServerEndPoint server, ReplicationChangeOptions options log?.WriteLine($"Making {Format.ToString(srv.EndPoint)} a master..."); try { - srv.ReplicaOf(null, flags); + await srv.ReplicaOfAsync(null, flags); } catch (Exception ex) { @@ -445,13 +445,15 @@ internal void MakeMaster(ServerEndPoint server, ReplicationChangeOptions options } // also, in case it was a replica a moment ago, and hasn't got the tie-breaker yet, we re-send the tie-breaker to this one - if (!tieBreakerKey.IsNull) + if (!tieBreakerKey.IsNull && !server.IsReplica) { log?.WriteLine($"Resending tie-breaker to {Format.ToString(server.EndPoint)}..."); - msg = Message.Create(0, flags, RedisCommand.SET, tieBreakerKey, newMaster); -#pragma warning disable CS0618 - server.WriteDirectFireAndForgetSync(msg, ResultProcessor.DemandOK); -#pragma warning restore CS0618 + msg = Message.Create(0, flags | CommandFlags.FireAndForget, RedisCommand.SET, tieBreakerKey, newMaster); + try + { + await server.WriteDirectAsync(msg, ResultProcessor.DemandOK); + } + catch { } } // There's an inherent race here in zero-latency environments (e.g. when Redis is on localhost) when a broadcast is specified @@ -465,7 +467,7 @@ internal void MakeMaster(ServerEndPoint server, ReplicationChangeOptions options // We want everyone possible to pick it up. // We broadcast before *and after* the change to remote members, so that they don't go without detecting a change happened. // This eliminates the race of pub/sub *then* re-slaving happening, since a method both precedes and follows. - void Broadcast(ReadOnlySpan serverNodes) + async Task BroadcastAsync(ServerEndPoint[] serverNodes) { if ((options & ReplicationChangeOptions.Broadcast) != 0 && ConfigurationChangedChannel != null && CommandMap.IsAvailable(RedisCommand.PUBLISH)) @@ -475,16 +477,14 @@ void Broadcast(ReadOnlySpan serverNodes) { if (!node.IsConnected) continue; log?.WriteLine($"Broadcasting via {Format.ToString(node.EndPoint)}..."); - msg = Message.Create(-1, flags, RedisCommand.PUBLISH, channel, newMaster); -#pragma warning disable CS0618 - node.WriteDirectFireAndForgetSync(msg, ResultProcessor.Int64); -#pragma warning restore CS0618 + msg = Message.Create(-1, flags | CommandFlags.FireAndForget, RedisCommand.PUBLISH, channel, newMaster); + await node.WriteDirectAsync(msg, ResultProcessor.Int64); } } } // Send a message before it happens - because afterwards a new replica may be unresponsive - Broadcast(nodes); + await BroadcastAsync(nodes); if ((options & ReplicationChangeOptions.ReplicateToOtherEndpoints) != 0) { @@ -494,16 +494,14 @@ void Broadcast(ReadOnlySpan serverNodes) log?.WriteLine($"Replicating to {Format.ToString(node.EndPoint)}..."); msg = RedisServer.CreateReplicaOfMessage(node, server.EndPoint, flags); -#pragma warning disable CS0618 - node.WriteDirectFireAndForgetSync(msg, ResultProcessor.DemandOK); -#pragma warning restore CS0618 + await node.WriteDirectAsync(msg, ResultProcessor.DemandOK); } } // ...and send one after it happens - because the first broadcast may have landed on a secondary client // and it can reconfigure before any topology change actually happened. This is most likely to happen // in low-latency environments. - Broadcast(nodes); + await BroadcastAsync(nodes); // and reconfigure the muxer log?.WriteLine("Reconfiguring all endpoints..."); @@ -513,7 +511,7 @@ void Broadcast(ReadOnlySpan serverNodes) { Interlocked.Exchange(ref activeConfigCause, null); } - if (!ReconfigureAsync(first: false, reconfigureAll: true, log, srv.EndPoint, "make master").ObserveErrors().Wait(5000)) + if (!await ReconfigureAsync(first: false, reconfigureAll: true, log, srv.EndPoint, "make master")) { log?.WriteLine("Verifying the configuration was incomplete; please verify"); } diff --git a/src/StackExchange.Redis/Enums/CommandFlags.cs b/src/StackExchange.Redis/Enums/CommandFlags.cs index 119fe22bb..074779fb4 100644 --- a/src/StackExchange.Redis/Enums/CommandFlags.cs +++ b/src/StackExchange.Redis/Enums/CommandFlags.cs @@ -18,7 +18,7 @@ public enum CommandFlags /// /// From 2.0, this flag is not used /// - [Obsolete("From 2.0, this flag is not used", false)] + [Obsolete("From 2.0, this flag is not used, this will be removed in 3.0.", false)] HighPriority = 1, /// /// The caller is not interested in the result; the caller will immediately receive a default-value diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 792807268..724a13523 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -418,12 +418,20 @@ public partial interface IServer : IRedis Task LastSaveAsync(CommandFlags flags = CommandFlags.None); /// - /// Promote the selected node to be master + /// Promote the selected node to be primary. /// /// The options to use for this topology change. /// The log to write output to. + [Obsolete("Please use " + nameof(MakePrimaryAsync) + ", this will be removed in 3.0.")] void MakeMaster(ReplicationChangeOptions options, TextWriter log = null); + /// + /// Promote the selected node to be primary. + /// + /// The options to use for this topology change. + /// The log to write output to. + Task MakePrimaryAsync(ReplicationChangeOptions options, TextWriter log = null); + /// /// Returns the role info for the current server. /// @@ -540,7 +548,7 @@ public partial interface IServer : IRedis /// Endpoint of the new master to replicate from. /// The command flags to use. /// https://redis.io/commands/replicaof - [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicaOf) + " instead.")] + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicaOfAsync) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] void SlaveOf(EndPoint master, CommandFlags flags = CommandFlags.None); @@ -550,6 +558,7 @@ public partial interface IServer : IRedis /// Endpoint of the new master to replicate from. /// The command flags to use. /// https://redis.io/commands/replicaof + [Obsolete("Please use " + nameof(ReplicaOfAsync) + ", this will be removed in 3.0.")] void ReplicaOf(EndPoint master, CommandFlags flags = CommandFlags.None); /// @@ -558,7 +567,7 @@ public partial interface IServer : IRedis /// Endpoint of the new master to replicate from. /// The command flags to use. /// https://redis.io/commands/replicaof - [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicaOfAsync) + " instead.")] + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicaOfAsync) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task SlaveOfAsync(EndPoint master, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 026124f1c..fb568736e 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -336,7 +336,16 @@ public void MakeMaster(ReplicationChangeOptions options, TextWriter log = null) { using (var proxy = LogProxy.TryCreate(log)) { - multiplexer.MakeMaster(server, options, proxy); + // Do you believe in magic? + multiplexer.MakePrimaryAsync(server, options, proxy).Wait(60000); + } + } + + public async Task MakePrimaryAsync(ReplicationChangeOptions options, TextWriter log = null) + { + using (var proxy = LogProxy.TryCreate(log)) + { + await multiplexer.MakePrimaryAsync(server, options, proxy); } } @@ -571,6 +580,32 @@ internal static Message CreateReplicaOfMessage(ServerEndPoint sendMessageTo, End return Message.Create(-1, flags, sendMessageTo.GetFeatures().ReplicaCommands ? RedisCommand.REPLICAOF : RedisCommand.SLAVEOF, host, port); } + private Message GetTiebreakerRemovalMessage() + { + var configuration = multiplexer.RawConfig; + + if (!string.IsNullOrWhiteSpace(configuration.TieBreaker) && multiplexer.CommandMap.IsAvailable(RedisCommand.DEL)) + { + var msg = Message.Create(0, CommandFlags.FireAndForget | CommandFlags.NoRedirect, RedisCommand.DEL, (RedisKey)configuration.TieBreaker); + msg.SetInternalCall(); + return msg; + } + return null; + } + + private Message GetConfigChangeMessage() + { + // attempt to broadcast a reconfigure message to anybody listening to this server + var channel = multiplexer.ConfigurationChangedChannel; + if (channel != null && multiplexer.CommandMap.IsAvailable(RedisCommand.PUBLISH)) + { + var msg = Message.Create(-1, CommandFlags.FireAndForget | CommandFlags.NoRedirect, RedisCommand.PUBLISH, (RedisValue)channel, RedisLiterals.Wildcard); + msg.SetInternalCall(); + return msg; + } + return null; + } + internal override Task ExecuteAsync(Message message, ResultProcessor processor, ServerEndPoint server = null) { // inject our expected server automatically @@ -625,46 +660,56 @@ public void ReplicaOf(EndPoint master, CommandFlags flags = CommandFlags.None) { throw new ArgumentException("Cannot replicate to self"); } - // prepare the actual replicaof message (not sent yet) - var replicaOfMsg = CreateReplicaOfMessage(server, master, flags); - - var configuration = multiplexer.RawConfig; +#pragma warning disable CS0618 // attempt to cease having an opinion on the master; will resume that when replication completes // (note that this may fail; we aren't depending on it) - if (!string.IsNullOrWhiteSpace(configuration.TieBreaker) - && multiplexer.CommandMap.IsAvailable(RedisCommand.DEL)) + if (GetTiebreakerRemovalMessage() is Message tieBreakerRemoval) { - var del = Message.Create(0, CommandFlags.FireAndForget | CommandFlags.NoRedirect, RedisCommand.DEL, (RedisKey)configuration.TieBreaker); - del.SetInternalCall(); -#pragma warning disable CS0618 - server.WriteDirectFireAndForgetSync(del, ResultProcessor.Boolean); -#pragma warning restore CS0618 + tieBreakerRemoval.SetSource(ResultProcessor.Boolean, null); + server.GetBridge(tieBreakerRemoval).TryWriteSync(tieBreakerRemoval, server.IsReplica); } + + var replicaOfMsg = CreateReplicaOfMessage(server, master, flags); ExecuteSync(replicaOfMsg, ResultProcessor.DemandOK); // attempt to broadcast a reconfigure message to anybody listening to this server - var channel = multiplexer.ConfigurationChangedChannel; - if (channel != null && multiplexer.CommandMap.IsAvailable(RedisCommand.PUBLISH)) + if (GetConfigChangeMessage() is Message configChangeMessage) { - var pub = Message.Create(-1, CommandFlags.FireAndForget | CommandFlags.NoRedirect, RedisCommand.PUBLISH, (RedisValue)channel, RedisLiterals.Wildcard); - pub.SetInternalCall(); -#pragma warning disable CS0618 - server.WriteDirectFireAndForgetSync(pub, ResultProcessor.Int64); -#pragma warning restore CS0618 + configChangeMessage.SetSource(ResultProcessor.Int64, null); + server.GetBridge(configChangeMessage).TryWriteSync(configChangeMessage, server.IsReplica); } +#pragma warning restore CS0618 } Task IServer.SlaveOfAsync(EndPoint master, CommandFlags flags) => ReplicaOfAsync(master, flags); - public Task ReplicaOfAsync(EndPoint master, CommandFlags flags = CommandFlags.None) + public async Task ReplicaOfAsync(EndPoint master, CommandFlags flags = CommandFlags.None) { - var msg = CreateReplicaOfMessage(server, master, flags); if (master == server.EndPoint) { throw new ArgumentException("Cannot replicate to self"); } - return ExecuteAsync(msg, ResultProcessor.DemandOK); + + // attempt to cease having an opinion on the master; will resume that when replication completes + // (note that this may fail; we aren't depending on it) + if (GetTiebreakerRemovalMessage() is Message tieBreakerRemoval && !server.IsReplica) + { + try + { + await server.WriteDirectAsync(tieBreakerRemoval, ResultProcessor.Boolean); + } + catch { } + } + + var msg = CreateReplicaOfMessage(server, master, flags); + await ExecuteAsync(msg, ResultProcessor.DemandOK); + + // attempt to broadcast a reconfigure message to anybody listening to this server + if (GetConfigChangeMessage() is Message configChangeMessage) + { + await server.WriteDirectAsync(configChangeMessage, ResultProcessor.Int64); + } } private static void FixFlags(Message message, ServerEndPoint server) diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index a93fb438f..560bba8c6 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -333,9 +333,7 @@ internal async Task AutoConfigureAsync(PhysicalConnection connection, LogProxy l log?.WriteLine($"{Format.ToString(this)}: Auto-configuring..."); var commandMap = Multiplexer.CommandMap; -#pragma warning disable CS0618 - const CommandFlags flags = CommandFlags.FireAndForget | CommandFlags.HighPriority | CommandFlags.NoRedirect; -#pragma warning restore CS0618 + const CommandFlags flags = CommandFlags.FireAndForget | CommandFlags.NoRedirect; var features = GetFeatures(); Message msg; @@ -670,10 +668,11 @@ internal bool CheckInfoReplication() if (version >= RedisFeatures.v2_8_0 && Multiplexer.CommandMap.IsAvailable(RedisCommand.INFO) && (bridge = GetBridge(ConnectionType.Interactive, false)) != null) { -#pragma warning disable CS0618 - var msg = Message.Create(-1, CommandFlags.FireAndForget | CommandFlags.HighPriority | CommandFlags.NoRedirect, RedisCommand.INFO, RedisLiterals.replication); + var msg = Message.Create(-1, CommandFlags.FireAndForget | CommandFlags.NoRedirect, RedisCommand.INFO, RedisLiterals.replication); msg.SetInternalCall(); - WriteDirectFireAndForgetSync(msg, ResultProcessor.AutoConfigure, bridge); + msg.SetSource(ResultProcessor.AutoConfigure, null); +#pragma warning disable CS0618 + bridge.TryWriteSync(msg, isReplica); #pragma warning restore CS0618 return true; } @@ -764,17 +763,6 @@ static async Task Awaited(ServerEndPoint @this, Message message, ValueTask(Message message, ResultProcessor processor, PhysicalBridge bridge = null) - { - if (message != null) - { - message.SetSource(processor, null); - Multiplexer.Trace("Enqueue: " + message); - (bridge ?? GetBridge(message)).TryWriteSync(message, isReplica); - } - } - internal void ReportNextFailure() { interactive?.ReportNextFailure(); diff --git a/tests/StackExchange.Redis.Tests/Failover.cs b/tests/StackExchange.Redis.Tests/Failover.cs index 1aa26e237..50565580f 100644 --- a/tests/StackExchange.Redis.Tests/Failover.cs +++ b/tests/StackExchange.Redis.Tests/Failover.cs @@ -9,7 +9,7 @@ namespace StackExchange.Redis.Tests { public class Failover : TestBase, IAsyncLifetime { - protected override string GetConfiguration() => GetMasterReplicaConfig().ToString(); + protected override string GetConfiguration() => GetPrimaryReplicaConfig().ToString(); public Failover(ITestOutputHelper output) : base(output) { @@ -21,24 +21,24 @@ public async Task InitializeAsync() { using (var mutex = Create()) { - var shouldBeMaster = mutex.GetServer(TestConfig.Current.FailoverMasterServerAndPort); - if (shouldBeMaster.IsReplica) + var shouldBePrimary = mutex.GetServer(TestConfig.Current.FailoverMasterServerAndPort); + if (shouldBePrimary.IsReplica) { - Log(shouldBeMaster.EndPoint + " should be master, fixing..."); - shouldBeMaster.MakeMaster(ReplicationChangeOptions.SetTiebreaker); + Log(shouldBePrimary.EndPoint + " should be primary, fixing..."); + await shouldBePrimary.MakePrimaryAsync(ReplicationChangeOptions.SetTiebreaker); } var shouldBeReplica = mutex.GetServer(TestConfig.Current.FailoverReplicaServerAndPort); if (!shouldBeReplica.IsReplica) { Log(shouldBeReplica.EndPoint + " should be a replica, fixing..."); - shouldBeReplica.ReplicaOf(shouldBeMaster.EndPoint); + await shouldBeReplica.ReplicaOfAsync(shouldBePrimary.EndPoint); await Task.Delay(2000).ForAwait(); } } } - private static ConfigurationOptions GetMasterReplicaConfig() + private static ConfigurationOptions GetPrimaryReplicaConfig() { return new ConfigurationOptions { @@ -99,10 +99,10 @@ public async Task ConfigVerifyReceiveConfigChangeBroadcast() Interlocked.Exchange(ref total, 0); - // and send a second time via a re-master operation + // and send a second time via a re-primary operation var server = GetServer(sender); if (server.IsReplica) Skip.Inconclusive("didn't expect a replica"); - server.MakeMaster(ReplicationChangeOptions.Broadcast); + await server.MakePrimaryAsync(ReplicationChangeOptions.Broadcast); await Task.Delay(1000).ConfigureAwait(false); GetServer(receiver).Ping(); GetServer(receiver).Ping(); @@ -113,7 +113,7 @@ public async Task ConfigVerifyReceiveConfigChangeBroadcast() [Fact] public async Task DereplicateGoesToPrimary() { - ConfigurationOptions config = GetMasterReplicaConfig(); + ConfigurationOptions config = GetPrimaryReplicaConfig(); config.ConfigCheckSeconds = 5; using (var conn = ConnectionMultiplexer.Connect(config)) { @@ -123,8 +123,8 @@ public async Task DereplicateGoesToPrimary() primary.Ping(); secondary.Ping(); - primary.MakeMaster(ReplicationChangeOptions.SetTiebreaker); - secondary.MakeMaster(ReplicationChangeOptions.None); + await primary.MakePrimaryAsync(ReplicationChangeOptions.SetTiebreaker); + await secondary.MakePrimaryAsync(ReplicationChangeOptions.None); await Task.Delay(100).ConfigureAwait(false); @@ -150,22 +150,22 @@ public async Task DereplicateGoesToPrimary() var ex = Assert.Throws(() => db.IdentifyEndpoint(key, CommandFlags.DemandReplica)); Assert.StartsWith("No connection is active/available to service this operation: EXISTS " + Me(), ex.Message); - Writer.WriteLine("Invoking MakeMaster()..."); - primary.MakeMaster(ReplicationChangeOptions.Broadcast | ReplicationChangeOptions.ReplicateToOtherEndpoints | ReplicationChangeOptions.SetTiebreaker, Writer); - Writer.WriteLine("Finished MakeMaster() call."); + Writer.WriteLine("Invoking MakePrimaryAsync()..."); + await primary.MakePrimaryAsync(ReplicationChangeOptions.Broadcast | ReplicationChangeOptions.ReplicateToOtherEndpoints | ReplicationChangeOptions.SetTiebreaker, Writer); + Writer.WriteLine("Finished MakePrimaryAsync() call."); await Task.Delay(100).ConfigureAwait(false); - Writer.WriteLine("Invoking Ping() (post-master)"); + Writer.WriteLine("Invoking Ping() (post-primary)"); primary.Ping(); secondary.Ping(); - Writer.WriteLine("Finished Ping() (post-master)"); + Writer.WriteLine("Finished Ping() (post-primary)"); Assert.True(primary.IsConnected, $"{primary.EndPoint} is not connected."); Assert.True(secondary.IsConnected, $"{secondary.EndPoint} is not connected."); - Writer.WriteLine($"{primary.EndPoint}: {primary.ServerType}, Mode: {(primary.IsReplica ? "Replica" : "Master")}"); - Writer.WriteLine($"{secondary.EndPoint}: {secondary.ServerType}, Mode: {(secondary.IsReplica ? "Replica" : "Master")}"); + Writer.WriteLine($"{primary.EndPoint}: {primary.ServerType}, Mode: {(primary.IsReplica ? "Replica" : "Primary")}"); + Writer.WriteLine($"{secondary.EndPoint}: {secondary.ServerType}, Mode: {(secondary.IsReplica ? "Replica" : "Primary")}"); // Create a separate multiplexer with a valid view of the world to distinguish between failures of // server topology changes from failures to recognize those changes @@ -175,10 +175,10 @@ public async Task DereplicateGoesToPrimary() var primary2 = conn2.GetServer(TestConfig.Current.FailoverMasterServerAndPort); var secondary2 = conn2.GetServer(TestConfig.Current.FailoverReplicaServerAndPort); - Writer.WriteLine($"Check: {primary2.EndPoint}: {primary2.ServerType}, Mode: {(primary2.IsReplica ? "Replica" : "Master")}"); - Writer.WriteLine($"Check: {secondary2.EndPoint}: {secondary2.ServerType}, Mode: {(secondary2.IsReplica ? "Replica" : "Master")}"); + Writer.WriteLine($"Check: {primary2.EndPoint}: {primary2.ServerType}, Mode: {(primary2.IsReplica ? "Replica" : "Primary")}"); + Writer.WriteLine($"Check: {secondary2.EndPoint}: {secondary2.ServerType}, Mode: {(secondary2.IsReplica ? "Replica" : "Primary")}"); - Assert.False(primary2.IsReplica, $"{primary2.EndPoint} should be a master (verification connection)."); + Assert.False(primary2.IsReplica, $"{primary2.EndPoint} should be a primary (verification connection)."); Assert.True(secondary2.IsReplica, $"{secondary2.EndPoint} should be a replica (verification connection)."); var db2 = conn2.GetDatabase(); @@ -191,7 +191,7 @@ public async Task DereplicateGoesToPrimary() await UntilCondition(TimeSpan.FromSeconds(20), () => !primary.IsReplica && secondary.IsReplica); - Assert.False(primary.IsReplica, $"{primary.EndPoint} should be a master."); + Assert.False(primary.IsReplica, $"{primary.EndPoint} should be a primary."); Assert.True(secondary.IsReplica, $"{secondary.EndPoint} should be a replica."); Assert.Equal(primary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.PreferMaster)); @@ -203,9 +203,9 @@ public async Task DereplicateGoesToPrimary() #if DEBUG [Fact] - public async Task SubscriptionsSurviveMasterSwitchAsync() + public async Task SubscriptionsSurvivePrimarySwitchAsync() { - static void TopologyFail() => Skip.Inconclusive("Replication tolopogy change failed...and that's both inconsistent and not what we're testing."); + static void TopologyFail() => Skip.Inconclusive("Replication topology change failed...and that's both inconsistent and not what we're testing."); if (RunningInCI) { @@ -220,14 +220,14 @@ public async Task SubscriptionsSurviveMasterSwitchAsync() var subA = a.GetSubscriber(); var subB = b.GetSubscriber(); - long masterChanged = 0, aCount = 0, bCount = 0; + long primaryChanged = 0, aCount = 0, bCount = 0; a.ConfigurationChangedBroadcast += delegate { - Log("A noticed config broadcast: " + Interlocked.Increment(ref masterChanged)); + Log("A noticed config broadcast: " + Interlocked.Increment(ref primaryChanged)); }; b.ConfigurationChangedBroadcast += delegate { - Log("B noticed config broadcast: " + Interlocked.Increment(ref masterChanged)); + Log("B noticed config broadcast: " + Interlocked.Increment(ref primaryChanged)); }; subA.Subscribe(channel, (_, message) => { @@ -263,34 +263,34 @@ public async Task SubscriptionsSurviveMasterSwitchAsync() Assert.Equal(2, Interlocked.Read(ref aCount)); Assert.Equal(2, Interlocked.Read(ref bCount)); - Assert.Equal(0, Interlocked.Read(ref masterChanged)); + Assert.Equal(0, Interlocked.Read(ref primaryChanged)); try { - Interlocked.Exchange(ref masterChanged, 0); + Interlocked.Exchange(ref primaryChanged, 0); Interlocked.Exchange(ref aCount, 0); Interlocked.Exchange(ref bCount, 0); - Log("Changing master..."); + Log("Changing primary..."); using (var sw = new StringWriter()) { - a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).MakeMaster(ReplicationChangeOptions.All, sw); + await a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).MakePrimaryAsync(ReplicationChangeOptions.All, sw); Log(sw.ToString()); } Log("Waiting for connection B to detect..."); await UntilCondition(TimeSpan.FromSeconds(10), () => b.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica).ForAwait(); subA.Ping(); subB.Ping(); - Log("Falover 2 Attempted. Pausing..."); - Log(" A " + TestConfig.Current.FailoverMasterServerAndPort + " status: " + (a.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica ? "Replica" : "Master")); - Log(" A " + TestConfig.Current.FailoverReplicaServerAndPort + " status: " + (a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica ? "Replica" : "Master")); - Log(" B " + TestConfig.Current.FailoverMasterServerAndPort + " status: " + (b.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica ? "Replica" : "Master")); - Log(" B " + TestConfig.Current.FailoverReplicaServerAndPort + " status: " + (b.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica ? "Replica" : "Master")); + Log("Failover 2 Attempted. Pausing..."); + Log(" A " + TestConfig.Current.FailoverMasterServerAndPort + " status: " + (a.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica ? "Replica" : "Primary")); + Log(" A " + TestConfig.Current.FailoverReplicaServerAndPort + " status: " + (a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica ? "Replica" : "Primary")); + Log(" B " + TestConfig.Current.FailoverMasterServerAndPort + " status: " + (b.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica ? "Replica" : "Primary")); + Log(" B " + TestConfig.Current.FailoverReplicaServerAndPort + " status: " + (b.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica ? "Replica" : "Primary")); if (!a.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica) { TopologyFail(); } - Log("Falover 2 Complete."); + Log("Failover 2 Complete."); Assert.True(a.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverMasterServerAndPort} should be a replica"); Assert.False(a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a master"); @@ -334,12 +334,12 @@ public async Task SubscriptionsSurviveMasterSwitchAsync() Log("Counts so far:"); Log(" aCount: " + Interlocked.Read(ref aCount)); Log(" bCount: " + Interlocked.Read(ref bCount)); - Log(" masterChanged: " + Interlocked.Read(ref masterChanged)); + Log(" primaryChanged: " + Interlocked.Read(ref primaryChanged)); Assert.Equal(2, Interlocked.Read(ref aCount)); Assert.Equal(2, Interlocked.Read(ref bCount)); - // Expect 10, because a sees a, but b sees a and b due to replication - Assert.Equal(10, Interlocked.CompareExchange(ref masterChanged, 0, 0)); + // Expect 12, because a sees a, but b sees a and b due to replication + Assert.Equal(12, Interlocked.CompareExchange(ref primaryChanged, 0, 0)); } catch { @@ -353,7 +353,7 @@ public async Task SubscriptionsSurviveMasterSwitchAsync() Log("Restoring configuration..."); try { - a.GetServer(TestConfig.Current.FailoverMasterServerAndPort).MakeMaster(ReplicationChangeOptions.All); + await a.GetServer(TestConfig.Current.FailoverMasterServerAndPort).MakePrimaryAsync(ReplicationChangeOptions.All); await Task.Delay(1000).ForAwait(); } catch { /* Don't bomb here */ } From 528d732f4daf2742e46d6fdad006e0e735c49abf Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 8 Feb 2022 11:00:28 -0500 Subject: [PATCH 073/435] Docs: Global cleanup (#1976) This one's a big docs cleanup across the board. There's a few very minor code tweaks in here but mostly: docs. We had some typos, some wrong words, and a lot of inconsistency so this is a pass at tightening all that up. It's based on top of #1969 (for minimal merge conflicts), so currently last in the merge list. But: ready for review. Highly recommend using the "Viewed" checkbox on files in case there are merge conflicts later...this one's got a few in it. Co-authored-by: mgravell --- .editorconfig | 3 + docs/Configuration.md | 4 +- docs/Timeouts.md | 10 +- src/StackExchange.Redis/AssemblyInfoHack.cs | 2 +- src/StackExchange.Redis/BacklogPolicy.cs | 2 +- src/StackExchange.Redis/BufferReader.cs | 2 + .../ChannelMessageQueue.cs | 39 +- src/StackExchange.Redis/ClientInfo.cs | 122 ++-- .../ClusterConfiguration.cs | 79 ++- src/StackExchange.Redis/CommandBytes.cs | 5 +- src/StackExchange.Redis/CommandMap.cs | 22 +- src/StackExchange.Redis/CommandTrace.cs | 4 +- src/StackExchange.Redis/Condition.cs | 21 +- .../ConfigurationOptions.cs | 73 ++- src/StackExchange.Redis/ConnectionCounters.cs | 60 +- .../ConnectionFailedEventArgs.cs | 20 +- .../ConnectionMultiplexer.cs | 305 +++++----- src/StackExchange.Redis/CursorEnumerable.cs | 26 +- src/StackExchange.Redis/EndPointCollection.cs | 19 +- src/StackExchange.Redis/EndPointEventArgs.cs | 12 +- src/StackExchange.Redis/Enums/Aggregate.cs | 8 +- src/StackExchange.Redis/Enums/Bitwise.cs | 2 +- src/StackExchange.Redis/Enums/ClientFlags.cs | 122 +++- src/StackExchange.Redis/Enums/ClientType.cs | 4 +- src/StackExchange.Redis/Enums/CommandFlags.cs | 6 +- .../Enums/CommandStatus.cs | 6 +- .../Enums/ConnectionFailureType.cs | 22 +- .../Enums/ConnectionType.cs | 8 +- src/StackExchange.Redis/Enums/Exclude.cs | 10 +- .../Enums/ExportOptions.cs | 14 +- src/StackExchange.Redis/Enums/GeoUnit.cs | 11 +- .../Enums/MigrateOptions.cs | 6 +- src/StackExchange.Redis/Enums/Order.cs | 8 +- src/StackExchange.Redis/Enums/PositionKind.cs | 2 +- src/StackExchange.Redis/Enums/Proxy.cs | 8 +- src/StackExchange.Redis/Enums/RedisType.cs | 28 +- .../Enums/ReplicationChangeOptions.cs | 16 +- src/StackExchange.Redis/Enums/ResultType.cs | 16 +- .../Enums/RetransmissionReasonType.cs | 8 +- src/StackExchange.Redis/Enums/SaveType.cs | 15 +- src/StackExchange.Redis/Enums/ServerType.cs | 12 +- src/StackExchange.Redis/Enums/SetOperation.cs | 4 +- src/StackExchange.Redis/Enums/ShutdownMode.cs | 10 +- src/StackExchange.Redis/Enums/SortType.cs | 9 +- src/StackExchange.Redis/Enums/When.cs | 4 +- src/StackExchange.Redis/ExceptionFactory.cs | 2 - src/StackExchange.Redis/Exceptions.cs | 1 - src/StackExchange.Redis/ExponentialRetry.cs | 10 +- src/StackExchange.Redis/ExtensionMethods.cs | 6 +- src/StackExchange.Redis/GeoEntry.cs | 8 +- src/StackExchange.Redis/HashEntry.cs | 10 +- .../HashSlotMovedEventArgs.cs | 5 +- src/StackExchange.Redis/Interfaces/IBatch.cs | 9 +- .../Interfaces/IConnectionMultiplexer.cs | 98 +-- .../Interfaces/IDatabase.cs | 538 +++++++++++------ .../Interfaces/IDatabaseAsync.cs | 565 +++++++++++------- .../Interfaces/IReconnectRetryPolicy.cs | 8 +- src/StackExchange.Redis/Interfaces/IRedis.cs | 2 +- .../Interfaces/IRedisAsync.cs | 12 +- .../Interfaces/IScanningCursor.cs | 10 +- src/StackExchange.Redis/Interfaces/IServer.cs | 150 +++-- .../Interfaces/ISubscriber.cs | 30 +- .../Interfaces/ITransaction.cs | 7 +- .../InternalErrorEventArgs.cs | 7 +- .../KeyspaceIsolation/BatchWrapper.cs | 9 +- .../KeyspaceIsolation/DatabaseExtension.cs | 4 +- .../KeyspaceIsolation/TransactionWrapper.cs | 24 +- src/StackExchange.Redis/Lease.cs | 2 +- src/StackExchange.Redis/LinearRetry.cs | 4 +- src/StackExchange.Redis/LuaScript.cs | 17 +- .../Maintenance/AzureMaintenanceEvent.cs | 4 +- src/StackExchange.Redis/Message.cs | 34 +- src/StackExchange.Redis/MessageCompletable.cs | 5 +- src/StackExchange.Redis/NameValueEntry.cs | 10 +- src/StackExchange.Redis/PerfCounterHelper.cs | 4 +- src/StackExchange.Redis/PhysicalBridge.cs | 6 +- src/StackExchange.Redis/PhysicalConnection.cs | 6 +- src/StackExchange.Redis/RawResult.cs | 1 - src/StackExchange.Redis/RedisBase.cs | 1 - src/StackExchange.Redis/RedisBatch.cs | 4 +- src/StackExchange.Redis/RedisChannel.cs | 46 +- .../RedisErrorEventArgs.cs | 5 +- src/StackExchange.Redis/RedisFeatures.cs | 26 +- src/StackExchange.Redis/RedisKey.cs | 4 +- src/StackExchange.Redis/RedisResult.cs | 55 +- src/StackExchange.Redis/RedisServer.cs | 12 +- src/StackExchange.Redis/RedisSubscriber.cs | 10 +- src/StackExchange.Redis/RedisTransaction.cs | 55 +- src/StackExchange.Redis/RedisValue.cs | 53 +- src/StackExchange.Redis/ResultProcessor.cs | 16 +- .../ScriptParameterMapper.cs | 43 +- src/StackExchange.Redis/ServerEndPoint.cs | 62 +- .../ServerSelectionStrategy.cs | 9 +- src/StackExchange.Redis/SortedSetEntry.cs | 6 +- tests/StackExchange.Redis.Tests/Config.cs | 4 +- tests/StackExchange.Redis.Tests/Deprecated.cs | 4 +- .../StackExchange.Redis.Tests/Helpers/Skip.cs | 2 - .../Helpers/redis-sharp.cs | 2 - tests/StackExchange.Redis.Tests/MassiveOps.cs | 8 +- .../SharedConnectionFixture.cs | 2 +- .../StackExchange.Redis.Tests/Transactions.cs | 4 +- .../WrapperBaseTests.cs | 6 +- 102 files changed, 1822 insertions(+), 1414 deletions(-) diff --git a/.editorconfig b/.editorconfig index 9322d8fec..27ae69cf2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -89,5 +89,8 @@ csharp_space_after_keywords_in_control_flow_statements = true:suggestion # Language settings csharp_prefer_simple_default_expression = false:none +# RCS1194: Implement exception constructors. +dotnet_diagnostic.RCS1194.severity = none + # RCS1229: Use async/await when necessary. dotnet_diagnostic.RCS1229.severity = none \ No newline at end of file diff --git a/docs/Configuration.md b/docs/Configuration.md index 7e35a96d0..443120ec2 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -165,10 +165,10 @@ The above is equivalent to (in the connection string): $INFO=,$SELECT=use ``` -Twemproxy +twemproxy --- -[Twemproxy](https://github.com/twitter/twemproxy) is a tool that allows multiple redis instances to be used as though it were a single server, with inbuilt sharding and fault tolerance (much like redis cluster, but implemented separately). The feature-set available to Twemproxy is reduced. To avoid having to configure this manually, the `Proxy` option can be used: +[twemproxy](https://github.com/twitter/twemproxy) is a tool that allows multiple redis instances to be used as though it were a single server, with inbuilt sharding and fault tolerance (much like redis cluster, but implemented separately). The feature-set available to Twemproxy is reduced. To avoid having to configure this manually, the `Proxy` option can be used: ```csharp var options = new ConfigurationOptions diff --git a/docs/Timeouts.md b/docs/Timeouts.md index 8f78d3db8..9b6dd21ce 100644 --- a/docs/Timeouts.md +++ b/docs/Timeouts.md @@ -71,13 +71,15 @@ How to configure this setting: > **Important Note:** the value specified in this configuration element is a *per-core* setting. For example, if you have a 4 core machine and want your minIOThreads setting to be 200 at runtime, you would use ``. - - Outside of ASP.NET, use the [ThreadPool.SetMinThreads(…)](https://docs.microsoft.com/en-us/dotnet/api/system.threading.threadpool.setminthreads?view=netcore-2.0#System_Threading_ThreadPool_SetMinThreads_System_Int32_System_Int32_) API. - -- In .Net Core, add Environment Variable COMPlus_ThreadPool_ForceMinWorkerThreads to overwrite default MinThreads setting, according to [Environment/Registry Configuration Knobs](https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/clr-configuration-knobs.md) - You can also use the same ThreadPool.SetMinThreads() Method as described above. + - Outside of ASP.NET, use one of the methods described in [Run-time configuration options for threading +](https://docs.microsoft.com/dotnet/core/run-time-config/threading#minimum-threads): + - [ThreadPool.SetMinThreads(…)](https://docs.microsoft.com/dotnet/api/system.threading.threadpool.setminthreads) + - The `ThreadPoolMinThreads` MSBuild property + - The `System.Threading.ThreadPool.MinThreads` setting in your `runtimeconfig.json` Explanation for abbreviations appearing in exception messages --- -By default Redis Timeout exception(s) includes useful information, which can help in uderstanding & diagnosing the timeouts. Some of the abbrivations are as follows: +By default Redis Timeout exception(s) includes useful information, which can help in understanding & diagnosing the timeouts. Some of the abbreviations are as follows: | Abbreviation | Long Name | Meaning | | ------------- | ---------------------- | ---------------------------- | diff --git a/src/StackExchange.Redis/AssemblyInfoHack.cs b/src/StackExchange.Redis/AssemblyInfoHack.cs index ec7037b0f..71560c853 100644 --- a/src/StackExchange.Redis/AssemblyInfoHack.cs +++ b/src/StackExchange.Redis/AssemblyInfoHack.cs @@ -1,4 +1,4 @@ -// Yes, this is embarassing. However, in .NET Core the including AssemblyInfo (ifdef'd or not) will screw with +// Yes, this is embarrassing. However, in .NET Core the including AssemblyInfo (ifdef'd or not) will screw with // your version numbers. Therefore, we need to move the attribute out into another file...this file. // When .csproj merges in, this should be able to return to Properties/AssemblyInfo.cs using System; diff --git a/src/StackExchange.Redis/BacklogPolicy.cs b/src/StackExchange.Redis/BacklogPolicy.cs index 4fb9e67c7..baa13ae20 100644 --- a/src/StackExchange.Redis/BacklogPolicy.cs +++ b/src/StackExchange.Redis/BacklogPolicy.cs @@ -31,7 +31,7 @@ public sealed class BacklogPolicy /// /// Whether to queue commands while disconnected. /// True means queue for attempts up until their timeout. - /// False means to fail ASAP and queue nothing. + /// means to fail ASAP and queue nothing. /// public bool QueueWhileDisconnected { get; init; } diff --git a/src/StackExchange.Redis/BufferReader.cs b/src/StackExchange.Redis/BufferReader.cs index 659ab1b75..b248ac9c8 100644 --- a/src/StackExchange.Redis/BufferReader.cs +++ b/src/StackExchange.Redis/BufferReader.cs @@ -10,6 +10,7 @@ internal enum ConsumeResult Success, NeedMoreData, } + internal ref struct BufferReader { private long _totalConsumed; @@ -209,6 +210,7 @@ public int ConsumeByte() Consume(1); return value; } + public int PeekByte() => IsEmpty ? -1 : _current[OffsetThisSpan]; public ReadOnlySequence SliceFromCurrent() diff --git a/src/StackExchange.Redis/ChannelMessageQueue.cs b/src/StackExchange.Redis/ChannelMessageQueue.cs index 2f01bce59..bc32d84d1 100644 --- a/src/StackExchange.Redis/ChannelMessageQueue.cs +++ b/src/StackExchange.Redis/ChannelMessageQueue.cs @@ -7,25 +7,22 @@ namespace StackExchange.Redis { /// - /// Represents a message that is broadcast via pub/sub + /// Represents a message that is broadcast via publish/subscribe. /// public readonly struct ChannelMessage { - private readonly ChannelMessageQueue _queue; // this is *smaller* than storing a RedisChannel for the subscribed channel + // this is *smaller* than storing a RedisChannel for the subscribed channel + private readonly ChannelMessageQueue _queue; + /// - /// See Object.ToString + /// The Channel:Message string representation. /// public override string ToString() => ((string)Channel) + ":" + ((string)Message); - /// - /// See Object.GetHashCode - /// + /// public override int GetHashCode() => Channel.GetHashCode() ^ Message.GetHashCode(); - /// - /// See Object.Equals - /// - /// The to compare. + /// public override bool Equals(object obj) => obj is ChannelMessage cm && cm.Channel == Channel && cm.Message == Message; internal ChannelMessage(ChannelMessageQueue queue, in RedisChannel channel, in RedisValue value) @@ -36,16 +33,17 @@ internal ChannelMessage(ChannelMessageQueue queue, in RedisChannel channel, in R } /// - /// The channel that the subscription was created from + /// The channel that the subscription was created from. /// public RedisChannel SubscriptionChannel => _queue.Channel; /// - /// The channel that the message was broadcast to + /// The channel that the message was broadcast to. /// public RedisChannel Channel { get; } + /// - /// The value that was broadcast + /// The value that was broadcast. /// public RedisValue Message { get; } @@ -61,25 +59,28 @@ internal ChannelMessage(ChannelMessageQueue queue, in RedisChannel channel, in R } /// - /// Represents a message queue of ordered pub/sub notifications + /// Represents a message queue of ordered pub/sub notifications. /// - /// To create a ChannelMessageQueue, use ISubscriber.Subscribe[Async](RedisKey) + /// + /// To create a ChannelMessageQueue, use + /// or . + /// public sealed class ChannelMessageQueue { private readonly Channel _queue; /// - /// The Channel that was subscribed for this queue + /// The Channel that was subscribed for this queue. /// public RedisChannel Channel { get; } private RedisSubscriber _parent; /// - /// See Object.ToString + /// The string representation of this channel. /// public override string ToString() => (string)Channel; /// - /// An awaitable task the indicates completion of the queue (including drain of data) + /// An awaitable task the indicates completion of the queue (including drain of data). /// public Task Completion => _queue.Reader.Completion; @@ -97,9 +98,7 @@ internal ChannelMessageQueue(in RedisChannel redisChannel, RedisSubscriber paren AllowSynchronousContinuations = false, }; -#pragma warning disable RCS1231 // Make parameter ref read-only. - uses as a delegate for Action private void Write(in RedisChannel channel, in RedisValue value) -#pragma warning restore RCS1231 // Make parameter ref read-only. { var writer = _queue.Writer; writer.TryWrite(new ChannelMessage(this, channel, value)); diff --git a/src/StackExchange.Redis/ClientInfo.cs b/src/StackExchange.Redis/ClientInfo.cs index 92620bcc7..a38a0f3d2 100644 --- a/src/StackExchange.Redis/ClientInfo.cs +++ b/src/StackExchange.Redis/ClientInfo.cs @@ -5,106 +5,160 @@ namespace StackExchange.Redis { /// - /// Represents the state of an individual client connection to redis + /// Represents the state of an individual client connection to redis. /// public sealed class ClientInfo { internal static readonly ResultProcessor Processor = new ClientInfoProcessor(); /// - /// Address (host and port) of the client + /// Address (host and port) of the client. /// public EndPoint Address { get; private set; } /// - /// total duration of the connection in seconds + /// Total duration of the connection in seconds. /// public int AgeSeconds { get; private set; } /// - /// current database ID + /// Current database ID. /// public int Database { get; private set; } /// - /// The flags associated with this connection + /// The flags associated with this connection. /// public ClientFlags Flags { get; private set; } /// /// The client flags can be a combination of: - /// - /// A: connection to be closed ASAP - /// b: the client is waiting in a blocking operation - /// c: connection to be closed after writing entire reply - /// d: a watched keys has been modified - EXEC will fail - /// i: the client is waiting for a VM I/O (deprecated) - /// M: the client is a master - /// N: no specific flag set - /// O: the client is a replica in MONITOR mode - /// P: the client is a Pub/Sub subscriber - /// r: the client is in readonly mode against a cluster node - /// S: the client is a normal replica server - /// U: the client is connected via a Unix domain socket - /// x: the client is in a MULTI/EXEC context - /// t: the client enabled keys tracking in order to perform client side caching - /// R: the client tracking target client is invalid - /// B: the client enabled broadcast tracking mode + /// + /// + /// A + /// Connection to be closed ASAP. + /// + /// + /// b + /// The client is waiting in a blocking operation. + /// + /// + /// c + /// Connection to be closed after writing entire reply. + /// + /// + /// d + /// A watched keys has been modified - EXEC will fail. + /// + /// + /// i + /// The client is waiting for a VM I/O (deprecated). + /// + /// + /// M + /// The client is a primary. + /// + /// + /// N + /// No specific flag set. + /// + /// + /// O + /// The client is a replica in MONITOR mode. + /// + /// + /// P + /// The client is a Pub/Sub subscriber. + /// + /// + /// r + /// The client is in readonly mode against a cluster node. + /// + /// + /// S + /// The client is a normal replica server. + /// + /// + /// u + /// The client is unblocked. + /// + /// + /// U + /// The client is unblocked. + /// + /// + /// x + /// The client is in a MULTI/EXEC context. + /// + /// + /// t + /// The client enabled keys tracking in order to perform client side caching. + /// + /// + /// R + /// The client tracking target client is invalid. + /// + /// + /// B + /// The client enabled broadcast tracking mode. + /// + /// /// + /// https://redis.io/commands/client-list public string FlagsRaw { get; private set; } /// - /// The host of the client (typically an IP address) + /// The host of the client (typically an IP address). /// public string Host => Format.TryGetHostPort(Address, out string host, out _) ? host : null; /// - /// idle time of the connection in seconds + /// Idle time of the connection in seconds. /// public int IdleSeconds { get; private set; } /// - /// last command played + /// Last command played. /// public string LastCommand { get; private set; } /// - /// The name allocated to this connection, if any + /// The name allocated to this connection, if any. /// public string Name { get; private set; } /// - /// number of pattern matching subscriptions + /// Number of pattern matching subscriptions. /// public int PatternSubscriptionCount { get; private set; } /// - /// The port of the client + /// The port of the client. /// public int Port => Format.TryGetHostPort(Address, out _, out int port) ? port : 0; /// - /// The raw content from redis + /// The raw content from redis. /// public string Raw { get; private set; } /// - /// number of channel subscriptions + /// Number of channel subscriptions. /// public int SubscriptionCount { get; private set; } /// - /// number of commands in a MULTI/EXEC context + /// Number of commands in a MULTI/EXEC context. /// public int TransactionCommandLength { get; private set; } /// - /// an unique 64-bit client ID (introduced in Redis 2.8.12). + /// A unique 64-bit client ID (introduced in Redis 2.8.12). /// public long Id { get;private set; } /// - /// Format the object as a string + /// Format the object as a string. /// public override string ToString() { @@ -113,7 +167,7 @@ public override string ToString() } /// - /// The class of the connection + /// The class of the connection. /// public ClientType ClientType { diff --git a/src/StackExchange.Redis/ClusterConfiguration.cs b/src/StackExchange.Redis/ClusterConfiguration.cs index 6a63b93b6..f05193fe6 100644 --- a/src/StackExchange.Redis/ClusterConfiguration.cs +++ b/src/StackExchange.Redis/ClusterConfiguration.cs @@ -9,14 +9,14 @@ namespace StackExchange.Redis { /// - /// Indicates a range of slots served by a cluster node + /// Indicates a range of slots served by a cluster node. /// public readonly struct SlotRange : IEquatable, IComparable, IComparable { private readonly short from, to; /// - /// Create a new SlotRange value + /// Create a new SlotRange value. /// /// The slot ID to start at. /// The slot ID to end at. @@ -34,18 +34,19 @@ private SlotRange(short from, short to) this.from = from; this.to = to; } + /// - /// The start of the range (inclusive) + /// The start of the range (inclusive). /// public int From => from; /// - /// The end of the range (inclusive) + /// The end of the range (inclusive). /// public int To => to; /// - /// Indicates whether two ranges are not equal + /// Indicates whether two ranges are not equal. /// /// The first slot range. /// The second slot range. @@ -93,7 +94,8 @@ public static bool TryParse(string range, out SlotRange value) } /// - /// Compares the current instance with another object of the same type and returns an integer that indicates whether the current instance precedes, follows, or occurs in the same position in the sort order as the other object. + /// Compares the current instance with another object of the same type and returns an integer that indicates + /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the other object. /// /// The other slot range to compare to. public int CompareTo(SlotRange other) @@ -103,20 +105,18 @@ public int CompareTo(SlotRange other) } /// - /// See Object.Equals + /// See . /// /// The other slot range to compare to. public override bool Equals(object obj) => obj is SlotRange sRange && Equals(sRange); /// - /// Indicates whether two ranges are equal + /// Indicates whether two ranges are equal. /// /// The other slot range to compare to. public bool Equals(SlotRange other) => other.from == from && other.to == to; - /// - /// See Object.GetHashCode() - /// + /// public override int GetHashCode() { int x = from, y = to; // makes CS0675 a little happier @@ -124,7 +124,7 @@ public override int GetHashCode() } /// - /// See Object.ToString() + /// String representation ("{from}-{to}") of the range. /// public override string ToString() => from == to ? from.ToString() : (from + "-" + to); @@ -151,7 +151,7 @@ private static bool TryParseInt16(string s, int offset, int count, out short val } /// - /// Describes the state of the cluster as reported by a single node + /// Describes the state of the cluster as reported by a single node. /// public sealed class ClusterConfiguration { @@ -212,18 +212,17 @@ internal ClusterConfiguration(ServerSelectionStrategy serverSelectionStrategy, s } /// - /// Gets all nodes contained in the configuration + /// Gets all nodes contained in the configuration. /// - /// public ICollection Nodes => nodeLookup.Values; /// - /// The node that was asked for the configuration + /// The node that was asked for the configuration. /// public EndPoint Origin { get; } /// - /// Obtain the node relating to a specified endpoint + /// Obtain the node relating to a specified endpoint. /// /// The endpoint to get a cluster node from. public ClusterNode this[EndPoint endpoint] => endpoint == null @@ -322,8 +321,9 @@ internal ClusterNode(ClusterConfiguration configuration, string raw, EndPoint or Slots = slots?.AsReadOnly() ?? (IList)Array.Empty(); IsConnected = parts[7] == "connected"; // Can be "connected" or "disconnected" } + /// - /// Gets all child nodes of the current node + /// Gets all child nodes of the current node. /// public IList Children { @@ -345,43 +345,44 @@ public IList Children } /// - /// Gets the endpoint of the current node + /// Gets the endpoint of the current node. /// public EndPoint EndPoint { get; } /// - /// Gets whether this is the node which responded to the CLUSTER NODES request + /// Gets whether this is the node which responded to the CLUSTER NODES request. /// public bool IsMyself { get; } /// - /// Gets whether this node is a replica + /// Gets whether this node is a replica. /// - [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(IsReplica) + " instead.")] + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(IsReplica) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public bool IsSlave => IsReplica; + /// - /// Gets whether this node is a replica + /// Gets whether this node is a replica. /// public bool IsReplica { get; } /// - /// Gets whether this node is flagged as noaddr + /// Gets whether this node is flagged as noaddr. /// public bool IsNoAddr { get; } /// - /// Gets the node's connection status + /// Gets the node's connection status. /// public bool IsConnected { get; } /// - /// Gets the unique node-id of the current node + /// Gets the unique node-id of the current node. /// public string NodeId { get; } /// - /// Gets the parent node of the current node + /// Gets the parent node of the current node. /// public ClusterNode Parent { @@ -395,22 +396,23 @@ public ClusterNode Parent } /// - /// Gets the unique node-id of the parent of the current node + /// Gets the unique node-id of the parent of the current node. /// public string ParentNodeId { get; } /// - /// The configuration as reported by the server + /// The configuration as reported by the server. /// public string Raw { get; } /// - /// The slots owned by this server + /// The slots owned by this server. /// public IList Slots { get; } /// - /// Compares the current instance with another object of the same type and returns an integer that indicates whether the current instance precedes, follows, or occurs in the same position in the sort order as the other object. + /// Compares the current instance with another object of the same type and returns an integer that indicates + /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the other object. /// /// The to compare to. public int CompareTo(ClusterNode other) @@ -428,13 +430,13 @@ public int CompareTo(ClusterNode other) } /// - /// See Object.Equals + /// See . /// /// The to compare to. public override bool Equals(object obj) => Equals(obj as ClusterNode); /// - /// Indicates whether two ClusterNode instances are equivalent + /// Indicates whether two instances are equivalent. /// /// The to compare to. public bool Equals(ClusterNode other) @@ -444,13 +446,11 @@ public bool Equals(ClusterNode other) return ToString() == other.ToString(); // lazy, but effective - plus only computes once } - /// - /// See object.GetHashCode() - /// + /// public override int GetHashCode() => ToString().GetHashCode(); /// - /// See Object.ToString() + /// A string summary of this cluster configuration. /// public override string ToString() { @@ -490,9 +490,6 @@ internal bool ServesSlot(int hashSlot) return false; } - int IComparable.CompareTo(object obj) - { - return CompareTo(obj as ClusterNode); - } + int IComparable.CompareTo(object obj) => CompareTo(obj as ClusterNode); } } diff --git a/src/StackExchange.Redis/CommandBytes.cs b/src/StackExchange.Redis/CommandBytes.cs index d3eab23c6..e5e06280a 100644 --- a/src/StackExchange.Redis/CommandBytes.cs +++ b/src/StackExchange.Redis/CommandBytes.cs @@ -46,6 +46,7 @@ public override int GetHashCode() hashCode = (hashCode * -1521134295) + _3.GetHashCode(); return hashCode; } + public override bool Equals(object obj) => obj is CommandBytes cb && Equals(cb); bool IEquatable.Equals(CommandBytes other) => _0 == other._0 && _1 == other._1 && _2 == other._2 && _3 == other._3; @@ -78,9 +79,7 @@ public unsafe int Length public bool IsEmpty => _0 == 0L; // cheap way of checking zero length -#pragma warning disable RCS1231 // Make parameter ref read-only. - spans are tiny! public unsafe void CopyTo(Span target) -#pragma warning restore RCS1231 // Make parameter ref read-only. { fixed (ulong* uPtr = &_0) { @@ -121,9 +120,7 @@ public unsafe CommandBytes(string value) } } -#pragma warning disable RCS1231 // Make parameter ref read-only. - spans are tiny! public unsafe CommandBytes(ReadOnlySpan value) -#pragma warning restore RCS1231 // Make parameter ref read-only. { if (value.Length > MaxLength) throw new ArgumentOutOfRangeException("Maximum command length exceeded: " + value.Length + " bytes"); _0 = _1 = _2 = _3 = 0L; diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index ae0ea8eaf..fb43b45b2 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -5,23 +5,21 @@ namespace StackExchange.Redis { /// - /// Represents the commands mapped on a particular configuration + /// Represents the commands mapped on a particular configuration. /// public sealed class CommandMap { private readonly CommandBytes[] map; - internal CommandMap(CommandBytes[] map) - { - this.map = map; - } + internal CommandMap(CommandBytes[] map) => this.map = map; + /// - /// The default commands specified by redis + /// The default commands specified by redis. /// public static CommandMap Default { get; } = CreateImpl(null, null); /// - /// The commands available to https://github.com/twitter/twemproxy + /// The commands available to twemproxy. /// /// https://github.com/twitter/twemproxy/blob/master/notes/redis.md public static CommandMap Twemproxy { get; } = CreateImpl(null, exclusions: new HashSet @@ -54,7 +52,7 @@ internal CommandMap(CommandBytes[] map) }); /// - /// The commands available to https://ssdb.io/ + /// The commands available to SSDB. /// /// https://ssdb.io/docs/redis-to-ssdb.html public static CommandMap SSDB { get; } = Create(new HashSet { @@ -66,7 +64,7 @@ internal CommandMap(CommandBytes[] map) }, true); /// - /// The commands available to https://redis.io/topics/sentinel + /// The commands available to Sentinel. /// /// https://redis.io/topics/sentinel public static CommandMap Sentinel { get; } = Create(new HashSet { @@ -74,7 +72,7 @@ internal CommandMap(CommandBytes[] map) "auth", "ping", "info", "role", "sentinel", "subscribe", "shutdown", "psubscribe", "unsubscribe", "punsubscribe" }, true); /// - /// Create a new CommandMap, customizing some commands + /// Create a new , customizing some commands. /// /// The commands to override. public static CommandMap Create(Dictionary overrides) @@ -95,7 +93,7 @@ public static CommandMap Create(Dictionary overrides) } /// - /// Creates a CommandMap by specifying which commands are available or unavailable + /// Creates a by specifying which commands are available or unavailable. /// /// The commands to specify. /// Whether the commands are available or excluded. @@ -139,7 +137,7 @@ public static CommandMap Create(HashSet commands, bool available = true) } /// - /// See Object.ToString() + /// See . /// public override string ToString() { diff --git a/src/StackExchange.Redis/CommandTrace.cs b/src/StackExchange.Redis/CommandTrace.cs index 956b7a098..000199fb4 100644 --- a/src/StackExchange.Redis/CommandTrace.cs +++ b/src/StackExchange.Redis/CommandTrace.cs @@ -3,7 +3,7 @@ namespace StackExchange.Redis { /// - /// Represents the information known about long-running commands + /// Represents the information known about long-running commands. /// public sealed class CommandTrace { @@ -26,7 +26,7 @@ internal CommandTrace(long uniqueId, long time, long duration, RedisValue[] argu public RedisValue[] Arguments { get; } /// - /// The amount of time needed for its execution + /// The amount of time needed for its execution. /// public TimeSpan Duration { get; } diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs index b2230fbde..caac60f65 100644 --- a/src/StackExchange.Redis/Condition.cs +++ b/src/StackExchange.Redis/Condition.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; -#pragma warning disable RCS1231 - namespace StackExchange.Redis { /// - /// Describes a pre-condition used in a redis transaction. + /// Describes a precondition used in a redis transaction. /// public abstract class Condition { @@ -241,7 +239,7 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value) public static Condition SortedSetLengthEqual(RedisKey key, long length, double min = double.NegativeInfinity, double max = double.PositiveInfinity) => new SortedSetRangeLengthCondition(key, min, max, 0, length); /// - /// Enforces that the given sorted set cardinality is less than a certain value + /// Enforces that the given sorted set cardinality is less than a certain value. /// /// The key of the sorted set to check. /// The length the sorted set must be less than. @@ -366,10 +364,8 @@ internal sealed class ConditionProcessor : ResultProcessor { public static readonly ConditionProcessor Default = new(); -#pragma warning disable RCS1231 // Make parameter ref read-only. public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, RedisValue value = default(RedisValue)) => new ConditionMessage(condition, db, flags, command, key, value); -#pragma warning restore RCS1231 // Make parameter ref read-only. public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, in RedisValue value1) => new ConditionMessage(condition, db, flags, command, key, value, value1); @@ -556,12 +552,9 @@ internal override bool TryValidate(in RawResult result, out bool value) { case RedisType.SortedSet: var parsedValue = RedisValue.Null; - if (!result.IsNull) + if (!result.IsNull && result.TryGetDouble(out var val)) { - if (result.TryGetDouble(out var val)) - { - parsedValue = val; - } + parsedValue = val; } value = (parsedValue == expectedValue) == expectedEqual; @@ -596,6 +589,8 @@ internal override Condition MapKeys(Func map) => private readonly long index; private readonly RedisValue? expectedValue; private readonly RedisKey key; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1242:Do not pass non-read-only struct by read-only reference.", Justification = "Attribute")] public ListCondition(in RedisKey key, long index, bool expectedResult, in RedisValue? expectedValue) { if (key.IsNull) throw new ArgumentNullException(nameof(key)); @@ -827,7 +822,7 @@ internal override bool TryValidate(in RawResult result, out bool value) } /// - /// Indicates the status of a condition as part of a transaction + /// Indicates the status of a condition as part of a transaction. /// public sealed class ConditionResult { @@ -844,7 +839,7 @@ internal ConditionResult(Condition condition) } /// - /// Indicates whether the condition was satisfied + /// Indicates whether the condition was satisfied. /// public bool WasSatisfied => wasSatisfied; diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 6914abc1a..a36a4d06b 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.IO; using System.Linq; using System.Net; using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Threading; using System.Threading.Tasks; using static StackExchange.Redis.ConnectionMultiplexer; @@ -202,7 +202,7 @@ public bool UseSsl } /// - /// Automatically encodes and decodes channels + /// Automatically encodes and decodes channels. /// public RedisChannel ChannelPrefix { get; set; } @@ -216,13 +216,13 @@ public bool CheckCertificateRevocation } /// - /// Create a certificate validation check that checks against the supplied issuer even if not known by the machine + /// Create a certificate validation check that checks against the supplied issuer even if not known by the machine. /// /// The file system path to find the certificate at. public void TrustIssuer(string issuerCertificatePath) => CertificateValidationCallback = TrustIssuerCallback(issuerCertificatePath); /// - /// Create a certificate validation check that checks against the supplied issuer even if not known by the machine + /// Create a certificate validation check that checks against the supplied issuer even if not known by the machine. /// /// The issuer to trust. public void TrustIssuer(X509Certificate2 issuer) => CertificateValidationCallback = TrustIssuerCallback(issuer); @@ -254,12 +254,12 @@ private static bool CheckTrustedIssuer(X509Certificate2 certificateToValidate, X } /// - /// The client name to use for all connections + /// The client name to use for all connections. /// public string ClientName { get; set; } /// - /// The number of times to repeat the initial connect cycle if no servers respond promptly + /// The number of times to repeat the initial connect cycle if no servers respond promptly. /// public int ConnectRetry { @@ -268,7 +268,7 @@ public int ConnectRetry } /// - /// The command-map associated with this configuration + /// The command-map associated with this configuration. /// public CommandMap CommandMap { @@ -281,7 +281,7 @@ public CommandMap CommandMap } /// - /// Channel to use for broadcasting and listening for configuration change notification + /// Channel to use for broadcasting and listening for configuration change notification. /// public string ConfigurationChannel { @@ -290,7 +290,7 @@ public string ConfigurationChannel } /// - /// Specifies the time in milliseconds that should be allowed for connection (defaults to 5 seconds unless SyncTimeout is higher) + /// Specifies the time in milliseconds that should be allowed for connection (defaults to 5 seconds unless SyncTimeout is higher). /// public int ConnectTimeout { @@ -299,12 +299,12 @@ public int ConnectTimeout } /// - /// Specifies the default database to be used when calling ConnectionMultiplexer.GetDatabase() without any parameters + /// Specifies the default database to be used when calling without any parameters. /// public int? DefaultDatabase { get; set; } /// - /// The server version to assume + /// The server version to assume. /// public Version DefaultVersion { @@ -313,12 +313,13 @@ public Version DefaultVersion } /// - /// The endpoints defined for this configuration + /// The endpoints defined for this configuration. /// public EndPointCollection EndPoints { get; } = new EndPointCollection(); /// - /// Use ThreadPriority.AboveNormal for SocketManager reader and writer threads (true by default). If false, ThreadPriority.Normal will be used. + /// Use ThreadPriority.AboveNormal for SocketManager reader and writer threads (true by default). + /// If , will be used. /// public bool HighPrioritySocketThreads { @@ -326,9 +327,8 @@ public bool HighPrioritySocketThreads set => highPrioritySocketThreads = value; } - // Use coalesce expression. /// - /// Specifies the time in seconds at which connections should be pinged to ensure validity + /// Specifies the time in seconds at which connections should be pinged to ensure validity. /// public int KeepAlive { @@ -347,7 +347,7 @@ public int KeepAlive public string Password { get; set; } /// - /// Specifies whether asynchronous operations should be invoked in a way that guarantees their original delivery order + /// Specifies whether asynchronous operations should be invoked in a way that guarantees their original delivery order. /// [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue), false)] public bool PreserveAsyncOrder @@ -357,7 +357,7 @@ public bool PreserveAsyncOrder } /// - /// Type of proxy to use (if any); for example Proxy.Twemproxy. + /// Type of proxy to use (if any); for example . /// public Proxy Proxy { @@ -395,8 +395,7 @@ public bool ResolveDns } /// - /// Specifies the time in milliseconds that the system should allow for responses before concluding that the socket is unhealthy - /// (defaults to SyncTimeout) + /// Specifies the time in milliseconds that the system should allow for responses before concluding that the socket is unhealthy. /// [Obsolete("This setting no longer has any effect, and should not be used")] public int ResponseTimeout @@ -411,13 +410,13 @@ public int ResponseTimeout public string ServiceName { get; set; } /// - /// Gets or sets the SocketManager instance to be used with these options; if this is null a shared cross-multiplexer SocketManager - /// is used + /// Gets or sets the SocketManager instance to be used with these options. + /// If this is null a shared cross-multiplexer is used. /// public SocketManager SocketManager { get; set; } /// - /// Indicates whether the connection should be encrypted + /// Indicates whether the connection should be encrypted. /// public bool Ssl { @@ -426,7 +425,7 @@ public bool Ssl } /// - /// The target-host to use when validating SSL certificate; setting a value here enables SSL mode + /// The target-host to use when validating SSL certificate; setting a value here enables SSL mode. /// public string SslHost { @@ -440,7 +439,7 @@ public string SslHost public SslProtocols? SslProtocols { get; set; } /// - /// Specifies the time in milliseconds that the system should allow for synchronous operations (defaults to 5 seconds) + /// Specifies the time in milliseconds that the system should allow for synchronous operations (defaults to 5 seconds). /// public int SyncTimeout { @@ -449,7 +448,7 @@ public int SyncTimeout } /// - /// Tie-breaker used to choose between masters (must match the endpoint exactly) + /// Tie-breaker used to choose between masters (must match the endpoint exactly). /// public string TieBreaker { @@ -458,7 +457,7 @@ public string TieBreaker } /// - /// The size of the output buffer to use + /// The size of the output buffer to use. /// [Obsolete("This setting no longer has any effect, and should not be used")] public int WriteBuffer @@ -481,7 +480,7 @@ internal RemoteCertificateValidationCallback CertificateValidationCallback } /// - /// Check configuration every n seconds (every minute by default) + /// Check configuration every n seconds (every minute by default). /// public int ConfigCheckSeconds { @@ -490,10 +489,10 @@ public int ConfigCheckSeconds } /// - /// Parse the configuration from a comma-delimited configuration string + /// Parse the configuration from a comma-delimited configuration string. /// /// The configuration string to parse. - /// is null. + /// is . /// is empty. public static ConfigurationOptions Parse(string configuration) { @@ -503,11 +502,11 @@ public static ConfigurationOptions Parse(string configuration) } /// - /// Parse the configuration from a comma-delimited configuration string + /// Parse the configuration from a comma-delimited configuration string. /// /// The configuration string to parse. /// Whether to ignore unknown elements in . - /// is null. + /// is . /// is empty. public static ConfigurationOptions Parse(string configuration, bool ignoreUnknown) { @@ -517,7 +516,7 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow } /// - /// Create a copy of the configuration + /// Create a copy of the configuration. /// public ConfigurationOptions Clone() { @@ -575,12 +574,12 @@ public ConfigurationOptions Apply(Action configure) } /// - /// Resolve the default port for any endpoints that did not have a port explicitly specified + /// Resolve the default port for any endpoints that did not have a port explicitly specified. /// public void SetDefaultPorts() => EndPoints.SetDefaultPorts(Ssl ? 6380 : 6379); /// - /// Sets default config settings required for sentinel usage + /// Sets default config settings required for sentinel usage. /// internal void SetSentinelDefaults() { @@ -834,7 +833,7 @@ private void DoParse(string configuration, bool ignoreUnknown) case OptionKeys.WriteBuffer: #pragma warning disable CS0618 // Type or member is obsolete WriteBuffer = OptionKeys.ParseInt32(key, value); -#pragma warning restore CS0618 // Type or member is obsolete +#pragma warning restore CS0618 break; case OptionKeys.Proxy: Proxy = OptionKeys.ParseProxy(key, value); @@ -842,7 +841,7 @@ private void DoParse(string configuration, bool ignoreUnknown) case OptionKeys.ResponseTimeout: #pragma warning disable CS0618 // Type or member is obsolete ResponseTimeout = OptionKeys.ParseInt32(key, value, minValue: 1); -#pragma warning restore CS0618 // Type or member is obsolete +#pragma warning restore CS0618 break; case OptionKeys.DefaultDatabase: DefaultDatabase = OptionKeys.ParseInt32(key, value); @@ -881,7 +880,7 @@ private void DoParse(string configuration, bool ignoreUnknown) } } - // Microsoft Azure team wants abortConnect=false by default + ///Microsoft Azure team wants abortConnect=false by default. private bool GetDefaultAbortOnConnectFailSetting() => !IsAzureEndpoint(); /// diff --git a/src/StackExchange.Redis/ConnectionCounters.cs b/src/StackExchange.Redis/ConnectionCounters.cs index b9a3da5be..5be2ae488 100644 --- a/src/StackExchange.Redis/ConnectionCounters.cs +++ b/src/StackExchange.Redis/ConnectionCounters.cs @@ -3,7 +3,7 @@ namespace StackExchange.Redis { /// - /// Illustrates the counters associated with an individual connection + /// Illustrates the counters associated with an individual connection. /// public class ConnectionCounters { @@ -13,78 +13,78 @@ internal ConnectionCounters(ConnectionType connectionType) } /// - /// The number of operations that have been completed asynchronously + /// The number of operations that have been completed asynchronously. /// public long CompletedAsynchronously { get; internal set; } /// - /// The number of operations that have been completed synchronously + /// The number of operations that have been completed synchronously. /// public long CompletedSynchronously { get; internal set; } /// - /// The type of this connection + /// The type of this connection. /// public ConnectionType ConnectionType { get; } /// - /// The number of operations that failed to complete asynchronously + /// The number of operations that failed to complete asynchronously. /// public long FailedAsynchronously { get; internal set; } /// - /// Indicates if there are any pending items or failures on this connection + /// Indicates if there are any pending items or failures on this connection. /// public bool IsEmpty => PendingUnsentItems == 0 && SentItemsAwaitingResponse == 0 && ResponsesAwaitingAsyncCompletion == 0 && FailedAsynchronously == 0; /// - /// Indicates the total number of messages despatched to a non-preferred endpoint, for example sent to a master - /// when the caller stated a preference of replica + /// Indicates the total number of messages dispatched to a non-preferred endpoint, for example sent + /// to a primary when the caller stated a preference of replica. /// public long NonPreferredEndpointCount { get; internal set; } /// - /// The number of operations performed on this connection + /// The number of operations performed on this connection. /// public long OperationCount { get; internal set; } /// - /// Operations that have been requested, but which have not yet been sent to the server + /// Operations that have been requested, but which have not yet been sent to the server. /// public int PendingUnsentItems { get; internal set; } /// - /// Operations for which the response has been processed, but which are awaiting asynchronous completion + /// Operations for which the response has been processed, but which are awaiting asynchronous completion. /// public int ResponsesAwaitingAsyncCompletion { get; internal set; } /// - /// Operations that have been sent to the server, but which are awaiting a response + /// Operations that have been sent to the server, but which are awaiting a response. /// public int SentItemsAwaitingResponse { get; internal set; } /// - /// The number of sockets used by this logical connection (total, including reconnects) + /// The number of sockets used by this logical connection (total, including reconnects). /// public long SocketCount { get; internal set; } /// - /// The number of subscriptions (with and without patterns) currently held against this connection + /// The number of subscriptions (with and without patterns) currently held against this connection. /// public long Subscriptions { get;internal set; } /// - /// Indicates the total number of outstanding items against this connection + /// Indicates the total number of outstanding items against this connection. /// public int TotalOutstanding => PendingUnsentItems + SentItemsAwaitingResponse + ResponsesAwaitingAsyncCompletion; /// - /// Indicates the total number of writers items against this connection + /// Indicates the total number of writers items against this connection. /// public int WriterCount { get; internal set; } /// - /// See Object.ToString() + /// See . /// public override string ToString() { @@ -109,20 +109,18 @@ internal void Add(ConnectionCounters other) WriterCount += other.WriterCount; } - internal bool Any() - { - return CompletedAsynchronously != 0 - || CompletedSynchronously != 0 - || FailedAsynchronously != 0 - || NonPreferredEndpointCount != 0 - || OperationCount != 0 - || PendingUnsentItems != 0 - || ResponsesAwaitingAsyncCompletion != 0 - || SentItemsAwaitingResponse != 0 - || SocketCount != 0 - || Subscriptions != 0 - || WriterCount != 0; - } + internal bool Any() => + CompletedAsynchronously != 0 + || CompletedSynchronously != 0 + || FailedAsynchronously != 0 + || NonPreferredEndpointCount != 0 + || OperationCount != 0 + || PendingUnsentItems != 0 + || ResponsesAwaitingAsyncCompletion != 0 + || SentItemsAwaitingResponse != 0 + || SocketCount != 0 + || Subscriptions != 0 + || WriterCount != 0; internal void Append(StringBuilder sb) { diff --git a/src/StackExchange.Redis/ConnectionFailedEventArgs.cs b/src/StackExchange.Redis/ConnectionFailedEventArgs.cs index e87a7eff4..0ec804f88 100644 --- a/src/StackExchange.Redis/ConnectionFailedEventArgs.cs +++ b/src/StackExchange.Redis/ConnectionFailedEventArgs.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis { /// - /// Contains information about a server connection failure + /// Contains information about a server connection failure. /// public class ConnectionFailedEventArgs : EventArgs, ICompletable { @@ -29,7 +29,7 @@ internal ConnectionFailedEventArgs(EventHandler handl /// Redis endpoint. /// Redis connection type. /// Redis connection failure type. - /// The exception occured. + /// The exception that occurred. /// Connection physical name. public ConnectionFailedEventArgs(object sender, EndPoint endPoint, ConnectionType connectionType, ConnectionFailureType failureType, Exception exception, string physicalName) : this (null, sender, endPoint, connectionType, failureType, exception, physicalName) @@ -39,31 +39,27 @@ public ConnectionFailedEventArgs(object sender, EndPoint endPoint, ConnectionTyp private readonly string _physicalName; /// - /// Gets the connection-type of the failing connection + /// Gets the connection-type of the failing connection. /// public ConnectionType ConnectionType { get; } /// - /// Gets the failing server-endpoint + /// Gets the failing server-endpoint. /// public EndPoint EndPoint { get; } /// - /// Gets the exception if available (this can be null) + /// Gets the exception if available (this can be null). /// public Exception Exception { get; } /// - /// The type of failure + /// The type of failure. /// public ConnectionFailureType FailureType { get; } - void ICompletable.AppendStormLog(StringBuilder sb) - { - sb.Append("event, connection-failed: "); - if (EndPoint == null) sb.Append("n/a"); - else sb.Append(Format.ToString(EndPoint)); - } + void ICompletable.AppendStormLog(StringBuilder sb) => + sb.Append("event, connection-failed: ").Append(EndPoint != null ? Format.ToString(EndPoint) : "n/a"); bool ICompletable.TryComplete(bool isAsync) => ConnectionMultiplexer.TryCompleteHandler(handler, sender, this, isAsync); diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 1616a5d14..3b827f7f8 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -20,8 +20,10 @@ namespace StackExchange.Redis { /// - /// Represents an inter-related group of connections to redis servers + /// Represents an inter-related group of connections to redis servers. + /// A reference to this should be held and re-used. /// + /// https://stackexchange.github.io/StackExchange.Redis/PipelinesMultiplexers public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplexer // implies : IConnectionMultiplexer and : IDisposable { [Flags] @@ -34,7 +36,8 @@ private enum FeatureFlags private static FeatureFlags s_featureFlags; /// - /// Enables or disables a feature flag; this should only be used under support guidance, and should not be rapidly toggled + /// Enables or disables a feature flag. + /// This should only be used under support guidance, and should not be rapidly toggled. /// [EditorBrowsable(EditorBrowsableState.Never)] [Browsable(false)] @@ -60,7 +63,8 @@ static ConnectionMultiplexer() } /// - /// Returns the state of a feature flag; this should only be used under support guidance + /// Returns the state of a feature flag. + /// This should only be used under support guidance. /// [EditorBrowsable(EditorBrowsableState.Never)] [Browsable(false)] @@ -76,7 +80,7 @@ public static bool GetFeatureFlag(string flag) private static int _collectedWithoutDispose; internal static int CollectedWithoutDispose => Thread.VolatileRead(ref _collectedWithoutDispose); /// - /// Invoked by the garbage collector + /// Invoked by the garbage collector. /// ~ConnectionMultiplexer() { @@ -97,24 +101,26 @@ bool IInternalConnectionMultiplexer.IgnoreConnect } /// - /// For debugging: when not enabled, servers cannot connect + /// For debugging: when not enabled, servers cannot connect. /// internal volatile bool AllowConnect = true; /// - /// For debugging: when not enabled, end-connect is silently ignored (to simulate a long-running connect) + /// For debugging: when not enabled, end-connect is silently ignored (to simulate a long-running connect). /// internal volatile bool IgnoreConnect; /// - /// Tracks overall connection multiplexer counts + /// Tracks overall connection multiplexer counts. /// internal int _connectAttemptCount = 0, _connectCompletedCount = 0, _connectionCloseCount = 0; /// - /// Provides a way of overriding the default Task Factory. If not set, it will use the default Task.Factory. + /// Provides a way of overriding the default Task Factory. + /// If not set, it will use the default . /// Useful when top level code sets it's own factory which may interfere with Redis queries. /// + [Obsolete("No longer used, will be removed in 3.0.")] public static TaskFactory Factory { get => _factory ?? Task.Factory; @@ -122,7 +128,7 @@ public static TaskFactory Factory } /// - /// Get summary statistics associates with this server + /// Get summary statistics associated with all servers in this multiplexer. /// public ServerCounters GetCounters() { @@ -137,7 +143,7 @@ public ServerCounters GetCounters() } /// - /// Gets the client-name that will be used on all new connections + /// Gets the client-name that will be used on all new connections. /// public string ClientName => RawConfig.ClientName ?? GetDefaultClientName(); @@ -153,7 +159,7 @@ private static string GetDefaultClientName() /// /// Tries to get the RoleInstance Id if Microsoft.WindowsAzure.ServiceRuntime is loaded. - /// In case of any failure, swallows the exception and returns null + /// In case of any failure, swallows the exception and returns null. /// internal static string TryGetAzureRoleInstanceIdNoThrow() { @@ -197,7 +203,7 @@ internal static string TryGetAzureRoleInstanceIdNoThrow() } /// - /// Gets the configuration of the connection + /// Gets the configuration of the connection. /// public string Configuration => RawConfig.ToString(); @@ -207,8 +213,7 @@ internal void OnConnectionFailed(EndPoint endpoint, ConnectionType connectionTyp var handler = ConnectionFailed; if (handler != null) { - ConnectionMultiplexer.CompleteAsWorker( - new ConnectionFailedEventArgs(handler, this, endpoint, connectionType, failureType, exception, physicalName)); + CompleteAsWorker(new ConnectionFailedEventArgs(handler, this, endpoint, connectionType, failureType, exception, physicalName)); } if (reconfigure) { @@ -225,12 +230,12 @@ internal void OnInternalError(Exception exception, EndPoint endpoint = null, Con var handler = InternalError; if (handler != null) { - ConnectionMultiplexer.CompleteAsWorker( - new InternalErrorEventArgs(handler, this, endpoint, connectionType, exception, origin)); + CompleteAsWorker(new InternalErrorEventArgs(handler, this, endpoint, connectionType, exception, origin)); } } catch - { // our internal error event failed; whatcha gonna do, exactly? + { + // Our internal error event failed...whatcha gonna do, exactly? } } @@ -240,8 +245,7 @@ internal void OnConnectionRestored(EndPoint endpoint, ConnectionType connectionT var handler = ConnectionRestored; if (handler != null) { - ConnectionMultiplexer.CompleteAsWorker( - new ConnectionFailedEventArgs(handler, this, endpoint, connectionType, ConnectionFailureType.None, null, physicalName)); + CompleteAsWorker(new ConnectionFailedEventArgs(handler, this, endpoint, connectionType, ConnectionFailureType.None, null, physicalName)); } ReconfigureIfNeeded(endpoint, false, "connection restored"); } @@ -251,7 +255,7 @@ private void OnEndpointChanged(EndPoint endpoint, EventHandler OnEndpointChanged(endpoint, ConfigurationChangedBroadcast); /// - /// A server replied with an error message; + /// Raised when a server replied with an error message. /// public event EventHandler ErrorMessage; internal void OnErrorMessage(EndPoint endpoint, string message) @@ -268,9 +272,7 @@ internal void OnErrorMessage(EndPoint endpoint, string message) var handler = ErrorMessage; if (handler != null) { - ConnectionMultiplexer.CompleteAsWorker( - new RedisErrorEventArgs(handler, this, endpoint, message) - ); + CompleteAsWorker(new RedisErrorEventArgs(handler, this, endpoint, message)); } } @@ -298,7 +300,7 @@ private static void Write(ZipArchive zip, string name, Task task, Action - /// Write the configuration of all servers to an output stream + /// Write the configuration of all servers to an output stream. /// /// The destination stream to write the export to. /// The options to use for this export. @@ -306,7 +308,7 @@ public void ExportConfiguration(Stream destination, ExportOptions options = Expo { if (destination == null) throw new ArgumentNullException(nameof(destination)); - // what is possible, given the command map? + // What is possible, given the command map? ExportOptions mask = 0; if (CommandMap.IsAvailable(RedisCommand.INFO)) mask |= ExportOptions.Info; if (CommandMap.IsAvailable(RedisCommand.CONFIG)) mask |= ExportOptions.Config; @@ -458,7 +460,8 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt // There's an inherent race here in zero-latency environments (e.g. when Redis is on localhost) when a broadcast is specified // The broadcast can get back from redis and trigger a reconfigure before we get a chance to get to ReconfigureAsync() below - // This results in running an outdated reconfig and the .CompareExchange() (due to already running a reconfig) failing...making our needed reconfig a no-op. + // This results in running an outdated reconfiguration and the .CompareExchange() (due to already running a reconfiguration) + // failing...making our needed reconfiguration a no-op. // If we don't block *that* run, then *our* run (at low latency) gets blocked. Then we're waiting on the // ConfigurationOptions.ConfigCheckSeconds interval to identify the current (created by this method call) topology correctly. var blockingReconfig = Interlocked.CompareExchange(ref activeConfigCause, "Block: Pending Master Reconfig", null) == null; @@ -523,7 +526,7 @@ internal void CheckMessage(Message message) throw ExceptionFactory.AdminModeNotEnabled(IncludeDetailInExceptions, message.Command, message, null); if (message.Command != RedisCommand.UNKNOWN) CommandMap.AssertAvailable(message.Command); - // using >= here because we will be adding 1 for the command itself (which is an arg for the purposes of the multi-bulk protocol) + // using >= here because we will be adding 1 for the command itself (which is an argument for the purposes of the multi-bulk protocol) if (message.ArgCount >= PhysicalConnection.REDIS_MAX_ARGS) throw ExceptionFactory.TooManyArgs(message.CommandAndKey, message.ArgCount); } private const string NoContent = "(no content)"; @@ -545,28 +548,28 @@ private static void WriteNormalizingLineEndings(string source, StreamWriter writ } /// - /// Raised whenever a physical connection fails + /// Raised whenever a physical connection fails. /// public event EventHandler ConnectionFailed; /// - /// Raised whenever an internal error occurs (this is primarily for debugging) + /// Raised whenever an internal error occurs (this is primarily for debugging). /// public event EventHandler InternalError; /// - /// Raised whenever a physical connection is established + /// Raised whenever a physical connection is established. /// public event EventHandler ConnectionRestored; /// - /// Raised when configuration changes are detected + /// Raised when configuration changes are detected. /// public event EventHandler ConfigurationChanged; /// - /// Raised when nodes are explicitly requested to reconfigure via broadcast; - /// this usually means master/replica changes + /// Raised when nodes are explicitly requested to reconfigure via broadcast. + /// This usually means primary/replica role changes. /// public event EventHandler ConfigurationChangedBroadcast; @@ -576,16 +579,17 @@ private static void WriteNormalizingLineEndings(string source, StreamWriter writ public event EventHandler ServerMaintenanceEvent; /// - /// Gets the synchronous timeout associated with the connections + /// Gets the synchronous timeout associated with the connections. /// public int TimeoutMilliseconds { get; } + /// - /// Gets the asynchronous timeout associated with the connections + /// Gets the asynchronous timeout associated with the connections. /// internal int AsyncTimeoutMilliseconds { get; } /// - /// Gets all endpoints defined on the server + /// Gets all endpoints defined on the multiplexer. /// /// Whether to get only the endpoints specified explicitly in the config. public EndPoint[] GetEndPoints(bool configuredOnly = false) @@ -604,7 +608,7 @@ internal bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool i } /// - /// Wait for a given asynchronous operation to complete (or timeout) + /// Wait for a given asynchronous operation to complete (or timeout). /// /// The task to wait on. public void Wait(Task task) @@ -621,7 +625,7 @@ public void Wait(Task task) } /// - /// Wait for a given asynchronous operation to complete (or timeout) + /// Wait for a given asynchronous operation to complete (or timeout). /// /// The type contains in the task to wait on. /// The task to wait on. @@ -646,7 +650,7 @@ private static bool IsSingle(AggregateException aex) } /// - /// Wait for the given asynchronous operations to complete (or timeout) + /// Wait for the given asynchronous operations to complete (or timeout). /// /// The tasks to wait on. public void WaitAll(params Task[] tasks) @@ -665,13 +669,12 @@ private static bool WaitAllIgnoreErrors(Task[] tasks, int timeout) var watch = Stopwatch.StartNew(); try { - // if none error, great + // If no error, great if (Task.WaitAll(tasks, timeout)) return true; } catch { } - // if we get problems, need to give the non-failing ones time to finish - // to be fair and reasonable + // If we get problems, need to give the non-failing ones time to be fair and reasonable for (int i = 0; i < tasks.Length; i++) { var task = tasks[i]; @@ -782,7 +785,7 @@ private static async Task WaitAllIgnoreErrorsAsync(string name, Task[] tas } /// - /// Raised when a hash-slot has been relocated + /// Raised when a hash-slot has been relocated. /// public event EventHandler HashSlotMoved; @@ -791,13 +794,12 @@ internal void OnHashSlotMoved(int hashSlot, EndPoint old, EndPoint @new) var handler = HashSlotMoved; if (handler != null) { - ConnectionMultiplexer.CompleteAsWorker( - new HashSlotMovedEventArgs(handler, this, hashSlot, old, @new)); + CompleteAsWorker(new HashSlotMovedEventArgs(handler, this, hashSlot, old, @new)); } } /// - /// Compute the hash-slot of a specified key + /// Compute the hash-slot of a specified key. /// /// The key to get a hash slot ID for. public int HashSlot(RedisKey key) => ServerSelectionStrategy.HashSlot(key); @@ -845,7 +847,7 @@ internal ServerEndPoint AnyServer(ServerType serverType, uint startOffset, Redis internal bool IsDisposed => _isDisposed; /// - /// Create a new ConnectionMultiplexer instance. + /// Creates a new instance. /// /// The string configuration to use for this multiplexer. /// The to log to. @@ -853,7 +855,7 @@ public static Task ConnectAsync(string configuration, Tex ConnectAsync(ConfigurationOptions.Parse(configuration), log); /// - /// Create a new ConnectionMultiplexer instance. + /// Creates a new instance. /// /// The string configuration to use for this multiplexer. /// Action to further modify the parsed configuration options. @@ -862,10 +864,11 @@ public static Task ConnectAsync(string configuration, Act ConnectAsync(ConfigurationOptions.Parse(configuration).Apply(configure), log); /// - /// Create a new ConnectionMultiplexer instance. + /// Creates a new instance. /// /// The configuration options to use for this multiplexer. /// The to log to. + /// Note: For Sentinel, do not specify a - this is handled automatically. public static Task ConnectAsync(ConfigurationOptions configuration, TextWriter log = null) { SocketConnection.AssertDependencies(); @@ -951,7 +954,7 @@ internal static ConfigurationOptions PrepareConfig(object configuration, bool se return config; } - internal class LogProxy : IDisposable + internal sealed class LogProxy : IDisposable { public static LogProxy TryCreate(TextWriter writer) => writer == null ? null : new LogProxy(writer); @@ -1006,12 +1009,12 @@ private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions conf connectHandler = null; if (log != null) { - // create a detachable event-handler to log detailed errors if something happens during connect/handshake + // Create a detachable event-handler to log detailed errors if something happens during connect/handshake connectHandler = (_, a) => { try { - lock (log.SyncLock) // keep the outer and any inner errors contiguous + lock (log.SyncLock) // Keep the outer and any inner errors contiguous { var ex = a.Exception; log?.WriteLine($"connection failed: {Format.ToString(a.EndPoint)} ({a.ConnectionType}, {a.FailureType}): {ex?.Message ?? "(unknown)"}"); @@ -1029,7 +1032,7 @@ private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions conf } /// - /// Create a new ConnectionMultiplexer instance. + /// Creates a new instance. /// /// The string configuration to use for this multiplexer. /// The to log to. @@ -1037,7 +1040,7 @@ public static ConnectionMultiplexer Connect(string configuration, TextWriter log Connect(ConfigurationOptions.Parse(configuration), log); /// - /// Create a new ConnectionMultiplexer instance. + /// Creates a new instance. /// /// The string configuration to use for this multiplexer. /// Action to further modify the parsed configuration options. @@ -1046,10 +1049,11 @@ public static ConnectionMultiplexer Connect(string configuration, Action - /// Create a new ConnectionMultiplexer instance. + /// Creates a new instance. /// /// The configuration options to use for this multiplexer. /// The to log to. + /// Note: For Sentinel, do not specify a - this is handled automatically. public static ConnectionMultiplexer Connect(ConfigurationOptions configuration, TextWriter log = null) { SocketConnection.AssertDependencies(); @@ -1063,7 +1067,7 @@ public static ConnectionMultiplexer Connect(ConfigurationOptions configuration, } /// - /// Create a new ConnectionMultiplexer instance that connects to a sentinel server + /// Create a new instance that connects to a Sentinel server. /// /// The string configuration to use for this multiplexer. /// The to log to. @@ -1074,7 +1078,7 @@ public static ConnectionMultiplexer SentinelConnect(string configuration, TextWr } /// - /// Create a new ConnectionMultiplexer instance that connects to a sentinel server + /// Create a new instance that connects to a Sentinel server. /// /// The string configuration to use for this multiplexer. /// The to log to. @@ -1085,7 +1089,7 @@ public static Task SentinelConnectAsync(string configurat } /// - /// Create a new ConnectionMultiplexer instance that connects to a sentinel server + /// Create a new instance that connects to a Sentinel server. /// /// The configuration options to use for this multiplexer. /// The to log to. @@ -1096,7 +1100,7 @@ public static ConnectionMultiplexer SentinelConnect(ConfigurationOptions configu } /// - /// Create a new ConnectionMultiplexer instance that connects to a sentinel server + /// Create a new instance that connects to a Sentinel server. /// /// The configuration options to use for this multiplexer. /// The to log to. @@ -1107,8 +1111,8 @@ public static Task SentinelConnectAsync(ConfigurationOpti } /// - /// Create a new ConnectionMultiplexer instance that connects to a sentinel server, discovers the current master server - /// for the specified ServiceName in the config and returns a managed connection to the current master server + /// Create a new instance that connects to a sentinel server, discovers the current primary server + /// for the specified in the config and returns a managed connection to the current primary server. /// /// The string configuration to use for this multiplexer. /// The to log to. @@ -1118,8 +1122,8 @@ private static ConnectionMultiplexer SentinelMasterConnect(string configuration, } /// - /// Create a new ConnectionMultiplexer instance that connects to a sentinel server, discovers the current master server - /// for the specified ServiceName in the config and returns a managed connection to the current master server + /// Create a new instance that connects to a sentinel server, discovers the current primary server + /// for the specified in the config and returns a managed connection to the current primary server. /// /// The configuration options to use for this multiplexer. /// The to log to. @@ -1128,15 +1132,15 @@ private static ConnectionMultiplexer SentinelMasterConnect(ConfigurationOptions var sentinelConnection = SentinelConnect(configuration, log); var muxer = sentinelConnection.GetSentinelMasterConnection(configuration, log); - // set reference to sentinel connection so that we can dispose it + // Set reference to sentinel connection so that we can dispose it muxer.sentinelConnection = sentinelConnection; return muxer; } /// - /// Create a new ConnectionMultiplexer instance that connects to a sentinel server, discovers the current master server - /// for the specified ServiceName in the config and returns a managed connection to the current master server + /// Create a new instance that connects to a sentinel server, discovers the current primary server + /// for the specified in the config and returns a managed connection to the current primary server. /// /// The string configuration to use for this multiplexer. /// The to log to. @@ -1146,8 +1150,8 @@ private static Task SentinelMasterConnectAsync(string con } /// - /// Create a new ConnectionMultiplexer instance that connects to a sentinel server, discovers the current master server - /// for the specified ServiceName in the config and returns a managed connection to the current master server + /// Create a new instance that connects to a sentinel server, discovers the current primary server + /// for the specified in the config and returns a managed connection to the current primary server. /// /// The configuration options to use for this multiplexer. /// The to log to. @@ -1156,7 +1160,7 @@ private static async Task SentinelMasterConnectAsync(Conf var sentinelConnection = await SentinelConnectAsync(configuration, log).ForAwait(); var muxer = sentinelConnection.GetSentinelMasterConnection(configuration, log); - // set reference to sentinel connection so that we can dispose it + // Set reference to sentinel connection so that we can dispose it muxer.sentinelConnection = sentinelConnection; return muxer; @@ -1409,7 +1413,7 @@ internal long LastHeartbeatSecondsAgo internal static long LastGlobalHeartbeatSecondsAgo => unchecked(Environment.TickCount - Thread.VolatileRead(ref lastGlobalHeartbeatTicks)) / 1000; /// - /// Obtain a pub/sub subscriber connection to the specified server + /// Obtain a pub/sub subscriber connection to the specified server. /// /// The async state object to pass to the created . public ISubscriber GetSubscriber(object asyncState = null) @@ -1418,7 +1422,9 @@ public ISubscriber GetSubscriber(object asyncState = null) return new RedisSubscriber(this, asyncState); } - // applies common db number defaults and rules + /// + /// Applies common DB number defaults and rules. + /// internal int ApplyDefaultDatabase(int db) { if (db == -1) @@ -1432,14 +1438,14 @@ internal int ApplyDefaultDatabase(int db) if (db != 0 && RawConfig.Proxy == Proxy.Twemproxy) { - throw new NotSupportedException("Twemproxy only supports database 0"); + throw new NotSupportedException("twemproxy only supports database 0"); } return db; } /// - /// Obtain an interactive connection to a database inside redis + /// Obtain an interactive connection to a database inside redis. /// /// The ID to get a database for. /// The async state to pass into the resulting . @@ -1454,14 +1460,14 @@ public IDatabase GetDatabase(int db = -1, object asyncState = null) // DB zero is stored separately, since 0-only is a massively common use-case private const int MaxCachedDatabaseInstance = 16; // 17 items - [0,16] - // side note: "databases 16" is the default in redis.conf; happy to store one extra to get nice alignment etc + // Side note: "databases 16" is the default in redis.conf; happy to store one extra to get nice alignment etc private IDatabase dbCacheZero; private IDatabase[] dbCacheLow; private IDatabase GetCachedDatabaseInstance(int db) // note that we already trust db here; only caller checks range { - // note we don't need to worry about *always* returning the same instance - // - if two threads ask for db 3 at the same time, it is OK for them to get - // different instances, one of which (arbitrarily) ends up cached for later use + // Note: we don't need to worry about *always* returning the same instance. + // If two threads ask for db 3 at the same time, it is OK for them to get + // different instances, one of which (arbitrarily) ends up cached for later use. if (db == 0) { return dbCacheZero ??= new RedisDatabase(this, 0, null); @@ -1471,7 +1477,7 @@ public IDatabase GetDatabase(int db = -1, object asyncState = null) } /// - /// Obtain a configuration API for an individual server + /// Obtain a configuration API for an individual server. /// /// The host to get a server for. /// The port for to get a server for. @@ -1479,21 +1485,21 @@ public IDatabase GetDatabase(int db = -1, object asyncState = null) public IServer GetServer(string host, int port, object asyncState = null) => GetServer(Format.ParseEndPoint(host, port), asyncState); /// - /// Obtain a configuration API for an individual server + /// Obtain a configuration API for an individual server. /// /// The "host:port" string to get a server for. /// The async state to pass into the resulting . public IServer GetServer(string hostAndPort, object asyncState = null) => GetServer(Format.TryParseEndPoint(hostAndPort), asyncState); /// - /// Obtain a configuration API for an individual server + /// Obtain a configuration API for an individual server. /// /// The host to get a server for. /// The port for to get a server for. public IServer GetServer(IPAddress host, int port) => GetServer(new IPEndPoint(host, port)); /// - /// Obtain a configuration API for an individual server + /// Obtain a configuration API for an individual server. /// /// The endpoint to get a server for. /// The async state to pass into the resulting . @@ -1507,7 +1513,7 @@ public IServer GetServer(EndPoint endpoint, object asyncState = null) } /// - /// The number of operations that have been performed on all connections + /// The number of operations that have been performed on all connections. /// public long OperationCount { @@ -1544,7 +1550,7 @@ internal bool ReconfigureIfNeeded(EndPoint blame, bool fromBroadcast, string cau } /// - /// Reconfigure the current connections based on the existing configuration + /// Reconfigure the current connections based on the existing configuration. /// /// The to log to. public async Task ConfigureAsync(TextWriter log = null) @@ -1556,13 +1562,13 @@ public async Task ConfigureAsync(TextWriter log = null) } /// - /// Reconfigure the current connections based on the existing configuration + /// Reconfigure the current connections based on the existing configuration. /// /// The to log to. public bool Configure(TextWriter log = null) { - // note we expect ReconfigureAsync to internally allow [n] duration, - // so to avoid near misses, here we wait 2*[n] + // Note we expect ReconfigureAsync to internally allow [n] duration, + // so to avoid near misses, here we wait 2*[n]. using (var logProxy = LogProxy.TryCreate(log)) { var task = ReconfigureAsync(first: false, reconfigureAll: true, logProxy, null, "configure"); @@ -1597,7 +1603,7 @@ internal int SyncConnectTimeout(bool forConnect) } /// - /// Provides a text overview of the status of all connections + /// Provides a text overview of the status of all connections. /// public string GetStatus() { @@ -1609,7 +1615,7 @@ public string GetStatus() } /// - /// Provides a text overview of the status of all connections + /// Provides a text overview of the status of all connections. /// /// The to log to. public void GetStatus(TextWriter log) @@ -1619,6 +1625,7 @@ public void GetStatus(TextWriter log) GetStatus(proxy); } } + internal void GetStatus(LogProxy log) { if (log == null) return; @@ -1640,10 +1647,12 @@ private void ActivateAllServers(LogProxy log) server.Activate(ConnectionType.Interactive, log); if (CommandMap.IsAvailable(RedisCommand.SUBSCRIBE)) { - server.Activate(ConnectionType.Subscription, null); // no need to log the SUB stuff + // Intentionally not logging the sub connection + server.Activate(ConnectionType.Subscription, null); } } } + internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogProxy log, EndPoint blame, string cause, bool publishReconfigure = false, CommandFlags publishReconfigureFlags = CommandFlags.None) { if (_isDisposed) throw new ObjectDisposedException(ToString()); @@ -1707,12 +1716,12 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP Stopwatch watch = null; int iterCount = first ? 2 : 1; - // this is fix for https://github.com/StackExchange/StackExchange.Redis/issues/300 + // This is fix for https://github.com/StackExchange/StackExchange.Redis/issues/300 // auto discoverability of cluster nodes is made synchronous. - // we try to connect to endpoints specified inside the user provided configuration - // and when we encounter one such endpoint to which we are able to successfully connect, - // we get the list of cluster nodes from this endpoint and try to proactively connect - // to these nodes instead of relying on auto configure + // We try to connect to endpoints specified inside the user provided configuration + // and when we encounter an endpoint to which we are able to successfully connect, + // we get the list of cluster nodes from that endpoint and try to proactively connect + // to listed nodes instead of relying on auto configure. for (int iter = 0; iter < iterCount; ++iter) { if (endpoints == null) break; @@ -1788,7 +1797,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP server.ClearUnselectable(UnselectableFlags.DidNotRespond); log?.WriteLine($"{Format.ToString(server)}: Returned with success as {server.ServerType} {(server.IsReplica ? "replica" : "primary")} (Source: {task.Result})"); - // count the server types + // Count the server types switch (server.ServerType) { case ServerType.Twemproxy: @@ -1805,14 +1814,14 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP if (clusterCount > 0 && !encounteredConnectedClusterServer) { - // we have encountered a connected server with clustertype for the first time. + // We have encountered a connected server with a cluster type for the first time. // so we will get list of other nodes from this server using "CLUSTER NODES" command // and try to connect to these other nodes in the next iteration encounteredConnectedClusterServer = true; updatedClusterEndpointCollection = await GetEndpointsFromClusterNodes(server, log).ForAwait(); } - // set the server UnselectableFlags and update masters list + // Set the server UnselectableFlags and update masters list switch (server.ServerType) { case ServerType.Twemproxy: @@ -1853,13 +1862,13 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP } else { - break; // we do not want to repeat the second iteration + break; // We do not want to repeat the second iteration } } if (clusterCount == 0) { - // set the serverSelectionStrategy + // Set the serverSelectionStrategy if (RawConfig.Proxy == Proxy.Twemproxy) { ServerSelectionStrategy.ServerType = ServerType.Twemproxy; @@ -2007,7 +2016,8 @@ private static ServerEndPoint NominatePreferredMaster(LogProxy log, ServerEndPoi { Dictionary uniques = null; if (useTieBreakers) - { // count the votes + { + // Count the votes uniques = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < servers.Length; i++) { @@ -2118,14 +2128,15 @@ private static string DeDotifyHost(string input) { if (string.IsNullOrWhiteSpace(input)) return input; // GIGO - if (!char.IsLetter(input[0])) return input; // need first char to be alpha for this to work + if (!char.IsLetter(input[0])) return input; // Need first char to be alpha for this to work int periodPosition = input.IndexOf('.'); - if (periodPosition <= 0) return input; // no period or starts with a period? nothing useful to split + if (periodPosition <= 0) return input; // No period or starts with a period? Then nothing useful to split int colonPosition = input.IndexOf(':'); if (colonPosition > 0) - { // has a port specifier + { + // Has a port specifier return input.Substring(0, periodPosition) + input.Substring(colonPosition); } else @@ -2174,7 +2185,7 @@ private bool PrepareToPushMessageToBridge(Message message, ResultProcessor server = ServerSelectionStrategy.Select(message, allowDisconnected: true); } } - else // a server was specified; do we trust their choice, though? + else // A server was specified - do we trust their choice, though? { if (message.IsMasterOnly() && server.IsReplica) { @@ -2194,7 +2205,7 @@ private bool PrepareToPushMessageToBridge(Message message, ResultProcessor // If we're not allowed to queue while disconnected, we'll bomb out below. if (!server.IsConnected && !RawConfig.BacklogPolicy.QueueWhileDisconnected) { - // well, that's no use! + // Well, that's no use! server = null; } } @@ -2230,7 +2241,7 @@ private WriteResult TryPushMessageToBridgeSync(Message message, ResultProcess => PrepareToPushMessageToBridge(message, processor, resultBox, ref server) ? server.TryWriteSync(message) : WriteResult.NoConnectionAvailable; /// - /// See Object.ToString() + /// Gets the client name for this multiplexer. /// public override string ToString() { @@ -2239,13 +2250,13 @@ public override string ToString() return s; } - internal readonly byte[] ConfigurationChangedChannel; // this gets accessed for every received event; let's make sure we can process it "raw" - internal readonly byte[] UniqueId = Guid.NewGuid().ToByteArray(); // unique identifier used when tracing + internal readonly byte[] ConfigurationChangedChannel; // This gets accessed for every received event; let's make sure we can process it "raw" + internal readonly byte[] UniqueId = Guid.NewGuid().ToByteArray(); // Unique identifier used when tracing /// - /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order + /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order. /// - [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue), false)] + [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue) + ", will be removed in 3.0", false)] public bool PreserveAsyncOrder { get => false; @@ -2253,7 +2264,7 @@ public bool PreserveAsyncOrder } /// - /// Indicates whether any servers are connected + /// Indicates whether any servers are connected. /// public bool IsConnected { @@ -2267,7 +2278,7 @@ public bool IsConnected } /// - /// Indicates whether any servers are currently trying to connect + /// Indicates whether any servers are currently trying to connect. /// public bool IsConnecting { @@ -2290,11 +2301,9 @@ public bool IsConnecting internal ConnectionMultiplexer sentinelConnection = null; /// - /// Initializes the connection as a Sentinel connection and adds - /// the necessary event handlers to track changes to the managed - /// masters. + /// Initializes the connection as a Sentinel connection and adds the necessary event handlers to track changes to the managed primaries. /// - /// + /// The writer to log to, if any. internal void InitializeSentinel(LogProxy logProxy) { if (ServerSelectionStrategy.ServerType != ServerType.Sentinel) @@ -2336,8 +2345,7 @@ internal void InitializeSentinel(LogProxy logProxy) } // If we lose connection to a sentinel server, - // We need to reconfigure to make sure we still have - // a subscription to the +switch-master channel. + // we need to reconfigure to make sure we still have a subscription to the +switch-master channel ConnectionFailed += (sender, e) => { // Reconfigure to get subscriptions back online @@ -2356,16 +2364,17 @@ internal void InitializeSentinel(LogProxy logProxy) } /// - /// Returns a managed connection to the master server indicated by - /// the ServiceName in the config. + /// Returns a managed connection to the primary server indicated by the in the config. /// - /// the configuration to be used when connecting to the master - /// + /// The configuration to be used when connecting to the primary. + /// The writer to log to, if any. public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions config, TextWriter log = null) { if (ServerSelectionStrategy.ServerType != ServerType.Sentinel) + { throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, "Sentinel: The ConnectionMultiplexer is not a Sentinel connection. Detected as: " + ServerSelectionStrategy.ServerType); + } if (string.IsNullOrEmpty(config.ServiceName)) throw new ArgumentException("A ServiceName must be specified."); @@ -2395,7 +2404,7 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co EndPoint[] replicaEndPoints = GetReplicasForService(config.ServiceName) ?? GetReplicasForService(config.ServiceName); - // Replace the master endpoint, if we found another one + // Replace the primary endpoint, if we found another one // If not, assume the last state is the best we have and minimize the race if (config.EndPoints.Count == 1) { @@ -2448,6 +2457,7 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co return connection; } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "We don't care.")] internal void OnManagedConnectionRestored(object sender, ConnectionFailedEventArgs e) { ConnectionMultiplexer connection = (ConnectionMultiplexer)sender; @@ -2488,6 +2498,7 @@ internal void OnManagedConnectionRestored(object sender, ConnectionFailedEventAr } } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "We don't care.")] internal void OnManagedConnectionFailed(object sender, ConnectionFailedEventArgs e) { ConnectionMultiplexer connection = (ConnectionMultiplexer)sender; @@ -2543,9 +2554,9 @@ internal EndPoint[] GetReplicasForService(string serviceName) => /// /// Switches the SentinelMasterConnection over to a new master. /// - /// The endpoint responsible for the switch - /// The connection that should be switched over to a new master endpoint - /// Log to write to, if any + /// The endpoint responsible for the switch. + /// The connection that should be switched over to a new master endpoint. + /// The writer to log to, if any. internal void SwitchMaster(EndPoint switchBlame, ConnectionMultiplexer connection, TextWriter log = null) { if (log == null) log = TextWriter.Null; @@ -2597,8 +2608,7 @@ internal void UpdateSentinelAddressList(string serviceName) }) .FirstOrDefault(r => r != null); - // Ignore errors, as having an updated sentinel list is - // not essential + // Ignore errors, as having an updated sentinel list is not essential if (firstCompleteRequest == null) return; @@ -2617,7 +2627,7 @@ internal void UpdateSentinelAddressList(string serviceName) } /// - /// Close all connections and release all resources associated with this object + /// Close all connections and release all resources associated with this object. /// /// Whether to allow all in-queue commands to complete first. public void Close(bool allowCommandsToComplete = true) @@ -2677,7 +2687,7 @@ private Task[] QuitAllServers() } /// - /// Close all connections and release all resources associated with this object + /// Close all connections and release all resources associated with this object. /// /// Whether to allow all in-queue commands to complete first. public async Task CloseAsync(bool allowCommandsToComplete = true) @@ -2698,7 +2708,7 @@ public async Task CloseAsync(bool allowCommandsToComplete = true) } /// - /// Release all resources associated with this object + /// Release all resources associated with this object. /// public void Dispose() { @@ -2781,14 +2791,14 @@ internal T ExecuteSyncImpl(Message message, ResultProcessor processor, Ser { if (_isDisposed) throw new ObjectDisposedException(ToString()); - if (message == null) // fire-and forget could involve a no-op, represented by null - for example Increment by 0 + if (message == null) // Fire-and forget could involve a no-op, represented by null - for example Increment by 0 { return default(T); } if (message.IsFireAndForget) { -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Type or member is obsolete TryPushMessageToBridgeSync(message, processor, null, ref server); #pragma warning restore CS0618 Interlocked.Increment(ref fireAndForgets); @@ -2800,7 +2810,7 @@ internal T ExecuteSyncImpl(Message message, ResultProcessor processor, Ser lock (source) { -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Type or member is obsolete var result = TryPushMessageToBridgeSync(message, processor, source, ref server); #pragma warning restore CS0618 if (result != WriteResult.Success) @@ -2817,10 +2827,10 @@ internal T ExecuteSyncImpl(Message message, ResultProcessor processor, Ser Trace("Timeout performing " + message); Interlocked.Increment(ref syncTimeouts); throw ExceptionFactory.Timeout(this, null, message, server); - // very important not to return "source" to the pool here + // Very important not to return "source" to the pool here } } - // snapshot these so that we can recycle the box + // Snapshot these so that we can recycle the box var val = source.GetResult(out var ex, canRecycle: true); // now that we aren't locking it... if (ex != null) throw ex; Trace(message + " received " + val); @@ -2841,18 +2851,18 @@ internal T ExecuteSyncImpl(Message message, ResultProcessor processor, Ser internal int haveStormLog = 0; internal string stormLogSnapshot; /// - /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time; - /// set to a negative value to disable this feature) + /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time). + /// Set to a negative value to disable this feature. /// public int StormLogThreshold { get; set; } = 15; /// - /// Obtains the log of unusual busy patterns + /// Obtains the log of unusual busy patterns. /// public string GetStormLog() => Volatile.Read(ref stormLogSnapshot); /// - /// Resets the log of unusual busy patterns + /// Resets the log of unusual busy patterns. /// public void ResetStormLog() { @@ -2865,10 +2875,10 @@ public void ResetStormLog() internal void OnAsyncTimeout() => Interlocked.Increment(ref asyncTimeouts); /// - /// Request all compatible clients to reconfigure or reconnect + /// Sends request to all compatible clients to reconfigure or reconnect. /// - /// The command flags to use.2 - /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending) + /// The command flags to use. + /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending). public long PublishReconfigure(CommandFlags flags = CommandFlags.None) { byte[] channel = ConfigurationChangedChannel; @@ -2891,10 +2901,10 @@ private long PublishReconfigureImpl(CommandFlags flags) } /// - /// Request all compatible clients to reconfigure or reconnect + /// Sends request to all compatible clients to reconfigure or reconnect. /// /// The command flags to use. - /// The number of instances known to have received the message (however, the actual number can be higher) + /// The number of instances known to have received the message (however, the actual number can be higher). public Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None) { byte[] channel = ConfigurationChangedChannel; @@ -2904,7 +2914,8 @@ public Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None } /// - /// Get the hash-slot associated with a given key, if applicable; this can be useful for grouping operations + /// Get the hash-slot associated with a given key, if applicable. + /// This can be useful for grouping operations. /// /// The to determine the hash slot for. public int GetHashSlot(RedisKey key) => ServerSelectionStrategy.HashSlot(key); diff --git a/src/StackExchange.Redis/CursorEnumerable.cs b/src/StackExchange.Redis/CursorEnumerable.cs index 8e42b918e..12cb70fe7 100644 --- a/src/StackExchange.Redis/CursorEnumerable.cs +++ b/src/StackExchange.Redis/CursorEnumerable.cs @@ -10,7 +10,7 @@ namespace StackExchange.Redis { /// - /// Provides the ability to iterate over a cursor-based sequence of redis data, synchronously or asynchronously + /// Provides the ability to iterate over a cursor-based sequence of redis data, synchronously or asynchronously. /// internal abstract class CursorEnumerable : IEnumerable, IScanningCursor, IAsyncEnumerable { @@ -35,11 +35,12 @@ private protected CursorEnumerable(RedisBase redis, ServerEndPoint server, int d } /// - /// Gets an enumerator for the sequence + /// Gets an enumerator for the sequence. /// public Enumerator GetEnumerator() => new Enumerator(this, default); + /// - /// Gets an enumerator for the sequence + /// Gets an enumerator for the sequence. /// public Enumerator GetAsyncEnumerator(CancellationToken cancellationToken) => new Enumerator(this, cancellationToken); @@ -74,7 +75,7 @@ private protected virtual Task GetNextPageAsync(IScanningCursor obj, } /// - /// Provides the ability to iterate over a cursor-based sequence of redis data, synchronously or asynchronously + /// Provides the ability to iterate over a cursor-based sequence of redis data, synchronously or asynchronously. /// public class Enumerator : IEnumerator, IScanningCursor, IAsyncEnumerator { @@ -88,7 +89,7 @@ internal Enumerator(CursorEnumerable parent, CancellationToken cancellationTo } /// - /// Gets the current value of the enumerator + /// Gets the current value of the enumerator. /// public T Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -100,7 +101,7 @@ public T Current { } /// - /// Release all resources associated with this enumerator + /// Release all resources associated with this enumerator. /// public void Dispose() { @@ -122,7 +123,7 @@ private void SetComplete() } /// - /// Release all resources associated with this enumerator + /// Release all resources associated with this enumerator. /// public ValueTask DisposeAsync() { @@ -181,7 +182,7 @@ private void ProcessReply(in ScanResult result, bool isInitial) } /// - /// Try to move to the next item in the sequence + /// Try to move to the next item in the sequence. /// public bool MoveNext() => SimpleNext() || SlowNextSync(); @@ -199,7 +200,7 @@ private protected TResult Wait(Task pending, Message message) } /// - /// Try to move to the next item in the sequence + /// Try to move to the next item in the sequence. /// public ValueTask MoveNextAsync() { @@ -297,7 +298,7 @@ private static void Recycle(ref T[] array, ref bool isPooled) } /// - /// Reset the enumerator + /// Reset the enumerator. /// public void Reset() { @@ -323,8 +324,11 @@ public void Reset() int IScanningCursor.PageOffset => _pageOffset; } + /// + /// The cursor position. + /// /// - /// This may fail on cluster-proxy; I'm OK with this for now + /// This may fail on cluster-proxy - I'm OK with this for now. /// long IScanningCursor.Cursor => activeCursor?.Cursor ?? (long)initialCursor; diff --git a/src/StackExchange.Redis/EndPointCollection.cs b/src/StackExchange.Redis/EndPointCollection.cs index cf9fb6fc0..65b6791fb 100644 --- a/src/StackExchange.Redis/EndPointCollection.cs +++ b/src/StackExchange.Redis/EndPointCollection.cs @@ -7,35 +7,35 @@ namespace StackExchange.Redis { /// - /// A list of endpoints + /// A list of endpoints. /// public sealed class EndPointCollection : Collection, IEnumerable { /// - /// Create a new EndPointCollection + /// Create a new . /// public EndPointCollection() {} /// - /// Create a new EndPointCollection + /// Create a new . /// /// The endpoints to add to the collection. public EndPointCollection(IList endpoints) : base(endpoints) {} /// - /// Format an endpoint + /// Format an . /// /// The endpoint to get a string representation for. public static string ToString(EndPoint endpoint) => Format.ToString(endpoint); /// - /// Attempt to parse a string into an EndPoint + /// Attempt to parse a string into an . /// /// The endpoint string to parse. public static EndPoint TryParse(string endpoint) => Format.TryParseEndPoint(endpoint); /// - /// Adds a new endpoint to the list + /// Adds a new endpoint to the list. /// /// The host:port string to add an endpoint for to the collection. public void Add(string hostAndPort) @@ -66,7 +66,7 @@ public void Add(string hostAndPort) /// Try adding a new endpoint to the list. /// /// The endpoint to add. - /// True if the endpoint was added or false if not. + /// if the endpoint was added, if not. public bool TryAdd(EndPoint endpoint) { if (endpoint == null) @@ -86,7 +86,7 @@ public bool TryAdd(EndPoint endpoint) } /// - /// See Collection<T>.InsertItem() + /// See . /// /// The index to add into the collection at. /// The item to insert at . @@ -103,8 +103,9 @@ protected override void InsertItem(int index, EndPoint item) base.InsertItem(index, item); } + /// - /// See Collection<T>.SetItem() + /// See . /// /// The index to replace an endpoint at. /// The item to replace the existing endpoint at . diff --git a/src/StackExchange.Redis/EndPointEventArgs.cs b/src/StackExchange.Redis/EndPointEventArgs.cs index 69be9bdf3..cbd87c7af 100644 --- a/src/StackExchange.Redis/EndPointEventArgs.cs +++ b/src/StackExchange.Redis/EndPointEventArgs.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis { /// - /// Event information related to redis endpoints + /// Event information related to redis endpoints. /// public class EndPointEventArgs : EventArgs, ICompletable { @@ -29,16 +29,12 @@ public EndPointEventArgs(object sender, EndPoint endpoint) } /// - /// The endpoint involved in this event (this can be null) + /// The endpoint involved in this event (this can be null). /// public EndPoint EndPoint { get; } - void ICompletable.AppendStormLog(StringBuilder sb) - { - sb.Append("event, endpoint: "); - if (EndPoint == null) sb.Append("n/a"); - else sb.Append(Format.ToString(EndPoint)); - } + void ICompletable.AppendStormLog(StringBuilder sb) => + sb.Append("event, endpoint: ").Append(EndPoint != null ? Format.ToString(EndPoint) : "n/a"); bool ICompletable.TryComplete(bool isAsync) => ConnectionMultiplexer.TryCompleteHandler(handler, sender, this, isAsync); } diff --git a/src/StackExchange.Redis/Enums/Aggregate.cs b/src/StackExchange.Redis/Enums/Aggregate.cs index 662eca989..0c4d890fa 100644 --- a/src/StackExchange.Redis/Enums/Aggregate.cs +++ b/src/StackExchange.Redis/Enums/Aggregate.cs @@ -1,20 +1,20 @@ namespace StackExchange.Redis { /// - /// Specifies how elements should be aggregated when combining sorted sets + /// Specifies how elements should be aggregated when combining sorted sets. /// public enum Aggregate { /// - /// The values of the combined elements are added + /// The values of the combined elements are added. /// Sum, /// - /// The least value of the combined elements is used + /// The least value of the combined elements is used. /// Min, /// - /// The greatest value of the combined elements is used + /// The greatest value of the combined elements is used. /// Max } diff --git a/src/StackExchange.Redis/Enums/Bitwise.cs b/src/StackExchange.Redis/Enums/Bitwise.cs index ebaaceee9..ada2e99c5 100644 --- a/src/StackExchange.Redis/Enums/Bitwise.cs +++ b/src/StackExchange.Redis/Enums/Bitwise.cs @@ -20,6 +20,6 @@ public enum Bitwise /// /// Not /// - Not + Not, } } diff --git a/src/StackExchange.Redis/Enums/ClientFlags.cs b/src/StackExchange.Redis/Enums/ClientFlags.cs index 559a13799..1f400fc71 100644 --- a/src/StackExchange.Redis/Enums/ClientFlags.cs +++ b/src/StackExchange.Redis/Enums/ClientFlags.cs @@ -5,96 +5,156 @@ namespace StackExchange.Redis { /// /// The client flags can be a combination of: - /// O: the client is a replica in MONITOR mode - /// S: the client is a normal replica server - /// M: the client is a master - /// x: the client is in a MULTI/EXEC context - /// b: the client is waiting in a blocking operation - /// i: the client is waiting for a VM I/O (deprecated) - /// d: a watched keys has been modified - EXEC will fail - /// c: connection to be closed after writing entire reply - /// u: the client is unblocked - /// A: connection to be closed ASAP - /// N: no specific flag set + /// + /// + /// A + /// Connection to be closed ASAP. + /// + /// + /// b + /// The client is waiting in a blocking operation. + /// + /// + /// c + /// Connection to be closed after writing entire reply. + /// + /// + /// d + /// A watched keys has been modified - EXEC will fail. + /// + /// + /// i + /// The client is waiting for a VM I/O (deprecated). + /// + /// + /// M + /// The client is a primary. + /// + /// + /// N + /// No specific flag set. + /// + /// + /// O + /// The client is a replica in MONITOR mode. + /// + /// + /// P + /// The client is a Pub/Sub subscriber. + /// + /// + /// r + /// The client is in readonly mode against a cluster node. + /// + /// + /// S + /// The client is a normal replica server. + /// + /// + /// u + /// The client is unblocked. + /// + /// + /// U + /// The client is unblocked. + /// + /// + /// x + /// The client is in a MULTI/EXEC context. + /// + /// + /// t + /// The client enabled keys tracking in order to perform client side caching. + /// + /// + /// R + /// The client tracking target client is invalid. + /// + /// + /// B + /// The client enabled broadcast tracking mode. + /// + /// /// + /// https://redis.io/commands/client-list [Flags] [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "Compatibility")] public enum ClientFlags : long { /// - /// no specific flag set + /// No specific flag set. /// None = 0, /// - /// the client is a replica in MONITOR mode + /// The client is a replica in MONITOR mode. /// - [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicaMonitor) + " instead.")] + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicaMonitor) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] SlaveMonitor = 1, /// - /// the client is a replica in MONITOR mode + /// The client is a replica in MONITOR mode. /// ReplicaMonitor = 1, // as an implementation detail, note that enum.ToString on [Flags] prefers *later* options when naming Flags /// - /// the client is a normal replica server + /// The client is a normal replica server. /// - [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(Replica) + " instead.")] + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(Replica) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Slave = 2, /// - /// the client is a normal replica server + /// The client is a normal replica server. /// Replica = 2, // as an implementation detail, note that enum.ToString on [Flags] prefers *later* options when naming Flags /// - /// the client is a master + /// The client is a master. /// Master = 4, /// - /// the client is in a MULTI/EXEC context + /// The client is in a MULTI/EXEC context. /// Transaction = 8, /// - /// the client is waiting in a blocking operation + /// The client is waiting in a blocking operation. /// Blocked = 16, /// - /// a watched keys has been modified - EXEC will fail + /// A watched keys has been modified - EXEC will fail. /// TransactionDoomed = 32, /// - /// connection to be closed after writing entire reply + /// Connection to be closed after writing entire reply. /// Closing = 64, /// - /// the client is unblocked + /// The client is unblocked. /// Unblocked = 128, /// - /// connection to be closed ASAP + /// Connection to be closed ASAP. /// CloseASAP = 256, /// - /// the client is a Pub/Sub subscriber + /// The client is a Pub/Sub subscriber. /// PubSubSubscriber = 512, /// - /// the client is in readonly mode against a cluster node + /// The client is in readonly mode against a cluster node. /// ReadOnlyCluster = 1024, /// - /// the client is connected via a Unix domain socket + /// The client is connected via a Unix domain socket. /// UnixDomainSocket = 2048, /// - /// the client enabled keys tracking in order to perform client side caching + /// The client enabled keys tracking in order to perform client side caching. /// KeysTracking = 4096, /// - /// the client tracking target client is invalid + /// The client tracking target client is invalid. /// TrackingTargetInvalid = 8192, /// - /// the client enabled broadcast tracking mode + /// The client enabled broadcast tracking mode. /// BroadcastTracking = 16384, } diff --git a/src/StackExchange.Redis/Enums/ClientType.cs b/src/StackExchange.Redis/Enums/ClientType.cs index d7bff3f25..498c7dd70 100644 --- a/src/StackExchange.Redis/Enums/ClientType.cs +++ b/src/StackExchange.Redis/Enums/ClientType.cs @@ -16,11 +16,11 @@ public enum ClientType /// /// Replication connections /// - Replica = 1, // / as an implementation detail, note that enum.ToString without [Flags] preferes *earlier* values + Replica = 1, // as an implementation detail, note that enum.ToString without [Flags] prefers *earlier* values /// /// Replication connections /// - [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(Replica) + " instead.")] + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(Replica) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Slave = 1, /// diff --git a/src/StackExchange.Redis/Enums/CommandFlags.cs b/src/StackExchange.Redis/Enums/CommandFlags.cs index 074779fb4..9bcc1ffcd 100644 --- a/src/StackExchange.Redis/Enums/CommandFlags.cs +++ b/src/StackExchange.Redis/Enums/CommandFlags.cs @@ -3,7 +3,7 @@ namespace StackExchange.Redis { - /// + /// /// Behaviour markers associated with a given command /// [Flags] @@ -41,7 +41,7 @@ public enum CommandFlags /// This operation should be performed on the replica if it is available, but will be performed on /// a master if no replicas are available. Suitable for read operations only. /// - [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(PreferReplica) + " instead.")] + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(PreferReplica) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] PreferSlave = 8, @@ -59,7 +59,7 @@ public enum CommandFlags /// /// This operation should only be performed on a replica. Suitable for read operations only. /// - [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(DemandReplica) + " instead.")] + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(DemandReplica) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] DemandSlave = 12, diff --git a/src/StackExchange.Redis/Enums/CommandStatus.cs b/src/StackExchange.Redis/Enums/CommandStatus.cs index bc35845ae..f4cdd1810 100644 --- a/src/StackExchange.Redis/Enums/CommandStatus.cs +++ b/src/StackExchange.Redis/Enums/CommandStatus.cs @@ -1,12 +1,12 @@ namespace StackExchange.Redis { /// - /// track status of a command while communicating with Redis + /// Track status of a command while communicating with Redis. /// public enum CommandStatus { /// - /// command status unknown. + /// Command status unknown. /// Unknown, /// @@ -14,7 +14,7 @@ public enum CommandStatus /// WaitingToBeSent, /// - /// command has been sent to Redis. + /// Command has been sent to Redis. /// Sent, } diff --git a/src/StackExchange.Redis/Enums/ConnectionFailureType.cs b/src/StackExchange.Redis/Enums/ConnectionFailureType.cs index 9213de8b0..57958ecbc 100644 --- a/src/StackExchange.Redis/Enums/ConnectionFailureType.cs +++ b/src/StackExchange.Redis/Enums/ConnectionFailureType.cs @@ -1,48 +1,48 @@ namespace StackExchange.Redis { /// - /// The known types of connection failure + /// The known types of connection failure. /// public enum ConnectionFailureType { /// - /// This event is not a failure + /// This event is not a failure. /// None, /// - /// No viable connections were available for this operation + /// No viable connections were available for this operation. /// UnableToResolvePhysicalConnection, /// - /// The socket for this connection failed + /// The socket for this connection failed. /// SocketFailure, /// - /// Either SSL Stream or Redis authentication failed + /// Either SSL Stream or Redis authentication failed. /// AuthenticationFailure, /// - /// An unexpected response was received from the server + /// An unexpected response was received from the server. /// ProtocolFailure, /// - /// An unknown internal error occurred + /// An unknown internal error occurred. /// InternalFailure, /// - /// The socket was closed + /// The socket was closed. /// SocketClosed, /// - /// The socket was closed + /// The socket was closed. /// ConnectionDisposed, /// - /// The database is loading and is not available for use + /// The database is loading and is not available for use. /// Loading, /// - /// It has not been possible to create an intial connection to the redis server(s) + /// It has not been possible to create an initial connection to the redis server(s). /// UnableToConnect } diff --git a/src/StackExchange.Redis/Enums/ConnectionType.cs b/src/StackExchange.Redis/Enums/ConnectionType.cs index efba46677..ead82f222 100644 --- a/src/StackExchange.Redis/Enums/ConnectionType.cs +++ b/src/StackExchange.Redis/Enums/ConnectionType.cs @@ -1,20 +1,20 @@ namespace StackExchange.Redis { /// - /// The type of a connection + /// The type of a connection. /// public enum ConnectionType { /// - /// Not connection-type related + /// Not connection-type related. /// None = 0, /// - /// An interactive connection handles request/response commands for accessing data on demand + /// An interactive connection handles request/response commands for accessing data on demand. /// Interactive, /// - /// A subscriber connection recieves unsolicted messages from the server as pub/sub events occur + /// A subscriber connection receives unsolicited messages from the server as pub/sub events occur. /// Subscription } diff --git a/src/StackExchange.Redis/Enums/Exclude.cs b/src/StackExchange.Redis/Enums/Exclude.cs index b8a9db0b2..4da2e9ef4 100644 --- a/src/StackExchange.Redis/Enums/Exclude.cs +++ b/src/StackExchange.Redis/Enums/Exclude.cs @@ -4,25 +4,25 @@ namespace StackExchange.Redis { /// /// When performing a range query, by default the start / stop limits are inclusive; - /// however, both can also be specified separately as exclusive + /// however, both can also be specified separately as exclusive. /// [Flags] public enum Exclude { /// - /// Both start and stop are inclusive + /// Both start and stop are inclusive. /// None = 0, /// - /// Start is exclusive, stop is inclusive + /// Start is exclusive, stop is inclusive. /// Start = 1, /// - /// Start is inclusive, stop is exclusive + /// Start is inclusive, stop is exclusive. /// Stop = 2, /// - /// Both start and stop are exclusive + /// Both start and stop are exclusive. /// Both = Start | Stop } diff --git a/src/StackExchange.Redis/Enums/ExportOptions.cs b/src/StackExchange.Redis/Enums/ExportOptions.cs index c6891b537..fd29dd388 100644 --- a/src/StackExchange.Redis/Enums/ExportOptions.cs +++ b/src/StackExchange.Redis/Enums/ExportOptions.cs @@ -3,33 +3,33 @@ namespace StackExchange.Redis { /// - /// Which settings to export + /// Which settings to export. /// [Flags] public enum ExportOptions { /// - /// No options + /// No options. /// None = 0, /// - /// The output of INFO + /// The output of INFO. /// Info = 1, /// - /// The output of CONFIG GET * + /// The output of CONFIG GET *. /// Config = 2, /// - /// The output of CLIENT LIST + /// The output of CLIENT LIST. /// Client = 4, /// - /// The output of CLUSTER NODES + /// The output of CLUSTER NODES. /// Cluster = 8, /// - /// Everything available + /// Everything available. /// All = -1 } diff --git a/src/StackExchange.Redis/Enums/GeoUnit.cs b/src/StackExchange.Redis/Enums/GeoUnit.cs index a88e2deec..2d1c599c8 100644 --- a/src/StackExchange.Redis/Enums/GeoUnit.cs +++ b/src/StackExchange.Redis/Enums/GeoUnit.cs @@ -1,10 +1,7 @@ -using System; -using System.ComponentModel; - -namespace StackExchange.Redis +namespace StackExchange.Redis { /// - /// Units associated with Geo Commands + /// Units associated with Geo Commands. /// public enum GeoUnit { @@ -23,6 +20,6 @@ public enum GeoUnit /// /// Feet /// - Feet + Feet, } -} \ No newline at end of file +} diff --git a/src/StackExchange.Redis/Enums/MigrateOptions.cs b/src/StackExchange.Redis/Enums/MigrateOptions.cs index 68095bd77..561b5494d 100644 --- a/src/StackExchange.Redis/Enums/MigrateOptions.cs +++ b/src/StackExchange.Redis/Enums/MigrateOptions.cs @@ -3,13 +3,13 @@ namespace StackExchange.Redis { /// - /// Additional options for the MIGRATE command + /// Additional options for the MIGRATE command. /// [Flags] public enum MigrateOptions { /// - /// No options specified + /// No options specified. /// None = 0, /// @@ -19,6 +19,6 @@ public enum MigrateOptions /// /// Replace existing key on the remote instance. /// - Replace = 2 + Replace = 2, } } diff --git a/src/StackExchange.Redis/Enums/Order.cs b/src/StackExchange.Redis/Enums/Order.cs index 34f4cb36f..48364a7dd 100644 --- a/src/StackExchange.Redis/Enums/Order.cs +++ b/src/StackExchange.Redis/Enums/Order.cs @@ -1,17 +1,17 @@ namespace StackExchange.Redis { /// - /// The direction in which to sequence elements + /// The direction in which to sequence elements. /// public enum Order { /// - /// Ordered from low values to high values + /// Ordered from low values to high values. /// Ascending, /// - /// Ordered from high values to low values + /// Ordered from high values to low values. /// - Descending + Descending, } } diff --git a/src/StackExchange.Redis/Enums/PositionKind.cs b/src/StackExchange.Redis/Enums/PositionKind.cs index e52b7a17d..81a705090 100644 --- a/src/StackExchange.Redis/Enums/PositionKind.cs +++ b/src/StackExchange.Redis/Enums/PositionKind.cs @@ -4,6 +4,6 @@ internal enum PositionKind { Beginning = 0, Explicit = 1, - New = 2 + New = 2, } } diff --git a/src/StackExchange.Redis/Enums/Proxy.cs b/src/StackExchange.Redis/Enums/Proxy.cs index ed87ba495..64dcada45 100644 --- a/src/StackExchange.Redis/Enums/Proxy.cs +++ b/src/StackExchange.Redis/Enums/Proxy.cs @@ -1,17 +1,17 @@ namespace StackExchange.Redis { /// - /// Specifies the proxy that is being used to communicate to redis + /// Specifies the proxy that is being used to communicate to redis. /// public enum Proxy { /// - /// Direct communication to the redis server(s) + /// Direct communication to the redis server(s). /// None, /// - /// Communication via twemproxy + /// Communication via twemproxy. /// - Twemproxy + Twemproxy, } } diff --git a/src/StackExchange.Redis/Enums/RedisType.cs b/src/StackExchange.Redis/Enums/RedisType.cs index 54d49cd03..5072918d0 100644 --- a/src/StackExchange.Redis/Enums/RedisType.cs +++ b/src/StackExchange.Redis/Enums/RedisType.cs @@ -1,39 +1,49 @@ namespace StackExchange.Redis { /// - /// The intrinsinc data-types supported by redis + /// The intrinsic data-types supported by redis. /// /// https://redis.io/topics/data-types public enum RedisType { /// - /// The specified key does not exist + /// The specified key does not exist. /// None, /// - /// Strings are the most basic kind of Redis value. Redis Strings are binary safe, this means that a Redis string can contain any kind of data, for instance a JPEG image or a serialized Ruby object. + /// Strings are the most basic kind of Redis value. Redis Strings are binary safe, this means that + /// a Redis string can contain any kind of data, for instance a JPEG image or a serialized Ruby object. /// A String value can be at max 512 Megabytes in length. /// /// https://redis.io/commands#string String, /// - /// Redis Lists are simply lists of strings, sorted by insertion order. It is possible to add elements to a Redis List pushing new elements on the head (on the left) or on the tail (on the right) of the list. + /// Redis Lists are simply lists of strings, sorted by insertion order. + /// It is possible to add elements to a Redis List pushing new elements on the head (on the left) or + /// on the tail (on the right) of the list. /// /// https://redis.io/commands#list List, /// - /// Redis Sets are an unordered collection of Strings. It is possible to add, remove, and test for existence of members in O(1) (constant time regardless of the number of elements contained inside the Set). - /// Redis Sets have the desirable property of not allowing repeated members. Adding the same element multiple times will result in a set having a single copy of this element. Practically speaking this means that adding a member does not require a check if exists then add operation. + /// Redis Sets are an unordered collection of Strings. It is possible to add, remove, and test for + /// existence of members in O(1) (constant time regardless of the number of elements contained inside the Set). + /// Redis Sets have the desirable property of not allowing repeated members. + /// Adding the same element multiple times will result in a set having a single copy of this element. + /// Practically speaking this means that adding a member does not require a check if exists then add operation. /// /// https://redis.io/commands#set Set, /// - /// Redis Sorted Sets are, similarly to Redis Sets, non repeating collections of Strings. The difference is that every member of a Sorted Set is associated with score, that is used in order to take the sorted set ordered, from the smallest to the greatest score. While members are unique, scores may be repeated. + /// Redis Sorted Sets are, similarly to Redis Sets, non repeating collections of Strings. + /// The difference is that every member of a Sorted Set is associated with score, that is used + /// in order to take the sorted set ordered, from the smallest to the greatest score. + /// While members are unique, scores may be repeated. /// /// https://redis.io/commands#sorted_set SortedSet, /// - /// Redis Hashes are maps between string fields and string values, so they are the perfect data type to represent objects (eg: A User with a number of fields like name, surname, age, and so forth) + /// Redis Hashes are maps between string fields and string values, so they are the perfect data type + /// to represent objects (e.g. A User with a number of fields like name, surname, age, and so forth). /// /// https://redis.io/commands#hash Hash, @@ -45,7 +55,7 @@ public enum RedisType /// https://redis.io/commands#stream Stream, /// - /// The data-type was not recognised by the client library + /// The data-type was not recognised by the client library. /// Unknown, } diff --git a/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs b/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs index 4a76c0157..a8b8acbe1 100644 --- a/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs +++ b/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs @@ -4,36 +4,36 @@ namespace StackExchange.Redis { /// - /// Additional operations to perform when making a server a master + /// Additional operations to perform when making a server a master. /// [Flags] [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "Compatibility")] public enum ReplicationChangeOptions { /// - /// No additional operations + /// No additional operations. /// None = 0, /// - /// Set the tie-breaker key on all available masters, to specify this server + /// Set the tie-breaker key on all available masters, to specify this server. /// SetTiebreaker = 1, /// - /// Broadcast to the pub-sub channel to listening clients to reconfigure themselves + /// Broadcast to the pub-sub channel to listening clients to reconfigure themselves. /// Broadcast = 2, /// - /// Issue a REPLICAOF to all other known nodes, making this this master of all + /// Issue a REPLICAOF to all other known nodes, making this primary of all. /// - [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicateToOtherEndpoints) + " instead.")] + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicateToOtherEndpoints) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] EnslaveSubordinates = 4, /// - /// Issue a REPLICAOF to all other known nodes, making this this master of all + /// Issue a REPLICAOF to all other known nodes, making this primary of all. /// ReplicateToOtherEndpoints = 4, // note ToString prefers *later* options /// - /// All additional operations + /// All additional operations. /// All = SetTiebreaker | Broadcast | ReplicateToOtherEndpoints, } diff --git a/src/StackExchange.Redis/Enums/ResultType.cs b/src/StackExchange.Redis/Enums/ResultType.cs index e9a8c4e17..3ea559d0a 100644 --- a/src/StackExchange.Redis/Enums/ResultType.cs +++ b/src/StackExchange.Redis/Enums/ResultType.cs @@ -1,33 +1,33 @@ namespace StackExchange.Redis { /// - /// The underlying result type as defined by redis + /// The underlying result type as defined by Redis. /// public enum ResultType : byte { /// - /// No value was received + /// No value was received. /// None = 0, /// - /// Basic strings typically represent status results such as "OK" + /// Basic strings typically represent status results such as "OK". /// SimpleString = 1, /// - /// Error strings represent invalid operation results from the server + /// Error strings represent invalid operation results from the server. /// Error = 2, /// - /// Integers are returned for count operations and some integer-based increment operations + /// Integers are returned for count operations and some integer-based increment operations. /// Integer = 3, /// - /// Bulk strings represent typical user content values + /// Bulk strings represent typical user content values. /// BulkString = 4, /// - /// Multi-bulk replies represent complex results such as arrays + /// Multi-bulk replies represent complex results such as arrays. /// - MultiBulk = 5 + MultiBulk = 5, } } diff --git a/src/StackExchange.Redis/Enums/RetransmissionReasonType.cs b/src/StackExchange.Redis/Enums/RetransmissionReasonType.cs index 7fdf3847e..18529b029 100644 --- a/src/StackExchange.Redis/Enums/RetransmissionReasonType.cs +++ b/src/StackExchange.Redis/Enums/RetransmissionReasonType.cs @@ -13,16 +13,16 @@ public enum RetransmissionReasonType { /// - /// No stated reason + /// No stated reason. /// None = 0, /// - /// Issued to investigate which node owns a key + /// Issued to investigate which node owns a key. /// Ask, /// - /// A node has indicated that it does *not* own the given key + /// A node has indicated that it does *not* own the given key. /// - Moved + Moved, } } diff --git a/src/StackExchange.Redis/Enums/SaveType.cs b/src/StackExchange.Redis/Enums/SaveType.cs index 39325f3d7..ac2a88335 100644 --- a/src/StackExchange.Redis/Enums/SaveType.cs +++ b/src/StackExchange.Redis/Enums/SaveType.cs @@ -3,25 +3,30 @@ namespace StackExchange.Redis { /// - /// The type of save operation to perform + /// The type of save operation to perform. /// public enum SaveType { /// - /// Instruct Redis to start an Append Only File rewrite process. The rewrite will create a small optimized version of the current Append Only File. + /// Instruct Redis to start an Append Only File rewrite process. + /// The rewrite will create a small optimized version of the current Append Only File. /// /// https://redis.io/commands/bgrewriteaof BackgroundRewriteAppendOnlyFile, /// - /// Save the DB in background. The OK code is immediately returned. Redis forks, the parent continues to serve the clients, the child saves the DB on disk then exits. A client my be able to check if the operation succeeded using the LASTSAVE command. + /// Save the DB in background. The OK code is immediately returned. + /// Redis forks, the parent continues to serve the clients, the child saves the DB on disk then exits. + /// A client my be able to check if the operation succeeded using the LASTSAVE command. /// /// https://redis.io/commands/bgsave BackgroundSave, /// - /// Save the DB in foreground. This is almost never a good thing to do, and could cause significant blocking. Only do this if you know you need to save + /// Save the DB in foreground. + /// This is almost never a good thing to do, and could cause significant blocking. + /// Only do this if you know you need to save. /// /// https://redis.io/commands/save [Obsolete("Saving on the foreground can cause significant blocking; use with extreme caution")] - ForegroundSave + ForegroundSave, } } diff --git a/src/StackExchange.Redis/Enums/ServerType.cs b/src/StackExchange.Redis/Enums/ServerType.cs index 80072f34a..c3b6abaf4 100644 --- a/src/StackExchange.Redis/Enums/ServerType.cs +++ b/src/StackExchange.Redis/Enums/ServerType.cs @@ -1,25 +1,25 @@ namespace StackExchange.Redis { /// - /// Indicates the flavor of a particular redis server + /// Indicates the flavor of a particular redis server. /// public enum ServerType { /// - /// Classic redis-server server + /// Classic redis-server server. /// Standalone, /// - /// Monitoring/configuration redis-sentinel server + /// Monitoring/configuration redis-sentinel server. /// Sentinel, /// - /// Distributed redis-cluster server + /// Distributed redis-cluster server. /// Cluster, /// - /// Distributed redis installation via twemproxy + /// Distributed redis installation via twemproxy. /// - Twemproxy + Twemproxy, } } diff --git a/src/StackExchange.Redis/Enums/SetOperation.cs b/src/StackExchange.Redis/Enums/SetOperation.cs index fdb1acda4..b88b0c8f0 100644 --- a/src/StackExchange.Redis/Enums/SetOperation.cs +++ b/src/StackExchange.Redis/Enums/SetOperation.cs @@ -1,7 +1,7 @@ namespace StackExchange.Redis { /// - /// Describes an algebraic set operation that can be performed to combine multiple sets + /// Describes an algebraic set operation that can be performed to combine multiple sets. /// public enum SetOperation { @@ -16,6 +16,6 @@ public enum SetOperation /// /// Returns the members of the set resulting from the difference between the first set and all the successive sets. /// - Difference + Difference, } } diff --git a/src/StackExchange.Redis/Enums/ShutdownMode.cs b/src/StackExchange.Redis/Enums/ShutdownMode.cs index 3a0abacc3..dfd46b70f 100644 --- a/src/StackExchange.Redis/Enums/ShutdownMode.cs +++ b/src/StackExchange.Redis/Enums/ShutdownMode.cs @@ -1,21 +1,21 @@ namespace StackExchange.Redis { /// - /// Defines the persistence behaviour of the server during shutdown + /// Defines the persistence behaviour of the server during shutdown. /// public enum ShutdownMode { /// - /// The data is persisted if save points are configured + /// The data is persisted if save points are configured. /// Default, /// - /// The data is NOT persisted even if save points are configured + /// The data is NOT persisted even if save points are configured. /// Never, /// - /// The data is persisted even if save points are NOT configured + /// The data is persisted even if save points are NOT configured. /// - Always + Always, } } diff --git a/src/StackExchange.Redis/Enums/SortType.cs b/src/StackExchange.Redis/Enums/SortType.cs index a1a034fc6..9fc3a20ae 100644 --- a/src/StackExchange.Redis/Enums/SortType.cs +++ b/src/StackExchange.Redis/Enums/SortType.cs @@ -1,17 +1,18 @@ namespace StackExchange.Redis { /// - /// Specifies how to compare elements for sorting + /// Specifies how to compare elements for sorting. /// public enum SortType { /// - /// Elements are interpreted as a double-precision floating point number and sorted numerically + /// Elements are interpreted as a double-precision floating point number and sorted numerically. /// Numeric, /// - /// Elements are sorted using their alphabetic form (Redis is UTF-8 aware as long as the !LC_COLLATE environment variable is set at the server) + /// Elements are sorted using their alphabetic form + /// (Redis is UTF-8 aware as long as the !LC_COLLATE environment variable is set at the server). /// - Alphabetic + Alphabetic, } } diff --git a/src/StackExchange.Redis/Enums/When.cs b/src/StackExchange.Redis/Enums/When.cs index ed931298a..d0bc5d303 100644 --- a/src/StackExchange.Redis/Enums/When.cs +++ b/src/StackExchange.Redis/Enums/When.cs @@ -1,7 +1,7 @@ namespace StackExchange.Redis { /// - /// Indicates when this operation should be performed (only some variations are legal in a given context) + /// Indicates when this operation should be performed (only some variations are legal in a given context). /// public enum When { @@ -16,6 +16,6 @@ public enum When /// /// The operation should only occur when there is not an existing value. /// - NotExists + NotExists, } } diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 3f711ed0f..598517d4b 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -156,9 +156,7 @@ internal static Exception NoConnectionAvailable( return ex; } -#pragma warning disable RCS1231 // Make parameter ref read-only. - spans are tiny! internal static Exception PopulateInnerExceptions(ReadOnlySpan serverSnapshot) -#pragma warning restore RCS1231 // Make parameter ref read-only. { var innerExceptions = new List(); diff --git a/src/StackExchange.Redis/Exceptions.cs b/src/StackExchange.Redis/Exceptions.cs index aaacf4950..fe7da1424 100644 --- a/src/StackExchange.Redis/Exceptions.cs +++ b/src/StackExchange.Redis/Exceptions.cs @@ -1,7 +1,6 @@ using System; using System.Runtime.Serialization; -#pragma warning disable RCS1194 // Implement exception constructors. namespace StackExchange.Redis { /// diff --git a/src/StackExchange.Redis/ExponentialRetry.cs b/src/StackExchange.Redis/ExponentialRetry.cs index 31a909605..e957a3f19 100644 --- a/src/StackExchange.Redis/ExponentialRetry.cs +++ b/src/StackExchange.Redis/ExponentialRetry.cs @@ -13,7 +13,7 @@ public class ExponentialRetry : IReconnectRetryPolicy private static Random r; /// - /// Initializes a new instance using the specified back off interval with default maxDeltaBackOffMilliseconds of 10 seconds + /// Initializes a new instance using the specified back off interval with default maxDeltaBackOffMilliseconds of 10 seconds. /// /// time in milliseconds for the back-off interval between retries public ExponentialRetry(int deltaBackOffMilliseconds) : this(deltaBackOffMilliseconds, (int)TimeSpan.FromSeconds(10).TotalMilliseconds) {} @@ -21,8 +21,8 @@ public ExponentialRetry(int deltaBackOffMilliseconds) : this(deltaBackOffMillise /// /// Initializes a new instance using the specified back off interval. /// - /// time in milliseconds for the back-off interval between retries - /// time in milliseconds for the maximum value that the back-off interval can exponentially grow up to + /// time in milliseconds for the back-off interval between retries. + /// time in milliseconds for the maximum value that the back-off interval can exponentially grow up to. public ExponentialRetry(int deltaBackOffMilliseconds, int maxDeltaBackOffMilliseconds) { this.deltaBackOffMilliseconds = deltaBackOffMilliseconds; @@ -32,8 +32,8 @@ public ExponentialRetry(int deltaBackOffMilliseconds, int maxDeltaBackOffMillise /// /// This method is called by the ConnectionMultiplexer to determine if a reconnect operation can be retried now. /// - /// The number of times reconnect retries have already been made by the ConnectionMultiplexer while it was in the connecting state - /// Total elapsed time in milliseconds since the last reconnect retry was made + /// The number of times reconnect retries have already been made by the ConnectionMultiplexer while it was in the connecting state. + /// Total elapsed time in milliseconds since the last reconnect retry was made. public bool ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSinceLastRetry) { var exponential = (int)Math.Min(maxDeltaBackOffMilliseconds, deltaBackOffMilliseconds * Math.Pow(1.1, currentRetryCount)); diff --git a/src/StackExchange.Redis/ExtensionMethods.cs b/src/StackExchange.Redis/ExtensionMethods.cs index ed3424169..9fefeda06 100644 --- a/src/StackExchange.Redis/ExtensionMethods.cs +++ b/src/StackExchange.Redis/ExtensionMethods.cs @@ -11,7 +11,7 @@ namespace StackExchange.Redis { /// - /// Utility methods + /// Utility methods. /// public static class ExtensionMethods { @@ -188,7 +188,7 @@ public static Stream AsStream(this Lease bytes, bool ownsLease = true) public static string DecodeString(this Lease bytes, Encoding encoding = null) { if (bytes == null) return null; - if (encoding == null) encoding = Encoding.UTF8; + encoding ??= Encoding.UTF8; if (bytes.Length == 0) return ""; var segment = bytes.ArraySegment; return encoding.GetString(segment.Array, segment.Offset, segment.Count); @@ -202,7 +202,7 @@ public static string DecodeString(this Lease bytes, Encoding encoding = nu public static Lease DecodeLease(this Lease bytes, Encoding encoding = null) { if (bytes == null) return null; - if (encoding == null) encoding = Encoding.UTF8; + encoding ??= Encoding.UTF8; if (bytes.Length == 0) return Lease.Empty; var bytesSegment = bytes.ArraySegment; var charCount = encoding.GetCharCount(bytesSegment.Array, bytesSegment.Offset, bytesSegment.Count); diff --git a/src/StackExchange.Redis/GeoEntry.cs b/src/StackExchange.Redis/GeoEntry.cs index af3b9f646..acb75a31f 100644 --- a/src/StackExchange.Redis/GeoEntry.cs +++ b/src/StackExchange.Redis/GeoEntry.cs @@ -111,7 +111,7 @@ public GeoPosition(double longitude, double latitude) } /// - /// See . + /// A "{long} {lat}" string representation of this position. /// public override string ToString() => string.Format("{0} {1}", Longitude, Latitude); @@ -190,13 +190,11 @@ public GeoEntry(double longitude, double latitude, RedisValue member) public double Latitude => Position.Latitude; /// - /// See . + /// A "({Longitude},{Latitude})={Member}" string representation of this entry. /// public override string ToString() => $"({Longitude},{Latitude})={Member}"; - /// - /// See . - /// + /// public override int GetHashCode() => Position.GetHashCode() ^ Member.GetHashCode(); /// diff --git a/src/StackExchange.Redis/HashEntry.cs b/src/StackExchange.Redis/HashEntry.cs index 984e8f678..c0f89ca1a 100644 --- a/src/StackExchange.Redis/HashEntry.cs +++ b/src/StackExchange.Redis/HashEntry.cs @@ -40,27 +40,25 @@ public HashEntry(RedisValue name, RedisValue value) public RedisValue Key => name; /// - /// Converts to a key/value pair + /// Converts to a key/value pair. /// /// The to create a from. public static implicit operator KeyValuePair(HashEntry value) => new KeyValuePair(value.name, value.value); /// - /// Converts from a key/value pair + /// Converts from a key/value pair. /// /// The to get a from. public static implicit operator HashEntry(KeyValuePair value) => new HashEntry(value.Key, value.Value); /// - /// See . + /// A "{name}: {value}" string representation of this entry. /// public override string ToString() => name + ": " + value; - /// - /// See . - /// + /// public override int GetHashCode() => name.GetHashCode() ^ value.GetHashCode(); /// diff --git a/src/StackExchange.Redis/HashSlotMovedEventArgs.cs b/src/StackExchange.Redis/HashSlotMovedEventArgs.cs index 47b7ee0af..0204359d9 100644 --- a/src/StackExchange.Redis/HashSlotMovedEventArgs.cs +++ b/src/StackExchange.Redis/HashSlotMovedEventArgs.cs @@ -51,9 +51,6 @@ public HashSlotMovedEventArgs(object sender, int hashSlot, EndPoint old, EndPoin bool ICompletable.TryComplete(bool isAsync) => ConnectionMultiplexer.TryCompleteHandler(handler, sender, this, isAsync); - void ICompletable.AppendStormLog(StringBuilder sb) - { - sb.Append("event, slot-moved: ").Append(HashSlot); - } + void ICompletable.AppendStormLog(StringBuilder sb) => sb.Append("event, slot-moved: ").Append(HashSlot); } } diff --git a/src/StackExchange.Redis/Interfaces/IBatch.cs b/src/StackExchange.Redis/Interfaces/IBatch.cs index 34f125332..a0a71becb 100644 --- a/src/StackExchange.Redis/Interfaces/IBatch.cs +++ b/src/StackExchange.Redis/Interfaces/IBatch.cs @@ -1,8 +1,8 @@ namespace StackExchange.Redis { /// - /// Represents a block of operations that will be sent to the server together; - /// this can be useful to reduce packet fragmentation on slow connections - it + /// Represents a block of operations that will be sent to the server together. + /// This can be useful to reduce packet fragmentation on slow connections - it /// can improve the time to get *all* the operations processed, with the trade-off /// of a slower time to get the *first* operation processed; this is usually /// a good thing. Unless this batch is a transaction, there is no guarantee @@ -12,9 +12,8 @@ public interface IBatch : IDatabaseAsync { /// /// Execute the batch operation, sending all queued commands to the server. - /// Note that this operation is neither synchronous nor truly asynchronous - it - /// simply enqueues the buffered messages. To check on completion, you should - /// check the individual responses. + /// Note that this operation is neither synchronous nor truly asynchronous - it simply enqueues the buffered messages. + /// To check on completion, you should check the individual responses. /// void Execute(); } diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index f4770e4f8..1d11e3fbb 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -16,152 +16,152 @@ internal interface IInternalConnectionMultiplexer : IConnectionMultiplexer } /// - /// Represents the abstract multiplexer API + /// Represents the abstract multiplexer API. /// public interface IConnectionMultiplexer : IDisposable { /// - /// Gets the client-name that will be used on all new connections + /// Gets the client-name that will be used on all new connections. /// string ClientName { get; } /// - /// Gets the configuration of the connection + /// Gets the configuration of the connection. /// string Configuration { get; } /// - /// Gets the timeout associated with the connections + /// Gets the timeout associated with the connections. /// int TimeoutMilliseconds { get; } /// - /// The number of operations that have been performed on all connections + /// The number of operations that have been performed on all connections. /// long OperationCount { get; } /// - /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order + /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order. /// [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue), false)] bool PreserveAsyncOrder { get; set; } /// - /// Indicates whether any servers are connected + /// Indicates whether any servers are connected. /// bool IsConnected { get; } /// - /// Indicates whether any servers are connected + /// Indicates whether any servers are connecting. /// bool IsConnecting { get; } /// - /// Should exceptions include identifiable details? (key names, additional .Data annotations) + /// Should exceptions include identifiable details? (key names, additional annotations). /// bool IncludeDetailInExceptions { get; set; } /// - /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time; - /// set to a negative value to disable this feature) + /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time. + /// Set to a negative value to disable this feature). /// int StormLogThreshold { get; set; } /// - /// Register a callback to provide an on-demand ambient session provider based on the - /// calling context; the implementing code is responsible for reliably resolving the same provider - /// based on ambient context, or returning null to not profile + /// Register a callback to provide an on-demand ambient session provider based on the calling context. + /// The implementing code is responsible for reliably resolving the same provider + /// based on ambient context, or returning null to not profile. /// /// The profiling session provider. void RegisterProfiler(Func profilingSessionProvider); /// - /// Get summary statistics associates with this server + /// Get summary statistics associates with this server. /// ServerCounters GetCounters(); /// - /// A server replied with an error message; + /// A server replied with an error message. /// event EventHandler ErrorMessage; /// - /// Raised whenever a physical connection fails + /// Raised whenever a physical connection fails. /// event EventHandler ConnectionFailed; /// - /// Raised whenever an internal error occurs (this is primarily for debugging) + /// Raised whenever an internal error occurs (this is primarily for debugging). /// event EventHandler InternalError; /// - /// Raised whenever a physical connection is established + /// Raised whenever a physical connection is established. /// event EventHandler ConnectionRestored; /// - /// Raised when configuration changes are detected + /// Raised when configuration changes are detected. /// event EventHandler ConfigurationChanged; /// - /// Raised when nodes are explicitly requested to reconfigure via broadcast; - /// this usually means master/replica changes + /// Raised when nodes are explicitly requested to reconfigure via broadcast. + /// This usually means primary/replica role changes. /// event EventHandler ConfigurationChangedBroadcast; /// - /// Gets all endpoints defined on the server + /// Gets all endpoints defined on the multiplexer. /// /// Whether to return only the explicitly configured endpoints. EndPoint[] GetEndPoints(bool configuredOnly = false); /// - /// Wait for a given asynchronous operation to complete (or timeout) + /// Wait for a given asynchronous operation to complete (or timeout). /// /// The task to wait on. void Wait(Task task); /// - /// Wait for a given asynchronous operation to complete (or timeout) + /// Wait for a given asynchronous operation to complete (or timeout). /// /// The type in . /// The task to wait on. T Wait(Task task); /// - /// Wait for the given asynchronous operations to complete (or timeout) + /// Wait for the given asynchronous operations to complete (or timeout). /// /// The tasks to wait on. void WaitAll(params Task[] tasks); /// - /// Raised when a hash-slot has been relocated + /// Raised when a hash-slot has been relocated. /// event EventHandler HashSlotMoved; /// - /// Compute the hash-slot of a specified key + /// Compute the hash-slot of a specified key. /// /// The key to get a slot ID for. int HashSlot(RedisKey key); /// - /// Obtain a pub/sub subscriber connection to the specified server + /// Obtain a pub/sub subscriber connection to the specified server. /// /// The async state to pass to the created . ISubscriber GetSubscriber(object asyncState = null); /// - /// Obtain an interactive connection to a database inside redis + /// Obtain an interactive connection to a database inside redis. /// /// The database ID to get. /// The async state to pass to the created . IDatabase GetDatabase(int db = -1, object asyncState = null); /// - /// Obtain a configuration API for an individual server + /// Obtain a configuration API for an individual server. /// /// The host to get a server for. /// The specific port for to get a server for. @@ -169,98 +169,98 @@ public interface IConnectionMultiplexer : IDisposable IServer GetServer(string host, int port, object asyncState = null); /// - /// Obtain a configuration API for an individual server + /// Obtain a configuration API for an individual server. /// /// The "host:port" string to get a server for. /// The async state to pass to the created . IServer GetServer(string hostAndPort, object asyncState = null); /// - /// Obtain a configuration API for an individual server + /// Obtain a configuration API for an individual server. /// /// The host to get a server for. /// The specific port for to get a server for. IServer GetServer(IPAddress host, int port); /// - /// Obtain a configuration API for an individual server + /// Obtain a configuration API for an individual server. /// /// The endpoint to get a server for. /// The async state to pass to the created . IServer GetServer(EndPoint endpoint, object asyncState = null); /// - /// Reconfigure the current connections based on the existing configuration + /// Reconfigure the current connections based on the existing configuration. /// /// The log to write output to. Task ConfigureAsync(TextWriter log = null); /// - /// Reconfigure the current connections based on the existing configuration + /// Reconfigure the current connections based on the existing configuration. /// /// The log to write output to. bool Configure(TextWriter log = null); /// - /// Provides a text overview of the status of all connections + /// Provides a text overview of the status of all connections. /// string GetStatus(); /// - /// Provides a text overview of the status of all connections + /// Provides a text overview of the status of all connections. /// /// The log to write output to. void GetStatus(TextWriter log); /// - /// See Object.ToString() + /// See . /// string ToString(); /// - /// Close all connections and release all resources associated with this object + /// Close all connections and release all resources associated with this object. /// /// Whether to allow in-queue commands to complete first. void Close(bool allowCommandsToComplete = true); /// - /// Close all connections and release all resources associated with this object + /// Close all connections and release all resources associated with this object. /// /// Whether to allow in-queue commands to complete first. Task CloseAsync(bool allowCommandsToComplete = true); /// - /// Obtains the log of unusual busy patterns + /// Obtains the log of unusual busy patterns. /// string GetStormLog(); /// - /// Resets the log of unusual busy patterns + /// Resets the log of unusual busy patterns. /// void ResetStormLog(); /// - /// Request all compatible clients to reconfigure or reconnect + /// Request all compatible clients to reconfigure or reconnect. /// /// The command flags to use. - /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending) + /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending). long PublishReconfigure(CommandFlags flags = CommandFlags.None); /// - /// Request all compatible clients to reconfigure or reconnect + /// Request all compatible clients to reconfigure or reconnect. /// /// The command flags to use. - /// The number of instances known to have received the message (however, the actual number can be higher) + /// The number of instances known to have received the message (however, the actual number can be higher). Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None); /// - /// Get the hash-slot associated with a given key, if applicable; this can be useful for grouping operations + /// Get the hash-slot associated with a given key, if applicable; this can be useful for grouping operations. /// /// The key to get a the slot for. int GetHashSlot(RedisKey key); /// - /// Write the configuration of all servers to an output stream + /// Write the configuration of all servers to an output stream. /// /// The destination stream to write the export to. /// The options to use for this export. diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 3a9cc3e4e..e200e3cc3 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -31,7 +31,8 @@ public interface IDatabase : IRedis, IDatabaseAsync ITransaction CreateTransaction(object asyncState = null); /// - /// Atomically transfer a key from a source Redis instance to a destination Redis instance. On success the key is deleted from the original instance by default, and is guaranteed to exist in the target instance. + /// Atomically transfer a key from a source Redis instance to a destination Redis instance. + /// On success the key is deleted from the original instance by default, and is guaranteed to exist in the target instance. /// /// The key to migrate. /// The server to migrate the key to. @@ -43,7 +44,8 @@ public interface IDatabase : IRedis, IDatabaseAsync void KeyMigrate(RedisKey key, EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, MigrateOptions migrateOptions = MigrateOptions.None, CommandFlags flags = CommandFlags.None); /// - /// Returns the raw DEBUG OBJECT output for a key; this command is not fully documented and should be avoided unless you have good reason, and then avoided anyway. + /// Returns the raw DEBUG OBJECT output for a key. + /// This command is not fully documented and should be avoided unless you have good reason, and then avoided anyway. /// /// The key to debug. /// The flags to use for this migration. @@ -52,29 +54,35 @@ public interface IDatabase : IRedis, IDatabaseAsync RedisValue DebugObject(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Add the specified member to the set stored at key. Specified members that are already a member of this set are ignored. If key does not exist, a new set is created before adding the specified members. + /// Add the specified member to the set stored at key. + /// Specified members that are already a member of this set are ignored. + /// If key does not exist, a new set is created before adding the specified members. /// /// The key of the set. /// The longitude of geo entry. /// The latitude of the geo entry. /// The value to set at this entry. /// The flags to use for this operation. - /// True if the specified member was not already present in the set, else False. + /// if the specified member was not already present in the set, else . /// https://redis.io/commands/geoadd bool GeoAdd(RedisKey key, double longitude, double latitude, RedisValue member, CommandFlags flags = CommandFlags.None); /// - /// Add the specified member to the set stored at key. Specified members that are already a member of this set are ignored. If key does not exist, a new set is created before adding the specified members. + /// Add the specified member to the set stored at key. + /// Specified members that are already a member of this set are ignored. + /// If key does not exist, a new set is created before adding the specified members. /// /// The key of the set. /// The geo value to store. /// The flags to use for this operation. - /// True if the specified member was not already present in the set, else False + /// if the specified member was not already present in the set, else . /// https://redis.io/commands/geoadd bool GeoAdd(RedisKey key, GeoEntry value, CommandFlags flags = CommandFlags.None); /// - /// Add the specified members to the set stored at key. Specified members that are already a member of this set are ignored. If key does not exist, a new set is created before adding the specified members. + /// Add the specified members to the set stored at key. + /// Specified members that are already a member of this set are ignored. + /// If key does not exist, a new set is created before adding the specified members. /// /// The key of the set. /// The geo values add to the set. @@ -84,12 +92,13 @@ public interface IDatabase : IRedis, IDatabaseAsync long GeoAdd(RedisKey key, GeoEntry[] values, CommandFlags flags = CommandFlags.None); /// - /// Removes the specified member from the geo sorted set stored at key. Non existing members are ignored. + /// Removes the specified member from the geo sorted set stored at key. + /// Non existing members are ignored. /// /// The key of the set. /// The geo value to remove. /// The flags to use for this operation. - /// True if the member existed in the sorted set and was removed; False otherwise. + /// if the member existed in the sorted set and was removed, else . /// https://redis.io/commands/zrem bool GeoRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); @@ -101,7 +110,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The second member to check. /// The unit of distance to return (defaults to meters). /// The flags to use for this operation. - /// The command returns the distance as a double (represented as a string) in the specified unit, or NULL if one or both the elements are missing. + /// The command returns the distance as a double (represented as a string) in the specified unit, or if one or both the elements are missing. /// https://redis.io/commands/geodist double? GeoDistance(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None); @@ -131,7 +140,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the set. /// The members to get. /// The flags to use for this operation. - /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command.Non existing elements are reported as NULL elements of the array. + /// + /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command. + /// Non existing elements are reported as NULL elements of the array. + /// /// https://redis.io/commands/geopos GeoPosition?[] GeoPosition(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); @@ -141,12 +153,16 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the set. /// The member to get. /// The flags to use for this operation. - /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command.Non existing elements are reported as NULL elements of the array. + /// + /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command. + /// Non existing elements are reported as NULL elements of the array. + /// /// https://redis.io/commands/geopos GeoPosition? GeoPosition(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// - /// Return the members of a sorted set populated with geospatial information using GEOADD, which are within the borders of the area specified with the center location and the maximum distance from the center (the radius). + /// Return the members of a sorted set populated with geospatial information using GEOADD, which are + /// within the borders of the area specified with the center location and the maximum distance from the center (the radius). /// /// The key of the set. /// The member to get a radius of results from. @@ -161,7 +177,8 @@ public interface IDatabase : IRedis, IDatabaseAsync GeoRadiusResult[] GeoRadius(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); /// - /// Return the members of a sorted set populated with geospatial information using GEOADD, which are within the borders of the area specified with the center location and the maximum distance from the center (the radius). + /// Return the members of a sorted set populated with geospatial information using GEOADD, which are + /// within the borders of the area specified with the center location and the maximum distance from the center (the radius). /// /// The key of the set. /// The longitude of the point to get a radius of results from. @@ -177,7 +194,9 @@ public interface IDatabase : IRedis, IDatabaseAsync GeoRadiusResult[] GeoRadius(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); /// - /// Decrements the number stored at field in the hash stored at key by decrement. If key does not exist, a new key holding a hash is created. If field does not exist the value is set to 0 before the operation is performed. + /// Decrements the number stored at field in the hash stored at key by decrement. + /// If key does not exist, a new key holding a hash is created. + /// If field does not exist the value is set to 0 before the operation is performed. /// /// The key of the hash. /// The field in the hash to decrement. @@ -189,7 +208,8 @@ public interface IDatabase : IRedis, IDatabaseAsync long HashDecrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None); /// - /// Decrement the specified field of an hash stored at key, and representing a floating point number, by the specified decrement. If the field does not exist, it is set to 0 before performing the operation. + /// Decrement the specified field of an hash stored at key, and representing a floating point number, by the specified decrement. + /// If the field does not exist, it is set to 0 before performing the operation. /// /// The key of the hash. /// The field in the hash to decrement. @@ -201,7 +221,8 @@ public interface IDatabase : IRedis, IDatabaseAsync double HashDecrement(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None); /// - /// Removes the specified fields from the hash stored at key. Non-existing fields are ignored. Non-existing keys are treated as empty hashes and this command returns 0. + /// Removes the specified fields from the hash stored at key. + /// Non-existing fields are ignored. Non-existing keys are treated as empty hashes and this command returns 0. /// /// The key of the hash. /// The field in the hash to delete. @@ -211,7 +232,8 @@ public interface IDatabase : IRedis, IDatabaseAsync bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// - /// Removes the specified fields from the hash stored at key. Non-existing fields are ignored. Non-existing keys are treated as empty hashes and this command returns 0. + /// Removes the specified fields from the hash stored at key. + /// Non-existing fields are ignored. Non-existing keys are treated as empty hashes and this command returns 0. /// /// The key of the hash. /// The fields in the hash to delete. @@ -226,7 +248,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the hash. /// The field in the hash to check. /// The flags to use for this operation. - /// 1 if the hash contains field. 0 if the hash does not contain field, or key does not exist. + /// if the hash contains field, if the hash does not contain field, or key does not exist. /// https://redis.io/commands/hexists bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); @@ -271,7 +293,9 @@ public interface IDatabase : IRedis, IDatabaseAsync HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Increments the number stored at field in the hash stored at key by increment. If key does not exist, a new key holding a hash is created. If field does not exist the value is set to 0 before the operation is performed. + /// Increments the number stored at field in the hash stored at key by increment. + /// If key does not exist, a new key holding a hash is created. + /// If field does not exist the value is set to 0 before the operation is performed. /// /// The key of the hash. /// The field in the hash to increment. @@ -283,7 +307,8 @@ public interface IDatabase : IRedis, IDatabaseAsync long HashIncrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None); /// - /// Increment the specified field of an hash stored at key, and representing a floating point number, by the specified increment. If the field does not exist, it is set to 0 before performing the operation. + /// Increment the specified field of an hash stored at key, and representing a floating point number, by the specified increment. + /// If the field does not exist, it is set to 0 before performing the operation. /// /// The key of the hash. /// The field in the hash to increment. @@ -324,7 +349,8 @@ public interface IDatabase : IRedis, IDatabaseAsync IEnumerable HashScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags); /// - /// The HSCAN command is used to incrementally iterate over a hash; note: to resume an iteration via cursor, cast the original enumerable or enumerator to IScanningCursor. + /// The HSCAN command is used to incrementally iterate over a hash. + /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to . /// /// The key of the hash. /// The pattern of keys to get entries for. @@ -337,7 +363,9 @@ public interface IDatabase : IRedis, IDatabaseAsync IEnumerable HashScan(RedisKey key, RedisValue pattern = default(RedisValue), int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// - /// Sets the specified fields to their respective values in the hash stored at key. This command overwrites any specified fields that already exist in the hash, leaving other unspecified fields untouched. If key does not exist, a new key holding a hash is created. + /// Sets the specified fields to their respective values in the hash stored at key. + /// This command overwrites any specified fields that already exist in the hash, leaving other unspecified fields untouched. + /// If key does not exist, a new key holding a hash is created. /// /// The key of the hash. /// The entries to set in the hash. @@ -346,14 +374,16 @@ public interface IDatabase : IRedis, IDatabaseAsync void HashSet(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None); /// - /// Sets field in the hash stored at key to value. If key does not exist, a new key holding a hash is created. If field already exists in the hash, it is overwritten. + /// Sets field in the hash stored at key to value. + /// If key does not exist, a new key holding a hash is created. + /// If field already exists in the hash, it is overwritten. /// /// The key of the hash. /// The field to set in the hash. /// The value to set. /// Which conditions under which to set the field value (defaults to always). /// The flags to use for this operation. - /// True if field is a new field in the hash and value was set. False if field already exists in the hash and the value was updated. + /// if field is a new field in the hash and value was set, if field already exists in the hash and the value was updated. /// https://redis.io/commands/hset /// https://redis.io/commands/hsetnx bool HashSet(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); @@ -364,7 +394,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the hash. /// The field containing the string /// The flags to use for this operation. - /// the length of the string at field, or 0 when key does not exist. + /// The length of the string at field, or 0 when key does not exist. /// https://redis.io/commands/hstrlen long HashStringLength(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); @@ -383,7 +413,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the hyperloglog. /// The value to add. /// The flags to use for this operation. - /// True if at least 1 HyperLogLog internal register was altered, false otherwise. + /// if at least 1 HyperLogLog internal register was altered, otherwise. /// https://redis.io/commands/pfadd bool HyperLogLogAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); @@ -393,7 +423,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the hyperloglog. /// The values to add. /// The flags to use for this operation. - /// True if at least 1 HyperLogLog internal register was altered, false otherwise. + /// if at least 1 HyperLogLog internal register was altered, otherwise. /// https://redis.io/commands/pfadd bool HyperLogLogAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); @@ -448,7 +478,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The key to delete. /// The flags to use for this operation. - /// True if the key was removed. + /// if the key was removed. /// https://redis.io/commands/del /// https://redis.io/commands/unlink bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -465,11 +495,12 @@ public interface IDatabase : IRedis, IDatabaseAsync long KeyDelete(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Serialize the value stored at key in a Redis-specific format and return it to the user. The returned value can be synthesized back into a Redis key using the RESTORE command. + /// Serialize the value stored at key in a Redis-specific format and return it to the user. + /// The returned value can be synthesized back into a Redis key using the RESTORE command. /// /// The key to dump. /// The flags to use for this operation. - /// the serialized value. + /// The serialized value. /// https://redis.io/commands/dump byte[] KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -478,7 +509,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The key to check. /// The flags to use for this operation. - /// 1 if the key exists. 0 if the key does not exist. + /// if the key exists. if the key does not exist. /// https://redis.io/commands/exists bool KeyExists(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -492,49 +523,71 @@ public interface IDatabase : IRedis, IDatabaseAsync long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Set a timeout on key. After the timeout has expired, the key will automatically be deleted. A key with an associated timeout is said to be volatile in Redis terminology. + /// Set a timeout on key. After the timeout has expired, the key will automatically be deleted. + /// A key with an associated timeout is said to be volatile in Redis terminology. /// /// The key to set the expiration for. /// The timeout to set. /// The flags to use for this operation. - /// 1 if the timeout was set. 0 if key does not exist or the timeout could not be set. - /// If key is updated before the timeout has expired, then the timeout is removed as if the PERSIST command was invoked on key. - /// For Redis versions < 2.1.3, existing timeouts cannot be overwritten. So, if key already has an associated timeout, it will do nothing and return 0. Since Redis 2.1.3, you can update the timeout of a key. It is also possible to remove the timeout using the PERSIST command. See the page on key expiry for more information. + /// if the timeout was set. if key does not exist or the timeout could not be set. + /// + /// If key is updated before the timeout has expired, then the timeout is removed as if the PERSIST command was invoked on key. + /// + /// For Redis versions < 2.1.3, existing timeouts cannot be overwritten. + /// So, if key already has an associated timeout, it will do nothing and return 0. + /// + /// + /// Since Redis 2.1.3, you can update the timeout of a key. + /// It is also possible to remove the timeout using the PERSIST command. See the page on key expiry for more information. + /// + /// /// https://redis.io/commands/expire /// https://redis.io/commands/pexpire /// https://redis.io/commands/persist bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); /// - /// Set a timeout on key. After the timeout has expired, the key will automatically be deleted. A key with an associated timeout is said to be volatile in Redis terminology. + /// Set a timeout on key. After the timeout has expired, the key will automatically be deleted. + /// A key with an associated timeout is said to be volatile in Redis terminology. /// /// The key to set the expiration for. /// The exact date to expiry to set. /// The flags to use for this operation. - /// 1 if the timeout was set. 0 if key does not exist or the timeout could not be set. - /// If key is updated before the timeout has expired, then the timeout is removed as if the PERSIST command was invoked on key. - /// For Redis versions < 2.1.3, existing timeouts cannot be overwritten. So, if key already has an associated timeout, it will do nothing and return 0. Since Redis 2.1.3, you can update the timeout of a key. It is also possible to remove the timeout using the PERSIST command. See the page on key expiry for more information. + /// if the timeout was set. if key does not exist or the timeout could not be set. + /// + /// If key is updated before the timeout has expired, then the timeout is removed as if the PERSIST command was invoked on key. + /// + /// For Redis versions < 2.1.3, existing timeouts cannot be overwritten. + /// So, if key already has an associated timeout, it will do nothing and return 0. + /// + /// + /// Since Redis 2.1.3, you can update the timeout of a key. + /// It is also possible to remove the timeout using the PERSIST command. See the page on key expiry for more information. + /// + /// /// https://redis.io/commands/expireat /// https://redis.io/commands/pexpireat /// https://redis.io/commands/persist bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None); /// - /// Returns the time since the object stored at the specified key is idle (not requested by read or write operations) + /// Returns the time since the object stored at the specified key is idle (not requested by read or write operations). /// /// The key to get the time of. /// The flags to use for this operation. - /// The time since the object stored at the specified key is idle + /// The time since the object stored at the specified key is idle. /// https://redis.io/commands/object TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Move key from the currently selected database (see SELECT) to the specified destination database. When key already exists in the destination database, or it does not exist in the source database, it does nothing. It is possible to use MOVE as a locking primitive because of this. + /// Move key from the currently selected database (see SELECT) to the specified destination database. + /// When key already exists in the destination database, or it does not exist in the source database, it does nothing. + /// It is possible to use MOVE as a locking primitive because of this. /// /// The key to move. /// The database to move the key to. /// The flags to use for this operation. - /// 1 if key was moved; 0 if key was not moved. + /// if key was moved. if key was not moved. /// https://redis.io/commands/move bool KeyMove(RedisKey key, int database, CommandFlags flags = CommandFlags.None); @@ -543,7 +596,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The key to persist. /// The flags to use for this operation. - /// 1 if the timeout was removed. 0 if key does not exist or does not have an associated timeout. + /// if the timeout was removed. if key does not exist or does not have an associated timeout. /// https://redis.io/commands/persist bool KeyPersist(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -556,20 +609,21 @@ public interface IDatabase : IRedis, IDatabaseAsync RedisKey KeyRandom(CommandFlags flags = CommandFlags.None); /// - /// Renames key to newkey. It returns an error when the source and destination names are the same, or when key does not exist. + /// Renames to . + /// It returns an error when the source and destination names are the same, or when key does not exist. /// /// The key to rename. /// The key to rename to. /// What conditions to rename under (defaults to always). /// The flags to use for this operation. - /// True if the key was renamed, false otherwise. + /// if the key was renamed, otherwise. /// https://redis.io/commands/rename /// https://redis.io/commands/renamenx bool KeyRename(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None); /// /// Create a key associated with a value that is obtained by deserializing the provided serialized value (obtained via DUMP). - /// If ttl is 0 the key is created without any expire, otherwise the specified expire time(in milliseconds) is set. + /// If is 0 the key is created without any expire, otherwise the specified expire time (in milliseconds) is set. /// /// The key to restore. /// The value of the key. @@ -579,7 +633,8 @@ public interface IDatabase : IRedis, IDatabaseAsync void KeyRestore(RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None); /// - /// Returns the remaining time to live of a key that has a timeout. This introspection capability allows a Redis client to check how many seconds a given key will continue to be part of the dataset. + /// Returns the remaining time to live of a key that has a timeout. + /// This introspection capability allows a Redis client to check how many seconds a given key will continue to be part of the dataset. /// /// The key to check. /// The flags to use for this operation. @@ -588,7 +643,8 @@ public interface IDatabase : IRedis, IDatabaseAsync TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Returns the string representation of the type of the value stored at key. The different types that can be returned are: string, list, set, zset and hash. + /// Returns the string representation of the type of the value stored at key. + /// The different types that can be returned are: string, list, set, zset and hash. /// /// The key to get the type of. /// The flags to use for this operation. @@ -597,10 +653,13 @@ public interface IDatabase : IRedis, IDatabaseAsync RedisType KeyType(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Returns the element at index index in the list stored at key. The index is zero-based, so 0 means the first element, 1 the second element and so on. Negative indices can be used to designate elements starting at the tail of the list. Here, -1 means the last element, -2 means the penultimate and so forth. + /// Returns the element at index in the list stored at key. + /// The index is zero-based, so 0 means the first element, 1 the second element and so on. + /// Negative indices can be used to designate elements starting at the tail of the list. + /// Here, -1 means the last element, -2 means the penultimate and so forth. /// /// The key of the list. - /// The index position to ge the value at. + /// The index position to get the value at. /// The flags to use for this operation. /// The requested element, or nil when index is out of range. /// https://redis.io/commands/lindex @@ -640,18 +699,19 @@ public interface IDatabase : IRedis, IDatabaseAsync RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Removes and returns count elements from the tail of the list stored at key. - /// If there are less elements in the list than count, removes and returns all the elements in the list. + /// Removes and returns count elements from the head of the list stored at key. + /// If the list contains less than count elements, removes and returns the number of elements in the list. /// /// The key of the list. - /// The number of items to remove. + /// The number of elements to remove /// The flags to use for this operation. /// Array of values that were popped, or nil if the key doesn't exist. /// https://redis.io/commands/lpop RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// - /// Insert the specified value at the head of the list stored at key. If key does not exist, it is created as empty list before performing the push operations. + /// Insert the specified value at the head of the list stored at key. + /// If key does not exist, it is created as empty list before performing the push operations. /// /// The key of the list. /// The value to add to the head of the list. @@ -663,10 +723,11 @@ public interface IDatabase : IRedis, IDatabaseAsync long ListLeftPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); /// - /// Insert the specified value at the head of the list stored at key. If key does not exist, it is created as empty list before performing the push operations. + /// Insert the specified value at the head of the list stored at key. + /// If key does not exist, it is created as empty list before performing the push operations. /// /// The key of the list. - /// The values to add to the head of the list. + /// The value to add to the head of the list. /// Which conditions to add to the list under (defaults to always). /// The flags to use for this operation. /// The length of the list after the push operations. @@ -675,8 +736,10 @@ public interface IDatabase : IRedis, IDatabaseAsync long ListLeftPush(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); /// - /// Insert all the specified values at the head of the list stored at key. If key does not exist, it is created as empty list before performing the push operations. - /// Elements are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. So for instance the command LPUSH mylist a b c will result into a list containing c as first element, b as second element and a as third element. + /// Insert all the specified values at the head of the list stored at key. + /// If key does not exist, it is created as empty list before performing the push operations. + /// Elements are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. + /// So for instance the command LPUSH mylist a b c will result into a list containing c as first element, b as second element and a as third element. /// /// The key of the list. /// The values to add to the head of the list. @@ -695,7 +758,8 @@ public interface IDatabase : IRedis, IDatabaseAsync long ListLength(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Returns the specified elements of the list stored at key. The offsets start and stop are zero-based indexes, with 0 being the first element of the list (the head of the list), 1 being the next element and so on. + /// Returns the specified elements of the list stored at key. + /// The offsets start and stop are zero-based indexes, with 0 being the first element of the list (the head of the list), 1 being the next element and so on. /// These offsets can also be negative numbers indicating offsets starting at the end of the list.For example, -1 is the last element of the list, -2 the penultimate, and so on. /// Note that if you have a list of numbers from 0 to 100, LRANGE list 0 10 will return 11 elements, that is, the rightmost item is included. /// @@ -708,10 +772,13 @@ public interface IDatabase : IRedis, IDatabaseAsync RedisValue[] ListRange(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None); /// - /// Removes the first count occurrences of elements equal to value from the list stored at key. The count argument influences the operation in the following ways: - /// count > 0: Remove elements equal to value moving from head to tail. - /// count < 0: Remove elements equal to value moving from tail to head. - /// count = 0: Remove all elements equal to value. + /// Removes the first count occurrences of elements equal to value from the list stored at key. + /// The count argument influences the operation in the following ways: + /// + /// count > 0: Remove elements equal to value moving from head to tail. + /// count < 0: Remove elements equal to value moving from tail to head. + /// count = 0: Remove all elements equal to value. + /// /// /// The key of the list. /// The value to remove from the list. @@ -731,11 +798,11 @@ public interface IDatabase : IRedis, IDatabaseAsync RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Removes and returns count elements from the head of the list stored at key. - /// If there are less elements in the list than count, removes and returns all the elements in the list. + /// Removes and returns count elements from the end the list stored at key. + /// If the list contains less than count elements, removes and returns the number of elements in the list. /// /// The key of the list. - /// tThe number of items to remove. + /// The number of elements to pop /// The flags to use for this operation. /// Array of values that were popped, or nil if the key doesn't exist. /// https://redis.io/commands/rpop @@ -752,7 +819,8 @@ public interface IDatabase : IRedis, IDatabaseAsync RedisValue ListRightPopLeftPush(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None); /// - /// Insert the specified value at the tail of the list stored at key. If key does not exist, it is created as empty list before performing the push operation. + /// Insert the specified value at the tail of the list stored at key. + /// If key does not exist, it is created as empty list before performing the push operation. /// /// The key of the list. /// The value to add to the tail of the list. @@ -764,7 +832,8 @@ public interface IDatabase : IRedis, IDatabaseAsync long ListRightPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); /// - /// Insert the specified value at the tail of the list stored at key. If key does not exist, it is created as empty list before performing the push operation. + /// Insert the specified value at the tail of the list stored at key. + /// If key does not exist, it is created as empty list before performing the push operation. /// /// The key of the list. /// The values to add to the tail of the list. @@ -776,8 +845,10 @@ public interface IDatabase : IRedis, IDatabaseAsync long ListRightPush(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); /// - /// Insert all the specified values at the tail of the list stored at key. If key does not exist, it is created as empty list before performing the push operation. - /// Elements are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. So for instance the command RPUSH mylist a b c will result into a list containing a as first element, b as second element and c as third element. + /// Insert all the specified values at the tail of the list stored at key. + /// If key does not exist, it is created as empty list before performing the push operation. + /// Elements are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. + /// So for instance the command RPUSH mylist a b c will result into a list containing a as first element, b as second element and c as third element. /// /// The key of the list. /// The values to add to the tail of the list. @@ -787,7 +858,9 @@ public interface IDatabase : IRedis, IDatabaseAsync long ListRightPush(RedisKey key, RedisValue[] values, CommandFlags flags); /// - /// Sets the list element at index to value. For more information on the index argument, see ListGetByIndex. An error is returned for out of range indexes. + /// Sets the list element at index to value. + /// For more information on the index argument, see . + /// An error is returned for out of range indexes. /// /// The key of the list. /// The index to set the value at. @@ -797,7 +870,8 @@ public interface IDatabase : IRedis, IDatabaseAsync void ListSetByIndex(RedisKey key, long index, RedisValue value, CommandFlags flags = CommandFlags.None); /// - /// Trim an existing list so that it will contain only the specified range of elements specified. Both start and stop are zero-based indexes, where 0 is the first element of the list (the head), 1 the next element and so on. + /// Trim an existing list so that it will contain only the specified range of elements specified. + /// Both start and stop are zero-based indexes, where 0 is the first element of the list (the head), 1 the next element and so on. /// For example: LTRIM foobar 0 2 will modify the list stored at foobar so that only the first three elements of the list will remain. /// start and end can also be negative numbers indicating offsets from the end of the list, where -1 is the last element of the list, -2 the penultimate element and so on. /// @@ -815,7 +889,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The value to set at the key. /// The expiration of the lock key. /// The flags to use for this operation. - /// True if the lock was successfully extended. + /// if the lock was successfully extended. bool LockExtend(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None); /// @@ -830,9 +904,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// Releases a lock, if the token value is correct. /// /// The key of the lock. - /// The value at the key tht must match. + /// The value at the key that must match. /// The flags to use for this operation. - /// True if the lock was successfully released, false otherwise. + /// if the lock was successfully released, otherwise. bool LockRelease(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -842,7 +916,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The value to set at the key. /// The expiration of the lock key. /// The flags to use for this operation. - /// True if the lock was successfully taken, false otherwise. + /// if the lock was successfully taken, otherwise. bool LockTake(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None); /// @@ -859,26 +933,24 @@ public interface IDatabase : IRedis, IDatabaseAsync long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None); /// - /// Execute an arbitrary command against the server; this is primarily intended for - /// executing modules, but may also be used to provide access to new features that lack - /// a direct API. + /// Execute an arbitrary command against the server; this is primarily intended for executing modules, + /// but may also be used to provide access to new features that lack a direct API. /// /// The command to run. /// The arguments to pass for the command. - /// This API should be considered an advanced feature; inappropriate use can be harmful - /// A dynamic representation of the command's result + /// This API should be considered an advanced feature; inappropriate use can be harmful. + /// A dynamic representation of the command's result. RedisResult Execute(string command, params object[] args); /// - /// Execute an arbitrary command against the server; this is primarily intended for - /// executing modules, but may also be used to provide access to new features that lack - /// a direct API. + /// Execute an arbitrary command against the server; this is primarily intended for executing modules, + /// but may also be used to provide access to new features that lack a direct API. /// /// The command to run. /// The arguments to pass for the command. /// The flags to use for this operation. - /// This API should be considered an advanced feature; inappropriate use can be harmful - /// A dynamic representation of the command's result + /// This API should be considered an advanced feature; inappropriate use can be harmful. + /// A dynamic representation of the command's result. RedisResult Execute(string command, ICollection args, CommandFlags flags = CommandFlags.None); /// @@ -888,19 +960,19 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The keys to execute against. /// The values to execute against. /// The flags to use for this operation. - /// A dynamic representation of the script's result + /// A dynamic representation of the script's result. /// https://redis.io/commands/eval /// https://redis.io/commands/evalsha RedisResult ScriptEvaluate(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None); /// - /// Execute a Lua script against the server using just the SHA1 hash + /// Execute a Lua script against the server using just the SHA1 hash. /// /// The hash of the script to execute. /// The keys to execute against. /// The values to execute against. /// The flags to use for this operation. - /// A dynamic representation of the script's result + /// A dynamic representation of the script's result. /// https://redis.io/commands/evalsha RedisResult ScriptEvaluate(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None); @@ -911,7 +983,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The script to execute. /// The parameters to pass to the script. /// The flags to use for this operation. - /// A dynamic representation of the script's result + /// A dynamic representation of the script's result. /// https://redis.io/commands/eval RedisResult ScriptEvaluate(LuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None); @@ -923,7 +995,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The already-loaded script to execute. /// The parameters to pass to the script. /// The flags to use for this operation. - /// A dynamic representation of the script's result + /// A dynamic representation of the script's result. /// https://redis.io/commands/eval RedisResult ScriptEvaluate(LoadedLuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None); @@ -935,7 +1007,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the set. /// The value to add to the set. /// The flags to use for this operation. - /// True if the specified member was not already present in the set, else False + /// if the specified member was not already present in the set, else . /// https://redis.io/commands/sadd bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); @@ -977,7 +1049,8 @@ public interface IDatabase : IRedis, IDatabaseAsync RedisValue[] SetCombine(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// This command is equal to SetCombine, but instead of returning the resulting set, it is stored in destination. If destination already exists, it is overwritten. + /// This command is equal to SetCombine, but instead of returning the resulting set, it is stored in destination. + /// If destination already exists, it is overwritten. /// /// The operation to perform. /// The key of the destination set. @@ -991,7 +1064,8 @@ public interface IDatabase : IRedis, IDatabaseAsync long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); /// - /// This command is equal to SetCombine, but instead of returning the resulting set, it is stored in destination. If destination already exists, it is overwritten. + /// This command is equal to SetCombine, but instead of returning the resulting set, it is stored in destination. + /// If destination already exists, it is overwritten. /// /// The operation to perform. /// The key of the destination set. @@ -1009,7 +1083,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the set. /// The value to check for . /// The flags to use for this operation. - /// 1 if the element is a member of the set. 0 if the element is not a member of the set, or if key does not exist. + /// + /// if the element is a member of the set. + /// if the element is not a member of the set, or if key does not exist. + /// /// https://redis.io/commands/sismember bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); @@ -1032,14 +1109,18 @@ public interface IDatabase : IRedis, IDatabaseAsync RedisValue[] SetMembers(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Move member from the set at source to the set at destination. This operation is atomic. In every given moment the element will appear to be a member of source or destination for other clients. + /// Move member from the set at source to the set at destination. + /// This operation is atomic. In every given moment the element will appear to be a member of source or destination for other clients. /// When the specified element already exists in the destination set, it is only removed from the source set. /// /// The key of the source set. /// The key of the destination set. /// The value to move. /// The flags to use for this operation. - /// 1 if the element is moved. 0 if the element is not a member of source and no operation was performed. + /// + /// if the element is moved. + /// if the element is not a member of source and no operation was performed. + /// /// https://redis.io/commands/smove bool SetMove(RedisKey source, RedisKey destination, RedisValue value, CommandFlags flags = CommandFlags.None); @@ -1067,33 +1148,36 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The key of the set. /// The flags to use for this operation. - /// The randomly selected element, or nil when key does not exist + /// The randomly selected element, or nil when key does not exist. /// https://redis.io/commands/srandmember RedisValue SetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Return an array of count distinct elements if count is positive. If called with a negative count the behavior changes and the command is allowed to return the same element multiple times. + /// Return an array of count distinct elements if count is positive. + /// If called with a negative count the behavior changes and the command is allowed to return the same element multiple times. /// In this case the number of returned elements is the absolute value of the specified count. /// /// The key of the set. /// The count of members to get. /// The flags to use for this operation. - /// An array of elements, or an empty array when key does not exist + /// An array of elements, or an empty array when key does not exist. /// https://redis.io/commands/srandmember RedisValue[] SetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// - /// Remove the specified member from the set stored at key. Specified members that are not a member of this set are ignored. + /// Remove the specified member from the set stored at key. + /// Specified members that are not a member of this set are ignored. /// /// The key of the set. /// The value to remove. /// The flags to use for this operation. - /// True if the specified member was already present in the set, else False + /// if the specified member was already present in the set, otherwise. /// https://redis.io/commands/srem bool SetRemove(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// - /// Remove the specified members from the set stored at key. Specified members that are not a member of this set are ignored. + /// Remove the specified members from the set stored at key. + /// Specified members that are not a member of this set are ignored. /// /// The key of the set. /// The values to remove. @@ -1103,7 +1187,7 @@ public interface IDatabase : IRedis, IDatabaseAsync long SetRemove(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); /// - /// The SSCAN command is used to incrementally iterate over set + /// The SSCAN command is used to incrementally iterate over a set. /// /// The key of the set. /// The pattern to match. @@ -1114,7 +1198,8 @@ public interface IDatabase : IRedis, IDatabaseAsync IEnumerable SetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags); /// - /// The SSCAN command is used to incrementally iterate over set; note: to resume an iteration via cursor, cast the original enumerable or enumerator to IScanningCursor. + /// The SSCAN command is used to incrementally iterate over set. + /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to . /// /// The key of the set. /// The pattern to match. @@ -1127,11 +1212,12 @@ public interface IDatabase : IRedis, IDatabaseAsync IEnumerable SetScan(RedisKey key, RedisValue pattern = default(RedisValue), int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// - /// Sorts a list, set or sorted set (numerically or alphabetically, ascending by default); By default, the elements themselves are compared, but the values can also be - /// used to perform external key-lookups using the by parameter. By default, the elements themselves are returned, but external key-lookups (one or many) can - /// be performed instead by specifying the get parameter (note that # specifies the element itself, when used in get). - /// Referring to the redis SORT documentation for examples is recommended. When used in hashes, by and get - /// can be used to specify fields using -> notation (again, refer to redis documentation). + /// Sorts a list, set or sorted set (numerically or alphabetically, ascending by default). + /// By default, the elements themselves are compared, but the values can also be used to perform external key-lookups using the by parameter. + /// By default, the elements themselves are returned, but external key-lookups (one or many) can be performed instead by specifying + /// the get parameter (note that # specifies the element itself, when used in get). + /// Referring to the redis SORT documentation for examples is recommended. + /// When used in hashes, by and get can be used to specify fields using -> notation (again, refer to redis documentation). /// /// The key of the list, set, or sorted set. /// How many entries to skip on the return. @@ -1146,11 +1232,12 @@ public interface IDatabase : IRedis, IDatabaseAsync RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None); /// - /// Sorts a list, set or sorted set (numerically or alphabetically, ascending by default); By default, the elements themselves are compared, but the values can also be - /// used to perform external key-lookups using the by parameter. By default, the elements themselves are returned, but external key-lookups (one or many) can - /// be performed instead by specifying the get parameter (note that # specifies the element itself, when used in get). - /// Referring to the redis SORT documentation for examples is recommended. When used in hashes, by and get - /// can be used to specify fields using -> notation (again, refer to redis documentation). + /// Sorts a list, set or sorted set (numerically or alphabetically, ascending by default). + /// By default, the elements themselves are compared, but the values can also be used to perform external key-lookups using the by parameter. + /// By default, the elements themselves are returned, but external key-lookups (one or many) can be performed instead by specifying + /// the get parameter (note that # specifies the element itself, when used in get). + /// Referring to the redis SORT documentation for examples is recommended. + /// When used in hashes, by and get can be used to specify fields using -> notation (again, refer to redis documentation). /// /// The destination key to store results in. /// The key of the list, set, or sorted set. @@ -1166,30 +1253,33 @@ public interface IDatabase : IRedis, IDatabaseAsync long SortAndStore(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None); /// - /// Adds the specified member with the specified score to the sorted set stored at key. If the specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. + /// Adds the specified member with the specified score to the sorted set stored at key. + /// If the specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. /// /// The key of the sorted set. /// The member to add to the sorted set. /// The score for the member to add to the sorted set. /// The flags to use for this operation. - /// True if the value was added, False if it already existed (the score is still updated) + /// if the value was added. if it already existed (the score is still updated). /// https://redis.io/commands/zadd bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags); /// - /// Adds the specified member with the specified score to the sorted set stored at key. If the specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. + /// Adds the specified member with the specified score to the sorted set stored at key. + /// If the specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. /// /// The key of the sorted set. /// The member to add to the sorted set. /// The score for the member to add to the sorted set. /// What conditions to add the element under (defaults to always). /// The flags to use for this operation. - /// True if the value was added, False if it already existed (the score is still updated) + /// if the value was added. if it already existed (the score is still updated). /// https://redis.io/commands/zadd bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None); /// - /// Adds all the specified members with the specified scores to the sorted set stored at key. If a specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. + /// Adds all the specified members with the specified scores to the sorted set stored at key. + /// If a specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. /// /// The key of the sorted set. /// The members and values to add to the sorted set. @@ -1199,7 +1289,8 @@ public interface IDatabase : IRedis, IDatabaseAsync long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags flags); /// - /// Adds all the specified members with the specified scores to the sorted set stored at key. If a specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. + /// Adds all the specified members with the specified scores to the sorted set stored at key. + /// If a specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. /// /// The key of the sorted set. /// The members and values to add to the sorted set. @@ -1221,7 +1312,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// https://redis.io/commands/zunionstore /// https://redis.io/commands/zinterstore - /// the number of elements in the resulting sorted set at destination + /// The number of elements in the resulting sorted set at destination. long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// @@ -1236,11 +1327,12 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// https://redis.io/commands/zunionstore /// https://redis.io/commands/zinterstore - /// the number of elements in the resulting sorted set at destination + /// The number of elements in the resulting sorted set at destination. long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[] weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// - /// Decrements the score of member in the sorted set stored at key by decrement. If member does not exist in the sorted set, it is added with -decrement as its score (as if its previous score was 0.0). + /// Decrements the score of member in the sorted set stored at key by decrement. + /// If member does not exist in the sorted set, it is added with -decrement as its score (as if its previous score was 0.0). /// /// The key of the sorted set. /// The member to decrement. @@ -1274,7 +1366,8 @@ public interface IDatabase : IRedis, IDatabaseAsync long SortedSetLength(RedisKey key, double min = double.NegativeInfinity, double max = double.PositiveInfinity, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// - /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering, this command returns the number of elements in the sorted set at key with a value between min and max. + /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering. + /// This command returns the number of elements in the sorted set at key with a value between min and max. /// /// The key of the sorted set. /// The min value to filter by. @@ -1286,8 +1379,11 @@ public interface IDatabase : IRedis, IDatabaseAsync long SortedSetLengthByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// - /// Returns the specified range of elements in the sorted set stored at key. By default the elements are considered to be ordered from the lowest to the highest score. Lexicographical order is used for elements with equal score. - /// Both start and stop are zero-based indexes, where 0 is the first element, 1 is the next element and so on. They can also be negative numbers indicating offsets from the end of the sorted set, with -1 being the last element of the sorted set, -2 the penultimate element and so on. + /// Returns the specified range of elements in the sorted set stored at key. + /// By default the elements are considered to be ordered from the lowest to the highest score. + /// Lexicographical order is used for elements with equal score. + /// Both start and stop are zero-based indexes, where 0 is the first element, 1 is the next element and so on. + /// They can also be negative numbers indicating offsets from the end of the sorted set, with -1 being the last element of the sorted set, -2 the penultimate element and so on. /// /// The key of the sorted set. /// The start index to get. @@ -1300,8 +1396,11 @@ public interface IDatabase : IRedis, IDatabaseAsync RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); /// - /// Returns the specified range of elements in the sorted set stored at key. By default the elements are considered to be ordered from the lowest to the highest score. Lexicographical order is used for elements with equal score. - /// Both start and stop are zero-based indexes, where 0 is the first element, 1 is the next element and so on. They can also be negative numbers indicating offsets from the end of the sorted set, with -1 being the last element of the sorted set, -2 the penultimate element and so on. + /// Returns the specified range of elements in the sorted set stored at key. + /// By default the elements are considered to be ordered from the lowest to the highest score. + /// Lexicographical order is used for elements with equal score. + /// Both start and stop are zero-based indexes, where 0 is the first element, 1 is the next element and so on. + /// They can also be negative numbers indicating offsets from the end of the sorted set, with -1 being the last element of the sorted set, -2 the penultimate element and so on. /// /// The key of the sorted set. /// The start index to get. @@ -1314,8 +1413,11 @@ public interface IDatabase : IRedis, IDatabaseAsync SortedSetEntry[] SortedSetRangeByRankWithScores(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); /// - /// Returns the specified range of elements in the sorted set stored at key. By default the elements are considered to be ordered from the lowest to the highest score. Lexicographical order is used for elements with equal score. - /// Start and stop are used to specify the min and max range for score values. Similar to other range methods the values are inclusive. + /// Returns the specified range of elements in the sorted set stored at key. + /// By default the elements are considered to be ordered from the lowest to the highest score. + /// Lexicographical order is used for elements with equal score. + /// Start and stop are used to specify the min and max range for score values. + /// Similar to other range methods the values are inclusive. /// /// The key of the sorted set. /// The minimum score to filter by. @@ -1338,8 +1440,11 @@ RedisValue[] SortedSetRangeByScore(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Returns the specified range of elements in the sorted set stored at key. By default the elements are considered to be ordered from the lowest to the highest score. Lexicographical order is used for elements with equal score. - /// Start and stop are used to specify the min and max range for score values. Similar to other range methods the values are inclusive. + /// Returns the specified range of elements in the sorted set stored at key. + /// By default the elements are considered to be ordered from the lowest to the highest score. + /// Lexicographical order is used for elements with equal score. + /// Start and stop are used to specify the min and max range for score values. + /// Similar to other range methods the values are inclusive. /// /// The key of the sorted set. /// The minimum score to filter by. @@ -1362,7 +1467,8 @@ SortedSetEntry[] SortedSetRangeByScoreWithScores(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering, this command returns all the elements in the sorted set at key with a value between min and max. + /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering. + /// This command returns all the elements in the sorted set at key with a value between min and max. /// /// The key of the sorted set. /// The min value to filter by. @@ -1372,7 +1478,7 @@ SortedSetEntry[] SortedSetRangeByScoreWithScores(RedisKey key, /// How many items to take. /// The flags to use for this operation. /// https://redis.io/commands/zrangebylex - /// list of elements in the specified score range. + /// List of elements in the specified score range. RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min, RedisValue max, @@ -1382,7 +1488,8 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, CommandFlags flags = CommandFlags.None); // defaults removed to avoid ambiguity with overload with order /// - /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering, this command returns all the elements in the sorted set at key with a value between min and max. + /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering. + /// This command returns all the elements in the sorted set at key with a value between min and max. /// /// The key of the sorted set. /// The min value to filter by. @@ -1394,7 +1501,7 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// The flags to use for this operation. /// https://redis.io/commands/zrangebylex /// https://redis.io/commands/zrevrangebylex - /// list of elements in the specified score range. + /// List of elements in the specified score range. RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min = default(RedisValue), RedisValue max = default(RedisValue), @@ -1405,13 +1512,14 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Returns the rank of member in the sorted set stored at key, by default with the scores ordered from low to high. The rank (or index) is 0-based, which means that the member with the lowest score has rank 0. + /// Returns the rank of member in the sorted set stored at key, by default with the scores ordered from low to high. + /// The rank (or index) is 0-based, which means that the member with the lowest score has rank 0. /// /// The key of the sorted set. /// The member to get the rank of. /// The order to sort by (defaults to ascending). /// The flags to use for this operation. - /// If member exists in the sorted set, the rank of member; If member does not exist in the sorted set or key does not exist, null + /// If member exists in the sorted set, the rank of member. If member does not exist in the sorted set or key does not exist, . /// https://redis.io/commands/zrank /// https://redis.io/commands/zrevrank long? SortedSetRank(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); @@ -1422,7 +1530,7 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// The key of the sorted set. /// The member to remove. /// The flags to use for this operation. - /// True if the member existed in the sorted set and was removed; False otherwise. + /// if the member existed in the sorted set and was removed. otherwise. /// https://redis.io/commands/zrem bool SortedSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); @@ -1437,7 +1545,10 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, long SortedSetRemove(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); /// - /// Removes all elements in the sorted set stored at key with rank between start and stop. Both start and stop are 0 -based indexes with 0 being the element with the lowest score. These indexes can be negative numbers, where they indicate offsets starting at the element with the highest score. For example: -1 is the element with the highest score, -2 the element with the second highest score and so forth. + /// Removes all elements in the sorted set stored at key with rank between start and stop. + /// Both start and stop are 0 -based indexes with 0 being the element with the lowest score. + /// These indexes can be negative numbers, where they indicate offsets starting at the element with the highest score. + /// For example: -1 is the element with the highest score, -2 the element with the second highest score and so forth. /// /// The key of the sorted set. /// The minimum rank to remove. @@ -1460,19 +1571,20 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, long SortedSetRemoveRangeByScore(RedisKey key, double start, double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// - /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering, this command removes all elements in the sorted set stored at key between the lexicographical range specified by min and max. + /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering. + /// This command removes all elements in the sorted set stored at key between the lexicographical range specified by min and max. /// /// The key of the sorted set. /// The minimum value to remove. /// The maximum value to remove. /// Which of and to exclude (defaults to both inclusive). /// The flags to use for this operation. - /// the number of elements removed. + /// The number of elements removed. /// https://redis.io/commands/zremrangebylex long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// - /// The ZSCAN command is used to incrementally iterate over a sorted set + /// The ZSCAN command is used to incrementally iterate over a sorted set. /// /// The key of the sorted set. /// The pattern to match. @@ -1483,7 +1595,8 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, IEnumerable SortedSetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags); /// - /// The ZSCAN command is used to incrementally iterate over a sorted set; note: to resume an iteration via cursor, cast the original enumerable or enumerator to IScanningCursor. + /// The ZSCAN command is used to incrementally iterate over a sorted set + /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to IScanningCursor. /// /// The key of the sorted set. /// The pattern to match. @@ -1501,7 +1614,8 @@ IEnumerable SortedSetScan(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Returns the score of member in the sorted set at key; If member does not exist in the sorted set, or key does not exist, nil is returned. + /// Returns the score of member in the sorted set at key. + /// If member does not exist in the sorted set, or key does not exist, nil is returned. /// /// The key of the sorted set. /// The member to get a score for. @@ -1556,7 +1670,9 @@ IEnumerable SortedSetScan(RedisKey key, long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); /// - /// Adds an entry using the specified values to the given stream key. If key does not exist, a new key holding a stream is created. The command returns the ID of the newly created stream entry. + /// Adds an entry using the specified values to the given stream key. + /// If key does not exist, a new key holding a stream is created. + /// The command returns the ID of the newly created stream entry. /// /// The key of the stream. /// The field name for the stream entry. @@ -1570,7 +1686,9 @@ IEnumerable SortedSetScan(RedisKey key, RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); /// - /// Adds an entry using the specified values to the given stream key. If key does not exist, a new key holding a stream is created. The command returns the ID of the newly created stream entry. + /// Adds an entry using the specified values to the given stream key. + /// If key does not exist, a new key holding a stream is created. + /// The command returns the ID of the newly created stream entry. /// /// The key of the stream. /// The fields and their associated values to set in the stream entry. @@ -1583,7 +1701,8 @@ IEnumerable SortedSetScan(RedisKey key, RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); /// - /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. This method returns the complete message for the claimed message(s). + /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. + /// This method returns the complete message for the claimed message(s). /// /// The key of the stream. /// The consumer group. @@ -1596,7 +1715,8 @@ IEnumerable SortedSetScan(RedisKey key, StreamEntry[] StreamClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); /// - /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. This method returns the IDs for the claimed message(s). + /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. + /// This method returns the IDs for the claimed message(s). /// /// The key of the stream. /// The consumer group. @@ -1615,16 +1735,17 @@ IEnumerable SortedSetScan(RedisKey key, /// The name of the consumer group. /// The position from which to read for the consumer group. /// The flags to use for this operation. - /// True if successful, otherwise false. + /// if successful, otherwise. bool StreamConsumerGroupSetPosition(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None); /// - /// Retrieve information about the consumers for the given consumer group. This is the equivalent of calling "XINFO GROUPS key group". + /// Retrieve information about the consumers for the given consumer group. + /// This is the equivalent of calling "XINFO GROUPS key group". /// /// The key of the stream. /// The consumer group name. /// The flags to use for this operation. - /// An instance of for each of the consumer group's consumers. + /// An instance of for each of the consumer group's consumers. /// https://redis.io/topics/streams-intro StreamConsumerInfo[] StreamConsumerInfo(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); @@ -1635,7 +1756,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The name of the group to create. /// The position to begin reading the stream. Defaults to . /// The flags to use for this operation. - /// True if the group was created. + /// if the group was created, otherwise. /// https://redis.io/topics/streams-intro bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisValue? position, CommandFlags flags); @@ -1647,7 +1768,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The position to begin reading the stream. Defaults to . /// Create the stream if it does not already exist. /// The flags to use for this operation. - /// True if the group was created. + /// if the group was created, otherwise. /// https://redis.io/topics/streams-intro bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None); @@ -1677,7 +1798,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the stream. /// The name of the consumer group. /// The flags to use for this operation. - /// True if deleted, otherwise false. + /// if deleted, otherwise. bool StreamDeleteConsumerGroup(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); /// @@ -1685,7 +1806,7 @@ IEnumerable SortedSetScan(RedisKey key, /// /// The key of the stream. /// The flags to use for this operation. - /// An instance of for each of the stream's groups. + /// An instance of for each of the stream's groups. /// https://redis.io/topics/streams-intro StreamGroupInfo[] StreamGroupInfo(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -1694,7 +1815,7 @@ IEnumerable SortedSetScan(RedisKey key, /// /// The key of the stream. /// The flags to use for this operation. - /// A instance with information about the stream. + /// A instance with information about the stream. /// https://redis.io/topics/streams-intro StreamInfo StreamInfo(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -1709,11 +1830,16 @@ IEnumerable SortedSetScan(RedisKey key, /// /// View information about pending messages for a stream. + /// A pending message is a message read using StreamReadGroup (XREADGROUP) but not yet acknowledged. /// /// The key of the stream. /// The name of the consumer group /// The flags to use for this operation. - /// An instance of . contains the number of pending messages, the highest and lowest ID of the pending messages, and the consumers with their pending message count. + /// + /// An instance of . + /// contains the number of pending messages. + /// The highest and lowest ID of the pending messages, and the consumers with their pending message count. + /// /// The equivalent of calling XPENDING key group. /// https://redis.io/commands/xpending StreamPendingInfo StreamPending(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); @@ -1740,7 +1866,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The minimum ID from which to read the stream. The method will default to reading from the beginning of the stream. /// The maximum ID to read to within the stream. The method will default to reading to the end of the stream. /// The maximum number of messages to return. - /// The order of the messages. will execute XRANGE and wil execute XREVRANGE. + /// The order of the messages. will execute XRANGE and will execute XREVRANGE. /// The flags to use for this operation. /// Returns an instance of for each message returned. /// https://redis.io/commands/xrange @@ -1753,7 +1879,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The position from which to read the stream. /// The maximum number of messages to return. /// The flags to use for this operation. - /// Returns a value of for each message returned. + /// Returns an instance of for each message returned. /// Equivalent of calling XREAD COUNT num STREAMS key id. /// https://redis.io/commands/xread StreamEntry[] StreamRead(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None); @@ -1775,7 +1901,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the stream. /// The name of the consumer group. /// The consumer name. - /// The position from which to read the stream. Defaults to when null. + /// The position from which to read the stream. Defaults to when . /// The maximum number of messages to return. /// The flags to use for this operation. /// Returns a value of for each message returned. @@ -1788,7 +1914,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the stream. /// The name of the consumer group. /// The consumer name. - /// The position from which to read the stream. Defaults to when null. + /// The position from which to read the stream. Defaults to when . /// The maximum number of messages to return. /// When true, the message will not be added to the pending message list. /// The flags to use for this operation. @@ -1797,8 +1923,8 @@ IEnumerable SortedSetScan(RedisKey key, StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None); /// - /// Read from multiple streams into the given consumer group. The consumer group with the given - /// will need to have been created for each stream prior to calling this method. + /// Read from multiple streams into the given consumer group. + /// The consumer group with the given will need to have been created for each stream prior to calling this method. /// /// Array of streams and the positions from which to begin reading for each stream. /// The name of the consumer group. @@ -1811,8 +1937,8 @@ IEnumerable SortedSetScan(RedisKey key, RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags); /// - /// Read from multiple streams into the given consumer group. The consumer group with the given - /// will need to have been created for each stream prior to calling this method. + /// Read from multiple streams into the given consumer group. + /// The consumer group with the given will need to have been created for each stream prior to calling this method. /// /// Array of streams and the positions from which to begin reading for each stream. /// The name of the consumer group. @@ -1837,8 +1963,8 @@ IEnumerable SortedSetScan(RedisKey key, long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); /// - /// If key already exists and is a string, this command appends the value at the end of the string. If key does not exist it is created and set as an empty string, - /// so APPEND will be similar to SET in this special case. + /// If key already exists and is a string, this command appends the value at the end of the string. + /// If key does not exist it is created and set as an empty string, so APPEND will be similar to SET in this special case. /// /// The key of the string. /// The value to append to the string. @@ -1849,7 +1975,8 @@ IEnumerable SortedSetScan(RedisKey key, /// /// Count the number of set bits (population counting) in a string. - /// By default all the bytes contained in the string are examined. It is possible to specify the counting operation only in an interval passing the additional arguments start and end. + /// By default all the bytes contained in the string are examined. + /// It is possible to specify the counting operation only in an interval passing the additional arguments start and end. /// Like for the GETRANGE command start and end can contain negative values in order to index bytes starting from the end of the string, where -1 is the last byte, -2 is the penultimate, and so forth. /// /// The key of the string. @@ -1864,7 +1991,7 @@ IEnumerable SortedSetScan(RedisKey key, /// Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key. /// The BITOP command supports four bitwise operations; note that NOT is a unary operator: the second key should be omitted in this case /// and only the first key will be considered. - /// The result of the operation is always stored at destkey. + /// The result of the operation is always stored at . /// /// The operation to perform. /// The destination key to store the result in. @@ -1878,7 +2005,7 @@ IEnumerable SortedSetScan(RedisKey key, /// /// Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key. /// The BITOP command supports four bitwise operations; note that NOT is a unary operator. - /// The result of the operation is always stored at destkey. + /// The result of the operation is always stored at . /// /// The operation to perform. /// The destination key to store the result in. @@ -1891,7 +2018,8 @@ IEnumerable SortedSetScan(RedisKey key, /// /// Return the position of the first bit set to 1 or 0 in a string. /// The position is returned thinking at the string as an array of bits from left to right where the first byte most significant bit is at position 0, the second byte most significant bit is at position 8 and so forth. - /// An start and end may be specified; these are in bytes, not bits; start and end can contain negative values in order to index bytes starting from the end of the string, where -1 is the last byte, -2 is the penultimate, and so forth. + /// A and may be specified - these are in bytes, not bits. + /// and can contain negative values in order to index bytes starting from the end of the string, where -1 is the last byte, -2 is the penultimate, and so forth. /// /// The key of the string. /// True to check for the first 1 bit, false to check for the first 0 bit. @@ -1904,8 +2032,10 @@ IEnumerable SortedSetScan(RedisKey key, long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None); /// - /// Decrements the number stored at key by decrement. If the key does not exist, it is set to 0 before performing the operation. - /// An error is returned if the key contains a value of the wrong type or contains a string that is not representable as integer. This operation is limited to 64 bit signed integers. + /// Decrements the number stored at key by decrement. + /// If the key does not exist, it is set to 0 before performing the operation. + /// An error is returned if the key contains a value of the wrong type or contains a string that is not representable as integer. + /// This operation is limited to 64 bit signed integers. /// /// The key of the string. /// The amount to decrement by (defaults to 1). @@ -1916,7 +2046,9 @@ IEnumerable SortedSetScan(RedisKey key, long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); /// - /// Decrements the string representing a floating point number stored at key by the specified decrement. If the key does not exist, it is set to 0 before performing the operation. The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. + /// Decrements the string representing a floating point number stored at key by the specified decrement. + /// If the key does not exist, it is set to 0 before performing the operation. + /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. /// /// The key of the string. /// The amount to decrement by (defaults to 1). @@ -1926,7 +2058,8 @@ IEnumerable SortedSetScan(RedisKey key, double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None); /// - /// Get the value of key. If the key does not exist the special value nil is returned. An error is returned if the value stored at key is not a string, because GET only handles string values. + /// Get the value of key. If the key does not exist the special value nil is returned. + /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. @@ -1935,7 +2068,8 @@ IEnumerable SortedSetScan(RedisKey key, RedisValue StringGet(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Returns the values of all specified keys. For every key that does not hold a string value or does not exist, the special value nil is returned. + /// Returns the values of all specified keys. + /// For every key that does not hold a string value or does not exist, the special value nil is returned. /// /// The keys of the strings. /// The flags to use for this operation. @@ -1944,7 +2078,8 @@ IEnumerable SortedSetScan(RedisKey key, RedisValue[] StringGet(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Get the value of key. If the key does not exist the special value nil is returned. An error is returned if the value stored at key is not a string, because GET only handles string values. + /// Get the value of key. If the key does not exist the special value nil is returned. + /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. @@ -1964,7 +2099,9 @@ IEnumerable SortedSetScan(RedisKey key, bool StringGetBit(RedisKey key, long offset, CommandFlags flags = CommandFlags.None); /// - /// Returns the substring of the string value stored at key, determined by the offsets start and end (both are inclusive). Negative offsets can be used in order to provide an offset starting from the end of the string. So -1 means the last character, -2 the penultimate and so forth. + /// Returns the substring of the string value stored at key, determined by the offsets start and end (both are inclusive). + /// Negative offsets can be used in order to provide an offset starting from the end of the string. + /// So -1 means the last character, -2 the penultimate and so forth. /// /// The key of the string. /// The start index of the substring to get. @@ -1985,7 +2122,9 @@ IEnumerable SortedSetScan(RedisKey key, RedisValue StringGetSet(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// - /// Get the value of key and delete the key. If the key does not exist the special value nil is returned. An error is returned if the value stored at key is not a string, because GET only handles string values. + /// Get the value of key and delete the key. + /// If the key does not exist the special value nil is returned. + /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. @@ -1994,7 +2133,9 @@ IEnumerable SortedSetScan(RedisKey key, RedisValue StringGetDelete(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Get the value of key. If the key does not exist the special value nil is returned. An error is returned if the value stored at key is not a string, because GET only handles string values. + /// Get the value of key. + /// If the key does not exist the special value nil is returned. + /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. @@ -2003,7 +2144,10 @@ IEnumerable SortedSetScan(RedisKey key, RedisValueWithExpiry StringGetWithExpiry(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Increments the number stored at key by increment. If the key does not exist, it is set to 0 before performing the operation. An error is returned if the key contains a value of the wrong type or contains a string that is not representable as integer. This operation is limited to 64 bit signed integers. + /// Increments the number stored at key by increment. + /// If the key does not exist, it is set to 0 before performing the operation. + /// An error is returned if the key contains a value of the wrong type or contains a string that is not representable as integer. + /// This operation is limited to 64 bit signed integers. /// /// The key of the string. /// The amount to increment by (defaults to 1). @@ -2014,7 +2158,9 @@ IEnumerable SortedSetScan(RedisKey key, long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); /// - /// Increments the string representing a floating point number stored at key by the specified increment. If the key does not exist, it is set to 0 before performing the operation. The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. + /// Increments the string representing a floating point number stored at key by the specified increment. + /// If the key does not exist, it is set to 0 before performing the operation. + /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. /// /// The key of the string. /// The amount to increment by (defaults to 1). @@ -2028,7 +2174,7 @@ IEnumerable SortedSetScan(RedisKey key, /// /// The key of the string. /// The flags to use for this operation. - /// the length of the string at key, or 0 when key does not exist. + /// The length of the string at key, or 0 when key does not exist. /// https://redis.io/commands/strlen long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -2040,24 +2186,26 @@ IEnumerable SortedSetScan(RedisKey key, /// The expiry to set. /// Which condition to set the value under (defaults to always). /// The flags to use for this operation. - /// True if the string was set, false otherwise. + /// if the string was set, otherwise. /// https://redis.io/commands/set bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None); /// - /// Sets the given keys to their respective values. If "not exists" is specified, this will not perform any operation at all even if just a single key already exists. + /// Sets the given keys to their respective values. + /// If is specified, this will not perform any operation at all even if just a single key already exists. /// /// The keys and values to set. /// Which condition to set the value under (defaults to always). /// The flags to use for this operation. - /// True if the keys were set, else False + /// if the keys were set, otherwise. /// https://redis.io/commands/mset /// https://redis.io/commands/msetnx bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); /// /// Sets or clears the bit at offset in the string value stored at key. - /// The bit is either set or cleared depending on value, which can be either 0 or 1. When key does not exist, a new string value is created.The string is grown to make sure it can hold a bit at offset. + /// The bit is either set or cleared depending on value, which can be either 0 or 1. + /// When key does not exist, a new string value is created.The string is grown to make sure it can hold a bit at offset. /// /// The key of the string. /// The offset in the string to set . @@ -2068,7 +2216,9 @@ IEnumerable SortedSetScan(RedisKey key, bool StringSetBit(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None); /// - /// Overwrites part of the string stored at key, starting at the specified offset, for the entire length of value. If the offset is larger than the current length of the string at key, the string is padded with zero-bytes to make offset fit. Non-existing keys are considered as empty strings, so this command will make sure it holds a string large enough to be able to set value at offset. + /// Overwrites part of the string stored at key, starting at the specified offset, for the entire length of value. + /// If the offset is larger than the current length of the string at key, the string is padded with zero-bytes to make offset fit. + /// Non-existing keys are considered as empty strings, so this command will make sure it holds a string large enough to be able to set value at offset. /// /// The key of the string. /// The offset in the string to overwrite. @@ -2083,12 +2233,12 @@ IEnumerable SortedSetScan(RedisKey key, /// /// The key to touch. /// The flags to use for this operation. - /// True if the key was touched. + /// if the key was touched, otherwise. /// https://redis.io/commands/touch bool KeyTouch(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Alters the last access time of a keys. A key is ignored if it does not exist. + /// Alters the last access time of the specified . A key is ignored if it does not exist. /// /// The keys to touch. /// The flags to use for this operation. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 9626945a0..a0215b6af 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -6,20 +6,20 @@ namespace StackExchange.Redis { /// - /// Describes functionality that is common to both standalone redis servers and redis clusters + /// Describes functionality that is common to both standalone redis servers and redis clusters. /// public interface IDatabaseAsync : IRedisAsync { /// - /// Indicates whether the instance can communicate with the server (resolved - /// using the supplied key and optional flags) + /// Indicates whether the instance can communicate with the server (resolved using the supplied key and optional flags). /// /// The key to check for. /// The flags to use for this operation. bool IsConnected(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Atomically transfer a key from a source Redis instance to a destination Redis instance. On success the key is deleted from the original instance by default, and is guaranteed to exist in the target instance. + /// Atomically transfer a key from a source Redis instance to a destination Redis instance. + /// On success the key is deleted from the original instance by default, and is guaranteed to exist in the target instance. /// /// The key to migrate. /// The server to migrate the key to. @@ -31,7 +31,8 @@ public interface IDatabaseAsync : IRedisAsync Task KeyMigrateAsync(RedisKey key, EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, MigrateOptions migrateOptions = MigrateOptions.None, CommandFlags flags = CommandFlags.None); /// - /// Returns the raw DEBUG OBJECT output for a key; this command is not fully documented and should be avoided unless you have good reason, and then avoided anyway. + /// Returns the raw DEBUG OBJECT output for a key. + /// This command is not fully documented and should be avoided unless you have good reason, and then avoided anyway. /// /// The key to debug. /// The flags to use for this migration. @@ -40,29 +41,35 @@ public interface IDatabaseAsync : IRedisAsync Task DebugObjectAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Add the specified member to the set stored at key. Specified members that are already a member of this set are ignored. If key does not exist, a new set is created before adding the specified members. + /// Add the specified member to the set stored at key. + /// Specified members that are already a member of this set are ignored. + /// If key does not exist, a new set is created before adding the specified members. /// /// The key of the set. /// The longitude of geo entry. /// The latitude of the geo entry. /// The value to set at this entry. /// The flags to use for this operation. - /// True if the specified member was not already present in the set, else False. + /// if the specified member was not already present in the set, else . /// https://redis.io/commands/geoadd Task GeoAddAsync(RedisKey key, double longitude, double latitude, RedisValue member, CommandFlags flags = CommandFlags.None); /// - /// Add the specified member to the set stored at key. Specified members that are already a member of this set are ignored. If key does not exist, a new set is created before adding the specified members. + /// Add the specified member to the set stored at key. + /// Specified members that are already a member of this set are ignored. + /// If key does not exist, a new set is created before adding the specified members. /// /// The key of the set. /// The geo value to store. /// The flags to use for this operation. - /// True if the specified member was not already present in the set, else False + /// if the specified member was not already present in the set, else . /// https://redis.io/commands/geoadd Task GeoAddAsync(RedisKey key, StackExchange.Redis.GeoEntry value, CommandFlags flags = CommandFlags.None); /// - /// Add the specified members to the set stored at key. Specified members that are already a member of this set are ignored. If key does not exist, a new set is created before adding the specified members. + /// Add the specified members to the set stored at key. + /// Specified members that are already a member of this set are ignored. + /// If key does not exist, a new set is created before adding the specified members. /// /// The key of the set. /// The geo values add to the set. @@ -72,12 +79,13 @@ public interface IDatabaseAsync : IRedisAsync Task GeoAddAsync(RedisKey key, GeoEntry[] values, CommandFlags flags = CommandFlags.None); /// - /// Removes the specified member from the geo sorted set stored at key. Non existing members are ignored. + /// Removes the specified member from the geo sorted set stored at key. + /// Non existing members are ignored. /// /// The key of the set. /// The geo value to remove. /// The flags to use for this operation. - /// True if the member existed in the sorted set and was removed; False otherwise. + /// if the member existed in the sorted set and was removed, else . /// https://redis.io/commands/zrem Task GeoRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); @@ -89,7 +97,7 @@ public interface IDatabaseAsync : IRedisAsync /// The second member to check. /// The unit of distance to return (defaults to meters). /// The flags to use for this operation. - /// The command returns the distance as a double (represented as a string) in the specified unit, or NULL if one or both the elements are missing. + /// The command returns the distance as a double (represented as a string) in the specified unit, or if one or both the elements are missing. /// https://redis.io/commands/geodist Task GeoDistanceAsync(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None); @@ -119,7 +127,10 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the set. /// The members to get. /// The flags to use for this operation. - /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command.Non existing elements are reported as NULL elements of the array. + /// + /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command. + /// Non existing elements are reported as NULL elements of the array. + /// /// https://redis.io/commands/geopos Task GeoPositionAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); @@ -129,12 +140,16 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the set. /// The member to get. /// The flags to use for this operation. - /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command.Non existing elements are reported as NULL elements of the array. + /// + /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command. + /// Non existing elements are reported as NULL elements of the array. + /// /// https://redis.io/commands/geopos Task GeoPositionAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// - /// Return the members of a sorted set populated with geospatial information using GEOADD, which are within the borders of the area specified with the center location and the maximum distance from the center (the radius). + /// Return the members of a sorted set populated with geospatial information using GEOADD, which are + /// within the borders of the area specified with the center location and the maximum distance from the center (the radius). /// /// The key of the set. /// The member to get a radius of results from. @@ -149,7 +164,8 @@ public interface IDatabaseAsync : IRedisAsync Task GeoRadiusAsync(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); /// - /// Return the members of a sorted set populated with geospatial information using GEOADD, which are within the borders of the area specified with the center location and the maximum distance from the center (the radius). + /// Return the members of a sorted set populated with geospatial information using GEOADD, which are + /// within the borders of the area specified with the center location and the maximum distance from the center (the radius). /// /// The key of the set. /// The longitude of the point to get a radius of results from. @@ -165,7 +181,9 @@ public interface IDatabaseAsync : IRedisAsync Task GeoRadiusAsync(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); /// - /// Decrements the number stored at field in the hash stored at key by decrement. If key does not exist, a new key holding a hash is created. If field does not exist the value is set to 0 before the operation is performed. + /// Decrements the number stored at field in the hash stored at key by decrement. + /// If key does not exist, a new key holding a hash is created. + /// If field does not exist the value is set to 0 before the operation is performed. /// /// The key of the hash. /// The field in the hash to decrement. @@ -177,7 +195,8 @@ public interface IDatabaseAsync : IRedisAsync Task HashDecrementAsync(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None); /// - /// Decrement the specified field of an hash stored at key, and representing a floating point number, by the specified decrement. If the field does not exist, it is set to 0 before performing the operation. + /// Decrement the specified field of an hash stored at key, and representing a floating point number, by the specified decrement. + /// If the field does not exist, it is set to 0 before performing the operation. /// /// The key of the hash. /// The field in the hash to decrement. @@ -189,7 +208,8 @@ public interface IDatabaseAsync : IRedisAsync Task HashDecrementAsync(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None); /// - /// Removes the specified fields from the hash stored at key. Non-existing fields are ignored. Non-existing keys are treated as empty hashes and this command returns 0. + /// Removes the specified fields from the hash stored at key. + /// Non-existing fields are ignored. Non-existing keys are treated as empty hashes and this command returns 0. /// /// The key of the hash. /// The field in the hash to delete. @@ -199,7 +219,8 @@ public interface IDatabaseAsync : IRedisAsync Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// - /// Removes the specified fields from the hash stored at key. Non-existing fields are ignored. Non-existing keys are treated as empty hashes and this command returns 0. + /// Removes the specified fields from the hash stored at key. + /// Non-existing fields are ignored. Non-existing keys are treated as empty hashes and this command returns 0. /// /// The key of the hash. /// The fields in the hash to delete. @@ -214,7 +235,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the hash. /// The field in the hash to check. /// The flags to use for this operation. - /// 1 if the hash contains field. 0 if the hash does not contain field, or key does not exist. + /// if the hash contains field, if the hash does not contain field, or key does not exist. /// https://redis.io/commands/hexists Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); @@ -259,7 +280,9 @@ public interface IDatabaseAsync : IRedisAsync Task HashGetAllAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Increments the number stored at field in the hash stored at key by increment. If key does not exist, a new key holding a hash is created. If field does not exist the value is set to 0 before the operation is performed. + /// Increments the number stored at field in the hash stored at key by increment. + /// If key does not exist, a new key holding a hash is created. + /// If field does not exist the value is set to 0 before the operation is performed. /// /// The key of the hash. /// The field in the hash to increment. @@ -271,7 +294,8 @@ public interface IDatabaseAsync : IRedisAsync Task HashIncrementAsync(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None); /// - /// Increment the specified field of an hash stored at key, and representing a floating point number, by the specified increment. If the field does not exist, it is set to 0 before performing the operation. + /// Increment the specified field of an hash stored at key, and representing a floating point number, by the specified increment. + /// If the field does not exist, it is set to 0 before performing the operation. /// /// The key of the hash. /// The field in the hash to increment. @@ -301,7 +325,8 @@ public interface IDatabaseAsync : IRedisAsync Task HashLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// The HSCAN command is used to incrementally iterate over a hash; note: to resume an iteration via cursor, cast the original enumerable or enumerator to IScanningCursor. + /// The HSCAN command is used to incrementally iterate over a hash. + /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to . /// /// The key of the hash. /// The pattern of keys to get entries for. @@ -314,7 +339,9 @@ public interface IDatabaseAsync : IRedisAsync IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern = default(RedisValue), int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// - /// Sets the specified fields to their respective values in the hash stored at key. This command overwrites any specified fields that already exist in the hash, leaving other unspecified fields untouched. If key does not exist, a new key holding a hash is created. + /// Sets the specified fields to their respective values in the hash stored at key. + /// This command overwrites any specified fields that already exist in the hash, leaving other unspecified fields untouched. + /// If key does not exist, a new key holding a hash is created. /// /// The key of the hash. /// The entries to set in the hash. @@ -323,14 +350,16 @@ public interface IDatabaseAsync : IRedisAsync Task HashSetAsync(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None); /// - /// Sets field in the hash stored at key to value. If key does not exist, a new key holding a hash is created. If field already exists in the hash, it is overwritten. + /// Sets field in the hash stored at key to value. + /// If key does not exist, a new key holding a hash is created. + /// If field already exists in the hash, it is overwritten. /// /// The key of the hash. /// The field to set in the hash. /// The value to set. /// Which conditions under which to set the field value (defaults to always). /// The flags to use for this operation. - /// 1 if field is a new field in the hash and value was set. 0 if field already exists in the hash and the value was updated. + /// if field is a new field in the hash and value was set, if field already exists in the hash and the value was updated. /// https://redis.io/commands/hset /// https://redis.io/commands/hsetnx Task HashSetAsync(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); @@ -341,7 +370,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the hash. /// The field containing the string /// The flags to use for this operation. - /// the length of the string at field, or 0 when key does not exist. + /// The length of the string at field, or 0 when key does not exist. /// https://redis.io/commands/hstrlen Task HashStringLengthAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); @@ -360,7 +389,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the hyperloglog. /// The value to add. /// The flags to use for this operation. - /// True if at least 1 HyperLogLog internal register was altered, false otherwise. + /// if at least 1 HyperLogLog internal register was altered, otherwise. /// https://redis.io/commands/pfadd Task HyperLogLogAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); @@ -370,7 +399,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the hyperloglog. /// The values to add. /// The flags to use for this operation. - /// True if at least 1 HyperLogLog internal register was altered, false otherwise. + /// if at least 1 HyperLogLog internal register was altered, otherwise. /// https://redis.io/commands/pfadd Task HyperLogLogAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); @@ -425,7 +454,7 @@ public interface IDatabaseAsync : IRedisAsync /// /// The key to delete. /// The flags to use for this operation. - /// True if the key was removed. + /// if the key was removed. /// https://redis.io/commands/del /// https://redis.io/commands/unlink Task KeyDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -442,11 +471,12 @@ public interface IDatabaseAsync : IRedisAsync Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Serialize the value stored at key in a Redis-specific format and return it to the user. The returned value can be synthesized back into a Redis key using the RESTORE command. + /// Serialize the value stored at key in a Redis-specific format and return it to the user. + /// The returned value can be synthesized back into a Redis key using the RESTORE command. /// /// The key to dump. /// The flags to use for this operation. - /// the serialized value. + /// The serialized value. /// https://redis.io/commands/dump Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -455,7 +485,7 @@ public interface IDatabaseAsync : IRedisAsync /// /// The key to check. /// The flags to use for this operation. - /// 1 if the key exists. 0 if the key does not exist. + /// if the key exists. if the key does not exist. /// https://redis.io/commands/exists Task KeyExistsAsync(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -469,49 +499,71 @@ public interface IDatabaseAsync : IRedisAsync Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Set a timeout on key. After the timeout has expired, the key will automatically be deleted. A key with an associated timeout is said to be volatile in Redis terminology. + /// Set a timeout on key. After the timeout has expired, the key will automatically be deleted. + /// A key with an associated timeout is said to be volatile in Redis terminology. /// /// The key to set the expiration for. /// The timeout to set. /// The flags to use for this operation. - /// 1 if the timeout was set. 0 if key does not exist or the timeout could not be set. - /// If key is updated before the timeout has expired, then the timeout is removed as if the PERSIST command was invoked on key. - /// For Redis versions < 2.1.3, existing timeouts cannot be overwritten. So, if key already has an associated timeout, it will do nothing and return 0. Since Redis 2.1.3, you can update the timeout of a key. It is also possible to remove the timeout using the PERSIST command. See the page on key expiry for more information. + /// if the timeout was set. if key does not exist or the timeout could not be set. + /// + /// If key is updated before the timeout has expired, then the timeout is removed as if the PERSIST command was invoked on key. + /// + /// For Redis versions < 2.1.3, existing timeouts cannot be overwritten. + /// So, if key already has an associated timeout, it will do nothing and return 0. + /// + /// + /// Since Redis 2.1.3, you can update the timeout of a key. + /// It is also possible to remove the timeout using the PERSIST command. See the page on key expiry for more information. + /// + /// /// https://redis.io/commands/expire /// https://redis.io/commands/pexpire /// https://redis.io/commands/persist Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); /// - /// Set a timeout on key. After the timeout has expired, the key will automatically be deleted. A key with an associated timeout is said to be volatile in Redis terminology. + /// Set a timeout on key. After the timeout has expired, the key will automatically be deleted. + /// A key with an associated timeout is said to be volatile in Redis terminology. /// /// The key to set the expiration for. /// The exact date to expiry to set. /// The flags to use for this operation. - /// 1 if the timeout was set. 0 if key does not exist or the timeout could not be set. - /// If key is updated before the timeout has expired, then the timeout is removed as if the PERSIST command was invoked on key. - /// For Redis versions < 2.1.3, existing timeouts cannot be overwritten. So, if key already has an associated timeout, it will do nothing and return 0. Since Redis 2.1.3, you can update the timeout of a key. It is also possible to remove the timeout using the PERSIST command. See the page on key expiry for more information. + /// if the timeout was set. if key does not exist or the timeout could not be set. + /// + /// If key is updated before the timeout has expired, then the timeout is removed as if the PERSIST command was invoked on key. + /// + /// For Redis versions < 2.1.3, existing timeouts cannot be overwritten. + /// So, if key already has an associated timeout, it will do nothing and return 0. + /// + /// + /// Since Redis 2.1.3, you can update the timeout of a key. + /// It is also possible to remove the timeout using the PERSIST command. See the page on key expiry for more information. + /// + /// /// https://redis.io/commands/expireat /// https://redis.io/commands/pexpireat /// https://redis.io/commands/persist Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None); /// - /// Returns the time since the object stored at the specified key is idle (not requested by read or write operations) + /// Returns the time since the object stored at the specified key is idle (not requested by read or write operations). /// /// The key to get the time of. /// The flags to use for this operation. - /// The time since the object stored at the specified key is idle + /// The time since the object stored at the specified key is idle. /// https://redis.io/commands/object Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Move key from the currently selected database (see SELECT) to the specified destination database. When key already exists in the destination database, or it does not exist in the source database, it does nothing. It is possible to use MOVE as a locking primitive because of this. + /// Move key from the currently selected database (see SELECT) to the specified destination database. + /// When key already exists in the destination database, or it does not exist in the source database, it does nothing. + /// It is possible to use MOVE as a locking primitive because of this. /// /// The key to move. /// The database to move the key to. /// The flags to use for this operation. - /// 1 if key was moved; 0 if key was not moved. + /// if key was moved. if key was not moved. /// https://redis.io/commands/move Task KeyMoveAsync(RedisKey key, int database, CommandFlags flags = CommandFlags.None); @@ -520,7 +572,7 @@ public interface IDatabaseAsync : IRedisAsync /// /// The key to persist. /// The flags to use for this operation. - /// 1 if the timeout was removed. 0 if key does not exist or does not have an associated timeout. + /// if the timeout was removed. if key does not exist or does not have an associated timeout. /// https://redis.io/commands/persist Task KeyPersistAsync(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -533,20 +585,21 @@ public interface IDatabaseAsync : IRedisAsync Task KeyRandomAsync(CommandFlags flags = CommandFlags.None); /// - /// Renames key to newkey. It returns an error when the source and destination names are the same, or when key does not exist. + /// Renames to . + /// It returns an error when the source and destination names are the same, or when key does not exist. /// /// The key to rename. /// The key to rename to. /// What conditions to rename under (defaults to always). /// The flags to use for this operation. - /// True if the key was renamed, false otherwise. + /// if the key was renamed, otherwise. /// https://redis.io/commands/rename /// https://redis.io/commands/renamenx Task KeyRenameAsync(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None); /// /// Create a key associated with a value that is obtained by deserializing the provided serialized value (obtained via DUMP). - /// If ttl is 0 the key is created without any expire, otherwise the specified expire time(in milliseconds) is set. + /// If is 0 the key is created without any expire, otherwise the specified expire time (in milliseconds) is set. /// /// The key to restore. /// The value of the key. @@ -556,7 +609,8 @@ public interface IDatabaseAsync : IRedisAsync Task KeyRestoreAsync(RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None); /// - /// Returns the remaining time to live of a key that has a timeout. This introspection capability allows a Redis client to check how many seconds a given key will continue to be part of the dataset. + /// Returns the remaining time to live of a key that has a timeout. + /// This introspection capability allows a Redis client to check how many seconds a given key will continue to be part of the dataset. /// /// The key to check. /// The flags to use for this operation. @@ -565,7 +619,8 @@ public interface IDatabaseAsync : IRedisAsync Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Returns the string representation of the type of the value stored at key. The different types that can be returned are: string, list, set, zset and hash. + /// Returns the string representation of the type of the value stored at key. + /// The different types that can be returned are: string, list, set, zset and hash. /// /// The key to get the type of. /// The flags to use for this operation. @@ -574,10 +629,13 @@ public interface IDatabaseAsync : IRedisAsync Task KeyTypeAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Returns the element at index index in the list stored at key. The index is zero-based, so 0 means the first element, 1 the second element and so on. Negative indices can be used to designate elements starting at the tail of the list. Here, -1 means the last element, -2 means the penultimate and so forth. + /// Returns the element at index in the list stored at key. + /// The index is zero-based, so 0 means the first element, 1 the second element and so on. + /// Negative indices can be used to designate elements starting at the tail of the list. + /// Here, -1 means the last element, -2 means the penultimate and so forth. /// /// The key of the list. - /// The index position to ge the value at. + /// The index position to get the value at. /// The flags to use for this operation. /// The requested element, or nil when index is out of range. /// https://redis.io/commands/lindex @@ -618,17 +676,18 @@ public interface IDatabaseAsync : IRedisAsync /// /// Removes and returns count elements from the head of the list stored at key. - /// If the list contains less than count elements, removes and returns the number of elements in the list + /// If the list contains less than count elements, removes and returns the number of elements in the list. /// /// The key of the list. /// The number of elements to remove /// The flags to use for this operation. - /// Array of values that were popped, or nil if the key doesn't exist + /// Array of values that were popped, or nil if the key doesn't exist. /// https://redis.io/commands/lpop Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// - /// Insert the specified value at the head of the list stored at key. If key does not exist, it is created as empty list before performing the push operations. + /// Insert the specified value at the head of the list stored at key. + /// If key does not exist, it is created as empty list before performing the push operations. /// /// The key of the list. /// The value to add to the head of the list. @@ -640,7 +699,8 @@ public interface IDatabaseAsync : IRedisAsync Task ListLeftPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); /// - /// Insert the specified value at the head of the list stored at key. If key does not exist, it is created as empty list before performing the push operations. + /// Insert the specified value at the head of the list stored at key. + /// If key does not exist, it is created as empty list before performing the push operations. /// /// The key of the list. /// The value to add to the head of the list. @@ -652,8 +712,10 @@ public interface IDatabaseAsync : IRedisAsync Task ListLeftPushAsync(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); /// - /// Insert all the specified values at the head of the list stored at key. If key does not exist, it is created as empty list before performing the push operations. - /// Elements are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. So for instance the command LPUSH mylist a b c will result into a list containing c as first element, b as second element and a as third element. + /// Insert all the specified values at the head of the list stored at key. + /// If key does not exist, it is created as empty list before performing the push operations. + /// Elements are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. + /// So for instance the command LPUSH mylist a b c will result into a list containing c as first element, b as second element and a as third element. /// /// The key of the list. /// The values to add to the head of the list. @@ -672,7 +734,8 @@ public interface IDatabaseAsync : IRedisAsync Task ListLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Returns the specified elements of the list stored at key. The offsets start and stop are zero-based indexes, with 0 being the first element of the list (the head of the list), 1 being the next element and so on. + /// Returns the specified elements of the list stored at key. + /// The offsets start and stop are zero-based indexes, with 0 being the first element of the list (the head of the list), 1 being the next element and so on. /// These offsets can also be negative numbers indicating offsets starting at the end of the list.For example, -1 is the last element of the list, -2 the penultimate, and so on. /// Note that if you have a list of numbers from 0 to 100, LRANGE list 0 10 will return 11 elements, that is, the rightmost item is included. /// @@ -685,10 +748,13 @@ public interface IDatabaseAsync : IRedisAsync Task ListRangeAsync(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None); /// - /// Removes the first count occurrences of elements equal to value from the list stored at key. The count argument influences the operation in the following ways: - /// count > 0: Remove elements equal to value moving from head to tail. - /// count < 0: Remove elements equal to value moving from tail to head. - /// count = 0: Remove all elements equal to value. + /// Removes the first count occurrences of elements equal to value from the list stored at key. + /// The count argument influences the operation in the following ways: + /// + /// count > 0: Remove elements equal to value moving from head to tail. + /// count < 0: Remove elements equal to value moving from tail to head. + /// count = 0: Remove all elements equal to value. + /// /// /// The key of the list. /// The value to remove from the list. @@ -709,12 +775,12 @@ public interface IDatabaseAsync : IRedisAsync /// /// Removes and returns count elements from the end the list stored at key. - /// If the list contains less than count elements, removes and returns the number of elements in the list + /// If the list contains less than count elements, removes and returns the number of elements in the list. /// /// The key of the list. /// The number of elements to pop /// The flags to use for this operation. - /// Array of values that were popped, or nil if the key doesn't exist + /// Array of values that were popped, or nil if the key doesn't exist. /// https://redis.io/commands/rpop Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); @@ -729,7 +795,8 @@ public interface IDatabaseAsync : IRedisAsync Task ListRightPopLeftPushAsync(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None); /// - /// Insert the specified value at the tail of the list stored at key. If key does not exist, it is created as empty list before performing the push operation. + /// Insert the specified value at the tail of the list stored at key. + /// If key does not exist, it is created as empty list before performing the push operation. /// /// The key of the list. /// The value to add to the tail of the list. @@ -741,7 +808,8 @@ public interface IDatabaseAsync : IRedisAsync Task ListRightPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); /// - /// Insert the specified value at the tail of the list stored at key. If key does not exist, it is created as empty list before performing the push operation. + /// Insert the specified value at the tail of the list stored at key. + /// If key does not exist, it is created as empty list before performing the push operation. /// /// The key of the list. /// The values to add to the tail of the list. @@ -753,8 +821,10 @@ public interface IDatabaseAsync : IRedisAsync Task ListRightPushAsync(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); /// - /// Insert all the specified values at the tail of the list stored at key. If key does not exist, it is created as empty list before performing the push operation. - /// Elements are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. So for instance the command RPUSH mylist a b c will result into a list containing a as first element, b as second element and c as third element. + /// Insert all the specified values at the tail of the list stored at key. + /// If key does not exist, it is created as empty list before performing the push operation. + /// Elements are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. + /// So for instance the command RPUSH mylist a b c will result into a list containing a as first element, b as second element and c as third element. /// /// The key of the list. /// The values to add to the tail of the list. @@ -764,7 +834,9 @@ public interface IDatabaseAsync : IRedisAsync Task ListRightPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags); /// - /// Sets the list element at index to value. For more information on the index argument, see ListGetByIndex. An error is returned for out of range indexes. + /// Sets the list element at index to value. + /// For more information on the index argument, see . + /// An error is returned for out of range indexes. /// /// The key of the list. /// The index to set the value at. @@ -774,7 +846,8 @@ public interface IDatabaseAsync : IRedisAsync Task ListSetByIndexAsync(RedisKey key, long index, RedisValue value, CommandFlags flags = CommandFlags.None); /// - /// Trim an existing list so that it will contain only the specified range of elements specified. Both start and stop are zero-based indexes, where 0 is the first element of the list (the head), 1 the next element and so on. + /// Trim an existing list so that it will contain only the specified range of elements specified. + /// Both start and stop are zero-based indexes, where 0 is the first element of the list (the head), 1 the next element and so on. /// For example: LTRIM foobar 0 2 will modify the list stored at foobar so that only the first three elements of the list will remain. /// start and end can also be negative numbers indicating offsets from the end of the list, where -1 is the last element of the list, -2 the penultimate element and so on. /// @@ -792,7 +865,7 @@ public interface IDatabaseAsync : IRedisAsync /// The value to set at the key. /// The expiration of the lock key. /// The flags to use for this operation. - /// True if the lock was successfully extended. + /// if the lock was successfully extended. Task LockExtendAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None); /// @@ -807,9 +880,9 @@ public interface IDatabaseAsync : IRedisAsync /// Releases a lock, if the token value is correct. /// /// The key of the lock. - /// The value at the key tht must match. + /// The value at the key that must match. /// The flags to use for this operation. - /// True if the lock was successfully released, false otherwise. + /// if the lock was successfully released, otherwise. Task LockReleaseAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -819,7 +892,7 @@ public interface IDatabaseAsync : IRedisAsync /// The value to set at the key. /// The expiration of the lock key. /// The flags to use for this operation. - /// True if the lock was successfully taken, false otherwise. + /// if the lock was successfully taken, otherwise. Task LockTakeAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None); /// @@ -836,26 +909,24 @@ public interface IDatabaseAsync : IRedisAsync Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None); /// - /// Execute an arbitrary command against the server; this is primarily intended for - /// executing modules, but may also be used to provide access to new features that lack - /// a direct API. + /// Execute an arbitrary command against the server; this is primarily intended for executing modules, + /// but may also be used to provide access to new features that lack a direct API. /// /// The command to run. /// The arguments to pass for the command. - /// This API should be considered an advanced feature; inappropriate use can be harmful - /// A dynamic representation of the command's result + /// This API should be considered an advanced feature; inappropriate use can be harmful. + /// A dynamic representation of the command's result. Task ExecuteAsync(string command, params object[] args); /// - /// Execute an arbitrary command against the server; this is primarily intended for - /// executing modules, but may also be used to provide access to new features that lack - /// a direct API. + /// Execute an arbitrary command against the server; this is primarily intended for executing modules, + /// but may also be used to provide access to new features that lack a direct API. /// /// The command to run. /// The arguments to pass for the command. /// The flags to use for this operation. - /// This API should be considered an advanced feature; inappropriate use can be harmful - /// A dynamic representation of the command's result + /// This API should be considered an advanced feature; inappropriate use can be harmful. + /// A dynamic representation of the command's result. Task ExecuteAsync(string command, ICollection args, CommandFlags flags = CommandFlags.None); /// @@ -865,19 +936,19 @@ public interface IDatabaseAsync : IRedisAsync /// The keys to execute against. /// The values to execute against. /// The flags to use for this operation. - /// A dynamic representation of the script's result + /// A dynamic representation of the script's result. /// https://redis.io/commands/eval /// https://redis.io/commands/evalsha Task ScriptEvaluateAsync(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None); /// - /// Execute a Lua script against the server using just the SHA1 hash + /// Execute a Lua script against the server using just the SHA1 hash. /// /// The hash of the script to execute. /// The keys to execute against. /// The values to execute against. /// The flags to use for this operation. - /// A dynamic representation of the script's result + /// A dynamic representation of the script's result. /// https://redis.io/commands/evalsha Task ScriptEvaluateAsync(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None); @@ -888,7 +959,7 @@ public interface IDatabaseAsync : IRedisAsync /// The script to execute. /// The parameters to pass to the script. /// The flags to use for this operation. - /// A dynamic representation of the script's result + /// A dynamic representation of the script's result. /// https://redis.io/commands/eval Task ScriptEvaluateAsync(LuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None); @@ -900,7 +971,7 @@ public interface IDatabaseAsync : IRedisAsync /// The already-loaded script to execute. /// The parameters to pass to the script. /// The flags to use for this operation. - /// A dynamic representation of the script's result + /// A dynamic representation of the script's result. /// https://redis.io/commands/eval Task ScriptEvaluateAsync(LoadedLuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None); @@ -912,7 +983,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the set. /// The value to add to the set. /// The flags to use for this operation. - /// True if the specified member was not already present in the set, else False + /// if the specified member was not already present in the set, else . /// https://redis.io/commands/sadd Task SetAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); @@ -954,7 +1025,8 @@ public interface IDatabaseAsync : IRedisAsync Task SetCombineAsync(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// This command is equal to SetCombine, but instead of returning the resulting set, it is stored in destination. If destination already exists, it is overwritten. + /// This command is equal to SetCombine, but instead of returning the resulting set, it is stored in destination. + /// If destination already exists, it is overwritten. /// /// The operation to perform. /// The key of the destination set. @@ -968,7 +1040,8 @@ public interface IDatabaseAsync : IRedisAsync Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); /// - /// This command is equal to SetCombine, but instead of returning the resulting set, it is stored in destination. If destination already exists, it is overwritten. + /// This command is equal to SetCombine, but instead of returning the resulting set, it is stored in destination. + /// If destination already exists, it is overwritten. /// /// The operation to perform. /// The key of the destination set. @@ -986,7 +1059,10 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the set. /// The value to check for . /// The flags to use for this operation. - /// 1 if the element is a member of the set. 0 if the element is not a member of the set, or if key does not exist. + /// + /// if the element is a member of the set. + /// if the element is not a member of the set, or if key does not exist. + /// /// https://redis.io/commands/sismember Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); @@ -1009,14 +1085,18 @@ public interface IDatabaseAsync : IRedisAsync Task SetMembersAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Move member from the set at source to the set at destination. This operation is atomic. In every given moment the element will appear to be a member of source or destination for other clients. + /// Move member from the set at source to the set at destination. + /// This operation is atomic. In every given moment the element will appear to be a member of source or destination for other clients. /// When the specified element already exists in the destination set, it is only removed from the source set. /// /// The key of the source set. /// The key of the destination set. /// The value to move. /// The flags to use for this operation. - /// 1 if the element is moved. 0 if the element is not a member of source and no operation was performed. + /// + /// if the element is moved. + /// if the element is not a member of source and no operation was performed. + /// /// https://redis.io/commands/smove Task SetMoveAsync(RedisKey source, RedisKey destination, RedisValue value, CommandFlags flags = CommandFlags.None); @@ -1044,33 +1124,36 @@ public interface IDatabaseAsync : IRedisAsync /// /// The key of the set. /// The flags to use for this operation. - /// The randomly selected element, or nil when key does not exist + /// The randomly selected element, or nil when key does not exist. /// https://redis.io/commands/srandmember Task SetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Return an array of count distinct elements if count is positive. If called with a negative count the behavior changes and the command is allowed to return the same element multiple times. + /// Return an array of count distinct elements if count is positive. + /// If called with a negative count the behavior changes and the command is allowed to return the same element multiple times. /// In this case the number of returned elements is the absolute value of the specified count. /// /// The key of the set. /// The count of members to get. /// The flags to use for this operation. - /// An array of elements, or an empty array when key does not exist + /// An array of elements, or an empty array when key does not exist. /// https://redis.io/commands/srandmember Task SetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// - /// Remove the specified member from the set stored at key. Specified members that are not a member of this set are ignored. + /// Remove the specified member from the set stored at key. + /// Specified members that are not a member of this set are ignored. /// /// The key of the set. /// The value to remove. /// The flags to use for this operation. - /// True if the specified member was already present in the set, else False + /// if the specified member was already present in the set, otherwise. /// https://redis.io/commands/srem Task SetRemoveAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// - /// Remove the specified members from the set stored at key. Specified members that are not a member of this set are ignored. + /// Remove the specified members from the set stored at key. + /// Specified members that are not a member of this set are ignored. /// /// The key of the set. /// The values to remove. @@ -1080,11 +1163,26 @@ public interface IDatabaseAsync : IRedisAsync Task SetRemoveAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); /// - /// Sorts a list, set or sorted set (numerically or alphabetically, ascending by default); By default, the elements themselves are compared, but the values can also be - /// used to perform external key-lookups using the by parameter. By default, the elements themselves are returned, but external key-lookups (one or many) can - /// be performed instead by specifying the get parameter (note that # specifies the element itself, when used in get). - /// Referring to the redis SORT documentation for examples is recommended. When used in hashes, by and get - /// can be used to specify fields using -> notation (again, refer to redis documentation). + /// The SSCAN command is used to incrementally iterate over set. + /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to . + /// + /// The key of the set. + /// The pattern to match. + /// The page size to iterate by. + /// The cursor position to start at. + /// The page offset to start at. + /// The flags to use for this operation. + /// Yields all matching elements of the set. + /// https://redis.io/commands/sscan + IAsyncEnumerable SetScanAsync(RedisKey key, RedisValue pattern = default(RedisValue), int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + + /// + /// Sorts a list, set or sorted set (numerically or alphabetically, ascending by default). + /// By default, the elements themselves are compared, but the values can also be used to perform external key-lookups using the by parameter. + /// By default, the elements themselves are returned, but external key-lookups (one or many) can be performed instead by specifying + /// the get parameter (note that # specifies the element itself, when used in get). + /// Referring to the redis SORT documentation for examples is recommended. + /// When used in hashes, by and get can be used to specify fields using -> notation (again, refer to redis documentation). /// /// The key of the list, set, or sorted set. /// How many entries to skip on the return. @@ -1099,11 +1197,12 @@ public interface IDatabaseAsync : IRedisAsync Task SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None); /// - /// Sorts a list, set or sorted set (numerically or alphabetically, ascending by default); By default, the elements themselves are compared, but the values can also be - /// used to perform external key-lookups using the by parameter. By default, the elements themselves are returned, but external key-lookups (one or many) can - /// be performed instead by specifying the get parameter (note that # specifies the element itself, when used in get). - /// Referring to the redis SORT documentation for examples is recommended. When used in hashes, by and get - /// can be used to specify fields using -> notation (again, refer to redis documentation). + /// Sorts a list, set or sorted set (numerically or alphabetically, ascending by default). + /// By default, the elements themselves are compared, but the values can also be used to perform external key-lookups using the by parameter. + /// By default, the elements themselves are returned, but external key-lookups (one or many) can be performed instead by specifying + /// the get parameter (note that # specifies the element itself, when used in get). + /// Referring to the redis SORT documentation for examples is recommended. + /// When used in hashes, by and get can be used to specify fields using -> notation (again, refer to redis documentation). /// /// The destination key to store results in. /// The key of the list, set, or sorted set. @@ -1119,30 +1218,33 @@ public interface IDatabaseAsync : IRedisAsync Task SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None); /// - /// Adds the specified member with the specified score to the sorted set stored at key. If the specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. + /// Adds the specified member with the specified score to the sorted set stored at key. + /// If the specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. /// /// The key of the sorted set. /// The member to add to the sorted set. /// The score for the member to add to the sorted set. /// The flags to use for this operation. - /// True if the value was added, False if it already existed (the score is still updated) + /// if the value was added. if it already existed (the score is still updated). /// https://redis.io/commands/zadd Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, CommandFlags flags); /// - /// Adds the specified member with the specified score to the sorted set stored at key. If the specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. + /// Adds the specified member with the specified score to the sorted set stored at key. + /// If the specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. /// /// The key of the sorted set. /// The member to add to the sorted set. /// The score for the member to add to the sorted set. /// What conditions to add the element under (defaults to always). /// The flags to use for this operation. - /// True if the value was added, False if it already existed (the score is still updated) + /// if the value was added. if it already existed (the score is still updated). /// https://redis.io/commands/zadd Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None); /// - /// Adds all the specified members with the specified scores to the sorted set stored at key. If a specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. + /// Adds all the specified members with the specified scores to the sorted set stored at key. + /// If a specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. /// /// The key of the sorted set. /// The members and values to add to the sorted set. @@ -1152,7 +1254,8 @@ public interface IDatabaseAsync : IRedisAsync Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, CommandFlags flags); /// - /// Adds all the specified members with the specified scores to the sorted set stored at key. If a specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. + /// Adds all the specified members with the specified scores to the sorted set stored at key. + /// If a specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. /// /// The key of the sorted set. /// The members and values to add to the sorted set. @@ -1174,7 +1277,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// https://redis.io/commands/zunionstore /// https://redis.io/commands/zinterstore - /// the number of elements in the resulting sorted set at destination + /// The number of elements in the resulting sorted set at destination. Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// @@ -1189,11 +1292,12 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// https://redis.io/commands/zunionstore /// https://redis.io/commands/zinterstore - /// the number of elements in the resulting sorted set at destination + /// The number of elements in the resulting sorted set at destination. Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[] weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// - /// Decrements the score of member in the sorted set stored at key by decrement. If member does not exist in the sorted set, it is added with -decrement as its score (as if its previous score was 0.0). + /// Decrements the score of member in the sorted set stored at key by decrement. + /// If member does not exist in the sorted set, it is added with -decrement as its score (as if its previous score was 0.0). /// /// The key of the sorted set. /// The member to decrement. @@ -1227,7 +1331,8 @@ public interface IDatabaseAsync : IRedisAsync Task SortedSetLengthAsync(RedisKey key, double min = double.NegativeInfinity, double max = double.PositiveInfinity, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// - /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering, this command returns the number of elements in the sorted set at key with a value between min and max. + /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering. + /// This command returns the number of elements in the sorted set at key with a value between min and max. /// /// The key of the sorted set. /// The min value to filter by. @@ -1239,8 +1344,11 @@ public interface IDatabaseAsync : IRedisAsync Task SortedSetLengthByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// - /// Returns the specified range of elements in the sorted set stored at key. By default the elements are considered to be ordered from the lowest to the highest score. Lexicographical order is used for elements with equal score. - /// Both start and stop are zero-based indexes, where 0 is the first element, 1 is the next element and so on. They can also be negative numbers indicating offsets from the end of the sorted set, with -1 being the last element of the sorted set, -2 the penultimate element and so on. + /// Returns the specified range of elements in the sorted set stored at key. + /// By default the elements are considered to be ordered from the lowest to the highest score. + /// Lexicographical order is used for elements with equal score. + /// Both start and stop are zero-based indexes, where 0 is the first element, 1 is the next element and so on. + /// They can also be negative numbers indicating offsets from the end of the sorted set, with -1 being the last element of the sorted set, -2 the penultimate element and so on. /// /// The key of the sorted set. /// The start index to get. @@ -1253,8 +1361,11 @@ public interface IDatabaseAsync : IRedisAsync Task SortedSetRangeByRankAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); /// - /// Returns the specified range of elements in the sorted set stored at key. By default the elements are considered to be ordered from the lowest to the highest score. Lexicographical order is used for elements with equal score. - /// Both start and stop are zero-based indexes, where 0 is the first element, 1 is the next element and so on. They can also be negative numbers indicating offsets from the end of the sorted set, with -1 being the last element of the sorted set, -2 the penultimate element and so on. + /// Returns the specified range of elements in the sorted set stored at key. + /// By default the elements are considered to be ordered from the lowest to the highest score. + /// Lexicographical order is used for elements with equal score. + /// Both start and stop are zero-based indexes, where 0 is the first element, 1 is the next element and so on. + /// They can also be negative numbers indicating offsets from the end of the sorted set, with -1 being the last element of the sorted set, -2 the penultimate element and so on. /// /// The key of the sorted set. /// The start index to get. @@ -1267,8 +1378,11 @@ public interface IDatabaseAsync : IRedisAsync Task SortedSetRangeByRankWithScoresAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); /// - /// Returns the specified range of elements in the sorted set stored at key. By default the elements are considered to be ordered from the lowest to the highest score. Lexicographical order is used for elements with equal score. - /// Start and stop are used to specify the min and max range for score values. Similar to other range methods the values are inclusive. + /// Returns the specified range of elements in the sorted set stored at key. + /// By default the elements are considered to be ordered from the lowest to the highest score. + /// Lexicographical order is used for elements with equal score. + /// Start and stop are used to specify the min and max range for score values. + /// Similar to other range methods the values are inclusive. /// /// The key of the sorted set. /// The minimum score to filter by. @@ -1291,8 +1405,11 @@ Task SortedSetRangeByScoreAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Returns the specified range of elements in the sorted set stored at key. By default the elements are considered to be ordered from the lowest to the highest score. Lexicographical order is used for elements with equal score. - /// Start and stop are used to specify the min and max range for score values. Similar to other range methods the values are inclusive. + /// Returns the specified range of elements in the sorted set stored at key. + /// By default the elements are considered to be ordered from the lowest to the highest score. + /// Lexicographical order is used for elements with equal score. + /// Start and stop are used to specify the min and max range for score values. + /// Similar to other range methods the values are inclusive. /// /// The key of the sorted set. /// The minimum score to filter by. @@ -1315,7 +1432,8 @@ Task SortedSetRangeByScoreWithScoresAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering, this command returns all the elements in the sorted set at key with a value between min and max. + /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering. + /// This command returns all the elements in the sorted set at key with a value between min and max. /// /// The key of the sorted set. /// The min value to filter by. @@ -1325,7 +1443,7 @@ Task SortedSetRangeByScoreWithScoresAsync(RedisKey key, /// How many items to take. /// The flags to use for this operation. /// https://redis.io/commands/zrangebylex - /// list of elements in the specified score range. + /// List of elements in the specified score range. Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, @@ -1335,7 +1453,8 @@ Task SortedSetRangeByValueAsync(RedisKey key, CommandFlags flags = CommandFlags.None); // defaults removed to avoid ambiguity with overload with order /// - /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering, this command returns all the elements in the sorted set at key with a value between min and max. + /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering. + /// This command returns all the elements in the sorted set at key with a value between min and max. /// /// The key of the sorted set. /// The min value to filter by. @@ -1347,7 +1466,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The flags to use for this operation. /// https://redis.io/commands/zrangebylex /// https://redis.io/commands/zrevrangebylex - /// list of elements in the specified score range. + /// List of elements in the specified score range. Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min = default(RedisValue), RedisValue max = default(RedisValue), @@ -1358,13 +1477,14 @@ Task SortedSetRangeByValueAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Returns the rank of member in the sorted set stored at key, by default with the scores ordered from low to high. The rank (or index) is 0-based, which means that the member with the lowest score has rank 0. + /// Returns the rank of member in the sorted set stored at key, by default with the scores ordered from low to high. + /// The rank (or index) is 0-based, which means that the member with the lowest score has rank 0. /// /// The key of the sorted set. /// The member to get the rank of. /// The order to sort by (defaults to ascending). /// The flags to use for this operation. - /// If member exists in the sorted set, the rank of member; If member does not exist in the sorted set or key does not exist, null + /// If member exists in the sorted set, the rank of member. If member does not exist in the sorted set or key does not exist, . /// https://redis.io/commands/zrank /// https://redis.io/commands/zrevrank Task SortedSetRankAsync(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); @@ -1375,7 +1495,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The key of the sorted set. /// The member to remove. /// The flags to use for this operation. - /// True if the member existed in the sorted set and was removed; False otherwise. + /// if the member existed in the sorted set and was removed. otherwise. /// https://redis.io/commands/zrem Task SortedSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); @@ -1390,7 +1510,10 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task SortedSetRemoveAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); /// - /// Removes all elements in the sorted set stored at key with rank between start and stop. Both start and stop are 0 -based indexes with 0 being the element with the lowest score. These indexes can be negative numbers, where they indicate offsets starting at the element with the highest score. For example: -1 is the element with the highest score, -2 the element with the second highest score and so forth. + /// Removes all elements in the sorted set stored at key with rank between start and stop. + /// Both start and stop are 0 -based indexes with 0 being the element with the lowest score. + /// These indexes can be negative numbers, where they indicate offsets starting at the element with the highest score. + /// For example: -1 is the element with the highest score, -2 the element with the second highest score and so forth. /// /// The key of the sorted set. /// The minimum rank to remove. @@ -1413,45 +1536,39 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task SortedSetRemoveRangeByScoreAsync(RedisKey key, double start, double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// - /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering, this command removes all elements in the sorted set stored at key between the lexicographical range specified by min and max. + /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering. + /// This command removes all elements in the sorted set stored at key between the lexicographical range specified by min and max. /// /// The key of the sorted set. /// The minimum value to remove. /// The maximum value to remove. /// Which of and to exclude (defaults to both inclusive). /// The flags to use for this operation. - /// the number of elements removed. + /// The number of elements removed. /// https://redis.io/commands/zremrangebylex Task SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// - /// The SSCAN command is used to incrementally iterate over set; note: to resume an iteration via cursor, cast the original enumerable or enumerator to IScanningCursor. - /// - /// The key of the set. - /// The pattern to match. - /// The page size to iterate by. - /// The cursor position to start at. - /// The page offset to start at. - /// The flags to use for this operation. - /// Yields all matching elements of the set. - /// https://redis.io/commands/sscan - IAsyncEnumerable SetScanAsync(RedisKey key, RedisValue pattern = default(RedisValue), int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); - - /// - /// The ZSCAN command is used to incrementally iterate over a sorted set + /// The ZSCAN command is used to incrementally iterate over a sorted set. /// /// The key of the sorted set. /// The pattern to match. /// The page size to iterate by. - /// The flags to use for this operation. /// The cursor position to start at. /// The page offset to start at. + /// The flags to use for this operation. /// Yields all matching elements of the sorted set. /// https://redis.io/commands/zscan - IAsyncEnumerable SortedSetScanAsync(RedisKey key, RedisValue pattern = default(RedisValue), int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + IAsyncEnumerable SortedSetScanAsync(RedisKey key, + RedisValue pattern = default(RedisValue), + int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, + long cursor = RedisBase.CursorUtils.Origin, + int pageOffset = 0, + CommandFlags flags = CommandFlags.None); /// - /// Returns the score of member in the sorted set at key; If member does not exist in the sorted set, or key does not exist, nil is returned. + /// Returns the score of member in the sorted set at key. + /// If member does not exist in the sorted set, or key does not exist, nil is returned. /// /// The key of the sorted set. /// The member to get a score for. @@ -1506,7 +1623,9 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); /// - /// Adds an entry using the specified values to the given stream key. If key does not exist, a new key holding a stream is created. The command returns the ID of the newly created stream entry. + /// Adds an entry using the specified values to the given stream key. + /// If key does not exist, a new key holding a stream is created. + /// The command returns the ID of the newly created stream entry. /// /// The key of the stream. /// The field name for the stream entry. @@ -1520,7 +1639,9 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); /// - /// Adds an entry using the specified values to the given stream key. If key does not exist, a new key holding a stream is created. The command returns the ID of the newly created stream entry. + /// Adds an entry using the specified values to the given stream key. + /// If key does not exist, a new key holding a stream is created. + /// The command returns the ID of the newly created stream entry. /// /// The key of the stream. /// The fields and their associated values to set in the stream entry. @@ -1533,11 +1654,12 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); /// - /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. This method returns the complete message for the claimed message(s). + /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. + /// This method returns the complete message for the claimed message(s). /// /// The key of the stream. /// The consumer group. - /// The consumer claiming the given messages. + /// The consumer claiming the given message(s). /// The minimum message idle time to allow the reassignment of the message(s). /// The IDs of the messages to claim for the given consumer. /// The flags to use for this operation. @@ -1546,7 +1668,8 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StreamClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); /// - /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. This method returns the IDs for the claimed message(s). + /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. + /// This method returns the IDs for the claimed message(s). /// /// The key of the stream. /// The consumer group. @@ -1565,11 +1688,12 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The name of the consumer group. /// The position from which to read for the consumer group. /// The flags to use for this operation. - /// True if successful, otherwise false. + /// if successful, otherwise. Task StreamConsumerGroupSetPositionAsync(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None); /// - /// Retrieve information about the consumers for the given consumer group. This is the equivalent of calling "XINFO GROUPS key group". + /// Retrieve information about the consumers for the given consumer group. + /// This is the equivalent of calling "XINFO GROUPS key group". /// /// The key of the stream. /// The consumer group name. @@ -1585,7 +1709,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The name of the group to create. /// The position to begin reading the stream. Defaults to . /// The flags to use for this operation. - /// True if the group was created. + /// if the group was created, otherwise. /// https://redis.io/topics/streams-intro Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position, CommandFlags flags); @@ -1597,7 +1721,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The position to begin reading the stream. Defaults to . /// Create the stream if it does not already exist. /// The flags to use for this operation. - /// True if the group was created. + /// if the group was created, otherwise. /// https://redis.io/topics/streams-intro Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None); @@ -1627,7 +1751,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The key of the stream. /// The name of the consumer group. /// The flags to use for this operation. - /// True if deleted, otherwise false. + /// if deleted, otherwise. Task StreamDeleteConsumerGroupAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); /// @@ -1658,12 +1782,17 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StreamLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// View information about pending messages for a stream. A pending message is a message read using StreamReadGroup (XREADGROUP) but not yet acknowledged. + /// View information about pending messages for a stream. + /// A pending message is a message read using StreamReadGroup (XREADGROUP) but not yet acknowledged. /// /// The key of the stream. /// The name of the consumer group /// The flags to use for this operation. - /// An instance of . contains the number of pending messages, the highest and lowest ID of the pending messages, and the consumers with their pending message count. + /// + /// An instance of . + /// contains the number of pending messages. + /// The highest and lowest ID of the pending messages, and the consumers with their pending message count. + /// /// The equivalent of calling XPENDING key group. /// https://redis.io/commands/xpending Task StreamPendingAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); @@ -1690,7 +1819,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The minimum ID from which to read the stream. The method will default to reading from the beginning of the stream. /// The maximum ID to read to within the stream. The method will default to reading to the end of the stream. /// The maximum number of messages to return. - /// The order of the messages. will execute XRANGE and wil execute XREVRANGE. + /// The order of the messages. will execute XRANGE and will execute XREVRANGE. /// The flags to use for this operation. /// Returns an instance of for each message returned. /// https://redis.io/commands/xrange @@ -1725,7 +1854,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The key of the stream. /// The name of the consumer group. /// The consumer name. - /// The position from which to read the stream. Defaults to when null. + /// The position from which to read the stream. Defaults to when . /// The maximum number of messages to return. /// The flags to use for this operation. /// Returns a value of for each message returned. @@ -1738,7 +1867,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The key of the stream. /// The name of the consumer group. /// The consumer name. - /// The position from which to read the stream. Defaults to when null. + /// The position from which to read the stream. Defaults to when . /// The maximum number of messages to return. /// When true, the message will not be added to the pending message list. /// The flags to use for this operation. @@ -1747,8 +1876,8 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None); /// - /// Read from multiple streams into the given consumer group. The consumer group with the given - /// will need to have been created for each stream prior to calling this method. + /// Read from multiple streams into the given consumer group. + /// The consumer group with the given will need to have been created for each stream prior to calling this method. /// /// Array of streams and the positions from which to begin reading for each stream. /// The name of the consumer group. @@ -1761,8 +1890,8 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags); /// - /// Read from multiple streams into the given consumer group. The consumer group with the given - /// will need to have been created for each stream prior to calling this method. + /// Read from multiple streams into the given consumer group. + /// The consumer group with the given will need to have been created for each stream prior to calling this method. /// /// Array of streams and the positions from which to begin reading for each stream. /// The name of the consumer group. @@ -1787,8 +1916,8 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); /// - /// If key already exists and is a string, this command appends the value at the end of the string. If key does not exist it is created and set as an empty string, - /// so APPEND will be similar to SET in this special case. + /// If key already exists and is a string, this command appends the value at the end of the string. + /// If key does not exist it is created and set as an empty string, so APPEND will be similar to SET in this special case. /// /// The key of the string. /// The value to append to the string. @@ -1799,7 +1928,8 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// /// Count the number of set bits (population counting) in a string. - /// By default all the bytes contained in the string are examined. It is possible to specify the counting operation only in an interval passing the additional arguments start and end. + /// By default all the bytes contained in the string are examined. + /// It is possible to specify the counting operation only in an interval passing the additional arguments start and end. /// Like for the GETRANGE command start and end can contain negative values in order to index bytes starting from the end of the string, where -1 is the last byte, -2 is the penultimate, and so forth. /// /// The key of the string. @@ -1814,7 +1944,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key. /// The BITOP command supports four bitwise operations; note that NOT is a unary operator: the second key should be omitted in this case /// and only the first key will be considered. - /// The result of the operation is always stored at destkey. + /// The result of the operation is always stored at . /// /// The operation to perform. /// The destination key to store the result in. @@ -1828,7 +1958,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// /// Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key. /// The BITOP command supports four bitwise operations; note that NOT is a unary operator. - /// The result of the operation is always stored at destkey. + /// The result of the operation is always stored at . /// /// The operation to perform. /// The destination key to store the result in. @@ -1841,7 +1971,8 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// /// Return the position of the first bit set to 1 or 0 in a string. /// The position is returned thinking at the string as an array of bits from left to right where the first byte most significant bit is at position 0, the second byte most significant bit is at position 8 and so forth. - /// An start and end may be specified; these are in bytes, not bits; start and end can contain negative values in order to index bytes starting from the end of the string, where -1 is the last byte, -2 is the penultimate, and so forth. + /// A and may be specified - these are in bytes, not bits. + /// and can contain negative values in order to index bytes starting from the end of the string, where -1 is the last byte, -2 is the penultimate, and so forth. /// /// The key of the string. /// True to check for the first 1 bit, false to check for the first 0 bit. @@ -1854,8 +1985,10 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None); /// - /// Decrements the number stored at key by decrement. If the key does not exist, it is set to 0 before performing the operation. - /// An error is returned if the key contains a value of the wrong type or contains a string that is not representable as integer. This operation is limited to 64 bit signed integers. + /// Decrements the number stored at key by decrement. + /// If the key does not exist, it is set to 0 before performing the operation. + /// An error is returned if the key contains a value of the wrong type or contains a string that is not representable as integer. + /// This operation is limited to 64 bit signed integers. /// /// The key of the string. /// The amount to decrement by (defaults to 1). @@ -1866,7 +1999,9 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); /// - /// Decrements the string representing a floating point number stored at key by the specified decrement. If the key does not exist, it is set to 0 before performing the operation. The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. + /// Decrements the string representing a floating point number stored at key by the specified decrement. + /// If the key does not exist, it is set to 0 before performing the operation. + /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. /// /// The key of the string. /// The amount to decrement by (defaults to 1). @@ -1876,7 +2011,8 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None); /// - /// Get the value of key. If the key does not exist the special value nil is returned. An error is returned if the value stored at key is not a string, because GET only handles string values. + /// Get the value of key. If the key does not exist the special value nil is returned. + /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. @@ -1885,7 +2021,8 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StringGetAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Returns the values of all specified keys. For every key that does not hold a string value or does not exist, the special value nil is returned. + /// Returns the values of all specified keys. + /// For every key that does not hold a string value or does not exist, the special value nil is returned. /// /// The keys of the strings. /// The flags to use for this operation. @@ -1894,7 +2031,8 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StringGetAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Get the value of key. If the key does not exist the special value nil is returned. An error is returned if the value stored at key is not a string, because GET only handles string values. + /// Get the value of key. If the key does not exist the special value nil is returned. + /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. @@ -1914,7 +2052,9 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StringGetBitAsync(RedisKey key, long offset, CommandFlags flags = CommandFlags.None); /// - /// Returns the substring of the string value stored at key, determined by the offsets start and end (both are inclusive). Negative offsets can be used in order to provide an offset starting from the end of the string. So -1 means the last character, -2 the penultimate and so forth. + /// Returns the substring of the string value stored at key, determined by the offsets start and end (both are inclusive). + /// Negative offsets can be used in order to provide an offset starting from the end of the string. + /// So -1 means the last character, -2 the penultimate and so forth. /// /// The key of the string. /// The start index of the substring to get. @@ -1935,7 +2075,9 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StringGetSetAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// - /// Get the value of key and delete the key. If the key does not exist the special value nil is returned. An error is returned if the value stored at key is not a string, because GET only handles string values. + /// Get the value of key and delete the key. + /// If the key does not exist the special value nil is returned. + /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. @@ -1944,7 +2086,9 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StringGetDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Get the value of key. If the key does not exist the special value nil is returned. An error is returned if the value stored at key is not a string, because GET only handles string values. + /// Get the value of key. + /// If the key does not exist the special value nil is returned. + /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. @@ -1953,7 +2097,10 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StringGetWithExpiryAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Increments the number stored at key by increment. If the key does not exist, it is set to 0 before performing the operation. An error is returned if the key contains a value of the wrong type or contains a string that is not representable as integer. This operation is limited to 64 bit signed integers. + /// Increments the number stored at key by increment. + /// If the key does not exist, it is set to 0 before performing the operation. + /// An error is returned if the key contains a value of the wrong type or contains a string that is not representable as integer. + /// This operation is limited to 64 bit signed integers. /// /// The key of the string. /// The amount to increment by (defaults to 1). @@ -1964,7 +2111,9 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StringIncrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); /// - /// Increments the string representing a floating point number stored at key by the specified increment. If the key does not exist, it is set to 0 before performing the operation. The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. + /// Increments the string representing a floating point number stored at key by the specified increment. + /// If the key does not exist, it is set to 0 before performing the operation. + /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. /// /// The key of the string. /// The amount to increment by (defaults to 1). @@ -1978,7 +2127,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// /// The key of the string. /// The flags to use for this operation. - /// the length of the string at key, or 0 when key does not exist. + /// The length of the string at key, or 0 when key does not exist. /// https://redis.io/commands/strlen Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -1990,24 +2139,26 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The expiry to set. /// Which condition to set the value under (defaults to always). /// The flags to use for this operation. - /// True if the string was set, false otherwise. + /// if the string was set, otherwise. /// https://redis.io/commands/set Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None); /// - /// Sets the given keys to their respective values. If "not exists" is specified, this will not perform any operation at all even if just a single key already exists. + /// Sets the given keys to their respective values. + /// If is specified, this will not perform any operation at all even if just a single key already exists. /// /// The keys and values to set. /// Which condition to set the value under (defaults to always). /// The flags to use for this operation. - /// True if the keys were set, else False + /// if the keys were set, otherwise. /// https://redis.io/commands/mset /// https://redis.io/commands/msetnx Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); /// /// Sets or clears the bit at offset in the string value stored at key. - /// The bit is either set or cleared depending on value, which can be either 0 or 1. When key does not exist, a new string value is created.The string is grown to make sure it can hold a bit at offset. + /// The bit is either set or cleared depending on value, which can be either 0 or 1. + /// When key does not exist, a new string value is created.The string is grown to make sure it can hold a bit at offset. /// /// The key of the string. /// The offset in the string to set . @@ -2018,7 +2169,9 @@ Task SortedSetRangeByValueAsync(RedisKey key, Task StringSetBitAsync(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None); /// - /// Overwrites part of the string stored at key, starting at the specified offset, for the entire length of value. If the offset is larger than the current length of the string at key, the string is padded with zero-bytes to make offset fit. Non-existing keys are considered as empty strings, so this command will make sure it holds a string large enough to be able to set value at offset. + /// Overwrites part of the string stored at key, starting at the specified offset, for the entire length of value. + /// If the offset is larger than the current length of the string at key, the string is padded with zero-bytes to make offset fit. + /// Non-existing keys are considered as empty strings, so this command will make sure it holds a string large enough to be able to set value at offset. /// /// The key of the string. /// The offset in the string to overwrite. @@ -2028,17 +2181,17 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// https://redis.io/commands/setrange Task StringSetRangeAsync(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None); - /// - /// Touch the specified key. + /// + /// Alters the last access time of a key. /// /// The key to touch. /// The flags to use for this operation. - /// True if the key was touched. + /// if the key was touched, otherwise. /// https://redis.io/commands/touch Task KeyTouchAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// - /// Youch the specified keys. A key is ignored if it does not exist. + /// Alters the last access time of the specified . A key is ignored if it does not exist. /// /// The keys to touch. /// The flags to use for this operation. diff --git a/src/StackExchange.Redis/Interfaces/IReconnectRetryPolicy.cs b/src/StackExchange.Redis/Interfaces/IReconnectRetryPolicy.cs index d2930dc2f..7bb29843a 100644 --- a/src/StackExchange.Redis/Interfaces/IReconnectRetryPolicy.cs +++ b/src/StackExchange.Redis/Interfaces/IReconnectRetryPolicy.cs @@ -1,15 +1,15 @@ -namespace StackExchange.Redis +namespace StackExchange.Redis { /// - /// Describes retry policy functionality that can be provided to the multiplexer to be used for connection reconnects + /// Describes retry policy functionality that can be provided to the multiplexer to be used for connection reconnects. /// public interface IReconnectRetryPolicy { /// /// This method is called by the multiplexer to determine if a reconnect operation can be retried now. /// - /// The number of times reconnect retries have already been made by the multiplexer while it was in connecting state - /// Total time elapsed in milliseconds since the last reconnect retry was made + /// The number of times reconnect retries have already been made by the multiplexer while it was in connecting state. + /// Total time elapsed in milliseconds since the last reconnect retry was made. bool ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSinceLastRetry); } } diff --git a/src/StackExchange.Redis/Interfaces/IRedis.cs b/src/StackExchange.Redis/Interfaces/IRedis.cs index ac18b7898..c945e01ec 100644 --- a/src/StackExchange.Redis/Interfaces/IRedis.cs +++ b/src/StackExchange.Redis/Interfaces/IRedis.cs @@ -3,7 +3,7 @@ namespace StackExchange.Redis { /// - /// Common operations available to all redis connections + /// Common operations available to all redis connections. /// public partial interface IRedis : IRedisAsync { diff --git a/src/StackExchange.Redis/Interfaces/IRedisAsync.cs b/src/StackExchange.Redis/Interfaces/IRedisAsync.cs index 91e2dd7dd..9f5b7a701 100644 --- a/src/StackExchange.Redis/Interfaces/IRedisAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IRedisAsync.cs @@ -4,12 +4,12 @@ namespace StackExchange.Redis { /// - /// Common operations available to all redis connections + /// Common operations available to all redis connections. /// public partial interface IRedisAsync { /// - /// Gets the multiplexer that created this instance + /// Gets the multiplexer that created this instance. /// IConnectionMultiplexer Multiplexer { get; } @@ -22,26 +22,26 @@ public partial interface IRedisAsync Task PingAsync(CommandFlags flags = CommandFlags.None); /// - /// Wait for a given asynchronous operation to complete (or timeout), reporting which + /// Wait for a given asynchronous operation to complete (or timeout), reporting which. /// /// The task to wait on. bool TryWait(Task task); /// - /// Wait for a given asynchronous operation to complete (or timeout) + /// Wait for a given asynchronous operation to complete (or timeout). /// /// The task to wait on. void Wait(Task task); /// - /// Wait for a given asynchronous operation to complete (or timeout) + /// Wait for a given asynchronous operation to complete (or timeout). /// /// The type of task to wait on. /// The task to wait on. T Wait(Task task); /// - /// Wait for the given asynchronous operations to complete (or timeout) + /// Wait for the given asynchronous operations to complete (or timeout). /// /// The tasks to wait on. void WaitAll(params Task[] tasks); diff --git a/src/StackExchange.Redis/Interfaces/IScanningCursor.cs b/src/StackExchange.Redis/Interfaces/IScanningCursor.cs index 4cbe6d92e..a9c8c45cf 100644 --- a/src/StackExchange.Redis/Interfaces/IScanningCursor.cs +++ b/src/StackExchange.Redis/Interfaces/IScanningCursor.cs @@ -1,23 +1,23 @@ namespace StackExchange.Redis { /// - /// Represents a resumable, cursor-based scanning operation + /// Represents a resumable, cursor-based scanning operation. /// public interface IScanningCursor { /// - /// Returns the cursor that represents the *active* page of results (not the pending/next page of results as returned by SCAN/HSCAN/ZSCAN/SSCAN) + /// Returns the cursor that represents the *active* page of results (not the pending/next page of results as returned by SCAN/HSCAN/ZSCAN/SSCAN). /// long Cursor { get; } /// - /// The page size of the current operation + /// The page size of the current operation. /// int PageSize { get; } /// - /// The offset into the current page + /// The offset into the current page. /// int PageOffset { get; } } -} \ No newline at end of file +} diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 724a13523..44d89d118 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -9,73 +9,75 @@ namespace StackExchange.Redis { /// - /// Provides configuration controls of a redis server + /// Provides configuration controls of a redis server. /// public partial interface IServer : IRedis { /// - /// Gets the cluster configuration associated with this server, if known + /// Gets the cluster configuration associated with this server, if known. /// ClusterConfiguration ClusterConfiguration { get; } /// - /// Gets the address of the connected server + /// Gets the address of the connected server. /// EndPoint EndPoint { get; } /// - /// Gets the features available to the connected server + /// Gets the features available to the connected server. /// RedisFeatures Features { get; } /// - /// Gets whether the connection to the server is active and usable + /// Gets whether the connection to the server is active and usable. /// bool IsConnected { get; } /// - /// Gets whether the connected server is a replica + /// Gets whether the connected server is a replica. /// - [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(IsReplica) + " instead.")] + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(IsReplica) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] bool IsSlave { get; } /// - /// Gets whether the connected server is a replica + /// Gets whether the connected server is a replica. /// bool IsReplica { get; } /// - /// Explicitly opt in for replica writes on writable replica + /// Explicitly opt in for replica writes on writable replica. /// - [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(AllowReplicaWrites) + " instead.")] + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(AllowReplicaWrites) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] bool AllowSlaveWrites { get; set; } /// - /// Explicitly opt in for replica writes on writable replica + /// Explicitly opt in for replica writes on writable replica. /// bool AllowReplicaWrites { get; set; } /// - /// Gets the operating mode of the connected server + /// Gets the operating mode of the connected server. /// ServerType ServerType { get; } /// - /// Gets the version of the connected server + /// Gets the version of the connected server. /// Version Version { get; } /// - /// The number of databases supported on this server + /// The number of databases supported on this server. /// int DatabaseCount { get; } /// /// The CLIENT KILL command closes a given client connection identified by ip:port. /// The ip:port should match a line returned by the CLIENT LIST command. - /// Due to the single-threaded nature of Redis, it is not possible to kill a client connection while it is executing a command.From the client point of view, the connection can never be closed in the middle of the execution of a command.However, the client will notice the connection has been closed only when the next command is sent (and results in network error). + /// Due to the single-threaded nature of Redis, it is not possible to kill a client connection while it is executing a command. + /// From the client point of view, the connection can never be closed in the middle of the execution of a command. + /// However, the client will notice the connection has been closed only when the next command is sent (and results in network error). /// /// The endpoint of the client to kill. /// The command flags to use. @@ -85,7 +87,9 @@ public partial interface IServer : IRedis /// /// The CLIENT KILL command closes a given client connection identified by ip:port. /// The ip:port should match a line returned by the CLIENT LIST command. - /// Due to the single-threaded nature of Redis, it is not possible to kill a client connection while it is executing a command.From the client point of view, the connection can never be closed in the middle of the execution of a command.However, the client will notice the connection has been closed only when the next command is sent (and results in network error). + /// Due to the single-threaded nature of Redis, it is not possible to kill a client connection while it is executing a command. + /// From the client point of view, the connection can never be closed in the middle of the execution of a command. + /// However, the client will notice the connection has been closed only when the next command is sent (and results in network error). /// /// The endpoint of the client to kill. /// The command flags to use. @@ -93,24 +97,24 @@ public partial interface IServer : IRedis Task ClientKillAsync(EndPoint endpoint, CommandFlags flags = CommandFlags.None); /// - /// The CLIENT KILL command closes multiple connections that match the specified filters + /// The CLIENT KILL command closes multiple connections that match the specified filters. /// /// The ID of the client to kill. /// The type of client. /// The endpoint to kill. - /// Whether to kskip the current connection. + /// Whether to skip the current connection. /// The command flags to use. /// the number of clients killed. /// https://redis.io/commands/client-kill long ClientKill(long? id = null, ClientType? clientType = null, EndPoint endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None); /// - /// The CLIENT KILL command closes multiple connections that match the specified filters + /// The CLIENT KILL command closes multiple connections that match the specified filters. /// /// The ID of the client to kill. /// The type of client. /// The endpoint to kill. - /// Whether to kskip the current connection. + /// Whether to skip the current connection. /// The command flags to use. /// the number of clients killed. /// https://redis.io/commands/client-kill @@ -131,25 +135,25 @@ public partial interface IServer : IRedis Task ClientListAsync(CommandFlags flags = CommandFlags.None); /// - /// Obtains the current CLUSTER NODES output from a cluster server + /// Obtains the current CLUSTER NODES output from a cluster server. /// /// The command flags to use. ClusterConfiguration ClusterNodes(CommandFlags flags = CommandFlags.None); /// - /// Obtains the current CLUSTER NODES output from a cluster server + /// Obtains the current CLUSTER NODES output from a cluster server. /// /// The command flags to use. Task ClusterNodesAsync(CommandFlags flags = CommandFlags.None); /// - /// Obtains the current raw CLUSTER NODES output from a cluster server + /// Obtains the current raw CLUSTER NODES output from a cluster server. /// /// The command flags to use. string ClusterNodesRaw(CommandFlags flags = CommandFlags.None); /// - /// Obtains the current raw CLUSTER NODES output from a cluster server + /// Obtains the current raw CLUSTER NODES output from a cluster server. /// /// The command flags to use. Task ClusterNodesRawAsync(CommandFlags flags = CommandFlags.None); @@ -187,21 +191,26 @@ public partial interface IServer : IRedis Task ConfigResetStatisticsAsync(CommandFlags flags = CommandFlags.None); /// - /// The CONFIG REWRITE command rewrites the redis.conf file the server was started with, applying the minimal changes needed to make it reflecting the configuration currently used by the server, that may be different compared to the original one because of the use of the CONFIG SET command. + /// The CONFIG REWRITE command rewrites the redis.conf file the server was started with, + /// applying the minimal changes needed to make it reflecting the configuration currently + /// used by the server, that may be different compared to the original one because of the use of the CONFIG SET command. /// /// The command flags to use. /// https://redis.io/commands/config-rewrite void ConfigRewrite(CommandFlags flags = CommandFlags.None); /// - /// The CONFIG REWRITE command rewrites the redis.conf file the server was started with, applying the minimal changes needed to make it reflecting the configuration currently used by the server, that may be different compared to the original one because of the use of the CONFIG SET command. + /// The CONFIG REWRITE command rewrites the redis.conf file the server was started with, + /// applying the minimal changes needed to make it reflecting the configuration currently + /// used by the server, that may be different compared to the original one because of the use of the CONFIG SET command. /// /// The command flags to use. /// https://redis.io/commands/config-rewrite Task ConfigRewriteAsync(CommandFlags flags = CommandFlags.None); /// - /// The CONFIG SET command is used in order to reconfigure the server at runtime without the need to restart Redis. You can change both trivial parameters or switch from one to another persistence option using this command. + /// The CONFIG SET command is used in order to reconfigure the server at runtime without the need to restart Redis. + /// You can change both trivial parameters or switch from one to another persistence option using this command. /// /// The setting name. /// The new setting value. @@ -210,7 +219,8 @@ public partial interface IServer : IRedis void ConfigSet(RedisValue setting, RedisValue value, CommandFlags flags = CommandFlags.None); /// - /// The CONFIG SET command is used in order to reconfigure the server at runtime without the need to restart Redis. You can change both trivial parameters or switch from one to another persistence option using this command. + /// The CONFIG SET command is used in order to reconfigure the server at runtime without the need to restart Redis. + /// You can change both trivial parameters or switch from one to another persistence option using this command. /// /// The setting name. /// The new setting value. @@ -327,7 +337,7 @@ public partial interface IServer : IRedis Task FlushDatabaseAsync(int database = -1, CommandFlags flags = CommandFlags.None); /// - /// Get summary statistics associates with this server + /// Get summary statistics associates with this server. /// ServerCounters GetCounters(); @@ -376,7 +386,9 @@ public partial interface IServer : IRedis IEnumerable Keys(int database, RedisValue pattern, int pageSize, CommandFlags flags); /// - /// Returns all keys matching pattern; the KEYS or SCAN commands will be used based on the server capabilities; note: to resume an iteration via cursor, cast the original enumerable or enumerator to IScanningCursor. + /// Returns all keys matching pattern. + /// The KEYS or SCAN commands will be used based on the server capabilities. + /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to IScanningCursor. /// /// The database ID. /// The pattern to use. @@ -390,7 +402,9 @@ public partial interface IServer : IRedis IEnumerable Keys(int database = -1, RedisValue pattern = default(RedisValue), int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// - /// Returns all keys matching pattern; the KEYS or SCAN commands will be used based on the server capabilities; note: to resume an iteration via cursor, cast the original enumerable or enumerator to IScanningCursor. + /// Returns all keys matching pattern. + /// The KEYS or SCAN commands will be used based on the server capabilities. + /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to IScanningCursor. /// /// The database ID. /// The pattern to use. @@ -404,14 +418,18 @@ public partial interface IServer : IRedis IAsyncEnumerable KeysAsync(int database = -1, RedisValue pattern = default(RedisValue), int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// - /// Return the time of the last DB save executed with success. A client may check if a BGSAVE command succeeded reading the LASTSAVE value, then issuing a BGSAVE command and checking at regular intervals every N seconds if LASTSAVE changed. + /// Return the time of the last DB save executed with success. + /// A client may check if a BGSAVE command succeeded reading the LASTSAVE value, then issuing a BGSAVE command + /// and checking at regular intervals every N seconds if LASTSAVE changed. /// /// The command flags to use. /// https://redis.io/commands/lastsave DateTime LastSave(CommandFlags flags = CommandFlags.None); /// - /// Return the time of the last DB save executed with success. A client may check if a BGSAVE command succeeded reading the LASTSAVE value, then issuing a BGSAVE command and checking at regular intervals every N seconds if LASTSAVE changed. + /// Return the time of the last DB save executed with success. + /// A client may check if a BGSAVE command succeeded reading the LASTSAVE value, then issuing a BGSAVE command + /// and checking at regular intervals every N seconds if LASTSAVE changed. /// /// The command flags to use. /// https://redis.io/commands/lastsave @@ -445,7 +463,7 @@ public partial interface IServer : IRedis Task RoleAsync(CommandFlags flags = CommandFlags.None); /// - /// Explicitly request the database to persist the current state to disk + /// Explicitly request the database to persist the current state to disk. /// /// The method of the save (e.g. background or foreground). /// The command flags to use. @@ -456,7 +474,7 @@ public partial interface IServer : IRedis void Save(SaveType type, CommandFlags flags = CommandFlags.None); /// - /// Explicitly request the database to persist the current state to disk + /// Explicitly request the database to persist the current state to disk. /// /// The method of the save (e.g. background or foreground). /// The command flags to use. @@ -467,68 +485,68 @@ public partial interface IServer : IRedis Task SaveAsync(SaveType type, CommandFlags flags = CommandFlags.None); /// - /// Indicates whether the specified script is defined on the server + /// Indicates whether the specified script is defined on the server. /// /// The text of the script to check for on the server. /// The command flags to use. bool ScriptExists(string script, CommandFlags flags = CommandFlags.None); /// - /// Indicates whether the specified script hash is defined on the server + /// Indicates whether the specified script hash is defined on the server. /// /// The SHA1 of the script to check for on the server. /// The command flags to use. bool ScriptExists(byte[] sha1, CommandFlags flags = CommandFlags.None); /// - /// Indicates whether the specified script is defined on the server + /// Indicates whether the specified script is defined on the server. /// /// The text of the script to check for on the server. /// The command flags to use. Task ScriptExistsAsync(string script, CommandFlags flags = CommandFlags.None); /// - /// Indicates whether the specified script hash is defined on the server + /// Indicates whether the specified script hash is defined on the server. /// /// The SHA1 of the script to check for on the server. /// The command flags to use. Task ScriptExistsAsync(byte[] sha1, CommandFlags flags = CommandFlags.None); /// - /// Removes all cached scripts on this server + /// Removes all cached scripts on this server. /// /// The command flags to use. void ScriptFlush(CommandFlags flags = CommandFlags.None); /// - /// Removes all cached scripts on this server + /// Removes all cached scripts on this server. /// /// The command flags to use. Task ScriptFlushAsync(CommandFlags flags = CommandFlags.None); /// - /// Explicitly defines a script on the server + /// Explicitly defines a script on the server. /// /// The script to load. /// The command flags to use. byte[] ScriptLoad(string script, CommandFlags flags = CommandFlags.None); /// - /// Explicitly defines a script on the server + /// Explicitly defines a script on the server. /// /// The script to load. /// The command flags to use. LoadedLuaScript ScriptLoad(LuaScript script, CommandFlags flags = CommandFlags.None); /// - /// Explicitly defines a script on the server + /// Explicitly defines a script on the server. /// /// The script to load. /// The command flags to use. Task ScriptLoadAsync(string script, CommandFlags flags = CommandFlags.None); /// - /// Explicitly defines a script on the server + /// Explicitly defines a script on the server. /// /// The script to load. /// The command flags to use. @@ -543,7 +561,10 @@ public partial interface IServer : IRedis void Shutdown(ShutdownMode shutdownMode = ShutdownMode.Default, CommandFlags flags = CommandFlags.None); /// - /// The REPLICAOF command can change the replication settings of a replica on the fly. If a Redis server is already acting as replica, specifying a null master will turn off the replication, turning the Redis server into a MASTER. Specifying a non-null master will make the server a replica of another server listening at the specified hostname and port. + /// The REPLICAOF command can change the replication settings of a replica on the fly. + /// If a Redis server is already acting as replica, specifying a null master will turn off the replication, + /// turning the Redis server into a MASTER. Specifying a non-null master will make the server a replica of + /// another server listening at the specified hostname and port. /// /// Endpoint of the new master to replicate from. /// The command flags to use. @@ -553,7 +574,10 @@ public partial interface IServer : IRedis void SlaveOf(EndPoint master, CommandFlags flags = CommandFlags.None); /// - /// The REPLICAOF command can change the replication settings of a replica on the fly. If a Redis server is already acting as replica, specifying a null master will turn off the replication, turning the Redis server into a MASTER. Specifying a non-null master will make the server a replica of another server listening at the specified hostname and port. + /// The REPLICAOF command can change the replication settings of a replica on the fly. + /// If a Redis server is already acting as replica, specifying a null master will turn off the replication, + /// turning the Redis server into a MASTER. Specifying a non-null master will make the server a replica of + /// another server listening at the specified hostname and port. /// /// Endpoint of the new master to replicate from. /// The command flags to use. @@ -562,7 +586,10 @@ public partial interface IServer : IRedis void ReplicaOf(EndPoint master, CommandFlags flags = CommandFlags.None); /// - /// The REPLICAOF command can change the replication settings of a replica on the fly. If a Redis server is already acting as replica, specifying a null master will turn off the replication, turning the Redis server into a MASTER. Specifying a non-null master will make the server a replica of another server listening at the specified hostname and port. + /// The REPLICAOF command can change the replication settings of a replica on the fly. + /// If a Redis server is already acting as replica, specifying a null master will turn off the replication, + /// turning the Redis server into a MASTER. Specifying a non-null master will make the server a replica of + /// another server listening at the specified hostname and port. /// /// Endpoint of the new master to replicate from. /// The command flags to use. @@ -572,7 +599,10 @@ public partial interface IServer : IRedis Task SlaveOfAsync(EndPoint master, CommandFlags flags = CommandFlags.None); /// - /// The REPLICAOF command can change the replication settings of a replica on the fly. If a Redis server is already acting as replica, specifying a null master will turn off the replication, turning the Redis server into a MASTER. Specifying a non-null master will make the server a replica of another server listening at the specified hostname and port. + /// The REPLICAOF command can change the replication settings of a replica on the fly. + /// If a Redis server is already acting as replica, specifying a null master will turn off the replication, + /// turning the Redis server into a MASTER. Specifying a non-null master will make the server a replica of + /// another server listening at the specified hostname and port. /// /// Endpoint of the new master to replicate from. /// The command flags to use. @@ -580,7 +610,8 @@ public partial interface IServer : IRedis Task ReplicaOfAsync(EndPoint master, CommandFlags flags = CommandFlags.None); /// - /// To read the slow log the SLOWLOG GET command is used, that returns every entry in the slow log. It is possible to return only the N most recent entries passing an additional argument to the command (for instance SLOWLOG GET 10). + /// To read the slow log the SLOWLOG GET command is used, that returns every entry in the slow log. + /// It is possible to return only the N most recent entries passing an additional argument to the command (for instance SLOWLOG GET 10). /// /// The count of items to get. /// The command flags to use. @@ -588,7 +619,8 @@ public partial interface IServer : IRedis CommandTrace[] SlowlogGet(int count = 0, CommandFlags flags = CommandFlags.None); /// - /// To read the slow log the SLOWLOG GET command is used, that returns every entry in the slow log. It is possible to return only the N most recent entries passing an additional argument to the command (for instance SLOWLOG GET 10). + /// To read the slow log the SLOWLOG GET command is used, that returns every entry in the slow log. + /// It is possible to return only the N most recent entries passing an additional argument to the command (for instance SLOWLOG GET 10). /// /// The count of items to get. /// The command flags to use. @@ -610,7 +642,8 @@ public partial interface IServer : IRedis Task SlowlogResetAsync(CommandFlags flags = CommandFlags.None); /// - /// Lists the currently active channels. An active channel is a Pub/Sub channel with one ore more subscribers (not including clients subscribed to patterns). + /// Lists the currently active channels. + /// An active channel is a Pub/Sub channel with one ore more subscribers (not including clients subscribed to patterns). /// /// The channel name pattern to get channels for. /// The command flags to use. @@ -619,7 +652,8 @@ public partial interface IServer : IRedis RedisChannel[] SubscriptionChannels(RedisChannel pattern = default(RedisChannel), CommandFlags flags = CommandFlags.None); /// - /// Lists the currently active channels. An active channel is a Pub/Sub channel with one ore more subscribers (not including clients subscribed to patterns). + /// Lists the currently active channels. + /// An active channel is a Pub/Sub channel with one ore more subscribers (not including clients subscribed to patterns). /// /// The channel name pattern to get channels for. /// The command flags to use. @@ -628,7 +662,8 @@ public partial interface IServer : IRedis Task SubscriptionChannelsAsync(RedisChannel pattern = default(RedisChannel), CommandFlags flags = CommandFlags.None); /// - /// Returns the number of subscriptions to patterns (that are performed using the PSUBSCRIBE command). Note that this is not just the count of clients subscribed to patterns but the total number of patterns all the clients are subscribed to. + /// Returns the number of subscriptions to patterns (that are performed using the PSUBSCRIBE command). + /// Note that this is not just the count of clients subscribed to patterns but the total number of patterns all the clients are subscribed to. /// /// The command flags to use. /// the number of patterns all the clients are subscribed to. @@ -636,7 +671,8 @@ public partial interface IServer : IRedis long SubscriptionPatternCount(CommandFlags flags = CommandFlags.None); /// - /// Returns the number of subscriptions to patterns (that are performed using the PSUBSCRIBE command). Note that this is not just the count of clients subscribed to patterns but the total number of patterns all the clients are subscribed to. + /// Returns the number of subscriptions to patterns (that are performed using the PSUBSCRIBE command). + /// Note that this is not just the count of clients subscribed to patterns but the total number of patterns all the clients are subscribed to. /// /// The command flags to use. /// the number of patterns all the clients are subscribed to. @@ -886,7 +922,7 @@ public partial interface IServer : IRedis /// The command flags to use. /// an array of replica state KeyValuePair arrays /// https://redis.io/topics/sentinel - [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(SentinelReplicas) + " instead.")] + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(SentinelReplicas) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] KeyValuePair[][] SentinelSlaves(string serviceName, CommandFlags flags = CommandFlags.None); @@ -906,7 +942,7 @@ public partial interface IServer : IRedis /// The command flags to use. /// an array of replica state KeyValuePair arrays /// https://redis.io/topics/sentinel - [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(SentinelReplicasAsync) + " instead.")] + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(SentinelReplicasAsync) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task[][]> SentinelSlavesAsync(string serviceName, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/Interfaces/ISubscriber.cs b/src/StackExchange.Redis/Interfaces/ISubscriber.cs index f26ea7a11..6755a603a 100644 --- a/src/StackExchange.Redis/Interfaces/ISubscriber.cs +++ b/src/StackExchange.Redis/Interfaces/ISubscriber.cs @@ -5,27 +5,27 @@ namespace StackExchange.Redis { /// - /// A redis connection used as the subscriber in a pub/sub scenario + /// A redis connection used as the subscriber in a pub/sub scenario. /// public interface ISubscriber : IRedis { /// - /// Indicate exactly which redis server we are talking to + /// Indicate exactly which redis server we are talking to. /// /// The channel to identify the server endpoint by. /// The command flags to use. EndPoint IdentifyEndpoint(RedisChannel channel, CommandFlags flags = CommandFlags.None); /// - /// Indicate exactly which redis server we are talking to + /// Indicate exactly which redis server we are talking to. /// /// The channel to identify the server endpoint by. /// The command flags to use. Task IdentifyEndpointAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None); /// - /// Indicates whether the instance can communicate with the server; - /// if a channel is specified, the existing subscription map is queried to + /// Indicates whether the instance can communicate with the server. + /// If a channel is specified, the existing subscription map is queried to /// resolve the server responsible for that subscription - otherwise the /// server is chosen arbitrarily from the masters. /// @@ -99,16 +99,16 @@ public interface ISubscriber : IRedis Task SubscribeAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None); /// - /// Indicate to which redis server we are actively subscribed for a given channel; returns null if - /// the channel is not actively subscribed + /// Indicate to which redis server we are actively subscribed for a given channel. /// /// The channel to check which server endpoint was subscribed on. + /// The subscribed endpoint for the given , if the channel is not actively subscribed. EndPoint SubscribedEndpoint(RedisChannel channel); /// - /// Unsubscribe from a specified message channel; note; if no handler is specified, the subscription is canceled regardless - /// of the subscribers; if a handler is specified, the subscription is only canceled if this handler is the - /// last handler remaining against the channel + /// Unsubscribe from a specified message channel. + /// Note: if no handler is specified, the subscription is canceled regardless of the subscribers. + /// If a handler is specified, the subscription is only canceled if this handler is the last handler remaining against the channel. /// /// The channel that was subscribed to. /// The handler to no longer invoke when a message is received on . @@ -118,7 +118,7 @@ public interface ISubscriber : IRedis void Unsubscribe(RedisChannel channel, Action handler = null, CommandFlags flags = CommandFlags.None); /// - /// Unsubscribe all subscriptions on this instance + /// Unsubscribe all subscriptions on this instance. /// /// The command flags to use. /// https://redis.io/commands/unsubscribe @@ -126,7 +126,7 @@ public interface ISubscriber : IRedis void UnsubscribeAll(CommandFlags flags = CommandFlags.None); /// - /// Unsubscribe all subscriptions on this instance + /// Unsubscribe all subscriptions on this instance. /// /// The command flags to use. /// https://redis.io/commands/unsubscribe @@ -134,9 +134,9 @@ public interface ISubscriber : IRedis Task UnsubscribeAllAsync(CommandFlags flags = CommandFlags.None); /// - /// Unsubscribe from a specified message channel; note; if no handler is specified, the subscription is canceled regardless - /// of the subscribers; if a handler is specified, the subscription is only canceled if this handler is the - /// last handler remaining against the channel + /// Unsubscribe from a specified message channel. + /// Note: if no handler is specified, the subscription is canceled regardless of the subscribers. + /// If a handler is specified, the subscription is only canceled if this handler is the last handler remaining against the channel. /// /// The channel that was subscribed to. /// The handler to no longer invoke when a message is received on . diff --git a/src/StackExchange.Redis/Interfaces/ITransaction.cs b/src/StackExchange.Redis/Interfaces/ITransaction.cs index 230405282..0b8a25c6e 100644 --- a/src/StackExchange.Redis/Interfaces/ITransaction.cs +++ b/src/StackExchange.Redis/Interfaces/ITransaction.cs @@ -11,12 +11,13 @@ namespace StackExchange.Redis /// the constraint checks have arrived. /// /// https://redis.io/topics/transactions - /// Note that on a cluster, it may be required that all keys involved in the transaction - /// (including constraints) are in the same hash-slot + /// + /// Note that on a cluster, it may be required that all keys involved in the transaction (including constraints) are in the same hash-slot. + /// public interface ITransaction : IBatch { /// - /// Adds a precondition for this transaction + /// Adds a precondition for this transaction. /// /// The condition to add to the transaction. ConditionResult AddCondition(Condition condition); diff --git a/src/StackExchange.Redis/InternalErrorEventArgs.cs b/src/StackExchange.Redis/InternalErrorEventArgs.cs index 0d0767c2d..7403b0398 100644 --- a/src/StackExchange.Redis/InternalErrorEventArgs.cs +++ b/src/StackExchange.Redis/InternalErrorEventArgs.cs @@ -27,7 +27,7 @@ internal InternalErrorEventArgs(EventHandler handler, ob /// The source of the event. /// /// Redis connection type. - /// The exception occured. + /// The exception that occurred. /// Origin. public InternalErrorEventArgs(object sender, EndPoint endpoint, ConnectionType connectionType, Exception exception, string origin) : this (null, sender, endpoint, connectionType, exception, origin) @@ -57,7 +57,10 @@ public InternalErrorEventArgs(object sender, EndPoint endpoint, ConnectionType c void ICompletable.AppendStormLog(StringBuilder sb) { sb.Append("event, internal-error: ").Append(Origin); - if (EndPoint != null) sb.Append(", ").Append(Format.ToString(EndPoint)); + if (EndPoint != null) + { + sb.Append(", ").Append(Format.ToString(EndPoint)); + } } bool ICompletable.TryComplete(bool isAsync) => ConnectionMultiplexer.TryCompleteHandler(handler, sender, this, isAsync); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/BatchWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/BatchWrapper.cs index b551f1807..a8fb90162 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/BatchWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/BatchWrapper.cs @@ -2,13 +2,8 @@ { internal sealed class BatchWrapper : WrapperBase, IBatch { - public BatchWrapper(IBatch inner, byte[] prefix) : base(inner, prefix) - { - } + public BatchWrapper(IBatch inner, byte[] prefix) : base(inner, prefix) { } - public void Execute() - { - Inner.Execute(); - } + public void Execute() => Inner.Execute(); } } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs index 077f8daad..caa68f34e 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs @@ -9,7 +9,7 @@ public static class DatabaseExtensions { /// /// Creates a new instance that provides an isolated key space - /// of the specified underyling database instance. + /// of the specified underlying database instance. /// /// /// The underlying database instance that the returned instance shall use. @@ -20,7 +20,7 @@ public static class DatabaseExtensions /// /// A new instance that invokes the specified underlying /// but prepends the specified - /// to all key paramters and thus forms a logical key space isolation. + /// to all key parameters and thus forms a logical key space isolation. /// /// /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/TransactionWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/TransactionWrapper.cs index 2e941fde9..c3e8665bf 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/TransactionWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/TransactionWrapper.cs @@ -4,28 +4,14 @@ namespace StackExchange.Redis.KeyspaceIsolation { internal sealed class TransactionWrapper : WrapperBase, ITransaction { - public TransactionWrapper(ITransaction inner, byte[] prefix) : base(inner, prefix) - { - } + public TransactionWrapper(ITransaction inner, byte[] prefix) : base(inner, prefix) { } - public ConditionResult AddCondition(Condition condition) - { - return Inner.AddCondition(condition?.MapKeys(GetMapFunction())); - } + public ConditionResult AddCondition(Condition condition) => Inner.AddCondition(condition?.MapKeys(GetMapFunction())); - public bool Execute(CommandFlags flags = CommandFlags.None) - { - return Inner.Execute(flags); - } + public bool Execute(CommandFlags flags = CommandFlags.None) => Inner.Execute(flags); - public Task ExecuteAsync(CommandFlags flags = CommandFlags.None) - { - return Inner.ExecuteAsync(flags); - } + public Task ExecuteAsync(CommandFlags flags = CommandFlags.None) => Inner.ExecuteAsync(flags); - public void Execute() - { - Inner.Execute(); - } + public void Execute() => Inner.Execute(); } } diff --git a/src/StackExchange.Redis/Lease.cs b/src/StackExchange.Redis/Lease.cs index dc0869019..1dbbcea1b 100644 --- a/src/StackExchange.Redis/Lease.cs +++ b/src/StackExchange.Redis/Lease.cs @@ -12,7 +12,7 @@ namespace StackExchange.Redis public sealed class Lease : IMemoryOwner { /// - /// A lease of length zero + /// A lease of length zero. /// public static Lease Empty { get; } = new Lease(System.Array.Empty(), 0); diff --git a/src/StackExchange.Redis/LinearRetry.cs b/src/StackExchange.Redis/LinearRetry.cs index 809eaff15..ddd269eb3 100644 --- a/src/StackExchange.Redis/LinearRetry.cs +++ b/src/StackExchange.Redis/LinearRetry.cs @@ -11,10 +11,8 @@ public class LinearRetry : IReconnectRetryPolicy /// Initializes a new instance using the specified maximum retry elapsed time allowed. /// /// maximum elapsed time in milliseconds to be allowed for it to perform retries. - public LinearRetry(int maxRetryElapsedTimeAllowedMilliseconds) - { + public LinearRetry(int maxRetryElapsedTimeAllowedMilliseconds) => this.maxRetryElapsedTimeAllowedMilliseconds = maxRetryElapsedTimeAllowedMilliseconds; - } /// /// This method is called by the ConnectionMultiplexer to determine if a reconnect operation can be retried now. diff --git a/src/StackExchange.Redis/LuaScript.cs b/src/StackExchange.Redis/LuaScript.cs index 8e332461e..9d85b3324 100644 --- a/src/StackExchange.Redis/LuaScript.cs +++ b/src/StackExchange.Redis/LuaScript.cs @@ -19,8 +19,10 @@ namespace StackExchange.Redis /// public sealed class LuaScript { - // Since the mapping of "script text" -> LuaScript doesn't depend on any particular details of - // the redis connection itself, this cache is global. + /// + /// Since the mapping of "script text" -> LuaScript doesn't depend on any particular details of + /// the redis connection itself, this cache is global. + /// private static readonly ConcurrentDictionary Cache = new(); /// @@ -56,8 +58,7 @@ internal LuaScript(string originalScript, string executableScript, string[] argu } /// - /// Finalizer, used to prompt cleanups of the script cache when - /// a LuaScript reference goes out of scope. + /// Finalizer - used to prompt cleanups of the script cache when a LuaScript reference goes out of scope. /// ~LuaScript() { @@ -255,8 +256,8 @@ internal LoadedLuaScript(LuaScript original, byte[] hash) /// /// Evaluates this LoadedLuaScript against the given database, extracting parameters for the passed in object if any. /// - /// This method sends the SHA1 hash of the ExecutableScript instead of the script itself. If the script has not - /// been loaded into the passed Redis instance it will fail. + /// This method sends the SHA1 hash of the ExecutableScript instead of the script itself. + /// If the script has not been loaded into the passed Redis instance, it will fail. /// /// /// The redis database to evaluate against. @@ -272,8 +273,8 @@ public RedisResult Evaluate(IDatabase db, object ps = null, RedisKey? withKeyPre /// /// Evaluates this LoadedLuaScript against the given database, extracting parameters for the passed in object if any. /// - /// This method sends the SHA1 hash of the ExecutableScript instead of the script itself. If the script has not - /// been loaded into the passed Redis instance it will fail. + /// This method sends the SHA1 hash of the ExecutableScript instead of the script itself. + /// If the script has not been loaded into the passed Redis instance, it will fail. /// /// /// The redis database to evaluate against. diff --git a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs index bf8b620c5..ad6279921 100644 --- a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs @@ -2,7 +2,7 @@ using System.Globalization; using System.Net; using System.Threading.Tasks; -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +#if NETCOREAPP using System.Buffers.Text; #endif @@ -58,7 +58,7 @@ internal AzureMaintenanceEvent(string azureEvent) if (key.Length > 0 && value.Length > 0) { -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +#if NETCOREAPP switch (key) { case var _ when key.SequenceEqual(nameof(NotificationType).AsSpan()): diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 84bbcd6d0..f99126f08 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -70,7 +70,7 @@ internal abstract class Message : ICompletable | CommandFlags.DemandReplica | CommandFlags.PreferMaster | CommandFlags.PreferReplica -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Type or member is obsolete | CommandFlags.HighPriority #pragma warning restore CS0618 | CommandFlags.FireAndForget @@ -376,6 +376,14 @@ public static bool IsMasterOnly(RedisCommand command) } } + /// Gets whether this is primary-only. + /// + /// Note that the constructor runs the switch statement above, so + /// this will already be true for primary-only commands, even if the + /// user specified etc. + /// + public bool IsMasterOnly() => GetMasterReplicaFlags(Flags) == CommandFlags.DemandMaster; + public virtual void AppendStormLog(StringBuilder sb) { if (Db >= 0) sb.Append(Db).Append(':'); @@ -384,13 +392,6 @@ public virtual void AppendStormLog(StringBuilder sb) public virtual int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => ServerSelectionStrategy.NoSlot; - /// - /// Note that the constructor runs the switch statement above, so - /// this will already be true for master-only commands, even if the - /// user specified PreferMaster etc - /// - public bool IsMasterOnly() => GetMasterReplicaFlags(Flags) == CommandFlags.DemandMaster; - /// /// This does a few important things: /// 1: it suppresses error events for commands that the user isn't interested in @@ -664,11 +665,7 @@ internal void SetWriteTime() internal bool HasTimedOut(int now, int timeoutMilliseconds, out int millisecondsTaken) { millisecondsTaken = unchecked(now - _writeTickCount); // note: we can't just check "if sent < cutoff" because of wrap-around - if (millisecondsTaken >= timeoutMilliseconds) - { - return true; - } - return false; + return millisecondsTaken >= timeoutMilliseconds; } internal void SetAsking(bool value) @@ -685,8 +682,11 @@ internal void SetPreferMaster() => internal void SetPreferReplica() => Flags = (Flags & ~MaskMasterServerPreference) | CommandFlags.PreferReplica; + /// + /// Sets the processor and box for this message to execute. + /// /// - /// Note order here reversed to prevent overload resolution errors + /// Note order here is reversed to prevent overload resolution errors. /// internal void SetSource(ResultProcessor resultProcessor, IResultBox resultBox) { @@ -694,6 +694,12 @@ internal void SetSource(ResultProcessor resultProcessor, IResultBox resultBox) this.resultProcessor = resultProcessor; } + /// + /// Sets the box and processor for this message to execute. + /// + /// + /// Note order here is reversed to prevent overload resolution errors. + /// internal void SetSource(IResultBox resultBox, ResultProcessor resultProcessor) { this.resultBox = resultBox; diff --git a/src/StackExchange.Redis/MessageCompletable.cs b/src/StackExchange.Redis/MessageCompletable.cs index 481477a0a..7648bc42e 100644 --- a/src/StackExchange.Redis/MessageCompletable.cs +++ b/src/StackExchange.Redis/MessageCompletable.cs @@ -48,9 +48,6 @@ public bool TryComplete(bool isAsync) } } - void ICompletable.AppendStormLog(StringBuilder sb) - { - sb.Append("event, pub/sub: ").Append((string)channel); - } + void ICompletable.AppendStormLog(StringBuilder sb) => sb.Append("event, pub/sub: ").Append((string)channel); } } diff --git a/src/StackExchange.Redis/NameValueEntry.cs b/src/StackExchange.Redis/NameValueEntry.cs index 20332e7bf..6756cfbb3 100644 --- a/src/StackExchange.Redis/NameValueEntry.cs +++ b/src/StackExchange.Redis/NameValueEntry.cs @@ -32,27 +32,25 @@ public NameValueEntry(RedisValue name, RedisValue value) public RedisValue Value => value; /// - /// Converts to a key/value pair + /// Converts to a key/value pair. /// /// The to create a from. public static implicit operator KeyValuePair(NameValueEntry value) => new KeyValuePair(value.name, value.value); /// - /// Converts from a key/value pair + /// Converts from a key/value pair. /// /// The to get a from. public static implicit operator NameValueEntry(KeyValuePair value) => new NameValueEntry(value.Key, value.Value); /// - /// See Object.ToString() + /// The "{name}: {value}" string representation. /// public override string ToString() => name + ": " + value; - /// - /// See Object.GetHashCode() - /// + /// public override int GetHashCode() => name.GetHashCode() ^ value.GetHashCode(); /// diff --git a/src/StackExchange.Redis/PerfCounterHelper.cs b/src/StackExchange.Redis/PerfCounterHelper.cs index 876e66bc5..223e719c2 100644 --- a/src/StackExchange.Redis/PerfCounterHelper.cs +++ b/src/StackExchange.Redis/PerfCounterHelper.cs @@ -1,8 +1,10 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; -using System.Runtime.Versioning; using System.Threading; +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif namespace StackExchange.Redis { diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 025702fa7..f820569ff 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -366,7 +366,7 @@ internal void KeepAlive() Multiplexer.Trace("Enqueue: " + msg); Multiplexer.OnInfoMessage($"heartbeat ({physical?.LastWriteSecondsAgo}s >= {ServerEndPoint?.WriteEverySeconds}s, {physical?.GetSentAwaitingResponseCount()} waiting) '{msg.CommandAndKey}' on '{PhysicalName}' (v{features.Version})"); physical?.UpdateLastWriteTime(); // preemptively -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Type or member is obsolete var result = TryWriteSync(msg, ServerEndPoint.IsReplica); #pragma warning restore CS0618 @@ -634,7 +634,7 @@ internal bool TryEnqueue(List messages, bool isReplica) // deliberately not taking a single lock here; we don't care if // other threads manage to interleave - in fact, it would be desirable // (to avoid a batch monopolising the connection) -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Type or member is obsolete WriteMessageTakingWriteLockSync(physical, message); #pragma warning restore CS0618 LogNonPreferred(message.Flags, isReplica); @@ -794,7 +794,7 @@ private bool TryPushToBacklog(Message message, bool onlyIfExists, bool bypassBac [MethodImpl(MethodImplOptions.AggressiveInlining)] private void BacklogEnqueue(Message message, PhysicalConnection physical) - { + { _backlog.Enqueue(message); Interlocked.Increment(ref _backlogTotalEnqueued); } diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 5e7e0da93..3a1bb5b3b 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -975,10 +975,8 @@ private static void WriteUnifiedBlob(PipeWriter writer, byte[] value) WriteUnifiedSpan(writer, new ReadOnlySpan(value)); } } - -#pragma warning disable RCS1231 // Make parameter ref read-only. + private static void WriteUnifiedSpan(PipeWriter writer, ReadOnlySpan value) -#pragma warning restore RCS1231 // Make parameter ref read-only. { // ${len}\r\n = 3 + MaxInt32TextLen // {value}\r\n = 2 + value.Length @@ -1020,9 +1018,7 @@ private static int AppendToSpanCommand(Span span, in CommandBytes value, i return WriteCrlf(span, offset); } -#pragma warning disable RCS1231 // Make parameter ref read-only. - spans are tiny private static int AppendToSpan(Span span, ReadOnlySpan value, int offset = 0) -#pragma warning restore RCS1231 // Make parameter ref read-only. { offset = WriteRaw(span, value.Length, offset: offset); value.CopyTo(span.Slice(offset, value.Length)); diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 58f40361d..6226b102f 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -1,7 +1,6 @@ using System; using System.Buffers; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Text; using Pipelines.Sockets.Unofficial.Arenas; diff --git a/src/StackExchange.Redis/RedisBase.cs b/src/StackExchange.Redis/RedisBase.cs index 9d42d05a3..492e7f120 100644 --- a/src/StackExchange.Redis/RedisBase.cs +++ b/src/StackExchange.Redis/RedisBase.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; namespace StackExchange.Redis diff --git a/src/StackExchange.Redis/RedisBatch.cs b/src/StackExchange.Redis/RedisBatch.cs index 7abe234c5..810eca945 100644 --- a/src/StackExchange.Redis/RedisBatch.cs +++ b/src/StackExchange.Redis/RedisBatch.cs @@ -86,10 +86,8 @@ internal override Task ExecuteAsync(Message message, ResultProcessor pr return task; } - internal override T ExecuteSync(Message message, ResultProcessor processor, ServerEndPoint server = null) - { + internal override T ExecuteSync(Message message, ResultProcessor processor, ServerEndPoint server = null) => throw new NotSupportedException("ExecuteSync cannot be used inside a batch"); - } private static void FailNoServer(List messages) { diff --git a/src/StackExchange.Redis/RedisChannel.cs b/src/StackExchange.Redis/RedisChannel.cs index 14fa956cf..ced2e0d04 100644 --- a/src/StackExchange.Redis/RedisChannel.cs +++ b/src/StackExchange.Redis/RedisChannel.cs @@ -4,7 +4,7 @@ namespace StackExchange.Redis { /// - /// Represents a pub/sub channel name + /// Represents a pub/sub channel name. /// public readonly struct RedisChannel : IEquatable { @@ -12,21 +12,21 @@ namespace StackExchange.Redis internal readonly bool IsPatternBased; /// - /// Indicates whether the channel-name is either null or a zero-length value + /// Indicates whether the channel-name is either null or a zero-length value. /// public bool IsNullOrEmpty => Value == null || Value.Length == 0; internal bool IsNull => Value == null; /// - /// Create a new redis channel from a buffer, explicitly controlling the pattern mode + /// Create a new redis channel from a buffer, explicitly controlling the pattern mode. /// /// The name of the channel to create. /// The mode for name matching. public RedisChannel(byte[] value, PatternMode mode) : this(value, DeterminePatternBased(value, mode)) {} /// - /// Create a new redis channel from a string, explicitly controlling the pattern mode + /// Create a new redis channel from a string, explicitly controlling the pattern mode. /// /// The string name of the channel to create. /// The mode for name matching. @@ -47,42 +47,42 @@ private RedisChannel(byte[] value, bool isPatternBased) }; /// - /// Indicate whether two channel names are not equal + /// Indicate whether two channel names are not equal. /// /// The first to compare. /// The second to compare. public static bool operator !=(RedisChannel x, RedisChannel y) => !(x == y); /// - /// Indicate whether two channel names are not equal + /// Indicate whether two channel names are not equal. /// /// The first to compare. /// The second to compare. public static bool operator !=(string x, RedisChannel y) => !(x == y); /// - /// Indicate whether two channel names are not equal + /// Indicate whether two channel names are not equal. /// /// The first to compare. /// The second to compare. public static bool operator !=(byte[] x, RedisChannel y) => !(x == y); /// - /// Indicate whether two channel names are not equal + /// Indicate whether two channel names are not equal. /// /// The first to compare. /// The second to compare. public static bool operator !=(RedisChannel x, string y) => !(x == y); /// - /// Indicate whether two channel names are not equal + /// Indicate whether two channel names are not equal. /// /// The first to compare. /// The second to compare. public static bool operator !=(RedisChannel x, byte[] y) => !(x == y); /// - /// Indicate whether two channel names are equal + /// Indicate whether two channel names are equal. /// /// The first to compare. /// The second to compare. @@ -90,7 +90,7 @@ private RedisChannel(byte[] value, bool isPatternBased) x.IsPatternBased == y.IsPatternBased && RedisValue.Equals(x.Value, y.Value); /// - /// Indicate whether two channel names are equal + /// Indicate whether two channel names are equal. /// /// The first to compare. /// The second to compare. @@ -98,14 +98,14 @@ private RedisChannel(byte[] value, bool isPatternBased) RedisValue.Equals(x == null ? null : Encoding.UTF8.GetBytes(x), y.Value); /// - /// Indicate whether two channel names are equal + /// Indicate whether two channel names are equal. /// /// The first to compare. /// The second to compare. public static bool operator ==(byte[] x, RedisChannel y) => RedisValue.Equals(x, y.Value); /// - /// Indicate whether two channel names are equal + /// Indicate whether two channel names are equal. /// /// The first to compare. /// The second to compare. @@ -113,14 +113,14 @@ private RedisChannel(byte[] value, bool isPatternBased) RedisValue.Equals(x.Value, y == null ? null : Encoding.UTF8.GetBytes(y)); /// - /// Indicate whether two channel names are equal + /// Indicate whether two channel names are equal. /// /// The first to compare. /// The second to compare. public static bool operator ==(RedisChannel x, byte[] y) => RedisValue.Equals(x.Value, y); /// - /// See Object.Equals + /// See . /// /// The to compare to. public override bool Equals(object obj) => obj switch @@ -132,18 +132,16 @@ private RedisChannel(byte[] value, bool isPatternBased) }; /// - /// Indicate whether two channel names are equal + /// Indicate whether two channel names are equal. /// /// The to compare to. public bool Equals(RedisChannel other) => IsPatternBased == other.IsPatternBased && RedisValue.Equals(Value, other.Value); - /// - /// See Object.GetHashCode - /// + /// public override int GetHashCode() => RedisValue.GetHashCode(Value) + (IsPatternBased ? 1 : 0); /// - /// Obtains a string representation of the channel name + /// Obtains a string representation of the channel name. /// public override string ToString() => ((string)this) ?? "(null)"; @@ -164,20 +162,20 @@ internal void AssertNotNull() internal RedisChannel Clone() => (byte[])Value?.Clone(); /// - /// The matching pattern for this channel + /// The matching pattern for this channel. /// public enum PatternMode { /// - /// Will be treated as a pattern if it includes * + /// Will be treated as a pattern if it includes *. /// Auto = 0, /// - /// Never a pattern + /// Never a pattern. /// Literal = 1, /// - /// Always a pattern + /// Always a pattern. /// Pattern = 2 } diff --git a/src/StackExchange.Redis/RedisErrorEventArgs.cs b/src/StackExchange.Redis/RedisErrorEventArgs.cs index 3722f8df4..92186d346 100644 --- a/src/StackExchange.Redis/RedisErrorEventArgs.cs +++ b/src/StackExchange.Redis/RedisErrorEventArgs.cs @@ -42,10 +42,7 @@ public RedisErrorEventArgs(object sender, EndPoint endpoint, string message) /// public string Message { get; } - void ICompletable.AppendStormLog(StringBuilder sb) - { - sb.Append("event, error: ").Append(Message); - } + void ICompletable.AppendStormLog(StringBuilder sb) => sb.Append("event, error: ").Append(Message); bool ICompletable.TryComplete(bool isAsync) => ConnectionMultiplexer.TryCompleteHandler(handler, sender, this, isAsync); } diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index bb5479380..7e23ffbc7 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -7,7 +7,7 @@ namespace StackExchange.Redis { /// - /// Provides basic information about the features available on a particular version of Redis + /// Provides basic information about the features available on a particular version of Redis. /// public readonly struct RedisFeatures { @@ -41,7 +41,7 @@ public readonly struct RedisFeatures private readonly Version version; /// - /// Create a new RedisFeatures instance for the given version + /// Create a new RedisFeatures instance for the given version. /// /// The version of redis to base the feature set on. public RedisFeatures(Version version) @@ -80,7 +80,7 @@ public RedisFeatures(Version version) public bool HashStringLength => Version >= v3_2_0; /// - /// Does HDEL support varadic usage? + /// Does HDEL support variadic usage? /// public bool HashVaradicDelete => Version >= v2_4_0; @@ -145,7 +145,7 @@ public RedisFeatures(Version version) public bool SetConditional => Version >= v2_6_12; /// - /// Does SADD support varadic usage? + /// Does SADD support variadic usage? /// public bool SetVaradicAddRemove => Version >= v2_4_0; @@ -192,7 +192,7 @@ public RedisFeatures(Version version) /// /// Is PFCOUNT supported on replicas? /// - [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(HyperLogLogCountReplicaSafe) + " instead.")] + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(HyperLogLogCountReplicaSafe) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public bool HyperLogLogCountSlaveSafe => HyperLogLogCountReplicaSafe; @@ -237,7 +237,7 @@ public RedisFeatures(Version version) public bool PushMultiple => Version >= v4_0_0; /// - /// Create a string representation of the available features + /// Create a string representation of the available features. /// public override string ToString() { @@ -266,18 +266,22 @@ orderby prop.Name /// A 32-bit signed integer that is the hash code for this instance. public override int GetHashCode() => Version.GetHashCode(); - /// Indicates whether this instance and a specified object are equal. - /// true if and this instance are the same type and represent the same value; otherwise, false. - /// The object to compare with the current instance. + /// + /// Indicates whether this instance and a specified object are equal. + /// + /// + /// if and this instance are the same type and represent the same value, otherwise. + /// + /// The object to compare with the current instance. public override bool Equals(object obj) => obj is RedisFeatures f && f.Version == Version; /// - /// Checks if 2 RedisFeatures are .Equal(). + /// Checks if 2 are .Equal(). /// public static bool operator ==(RedisFeatures left, RedisFeatures right) => left.Equals(right); /// - /// Checks if 2 RedisFeatures are not .Equal(). + /// Checks if 2 are not .Equal(). /// public static bool operator !=(RedisFeatures left, RedisFeatures right) => !left.Equals(right); } diff --git a/src/StackExchange.Redis/RedisKey.cs b/src/StackExchange.Redis/RedisKey.cs index 4b20cdd38..6739250ef 100644 --- a/src/StackExchange.Redis/RedisKey.cs +++ b/src/StackExchange.Redis/RedisKey.cs @@ -144,9 +144,7 @@ private static bool CompositeEquals(byte[] keyPrefix0, object keyValue0, byte[] return RedisValue.Equals(ConcatenateBytes(keyPrefix0, keyValue0, null), ConcatenateBytes(keyPrefix1, keyValue1, null)); } - /// - /// See . - /// + /// public override int GetHashCode() { int chk0 = KeyPrefix == null ? 0 : RedisValue.GetHashCode(KeyPrefix), diff --git a/src/StackExchange.Redis/RedisResult.cs b/src/StackExchange.Redis/RedisResult.cs index 0b62aef4b..b79d6ad7b 100644 --- a/src/StackExchange.Redis/RedisResult.cs +++ b/src/StackExchange.Redis/RedisResult.cs @@ -44,9 +44,10 @@ public static RedisResult Create(RedisResult[] values) /// internal static RedisResult NullArray { get; } = new ArrayRedisResult(null); - // internally, this is very similar to RawResult, except it is designed to be usable - // outside of the IO-processing pipeline: the buffers are standalone, etc - + /// + /// Internally, this is very similar to RawResult, except it is designed to be usable, + /// outside of the IO-processing pipeline: the buffers are standalone, etc. + /// internal static RedisResult TryCreate(PhysicalConnection connection, in RawResult result) { try @@ -535,38 +536,24 @@ object IConvertible.ToType(Type conversionType, IFormatProvider provider) { switch (System.Type.GetTypeCode(conversionType)) { - case TypeCode.Boolean: - return AsBoolean(); - case TypeCode.Char: - checked { return (char)AsInt32(); } - case TypeCode.SByte: - checked { return (sbyte)AsInt32(); } - case TypeCode.Byte: - checked { return (byte)AsInt32(); } - case TypeCode.Int16: - checked { return (short)AsInt32(); } - case TypeCode.UInt16: - checked { return (ushort)AsInt32(); } - case TypeCode.Int32: - return AsInt32(); - case TypeCode.UInt32: - checked { return (uint)AsInt64(); } - case TypeCode.Int64: - return AsInt64(); - case TypeCode.UInt64: - checked { return (ulong)AsInt64(); } - case TypeCode.Single: - return (float)AsDouble(); - case TypeCode.Double: - return AsDouble(); - case TypeCode.Decimal: - if (Type == ResultType.Integer) return AsInt64(); - break; - case TypeCode.String: - return AsString(); + case TypeCode.Boolean: return AsBoolean(); + case TypeCode.Char: checked { return (char)AsInt32(); } + case TypeCode.SByte: checked { return (sbyte)AsInt32(); } + case TypeCode.Byte: checked { return (byte)AsInt32(); } + case TypeCode.Int16: checked { return (short)AsInt32(); } + case TypeCode.UInt16: checked { return (ushort)AsInt32(); } + case TypeCode.Int32: return AsInt32(); + case TypeCode.UInt32: checked { return (uint)AsInt64(); } + case TypeCode.Int64: return AsInt64(); + case TypeCode.UInt64: checked { return (ulong)AsInt64(); } + case TypeCode.Single: return (float)AsDouble(); + case TypeCode.Double: return AsDouble(); + case TypeCode.Decimal when Type == ResultType.Integer: return AsInt64(); + case TypeCode.String: return AsString(); + default: + ThrowNotSupported(); + return default; } - ThrowNotSupported(); - return default; } void ThrowNotSupported([CallerMemberName] string caller = null) diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index fb568736e..c8678ed72 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -10,8 +10,6 @@ using Pipelines.Sockets.Unofficial.Arenas; using static StackExchange.Redis.ConnectionMultiplexer; -#pragma warning disable RCS1231 // Make parameter ref read-only. - namespace StackExchange.Redis { internal sealed class RedisServer : RedisBase, IServer @@ -661,7 +659,7 @@ public void ReplicaOf(EndPoint master, CommandFlags flags = CommandFlags.None) throw new ArgumentException("Cannot replicate to self"); } -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Type or member is obsolete // attempt to cease having an opinion on the master; will resume that when replication completes // (note that this may fail; we aren't depending on it) if (GetTiebreakerRemovalMessage() is Message tieBreakerRemoval) @@ -732,9 +730,9 @@ private static void FixFlags(Message message, ServerEndPoint server) { SaveType.BackgroundRewriteAppendOnlyFile => Message.Create(-1, flags, RedisCommand.BGREWRITEAOF), SaveType.BackgroundSave => Message.Create(-1, flags, RedisCommand.BGSAVE), -#pragma warning disable 0618 +#pragma warning disable CS0618 // Type or member is obsolete SaveType.ForegroundSave => Message.Create(-1, flags, RedisCommand.SAVE), -#pragma warning restore 0618 +#pragma warning restore CS0618 _ => throw new ArgumentOutOfRangeException(nameof(type)), }; @@ -742,9 +740,9 @@ private static void FixFlags(Message message, ServerEndPoint server) { SaveType.BackgroundRewriteAppendOnlyFile => ResultProcessor.BackgroundSaveAOFStarted, SaveType.BackgroundSave => ResultProcessor.BackgroundSaveStarted, -#pragma warning disable 0618 +#pragma warning disable CS0618 // Type or member is obsolete SaveType.ForegroundSave => ResultProcessor.DemandOK, -#pragma warning restore 0618 +#pragma warning restore CS0618 _ => throw new ArgumentOutOfRangeException(nameof(type)), }; diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index 5ca22dfb5..f97f771f0 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -43,7 +43,7 @@ internal bool TryRemoveSubscription(in RedisChannel channel, out Subscription su /// /// Gets the subscriber counts for a channel. /// - /// True if there's a subscription registered at all. + /// if there's a subscription registered at all. internal bool GetSubscriberCounts(in RedisChannel channel, out int handlers, out int queues) { if (subscriptions.TryGetValue(channel, out var sub)) @@ -280,10 +280,10 @@ public Task IdentifyEndpointAsync(RedisChannel channel, CommandFlags f return ExecuteAsync(msg, ResultProcessor.ConnectionIdentity); } - /// + /// /// This is *could* we be connected, as in "what's the theoretical endpoint for this channel?", /// rather than if we're actually connected and actually listening on that channel. - /// + /// public bool IsConnected(RedisChannel channel = default(RedisChannel)) { var server = multiplexer.GetSubscribedServer(channel) ?? multiplexer.SelectServer(RedisCommand.SUBSCRIBE, CommandFlags.DemandMaster, channel); @@ -327,7 +327,7 @@ private Message CreatePingMessage(CommandFlags flags) return msg; } - private void ThrowIfNull(in RedisChannel channel) + private static void ThrowIfNull(in RedisChannel channel) { if (channel.IsNullOrEmpty) { @@ -463,7 +463,7 @@ private Task UnsubscribeFromServerAsync(Subscription sub, RedisChannel cha /// /// Unregisters a handler or queue and returns if we should remove it from the server. /// - /// True if we should remove the subscription from the server, false otherwise. + /// if we should remove the subscription from the server, otherwise. private bool UnregisterSubscription(in RedisChannel channel, Action handler, ChannelMessageQueue queue, out Subscription sub) { ThrowIfNull(channel); diff --git a/src/StackExchange.Redis/RedisTransaction.cs b/src/StackExchange.Redis/RedisTransaction.cs index cec65c311..337658430 100644 --- a/src/StackExchange.Redis/RedisTransaction.cs +++ b/src/StackExchange.Redis/RedisTransaction.cs @@ -42,10 +42,7 @@ public ConditionResult AddCondition(Condition condition) } } - public void Execute() - { - Execute(CommandFlags.FireAndForget); - } + public void Execute() => Execute(CommandFlags.FireAndForget); public bool Execute(CommandFlags flags) { @@ -176,7 +173,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { if (message is QueuedMessage q) { - connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog($"observed QUEUED for " + q.Wrapped?.CommandAndKey); + connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog("Observed QUEUED for " + q.Wrapped?.CommandAndKey); q.WasQueued = true; } return true; @@ -193,8 +190,8 @@ private class TransactionMessage : Message, IMultiMessage public TransactionMessage(int db, CommandFlags flags, List conditions, List operations) : base(db, flags, RedisCommand.EXEC) { - InnerOperations = (operations == null || operations.Count == 0) ? Array.Empty() : operations.ToArray(); - this.conditions = (conditions == null || conditions.Count == 0) ? Array.Empty(): conditions.ToArray(); + InnerOperations = (operations?.Count > 0) ? operations.ToArray() : Array.Empty(); + this.conditions = (conditions?.Count > 0) ? conditions.ToArray() : Array.Empty(); } internal override void SetExceptionAndComplete(Exception exception, PhysicalBridge bridge) @@ -215,7 +212,10 @@ internal override void SetExceptionAndComplete(Exception exception, PhysicalBrid public override void AppendStormLog(StringBuilder sb) { base.AppendStormLog(sb); - if (conditions.Length != 0) sb.Append(", ").Append(conditions.Length).Append(" conditions"); + if (conditions.Length != 0) + { + sb.Append(", ").Append(conditions.Length).Append(" conditions"); + } sb.Append(", ").Append(InnerOperations.Length).Append(" operations"); } @@ -250,15 +250,15 @@ public IEnumerable GetMessages(PhysicalConnection connection) { try { - // Important: if the server supports EXECABORT, then we can check the pre-conditions (pause there), - // which will usually be pretty small and cheap to do - if that passes, we can just isue all the commands + // Important: if the server supports EXECABORT, then we can check the preconditions (pause there), + // which will usually be pretty small and cheap to do - if that passes, we can just issue all the commands // and rely on EXECABORT to kick us if we are being idiotic inside the MULTI. However, if the server does // *not* support EXECABORT, then we need to explicitly check for QUEUED anyway; we might as well defer // checking the preconditions to the same time to avoid having to pause twice. This will mean that on - // up-version servers, pre-condition failures exit with UNWATCH; and on down-version servers pre-condition - // failures exit with DISCARD - but that's ok : both work fine + // up-version servers, precondition failures exit with UNWATCH; and on down-version servers precondition + // failures exit with DISCARD - but that's okay : both work fine - // PART 1: issue the pre-conditions + // PART 1: issue the preconditions if (!IsAborted && conditions.Length != 0) { sb.AppendLine("issuing conditions..."); @@ -286,8 +286,8 @@ public IEnumerable GetMessages(PhysicalConnection connection) sb.AppendLine("checking conditions in the *early* path"); // need to get those sent ASAP; if they are stuck in the buffers, we die multiplexer.Trace("Flushing and waiting for precondition responses"); -#pragma warning disable CS0618 - connection.FlushSync(true, multiplexer.TimeoutMilliseconds); // make sure they get sent, so we can check for QUEUED (and the pre-conditions if necessary) +#pragma warning disable CS0618 // Type or member is obsolete + connection.FlushSync(true, multiplexer.TimeoutMilliseconds); // make sure they get sent, so we can check for QUEUED (and the preconditions if necessary) #pragma warning restore CS0618 if (Monitor.Wait(lastBox, multiplexer.TimeoutMilliseconds)) @@ -298,7 +298,7 @@ public IEnumerable GetMessages(PhysicalConnection connection) sb.Append("after condition check, we are ").Append(command).AppendLine(); } else - { // timeout running pre-conditions + { // timeout running preconditions multiplexer.Trace("Timeout checking preconditions"); command = RedisCommand.UNWATCH; @@ -312,7 +312,7 @@ public IEnumerable GetMessages(PhysicalConnection connection) // PART 2: begin the transaction if (!IsAborted) { - multiplexer.Trace("Begining transaction"); + multiplexer.Trace("Beginning transaction"); yield return Message.Create(-1, CommandFlags.None, RedisCommand.MULTI); sb.AppendLine("issued MULTI"); } @@ -345,8 +345,8 @@ public IEnumerable GetMessages(PhysicalConnection connection) sb.AppendLine("checking conditions in the *late* path"); multiplexer.Trace("Flushing and waiting for precondition+queued responses"); -#pragma warning disable CS0618 - connection.FlushSync(true, multiplexer.TimeoutMilliseconds); // make sure they get sent, so we can check for QUEUED (and the pre-conditions if necessary) +#pragma warning disable CS0618 // Type or member is obsolete + connection.FlushSync(true, multiplexer.TimeoutMilliseconds); // make sure they get sent, so we can check for QUEUED (and the preconditions if necessary) #pragma warning restore CS0618 if (Monitor.Wait(lastBox, multiplexer.TimeoutMilliseconds)) { @@ -406,10 +406,8 @@ public IEnumerable GetMessages(PhysicalConnection connection) } } - protected override void WriteImpl(PhysicalConnection physical) - { - physical.WriteHeader(Command, 0); - } + protected override void WriteImpl(PhysicalConnection physical) => physical.WriteHeader(Command, 0); + public override int ArgCount => 0; private bool AreAllConditionsSatisfied(ConnectionMultiplexer multiplexer) @@ -434,7 +432,7 @@ private bool AreAllConditionsSatisfied(ConnectionMultiplexer multiplexer) private class TransactionProcessor : ResultProcessor { - public static readonly TransactionProcessor Default = new TransactionProcessor(); + public static readonly TransactionProcessor Default = new(); public override bool SetResult(PhysicalConnection connection, Message message, in RawResult result) { @@ -487,7 +485,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var arr = result.GetItems(); if (result.IsNull) { - connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog($"aborting wrapped messages (failed watch)"); + connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog("Aborting wrapped messages (failed watch)"); connection.Trace("Server aborted due to failed WATCH"); foreach (var op in wrapped) { @@ -501,7 +499,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes else if (wrapped.Length == arr.Length) { connection.Trace("Server committed; processing nested replies"); - connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog($"processing {arr.Length} wrapped messages"); + connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog($"Processing {arr.Length} wrapped messages"); int i = 0; foreach(ref RawResult item in arr) @@ -523,10 +521,9 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes // the pending tasks foreach (var op in wrapped) { - var inner = op?.Wrapped; - if(inner != null) + if (op?.Wrapped is Message inner) { - inner.Fail(ConnectionFailureType.ProtocolFailure, null, "transaction failure"); + inner.Fail(ConnectionFailureType.ProtocolFailure, null, "Transaction failure"); inner.Complete(); } } diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index cb6a90fa9..856300206 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -11,7 +11,7 @@ namespace StackExchange.Redis { /// - /// Represents values that can be stored in redis + /// Represents values that can be stored in redis. /// public readonly struct RedisValue : IEquatable, IComparable, IComparable, IConvertible { @@ -21,7 +21,6 @@ namespace StackExchange.Redis private readonly ReadOnlyMemory _memory; private readonly long _overlappedBits64; - // internal bool IsNullOrDefaultValue { get { return (valueBlob == null && valueInt64 == 0L) || ((object)valueBlob == (object)NullSentinel); } } private RedisValue(long overlappedValue64, ReadOnlyMemory memory, object objectOrSentinel) { _overlappedBits64 = overlappedValue64; @@ -42,10 +41,10 @@ internal RedisValue(object obj, long overlappedBits) /// public RedisValue(string value) : this(0, default, value) { } -#pragma warning disable RCS1085 // Use auto-implemented property. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1085:Use auto-implemented property.", Justification = "Intentional field ref")] internal object DirectObject => _objectOrSentinel; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1085:Use auto-implemented property.", Justification = "Intentional field ref")] internal long DirectOverlappedBits64 => _overlappedBits64; -#pragma warning restore RCS1085 // Use auto-implemented property. private readonly static object Sentinel_SignedInteger = new(); private readonly static object Sentinel_UnsignedInteger = new(); @@ -93,7 +92,7 @@ public static RedisValue Unbox(object value) } /// - /// Represents the string "" + /// Represents the string "". /// public static RedisValue EmptyString { get; } = new RedisValue(0, default, Sentinel_Raw); @@ -103,22 +102,22 @@ public static RedisValue Unbox(object value) static readonly object[] s_CommonInt32 = Enumerable.Range(-1, 22).Select(i => (object)i).ToArray(); // [-1,20] = 22 values /// - /// A null value + /// A null value. /// public static RedisValue Null { get; } = new RedisValue(0, default, null); /// - /// Indicates whether the value is a primitive integer (signed or unsigned) + /// Indicates whether the value is a primitive integer (signed or unsigned). /// public bool IsInteger => _objectOrSentinel == Sentinel_SignedInteger || _objectOrSentinel == Sentinel_UnsignedInteger; /// - /// Indicates whether the value should be considered a null value + /// Indicates whether the value should be considered a null value. /// public bool IsNull => _objectOrSentinel == null; /// - /// Indicates whether the value is either null or a zero-length value + /// Indicates whether the value is either null or a zero-length value. /// public bool IsNullOrEmpty { @@ -133,12 +132,12 @@ public bool IsNullOrEmpty } /// - /// Indicates whether the value is greater than zero-length or has an integer value + /// Indicates whether the value is greater than zero-length or has an integer value. /// public bool HasValue => !IsNullOrEmpty; /// - /// Indicates whether two RedisValue values are equivalent + /// Indicates whether two RedisValue values are equivalent. /// /// The first to compare. /// The second to compare. @@ -163,7 +162,7 @@ internal ulong OverlappedValueUInt64 } /// - /// Indicates whether two RedisValue values are equivalent + /// Indicates whether two RedisValue values are equivalent. /// /// The first to compare. /// The second to compare. @@ -214,7 +213,7 @@ internal ulong OverlappedValueUInt64 } /// - /// See Object.Equals() + /// See . /// /// The other to compare. public override bool Equals(object obj) @@ -226,14 +225,12 @@ public override bool Equals(object obj) } /// - /// Indicates whether two RedisValue values are equivalent + /// Indicates whether two RedisValue values are equivalent. /// /// The to compare to. public bool Equals(RedisValue other) => this == other; - /// - /// See Object.GetHashCode() - /// + /// public override int GetHashCode() => GetHashCode(this); private static int GetHashCode(RedisValue x) { @@ -249,7 +246,7 @@ private static int GetHashCode(RedisValue x) } /// - /// Returns a string representation of the value + /// Returns a string representation of the value. /// public override string ToString() => (string)this; @@ -341,7 +338,7 @@ internal StorageType Type }; /// - /// Compare against a RedisValue for relative order + /// Compare against a RedisValue for relative order. /// /// The other to compare. public int CompareTo(RedisValue other) => CompareTo(this, other); @@ -847,7 +844,7 @@ public static implicit operator byte[] (RedisValue value) } /// - /// Converts a to a ReadOnlyMemory + /// Converts a to a . /// /// The to convert. public static implicit operator ReadOnlyMemory(RedisValue value) @@ -942,10 +939,10 @@ internal RedisValue Simplify() } /// - /// Convert to a signed long if possible, returning true. - /// Returns false otherwise. + /// Convert to a signed if possible. /// /// The value, if conversion was possible. + /// if successfully parsed, otherwise. public bool TryParse(out long val) { switch (Type) @@ -976,10 +973,10 @@ public bool TryParse(out long val) } /// - /// Convert to a int if possible, returning true. - /// Returns false otherwise. + /// Convert to an if possible. /// /// The value, if conversion was possible. + /// if successfully parsed, otherwise. public bool TryParse(out int val) { if (!TryParse(out long l) || l > int.MaxValue || l < int.MinValue) @@ -993,10 +990,10 @@ public bool TryParse(out int val) } /// - /// Convert to a double if possible, returning true. - /// Returns false otherwise. + /// Convert to a if possible. /// /// The value, if conversion was possible. + /// if successfully parsed, otherwise. public bool TryParse(out double val) { switch (Type) @@ -1024,8 +1021,8 @@ public bool TryParse(out double val) } /// - /// Create a RedisValue from a MemoryStream; it will *attempt* to use the internal buffer - /// directly, but if this isn't possible it will fallback to ToArray + /// Create a from a . + /// It will *attempt* to use the internal buffer directly, but if this isn't possible it will fallback to . /// /// The to create a value from. public static RedisValue CreateFrom(MemoryStream stream) diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index d8c9efd18..b29a87d3f 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -512,7 +512,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes byte[] hash = null; if (!message.IsInternalCall) { - hash = ParseSHA1(asciiHash); // external caller wants the hex bytes, not the ascii bytes + hash = ParseSHA1(asciiHash); // external caller wants the hex bytes, not the ASCII bytes } if (message is RedisDatabase.ScriptLoadMessage sl) { @@ -739,7 +739,8 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } if (roleSeen) - { // these are in the same section, if presnt + { + // These are in the same section, if present server.MasterEndPoint = Format.TryParseEndPoint(masterHost, masterPort); } } @@ -958,7 +959,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes case 2: if (arr[0].TryGetInt64(out unixTime) && arr[1].TryGetInt64(out long micros)) { - var time = RedisBase.UnixEpoch.AddSeconds(unixTime).AddTicks(micros * 10); // datetime ticks are 100ns + var time = RedisBase.UnixEpoch.AddSeconds(unixTime).AddTicks(micros * 10); // DateTime ticks are 100ns SetResult(message, time); return true; } @@ -1716,8 +1717,13 @@ protected override StreamConsumerInfo ParseItem(in RawResult result) private static class KeyValuePairParser { internal static readonly CommandBytes - Name = "name", Consumers = "consumers", Pending = "pending", Idle = "idle", LastDeliveredId = "last-delivered-id", - IP = "ip", Port = "port"; + Name = "name", + Consumers = "consumers", + Pending = "pending", + Idle = "idle", + LastDeliveredId = "last-delivered-id", + IP = "ip", + Port = "port"; internal static bool TryRead(Sequence pairs, in CommandBytes key, ref long value) { diff --git a/src/StackExchange.Redis/ScriptParameterMapper.cs b/src/StackExchange.Redis/ScriptParameterMapper.cs index feab17c17..314f6a835 100644 --- a/src/StackExchange.Redis/ScriptParameterMapper.cs +++ b/src/StackExchange.Redis/ScriptParameterMapper.cs @@ -39,7 +39,7 @@ private static string[] ExtractParameters(string script) { var prevChar = script[ix]; - // don't consider this a parameter if it's in the middle of word (ie. if it's preceeded by a letter) + // don't consider this a parameter if it's in the middle of word (i.e. if it's preceded by a letter) if (char.IsLetterOrDigit(prevChar) || prevChar == '_') continue; // this is an escape, ignore it @@ -125,8 +125,7 @@ static ScriptParameterMapper() } /// - /// Turns a script with @namedParameters into a LuaScript that can be executed - /// against a given IDatabase(Async) object + /// Turns a script with @namedParameters into a LuaScript that can be executed against a given IDatabase(Async) object. /// /// The script to prepare. public static LuaScript PrepareScript(string script) @@ -137,26 +136,26 @@ public static LuaScript PrepareScript(string script) return new LuaScript(script, ordinalScript, ps); } - private static readonly HashSet ConvertableTypes = - new HashSet { - typeof(int), - typeof(int?), - typeof(long), - typeof(long?), - typeof(double), - typeof(double?), - typeof(string), - typeof(byte[]), - typeof(ReadOnlyMemory), - typeof(bool), - typeof(bool?), - - typeof(RedisKey), - typeof(RedisValue) - }; + private static readonly HashSet ConvertableTypes = new() + { + typeof(int), + typeof(int?), + typeof(long), + typeof(long?), + typeof(double), + typeof(double?), + typeof(string), + typeof(byte[]), + typeof(ReadOnlyMemory), + typeof(bool), + typeof(bool?), + + typeof(RedisKey), + typeof(RedisValue) + }; /// - /// Determines whether or not the given type can be used to provide parameters for the given LuaScript. + /// Determines whether or not the given type can be used to provide parameters for the given . /// /// The type of the parameter. /// The script to match against. @@ -279,7 +278,7 @@ public static bool IsValidParameterHash(Type t, LuaScript script, out string mis valuesResult = Expression.NewArrayInit(typeof(RedisValue), args.Select(arg => { var member = GetMember(objTyped, arg); - if (member.Type == typeof(RedisValue)) return member; // pass-thru + if (member.Type == typeof(RedisValue)) return member; // pass-through if (member.Type == typeof(RedisKey)) { // need to apply prefix (note we can re-use the body from earlier) var val = keysResultArr[keys.IndexOf(arg)]; diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 560bba8c6..271d0c61c 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -127,7 +127,7 @@ internal Exception LastException var subEx = subscription?.LastException; var subExData = subEx?.Data; - //check if subscription endpoint has a better lastexception + //check if subscription endpoint has a better last exception if (subExData != null && subExData.Contains("Redis-FailureType") && subExData["Redis-FailureType"]?.ToString() != nameof(ConnectionFailureType.UnableToConnect)) { return subEx; @@ -136,14 +136,7 @@ internal Exception LastException } } - internal PhysicalBridge.State ConnectionState - { - get - { - var tmp = interactive; - return tmp?.ConnectionState ?? State.Disconnected; - } - } + internal State ConnectionState => interactive?.ConnectionState ?? State.Disconnected; public bool IsReplica { get { return isReplica; } set { SetConfig(ref isReplica, value); } } @@ -162,15 +155,31 @@ public long OperationCount public bool RequiresReadMode => serverType == ServerType.Cluster && IsReplica; - public ServerType ServerType { get { return serverType; } set { SetConfig(ref serverType, value); } } + public ServerType ServerType + { + get => serverType; + set => SetConfig(ref serverType, value); + } - public bool ReplicaReadOnly { get { return replicaReadOnly; } set { SetConfig(ref replicaReadOnly, value); } } + public bool ReplicaReadOnly + { + get => replicaReadOnly; + set => SetConfig(ref replicaReadOnly, value); + } public bool AllowReplicaWrites { get; set; } - public Version Version { get { return version; } set { SetConfig(ref version, value); } } + public Version Version + { + get => version; + set => SetConfig(ref version, value); + } - public int WriteEverySeconds { get { return writeEverySeconds; } set { SetConfig(ref writeEverySeconds, value); } } + public int WriteEverySeconds + { + get => writeEverySeconds; + set => SetConfig(ref writeEverySeconds, value); + } internal ConnectionMultiplexer Multiplexer { get; } @@ -308,10 +317,7 @@ public void SetUnselectable(UnselectableFlags flags) public ValueTask TryWriteAsync(Message message) => GetBridge(message)?.TryWriteAsync(message, isReplica) ?? new ValueTask(WriteResult.NoConnectionAvailable); - internal void Activate(ConnectionType type, LogProxy log) - { - GetBridge(type, true, log); - } + internal void Activate(ConnectionType type, LogProxy log) => GetBridge(type, true, log); internal void AddScript(string script, byte[] hash) { @@ -411,7 +417,7 @@ internal async Task AutoConfigureAsync(PhysicalConnection connection, LogProxy l private int _nextReplicaOffset; internal uint NextReplicaOffset() // used to round-robin between multiple replicas - => (uint)System.Threading.Interlocked.Increment(ref _nextReplicaOffset); + => (uint)Interlocked.Increment(ref _nextReplicaOffset); internal Task Close(ConnectionType connectionType) { @@ -505,11 +511,7 @@ internal byte[] GetScriptHash(string script, RedisCommand command) return found; } - internal string GetStormLog(Message message) - { - var bridge = GetBridge(message); - return bridge?.GetStormLog(); - } + internal string GetStormLog(Message message) => GetBridge(message)?.GetStormLog(); internal Message GetTracerMessage(bool assertIdentity) { @@ -643,16 +645,14 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) } } - internal int LastInfoReplicationCheckSecondsAgo - { - get { return unchecked(Environment.TickCount - Thread.VolatileRead(ref lastInfoReplicationCheckTicks)) / 1000; } - } + internal int LastInfoReplicationCheckSecondsAgo => + unchecked(Environment.TickCount - Thread.VolatileRead(ref lastInfoReplicationCheckTicks)) / 1000; private EndPoint masterEndPoint; public EndPoint MasterEndPoint { - get { return masterEndPoint; } - set { SetConfig(ref masterEndPoint, value); } + get => masterEndPoint; + set => SetConfig(ref masterEndPoint, value); } /// @@ -671,7 +671,7 @@ internal bool CheckInfoReplication() var msg = Message.Create(-1, CommandFlags.FireAndForget | CommandFlags.NoRedirect, RedisCommand.INFO, RedisLiterals.replication); msg.SetInternalCall(); msg.SetSource(ResultProcessor.AutoConfigure, null); -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Type or member is obsolete bridge.TryWriteSync(msg, isReplica); #pragma warning restore CS0618 return true; @@ -903,8 +903,8 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy log) { return; } - var connType = bridge.ConnectionType; + var connType = bridge.ConnectionType; if (connType == ConnectionType.Interactive) { await AutoConfigureAsync(connection, log); diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index 7919d4615..0cfdf7fd0 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -71,6 +71,9 @@ public int HashSlot(in RedisKey key) public int HashSlot(in RedisChannel channel) => ServerType == ServerType.Standalone || channel.IsNull ? NoSlot : GetClusterSlot((byte[])channel); + /// + /// Gets the hashslot for a given byte sequence. + /// /// /// HASH_SLOT = CRC16(key) mod 16384 /// @@ -107,7 +110,7 @@ public ServerEndPoint Select(Message message, bool allowDisconnected = false) switch (ServerType) { case ServerType.Cluster: - case ServerType.Twemproxy: // strictly speaking twemproxy uses a different hashing algo, but the hash-tag behavior is + case ServerType.Twemproxy: // strictly speaking twemproxy uses a different hashing algorithm, but the hash-tag behavior is // the same, so this does a pretty good job of spotting illegal commands before sending them slot = message.GetHashSlot(this); @@ -171,13 +174,13 @@ public bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isM else { message.PrepareToResend(resendVia, isMoved); -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Type or member is obsolete retry = resendVia.TryWriteSync(message) == WriteResult.Success; #pragma warning restore CS0618 } } - if (isMoved) // update map; note we can still update the map even if we aren't actually goint to resend + if (isMoved) // update map; note we can still update the map even if we aren't actually going to resend { var arr = MapForMutation(); var oldServer = arr[hashSlot]; diff --git a/src/StackExchange.Redis/SortedSetEntry.cs b/src/StackExchange.Redis/SortedSetEntry.cs index 4bd09899b..9214b5485 100644 --- a/src/StackExchange.Redis/SortedSetEntry.cs +++ b/src/StackExchange.Redis/SortedSetEntry.cs @@ -60,13 +60,11 @@ public SortedSetEntry(RedisValue element, double score) public static implicit operator SortedSetEntry(KeyValuePair value) => new SortedSetEntry(value.Key, value.Value); /// - /// See . + /// A "{element}: {score}" string representation of the entry. /// public override string ToString() => element + ": " + score; - /// - /// See . - /// + /// public override int GetHashCode() => element.GetHashCode() ^ score.GetHashCode(); /// diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index 2a5cf6625..221b776d6 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -463,9 +463,9 @@ public void DefaultThreadPoolManagerIsDetected() [Theory] [InlineData("myDNS:myPort,password=myPassword,connectRetry=3,connectTimeout=15000,syncTimeout=15000,defaultDatabase=0,abortConnect=false,ssl=true,sslProtocols=Tls12", SslProtocols.Tls12)] [InlineData("myDNS:myPort,password=myPassword,abortConnect=false,ssl=true,sslProtocols=Tls12", SslProtocols.Tls12)] -#pragma warning disable CS0618 // obsolete +#pragma warning disable CS0618 // Type or member is obsolete [InlineData("myDNS:myPort,password=myPassword,abortConnect=false,ssl=true,sslProtocols=Ssl3", SslProtocols.Ssl3)] -#pragma warning restore CS0618 // obsolete +#pragma warning restore CS0618 [InlineData("myDNS:myPort,password=myPassword,abortConnect=false,ssl=true,sslProtocols=Tls12 ", SslProtocols.Tls12)] public void ParseTlsWithoutTrailingComma(string configString, SslProtocols expected) { diff --git a/tests/StackExchange.Redis.Tests/Deprecated.cs b/tests/StackExchange.Redis.Tests/Deprecated.cs index 68c76bea3..aa05ab5d0 100644 --- a/tests/StackExchange.Redis.Tests/Deprecated.cs +++ b/tests/StackExchange.Redis.Tests/Deprecated.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis.Tests { /// - /// Testing that things we depcreate still parse, but are otherwise defaults. + /// Testing that things we deprecate still parse, but are otherwise defaults. /// public class Deprecated : TestBase { @@ -52,6 +52,6 @@ public void ResponseTimeout() options = ConfigurationOptions.Parse("responseTimeout=1000"); Assert.Equal(0, options.ResponseTimeout); } -#pragma warning restore CS0618 // Type or member is obsolete +#pragma warning restore CS0618 } } diff --git a/tests/StackExchange.Redis.Tests/Helpers/Skip.cs b/tests/StackExchange.Redis.Tests/Helpers/Skip.cs index d09e5b6d7..6fa8a6911 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Skip.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Skip.cs @@ -42,12 +42,10 @@ internal static void IfMissingDatabase(IConnectionMultiplexer conn, int dbId) } } -#pragma warning disable RCS1194 // Implement exception constructors. public class SkipTestException : Exception { public string MissingFeatures { get; set; } public SkipTestException(string reason) : base(reason) { } } -#pragma warning restore RCS1194 // Implement exception constructors. } diff --git a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs index eafd4360f..39a9af890 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs @@ -32,13 +32,11 @@ public enum KeyType None, String, List, Set } -#pragma warning disable RCS1194 // Implement exception constructors. public class ResponseException : Exception { public string Code { get; } public ResponseException(string code) : base("Response error") => Code = code; } -#pragma warning restore RCS1194 // Implement exception constructors. public Redis(string host, int port) { diff --git a/tests/StackExchange.Redis.Tests/MassiveOps.cs b/tests/StackExchange.Redis.Tests/MassiveOps.cs index 6f5d1e26e..a78aa44e1 100644 --- a/tests/StackExchange.Redis.Tests/MassiveOps.cs +++ b/tests/StackExchange.Redis.Tests/MassiveOps.cs @@ -47,9 +47,11 @@ static void nonTrivial(Task _) for (int i = 0; i <= AsyncOpsQty; i++) { var t = conn.StringSetAsync(key, i); -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - if (withContinuation) t.ContinueWith(nonTrivial); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + if (withContinuation) + { + // Intentionally unawaited + _ = t.ContinueWith(nonTrivial); + } } Assert.Equal(AsyncOpsQty, await conn.StringGetAsync(key).ForAwait()); watch.Stop(); diff --git a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs index a91047be1..fb1627b62 100644 --- a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs @@ -62,7 +62,7 @@ public bool IgnoreConnect public long OperationCount => _inner.OperationCount; -#pragma warning disable CS0618 +#pragma warning disable CS0618 // Type or member is obsolete public bool PreserveAsyncOrder { get => _inner.PreserveAsyncOrder; set => _inner.PreserveAsyncOrder = value; } #pragma warning restore CS0618 diff --git a/tests/StackExchange.Redis.Tests/Transactions.cs b/tests/StackExchange.Redis.Tests/Transactions.cs index 7e83282c5..58ccf1987 100644 --- a/tests/StackExchange.Redis.Tests/Transactions.cs +++ b/tests/StackExchange.Redis.Tests/Transactions.cs @@ -1,6 +1,4 @@ -#pragma warning disable RCS1090 // Call 'ConfigureAwait(false)'. - -using System; +using System; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index 98e29cfd6..f7bf9a6be 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -22,12 +22,10 @@ public WrapperBaseTests() wrapper = new WrapperBase(mock.Object, Encoding.UTF8.GetBytes("prefix:")); } -#pragma warning disable RCS1047 // Non-asynchronous method name should not end with 'Async'. - [Fact] - public void DebugObjectAsync() + public async Task DebugObjectAsync() { - wrapper.DebugObjectAsync("key", CommandFlags.None); + await wrapper.DebugObjectAsync("key", CommandFlags.None); mock.Verify(_ => _.DebugObjectAsync("prefix:key", CommandFlags.None)); } From 995581aff54569563cdab8d1d181fdaf154c4139 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 8 Feb 2022 22:43:44 -0500 Subject: [PATCH 074/435] Solution cleanup (#1983) Cleanup pass - mostly on tests this round to get solution & analyzers back to clean. --- .editorconfig | 14 +- src/StackExchange.Redis/GlobalSuppressions.cs | 14 ++ src/StackExchange.Redis/PhysicalBridge.cs | 5 +- src/StackExchange.Redis/PhysicalConnection.cs | 6 +- src/StackExchange.Redis/RawResult.cs | 1 - src/StackExchange.Redis/RedisSubscriber.cs | 3 +- src/StackExchange.Redis/ServerEndPoint.cs | 3 +- .../AggresssiveTests.cs | 8 +- .../AzureMaintenanceEventTests.cs | 2 +- .../StackExchange.Redis.Tests/BacklogTests.cs | 12 +- tests/StackExchange.Redis.Tests/BasicOps.cs | 6 +- tests/StackExchange.Redis.Tests/Cluster.cs | 6 +- tests/StackExchange.Redis.Tests/Config.cs | 7 +- .../ConnectFailTimeout.cs | 2 +- .../ConnectingFailDetection.cs | 6 +- .../StackExchange.Redis.Tests/Constraints.cs | 2 +- .../DatabaseWrapperTests.cs | 44 ++-- .../EventArgsTests.cs | 35 +-- tests/StackExchange.Redis.Tests/Failover.cs | 10 +- tests/StackExchange.Redis.Tests/GeoTests.cs | 4 +- .../GlobalSuppressions.cs | 22 +- .../Helpers/Attributes.cs | 6 +- .../Helpers/redis-sharp.cs | 18 +- .../Issues/Issue1101.cs | 25 ++- .../Issues/Issue25.cs | 8 +- .../Issues/SO24807536.cs | 2 +- .../Issues/SO25567566.cs | 2 +- .../KeysAndValues.cs | 6 +- tests/StackExchange.Redis.Tests/Lex.cs | 2 +- tests/StackExchange.Redis.Tests/Lists.cs | 11 +- tests/StackExchange.Redis.Tests/Locking.cs | 2 +- tests/StackExchange.Redis.Tests/Migrate.cs | 4 +- tests/StackExchange.Redis.Tests/Naming.cs | 2 +- tests/StackExchange.Redis.Tests/PubSub.cs | 22 +- .../PubSubMultiserver.cs | 31 ++- .../RedisValueEquivalency.cs | 2 +- tests/StackExchange.Redis.Tests/SSL.cs | 6 +- tests/StackExchange.Redis.Tests/Scans.cs | 2 +- tests/StackExchange.Redis.Tests/Sentinel.cs | 10 +- .../StackExchange.Redis.Tests/SentinelBase.cs | 20 +- .../SentinelFailover.cs | 10 +- tests/StackExchange.Redis.Tests/Sets.cs | 8 +- .../SharedConnectionFixture.cs | 204 ++++-------------- tests/StackExchange.Redis.Tests/SortedSets.cs | 2 +- tests/StackExchange.Redis.Tests/Streams.cs | 6 +- tests/StackExchange.Redis.Tests/Strings.cs | 4 +- tests/StackExchange.Redis.Tests/TestBase.cs | 27 ++- tests/StackExchange.Redis.Tests/Values.cs | 5 +- .../WithKeyPrefixTests.cs | 2 +- .../WrapperBaseTests.cs | 40 ++-- .../GlobalSuppressions.cs | 8 + 51 files changed, 306 insertions(+), 403 deletions(-) create mode 100644 src/StackExchange.Redis/GlobalSuppressions.cs create mode 100644 toys/StackExchange.Redis.Server/GlobalSuppressions.cs diff --git a/.editorconfig b/.editorconfig index 27ae69cf2..cb7636856 100644 --- a/.editorconfig +++ b/.editorconfig @@ -89,8 +89,20 @@ csharp_space_after_keywords_in_control_flow_statements = true:suggestion # Language settings csharp_prefer_simple_default_expression = false:none +# IDE0090: Use 'new(...)' +dotnet_diagnostic.IDE0090.severity = silent + +# RCS1098: Constant values should be placed on right side of comparisons. +dotnet_diagnostic.RCS1098.severity = none + # RCS1194: Implement exception constructors. dotnet_diagnostic.RCS1194.severity = none # RCS1229: Use async/await when necessary. -dotnet_diagnostic.RCS1229.severity = none \ No newline at end of file +dotnet_diagnostic.RCS1229.severity = none + +# RCS1233: Use short-circuiting operator. +dotnet_diagnostic.RCS1233.severity = none + +# RCS1234: Duplicate enum value. +dotnet_diagnostic.RCS1234.severity = none diff --git a/src/StackExchange.Redis/GlobalSuppressions.cs b/src/StackExchange.Redis/GlobalSuppressions.cs new file mode 100644 index 000000000..622713622 --- /dev/null +++ b/src/StackExchange.Redis/GlobalSuppressions.cs @@ -0,0 +1,14 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~P:StackExchange.Redis.Message.IsAdmin")] +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.ServerEndPoint.GetBridge(StackExchange.Redis.RedisCommand,System.Boolean)~StackExchange.Redis.PhysicalBridge")] +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisValue.op_Equality(StackExchange.Redis.RedisValue,StackExchange.Redis.RedisValue)~System.Boolean")] +[assembly: SuppressMessage("Style", "IDE0075:Simplify conditional expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisSubscriber.Unsubscribe(StackExchange.Redis.RedisChannel@,System.Action{StackExchange.Redis.RedisChannel,StackExchange.Redis.RedisValue},StackExchange.Redis.ChannelMessageQueue,StackExchange.Redis.CommandFlags)~System.Boolean")] +[assembly: SuppressMessage("Roslynator", "RCS1104:Simplify conditional expression.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisSubscriber.Unsubscribe(StackExchange.Redis.RedisChannel@,System.Action{StackExchange.Redis.RedisChannel,StackExchange.Redis.RedisValue},StackExchange.Redis.ChannelMessageQueue,StackExchange.Redis.CommandFlags)~System.Boolean")] +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Message.IsMasterOnly(StackExchange.Redis.RedisCommand)~System.Boolean")] +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Message.RequiresDatabase(StackExchange.Redis.RedisCommand)~System.Boolean")] diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index f820569ff..4e4f3097a 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -31,11 +31,12 @@ internal sealed class PhysicalBridge : IDisposable /// We have 1 queue in play on this bridge. /// We're bypassing the queue for handshake events that go straight to the socket. /// Everything else that's not an internal call goes into the queue if there is a queue. - /// + /// + /// /// In a later release we want to remove per-server events from this queue completely and shunt queued messages /// to another capable primary connection if one is available to process them faster (order is already hosed). /// For now, simplicity in: queue it all, replay or timeout it all. - /// + /// private readonly ConcurrentQueue _backlog = new(); private bool BacklogHasItems => !_backlog.IsEmpty; private int _backlogProcessorIsRunning = 0; diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 3a1bb5b3b..27f17a6ba 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -975,7 +975,7 @@ private static void WriteUnifiedBlob(PipeWriter writer, byte[] value) WriteUnifiedSpan(writer, new ReadOnlySpan(value)); } } - + private static void WriteUnifiedSpan(PipeWriter writer, ReadOnlySpan value) { // ${len}\r\n = 3 + MaxInt32TextLen @@ -1274,7 +1274,7 @@ public override string ToString() => $"SentAwaitingResponse: {MessagesSentAwaitingResponse}, AvailableOnSocket: {BytesAvailableOnSocket} byte(s), InReadPipe: {BytesInReadPipe} byte(s), InWritePipe: {BytesInWritePipe} byte(s), ReadStatus: {ReadStatus}, WriteStatus: {WriteStatus}"; /// - /// The default connection stats, notable *not* the same as default since initializers don't run. + /// The default connection stats, notable *not* the same as default since initializers don't run. /// public static ConnectionStatus Default { get; } = new() { @@ -1513,7 +1513,7 @@ private void MatchResult(in RawResult result) if (!_writtenAwaitingResponse.TryDequeue(out msg)) { throw new InvalidOperationException("Received response with no message waiting: " + result.ToString()); - }; + } #else if (_writtenAwaitingResponse.Count == 0) { diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 6226b102f..991bcc92d 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -374,4 +374,3 @@ internal bool TryGetInt64(out long value) } } } - diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index f97f771f0..0bd86467e 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -167,7 +167,7 @@ internal Message GetMessage(RedisChannel channel, SubscriptionAction action, Com SubscriptionAction.Subscribe when !isPattern => RedisCommand.SUBSCRIBE, SubscriptionAction.Unsubscribe when !isPattern => RedisCommand.UNSUBSCRIBE, - _ => throw new ArgumentOutOfRangeException("This would be an impressive boolean feat"), + _ => throw new ArgumentOutOfRangeException(nameof(action), "This would be an impressive boolean feat"), }; // TODO: Consider flags here - we need to pass Fire and Forget, but don't want to intermingle Primary/Replica @@ -418,7 +418,6 @@ public Task EnsureSubscribedToServerAsync(Subscription sub, RedisChannel c void ISubscriber.Unsubscribe(RedisChannel channel, Action handler, CommandFlags flags) => Unsubscribe(channel, handler, null, flags); - [SuppressMessage("Style", "IDE0075:Simplify conditional expression", Justification = "The suggestion sucks.")] public bool Unsubscribe(in RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags) { ThrowIfNull(channel); diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 271d0c61c..79e9b8d63 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -272,13 +272,12 @@ public void SetClusterConfiguration(ClusterConfiguration configuration) } } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0071:Simplify interpolation", Justification = "Allocations (string.Concat vs. string.Format)")] public void UpdateNodeRelations(ClusterConfiguration configuration) { var thisNode = configuration.Nodes.FirstOrDefault(x => x.EndPoint.Equals(EndPoint)); if (thisNode != null) { - Multiplexer.Trace($"Updating node relations for {thisNode.EndPoint.ToString()}..."); + Multiplexer.Trace($"Updating node relations for {Format.ToString(thisNode.EndPoint)}..."); List replicas = null; ServerEndPoint master = null; foreach (var node in configuration.Nodes) diff --git a/tests/StackExchange.Redis.Tests/AggresssiveTests.cs b/tests/StackExchange.Redis.Tests/AggresssiveTests.cs index 438431a75..82ba6c859 100644 --- a/tests/StackExchange.Redis.Tests/AggresssiveTests.cs +++ b/tests/StackExchange.Redis.Tests/AggresssiveTests.cs @@ -119,7 +119,7 @@ private void BatchRunIntegers(IDatabase db) Writer.WriteLine($"tally: {count}"); } - private void BatchRunPings(IDatabase db) + private static void BatchRunPings(IDatabase db) { Task[] tasks = new Task[InnerCount]; for (int i = 0; i < IterationCount; i++) @@ -175,7 +175,7 @@ private async Task BatchRunIntegersAsync(IDatabase db) Writer.WriteLine($"tally: {count}"); } - private async Task BatchRunPingsAsync(IDatabase db) + private static async Task BatchRunPingsAsync(IDatabase db) { Task[] tasks = new Task[InnerCount]; for (int i = 0; i < IterationCount; i++) @@ -240,7 +240,7 @@ private void TranRunIntegers(IDatabase db) Writer.WriteLine($"tally: {count}"); } - private void TranRunPings(IDatabase db) + private static void TranRunPings(IDatabase db) { var key = Me(); db.KeyDelete(key); @@ -300,7 +300,7 @@ private async Task TranRunIntegersAsync(IDatabase db) Writer.WriteLine($"tally: {count}"); } - private async Task TranRunPingsAsync(IDatabase db) + private static async Task TranRunPingsAsync(IDatabase db) { var key = Me(); db.KeyDelete(key); diff --git a/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs b/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs index bd4b966f9..07d33e951 100644 --- a/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs +++ b/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs @@ -38,7 +38,7 @@ public void TestAzureMaintenanceEventStrings(string message, AzureNotificationTy { expectedStartTimeUtc = DateTime.SpecifyKind(startTimeUtc, DateTimeKind.Utc); } - IPAddress.TryParse(expectedIP, out IPAddress expectedIPAddress); + _ = IPAddress.TryParse(expectedIP, out IPAddress expectedIPAddress); var azureMaintenance = new AzureMaintenanceEvent(message); diff --git a/tests/StackExchange.Redis.Tests/BacklogTests.cs b/tests/StackExchange.Redis.Tests/BacklogTests.cs index 990d15d61..5cce8a407 100644 --- a/tests/StackExchange.Redis.Tests/BacklogTests.cs +++ b/tests/StackExchange.Redis.Tests/BacklogTests.cs @@ -77,7 +77,7 @@ void PrintSnapshot(ConnectionMultiplexer muxer) Writer.WriteLine("Test: Allowing reconnect"); muxer.AllowConnect = true; Writer.WriteLine("Test: Awaiting reconnect"); - await UntilCondition(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); + await UntilConditionAsync(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); Writer.WriteLine("Test: Reconnecting"); Assert.True(muxer.IsConnected); @@ -158,7 +158,7 @@ public async Task QueuesAndFlushesAfterReconnectingAsync() Writer.WriteLine("Test: Allowing reconnect"); muxer.AllowConnect = true; Writer.WriteLine("Test: Awaiting reconnect"); - await UntilCondition(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); + await UntilConditionAsync(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); Writer.WriteLine("Test: Checking reconnected 1"); Assert.True(muxer.IsConnected); @@ -253,7 +253,7 @@ void disconnectedPings(int id) Assert.False(muxer.IsConnected); // Give the tasks time to queue - await UntilCondition(TimeSpan.FromSeconds(5), () => server.GetBridgeStatus(ConnectionType.Interactive).BacklogMessagesPending >= 3); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => server.GetBridgeStatus(ConnectionType.Interactive).BacklogMessagesPending >= 3); var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); Log($"Test Stats: (BacklogMessagesPending: {disconnectedStats.BacklogMessagesPending}, TotalBacklogMessagesQueued: {disconnectedStats.TotalBacklogMessagesQueued})"); @@ -262,7 +262,7 @@ void disconnectedPings(int id) Writer.WriteLine("Test: Allowing reconnect"); muxer.AllowConnect = true; Writer.WriteLine("Test: Awaiting reconnect"); - await UntilCondition(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); + await UntilConditionAsync(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); Writer.WriteLine("Test: Checking reconnected 1"); Assert.True(muxer.IsConnected); @@ -359,7 +359,7 @@ static Task PingAsync(ServerEndPoint server, CommandFlags flags = Comm Writer.WriteLine("Test: Allowing reconnect"); muxer.AllowConnect = true; Writer.WriteLine("Test: Awaiting reconnect"); - await UntilCondition(TimeSpan.FromSeconds(3), () => server.IsConnected).ForAwait(); + await UntilConditionAsync(TimeSpan.FromSeconds(3), () => server.IsConnected).ForAwait(); Writer.WriteLine("Test: Checking reconnected 1"); Assert.True(server.IsConnected); @@ -384,7 +384,7 @@ static Task PingAsync(ServerEndPoint server, CommandFlags flags = Comm _ = PingAsync(server); _ = PingAsync(server); Writer.WriteLine("Test: Last Ping issued"); - lastPing = PingAsync(server); ; + lastPing = PingAsync(server); // We should see none queued Writer.WriteLine("Test: BacklogMessagesPending check"); diff --git a/tests/StackExchange.Redis.Tests/BasicOps.cs b/tests/StackExchange.Redis.Tests/BasicOps.cs index 7a1662d99..0b8a14247 100644 --- a/tests/StackExchange.Redis.Tests/BasicOps.cs +++ b/tests/StackExchange.Redis.Tests/BasicOps.cs @@ -295,10 +295,10 @@ public async Task TestSevered() var server = GetServer(muxer); server.SimulateConnectionFailure(SimulatedFailureType.All); var watch = Stopwatch.StartNew(); - await UntilCondition(TimeSpan.FromSeconds(10), () => server.IsConnected); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => server.IsConnected); watch.Stop(); Log("Time to re-establish: {0}ms (any order)", watch.ElapsedMilliseconds); - await UntilCondition(TimeSpan.FromSeconds(10), () => key == db.StringGet(key)); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => key == db.StringGet(key)); Debug.WriteLine("Pinging..."); Assert.Equal(key, db.StringGet(key)); } @@ -394,7 +394,7 @@ public void IncrDifferentSizes() } } - private void Incr(IDatabase database, RedisKey key, int delta, ref int total) + private static void Incr(IDatabase database, RedisKey key, int delta, ref int total) { database.StringIncrement(key, delta, CommandFlags.FireAndForget); total += delta; diff --git a/tests/StackExchange.Redis.Tests/Cluster.cs b/tests/StackExchange.Redis.Tests/Cluster.cs index 6e6e6e7c3..51282db1b 100644 --- a/tests/StackExchange.Redis.Tests/Cluster.cs +++ b/tests/StackExchange.Redis.Tests/Cluster.cs @@ -153,8 +153,8 @@ static string StringGet(IServer server, RedisKey key, CommandFlags flags = Comma var db = conn.GetDatabase(); db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, value, flags: CommandFlags.FireAndForget); - servers.First().Ping(); - var config = servers.First().ClusterConfiguration; + servers[0].Ping(); + var config = servers[0].ClusterConfiguration; Assert.NotNull(config); int slot = conn.HashSlot(key); var rightMasterNode = config.GetBySlot(key); @@ -426,7 +426,7 @@ public void SScan() { db.SetAdd(key, i, CommandFlags.FireAndForget); totalUnfiltered += i; - if (i.ToString().Contains("3")) totalFiltered += i; + if (i.ToString().Contains('3')) totalFiltered += i; } var unfilteredActual = db.SetScan(key).Select(x => (int)x).Sum(); var filteredActual = db.SetScan(key, "*3*").Select(x => (int)x).Sum(); diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index 221b776d6..99ef60996 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -42,7 +42,7 @@ public void ConfigurationOption_CheckCertificateRevocation(string conString, boo var options = ConfigurationOptions.Parse($"host,{conString}"); Assert.Equal(expectedValue, options.CheckCertificateRevocation); var toString = options.ToString(); - Assert.True(toString.IndexOf(conString, StringComparison.CurrentCultureIgnoreCase) >= 0); + Assert.Contains(conString, toString, StringComparison.CurrentCultureIgnoreCase); } [Fact] @@ -514,10 +514,7 @@ public void Apply() Assert.Equal("FooApply", options.ClientName); var randomName = Guid.NewGuid().ToString(); - var result = options.Apply(options => - { - options.ClientName = randomName; - }); + var result = options.Apply(options => options.ClientName = randomName); Assert.Equal(randomName, options.ClientName); Assert.Equal(randomName, result.ClientName); diff --git a/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs b/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs index 6a9d2a399..014223a24 100644 --- a/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs +++ b/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs @@ -37,7 +37,7 @@ void innerScenario() } // Heartbeat should reconnect by now - await UntilCondition(TimeSpan.FromSeconds(10), () => server.IsConnected); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => server.IsConnected); Log("pinging - expect success"); var time = server.Ping(); diff --git a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs index 606745a08..8d3af063c 100644 --- a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs +++ b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs @@ -41,7 +41,7 @@ public async Task FastNoticesFailOnConnectingSyncCompletion() // should reconnect within 1 keepalive interval muxer.AllowConnect = true; Log("Waiting for reconnect"); - await UntilCondition(TimeSpan.FromSeconds(2), () => muxer.IsConnected).ForAwait(); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => muxer.IsConnected).ForAwait(); Assert.True(muxer.IsConnected); } @@ -81,7 +81,7 @@ public async Task FastNoticesFailOnConnectingAsyncCompletion() // should reconnect within 1 keepalive interval muxer.AllowConnect = true; Log("Waiting for reconnect"); - await UntilCondition(TimeSpan.FromSeconds(2), () => muxer.IsConnected).ForAwait(); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => muxer.IsConnected).ForAwait(); Assert.True(muxer.IsConnected); } @@ -126,7 +126,7 @@ public async Task Issue922_ReconnectRaised() var server = muxer.GetServer(TestConfig.Current.MasterServerAndPort); server.SimulateConnectionFailure(SimulatedFailureType.All); - await UntilCondition(TimeSpan.FromSeconds(10), () => Volatile.Read(ref failCount) >= 2 && Volatile.Read(ref restoreCount) >= 2); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Volatile.Read(ref failCount) >= 2 && Volatile.Read(ref restoreCount) >= 2); // interactive+subscriber = 2 var failCountSnapshot = Volatile.Read(ref failCount); diff --git a/tests/StackExchange.Redis.Tests/Constraints.cs b/tests/StackExchange.Redis.Tests/Constraints.cs index de1388d6d..5727127e6 100644 --- a/tests/StackExchange.Redis.Tests/Constraints.cs +++ b/tests/StackExchange.Redis.Tests/Constraints.cs @@ -34,7 +34,7 @@ public async Task TestManualIncr() } } - public async Task ManualIncrAsync(IDatabase connection, RedisKey key) + public static async Task ManualIncrAsync(IDatabase connection, RedisKey key) { var oldVal = (long?)await connection.StringGetAsync(key).ForAwait(); var newVal = (oldVal ?? 0) + 1; diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index eac2aa3f5..6c08a3602 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -27,7 +27,7 @@ public DatabaseWrapperTests() [Fact] public void CreateBatch() { - object asyncState = new object(); + object asyncState = new(); IBatch innerBatch = new Mock().Object; mock.Setup(_ => _.CreateBatch(asyncState)).Returns(innerBatch); IBatch wrappedBatch = wrapper.CreateBatch(asyncState); @@ -39,7 +39,7 @@ public void CreateBatch() [Fact] public void CreateTransaction() { - object asyncState = new object(); + object asyncState = new(); ITransaction innerTransaction = new Mock().Object; mock.Setup(_ => _.CreateTransaction(asyncState)).Returns(innerTransaction); ITransaction wrappedTransaction = wrapper.CreateTransaction(asyncState); @@ -86,7 +86,7 @@ public void HashDelete_1() [Fact] public void HashDelete_2() { - RedisValue[] hashFields = new RedisValue[0]; + RedisValue[] hashFields = Array.Empty(); wrapper.HashDelete("key", hashFields, CommandFlags.None); mock.Verify(_ => _.HashDelete("prefix:key", hashFields, CommandFlags.None)); } @@ -108,7 +108,7 @@ public void HashGet_1() [Fact] public void HashGet_2() { - RedisValue[] hashFields = new RedisValue[0]; + RedisValue[] hashFields = Array.Empty(); wrapper.HashGet("key", hashFields, CommandFlags.None); mock.Verify(_ => _.HashGet("prefix:key", hashFields, CommandFlags.None)); } @@ -165,7 +165,7 @@ public void HashScan_Full() [Fact] public void HashSet_1() { - HashEntry[] hashFields = new HashEntry[0]; + HashEntry[] hashFields = Array.Empty(); wrapper.HashSet("key", hashFields, CommandFlags.None); mock.Verify(_ => _.HashSet("prefix:key", hashFields, CommandFlags.None)); } @@ -201,7 +201,7 @@ public void HyperLogLogAdd_1() [Fact] public void HyperLogLogAdd_2() { - RedisValue[] values = new RedisValue[0]; + RedisValue[] values = Array.Empty(); wrapper.HyperLogLogAdd("key", values, CommandFlags.None); mock.Verify(_ => _.HyperLogLogAdd("prefix:key", values, CommandFlags.None)); } @@ -320,7 +320,7 @@ public void KeyRename() [Fact] public void KeyRestore() { - byte[] value = new byte[0]; + byte[] value = Array.Empty(); TimeSpan expiry = TimeSpan.FromSeconds(123); wrapper.KeyRestore("key", value, expiry, CommandFlags.None); mock.Verify(_ => _.KeyRestore("prefix:key", value, expiry, CommandFlags.None)); @@ -385,7 +385,7 @@ public void ListLeftPush_1() [Fact] public void ListLeftPush_2() { - RedisValue[] values = new RedisValue[0]; + RedisValue[] values = Array.Empty(); wrapper.ListLeftPush("key", values, CommandFlags.None); mock.Verify(_ => _.ListLeftPush("prefix:key", values, CommandFlags.None)); } @@ -450,7 +450,7 @@ public void ListRightPush_1() [Fact] public void ListRightPush_2() { - RedisValue[] values = new RedisValue[0]; + RedisValue[] values = Array.Empty(); wrapper.ListRightPush("key", values, CommandFlags.None); mock.Verify(_ => _.ListRightPush("prefix:key", values, CommandFlags.None)); } @@ -517,8 +517,8 @@ public void Publish() [Fact] public void ScriptEvaluate_1() { - byte[] hash = new byte[0]; - RedisValue[] values = new RedisValue[0]; + byte[] hash = Array.Empty(); + RedisValue[] values = Array.Empty(); RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; wrapper.ScriptEvaluate(hash, keys, values, CommandFlags.None); @@ -528,7 +528,7 @@ public void ScriptEvaluate_1() [Fact] public void ScriptEvaluate_2() { - RedisValue[] values = new RedisValue[0]; + RedisValue[] values = Array.Empty(); RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; wrapper.ScriptEvaluate("script", keys, values, CommandFlags.None); @@ -545,7 +545,7 @@ public void SetAdd_1() [Fact] public void SetAdd_2() { - RedisValue[] values = new RedisValue[0]; + RedisValue[] values = Array.Empty(); wrapper.SetAdd("key", values, CommandFlags.None); mock.Verify(_ => _.SetAdd("prefix:key", values, CommandFlags.None)); } @@ -651,7 +651,7 @@ public void SetRemove_1() [Fact] public void SetRemove_2() { - RedisValue[] values = new RedisValue[0]; + RedisValue[] values = Array.Empty(); wrapper.SetRemove("key", values, CommandFlags.None); mock.Verify(_ => _.SetRemove("prefix:key", values, CommandFlags.None)); } @@ -706,7 +706,7 @@ public void SortedSetAdd_1() [Fact] public void SortedSetAdd_2() { - SortedSetEntry[] values = new SortedSetEntry[0]; + SortedSetEntry[] values = Array.Empty(); wrapper.SortedSetAdd("key", values, When.Exists, CommandFlags.None); mock.Verify(_ => _.SortedSetAdd("prefix:key", values, When.Exists, CommandFlags.None)); } @@ -814,7 +814,7 @@ public void SortedSetRemove_1() [Fact] public void SortedSetRemove_2() { - RedisValue[] members = new RedisValue[0]; + RedisValue[] members = Array.Empty(); wrapper.SortedSetRemove("key", members, CommandFlags.None); mock.Verify(_ => _.SortedSetRemove("prefix:key", members, CommandFlags.None)); } @@ -886,7 +886,7 @@ public void StreamAdd_1() [Fact] public void StreamAdd_2() { - var fields = new NameValueEntry[0]; + var fields = Array.Empty(); wrapper.StreamAdd("key", fields, "*", 1000, true, CommandFlags.None); mock.Verify(_ => _.StreamAdd("prefix:key", fields, "*", 1000, true, CommandFlags.None)); } @@ -894,7 +894,7 @@ public void StreamAdd_2() [Fact] public void StreamClaimMessages() { - var messageIds = new RedisValue[0]; + var messageIds = Array.Empty(); wrapper.StreamClaim("key", "group", "consumer", 1000, messageIds, CommandFlags.None); mock.Verify(_ => _.StreamClaim("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); } @@ -902,7 +902,7 @@ public void StreamClaimMessages() [Fact] public void StreamClaimMessagesReturningIds() { - var messageIds = new RedisValue[0]; + var messageIds = Array.Empty(); wrapper.StreamClaimIdsOnly("key", "group", "consumer", 1000, messageIds, CommandFlags.None); mock.Verify(_ => _.StreamClaimIdsOnly("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); } @@ -952,7 +952,7 @@ public void StreamLength() [Fact] public void StreamMessagesDelete() { - var messageIds = new RedisValue[] { }; + var messageIds = Array.Empty(); wrapper.StreamDelete("key", messageIds, CommandFlags.None); mock.Verify(_ => _.StreamDelete("prefix:key", messageIds, CommandFlags.None)); } @@ -995,7 +995,7 @@ public void StreamRange() [Fact] public void StreamRead_1() { - var streamPositions = new StreamPosition[] { }; + var streamPositions = Array.Empty(); wrapper.StreamRead(streamPositions, null, CommandFlags.None); mock.Verify(_ => _.StreamRead(streamPositions, null, CommandFlags.None)); } @@ -1017,7 +1017,7 @@ public void StreamStreamReadGroup_1() [Fact] public void StreamStreamReadGroup_2() { - var streamPositions = new StreamPosition[] { }; + var streamPositions = Array.Empty(); wrapper.StreamReadGroup(streamPositions, "group", "consumer", 10, false, CommandFlags.None); mock.Verify(_ => _.StreamReadGroup(streamPositions, "group", "consumer", 10, false, CommandFlags.None)); } diff --git a/tests/StackExchange.Redis.Tests/EventArgsTests.cs b/tests/StackExchange.Redis.Tests/EventArgsTests.cs index 5d7a18a43..2e716db82 100644 --- a/tests/StackExchange.Redis.Tests/EventArgsTests.cs +++ b/tests/StackExchange.Redis.Tests/EventArgsTests.cs @@ -79,46 +79,25 @@ public static DiagnosticStub Create() DiagnosticStub stub = new DiagnosticStub(); stub.ConfigurationChangedBroadcastHandler - = (obj, args) => - { - stub.Message = ConfigurationChangedBroadcastHandlerMessage; - }; + = (obj, args) => stub.Message = ConfigurationChangedBroadcastHandlerMessage; stub.ErrorMessageHandler - = (obj, args) => - { - stub.Message = ErrorMessageHandlerMessage; - }; + = (obj, args) => stub.Message = ErrorMessageHandlerMessage; stub.ConnectionFailedHandler - = (obj, args) => - { - stub.Message = ConnectionFailedHandlerMessage; - }; + = (obj, args) => stub.Message = ConnectionFailedHandlerMessage; stub.InternalErrorHandler - = (obj, args) => - { - stub.Message = InternalErrorHandlerMessage; - }; + = (obj, args) => stub.Message = InternalErrorHandlerMessage; stub.ConnectionRestoredHandler - = (obj, args) => - { - stub.Message = ConnectionRestoredHandlerMessage; - }; + = (obj, args) => stub.Message = ConnectionRestoredHandlerMessage; stub.ConfigurationChangedHandler - = (obj, args) => - { - stub.Message = ConfigurationChangedHandlerMessage; - }; + = (obj, args) => stub.Message = ConfigurationChangedHandlerMessage; stub.HashSlotMovedHandler - = (obj, args) => - { - stub.Message = HashSlotMovedHandlerMessage; - }; + = (obj, args) => stub.Message = HashSlotMovedHandlerMessage; return stub; } diff --git a/tests/StackExchange.Redis.Tests/Failover.cs b/tests/StackExchange.Redis.Tests/Failover.cs index 50565580f..39d1699f9 100644 --- a/tests/StackExchange.Redis.Tests/Failover.cs +++ b/tests/StackExchange.Redis.Tests/Failover.cs @@ -189,7 +189,7 @@ public async Task DereplicateGoesToPrimary() Assert.Equal(secondary2.EndPoint, db2.IdentifyEndpoint(key, CommandFlags.DemandReplica)); } - await UntilCondition(TimeSpan.FromSeconds(20), () => !primary.IsReplica && secondary.IsReplica); + await UntilConditionAsync(TimeSpan.FromSeconds(20), () => !primary.IsReplica && secondary.IsReplica); Assert.False(primary.IsReplica, $"{primary.EndPoint} should be a primary."); Assert.True(secondary.IsReplica, $"{secondary.EndPoint} should be a replica."); @@ -259,7 +259,7 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() Log(" SubA ping: " + subA.Ping()); Log(" SubB ping: " + subB.Ping()); // If redis is under load due to this suite, it may take a moment to send across. - await UntilCondition(TimeSpan.FromSeconds(5), () => Interlocked.Read(ref aCount) == 2 && Interlocked.Read(ref bCount) == 2).ForAwait(); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => Interlocked.Read(ref aCount) == 2 && Interlocked.Read(ref bCount) == 2).ForAwait(); Assert.Equal(2, Interlocked.Read(ref aCount)); Assert.Equal(2, Interlocked.Read(ref bCount)); @@ -277,7 +277,7 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() Log(sw.ToString()); } Log("Waiting for connection B to detect..."); - await UntilCondition(TimeSpan.FromSeconds(10), () => b.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica).ForAwait(); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => b.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica).ForAwait(); subA.Ping(); subB.Ping(); Log("Failover 2 Attempted. Pausing..."); @@ -294,7 +294,7 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() Assert.True(a.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverMasterServerAndPort} should be a replica"); Assert.False(a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a master"); - await UntilCondition(TimeSpan.FromSeconds(10), () => b.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica).ForAwait(); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => b.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica).ForAwait(); var sanityCheck = b.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica; if (!sanityCheck) { @@ -329,7 +329,7 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() subA.Ping(); subB.Ping(); Log("Ping Complete. Checking..."); - await UntilCondition(TimeSpan.FromSeconds(10), () => Interlocked.Read(ref aCount) == 2 && Interlocked.Read(ref bCount) == 2).ForAwait(); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Interlocked.Read(ref aCount) == 2 && Interlocked.Read(ref bCount) == 2).ForAwait(); Log("Counts so far:"); Log(" aCount: " + Interlocked.Read(ref aCount)); diff --git a/tests/StackExchange.Redis.Tests/GeoTests.cs b/tests/StackExchange.Redis.Tests/GeoTests.cs index 476604da2..92a90f926 100644 --- a/tests/StackExchange.Redis.Tests/GeoTests.cs +++ b/tests/StackExchange.Redis.Tests/GeoTests.cs @@ -10,12 +10,12 @@ public class GeoTests : TestBase { public GeoTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } - public static GeoEntry + private readonly static GeoEntry palermo = new GeoEntry(13.361389, 38.115556, "Palermo"), catania = new GeoEntry(15.087269, 37.502669, "Catania"), agrigento = new GeoEntry(13.5765, 37.311, "Agrigento"), cefalù = new GeoEntry(14.0188, 38.0084, "Cefalù"); - public static GeoEntry[] all = { palermo, catania, agrigento, cefalù }; + private readonly static GeoEntry[] all = { palermo, catania, agrigento, cefalù }; [Fact] public void GeoAdd() diff --git a/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs b/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs index 5eb06cc54..1a748d444 100644 --- a/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs +++ b/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs @@ -4,9 +4,19 @@ // Project-level suppressions either have no target or are given // a specific target and scoped to a namespace, type, member, etc. -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionFailedErrors.SSLCertificateValidationError(System.Boolean)")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.PubSub.ExplicitPublishMode")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSL.ConnectToSSLServer(System.Boolean,System.Boolean)")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSL.ShowCertFailures(StackExchange.Redis.Tests.Helpers.TextWriterOutputHelper)~System.Net.Security.RemoteCertificateValidationCallback")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionShutdown.ShutdownRaisesConnectionFailedAndRestore")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Issues.BgSaveResponse.ShouldntThrowException(StackExchange.Redis.SaveType)")] +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionFailedErrors.SSLCertificateValidationError(System.Boolean)")] +[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.PubSub.ExplicitPublishMode")] +[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSL.ConnectToSSLServer(System.Boolean,System.Boolean)")] +[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSL.ShowCertFailures(StackExchange.Redis.Tests.Helpers.TextWriterOutputHelper)~System.Net.Security.RemoteCertificateValidationCallback")] +[assembly: SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionShutdown.ShutdownRaisesConnectionFailedAndRestore")] +[assembly: SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Issues.BgSaveResponse.ShouldntThrowException(StackExchange.Redis.SaveType)")] +[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Sentinel.MasterConnectTest~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Sentinel.MasterConnectAsyncTest~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelBase.WaitForReplicationAsync(StackExchange.Redis.IServer,System.Nullable{System.TimeSpan})~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelFailover.ManagedMasterConnectionEndToEndWithFailoverTest~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Performance", "CA1846:Prefer 'AsSpan' over 'Substring'", Justification = "", Scope = "member", Target = "~M:RedisSharp.Redis.ReadData~System.Byte[]")] +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Naming.IgnoreMethodConventions(System.Reflection.MethodInfo)~System.Boolean")] +[assembly: SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelBase.WaitForReadyAsync(System.Net.EndPoint,System.Boolean,System.Nullable{System.TimeSpan})~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelBase.WaitForRoleAsync(StackExchange.Redis.IServer,System.String,System.Nullable{System.TimeSpan})~System.Threading.Tasks.Task")] diff --git a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs index 33d8d3631..8d9c09796 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs @@ -150,7 +150,11 @@ public class SkippableMessageBus : IMessageBus public int DynamicallySkippedTestCount { get; private set; } - public void Dispose() { } + public void Dispose() + { + InnerBus.Dispose(); + GC.SuppressFinalize(this); + } public bool QueueMessage(IMessageSinkMessage message) { diff --git a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs index 39a9af890..8d6c97b79 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs @@ -289,7 +289,7 @@ private bool SendCommand(string cmd, params object[] args) } [Conditional("DEBUG")] - private void Log(string fmt, params object[] args) + private static void Log(string fmt, params object[] args) { Console.WriteLine("{0}", string.Format(fmt, args).Trim()); } @@ -403,7 +403,7 @@ private byte[] ReadData() string r = ReadLine(); Log("R: {0}", r); if (r.Length == 0) - throw new ResponseException("Zero length respose"); + throw new ResponseException("Zero length response"); char c = r[0]; if (c == '-') @@ -438,7 +438,7 @@ private byte[] ReadData() if (c == '*') { if (int.TryParse(r.Substring(1), out int n)) - return n <= 0 ? new byte[0] : ReadData(); + return n <= 0 ? Array.Empty() : ReadData(); throw new ResponseException("Unexpected length parameter" + r); } @@ -610,7 +610,7 @@ public string[] Keys { string commandResponse = Encoding.UTF8.GetString(SendExpectData(null, "KEYS *\r\n")); if (commandResponse.Length < 1) - return new string[0]; + return Array.Empty(); else return commandResponse.Split(' '); } @@ -622,7 +622,7 @@ public string[] GetKeys(string pattern) throw new ArgumentNullException(nameof(pattern)); var keys = SendExpectData(null, "KEYS {0}\r\n", pattern); if (keys.Length == 0) - return new string[0]; + return Array.Empty(); return Encoding.UTF8.GetString(keys).Split(' '); } @@ -631,7 +631,7 @@ public byte[][] GetKeys(params string[] keys) if (keys == null) throw new ArgumentNullException(nameof(keys)); if (keys.Length == 0) - throw new ArgumentException("keys"); + throw new ArgumentOutOfRangeException(nameof(keys)); return SendDataCommandExpectMultiBulkReply(null, "MGET {0}\r\n", string.Join(" ", keys)); } @@ -746,7 +746,7 @@ public bool RemoveFromSet(string key, string member) public byte[][] GetUnionOfSets(params string[] keys) { if (keys == null) - throw new ArgumentNullException(); + throw new ArgumentNullException(nameof(keys)); return SendDataCommandExpectMultiBulkReply(null, "SUNION " + string.Join(" ", keys) + "\r\n"); } @@ -773,7 +773,7 @@ public void StoreUnionOfSets(string destKey, params string[] keys) public byte[][] GetIntersectionOfSets(params string[] keys) { if (keys == null) - throw new ArgumentNullException(); + throw new ArgumentNullException(nameof(keys)); return SendDataCommandExpectMultiBulkReply(null, "SINTER " + string.Join(" ", keys) + "\r\n"); } @@ -786,7 +786,7 @@ public void StoreIntersectionOfSets(string destKey, params string[] keys) public byte[][] GetDifferenceOfSets(params string[] keys) { if (keys == null) - throw new ArgumentNullException(); + throw new ArgumentNullException(nameof(keys)); return SendDataCommandExpectMultiBulkReply(null, "SDIFF " + string.Join(" ", keys) + "\r\n"); } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs b/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs index c81a6e004..d92be44f3 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs @@ -15,10 +15,13 @@ public Issue1101(ITestOutputHelper output) : base(output) { } private static void AssertCounts(ISubscriber pubsub, in RedisChannel channel, bool has, int handlers, int queues) { - var aHas = (pubsub.Multiplexer as ConnectionMultiplexer).GetSubscriberCounts(channel, out var ah, out var aq); - Assert.Equal(has, aHas); - Assert.Equal(handlers, ah); - Assert.Equal(queues, aq); + if (pubsub.Multiplexer is ConnectionMultiplexer muxer) + { + var aHas = muxer.GetSubscriberCounts(channel, out var ah, out var aq); + Assert.Equal(has, aHas); + Assert.Equal(handlers, ah); + Assert.Equal(queues, aq); + } } [Fact] public async Task ExecuteWithUnsubscribeViaChannel() @@ -43,7 +46,7 @@ public async Task ExecuteWithUnsubscribeViaChannel() second.OnMessage(_ => Interlocked.Increment(ref i)); await Task.Delay(200); await pubsub.PublishAsync(name, "abc"); - await UntilCondition(TimeSpan.FromSeconds(10), () => values.Count == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); lock (values) { Assert.Equal("abc", Assert.Single(values)); @@ -56,7 +59,7 @@ public async Task ExecuteWithUnsubscribeViaChannel() await first.UnsubscribeAsync(); await Task.Delay(200); await pubsub.PublishAsync(name, "def"); - await UntilCondition(TimeSpan.FromSeconds(10), () => values.Count == 1 && Volatile.Read(ref i) == 2); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1 && Volatile.Read(ref i) == 2); lock (values) { Assert.Equal("abc", Assert.Single(values)); @@ -69,7 +72,7 @@ public async Task ExecuteWithUnsubscribeViaChannel() await second.UnsubscribeAsync(); await Task.Delay(200); await pubsub.PublishAsync(name, "ghi"); - await UntilCondition(TimeSpan.FromSeconds(10), () => values.Count == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); lock (values) { Assert.Equal("abc", Assert.Single(values)); @@ -110,7 +113,7 @@ public async Task ExecuteWithUnsubscribeViaSubscriber() await Task.Delay(100); await pubsub.PublishAsync(name, "abc"); - await UntilCondition(TimeSpan.FromSeconds(10), () => values.Count == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); lock (values) { Assert.Equal("abc", Assert.Single(values)); @@ -123,7 +126,7 @@ public async Task ExecuteWithUnsubscribeViaSubscriber() await pubsub.UnsubscribeAsync(name); await Task.Delay(100); await pubsub.PublishAsync(name, "def"); - await UntilCondition(TimeSpan.FromSeconds(10), () => values.Count == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); lock (values) { Assert.Equal("abc", Assert.Single(values)); @@ -161,7 +164,7 @@ public async Task ExecuteWithUnsubscribeViaClearAll() second.OnMessage(_ => Interlocked.Increment(ref i)); await Task.Delay(100); await pubsub.PublishAsync(name, "abc"); - await UntilCondition(TimeSpan.FromSeconds(10), () => values.Count == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); lock (values) { Assert.Equal("abc", Assert.Single(values)); @@ -174,7 +177,7 @@ public async Task ExecuteWithUnsubscribeViaClearAll() await pubsub.UnsubscribeAllAsync(); await Task.Delay(100); await pubsub.PublishAsync(name, "def"); - await UntilCondition(TimeSpan.FromSeconds(10), () => values.Count == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); lock (values) { Assert.Equal("abc", Assert.Single(values)); diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue25.cs b/tests/StackExchange.Redis.Tests/Issues/Issue25.cs index e4a2ac218..6ea9dc283 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue25.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue25.cs @@ -29,9 +29,7 @@ public void UnkonwnKeywordHandling_Ignore() [Fact] public void UnkonwnKeywordHandling_ExplicitFail() { - var ex = Assert.Throws(() => { - ConfigurationOptions.Parse("ssl2=true", false); - }); + var ex = Assert.Throws(() => ConfigurationOptions.Parse("ssl2=true", false)); Assert.StartsWith("Keyword 'ssl2' is not supported", ex.Message); Assert.Equal("ssl2", ex.ParamName); } @@ -39,9 +37,7 @@ public void UnkonwnKeywordHandling_ExplicitFail() [Fact] public void UnkonwnKeywordHandling_ImplicitFail() { - var ex = Assert.Throws(() => { - ConfigurationOptions.Parse("ssl2=true"); - }); + var ex = Assert.Throws(() => ConfigurationOptions.Parse("ssl2=true")); Assert.StartsWith("Keyword 'ssl2' is not supported", ex.Message); Assert.Equal("ssl2", ex.ParamName); } diff --git a/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs b/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs index 66b16ce7d..130b340cd 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs @@ -31,7 +31,7 @@ public async Task Exec() Assert.Equal("some value", fullWait.Result); // wait for expiry - await UntilCondition(TimeSpan.FromSeconds(10), () => !cache.KeyExists(key)).ForAwait(); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => !cache.KeyExists(key)).ForAwait(); // test once expired keyExists = cache.KeyExists(key); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO25567566.cs b/tests/StackExchange.Redis.Tests/Issues/SO25567566.cs index b656849f6..2a322d943 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO25567566.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO25567566.cs @@ -22,7 +22,7 @@ public async Task Execute() } } - private async Task DoStuff(ConnectionMultiplexer conn) + private static async Task DoStuff(ConnectionMultiplexer conn) { var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/KeysAndValues.cs b/tests/StackExchange.Redis.Tests/KeysAndValues.cs index b96b2a90f..93f4b99b8 100644 --- a/tests/StackExchange.Redis.Tests/KeysAndValues.cs +++ b/tests/StackExchange.Redis.Tests/KeysAndValues.cs @@ -22,7 +22,7 @@ public void TestValues() RedisValue emptyString = ""; CheckNotNull(emptyString); - RedisValue emptyBlob = new byte[0]; + RedisValue emptyBlob = Array.Empty(); CheckNotNull(emptyBlob); RedisValue a0 = new string('a', 1); @@ -77,7 +77,7 @@ internal static void CheckSame(RedisValue x, RedisValue y) Assert.True(x.GetHashCode() == y.GetHashCode(), "GetHashCode"); } - private void CheckNotSame(RedisValue x, RedisValue y) + private static void CheckNotSame(RedisValue x, RedisValue y) { Assert.False(Equals(x, y)); Assert.False(Equals(y, x)); @@ -92,7 +92,7 @@ private void CheckNotSame(RedisValue x, RedisValue y) Assert.False(x.GetHashCode() == y.GetHashCode()); // well, very unlikely } - private void CheckNotNull(RedisValue value) + private static void CheckNotNull(RedisValue value) { Assert.False(value.IsNull); Assert.NotNull((byte[])value); diff --git a/tests/StackExchange.Redis.Tests/Lex.cs b/tests/StackExchange.Redis.Tests/Lex.cs index e11657be3..0e56b7dea 100644 --- a/tests/StackExchange.Redis.Tests/Lex.cs +++ b/tests/StackExchange.Redis.Tests/Lex.cs @@ -95,7 +95,7 @@ public void RemoveRangeByLex() } } - private void Equate(RedisValue[] actual, long count, params string[] expected) + private static void Equate(RedisValue[] actual, long count, params string[] expected) { Assert.Equal(expected.Length, count); Assert.Equal(expected.Length, actual.Length); diff --git a/tests/StackExchange.Redis.Tests/Lists.cs b/tests/StackExchange.Redis.Tests/Lists.cs index 0c1fa332a..8fa212c43 100644 --- a/tests/StackExchange.Redis.Tests/Lists.cs +++ b/tests/StackExchange.Redis.Tests/Lists.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -41,7 +42,7 @@ public void ListLeftPushEmptyValues() var db = conn.GetDatabase(); RedisKey key = "testlist"; db.KeyDelete(key, CommandFlags.FireAndForget); - var result = db.ListLeftPush(key, new RedisValue[0], When.Always, CommandFlags.None); + var result = db.ListLeftPush(key, Array.Empty(), When.Always, CommandFlags.None); Assert.Equal(0, result); } } @@ -111,7 +112,7 @@ public async Task ListLeftPushAsyncEmptyValues() var db = conn.GetDatabase(); RedisKey key = "testlist"; db.KeyDelete(key, CommandFlags.FireAndForget); - var result = await db.ListLeftPushAsync(key, new RedisValue[0], When.Always, CommandFlags.None); + var result = await db.ListLeftPushAsync(key, Array.Empty(), When.Always, CommandFlags.None); Assert.Equal(0, result); } } @@ -181,7 +182,7 @@ public void ListRightPushEmptyValues() var db = conn.GetDatabase(); RedisKey key = "testlist"; db.KeyDelete(key, CommandFlags.FireAndForget); - var result = db.ListRightPush(key, new RedisValue[0], When.Always, CommandFlags.None); + var result = db.ListRightPush(key, Array.Empty(), When.Always, CommandFlags.None); Assert.Equal(0, result); } } @@ -251,7 +252,7 @@ public async Task ListRightPushAsyncEmptyValues() var db = conn.GetDatabase(); RedisKey key = "testlist"; db.KeyDelete(key, CommandFlags.FireAndForget); - var result = await db.ListRightPushAsync(key, new RedisValue[0], When.Always, CommandFlags.None); + var result = await db.ListRightPushAsync(key, Array.Empty(), When.Always, CommandFlags.None); Assert.Equal(0, result); } } diff --git a/tests/StackExchange.Redis.Tests/Locking.cs b/tests/StackExchange.Redis.Tests/Locking.cs index 760b1b65f..e931af60b 100644 --- a/tests/StackExchange.Redis.Tests/Locking.cs +++ b/tests/StackExchange.Redis.Tests/Locking.cs @@ -76,7 +76,7 @@ public void TestOpCountByVersionLocal_UpLevel() } } - private void TestLockOpCountByVersion(IConnectionMultiplexer conn, int expectedOps, bool existFirst) + private static void TestLockOpCountByVersion(IConnectionMultiplexer conn, int expectedOps, bool existFirst) { const int LockDuration = 30; RedisKey Key = Me(); diff --git a/tests/StackExchange.Redis.Tests/Migrate.cs b/tests/StackExchange.Redis.Tests/Migrate.cs index 73711129d..ca148e8e3 100644 --- a/tests/StackExchange.Redis.Tests/Migrate.cs +++ b/tests/StackExchange.Redis.Tests/Migrate.cs @@ -36,7 +36,7 @@ public async Task Basic() // we keep seeing it fail on the CI server where the key has *left* the origin, but // has *not* yet arrived at the destination; adding a pause while we investigate with // the redis folks - await UntilCondition(TimeSpan.FromSeconds(15), () => !fromDb.KeyExists(key) && toDb.KeyExists(key)); + await UntilConditionAsync(TimeSpan.FromSeconds(15), () => !fromDb.KeyExists(key) && toDb.KeyExists(key)); Assert.False(fromDb.KeyExists(key), "Exists at source"); Assert.True(toDb.KeyExists(key), "Exists at destination"); @@ -45,7 +45,7 @@ public async Task Basic() } } - private async Task IsWindows(ConnectionMultiplexer conn) + private static async Task IsWindows(ConnectionMultiplexer conn) { var server = conn.GetServer(conn.GetEndPoints().First()); var section = (await server.InfoAsync("server")).Single(); diff --git a/tests/StackExchange.Redis.Tests/Naming.cs b/tests/StackExchange.Redis.Tests/Naming.cs index ca9071f09..012359462 100644 --- a/tests/StackExchange.Redis.Tests/Naming.cs +++ b/tests/StackExchange.Redis.Tests/Naming.cs @@ -237,7 +237,7 @@ static bool IsAsyncMethod(Type returnType) } } - private void CheckName(MemberInfo member, bool isAsync) + private static void CheckName(MemberInfo member, bool isAsync) { if (isAsync) Assert.True(member.Name.EndsWith("Async"), member.Name + ":Name - end *Async"); else Assert.False(member.Name.EndsWith("Async"), member.Name + ":Name - don't end *Async"); diff --git a/tests/StackExchange.Redis.Tests/PubSub.cs b/tests/StackExchange.Redis.Tests/PubSub.cs index 2dce49c5b..13e13874f 100644 --- a/tests/StackExchange.Redis.Tests/PubSub.cs +++ b/tests/StackExchange.Redis.Tests/PubSub.cs @@ -30,7 +30,7 @@ public async Task ExplicitPublishMode() pub.Subscribe("abc*", (x, y) => Interlocked.Increment(ref d)); pub.Publish("abcd", "efg"); - await UntilCondition(TimeSpan.FromSeconds(10), + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Thread.VolatileRead(ref b) == 1 && Thread.VolatileRead(ref c) == 1 && Thread.VolatileRead(ref d) == 1); @@ -40,7 +40,7 @@ await UntilCondition(TimeSpan.FromSeconds(10), Assert.Equal(1, Thread.VolatileRead(ref d)); pub.Publish("*bcd", "efg"); - await UntilCondition(TimeSpan.FromSeconds(10), () => Thread.VolatileRead(ref a) == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Thread.VolatileRead(ref a) == 1); Assert.Equal(1, Thread.VolatileRead(ref a)); } } @@ -90,13 +90,13 @@ public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string br await PingAsync(pub, sub, 3).ForAwait(); - await UntilCondition(TimeSpan.FromSeconds(5), () => received.Count == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => received.Count == 1); lock (received) { Assert.Single(received); } // Give handler firing a moment - await UntilCondition(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); // unsubscribe from first; should still see second @@ -108,7 +108,7 @@ public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string br Assert.Single(received); } - await UntilCondition(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 2); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 2); var secondHandlerCount = Thread.VolatileRead(ref secondHandler); Log("Expecting 2 from second handler, got: " + secondHandlerCount); @@ -166,7 +166,7 @@ public async Task TestBasicPubSubFireAndForget() var count = sub.Publish(key, "def", CommandFlags.FireAndForget); await PingAsync(pub, sub).ForAwait(); - await UntilCondition(TimeSpan.FromSeconds(5), () => received.Count == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => received.Count == 1); Log(profiler); lock (received) @@ -235,14 +235,14 @@ public async Task TestPatternPubSub() var count = sub.Publish("abc", "def"); await PingAsync(pub, sub).ForAwait(); - await UntilCondition(TimeSpan.FromSeconds(5), () => received.Count == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => received.Count == 1); lock (received) { Assert.Single(received); } // Give reception a bit, the handler could be delayed under load - await UntilCondition(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); sub.Unsubscribe("a*c"); @@ -746,7 +746,7 @@ public async Task AzureRedisEventsAutomaticSubscribe() using (var connection = await ConnectionMultiplexer.ConnectAsync(options)) { - connection.ServerMaintenanceEvent += (object sender, ServerMaintenanceEvent e) => + connection.ServerMaintenanceEvent += (object _, ServerMaintenanceEvent e) => { if (e is AzureMaintenanceEvent) { @@ -812,7 +812,7 @@ await sub.SubscribeAsync(channel, delegate muxer.AllowConnect = true; Log("Waiting on reconnect"); // Wait until we're reconnected - await UntilCondition(TimeSpan.FromSeconds(10), () => sub.IsConnected(channel)); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => sub.IsConnected(channel)); Log("Reconnected"); // Ensure we're reconnected Assert.True(sub.IsConnected(channel)); @@ -843,7 +843,7 @@ await sub.SubscribeAsync(channel, delegate // Give it a few seconds to get our messages Log("Waiting for 2 messages"); - await UntilCondition(TimeSpan.FromSeconds(5), () => Thread.VolatileRead(ref counter) == 2); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => Thread.VolatileRead(ref counter) == 2); var counter2 = Thread.VolatileRead(ref counter); Log($"Expecting 2 messages, got {counter2}"); diff --git a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs index e3f9590b3..4c3a41772 100644 --- a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs +++ b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs @@ -36,7 +36,7 @@ public async Task ClusterNodeSubscriptionFailover() var count = 0; Log("Subscribing..."); - await sub.SubscribeAsync(channel, (channel, val) => + await sub.SubscribeAsync(channel, (_, val) => { Interlocked.Increment(ref count); Log("Message: " + val); @@ -47,7 +47,7 @@ await sub.SubscribeAsync(channel, (channel, val) => Assert.Equal(0, count); var publishedTo = await sub.PublishAsync(channel, "message1"); // Client -> Redis -> Client -> handler takes just a moment - await UntilCondition(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); Assert.Equal(1, count); Log($" Published (1) to {publishedTo} subscriber(s)."); Assert.Equal(1, publishedTo); @@ -64,7 +64,7 @@ await sub.SubscribeAsync(channel, (channel, val) => var initialServer = subscription.GetCurrentServer(); Assert.NotNull(initialServer); Assert.True(initialServer.IsConnected); - Log($"Connected to: " + initialServer); + Log("Connected to: " + initialServer); muxer.AllowConnect = false; subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.AllSubscription); @@ -72,20 +72,20 @@ await sub.SubscribeAsync(channel, (channel, val) => Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); Assert.False(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); - await UntilCondition(TimeSpan.FromSeconds(5), () => subscription.IsConnected); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); Assert.True(subscription.IsConnected); var newServer = subscription.GetCurrentServer(); Assert.NotNull(newServer); Assert.NotEqual(newServer, initialServer); - Log($"Now connected to: " + newServer); + Log("Now connected to: " + newServer); count = 0; Log("Publishing (2)..."); Assert.Equal(0, count); publishedTo = await sub.PublishAsync(channel, "message2"); // Client -> Redis -> Client -> handler takes just a moment - await UntilCondition(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); Assert.Equal(1, count); Log($" Published (2) to {publishedTo} subscriber(s)."); @@ -107,7 +107,7 @@ public async Task PrimaryReplicaSubscriptionFailover(CommandFlags flags, bool ex var count = 0; Log("Subscribing..."); - await sub.SubscribeAsync(channel, (channel, val) => + await sub.SubscribeAsync(channel, (_, val) => { Interlocked.Increment(ref count); Log("Message: " + val); @@ -118,7 +118,7 @@ await sub.SubscribeAsync(channel, (channel, val) => Assert.Equal(0, count); var publishedTo = await sub.PublishAsync(channel, "message1"); // Client -> Redis -> Client -> handler takes just a moment - await UntilCondition(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); Assert.Equal(1, count); Log($" Published (1) to {publishedTo} subscriber(s)."); @@ -134,7 +134,7 @@ await sub.SubscribeAsync(channel, (channel, val) => var initialServer = subscription.GetCurrentServer(); Assert.NotNull(initialServer); Assert.True(initialServer.IsConnected); - Log($"Connected to: " + initialServer); + Log("Connected to: " + initialServer); muxer.AllowConnect = false; subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.AllSubscription); @@ -144,39 +144,38 @@ await sub.SubscribeAsync(channel, (channel, val) => if (expectSuccess) { - await UntilCondition(TimeSpan.FromSeconds(5), () => subscription.IsConnected); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); Assert.True(subscription.IsConnected); var newServer = subscription.GetCurrentServer(); Assert.NotNull(newServer); Assert.NotEqual(newServer, initialServer); - Log($"Now connected to: " + newServer); + Log("Now connected to: " + newServer); } else { // This subscription shouldn't be able to reconnect by flags (demanding an unavailable server) - await UntilCondition(TimeSpan.FromSeconds(2), () => subscription.IsConnected); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => subscription.IsConnected); Assert.False(subscription.IsConnected); Log("Unable to reconnect (as expected)"); // Allow connecting back to the original muxer.AllowConnect = true; - await UntilCondition(TimeSpan.FromSeconds(2), () => subscription.IsConnected); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => subscription.IsConnected); Assert.True(subscription.IsConnected); var newServer = subscription.GetCurrentServer(); Assert.NotNull(newServer); Assert.Equal(newServer, initialServer); - Log($"Now connected to: " + newServer); + Log("Now connected to: " + newServer); } - count = 0; Log("Publishing (2)..."); Assert.Equal(0, count); publishedTo = await sub.PublishAsync(channel, "message2"); // Client -> Redis -> Client -> handler takes just a moment - await UntilCondition(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); Assert.Equal(1, count); Log($" Published (2) to {publishedTo} subscriber(s)."); diff --git a/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs b/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs index 5d9da3b16..813f80c35 100644 --- a/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs +++ b/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs @@ -152,7 +152,7 @@ private static void CheckString(RedisValue value, string expected) private static byte[] Bytes(string s) => s == null ? null : Encoding.UTF8.GetBytes(s); - private string LineNumber([CallerLineNumber] int lineNumber = 0) => lineNumber.ToString(); + private static string LineNumber([CallerLineNumber] int lineNumber = 0) => lineNumber.ToString(); [Fact] public void RedisValueStartsWith() diff --git a/tests/StackExchange.Redis.Tests/SSL.cs b/tests/StackExchange.Redis.Tests/SSL.cs index e8b8e5a23..d68fef7f8 100644 --- a/tests/StackExchange.Redis.Tests/SSL.cs +++ b/tests/StackExchange.Redis.Tests/SSL.cs @@ -289,12 +289,8 @@ public void RedisLabsEnvironmentVariableClientCertificate(bool setEnv) } } } - catch (RedisConnectionException ex) + catch (RedisConnectionException ex) when (!setEnv && ex.FailureType == ConnectionFailureType.UnableToConnect) { - if (setEnv || ex.FailureType != ConnectionFailureType.UnableToConnect) - { - throw; - } } finally { diff --git a/tests/StackExchange.Redis.Tests/Scans.cs b/tests/StackExchange.Redis.Tests/Scans.cs index 6b6b1401f..a233722cf 100644 --- a/tests/StackExchange.Redis.Tests/Scans.cs +++ b/tests/StackExchange.Redis.Tests/Scans.cs @@ -352,7 +352,7 @@ public void HashScanThresholds() } } - private bool GotCursors(IConnectionMultiplexer conn, RedisKey key, int count) + private static bool GotCursors(IConnectionMultiplexer conn, RedisKey key, int count) { var db = conn.GetDatabase(); db.KeyDelete(key, CommandFlags.FireAndForget); diff --git a/tests/StackExchange.Redis.Tests/Sentinel.cs b/tests/StackExchange.Redis.Tests/Sentinel.cs index fbe4e05e9..e3f228dfe 100644 --- a/tests/StackExchange.Redis.Tests/Sentinel.cs +++ b/tests/StackExchange.Redis.Tests/Sentinel.cs @@ -43,7 +43,7 @@ public async Task MasterConnectTest() Assert.Equal(expected, value); // force read from replica, replication has some lag - await WaitForReplicationAsync(servers.First(), TimeSpan.FromSeconds(10)).ForAwait(); + await WaitForReplicationAsync(servers[0], TimeSpan.FromSeconds(10)).ForAwait(); value = db.StringGet(key, CommandFlags.DemandReplica); Assert.Equal(expected, value); } @@ -79,7 +79,7 @@ public async Task MasterConnectAsyncTest() Assert.Equal(expected, value); // force read from replica, replication has some lag - await WaitForReplicationAsync(servers.First(), TimeSpan.FromSeconds(10)).ForAwait(); + await WaitForReplicationAsync(servers[0], TimeSpan.FromSeconds(10)).ForAwait(); value = await db.StringGetAsync(key, CommandFlags.DemandReplica); Assert.Equal(expected, value); } @@ -362,7 +362,7 @@ public async Task SentinelMastersAsyncTest() public async Task SentinelReplicasTest() { // Give previous test run a moment to reset when multi-framework failover is in play. - await UntilCondition(TimeSpan.FromSeconds(5), () => SentinelServerA.SentinelReplicas(ServiceName).Length > 0); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => SentinelServerA.SentinelReplicas(ServiceName).Length > 0); var replicaConfigs = SentinelServerA.SentinelReplicas(ServiceName); Assert.True(replicaConfigs.Length > 0, "Has replicaConfigs"); @@ -382,7 +382,7 @@ public async Task SentinelReplicasTest() public async Task SentinelReplicasAsyncTest() { // Give previous test run a moment to reset when multi-framework failover is in play. - await UntilCondition(TimeSpan.FromSeconds(5), () => SentinelServerA.SentinelReplicas(ServiceName).Length > 0); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => SentinelServerA.SentinelReplicas(ServiceName).Length > 0); var replicaConfigs = await SentinelServerA.SentinelReplicasAsync(ServiceName).ForAwait(); Assert.True(replicaConfigs.Length > 0, "Has replicaConfigs"); @@ -426,7 +426,7 @@ public async Task ReadOnlyConnectionReplicasTest() var readonlyConn = await ConnectionMultiplexer.ConnectAsync(config); - await UntilCondition(TimeSpan.FromSeconds(2), () => readonlyConn.IsConnected); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => readonlyConn.IsConnected); Assert.True(readonlyConn.IsConnected); var db = readonlyConn.GetDatabase(); var s = db.StringGet("test"); diff --git a/tests/StackExchange.Redis.Tests/SentinelBase.cs b/tests/StackExchange.Redis.Tests/SentinelBase.cs index c46e9691f..99b4c9697 100644 --- a/tests/StackExchange.Redis.Tests/SentinelBase.cs +++ b/tests/StackExchange.Redis.Tests/SentinelBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; @@ -12,8 +11,8 @@ namespace StackExchange.Redis.Tests { public class SentinelBase : TestBase, IAsyncLifetime { - protected string ServiceName => TestConfig.Current.SentinelSeviceName; - protected ConfigurationOptions ServiceOptions => new ConfigurationOptions { ServiceName = ServiceName, AllowAdmin = true }; + protected static string ServiceName => TestConfig.Current.SentinelSeviceName; + protected static ConfigurationOptions ServiceOptions => new ConfigurationOptions { ServiceName = ServiceName, AllowAdmin = true }; protected ConnectionMultiplexer Conn { get; set; } protected IServer SentinelServerA { get; set; } @@ -148,13 +147,16 @@ protected async Task WaitForReplicationAsync(IServer master, TimeSpan? duration static void LogEndpoints(IServer master, Action log) { - var serverEndpoints = (master.Multiplexer as ConnectionMultiplexer).GetServerSnapshot(); - log("Endpoints:"); - foreach (var serverEndpoint in serverEndpoints) + if (master.Multiplexer is ConnectionMultiplexer muxer) { - log($" {serverEndpoint}:"); - var server = master.Multiplexer.GetServer(serverEndpoint.EndPoint); - log($" Server: (Connected={server.IsConnected}, Type={server.ServerType}, IsReplica={server.IsReplica}, Unselectable={serverEndpoint.GetUnselectableFlags()})"); + var serverEndpoints = muxer.GetServerSnapshot(); + log("Endpoints:"); + foreach (var serverEndpoint in serverEndpoints) + { + log($" {serverEndpoint}:"); + var server = master.Multiplexer.GetServer(serverEndpoint.EndPoint); + log($" Server: (Connected={server.IsConnected}, Type={server.ServerType}, IsReplica={server.IsReplica}, Unselectable={serverEndpoint.GetUnselectableFlags()})"); + } } } diff --git a/tests/StackExchange.Redis.Tests/SentinelFailover.cs b/tests/StackExchange.Redis.Tests/SentinelFailover.cs index 0f1bb72cc..7be012c5d 100644 --- a/tests/StackExchange.Redis.Tests/SentinelFailover.cs +++ b/tests/StackExchange.Redis.Tests/SentinelFailover.cs @@ -17,13 +17,9 @@ public async Task ManagedMasterConnectionEndToEndWithFailoverTest() { var connectionString = $"{TestConfig.Current.SentinelServer}:{TestConfig.Current.SentinelPortA},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; var conn = await ConnectionMultiplexer.ConnectAsync(connectionString); - conn.ConfigurationChanged += (s, e) => { - Log($"Configuration changed: {e.EndPoint}"); - }; + conn.ConfigurationChanged += (s, e) => Log($"Configuration changed: {e.EndPoint}"); var sub = conn.GetSubscriber(); - sub.Subscribe("*", (channel, message) => { - Log($"Sub: {channel}, message:{message}"); - }); + sub.Subscribe("*", (channel, message) => Log($"Sub: {channel}, message:{message}")); var db = conn.GetDatabase(); await db.PingAsync(); @@ -52,7 +48,7 @@ public async Task ManagedMasterConnectionEndToEndWithFailoverTest() Log("Waiting for first replication check..."); // force read from replica, replication has some lag - await WaitForReplicationAsync(servers.First()).ForAwait(); + await WaitForReplicationAsync(servers[0]).ForAwait(); value = await db.StringGetAsync(key, CommandFlags.DemandReplica); Assert.Equal(expected, value); diff --git a/tests/StackExchange.Redis.Tests/Sets.cs b/tests/StackExchange.Redis.Tests/Sets.cs index c17d22598..bcf43fede 100644 --- a/tests/StackExchange.Redis.Tests/Sets.cs +++ b/tests/StackExchange.Redis.Tests/Sets.cs @@ -25,7 +25,7 @@ public void SScan() { db.SetAdd(key, i, CommandFlags.FireAndForget); totalUnfiltered += i; - if (i.ToString().Contains("3")) totalFiltered += i; + if (i.ToString().Contains('3')) totalFiltered += i; } var unfilteredActual = db.SetScan(key).Select(x => (int)x).Sum(); @@ -50,7 +50,7 @@ public async Task SetRemoveArgTests() Assert.Throws(() => db.SetRemove(key, values)); await Assert.ThrowsAsync(async () => await db.SetRemoveAsync(key, values).ForAwait()).ForAwait(); - values = new RedisValue[0]; + values = Array.Empty(); Assert.Equal(0, db.SetRemove(key, values)); Assert.Equal(0, await db.SetRemoveAsync(key, values).ForAwait()); } @@ -200,7 +200,7 @@ public void SetAdd_Zero() db.KeyDelete(key, CommandFlags.FireAndForget); - var result = db.SetAdd(key, new RedisValue[0]); + var result = db.SetAdd(key, Array.Empty()); Assert.Equal(0, result); Assert.Equal(0, db.SetLength(key)); @@ -217,7 +217,7 @@ public async Task SetAdd_Zero_Async() db.KeyDelete(key, CommandFlags.FireAndForget); - var t = db.SetAddAsync(key, new RedisValue[0]); + var t = db.SetAddAsync(key, Array.Empty()); Assert.True(t.IsCompleted); // sync var count = await t; Assert.Equal(0, count); diff --git a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs index fb1627b62..09445f6cd 100644 --- a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs @@ -75,222 +75,105 @@ public bool IgnoreConnect public event EventHandler ErrorMessage { - add - { - _inner.ErrorMessage += value; - } - - remove - { - _inner.ErrorMessage -= value; - } + add => _inner.ErrorMessage += value; + remove => _inner.ErrorMessage -= value; } public event EventHandler ConnectionFailed { - add - { - _inner.ConnectionFailed += value; - } - - remove - { - _inner.ConnectionFailed -= value; - } + add => _inner.ConnectionFailed += value; + remove => _inner.ConnectionFailed -= value; } public event EventHandler InternalError { - add - { - _inner.InternalError += value; - } - - remove - { - _inner.InternalError -= value; - } + add => _inner.InternalError += value; + remove => _inner.InternalError -= value; } public event EventHandler ConnectionRestored { - add - { - _inner.ConnectionRestored += value; - } - - remove - { - _inner.ConnectionRestored -= value; - } + add => _inner.ConnectionRestored += value; + remove => _inner.ConnectionRestored -= value; } public event EventHandler ConfigurationChanged { - add - { - _inner.ConfigurationChanged += value; - } - - remove - { - _inner.ConfigurationChanged -= value; - } + add => _inner.ConfigurationChanged += value; + remove => _inner.ConfigurationChanged -= value; } public event EventHandler ConfigurationChangedBroadcast { - add - { - _inner.ConfigurationChangedBroadcast += value; - } - - remove - { - _inner.ConfigurationChangedBroadcast -= value; - } + add => _inner.ConfigurationChangedBroadcast += value; + remove => _inner.ConfigurationChangedBroadcast -= value; } public event EventHandler HashSlotMoved { - add - { - _inner.HashSlotMoved += value; - } - - remove - { - _inner.HashSlotMoved -= value; - } + add => _inner.HashSlotMoved += value; + remove => _inner.HashSlotMoved -= value; } - public void Close(bool allowCommandsToComplete = true) - { - _inner.Close(allowCommandsToComplete); - } + public void Close(bool allowCommandsToComplete = true) => _inner.Close(allowCommandsToComplete); - public Task CloseAsync(bool allowCommandsToComplete = true) - { - return _inner.CloseAsync(allowCommandsToComplete); - } + public Task CloseAsync(bool allowCommandsToComplete = true) => _inner.CloseAsync(allowCommandsToComplete); - public bool Configure(TextWriter log = null) - { - return _inner.Configure(log); - } + public bool Configure(TextWriter log = null) => _inner.Configure(log); - public Task ConfigureAsync(TextWriter log = null) - { - return _inner.ConfigureAsync(log); - } + public Task ConfigureAsync(TextWriter log = null) => _inner.ConfigureAsync(log); public void Dispose() { } // DO NOT call _inner.Dispose(); - public ServerCounters GetCounters() - { - return _inner.GetCounters(); - } + public ServerCounters GetCounters() => _inner.GetCounters(); - public IDatabase GetDatabase(int db = -1, object asyncState = null) - { - return _inner.GetDatabase(db, asyncState); - } + public IDatabase GetDatabase(int db = -1, object asyncState = null) => _inner.GetDatabase(db, asyncState); - public EndPoint[] GetEndPoints(bool configuredOnly = false) - { - return _inner.GetEndPoints(configuredOnly); - } + public EndPoint[] GetEndPoints(bool configuredOnly = false) => _inner.GetEndPoints(configuredOnly); - public int GetHashSlot(RedisKey key) - { - return _inner.GetHashSlot(key); - } + public int GetHashSlot(RedisKey key) => _inner.GetHashSlot(key); - public IServer GetServer(string host, int port, object asyncState = null) - { - return _inner.GetServer(host, port, asyncState); - } + public IServer GetServer(string host, int port, object asyncState = null) => _inner.GetServer(host, port, asyncState); - public IServer GetServer(string hostAndPort, object asyncState = null) - { - return _inner.GetServer(hostAndPort, asyncState); - } + public IServer GetServer(string hostAndPort, object asyncState = null) => _inner.GetServer(hostAndPort, asyncState); - public IServer GetServer(IPAddress host, int port) - { - return _inner.GetServer(host, port); - } + public IServer GetServer(IPAddress host, int port) => _inner.GetServer(host, port); - public IServer GetServer(EndPoint endpoint, object asyncState = null) - { - return _inner.GetServer(endpoint, asyncState); - } + public IServer GetServer(EndPoint endpoint, object asyncState = null) => _inner.GetServer(endpoint, asyncState); - public string GetStatus() - { - return _inner.GetStatus(); - } + public string GetStatus() => _inner.GetStatus(); - public void GetStatus(TextWriter log) - { - _inner.GetStatus(log); - } + public void GetStatus(TextWriter log) => _inner.GetStatus(log); - public string GetStormLog() - { - return _inner.GetStormLog(); - } + public string GetStormLog() => _inner.GetStormLog(); - public ISubscriber GetSubscriber(object asyncState = null) - { - return _inner.GetSubscriber(asyncState); - } + public ISubscriber GetSubscriber(object asyncState = null) => _inner.GetSubscriber(asyncState); - public int HashSlot(RedisKey key) - { - return _inner.HashSlot(key); - } + public int HashSlot(RedisKey key) => _inner.HashSlot(key); - public long PublishReconfigure(CommandFlags flags = CommandFlags.None) - { - return _inner.PublishReconfigure(flags); - } + public long PublishReconfigure(CommandFlags flags = CommandFlags.None) => _inner.PublishReconfigure(flags); - public Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None) - { - return _inner.PublishReconfigureAsync(flags); - } + public Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None) => _inner.PublishReconfigureAsync(flags); - public void RegisterProfiler(Func profilingSessionProvider) - { - _inner.RegisterProfiler(profilingSessionProvider); - } + public void RegisterProfiler(Func profilingSessionProvider) => _inner.RegisterProfiler(profilingSessionProvider); - public void ResetStormLog() - { - _inner.ResetStormLog(); - } + public void ResetStormLog() => _inner.ResetStormLog(); - public void Wait(Task task) - { - _inner.Wait(task); - } + public void Wait(Task task) => _inner.Wait(task); - public T Wait(Task task) - { - return _inner.Wait(task); - } + public T Wait(Task task) => _inner.Wait(task); - public void WaitAll(params Task[] tasks) - { - _inner.WaitAll(tasks); - } + public void WaitAll(params Task[] tasks) => _inner.WaitAll(tasks); public void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All) => _inner.ExportConfiguration(destination, options); } - public void Dispose() => _actualConnection.Dispose(); + public void Dispose() + { + _actualConnection.Dispose(); + GC.SuppressFinalize(this); + } protected void OnInternalError(object sender, InternalErrorEventArgs e) { @@ -338,7 +221,6 @@ public void Teardown(TextWriter output) var subscription = ep.GetBridge(ConnectionType.Subscription); TestBase.Log(output, $" {Format.ToString(subscription)}: " + subscription.GetStatus()); } - } } } diff --git a/tests/StackExchange.Redis.Tests/SortedSets.cs b/tests/StackExchange.Redis.Tests/SortedSets.cs index cea1f518c..c54e9c438 100644 --- a/tests/StackExchange.Redis.Tests/SortedSets.cs +++ b/tests/StackExchange.Redis.Tests/SortedSets.cs @@ -9,7 +9,7 @@ public class SortedSets : TestBase { public SortedSets(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - public static SortedSetEntry[] entries = new SortedSetEntry[] + private static readonly SortedSetEntry[] entries = new SortedSetEntry[] { new SortedSetEntry("a", 1), new SortedSetEntry("b", 2), diff --git a/tests/StackExchange.Redis.Tests/Streams.cs b/tests/StackExchange.Redis.Tests/Streams.cs index b81949b55..f0294c0d7 100644 --- a/tests/StackExchange.Redis.Tests/Streams.cs +++ b/tests/StackExchange.Redis.Tests/Streams.cs @@ -1002,7 +1002,7 @@ public void StreamGroupInfoGet() static bool IsMessageId(string value) { if (string.IsNullOrWhiteSpace(value)) return false; - return value.Length >= 3 && value.Contains("-"); + return value.Length >= 3 && value.Contains('-'); } } @@ -1317,7 +1317,7 @@ public void StreamReadExpectedExceptionEmptyStreamList() var db = conn.GetDatabase(); - var emptyList = new StreamPosition[0]; + var emptyList = Array.Empty(); Assert.Throws(() => db.StreamRead(emptyList)); } @@ -1792,7 +1792,7 @@ public void StreamReadGroupMultiStreamWithNoAckShowsNoPendingMessages() } } - private RedisKey GetUniqueKey(string type) => $"{type}_stream_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; + private static RedisKey GetUniqueKey(string type) => $"{type}_stream_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; [Fact] public async Task StreamReadIndexerUsage() diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index 30099cd22..b7db11fb4 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -369,7 +369,7 @@ public async Task HashStringLengthAsync() Skip.IfMissingFeature(conn, nameof(RedisFeatures.HashStringLength), r => r.HashStringLength); var database = conn.GetDatabase(); var key = Me(); - var value = "hello world"; + const string value = "hello world"; database.HashSet(key, "field", value); var resAsync = database.HashStringLengthAsync(key, "field"); var resNonExistingAsync = database.HashStringLengthAsync(key, "non-existing-field"); @@ -386,7 +386,7 @@ public void HashStringLength() Skip.IfMissingFeature(conn, nameof(RedisFeatures.HashStringLength), r => r.HashStringLength); var database = conn.GetDatabase(); var key = Me(); - var value = "hello world"; + const string value = "hello world"; database.HashSet(key, "field", value); Assert.Equal(value.Length, database.HashStringLength(key, "field")); Assert.Equal(0, database.HashStringLength(key, "non-existing-field")); diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 60636db54..f5dfd8ae2 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -36,10 +36,16 @@ protected TestBase(ITestOutputHelper output, SharedConnectionFixture fixture = n ClearAmbientFailures(); } - /// Useful to temporarily get extra worker threads for an otherwise synchronous test case which will 'block' the thread, on a synchronous API like Task.Wait() or Task.Result - /// Must NOT be used for test cases which *goes async*, as then the inferred return type will become 'async void', and we will fail to observe the result of the async part + /// + /// Useful to temporarily get extra worker threads for an otherwise synchronous test case which will 'block' the thread, + /// on a synchronous API like or . + /// + /// + /// Must NOT be used for test cases which *goes async*, as then the inferred return type will become 'async void', + /// and we will fail to observe the result of the async part. + /// /// See 'ConnectFailTimeout' class for example usage. - protected Task RunBlockingSynchronousWithExtraThreadAsync(Action testScenario) => Task.Factory.StartNew(testScenario, CancellationToken.None, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + protected static Task RunBlockingSynchronousWithExtraThreadAsync(Action testScenario) => Task.Factory.StartNew(testScenario, CancellationToken.None, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); protected void LogNoTime(string message) => LogNoTime(Writer, message); internal static void LogNoTime(TextWriter output, string message) @@ -87,7 +93,7 @@ protected ProfiledCommandEnumerable Log(ProfilingSession session) return profile; } - protected void CollectGarbage() + protected static void CollectGarbage() { GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); @@ -99,6 +105,8 @@ public void Dispose() { _fixture?.Teardown(Writer); Teardown(); + Writer.Dispose(); + GC.SuppressFinalize(this); } #if VERBOSE @@ -211,7 +219,7 @@ public void Teardown() Log($"Service Counts: (Scheduler) Queue: {pool?.TotalServicedByQueue.ToString()}, Pool: {pool?.TotalServicedByPool.ToString()}, Workers: {pool?.WorkerCount.ToString()}, Available: {pool?.AvailableCount.ToString()}"); } - protected IServer GetServer(IConnectionMultiplexer muxer) + protected static IServer GetServer(IConnectionMultiplexer muxer) { EndPoint[] endpoints = muxer.GetEndPoints(); IServer result = null; @@ -226,7 +234,7 @@ protected IServer GetServer(IConnectionMultiplexer muxer) return result; } - protected IServer GetAnyMaster(IConnectionMultiplexer muxer) + protected static IServer GetAnyMaster(IConnectionMultiplexer muxer) { foreach (var endpoint in muxer.GetEndPoints()) { @@ -300,10 +308,7 @@ internal virtual IInternalConnectionMultiplexer Create( caller); muxer.InternalError += OnInternalError; muxer.ConnectionFailed += OnConnectionFailed; - muxer.ConnectionRestored += (s, e) => - { - Log($"Connection Restored ({e.ConnectionType},{e.FailureType}): {e.Exception}"); - }; + muxer.ConnectionRestored += (s, e) => Log($"Connection Restored ({e.ConnectionType},{e.FailureType}): {e.Exception}"); return muxer; } @@ -479,7 +484,7 @@ void callback() } private static readonly TimeSpan DefaultWaitPerLoop = TimeSpan.FromMilliseconds(50); - protected async Task UntilCondition(TimeSpan maxWaitTime, Func predicate, TimeSpan? waitPerLoop = null) + protected static async Task UntilConditionAsync(TimeSpan maxWaitTime, Func predicate, TimeSpan? waitPerLoop = null) { TimeSpan spent = TimeSpan.Zero; while (spent < maxWaitTime && !predicate()) diff --git a/tests/StackExchange.Redis.Tests/Values.cs b/tests/StackExchange.Redis.Tests/Values.cs index 0b3b1fad2..ecf702136 100644 --- a/tests/StackExchange.Redis.Tests/Values.cs +++ b/tests/StackExchange.Redis.Tests/Values.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Text; using Xunit; using Xunit.Abstractions; @@ -24,7 +25,7 @@ public void NullValueChecks() Assert.False(n.HasValue); Assert.True(n.IsNullOrEmpty); - RedisValue emptyArr = new byte[0]; + RedisValue emptyArr = Array.Empty(); Assert.False(emptyArr.IsNull); Assert.False(emptyArr.IsInteger); Assert.False(emptyArr.HasValue); diff --git a/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs b/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs index 8ff0cf0ac..b00bf3127 100644 --- a/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs +++ b/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs @@ -16,7 +16,7 @@ public void BlankPrefixYieldsSame_Bytes() using (var conn = Create()) { var raw = conn.GetDatabase(); - var prefixed = raw.WithKeyPrefix(new byte[0]); + var prefixed = raw.WithKeyPrefix(Array.Empty()); Assert.Same(raw, prefixed); } } diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index f7bf9a6be..ecb386033 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -53,7 +53,7 @@ public void HashDeleteAsync_1() [Fact] public void HashDeleteAsync_2() { - RedisValue[] hashFields = new RedisValue[0]; + RedisValue[] hashFields = Array.Empty(); wrapper.HashDeleteAsync("key", hashFields, CommandFlags.None); mock.Verify(_ => _.HashDeleteAsync("prefix:key", hashFields, CommandFlags.None)); } @@ -82,7 +82,7 @@ public void HashGetAsync_1() [Fact] public void HashGetAsync_2() { - RedisValue[] hashFields = new RedisValue[0]; + RedisValue[] hashFields = Array.Empty(); wrapper.HashGetAsync("key", hashFields, CommandFlags.None); mock.Verify(_ => _.HashGetAsync("prefix:key", hashFields, CommandFlags.None)); } @@ -118,7 +118,7 @@ public void HashLengthAsync() [Fact] public void HashSetAsync_1() { - HashEntry[] hashFields = new HashEntry[0]; + HashEntry[] hashFields = Array.Empty(); wrapper.HashSetAsync("key", hashFields, CommandFlags.None); mock.Verify(_ => _.HashSetAsync("prefix:key", hashFields, CommandFlags.None)); } @@ -154,7 +154,7 @@ public void HyperLogLogAddAsync_1() [Fact] public void HyperLogLogAddAsync_2() { - var values = new RedisValue[0]; + var values = Array.Empty(); wrapper.HyperLogLogAddAsync("key", values, CommandFlags.None); mock.Verify(_ => _.HyperLogLogAddAsync("prefix:key", values, CommandFlags.None)); } @@ -280,7 +280,7 @@ public void KeyRenameAsync() [Fact] public void KeyRestoreAsync() { - byte[] value = new byte[0]; + byte[] value = Array.Empty(); TimeSpan expiry = TimeSpan.FromSeconds(123); wrapper.KeyRestoreAsync("key", value, expiry, CommandFlags.None); mock.Verify(_ => _.KeyRestoreAsync("prefix:key", value, expiry, CommandFlags.None)); @@ -345,7 +345,7 @@ public void ListLeftPushAsync_1() [Fact] public void ListLeftPushAsync_2() { - RedisValue[] values = new RedisValue[0]; + RedisValue[] values = Array.Empty(); wrapper.ListLeftPushAsync("key", values, CommandFlags.None); mock.Verify(_ => _.ListLeftPushAsync("prefix:key", values, CommandFlags.None)); } @@ -410,7 +410,7 @@ public void ListRightPushAsync_1() [Fact] public void ListRightPushAsync_2() { - RedisValue[] values = new RedisValue[0]; + RedisValue[] values = Array.Empty(); wrapper.ListRightPushAsync("key", values, CommandFlags.None); mock.Verify(_ => _.ListRightPushAsync("prefix:key", values, CommandFlags.None)); } @@ -477,8 +477,8 @@ public void PublishAsync() [Fact] public void ScriptEvaluateAsync_1() { - byte[] hash = new byte[0]; - RedisValue[] values = new RedisValue[0]; + byte[] hash = Array.Empty(); + RedisValue[] values = Array.Empty(); RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; wrapper.ScriptEvaluateAsync(hash, keys, values, CommandFlags.None); @@ -488,7 +488,7 @@ public void ScriptEvaluateAsync_1() [Fact] public void ScriptEvaluateAsync_2() { - RedisValue[] values = new RedisValue[0]; + RedisValue[] values = Array.Empty(); RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; wrapper.ScriptEvaluateAsync("script", keys, values, CommandFlags.None); @@ -505,7 +505,7 @@ public void SetAddAsync_1() [Fact] public void SetAddAsync_2() { - RedisValue[] values = new RedisValue[0]; + RedisValue[] values = Array.Empty(); wrapper.SetAddAsync("key", values, CommandFlags.None); mock.Verify(_ => _.SetAddAsync("prefix:key", values, CommandFlags.None)); } @@ -611,7 +611,7 @@ public void SetRemoveAsync_1() [Fact] public void SetRemoveAsync_2() { - RedisValue[] values = new RedisValue[0]; + RedisValue[] values = Array.Empty(); wrapper.SetRemoveAsync("key", values, CommandFlags.None); mock.Verify(_ => _.SetRemoveAsync("prefix:key", values, CommandFlags.None)); } @@ -652,7 +652,7 @@ public void SortedSetAddAsync_1() [Fact] public void SortedSetAddAsync_2() { - SortedSetEntry[] values = new SortedSetEntry[0]; + SortedSetEntry[] values = Array.Empty(); wrapper.SortedSetAddAsync("key", values, When.Exists, CommandFlags.None); mock.Verify(_ => _.SortedSetAddAsync("prefix:key", values, When.Exists, CommandFlags.None)); } @@ -760,7 +760,7 @@ public void SortedSetRemoveAsync_1() [Fact] public void SortedSetRemoveAsync_2() { - RedisValue[] members = new RedisValue[0]; + RedisValue[] members = Array.Empty(); wrapper.SortedSetRemoveAsync("key", members, CommandFlags.None); mock.Verify(_ => _.SortedSetRemoveAsync("prefix:key", members, CommandFlags.None)); } @@ -818,7 +818,7 @@ public void StreamAddAsync_1() [Fact] public void StreamAddAsync_2() { - var fields = new NameValueEntry[0]; + var fields = Array.Empty(); wrapper.StreamAddAsync("key", fields, "*", 1000, true, CommandFlags.None); mock.Verify(_ => _.StreamAddAsync("prefix:key", fields, "*", 1000, true, CommandFlags.None)); } @@ -826,7 +826,7 @@ public void StreamAddAsync_2() [Fact] public void StreamClaimMessagesAsync() { - var messageIds = new RedisValue[0]; + var messageIds = Array.Empty(); wrapper.StreamClaimAsync("key", "group", "consumer", 1000, messageIds, CommandFlags.None); mock.Verify(_ => _.StreamClaimAsync("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); } @@ -834,7 +834,7 @@ public void StreamClaimMessagesAsync() [Fact] public void StreamClaimMessagesReturningIdsAsync() { - var messageIds = new RedisValue[0]; + var messageIds = Array.Empty(); wrapper.StreamClaimIdsOnlyAsync("key", "group", "consumer", 1000, messageIds, CommandFlags.None); mock.Verify(_ => _.StreamClaimIdsOnlyAsync("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); } @@ -884,7 +884,7 @@ public void StreamLengthAsync() [Fact] public void StreamMessagesDeleteAsync() { - var messageIds = new RedisValue[] { }; + var messageIds = Array.Empty(); wrapper.StreamDeleteAsync("key", messageIds, CommandFlags.None); mock.Verify(_ => _.StreamDeleteAsync("prefix:key", messageIds, CommandFlags.None)); } @@ -927,7 +927,7 @@ public void StreamRangeAsync() [Fact] public void StreamReadAsync_1() { - var streamPositions = new StreamPosition[] { }; + var streamPositions = Array.Empty(); wrapper.StreamReadAsync(streamPositions, null, CommandFlags.None); mock.Verify(_ => _.StreamReadAsync(streamPositions, null, CommandFlags.None)); } @@ -949,7 +949,7 @@ public void StreamReadGroupAsync_1() [Fact] public void StreamStreamReadGroupAsync_2() { - var streamPositions = new StreamPosition[] { }; + var streamPositions = Array.Empty(); wrapper.StreamReadGroupAsync(streamPositions, "group", "consumer", 10, false, CommandFlags.None); mock.Verify(_ => _.StreamReadGroupAsync(streamPositions, "group", "consumer", 10, false, CommandFlags.None)); } diff --git a/toys/StackExchange.Redis.Server/GlobalSuppressions.cs b/toys/StackExchange.Redis.Server/GlobalSuppressions.cs new file mode 100644 index 000000000..150a689ac --- /dev/null +++ b/toys/StackExchange.Redis.Server/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.TypedRedisValue.ToString~System.String")] From dba0b7aca8f8d29f7d88417312a4e7b1273137a7 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 9 Feb 2022 18:36:32 -0500 Subject: [PATCH 075/435] Solution cleanup 2 (#1984) Cleans up the main project and removes unused code (yay analyzers!) This is a few misc. unused paths just getting it in 1 PR since there aren't that many. --- src/StackExchange.Redis/BufferReader.cs | 13 ------------- src/StackExchange.Redis/CommandMap.cs | 6 ------ .../ConfigurationOptions.cs | 8 ++++---- .../ConnectionMultiplexer.cs | 10 +++++++--- src/StackExchange.Redis/CursorEnumerable.cs | 2 ++ src/StackExchange.Redis/GlobalSuppressions.cs | 7 +++++++ .../Maintenance/AzureMaintenanceEvent.cs | 4 ++-- src/StackExchange.Redis/PhysicalBridge.cs | 19 +++++++------------ src/StackExchange.Redis/RedisValue.cs | 2 +- src/StackExchange.Redis/ResultProcessor.cs | 2 +- tests/BasicTest/BasicTest.csproj | 1 - .../BasicTestBaseline.csproj | 1 - toys/KestrelRedisServer/Startup.cs | 6 +++++- 13 files changed, 36 insertions(+), 45 deletions(-) diff --git a/src/StackExchange.Redis/BufferReader.cs b/src/StackExchange.Redis/BufferReader.cs index b248ac9c8..6403053d9 100644 --- a/src/StackExchange.Redis/BufferReader.cs +++ b/src/StackExchange.Redis/BufferReader.cs @@ -178,7 +178,6 @@ public void Consume(int count) if (haveTrailingCR) { if (span[0] == '\n') return totalSkipped - 1; - haveTrailingCR = false; } int found = span.VectorSafeIndexOfCRLF(); @@ -191,18 +190,6 @@ public void Consume(int count) return -1; } - //internal static bool HasBytes(BufferReader reader, int count) // very deliberately not ref; want snapshot - //{ - // if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); - // do - // { - // var available = reader.RemainingThisSpan; - // if (count <= available) return true; - // count -= available; - // } while (reader.FetchNextSegment()); - // return false; - //} - public int ConsumeByte() { if (IsEmpty) return -1; diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index fb43b45b2..b37a51512 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -185,7 +185,6 @@ private static CommandMap CreateImpl(Dictionary caseInsensitiveO var commands = (RedisCommand[])Enum.GetValues(typeof(RedisCommand)); var map = new CommandBytes[commands.Length]; - bool haveDelta = false; for (int i = 0; i < commands.Length; i++) { int idx = (int)commands[i]; @@ -201,14 +200,9 @@ private static CommandMap CreateImpl(Dictionary caseInsensitiveO { value = tmp; } - if (value != name) haveDelta = true; - // TODO: bug? - haveDelta = true; map[idx] = new CommandBytes(value); } } - if (!haveDelta && Default != null) return Default; - return new CommandMap(map); } } diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index a36a4d06b..a22d23a52 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -332,7 +332,7 @@ public bool HighPrioritySocketThreads /// public int KeepAlive { - get => keepAlive.GetValueOrDefault(-1); + get => keepAlive ?? -1; set => keepAlive = value; } @@ -443,7 +443,7 @@ public string SslHost /// public int SyncTimeout { - get => syncTimeout.GetValueOrDefault(5000); + get => syncTimeout ?? 5000; set => syncTimeout = value; } @@ -484,7 +484,7 @@ internal RemoteCertificateValidationCallback CertificateValidationCallback /// public int ConfigCheckSeconds { - get => configCheckSeconds.GetValueOrDefault(60); + get => configCheckSeconds ?? 60; set => configCheckSeconds = value; } @@ -919,7 +919,7 @@ private string InferSslHostFromEndpoints() { var dnsEndpoints = EndPoints.Select(endpoint => endpoint as DnsEndPoint); string dnsHost = dnsEndpoints.FirstOrDefault()?.Host; - if (dnsEndpoints.All(dnsEndpoint => (dnsEndpoint != null && dnsEndpoint.Host == dnsHost))) + if (dnsEndpoints.All(dnsEndpoint => dnsEndpoint != null && dnsEndpoint.Host == dnsHost)) { return dnsHost; } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 3b827f7f8..1cb4df345 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1473,7 +1473,7 @@ public IDatabase GetDatabase(int db = -1, object asyncState = null) return dbCacheZero ??= new RedisDatabase(this, 0, null); } var arr = dbCacheLow ??= new IDatabase[MaxCachedDatabaseInstance]; - return arr[db - 1] ?? (arr[db - 1] = new RedisDatabase(this, db, null)); + return arr[db - 1] ??= new RedisDatabase(this, db, null); } /// @@ -2137,7 +2137,11 @@ private static string DeDotifyHost(string input) if (colonPosition > 0) { // Has a port specifier +#if NETCOREAPP + return string.Concat(input.AsSpan(0, periodPosition), input.AsSpan(colonPosition)); +#else return input.Substring(0, periodPosition) + input.Substring(colonPosition); +#endif } else { @@ -2316,7 +2320,7 @@ internal void InitializeSentinel(LogProxy logProxy) if (sub.SubscribedEndpoint("+switch-master") == null) { - sub.Subscribe("+switch-master", (channel, message) => + sub.Subscribe("+switch-master", (_, message) => { string[] messageParts = ((string)message).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); EndPoint switchBlame = Format.TryParseEndPoint(string.Format("{0}:{1}", messageParts[1], messageParts[2])); @@ -2355,7 +2359,7 @@ internal void InitializeSentinel(LogProxy logProxy) // Subscribe to new sentinels being added if (sub.SubscribedEndpoint("+sentinel") == null) { - sub.Subscribe("+sentinel", (channel, message) => + sub.Subscribe("+sentinel", (_, message) => { string[] messageParts = ((string)message).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); UpdateSentinelAddressList(messageParts[0]); diff --git a/src/StackExchange.Redis/CursorEnumerable.cs b/src/StackExchange.Redis/CursorEnumerable.cs index 12cb70fe7..686f412e1 100644 --- a/src/StackExchange.Redis/CursorEnumerable.cs +++ b/src/StackExchange.Redis/CursorEnumerable.cs @@ -107,6 +107,7 @@ public void Dispose() { _state = State.Disposed; SetComplete(); + GC.SuppressFinalize(this); } private void SetComplete() @@ -128,6 +129,7 @@ private void SetComplete() public ValueTask DisposeAsync() { Dispose(); + GC.SuppressFinalize(this); return default; } diff --git a/src/StackExchange.Redis/GlobalSuppressions.cs b/src/StackExchange.Redis/GlobalSuppressions.cs index 622713622..0e0d1c783 100644 --- a/src/StackExchange.Redis/GlobalSuppressions.cs +++ b/src/StackExchange.Redis/GlobalSuppressions.cs @@ -12,3 +12,10 @@ [assembly: SuppressMessage("Roslynator", "RCS1104:Simplify conditional expression.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisSubscriber.Unsubscribe(StackExchange.Redis.RedisChannel@,System.Action{StackExchange.Redis.RedisChannel,StackExchange.Redis.RedisValue},StackExchange.Redis.ChannelMessageQueue,StackExchange.Redis.CommandFlags)~System.Boolean")] [assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Message.IsMasterOnly(StackExchange.Redis.RedisCommand)~System.Boolean")] [assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Message.RequiresDatabase(StackExchange.Redis.RedisCommand)~System.Boolean")] +[assembly: SuppressMessage("Style", "IDE0180:Use tuple to swap values", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisDatabase.ReverseLimits(StackExchange.Redis.Order,StackExchange.Redis.Exclude@,StackExchange.Redis.RedisValue@,StackExchange.Redis.RedisValue@)")] +[assembly: SuppressMessage("Style", "IDE0180:Use tuple to swap values", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisDatabase.GetSortedSetRangeByScoreMessage(StackExchange.Redis.RedisKey,System.Double,System.Double,StackExchange.Redis.Exclude,StackExchange.Redis.Order,System.Int64,System.Int64,StackExchange.Redis.CommandFlags,System.Boolean)~StackExchange.Redis.Message")] +[assembly: SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.PhysicalConnection.FlushSync(System.Boolean,System.Int32)~StackExchange.Redis.WriteResult")] +[assembly: SuppressMessage("Usage", "CA2219:Do not raise exceptions in finally clauses", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.PhysicalBridge.ProcessBacklogAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2249:Consider using 'string.Contains' instead of 'string.IndexOf'", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.ClientInfo.AddFlag(StackExchange.Redis.ClientFlags@,System.String,StackExchange.Redis.ClientFlags,System.Char)")] +[assembly: SuppressMessage("Style", "IDE0070:Use 'System.HashCode'", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.CommandBytes.GetHashCode~System.Int32")] +[assembly: SuppressMessage("Roslynator", "RCS1085:Use auto-implemented property.", Justification = "", Scope = "member", Target = "~P:StackExchange.Redis.RedisValue.OverlappedValueInt64")] diff --git a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs index ad6279921..a413dab1e 100644 --- a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs @@ -129,7 +129,7 @@ internal async static Task AddListenerAsync(ConnectionMultiplexer multiplexer, C return; } - await sub.SubscribeAsync(PubSubChannelName, async (channel, message) => + await sub.SubscribeAsync(PubSubChannelName, async (_, message) => { var newMessage = new AzureMaintenanceEvent(message); multiplexer.InvokeServerMaintenanceEvent(newMessage); @@ -180,7 +180,7 @@ await sub.SubscribeAsync(PubSubChannelName, async (channel, message) => /// public int NonSslPort { get; } - private AzureNotificationType ParseNotificationType(string typeString) => typeString switch + private static AzureNotificationType ParseNotificationType(string typeString) => typeString switch { "NodeMaintenanceScheduled" => AzureNotificationType.NodeMaintenanceScheduled, "NodeMaintenanceStarting" => AzureNotificationType.NodeMaintenanceStarting, diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 4e4f3097a..7d5b250ee 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -152,7 +152,7 @@ private WriteResult QueueOrFailMessage(Message message) if (message.Command != RedisCommand.QUIT) { message.SetEnqueued(null); - BacklogEnqueue(message, null); + BacklogEnqueue(message); // Note: we don't start a worker on each message here return WriteResult.Success; // Successfully queued, so indicate success } @@ -784,7 +784,7 @@ private bool TryPushToBacklog(Message message, bool onlyIfExists, bool bypassBac return false; } - BacklogEnqueue(message, physical); + BacklogEnqueue(message); // The correct way to decide to start backlog process is not based on previously empty // but based on a) not empty now (we enqueued!) and b) no backlog processor already running. @@ -794,7 +794,7 @@ private bool TryPushToBacklog(Message message, bool onlyIfExists, bool bypassBac } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void BacklogEnqueue(Message message, PhysicalConnection physical) + private void BacklogEnqueue(Message message) { _backlog.Enqueue(message); Interlocked.Increment(ref _backlogTotalEnqueued); @@ -1055,14 +1055,13 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect // AVOID REORDERING MESSAGES // Prefer to add it to the backlog if this thread can see that there might already be a message backlog. - // We do this before attempting to take the writelock, because we won't actually write, we'll just let the backlog get processed in due course + // We do this before attempting to take the write lock, because we won't actually write, we'll just let the backlog get processed in due course if (TryPushToBacklog(message, onlyIfExists: true, bypassBacklog: bypassBacklog)) { return new ValueTask(WriteResult.Success); // queued counts as success } bool releaseLock = true; // fine to default to true, as it doesn't matter until token is a "success" - int lockTaken = 0; #if NETCOREAPP bool gotLock = false; #else @@ -1103,9 +1102,6 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect if (!token.Success) return new ValueTask(TimedOutBeforeWrite(message)); #endif } -#if DEBUG - lockTaken = Environment.TickCount; -#endif var result = WriteMessageInsideLock(physical, message); if (result == WriteResult.Success) { @@ -1114,9 +1110,9 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect { releaseLock = false; // so we don't release prematurely #if NETCOREAPP - return CompleteWriteAndReleaseLockAsync(flush, message, lockTaken); + return CompleteWriteAndReleaseLockAsync(flush, message); #else - return CompleteWriteAndReleaseLockAsync(token, flush, message, lockTaken); + return CompleteWriteAndReleaseLockAsync(token, flush, message); #endif } @@ -1205,8 +1201,7 @@ private async ValueTask CompleteWriteAndReleaseLockAsync( LockToken lockToken, #endif ValueTask flush, - Message message, - int lockTaken) + Message message) { #if !NETCOREAPP using (lockToken) diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index 856300206..8759385e8 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -463,7 +463,7 @@ internal static RedisValue TryParse(object obj, out bool valid) [CLSCompliant(false)] public static implicit operator RedisValue(ulong value) { - const ulong MSB = (1UL) << 63; + const ulong MSB = 1UL << 63; return (value & MSB) == 0 ? new RedisValue((long)value, default, Sentinel_SignedInteger) // prefer signed whenever we can : new RedisValue(unchecked((long)value), default, Sentinel_UnsignedInteger); // with unsigned as the fallback diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index b29a87d3f..a69dc6880 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -2011,7 +2011,7 @@ protected StreamEntry[] ParseRedisStreamEntries(in RawResult result) } return result.GetItems().ToArray( - (in RawResult item, in StreamProcessorBase obj) => ParseRedisStreamEntry(item), this); + (in RawResult item, in StreamProcessorBase _) => ParseRedisStreamEntry(item), this); } protected static NameValueEntry[] ParseStreamEntryValues(in RawResult result) diff --git a/tests/BasicTest/BasicTest.csproj b/tests/BasicTest/BasicTest.csproj index 7c283e13f..70d2c8ecf 100644 --- a/tests/BasicTest/BasicTest.csproj +++ b/tests/BasicTest/BasicTest.csproj @@ -7,7 +7,6 @@ Exe BasicTest win7-x64 - false diff --git a/tests/BasicTestBaseline/BasicTestBaseline.csproj b/tests/BasicTestBaseline/BasicTestBaseline.csproj index 474dd3e1b..571261f73 100644 --- a/tests/BasicTestBaseline/BasicTestBaseline.csproj +++ b/tests/BasicTestBaseline/BasicTestBaseline.csproj @@ -7,7 +7,6 @@ Exe BasicTestBaseline win7-x64 - false $(DefineConstants);TEST_BASELINE diff --git a/toys/KestrelRedisServer/Startup.cs b/toys/KestrelRedisServer/Startup.cs index 8d7d43f38..ab991c5fa 100644 --- a/toys/KestrelRedisServer/Startup.cs +++ b/toys/KestrelRedisServer/Startup.cs @@ -17,7 +17,11 @@ public class Startup : IDisposable public void ConfigureServices(IServiceCollection services) => services.Add(new ServiceDescriptor(typeof(RespServer), _server)); - public void Dispose() => _server.Dispose(); + public void Dispose() + { + _server.Dispose(); + GC.SuppressFinalize(this); + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime) From 80701beb315b30ade972a4cc2d7cf6c6862bee8c Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 9 Feb 2022 18:45:42 -0500 Subject: [PATCH 076/435] Client name: append library version suffix (#1985) This changes the default client name to append "(v2.5.x)" on the end (no spaces because Redis doesn't support spaces in client names). It won't append if someone is setting their client name explicitly in options, only for our default, so should be a safe change. I'm not sure if a `Utils` class (internal) is best here, but seemed like the right path. I really didn't want to ref version from `ExceptionFactory` in the multiplexer... --- Directory.Build.props | 2 +- docs/ReleaseNotes.md | 1 + .../ConnectionMultiplexer.cs | 7 +++++-- src/StackExchange.Redis/ExceptionFactory.cs | 13 +------------ src/StackExchange.Redis/PhysicalConnection.cs | 2 +- src/StackExchange.Redis/Utils.cs | 19 +++++++++++++++++++ tests/StackExchange.Redis.Tests/Config.cs | 4 ++-- .../ExceptionFactoryTests.cs | 2 +- 8 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 src/StackExchange.Redis/Utils.cs diff --git a/Directory.Build.props b/Directory.Build.props index 6227de316..e66dd6b0a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,7 +15,7 @@ https://github.com/StackExchange/StackExchange.Redis/ MIT - 9.0 + 10.0 git https://github.com/StackExchange/StackExchange.Redis/ diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 64b26cac4..396525601 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -14,6 +14,7 @@ - Fixes a race in subscribing immediately before a publish - Fixes subscription routing on clusters (spreading instead of choosing 1 node) - More correctly reconnects subscriptions on connection failures, including to other endpoints +- Adds "(vX.X.X)" version suffix to the default client ID so server-side `CLIENT LIST` can more easily see what's connected (#1985 via NickCraver) ## 2.2.88 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 1cb4df345..7f7683c13 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -149,12 +149,15 @@ public ServerCounters GetCounters() private static string defaultClientName; + /// + /// Gets the client name for a connection, with the library version appended. + /// private static string GetDefaultClientName() { - return defaultClientName ??= TryGetAzureRoleInstanceIdNoThrow() + return defaultClientName ??= (TryGetAzureRoleInstanceIdNoThrow() ?? Environment.MachineName ?? Environment.GetEnvironmentVariable("ComputerName") - ?? "StackExchange.Redis"; + ?? "StackExchange.Redis") + "(v" + Utils.GetLibVersion() + ")"; } /// diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 598517d4b..92bbb2a0e 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -199,17 +199,6 @@ internal static Exception NoCursor(RedisCommand command) return new RedisCommandException("Command cannot be used with a cursor: " + s); } - private static string _libVersion; - internal static string GetLibVersion() - { - if (_libVersion == null) - { - var assembly = typeof(ConnectionMultiplexer).Assembly; - _libVersion = ((AssemblyFileVersionAttribute)Attribute.GetCustomAttribute(assembly, typeof(AssemblyFileVersionAttribute)))?.Version - ?? assembly.GetName().Version.ToString(); - } - return _libVersion; - } private static void Add(List> data, StringBuilder sb, string lk, string sk, string v) { if (v != null) @@ -365,7 +354,7 @@ ServerEndPoint server Add(data, sb, "Local-CPU", "Local-CPU", PerfCounterHelper.GetSystemCpuPercent()); } - Add(data, sb, "Version", "v", GetLibVersion()); + Add(data, sb, "Version", "v", Utils.GetLibVersion()); } private static void AddExceptionDetail(Exception exception, Message message, ServerEndPoint server, string label) diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 27f17a6ba..10da21fe1 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -434,7 +434,7 @@ void add(string lk, string sk, string v) } } - add("Version", "v", ExceptionFactory.GetLibVersion()); + add("Version", "v", Utils.GetLibVersion()); outerException = new RedisConnectionException(failureType, exMessage.ToString(), innerException); diff --git a/src/StackExchange.Redis/Utils.cs b/src/StackExchange.Redis/Utils.cs new file mode 100644 index 000000000..b5ec20670 --- /dev/null +++ b/src/StackExchange.Redis/Utils.cs @@ -0,0 +1,19 @@ +using System; +using System.Reflection; + +namespace StackExchange.Redis; + +internal static class Utils +{ + private static string _libVersion; + internal static string GetLibVersion() + { + if (_libVersion == null) + { + var assembly = typeof(ConnectionMultiplexer).Assembly; + _libVersion = ((AssemblyFileVersionAttribute)Attribute.GetCustomAttribute(assembly, typeof(AssemblyFileVersionAttribute)))?.Version + ?? assembly.GetName().Version.ToString(); + } + return _libVersion; + } +} diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index 99ef60996..271e8a386 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -243,12 +243,12 @@ public void DefaultClientName() { using (var muxer = Create(allowAdmin: true, caller: null)) // force default naming to kick in { - Assert.Equal(Environment.MachineName, muxer.ClientName); + Assert.Equal($"{Environment.MachineName}(v{Utils.GetLibVersion()})", muxer.ClientName); var conn = muxer.GetDatabase(); conn.Ping(); var name = (string)GetAnyMaster(muxer).Execute("CLIENT", "GETNAME"); - Assert.Equal(Environment.MachineName, name); + Assert.Equal($"{Environment.MachineName}(v{Utils.GetLibVersion()})", name); } } diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 31585159f..8c84227be 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -23,7 +23,7 @@ public void NullLastException() [Fact] public void CanGetVersion() { - var libVer = ExceptionFactory.GetLibVersion(); + var libVer = Utils.GetLibVersion(); Assert.Matches(@"2\.[0-9]+\.[0-9]+(\.[0-9]+)?", libVer); } From 74adca8d69ced316b2d4f57d2e8375d40de06335 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 9 Feb 2022 20:43:28 -0500 Subject: [PATCH 077/435] Proxy: move to extension methods (#1982) This should make PRs like #1977 and #1425 much easier. --- .../ConnectionMultiplexer.cs | 42 ++++++++++++------- src/StackExchange.Redis/Enums/Proxy.cs | 30 +++++++++++++ src/StackExchange.Redis/Enums/ServerType.cs | 20 +++++++++ .../Interfaces/IConnectionMultiplexer.cs | 2 +- src/StackExchange.Redis/ServerEndPoint.cs | 6 +-- 5 files changed, 81 insertions(+), 19 deletions(-) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 7f7683c13..a8834bc64 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -572,7 +572,7 @@ private static void WriteNormalizingLineEndings(string source, StreamWriter writ /// /// Raised when nodes are explicitly requested to reconfigure via broadcast. - /// This usually means primary/replica role changes. + /// This usually means primary/replica changes. /// public event EventHandler ConfigurationChangedBroadcast; @@ -1421,7 +1421,10 @@ internal long LastHeartbeatSecondsAgo /// The async state object to pass to the created . public ISubscriber GetSubscriber(object asyncState = null) { - if (RawConfig.Proxy == Proxy.Twemproxy) throw new NotSupportedException("The pub/sub API is not available via twemproxy"); + if (!RawConfig.Proxy.SupportsPubSub()) + { + throw new NotSupportedException($"The pub/sub API is not available via {RawConfig.Proxy}"); + } return new RedisSubscriber(this, asyncState); } @@ -1439,9 +1442,9 @@ internal int ApplyDefaultDatabase(int db) throw new ArgumentOutOfRangeException(nameof(db)); } - if (db != 0 && RawConfig.Proxy == Proxy.Twemproxy) + if (db != 0 && !RawConfig.Proxy.SupportsDatabases()) { - throw new NotSupportedException("twemproxy only supports database 0"); + throw new NotSupportedException($"{RawConfig.Proxy} only supports database 0"); } return db; @@ -1509,7 +1512,10 @@ public IDatabase GetDatabase(int db = -1, object asyncState = null) public IServer GetServer(EndPoint endpoint, object asyncState = null) { if (endpoint == null) throw new ArgumentNullException(nameof(endpoint)); - if (RawConfig.Proxy == Proxy.Twemproxy) throw new NotSupportedException("The server API is not available via twemproxy"); + if (!RawConfig.Proxy.SupportsServerApi()) + { + throw new NotSupportedException($"The server API is not available via {RawConfig.Proxy}"); + } var server = (ServerEndPoint)servers[endpoint]; if (server == null) throw new ArgumentException("The specified endpoint is not defined", nameof(endpoint)); return new RedisServer(this, server, asyncState); @@ -1885,18 +1891,24 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP ServerSelectionStrategy.ServerType = ServerType.Standalone; } - var preferred = NominatePreferredMaster(log, servers, useTieBreakers, masters); - foreach (var master in masters) + // If multiple primaries are detected, nominate the preferred one + // ...but not if the type of server we're connected to supports and expects multiple primaries + // ...for those cases, we want to allow sending to any primary endpoint. + if (ServerSelectionStrategy.ServerType.HasSinglePrimary()) { - if (master == preferred || master.IsReplica) - { - log?.WriteLine($"{Format.ToString(master)}: Clearing as RedundantMaster"); - master.ClearUnselectable(UnselectableFlags.RedundantMaster); - } - else + var preferred = NominatePreferredMaster(log, servers, useTieBreakers, masters); + foreach (var master in masters) { - log?.WriteLine($"{Format.ToString(master)}: Setting as RedundantMaster"); - master.SetUnselectable(UnselectableFlags.RedundantMaster); + if (master == preferred || master.IsReplica) + { + log?.WriteLine($"{Format.ToString(master)}: Clearing as RedundantMaster"); + master.ClearUnselectable(UnselectableFlags.RedundantMaster); + } + else + { + log?.WriteLine($"{Format.ToString(master)}: Setting as RedundantMaster"); + master.SetUnselectable(UnselectableFlags.RedundantMaster); + } } } } diff --git a/src/StackExchange.Redis/Enums/Proxy.cs b/src/StackExchange.Redis/Enums/Proxy.cs index 64dcada45..38a71a8e1 100644 --- a/src/StackExchange.Redis/Enums/Proxy.cs +++ b/src/StackExchange.Redis/Enums/Proxy.cs @@ -14,4 +14,34 @@ public enum Proxy /// Twemproxy, } + + internal static class ProxyExtensions + { + /// + /// Whether a proxy supports databases (e.g. database > 0). + /// + public static bool SupportsDatabases(this Proxy proxy) => proxy switch + { + Proxy.Twemproxy => false, + _ => true + }; + + /// + /// Whether a proxy supports pub/sub. + /// + public static bool SupportsPubSub(this Proxy proxy) => proxy switch + { + Proxy.Twemproxy => false, + _ => true + }; + + /// + /// Whether a proxy supports the ConnectionMultiplexer.GetServer. + /// + public static bool SupportsServerApi(this Proxy proxy) => proxy switch + { + Proxy.Twemproxy => false, + _ => true + }; + } } diff --git a/src/StackExchange.Redis/Enums/ServerType.cs b/src/StackExchange.Redis/Enums/ServerType.cs index c3b6abaf4..b48c80aeb 100644 --- a/src/StackExchange.Redis/Enums/ServerType.cs +++ b/src/StackExchange.Redis/Enums/ServerType.cs @@ -22,4 +22,24 @@ public enum ServerType /// Twemproxy, } + + internal static class ServerTypeExtensions + { + /// + /// Whether a server type can have only a single primary, meaning an election if multiple are found. + /// + public static bool HasSinglePrimary(this ServerType type) => type switch + { + _ => true + }; + + /// + /// Whether a server type supports . + /// + public static bool SupportsAutoConfigure(this ServerType type) => type switch + { + ServerType.Twemproxy => false, + _ => true + }; + } } diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index 1d11e3fbb..00ba41307 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -107,7 +107,7 @@ public interface IConnectionMultiplexer : IDisposable /// /// Raised when nodes are explicitly requested to reconfigure via broadcast. - /// This usually means primary/replica role changes. + /// This usually means primary/replica changes. /// event EventHandler ConfigurationChangedBroadcast; diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 79e9b8d63..aa3f4c524 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -328,10 +328,10 @@ internal void AddScript(string script, byte[] hash) internal async Task AutoConfigureAsync(PhysicalConnection connection, LogProxy log = null) { - if (serverType == ServerType.Twemproxy) + if (!serverType.SupportsAutoConfigure()) { - // don't try to detect configuration; all the config commands are disabled, and - // the fallback master/replica detection won't help + // Don't try to detect configuration. + // All the config commands are disabled and the fallback primary/replica detection won't help return; } From 34ba699dda712623d61d8711bc061946b5545850 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 15 Feb 2022 10:08:25 -0500 Subject: [PATCH 078/435] Fix for #1967: Sanitizing message failures (#1990) There seems to be just 1 case not covered by the `IncludeDetailsInExceptions` option on `ConnectionMultiplexer` - this remedies that. The option is on by default so this shouldn't break people like I thought initially. Overall, we should probably also move this option to `ConfigurationOptions` and defaults if we do that (#1987). --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Message.cs | 15 ++++++++++--- src/StackExchange.Redis/PhysicalBridge.cs | 6 ++--- src/StackExchange.Redis/RedisBatch.cs | 10 ++++----- src/StackExchange.Redis/RedisTransaction.cs | 2 +- src/StackExchange.Redis/ResultProcessor.cs | 21 ++++++++++++------ .../ExceptionFactoryTests.cs | 22 +++++++++++++++++++ 7 files changed, 58 insertions(+), 19 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 396525601..3e8a84264 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -15,6 +15,7 @@ - Fixes subscription routing on clusters (spreading instead of choosing 1 node) - More correctly reconnects subscriptions on connection failures, including to other endpoints - Adds "(vX.X.X)" version suffix to the default client ID so server-side `CLIENT LIST` can more easily see what's connected (#1985 via NickCraver) +- Fix for including (or not including) key names on some message failures (#1990 via NickCraver) ## 2.2.88 diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index f99126f08..36befac70 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -403,9 +403,18 @@ public virtual void AppendStormLog(StringBuilder sb) /// public void SetInternalCall() => Flags |= InternalCallFlag; + /// + /// Gets a string representation of this message: "[{DB}]:{CommandAndKey} ({resultProcessor})" + /// public override string ToString() => $"[{Db}]:{CommandAndKey} ({resultProcessor?.GetType().Name ?? "(n/a)"})"; + /// + /// Gets a string representation of this message without the key: "[{DB}]:{Command} ({resultProcessor})" + /// + public string ToStringCommandOnly() => + $"[{Db}]:{Command} ({resultProcessor?.GetType().Name ?? "(n/a)"})"; + public void SetResponseReceived() => performance?.SetResponseReceived(); bool ICompletable.TryComplete(bool isAsync) { Complete(); return true; } @@ -562,10 +571,10 @@ internal bool ComputeResult(PhysicalConnection connection, in RawResult result) } } - internal void Fail(ConnectionFailureType failure, Exception innerException, string annotation) + internal void Fail(ConnectionFailureType failure, Exception innerException, string annotation, ConnectionMultiplexer muxer) { PhysicalConnection.IdentifyFailureType(innerException, ref failure); - resultProcessor?.ConnectionFail(this, failure, innerException, annotation); + resultProcessor?.ConnectionFail(this, failure, innerException, annotation, muxer); } internal virtual void SetExceptionAndComplete(Exception exception, PhysicalBridge bridge) @@ -717,7 +726,7 @@ internal void WriteTo(PhysicalConnection physical) catch (Exception ex) when (ex is not RedisCommandException) // these have specific meaning; don't wrap { physical?.OnInternalError(ex); - Fail(ConnectionFailureType.InternalFailure, ex, null); + Fail(ConnectionFailureType.InternalFailure, ex, null, physical?.BridgeCouldBeNull?.Multiplexer); } } diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 7d5b250ee..bf851c5bc 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -668,7 +668,7 @@ private WriteResult WriteMessageInsideLock(PhysicalConnection physical, Message // we screwed up; abort; note that WriteMessageToServer already // killed the underlying connection Trace("Unable to write to server"); - message.Fail(ConnectionFailureType.ProtocolFailure, null, "failure before write: " + result.ToString()); + message.Fail(ConnectionFailureType.ProtocolFailure, null, "failure before write: " + result.ToString(), Multiplexer); message.Complete(); return result; } @@ -1425,7 +1425,7 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne catch (RedisCommandException ex) when (!isQueued) { Trace("Write failed: " + ex.Message); - message.Fail(ConnectionFailureType.InternalFailure, ex, null); + message.Fail(ConnectionFailureType.InternalFailure, ex, null, Multiplexer); message.Complete(); // this failed without actually writing; we're OK with that... unless there's a transaction @@ -1440,7 +1440,7 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne catch (Exception ex) { Trace("Write failed: " + ex.Message); - message.Fail(ConnectionFailureType.InternalFailure, ex, null); + message.Fail(ConnectionFailureType.InternalFailure, ex, null, Multiplexer); message.Complete(); // we're not sure *what* happened here; probably an IOException; kill the connection diff --git a/src/StackExchange.Redis/RedisBatch.cs b/src/StackExchange.Redis/RedisBatch.cs index 810eca945..e9790202d 100644 --- a/src/StackExchange.Redis/RedisBatch.cs +++ b/src/StackExchange.Redis/RedisBatch.cs @@ -27,13 +27,13 @@ public void Execute() var server = multiplexer.SelectServer(message); if (server == null) { - FailNoServer(snapshot); + FailNoServer(multiplexer, snapshot); throw ExceptionFactory.NoConnectionAvailable(multiplexer, message, server); } var bridge = server.GetBridge(message); if (bridge == null) { - FailNoServer(snapshot); + FailNoServer(multiplexer, snapshot); throw ExceptionFactory.NoConnectionAvailable(multiplexer, message, server); } @@ -58,7 +58,7 @@ public void Execute() { if (!pair.Key.TryEnqueue(pair.Value, pair.Key.ServerEndPoint.IsReplica)) { - FailNoServer(pair.Value); + FailNoServer(multiplexer, pair.Value); } } } @@ -89,12 +89,12 @@ internal override Task ExecuteAsync(Message message, ResultProcessor pr internal override T ExecuteSync(Message message, ResultProcessor processor, ServerEndPoint server = null) => throw new NotSupportedException("ExecuteSync cannot be used inside a batch"); - private static void FailNoServer(List messages) + private static void FailNoServer(ConnectionMultiplexer muxer, List messages) { if (messages == null) return; foreach(var msg in messages) { - msg.Fail(ConnectionFailureType.UnableToResolvePhysicalConnection, null, "unable to write batch"); + msg.Fail(ConnectionFailureType.UnableToResolvePhysicalConnection, null, "unable to write batch", muxer); msg.Complete(); } } diff --git a/src/StackExchange.Redis/RedisTransaction.cs b/src/StackExchange.Redis/RedisTransaction.cs index 337658430..35943c80f 100644 --- a/src/StackExchange.Redis/RedisTransaction.cs +++ b/src/StackExchange.Redis/RedisTransaction.cs @@ -523,7 +523,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { if (op?.Wrapped is Message inner) { - inner.Fail(ConnectionFailureType.ProtocolFailure, null, "Transaction failure"); + inner.Fail(ConnectionFailureType.ProtocolFailure, null, "Transaction failure", connection?.BridgeCouldBeNull?.Multiplexer); inner.Complete(); } } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index a69dc6880..44149497e 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Net; +using System.Text; using System.Text.RegularExpressions; using Pipelines.Sockets.Unofficial.Arenas; @@ -150,16 +151,22 @@ public static readonly HashEntryArrayProcessor HashEntryArray = new HashEntryArrayProcessor(); [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Conditionally run on instance")] - public void ConnectionFail(Message message, ConnectionFailureType fail, Exception innerException, string annotation) + public void ConnectionFail(Message message, ConnectionFailureType fail, Exception innerException, string annotation, ConnectionMultiplexer muxer) { PhysicalConnection.IdentifyFailureType(innerException, ref fail); - string exMessage = fail.ToString() + (message == null ? "" : (" on " + ( - fail == ConnectionFailureType.ProtocolFailure ? message.ToString() : message.CommandAndKey))); - if (!string.IsNullOrWhiteSpace(annotation)) exMessage += ", " + annotation; - - var ex = innerException == null ? new RedisConnectionException(fail, exMessage) - : new RedisConnectionException(fail, exMessage, innerException); + var sb = new StringBuilder(fail.ToString()); + if (message is not null) + { + sb.Append(" on "); + sb.Append(muxer?.IncludeDetailInExceptions == true ? message.ToString() : message.ToStringCommandOnly()); + } + if (!string.IsNullOrWhiteSpace(annotation)) + { + sb.Append(", "); + sb.Append(annotation); + } + var ex = new RedisConnectionException(fail, sb.ToString(), innerException); SetException(message, ex); } diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 8c84227be..ab19e60c4 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -212,5 +212,27 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple ClearAmbientFailures(); } } + + [Theory] + [InlineData(true, ConnectionFailureType.ProtocolFailure, "ProtocolFailure on [0]:GET myKey (StringProcessor), my annotation")] + [InlineData(true, ConnectionFailureType.ConnectionDisposed, "ConnectionDisposed on [0]:GET myKey (StringProcessor), my annotation")] + [InlineData(false, ConnectionFailureType.ProtocolFailure, "ProtocolFailure on [0]:GET (StringProcessor), my annotation")] + [InlineData(false, ConnectionFailureType.ConnectionDisposed, "ConnectionDisposed on [0]:GET (StringProcessor), my annotation")] + public void MessageFail(bool includeDetail, ConnectionFailureType failType, string messageStart) + { + using var muxer = Create(shared: false); + muxer.IncludeDetailInExceptions = includeDetail; + + var message = Message.Create(0, CommandFlags.None, RedisCommand.GET, (RedisKey)"myKey"); + var resultBox = SimpleResultBox.Create(); + message.SetSource(ResultProcessor.String, resultBox); + + message.Fail(failType, null, "my annotation", muxer as ConnectionMultiplexer); + + resultBox.GetResult(out var ex); + Assert.NotNull(ex); + + Assert.StartsWith(messageStart, ex.Message); + } } } From 4fb787cc09ad12bdad536a616e262cd739ff00ec Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 15 Feb 2022 10:17:22 -0500 Subject: [PATCH 079/435] Fix for #1564: Return empty array instead of single element nil array in value array processor (#1993) This changes the return of a nil result through the `RedisValueArrayProcessor` so that it's a `[]` instead of a single element `[ nil ]` in our handling. This affects the following commands, which are multibulk when a count is provided (even if it's 1), otherwise they are bulkstring. This change affects the non-count case when it's null. Instead of a single element array with a nil value, we'd return an empty array as the Redis surface area intends: - `LPOP`/`RPOP` - `SRANDMEMBER` - `SPOP` The other usages of `RedisValueArrayProcessor` are _always_ multibulk and are not affected: - `HMGET` - `HKEYS` - `HVALS` - `LRANGE` - `MGET` - `SDIFF` - `SINTER` - `SUNION` - `SMEMBERS` - `SORT` - `XCLAIM` - `Z(REV)RANGE` - `Z(REV)RANGEBYLEX` - `Z(REV)RANGEBYSCORE` --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ResultProcessor.cs | 5 ++++- tests/StackExchange.Redis.Tests/Sets.cs | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 3e8a84264..c3824cad7 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -16,6 +16,7 @@ - More correctly reconnects subscriptions on connection failures, including to other endpoints - Adds "(vX.X.X)" version suffix to the default client ID so server-side `CLIENT LIST` can more easily see what's connected (#1985 via NickCraver) - Fix for including (or not including) key names on some message failures (#1990 via NickCraver) +- Fixed return of nil results in `LPOP`, `RPOP`, `SRANDMEMBER`, and `SPOP` (#1993 via NickCraver) ## 2.2.88 diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 44149497e..085cd7ac7 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -1249,7 +1249,10 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { // allow a single item to pass explicitly pretending to be an array; example: SPOP {key} 1 case ResultType.BulkString: - var arr = new[] { result.AsRedisValue() }; + // If the result is nil, the result should be an empty array + var arr = result.IsNull + ? Array.Empty() + : new[] { result.AsRedisValue() }; SetResult(message, arr); return true; case ResultType.MultiBulk: diff --git a/tests/StackExchange.Redis.Tests/Sets.cs b/tests/StackExchange.Redis.Tests/Sets.cs index bcf43fede..c028f1cc9 100644 --- a/tests/StackExchange.Redis.Tests/Sets.cs +++ b/tests/StackExchange.Redis.Tests/Sets.cs @@ -84,6 +84,7 @@ public void SetPopMulti_Multi() Assert.Equal(7, db.SetLength(key)); } } + [Fact] public void SetPopMulti_Single() { @@ -225,5 +226,22 @@ public async Task SetAdd_Zero_Async() Assert.Equal(0, db.SetLength(key)); } } + + [Fact] + public void SetPopMulti_Nil() + { + using (var conn = Create()) + { + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SetPopMultiple), r => r.SetPopMultiple); + + var db = conn.GetDatabase(); + var key = Me(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + + var arr = db.SetPop(key, 1); + Assert.Empty(arr); + } + } } } From cbc7cc9e3ef26ac7998d0a844385bfb7c6dc93a7 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 15 Feb 2022 10:34:07 -0500 Subject: [PATCH 080/435] Rollback: remove .NET 6 build (#1992) This removes the .NET 6 specific build from the main library. In testing many previous performance issues filed around past releases (vs. 1.2.6), I was testing this example: ```cs using StackExchange.Redis; using System; using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; var options = ConfigurationOptions.Parse("localhost,abortConnect=false"); using ConnectionMultiplexer muxer = ConnectionMultiplexer.Connect(options); var subscriber = muxer.GetSubscriber(); int count = 0; var sw = Stopwatch.StartNew(); Parallel.For(0, 1000, x => { subscriber.Publish("cache-events:cache-testing", "hey"); Interlocked.Increment(ref count); }); sw.Stop(); Console.WriteLine($"Total in {sw.ElapsedMilliseconds} ms"); ``` This parallel thread contention state is one of the things we hoped the .NET 6 behavior for thread pools would help with. It comes from 2 main factors: defaulting to the primary thread pool (rather than our own) and the backlog moving back to `Task.Run` instead of a thread (which has ~11% impact on throughput due to startup cost). This happens here: - [Socket Manager -> ThreadPool](https://github.com/StackExchange/StackExchange.Redis/blob/74adca8d69ced316b2d4f57d2e8375d40de06335/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs#L29-L33) - [PhysicalBridge backlog](https://github.com/StackExchange/StackExchange.Redis/blob/74adca8d69ced316b2d4f57d2e8375d40de06335/src/StackExchange.Redis/PhysicalBridge.cs#L821-L824) In the `net5.0` build the sample takes ~100-105ms on an 8-core machine, running under .NET 6. With the `net6.0` build in play (before this PR) and using those changes, we end up in the 20,000-30,000ms territory. With only the socket manager change (leaving the Backlog as a `Task.Run`), we're in the 400-500ms territory. In short, trying to use the .NET 6 changes to our advantage here greatly regresses some use cases drastically enough I advise we do not enable it. We should look at these cases further before adding the `net6.0` paths to any release build. I can go further than this PR and remove the code paths completely, but I'd like to have these in play easily to discuss with the .NET team as a use case to look at and get some advice. cc @stephentoub on this one (happy to sync up and help repro) --- src/StackExchange.Redis/StackExchange.Redis.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 9f2fbb9cd..852fed58e 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -1,7 +1,8 @@  - net461;netstandard2.0;net472;netcoreapp3.1;net5.0;net6.0 + + net461;netstandard2.0;net472;netcoreapp3.1;net5.0 High performance Redis client, incorporating both synchronous and asynchronous usage. StackExchange.Redis StackExchange.Redis From 93363aa5885b6006d9d9bb7d423ddf7f94073c01 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 18 Feb 2022 16:53:40 -0500 Subject: [PATCH 081/435] v2.5: Docs! (#1999) --- docs/Configuration.md | 3 ++- docs/ReleaseNotes.md | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 443120ec2..6f7885d9b 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -99,7 +99,8 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a | | `CheckCertificateRevocation` | `true` | A Boolean value that specifies whether the certificate revocation list is checked during authentication. | Additional code-only options: -- ReconnectRetryPolicy (`IReconnectRetryPolicy`) - Default: `ReconnectRetryPolicy = LinearRetry(ConnectTimeout);` +- ReconnectRetryPolicy (`IReconnectRetryPolicy`) - Default: `ReconnectRetryPolicy = ExponentialRetry(ConnectTimeout / 2);` +- BacklogPolicy - Default: `BacklogPolicy = BacklogPolicy.Default;` Tokens in the configuration string are comma-separated; any without an `=` sign are assumed to be redis server endpoints. Endpoints without an explicit port will use 6379 if ssl is not enabled, and 6380 if ssl is enabled. Tokens starting with `$` are taken to represent command maps, for example: `$config=cfg`. diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index c3824cad7..8cf47951d 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,7 +1,12 @@ # Release Notes -## Unreleased +## 2.5.27 (prerelease) +- Adds a backlog/retry mechanism for commands issued while a connection isn't available (#1912 via NickCraver) + - Commands will be queued if a multiplexer isn't yet connected to a Redis server. + - Commands will be queued if a connection is lost and then sent to the server when the connection is restored. + - All commands queued will only remain in the backlog for the duration of the configured timeout. + - To revert to previous behavior, a new `ConfigurationOptions.BacklogPolicy` is available - old behavior is configured via `options.BacklogPolicy = BacklogPolicy.FailFast`. This backlogs nothing and fails commands immediately if no connection is available. - Makes `StreamEntry` constructor public for better unit test experience (#1923 via WeihanLi) - Fix integer overflow error (issue #1926) with 2GiB+ result payloads (#1928 via mgravell) - Update assumed redis versions to v2.8 or v4.0 in the Azure case (#1929 via NickCraver) From a65b43236d6df44053281f5973ce0a8836b617d3 Mon Sep 17 00:00:00 2001 From: Gunnar Liljas Date: Fri, 18 Feb 2022 23:54:34 +0100 Subject: [PATCH 082/435] Ensure valid timeout settings in ExponentialRetry (#1921) Setting a timeout from the connectionstring can cause `deltaBackOffMilliseconds `to be higher than `maxDeltaBackOffMilliseconds`, which will cause an `ArgumentOutOfRangeException `('minValue' cannot be greater than maxValue.) in `ShouldRetry`. --- docs/ReleaseNotes.md | 3 +++ src/StackExchange.Redis/ExponentialRetry.cs | 15 ++++++++++++++- .../ConnectionReconnectRetryPolicyTests.cs | 18 +++++++++++++++++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 8cf47951d..700d002cc 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,5 +1,8 @@ # Release Notes +## Unreleased +- Adds bounds checking for `ExponentialRetry` backoff policy (#1921 via gliljas) + ## 2.5.27 (prerelease) - Adds a backlog/retry mechanism for commands issued while a connection isn't available (#1912 via NickCraver) diff --git a/src/StackExchange.Redis/ExponentialRetry.cs b/src/StackExchange.Redis/ExponentialRetry.cs index e957a3f19..fd7f908a4 100644 --- a/src/StackExchange.Redis/ExponentialRetry.cs +++ b/src/StackExchange.Redis/ExponentialRetry.cs @@ -16,7 +16,7 @@ public class ExponentialRetry : IReconnectRetryPolicy /// Initializes a new instance using the specified back off interval with default maxDeltaBackOffMilliseconds of 10 seconds. /// /// time in milliseconds for the back-off interval between retries - public ExponentialRetry(int deltaBackOffMilliseconds) : this(deltaBackOffMilliseconds, (int)TimeSpan.FromSeconds(10).TotalMilliseconds) {} + public ExponentialRetry(int deltaBackOffMilliseconds) : this(deltaBackOffMilliseconds, Math.Max(deltaBackOffMilliseconds, (int)TimeSpan.FromSeconds(10).TotalMilliseconds)) { } /// /// Initializes a new instance using the specified back off interval. @@ -25,6 +25,19 @@ public ExponentialRetry(int deltaBackOffMilliseconds) : this(deltaBackOffMillise /// time in milliseconds for the maximum value that the back-off interval can exponentially grow up to. public ExponentialRetry(int deltaBackOffMilliseconds, int maxDeltaBackOffMilliseconds) { + if (deltaBackOffMilliseconds < 0) + { + throw new ArgumentOutOfRangeException(nameof(deltaBackOffMilliseconds), $"{nameof(deltaBackOffMilliseconds)} must be greater than or equal to zero"); + } + if (maxDeltaBackOffMilliseconds < 0) + { + throw new ArgumentOutOfRangeException(nameof(maxDeltaBackOffMilliseconds), $"{nameof(maxDeltaBackOffMilliseconds)} must be greater than or equal to zero"); + } + if (maxDeltaBackOffMilliseconds < deltaBackOffMilliseconds) + { + throw new ArgumentOutOfRangeException(nameof(maxDeltaBackOffMilliseconds), $"{nameof(maxDeltaBackOffMilliseconds)} must be greater than or equal to {nameof(deltaBackOffMilliseconds)}"); + } + this.deltaBackOffMilliseconds = deltaBackOffMilliseconds; this.maxDeltaBackOffMilliseconds = maxDeltaBackOffMilliseconds; } diff --git a/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs b/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs index a3d0e95bc..cdce5dd76 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs @@ -25,6 +25,22 @@ public void TestExponentialMaxRetry() Assert.True(exponentialRetry.ShouldRetry(long.MaxValue, (int)TimeSpan.FromSeconds(30).TotalMilliseconds)); } + [Fact] + public void TestExponentialRetryArgs() + { + new ExponentialRetry(5000); + new ExponentialRetry(5000, 10000); + + var ex = Assert.Throws(() => new ExponentialRetry(-1)); + Assert.Equal("deltaBackOffMilliseconds", ex.ParamName); + + ex = Assert.Throws(() => new ExponentialRetry(5000, -1)); + Assert.Equal("maxDeltaBackOffMilliseconds", ex.ParamName); + + ex = Assert.Throws(() => new ExponentialRetry(10000, 5000)); + Assert.Equal("maxDeltaBackOffMilliseconds", ex.ParamName); + } + [Fact] public void TestLinearRetry() { @@ -34,4 +50,4 @@ public void TestLinearRetry() Assert.True(linearRetry.ShouldRetry(1, 5000)); } } -} \ No newline at end of file +} From 1faa51dc1689e53f34d1c781895a812f4bc8fe7c Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 19 Feb 2022 14:41:36 -0500 Subject: [PATCH 083/435] Tests: Improve QueuesAndFlushesAfterReconnectingClusterAsync (#2004) Make this a bit more resilient as the cluster changes members from other tests firing in CI. --- tests/StackExchange.Redis.Tests/BacklogTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/StackExchange.Redis.Tests/BacklogTests.cs b/tests/StackExchange.Redis.Tests/BacklogTests.cs index 5cce8a407..021eb8267 100644 --- a/tests/StackExchange.Redis.Tests/BacklogTests.cs +++ b/tests/StackExchange.Redis.Tests/BacklogTests.cs @@ -326,7 +326,10 @@ public async Task QueuesAndFlushesAfterReconnectingClusterAsync() RedisKey meKey = Me(); var getMsg = Message.Create(0, CommandFlags.None, RedisCommand.GET, meKey); - var server = muxer.SelectServer(getMsg); // Get the server specifically for this message's hash slot + ServerEndPoint server = null; // Get the server specifically for this message's hash slot + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => (server = muxer.SelectServer(getMsg)) != null); + + Assert.NotNull(server); var stats = server.GetBridgeStatus(ConnectionType.Interactive); Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal From 5e2b43ced8e8b9c9ffbcd13b0b5e9fa9ffa38fef Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 22 Feb 2022 08:29:28 -0500 Subject: [PATCH 084/435] Misc connection/subscription fixes (#2001) In investigating an issue in #1989, I found a few gaps. Overall: 1. Twemproxy has an out of date CommandMap, which propagated to Envoy. 2. We were expecting both interactive and subscription connections to complete to complete the async handler...but we shouldn't because subscriptions may be disabled. 3. RedisSubscriber changes on the sync path weren't validating the message (asserting the command map has it enabled). This fixes all of the above and adds another test considering all 3. --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/CommandMap.cs | 12 +++--------- src/StackExchange.Redis/ConnectionMultiplexer.cs | 2 +- src/StackExchange.Redis/RedisSubscriber.cs | 2 +- src/StackExchange.Redis/ServerEndPoint.cs | 5 ++++- tests/StackExchange.Redis.Tests/Config.cs | 15 +++++++++++++++ 6 files changed, 25 insertions(+), 12 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 700d002cc..dcd54707e 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -2,6 +2,7 @@ ## Unreleased - Adds bounds checking for `ExponentialRetry` backoff policy (#1921 via gliljas) +- When `SUBSCRIBE` is disabled, give proper errors and connect faster (#2001 via NickCraver) ## 2.5.27 (prerelease) diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index b37a51512..617c8d22f 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -26,25 +26,19 @@ public sealed class CommandMap { // see https://github.com/twitter/twemproxy/blob/master/notes/redis.md RedisCommand.KEYS, RedisCommand.MIGRATE, RedisCommand.MOVE, RedisCommand.OBJECT, RedisCommand.RANDOMKEY, - RedisCommand.RENAME, RedisCommand.RENAMENX, RedisCommand.SORT, RedisCommand.SCAN, + RedisCommand.RENAME, RedisCommand.RENAMENX, RedisCommand.SCAN, - RedisCommand.BITOP, RedisCommand.MSET, RedisCommand.MSETNX, - - RedisCommand.HSCAN, + RedisCommand.BITOP, RedisCommand.MSETNX, RedisCommand.BLPOP, RedisCommand.BRPOP, RedisCommand.BRPOPLPUSH, // yeah, me neither! - RedisCommand.SSCAN, - - RedisCommand.ZSCAN, - RedisCommand.PSUBSCRIBE, RedisCommand.PUBLISH, RedisCommand.PUNSUBSCRIBE, RedisCommand.SUBSCRIBE, RedisCommand.UNSUBSCRIBE, RedisCommand.DISCARD, RedisCommand.EXEC, RedisCommand.MULTI, RedisCommand.UNWATCH, RedisCommand.WATCH, RedisCommand.SCRIPT, - RedisCommand.ECHO, RedisCommand.PING, RedisCommand.QUIT, RedisCommand.SELECT, + RedisCommand.ECHO, RedisCommand.PING, RedisCommand.SELECT, RedisCommand.BGREWRITEAOF, RedisCommand.BGSAVE, RedisCommand.CLIENT, RedisCommand.CLUSTER, RedisCommand.CONFIG, RedisCommand.DBSIZE, RedisCommand.DEBUG, RedisCommand.FLUSHALL, RedisCommand.FLUSHDB, RedisCommand.INFO, RedisCommand.LASTSAVE, RedisCommand.MONITOR, RedisCommand.REPLICAOF, diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index a8834bc64..2bf4e493a 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1654,7 +1654,7 @@ private void ActivateAllServers(LogProxy log) foreach (var server in GetServerSnapshot()) { server.Activate(ConnectionType.Interactive, log); - if (CommandMap.IsAvailable(RedisCommand.SUBSCRIBE)) + if (server.SupportsSubscriptions) { // Intentionally not logging the sub connection server.Activate(ConnectionType.Subscription, null); diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index 0bd86467e..696b68204 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -378,7 +378,7 @@ internal bool EnsureSubscribedToServer(Subscription sub, RedisChannel channel, C sub.SetCurrentServer(null); // we're not appropriately connected, so blank it out for eligible reconnection var message = sub.GetMessage(channel, SubscriptionAction.Subscribe, flags, internalCall); var selected = multiplexer.SelectServer(message); - return multiplexer.ExecuteSyncImpl(message, sub.Processor, selected); + return ExecuteSync(message, sub.Processor, selected); } Task ISubscriber.SubscribeAsync(RedisChannel channel, Action handler, CommandFlags flags) diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index aa3f4c524..f2db0ebd8 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -75,6 +75,8 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) public bool IsConnected => interactive?.IsConnected == true; public bool IsSubscriberConnected => subscription?.IsConnected == true; + public bool SupportsSubscriptions => Multiplexer.CommandMap.IsAvailable(RedisCommand.SUBSCRIBE); + public bool IsConnecting => interactive?.IsConnecting == true; private readonly List> _pendingConnectionMonitors = new List>(); @@ -629,9 +631,10 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) // Since we're issuing commands inside a SetResult path in a message, we'd create a deadlock by waiting. Multiplexer.EnsureSubscriptions(CommandFlags.FireAndForget); } - if (IsConnected && IsSubscriberConnected) + if (IsConnected && (IsSubscriberConnected || !SupportsSubscriptions)) { // Only connect on the second leg - we can accomplish this by checking both + // Or the first leg, if we're only making 1 connection because subscriptions aren't supported CompletePendingConnectionMonitors(source); } diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index 271e8a386..0494874c8 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -263,6 +263,21 @@ public void ReadConfigWithConfigDisabled() } } + [Fact] + public void ConnectWithSubscribeDisabled() + { + using (var muxer = Create(allowAdmin: true, disabledCommands: new[] { "subscribe" })) + { + Assert.True(muxer.IsConnected); + var servers = muxer.GetServerSnapshot(); + Assert.True(servers[0].IsConnected); + Assert.False(servers[0].IsSubscriberConnected); + + var ex = Assert.Throws(() => muxer.GetSubscriber().Subscribe(Me(), (_, _) => GC.KeepAlive(this))); + Assert.Equal("This operation has been disabled in the command-map and cannot be used: SUBSCRIBE", ex.Message); + } + } + [Fact] public void ReadConfig() { From 8197e970512b9cd762e66f65b8df9995191723a5 Mon Sep 17 00:00:00 2001 From: Karthick Ramachandran Date: Tue, 22 Feb 2022 08:33:32 -0800 Subject: [PATCH 085/435] support for envoyproxy (#1989) Envoy proxy for redis: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_protocols/redis Envoy does the heavy lifting of redis cluster instance discovery and partitioning commands among redis cluster instances from the clients to a set of proxy instances. Clients can round robin or randomly select a proxy to connect to and the proxy will do the right thing of picking the right cluster instance for a given command. Apart from the glue of wiring up the new proxy in Enums and disabling pub/sub for the proxy (similar to Twemproxy), this PR contains the following major change in the primary node selection; When an envoy proxy mode is selected for connection, the check for verifying if the node is master or replica is disabled. In the envoy proxy world we can connect to any proxy instance. All we care about here is ensuring that the load is balanced between the proxy instances (which is accomplished through the current round-robin logic). @mgravell raised a valid point offline that the order of operations wouldn't be guaranteed in this mode and this is indeed one of the limitation of this approach. The commands that form the foundation for redis transactions (exec, discard, multi, watch, unwatch) (https://redis.io/topics/transactions) are not supported by Envoy (https://github.com/envoyproxy/envoy/blob/main/source/extensions/filters/network/common/redis/supported_commands.h) and hence added to the exclusion list in PR. The testing is inspired from this PR: https://github.com/StackExchange/StackExchange.Redis/pull/1425. Open to suggestions on that. Co-authored-by: Nick Craver --- docs/Configuration.md | 13 ++++- src/StackExchange.Redis/CommandMap.cs | 34 ++++++++++++- .../ConfigurationOptions.cs | 1 + .../ConnectionMultiplexer.cs | 6 +++ src/StackExchange.Redis/Enums/Proxy.cs | 7 +++ src/StackExchange.Redis/Enums/ServerType.cs | 6 +++ src/StackExchange.Redis/PhysicalConnection.cs | 2 +- src/StackExchange.Redis/RedisBase.cs | 2 +- src/StackExchange.Redis/ServerEndPoint.cs | 15 ++++-- .../ServerSelectionStrategy.cs | 7 +-- tests/RedisConfigs/Docker/install-envoy.sh | 9 ++++ tests/RedisConfigs/Docker/supervisord.conf | 7 +++ tests/RedisConfigs/Dockerfile | 10 +++- tests/RedisConfigs/Envoy/envoy.yaml | 49 +++++++++++++++++++ tests/RedisConfigs/docker-compose.yml | 1 + tests/RedisConfigs/start-all.sh | 11 ++++- tests/RedisConfigs/start-cluster.cmd | 4 +- tests/StackExchange.Redis.Tests/EnvoyTests.cs | 43 ++++++++++++++++ .../Helpers/TestConfig.cs | 5 ++ 19 files changed, 217 insertions(+), 15 deletions(-) create mode 100644 tests/RedisConfigs/Docker/install-envoy.sh create mode 100644 tests/RedisConfigs/Envoy/envoy.yaml create mode 100644 tests/StackExchange.Redis.Tests/EnvoyTests.cs diff --git a/docs/Configuration.md b/docs/Configuration.md index 6f7885d9b..cce38dc17 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -86,7 +86,7 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a | name={string} | `ClientName` | `null` | Identification for the connection within redis | | password={string} | `Password` | `null` | Password for the redis server | | user={string} | `User` | `null` | User for the redis server (for use with ACLs on redis 6 and above) | -| proxy={proxy type} | `Proxy` | `Proxy.None` | Type of proxy in use (if any); for example "twemproxy" | +| proxy={proxy type} | `Proxy` | `Proxy.None` | Type of proxy in use (if any); for example "twemproxy/envoyproxy" | | resolveDns={bool} | `ResolveDns` | `false` | Specifies that DNS resolution should be explicit and eager, rather than implicit | | serviceName={string} | `ServiceName` | `null` | Used for connecting to a sentinel master service | | ssl={bool} | `Ssl` | `false` | Specifies that SSL encryption should be used | @@ -179,6 +179,17 @@ var options = new ConfigurationOptions }; ``` +envoyproxy +--- +[Envoyproxy](https://github.com/envoyproxy/envoy) is a tool that allows to front a redis cluster with a set of proxies, with inbuilt discovery and fault tolerance. The feature-set available to Envoyproxy is reduced. To avoid having to configure this manually, the `Proxy` option can be used: +```csharp +var options = new ConfigurationOptions+{ + EndPoints = { "my-proxy1", "my-proxy2", "my-proxy3" }, + Proxy = Proxy.Envoyproxy +}; +``` + + Tiebreakers and Configuration Change Announcements --- diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index 617c8d22f..042ccb4f9 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -38,13 +38,45 @@ public sealed class CommandMap RedisCommand.SCRIPT, - RedisCommand.ECHO, RedisCommand.PING, RedisCommand.SELECT, + RedisCommand.ECHO, RedisCommand.SELECT, RedisCommand.BGREWRITEAOF, RedisCommand.BGSAVE, RedisCommand.CLIENT, RedisCommand.CLUSTER, RedisCommand.CONFIG, RedisCommand.DBSIZE, RedisCommand.DEBUG, RedisCommand.FLUSHALL, RedisCommand.FLUSHDB, RedisCommand.INFO, RedisCommand.LASTSAVE, RedisCommand.MONITOR, RedisCommand.REPLICAOF, RedisCommand.SAVE, RedisCommand.SHUTDOWN, RedisCommand.SLAVEOF, RedisCommand.SLOWLOG, RedisCommand.SYNC, RedisCommand.TIME }); + /// + /// The commands available to envoyproxy. + /// + public static CommandMap Envoyproxy { get; } = CreateImpl(null, exclusions: new HashSet + { + // see https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_protocols/redis.html?highlight=redis + RedisCommand.KEYS, RedisCommand.MIGRATE, RedisCommand.MOVE, RedisCommand.OBJECT, RedisCommand.RANDOMKEY, + RedisCommand.RENAME, RedisCommand.RENAMENX, RedisCommand.SORT, RedisCommand.SCAN, + + RedisCommand.BITOP, RedisCommand.MSETNX, + + RedisCommand.BLPOP, RedisCommand.BRPOP, RedisCommand.BRPOPLPUSH, // yeah, me neither! + + RedisCommand.PSUBSCRIBE, RedisCommand.PUBLISH, RedisCommand.PUNSUBSCRIBE, RedisCommand.SUBSCRIBE, RedisCommand.UNSUBSCRIBE, + + RedisCommand.DISCARD, RedisCommand.EXEC, RedisCommand.MULTI, RedisCommand.UNWATCH, RedisCommand.WATCH, + + RedisCommand.SCRIPT, + + RedisCommand.ECHO, RedisCommand.QUIT, RedisCommand.SELECT, + + RedisCommand.BGREWRITEAOF, RedisCommand.BGSAVE, RedisCommand.CLIENT, RedisCommand.CLUSTER, RedisCommand.CONFIG, RedisCommand.DBSIZE, + RedisCommand.DEBUG, RedisCommand.FLUSHALL, RedisCommand.FLUSHDB, RedisCommand.INFO, RedisCommand.LASTSAVE, RedisCommand.MONITOR, RedisCommand.REPLICAOF, + RedisCommand.SAVE, RedisCommand.SHUTDOWN, RedisCommand.SLAVEOF, RedisCommand.SLOWLOG, RedisCommand.SYNC, RedisCommand.TIME, + + // supported by envoy but not enabled by stack exchange + // RedisCommand.BITFIELD, + // + // RedisCommand.GEORADIUS_RO, + // RedisCommand.GEORADIUSBYMEMBER_RO, + }); + /// /// The commands available to SSDB. /// diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index a22d23a52..887d2a45c 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -275,6 +275,7 @@ public CommandMap CommandMap get => commandMap ?? Proxy switch { Proxy.Twemproxy => CommandMap.Twemproxy, + Proxy.Envoyproxy => CommandMap.Envoyproxy, _ => CommandMap.Default, }; set => commandMap = value ?? throw new ArgumentNullException(nameof(value)); diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 2bf4e493a..177645ed9 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1810,6 +1810,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP switch (server.ServerType) { case ServerType.Twemproxy: + case ServerType.Envoyproxy: case ServerType.Standalone: standaloneCount++; break; @@ -1834,6 +1835,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP switch (server.ServerType) { case ServerType.Twemproxy: + case ServerType.Envoyproxy: case ServerType.Sentinel: case ServerType.Standalone: case ServerType.Cluster: @@ -1882,6 +1884,10 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP { ServerSelectionStrategy.ServerType = ServerType.Twemproxy; } + else if (RawConfig.Proxy == Proxy.Envoyproxy) + { + ServerSelectionStrategy.ServerType = ServerType.Envoyproxy; + } else if (standaloneCount == 0 && sentinelCount > 0) { ServerSelectionStrategy.ServerType = ServerType.Sentinel; diff --git a/src/StackExchange.Redis/Enums/Proxy.cs b/src/StackExchange.Redis/Enums/Proxy.cs index 38a71a8e1..59ab941cf 100644 --- a/src/StackExchange.Redis/Enums/Proxy.cs +++ b/src/StackExchange.Redis/Enums/Proxy.cs @@ -13,6 +13,10 @@ public enum Proxy /// Communication via twemproxy. /// Twemproxy, + /// + /// Communication via envoyproxy. + /// + Envoyproxy, } internal static class ProxyExtensions @@ -23,6 +27,7 @@ internal static class ProxyExtensions public static bool SupportsDatabases(this Proxy proxy) => proxy switch { Proxy.Twemproxy => false, + Proxy.Envoyproxy => false, _ => true }; @@ -32,6 +37,7 @@ internal static class ProxyExtensions public static bool SupportsPubSub(this Proxy proxy) => proxy switch { Proxy.Twemproxy => false, + Proxy.Envoyproxy => false, _ => true }; @@ -41,6 +47,7 @@ internal static class ProxyExtensions public static bool SupportsServerApi(this Proxy proxy) => proxy switch { Proxy.Twemproxy => false, + Proxy.Envoyproxy => false, _ => true }; } diff --git a/src/StackExchange.Redis/Enums/ServerType.cs b/src/StackExchange.Redis/Enums/ServerType.cs index b48c80aeb..1d86ae83c 100644 --- a/src/StackExchange.Redis/Enums/ServerType.cs +++ b/src/StackExchange.Redis/Enums/ServerType.cs @@ -21,6 +21,10 @@ public enum ServerType /// Distributed redis installation via twemproxy. /// Twemproxy, + /// + /// Redis cluster via envoyproxy. + /// + Envoyproxy, } internal static class ServerTypeExtensions @@ -30,6 +34,7 @@ internal static class ServerTypeExtensions /// public static bool HasSinglePrimary(this ServerType type) => type switch { + ServerType.Envoyproxy => false, _ => true }; @@ -39,6 +44,7 @@ internal static class ServerTypeExtensions public static bool SupportsAutoConfigure(this ServerType type) => type switch { ServerType.Twemproxy => false, + ServerType.Envoyproxy => false, _ => true }; } diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 10da21fe1..4b32ecfee 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -573,7 +573,7 @@ internal Message GetSelectDatabaseCommand(int targetDatabase, Message message) } int available = serverEndpoint.Databases; - if (!serverEndpoint.HasDatabases) // only db0 is available on cluster/twemproxy + if (!serverEndpoint.HasDatabases) // only db0 is available on cluster/twemproxy/envoyproxy { if (targetDatabase != 0) { // should never see this, since the API doesn't allow it; thus not too worried about ExceptionFactory diff --git a/src/StackExchange.Redis/RedisBase.cs b/src/StackExchange.Redis/RedisBase.cs index 492e7f120..10544a024 100644 --- a/src/StackExchange.Redis/RedisBase.cs +++ b/src/StackExchange.Redis/RedisBase.cs @@ -108,7 +108,7 @@ private ResultProcessor.TimingProcessor.TimerMessage GetTimerMessage(CommandFlag if (map.IsAvailable(RedisCommand.ECHO)) return ResultProcessor.TimingProcessor.CreateMessage(-1, flags, RedisCommand.ECHO, RedisLiterals.PING); // as our fallback, we'll do something odd... we'll treat a key like a value, out of sheer desperation - // note: this usually means: twemproxy - in which case we're fine anyway, since the proxy does the routing + // note: this usually means: twemproxy/envoyproxy - in which case we're fine anyway, since the proxy does the routing return ResultProcessor.TimingProcessor.CreateMessage(0, flags, RedisCommand.EXISTS, (RedisValue)multiplexer.UniqueId); } diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index f2db0ebd8..2aaf6a63e 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -56,11 +56,18 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) writeEverySeconds = config.KeepAlive > 0 ? config.KeepAlive : 60; serverType = ServerType.Standalone; ConfigCheckSeconds = Multiplexer.RawConfig.ConfigCheckSeconds; - // overrides for twemproxy - if (multiplexer.RawConfig.Proxy == Proxy.Twemproxy) + + // overrides for twemproxy/envoyproxy + switch (multiplexer.RawConfig.Proxy) { - databases = 1; - serverType = ServerType.Twemproxy; + case Proxy.Twemproxy: + databases = 1; + serverType = ServerType.Twemproxy; + break; + case Proxy.Envoyproxy: + databases = 1; + serverType = ServerType.Envoyproxy; + break; } } diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index 0cfdf7fd0..70e5cb294 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -110,9 +110,10 @@ public ServerEndPoint Select(Message message, bool allowDisconnected = false) switch (ServerType) { case ServerType.Cluster: - case ServerType.Twemproxy: // strictly speaking twemproxy uses a different hashing algorithm, but the hash-tag behavior is - // the same, so this does a pretty good job of spotting illegal commands before sending them - + // strictly speaking some proxies use a different hashing algorithm, but the hash-tag behavior is + // the same, so this does a pretty good job of spotting illegal commands before sending them + case ServerType.Twemproxy: + case ServerType.Envoyproxy: slot = message.GetHashSlot(this); if (slot == MultipleSlots) throw ExceptionFactory.MultiSlot(multiplexer.IncludeDetailInExceptions, message); break; diff --git a/tests/RedisConfigs/Docker/install-envoy.sh b/tests/RedisConfigs/Docker/install-envoy.sh new file mode 100644 index 000000000..9bf7c9863 --- /dev/null +++ b/tests/RedisConfigs/Docker/install-envoy.sh @@ -0,0 +1,9 @@ +# instructions from https://www.envoyproxy.io/docs/envoy/latest/start/install +apt update +apt -y install debian-keyring debian-archive-keyring apt-transport-https curl lsb-release +curl -sL 'https://deb.dl.getenvoy.io/public/gpg.8115BA8E629CC074.key' | gpg --dearmor -o /usr/share/keyrings/getenvoy-keyring.gpg +echo "deb [arch=amd64 signed-by=/usr/share/keyrings/getenvoy-keyring.gpg] https://deb.dl.getenvoy.io/public/deb/debian $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/getenvoy.list +apt update +apt install getenvoy-envoy + + diff --git a/tests/RedisConfigs/Docker/supervisord.conf b/tests/RedisConfigs/Docker/supervisord.conf index 2b21cff9e..e29b937e8 100644 --- a/tests/RedisConfigs/Docker/supervisord.conf +++ b/tests/RedisConfigs/Docker/supervisord.conf @@ -92,6 +92,13 @@ stdout_logfile=/var/log/supervisor/%(program_name)s.log stderr_logfile=/var/log/supervisor/%(program_name)s.log autorestart=true +[program:redis-7015] +command=/usr/bin/envoy -c /envoy/envoy.yaml +directory=/envoy +stdout_logfile=/var/log/supervisor/%(program_name)s.log +stderr_logfile=/var/log/supervisor/%(program_name)s.log +autorestart=true + [program:sentinel-26379] command=/usr/local/bin/redis-server /data/Sentinel/sentinel-26379.conf --sentinel directory=/data/Sentinel diff --git a/tests/RedisConfigs/Dockerfile b/tests/RedisConfigs/Dockerfile index 047da2975..a37c99ca2 100644 --- a/tests/RedisConfigs/Dockerfile +++ b/tests/RedisConfigs/Dockerfile @@ -10,10 +10,16 @@ RUN chown -R redis:redis /data COPY Docker/docker-entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/docker-entrypoint.sh -RUN apt-get -y update && apt-get install -y git gcc make supervisor && apt-get clean +RUN apt-get -y update && apt-get install -y git gcc make supervisor +COPY Docker/install-envoy.sh /usr/local/bin +RUN sh /usr/local/bin/install-envoy.sh + +RUN apt-get clean + +COPY Envoy/envoy.yaml /envoy/envoy.yaml ADD Docker/supervisord.conf /etc/ ENTRYPOINT ["docker-entrypoint.sh"] -EXPOSE 6379 6380 6381 6382 6383 7000 7001 7002 7003 7004 7005 7010 7011 26379 26380 26381 +EXPOSE 6379 6380 6381 6382 6383 7000 7001 7002 7003 7004 7005 7010 7011 7015 26379 26380 26381 diff --git a/tests/RedisConfigs/Envoy/envoy.yaml b/tests/RedisConfigs/Envoy/envoy.yaml new file mode 100644 index 000000000..3f1c5c1a9 --- /dev/null +++ b/tests/RedisConfigs/Envoy/envoy.yaml @@ -0,0 +1,49 @@ +admin: + access_log_path: "/dev/null" + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 8001 +static_resources: + listeners: + - name: redis_listener + address: + socket_address: + address: 0.0.0.0 + port_value: 7015 + filter_chains: + - filters: + - name: envoy.filters.network.redis_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.redis_proxy.v3.RedisProxy + stat_prefix: envoy_redis_stats + settings: + op_timeout: 5s + enable_redirection: true + prefix_routes: + catch_all_route: + cluster: redis_cluster + clusters: + - name: redis_cluster + connect_timeout: 1s + dns_lookup_family: V4_ONLY + lb_policy: CLUSTER_PROVIDED + load_assignment: + cluster_name: redis_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 7000 + cluster_type: + name: envoy.clusters.redis + typed_config: + "@type": type.googleapis.com/google.protobuf.Struct + value: + cluster_refresh_rate: 30s + cluster_refresh_timeout: 0.5s + redirect_refresh_interval: 10s + redirect_refresh_threshold: 10 diff --git a/tests/RedisConfigs/docker-compose.yml b/tests/RedisConfigs/docker-compose.yml index cb3dd099c..afc4cbf3c 100644 --- a/tests/RedisConfigs/docker-compose.yml +++ b/tests/RedisConfigs/docker-compose.yml @@ -10,6 +10,7 @@ services: - 6379-6383:6379-6383 - 7000-7006:7000-7006 - 7010-7011:7010-7011 + - 7015:7015 - 26379-26381:26379-26381 sysctls : net.core.somaxconn: '511' diff --git a/tests/RedisConfigs/start-all.sh b/tests/RedisConfigs/start-all.sh index 67a58f53f..40773a578 100644 --- a/tests/RedisConfigs/start-all.sh +++ b/tests/RedisConfigs/start-all.sh @@ -44,4 +44,13 @@ redis-server sentinel-26380.conf --sentinel &>/dev/null & redis-server sentinel-26381.conf --sentinel &>/dev/null & popd > /dev/null -echo Servers started. \ No newline at end of file +#Envoy Servers +# Installation: https://www.envoyproxy.io/docs/envoy/latest/start/install +# Use Envoy on Ubuntu Linux to install on WSL2 +echo Starting Envoy: 7015 +pushd Envoy > /dev/null +echo "${INDENT}Envoy: 7015" +envoy -c envoy.yaml &> /dev/null & +popd > /dev/null + +echo Servers started. diff --git a/tests/RedisConfigs/start-cluster.cmd b/tests/RedisConfigs/start-cluster.cmd index 3ae4f4475..2db6be94a 100644 --- a/tests/RedisConfigs/start-cluster.cmd +++ b/tests/RedisConfigs/start-cluster.cmd @@ -7,4 +7,6 @@ pushd %~dp0\Cluster @start "Redis (Cluster): 7003" /min ..\3.0.503\redis-server.exe cluster-7003.conf @start "Redis (Cluster): 7004" /min ..\3.0.503\redis-server.exe cluster-7004.conf @start "Redis (Cluster): 7005" /min ..\3.0.503\redis-server.exe cluster-7005.conf -popd \ No newline at end of file +popd +REM envoy doesnt have an windows image, only a docker +REM need to explore if we can setup host networking \ No newline at end of file diff --git a/tests/StackExchange.Redis.Tests/EnvoyTests.cs b/tests/StackExchange.Redis.Tests/EnvoyTests.cs new file mode 100644 index 000000000..5227b73dc --- /dev/null +++ b/tests/StackExchange.Redis.Tests/EnvoyTests.cs @@ -0,0 +1,43 @@ +using System; +using System.Text; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests +{ + public class EnvoyTests : TestBase + { + public EnvoyTests(ITestOutputHelper output) : base(output) { } + + protected override string GetConfiguration() => TestConfig.Current.ProxyServerAndPort; + + /// + /// Tests basic envoy connection with the ability to set and get a key. + /// + [Fact] + public void TestBasicEnvoyConnection() + { + var sb = new StringBuilder(); + Writer.EchoTo(sb); + try + { + using (var muxer = Create(configuration: GetConfiguration(), keepAlive: 1, connectTimeout: 2000, allowAdmin: true, shared: false, proxy: Proxy.Envoyproxy, log: Writer)) + { + var db = muxer.GetDatabase(); + + const string key = "foobar"; + const string value = "barfoo"; + db.StringSet(key, value); + + var expectedVal = db.StringGet(key); + + Assert.Equal(expectedVal, value); + } + } + catch (TimeoutException) when (sb.ToString().Contains("Returned, but incorrectly")) + { + Skip.Inconclusive("Envoy server not found."); + } + } + } +} diff --git a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs index a11afa18c..455aa2dd8 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs @@ -102,6 +102,11 @@ public class Config public string SSDBServer { get; set; } public int SSDBPort { get; set; } = 8888; + + public string ProxyServer { get; set; } = "127.0.0.1"; + public int ProxyPort { get; set; } = 7015; + + public string ProxyServerAndPort => ProxyServer + ":" + ProxyPort.ToString(); } } } From 7cff1610d99f9b95297f87ae5923069474d35fc7 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 22 Feb 2022 18:15:20 -0500 Subject: [PATCH 086/435] Add #1989 to release notes! --- docs/ReleaseNotes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index dcd54707e..8e3b3532e 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -2,6 +2,7 @@ ## Unreleased - Adds bounds checking for `ExponentialRetry` backoff policy (#1921 via gliljas) +- Adds Envoy proxy support (#1989 via rkarthick) - When `SUBSCRIBE` is disabled, give proper errors and connect faster (#2001 via NickCraver) ## 2.5.27 (prerelease) From d59d34ecc9b35735a8c51a150143383eba1de35d Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 24 Feb 2022 10:04:32 -0500 Subject: [PATCH 087/435] v2.5 Backlog: Use AutoResetEvent for backlog thread lingering (#2008) This prevents so many threads from starting/stopping as we finish flushing a backlog. In short: starting a Thread is expensive, really expensive in the grand scheme of things. By ending the thread immediately when a backlog finished flushing, it had a decent change to start back up immediately to get the next item if a backlog was triggered by the lock transfer. The act of finishing the backlog itself was happening _inside the lock_ and exiting could take a moment causing an immediate re-queue of a follow-up item. This meant: lots of threads starting under parallel high contention load leading to higher CPU, more thread starts, and more allocations from thread starts. Here's a memory view: | Before | After | |--------|-------| |![before](https://user-images.githubusercontent.com/454813/155448450-3fcfddaa-7269-4edb-90e7-7d0543d894bf.png)|![after](https://user-images.githubusercontent.com/454813/155448549-f9b81c0f-5c37-4f59-b866-423dc232ed65.png)| |![before](https://user-images.githubusercontent.com/454813/155448575-f99b95b2-4892-4160-bc44-1bd498a466e2.png)|![after](https://user-images.githubusercontent.com/454813/155448502-a96802e6-97be-4c66-a7f2-fae640a8bb70.png)| (note the 570k `object[]` instances - those are almost _entirely_ thread starts) Here are thread views: | Before | After | |--------|-------| |![before](https://user-images.githubusercontent.com/454813/155448052-4f89a106-156d-4353-bc9b-2df7a5b6178e.png)|![after](https://user-images.githubusercontent.com/454813/155448073-0004d0c1-a574-42e2-904a-205820545586.png)| (note the scrollbar size) This was initially discovered from testing some of the heavy parallel scenarios vs 1.2.6. For an example scenario with heavy ops load (pictured in profiles above): | Run | 1.2.6 | 2.2.88 | `main` (v2.5.x) | After PR | |------|--------|--------|--------|--------| | 1 | 82,426 ms | 91,135 ms | 99,262 ms | 84,562 ms | | 2 | 82,335 ms | 90,674 ms | 100,462 ms | 86,211 ms | | 3 | 82,059 ms | 91,041 ms | 99,968 ms | 86,283 ms | | 4 | 82,435 ms | 90,645 ms | 102,968 ms | 86,153 ms | So note that this is an improvement over 2.2.88 even with the new backlog functionality. We're not quite at the 1.2.6 levels of performance but we're a) closer, and b) a lot of things are more correct in 2.x, and there's a cost to that. I'm very happy with the wins here. All of that is timings, but CPU usage for the same load is dramatically lower as well though this will depend on workload. Example code: ```cs using StackExchange.Redis; using System.Diagnostics; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); var taskList = new List(); var options = ConfigurationOptions.Parse("localhost"); var connection = ConnectionMultiplexer.Connect(options); for (int i = 0; i < 10; i++) { var i1 = i; var task = new Task(() => Run(i1, connection)); task.Start(); taskList.Add(task); } Task.WaitAll(taskList.ToArray()); stopwatch.Stop(); Console.WriteLine($"Done. {stopwatch.ElapsedMilliseconds} ms"); static void Run(int taskId, ConnectionMultiplexer connection) { Console.WriteLine($"{taskId} Started"); var database = connection.GetDatabase(0); for (int i = 0; i < 100000; i++) { database.StringSet(i.ToString(), i.ToString()); } Console.WriteLine($"{taskId} Insert completed"); for (int i = 0; i < 100000; i++) { var result = database.StringGet(i.ToString()); } Console.WriteLine($"{taskId} Completed"); } ``` Anyway...yeah, this was a problem. An AutoReset event is the best way I can think of to solve it. Throwing this up for review, maybe we have an even better idea of how to solve it. --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/PhysicalBridge.cs | 29 +++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 8e3b3532e..9f56a23ee 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -4,6 +4,7 @@ - Adds bounds checking for `ExponentialRetry` backoff policy (#1921 via gliljas) - Adds Envoy proxy support (#1989 via rkarthick) - When `SUBSCRIBE` is disabled, give proper errors and connect faster (#2001 via NickCraver) +- Improve concurrent load performance when backlogs are utilized (#2008 via NickCraver) ## 2.5.27 (prerelease) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index bf851c5bc..8e88ec0b0 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -836,6 +836,10 @@ private void StartBacklogProcessor() thread.Start(this); #endif } + else + { + _backlogAutoReset.Set(); + } } /// @@ -909,7 +913,7 @@ private async Task ProcessBacklogAsync() // TODO: vNext handoff this backlog to another primary ("can handle everything") connection // and remove any per-server commands. This means we need to track a bit of whether something // was server-endpoint-specific in PrepareToPushMessageToBridge (was the server ref null or not) - await ProcessBridgeBacklogAsync(); // Needs handoff + await ProcessBridgeBacklogAsync().ConfigureAwait(false); // Needs handoff } } catch @@ -938,6 +942,13 @@ private async Task ProcessBacklogAsync() } } + /// + /// Reset event for monitoring backlog additions mid-run. + /// This allows us to keep the thread around for a full flush and prevent "feathering the throttle" trying + /// to flush it. In short, we don't start and stop so many threads with a bit of linger. + /// + private readonly AutoResetEvent _backlogAutoReset = new AutoResetEvent(false); + private async Task ProcessBridgeBacklogAsync() { // Importantly: don't assume we have a physical connection here @@ -947,6 +958,7 @@ private async Task ProcessBridgeBacklogAsync() #else LockToken token = default; #endif + _backlogAutoReset.Reset(); try { _backlogStatus = BacklogStatus.Starting; @@ -985,7 +997,20 @@ private async Task ProcessBridgeBacklogAsync() { // Note that we're actively taking it off the queue here, not peeking // If there's nothing left in queue, we're done. - if (!BacklogTryDequeue(out message)) break; + if (!BacklogTryDequeue(out message)) + { + // The cost of starting a new thread is high, and we can bounce in and out of the backlog a lot. + // So instead of just exiting, keep this thread waiting for 5 seconds to see if we got another backlog item. + var gotMore = _backlogAutoReset.WaitOne(5000); + if (gotMore) + { + continue; + } + else + { + break; + } + } } try From 6718fea1539c803a16498e09e2bfb4acbec045c4 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 24 Feb 2022 10:36:55 -0500 Subject: [PATCH 088/435] #2008 Follow-ups (#2009) A few tweaks to the changes #2008 for disposing and normalization. --- src/StackExchange.Redis/PhysicalBridge.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 8e88ec0b0..b9c0a28df 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -113,6 +113,7 @@ public enum State : byte public void Dispose() { isDisposed = true; + _backlogAutoReset?.Dispose(); using (var tmp = physical) { physical = null; @@ -913,7 +914,7 @@ private async Task ProcessBacklogAsync() // TODO: vNext handoff this backlog to another primary ("can handle everything") connection // and remove any per-server commands. This means we need to track a bit of whether something // was server-endpoint-specific in PrepareToPushMessageToBridge (was the server ref null or not) - await ProcessBridgeBacklogAsync().ConfigureAwait(false); // Needs handoff + await ProcessBridgeBacklogAsync().ForAwait(); // Needs handoff } } catch @@ -976,10 +977,10 @@ private async Task ProcessBridgeBacklogAsync() // try and get the lock; if unsuccessful, retry #if NETCOREAPP - gotLock = await _singleWriterMutex.WaitAsync(TimeoutMilliseconds).ConfigureAwait(false); + gotLock = await _singleWriterMutex.WaitAsync(TimeoutMilliseconds).ForAwait(); if (gotLock) break; // got the lock; now go do something with it #else - token = await _singleWriterMutex.TryWaitAsync().ConfigureAwait(false); + token = await _singleWriterMutex.TryWaitAsync().ForAwait(); if (token.Success) break; // got the lock; now go do something with it #endif } @@ -1021,7 +1022,7 @@ private async Task ProcessBridgeBacklogAsync() if (result == WriteResult.Success) { _backlogStatus = BacklogStatus.Flushing; - result = await physical.FlushAsync(false).ConfigureAwait(false); + result = await physical.FlushAsync(false).ForAwait(); } _backlogStatus = BacklogStatus.MarkingInactive; From 0d8de9b5927b83039c5daaaeb5ecce9ce0fcfdd9 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 25 Feb 2022 14:04:19 -0500 Subject: [PATCH 089/435] Envoy: Handle both failure race errors (#2011) This should make AppVeyor a bit friendlier. --- tests/StackExchange.Redis.Tests/EnvoyTests.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/StackExchange.Redis.Tests/EnvoyTests.cs b/tests/StackExchange.Redis.Tests/EnvoyTests.cs index 5227b73dc..b76761096 100644 --- a/tests/StackExchange.Redis.Tests/EnvoyTests.cs +++ b/tests/StackExchange.Redis.Tests/EnvoyTests.cs @@ -38,6 +38,14 @@ public void TestBasicEnvoyConnection() { Skip.Inconclusive("Envoy server not found."); } + catch (AggregateException) + { + Skip.Inconclusive("Envoy server not found."); + } + catch (RedisConnectionException) when (sb.ToString().Contains("It was not possible to connect to the redis server(s)")) + { + Skip.Inconclusive("Envoy server not found."); + } } } } From 511ba106a559b6aabc2808d7d6db4f7239403532 Mon Sep 17 00:00:00 2001 From: Martinek Vilmos Date: Sat, 26 Feb 2022 00:12:04 +0100 Subject: [PATCH 090/435] Implement GET option of SET command (#2003) Redis 6.2.0 has deprecated the [GETSET](https://redis.io/commands/getset) command and in turn added the `GET` option to the [SET](https://redis.io/commands/set) command. Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + .../Interfaces/IDatabase.cs | 13 +++ .../Interfaces/IDatabaseAsync.cs | 13 +++ .../KeyspaceIsolation/DatabaseWrapper.cs | 5 + .../KeyspaceIsolation/WrapperBase.cs | 5 + src/StackExchange.Redis/Message.cs | 33 +++++++ src/StackExchange.Redis/RedisDatabase.cs | 49 ++++++++++ src/StackExchange.Redis/RedisFeatures.cs | 13 ++- tests/StackExchange.Redis.Tests/Strings.cs | 93 +++++++++++++++++++ 9 files changed, 224 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 9f56a23ee..4711f2d18 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -4,6 +4,7 @@ - Adds bounds checking for `ExponentialRetry` backoff policy (#1921 via gliljas) - Adds Envoy proxy support (#1989 via rkarthick) - When `SUBSCRIBE` is disabled, give proper errors and connect faster (#2001 via NickCraver) +- Adds `GET` on `SET` command support (present in Redis 6.2+ - #2003 via martinekvili) - Improve concurrent load performance when backlogs are utilized (#2008 via NickCraver) ## 2.5.27 (prerelease) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index e200e3cc3..500047dd2 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -2202,6 +2202,19 @@ IEnumerable SortedSetScan(RedisKey key, /// https://redis.io/commands/msetnx bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); + /// + /// Atomically sets key to value and returns the previous value (if any) stored at . + /// + /// The key of the string. + /// The value to set. + /// The expiry to set. + /// Which condition to set the value under (defaults to ). + /// The flags to use for this operation. + /// The previous value stored at , or nil when key did not exist. + /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. + /// https://redis.io/commands/set + RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None); + /// /// Sets or clears the bit at offset in the string value stored at key. /// The bit is either set or cleared depending on value, which can be either 0 or 1. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index a0215b6af..5dddaeefd 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -2155,6 +2155,19 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// https://redis.io/commands/msetnx Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); + /// + /// Atomically sets key to value and returns the previous value (if any) stored at . + /// + /// The key of the string. + /// The value to set. + /// The expiry to set. + /// Which condition to set the value under (defaults to ). + /// The flags to use for this operation. + /// The previous value stored at , or nil when key did not exist. + /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. + /// https://redis.io/commands/set + Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None); + /// /// Sets or clears the bit at offset in the string value stored at key. /// The bit is either set or cleared depending on value, which can be either 0 or 1. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 2a6842c49..376279ae0 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -876,6 +876,11 @@ public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, W return Inner.StringSet(ToInner(key), value, expiry, when, flags); } + public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + return Inner.StringSetAndGet(ToInner(key), value, expiry, when, flags); + } + public bool StringSetBit(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) { return Inner.StringSetBit(ToInner(key), offset, bit, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 81d8f9b6b..d054f0a85 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -867,6 +867,11 @@ public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expir return Inner.StringSetAsync(ToInner(key), value, expiry, when, flags); } + public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + return Inner.StringSetAndGetAsync(ToInner(key), value, expiry, when, flags); + } + public Task StringSetBitAsync(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) { return Inner.StringSetBitAsync(ToInner(key), offset, bit, flags); diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 36befac70..8872c39fc 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -279,6 +279,9 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3) => new CommandKeyValueValueValueValueMessage(db, flags, command, key, value0, value1, value2, value3); + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) => + new CommandKeyValueValueValueValueValueMessage(db, flags, command, key, value0, value1, value2, value3, value4); + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value0, in RedisValue value1) => new CommandValueValueMessage(db, flags, command, value0, value1); @@ -1106,6 +1109,36 @@ protected override void WriteImpl(PhysicalConnection physical) public override int ArgCount => 5; } + private sealed class CommandKeyValueValueValueValueValueMessage : CommandKeyBase + { + private readonly RedisValue value0, value1, value2, value3, value4; + public CommandKeyValueValueValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) : base(db, flags, command, key) + { + value0.AssertNotNull(); + value1.AssertNotNull(); + value2.AssertNotNull(); + value3.AssertNotNull(); + value4.AssertNotNull(); + this.value0 = value0; + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.value4 = value4; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, 6); + physical.Write(Key); + physical.WriteBulkString(value0); + physical.WriteBulkString(value1); + physical.WriteBulkString(value2); + physical.WriteBulkString(value3); + physical.WriteBulkString(value4); + } + public override int ArgCount => 6; + } + private sealed class CommandMessage : Message { public CommandMessage(int db, CommandFlags flags, RedisCommand command) : base(db, flags, command) { } diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index e8b54c2b1..b08c1768d 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -2588,6 +2588,18 @@ public Task StringSetAsync(KeyValuePair[] values, Wh return ExecuteAsync(msg, ResultProcessor.Boolean); } + public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + var msg = GetStringSetAndGetMessage(key, value, expiry, when, flags); + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + + public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + var msg = GetStringSetAndGetMessage(key, value, expiry, when, flags); + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + public bool StringSetBit(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.SETBIT, key, offset, bit); @@ -3527,6 +3539,43 @@ private Message GetStringSetMessage(RedisKey key, RedisValue value, TimeSpan? ex }; } + private Message GetStringSetAndGetMessage(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + WhenAlwaysOrExistsOrNotExists(when); + if (value.IsNull) return Message.Create(Database, flags, RedisCommand.GETDEL, key); + + if (expiry == null || expiry.Value == TimeSpan.MaxValue) + { // no expiry + switch (when) + { + case When.Always: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.GET); + case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.GET); + case When.NotExists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.NX, RedisLiterals.GET); + } + } + long milliseconds = expiry.Value.Ticks / TimeSpan.TicksPerMillisecond; + + if ((milliseconds % 1000) == 0) + { + // a nice round number of seconds + long seconds = milliseconds / 1000; + switch (when) + { + case When.Always: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.GET); + case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX, RedisLiterals.GET); + case When.NotExists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.NX, RedisLiterals.GET); + } + } + + return when switch + { + When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.GET), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX, RedisLiterals.GET), + When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.NX, RedisLiterals.GET), + _ => throw new NotSupportedException(), + }; + } + private Message IncrMessage(RedisKey key, long value, CommandFlags flags) { switch (value) diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 7e23ffbc7..b349eeafe 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -36,7 +36,8 @@ public readonly struct RedisFeatures v4_0_0 = new Version(4, 0, 0), v4_9_1 = new Version(4, 9, 1), // 5.0 RC1 is version 4.9.1; // 5.0 RC1 is version 4.9.1 v5_0_0 = new Version(5, 0, 0), - v6_2_0 = new Version(6, 2, 0); + v6_2_0 = new Version(6, 2, 0), + v6_9_240 = new Version(6, 9, 240); // 7.0 RC1 is version 6.9.240 private readonly Version version; @@ -74,6 +75,16 @@ public RedisFeatures(Version version) /// public bool GetDelete => Version >= v6_2_0; + /// + /// Does SET support the GET option? + /// + public bool SetAndGet => Version >= v6_2_0; + + /// + /// Does SET allow the NX and GET options to be used together? + /// + public bool SetNotExistsAndGet => Version >= v6_9_240; + /// /// Is HSTRLEN available? /// diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index b7db11fb4..99cd35e34 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -194,6 +194,99 @@ public async Task SetNotExists() } } + [Fact] + public async Task SetAndGet() + { + using (var muxer = Create()) + { + Skip.IfMissingFeature(muxer, nameof(RedisFeatures.SetAndGet), r => r.SetAndGet); + + var conn = muxer.GetDatabase(); + var prefix = Me(); + conn.KeyDelete(prefix + "1", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "2", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "3", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "4", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "5", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "6", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "7", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "8", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "9", CommandFlags.FireAndForget); + conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); + conn.StringSet(prefix + "2", "abc", flags: CommandFlags.FireAndForget); + conn.StringSet(prefix + "4", "abc", flags: CommandFlags.FireAndForget); + conn.StringSet(prefix + "6", "abc", flags: CommandFlags.FireAndForget); + conn.StringSet(prefix + "7", "abc", flags: CommandFlags.FireAndForget); + conn.StringSet(prefix + "8", "abc", flags: CommandFlags.FireAndForget); + conn.StringSet(prefix + "9", "abc", flags: CommandFlags.FireAndForget); + + var x0 = conn.StringSetAndGetAsync(prefix + "1", RedisValue.Null); + var x1 = conn.StringSetAndGetAsync(prefix + "2", "def"); + var x2 = conn.StringSetAndGetAsync(prefix + "3", "def"); + var x3 = conn.StringSetAndGetAsync(prefix + "4", "def", when: When.Exists); + var x4 = conn.StringSetAndGetAsync(prefix + "5", "def", when: When.Exists); + var x5 = conn.StringSetAndGetAsync(prefix + "6", "def", expiry: TimeSpan.FromSeconds(4)); + var x6 = conn.StringSetAndGetAsync(prefix + "7", "def", expiry: TimeSpan.FromMilliseconds(4001)); + var x7 = conn.StringSetAndGetAsync(prefix + "8", "def", expiry: TimeSpan.FromSeconds(4), when: When.Exists); + var x8 = conn.StringSetAndGetAsync(prefix + "9", "def", expiry: TimeSpan.FromMilliseconds(4001), when: When.Exists); + + var s0 = conn.StringGetAsync(prefix + "1"); + var s1 = conn.StringGetAsync(prefix + "2"); + var s2 = conn.StringGetAsync(prefix + "3"); + var s3 = conn.StringGetAsync(prefix + "4"); + var s4 = conn.StringGetAsync(prefix + "5"); + + Assert.Equal("abc", await x0); + Assert.Equal("abc", await x1); + Assert.Equal(RedisValue.Null, await x2); + Assert.Equal("abc", await x3); + Assert.Equal(RedisValue.Null, await x4); + Assert.Equal("abc", await x5); + Assert.Equal("abc", await x6); + Assert.Equal("abc", await x7); + Assert.Equal("abc", await x8); + + Assert.Equal(RedisValue.Null, await s0); + Assert.Equal("def", await s1); + Assert.Equal("def", await s2); + Assert.Equal("def", await s3); + Assert.Equal(RedisValue.Null, await s4); + } + } + + [Fact] + public async Task SetNotExistsAndGet() + { + using (var muxer = Create()) + { + Skip.IfMissingFeature(muxer, nameof(RedisFeatures.SetNotExistsAndGet), r => r.SetNotExistsAndGet); + + var conn = muxer.GetDatabase(); + var prefix = Me(); + conn.KeyDelete(prefix + "1", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "2", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "3", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "4", CommandFlags.FireAndForget); + conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); + + var x0 = conn.StringSetAndGetAsync(prefix + "1", "def", when: When.NotExists); + var x1 = conn.StringSetAndGetAsync(prefix + "2", "def", when: When.NotExists); + var x2 = conn.StringSetAndGetAsync(prefix + "3", "def", expiry: TimeSpan.FromSeconds(4), when: When.NotExists); + var x3 = conn.StringSetAndGetAsync(prefix + "4", "def", expiry: TimeSpan.FromMilliseconds(4001), when: When.NotExists); + + var s0 = conn.StringGetAsync(prefix + "1"); + var s1 = conn.StringGetAsync(prefix + "2"); + + Assert.Equal("abc", await x0); + Assert.Equal(RedisValue.Null, await x1); + Assert.Equal(RedisValue.Null, await x2); + Assert.Equal(RedisValue.Null, await x3); + + Assert.Equal("abc", await s0); + Assert.Equal("def", await s1); + } + } + [Fact] public async Task Ranges() { From 32dea994e1261e9fef368635f3a7327e4bcbb117 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 25 Feb 2022 19:49:01 -0500 Subject: [PATCH 091/435] Backlog locking fixes - more #2008 follow-up (#2015) In troubleshooting these 2 tests, I realized what's happening: a really dumb placement mistake in #2008. Now, instead of locking inside the damn lock, it loops outside a bit cleaner and higher up. Performance wins are the same but it's a lot sander and doesn't block both the backlog and the writer for another 5 seconds. Now only the thread lingers and it'll try to get the lock when running another pass, if it gets any in the next 5 seconds. --- src/StackExchange.Redis/ExceptionFactory.cs | 5 +-- src/StackExchange.Redis/PhysicalBridge.cs | 36 ++++++++++--------- tests/StackExchange.Redis.Tests/BasicOps.cs | 4 ++- .../ExceptionFactoryTests.cs | 6 ++-- .../PubSubMultiserver.cs | 4 +-- 5 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 92bbb2a0e..6228bbb90 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -308,10 +308,7 @@ ServerEndPoint server Add(data, sb, "Queue-Awaiting-Write", "qu", bs.BacklogMessagesPending.ToString()); Add(data, sb, "Queue-Awaiting-Response", "qs", bs.Connection.MessagesSentAwaitingResponse.ToString()); Add(data, sb, "Active-Writer", "aw", bs.IsWriterActive.ToString()); - if (bs.BacklogMessagesPending != 0) - { - Add(data, sb, "Backlog-Writer", "bw", bs.BacklogStatus.ToString()); - } + Add(data, sb, "Backlog-Writer", "bw", bs.BacklogStatus.ToString()); if (bs.Connection.ReadStatus != PhysicalConnection.ReadStatus.NA) Add(data, sb, "Read-State", "rs", bs.Connection.ReadStatus.ToString()); if (bs.Connection.WriteStatus != PhysicalConnection.WriteStatus.NA) Add(data, sb, "Write-State", "ws", bs.Connection.WriteStatus.ToString()); diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index b9c0a28df..92d2f83f7 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -887,6 +887,7 @@ internal enum BacklogStatus : byte Starting, Started, CheckingForWork, + SpinningDown, CheckingForTimeout, CheckingForTimeoutComplete, RecordingTimeout, @@ -909,12 +910,25 @@ private async Task ProcessBacklogAsync() _backlogStatus = BacklogStatus.Starting; try { - if (!_backlog.IsEmpty) + while (true) { - // TODO: vNext handoff this backlog to another primary ("can handle everything") connection - // and remove any per-server commands. This means we need to track a bit of whether something - // was server-endpoint-specific in PrepareToPushMessageToBridge (was the server ref null or not) - await ProcessBridgeBacklogAsync().ForAwait(); // Needs handoff + if (!_backlog.IsEmpty) + { + // TODO: vNext handoff this backlog to another primary ("can handle everything") connection + // and remove any per-server commands. This means we need to track a bit of whether something + // was server-endpoint-specific in PrepareToPushMessageToBridge (was the server ref null or not) + await ProcessBridgeBacklogAsync().ForAwait(); + } + + // The cost of starting a new thread is high, and we can bounce in and out of the backlog a lot. + // So instead of just exiting, keep this thread waiting for 5 seconds to see if we got another backlog item. + _backlogStatus = BacklogStatus.SpinningDown; + // Note this is happening *outside* the lock + var gotMore = _backlogAutoReset.WaitOne(5000); + if (!gotMore) + { + break; + } } } catch @@ -1000,17 +1014,7 @@ private async Task ProcessBridgeBacklogAsync() // If there's nothing left in queue, we're done. if (!BacklogTryDequeue(out message)) { - // The cost of starting a new thread is high, and we can bounce in and out of the backlog a lot. - // So instead of just exiting, keep this thread waiting for 5 seconds to see if we got another backlog item. - var gotMore = _backlogAutoReset.WaitOne(5000); - if (gotMore) - { - continue; - } - else - { - break; - } + break; } } diff --git a/tests/StackExchange.Redis.Tests/BasicOps.cs b/tests/StackExchange.Redis.Tests/BasicOps.cs index 0b8a14247..5ce3a8a29 100644 --- a/tests/StackExchange.Redis.Tests/BasicOps.cs +++ b/tests/StackExchange.Redis.Tests/BasicOps.cs @@ -26,7 +26,7 @@ public async Task PingOnce() } [Fact] - public void RapidDispose() + public async Task RapidDispose() { RedisKey key = Me(); using (var primary = Create()) @@ -41,6 +41,8 @@ public void RapidDispose() secondary.GetDatabase().StringIncrement(key, flags: CommandFlags.FireAndForget); } } + // Give it a moment to get through the pipe...they were fire and forget + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => 10 == (int)conn.StringGet(key)); Assert.Equal(10, (int)conn.StringGet(key)); } } diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index ab19e60c4..d6e56ddd5 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -120,7 +120,7 @@ public void TimeoutException() Assert.StartsWith("Test Timeout, command=PING", ex.Message); Assert.Contains("clientName: " + nameof(TimeoutException), ex.Message); // Ensure our pipe numbers are in place - Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); + Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); Assert.Contains("mc: 1/1/0", ex.Message); Assert.Contains("serverEndpoint: " + server.EndPoint, ex.Message); Assert.Contains("IOCP: ", ex.Message); @@ -194,13 +194,13 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple // Ensure our pipe numbers are in place if they should be if (hasDetail) { - Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); + Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); Assert.Contains($"mc: {connCount}/{completeCount}/0", ex.Message); Assert.Contains("serverEndpoint: " + server.EndPoint.ToString().Replace("Unspecified/", ""), ex.Message); } else { - Assert.DoesNotContain("inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); + Assert.DoesNotContain("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); Assert.DoesNotContain($"mc: {connCount}/{completeCount}/0", ex.Message); Assert.DoesNotContain("serverEndpoint: " + server.EndPoint.ToString().Replace("Unspecified/", ""), ex.Message); } diff --git a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs index 4c3a41772..15979d90e 100644 --- a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs +++ b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs @@ -155,13 +155,13 @@ await sub.SubscribeAsync(channel, (_, val) => else { // This subscription shouldn't be able to reconnect by flags (demanding an unavailable server) - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => subscription.IsConnected); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); Assert.False(subscription.IsConnected); Log("Unable to reconnect (as expected)"); // Allow connecting back to the original muxer.AllowConnect = true; - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => subscription.IsConnected); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); Assert.True(subscription.IsConnected); var newServer = subscription.GetCurrentServer(); From 65c1f28497f3b0cf0176148e53614f143ab9cbd4 Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Fri, 25 Feb 2022 18:58:15 -0600 Subject: [PATCH 092/435] Added CommandMap check before requesting cluster nodes (#2014) Fixes #2012 Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 3 ++- .../ConnectionMultiplexer.cs | 2 +- .../ConnectCustomConfig.cs | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 4711f2d18..8252fe7de 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -5,7 +5,8 @@ - Adds Envoy proxy support (#1989 via rkarthick) - When `SUBSCRIBE` is disabled, give proper errors and connect faster (#2001 via NickCraver) - Adds `GET` on `SET` command support (present in Redis 6.2+ - #2003 via martinekvili) -- Improve concurrent load performance when backlogs are utilized (#2008 via NickCraver) +- Improves concurrent load performance when backlogs are utilized (#2008 via NickCraver) +- Improves cluster connections when `CLUSTER` command is disabled (#2014 via tylerohlsen) ## 2.5.27 (prerelease) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 177645ed9..40027a2cb 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1822,7 +1822,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP break; } - if (clusterCount > 0 && !encounteredConnectedClusterServer) + if (clusterCount > 0 && !encounteredConnectedClusterServer && CommandMap.IsAvailable(RedisCommand.CLUSTER)) { // We have encountered a connected server with a cluster type for the first time. // so we will get list of other nodes from this server using "CLUSTER NODES" command diff --git a/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs b/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs index 2bed55caa..dc2ec8cfa 100644 --- a/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs +++ b/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs @@ -26,6 +26,24 @@ public void DisabledCommandsStillConnect(string disabledCommands) Assert.True(db.IsConnected(default(RedisKey))); } + [Theory] + [InlineData("config")] + [InlineData("info")] + [InlineData("get")] + [InlineData("cluster")] + [InlineData("config,get")] + [InlineData("info,get")] + [InlineData("config,info,get")] + [InlineData("config,info,get,cluster")] + public void DisabledCommandsStillConnectCluster(string disabledCommands) + { + using var muxer = Create(allowAdmin: true, configuration: TestConfig.Current.ClusterServersAndPorts, disabledCommands: disabledCommands.Split(','), log: Writer); + + var db = muxer.GetDatabase(); + db.Ping(); + Assert.True(db.IsConnected(default(RedisKey))); + } + [Fact] public void TieBreakerIntact() { From f78aee8c42a879511a037b08b60e0f59df5737af Mon Sep 17 00:00:00 2001 From: Nick <73682165+ja-nick@users.noreply.github.com> Date: Thu, 3 Mar 2022 00:43:47 +1100 Subject: [PATCH 093/435] Update Timeouts.md (#2018) The abbreviations table claims that 'v' represents the version of Redis but I'm pretty sure it actually shows the version of the `StackExchange.Redis` library currently in use. --- docs/Timeouts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Timeouts.md b/docs/Timeouts.md index 9b6dd21ce..e84b8436a 100644 --- a/docs/Timeouts.md +++ b/docs/Timeouts.md @@ -97,7 +97,7 @@ By default Redis Timeout exception(s) includes useful information, which can hel |IOCP | IOCP: (Busy=0,Free=500,Min=248,Max=500)| Runtime Global Thread Pool IO Threads. | |WORKER | WORKER: (Busy=170,Free=330,Min=248,Max=500)| Runtime Global Thread Pool Worker Threads.| |POOL | POOL: (Threads=8,QueuedItems=0,CompletedItems=42)| Thread Pool Work Item Stats.| -|v | Redis Version: version |Current redis version you are currently using in your application.| +|v | Redis Version: version |The `StackExchange.Redis` version you are currently using in your application.| |active | Message-Current: {string} |Included in exception message when `IncludeDetailInExceptions=True` on multiplexer| |next | Message-Next: {string} |When `IncludeDetailInExceptions=True` on multiplexer, it might include command and key, otherwise only command.| |Local-CPU | %CPU or Not Available |When `IncludePerformanceCountersInExceptions=True` on multiplexer, Local CPU %age will be included in exception message. It might not work in all environments where application is hosted. | From ee687dc96ecf40191134c1ee4cc1c1a042a5970b Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 3 Mar 2022 09:34:20 -0500 Subject: [PATCH 094/435] Connection logging: enhancements! (#2019) This tweaks logging a bit to make it easier to parse and switches timings to `ValueStopwatch` while I'm in here adding one. Helps investigate things like #2017. --- docs/ReleaseNotes.md | 1 + .../ConnectionMultiplexer.cs | 92 +++++++++++-------- src/StackExchange.Redis/ValueStopwatch.cs | 32 +++++++ 3 files changed, 89 insertions(+), 36 deletions(-) create mode 100644 src/StackExchange.Redis/ValueStopwatch.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 8252fe7de..d62389d5b 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,6 +7,7 @@ - Adds `GET` on `SET` command support (present in Redis 6.2+ - #2003 via martinekvili) - Improves concurrent load performance when backlogs are utilized (#2008 via NickCraver) - Improves cluster connections when `CLUSTER` command is disabled (#2014 via tylerohlsen) +- Improves connection logging and adds overall timing to it (#2019 via NickCraver) ## 2.5.27 (prerelease) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 40027a2cb..cd1b44499 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -669,7 +669,7 @@ private static bool WaitAllIgnoreErrors(Task[] tasks, int timeout) { if (tasks == null) throw new ArgumentNullException(nameof(tasks)); if (tasks.Length == 0) return true; - var watch = Stopwatch.StartNew(); + var watch = ValueStopwatch.StartNew(); try { // If no error, great @@ -683,7 +683,7 @@ private static bool WaitAllIgnoreErrors(Task[] tasks, int timeout) var task = tasks[i]; if (!task.IsCanceled && !task.IsCompleted && !task.IsFaulted) { - var remaining = timeout - checked((int)watch.ElapsedMilliseconds); + var remaining = timeout - watch.ElapsedMilliseconds; if (remaining <= 0) return false; try { @@ -742,12 +742,12 @@ private static async Task WaitAllIgnoreErrorsAsync(string name, Task[] tas return true; } - var watch = Stopwatch.StartNew(); + var watch = ValueStopwatch.StartNew(); LogWithThreadPoolStats(log, $"Awaiting {tasks.Length} {name} task completion(s) for {timeoutMilliseconds}ms", out _); try { // if none error, great - var remaining = timeoutMilliseconds - checked((int)watch.ElapsedMilliseconds); + var remaining = timeoutMilliseconds - watch.ElapsedMilliseconds; if (remaining <= 0) { LogWithThreadPoolStats(log, "Timeout before awaiting for tasks", out _); @@ -769,7 +769,7 @@ private static async Task WaitAllIgnoreErrorsAsync(string name, Task[] tas var task = tasks[i]; if (!task.IsCanceled && !task.IsCompleted && !task.IsFaulted) { - var remaining = timeoutMilliseconds - checked((int)watch.ElapsedMilliseconds); + var remaining = timeoutMilliseconds - watch.ElapsedMilliseconds; if (remaining <= 0) { LogWithThreadPoolStats(log, "Timeout awaiting tasks", out _); @@ -891,7 +891,8 @@ private static async Task ConnectImplAsync(ConfigurationO { try { - log?.WriteLine($"Connecting (async) on {RuntimeInformation.FrameworkDescription}"); + var sw = ValueStopwatch.StartNew(); + logProxy?.WriteLine($"Connecting (async) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); muxer = CreateMultiplexer(configuration, logProxy, out connectHandler); killMe = muxer; @@ -912,6 +913,8 @@ private static async Task ConnectImplAsync(ConfigurationO await Maintenance.ServerMaintenanceEvent.AddListenersAsync(muxer, logProxy).ForAwait(); + logProxy?.WriteLine($"Total connect time: {sw.ElapsedMilliseconds:n0} ms"); + return muxer; } finally @@ -998,6 +1001,16 @@ public void WriteLine(string message = null) } } } + public void WriteLine(string prefix, string message) + { + if (_log != null) // note: double-checked + { + lock (SyncLock) + { + _log?.WriteLine($"{DateTime.UtcNow:HH:mm:ss.ffff}: {prefix}{message}"); + } + } + } public void Dispose() { if (_log != null) // note: double-checked @@ -1020,7 +1033,7 @@ private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions conf lock (log.SyncLock) // Keep the outer and any inner errors contiguous { var ex = a.Exception; - log?.WriteLine($"connection failed: {Format.ToString(a.EndPoint)} ({a.ConnectionType}, {a.FailureType}): {ex?.Message ?? "(unknown)"}"); + log?.WriteLine($"Connection failed: {Format.ToString(a.EndPoint)} ({a.ConnectionType}, {a.FailureType}): {ex?.Message ?? "(unknown)"}"); while ((ex = ex.InnerException) != null) { log?.WriteLine($"> {ex.Message}"); @@ -1178,7 +1191,8 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat { try { - log?.WriteLine($"Connecting (sync) on {RuntimeInformation.FrameworkDescription}"); + var sw = ValueStopwatch.StartNew(); + logProxy?.WriteLine($"Connecting (sync) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); muxer = CreateMultiplexer(configuration, logProxy, out connectHandler); killMe = muxer; @@ -1211,6 +1225,8 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat Maintenance.ServerMaintenanceEvent.AddListenersAsync(muxer, logProxy).Wait(muxer.SyncConnectTimeout(true)); + logProxy?.WriteLine($"Total connect time: {sw.ElapsedMilliseconds:n0} ms"); + return muxer; } finally @@ -1640,11 +1656,12 @@ internal void GetStatus(LogProxy log) if (log == null) return; var tmp = GetServerSnapshot(); + log?.WriteLine("Endpoint Summary:"); foreach (var server in tmp) { - log?.WriteLine(server.Summary()); - log?.WriteLine(server.GetCounters().ToString()); - log?.WriteLine(server.GetProfile()); + log?.WriteLine(prefix: " ", message: server.Summary()); + log?.WriteLine(prefix: " ", message: server.GetCounters().ToString()); + log?.WriteLine(prefix: " ", message: server.GetProfile()); } log?.WriteLine($"Sync timeouts: {Interlocked.Read(ref syncTimeouts)}; async timeouts: {Interlocked.Read(ref asyncTimeouts)}; fire and forget: {Interlocked.Read(ref fireAndForgets)}; last heartbeat: {LastHeartbeatSecondsAgo}s ago"); } @@ -1722,7 +1739,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP ServerEndPoint[] servers = null; bool encounteredConnectedClusterServer = false; - Stopwatch watch = null; + ValueStopwatch? watch = null; int iterCount = first ? 2 : 1; // This is fix for https://github.com/StackExchange/StackExchange.Redis/issues/300 @@ -1753,8 +1770,8 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP available[i] = server.OnConnectedAsync(log, sendTracerIfConnected: true, autoConfigureIfConnected: reconfigureAll); } - watch ??= Stopwatch.StartNew(); - var remaining = RawConfig.ConnectTimeout - checked((int)watch.ElapsedMilliseconds); + watch ??= ValueStopwatch.StartNew(); + var remaining = RawConfig.ConnectTimeout - watch.Value.ElapsedMilliseconds; log?.WriteLine($"Allowing {available.Length} endpoint(s) {TimeSpan.FromMilliseconds(remaining)} to respond..."); Trace("Allowing endpoints " + TimeSpan.FromMilliseconds(remaining) + " to respond..."); var allConnected = await WaitAllIgnoreErrorsAsync("available", available, remaining, log).ForAwait(); @@ -1772,10 +1789,11 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP } } + log?.WriteLine($"Endpoint summary:"); // Log current state after await foreach (var server in servers) { - log?.WriteLine($"{Format.ToString(server.EndPoint)}: Endpoint is {server.ConnectionState}"); + log?.WriteLine($" {Format.ToString(server.EndPoint)}: Endpoint is {server.ConnectionState}"); } EndPointCollection updatedClusterEndpointCollection = null; @@ -1790,21 +1808,21 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP var aex = task.Exception; foreach (var ex in aex.InnerExceptions) { - log?.WriteLine($"{Format.ToString(server)}: Faulted: {ex.Message}"); + log?.WriteLine($" {Format.ToString(server)}: Faulted: {ex.Message}"); failureMessage = ex.Message; } } else if (task.IsCanceled) { server.SetUnselectable(UnselectableFlags.DidNotRespond); - log?.WriteLine($"{Format.ToString(server)}: Connect task canceled"); + log?.WriteLine($" {Format.ToString(server)}: Connect task canceled"); } else if (task.IsCompleted) { if (task.Result != "Disconnected") { server.ClearUnselectable(UnselectableFlags.DidNotRespond); - log?.WriteLine($"{Format.ToString(server)}: Returned with success as {server.ServerType} {(server.IsReplica ? "replica" : "primary")} (Source: {task.Result})"); + log?.WriteLine($" {Format.ToString(server)}: Returned with success as {server.ServerType} {(server.IsReplica ? "replica" : "primary")} (Source: {task.Result})"); // Count the server types switch (server.ServerType) @@ -1857,13 +1875,13 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP else { server.SetUnselectable(UnselectableFlags.DidNotRespond); - log?.WriteLine($"{Format.ToString(server)}: Returned, but incorrectly"); + log?.WriteLine($" {Format.ToString(server)}: Returned, but incorrectly"); } } else { server.SetUnselectable(UnselectableFlags.DidNotRespond); - log?.WriteLine($"{Format.ToString(server)}: Did not respond"); + log?.WriteLine($" {Format.ToString(server)}: Did not respond"); } } @@ -1951,9 +1969,9 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP healthy = standaloneCount != 0 || clusterCount != 0 || sentinelCount != 0; if (first && !healthy && attemptsLeft > 0) { - log?.WriteLine("resetting failing connections to retry..."); + log?.WriteLine("Resetting failing connections to retry..."); ResetAllNonConnected(); - log?.WriteLine($"retrying; attempts left: {attemptsLeft}..."); + log?.WriteLine($" Retrying - attempts left: {attemptsLeft}..."); } //WTF("?: " + attempts); } while (first && !healthy && attemptsLeft > 0); @@ -2035,6 +2053,8 @@ private void ResetAllNonConnected() private static ServerEndPoint NominatePreferredMaster(LogProxy log, ServerEndPoint[] servers, bool useTieBreakers, List masters) { + log?.WriteLine("Election summary:"); + Dictionary uniques = null; if (useTieBreakers) { @@ -2047,11 +2067,11 @@ private static ServerEndPoint NominatePreferredMaster(LogProxy log, ServerEndPoi if (string.IsNullOrWhiteSpace(serverResult)) { - log?.WriteLine($"Election: {Format.ToString(server)} had no tiebreaker set"); + log?.WriteLine($" Election: {Format.ToString(server)} had no tiebreaker set"); } else { - log?.WriteLine($"Election: {Format.ToString(server)} nominates: {serverResult}"); + log?.WriteLine($" Election: {Format.ToString(server)} nominates: {serverResult}"); if (!uniques.TryGetValue(serverResult, out int count)) count = 0; uniques[serverResult] = count + 1; } @@ -2061,37 +2081,37 @@ private static ServerEndPoint NominatePreferredMaster(LogProxy log, ServerEndPoi switch (masters.Count) { case 0: - log?.WriteLine("Election: No masters detected"); + log?.WriteLine(" Election: No masters detected"); return null; case 1: - log?.WriteLine($"Election: Single master detected: {Format.ToString(masters[0].EndPoint)}"); + log?.WriteLine($" Election: Single master detected: {Format.ToString(masters[0].EndPoint)}"); return masters[0]; default: - log?.WriteLine("Election: Multiple masters detected..."); + log?.WriteLine(" Election: Multiple masters detected..."); if (useTieBreakers && uniques != null) { switch (uniques.Count) { case 0: - log?.WriteLine("Election: No nominations by tie-breaker"); + log?.WriteLine(" Election: No nominations by tie-breaker"); break; case 1: string unanimous = uniques.Keys.Single(); - log?.WriteLine($"Election: Tie-breaker unanimous: {unanimous}"); + log?.WriteLine($" Election: Tie-breaker unanimous: {unanimous}"); var found = SelectServerByElection(servers, unanimous, log); if (found != null) { - log?.WriteLine($"Election: Elected: {Format.ToString(found.EndPoint)}"); + log?.WriteLine($" Election: Elected: {Format.ToString(found.EndPoint)}"); return found; } break; default: - log?.WriteLine("Election is contested:"); + log?.WriteLine(" Election is contested:"); ServerEndPoint highest = null; bool arbitrary = false; foreach (var pair in uniques.OrderByDescending(x => x.Value)) { - log?.WriteLine($"Election: {pair.Key} has {pair.Value} votes"); + log?.WriteLine($" Election: {pair.Key} has {pair.Value} votes"); if (highest == null) { highest = SelectServerByElection(servers, pair.Key, log); @@ -2106,11 +2126,11 @@ private static ServerEndPoint NominatePreferredMaster(LogProxy log, ServerEndPoi { if (arbitrary) { - log?.WriteLine($"Election: Choosing master arbitrarily: {Format.ToString(highest.EndPoint)}"); + log?.WriteLine($" Election: Choosing master arbitrarily: {Format.ToString(highest.EndPoint)}"); } else { - log?.WriteLine($"Election: Elected: {Format.ToString(highest.EndPoint)}"); + log?.WriteLine($" Election: Elected: {Format.ToString(highest.EndPoint)}"); } return highest; } @@ -2120,7 +2140,7 @@ private static ServerEndPoint NominatePreferredMaster(LogProxy log, ServerEndPoi break; } - log?.WriteLine($"Election: Choosing master arbitrarily: {Format.ToString(masters[0].EndPoint)}"); + log?.WriteLine($" Election: Choosing master arbitrarily: {Format.ToString(masters[0].EndPoint)}"); return masters[0]; } @@ -2413,7 +2433,7 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co bool success = false; ConnectionMultiplexer connection = null; - var sw = Stopwatch.StartNew(); + var sw = ValueStopwatch.StartNew(); do { // Get an initial endpoint - try twice diff --git a/src/StackExchange.Redis/ValueStopwatch.cs b/src/StackExchange.Redis/ValueStopwatch.cs new file mode 100644 index 000000000..f29738aaf --- /dev/null +++ b/src/StackExchange.Redis/ValueStopwatch.cs @@ -0,0 +1,32 @@ +using System; +using System.Diagnostics; + +namespace StackExchange.Redis; + +/// +/// Optimization over , from https://github.com/dotnet/aspnetcore/blob/main/src/Shared/ValueStopwatch/ValueStopwatch.cs +/// +internal struct ValueStopwatch +{ + private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + private readonly long _startTimestamp; + public bool IsActive => _startTimestamp != 0; + + private ValueStopwatch(long startTimestamp) => _startTimestamp = startTimestamp; + public static ValueStopwatch StartNew() => new ValueStopwatch(Stopwatch.GetTimestamp()); + + public int ElapsedMilliseconds => checked((int)GetElapsedTime().TotalMilliseconds); + + public TimeSpan GetElapsedTime() + { + if (!IsActive) + { + throw new InvalidOperationException("An uninitialized, or 'default', ValueStopwatch cannot be used to get elapsed time."); + } + + var end = Stopwatch.GetTimestamp(); + var timestampDelta = end - _startTimestamp; + var ticks = (long)(TimestampToTicks * timestampDelta); + return new TimeSpan(ticks); + } +} From 14946058788212c62199b209ab18eaf8b8e662d8 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 3 Mar 2022 11:40:50 -0500 Subject: [PATCH 095/435] Configuration: An idea on separation (#1987) This is just an idea pitch (scrutinize heavily!) to see if the direction even works. The intent is for a consumer or wrapper library to specify a default options provider (or extend the built-in ones). For example Azure could override `IsMatch` and maintain its own domain list, as could AWS, etc. Problems with this: - Modifying the `KnownProviders` list on `DefaultOptionsProvider` is awkward, but `ConfigurationOptions` can also get a `.Defaults` set directly, or...other ideas? Maybe `GetForEndpoints` should get on `ConfigurationOptions` as a `public static` instead? Eyes should be given here on new public members (and naming!) on `ConnectionMultiplexer`. Maintenance events are meant to be extensible as part of this change. --- docs/ReleaseNotes.md | 1 + .../Configuration/AzureOptionsProvider.cs | 81 ++++++ .../Configuration/DefaultOptionsProvider.cs | 259 ++++++++++++++++++ .../ConfigurationOptions.cs | 165 ++++------- .../ConnectionMultiplexer.cs | 76 +---- src/StackExchange.Redis/ExceptionFactory.cs | 2 +- .../Maintenance/AzureMaintenanceEvent.cs | 19 +- .../Maintenance/ServerMaintenanceEvent.cs | 24 +- tests/StackExchange.Redis.Tests/Config.cs | 4 +- .../ConnectionFailedErrors.cs | 3 +- .../ConnectionReconnectRetryPolicyTests.cs | 4 +- .../DefaultOptions.cs | 158 +++++++++++ 12 files changed, 595 insertions(+), 201 deletions(-) create mode 100644 src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs create mode 100644 src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs create mode 100644 tests/StackExchange.Redis.Tests/DefaultOptions.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index d62389d5b..8df1cfbba 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -2,6 +2,7 @@ ## Unreleased - Adds bounds checking for `ExponentialRetry` backoff policy (#1921 via gliljas) +- Adds DefaultOptionsProvider support for endpoint-based defaults configuration (#1987 via NickCraver) - Adds Envoy proxy support (#1989 via rkarthick) - When `SUBSCRIBE` is disabled, give proper errors and connect faster (#2001 via NickCraver) - Adds `GET` on `SET` command support (present in Redis 6.2+ - #2003 via martinekvili) diff --git a/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs b/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs new file mode 100644 index 000000000..e4ccc92a1 --- /dev/null +++ b/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using StackExchange.Redis.Maintenance; + +namespace StackExchange.Redis.Configuration +{ + /// + /// Options provider for Azure environments. + /// + public class AzureOptionsProvider : DefaultOptionsProvider + { + /// + /// Allow connecting after startup, in the cases where remote cache isn't ready or is overloaded. + /// + public override bool AbortOnConnectFail => false; + + /// + /// The minimum version of Redis in Azure is 4, so use the widest set of available commands when connecting. + /// + public override Version DefaultVersion => RedisFeatures.v4_0_0; + + /// + /// List of domains known to be Azure Redis, so we can light up some helpful functionality + /// for minimizing downtime during maintenance events and such. + /// + private static readonly string[] azureRedisDomains = new[] + { + ".redis.cache.windows.net", + ".redis.cache.chinacloudapi.cn", + ".redis.cache.usgovcloudapi.net", + ".redis.cache.cloudapi.de", + ".redisenterprise.cache.azure.net", + }; + + /// + public override bool IsMatch(EndPoint endpoint) + { + if (endpoint is DnsEndPoint dnsEp) + { + foreach (var host in azureRedisDomains) + { + if (dnsEp.Host.EndsWith(host, StringComparison.InvariantCultureIgnoreCase)) + { + return true; + } + } + } + return false; + } + + /// + public override Task AfterConnectAsync(ConnectionMultiplexer muxer, Action log) + => AzureMaintenanceEvent.AddListenerAsync(muxer, log); + + /// + public override bool GetDefaultSsl(EndPointCollection endPoints) + { + foreach (var ep in endPoints) + { + switch (ep) + { + case DnsEndPoint dns: + if (dns.Port == 6380) + { + return true; + } + break; + case IPEndPoint ip: + if (ip.Port == 6380) + { + return true; + } + break; + } + } + return false; + } + } +} diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs new file mode 100644 index 000000000..98df24ce7 --- /dev/null +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; + +namespace StackExchange.Redis.Configuration +{ + /// + /// A defaults providers for . + /// This providers defaults not explicitly specified and is present to be inherited by environments that want to provide + /// better defaults for their use case, e.g. in a single wrapper library used many places. + /// + /// + /// Why not just have a default instance? Good question! + /// Since we null coalesce down to the defaults, there's an inherent pit-of-failure with that approach of . + /// If you forget anything or if someone creates a provider nulling these out...kaboom. + /// + public class DefaultOptionsProvider + { + /// + /// The known providers to match against (built into the lbirary) - the default set. + /// If none of these match, is used. + /// + private static readonly List BuiltInProviders = new() + { + new AzureOptionsProvider() + }; + + /// + /// The current list of providers to match (potentially modified from defaults via . + /// + private static LinkedList KnownProviders { get; } = new (BuiltInProviders); + + /// + /// Adds a provider to match endpoints against. The last provider added has the highest priority. + /// If you want your provider to match everything, implement as return true;. + /// + /// The provider to add. + public static void AddProvider(DefaultOptionsProvider provider) => KnownProviders.AddFirst(provider); + + /// + /// Whether this options provider matches a given endpoint, for automatically selecting a provider based on what's being connected to. + /// + public virtual bool IsMatch(EndPoint endpoint) => false; + + /// + /// Gets a provider for the given endpoints, falling back to if nothing more specific is found. + /// + internal static Func GetForEndpoints { get; } = (endpoints) => + { + foreach (var provider in KnownProviders) + { + foreach (var endpoint in endpoints) + { + if (provider.IsMatch(endpoint)) + { + return provider; + } + } + } + + return new DefaultOptionsProvider(); + }; + + /// + /// Gets or sets whether connect/configuration timeouts should be explicitly notified via a TimeoutException. + /// + public virtual bool AbortOnConnectFail => true; + + /// + /// Indicates whether admin operations should be allowed. + /// + public virtual bool AllowAdmin => false; + + /// + /// The backlog policy to be used for commands when a connection is unhealthy. + /// + public virtual BacklogPolicy BacklogPolicy => BacklogPolicy.Default; + + /// + /// A Boolean value that specifies whether the certificate revocation list is checked during authentication. + /// + public virtual bool CheckCertificateRevocation => true; + + /// + /// The number of times to repeat the initial connect cycle if no servers respond promptly. + /// + public virtual int ConnectRetry => 3; + + /// + /// Specifies the time that should be allowed for connection. + /// Falls back to Max(5000, SyncTimeout) if null. + /// + public virtual TimeSpan? ConnectTimeout => null; + + /// + /// The command-map associated with this configuration. + /// + public virtual CommandMap CommandMap => null; + + /// + /// Channel to use for broadcasting and listening for configuration change notification. + /// + public virtual string ConfigurationChannel => "__Booksleeve_MasterChanged"; + + /// + /// The server version to assume. + /// + public virtual Version DefaultVersion => RedisFeatures.v3_0_0; + + /// + /// Specifies the time interval at which connections should be pinged to ensure validity. + /// + public virtual TimeSpan KeepAliveInterval => TimeSpan.FromSeconds(60); + + /// + /// Type of proxy to use (if any); for example . + /// + public virtual Proxy Proxy => Proxy.None; + + /// + /// The retry policy to be used for connection reconnects. + /// + public virtual IReconnectRetryPolicy ReconnectRetryPolicy => null; + + /// + /// Indicates whether endpoints should be resolved via DNS before connecting. + /// If enabled the ConnectionMultiplexer will not re-resolve DNS when attempting to re-connect after a connection failure. + /// + public virtual bool ResolveDns => false; + + /// + /// Specifies the time that the system should allow for synchronous operations. + /// + public virtual TimeSpan SyncTimeout => TimeSpan.FromSeconds(5); + + /// + /// Tie-breaker used to choose between masters (must match the endpoint exactly). + /// + public virtual string TieBreaker => "__Booksleeve_TieBreak"; + + /// + /// Check configuration every n interval. + /// + public virtual TimeSpan ConfigCheckInterval => TimeSpan.FromMinutes(1); + + // We memoize this to reduce cost on re-access + private string defaultClientName; + /// + /// The default client name for a connection, with the library version appended. + /// + public string ClientName => defaultClientName ??= GetDefaultClientName(); + + /// + /// Gets the default client name for a connection. + /// + protected virtual string GetDefaultClientName() => + (TryGetAzureRoleInstanceIdNoThrow() + ?? ComputerName + ?? "StackExchange.Redis") + "(SE.Redis-v" + LibraryVersion + ")"; + + /// + /// String version of the StackExchange.Redis library, for use in any options. + /// + protected static string LibraryVersion => Utils.GetLibVersion(); + + /// + /// Name of the machine we're running on, for use in any options. + /// + protected static string ComputerName => Environment.MachineName ?? Environment.GetEnvironmentVariable("ComputerName"); + + /// + /// Tries to get the RoleInstance Id if Microsoft.WindowsAzure.ServiceRuntime is loaded. + /// In case of any failure, swallows the exception and returns null. + /// + /// + /// Azure, in the default provider? Yes, to maintain existing compatibility/convenience. + /// Source != destination here. + /// + internal static string TryGetAzureRoleInstanceIdNoThrow() + { + string roleInstanceId; + try + { + Assembly asm = null; + foreach (var asmb in AppDomain.CurrentDomain.GetAssemblies()) + { + if (asmb.GetName().Name.Equals("Microsoft.WindowsAzure.ServiceRuntime")) + { + asm = asmb; + break; + } + } + if (asm == null) + return null; + + var type = asm.GetType("Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment"); + + // https://msdn.microsoft.com/en-us/library/microsoft.windowsazure.serviceruntime.roleenvironment.isavailable.aspx + if (!(bool)type.GetProperty("IsAvailable").GetValue(null, null)) + return null; + + var currentRoleInstanceProp = type.GetProperty("CurrentRoleInstance"); + var currentRoleInstanceId = currentRoleInstanceProp.GetValue(null, null); + roleInstanceId = currentRoleInstanceId.GetType().GetProperty("Id").GetValue(currentRoleInstanceId, null).ToString(); + + if (string.IsNullOrEmpty(roleInstanceId)) + { + roleInstanceId = null; + } + } + catch (Exception) + { + //silently ignores the exception + roleInstanceId = null; + } + return roleInstanceId; + } + + /// + /// The action to perform, if any, immediately after an initial connection completes. + /// + /// The multiplexer that just connected. + /// The logger for the connection, to emit to the connection output log. + public virtual Task AfterConnectAsync(ConnectionMultiplexer multiplexer, Action log) => Task.CompletedTask; + + /// + /// Gets the default SSL "enabled or not" based on a set of endpoints. + /// Note: this setting then applies for *all* endpoints. + /// + /// The configured endpoints to determine SSL usage from (e.g. from the port). + /// Whether to enable SSL for connections (unless excplicitly overriden in a direct set). + public virtual bool GetDefaultSsl(EndPointCollection endPoints) => false; + + /// + /// Gets the SSL Host to check for when connecting to endpoints (customizable in case of internal certificate shenanigans. + /// + /// The configured endpoints to determine SSL host from (e.g. from the port). + /// The common host, if any, detected from the endpoint collection. + public virtual string GetSslHostFromEndpoints(EndPointCollection endPoints) + { + string commonHost = null; + foreach (var endpoint in endPoints) + { + if (endpoint is DnsEndPoint dnsEndpoint) + { + commonHost ??= dnsEndpoint.Host; + // Mismatch detected, no assumptions. + if (dnsEndpoint.Host != commonHost) + { + return null; + } + } + } + return commonHost; + } + } +} diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 887d2a45c..4108ba1de 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using StackExchange.Redis.Configuration; using static StackExchange.Redis.ConnectionMultiplexer; namespace StackExchange.Redis @@ -18,8 +19,6 @@ namespace StackExchange.Redis /// public sealed class ConfigurationOptions : ICloneable { - internal const string DefaultTieBreaker = "__Booksleeve_TieBreak", DefaultConfigurationChannel = "__Booksleeve_MasterChanged"; - private static class OptionKeys { public static int ParseInt32(string key, string value, int minValue = int.MinValue, int maxValue = int.MaxValue) @@ -132,6 +131,8 @@ public static string TryNormalize(string value) } } + private DefaultOptionsProvider defaultOptions; + private bool? allowAdmin, abortOnConnectFail, highPrioritySocketThreads, resolveDns, ssl, checkCertificateRevocation; private string tieBreaker, sslHost, configChannel; @@ -140,7 +141,7 @@ public static string TryNormalize(string value) private Version defaultVersion; - private int? keepAlive, asyncTimeout, syncTimeout, connectTimeout, responseTimeout, writeBuffer, connectRetry, configCheckSeconds; + private int? keepAlive, asyncTimeout, syncTimeout, connectTimeout, responseTimeout, connectRetry, configCheckSeconds; private Proxy? proxy; @@ -163,25 +164,36 @@ public static string TryNormalize(string value) public event RemoteCertificateValidationCallback CertificateValidation; /// - /// Gets or sets whether connect/configuration timeouts should be explicitly notified via a TimeoutException + /// The default (not explicitly configured) options for this connection, fetched based on our parsed endpoints. + /// + public DefaultOptionsProvider Defaults + { + get => defaultOptions ??= DefaultOptionsProvider.GetForEndpoints(EndPoints); + set => defaultOptions = value; + } + + internal Func, Task> AfterConnectAsync => Defaults.AfterConnectAsync; + + /// + /// Gets or sets whether connect/configuration timeouts should be explicitly notified via a TimeoutException. /// public bool AbortOnConnectFail { - get => abortOnConnectFail ?? GetDefaultAbortOnConnectFailSetting(); + get => abortOnConnectFail ?? Defaults.AbortOnConnectFail; set => abortOnConnectFail = value; } /// - /// Indicates whether admin operations should be allowed + /// Indicates whether admin operations should be allowed. /// public bool AllowAdmin { - get => allowAdmin.GetValueOrDefault(); + get => allowAdmin ?? Defaults.AllowAdmin; set => allowAdmin = value; } /// - /// Specifies the time in milliseconds that the system should allow for asynchronous operations (defaults to SyncTimeout) + /// Specifies the time in milliseconds that the system should allow for asynchronous operations (defaults to SyncTimeout). /// public int AsyncTimeout { @@ -192,7 +204,7 @@ public int AsyncTimeout /// /// Indicates whether the connection should be encrypted /// - [Obsolete("Please use .Ssl instead of .UseSsl"), + [Obsolete("Please use .Ssl instead of .UseSsl, will be removed in 3.0."), Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public bool UseSsl @@ -211,7 +223,7 @@ public bool UseSsl /// public bool CheckCertificateRevocation { - get => checkCertificateRevocation ?? true; + get => checkCertificateRevocation ?? Defaults.CheckCertificateRevocation; set => checkCertificateRevocation = value; } @@ -263,7 +275,7 @@ private static bool CheckTrustedIssuer(X509Certificate2 certificateToValidate, X /// public int ConnectRetry { - get => connectRetry ?? 3; + get => connectRetry ?? Defaults.ConnectRetry; set => connectRetry = value; } @@ -272,7 +284,7 @@ public int ConnectRetry /// public CommandMap CommandMap { - get => commandMap ?? Proxy switch + get => commandMap ?? Defaults.CommandMap ?? Proxy switch { Proxy.Twemproxy => CommandMap.Twemproxy, Proxy.Envoyproxy => CommandMap.Envoyproxy, @@ -286,7 +298,7 @@ public CommandMap CommandMap /// public string ConfigurationChannel { - get => configChannel ?? DefaultConfigurationChannel; + get => configChannel ?? Defaults.ConfigurationChannel; set => configChannel = value; } @@ -295,7 +307,7 @@ public string ConfigurationChannel /// public int ConnectTimeout { - get => connectTimeout ?? Math.Max(5000, SyncTimeout); + get => connectTimeout ?? ((int?)Defaults.ConnectTimeout?.TotalMilliseconds) ?? Math.Max(5000, SyncTimeout); set => connectTimeout = value; } @@ -309,7 +321,7 @@ public int ConnectTimeout /// public Version DefaultVersion { - get => defaultVersion ?? (IsAzureEndpoint() ? RedisFeatures.v4_0_0 : RedisFeatures.v3_0_0); + get => defaultVersion ?? Defaults.DefaultVersion; set => defaultVersion = value; } @@ -330,10 +342,11 @@ public bool HighPrioritySocketThreads /// /// Specifies the time in seconds at which connections should be pinged to ensure validity. + /// -1 Defaults to 60 Seconds /// public int KeepAlive { - get => keepAlive ?? -1; + get => keepAlive ?? (int)Defaults.KeepAliveInterval.TotalSeconds; set => keepAlive = value; } @@ -350,7 +363,7 @@ public int KeepAlive /// /// Specifies whether asynchronous operations should be invoked in a way that guarantees their original delivery order. /// - [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue), false)] + [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue) + ", will be removed in 3.0.", false)] public bool PreserveAsyncOrder { get => false; @@ -362,7 +375,7 @@ public bool PreserveAsyncOrder /// public Proxy Proxy { - get => proxy.GetValueOrDefault(); + get => proxy ?? Defaults.Proxy; set => proxy = value; } @@ -371,7 +384,7 @@ public Proxy Proxy /// public IReconnectRetryPolicy ReconnectRetryPolicy { - get => reconnectRetryPolicy ??= new ExponentialRetry(ConnectTimeout / 2); + get => reconnectRetryPolicy ??= Defaults.ReconnectRetryPolicy ?? new ExponentialRetry(ConnectTimeout / 2); set => reconnectRetryPolicy = value; } @@ -380,25 +393,24 @@ public IReconnectRetryPolicy ReconnectRetryPolicy /// public BacklogPolicy BacklogPolicy { - get => backlogPolicy ?? BacklogPolicy.Default; + get => backlogPolicy ?? Defaults.BacklogPolicy; set => backlogPolicy = value; } /// /// Indicates whether endpoints should be resolved via DNS before connecting. - /// If enabled the ConnectionMultiplexer will not re-resolve DNS - /// when attempting to re-connect after a connection failure. + /// If enabled the ConnectionMultiplexer will not re-resolve DNS when attempting to re-connect after a connection failure. /// public bool ResolveDns { - get => resolveDns.GetValueOrDefault(); + get => resolveDns ?? Defaults.ResolveDns; set => resolveDns = value; } /// /// Specifies the time in milliseconds that the system should allow for responses before concluding that the socket is unhealthy. /// - [Obsolete("This setting no longer has any effect, and should not be used")] + [Obsolete("This setting no longer has any effect, and should not be used - will be removed in 3.0.")] public int ResponseTimeout { get => 0; @@ -421,7 +433,7 @@ public int ResponseTimeout /// public bool Ssl { - get => ssl.GetValueOrDefault(); + get => ssl ?? Defaults.GetDefaultSsl(EndPoints); set => ssl = value; } @@ -430,7 +442,7 @@ public bool Ssl /// public string SslHost { - get => sslHost ?? InferSslHostFromEndpoints(); + get => sslHost ?? Defaults.GetSslHostFromEndpoints(EndPoints); set => sslHost = value; } @@ -444,7 +456,7 @@ public string SslHost /// public int SyncTimeout { - get => syncTimeout ?? 5000; + get => syncTimeout ?? (int)Defaults.SyncTimeout.TotalMilliseconds; set => syncTimeout = value; } @@ -453,14 +465,14 @@ public int SyncTimeout /// public string TieBreaker { - get => tieBreaker ?? DefaultTieBreaker; + get => tieBreaker ?? Defaults.TieBreaker; set => tieBreaker = value; } /// /// The size of the output buffer to use. /// - [Obsolete("This setting no longer has any effect, and should not be used")] + [Obsolete("This setting no longer has any effect, and should not be used - will be removed in 3.0.")] public int WriteBuffer { get => 0; @@ -485,7 +497,7 @@ internal RemoteCertificateValidationCallback CertificateValidationCallback /// public int ConfigCheckSeconds { - get => configCheckSeconds ?? 60; + get => configCheckSeconds ?? (int)Defaults.ConfigCheckInterval.TotalSeconds; set => configCheckSeconds = value; } @@ -495,12 +507,7 @@ public int ConfigCheckSeconds /// The configuration string to parse. /// is . /// is empty. - public static ConfigurationOptions Parse(string configuration) - { - var options = new ConfigurationOptions(); - options.DoParse(configuration, false); - return options; - } + public static ConfigurationOptions Parse(string configuration) => Parse(configuration, false); /// /// Parse the configuration from a comma-delimited configuration string. @@ -509,12 +516,8 @@ public static ConfigurationOptions Parse(string configuration) /// Whether to ignore unknown elements in . /// is . /// is empty. - public static ConfigurationOptions Parse(string configuration, bool ignoreUnknown) - { - var options = new ConfigurationOptions(); - options.DoParse(configuration, ignoreUnknown); - return options; - } + public static ConfigurationOptions Parse(string configuration, bool ignoreUnknown) => + new ConfigurationOptions().DoParse(configuration, ignoreUnknown); /// /// Create a copy of the configuration. @@ -523,6 +526,7 @@ public ConfigurationOptions Clone() { var options = new ConfigurationOptions { + defaultOptions = defaultOptions, ClientName = ClientName, ServiceName = ServiceName, keepAlive = keepAlive, @@ -534,7 +538,6 @@ public ConfigurationOptions Clone() User = User, Password = Password, tieBreaker = tieBreaker, - writeBuffer = writeBuffer, ssl = ssl, sslHost = sslHost, highPrioritySocketThreads = highPrioritySocketThreads, @@ -623,7 +626,6 @@ public string ToString(bool includePassword) Append(sb, OptionKeys.User, User); Append(sb, OptionKeys.Password, (includePassword || string.IsNullOrEmpty(Password)) ? Password : "*****"); Append(sb, OptionKeys.TieBreaker, tieBreaker); - Append(sb, OptionKeys.WriteBuffer, writeBuffer); Append(sb, OptionKeys.Ssl, ssl); Append(sb, OptionKeys.SslProtocols, SslProtocols?.ToString().Replace(',', '|')); Append(sb, OptionKeys.CheckCertificateRevocation, checkCertificateRevocation); @@ -721,7 +723,7 @@ private static void Append(StringBuilder sb, string prefix, object value) private void Clear() { ClientName = ServiceName = User = Password = tieBreaker = sslHost = configChannel = null; - keepAlive = syncTimeout = asyncTimeout = connectTimeout = writeBuffer = connectRetry = configCheckSeconds = DefaultDatabase = null; + keepAlive = syncTimeout = asyncTimeout = connectTimeout = connectRetry = configCheckSeconds = DefaultDatabase = null; allowAdmin = abortOnConnectFail = highPrioritySocketThreads = resolveDns = ssl = null; SslProtocols = null; defaultVersion = null; @@ -730,13 +732,13 @@ private void Clear() CertificateSelection = null; CertificateValidation = null; - ChannelPrefix = default(RedisChannel); + ChannelPrefix = default; SocketManager = null; } object ICloneable.Clone() => Clone(); - private void DoParse(string configuration, bool ignoreUnknown) + private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) { if (configuration == null) { @@ -745,7 +747,7 @@ private void DoParse(string configuration, bool ignoreUnknown) if (string.IsNullOrWhiteSpace(configuration)) { - throw new ArgumentException("is empty", configuration); + throw new ArgumentException("is empty", nameof(configuration)); } Clear(); @@ -831,34 +833,27 @@ private void DoParse(string configuration, bool ignoreUnknown) case OptionKeys.HighPrioritySocketThreads: HighPrioritySocketThreads = OptionKeys.ParseBoolean(key, value); break; - case OptionKeys.WriteBuffer: -#pragma warning disable CS0618 // Type or member is obsolete - WriteBuffer = OptionKeys.ParseInt32(key, value); -#pragma warning restore CS0618 - break; case OptionKeys.Proxy: Proxy = OptionKeys.ParseProxy(key, value); break; - case OptionKeys.ResponseTimeout: -#pragma warning disable CS0618 // Type or member is obsolete - ResponseTimeout = OptionKeys.ParseInt32(key, value, minValue: 1); -#pragma warning restore CS0618 - break; case OptionKeys.DefaultDatabase: DefaultDatabase = OptionKeys.ParseInt32(key, value); break; - case OptionKeys.PreserveAsyncOrder: - break; case OptionKeys.SslProtocols: SslProtocols = OptionKeys.ParseSslProtocols(key, value); break; + // Deprecated options we ignore... + case OptionKeys.PreserveAsyncOrder: + case OptionKeys.ResponseTimeout: + case OptionKeys.WriteBuffer: + break; default: if (!string.IsNullOrEmpty(key) && key[0] == '$') { var cmdName = option.Substring(1, idx - 1); if (Enum.TryParse(cmdName, true, out RedisCommand cmd)) { - if (map == null) map = new Dictionary(StringComparer.OrdinalIgnoreCase); + map ??= new Dictionary(StringComparer.OrdinalIgnoreCase); map[cmdName] = value; } } @@ -879,53 +874,7 @@ private void DoParse(string configuration, bool ignoreUnknown) { CommandMap = CommandMap.Create(map); } - } - - ///Microsoft Azure team wants abortConnect=false by default. - private bool GetDefaultAbortOnConnectFailSetting() => !IsAzureEndpoint(); - - /// - /// List of domains known to be Azure Redis, so we can light up some helpful functionality - /// for minimizing downtime during maintenance events and such. - /// - private static readonly List azureRedisDomains = new() - { - ".redis.cache.windows.net", - ".redis.cache.chinacloudapi.cn", - ".redis.cache.usgovcloudapi.net", - ".redis.cache.cloudapi.de", - ".redisenterprise.cache.azure.net", - }; - - internal bool IsAzureEndpoint() - { - foreach (var ep in EndPoints) - { - if (ep is DnsEndPoint dnsEp) - { - foreach (var host in azureRedisDomains) - { - if (dnsEp.Host.EndsWith(host, StringComparison.InvariantCultureIgnoreCase)) - { - return true; - } - } - } - } - - return false; - } - - private string InferSslHostFromEndpoints() - { - var dnsEndpoints = EndPoints.Select(endpoint => endpoint as DnsEndPoint); - string dnsHost = dnsEndpoints.FirstOrDefault()?.Host; - if (dnsEndpoints.All(dnsEndpoint => dnsEndpoint != null && dnsEndpoint.Host == dnsHost)) - { - return dnsHost; - } - - return null; + return this; } } } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index cd1b44499..070c025ae 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -145,65 +145,10 @@ public ServerCounters GetCounters() /// /// Gets the client-name that will be used on all new connections. /// - public string ClientName => RawConfig.ClientName ?? GetDefaultClientName(); - - private static string defaultClientName; - - /// - /// Gets the client name for a connection, with the library version appended. - /// - private static string GetDefaultClientName() - { - return defaultClientName ??= (TryGetAzureRoleInstanceIdNoThrow() - ?? Environment.MachineName - ?? Environment.GetEnvironmentVariable("ComputerName") - ?? "StackExchange.Redis") + "(v" + Utils.GetLibVersion() + ")"; - } - - /// - /// Tries to get the RoleInstance Id if Microsoft.WindowsAzure.ServiceRuntime is loaded. - /// In case of any failure, swallows the exception and returns null. - /// - internal static string TryGetAzureRoleInstanceIdNoThrow() - { - string roleInstanceId; - // TODO: CoreCLR port pending https://github.com/dotnet/coreclr/issues/919 - try - { - Assembly asm = null; - foreach (var asmb in AppDomain.CurrentDomain.GetAssemblies()) - { - if (asmb.GetName().Name.Equals("Microsoft.WindowsAzure.ServiceRuntime")) - { - asm = asmb; - break; - } - } - if (asm == null) - return null; - - var type = asm.GetType("Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment"); - - // https://msdn.microsoft.com/en-us/library/microsoft.windowsazure.serviceruntime.roleenvironment.isavailable.aspx - if (!(bool)type.GetProperty("IsAvailable").GetValue(null, null)) - return null; - - var currentRoleInstanceProp = type.GetProperty("CurrentRoleInstance"); - var currentRoleInstanceId = currentRoleInstanceProp.GetValue(null, null); - roleInstanceId = currentRoleInstanceId.GetType().GetProperty("Id").GetValue(currentRoleInstanceId, null).ToString(); - - if (string.IsNullOrEmpty(roleInstanceId)) - { - roleInstanceId = null; - } - } - catch (Exception) - { - //silently ignores the exception - roleInstanceId = null; - } - return roleInstanceId; - } + /// + /// We null coalesce here instead of in Options so that we don't populate it everywhere (e.g. .ToString()), given it's a default. + /// + public string ClientName => RawConfig.ClientName ?? RawConfig.Defaults.ClientName; /// /// Gets the configuration of the connection. @@ -911,7 +856,7 @@ private static async Task ConnectImplAsync(ConfigurationO muxer.InitializeSentinel(logProxy); } - await Maintenance.ServerMaintenanceEvent.AddListenersAsync(muxer, logProxy).ForAwait(); + await configuration.AfterConnectAsync(muxer, logProxy != null ? logProxy.WriteLine : LogProxy.NullWriter).ForAwait(); logProxy?.WriteLine($"Total connect time: {sw.ElapsedMilliseconds:n0} ms"); @@ -978,6 +923,7 @@ public override string ToString() return s ?? base.ToString(); } private TextWriter _log; + internal static Action NullWriter = _ => {}; public object SyncLock => this; private LogProxy(TextWriter log) => _log = log; @@ -1223,7 +1169,7 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat muxer.InitializeSentinel(logProxy); } - Maintenance.ServerMaintenanceEvent.AddListenersAsync(muxer, logProxy).Wait(muxer.SyncConnectTimeout(true)); + configuration.AfterConnectAsync(muxer, logProxy != null ? logProxy.WriteLine : LogProxy.NullWriter).Wait(muxer.SyncConnectTimeout(true)); logProxy?.WriteLine($"Total connect time: {sw.ElapsedMilliseconds:n0} ms"); @@ -1679,6 +1625,14 @@ private void ActivateAllServers(LogProxy log) } } + /// + /// Triggers a reconfigure of this multiplexer. + /// This re-assessment of all server endpoints to get the current topology and adjust, the same as if we had first connected. + /// TODO: Naming? + /// + public Task ReconfigureAsync(string reason) => + ReconfigureAsync(first: false, reconfigureAll: false, log: null, blame: null, cause: reason); + internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogProxy log, EndPoint blame, string cause, bool publishReconfigure = false, CommandFlags publishReconfigureFlags = CommandFlags.None) { if (_isDisposed) throw new ObjectDisposedException(ToString()); diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 6228bbb90..6a42d02af 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -122,7 +122,7 @@ internal static Exception NoConnectionAvailable( else if (!multiplexer.RawConfig.AbortOnConnectFail && attempts > multiplexer.RawConfig.ConnectRetry && completions == 0) { // Attempted use after a full initial retry connect count # of failures - // This can happen in Azure often, where user disables abort and has the wrong config + // This can happen in cloud environments often, where user disables abort and has the wrong config initialMessage = $"Connection to Redis never succeeded (attempts: {attempts} - check your config), unable to service operation: "; } else diff --git a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs index a413dab1e..84d90ea85 100644 --- a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs @@ -80,8 +80,6 @@ internal AzureMaintenanceEvent(string azureEvent) case var _ when key.SequenceEqual("NonSSLPort".AsSpan()) && Format.TryParseInt32(value, out var nonsslport): NonSslPort = nonsslport; break; - default: - break; } #else switch (key) @@ -105,8 +103,6 @@ internal AzureMaintenanceEvent(string azureEvent) case var _ when key.SequenceEqual("NonSSLPort".AsSpan()) && Format.TryParseInt32(value.ToString(), out var nonsslport): NonSslPort = nonsslport; break; - default: - break; } #endif } @@ -118,35 +114,40 @@ internal AzureMaintenanceEvent(string azureEvent) } } - internal async static Task AddListenerAsync(ConnectionMultiplexer multiplexer, ConnectionMultiplexer.LogProxy logProxy) + internal async static Task AddListenerAsync(ConnectionMultiplexer multiplexer, Action log = null) { + if (!multiplexer.CommandMap.IsAvailable(RedisCommand.SUBSCRIBE)) + { + return; + } + try { var sub = multiplexer.GetSubscriber(); if (sub == null) { - logProxy?.WriteLine("Failed to GetSubscriber for AzureRedisEvents"); + log?.Invoke("Failed to GetSubscriber for AzureRedisEvents"); return; } await sub.SubscribeAsync(PubSubChannelName, async (_, message) => { var newMessage = new AzureMaintenanceEvent(message); - multiplexer.InvokeServerMaintenanceEvent(newMessage); + newMessage.NotifyMultiplexer(multiplexer); switch (newMessage.NotificationType) { case AzureNotificationType.NodeMaintenanceEnded: case AzureNotificationType.NodeMaintenanceFailoverComplete: case AzureNotificationType.NodeMaintenanceScaleComplete: - await multiplexer.ReconfigureAsync(first: false, reconfigureAll: true, log: logProxy, blame: null, cause: $"Azure Event: {newMessage.NotificationType}").ForAwait(); + await multiplexer.ReconfigureAsync($"Azure Event: {newMessage.NotificationType}").ForAwait(); break; } }).ForAwait(); } catch (Exception e) { - logProxy?.WriteLine($"Encountered exception: {e}"); + log?.Invoke($"Encountered exception: {e}"); } } diff --git a/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs index 20246eacb..e9746747e 100644 --- a/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs @@ -1,11 +1,9 @@ using System; -using System.Threading.Tasks; -using static StackExchange.Redis.ConnectionMultiplexer; namespace StackExchange.Redis.Maintenance { /// - /// Base class for all server maintenance events + /// Base class for all server maintenance events. /// public class ServerMaintenanceEvent { @@ -14,20 +12,6 @@ internal ServerMaintenanceEvent() ReceivedTimeUtc = DateTime.UtcNow; } - internal async static Task AddListenersAsync(ConnectionMultiplexer muxer, LogProxy logProxy) - { - if (!muxer.CommandMap.IsAvailable(RedisCommand.SUBSCRIBE)) - { - return; - } - - if (muxer.RawConfig.IsAzureEndpoint()) - { - await AzureMaintenanceEvent.AddListenerAsync(muxer, logProxy).ForAwait(); - } - // Other providers could be added here later - } - /// /// Raw message received from the server. /// @@ -47,5 +31,11 @@ internal async static Task AddListenersAsync(ConnectionMultiplexer muxer, LogPro /// Returns a string representing the maintenance event with all of its properties. /// public override string ToString() => RawMessage; + + /// + /// Notifies a ConnectionMultiplexer of this event, for anyone observing its handler. + /// + protected void NotifyMultiplexer(ConnectionMultiplexer multiplexer) + => multiplexer.InvokeServerMaintenanceEvent(this); } } diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index 0494874c8..969836b96 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -243,12 +243,12 @@ public void DefaultClientName() { using (var muxer = Create(allowAdmin: true, caller: null)) // force default naming to kick in { - Assert.Equal($"{Environment.MachineName}(v{Utils.GetLibVersion()})", muxer.ClientName); + Assert.Equal($"{Environment.MachineName}(SE.Redis-v{Utils.GetLibVersion()})", muxer.ClientName); var conn = muxer.GetDatabase(); conn.Ping(); var name = (string)GetAnyMaster(muxer).Execute("CLIENT", "GETNAME"); - Assert.Equal($"{Environment.MachineName}(v{Utils.GetLibVersion()})", name); + Assert.Equal($"{Environment.MachineName}(SE.Redis-v{Utils.GetLibVersion()})", name); } } diff --git a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs b/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs index 94623e4fd..9989a4bb7 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Security.Authentication; using System.Threading.Tasks; +using StackExchange.Redis.Configuration; using Xunit; using Xunit.Abstractions; @@ -166,7 +167,7 @@ void innerScenario() [Fact] public void TryGetAzureRoleInstanceIdNoThrow() { - Assert.Null(ConnectionMultiplexer.TryGetAzureRoleInstanceIdNoThrow()); + Assert.Null(DefaultOptionsProvider.TryGetAzureRoleInstanceIdNoThrow()); } #if DEBUG diff --git a/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs b/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs index cdce5dd76..5f9fe64dc 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs @@ -28,8 +28,8 @@ public void TestExponentialMaxRetry() [Fact] public void TestExponentialRetryArgs() { - new ExponentialRetry(5000); - new ExponentialRetry(5000, 10000); + _ = new ExponentialRetry(5000); + _ = new ExponentialRetry(5000, 10000); var ex = Assert.Throws(() => new ExponentialRetry(-1)); Assert.Equal("deltaBackOffMilliseconds", ex.ParamName); diff --git a/tests/StackExchange.Redis.Tests/DefaultOptions.cs b/tests/StackExchange.Redis.Tests/DefaultOptions.cs new file mode 100644 index 000000000..d5317cfbb --- /dev/null +++ b/tests/StackExchange.Redis.Tests/DefaultOptions.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using StackExchange.Redis.Configuration; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests +{ + public class DefaultOptions : TestBase + { + public DefaultOptions(ITestOutputHelper output) : base(output) { } + + public class TestOptionsProvider : DefaultOptionsProvider + { + private readonly string _domainSuffix; + public TestOptionsProvider(string domainSuffix) => _domainSuffix = domainSuffix; + + public override bool AbortOnConnectFail => true; + public override TimeSpan? ConnectTimeout => TimeSpan.FromSeconds(123); + public override bool AllowAdmin => true; + public override BacklogPolicy BacklogPolicy => BacklogPolicy.FailFast; + public override bool CheckCertificateRevocation => true; + public override CommandMap CommandMap => CommandMap.Create(new HashSet() { "SELECT" }); + public override TimeSpan ConfigCheckInterval => TimeSpan.FromSeconds(124); + public override string ConfigurationChannel => "TestConfigChannel"; + public override int ConnectRetry => 123; + public override Version DefaultVersion => new Version(1, 2, 3, 4); + protected override string GetDefaultClientName() => "TestPrefix-" + base.GetDefaultClientName(); + public override bool IsMatch(EndPoint endpoint) => endpoint is DnsEndPoint dnsep && dnsep.Host.EndsWith(_domainSuffix); + public override TimeSpan KeepAliveInterval => TimeSpan.FromSeconds(125); + public override Proxy Proxy => Proxy.Twemproxy; + public override IReconnectRetryPolicy ReconnectRetryPolicy => new TestRetryPolicy(); + public override bool ResolveDns => true; + public override TimeSpan SyncTimeout => TimeSpan.FromSeconds(126); + public override string TieBreaker => "TestTiebreaker"; + } + + public class TestRetryPolicy : IReconnectRetryPolicy + { + public bool ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSinceLastRetry) => false; + } + + [Fact] + public void IsMatchOnDomain() + { + DefaultOptionsProvider.AddProvider(new TestOptionsProvider(".testdomain")); + + var epc = new EndPointCollection(new List() { new DnsEndPoint("local.testdomain", 0) }); + var provider = DefaultOptionsProvider.GetForEndpoints(epc); + Assert.IsType(provider); + + epc = new EndPointCollection(new List() { new DnsEndPoint("local.nottestdomain", 0) }); + provider = DefaultOptionsProvider.GetForEndpoints(epc); + Assert.IsType(provider); + } + + [Fact] + public void AllOverridesFromDefaultsProp() + { + var options = ConfigurationOptions.Parse("localhost"); + Assert.IsType(options.Defaults); + options.Defaults = new TestOptionsProvider(""); + Assert.IsType(options.Defaults); + AssertAllOverrides(options); + } + + [Fact] + public void AllOverridesFromEndpointsParse() + { + DefaultOptionsProvider.AddProvider(new TestOptionsProvider(".parse")); + var options = ConfigurationOptions.Parse("localhost.parse:6379"); + Assert.IsType(options.Defaults); + AssertAllOverrides(options); + } + + private static void AssertAllOverrides(ConfigurationOptions options) + { + Assert.True(options.AbortOnConnectFail); + Assert.Equal(TimeSpan.FromSeconds(123), TimeSpan.FromMilliseconds(options.ConnectTimeout)); + + Assert.True(options.AllowAdmin); + Assert.Equal(BacklogPolicy.FailFast, options.BacklogPolicy); + Assert.True(options.CheckCertificateRevocation); + + Assert.True(options.CommandMap.IsAvailable(RedisCommand.SELECT)); + Assert.False(options.CommandMap.IsAvailable(RedisCommand.GET)); + + Assert.Equal(TimeSpan.FromSeconds(124), TimeSpan.FromSeconds(options.ConfigCheckSeconds)); + Assert.Equal("TestConfigChannel", options.ConfigurationChannel); + Assert.Equal(123, options.ConnectRetry); + Assert.Equal(new Version(1, 2, 3, 4), options.DefaultVersion); + + Assert.Equal(TimeSpan.FromSeconds(125), TimeSpan.FromSeconds(options.KeepAlive)); + Assert.Equal(Proxy.Twemproxy, options.Proxy); + Assert.IsType(options.ReconnectRetryPolicy); + Assert.True(options.ResolveDns); + Assert.Equal(TimeSpan.FromSeconds(126), TimeSpan.FromMilliseconds(options.SyncTimeout)); + Assert.Equal("TestTiebreaker", options.TieBreaker); + } + + public class TestAfterConnectOptionsProvider : DefaultOptionsProvider + { + public int Calls; + + public override Task AfterConnectAsync(ConnectionMultiplexer muxer, Action log) + { + Interlocked.Increment(ref Calls); + log("TestAfterConnectOptionsProvider.AfterConnectAsync!"); + return Task.CompletedTask; + } + } + + [Fact] + public async Task AfterConnectAsyncHandler() + { + var options = ConfigurationOptions.Parse(GetConfiguration()); + var provider = new TestAfterConnectOptionsProvider(); + options.Defaults = provider; + + using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); + + Assert.True(muxer.IsConnected); + Assert.Equal(1, provider.Calls); + } + + public class TestClientNameOptionsProvider : DefaultOptionsProvider + { + protected override string GetDefaultClientName() => "Hey there"; + } + + [Fact] + public async Task ClientNameOverride() + { + var options = ConfigurationOptions.Parse(GetConfiguration()); + options.Defaults = new TestClientNameOptionsProvider(); + + using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); + + Assert.True(muxer.IsConnected); + Assert.Equal("Hey there", muxer.ClientName); + } + + [Fact] + public async Task ClientNameExplicitWins() + { + var options = ConfigurationOptions.Parse(GetConfiguration() + ",name=FooBar"); + options.Defaults = new TestClientNameOptionsProvider(); + + using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); + + Assert.True(muxer.IsConnected); + Assert.Equal("FooBar", muxer.ClientName); + } + } +} From a2a5ac6807ef5205e199ede4a3a432b9f920bed6 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 5 Mar 2022 15:10:12 -0500 Subject: [PATCH 096/435] v2.5 (#2021) This is mainly versioning and a release notes overhaul (normalization). I was tidying release notes anyway so went through and tried to normalize attribution, add attribution where missing, and link to issues and PRs. --- docs/ReleaseNotes.md | 351 +++++++++++++++++++++---------------------- version.json | 2 +- 2 files changed, 174 insertions(+), 179 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 8df1cfbba..326c50e16 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,204 +1,206 @@ # Release Notes -## Unreleased -- Adds bounds checking for `ExponentialRetry` backoff policy (#1921 via gliljas) -- Adds DefaultOptionsProvider support for endpoint-based defaults configuration (#1987 via NickCraver) -- Adds Envoy proxy support (#1989 via rkarthick) -- When `SUBSCRIBE` is disabled, give proper errors and connect faster (#2001 via NickCraver) -- Adds `GET` on `SET` command support (present in Redis 6.2+ - #2003 via martinekvili) -- Improves concurrent load performance when backlogs are utilized (#2008 via NickCraver) -- Improves cluster connections when `CLUSTER` command is disabled (#2014 via tylerohlsen) -- Improves connection logging and adds overall timing to it (#2019 via NickCraver) +## 2.5.43 + +- Adds: Bounds checking for `ExponentialRetry` backoff policy ([#1921 by gliljas](https://github.com/StackExchange/StackExchange.Redis/pull/1921)) +- Adds: `DefaultOptionsProvider` support for endpoint-based defaults configuration ([#1987 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1987)) +- Adds: Envoy proxy support ([#1989 by rkarthick](https://github.com/StackExchange/StackExchange.Redis/pull/1989)) +- Performance: When `SUBSCRIBE` is disabled, give proper errors and connect faster ([#2001 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2001)) +- Adds: `GET` on `SET` command support (present in Redis 6.2+ - [#2003 by martinekvili](https://github.com/StackExchange/StackExchange.Redis/pull/2003)) +- Performance: Improves concurrent load performance when backlogs are utilized ([#2008 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2008)) +- Stability: Improves cluster connections when `CLUSTER` command is disabled ([#2014 by tylerohlsen](https://github.com/StackExchange/StackExchange.Redis/pull/2014)) +- Logging: Improves connection logging and adds overall timing to it ([#2019 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2019)) ## 2.5.27 (prerelease) -- Adds a backlog/retry mechanism for commands issued while a connection isn't available (#1912 via NickCraver) +- Adds: a backlog/retry mechanism for commands issued while a connection isn't available ([#1912 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1912)) - Commands will be queued if a multiplexer isn't yet connected to a Redis server. - Commands will be queued if a connection is lost and then sent to the server when the connection is restored. - All commands queued will only remain in the backlog for the duration of the configured timeout. - To revert to previous behavior, a new `ConfigurationOptions.BacklogPolicy` is available - old behavior is configured via `options.BacklogPolicy = BacklogPolicy.FailFast`. This backlogs nothing and fails commands immediately if no connection is available. -- Makes `StreamEntry` constructor public for better unit test experience (#1923 via WeihanLi) -- Fix integer overflow error (issue #1926) with 2GiB+ result payloads (#1928 via mgravell) -- Update assumed redis versions to v2.8 or v4.0 in the Azure case (#1929 via NickCraver) -- Fix profiler showing `EVAL` instead `EVALSHA` (#1930 via martinpotter) -- Moved tiebreaker fetching in connections into the handshake phase (streamline + simplification) (#1931 via NickCraver) -- Fixed potential disposed object usage around Arenas (pulling in [Piplines.Sockets.Unofficial#63](https://github.com/mgravell/Pipelines.Sockets.Unofficial/pull/63) by MarcGravell) -- Adds thread pool work item stats to exception messages to help diagnose contention (#1964 via NickCraver) -- Overhauls pub/sub implementation for correctness (#1947 via NickCraver) +- Adds: Makes `StreamEntry` constructor public for better unit test experience ([#1923 by WeihanLi](https://github.com/StackExchange/StackExchange.Redis/pull/1923)) +- Fix: Integer overflow error (issue #1926) with 2GiB+ result payloads ([#1928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1928)) +- Change: Update assumed redis versions to v2.8 or v4.0 in the Azure case ([#1929 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1929)) +- Fix: Profiler showing `EVAL` instead `EVALSHA` ([#1930 by martinpotter](https://github.com/StackExchange/StackExchange.Redis/pull/1930)) +- Performance: Moved tiebreaker fetching in connections into the handshake phase (streamline + simplification) ([#1931 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1931)) +- Stability: Fixed potential disposed object usage around Arenas (pulling in [Piplines.Sockets.Unofficial#63](https://github.com/mgravell/Pipelines.Sockets.Unofficial/pull/63) by MarcGravell) +- Adds: Thread pool work item stats to exception messages to help diagnose contention ([#1964 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1964)) +- Fix/Performance: Overhauls pub/sub implementation for correctness ([#1947 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1947)) - Fixes a race in subscribing right after connected - Fixes a race in subscribing immediately before a publish - Fixes subscription routing on clusters (spreading instead of choosing 1 node) - More correctly reconnects subscriptions on connection failures, including to other endpoints -- Adds "(vX.X.X)" version suffix to the default client ID so server-side `CLIENT LIST` can more easily see what's connected (#1985 via NickCraver) -- Fix for including (or not including) key names on some message failures (#1990 via NickCraver) -- Fixed return of nil results in `LPOP`, `RPOP`, `SRANDMEMBER`, and `SPOP` (#1993 via NickCraver) +- Adds "(vX.X.X)" version suffix to the default client ID so server-side `CLIENT LIST` can more easily see what's connected ([#1985 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1985)) +- Fix: Properly including or excluding key names on some message failures ([#1990 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1990)) +- Fix: Correct return of nil results in `LPOP`, `RPOP`, `SRANDMEMBER`, and `SPOP` ([#1993 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1993)) ## 2.2.88 -- Connection backoff default is now exponential instead of linear (#1896 via lolodi) -- Add support for NodeMaintenanceScaleComplete event (handles Redis cluster scaling) (#1902 via NickCraver) +- Change: Connection backoff default is now exponential instead of linear ([#1896 by lolodi](https://github.com/StackExchange/StackExchange.Redis/pull/1896)) +- Adds: Support for NodeMaintenanceScaleComplete event (handles Redis cluster scaling) ([#1902 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1902)) ## 2.2.79 -- NRediSearch: Support on json index (#1808 via AvitalFineRedis) -- NRediSearch: Support sortable TagFields and unNormalizedForm for Tag & Text Fields (#1862 via slorello89 & AvitalFineRedis) -- fix potential errors getting socket bytes (#1836 via NickCraver) -- logging additions (.NET Version and timestamps) for better debugging (#1796 via philon-msft) -- add: `Condition` API (transactions) now supports `StreamLengthEqual` and variants (#1807 via AlphaGremlin) -- Add support for count argument to `ListLeftPop`, `ListLeftPopAsync`, `ListRightPop`, and `ListRightPopAsync` (#1850 via jjfmarket) -- fix potential task/thread exhaustion from the backlog processor (#1854 via mgravell) -- add support for listening to Azure Maintenance Events (#1876 via amsoedal) -- add `StringGetDelete`/`StringGetDeleteAsync` API for Redis `GETDEL` command(#1840 via WeihanLi) +- NRediSearch: Support on json index ([#1808 by AvitalFineRedis](https://github.com/StackExchange/StackExchange.Redis/pull/1808)) +- NRediSearch: Support sortable TagFields and unNormalizedForm for Tag & Text Fields ([#1862 by slorello89 & AvitalFineRedis](https://github.com/StackExchange/StackExchange.Redis/pull/1862)) +- Fix: Potential errors getting socket bytes ([#1836 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1836)) +- Logging: Adds (.NET Version and timestamps) for better debugging ([#1796 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/1796)) +- Adds: `Condition` APIs (transactions), now supports `StreamLengthEqual` and variants ([#1807 by AlphaGremlin](https://github.com/StackExchange/StackExchange.Redis/pull/1807)) +- Adds: Support for count argument to `ListLeftPop`, `ListLeftPopAsync`, `ListRightPop`, and `ListRightPopAsync` ([#1850 by jjfmarket](https://github.com/StackExchange/StackExchange.Redis/pull/1850)) +- Fix: Potential task/thread exhaustion from the backlog processor ([#1854 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1854)) +- Adds: Support for listening to Azure Maintenance Events ([#1876 by amsoedal](https://github.com/StackExchange/StackExchange.Redis/pull/1876)) +- Adds: `StringGetDelete`/`StringGetDeleteAsync` APIs for Redis `GETDEL` command([#1840 by WeihanLi](https://github.com/StackExchange/StackExchange.Redis/pull/1840)) ## 2.2.62 -- Sentinel potential memory leak fix in OnManagedConnectionFailed handler (#1710 via alexSatov) -- fix issue where `GetOutstandingCount` could obscure underlying faults by faulting itself (#1792 via mgravell) -- fix issue #1719 with backlog messages becoming reordered (#1779 via TimLovellSmith) +- Stability: Sentinel potential memory leak fix in OnManagedConnectionFailed handler ([#1710 by alexSatov](https://github.com/StackExchange/StackExchange.Redis/pull/1710)) +- Fix: `GetOutstandingCount` could obscure underlying faults by faulting itself ([#1792 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1792)) +- Fix [#1719](https://github.com/StackExchange/StackExchange.Redis/issues/1791): With backlog messages becoming reordered ([#1779 by TimLovellSmith](https://github.com/StackExchange/StackExchange.Redis/pull/1779)) ## 2.2.50 -- performance optimization for PING accuracy (#1714 via eduardobr) -- improvement to reconnect logic (exponential backoff) (#1735 via deepakverma) -- refresh replica endpoint list on failover (#1684 by laurauzcategui) -- fix for ReconfigureAsync re-entrancy (caused connection issues) (#1772 by NickCraver) -- fix for ReconfigureAsync Sentinel race resulting in NoConnectionAvailable when using DemandMaster (#1773 by NickCraver) -- resolve race in AUTH and other connection reconfigurations (#1759 via TimLovellSmith and NickCraver) +- Performance: Optimization for PING accuracy ([#1714 by eduardobr](https://github.com/StackExchange/StackExchange.Redis/pull/1714)) +- Fix: Improvement to reconnect logic (exponential backoff) ([#1735 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/1735)) +- Adds: Refresh replica endpoint list on failover ([#1684 by laurauzcategui](https://github.com/StackExchange/StackExchange.Redis/pull/1684)) +- Fix: `ReconfigureAsync` re-entrancy (caused connection issues) ([#1772 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1772)) +- Fix: `ReconfigureAsync` Sentinel race resulting in NoConnectionAvailable when using DemandMaster ([#1773 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1773)) +- Stability: Resolve race in AUTH and other connection reconfigurations ([#1759 by TimLovellSmith and NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1759)) ## 2.2.4 -- fix ambiguous signature of the new `RPUSHX`/`LPUSHX` methods (#1620) +- Fix: Ambiguous signature of the new `RPUSHX`/`LPUSHX` methods ([#1620 by stefanloerwald](https://github.com/StackExchange/StackExchange.Redis/pull/1620)) ## 2.2.3 -- add .NET 5 target -- fix mutex race condition (#1585 via arsnyder16) -- allow `CheckCertificateRevocation` to be controlled via the config string (#1591 via lwlwalker) -- fix range end-value inversion (#1573 via tombatron) -- add `ROLE` support (#1551 via zmj) -- add varadic `RPUSHX`/`LPUSHX` support (#1557 via dmytrohridin) -- fix server-selection strategy race condition (#1532 via deepakverma) -- fix sentinel default port (#1525 via ejsmith) -- fix `Int64` parse scenario (#1568 via arsnyder16) -- force replication check during failover (via joroda) -- documentation tweaks (multiple) -- fix backlog contention issue (#1612, see also #1574 via devbv) +- Adds: .NET 5 target +- Fix: Mutex race condition ([#1585 by arsnyder16](https://github.com/StackExchange/StackExchange.Redis/pull/1585)) +- Adds: `CheckCertificateRevocation` can be controlled via the config string ([#1591 by lwlwalker](https://github.com/StackExchange/StackExchange.Redis/pull/1591)) +- Fix: Range end-value inversion ([#1573 by tombatron](https://github.com/StackExchange/StackExchange.Redis/pull/1573)) +- Adds: `ROLE` support ([#1551 by zmj](https://github.com/StackExchange/StackExchange.Redis/pull/1551)) +- Adds: varadic `RPUSHX`/`LPUSHX` support ([#1557 by dmytrohridin](https://github.com/StackExchange/StackExchange.Redis/pull/1557)) +- Fix: Server-selection strategy race condition ([#1532 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/1532)) +- Fix: Sentinel default port ([#1525 by ejsmith](https://github.com/StackExchange/StackExchange.Redis/pull/1525)) +- Fix: `Int64` parse scenario ([#1568 by arsnyder16](https://github.com/StackExchange/StackExchange.Redis/pull/1568)) +- Add: Force replication check during failover ([#1563 by aravindyeduvaka & joroda](https://github.com/StackExchange/StackExchange.Redis/pull/1563)) +- Documentation tweaks (multiple) +- Fix: Backlog contention issue ([#1612 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1612/), see also [#1574 by devbv](https://github.com/StackExchange/StackExchange.Redis/pull/1574/)) ## 2.1.58 -- fix: `[*]SCAN` - fix possible NRE scenario if the iterator is disposed with an incomplete operation in flight -- fix: `[*]SCAN` - treat the cursor as an opaque value whenever possible, for compatibility with `redis-cluster-proxy` -- add: `[*]SCAN` - include additional exception data in the case of faults +- Fix: `[*]SCAN` - fix possible NRE scenario if the iterator is disposed with an incomplete operation in flight +- Fix: `[*]SCAN` - treat the cursor as an opaque value whenever possible, for compatibility with `redis-cluster-proxy` +- Adds: `[*]SCAN` - include additional exception data in the case of faults ## 2.1.55 -- identify assembly binding problem on .NET Framework; drops `System.IO.Pipelines` to 4.7.1, and identifies new `System.Buffers` binding failure on 4.7.2 +- Adds: Identification of assembly binding problem on .NET Framework. Drops `System.IO.Pipelines` to 4.7.1, and identifies new `System.Buffers` binding failure on 4.7.2 ## 2.1.50 -- add: bind direct to sentinel-managed instances from a configuration string/object (#1431 via ejsmith) -- add last-delivered-id to `StreamGroupInfo` (#1477 via AndyPook) -- update naming of replication-related commands to reflect Redis 5 naming (#1488/#945) -- fix: the `IServer` commands that are database-specific (`DBSIZE`, `FLUSHDB`, `KEYS`, `SCAN`) now respect the default database on the config (#1460) -- library updates +- Adds: Bind directly to sentinel-managed instances from a configuration string/object ([#1431 by ejsmith](https://github.com/StackExchange/StackExchange.Redis/pull/1431)) +- Adds: `last-delivered-id` to `StreamGroupInfo` ([#1477 by AndyPook](https://github.com/StackExchange/StackExchange.Redis/pull/1477)) +- Change: Update naming of replication-related commands to reflect Redis 5 naming ([#1488 by mgravell](https://github.com/StackExchange/StackExchange.Redis/issues/1488) & [#945 by mgravell](https://github.com/StackExchange/StackExchange.Redis/issues/945)) +- Fix [#1460](https://github.com/StackExchange/StackExchange.Redis/issues/1460): `IServer` commands that are database-specific (`DBSIZE`, `FLUSHDB`, `KEYS`, `SCAN`) now respect the default database on the config ([#1468 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1468)) +- Library updates ## 2.1.39 -- fix: mutex around connection was not "fair"; in specific scenario could lead to out-of-order commands (#1440) -- fix: update libs (#1432) -- fix: timing error on linux (#1433 via pengweiqhca) -- fix: add `auth` to command-map for sentinal (#1428 via ejsmith) +- Fix: Mutex around connection was not "fair"; in specific scenario could lead to out-of-order commands ([#1440 by kennygea](https://github.com/StackExchange/StackExchange.Redis/pull/1440)) +- Fix [#1432](https://github.com/StackExchange/StackExchange.Redis/issues/1432): Update dependencies +- Fix: Timing error on linux ([#1433 by pengweiqhca](https://github.com/StackExchange/StackExchange.Redis/pull/1433)) +- Fix: Add `auth` to command-map for Sentinel ([#1428 by ejsmith](https://github.com/StackExchange/StackExchange.Redis/pull/1428)) ## 2.1.30 -- fix deterministic builds +- Build: Fix deterministic builds ## 2.1.28 -- fix: stability in new sentinel APIs -- fix: include `SslProtocolos` in `ConfigurationOptions.ToString()` (#1408 via vksampath and Sampath Vuyyuru -- fix: clarify messaging around disconnected multiplexers (#1396) -- change: tweak methods of new sentinel API (this is technically a breaking change, but since this is a new API that was pulled quickly, we consider this to be acceptable) -- add: new thread`SocketManager` mode (opt-in) to always use the regular thread-pool instead of the dedicated pool -- add: improved counters in/around error messages -- add: new `User` property on `ConfigurationOptions` -- build: enable deterministic builds (note: this failed; fixed in 2.1.30) +- Fix: Stability in new sentinel APIs +- Fix: Include `SslProtocolos` in `ConfigurationOptions.ToString()` ([#1408 by vksampath and Sampath Vuyyuru](https://github.com/StackExchange/StackExchange.Redis/pull/1408)) +- Fix: Clarify messaging around disconnected multiplexers ([#1396 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1396)) +- Change: Tweak methods of new sentinel API (this is technically a breaking change, but since this is a new API that was pulled quickly, we consider this to be acceptable) +- Adds: New thread `SocketManager` mode (opt-in) to always use the regular thread-pool instead of the dedicated pool +- Adds: Improved counters in/around error messages +- Adds: New `User` property on `ConfigurationOptions` +- Build: Enable deterministic builds (note: this failed; fixed in 2.1.30) ## 2.1.0 -- fix: ensure active-message is cleared (#1374 via hamish-omny) -- add: sentinel support (#1067 via shadim; #692 via lexxdark) -- add: `IAsyncEnumerable` scanning APIs now supported (#1087) -- add: new API for use with misbehaving sync-contexts ([more info](https://stackexchange.github.io/StackExchange.Redis/ThreadTheft)) -- add: `TOUCH` support (#1291 via gkorland) -- add: `Condition` API (transactions) now supports `SortedSetLengthEqual` (#1332 via phosphene47) -- add: `SocketManager` is now more configurable (#1115, via naile) -- add: NRediSearch updated in line with JRediSearch (#1267, via tombatron; #1199 via oruchreis) -- add: support for `CheckCertificatRevocation` configuration (#1234, via BLun78 and V912736) -- add: more details about exceptions (#1190, via marafiq) -- add: new stream APIs (#1141 and #1154 via ttingen) -- add: event-args now mockable (#1326 via n1l) -- fix: no-op when adding 0 values to a set (#1283 via omeaart) -- add: support for `LATENCY` and `MEMORY` (#1204) -- add: support for `HSTRLEN` (#1241 via eitanhs) -- add: `GeoRadiusResult` is now mockable (#1175 via firenero) -- fix: various documentation fixes (#1162, #1135, #1203, #1240, #1245, #1159, #1311, #1339, #1336) -- fix: rare race-condition around exception data (#1342) -- fix: `ScriptEvaluateAsync` keyspace isolation (#1377 via gliljas) -- fix: F# compatibility enhancements (#1386) -- fix: improved `ScriptResult` null support (#1392) -- fix: error with DNS resolution breaking endpoint iterator (#1393) -- tests: better docker support for tests (#1389 via ejsmith; #1391) -- tests: general test improvements (#1183, #1385, #1384) +- Fix: Ensure active-message is cleared ([#1374 by hamish-omny](https://github.com/StackExchange/StackExchange.Redis/pull/1374)) +- Adds: Sentinel support ([#1067 by shadim](https://github.com/StackExchange/StackExchange.Redis/pull/1067), [#692 by lexxdark](https://github.com/StackExchange/StackExchange.Redis/pull/692)) +- Adds: `IAsyncEnumerable` scanning APIs now supported ([#1087 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1087)) +- Adds: New API for use with misbehaving sync-contexts ([more info](https://stackexchange.github.io/StackExchange.Redis/ThreadTheft)) +- Adds: `TOUCH` support ([#1291 by gkorland](https://github.com/StackExchange/StackExchange.Redis/pull/1291)) +- Adds: `Condition` API (transactions) now supports `SortedSetLengthEqual` ([#1332 by phosphene47](https://github.com/StackExchange/StackExchange.Redis/pull/1332)) +- Adds: `SocketManager` is now more configurable ([#1115 by naile](https://github.com/StackExchange/StackExchange.Redis/pull/1115)) +- Adds: NRediSearch updated in line with JRediSearch ([#1267 by tombatron](https://github.com/StackExchange/StackExchange.Redis/pull/1267), [#1199 by oruchreis](https://github.com/StackExchange/StackExchange.Redis/pull/1199)) +- Adds: Support for `CheckCertificatRevocation` configuration ([#1234 by BLun78 and V912736](https://github.com/StackExchange/StackExchange.Redis/pull/1234)) +- Adds: More details about exceptions ([#1190 by marafiq](https://github.com/StackExchange/StackExchange.Redis/pull/1190)) +- Adds: Updated `StreamCreateConsumerGroup` methods to use the `MKSTREAM` option ([#1141 via ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/1141)) +- Adds: Support for NOACK in the StreamReadGroup methods ([#1154 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/1154)) +- Adds: Event-args now mockable ([#1326 by n1l](https://github.com/StackExchange/StackExchange.Redis/pull/1326)) +- Fix: No-op when adding 0 values to a set ([#1283 by omeaart](https://github.com/StackExchange/StackExchange.Redis/pull/1283)) +- Adds: Support for `LATENCY` and `MEMORY` ([#1204 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1204)) +- Adds: Support for `HSTRLEN` ([#1241 by eitanhs](https://github.com/StackExchange/StackExchange.Redis/pull/1241)) +- Adds: `GeoRadiusResult` is now mockable ([#1175 by firenero](https://github.com/StackExchange/StackExchange.Redis/pull/1175)) +- Fix: Various documentation fixes ([#1162 by SnakyBeaky](https://github.com/StackExchange/StackExchange.Redis/pull/1162), [#1135 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/1135), [#1203 by caveman-dick](https://github.com/StackExchange/StackExchange.Redis/pull/1203), [#1240 by Excelan](https://github.com/StackExchange/StackExchange.Redis/pull/1240), [#1245 by francoance](https://github.com/StackExchange/StackExchange.Redis/pull/1245), [#1159 by odyth](https://github.com/StackExchange/StackExchange.Redis/pull/1159), [#1311 by DillonAd](https://github.com/StackExchange/StackExchange.Redis/pull/1311), [#1339 by vp89](https://github.com/StackExchange/StackExchange.Redis/pull/1339), [#1336 by ERGeorgiev](https://github.com/StackExchange/StackExchange.Redis/issues/1336)) +- Fix: Rare race-condition around exception data ([#1342 by AdamOutcalt](https://github.com/StackExchange/StackExchange.Redis/pull/1342)) +- Fix: `ScriptEvaluateAsync` keyspace isolation ([#1377 by gliljas](https://github.com/StackExchange/StackExchange.Redis/pull/1377)) +- Fix: F# compatibility enhancements ([#1386 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1386)) +- Fix: Improved `ScriptResult` null support ([#1392 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1392)) +- Fix: Error with DNS resolution breaking endpoint iterator ([#1393 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1393)) +- Tests: Better docker support for tests ([#1389 by ejsmith](https://github.com/StackExchange/StackExchange.Redis/pull/1389), [#1391 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1391)) +- Tests: General test improvements ([#1183 by mtreske](https://github.com/StackExchange/StackExchange.Redis/issues/1183), [#1385 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1385), [#1384 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1384)) ## 2.0.601 -- add: tracking for current and next messages to help with debugging timeout issues - helpful in cases of large pipeline blockers +- Adds: Tracking for current and next messages to help with debugging timeout issues - helpful in cases of large pipeline blockers ## 2.0.600 -- add: `ulong` support to `RedisValue` and `RedisResult` (#1103) -- fix: remove odd equality: `"-" != 0` (we do, however, still allow `"-0"`, as that is at least semantically valid, and is logically `== 0`) (related to #1103) -- performance: rework how pub/sub queues are stored - reduces delegate overheads (related to #1101) -- fix #1108 - ensure that we don't try appending log data to the `TextWriter` once we've returned from a method that accepted one +- Adds: `ulong` support to `RedisValue` and `RedisResult` ([#1104 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1104)) +- Fix: Remove odd equality: `"-" != 0` (we do, however, still allow `"-0"`, as that is at least semantically valid, and is logically `== 0`) (related to [#1103](https://github.com/StackExchange/StackExchange.Redis/issues/1103)) +- Performance: Rework how pub/sub queues are stored - reduces delegate overheads (related to [#1101](https://github.com/StackExchange/StackExchange.Redis/issues/1101)) +- Fix [#1108](https://github.com/StackExchange/StackExchange.Redis/issues/1108): Ensure that we don't try appending log data to the `TextWriter` once we've returned from a method that accepted one ## 2.0.593 -- performance: unify spin-wait usage on sync/async paths to one competitor -- fix #1101 - when a `ChannelMessageQueue` is involved, unsubscribing *via any route* should still unsubscribe and mark the queue-writer as complete +- Performance: Unify spin-wait usage on sync/async paths to one competitor +- Fix [#1101](https://github.com/StackExchange/StackExchange.Redis/issues/1101) - when a `ChannelMessageQueue` is involved, unsubscribing *via any route* should still unsubscribe and mark the queue-writer as complete ## 2.0.588 -- stability and performance: resolve intermittent stall in the write-lock that could lead to unexpected timeouts even when at low/reasonable (but concurrent) load +- Stability/Performance: Resolve intermittent stall in the write-lock that could lead to unexpected timeouts even when at low/reasonable (but concurrent) load ## 2.0.571 -- performance: use new [arena allocation API](https://mgravell.github.io/Pipelines.Sockets.Unofficial/docs/arenas) to avoid `RawResult[]` overhead -- performance: massively simplified how `ResultBox` is implemented, in particular to reduce `TaskCompletionSource` allocations -- performance: fix sync-over-async issue with async call paths, and fix the [SemaphoreSlim](https://blog.marcgravell.com/2019/02/fun-with-spiral-of-death.html) problems that this uncovered -- performance: re-introduce the unsent backlog queue, in particular to improve async performance -- performance: simplify how completions are reactivated, so that external callers use their originating pool, not the dedicated IO pools (prevent thread stealing) -- fix: update Pipelines.Sockets.Unofficial to prevent issue with incorrect buffer re-use in corner-case -- fix: `KeyDeleteAsync` could, in some cases, always use `DEL` (instead of `UNLINK`) -- fix: last unanswered write time was incorrect -- change: use higher `Pipe` thresholds when sending +- Performance: Use new [arena allocation API](https://mgravell.github.io/Pipelines.Sockets.Unofficial/docs/arenas) to avoid `RawResult[]` overhead +- Performance: Massively simplified how `ResultBox` is implemented, in particular to reduce `TaskCompletionSource` allocations +- Performance: Fix sync-over-async issue with async call paths, and fix the [SemaphoreSlim](https://blog.marcgravell.com/2019/02/fun-with-spiral-of-death.html) problems that this uncovered +- Performance: Reintroduce the unsent backlog queue, in particular to improve async performance +- Performance: Simplify how completions are reactivated, so that external callers use their originating pool, not the dedicated IO pools (prevent thread stealing) +- Fix: Update `Pipelines.Sockets.Unofficial` to prevent issue with incorrect buffer re-use in corner-case +- Fix: `KeyDeleteAsync` could, in some cases, always use `DEL` (instead of `UNLINK`) +- Fix: Last unanswered write time was incorrect +- Change: Use higher `Pipe` thresholds when sending ## 2.0.519 -- adapt to late changes in the RC streams API (#983, #1007) -- documentation fixes (#997, #1005) -- build: switch to SDK 2.1.500 +- Fix [#1007](https://github.com/StackExchange/StackExchange.Redis/issues/1007): Adapt to late changes in the RC streams API ([#983 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/983)) +- Documentation fixes ([#997 by MerelyRBLX](https://github.com/StackExchange/StackExchange.Redis/pull/997), [#1005 by zBrianW](https://github.com/StackExchange/StackExchange.Redis/pull/1005)) +- Build: Switch to SDK 2.1.500 ## 2.0.513 -- fix #961 - fix assembly binding redirect problems; IMPORTANT: this drops to an older `System.Buffers` version - if you have manually added redirects for `4.0.3.0`, you may need to manually update to `4.0.2.0` (or remove completely) -- fix #962 - avoid NRE in edge-case when fetching bridge +- Fix [#961](https://github.com/StackExchange/StackExchange.Redis/issues/962) - fix assembly binding redirect problems; IMPORTANT: this drops to an older `System.Buffers` version - if you have manually added redirects for `4.0.3.0`, you may need to manually update to `4.0.2.0` (or remove completely) +- Fix [#962](https://github.com/StackExchange/StackExchange.Redis/issues/962): Avoid NRE in edge-case when fetching bridge ## 2.0.505 -- fix #943 - ensure transaction inner tasks are completed prior to completing the outer transaction task -- fix #946 - reinstate missing `TryParse` methods on `RedisValue` -- fix #940 - off-by-one on pre-boxed integer cache (NRediSearch) +- Fix [#943](https://github.com/StackExchange/StackExchange.Redis/issues/943): Ensure transaction inner tasks are completed prior to completing the outer transaction task +- Fix [#946](https://github.com/StackExchange/StackExchange.Redis/issues/946): Reinstate missing `TryParse` methods on `RedisValue` +- Fix [#940](https://github.com/StackExchange/StackExchange.Redis/issues/940): Off-by-one on pre-boxed integer cache (NRediSearch) ## 2.0.495 @@ -206,95 +208,88 @@ The key focus of this release is stability and reliability. -- HARD BREAK: the package identity has changed; instead of `StackExchange.Redis` (not strong-named) and `StackExchange.Redis.StrongName` (strong-named), we are now +- **Hard Break**: The package identity has changed; instead of `StackExchange.Redis` (not strong-named) and `StackExchange.Redis.StrongName` (strong-named), we are now only releasing `StackExchange.Redis` (strong-named). This is a binary breaking change that requires consumers to be re-compiled; it cannot be applied via binding-redirects -- HARD BREAK: the platform targets have been rationalized - supported targets are .NETStandard 2.0 (and above), .NETFramework 4.6.1 (and above), and .NETFramework 4.7.2 (and above) +- **Hard Break**: The platform targets have been rationalized - supported targets are .NETStandard 2.0 (and above), .NETFramework 4.6.1 (and above), and .NETFramework 4.7.2 (and above) (note - the last two are mainly due to assembly binding problems) -- HARD BREAK: the profiling API has been overhauled and simplified; full documentation is [provided here](https://stackexchange.github.io/StackExchange.Redis/Profiling_v2.html) -- SOFT BREAK: the `PreserveAsyncOrder` behaviour of the pub/sub API has been deprecated; a *new* API has been provided for scenarios that require in-order pub/sub handling - - the `Subscribe` method has a new overload *without* a handler parameter which returns a `ChannelMessageQueue`, which provides `async` ordered access to messsages) -- internal: the network architecture has moved to use `System.IO.Pipelines`; this has allowed us to simplify and unify a lot of the network code, and in particular - fix a lot of problems relating to how the library worked with TLS and/or .NETStandard -- change: as a result of the `System.IO.Pipelines` change, the error-reporting on timeouts is now much simpler and clearer; the [timeouts documentation](Timeouts.md) has been updated -- removed: the `HighPriority` (queue-jumping) flag is now deprecated -- internal: most buffers internally now make use of pooled memory; `RedisValue` no longer pre-emptively allocates buffers -- internal: added new custom thread-pool for handling async continuations to avoid thread-pool starvation issues -- internal: all IL generation has been removed; the library should now work on platforms that do not allow runtime-emit -- added: asynchronous operations now have full support for reporting timeouts -- added: new APIs now exist to work with pooled memory without allocations - `RedisValue.CreateFrom(MemoryStream)` and `operator` support for `Memory` and `ReadOnlyMemory`; and `IDatabase.StringGetLease[Async](...)`, `IDatabase.HashGetLease[Async](...)`, `Lease.AsStream()`) -- added: ["streams"](https://redis.io/topics/streams-intro) support (thanks to [ttingen](https://github.com/ttingen) for their contribution) -- various missing commands / overloads have been added; `Execute[Async]` for additional commands is now available on `IServer` -- fix: a *lot* of general bugs and issues have been resolved -- ACCIDENTAL BREAK: `RedisValue.TryParse` was accidentally ommitted in the overhaul; this has been rectified and will be available in the next build +- **Hard Break**: The profiling API has been overhauled and simplified; full documentation is [provided here](https://stackexchange.github.io/StackExchange.Redis/Profiling_v2.html) +- **Soft Break**: The `PreserveAsyncOrder` behaviour of the pub/sub API has been deprecated; a *new* API has been provided for scenarios that require in-order pub/sub handling - + the `Subscribe` method has a new overload *without* a handler parameter which returns a `ChannelMessageQueue`, which provides `async` ordered access to messages) +- Internal: The network architecture has moved to use `System.IO.Pipelines`; this has allowed us to simplify and unify a lot of the network code, and in particular fix a lot of problems relating to how the library worked with TLS and/or .NETStandard +- Change: As a result of the `System.IO.Pipelines` change, the error-reporting on timeouts is now much simpler and clearer; the [timeouts documentation](Timeouts.md) has been updated +- Removed: The `HighPriority` (queue-jumping) flag is now deprecated +- Internal: Most buffers internally now make use of pooled memory; `RedisValue` no longer preemptively allocates buffers +- Internal: Added new custom thread-pool for handling async continuations to avoid thread-pool starvation issues +- Internal: All IL generation has been removed; the library should now work on platforms that do not allow runtime-emit +- Adds: asynchronous operations now have full support for reporting timeouts +- Adds: new APIs now exist to work with pooled memory without allocations - `RedisValue.CreateFrom(MemoryStream)` and `operator` support for `Memory` and `ReadOnlyMemory`; and `IDatabase.StringGetLease[Async](...)`, `IDatabase.HashGetLease[Async](...)`, `Lease.AsStream()`) +- Adds: ["streams"](https://redis.io/topics/streams-intro) support (thanks to [ttingen](https://github.com/ttingen) for their contribution) +- Adds: Various missing commands / overloads have been added; `Execute[Async]` for additional commands is now available on `IServer` +- Fix: A *lot* of general bugs and issues have been resolved +- **Break**: `RedisValue.TryParse` was accidentally omitted in the overhaul; this has been rectified and will be available in the next build a more complete list of issues addressed can be seen in [this tracking issue](https://github.com/StackExchange/StackExchange.Redis/issues/871) -Note: we currently have no plans to do an additional 1.* release. In particular, even though there was a `1.2.7-alpha` build on nuget, we *do not* currently have +Note: we currently have no plans to do an additional `1.*` release. In particular, even though there was a `1.2.7-alpha` build on nuget, we *do not* currently have plans to release `1.2.7`. --- ## 1.2.6 -- fix change to `cluster nodes` output when using cluster-enabled target and 4.0+ (see [redis #4186](https://github.com/antirez/redis/issues/4186) +- Change: `cluster nodes` output when using cluster-enabled target and 4.0+ (see [redis #4186](https://github.com/antirez/redis/issues/4186) ## 1.2.5 -- critical fix: "poll mode" was disabled in the build for net45/net60 - impact: IO jams and lack of reader during high load +- (Critical) Fix: "poll mode" was disabled in the build for `net45`/`net46` - Impact: IO jams and lack of reader during high load ## 1.2.4 -- fix: incorrect build configuration (#649) +- Fix: Incorrect build configuration ([#649 by jrlost](https://github.com/StackExchange/StackExchange.Redis/issues/649)) ## 1.2.3 -- fix: when using `redis-cluster` with multiple replicas, use round-robin when selecting replica (#610) -- add: can specify `NoScriptCache` flag when using `ScriptEvaluate` to bypass all cache features (always uses `EVAL` instead of `SCRIPT LOAD` and `EVALSHA`) (#617) +- Fix: When using `redis-cluster` with multiple replicas, use round-robin when selecting replica ([#610 by mgravell](https://github.com/StackExchange/StackExchange.Redis/issues/610)) +- Adds: Can specify `NoScriptCache` flag when using `ScriptEvaluate` to bypass all cache features (always uses `EVAL` instead of `SCRIPT LOAD` and `EVALSHA`) ([#617 by Funbit](https://github.com/StackExchange/StackExchange.Redis/issues/617)) -## 1.2.2 (preview): +## 1.2.2 (preview) -- **UNAVAILABLE**: .NET 4.0 support is not in this build, due to [a build issue](https://github.com/dotnet/cli/issues/5993) - looking into solutions -- add: make performance-counter tracking opt-in (`IncludePerformanceCountersInExceptions`) as it was causing problems (#587) -- add: can now specifiy allowed SSL/TLS protocols (#603) -- add: track message status in exceptions (#576) -- add: `GetDatabase()` optimization for DB 0 and low numbered databases: `IDatabase` instance is retained and recycled (as long as no `asyncState` is provided) -- improved connection retry policy (#510, #572) -- add `Execute`/`ExecuteAsync` API to support "modules"; [more info](https://blog.marcgravell.com/2017/04/stackexchangeredis-and-redis-40-modules.html) -- fix: timeout link fixed re /docs change (below) +- **Break**: .NET 4.0 support is not in this build, due to [a build issue](https://github.com/dotnet/cli/issues/5993) - looking into solutions +- Adds: Make performance-counter tracking opt-in (`IncludePerformanceCountersInExceptions`) as it was causing problems ([#587 by AlexanderKot](https://github.com/StackExchange/StackExchange.Redis/issues/587)) +- Adds: Can now specifiy allowed SSL/TLS protocols ([#603 by JonCole](https://github.com/StackExchange/StackExchange.Redis/pull/603)) +- Adds: Track message status in exceptions ([#576 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/576)) +- Adds: `GetDatabase()` optimization for DB 0 and low numbered databases: `IDatabase` instance is retained and recycled (as long as no `asyncState` is provided) +- Performance: Improved connection retry policy ([#510 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/510), [#572 by deepakverma](https://github.com/StackExchange/StackExchange.Redis/pull/572)) +- Adds: `Execute`/`ExecuteAsync` API to support "modules"; [more info](https://blog.marcgravell.com/2017/04/stackexchangeredis-and-redis-40-modules.html) +- Fix: Timeout link fixed re /docs change (below) - [`NRediSearch`](https://www.nuget.org/packages/NRediSearch/) added as exploration into "modules" - -Other changes (not library related) - -- (project) refactor /docs for github pages -- improve release note tracking -- rework build process to use csproj +- Other changes (not library related) + - Change: Refactor /docs for github pages + - Change: Improve release note tracking + - Build: Rework build process to use csproj ## 1.2.1 -- fix: avoid overlapping per-endpoint heartbeats - -## 1.2.0 - -- (same as 1.2.0-alpha1) +- Fix: Avoid overlapping per-endpoint heartbeats -## 1.2.0-alpha1 +## 1.2.0 (same as 1.2.0-alpha1) -- add: GEO commands (#489) -- add: ZADD support for new NX/XX switches (#520) -- add: core-clr preview support improvements +- Adds: GEO commands ([#489 by wjdavis5](https://github.com/StackExchange/StackExchange.Redis/pull/489)) +- Adds: ZADD support for new NX/XX switches ([#520 by seniorquico](https://github.com/StackExchange/StackExchange.Redis/pull/520)) +- Adds: core-clr preview support improvements ## 1.1.608 -- fix: bug with race condition in servers indexer (related: 1.1.606) +- Fix: Bug with race condition in servers indexer (related: 1.1.606) ## 1.1.607 -- fix: ensure socket-mode polling is enabled (.net) +- Fix: Ensure socket-mode polling is enabled (.net) ## 1.1.606 -- fix: bug with race condition in servers indexer +- Fix: Bug with race condition in servers indexer -## and the rest +## ...and the rest -(I'm happy to take PRs for change history going back in time) +(We're happy to take PRs for change history going back in time or any fixes here!) diff --git a/version.json b/version.json index 8191d532a..2d8ba31f6 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "2.5-prerelease", + "version": "2.5", "versionHeightOffset": -1, "assemblyVersion": "2.0", "publicReleaseRefSpec": [ From f1a84f94fd98fbdd615e8d39c6648167e8c4a8e2 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sun, 6 Mar 2022 14:21:46 -0500 Subject: [PATCH 097/435] Adds ConsoleTest/Baseline for some heavier benchmarks (#2022) This adds some console tests for performance work. --- StackExchange.Redis.sln | 14 ++ tests/ConsoleTest/ConsoleTest.csproj | 13 ++ tests/ConsoleTest/Program.cs | 122 ++++++++++++++++++ .../ConsoleTestBaseline.csproj | 19 +++ 4 files changed, 168 insertions(+) create mode 100644 tests/ConsoleTest/ConsoleTest.csproj create mode 100644 tests/ConsoleTest/Program.cs create mode 100644 tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index d1a585738..514205e64 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -134,6 +134,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{A9F81D tests\RedisConfigs\Docker\supervisord.conf = tests\RedisConfigs\Docker\supervisord.conf EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleTest", "tests\ConsoleTest\ConsoleTest.csproj", "{A0F89B8B-32A3-4C28-8F1B-ADE343F16137}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleTestBaseline", "tests\ConsoleTestBaseline\ConsoleTestBaseline.csproj", "{69A0ACF2-DF1F-4F49-B554-F732DCA938A3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -176,6 +180,14 @@ Global {8FB98E7D-DAE2-4465-BD9A-104000E0A2D4}.Debug|Any CPU.Build.0 = Debug|Any CPU {8FB98E7D-DAE2-4465-BD9A-104000E0A2D4}.Release|Any CPU.ActiveCfg = Release|Any CPU {8FB98E7D-DAE2-4465-BD9A-104000E0A2D4}.Release|Any CPU.Build.0 = Release|Any CPU + {A0F89B8B-32A3-4C28-8F1B-ADE343F16137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0F89B8B-32A3-4C28-8F1B-ADE343F16137}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0F89B8B-32A3-4C28-8F1B-ADE343F16137}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0F89B8B-32A3-4C28-8F1B-ADE343F16137}.Release|Any CPU.Build.0 = Release|Any CPU + {69A0ACF2-DF1F-4F49-B554-F732DCA938A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69A0ACF2-DF1F-4F49-B554-F732DCA938A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69A0ACF2-DF1F-4F49-B554-F732DCA938A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69A0ACF2-DF1F-4F49-B554-F732DCA938A3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -196,6 +208,8 @@ Global {153A10E4-E668-41AD-9E0F-6785CE7EED66} = {3AD17044-6BFF-4750-9AC2-2CA466375F2A} {D58114AE-4998-4647-AFCA-9353D20495AE} = {E25031D3-5C64-430D-B86F-697B66816FD8} {A9F81DA3-DA82-423E-A5DD-B11C37548E06} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05} + {A0F89B8B-32A3-4C28-8F1B-ADE343F16137} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} + {69A0ACF2-DF1F-4F49-B554-F732DCA938A3} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {193AA352-6748-47C1-A5FC-C9AA6B5F000B} diff --git a/tests/ConsoleTest/ConsoleTest.csproj b/tests/ConsoleTest/ConsoleTest.csproj new file mode 100644 index 000000000..883ab204a --- /dev/null +++ b/tests/ConsoleTest/ConsoleTest.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + Exe + enable + enable + + + + + + diff --git a/tests/ConsoleTest/Program.cs b/tests/ConsoleTest/Program.cs new file mode 100644 index 000000000..1d33a968e --- /dev/null +++ b/tests/ConsoleTest/Program.cs @@ -0,0 +1,122 @@ +using StackExchange.Redis; +using System.Diagnostics; +using System.Reflection; + +Stopwatch stopwatch = new Stopwatch(); +stopwatch.Start(); + +var options = ConfigurationOptions.Parse("localhost"); +//options.SocketManager = SocketManager.ThreadPool; +var connection = ConnectionMultiplexer.Connect(options); + +var startTime = DateTime.UtcNow; +var startCpuUsage = Process.GetCurrentProcess().TotalProcessorTime; + +var scenario = args?.Length > 0 ? args[0] : "parallel"; + +switch (scenario) +{ + case "parallel": + Console.WriteLine("Parallel task test..."); + ParallelTasks(connection); + break; + case "mass-insert": + Console.WriteLine("Mass insert test..."); + MassInsert(connection); + break; + case "mass-publish": + Console.WriteLine("Mass publish test..."); + MassPublish(connection); + break; + default: + Console.WriteLine("Scenario " + scenario + " is not recognized"); + break; +} + +stopwatch.Stop(); + +Console.WriteLine(""); +Console.WriteLine($"Done. {stopwatch.ElapsedMilliseconds} ms"); + +var endTime = DateTime.UtcNow; +var endCpuUsage = Process.GetCurrentProcess().TotalProcessorTime; +var cpuUsedMs = (endCpuUsage - startCpuUsage).TotalMilliseconds; +var totalMsPassed = (endTime - startTime).TotalMilliseconds; +var cpuUsageTotal = cpuUsedMs / (Environment.ProcessorCount * totalMsPassed); +Console.WriteLine("Avg CPU: " + (cpuUsageTotal * 100)); +Console.WriteLine("Lib Version: " + GetLibVersion()); + +static void MassInsert(ConnectionMultiplexer connection) +{ + const int NUM_INSERTIONS = 100000; + int matchErrors = 0; + + var database = connection.GetDatabase(0); + + for (int i = 0; i < NUM_INSERTIONS; i++) + { + var key = $"StackExchange.Redis.Test.{i}"; + var value = i.ToString(); + + database.StringSet(key, value); + var retrievedValue = database.StringGet(key); + + if (retrievedValue != value) + { + matchErrors++; + } + + if (i > 0 && i % 5000 == 0) + { + Console.WriteLine(i); + } + } + + Console.WriteLine($"Match errors: {matchErrors}"); +} + +static void ParallelTasks(ConnectionMultiplexer connection) +{ + static void ParallelRun(int taskId, ConnectionMultiplexer connection) + { + Console.Write($"{taskId} Started, "); + var database = connection.GetDatabase(0); + + for (int i = 0; i < 100000; i++) + { + database.StringSet(i.ToString(), i.ToString()); + } + + Console.Write($"{taskId} Insert completed, "); + + for (int i = 0; i < 100000; i++) + { + var result = database.StringGet(i.ToString()); + } + Console.Write($"{taskId} Completed, "); + } + + var taskList = new List(); + for (int i = 0; i < 10; i++) + { + var i1 = i; + var task = new Task(() => ParallelRun(i1, connection)); + task.Start(); + taskList.Add(task); + } + Task.WaitAll(taskList.ToArray()); +} + +static void MassPublish(ConnectionMultiplexer connection) +{ + var subscriber = connection.GetSubscriber(); + Parallel.For(0, 1000, _ => subscriber.Publish("cache-events:cache-testing", "hey")); +} + +static string GetLibVersion() +{ + var assembly = typeof(ConnectionMultiplexer).Assembly; + return (Attribute.GetCustomAttribute(assembly, typeof(AssemblyFileVersionAttribute)) as AssemblyFileVersionAttribute)?.Version + ?? assembly.GetName().Version?.ToString() + ?? "Unknown"; +} diff --git a/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj b/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj new file mode 100644 index 000000000..130f9bf9e --- /dev/null +++ b/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + Exe + enable + enable + + + + + + + + + + + + From d3dc356b0e8c43ab673e89b551f505122ee42bf5 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 7 Mar 2022 11:16:28 -0500 Subject: [PATCH 098/435] Fix for #1988 - respect SELECT missing from command map (#2023) This resolves #1988 by respecting when the `SELECT` command has been disabled. It's memoized which seems bit extreme here but that's because we're inside the lock and every op matters. Measured locally to ensure no regressions. --- docs/ReleaseNotes.md | 4 +++ src/StackExchange.Redis/PhysicalBridge.cs | 7 ++-- src/StackExchange.Redis/PhysicalConnection.cs | 3 +- src/StackExchange.Redis/ServerEndPoint.cs | 33 ++++++++++++++++--- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 326c50e16..9cbce2f96 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,5 +1,9 @@ # Release Notes +## Unreleased + +- Fix [#1988](https://github.com/StackExchange/StackExchange.Redis/issues/1988): Don't issue `SELECT` commands if explicitly disabled ([#2023 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2023)) + ## 2.5.43 - Adds: Bounds checking for `ExponentialRetry` backoff policy ([#1921 by gliljas](https://github.com/StackExchange/StackExchange.Redis/pull/1921)) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 92d2f83f7..e39cad16a 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -1372,7 +1372,7 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne LastCommand = cmd; bool isMasterOnly = message.IsMasterOnly(); - if (isMasterOnly && ServerEndPoint.IsReplica && (ServerEndPoint.ReplicaReadOnly || !ServerEndPoint.AllowReplicaWrites)) + if (isMasterOnly && !ServerEndPoint.SupportsPrimaryWrites) { throw ExceptionFactory.MasterOnly(Multiplexer.IncludeDetailInExceptions, message.Command, message, ServerEndPoint); } @@ -1447,7 +1447,10 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne case RedisCommand.UNKNOWN: case RedisCommand.DISCARD: case RedisCommand.EXEC: - connection.SetUnknownDatabase(); + if (ServerEndPoint.SupportsDatabases) + { + connection.SetUnknownDatabase(); + } break; } return WriteResult.Success; diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 4b32ecfee..f699d9055 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -573,7 +573,8 @@ internal Message GetSelectDatabaseCommand(int targetDatabase, Message message) } int available = serverEndpoint.Databases; - if (!serverEndpoint.HasDatabases) // only db0 is available on cluster/twemproxy/envoyproxy + // Only db0 is available on cluster/twemproxy/envoyproxy + if (!serverEndpoint.SupportsDatabases) { if (targetDatabase != 0) { // should never see this, since the API doesn't allow it; thus not too worried about ExceptionFactory diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 2aaf6a63e..e87e2a73e 100755 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -32,9 +32,9 @@ internal sealed partial class ServerEndPoint : IDisposable private int databases, writeEverySeconds; private PhysicalBridge interactive, subscription; - private bool isDisposed; + private bool isDisposed, replicaReadOnly, isReplica, allowReplicaWrites; + private bool? supportsDatabases, supportsPrimaryWrites; private ServerType serverType; - private bool replicaReadOnly, isReplica; private volatile UnselectableFlags unselectableReasons; private Version version; @@ -76,8 +76,17 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) public int Databases { get { return databases; } set { SetConfig(ref databases, value); } } public EndPoint EndPoint { get; } + /// + /// Whether this endpoint supports databases at all. + /// Note that some servers are cluster but present as standalone (e.g. Redis Enterprise), so we respect + /// being disabled here as a performance workaround. + /// + /// + /// This is memoized because it's accessed on hot paths inside the write lock. + /// + public bool SupportsDatabases => + supportsDatabases ??= (serverType == ServerType.Standalone && Multiplexer.RawConfig.CommandMap.IsAvailable(RedisCommand.SELECT)); - public bool HasDatabases => serverType == ServerType.Standalone; public bool IsConnected => interactive?.IsConnected == true; public bool IsSubscriberConnected => subscription?.IsConnected == true; @@ -85,6 +94,7 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) public bool SupportsSubscriptions => Multiplexer.CommandMap.IsAvailable(RedisCommand.SUBSCRIBE); public bool IsConnecting => interactive?.IsConnecting == true; + public bool SupportsPrimaryWrites => supportsPrimaryWrites ??= (!IsReplica || !ReplicaReadOnly || AllowReplicaWrites); private readonly List> _pendingConnectionMonitors = new List>(); @@ -176,7 +186,15 @@ public bool ReplicaReadOnly set => SetConfig(ref replicaReadOnly, value); } - public bool AllowReplicaWrites { get; set; } + public bool AllowReplicaWrites + { + get => allowReplicaWrites; + set + { + allowReplicaWrites = value; + ClearMemoized(); + } + } public Version Version { @@ -946,10 +964,17 @@ private void SetConfig(ref T field, T value, [CallerMemberName] string caller { Multiplexer.Trace(caller + " changed from " + field + " to " + value, "Configuration"); field = value; + ClearMemoized(); Multiplexer.ReconfigureIfNeeded(EndPoint, false, caller); } } + private void ClearMemoized() + { + supportsDatabases = null; + supportsPrimaryWrites = null; + } + /// /// For testing only /// From a387715e53e78a2a5c92243e4643d92354c451ba Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 7 Mar 2022 18:15:08 -0500 Subject: [PATCH 099/435] LogProxy and other misc cleanup (#2024) This cleans up a few things by moving `LogProxy` out to it's own non-nested class and some more shuffling in `ServerEndPoint` and comment normalization - getting this out of the way before a "primary" PR. --- .../ConfigurationOptions.cs | 1 - .../ConnectionMultiplexer.cs | 63 +------- src/StackExchange.Redis/Enums/ServerType.cs | 2 +- src/StackExchange.Redis/LogProxy.cs | 65 ++++++++ src/StackExchange.Redis/Message.cs | 1 - src/StackExchange.Redis/PhysicalBridge.cs | 12 +- src/StackExchange.Redis/PhysicalConnection.cs | 23 +-- src/StackExchange.Redis/RedisServer.cs | 1 - src/StackExchange.Redis/RedisSubscriber.cs | 1 - src/StackExchange.Redis/ResultProcessor.cs | 4 +- src/StackExchange.Redis/ServerEndPoint.cs | 141 +++++++++--------- tests/StackExchange.Redis.Tests/EnvoyTests.cs | 2 +- 12 files changed, 161 insertions(+), 155 deletions(-) create mode 100644 src/StackExchange.Redis/LogProxy.cs mode change 100755 => 100644 src/StackExchange.Redis/ServerEndPoint.cs diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 4108ba1de..3ad15b050 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -10,7 +10,6 @@ using System.Threading; using System.Threading.Tasks; using StackExchange.Redis.Configuration; -using static StackExchange.Redis.ConnectionMultiplexer; namespace StackExchange.Redis { diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 070c025ae..2734b31e5 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -904,67 +904,6 @@ internal static ConfigurationOptions PrepareConfig(object configuration, bool se return config; } - - internal sealed class LogProxy : IDisposable - { - public static LogProxy TryCreate(TextWriter writer) - => writer == null ? null : new LogProxy(writer); - - public override string ToString() - { - string s = null; - if (_log != null) - { - lock (SyncLock) - { - s = _log?.ToString(); - } - } - return s ?? base.ToString(); - } - private TextWriter _log; - internal static Action NullWriter = _ => {}; - - public object SyncLock => this; - private LogProxy(TextWriter log) => _log = log; - public void WriteLine() - { - if (_log != null) // note: double-checked - { - lock (SyncLock) - { - _log?.WriteLine(); - } - } - } - public void WriteLine(string message = null) - { - if (_log != null) // note: double-checked - { - lock (SyncLock) - { - _log?.WriteLine($"{DateTime.UtcNow:HH:mm:ss.ffff}: {message}"); - } - } - } - public void WriteLine(string prefix, string message) - { - if (_log != null) // note: double-checked - { - lock (SyncLock) - { - _log?.WriteLine($"{DateTime.UtcNow:HH:mm:ss.ffff}: {prefix}{message}"); - } - } - } - public void Dispose() - { - if (_log != null) // note: double-checked - { - lock (SyncLock) { _log = null; } - } - } - } private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions configuration, LogProxy log, out EventHandler connectHandler) { var muxer = new ConnectionMultiplexer(configuration); @@ -1743,7 +1682,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP } } - log?.WriteLine($"Endpoint summary:"); + log?.WriteLine("Endpoint summary:"); // Log current state after await foreach (var server in servers) { diff --git a/src/StackExchange.Redis/Enums/ServerType.cs b/src/StackExchange.Redis/Enums/ServerType.cs index 1d86ae83c..6414fc5ea 100644 --- a/src/StackExchange.Redis/Enums/ServerType.cs +++ b/src/StackExchange.Redis/Enums/ServerType.cs @@ -39,7 +39,7 @@ internal static class ServerTypeExtensions }; /// - /// Whether a server type supports . + /// Whether a server type supports . /// public static bool SupportsAutoConfigure(this ServerType type) => type switch { diff --git a/src/StackExchange.Redis/LogProxy.cs b/src/StackExchange.Redis/LogProxy.cs new file mode 100644 index 000000000..6d24621ad --- /dev/null +++ b/src/StackExchange.Redis/LogProxy.cs @@ -0,0 +1,65 @@ +using System; +using System.IO; + +namespace StackExchange.Redis; + +internal sealed class LogProxy : IDisposable +{ + public static LogProxy TryCreate(TextWriter writer) + => writer == null ? null : new LogProxy(writer); + + public override string ToString() + { + string s = null; + if (_log != null) + { + lock (SyncLock) + { + s = _log?.ToString(); + } + } + return s ?? base.ToString(); + } + private TextWriter _log; + internal static Action NullWriter = _ => { }; + + public object SyncLock => this; + private LogProxy(TextWriter log) => _log = log; + public void WriteLine() + { + if (_log != null) // note: double-checked + { + lock (SyncLock) + { + _log?.WriteLine(); + } + } + } + public void WriteLine(string message = null) + { + if (_log != null) // note: double-checked + { + lock (SyncLock) + { + _log?.WriteLine($"{DateTime.UtcNow:HH:mm:ss.ffff}: {message}"); + } + } + } + public void WriteLine(string prefix, string message) + { + if (_log != null) // note: double-checked + { + lock (SyncLock) + { + _log?.WriteLine($"{DateTime.UtcNow:HH:mm:ss.ffff}: {prefix}{message}"); + } + } + } + public void Dispose() + { + if (_log != null) // note: double-checked + { + lock (SyncLock) { _log = null; } + } + } +} diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 8872c39fc..85c2cff7e 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -6,7 +6,6 @@ using System.Text; using System.Threading; using StackExchange.Redis.Profiling; -using static StackExchange.Redis.ConnectionMultiplexer; namespace StackExchange.Redis { diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index e39cad16a..53060d6a4 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -5,9 +5,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading; -using System.Threading.Channels; using System.Threading.Tasks; -using static StackExchange.Redis.ConnectionMultiplexer; #if !NETCOREAPP using Pipelines.Sockets.Unofficial.Threading; using static Pipelines.Sockets.Unofficial.Threading.MutexSlim; @@ -1433,8 +1431,8 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne message.SetRequestSent(); IncrementOpCount(); - // some commands smash our ability to trust the database; some commands - // demand an immediate flush + // Some commands smash our ability to trust the database + // and some commands demand an immediate flush switch (cmd) { case RedisCommand.EVAL: @@ -1460,11 +1458,11 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne Trace("Write failed: " + ex.Message); message.Fail(ConnectionFailureType.InternalFailure, ex, null, Multiplexer); message.Complete(); - // this failed without actually writing; we're OK with that... unless there's a transaction + // This failed without actually writing; we're OK with that... unless there's a transaction if (connection?.TransactionActive == true) { - // we left it in a broken state; need to kill the connection + // We left it in a broken state - need to kill the connection connection.RecordConnectionFailed(ConnectionFailureType.ProtocolFailure, ex); return WriteResult.WriteFailure; } @@ -1476,7 +1474,7 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne message.Fail(ConnectionFailureType.InternalFailure, ex, null, Multiplexer); message.Complete(); - // we're not sure *what* happened here; probably an IOException; kill the connection + // We're not sure *what* happened here - probably an IOException; kill the connection connection?.RecordConnectionFailed(ConnectionFailureType.InternalFailure, ex); return WriteResult.WriteFailure; } diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index f699d9055..8bae13482 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -17,7 +17,6 @@ using System.Threading.Tasks; using Pipelines.Sockets.Unofficial; using Pipelines.Sockets.Unofficial.Arenas; -using static StackExchange.Redis.ConnectionMultiplexer; namespace StackExchange.Redis { @@ -211,7 +210,7 @@ private enum ReadMode : byte { NotSpecified, ReadOnly, - ReadWrite + ReadWrite, } private readonly WeakReference _bridge; @@ -242,7 +241,6 @@ internal void Shutdown() try { ioPipe.Input?.Complete(); } catch { } try { ioPipe.Output?.CancelPendingFlush(); } catch { } try { ioPipe.Output?.Complete(); } catch { } - try { using (ioPipe as IDisposable) { } } catch { } } @@ -283,7 +281,10 @@ public Task FlushAsync() { _writeStatus = WriteStatus.Flushing; var flush = tmp.FlushAsync(); - if (!flush.IsCompletedSuccessfully) return AwaitedFlush(flush); + if (!flush.IsCompletedSuccessfully) + { + return AwaitedFlush(flush); + } _writeStatus = WriteStatus.Flushed; UpdateLastWriteTime(); } @@ -577,7 +578,8 @@ internal Message GetSelectDatabaseCommand(int targetDatabase, Message message) if (!serverEndpoint.SupportsDatabases) { if (targetDatabase != 0) - { // should never see this, since the API doesn't allow it; thus not too worried about ExceptionFactory + { + // We should never see this, since the API doesn't allow it; thus not too worried about ExceptionFactory throw new RedisCommandException("Multiple databases are not supported on this server; cannot switch to database: " + targetDatabase); } return null; @@ -585,7 +587,7 @@ internal Message GetSelectDatabaseCommand(int targetDatabase, Message message) if (message.Command == RedisCommand.SELECT) { - // this could come from an EVAL/EVALSHA inside a transaction, for example; we'll accept it + // This could come from an EVAL/EVALSHA inside a transaction, for example; we'll accept it BridgeCouldBeNull?.Trace("Switching database: " + targetDatabase); currentDatabase = targetDatabase; return null; @@ -593,11 +595,12 @@ internal Message GetSelectDatabaseCommand(int targetDatabase, Message message) if (TransactionActive) { - // should never see this, since the API doesn't allow it; thus not too worried about ExceptionFactory + // Should never see this, since the API doesn't allow it, thus not too worried about ExceptionFactory throw new RedisCommandException("Multiple databases inside a transaction are not currently supported: " + targetDatabase); } - if (available != 0 && targetDatabase >= available) // we positively know it is out of range + // We positively know it is out of range + if (available != 0 && targetDatabase >= available) { throw ExceptionFactory.DatabaseOutfRange(IncludeDetailInExceptions, targetDatabase, message, serverEndpoint); } @@ -609,8 +612,8 @@ internal Message GetSelectDatabaseCommand(int targetDatabase, Message message) internal static Message GetSelectDatabaseCommand(int targetDatabase) { return targetDatabase < DefaultRedisDatabaseCount - ? ReusableChangeDatabaseCommands[targetDatabase] // 0-15 by default - : Message.Create(targetDatabase, CommandFlags.FireAndForget, RedisCommand.SELECT); + ? ReusableChangeDatabaseCommands[targetDatabase] // 0-15 by default + : Message.Create(targetDatabase, CommandFlags.FireAndForget, RedisCommand.SELECT); } internal int GetSentAwaitingResponseCount() diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index c8678ed72..1c5547a47 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -8,7 +8,6 @@ using System.Text; using System.Threading.Tasks; using Pipelines.Sockets.Unofficial.Arenas; -using static StackExchange.Redis.ConnectionMultiplexer; namespace StackExchange.Redis { diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index 696b68204..cb769a569 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; using System.Net; using System.Threading; using System.Threading.Tasks; diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 085cd7ac7..7ebf2935c 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -648,8 +648,8 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal sealed class AutoConfigureProcessor : ResultProcessor { - private ConnectionMultiplexer.LogProxy Log { get; } - public AutoConfigureProcessor(ConnectionMultiplexer.LogProxy log = null) => Log = log; + private LogProxy Log { get; } + public AutoConfigureProcessor(LogProxy log = null) => Log = log; public override bool SetResult(PhysicalConnection connection, Message message, in RawResult result) { diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs old mode 100755 new mode 100644 index e87e2a73e..6c7c14afa --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -8,7 +8,6 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using static StackExchange.Redis.ConnectionMultiplexer; using static StackExchange.Redis.PhysicalBridge; namespace StackExchange.Redis @@ -19,7 +18,7 @@ internal enum UnselectableFlags None = 0, RedundantMaster = 1, DidNotRespond = 2, - ServerType = 4 + ServerType = 4, } internal sealed partial class ServerEndPoint : IDisposable @@ -71,11 +70,10 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) } } - public ClusterConfiguration ClusterConfiguration { get; private set; } + public EndPoint EndPoint { get; } - public int Databases { get { return databases; } set { SetConfig(ref databases, value); } } + public ClusterConfiguration ClusterConfiguration { get; private set; } - public EndPoint EndPoint { get; } /// /// Whether this endpoint supports databases at all. /// Note that some servers are cluster but present as standalone (e.g. Redis Enterprise), so we respect @@ -87,13 +85,17 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) public bool SupportsDatabases => supportsDatabases ??= (serverType == ServerType.Standalone && Multiplexer.RawConfig.CommandMap.IsAvailable(RedisCommand.SELECT)); + public int Databases + { + get => databases; + set => SetConfig(ref databases, value); + } + public bool IsConnecting => interactive?.IsConnecting == true; public bool IsConnected => interactive?.IsConnected == true; public bool IsSubscriberConnected => subscription?.IsConnected == true; public bool SupportsSubscriptions => Multiplexer.CommandMap.IsAvailable(RedisCommand.SUBSCRIBE); - - public bool IsConnecting => interactive?.IsConnecting == true; public bool SupportsPrimaryWrites => supportsPrimaryWrites ??= (!IsReplica || !ReplicaReadOnly || AllowReplicaWrites); private readonly List> _pendingConnectionMonitors = new List>(); @@ -157,20 +159,7 @@ internal Exception LastException internal State ConnectionState => interactive?.ConnectionState ?? State.Disconnected; - public bool IsReplica { get { return isReplica; } set { SetConfig(ref isReplica, value); } } - - public long OperationCount - { - get - { - long total = 0; - var tmp = interactive; - if (tmp != null) total += tmp.OperationCount; - tmp = subscription; - if (tmp != null) total += tmp.OperationCount; - return total; - } - } + public long OperationCount => interactive?.OperationCount ?? 0 + subscription?.OperationCount ?? 0; public bool RequiresReadMode => serverType == ServerType.Cluster && IsReplica; @@ -180,6 +169,12 @@ public ServerType ServerType set => SetConfig(ref serverType, value); } + public bool IsReplica + { + get => isReplica; + set => SetConfig(ref isReplica, value); + } + public bool ReplicaReadOnly { get => replicaReadOnly; @@ -210,19 +205,6 @@ public int WriteEverySeconds internal ConnectionMultiplexer Multiplexer { get; } - public void ClearUnselectable(UnselectableFlags flags) - { - var oldFlags = unselectableReasons; - if (oldFlags != 0) - { - unselectableReasons &= ~flags; - if (unselectableReasons != oldFlags) - { - Multiplexer.Trace(unselectableReasons == 0 ? "Now usable" : ("Now unusable: " + flags), ToString()); - } - } - } - public void Dispose() { isDisposed = true; @@ -336,6 +318,19 @@ public void SetUnselectable(UnselectableFlags flags) } } + public void ClearUnselectable(UnselectableFlags flags) + { + var oldFlags = unselectableReasons; + if (oldFlags != 0) + { + unselectableReasons &= ~flags; + if (unselectableReasons != oldFlags) + { + Multiplexer.Trace(unselectableReasons == 0 ? "Now usable" : ("Now unusable: " + flags), ToString()); + } + } + } + public override string ToString() => Format.ToString(EndPoint); [Obsolete("prefer async")] @@ -414,10 +409,11 @@ internal async Task AutoConfigureAsync(PhysicalConnection connection, LogProxy l } else if (commandMap.IsAvailable(RedisCommand.SET)) { - // this is a nasty way to find if we are a replica, and it will only work on up-level servers, but... + // This is a nasty way to find if we are a replica, and it will only work on up-level servers, but... RedisKey key = Multiplexer.UniqueId; - // the actual value here doesn't matter (we detect the error code if it fails); the value here is to at least give some - // indication to anyone watching via "monitor", but we could send two guids (key/value) and it would work the same + // The actual value here doesn't matter (we detect the error code if it fails). + // The value here is to at least give some indication to anyone watching via "monitor", + // but we could send two GUIDs (key/value) and it would work the same. msg = Message.Create(0, flags, RedisCommand.SET, key, RedisLiterals.replica_read_only, RedisLiterals.PX, 1, RedisLiterals.NX); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, autoConfigProcessor).ForAwait(); @@ -442,7 +438,10 @@ internal async Task AutoConfigureAsync(PhysicalConnection connection, LogProxy l } private int _nextReplicaOffset; - internal uint NextReplicaOffset() // used to round-robin between multiple replicas + /// + /// Used to round-robin between multiple replicas + /// + internal uint NextReplicaOffset() => (uint)Interlocked.Increment(ref _nextReplicaOffset); internal Task Close(ConnectionType connectionType) @@ -476,15 +475,18 @@ internal void FlushScriptCache() private string runId; internal string RunId { - get { return runId; } + get => runId; set { - if (value != runId) // we only care about changes + // We only care about changes + if (value != runId) { - // if we had an old run-id, and it has changed, then the - // server has been restarted; which means the script cache - // is toast - if (runId != null) FlushScriptCache(); + // If we had an old run-id, and it has changed, then the server has been restarted + // ...which means the script cache is toast + if (runId != null) + { + FlushScriptCache(); + } runId = value; } } @@ -527,7 +529,7 @@ internal byte[] GetScriptHash(string script, RedisCommand command) var found = (byte[])knownScripts[script]; if (found == null && command == RedisCommand.EVALSHA) { - // the script provided is a hex sha; store and re-use the ascii for that + // The script provided is a hex SHA - store and re-use the ASCii for that found = Encoding.ASCII.GetBytes(script); lock (knownScripts) { @@ -541,10 +543,10 @@ internal byte[] GetScriptHash(string script, RedisCommand command) internal Message GetTracerMessage(bool assertIdentity) { - // different configurations block certain commands, as can ad-hoc local configurations, so - // we'll do the best with what we have available. - // note that the muxer-ctor asserts that one of ECHO, PING, TIME of GET is available - // see also: TracerProcessor + // Different configurations block certain commands, as can ad-hoc local configurations, so + // we'll do the best with what we have available. + // Note: muxer-ctor asserts that one of ECHO, PING, TIME of GET is available + // See also: TracerProcessor var map = Multiplexer.CommandMap; Message msg; const CommandFlags flags = CommandFlags.NoRedirect | CommandFlags.FireAndForget; @@ -562,7 +564,7 @@ internal Message GetTracerMessage(bool assertIdentity) } else if (!assertIdentity && map.IsAvailable(RedisCommand.ECHO)) { - // we'll use echo as a PING substitute if it is all we have (in preference to EXISTS) + // We'll use echo as a PING substitute if it is all we have (in preference to EXISTS) msg = Message.Create(-1, flags, RedisCommand.ECHO, (RedisValue)Multiplexer.UniqueId); } else @@ -578,7 +580,7 @@ internal Message GetTracerMessage(bool assertIdentity) internal bool IsSelectable(RedisCommand command, bool allowDisconnected = false) { - // Until we've connected at least once, we're going too have a DidNotRespond unselectable reason present + // Until we've connected at least once, we're going to have a DidNotRespond unselectable reason present var bridge = unselectableReasons == 0 || (allowDisconnected && unselectableReasons == UnselectableFlags.DidNotRespond) ? GetBridge(command, false) : null; @@ -612,6 +614,18 @@ internal void OnDisconnected(PhysicalBridge bridge) internal Task OnEstablishingAsync(PhysicalConnection connection, LogProxy log) { + static async Task OnEstablishingAsyncAwaited(PhysicalConnection connection, Task handshake) + { + try + { + await handshake.ForAwait(); + } + catch (Exception ex) + { + connection.RecordConnectionFailed(ConnectionFailureType.InternalFailure, ex); + } + } + try { if (connection == null) return Task.CompletedTask; @@ -627,18 +641,6 @@ internal Task OnEstablishingAsync(PhysicalConnection connection, LogProxy log) return Task.CompletedTask; } - private static async Task OnEstablishingAsyncAwaited(PhysicalConnection connection, Task handshake) - { - try - { - await handshake.ForAwait(); - } - catch (Exception ex) - { - connection.RecordConnectionFailed(ConnectionFailureType.InternalFailure, ex); - } - } - internal void OnFullyEstablished(PhysicalConnection connection, string source) { try @@ -691,9 +693,9 @@ internal bool CheckInfoReplication() { lastInfoReplicationCheckTicks = Environment.TickCount; ResetExponentiallyReplicationCheck(); - PhysicalBridge bridge; + if (version >= RedisFeatures.v2_8_0 && Multiplexer.CommandMap.IsAvailable(RedisCommand.INFO) - && (bridge = GetBridge(ConnectionType.Interactive, false)) != null) + && GetBridge(ConnectionType.Interactive, false) is PhysicalBridge bridge) { var msg = Message.Create(-1, CommandFlags.FireAndForget | CommandFlags.NoRedirect, RedisCommand.INFO, RedisLiterals.replication); msg.SetInternalCall(); @@ -711,10 +713,12 @@ internal bool CheckInfoReplication() [ThreadStatic] private static Random r; - // Forces frequent replication check starting from 1 second up to max ConfigCheckSeconds with an exponential increment + /// + /// Forces frequent replication check starting from 1 second up to max ConfigCheckSeconds with an exponential increment. + /// internal void ForceExponentialBackoffReplicationCheck() { - ConfigCheckSeconds = 1; // start checking info replication more frequently + ConfigCheckSeconds = 1; } private void ResetExponentiallyReplicationCheck() @@ -732,6 +736,7 @@ private void ResetExponentiallyReplicationCheck() internal void OnHeartbeat() { // don't overlap operations on an endpoint + // Don't overlap heartbeat operations on an endpoint if (Interlocked.CompareExchange(ref _heartBeatActive, 1, 0) == 0) { try @@ -892,7 +897,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy log) return; } Message msg; - // note that we need "" (not null) for password in the case of 'nopass' logins + // Note that we need "" (not null) for password in the case of 'nopass' logins string user = Multiplexer.RawConfig.User, password = Multiplexer.RawConfig.Password ?? ""; if (!string.IsNullOrWhiteSpace(user)) { diff --git a/tests/StackExchange.Redis.Tests/EnvoyTests.cs b/tests/StackExchange.Redis.Tests/EnvoyTests.cs index b76761096..e3654a570 100644 --- a/tests/StackExchange.Redis.Tests/EnvoyTests.cs +++ b/tests/StackExchange.Redis.Tests/EnvoyTests.cs @@ -34,7 +34,7 @@ public void TestBasicEnvoyConnection() Assert.Equal(expectedVal, value); } } - catch (TimeoutException) when (sb.ToString().Contains("Returned, but incorrectly")) + catch (TimeoutException ex) when (ex.Message == "Connect timeout" || sb.ToString().Contains("Returned, but incorrectly")) { Skip.Inconclusive("Envoy server not found."); } From 58437c59cacf5b3df9e1ab64547ba1a11737c97f Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 8 Mar 2022 09:57:04 -0500 Subject: [PATCH 100/435] Rename step 1: master -> primary (#2026) This is the non-public surface area change of "master" to "primary", explicitly leaving the public ones for a much smaller PR we can critique more closely. Bonus points: the build badge has been pointing to the wrong branch for years, woops. --- .github/workflows/CI.yml | 4 +- README.md | 2 +- StackExchange.Redis.sln | 4 +- appveyor.yml | 4 +- docs/Basics.md | 4 +- docs/Configuration.md | 18 +- docs/Testing.md | 6 +- .../ClusterConfiguration.cs | 6 +- .../Configuration/DefaultOptionsProvider.cs | 2 +- .../ConfigurationOptions.cs | 2 +- .../ConnectionMultiplexer.cs | 166 +++++++++--------- src/StackExchange.Redis/Enums/ClientFlags.cs | 2 +- src/StackExchange.Redis/Enums/CommandFlags.cs | 10 +- .../Enums/ReplicationChangeOptions.cs | 4 +- src/StackExchange.Redis/ExceptionFactory.cs | 2 +- src/StackExchange.Redis/GlobalSuppressions.cs | 2 +- src/StackExchange.Redis/Interfaces/IServer.cs | 104 ++++++----- .../Interfaces/ISubscriber.cs | 2 +- src/StackExchange.Redis/Message.cs | 40 ++--- src/StackExchange.Redis/PhysicalBridge.cs | 14 +- src/StackExchange.Redis/PhysicalConnection.cs | 4 +- src/StackExchange.Redis/RedisDatabase.cs | 24 +-- src/StackExchange.Redis/RedisServer.cs | 20 +-- src/StackExchange.Redis/ResultProcessor.cs | 42 ++--- src/StackExchange.Redis/Role.cs | 12 +- src/StackExchange.Redis/ServerEndPoint.cs | 21 ++- .../ServerSelectionStrategy.cs | 19 +- .../{master-6379.conf => primary-6379.conf} | 2 +- tests/RedisConfigs/Docker/supervisord.conf | 8 +- .../{master-6382.conf => primary-6382.conf} | 2 +- .../RedisConfigs/Sentinel/sentinel-26379.conf | 8 +- .../RedisConfigs/Sentinel/sentinel-26380.conf | 8 +- .../RedisConfigs/Sentinel/sentinel-26381.conf | 8 +- tests/RedisConfigs/start-all.sh | 8 +- tests/RedisConfigs/start-basic.cmd | 4 +- tests/RedisConfigs/start-basic.sh | 4 +- tests/RedisConfigs/start-failover.cmd | 2 +- tests/StackExchange.Redis.Tests/AsyncTests.cs | 4 +- .../StackExchange.Redis.Tests/BacklogTests.cs | 8 +- tests/StackExchange.Redis.Tests/Cluster.cs | 66 +++---- tests/StackExchange.Redis.Tests/Config.cs | 28 +-- .../ConnectCustomConfig.cs | 2 +- .../ConnectToUnexistingHost.cs | 4 +- .../ConnectingFailDetection.cs | 6 +- .../ConnectionShutdown.cs | 4 +- tests/StackExchange.Redis.Tests/Databases.cs | 6 +- .../ExceptionFactoryTests.cs | 2 +- tests/StackExchange.Redis.Tests/Failover.cs | 36 ++-- .../GlobalSuppressions.cs | 6 +- .../Helpers/TestConfig.cs | 14 +- .../Issues/DefaultDatabase.cs | 4 +- .../Issues/Issue182.cs | 2 +- .../Issues/SO25567566.cs | 2 +- tests/StackExchange.Redis.Tests/Keys.cs | 4 +- tests/StackExchange.Redis.Tests/Locking.cs | 2 +- tests/StackExchange.Redis.Tests/Migrate.cs | 2 +- .../{MultiMaster.cs => MultiPrimary.cs} | 25 ++- tests/StackExchange.Redis.Tests/Naming.cs | 24 +-- .../StackExchange.Redis.Tests/Performance.cs | 2 +- tests/StackExchange.Redis.Tests/Profiling.cs | 2 +- tests/StackExchange.Redis.Tests/PubSub.cs | 6 +- .../PubSubMultiserver.cs | 2 +- tests/StackExchange.Redis.Tests/RealWorld.cs | 4 +- tests/StackExchange.Redis.Tests/Roles.cs | 16 +- tests/StackExchange.Redis.Tests/Scripting.cs | 18 +- tests/StackExchange.Redis.Tests/Sentinel.cs | 64 +++---- .../StackExchange.Redis.Tests/SentinelBase.cs | 48 ++--- .../SentinelFailover.cs | 28 +-- tests/StackExchange.Redis.Tests/Sets.cs | 2 +- tests/StackExchange.Redis.Tests/Sockets.cs | 2 +- tests/StackExchange.Redis.Tests/TestBase.cs | 10 +- .../StackExchange.Redis.Tests/TestConfig.json | 2 +- 72 files changed, 523 insertions(+), 528 deletions(-) rename tests/RedisConfigs/Basic/{master-6379.conf => primary-6379.conf} (80%) rename tests/RedisConfigs/Failover/{master-6382.conf => primary-6382.conf} (80%) rename tests/StackExchange.Redis.Tests/{MultiMaster.cs => MultiPrimary.cs} (77%) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 44bd09eb6..098dc0ced 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -56,10 +56,10 @@ jobs: - name: Start Redis Services (v3.0.503) working-directory: .\tests\RedisConfigs\3.0.503 run: | - .\redis-server.exe --service-install --service-name "redis-6379" "..\Basic\master-6379.conf" + .\redis-server.exe --service-install --service-name "redis-6379" "..\Basic\primary-6379.conf" .\redis-server.exe --service-install --service-name "redis-6380" "..\Basic\replica-6380.conf" .\redis-server.exe --service-install --service-name "redis-6381" "..\Basic\secure-6381.conf" - .\redis-server.exe --service-install --service-name "redis-6382" "..\Failover\master-6382.conf" + .\redis-server.exe --service-install --service-name "redis-6382" "..\Failover\primary-6382.conf" .\redis-server.exe --service-install --service-name "redis-6383" "..\Failover\replica-6383.conf" .\redis-server.exe --service-install --service-name "redis-7000" "..\Cluster\cluster-7000.conf" --dir "..\Cluster" .\redis-server.exe --service-install --service-name "redis-7001" "..\Cluster\cluster-7001.conf" --dir "..\Cluster" diff --git a/README.md b/README.md index 732579c1b..da72b84ae 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ For all documentation, [see here](https://stackexchange.github.io/StackExchange. #### Build Status -[![Build status](https://ci.appveyor.com/api/projects/status/2o3frasprum8mbaj/branch/master?svg=true)](https://ci.appveyor.com/project/StackExchange/stackexchange-redis/branch/master) +[![Build status](https://ci.appveyor.com/api/projects/status/2o3frasprum8mbaj/branch/main?svg=true)](https://ci.appveyor.com/project/StackExchange/stackexchange-redis/branch/main) #### Package Status diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index 514205e64..54de8bdb0 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -68,7 +68,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Cluster", "Cluster", "{A3B4 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Basic", "Basic", "{38BDEEED-7BEB-4B1F-9CE0-256D63F9C502}" ProjectSection(SolutionItems) = preProject - tests\RedisConfigs\Basic\master-6379.conf = tests\RedisConfigs\Basic\master-6379.conf + tests\RedisConfigs\Basic\primary-6379.conf = tests\RedisConfigs\Basic\primary-6379.conf tests\RedisConfigs\Basic\replica-6380.conf = tests\RedisConfigs\Basic\replica-6380.conf tests\RedisConfigs\Basic\secure-6381.conf = tests\RedisConfigs\Basic\secure-6381.conf EndProjectSection @@ -79,7 +79,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestConsole", "toys\TestCon EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Failover", "Failover", "{D082703F-1652-4C35-840D-7D377F6B9979}" ProjectSection(SolutionItems) = preProject - tests\RedisConfigs\Failover\master-6382.conf = tests\RedisConfigs\Failover\master-6382.conf + tests\RedisConfigs\Failover\primary-6382.conf = tests\RedisConfigs\Failover\primary-6382.conf tests\RedisConfigs\Failover\replica-6383.conf = tests\RedisConfigs\Failover\replica-6383.conf EndProjectSection EndProject diff --git a/appveyor.yml b/appveyor.yml index a2107f48c..5c6123e37 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,13 +10,13 @@ install: cd tests\RedisConfigs\3.0.503 - redis-server.exe --service-install --service-name "redis-6379" "..\Basic\master-6379.conf" + redis-server.exe --service-install --service-name "redis-6379" "..\Basic\primary-6379.conf" redis-server.exe --service-install --service-name "redis-6380" "..\Basic\replica-6380.conf" redis-server.exe --service-install --service-name "redis-6381" "..\Basic\secure-6381.conf" - redis-server.exe --service-install --service-name "redis-6382" "..\Failover\master-6382.conf" + redis-server.exe --service-install --service-name "redis-6382" "..\Failover\primary-6382.conf" redis-server.exe --service-install --service-name "redis-6383" "..\Failover\replica-6383.conf" diff --git a/docs/Basics.md b/docs/Basics.md index eddcb98a7..4d843cb3a 100644 --- a/docs/Basics.md +++ b/docs/Basics.md @@ -12,13 +12,13 @@ ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); Note that `ConnectionMultiplexer` implements `IDisposable` and can be disposed when no longer required. This is deliberately not showing `using` statement usage, because it is exceptionally rare that you would want to use a `ConnectionMultiplexer` briefly, as the idea is to re-use this object. -A more complicated scenario might involve a master/replica setup; for this usage, simply specify all the desired nodes that make up that logical redis tier (it will automatically identify the master): +A more complicated scenario might involve a primary/replica setup; for this usage, simply specify all the desired nodes that make up that logical redis tier (it will automatically identify the primary): ```csharp ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("server1:6379,server2:6379"); ``` -If it finds both nodes are masters, a tie-breaker key can optionally be specified that can be used to resolve the issue, however such a condition is fortunately very rare. +If it finds both nodes are primaries, a tie-breaker key can optionally be specified that can be used to resolve the issue, however such a condition is fortunately very rare. Once you have a `ConnectionMultiplexer`, there are 3 main things you might want to do: diff --git a/docs/Configuration.md b/docs/Configuration.md index cce38dc17..b17c623f4 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -31,11 +31,11 @@ var conn = ConnectionMultiplexer.Connect("redis0:6380,redis1:6380,allowAdmin=tru ``` If you specify a serviceName in the connection string, it will trigger sentinel mode. This example will connect to a sentinel server on the local machine -using the default sentinel port (26379), discover the current master server for the `mymaster` service and return a managed connection -pointing to that master server that will automatically be updated if the master changes: +using the default sentinel port (26379), discover the current primary server for the `myprimary` service and return a managed connection +pointing to that primary server that will automatically be updated if the primary changes: ```csharp -var conn = ConnectionMultiplexer.Connect("localhost,serviceName=mymaster"); +var conn = ConnectionMultiplexer.Connect("localhost,serviceName=myprimary"); ``` An overview of mapping between the `string` and `ConfigurationOptions` representation is shown below, but you can switch between them trivially: @@ -88,13 +88,13 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a | user={string} | `User` | `null` | User for the redis server (for use with ACLs on redis 6 and above) | | proxy={proxy type} | `Proxy` | `Proxy.None` | Type of proxy in use (if any); for example "twemproxy/envoyproxy" | | resolveDns={bool} | `ResolveDns` | `false` | Specifies that DNS resolution should be explicit and eager, rather than implicit | -| serviceName={string} | `ServiceName` | `null` | Used for connecting to a sentinel master service | +| serviceName={string} | `ServiceName` | `null` | Used for connecting to a sentinel primary service | | ssl={bool} | `Ssl` | `false` | Specifies that SSL encryption should be used | | sslHost={string} | `SslHost` | `null` | Enforces a particular SSL host identity on the server's certificate | | sslProtocols={enum} | `SslProtocols` | `null` | Ssl/Tls versions supported when using an encrypted connection. Use '\|' to provide multiple values. | | syncTimeout={int} | `SyncTimeout` | `5000` | Time (ms) to allow for synchronous operations | | asyncTimeout={int} | `AsyncTimeout` | `SyncTimeout` | Time (ms) to allow for asynchronous operations | -| tiebreaker={string} | `TieBreaker` | `__Booksleeve_TieBreak` | Key to use for selecting a server in an ambiguous master scenario | +| tiebreaker={string} | `TieBreaker` | `__Booksleeve_TieBreak` | Key to use for selecting a server in an ambiguous primary scenario | | version={string} | `DefaultVersion` | (`3.0` in Azure, else `2.0`) | Redis version level (useful when the server does not make this available) | | | `CheckCertificateRevocation` | `true` | A Boolean value that specifies whether the certificate revocation list is checked during authentication. | @@ -117,7 +117,7 @@ These options are parsed in connection strings for backwards compatibility (mean Automatic and Manual Configuration --- -In many common scenarios, StackExchange.Redis will automatically configure a lot of settings, including the server type and version, connection timeouts, and master/replica relationships. Sometimes, though, the commands for this have been disabled on the redis server. In this case, it is useful to provide more information: +In many common scenarios, StackExchange.Redis will automatically configure a lot of settings, including the server type and version, connection timeouts, and primary/replica relationships. Sometimes, though, the commands for this have been disabled on the redis server. In this case, it is useful to provide more information: ```csharp ConfigurationOptions config = new ConfigurationOptions @@ -193,13 +193,13 @@ var options = new ConfigurationOptions+{ Tiebreakers and Configuration Change Announcements --- -Normally StackExchange.Redis will resolve master/replica nodes automatically. However, if you are not using a management tool such as redis-sentinel or redis cluster, there is a chance that occasionally you will get multiple master nodes (for example, while resetting a node for maintenance it may reappear on the network as a master). To help with this, StackExchange.Redis can use the notion of a *tie-breaker* - which is only used when multiple masters are detected (not including redis cluster, where multiple masters are *expected*). For compatibility with BookSleeve, this defaults to the key named `"__Booksleeve_TieBreak"` (always in database 0). This is used as a crude voting mechanism to help determine the *preferred* master, so that work is routed correctly. +Normally StackExchange.Redis will resolve primary/replica nodes automatically. However, if you are not using a management tool such as redis-sentinel or redis cluster, there is a chance that occasionally you will get multiple primary nodes (for example, while resetting a node for maintenance it may reappear on the network as a primary). To help with this, StackExchange.Redis can use the notion of a *tie-breaker* - which is only used when multiple primaries are detected (not including redis cluster, where multiple primaries are *expected*). For compatibility with BookSleeve, this defaults to the key named `"__Booksleeve_TieBreak"` (always in database 0). This is used as a crude voting mechanism to help determine the *preferred* primary, so that work is routed correctly. -Likewise, when the configuration is changed (especially the master/replica configuration), it will be important for connected instances to make themselves aware of the new situation (via `INFO`, `CONFIG`, etc - where available). StackExchange.Redis does this by automatically subscribing to a pub/sub channel upon which such notifications may be sent. For similar reasons, this defaults to `"__Booksleeve_MasterChanged"`. +Likewise, when the configuration is changed (especially the primary/replica configuration), it will be important for connected instances to make themselves aware of the new situation (via `INFO`, `CONFIG`, etc - where available). StackExchange.Redis does this by automatically subscribing to a pub/sub channel upon which such notifications may be sent. For similar reasons, this defaults to `"__Booksleeve_MasterChanged"`. Both options can be customized or disabled (set to `""`), via the `.ConfigurationChannel` and `.TieBreaker` configuration properties. -These settings are also used by the `IServer.MakeMaster()` method, which can set the tie-breaker in the database and broadcast the configuration change message. The configuration message can also be used separately to master/replica changes simply to request all nodes to refresh their configurations, via the `ConnectionMultiplexer.PublishReconfigure` method. +These settings are also used by the `IServer.MakeMaster()` method, which can set the tie-breaker in the database and broadcast the configuration change message. The configuration message can also be used separately to primary/replica changes simply to request all nodes to refresh their configurations, via the `ConnectionMultiplexer.PublishReconfigure` method. ReconnectRetryPolicy --- diff --git a/docs/Testing.md b/docs/Testing.md index f9c812811..60e56a429 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -16,12 +16,12 @@ The unit and integration tests here are fairly straightforward. There are 2 prim Tests default to `127.0.0.1` as their server, however you can override any of the test IPs/Hostnames and ports by placing a `TestConfig.json` in the `StackExchange.Redis.Tests\` folder. This file is intentionally in `.gitignore` already, as it's for *your* personal overrides. This is useful for testing local or remote servers, different versions, various ports, etc. -You can find all the JSON properties at [TestConfig.cs](https://github.com/StackExchange/StackExchange.Redis/blob/master/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs). An example override (everything not specified being a default) would look like this: +You can find all the JSON properties at [TestConfig.cs](https://github.com/StackExchange/StackExchange.Redis/blob/main/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs). An example override (everything not specified being a default) would look like this: ```json { "RunLongRunning": true, - "MasterServer": "192.168.0.42", - "MasterPort": 12345 + "PrimaryServer": "192.168.0.42", + "PrimaryPort": 12345 } ``` Note: if a server isn't specified, the related tests should be skipped as inconclusive. diff --git a/src/StackExchange.Redis/ClusterConfiguration.cs b/src/StackExchange.Redis/ClusterConfiguration.cs index f05193fe6..e197f38d4 100644 --- a/src/StackExchange.Redis/ClusterConfiguration.cs +++ b/src/StackExchange.Redis/ClusterConfiguration.cs @@ -171,7 +171,7 @@ internal ClusterConfiguration(ServerSelectionStrategy serverSelectionStrategy, s if (string.IsNullOrWhiteSpace(line)) continue; var node = new ClusterNode(this, line, origin); - // Be resilient to ":0 {master,replica},fail,noaddr" nodes, and nodes where the endpoint doesn't parse + // Be resilient to ":0 {primary,replica},fail,noaddr" nodes, and nodes where the endpoint doesn't parse if (node.IsNoAddr || node.EndPoint == null) continue; @@ -419,9 +419,9 @@ public int CompareTo(ClusterNode other) { if (other == null) return -1; - if (IsReplica != other.IsReplica) return IsReplica ? 1 : -1; // masters first + if (IsReplica != other.IsReplica) return IsReplica ? 1 : -1; // primaries first - if (IsReplica) // both replicas? compare by parent, so we get masters A, B, C and then replicas of A, B, C + if (IsReplica) // both replicas? compare by parent, so we get primaries A, B, C and then replicas of A, B, C { int i = string.CompareOrdinal(ParentNodeId, other.ParentNodeId); if (i != 0) return i; diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index 98df24ce7..f722b009b 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -136,7 +136,7 @@ public class DefaultOptionsProvider public virtual TimeSpan SyncTimeout => TimeSpan.FromSeconds(5); /// - /// Tie-breaker used to choose between masters (must match the endpoint exactly). + /// Tie-breaker used to choose between primaries (must match the endpoint exactly). /// public virtual string TieBreaker => "__Booksleeve_TieBreak"; diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 3ad15b050..f0fdb733e 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -460,7 +460,7 @@ public int SyncTimeout } /// - /// Tie-breaker used to choose between masters (must match the endpoint exactly). + /// Tie-breaker used to choose between primaries (must match the endpoint exactly). /// public string TieBreaker { diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 2734b31e5..032b413d5 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -360,7 +360,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt } var nodes = GetServerSnapshot().ToArray(); // Have to array because async/await - RedisValue newMaster = Format.ToString(server.EndPoint); + RedisValue newPrimary = Format.ToString(server.EndPoint); RedisKey tieBreakerKey = default(RedisKey); // try and write this everywhere; don't worry if some folks reject our advances @@ -373,7 +373,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt { if (!node.IsConnected || node.IsReplica) continue; log?.WriteLine($"Attempting to set tie-breaker on {Format.ToString(node.EndPoint)}..."); - msg = Message.Create(0, flags | CommandFlags.FireAndForget, RedisCommand.SET, tieBreakerKey, newMaster); + msg = Message.Create(0, flags | CommandFlags.FireAndForget, RedisCommand.SET, tieBreakerKey, newPrimary); try { await node.WriteDirectAsync(msg, ResultProcessor.DemandOK); @@ -383,7 +383,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt } // stop replicating, promote to a standalone primary - log?.WriteLine($"Making {Format.ToString(srv.EndPoint)} a master..."); + log?.WriteLine($"Making {Format.ToString(srv.EndPoint)} a primary..."); try { await srv.ReplicaOfAsync(null, flags); @@ -398,7 +398,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt if (!tieBreakerKey.IsNull && !server.IsReplica) { log?.WriteLine($"Resending tie-breaker to {Format.ToString(server.EndPoint)}..."); - msg = Message.Create(0, flags | CommandFlags.FireAndForget, RedisCommand.SET, tieBreakerKey, newMaster); + msg = Message.Create(0, flags | CommandFlags.FireAndForget, RedisCommand.SET, tieBreakerKey, newPrimary); try { await server.WriteDirectAsync(msg, ResultProcessor.DemandOK); @@ -412,7 +412,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt // failing...making our needed reconfiguration a no-op. // If we don't block *that* run, then *our* run (at low latency) gets blocked. Then we're waiting on the // ConfigurationOptions.ConfigCheckSeconds interval to identify the current (created by this method call) topology correctly. - var blockingReconfig = Interlocked.CompareExchange(ref activeConfigCause, "Block: Pending Master Reconfig", null) == null; + var blockingReconfig = Interlocked.CompareExchange(ref activeConfigCause, "Block: Pending Primary Reconfig", null) == null; // Try and broadcast the fact a change happened to all members // We want everyone possible to pick it up. @@ -428,7 +428,7 @@ async Task BroadcastAsync(ServerEndPoint[] serverNodes) { if (!node.IsConnected) continue; log?.WriteLine($"Broadcasting via {Format.ToString(node.EndPoint)}..."); - msg = Message.Create(-1, flags | CommandFlags.FireAndForget, RedisCommand.PUBLISH, channel, newMaster); + msg = Message.Create(-1, flags | CommandFlags.FireAndForget, RedisCommand.PUBLISH, channel, newPrimary); await node.WriteDirectAsync(msg, ResultProcessor.Int64); } } @@ -462,7 +462,7 @@ async Task BroadcastAsync(ServerEndPoint[] serverNodes) { Interlocked.Exchange(ref activeConfigCause, null); } - if (!await ReconfigureAsync(first: false, reconfigureAll: true, log, srv.EndPoint, "make master")) + if (!await ReconfigureAsync(first: false, reconfigureAll: true, log, srv.EndPoint, cause: nameof(MakePrimaryAsync))) { log?.WriteLine("Verifying the configuration was incomplete; please verify"); } @@ -822,7 +822,7 @@ public static Task ConnectAsync(ConfigurationOptions conf SocketConnection.AssertDependencies(); if (IsSentinel(configuration)) - return SentinelMasterConnectAsync(configuration, log); + return SentinelPrimaryConnectAsync(configuration, log); return ConnectImplAsync(PrepareConfig(configuration), log); } @@ -961,7 +961,7 @@ public static ConnectionMultiplexer Connect(ConfigurationOptions configuration, if (IsSentinel(configuration)) { - return SentinelMasterConnect(configuration, log); + return SentinelPrimaryConnect(configuration, log); } return ConnectImpl(PrepareConfig(configuration), log); @@ -1017,9 +1017,9 @@ public static Task SentinelConnectAsync(ConfigurationOpti /// /// The string configuration to use for this multiplexer. /// The to log to. - private static ConnectionMultiplexer SentinelMasterConnect(string configuration, TextWriter log = null) + private static ConnectionMultiplexer SentinelPrimaryConnect(string configuration, TextWriter log = null) { - return SentinelMasterConnect(PrepareConfig(configuration, sentinel: true), log); + return SentinelPrimaryConnect(PrepareConfig(configuration, sentinel: true), log); } /// @@ -1028,7 +1028,7 @@ private static ConnectionMultiplexer SentinelMasterConnect(string configuration, /// /// The configuration options to use for this multiplexer. /// The to log to. - private static ConnectionMultiplexer SentinelMasterConnect(ConfigurationOptions configuration, TextWriter log = null) + private static ConnectionMultiplexer SentinelPrimaryConnect(ConfigurationOptions configuration, TextWriter log = null) { var sentinelConnection = SentinelConnect(configuration, log); @@ -1045,9 +1045,9 @@ private static ConnectionMultiplexer SentinelMasterConnect(ConfigurationOptions /// /// The string configuration to use for this multiplexer. /// The to log to. - private static Task SentinelMasterConnectAsync(string configuration, TextWriter log = null) + private static Task SentinelPrimaryConnectAsync(string configuration, TextWriter log = null) { - return SentinelMasterConnectAsync(PrepareConfig(configuration, sentinel: true), log); + return SentinelPrimaryConnectAsync(PrepareConfig(configuration, sentinel: true), log); } /// @@ -1056,7 +1056,7 @@ private static Task SentinelMasterConnectAsync(string con /// /// The configuration options to use for this multiplexer. /// The to log to. - private static async Task SentinelMasterConnectAsync(ConfigurationOptions configuration, TextWriter log = null) + private static async Task SentinelPrimaryConnectAsync(ConfigurationOptions configuration, TextWriter log = null) { var sentinelConnection = await SentinelConnectAsync(configuration, log).ForAwait(); @@ -1628,7 +1628,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP { throw new InvalidOperationException("No nodes to consider"); } - List masters = new List(endpoints.Count); + List primaries = new List(endpoints.Count); ServerEndPoint[] servers = null; bool encounteredConnectedClusterServer = false; @@ -1742,7 +1742,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP updatedClusterEndpointCollection = await GetEndpointsFromClusterNodes(server, log).ForAwait(); } - // Set the server UnselectableFlags and update masters list + // Set the server UnselectableFlags and update primaries list switch (server.ServerType) { case ServerType.Twemproxy: @@ -1753,11 +1753,11 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP server.ClearUnselectable(UnselectableFlags.ServerType); if (server.IsReplica) { - server.ClearUnselectable(UnselectableFlags.RedundantMaster); + server.ClearUnselectable(UnselectableFlags.RedundantPrimary); } else { - masters.Add(server); + primaries.Add(server); } break; default: @@ -1813,18 +1813,18 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP // ...for those cases, we want to allow sending to any primary endpoint. if (ServerSelectionStrategy.ServerType.HasSinglePrimary()) { - var preferred = NominatePreferredMaster(log, servers, useTieBreakers, masters); - foreach (var master in masters) + var preferred = NominatePreferredPrimary(log, servers, useTieBreakers, primaries); + foreach (var primary in primaries) { - if (master == preferred || master.IsReplica) + if (primary == preferred || primary.IsReplica) { - log?.WriteLine($"{Format.ToString(master)}: Clearing as RedundantMaster"); - master.ClearUnselectable(UnselectableFlags.RedundantMaster); + log?.WriteLine($"{Format.ToString(primary)}: Clearing as RedundantPrimary"); + primary.ClearUnselectable(UnselectableFlags.RedundantPrimary); } else { - log?.WriteLine($"{Format.ToString(master)}: Setting as RedundantMaster"); - master.SetUnselectable(UnselectableFlags.RedundantMaster); + log?.WriteLine($"{Format.ToString(primary)}: Setting as RedundantPrimary"); + primary.SetUnselectable(UnselectableFlags.RedundantPrimary); } } } @@ -1944,7 +1944,7 @@ private void ResetAllNonConnected() [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Partial - may use instance data")] partial void OnTraceLog(LogProxy log, [CallerMemberName] string caller = null); - private static ServerEndPoint NominatePreferredMaster(LogProxy log, ServerEndPoint[] servers, bool useTieBreakers, List masters) + private static ServerEndPoint NominatePreferredPrimary(LogProxy log, ServerEndPoint[] servers, bool useTieBreakers, List primaries) { log?.WriteLine("Election summary:"); @@ -1971,16 +1971,16 @@ private static ServerEndPoint NominatePreferredMaster(LogProxy log, ServerEndPoi } } - switch (masters.Count) + switch (primaries.Count) { case 0: - log?.WriteLine(" Election: No masters detected"); + log?.WriteLine(" Election: No primaries detected"); return null; case 1: - log?.WriteLine($" Election: Single master detected: {Format.ToString(masters[0].EndPoint)}"); - return masters[0]; + log?.WriteLine($" Election: Single primary detected: {Format.ToString(primaries[0].EndPoint)}"); + return primaries[0]; default: - log?.WriteLine(" Election: Multiple masters detected..."); + log?.WriteLine(" Election: Multiple primaries detected..."); if (useTieBreakers && uniques != null) { switch (uniques.Count) @@ -2019,7 +2019,7 @@ private static ServerEndPoint NominatePreferredMaster(LogProxy log, ServerEndPoi { if (arbitrary) { - log?.WriteLine($" Election: Choosing master arbitrarily: {Format.ToString(highest.EndPoint)}"); + log?.WriteLine($" Election: Choosing primary arbitrarily: {Format.ToString(highest.EndPoint)}"); } else { @@ -2033,8 +2033,8 @@ private static ServerEndPoint NominatePreferredMaster(LogProxy log, ServerEndPoi break; } - log?.WriteLine($" Election: Choosing master arbitrarily: {Format.ToString(masters[0].EndPoint)}"); - return masters[0]; + log?.WriteLine($" Election: Choosing primary arbitrarily: {Format.ToString(primaries[0].EndPoint)}"); + return primaries[0]; } private static ServerEndPoint SelectServerByElection(ServerEndPoint[] servers, string endpoint, LogProxy log) @@ -2125,9 +2125,9 @@ private bool PrepareToPushMessageToBridge(Message message, ResultProcessor } else // A server was specified - do we trust their choice, though? { - if (message.IsMasterOnly() && server.IsReplica) + if (message.IsPrimaryOnly() && server.IsReplica) { - throw ExceptionFactory.MasterOnly(IncludeDetailInExceptions, message.Command, message, server); + throw ExceptionFactory.PrimaryOnly(IncludeDetailInExceptions, message.Command, message, server); } switch (server.ServerType) @@ -2233,7 +2233,7 @@ public bool IsConnecting internal ServerSelectionStrategy ServerSelectionStrategy { get; } - internal Timer sentinelMasterReconnectTimer; + internal Timer sentinelPrimaryReconnectTimer; internal Dictionary sentinelConnectionChildren = new Dictionary(); internal ConnectionMultiplexer sentinelConnection = null; @@ -2261,7 +2261,7 @@ internal void InitializeSentinel(LogProxy logProxy) lock (sentinelConnectionChildren) { - // Switch the master if we have connections for that service + // Switch the primary if we have connections for that service if (sentinelConnectionChildren.ContainsKey(messageParts[0])) { ConnectionMultiplexer child = sentinelConnectionChildren[messageParts[0]]; @@ -2275,7 +2275,7 @@ internal void InitializeSentinel(LogProxy logProxy) } else { - SwitchMaster(switchBlame, sentinelConnectionChildren[messageParts[0]]); + SwitchPrimary(switchBlame, sentinelConnectionChildren[messageParts[0]]); } } } @@ -2330,13 +2330,13 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co do { // Get an initial endpoint - try twice - EndPoint newMasterEndPoint = GetConfiguredMasterForService(config.ServiceName) - ?? GetConfiguredMasterForService(config.ServiceName); + EndPoint newPrimaryEndPoint = GetConfiguredPrimaryForService(config.ServiceName) + ?? GetConfiguredPrimaryForService(config.ServiceName); - if (newMasterEndPoint == null) + if (newPrimaryEndPoint == null) { throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, - $"Sentinel: Failed connecting to configured master for service: {config.ServiceName}"); + $"Sentinel: Failed connecting to configured primary for service: {config.ServiceName}"); } EndPoint[] replicaEndPoints = GetReplicasForService(config.ServiceName) @@ -2346,12 +2346,12 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co // If not, assume the last state is the best we have and minimize the race if (config.EndPoints.Count == 1) { - config.EndPoints[0] = newMasterEndPoint; + config.EndPoints[0] = newPrimaryEndPoint; } else { config.EndPoints.Clear(); - config.EndPoints.TryAdd(newMasterEndPoint); + config.EndPoints.TryAdd(newPrimaryEndPoint); } foreach (var replicaEndPoint in replicaEndPoints) @@ -2361,9 +2361,9 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co connection = ConnectImpl(config, log); - // verify role is master according to: + // verify role is primary according to: // https://redis.io/topics/sentinel-clients - if (connection.GetServer(newMasterEndPoint)?.Role().Value == RedisLiterals.master) + if (connection.GetServer(newPrimaryEndPoint)?.Role().Value == RedisLiterals.master) { success = true; break; @@ -2375,13 +2375,13 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co if (!success) { throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, - $"Sentinel: Failed connecting to configured master for service: {config.ServiceName}"); + $"Sentinel: Failed connecting to configured primary for service: {config.ServiceName}"); } - // Attach to reconnect event to ensure proper connection to the new master + // Attach to reconnect event to ensure proper connection to the new primary connection.ConnectionRestored += OnManagedConnectionRestored; - // If we lost the connection, run a switch to a least try and get updated info about the master + // If we lost the connection, run a switch to a least try and get updated info about the primary connection.ConnectionFailed += OnManagedConnectionFailed; lock (sentinelConnectionChildren) @@ -2390,7 +2390,7 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co } // Perform the initial switchover - SwitchMaster(RawConfig.EndPoints[0], connection, log); + SwitchPrimary(RawConfig.EndPoints[0], connection, log); return connection; } @@ -2400,33 +2400,33 @@ internal void OnManagedConnectionRestored(object sender, ConnectionFailedEventAr { ConnectionMultiplexer connection = (ConnectionMultiplexer)sender; - var oldTimer = Interlocked.Exchange(ref connection.sentinelMasterReconnectTimer, null); + var oldTimer = Interlocked.Exchange(ref connection.sentinelPrimaryReconnectTimer, null); oldTimer?.Dispose(); try { // Run a switch to make sure we have update-to-date - // information about which master we should connect to - SwitchMaster(e.EndPoint, connection); + // information about which primary we should connect to + SwitchPrimary(e.EndPoint, connection); try { - // Verify that the reconnected endpoint is a master, + // Verify that the reconnected endpoint is a primary, // and the correct one otherwise we should reconnect - if (connection.GetServer(e.EndPoint).IsReplica || e.EndPoint != connection.currentSentinelMasterEndPoint) + if (connection.GetServer(e.EndPoint).IsReplica || e.EndPoint != connection.currentSentinelPrimaryEndPoint) { - // This isn't a master, so try connecting again - SwitchMaster(e.EndPoint, connection); + // This isn't a primary, so try connecting again + SwitchPrimary(e.EndPoint, connection); } } catch (Exception) { // If we get here it means that we tried to reconnect to a server that is no longer - // considered a master by Sentinel and was removed from the list of endpoints. + // considered a primary by Sentinel and was removed from the list of endpoints. // If we caught an exception, we may have gotten a stale endpoint // we are not aware of, so retry - SwitchMaster(e.EndPoint, connection); + SwitchPrimary(e.EndPoint, connection); } } catch (Exception) @@ -2440,30 +2440,30 @@ internal void OnManagedConnectionRestored(object sender, ConnectionFailedEventAr internal void OnManagedConnectionFailed(object sender, ConnectionFailedEventArgs e) { ConnectionMultiplexer connection = (ConnectionMultiplexer)sender; - // Periodically check to see if we can reconnect to the proper master. + // Periodically check to see if we can reconnect to the proper primary. // This is here in case we lost our subscription to a good sentinel instance - // or if we miss the published master change - if (connection.sentinelMasterReconnectTimer == null) + // or if we miss the published primary change. + if (connection.sentinelPrimaryReconnectTimer == null) { - connection.sentinelMasterReconnectTimer = new Timer(_ => + connection.sentinelPrimaryReconnectTimer = new Timer(_ => { try { // Attempt, but do not fail here - SwitchMaster(e.EndPoint, connection); + SwitchPrimary(e.EndPoint, connection); } catch (Exception) { } finally { - connection.sentinelMasterReconnectTimer?.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan); + connection.sentinelPrimaryReconnectTimer?.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan); } }, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan); } } - internal EndPoint GetConfiguredMasterForService(string serviceName) => + internal EndPoint GetConfiguredPrimaryForService(string serviceName) => GetServerSnapshot() .ToArray() .Where(s => s.ServerType == ServerType.Sentinel) @@ -2475,7 +2475,7 @@ internal EndPoint GetConfiguredMasterForService(string serviceName) => }) .FirstOrDefault(r => r != null); - internal EndPoint currentSentinelMasterEndPoint; + internal EndPoint currentSentinelPrimaryEndPoint; internal EndPoint[] GetReplicasForService(string serviceName) => GetServerSnapshot() @@ -2490,12 +2490,12 @@ internal EndPoint[] GetReplicasForService(string serviceName) => .FirstOrDefault(r => r != null); /// - /// Switches the SentinelMasterConnection over to a new master. + /// Switches the SentinelMasterConnection over to a new primary. /// /// The endpoint responsible for the switch. - /// The connection that should be switched over to a new master endpoint. + /// The connection that should be switched over to a new primary endpoint. /// The writer to log to, if any. - internal void SwitchMaster(EndPoint switchBlame, ConnectionMultiplexer connection, TextWriter log = null) + internal void SwitchPrimary(EndPoint switchBlame, ConnectionMultiplexer connection, TextWriter log = null) { if (log == null) log = TextWriter.Null; @@ -2503,30 +2503,30 @@ internal void SwitchMaster(EndPoint switchBlame, ConnectionMultiplexer connectio { string serviceName = connection.RawConfig.ServiceName; - // Get new master - try twice - EndPoint newMasterEndPoint = GetConfiguredMasterForService(serviceName) - ?? GetConfiguredMasterForService(serviceName) - ?? throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, - $"Sentinel: Failed connecting to switch master for service: {serviceName}"); + // Get new primary - try twice + EndPoint newPrimaryEndPoint = GetConfiguredPrimaryForService(serviceName) + ?? GetConfiguredPrimaryForService(serviceName) + ?? throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, + $"Sentinel: Failed connecting to switch primary for service: {serviceName}"); - connection.currentSentinelMasterEndPoint = newMasterEndPoint; + connection.currentSentinelPrimaryEndPoint = newPrimaryEndPoint; - if (!connection.servers.Contains(newMasterEndPoint)) + if (!connection.servers.Contains(newPrimaryEndPoint)) { EndPoint[] replicaEndPoints = GetReplicasForService(serviceName) ?? GetReplicasForService(serviceName); connection.servers.Clear(); connection.RawConfig.EndPoints.Clear(); - connection.RawConfig.EndPoints.TryAdd(newMasterEndPoint); + connection.RawConfig.EndPoints.TryAdd(newPrimaryEndPoint); foreach (var replicaEndPoint in replicaEndPoints) { connection.RawConfig.EndPoints.TryAdd(replicaEndPoint); } - Trace(string.Format("Switching master to {0}", newMasterEndPoint)); + Trace($"Switching primary to {newPrimaryEndPoint}"); // Trigger a reconfigure connection.ReconfigureAsync(first: false, reconfigureAll: false, logProxy, switchBlame, - string.Format("master switch {0}", serviceName), false, CommandFlags.PreferMaster).Wait(); + $"Primary switch {serviceName}", false, CommandFlags.PreferMaster).Wait(); UpdateSentinelAddressList(serviceName); } @@ -2653,7 +2653,7 @@ public void Dispose() GC.SuppressFinalize(this); Close(!_isDisposed); sentinelConnection?.Dispose(); - var oldTimer = Interlocked.Exchange(ref sentinelMasterReconnectTimer, null); + var oldTimer = Interlocked.Exchange(ref sentinelPrimaryReconnectTimer, null); oldTimer?.Dispose(); } diff --git a/src/StackExchange.Redis/Enums/ClientFlags.cs b/src/StackExchange.Redis/Enums/ClientFlags.cs index 1f400fc71..42baa49ae 100644 --- a/src/StackExchange.Redis/Enums/ClientFlags.cs +++ b/src/StackExchange.Redis/Enums/ClientFlags.cs @@ -106,7 +106,7 @@ public enum ClientFlags : long /// Replica = 2, // as an implementation detail, note that enum.ToString on [Flags] prefers *later* options when naming Flags /// - /// The client is a master. + /// The client is a primary. /// Master = 4, /// diff --git a/src/StackExchange.Redis/Enums/CommandFlags.cs b/src/StackExchange.Redis/Enums/CommandFlags.cs index 9bcc1ffcd..bc93f328e 100644 --- a/src/StackExchange.Redis/Enums/CommandFlags.cs +++ b/src/StackExchange.Redis/Enums/CommandFlags.cs @@ -27,19 +27,19 @@ public enum CommandFlags FireAndForget = 2, /// - /// This operation should be performed on the master if it is available, but read operations may - /// be performed on a replica if no master is available. This is the default option. + /// This operation should be performed on the primary if it is available, but read operations may + /// be performed on a replica if no primary is available. This is the default option. /// PreferMaster = 0, /// - /// This operation should only be performed on the master. + /// This operation should only be performed on the primary. /// DemandMaster = 4, /// /// This operation should be performed on the replica if it is available, but will be performed on - /// a master if no replicas are available. Suitable for read operations only. + /// a primary if no replicas are available. Suitable for read operations only. /// [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(PreferReplica) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] @@ -47,7 +47,7 @@ public enum CommandFlags /// /// This operation should be performed on the replica if it is available, but will be performed on - /// a master if no replicas are available. Suitable for read operations only. + /// a primary if no replicas are available. Suitable for read operations only. /// PreferReplica = 8, // note: we're using a 2-bit set here, which [Flags] formatting hates; position is doing the best we can for reasonable outcomes here diff --git a/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs b/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs index a8b8acbe1..12f84ffba 100644 --- a/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs +++ b/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs @@ -4,7 +4,7 @@ namespace StackExchange.Redis { /// - /// Additional operations to perform when making a server a master. + /// Additional operations to perform when making a server a primary. /// [Flags] [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "Compatibility")] @@ -15,7 +15,7 @@ public enum ReplicationChangeOptions /// None = 0, /// - /// Set the tie-breaker key on all available masters, to specify this server. + /// Set the tie-breaker key on all available primaries, to specify this server. /// SetTiebreaker = 1, /// diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 6a42d02af..0a89bf04a 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -60,7 +60,7 @@ internal static Exception DatabaseRequired(bool includeDetail, RedisCommand comm return ex; } - internal static Exception MasterOnly(bool includeDetail, RedisCommand command, Message message, ServerEndPoint server) + internal static Exception PrimaryOnly(bool includeDetail, RedisCommand command, Message message, ServerEndPoint server) { string s = GetLabel(includeDetail, command, message); var ex = new RedisCommandException("Command cannot be issued to a replica: " + s); diff --git a/src/StackExchange.Redis/GlobalSuppressions.cs b/src/StackExchange.Redis/GlobalSuppressions.cs index 0e0d1c783..3882f4776 100644 --- a/src/StackExchange.Redis/GlobalSuppressions.cs +++ b/src/StackExchange.Redis/GlobalSuppressions.cs @@ -10,7 +10,7 @@ [assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisValue.op_Equality(StackExchange.Redis.RedisValue,StackExchange.Redis.RedisValue)~System.Boolean")] [assembly: SuppressMessage("Style", "IDE0075:Simplify conditional expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisSubscriber.Unsubscribe(StackExchange.Redis.RedisChannel@,System.Action{StackExchange.Redis.RedisChannel,StackExchange.Redis.RedisValue},StackExchange.Redis.ChannelMessageQueue,StackExchange.Redis.CommandFlags)~System.Boolean")] [assembly: SuppressMessage("Roslynator", "RCS1104:Simplify conditional expression.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisSubscriber.Unsubscribe(StackExchange.Redis.RedisChannel@,System.Action{StackExchange.Redis.RedisChannel,StackExchange.Redis.RedisValue},StackExchange.Redis.ChannelMessageQueue,StackExchange.Redis.CommandFlags)~System.Boolean")] -[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Message.IsMasterOnly(StackExchange.Redis.RedisCommand)~System.Boolean")] +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Message.IsPrimaryOnly(StackExchange.Redis.RedisCommand)~System.Boolean")] [assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Message.RequiresDatabase(StackExchange.Redis.RedisCommand)~System.Boolean")] [assembly: SuppressMessage("Style", "IDE0180:Use tuple to swap values", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisDatabase.ReverseLimits(StackExchange.Redis.Order,StackExchange.Redis.Exclude@,StackExchange.Redis.RedisValue@,StackExchange.Redis.RedisValue@)")] [assembly: SuppressMessage("Style", "IDE0180:Use tuple to swap values", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisDatabase.GetSortedSetRangeByScoreMessage(StackExchange.Redis.RedisKey,System.Double,System.Double,StackExchange.Redis.Exclude,StackExchange.Redis.Order,System.Int64,System.Int64,StackExchange.Redis.CommandFlags,System.Boolean)~StackExchange.Redis.Message")] diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 44d89d118..c6980caae 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -562,11 +562,11 @@ public partial interface IServer : IRedis /// /// The REPLICAOF command can change the replication settings of a replica on the fly. - /// If a Redis server is already acting as replica, specifying a null master will turn off the replication, - /// turning the Redis server into a MASTER. Specifying a non-null master will make the server a replica of + /// If a Redis server is already acting as replica, specifying a null primary will turn off the replication, + /// turning the Redis server into a PRIMARY. Specifying a non-null primary will make the server a replica of /// another server listening at the specified hostname and port. /// - /// Endpoint of the new master to replicate from. + /// Endpoint of the new primary to replicate from. /// The command flags to use. /// https://redis.io/commands/replicaof [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicaOfAsync) + " instead, this will be removed in 3.0.")] @@ -575,11 +575,11 @@ public partial interface IServer : IRedis /// /// The REPLICAOF command can change the replication settings of a replica on the fly. - /// If a Redis server is already acting as replica, specifying a null master will turn off the replication, - /// turning the Redis server into a MASTER. Specifying a non-null master will make the server a replica of + /// If a Redis server is already acting as replica, specifying a null primary will turn off the replication, + /// turning the Redis server into a PRIMARY. Specifying a non-null primary will make the server a replica of /// another server listening at the specified hostname and port. /// - /// Endpoint of the new master to replicate from. + /// Endpoint of the new primary to replicate from. /// The command flags to use. /// https://redis.io/commands/replicaof [Obsolete("Please use " + nameof(ReplicaOfAsync) + ", this will be removed in 3.0.")] @@ -587,11 +587,11 @@ public partial interface IServer : IRedis /// /// The REPLICAOF command can change the replication settings of a replica on the fly. - /// If a Redis server is already acting as replica, specifying a null master will turn off the replication, - /// turning the Redis server into a MASTER. Specifying a non-null master will make the server a replica of + /// If a Redis server is already acting as replica, specifying a null primary will turn off the replication, + /// turning the Redis server into a PRIMARY. Specifying a non-null primary will make the server a replica of /// another server listening at the specified hostname and port. /// - /// Endpoint of the new master to replicate from. + /// Endpoint of the new primary to replicate from. /// The command flags to use. /// https://redis.io/commands/replicaof [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicaOfAsync) + " instead, this will be removed in 3.0.")] @@ -600,11 +600,11 @@ public partial interface IServer : IRedis /// /// The REPLICAOF command can change the replication settings of a replica on the fly. - /// If a Redis server is already acting as replica, specifying a null master will turn off the replication, - /// turning the Redis server into a MASTER. Specifying a non-null master will make the server a replica of + /// If a Redis server is already acting as replica, specifying a null primary will turn off the replication, + /// turning the Redis server into a PRIMARY. Specifying a non-null primary will make the server a replica of /// another server listening at the specified hostname and port. /// - /// Endpoint of the new master to replicate from. + /// Endpoint of the new primary to replicate from. /// The command flags to use. /// https://redis.io/commands/replicaof Task ReplicaOfAsync(EndPoint master, CommandFlags flags = CommandFlags.None); @@ -826,137 +826,133 @@ public partial interface IServer : IRedis #region Sentinel /// - /// Returns the ip and port number of the master with that name. - /// If a failover is in progress or terminated successfully for this master it returns the address and port of the promoted replica. + /// Returns the IP and port number of the primary with that name. + /// If a failover is in progress or terminated successfully for this primary it returns the address and port of the promoted replica. /// /// The sentinel service name. /// The command flags to use. - /// the master ip and port + /// The primary IP and port. /// https://redis.io/topics/sentinel EndPoint SentinelGetMasterAddressByName(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Returns the ip and port number of the master with that name. - /// If a failover is in progress or terminated successfully for this master it returns the address and port of the promoted replica. + /// Returns the IP and port number of the primary with that name. + /// If a failover is in progress or terminated successfully for this primary it returns the address and port of the promoted replica. /// /// The sentinel service name. /// The command flags to use. - /// the master ip and port + /// The primary IP and port. /// https://redis.io/topics/sentinel Task SentinelGetMasterAddressByNameAsync(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Returns the ip and port numbers of all known Sentinels - /// for the given service name. + /// Returns the IP and port numbers of all known Sentinels for the given service name. /// - /// the sentinel service name + /// The sentinel service name. /// The command flags to use. - /// a list of the sentinel ips and ports + /// A list of the sentinel IPs and ports. EndPoint[] SentinelGetSentinelAddresses(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Returns the ip and port numbers of all known Sentinels - /// for the given service name. + /// Returns the IP and port numbers of all known Sentinels for the given service name. /// - /// the sentinel service name + /// The sentinel service name. /// The command flags to use. - /// a list of the sentinel ips and ports + /// A list of the sentinel IPs and ports. Task SentinelGetSentinelAddressesAsync(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Returns the ip and port numbers of all known Sentinel replicas - /// for the given service name. + /// Returns the IP and port numbers of all known Sentinel replicas for the given service name. /// - /// the sentinel service name + /// The sentinel service name. /// The command flags to use. - /// a list of the replica ips and ports + /// A list of the replica IPs and ports. EndPoint[] SentinelGetReplicaAddresses(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Returns the ip and port numbers of all known Sentinel replicas - /// for the given service name. + /// Returns the IP and port numbers of all known Sentinel replicas for the given service name. /// - /// the sentinel service name + /// The sentinel service name. /// The command flags to use. - /// a list of the replica ips and ports + /// A list of the replica IPs and ports. Task SentinelGetReplicaAddressesAsync(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Show the state and info of the specified master. + /// Show the state and info of the specified primary. /// /// The sentinel service name. /// The command flags to use. - /// the master state as KeyValuePairs + /// The primaries state as KeyValuePairs. /// https://redis.io/topics/sentinel KeyValuePair[] SentinelMaster(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Show the state and info of the specified master. + /// Show the state and info of the specified primary. /// /// The sentinel service name. /// The command flags to use. - /// the master state as KeyValuePairs + /// The primaries state as KeyValuePairs. /// https://redis.io/topics/sentinel Task[]> SentinelMasterAsync(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Show a list of monitored masters and their state. + /// Show a list of monitored primaries and their state. /// /// The command flags to use. - /// an array of master state KeyValuePair arrays + /// An array of primaries state KeyValuePair arrays. /// https://redis.io/topics/sentinel KeyValuePair[][] SentinelMasters(CommandFlags flags = CommandFlags.None); /// - /// Show a list of monitored masters and their state. + /// Show a list of monitored primaries and their state. /// /// The command flags to use. - /// an array of master state KeyValuePair arrays + /// An array of primaries state KeyValuePair arrays. /// https://redis.io/topics/sentinel Task[][]> SentinelMastersAsync(CommandFlags flags = CommandFlags.None); /// - /// Show a list of replicas for this master, and their state. + /// Show a list of replicas for this primary, and their state. /// /// The sentinel service name. /// The command flags to use. - /// an array of replica state KeyValuePair arrays + /// An array of replica state KeyValuePair arrays. /// https://redis.io/topics/sentinel [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(SentinelReplicas) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] KeyValuePair[][] SentinelSlaves(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Show a list of replicas for this master, and their state. + /// Show a list of replicas for this primary, and their state. /// /// The sentinel service name. /// The command flags to use. - /// an array of replica state KeyValuePair arrays + /// An array of replica state KeyValuePair arrays. /// https://redis.io/topics/sentinel KeyValuePair[][] SentinelReplicas(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Show a list of replicas for this master, and their state. + /// Show a list of replicas for this primary, and their state. /// /// The sentinel service name. /// The command flags to use. - /// an array of replica state KeyValuePair arrays + /// An array of replica state KeyValuePair arrays. /// https://redis.io/topics/sentinel [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(SentinelReplicasAsync) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task[][]> SentinelSlavesAsync(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Show a list of replicas for this master, and their state. + /// Show a list of replicas for this primary, and their state. /// /// The sentinel service name. /// The command flags to use. - /// an array of replica state KeyValuePair arrays + /// An array of replica state KeyValuePair arrays. /// https://redis.io/topics/sentinel Task[][]> SentinelReplicasAsync(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Force a failover as if the master was not reachable, and without asking for agreement to other Sentinels + /// Force a failover as if the primary was not reachable, and without asking for agreement to other Sentinels /// (however a new version of the configuration will be published so that the other Sentinels will update their configurations). /// /// The sentinel service name. @@ -965,7 +961,7 @@ public partial interface IServer : IRedis void SentinelFailover(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Force a failover as if the master was not reachable, and without asking for agreement to other Sentinels + /// Force a failover as if the primary was not reachable, and without asking for agreement to other Sentinels /// (however a new version of the configuration will be published so that the other Sentinels will update their configurations). /// /// The sentinel service name. @@ -974,7 +970,7 @@ public partial interface IServer : IRedis Task SentinelFailoverAsync(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Show a list of sentinels for a master, and their state. + /// Show a list of sentinels for a primary, and their state. /// /// The sentinel service name. /// The command flags to use. @@ -982,7 +978,7 @@ public partial interface IServer : IRedis KeyValuePair[][] SentinelSentinels(string serviceName, CommandFlags flags = CommandFlags.None); /// - /// Show a list of sentinels for a master, and their state. + /// Show a list of sentinels for a primary, and their state. /// /// The sentinel service name. /// The command flags to use. diff --git a/src/StackExchange.Redis/Interfaces/ISubscriber.cs b/src/StackExchange.Redis/Interfaces/ISubscriber.cs index 6755a603a..dd455a6dd 100644 --- a/src/StackExchange.Redis/Interfaces/ISubscriber.cs +++ b/src/StackExchange.Redis/Interfaces/ISubscriber.cs @@ -27,7 +27,7 @@ public interface ISubscriber : IRedis /// Indicates whether the instance can communicate with the server. /// If a channel is specified, the existing subscription map is queried to /// resolve the server responsible for that subscription - otherwise the - /// server is chosen arbitrarily from the masters. + /// server is chosen arbitrarily from the primaries. /// /// The channel to identify the server endpoint by. bool IsConnected(RedisChannel channel = default(RedisChannel)); diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 85c2cff7e..a5c925697 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -59,10 +59,10 @@ internal abstract class Message : ICompletable ScriptUnavailableFlag = (CommandFlags)256, DemandSubscriptionConnection = (CommandFlags)2048; - private const CommandFlags MaskMasterServerPreference = CommandFlags.DemandMaster - | CommandFlags.DemandReplica - | CommandFlags.PreferMaster - | CommandFlags.PreferReplica; + private const CommandFlags MaskPrimaryServerPreference = CommandFlags.DemandMaster + | CommandFlags.DemandReplica + | CommandFlags.PreferMaster + | CommandFlags.PreferReplica; private const CommandFlags UserSelectableFlags = CommandFlags.None | CommandFlags.DemandMaster @@ -106,30 +106,30 @@ protected Message(int db, CommandFlags flags, RedisCommand command) } } - bool masterOnly = IsMasterOnly(command); + bool primaryOnly = IsPrimaryOnly(command); Db = db; this.command = command; Flags = flags & UserSelectableFlags; - if (masterOnly) SetMasterOnly(); + if (primaryOnly) SetPrimaryOnly(); CreatedDateTime = DateTime.UtcNow; CreatedTimestamp = Stopwatch.GetTimestamp(); Status = CommandStatus.WaitingToBeSent; } - internal void SetMasterOnly() + internal void SetPrimaryOnly() { - switch (GetMasterReplicaFlags(Flags)) + switch (GetPrimaryReplicaFlags(Flags)) { case CommandFlags.DemandReplica: - throw ExceptionFactory.MasterOnly(false, command, null, null); + throw ExceptionFactory.PrimaryOnly(false, command, null, null); case CommandFlags.DemandMaster: // already fine as-is break; case CommandFlags.PreferMaster: case CommandFlags.PreferReplica: - default: // we will run this on the master, then - Flags = SetMasterReplicaFlags(Flags, CommandFlags.DemandMaster); + default: // we will run this on the primary, then + Flags = SetPrimaryReplicaFlags(Flags, CommandFlags.DemandMaster); break; } } @@ -296,7 +296,7 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i public static Message CreateInSlot(int db, int slot, CommandFlags flags, RedisCommand command, RedisValue[] values) => new CommandSlotValuesMessage(db, slot, flags, command, values); - public static bool IsMasterOnly(RedisCommand command) + public static bool IsPrimaryOnly(RedisCommand command) { switch (command) { @@ -384,7 +384,7 @@ public static bool IsMasterOnly(RedisCommand command) /// this will already be true for primary-only commands, even if the /// user specified etc. /// - public bool IsMasterOnly() => GetMasterReplicaFlags(Flags) == CommandFlags.DemandMaster; + public bool IsPrimaryOnly() => GetPrimaryReplicaFlags(Flags) == CommandFlags.DemandMaster; public virtual void AppendStormLog(StringBuilder sb) { @@ -490,10 +490,10 @@ internal static Message Create(int db, CommandFlags flags, RedisCommand command, return new CommandKeyValuesKeyMessage(db, flags, command, key0, values, key1); } - internal static CommandFlags GetMasterReplicaFlags(CommandFlags flags) + internal static CommandFlags GetPrimaryReplicaFlags(CommandFlags flags) { // for the purposes of the switch, we only care about two bits - return flags & MaskMasterServerPreference; + return flags & MaskPrimaryServerPreference; } internal static bool RequiresDatabase(RedisCommand command) @@ -543,11 +543,11 @@ internal static bool RequiresDatabase(RedisCommand command) } } - internal static CommandFlags SetMasterReplicaFlags(CommandFlags everything, CommandFlags masterReplica) + internal static CommandFlags SetPrimaryReplicaFlags(CommandFlags everything, CommandFlags primaryReplica) { // take away the two flags we don't want, and add back the ones we care about return (everything & ~(CommandFlags.DemandMaster | CommandFlags.DemandReplica | CommandFlags.PreferMaster | CommandFlags.PreferReplica)) - | masterReplica; + | primaryReplica; } internal void Cancel() => resultBox?.Cancel(); @@ -687,11 +687,11 @@ internal void SetAsking(bool value) internal void SetNoRedirect() => Flags |= CommandFlags.NoRedirect; - internal void SetPreferMaster() => - Flags = (Flags & ~MaskMasterServerPreference) | CommandFlags.PreferMaster; + internal void SetPreferPrimary() => + Flags = (Flags & ~MaskPrimaryServerPreference) | CommandFlags.PreferMaster; internal void SetPreferReplica() => - Flags = (Flags & ~MaskMasterServerPreference) | CommandFlags.PreferReplica; + Flags = (Flags & ~MaskPrimaryServerPreference) | CommandFlags.PreferReplica; /// /// Sets the processor and box for this message to execute. diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 53060d6a4..cbce91c8d 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -436,7 +436,7 @@ internal void OnDisconnected(ConnectionFailureType failureType, PhysicalConnecti physical = null; if (oldState == State.ConnectedEstablished && !ServerEndPoint.IsReplica) { - // if the disconnected endpoint was a master endpoint run info replication + // if the disconnected endpoint was a primary endpoint run info replication // more frequently on it's replica with exponential increments foreach (var r in ServerEndPoint.Replicas) { @@ -1324,12 +1324,12 @@ private void LogNonPreferred(CommandFlags flags, bool isReplica) { if (isReplica) { - if (Message.GetMasterReplicaFlags(flags) == CommandFlags.PreferMaster) + if (Message.GetPrimaryReplicaFlags(flags) == CommandFlags.PreferMaster) Interlocked.Increment(ref nonPreferredEndpointCount); } else { - if (Message.GetMasterReplicaFlags(flags) == CommandFlags.PreferReplica) + if (Message.GetPrimaryReplicaFlags(flags) == CommandFlags.PreferReplica) Interlocked.Increment(ref nonPreferredEndpointCount); } } @@ -1368,11 +1368,11 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne { var cmd = message.Command; LastCommand = cmd; - bool isMasterOnly = message.IsMasterOnly(); + bool isPrimaryOnly = message.IsPrimaryOnly(); - if (isMasterOnly && !ServerEndPoint.SupportsPrimaryWrites) + if (isPrimaryOnly && !ServerEndPoint.SupportsPrimaryWrites) { - throw ExceptionFactory.MasterOnly(Multiplexer.IncludeDetailInExceptions, message.Command, message, ServerEndPoint); + throw ExceptionFactory.PrimaryOnly(Multiplexer.IncludeDetailInExceptions, message.Command, message, ServerEndPoint); } switch(cmd) { @@ -1393,7 +1393,7 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne // we run it as Fire and Forget. if (cmd != RedisCommand.AUTH) { - var readmode = connection.GetReadModeCommand(isMasterOnly); + var readmode = connection.GetReadModeCommand(isPrimaryOnly); if (readmode != null) { connection.EnqueueInsideWriteLock(readmode); diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 8bae13482..2187515e3 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -536,11 +536,11 @@ internal void GetCounters(ConnectionCounters counters) counters.Subscriptions = SubscriptionCount; } - internal Message GetReadModeCommand(bool isMasterOnly) + internal Message GetReadModeCommand(bool isPrimaryOnly) { if (BridgeCouldBeNull?.ServerEndPoint?.RequiresReadMode == true) { - ReadMode requiredReadMode = isMasterOnly ? ReadMode.ReadWrite : ReadMode.ReadOnly; + ReadMode requiredReadMode = isPrimaryOnly ? ReadMode.ReadWrite : ReadMode.ReadOnly; if (requiredReadMode != currentReadMode) { currentReadMode = requiredReadMode; diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index b08c1768d..b951bef0a 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -517,8 +517,8 @@ public long HyperLogLogLength(RedisKey key, CommandFlags flags = CommandFlags.No { var features = GetFeatures(key, flags, out ServerEndPoint server); var cmd = Message.Create(Database, flags, RedisCommand.PFCOUNT, key); - // technically a write / master-only command until 2.8.18 - if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetMasterOnly(); + // technically a write / primary-only command until 2.8.18 + if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetPrimaryOnly(); return ExecuteSync(cmd, ResultProcessor.Int64, server); } @@ -530,8 +530,8 @@ public long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags if (keys.Length != 0) { var features = GetFeatures(keys[0], flags, out server); - // technically a write / master-only command until 2.8.18 - if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetMasterOnly(); + // technically a write / primary-only command until 2.8.18 + if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetPrimaryOnly(); } return ExecuteSync(cmd, ResultProcessor.Int64, server); } @@ -540,8 +540,8 @@ public Task HyperLogLogLengthAsync(RedisKey key, CommandFlags flags = Comm { var features = GetFeatures(key, flags, out ServerEndPoint server); var cmd = Message.Create(Database, flags, RedisCommand.PFCOUNT, key); - // technically a write / master-only command until 2.8.18 - if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetMasterOnly(); + // technically a write / primary-only command until 2.8.18 + if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetPrimaryOnly(); return ExecuteAsync(cmd, ResultProcessor.Int64, server); } @@ -553,8 +553,8 @@ public Task HyperLogLogLengthAsync(RedisKey[] keys, CommandFlags flags = C if (keys.Length != 0) { var features = GetFeatures(keys[0], flags, out server); - // technically a write / master-only command until 2.8.18 - if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetMasterOnly(); + // technically a write / primary-only command until 2.8.18 + if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetPrimaryOnly(); } return ExecuteAsync(cmd, ResultProcessor.Int64, server); } @@ -3012,12 +3012,12 @@ private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long } if (destination.IsNull) return Message.Create(Database, flags, RedisCommand.SORT, key, values.ToArray()); - // because we are using STORE, we need to push this to a master - if (Message.GetMasterReplicaFlags(flags) == CommandFlags.DemandReplica) + // Because we are using STORE, we need to push this to a primary + if (Message.GetPrimaryReplicaFlags(flags) == CommandFlags.DemandReplica) { - throw ExceptionFactory.MasterOnly(multiplexer.IncludeDetailInExceptions, RedisCommand.SORT, null, null); + throw ExceptionFactory.PrimaryOnly(multiplexer.IncludeDetailInExceptions, RedisCommand.SORT, null, null); } - flags = Message.SetMasterReplicaFlags(flags, CommandFlags.DemandMaster); + flags = Message.SetPrimaryReplicaFlags(flags, CommandFlags.DemandMaster); values.Add(RedisLiterals.STORE); return Message.Create(Database, flags, RedisCommand.SORT, key, values.ToArray(), destination); } diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 1c5547a47..46b6c54ec 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -554,24 +554,24 @@ public Task TimeAsync(CommandFlags flags = CommandFlags.None) return ExecuteAsync(msg, ResultProcessor.DateTime); } - internal static Message CreateReplicaOfMessage(ServerEndPoint sendMessageTo, EndPoint masterEndpoint, CommandFlags flags = CommandFlags.None) + internal static Message CreateReplicaOfMessage(ServerEndPoint sendMessageTo, EndPoint primaryEndpoint, CommandFlags flags = CommandFlags.None) { RedisValue host, port; - if (masterEndpoint == null) + if (primaryEndpoint == null) { host = "NO"; port = "ONE"; } else { - if (Format.TryGetHostPort(masterEndpoint, out string hostRaw, out int portRaw)) + if (Format.TryGetHostPort(primaryEndpoint, out string hostRaw, out int portRaw)) { host = hostRaw; port = portRaw; } else { - throw new NotSupportedException("Unknown endpoint type: " + masterEndpoint.GetType().Name); + throw new NotSupportedException("Unknown endpoint type: " + primaryEndpoint.GetType().Name); } } return Message.Create(-1, flags, sendMessageTo.GetFeatures().ReplicaCommands ? RedisCommand.REPLICAOF : RedisCommand.SLAVEOF, host, port); @@ -688,8 +688,8 @@ public async Task ReplicaOfAsync(EndPoint master, CommandFlags flags = CommandFl throw new ArgumentException("Cannot replicate to self"); } - // attempt to cease having an opinion on the master; will resume that when replication completes - // (note that this may fail; we aren't depending on it) + // Attempt to cease having an opinion on the primary - will resume that when replication completes + // (note that this may fail - we aren't depending on it) if (GetTiebreakerRemovalMessage() is Message tieBreakerRemoval && !server.IsReplica) { try @@ -714,13 +714,13 @@ private static void FixFlags(Message message, ServerEndPoint server) // since the server is specified explicitly, we don't want defaults // to make the "non-preferred-endpoint" counters look artificially // inflated; note we only change *prefer* options - switch (Message.GetMasterReplicaFlags(message.Flags)) + switch (Message.GetPrimaryReplicaFlags(message.Flags)) { case CommandFlags.PreferMaster: if (server.IsReplica) message.SetPreferReplica(); break; case CommandFlags.PreferReplica: - if (!server.IsReplica) message.SetPreferMaster(); + if (!server.IsReplica) message.SetPreferPrimary(); break; } } @@ -853,13 +853,13 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes public EndPoint SentinelGetMasterAddressByName(string serviceName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.GETMASTERADDRBYNAME, (RedisValue)serviceName); - return ExecuteSync(msg, ResultProcessor.SentinelMasterEndpoint); + return ExecuteSync(msg, ResultProcessor.SentinelPrimaryEndpoint); } public Task SentinelGetMasterAddressByNameAsync(string serviceName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.GETMASTERADDRBYNAME, (RedisValue)serviceName); - return ExecuteAsync(msg, ResultProcessor.SentinelMasterEndpoint); + return ExecuteAsync(msg, ResultProcessor.SentinelPrimaryEndpoint); } public EndPoint[] SentinelGetSentinelAddresses(string serviceName, CommandFlags flags = CommandFlags.None) diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 7ebf2935c..933cf3b31 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -129,7 +129,7 @@ public static readonly ResultProcessor #region Sentinel public static readonly ResultProcessor - SentinelMasterEndpoint = new SentinelGetMasterAddressByNameProcessor(); + SentinelPrimaryEndpoint = new SentinelGetPrimaryAddressByNameProcessor(); public static readonly ResultProcessor SentinelAddressesEndPoints = new SentinelGetSentinelAddressesProcessor(); @@ -681,7 +681,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes SetResult(message, true); return true; } - string masterHost = null, masterPort = null; + string primaryHost = null, primaryPort = null; bool roleSeen = false; using (var reader = new StringReader(info)) { @@ -697,7 +697,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { case "master": server.IsReplica = false; - Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (INFO) role: master"); + Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (INFO) role: primary"); break; case "replica": case "slave": @@ -708,11 +708,11 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } else if ((val = Extract(line, "master_host:")) != null) { - masterHost = val; + primaryHost = val; } else if ((val = Extract(line, "master_port:")) != null) { - masterPort = val; + primaryPort = val; } else if ((val = Extract(line, "redis_version:")) != null) { @@ -748,7 +748,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (roleSeen) { // These are in the same section, if present - server.MasterEndPoint = Format.TryParseEndPoint(masterHost, masterPort); + server.PrimaryEndPoint = Format.TryParseEndPoint(primaryHost, primaryPort); } } } @@ -1409,7 +1409,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes ref var val = ref items[0]; Role role; - if (val.IsEqual(RedisLiterals.master)) role = ParseMaster(items); + if (val.IsEqual(RedisLiterals.master)) role = ParsePrimary(items); else if (val.IsEqual(RedisLiterals.slave)) role = ParseReplica(items, RedisLiterals.slave); else if (val.IsEqual(RedisLiterals.replica)) role = ParseReplica(items, RedisLiterals.replica); // for when "slave" is deprecated else if (val.IsEqual(RedisLiterals.sentinel)) role = ParseSentinel(items); @@ -1420,7 +1420,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return true; } - private static Role ParseMaster(in Sequence items) + private static Role ParsePrimary(in Sequence items) { if (items.Length < 3) { @@ -1443,7 +1443,7 @@ private static Role ParseMaster(in Sequence items) replicas = new List((int)replicaItems.Length); for (int i = 0; i < replicaItems.Length; i++) { - if (TryParseMasterReplica(replicaItems[i].GetItems(), out var replica)) + if (TryParsePrimaryReplica(replicaItems[i].GetItems(), out var replica)) { replicas.Add(replica); } @@ -1457,7 +1457,7 @@ private static Role ParseMaster(in Sequence items) return new Role.Master(offset, replicas); } - private static bool TryParseMasterReplica(in Sequence items, out Role.Master.Replica replica) + private static bool TryParsePrimaryReplica(in Sequence items, out Role.Master.Replica replica) { if (items.Length < 3) { @@ -1465,9 +1465,9 @@ private static bool TryParseMasterReplica(in Sequence items, out Role return false; } - var masterIp = items[0].GetString(); + var primaryIp = items[0].GetString(); - if (!items[1].TryGetInt64(out var masterPort) || masterPort > int.MaxValue) + if (!items[1].TryGetInt64(out var primaryPort) || primaryPort > int.MaxValue) { replica = default; return false; @@ -1479,7 +1479,7 @@ private static bool TryParseMasterReplica(in Sequence items, out Role return false; } - replica = new Role.Master.Replica(masterIp, (int)masterPort, replicationOffset); + replica = new Role.Master.Replica(primaryIp, (int)primaryPort, replicationOffset); return true; } @@ -1490,9 +1490,9 @@ private static Role ParseReplica(in Sequence items, string role) return null; } - var masterIp = items[1].GetString(); + var primaryIp = items[1].GetString(); - if (!items[2].TryGetInt64(out var masterPort) || masterPort > int.MaxValue) + if (!items[2].TryGetInt64(out var primaryPort) || primaryPort > int.MaxValue) { return null; } @@ -1512,7 +1512,7 @@ private static Role ParseReplica(in Sequence items, string role) return null; } - return new Role.Replica(role, masterIp, (int)masterPort, replicationState, replicationOffset); + return new Role.Replica(role, primaryIp, (int)primaryPort, replicationState, replicationOffset); } private static Role ParseSentinel(in Sequence items) @@ -1521,8 +1521,8 @@ private static Role ParseSentinel(in Sequence items) { return null; } - var masters = items[1].GetItemsAsStrings(); - return new Role.Sentinel(masters); + var primaries = items[1].GetItemsAsStrings(); + return new Role.Sentinel(primaries); } } @@ -2219,7 +2219,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes #region Sentinel - private sealed class SentinelGetMasterAddressByNameProcessor : ResultProcessor + private sealed class SentinelGetPrimaryAddressByNameProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { @@ -2271,7 +2271,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return true; case ResultType.SimpleString: - //We don't want to blow up if the master is not found + // We don't want to blow up if the primary is not found if (result.IsNull) return true; break; @@ -2304,7 +2304,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes break; case ResultType.SimpleString: - //We don't want to blow up if the master is not found + // We don't want to blow up if the primary is not found if (result.IsNull) return true; break; diff --git a/src/StackExchange.Redis/Role.cs b/src/StackExchange.Redis/Role.cs index 293f7281e..c2b1650c4 100644 --- a/src/StackExchange.Redis/Role.cs +++ b/src/StackExchange.Redis/Role.cs @@ -19,7 +19,7 @@ public abstract class Role private Role(string role) => Value = role; /// - /// Result of the ROLE command for a master node. + /// Result of the ROLE command for a primary node. /// /// https://redis.io/commands/role#master-output public sealed class Master : Role @@ -76,12 +76,12 @@ internal Master(long offset, ICollection replicas) : base(RedisLiterals public sealed class Replica : Role { /// - /// The IP address of the master node for this replica. + /// The IP address of the primary node for this replica. /// public string MasterIp { get; } /// - /// The port number of the master node for this replica. + /// The port number of the primary node for this replica. /// public int MasterPort { get; } @@ -111,13 +111,13 @@ internal Replica(string role, string ip, int port, string state, long offset) : public sealed class Sentinel : Role { /// - /// Master names monitored by this sentinel node. + /// Primary names monitored by this sentinel node. /// public ICollection MonitoredMasters { get; } - internal Sentinel(ICollection masters) : base(RedisLiterals.sentinel) + internal Sentinel(ICollection primaries) : base(RedisLiterals.sentinel) { - MonitoredMasters = masters; + MonitoredMasters = primaries; } } diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 6c7c14afa..0c86bf3ba 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -16,14 +16,14 @@ namespace StackExchange.Redis internal enum UnselectableFlags { None = 0, - RedundantMaster = 1, + RedundantPrimary = 1, DidNotRespond = 2, ServerType = 4, } internal sealed partial class ServerEndPoint : IDisposable { - internal volatile ServerEndPoint Master; + internal volatile ServerEndPoint Primary; internal volatile ServerEndPoint[] Replicas = Array.Empty(); private static readonly Regex nameSanitizer = new Regex("[^!-~]", RegexOptions.Compiled); @@ -288,19 +288,19 @@ public void UpdateNodeRelations(ClusterConfiguration configuration) { Multiplexer.Trace($"Updating node relations for {Format.ToString(thisNode.EndPoint)}..."); List replicas = null; - ServerEndPoint master = null; + ServerEndPoint primary = null; foreach (var node in configuration.Nodes) { if (node.NodeId == thisNode.ParentNodeId) { - master = Multiplexer.GetServerEndPoint(node.EndPoint); + primary = Multiplexer.GetServerEndPoint(node.EndPoint); } else if (node.ParentNodeId == thisNode.NodeId) { (replicas ??= new List()).Add(Multiplexer.GetServerEndPoint(node.EndPoint)); } } - Master = master; + Primary = primary; Replicas = replicas?.ToArray() ?? Array.Empty(); } } @@ -677,11 +677,11 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) internal int LastInfoReplicationCheckSecondsAgo => unchecked(Environment.TickCount - Thread.VolatileRead(ref lastInfoReplicationCheckTicks)) / 1000; - private EndPoint masterEndPoint; - public EndPoint MasterEndPoint + private EndPoint primaryEndPoint; + public EndPoint PrimaryEndPoint { - get => masterEndPoint; - set => SetConfig(ref masterEndPoint, value); + get => primaryEndPoint; + set => SetConfig(ref primaryEndPoint, value); } /// @@ -735,7 +735,6 @@ private void ResetExponentiallyReplicationCheck() private int _heartBeatActive; internal void OnHeartbeat() { - // don't overlap operations on an endpoint // Don't overlap heartbeat operations on an endpoint if (Interlocked.CompareExchange(ref _heartBeatActive, 1, 0) == 0) { @@ -811,7 +810,7 @@ internal Task SendTracerAsync(LogProxy log = null) internal string Summary() { var sb = new StringBuilder(Format.ToString(EndPoint)) - .Append(": ").Append(serverType).Append(" v").Append(version).Append(", ").Append(isReplica ? "replica" : "master"); + .Append(": ").Append(serverType).Append(" v").Append(version).Append(", ").Append(isReplica ? "replica" : "primary"); if (databases > 0) sb.Append("; ").Append(databases).Append(" databases"); if (writeEverySeconds > 0) diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index 70e5cb294..85babc63e 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -149,11 +149,11 @@ public bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isM message.SetNoRedirect(); // once is enough if (isMoved) message.SetInternalCall(); - // note that everything so far is talking about MASTER nodes; we might be - // wanting a REPLICA, so we'll check + // Note that everything so far is talking about PRIMARY nodes + // We might be wanting a REPLICA, so we'll check ServerEndPoint resendVia = null; var command = message.Command; - switch (Message.GetMasterReplicaFlags(message.Flags)) + switch (Message.GetPrimaryReplicaFlags(message.Flags)) { case CommandFlags.DemandMaster: resendVia = server.IsSelectable(command, isMoved) ? server : null; @@ -247,14 +247,14 @@ private static unsafe int IndexOf(byte* ptr, byte value, int start, int end) private ServerEndPoint Any(RedisCommand command, CommandFlags flags, bool allowDisconnected) => multiplexer.AnyServer(ServerType, (uint)Interlocked.Increment(ref anyStartOffset), command, flags, allowDisconnected); - private static ServerEndPoint FindMaster(ServerEndPoint endpoint, RedisCommand command) + private static ServerEndPoint FindPrimary(ServerEndPoint endpoint, RedisCommand command) { int max = 5; do { if (!endpoint.IsReplica && endpoint.IsSelectable(command)) return endpoint; - endpoint = endpoint.Master; + endpoint = endpoint.Primary; } while (endpoint != null && --max != 0); return null; } @@ -290,13 +290,14 @@ private ServerEndPoint[] MapForMutation() private ServerEndPoint Select(int slot, RedisCommand command, CommandFlags flags, bool allowDisconnected) { - flags = Message.GetMasterReplicaFlags(flags); // only interested in master/replica preferences + // Only interested in primary/replica preferences + flags = Message.GetPrimaryReplicaFlags(flags); ServerEndPoint[] arr; if (slot == NoSlot || (arr = map) == null) return Any(command, flags, allowDisconnected); ServerEndPoint endpoint = arr[slot], testing; - // but: ^^^ is the MASTER slots; if we want a replica, we need to do some thinking + // but: ^^^ is the PRIMARY slots; if we want a replica, we need to do some thinking if (endpoint != null) { @@ -309,9 +310,9 @@ private ServerEndPoint Select(int slot, RedisCommand command, CommandFlags flags if (testing != null) return testing; break; case CommandFlags.DemandMaster: - return FindMaster(endpoint, command) ?? Any(command, flags, allowDisconnected); + return FindPrimary(endpoint, command) ?? Any(command, flags, allowDisconnected); case CommandFlags.PreferMaster: - testing = FindMaster(endpoint, command); + testing = FindPrimary(endpoint, command); if (testing != null) return testing; break; } diff --git a/tests/RedisConfigs/Basic/master-6379.conf b/tests/RedisConfigs/Basic/primary-6379.conf similarity index 80% rename from tests/RedisConfigs/Basic/master-6379.conf rename to tests/RedisConfigs/Basic/primary-6379.conf index 4ea261c1f..1f4d96da5 100644 --- a/tests/RedisConfigs/Basic/master-6379.conf +++ b/tests/RedisConfigs/Basic/primary-6379.conf @@ -5,5 +5,5 @@ databases 2000 maxmemory 6gb dir "../Temp" appendonly no -dbfilename "master-6379.rdb" +dbfilename "primary-6379.rdb" save "" \ No newline at end of file diff --git a/tests/RedisConfigs/Docker/supervisord.conf b/tests/RedisConfigs/Docker/supervisord.conf index e29b937e8..b828ead92 100644 --- a/tests/RedisConfigs/Docker/supervisord.conf +++ b/tests/RedisConfigs/Docker/supervisord.conf @@ -1,8 +1,8 @@ [supervisord] nodaemon=false -[program:master-6379] -command=/usr/local/bin/redis-server /data/Basic/master-6379.conf +[program:primary-6379] +command=/usr/local/bin/redis-server /data/Basic/primary-6379.conf directory=/data/Basic stdout_logfile=/var/log/supervisor/%(program_name)s.log stderr_logfile=/var/log/supervisor/%(program_name)s.log @@ -22,8 +22,8 @@ stdout_logfile=/var/log/supervisor/%(program_name)s.log stderr_logfile=/var/log/supervisor/%(program_name)s.log autorestart=true -[program:master-6382] -command=/usr/local/bin/redis-server /data/Failover/master-6382.conf +[program:primary-6382] +command=/usr/local/bin/redis-server /data/Failover/primary-6382.conf directory=/data/Failover stdout_logfile=/var/log/supervisor/%(program_name)s.log stderr_logfile=/var/log/supervisor/%(program_name)s.log diff --git a/tests/RedisConfigs/Failover/master-6382.conf b/tests/RedisConfigs/Failover/primary-6382.conf similarity index 80% rename from tests/RedisConfigs/Failover/master-6382.conf rename to tests/RedisConfigs/Failover/primary-6382.conf index e57c55190..c19e8c701 100644 --- a/tests/RedisConfigs/Failover/master-6382.conf +++ b/tests/RedisConfigs/Failover/primary-6382.conf @@ -5,5 +5,5 @@ databases 2000 maxmemory 2gb dir "../Temp" appendonly no -dbfilename "master-6382.rdb" +dbfilename "primary-6382.rdb" save "" \ No newline at end of file diff --git a/tests/RedisConfigs/Sentinel/sentinel-26379.conf b/tests/RedisConfigs/Sentinel/sentinel-26379.conf index 6d10f6030..27cefe69a 100644 --- a/tests/RedisConfigs/Sentinel/sentinel-26379.conf +++ b/tests/RedisConfigs/Sentinel/sentinel-26379.conf @@ -1,6 +1,6 @@ port 26379 -sentinel monitor mymaster 127.0.0.1 7010 1 -sentinel down-after-milliseconds mymaster 1000 -sentinel failover-timeout mymaster 1000 -sentinel config-epoch mymaster 0 +sentinel monitor myprimary 127.0.0.1 7010 1 +sentinel down-after-milliseconds myprimary 1000 +sentinel failover-timeout myprimary 1000 +sentinel config-epoch myprimary 0 dir "../Temp" \ No newline at end of file diff --git a/tests/RedisConfigs/Sentinel/sentinel-26380.conf b/tests/RedisConfigs/Sentinel/sentinel-26380.conf index fa044227e..b01a4d080 100644 --- a/tests/RedisConfigs/Sentinel/sentinel-26380.conf +++ b/tests/RedisConfigs/Sentinel/sentinel-26380.conf @@ -1,6 +1,6 @@ port 26380 -sentinel monitor mymaster 127.0.0.1 7010 1 -sentinel down-after-milliseconds mymaster 1000 -sentinel failover-timeout mymaster 1000 -sentinel config-epoch mymaster 0 +sentinel monitor myprimary 127.0.0.1 7010 1 +sentinel down-after-milliseconds myprimary 1000 +sentinel failover-timeout myprimary 1000 +sentinel config-epoch myprimary 0 dir "../Temp" \ No newline at end of file diff --git a/tests/RedisConfigs/Sentinel/sentinel-26381.conf b/tests/RedisConfigs/Sentinel/sentinel-26381.conf index fa49c9e14..ee8022a5a 100644 --- a/tests/RedisConfigs/Sentinel/sentinel-26381.conf +++ b/tests/RedisConfigs/Sentinel/sentinel-26381.conf @@ -1,6 +1,6 @@ port 26381 -sentinel monitor mymaster 127.0.0.1 7010 1 -sentinel down-after-milliseconds mymaster 1000 -sentinel failover-timeout mymaster 1000 -sentinel config-epoch mymaster 0 +sentinel monitor myprimary 127.0.0.1 7010 1 +sentinel down-after-milliseconds myprimary 1000 +sentinel failover-timeout myprimary 1000 +sentinel config-epoch myprimary 0 dir "../Temp" diff --git a/tests/RedisConfigs/start-all.sh b/tests/RedisConfigs/start-all.sh index 40773a578..3f0a5c910 100644 --- a/tests/RedisConfigs/start-all.sh +++ b/tests/RedisConfigs/start-all.sh @@ -4,8 +4,8 @@ echo "Starting Redis servers for testing..." #Basic Servers echo "Starting Basic: 6379-6382" pushd Basic > /dev/null -echo "${INDENT}Master: 6379" -redis-server master-6379.conf &>/dev/null & +echo "${INDENT}Primary: 6379" +redis-server primary-6379.conf &>/dev/null & echo "${INDENT}Replica: 6380" redis-server replica-6380.conf &>/dev/null & echo "${INDENT}Secure: 6381" @@ -15,8 +15,8 @@ popd > /dev/null #Failover Servers echo Starting Failover: 6382-6383 pushd Failover > /dev/null -echo "${INDENT}Master: 6382" -redis-server master-6382.conf &>/dev/null & +echo "${INDENT}Primary: 6382" +redis-server primary-6382.conf &>/dev/null & echo "${INDENT}Replica: 6383" redis-server replica-6383.conf &>/dev/null & popd > /dev/null diff --git a/tests/RedisConfigs/start-basic.cmd b/tests/RedisConfigs/start-basic.cmd index 558499eb5..586d38c33 100644 --- a/tests/RedisConfigs/start-basic.cmd +++ b/tests/RedisConfigs/start-basic.cmd @@ -1,8 +1,8 @@ @echo off echo Starting Basic: pushd %~dp0\Basic -echo Master: 6379 -@start "Redis (Master): 6379" /min ..\3.0.503\redis-server.exe master-6379.conf +echo Primary: 6379 +@start "Redis (Primary): 6379" /min ..\3.0.503\redis-server.exe primary-6379.conf echo Replica: 6380 @start "Redis (Replica): 6380" /min ..\3.0.503\redis-server.exe replica-6380.conf echo Secure: 6381 diff --git a/tests/RedisConfigs/start-basic.sh b/tests/RedisConfigs/start-basic.sh index cd76034c1..35a98a335 100644 --- a/tests/RedisConfigs/start-basic.sh +++ b/tests/RedisConfigs/start-basic.sh @@ -4,8 +4,8 @@ echo "Starting Redis servers for testing..." #Basic Servers echo "Starting Basic: 6379-6382" pushd Basic > /dev/null -echo "${INDENT}Master: 6379" -redis-server master-6379.conf &>/dev/null & +echo "${INDENT}Primary: 6379" +redis-server primary-6379.conf &>/dev/null & echo "${INDENT}Replica: 6380" redis-server replica-6380.conf &>/dev/null & echo "${INDENT}Secure: 6381" diff --git a/tests/RedisConfigs/start-failover.cmd b/tests/RedisConfigs/start-failover.cmd index 513d1b337..e696bdfa3 100644 --- a/tests/RedisConfigs/start-failover.cmd +++ b/tests/RedisConfigs/start-failover.cmd @@ -2,7 +2,7 @@ echo Starting Failover: pushd %~dp0\Failover echo Master: 6382 -@start "Redis (Failover Master): 6382" /min ..\3.0.503\redis-server.exe master-6382.conf +@start "Redis (Failover Master): 6382" /min ..\3.0.503\redis-server.exe primary-6382.conf echo Replica: 6383 @start "Redis (Failover Replica): 6383" /min ..\3.0.503\redis-server.exe replica-6383.conf popd \ No newline at end of file diff --git a/tests/StackExchange.Redis.Tests/AsyncTests.cs b/tests/StackExchange.Redis.Tests/AsyncTests.cs index 1ea26e76e..4d49e7d5b 100644 --- a/tests/StackExchange.Redis.Tests/AsyncTests.cs +++ b/tests/StackExchange.Redis.Tests/AsyncTests.cs @@ -12,7 +12,7 @@ public class AsyncTests : TestBase { public AsyncTests(ITestOutputHelper output) : base(output) { } - protected override string GetConfiguration() => TestConfig.Current.MasterServerAndPort; + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; [Fact] public void AsyncTasksReportFailureIfServerUnavailable() @@ -21,7 +21,7 @@ public void AsyncTasksReportFailureIfServerUnavailable() using (var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast)) { - var server = conn.GetServer(TestConfig.Current.MasterServer, TestConfig.Current.MasterPort); + var server = conn.GetServer(TestConfig.Current.PrimaryServer, TestConfig.Current.PrimaryPort); RedisKey key = Me(); var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/BacklogTests.cs b/tests/StackExchange.Redis.Tests/BacklogTests.cs index 021eb8267..dc64144e0 100644 --- a/tests/StackExchange.Redis.Tests/BacklogTests.cs +++ b/tests/StackExchange.Redis.Tests/BacklogTests.cs @@ -10,7 +10,7 @@ public class BacklogTests : TestBase { public BacklogTests(ITestOutputHelper output) : base (output) { } - protected override string GetConfiguration() => TestConfig.Current.MasterServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; [Fact] public async Task FailFast() @@ -48,7 +48,7 @@ void PrintSnapshot(ConnectionMultiplexer muxer) AsyncTimeout = 5000, AllowAdmin = true, }; - options.EndPoints.Add(TestConfig.Current.MasterServerAndPort); + options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); @@ -121,7 +121,7 @@ public async Task QueuesAndFlushesAfterReconnectingAsync() AllowAdmin = true, SocketManager = SocketManager.ThreadPool, }; - options.EndPoints.Add(TestConfig.Current.MasterServerAndPort); + options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); muxer.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); @@ -213,7 +213,7 @@ public async Task QueuesAndFlushesAfterReconnecting() AllowAdmin = true, SocketManager = SocketManager.ThreadPool, }; - options.EndPoints.Add(TestConfig.Current.MasterServerAndPort); + options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); muxer.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); diff --git a/tests/StackExchange.Redis.Tests/Cluster.cs b/tests/StackExchange.Redis.Tests/Cluster.cs index 51282db1b..e1069d7f8 100644 --- a/tests/StackExchange.Redis.Tests/Cluster.cs +++ b/tests/StackExchange.Redis.Tests/Cluster.cs @@ -87,7 +87,7 @@ public void Connect() } Assert.Equal(TestConfig.Current.ClusterServerCount, endpoints.Length); - int masters = 0, replicas = 0; + int primaries = 0, replicas = 0; var failed = new List(); foreach (var endpoint in endpoints) { @@ -109,7 +109,7 @@ public void Connect() Assert.Equal(ServerType.Cluster, server.ServerType); if (server.IsReplica) replicas++; - else masters++; + else primaries++; } if (failed.Count != 0) { @@ -122,7 +122,7 @@ public void Connect() } Assert.Equal(TestConfig.Current.ClusterServerCount / 2, replicas); - Assert.Equal(TestConfig.Current.ClusterServerCount / 2, masters); + Assert.Equal(TestConfig.Current.ClusterServerCount / 2, primaries); } } @@ -157,39 +157,39 @@ static string StringGet(IServer server, RedisKey key, CommandFlags flags = Comma var config = servers[0].ClusterConfiguration; Assert.NotNull(config); int slot = conn.HashSlot(key); - var rightMasterNode = config.GetBySlot(key); - Assert.NotNull(rightMasterNode); - Log("Right Master: {0} {1}", rightMasterNode.EndPoint, rightMasterNode.NodeId); + var rightPrimaryNode = config.GetBySlot(key); + Assert.NotNull(rightPrimaryNode); + Log("Right Primary: {0} {1}", rightPrimaryNode.EndPoint, rightPrimaryNode.NodeId); - string a = StringGet(conn.GetServer(rightMasterNode.EndPoint), key); - Assert.Equal(value, a); // right master + string a = StringGet(conn.GetServer(rightPrimaryNode.EndPoint), key); + Assert.Equal(value, a); // right primary - var node = config.Nodes.FirstOrDefault(x => !x.IsReplica && x.NodeId != rightMasterNode.NodeId); + var node = config.Nodes.FirstOrDefault(x => !x.IsReplica && x.NodeId != rightPrimaryNode.NodeId); Assert.NotNull(node); - Log("Using Master: {0}", node.EndPoint, node.NodeId); + Log("Using Primary: {0}", node.EndPoint, node.NodeId); { string b = StringGet(conn.GetServer(node.EndPoint), key); - Assert.Equal(value, b); // wrong master, allow redirect + Assert.Equal(value, b); // wrong primary, allow redirect var ex = Assert.Throws(() => StringGet(conn.GetServer(node.EndPoint), key, CommandFlags.NoRedirect)); - Assert.StartsWith($"Key has MOVED to Endpoint {rightMasterNode.EndPoint} and hashslot {slot}", ex.Message); + Assert.StartsWith($"Key has MOVED to Endpoint {rightPrimaryNode.EndPoint} and hashslot {slot}", ex.Message); } - node = config.Nodes.FirstOrDefault(x => x.IsReplica && x.ParentNodeId == rightMasterNode.NodeId); + node = config.Nodes.FirstOrDefault(x => x.IsReplica && x.ParentNodeId == rightPrimaryNode.NodeId); Assert.NotNull(node); { string d = StringGet(conn.GetServer(node.EndPoint), key); Assert.Equal(value, d); // right replica } - node = config.Nodes.FirstOrDefault(x => x.IsReplica && x.ParentNodeId != rightMasterNode.NodeId); + node = config.Nodes.FirstOrDefault(x => x.IsReplica && x.ParentNodeId != rightPrimaryNode.NodeId); Assert.NotNull(node); { string e = StringGet(conn.GetServer(node.EndPoint), key); Assert.Equal(value, e); // wrong replica, allow redirect var ex = Assert.Throws(() => StringGet(conn.GetServer(node.EndPoint), key, CommandFlags.NoRedirect)); - Assert.StartsWith($"Key has MOVED to Endpoint {rightMasterNode.EndPoint} and hashslot {slot}", ex.Message); + Assert.StartsWith($"Key has MOVED to Endpoint {rightPrimaryNode.EndPoint} and hashslot {slot}", ex.Message); } } } @@ -669,41 +669,41 @@ public void MovedProfiling() Assert.NotNull(config); //int slot = conn.HashSlot(Key); - var rightMasterNode = config.GetBySlot(Key); - Assert.NotNull(rightMasterNode); + var rightPrimaryNode = config.GetBySlot(Key); + Assert.NotNull(rightPrimaryNode); - string a = (string)conn.GetServer(rightMasterNode.EndPoint).Execute("GET", Key); - Assert.Equal(Value, a); // right master + string a = (string)conn.GetServer(rightPrimaryNode.EndPoint).Execute("GET", Key); + Assert.Equal(Value, a); // right primary - var wrongMasterNode = config.Nodes.FirstOrDefault(x => !x.IsReplica && x.NodeId != rightMasterNode.NodeId); - Assert.NotNull(wrongMasterNode); + var wrongPrimaryNode = config.Nodes.FirstOrDefault(x => !x.IsReplica && x.NodeId != rightPrimaryNode.NodeId); + Assert.NotNull(wrongPrimaryNode); - string b = (string)conn.GetServer(wrongMasterNode.EndPoint).Execute("GET", Key); - Assert.Equal(Value, b); // wrong master, allow redirect + string b = (string)conn.GetServer(wrongPrimaryNode.EndPoint).Execute("GET", Key); + Assert.Equal(Value, b); // wrong primary, allow redirect var msgs = profiler.GetSession().FinishProfiling().ToList(); // verify that things actually got recorded properly, and the retransmission profilings are connected as expected { - // expect 1 DEL, 1 SET, 1 GET (to right master), 1 GET (to wrong master) that was responded to by an ASK, and 1 GET (to right master or a replica of it) + // expect 1 DEL, 1 SET, 1 GET (to right primary), 1 GET (to wrong primary) that was responded to by an ASK, and 1 GET (to right primary or a replica of it) Assert.Equal(5, msgs.Count); Assert.Equal(1, msgs.Count(c => c.Command == "DEL" || c.Command == "UNLINK")); Assert.Equal(1, msgs.Count(c => c.Command == "SET")); Assert.Equal(3, msgs.Count(c => c.Command == "GET")); - var toRightMasterNotRetransmission = msgs.Where(m => m.Command == "GET" && m.EndPoint.Equals(rightMasterNode.EndPoint) && m.RetransmissionOf == null); - Assert.Single(toRightMasterNotRetransmission); + var toRightPrimaryNotRetransmission = msgs.Where(m => m.Command == "GET" && m.EndPoint.Equals(rightPrimaryNode.EndPoint) && m.RetransmissionOf == null); + Assert.Single(toRightPrimaryNotRetransmission); - var toWrongMasterWithoutRetransmission = msgs.Where(m => m.Command == "GET" && m.EndPoint.Equals(wrongMasterNode.EndPoint) && m.RetransmissionOf == null).ToList(); - Assert.Single(toWrongMasterWithoutRetransmission); + var toWrongPrimaryWithoutRetransmission = msgs.Where(m => m.Command == "GET" && m.EndPoint.Equals(wrongPrimaryNode.EndPoint) && m.RetransmissionOf == null).ToList(); + Assert.Single(toWrongPrimaryWithoutRetransmission); - var toRightMasterOrReplicaAsRetransmission = msgs.Where(m => m.Command == "GET" && (m.EndPoint.Equals(rightMasterNode.EndPoint) || rightMasterNode.Children.Any(c => m.EndPoint.Equals(c.EndPoint))) && m.RetransmissionOf != null).ToList(); - Assert.Single(toRightMasterOrReplicaAsRetransmission); + var toRightPrimaryOrReplicaAsRetransmission = msgs.Where(m => m.Command == "GET" && (m.EndPoint.Equals(rightPrimaryNode.EndPoint) || rightPrimaryNode.Children.Any(c => m.EndPoint.Equals(c.EndPoint))) && m.RetransmissionOf != null).ToList(); + Assert.Single(toRightPrimaryOrReplicaAsRetransmission); - var originalWrongMaster = toWrongMasterWithoutRetransmission.Single(); - var retransmissionToRight = toRightMasterOrReplicaAsRetransmission.Single(); + var originalWrongPrimary = toWrongPrimaryWithoutRetransmission.Single(); + var retransmissionToRight = toRightPrimaryOrReplicaAsRetransmission.Single(); - Assert.True(ReferenceEquals(originalWrongMaster, retransmissionToRight.RetransmissionOf)); + Assert.True(ReferenceEquals(originalWrongPrimary, retransmissionToRight.RetransmissionOf)); } foreach (var msg in msgs) diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index 969836b96..d579f4c0a 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -209,7 +209,7 @@ public void GetSlowlog(int count) { using (var muxer = Create(allowAdmin: true)) { - var rows = GetAnyMaster(muxer).SlowlogGet(count); + var rows = GetAnyPrimary(muxer).SlowlogGet(count); Assert.NotNull(rows); } } @@ -219,7 +219,7 @@ public void ClearSlowlog() { using (var muxer = Create(allowAdmin: true)) { - GetAnyMaster(muxer).SlowlogReset(); + GetAnyPrimary(muxer).SlowlogReset(); } } @@ -233,7 +233,7 @@ public void ClientName() var conn = muxer.GetDatabase(); conn.Ping(); - var name = (string)GetAnyMaster(muxer).Execute("CLIENT", "GETNAME"); + var name = (string)GetAnyPrimary(muxer).Execute("CLIENT", "GETNAME"); Assert.Equal("TestRig", name); } } @@ -247,7 +247,7 @@ public void DefaultClientName() var conn = muxer.GetDatabase(); conn.Ping(); - var name = (string)GetAnyMaster(muxer).Execute("CLIENT", "GETNAME"); + var name = (string)GetAnyPrimary(muxer).Execute("CLIENT", "GETNAME"); Assert.Equal($"{Environment.MachineName}(SE.Redis-v{Utils.GetLibVersion()})", name); } } @@ -257,7 +257,7 @@ public void ReadConfigWithConfigDisabled() { using (var muxer = Create(allowAdmin: true, disabledCommands: new[] { "config", "info" })) { - var conn = GetAnyMaster(muxer); + var conn = GetAnyPrimary(muxer); var ex = Assert.Throws(() => conn.ConfigGet()); Assert.Equal("This operation has been disabled in the command-map and cannot be used: CONFIG", ex.Message); } @@ -284,7 +284,7 @@ public void ReadConfig() using (var muxer = Create(allowAdmin: true)) { Log("about to get config"); - var conn = GetAnyMaster(muxer); + var conn = GetAnyPrimary(muxer); var all = conn.ConfigGet(); Assert.True(all.Length > 0, "any"); @@ -296,7 +296,7 @@ public void ReadConfig() Assert.True(pairs.ContainsKey("port"), "port"); val = int.Parse(pairs["port"]); - Assert.Equal(TestConfig.Current.MasterPort, val); + Assert.Equal(TestConfig.Current.PrimaryPort, val); } } @@ -305,7 +305,7 @@ public void GetTime() { using (var muxer = Create()) { - var server = GetAnyMaster(muxer); + var server = GetAnyPrimary(muxer); var serverTime = server.Time(); var localTime = DateTime.UtcNow; Log("Server: " + serverTime.ToString(CultureInfo.InvariantCulture)); @@ -334,7 +334,7 @@ public void GetInfo() { using (var muxer = Create(allowAdmin: true)) { - var server = GetAnyMaster(muxer); + var server = GetAnyPrimary(muxer); var info1 = server.Info(); Assert.True(info1.Length > 5); Log("All sections"); @@ -365,7 +365,7 @@ public void GetInfoRaw() { using (var muxer = Create(allowAdmin: true)) { - var server = GetAnyMaster(muxer); + var server = GetAnyPrimary(muxer); var info = server.InfoRaw(); Assert.Contains("used_cpu_sys", info); Assert.Contains("used_cpu_user", info); @@ -378,7 +378,7 @@ public void GetClients() var name = Guid.NewGuid().ToString(); using (var muxer = Create(clientName: name, allowAdmin: true)) { - var server = GetAnyMaster(muxer); + var server = GetAnyPrimary(muxer); var clients = server.ClientList(); Assert.True(clients.Length > 0, "no clients"); // ourselves! Assert.True(clients.Any(x => x.Name == name), "expected: " + name); @@ -390,7 +390,7 @@ public void SlowLog() { using (var muxer = Create(allowAdmin: true)) { - var server = GetAnyMaster(muxer); + var server = GetAnyPrimary(muxer); server.SlowlogGet(); server.SlowlogReset(); } @@ -405,7 +405,7 @@ public async Task TestAutomaticHeartbeat() try { configMuxer.GetDatabase(); - var srv = GetAnyMaster(configMuxer); + var srv = GetAnyPrimary(configMuxer); oldTimeout = srv.ConfigGet("timeout")[0].Value; srv.ConfigSet("timeout", 5); @@ -427,7 +427,7 @@ public async Task TestAutomaticHeartbeat() { if (!oldTimeout.IsNull) { - var srv = GetAnyMaster(configMuxer); + var srv = GetAnyPrimary(configMuxer); srv.ConfigSet("timeout", oldTimeout); } } diff --git a/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs b/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs index dc2ec8cfa..bd9a09993 100644 --- a/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs +++ b/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs @@ -8,7 +8,7 @@ public class ConnectCustomConfig : TestBase public ConnectCustomConfig(ITestOutputHelper output) : base (output) { } // So we're triggering tiebreakers here - protected override string GetConfiguration() => TestConfig.Current.MasterServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; [Theory] [InlineData("config")] diff --git a/tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs b/tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs index 03757d918..d648b5784 100644 --- a/tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs +++ b/tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs @@ -48,7 +48,7 @@ void innerScenario() { var ex = Assert.Throws(() => { - using (ConnectionMultiplexer.Connect(TestConfig.Current.MasterServer + ":6500,connectTimeout=1000,connectRetry=0", Writer)) { } + using (ConnectionMultiplexer.Connect(TestConfig.Current.PrimaryServer + ":6500,connectTimeout=1000,connectRetry=0", Writer)) { } }); Log(ex.ToString()); } @@ -70,7 +70,7 @@ public async Task CreateDisconnectedNonsenseConnection_IP() await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); void innerScenario() { - using (var conn = ConnectionMultiplexer.Connect(TestConfig.Current.MasterServer + ":6500,abortConnect=false,connectTimeout=1000,connectRetry=0", Writer)) + using (var conn = ConnectionMultiplexer.Connect(TestConfig.Current.PrimaryServer + ":6500,abortConnect=false,connectTimeout=1000,connectRetry=0", Writer)) { Assert.False(conn.GetServer(conn.GetEndPoints().Single()).IsConnected); Assert.False(conn.GetDatabase().IsConnected(default(RedisKey))); diff --git a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs index 8d3af063c..9dd0ad570 100644 --- a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs +++ b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs @@ -10,7 +10,7 @@ public class ConnectingFailDetection : TestBase { public ConnectingFailDetection(ITestOutputHelper output) : base (output) { } - protected override string GetConfiguration() => TestConfig.Current.MasterServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; [Fact] public async Task FastNoticesFailOnConnectingSyncCompletion() @@ -95,7 +95,7 @@ public async Task FastNoticesFailOnConnectingAsyncCompletion() [Fact] public async Task Issue922_ReconnectRaised() { - var config = ConfigurationOptions.Parse(TestConfig.Current.MasterServerAndPort); + var config = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort); config.AbortOnConnectFail = true; config.KeepAlive = 1; config.SyncTimeout = 1000; @@ -123,7 +123,7 @@ public async Task Issue922_ReconnectRaised() Assert.Equal(0, Volatile.Read(ref failCount)); Assert.Equal(0, Volatile.Read(ref restoreCount)); - var server = muxer.GetServer(TestConfig.Current.MasterServerAndPort); + var server = muxer.GetServer(TestConfig.Current.PrimaryServerAndPort); server.SimulateConnectionFailure(SimulatedFailureType.All); await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Volatile.Read(ref failCount) >= 2 && Volatile.Read(ref restoreCount) >= 2); diff --git a/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs b/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs index d75054ca4..df85d2818 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis.Tests { public class ConnectionShutdown : TestBase { - protected override string GetConfiguration() => TestConfig.Current.MasterServerAndPort; + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; public ConnectionShutdown(ITestOutputHelper output) : base(output) { } [Fact(Skip = "Unfriendly")] @@ -35,7 +35,7 @@ public async Task ShutdownRaisesConnectionFailedAndRestore() await Task.Delay(1).ForAwait(); // To make compiler happy in Release conn.AllowConnect = false; - var server = conn.GetServer(TestConfig.Current.MasterServer, TestConfig.Current.MasterPort); + var server = conn.GetServer(TestConfig.Current.PrimaryServer, TestConfig.Current.PrimaryPort); SetExpectedAmbientFailureCount(2); server.SimulateConnectionFailure(SimulatedFailureType.All); diff --git a/tests/StackExchange.Redis.Tests/Databases.cs b/tests/StackExchange.Redis.Tests/Databases.cs index 13ee2c25b..678defe38 100644 --- a/tests/StackExchange.Redis.Tests/Databases.cs +++ b/tests/StackExchange.Redis.Tests/Databases.cs @@ -18,7 +18,7 @@ public async Task CountKeys() { Skip.IfMissingDatabase(muxer, db1Id); Skip.IfMissingDatabase(muxer, db2Id); - var server = GetAnyMaster(muxer); + var server = GetAnyPrimary(muxer); server.FlushDatabase(db1Id, CommandFlags.FireAndForget); server.FlushDatabase(db2Id, CommandFlags.FireAndForget); } @@ -33,7 +33,7 @@ public async Task CountKeys() dba.StringIncrement(key, flags: CommandFlags.FireAndForget); dbb.StringIncrement(key, flags: CommandFlags.FireAndForget); - var server = GetAnyMaster(muxer); + var server = GetAnyPrimary(muxer); var c0 = server.DatabaseSizeAsync(db1Id); var c1 = server.DatabaseSizeAsync(db2Id); var c2 = server.DatabaseSizeAsync(); // using default DB, which is db2Id @@ -49,7 +49,7 @@ public void DatabaseCount() { using (var muxer = Create(allowAdmin: true)) { - var server = GetAnyMaster(muxer); + var server = GetAnyPrimary(muxer); var count = server.DatabaseCount; Log("Count: " + count); var configVal = server.ConfigGet("databases")[0].Value; diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index d6e56ddd5..a52c6832d 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -165,7 +165,7 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple ConnectionMultiplexer muxer; if (abortOnConnect) { - options.EndPoints.Add(TestConfig.Current.MasterServerAndPort); + options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); muxer = ConnectionMultiplexer.Connect(options, Writer); } else diff --git a/tests/StackExchange.Redis.Tests/Failover.cs b/tests/StackExchange.Redis.Tests/Failover.cs index 39d1699f9..b0257b76c 100644 --- a/tests/StackExchange.Redis.Tests/Failover.cs +++ b/tests/StackExchange.Redis.Tests/Failover.cs @@ -21,7 +21,7 @@ public async Task InitializeAsync() { using (var mutex = Create()) { - var shouldBePrimary = mutex.GetServer(TestConfig.Current.FailoverMasterServerAndPort); + var shouldBePrimary = mutex.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort); if (shouldBePrimary.IsReplica) { Log(shouldBePrimary.EndPoint + " should be primary, fixing..."); @@ -46,7 +46,7 @@ private static ConfigurationOptions GetPrimaryReplicaConfig() SyncTimeout = 100000, EndPoints = { - { TestConfig.Current.FailoverMasterServer, TestConfig.Current.FailoverMasterPort }, + { TestConfig.Current.FailoverPrimaryServer, TestConfig.Current.FailoverPrimaryPort }, { TestConfig.Current.FailoverReplicaServer, TestConfig.Current.FailoverReplicaPort }, } }; @@ -117,7 +117,7 @@ public async Task DereplicateGoesToPrimary() config.ConfigCheckSeconds = 5; using (var conn = ConnectionMultiplexer.Connect(config)) { - var primary = conn.GetServer(TestConfig.Current.FailoverMasterServerAndPort); + var primary = conn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort); var secondary = conn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort); primary.Ping(); @@ -136,7 +136,7 @@ public async Task DereplicateGoesToPrimary() conn.Configure(writer); string log = writer.ToString(); Writer.WriteLine(log); - bool isUnanimous = log.Contains("tie-break is unanimous at " + TestConfig.Current.FailoverMasterServerAndPort); + bool isUnanimous = log.Contains("tie-break is unanimous at " + TestConfig.Current.FailoverPrimaryServerAndPort); if (!isUnanimous) Skip.Inconclusive("this is timing sensitive; unable to verify this time"); } // k, so we know everyone loves 6379; is that what we get? @@ -172,7 +172,7 @@ public async Task DereplicateGoesToPrimary() Writer.WriteLine("Connecting to secondary validation connection."); using (var conn2 = ConnectionMultiplexer.Connect(config)) { - var primary2 = conn2.GetServer(TestConfig.Current.FailoverMasterServerAndPort); + var primary2 = conn2.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort); var secondary2 = conn2.GetServer(TestConfig.Current.FailoverReplicaServerAndPort); Writer.WriteLine($"Check: {primary2.EndPoint}: {primary2.ServerType}, Mode: {(primary2.IsReplica ? "Replica" : "Primary")}"); @@ -240,13 +240,13 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() Interlocked.Increment(ref bCount); }); - Assert.False(a.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverMasterServerAndPort} should be a master"); + Assert.False(a.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverPrimaryServerAndPort} should be a primary"); if (!a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica) { TopologyFail(); } Assert.True(a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a replica"); - Assert.False(b.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica, $"B Connection: {TestConfig.Current.FailoverMasterServerAndPort} should be a master"); + Assert.False(b.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica, $"B Connection: {TestConfig.Current.FailoverPrimaryServerAndPort} should be a primary"); Assert.True(b.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"B Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a replica"); Log("Failover 1 Complete"); @@ -277,25 +277,25 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() Log(sw.ToString()); } Log("Waiting for connection B to detect..."); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => b.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica).ForAwait(); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => b.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica).ForAwait(); subA.Ping(); subB.Ping(); Log("Failover 2 Attempted. Pausing..."); - Log(" A " + TestConfig.Current.FailoverMasterServerAndPort + " status: " + (a.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica ? "Replica" : "Primary")); + Log(" A " + TestConfig.Current.FailoverPrimaryServerAndPort + " status: " + (a.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica ? "Replica" : "Primary")); Log(" A " + TestConfig.Current.FailoverReplicaServerAndPort + " status: " + (a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica ? "Replica" : "Primary")); - Log(" B " + TestConfig.Current.FailoverMasterServerAndPort + " status: " + (b.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica ? "Replica" : "Primary")); + Log(" B " + TestConfig.Current.FailoverPrimaryServerAndPort + " status: " + (b.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica ? "Replica" : "Primary")); Log(" B " + TestConfig.Current.FailoverReplicaServerAndPort + " status: " + (b.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica ? "Replica" : "Primary")); - if (!a.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica) + if (!a.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica) { TopologyFail(); } Log("Failover 2 Complete."); - Assert.True(a.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverMasterServerAndPort} should be a replica"); - Assert.False(a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a master"); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => b.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica).ForAwait(); - var sanityCheck = b.GetServer(TestConfig.Current.FailoverMasterServerAndPort).IsReplica; + Assert.True(a.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverPrimaryServerAndPort} should be a replica"); + Assert.False(a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a primary"); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => b.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica).ForAwait(); + var sanityCheck = b.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica; if (!sanityCheck) { Log("FAILURE: B has not detected the topology change."); @@ -308,8 +308,8 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() } //Skip.Inconclusive("Not enough latency."); } - Assert.True(sanityCheck, $"B Connection: {TestConfig.Current.FailoverMasterServerAndPort} should be a replica"); - Assert.False(b.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"B Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a master"); + Assert.True(sanityCheck, $"B Connection: {TestConfig.Current.FailoverPrimaryServerAndPort} should be a replica"); + Assert.False(b.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"B Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a primary"); Log("Pause complete"); Log(" A outstanding: " + a.GetCounters().TotalOutstanding); @@ -353,7 +353,7 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() Log("Restoring configuration..."); try { - await a.GetServer(TestConfig.Current.FailoverMasterServerAndPort).MakePrimaryAsync(ReplicationChangeOptions.All); + await a.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).MakePrimaryAsync(ReplicationChangeOptions.All); await Task.Delay(1000).ForAwait(); } catch { /* Don't bomb here */ } diff --git a/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs b/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs index 1a748d444..e72154301 100644 --- a/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs +++ b/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs @@ -12,10 +12,10 @@ [assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSL.ShowCertFailures(StackExchange.Redis.Tests.Helpers.TextWriterOutputHelper)~System.Net.Security.RemoteCertificateValidationCallback")] [assembly: SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionShutdown.ShutdownRaisesConnectionFailedAndRestore")] [assembly: SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Issues.BgSaveResponse.ShouldntThrowException(StackExchange.Redis.SaveType)")] -[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Sentinel.MasterConnectTest~System.Threading.Tasks.Task")] -[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Sentinel.MasterConnectAsyncTest~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Sentinel.PrimaryConnectTest~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Sentinel.PrimaryConnectAsyncTest~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelBase.WaitForReplicationAsync(StackExchange.Redis.IServer,System.Nullable{System.TimeSpan})~System.Threading.Tasks.Task")] -[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelFailover.ManagedMasterConnectionEndToEndWithFailoverTest~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelFailover.ManagedPrimaryConnectionEndToEndWithFailoverTest~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Performance", "CA1846:Prefer 'AsSpan' over 'Substring'", Justification = "", Scope = "member", Target = "~M:RedisSharp.Redis.ReadData~System.Byte[]")] [assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Naming.IgnoreMethodConventions(System.Reflection.MethodInfo)~System.Boolean")] [assembly: SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelBase.WaitForReadyAsync(System.Net.EndPoint,System.Boolean,System.Nullable{System.TimeSpan})~System.Threading.Tasks.Task")] diff --git a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs index 455aa2dd8..e11855689 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs @@ -48,9 +48,9 @@ public class Config public bool RunLongRunning { get; set; } public bool LogToConsole { get; set; } - public string MasterServer { get; set; } = "127.0.0.1"; - public int MasterPort { get; set; } = 6379; - public string MasterServerAndPort => MasterServer + ":" + MasterPort.ToString(); + public string PrimaryServer { get; set; } = "127.0.0.1"; + public int PrimaryPort { get; set; } = 6379; + public string PrimaryServerAndPort => PrimaryServer + ":" + PrimaryPort.ToString(); public string ReplicaServer { get; set; } = "127.0.0.1"; public int ReplicaPort { get; set; } = 6380; @@ -62,9 +62,9 @@ public class Config public string SecureServerAndPort => SecureServer + ":" + SecurePort.ToString(); // Separate servers for failover tests, so they don't wreak havoc on all others - public string FailoverMasterServer { get; set; } = "127.0.0.1"; - public int FailoverMasterPort { get; set; } = 6382; - public string FailoverMasterServerAndPort => FailoverMasterServer + ":" + FailoverMasterPort.ToString(); + public string FailoverPrimaryServer { get; set; } = "127.0.0.1"; + public int FailoverPrimaryPort { get; set; } = 6382; + public string FailoverPrimaryServerAndPort => FailoverPrimaryServer + ":" + FailoverPrimaryPort.ToString(); public string FailoverReplicaServer { get; set; } = "127.0.0.1"; public int FailoverReplicaPort { get; set; } = 6383; @@ -83,7 +83,7 @@ public class Config public int SentinelPortA { get; set; } = 26379; public int SentinelPortB { get; set; } = 26380; public int SentinelPortC { get; set; } = 26381; - public string SentinelSeviceName { get; set; } = "mymaster"; + public string SentinelSeviceName { get; set; } = "myprimary"; public string ClusterServer { get; set; } = "127.0.0.1"; public int ClusterStartPort { get; set; } = 7000; diff --git a/tests/StackExchange.Redis.Tests/Issues/DefaultDatabase.cs b/tests/StackExchange.Redis.Tests/Issues/DefaultDatabase.cs index 5166c5a0a..9bd4e17b6 100644 --- a/tests/StackExchange.Redis.Tests/Issues/DefaultDatabase.cs +++ b/tests/StackExchange.Redis.Tests/Issues/DefaultDatabase.cs @@ -28,7 +28,7 @@ public void ConfigurationOptions_UnspecifiedDefaultDb() var log = new StringWriter(); try { - using (var conn = ConnectionMultiplexer.Connect(TestConfig.Current.MasterServerAndPort, log)) { + using (var conn = ConnectionMultiplexer.Connect(TestConfig.Current.PrimaryServerAndPort, log)) { var db = conn.GetDatabase(); Assert.Equal(0, db.Database); } @@ -45,7 +45,7 @@ public void ConfigurationOptions_SpecifiedDefaultDb() var log = new StringWriter(); try { - using (var conn = ConnectionMultiplexer.Connect($"{TestConfig.Current.MasterServerAndPort},defaultDatabase=3", log)) { + using (var conn = ConnectionMultiplexer.Connect($"{TestConfig.Current.PrimaryServerAndPort},defaultDatabase=3", log)) { var db = conn.GetDatabase(); Assert.Equal(3, db.Database); } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue182.cs b/tests/StackExchange.Redis.Tests/Issues/Issue182.cs index 1c541ce99..1b75c1240 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue182.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue182.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis.Tests.Issues { public class Issue182 : TestBase { - protected override string GetConfiguration() => $"{TestConfig.Current.MasterServerAndPort},responseTimeout=10000"; + protected override string GetConfiguration() => $"{TestConfig.Current.PrimaryServerAndPort},responseTimeout=10000"; public Issue182(ITestOutputHelper output) : base (output) { } diff --git a/tests/StackExchange.Redis.Tests/Issues/SO25567566.cs b/tests/StackExchange.Redis.Tests/Issues/SO25567566.cs index 2a322d943..363419ae8 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO25567566.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO25567566.cs @@ -7,7 +7,7 @@ namespace StackExchange.Redis.Tests.Issues { public class SO25567566 : TestBase { - protected override string GetConfiguration() => TestConfig.Current.MasterServerAndPort; + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; public SO25567566(ITestOutputHelper output) : base(output) { } [FactLongRunning] diff --git a/tests/StackExchange.Redis.Tests/Keys.cs b/tests/StackExchange.Redis.Tests/Keys.cs index 8dbe330ee..0edaa1939 100644 --- a/tests/StackExchange.Redis.Tests/Keys.cs +++ b/tests/StackExchange.Redis.Tests/Keys.cs @@ -19,7 +19,7 @@ public void TestScan() { var dbId = TestConfig.GetDedicatedDB(); var db = muxer.GetDatabase(dbId); - var server = GetAnyMaster(muxer); + var server = GetAnyPrimary(muxer); var prefix = Me(); server.FlushDatabase(dbId, flags: CommandFlags.FireAndForget); @@ -41,7 +41,7 @@ public void FlushFetchRandomKey() Skip.IfMissingDatabase(conn, dbId); var db = conn.GetDatabase(dbId); var prefix = Me(); - conn.GetServer(TestConfig.Current.MasterServerAndPort).FlushDatabase(dbId, CommandFlags.FireAndForget); + conn.GetServer(TestConfig.Current.PrimaryServerAndPort).FlushDatabase(dbId, CommandFlags.FireAndForget); string anyKey = db.KeyRandom(); Assert.Null(anyKey); diff --git a/tests/StackExchange.Redis.Tests/Locking.cs b/tests/StackExchange.Redis.Tests/Locking.cs index e931af60b..dfe4ab095 100644 --- a/tests/StackExchange.Redis.Tests/Locking.cs +++ b/tests/StackExchange.Redis.Tests/Locking.cs @@ -10,7 +10,7 @@ namespace StackExchange.Redis.Tests [Collection(NonParallelCollection.Name)] public class Locking : TestBase { - protected override string GetConfiguration() => TestConfig.Current.MasterServerAndPort; + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; public Locking(ITestOutputHelper output) : base (output) { } public enum TestMode diff --git a/tests/StackExchange.Redis.Tests/Migrate.cs b/tests/StackExchange.Redis.Tests/Migrate.cs index ca148e8e3..9a89e006e 100644 --- a/tests/StackExchange.Redis.Tests/Migrate.cs +++ b/tests/StackExchange.Redis.Tests/Migrate.cs @@ -14,7 +14,7 @@ public Migrate(ITestOutputHelper output) : base (output) { } public async Task Basic() { var fromConfig = new ConfigurationOptions { EndPoints = { { TestConfig.Current.SecureServer, TestConfig.Current.SecurePort } }, Password = TestConfig.Current.SecurePassword, AllowAdmin = true }; - var toConfig = new ConfigurationOptions { EndPoints = { { TestConfig.Current.MasterServer, TestConfig.Current.MasterPort } }, AllowAdmin = true }; + var toConfig = new ConfigurationOptions { EndPoints = { { TestConfig.Current.PrimaryServer, TestConfig.Current.PrimaryPort } }, AllowAdmin = true }; using (var from = ConnectionMultiplexer.Connect(fromConfig, Writer)) using (var to = ConnectionMultiplexer.Connect(toConfig, Writer)) { diff --git a/tests/StackExchange.Redis.Tests/MultiMaster.cs b/tests/StackExchange.Redis.Tests/MultiPrimary.cs similarity index 77% rename from tests/StackExchange.Redis.Tests/MultiMaster.cs rename to tests/StackExchange.Redis.Tests/MultiPrimary.cs index 3fbbbc330..448336cb8 100644 --- a/tests/StackExchange.Redis.Tests/MultiMaster.cs +++ b/tests/StackExchange.Redis.Tests/MultiPrimary.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.IO; using System.Linq; using System.Text; using Xunit; @@ -7,11 +6,11 @@ namespace StackExchange.Redis.Tests { - public class MultiMaster : TestBase + public class MultiPrimary : TestBase { protected override string GetConfiguration() => - TestConfig.Current.MasterServerAndPort + "," + TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword; - public MultiMaster(ITestOutputHelper output) : base (output) { } + TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword; + public MultiPrimary(ITestOutputHelper output) : base (output) { } [Fact] public void CannotFlushReplica() @@ -36,19 +35,19 @@ public void TestMultiNoTieBreak() Writer.EchoTo(log); using (Create(log: Writer, tieBreaker: "")) { - Assert.Contains("Choosing master arbitrarily", log.ToString()); + Assert.Contains("Choosing primary arbitrarily", log.ToString()); } } public static IEnumerable GetConnections() { - yield return new object[] { TestConfig.Current.MasterServerAndPort, TestConfig.Current.MasterServerAndPort, TestConfig.Current.MasterServerAndPort }; + yield return new object[] { TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.PrimaryServerAndPort }; yield return new object[] { TestConfig.Current.SecureServerAndPort, TestConfig.Current.SecureServerAndPort, TestConfig.Current.SecureServerAndPort }; - yield return new object[] { TestConfig.Current.SecureServerAndPort, TestConfig.Current.MasterServerAndPort, null }; - yield return new object[] { TestConfig.Current.MasterServerAndPort, TestConfig.Current.SecureServerAndPort, null }; + yield return new object[] { TestConfig.Current.SecureServerAndPort, TestConfig.Current.PrimaryServerAndPort, null }; + yield return new object[] { TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.SecureServerAndPort, null }; - yield return new object[] { null, TestConfig.Current.MasterServerAndPort, TestConfig.Current.MasterServerAndPort }; - yield return new object[] { TestConfig.Current.MasterServerAndPort, null, TestConfig.Current.MasterServerAndPort }; + yield return new object[] { null, TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.PrimaryServerAndPort }; + yield return new object[] { TestConfig.Current.PrimaryServerAndPort, null, TestConfig.Current.PrimaryServerAndPort }; yield return new object[] { null, TestConfig.Current.SecureServerAndPort, TestConfig.Current.SecureServerAndPort }; yield return new object[] { TestConfig.Current.SecureServerAndPort, null, TestConfig.Current.SecureServerAndPort }; yield return new object[] { null, null, null }; @@ -59,7 +58,7 @@ public void TestMultiWithTiebreak(string a, string b, string elected) { const string TieBreak = "__tie__"; // set the tie-breakers to the expected state - using (var aConn = ConnectionMultiplexer.Connect(TestConfig.Current.MasterServerAndPort)) + using (var aConn = ConnectionMultiplexer.Connect(TestConfig.Current.PrimaryServerAndPort)) { aConn.GetDatabase().StringSet(TieBreak, a); } @@ -84,12 +83,12 @@ public void TestMultiWithTiebreak(string a, string b, string elected) if ((a == b && nullCount == 0) || nullCount == 1) { Assert.True(text.Contains("Election: Tie-breaker unanimous"), "unanimous"); - Assert.False(text.Contains("Election: Choosing master arbitrarily"), "arbitrarily"); + Assert.False(text.Contains("Election: Choosing primary arbitrarily"), "arbitrarily"); } else { Assert.False(text.Contains("Election: Tie-breaker unanimous"), "unanimous"); - Assert.True(text.Contains("Election: Choosing master arbitrarily"), "arbitrarily"); + Assert.True(text.Contains("Election: Choosing primary arbitrarily"), "arbitrarily"); } } } diff --git a/tests/StackExchange.Redis.Tests/Naming.cs b/tests/StackExchange.Redis.Tests/Naming.cs index 012359462..809be4574 100644 --- a/tests/StackExchange.Redis.Tests/Naming.cs +++ b/tests/StackExchange.Redis.Tests/Naming.cs @@ -35,33 +35,33 @@ public void ShowReadOnlyOperations() Assert.NotNull(msg); var cmd = typeof(ConnectionMultiplexer).Assembly.GetType("StackExchange.Redis.RedisCommand"); Assert.NotNull(cmd); - var masterOnlyMethod = msg.GetMethod(nameof(Message.IsMasterOnly), BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); - Assert.NotNull(masterOnlyMethod); + var primaryOnlyMethod = msg.GetMethod(nameof(Message.IsPrimaryOnly), BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); + Assert.NotNull(primaryOnlyMethod); object[] args = new object[1]; - List masterReplica = new List(); - List masterOnly = new List(); + List primaryReplica = new List(); + List primaryOnly = new List(); foreach (var val in Enum.GetValues(cmd)) { args[0] = val; - bool isMasterOnly = (bool)masterOnlyMethod.Invoke(null, args); - (isMasterOnly ? masterOnly : masterReplica).Add(val); + bool isPrimaryOnly = (bool)primaryOnlyMethod.Invoke(null, args); + (isPrimaryOnly ? primaryOnly : primaryReplica).Add(val); - if (!isMasterOnly) + if (!isPrimaryOnly) { Log(val?.ToString()); } } - Log("master-only: {0}, vs master/replica: {1}", masterOnly.Count, masterReplica.Count); + Log("primary-only: {0}, vs primary/replica: {1}", primaryOnly.Count, primaryReplica.Count); Log(""); - Log("master-only:"); - foreach (var val in masterOnly) + Log("primary-only:"); + foreach (var val in primaryOnly) { Log(val?.ToString()); } Log(""); - Log("master/replica:"); - foreach (var val in masterReplica) + Log("primary/replica:"); + foreach (var val in primaryReplica) { Log(val?.ToString()); } diff --git a/tests/StackExchange.Redis.Tests/Performance.cs b/tests/StackExchange.Redis.Tests/Performance.cs index c88921243..bab2b6412 100644 --- a/tests/StackExchange.Redis.Tests/Performance.cs +++ b/tests/StackExchange.Redis.Tests/Performance.cs @@ -50,7 +50,7 @@ public void VerifyPerformanceImprovement() } } - using (var conn = new RedisSharp.Redis(TestConfig.Current.MasterServer, TestConfig.Current.MasterPort)) + using (var conn = new RedisSharp.Redis(TestConfig.Current.PrimaryServer, TestConfig.Current.PrimaryPort)) { // do these outside the timings, just to ensure the core methods are JITted etc for (int db = 0; db < 5; db++) diff --git a/tests/StackExchange.Redis.Tests/Profiling.cs b/tests/StackExchange.Redis.Tests/Profiling.cs index 568302fc1..9c43f9bca 100644 --- a/tests/StackExchange.Redis.Tests/Profiling.cs +++ b/tests/StackExchange.Redis.Tests/Profiling.cs @@ -20,7 +20,7 @@ public void Simple() { using (var conn = Create()) { - var server = conn.GetServer(TestConfig.Current.MasterServerAndPort); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); var script = LuaScript.Prepare("return redis.call('get', @key)"); var loaded = script.Load(server); var key = Me(); diff --git a/tests/StackExchange.Redis.Tests/PubSub.cs b/tests/StackExchange.Redis.Tests/PubSub.cs index 13e13874f..02068aa43 100644 --- a/tests/StackExchange.Redis.Tests/PubSub.cs +++ b/tests/StackExchange.Redis.Tests/PubSub.cs @@ -56,7 +56,7 @@ public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string br { using (var muxer = Create(channelPrefix: channelPrefix, shared: false, log: Writer)) { - var pub = GetAnyMaster(muxer); + var pub = GetAnyPrimary(muxer); var sub = muxer.GetSubscriber(); await PingAsync(pub, sub).ForAwait(); HashSet received = new(); @@ -136,7 +136,7 @@ public async Task TestBasicPubSubFireAndForget() using (var muxer = Create(shared: false, log: Writer)) { var profiler = muxer.AddProfiler(); - var pub = GetAnyMaster(muxer); + var pub = GetAnyPrimary(muxer); var sub = muxer.GetSubscriber(); RedisChannel key = Me() + Guid.NewGuid(); @@ -208,7 +208,7 @@ public async Task TestPatternPubSub() { using (var muxer = Create(shared: false, log: Writer)) { - var pub = GetAnyMaster(muxer); + var pub = GetAnyPrimary(muxer); var sub = muxer.GetSubscriber(); HashSet received = new(); diff --git a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs index 15979d90e..f557f5549 100644 --- a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs +++ b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs @@ -99,7 +99,7 @@ await sub.SubscribeAsync(channel, (_, val) => [InlineData(CommandFlags.DemandReplica, false)] public async Task PrimaryReplicaSubscriptionFailover(CommandFlags flags, bool expectSuccess) { - var config = TestConfig.Current.MasterServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + var config = TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; Log("Connecting..."); using var muxer = Create(configuration: config, shared: false, allowAdmin: true) as ConnectionMultiplexer; var sub = muxer.GetSubscriber(); diff --git a/tests/StackExchange.Redis.Tests/RealWorld.cs b/tests/StackExchange.Redis.Tests/RealWorld.cs index 7b8d75ca7..2d6760ae4 100644 --- a/tests/StackExchange.Redis.Tests/RealWorld.cs +++ b/tests/StackExchange.Redis.Tests/RealWorld.cs @@ -12,12 +12,12 @@ public RealWorld(ITestOutputHelper output) : base(output) { } public async Task WhyDoesThisNotWork() { Log("first:"); - var config = ConfigurationOptions.Parse("localhost:6379,localhost:6380,name=Core (Q&A),tiebreaker=:RedisMaster,abortConnect=False"); + var config = ConfigurationOptions.Parse("localhost:6379,localhost:6380,name=Core (Q&A),tiebreaker=:RedisPrimary,abortConnect=False"); Assert.Equal(2, config.EndPoints.Count); Log("Endpoint 0: {0} (AddressFamily: {1})", config.EndPoints[0], config.EndPoints[0].AddressFamily); Log("Endpoint 1: {0} (AddressFamily: {1})", config.EndPoints[1], config.EndPoints[1].AddressFamily); - using (var conn = ConnectionMultiplexer.Connect("localhost:6379,localhost:6380,name=Core (Q&A),tiebreaker=:RedisMaster,abortConnect=False", Writer)) + using (var conn = ConnectionMultiplexer.Connect("localhost:6379,localhost:6380,name=Core (Q&A),tiebreaker=:RedisPrimary,abortConnect=False", Writer)) { Log(""); Log("pausing..."); diff --git a/tests/StackExchange.Redis.Tests/Roles.cs b/tests/StackExchange.Redis.Tests/Roles.cs index 686960ce9..21cee3a8d 100644 --- a/tests/StackExchange.Redis.Tests/Roles.cs +++ b/tests/StackExchange.Redis.Tests/Roles.cs @@ -16,18 +16,18 @@ public Roles(ITestOutputHelper output, SharedConnectionFixture fixture) : base(o [Theory] [InlineData(true)] [InlineData(false)] - public void MasterRole(bool allowAdmin) // should work with or without admin now + public void PrimaryRole(bool allowAdmin) // should work with or without admin now { using var muxer = Create(allowAdmin: allowAdmin); - var server = muxer.GetServer(TestConfig.Current.MasterServerAndPort); + var server = muxer.GetServer(TestConfig.Current.PrimaryServerAndPort); var role = server.Role(); Assert.NotNull(role); Assert.Equal(role.Value, RedisLiterals.master); - var master = role as Role.Master; - Assert.NotNull(master); - Assert.NotNull(master.Replicas); - Assert.Contains(master.Replicas, r => + var primary = role as Role.Master; + Assert.NotNull(primary); + Assert.NotNull(primary.Replicas); + Assert.Contains(primary.Replicas, r => r.Ip == TestConfig.Current.ReplicaServer && r.Port == TestConfig.Current.ReplicaPort); } @@ -43,8 +43,8 @@ public void ReplicaRole() Assert.NotNull(role); var replica = role as Role.Replica; Assert.NotNull(replica); - Assert.Equal(replica.MasterIp, TestConfig.Current.MasterServer); - Assert.Equal(replica.MasterPort, TestConfig.Current.MasterPort); + Assert.Equal(replica.MasterIp, TestConfig.Current.PrimaryServer); + Assert.Equal(replica.MasterPort, TestConfig.Current.PrimaryPort); } } } diff --git a/tests/StackExchange.Redis.Tests/Scripting.cs b/tests/StackExchange.Redis.Tests/Scripting.cs index 62b93f60f..6c3236e8a 100644 --- a/tests/StackExchange.Redis.Tests/Scripting.cs +++ b/tests/StackExchange.Redis.Tests/Scripting.cs @@ -411,7 +411,7 @@ public async Task CheckLoads(bool async) Skip.IfMissingFeature(conn0, nameof(RedisFeatures.Scripting), f => f.Scripting); // note that these are on different connections (so we wouldn't expect // the flush to drop the local cache - assume it is a surprise!) - var server = conn0.GetServer(TestConfig.Current.MasterServerAndPort); + var server = conn0.GetServer(TestConfig.Current.PrimaryServerAndPort); var db = conn1.GetDatabase(); const string script = "return 1;"; @@ -460,7 +460,7 @@ public void CompareScriptToDirect() using (var conn = Create(allowAdmin: true)) { Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); - var server = conn.GetServer(TestConfig.Current.MasterServerAndPort); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); server.ScriptLoad(Script); @@ -511,7 +511,7 @@ public void TestCallByHash() using (var conn = Create(allowAdmin: true)) { Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); - var server = conn.GetServer(TestConfig.Current.MasterServerAndPort); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); byte[] hash = server.ScriptLoad(Script); @@ -540,7 +540,7 @@ public void SimpleLuaScript() using (var conn = Create(allowAdmin: true)) { Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); - var server = conn.GetServer(TestConfig.Current.MasterServerAndPort); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); var prepared = LuaScript.Prepare(Script); @@ -592,7 +592,7 @@ public void SimpleRawScriptEvaluate() using (var conn = Create(allowAdmin: true)) { Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); - var server = conn.GetServer(TestConfig.Current.MasterServerAndPort); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); var db = conn.GetDatabase(); @@ -642,7 +642,7 @@ public void LuaScriptWithKeys() using (var conn = Create(allowAdmin: true)) { Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); - var server = conn.GetServer(TestConfig.Current.MasterServerAndPort); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); var script = LuaScript.Prepare(Script); @@ -672,7 +672,7 @@ public void NoInlineReplacement() using (var conn = Create(allowAdmin: true)) { Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); - var server = conn.GetServer(TestConfig.Current.MasterServerAndPort); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); var script = LuaScript.Prepare(Script); @@ -708,7 +708,7 @@ public void SimpleLoadedLuaScript() using (var conn = Create(allowAdmin: true)) { Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); - var server = conn.GetServer(TestConfig.Current.MasterServerAndPort); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); var prepared = LuaScript.Prepare(Script); @@ -761,7 +761,7 @@ public void LoadedLuaScriptWithKeys() using (var conn = Create(allowAdmin: true)) { Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); - var server = conn.GetServer(TestConfig.Current.MasterServerAndPort); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); var script = LuaScript.Prepare(Script); diff --git a/tests/StackExchange.Redis.Tests/Sentinel.cs b/tests/StackExchange.Redis.Tests/Sentinel.cs index e3f228dfe..5970f6852 100644 --- a/tests/StackExchange.Redis.Tests/Sentinel.cs +++ b/tests/StackExchange.Redis.Tests/Sentinel.cs @@ -13,7 +13,7 @@ public class Sentinel : SentinelBase public Sentinel(ITestOutputHelper output) : base(output) { } [Fact] - public async Task MasterConnectTest() + public async Task PrimaryConnectTest() { var connectionString = $"{TestConfig.Current.SentinelServer},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; var conn = ConnectionMultiplexer.Connect(connectionString); @@ -27,11 +27,11 @@ public async Task MasterConnectTest() var servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); Assert.Equal(2, servers.Length); - var master = servers.FirstOrDefault(s => !s.IsReplica); - Assert.NotNull(master); + var primary = servers.FirstOrDefault(s => !s.IsReplica); + Assert.NotNull(primary); var replica = servers.FirstOrDefault(s => s.IsReplica); Assert.NotNull(replica); - Assert.NotEqual(master.EndPoint.ToString(), replica.EndPoint.ToString()); + Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString()); var expected = DateTime.Now.Ticks.ToString(); Log("Tick Key: " + expected); @@ -49,7 +49,7 @@ public async Task MasterConnectTest() } [Fact] - public async Task MasterConnectAsyncTest() + public async Task PrimaryConnectAsyncTest() { var connectionString = $"{TestConfig.Current.SentinelServer},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; var conn = await ConnectionMultiplexer.ConnectAsync(connectionString); @@ -63,11 +63,11 @@ public async Task MasterConnectAsyncTest() var servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); Assert.Equal(2, servers.Length); - var master = servers.FirstOrDefault(s => !s.IsReplica); - Assert.NotNull(master); + var primary = servers.FirstOrDefault(s => !s.IsReplica); + Assert.NotNull(primary); var replica = servers.FirstOrDefault(s => s.IsReplica); Assert.NotNull(replica); - Assert.NotEqual(master.EndPoint.ToString(), replica.EndPoint.ToString()); + Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString()); var expected = DateTime.Now.Ticks.ToString(); Log("Tick Key: " + expected); @@ -139,33 +139,33 @@ public void PingTest() } [Fact] - public void SentinelGetMasterAddressByNameTest() + public void SentinelGetPrimaryAddressByNameTest() { foreach (var server in SentinelsServers) { - var master = server.SentinelMaster(ServiceName); + var primary = server.SentinelMaster(ServiceName); var endpoint = server.SentinelGetMasterAddressByName(ServiceName); Assert.NotNull(endpoint); var ipEndPoint = endpoint as IPEndPoint; Assert.NotNull(ipEndPoint); - Assert.Equal(master.ToDictionary()["ip"], ipEndPoint.Address.ToString()); - Assert.Equal(master.ToDictionary()["port"], ipEndPoint.Port.ToString()); + Assert.Equal(primary.ToDictionary()["ip"], ipEndPoint.Address.ToString()); + Assert.Equal(primary.ToDictionary()["port"], ipEndPoint.Port.ToString()); Log("{0}:{1}", ipEndPoint.Address, ipEndPoint.Port); } } [Fact] - public async Task SentinelGetMasterAddressByNameAsyncTest() + public async Task SentinelGetPrimaryAddressByNameAsyncTest() { foreach (var server in SentinelsServers) { - var master = server.SentinelMaster(ServiceName); + var primary = server.SentinelMaster(ServiceName); var endpoint = await server.SentinelGetMasterAddressByNameAsync(ServiceName).ForAwait(); Assert.NotNull(endpoint); var ipEndPoint = endpoint as IPEndPoint; Assert.NotNull(ipEndPoint); - Assert.Equal(master.ToDictionary()["ip"], ipEndPoint.Address.ToString()); - Assert.Equal(master.ToDictionary()["port"], ipEndPoint.Port.ToString()); + Assert.Equal(primary.ToDictionary()["ip"], ipEndPoint.Address.ToString()); + Assert.Equal(primary.ToDictionary()["port"], ipEndPoint.Port.ToString()); Log("{0}:{1}", ipEndPoint.Address, ipEndPoint.Port); } } @@ -191,7 +191,7 @@ public async Task SentinelGetMasterAddressByNameAsyncNegativeTest() } [Fact] - public void SentinelMasterTest() + public void SentinelPrimaryTest() { foreach (var server in SentinelsServers) { @@ -206,7 +206,7 @@ public void SentinelMasterTest() } [Fact] - public async Task SentinelMasterAsyncTest() + public async Task SentinelPrimaryAsyncTest() { foreach (var server in SentinelsServers) { @@ -325,14 +325,14 @@ public async Task SentinelSentinelsAsyncTest() } [Fact] - public void SentinelMastersTest() + public void SentinelPrimariesTest() { - var masterConfigs = SentinelServerA.SentinelMasters(); - Assert.Single(masterConfigs); - Assert.True(masterConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); - Assert.Equal(ServiceName, masterConfigs[0].ToDictionary()["name"]); - Assert.StartsWith("master", masterConfigs[0].ToDictionary()["flags"]); - foreach (var config in masterConfigs) + var primaryConfigs = SentinelServerA.SentinelMasters(); + Assert.Single(primaryConfigs); + Assert.True(primaryConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); + Assert.Equal(ServiceName, primaryConfigs[0].ToDictionary()["name"]); + Assert.StartsWith("master", primaryConfigs[0].ToDictionary()["flags"]); + foreach (var config in primaryConfigs) { foreach (var kvp in config) { @@ -342,14 +342,14 @@ public void SentinelMastersTest() } [Fact] - public async Task SentinelMastersAsyncTest() + public async Task SentinelPrimariesAsyncTest() { - var masterConfigs = await SentinelServerA.SentinelMastersAsync().ForAwait(); - Assert.Single(masterConfigs); - Assert.True(masterConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); - Assert.Equal(ServiceName, masterConfigs[0].ToDictionary()["name"]); - Assert.StartsWith("master", masterConfigs[0].ToDictionary()["flags"]); - foreach (var config in masterConfigs) + var primaryConfigs = await SentinelServerA.SentinelMastersAsync().ForAwait(); + Assert.Single(primaryConfigs); + Assert.True(primaryConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); + Assert.Equal(ServiceName, primaryConfigs[0].ToDictionary()["name"]); + Assert.StartsWith("master", primaryConfigs[0].ToDictionary()["flags"]); + foreach (var config in primaryConfigs) { foreach (var kvp in config) { diff --git a/tests/StackExchange.Redis.Tests/SentinelBase.cs b/tests/StackExchange.Redis.Tests/SentinelBase.cs index 99b4c9697..8622b4551 100644 --- a/tests/StackExchange.Redis.Tests/SentinelBase.cs +++ b/tests/StackExchange.Redis.Tests/SentinelBase.cs @@ -54,7 +54,7 @@ public async Task InitializeAsync() SentinelServerC = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortC); SentinelsServers = new[] { SentinelServerA, SentinelServerB, SentinelServerC }; - // wait until we are in a state of a single master and replica + // Wait until we are in a state of a single primary and replica await WaitForReadyAsync(); } @@ -67,23 +67,23 @@ protected class IpComparer : IEqualityComparer public int GetHashCode(string obj) => obj.GetHashCode(); } - protected async Task WaitForReadyAsync(EndPoint expectedMaster = null, bool waitForReplication = false, TimeSpan? duration = null) + protected async Task WaitForReadyAsync(EndPoint expectedPrimary = null, bool waitForReplication = false, TimeSpan? duration = null) { duration ??= TimeSpan.FromSeconds(30); var sw = Stopwatch.StartNew(); - // wait until we have 1 master and 1 replica and have verified their roles - var master = SentinelServerA.SentinelGetMasterAddressByName(ServiceName); - if (expectedMaster != null && expectedMaster.ToString() != master.ToString()) + // wait until we have 1 primary and 1 replica and have verified their roles + var primary = SentinelServerA.SentinelGetMasterAddressByName(ServiceName); + if (expectedPrimary != null && expectedPrimary.ToString() != primary.ToString()) { while (sw.Elapsed < duration.Value) { await Task.Delay(1000).ForAwait(); try { - master = SentinelServerA.SentinelGetMasterAddressByName(ServiceName); - if (expectedMaster.ToString() == master.ToString()) + primary = SentinelServerA.SentinelGetMasterAddressByName(ServiceName); + if (expectedPrimary.ToString() == primary.ToString()) break; } catch (Exception) @@ -92,13 +92,13 @@ protected async Task WaitForReadyAsync(EndPoint expectedMaster = null, bool wait } } } - if (expectedMaster != null && expectedMaster.ToString() != master.ToString()) - throw new RedisException($"Master was expected to be {expectedMaster}"); - Log($"Master is {master}"); + if (expectedPrimary != null && expectedPrimary.ToString() != primary.ToString()) + throw new RedisException($"Primary was expected to be {expectedPrimary}"); + Log($"Primary is {primary}"); using var checkConn = Conn.GetSentinelMasterConnection(ServiceOptions); - await WaitForRoleAsync(checkConn.GetServer(master), "master", duration.Value.Subtract(sw.Elapsed)).ForAwait(); + await WaitForRoleAsync(checkConn.GetServer(primary), "master", duration.Value.Subtract(sw.Elapsed)).ForAwait(); var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); if (replicas.Length > 0) @@ -110,7 +110,7 @@ protected async Task WaitForReadyAsync(EndPoint expectedMaster = null, bool wait if (waitForReplication) { - await WaitForReplicationAsync(checkConn.GetServer(master), duration.Value.Subtract(sw.Elapsed)).ForAwait(); + await WaitForReplicationAsync(checkConn.GetServer(primary), duration.Value.Subtract(sw.Elapsed)).ForAwait(); } } @@ -141,48 +141,48 @@ protected async Task WaitForRoleAsync(IServer server, string role, TimeSpan? dur throw new RedisException($"Timeout waiting for server ({server.EndPoint}) to have expected role (\"{role}\") assigned"); } - protected async Task WaitForReplicationAsync(IServer master, TimeSpan? duration = null) + protected async Task WaitForReplicationAsync(IServer primary, TimeSpan? duration = null) { duration ??= TimeSpan.FromSeconds(10); - static void LogEndpoints(IServer master, Action log) + static void LogEndpoints(IServer primary, Action log) { - if (master.Multiplexer is ConnectionMultiplexer muxer) + if (primary.Multiplexer is ConnectionMultiplexer muxer) { var serverEndpoints = muxer.GetServerSnapshot(); log("Endpoints:"); foreach (var serverEndpoint in serverEndpoints) { log($" {serverEndpoint}:"); - var server = master.Multiplexer.GetServer(serverEndpoint.EndPoint); + var server = primary.Multiplexer.GetServer(serverEndpoint.EndPoint); log($" Server: (Connected={server.IsConnected}, Type={server.ServerType}, IsReplica={server.IsReplica}, Unselectable={serverEndpoint.GetUnselectableFlags()})"); } } } - Log("Waiting for master/replica replication to be in sync..."); + Log("Waiting for primary/replica replication to be in sync..."); var sw = Stopwatch.StartNew(); while (sw.Elapsed < duration.Value) { - var info = master.Info("replication"); + var info = primary.Info("replication"); var replicationInfo = info.FirstOrDefault(f => f.Key == "Replication")?.ToArray().ToDictionary(); var replicaInfo = replicationInfo?.FirstOrDefault(i => i.Key.StartsWith("slave")).Value?.Split(',').ToDictionary(i => i.Split('=').First(), i => i.Split('=').Last()); var replicaOffset = replicaInfo?["offset"]; - var masterOffset = replicationInfo?["master_repl_offset"]; + var primaryOffset = replicationInfo?["master_repl_offset"]; - if (replicaOffset == masterOffset) + if (replicaOffset == primaryOffset) { - Log($"Done waiting for master ({masterOffset}) / replica ({replicaOffset}) replication to be in sync"); - LogEndpoints(master, Log); + Log($"Done waiting for primary ({primaryOffset}) / replica ({replicaOffset}) replication to be in sync"); + LogEndpoints(primary, Log); return; } - Log($"Waiting for master ({masterOffset}) / replica ({replicaOffset}) replication to be in sync..."); + Log($"Waiting for primary ({primaryOffset}) / replica ({replicaOffset}) replication to be in sync..."); await Task.Delay(250).ForAwait(); } - throw new RedisException("Timeout waiting for test servers master/replica replication to be in sync."); + throw new RedisException("Timeout waiting for test servers primary/replica replication to be in sync."); } } } diff --git a/tests/StackExchange.Redis.Tests/SentinelFailover.cs b/tests/StackExchange.Redis.Tests/SentinelFailover.cs index 7be012c5d..81971925a 100644 --- a/tests/StackExchange.Redis.Tests/SentinelFailover.cs +++ b/tests/StackExchange.Redis.Tests/SentinelFailover.cs @@ -13,7 +13,7 @@ public class SentinelFailover : SentinelBase, IAsyncLifetime public SentinelFailover(ITestOutputHelper output) : base(output) { } [Fact] - public async Task ManagedMasterConnectionEndToEndWithFailoverTest() + public async Task ManagedPrimaryConnectionEndToEndWithFailoverTest() { var connectionString = $"{TestConfig.Current.SentinelServer}:{TestConfig.Current.SentinelPortA},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; var conn = await ConnectionMultiplexer.ConnectAsync(connectionString); @@ -30,13 +30,13 @@ public async Task ManagedMasterConnectionEndToEndWithFailoverTest() var servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); Assert.Equal(2, servers.Length); - var master = servers.FirstOrDefault(s => !s.IsReplica); - Assert.NotNull(master); + var primary = servers.FirstOrDefault(s => !s.IsReplica); + Assert.NotNull(primary); var replica = servers.FirstOrDefault(s => s.IsReplica); Assert.NotNull(replica); - Assert.NotEqual(master.EndPoint.ToString(), replica.EndPoint.ToString()); + Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString()); - // set string value on current master + // Set string value on current primary var expected = DateTime.Now.Ticks.ToString(); Log("Tick Key: " + expected); var key = Me(); @@ -63,7 +63,7 @@ public async Task ManagedMasterConnectionEndToEndWithFailoverTest() SentinelServerA.SentinelFailover(ServiceName); // There's no point in doing much for 10 seconds - this is a built-in delay of how Sentinel works. - // The actual completion invoking the replication of the former master is handled via + // The actual completion invoking the replication of the former primary is handled via // https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L4799-L4808 // ...which is invoked by INFO polls every 10 seconds (https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L81) // ...which is calling https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L2666 @@ -71,9 +71,9 @@ public async Task ManagedMasterConnectionEndToEndWithFailoverTest() // So...we're waiting 10 seconds, no matter what. Might as well just idle to be more stable. await Task.Delay(TimeSpan.FromSeconds(10)); - // wait until the replica becomes the master + // wait until the replica becomes the primary Log("Waiting for ready post-failover..."); - await WaitForReadyAsync(expectedMaster: replicas[0]); + await WaitForReadyAsync(expectedPrimary: replicas[0]); Log($"Time to failover: {sw.Elapsed}"); endpoints = conn.GetEndPoints(); @@ -82,20 +82,20 @@ public async Task ManagedMasterConnectionEndToEndWithFailoverTest() servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); Assert.Equal(2, servers.Length); - var newMaster = servers.FirstOrDefault(s => !s.IsReplica); - Assert.NotNull(newMaster); - Assert.Equal(replica.EndPoint.ToString(), newMaster.EndPoint.ToString()); + var newPrimary = servers.FirstOrDefault(s => !s.IsReplica); + Assert.NotNull(newPrimary); + Assert.Equal(replica.EndPoint.ToString(), newPrimary.EndPoint.ToString()); var newReplica = servers.FirstOrDefault(s => s.IsReplica); Assert.NotNull(newReplica); - Assert.Equal(master.EndPoint.ToString(), newReplica.EndPoint.ToString()); - Assert.NotEqual(master.EndPoint.ToString(), replica.EndPoint.ToString()); + Assert.Equal(primary.EndPoint.ToString(), newReplica.EndPoint.ToString()); + Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString()); value = await db.StringGetAsync(key); Assert.Equal(expected, value); Log("Waiting for second replication check..."); // force read from replica, replication has some lag - await WaitForReplicationAsync(newMaster).ForAwait(); + await WaitForReplicationAsync(newPrimary).ForAwait(); value = await db.StringGetAsync(key, CommandFlags.DemandReplica); Assert.Equal(expected, value); } diff --git a/tests/StackExchange.Redis.Tests/Sets.cs b/tests/StackExchange.Redis.Tests/Sets.cs index c028f1cc9..815d8d78e 100644 --- a/tests/StackExchange.Redis.Tests/Sets.cs +++ b/tests/StackExchange.Redis.Tests/Sets.cs @@ -16,7 +16,7 @@ public void SScan() { using (var conn = Create()) { - var server = GetAnyMaster(conn); + var server = GetAnyPrimary(conn); RedisKey key = Me(); var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/Sockets.cs b/tests/StackExchange.Redis.Tests/Sockets.cs index 252e4650f..67b5b573f 100644 --- a/tests/StackExchange.Redis.Tests/Sockets.cs +++ b/tests/StackExchange.Redis.Tests/Sockets.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis.Tests { public class Sockets : TestBase { - protected override string GetConfiguration() => TestConfig.Current.MasterServerAndPort; + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; public Sockets(ITestOutputHelper output) : base (output) { } [FactLongRunning] diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index f5dfd8ae2..8ddb344a1 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -21,7 +21,7 @@ public abstract class TestBase : IDisposable protected TextWriterOutputHelper Writer { get; } protected static bool RunningInCI { get; } = Environment.GetEnvironmentVariable("APPVEYOR") != null; protected virtual string GetConfiguration() => GetDefaultConfiguration(); - internal static string GetDefaultConfiguration() => TestConfig.Current.MasterServerAndPort; + internal static string GetDefaultConfiguration() => TestConfig.Current.PrimaryServerAndPort; private readonly SharedConnectionFixture _fixture; @@ -227,21 +227,21 @@ protected static IServer GetServer(IConnectionMultiplexer muxer) { var server = muxer.GetServer(endpoint); if (server.IsReplica || !server.IsConnected) continue; - if (result != null) throw new InvalidOperationException("Requires exactly one master endpoint (found " + server.EndPoint + " and " + result.EndPoint + ")"); + if (result != null) throw new InvalidOperationException("Requires exactly one primary endpoint (found " + server.EndPoint + " and " + result.EndPoint + ")"); result = server; } - if (result == null) throw new InvalidOperationException("Requires exactly one master endpoint (found none)"); + if (result == null) throw new InvalidOperationException("Requires exactly one primary endpoint (found none)"); return result; } - protected static IServer GetAnyMaster(IConnectionMultiplexer muxer) + protected static IServer GetAnyPrimary(IConnectionMultiplexer muxer) { foreach (var endpoint in muxer.GetEndPoints()) { var server = muxer.GetServer(endpoint); if (!server.IsReplica) return server; } - throw new InvalidOperationException("Requires a master endpoint (found none)"); + throw new InvalidOperationException("Requires a primary endpoint (found none)"); } internal virtual IInternalConnectionMultiplexer Create( diff --git a/tests/StackExchange.Redis.Tests/TestConfig.json b/tests/StackExchange.Redis.Tests/TestConfig.json index 460c7c5ca..c652a4583 100644 --- a/tests/StackExchange.Redis.Tests/TestConfig.json +++ b/tests/StackExchange.Redis.Tests/TestConfig.json @@ -1,6 +1,6 @@ { //"LogToConsole": false, - //"MasterServer": "[::1]", + //"PrimaryServer": "[::1]", //"ReplicaServer": "[::1]", //"SecureServer": "[::1]" } \ No newline at end of file From 7ff36c5eb6a0d452c62e8c66369c182207fbd7af Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 9 Mar 2022 22:11:45 -0500 Subject: [PATCH 101/435] Add ConfigurationOptions.BeforeSocketConnect (#2031) This adds a new config `Action` that allows modification of a socket after creating it and before connecting. It's passing the endpoint and connection type as well so if necessary per-connection options can be set. Resolves #1472. --- docs/Configuration.md | 4 +++ docs/ReleaseNotes.md | 1 + .../ConfigurationOptions.cs | 9 +++++ src/StackExchange.Redis/PhysicalConnection.cs | 3 +- tests/StackExchange.Redis.Tests/Config.cs | 34 +++++++++++++++++++ 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index b17c623f4..0590edd54 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -100,7 +100,11 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a Additional code-only options: - ReconnectRetryPolicy (`IReconnectRetryPolicy`) - Default: `ReconnectRetryPolicy = ExponentialRetry(ConnectTimeout / 2);` + - Determines how often a multiplexer will try to reconnect after a failure - BacklogPolicy - Default: `BacklogPolicy = BacklogPolicy.Default;` + - Determines how commands will be queued (or not) during a disconnect, for sending when it's available again +- BeforeSocketConnect - Default: `null` + - Allows modifying a `Socket` before connecting (for advanced scenarios) Tokens in the configuration string are comma-separated; any without an `=` sign are assumed to be redis server endpoints. Endpoints without an explicit port will use 6379 if ssl is not enabled, and 6380 if ssl is enabled. Tokens starting with `$` are taken to represent command maps, for example: `$config=cfg`. diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 9cbce2f96..a6f8cdb08 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -3,6 +3,7 @@ ## Unreleased - Fix [#1988](https://github.com/StackExchange/StackExchange.Redis/issues/1988): Don't issue `SELECT` commands if explicitly disabled ([#2023 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2023)) +- Adds: `ConfigurationOptions.BeforeSocketConnect` for configuring sockets between creation and connection ([#2031 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2031)) ## 2.5.43 diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index f0fdb733e..ce220b257 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using System.Net.Security; +using System.Net.Sockets; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -171,6 +172,13 @@ public DefaultOptionsProvider Defaults set => defaultOptions = value; } + /// + /// Allows modification of a between creation and connection. + /// Passed in is the endpoint we're connecting to, which type of connection it is, and the socket itself. + /// For example, a specific local IP endpoint could be bound, linger time altered, etc. + /// + public Action BeforeSocketConnect { get; set; } + internal Func, Task> AfterConnectAsync => Defaults.AfterConnectAsync; /// @@ -557,6 +565,7 @@ public ConfigurationOptions Clone() BacklogPolicy = backlogPolicy, SslProtocols = SslProtocols, checkCertificateRevocation = checkCertificateRevocation, + BeforeSocketConnect = BeforeSocketConnect, }; foreach (var item in EndPoints) { diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 2187515e3..f81cbf0b4 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -69,7 +69,7 @@ internal void GetBytes(out long sent, out long received) internal bool HasOutputPipe => _ioPipe?.Output != null; private Socket _socket; - private Socket VolatileSocket => Volatile.Read(ref _socket); + internal Socket VolatileSocket => Volatile.Read(ref _socket); public PhysicalConnection(PhysicalBridge bridge) { @@ -96,6 +96,7 @@ internal async Task BeginConnectAsync(LogProxy log) Trace("Connecting..."); _socket = SocketManager.CreateSocket(endpoint); + bridge.Multiplexer.RawConfig.BeforeSocketConnect?.Invoke(endpoint, bridge.ConnectionType, _socket); bridge.Multiplexer.OnConnecting(endpoint, bridge.ConnectionType); log?.WriteLine($"{Format.ToString(endpoint)}: BeginConnectAsync"); diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index d579f4c0a..653f91f34 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.Sockets; using System.Security.Authentication; +using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -535,5 +536,38 @@ public void Apply() Assert.Equal(randomName, result.ClientName); Assert.Equal(result, options); } + + [Fact] + public void BeforeSocketConnect() + { + var options = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort); + int count = 0; + options.BeforeSocketConnect = (endpoint, connType, socket) => + { + Interlocked.Increment(ref count); + Log($"Endpoint: {endpoint}, ConnType: {connType}, Socket: {socket}"); + socket.DontFragment = true; + socket.Ttl = (short)(connType == ConnectionType.Interactive ? 12 : 123); + }; + var muxer = ConnectionMultiplexer.Connect(options); + Assert.True(muxer.IsConnected); + Assert.Equal(2, count); + + var endpoint = muxer.GetServerSnapshot()[0]; + var interactivePhysical = endpoint.GetBridge(ConnectionType.Interactive).TryConnect(null); + var subscriptionPhysical = endpoint.GetBridge(ConnectionType.Subscription).TryConnect(null); + Assert.NotNull(interactivePhysical); + Assert.NotNull(subscriptionPhysical); + + var interactiveSocket = interactivePhysical.VolatileSocket; + var subscriptionSocket = subscriptionPhysical.VolatileSocket; + Assert.NotNull(interactiveSocket); + Assert.NotNull(subscriptionSocket); + + Assert.Equal(12, interactiveSocket.Ttl); + Assert.Equal(123, subscriptionSocket.Ttl); + Assert.True(interactiveSocket.DontFragment); + Assert.True(subscriptionSocket.DontFragment); + } } } From ffebc5834e35a4dd48f27c8dd9d7ef4a5ea6f481 Mon Sep 17 00:00:00 2001 From: NicoAvanzDev <35104310+NicoAvanzDev@users.noreply.github.com> Date: Thu, 10 Mar 2022 04:32:56 +0100 Subject: [PATCH 102/435] Allow XTRIM maxLength equal 0 (#2030) Since "XTRIM key MAXLEN 0" is allowed by Redis, it may be useful remove the coded constraint "MAXLEN > 0" in order to clean the whole stream. --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/RedisDatabase.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index a6f8cdb08..53e00ecae 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -3,6 +3,7 @@ ## Unreleased - Fix [#1988](https://github.com/StackExchange/StackExchange.Redis/issues/1988): Don't issue `SELECT` commands if explicitly disabled ([#2023 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2023)) +- Fix: Allow `XTRIM` `MAXLEN` argument to be `0` ([#2030 by NicoAvanzDev](https://github.com/StackExchange/StackExchange.Redis/pull/2030)) - Adds: `ConfigurationOptions.BeforeSocketConnect` for configuring sockets between creation and connection ([#2031 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2031)) ## 2.5.43 diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index b951bef0a..1aa3a84d0 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -3402,9 +3402,9 @@ private Message GetSingleStreamReadMessage(RedisKey key, RedisValue afterId, int private Message GetStreamTrimMessage(RedisKey key, int maxLength, bool useApproximateMaxLength, CommandFlags flags) { - if (maxLength <= 0) + if (maxLength < 0) { - throw new ArgumentOutOfRangeException(nameof(maxLength), "maxLength must be greater than 0."); + throw new ArgumentOutOfRangeException(nameof(maxLength), "maxLength must be equal to or greater than 0."); } var values = new RedisValue[2 + (useApproximateMaxLength ? 1 : 0)]; From 6a047ef536e3cc255eca87604dbde2d51a37de9b Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 10 Mar 2022 20:38:31 -0500 Subject: [PATCH 103/435] Solution: Centralized package versions and test/toy dependency updates (#2034) This migrates to Directory.Pacakges.props and bumps versions of things we don't want to cause binding redirect pain on. --- .github/.github.csproj | 4 +-- Directory.Build.props | 3 +- Directory.Build.targets | 25 ----------------- Directory.Packages.props | 28 +++++++++++++++++++ StackExchange.Redis.sln | 1 + src/Directory.Build.props | 6 ++-- .../BasicTestBaseline.csproj | 2 +- .../ConsoleTestBaseline.csproj | 4 +-- .../StackExchange.Redis.Server.csproj | 2 +- .../TestConsoleBaseline.csproj | 2 +- 10 files changed, 40 insertions(+), 37 deletions(-) create mode 100644 Directory.Packages.props diff --git a/.github/.github.csproj b/.github/.github.csproj index 5a3b2f1f1..008099327 100644 --- a/.github/.github.csproj +++ b/.github/.github.csproj @@ -1,5 +1,5 @@ - + - netcoreapp3.1 + net6.0 \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index e66dd6b0a..b01dc7c53 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -25,6 +25,7 @@ false true false + true true @@ -35,6 +36,6 @@ - + diff --git a/Directory.Build.targets b/Directory.Build.targets index b52c8705b..687e19684 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,30 +1,5 @@ - - - $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..b3bffc5c6 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index 54de8bdb0..71e2d5f90 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution build.ps1 = build.ps1 Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets + Directory.Packages.props = Directory.Packages.props global.json = global.json NuGet.Config = NuGet.Config README.md = README.md diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 93251be41..8af74b69c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,8 +5,8 @@ true - - - + + + diff --git a/tests/BasicTestBaseline/BasicTestBaseline.csproj b/tests/BasicTestBaseline/BasicTestBaseline.csproj index 571261f73..71fd51459 100644 --- a/tests/BasicTestBaseline/BasicTestBaseline.csproj +++ b/tests/BasicTestBaseline/BasicTestBaseline.csproj @@ -16,7 +16,7 @@ - + diff --git a/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj b/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj index 130f9bf9e..dc644561d 100644 --- a/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj +++ b/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj @@ -12,8 +12,6 @@ - - - + diff --git a/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj b/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj index b1c16a9c1..1fba90d7f 100644 --- a/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj +++ b/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj @@ -14,6 +14,6 @@ - + diff --git a/toys/TestConsoleBaseline/TestConsoleBaseline.csproj b/toys/TestConsoleBaseline/TestConsoleBaseline.csproj index a9d0162e7..4368a8274 100644 --- a/toys/TestConsoleBaseline/TestConsoleBaseline.csproj +++ b/toys/TestConsoleBaseline/TestConsoleBaseline.csproj @@ -14,6 +14,6 @@ - + From 45b2ba144eeff8cbf8a6ded35bddfc1d27ef04ec Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 16 Mar 2022 08:44:49 -0400 Subject: [PATCH 104/435] ConnectionMultiplexer.Factory: cleanup (#2039) This option isn't used anymore - nuke it (non-API beak). --- src/StackExchange.Redis/ConnectionMultiplexer.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 032b413d5..071235edc 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -74,8 +74,6 @@ public static bool GetFeatureFlag(string flag) internal static bool PreventThreadTheft => (s_featureFlags & FeatureFlags.PreventThreadTheft) != 0; - private static TaskFactory _factory = null; - #if DEBUG private static int _collectedWithoutDispose; internal static int CollectedWithoutDispose => Thread.VolatileRead(ref _collectedWithoutDispose); @@ -116,15 +114,13 @@ bool IInternalConnectionMultiplexer.IgnoreConnect internal int _connectAttemptCount = 0, _connectCompletedCount = 0, _connectionCloseCount = 0; /// - /// Provides a way of overriding the default Task Factory. - /// If not set, it will use the default . - /// Useful when top level code sets it's own factory which may interfere with Redis queries. + /// No longer used. /// [Obsolete("No longer used, will be removed in 3.0.")] public static TaskFactory Factory { - get => _factory ?? Task.Factory; - set => _factory = value; + get => Task.Factory; + set { } } /// From d62b01588f85eb81f4e91853ee648674d6154f01 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 16 Mar 2022 20:48:05 -0400 Subject: [PATCH 105/435] Fix for #1813 - don't actually connect to a null endpoint (#2042) We log this, but then forgot to abort, woops. Actually bail in this case, no reason to rapidly issue sockets to nowhere. --- src/StackExchange.Redis/PhysicalConnection.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index f81cbf0b4..f1e03b654 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -92,6 +92,7 @@ internal async Task BeginConnectAsync(LogProxy log) if (endpoint == null) { log?.WriteLine("No endpoint"); + return; } Trace("Connecting..."); From 4ed7cc46518f98b934b306fcf1be615bf3e77241 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 18 Mar 2022 08:04:04 -0400 Subject: [PATCH 106/435] Project maintenance: public API tracking (#2043) This tracks public API changes to ensure we don't break and makes additions or removals visible in PRs. Also moves the InternalsVisibleTo into the csproj. --- Directory.Packages.props | 1 + src/Directory.Build.props | 7 +- src/StackExchange.Redis/AssemblyInfoHack.cs | 3 - src/StackExchange.Redis/PublicAPI.Shipped.txt | 1594 +++++++++++++++++ .../PublicAPI.Unshipped.txt | 1 + .../StackExchange.Redis.csproj | 5 + 6 files changed, 1605 insertions(+), 6 deletions(-) create mode 100644 src/StackExchange.Redis/PublicAPI.Shipped.txt create mode 100644 src/StackExchange.Redis/PublicAPI.Unshipped.txt diff --git a/Directory.Packages.props b/Directory.Packages.props index b3bffc5c6..67cd6e2df 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,6 +11,7 @@ + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 8af74b69c..c50acad80 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,9 +4,10 @@ true true - - - + + + + diff --git a/src/StackExchange.Redis/AssemblyInfoHack.cs b/src/StackExchange.Redis/AssemblyInfoHack.cs index 71560c853..50cdc2c1c 100644 --- a/src/StackExchange.Redis/AssemblyInfoHack.cs +++ b/src/StackExchange.Redis/AssemblyInfoHack.cs @@ -2,8 +2,5 @@ // your version numbers. Therefore, we need to move the attribute out into another file...this file. // When .csproj merges in, this should be able to return to Properties/AssemblyInfo.cs using System; -using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("StackExchange.Redis.Server, PublicKey=00240000048000009400000006020000002400005253413100040000010001007791a689e9d8950b44a9a8886baad2ea180e7a8a854f158c9b98345ca5009cdd2362c84f368f1c3658c132b3c0f74e44ff16aeb2e5b353b6e0fe02f923a050470caeac2bde47a2238a9c7125ed7dab14f486a5a64558df96640933b9f2b6db188fc4a820f96dce963b662fa8864adbff38e5b4542343f162ecdc6dad16912fff")] -[assembly: InternalsVisibleTo("StackExchange.Redis.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001007791a689e9d8950b44a9a8886baad2ea180e7a8a854f158c9b98345ca5009cdd2362c84f368f1c3658c132b3c0f74e44ff16aeb2e5b353b6e0fe02f923a050470caeac2bde47a2238a9c7125ed7dab14f486a5a64558df96640933b9f2b6db188fc4a820f96dce963b662fa8864adbff38e5b4542343f162ecdc6dad16912fff")] [assembly: CLSCompliant(true)] diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt new file mode 100644 index 000000000..0f4087370 --- /dev/null +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -0,0 +1,1594 @@ +abstract StackExchange.Redis.RedisResult.IsNull.get -> bool +abstract StackExchange.Redis.RedisResult.Type.get -> StackExchange.Redis.ResultType +override StackExchange.Redis.ChannelMessage.Equals(object obj) -> bool +override StackExchange.Redis.ChannelMessage.GetHashCode() -> int +override StackExchange.Redis.ChannelMessage.ToString() -> string +override StackExchange.Redis.ChannelMessageQueue.ToString() -> string +override StackExchange.Redis.ClientInfo.ToString() -> string +override StackExchange.Redis.ClusterNode.Equals(object obj) -> bool +override StackExchange.Redis.ClusterNode.GetHashCode() -> int +override StackExchange.Redis.ClusterNode.ToString() -> string +override StackExchange.Redis.CommandMap.ToString() -> string +override StackExchange.Redis.Configuration.AzureOptionsProvider.AbortOnConnectFail.get -> bool +override StackExchange.Redis.Configuration.AzureOptionsProvider.AfterConnectAsync(StackExchange.Redis.ConnectionMultiplexer muxer, System.Action log) -> System.Threading.Tasks.Task +override StackExchange.Redis.Configuration.AzureOptionsProvider.DefaultVersion.get -> System.Version +override StackExchange.Redis.Configuration.AzureOptionsProvider.GetDefaultSsl(StackExchange.Redis.EndPointCollection endPoints) -> bool +override StackExchange.Redis.Configuration.AzureOptionsProvider.IsMatch(System.Net.EndPoint endpoint) -> bool +override StackExchange.Redis.ConfigurationOptions.ToString() -> string +override StackExchange.Redis.ConnectionCounters.ToString() -> string +override StackExchange.Redis.ConnectionFailedEventArgs.ToString() -> string +override StackExchange.Redis.ConnectionMultiplexer.ToString() -> string +override StackExchange.Redis.GeoEntry.Equals(object obj) -> bool +override StackExchange.Redis.GeoEntry.GetHashCode() -> int +override StackExchange.Redis.GeoEntry.ToString() -> string +override StackExchange.Redis.GeoPosition.Equals(object obj) -> bool +override StackExchange.Redis.GeoPosition.GetHashCode() -> int +override StackExchange.Redis.GeoPosition.ToString() -> string +override StackExchange.Redis.GeoRadiusResult.ToString() -> string +override StackExchange.Redis.HashEntry.Equals(object obj) -> bool +override StackExchange.Redis.HashEntry.GetHashCode() -> int +override StackExchange.Redis.HashEntry.ToString() -> string +override StackExchange.Redis.Maintenance.ServerMaintenanceEvent.ToString() -> string +override StackExchange.Redis.NameValueEntry.Equals(object obj) -> bool +override StackExchange.Redis.NameValueEntry.GetHashCode() -> int +override StackExchange.Redis.NameValueEntry.ToString() -> string +override StackExchange.Redis.RedisChannel.Equals(object obj) -> bool +override StackExchange.Redis.RedisChannel.GetHashCode() -> int +override StackExchange.Redis.RedisChannel.ToString() -> string +override StackExchange.Redis.RedisConnectionException.GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) -> void +override StackExchange.Redis.RedisFeatures.Equals(object obj) -> bool +override StackExchange.Redis.RedisFeatures.GetHashCode() -> int +override StackExchange.Redis.RedisFeatures.ToString() -> string +override StackExchange.Redis.RedisKey.Equals(object obj) -> bool +override StackExchange.Redis.RedisKey.GetHashCode() -> int +override StackExchange.Redis.RedisKey.ToString() -> string +override StackExchange.Redis.RedisTimeoutException.GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) -> void +override StackExchange.Redis.RedisValue.Equals(object obj) -> bool +override StackExchange.Redis.RedisValue.GetHashCode() -> int +override StackExchange.Redis.RedisValue.ToString() -> string +override StackExchange.Redis.Role.ToString() -> string +override StackExchange.Redis.ServerCounters.ToString() -> string +override StackExchange.Redis.SlotRange.Equals(object obj) -> bool +override StackExchange.Redis.SlotRange.GetHashCode() -> int +override StackExchange.Redis.SlotRange.ToString() -> string +override StackExchange.Redis.SocketManager.ToString() -> string +override StackExchange.Redis.SortedSetEntry.Equals(object obj) -> bool +override StackExchange.Redis.SortedSetEntry.GetHashCode() -> int +override StackExchange.Redis.SortedSetEntry.ToString() -> string +StackExchange.Redis.Aggregate +StackExchange.Redis.Aggregate.Max = 2 -> StackExchange.Redis.Aggregate +StackExchange.Redis.Aggregate.Min = 1 -> StackExchange.Redis.Aggregate +StackExchange.Redis.Aggregate.Sum = 0 -> StackExchange.Redis.Aggregate +StackExchange.Redis.BacklogPolicy +StackExchange.Redis.BacklogPolicy.AbortPendingOnConnectionFailure.get -> bool +StackExchange.Redis.BacklogPolicy.AbortPendingOnConnectionFailure.init -> void +StackExchange.Redis.BacklogPolicy.BacklogPolicy() -> void +StackExchange.Redis.BacklogPolicy.QueueWhileDisconnected.get -> bool +StackExchange.Redis.BacklogPolicy.QueueWhileDisconnected.init -> void +StackExchange.Redis.Bitwise +StackExchange.Redis.Bitwise.And = 0 -> StackExchange.Redis.Bitwise +StackExchange.Redis.Bitwise.Not = 3 -> StackExchange.Redis.Bitwise +StackExchange.Redis.Bitwise.Or = 1 -> StackExchange.Redis.Bitwise +StackExchange.Redis.Bitwise.Xor = 2 -> StackExchange.Redis.Bitwise +StackExchange.Redis.ChannelMessage +StackExchange.Redis.ChannelMessage.Channel.get -> StackExchange.Redis.RedisChannel +StackExchange.Redis.ChannelMessage.ChannelMessage() -> void +StackExchange.Redis.ChannelMessage.Message.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.ChannelMessage.SubscriptionChannel.get -> StackExchange.Redis.RedisChannel +StackExchange.Redis.ChannelMessageQueue +StackExchange.Redis.ChannelMessageQueue.Channel.get -> StackExchange.Redis.RedisChannel +StackExchange.Redis.ChannelMessageQueue.Completion.get -> System.Threading.Tasks.Task +StackExchange.Redis.ChannelMessageQueue.OnMessage(System.Action handler) -> void +StackExchange.Redis.ChannelMessageQueue.OnMessage(System.Func handler) -> void +StackExchange.Redis.ChannelMessageQueue.ReadAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +StackExchange.Redis.ChannelMessageQueue.TryGetCount(out int count) -> bool +StackExchange.Redis.ChannelMessageQueue.TryRead(out StackExchange.Redis.ChannelMessage item) -> bool +StackExchange.Redis.ChannelMessageQueue.Unsubscribe(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.ChannelMessageQueue.UnsubscribeAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.Blocked = 16 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.BroadcastTracking = 16384 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.CloseASAP = 256 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.Closing = 64 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.KeysTracking = 4096 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.Master = 4 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.None = 0 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.PubSubSubscriber = 512 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.ReadOnlyCluster = 1024 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.Replica = 2 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.ReplicaMonitor = 1 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.Slave = 2 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.SlaveMonitor = 1 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.TrackingTargetInvalid = 8192 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.Transaction = 8 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.TransactionDoomed = 32 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.Unblocked = 128 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientFlags.UnixDomainSocket = 2048 -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientInfo +StackExchange.Redis.ClientInfo.Address.get -> System.Net.EndPoint +StackExchange.Redis.ClientInfo.AgeSeconds.get -> int +StackExchange.Redis.ClientInfo.ClientInfo() -> void +StackExchange.Redis.ClientInfo.ClientType.get -> StackExchange.Redis.ClientType +StackExchange.Redis.ClientInfo.Database.get -> int +StackExchange.Redis.ClientInfo.Flags.get -> StackExchange.Redis.ClientFlags +StackExchange.Redis.ClientInfo.FlagsRaw.get -> string +StackExchange.Redis.ClientInfo.Host.get -> string +StackExchange.Redis.ClientInfo.Id.get -> long +StackExchange.Redis.ClientInfo.IdleSeconds.get -> int +StackExchange.Redis.ClientInfo.LastCommand.get -> string +StackExchange.Redis.ClientInfo.Name.get -> string +StackExchange.Redis.ClientInfo.PatternSubscriptionCount.get -> int +StackExchange.Redis.ClientInfo.Port.get -> int +StackExchange.Redis.ClientInfo.Raw.get -> string +StackExchange.Redis.ClientInfo.SubscriptionCount.get -> int +StackExchange.Redis.ClientInfo.TransactionCommandLength.get -> int +StackExchange.Redis.ClientType +StackExchange.Redis.ClientType.Normal = 0 -> StackExchange.Redis.ClientType +StackExchange.Redis.ClientType.PubSub = 2 -> StackExchange.Redis.ClientType +StackExchange.Redis.ClientType.Replica = 1 -> StackExchange.Redis.ClientType +StackExchange.Redis.ClientType.Slave = 1 -> StackExchange.Redis.ClientType +StackExchange.Redis.ClusterConfiguration +StackExchange.Redis.ClusterConfiguration.GetBySlot(int slot) -> StackExchange.Redis.ClusterNode +StackExchange.Redis.ClusterConfiguration.GetBySlot(StackExchange.Redis.RedisKey key) -> StackExchange.Redis.ClusterNode +StackExchange.Redis.ClusterConfiguration.Nodes.get -> System.Collections.Generic.ICollection +StackExchange.Redis.ClusterConfiguration.Origin.get -> System.Net.EndPoint +StackExchange.Redis.ClusterConfiguration.this[System.Net.EndPoint endpoint].get -> StackExchange.Redis.ClusterNode +StackExchange.Redis.ClusterNode +StackExchange.Redis.ClusterNode.Children.get -> System.Collections.Generic.IList +StackExchange.Redis.ClusterNode.CompareTo(StackExchange.Redis.ClusterNode other) -> int +StackExchange.Redis.ClusterNode.EndPoint.get -> System.Net.EndPoint +StackExchange.Redis.ClusterNode.Equals(StackExchange.Redis.ClusterNode other) -> bool +StackExchange.Redis.ClusterNode.IsConnected.get -> bool +StackExchange.Redis.ClusterNode.IsMyself.get -> bool +StackExchange.Redis.ClusterNode.IsNoAddr.get -> bool +StackExchange.Redis.ClusterNode.IsReplica.get -> bool +StackExchange.Redis.ClusterNode.IsSlave.get -> bool +StackExchange.Redis.ClusterNode.NodeId.get -> string +StackExchange.Redis.ClusterNode.Parent.get -> StackExchange.Redis.ClusterNode +StackExchange.Redis.ClusterNode.ParentNodeId.get -> string +StackExchange.Redis.ClusterNode.Raw.get -> string +StackExchange.Redis.ClusterNode.Slots.get -> System.Collections.Generic.IList +StackExchange.Redis.CommandFlags +StackExchange.Redis.CommandFlags.DemandMaster = 4 -> StackExchange.Redis.CommandFlags +StackExchange.Redis.CommandFlags.DemandReplica = StackExchange.Redis.CommandFlags.DemandMaster | StackExchange.Redis.CommandFlags.PreferReplica -> StackExchange.Redis.CommandFlags +StackExchange.Redis.CommandFlags.DemandSlave = StackExchange.Redis.CommandFlags.DemandMaster | StackExchange.Redis.CommandFlags.PreferReplica -> StackExchange.Redis.CommandFlags +StackExchange.Redis.CommandFlags.FireAndForget = 2 -> StackExchange.Redis.CommandFlags +StackExchange.Redis.CommandFlags.HighPriority = 1 -> StackExchange.Redis.CommandFlags +StackExchange.Redis.CommandFlags.None = 0 -> StackExchange.Redis.CommandFlags +StackExchange.Redis.CommandFlags.NoRedirect = 64 -> StackExchange.Redis.CommandFlags +StackExchange.Redis.CommandFlags.NoScriptCache = 512 -> StackExchange.Redis.CommandFlags +StackExchange.Redis.CommandFlags.PreferMaster = 0 -> StackExchange.Redis.CommandFlags +StackExchange.Redis.CommandFlags.PreferReplica = 8 -> StackExchange.Redis.CommandFlags +StackExchange.Redis.CommandFlags.PreferSlave = 8 -> StackExchange.Redis.CommandFlags +StackExchange.Redis.CommandMap +StackExchange.Redis.CommandStatus +StackExchange.Redis.CommandStatus.Sent = 2 -> StackExchange.Redis.CommandStatus +StackExchange.Redis.CommandStatus.Unknown = 0 -> StackExchange.Redis.CommandStatus +StackExchange.Redis.CommandStatus.WaitingToBeSent = 1 -> StackExchange.Redis.CommandStatus +StackExchange.Redis.CommandTrace +StackExchange.Redis.CommandTrace.Arguments.get -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.CommandTrace.Duration.get -> System.TimeSpan +StackExchange.Redis.CommandTrace.GetHelpUrl() -> string +StackExchange.Redis.CommandTrace.Time.get -> System.DateTime +StackExchange.Redis.CommandTrace.UniqueId.get -> long +StackExchange.Redis.Condition +StackExchange.Redis.ConditionResult +StackExchange.Redis.ConditionResult.WasSatisfied.get -> bool +StackExchange.Redis.Configuration.AzureOptionsProvider +StackExchange.Redis.Configuration.AzureOptionsProvider.AzureOptionsProvider() -> void +StackExchange.Redis.Configuration.DefaultOptionsProvider +StackExchange.Redis.Configuration.DefaultOptionsProvider.ClientName.get -> string +StackExchange.Redis.Configuration.DefaultOptionsProvider.DefaultOptionsProvider() -> void +StackExchange.Redis.ConfigurationOptions +StackExchange.Redis.ConfigurationOptions.AbortOnConnectFail.get -> bool +StackExchange.Redis.ConfigurationOptions.AbortOnConnectFail.set -> void +StackExchange.Redis.ConfigurationOptions.AllowAdmin.get -> bool +StackExchange.Redis.ConfigurationOptions.AllowAdmin.set -> void +StackExchange.Redis.ConfigurationOptions.Apply(System.Action configure) -> StackExchange.Redis.ConfigurationOptions +StackExchange.Redis.ConfigurationOptions.AsyncTimeout.get -> int +StackExchange.Redis.ConfigurationOptions.AsyncTimeout.set -> void +StackExchange.Redis.ConfigurationOptions.BacklogPolicy.get -> StackExchange.Redis.BacklogPolicy +StackExchange.Redis.ConfigurationOptions.BacklogPolicy.set -> void +StackExchange.Redis.ConfigurationOptions.BeforeSocketConnect.get -> System.Action +StackExchange.Redis.ConfigurationOptions.BeforeSocketConnect.set -> void +StackExchange.Redis.ConfigurationOptions.CertificateSelection -> System.Net.Security.LocalCertificateSelectionCallback +StackExchange.Redis.ConfigurationOptions.CertificateValidation -> System.Net.Security.RemoteCertificateValidationCallback +StackExchange.Redis.ConfigurationOptions.ChannelPrefix.get -> StackExchange.Redis.RedisChannel +StackExchange.Redis.ConfigurationOptions.ChannelPrefix.set -> void +StackExchange.Redis.ConfigurationOptions.CheckCertificateRevocation.get -> bool +StackExchange.Redis.ConfigurationOptions.CheckCertificateRevocation.set -> void +StackExchange.Redis.ConfigurationOptions.ClientName.get -> string +StackExchange.Redis.ConfigurationOptions.ClientName.set -> void +StackExchange.Redis.ConfigurationOptions.Clone() -> StackExchange.Redis.ConfigurationOptions +StackExchange.Redis.ConfigurationOptions.CommandMap.get -> StackExchange.Redis.CommandMap +StackExchange.Redis.ConfigurationOptions.CommandMap.set -> void +StackExchange.Redis.ConfigurationOptions.ConfigCheckSeconds.get -> int +StackExchange.Redis.ConfigurationOptions.ConfigCheckSeconds.set -> void +StackExchange.Redis.ConfigurationOptions.ConfigurationChannel.get -> string +StackExchange.Redis.ConfigurationOptions.ConfigurationChannel.set -> void +StackExchange.Redis.ConfigurationOptions.ConfigurationOptions() -> void +StackExchange.Redis.ConfigurationOptions.ConnectRetry.get -> int +StackExchange.Redis.ConfigurationOptions.ConnectRetry.set -> void +StackExchange.Redis.ConfigurationOptions.ConnectTimeout.get -> int +StackExchange.Redis.ConfigurationOptions.ConnectTimeout.set -> void +StackExchange.Redis.ConfigurationOptions.DefaultDatabase.get -> int? +StackExchange.Redis.ConfigurationOptions.DefaultDatabase.set -> void +StackExchange.Redis.ConfigurationOptions.Defaults.get -> StackExchange.Redis.Configuration.DefaultOptionsProvider +StackExchange.Redis.ConfigurationOptions.Defaults.set -> void +StackExchange.Redis.ConfigurationOptions.DefaultVersion.get -> System.Version +StackExchange.Redis.ConfigurationOptions.DefaultVersion.set -> void +StackExchange.Redis.ConfigurationOptions.EndPoints.get -> StackExchange.Redis.EndPointCollection +StackExchange.Redis.ConfigurationOptions.HighPrioritySocketThreads.get -> bool +StackExchange.Redis.ConfigurationOptions.HighPrioritySocketThreads.set -> void +StackExchange.Redis.ConfigurationOptions.KeepAlive.get -> int +StackExchange.Redis.ConfigurationOptions.KeepAlive.set -> void +StackExchange.Redis.ConfigurationOptions.Password.get -> string +StackExchange.Redis.ConfigurationOptions.Password.set -> void +StackExchange.Redis.ConfigurationOptions.PreserveAsyncOrder.get -> bool +StackExchange.Redis.ConfigurationOptions.PreserveAsyncOrder.set -> void +StackExchange.Redis.ConfigurationOptions.Proxy.get -> StackExchange.Redis.Proxy +StackExchange.Redis.ConfigurationOptions.Proxy.set -> void +StackExchange.Redis.ConfigurationOptions.ReconnectRetryPolicy.get -> StackExchange.Redis.IReconnectRetryPolicy +StackExchange.Redis.ConfigurationOptions.ReconnectRetryPolicy.set -> void +StackExchange.Redis.ConfigurationOptions.ResolveDns.get -> bool +StackExchange.Redis.ConfigurationOptions.ResolveDns.set -> void +StackExchange.Redis.ConfigurationOptions.ResponseTimeout.get -> int +StackExchange.Redis.ConfigurationOptions.ResponseTimeout.set -> void +StackExchange.Redis.ConfigurationOptions.ServiceName.get -> string +StackExchange.Redis.ConfigurationOptions.ServiceName.set -> void +StackExchange.Redis.ConfigurationOptions.SetDefaultPorts() -> void +StackExchange.Redis.ConfigurationOptions.SocketManager.get -> StackExchange.Redis.SocketManager +StackExchange.Redis.ConfigurationOptions.SocketManager.set -> void +StackExchange.Redis.ConfigurationOptions.Ssl.get -> bool +StackExchange.Redis.ConfigurationOptions.Ssl.set -> void +StackExchange.Redis.ConfigurationOptions.SslHost.get -> string +StackExchange.Redis.ConfigurationOptions.SslHost.set -> void +StackExchange.Redis.ConfigurationOptions.SslProtocols.get -> System.Security.Authentication.SslProtocols? +StackExchange.Redis.ConfigurationOptions.SslProtocols.set -> void +StackExchange.Redis.ConfigurationOptions.SyncTimeout.get -> int +StackExchange.Redis.ConfigurationOptions.SyncTimeout.set -> void +StackExchange.Redis.ConfigurationOptions.TieBreaker.get -> string +StackExchange.Redis.ConfigurationOptions.TieBreaker.set -> void +StackExchange.Redis.ConfigurationOptions.ToString(bool includePassword) -> string +StackExchange.Redis.ConfigurationOptions.TrustIssuer(string issuerCertificatePath) -> void +StackExchange.Redis.ConfigurationOptions.TrustIssuer(System.Security.Cryptography.X509Certificates.X509Certificate2 issuer) -> void +StackExchange.Redis.ConfigurationOptions.User.get -> string +StackExchange.Redis.ConfigurationOptions.User.set -> void +StackExchange.Redis.ConfigurationOptions.UseSsl.get -> bool +StackExchange.Redis.ConfigurationOptions.UseSsl.set -> void +StackExchange.Redis.ConfigurationOptions.WriteBuffer.get -> int +StackExchange.Redis.ConfigurationOptions.WriteBuffer.set -> void +StackExchange.Redis.ConnectionCounters +StackExchange.Redis.ConnectionCounters.CompletedAsynchronously.get -> long +StackExchange.Redis.ConnectionCounters.CompletedSynchronously.get -> long +StackExchange.Redis.ConnectionCounters.ConnectionType.get -> StackExchange.Redis.ConnectionType +StackExchange.Redis.ConnectionCounters.FailedAsynchronously.get -> long +StackExchange.Redis.ConnectionCounters.IsEmpty.get -> bool +StackExchange.Redis.ConnectionCounters.NonPreferredEndpointCount.get -> long +StackExchange.Redis.ConnectionCounters.OperationCount.get -> long +StackExchange.Redis.ConnectionCounters.PendingUnsentItems.get -> int +StackExchange.Redis.ConnectionCounters.ResponsesAwaitingAsyncCompletion.get -> int +StackExchange.Redis.ConnectionCounters.SentItemsAwaitingResponse.get -> int +StackExchange.Redis.ConnectionCounters.SocketCount.get -> long +StackExchange.Redis.ConnectionCounters.Subscriptions.get -> long +StackExchange.Redis.ConnectionCounters.TotalOutstanding.get -> int +StackExchange.Redis.ConnectionCounters.WriterCount.get -> int +StackExchange.Redis.ConnectionFailedEventArgs +StackExchange.Redis.ConnectionFailedEventArgs.ConnectionFailedEventArgs(object sender, System.Net.EndPoint endPoint, StackExchange.Redis.ConnectionType connectionType, StackExchange.Redis.ConnectionFailureType failureType, System.Exception exception, string physicalName) -> void +StackExchange.Redis.ConnectionFailedEventArgs.ConnectionType.get -> StackExchange.Redis.ConnectionType +StackExchange.Redis.ConnectionFailedEventArgs.EndPoint.get -> System.Net.EndPoint +StackExchange.Redis.ConnectionFailedEventArgs.Exception.get -> System.Exception +StackExchange.Redis.ConnectionFailedEventArgs.FailureType.get -> StackExchange.Redis.ConnectionFailureType +StackExchange.Redis.ConnectionFailureType +StackExchange.Redis.ConnectionFailureType.AuthenticationFailure = 3 -> StackExchange.Redis.ConnectionFailureType +StackExchange.Redis.ConnectionFailureType.ConnectionDisposed = 7 -> StackExchange.Redis.ConnectionFailureType +StackExchange.Redis.ConnectionFailureType.InternalFailure = 5 -> StackExchange.Redis.ConnectionFailureType +StackExchange.Redis.ConnectionFailureType.Loading = 8 -> StackExchange.Redis.ConnectionFailureType +StackExchange.Redis.ConnectionFailureType.None = 0 -> StackExchange.Redis.ConnectionFailureType +StackExchange.Redis.ConnectionFailureType.ProtocolFailure = 4 -> StackExchange.Redis.ConnectionFailureType +StackExchange.Redis.ConnectionFailureType.SocketClosed = 6 -> StackExchange.Redis.ConnectionFailureType +StackExchange.Redis.ConnectionFailureType.SocketFailure = 2 -> StackExchange.Redis.ConnectionFailureType +StackExchange.Redis.ConnectionFailureType.UnableToConnect = 9 -> StackExchange.Redis.ConnectionFailureType +StackExchange.Redis.ConnectionFailureType.UnableToResolvePhysicalConnection = 1 -> StackExchange.Redis.ConnectionFailureType +StackExchange.Redis.ConnectionMultiplexer +StackExchange.Redis.ConnectionMultiplexer.ClientName.get -> string +StackExchange.Redis.ConnectionMultiplexer.Close(bool allowCommandsToComplete = true) -> void +StackExchange.Redis.ConnectionMultiplexer.CloseAsync(bool allowCommandsToComplete = true) -> System.Threading.Tasks.Task +StackExchange.Redis.ConnectionMultiplexer.Configuration.get -> string +StackExchange.Redis.ConnectionMultiplexer.ConfigurationChanged -> System.EventHandler +StackExchange.Redis.ConnectionMultiplexer.ConfigurationChangedBroadcast -> System.EventHandler +StackExchange.Redis.ConnectionMultiplexer.Configure(System.IO.TextWriter log = null) -> bool +StackExchange.Redis.ConnectionMultiplexer.ConfigureAsync(System.IO.TextWriter log = null) -> System.Threading.Tasks.Task +StackExchange.Redis.ConnectionMultiplexer.ConnectionFailed -> System.EventHandler +StackExchange.Redis.ConnectionMultiplexer.ConnectionRestored -> System.EventHandler +StackExchange.Redis.ConnectionMultiplexer.Dispose() -> void +StackExchange.Redis.ConnectionMultiplexer.ErrorMessage -> System.EventHandler +StackExchange.Redis.ConnectionMultiplexer.ExportConfiguration(System.IO.Stream destination, StackExchange.Redis.ExportOptions options = (StackExchange.Redis.ExportOptions)-1) -> void +StackExchange.Redis.ConnectionMultiplexer.GetCounters() -> StackExchange.Redis.ServerCounters +StackExchange.Redis.ConnectionMultiplexer.GetDatabase(int db = -1, object asyncState = null) -> StackExchange.Redis.IDatabase +StackExchange.Redis.ConnectionMultiplexer.GetEndPoints(bool configuredOnly = false) -> System.Net.EndPoint[] +StackExchange.Redis.ConnectionMultiplexer.GetHashSlot(StackExchange.Redis.RedisKey key) -> int +StackExchange.Redis.ConnectionMultiplexer.GetSentinelMasterConnection(StackExchange.Redis.ConfigurationOptions config, System.IO.TextWriter log = null) -> StackExchange.Redis.ConnectionMultiplexer +StackExchange.Redis.ConnectionMultiplexer.GetServer(string host, int port, object asyncState = null) -> StackExchange.Redis.IServer +StackExchange.Redis.ConnectionMultiplexer.GetServer(string hostAndPort, object asyncState = null) -> StackExchange.Redis.IServer +StackExchange.Redis.ConnectionMultiplexer.GetServer(System.Net.EndPoint endpoint, object asyncState = null) -> StackExchange.Redis.IServer +StackExchange.Redis.ConnectionMultiplexer.GetServer(System.Net.IPAddress host, int port) -> StackExchange.Redis.IServer +StackExchange.Redis.ConnectionMultiplexer.GetStatus() -> string +StackExchange.Redis.ConnectionMultiplexer.GetStatus(System.IO.TextWriter log) -> void +StackExchange.Redis.ConnectionMultiplexer.GetStormLog() -> string +StackExchange.Redis.ConnectionMultiplexer.GetSubscriber(object asyncState = null) -> StackExchange.Redis.ISubscriber +StackExchange.Redis.ConnectionMultiplexer.HashSlot(StackExchange.Redis.RedisKey key) -> int +StackExchange.Redis.ConnectionMultiplexer.HashSlotMoved -> System.EventHandler +StackExchange.Redis.ConnectionMultiplexer.IncludeDetailInExceptions.get -> bool +StackExchange.Redis.ConnectionMultiplexer.IncludeDetailInExceptions.set -> void +StackExchange.Redis.ConnectionMultiplexer.IncludePerformanceCountersInExceptions.get -> bool +StackExchange.Redis.ConnectionMultiplexer.IncludePerformanceCountersInExceptions.set -> void +StackExchange.Redis.ConnectionMultiplexer.InternalError -> System.EventHandler +StackExchange.Redis.ConnectionMultiplexer.IsConnected.get -> bool +StackExchange.Redis.ConnectionMultiplexer.IsConnecting.get -> bool +StackExchange.Redis.ConnectionMultiplexer.OperationCount.get -> long +StackExchange.Redis.ConnectionMultiplexer.PreserveAsyncOrder.get -> bool +StackExchange.Redis.ConnectionMultiplexer.PreserveAsyncOrder.set -> void +StackExchange.Redis.ConnectionMultiplexer.PublishReconfigure(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.ConnectionMultiplexer.PublishReconfigureAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.ConnectionMultiplexer.ReconfigureAsync(string reason) -> System.Threading.Tasks.Task +StackExchange.Redis.ConnectionMultiplexer.RegisterProfiler(System.Func profilingSessionProvider) -> void +StackExchange.Redis.ConnectionMultiplexer.ResetStormLog() -> void +StackExchange.Redis.ConnectionMultiplexer.ServerMaintenanceEvent -> System.EventHandler +StackExchange.Redis.ConnectionMultiplexer.StormLogThreshold.get -> int +StackExchange.Redis.ConnectionMultiplexer.StormLogThreshold.set -> void +StackExchange.Redis.ConnectionMultiplexer.TimeoutMilliseconds.get -> int +StackExchange.Redis.ConnectionMultiplexer.Wait(System.Threading.Tasks.Task task) -> void +StackExchange.Redis.ConnectionMultiplexer.Wait(System.Threading.Tasks.Task task) -> T +StackExchange.Redis.ConnectionMultiplexer.WaitAll(params System.Threading.Tasks.Task[] tasks) -> void +StackExchange.Redis.ConnectionType +StackExchange.Redis.ConnectionType.Interactive = 1 -> StackExchange.Redis.ConnectionType +StackExchange.Redis.ConnectionType.None = 0 -> StackExchange.Redis.ConnectionType +StackExchange.Redis.ConnectionType.Subscription = 2 -> StackExchange.Redis.ConnectionType +StackExchange.Redis.EndPointCollection +StackExchange.Redis.EndPointCollection.Add(string host, int port) -> void +StackExchange.Redis.EndPointCollection.Add(string hostAndPort) -> void +StackExchange.Redis.EndPointCollection.Add(System.Net.IPAddress host, int port) -> void +StackExchange.Redis.EndPointCollection.EndPointCollection() -> void +StackExchange.Redis.EndPointCollection.EndPointCollection(System.Collections.Generic.IList endpoints) -> void +StackExchange.Redis.EndPointCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator +StackExchange.Redis.EndPointCollection.TryAdd(System.Net.EndPoint endpoint) -> bool +StackExchange.Redis.EndPointEventArgs +StackExchange.Redis.EndPointEventArgs.EndPoint.get -> System.Net.EndPoint +StackExchange.Redis.EndPointEventArgs.EndPointEventArgs(object sender, System.Net.EndPoint endpoint) -> void +StackExchange.Redis.Exclude +StackExchange.Redis.Exclude.Both = StackExchange.Redis.Exclude.Start | StackExchange.Redis.Exclude.Stop -> StackExchange.Redis.Exclude +StackExchange.Redis.Exclude.None = 0 -> StackExchange.Redis.Exclude +StackExchange.Redis.Exclude.Start = 1 -> StackExchange.Redis.Exclude +StackExchange.Redis.Exclude.Stop = 2 -> StackExchange.Redis.Exclude +StackExchange.Redis.ExponentialRetry +StackExchange.Redis.ExponentialRetry.ExponentialRetry(int deltaBackOffMilliseconds) -> void +StackExchange.Redis.ExponentialRetry.ExponentialRetry(int deltaBackOffMilliseconds, int maxDeltaBackOffMilliseconds) -> void +StackExchange.Redis.ExponentialRetry.ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSinceLastRetry) -> bool +StackExchange.Redis.ExportOptions +StackExchange.Redis.ExportOptions.All = -1 -> StackExchange.Redis.ExportOptions +StackExchange.Redis.ExportOptions.Client = 4 -> StackExchange.Redis.ExportOptions +StackExchange.Redis.ExportOptions.Cluster = 8 -> StackExchange.Redis.ExportOptions +StackExchange.Redis.ExportOptions.Config = 2 -> StackExchange.Redis.ExportOptions +StackExchange.Redis.ExportOptions.Info = 1 -> StackExchange.Redis.ExportOptions +StackExchange.Redis.ExportOptions.None = 0 -> StackExchange.Redis.ExportOptions +StackExchange.Redis.ExtensionMethods +StackExchange.Redis.GeoEntry +StackExchange.Redis.GeoEntry.Equals(StackExchange.Redis.GeoEntry other) -> bool +StackExchange.Redis.GeoEntry.GeoEntry() -> void +StackExchange.Redis.GeoEntry.GeoEntry(double longitude, double latitude, StackExchange.Redis.RedisValue member) -> void +StackExchange.Redis.GeoEntry.Latitude.get -> double +StackExchange.Redis.GeoEntry.Longitude.get -> double +StackExchange.Redis.GeoEntry.Member.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.GeoEntry.Position.get -> StackExchange.Redis.GeoPosition +StackExchange.Redis.GeoPosition +StackExchange.Redis.GeoPosition.Equals(StackExchange.Redis.GeoPosition other) -> bool +StackExchange.Redis.GeoPosition.GeoPosition() -> void +StackExchange.Redis.GeoPosition.GeoPosition(double longitude, double latitude) -> void +StackExchange.Redis.GeoPosition.Latitude.get -> double +StackExchange.Redis.GeoPosition.Longitude.get -> double +StackExchange.Redis.GeoRadiusOptions +StackExchange.Redis.GeoRadiusOptions.Default = StackExchange.Redis.GeoRadiusOptions.WithCoordinates | StackExchange.Redis.GeoRadiusOptions.WithDistance -> StackExchange.Redis.GeoRadiusOptions +StackExchange.Redis.GeoRadiusOptions.None = 0 -> StackExchange.Redis.GeoRadiusOptions +StackExchange.Redis.GeoRadiusOptions.WithCoordinates = 1 -> StackExchange.Redis.GeoRadiusOptions +StackExchange.Redis.GeoRadiusOptions.WithDistance = 2 -> StackExchange.Redis.GeoRadiusOptions +StackExchange.Redis.GeoRadiusOptions.WithGeoHash = 4 -> StackExchange.Redis.GeoRadiusOptions +StackExchange.Redis.GeoRadiusResult +StackExchange.Redis.GeoRadiusResult.Distance.get -> double? +StackExchange.Redis.GeoRadiusResult.GeoRadiusResult() -> void +StackExchange.Redis.GeoRadiusResult.GeoRadiusResult(in StackExchange.Redis.RedisValue member, double? distance, long? hash, StackExchange.Redis.GeoPosition? position) -> void +StackExchange.Redis.GeoRadiusResult.Hash.get -> long? +StackExchange.Redis.GeoRadiusResult.Member.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.GeoRadiusResult.Position.get -> StackExchange.Redis.GeoPosition? +StackExchange.Redis.GeoUnit +StackExchange.Redis.GeoUnit.Feet = 3 -> StackExchange.Redis.GeoUnit +StackExchange.Redis.GeoUnit.Kilometers = 1 -> StackExchange.Redis.GeoUnit +StackExchange.Redis.GeoUnit.Meters = 0 -> StackExchange.Redis.GeoUnit +StackExchange.Redis.GeoUnit.Miles = 2 -> StackExchange.Redis.GeoUnit +StackExchange.Redis.HashEntry +StackExchange.Redis.HashEntry.Equals(StackExchange.Redis.HashEntry other) -> bool +StackExchange.Redis.HashEntry.HashEntry() -> void +StackExchange.Redis.HashEntry.HashEntry(StackExchange.Redis.RedisValue name, StackExchange.Redis.RedisValue value) -> void +StackExchange.Redis.HashEntry.Key.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.HashEntry.Name.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.HashEntry.Value.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.HashSlotMovedEventArgs +StackExchange.Redis.HashSlotMovedEventArgs.HashSlot.get -> int +StackExchange.Redis.HashSlotMovedEventArgs.HashSlotMovedEventArgs(object sender, int hashSlot, System.Net.EndPoint old, System.Net.EndPoint new) -> void +StackExchange.Redis.HashSlotMovedEventArgs.NewEndPoint.get -> System.Net.EndPoint +StackExchange.Redis.HashSlotMovedEventArgs.OldEndPoint.get -> System.Net.EndPoint +StackExchange.Redis.IBatch +StackExchange.Redis.IBatch.Execute() -> void +StackExchange.Redis.IConnectionMultiplexer +StackExchange.Redis.IConnectionMultiplexer.ClientName.get -> string +StackExchange.Redis.IConnectionMultiplexer.Close(bool allowCommandsToComplete = true) -> void +StackExchange.Redis.IConnectionMultiplexer.CloseAsync(bool allowCommandsToComplete = true) -> System.Threading.Tasks.Task +StackExchange.Redis.IConnectionMultiplexer.Configuration.get -> string +StackExchange.Redis.IConnectionMultiplexer.ConfigurationChanged -> System.EventHandler +StackExchange.Redis.IConnectionMultiplexer.ConfigurationChangedBroadcast -> System.EventHandler +StackExchange.Redis.IConnectionMultiplexer.Configure(System.IO.TextWriter log = null) -> bool +StackExchange.Redis.IConnectionMultiplexer.ConfigureAsync(System.IO.TextWriter log = null) -> System.Threading.Tasks.Task +StackExchange.Redis.IConnectionMultiplexer.ConnectionFailed -> System.EventHandler +StackExchange.Redis.IConnectionMultiplexer.ConnectionRestored -> System.EventHandler +StackExchange.Redis.IConnectionMultiplexer.ErrorMessage -> System.EventHandler +StackExchange.Redis.IConnectionMultiplexer.ExportConfiguration(System.IO.Stream destination, StackExchange.Redis.ExportOptions options = (StackExchange.Redis.ExportOptions)-1) -> void +StackExchange.Redis.IConnectionMultiplexer.GetCounters() -> StackExchange.Redis.ServerCounters +StackExchange.Redis.IConnectionMultiplexer.GetDatabase(int db = -1, object asyncState = null) -> StackExchange.Redis.IDatabase +StackExchange.Redis.IConnectionMultiplexer.GetEndPoints(bool configuredOnly = false) -> System.Net.EndPoint[] +StackExchange.Redis.IConnectionMultiplexer.GetHashSlot(StackExchange.Redis.RedisKey key) -> int +StackExchange.Redis.IConnectionMultiplexer.GetServer(string host, int port, object asyncState = null) -> StackExchange.Redis.IServer +StackExchange.Redis.IConnectionMultiplexer.GetServer(string hostAndPort, object asyncState = null) -> StackExchange.Redis.IServer +StackExchange.Redis.IConnectionMultiplexer.GetServer(System.Net.EndPoint endpoint, object asyncState = null) -> StackExchange.Redis.IServer +StackExchange.Redis.IConnectionMultiplexer.GetServer(System.Net.IPAddress host, int port) -> StackExchange.Redis.IServer +StackExchange.Redis.IConnectionMultiplexer.GetStatus() -> string +StackExchange.Redis.IConnectionMultiplexer.GetStatus(System.IO.TextWriter log) -> void +StackExchange.Redis.IConnectionMultiplexer.GetStormLog() -> string +StackExchange.Redis.IConnectionMultiplexer.GetSubscriber(object asyncState = null) -> StackExchange.Redis.ISubscriber +StackExchange.Redis.IConnectionMultiplexer.HashSlot(StackExchange.Redis.RedisKey key) -> int +StackExchange.Redis.IConnectionMultiplexer.HashSlotMoved -> System.EventHandler +StackExchange.Redis.IConnectionMultiplexer.IncludeDetailInExceptions.get -> bool +StackExchange.Redis.IConnectionMultiplexer.IncludeDetailInExceptions.set -> void +StackExchange.Redis.IConnectionMultiplexer.InternalError -> System.EventHandler +StackExchange.Redis.IConnectionMultiplexer.IsConnected.get -> bool +StackExchange.Redis.IConnectionMultiplexer.IsConnecting.get -> bool +StackExchange.Redis.IConnectionMultiplexer.OperationCount.get -> long +StackExchange.Redis.IConnectionMultiplexer.PreserveAsyncOrder.get -> bool +StackExchange.Redis.IConnectionMultiplexer.PreserveAsyncOrder.set -> void +StackExchange.Redis.IConnectionMultiplexer.PublishReconfigure(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IConnectionMultiplexer.PublishReconfigureAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IConnectionMultiplexer.RegisterProfiler(System.Func profilingSessionProvider) -> void +StackExchange.Redis.IConnectionMultiplexer.ResetStormLog() -> void +StackExchange.Redis.IConnectionMultiplexer.StormLogThreshold.get -> int +StackExchange.Redis.IConnectionMultiplexer.StormLogThreshold.set -> void +StackExchange.Redis.IConnectionMultiplexer.TimeoutMilliseconds.get -> int +StackExchange.Redis.IConnectionMultiplexer.ToString() -> string +StackExchange.Redis.IConnectionMultiplexer.Wait(System.Threading.Tasks.Task task) -> void +StackExchange.Redis.IConnectionMultiplexer.Wait(System.Threading.Tasks.Task task) -> T +StackExchange.Redis.IConnectionMultiplexer.WaitAll(params System.Threading.Tasks.Task[] tasks) -> void +StackExchange.Redis.IDatabase +StackExchange.Redis.IDatabase.CreateBatch(object asyncState = null) -> StackExchange.Redis.IBatch +StackExchange.Redis.IDatabase.CreateTransaction(object asyncState = null) -> StackExchange.Redis.ITransaction +StackExchange.Redis.IDatabase.Database.get -> int +StackExchange.Redis.IDatabase.DebugObject(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.Execute(string command, params object[] args) -> StackExchange.Redis.RedisResult +StackExchange.Redis.IDatabase.Execute(string command, System.Collections.Generic.ICollection args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult +StackExchange.Redis.IDatabase.GeoAdd(StackExchange.Redis.RedisKey key, double longitude, double latitude, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.GeoAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.GeoEntry value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.GeoAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.GeoEntry[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.GeoDistance(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member1, StackExchange.Redis.RedisValue member2, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double? +StackExchange.Redis.IDatabase.GeoHash(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string +StackExchange.Redis.IDatabase.GeoHash(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string[] +StackExchange.Redis.IDatabase.GeoPosition(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoPosition? +StackExchange.Redis.IDatabase.GeoPosition(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoPosition?[] +StackExchange.Redis.IDatabase.GeoRadius(StackExchange.Redis.RedisKey key, double longitude, double latitude, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoRadiusResult[] +StackExchange.Redis.IDatabase.GeoRadius(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoRadiusResult[] +StackExchange.Redis.IDatabase.GeoRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.HashDecrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double +StackExchange.Redis.IDatabase.HashDecrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.HashDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.HashDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.HashExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.HashGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.HashGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.HashGetAll(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.HashEntry[] +StackExchange.Redis.IDatabase.HashGetLease(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease +StackExchange.Redis.IDatabase.HashIncrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double +StackExchange.Redis.IDatabase.HashIncrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.HashKeys(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.HashLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.HashScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable +StackExchange.Redis.IDatabase.HashScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable +StackExchange.Redis.IDatabase.HashSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[] hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IDatabase.HashSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.HashStringLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.HashValues(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.HyperLogLogAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.HyperLogLogAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.HyperLogLogLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.HyperLogLogLength(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.HyperLogLogMerge(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IDatabase.HyperLogLogMerge(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] sourceKeys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IDatabase.IdentifyEndpoint(StackExchange.Redis.RedisKey key = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint +StackExchange.Redis.IDatabase.KeyDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.KeyDelete(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.KeyDump(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> byte[] +StackExchange.Redis.IDatabase.KeyExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.KeyExists(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.KeyIdleTime(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.TimeSpan? +StackExchange.Redis.IDatabase.KeyMigrate(StackExchange.Redis.RedisKey key, System.Net.EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, StackExchange.Redis.MigrateOptions migrateOptions = StackExchange.Redis.MigrateOptions.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IDatabase.KeyMove(StackExchange.Redis.RedisKey key, int database, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.KeyPersist(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.KeyRandom(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisKey +StackExchange.Redis.IDatabase.KeyRename(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisKey newKey, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.KeyRestore(StackExchange.Redis.RedisKey key, byte[] value, System.TimeSpan? expiry = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IDatabase.KeyTimeToLive(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.TimeSpan? +StackExchange.Redis.IDatabase.KeyTouch(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.KeyTouch(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.KeyType(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisType +StackExchange.Redis.IDatabase.ListGetByIndex(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.ListInsertAfter(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pivot, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.ListInsertBefore(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pivot, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.ListLeftPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.ListLeftPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags) -> long +StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.ListLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.ListRange(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.ListRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, long count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.ListRightPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.ListRightPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.ListRightPopLeftPush(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.ListRightPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.ListRightPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags) -> long +StackExchange.Redis.IDatabase.ListRightPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.ListSetByIndex(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IDatabase.ListTrim(StackExchange.Redis.RedisKey key, long start, long stop, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IDatabase.LockExtend(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.LockQuery(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.LockRelease(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.LockTake(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.Publish(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.ScriptEvaluate(byte[] hash, StackExchange.Redis.RedisKey[] keys = null, StackExchange.Redis.RedisValue[] values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult +StackExchange.Redis.IDatabase.ScriptEvaluate(StackExchange.Redis.LoadedLuaScript script, object parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult +StackExchange.Redis.IDatabase.ScriptEvaluate(StackExchange.Redis.LuaScript script, object parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult +StackExchange.Redis.IDatabase.ScriptEvaluate(string script, StackExchange.Redis.RedisKey[] keys = null, StackExchange.Redis.RedisValue[] values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult +StackExchange.Redis.IDatabase.SetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.SetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SetCombine(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SetCombine(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.SetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SetMembers(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SetMove(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.SetPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SetPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.SetRandomMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.SetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.SetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable +StackExchange.Redis.IDatabase.SetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable +StackExchange.Redis.IDatabase.Sort(StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[] get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SortAndStore(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[] get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.CommandFlags flags) -> bool +StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[] values, StackExchange.Redis.CommandFlags flags) -> long +StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] keys, double[] weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetDecrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double +StackExchange.Redis.IDatabase.SortedSetIncrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double +StackExchange.Redis.IDatabase.SortedSetLength(StackExchange.Redis.RedisKey key, double min = -Infinity, double max = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetLengthByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[] +StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry? +StackExchange.Redis.IDatabase.SortedSetRangeByRank(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SortedSetRangeByRankWithScores(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[] +StackExchange.Redis.IDatabase.SortedSetRangeByScore(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SortedSetRangeByScoreWithScores(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[] +StackExchange.Redis.IDatabase.SortedSetRangeByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue max = default(StackExchange.Redis.RedisValue), StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SortedSetRangeByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude, long skip, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SortedSetRank(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long? +StackExchange.Redis.IDatabase.SortedSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.SortedSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetRemoveRangeByRank(StackExchange.Redis.RedisKey key, long start, long stop, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetRemoveRangeByScore(StackExchange.Redis.RedisKey key, double start, double stop, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetRemoveRangeByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable +StackExchange.Redis.IDatabase.SortedSetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable +StackExchange.Redis.IDatabase.SortedSetScore(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double? +StackExchange.Redis.IDatabase.StreamAcknowledge(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue messageId, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StreamAcknowledge(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[] streamPairs, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StreamClaim(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[] +StackExchange.Redis.IDatabase.StreamClaimIdsOnly(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.StreamConsumerGroupSetPosition(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue position, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.StreamConsumerInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamConsumerInfo[] +StackExchange.Redis.IDatabase.StreamCreateConsumerGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue? position = null, bool createStream = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.StreamCreateConsumerGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue? position, StackExchange.Redis.CommandFlags flags) -> bool +StackExchange.Redis.IDatabase.StreamDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StreamDeleteConsumer(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StreamDeleteConsumerGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.StreamGroupInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamGroupInfo[] +StackExchange.Redis.IDatabase.StreamInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamInfo +StackExchange.Redis.IDatabase.StreamLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StreamPending(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamPendingInfo +StackExchange.Redis.IDatabase.StreamPendingMessages(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, int count, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamPendingMessageInfo[] +StackExchange.Redis.IDatabase.StreamRange(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, int? count = null, StackExchange.Redis.Order messageOrder = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[] +StackExchange.Redis.IDatabase.StreamRead(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue position, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[] +StackExchange.Redis.IDatabase.StreamRead(StackExchange.Redis.StreamPosition[] streamPositions, int? countPerStream = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisStream[] +StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position = null, int? count = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[] +StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position, int? count, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.StreamEntry[] +StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.StreamPosition[] streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisStream[] +StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.StreamPosition[] streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.RedisStream[] +StackExchange.Redis.IDatabase.StreamTrim(StackExchange.Redis.RedisKey key, int maxLength, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringAppend(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringBitCount(StackExchange.Redis.RedisKey key, long start = 0, long end = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringBitOperation(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringBitOperation(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringBitPosition(StackExchange.Redis.RedisKey key, bool bit, long start = 0, long end = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringDecrement(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double +StackExchange.Redis.IDatabase.StringDecrement(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StringGet(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.StringGetBit(StackExchange.Redis.RedisKey key, long offset, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.StringGetDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StringGetLease(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease +StackExchange.Redis.IDatabase.StringGetRange(StackExchange.Redis.RedisKey key, long start, long end, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StringGetSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StringGetWithExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValueWithExpiry +StackExchange.Redis.IDatabase.StringIncrement(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double +StackExchange.Redis.IDatabase.StringIncrement(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.StringSetAndGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StringSetBit(StackExchange.Redis.RedisKey key, long offset, bool bit, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.StringSetRange(StackExchange.Redis.RedisKey key, long offset, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabaseAsync +StackExchange.Redis.IDatabaseAsync.DebugObjectAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ExecuteAsync(string command, params object[] args) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ExecuteAsync(string command, System.Collections.Generic.ICollection args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.GeoAddAsync(StackExchange.Redis.RedisKey key, double longitude, double latitude, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.GeoAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.GeoEntry value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.GeoAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.GeoEntry[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.GeoDistanceAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member1, StackExchange.Redis.RedisValue member2, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.GeoHashAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.GeoHashAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.GeoPositionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.GeoPositionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.GeoRadiusAsync(StackExchange.Redis.RedisKey key, double longitude, double latitude, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.GeoRadiusAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.GeoRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashDecrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashDecrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashExistsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashGetAllAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashGetLeaseAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task> +StackExchange.Redis.IDatabaseAsync.HashIncrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashIncrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashKeysAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable +StackExchange.Redis.IDatabaseAsync.HashSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[] hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashStringLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HashValuesAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HyperLogLogAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HyperLogLogAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HyperLogLogLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HyperLogLogLengthAsync(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HyperLogLogMergeAsync(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.HyperLogLogMergeAsync(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] sourceKeys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.IdentifyEndpointAsync(StackExchange.Redis.RedisKey key = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.IsConnected(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabaseAsync.KeyDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyDeleteAsync(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyDumpAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyExistsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyExistsAsync(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyIdleTimeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyMigrateAsync(StackExchange.Redis.RedisKey key, System.Net.EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, StackExchange.Redis.MigrateOptions migrateOptions = StackExchange.Redis.MigrateOptions.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyMoveAsync(StackExchange.Redis.RedisKey key, int database, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyPersistAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyRandomAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyRenameAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisKey newKey, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyRestoreAsync(StackExchange.Redis.RedisKey key, byte[] value, System.TimeSpan? expiry = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyTimeToLiveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyTouchAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyTouchAsync(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyTypeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListGetByIndexAsync(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListInsertAfterAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pivot, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListInsertBeforeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pivot, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListLeftPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListLeftPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListRangeAsync(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, long count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListRightPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListRightPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListRightPopLeftPushAsync(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListRightPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListRightPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListRightPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListSetByIndexAsync(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ListTrimAsync(StackExchange.Redis.RedisKey key, long start, long stop, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.LockExtendAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.LockQueryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.LockReleaseAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.LockTakeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.PublishAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(byte[] hash, StackExchange.Redis.RedisKey[] keys = null, StackExchange.Redis.RedisValue[] values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(StackExchange.Redis.LoadedLuaScript script, object parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(StackExchange.Redis.LuaScript script, object parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(string script, StackExchange.Redis.RedisKey[] keys = null, StackExchange.Redis.RedisValue[] values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetMembersAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetMoveAsync(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetRandomMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SetScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable +StackExchange.Redis.IDatabaseAsync.SortAndStoreAsync(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[] get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortAsync(StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[] get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[] values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] keys, double[] weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetDecrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetIncrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetLengthAsync(StackExchange.Redis.RedisKey key, double min = -Infinity, double max = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetLengthByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetRangeByRankAsync(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetRangeByRankWithScoresAsync(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetRangeByScoreAsync(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetRangeByScoreWithScoresAsync(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetRangeByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue max = default(StackExchange.Redis.RedisValue), StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetRangeByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude, long skip, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetRankAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetRemoveRangeByRankAsync(StackExchange.Redis.RedisKey key, long start, long stop, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetRemoveRangeByScoreAsync(StackExchange.Redis.RedisKey key, double start, double stop, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetRemoveRangeByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable +StackExchange.Redis.IDatabaseAsync.SortedSetScoreAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue messageId, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[] streamPairs, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamClaimAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamClaimIdsOnlyAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamConsumerGroupSetPositionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue position, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamConsumerInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamCreateConsumerGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue? position = null, bool createStream = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamCreateConsumerGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue? position, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamDeleteConsumerAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamDeleteConsumerGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamGroupInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamPendingAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamPendingMessagesAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, int count, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamRangeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, int? count = null, StackExchange.Redis.Order messageOrder = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamReadAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue position, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamReadAsync(StackExchange.Redis.StreamPosition[] streamPositions, int? countPerStream = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position = null, int? count = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position, int? count, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.StreamPosition[] streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.StreamPosition[] streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StreamTrimAsync(StackExchange.Redis.RedisKey key, int maxLength, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringAppendAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringBitCountAsync(StackExchange.Redis.RedisKey key, long start = 0, long end = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringBitOperationAsync(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringBitOperationAsync(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringBitPositionAsync(StackExchange.Redis.RedisKey key, bool bit, long start = 0, long end = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringDecrementAsync(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringDecrementAsync(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringGetAsync(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringGetBitAsync(StackExchange.Redis.RedisKey key, long offset, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringGetDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringGetLeaseAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task> +StackExchange.Redis.IDatabaseAsync.StringGetRangeAsync(StackExchange.Redis.RedisKey key, long start, long end, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringGetSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringGetWithExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringIncrementAsync(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringIncrementAsync(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringSetAsync(System.Collections.Generic.KeyValuePair[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringSetBitAsync(StackExchange.Redis.RedisKey key, long offset, bool bit, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringSetRangeAsync(StackExchange.Redis.RedisKey key, long offset, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.InternalErrorEventArgs +StackExchange.Redis.InternalErrorEventArgs.ConnectionType.get -> StackExchange.Redis.ConnectionType +StackExchange.Redis.InternalErrorEventArgs.EndPoint.get -> System.Net.EndPoint +StackExchange.Redis.InternalErrorEventArgs.Exception.get -> System.Exception +StackExchange.Redis.InternalErrorEventArgs.InternalErrorEventArgs(object sender, System.Net.EndPoint endpoint, StackExchange.Redis.ConnectionType connectionType, System.Exception exception, string origin) -> void +StackExchange.Redis.InternalErrorEventArgs.Origin.get -> string +StackExchange.Redis.IReconnectRetryPolicy +StackExchange.Redis.IReconnectRetryPolicy.ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSinceLastRetry) -> bool +StackExchange.Redis.IRedis +StackExchange.Redis.IRedis.Ping(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.TimeSpan +StackExchange.Redis.IRedisAsync +StackExchange.Redis.IRedisAsync.Multiplexer.get -> StackExchange.Redis.IConnectionMultiplexer +StackExchange.Redis.IRedisAsync.PingAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IRedisAsync.TryWait(System.Threading.Tasks.Task task) -> bool +StackExchange.Redis.IRedisAsync.Wait(System.Threading.Tasks.Task task) -> void +StackExchange.Redis.IRedisAsync.Wait(System.Threading.Tasks.Task task) -> T +StackExchange.Redis.IRedisAsync.WaitAll(params System.Threading.Tasks.Task[] tasks) -> void +StackExchange.Redis.IScanningCursor +StackExchange.Redis.IScanningCursor.Cursor.get -> long +StackExchange.Redis.IScanningCursor.PageOffset.get -> int +StackExchange.Redis.IScanningCursor.PageSize.get -> int +StackExchange.Redis.IServer +StackExchange.Redis.IServer.AllowReplicaWrites.get -> bool +StackExchange.Redis.IServer.AllowReplicaWrites.set -> void +StackExchange.Redis.IServer.AllowSlaveWrites.get -> bool +StackExchange.Redis.IServer.AllowSlaveWrites.set -> void +StackExchange.Redis.IServer.ClientKill(long? id = null, StackExchange.Redis.ClientType? clientType = null, System.Net.EndPoint endpoint = null, bool skipMe = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IServer.ClientKill(System.Net.EndPoint endpoint, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.ClientKillAsync(long? id = null, StackExchange.Redis.ClientType? clientType = null, System.Net.EndPoint endpoint = null, bool skipMe = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ClientKillAsync(System.Net.EndPoint endpoint, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ClientList(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ClientInfo[] +StackExchange.Redis.IServer.ClientListAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ClusterConfiguration.get -> StackExchange.Redis.ClusterConfiguration +StackExchange.Redis.IServer.ClusterNodes(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ClusterConfiguration +StackExchange.Redis.IServer.ClusterNodesAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ClusterNodesRaw(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string +StackExchange.Redis.IServer.ClusterNodesRawAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ConfigGet(StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[] +StackExchange.Redis.IServer.ConfigGetAsync(StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[]> +StackExchange.Redis.IServer.ConfigResetStatistics(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.ConfigResetStatisticsAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ConfigRewrite(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.ConfigRewriteAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ConfigSet(StackExchange.Redis.RedisValue setting, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.ConfigSetAsync(StackExchange.Redis.RedisValue setting, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.DatabaseCount.get -> int +StackExchange.Redis.IServer.DatabaseSize(int database = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IServer.DatabaseSizeAsync(int database = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.Echo(StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IServer.EchoAsync(StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.EndPoint.get -> System.Net.EndPoint +StackExchange.Redis.IServer.Execute(string command, params object[] args) -> StackExchange.Redis.RedisResult +StackExchange.Redis.IServer.Execute(string command, System.Collections.Generic.ICollection args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult +StackExchange.Redis.IServer.ExecuteAsync(string command, params object[] args) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ExecuteAsync(string command, System.Collections.Generic.ICollection args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.Features.get -> StackExchange.Redis.RedisFeatures +StackExchange.Redis.IServer.FlushAllDatabases(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.FlushAllDatabasesAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.FlushDatabase(int database = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.FlushDatabaseAsync(int database = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.GetCounters() -> StackExchange.Redis.ServerCounters +StackExchange.Redis.IServer.Info(StackExchange.Redis.RedisValue section = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Linq.IGrouping>[] +StackExchange.Redis.IServer.InfoAsync(StackExchange.Redis.RedisValue section = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task>[]> +StackExchange.Redis.IServer.InfoRaw(StackExchange.Redis.RedisValue section = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string +StackExchange.Redis.IServer.InfoRawAsync(StackExchange.Redis.RedisValue section = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.IsConnected.get -> bool +StackExchange.Redis.IServer.IsReplica.get -> bool +StackExchange.Redis.IServer.IsSlave.get -> bool +StackExchange.Redis.IServer.Keys(int database = -1, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable +StackExchange.Redis.IServer.Keys(int database, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable +StackExchange.Redis.IServer.KeysAsync(int database = -1, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable +StackExchange.Redis.IServer.LastSave(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.DateTime +StackExchange.Redis.IServer.LastSaveAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.LatencyDoctor(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string +StackExchange.Redis.IServer.LatencyDoctorAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.LatencyHistory(string eventName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LatencyHistoryEntry[] +StackExchange.Redis.IServer.LatencyHistoryAsync(string eventName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.LatencyLatest(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LatencyLatestEntry[] +StackExchange.Redis.IServer.LatencyLatestAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.LatencyReset(string[] eventNames = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IServer.LatencyResetAsync(string[] eventNames = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.MakeMaster(StackExchange.Redis.ReplicationChangeOptions options, System.IO.TextWriter log = null) -> void +StackExchange.Redis.IServer.MakePrimaryAsync(StackExchange.Redis.ReplicationChangeOptions options, System.IO.TextWriter log = null) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.MemoryAllocatorStats(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string +StackExchange.Redis.IServer.MemoryAllocatorStatsAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.MemoryDoctor(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string +StackExchange.Redis.IServer.MemoryDoctorAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.MemoryPurge(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.MemoryPurgeAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.MemoryStats(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult +StackExchange.Redis.IServer.MemoryStatsAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ReplicaOf(System.Net.EndPoint master, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.ReplicaOfAsync(System.Net.EndPoint master, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.Role(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Role +StackExchange.Redis.IServer.RoleAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.Save(StackExchange.Redis.SaveType type, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.SaveAsync(StackExchange.Redis.SaveType type, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ScriptExists(byte[] sha1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IServer.ScriptExists(string script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IServer.ScriptExistsAsync(byte[] sha1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ScriptExistsAsync(string script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ScriptFlush(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.ScriptFlushAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ScriptLoad(StackExchange.Redis.LuaScript script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LoadedLuaScript +StackExchange.Redis.IServer.ScriptLoad(string script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> byte[] +StackExchange.Redis.IServer.ScriptLoadAsync(StackExchange.Redis.LuaScript script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ScriptLoadAsync(string script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SentinelFailover(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.SentinelFailoverAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SentinelGetMasterAddressByName(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint +StackExchange.Redis.IServer.SentinelGetMasterAddressByNameAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SentinelGetReplicaAddresses(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint[] +StackExchange.Redis.IServer.SentinelGetReplicaAddressesAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SentinelGetSentinelAddresses(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint[] +StackExchange.Redis.IServer.SentinelGetSentinelAddressesAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SentinelMaster(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[] +StackExchange.Redis.IServer.SentinelMasterAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[]> +StackExchange.Redis.IServer.SentinelMasters(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[][] +StackExchange.Redis.IServer.SentinelMastersAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[][]> +StackExchange.Redis.IServer.SentinelReplicas(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[][] +StackExchange.Redis.IServer.SentinelReplicasAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[][]> +StackExchange.Redis.IServer.SentinelSentinels(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[][] +StackExchange.Redis.IServer.SentinelSentinelsAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[][]> +StackExchange.Redis.IServer.SentinelSlaves(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[][] +StackExchange.Redis.IServer.SentinelSlavesAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[][]> +StackExchange.Redis.IServer.ServerType.get -> StackExchange.Redis.ServerType +StackExchange.Redis.IServer.Shutdown(StackExchange.Redis.ShutdownMode shutdownMode = StackExchange.Redis.ShutdownMode.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.SlaveOf(System.Net.EndPoint master, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.SlaveOfAsync(System.Net.EndPoint master, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SlowlogGet(int count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.CommandTrace[] +StackExchange.Redis.IServer.SlowlogGetAsync(int count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SlowlogReset(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.SlowlogResetAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SubscriptionChannels(StackExchange.Redis.RedisChannel pattern = default(StackExchange.Redis.RedisChannel), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisChannel[] +StackExchange.Redis.IServer.SubscriptionChannelsAsync(StackExchange.Redis.RedisChannel pattern = default(StackExchange.Redis.RedisChannel), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SubscriptionPatternCount(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IServer.SubscriptionPatternCountAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SubscriptionSubscriberCount(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IServer.SubscriptionSubscriberCountAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SwapDatabases(int first, int second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.SwapDatabasesAsync(int first, int second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.Time(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.DateTime +StackExchange.Redis.IServer.TimeAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.Version.get -> System.Version +StackExchange.Redis.ISubscriber +StackExchange.Redis.ISubscriber.IdentifyEndpoint(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint +StackExchange.Redis.ISubscriber.IdentifyEndpointAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.ISubscriber.IsConnected(StackExchange.Redis.RedisChannel channel = default(StackExchange.Redis.RedisChannel)) -> bool +StackExchange.Redis.ISubscriber.Publish(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.ISubscriber.PublishAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.ISubscriber.Subscribe(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ChannelMessageQueue +StackExchange.Redis.ISubscriber.Subscribe(StackExchange.Redis.RedisChannel channel, System.Action handler, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.ISubscriber.SubscribeAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.ISubscriber.SubscribeAsync(StackExchange.Redis.RedisChannel channel, System.Action handler, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.ISubscriber.SubscribedEndpoint(StackExchange.Redis.RedisChannel channel) -> System.Net.EndPoint +StackExchange.Redis.ISubscriber.Unsubscribe(StackExchange.Redis.RedisChannel channel, System.Action handler = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.ISubscriber.UnsubscribeAll(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.ISubscriber.UnsubscribeAllAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.ISubscriber.UnsubscribeAsync(StackExchange.Redis.RedisChannel channel, System.Action handler = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.ITransaction +StackExchange.Redis.ITransaction.AddCondition(StackExchange.Redis.Condition condition) -> StackExchange.Redis.ConditionResult +StackExchange.Redis.ITransaction.Execute(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.ITransaction.ExecuteAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.KeyspaceIsolation.DatabaseExtensions +StackExchange.Redis.LatencyHistoryEntry +StackExchange.Redis.LatencyHistoryEntry.DurationMilliseconds.get -> int +StackExchange.Redis.LatencyHistoryEntry.LatencyHistoryEntry() -> void +StackExchange.Redis.LatencyHistoryEntry.Timestamp.get -> System.DateTime +StackExchange.Redis.LatencyLatestEntry +StackExchange.Redis.LatencyLatestEntry.DurationMilliseconds.get -> int +StackExchange.Redis.LatencyLatestEntry.EventName.get -> string +StackExchange.Redis.LatencyLatestEntry.LatencyLatestEntry() -> void +StackExchange.Redis.LatencyLatestEntry.MaxDurationMilliseconds.get -> int +StackExchange.Redis.LatencyLatestEntry.Timestamp.get -> System.DateTime +StackExchange.Redis.Lease +StackExchange.Redis.Lease.ArraySegment.get -> System.ArraySegment +StackExchange.Redis.Lease.Dispose() -> void +StackExchange.Redis.Lease.Length.get -> int +StackExchange.Redis.Lease.Memory.get -> System.Memory +StackExchange.Redis.Lease.Span.get -> System.Span +StackExchange.Redis.LinearRetry +StackExchange.Redis.LinearRetry.LinearRetry(int maxRetryElapsedTimeAllowedMilliseconds) -> void +StackExchange.Redis.LinearRetry.ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSinceLastRetry) -> bool +StackExchange.Redis.LoadedLuaScript +StackExchange.Redis.LoadedLuaScript.Evaluate(StackExchange.Redis.IDatabase db, object ps = null, StackExchange.Redis.RedisKey? withKeyPrefix = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult +StackExchange.Redis.LoadedLuaScript.EvaluateAsync(StackExchange.Redis.IDatabaseAsync db, object ps = null, StackExchange.Redis.RedisKey? withKeyPrefix = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.LoadedLuaScript.ExecutableScript.get -> string +StackExchange.Redis.LoadedLuaScript.Hash.get -> byte[] +StackExchange.Redis.LoadedLuaScript.OriginalScript.get -> string +StackExchange.Redis.LuaScript +StackExchange.Redis.LuaScript.Evaluate(StackExchange.Redis.IDatabase db, object ps = null, StackExchange.Redis.RedisKey? withKeyPrefix = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult +StackExchange.Redis.LuaScript.EvaluateAsync(StackExchange.Redis.IDatabaseAsync db, object ps = null, StackExchange.Redis.RedisKey? withKeyPrefix = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.LuaScript.ExecutableScript.get -> string +StackExchange.Redis.LuaScript.Load(StackExchange.Redis.IServer server, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LoadedLuaScript +StackExchange.Redis.LuaScript.LoadAsync(StackExchange.Redis.IServer server, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.LuaScript.OriginalScript.get -> string +StackExchange.Redis.Maintenance.AzureMaintenanceEvent +StackExchange.Redis.Maintenance.AzureMaintenanceEvent.IPAddress.get -> System.Net.IPAddress +StackExchange.Redis.Maintenance.AzureMaintenanceEvent.IsReplica.get -> bool +StackExchange.Redis.Maintenance.AzureMaintenanceEvent.NonSslPort.get -> int +StackExchange.Redis.Maintenance.AzureMaintenanceEvent.NotificationType.get -> StackExchange.Redis.Maintenance.AzureNotificationType +StackExchange.Redis.Maintenance.AzureMaintenanceEvent.NotificationTypeString.get -> string +StackExchange.Redis.Maintenance.AzureMaintenanceEvent.SslPort.get -> int +StackExchange.Redis.Maintenance.AzureNotificationType +StackExchange.Redis.Maintenance.AzureNotificationType.NodeMaintenanceEnded = 4 -> StackExchange.Redis.Maintenance.AzureNotificationType +StackExchange.Redis.Maintenance.AzureNotificationType.NodeMaintenanceFailoverComplete = 5 -> StackExchange.Redis.Maintenance.AzureNotificationType +StackExchange.Redis.Maintenance.AzureNotificationType.NodeMaintenanceScaleComplete = 6 -> StackExchange.Redis.Maintenance.AzureNotificationType +StackExchange.Redis.Maintenance.AzureNotificationType.NodeMaintenanceScheduled = 1 -> StackExchange.Redis.Maintenance.AzureNotificationType +StackExchange.Redis.Maintenance.AzureNotificationType.NodeMaintenanceStart = 3 -> StackExchange.Redis.Maintenance.AzureNotificationType +StackExchange.Redis.Maintenance.AzureNotificationType.NodeMaintenanceStarting = 2 -> StackExchange.Redis.Maintenance.AzureNotificationType +StackExchange.Redis.Maintenance.AzureNotificationType.Unknown = 0 -> StackExchange.Redis.Maintenance.AzureNotificationType +StackExchange.Redis.Maintenance.ServerMaintenanceEvent +StackExchange.Redis.Maintenance.ServerMaintenanceEvent.RawMessage.get -> string +StackExchange.Redis.Maintenance.ServerMaintenanceEvent.ReceivedTimeUtc.get -> System.DateTime +StackExchange.Redis.Maintenance.ServerMaintenanceEvent.StartTimeUtc.get -> System.DateTime? +StackExchange.Redis.MigrateOptions +StackExchange.Redis.MigrateOptions.Copy = 1 -> StackExchange.Redis.MigrateOptions +StackExchange.Redis.MigrateOptions.None = 0 -> StackExchange.Redis.MigrateOptions +StackExchange.Redis.MigrateOptions.Replace = 2 -> StackExchange.Redis.MigrateOptions +StackExchange.Redis.NameValueEntry +StackExchange.Redis.NameValueEntry.Equals(StackExchange.Redis.NameValueEntry other) -> bool +StackExchange.Redis.NameValueEntry.Name.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.NameValueEntry.NameValueEntry() -> void +StackExchange.Redis.NameValueEntry.NameValueEntry(StackExchange.Redis.RedisValue name, StackExchange.Redis.RedisValue value) -> void +StackExchange.Redis.NameValueEntry.Value.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.Order +StackExchange.Redis.Order.Ascending = 0 -> StackExchange.Redis.Order +StackExchange.Redis.Order.Descending = 1 -> StackExchange.Redis.Order +StackExchange.Redis.Profiling.IProfiledCommand +StackExchange.Redis.Profiling.IProfiledCommand.Command.get -> string +StackExchange.Redis.Profiling.IProfiledCommand.CommandCreated.get -> System.DateTime +StackExchange.Redis.Profiling.IProfiledCommand.CreationToEnqueued.get -> System.TimeSpan +StackExchange.Redis.Profiling.IProfiledCommand.Db.get -> int +StackExchange.Redis.Profiling.IProfiledCommand.ElapsedTime.get -> System.TimeSpan +StackExchange.Redis.Profiling.IProfiledCommand.EndPoint.get -> System.Net.EndPoint +StackExchange.Redis.Profiling.IProfiledCommand.EnqueuedToSending.get -> System.TimeSpan +StackExchange.Redis.Profiling.IProfiledCommand.Flags.get -> StackExchange.Redis.CommandFlags +StackExchange.Redis.Profiling.IProfiledCommand.ResponseToCompletion.get -> System.TimeSpan +StackExchange.Redis.Profiling.IProfiledCommand.RetransmissionOf.get -> StackExchange.Redis.Profiling.IProfiledCommand +StackExchange.Redis.Profiling.IProfiledCommand.RetransmissionReason.get -> StackExchange.Redis.RetransmissionReasonType? +StackExchange.Redis.Profiling.IProfiledCommand.SentToResponse.get -> System.TimeSpan +StackExchange.Redis.Profiling.ProfiledCommandEnumerable +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Count() -> int +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Count(System.Func predicate) -> int +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator.Current.get -> StackExchange.Redis.Profiling.IProfiledCommand +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator.Dispose() -> void +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator.Enumerator() -> void +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator.MoveNext() -> bool +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator.Reset() -> void +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.GetEnumerator() -> StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.ProfiledCommandEnumerable() -> void +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.ToArray() -> StackExchange.Redis.Profiling.IProfiledCommand[] +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.ToList() -> System.Collections.Generic.List +StackExchange.Redis.Profiling.ProfilingSession +StackExchange.Redis.Profiling.ProfilingSession.FinishProfiling() -> StackExchange.Redis.Profiling.ProfiledCommandEnumerable +StackExchange.Redis.Profiling.ProfilingSession.ProfilingSession(object userToken = null) -> void +StackExchange.Redis.Profiling.ProfilingSession.UserToken.get -> object +StackExchange.Redis.Proxy +StackExchange.Redis.Proxy.Envoyproxy = 2 -> StackExchange.Redis.Proxy +StackExchange.Redis.Proxy.None = 0 -> StackExchange.Redis.Proxy +StackExchange.Redis.Proxy.Twemproxy = 1 -> StackExchange.Redis.Proxy +StackExchange.Redis.RedisChannel +StackExchange.Redis.RedisChannel.Equals(StackExchange.Redis.RedisChannel other) -> bool +StackExchange.Redis.RedisChannel.IsNullOrEmpty.get -> bool +StackExchange.Redis.RedisChannel.PatternMode +StackExchange.Redis.RedisChannel.PatternMode.Auto = 0 -> StackExchange.Redis.RedisChannel.PatternMode +StackExchange.Redis.RedisChannel.PatternMode.Literal = 1 -> StackExchange.Redis.RedisChannel.PatternMode +StackExchange.Redis.RedisChannel.PatternMode.Pattern = 2 -> StackExchange.Redis.RedisChannel.PatternMode +StackExchange.Redis.RedisChannel.RedisChannel() -> void +StackExchange.Redis.RedisChannel.RedisChannel(byte[] value, StackExchange.Redis.RedisChannel.PatternMode mode) -> void +StackExchange.Redis.RedisChannel.RedisChannel(string value, StackExchange.Redis.RedisChannel.PatternMode mode) -> void +StackExchange.Redis.RedisCommandException +StackExchange.Redis.RedisCommandException.RedisCommandException(string message) -> void +StackExchange.Redis.RedisCommandException.RedisCommandException(string message, System.Exception innerException) -> void +StackExchange.Redis.RedisConnectionException +StackExchange.Redis.RedisConnectionException.CommandStatus.get -> StackExchange.Redis.CommandStatus +StackExchange.Redis.RedisConnectionException.FailureType.get -> StackExchange.Redis.ConnectionFailureType +StackExchange.Redis.RedisConnectionException.RedisConnectionException(StackExchange.Redis.ConnectionFailureType failureType, string message) -> void +StackExchange.Redis.RedisConnectionException.RedisConnectionException(StackExchange.Redis.ConnectionFailureType failureType, string message, System.Exception innerException) -> void +StackExchange.Redis.RedisConnectionException.RedisConnectionException(StackExchange.Redis.ConnectionFailureType failureType, string message, System.Exception innerException, StackExchange.Redis.CommandStatus commandStatus) -> void +StackExchange.Redis.RedisErrorEventArgs +StackExchange.Redis.RedisErrorEventArgs.EndPoint.get -> System.Net.EndPoint +StackExchange.Redis.RedisErrorEventArgs.Message.get -> string +StackExchange.Redis.RedisErrorEventArgs.RedisErrorEventArgs(object sender, System.Net.EndPoint endpoint, string message) -> void +StackExchange.Redis.RedisException +StackExchange.Redis.RedisException.RedisException(string message) -> void +StackExchange.Redis.RedisException.RedisException(string message, System.Exception innerException) -> void +StackExchange.Redis.RedisException.RedisException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext ctx) -> void +StackExchange.Redis.RedisFeatures +StackExchange.Redis.RedisFeatures.BitwiseOperations.get -> bool +StackExchange.Redis.RedisFeatures.ClientName.get -> bool +StackExchange.Redis.RedisFeatures.ExecAbort.get -> bool +StackExchange.Redis.RedisFeatures.ExpireOverwrite.get -> bool +StackExchange.Redis.RedisFeatures.Geo.get -> bool +StackExchange.Redis.RedisFeatures.GetDelete.get -> bool +StackExchange.Redis.RedisFeatures.HashStringLength.get -> bool +StackExchange.Redis.RedisFeatures.HashVaradicDelete.get -> bool +StackExchange.Redis.RedisFeatures.HyperLogLogCountReplicaSafe.get -> bool +StackExchange.Redis.RedisFeatures.HyperLogLogCountSlaveSafe.get -> bool +StackExchange.Redis.RedisFeatures.IncrementFloat.get -> bool +StackExchange.Redis.RedisFeatures.InfoSections.get -> bool +StackExchange.Redis.RedisFeatures.KeyTouch.get -> bool +StackExchange.Redis.RedisFeatures.ListInsert.get -> bool +StackExchange.Redis.RedisFeatures.Memory.get -> bool +StackExchange.Redis.RedisFeatures.MillisecondExpiry.get -> bool +StackExchange.Redis.RedisFeatures.Module.get -> bool +StackExchange.Redis.RedisFeatures.MultipleRandom.get -> bool +StackExchange.Redis.RedisFeatures.Persist.get -> bool +StackExchange.Redis.RedisFeatures.PushIfNotExists.get -> bool +StackExchange.Redis.RedisFeatures.PushMultiple.get -> bool +StackExchange.Redis.RedisFeatures.RedisFeatures() -> void +StackExchange.Redis.RedisFeatures.RedisFeatures(System.Version version) -> void +StackExchange.Redis.RedisFeatures.ReplicaCommands.get -> bool +StackExchange.Redis.RedisFeatures.Scan.get -> bool +StackExchange.Redis.RedisFeatures.Scripting.get -> bool +StackExchange.Redis.RedisFeatures.ScriptingDatabaseSafe.get -> bool +StackExchange.Redis.RedisFeatures.SetAndGet.get -> bool +StackExchange.Redis.RedisFeatures.SetConditional.get -> bool +StackExchange.Redis.RedisFeatures.SetNotExistsAndGet.get -> bool +StackExchange.Redis.RedisFeatures.SetPopMultiple.get -> bool +StackExchange.Redis.RedisFeatures.SetVaradicAddRemove.get -> bool +StackExchange.Redis.RedisFeatures.SortedSetPop.get -> bool +StackExchange.Redis.RedisFeatures.Streams.get -> bool +StackExchange.Redis.RedisFeatures.StringLength.get -> bool +StackExchange.Redis.RedisFeatures.StringSetRange.get -> bool +StackExchange.Redis.RedisFeatures.SwapDB.get -> bool +StackExchange.Redis.RedisFeatures.Time.get -> bool +StackExchange.Redis.RedisFeatures.Unlink.get -> bool +StackExchange.Redis.RedisFeatures.Version.get -> System.Version +StackExchange.Redis.RedisKey +StackExchange.Redis.RedisKey.Append(StackExchange.Redis.RedisKey suffix) -> StackExchange.Redis.RedisKey +StackExchange.Redis.RedisKey.Equals(StackExchange.Redis.RedisKey other) -> bool +StackExchange.Redis.RedisKey.Prepend(StackExchange.Redis.RedisKey prefix) -> StackExchange.Redis.RedisKey +StackExchange.Redis.RedisKey.RedisKey() -> void +StackExchange.Redis.RedisKey.RedisKey(string key) -> void +StackExchange.Redis.RedisResult +StackExchange.Redis.RedisResult.RedisResult() -> void +StackExchange.Redis.RedisResult.ToDictionary(System.Collections.Generic.IEqualityComparer comparer = null) -> System.Collections.Generic.Dictionary +StackExchange.Redis.RedisServerException +StackExchange.Redis.RedisServerException.RedisServerException(string message) -> void +StackExchange.Redis.RedisStream +StackExchange.Redis.RedisStream.Entries.get -> StackExchange.Redis.StreamEntry[] +StackExchange.Redis.RedisStream.Key.get -> StackExchange.Redis.RedisKey +StackExchange.Redis.RedisStream.RedisStream() -> void +StackExchange.Redis.RedisTimeoutException +StackExchange.Redis.RedisTimeoutException.Commandstatus.get -> StackExchange.Redis.CommandStatus +StackExchange.Redis.RedisTimeoutException.RedisTimeoutException(string message, StackExchange.Redis.CommandStatus commandStatus) -> void +StackExchange.Redis.RedisType +StackExchange.Redis.RedisType.Hash = 5 -> StackExchange.Redis.RedisType +StackExchange.Redis.RedisType.List = 2 -> StackExchange.Redis.RedisType +StackExchange.Redis.RedisType.None = 0 -> StackExchange.Redis.RedisType +StackExchange.Redis.RedisType.Set = 3 -> StackExchange.Redis.RedisType +StackExchange.Redis.RedisType.SortedSet = 4 -> StackExchange.Redis.RedisType +StackExchange.Redis.RedisType.Stream = 6 -> StackExchange.Redis.RedisType +StackExchange.Redis.RedisType.String = 1 -> StackExchange.Redis.RedisType +StackExchange.Redis.RedisType.Unknown = 7 -> StackExchange.Redis.RedisType +StackExchange.Redis.RedisValue +StackExchange.Redis.RedisValue.Box() -> object +StackExchange.Redis.RedisValue.CompareTo(StackExchange.Redis.RedisValue other) -> int +StackExchange.Redis.RedisValue.Equals(StackExchange.Redis.RedisValue other) -> bool +StackExchange.Redis.RedisValue.HasValue.get -> bool +StackExchange.Redis.RedisValue.IsInteger.get -> bool +StackExchange.Redis.RedisValue.IsNull.get -> bool +StackExchange.Redis.RedisValue.IsNullOrEmpty.get -> bool +StackExchange.Redis.RedisValue.Length() -> long +StackExchange.Redis.RedisValue.RedisValue() -> void +StackExchange.Redis.RedisValue.RedisValue(string value) -> void +StackExchange.Redis.RedisValue.StartsWith(StackExchange.Redis.RedisValue value) -> bool +StackExchange.Redis.RedisValue.TryParse(out double val) -> bool +StackExchange.Redis.RedisValue.TryParse(out int val) -> bool +StackExchange.Redis.RedisValue.TryParse(out long val) -> bool +StackExchange.Redis.RedisValueWithExpiry +StackExchange.Redis.RedisValueWithExpiry.Expiry.get -> System.TimeSpan? +StackExchange.Redis.RedisValueWithExpiry.RedisValueWithExpiry() -> void +StackExchange.Redis.RedisValueWithExpiry.RedisValueWithExpiry(StackExchange.Redis.RedisValue value, System.TimeSpan? expiry) -> void +StackExchange.Redis.RedisValueWithExpiry.Value.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.ReplicationChangeOptions +StackExchange.Redis.ReplicationChangeOptions.All = StackExchange.Redis.ReplicationChangeOptions.SetTiebreaker | StackExchange.Redis.ReplicationChangeOptions.Broadcast | StackExchange.Redis.ReplicationChangeOptions.EnslaveSubordinates -> StackExchange.Redis.ReplicationChangeOptions +StackExchange.Redis.ReplicationChangeOptions.Broadcast = 2 -> StackExchange.Redis.ReplicationChangeOptions +StackExchange.Redis.ReplicationChangeOptions.EnslaveSubordinates = 4 -> StackExchange.Redis.ReplicationChangeOptions +StackExchange.Redis.ReplicationChangeOptions.None = 0 -> StackExchange.Redis.ReplicationChangeOptions +StackExchange.Redis.ReplicationChangeOptions.ReplicateToOtherEndpoints = 4 -> StackExchange.Redis.ReplicationChangeOptions +StackExchange.Redis.ReplicationChangeOptions.SetTiebreaker = 1 -> StackExchange.Redis.ReplicationChangeOptions +StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.BulkString = 4 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Error = 2 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Integer = 3 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.MultiBulk = 5 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.None = 0 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.SimpleString = 1 -> StackExchange.Redis.ResultType +StackExchange.Redis.RetransmissionReasonType +StackExchange.Redis.RetransmissionReasonType.Ask = 1 -> StackExchange.Redis.RetransmissionReasonType +StackExchange.Redis.RetransmissionReasonType.Moved = 2 -> StackExchange.Redis.RetransmissionReasonType +StackExchange.Redis.RetransmissionReasonType.None = 0 -> StackExchange.Redis.RetransmissionReasonType +StackExchange.Redis.Role +StackExchange.Redis.Role.Master +StackExchange.Redis.Role.Master.Replica +StackExchange.Redis.Role.Master.Replica.Ip.get -> string +StackExchange.Redis.Role.Master.Replica.Port.get -> int +StackExchange.Redis.Role.Master.Replica.Replica() -> void +StackExchange.Redis.Role.Master.Replica.ReplicationOffset.get -> long +StackExchange.Redis.Role.Master.Replicas.get -> System.Collections.Generic.ICollection +StackExchange.Redis.Role.Master.ReplicationOffset.get -> long +StackExchange.Redis.Role.Replica +StackExchange.Redis.Role.Replica.MasterIp.get -> string +StackExchange.Redis.Role.Replica.MasterPort.get -> int +StackExchange.Redis.Role.Replica.ReplicationOffset.get -> long +StackExchange.Redis.Role.Replica.State.get -> string +StackExchange.Redis.Role.Sentinel +StackExchange.Redis.Role.Sentinel.MonitoredMasters.get -> System.Collections.Generic.ICollection +StackExchange.Redis.Role.Unknown +StackExchange.Redis.Role.Value.get -> string +StackExchange.Redis.SaveType +StackExchange.Redis.SaveType.BackgroundRewriteAppendOnlyFile = 0 -> StackExchange.Redis.SaveType +StackExchange.Redis.SaveType.BackgroundSave = 1 -> StackExchange.Redis.SaveType +StackExchange.Redis.SaveType.ForegroundSave = 2 -> StackExchange.Redis.SaveType +StackExchange.Redis.ServerCounters +StackExchange.Redis.ServerCounters.EndPoint.get -> System.Net.EndPoint +StackExchange.Redis.ServerCounters.Interactive.get -> StackExchange.Redis.ConnectionCounters +StackExchange.Redis.ServerCounters.Other.get -> StackExchange.Redis.ConnectionCounters +StackExchange.Redis.ServerCounters.ServerCounters(System.Net.EndPoint endpoint) -> void +StackExchange.Redis.ServerCounters.Subscription.get -> StackExchange.Redis.ConnectionCounters +StackExchange.Redis.ServerCounters.TotalOutstanding.get -> long +StackExchange.Redis.ServerType +StackExchange.Redis.ServerType.Cluster = 2 -> StackExchange.Redis.ServerType +StackExchange.Redis.ServerType.Envoyproxy = 4 -> StackExchange.Redis.ServerType +StackExchange.Redis.ServerType.Sentinel = 1 -> StackExchange.Redis.ServerType +StackExchange.Redis.ServerType.Standalone = 0 -> StackExchange.Redis.ServerType +StackExchange.Redis.ServerType.Twemproxy = 3 -> StackExchange.Redis.ServerType +StackExchange.Redis.SetOperation +StackExchange.Redis.SetOperation.Difference = 2 -> StackExchange.Redis.SetOperation +StackExchange.Redis.SetOperation.Intersect = 1 -> StackExchange.Redis.SetOperation +StackExchange.Redis.SetOperation.Union = 0 -> StackExchange.Redis.SetOperation +StackExchange.Redis.ShutdownMode +StackExchange.Redis.ShutdownMode.Always = 2 -> StackExchange.Redis.ShutdownMode +StackExchange.Redis.ShutdownMode.Default = 0 -> StackExchange.Redis.ShutdownMode +StackExchange.Redis.ShutdownMode.Never = 1 -> StackExchange.Redis.ShutdownMode +StackExchange.Redis.SlotRange +StackExchange.Redis.SlotRange.CompareTo(StackExchange.Redis.SlotRange other) -> int +StackExchange.Redis.SlotRange.Equals(StackExchange.Redis.SlotRange other) -> bool +StackExchange.Redis.SlotRange.From.get -> int +StackExchange.Redis.SlotRange.SlotRange() -> void +StackExchange.Redis.SlotRange.SlotRange(int from, int to) -> void +StackExchange.Redis.SlotRange.To.get -> int +StackExchange.Redis.SocketManager +StackExchange.Redis.SocketManager.Dispose() -> void +StackExchange.Redis.SocketManager.Name.get -> string +StackExchange.Redis.SocketManager.SocketManager(string name = null, int workerCount = 0, StackExchange.Redis.SocketManager.SocketManagerOptions options = StackExchange.Redis.SocketManager.SocketManagerOptions.None) -> void +StackExchange.Redis.SocketManager.SocketManager(string name) -> void +StackExchange.Redis.SocketManager.SocketManager(string name, bool useHighPrioritySocketThreads) -> void +StackExchange.Redis.SocketManager.SocketManager(string name, int workerCount, bool useHighPrioritySocketThreads) -> void +StackExchange.Redis.SocketManager.SocketManagerOptions +StackExchange.Redis.SocketManager.SocketManagerOptions.None = 0 -> StackExchange.Redis.SocketManager.SocketManagerOptions +StackExchange.Redis.SocketManager.SocketManagerOptions.UseHighPrioritySocketThreads = 1 -> StackExchange.Redis.SocketManager.SocketManagerOptions +StackExchange.Redis.SocketManager.SocketManagerOptions.UseThreadPool = 2 -> StackExchange.Redis.SocketManager.SocketManagerOptions +StackExchange.Redis.SortedSetEntry +StackExchange.Redis.SortedSetEntry.CompareTo(object obj) -> int +StackExchange.Redis.SortedSetEntry.CompareTo(StackExchange.Redis.SortedSetEntry other) -> int +StackExchange.Redis.SortedSetEntry.Element.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.SortedSetEntry.Equals(StackExchange.Redis.SortedSetEntry other) -> bool +StackExchange.Redis.SortedSetEntry.Key.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.SortedSetEntry.Score.get -> double +StackExchange.Redis.SortedSetEntry.SortedSetEntry() -> void +StackExchange.Redis.SortedSetEntry.SortedSetEntry(StackExchange.Redis.RedisValue element, double score) -> void +StackExchange.Redis.SortedSetEntry.Value.get -> double +StackExchange.Redis.SortType +StackExchange.Redis.SortType.Alphabetic = 1 -> StackExchange.Redis.SortType +StackExchange.Redis.SortType.Numeric = 0 -> StackExchange.Redis.SortType +StackExchange.Redis.StreamConsumer +StackExchange.Redis.StreamConsumer.Name.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.StreamConsumer.PendingMessageCount.get -> int +StackExchange.Redis.StreamConsumer.StreamConsumer() -> void +StackExchange.Redis.StreamConsumerInfo +StackExchange.Redis.StreamConsumerInfo.IdleTimeInMilliseconds.get -> long +StackExchange.Redis.StreamConsumerInfo.Name.get -> string +StackExchange.Redis.StreamConsumerInfo.PendingMessageCount.get -> int +StackExchange.Redis.StreamConsumerInfo.StreamConsumerInfo() -> void +StackExchange.Redis.StreamEntry +StackExchange.Redis.StreamEntry.Id.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.StreamEntry.IsNull.get -> bool +StackExchange.Redis.StreamEntry.StreamEntry() -> void +StackExchange.Redis.StreamEntry.StreamEntry(StackExchange.Redis.RedisValue id, StackExchange.Redis.NameValueEntry[] values) -> void +StackExchange.Redis.StreamEntry.this[StackExchange.Redis.RedisValue fieldName].get -> StackExchange.Redis.RedisValue +StackExchange.Redis.StreamEntry.Values.get -> StackExchange.Redis.NameValueEntry[] +StackExchange.Redis.StreamGroupInfo +StackExchange.Redis.StreamGroupInfo.ConsumerCount.get -> int +StackExchange.Redis.StreamGroupInfo.LastDeliveredId.get -> string +StackExchange.Redis.StreamGroupInfo.Name.get -> string +StackExchange.Redis.StreamGroupInfo.PendingMessageCount.get -> int +StackExchange.Redis.StreamGroupInfo.StreamGroupInfo() -> void +StackExchange.Redis.StreamInfo +StackExchange.Redis.StreamInfo.ConsumerGroupCount.get -> int +StackExchange.Redis.StreamInfo.FirstEntry.get -> StackExchange.Redis.StreamEntry +StackExchange.Redis.StreamInfo.LastEntry.get -> StackExchange.Redis.StreamEntry +StackExchange.Redis.StreamInfo.LastGeneratedId.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.StreamInfo.Length.get -> int +StackExchange.Redis.StreamInfo.RadixTreeKeys.get -> int +StackExchange.Redis.StreamInfo.RadixTreeNodes.get -> int +StackExchange.Redis.StreamInfo.StreamInfo() -> void +StackExchange.Redis.StreamPendingInfo +StackExchange.Redis.StreamPendingInfo.Consumers.get -> StackExchange.Redis.StreamConsumer[] +StackExchange.Redis.StreamPendingInfo.HighestPendingMessageId.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.StreamPendingInfo.LowestPendingMessageId.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.StreamPendingInfo.PendingMessageCount.get -> int +StackExchange.Redis.StreamPendingInfo.StreamPendingInfo() -> void +StackExchange.Redis.StreamPendingMessageInfo +StackExchange.Redis.StreamPendingMessageInfo.ConsumerName.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.StreamPendingMessageInfo.DeliveryCount.get -> int +StackExchange.Redis.StreamPendingMessageInfo.IdleTimeInMilliseconds.get -> long +StackExchange.Redis.StreamPendingMessageInfo.MessageId.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.StreamPendingMessageInfo.StreamPendingMessageInfo() -> void +StackExchange.Redis.StreamPosition +StackExchange.Redis.StreamPosition.Key.get -> StackExchange.Redis.RedisKey +StackExchange.Redis.StreamPosition.Position.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.StreamPosition.StreamPosition() -> void +StackExchange.Redis.StreamPosition.StreamPosition(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue position) -> void +StackExchange.Redis.When +StackExchange.Redis.When.Always = 0 -> StackExchange.Redis.When +StackExchange.Redis.When.Exists = 1 -> StackExchange.Redis.When +StackExchange.Redis.When.NotExists = 2 -> StackExchange.Redis.When +static StackExchange.Redis.BacklogPolicy.Default.get -> StackExchange.Redis.BacklogPolicy +static StackExchange.Redis.BacklogPolicy.FailFast.get -> StackExchange.Redis.BacklogPolicy +static StackExchange.Redis.ChannelMessage.operator !=(StackExchange.Redis.ChannelMessage left, StackExchange.Redis.ChannelMessage right) -> bool +static StackExchange.Redis.ChannelMessage.operator ==(StackExchange.Redis.ChannelMessage left, StackExchange.Redis.ChannelMessage right) -> bool +static StackExchange.Redis.CommandMap.Create(System.Collections.Generic.Dictionary overrides) -> StackExchange.Redis.CommandMap +static StackExchange.Redis.CommandMap.Create(System.Collections.Generic.HashSet commands, bool available = true) -> StackExchange.Redis.CommandMap +static StackExchange.Redis.CommandMap.Default.get -> StackExchange.Redis.CommandMap +static StackExchange.Redis.CommandMap.Envoyproxy.get -> StackExchange.Redis.CommandMap +static StackExchange.Redis.CommandMap.Sentinel.get -> StackExchange.Redis.CommandMap +static StackExchange.Redis.CommandMap.SSDB.get -> StackExchange.Redis.CommandMap +static StackExchange.Redis.CommandMap.Twemproxy.get -> StackExchange.Redis.CommandMap +static StackExchange.Redis.Condition.HashEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.HashExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.HashLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.HashLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.HashLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.HashNotEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.HashNotExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.KeyExists(StackExchange.Redis.RedisKey key) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.KeyNotExists(StackExchange.Redis.RedisKey key) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.ListIndexEqual(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.ListIndexExists(StackExchange.Redis.RedisKey key, long index) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.ListIndexNotEqual(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.ListIndexNotExists(StackExchange.Redis.RedisKey key, long index) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.ListLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.ListLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.ListLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SetLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SetLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SetLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SetNotContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SortedSetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SortedSetEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.RedisValue score) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SortedSetLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SortedSetLengthEqual(StackExchange.Redis.RedisKey key, long length, double min = -Infinity, double max = Infinity) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SortedSetLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SortedSetLengthGreaterThan(StackExchange.Redis.RedisKey key, long length, double min = -Infinity, double max = Infinity) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SortedSetLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SortedSetLengthLessThan(StackExchange.Redis.RedisKey key, long length, double min = -Infinity, double max = Infinity) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SortedSetNotContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SortedSetNotEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.RedisValue score) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SortedSetScoreExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue score) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SortedSetScoreExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue score, StackExchange.Redis.RedisValue count) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SortedSetScoreNotExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue score) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.SortedSetScoreNotExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue score, StackExchange.Redis.RedisValue count) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.StreamLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.StreamLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.StreamLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.StringEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.StringLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.StringLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.StringLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition +static StackExchange.Redis.Condition.StringNotEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition +static StackExchange.Redis.Configuration.DefaultOptionsProvider.AddProvider(StackExchange.Redis.Configuration.DefaultOptionsProvider provider) -> void +static StackExchange.Redis.Configuration.DefaultOptionsProvider.ComputerName.get -> string +static StackExchange.Redis.Configuration.DefaultOptionsProvider.LibraryVersion.get -> string +static StackExchange.Redis.ConfigurationOptions.Parse(string configuration) -> StackExchange.Redis.ConfigurationOptions +static StackExchange.Redis.ConfigurationOptions.Parse(string configuration, bool ignoreUnknown) -> StackExchange.Redis.ConfigurationOptions +static StackExchange.Redis.ConnectionMultiplexer.Connect(StackExchange.Redis.ConfigurationOptions configuration, System.IO.TextWriter log = null) -> StackExchange.Redis.ConnectionMultiplexer +static StackExchange.Redis.ConnectionMultiplexer.Connect(string configuration, System.Action configure, System.IO.TextWriter log = null) -> StackExchange.Redis.ConnectionMultiplexer +static StackExchange.Redis.ConnectionMultiplexer.Connect(string configuration, System.IO.TextWriter log = null) -> StackExchange.Redis.ConnectionMultiplexer +static StackExchange.Redis.ConnectionMultiplexer.ConnectAsync(StackExchange.Redis.ConfigurationOptions configuration, System.IO.TextWriter log = null) -> System.Threading.Tasks.Task +static StackExchange.Redis.ConnectionMultiplexer.ConnectAsync(string configuration, System.Action configure, System.IO.TextWriter log = null) -> System.Threading.Tasks.Task +static StackExchange.Redis.ConnectionMultiplexer.ConnectAsync(string configuration, System.IO.TextWriter log = null) -> System.Threading.Tasks.Task +static StackExchange.Redis.ConnectionMultiplexer.Factory.get -> System.Threading.Tasks.TaskFactory +static StackExchange.Redis.ConnectionMultiplexer.Factory.set -> void +static StackExchange.Redis.ConnectionMultiplexer.GetFeatureFlag(string flag) -> bool +static StackExchange.Redis.ConnectionMultiplexer.SentinelConnect(StackExchange.Redis.ConfigurationOptions configuration, System.IO.TextWriter log = null) -> StackExchange.Redis.ConnectionMultiplexer +static StackExchange.Redis.ConnectionMultiplexer.SentinelConnect(string configuration, System.IO.TextWriter log = null) -> StackExchange.Redis.ConnectionMultiplexer +static StackExchange.Redis.ConnectionMultiplexer.SentinelConnectAsync(StackExchange.Redis.ConfigurationOptions configuration, System.IO.TextWriter log = null) -> System.Threading.Tasks.Task +static StackExchange.Redis.ConnectionMultiplexer.SentinelConnectAsync(string configuration, System.IO.TextWriter log = null) -> System.Threading.Tasks.Task +static StackExchange.Redis.ConnectionMultiplexer.SetFeatureFlag(string flag, bool enabled) -> void +static StackExchange.Redis.EndPointCollection.ToString(System.Net.EndPoint endpoint) -> string +static StackExchange.Redis.EndPointCollection.TryParse(string endpoint) -> System.Net.EndPoint +static StackExchange.Redis.ExtensionMethods.AsStream(this StackExchange.Redis.Lease bytes, bool ownsLease = true) -> System.IO.Stream +static StackExchange.Redis.ExtensionMethods.DecodeLease(this StackExchange.Redis.Lease bytes, System.Text.Encoding encoding = null) -> StackExchange.Redis.Lease +static StackExchange.Redis.ExtensionMethods.DecodeString(this StackExchange.Redis.Lease bytes, System.Text.Encoding encoding = null) -> string +static StackExchange.Redis.ExtensionMethods.ToDictionary(this StackExchange.Redis.HashEntry[] hash) -> System.Collections.Generic.Dictionary +static StackExchange.Redis.ExtensionMethods.ToDictionary(this StackExchange.Redis.SortedSetEntry[] sortedSet) -> System.Collections.Generic.Dictionary +static StackExchange.Redis.ExtensionMethods.ToDictionary(this System.Collections.Generic.KeyValuePair[] pairs) -> System.Collections.Generic.Dictionary +static StackExchange.Redis.ExtensionMethods.ToDictionary(this System.Collections.Generic.KeyValuePair[] pairs) -> System.Collections.Generic.Dictionary +static StackExchange.Redis.ExtensionMethods.ToRedisValueArray(this string[] values) -> StackExchange.Redis.RedisValue[] +static StackExchange.Redis.ExtensionMethods.ToStringArray(this StackExchange.Redis.RedisValue[] values) -> string[] +static StackExchange.Redis.ExtensionMethods.ToStringDictionary(this StackExchange.Redis.HashEntry[] hash) -> System.Collections.Generic.Dictionary +static StackExchange.Redis.ExtensionMethods.ToStringDictionary(this StackExchange.Redis.SortedSetEntry[] sortedSet) -> System.Collections.Generic.Dictionary +static StackExchange.Redis.ExtensionMethods.ToStringDictionary(this System.Collections.Generic.KeyValuePair[] pairs) -> System.Collections.Generic.Dictionary +static StackExchange.Redis.GeoEntry.operator !=(StackExchange.Redis.GeoEntry x, StackExchange.Redis.GeoEntry y) -> bool +static StackExchange.Redis.GeoEntry.operator ==(StackExchange.Redis.GeoEntry x, StackExchange.Redis.GeoEntry y) -> bool +static StackExchange.Redis.GeoPosition.operator !=(StackExchange.Redis.GeoPosition x, StackExchange.Redis.GeoPosition y) -> bool +static StackExchange.Redis.GeoPosition.operator ==(StackExchange.Redis.GeoPosition x, StackExchange.Redis.GeoPosition y) -> bool +static StackExchange.Redis.HashEntry.implicit operator StackExchange.Redis.HashEntry(System.Collections.Generic.KeyValuePair value) -> StackExchange.Redis.HashEntry +static StackExchange.Redis.HashEntry.implicit operator System.Collections.Generic.KeyValuePair(StackExchange.Redis.HashEntry value) -> System.Collections.Generic.KeyValuePair +static StackExchange.Redis.HashEntry.operator !=(StackExchange.Redis.HashEntry x, StackExchange.Redis.HashEntry y) -> bool +static StackExchange.Redis.HashEntry.operator ==(StackExchange.Redis.HashEntry x, StackExchange.Redis.HashEntry y) -> bool +static StackExchange.Redis.KeyspaceIsolation.DatabaseExtensions.WithKeyPrefix(this StackExchange.Redis.IDatabase database, StackExchange.Redis.RedisKey keyPrefix) -> StackExchange.Redis.IDatabase +static StackExchange.Redis.Lease.Create(int length, bool clear = true) -> StackExchange.Redis.Lease +static StackExchange.Redis.Lease.Empty.get -> StackExchange.Redis.Lease +static StackExchange.Redis.LuaScript.GetCachedScriptCount() -> int +static StackExchange.Redis.LuaScript.Prepare(string script) -> StackExchange.Redis.LuaScript +static StackExchange.Redis.LuaScript.PurgeCache() -> void +static StackExchange.Redis.NameValueEntry.implicit operator StackExchange.Redis.NameValueEntry(System.Collections.Generic.KeyValuePair value) -> StackExchange.Redis.NameValueEntry +static StackExchange.Redis.NameValueEntry.implicit operator System.Collections.Generic.KeyValuePair(StackExchange.Redis.NameValueEntry value) -> System.Collections.Generic.KeyValuePair +static StackExchange.Redis.NameValueEntry.operator !=(StackExchange.Redis.NameValueEntry x, StackExchange.Redis.NameValueEntry y) -> bool +static StackExchange.Redis.NameValueEntry.operator ==(StackExchange.Redis.NameValueEntry x, StackExchange.Redis.NameValueEntry y) -> bool +static StackExchange.Redis.RedisChannel.implicit operator byte[](StackExchange.Redis.RedisChannel key) -> byte[] +static StackExchange.Redis.RedisChannel.implicit operator StackExchange.Redis.RedisChannel(byte[] key) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.implicit operator StackExchange.Redis.RedisChannel(string key) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.implicit operator string(StackExchange.Redis.RedisChannel key) -> string +static StackExchange.Redis.RedisChannel.operator !=(byte[] x, StackExchange.Redis.RedisChannel y) -> bool +static StackExchange.Redis.RedisChannel.operator !=(StackExchange.Redis.RedisChannel x, byte[] y) -> bool +static StackExchange.Redis.RedisChannel.operator !=(StackExchange.Redis.RedisChannel x, StackExchange.Redis.RedisChannel y) -> bool +static StackExchange.Redis.RedisChannel.operator !=(StackExchange.Redis.RedisChannel x, string y) -> bool +static StackExchange.Redis.RedisChannel.operator !=(string x, StackExchange.Redis.RedisChannel y) -> bool +static StackExchange.Redis.RedisChannel.operator ==(byte[] x, StackExchange.Redis.RedisChannel y) -> bool +static StackExchange.Redis.RedisChannel.operator ==(StackExchange.Redis.RedisChannel x, byte[] y) -> bool +static StackExchange.Redis.RedisChannel.operator ==(StackExchange.Redis.RedisChannel x, StackExchange.Redis.RedisChannel y) -> bool +static StackExchange.Redis.RedisChannel.operator ==(StackExchange.Redis.RedisChannel x, string y) -> bool +static StackExchange.Redis.RedisChannel.operator ==(string x, StackExchange.Redis.RedisChannel y) -> bool +static StackExchange.Redis.RedisFeatures.operator !=(StackExchange.Redis.RedisFeatures left, StackExchange.Redis.RedisFeatures right) -> bool +static StackExchange.Redis.RedisFeatures.operator ==(StackExchange.Redis.RedisFeatures left, StackExchange.Redis.RedisFeatures right) -> bool +static StackExchange.Redis.RedisKey.implicit operator byte[](StackExchange.Redis.RedisKey key) -> byte[] +static StackExchange.Redis.RedisKey.implicit operator StackExchange.Redis.RedisKey(byte[] key) -> StackExchange.Redis.RedisKey +static StackExchange.Redis.RedisKey.implicit operator StackExchange.Redis.RedisKey(string key) -> StackExchange.Redis.RedisKey +static StackExchange.Redis.RedisKey.implicit operator string(StackExchange.Redis.RedisKey key) -> string +static StackExchange.Redis.RedisKey.operator !=(byte[] x, StackExchange.Redis.RedisKey y) -> bool +static StackExchange.Redis.RedisKey.operator !=(StackExchange.Redis.RedisKey x, byte[] y) -> bool +static StackExchange.Redis.RedisKey.operator !=(StackExchange.Redis.RedisKey x, StackExchange.Redis.RedisKey y) -> bool +static StackExchange.Redis.RedisKey.operator !=(StackExchange.Redis.RedisKey x, string y) -> bool +static StackExchange.Redis.RedisKey.operator !=(string x, StackExchange.Redis.RedisKey y) -> bool +static StackExchange.Redis.RedisKey.operator +(StackExchange.Redis.RedisKey x, StackExchange.Redis.RedisKey y) -> StackExchange.Redis.RedisKey +static StackExchange.Redis.RedisKey.operator ==(byte[] x, StackExchange.Redis.RedisKey y) -> bool +static StackExchange.Redis.RedisKey.operator ==(StackExchange.Redis.RedisKey x, byte[] y) -> bool +static StackExchange.Redis.RedisKey.operator ==(StackExchange.Redis.RedisKey x, StackExchange.Redis.RedisKey y) -> bool +static StackExchange.Redis.RedisKey.operator ==(StackExchange.Redis.RedisKey x, string y) -> bool +static StackExchange.Redis.RedisKey.operator ==(string x, StackExchange.Redis.RedisKey y) -> bool +static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisChannel channel) -> StackExchange.Redis.RedisResult +static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisKey key) -> StackExchange.Redis.RedisResult +static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisResult[] values) -> StackExchange.Redis.RedisResult +static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue value, StackExchange.Redis.ResultType? resultType = null) -> StackExchange.Redis.RedisResult +static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[] values) -> StackExchange.Redis.RedisResult +static StackExchange.Redis.RedisResult.explicit operator bool(StackExchange.Redis.RedisResult result) -> bool +static StackExchange.Redis.RedisResult.explicit operator bool?(StackExchange.Redis.RedisResult result) -> bool? +static StackExchange.Redis.RedisResult.explicit operator bool[](StackExchange.Redis.RedisResult result) -> bool[] +static StackExchange.Redis.RedisResult.explicit operator byte[](StackExchange.Redis.RedisResult result) -> byte[] +static StackExchange.Redis.RedisResult.explicit operator byte[][](StackExchange.Redis.RedisResult result) -> byte[][] +static StackExchange.Redis.RedisResult.explicit operator double(StackExchange.Redis.RedisResult result) -> double +static StackExchange.Redis.RedisResult.explicit operator double?(StackExchange.Redis.RedisResult result) -> double? +static StackExchange.Redis.RedisResult.explicit operator double[](StackExchange.Redis.RedisResult result) -> double[] +static StackExchange.Redis.RedisResult.explicit operator int(StackExchange.Redis.RedisResult result) -> int +static StackExchange.Redis.RedisResult.explicit operator int?(StackExchange.Redis.RedisResult result) -> int? +static StackExchange.Redis.RedisResult.explicit operator int[](StackExchange.Redis.RedisResult result) -> int[] +static StackExchange.Redis.RedisResult.explicit operator long(StackExchange.Redis.RedisResult result) -> long +static StackExchange.Redis.RedisResult.explicit operator long?(StackExchange.Redis.RedisResult result) -> long? +static StackExchange.Redis.RedisResult.explicit operator long[](StackExchange.Redis.RedisResult result) -> long[] +static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisKey(StackExchange.Redis.RedisResult result) -> StackExchange.Redis.RedisKey +static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisKey[](StackExchange.Redis.RedisResult result) -> StackExchange.Redis.RedisKey[] +static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisResult[](StackExchange.Redis.RedisResult result) -> StackExchange.Redis.RedisResult[] +static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisValue(StackExchange.Redis.RedisResult result) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisValue[](StackExchange.Redis.RedisResult result) -> StackExchange.Redis.RedisValue[] +static StackExchange.Redis.RedisResult.explicit operator string(StackExchange.Redis.RedisResult result) -> string +static StackExchange.Redis.RedisResult.explicit operator string[](StackExchange.Redis.RedisResult result) -> string[] +static StackExchange.Redis.RedisResult.explicit operator ulong(StackExchange.Redis.RedisResult result) -> ulong +static StackExchange.Redis.RedisResult.explicit operator ulong?(StackExchange.Redis.RedisResult result) -> ulong? +static StackExchange.Redis.RedisResult.explicit operator ulong[](StackExchange.Redis.RedisResult result) -> ulong[] +static StackExchange.Redis.RedisValue.CreateFrom(System.IO.MemoryStream stream) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.EmptyString.get -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.explicit operator bool(StackExchange.Redis.RedisValue value) -> bool +static StackExchange.Redis.RedisValue.explicit operator bool?(StackExchange.Redis.RedisValue value) -> bool? +static StackExchange.Redis.RedisValue.explicit operator decimal(StackExchange.Redis.RedisValue value) -> decimal +static StackExchange.Redis.RedisValue.explicit operator decimal?(StackExchange.Redis.RedisValue value) -> decimal? +static StackExchange.Redis.RedisValue.explicit operator double(StackExchange.Redis.RedisValue value) -> double +static StackExchange.Redis.RedisValue.explicit operator double?(StackExchange.Redis.RedisValue value) -> double? +static StackExchange.Redis.RedisValue.explicit operator float(StackExchange.Redis.RedisValue value) -> float +static StackExchange.Redis.RedisValue.explicit operator float?(StackExchange.Redis.RedisValue value) -> float? +static StackExchange.Redis.RedisValue.explicit operator int(StackExchange.Redis.RedisValue value) -> int +static StackExchange.Redis.RedisValue.explicit operator int?(StackExchange.Redis.RedisValue value) -> int? +static StackExchange.Redis.RedisValue.explicit operator long(StackExchange.Redis.RedisValue value) -> long +static StackExchange.Redis.RedisValue.explicit operator long?(StackExchange.Redis.RedisValue value) -> long? +static StackExchange.Redis.RedisValue.explicit operator uint(StackExchange.Redis.RedisValue value) -> uint +static StackExchange.Redis.RedisValue.explicit operator uint?(StackExchange.Redis.RedisValue value) -> uint? +static StackExchange.Redis.RedisValue.explicit operator ulong(StackExchange.Redis.RedisValue value) -> ulong +static StackExchange.Redis.RedisValue.explicit operator ulong?(StackExchange.Redis.RedisValue value) -> ulong? +static StackExchange.Redis.RedisValue.implicit operator byte[](StackExchange.Redis.RedisValue value) -> byte[] +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(bool value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(bool? value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(byte[] value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(double value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(double? value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(int value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(int? value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(long value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(long? value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(string value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(System.Memory value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(System.ReadOnlyMemory value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(uint value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(uint? value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(ulong value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(ulong? value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator string(StackExchange.Redis.RedisValue value) -> string +static StackExchange.Redis.RedisValue.implicit operator System.ReadOnlyMemory(StackExchange.Redis.RedisValue value) -> System.ReadOnlyMemory +static StackExchange.Redis.RedisValue.Null.get -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.operator !=(StackExchange.Redis.RedisValue x, StackExchange.Redis.RedisValue y) -> bool +static StackExchange.Redis.RedisValue.operator ==(StackExchange.Redis.RedisValue x, StackExchange.Redis.RedisValue y) -> bool +static StackExchange.Redis.RedisValue.Unbox(object value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.SlotRange.operator !=(StackExchange.Redis.SlotRange x, StackExchange.Redis.SlotRange y) -> bool +static StackExchange.Redis.SlotRange.operator ==(StackExchange.Redis.SlotRange x, StackExchange.Redis.SlotRange y) -> bool +static StackExchange.Redis.SlotRange.TryParse(string range, out StackExchange.Redis.SlotRange value) -> bool +static StackExchange.Redis.SocketManager.Shared.get -> StackExchange.Redis.SocketManager +static StackExchange.Redis.SocketManager.ThreadPool.get -> StackExchange.Redis.SocketManager +static StackExchange.Redis.SortedSetEntry.implicit operator StackExchange.Redis.SortedSetEntry(System.Collections.Generic.KeyValuePair value) -> StackExchange.Redis.SortedSetEntry +static StackExchange.Redis.SortedSetEntry.implicit operator System.Collections.Generic.KeyValuePair(StackExchange.Redis.SortedSetEntry value) -> System.Collections.Generic.KeyValuePair +static StackExchange.Redis.SortedSetEntry.operator !=(StackExchange.Redis.SortedSetEntry x, StackExchange.Redis.SortedSetEntry y) -> bool +static StackExchange.Redis.SortedSetEntry.operator ==(StackExchange.Redis.SortedSetEntry x, StackExchange.Redis.SortedSetEntry y) -> bool +static StackExchange.Redis.StreamEntry.Null.get -> StackExchange.Redis.StreamEntry +static StackExchange.Redis.StreamPosition.Beginning.get -> StackExchange.Redis.RedisValue +static StackExchange.Redis.StreamPosition.NewMessages.get -> StackExchange.Redis.RedisValue +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.AbortOnConnectFail.get -> bool +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.AfterConnectAsync(StackExchange.Redis.ConnectionMultiplexer multiplexer, System.Action log) -> System.Threading.Tasks.Task +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.AllowAdmin.get -> bool +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.BacklogPolicy.get -> StackExchange.Redis.BacklogPolicy +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.CheckCertificateRevocation.get -> bool +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.CommandMap.get -> StackExchange.Redis.CommandMap +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ConfigCheckInterval.get -> System.TimeSpan +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ConfigurationChannel.get -> string +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ConnectRetry.get -> int +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ConnectTimeout.get -> System.TimeSpan? +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.DefaultVersion.get -> System.Version +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetDefaultClientName() -> string +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetDefaultSsl(StackExchange.Redis.EndPointCollection endPoints) -> bool +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetSslHostFromEndpoints(StackExchange.Redis.EndPointCollection endPoints) -> string +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IsMatch(System.Net.EndPoint endpoint) -> bool +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.KeepAliveInterval.get -> System.TimeSpan +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Proxy.get -> StackExchange.Redis.Proxy +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ReconnectRetryPolicy.get -> StackExchange.Redis.IReconnectRetryPolicy +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ResolveDns.get -> bool +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.SyncTimeout.get -> System.TimeSpan +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.TieBreaker.get -> string \ No newline at end of file diff --git a/src/StackExchange.Redis/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..5f282702b --- /dev/null +++ b/src/StackExchange.Redis/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 852fed58e..b7cde797f 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -29,4 +29,9 @@ + + + + + \ No newline at end of file From 1907f07bd9c59a5b4452e217bff243e0a56921f8 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 18 Mar 2022 09:12:09 -0400 Subject: [PATCH 107/435] String SET: add support for KEEPTTL (#2029) Fix for #1920, adding support for the `KEEPTTL` flag on `SET`, present since 6.0.0. Thoughts: - This needs careful eyes for no breaking changes - Should we add additional checks if _both_ expiry and keepTtl are passed? In code: keepTtl only works if expiry is null, but we don't explicitly throw. --- docs/ReleaseNotes.md | 1 + .../Interfaces/IDatabase.cs | 31 +++- .../Interfaces/IDatabaseAsync.cs | 31 +++- .../KeyspaceIsolation/DatabaseWrapper.cs | 14 +- .../KeyspaceIsolation/WrapperBase.cs | 14 +- src/StackExchange.Redis/PublicAPI.Shipped.txt | 15 +- src/StackExchange.Redis/RedisDatabase.cs | 139 +++++++++++------- src/StackExchange.Redis/RedisFeatures.cs | 26 ++-- src/StackExchange.Redis/RedisLiterals.cs | 1 + .../DatabaseWrapperTests.cs | 8 + tests/StackExchange.Redis.Tests/Strings.cs | 52 +++++++ .../WrapperBaseTests.cs | 8 + 12 files changed, 262 insertions(+), 78 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 53e00ecae..e11989cb7 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -3,6 +3,7 @@ ## Unreleased - Fix [#1988](https://github.com/StackExchange/StackExchange.Redis/issues/1988): Don't issue `SELECT` commands if explicitly disabled ([#2023 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2023)) +- Adds: `KEEPTTL` support on `SET` operations ([#2029 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2029)) - Fix: Allow `XTRIM` `MAXLEN` argument to be `0` ([#2030 by NicoAvanzDev](https://github.com/StackExchange/StackExchange.Redis/pull/2030)) - Adds: `ConfigurationOptions.BeforeSocketConnect` for configuring sockets between creation and connection ([#2031 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2031)) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 500047dd2..578637d29 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -2188,7 +2188,20 @@ IEnumerable SortedSetScan(RedisKey key, /// The flags to use for this operation. /// if the string was set, otherwise. /// https://redis.io/commands/set - bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None); + bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags); + + /// + /// Set key to hold the string value. If key already holds a value, it is overwritten, regardless of its type. + /// + /// The key of the string. + /// The value to set. + /// The expiry to set. + /// Whether to maintain the existing key's TTL (KEEPTTL flag). + /// Which condition to set the value under (defaults to always). + /// The flags to use for this operation. + /// if the string was set, otherwise. + /// https://redis.io/commands/set + bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); /// /// Sets the given keys to their respective values. @@ -2213,7 +2226,21 @@ IEnumerable SortedSetScan(RedisKey key, /// The previous value stored at , or nil when key did not exist. /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. /// https://redis.io/commands/set - RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None); + RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags); + + /// + /// Atomically sets key to value and returns the previous value (if any) stored at . + /// + /// The key of the string. + /// The value to set. + /// The expiry to set. + /// Whether to maintain the existing key's TTL (KEEPTTL flag). + /// Which condition to set the value under (defaults to ). + /// The flags to use for this operation. + /// The previous value stored at , or nil when key did not exist. + /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. + /// https://redis.io/commands/set + RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); /// /// Sets or clears the bit at offset in the string value stored at key. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 5dddaeefd..6210a01fc 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -2141,7 +2141,20 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The flags to use for this operation. /// if the string was set, otherwise. /// https://redis.io/commands/set - Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None); + Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags); + + /// + /// Set key to hold the string value. If key already holds a value, it is overwritten, regardless of its type. + /// + /// The key of the string. + /// The value to set. + /// The expiry to set. + /// Whether to maintain the existing key's TTL (KEEPTTL flag). + /// Which condition to set the value under (defaults to always). + /// The flags to use for this operation. + /// if the string was set, otherwise. + /// https://redis.io/commands/set + Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); /// /// Sets the given keys to their respective values. @@ -2166,7 +2179,21 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The previous value stored at , or nil when key did not exist. /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. /// https://redis.io/commands/set - Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None); + Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags); + + /// + /// Atomically sets key to value and returns the previous value (if any) stored at . + /// + /// The key of the string. + /// The value to set. + /// The expiry to set. + /// Whether to maintain the existing key's TTL (KEEPTTL flag). + /// Which condition to set the value under (defaults to ). + /// The flags to use for this operation. + /// The previous value stored at , or nil when key did not exist. + /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. + /// https://redis.io/commands/set + Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); /// /// Sets or clears the bit at offset in the string value stored at key. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 376279ae0..4153661aa 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -871,16 +871,26 @@ public bool StringSet(KeyValuePair[] values, When when = W return Inner.StringSet(ToInner(values), when, flags); } - public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) { return Inner.StringSet(ToInner(key), value, expiry, when, flags); } - public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + return Inner.StringSet(ToInner(key), value, expiry, keepTtl, when, flags); + } + + public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) { return Inner.StringSetAndGet(ToInner(key), value, expiry, when, flags); } + public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + return Inner.StringSetAndGet(ToInner(key), value, expiry, keepTtl, when, flags); + } + public bool StringSetBit(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) { return Inner.StringSetBit(ToInner(key), offset, bit, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index d054f0a85..008318223 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -862,16 +862,26 @@ public Task StringSetAsync(KeyValuePair[] values, Wh return Inner.StringSetAsync(ToInner(values), when, flags); } - public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) { return Inner.StringSetAsync(ToInner(key), value, expiry, when, flags); } - public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + return Inner.StringSetAsync(ToInner(key), value, expiry, keepTtl, when, flags); + } + + public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) { return Inner.StringSetAndGetAsync(ToInner(key), value, expiry, when, flags); } + public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + return Inner.StringSetAndGetAsync(ToInner(key), value, expiry, keepTtl, when, flags); + } + public Task StringSetBitAsync(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) { return Inner.StringSetBitAsync(ToInner(key), offset, bit, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 0f4087370..ac04a1a5e 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -646,9 +646,11 @@ StackExchange.Redis.IDatabase.StringGetWithExpiry(StackExchange.Redis.RedisKey k StackExchange.Redis.IDatabase.StringIncrement(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double StackExchange.Redis.IDatabase.StringIncrement(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> bool StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.StringSetAndGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StringSetAndGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StringSetAndGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.StringSetBit(StackExchange.Redis.RedisKey key, long offset, bool bit, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.StringSetRange(StackExchange.Redis.RedisKey key, long offset, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabaseAsync @@ -827,8 +829,10 @@ StackExchange.Redis.IDatabaseAsync.StringGetWithExpiryAsync(StackExchange.Redis. StackExchange.Redis.IDatabaseAsync.StringIncrementAsync(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task StackExchange.Redis.IDatabaseAsync.StringIncrementAsync(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task StackExchange.Redis.IDatabaseAsync.StringLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task StackExchange.Redis.IDatabaseAsync.StringSetAsync(System.Collections.Generic.KeyValuePair[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task StackExchange.Redis.IDatabaseAsync.StringSetBitAsync(StackExchange.Redis.RedisKey key, long offset, bool bit, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task StackExchange.Redis.IDatabaseAsync.StringSetRangeAsync(StackExchange.Redis.RedisKey key, long offset, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task @@ -1151,6 +1155,7 @@ StackExchange.Redis.RedisFeatures.Scripting.get -> bool StackExchange.Redis.RedisFeatures.ScriptingDatabaseSafe.get -> bool StackExchange.Redis.RedisFeatures.SetAndGet.get -> bool StackExchange.Redis.RedisFeatures.SetConditional.get -> bool +StackExchange.Redis.RedisFeatures.SetKeepTtl.get -> bool StackExchange.Redis.RedisFeatures.SetNotExistsAndGet.get -> bool StackExchange.Redis.RedisFeatures.SetPopMultiple.get -> bool StackExchange.Redis.RedisFeatures.SetVaradicAddRemove.get -> bool @@ -1591,4 +1596,4 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Proxy.get -> St virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ReconnectRetryPolicy.get -> StackExchange.Redis.IReconnectRetryPolicy virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ResolveDns.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.SyncTimeout.get -> System.TimeSpan -virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.TieBreaker.get -> string \ No newline at end of file +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.TieBreaker.get -> string diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 1aa3a84d0..d1cf0070c 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -2564,9 +2564,12 @@ public Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFl return ExecuteAsync(msg, ResultProcessor.Int64); } - public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + StringSet(key, value, expiry, false, when, flags); + + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetStringSetMessage(key, value, expiry, when, flags); + var msg = GetStringSetMessage(key, value, expiry, keepTtl, when, flags); return ExecuteSync(msg, ResultProcessor.Boolean); } @@ -2576,9 +2579,12 @@ public bool StringSet(KeyValuePair[] values, When when = W return ExecuteSync(msg, ResultProcessor.Boolean); } - public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + StringSetAsync(key, value, expiry, false, when, flags); + + public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetStringSetMessage(key, value, expiry, when, flags); + var msg = GetStringSetMessage(key, value, expiry, keepTtl, when, flags); return ExecuteAsync(msg, ResultProcessor.Boolean); } @@ -2588,15 +2594,21 @@ public Task StringSetAsync(KeyValuePair[] values, Wh return ExecuteAsync(msg, ResultProcessor.Boolean); } - public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + StringSetAndGet(key, value, expiry, false, when, flags); + + public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetStringSetAndGetMessage(key, value, expiry, when, flags); + var msg = GetStringSetAndGetMessage(key, value, expiry, keepTtl, when, flags); return ExecuteSync(msg, ResultProcessor.RedisValue); } - public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + StringSetAndGetAsync(key, value, expiry, false, when, flags); + + public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetStringSetAndGetMessage(key, value, expiry, when, flags); + var msg = GetStringSetAndGetMessage(key, value, expiry, keepTtl, when, flags); return ExecuteAsync(msg, ResultProcessor.RedisValue); } @@ -3486,7 +3498,7 @@ private Message GetStringSetMessage(KeyValuePair[] values, switch (values.Length) { case 0: return null; - case 1: return GetStringSetMessage(values[0].Key, values[0].Value, null, when, flags); + case 1: return GetStringSetMessage(values[0].Key, values[0].Value, null, false, when, flags); default: WhenAlwaysOrNotExists(when); int slot = ServerSelectionStrategy.NoSlot, offset = 0; @@ -3502,19 +3514,30 @@ private Message GetStringSetMessage(KeyValuePair[] values, } } - private Message GetStringSetMessage(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + private Message GetStringSetMessage( + RedisKey key, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) { WhenAlwaysOrExistsOrNotExists(when); if (value.IsNull) return Message.Create(Database, flags, RedisCommand.DEL, key); if (expiry == null || expiry.Value == TimeSpan.MaxValue) - { // no expiry - switch (when) + { + // no expiry + return when switch { - case When.Always: return Message.Create(Database, flags, RedisCommand.SET, key, value); - case When.NotExists: return Message.Create(Database, flags, RedisCommand.SETNX, key, value); - case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX); - } + When.Always when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value), + When.Always when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.KEEPTTL), + When.NotExists when !keepTtl => Message.Create(Database, flags, RedisCommand.SETNX, key, value), + When.NotExists when keepTtl => Message.Create(Database, flags, RedisCommand.SETNX, key, value, RedisLiterals.KEEPTTL), + When.Exists when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX), + When.Exists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.KEEPTTL), + _ => throw new ArgumentOutOfRangeException(nameof(when)), + }; } long milliseconds = expiry.Value.Ticks / TimeSpan.TicksPerMillisecond; @@ -3522,36 +3545,48 @@ private Message GetStringSetMessage(RedisKey key, RedisValue value, TimeSpan? ex { // a nice round number of seconds long seconds = milliseconds / 1000; - switch (when) + return when switch { - case When.Always: return Message.Create(Database, flags, RedisCommand.SETEX, key, seconds, value); - case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX); - case When.NotExists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.NX); - } + When.Always => Message.Create(Database, flags, RedisCommand.SETEX, key, seconds, value), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX), + When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.NX), + _ => throw new ArgumentOutOfRangeException(nameof(when)), + }; } return when switch { - When.Always => Message.Create(Database, flags, RedisCommand.PSETEX, key, milliseconds, value), - When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX), + When.Always => Message.Create(Database, flags, RedisCommand.PSETEX, key, milliseconds, value), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX), When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.NX), - _ => throw new NotSupportedException(), + _ => throw new ArgumentOutOfRangeException(nameof(when)), }; } - private Message GetStringSetAndGetMessage(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None) + private Message GetStringSetAndGetMessage( + RedisKey key, + RedisValue value, + TimeSpan? expiry = null, + bool keepTtl = false, + When when = When.Always, + CommandFlags flags = CommandFlags.None) { WhenAlwaysOrExistsOrNotExists(when); if (value.IsNull) return Message.Create(Database, flags, RedisCommand.GETDEL, key); if (expiry == null || expiry.Value == TimeSpan.MaxValue) - { // no expiry - switch (when) + { + // no expiry + return when switch { - case When.Always: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.GET); - case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.GET); - case When.NotExists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.NX, RedisLiterals.GET); - } + When.Always when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.GET), + When.Always when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.GET, RedisLiterals.KEEPTTL), + When.Exists when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.GET), + When.Exists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.GET, RedisLiterals.KEEPTTL), + When.NotExists when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.NX, RedisLiterals.GET), + When.NotExists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.NX, RedisLiterals.GET, RedisLiterals.KEEPTTL), + _ => throw new ArgumentOutOfRangeException(nameof(when)), + }; } long milliseconds = expiry.Value.Ticks / TimeSpan.TicksPerMillisecond; @@ -3559,40 +3594,34 @@ private Message GetStringSetAndGetMessage(RedisKey key, RedisValue value, TimeSp { // a nice round number of seconds long seconds = milliseconds / 1000; - switch (when) + return when switch { - case When.Always: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.GET); - case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX, RedisLiterals.GET); - case When.NotExists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.NX, RedisLiterals.GET); - } + When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.GET), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX, RedisLiterals.GET), + When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.NX, RedisLiterals.GET), + _ => throw new ArgumentOutOfRangeException(nameof(when)), + }; } return when switch { - When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.GET), - When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX, RedisLiterals.GET), + When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.GET), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX, RedisLiterals.GET), When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.NX, RedisLiterals.GET), - _ => throw new NotSupportedException(), + _ => throw new ArgumentOutOfRangeException(nameof(when)), }; } - private Message IncrMessage(RedisKey key, long value, CommandFlags flags) + private Message IncrMessage(RedisKey key, long value, CommandFlags flags) => value switch { - switch (value) - { - case 0: - if ((flags & CommandFlags.FireAndForget) != 0) return null; - return Message.Create(Database, flags, RedisCommand.INCRBY, key, value); - case 1: - return Message.Create(Database, flags, RedisCommand.INCR, key); - case -1: - return Message.Create(Database, flags, RedisCommand.DECR, key); - default: - return value > 0 - ? Message.Create(Database, flags, RedisCommand.INCRBY, key, value) - : Message.Create(Database, flags, RedisCommand.DECRBY, key, -value); - } - } + 0 => ((flags & CommandFlags.FireAndForget) != 0) + ? null + : Message.Create(Database, flags, RedisCommand.INCRBY, key, value), + 1 => Message.Create(Database, flags, RedisCommand.INCR, key), + -1 => Message.Create(Database, flags, RedisCommand.DECR, key), + > 0 => Message.Create(Database, flags, RedisCommand.INCRBY, key, value), + _ => Message.Create(Database, flags, RedisCommand.DECRBY, key, -value), + }; private static RedisCommand SetOperationCommand(SetOperation operation, bool store) => operation switch { diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index b349eeafe..a59af8c67 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -36,6 +36,7 @@ public readonly struct RedisFeatures v4_0_0 = new Version(4, 0, 0), v4_9_1 = new Version(4, 9, 1), // 5.0 RC1 is version 4.9.1; // 5.0 RC1 is version 4.9.1 v5_0_0 = new Version(5, 0, 0), + v6_0_0 = new Version(6, 0, 0), v6_2_0 = new Version(6, 2, 0), v6_9_240 = new Version(6, 9, 240); // 7.0 RC1 is version 6.9.240 @@ -75,16 +76,6 @@ public RedisFeatures(Version version) /// public bool GetDelete => Version >= v6_2_0; - /// - /// Does SET support the GET option? - /// - public bool SetAndGet => Version >= v6_2_0; - - /// - /// Does SET allow the NX and GET options to be used together? - /// - public bool SetNotExistsAndGet => Version >= v6_9_240; - /// /// Is HSTRLEN available? /// @@ -150,11 +141,26 @@ public RedisFeatures(Version version) /// public bool Scripting => Version >= v2_5_7; + /// + /// Does SET support the GET option? + /// + public bool SetAndGet => Version >= v6_2_0; + /// /// Does SET have the EX|PX|NX|XX extensions? /// public bool SetConditional => Version >= v2_6_12; + /// + /// Does SET have the KEEPTTL extension? + /// + public bool SetKeepTtl => Version >= v6_0_0; + + /// + /// Does SET allow the NX and GET options to be used together? + /// + public bool SetNotExistsAndGet => Version >= v6_9_240; + /// /// Does SADD support variadic usage? /// diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 79950a2a1..220f1fcf0 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -62,6 +62,7 @@ public static readonly RedisValue HISTORY = "HISTORY", ID = "ID", IDLETIME = "IDLETIME", + KEEPTTL = "KEEPTTL", KILL = "KILL", LATEST = "LATEST", LIMIT = "LIMIT", diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 6c08a3602..061e782a9 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -1162,6 +1162,14 @@ public void StringSet_1() [Fact] public void StringSet_2() + { + TimeSpan? expiry = null; + wrapper.StringSet("key", "value", expiry, true, When.Exists, CommandFlags.None); + mock.Verify(_ => _.StringSet("prefix:key", "value", expiry, true, When.Exists, CommandFlags.None)); + } + + [Fact] + public void StringSet_3() { KeyValuePair[] values = new KeyValuePair[] { new KeyValuePair("a", "x"), new KeyValuePair("b", "y") }; Expression[], bool>> valid = _ => _.Length == 2 && _[0].Key == "prefix:a" && _[0].Value == "x" && _[1].Key == "prefix:b" && _[1].Value == "y"; diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index 99cd35e34..d156e31be 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -194,6 +194,47 @@ public async Task SetNotExists() } } + [Fact] + public async Task SetKeepTtl() + { + using (var muxer = Create()) + { + Skip.IfMissingFeature(muxer, nameof(RedisFeatures.SetKeepTtl), r => r.SetKeepTtl); + + var conn = muxer.GetDatabase(); + var prefix = Me(); + conn.KeyDelete(prefix + "1", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "2", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "3", CommandFlags.FireAndForget); + conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); + conn.StringSet(prefix + "2", "abc", expiry: TimeSpan.FromMinutes(5), flags: CommandFlags.FireAndForget); + conn.StringSet(prefix + "3", "abc", expiry: TimeSpan.FromMinutes(10), flags: CommandFlags.FireAndForget); + + var x0 = conn.KeyTimeToLiveAsync(prefix + "1"); + var x1 = conn.KeyTimeToLiveAsync(prefix + "2"); + var x2 = conn.KeyTimeToLiveAsync(prefix + "3"); + + Assert.Null(await x0); + Assert.True(await x1 > TimeSpan.FromMinutes(4), "Over 4"); + Assert.True(await x1 <= TimeSpan.FromMinutes(5), "Under 5"); + Assert.True(await x2 > TimeSpan.FromMinutes(9), "Over 9"); + Assert.True(await x2 <= TimeSpan.FromMinutes(10), "Under 10"); + + conn.StringSet(prefix + "1", "def", keepTtl: true, flags: CommandFlags.FireAndForget); + conn.StringSet(prefix + "2", "def", flags: CommandFlags.FireAndForget); + conn.StringSet(prefix + "3", "def", keepTtl: true, flags: CommandFlags.FireAndForget); + + var y0 = conn.KeyTimeToLiveAsync(prefix + "1"); + var y1 = conn.KeyTimeToLiveAsync(prefix + "2"); + var y2 = conn.KeyTimeToLiveAsync(prefix + "3"); + + Assert.Null(await y0); + Assert.Null(await y1); + Assert.True(await y2 > TimeSpan.FromMinutes(9), "Over 9"); + Assert.True(await y2 <= TimeSpan.FromMinutes(10), "Under 10"); + } + } + [Fact] public async Task SetAndGet() { @@ -212,6 +253,7 @@ public async Task SetAndGet() conn.KeyDelete(prefix + "7", CommandFlags.FireAndForget); conn.KeyDelete(prefix + "8", CommandFlags.FireAndForget); conn.KeyDelete(prefix + "9", CommandFlags.FireAndForget); + conn.KeyDelete(prefix + "10", CommandFlags.FireAndForget); conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); conn.StringSet(prefix + "2", "abc", flags: CommandFlags.FireAndForget); conn.StringSet(prefix + "4", "abc", flags: CommandFlags.FireAndForget); @@ -219,6 +261,7 @@ public async Task SetAndGet() conn.StringSet(prefix + "7", "abc", flags: CommandFlags.FireAndForget); conn.StringSet(prefix + "8", "abc", flags: CommandFlags.FireAndForget); conn.StringSet(prefix + "9", "abc", flags: CommandFlags.FireAndForget); + conn.StringSet(prefix + "10", "abc", expiry: TimeSpan.FromMinutes(10), flags: CommandFlags.FireAndForget); var x0 = conn.StringSetAndGetAsync(prefix + "1", RedisValue.Null); var x1 = conn.StringSetAndGetAsync(prefix + "2", "def"); @@ -230,6 +273,10 @@ public async Task SetAndGet() var x7 = conn.StringSetAndGetAsync(prefix + "8", "def", expiry: TimeSpan.FromSeconds(4), when: When.Exists); var x8 = conn.StringSetAndGetAsync(prefix + "9", "def", expiry: TimeSpan.FromMilliseconds(4001), when: When.Exists); + var y0 = conn.StringSetAndGetAsync(prefix + "10", "def", keepTtl: true); + var y1 = conn.KeyTimeToLiveAsync(prefix + "10"); + var y2 = conn.StringGetAsync(prefix + "10"); + var s0 = conn.StringGetAsync(prefix + "1"); var s1 = conn.StringGetAsync(prefix + "2"); var s2 = conn.StringGetAsync(prefix + "3"); @@ -246,6 +293,11 @@ public async Task SetAndGet() Assert.Equal("abc", await x7); Assert.Equal("abc", await x8); + Assert.Equal("abc", await y0); + Assert.True(await y1 <= TimeSpan.FromMinutes(10), "Under 10 min"); + Assert.True(await y1 >= TimeSpan.FromMinutes(8), "Over 8 min"); + Assert.Equal("def", await y2); + Assert.Equal(RedisValue.Null, await s0); Assert.Equal("def", await s1); Assert.Equal("def", await s2); diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index ecb386033..c747d403f 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -1094,6 +1094,14 @@ public void StringSetAsync_1() [Fact] public void StringSetAsync_2() + { + TimeSpan? expiry = null; + wrapper.StringSetAsync("key", "value", expiry, true, When.Exists, CommandFlags.None); + mock.Verify(_ => _.StringSetAsync("prefix:key", "value", expiry, true, When.Exists, CommandFlags.None)); + } + + [Fact] + public void StringSetAsync_3() { KeyValuePair[] values = new KeyValuePair[] { new KeyValuePair("a", "x"), new KeyValuePair("b", "y") }; Expression[], bool>> valid = _ => _.Length == 2 && _[0].Key == "prefix:a" && _[0].Value == "x" && _[1].Key == "prefix:b" && _[1].Value == "y"; From f500f90506fe5f5e6b45e83a527117059ec22012 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 18 Mar 2022 09:52:13 -0400 Subject: [PATCH 108/435] Test Suite: Enable Nullable Reference Types (#2040) I want to enable NRTs on the main library, because in testing doing so I found a lot of interesting issues we should have caught especially in result processors and unhandled cases. I figure the best way to do that is to first enable NRTs on our test suite to get the experience similar to what our users would when we shipped a NRT-enabled library version. Either we do or we don't, but having the tests NRT aware is a new win and let's us explore what that diff would look like. --- Directory.Packages.props | 2 +- .../AggresssiveTests.cs | 8 +- tests/StackExchange.Redis.Tests/AsyncTests.cs | 1 + .../AzureMaintenanceEventTests.cs | 2 +- .../StackExchange.Redis.Tests/BacklogTests.cs | 2 +- tests/StackExchange.Redis.Tests/BasicOps.cs | 11 +-- tests/StackExchange.Redis.Tests/BoxUnbox.cs | 6 +- tests/StackExchange.Redis.Tests/Cluster.cs | 2 +- .../ConnectCustomConfig.cs | 4 +- .../ConnectionFailedErrors.cs | 1 + tests/StackExchange.Redis.Tests/Deprecated.cs | 6 +- .../EventArgsTests.cs | 52 ++++++------- .../ExceptionFactoryTests.cs | 6 +- tests/StackExchange.Redis.Tests/Expiry.cs | 14 ++-- tests/StackExchange.Redis.Tests/GeoTests.cs | 18 +++-- .../Helpers/Attributes.cs | 4 +- .../StackExchange.Redis.Tests/Helpers/Skip.cs | 15 +++- .../Helpers/TestConfig.cs | 16 ++-- .../Helpers/TextWriterOutputHelper.cs | 11 ++- .../Helpers/redis-sharp.cs | 1 + .../Issues/Massive Delete.cs | 4 +- .../Issues/SO11766033.cs | 2 +- .../KeysAndValues.cs | 8 +- tests/StackExchange.Redis.Tests/Locking.cs | 20 ++--- .../StackExchange.Redis.Tests/MultiPrimary.cs | 16 ++-- tests/StackExchange.Redis.Tests/Naming.cs | 19 ++--- tests/StackExchange.Redis.Tests/Parse.cs | 8 +- tests/StackExchange.Redis.Tests/Profiling.cs | 4 +- tests/StackExchange.Redis.Tests/PubSub.cs | 4 +- .../PubSubCommand.cs | 4 +- .../PubSubMultiserver.cs | 6 +- .../RedisValueEquivalency.cs | 8 +- tests/StackExchange.Redis.Tests/SSL.cs | 16 ++-- tests/StackExchange.Redis.Tests/Scans.cs | 8 +- tests/StackExchange.Redis.Tests/Scripting.cs | 5 +- tests/StackExchange.Redis.Tests/Sentinel.cs | 12 +-- .../StackExchange.Redis.Tests/SentinelBase.cs | 18 +++-- tests/StackExchange.Redis.Tests/Sets.cs | 2 +- .../SharedConnectionFixture.cs | 18 ++--- .../StackExchange.Redis.Tests.csproj | 1 + tests/StackExchange.Redis.Tests/Streams.cs | 4 +- tests/StackExchange.Redis.Tests/Strings.cs | 8 +- tests/StackExchange.Redis.Tests/TestBase.cs | 76 +++++++++---------- .../StackExchange.Redis.Tests/Transactions.cs | 54 ++++++++----- .../WithKeyPrefixTests.cs | 6 +- 45 files changed, 276 insertions(+), 237 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 67cd6e2df..689a6607e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,7 +23,7 @@ - + \ No newline at end of file diff --git a/tests/StackExchange.Redis.Tests/AggresssiveTests.cs b/tests/StackExchange.Redis.Tests/AggresssiveTests.cs index 82ba6c859..fd2a45cc2 100644 --- a/tests/StackExchange.Redis.Tests/AggresssiveTests.cs +++ b/tests/StackExchange.Redis.Tests/AggresssiveTests.cs @@ -80,11 +80,11 @@ public void RunCompetingBatchesOnSameMuxer() { var db = muxer.GetDatabase(); - Thread x = new Thread(state => BatchRunPings((IDatabase)state)) + Thread x = new Thread(state => BatchRunPings((IDatabase)state!)) { Name = nameof(BatchRunPings) }; - Thread y = new Thread(state => BatchRunIntegers((IDatabase)state)) + Thread y = new Thread(state => BatchRunIntegers((IDatabase)state!)) { Name = nameof(BatchRunIntegers) }; @@ -200,11 +200,11 @@ public void RunCompetingTransactionsOnSameMuxer() { var db = muxer.GetDatabase(); - Thread x = new Thread(state => TranRunPings((IDatabase)state)) + Thread x = new Thread(state => TranRunPings((IDatabase)state!)) { Name = nameof(BatchRunPings) }; - Thread y = new Thread(state => TranRunIntegers((IDatabase)state)) + Thread y = new Thread(state => TranRunIntegers((IDatabase)state!)) { Name = nameof(BatchRunIntegers) }; diff --git a/tests/StackExchange.Redis.Tests/AsyncTests.cs b/tests/StackExchange.Redis.Tests/AsyncTests.cs index 4d49e7d5b..91098ebf1 100644 --- a/tests/StackExchange.Redis.Tests/AsyncTests.cs +++ b/tests/StackExchange.Redis.Tests/AsyncTests.cs @@ -37,6 +37,7 @@ public void AsyncTasksReportFailureIfServerUnavailable() var c = db.SetAddAsync(key, "c"); Assert.True(c.IsFaulted, "faulted"); + Assert.NotNull(c.Exception); var ex = c.Exception.InnerExceptions.Single(); Assert.IsType(ex); Assert.StartsWith("No connection is active/available to service this operation: SADD " + key.ToString(), ex.Message); diff --git a/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs b/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs index 07d33e951..81359be79 100644 --- a/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs +++ b/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs @@ -38,7 +38,7 @@ public void TestAzureMaintenanceEventStrings(string message, AzureNotificationTy { expectedStartTimeUtc = DateTime.SpecifyKind(startTimeUtc, DateTimeKind.Utc); } - _ = IPAddress.TryParse(expectedIP, out IPAddress expectedIPAddress); + _ = IPAddress.TryParse(expectedIP, out IPAddress? expectedIPAddress); var azureMaintenance = new AzureMaintenanceEvent(message); diff --git a/tests/StackExchange.Redis.Tests/BacklogTests.cs b/tests/StackExchange.Redis.Tests/BacklogTests.cs index dc64144e0..6d34b3209 100644 --- a/tests/StackExchange.Redis.Tests/BacklogTests.cs +++ b/tests/StackExchange.Redis.Tests/BacklogTests.cs @@ -326,7 +326,7 @@ public async Task QueuesAndFlushesAfterReconnectingClusterAsync() RedisKey meKey = Me(); var getMsg = Message.Create(0, CommandFlags.None, RedisCommand.GET, meKey); - ServerEndPoint server = null; // Get the server specifically for this message's hash slot + ServerEndPoint? server = null; // Get the server specifically for this message's hash slot await UntilConditionAsync(TimeSpan.FromSeconds(10), () => (server = muxer.SelectServer(getMsg)) != null); Assert.NotNull(server); diff --git a/tests/StackExchange.Redis.Tests/BasicOps.cs b/tests/StackExchange.Redis.Tests/BasicOps.cs index 5ce3a8a29..ed15d1f06 100644 --- a/tests/StackExchange.Redis.Tests/BasicOps.cs +++ b/tests/StackExchange.Redis.Tests/BasicOps.cs @@ -70,7 +70,7 @@ public void GetWithNullKey() using (var muxer = Create()) { var db = muxer.GetDatabase(); - const string key = null; + const string? key = null; var ex = Assert.Throws(() => db.StringGet(key)); Assert.Equal("A null key is not valid in this context", ex.Message); } @@ -82,7 +82,7 @@ public void SetWithNullKey() using (var muxer = Create()) { var db = muxer.GetDatabase(); - const string key = null, value = "abc"; + const string? key = null, value = "abc"; var ex = Assert.Throws(() => db.StringSet(key, value)); Assert.Equal("A null key is not valid in this context", ex.Message); } @@ -94,7 +94,8 @@ public void SetWithNullValue() using (var muxer = Create()) { var db = muxer.GetDatabase(); - string key = Me(), value = null; + string key = Me(); + string? value = null; db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); @@ -226,10 +227,10 @@ public async Task GetWithExpiry(bool exists, bool hasExpiry) { Assert.Equal("val", asyncResult.Value); Assert.Equal(hasExpiry, asyncResult.Expiry.HasValue); - if (hasExpiry) Assert.True(asyncResult.Expiry.Value.TotalMinutes >= 4.9 && asyncResult.Expiry.Value.TotalMinutes <= 5); + if (hasExpiry) Assert.True(asyncResult.Expiry!.Value.TotalMinutes >= 4.9 && asyncResult.Expiry.Value.TotalMinutes <= 5); Assert.Equal("val", syncResult.Value); Assert.Equal(hasExpiry, syncResult.Expiry.HasValue); - if (hasExpiry) Assert.True(syncResult.Expiry.Value.TotalMinutes >= 4.9 && syncResult.Expiry.Value.TotalMinutes <= 5); + if (hasExpiry) Assert.True(syncResult.Expiry!.Value.TotalMinutes >= 4.9 && syncResult.Expiry.Value.TotalMinutes <= 5); } else { diff --git a/tests/StackExchange.Redis.Tests/BoxUnbox.cs b/tests/StackExchange.Redis.Tests/BoxUnbox.cs index 8f0c31064..9ae082d84 100644 --- a/tests/StackExchange.Redis.Tests/BoxUnbox.cs +++ b/tests/StackExchange.Redis.Tests/BoxUnbox.cs @@ -86,17 +86,17 @@ public static IEnumerable RoundTripValues new object[] { (RedisValue)double.NaN }, new object[] { (RedisValue)true }, new object[] { (RedisValue)false }, - new object[] { (RedisValue)(string)null }, + new object[] { (RedisValue)(string?)null }, new object[] { (RedisValue)"abc" }, new object[] { (RedisValue)s_abc }, new object[] { (RedisValue)new Memory(s_abc) }, new object[] { (RedisValue)new ReadOnlyMemory(s_abc) }, }; - public static IEnumerable UnboxValues + public static IEnumerable UnboxValues => new [] { - new object[] { null, RedisValue.Null }, + new object?[] { null, RedisValue.Null }, new object[] { "", RedisValue.EmptyString }, new object[] { 0, (RedisValue)0 }, new object[] { 1, (RedisValue)1 }, diff --git a/tests/StackExchange.Redis.Tests/Cluster.cs b/tests/StackExchange.Redis.Tests/Cluster.cs index e1069d7f8..d76570482 100644 --- a/tests/StackExchange.Redis.Tests/Cluster.cs +++ b/tests/StackExchange.Redis.Tests/Cluster.cs @@ -719,7 +719,7 @@ public void MovedProfiling() { // imprecision of DateTime.UtcNow makes this pretty approximate Assert.True(msg.RetransmissionOf.CommandCreated <= msg.CommandCreated); - Assert.Equal(RetransmissionReasonType.Moved, msg.RetransmissionReason.Value); + Assert.Equal(RetransmissionReasonType.Moved, msg.RetransmissionReason); } else { diff --git a/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs b/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs index bd9a09993..5ba33e872 100644 --- a/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs +++ b/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs @@ -47,7 +47,7 @@ public void DisabledCommandsStillConnectCluster(string disabledCommands) [Fact] public void TieBreakerIntact() { - using var muxer = Create(allowAdmin: true, log: Writer) as ConnectionMultiplexer; + using var muxer = (Create(allowAdmin: true, log: Writer) as ConnectionMultiplexer)!; var tiebreaker = muxer.GetDatabase().StringGet(muxer.RawConfig.TieBreaker); Log($"Tiebreaker: {tiebreaker}"); @@ -62,7 +62,7 @@ public void TieBreakerIntact() [Fact] public void TieBreakerSkips() { - using var muxer = Create(allowAdmin: true, disabledCommands: new[] { "get" }, log: Writer) as ConnectionMultiplexer; + using var muxer = (Create(allowAdmin: true, disabledCommands: new[] { "get" }, log: Writer) as ConnectionMultiplexer)!; Assert.Throws(() => muxer.GetDatabase().StringGet(muxer.RawConfig.TieBreaker)); var snapshot = muxer.GetServerSnapshot(); diff --git a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs b/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs index 9989a4bb7..606099c07 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs @@ -86,6 +86,7 @@ void innerScenario() var rde = Assert.IsType(ex.InnerException); Assert.Equal(CommandStatus.WaitingToBeSent, ex.CommandStatus); Assert.Equal(ConnectionFailureType.AuthenticationFailure, rde.FailureType); + Assert.NotNull(rde.InnerException); Assert.Equal("Error: NOAUTH Authentication required. Verify if the Redis password provided is correct.", rde.InnerException.Message); } diff --git a/tests/StackExchange.Redis.Tests/Deprecated.cs b/tests/StackExchange.Redis.Tests/Deprecated.cs index aa05ab5d0..ba55dc8a3 100644 --- a/tests/StackExchange.Redis.Tests/Deprecated.cs +++ b/tests/StackExchange.Redis.Tests/Deprecated.cs @@ -15,7 +15,7 @@ public Deprecated(ITestOutputHelper output) : base(output) { } [Fact] public void PreserveAsyncOrder() { - Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.PreserveAsyncOrder)), typeof(ObsoleteAttribute))); + Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.PreserveAsyncOrder))!, typeof(ObsoleteAttribute))); var options = ConfigurationOptions.Parse("name=Hello"); Assert.False(options.PreserveAsyncOrder); @@ -32,7 +32,7 @@ public void PreserveAsyncOrder() [Fact] public void WriteBufferParse() { - Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.WriteBuffer)), typeof(ObsoleteAttribute))); + Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.WriteBuffer))!, typeof(ObsoleteAttribute))); var options = ConfigurationOptions.Parse("name=Hello"); Assert.Equal(0, options.WriteBuffer); @@ -44,7 +44,7 @@ public void WriteBufferParse() [Fact] public void ResponseTimeout() { - Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.ResponseTimeout)), typeof(ObsoleteAttribute))); + Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.ResponseTimeout))!, typeof(ObsoleteAttribute))); var options = ConfigurationOptions.Parse("name=Hello"); Assert.Equal(0, options.ResponseTimeout); diff --git a/tests/StackExchange.Redis.Tests/EventArgsTests.cs b/tests/StackExchange.Redis.Tests/EventArgsTests.cs index 2e716db82..bd1041e8e 100644 --- a/tests/StackExchange.Redis.Tests/EventArgsTests.cs +++ b/tests/StackExchange.Redis.Tests/EventArgsTests.cs @@ -27,7 +27,7 @@ HashSlotMovedEventArgs hashSlotMovedArgsMock = Substitute.For( default, default, default, default); - DiagnosticStub stub = DiagnosticStub.Create(); + DiagnosticStub stub = new DiagnosticStub(); stub.ConfigurationChangedBroadcastHandler(default, endpointArgsMock); Assert.Equal(stub.Message,DiagnosticStub.ConfigurationChangedBroadcastHandlerMessage); @@ -74,73 +74,69 @@ public const string ConfigurationChangedHandlerMessage public const string HashSlotMovedHandlerMessage = "HashSlotMovedHandler invoked"; - public static DiagnosticStub Create() + public DiagnosticStub() { - DiagnosticStub stub = new DiagnosticStub(); + ConfigurationChangedBroadcastHandler + = (obj, args) => Message = ConfigurationChangedBroadcastHandlerMessage; - stub.ConfigurationChangedBroadcastHandler - = (obj, args) => stub.Message = ConfigurationChangedBroadcastHandlerMessage; + ErrorMessageHandler + = (obj, args) => Message = ErrorMessageHandlerMessage; - stub.ErrorMessageHandler - = (obj, args) => stub.Message = ErrorMessageHandlerMessage; + ConnectionFailedHandler + = (obj, args) => Message = ConnectionFailedHandlerMessage; - stub.ConnectionFailedHandler - = (obj, args) => stub.Message = ConnectionFailedHandlerMessage; + InternalErrorHandler + = (obj, args) => Message = InternalErrorHandlerMessage; - stub.InternalErrorHandler - = (obj, args) => stub.Message = InternalErrorHandlerMessage; + ConnectionRestoredHandler + = (obj, args) => Message = ConnectionRestoredHandlerMessage; - stub.ConnectionRestoredHandler - = (obj, args) => stub.Message = ConnectionRestoredHandlerMessage; + ConfigurationChangedHandler + = (obj, args) => Message = ConfigurationChangedHandlerMessage; - stub.ConfigurationChangedHandler - = (obj, args) => stub.Message = ConfigurationChangedHandlerMessage; - - stub.HashSlotMovedHandler - = (obj, args) => stub.Message = HashSlotMovedHandlerMessage; - - return stub; + HashSlotMovedHandler + = (obj, args) => Message = HashSlotMovedHandlerMessage; } - public string Message { get; private set; } + public string? Message { get; private set; } - public Action ConfigurationChangedBroadcastHandler + public Action ConfigurationChangedBroadcastHandler { get; private set; } - public Action ErrorMessageHandler + public Action ErrorMessageHandler { get; private set; } - public Action ConnectionFailedHandler + public Action ConnectionFailedHandler { get; private set; } - public Action InternalErrorHandler + public Action InternalErrorHandler { get; private set; } - public Action ConnectionRestoredHandler + public Action ConnectionRestoredHandler { get; private set; } - public Action ConfigurationChangedHandler + public Action ConfigurationChangedHandler { get; private set; } - public Action HashSlotMovedHandler + public Action HashSlotMovedHandler { get; private set; diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index a52c6832d..51c762729 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -107,7 +107,7 @@ public void TimeoutException() { try { - using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false) as ConnectionMultiplexer) + using (var muxer = (Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false) as ConnectionMultiplexer)!) { var server = GetServer(muxer); muxer.AllowConnect = false; @@ -196,13 +196,13 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple { Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); Assert.Contains($"mc: {connCount}/{completeCount}/0", ex.Message); - Assert.Contains("serverEndpoint: " + server.EndPoint.ToString().Replace("Unspecified/", ""), ex.Message); + Assert.Contains("serverEndpoint: " + server.EndPoint.ToString()?.Replace("Unspecified/", ""), ex.Message); } else { Assert.DoesNotContain("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); Assert.DoesNotContain($"mc: {connCount}/{completeCount}/0", ex.Message); - Assert.DoesNotContain("serverEndpoint: " + server.EndPoint.ToString().Replace("Unspecified/", ""), ex.Message); + Assert.DoesNotContain("serverEndpoint: " + server.EndPoint.ToString()?.Replace("Unspecified/", ""), ex.Message); } Assert.DoesNotContain("Unspecified/", ex.Message); } diff --git a/tests/StackExchange.Redis.Tests/Expiry.cs b/tests/StackExchange.Redis.Tests/Expiry.cs index afa21d917..7902c7a60 100644 --- a/tests/StackExchange.Redis.Tests/Expiry.cs +++ b/tests/StackExchange.Redis.Tests/Expiry.cs @@ -10,7 +10,7 @@ public class Expiry : TestBase { public Expiry(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } - private static string[] GetMap(bool disablePTimes) => disablePTimes ? (new[] { "pexpire", "pexpireat", "pttl" }) : null; + private static string[]? GetMap(bool disablePTimes) => disablePTimes ? (new[] { "pexpire", "pexpireat", "pttl" }) : null; [Theory] [InlineData(true)] @@ -82,18 +82,22 @@ public async Task TestBasicExpiryDateTime(bool disablePTimes, bool utc) var f = conn.KeyTimeToLiveAsync(key); Assert.Null(await a); - var time = await b; + var timeResult = await b; + Assert.NotNull(timeResult); + TimeSpan time = timeResult.Value; // Adjust for server time offset, if any when checking expectations time -= offset; - Assert.NotNull(time); Log("Time: {0}, Expected: {1}-{2}", time, TimeSpan.FromMinutes(59), TimeSpan.FromMinutes(60)); Assert.True(time >= TimeSpan.FromMinutes(59)); Assert.True(time <= TimeSpan.FromMinutes(60.1)); Assert.Null(await c); - time = await d; - Assert.NotNull(time); + + timeResult = await d; + Assert.NotNull(timeResult); + time = timeResult.Value; + Assert.True(time >= TimeSpan.FromMinutes(89)); Assert.True(time <= TimeSpan.FromMinutes(90.1)); Assert.Null(await e); diff --git a/tests/StackExchange.Redis.Tests/GeoTests.cs b/tests/StackExchange.Redis.Tests/GeoTests.cs index 92a90f926..3a9008c07 100644 --- a/tests/StackExchange.Redis.Tests/GeoTests.cs +++ b/tests/StackExchange.Redis.Tests/GeoTests.cs @@ -148,15 +148,21 @@ public void GeoRadius() Assert.Equal(2, results.Length); Assert.Equal(results[0].Member, cefalù.Member); - Assert.Equal(0, results[0].Distance.Value); - Assert.Equal(Math.Round(results[0].Position.Value.Longitude, 5), Math.Round(cefalù.Position.Longitude, 5)); - Assert.Equal(Math.Round(results[0].Position.Value.Latitude, 5), Math.Round(cefalù.Position.Latitude, 5)); + Assert.Equal(0, results[0].Distance); + var position0 = results[0].Position; + Assert.NotNull(position0); + Assert.Equal(Math.Round(position0.Value.Longitude, 5), Math.Round(cefalù.Position.Longitude, 5)); + Assert.Equal(Math.Round(position0.Value.Latitude, 5), Math.Round(cefalù.Position.Latitude, 5)); Assert.False(results[0].Hash.HasValue); Assert.Equal(results[1].Member, palermo.Member); - Assert.Equal(Math.Round(36.5319, 6), Math.Round(results[1].Distance.Value, 6)); - Assert.Equal(Math.Round(results[1].Position.Value.Longitude, 5), Math.Round(palermo.Position.Longitude, 5)); - Assert.Equal(Math.Round(results[1].Position.Value.Latitude, 5), Math.Round(palermo.Position.Latitude, 5)); + var distance1 = results[1].Distance; + Assert.NotNull(distance1); + Assert.Equal(Math.Round(36.5319, 6), Math.Round(distance1.Value, 6)); + var position1 = results[1].Position; + Assert.NotNull(position1); + Assert.Equal(Math.Round(position1.Value.Longitude, 5), Math.Round(palermo.Position.Longitude, 5)); + Assert.Equal(Math.Round(position1.Value.Latitude, 5), Math.Round(palermo.Position.Latitude, 5)); Assert.False(results[1].Hash.HasValue); results = db.GeoRadius(key, cefalù.Member, 60, GeoUnit.Miles, 2, Order.Ascending, GeoRadiusOptions.None); diff --git a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs index 8d9c09796..a2081644c 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs @@ -89,7 +89,7 @@ protected override string GetDisplayName(IAttributeInfo factAttribute, string di [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] public SkippableTestCase() { } - public SkippableTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[] testMethodArguments = null) + public SkippableTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[]? testMethodArguments = null) : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) { } @@ -139,7 +139,7 @@ protected override string GetDisplayName(IAttributeInfo factAttribute, string di [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] public NamedSkippedDataRowTestCase() { } - public NamedSkippedDataRowTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, string skipReason, object[] testMethodArguments = null) + public NamedSkippedDataRowTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, string skipReason, object[]? testMethodArguments = null) : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, skipReason, testMethodArguments) { } } diff --git a/tests/StackExchange.Redis.Tests/Helpers/Skip.cs b/tests/StackExchange.Redis.Tests/Helpers/Skip.cs index 6fa8a6911..bf51afe3a 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Skip.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Skip.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace StackExchange.Redis.Tests { @@ -7,7 +8,11 @@ public static class Skip { public static void Inconclusive(string message) => throw new SkipTestException(message); - public static void IfNoConfig(string prop, string value) + public static void IfNoConfig(string prop, +#if NETCOREAPP + [NotNull] +#endif + string? value) { if (string.IsNullOrEmpty(value)) { @@ -15,7 +20,11 @@ public static void IfNoConfig(string prop, string value) } } - public static void IfNoConfig(string prop, List values) + public static void IfNoConfig(string prop, +#if NETCOREAPP + [NotNull] +#endif + List? values) { if (values == null || values.Count == 0) { @@ -44,7 +53,7 @@ internal static void IfMissingDatabase(IConnectionMultiplexer conn, int dbId) public class SkipTestException : Exception { - public string MissingFeatures { get; set; } + public string? MissingFeatures { get; set; } public SkipTestException(string reason) : base(reason) { } } diff --git a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs index e11855689..a6c94611f 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs @@ -13,7 +13,7 @@ public static class TestConfig public static Config Current { get; } private static int _db = 17; - public static int GetDedicatedDB(IConnectionMultiplexer conn = null) + public static int GetDedicatedDB(IConnectionMultiplexer? conn = null) { int db = Interlocked.Increment(ref _db); if (conn != null) Skip.IfMissingDatabase(conn, db); @@ -31,7 +31,7 @@ static TestConfig() { using (var reader = new StreamReader(stream)) { - Current = JsonConvert.DeserializeObject(reader.ReadToEnd()); + Current = JsonConvert.DeserializeObject(reader.ReadToEnd()) ?? new Config(); } } } @@ -90,17 +90,17 @@ public class Config public int ClusterServerCount { get; set; } = 6; public string ClusterServersAndPorts => string.Join(",", Enumerable.Range(ClusterStartPort, ClusterServerCount).Select(port => ClusterServer + ":" + port)); - public string SslServer { get; set; } + public string? SslServer { get; set; } public int SslPort { get; set; } - public string RedisLabsSslServer { get; set; } + public string? RedisLabsSslServer { get; set; } public int RedisLabsSslPort { get; set; } = 6379; - public string RedisLabsPfxPath { get; set; } + public string? RedisLabsPfxPath { get; set; } - public string AzureCacheServer { get; set; } - public string AzureCachePassword { get; set; } + public string? AzureCacheServer { get; set; } + public string? AzureCachePassword { get; set; } - public string SSDBServer { get; set; } + public string? SSDBServer { get; set; } public int SSDBPort { get; set; } = 8888; public string ProxyServer { get; set; } = "127.0.0.1"; diff --git a/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs b/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs index bdcb7b55f..6ccbb478b 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis.Tests.Helpers public class TextWriterOutputHelper : TextWriter { private StringBuilder Buffer { get; } = new StringBuilder(2048); - private StringBuilder Echo { get; set; } + private StringBuilder? Echo { get; set; } public override Encoding Encoding => Encoding.UTF8; private readonly ITestOutputHelper Output; private readonly bool ToConsole; @@ -20,7 +20,7 @@ public TextWriterOutputHelper(ITestOutputHelper outputHelper, bool echoToConsole public void EchoTo(StringBuilder sb) => Echo = sb; - public void WriteLineNoTime(string value) + public void WriteLineNoTime(string? value) { try { @@ -34,8 +34,13 @@ public void WriteLineNoTime(string value) } } - public override void WriteLine(string value) + public override void WriteLine(string? value) { + if (value is null) + { + return; + } + try { // Prevent double timestamps diff --git a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs index 8d6c97b79..02b6b6f01 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs @@ -11,6 +11,7 @@ // // Licensed under the same terms of reddis: new BSD license. // +#nullable disable using System; using System.Collections.Generic; diff --git a/tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs b/tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs index 6194e99dd..0067c1522 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs @@ -17,7 +17,7 @@ private void Prep(int db, string key) { Skip.IfMissingDatabase(muxer, db); GetServer(muxer).FlushDatabase(db); - Task last = null; + Task? last = null; var conn = muxer.GetDatabase(db); for (int i = 0; i < 10000; i++) { @@ -42,7 +42,7 @@ public async Task ExecuteMassiveDelete() var conn = muxer.GetDatabase(dbId); var originally = await conn.SetLengthAsync(key).ForAwait(); int keepChecking = 1; - Task last = null; + Task? last = null; while (Volatile.Read(ref keepChecking) == 1) { throttle.Wait(); // acquire diff --git a/tests/StackExchange.Redis.Tests/Issues/SO11766033.cs b/tests/StackExchange.Redis.Tests/Issues/SO11766033.cs index 7ea6154ff..de3a9c3e9 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO11766033.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO11766033.cs @@ -13,7 +13,7 @@ public void TestNullString() using (var muxer = Create()) { var redis = muxer.GetDatabase(); - const string expectedTestValue = null; + const string? expectedTestValue = null; var uid = Me(); redis.StringSetAsync(uid, "abc"); redis.StringSetAsync(uid, expectedTestValue); diff --git a/tests/StackExchange.Redis.Tests/KeysAndValues.cs b/tests/StackExchange.Redis.Tests/KeysAndValues.cs index 93f4b99b8..b2c8011dc 100644 --- a/tests/StackExchange.Redis.Tests/KeysAndValues.cs +++ b/tests/StackExchange.Redis.Tests/KeysAndValues.cs @@ -13,10 +13,10 @@ public void TestValues() RedisValue @default = default(RedisValue); CheckNull(@default); - RedisValue nullString = (string)null; + RedisValue nullString = (string?)null; CheckNull(nullString); - RedisValue nullBlob = (byte[])null; + RedisValue nullBlob = (byte[]?)null; CheckNull(nullBlob); RedisValue emptyString = ""; @@ -104,8 +104,8 @@ private static void CheckNotNull(RedisValue value) CheckSame(value, value); CheckNotSame(value, default(RedisValue)); - CheckNotSame(value, (string)null); - CheckNotSame(value, (byte[])null); + CheckNotSame(value, (string?)null); + CheckNotSame(value, (byte[]?)null); } internal static void CheckNull(RedisValue value) diff --git a/tests/StackExchange.Redis.Tests/Locking.cs b/tests/StackExchange.Redis.Tests/Locking.cs index dfe4ab095..2afcd0461 100644 --- a/tests/StackExchange.Redis.Tests/Locking.cs +++ b/tests/StackExchange.Redis.Tests/Locking.cs @@ -38,11 +38,11 @@ public void AggressiveParallel(TestMode testMode) using (var c1 = Create(testMode)) using (var c2 = Create(testMode)) { - void cb(object obj) + void cb(object? obj) { try { - var conn = (IDatabase)obj; + var conn = (IDatabase?)obj!; conn.Multiplexer.ErrorMessage += delegate { Interlocked.Increment(ref errorCount); }; for (int i = 0; i < 1000; i++) @@ -148,9 +148,9 @@ public async Task TakeLockAndExtend(TestMode mode) Assert.True(await t1, "1"); Assert.False(await t1b, "1b"); Assert.Equal(right, await t2); - if (withTran) Assert.False(await t3, "3"); + if (withTran) Assert.False(await t3!, "3"); Assert.Equal(right, await t4); - if (withTran) Assert.False(await t5, "5"); + if (withTran) Assert.False(await t5!, "5"); Assert.Equal(right, await t6); var ttl = (await t7).Value.TotalSeconds; Assert.True(ttl > 0 && ttl <= 20, "7"); @@ -199,9 +199,9 @@ public async Task TestBasicLockNotTaken(TestMode testMode) { int errorCount = 0; conn.ErrorMessage += delegate { Interlocked.Increment(ref errorCount); }; - Task taken = null; - Task newValue = null; - Task ttl = null; + Task? taken = null; + Task? newValue = null; + Task? ttl = null; const int LOOP = 50; var db = conn.GetDatabase(); @@ -213,9 +213,9 @@ public async Task TestBasicLockNotTaken(TestMode testMode) newValue = db.StringGetAsync(key); ttl = db.KeyTimeToLiveAsync(key); } - Assert.True(await taken, "taken"); - Assert.Equal("new-value", await newValue); - var ttlValue = (await ttl).Value.TotalSeconds; + Assert.True(await taken!, "taken"); + Assert.Equal("new-value", await newValue!); + var ttlValue = (await ttl!).Value.TotalSeconds; Assert.True(ttlValue >= 8 && ttlValue <= 10, "ttl"); Assert.Equal(0, errorCount); diff --git a/tests/StackExchange.Redis.Tests/MultiPrimary.cs b/tests/StackExchange.Redis.Tests/MultiPrimary.cs index 448336cb8..022fd3d5f 100644 --- a/tests/StackExchange.Redis.Tests/MultiPrimary.cs +++ b/tests/StackExchange.Redis.Tests/MultiPrimary.cs @@ -39,18 +39,18 @@ public void TestMultiNoTieBreak() } } - public static IEnumerable GetConnections() + public static IEnumerable GetConnections() { yield return new object[] { TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.PrimaryServerAndPort }; yield return new object[] { TestConfig.Current.SecureServerAndPort, TestConfig.Current.SecureServerAndPort, TestConfig.Current.SecureServerAndPort }; - yield return new object[] { TestConfig.Current.SecureServerAndPort, TestConfig.Current.PrimaryServerAndPort, null }; - yield return new object[] { TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.SecureServerAndPort, null }; + yield return new object?[] { TestConfig.Current.SecureServerAndPort, TestConfig.Current.PrimaryServerAndPort, null }; + yield return new object?[] { TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.SecureServerAndPort, null }; - yield return new object[] { null, TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.PrimaryServerAndPort }; - yield return new object[] { TestConfig.Current.PrimaryServerAndPort, null, TestConfig.Current.PrimaryServerAndPort }; - yield return new object[] { null, TestConfig.Current.SecureServerAndPort, TestConfig.Current.SecureServerAndPort }; - yield return new object[] { TestConfig.Current.SecureServerAndPort, null, TestConfig.Current.SecureServerAndPort }; - yield return new object[] { null, null, null }; + yield return new object?[] { null, TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.PrimaryServerAndPort }; + yield return new object?[] { TestConfig.Current.PrimaryServerAndPort, null, TestConfig.Current.PrimaryServerAndPort }; + yield return new object?[] { null, TestConfig.Current.SecureServerAndPort, TestConfig.Current.SecureServerAndPort }; + yield return new object?[] { TestConfig.Current.SecureServerAndPort, null, TestConfig.Current.SecureServerAndPort }; + yield return new object?[] { null, null, null }; } [Theory, MemberData(nameof(GetConnections))] diff --git a/tests/StackExchange.Redis.Tests/Naming.cs b/tests/StackExchange.Redis.Tests/Naming.cs index 809be4574..6305bc6b9 100644 --- a/tests/StackExchange.Redis.Tests/Naming.cs +++ b/tests/StackExchange.Redis.Tests/Naming.cs @@ -31,25 +31,16 @@ public void CheckSignatures(Type type, bool isAsync) [Fact] public void ShowReadOnlyOperations() { - var msg = typeof(ConnectionMultiplexer).Assembly.GetType("StackExchange.Redis.Message"); - Assert.NotNull(msg); - var cmd = typeof(ConnectionMultiplexer).Assembly.GetType("StackExchange.Redis.RedisCommand"); - Assert.NotNull(cmd); - var primaryOnlyMethod = msg.GetMethod(nameof(Message.IsPrimaryOnly), BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); - Assert.NotNull(primaryOnlyMethod); - object[] args = new object[1]; - List primaryReplica = new List(); List primaryOnly = new List(); - foreach (var val in Enum.GetValues(cmd)) + foreach (var val in (RedisCommand[])Enum.GetValues(typeof(RedisCommand))) { - args[0] = val; - bool isPrimaryOnly = (bool)primaryOnlyMethod.Invoke(null, args); + bool isPrimaryOnly = Message.IsPrimaryOnly(val); (isPrimaryOnly ? primaryOnly : primaryReplica).Add(val); if (!isPrimaryOnly) { - Log(val?.ToString()); + Log(val.ToString()); } } Log("primary-only: {0}, vs primary/replica: {1}", primaryOnly.Count, primaryReplica.Count); @@ -104,7 +95,7 @@ private static bool UsesKey(Type type) if (type.IsArray) { - if (UsesKey(type.GetElementType())) return true; + if (UsesKey(type.GetElementType()!)) return true; } if (type.IsGenericType) // KVP, etc { @@ -170,7 +161,7 @@ public void CheckSyncAsyncMethodsMatch(Type from, Type to) private void CheckMethod(MethodInfo method, bool isAsync) { - string shortName = method.Name, fullName = method.DeclaringType.Name + "." + shortName; + string shortName = method.Name, fullName = method.DeclaringType?.Name + "." + shortName; switch (shortName) { diff --git a/tests/StackExchange.Redis.Tests/Parse.cs b/tests/StackExchange.Redis.Tests/Parse.cs index 354262646..27bdea01a 100644 --- a/tests/StackExchange.Redis.Tests/Parse.cs +++ b/tests/StackExchange.Redis.Tests/Parse.cs @@ -46,7 +46,7 @@ public void ParseAsSingleChunk(string ascii, int expected) public void ParseAsLotsOfChunks(string ascii, int expected) { var bytes = Encoding.ASCII.GetBytes(ascii); - FragmentedSegment chain = null, tail = null; + FragmentedSegment? chain = null, tail = null; for (int i = 0; i < bytes.Length; i++) { var next = new FragmentedSegment(i, new ReadOnlyMemory(bytes, i, 1)); @@ -60,7 +60,7 @@ public void ParseAsLotsOfChunks(string ascii, int expected) } tail = next; } - var buffer = new ReadOnlySequence(chain, 0, tail, 1); + var buffer = new ReadOnlySequence(chain!, 0, tail!, 1); Assert.Equal(bytes.Length, buffer.Length); using (var arena = new Arena()) { @@ -90,9 +90,9 @@ public FragmentedSegment(long runningIndex, ReadOnlyMemory memory) Memory = memory; } - public new FragmentedSegment Next + public new FragmentedSegment? Next { - get => (FragmentedSegment)base.Next; + get => (FragmentedSegment?)base.Next; set => base.Next = value; } } diff --git a/tests/StackExchange.Redis.Tests/Profiling.cs b/tests/StackExchange.Redis.Tests/Profiling.cs index 9c43f9bca..613972187 100644 --- a/tests/StackExchange.Redis.Tests/Profiling.cs +++ b/tests/StackExchange.Redis.Tests/Profiling.cs @@ -45,7 +45,7 @@ public void Simple() var i = 0; foreach (var cmd in cmds) { - Log("Command {0} (DB: {1}): {2}", i++, cmd.Db, cmd.ToString().Replace("\n", ", ")); + Log("Command {0} (DB: {1}): {2}", i++, cmd.Db, cmd?.ToString()?.Replace("\n", ", ")); } var all = string.Join(",", cmds.Select(x => x.Command)); @@ -211,7 +211,7 @@ internal class PerThreadProfiler { private readonly ThreadLocal perThreadSession = new ThreadLocal(() => new ProfilingSession()); - public ProfilingSession GetSession() => perThreadSession.Value; + public ProfilingSession GetSession() => perThreadSession.Value!; } internal class AsyncLocalProfiler diff --git a/tests/StackExchange.Redis.Tests/PubSub.cs b/tests/StackExchange.Redis.Tests/PubSub.cs index 02068aa43..502e3ffe4 100644 --- a/tests/StackExchange.Redis.Tests/PubSub.cs +++ b/tests/StackExchange.Redis.Tests/PubSub.cs @@ -746,7 +746,7 @@ public async Task AzureRedisEventsAutomaticSubscribe() using (var connection = await ConnectionMultiplexer.ConnectAsync(options)) { - connection.ServerMaintenanceEvent += (object _, ServerMaintenanceEvent e) => + connection.ServerMaintenanceEvent += (object? _, ServerMaintenanceEvent e) => { if (e is AzureMaintenanceEvent) { @@ -765,7 +765,7 @@ public async Task AzureRedisEventsAutomaticSubscribe() [Fact] public async Task SubscriptionsSurviveConnectionFailureAsync() { - using (var muxer = Create(allowAdmin: true, shared: false, log: Writer, syncTimeout: 1000) as ConnectionMultiplexer) + using (var muxer = (Create(allowAdmin: true, shared: false, log: Writer, syncTimeout: 1000) as ConnectionMultiplexer)!) { var profiler = muxer.AddProfiler(); RedisChannel channel = Me(); diff --git a/tests/StackExchange.Redis.Tests/PubSubCommand.cs b/tests/StackExchange.Redis.Tests/PubSubCommand.cs index 45d17a921..1a3f775f2 100644 --- a/tests/StackExchange.Redis.Tests/PubSubCommand.cs +++ b/tests/StackExchange.Redis.Tests/PubSubCommand.cs @@ -61,7 +61,7 @@ public async Task SubscriberCountAsync() internal static class Util { public static async Task WithTimeout(this Task task, int timeoutMs, - [CallerMemberName] string caller = null, [CallerLineNumber] int line = 0) + [CallerMemberName] string? caller = null, [CallerLineNumber] int line = 0) { var cts = new CancellationTokenSource(); if (task == await Task.WhenAny(task, Task.Delay(timeoutMs, cts.Token)).ForAwait()) @@ -75,7 +75,7 @@ public static async Task WithTimeout(this Task task, int timeoutMs, } } public static async Task WithTimeout(this Task task, int timeoutMs, - [CallerMemberName] string caller = null, [CallerLineNumber] int line = 0) + [CallerMemberName] string? caller = null, [CallerLineNumber] int line = 0) { var cts = new CancellationTokenSource(); if (task == await Task.WhenAny(task, Task.Delay(timeoutMs, cts.Token)).ForAwait()) diff --git a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs index f557f5549..4e55b3dbd 100644 --- a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs +++ b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs @@ -15,7 +15,7 @@ public PubSubMultiserver(ITestOutputHelper output, SharedConnectionFixture fixtu [Fact] public void ChannelSharding() { - using var muxer = Create(channelPrefix: Me()) as ConnectionMultiplexer; + using var muxer = (Create(channelPrefix: Me()) as ConnectionMultiplexer)!; var defaultSlot = muxer.ServerSelectionStrategy.HashSlot(default(RedisChannel)); var slot1 = muxer.ServerSelectionStrategy.HashSlot((RedisChannel)"hey"); @@ -30,7 +30,7 @@ public void ChannelSharding() public async Task ClusterNodeSubscriptionFailover() { Log("Connecting..."); - using var muxer = Create(allowAdmin: true) as ConnectionMultiplexer; + using var muxer = (Create(allowAdmin: true) as ConnectionMultiplexer)!; var sub = muxer.GetSubscriber(); var channel = (RedisChannel)Me(); @@ -101,7 +101,7 @@ public async Task PrimaryReplicaSubscriptionFailover(CommandFlags flags, bool ex { var config = TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; Log("Connecting..."); - using var muxer = Create(configuration: config, shared: false, allowAdmin: true) as ConnectionMultiplexer; + using var muxer = (Create(configuration: config, shared: false, allowAdmin: true) as ConnectionMultiplexer)!; var sub = muxer.GetSubscriber(); var channel = (RedisChannel)(Me() + flags.ToString()); // Individual channel per case to not overlap publishers diff --git a/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs b/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs index 813f80c35..87e503883 100644 --- a/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs +++ b/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs @@ -23,7 +23,7 @@ static void Check(RedisValue known, RedisValue test) else { Assert.False(test.IsNull); - Assert.Equal((int)known, ((int?)test).Value); + Assert.Equal((int)known, ((int?)test)!.Value); Assert.Equal((int)known, (int)test); } Assert.Equal((int)known, (int)test); @@ -62,7 +62,7 @@ static void Check(RedisValue known, RedisValue test) else { Assert.False(test.IsNull); - Assert.Equal((long)known, ((long?)test).Value); + Assert.Equal((long)known, ((long?)test!).Value); Assert.Equal((long)known, (long)test); } Assert.Equal((long)known, (long)test); @@ -101,7 +101,7 @@ static void Check(RedisValue known, RedisValue test) else { Assert.False(test.IsNull); - Assert.Equal((double)known, ((double?)test).Value); + Assert.Equal((double)known, ((double?)test)!.Value); Assert.Equal((double)known, (double)test); } Assert.Equal((double)known, (double)test); @@ -150,7 +150,7 @@ private static void CheckString(RedisValue value, string expected) Assert.True(s == expected, $"'{s}' vs '{expected}'"); } - private static byte[] Bytes(string s) => s == null ? null : Encoding.UTF8.GetBytes(s); + private static byte[]? Bytes(string? s) => s == null ? null : Encoding.UTF8.GetBytes(s); private static string LineNumber([CallerLineNumber] int lineNumber = 0) => lineNumber.ToString(); diff --git a/tests/StackExchange.Redis.Tests/SSL.cs b/tests/StackExchange.Redis.Tests/SSL.cs index d68fef7f8..dc6c6dfb0 100644 --- a/tests/StackExchange.Redis.Tests/SSL.cs +++ b/tests/StackExchange.Redis.Tests/SSL.cs @@ -57,7 +57,7 @@ public async Task ConnectToSSLServer(bool useSsl, bool specifyHost) { var server = TestConfig.Current.SslServer; int? port = TestConfig.Current.SslPort; - string password = ""; + string? password = ""; bool isAzure = false; if (string.IsNullOrWhiteSpace(server) && useSsl) { @@ -75,7 +75,7 @@ public async Task ConnectToSSLServer(bool useSsl, bool specifyHost) SyncTimeout = Debugger.IsAttached ? int.MaxValue : 5000, Password = password, }; - var map = new Dictionary + var map = new Dictionary { ["config"] = null // don't rely on config working }; @@ -94,7 +94,7 @@ public async Task ConnectToSSLServer(bool useSsl, bool specifyHost) config.CertificateValidation += (sender, cert, chain, errors) => { Log("errors: " + errors); - Log("cert issued to: " + cert.Subject); + Log("cert issued to: " + cert?.Subject); return true; // fingers in ears, pretend we don't know this is wrong }; } @@ -320,7 +320,7 @@ public void SSLHostInferredFromEndpoints() Assert.True(options.SslHost == null); } - private void Check(string name, object x, object y) + private void Check(string name, object? x, object? y) { Writer.WriteLine($"{name}: {(x == null ? "(null)" : x.ToString())} vs {(y == null ? "(null)" : y.ToString())}"); Assert.Equal(x, y); @@ -399,8 +399,12 @@ public void SSLParseViaConfig_Issue883_ConfigObject() } } - public static RemoteCertificateValidationCallback ShowCertFailures(TextWriterOutputHelper output) { - if (output == null) return null; + public static RemoteCertificateValidationCallback? ShowCertFailures(TextWriterOutputHelper output) + { + if (output == null) + { + return null; + } return (sender, certificate, chain, sslPolicyErrors) => { diff --git a/tests/StackExchange.Redis.Tests/Scans.cs b/tests/StackExchange.Redis.Tests/Scans.cs index a233722cf..18a1f1308 100644 --- a/tests/StackExchange.Redis.Tests/Scans.cs +++ b/tests/StackExchange.Redis.Tests/Scans.cs @@ -17,7 +17,7 @@ public Scans(ITestOutputHelper output, SharedConnectionFixture fixture) : base ( [InlineData(false)] public void KeysScan(bool supported) { - string[] disabledCommands = supported ? null : new[] { "scan" }; + string[]? disabledCommands = supported ? null : new[] { "scan" }; using (var conn = Create(disabledCommands: disabledCommands, allowAdmin: true)) { var dbId = TestConfig.GetDedicatedDB(conn); @@ -191,7 +191,7 @@ public void ScanResume() [InlineData(false)] public void SetScan(bool supported) { - string[] disabledCommands = supported ? null : new[] { "sscan" }; + string[]? disabledCommands = supported ? null : new[] { "sscan" }; using (var conn = Create(disabledCommands: disabledCommands)) { RedisKey key = Me(); @@ -214,7 +214,7 @@ public void SetScan(bool supported) [InlineData(false)] public void SortedSetScan(bool supported) { - string[] disabledCommands = supported ? null : new[] { "zscan" }; + string[]? disabledCommands = supported ? null : new[] { "zscan" }; using (var conn = Create(disabledCommands: disabledCommands)) { RedisKey key = Me() + supported; @@ -282,7 +282,7 @@ public void SortedSetScan(bool supported) [InlineData(false)] public void HashScan(bool supported) { - string[] disabledCommands = supported ? null : new[] { "hscan" }; + string[]? disabledCommands = supported ? null : new[] { "hscan" }; using (var conn = Create(disabledCommands: disabledCommands)) { RedisKey key = Me(); diff --git a/tests/StackExchange.Redis.Tests/Scripting.cs b/tests/StackExchange.Redis.Tests/Scripting.cs index 6c3236e8a..56caaef6f 100644 --- a/tests/StackExchange.Redis.Tests/Scripting.cs +++ b/tests/StackExchange.Redis.Tests/Scripting.cs @@ -316,6 +316,7 @@ public void ScriptThrowsErrorInsideTransaction() Assert.Equal(2L, c.Result); Assert.True(QuickWait(b).IsFaulted, "should be faulted"); + Assert.NotNull(b.Exception); Assert.Single(b.Exception.InnerExceptions); var ex = b.Exception.InnerExceptions.Single(); Assert.IsType(ex); @@ -1059,7 +1060,7 @@ public void ScriptWithKeyPrefixCompare() [Fact] public void RedisResultUnderstandsNullArrayNull() => TestNullArray(null); - private static void TestNullArray(RedisResult value) + private static void TestNullArray(RedisResult? value) { Assert.True(value == null || value.IsNull); @@ -1080,7 +1081,7 @@ private static void TestNullArray(RedisResult value) [Fact] public void RedisResultUnderstandsNullValue() => TestNullValue(RedisResult.Create(RedisValue.Null, ResultType.None)); - private static void TestNullValue(RedisResult value) + private static void TestNullValue(RedisResult? value) { Assert.True(value == null || value.IsNull); diff --git a/tests/StackExchange.Redis.Tests/Sentinel.cs b/tests/StackExchange.Redis.Tests/Sentinel.cs index 5970f6852..821be3258 100644 --- a/tests/StackExchange.Redis.Tests/Sentinel.cs +++ b/tests/StackExchange.Redis.Tests/Sentinel.cs @@ -225,7 +225,7 @@ public void SentinelSentinelsTest() { var sentinels = SentinelServerA.SentinelSentinels(ServiceName); - var expected = new List { + var expected = new List { SentinelServerB.EndPoint.ToString(), SentinelServerC.EndPoint.ToString() }; @@ -247,7 +247,7 @@ public void SentinelSentinelsTest() var data = kv.ToDictionary(); actual.Add(data["ip"] + ":" + data["port"]); } - expected = new List { + expected = new List { SentinelServerA.EndPoint.ToString(), SentinelServerC.EndPoint.ToString() }; @@ -262,7 +262,7 @@ public void SentinelSentinelsTest() var data = kv.ToDictionary(); actual.Add(data["ip"] + ":" + data["port"]); } - expected = new List { + expected = new List { SentinelServerA.EndPoint.ToString(), SentinelServerB.EndPoint.ToString() }; @@ -276,7 +276,7 @@ public void SentinelSentinelsTest() public async Task SentinelSentinelsAsyncTest() { var sentinels = await SentinelServerA.SentinelSentinelsAsync(ServiceName).ForAwait(); - var expected = new List { + var expected = new List { SentinelServerB.EndPoint.ToString(), SentinelServerC.EndPoint.ToString() }; @@ -293,7 +293,7 @@ public async Task SentinelSentinelsAsyncTest() sentinels = await SentinelServerB.SentinelSentinelsAsync(ServiceName).ForAwait(); - expected = new List { + expected = new List { SentinelServerA.EndPoint.ToString(), SentinelServerC.EndPoint.ToString() }; @@ -309,7 +309,7 @@ public async Task SentinelSentinelsAsyncTest() Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); sentinels = await SentinelServerC.SentinelSentinelsAsync(ServiceName).ForAwait(); - expected = new List { + expected = new List { SentinelServerA.EndPoint.ToString(), SentinelServerB.EndPoint.ToString() }; diff --git a/tests/StackExchange.Redis.Tests/SentinelBase.cs b/tests/StackExchange.Redis.Tests/SentinelBase.cs index 8622b4551..2b4528bf4 100644 --- a/tests/StackExchange.Redis.Tests/SentinelBase.cs +++ b/tests/StackExchange.Redis.Tests/SentinelBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Threading.Tasks; @@ -20,11 +21,13 @@ public class SentinelBase : TestBase, IAsyncLifetime protected IServer SentinelServerC { get; set; } public IServer[] SentinelsServers { get; set; } +#nullable disable public SentinelBase(ITestOutputHelper output) : base(output) { Skip.IfNoConfig(nameof(TestConfig.Config.SentinelServer), TestConfig.Current.SentinelServer); Skip.IfNoConfig(nameof(TestConfig.Config.SentinelSeviceName), TestConfig.Current.SentinelSeviceName); } +#nullable enable public Task DisposeAsync() => Task.CompletedTask; @@ -49,11 +52,12 @@ public async Task InitializeAsync() } } Assert.True(Conn.IsConnected); - SentinelServerA = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA); - SentinelServerB = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortB); - SentinelServerC = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortC); + SentinelServerA = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA)!; + SentinelServerB = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortB)!; + SentinelServerC = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortC)!; SentinelsServers = new[] { SentinelServerA, SentinelServerB, SentinelServerC }; + SentinelServerA.AllowReplicaWrites = true; // Wait until we are in a state of a single primary and replica await WaitForReadyAsync(); } @@ -61,13 +65,13 @@ public async Task InitializeAsync() // Sometimes it's global, sometimes it's local // Depends what mood Redis is in but they're equal and not the point of our tests protected static readonly IpComparer _ipComparer = new IpComparer(); - protected class IpComparer : IEqualityComparer + protected class IpComparer : IEqualityComparer { - public bool Equals(string x, string y) => x == y || x?.Replace("0.0.0.0", "127.0.0.1") == y?.Replace("0.0.0.0", "127.0.0.1"); - public int GetHashCode(string obj) => obj.GetHashCode(); + public bool Equals(string? x, string? y) => x == y || x?.Replace("0.0.0.0", "127.0.0.1") == y?.Replace("0.0.0.0", "127.0.0.1"); + public int GetHashCode(string? obj) => obj?.GetHashCode() ?? 0; } - protected async Task WaitForReadyAsync(EndPoint expectedPrimary = null, bool waitForReplication = false, TimeSpan? duration = null) + protected async Task WaitForReadyAsync(EndPoint? expectedPrimary = null, bool waitForReplication = false, TimeSpan? duration = null) { duration ??= TimeSpan.FromSeconds(30); diff --git a/tests/StackExchange.Redis.Tests/Sets.cs b/tests/StackExchange.Redis.Tests/Sets.cs index 815d8d78e..67bd76ce6 100644 --- a/tests/StackExchange.Redis.Tests/Sets.cs +++ b/tests/StackExchange.Redis.Tests/Sets.cs @@ -46,7 +46,7 @@ public async Task SetRemoveArgTests() var db = conn.GetDatabase(); var key = Me(); - RedisValue[] values = null; + RedisValue[]? values = null; Assert.Throws(() => db.SetRemove(key, values)); await Assert.ThrowsAsync(async () => await db.SetRemoveAsync(key, values).ForAwait()).ForAwait(); diff --git a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs index 09445f6cd..be9df7981 100644 --- a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs @@ -119,27 +119,27 @@ public event EventHandler HashSlotMoved public Task CloseAsync(bool allowCommandsToComplete = true) => _inner.CloseAsync(allowCommandsToComplete); - public bool Configure(TextWriter log = null) => _inner.Configure(log); + public bool Configure(TextWriter? log = null) => _inner.Configure(log); - public Task ConfigureAsync(TextWriter log = null) => _inner.ConfigureAsync(log); + public Task ConfigureAsync(TextWriter? log = null) => _inner.ConfigureAsync(log); public void Dispose() { } // DO NOT call _inner.Dispose(); public ServerCounters GetCounters() => _inner.GetCounters(); - public IDatabase GetDatabase(int db = -1, object asyncState = null) => _inner.GetDatabase(db, asyncState); + public IDatabase GetDatabase(int db = -1, object? asyncState = null) => _inner.GetDatabase(db, asyncState); public EndPoint[] GetEndPoints(bool configuredOnly = false) => _inner.GetEndPoints(configuredOnly); public int GetHashSlot(RedisKey key) => _inner.GetHashSlot(key); - public IServer GetServer(string host, int port, object asyncState = null) => _inner.GetServer(host, port, asyncState); + public IServer GetServer(string host, int port, object? asyncState = null) => _inner.GetServer(host, port, asyncState); - public IServer GetServer(string hostAndPort, object asyncState = null) => _inner.GetServer(hostAndPort, asyncState); + public IServer GetServer(string hostAndPort, object? asyncState = null) => _inner.GetServer(hostAndPort, asyncState); public IServer GetServer(IPAddress host, int port) => _inner.GetServer(host, port); - public IServer GetServer(EndPoint endpoint, object asyncState = null) => _inner.GetServer(endpoint, asyncState); + public IServer GetServer(EndPoint endpoint, object? asyncState = null) => _inner.GetServer(endpoint, asyncState); public string GetStatus() => _inner.GetStatus(); @@ -147,7 +147,7 @@ public event EventHandler HashSlotMoved public string GetStormLog() => _inner.GetStormLog(); - public ISubscriber GetSubscriber(object asyncState = null) => _inner.GetSubscriber(asyncState); + public ISubscriber GetSubscriber(object? asyncState = null) => _inner.GetSubscriber(asyncState); public int HashSlot(RedisKey key) => _inner.HashSlot(key); @@ -175,7 +175,7 @@ public void Dispose() GC.SuppressFinalize(this); } - protected void OnInternalError(object sender, InternalErrorEventArgs e) + protected void OnInternalError(object? sender, InternalErrorEventArgs e) { Interlocked.Increment(ref privateFailCount); lock (privateExceptions) @@ -183,7 +183,7 @@ protected void OnInternalError(object sender, InternalErrorEventArgs e) privateExceptions.Add(TestBase.Time() + ": Internal error: " + e.Origin + ", " + EndPointCollection.ToString(e.EndPoint) + "/" + e.ConnectionType); } } - protected void OnConnectionFailed(object sender, ConnectionFailedEventArgs e) + protected void OnConnectionFailed(object? sender, ConnectionFailedEventArgs e) { Interlocked.Increment(ref privateFailCount); lock (privateExceptions) diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index 454e97982..112513a85 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -5,6 +5,7 @@ true true full + enable true diff --git a/tests/StackExchange.Redis.Tests/Streams.cs b/tests/StackExchange.Redis.Tests/Streams.cs index f0294c0d7..9379d4998 100644 --- a/tests/StackExchange.Redis.Tests/Streams.cs +++ b/tests/StackExchange.Redis.Tests/Streams.cs @@ -1813,10 +1813,10 @@ await db.StreamAddAsync(streamName, new[] { var streamResult = await db.StreamRangeAsync(streamName, count: 1000); var evntJson = streamResult - .Select(x => (dynamic)JsonConvert.DeserializeObject(x["msg"])) + .Select(x => (dynamic?)JsonConvert.DeserializeObject(x["msg"])) .ToList(); var obj = Assert.Single(evntJson); - Assert.Equal(123, (int)obj.id); + Assert.Equal(123, (int)obj!.id); Assert.Equal("test", (string)obj.name); } } diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index d156e31be..5ee9ea19a 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -34,16 +34,16 @@ public async Task Append() var s3 = conn.StringGetAsync(key); var l2 = server.Features.StringLength ? conn.StringLengthAsync(key) : null; - Assert.Null((string)await s0); + Assert.Null((string?)await s0); Assert.Equal("abc", await s1); Assert.Equal(8, await result); Assert.Equal("abcdefgh", await s3); if (server.Features.StringLength) { - Assert.Equal(0, await l0); - Assert.Equal(3, await l1); - Assert.Equal(8, await l2); + Assert.Equal(0, await l0!); + Assert.Equal(3, await l1!); + Assert.Equal(8, await l2!); } } } diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 8ddb344a1..7dfec3994 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -23,11 +23,11 @@ public abstract class TestBase : IDisposable protected virtual string GetConfiguration() => GetDefaultConfiguration(); internal static string GetDefaultConfiguration() => TestConfig.Current.PrimaryServerAndPort; - private readonly SharedConnectionFixture _fixture; + private readonly SharedConnectionFixture? _fixture; protected bool SharedFixtureAvailable => _fixture != null && _fixture.IsEnabled; - protected TestBase(ITestOutputHelper output, SharedConnectionFixture fixture = null) + protected TestBase(ITestOutputHelper output, SharedConnectionFixture? fixture = null) { Output = output; Output.WriteFrameworkVersion(); @@ -48,7 +48,7 @@ protected TestBase(ITestOutputHelper output, SharedConnectionFixture fixture = n protected static Task RunBlockingSynchronousWithExtraThreadAsync(Action testScenario) => Task.Factory.StartNew(testScenario, CancellationToken.None, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); protected void LogNoTime(string message) => LogNoTime(Writer, message); - internal static void LogNoTime(TextWriter output, string message) + internal static void LogNoTime(TextWriter output, string? message) { lock (output) { @@ -59,7 +59,7 @@ internal static void LogNoTime(TextWriter output, string message) Console.WriteLine(message); } } - protected void Log(string message) => LogNoTime(Writer, message); + protected void Log(string? message) => LogNoTime(Writer, message); public static void Log(TextWriter output, string message) { lock (output) @@ -71,7 +71,7 @@ public static void Log(TextWriter output, string message) Console.WriteLine(message); } } - protected void Log(string message, params object[] args) + protected void Log(string message, params object?[] args) { lock (Output) { @@ -140,7 +140,7 @@ static TestBase() } internal static string Time() => DateTime.UtcNow.ToString("HH:mm:ss.ffff"); - protected void OnConnectionFailed(object sender, ConnectionFailedEventArgs e) + protected void OnConnectionFailed(object? sender, ConnectionFailedEventArgs e) { Interlocked.Increment(ref privateFailCount); lock (privateExceptions) @@ -150,7 +150,7 @@ protected void OnConnectionFailed(object sender, ConnectionFailedEventArgs e) Log($"Connection Failed ({e.ConnectionType},{e.FailureType}): {e.Exception}"); } - protected void OnInternalError(object sender, InternalErrorEventArgs e) + protected void OnInternalError(object? sender, InternalErrorEventArgs e) { Interlocked.Increment(ref privateFailCount); lock (privateExceptions) @@ -222,7 +222,7 @@ public void Teardown() protected static IServer GetServer(IConnectionMultiplexer muxer) { EndPoint[] endpoints = muxer.GetEndPoints(); - IServer result = null; + IServer? result = null; foreach (var endpoint in endpoints) { var server = muxer.GetServer(endpoint); @@ -245,27 +245,27 @@ protected static IServer GetAnyPrimary(IConnectionMultiplexer muxer) } internal virtual IInternalConnectionMultiplexer Create( - string clientName = null, + string? clientName = null, int? syncTimeout = null, bool? allowAdmin = null, int? keepAlive = null, int? connectTimeout = null, - string password = null, - string tieBreaker = null, - TextWriter log = null, + string? password = null, + string? tieBreaker = null, + TextWriter? log = null, bool fail = true, - string[] disabledCommands = null, - string[] enabledCommands = null, + string[]? disabledCommands = null, + string[]? enabledCommands = null, bool checkConnect = true, - string failMessage = null, - string channelPrefix = null, + string? failMessage = null, + string? channelPrefix = null, Proxy? proxy = null, - string configuration = null, + string? configuration = null, bool logTransactionData = true, bool shared = true, int? defaultDatabase = null, - BacklogPolicy backlogPolicy = null, - [CallerMemberName] string caller = null) + BacklogPolicy? backlogPolicy = null, + [CallerMemberName] string? caller = null) { if (Output == null) { @@ -313,29 +313,29 @@ internal virtual IInternalConnectionMultiplexer Create( } public static ConnectionMultiplexer CreateDefault( - TextWriter output, - string clientName = null, + TextWriter? output, + string? clientName = null, int? syncTimeout = null, bool? allowAdmin = null, int? keepAlive = null, int? connectTimeout = null, - string password = null, - string tieBreaker = null, - TextWriter log = null, + string? password = null, + string? tieBreaker = null, + TextWriter? log = null, bool fail = true, - string[] disabledCommands = null, - string[] enabledCommands = null, + string[]? disabledCommands = null, + string[]? enabledCommands = null, bool checkConnect = true, - string failMessage = null, - string channelPrefix = null, + string? failMessage = null, + string? channelPrefix = null, Proxy? proxy = null, - string configuration = null, + string? configuration = null, bool logTransactionData = true, int? defaultDatabase = null, - BacklogPolicy backlogPolicy = null, - [CallerMemberName] string caller = null) + BacklogPolicy? backlogPolicy = null, + [CallerMemberName] string? caller = null) { - StringWriter localLog = null; + StringWriter? localLog = null; if (log == null) { log = localLog = new StringWriter(); @@ -389,7 +389,7 @@ public static ConnectionMultiplexer CreateDefault( Log(output, "Connect took: " + watch.ElapsedMilliseconds + "ms"); } var muxer = task.Result; - if (checkConnect && (muxer == null || !muxer.IsConnected)) + if (checkConnect && !muxer.IsConnected) { // If fail is true, we throw. Assert.False(fail, failMessage + "Server is not available"); @@ -423,15 +423,15 @@ public static ConnectionMultiplexer CreateDefault( } } - public static string Me([CallerFilePath] string filePath = null, [CallerMemberName] string caller = null) => + public static string Me([CallerFilePath] string? filePath = null, [CallerMemberName] string? caller = null) => Environment.Version.ToString() + Path.GetFileNameWithoutExtension(filePath) + "-" + caller; - protected static TimeSpan RunConcurrent(Action work, int threads, int timeout = 10000, [CallerMemberName] string caller = null) + protected static TimeSpan RunConcurrent(Action work, int threads, int timeout = 10000, [CallerMemberName] string? caller = null) { if (work == null) throw new ArgumentNullException(nameof(work)); if (threads < 1) throw new ArgumentOutOfRangeException(nameof(threads)); if (string.IsNullOrWhiteSpace(caller)) caller = Me(); - Stopwatch watch = null; + Stopwatch? watch = null; ManualResetEvent allDone = new ManualResetEvent(false); object token = new object(); int active = 0; @@ -453,7 +453,7 @@ void callback() work(); if (Interlocked.Decrement(ref active) == 0) { - watch.Stop(); + watch?.Stop(); allDone.Set(); } } @@ -480,7 +480,7 @@ void callback() throw new TimeoutException(); } - return watch.Elapsed; + return watch?.Elapsed ?? TimeSpan.Zero; } private static readonly TimeSpan DefaultWaitPerLoop = TimeSpan.FromMilliseconds(50); diff --git a/tests/StackExchange.Redis.Tests/Transactions.cs b/tests/StackExchange.Redis.Tests/Transactions.cs index 58ccf1987..4b23c0019 100644 --- a/tests/StackExchange.Redis.Tests/Transactions.cs +++ b/tests/StackExchange.Redis.Tests/Transactions.cs @@ -374,7 +374,7 @@ public async Task BasicTranWithStringLengthCondition(string value, ComparisonTyp db.KeyDelete(key2, CommandFlags.FireAndForget); var expectSuccess = false; - Condition condition = null; + Condition? condition = null; var valueLength = value?.Length ?? 0; switch (type) { @@ -393,6 +393,8 @@ public async Task BasicTranWithStringLengthCondition(string value, ComparisonTyp condition = Condition.StringLengthLessThan(key2, length); Assert.Contains("String length < " + length, condition.ToString()); break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); } if (value != null) db.StringSet(key2, value, flags: CommandFlags.FireAndForget); @@ -452,7 +454,7 @@ public async Task BasicTranWithHashLengthCondition(string value, ComparisonType db.KeyDelete(key2, CommandFlags.FireAndForget); var expectSuccess = false; - Condition condition = null; + Condition? condition = null; var valueLength = value?.Length ?? 0; switch (type) { @@ -468,11 +470,13 @@ public async Task BasicTranWithHashLengthCondition(string value, ComparisonType expectSuccess = valueLength < length; condition = Condition.HashLengthLessThan(key2, length); break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); } for (var i = 0; i < valueLength; i++) { - db.HashSet(key2, i, value[i].ToString(), flags: CommandFlags.FireAndForget); + db.HashSet(key2, i, value![i].ToString(), flags: CommandFlags.FireAndForget); } Assert.False(db.KeyExists(key)); Assert.Equal(valueLength, db.HashLength(key2)); @@ -530,7 +534,7 @@ public async Task BasicTranWithSetCardinalityCondition(string value, ComparisonT db.KeyDelete(key2, CommandFlags.FireAndForget); var expectSuccess = false; - Condition condition = null; + Condition? condition = null; var valueLength = value?.Length ?? 0; switch (type) { @@ -546,6 +550,8 @@ public async Task BasicTranWithSetCardinalityCondition(string value, ComparisonT expectSuccess = valueLength < length; condition = Condition.SetLengthLessThan(key2, length); break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); } for (var i = 0; i < valueLength; i++) @@ -650,7 +656,7 @@ public async Task BasicTranWithSortedSetCardinalityCondition(string value, Compa db.KeyDelete(key2, CommandFlags.FireAndForget); var expectSuccess = false; - Condition condition = null; + Condition? condition = null; var valueLength = value?.Length ?? 0; switch (type) { @@ -666,6 +672,8 @@ public async Task BasicTranWithSortedSetCardinalityCondition(string value, Compa expectSuccess = valueLength < length; condition = Condition.SortedSetLengthLessThan(key2, length); break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); } for (var i = 0; i < valueLength; i++) @@ -728,7 +736,7 @@ public async Task BasicTranWithSortedSetRangeCountCondition(double min, double m db.KeyDelete(key2, CommandFlags.FireAndForget); var expectSuccess = false; - Condition condition = null; + Condition? condition = null; var valueLength = (int)(max - min) + 1; switch (type) { @@ -744,6 +752,8 @@ public async Task BasicTranWithSortedSetRangeCountCondition(double min, double m expectSuccess = valueLength < length; condition = Condition.SortedSetLengthLessThan(key2, length, min, max); break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); } for (var i = 0; i < 5; i++) @@ -1026,7 +1036,7 @@ public async Task BasicTranWithListLengthCondition(string value, ComparisonType db.KeyDelete(key2, CommandFlags.FireAndForget); var expectSuccess = false; - Condition condition = null; + Condition? condition = null; var valueLength = value?.Length ?? 0; switch (type) { @@ -1042,6 +1052,8 @@ public async Task BasicTranWithListLengthCondition(string value, ComparisonType expectSuccess = valueLength < length; condition = Condition.ListLengthLessThan(key2, length); break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); } for (var i = 0; i < valueLength; i++) @@ -1106,22 +1118,24 @@ public async Task BasicTranWithStreamLengthCondition(string value, ComparisonTyp db.KeyDelete(key2, CommandFlags.FireAndForget); var expectSuccess = false; - Condition condition = null; + Condition? condition = null; var valueLength = value?.Length ?? 0; switch (type) { - case ComparisonType.Equal: - expectSuccess = valueLength == length; - condition = Condition.StreamLengthEqual(key2, length); - break; - case ComparisonType.GreaterThan: - expectSuccess = valueLength > length; - condition = Condition.StreamLengthGreaterThan(key2, length); - break; - case ComparisonType.LessThan: - expectSuccess = valueLength < length; - condition = Condition.StreamLengthLessThan(key2, length); - break; + case ComparisonType.Equal: + expectSuccess = valueLength == length; + condition = Condition.StreamLengthEqual(key2, length); + break; + case ComparisonType.GreaterThan: + expectSuccess = valueLength > length; + condition = Condition.StreamLengthGreaterThan(key2, length); + break; + case ComparisonType.LessThan: + expectSuccess = valueLength < length; + condition = Condition.StreamLengthLessThan(key2, length); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); } RedisValue fieldName = "Test"; for (var i = 0; i < valueLength; i++) diff --git a/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs b/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs index b00bf3127..bd32c235c 100644 --- a/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs +++ b/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs @@ -39,7 +39,7 @@ public void NullPrefixIsError_Bytes() { using var conn = Create(); var raw = conn.GetDatabase(); - raw.WithKeyPrefix((byte[])null); + raw.WithKeyPrefix((byte[]?)null); }); } @@ -50,7 +50,7 @@ public void NullPrefixIsError_String() { using var conn = Create(); var raw = conn.GetDatabase(); - raw.WithKeyPrefix((string)null); + raw.WithKeyPrefix((string?)null); }); } @@ -62,7 +62,7 @@ public void NullDatabaseIsError(string prefix) { Assert.Throws(() => { - IDatabase raw = null; + IDatabase? raw = null; raw.WithKeyPrefix(prefix); }); } From cbd6d7e7688bc33d6eb8feaf0d732db794d94e82 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 19 Mar 2022 11:01:32 -0400 Subject: [PATCH 109/435] Bug: fix ClientKill/ClientKillAsync when by ClientType (#2048) This is an errant addition to client kill I guess no one tripped on for 8+ years, just happened across it in NRT deep dives. --- docs/ReleaseNotes.md | 2 ++ src/StackExchange.Redis/RedisServer.cs | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index e11989cb7..fa55fbb71 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,6 +6,8 @@ - Adds: `KEEPTTL` support on `SET` operations ([#2029 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2029)) - Fix: Allow `XTRIM` `MAXLEN` argument to be `0` ([#2030 by NicoAvanzDev](https://github.com/StackExchange/StackExchange.Redis/pull/2030)) - Adds: `ConfigurationOptions.BeforeSocketConnect` for configuring sockets between creation and connection ([#2031 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2031)) +- Fix [#1813](https://github.com/StackExchange/StackExchange.Redis/issues/1813): Don't connect to endpoints we failed to parse ([#2042 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2042)) +- Fix: `ClientKill`/`ClientKillAsync` when using `ClientType` ([#2048 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2048)) ## 2.5.43 diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 46b6c54ec..c50976892 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -100,7 +100,6 @@ private Message GetClientKillMessage(EndPoint endpoint, long? id, ClientType? cl default: throw new ArgumentOutOfRangeException(nameof(clientType)); } - parts.Add(id.Value); } if (endpoint != null) { From 9fb222e86fe596558e07f8ecfdc9a901a4893fb4 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 19 Mar 2022 18:21:33 -0400 Subject: [PATCH 110/435] ConfigurationOptions: shuffle in prep for no-clone (#2049) Cleaning up a bit before moving to no-clone for adjustable configuration. No functionality changes, just moving and ordering lots of cheese to make things easier, same with NRT diffs later. --- .../ConfigurationOptions.cs | 165 +- .../ConnectionMultiplexer.Compat.cs | 19 + .../ConnectionMultiplexer.Debug.cs | 39 + .../ConnectionMultiplexer.Events.cs | 120 ++ ...nnectionMultiplexer.ExportConfiguration.cs | 141 ++ .../ConnectionMultiplexer.FeatureFlags.cs | 56 + .../ConnectionMultiplexer.Profiling.cs | 23 +- .../ConnectionMultiplexer.ReaderWriter.cs | 48 +- .../ConnectionMultiplexer.Sentinel.cs | 417 ++++ .../ConnectionMultiplexer.StormLog.cs | 28 + .../ConnectionMultiplexer.Threading.cs | 57 +- .../ConnectionMultiplexer.Verbose.cs | 79 +- .../ConnectionMultiplexer.cs | 1892 +++++------------ src/StackExchange.Redis/EndPointCollection.cs | 52 + .../Maintenance/ServerMaintenanceEvent.cs | 2 +- src/StackExchange.Redis/PublicAPI.Shipped.txt | 1 + .../ServerSelectionStrategy.cs | 5 +- src/StackExchange.Redis/WriteResult.cs | 9 + tests/StackExchange.Redis.Tests/SSL.cs | 2 +- .../SharedConnectionFixture.cs | 2 +- 20 files changed, 1572 insertions(+), 1585 deletions(-) create mode 100644 src/StackExchange.Redis/ConnectionMultiplexer.Compat.cs create mode 100644 src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs create mode 100644 src/StackExchange.Redis/ConnectionMultiplexer.Events.cs create mode 100644 src/StackExchange.Redis/ConnectionMultiplexer.ExportConfiguration.cs create mode 100644 src/StackExchange.Redis/ConnectionMultiplexer.FeatureFlags.cs create mode 100644 src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs create mode 100644 src/StackExchange.Redis/ConnectionMultiplexer.StormLog.cs create mode 100644 src/StackExchange.Redis/WriteResult.cs diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index ce220b257..d96bc3f65 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -335,7 +335,7 @@ public Version DefaultVersion /// /// The endpoints defined for this configuration. /// - public EndPointCollection EndPoints { get; } = new EndPointCollection(); + public EndPointCollection EndPoints { get; init; } = new EndPointCollection(); /// /// Use ThreadPriority.AboveNormal for SocketManager reader and writer threads (true by default). @@ -529,50 +529,43 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow /// /// Create a copy of the configuration. /// - public ConfigurationOptions Clone() - { - var options = new ConfigurationOptions - { - defaultOptions = defaultOptions, - ClientName = ClientName, - ServiceName = ServiceName, - keepAlive = keepAlive, - syncTimeout = syncTimeout, - asyncTimeout = asyncTimeout, - allowAdmin = allowAdmin, - defaultVersion = defaultVersion, - connectTimeout = connectTimeout, - User = User, - Password = Password, - tieBreaker = tieBreaker, - ssl = ssl, - sslHost = sslHost, - highPrioritySocketThreads = highPrioritySocketThreads, - configChannel = configChannel, - abortOnConnectFail = abortOnConnectFail, - resolveDns = resolveDns, - proxy = proxy, - commandMap = commandMap, - CertificateValidationCallback = CertificateValidationCallback, - CertificateSelectionCallback = CertificateSelectionCallback, - ChannelPrefix = ChannelPrefix.Clone(), - SocketManager = SocketManager, - connectRetry = connectRetry, - configCheckSeconds = configCheckSeconds, - responseTimeout = responseTimeout, - DefaultDatabase = DefaultDatabase, - ReconnectRetryPolicy = reconnectRetryPolicy, - BacklogPolicy = backlogPolicy, - SslProtocols = SslProtocols, - checkCertificateRevocation = checkCertificateRevocation, - BeforeSocketConnect = BeforeSocketConnect, - }; - foreach (var item in EndPoints) - { - options.EndPoints.Add(item); - } - return options; - } + public ConfigurationOptions Clone() => new ConfigurationOptions + { + defaultOptions = defaultOptions, + ClientName = ClientName, + ServiceName = ServiceName, + keepAlive = keepAlive, + syncTimeout = syncTimeout, + asyncTimeout = asyncTimeout, + allowAdmin = allowAdmin, + defaultVersion = defaultVersion, + connectTimeout = connectTimeout, + User = User, + Password = Password, + tieBreaker = tieBreaker, + ssl = ssl, + sslHost = sslHost, + highPrioritySocketThreads = highPrioritySocketThreads, + configChannel = configChannel, + abortOnConnectFail = abortOnConnectFail, + resolveDns = resolveDns, + proxy = proxy, + commandMap = commandMap, + CertificateValidationCallback = CertificateValidationCallback, + CertificateSelectionCallback = CertificateSelectionCallback, + ChannelPrefix = ChannelPrefix.Clone(), + SocketManager = SocketManager, + connectRetry = connectRetry, + configCheckSeconds = configCheckSeconds, + responseTimeout = responseTimeout, + DefaultDatabase = DefaultDatabase, + ReconnectRetryPolicy = reconnectRetryPolicy, + BacklogPolicy = backlogPolicy, + SslProtocols = SslProtocols, + checkCertificateRevocation = checkCertificateRevocation, + BeforeSocketConnect = BeforeSocketConnect, + EndPoints = new EndPointCollection(EndPoints), + }; /// /// Apply settings to configure this instance of , e.g. for a specific scenario. @@ -585,23 +578,30 @@ public ConfigurationOptions Apply(Action configure) return this; } + internal ConfigurationOptions WithDefaults(bool sentinel = false) + { + if (sentinel) + { + // this is required when connecting to sentinel servers + TieBreaker = ""; + CommandMap = CommandMap.Sentinel; + + // use default sentinel port + EndPoints.SetDefaultPorts(26379); + } + else + { + SetDefaultPorts(); + } + return this; + } + /// /// Resolve the default port for any endpoints that did not have a port explicitly specified. /// public void SetDefaultPorts() => EndPoints.SetDefaultPorts(Ssl ? 6380 : 6379); - /// - /// Sets default config settings required for sentinel usage. - /// - internal void SetSentinelDefaults() - { - // this is required when connecting to sentinel servers - TieBreaker = ""; - CommandMap = CommandMap.Sentinel; - - // use default sentinel port - EndPoints.SetDefaultPorts(26379); - } + internal bool IsSentinel => !string.IsNullOrEmpty(ServiceName); /// /// Returns the effective configuration string for this configuration, including Redis credentials. @@ -652,57 +652,6 @@ public string ToString(bool includePassword) return sb.ToString(); } - internal bool HasDnsEndPoints() - { - foreach (var endpoint in EndPoints) - { - if (endpoint is DnsEndPoint) - { - return true; - } - } - return false; - } - - internal async Task ResolveEndPointsAsync(ConnectionMultiplexer multiplexer, LogProxy log) - { - var cache = new Dictionary(StringComparer.OrdinalIgnoreCase); - for (int i = 0; i < EndPoints.Count; i++) - { - if (EndPoints[i] is DnsEndPoint dns) - { - try - { - if (dns.Host == ".") - { - EndPoints[i] = new IPEndPoint(IPAddress.Loopback, dns.Port); - } - else if (cache.TryGetValue(dns.Host, out IPAddress ip)) - { // use cache - EndPoints[i] = new IPEndPoint(ip, dns.Port); - } - else - { - log?.WriteLine($"Using DNS to resolve '{dns.Host}'..."); - var ips = await Dns.GetHostAddressesAsync(dns.Host).ObserveErrors().ForAwait(); - if (ips.Length == 1) - { - ip = ips[0]; - log?.WriteLine($"'{dns.Host}' => {ip}"); - cache[dns.Host] = ip; - EndPoints[i] = new IPEndPoint(ip, dns.Port); - } - } - } - catch (Exception ex) - { - multiplexer.OnInternalError(ex); - log?.WriteLine(ex.Message); - } - } - } - } - private static void Append(StringBuilder sb, object value) { if (value == null) return; diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Compat.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Compat.cs new file mode 100644 index 000000000..f105fe2ca --- /dev/null +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Compat.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; + +namespace StackExchange.Redis; + +public partial class ConnectionMultiplexer +{ + /// + /// No longer used. + /// + [Obsolete("No longer used, will be removed in 3.0.")] + public static TaskFactory Factory { get => Task.Factory; set { } } + + /// + /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order. + /// + [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue) + ", will be removed in 3.0", false)] + public bool PreserveAsyncOrder { get => false; set { } } +} diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs new file mode 100644 index 000000000..da3f61be9 --- /dev/null +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs @@ -0,0 +1,39 @@ +using System.Threading; + +namespace StackExchange.Redis; + +public partial class ConnectionMultiplexer +{ + private static int _collectedWithoutDispose; + internal static int CollectedWithoutDispose => Thread.VolatileRead(ref _collectedWithoutDispose); + + /// + /// Invoked by the garbage collector. + /// + ~ConnectionMultiplexer() + { + Interlocked.Increment(ref _collectedWithoutDispose); + } + + bool IInternalConnectionMultiplexer.AllowConnect + { + get => AllowConnect; + set => AllowConnect = value; + } + + bool IInternalConnectionMultiplexer.IgnoreConnect + { + get => IgnoreConnect; + set => IgnoreConnect = value; + } + + /// + /// For debugging: when not enabled, servers cannot connect. + /// + internal volatile bool AllowConnect = true; + + /// + /// For debugging: when not enabled, end-connect is silently ignored (to simulate a long-running connect). + /// + internal volatile bool IgnoreConnect; +} diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Events.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Events.cs new file mode 100644 index 000000000..4824bb4ce --- /dev/null +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Events.cs @@ -0,0 +1,120 @@ +using System; +using System.Net; +using System.Runtime.CompilerServices; +using StackExchange.Redis.Maintenance; + +namespace StackExchange.Redis; + +public partial class ConnectionMultiplexer +{ + /// + /// Raised whenever a physical connection fails. + /// + public event EventHandler ConnectionFailed; + internal void OnConnectionFailed(EndPoint endpoint, ConnectionType connectionType, ConnectionFailureType failureType, Exception exception, bool reconfigure, string physicalName) + { + if (_isDisposed) return; + var handler = ConnectionFailed; + if (handler != null) + { + CompleteAsWorker(new ConnectionFailedEventArgs(handler, this, endpoint, connectionType, failureType, exception, physicalName)); + } + if (reconfigure) + { + ReconfigureIfNeeded(endpoint, false, "connection failed"); + } + } + + /// + /// Raised whenever an internal error occurs (this is primarily for debugging). + /// + public event EventHandler InternalError; + internal void OnInternalError(Exception exception, EndPoint endpoint = null, ConnectionType connectionType = ConnectionType.None, [CallerMemberName] string origin = null) + { + try + { + if (_isDisposed) return; + Trace("Internal error: " + origin + ", " + exception == null ? "unknown" : exception.Message); + var handler = InternalError; + if (handler != null) + { + CompleteAsWorker(new InternalErrorEventArgs(handler, this, endpoint, connectionType, exception, origin)); + } + } + catch + { + // Our internal error event failed...whatcha gonna do, exactly? + } + } + + /// + /// Raised whenever a physical connection is established. + /// + public event EventHandler ConnectionRestored; + internal void OnConnectionRestored(EndPoint endpoint, ConnectionType connectionType, string physicalName) + { + if (_isDisposed) return; + var handler = ConnectionRestored; + if (handler != null) + { + CompleteAsWorker(new ConnectionFailedEventArgs(handler, this, endpoint, connectionType, ConnectionFailureType.None, null, physicalName)); + } + ReconfigureIfNeeded(endpoint, false, "connection restored"); + } + + /// + /// Raised when configuration changes are detected. + /// + public event EventHandler ConfigurationChanged; + internal void OnConfigurationChanged(EndPoint endpoint) => OnEndpointChanged(endpoint, ConfigurationChanged); + + /// + /// Raised when nodes are explicitly requested to reconfigure via broadcast. + /// This usually means primary/replica changes. + /// + public event EventHandler ConfigurationChangedBroadcast; + internal void OnConfigurationChangedBroadcast(EndPoint endpoint) => OnEndpointChanged(endpoint, ConfigurationChangedBroadcast); + + private void OnEndpointChanged(EndPoint endpoint, EventHandler handler) + { + if (_isDisposed) return; + if (handler != null) + { + CompleteAsWorker(new EndPointEventArgs(handler, this, endpoint)); + } + } + + /// + /// Raised when server indicates a maintenance event is going to happen. + /// + public event EventHandler ServerMaintenanceEvent; + internal void OnServerMaintenanceEvent(ServerMaintenanceEvent e) => + ServerMaintenanceEvent?.Invoke(this, e); + + /// + /// Raised when a hash-slot has been relocated. + /// + public event EventHandler HashSlotMoved; + internal void OnHashSlotMoved(int hashSlot, EndPoint old, EndPoint @new) + { + var handler = HashSlotMoved; + if (handler != null) + { + CompleteAsWorker(new HashSlotMovedEventArgs(handler, this, hashSlot, old, @new)); + } + } + + /// + /// Raised when a server replied with an error message. + /// + public event EventHandler ErrorMessage; + internal void OnErrorMessage(EndPoint endpoint, string message) + { + if (_isDisposed) return; + var handler = ErrorMessage; + if (handler != null) + { + CompleteAsWorker(new RedisErrorEventArgs(handler, this, endpoint, message)); + } + } +} diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.ExportConfiguration.cs b/src/StackExchange.Redis/ConnectionMultiplexer.ExportConfiguration.cs new file mode 100644 index 000000000..d9fafdea6 --- /dev/null +++ b/src/StackExchange.Redis/ConnectionMultiplexer.ExportConfiguration.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; + +namespace StackExchange.Redis; + +public partial class ConnectionMultiplexer +{ + private const string NoContent = "(no content)"; + + /// + /// Write the configuration of all servers to an output stream. + /// + /// The destination stream to write the export to. + /// The options to use for this export. + public void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All) + { + if (destination == null) throw new ArgumentNullException(nameof(destination)); + + // What is possible, given the command map? + ExportOptions mask = 0; + if (CommandMap.IsAvailable(RedisCommand.INFO)) mask |= ExportOptions.Info; + if (CommandMap.IsAvailable(RedisCommand.CONFIG)) mask |= ExportOptions.Config; + if (CommandMap.IsAvailable(RedisCommand.CLIENT)) mask |= ExportOptions.Client; + if (CommandMap.IsAvailable(RedisCommand.CLUSTER)) mask |= ExportOptions.Cluster; + options &= mask; + + using (var zip = new ZipArchive(destination, ZipArchiveMode.Create, true)) + { + var arr = GetServerSnapshot(); + foreach (var server in arr) + { + const CommandFlags flags = CommandFlags.None; + if (!server.IsConnected) continue; + var api = GetServer(server.EndPoint); + + List tasks = new List(); + if ((options & ExportOptions.Info) != 0) + { + tasks.Add(api.InfoRawAsync(flags: flags)); + } + if ((options & ExportOptions.Config) != 0) + { + tasks.Add(api.ConfigGetAsync(flags: flags)); + } + if ((options & ExportOptions.Client) != 0) + { + tasks.Add(api.ClientListAsync(flags: flags)); + } + if ((options & ExportOptions.Cluster) != 0) + { + tasks.Add(api.ClusterNodesRawAsync(flags: flags)); + } + + WaitAllIgnoreErrors(tasks.ToArray()); + + int index = 0; + var prefix = Format.ToString(server.EndPoint); + if ((options & ExportOptions.Info) != 0) + { + Write(zip, prefix + "/info.txt", tasks[index++], WriteNormalizingLineEndings); + } + if ((options & ExportOptions.Config) != 0) + { + Write[]>(zip, prefix + "/config.txt", tasks[index++], (settings, writer) => + { + foreach (var setting in settings) + { + writer.WriteLine("{0}={1}", setting.Key, setting.Value); + } + }); + } + if ((options & ExportOptions.Client) != 0) + { + Write(zip, prefix + "/clients.txt", tasks[index++], (clients, writer) => + { + if (clients == null) + { + writer.WriteLine(NoContent); + } + else + { + foreach (var client in clients) + { + writer.WriteLine(client.Raw); + } + } + }); + } + if ((options & ExportOptions.Cluster) != 0) + { + Write(zip, prefix + "/nodes.txt", tasks[index++], WriteNormalizingLineEndings); + } + } + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] + private static void Write(ZipArchive zip, string name, Task task, Action callback) + { + var entry = zip.CreateEntry(name, CompressionLevel.Optimal); + using (var stream = entry.Open()) + using (var writer = new StreamWriter(stream)) + { + TaskStatus status = task.Status; + switch (status) + { + case TaskStatus.RanToCompletion: + T val = ((Task)task).Result; + callback(val, writer); + break; + case TaskStatus.Faulted: + writer.WriteLine(string.Join(", ", task.Exception.InnerExceptions.Select(x => x.Message))); + break; + default: + writer.WriteLine(status.ToString()); + break; + } + } + } + + private static void WriteNormalizingLineEndings(string source, StreamWriter writer) + { + if (source == null) + { + writer.WriteLine(NoContent); + } + else + { + using (var reader = new StringReader(source)) + { + string line; + while ((line = reader.ReadLine()) != null) + writer.WriteLine(line); // normalize line endings + } + } + } +} diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.FeatureFlags.cs b/src/StackExchange.Redis/ConnectionMultiplexer.FeatureFlags.cs new file mode 100644 index 000000000..a6c2168f6 --- /dev/null +++ b/src/StackExchange.Redis/ConnectionMultiplexer.FeatureFlags.cs @@ -0,0 +1,56 @@ +using System; +using System.ComponentModel; +using System.Threading; + +namespace StackExchange.Redis; + +public partial class ConnectionMultiplexer +{ + private static FeatureFlags s_featureFlags; + + [Flags] + private enum FeatureFlags + { + None, + PreventThreadTheft = 1, + } + + private static void SetAutodetectFeatureFlags() + { + bool value = false; + try + { // attempt to detect a known problem scenario + value = SynchronizationContext.Current?.GetType()?.Name + == "LegacyAspNetSynchronizationContext"; + } + catch { } + SetFeatureFlag(nameof(FeatureFlags.PreventThreadTheft), value); + } + + /// + /// Enables or disables a feature flag. + /// This should only be used under support guidance, and should not be rapidly toggled. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public static void SetFeatureFlag(string flag, bool enabled) + { + if (Enum.TryParse(flag, true, out var flags)) + { + if (enabled) s_featureFlags |= flags; + else s_featureFlags &= ~flags; + } + } + + /// + /// Returns the state of a feature flag. + /// This should only be used under support guidance. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public static bool GetFeatureFlag(string flag) + => Enum.TryParse(flag, true, out var flags) + && (s_featureFlags & flags) == flags; + + internal static bool PreventThreadTheft => (s_featureFlags & FeatureFlags.PreventThreadTheft) != 0; +} diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs index 29a1d0246..90477466f 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs @@ -1,18 +1,17 @@ using System; using StackExchange.Redis.Profiling; -namespace StackExchange.Redis +namespace StackExchange.Redis; + +public partial class ConnectionMultiplexer { - public partial class ConnectionMultiplexer - { - private Func _profilingSessionProvider; + private Func _profilingSessionProvider; - /// - /// Register a callback to provide an on-demand ambient session provider based on the - /// calling context; the implementing code is responsible for reliably resolving the same provider - /// based on ambient context, or returning null to not profile - /// - /// The session provider to register. - public void RegisterProfiler(Func profilingSessionProvider) => _profilingSessionProvider = profilingSessionProvider; - } + /// + /// Register a callback to provide an on-demand ambient session provider based on the + /// calling context; the implementing code is responsible for reliably resolving the same provider + /// based on ambient context, or returning null to not profile + /// + /// The session provider to register. + public void RegisterProfiler(Func profilingSessionProvider) => _profilingSessionProvider = profilingSessionProvider; } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs b/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs index 6a298b4f0..e898b78a5 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs @@ -1,36 +1,32 @@ -using System; +namespace StackExchange.Redis; -namespace StackExchange.Redis +public partial class ConnectionMultiplexer { - public partial class ConnectionMultiplexer - { - internal SocketManager SocketManager { get; private set; } + internal SocketManager SocketManager { get; private set; } - partial void OnCreateReaderWriter(ConfigurationOptions configuration) - { - SocketManager = configuration.SocketManager ?? GetDefaultSocketManager(); - } + private void OnCreateReaderWriter(ConfigurationOptions configuration) + { + SocketManager = configuration.SocketManager ?? GetDefaultSocketManager(); + } - partial void OnCloseReaderWriter() - { - SocketManager = null; - } - partial void OnWriterCreated(); + private void OnCloseReaderWriter() + { + SocketManager = null; + } - /// - /// .NET 6.0+ has changes to sync-over-async stalls in the .NET primary thread pool - /// If we're in that environment, by default remove the overhead of our own threadpool - /// This will eliminate some context-switching overhead and better-size threads on both large - /// and small environments, from 16 core machines to single core VMs where the default 10 threads - /// isn't an ideal situation. - /// - internal static SocketManager GetDefaultSocketManager() - { + /// + /// .NET 6.0+ has changes to sync-over-async stalls in the .NET primary thread pool + /// If we're in that environment, by default remove the overhead of our own threadpool + /// This will eliminate some context-switching overhead and better-size threads on both large + /// and small environments, from 16 core machines to single core VMs where the default 10 threads + /// isn't an ideal situation. + /// + internal static SocketManager GetDefaultSocketManager() + { #if NET6_0_OR_GREATER - return SocketManager.ThreadPool; + return SocketManager.ThreadPool; #else - return SocketManager.Shared; + return SocketManager.Shared; #endif - } } } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs new file mode 100644 index 000000000..fc8dd3ae6 --- /dev/null +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs @@ -0,0 +1,417 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Pipelines.Sockets.Unofficial; + +namespace StackExchange.Redis; + +public partial class ConnectionMultiplexer +{ + internal EndPoint currentSentinelPrimaryEndPoint; + internal Timer sentinelPrimaryReconnectTimer; + internal Dictionary sentinelConnectionChildren = new Dictionary(); + internal ConnectionMultiplexer sentinelConnection = null; + + /// + /// Initializes the connection as a Sentinel connection and adds the necessary event handlers to track changes to the managed primaries. + /// + /// The writer to log to, if any. + internal void InitializeSentinel(LogProxy logProxy) + { + if (ServerSelectionStrategy.ServerType != ServerType.Sentinel) + { + return; + } + + // Subscribe to sentinel change events + ISubscriber sub = GetSubscriber(); + + if (sub.SubscribedEndpoint("+switch-master") == null) + { + sub.Subscribe("+switch-master", (_, message) => + { + string[] messageParts = ((string)message).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + EndPoint switchBlame = Format.TryParseEndPoint(string.Format("{0}:{1}", messageParts[1], messageParts[2])); + + lock (sentinelConnectionChildren) + { + // Switch the primary if we have connections for that service + if (sentinelConnectionChildren.ContainsKey(messageParts[0])) + { + ConnectionMultiplexer child = sentinelConnectionChildren[messageParts[0]]; + + // Is the connection still valid? + if (child.IsDisposed) + { + child.ConnectionFailed -= OnManagedConnectionFailed; + child.ConnectionRestored -= OnManagedConnectionRestored; + sentinelConnectionChildren.Remove(messageParts[0]); + } + else + { + SwitchPrimary(switchBlame, sentinelConnectionChildren[messageParts[0]]); + } + } + } + }, CommandFlags.FireAndForget); + } + + // If we lose connection to a sentinel server, + // we need to reconfigure to make sure we still have a subscription to the +switch-master channel + ConnectionFailed += (sender, e) => + { + // Reconfigure to get subscriptions back online + ReconfigureAsync(first: false, reconfigureAll: true, logProxy, e.EndPoint, "Lost sentinel connection", false).Wait(); + }; + + // Subscribe to new sentinels being added + if (sub.SubscribedEndpoint("+sentinel") == null) + { + sub.Subscribe("+sentinel", (_, message) => + { + string[] messageParts = ((string)message).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + UpdateSentinelAddressList(messageParts[0]); + }, CommandFlags.FireAndForget); + } + } + + + /// + /// Create a new instance that connects to a Sentinel server. + /// + /// The string configuration to use for this multiplexer. + /// The to log to. + public static ConnectionMultiplexer SentinelConnect(string configuration, TextWriter log = null) => + SentinelConnect(ConfigurationOptions.Parse(configuration), log); + + /// + /// Create a new instance that connects to a Sentinel server. + /// + /// The string configuration to use for this multiplexer. + /// The to log to. + public static Task SentinelConnectAsync(string configuration, TextWriter log = null) => + SentinelConnectAsync(ConfigurationOptions.Parse(configuration), log); + + /// + /// Create a new instance that connects to a Sentinel server. + /// + /// The configuration options to use for this multiplexer. + /// The to log to. + public static ConnectionMultiplexer SentinelConnect(ConfigurationOptions configuration, TextWriter log = null) + { + SocketConnection.AssertDependencies(); + return ConnectImpl(PrepareConfig(configuration, sentinel: true), log); + } + + /// + /// Create a new instance that connects to a Sentinel server. + /// + /// The configuration options to use for this multiplexer. + /// The to log to. + public static Task SentinelConnectAsync(ConfigurationOptions configuration, TextWriter log = null) + { + SocketConnection.AssertDependencies(); + return ConnectImplAsync(PrepareConfig(configuration, sentinel: true), log); + } + + /// + /// Create a new instance that connects to a sentinel server, discovers the current primary server + /// for the specified in the config and returns a managed connection to the current primary server. + /// + /// The configuration options to use for this multiplexer. + /// The to log to. + private static ConnectionMultiplexer SentinelPrimaryConnect(ConfigurationOptions configuration, TextWriter log = null) + { + var sentinelConnection = SentinelConnect(configuration, log); + + var muxer = sentinelConnection.GetSentinelMasterConnection(configuration, log); + // Set reference to sentinel connection so that we can dispose it + muxer.sentinelConnection = sentinelConnection; + + return muxer; + } + + /// + /// Create a new instance that connects to a sentinel server, discovers the current primary server + /// for the specified in the config and returns a managed connection to the current primary server. + /// + /// The configuration options to use for this multiplexer. + /// The to log to. + private static async Task SentinelPrimaryConnectAsync(ConfigurationOptions configuration, TextWriter log = null) + { + var sentinelConnection = await SentinelConnectAsync(configuration, log).ForAwait(); + + var muxer = sentinelConnection.GetSentinelMasterConnection(configuration, log); + // Set reference to sentinel connection so that we can dispose it + muxer.sentinelConnection = sentinelConnection; + + return muxer; + } + + /// + /// Returns a managed connection to the primary server indicated by the in the config. + /// + /// The configuration to be used when connecting to the primary. + /// The writer to log to, if any. + public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions config, TextWriter log = null) + { + if (ServerSelectionStrategy.ServerType != ServerType.Sentinel) + { + throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, + "Sentinel: The ConnectionMultiplexer is not a Sentinel connection. Detected as: " + ServerSelectionStrategy.ServerType); + } + + if (string.IsNullOrEmpty(config.ServiceName)) + { + throw new ArgumentException("A ServiceName must be specified."); + } + + lock (sentinelConnectionChildren) + { + if (sentinelConnectionChildren.TryGetValue(config.ServiceName, out var sentinelConnectionChild) && !sentinelConnectionChild.IsDisposed) + return sentinelConnectionChild; + } + + bool success = false; + ConnectionMultiplexer connection = null; + + var sw = ValueStopwatch.StartNew(); + do + { + // Get an initial endpoint - try twice + EndPoint newPrimaryEndPoint = GetConfiguredPrimaryForService(config.ServiceName) + ?? GetConfiguredPrimaryForService(config.ServiceName); + + if (newPrimaryEndPoint == null) + { + throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, + $"Sentinel: Failed connecting to configured primary for service: {config.ServiceName}"); + } + + EndPoint[] replicaEndPoints = GetReplicasForService(config.ServiceName) + ?? GetReplicasForService(config.ServiceName); + + // Replace the primary endpoint, if we found another one + // If not, assume the last state is the best we have and minimize the race + if (config.EndPoints.Count == 1) + { + config.EndPoints[0] = newPrimaryEndPoint; + } + else + { + config.EndPoints.Clear(); + config.EndPoints.TryAdd(newPrimaryEndPoint); + } + + foreach (var replicaEndPoint in replicaEndPoints) + { + config.EndPoints.TryAdd(replicaEndPoint); + } + + connection = ConnectImpl(config, log); + + // verify role is primary according to: + // https://redis.io/topics/sentinel-clients + if (connection.GetServer(newPrimaryEndPoint)?.Role().Value == RedisLiterals.master) + { + success = true; + break; + } + + Thread.Sleep(100); + } while (sw.ElapsedMilliseconds < config.ConnectTimeout); + + if (!success) + { + throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, + $"Sentinel: Failed connecting to configured primary for service: {config.ServiceName}"); + } + + // Attach to reconnect event to ensure proper connection to the new primary + connection.ConnectionRestored += OnManagedConnectionRestored; + + // If we lost the connection, run a switch to a least try and get updated info about the primary + connection.ConnectionFailed += OnManagedConnectionFailed; + + lock (sentinelConnectionChildren) + { + sentinelConnectionChildren[connection.RawConfig.ServiceName] = connection; + } + + // Perform the initial switchover + SwitchPrimary(RawConfig.EndPoints[0], connection, log); + + return connection; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "We don't care.")] + internal void OnManagedConnectionRestored(object sender, ConnectionFailedEventArgs e) + { + ConnectionMultiplexer connection = (ConnectionMultiplexer)sender; + + var oldTimer = Interlocked.Exchange(ref connection.sentinelPrimaryReconnectTimer, null); + oldTimer?.Dispose(); + + try + { + // Run a switch to make sure we have update-to-date + // information about which primary we should connect to + SwitchPrimary(e.EndPoint, connection); + + try + { + // Verify that the reconnected endpoint is a primary, + // and the correct one otherwise we should reconnect + if (connection.GetServer(e.EndPoint).IsReplica || e.EndPoint != connection.currentSentinelPrimaryEndPoint) + { + // This isn't a primary, so try connecting again + SwitchPrimary(e.EndPoint, connection); + } + } + catch (Exception) + { + // If we get here it means that we tried to reconnect to a server that is no longer + // considered a primary by Sentinel and was removed from the list of endpoints. + + // If we caught an exception, we may have gotten a stale endpoint + // we are not aware of, so retry + SwitchPrimary(e.EndPoint, connection); + } + } + catch (Exception) + { + // Log, but don't throw in an event handler + // TODO: Log via new event handler? a la ConnectionFailed? + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "We don't care.")] + internal void OnManagedConnectionFailed(object sender, ConnectionFailedEventArgs e) + { + ConnectionMultiplexer connection = (ConnectionMultiplexer)sender; + // Periodically check to see if we can reconnect to the proper primary. + // This is here in case we lost our subscription to a good sentinel instance + // or if we miss the published primary change. + if (connection.sentinelPrimaryReconnectTimer == null) + { + connection.sentinelPrimaryReconnectTimer = new Timer(_ => + { + try + { + // Attempt, but do not fail here + SwitchPrimary(e.EndPoint, connection); + } + catch (Exception) + { + } + finally + { + connection.sentinelPrimaryReconnectTimer?.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan); + } + }, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan); + } + } + + internal EndPoint GetConfiguredPrimaryForService(string serviceName) => + GetServerSnapshot() + .ToArray() + .Where(s => s.ServerType == ServerType.Sentinel) + .AsParallel() + .Select(s => + { + try { return GetServer(s.EndPoint).SentinelGetMasterAddressByName(serviceName); } + catch { return null; } + }) + .FirstOrDefault(r => r != null); + + internal EndPoint[] GetReplicasForService(string serviceName) => + GetServerSnapshot() + .ToArray() + .Where(s => s.ServerType == ServerType.Sentinel) + .AsParallel() + .Select(s => + { + try { return GetServer(s.EndPoint).SentinelGetReplicaAddresses(serviceName); } + catch { return null; } + }) + .FirstOrDefault(r => r != null); + + /// + /// Switches the SentinelMasterConnection over to a new primary. + /// + /// The endpoint responsible for the switch. + /// The connection that should be switched over to a new primary endpoint. + /// The writer to log to, if any. + internal void SwitchPrimary(EndPoint switchBlame, ConnectionMultiplexer connection, TextWriter log = null) + { + if (log == null) log = TextWriter.Null; + + using (var logProxy = LogProxy.TryCreate(log)) + { + string serviceName = connection.RawConfig.ServiceName; + + // Get new primary - try twice + EndPoint newPrimaryEndPoint = GetConfiguredPrimaryForService(serviceName) + ?? GetConfiguredPrimaryForService(serviceName) + ?? throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, + $"Sentinel: Failed connecting to switch primary for service: {serviceName}"); + + connection.currentSentinelPrimaryEndPoint = newPrimaryEndPoint; + + if (!connection.servers.Contains(newPrimaryEndPoint)) + { + EndPoint[] replicaEndPoints = GetReplicasForService(serviceName) + ?? GetReplicasForService(serviceName); + + connection.servers.Clear(); + connection.RawConfig.EndPoints.Clear(); + connection.RawConfig.EndPoints.TryAdd(newPrimaryEndPoint); + foreach (var replicaEndPoint in replicaEndPoints) + { + connection.RawConfig.EndPoints.TryAdd(replicaEndPoint); + } + Trace($"Switching primary to {newPrimaryEndPoint}"); + // Trigger a reconfigure + connection.ReconfigureAsync(first: false, reconfigureAll: false, logProxy, switchBlame, + $"Primary switch {serviceName}", false, CommandFlags.PreferMaster).Wait(); + + UpdateSentinelAddressList(serviceName); + } + } + } + + internal void UpdateSentinelAddressList(string serviceName) + { + var firstCompleteRequest = GetServerSnapshot() + .ToArray() + .Where(s => s.ServerType == ServerType.Sentinel) + .AsParallel() + .Select(s => + { + try { return GetServer(s.EndPoint).SentinelGetSentinelAddresses(serviceName); } + catch { return null; } + }) + .FirstOrDefault(r => r != null); + + // Ignore errors, as having an updated sentinel list is not essential + if (firstCompleteRequest == null) + return; + + bool hasNew = false; + foreach (EndPoint newSentinel in firstCompleteRequest.Where(x => !RawConfig.EndPoints.Contains(x))) + { + hasNew = true; + RawConfig.EndPoints.TryAdd(newSentinel); + } + + if (hasNew) + { + // Reconfigure the sentinel multiplexer if we added new endpoints + ReconfigureAsync(first: false, reconfigureAll: true, null, RawConfig.EndPoints[0], "Updating Sentinel List", false).Wait(); + } + } +} diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.StormLog.cs b/src/StackExchange.Redis/ConnectionMultiplexer.StormLog.cs new file mode 100644 index 000000000..2e1ad1b5f --- /dev/null +++ b/src/StackExchange.Redis/ConnectionMultiplexer.StormLog.cs @@ -0,0 +1,28 @@ +using System.Threading; + +namespace StackExchange.Redis; + +public partial class ConnectionMultiplexer +{ + internal int haveStormLog = 0; + internal string stormLogSnapshot; + /// + /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time). + /// Set to a negative value to disable this feature. + /// + public int StormLogThreshold { get; set; } = 15; + + /// + /// Obtains the log of unusual busy patterns. + /// + public string GetStormLog() => Volatile.Read(ref stormLogSnapshot); + + /// + /// Resets the log of unusual busy patterns. + /// + public void ResetStormLog() + { + Interlocked.Exchange(ref stormLogSnapshot, null); + Interlocked.Exchange(ref haveStormLog, 0); + } +} diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Threading.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Threading.cs index f23d010cc..3be1e0704 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Threading.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Threading.cs @@ -2,49 +2,48 @@ using System.Threading; using Pipelines.Sockets.Unofficial; -namespace StackExchange.Redis +namespace StackExchange.Redis; + +public partial class ConnectionMultiplexer { - public partial class ConnectionMultiplexer + private static readonly WaitCallback s_CompleteAsWorker = s => ((ICompletable)s).TryComplete(true); + internal static void CompleteAsWorker(ICompletable completable) { - private static readonly WaitCallback s_CompleteAsWorker = s => ((ICompletable)s).TryComplete(true); - internal static void CompleteAsWorker(ICompletable completable) + if (completable != null) { - if (completable != null) - { - ThreadPool.QueueUserWorkItem(s_CompleteAsWorker, completable); - } + ThreadPool.QueueUserWorkItem(s_CompleteAsWorker, completable); } + } - internal static bool TryCompleteHandler(EventHandler handler, object sender, T args, bool isAsync) where T : EventArgs, ICompletable + internal static bool TryCompleteHandler(EventHandler handler, object sender, T args, bool isAsync) where T : EventArgs, ICompletable + { + if (handler == null) return true; + if (isAsync) { - if (handler == null) return true; - if (isAsync) + if (handler.IsSingle()) { - if (handler.IsSingle()) + try { - try - { - handler(sender, args); - } - catch { } + handler(sender, args); } - else + catch { } + } + else + { + foreach (EventHandler sub in handler.AsEnumerable()) { - foreach (EventHandler sub in handler.AsEnumerable()) + try { - try - { - sub(sender, args); - } - catch { } + sub(sender, args); } + catch { } } - return true; - } - else - { - return false; } + return true; + } + else + { + return false; } } } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Verbose.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Verbose.cs index 7a61096b6..bd06bab44 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Verbose.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Verbose.cs @@ -3,57 +3,56 @@ using System.Net; using System.Runtime.CompilerServices; -namespace StackExchange.Redis +namespace StackExchange.Redis; + +public partial class ConnectionMultiplexer { - public partial class ConnectionMultiplexer - { - internal event Action MessageFaulted; - internal event Action Closing; - internal event Action PreTransactionExec, TransactionLog, InfoMessage; - internal event Action Connecting; - internal event Action Resurrecting; + internal event Action MessageFaulted; + internal event Action Closing; + internal event Action PreTransactionExec, TransactionLog, InfoMessage; + internal event Action Connecting; + internal event Action Resurrecting; - partial void OnTrace(string message, string category); - static partial void OnTraceWithoutContext(string message, string category); + partial void OnTrace(string message, string category); + static partial void OnTraceWithoutContext(string message, string category); - [Conditional("VERBOSE")] - internal void Trace(string message, [CallerMemberName] string category = null) => OnTrace(message, category); + [Conditional("VERBOSE")] + internal void Trace(string message, [CallerMemberName] string category = null) => OnTrace(message, category); - [Conditional("VERBOSE")] - internal void Trace(bool condition, string message, [CallerMemberName] string category = null) - { - if (condition) OnTrace(message, category); - } + [Conditional("VERBOSE")] + internal void Trace(bool condition, string message, [CallerMemberName] string category = null) + { + if (condition) OnTrace(message, category); + } - [Conditional("VERBOSE")] - internal static void TraceWithoutContext(string message, [CallerMemberName] string category = null) => OnTraceWithoutContext(message, category); + [Conditional("VERBOSE")] + internal static void TraceWithoutContext(string message, [CallerMemberName] string category = null) => OnTraceWithoutContext(message, category); - [Conditional("VERBOSE")] - internal static void TraceWithoutContext(bool condition, string message, [CallerMemberName] string category = null) - { - if (condition) OnTraceWithoutContext(message, category); - } + [Conditional("VERBOSE")] + internal static void TraceWithoutContext(bool condition, string message, [CallerMemberName] string category = null) + { + if (condition) OnTraceWithoutContext(message, category); + } - [Conditional("VERBOSE")] - internal void OnMessageFaulted(Message msg, Exception fault, [CallerMemberName] string origin = default, [CallerFilePath] string path = default, [CallerLineNumber] int lineNumber = default) => - MessageFaulted?.Invoke(msg?.CommandAndKey, fault, $"{origin} ({path}#{lineNumber})"); + [Conditional("VERBOSE")] + internal void OnMessageFaulted(Message msg, Exception fault, [CallerMemberName] string origin = default, [CallerFilePath] string path = default, [CallerLineNumber] int lineNumber = default) => + MessageFaulted?.Invoke(msg?.CommandAndKey, fault, $"{origin} ({path}#{lineNumber})"); - [Conditional("VERBOSE")] - internal void OnInfoMessage(string message) => InfoMessage?.Invoke(message); + [Conditional("VERBOSE")] + internal void OnInfoMessage(string message) => InfoMessage?.Invoke(message); - [Conditional("VERBOSE")] - internal void OnClosing(bool complete) => Closing?.Invoke(complete); + [Conditional("VERBOSE")] + internal void OnClosing(bool complete) => Closing?.Invoke(complete); - [Conditional("VERBOSE")] - internal void OnConnecting(EndPoint endpoint, ConnectionType connectionType) => Connecting?.Invoke(endpoint, connectionType); + [Conditional("VERBOSE")] + internal void OnConnecting(EndPoint endpoint, ConnectionType connectionType) => Connecting?.Invoke(endpoint, connectionType); - [Conditional("VERBOSE")] - internal void OnResurrecting(EndPoint endpoint, ConnectionType connectionType) => Resurrecting.Invoke(endpoint, connectionType); + [Conditional("VERBOSE")] + internal void OnResurrecting(EndPoint endpoint, ConnectionType connectionType) => Resurrecting.Invoke(endpoint, connectionType); - [Conditional("VERBOSE")] - internal void OnPreTransactionExec(Message message) => PreTransactionExec?.Invoke(message.CommandAndKey); + [Conditional("VERBOSE")] + internal void OnPreTransactionExec(Message message) => PreTransactionExec?.Invoke(message.CommandAndKey); - [Conditional("VERBOSE")] - internal void OnTransactionLog(string message) => TransactionLog?.Invoke(message); - } + [Conditional("VERBOSE")] + internal void OnTransactionLog(string message) => TransactionLog?.Invoke(message); } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 071235edc..581cce306 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1,20 +1,15 @@ using System; using System.Collections; using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; using System.IO; -using System.IO.Compression; using System.Linq; using System.Net; -using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using Pipelines.Sockets.Unofficial; -using StackExchange.Redis.Maintenance; using StackExchange.Redis.Profiling; namespace StackExchange.Redis @@ -26,117 +21,61 @@ namespace StackExchange.Redis /// https://stackexchange.github.io/StackExchange.Redis/PipelinesMultiplexers public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplexer // implies : IConnectionMultiplexer and : IDisposable { - [Flags] - private enum FeatureFlags - { - None, - PreventThreadTheft = 1, - } - - private static FeatureFlags s_featureFlags; - - /// - /// Enables or disables a feature flag. - /// This should only be used under support guidance, and should not be rapidly toggled. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Browsable(false)] - public static void SetFeatureFlag(string flag, bool enabled) - { - if (Enum.TryParse(flag, true, out var flags)) - { - if (enabled) s_featureFlags |= flags; - else s_featureFlags &= ~flags; - } - } + internal const int MillisecondsPerHeartbeat = 1000; - static ConnectionMultiplexer() - { - bool value = false; - try - { // attempt to detect a known problem scenario - value = SynchronizationContext.Current?.GetType()?.Name - == "LegacyAspNetSynchronizationContext"; - } - catch { } - SetFeatureFlag(nameof(FeatureFlags.PreventThreadTheft), value); - } + // This gets accessed for every received event; let's make sure we can process it "raw" + internal readonly byte[] ConfigurationChangedChannel; + // Unique identifier used when tracing + internal readonly byte[] UniqueId = Guid.NewGuid().ToByteArray(); /// - /// Returns the state of a feature flag. - /// This should only be used under support guidance. + /// Tracks overall connection multiplexer counts. /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Browsable(false)] - public static bool GetFeatureFlag(string flag) - => Enum.TryParse(flag, true, out var flags) - && (s_featureFlags & flags) == flags; + internal int _connectAttemptCount = 0, _connectCompletedCount = 0, _connectionCloseCount = 0; + private long syncTimeouts, fireAndForgets, asyncTimeouts; + private string failureMessage, activeConfigCause; + private IDisposable pulse; - internal static bool PreventThreadTheft => (s_featureFlags & FeatureFlags.PreventThreadTheft) != 0; + private readonly Hashtable servers = new Hashtable(); + private volatile ServerSnapshot _serverSnapshot = ServerSnapshot.Empty; -#if DEBUG - private static int _collectedWithoutDispose; - internal static int CollectedWithoutDispose => Thread.VolatileRead(ref _collectedWithoutDispose); - /// - /// Invoked by the garbage collector. - /// - ~ConnectionMultiplexer() - { - Interlocked.Increment(ref _collectedWithoutDispose); - } -#endif + private volatile bool _isDisposed; + internal bool IsDisposed => _isDisposed; - bool IInternalConnectionMultiplexer.AllowConnect - { - get => AllowConnect; - set => AllowConnect = value; - } + internal CommandMap CommandMap { get; } + internal ConfigurationOptions RawConfig { get; } + internal ServerSelectionStrategy ServerSelectionStrategy { get; } + internal Exception LastException { get; set; } - bool IInternalConnectionMultiplexer.IgnoreConnect - { - get => IgnoreConnect; - set => IgnoreConnect = value; - } + private int _activeHeartbeatErrors, lastHeartbeatTicks; + internal long LastHeartbeatSecondsAgo => + pulse is null + ? -1 + : unchecked(Environment.TickCount - Thread.VolatileRead(ref lastHeartbeatTicks)) / 1000; - /// - /// For debugging: when not enabled, servers cannot connect. - /// - internal volatile bool AllowConnect = true; + private static int lastGlobalHeartbeatTicks = Environment.TickCount; + internal static long LastGlobalHeartbeatSecondsAgo => + unchecked(Environment.TickCount - Thread.VolatileRead(ref lastGlobalHeartbeatTicks)) / 1000; /// - /// For debugging: when not enabled, end-connect is silently ignored (to simulate a long-running connect). + /// Should exceptions include identifiable details? (key names, additional .Data annotations) /// - internal volatile bool IgnoreConnect; + public bool IncludeDetailInExceptions { get; set; } /// - /// Tracks overall connection multiplexer counts. + /// Should exceptions include performance counter details? (CPU usage, etc - note that this can be problematic on some platforms) /// - internal int _connectAttemptCount = 0, _connectCompletedCount = 0, _connectionCloseCount = 0; + public bool IncludePerformanceCountersInExceptions { get; set; } /// - /// No longer used. + /// Gets the synchronous timeout associated with the connections. /// - [Obsolete("No longer used, will be removed in 3.0.")] - public static TaskFactory Factory - { - get => Task.Factory; - set { } - } + public int TimeoutMilliseconds { get; } /// - /// Get summary statistics associated with all servers in this multiplexer. + /// Gets the asynchronous timeout associated with the connections. /// - public ServerCounters GetCounters() - { - var snapshot = GetServerSnapshot(); - - var counters = new ServerCounters(null); - for (int i = 0; i < snapshot.Length; i++) - { - counters.Add(snapshot[i].GetCounters()); - } - return counters; - } + internal int AsyncTimeoutMilliseconds { get; } /// /// Gets the client-name that will be used on all new connections. @@ -151,195 +90,128 @@ public ServerCounters GetCounters() /// public string Configuration => RawConfig.ToString(); - internal void OnConnectionFailed(EndPoint endpoint, ConnectionType connectionType, ConnectionFailureType failureType, Exception exception, bool reconfigure, string physicalName) + /// + /// Indicates whether any servers are connected. + /// + public bool IsConnected { - if (_isDisposed) return; - var handler = ConnectionFailed; - if (handler != null) - { - CompleteAsWorker(new ConnectionFailedEventArgs(handler, this, endpoint, connectionType, failureType, exception, physicalName)); - } - if (reconfigure) + get { - ReconfigureIfNeeded(endpoint, false, "connection failed"); + var tmp = GetServerSnapshot(); + for (int i = 0; i < tmp.Length; i++) + if (tmp[i].IsConnected) return true; + return false; } } - internal void OnInternalError(Exception exception, EndPoint endpoint = null, ConnectionType connectionType = ConnectionType.None, [CallerMemberName] string origin = null) + /// + /// Indicates whether any servers are currently trying to connect. + /// + public bool IsConnecting { - try - { - if (_isDisposed) return; - Trace("Internal error: " + origin + ", " + exception == null ? "unknown" : exception.Message); - var handler = InternalError; - if (handler != null) - { - CompleteAsWorker(new InternalErrorEventArgs(handler, this, endpoint, connectionType, exception, origin)); - } - } - catch + get { - // Our internal error event failed...whatcha gonna do, exactly? + var tmp = GetServerSnapshot(); + for (int i = 0; i < tmp.Length; i++) + if (tmp[i].IsConnecting) return true; + return false; } } - internal void OnConnectionRestored(EndPoint endpoint, ConnectionType connectionType, string physicalName) + static ConnectionMultiplexer() { - if (_isDisposed) return; - var handler = ConnectionRestored; - if (handler != null) - { - CompleteAsWorker(new ConnectionFailedEventArgs(handler, this, endpoint, connectionType, ConnectionFailureType.None, null, physicalName)); - } - ReconfigureIfNeeded(endpoint, false, "connection restored"); + SetAutodetectFeatureFlags(); } - private void OnEndpointChanged(EndPoint endpoint, EventHandler handler) + private ConnectionMultiplexer(ConfigurationOptions configuration) { - if (_isDisposed) return; - if (handler != null) + IncludeDetailInExceptions = true; + IncludePerformanceCountersInExceptions = false; + + RawConfig = configuration ?? throw new ArgumentNullException(nameof(configuration)); + + var map = CommandMap = configuration.CommandMap; + if (!string.IsNullOrWhiteSpace(configuration.Password)) map.AssertAvailable(RedisCommand.AUTH); + + if (!map.IsAvailable(RedisCommand.ECHO) && !map.IsAvailable(RedisCommand.PING) && !map.IsAvailable(RedisCommand.TIME)) { - CompleteAsWorker(new EndPointEventArgs(handler, this, endpoint)); + // I mean really, give me a CHANCE! I need *something* to check the server is available to me... + // see also: SendTracer (matching logic) + map.AssertAvailable(RedisCommand.EXISTS); } - } - internal void OnConfigurationChanged(EndPoint endpoint) => OnEndpointChanged(endpoint, ConfigurationChanged); - internal void OnConfigurationChangedBroadcast(EndPoint endpoint) => OnEndpointChanged(endpoint, ConfigurationChangedBroadcast); + TimeoutMilliseconds = configuration.SyncTimeout; + AsyncTimeoutMilliseconds = configuration.AsyncTimeout; - /// - /// Raised when a server replied with an error message. - /// - public event EventHandler ErrorMessage; - internal void OnErrorMessage(EndPoint endpoint, string message) - { - if (_isDisposed) return; - var handler = ErrorMessage; - if (handler != null) + OnCreateReaderWriter(configuration); + ServerSelectionStrategy = new ServerSelectionStrategy(this); + + var configChannel = configuration.ConfigurationChannel; + if (!string.IsNullOrWhiteSpace(configChannel)) { - CompleteAsWorker(new RedisErrorEventArgs(handler, this, endpoint, message)); + ConfigurationChangedChannel = Encoding.UTF8.GetBytes(configChannel); } + lastHeartbeatTicks = Environment.TickCount; } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] - private static void Write(ZipArchive zip, string name, Task task, Action callback) + private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions configuration, LogProxy log, out EventHandler connectHandler) { - var entry = zip.CreateEntry(name, CompressionLevel.Optimal); - using (var stream = entry.Open()) - using (var writer = new StreamWriter(stream)) + var muxer = new ConnectionMultiplexer(configuration); + connectHandler = null; + if (log is not null) { - TaskStatus status = task.Status; - switch (status) + // Create a detachable event-handler to log detailed errors if something happens during connect/handshake + connectHandler = (_, a) => { - case TaskStatus.RanToCompletion: - T val = ((Task)task).Result; - callback(val, writer); - break; - case TaskStatus.Faulted: - writer.WriteLine(string.Join(", ", task.Exception.InnerExceptions.Select(x => x.Message))); - break; - default: - writer.WriteLine(status.ToString()); - break; - } + try + { + lock (log.SyncLock) // Keep the outer and any inner errors contiguous + { + var ex = a.Exception; + log?.WriteLine($"Connection failed: {Format.ToString(a.EndPoint)} ({a.ConnectionType}, {a.FailureType}): {ex?.Message ?? "(unknown)"}"); + while ((ex = ex.InnerException) != null) + { + log?.WriteLine($"> {ex.Message}"); + } + } + } + catch { } + }; + muxer.ConnectionFailed += connectHandler; } + return muxer; } + /// - /// Write the configuration of all servers to an output stream. + /// Get summary statistics associated with all servers in this multiplexer. /// - /// The destination stream to write the export to. - /// The options to use for this export. - public void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All) + public ServerCounters GetCounters() { - if (destination == null) throw new ArgumentNullException(nameof(destination)); - - // What is possible, given the command map? - ExportOptions mask = 0; - if (CommandMap.IsAvailable(RedisCommand.INFO)) mask |= ExportOptions.Info; - if (CommandMap.IsAvailable(RedisCommand.CONFIG)) mask |= ExportOptions.Config; - if (CommandMap.IsAvailable(RedisCommand.CLIENT)) mask |= ExportOptions.Client; - if (CommandMap.IsAvailable(RedisCommand.CLUSTER)) mask |= ExportOptions.Cluster; - options &= mask; - - using (var zip = new ZipArchive(destination, ZipArchiveMode.Create, true)) + var counters = new ServerCounters(null); + var snapshot = GetServerSnapshot(); + for (int i = 0; i < snapshot.Length; i++) { - var arr = GetServerSnapshot(); - foreach (var server in arr) - { - const CommandFlags flags = CommandFlags.None; - if (!server.IsConnected) continue; - var api = GetServer(server.EndPoint); - - List tasks = new List(); - if ((options & ExportOptions.Info) != 0) - { - tasks.Add(api.InfoRawAsync(flags: flags)); - } - if ((options & ExportOptions.Config) != 0) - { - tasks.Add(api.ConfigGetAsync(flags: flags)); - } - if ((options & ExportOptions.Client) != 0) - { - tasks.Add(api.ClientListAsync(flags: flags)); - } - if ((options & ExportOptions.Cluster) != 0) - { - tasks.Add(api.ClusterNodesRawAsync(flags: flags)); - } - - WaitAllIgnoreErrors(tasks.ToArray()); - - int index = 0; - var prefix = Format.ToString(server.EndPoint); - if ((options & ExportOptions.Info) != 0) - { - Write(zip, prefix + "/info.txt", tasks[index++], WriteNormalizingLineEndings); - } - if ((options & ExportOptions.Config) != 0) - { - Write[]>(zip, prefix + "/config.txt", tasks[index++], (settings, writer) => - { - foreach (var setting in settings) - { - writer.WriteLine("{0}={1}", setting.Key, setting.Value); - } - }); - } - if ((options & ExportOptions.Client) != 0) - { - Write(zip, prefix + "/clients.txt", tasks[index++], (clients, writer) => - { - if (clients == null) - { - writer.WriteLine(NoContent); - } - else - { - foreach (var client in clients) - { - writer.WriteLine(client.Raw); - } - } - }); - } - if ((options & ExportOptions.Cluster) != 0) - { - Write(zip, prefix + "/nodes.txt", tasks[index++], WriteNormalizingLineEndings); - } - } + counters.Add(snapshot[i].GetCounters()); } + return counters; } internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOptions options, LogProxy log) { + _ = server ?? throw new ArgumentNullException(nameof(server)); + var cmd = server.GetFeatures().ReplicaCommands ? RedisCommand.REPLICAOF : RedisCommand.SLAVEOF; CommandMap.AssertAvailable(cmd); - if (!RawConfig.AllowAdmin) throw ExceptionFactory.AdminModeNotEnabled(IncludeDetailInExceptions, cmd, null, server); - - if (server == null) throw new ArgumentNullException(nameof(server)); + if (!RawConfig.AllowAdmin) + { + throw ExceptionFactory.AdminModeNotEnabled(IncludeDetailInExceptions, cmd, null, server); + } var srv = new RedisServer(this, server, null); - if (!srv.IsConnected) throw ExceptionFactory.NoConnectionAvailable(this, null, server, GetServerSnapshot(), command: cmd); + if (!srv.IsConnected) + { + throw ExceptionFactory.NoConnectionAvailable(this, null, server, GetServerSnapshot(), command: cmd); + } const CommandFlags flags = CommandFlags.NoRedirect; Message msg; @@ -360,7 +232,8 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt RedisKey tieBreakerKey = default(RedisKey); // try and write this everywhere; don't worry if some folks reject our advances - if ((options & ReplicationChangeOptions.SetTiebreaker) != 0 && !string.IsNullOrWhiteSpace(RawConfig.TieBreaker) + if (options.HasFlag(ReplicationChangeOptions.SetTiebreaker) + && !string.IsNullOrWhiteSpace(RawConfig.TieBreaker) && CommandMap.IsAvailable(RedisCommand.SET)) { tieBreakerKey = RawConfig.TieBreaker; @@ -416,7 +289,8 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt // This eliminates the race of pub/sub *then* re-slaving happening, since a method both precedes and follows. async Task BroadcastAsync(ServerEndPoint[] serverNodes) { - if ((options & ReplicationChangeOptions.Broadcast) != 0 && ConfigurationChangedChannel != null + if (options.HasFlag(ReplicationChangeOptions.Broadcast) + && ConfigurationChangedChannel != null && CommandMap.IsAvailable(RedisCommand.PUBLISH)) { RedisValue channel = ConfigurationChangedChannel; @@ -433,7 +307,7 @@ async Task BroadcastAsync(ServerEndPoint[] serverNodes) // Send a message before it happens - because afterwards a new replica may be unresponsive await BroadcastAsync(nodes); - if ((options & ReplicationChangeOptions.ReplicateToOtherEndpoints) != 0) + if (options.HasFlag(ReplicationChangeOptions.ReplicateToOtherEndpoints)) { foreach (var node in nodes) { @@ -467,89 +341,23 @@ async Task BroadcastAsync(ServerEndPoint[] serverNodes) internal void CheckMessage(Message message) { if (!RawConfig.AllowAdmin && message.IsAdmin) - throw ExceptionFactory.AdminModeNotEnabled(IncludeDetailInExceptions, message.Command, message, null); - if (message.Command != RedisCommand.UNKNOWN) CommandMap.AssertAvailable(message.Command); - - // using >= here because we will be adding 1 for the command itself (which is an argument for the purposes of the multi-bulk protocol) - if (message.ArgCount >= PhysicalConnection.REDIS_MAX_ARGS) throw ExceptionFactory.TooManyArgs(message.CommandAndKey, message.ArgCount); - } - private const string NoContent = "(no content)"; - private static void WriteNormalizingLineEndings(string source, StreamWriter writer) - { - if (source == null) { - writer.WriteLine(NoContent); + throw ExceptionFactory.AdminModeNotEnabled(IncludeDetailInExceptions, message.Command, message, null); } - else + if (message.Command != RedisCommand.UNKNOWN) { - using (var reader = new StringReader(source)) - { - string line; - while ((line = reader.ReadLine()) != null) - writer.WriteLine(line); // normalize line endings - } + CommandMap.AssertAvailable(message.Command); } - } - - /// - /// Raised whenever a physical connection fails. - /// - public event EventHandler ConnectionFailed; - - /// - /// Raised whenever an internal error occurs (this is primarily for debugging). - /// - public event EventHandler InternalError; - - /// - /// Raised whenever a physical connection is established. - /// - public event EventHandler ConnectionRestored; - - /// - /// Raised when configuration changes are detected. - /// - public event EventHandler ConfigurationChanged; - - /// - /// Raised when nodes are explicitly requested to reconfigure via broadcast. - /// This usually means primary/replica changes. - /// - public event EventHandler ConfigurationChangedBroadcast; - - /// - /// Raised when server indicates a maintenance event is going to happen. - /// - public event EventHandler ServerMaintenanceEvent; - - /// - /// Gets the synchronous timeout associated with the connections. - /// - public int TimeoutMilliseconds { get; } - - /// - /// Gets the asynchronous timeout associated with the connections. - /// - internal int AsyncTimeoutMilliseconds { get; } - /// - /// Gets all endpoints defined on the multiplexer. - /// - /// Whether to get only the endpoints specified explicitly in the config. - public EndPoint[] GetEndPoints(bool configuredOnly = false) - { - if (configuredOnly) return RawConfig.EndPoints.ToArray(); - - return _serverSnapshot.GetEndPoints(); + // using >= here because we will be adding 1 for the command itself (which is an argument for the purposes of the multi-bulk protocol) + if (message.ArgCount >= PhysicalConnection.REDIS_MAX_ARGS) + { + throw ExceptionFactory.TooManyArgs(message.CommandAndKey, message.ArgCount); + } } - internal void InvokeServerMaintenanceEvent(ServerMaintenanceEvent e) - => ServerMaintenanceEvent?.Invoke(this, e); - - internal bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isMoved) - { - return ServerSelectionStrategy.TryResend(hashSlot, message, endpoint, isMoved); - } + internal bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isMoved) => + ServerSelectionStrategy.TryResend(hashSlot, message, endpoint, isMoved); /// /// Wait for a given asynchronous operation to complete (or timeout). @@ -557,10 +365,13 @@ internal bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool i /// The task to wait on. public void Wait(Task task) { - if (task == null) throw new ArgumentNullException(nameof(task)); + _ = task ?? throw new ArgumentNullException(nameof(task)); try { - if (!task.Wait(TimeoutMilliseconds)) throw new TimeoutException(); + if (!task.Wait(TimeoutMilliseconds)) + { + throw new TimeoutException(); + } } catch (AggregateException aex) when (IsSingle(aex)) { @@ -575,10 +386,13 @@ public void Wait(Task task) /// The task to wait on. public T Wait(Task task) { - if (task == null) throw new ArgumentNullException(nameof(task)); + _ = task ?? throw new ArgumentNullException(nameof(task)); try { - if (!task.Wait(TimeoutMilliseconds)) throw new TimeoutException(); + if (!task.Wait(TimeoutMilliseconds)) + { + throw new TimeoutException(); + } } catch (AggregateException aex) when (IsSingle(aex)) { @@ -589,8 +403,14 @@ public T Wait(Task task) private static bool IsSingle(AggregateException aex) { - try { return aex != null && aex.InnerExceptions.Count == 1; } - catch { return false; } + try + { + return aex?.InnerExceptions.Count == 1; + } + catch + { + return false; + } } /// @@ -599,22 +419,23 @@ private static bool IsSingle(AggregateException aex) /// The tasks to wait on. public void WaitAll(params Task[] tasks) { - if (tasks == null) throw new ArgumentNullException(nameof(tasks)); + _ = tasks ?? throw new ArgumentNullException(nameof(tasks)); if (tasks.Length == 0) return; - if (!Task.WaitAll(tasks, TimeoutMilliseconds)) throw new TimeoutException(); - } - - private bool WaitAllIgnoreErrors(Task[] tasks) => WaitAllIgnoreErrors(tasks, TimeoutMilliseconds); + if (!Task.WaitAll(tasks, TimeoutMilliseconds)) + { + throw new TimeoutException(); + } + } - private static bool WaitAllIgnoreErrors(Task[] tasks, int timeout) + private bool WaitAllIgnoreErrors(Task[] tasks) { - if (tasks == null) throw new ArgumentNullException(nameof(tasks)); + _ = tasks ?? throw new ArgumentNullException(nameof(tasks)); if (tasks.Length == 0) return true; var watch = ValueStopwatch.StartNew(); try { // If no error, great - if (Task.WaitAll(tasks, timeout)) return true; + if (Task.WaitAll(tasks, TimeoutMilliseconds)) return true; } catch { } @@ -624,7 +445,7 @@ private static bool WaitAllIgnoreErrors(Task[] tasks, int timeout) var task = tasks[i]; if (!task.IsCanceled && !task.IsCompleted && !task.IsFaulted) { - var remaining = timeout - watch.ElapsedMilliseconds; + var remaining = TimeoutMilliseconds - watch.ElapsedMilliseconds; if (remaining <= 0) return false; try { @@ -637,52 +458,37 @@ private static bool WaitAllIgnoreErrors(Task[] tasks, int timeout) return false; } - internal bool AuthSuspect { get; private set; } - internal void SetAuthSuspect() => AuthSuspect = true; - - private static void LogWithThreadPoolStats(LogProxy log, string message, out int busyWorkerCount) - { - busyWorkerCount = 0; - if (log != null) - { - var sb = new StringBuilder(); - sb.Append(message); - busyWorkerCount = PerfCounterHelper.GetThreadPoolStats(out string iocp, out string worker, out string workItems); - sb.Append(", IOCP: ").Append(iocp).Append(", WORKER: ").Append(worker); - if (workItems != null) - { - sb.Append(", POOL: ").Append(workItems); - } - log?.WriteLine(sb.ToString()); - } - } - - private static bool AllComplete(Task[] tasks) - { - for (int i = 0; i < tasks.Length; i++) - { - var task = tasks[i]; - if (!task.IsCanceled && !task.IsCompleted && !task.IsFaulted) - return false; - } - return true; - } - private static async Task WaitAllIgnoreErrorsAsync(string name, Task[] tasks, int timeoutMilliseconds, LogProxy log, [CallerMemberName] string caller = null, [CallerLineNumber] int callerLineNumber = 0) { - if (tasks == null) throw new ArgumentNullException(nameof(tasks)); + _ = tasks ?? throw new ArgumentNullException(nameof(tasks)); if (tasks.Length == 0) { log?.WriteLine("No tasks to await"); return true; } - if (AllComplete(tasks)) { log?.WriteLine("All tasks are already complete"); return true; } + static void LogWithThreadPoolStats(LogProxy log, string message, out int busyWorkerCount) + { + busyWorkerCount = 0; + if (log != null) + { + var sb = new StringBuilder(); + sb.Append(message); + busyWorkerCount = PerfCounterHelper.GetThreadPoolStats(out string iocp, out string worker, out string workItems); + sb.Append(", IOCP: ").Append(iocp).Append(", WORKER: ").Append(worker); + if (workItems != null) + { + sb.Append(", POOL: ").Append(workItems); + } + log?.WriteLine(sb.ToString()); + } + } + var watch = ValueStopwatch.StartNew(); LogWithThreadPoolStats(log, $"Awaiting {tasks.Length} {name} task completion(s) for {timeoutMilliseconds}ms", out _); try @@ -728,67 +534,19 @@ private static async Task WaitAllIgnoreErrorsAsync(string name, Task[] tas return false; } - /// - /// Raised when a hash-slot has been relocated. - /// - public event EventHandler HashSlotMoved; - - internal void OnHashSlotMoved(int hashSlot, EndPoint old, EndPoint @new) - { - var handler = HashSlotMoved; - if (handler != null) - { - CompleteAsWorker(new HashSlotMovedEventArgs(handler, this, hashSlot, old, @new)); - } - } - - /// - /// Compute the hash-slot of a specified key. - /// - /// The key to get a hash slot ID for. - public int HashSlot(RedisKey key) => ServerSelectionStrategy.HashSlot(key); - - internal ServerEndPoint AnyServer(ServerType serverType, uint startOffset, RedisCommand command, CommandFlags flags, bool allowDisconnected) + private static bool AllComplete(Task[] tasks) { - var tmp = GetServerSnapshot(); - int len = tmp.Length; - ServerEndPoint fallback = null; - for (int i = 0; i < len; i++) + for (int i = 0; i < tasks.Length; i++) { - var server = tmp[(int)(((uint)i + startOffset) % len)]; - if (server != null && server.ServerType == serverType && server.IsSelectable(command, allowDisconnected)) - { - if (server.IsReplica) - { - switch (flags) - { - case CommandFlags.DemandReplica: - case CommandFlags.PreferReplica: - return server; - case CommandFlags.PreferMaster: - fallback = server; - break; - } - } - else - { - switch (flags) - { - case CommandFlags.DemandMaster: - case CommandFlags.PreferMaster: - return server; - case CommandFlags.PreferReplica: - fallback = server; - break; - } - } - } + var task = tasks[i]; + if (!task.IsCanceled && !task.IsCompleted && !task.IsFaulted) + return false; } - return fallback; + return true; } - private volatile bool _isDisposed; - internal bool IsDisposed => _isDisposed; + internal bool AuthSuspect { get; private set; } + internal void SetAuthSuspect() => AuthSuspect = true; /// /// Creates a new instance. @@ -817,10 +575,9 @@ public static Task ConnectAsync(ConfigurationOptions conf { SocketConnection.AssertDependencies(); - if (IsSentinel(configuration)) - return SentinelPrimaryConnectAsync(configuration, log); - - return ConnectImplAsync(PrepareConfig(configuration), log); + return configuration?.IsSentinel == true + ? SentinelPrimaryConnectAsync(configuration, log) + : ConnectImplAsync(PrepareConfig(configuration), log); } private static async Task ConnectImplAsync(ConfigurationOptions configuration, TextWriter log = null) @@ -828,104 +585,51 @@ private static async Task ConnectImplAsync(ConfigurationO IDisposable killMe = null; EventHandler connectHandler = null; ConnectionMultiplexer muxer = null; - using (var logProxy = LogProxy.TryCreate(log)) + using var logProxy = LogProxy.TryCreate(log); + try { - try - { - var sw = ValueStopwatch.StartNew(); - logProxy?.WriteLine($"Connecting (async) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); - - muxer = CreateMultiplexer(configuration, logProxy, out connectHandler); - killMe = muxer; - Interlocked.Increment(ref muxer._connectAttemptCount); - bool configured = await muxer.ReconfigureAsync(first: true, reconfigureAll: false, logProxy, null, "connect").ObserveErrors().ForAwait(); - if (!configured) - { - throw ExceptionFactory.UnableToConnect(muxer, muxer.failureMessage); - } - killMe = null; - Interlocked.Increment(ref muxer._connectCompletedCount); + var sw = ValueStopwatch.StartNew(); + logProxy?.WriteLine($"Connecting (async) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); - if (muxer.ServerSelectionStrategy.ServerType == ServerType.Sentinel) - { - // Initialize the Sentinel handlers - muxer.InitializeSentinel(logProxy); - } - - await configuration.AfterConnectAsync(muxer, logProxy != null ? logProxy.WriteLine : LogProxy.NullWriter).ForAwait(); - - logProxy?.WriteLine($"Total connect time: {sw.ElapsedMilliseconds:n0} ms"); - - return muxer; + muxer = CreateMultiplexer(configuration, logProxy, out connectHandler); + killMe = muxer; + Interlocked.Increment(ref muxer._connectAttemptCount); + bool configured = await muxer.ReconfigureAsync(first: true, reconfigureAll: false, logProxy, null, "connect").ObserveErrors().ForAwait(); + if (!configured) + { + throw ExceptionFactory.UnableToConnect(muxer, muxer.failureMessage); } - finally + killMe = null; + Interlocked.Increment(ref muxer._connectCompletedCount); + + if (muxer.ServerSelectionStrategy.ServerType == ServerType.Sentinel) { - if (connectHandler != null) muxer.ConnectionFailed -= connectHandler; - if (killMe != null) try { killMe.Dispose(); } catch { } + // Initialize the Sentinel handlers + muxer.InitializeSentinel(logProxy); } - } - } - private static bool IsSentinel(ConfigurationOptions configuration) - { - return !string.IsNullOrEmpty(configuration?.ServiceName); - } + await configuration.AfterConnectAsync(muxer, logProxy != null ? logProxy.WriteLine : LogProxy.NullWriter).ForAwait(); - internal static ConfigurationOptions PrepareConfig(object configuration, bool sentinel = false) - { - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - ConfigurationOptions config; - if (configuration is string s) - { - config = ConfigurationOptions.Parse(s); - } - else if (configuration is ConfigurationOptions configurationOptions) - { - config = (configurationOptions).Clone(); - } - else - { - throw new ArgumentException("Invalid configuration object", nameof(configuration)); - } - if (config.EndPoints.Count == 0) throw new ArgumentException("No endpoints specified", nameof(configuration)); + logProxy?.WriteLine($"Total connect time: {sw.ElapsedMilliseconds:n0} ms"); - if (sentinel) + return muxer; + } + finally { - config.SetSentinelDefaults(); - - return config; + if (connectHandler != null) muxer.ConnectionFailed -= connectHandler; + if (killMe != null) try { killMe.Dispose(); } catch { } } - - config.SetDefaultPorts(); - - return config; } - private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions configuration, LogProxy log, out EventHandler connectHandler) + + internal static ConfigurationOptions PrepareConfig(ConfigurationOptions config, bool sentinel = false) { - var muxer = new ConnectionMultiplexer(configuration); - connectHandler = null; - if (log != null) + _ = config ?? throw new ArgumentNullException(nameof(config)); + if (config.EndPoints.Count == 0) { - // Create a detachable event-handler to log detailed errors if something happens during connect/handshake - connectHandler = (_, a) => - { - try - { - lock (log.SyncLock) // Keep the outer and any inner errors contiguous - { - var ex = a.Exception; - log?.WriteLine($"Connection failed: {Format.ToString(a.EndPoint)} ({a.ConnectionType}, {a.FailureType}): {ex?.Message ?? "(unknown)"}"); - while ((ex = ex.InnerException) != null) - { - log?.WriteLine($"> {ex.Message}"); - } - } - } - catch { } - }; - muxer.ConnectionFailed += connectHandler; + throw new ArgumentException("No endpoints specified", nameof(config)); } - return muxer; + + return config.Clone().WithDefaults(sentinel); } /// @@ -955,112 +659,9 @@ public static ConnectionMultiplexer Connect(ConfigurationOptions configuration, { SocketConnection.AssertDependencies(); - if (IsSentinel(configuration)) - { - return SentinelPrimaryConnect(configuration, log); - } - - return ConnectImpl(PrepareConfig(configuration), log); - } - - /// - /// Create a new instance that connects to a Sentinel server. - /// - /// The string configuration to use for this multiplexer. - /// The to log to. - public static ConnectionMultiplexer SentinelConnect(string configuration, TextWriter log = null) - { - SocketConnection.AssertDependencies(); - return ConnectImpl(PrepareConfig(configuration, sentinel: true), log); - } - - /// - /// Create a new instance that connects to a Sentinel server. - /// - /// The string configuration to use for this multiplexer. - /// The to log to. - public static Task SentinelConnectAsync(string configuration, TextWriter log = null) - { - SocketConnection.AssertDependencies(); - return ConnectImplAsync(PrepareConfig(configuration, sentinel: true), log); - } - - /// - /// Create a new instance that connects to a Sentinel server. - /// - /// The configuration options to use for this multiplexer. - /// The to log to. - public static ConnectionMultiplexer SentinelConnect(ConfigurationOptions configuration, TextWriter log = null) - { - SocketConnection.AssertDependencies(); - return ConnectImpl(PrepareConfig(configuration, sentinel: true), log); - } - - /// - /// Create a new instance that connects to a Sentinel server. - /// - /// The configuration options to use for this multiplexer. - /// The to log to. - public static Task SentinelConnectAsync(ConfigurationOptions configuration, TextWriter log = null) - { - SocketConnection.AssertDependencies(); - return ConnectImplAsync(PrepareConfig(configuration, sentinel: true), log); - } - - /// - /// Create a new instance that connects to a sentinel server, discovers the current primary server - /// for the specified in the config and returns a managed connection to the current primary server. - /// - /// The string configuration to use for this multiplexer. - /// The to log to. - private static ConnectionMultiplexer SentinelPrimaryConnect(string configuration, TextWriter log = null) - { - return SentinelPrimaryConnect(PrepareConfig(configuration, sentinel: true), log); - } - - /// - /// Create a new instance that connects to a sentinel server, discovers the current primary server - /// for the specified in the config and returns a managed connection to the current primary server. - /// - /// The configuration options to use for this multiplexer. - /// The to log to. - private static ConnectionMultiplexer SentinelPrimaryConnect(ConfigurationOptions configuration, TextWriter log = null) - { - var sentinelConnection = SentinelConnect(configuration, log); - - var muxer = sentinelConnection.GetSentinelMasterConnection(configuration, log); - // Set reference to sentinel connection so that we can dispose it - muxer.sentinelConnection = sentinelConnection; - - return muxer; - } - - /// - /// Create a new instance that connects to a sentinel server, discovers the current primary server - /// for the specified in the config and returns a managed connection to the current primary server. - /// - /// The string configuration to use for this multiplexer. - /// The to log to. - private static Task SentinelPrimaryConnectAsync(string configuration, TextWriter log = null) - { - return SentinelPrimaryConnectAsync(PrepareConfig(configuration, sentinel: true), log); - } - - /// - /// Create a new instance that connects to a sentinel server, discovers the current primary server - /// for the specified in the config and returns a managed connection to the current primary server. - /// - /// The configuration options to use for this multiplexer. - /// The to log to. - private static async Task SentinelPrimaryConnectAsync(ConfigurationOptions configuration, TextWriter log = null) - { - var sentinelConnection = await SentinelConnectAsync(configuration, log).ForAwait(); - - var muxer = sentinelConnection.GetSentinelMasterConnection(configuration, log); - // Set reference to sentinel connection so that we can dispose it - muxer.sentinelConnection = sentinelConnection; - - return muxer; + return configuration?.IsSentinel == true + ? SentinelPrimaryConnect(configuration, log) + : ConnectImpl(PrepareConfig(configuration), log); } private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configuration, TextWriter log) @@ -1068,60 +669,54 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat IDisposable killMe = null; EventHandler connectHandler = null; ConnectionMultiplexer muxer = null; - using (var logProxy = LogProxy.TryCreate(log)) + using var logProxy = LogProxy.TryCreate(log); + try { - try - { - var sw = ValueStopwatch.StartNew(); - logProxy?.WriteLine($"Connecting (sync) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); + var sw = ValueStopwatch.StartNew(); + logProxy?.WriteLine($"Connecting (sync) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); - muxer = CreateMultiplexer(configuration, logProxy, out connectHandler); - killMe = muxer; - Interlocked.Increment(ref muxer._connectAttemptCount); - // note that task has timeouts internally, so it might take *just over* the regular timeout - var task = muxer.ReconfigureAsync(first: true, reconfigureAll: false, logProxy, null, "connect"); + muxer = CreateMultiplexer(configuration, logProxy, out connectHandler); + killMe = muxer; + Interlocked.Increment(ref muxer._connectAttemptCount); + // note that task has timeouts internally, so it might take *just over* the regular timeout + var task = muxer.ReconfigureAsync(first: true, reconfigureAll: false, logProxy, null, "connect"); - if (!task.Wait(muxer.SyncConnectTimeout(true))) + if (!task.Wait(muxer.SyncConnectTimeout(true))) + { + task.ObserveErrors(); + if (muxer.RawConfig.AbortOnConnectFail) { - task.ObserveErrors(); - if (muxer.RawConfig.AbortOnConnectFail) - { - throw ExceptionFactory.UnableToConnect(muxer, "ConnectTimeout"); - } - else - { - muxer.LastException = ExceptionFactory.UnableToConnect(muxer, "ConnectTimeout"); - } + throw ExceptionFactory.UnableToConnect(muxer, "ConnectTimeout"); } - - if (!task.Result) throw ExceptionFactory.UnableToConnect(muxer, muxer.failureMessage); - killMe = null; - Interlocked.Increment(ref muxer._connectCompletedCount); - - if (muxer.ServerSelectionStrategy.ServerType == ServerType.Sentinel) + else { - // Initialize the Sentinel handlers - muxer.InitializeSentinel(logProxy); + muxer.LastException = ExceptionFactory.UnableToConnect(muxer, "ConnectTimeout"); } + } - configuration.AfterConnectAsync(muxer, logProxy != null ? logProxy.WriteLine : LogProxy.NullWriter).Wait(muxer.SyncConnectTimeout(true)); - - logProxy?.WriteLine($"Total connect time: {sw.ElapsedMilliseconds:n0} ms"); + if (!task.Result) throw ExceptionFactory.UnableToConnect(muxer, muxer.failureMessage); + killMe = null; + Interlocked.Increment(ref muxer._connectCompletedCount); - return muxer; - } - finally + if (muxer.ServerSelectionStrategy.ServerType == ServerType.Sentinel) { - if (connectHandler != null && muxer != null) muxer.ConnectionFailed -= connectHandler; - if (killMe != null) try { killMe.Dispose(); } catch { } + // Initialize the Sentinel handlers + muxer.InitializeSentinel(logProxy); } + + configuration.AfterConnectAsync(muxer, logProxy != null ? logProxy.WriteLine : LogProxy.NullWriter).Wait(muxer.SyncConnectTimeout(true)); + + logProxy?.WriteLine($"Total connect time: {sw.ElapsedMilliseconds:n0} ms"); + + return muxer; + } + finally + { + if (connectHandler != null && muxer != null) muxer.ConnectionFailed -= connectHandler; + if (killMe != null) try { killMe.Dispose(); } catch { } } } - private string failureMessage; - private readonly Hashtable servers = new Hashtable(); - private volatile ServerSnapshot _serverSnapshot = ServerSnapshot.Empty; - ReadOnlySpan IInternalConnectionMultiplexer.GetServerSnapshot() => GetServerSnapshot(); internal ReadOnlySpan GetServerSnapshot() => _serverSnapshot.Span; private sealed class ServerSnapshot @@ -1138,7 +733,10 @@ private ServerSnapshot(ServerEndPoint[] arr, int count) internal ServerSnapshot Add(ServerEndPoint value) { - if (value == null) return this; + if (value == null) + { + return this; + } ServerEndPoint[] arr; if (_arr.Length > _count) @@ -1172,9 +770,11 @@ internal EndPoint[] GetEndPoints() internal ServerEndPoint GetServerEndPoint(EndPoint endpoint, LogProxy log = null, bool activate = true) { - if (endpoint == null) return null; - var server = (ServerEndPoint)servers[endpoint]; - if (server == null) + if (endpoint == null) + { + return null; + } + if (servers[endpoint] is not ServerEndPoint server) { bool isNew = false; lock (servers) @@ -1182,8 +782,10 @@ internal ServerEndPoint GetServerEndPoint(EndPoint endpoint, LogProxy log = null server = (ServerEndPoint)servers[endpoint]; if (server == null) { - if (_isDisposed) throw new ObjectDisposedException(ToString()); - + if (_isDisposed) + { + throw new ObjectDisposedException(ToString()); + } server = new ServerEndPoint(this, endpoint); servers.Add(endpoint, server); isNew = true; @@ -1196,42 +798,7 @@ internal ServerEndPoint GetServerEndPoint(EndPoint endpoint, LogProxy log = null return server; } - internal readonly CommandMap CommandMap; - - private ConnectionMultiplexer(ConfigurationOptions configuration) - { - IncludeDetailInExceptions = true; - IncludePerformanceCountersInExceptions = false; - - RawConfig = configuration ?? throw new ArgumentNullException(nameof(configuration)); - - var map = CommandMap = configuration.CommandMap; - if (!string.IsNullOrWhiteSpace(configuration.Password)) map.AssertAvailable(RedisCommand.AUTH); - - if (!map.IsAvailable(RedisCommand.ECHO) && !map.IsAvailable(RedisCommand.PING) && !map.IsAvailable(RedisCommand.TIME)) - { // I mean really, give me a CHANCE! I need *something* to check the server is available to me... - // see also: SendTracer (matching logic) - map.AssertAvailable(RedisCommand.EXISTS); - } - - TimeoutMilliseconds = configuration.SyncTimeout; - AsyncTimeoutMilliseconds = configuration.AsyncTimeout; - - OnCreateReaderWriter(configuration); - ServerSelectionStrategy = new ServerSelectionStrategy(this); - - var configChannel = configuration.ConfigurationChannel; - if (!string.IsNullOrWhiteSpace(configChannel)) - { - ConfigurationChangedChannel = Encoding.UTF8.GetBytes(configChannel); - } - lastHeartbeatTicks = Environment.TickCount; - } - - partial void OnCreateReaderWriter(ConfigurationOptions configuration); - - internal const int MillisecondsPerHeartbeat = 1000; - private sealed class TimerToken + private sealed class TimerToken { public TimerToken(ConnectionMultiplexer muxer) { @@ -1267,7 +834,6 @@ internal static IDisposable Create(ConnectionMultiplexer connection) } } - private int _activeHeartbeatErrors; private void OnHeartbeat() { try @@ -1297,21 +863,6 @@ private void OnHeartbeat() } } - private int lastHeartbeatTicks; - private static int lastGlobalHeartbeatTicks = Environment.TickCount; - internal long LastHeartbeatSecondsAgo - { - get - { - if (pulse == null) return -1; - return unchecked(Environment.TickCount - Thread.VolatileRead(ref lastHeartbeatTicks)) / 1000; - } - } - - internal Exception LastException { get; set; } - - internal static long LastGlobalHeartbeatSecondsAgo => unchecked(Environment.TickCount - Thread.VolatileRead(ref lastGlobalHeartbeatTicks)) / 1000; - /// /// Obtain a pub/sub subscriber connection to the specified server. /// @@ -1379,20 +930,67 @@ public IDatabase GetDatabase(int db = -1, object asyncState = null) return arr[db - 1] ??= new RedisDatabase(this, db, null); } + /// + /// Compute the hash-slot of a specified key. + /// + /// The key to get a hash slot ID for. + public int HashSlot(RedisKey key) => ServerSelectionStrategy.HashSlot(key); + + internal ServerEndPoint AnyServer(ServerType serverType, uint startOffset, RedisCommand command, CommandFlags flags, bool allowDisconnected) + { + var tmp = GetServerSnapshot(); + int len = tmp.Length; + ServerEndPoint fallback = null; + for (int i = 0; i < len; i++) + { + var server = tmp[(int)(((uint)i + startOffset) % len)]; + if (server != null && server.ServerType == serverType && server.IsSelectable(command, allowDisconnected)) + { + if (server.IsReplica) + { + switch (flags) + { + case CommandFlags.DemandReplica: + case CommandFlags.PreferReplica: + return server; + case CommandFlags.PreferMaster: + fallback = server; + break; + } + } + else + { + switch (flags) + { + case CommandFlags.DemandMaster: + case CommandFlags.PreferMaster: + return server; + case CommandFlags.PreferReplica: + fallback = server; + break; + } + } + } + } + return fallback; + } + /// /// Obtain a configuration API for an individual server. /// /// The host to get a server for. /// The port for to get a server for. /// The async state to pass into the resulting . - public IServer GetServer(string host, int port, object asyncState = null) => GetServer(Format.ParseEndPoint(host, port), asyncState); + public IServer GetServer(string host, int port, object asyncState = null) => + GetServer(Format.ParseEndPoint(host, port), asyncState); /// /// Obtain a configuration API for an individual server. /// /// The "host:port" string to get a server for. /// The async state to pass into the resulting . - public IServer GetServer(string hostAndPort, object asyncState = null) => GetServer(Format.TryParseEndPoint(hostAndPort), asyncState); + public IServer GetServer(string hostAndPort, object asyncState = null) => + GetServer(Format.TryParseEndPoint(hostAndPort), asyncState); /// /// Obtain a configuration API for an individual server. @@ -1408,16 +1006,22 @@ public IDatabase GetDatabase(int db = -1, object asyncState = null) /// The async state to pass into the resulting . public IServer GetServer(EndPoint endpoint, object asyncState = null) { - if (endpoint == null) throw new ArgumentNullException(nameof(endpoint)); + _ = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); if (!RawConfig.Proxy.SupportsServerApi()) { throw new NotSupportedException($"The server API is not available via {RawConfig.Proxy}"); } - var server = (ServerEndPoint)servers[endpoint]; - if (server == null) throw new ArgumentException("The specified endpoint is not defined", nameof(endpoint)); + var server = servers[endpoint] as ServerEndPoint ?? throw new ArgumentException("The specified endpoint is not defined", nameof(endpoint)); return new RedisServer(this, server, asyncState); } + /// + /// Get the hash-slot associated with a given key, if applicable. + /// This can be useful for grouping operations. + /// + /// The to determine the hash slot for. + public int GetHashSlot(RedisKey key) => ServerSelectionStrategy.HashSlot(key); + /// /// The number of operations that have been performed on all connections. /// @@ -1432,67 +1036,40 @@ public long OperationCount } } - private string activeConfigCause; - - internal bool ReconfigureIfNeeded(EndPoint blame, bool fromBroadcast, string cause, bool publishReconfigure = false, CommandFlags flags = CommandFlags.None) - { - if (fromBroadcast) - { - OnConfigurationChangedBroadcast(blame); - } - string activeCause = Volatile.Read(ref activeConfigCause); - if (activeCause == null) - { - bool reconfigureAll = fromBroadcast || publishReconfigure; - Trace("Configuration change detected; checking nodes", "Configuration"); - ReconfigureAsync(first: false, reconfigureAll, null, blame, cause, publishReconfigure, flags).ObserveErrors(); - return true; - } - else - { - Trace("Configuration change skipped; already in progress via " + activeCause, "Configuration"); - return false; - } - } - /// /// Reconfigure the current connections based on the existing configuration. /// /// The to log to. - public async Task ConfigureAsync(TextWriter log = null) + public bool Configure(TextWriter log = null) { - using (var logProxy = LogProxy.TryCreate(log)) + // Note we expect ReconfigureAsync to internally allow [n] duration, + // so to avoid near misses, here we wait 2*[n]. + using var logProxy = LogProxy.TryCreate(log); + var task = ReconfigureAsync(first: false, reconfigureAll: true, logProxy, null, "configure"); + if (!task.Wait(SyncConnectTimeout(false))) { - return await ReconfigureAsync(first: false, reconfigureAll: true, logProxy, null, "configure").ObserveErrors(); + task.ObserveErrors(); + if (RawConfig.AbortOnConnectFail) + { + throw new TimeoutException(); + } + else + { + LastException = new TimeoutException("ConnectTimeout"); + } + return false; } + return task.Result; } /// /// Reconfigure the current connections based on the existing configuration. /// /// The to log to. - public bool Configure(TextWriter log = null) + public async Task ConfigureAsync(TextWriter log = null) { - // Note we expect ReconfigureAsync to internally allow [n] duration, - // so to avoid near misses, here we wait 2*[n]. - using (var logProxy = LogProxy.TryCreate(log)) - { - var task = ReconfigureAsync(first: false, reconfigureAll: true, logProxy, null, "configure"); - if (!task.Wait(SyncConnectTimeout(false))) - { - task.ObserveErrors(); - if (RawConfig.AbortOnConnectFail) - { - throw new TimeoutException(); - } - else - { - LastException = new TimeoutException("ConnectTimeout"); - } - return false; - } - return task.Result; - } + using var logProxy = LogProxy.TryCreate(log); + return await ReconfigureAsync(first: false, reconfigureAll: true, logProxy, null, "configure").ObserveErrors(); } internal int SyncConnectTimeout(bool forConnect) @@ -1513,11 +1090,9 @@ internal int SyncConnectTimeout(bool forConnect) /// public string GetStatus() { - using (var sw = new StringWriter()) - { - GetStatus(sw); - return sw.ToString(); - } + using var sw = new StringWriter(); + GetStatus(sw); + return sw.ToString(); } /// @@ -1526,10 +1101,8 @@ public string GetStatus() /// The to log to. public void GetStatus(TextWriter log) { - using (var proxy = LogProxy.TryCreate(log)) - { - GetStatus(proxy); - } + using var proxy = LogProxy.TryCreate(log); + GetStatus(proxy); } internal void GetStatus(LogProxy log) @@ -1560,10 +1133,30 @@ private void ActivateAllServers(LogProxy log) } } + internal bool ReconfigureIfNeeded(EndPoint blame, bool fromBroadcast, string cause, bool publishReconfigure = false, CommandFlags flags = CommandFlags.None) + { + if (fromBroadcast) + { + OnConfigurationChangedBroadcast(blame); + } + string activeCause = Volatile.Read(ref activeConfigCause); + if (activeCause == null) + { + bool reconfigureAll = fromBroadcast || publishReconfigure; + Trace("Configuration change detected; checking nodes", "Configuration"); + ReconfigureAsync(first: false, reconfigureAll, null, blame, cause, publishReconfigure, flags).ObserveErrors(); + return true; + } + else + { + Trace("Configuration change skipped; already in progress via " + activeCause, "Configuration"); + return false; + } + } + /// /// Triggers a reconfigure of this multiplexer. /// This re-assessment of all server endpoints to get the current topology and adjust, the same as if we had first connected. - /// TODO: Naming? /// public Task ReconfigureAsync(string reason) => ReconfigureAsync(first: false, reconfigureAll: false, log: null, blame: null, cause: reason); @@ -1592,9 +1185,9 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP if (first) { - if (RawConfig.ResolveDns && RawConfig.HasDnsEndPoints()) + if (RawConfig.ResolveDns && RawConfig.EndPoints.HasDnsEndPoints()) { - var dns = RawConfig.ResolveEndPointsAsync(this, log).ObserveErrors(); + var dns = RawConfig.EndPoints.ResolveEndPointsAsync(this, log).ObserveErrors(); if (!await dns.TimeoutAfter(TimeoutMilliseconds).ForAwait()) { throw new TimeoutException("Timeout resolving endpoints"); @@ -1894,13 +1487,21 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP finally { Trace("Exiting reconfiguration..."); - OnTraceLog(log); if (ranThisCall) Interlocked.Exchange(ref activeConfigCause, null); if (!first) OnConfigurationChanged(blame); Trace("Reconfiguration exited"); } } + /// + /// Gets all endpoints defined on the multiplexer. + /// + /// Whether to get only the endpoints specified explicitly in the config. + public EndPoint[] GetEndPoints(bool configuredOnly = false) => + configuredOnly + ? RawConfig.EndPoints.ToArray() + : _serverSnapshot.GetEndPoints(); + private async Task GetEndpointsFromClusterNodes(ServerEndPoint server, LogProxy log) { var message = Message.Create(-1, CommandFlags.None, RedisCommand.CLUSTER, RedisLiterals.NODES); @@ -1936,10 +1537,6 @@ private void ResetAllNonConnected() } } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Used - it's a partial")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Partial - may use instance data")] - partial void OnTraceLog(LogProxy log, [CallerMemberName] string caller = null); - private static ServerEndPoint NominatePreferredPrimary(LogProxy log, ServerEndPoint[] servers, bool useTieBreakers, List primaries) { log?.WriteLine("Election summary:"); @@ -2081,20 +1678,23 @@ private static string DeDotifyHost(string input) internal void UpdateClusterRange(ClusterConfiguration configuration) { - if (configuration == null) return; + if (configuration is null) + { + return; + } foreach (var node in configuration.Nodes) { if (node.IsReplica || node.Slots.Count == 0) continue; foreach (var slot in node.Slots) { - var server = GetServerEndPoint(node.EndPoint); - if (server != null) ServerSelectionStrategy.UpdateClusterRange(slot.From, slot.To, server); + if (GetServerEndPoint(node.EndPoint) is ServerEndPoint server) + { + ServerSelectionStrategy.UpdateClusterRange(slot.From, slot.To, server); + } } } } - private IDisposable pulse; - internal ServerEndPoint SelectServer(Message message) => message == null ? null : ServerSelectionStrategy.Select(message); @@ -2167,6 +1767,7 @@ private bool PrepareToPushMessageToBridge(Message message, ResultProcessor Trace("No server or server unavailable - aborting: " + message); return false; } + private ValueTask TryPushMessageToBridgeAsync(Message message, ResultProcessor processor, IResultBox resultBox, ref ServerEndPoint server) => PrepareToPushMessageToBridge(message, processor, resultBox, ref server) ? server.TryWriteAsync(message) : new ValueTask(WriteResult.NoConnectionAvailable); @@ -2177,389 +1778,177 @@ private WriteResult TryPushMessageToBridgeSync(Message message, ResultProcess /// /// Gets the client name for this multiplexer. /// - public override string ToString() - { - string s = ClientName; - if (string.IsNullOrWhiteSpace(s)) s = GetType().Name; - return s; - } + public override string ToString() => string.IsNullOrWhiteSpace(ClientName) ? GetType().Name : ClientName; - internal readonly byte[] ConfigurationChangedChannel; // This gets accessed for every received event; let's make sure we can process it "raw" - internal readonly byte[] UniqueId = Guid.NewGuid().ToByteArray(); // Unique identifier used when tracing - - /// - /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order. - /// - [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue) + ", will be removed in 3.0", false)] - public bool PreserveAsyncOrder + internal Exception GetException(WriteResult result, Message message, ServerEndPoint server) => result switch { - get => false; - set { } - } + WriteResult.Success => null, + WriteResult.NoConnectionAvailable => ExceptionFactory.NoConnectionAvailable(this, message, server), + WriteResult.TimeoutBeforeWrite => ExceptionFactory.Timeout(this, "The timeout was reached before the message could be written to the output buffer, and it was not sent", message, server, result), + _ => ExceptionFactory.ConnectionFailure(IncludeDetailInExceptions, ConnectionFailureType.ProtocolFailure, "An unknown error occurred when writing the message", server), + }; - /// - /// Indicates whether any servers are connected. - /// - public bool IsConnected + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Intentional observation")] + internal static void ThrowFailed(TaskCompletionSource source, Exception unthrownException) { - get + try { - var tmp = GetServerSnapshot(); - for (int i = 0; i < tmp.Length; i++) - if (tmp[i].IsConnected) return true; - return false; + throw unthrownException; } - } - - /// - /// Indicates whether any servers are currently trying to connect. - /// - public bool IsConnecting - { - get + catch (Exception ex) { - var tmp = GetServerSnapshot(); - for (int i = 0; i < tmp.Length; i++) - if (tmp[i].IsConnecting) return true; - return false; + source.TrySetException(ex); + GC.KeepAlive(source.Task.Exception); + GC.SuppressFinalize(source.Task); } } - internal ConfigurationOptions RawConfig { get; } - - internal ServerSelectionStrategy ServerSelectionStrategy { get; } - - internal Timer sentinelPrimaryReconnectTimer; - - internal Dictionary sentinelConnectionChildren = new Dictionary(); - internal ConnectionMultiplexer sentinelConnection = null; - - /// - /// Initializes the connection as a Sentinel connection and adds the necessary event handlers to track changes to the managed primaries. - /// - /// The writer to log to, if any. - internal void InitializeSentinel(LogProxy logProxy) + internal T ExecuteSyncImpl(Message message, ResultProcessor processor, ServerEndPoint server) { - if (ServerSelectionStrategy.ServerType != ServerType.Sentinel) + if (_isDisposed) throw new ObjectDisposedException(ToString()); + + if (message == null) // Fire-and forget could involve a no-op, represented by null - for example Increment by 0 { - return; + return default(T); } - // Subscribe to sentinel change events - ISubscriber sub = GetSubscriber(); - - if (sub.SubscribedEndpoint("+switch-master") == null) + if (message.IsFireAndForget) { - sub.Subscribe("+switch-master", (_, message) => - { - string[] messageParts = ((string)message).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - EndPoint switchBlame = Format.TryParseEndPoint(string.Format("{0}:{1}", messageParts[1], messageParts[2])); - - lock (sentinelConnectionChildren) - { - // Switch the primary if we have connections for that service - if (sentinelConnectionChildren.ContainsKey(messageParts[0])) - { - ConnectionMultiplexer child = sentinelConnectionChildren[messageParts[0]]; - - // Is the connection still valid? - if (child.IsDisposed) - { - child.ConnectionFailed -= OnManagedConnectionFailed; - child.ConnectionRestored -= OnManagedConnectionRestored; - sentinelConnectionChildren.Remove(messageParts[0]); - } - else - { - SwitchPrimary(switchBlame, sentinelConnectionChildren[messageParts[0]]); - } - } - } - }, CommandFlags.FireAndForget); +#pragma warning disable CS0618 // Type or member is obsolete + TryPushMessageToBridgeSync(message, processor, null, ref server); +#pragma warning restore CS0618 + Interlocked.Increment(ref fireAndForgets); + return default(T); } - - // If we lose connection to a sentinel server, - // we need to reconfigure to make sure we still have a subscription to the +switch-master channel - ConnectionFailed += (sender, e) => + else { - // Reconfigure to get subscriptions back online - ReconfigureAsync(first: false, reconfigureAll: true, logProxy, e.EndPoint, "Lost sentinel connection", false).Wait(); - }; + var source = SimpleResultBox.Get(); - // Subscribe to new sentinels being added - if (sub.SubscribedEndpoint("+sentinel") == null) - { - sub.Subscribe("+sentinel", (_, message) => + lock (source) { - string[] messageParts = ((string)message).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - UpdateSentinelAddressList(messageParts[0]); - }, CommandFlags.FireAndForget); - } - } +#pragma warning disable CS0618 // Type or member is obsolete + var result = TryPushMessageToBridgeSync(message, processor, source, ref server); +#pragma warning restore CS0618 + if (result != WriteResult.Success) + { + throw GetException(result, message, server); + } - /// - /// Returns a managed connection to the primary server indicated by the in the config. - /// - /// The configuration to be used when connecting to the primary. - /// The writer to log to, if any. - public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions config, TextWriter log = null) - { - if (ServerSelectionStrategy.ServerType != ServerType.Sentinel) - { - throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, - "Sentinel: The ConnectionMultiplexer is not a Sentinel connection. Detected as: " + ServerSelectionStrategy.ServerType); + if (Monitor.Wait(source, TimeoutMilliseconds)) + { + Trace("Timely response to " + message); + } + else + { + Trace("Timeout performing " + message); + Interlocked.Increment(ref syncTimeouts); + throw ExceptionFactory.Timeout(this, null, message, server); + // Very important not to return "source" to the pool here + } + } + // Snapshot these so that we can recycle the box + var val = source.GetResult(out var ex, canRecycle: true); // now that we aren't locking it... + if (ex != null) throw ex; + Trace(message + " received " + val); + return val; } + } - if (string.IsNullOrEmpty(config.ServiceName)) - throw new ArgumentException("A ServiceName must be specified."); + internal Task ExecuteAsyncImpl(Message message, ResultProcessor processor, object state, ServerEndPoint server) + { + if (_isDisposed) throw new ObjectDisposedException(ToString()); - lock (sentinelConnectionChildren) + if (message == null) { - if (sentinelConnectionChildren.TryGetValue(config.ServiceName, out var sentinelConnectionChild) && !sentinelConnectionChild.IsDisposed) - return sentinelConnectionChild; + return CompletedTask.Default(state); } - bool success = false; - ConnectionMultiplexer connection = null; - - var sw = ValueStopwatch.StartNew(); - do + static async Task ExecuteAsyncImpl_Awaited(ConnectionMultiplexer @this, ValueTask write, TaskCompletionSource tcs, Message message, ServerEndPoint server) { - // Get an initial endpoint - try twice - EndPoint newPrimaryEndPoint = GetConfiguredPrimaryForService(config.ServiceName) - ?? GetConfiguredPrimaryForService(config.ServiceName); - - if (newPrimaryEndPoint == null) - { - throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, - $"Sentinel: Failed connecting to configured primary for service: {config.ServiceName}"); - } - - EndPoint[] replicaEndPoints = GetReplicasForService(config.ServiceName) - ?? GetReplicasForService(config.ServiceName); - - // Replace the primary endpoint, if we found another one - // If not, assume the last state is the best we have and minimize the race - if (config.EndPoints.Count == 1) - { - config.EndPoints[0] = newPrimaryEndPoint; - } - else - { - config.EndPoints.Clear(); - config.EndPoints.TryAdd(newPrimaryEndPoint); - } - - foreach (var replicaEndPoint in replicaEndPoints) - { - config.EndPoints.TryAdd(replicaEndPoint); - } - - connection = ConnectImpl(config, log); - - // verify role is primary according to: - // https://redis.io/topics/sentinel-clients - if (connection.GetServer(newPrimaryEndPoint)?.Role().Value == RedisLiterals.master) + var result = await write.ForAwait(); + if (result != WriteResult.Success) { - success = true; - break; + var ex = @this.GetException(result, message, server); + ThrowFailed(tcs, ex); } - - Thread.Sleep(100); - } while (sw.ElapsedMilliseconds < config.ConnectTimeout); - - if (!success) - { - throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, - $"Sentinel: Failed connecting to configured primary for service: {config.ServiceName}"); + return tcs == null ? default : await tcs.Task.ForAwait(); } - // Attach to reconnect event to ensure proper connection to the new primary - connection.ConnectionRestored += OnManagedConnectionRestored; - - // If we lost the connection, run a switch to a least try and get updated info about the primary - connection.ConnectionFailed += OnManagedConnectionFailed; - - lock (sentinelConnectionChildren) + TaskCompletionSource tcs = null; + IResultBox source = null; + if (!message.IsFireAndForget) { - sentinelConnectionChildren[connection.RawConfig.ServiceName] = connection; + source = TaskResultBox.Create(out tcs, state); } + var write = TryPushMessageToBridgeAsync(message, processor, source, ref server); + if (!write.IsCompletedSuccessfully) return ExecuteAsyncImpl_Awaited(this, write, tcs, message, server); - // Perform the initial switchover - SwitchPrimary(RawConfig.EndPoints[0], connection, log); - - return connection; - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "We don't care.")] - internal void OnManagedConnectionRestored(object sender, ConnectionFailedEventArgs e) - { - ConnectionMultiplexer connection = (ConnectionMultiplexer)sender; - - var oldTimer = Interlocked.Exchange(ref connection.sentinelPrimaryReconnectTimer, null); - oldTimer?.Dispose(); - - try - { - // Run a switch to make sure we have update-to-date - // information about which primary we should connect to - SwitchPrimary(e.EndPoint, connection); - - try - { - // Verify that the reconnected endpoint is a primary, - // and the correct one otherwise we should reconnect - if (connection.GetServer(e.EndPoint).IsReplica || e.EndPoint != connection.currentSentinelPrimaryEndPoint) - { - // This isn't a primary, so try connecting again - SwitchPrimary(e.EndPoint, connection); - } - } - catch (Exception) - { - // If we get here it means that we tried to reconnect to a server that is no longer - // considered a primary by Sentinel and was removed from the list of endpoints. - - // If we caught an exception, we may have gotten a stale endpoint - // we are not aware of, so retry - SwitchPrimary(e.EndPoint, connection); - } - } - catch (Exception) + if (tcs == null) { - // Log, but don't throw in an event handler - // TODO: Log via new event handler? a la ConnectionFailed? + return CompletedTask.Default(null); // F+F explicitly does not get async-state } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "We don't care.")] - internal void OnManagedConnectionFailed(object sender, ConnectionFailedEventArgs e) - { - ConnectionMultiplexer connection = (ConnectionMultiplexer)sender; - // Periodically check to see if we can reconnect to the proper primary. - // This is here in case we lost our subscription to a good sentinel instance - // or if we miss the published primary change. - if (connection.sentinelPrimaryReconnectTimer == null) + else { - connection.sentinelPrimaryReconnectTimer = new Timer(_ => + var result = write.Result; + if (result != WriteResult.Success) { - try - { - // Attempt, but do not fail here - SwitchPrimary(e.EndPoint, connection); - } - catch (Exception) - { - } - finally - { - connection.sentinelPrimaryReconnectTimer?.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan); - } - }, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan); + var ex = GetException(result, message, server); + ThrowFailed(tcs, ex); + } + return tcs.Task; } } - internal EndPoint GetConfiguredPrimaryForService(string serviceName) => - GetServerSnapshot() - .ToArray() - .Where(s => s.ServerType == ServerType.Sentinel) - .AsParallel() - .Select(s => - { - try { return GetServer(s.EndPoint).SentinelGetMasterAddressByName(serviceName); } - catch { return null; } - }) - .FirstOrDefault(r => r != null); - - internal EndPoint currentSentinelPrimaryEndPoint; - - internal EndPoint[] GetReplicasForService(string serviceName) => - GetServerSnapshot() - .ToArray() - .Where(s => s.ServerType == ServerType.Sentinel) - .AsParallel() - .Select(s => - { - try { return GetServer(s.EndPoint).SentinelGetReplicaAddresses(serviceName); } - catch { return null; } - }) - .FirstOrDefault(r => r != null); + internal void OnAsyncTimeout() => Interlocked.Increment(ref asyncTimeouts); /// - /// Switches the SentinelMasterConnection over to a new primary. + /// Sends request to all compatible clients to reconfigure or reconnect. /// - /// The endpoint responsible for the switch. - /// The connection that should be switched over to a new primary endpoint. - /// The writer to log to, if any. - internal void SwitchPrimary(EndPoint switchBlame, ConnectionMultiplexer connection, TextWriter log = null) + /// The command flags to use. + /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending). + public long PublishReconfigure(CommandFlags flags = CommandFlags.None) { - if (log == null) log = TextWriter.Null; - - using (var logProxy = LogProxy.TryCreate(log)) + if (ConfigurationChangedChannel is null) { - string serviceName = connection.RawConfig.ServiceName; - - // Get new primary - try twice - EndPoint newPrimaryEndPoint = GetConfiguredPrimaryForService(serviceName) - ?? GetConfiguredPrimaryForService(serviceName) - ?? throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, - $"Sentinel: Failed connecting to switch primary for service: {serviceName}"); - - connection.currentSentinelPrimaryEndPoint = newPrimaryEndPoint; - - if (!connection.servers.Contains(newPrimaryEndPoint)) - { - EndPoint[] replicaEndPoints = GetReplicasForService(serviceName) - ?? GetReplicasForService(serviceName); - - connection.servers.Clear(); - connection.RawConfig.EndPoints.Clear(); - connection.RawConfig.EndPoints.TryAdd(newPrimaryEndPoint); - foreach (var replicaEndPoint in replicaEndPoints) - { - connection.RawConfig.EndPoints.TryAdd(replicaEndPoint); - } - Trace($"Switching primary to {newPrimaryEndPoint}"); - // Trigger a reconfigure - connection.ReconfigureAsync(first: false, reconfigureAll: false, logProxy, switchBlame, - $"Primary switch {serviceName}", false, CommandFlags.PreferMaster).Wait(); - - UpdateSentinelAddressList(serviceName); - } + return 0; } - } - - internal void UpdateSentinelAddressList(string serviceName) - { - var firstCompleteRequest = GetServerSnapshot() - .ToArray() - .Where(s => s.ServerType == ServerType.Sentinel) - .AsParallel() - .Select(s => - { - try { return GetServer(s.EndPoint).SentinelGetSentinelAddresses(serviceName); } - catch { return null; } - }) - .FirstOrDefault(r => r != null); - - // Ignore errors, as having an updated sentinel list is not essential - if (firstCompleteRequest == null) - return; - - bool hasNew = false; - foreach (EndPoint newSentinel in firstCompleteRequest.Where(x => !RawConfig.EndPoints.Contains(x))) + else if (ReconfigureIfNeeded(null, false, "PublishReconfigure", true, flags)) { - hasNew = true; - RawConfig.EndPoints.TryAdd(newSentinel); + return -1; } - - if (hasNew) + else { - // Reconfigure the sentinel multiplexer if we added new endpoints - ReconfigureAsync(first: false, reconfigureAll: true, null, RawConfig.EndPoints[0], "Updating Sentinel List", false).Wait(); + return PublishReconfigureImpl(flags); } } + private long PublishReconfigureImpl(CommandFlags flags) => + ConfigurationChangedChannel is byte[] channel + ? GetSubscriber().Publish(channel, RedisLiterals.Wildcard, flags) + : 0; + + /// + /// Sends request to all compatible clients to reconfigure or reconnect. + /// + /// The command flags to use. + /// The number of instances known to have received the message (however, the actual number can be higher). + public Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None) => + ConfigurationChangedChannel is byte[] channel + ? GetSubscriber().PublishAsync(channel, RedisLiterals.Wildcard, flags) + : CompletedTask.Default(null); + + /// + /// Release all resources associated with this object. + /// + public void Dispose() + { + GC.SuppressFinalize(this); + Close(!_isDisposed); + sentinelConnection?.Dispose(); + var oldTimer = Interlocked.Exchange(ref sentinelPrimaryReconnectTimer, null); + oldTimer?.Dispose(); + } + /// /// Close all connections and release all resources associated with this object. /// @@ -2587,39 +1976,6 @@ public void Close(bool allowCommandsToComplete = true) Interlocked.Increment(ref _connectionCloseCount); } - partial void OnCloseReaderWriter(); - - private void DisposeAndClearServers() - { - lock (servers) - { - var iter = servers.GetEnumerator(); - while (iter.MoveNext()) - { - var server = (ServerEndPoint)iter.Value; - server.Dispose(); - } - servers.Clear(); - } - } - - private Task[] QuitAllServers() - { - var quits = new Task[2 * servers.Count]; - lock (servers) - { - var iter = servers.GetEnumerator(); - int index = 0; - while (iter.MoveNext()) - { - var server = (ServerEndPoint)iter.Value; - quits[index++] = server.Close(ConnectionType.Interactive); - quits[index++] = server.Close(ConnectionType.Subscription); - } - } - return quits; - } - /// /// Close all connections and release all resources associated with this object. /// @@ -2641,225 +1997,35 @@ public async Task CloseAsync(bool allowCommandsToComplete = true) DisposeAndClearServers(); } - /// - /// Release all resources associated with this object. - /// - public void Dispose() - { - GC.SuppressFinalize(this); - Close(!_isDisposed); - sentinelConnection?.Dispose(); - var oldTimer = Interlocked.Exchange(ref sentinelPrimaryReconnectTimer, null); - oldTimer?.Dispose(); - } - - internal Task ExecuteAsyncImpl(Message message, ResultProcessor processor, object state, ServerEndPoint server) + private void DisposeAndClearServers() { - if (_isDisposed) throw new ObjectDisposedException(ToString()); - - if (message == null) - { - return CompletedTask.Default(state); - } - - TaskCompletionSource tcs = null; - IResultBox source = null; - if (!message.IsFireAndForget) - { - source = TaskResultBox.Create(out tcs, state); - } - var write = TryPushMessageToBridgeAsync(message, processor, source, ref server); - if (!write.IsCompletedSuccessfully) return ExecuteAsyncImpl_Awaited(this, write, tcs, message, server); - - if (tcs == null) - { - return CompletedTask.Default(null); // F+F explicitly does not get async-state - } - else + lock (servers) { - var result = write.Result; - if (result != WriteResult.Success) + var iter = servers.GetEnumerator(); + while (iter.MoveNext()) { - var ex = GetException(result, message, server); - ThrowFailed(tcs, ex); + var server = (ServerEndPoint)iter.Value; + server.Dispose(); } - return tcs.Task; - } - } - - private static async Task ExecuteAsyncImpl_Awaited(ConnectionMultiplexer @this, ValueTask write, TaskCompletionSource tcs, Message message, ServerEndPoint server) - { - var result = await write.ForAwait(); - if (result != WriteResult.Success) - { - var ex = @this.GetException(result, message, server); - ThrowFailed(tcs, ex); - } - return tcs == null ? default(T) : await tcs.Task.ForAwait(); - } - - internal Exception GetException(WriteResult result, Message message, ServerEndPoint server) => result switch - { - WriteResult.Success => null, - WriteResult.NoConnectionAvailable => ExceptionFactory.NoConnectionAvailable(this, message, server), - WriteResult.TimeoutBeforeWrite => ExceptionFactory.Timeout(this, "The timeout was reached before the message could be written to the output buffer, and it was not sent", message, server, result), - _ => ExceptionFactory.ConnectionFailure(IncludeDetailInExceptions, ConnectionFailureType.ProtocolFailure, "An unknown error occurred when writing the message", server), - }; - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Intentional observation")] - internal static void ThrowFailed(TaskCompletionSource source, Exception unthrownException) - { - try - { - throw unthrownException; - } - catch (Exception ex) - { - source.TrySetException(ex); - GC.KeepAlive(source.Task.Exception); - GC.SuppressFinalize(source.Task); + servers.Clear(); } } - internal T ExecuteSyncImpl(Message message, ResultProcessor processor, ServerEndPoint server) + private Task[] QuitAllServers() { - if (_isDisposed) throw new ObjectDisposedException(ToString()); - - if (message == null) // Fire-and forget could involve a no-op, represented by null - for example Increment by 0 - { - return default(T); - } - - if (message.IsFireAndForget) - { -#pragma warning disable CS0618 // Type or member is obsolete - TryPushMessageToBridgeSync(message, processor, null, ref server); -#pragma warning restore CS0618 - Interlocked.Increment(ref fireAndForgets); - return default(T); - } - else + var quits = new Task[2 * servers.Count]; + lock (servers) { - var source = SimpleResultBox.Get(); - - lock (source) + var iter = servers.GetEnumerator(); + int index = 0; + while (iter.MoveNext()) { -#pragma warning disable CS0618 // Type or member is obsolete - var result = TryPushMessageToBridgeSync(message, processor, source, ref server); -#pragma warning restore CS0618 - if (result != WriteResult.Success) - { - throw GetException(result, message, server); - } - - if (Monitor.Wait(source, TimeoutMilliseconds)) - { - Trace("Timely response to " + message); - } - else - { - Trace("Timeout performing " + message); - Interlocked.Increment(ref syncTimeouts); - throw ExceptionFactory.Timeout(this, null, message, server); - // Very important not to return "source" to the pool here - } + var server = (ServerEndPoint)iter.Value; + quits[index++] = server.Close(ConnectionType.Interactive); + quits[index++] = server.Close(ConnectionType.Subscription); } - // Snapshot these so that we can recycle the box - var val = source.GetResult(out var ex, canRecycle: true); // now that we aren't locking it... - if (ex != null) throw ex; - Trace(message + " received " + val); - return val; - } - } - - /// - /// Should exceptions include identifiable details? (key names, additional .Data annotations) - /// - public bool IncludeDetailInExceptions { get; set; } - - /// - /// Should exceptions include performance counter details? (CPU usage, etc - note that this can be problematic on some platforms) - /// - public bool IncludePerformanceCountersInExceptions { get; set; } - - internal int haveStormLog = 0; - internal string stormLogSnapshot; - /// - /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time). - /// Set to a negative value to disable this feature. - /// - public int StormLogThreshold { get; set; } = 15; - - /// - /// Obtains the log of unusual busy patterns. - /// - public string GetStormLog() => Volatile.Read(ref stormLogSnapshot); - - /// - /// Resets the log of unusual busy patterns. - /// - public void ResetStormLog() - { - Interlocked.Exchange(ref stormLogSnapshot, null); - Interlocked.Exchange(ref haveStormLog, 0); - } - - private long syncTimeouts, fireAndForgets, asyncTimeouts; - - internal void OnAsyncTimeout() => Interlocked.Increment(ref asyncTimeouts); - - /// - /// Sends request to all compatible clients to reconfigure or reconnect. - /// - /// The command flags to use. - /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending). - public long PublishReconfigure(CommandFlags flags = CommandFlags.None) - { - byte[] channel = ConfigurationChangedChannel; - if (channel == null) return 0; - if (ReconfigureIfNeeded(null, false, "PublishReconfigure", true, flags)) - { - return -1; - } - else - { - return PublishReconfigureImpl(flags); } + return quits; } - - private long PublishReconfigureImpl(CommandFlags flags) - { - byte[] channel = ConfigurationChangedChannel; - if (channel == null) return 0; - return GetSubscriber().Publish(channel, RedisLiterals.Wildcard, flags); - } - - /// - /// Sends request to all compatible clients to reconfigure or reconnect. - /// - /// The command flags to use. - /// The number of instances known to have received the message (however, the actual number can be higher). - public Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None) - { - byte[] channel = ConfigurationChangedChannel; - if (channel == null) return CompletedTask.Default(null); - - return GetSubscriber().PublishAsync(channel, RedisLiterals.Wildcard, flags); - } - - /// - /// Get the hash-slot associated with a given key, if applicable. - /// This can be useful for grouping operations. - /// - /// The to determine the hash slot for. - public int GetHashSlot(RedisKey key) => ServerSelectionStrategy.HashSlot(key); - } - - internal enum WriteResult - { - Success, - NoConnectionAvailable, - TimeoutBeforeWrite, - WriteFailure, } } diff --git a/src/StackExchange.Redis/EndPointCollection.cs b/src/StackExchange.Redis/EndPointCollection.cs index 65b6791fb..372698b21 100644 --- a/src/StackExchange.Redis/EndPointCollection.cs +++ b/src/StackExchange.Redis/EndPointCollection.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Net; +using System.Threading.Tasks; namespace StackExchange.Redis { @@ -163,5 +164,56 @@ internal void SetDefaultPorts(int defaultPort) yield return this[i]; } } + + internal bool HasDnsEndPoints() + { + foreach (var endpoint in this) + { + if (endpoint is DnsEndPoint) + { + return true; + } + } + return false; + } + + internal async Task ResolveEndPointsAsync(ConnectionMultiplexer multiplexer, LogProxy log) + { + var cache = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < Count; i++) + { + if (this[i] is DnsEndPoint dns) + { + try + { + if (dns.Host == ".") + { + this[i] = new IPEndPoint(IPAddress.Loopback, dns.Port); + } + else if (cache.TryGetValue(dns.Host, out IPAddress ip)) + { // use cache + this[i] = new IPEndPoint(ip, dns.Port); + } + else + { + log?.WriteLine($"Using DNS to resolve '{dns.Host}'..."); + var ips = await Dns.GetHostAddressesAsync(dns.Host).ObserveErrors().ForAwait(); + if (ips.Length == 1) + { + ip = ips[0]; + log?.WriteLine($"'{dns.Host}' => {ip}"); + cache[dns.Host] = ip; + this[i] = new IPEndPoint(ip, dns.Port); + } + } + } + catch (Exception ex) + { + multiplexer.OnInternalError(ex); + log?.WriteLine(ex.Message); + } + } + } + } } } diff --git a/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs index e9746747e..dd18a803e 100644 --- a/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs @@ -36,6 +36,6 @@ internal ServerMaintenanceEvent() /// Notifies a ConnectionMultiplexer of this event, for anyone observing its handler. /// protected void NotifyMultiplexer(ConnectionMultiplexer multiplexer) - => multiplexer.InvokeServerMaintenanceEvent(this); + => multiplexer.OnServerMaintenanceEvent(this); } } diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index ac04a1a5e..05f280d3c 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -218,6 +218,7 @@ StackExchange.Redis.ConfigurationOptions.Defaults.set -> void StackExchange.Redis.ConfigurationOptions.DefaultVersion.get -> System.Version StackExchange.Redis.ConfigurationOptions.DefaultVersion.set -> void StackExchange.Redis.ConfigurationOptions.EndPoints.get -> StackExchange.Redis.EndPointCollection +StackExchange.Redis.ConfigurationOptions.EndPoints.init -> void StackExchange.Redis.ConfigurationOptions.HighPrioritySocketThreads.get -> bool StackExchange.Redis.ConfigurationOptions.HighPrioritySocketThreads.set -> void StackExchange.Redis.ConfigurationOptions.KeepAlive.get -> int diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index 85babc63e..1ed774b59 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -49,10 +49,7 @@ internal sealed class ServerSelectionStrategy private ServerEndPoint[] map; - public ServerSelectionStrategy(ConnectionMultiplexer multiplexer) - { - this.multiplexer = multiplexer; - } + public ServerSelectionStrategy(ConnectionMultiplexer multiplexer) => this.multiplexer = multiplexer; public ServerType ServerType { get; set; } = ServerType.Standalone; internal static int TotalSlots => RedisClusterSlotCount; diff --git a/src/StackExchange.Redis/WriteResult.cs b/src/StackExchange.Redis/WriteResult.cs new file mode 100644 index 000000000..b7e87b915 --- /dev/null +++ b/src/StackExchange.Redis/WriteResult.cs @@ -0,0 +1,9 @@ +namespace StackExchange.Redis; + +internal enum WriteResult +{ + Success, + NoConnectionAvailable, + TimeoutBeforeWrite, + WriteFailure, +} diff --git a/tests/StackExchange.Redis.Tests/SSL.cs b/tests/StackExchange.Redis.Tests/SSL.cs index dc6c6dfb0..5212178d9 100644 --- a/tests/StackExchange.Redis.Tests/SSL.cs +++ b/tests/StackExchange.Redis.Tests/SSL.cs @@ -339,7 +339,7 @@ public void Issue883_Exhaustive() Writer.WriteLine("Tessting: " + ci.Name); CultureInfo.CurrentCulture = ci; - var a = ConnectionMultiplexer.PrepareConfig("myDNS:883,password=mypassword,connectRetry=3,connectTimeout=5000,syncTimeout=5000,defaultDatabase=0,ssl=true,abortConnect=false"); + var a = ConnectionMultiplexer.PrepareConfig(ConfigurationOptions.Parse("myDNS:883,password=mypassword,connectRetry=3,connectTimeout=5000,syncTimeout=5000,defaultDatabase=0,ssl=true,abortConnect=false")); var b = ConnectionMultiplexer.PrepareConfig(new ConfigurationOptions { EndPoints = { { "myDNS", 883 } }, diff --git a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs index be9df7981..257dc47a0 100644 --- a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs @@ -63,7 +63,7 @@ public bool IgnoreConnect public long OperationCount => _inner.OperationCount; #pragma warning disable CS0618 // Type or member is obsolete - public bool PreserveAsyncOrder { get => _inner.PreserveAsyncOrder; set => _inner.PreserveAsyncOrder = value; } + public bool PreserveAsyncOrder { get => false; set { } } #pragma warning restore CS0618 public bool IsConnected => _inner.IsConnected; From af55fcb445537511f2afa39383bb7e7f45adb2fd Mon Sep 17 00:00:00 2001 From: Steve Lorello <42971704+slorello89@users.noreply.github.com> Date: Thu, 24 Mar 2022 21:33:29 -0400 Subject: [PATCH 111/435] Adding ZRangeStore (#2052) Adds ZRangeStore for #2047 APIs added: * SortedSetRangeAndStore * SortedSetRangeAndStoreAsync Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 1 + .../Enums/SortedSetOrder.cs | 32 ++ .../Interfaces/IDatabase.cs | 32 ++ .../Interfaces/IDatabaseAsync.cs | 32 ++ .../KeyspaceIsolation/DatabaseWrapper.cs | 15 + .../KeyspaceIsolation/WrapperBase.cs | 15 + src/StackExchange.Redis/Message.cs | 238 +++++++++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 7 + src/StackExchange.Redis/RedisDatabase.cs | 92 ++++ src/StackExchange.Redis/RedisFeatures.cs | 5 + src/StackExchange.Redis/RedisLiterals.cs | 3 + tests/StackExchange.Redis.Tests/SortedSets.cs | 494 +++++++++++++++++- 13 files changed, 966 insertions(+), 1 deletion(-) create mode 100644 src/StackExchange.Redis/Enums/SortedSetOrder.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index fa55fbb71..0aabbd931 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,7 @@ - Adds: `ConfigurationOptions.BeforeSocketConnect` for configuring sockets between creation and connection ([#2031 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2031)) - Fix [#1813](https://github.com/StackExchange/StackExchange.Redis/issues/1813): Don't connect to endpoints we failed to parse ([#2042 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2042)) - Fix: `ClientKill`/`ClientKillAsync` when using `ClientType` ([#2048 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2048)) +- Adds: Support for `ZRANGESTORE` with `.SortedSetRangeAndStore()`/`.SortedSetRangeAndStoreAsync()` ([#2052 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2052)) ## 2.5.43 diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index decb0f003..f794f4784 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -199,6 +199,7 @@ internal enum RedisCommand ZRANGE, ZRANGEBYLEX, ZRANGEBYSCORE, + ZRANGESTORE, ZRANK, ZREM, ZREMRANGEBYLEX, diff --git a/src/StackExchange.Redis/Enums/SortedSetOrder.cs b/src/StackExchange.Redis/Enums/SortedSetOrder.cs new file mode 100644 index 000000000..afb389d91 --- /dev/null +++ b/src/StackExchange.Redis/Enums/SortedSetOrder.cs @@ -0,0 +1,32 @@ +namespace StackExchange.Redis; + +/// +/// Enum to manage ordering in sorted sets. +/// +public enum SortedSetOrder +{ + /// + /// Bases ordering off of the rank in the sorted set. This means that your start and stop inside the sorted set will be some offset into the set. + /// + ByRank, + + /// + /// Bases ordering off of the score in the sorted set. This means your start/stop will be some number which is the score for each member in the sorted set. + /// + ByScore, + + /// + /// Bases ordering off of lexicographical order, this is only appropriate in an instance where all the members of your sorted set are given the same score + /// + ByLex, +} + +internal static class SortedSetOrderByExtensions +{ + public static RedisValue GetLiteral(this SortedSetOrder sortedSetOrder) => sortedSetOrder switch + { + SortedSetOrder.ByLex => RedisLiterals.BYLEX, + SortedSetOrder.ByScore => RedisLiterals.BYSCORE, + _ => RedisValue.Null + }; +} diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 578637d29..e788d478c 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1395,6 +1395,38 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/zrevrange RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); + /// + /// Takes the specified range of elements in the sorted set of the + /// and stores them in a new sorted set at the . + /// + /// The sorted set to take the range from. + /// Where the resulting set will be stored. + /// The starting point in the sorted set. If is , this should be a string. + /// The stopping point in the range of the sorted set. If is , this should be a string. + /// The ordering criteria to use for the range. Choices are , , and (defaults to ). + /// Whether to exclude and from the range check (defaults to both inclusive). + /// + /// The direction to consider the and in. + /// If , the must be smaller than the . + /// If , must be smaller than . + /// + /// The number of elements into the sorted set to skip. Note: this iterates after sorting so incurs O(n) cost for large values. + /// The maximum number of elements to pull into the new () set. + /// The flags to use for this operation. + /// https://redis.io/commands/zrangestore + /// The cardinality of (number of elements in) the newly created sorted set. + long SortedSetRangeAndStore( + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue start, + RedisValue stop, + SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long? take = null, + CommandFlags flags = CommandFlags.None); + /// /// Returns the specified range of elements in the sorted set stored at key. /// By default the elements are considered to be ordered from the lowest to the highest score. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 6210a01fc..b3cd2dc0f 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1360,6 +1360,38 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/zrevrange Task SortedSetRangeByRankAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); + /// + /// Takes the specified range of elements in the sorted set of the + /// and stores them in a new sorted set at the . + /// + /// The sorted set to take the range from. + /// Where the resulting set will be stored. + /// The starting point in the sorted set. If is , this should be a string. + /// The stopping point in the range of the sorted set. If is , this should be a string. + /// The ordering criteria to use for the range. Choices are , , and (defaults to ). + /// Whether to exclude and from the range check (defaults to both inclusive). + /// + /// The direction to consider the and in. + /// If , the must be smaller than the . + /// If , must be smaller than . + /// + /// The number of elements into the sorted set to skip. Note: this iterates after sorting so incurs O(n) cost for large values. + /// The maximum number of elements to pull into the new () set. + /// The flags to use for this operation. + /// https://redis.io/commands/zrangestore + /// The cardinality of (number of elements in) the newly created sorted set. + Task SortedSetRangeAndStoreAsync( + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue start, + RedisValue stop, + SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long? take = null, + CommandFlags flags = CommandFlags.None); + /// /// Returns the specified range of elements in the sorted set stored at key. /// By default the elements are considered to be ordered from the lowest to the highest score. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 4153661aa..221cba05d 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -576,6 +576,21 @@ public RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop return Inner.SortedSetRangeByRank(ToInner(key), start, stop, order, flags); } + public long SortedSetRangeAndStore( + RedisKey destinationKey, + RedisKey sourceKey, + RedisValue start, + RedisValue stop, + SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long? take = null, + CommandFlags flags = CommandFlags.None) + { + return Inner.SortedSetRangeAndStore(ToInner(sourceKey), ToInner(destinationKey), start, stop, sortedSetOrder, exclude, order, skip, take, flags); + } + public SortedSetEntry[] SortedSetRangeByRankWithScores(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) { return Inner.SortedSetRangeByRankWithScores(ToInner(key), start, stop, order, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 008318223..ae9d63e73 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -564,6 +564,21 @@ public Task SortedSetRangeByRankAsync(RedisKey key, long start = 0 return Inner.SortedSetRangeByRankAsync(ToInner(key), start, stop, order, flags); } + public Task SortedSetRangeAndStoreAsync( + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue start, + RedisValue stop, + SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long? take = null, + CommandFlags flags = CommandFlags.None) + { + return Inner.SortedSetRangeAndStoreAsync(ToInner(sourceKey), ToInner(destinationKey), start, stop, sortedSetOrder, exclude, order, skip, take, flags); + } + public Task SortedSetRangeByRankWithScoresAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) { return Inner.SortedSetRangeByRankWithScoresAsync(ToInner(key), start, stop, order, flags); diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index a5c925697..cb410a2da 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -293,6 +293,31 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) => new CommandValueValueValueValueValueMessage(db, flags, command, value0, value1, value2, value3, value4); + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, + in RedisKey key1, in RedisValue value0, in RedisValue value1) => + new CommandKeyKeyValueValueMessage(db, flags, command, key0, key1, value0, value1); + + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, + in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2) => + new CommandKeyKeyValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2); + + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, + in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3) => + new CommandKeyKeyValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2, value3); + + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, + in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) => + new CommandKeyKeyValueValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2, value3, value4); + + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, + in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4, in RedisValue value5) => + new CommandKeyKeyValueValueValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2, value3, value4, value5); + + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, + in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, + in RedisValue value4, in RedisValue value5, in RedisValue value6) => + new CommandKeyKeyValueValueValueValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2, value3, value4, value5, value6); + public static Message CreateInSlot(int db, int slot, CommandFlags flags, RedisCommand command, RedisValue[] values) => new CommandSlotValuesMessage(db, slot, flags, command, values); @@ -1138,6 +1163,219 @@ protected override void WriteImpl(PhysicalConnection physical) public override int ArgCount => 6; } + private sealed class CommandKeyKeyValueValueMessage : CommandKeyBase + { + private readonly RedisValue value0, value1; + private readonly RedisKey key1; + + public CommandKeyKeyValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, + in RedisKey key1, in RedisValue value0, in RedisValue value1) : base(db, flags, command, key0) + { + key1.AssertNotNull(); + value0.AssertNotNull(); + value1.AssertNotNull(); + this.key1 = key1; + this.value0 = value0; + this.value1 = value1; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, ArgCount); + physical.Write(Key); + physical.Write(key1); + physical.WriteBulkString(value0); + physical.WriteBulkString(value1); + } + + public override int ArgCount => 4; + } + + private sealed class CommandKeyKeyValueValueValueMessage : CommandKeyBase + { + private readonly RedisValue value0, value1, value2; + private readonly RedisKey key1; + + public CommandKeyKeyValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, + in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2) : base(db, flags, command, key0) + { + key1.AssertNotNull(); + value0.AssertNotNull(); + value1.AssertNotNull(); + value2.AssertNotNull(); + this.key1 = key1; + this.value0 = value0; + this.value1 = value1; + this.value2 = value2; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, ArgCount); + physical.Write(Key); + physical.Write(key1); + physical.WriteBulkString(value0); + physical.WriteBulkString(value1); + physical.WriteBulkString(value2); + } + + public override int ArgCount => 5; + } + + private sealed class CommandKeyKeyValueValueValueValueMessage : CommandKeyBase + { + private readonly RedisValue value0, value1, value2, value3; + private readonly RedisKey key1; + + public CommandKeyKeyValueValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, + in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3) : base(db, flags, command, key0) + { + key1.AssertNotNull(); + value0.AssertNotNull(); + value1.AssertNotNull(); + value2.AssertNotNull(); + value3.AssertNotNull(); + this.key1 = key1; + this.value0 = value0; + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, ArgCount); + physical.Write(Key); + physical.Write(key1); + physical.WriteBulkString(value0); + physical.WriteBulkString(value1); + physical.WriteBulkString(value2); + physical.WriteBulkString(value3); + } + + public override int ArgCount => 6; + } + + private sealed class CommandKeyKeyValueValueValueValueValueMessage : CommandKeyBase + { + private readonly RedisValue value0, value1, value2, value3, value4; + private readonly RedisKey key1; + + public CommandKeyKeyValueValueValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, + in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) : base(db, flags, command, key0) + { + key1.AssertNotNull(); + value0.AssertNotNull(); + value1.AssertNotNull(); + value2.AssertNotNull(); + value3.AssertNotNull(); + value4.AssertNotNull(); + this.key1 = key1; + this.value0 = value0; + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.value4 = value4; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, ArgCount); + physical.Write(Key); + physical.Write(key1); + physical.WriteBulkString(value0); + physical.WriteBulkString(value1); + physical.WriteBulkString(value2); + physical.WriteBulkString(value3); + physical.WriteBulkString(value4); + } + + public override int ArgCount => 7; + } + + private sealed class CommandKeyKeyValueValueValueValueValueValueMessage : CommandKeyBase + { + private readonly RedisValue value0, value1, value2, value3, value4, value5; + private readonly RedisKey key1; + + public CommandKeyKeyValueValueValueValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, + in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4, in RedisValue value5) : base(db, flags, command, key0) + { + key1.AssertNotNull(); + value0.AssertNotNull(); + value1.AssertNotNull(); + value2.AssertNotNull(); + value3.AssertNotNull(); + value4.AssertNotNull(); + value5.AssertNotNull(); + this.key1 = key1; + this.value0 = value0; + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.value4 = value4; + this.value5 = value5; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, ArgCount); + physical.Write(Key); + physical.Write(key1); + physical.WriteBulkString(value0); + physical.WriteBulkString(value1); + physical.WriteBulkString(value2); + physical.WriteBulkString(value3); + physical.WriteBulkString(value4); + physical.WriteBulkString(value5); + } + + public override int ArgCount => 8; + } + + private sealed class CommandKeyKeyValueValueValueValueValueValueValueMessage : CommandKeyBase + { + private readonly RedisValue value0, value1, value2, value3, value4, value5, value6; + private readonly RedisKey key1; + + public CommandKeyKeyValueValueValueValueValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, + in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4, in RedisValue value5, in RedisValue value6) : base(db, flags, command, key0) + { + key1.AssertNotNull(); + value0.AssertNotNull(); + value1.AssertNotNull(); + value2.AssertNotNull(); + value3.AssertNotNull(); + value4.AssertNotNull(); + value5.AssertNotNull(); + value6.AssertNotNull(); + this.key1 = key1; + this.value0 = value0; + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.value4 = value4; + this.value5 = value5; + this.value6 = value6; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, ArgCount); + physical.Write(Key); + physical.Write(key1); + physical.WriteBulkString(value0); + physical.WriteBulkString(value1); + physical.WriteBulkString(value2); + physical.WriteBulkString(value3); + physical.WriteBulkString(value4); + physical.WriteBulkString(value5); + physical.WriteBulkString(value6); + } + + public override int ArgCount => 9; + } + private sealed class CommandMessage : Message { public CommandMessage(int db, CommandFlags flags, RedisCommand command) : base(db, flags, command) { } diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 05f280d3c..6459d0e3e 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -589,6 +589,7 @@ StackExchange.Redis.IDatabase.SortedSetLengthByValue(StackExchange.Redis.RedisKe StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[] StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry? StackExchange.Redis.IDatabase.SortedSetRangeByRank(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SortedSetRangeAndStore(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.RedisValue start, StackExchange.Redis.RedisValue stop, StackExchange.Redis.SortedSetOrder sortedSetOrder = StackExchange.Redis.SortedSetOrder.ByRank, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long? take = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetRangeByRankWithScores(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[] StackExchange.Redis.IDatabase.SortedSetRangeByScore(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] StackExchange.Redis.IDatabase.SortedSetRangeByScoreWithScores(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[] @@ -773,6 +774,7 @@ StackExchange.Redis.IDatabaseAsync.SortedSetLengthByValueAsync(StackExchange.Red StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task StackExchange.Redis.IDatabaseAsync.SortedSetRangeByRankAsync(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.SortedSetRangeAndStoreAsync(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.RedisValue start, StackExchange.Redis.RedisValue stop, StackExchange.Redis.SortedSetOrder sortedSetOrder = StackExchange.Redis.SortedSetOrder.ByRank, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long? take = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task StackExchange.Redis.IDatabaseAsync.SortedSetRangeByRankWithScoresAsync(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task StackExchange.Redis.IDatabaseAsync.SortedSetRangeByScoreAsync(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task StackExchange.Redis.IDatabaseAsync.SortedSetRangeByScoreWithScoresAsync(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task @@ -1161,6 +1163,7 @@ StackExchange.Redis.RedisFeatures.SetNotExistsAndGet.get -> bool StackExchange.Redis.RedisFeatures.SetPopMultiple.get -> bool StackExchange.Redis.RedisFeatures.SetVaradicAddRemove.get -> bool StackExchange.Redis.RedisFeatures.SortedSetPop.get -> bool +StackExchange.Redis.RedisFeatures.SortedSetRangeStore.get -> bool StackExchange.Redis.RedisFeatures.Streams.get -> bool StackExchange.Redis.RedisFeatures.StringLength.get -> bool StackExchange.Redis.RedisFeatures.StringSetRange.get -> bool @@ -1304,6 +1307,10 @@ StackExchange.Redis.SortedSetEntry.Score.get -> double StackExchange.Redis.SortedSetEntry.SortedSetEntry() -> void StackExchange.Redis.SortedSetEntry.SortedSetEntry(StackExchange.Redis.RedisValue element, double score) -> void StackExchange.Redis.SortedSetEntry.Value.get -> double +StackExchange.Redis.SortedSetOrder +StackExchange.Redis.SortedSetOrder.ByLex = 2 -> StackExchange.Redis.SortedSetOrder +StackExchange.Redis.SortedSetOrder.ByRank = 0 -> StackExchange.Redis.SortedSetOrder +StackExchange.Redis.SortedSetOrder.ByScore = 1 -> StackExchange.Redis.SortedSetOrder StackExchange.Redis.SortType StackExchange.Redis.SortType.Alphabetic = 1 -> StackExchange.Redis.SortType StackExchange.Redis.SortType.Numeric = 0 -> StackExchange.Redis.SortType diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index d1cf0070c..1690b34b2 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1631,12 +1631,44 @@ public RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop return ExecuteSync(msg, ResultProcessor.RedisValueArray); } + public long SortedSetRangeAndStore( + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue start, + RedisValue stop, + SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long? take = null, + CommandFlags flags = CommandFlags.None) + { + var msg = CreateSortedSetRangeStoreMessage(Database, flags, sourceKey, destinationKey, start, stop, sortedSetOrder, order, exclude, skip, take); + return ExecuteSync(msg, ResultProcessor.Int64); + } + public Task SortedSetRangeByRankAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, order == Order.Descending ? RedisCommand.ZREVRANGE : RedisCommand.ZRANGE, key, start, stop); return ExecuteAsync(msg, ResultProcessor.RedisValueArray); } + public Task SortedSetRangeAndStoreAsync( + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue start, + RedisValue stop, + SortedSetOrder sortedSetOrder = SortedSetOrder.ByRank, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long? take = null, + CommandFlags flags = CommandFlags.None) + { + var msg = CreateSortedSetRangeStoreMessage(Database, flags, sourceKey, destinationKey, start, stop, sortedSetOrder, order, exclude, skip, take); + return ExecuteAsync(msg, ResultProcessor.Int64); + } + public SortedSetEntry[] SortedSetRangeByRankWithScores(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, order == Order.Descending ? RedisCommand.ZREVRANGE : RedisCommand.ZRANGE, key, start, stop, RedisLiterals.WITHSCORES); @@ -3980,6 +4012,66 @@ protected override RedisValue[] Parse(in RawResult result, out int count) } } + private static Message CreateSortedSetRangeStoreMessage( + int db, + CommandFlags flags, + RedisKey sourceKey, + RedisKey destinationKey, + RedisValue start, + RedisValue stop, + SortedSetOrder sortedSetOrder, + Order order, + Exclude exclude, + long skip, + long? take) + { + if (sortedSetOrder == SortedSetOrder.ByRank) + { + if (take > 0) + { + throw new ArgumentException("take argument is not valid when sortedSetOrder is ByRank you may want to try setting the SortedSetOrder to ByLex or ByScore", nameof(take)); + } + if (exclude != Exclude.None) + { + throw new ArgumentException("exclude argument is not valid when sortedSetOrder is ByRank, you may want to try setting the sortedSetOrder to ByLex or ByScore", nameof(exclude)); + } + + return order switch + { + Order.Ascending => Message.Create(db, flags, RedisCommand.ZRANGESTORE, destinationKey, sourceKey, start, stop), + Order.Descending => Message.Create(db, flags, RedisCommand.ZRANGESTORE, destinationKey, sourceKey, start, stop, RedisLiterals.REV), + _ => throw new ArgumentOutOfRangeException(nameof(order)) + }; + } + + RedisValue formattedStart = exclude switch + { + Exclude.Both or Exclude.Start => $"({start}", + _ when sortedSetOrder == SortedSetOrder.ByLex => $"[{start}", + _ => start + }; + + RedisValue formattedStop = exclude switch + { + Exclude.Both or Exclude.Stop => $"({stop}", + _ when sortedSetOrder == SortedSetOrder.ByLex => $"[{stop}", + _ => stop + }; + + return order switch + { + Order.Ascending when take != null && take > 0 => + Message.Create(db, flags, RedisCommand.ZRANGESTORE, destinationKey, sourceKey, formattedStart, formattedStop, sortedSetOrder.GetLiteral(), RedisLiterals.LIMIT, skip, take), + Order.Ascending => + Message.Create(db, flags, RedisCommand.ZRANGESTORE, destinationKey, sourceKey, formattedStart, formattedStop, sortedSetOrder.GetLiteral()), + Order.Descending when take != null && take > 0 => + Message.Create(db, flags, RedisCommand.ZRANGESTORE, destinationKey, sourceKey, formattedStart, formattedStop, sortedSetOrder.GetLiteral(), RedisLiterals.REV, RedisLiterals.LIMIT, skip, take), + Order.Descending => + Message.Create(db, flags, RedisCommand.ZRANGESTORE, destinationKey, sourceKey, formattedStart, formattedStop, sortedSetOrder.GetLiteral(), RedisLiterals.REV), + _ => throw new ArgumentOutOfRangeException(nameof(order)) + }; + } + private sealed class SortedSetCombineAndStoreCommandMessage : Message.CommandKeyBase // ZINTERSTORE and ZUNIONSTORE have a very unusual signature { private readonly RedisKey[] keys; diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index a59af8c67..f13e31a9c 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -171,6 +171,11 @@ public RedisFeatures(Version version) /// public bool SortedSetPop => Version >= v4_9_1; + /// + /// Is ZRANGESTORE available? + /// + public bool SortedSetRangeStore => Version >= v6_2_0; + /// /// Are Redis Streams available? /// diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 220f1fcf0..7d8f4f148 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -49,6 +49,8 @@ public static readonly RedisValue AND = "AND", BEFORE = "BEFORE", BY = "BY", + BYLEX = "BYLEX", + BYSCORE = "BYSCORE", CHANNELS = "CHANNELS", COPY = "COPY", COUNT = "COUNT", @@ -87,6 +89,7 @@ public static readonly RedisValue REPLACE = "REPLACE", RESET = "RESET", RESETSTAT = "RESETSTAT", + REV = "REV", REWRITE = "REWRITE", SAVE = "SAVE", SEGFAULT = "SEGFAULT", diff --git a/tests/StackExchange.Redis.Tests/SortedSets.cs b/tests/StackExchange.Redis.Tests/SortedSets.cs index c54e9c438..b950fc250 100644 --- a/tests/StackExchange.Redis.Tests/SortedSets.cs +++ b/tests/StackExchange.Redis.Tests/SortedSets.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -23,6 +24,34 @@ public SortedSets(ITestOutputHelper output, SharedConnectionFixture fixture) : b new SortedSetEntry("j", 10) }; + private static readonly SortedSetEntry[] entriesPow2 = new SortedSetEntry[] + { + new SortedSetEntry("a", 1), + new SortedSetEntry("b", 2), + new SortedSetEntry("c", 4), + new SortedSetEntry("d", 8), + new SortedSetEntry("e", 16), + new SortedSetEntry("f", 32), + new SortedSetEntry("g", 64), + new SortedSetEntry("h", 128), + new SortedSetEntry("i", 256), + new SortedSetEntry("j", 512) + }; + + private static readonly SortedSetEntry[] lexEntries = new SortedSetEntry[] + { + new SortedSetEntry("a", 0), + new SortedSetEntry("b", 0), + new SortedSetEntry("c", 0), + new SortedSetEntry("d", 0), + new SortedSetEntry("e", 0), + new SortedSetEntry("f", 0), + new SortedSetEntry("g", 0), + new SortedSetEntry("h", 0), + new SortedSetEntry("i", 0), + new SortedSetEntry("j", 0) + }; + [Fact] public void SortedSetPopMulti_Multi() { @@ -146,5 +175,468 @@ public async Task SortedSetPopMulti_Zero_Async() Assert.Equal(10, db.SortedSetLength(key)); } } + + [Fact] + public async Task SortedSetRangeStoreByRankAsync() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, entries, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 0, -1); + Assert.Equal(entries.Length, res); + } + + [Fact] + public async Task SortedSetRangeStoreByRankLimitedAsync() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, entries, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 1, 4); + var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); + Assert.Equal(4, res); + for (var i = 1; i < 5; i++) + { + Assert.Equal(entries[i], range[i-1]); + } + } + + [Fact] + public async Task SortedSetRangeStoreByScoreAsync() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 64, 128, SortedSetOrder.ByScore); + var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); + Assert.Equal(2, res); + for (var i = 6; i < 8; i++) + { + Assert.Equal(entriesPow2[i], range[i-6]); + } + } + + [Fact] + public async Task SortedSetRangeStoreByScoreAsyncDefault() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, double.NegativeInfinity, double.PositiveInfinity, SortedSetOrder.ByScore); + var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); + Assert.Equal(10, res); + for (var i = 0; i < entriesPow2.Length; i++) + { + Assert.Equal(entriesPow2[i], range[i]); + } + } + + [Fact] + public async Task SortedSetRangeStoreByScoreAsyncLimited() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, double.NegativeInfinity, double.PositiveInfinity, SortedSetOrder.ByScore, skip: 1, take: 6); + var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); + Assert.Equal(6, res); + for (var i = 1; i < 7; i++) + { + Assert.Equal(entriesPow2[i], range[i-1]); + } + } + + [Fact] + public async Task SortedSetRangeStoreByScoreAsyncExclusiveRange() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 32, 256, SortedSetOrder.ByScore, exclude: Exclude.Both); + var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); + Assert.Equal(2, res); + for (var i = 6; i < 8; i++) + { + Assert.Equal(entriesPow2[i], range[i-6]); + } + } + + [Fact] + public async Task SortedSetRangeStoreByScoreAsyncReverse() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, start: double.PositiveInfinity, double.NegativeInfinity, SortedSetOrder.ByScore, order: Order.Descending); + var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); + Assert.Equal(10, res); + for (var i = 0; i < entriesPow2.Length; i++) + { + Assert.Equal(entriesPow2[i], range[i]); + } + } + + [Fact] + public async Task SortedSetRangeStoreByLexAsync() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, lexEntries, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, "a", "j", SortedSetOrder.ByLex); + var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); + Assert.Equal(10, res); + for (var i = 0; i r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, lexEntries, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, "a", "j", SortedSetOrder.ByLex, Exclude.Both); + var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); + Assert.Equal(8, res); + for (var i = 1; i r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, lexEntries, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, "j", "a", SortedSetOrder.ByLex, exclude:Exclude.None, order: Order.Descending); + var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); + Assert.Equal(10, res); + for (var i = 0; i < lexEntries.Length; i++) + { + Assert.Equal(lexEntries[i], range[i]); + } + } + + [Fact] + public void SortedSetRangeStoreByRank() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, entries, CommandFlags.FireAndForget); + var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, 0, -1); + Assert.Equal(entries.Length, res); + } + + [Fact] + public void SortedSetRangeStoreByRankLimited() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, entries, CommandFlags.FireAndForget); + var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, 1, 4); + var range = db.SortedSetRangeByRankWithScores(destinationKey); + Assert.Equal(4, res); + for (var i = 1; i < 5; i++) + { + Assert.Equal(entries[i], range[i-1]); + } + } + + [Fact] + public void SortedSetRangeStoreByScore() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, entriesPow2, CommandFlags.FireAndForget); + var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, 64, 128, SortedSetOrder.ByScore); + var range = db.SortedSetRangeByRankWithScores(destinationKey); + Assert.Equal(2, res); + for (var i = 6; i < 8; i++) + { + Assert.Equal(entriesPow2[i], range[i-6]); + } + } + + [Fact] + public void SortedSetRangeStoreByScoreDefault() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, entriesPow2, CommandFlags.FireAndForget); + var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, double.NegativeInfinity, double.PositiveInfinity, SortedSetOrder.ByScore); + var range = db.SortedSetRangeByRankWithScores(destinationKey); + Assert.Equal(10, res); + for (var i = 0; i < entriesPow2.Length; i++) + { + Assert.Equal(entriesPow2[i], range[i]); + } + } + + [Fact] + public void SortedSetRangeStoreByScoreLimited() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, entriesPow2, CommandFlags.FireAndForget); + var res = db.SortedSetRangeAndStore(sourceKey, destinationKey,double.NegativeInfinity, double.PositiveInfinity, SortedSetOrder.ByScore, skip: 1, take: 6); + var range = db.SortedSetRangeByRankWithScores(destinationKey); + Assert.Equal(6, res); + for (var i = 1; i < 7; i++) + { + Assert.Equal(entriesPow2[i], range[i-1]); + } + } + + [Fact] + public void SortedSetRangeStoreByScoreExclusiveRange() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, entriesPow2, CommandFlags.FireAndForget); + var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, 32, 256, SortedSetOrder.ByScore, exclude: Exclude.Both); + var range = db.SortedSetRangeByRankWithScores(destinationKey); + Assert.Equal(2, res); + for (var i = 6; i < 8; i++) + { + Assert.Equal(entriesPow2[i], range[i-6]); + } + } + + [Fact] + public void SortedSetRangeStoreByScoreReverse() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, entriesPow2, CommandFlags.FireAndForget); + var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, start: double.PositiveInfinity, double.NegativeInfinity, SortedSetOrder.ByScore, order: Order.Descending); + var range = db.SortedSetRangeByRankWithScores(destinationKey); + Assert.Equal(10, res); + for (var i = 0; i < entriesPow2.Length; i++) + { + Assert.Equal(entriesPow2[i], range[i]); + } + } + + [Fact] + public void SortedSetRangeStoreByLex() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); + var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, "a", "j", SortedSetOrder.ByLex); + var range = db.SortedSetRangeByRankWithScores(destinationKey); + Assert.Equal(10, res); + for (var i = 0; i r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); + var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, "a", "j", SortedSetOrder.ByLex, Exclude.Both); + var range = db.SortedSetRangeByRankWithScores(destinationKey); + Assert.Equal(8, res); + for (var i = 1; i r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); + var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, "j", "a", SortedSetOrder.ByLex, Exclude.None, Order.Descending); + var range = db.SortedSetRangeByRankWithScores(destinationKey); + Assert.Equal(10, res); + for (var i = 0; i < lexEntries.Length; i++) + { + Assert.Equal(lexEntries[i], range[i]); + } + } + + [Fact] + public void SortedSetRangeStoreFailErroneousTake() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); + var exception = Assert.Throws(()=>db.SortedSetRangeAndStore(sourceKey, destinationKey,0,-1, take:5)); + Assert.Equal("take", exception.ParamName); + } + + [Fact] + public void SortedSetRangeStoreFailExclude() + { + using var conn = Create(); + Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); + var exception = Assert.Throws(()=>db.SortedSetRangeAndStore(sourceKey, destinationKey,0,-1, exclude: Exclude.Both)); + Assert.Equal("exclude", exception.ParamName); + } } } From 4dccb0ab0c7abfbb64d1fbd8dd08a40668fe2285 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 25 Mar 2022 20:41:12 -0400 Subject: [PATCH 112/435] ConfigurationOptions: observe modifications after ConnectionMultiplexer start (#2050) Overall changes: - **Biggest**: `ConfgurationOptions` passed into a `ConnectionMultiplexer.Connect*()` method is no longer cloned. This means it can be modified after connecting and changes will take effect on the next access. For example a user can rotate passwords or change timeouts on the fly. - A few things like `EndPoints` _are_ cloned and memoized to `ConnectionMultiplexer` and changing them is not respected because this creates paradoxes and races in the multiplexer. These are explicitly called out in `ConfigurationOptions` docs. - Moves `IncludeDetailInExceptions` and `IncludePerformanceCountersInExceptions` from `ConnectionMultiplexer` to `ConfigurationOptions` (with backwards compatible APIs). - Should we `[Obsolete]` now? - Move to a cleaner `TryGetTieBreaker` approach instead of Sentinel defaults needing to set it. - `SetDefaultPorts` is now more cleanly based on `ServerType` (rather than a `sentinel` bool everywhere...that felt icky. - Config validation is centralized --- docs/ReleaseNotes.md | 2 + .../Configuration/DefaultOptionsProvider.cs | 17 ++- .../ConfigurationOptions.cs | 112 +++++++++++++----- .../ConnectionMultiplexer.Sentinel.cs | 21 ++-- .../ConnectionMultiplexer.cs | 104 +++++++++------- src/StackExchange.Redis/EndPointCollection.cs | 17 ++- src/StackExchange.Redis/ExceptionFactory.cs | 12 +- .../Interfaces/IConnectionMultiplexer.cs | 3 + src/StackExchange.Redis/PhysicalBridge.cs | 4 +- src/StackExchange.Redis/PhysicalConnection.cs | 2 +- src/StackExchange.Redis/PublicAPI.Shipped.txt | 6 + src/StackExchange.Redis/RedisDatabase.cs | 2 +- src/StackExchange.Redis/RedisServer.cs | 8 +- src/StackExchange.Redis/ResultProcessor.cs | 2 +- src/StackExchange.Redis/ServerEndPoint.cs | 5 +- .../ServerSelectionStrategy.cs | 2 +- tests/StackExchange.Redis.Tests/Config.cs | 54 +++++++++ tests/StackExchange.Redis.Tests/Deprecated.cs | 17 +++ .../ExceptionFactoryTests.cs | 6 +- tests/StackExchange.Redis.Tests/SSL.cs | 8 +- .../SharedConnectionFixture.cs | 5 +- 21 files changed, 291 insertions(+), 118 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 0aabbd931..89b8d5ef3 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,8 @@ - Adds: `ConfigurationOptions.BeforeSocketConnect` for configuring sockets between creation and connection ([#2031 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2031)) - Fix [#1813](https://github.com/StackExchange/StackExchange.Redis/issues/1813): Don't connect to endpoints we failed to parse ([#2042 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2042)) - Fix: `ClientKill`/`ClientKillAsync` when using `ClientType` ([#2048 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2048)) +- Adds: Most `ConfigurationOptions` changes after `ConnectionMultiplexer` connections will now be respected, e.g. changing a timeout will work and changing a password for auth rotation would be used at the next reconnect ([#2050 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2050)) + - **Obsolete**: This change also moves `ConnectionMultiplexer.IncludeDetailInExceptions` and `ConnectionMultiplexer.IncludePerformanceCountersInExceptions` to `ConfigurationOptions`. The old properties are `[Obsolete]` proxies that work until 3.0 for compatibility. - Adds: Support for `ZRANGESTORE` with `.SortedSetRangeAndStore()`/`.SortedSetRangeAndStoreAsync()` ([#2052 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2052)) ## 2.5.43 diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index f722b009b..a3d78ffac 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -19,7 +19,7 @@ namespace StackExchange.Redis.Configuration public class DefaultOptionsProvider { /// - /// The known providers to match against (built into the lbirary) - the default set. + /// The known providers to match against (built into the library) - the default set. /// If none of these match, is used. /// private static readonly List BuiltInProviders = new() @@ -109,6 +109,19 @@ public class DefaultOptionsProvider /// public virtual Version DefaultVersion => RedisFeatures.v3_0_0; + /// + /// Should exceptions include identifiable details? (key names, additional .Data annotations) + /// + public virtual bool IncludeDetailInExceptions => true; + + /// + /// Should exceptions include performance counter details? + /// + /// + /// CPU usage, etc - note that this can be problematic on some platforms. + /// + public virtual bool IncludePerformanceCountersInExceptions => false; + /// /// Specifies the time interval at which connections should be pinged to ensure validity. /// @@ -230,7 +243,7 @@ internal static string TryGetAzureRoleInstanceIdNoThrow() /// Note: this setting then applies for *all* endpoints. /// /// The configured endpoints to determine SSL usage from (e.g. from the port). - /// Whether to enable SSL for connections (unless excplicitly overriden in a direct set). + /// Whether to enable SSL for connections (unless explicitly overridden in a direct set). public virtual bool GetDefaultSsl(EndPointCollection endPoints) => false; /// diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index d96bc3f65..a1797a2a5 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -15,8 +15,17 @@ namespace StackExchange.Redis { /// - /// The options relevant to a set of redis connections + /// The options relevant to a set of redis connections. /// + /// + /// Some options are not observed by a after initial creation: + /// + /// + /// + /// + /// + /// + /// public sealed class ConfigurationOptions : ICloneable { private static class OptionKeys @@ -133,7 +142,8 @@ public static string TryNormalize(string value) private DefaultOptionsProvider defaultOptions; - private bool? allowAdmin, abortOnConnectFail, highPrioritySocketThreads, resolveDns, ssl, checkCertificateRevocation; + private bool? allowAdmin, abortOnConnectFail, resolveDns, ssl, checkCertificateRevocation, + includeDetailInExceptions, includePerformanceCountersInExceptions; private string tieBreaker, sslHost, configChannel; @@ -289,6 +299,10 @@ public int ConnectRetry /// /// The command-map associated with this configuration. /// + /// + /// This is memoized when a connects. + /// Modifying it afterwards will have no effect on already-created multiplexers. + /// public CommandMap CommandMap { get => commandMap ?? Defaults.CommandMap ?? Proxy switch @@ -300,9 +314,22 @@ public CommandMap CommandMap set => commandMap = value ?? throw new ArgumentNullException(nameof(value)); } + /// + /// Gets the command map for a given server type, since some supersede settings when connecting. + /// + internal CommandMap GetCommandMap(ServerType? serverType) => serverType switch + { + ServerType.Sentinel => CommandMap.Sentinel, + _ => CommandMap, + }; + /// /// Channel to use for broadcasting and listening for configuration change notification. /// + /// + /// This is memoized when a connects. + /// Modifying it afterwards will have no effect on already-created multiplexers. + /// public string ConfigurationChannel { get => configChannel ?? Defaults.ConfigurationChannel; @@ -335,16 +362,42 @@ public Version DefaultVersion /// /// The endpoints defined for this configuration. /// + /// + /// This is memoized when a connects. + /// Modifying it afterwards will have no effect on already-created multiplexers. + /// public EndPointCollection EndPoints { get; init; } = new EndPointCollection(); /// /// Use ThreadPriority.AboveNormal for SocketManager reader and writer threads (true by default). /// If , will be used. /// + [Obsolete($"This setting no longer has any effect, please use {nameof(SocketManager.SocketManagerOptions)}.{nameof(SocketManager.SocketManagerOptions.UseHighPrioritySocketThreads)} instead - this setting will be removed in 3.0.")] public bool HighPrioritySocketThreads { - get => highPrioritySocketThreads ?? true; - set => highPrioritySocketThreads = value; + get => false; + set { } + } + + /// + /// Should exceptions include identifiable details? (key names, additional .Data annotations) + /// + public bool IncludeDetailInExceptions + { + get => includeDetailInExceptions ?? Defaults.IncludeDetailInExceptions; + set => includeDetailInExceptions = value; + } + + /// + /// Should exceptions include performance counter details? + /// + /// + /// CPU usage, etc - note that this can be problematic on some platforms. + /// + public bool IncludePerformanceCountersInExceptions + { + get => includePerformanceCountersInExceptions ?? Defaults.IncludePerformanceCountersInExceptions; + set => includePerformanceCountersInExceptions = value; } /// @@ -370,7 +423,7 @@ public int KeepAlive /// /// Specifies whether asynchronous operations should be invoked in a way that guarantees their original delivery order. /// - [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue) + ", will be removed in 3.0.", false)] + [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue) + " - this will be removed in 3.0.", false)] public bool PreserveAsyncOrder { get => false; @@ -433,6 +486,10 @@ public int ResponseTimeout /// Gets or sets the SocketManager instance to be used with these options. /// If this is null a shared cross-multiplexer is used. /// + /// + /// This is only used when a is created. + /// Modifying it afterwards will have no effect on already-created multiplexers. + /// public SocketManager SocketManager { get; set; } /// @@ -545,7 +602,6 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow tieBreaker = tieBreaker, ssl = ssl, sslHost = sslHost, - highPrioritySocketThreads = highPrioritySocketThreads, configChannel = configChannel, abortOnConnectFail = abortOnConnectFail, resolveDns = resolveDns, @@ -564,7 +620,7 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow SslProtocols = SslProtocols, checkCertificateRevocation = checkCertificateRevocation, BeforeSocketConnect = BeforeSocketConnect, - EndPoints = new EndPointCollection(EndPoints), + EndPoints = EndPoints.Clone(), }; /// @@ -578,31 +634,28 @@ public ConfigurationOptions Apply(Action configure) return this; } - internal ConfigurationOptions WithDefaults(bool sentinel = false) - { - if (sentinel) - { - // this is required when connecting to sentinel servers - TieBreaker = ""; - CommandMap = CommandMap.Sentinel; - - // use default sentinel port - EndPoints.SetDefaultPorts(26379); - } - else - { - SetDefaultPorts(); - } - return this; - } - /// /// Resolve the default port for any endpoints that did not have a port explicitly specified. /// - public void SetDefaultPorts() => EndPoints.SetDefaultPorts(Ssl ? 6380 : 6379); + public void SetDefaultPorts() => EndPoints.SetDefaultPorts(ServerType.Standalone, ssl: Ssl); internal bool IsSentinel => !string.IsNullOrEmpty(ServiceName); + /// + /// Gets a tie breaker if we both have one set, and should be using one. + /// + internal bool TryGetTieBreaker(out RedisKey tieBreaker) + { + var key = TieBreaker; + if (!IsSentinel && !string.IsNullOrWhiteSpace(key)) + { + tieBreaker = key; + return true; + } + tieBreaker = default; + return false; + } + /// /// Returns the effective configuration string for this configuration, including Redis credentials. /// @@ -638,7 +691,6 @@ public string ToString(bool includePassword) Append(sb, OptionKeys.SslProtocols, SslProtocols?.ToString().Replace(',', '|')); Append(sb, OptionKeys.CheckCertificateRevocation, checkCertificateRevocation); Append(sb, OptionKeys.SslHost, sslHost); - Append(sb, OptionKeys.HighPrioritySocketThreads, highPrioritySocketThreads); Append(sb, OptionKeys.ConfigChannel, configChannel); Append(sb, OptionKeys.AbortOnConnectFail, abortOnConnectFail); Append(sb, OptionKeys.ResolveDns, resolveDns); @@ -681,7 +733,7 @@ private void Clear() { ClientName = ServiceName = User = Password = tieBreaker = sslHost = configChannel = null; keepAlive = syncTimeout = asyncTimeout = connectTimeout = connectRetry = configCheckSeconds = DefaultDatabase = null; - allowAdmin = abortOnConnectFail = highPrioritySocketThreads = resolveDns = ssl = null; + allowAdmin = abortOnConnectFail = resolveDns = ssl = null; SslProtocols = null; defaultVersion = null; EndPoints.Clear(); @@ -787,9 +839,6 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) case OptionKeys.SslHost: SslHost = value; break; - case OptionKeys.HighPrioritySocketThreads: - HighPrioritySocketThreads = OptionKeys.ParseBoolean(key, value); - break; case OptionKeys.Proxy: Proxy = OptionKeys.ParseProxy(key, value); break; @@ -800,6 +849,7 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) SslProtocols = OptionKeys.ParseSslProtocols(key, value); break; // Deprecated options we ignore... + case OptionKeys.HighPrioritySocketThreads: case OptionKeys.PreserveAsyncOrder: case OptionKeys.ResponseTimeout: case OptionKeys.WriteBuffer: diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs index fc8dd3ae6..289b0a9d3 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs @@ -14,7 +14,7 @@ public partial class ConnectionMultiplexer internal EndPoint currentSentinelPrimaryEndPoint; internal Timer sentinelPrimaryReconnectTimer; internal Dictionary sentinelConnectionChildren = new Dictionary(); - internal ConnectionMultiplexer sentinelConnection = null; + internal ConnectionMultiplexer sentinelConnection; /// /// Initializes the connection as a Sentinel connection and adds the necessary event handlers to track changes to the managed primaries. @@ -79,7 +79,6 @@ internal void InitializeSentinel(LogProxy logProxy) } } - /// /// Create a new instance that connects to a Sentinel server. /// @@ -104,7 +103,7 @@ public static Task SentinelConnectAsync(string configurat public static ConnectionMultiplexer SentinelConnect(ConfigurationOptions configuration, TextWriter log = null) { SocketConnection.AssertDependencies(); - return ConnectImpl(PrepareConfig(configuration, sentinel: true), log); + return ConnectImpl(configuration, log, ServerType.Sentinel); } /// @@ -115,7 +114,7 @@ public static ConnectionMultiplexer SentinelConnect(ConfigurationOptions configu public static Task SentinelConnectAsync(ConfigurationOptions configuration, TextWriter log = null) { SocketConnection.AssertDependencies(); - return ConnectImplAsync(PrepareConfig(configuration, sentinel: true), log); + return ConnectImplAsync(configuration, log, ServerType.Sentinel); } /// @@ -243,7 +242,7 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co } // Perform the initial switchover - SwitchPrimary(RawConfig.EndPoints[0], connection, log); + SwitchPrimary(EndPoints[0], connection, log); return connection; } @@ -368,11 +367,11 @@ internal void SwitchPrimary(EndPoint switchBlame, ConnectionMultiplexer connecti ?? GetReplicasForService(serviceName); connection.servers.Clear(); - connection.RawConfig.EndPoints.Clear(); - connection.RawConfig.EndPoints.TryAdd(newPrimaryEndPoint); + connection.EndPoints.Clear(); + connection.EndPoints.TryAdd(newPrimaryEndPoint); foreach (var replicaEndPoint in replicaEndPoints) { - connection.RawConfig.EndPoints.TryAdd(replicaEndPoint); + connection.EndPoints.TryAdd(replicaEndPoint); } Trace($"Switching primary to {newPrimaryEndPoint}"); // Trigger a reconfigure @@ -402,16 +401,16 @@ internal void UpdateSentinelAddressList(string serviceName) return; bool hasNew = false; - foreach (EndPoint newSentinel in firstCompleteRequest.Where(x => !RawConfig.EndPoints.Contains(x))) + foreach (EndPoint newSentinel in firstCompleteRequest.Where(x => !EndPoints.Contains(x))) { hasNew = true; - RawConfig.EndPoints.TryAdd(newSentinel); + EndPoints.TryAdd(newSentinel); } if (hasNew) { // Reconfigure the sentinel multiplexer if we added new endpoints - ReconfigureAsync(first: false, reconfigureAll: true, null, RawConfig.EndPoints[0], "Updating Sentinel List", false).Wait(); + ReconfigureAsync(first: false, reconfigureAll: true, null, EndPoints[0], "Updating Sentinel List", false).Wait(); } } } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 581cce306..baa2ebb05 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -43,10 +43,13 @@ public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplex internal bool IsDisposed => _isDisposed; internal CommandMap CommandMap { get; } + internal EndPointCollection EndPoints { get; } internal ConfigurationOptions RawConfig { get; } internal ServerSelectionStrategy ServerSelectionStrategy { get; } internal Exception LastException { get; set; } + ConfigurationOptions IInternalConnectionMultiplexer.RawConfig => RawConfig; + private int _activeHeartbeatErrors, lastHeartbeatTicks; internal long LastHeartbeatSecondsAgo => pulse is null @@ -60,22 +63,35 @@ pulse is null /// /// Should exceptions include identifiable details? (key names, additional .Data annotations) /// - public bool IncludeDetailInExceptions { get; set; } + [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")] + public bool IncludeDetailInExceptions + { + get => RawConfig.IncludeDetailInExceptions; + set => RawConfig.IncludeDetailInExceptions = value; + } /// - /// Should exceptions include performance counter details? (CPU usage, etc - note that this can be problematic on some platforms) + /// Should exceptions include performance counter details? /// - public bool IncludePerformanceCountersInExceptions { get; set; } + /// + /// CPU usage, etc - note that this can be problematic on some platforms. + /// + [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludePerformanceCountersInExceptions)} instead - this will be removed in 3.0.")] + public bool IncludePerformanceCountersInExceptions + { + get => RawConfig.IncludePerformanceCountersInExceptions; + set => RawConfig.IncludePerformanceCountersInExceptions = value; + } /// /// Gets the synchronous timeout associated with the connections. /// - public int TimeoutMilliseconds { get; } + public int TimeoutMilliseconds => RawConfig.SyncTimeout; /// /// Gets the asynchronous timeout associated with the connections. /// - internal int AsyncTimeoutMilliseconds { get; } + internal int AsyncTimeoutMilliseconds => RawConfig.AsyncTimeout; /// /// Gets the client-name that will be used on all new connections. @@ -123,16 +139,17 @@ static ConnectionMultiplexer() SetAutodetectFeatureFlags(); } - private ConnectionMultiplexer(ConfigurationOptions configuration) + private ConnectionMultiplexer(ConfigurationOptions configuration, ServerType? serverType = null) { - IncludeDetailInExceptions = true; - IncludePerformanceCountersInExceptions = false; - RawConfig = configuration ?? throw new ArgumentNullException(nameof(configuration)); + EndPoints = RawConfig.EndPoints.Clone(); + EndPoints.SetDefaultPorts(serverType, ssl: RawConfig.Ssl); - var map = CommandMap = configuration.CommandMap; - if (!string.IsNullOrWhiteSpace(configuration.Password)) map.AssertAvailable(RedisCommand.AUTH); - + var map = CommandMap = configuration.GetCommandMap(serverType); + if (!string.IsNullOrWhiteSpace(configuration.Password)) + { + map.AssertAvailable(RedisCommand.AUTH); + } if (!map.IsAvailable(RedisCommand.ECHO) && !map.IsAvailable(RedisCommand.PING) && !map.IsAvailable(RedisCommand.TIME)) { // I mean really, give me a CHANCE! I need *something* to check the server is available to me... @@ -140,9 +157,6 @@ private ConnectionMultiplexer(ConfigurationOptions configuration) map.AssertAvailable(RedisCommand.EXISTS); } - TimeoutMilliseconds = configuration.SyncTimeout; - AsyncTimeoutMilliseconds = configuration.AsyncTimeout; - OnCreateReaderWriter(configuration); ServerSelectionStrategy = new ServerSelectionStrategy(this); @@ -154,9 +168,9 @@ private ConnectionMultiplexer(ConfigurationOptions configuration) lastHeartbeatTicks = Environment.TickCount; } - private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions configuration, LogProxy log, out EventHandler connectHandler) + private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions configuration, LogProxy log, ServerType? serverType, out EventHandler connectHandler) { - var muxer = new ConnectionMultiplexer(configuration); + var muxer = new ConnectionMultiplexer(configuration, serverType); connectHandler = null; if (log is not null) { @@ -205,7 +219,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt if (!RawConfig.AllowAdmin) { - throw ExceptionFactory.AdminModeNotEnabled(IncludeDetailInExceptions, cmd, null, server); + throw ExceptionFactory.AdminModeNotEnabled(RawConfig.IncludeDetailInExceptions, cmd, null, server); } var srv = new RedisServer(this, server, null); if (!srv.IsConnected) @@ -230,14 +244,11 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt var nodes = GetServerSnapshot().ToArray(); // Have to array because async/await RedisValue newPrimary = Format.ToString(server.EndPoint); - RedisKey tieBreakerKey = default(RedisKey); // try and write this everywhere; don't worry if some folks reject our advances - if (options.HasFlag(ReplicationChangeOptions.SetTiebreaker) - && !string.IsNullOrWhiteSpace(RawConfig.TieBreaker) + if (RawConfig.TryGetTieBreaker(out var tieBreakerKey) + && options.HasFlag(ReplicationChangeOptions.SetTiebreaker) && CommandMap.IsAvailable(RedisCommand.SET)) { - tieBreakerKey = RawConfig.TieBreaker; - foreach (var node in nodes) { if (!node.IsConnected || node.IsReplica) continue; @@ -342,7 +353,7 @@ internal void CheckMessage(Message message) { if (!RawConfig.AllowAdmin && message.IsAdmin) { - throw ExceptionFactory.AdminModeNotEnabled(IncludeDetailInExceptions, message.Command, message, null); + throw ExceptionFactory.AdminModeNotEnabled(RawConfig.IncludeDetailInExceptions, message.Command, message, null); } if (message.Command != RedisCommand.UNKNOWN) { @@ -577,11 +588,12 @@ public static Task ConnectAsync(ConfigurationOptions conf return configuration?.IsSentinel == true ? SentinelPrimaryConnectAsync(configuration, log) - : ConnectImplAsync(PrepareConfig(configuration), log); + : ConnectImplAsync(configuration, log); } - private static async Task ConnectImplAsync(ConfigurationOptions configuration, TextWriter log = null) + private static async Task ConnectImplAsync(ConfigurationOptions configuration, TextWriter log, ServerType? serverType = null) { + Validate(configuration); IDisposable killMe = null; EventHandler connectHandler = null; ConnectionMultiplexer muxer = null; @@ -591,7 +603,7 @@ private static async Task ConnectImplAsync(ConfigurationO var sw = ValueStopwatch.StartNew(); logProxy?.WriteLine($"Connecting (async) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); - muxer = CreateMultiplexer(configuration, logProxy, out connectHandler); + muxer = CreateMultiplexer(configuration, logProxy, serverType, out connectHandler); killMe = muxer; Interlocked.Increment(ref muxer._connectAttemptCount); bool configured = await muxer.ReconfigureAsync(first: true, reconfigureAll: false, logProxy, null, "connect").ObserveErrors().ForAwait(); @@ -621,15 +633,16 @@ private static async Task ConnectImplAsync(ConfigurationO } } - internal static ConfigurationOptions PrepareConfig(ConfigurationOptions config, bool sentinel = false) + private static void Validate(ConfigurationOptions config) { - _ = config ?? throw new ArgumentNullException(nameof(config)); + if (config is null) + { + throw new ArgumentNullException(nameof(config)); + } if (config.EndPoints.Count == 0) { throw new ArgumentException("No endpoints specified", nameof(config)); } - - return config.Clone().WithDefaults(sentinel); } /// @@ -661,11 +674,12 @@ public static ConnectionMultiplexer Connect(ConfigurationOptions configuration, return configuration?.IsSentinel == true ? SentinelPrimaryConnect(configuration, log) - : ConnectImpl(PrepareConfig(configuration), log); + : ConnectImpl(configuration, log); } - private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configuration, TextWriter log) + private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configuration, TextWriter log, ServerType? serverType = null) { + Validate(configuration); IDisposable killMe = null; EventHandler connectHandler = null; ConnectionMultiplexer muxer = null; @@ -675,7 +689,7 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat var sw = ValueStopwatch.StartNew(); logProxy?.WriteLine($"Connecting (sync) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); - muxer = CreateMultiplexer(configuration, logProxy, out connectHandler); + muxer = CreateMultiplexer(configuration, logProxy, serverType, out connectHandler); killMe = muxer; Interlocked.Increment(ref muxer._connectAttemptCount); // note that task has timeouts internally, so it might take *just over* the regular timeout @@ -1185,15 +1199,15 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP if (first) { - if (RawConfig.ResolveDns && RawConfig.EndPoints.HasDnsEndPoints()) + if (RawConfig.ResolveDns && EndPoints.HasDnsEndPoints()) { - var dns = RawConfig.EndPoints.ResolveEndPointsAsync(this, log).ObserveErrors(); + var dns = EndPoints.ResolveEndPointsAsync(this, log).ObserveErrors(); if (!await dns.TimeoutAfter(TimeoutMilliseconds).ForAwait()) { throw new TimeoutException("Timeout resolving endpoints"); } } - foreach (var endpoint in RawConfig.EndPoints) + foreach (var endpoint in EndPoints) { GetServerEndPoint(endpoint, log, false); } @@ -1209,8 +1223,8 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP attemptsLeft--; } int standaloneCount = 0, clusterCount = 0, sentinelCount = 0; - var endpoints = RawConfig.EndPoints; - bool useTieBreakers = !string.IsNullOrWhiteSpace(RawConfig.TieBreaker); + var endpoints = EndPoints; + bool useTieBreakers = RawConfig.TryGetTieBreaker(out var tieBreakerKey); log?.WriteLine($"{endpoints.Count} unique nodes specified ({(useTieBreakers ? "with" : "without")} tiebreaker)"); if (endpoints.Count == 0) @@ -1237,8 +1251,6 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP var available = new Task[endpoints.Count]; servers = new ServerEndPoint[available.Length]; - RedisKey tieBreakerKey = useTieBreakers ? (RedisKey)RawConfig.TieBreaker : default(RedisKey); - for (int i = 0; i < available.Length; i++) { Trace("Testing: " + Format.ToString(endpoints[i])); @@ -1499,7 +1511,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP /// Whether to get only the endpoints specified explicitly in the config. public EndPoint[] GetEndPoints(bool configuredOnly = false) => configuredOnly - ? RawConfig.EndPoints.ToArray() + ? EndPoints.ToArray() : _serverSnapshot.GetEndPoints(); private async Task GetEndpointsFromClusterNodes(ServerEndPoint server, LogProxy log) @@ -1723,7 +1735,7 @@ private bool PrepareToPushMessageToBridge(Message message, ResultProcessor { if (message.IsPrimaryOnly() && server.IsReplica) { - throw ExceptionFactory.PrimaryOnly(IncludeDetailInExceptions, message.Command, message, server); + throw ExceptionFactory.PrimaryOnly(RawConfig.IncludeDetailInExceptions, message.Command, message, server); } switch (server.ServerType) @@ -1731,7 +1743,7 @@ private bool PrepareToPushMessageToBridge(Message message, ResultProcessor case ServerType.Cluster: if (message.GetHashSlot(ServerSelectionStrategy) == ServerSelectionStrategy.MultipleSlots) { - throw ExceptionFactory.MultiSlot(IncludeDetailInExceptions, message); + throw ExceptionFactory.MultiSlot(RawConfig.IncludeDetailInExceptions, message); } break; } @@ -1757,7 +1769,7 @@ private bool PrepareToPushMessageToBridge(Message message, ResultProcessor int availableDatabases = server.Databases; if (availableDatabases > 0 && message.Db >= availableDatabases) { - throw ExceptionFactory.DatabaseOutfRange(IncludeDetailInExceptions, message.Db, message, server); + throw ExceptionFactory.DatabaseOutfRange(RawConfig.IncludeDetailInExceptions, message.Db, message, server); } } @@ -1785,7 +1797,7 @@ private WriteResult TryPushMessageToBridgeSync(Message message, ResultProcess WriteResult.Success => null, WriteResult.NoConnectionAvailable => ExceptionFactory.NoConnectionAvailable(this, message, server), WriteResult.TimeoutBeforeWrite => ExceptionFactory.Timeout(this, "The timeout was reached before the message could be written to the output buffer, and it was not sent", message, server, result), - _ => ExceptionFactory.ConnectionFailure(IncludeDetailInExceptions, ConnectionFailureType.ProtocolFailure, "An unknown error occurred when writing the message", server), + _ => ExceptionFactory.ConnectionFailure(RawConfig.IncludeDetailInExceptions, ConnectionFailureType.ProtocolFailure, "An unknown error occurred when writing the message", server), }; [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Intentional observation")] diff --git a/src/StackExchange.Redis/EndPointCollection.cs b/src/StackExchange.Redis/EndPointCollection.cs index 372698b21..25caae917 100644 --- a/src/StackExchange.Redis/EndPointCollection.cs +++ b/src/StackExchange.Redis/EndPointCollection.cs @@ -12,6 +12,13 @@ namespace StackExchange.Redis /// public sealed class EndPointCollection : Collection, IEnumerable { + private static class DefaultPorts + { + public static int Standard => 6379; + public static int Ssl => 6380; + public static int Sentinel => 26379; + } + /// /// Create a new . /// @@ -133,8 +140,14 @@ protected override void SetItem(int index, EndPoint item) base.SetItem(index, item); } - internal void SetDefaultPorts(int defaultPort) + internal void SetDefaultPorts(ServerType? serverType, bool ssl = false) { + int defaultPort = serverType switch + { + ServerType.Sentinel => DefaultPorts.Sentinel, + _ => ssl ? DefaultPorts.Ssl : DefaultPorts.Standard, + }; + for (int i = 0; i < Count; i++) { switch (this[i]) @@ -215,5 +228,7 @@ internal async Task ResolveEndPointsAsync(ConnectionMultiplexer multiplexer, Log } } } + + internal EndPointCollection Clone() => new EndPointCollection(this); } } diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 0a89bf04a..d38f694e8 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -98,7 +98,7 @@ internal static Exception NoConnectionAvailable( ReadOnlySpan serverSnapshot = default, RedisCommand command = default) { - string commandLabel = GetLabel(multiplexer.IncludeDetailInExceptions, message?.Command ?? command, message); + string commandLabel = GetLabel(multiplexer.RawConfig.IncludeDetailInExceptions, message?.Command ?? command, message); if (server != null) { @@ -141,13 +141,13 @@ internal static Exception NoConnectionAvailable( // Add counters and exception data if we have it List> data = null; - if (multiplexer.IncludeDetailInExceptions) + if (multiplexer.RawConfig.IncludeDetailInExceptions) { data = new List>(); AddCommonDetail(data, sb, message, multiplexer, server); } var ex = new RedisConnectionException(ConnectionFailureType.UnableToResolvePhysicalConnection, sb.ToString(), innerException, message?.Status ?? CommandStatus.Unknown); - if (multiplexer.IncludeDetailInExceptions) + if (multiplexer.RawConfig.IncludeDetailInExceptions) { CopyDataToException(data, ex); sb.Append("; ").Append(PerfCounterHelper.GetThreadPoolAndCPUSummary(multiplexer.IncludePerformanceCountersInExceptions)); @@ -261,7 +261,7 @@ internal static Exception Timeout(ConnectionMultiplexer multiplexer, string base }; CopyDataToException(data, ex); - if (multiplexer.IncludeDetailInExceptions) AddExceptionDetail(ex, message, server, null); + if (multiplexer.RawConfig.IncludeDetailInExceptions) AddExceptionDetail(ex, message, server, null); return ex; } @@ -288,8 +288,8 @@ ServerEndPoint server if (message != null) { message.TryGetHeadMessages(out var now, out var next); - if (now != null) Add(data, sb, "Message-Current", "active", multiplexer.IncludeDetailInExceptions ? now.CommandAndKey : now.Command.ToString()); - if (next != null) Add(data, sb, "Message-Next", "next", multiplexer.IncludeDetailInExceptions ? next.CommandAndKey : next.Command.ToString()); + if (now != null) Add(data, sb, "Message-Current", "active", multiplexer.RawConfig.IncludeDetailInExceptions ? now.CommandAndKey : now.Command.ToString()); + if (next != null) Add(data, sb, "Message-Next", "next", multiplexer.RawConfig.IncludeDetailInExceptions ? next.CommandAndKey : next.Command.ToString()); } // Add server data, if we have it diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index 00ba41307..6b46ffa24 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -13,6 +13,8 @@ internal interface IInternalConnectionMultiplexer : IConnectionMultiplexer bool IgnoreConnect { get; set; } ReadOnlySpan GetServerSnapshot(); + + ConfigurationOptions RawConfig { get; } } /// @@ -59,6 +61,7 @@ public interface IConnectionMultiplexer : IDisposable /// /// Should exceptions include identifiable details? (key names, additional annotations). /// + [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")] bool IncludeDetailInExceptions { get; set; } /// diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index cbce91c8d..fc8f58a6f 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -1372,7 +1372,7 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne if (isPrimaryOnly && !ServerEndPoint.SupportsPrimaryWrites) { - throw ExceptionFactory.PrimaryOnly(Multiplexer.IncludeDetailInExceptions, message.Command, message, ServerEndPoint); + throw ExceptionFactory.PrimaryOnly(Multiplexer.RawConfig.IncludeDetailInExceptions, message.Command, message, ServerEndPoint); } switch(cmd) { @@ -1487,7 +1487,7 @@ internal void SimulateConnectionFailure(SimulatedFailureType failureType) { if (!Multiplexer.RawConfig.AllowAdmin) { - throw ExceptionFactory.AdminModeNotEnabled(Multiplexer.IncludeDetailInExceptions, RedisCommand.DEBUG, null, ServerEndPoint); // close enough + throw ExceptionFactory.AdminModeNotEnabled(Multiplexer.RawConfig.IncludeDetailInExceptions, RedisCommand.DEBUG, null, ServerEndPoint); // close enough } physical?.SimulateConnectionFailure(failureType); } diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index f1e03b654..d7c5fb991 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -220,7 +220,7 @@ private enum ReadMode : byte public long LastWriteSecondsAgo => unchecked(Environment.TickCount - Thread.VolatileRead(ref lastWriteTickCount)) / 1000; - private bool IncludeDetailInExceptions => BridgeCouldBeNull?.Multiplexer.IncludeDetailInExceptions ?? false; + private bool IncludeDetailInExceptions => BridgeCouldBeNull?.Multiplexer.RawConfig.IncludeDetailInExceptions ?? false; [Conditional("VERBOSE")] internal void Trace(string message) => BridgeCouldBeNull?.Multiplexer?.Trace(message, ToString()); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 6459d0e3e..0c9045f63 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -221,6 +221,10 @@ StackExchange.Redis.ConfigurationOptions.EndPoints.get -> StackExchange.Redis.En StackExchange.Redis.ConfigurationOptions.EndPoints.init -> void StackExchange.Redis.ConfigurationOptions.HighPrioritySocketThreads.get -> bool StackExchange.Redis.ConfigurationOptions.HighPrioritySocketThreads.set -> void +StackExchange.Redis.ConfigurationOptions.IncludeDetailInExceptions.get -> bool +StackExchange.Redis.ConfigurationOptions.IncludeDetailInExceptions.set -> void +StackExchange.Redis.ConfigurationOptions.IncludePerformanceCountersInExceptions.get -> bool +StackExchange.Redis.ConfigurationOptions.IncludePerformanceCountersInExceptions.set -> void StackExchange.Redis.ConfigurationOptions.KeepAlive.get -> int StackExchange.Redis.ConfigurationOptions.KeepAlive.set -> void StackExchange.Redis.ConfigurationOptions.Password.get -> string @@ -1598,6 +1602,8 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.DefaultVersion. virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetDefaultClientName() -> string virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetDefaultSsl(StackExchange.Redis.EndPointCollection endPoints) -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetSslHostFromEndpoints(StackExchange.Redis.EndPointCollection endPoints) -> string +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludeDetailInExceptions.get -> bool +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludePerformanceCountersInExceptions.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IsMatch(System.Net.EndPoint endpoint) -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.KeepAliveInterval.get -> System.TimeSpan virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Proxy.get -> StackExchange.Redis.Proxy diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 1690b34b2..f05e2dd34 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -3059,7 +3059,7 @@ private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long // Because we are using STORE, we need to push this to a primary if (Message.GetPrimaryReplicaFlags(flags) == CommandFlags.DemandReplica) { - throw ExceptionFactory.PrimaryOnly(multiplexer.IncludeDetailInExceptions, RedisCommand.SORT, null, null); + throw ExceptionFactory.PrimaryOnly(multiplexer.RawConfig.IncludeDetailInExceptions, RedisCommand.SORT, null, null); } flags = Message.SetPrimaryReplicaFlags(flags, CommandFlags.DemandMaster); values.Add(RedisLiterals.STORE); diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index c50976892..9910f942c 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -395,14 +395,14 @@ public Task ScriptExistsAsync(byte[] sha1, CommandFlags flags = CommandFla public void ScriptFlush(CommandFlags flags = CommandFlags.None) { - if (!multiplexer.RawConfig.AllowAdmin) throw ExceptionFactory.AdminModeNotEnabled(multiplexer.IncludeDetailInExceptions, RedisCommand.SCRIPT, null, server); + if (!multiplexer.RawConfig.AllowAdmin) throw ExceptionFactory.AdminModeNotEnabled(multiplexer.RawConfig.IncludeDetailInExceptions, RedisCommand.SCRIPT, null, server); var msg = Message.Create(-1, flags, RedisCommand.SCRIPT, RedisLiterals.FLUSH); ExecuteSync(msg, ResultProcessor.DemandOK); } public Task ScriptFlushAsync(CommandFlags flags = CommandFlags.None) { - if (!multiplexer.RawConfig.AllowAdmin) throw ExceptionFactory.AdminModeNotEnabled(multiplexer.IncludeDetailInExceptions, RedisCommand.SCRIPT, null, server); + if (!multiplexer.RawConfig.AllowAdmin) throw ExceptionFactory.AdminModeNotEnabled(multiplexer.RawConfig.IncludeDetailInExceptions, RedisCommand.SCRIPT, null, server); var msg = Message.Create(-1, flags, RedisCommand.SCRIPT, RedisLiterals.FLUSH); return ExecuteAsync(msg, ResultProcessor.DemandOK); } @@ -580,9 +580,9 @@ private Message GetTiebreakerRemovalMessage() { var configuration = multiplexer.RawConfig; - if (!string.IsNullOrWhiteSpace(configuration.TieBreaker) && multiplexer.CommandMap.IsAvailable(RedisCommand.DEL)) + if (configuration.TryGetTieBreaker(out var tieBreakerKey) && multiplexer.CommandMap.IsAvailable(RedisCommand.DEL)) { - var msg = Message.Create(0, CommandFlags.FireAndForget | CommandFlags.NoRedirect, RedisCommand.DEL, (RedisKey)configuration.TieBreaker); + var msg = Message.Create(0, CommandFlags.FireAndForget | CommandFlags.NoRedirect, RedisCommand.DEL, tieBreakerKey); msg.SetInternalCall(); return msg; } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 933cf3b31..89e630813 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -159,7 +159,7 @@ public void ConnectionFail(Message message, ConnectionFailureType fail, Exceptio if (message is not null) { sb.Append(" on "); - sb.Append(muxer?.IncludeDetailInExceptions == true ? message.ToString() : message.ToStringCommandOnly()); + sb.Append(muxer?.RawConfig.IncludeDetailInExceptions == true ? message.ToString() : message.ToStringCommandOnly()); } if (!string.IsNullOrWhiteSpace(annotation)) { diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 0c86bf3ba..8151212c2 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -83,7 +83,7 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) /// This is memoized because it's accessed on hot paths inside the write lock. /// public bool SupportsDatabases => - supportsDatabases ??= (serverType == ServerType.Standalone && Multiplexer.RawConfig.CommandMap.IsAvailable(RedisCommand.SELECT)); + supportsDatabases ??= (serverType == ServerType.Standalone && Multiplexer.CommandMap.IsAvailable(RedisCommand.SELECT)); public int Databases { @@ -426,9 +426,8 @@ internal async Task AutoConfigureAsync(PhysicalConnection connection, LogProxy l } // If we are going to fetch a tie breaker, do so last and we'll get it in before the tracer fires completing the connection // But if GETs are disabled on this, do not fail the connection - we just don't get tiebreaker benefits - if (!string.IsNullOrEmpty(Multiplexer.RawConfig.TieBreaker) && Multiplexer.RawConfig.CommandMap.IsAvailable(RedisCommand.GET)) + if (Multiplexer.RawConfig.TryGetTieBreaker(out var tieBreakerKey) && Multiplexer.CommandMap.IsAvailable(RedisCommand.GET)) { - RedisKey tieBreakerKey = Multiplexer.RawConfig.TieBreaker; log?.WriteLine($"{Format.ToString(EndPoint)}: Requesting tie-break (Key=\"{tieBreakerKey}\")..."); msg = Message.Create(0, flags, RedisCommand.GET, tieBreakerKey); msg.SetInternalCall(); diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index 1ed774b59..37792e47a 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -112,7 +112,7 @@ public ServerEndPoint Select(Message message, bool allowDisconnected = false) case ServerType.Twemproxy: case ServerType.Envoyproxy: slot = message.GetHashSlot(this); - if (slot == MultipleSlots) throw ExceptionFactory.MultiSlot(multiplexer.IncludeDetailInExceptions, message); + if (slot == MultipleSlots) throw ExceptionFactory.MultiSlot(multiplexer.RawConfig.IncludeDetailInExceptions, message); break; } return Select(slot, message.Command, message.Flags, allowDisconnected); diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index 653f91f34..546600cf2 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.Sockets; using System.Security.Authentication; +using System.Text; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -569,5 +570,58 @@ public void BeforeSocketConnect() Assert.True(interactiveSocket.DontFragment); Assert.True(subscriptionSocket.DontFragment); } + + [Fact] + public async Task MutableOptions() + { + var options = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort + ",name=Details"); + var originalConfigChannel = options.ConfigurationChannel = "originalConfig"; + var originalUser = options.User = "originalUser"; + var originalPassword = options.Password = "originalPassword"; + Assert.Equal("Details", options.ClientName); + using var muxer = await ConnectionMultiplexer.ConnectAsync(options); + + // Same instance + Assert.Same(options, muxer.RawConfig); + // Copies + Assert.NotSame(options.EndPoints, muxer.EndPoints); + + // Same until forked - it's not cloned + Assert.Same(options.CommandMap, muxer.CommandMap); + options.CommandMap = CommandMap.Envoyproxy; + Assert.NotSame(options.CommandMap, muxer.CommandMap); + +#pragma warning disable CS0618 // Type or member is obsolete + // Defaults true + Assert.True(options.IncludeDetailInExceptions); + Assert.True(muxer.IncludeDetailInExceptions); + options.IncludeDetailInExceptions = false; + Assert.False(options.IncludeDetailInExceptions); + Assert.False(muxer.IncludeDetailInExceptions); + + // Defaults false + Assert.False(options.IncludePerformanceCountersInExceptions); + Assert.False(muxer.IncludePerformanceCountersInExceptions); + options.IncludePerformanceCountersInExceptions = true; + Assert.True(options.IncludePerformanceCountersInExceptions); + Assert.True(muxer.IncludePerformanceCountersInExceptions); +#pragma warning restore CS0618 + + var newName = Guid.NewGuid().ToString(); + options.ClientName = newName; + Assert.Equal(newName, muxer.ClientName); + + // TODO: This forks due to memoization of the byte[] for efficiency + // If we could cheaply detect change it'd be good to let this change + const string newConfigChannel = "newConfig"; + options.ConfigurationChannel = newConfigChannel; + Assert.Equal(newConfigChannel, options.ConfigurationChannel); + Assert.Equal(Encoding.UTF8.GetString(muxer.ConfigurationChangedChannel), originalConfigChannel); + + Assert.Equal(originalUser, muxer.RawConfig.User); + Assert.Equal(originalPassword, muxer.RawConfig.Password); + var newPass = options.Password = "newPassword"; + Assert.Equal(newPass, muxer.RawConfig.Password); + } } } diff --git a/tests/StackExchange.Redis.Tests/Deprecated.cs b/tests/StackExchange.Redis.Tests/Deprecated.cs index ba55dc8a3..274f40a69 100644 --- a/tests/StackExchange.Redis.Tests/Deprecated.cs +++ b/tests/StackExchange.Redis.Tests/Deprecated.cs @@ -12,6 +12,23 @@ public class Deprecated : TestBase public Deprecated(ITestOutputHelper output) : base(output) { } #pragma warning disable CS0618 // Type or member is obsolete + [Fact] + public void HighPrioritySocketThreads() + { + Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.HighPrioritySocketThreads))!, typeof(ObsoleteAttribute))); + + var options = ConfigurationOptions.Parse("name=Hello"); + Assert.False(options.HighPrioritySocketThreads); + + options = ConfigurationOptions.Parse("highPriorityThreads=true"); + Assert.Equal("", options.ToString()); + Assert.False(options.HighPrioritySocketThreads); + + options = ConfigurationOptions.Parse("highPriorityThreads=false"); + Assert.Equal("", options.ToString()); + Assert.False(options.HighPrioritySocketThreads); + } + [Fact] public void PreserveAsyncOrder() { diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 51c762729..60f7728da 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -180,8 +180,8 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple muxer.AllowConnect = false; muxer._connectAttemptCount = connCount; muxer._connectCompletedCount = completeCount; - muxer.IncludeDetailInExceptions = hasDetail; - muxer.IncludePerformanceCountersInExceptions = hasDetail; + options.IncludeDetailInExceptions = hasDetail; + options.IncludePerformanceCountersInExceptions = hasDetail; var msg = Message.Create(-1, CommandFlags.None, RedisCommand.PING); var rawEx = ExceptionFactory.NoConnectionAvailable(muxer, msg, new ServerEndPoint(muxer, server.EndPoint)); @@ -221,7 +221,7 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple public void MessageFail(bool includeDetail, ConnectionFailureType failType, string messageStart) { using var muxer = Create(shared: false); - muxer.IncludeDetailInExceptions = includeDetail; + muxer.RawConfig.IncludeDetailInExceptions = includeDetail; var message = Message.Create(0, CommandFlags.None, RedisCommand.GET, (RedisKey)"myKey"); var resultBox = SimpleResultBox.Create(); diff --git a/tests/StackExchange.Redis.Tests/SSL.cs b/tests/StackExchange.Redis.Tests/SSL.cs index 5212178d9..52b982d5c 100644 --- a/tests/StackExchange.Redis.Tests/SSL.cs +++ b/tests/StackExchange.Redis.Tests/SSL.cs @@ -336,11 +336,11 @@ public void Issue883_Exhaustive() Writer.WriteLine($"Checking {all.Length} cultures..."); foreach (var ci in all) { - Writer.WriteLine("Tessting: " + ci.Name); + Writer.WriteLine("Testing: " + ci.Name); CultureInfo.CurrentCulture = ci; - var a = ConnectionMultiplexer.PrepareConfig(ConfigurationOptions.Parse("myDNS:883,password=mypassword,connectRetry=3,connectTimeout=5000,syncTimeout=5000,defaultDatabase=0,ssl=true,abortConnect=false")); - var b = ConnectionMultiplexer.PrepareConfig(new ConfigurationOptions + var a = ConfigurationOptions.Parse("myDNS:883,password=mypassword,connectRetry=3,connectTimeout=5000,syncTimeout=5000,defaultDatabase=0,ssl=true,abortConnect=false"); + var b = new ConfigurationOptions { EndPoints = { { "myDNS", 883 } }, Password = "mypassword", @@ -350,7 +350,7 @@ public void Issue883_Exhaustive() DefaultDatabase = 0, Ssl = true, AbortOnConnectFail = false, - }); + }; Writer.WriteLine($"computed: {b.ToString(true)}"); Writer.WriteLine("Checking endpoints..."); diff --git a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs index 257dc47a0..97d8aa87a 100644 --- a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs @@ -70,7 +70,10 @@ public bool IgnoreConnect public bool IsConnecting => _inner.IsConnecting; - public bool IncludeDetailInExceptions { get => _inner.IncludeDetailInExceptions; set => _inner.IncludeDetailInExceptions = value; } + public ConfigurationOptions RawConfig => _inner.RawConfig; + + public bool IncludeDetailInExceptions { get => _inner.RawConfig.IncludeDetailInExceptions; set => _inner.RawConfig.IncludeDetailInExceptions = value; } + public int StormLogThreshold { get => _inner.StormLogThreshold; set => _inner.StormLogThreshold = value; } public event EventHandler ErrorMessage From b09a0c2ceee44a8985bb51bb2735efc638680a2b Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sun, 27 Mar 2022 12:53:35 -0400 Subject: [PATCH 113/435] DefaultOptionsProvider: prevent AddProvider race (#2056) Adding a known provider can trigger an iteration race in the `GetForEndpoints` foreach. Instead of locking everywhere, given the rarity of adding a provider, instead I'm creating a new list and swapping the ref for the next iteration. --- .../Configuration/DefaultOptionsProvider.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index a3d78ffac..563d357b6 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -24,20 +24,25 @@ public class DefaultOptionsProvider /// private static readonly List BuiltInProviders = new() { - new AzureOptionsProvider() + new AzureOptionsProvider(), }; /// /// The current list of providers to match (potentially modified from defaults via . /// - private static LinkedList KnownProviders { get; } = new (BuiltInProviders); + private static LinkedList KnownProviders { get; set; } = new (BuiltInProviders); /// /// Adds a provider to match endpoints against. The last provider added has the highest priority. /// If you want your provider to match everything, implement as return true;. /// /// The provider to add. - public static void AddProvider(DefaultOptionsProvider provider) => KnownProviders.AddFirst(provider); + public static void AddProvider(DefaultOptionsProvider provider) + { + var newList = new LinkedList(KnownProviders); + newList.AddFirst(provider); + KnownProviders = newList; + } /// /// Whether this options provider matches a given endpoint, for automatically selecting a provider based on what's being connected to. From b159173c8d73356ebd4d75d73d714a974addb3e3 Mon Sep 17 00:00:00 2001 From: Ben Bryant <30258875+benbryant0@users.noreply.github.com> Date: Mon, 28 Mar 2022 21:34:14 +0200 Subject: [PATCH 114/435] Implement GETEX command (#1743) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #1740 Should add support for all current options of [GETEX](https://redis.io/commands/getex). Questionable (non-)decisions (opinions wanted!): * I called the new methods `StringGet[Async]` as the behaviour is most similar to plain `StringGet` and I think the added expiry functionality is communicated by the parameters. * The relative expiry overload takes `TimeSpan?` while the absolute overload takes non-nullable `DateTime`. I don't like it, but I did this as I realised I'd have to cast to escape ambiguity: `(TimeSpan?)null` 🤮. But maybe consistency is more important? * I looked in the `tests` folder and cried a little; I have no idea what I'm supposed to update there. I just bumped the Redis version in `Dockerfile` since Docker is easy and you seem to use it in your GitHub actions. * I placed the tests in `Strings`, even though they involve expiry and the tests themselves go against the pattern in that file a bit. Felt better than bloating something catch-all like `BasicOps`. Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 1 + .../Interfaces/IDatabase.cs | 22 ++++++ .../Interfaces/IDatabaseAsync.cs | 22 ++++++ .../KeyspaceIsolation/DatabaseWrapper.cs | 10 +++ .../KeyspaceIsolation/WrapperBase.cs | 10 +++ src/StackExchange.Redis/Message.cs | 1 + src/StackExchange.Redis/PublicAPI.Shipped.txt | 4 + src/StackExchange.Redis/RedisDatabase.cs | 51 +++++++++--- src/StackExchange.Redis/RedisFeatures.cs | 15 ++-- src/StackExchange.Redis/RedisLiterals.cs | 3 + tests/StackExchange.Redis.Tests/Strings.cs | 77 +++++++++++++++++++ 12 files changed, 202 insertions(+), 15 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 89b8d5ef3..fde96bfd2 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -2,6 +2,7 @@ ## Unreleased +- Adds: `GETEX` support with `.StringGetSetExpiry()`/`.StringGetSetExpiryAsync()` ([#1743 by benbryant0](https://github.com/StackExchange/StackExchange.Redis/pull/1743)) - Fix [#1988](https://github.com/StackExchange/StackExchange.Redis/issues/1988): Don't issue `SELECT` commands if explicitly disabled ([#2023 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2023)) - Adds: `KEEPTTL` support on `SET` operations ([#2029 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2029)) - Fix: Allow `XTRIM` `MAXLEN` argument to be `0` ([#2030 by NicoAvanzDev](https://github.com/StackExchange/StackExchange.Redis/pull/2030)) diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index f794f4784..30a93ccf4 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -50,6 +50,7 @@ internal enum RedisCommand GET, GETBIT, GETDEL, + GETEX, GETRANGE, GETSET, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index e788d478c..c7c1d766c 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -2153,6 +2153,28 @@ IEnumerable SortedSetScan(RedisKey key, /// https://redis.io/commands/getset RedisValue StringGetSet(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); + /// + /// Gets the value of and update its (relative) expiry. + /// If the key does not exist, the result will be . + /// + /// The key of the string. + /// The expiry to set. will remove expiry. + /// The flags to use for this operation. + /// The value of key, or nil when key does not exist. + /// https://redis.io/commands/getex + RedisValue StringGetSetExpiry(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); + + /// + /// Gets the value of and update its (absolute) expiry. + /// If the key does not exist, the result will be . + /// + /// The key of the string. + /// The exact date and time to expire at. will remove expiry. + /// The flags to use for this operation. + /// The value of key, or nil when key does not exist. + /// https://redis.io/commands/getex + RedisValue StringGetSetExpiry(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None); + /// /// Get the value of key and delete the key. /// If the key does not exist the special value nil is returned. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index b3cd2dc0f..b9c4edbf1 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -2106,6 +2106,28 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// https://redis.io/commands/getset Task StringGetSetAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); + /// + /// Gets the value of and update its (relative) expiry. + /// If the key does not exist, the result will be . + /// + /// The key of the string. + /// The expiry to set. will remove expiry. + /// The flags to use for this operation. + /// The value of key, or nil when key does not exist. + /// https://redis.io/commands/getex + Task StringGetSetExpiryAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); + + /// + /// Gets the value of and update its (absolute) expiry. + /// If the key does not exist, the result will be . + /// + /// The key of the string. + /// The exact date and time to expire at. will remove expiry. + /// The flags to use for this operation. + /// The value of key, or nil when key does not exist. + /// https://redis.io/commands/getex + Task StringGetSetExpiryAsync(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None); + /// /// Get the value of key and delete the key. /// If the key does not exist the special value nil is returned. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 221cba05d..7a4f6b396 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -836,6 +836,16 @@ public RedisValue StringGet(RedisKey key, CommandFlags flags = CommandFlags.None return Inner.StringGet(ToInner(key), flags); } + public RedisValue StringGetSetExpiry(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) + { + return Inner.StringGetSetExpiry(ToInner(key), expiry, flags); + } + + public RedisValue StringGetSetExpiry(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) + { + return Inner.StringGetSetExpiry(ToInner(key), expiry, flags); + } + public LeaseStringGetLease(RedisKey key, CommandFlags flags = CommandFlags.None) { return Inner.StringGetLease(ToInner(key), flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index ae9d63e73..e1a5bc572 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -827,6 +827,16 @@ public Task StringGetAsync(RedisKey key, CommandFlags flags = Comman return Inner.StringGetAsync(ToInner(key), flags); } + public Task StringGetSetExpiryAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) + { + return Inner.StringGetSetExpiryAsync(ToInner(key), expiry, flags); + } + + public Task StringGetSetExpiryAsync(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) + { + return Inner.StringGetSetExpiryAsync(ToInner(key), expiry, flags); + } + public Task> StringGetLeaseAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { return Inner.StringGetLeaseAsync(ToInner(key), flags); diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index cb410a2da..4a366449f 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -338,6 +338,7 @@ public static bool IsPrimaryOnly(RedisCommand command) case RedisCommand.FLUSHALL: case RedisCommand.FLUSHDB: case RedisCommand.GETDEL: + case RedisCommand.GETEX: case RedisCommand.GETSET: case RedisCommand.HDEL: case RedisCommand.HINCRBY: diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 0c9045f63..43e27c17a 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -648,6 +648,8 @@ StackExchange.Redis.IDatabase.StringGetDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.IDatabase.StringGetLease(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease StackExchange.Redis.IDatabase.StringGetRange(StackExchange.Redis.RedisKey key, long start, long end, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.StringGetSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StringGetSetExpiry(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StringGetSetExpiry(StackExchange.Redis.RedisKey key, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.StringGetWithExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValueWithExpiry StackExchange.Redis.IDatabase.StringIncrement(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double StackExchange.Redis.IDatabase.StringIncrement(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long @@ -832,6 +834,8 @@ StackExchange.Redis.IDatabaseAsync.StringGetDeleteAsync(StackExchange.Redis.Redi StackExchange.Redis.IDatabaseAsync.StringGetLeaseAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task> StackExchange.Redis.IDatabaseAsync.StringGetRangeAsync(StackExchange.Redis.RedisKey key, long start, long end, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task StackExchange.Redis.IDatabaseAsync.StringGetSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringGetSetExpiryAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.StringGetSetExpiryAsync(StackExchange.Redis.RedisKey key, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task StackExchange.Redis.IDatabaseAsync.StringGetWithExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task StackExchange.Redis.IDatabaseAsync.StringIncrementAsync(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task StackExchange.Redis.IDatabaseAsync.StringIncrementAsync(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index f05e2dd34..c84c232c4 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -2464,6 +2464,30 @@ public RedisValue StringGet(RedisKey key, CommandFlags flags = CommandFlags.None return ExecuteSync(msg, ResultProcessor.RedisValue); } + public RedisValue StringGetSetExpiry(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) + { + var msg = GetStringGetExMessage(key, expiry, flags); + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + + public RedisValue StringGetSetExpiry(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) + { + var msg = GetStringGetExMessage(key, expiry, flags); + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + + public Task StringGetSetExpiryAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) + { + var msg = GetStringGetExMessage(key, expiry, flags); + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + + public Task StringGetSetExpiryAsync(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) + { + var msg = GetStringGetExMessage(key, expiry, flags); + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + public RedisValue[] StringGet(RedisKey[] keys, CommandFlags flags = CommandFlags.None) { if (keys == null) throw new ArgumentNullException(nameof(keys)); @@ -2702,6 +2726,12 @@ public Task StringSetRangeAsync(RedisKey key, long offset, RedisValu return ExecuteAsync(msg, ResultProcessor.RedisValue); } + private long GetMillisecondsUntil(DateTime when) => when.Kind switch + { + DateTimeKind.Local or DateTimeKind.Utc => (when.ToUniversalTime() - RedisBase.UnixEpoch).Ticks / TimeSpan.TicksPerMillisecond, + _ => throw new ArgumentException("Expiry time must be either Utc or Local", nameof(when)), + }; + private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, TimeSpan? expiry, out ServerEndPoint server) { TimeSpan duration; @@ -2732,16 +2762,7 @@ private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, DateTime? server = null; return Message.Create(Database, flags, RedisCommand.PERSIST, key); } - switch (when.Kind) - { - case DateTimeKind.Local: - case DateTimeKind.Utc: - break; // fine, we can work with that - default: - throw new ArgumentException("Expiry time must be either Utc or Local", nameof(expiry)); - } - long milliseconds = (when.ToUniversalTime() - RedisBase.UnixEpoch).Ticks / TimeSpan.TicksPerMillisecond; - + long milliseconds = GetMillisecondsUntil(when); if ((milliseconds % 1000) != 0) { var features = GetFeatures(key, flags, out server); @@ -3507,6 +3528,16 @@ private Message GetStringBitOperationMessage(Bitwise operation, RedisKey destina return Message.CreateInSlot(Database, slot, flags, RedisCommand.BITOP, new[] { op, destination.AsRedisValue(), first.AsRedisValue(), second.AsRedisValue() }); } + private Message GetStringGetExMessage(in RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => expiry switch + { + null => Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PERSIST), + _ => Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PX, (long)expiry.Value.TotalMilliseconds) + }; + + private Message GetStringGetExMessage(in RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) => expiry == DateTime.MaxValue + ? Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PERSIST) + : Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PXAT, GetMillisecondsUntil(expiry)); + private Message GetStringGetWithExpiryMessage(RedisKey key, CommandFlags flags, out ResultProcessor processor, out ServerEndPoint server) { if (this is IBatch) diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index f13e31a9c..077cb04e1 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -76,6 +76,11 @@ public RedisFeatures(Version version) /// public bool GetDelete => Version >= v6_2_0; + /// + /// Does GETEX exist? + /// + internal bool GetEx => Version >= v6_2_0; + /// /// Is HSTRLEN available? /// @@ -238,11 +243,6 @@ public RedisFeatures(Version version) /// public bool SetPopMultiple => Version >= v3_2_0; - /// - /// The Redis version of the server - /// - public Version Version => version ?? v2_0_0; - /// /// Are the Touch command available? /// @@ -258,6 +258,11 @@ public RedisFeatures(Version version) /// public bool PushMultiple => Version >= v4_0_0; + /// + /// The Redis version of the server + /// + public Version Version => version ?? v2_0_0; + /// /// Create a string representation of the available features. /// diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 7d8f4f148..4dc2b6657 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -57,6 +57,7 @@ public static readonly RedisValue DESC = "DESC", DOCTOR = "DOCTOR", EX = "EX", + EXAT = "EXAT", EXISTS = "EXISTS", FLUSH = "FLUSH", GET = "GET", @@ -83,9 +84,11 @@ public static readonly RedisValue OBJECT = "OBJECT", OR = "OR", PAUSE = "PAUSE", + PERSIST = "PERSIST", PING = "PING", PURGE = "PURGE", PX = "PX", + PXAT = "PXAT", REPLACE = "REPLACE", RESET = "RESET", RESETSTAT = "RESETSTAT", diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index 5ee9ea19a..adac8d87b 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -68,6 +68,83 @@ public async Task Set() } } + [Fact] + public async Task StringGetSetExpiryNoValue() + { + using var muxer = Create(); + Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetEx), r => r.GetEx); + + var conn = muxer.GetDatabase(); + var key = Me(); + conn.KeyDelete(key, CommandFlags.FireAndForget); + + var emptyVal = await conn.StringGetSetExpiryAsync(key, TimeSpan.FromHours(1)); + + Assert.Equal(RedisValue.Null, emptyVal); + } + + [Fact] + public async Task StringGetSetExpiryRelative() + { + using var muxer = Create(); + Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetEx), r => r.GetEx); + + var conn = muxer.GetDatabase(); + var key = Me(); + conn.KeyDelete(key, CommandFlags.FireAndForget); + + conn.StringSet(key, "abc", TimeSpan.FromHours(1)); + var relativeSec = conn.StringGetSetExpiryAsync(key, TimeSpan.FromMinutes(30)); + var relativeSecTtl = conn.KeyTimeToLiveAsync(key); + + Assert.Equal("abc", await relativeSec); + var time = await relativeSecTtl; + Assert.NotNull(time); + Assert.InRange(time.Value, TimeSpan.FromMinutes(29.8), TimeSpan.FromMinutes(30.2)); + } + + [Fact] + public async Task StringGetSetExpiryAbsolute() + { + using var muxer = Create(); + Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetEx), r => r.GetEx); + var conn = muxer.GetDatabase(); + var key = Me(); + conn.KeyDelete(key, CommandFlags.FireAndForget); + + conn.StringSet(key, "abc", TimeSpan.FromHours(1)); + var newDate = DateTime.UtcNow.AddMinutes(30); + var val = conn.StringGetSetExpiryAsync(key, newDate); + var valTtl = conn.KeyTimeToLiveAsync(key); + + Assert.Equal("abc", await val); + var time = await valTtl; + Assert.NotNull(time); + Assert.InRange(time.Value, TimeSpan.FromMinutes(29.8), TimeSpan.FromMinutes(30.2)); + + // And ensure our type checking works + var ex = await Assert.ThrowsAsync(() => conn.StringGetSetExpiryAsync(key, new DateTime(100, DateTimeKind.Unspecified))); + Assert.NotNull(ex); + } + + [Fact] + public async Task StringGetSetExpiryPersist() + { + using var muxer = Create(); + Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetEx), r => r.GetEx); + + var conn = muxer.GetDatabase(); + var key = Me(); + conn.KeyDelete(key, CommandFlags.FireAndForget); + + conn.StringSet(key, "abc", TimeSpan.FromHours(1)); + var val = conn.StringGetSetExpiryAsync(key, null); + var valTtl = conn.KeyTimeToLiveAsync(key); + + Assert.Equal("abc", await val); + Assert.Null(await valTtl); + } + [Fact] public async Task GetLease() { From 9e7b7fc984d035f4e49f4e85d278f042385169cc Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 4 Apr 2022 07:53:59 -0400 Subject: [PATCH 115/435] Tag release notes as 2.5.61 --- docs/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index fde96bfd2..c22fd8f9e 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -2,6 +2,9 @@ ## Unreleased + +## 2.5.61 + - Adds: `GETEX` support with `.StringGetSetExpiry()`/`.StringGetSetExpiryAsync()` ([#1743 by benbryant0](https://github.com/StackExchange/StackExchange.Redis/pull/1743)) - Fix [#1988](https://github.com/StackExchange/StackExchange.Redis/issues/1988): Don't issue `SELECT` commands if explicitly disabled ([#2023 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2023)) - Adds: `KEEPTTL` support on `SET` operations ([#2029 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2029)) From 75da236160c244f1ea0246f20d849519a6875acd Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 7 Apr 2022 20:22:39 -0400 Subject: [PATCH 116/435] Nullable Reference Types for main library (#2041) To help in reviewing this, [I've provided an overview to listen along while reviewing](https://www.youtube.com/watch?v=X7wBphRIUSA). This is exploring what NRTs ([nullable reference types](https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references)) would look like on the main library. This covers the migration + test migration (to get a feel for what users would see). Highlights are: - We return empty arrays where a `null` could have happened before to keep it non-nullable (e.g. for `FireAndForget` on a fetch). - `RedisKey`/`RedisValue`/friends are `string?` which changes a lot of `(string)` casts. - Script executions are always non-null (I think this was effectively the case before). - `IConvertible` is `object` and not `object?` and so `RedisValue` and `RedisResult` are...just lying. They can be null. I have no idea why the decision on not-nullable is the way it is. - Same for `IConvertible.ToType`, lying again. - `[return: NotNullIfNotNull("field")]` doesn't work like you'd think on `Task`/`Task`, so we have 2 versions of `ExecuteAsync` trees for nullable vs. non-nullable (e.g. non-nullable default value provided, like with the arrays mentioned above). - Note that generic overrides need `where T : default` here. Don't ask for details. Because I don't really understand why. - `.IsNullOrEmpty()` and `.IsNullOrWhiteSpace()` extensions are just easier due to lack of annotations on full framework. - This pattern is common, and we should maybe change it (very repetitive): - Perhaps an aggressive inline of a `TryGetException`? Don't want to make the 99.9% success case more expensive though. ```cs if (result != WriteResult.Success) { var ex = Multiplexer.GetException(result, message, this)!; ``` - Also common is `GetBridge(message)!`, which isn't null if `create` is `true`, so maybe break this into 2 methods with no `create` arg, one of which isn't nullable? (`TryGetBridge` isn't great because this is most often chained). - This returns null from the disposed path everywhere...should probably revisit. --- .editorconfig | 120 +- docs/ReleaseNotes.md | 5 + .../ChannelMessageQueue.cs | 58 +- src/StackExchange.Redis/ClientInfo.cs | 41 +- .../ClusterConfiguration.cs | 77 +- src/StackExchange.Redis/CommandBytes.cs | 8 +- src/StackExchange.Redis/CommandMap.cs | 16 +- src/StackExchange.Redis/CommandTrace.cs | 8 +- .../CompletedDefaultTask.cs | 16 +- src/StackExchange.Redis/Condition.cs | 42 +- .../Configuration/DefaultOptionsProvider.cs | 32 +- .../ConfigurationOptions.cs | 66 +- .../ConnectionFailedEventArgs.cs | 10 +- .../ConnectionMultiplexer.Events.cs | 26 +- ...nnectionMultiplexer.ExportConfiguration.cs | 7 +- .../ConnectionMultiplexer.Profiling.cs | 2 +- .../ConnectionMultiplexer.ReaderWriter.cs | 7 +- .../ConnectionMultiplexer.Sentinel.cs | 117 +- .../ConnectionMultiplexer.StormLog.cs | 4 +- .../ConnectionMultiplexer.Threading.cs | 8 +- .../ConnectionMultiplexer.Verbose.cs | 26 +- .../ConnectionMultiplexer.cs | 292 +-- src/StackExchange.Redis/CursorEnumerable.cs | 48 +- src/StackExchange.Redis/EndPointCollection.cs | 11 +- src/StackExchange.Redis/EndPointEventArgs.cs | 4 +- src/StackExchange.Redis/ExceptionFactory.cs | 55 +- src/StackExchange.Redis/Exceptions.cs | 10 +- src/StackExchange.Redis/ExponentialRetry.cs | 2 +- .../ExtensionMethods.Internal.cs | 13 + src/StackExchange.Redis/ExtensionMethods.cs | 140 +- src/StackExchange.Redis/Format.cs | 82 +- src/StackExchange.Redis/GeoEntry.cs | 4 +- src/StackExchange.Redis/HashEntry.cs | 2 +- .../HashSlotMovedEventArgs.cs | 8 +- .../Interfaces/IConnectionMultiplexer.cs | 16 +- .../Interfaces/IDatabase.cs | 42 +- .../Interfaces/IDatabaseAsync.cs | 40 +- src/StackExchange.Redis/Interfaces/IServer.cs | 72 +- .../Interfaces/ISubscriber.cs | 12 +- .../InternalErrorEventArgs.cs | 8 +- .../KeyspaceIsolation/DatabaseExtension.cs | 2 +- .../KeyspaceIsolation/DatabaseWrapper.cs | 1076 ++++------- .../KeyspaceIsolation/TransactionWrapper.cs | 2 +- .../KeyspaceIsolation/WrapperBase.cs | 1189 +++++-------- src/StackExchange.Redis/Lease.cs | 2 +- src/StackExchange.Redis/LogProxy.cs | 10 +- src/StackExchange.Redis/LuaScript.cs | 36 +- .../Maintenance/AzureMaintenanceEvent.cs | 8 +- .../Maintenance/ServerMaintenanceEvent.cs | 4 +- src/StackExchange.Redis/Message.cs | 30 +- src/StackExchange.Redis/MessageCompletable.cs | 6 +- src/StackExchange.Redis/NameValueEntry.cs | 2 +- src/StackExchange.Redis/NullableHacks.cs | 148 ++ src/StackExchange.Redis/PerfCounterHelper.cs | 6 +- src/StackExchange.Redis/PhysicalBridge.cs | 38 +- src/StackExchange.Redis/PhysicalConnection.cs | 128 +- .../Profiling/IProfiledCommand.cs | 2 +- .../Profiling/ProfiledCommand.cs | 29 +- .../Profiling/ProfiledCommandEnumerable.cs | 28 +- .../Profiling/ProfilingSession.cs | 16 +- src/StackExchange.Redis/PublicAPI.Shipped.txt | 1569 +++++++++-------- src/StackExchange.Redis/RawResult.cs | 22 +- src/StackExchange.Redis/RedisBase.cs | 28 +- src/StackExchange.Redis/RedisBatch.cs | 22 +- src/StackExchange.Redis/RedisChannel.cs | 29 +- src/StackExchange.Redis/RedisDatabase.cs | 329 ++-- .../RedisErrorEventArgs.cs | 4 +- src/StackExchange.Redis/RedisFeatures.cs | 2 +- src/StackExchange.Redis/RedisKey.cs | 52 +- src/StackExchange.Redis/RedisResult.cs | 247 +-- src/StackExchange.Redis/RedisServer.cs | 203 ++- src/StackExchange.Redis/RedisSubscriber.cs | 61 +- src/StackExchange.Redis/RedisTransaction.cs | 49 +- src/StackExchange.Redis/RedisValue.cs | 124 +- src/StackExchange.Redis/ResultBox.cs | 28 +- src/StackExchange.Redis/ResultProcessor.cs | 299 ++-- src/StackExchange.Redis/Role.cs | 8 +- .../ScriptParameterMapper.cs | 43 +- src/StackExchange.Redis/ServerCounters.cs | 4 +- src/StackExchange.Redis/ServerEndPoint.cs | 78 +- .../ServerSelectionStrategy.cs | 39 +- src/StackExchange.Redis/SocketManager.cs | 10 +- src/StackExchange.Redis/SortedSetEntry.cs | 4 +- .../StackExchange.Redis.csproj | 1 + src/StackExchange.Redis/StreamEntry.cs | 4 +- src/StackExchange.Redis/StreamGroupInfo.cs | 4 +- src/StackExchange.Redis/TaskExtensions.cs | 4 +- src/StackExchange.Redis/TaskSource.cs | 2 +- src/StackExchange.Redis/Utils.cs | 6 +- tests/BasicTest/Program.cs | 22 +- .../StackExchange.Redis.Tests/BacklogTests.cs | 4 +- tests/StackExchange.Redis.Tests/BasicOps.cs | 16 +- tests/StackExchange.Redis.Tests/BoxUnbox.cs | 2 +- tests/StackExchange.Redis.Tests/Cluster.cs | 34 +- tests/StackExchange.Redis.Tests/Config.cs | 17 +- .../ConnectionFailedErrors.cs | 2 +- .../EventArgsTests.cs | 112 +- .../ExceptionFactoryTests.cs | 8 +- tests/StackExchange.Redis.Tests/Execute.cs | 8 +- .../FloatingPoint.cs | 5 +- .../StackExchange.Redis.Tests/FormatTests.cs | 2 +- tests/StackExchange.Redis.Tests/GeoTests.cs | 5 +- tests/StackExchange.Redis.Tests/Hashes.cs | 22 +- .../StackExchange.Redis.Tests/Helpers/Skip.cs | 14 +- .../Helpers/redis-sharp.cs | 8 +- .../Issues/Issue1101.cs | 6 +- .../Issues/Issue182.cs | 4 +- .../Issues/Massive Delete.cs | 6 +- .../Issues/SO10504853.cs | 2 +- .../Issues/SO11766033.cs | 4 +- .../Issues/SO24807536.cs | 2 +- tests/StackExchange.Redis.Tests/Keys.cs | 5 +- .../KeysAndValues.cs | 12 +- tests/StackExchange.Redis.Tests/Latency.cs | 2 +- tests/StackExchange.Redis.Tests/Locking.cs | 2 +- tests/StackExchange.Redis.Tests/Memory.cs | 4 +- tests/StackExchange.Redis.Tests/Migrate.cs | 2 +- tests/StackExchange.Redis.Tests/Profiling.cs | 8 +- tests/StackExchange.Redis.Tests/PubSub.cs | 17 +- .../PubSubMultiserver.cs | 2 + .../RawResultTests.cs | 12 +- tests/StackExchange.Redis.Tests/SSL.cs | 4 +- tests/StackExchange.Redis.Tests/Scans.cs | 2 +- tests/StackExchange.Redis.Tests/Scripting.cs | 103 +- tests/StackExchange.Redis.Tests/Sentinel.cs | 1 + .../StackExchange.Redis.Tests/SentinelBase.cs | 10 +- tests/StackExchange.Redis.Tests/Sets.cs | 4 +- .../SharedConnectionFixture.cs | 8 +- tests/StackExchange.Redis.Tests/SortedSets.cs | 1 + tests/StackExchange.Redis.Tests/Streams.cs | 33 +- tests/StackExchange.Redis.Tests/Strings.cs | 16 +- tests/StackExchange.Redis.Tests/TestBase.cs | 4 +- .../StackExchange.Redis.Tests/Transactions.cs | 4 +- tests/StackExchange.Redis.Tests/Values.cs | 2 +- .../WithKeyPrefixTests.cs | 4 +- toys/KestrelRedisServer/Program.cs | 5 +- .../StackExchange.Redis.Server/RedisServer.cs | 9 +- toys/StackExchange.Redis.Server/RespServer.cs | 10 +- version.json | 2 +- 139 files changed, 3955 insertions(+), 4232 deletions(-) create mode 100644 src/StackExchange.Redis/ExtensionMethods.Internal.cs create mode 100644 src/StackExchange.Redis/NullableHacks.cs diff --git a/.editorconfig b/.editorconfig index cb7636856..6934b4a38 100644 --- a/.editorconfig +++ b/.editorconfig @@ -27,71 +27,123 @@ indent_size = 2 # Dotnet code style settings: [*.{cs,vb}] # Sort using and Import directives with System.* appearing first -dotnet_sort_system_directives_first = true +dotnet_sort_system_directives_first = true:warning # Avoid "this." and "Me." if not necessary -dotnet_style_qualification_for_field = false:suggestion -dotnet_style_qualification_for_property = false:suggestion -dotnet_style_qualification_for_method = false:suggestion -dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning + +# Modifiers +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning +dotnet_style_readonly_field = true:warning # Use language keywords instead of framework type names for type references -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning # Suggest more modern language features when available -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_object_initializer = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_null_propagation = true:warning +dotnet_style_coalesce_expression = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning +dotnet_style_prefer_auto_properties = true:suggestion # Ignore silly if statements -dotnet_style_prefer_conditional_expression_over_return = false:none +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion # Don't warn on things that actually need suppressing dotnet_remove_unnecessary_suppression_exclusions = CA1009,CA1063,CA1069,CA1416,CA1816,CA1822,CA2202,CS0618,IDE0060,IDE0062,RCS1047,RCS1085,RCS1090,RCS1194,RCS1231 +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const + # CSharp code style settings: [*.cs] # Prefer method-like constructs to have a expression-body -csharp_style_expression_bodied_methods = true:none -csharp_style_expression_bodied_constructors = true:none -csharp_style_expression_bodied_operators = true:none +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_operators = true:warning # Prefer property-like constructs to have an expression-body -csharp_style_expression_bodied_properties = true:none -csharp_style_expression_bodied_indexers = true:none -csharp_style_expression_bodied_accessors = true:none - -# Suggest more modern language features when available -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion -csharp_style_throw_expression = true:suggestion -csharp_style_conditional_delegate_call = true:suggestion +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_lambdas = true:warning +csharp_style_expression_bodied_local_functions = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning + +# Null-checking preferences +csharp_style_throw_expression = true:warning +csharp_style_conditional_delegate_call = true:warning + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,volatile,async:suggestion + +# Expression-level preferences +csharp_prefer_braces = true:silent +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:silent +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:warning csharp_prefer_simple_using_statement = true:silent +csharp_style_prefer_not_pattern = true:warning +csharp_style_prefer_switch_expression = true:warning # Disable range operator suggestions csharp_style_prefer_range_operator = false:none csharp_style_prefer_index_operator = false:none -# Newline settings +# New line preferences csharp_new_line_before_open_brace = all csharp_new_line_before_else = true csharp_new_line_before_catch = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true - -# Space settings -csharp_space_after_keywords_in_control_flow_statements = true:suggestion - -# Language settings -csharp_prefer_simple_default_expression = false:none +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true:warning +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false + +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true # IDE0090: Use 'new(...)' dotnet_diagnostic.IDE0090.severity = silent +# RCS1037: Remove trailing white-space. +dotnet_diagnostic.RCS1037.severity = error + # RCS1098: Constant values should be placed on right side of comparisons. dotnet_diagnostic.RCS1098.severity = none @@ -105,4 +157,4 @@ dotnet_diagnostic.RCS1229.severity = none dotnet_diagnostic.RCS1233.severity = none # RCS1234: Duplicate enum value. -dotnet_diagnostic.RCS1234.severity = none +dotnet_diagnostic.RCS1234.severity = none \ No newline at end of file diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index c22fd8f9e..59ac059c9 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -2,6 +2,11 @@ ## Unreleased +- Adds: [Nullable reference type](https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references) annotations ([#2041 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2041)) + - Adds annotations themselves for nullability to everything in the library + - Fixes a few internal edge cases that will now throw proper errors (rather than a downstream null reference) + - Fixes inconsistencies with `null` vs. empty array returns (preferring an not-null empty array in those edge cases) + - Note: does *not* increment a major version (as these are warnings to consumers), because: they're warnings (errors are opt-in), removing obsolete types with a 3.0 rev _would_ be binary breaking (this isn't), and reving to 3.0 would cause binding redirect pain for consumers. Bumping from 2.5 to 2.6 only for this change. ## 2.5.61 diff --git a/src/StackExchange.Redis/ChannelMessageQueue.cs b/src/StackExchange.Redis/ChannelMessageQueue.cs index bc32d84d1..14af669ef 100644 --- a/src/StackExchange.Redis/ChannelMessageQueue.cs +++ b/src/StackExchange.Redis/ChannelMessageQueue.cs @@ -17,14 +17,15 @@ public readonly struct ChannelMessage /// /// The Channel:Message string representation. /// - public override string ToString() => ((string)Channel) + ":" + ((string)Message); + public override string ToString() => ((string?)Channel) + ":" + ((string?)Message); /// public override int GetHashCode() => Channel.GetHashCode() ^ Message.GetHashCode(); /// - public override bool Equals(object obj) => obj is ChannelMessage cm + public override bool Equals(object? obj) => obj is ChannelMessage cm && cm.Channel == Channel && cm.Message == Message; + internal ChannelMessage(ChannelMessageQueue queue, in RedisChannel channel, in RedisValue value) { _queue = queue; @@ -72,12 +73,12 @@ public sealed class ChannelMessageQueue /// The Channel that was subscribed for this queue. /// public RedisChannel Channel { get; } - private RedisSubscriber _parent; + private RedisSubscriber? _parent; /// /// The string representation of this channel. /// - public override string ToString() => (string)Channel; + public override string? ToString() => (string?)Channel; /// /// An awaitable task the indicates completion of the queue (including drain of data). @@ -127,9 +128,9 @@ public bool TryGetCount(out int count) try { var prop = _queue.GetType().GetProperty("ItemsCountForDebugger", BindingFlags.Instance | BindingFlags.NonPublic); - if (prop != null) + if (prop is not null) { - count = (int)prop.GetValue(_queue); + count = (int)prop.GetValue(_queue)!; return true; } } @@ -138,7 +139,7 @@ public bool TryGetCount(out int count) return false; } - private Delegate _onMessageHandler; + private Delegate? _onMessageHandler; private void AssertOnMessage(Delegate handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); @@ -155,12 +156,12 @@ public void OnMessage(Action handler) AssertOnMessage(handler); ThreadPool.QueueUserWorkItem( - state => ((ChannelMessageQueue)state).OnMessageSyncImpl().RedisFireAndForget(), this); + state => ((ChannelMessageQueue)state!).OnMessageSyncImpl().RedisFireAndForget(), this); } private async Task OnMessageSyncImpl() { - var handler = (Action)_onMessageHandler; + var handler = (Action?)_onMessageHandler; while (!Completion.IsCompleted) { ChannelMessage next; @@ -168,21 +169,21 @@ private async Task OnMessageSyncImpl() catch (ChannelClosedException) { break; } // expected catch (Exception ex) { - _parent.multiplexer?.OnInternalError(ex); + _parent?.multiplexer?.OnInternalError(ex); break; } - try { handler(next); } + try { handler?.Invoke(next); } catch { } // matches MessageCompletable } } - internal static void Combine(ref ChannelMessageQueue head, ChannelMessageQueue queue) + internal static void Combine(ref ChannelMessageQueue? head, ChannelMessageQueue queue) { if (queue != null) { // insert at the start of the linked-list - ChannelMessageQueue old; + ChannelMessageQueue? old; do { old = Volatile.Read(ref head); @@ -200,12 +201,15 @@ public void OnMessage(Func handler) AssertOnMessage(handler); ThreadPool.QueueUserWorkItem( - state => ((ChannelMessageQueue)state).OnMessageAsyncImpl().RedisFireAndForget(), this); + state => ((ChannelMessageQueue)state!).OnMessageAsyncImpl().RedisFireAndForget(), this); } - internal static void Remove(ref ChannelMessageQueue head, ChannelMessageQueue queue) + internal static void Remove(ref ChannelMessageQueue? head, ChannelMessageQueue queue) { - if (queue == null) return; + if (queue is null) + { + return; + } bool found; do // if we fail due to a conflict, re-do from start @@ -223,7 +227,7 @@ internal static void Remove(ref ChannelMessageQueue head, ChannelMessageQueue qu } else { - ChannelMessageQueue previous = current; + ChannelMessageQueue? previous = current; current = Volatile.Read(ref previous._next); found = false; do @@ -242,13 +246,13 @@ internal static void Remove(ref ChannelMessageQueue head, ChannelMessageQueue qu } } previous = current; - current = Volatile.Read(ref previous._next); + current = Volatile.Read(ref previous!._next); } while (current != null); } } while (found); } - internal static int Count(ref ChannelMessageQueue head) + internal static int Count(ref ChannelMessageQueue? head) { var current = Volatile.Read(ref head); int count = 0; @@ -270,11 +274,11 @@ internal static void WriteAll(ref ChannelMessageQueue head, in RedisChannel chan } } - private ChannelMessageQueue _next; + private ChannelMessageQueue? _next; private async Task OnMessageAsyncImpl() { - var handler = (Func)_onMessageHandler; + var handler = (Func?)_onMessageHandler; while (!Completion.IsCompleted) { ChannelMessage next; @@ -282,20 +286,20 @@ private async Task OnMessageAsyncImpl() catch (ChannelClosedException) { break; } // expected catch (Exception ex) { - _parent.multiplexer?.OnInternalError(ex); + _parent?.multiplexer?.OnInternalError(ex); break; } try { - var task = handler(next); + var task = handler?.Invoke(next); if (task != null && task.Status != TaskStatus.RanToCompletion) await task.ForAwait(); } catch { } // matches MessageCompletable } } - internal static void MarkAllCompleted(ref ChannelMessageQueue head) + internal static void MarkAllCompleted(ref ChannelMessageQueue? head) { var current = Interlocked.Exchange(ref head, null); while (current != null) @@ -305,13 +309,13 @@ internal static void MarkAllCompleted(ref ChannelMessageQueue head) } } - private void MarkCompleted(Exception error = null) + private void MarkCompleted(Exception? error = null) { _parent = null; _queue.Writer.TryComplete(error); } - internal void UnsubscribeImpl(Exception error = null, CommandFlags flags = CommandFlags.None) + internal void UnsubscribeImpl(Exception? error = null, CommandFlags flags = CommandFlags.None) { var parent = _parent; _parent = null; @@ -322,7 +326,7 @@ internal void UnsubscribeImpl(Exception error = null, CommandFlags flags = Comma _queue.Writer.TryComplete(error); } - internal async Task UnsubscribeAsyncImpl(Exception error = null, CommandFlags flags = CommandFlags.None) + internal async Task UnsubscribeAsyncImpl(Exception? error = null, CommandFlags flags = CommandFlags.None) { var parent = _parent; _parent = null; diff --git a/src/StackExchange.Redis/ClientInfo.cs b/src/StackExchange.Redis/ClientInfo.cs index a38a0f3d2..539f72bce 100644 --- a/src/StackExchange.Redis/ClientInfo.cs +++ b/src/StackExchange.Redis/ClientInfo.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net; @@ -14,7 +15,7 @@ public sealed class ClientInfo /// /// Address (host and port) of the client. /// - public EndPoint Address { get; private set; } + public EndPoint? Address { get; private set; } /// /// Total duration of the connection in seconds. @@ -105,12 +106,12 @@ public sealed class ClientInfo /// /// /// https://redis.io/commands/client-list - public string FlagsRaw { get; private set; } + public string? FlagsRaw { get; private set; } /// /// The host of the client (typically an IP address). /// - public string Host => Format.TryGetHostPort(Address, out string host, out _) ? host : null; + public string? Host => Format.TryGetHostPort(Address, out string? host, out _) ? host : null; /// /// Idle time of the connection in seconds. @@ -120,12 +121,12 @@ public sealed class ClientInfo /// /// Last command played. /// - public string LastCommand { get; private set; } + public string? LastCommand { get; private set; } /// /// The name allocated to this connection, if any. /// - public string Name { get; private set; } + public string? Name { get; private set; } /// /// Number of pattern matching subscriptions. @@ -135,12 +136,12 @@ public sealed class ClientInfo /// /// The port of the client. /// - public int Port => Format.TryGetHostPort(Address, out _, out int port) ? port : 0; + public int Port => Format.TryGetHostPort(Address, out _, out int? port) ? port.Value : 0; /// /// The raw content from redis. /// - public string Raw { get; private set; } + public string? Raw { get; private set; } /// /// Number of channel subscriptions. @@ -179,15 +180,18 @@ public ClientType ClientType } } - internal static ClientInfo[] Parse(string input) + internal static bool TryParse(string? input, [NotNullWhen(true)] out ClientInfo[]? clientList) { - if (input == null) return null; + if (input == null) + { + clientList = null; + return false; + } var clients = new List(); using (var reader = new StringReader(input)) { - string line; - while ((line = reader.ReadLine()) != null) + while (reader.ReadLine() is string line) { var client = new ClientInfo { @@ -203,7 +207,7 @@ internal static ClientInfo[] Parse(string input) switch (key) { - case "addr": client.Address = Format.TryParseEndPoint(value); break; + case "addr" when Format.TryParseEndPoint(value, out var addr): client.Address = addr; break; case "age": client.AgeSeconds = Format.ParseInt32(value); break; case "idle": client.IdleSeconds = Format.ParseInt32(value); break; case "db": client.Database = Format.ParseInt32(value); break; @@ -243,7 +247,8 @@ internal static ClientInfo[] Parse(string input) } } - return clients.ToArray(); + clientList = clients.ToArray(); + return true; } private static void AddFlag(ref ClientFlags value, string raw, ClientFlags toAdd, char token) @@ -258,11 +263,13 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes switch(result.Type) { case ResultType.BulkString: - var raw = result.GetString(); - var clients = Parse(raw); - SetResult(message, clients); - return true; + if (TryParse(raw, out var clients)) + { + SetResult(message, clients); + return true; + } + break; } return false; } diff --git a/src/StackExchange.Redis/ClusterConfiguration.cs b/src/StackExchange.Redis/ClusterConfiguration.cs index e197f38d4..2592e6a22 100644 --- a/src/StackExchange.Redis/ClusterConfiguration.cs +++ b/src/StackExchange.Redis/ClusterConfiguration.cs @@ -68,7 +68,7 @@ public static bool TryParse(string range, out SlotRange value) { if (string.IsNullOrWhiteSpace(range)) { - value = default(SlotRange); + value = default; return false; } int i = range.IndexOf('-'); @@ -89,7 +89,7 @@ public static bool TryParse(string range, out SlotRange value) return true; } } - value = default(SlotRange); + value = default; return false; } @@ -108,7 +108,7 @@ public int CompareTo(SlotRange other) /// See . /// /// The other slot range to compare to. - public override bool Equals(object obj) => obj is SlotRange sRange && Equals(sRange); + public override bool Equals(object? obj) => obj is SlotRange sRange && Equals(sRange); /// /// Indicates whether two ranges are equal. @@ -147,7 +147,7 @@ private static bool TryParseInt16(string s, int offset, int count, out short val } } - int IComparable.CompareTo(object obj) => obj is SlotRange sRange ? CompareTo(sRange) : -1; + int IComparable.CompareTo(object? obj) => obj is SlotRange sRange ? CompareTo(sRange) : -1; } /// @@ -165,8 +165,7 @@ internal ClusterConfiguration(ServerSelectionStrategy serverSelectionStrategy, s Origin = origin; using (var reader = new StringReader(nodes)) { - string line; - while ((line = reader.ReadLine()) != null) + while (reader.ReadLine() is string line) { if (string.IsNullOrWhiteSpace(line)) continue; var node = new ClusterNode(this, line, origin); @@ -225,11 +224,11 @@ internal ClusterConfiguration(ServerSelectionStrategy serverSelectionStrategy, s /// Obtain the node relating to a specified endpoint. /// /// The endpoint to get a cluster node from. - public ClusterNode this[EndPoint endpoint] => endpoint == null + public ClusterNode? this[EndPoint endpoint] => endpoint == null ? null - : nodeLookup.TryGetValue(endpoint, out ClusterNode result) ? result : null; + : nodeLookup.TryGetValue(endpoint, out ClusterNode? result) ? result : null; - internal ClusterNode this[string nodeId] + internal ClusterNode? this[string nodeId] { get { @@ -246,7 +245,7 @@ internal ClusterNode this[string nodeId] /// Gets the node that serves the specified slot. /// /// The slot ID to get a node by. - public ClusterNode GetBySlot(int slot) + public ClusterNode? GetBySlot(int slot) { foreach(var node in Nodes) { @@ -259,7 +258,7 @@ public ClusterNode GetBySlot(int slot) /// Gets the node that serves the specified key's slot. /// /// The key to identify a node by. - public ClusterNode GetBySlot(RedisKey key) => GetBySlot(serverSelectionStrategy.HashSlot(key)); + public ClusterNode? GetBySlot(RedisKey key) => GetBySlot(serverSelectionStrategy.HashSlot(key)); } /// @@ -267,17 +266,11 @@ public ClusterNode GetBySlot(int slot) /// public sealed class ClusterNode : IEquatable, IComparable, IComparable { - private static readonly ClusterNode Dummy = new(); - private readonly ClusterConfiguration configuration; + private IList? children; + private ClusterNode? parent; + private string? toString; - private IList children; - - private ClusterNode parent; - - private string toString; - - internal ClusterNode() { } internal ClusterNode(ClusterConfiguration configuration, string raw, EndPoint origin) { // https://redis.io/commands/cluster-nodes @@ -292,7 +285,10 @@ internal ClusterNode(ClusterConfiguration configuration, string raw, EndPoint or int at = ep.IndexOf('@'); if (at >= 0) ep = ep.Substring(0, at); - EndPoint = Format.TryParseEndPoint(ep); + if (Format.TryParseEndPoint(ep, out var epResult)) + { + EndPoint = epResult; + } if (flags.Contains("myself")) { IsMyself = true; @@ -309,7 +305,7 @@ internal ClusterNode(ClusterConfiguration configuration, string raw, EndPoint or IsNoAddr = flags.Contains("noaddr"); ParentNodeId = string.IsNullOrWhiteSpace(parts[3]) ? null : parts[3]; - List slots = null; + List? slots = null; for (int i = 8; i < parts.Length; i++) { @@ -329,9 +325,9 @@ public IList Children { get { - if (children != null) return children; + if (children is not null) return children; - List nodes = null; + List? nodes = null; foreach (var node in configuration.Nodes) { if (node.ParentNodeId == NodeId) @@ -347,7 +343,7 @@ public IList Children /// /// Gets the endpoint of the current node. /// - public EndPoint EndPoint { get; } + public EndPoint? EndPoint { get; } /// /// Gets whether this is the node which responded to the CLUSTER NODES request. @@ -384,21 +380,12 @@ public IList Children /// /// Gets the parent node of the current node. /// - public ClusterNode Parent - { - get - { - if (parent != null) return parent == Dummy ? null : parent; - ClusterNode found = configuration[ParentNodeId]; - parent = found ?? Dummy; - return found; - } - } + public ClusterNode? Parent => (parent is not null) ? parent = configuration[ParentNodeId!] : null; /// /// Gets the unique node-id of the parent of the current node. /// - public string ParentNodeId { get; } + public string? ParentNodeId { get; } /// /// The configuration as reported by the server. @@ -415,7 +402,7 @@ public ClusterNode Parent /// whether the current instance precedes, follows, or occurs in the same position in the sort order as the other object. /// /// The to compare to. - public int CompareTo(ClusterNode other) + public int CompareTo(ClusterNode? other) { if (other == null) return -1; @@ -433,18 +420,13 @@ public int CompareTo(ClusterNode other) /// See . /// /// The to compare to. - public override bool Equals(object obj) => Equals(obj as ClusterNode); + public override bool Equals(object? obj) => Equals(obj as ClusterNode); /// /// Indicates whether two instances are equivalent. /// /// The to compare to. - public bool Equals(ClusterNode other) - { - if (other == null) return false; - - return ToString() == other.ToString(); // lazy, but effective - plus only computes once - } + public bool Equals(ClusterNode? other) => other is ClusterNode node && ToString() == node.ToString(); /// public override int GetHashCode() => ToString().GetHashCode(); @@ -454,13 +436,12 @@ public bool Equals(ClusterNode other) /// public override string ToString() { - if (toString != null) return toString; + if (toString is not null) return toString; var sb = new StringBuilder().Append(NodeId).Append(" at ").Append(EndPoint); if (IsReplica) { sb.Append(", replica of ").Append(ParentNodeId); - var parent = Parent; - if (parent != null) sb.Append(" at ").Append(parent.EndPoint); + if (Parent is ClusterNode parent) sb.Append(" at ").Append(parent.EndPoint); } var childCount = Children.Count; switch(childCount) @@ -490,6 +471,6 @@ internal bool ServesSlot(int hashSlot) return false; } - int IComparable.CompareTo(object obj) => CompareTo(obj as ClusterNode); + int IComparable.CompareTo(object? obj) => CompareTo(obj as ClusterNode); } } diff --git a/src/StackExchange.Redis/CommandBytes.cs b/src/StackExchange.Redis/CommandBytes.cs index e5e06280a..d9c96a3ab 100644 --- a/src/StackExchange.Redis/CommandBytes.cs +++ b/src/StackExchange.Redis/CommandBytes.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis { private static Encoding Encoding => Encoding.UTF8; - internal unsafe static CommandBytes TrimToFit(string value) + internal static unsafe CommandBytes TrimToFit(string value) { if (string.IsNullOrWhiteSpace(value)) return default; value = value.Trim(); @@ -47,7 +47,7 @@ public override int GetHashCode() return hashCode; } - public override bool Equals(object obj) => obj is CommandBytes cb && Equals(cb); + public override bool Equals(object? obj) => obj is CommandBytes cb && Equals(cb); bool IEquatable.Equals(CommandBytes other) => _0 == other._0 && _1 == other._1 && _2 == other._2 && _3 == other._3; @@ -102,10 +102,10 @@ public unsafe byte this[int index] } } - public unsafe CommandBytes(string value) + public unsafe CommandBytes(string? value) { _0 = _1 = _2 = _3 = 0L; - if (string.IsNullOrEmpty(value)) return; + if (value.IsNullOrEmpty()) return; var len = Encoding.GetByteCount(value); if (len > MaxLength) throw new ArgumentOutOfRangeException($"Command '{value}' exceeds library limit of {MaxLength} bytes"); diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index 042ccb4f9..8d4e0af53 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -101,7 +101,7 @@ public sealed class CommandMap /// Create a new , customizing some commands. /// /// The commands to override. - public static CommandMap Create(Dictionary overrides) + public static CommandMap Create(Dictionary? overrides) { if (overrides == null || overrides.Count == 0) return Default; @@ -113,7 +113,7 @@ public static CommandMap Create(Dictionary overrides) else { // need case insensitive - overrides = new Dictionary(overrides, StringComparer.OrdinalIgnoreCase); + overrides = new Dictionary(overrides, StringComparer.OrdinalIgnoreCase); } return CreateImpl(overrides, null); } @@ -127,9 +127,9 @@ public static CommandMap Create(HashSet commands, bool available = true) { if (available) { - var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); // nix everything - foreach (RedisCommand command in Enum.GetValues(typeof(RedisCommand))) + foreach (RedisCommand command in (RedisCommand[])Enum.GetValues(typeof(RedisCommand))) { dictionary[command.ToString()] = null; } @@ -145,7 +145,7 @@ public static CommandMap Create(HashSet commands, bool available = true) } else { - HashSet exclusions = null; + HashSet? exclusions = null; if (commands != null) { // nix the things that are specified @@ -206,7 +206,7 @@ internal CommandBytes GetBytes(string command) internal bool IsAvailable(RedisCommand command) => !map[(int)command].IsEmpty; - private static CommandMap CreateImpl(Dictionary caseInsensitiveOverrides, HashSet exclusions) + private static CommandMap CreateImpl(Dictionary? caseInsensitiveOverrides, HashSet? exclusions) { var commands = (RedisCommand[])Enum.GetValues(typeof(RedisCommand)); @@ -214,7 +214,7 @@ private static CommandMap CreateImpl(Dictionary caseInsensitiveO for (int i = 0; i < commands.Length; i++) { int idx = (int)commands[i]; - string name = commands[i].ToString(), value = name; + string? name = commands[i].ToString(), value = name; if (exclusions?.Contains(commands[i]) == true) { @@ -222,7 +222,7 @@ private static CommandMap CreateImpl(Dictionary caseInsensitiveO } else { - if (caseInsensitiveOverrides != null && caseInsensitiveOverrides.TryGetValue(name, out string tmp)) + if (caseInsensitiveOverrides != null && caseInsensitiveOverrides.TryGetValue(name, out string? tmp)) { value = tmp; } diff --git a/src/StackExchange.Redis/CommandTrace.cs b/src/StackExchange.Redis/CommandTrace.cs index 000199fb4..fcb9aefdf 100644 --- a/src/StackExchange.Redis/CommandTrace.cs +++ b/src/StackExchange.Redis/CommandTrace.cs @@ -44,13 +44,13 @@ internal CommandTrace(long uniqueId, long time, long duration, RedisValue[] argu /// /// Deduces a link to the redis documentation about the specified command /// - public string GetHelpUrl() + public string? GetHelpUrl() { if (Arguments == null || Arguments.Length == 0) return null; const string BaseUrl = "https://redis.io/commands/"; - string encoded0 = Uri.EscapeDataString(((string)Arguments[0]).ToLowerInvariant()); + string encoded0 = Uri.EscapeDataString(((string)Arguments[0]!).ToLowerInvariant()); if (Arguments.Length > 1) { @@ -62,7 +62,7 @@ public string GetHelpUrl() case "config": case "debug": case "pubsub": - string encoded1 = Uri.EscapeDataString(((string)Arguments[1]).ToLowerInvariant()); + string encoded1 = Uri.EscapeDataString(((string)Arguments[1]!).ToLowerInvariant()); return BaseUrl + encoded0 + "-" + encoded1; } } @@ -84,7 +84,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var subParts = item.GetItems(); if (!subParts[0].TryGetInt64(out long uniqueid) || !subParts[1].TryGetInt64(out long time) || !subParts[2].TryGetInt64(out long duration)) return false; - arr[i++] = new CommandTrace(uniqueid, time, duration, subParts[3].GetItemsAsValues()); + arr[i++] = new CommandTrace(uniqueid, time, duration, subParts[3].GetItemsAsValues()!); } SetResult(message, arr); return true; diff --git a/src/StackExchange.Redis/CompletedDefaultTask.cs b/src/StackExchange.Redis/CompletedDefaultTask.cs index feb35877c..1035cb6a8 100644 --- a/src/StackExchange.Redis/CompletedDefaultTask.cs +++ b/src/StackExchange.Redis/CompletedDefaultTask.cs @@ -4,11 +4,21 @@ namespace StackExchange.Redis { internal static class CompletedTask { - private static readonly Task defaultTask = FromResult(default(T), null); + private static readonly Task defaultTask = FromResult(default(T), null); - public static Task Default(object asyncState) => asyncState == null ? defaultTask : FromResult(default(T), asyncState); + public static Task Default(object? asyncState) => asyncState == null ? defaultTask : FromResult(default(T), asyncState); - public static Task FromResult(T value, object asyncState) + public static Task FromResult(T? value, object? asyncState) + { + if (asyncState == null) return Task.FromResult(value); + // note we do not need to deny exec-sync here; the value will be known + // before we hand it to them + var tcs = TaskSource.Create(asyncState); + tcs.SetResult(value); + return tcs.Task; + } + + public static Task FromDefault(T value, object? asyncState) { if (asyncState == null) return Task.FromResult(value); // note we do not need to deny exec-sync here; the value will be known diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs index caac60f65..85d78de8c 100644 --- a/src/StackExchange.Redis/Condition.cs +++ b/src/StackExchange.Redis/Condition.cs @@ -355,7 +355,7 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value) internal abstract void CheckCommands(CommandMap commandMap); - internal abstract IEnumerable CreateMessages(int db, IResultBox resultBox); + internal abstract IEnumerable CreateMessages(int db, IResultBox? resultBox); internal abstract int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy); internal abstract bool TryValidate(in RawResult result, out bool value); @@ -364,7 +364,7 @@ internal sealed class ConditionProcessor : ResultProcessor { public static readonly ConditionProcessor Default = new(); - public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, RedisValue value = default(RedisValue)) => + public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, RedisValue value = default) => new ConditionMessage(condition, db, flags, command, key, value); public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, in RedisValue value1) => @@ -461,12 +461,12 @@ public ExistsCondition(in RedisKey key, RedisType type, in RedisValue expectedVa } public override string ToString() => - (expectedValue.IsNull ? key.ToString() : ((string)key) + " " + type + " > " + expectedValue) + (expectedValue.IsNull ? key.ToString() : ((string?)key) + " " + type + " > " + expectedValue) + (expectedResult ? " exists" : " does not exists"); internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(cmd); - internal override IEnumerable CreateMessages(int db, IResultBox resultBox) + internal override IEnumerable CreateMessages(int db, IResultBox? resultBox) { yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key); @@ -529,13 +529,13 @@ public EqualsCondition(in RedisKey key, RedisType type, in RedisValue memberName } public override string ToString() => - (memberName.IsNull ? key.ToString() : ((string)key) + " " + type + " > " + memberName) + (memberName.IsNull ? key.ToString() : ((string?)key) + " " + type + " > " + memberName) + (expectedEqual ? " == " : " != ") + expectedValue; internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(cmd); - internal sealed override IEnumerable CreateMessages(int db, IResultBox resultBox) + internal sealed override IEnumerable CreateMessages(int db, IResultBox? resultBox) { yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key); @@ -558,7 +558,7 @@ internal override bool TryValidate(in RawResult result, out bool value) } value = (parsedValue == expectedValue) == expectedEqual; - ConnectionMultiplexer.TraceWithoutContext("actual: " + (string)parsedValue + "; expected: " + (string)expectedValue + + ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsedValue + "; expected: " + (string?)expectedValue + "; wanted: " + (expectedEqual ? "==" : "!=") + "; voting: " + value); return true; @@ -570,7 +570,7 @@ internal override bool TryValidate(in RawResult result, out bool value) case ResultType.Integer: var parsed = result.AsRedisValue(); value = (parsed == expectedValue) == expectedEqual; - ConnectionMultiplexer.TraceWithoutContext("actual: " + (string)parsed + "; expected: " + (string)expectedValue + + ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + (string?)expectedValue + "; wanted: " + (expectedEqual ? "==" : "!=") + "; voting: " + value); return true; } @@ -601,12 +601,12 @@ public ListCondition(in RedisKey key, long index, bool expectedResult, in RedisV } public override string ToString() => - ((string)key) + "[" + index.ToString() + "]" + ((string?)key) + "[" + index.ToString() + "]" + (expectedValue.HasValue ? (expectedResult ? " == " : " != ") + expectedValue.Value : (expectedResult ? " exists" : " does not exist")); internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.LINDEX); - internal sealed override IEnumerable CreateMessages(int db, IResultBox resultBox) + internal sealed override IEnumerable CreateMessages(int db, IResultBox? resultBox) { yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key); @@ -628,7 +628,7 @@ internal override bool TryValidate(in RawResult result, out bool value) if (expectedValue.HasValue) { value = (parsed == expectedValue.Value) == expectedResult; - ConnectionMultiplexer.TraceWithoutContext("actual: " + (string)parsed + "; expected: " + (string)expectedValue.Value + + ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + (string?)expectedValue.Value + "; wanted: " + (expectedResult ? "==" : "!=") + "; voting: " + value); } else @@ -673,13 +673,13 @@ public LengthCondition(in RedisKey key, RedisType type, int compareToResult, lon }; } - public override string ToString() => ((string)key) + " " + type + " length" + GetComparisonString() + expectedLength; + public override string ToString() => ((string?)key) + " " + type + " length" + GetComparisonString() + expectedLength; private string GetComparisonString() => compareToResult == 0 ? " == " : (compareToResult < 0 ? " > " : " < "); internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(cmd); - internal sealed override IEnumerable CreateMessages(int db, IResultBox resultBox) + internal sealed override IEnumerable CreateMessages(int db, IResultBox? resultBox) { yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key); @@ -699,7 +699,7 @@ internal override bool TryValidate(in RawResult result, out bool value) case ResultType.Integer: var parsed = result.AsRedisValue(); value = parsed.IsInteger && (expectedLength.CompareTo((long)parsed) == compareToResult); - ConnectionMultiplexer.TraceWithoutContext("actual: " + (string)parsed + "; expected: " + expectedLength + + ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + expectedLength + "; wanted: " + GetComparisonString() + "; voting: " + value); return true; } @@ -730,13 +730,13 @@ public SortedSetRangeLengthCondition(in RedisKey key, RedisValue min, RedisValue } public override string ToString() => - ((string)key) + " " + RedisType.SortedSet + " range[" + min + ", " + max + "] length" + GetComparisonString() + expectedLength; + ((string?)key) + " " + RedisType.SortedSet + " range[" + min + ", " + max + "] length" + GetComparisonString() + expectedLength; private string GetComparisonString() => compareToResult == 0 ? " == " : (compareToResult < 0 ? " > " : " < "); internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.ZCOUNT); - internal sealed override IEnumerable CreateMessages(int db, IResultBox resultBox) + internal sealed override IEnumerable CreateMessages(int db, IResultBox? resultBox) { yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key); @@ -756,7 +756,7 @@ internal override bool TryValidate(in RawResult result, out bool value) case ResultType.Integer: var parsed = result.AsRedisValue(); value = parsed.IsInteger && (expectedLength.CompareTo((long)parsed) == compareToResult); - ConnectionMultiplexer.TraceWithoutContext("actual: " + (string)parsed + "; expected: " + expectedLength + + ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsed + "; expected: " + expectedLength + "; wanted: " + GetComparisonString() + "; voting: " + value); return true; } @@ -792,7 +792,7 @@ public override string ToString() => internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.ZCOUNT); - internal sealed override IEnumerable CreateMessages(int db, IResultBox resultBox) + internal sealed override IEnumerable CreateMessages(int db, IResultBox? resultBox) { yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key); @@ -811,7 +811,7 @@ internal override bool TryValidate(in RawResult result, out bool value) case ResultType.Integer: var parsedValue = result.AsRedisValue(); value = (parsedValue == expectedValue) == expectedEqual; - ConnectionMultiplexer.TraceWithoutContext("actual: " + (string)parsedValue + "; expected: " + (string)expectedValue + "; wanted: " + (expectedEqual ? "==" : "!=") + "; voting: " + value); + ConnectionMultiplexer.TraceWithoutContext("actual: " + (string?)parsedValue + "; expected: " + (string?)expectedValue + "; wanted: " + (expectedEqual ? "==" : "!=") + "; voting: " + value); return true; } @@ -828,7 +828,7 @@ public sealed class ConditionResult { internal readonly Condition Condition; - private IResultBox resultBox; + private IResultBox? resultBox; private volatile bool wasSatisfied; @@ -845,7 +845,7 @@ internal ConditionResult(Condition condition) internal IEnumerable CreateMessages(int db) => Condition.CreateMessages(db, resultBox); - internal IResultBox GetBox() { return resultBox; } + internal IResultBox? GetBox() => resultBox; internal bool UnwrapBox() { if (resultBox != null) diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index 563d357b6..492dd7270 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -102,7 +102,7 @@ public static void AddProvider(DefaultOptionsProvider provider) /// /// The command-map associated with this configuration. /// - public virtual CommandMap CommandMap => null; + public virtual CommandMap? CommandMap => null; /// /// Channel to use for broadcasting and listening for configuration change notification. @@ -140,7 +140,7 @@ public static void AddProvider(DefaultOptionsProvider provider) /// /// The retry policy to be used for connection reconnects. /// - public virtual IReconnectRetryPolicy ReconnectRetryPolicy => null; + public virtual IReconnectRetryPolicy? ReconnectRetryPolicy => null; /// /// Indicates whether endpoints should be resolved via DNS before connecting. @@ -164,7 +164,7 @@ public static void AddProvider(DefaultOptionsProvider provider) public virtual TimeSpan ConfigCheckInterval => TimeSpan.FromMinutes(1); // We memoize this to reduce cost on re-access - private string defaultClientName; + private string? defaultClientName; /// /// The default client name for a connection, with the library version appended. /// @@ -186,7 +186,7 @@ protected virtual string GetDefaultClientName() => /// /// Name of the machine we're running on, for use in any options. /// - protected static string ComputerName => Environment.MachineName ?? Environment.GetEnvironmentVariable("ComputerName"); + protected static string ComputerName => Environment.MachineName ?? Environment.GetEnvironmentVariable("ComputerName") ?? "Unknown"; /// /// Tries to get the RoleInstance Id if Microsoft.WindowsAzure.ServiceRuntime is loaded. @@ -196,15 +196,15 @@ protected virtual string GetDefaultClientName() => /// Azure, in the default provider? Yes, to maintain existing compatibility/convenience. /// Source != destination here. /// - internal static string TryGetAzureRoleInstanceIdNoThrow() + internal static string? TryGetAzureRoleInstanceIdNoThrow() { - string roleInstanceId; + string? roleInstanceId; try { - Assembly asm = null; + Assembly? asm = null; foreach (var asmb in AppDomain.CurrentDomain.GetAssemblies()) { - if (asmb.GetName().Name.Equals("Microsoft.WindowsAzure.ServiceRuntime")) + if (asmb.GetName()?.Name?.Equals("Microsoft.WindowsAzure.ServiceRuntime") == true) { asm = asmb; break; @@ -216,14 +216,18 @@ internal static string TryGetAzureRoleInstanceIdNoThrow() var type = asm.GetType("Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment"); // https://msdn.microsoft.com/en-us/library/microsoft.windowsazure.serviceruntime.roleenvironment.isavailable.aspx - if (!(bool)type.GetProperty("IsAvailable").GetValue(null, null)) + if (type?.GetProperty("IsAvailable") is not PropertyInfo isAvailableProp + || isAvailableProp.GetValue(null, null) is not bool isAvailableVal + || !isAvailableVal) + { return null; + } var currentRoleInstanceProp = type.GetProperty("CurrentRoleInstance"); - var currentRoleInstanceId = currentRoleInstanceProp.GetValue(null, null); - roleInstanceId = currentRoleInstanceId.GetType().GetProperty("Id").GetValue(currentRoleInstanceId, null).ToString(); + var currentRoleInstanceId = currentRoleInstanceProp?.GetValue(null, null); + roleInstanceId = currentRoleInstanceId?.GetType().GetProperty("Id")?.GetValue(currentRoleInstanceId, null)?.ToString(); - if (string.IsNullOrEmpty(roleInstanceId)) + if (roleInstanceId.IsNullOrEmpty()) { roleInstanceId = null; } @@ -256,9 +260,9 @@ internal static string TryGetAzureRoleInstanceIdNoThrow() /// /// The configured endpoints to determine SSL host from (e.g. from the port). /// The common host, if any, detected from the endpoint collection. - public virtual string GetSslHostFromEndpoints(EndPointCollection endPoints) + public virtual string? GetSslHostFromEndpoints(EndPointCollection endPoints) { - string commonHost = null; + string? commonHost = null; foreach (var endpoint in endPoints) { if (endpoint is DnsEndPoint dnsEndpoint) diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index a1797a2a5..3a746a7d3 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -46,7 +46,7 @@ internal static bool ParseBoolean(string key, string value) internal static Version ParseVersion(string key, string value) { - if (!System.Version.TryParse(value, out Version tmp)) throw new ArgumentOutOfRangeException(key, $"Keyword '{key}' requires a version value; the value '{value}' is not recognised."); + if (!System.Version.TryParse(value, out Version? tmp)) throw new ArgumentOutOfRangeException(key, $"Keyword '{key}' requires a version value; the value '{value}' is not recognised."); return tmp; } @@ -56,7 +56,7 @@ internal static Proxy ParseProxy(string key, string value) return tmp; } - internal static SslProtocols ParseSslProtocols(string key, string value) + internal static SslProtocols ParseSslProtocols(string key, string? value) { //Flags expect commas as separators, but we need to use '|' since commas are already used in the connection string to mean something else value = value?.Replace("|", ","); @@ -66,10 +66,8 @@ internal static SslProtocols ParseSslProtocols(string key, string value) return tmp; } - internal static void Unknown(string key) - { + internal static void Unknown(string key) => throw new ArgumentException($"Keyword '{key}' is not supported.", key); - } internal const string AbortOnConnectFail = "abortConnect", @@ -132,7 +130,7 @@ internal const string public static string TryNormalize(string value) { - if (value != null && normalizedOptions.TryGetValue(value, out string tmp)) + if (value != null && normalizedOptions.TryGetValue(value, out string? tmp)) { return tmp ?? ""; } @@ -140,38 +138,38 @@ public static string TryNormalize(string value) } } - private DefaultOptionsProvider defaultOptions; + private DefaultOptionsProvider? defaultOptions; private bool? allowAdmin, abortOnConnectFail, resolveDns, ssl, checkCertificateRevocation, includeDetailInExceptions, includePerformanceCountersInExceptions; - private string tieBreaker, sslHost, configChannel; + private string? tieBreaker, sslHost, configChannel; - private CommandMap commandMap; + private CommandMap? commandMap; - private Version defaultVersion; + private Version? defaultVersion; private int? keepAlive, asyncTimeout, syncTimeout, connectTimeout, responseTimeout, connectRetry, configCheckSeconds; private Proxy? proxy; - private IReconnectRetryPolicy reconnectRetryPolicy; + private IReconnectRetryPolicy? reconnectRetryPolicy; - private BacklogPolicy backlogPolicy; + private BacklogPolicy? backlogPolicy; /// /// A LocalCertificateSelectionCallback delegate responsible for selecting the certificate used for authentication; note /// that this cannot be specified in the configuration-string. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")] - public event LocalCertificateSelectionCallback CertificateSelection; + public event LocalCertificateSelectionCallback? CertificateSelection; /// /// A RemoteCertificateValidationCallback delegate responsible for validating the certificate supplied by the remote party; note /// that this cannot be specified in the configuration-string. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")] - public event RemoteCertificateValidationCallback CertificateValidation; + public event RemoteCertificateValidationCallback? CertificateValidation; /// /// The default (not explicitly configured) options for this connection, fetched based on our parsed endpoints. @@ -187,7 +185,7 @@ public DefaultOptionsProvider Defaults /// Passed in is the endpoint we're connecting to, which type of connection it is, and the socket itself. /// For example, a specific local IP endpoint could be bound, linger time altered, etc. /// - public Action BeforeSocketConnect { get; set; } + public Action? BeforeSocketConnect { get; set; } internal Func, Task> AfterConnectAsync => Defaults.AfterConnectAsync; @@ -262,7 +260,7 @@ private static RemoteCertificateValidationCallback TrustIssuerCallback(X509Certi { if (issuer == null) throw new ArgumentNullException(nameof(issuer)); - return (object _, X509Certificate certificate, X509Chain __, SslPolicyErrors sslPolicyError) + return (object _, X509Certificate? certificate, X509Chain? __, SslPolicyErrors sslPolicyError) => sslPolicyError == SslPolicyErrors.RemoteCertificateChainErrors && certificate is X509Certificate2 v2 && CheckTrustedIssuer(v2, issuer); @@ -285,7 +283,7 @@ private static bool CheckTrustedIssuer(X509Certificate2 certificateToValidate, X /// /// The client name to use for all connections. /// - public string ClientName { get; set; } + public string? ClientName { get; set; } /// /// The number of times to repeat the initial connect cycle if no servers respond promptly. @@ -413,12 +411,12 @@ public int KeepAlive /// /// The user to use to authenticate with the server. /// - public string User { get; set; } + public string? User { get; set; } /// /// The password to use to authenticate with the server. /// - public string Password { get; set; } + public string? Password { get; set; } /// /// Specifies whether asynchronous operations should be invoked in a way that guarantees their original delivery order. @@ -480,7 +478,7 @@ public int ResponseTimeout /// /// The service name used to resolve a service via sentinel. /// - public string ServiceName { get; set; } + public string? ServiceName { get; set; } /// /// Gets or sets the SocketManager instance to be used with these options. @@ -490,7 +488,7 @@ public int ResponseTimeout /// This is only used when a is created. /// Modifying it afterwards will have no effect on already-created multiplexers. /// - public SocketManager SocketManager { get; set; } + public SocketManager? SocketManager { get; set; } /// /// Indicates whether the connection should be encrypted. @@ -504,7 +502,7 @@ public bool Ssl /// /// The target-host to use when validating SSL certificate; setting a value here enables SSL mode. /// - public string SslHost + public string? SslHost { get => sslHost ?? Defaults.GetSslHostFromEndpoints(EndPoints); set => sslHost = value; @@ -543,14 +541,14 @@ public int WriteBuffer set { } } - internal LocalCertificateSelectionCallback CertificateSelectionCallback + internal LocalCertificateSelectionCallback? CertificateSelectionCallback { get => CertificateSelection; private set => CertificateSelection = value; } // these just rip out the underlying handlers, bypassing the event accessors - needed when creating the SSL stream - internal RemoteCertificateValidationCallback CertificateValidationCallback + internal RemoteCertificateValidationCallback? CertificateValidationCallback { get => CertificateValidation; private set => CertificateValidation = value; @@ -615,8 +613,8 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow configCheckSeconds = configCheckSeconds, responseTimeout = responseTimeout, DefaultDatabase = DefaultDatabase, - ReconnectRetryPolicy = reconnectRetryPolicy, - BacklogPolicy = backlogPolicy, + reconnectRetryPolicy = reconnectRetryPolicy, + backlogPolicy = backlogPolicy, SslProtocols = SslProtocols, checkCertificateRevocation = checkCertificateRevocation, BeforeSocketConnect = BeforeSocketConnect, @@ -694,7 +692,7 @@ public string ToString(bool includePassword) Append(sb, OptionKeys.ConfigChannel, configChannel); Append(sb, OptionKeys.AbortOnConnectFail, abortOnConnectFail); Append(sb, OptionKeys.ResolveDns, resolveDns); - Append(sb, OptionKeys.ChannelPrefix, (string)ChannelPrefix); + Append(sb, OptionKeys.ChannelPrefix, (string?)ChannelPrefix); Append(sb, OptionKeys.ConnectRetry, connectRetry); Append(sb, OptionKeys.Proxy, proxy); Append(sb, OptionKeys.ConfigCheckSeconds, configCheckSeconds); @@ -715,9 +713,9 @@ private static void Append(StringBuilder sb, object value) } } - private static void Append(StringBuilder sb, string prefix, object value) + private static void Append(StringBuilder sb, string prefix, object? value) { - string s = value?.ToString(); + string? s = value?.ToString(); if (!string.IsNullOrWhiteSpace(s)) { if (sb.Length != 0) sb.Append(','); @@ -763,7 +761,7 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) // break it down by commas var arr = configuration.Split(StringSplits.Comma); - Dictionary map = null; + Dictionary? map = null; foreach (var paddedOption in arr) { var option = paddedOption.Trim(); @@ -860,7 +858,7 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) var cmdName = option.Substring(1, idx - 1); if (Enum.TryParse(cmdName, true, out RedisCommand cmd)) { - map ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + map ??= new Dictionary(StringComparer.OrdinalIgnoreCase); map[cmdName] = value; } } @@ -873,8 +871,10 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) } else { - var ep = Format.TryParseEndPoint(option); - if (ep != null && !EndPoints.Contains(ep)) EndPoints.Add(ep); + if (Format.TryParseEndPoint(option, out var ep) && !EndPoints.Contains(ep)) + { + EndPoints.Add(ep); + } } } if (map != null && map.Count != 0) diff --git a/src/StackExchange.Redis/ConnectionFailedEventArgs.cs b/src/StackExchange.Redis/ConnectionFailedEventArgs.cs index 0ec804f88..01f9ff408 100644 --- a/src/StackExchange.Redis/ConnectionFailedEventArgs.cs +++ b/src/StackExchange.Redis/ConnectionFailedEventArgs.cs @@ -9,9 +9,9 @@ namespace StackExchange.Redis /// public class ConnectionFailedEventArgs : EventArgs, ICompletable { - private readonly EventHandler handler; + private readonly EventHandler? handler; private readonly object sender; - internal ConnectionFailedEventArgs(EventHandler handler, object sender, EndPoint endPoint, ConnectionType connectionType, ConnectionFailureType failureType, Exception exception, string physicalName) + internal ConnectionFailedEventArgs(EventHandler? handler, object sender, EndPoint? endPoint, ConnectionType connectionType, ConnectionFailureType failureType, Exception? exception, string? physicalName) { this.handler = handler; this.sender = sender; @@ -46,12 +46,12 @@ public ConnectionFailedEventArgs(object sender, EndPoint endPoint, ConnectionTyp /// /// Gets the failing server-endpoint. /// - public EndPoint EndPoint { get; } + public EndPoint? EndPoint { get; } /// /// Gets the exception if available (this can be null). /// - public Exception Exception { get; } + public Exception? Exception { get; } /// /// The type of failure. @@ -66,6 +66,6 @@ void ICompletable.AppendStormLog(StringBuilder sb) => /// /// Returns the physical name of the connection. /// - public override string ToString() => _physicalName ?? base.ToString(); + public override string ToString() => _physicalName; } } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Events.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Events.cs index 4824bb4ce..0a8b95be5 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Events.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Events.cs @@ -10,8 +10,8 @@ public partial class ConnectionMultiplexer /// /// Raised whenever a physical connection fails. /// - public event EventHandler ConnectionFailed; - internal void OnConnectionFailed(EndPoint endpoint, ConnectionType connectionType, ConnectionFailureType failureType, Exception exception, bool reconfigure, string physicalName) + public event EventHandler? ConnectionFailed; + internal void OnConnectionFailed(EndPoint endpoint, ConnectionType connectionType, ConnectionFailureType failureType, Exception exception, bool reconfigure, string? physicalName) { if (_isDisposed) return; var handler = ConnectionFailed; @@ -28,8 +28,8 @@ internal void OnConnectionFailed(EndPoint endpoint, ConnectionType connectionTyp /// /// Raised whenever an internal error occurs (this is primarily for debugging). /// - public event EventHandler InternalError; - internal void OnInternalError(Exception exception, EndPoint endpoint = null, ConnectionType connectionType = ConnectionType.None, [CallerMemberName] string origin = null) + public event EventHandler? InternalError; + internal void OnInternalError(Exception exception, EndPoint? endpoint = null, ConnectionType connectionType = ConnectionType.None, [CallerMemberName] string? origin = null) { try { @@ -50,8 +50,8 @@ internal void OnInternalError(Exception exception, EndPoint endpoint = null, Con /// /// Raised whenever a physical connection is established. /// - public event EventHandler ConnectionRestored; - internal void OnConnectionRestored(EndPoint endpoint, ConnectionType connectionType, string physicalName) + public event EventHandler? ConnectionRestored; + internal void OnConnectionRestored(EndPoint endpoint, ConnectionType connectionType, string? physicalName) { if (_isDisposed) return; var handler = ConnectionRestored; @@ -65,17 +65,17 @@ internal void OnConnectionRestored(EndPoint endpoint, ConnectionType connectionT /// /// Raised when configuration changes are detected. /// - public event EventHandler ConfigurationChanged; + public event EventHandler? ConfigurationChanged; internal void OnConfigurationChanged(EndPoint endpoint) => OnEndpointChanged(endpoint, ConfigurationChanged); /// /// Raised when nodes are explicitly requested to reconfigure via broadcast. /// This usually means primary/replica changes. /// - public event EventHandler ConfigurationChangedBroadcast; + public event EventHandler? ConfigurationChangedBroadcast; internal void OnConfigurationChangedBroadcast(EndPoint endpoint) => OnEndpointChanged(endpoint, ConfigurationChangedBroadcast); - private void OnEndpointChanged(EndPoint endpoint, EventHandler handler) + private void OnEndpointChanged(EndPoint endpoint, EventHandler? handler) { if (_isDisposed) return; if (handler != null) @@ -87,15 +87,15 @@ private void OnEndpointChanged(EndPoint endpoint, EventHandler /// Raised when server indicates a maintenance event is going to happen. /// - public event EventHandler ServerMaintenanceEvent; + public event EventHandler? ServerMaintenanceEvent; internal void OnServerMaintenanceEvent(ServerMaintenanceEvent e) => ServerMaintenanceEvent?.Invoke(this, e); /// /// Raised when a hash-slot has been relocated. /// - public event EventHandler HashSlotMoved; - internal void OnHashSlotMoved(int hashSlot, EndPoint old, EndPoint @new) + public event EventHandler? HashSlotMoved; + internal void OnHashSlotMoved(int hashSlot, EndPoint? old, EndPoint @new) { var handler = HashSlotMoved; if (handler != null) @@ -107,7 +107,7 @@ internal void OnHashSlotMoved(int hashSlot, EndPoint old, EndPoint @new) /// /// Raised when a server replied with an error message. /// - public event EventHandler ErrorMessage; + public event EventHandler? ErrorMessage; internal void OnErrorMessage(EndPoint endpoint, string message) { if (_isDisposed) return; diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.ExportConfiguration.cs b/src/StackExchange.Redis/ConnectionMultiplexer.ExportConfiguration.cs index d9fafdea6..55c8deefb 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.ExportConfiguration.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.ExportConfiguration.cs @@ -113,7 +113,7 @@ private static void Write(ZipArchive zip, string name, Task task, Action x.Message))); + writer.WriteLine(string.Join(", ", task.Exception!.InnerExceptions.Select(x => x.Message))); break; default: writer.WriteLine(status.ToString()); @@ -132,9 +132,10 @@ private static void WriteNormalizingLineEndings(string source, StreamWriter writ { using (var reader = new StringReader(source)) { - string line; - while ((line = reader.ReadLine()) != null) + while (reader.ReadLine() is string line) + { writer.WriteLine(line); // normalize line endings + } } } } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs index 90477466f..e50498f13 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis; public partial class ConnectionMultiplexer { - private Func _profilingSessionProvider; + private Func? _profilingSessionProvider; /// /// Register a callback to provide an on-demand ambient session provider based on the diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs b/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs index e898b78a5..b9ae3a15a 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs @@ -1,9 +1,12 @@ -namespace StackExchange.Redis; +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis; public partial class ConnectionMultiplexer { - internal SocketManager SocketManager { get; private set; } + internal SocketManager? SocketManager { get; private set; } + [MemberNotNull(nameof(SocketManager))] private void OnCreateReaderWriter(ConfigurationOptions configuration) { SocketManager = configuration.SocketManager ?? GetDefaultSocketManager(); diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs index 289b0a9d3..f1c50a56a 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs @@ -11,16 +11,16 @@ namespace StackExchange.Redis; public partial class ConnectionMultiplexer { - internal EndPoint currentSentinelPrimaryEndPoint; - internal Timer sentinelPrimaryReconnectTimer; + internal EndPoint? currentSentinelPrimaryEndPoint; + internal Timer? sentinelPrimaryReconnectTimer; internal Dictionary sentinelConnectionChildren = new Dictionary(); - internal ConnectionMultiplexer sentinelConnection; + internal ConnectionMultiplexer? sentinelConnection; /// /// Initializes the connection as a Sentinel connection and adds the necessary event handlers to track changes to the managed primaries. /// /// The writer to log to, if any. - internal void InitializeSentinel(LogProxy logProxy) + internal void InitializeSentinel(LogProxy? logProxy) { if (ServerSelectionStrategy.ServerType != ServerType.Sentinel) { @@ -32,10 +32,11 @@ internal void InitializeSentinel(LogProxy logProxy) if (sub.SubscribedEndpoint("+switch-master") == null) { - sub.Subscribe("+switch-master", (_, message) => + sub.Subscribe("+switch-master", (__, message) => { - string[] messageParts = ((string)message).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - EndPoint switchBlame = Format.TryParseEndPoint(string.Format("{0}:{1}", messageParts[1], messageParts[2])); + string[] messageParts = ((string)message!).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + // We don't care about the result of this - we're just trying + _ = Format.TryParseEndPoint(string.Format("{0}:{1}", messageParts[1], messageParts[2]), out var switchBlame); lock (sentinelConnectionChildren) { @@ -63,17 +64,15 @@ internal void InitializeSentinel(LogProxy logProxy) // If we lose connection to a sentinel server, // we need to reconfigure to make sure we still have a subscription to the +switch-master channel ConnectionFailed += (sender, e) => - { // Reconfigure to get subscriptions back online ReconfigureAsync(first: false, reconfigureAll: true, logProxy, e.EndPoint, "Lost sentinel connection", false).Wait(); - }; // Subscribe to new sentinels being added if (sub.SubscribedEndpoint("+sentinel") == null) { sub.Subscribe("+sentinel", (_, message) => { - string[] messageParts = ((string)message).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + string[] messageParts = ((string)message!).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); UpdateSentinelAddressList(messageParts[0]); }, CommandFlags.FireAndForget); } @@ -84,7 +83,7 @@ internal void InitializeSentinel(LogProxy logProxy) /// /// The string configuration to use for this multiplexer. /// The to log to. - public static ConnectionMultiplexer SentinelConnect(string configuration, TextWriter log = null) => + public static ConnectionMultiplexer SentinelConnect(string configuration, TextWriter? log = null) => SentinelConnect(ConfigurationOptions.Parse(configuration), log); /// @@ -92,7 +91,7 @@ public static ConnectionMultiplexer SentinelConnect(string configuration, TextWr /// /// The string configuration to use for this multiplexer. /// The to log to. - public static Task SentinelConnectAsync(string configuration, TextWriter log = null) => + public static Task SentinelConnectAsync(string configuration, TextWriter? log = null) => SentinelConnectAsync(ConfigurationOptions.Parse(configuration), log); /// @@ -100,9 +99,11 @@ public static Task SentinelConnectAsync(string configurat /// /// The configuration options to use for this multiplexer. /// The to log to. - public static ConnectionMultiplexer SentinelConnect(ConfigurationOptions configuration, TextWriter log = null) + public static ConnectionMultiplexer SentinelConnect(ConfigurationOptions configuration, TextWriter? log = null) { SocketConnection.AssertDependencies(); + Validate(configuration); + return ConnectImpl(configuration, log, ServerType.Sentinel); } @@ -111,9 +112,11 @@ public static ConnectionMultiplexer SentinelConnect(ConfigurationOptions configu /// /// The configuration options to use for this multiplexer. /// The to log to. - public static Task SentinelConnectAsync(ConfigurationOptions configuration, TextWriter log = null) + public static Task SentinelConnectAsync(ConfigurationOptions configuration, TextWriter? log = null) { SocketConnection.AssertDependencies(); + Validate(configuration); + return ConnectImplAsync(configuration, log, ServerType.Sentinel); } @@ -123,7 +126,7 @@ public static Task SentinelConnectAsync(ConfigurationOpti /// /// The configuration options to use for this multiplexer. /// The to log to. - private static ConnectionMultiplexer SentinelPrimaryConnect(ConfigurationOptions configuration, TextWriter log = null) + private static ConnectionMultiplexer SentinelPrimaryConnect(ConfigurationOptions configuration, TextWriter? log = null) { var sentinelConnection = SentinelConnect(configuration, log); @@ -140,7 +143,7 @@ private static ConnectionMultiplexer SentinelPrimaryConnect(ConfigurationOptions /// /// The configuration options to use for this multiplexer. /// The to log to. - private static async Task SentinelPrimaryConnectAsync(ConfigurationOptions configuration, TextWriter log = null) + private static async Task SentinelPrimaryConnectAsync(ConfigurationOptions configuration, TextWriter? log = null) { var sentinelConnection = await SentinelConnectAsync(configuration, log).ForAwait(); @@ -156,7 +159,7 @@ private static async Task SentinelPrimaryConnectAsync(Con /// /// The configuration to be used when connecting to the primary. /// The writer to log to, if any. - public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions config, TextWriter log = null) + public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions config, TextWriter? log = null) { if (ServerSelectionStrategy.ServerType != ServerType.Sentinel) { @@ -164,35 +167,44 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co "Sentinel: The ConnectionMultiplexer is not a Sentinel connection. Detected as: " + ServerSelectionStrategy.ServerType); } - if (string.IsNullOrEmpty(config.ServiceName)) + var serviceName = config.ServiceName; + if (serviceName.IsNullOrEmpty()) { throw new ArgumentException("A ServiceName must be specified."); } lock (sentinelConnectionChildren) { - if (sentinelConnectionChildren.TryGetValue(config.ServiceName, out var sentinelConnectionChild) && !sentinelConnectionChild.IsDisposed) + if (sentinelConnectionChildren.TryGetValue(serviceName, out var sentinelConnectionChild) && !sentinelConnectionChild.IsDisposed) return sentinelConnectionChild; } bool success = false; - ConnectionMultiplexer connection = null; + ConnectionMultiplexer? connection = null; var sw = ValueStopwatch.StartNew(); do { - // Get an initial endpoint - try twice - EndPoint newPrimaryEndPoint = GetConfiguredPrimaryForService(config.ServiceName) - ?? GetConfiguredPrimaryForService(config.ServiceName); + // Sentinel has some fun race behavior internally - give things a few shots for a quicker overall connect. + const int queryAttempts = 2; + + EndPoint? newPrimaryEndPoint = null; + for (int i = 0; i < queryAttempts && newPrimaryEndPoint is null; i++) + { + newPrimaryEndPoint = GetConfiguredPrimaryForService(serviceName); + } - if (newPrimaryEndPoint == null) + if (newPrimaryEndPoint is null) { throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, $"Sentinel: Failed connecting to configured primary for service: {config.ServiceName}"); } - EndPoint[] replicaEndPoints = GetReplicasForService(config.ServiceName) - ?? GetReplicasForService(config.ServiceName); + EndPoint[]? replicaEndPoints = null; + for (int i = 0; i < queryAttempts && replicaEndPoints is null; i++) + { + replicaEndPoints = GetReplicasForService(serviceName); + } // Replace the primary endpoint, if we found another one // If not, assume the last state is the best we have and minimize the race @@ -206,16 +218,19 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co config.EndPoints.TryAdd(newPrimaryEndPoint); } - foreach (var replicaEndPoint in replicaEndPoints) + if (replicaEndPoints is not null) { - config.EndPoints.TryAdd(replicaEndPoint); + foreach (var replicaEndPoint in replicaEndPoints) + { + config.EndPoints.TryAdd(replicaEndPoint); + } } connection = ConnectImpl(config, log); // verify role is primary according to: // https://redis.io/topics/sentinel-clients - if (connection.GetServer(newPrimaryEndPoint)?.Role().Value == RedisLiterals.master) + if (connection.GetServer(newPrimaryEndPoint)?.Role()?.Value == RedisLiterals.master) { success = true; break; @@ -238,7 +253,7 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co lock (sentinelConnectionChildren) { - sentinelConnectionChildren[connection.RawConfig.ServiceName] = connection; + sentinelConnectionChildren[serviceName] = connection; } // Perform the initial switchover @@ -248,9 +263,12 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co } [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "We don't care.")] - internal void OnManagedConnectionRestored(object sender, ConnectionFailedEventArgs e) + internal void OnManagedConnectionRestored(object? sender, ConnectionFailedEventArgs e) { - ConnectionMultiplexer connection = (ConnectionMultiplexer)sender; + if (sender is not ConnectionMultiplexer connection) + { + return; // This should never happen - called from non-nullable ConnectionFailedEventArgs + } var oldTimer = Interlocked.Exchange(ref connection.sentinelPrimaryReconnectTimer, null); oldTimer?.Dispose(); @@ -289,9 +307,13 @@ internal void OnManagedConnectionRestored(object sender, ConnectionFailedEventAr } [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "We don't care.")] - internal void OnManagedConnectionFailed(object sender, ConnectionFailedEventArgs e) + internal void OnManagedConnectionFailed(object? sender, ConnectionFailedEventArgs e) { - ConnectionMultiplexer connection = (ConnectionMultiplexer)sender; + if (sender is not ConnectionMultiplexer connection) + { + return; // This should never happen - called from non-nullable ConnectionFailedEventArgs + } + // Periodically check to see if we can reconnect to the proper primary. // This is here in case we lost our subscription to a good sentinel instance // or if we miss the published primary change. @@ -315,7 +337,7 @@ internal void OnManagedConnectionFailed(object sender, ConnectionFailedEventArgs } } - internal EndPoint GetConfiguredPrimaryForService(string serviceName) => + internal EndPoint? GetConfiguredPrimaryForService(string serviceName) => GetServerSnapshot() .ToArray() .Where(s => s.ServerType == ServerType.Sentinel) @@ -327,7 +349,7 @@ internal EndPoint GetConfiguredPrimaryForService(string serviceName) => }) .FirstOrDefault(r => r != null); - internal EndPoint[] GetReplicasForService(string serviceName) => + internal EndPoint[]? GetReplicasForService(string serviceName) => GetServerSnapshot() .ToArray() .Where(s => s.ServerType == ServerType.Sentinel) @@ -345,33 +367,40 @@ internal EndPoint[] GetReplicasForService(string serviceName) => /// The endpoint responsible for the switch. /// The connection that should be switched over to a new primary endpoint. /// The writer to log to, if any. - internal void SwitchPrimary(EndPoint switchBlame, ConnectionMultiplexer connection, TextWriter log = null) + internal void SwitchPrimary(EndPoint? switchBlame, ConnectionMultiplexer connection, TextWriter? log = null) { if (log == null) log = TextWriter.Null; using (var logProxy = LogProxy.TryCreate(log)) { - string serviceName = connection.RawConfig.ServiceName; + if (connection.RawConfig.ServiceName is not string serviceName) + { + logProxy?.WriteLine("Service name not defined."); + return; + } // Get new primary - try twice EndPoint newPrimaryEndPoint = GetConfiguredPrimaryForService(serviceName) - ?? GetConfiguredPrimaryForService(serviceName) - ?? throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, + ?? GetConfiguredPrimaryForService(serviceName) + ?? throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, $"Sentinel: Failed connecting to switch primary for service: {serviceName}"); connection.currentSentinelPrimaryEndPoint = newPrimaryEndPoint; if (!connection.servers.Contains(newPrimaryEndPoint)) { - EndPoint[] replicaEndPoints = GetReplicasForService(serviceName) - ?? GetReplicasForService(serviceName); + EndPoint[]? replicaEndPoints = GetReplicasForService(serviceName) + ?? GetReplicasForService(serviceName); connection.servers.Clear(); connection.EndPoints.Clear(); connection.EndPoints.TryAdd(newPrimaryEndPoint); - foreach (var replicaEndPoint in replicaEndPoints) + if (replicaEndPoints is not null) { - connection.EndPoints.TryAdd(replicaEndPoint); + foreach (var replicaEndPoint in replicaEndPoints) + { + connection.EndPoints.TryAdd(replicaEndPoint); + } } Trace($"Switching primary to {newPrimaryEndPoint}"); // Trigger a reconfigure diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.StormLog.cs b/src/StackExchange.Redis/ConnectionMultiplexer.StormLog.cs index 2e1ad1b5f..a32687b5d 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.StormLog.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.StormLog.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis; public partial class ConnectionMultiplexer { internal int haveStormLog = 0; - internal string stormLogSnapshot; + internal string? stormLogSnapshot; /// /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time). /// Set to a negative value to disable this feature. @@ -15,7 +15,7 @@ public partial class ConnectionMultiplexer /// /// Obtains the log of unusual busy patterns. /// - public string GetStormLog() => Volatile.Read(ref stormLogSnapshot); + public string? GetStormLog() => Volatile.Read(ref stormLogSnapshot); /// /// Resets the log of unusual busy patterns. diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Threading.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Threading.cs index 3be1e0704..a4ad1a025 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Threading.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Threading.cs @@ -6,18 +6,18 @@ namespace StackExchange.Redis; public partial class ConnectionMultiplexer { - private static readonly WaitCallback s_CompleteAsWorker = s => ((ICompletable)s).TryComplete(true); + private static readonly WaitCallback s_CompleteAsWorker = s => ((ICompletable)s!).TryComplete(true); internal static void CompleteAsWorker(ICompletable completable) { - if (completable != null) + if (completable is not null) { ThreadPool.QueueUserWorkItem(s_CompleteAsWorker, completable); } } - internal static bool TryCompleteHandler(EventHandler handler, object sender, T args, bool isAsync) where T : EventArgs, ICompletable + internal static bool TryCompleteHandler(EventHandler? handler, object sender, T args, bool isAsync) where T : EventArgs, ICompletable { - if (handler == null) return true; + if (handler is null) return true; if (isAsync) { if (handler.IsSingle()) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Verbose.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Verbose.cs index bd06bab44..e4746963b 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Verbose.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Verbose.cs @@ -7,35 +7,35 @@ namespace StackExchange.Redis; public partial class ConnectionMultiplexer { - internal event Action MessageFaulted; - internal event Action Closing; - internal event Action PreTransactionExec, TransactionLog, InfoMessage; - internal event Action Connecting; - internal event Action Resurrecting; + internal event Action? MessageFaulted; + internal event Action? Closing; + internal event Action? PreTransactionExec, TransactionLog, InfoMessage; + internal event Action? Connecting; + internal event Action? Resurrecting; - partial void OnTrace(string message, string category); - static partial void OnTraceWithoutContext(string message, string category); + partial void OnTrace(string message, string? category); + static partial void OnTraceWithoutContext(string message, string? category); [Conditional("VERBOSE")] - internal void Trace(string message, [CallerMemberName] string category = null) => OnTrace(message, category); + internal void Trace(string message, [CallerMemberName] string? category = null) => OnTrace(message, category); [Conditional("VERBOSE")] - internal void Trace(bool condition, string message, [CallerMemberName] string category = null) + internal void Trace(bool condition, string message, [CallerMemberName] string? category = null) { if (condition) OnTrace(message, category); } [Conditional("VERBOSE")] - internal static void TraceWithoutContext(string message, [CallerMemberName] string category = null) => OnTraceWithoutContext(message, category); + internal static void TraceWithoutContext(string message, [CallerMemberName] string? category = null) => OnTraceWithoutContext(message, category); [Conditional("VERBOSE")] - internal static void TraceWithoutContext(bool condition, string message, [CallerMemberName] string category = null) + internal static void TraceWithoutContext(bool condition, string message, [CallerMemberName] string? category = null) { if (condition) OnTraceWithoutContext(message, category); } [Conditional("VERBOSE")] - internal void OnMessageFaulted(Message msg, Exception fault, [CallerMemberName] string origin = default, [CallerFilePath] string path = default, [CallerLineNumber] int lineNumber = default) => + internal void OnMessageFaulted(Message? msg, Exception? fault, [CallerMemberName] string? origin = default, [CallerFilePath] string? path = default, [CallerLineNumber] int lineNumber = default) => MessageFaulted?.Invoke(msg?.CommandAndKey, fault, $"{origin} ({path}#{lineNumber})"); [Conditional("VERBOSE")] @@ -48,7 +48,7 @@ internal void OnMessageFaulted(Message msg, Exception fault, [CallerMemberName] internal void OnConnecting(EndPoint endpoint, ConnectionType connectionType) => Connecting?.Invoke(endpoint, connectionType); [Conditional("VERBOSE")] - internal void OnResurrecting(EndPoint endpoint, ConnectionType connectionType) => Resurrecting.Invoke(endpoint, connectionType); + internal void OnResurrecting(EndPoint endpoint, ConnectionType connectionType) => Resurrecting?.Invoke(endpoint, connectionType); [Conditional("VERBOSE")] internal void OnPreTransactionExec(Message message) => PreTransactionExec?.Invoke(message.CommandAndKey); diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index baa2ebb05..484f802f6 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; @@ -24,7 +25,7 @@ public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplex internal const int MillisecondsPerHeartbeat = 1000; // This gets accessed for every received event; let's make sure we can process it "raw" - internal readonly byte[] ConfigurationChangedChannel; + internal readonly byte[]? ConfigurationChangedChannel; // Unique identifier used when tracing internal readonly byte[] UniqueId = Guid.NewGuid().ToByteArray(); @@ -33,8 +34,8 @@ public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplex /// internal int _connectAttemptCount = 0, _connectCompletedCount = 0, _connectionCloseCount = 0; private long syncTimeouts, fireAndForgets, asyncTimeouts; - private string failureMessage, activeConfigCause; - private IDisposable pulse; + private string? failureMessage, activeConfigCause; + private IDisposable? pulse; private readonly Hashtable servers = new Hashtable(); private volatile ServerSnapshot _serverSnapshot = ServerSnapshot.Empty; @@ -46,7 +47,7 @@ public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplex internal EndPointCollection EndPoints { get; } internal ConfigurationOptions RawConfig { get; } internal ServerSelectionStrategy ServerSelectionStrategy { get; } - internal Exception LastException { get; set; } + internal Exception? LastException { get; set; } ConfigurationOptions IInternalConnectionMultiplexer.RawConfig => RawConfig; @@ -168,7 +169,7 @@ private ConnectionMultiplexer(ConfigurationOptions configuration, ServerType? se lastHeartbeatTicks = Environment.TickCount; } - private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions configuration, LogProxy log, ServerType? serverType, out EventHandler connectHandler) + private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions configuration, LogProxy? log, ServerType? serverType, out EventHandler? connectHandler) { var muxer = new ConnectionMultiplexer(configuration, serverType); connectHandler = null; @@ -183,7 +184,7 @@ private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions conf { var ex = a.Exception; log?.WriteLine($"Connection failed: {Format.ToString(a.EndPoint)} ({a.ConnectionType}, {a.FailureType}): {ex?.Message ?? "(unknown)"}"); - while ((ex = ex.InnerException) != null) + while ((ex = ex?.InnerException) != null) { log?.WriteLine($"> {ex.Message}"); } @@ -210,7 +211,7 @@ public ServerCounters GetCounters() return counters; } - internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOptions options, LogProxy log) + internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOptions options, LogProxy? log) { _ = server ?? throw new ArgumentNullException(nameof(server)); @@ -469,7 +470,7 @@ private bool WaitAllIgnoreErrors(Task[] tasks) return false; } - private static async Task WaitAllIgnoreErrorsAsync(string name, Task[] tasks, int timeoutMilliseconds, LogProxy log, [CallerMemberName] string caller = null, [CallerLineNumber] int callerLineNumber = 0) + private static async Task WaitAllIgnoreErrorsAsync(string name, Task[] tasks, int timeoutMilliseconds, LogProxy? log, [CallerMemberName] string? caller = null, [CallerLineNumber] int callerLineNumber = 0) { _ = tasks ?? throw new ArgumentNullException(nameof(tasks)); if (tasks.Length == 0) @@ -483,16 +484,16 @@ private static async Task WaitAllIgnoreErrorsAsync(string name, Task[] tas return true; } - static void LogWithThreadPoolStats(LogProxy log, string message, out int busyWorkerCount) + static void LogWithThreadPoolStats(LogProxy? log, string message, out int busyWorkerCount) { busyWorkerCount = 0; - if (log != null) + if (log is not null) { var sb = new StringBuilder(); sb.Append(message); - busyWorkerCount = PerfCounterHelper.GetThreadPoolStats(out string iocp, out string worker, out string workItems); + busyWorkerCount = PerfCounterHelper.GetThreadPoolStats(out string iocp, out string worker, out string? workItems); sb.Append(", IOCP: ").Append(iocp).Append(", WORKER: ").Append(worker); - if (workItems != null) + if (workItems is not null) { sb.Append(", POOL: ").Append(workItems); } @@ -564,7 +565,7 @@ private static bool AllComplete(Task[] tasks) /// /// The string configuration to use for this multiplexer. /// The to log to. - public static Task ConnectAsync(string configuration, TextWriter log = null) => + public static Task ConnectAsync(string configuration, TextWriter? log = null) => ConnectAsync(ConfigurationOptions.Parse(configuration), log); /// @@ -573,7 +574,7 @@ public static Task ConnectAsync(string configuration, Tex /// The string configuration to use for this multiplexer. /// Action to further modify the parsed configuration options. /// The to log to. - public static Task ConnectAsync(string configuration, Action configure, TextWriter log = null) => + public static Task ConnectAsync(string configuration, Action configure, TextWriter? log = null) => ConnectAsync(ConfigurationOptions.Parse(configuration).Apply(configure), log); /// @@ -582,21 +583,21 @@ public static Task ConnectAsync(string configuration, Act /// The configuration options to use for this multiplexer. /// The to log to. /// Note: For Sentinel, do not specify a - this is handled automatically. - public static Task ConnectAsync(ConfigurationOptions configuration, TextWriter log = null) + public static Task ConnectAsync(ConfigurationOptions configuration, TextWriter? log = null) { SocketConnection.AssertDependencies(); + Validate(configuration); - return configuration?.IsSentinel == true + return configuration.IsSentinel ? SentinelPrimaryConnectAsync(configuration, log) : ConnectImplAsync(configuration, log); } - private static async Task ConnectImplAsync(ConfigurationOptions configuration, TextWriter log, ServerType? serverType = null) + private static async Task ConnectImplAsync(ConfigurationOptions configuration, TextWriter? log = null, ServerType? serverType = null) { - Validate(configuration); - IDisposable killMe = null; - EventHandler connectHandler = null; - ConnectionMultiplexer muxer = null; + IDisposable? killMe = null; + EventHandler? connectHandler = null; + ConnectionMultiplexer? muxer = null; using var logProxy = LogProxy.TryCreate(log); try { @@ -628,12 +629,12 @@ private static async Task ConnectImplAsync(ConfigurationO } finally { - if (connectHandler != null) muxer.ConnectionFailed -= connectHandler; + if (connectHandler != null && muxer != null) muxer.ConnectionFailed -= connectHandler; if (killMe != null) try { killMe.Dispose(); } catch { } } } - private static void Validate(ConfigurationOptions config) + private static void Validate([NotNull] ConfigurationOptions? config) { if (config is null) { @@ -650,7 +651,7 @@ private static void Validate(ConfigurationOptions config) /// /// The string configuration to use for this multiplexer. /// The to log to. - public static ConnectionMultiplexer Connect(string configuration, TextWriter log = null) => + public static ConnectionMultiplexer Connect(string configuration, TextWriter? log = null) => Connect(ConfigurationOptions.Parse(configuration), log); /// @@ -659,7 +660,7 @@ public static ConnectionMultiplexer Connect(string configuration, TextWriter log /// The string configuration to use for this multiplexer. /// Action to further modify the parsed configuration options. /// The to log to. - public static ConnectionMultiplexer Connect(string configuration, Action configure, TextWriter log = null) => + public static ConnectionMultiplexer Connect(string configuration, Action configure, TextWriter? log = null) => Connect(ConfigurationOptions.Parse(configuration).Apply(configure), log); /// @@ -668,21 +669,21 @@ public static ConnectionMultiplexer Connect(string configuration, ActionThe configuration options to use for this multiplexer. /// The to log to. /// Note: For Sentinel, do not specify a - this is handled automatically. - public static ConnectionMultiplexer Connect(ConfigurationOptions configuration, TextWriter log = null) + public static ConnectionMultiplexer Connect(ConfigurationOptions configuration, TextWriter? log = null) { SocketConnection.AssertDependencies(); + Validate(configuration); - return configuration?.IsSentinel == true + return configuration.IsSentinel ? SentinelPrimaryConnect(configuration, log) : ConnectImpl(configuration, log); } - private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configuration, TextWriter log, ServerType? serverType = null) + private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configuration, TextWriter? log, ServerType? serverType = null) { - Validate(configuration); - IDisposable killMe = null; - EventHandler connectHandler = null; - ConnectionMultiplexer muxer = null; + IDisposable? killMe = null; + EventHandler? connectHandler = null; + ConnectionMultiplexer? muxer = null; using var logProxy = LogProxy.TryCreate(log); try { @@ -782,24 +783,21 @@ internal EndPoint[] GetEndPoints() } } - internal ServerEndPoint GetServerEndPoint(EndPoint endpoint, LogProxy log = null, bool activate = true) + [return: NotNullIfNotNull("endpoint")] + internal ServerEndPoint? GetServerEndPoint(EndPoint? endpoint, LogProxy? log = null, bool activate = true) { - if (endpoint == null) - { - return null; - } - if (servers[endpoint] is not ServerEndPoint server) + if (endpoint == null) return null; + var server = (ServerEndPoint?)servers[endpoint]; + if (server == null) { bool isNew = false; lock (servers) { - server = (ServerEndPoint)servers[endpoint]; + server = (ServerEndPoint?)servers[endpoint]; if (server == null) { - if (_isDisposed) - { - throw new ObjectDisposedException(ToString()); - } + if (_isDisposed) throw new ObjectDisposedException(ToString()); + server = new ServerEndPoint(this, endpoint); servers.Add(endpoint, server); isNew = true; @@ -818,14 +816,14 @@ public TimerToken(ConnectionMultiplexer muxer) { _ref = new WeakReference(muxer); } - private Timer _timer; + private Timer? _timer; public void SetTimer(Timer timer) => _timer = timer; private readonly WeakReference _ref; private static readonly TimerCallback Heartbeat = state => { - var token = (TimerToken)state; - var muxer = (ConnectionMultiplexer)(token._ref?.Target); + var token = (TimerToken)state!; + var muxer = (ConnectionMultiplexer?)(token._ref?.Target); if (muxer != null) { muxer.OnHeartbeat(); @@ -881,7 +879,7 @@ private void OnHeartbeat() /// Obtain a pub/sub subscriber connection to the specified server. /// /// The async state object to pass to the created . - public ISubscriber GetSubscriber(object asyncState = null) + public ISubscriber GetSubscriber(object? asyncState = null) { if (!RawConfig.Proxy.SupportsPubSub()) { @@ -917,7 +915,7 @@ internal int ApplyDefaultDatabase(int db) /// /// The ID to get a database for. /// The async state to pass into the resulting . - public IDatabase GetDatabase(int db = -1, object asyncState = null) + public IDatabase GetDatabase(int db = -1, object? asyncState = null) { db = ApplyDefaultDatabase(db); @@ -929,8 +927,8 @@ public IDatabase GetDatabase(int db = -1, object asyncState = null) // DB zero is stored separately, since 0-only is a massively common use-case private const int MaxCachedDatabaseInstance = 16; // 17 items - [0,16] // Side note: "databases 16" is the default in redis.conf; happy to store one extra to get nice alignment etc - private IDatabase dbCacheZero; - private IDatabase[] dbCacheLow; + private IDatabase? dbCacheZero; + private IDatabase[]? dbCacheLow; private IDatabase GetCachedDatabaseInstance(int db) // note that we already trust db here; only caller checks range { // Note: we don't need to worry about *always* returning the same instance. @@ -950,11 +948,11 @@ public IDatabase GetDatabase(int db = -1, object asyncState = null) /// The key to get a hash slot ID for. public int HashSlot(RedisKey key) => ServerSelectionStrategy.HashSlot(key); - internal ServerEndPoint AnyServer(ServerType serverType, uint startOffset, RedisCommand command, CommandFlags flags, bool allowDisconnected) + internal ServerEndPoint? AnyServer(ServerType serverType, uint startOffset, RedisCommand command, CommandFlags flags, bool allowDisconnected) { var tmp = GetServerSnapshot(); int len = tmp.Length; - ServerEndPoint fallback = null; + ServerEndPoint? fallback = null; for (int i = 0; i < len; i++) { var server = tmp[(int)(((uint)i + startOffset) % len)]; @@ -995,7 +993,7 @@ internal ServerEndPoint AnyServer(ServerType serverType, uint startOffset, Redis /// The host to get a server for. /// The port for to get a server for. /// The async state to pass into the resulting . - public IServer GetServer(string host, int port, object asyncState = null) => + public IServer GetServer(string host, int port, object? asyncState = null) => GetServer(Format.ParseEndPoint(host, port), asyncState); /// @@ -1003,8 +1001,10 @@ public IServer GetServer(string host, int port, object asyncState = null) => /// /// The "host:port" string to get a server for. /// The async state to pass into the resulting . - public IServer GetServer(string hostAndPort, object asyncState = null) => - GetServer(Format.TryParseEndPoint(hostAndPort), asyncState); + public IServer GetServer(string hostAndPort, object? asyncState = null) => + Format.TryParseEndPoint(hostAndPort, out var ep) + ? GetServer(ep, asyncState) + : throw new ArgumentException($"The specified host and port could not be parsed: {hostAndPort}", nameof(hostAndPort)); /// /// Obtain a configuration API for an individual server. @@ -1018,7 +1018,7 @@ public IServer GetServer(string hostAndPort, object asyncState = null) => /// /// The endpoint to get a server for. /// The async state to pass into the resulting . - public IServer GetServer(EndPoint endpoint, object asyncState = null) + public IServer GetServer(EndPoint? endpoint, object? asyncState = null) { _ = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); if (!RawConfig.Proxy.SupportsServerApi()) @@ -1054,7 +1054,7 @@ public long OperationCount /// Reconfigure the current connections based on the existing configuration. /// /// The to log to. - public bool Configure(TextWriter log = null) + public bool Configure(TextWriter? log = null) { // Note we expect ReconfigureAsync to internally allow [n] duration, // so to avoid near misses, here we wait 2*[n]. @@ -1080,7 +1080,7 @@ public bool Configure(TextWriter log = null) /// Reconfigure the current connections based on the existing configuration. /// /// The to log to. - public async Task ConfigureAsync(TextWriter log = null) + public async Task ConfigureAsync(TextWriter? log = null) { using var logProxy = LogProxy.TryCreate(log); return await ReconfigureAsync(first: false, reconfigureAll: true, logProxy, null, "configure").ObserveErrors(); @@ -1119,7 +1119,7 @@ public void GetStatus(TextWriter log) GetStatus(proxy); } - internal void GetStatus(LogProxy log) + internal void GetStatus(LogProxy? log) { if (log == null) return; @@ -1134,7 +1134,7 @@ internal void GetStatus(LogProxy log) log?.WriteLine($"Sync timeouts: {Interlocked.Read(ref syncTimeouts)}; async timeouts: {Interlocked.Read(ref asyncTimeouts)}; fire and forget: {Interlocked.Read(ref fireAndForgets)}; last heartbeat: {LastHeartbeatSecondsAgo}s ago"); } - private void ActivateAllServers(LogProxy log) + private void ActivateAllServers(LogProxy? log) { foreach (var server in GetServerSnapshot()) { @@ -1147,14 +1147,14 @@ private void ActivateAllServers(LogProxy log) } } - internal bool ReconfigureIfNeeded(EndPoint blame, bool fromBroadcast, string cause, bool publishReconfigure = false, CommandFlags flags = CommandFlags.None) + internal bool ReconfigureIfNeeded(EndPoint? blame, bool fromBroadcast, string cause, bool publishReconfigure = false, CommandFlags flags = CommandFlags.None) { if (fromBroadcast) { - OnConfigurationChangedBroadcast(blame); + OnConfigurationChangedBroadcast(blame!); } - string activeCause = Volatile.Read(ref activeConfigCause); - if (activeCause == null) + string? activeCause = Volatile.Read(ref activeConfigCause); + if (activeCause is null) { bool reconfigureAll = fromBroadcast || publishReconfigure; Trace("Configuration change detected; checking nodes", "Configuration"); @@ -1175,7 +1175,7 @@ internal bool ReconfigureIfNeeded(EndPoint blame, bool fromBroadcast, string cau public Task ReconfigureAsync(string reason) => ReconfigureAsync(first: false, reconfigureAll: false, log: null, blame: null, cause: reason); - internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogProxy log, EndPoint blame, string cause, bool publishReconfigure = false, CommandFlags publishReconfigureFlags = CommandFlags.None) + internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogProxy? log, EndPoint? blame, string cause, bool publishReconfigure = false, CommandFlags publishReconfigureFlags = CommandFlags.None) { if (_isDisposed) throw new ObjectDisposedException(ToString()); bool showStats = log is not null; @@ -1233,7 +1233,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP } List primaries = new List(endpoints.Count); - ServerEndPoint[] servers = null; + ServerEndPoint[]? servers = null; bool encounteredConnectedClusterServer = false; ValueStopwatch? watch = null; @@ -1290,7 +1290,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP log?.WriteLine($" {Format.ToString(server.EndPoint)}: Endpoint is {server.ConnectionState}"); } - EndPointCollection updatedClusterEndpointCollection = null; + EndPointCollection? updatedClusterEndpointCollection = null; for (int i = 0; i < available.Length; i++) { var task = available[i]; @@ -1299,7 +1299,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP if (task.IsFaulted) { server.SetUnselectable(UnselectableFlags.DidNotRespond); - var aex = task.Exception; + var aex = task.Exception!; foreach (var ex in aex.InnerExceptions) { log?.WriteLine($" {Format.ToString(server)}: Faulted: {ex.Message}"); @@ -1414,7 +1414,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP // ...for those cases, we want to allow sending to any primary endpoint. if (ServerSelectionStrategy.ServerType.HasSinglePrimary()) { - var preferred = NominatePreferredPrimary(log, servers, useTieBreakers, primaries); + var preferred = NominatePreferredPrimary(log, servers!, useTieBreakers, primaries); foreach (var primary in primaries) { if (primary == preferred || primary.IsReplica) @@ -1454,7 +1454,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP GetStatus(log); } - string stormLog = GetStormLog(); + string? stormLog = GetStormLog(); if (!string.IsNullOrWhiteSpace(stormLog)) { log?.WriteLine(); @@ -1500,7 +1500,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP { Trace("Exiting reconfiguration..."); if (ranThisCall) Interlocked.Exchange(ref activeConfigCause, null); - if (!first) OnConfigurationChanged(blame); + if (!first && blame is not null) OnConfigurationChanged(blame); Trace("Reconfiguration exited"); } } @@ -1514,15 +1514,19 @@ public EndPoint[] GetEndPoints(bool configuredOnly = false) => ? EndPoints.ToArray() : _serverSnapshot.GetEndPoints(); - private async Task GetEndpointsFromClusterNodes(ServerEndPoint server, LogProxy log) + private async Task GetEndpointsFromClusterNodes(ServerEndPoint server, LogProxy? log) { var message = Message.Create(-1, CommandFlags.None, RedisCommand.CLUSTER, RedisLiterals.NODES); try { var clusterConfig = await ExecuteAsyncImpl(message, ResultProcessor.ClusterNodes, null, server).ForAwait(); - var clusterEndpoints = new EndPointCollection(clusterConfig.Nodes.Select(node => node.EndPoint).ToList()); + if (clusterConfig is null) + { + return null; + } + var clusterEndpoints = new EndPointCollection(clusterConfig.Nodes.Where(node => node.EndPoint is not null).Select(node => node.EndPoint!).ToList()); // Loop through nodes in the cluster and update nodes relations to other nodes - ServerEndPoint serverEndpoint = null; + ServerEndPoint? serverEndpoint = null; foreach (EndPoint endpoint in clusterEndpoints) { serverEndpoint = GetServerEndPoint(endpoint); @@ -1549,11 +1553,11 @@ private void ResetAllNonConnected() } } - private static ServerEndPoint NominatePreferredPrimary(LogProxy log, ServerEndPoint[] servers, bool useTieBreakers, List primaries) + private static ServerEndPoint? NominatePreferredPrimary(LogProxy? log, ServerEndPoint[] servers, bool useTieBreakers, List primaries) { log?.WriteLine("Election summary:"); - Dictionary uniques = null; + Dictionary? uniques = null; if (useTieBreakers) { // Count the votes @@ -1561,9 +1565,9 @@ private static ServerEndPoint NominatePreferredPrimary(LogProxy log, ServerEndPo for (int i = 0; i < servers.Length; i++) { var server = servers[i]; - string serverResult = server.TieBreakerResult; + string? serverResult = server.TieBreakerResult; - if (string.IsNullOrWhiteSpace(serverResult)) + if (serverResult.IsNullOrWhiteSpace()) { log?.WriteLine($" Election: {Format.ToString(server)} had no tiebreaker set"); } @@ -1605,7 +1609,7 @@ private static ServerEndPoint NominatePreferredPrimary(LogProxy log, ServerEndPo break; default: log?.WriteLine(" Election is contested:"); - ServerEndPoint highest = null; + ServerEndPoint? highest = null; bool arbitrary = false; foreach (var pair in uniques.OrderByDescending(x => x.Value)) { @@ -1642,7 +1646,7 @@ private static ServerEndPoint NominatePreferredPrimary(LogProxy log, ServerEndPo return primaries[0]; } - private static ServerEndPoint SelectServerByElection(ServerEndPoint[] servers, string endpoint, LogProxy log) + private static ServerEndPoint? SelectServerByElection(ServerEndPoint[] servers, string endpoint, LogProxy? log) { if (servers == null || string.IsNullOrWhiteSpace(endpoint)) return null; for (int i = 0; i < servers.Length; i++) @@ -1707,16 +1711,16 @@ internal void UpdateClusterRange(ClusterConfiguration configuration) } } - internal ServerEndPoint SelectServer(Message message) => + internal ServerEndPoint? SelectServer(Message? message) => message == null ? null : ServerSelectionStrategy.Select(message); - internal ServerEndPoint SelectServer(RedisCommand command, CommandFlags flags, in RedisKey key) => + internal ServerEndPoint? SelectServer(RedisCommand command, CommandFlags flags, in RedisKey key) => ServerSelectionStrategy.Select(command, key, flags); - internal ServerEndPoint SelectServer(RedisCommand command, CommandFlags flags, in RedisChannel channel) => + internal ServerEndPoint? SelectServer(RedisCommand command, CommandFlags flags, in RedisChannel channel) => ServerSelectionStrategy.Select(command, channel, flags); - private bool PrepareToPushMessageToBridge(Message message, ResultProcessor processor, IResultBox resultBox, ref ServerEndPoint server) + private bool PrepareToPushMessageToBridge(Message message, ResultProcessor? processor, IResultBox? resultBox, [NotNullWhen(true)] ref ServerEndPoint? server) { message.SetSource(processor, resultBox); @@ -1726,7 +1730,7 @@ private bool PrepareToPushMessageToBridge(Message message, ResultProcessor server = SelectServer(message); // If we didn't find one successfully, and we're allowed, queue for any viable server - if (server == null && message != null && RawConfig.BacklogPolicy.QueueWhileDisconnected) + if (server == null && RawConfig.BacklogPolicy.QueueWhileDisconnected) { server = ServerSelectionStrategy.Select(message, allowDisconnected: true); } @@ -1780,11 +1784,11 @@ private bool PrepareToPushMessageToBridge(Message message, ResultProcessor return false; } - private ValueTask TryPushMessageToBridgeAsync(Message message, ResultProcessor processor, IResultBox resultBox, ref ServerEndPoint server) + private ValueTask TryPushMessageToBridgeAsync(Message message, ResultProcessor? processor, IResultBox? resultBox, [NotNullWhen(true)] ref ServerEndPoint? server) => PrepareToPushMessageToBridge(message, processor, resultBox, ref server) ? server.TryWriteAsync(message) : new ValueTask(WriteResult.NoConnectionAvailable); [Obsolete("prefer async")] - private WriteResult TryPushMessageToBridgeSync(Message message, ResultProcessor processor, IResultBox resultBox, ref ServerEndPoint server) + private WriteResult TryPushMessageToBridgeSync(Message message, ResultProcessor? processor, IResultBox? resultBox, [NotNullWhen(true)] ref ServerEndPoint? server) => PrepareToPushMessageToBridge(message, processor, resultBox, ref server) ? server.TryWriteSync(message) : WriteResult.NoConnectionAvailable; /// @@ -1792,16 +1796,16 @@ private WriteResult TryPushMessageToBridgeSync(Message message, ResultProcess /// public override string ToString() => string.IsNullOrWhiteSpace(ClientName) ? GetType().Name : ClientName; - internal Exception GetException(WriteResult result, Message message, ServerEndPoint server) => result switch + internal Exception GetException(WriteResult result, Message message, ServerEndPoint? server) => result switch { - WriteResult.Success => null, + WriteResult.Success => throw new ArgumentOutOfRangeException(nameof(result), "Be sure to check result isn't successful before calling GetException."), WriteResult.NoConnectionAvailable => ExceptionFactory.NoConnectionAvailable(this, message, server), WriteResult.TimeoutBeforeWrite => ExceptionFactory.Timeout(this, "The timeout was reached before the message could be written to the output buffer, and it was not sent", message, server, result), _ => ExceptionFactory.ConnectionFailure(RawConfig.IncludeDetailInExceptions, ConnectionFailureType.ProtocolFailure, "An unknown error occurred when writing the message", server), }; [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Intentional observation")] - internal static void ThrowFailed(TaskCompletionSource source, Exception unthrownException) + internal static void ThrowFailed(TaskCompletionSource? source, Exception unthrownException) { try { @@ -1809,19 +1813,23 @@ internal static void ThrowFailed(TaskCompletionSource source, Exception un } catch (Exception ex) { - source.TrySetException(ex); - GC.KeepAlive(source.Task.Exception); - GC.SuppressFinalize(source.Task); + if (source is not null) + { + source.TrySetException(ex); + GC.KeepAlive(source.Task.Exception); + GC.SuppressFinalize(source.Task); + } } } - internal T ExecuteSyncImpl(Message message, ResultProcessor processor, ServerEndPoint server) + [return: NotNullIfNotNull("defaultValue")] + internal T? ExecuteSyncImpl(Message message, ResultProcessor? processor, ServerEndPoint? server, T? defaultValue = default) { if (_isDisposed) throw new ObjectDisposedException(ToString()); - if (message == null) // Fire-and forget could involve a no-op, represented by null - for example Increment by 0 + if (message is null) // Fire-and forget could involve a no-op, represented by null - for example Increment by 0 { - return default(T); + return defaultValue; } if (message.IsFireAndForget) @@ -1830,7 +1838,7 @@ internal T ExecuteSyncImpl(Message message, ResultProcessor processor, Ser TryPushMessageToBridgeSync(message, processor, null, ref server); #pragma warning restore CS0618 Interlocked.Increment(ref fireAndForgets); - return default(T); + return defaultValue; } else { @@ -1866,16 +1874,58 @@ internal T ExecuteSyncImpl(Message message, ResultProcessor processor, Ser } } - internal Task ExecuteAsyncImpl(Message message, ResultProcessor processor, object state, ServerEndPoint server) + internal Task ExecuteAsyncImpl(Message? message, ResultProcessor? processor, object? state, ServerEndPoint? server, T defaultValue) { + static async Task ExecuteAsyncImpl_Awaited(ConnectionMultiplexer @this, ValueTask write, TaskCompletionSource? tcs, Message message, ServerEndPoint? server, T defaultValue) + { + var result = await write.ForAwait(); + if (result != WriteResult.Success) + { + var ex = @this.GetException(result, message, server); + ThrowFailed(tcs, ex); + } + return tcs == null ? defaultValue : await tcs.Task.ForAwait(); + } + if (_isDisposed) throw new ObjectDisposedException(ToString()); if (message == null) { - return CompletedTask.Default(state); + return CompletedTask.FromDefault(defaultValue, state); } - static async Task ExecuteAsyncImpl_Awaited(ConnectionMultiplexer @this, ValueTask write, TaskCompletionSource tcs, Message message, ServerEndPoint server) + TaskCompletionSource? tcs = null; + IResultBox? source = null; + if (!message.IsFireAndForget) + { + source = TaskResultBox.Create(out tcs, state); + } + var write = TryPushMessageToBridgeAsync(message, processor, source, ref server); + if (!write.IsCompletedSuccessfully) + { + return ExecuteAsyncImpl_Awaited(this, write, tcs, message, server, defaultValue); + } + + if (tcs == null) + { + return CompletedTask.FromDefault(defaultValue, null); // F+F explicitly does not get async-state + } + else + { + var result = write.Result; + if (result != WriteResult.Success) + { + var ex = GetException(result, message, server); + ThrowFailed(tcs, ex); + } + return tcs.Task; + } + } + + internal Task ExecuteAsyncImpl(Message? message, ResultProcessor? processor, object? state, ServerEndPoint? server) + { + [return: NotNullIfNotNull("tcs")] + static async Task ExecuteAsyncImpl_Awaited(ConnectionMultiplexer @this, ValueTask write, TaskCompletionSource? tcs, Message message, ServerEndPoint? server) { var result = await write.ForAwait(); if (result != WriteResult.Success) @@ -1886,18 +1936,28 @@ static async Task ExecuteAsyncImpl_Awaited(ConnectionMultiplexer @this, Value return tcs == null ? default : await tcs.Task.ForAwait(); } - TaskCompletionSource tcs = null; - IResultBox source = null; + if (_isDisposed) throw new ObjectDisposedException(ToString()); + + if (message == null) + { + return CompletedTask.Default(state); + } + + TaskCompletionSource? tcs = null; + IResultBox? source = null; if (!message.IsFireAndForget) { - source = TaskResultBox.Create(out tcs, state); + source = TaskResultBox.Create(out tcs, state); + } + var write = TryPushMessageToBridgeAsync(message, processor, source!, ref server); + if (!write.IsCompletedSuccessfully) + { + return ExecuteAsyncImpl_Awaited(this, write, tcs, message, server); } - var write = TryPushMessageToBridgeAsync(message, processor, source, ref server); - if (!write.IsCompletedSuccessfully) return ExecuteAsyncImpl_Awaited(this, write, tcs, message, server); if (tcs == null) { - return CompletedTask.Default(null); // F+F explicitly does not get async-state + return CompletedTask.Default(null); // F+F explicitly does not get async-state } else { @@ -1920,18 +1980,13 @@ static async Task ExecuteAsyncImpl_Awaited(ConnectionMultiplexer @this, Value /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending). public long PublishReconfigure(CommandFlags flags = CommandFlags.None) { - if (ConfigurationChangedChannel is null) - { - return 0; - } - else if (ReconfigureIfNeeded(null, false, "PublishReconfigure", true, flags)) - { - return -1; - } - else + if (ConfigurationChangedChannel is not null) { - return PublishReconfigureImpl(flags); + return ReconfigureIfNeeded(null, false, "PublishReconfigure", true, flags) + ? -1 + : PublishReconfigureImpl(flags); } + return 0; } private long PublishReconfigureImpl(CommandFlags flags) => @@ -2016,8 +2071,7 @@ private void DisposeAndClearServers() var iter = servers.GetEnumerator(); while (iter.MoveNext()) { - var server = (ServerEndPoint)iter.Value; - server.Dispose(); + (iter.Value as ServerEndPoint)?.Dispose(); } servers.Clear(); } @@ -2032,7 +2086,7 @@ private Task[] QuitAllServers() int index = 0; while (iter.MoveNext()) { - var server = (ServerEndPoint)iter.Value; + var server = (ServerEndPoint)iter.Value!; quits[index++] = server.Close(ConnectionType.Interactive); quits[index++] = server.Close(ConnectionType.Subscription); } diff --git a/src/StackExchange.Redis/CursorEnumerable.cs b/src/StackExchange.Redis/CursorEnumerable.cs index 686f412e1..efe2db61a 100644 --- a/src/StackExchange.Redis/CursorEnumerable.cs +++ b/src/StackExchange.Redis/CursorEnumerable.cs @@ -15,14 +15,14 @@ namespace StackExchange.Redis internal abstract class CursorEnumerable : IEnumerable, IScanningCursor, IAsyncEnumerable { private readonly RedisBase redis; - private readonly ServerEndPoint server; + private readonly ServerEndPoint? server; private protected readonly int db; private protected readonly CommandFlags flags; private protected readonly int pageSize, initialOffset; private protected readonly RedisValue initialCursor; - private volatile IScanningCursor activeCursor; + private volatile IScanningCursor? activeCursor; - private protected CursorEnumerable(RedisBase redis, ServerEndPoint server, int db, int pageSize, in RedisValue cursor, int pageOffset, CommandFlags flags) + private protected CursorEnumerable(RedisBase redis, ServerEndPoint? server, int db, int pageSize, in RedisValue cursor, int pageOffset, CommandFlags flags) { if (pageOffset < 0) throw new ArgumentOutOfRangeException(nameof(pageOffset)); this.redis = redis; @@ -51,10 +51,10 @@ private protected CursorEnumerable(RedisBase redis, ServerEndPoint server, int d internal readonly struct ScanResult { public readonly RedisValue Cursor; - public readonly T[] ValuesOversized; + public readonly T[]? ValuesOversized; public readonly int Count; public readonly bool IsPooled; - public ScanResult(RedisValue cursor, T[] valuesOversized, int count, bool isPooled) + public ScanResult(RedisValue cursor, T[]? valuesOversized, int count, bool isPooled) { Cursor = cursor; ValuesOversized = valuesOversized; @@ -63,11 +63,11 @@ public ScanResult(RedisValue cursor, T[] valuesOversized, int count, bool isPool } } - private protected abstract Message CreateMessage(in RedisValue cursor); + private protected abstract Message? CreateMessage(in RedisValue cursor); - private protected abstract ResultProcessor Processor { get; } + private protected abstract ResultProcessor? Processor { get; } - private protected virtual Task GetNextPageAsync(IScanningCursor obj, RedisValue cursor, out Message message) + private protected virtual Task GetNextPageAsync(IScanningCursor obj, RedisValue cursor, out Message? message) { activeCursor = obj; message = CreateMessage(cursor); @@ -95,7 +95,7 @@ public T Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - Debug.Assert(_pageOffset >= 0 & _pageOffset < _pageCount & _pageOversized.Length >= _pageCount); + Debug.Assert(_pageOffset >= 0 & _pageOffset < _pageCount & _pageOversized!.Length >= _pageCount); return _pageOversized[_pageOffset]; } } @@ -133,7 +133,7 @@ public ValueTask DisposeAsync() return default; } - object IEnumerator.Current => _pageOversized[_pageOffset]; + object? IEnumerator.Current => _pageOversized![_pageOffset]; private bool SimpleNext() { @@ -145,11 +145,11 @@ private bool SimpleNext() return false; } - private T[] _pageOversized; + private T[]? _pageOversized; private int _pageCount, _pageOffset, _pageIndex = -1; private bool _isPooled; - private Task _pending; - private Message _pendingMessage; + private Task? _pending; + private Message? _pendingMessage; private RedisValue _currentCursor, _nextCursor; private volatile State _state; @@ -192,7 +192,7 @@ private bool SlowNextSync() { var pending = SlowNextAsync(); if (pending.IsCompletedSuccessfully) return pending.Result; - return Wait(pending.AsTask(), _pendingMessage); + return Wait(pending.AsTask(), _pendingMessage!); } private protected TResult Wait(Task pending, Message message) @@ -222,8 +222,8 @@ private ValueTask SlowNextAsync() _state = State.Running; goto case State.Running; case State.Running: - Task pending; - while ((pending = _pending) != null & _state == State.Running) + Task? pending; + while ((pending = _pending) != null && _state == State.Running) { if (!pending.IsCompleted) return AwaitedNextAsync(isInitial); ProcessReply(pending.Result, isInitial); @@ -266,8 +266,8 @@ private void TryAppendExceptionState(Exception ex) private async ValueTask AwaitedNextAsync(bool isInitial) { - Task pending; - while ((pending = _pending) != null & _state == State.Running) + Task? pending; + while ((pending = _pending) != null && _state == State.Running) { ScanResult scanResult; try @@ -288,7 +288,7 @@ private async ValueTask AwaitedNextAsync(bool isInitial) return false; } - private static void Recycle(ref T[] array, ref bool isPooled) + private static void Recycle(ref T[]? array, ref bool isPooled) { var tmp = array; array = null; @@ -338,19 +338,19 @@ public void Reset() int IScanningCursor.PageOffset => activeCursor?.PageOffset ?? initialOffset; - internal static CursorEnumerable From(RedisBase redis, ServerEndPoint server, Task pending, int pageOffset) + internal static CursorEnumerable From(RedisBase redis, ServerEndPoint? server, Task pending, int pageOffset) => new SingleBlockEnumerable(redis, server, pending, pageOffset); private class SingleBlockEnumerable : CursorEnumerable { private readonly Task _pending; - public SingleBlockEnumerable(RedisBase redis, ServerEndPoint server, + public SingleBlockEnumerable(RedisBase redis, ServerEndPoint? server, Task pending, int pageOffset) : base(redis, server, 0, int.MaxValue, 0, pageOffset, default) { _pending = pending; } - private protected override Task GetNextPageAsync(IScanningCursor obj, RedisValue cursor, out Message message) + private protected override Task GetNextPageAsync(IScanningCursor obj, RedisValue cursor, out Message? message) { message = null; return AwaitedGetNextPageAsync(); @@ -360,8 +360,8 @@ private async Task AwaitedGetNextPageAsync() var arr = (await _pending.ForAwait()) ?? Array.Empty(); return new ScanResult(RedisBase.CursorUtils.Origin, arr, arr.Length, false); } - private protected override ResultProcessor Processor => null; - private protected override Message CreateMessage(in RedisValue cursor) => null; + private protected override ResultProcessor? Processor => null; + private protected override Message? CreateMessage(in RedisValue cursor) => null; } } } diff --git a/src/StackExchange.Redis/EndPointCollection.cs b/src/StackExchange.Redis/EndPointCollection.cs index 25caae917..1b6095932 100644 --- a/src/StackExchange.Redis/EndPointCollection.cs +++ b/src/StackExchange.Redis/EndPointCollection.cs @@ -34,13 +34,13 @@ public EndPointCollection(IList endpoints) : base(endpoints) {} /// Format an . /// /// The endpoint to get a string representation for. - public static string ToString(EndPoint endpoint) => Format.ToString(endpoint); + public static string ToString(EndPoint? endpoint) => Format.ToString(endpoint); /// /// Attempt to parse a string into an . /// /// The endpoint string to parse. - public static EndPoint TryParse(string endpoint) => Format.TryParseEndPoint(endpoint); + public static EndPoint? TryParse(string endpoint) => Format.TryParseEndPoint(endpoint, out var result) ? result : null; /// /// Adds a new endpoint to the list. @@ -48,8 +48,7 @@ public EndPointCollection(IList endpoints) : base(endpoints) {} /// The host:port string to add an endpoint for to the collection. public void Add(string hostAndPort) { - var endpoint = Format.TryParseEndPoint(hostAndPort); - if (endpoint == null) + if (!Format.TryParseEndPoint(hostAndPort, out var endpoint)) { throw new ArgumentException($"Could not parse host and port from '{hostAndPort}'", nameof(hostAndPort)); } @@ -190,7 +189,7 @@ internal bool HasDnsEndPoints() return false; } - internal async Task ResolveEndPointsAsync(ConnectionMultiplexer multiplexer, LogProxy log) + internal async Task ResolveEndPointsAsync(ConnectionMultiplexer multiplexer, LogProxy? log) { var cache = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < Count; i++) @@ -203,7 +202,7 @@ internal async Task ResolveEndPointsAsync(ConnectionMultiplexer multiplexer, Log { this[i] = new IPEndPoint(IPAddress.Loopback, dns.Port); } - else if (cache.TryGetValue(dns.Host, out IPAddress ip)) + else if (cache.TryGetValue(dns.Host, out IPAddress? ip)) { // use cache this[i] = new IPEndPoint(ip, dns.Port); } diff --git a/src/StackExchange.Redis/EndPointEventArgs.cs b/src/StackExchange.Redis/EndPointEventArgs.cs index cbd87c7af..5f8cc6b18 100644 --- a/src/StackExchange.Redis/EndPointEventArgs.cs +++ b/src/StackExchange.Redis/EndPointEventArgs.cs @@ -9,9 +9,9 @@ namespace StackExchange.Redis /// public class EndPointEventArgs : EventArgs, ICompletable { - private readonly EventHandler handler; + private readonly EventHandler? handler; private readonly object sender; - internal EndPointEventArgs(EventHandler handler, object sender, EndPoint endpoint) + internal EndPointEventArgs(EventHandler? handler, object sender, EndPoint endpoint) { this.handler = handler; this.sender = sender; diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index d38f694e8..7200a3a06 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Reflection; using System.Text; using System.Threading; @@ -12,9 +11,9 @@ private const string DataCommandKey = "redis-command", DataSentStatusKey = "request-sent-status", DataServerKey = "redis-server", - timeoutHelpLink = "https://stackexchange.github.io/StackExchange.Redis/Timeouts"; + TimeoutHelpLink = "https://stackexchange.github.io/StackExchange.Redis/Timeouts"; - internal static Exception AdminModeNotEnabled(bool includeDetail, RedisCommand command, Message message, ServerEndPoint server) + internal static Exception AdminModeNotEnabled(bool includeDetail, RedisCommand command, Message? message, ServerEndPoint? server) { string s = GetLabel(includeDetail, command, message); var ex = new RedisCommandException("This operation is not available unless admin mode is enabled: " + s); @@ -30,7 +29,7 @@ internal static Exception CommandDisabled(string command) internal static Exception TooManyArgs(string command, int argCount) => new RedisCommandException($"This operation would involve too many arguments ({argCount + 1} vs the redis limit of {PhysicalConnection.REDIS_MAX_ARGS}): {command}"); - internal static Exception ConnectionFailure(bool includeDetail, ConnectionFailureType failureType, string message, ServerEndPoint server) + internal static Exception ConnectionFailure(bool includeDetail, ConnectionFailureType failureType, string message, ServerEndPoint? server) { var ex = new RedisConnectionException(failureType, message); if (includeDetail) AddExceptionDetail(ex, null, server, null); @@ -60,7 +59,7 @@ internal static Exception DatabaseRequired(bool includeDetail, RedisCommand comm return ex; } - internal static Exception PrimaryOnly(bool includeDetail, RedisCommand command, Message message, ServerEndPoint server) + internal static Exception PrimaryOnly(bool includeDetail, RedisCommand command, Message? message, ServerEndPoint? server) { string s = GetLabel(includeDetail, command, message); var ex = new RedisCommandException("Command cannot be issued to a replica: " + s); @@ -75,7 +74,7 @@ internal static Exception MultiSlot(bool includeDetail, Message message) return ex; } - internal static string GetInnerMostExceptionMessage(Exception e) + internal static string GetInnerMostExceptionMessage(Exception? e) { if (e == null) { @@ -93,8 +92,8 @@ internal static string GetInnerMostExceptionMessage(Exception e) internal static Exception NoConnectionAvailable( ConnectionMultiplexer multiplexer, - Message message, - ServerEndPoint server, + Message? message, + ServerEndPoint? server, ReadOnlySpan serverSnapshot = default, RedisCommand command = default) { @@ -140,7 +139,7 @@ internal static Exception NoConnectionAvailable( } // Add counters and exception data if we have it - List> data = null; + List>? data = null; if (multiplexer.RawConfig.IncludeDetailInExceptions) { data = new List>(); @@ -156,20 +155,19 @@ internal static Exception NoConnectionAvailable( return ex; } - internal static Exception PopulateInnerExceptions(ReadOnlySpan serverSnapshot) + internal static Exception? PopulateInnerExceptions(ReadOnlySpan serverSnapshot) { var innerExceptions = new List(); - if (serverSnapshot.Length > 0 && serverSnapshot[0].Multiplexer.LastException != null) + if (serverSnapshot.Length > 0 && serverSnapshot[0].Multiplexer.LastException is Exception ex) { - innerExceptions.Add(serverSnapshot[0].Multiplexer.LastException); + innerExceptions.Add(ex); } for (int i = 0; i < serverSnapshot.Length; i++) { - if (serverSnapshot[i].LastException != null) + if (serverSnapshot[i].LastException is Exception lastException) { - var lastException = serverSnapshot[i].LastException; innerExceptions.Add(lastException); } } @@ -199,7 +197,7 @@ internal static Exception NoCursor(RedisCommand command) return new RedisCommandException("Command cannot be used with a cursor: " + s); } - private static void Add(List> data, StringBuilder sb, string lk, string sk, string v) + private static void Add(List> data, StringBuilder sb, string? lk, string? sk, string? v) { if (v != null) { @@ -208,7 +206,7 @@ private static void Add(List> data, StringBuilder sb, stri } } - internal static Exception Timeout(ConnectionMultiplexer multiplexer, string baseErrorMessage, Message message, ServerEndPoint server, WriteResult? result = null) + internal static Exception Timeout(ConnectionMultiplexer multiplexer, string? baseErrorMessage, Message message, ServerEndPoint? server, WriteResult? result = null) { List> data = new List> { Tuple.Create("Message", message.CommandAndKey) }; var sb = new StringBuilder(); @@ -252,12 +250,12 @@ internal static Exception Timeout(ConnectionMultiplexer multiplexer, string base AddCommonDetail(data, sb, message, multiplexer, server); sb.Append(" (Please take a look at this article for some common client-side issues that can cause timeouts: "); - sb.Append(timeoutHelpLink); + sb.Append(TimeoutHelpLink); sb.Append(')'); var ex = new RedisTimeoutException(sb.ToString(), message?.Status ?? CommandStatus.Unknown) { - HelpLink = timeoutHelpLink + HelpLink = TimeoutHelpLink }; CopyDataToException(data, ex); @@ -265,7 +263,7 @@ internal static Exception Timeout(ConnectionMultiplexer multiplexer, string base return ex; } - private static void CopyDataToException(List> data, Exception ex) + private static void CopyDataToException(List>? data, Exception ex) { if (data != null) { @@ -280,9 +278,9 @@ private static void CopyDataToException(List> data, Except private static void AddCommonDetail( List> data, StringBuilder sb, - Message message, + Message? message, ConnectionMultiplexer multiplexer, - ServerEndPoint server + ServerEndPoint? server ) { if (message != null) @@ -322,7 +320,7 @@ ServerEndPoint server if (string.IsNullOrWhiteSpace(log)) Interlocked.Exchange(ref multiplexer.haveStormLog, 0); else Interlocked.Exchange(ref multiplexer.stormLogSnapshot, log); } - Add(data, sb, "Server-Endpoint", "serverEndpoint", server.EndPoint.ToString().Replace("Unspecified/", "")); + Add(data, sb, "Server-Endpoint", "serverEndpoint", (server.EndPoint.ToString() ?? "Unknown").Replace("Unspecified/", "")); } Add(data, sb, "Multiplexer-Connects", "mc", $"{multiplexer._connectAttemptCount}/{multiplexer._connectCompletedCount}/{multiplexer._connectionCloseCount}"); Add(data, sb, "Manager", "mgr", multiplexer.SocketManager?.GetState()); @@ -337,7 +335,7 @@ ServerEndPoint server Add(data, sb, "Key-HashSlot", "PerfCounterHelperkeyHashSlot", message.GetHashSlot(multiplexer.ServerSelectionStrategy).ToString()); } } - int busyWorkerCount = PerfCounterHelper.GetThreadPoolStats(out string iocp, out string worker, out string workItems); + int busyWorkerCount = PerfCounterHelper.GetThreadPoolStats(out string iocp, out string worker, out string? workItems); Add(data, sb, "ThreadPool-IO-Completion", "IOCP", iocp); Add(data, sb, "ThreadPool-Workers", "WORKER", worker); if (workItems != null) @@ -354,7 +352,7 @@ ServerEndPoint server Add(data, sb, "Version", "v", Utils.GetLibVersion()); } - private static void AddExceptionDetail(Exception exception, Message message, ServerEndPoint server, string label) + private static void AddExceptionDetail(Exception? exception, Message? message, ServerEndPoint? server, string? label) { if (exception != null) { @@ -372,12 +370,12 @@ private static void AddExceptionDetail(Exception exception, Message message, Ser } } - private static string GetLabel(bool includeDetail, RedisCommand command, Message message) + private static string GetLabel(bool includeDetail, RedisCommand command, Message? message) { return message == null ? command.ToString() : (includeDetail ? message.CommandAndKey : message.Command.ToString()); } - internal static Exception UnableToConnect(ConnectionMultiplexer muxer, string failureMessage=null) + internal static Exception UnableToConnect(ConnectionMultiplexer muxer, string? failureMessage = null) { var sb = new StringBuilder("It was not possible to connect to the redis server(s)."); if (muxer != null) @@ -385,7 +383,10 @@ internal static Exception UnableToConnect(ConnectionMultiplexer muxer, string fa if (muxer.AuthSuspect) sb.Append(" There was an authentication failure; check that passwords (or client certificates) are configured correctly."); else if (muxer.RawConfig.AbortOnConnectFail) sb.Append(" Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code."); } - if (!string.IsNullOrWhiteSpace(failureMessage)) sb.Append(' ').Append(failureMessage.Trim()); + if (!failureMessage.IsNullOrWhiteSpace()) + { + sb.Append(' ').Append(failureMessage.Trim()); + } return new RedisConnectionException(ConnectionFailureType.UnableToConnect, sb.ToString()); } diff --git a/src/StackExchange.Redis/Exceptions.cs b/src/StackExchange.Redis/Exceptions.cs index fe7da1424..17abcc21c 100644 --- a/src/StackExchange.Redis/Exceptions.cs +++ b/src/StackExchange.Redis/Exceptions.cs @@ -48,7 +48,7 @@ public RedisTimeoutException(string message, CommandStatus commandStatus) : base private RedisTimeoutException(SerializationInfo info, StreamingContext ctx) : base(info, ctx) { - Commandstatus = (CommandStatus)info.GetValue("commandStatus", typeof(CommandStatus)); + Commandstatus = info.GetValue("commandStatus", typeof(CommandStatus)) as CommandStatus? ?? CommandStatus.Unknown; } /// /// Serialization implementation; not intended for general usage. @@ -81,7 +81,7 @@ public RedisConnectionException(ConnectionFailureType failureType, string messag /// The type of connection failure. /// The message for the exception. /// The inner exception. - public RedisConnectionException(ConnectionFailureType failureType, string message, Exception innerException) : this(failureType, message, innerException, CommandStatus.Unknown) {} + public RedisConnectionException(ConnectionFailureType failureType, string message, Exception? innerException) : this(failureType, message, innerException, CommandStatus.Unknown) {} /// /// Creates a new . @@ -90,7 +90,7 @@ public RedisConnectionException(ConnectionFailureType failureType, string messag /// The message for the exception. /// The inner exception. /// The status of the command. - public RedisConnectionException(ConnectionFailureType failureType, string message, Exception innerException, CommandStatus commandStatus) : base(message, innerException) + public RedisConnectionException(ConnectionFailureType failureType, string message, Exception? innerException, CommandStatus commandStatus) : base(message, innerException) { FailureType = failureType; CommandStatus = commandStatus; @@ -109,7 +109,7 @@ public RedisConnectionException(ConnectionFailureType failureType, string messag private RedisConnectionException(SerializationInfo info, StreamingContext ctx) : base(info, ctx) { FailureType = (ConnectionFailureType)info.GetInt32("failureType"); - CommandStatus = (CommandStatus)info.GetValue("commandStatus", typeof(CommandStatus)); + CommandStatus = info.GetValue("commandStatus", typeof(CommandStatus)) as CommandStatus? ?? CommandStatus.Unknown; } /// /// Serialization implementation; not intended for general usage. @@ -141,7 +141,7 @@ public RedisException(string message) : base(message) { } /// /// The message for the exception. /// The inner exception. - public RedisException(string message, Exception innerException) : base(message, innerException) { } + public RedisException(string message, Exception? innerException) : base(message, innerException) { } /// /// Deserialization constructor; not intended for general usage. diff --git a/src/StackExchange.Redis/ExponentialRetry.cs b/src/StackExchange.Redis/ExponentialRetry.cs index fd7f908a4..4cf965abc 100644 --- a/src/StackExchange.Redis/ExponentialRetry.cs +++ b/src/StackExchange.Redis/ExponentialRetry.cs @@ -10,7 +10,7 @@ public class ExponentialRetry : IReconnectRetryPolicy private readonly int deltaBackOffMilliseconds; private readonly int maxDeltaBackOffMilliseconds = (int)TimeSpan.FromSeconds(10).TotalMilliseconds; [ThreadStatic] - private static Random r; + private static Random? r; /// /// Initializes a new instance using the specified back off interval with default maxDeltaBackOffMilliseconds of 10 seconds. diff --git a/src/StackExchange.Redis/ExtensionMethods.Internal.cs b/src/StackExchange.Redis/ExtensionMethods.Internal.cs new file mode 100644 index 000000000..e4903583b --- /dev/null +++ b/src/StackExchange.Redis/ExtensionMethods.Internal.cs @@ -0,0 +1,13 @@ +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis +{ + internal static class ExtensionMethodsInternal + { + public static bool IsNullOrEmpty([NotNullWhen(false)] this string? s) => + string.IsNullOrEmpty(s); + + public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? s) => + string.IsNullOrWhiteSpace(s); + } +} diff --git a/src/StackExchange.Redis/ExtensionMethods.cs b/src/StackExchange.Redis/ExtensionMethods.cs index 9fefeda06..b84381d4c 100644 --- a/src/StackExchange.Redis/ExtensionMethods.cs +++ b/src/StackExchange.Redis/ExtensionMethods.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.Security; using System.Runtime.CompilerServices; @@ -19,14 +20,18 @@ public static class ExtensionMethods /// Create a dictionary from an array of HashEntry values. /// /// The entry to convert to a dictionary. - public static Dictionary ToStringDictionary(this HashEntry[] hash) + [return: NotNullIfNotNull("hash")] + public static Dictionary? ToStringDictionary(this HashEntry[]? hash) { - if (hash == null) return null; + if (hash is null) + { + return null; + } var result = new Dictionary(hash.Length, StringComparer.Ordinal); for(int i = 0; i < hash.Length; i++) { - result.Add(hash[i].name, hash[i].value); + result.Add(hash[i].name!, hash[i].value!); } return result; } @@ -35,9 +40,13 @@ public static Dictionary ToStringDictionary(this HashEntry[] hash /// Create a dictionary from an array of HashEntry values. /// /// The entry to convert to a dictionary. - public static Dictionary ToDictionary(this HashEntry[] hash) + [return: NotNullIfNotNull("hash")] + public static Dictionary? ToDictionary(this HashEntry[]? hash) { - if (hash == null) return null; + if (hash is null) + { + return null; + } var result = new Dictionary(hash.Length); for (int i = 0; i < hash.Length; i++) @@ -51,14 +60,18 @@ public static Dictionary ToDictionary(this HashEntry[] h /// Create a dictionary from an array of SortedSetEntry values. /// /// The set entries to convert to a dictionary. - public static Dictionary ToStringDictionary(this SortedSetEntry[] sortedSet) + [return: NotNullIfNotNull("sortedSet")] + public static Dictionary? ToStringDictionary(this SortedSetEntry[]? sortedSet) { - if (sortedSet == null) return null; + if (sortedSet is null) + { + return null; + } var result = new Dictionary(sortedSet.Length, StringComparer.Ordinal); for (int i = 0; i < sortedSet.Length; i++) { - result.Add(sortedSet[i].element, sortedSet[i].score); + result.Add(sortedSet[i].element!, sortedSet[i].score); } return result; } @@ -67,9 +80,13 @@ public static Dictionary ToStringDictionary(this SortedSetEntry[ /// Create a dictionary from an array of SortedSetEntry values. /// /// The set entries to convert to a dictionary. - public static Dictionary ToDictionary(this SortedSetEntry[] sortedSet) + [return: NotNullIfNotNull("sortedSet")] + public static Dictionary? ToDictionary(this SortedSetEntry[]? sortedSet) { - if (sortedSet == null) return null; + if (sortedSet is null) + { + return null; + } var result = new Dictionary(sortedSet.Length); for (int i = 0; i < sortedSet.Length; i++) @@ -83,14 +100,18 @@ public static Dictionary ToDictionary(this SortedSetEntry[] /// Create a dictionary from an array of key/value pairs. /// /// The pairs to convert to a dictionary. - public static Dictionary ToStringDictionary(this KeyValuePair[] pairs) + [return: NotNullIfNotNull("pairs")] + public static Dictionary? ToStringDictionary(this KeyValuePair[]? pairs) { - if (pairs == null) return null; + if (pairs is null) + { + return null; + } var result = new Dictionary(pairs.Length, StringComparer.Ordinal); for (int i = 0; i < pairs.Length; i++) { - result.Add(pairs[i].Key, pairs[i].Value); + result.Add(pairs[i].Key!, pairs[i].Value!); } return result; } @@ -99,9 +120,13 @@ public static Dictionary ToStringDictionary(this KeyValuePair /// The pairs to convert to a dictionary. - public static Dictionary ToDictionary(this KeyValuePair[] pairs) + [return: NotNullIfNotNull("pairs")] + public static Dictionary? ToDictionary(this KeyValuePair[]? pairs) { - if (pairs == null) return null; + if (pairs is null) + { + return null; + } var result = new Dictionary(pairs.Length); for (int i = 0; i < pairs.Length; i++) @@ -115,9 +140,13 @@ public static Dictionary ToDictionary(this KeyValuePair /// The pairs to convert to a dictionary. - public static Dictionary ToDictionary(this KeyValuePair[] pairs) + [return: NotNullIfNotNull("pairs")] + public static Dictionary? ToDictionary(this KeyValuePair[]? pairs) { - if (pairs == null) return null; + if (pairs is null) + { + return null; + } var result = new Dictionary(pairs.Length, StringComparer.Ordinal); for (int i = 0; i < pairs.Length; i++) @@ -131,9 +160,14 @@ public static Dictionary ToDictionary(this KeyValuePair /// The string array to convert to RedisValues. - public static RedisValue[] ToRedisValueArray(this string[] values) + [return: NotNullIfNotNull("values")] + public static RedisValue[]? ToRedisValueArray(this string[]? values) { - if (values == null) return null; + if (values is null) + { + return null; + } + if (values.Length == 0) return Array.Empty(); return Array.ConvertAll(values, x => (RedisValue)x); } @@ -142,11 +176,16 @@ public static RedisValue[] ToRedisValueArray(this string[] values) /// Create an array of strings from an array of values. /// /// The values to convert to an array. - public static string[] ToStringArray(this RedisValue[] values) + [return: NotNullIfNotNull("values")] + public static string?[]? ToStringArray(this RedisValue[]? values) { - if (values == null) return null; + if (values == null) + { + return null; + } + if (values.Length == 0) return Array.Empty(); - return Array.ConvertAll(values, x => (string)x); + return Array.ConvertAll(values, x => (string?)x); } internal static void AuthenticateAsClient(this SslStream ssl, string host, SslProtocols? allowedProtocols, bool checkCertificateRevocation) @@ -172,12 +211,20 @@ private static void AuthenticateAsClientUsingDefaultProtocols(SslStream ssl, str /// /// The lease upon which to base the stream. /// If true, disposing the stream also disposes the lease. - public static Stream AsStream(this Lease bytes, bool ownsLease = true) + [return: NotNullIfNotNull("bytes")] + public static Stream? AsStream(this Lease? bytes, bool ownsLease = true) { - if (bytes == null) return null; // GIGO + if (bytes is null) + { + return null; // GIGO + } + var segment = bytes.ArraySegment; - if (ownsLease) return new LeaseMemoryStream(segment, bytes); - return new MemoryStream(segment.Array, segment.Offset, segment.Count, false, true); + if (ownsLease) + { + return new LeaseMemoryStream(segment, bytes); + } + return new MemoryStream(segment.Array!, segment.Offset, segment.Count, false, true); } /// @@ -185,13 +232,21 @@ public static Stream AsStream(this Lease bytes, bool ownsLease = true) /// /// The bytes to decode. /// The encoding to use. - public static string DecodeString(this Lease bytes, Encoding encoding = null) + [return: NotNullIfNotNull("bytes")] + public static string? DecodeString(this Lease bytes, Encoding? encoding = null) { - if (bytes == null) return null; + if (bytes is null) + { + return null; + } + encoding ??= Encoding.UTF8; - if (bytes.Length == 0) return ""; + if (bytes.Length == 0) + { + return ""; + } var segment = bytes.ArraySegment; - return encoding.GetString(segment.Array, segment.Offset, segment.Count); + return encoding.GetString(segment.Array!, segment.Offset, segment.Count); } /// @@ -199,17 +254,24 @@ public static string DecodeString(this Lease bytes, Encoding encoding = nu /// /// The bytes to decode. /// The encoding to use. - public static Lease DecodeLease(this Lease bytes, Encoding encoding = null) + [return: NotNullIfNotNull("bytes")] + public static Lease? DecodeLease(this Lease? bytes, Encoding? encoding = null) { - if (bytes == null) return null; + if (bytes is null) + { + return null; + } + encoding ??= Encoding.UTF8; - if (bytes.Length == 0) return Lease.Empty; + if (bytes.Length == 0) + { + return Lease.Empty; + } var bytesSegment = bytes.ArraySegment; - var charCount = encoding.GetCharCount(bytesSegment.Array, bytesSegment.Offset, bytesSegment.Count); + var charCount = encoding.GetCharCount(bytesSegment.Array!, bytesSegment.Offset, bytesSegment.Count); var chars = Lease.Create(charCount, false); var charsSegment = chars.ArraySegment; - encoding.GetChars(bytesSegment.Array, bytesSegment.Offset, bytesSegment.Count, - charsSegment.Array, charsSegment.Offset); + encoding.GetChars(bytesSegment.Array!, bytesSegment.Offset, bytesSegment.Count, charsSegment.Array!, charsSegment.Offset); return chars; } @@ -217,7 +279,7 @@ private sealed class LeaseMemoryStream : MemoryStream { private readonly IDisposable _parent; public LeaseMemoryStream(ArraySegment segment, IDisposable parent) - : base(segment.Array, segment.Offset, segment.Count, false, true) + : base(segment.Array!, segment.Offset, segment.Count, false, true) => _parent = parent; protected override void Dispose(bool disposing) @@ -274,11 +336,11 @@ internal static int VectorSafeIndexOfCRLF(this ReadOnlySpan span) #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static T[] ToArray(in this RawResult result, Projection selector) + internal static T[]? ToArray(in this RawResult result, Projection selector) => result.IsNull ? null : result.GetItems().ToArray(selector); [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static TTo[] ToArray(in this RawResult result, Projection selector, in TState state) + internal static TTo[]? ToArray(in this RawResult result, Projection selector, in TState state) => result.IsNull ? null : result.GetItems().ToArray(selector, in state); } } diff --git a/src/StackExchange.Redis/Format.cs b/src/StackExchange.Redis/Format.cs index 60b9f6e79..040545855 100644 --- a/src/StackExchange.Redis/Format.cs +++ b/src/StackExchange.Redis/Format.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Net; using System.Text; +using System.Diagnostics.CodeAnalysis; #if UNIX_SOCKET using System.Net.Sockets; #endif @@ -46,14 +47,19 @@ public static bool TryParseInt32(string s, out int value) => internal static EndPoint ParseEndPoint(string host, int port) { - if (IPAddress.TryParse(host, out IPAddress ip)) return new IPEndPoint(ip, port); + if (IPAddress.TryParse(host, out IPAddress? ip)) return new IPEndPoint(ip, port); return new DnsEndPoint(host, port); } - internal static EndPoint TryParseEndPoint(string host, string port) + internal static bool TryParseEndPoint(string host, string? port, [NotNullWhen(true)] out EndPoint? endpoint) { - if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(port)) return null; - return TryParseInt32(port, out int i) ? ParseEndPoint(host, i) : null; + if (!host.IsNullOrEmpty() && !port.IsNullOrEmpty() && TryParseInt32(port, out int i)) + { + endpoint = ParseEndPoint(host, i); + return true; + } + endpoint = null; + return false; } internal static string ToString(long value) => value.ToString(NumberFormatInfo.InvariantInfo); @@ -70,18 +76,19 @@ internal static string ToString(double value) return value.ToString("G17", NumberFormatInfo.InvariantInfo); } - internal static string ToString(object value) + [return: NotNullIfNotNull("value")] + internal static string? ToString(object? value) => value switch { - if (value == null) return ""; - if (value is long l) return ToString(l); - if (value is int i) return ToString(i); - if (value is float f) return ToString(f); - if (value is double d) return ToString(d); - if (value is EndPoint e) return ToString(e); - return Convert.ToString(value, CultureInfo.InvariantCulture); - } - - internal static string ToString(EndPoint endpoint) + null => "", + long l => ToString(l), + int i => ToString(i), + float f => ToString(f), + double d => ToString(d), + EndPoint e => ToString(e), + _ => Convert.ToString(value, CultureInfo.InvariantCulture) + }; + + internal static string ToString(EndPoint? endpoint) { switch (endpoint) { @@ -108,9 +115,9 @@ internal static string ToStringHostOnly(EndPoint endpoint) => _ => "" }; - internal static bool TryGetHostPort(EndPoint endpoint, out string host, out int port) + internal static bool TryGetHostPort(EndPoint? endpoint, [NotNullWhen(true)] out string? host, [NotNullWhen(true)] out int? port) { - if (endpoint != null) + if (endpoint is not null) { if (endpoint is IPEndPoint ip) { @@ -126,13 +133,13 @@ internal static bool TryGetHostPort(EndPoint endpoint, out string host, out int } } host = null; - port = 0; + port = null; return false; } - internal static bool TryParseDouble(string s, out double value) + internal static bool TryParseDouble(string? s, out double value) { - if (string.IsNullOrEmpty(s)) + if (s.IsNullOrEmpty()) { value = 0; return false; @@ -227,22 +234,31 @@ private static bool CaseInsensitiveASCIIEqual(string xLowerCase, ReadOnlySpan buffer) diff --git a/src/StackExchange.Redis/GeoEntry.cs b/src/StackExchange.Redis/GeoEntry.cs index acb75a31f..2926b9c3d 100644 --- a/src/StackExchange.Redis/GeoEntry.cs +++ b/src/StackExchange.Redis/GeoEntry.cs @@ -128,7 +128,7 @@ public GeoPosition(double longitude, double latitude) /// Compares two values for equality. /// /// The to compare to. - public override bool Equals(object obj) => obj is GeoPosition gpObj && Equals(gpObj); + public override bool Equals(object? obj) => obj is GeoPosition gpObj && Equals(gpObj); /// /// Compares two values for equality. @@ -201,7 +201,7 @@ public GeoEntry(double longitude, double latitude, RedisValue member) /// Compares two values for equality. /// /// The to compare to. - public override bool Equals(object obj) => obj is GeoEntry geObj && Equals(geObj); + public override bool Equals(object? obj) => obj is GeoEntry geObj && Equals(geObj); /// /// Compares two values for equality. diff --git a/src/StackExchange.Redis/HashEntry.cs b/src/StackExchange.Redis/HashEntry.cs index c0f89ca1a..e1a62380e 100644 --- a/src/StackExchange.Redis/HashEntry.cs +++ b/src/StackExchange.Redis/HashEntry.cs @@ -65,7 +65,7 @@ public static implicit operator HashEntry(KeyValuePair v /// Compares two values for equality. /// /// The to compare to. - public override bool Equals(object obj) => obj is HashEntry heObj && Equals(heObj); + public override bool Equals(object? obj) => obj is HashEntry heObj && Equals(heObj); /// /// Compares two values for equality. diff --git a/src/StackExchange.Redis/HashSlotMovedEventArgs.cs b/src/StackExchange.Redis/HashSlotMovedEventArgs.cs index 0204359d9..d92c2af8c 100644 --- a/src/StackExchange.Redis/HashSlotMovedEventArgs.cs +++ b/src/StackExchange.Redis/HashSlotMovedEventArgs.cs @@ -10,7 +10,7 @@ namespace StackExchange.Redis public class HashSlotMovedEventArgs : EventArgs, ICompletable { private readonly object sender; - private readonly EventHandler handler; + private readonly EventHandler? handler; /// /// The hash-slot that was relocated. @@ -20,15 +20,15 @@ public class HashSlotMovedEventArgs : EventArgs, ICompletable /// /// The old endpoint for this hash-slot (if known). /// - public EndPoint OldEndPoint { get; } + public EndPoint? OldEndPoint { get; } /// /// The new endpoint for this hash-slot (if known). /// public EndPoint NewEndPoint { get; } - internal HashSlotMovedEventArgs(EventHandler handler, object sender, - int hashSlot, EndPoint old, EndPoint @new) + internal HashSlotMovedEventArgs(EventHandler? handler, object sender, + int hashSlot, EndPoint? old, EndPoint @new) { this.handler = handler; this.sender = sender; diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index 6b46ffa24..0a898638a 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -154,14 +154,14 @@ public interface IConnectionMultiplexer : IDisposable /// Obtain a pub/sub subscriber connection to the specified server. /// /// The async state to pass to the created . - ISubscriber GetSubscriber(object asyncState = null); + ISubscriber GetSubscriber(object? asyncState = null); /// /// Obtain an interactive connection to a database inside redis. /// /// The database ID to get. /// The async state to pass to the created . - IDatabase GetDatabase(int db = -1, object asyncState = null); + IDatabase GetDatabase(int db = -1, object? asyncState = null); /// /// Obtain a configuration API for an individual server. @@ -169,14 +169,14 @@ public interface IConnectionMultiplexer : IDisposable /// The host to get a server for. /// The specific port for to get a server for. /// The async state to pass to the created . - IServer GetServer(string host, int port, object asyncState = null); + IServer GetServer(string host, int port, object? asyncState = null); /// /// Obtain a configuration API for an individual server. /// /// The "host:port" string to get a server for. /// The async state to pass to the created . - IServer GetServer(string hostAndPort, object asyncState = null); + IServer GetServer(string hostAndPort, object? asyncState = null); /// /// Obtain a configuration API for an individual server. @@ -190,19 +190,19 @@ public interface IConnectionMultiplexer : IDisposable /// /// The endpoint to get a server for. /// The async state to pass to the created . - IServer GetServer(EndPoint endpoint, object asyncState = null); + IServer GetServer(EndPoint endpoint, object? asyncState = null); /// /// Reconfigure the current connections based on the existing configuration. /// /// The log to write output to. - Task ConfigureAsync(TextWriter log = null); + Task ConfigureAsync(TextWriter? log = null); /// /// Reconfigure the current connections based on the existing configuration. /// /// The log to write output to. - bool Configure(TextWriter log = null); + bool Configure(TextWriter? log = null); /// /// Provides a text overview of the status of all connections. @@ -235,7 +235,7 @@ public interface IConnectionMultiplexer : IDisposable /// /// Obtains the log of unusual busy patterns. /// - string GetStormLog(); + string? GetStormLog(); /// /// Resets the log of unusual busy patterns. diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index c7c1d766c..213a7daf7 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -20,7 +20,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The async object state to be passed into the created . /// The created batch. - IBatch CreateBatch(object asyncState = null); + IBatch CreateBatch(object? asyncState = null); /// /// Allows creation of a group of operations that will be sent to the server as a single unit, @@ -28,7 +28,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The async object state to be passed into the created . /// The created transaction. - ITransaction CreateTransaction(object asyncState = null); + ITransaction CreateTransaction(object? asyncState = null); /// /// Atomically transfer a key from a source Redis instance to a destination Redis instance. @@ -122,7 +122,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The command returns an array where each element is the Geohash corresponding to each member name passed as argument to the command. /// https://redis.io/commands/geohash - string[] GeoHash(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); + string?[] GeoHash(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); /// /// Return valid Geohash strings representing the position of one or more elements in a sorted set value representing a geospatial index (where elements were added using GEOADD). @@ -132,7 +132,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The command returns an array where each element is the Geohash corresponding to each member name passed as argument to the command. /// https://redis.io/commands/geohash - string GeoHash(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + string? GeoHash(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// /// Return the positions (longitude,latitude) of all the specified members of the geospatial index represented by the sorted set at key. @@ -270,7 +270,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The value associated with field, or nil when field is not present in the hash or key does not exist. /// https://redis.io/commands/hget - Lease HashGetLease(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); + Lease? HashGetLease(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// /// Returns the values associated with the specified fields in the hash stored at key. @@ -360,7 +360,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// Yields all elements of the hash matching the pattern. /// https://redis.io/commands/hscan - IEnumerable HashScan(RedisKey key, RedisValue pattern = default(RedisValue), int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + IEnumerable HashScan(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// /// Sets the specified fields to their respective values in the hash stored at key. @@ -470,7 +470,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key to check. /// The flags to use for this operation. /// The endpoint serving the key. - EndPoint IdentifyEndpoint(RedisKey key = default(RedisKey), CommandFlags flags = CommandFlags.None); + EndPoint? IdentifyEndpoint(RedisKey key = default, CommandFlags flags = CommandFlags.None); /// /// Removes the specified key. A key is ignored if it does not exist. @@ -502,7 +502,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The serialized value. /// https://redis.io/commands/dump - byte[] KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None); + byte[]? KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None); /// /// Returns if key exists. @@ -963,7 +963,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// A dynamic representation of the script's result. /// https://redis.io/commands/eval /// https://redis.io/commands/evalsha - RedisResult ScriptEvaluate(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None); + RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); /// /// Execute a Lua script against the server using just the SHA1 hash. @@ -974,7 +974,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// A dynamic representation of the script's result. /// https://redis.io/commands/evalsha - RedisResult ScriptEvaluate(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None); + RedisResult ScriptEvaluate(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); /// /// Execute a lua script against the server, using previously prepared script. @@ -985,7 +985,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// A dynamic representation of the script's result. /// https://redis.io/commands/eval - RedisResult ScriptEvaluate(LuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None); + RedisResult ScriptEvaluate(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None); /// /// Execute a lua script against the server, using previously prepared and loaded script. @@ -997,7 +997,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// A dynamic representation of the script's result. /// https://redis.io/commands/eval - RedisResult ScriptEvaluate(LoadedLuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None); + RedisResult ScriptEvaluate(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None); /// /// Add the specified member to the set stored at key. @@ -1209,7 +1209,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// Yields all matching elements of the set. /// https://redis.io/commands/sscan - IEnumerable SetScan(RedisKey key, RedisValue pattern = default(RedisValue), int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + IEnumerable SetScan(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// /// Sorts a list, set or sorted set (numerically or alphabetically, ascending by default). @@ -1229,7 +1229,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The sorted elements, or the external values if get is specified. /// https://redis.io/commands/sort - RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None); + RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); /// /// Sorts a list, set or sorted set (numerically or alphabetically, ascending by default). @@ -1250,7 +1250,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The number of elements stored in the new list. /// https://redis.io/commands/sort - long SortAndStore(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None); + long SortAndStore(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); /// /// Adds the specified member with the specified score to the sorted set stored at key. @@ -1328,7 +1328,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/zunionstore /// https://redis.io/commands/zinterstore /// The number of elements in the resulting sorted set at destination. - long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[] weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// /// Decrements the score of member in the sorted set stored at key by decrement. @@ -1535,8 +1535,8 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// https://redis.io/commands/zrevrangebylex /// List of elements in the specified score range. RedisValue[] SortedSetRangeByValue(RedisKey key, - RedisValue min = default(RedisValue), - RedisValue max = default(RedisValue), + RedisValue min = default, + RedisValue max = default, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, @@ -1639,7 +1639,7 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// Yields all matching elements of the sorted set. /// https://redis.io/commands/zscan IEnumerable SortedSetScan(RedisKey key, - RedisValue pattern = default(RedisValue), + RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, @@ -2032,7 +2032,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The flags to use for this operation. /// The size of the string stored in the destination key, that is equal to the size of the longest input string. /// https://redis.io/commands/bitop - long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default(RedisKey), CommandFlags flags = CommandFlags.None); + long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, CommandFlags flags = CommandFlags.None); /// /// Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key. @@ -2117,7 +2117,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The flags to use for this operation. /// The value of key, or nil when key does not exist. /// https://redis.io/commands/get - Lease StringGetLease(RedisKey key, CommandFlags flags = CommandFlags.None); + Lease? StringGetLease(RedisKey key, CommandFlags flags = CommandFlags.None); /// /// Returns the bit value at offset in the string value stored at key. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index b9c4edbf1..0a9eb3ace 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -109,7 +109,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// The command returns an array where each element is the Geohash corresponding to each member name passed as argument to the command. /// https://redis.io/commands/geohash - Task GeoHashAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); + Task GeoHashAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); /// /// Return valid Geohash strings representing the position of one or more elements in a sorted set value representing a geospatial index (where elements were added using GEOADD). @@ -119,7 +119,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// The command returns an array where each element is the Geohash corresponding to each member name passed as argument to the command. /// https://redis.io/commands/geohash - Task GeoHashAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + Task GeoHashAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// /// Return the positions (longitude,latitude) of all the specified members of the geospatial index represented by the sorted set at key. @@ -257,7 +257,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// The value associated with field, or nil when field is not present in the hash or key does not exist. /// https://redis.io/commands/hget - Task> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); + Task?> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// /// Returns the values associated with the specified fields in the hash stored at key. @@ -336,7 +336,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// Yields all elements of the hash matching the pattern. /// https://redis.io/commands/hscan - IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern = default(RedisValue), int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// /// Sets the specified fields to their respective values in the hash stored at key. @@ -446,7 +446,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key to check. /// The flags to use for this operation. /// The endpoint serving the key. - Task IdentifyEndpointAsync(RedisKey key = default(RedisKey), CommandFlags flags = CommandFlags.None); + Task IdentifyEndpointAsync(RedisKey key = default, CommandFlags flags = CommandFlags.None); /// /// Removes the specified key. A key is ignored if it does not exist. @@ -478,7 +478,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// The serialized value. /// https://redis.io/commands/dump - Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// /// Returns if key exists. @@ -927,7 +927,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// This API should be considered an advanced feature; inappropriate use can be harmful. /// A dynamic representation of the command's result. - Task ExecuteAsync(string command, ICollection args, CommandFlags flags = CommandFlags.None); + Task ExecuteAsync(string command, ICollection? args, CommandFlags flags = CommandFlags.None); /// /// Execute a Lua script against the server. @@ -939,7 +939,7 @@ public interface IDatabaseAsync : IRedisAsync /// A dynamic representation of the script's result. /// https://redis.io/commands/eval /// https://redis.io/commands/evalsha - Task ScriptEvaluateAsync(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None); + Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); /// /// Execute a Lua script against the server using just the SHA1 hash. @@ -950,7 +950,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// A dynamic representation of the script's result. /// https://redis.io/commands/evalsha - Task ScriptEvaluateAsync(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None); + Task ScriptEvaluateAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); /// /// Execute a lua script against the server, using previously prepared script. @@ -961,7 +961,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// A dynamic representation of the script's result. /// https://redis.io/commands/eval - Task ScriptEvaluateAsync(LuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None); + Task ScriptEvaluateAsync(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None); /// /// Execute a lua script against the server, using previously prepared and loaded script. @@ -973,7 +973,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// A dynamic representation of the script's result. /// https://redis.io/commands/eval - Task ScriptEvaluateAsync(LoadedLuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None); + Task ScriptEvaluateAsync(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None); /// /// Add the specified member to the set stored at key. @@ -1174,7 +1174,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// Yields all matching elements of the set. /// https://redis.io/commands/sscan - IAsyncEnumerable SetScanAsync(RedisKey key, RedisValue pattern = default(RedisValue), int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + IAsyncEnumerable SetScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// /// Sorts a list, set or sorted set (numerically or alphabetically, ascending by default). @@ -1194,7 +1194,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// The sorted elements, or the external values if get is specified. /// https://redis.io/commands/sort - Task SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None); + Task SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); /// /// Sorts a list, set or sorted set (numerically or alphabetically, ascending by default). @@ -1215,7 +1215,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// The number of elements stored in the new list. /// https://redis.io/commands/sort - Task SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None); + Task SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); /// /// Adds the specified member with the specified score to the sorted set stored at key. @@ -1293,7 +1293,7 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/zunionstore /// https://redis.io/commands/zinterstore /// The number of elements in the resulting sorted set at destination. - Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[] weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// /// Decrements the score of member in the sorted set stored at key by decrement. @@ -1500,8 +1500,8 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// https://redis.io/commands/zrevrangebylex /// List of elements in the specified score range. Task SortedSetRangeByValueAsync(RedisKey key, - RedisValue min = default(RedisValue), - RedisValue max = default(RedisValue), + RedisValue min = default, + RedisValue max = default, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, @@ -1592,7 +1592,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// Yields all matching elements of the sorted set. /// https://redis.io/commands/zscan IAsyncEnumerable SortedSetScanAsync(RedisKey key, - RedisValue pattern = default(RedisValue), + RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, @@ -1985,7 +1985,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The flags to use for this operation. /// The size of the string stored in the destination key, that is equal to the size of the longest input string. /// https://redis.io/commands/bitop - Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default(RedisKey), CommandFlags flags = CommandFlags.None); + Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, CommandFlags flags = CommandFlags.None); /// /// Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key. @@ -2070,7 +2070,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The flags to use for this operation. /// The value of key, or nil when key does not exist. /// https://redis.io/commands/get - Task> StringGetLeaseAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + Task?> StringGetLeaseAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// /// Returns the bit value at offset in the string value stored at key. diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index c6980caae..6941e895c 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -16,7 +16,7 @@ public partial interface IServer : IRedis /// /// Gets the cluster configuration associated with this server, if known. /// - ClusterConfiguration ClusterConfiguration { get; } + ClusterConfiguration? ClusterConfiguration { get; } /// /// Gets the address of the connected server. @@ -106,7 +106,7 @@ public partial interface IServer : IRedis /// The command flags to use. /// the number of clients killed. /// https://redis.io/commands/client-kill - long ClientKill(long? id = null, ClientType? clientType = null, EndPoint endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None); + long ClientKill(long? id = null, ClientType? clientType = null, EndPoint? endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None); /// /// The CLIENT KILL command closes multiple connections that match the specified filters. @@ -118,7 +118,7 @@ public partial interface IServer : IRedis /// The command flags to use. /// the number of clients killed. /// https://redis.io/commands/client-kill - Task ClientKillAsync(long? id = null, ClientType? clientType = null, EndPoint endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None); + Task ClientKillAsync(long? id = null, ClientType? clientType = null, EndPoint? endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None); /// /// The CLIENT LIST command returns information and statistics about the client connections server in a mostly human readable format. @@ -138,25 +138,29 @@ public partial interface IServer : IRedis /// Obtains the current CLUSTER NODES output from a cluster server. /// /// The command flags to use. - ClusterConfiguration ClusterNodes(CommandFlags flags = CommandFlags.None); + /// https://redis.io/commands/cluster-nodes/ + ClusterConfiguration? ClusterNodes(CommandFlags flags = CommandFlags.None); /// /// Obtains the current CLUSTER NODES output from a cluster server. /// /// The command flags to use. - Task ClusterNodesAsync(CommandFlags flags = CommandFlags.None); + /// https://redis.io/commands/cluster-nodes/ + Task ClusterNodesAsync(CommandFlags flags = CommandFlags.None); /// /// Obtains the current raw CLUSTER NODES output from a cluster server. /// /// The command flags to use. - string ClusterNodesRaw(CommandFlags flags = CommandFlags.None); + /// https://redis.io/commands/cluster-nodes/ + string? ClusterNodesRaw(CommandFlags flags = CommandFlags.None); /// /// Obtains the current raw CLUSTER NODES output from a cluster server. /// /// The command flags to use. - Task ClusterNodesRawAsync(CommandFlags flags = CommandFlags.None); + /// https://redis.io/commands/cluster-nodes/ + Task ClusterNodesRawAsync(CommandFlags flags = CommandFlags.None); /// /// Get all configuration parameters matching the specified pattern. @@ -165,7 +169,7 @@ public partial interface IServer : IRedis /// The command flags to use. /// All matching configuration parameters. /// https://redis.io/commands/config-get - KeyValuePair[] ConfigGet(RedisValue pattern = default(RedisValue), CommandFlags flags = CommandFlags.None); + KeyValuePair[] ConfigGet(RedisValue pattern = default, CommandFlags flags = CommandFlags.None); /// /// Get all configuration parameters matching the specified pattern. @@ -174,7 +178,7 @@ public partial interface IServer : IRedis /// The command flags to use. /// All matching configuration parameters. /// https://redis.io/commands/config-get - Task[]> ConfigGetAsync(RedisValue pattern = default(RedisValue), CommandFlags flags = CommandFlags.None); + Task[]> ConfigGetAsync(RedisValue pattern = default, CommandFlags flags = CommandFlags.None); /// /// Resets the statistics reported by Redis using the INFO command. @@ -347,7 +351,7 @@ public partial interface IServer : IRedis /// The info section to get, if getting a specific one. /// The command flags to use. /// https://redis.io/commands/info - IGrouping>[] Info(RedisValue section = default(RedisValue), CommandFlags flags = CommandFlags.None); + IGrouping>[] Info(RedisValue section = default, CommandFlags flags = CommandFlags.None); /// /// The INFO command returns information and statistics about the server in a format that is simple to parse by computers and easy to read by humans. @@ -355,7 +359,7 @@ public partial interface IServer : IRedis /// The info section to get, if getting a specific one. /// The command flags to use. /// https://redis.io/commands/info - Task>[]> InfoAsync(RedisValue section = default(RedisValue), CommandFlags flags = CommandFlags.None); + Task>[]> InfoAsync(RedisValue section = default, CommandFlags flags = CommandFlags.None); /// /// The INFO command returns information and statistics about the server in a format that is simple to parse by computers and easy to read by humans. @@ -363,7 +367,7 @@ public partial interface IServer : IRedis /// The info section to get, if getting a specific one. /// The command flags to use. /// https://redis.io/commands/info - string InfoRaw(RedisValue section = default(RedisValue), CommandFlags flags = CommandFlags.None); + string? InfoRaw(RedisValue section = default, CommandFlags flags = CommandFlags.None); /// /// The INFO command returns information and statistics about the server in a format that is simple to parse by computers and easy to read by humans. @@ -371,7 +375,7 @@ public partial interface IServer : IRedis /// The info section to get, if getting a specific one. /// The command flags to use. /// https://redis.io/commands/info - Task InfoRawAsync(RedisValue section = default(RedisValue), CommandFlags flags = CommandFlags.None); + Task InfoRawAsync(RedisValue section = default, CommandFlags flags = CommandFlags.None); /// /// Returns all keys matching pattern; the KEYS or SCAN commands will be used based on the server capabilities. @@ -399,7 +403,7 @@ public partial interface IServer : IRedis /// Warning: consider KEYS as a command that should only be used in production environments with extreme care. /// https://redis.io/commands/keys /// https://redis.io/commands/scan - IEnumerable Keys(int database = -1, RedisValue pattern = default(RedisValue), int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + IEnumerable Keys(int database = -1, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// /// Returns all keys matching pattern. @@ -415,7 +419,7 @@ public partial interface IServer : IRedis /// Warning: consider KEYS as a command that should only be used in production environments with extreme care. /// https://redis.io/commands/keys /// https://redis.io/commands/scan - IAsyncEnumerable KeysAsync(int database = -1, RedisValue pattern = default(RedisValue), int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + IAsyncEnumerable KeysAsync(int database = -1, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// /// Return the time of the last DB save executed with success. @@ -440,15 +444,17 @@ public partial interface IServer : IRedis /// /// The options to use for this topology change. /// The log to write output to. + /// https://redis.io/commands/replicaof/ [Obsolete("Please use " + nameof(MakePrimaryAsync) + ", this will be removed in 3.0.")] - void MakeMaster(ReplicationChangeOptions options, TextWriter log = null); + void MakeMaster(ReplicationChangeOptions options, TextWriter? log = null); /// /// Promote the selected node to be primary. /// /// The options to use for this topology change. /// The log to write output to. - Task MakePrimaryAsync(ReplicationChangeOptions options, TextWriter log = null); + /// https://redis.io/commands/replicaof/ + Task MakePrimaryAsync(ReplicationChangeOptions options, TextWriter? log = null); /// /// Returns the role info for the current server. @@ -489,6 +495,7 @@ public partial interface IServer : IRedis /// /// The text of the script to check for on the server. /// The command flags to use. + /// https://redis.io/commands/script-exists/ bool ScriptExists(string script, CommandFlags flags = CommandFlags.None); /// @@ -496,6 +503,7 @@ public partial interface IServer : IRedis /// /// The SHA1 of the script to check for on the server. /// The command flags to use. + /// https://redis.io/commands/script-exists/ bool ScriptExists(byte[] sha1, CommandFlags flags = CommandFlags.None); /// @@ -503,6 +511,7 @@ public partial interface IServer : IRedis /// /// The text of the script to check for on the server. /// The command flags to use. + /// https://redis.io/commands/script-exists/ Task ScriptExistsAsync(string script, CommandFlags flags = CommandFlags.None); /// @@ -510,18 +519,21 @@ public partial interface IServer : IRedis /// /// The SHA1 of the script to check for on the server. /// The command flags to use. + /// https://redis.io/commands/script-exists/ Task ScriptExistsAsync(byte[] sha1, CommandFlags flags = CommandFlags.None); /// /// Removes all cached scripts on this server. /// /// The command flags to use. + /// https://redis.io/commands/script-flush/ void ScriptFlush(CommandFlags flags = CommandFlags.None); /// /// Removes all cached scripts on this server. /// /// The command flags to use. + /// https://redis.io/commands/script-flush/ Task ScriptFlushAsync(CommandFlags flags = CommandFlags.None); /// @@ -529,6 +541,7 @@ public partial interface IServer : IRedis /// /// The script to load. /// The command flags to use. + /// https://redis.io/commands/script-load/ byte[] ScriptLoad(string script, CommandFlags flags = CommandFlags.None); /// @@ -536,6 +549,7 @@ public partial interface IServer : IRedis /// /// The script to load. /// The command flags to use. + /// https://redis.io/commands/script-load/ LoadedLuaScript ScriptLoad(LuaScript script, CommandFlags flags = CommandFlags.None); /// @@ -543,6 +557,7 @@ public partial interface IServer : IRedis /// /// The script to load. /// The command flags to use. + /// https://redis.io/commands/script-load/ Task ScriptLoadAsync(string script, CommandFlags flags = CommandFlags.None); /// @@ -550,6 +565,7 @@ public partial interface IServer : IRedis /// /// The script to load. /// The command flags to use. + /// https://redis.io/commands/script-load/ Task ScriptLoadAsync(LuaScript script, CommandFlags flags = CommandFlags.None); /// @@ -649,7 +665,7 @@ public partial interface IServer : IRedis /// The command flags to use. /// a list of active channels, optionally matching the specified pattern. /// https://redis.io/commands/pubsub - RedisChannel[] SubscriptionChannels(RedisChannel pattern = default(RedisChannel), CommandFlags flags = CommandFlags.None); + RedisChannel[] SubscriptionChannels(RedisChannel pattern = default, CommandFlags flags = CommandFlags.None); /// /// Lists the currently active channels. @@ -659,7 +675,7 @@ public partial interface IServer : IRedis /// The command flags to use. /// a list of active channels, optionally matching the specified pattern. /// https://redis.io/commands/pubsub - Task SubscriptionChannelsAsync(RedisChannel pattern = default(RedisChannel), CommandFlags flags = CommandFlags.None); + Task SubscriptionChannelsAsync(RedisChannel pattern = default, CommandFlags flags = CommandFlags.None); /// /// Returns the number of subscriptions to patterns (that are performed using the PSUBSCRIBE command). @@ -732,12 +748,12 @@ public partial interface IServer : IRedis Task TimeAsync(CommandFlags flags = CommandFlags.None); /// - /// Gets a text-based latency diagnostic + /// Gets a text-based latency diagnostic. /// /// https://redis.io/topics/latency-monitor Task LatencyDoctorAsync(CommandFlags flags = CommandFlags.None); /// - /// Gets a text-based latency diagnostic + /// Gets a text-based latency diagnostic. /// /// https://redis.io/topics/latency-monitor string LatencyDoctor(CommandFlags flags = CommandFlags.None); @@ -746,12 +762,12 @@ public partial interface IServer : IRedis /// Resets the given events (or all if none are specified), discarding the currently logged latency spike events, and resetting the maximum event time register. /// /// https://redis.io/topics/latency-monitor - Task LatencyResetAsync(string[] eventNames = null, CommandFlags flags = CommandFlags.None); + Task LatencyResetAsync(string[]? eventNames = null, CommandFlags flags = CommandFlags.None); /// /// Resets the given events (or all if none are specified), discarding the currently logged latency spike events, and resetting the maximum event time register. /// /// https://redis.io/topics/latency-monitor - long LatencyReset(string[] eventNames = null, CommandFlags flags = CommandFlags.None); + long LatencyReset(string[]? eventNames = null, CommandFlags flags = CommandFlags.None); /// /// Fetch raw latency data from the event time series, as timestamp-latency pairs @@ -815,13 +831,13 @@ public partial interface IServer : IRedis /// Provides an internal statistics report from the memory allocator. /// /// https://redis.io/commands/memory-malloc-stats - Task MemoryAllocatorStatsAsync(CommandFlags flags = CommandFlags.None); + Task MemoryAllocatorStatsAsync(CommandFlags flags = CommandFlags.None); /// /// Provides an internal statistics report from the memory allocator. /// /// https://redis.io/commands/memory-malloc-stats - string MemoryAllocatorStats(CommandFlags flags = CommandFlags.None); + string? MemoryAllocatorStats(CommandFlags flags = CommandFlags.None); #region Sentinel @@ -833,7 +849,7 @@ public partial interface IServer : IRedis /// The command flags to use. /// The primary IP and port. /// https://redis.io/topics/sentinel - EndPoint SentinelGetMasterAddressByName(string serviceName, CommandFlags flags = CommandFlags.None); + EndPoint? SentinelGetMasterAddressByName(string serviceName, CommandFlags flags = CommandFlags.None); /// /// Returns the IP and port number of the primary with that name. @@ -843,7 +859,7 @@ public partial interface IServer : IRedis /// The command flags to use. /// The primary IP and port. /// https://redis.io/topics/sentinel - Task SentinelGetMasterAddressByNameAsync(string serviceName, CommandFlags flags = CommandFlags.None); + Task SentinelGetMasterAddressByNameAsync(string serviceName, CommandFlags flags = CommandFlags.None); /// /// Returns the IP and port numbers of all known Sentinels for the given service name. @@ -1051,7 +1067,7 @@ protected override bool TryParse(in RawResult raw, out LatencyLatestEntry parsed && items[2].TryGetInt64(out var duration) && items[3].TryGetInt64(out var maxDuration)) { - parsed = new LatencyLatestEntry(items[0].GetString(), timestamp, duration, maxDuration); + parsed = new LatencyLatestEntry(items[0].GetString()!, timestamp, duration, maxDuration); return true; } } diff --git a/src/StackExchange.Redis/Interfaces/ISubscriber.cs b/src/StackExchange.Redis/Interfaces/ISubscriber.cs index dd455a6dd..87a2b20a7 100644 --- a/src/StackExchange.Redis/Interfaces/ISubscriber.cs +++ b/src/StackExchange.Redis/Interfaces/ISubscriber.cs @@ -14,14 +14,14 @@ public interface ISubscriber : IRedis /// /// The channel to identify the server endpoint by. /// The command flags to use. - EndPoint IdentifyEndpoint(RedisChannel channel, CommandFlags flags = CommandFlags.None); + EndPoint? IdentifyEndpoint(RedisChannel channel, CommandFlags flags = CommandFlags.None); /// /// Indicate exactly which redis server we are talking to. /// /// The channel to identify the server endpoint by. /// The command flags to use. - Task IdentifyEndpointAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None); + Task IdentifyEndpointAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None); /// /// Indicates whether the instance can communicate with the server. @@ -30,7 +30,7 @@ public interface ISubscriber : IRedis /// server is chosen arbitrarily from the primaries. /// /// The channel to identify the server endpoint by. - bool IsConnected(RedisChannel channel = default(RedisChannel)); + bool IsConnected(RedisChannel channel = default); /// /// Posts a message to the given channel. @@ -103,7 +103,7 @@ public interface ISubscriber : IRedis /// /// The channel to check which server endpoint was subscribed on. /// The subscribed endpoint for the given , if the channel is not actively subscribed. - EndPoint SubscribedEndpoint(RedisChannel channel); + EndPoint? SubscribedEndpoint(RedisChannel channel); /// /// Unsubscribe from a specified message channel. @@ -115,7 +115,7 @@ public interface ISubscriber : IRedis /// The command flags to use. /// https://redis.io/commands/unsubscribe /// https://redis.io/commands/punsubscribe - void Unsubscribe(RedisChannel channel, Action handler = null, CommandFlags flags = CommandFlags.None); + void Unsubscribe(RedisChannel channel, Action? handler = null, CommandFlags flags = CommandFlags.None); /// /// Unsubscribe all subscriptions on this instance. @@ -143,6 +143,6 @@ public interface ISubscriber : IRedis /// The command flags to use. /// https://redis.io/commands/unsubscribe /// https://redis.io/commands/punsubscribe - Task UnsubscribeAsync(RedisChannel channel, Action handler = null, CommandFlags flags = CommandFlags.None); + Task UnsubscribeAsync(RedisChannel channel, Action? handler = null, CommandFlags flags = CommandFlags.None); } } diff --git a/src/StackExchange.Redis/InternalErrorEventArgs.cs b/src/StackExchange.Redis/InternalErrorEventArgs.cs index 7403b0398..8693d66f7 100644 --- a/src/StackExchange.Redis/InternalErrorEventArgs.cs +++ b/src/StackExchange.Redis/InternalErrorEventArgs.cs @@ -9,9 +9,9 @@ namespace StackExchange.Redis /// public class InternalErrorEventArgs : EventArgs, ICompletable { - private readonly EventHandler handler; + private readonly EventHandler? handler; private readonly object sender; - internal InternalErrorEventArgs(EventHandler handler, object sender, EndPoint endpoint, ConnectionType connectionType, Exception exception, string origin) + internal InternalErrorEventArgs(EventHandler? handler, object sender, EndPoint? endpoint, ConnectionType connectionType, Exception exception, string? origin) { this.handler = handler; this.sender = sender; @@ -42,7 +42,7 @@ public InternalErrorEventArgs(object sender, EndPoint endpoint, ConnectionType c /// /// Gets the failing server-endpoint (this can be null). /// - public EndPoint EndPoint { get; } + public EndPoint? EndPoint { get; } /// /// Gets the exception if available (this can be null). @@ -52,7 +52,7 @@ public InternalErrorEventArgs(object sender, EndPoint endpoint, ConnectionType c /// /// The underlying origin of the error. /// - public string Origin { get; } + public string? Origin { get; } void ICompletable.AppendStormLog(StringBuilder sb) { diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs index caa68f34e..7a327b2a8 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs @@ -61,7 +61,7 @@ public static IDatabase WithKeyPrefix(this IDatabase database, RedisKey keyPrefi database = wrapper.Inner; } - return new DatabaseWrapper(database, keyPrefix.AsPrefix()); + return new DatabaseWrapper(database, keyPrefix.AsPrefix()!); } } } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 7a4f6b396..411c86af0 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -10,396 +10,240 @@ public DatabaseWrapper(IDatabase inner, byte[] prefix) : base(inner, prefix) { } - public IBatch CreateBatch(object asyncState = null) - { - return new BatchWrapper(Inner.CreateBatch(asyncState), Prefix); - } + public IBatch CreateBatch(object? asyncState = null) => + new BatchWrapper(Inner.CreateBatch(asyncState), Prefix); - public ITransaction CreateTransaction(object asyncState = null) - { - return new TransactionWrapper(Inner.CreateTransaction(asyncState), Prefix); - } + public ITransaction CreateTransaction(object? asyncState = null) => + new TransactionWrapper(Inner.CreateTransaction(asyncState), Prefix); public int Database => Inner.Database; - public RedisValue DebugObject(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.DebugObject(ToInner(key), flags); - } + public RedisValue DebugObject(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.DebugObject(ToInner(key), flags); - public bool GeoAdd(RedisKey key, double longitude, double latitude, RedisValue member, CommandFlags flags = CommandFlags.None) - { - return Inner.GeoAdd(ToInner(key), longitude, latitude, member, flags); - } + public bool GeoAdd(RedisKey key, double longitude, double latitude, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.GeoAdd(ToInner(key), longitude, latitude, member, flags); - public long GeoAdd(RedisKey key, GeoEntry[] values, CommandFlags flags = CommandFlags.None) - { - return Inner.GeoAdd(ToInner(key), values, flags); - } + public long GeoAdd(RedisKey key, GeoEntry[] values, CommandFlags flags = CommandFlags.None) => + Inner.GeoAdd(ToInner(key), values, flags); - public bool GeoAdd(RedisKey key, GeoEntry value, CommandFlags flags = CommandFlags.None) - { - return Inner.GeoAdd(ToInner(key), value, flags); - } + public bool GeoAdd(RedisKey key, GeoEntry value, CommandFlags flags = CommandFlags.None) => + Inner.GeoAdd(ToInner(key), value, flags); - public bool GeoRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - return Inner.GeoRemove(ToInner(key), member, flags); - } + public bool GeoRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.GeoRemove(ToInner(key), member, flags); - public double? GeoDistance(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters,CommandFlags flags = CommandFlags.None) - { - return Inner.GeoDistance(ToInner(key), member1, member2, unit, flags); - } + public double? GeoDistance(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters,CommandFlags flags = CommandFlags.None) => + Inner.GeoDistance(ToInner(key), member1, member2, unit, flags); - public string[] GeoHash(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) - { - return Inner.GeoHash(ToInner(key), members, flags); - } + public string?[] GeoHash(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + Inner.GeoHash(ToInner(key), members, flags); - public string GeoHash(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - return Inner.GeoHash(ToInner(key), member, flags); - } + public string? GeoHash(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.GeoHash(ToInner(key), member, flags); - public GeoPosition?[] GeoPosition(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) - { - return Inner.GeoPosition(ToInner(key), members, flags); - } + public GeoPosition?[] GeoPosition(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + Inner.GeoPosition(ToInner(key), members, flags); - public GeoPosition? GeoPosition(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - return Inner.GeoPosition(ToInner(key), member, flags); - } + public GeoPosition? GeoPosition(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.GeoPosition(ToInner(key), member, flags); - public GeoRadiusResult[] GeoRadius(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null,GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) - { - return Inner.GeoRadius(ToInner(key), member, radius, unit, count, order, options, flags); - } + public GeoRadiusResult[] GeoRadius(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null,GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + Inner.GeoRadius(ToInner(key), member, radius, unit, count, order, options, flags); - public GeoRadiusResult[] GeoRadius(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) - { - return Inner.GeoRadius(ToInner(key), longitude, latitude, radius, unit, count, order, options, flags); - } + public GeoRadiusResult[] GeoRadius(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + Inner.GeoRadius(ToInner(key), longitude, latitude, radius, unit, count, order, options, flags); - public double HashDecrement(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) - { - return Inner.HashDecrement(ToInner(key), hashField, value, flags); - } + public double HashDecrement(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) => + Inner.HashDecrement(ToInner(key), hashField, value, flags); - public long HashDecrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) - { - return Inner.HashDecrement(ToInner(key), hashField, value, flags); - } + public long HashDecrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) => + Inner.HashDecrement(ToInner(key), hashField, value, flags); - public long HashDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) - { - return Inner.HashDelete(ToInner(key), hashFields, flags); - } + public long HashDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + Inner.HashDelete(ToInner(key), hashFields, flags); - public bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - { - return Inner.HashDelete(ToInner(key), hashField, flags); - } + public bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + Inner.HashDelete(ToInner(key), hashField, flags); - public bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - { - return Inner.HashExists(ToInner(key), hashField, flags); - } + public bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + Inner.HashExists(ToInner(key), hashField, flags); - public HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.HashGetAll(ToInner(key), flags); - } + public HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.HashGetAll(ToInner(key), flags); - public RedisValue[] HashGet(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) - { - return Inner.HashGet(ToInner(key), hashFields, flags); - } + public RedisValue[] HashGet(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + Inner.HashGet(ToInner(key), hashFields, flags); - public RedisValue HashGet(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - { - return Inner.HashGet(ToInner(key), hashField, flags); - } + public RedisValue HashGet(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + Inner.HashGet(ToInner(key), hashField, flags); - public Lease HashGetLease(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - { - return Inner.HashGetLease(ToInner(key), hashField, flags); - } + public Lease? HashGetLease(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + Inner.HashGetLease(ToInner(key), hashField, flags); - public double HashIncrement(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) - { - return Inner.HashIncrement(ToInner(key), hashField, value, flags); - } + public double HashIncrement(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) => + Inner.HashIncrement(ToInner(key), hashField, value, flags); - public long HashIncrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) - { - return Inner.HashIncrement(ToInner(key), hashField, value, flags); - } + public long HashIncrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) => + Inner.HashIncrement(ToInner(key), hashField, value, flags); - public RedisValue[] HashKeys(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.HashKeys(ToInner(key), flags); - } + public RedisValue[] HashKeys(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.HashKeys(ToInner(key), flags); - public long HashLength(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.HashLength(ToInner(key), flags); - } + public long HashLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.HashLength(ToInner(key), flags); - public bool HashSet(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.HashSet(ToInner(key), hashField, value, when, flags); - } + public bool HashSet(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashSet(ToInner(key), hashField, value, when, flags); - public long HashStringLength(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - { - return Inner.HashStringLength(ToInner(key), hashField, flags); - } + public long HashStringLength(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + Inner.HashStringLength(ToInner(key), hashField, flags); - public void HashSet(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None) - { + public void HashSet(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None) => Inner.HashSet(ToInner(key), hashFields, flags); - } - public RedisValue[] HashValues(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.HashValues(ToInner(key), flags); - } + public RedisValue[] HashValues(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.HashValues(ToInner(key), flags); - public bool HyperLogLogAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) - { - return Inner.HyperLogLogAdd(ToInner(key), values, flags); - } + public bool HyperLogLogAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + Inner.HyperLogLogAdd(ToInner(key), values, flags); - public bool HyperLogLogAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.HyperLogLogAdd(ToInner(key), value, flags); - } + public bool HyperLogLogAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.HyperLogLogAdd(ToInner(key), value, flags); - public long HyperLogLogLength(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.HyperLogLogLength(ToInner(key), flags); - } + public long HyperLogLogLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.HyperLogLogLength(ToInner(key), flags); - public long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.HyperLogLogLength(ToInner(keys), flags); - } + public long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.HyperLogLogLength(ToInner(keys), flags); - public void HyperLogLogMerge(RedisKey destination, RedisKey[] sourceKeys, CommandFlags flags = CommandFlags.None) - { + public void HyperLogLogMerge(RedisKey destination, RedisKey[] sourceKeys, CommandFlags flags = CommandFlags.None) => Inner.HyperLogLogMerge(ToInner(destination), ToInner(sourceKeys), flags); - } - public void HyperLogLogMerge(RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) - { + public void HyperLogLogMerge(RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => Inner.HyperLogLogMerge(ToInner(destination), ToInner(first), ToInner(second), flags); - } - public EndPoint IdentifyEndpoint(RedisKey key = default(RedisKey), CommandFlags flags = CommandFlags.None) - { - return Inner.IdentifyEndpoint(ToInner(key), flags); - } + public EndPoint? IdentifyEndpoint(RedisKey key = default, CommandFlags flags = CommandFlags.None) => + Inner.IdentifyEndpoint(ToInner(key), flags); - public long KeyDelete(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyDelete(ToInner(keys), flags); - } + public long KeyDelete(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.KeyDelete(ToInner(keys), flags); - public bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyDelete(ToInner(key), flags); - } + public bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyDelete(ToInner(key), flags); - public byte[] KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyDump(ToInner(key), flags); - } + public byte[]? KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyDump(ToInner(key), flags); - public bool KeyExists(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyExists(ToInner(key), flags); - } - public long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyExists(ToInner(keys), flags); - } + public bool KeyExists(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyExists(ToInner(key), flags); + public long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.KeyExists(ToInner(keys), flags); - public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyExpire(ToInner(key), expiry, flags); - } + public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) => + Inner.KeyExpire(ToInner(key), expiry, flags); - public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyExpire(ToInner(key), expiry, flags); - } + public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => + Inner.KeyExpire(ToInner(key), expiry, flags); - public TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyIdleTime(ToInner(key), flags); - } + public TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyIdleTime(ToInner(key), flags); - public void KeyMigrate(RedisKey key, EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, MigrateOptions migrateOptions = MigrateOptions.None, CommandFlags flags = CommandFlags.None) - { + public void KeyMigrate(RedisKey key, EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, MigrateOptions migrateOptions = MigrateOptions.None, CommandFlags flags = CommandFlags.None) => Inner.KeyMigrate(ToInner(key), toServer, toDatabase, timeoutMilliseconds, migrateOptions, flags); - } - public bool KeyMove(RedisKey key, int database, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyMove(ToInner(key), database, flags); - } + public bool KeyMove(RedisKey key, int database, CommandFlags flags = CommandFlags.None) => + Inner.KeyMove(ToInner(key), database, flags); - public bool KeyPersist(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyPersist(ToInner(key), flags); - } + public bool KeyPersist(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyPersist(ToInner(key), flags); - public RedisKey KeyRandom(CommandFlags flags = CommandFlags.None) - { + public RedisKey KeyRandom(CommandFlags flags = CommandFlags.None) => throw new NotSupportedException("RANDOMKEY is not supported when a key-prefix is specified"); - } - public bool KeyRename(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyRename(ToInner(key), ToInner(newKey), when, flags); - } + public bool KeyRename(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.KeyRename(ToInner(key), ToInner(newKey), when, flags); - public void KeyRestore(RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) - { + public void KeyRestore(RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) => Inner.KeyRestore(ToInner(key), value, expiry, flags); - } - public TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyTimeToLive(ToInner(key), flags); - } + public TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyTimeToLive(ToInner(key), flags); - public RedisType KeyType(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyType(ToInner(key), flags); - } + public RedisType KeyType(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyType(ToInner(key), flags); - public RedisValue ListGetByIndex(RedisKey key, long index, CommandFlags flags = CommandFlags.None) - { - return Inner.ListGetByIndex(ToInner(key), index, flags); - } + public RedisValue ListGetByIndex(RedisKey key, long index, CommandFlags flags = CommandFlags.None) => + Inner.ListGetByIndex(ToInner(key), index, flags); - public long ListInsertAfter(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.ListInsertAfter(ToInner(key), pivot, value, flags); - } + public long ListInsertAfter(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.ListInsertAfter(ToInner(key), pivot, value, flags); - public long ListInsertBefore(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.ListInsertBefore(ToInner(key), pivot, value, flags); - } + public long ListInsertBefore(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.ListInsertBefore(ToInner(key), pivot, value, flags); - public RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.ListLeftPop(ToInner(key), flags); - } + public RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.ListLeftPop(ToInner(key), flags); - public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) - { - return Inner.ListLeftPop(ToInner(key), count, flags); - } + public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.ListLeftPop(ToInner(key), count, flags); - public long ListLeftPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) - { - return Inner.ListLeftPush(ToInner(key), values, flags); - } + public long ListLeftPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + Inner.ListLeftPush(ToInner(key), values, flags); - public long ListLeftPush(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.ListLeftPush(ToInner(key), values, when, flags); - } + public long ListLeftPush(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.ListLeftPush(ToInner(key), values, when, flags); - public long ListLeftPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.ListLeftPush(ToInner(key), value, when, flags); - } + public long ListLeftPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.ListLeftPush(ToInner(key), value, when, flags); - public long ListLength(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.ListLength(ToInner(key), flags); - } + public long ListLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.ListLength(ToInner(key), flags); - public RedisValue[] ListRange(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRange(ToInner(key), start, stop, flags); - } + public RedisValue[] ListRange(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) => + Inner.ListRange(ToInner(key), start, stop, flags); - public long ListRemove(RedisKey key, RedisValue value, long count = 0, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRemove(ToInner(key), value, count, flags); - } + public long ListRemove(RedisKey key, RedisValue value, long count = 0, CommandFlags flags = CommandFlags.None) => + Inner.ListRemove(ToInner(key), value, count, flags); - public RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRightPop(ToInner(key), flags); - } + public RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPop(ToInner(key), flags); - public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRightPop(ToInner(key), count, flags); - } + public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPop(ToInner(key), count, flags); - public RedisValue ListRightPopLeftPush(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRightPopLeftPush(ToInner(source), ToInner(destination), flags); - } + public RedisValue ListRightPopLeftPush(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPopLeftPush(ToInner(source), ToInner(destination), flags); - public long ListRightPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRightPush(ToInner(key), values, flags); - } + public long ListRightPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPush(ToInner(key), values, flags); - public long ListRightPush(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRightPush(ToInner(key), values, when, flags); - } + public long ListRightPush(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPush(ToInner(key), values, when, flags); - public long ListRightPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRightPush(ToInner(key), value, when, flags); - } + public long ListRightPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPush(ToInner(key), value, when, flags); - public void ListSetByIndex(RedisKey key, long index, RedisValue value, CommandFlags flags = CommandFlags.None) - { + public void ListSetByIndex(RedisKey key, long index, RedisValue value, CommandFlags flags = CommandFlags.None) => Inner.ListSetByIndex(ToInner(key), index, value, flags); - } - public void ListTrim(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) - { + public void ListTrim(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) => Inner.ListTrim(ToInner(key), start, stop, flags); - } - public bool LockExtend(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) - { - return Inner.LockExtend(ToInner(key), value, expiry, flags); - } + public bool LockExtend(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => + Inner.LockExtend(ToInner(key), value, expiry, flags); - public RedisValue LockQuery(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.LockQuery(ToInner(key), flags); - } + public RedisValue LockQuery(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.LockQuery(ToInner(key), flags); - public bool LockRelease(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.LockRelease(ToInner(key), value, flags); - } + public bool LockRelease(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.LockRelease(ToInner(key), value, flags); - public bool LockTake(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) - { - return Inner.LockTake(ToInner(key), value, expiry, flags); - } + public bool LockTake(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => + Inner.LockTake(ToInner(key), value, expiry, flags); - public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) - { - return Inner.Publish(ToInner(channel), message, flags); - } + public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) => + Inner.Publish(ToInner(channel), message, flags); public RedisResult Execute(string command, params object[] args) => Inner.Execute(command, ToInner(args), CommandFlags.None); @@ -407,174 +251,108 @@ public RedisResult Execute(string command, params object[] args) public RedisResult Execute(string command, ICollection args, CommandFlags flags = CommandFlags.None) => Inner.Execute(command, ToInner(args), flags); - public RedisResult ScriptEvaluate(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None) - { + public RedisResult ScriptEvaluate(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - return Inner.ScriptEvaluate(hash, ToInner(keys), values, flags); - } + Inner.ScriptEvaluate(hash, ToInner(keys), values, flags); - public RedisResult ScriptEvaluate(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None) - { + public RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - return Inner.ScriptEvaluate(script, ToInner(keys), values, flags); - } + Inner.ScriptEvaluate(script, ToInner(keys), values, flags); - public RedisResult ScriptEvaluate(LuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None) - { + public RedisResult ScriptEvaluate(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - return script.Evaluate(Inner, parameters, Prefix, flags); - } + script.Evaluate(Inner, parameters, Prefix, flags); - public RedisResult ScriptEvaluate(LoadedLuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None) - { + public RedisResult ScriptEvaluate(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - return script.Evaluate(Inner, parameters, Prefix, flags); - } + script.Evaluate(Inner, parameters, Prefix, flags); - public long SetAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) - { - return Inner.SetAdd(ToInner(key), values, flags); - } + public long SetAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + Inner.SetAdd(ToInner(key), values, flags); - public bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.SetAdd(ToInner(key), value, flags); - } + public bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.SetAdd(ToInner(key), value, flags); - public long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.SetCombineAndStore(operation, ToInner(destination), ToInner(keys), flags); - } + public long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.SetCombineAndStore(operation, ToInner(destination), ToInner(keys), flags); - public long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) - { - return Inner.SetCombineAndStore(operation, ToInner(destination), ToInner(first), ToInner(second), flags); - } + public long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + Inner.SetCombineAndStore(operation, ToInner(destination), ToInner(first), ToInner(second), flags); - public RedisValue[] SetCombine(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.SetCombine(operation, ToInner(keys), flags); - } + public RedisValue[] SetCombine(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.SetCombine(operation, ToInner(keys), flags); - public RedisValue[] SetCombine(SetOperation operation, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) - { - return Inner.SetCombine(operation, ToInner(first), ToInner(second), flags); - } + public RedisValue[] SetCombine(SetOperation operation, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + Inner.SetCombine(operation, ToInner(first), ToInner(second), flags); - public bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.SetContains(ToInner(key), value, flags); - } + public bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.SetContains(ToInner(key), value, flags); - public long SetLength(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.SetLength(ToInner(key), flags); - } + public long SetLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.SetLength(ToInner(key), flags); - public RedisValue[] SetMembers(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.SetMembers(ToInner(key), flags); - } + public RedisValue[] SetMembers(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.SetMembers(ToInner(key), flags); - public bool SetMove(RedisKey source, RedisKey destination, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.SetMove(ToInner(source), ToInner(destination), value, flags); - } + public bool SetMove(RedisKey source, RedisKey destination, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.SetMove(ToInner(source), ToInner(destination), value, flags); - public RedisValue SetPop(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.SetPop(ToInner(key), flags); - } + public RedisValue SetPop(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.SetPop(ToInner(key), flags); - public RedisValue[] SetPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) - { - return Inner.SetPop(ToInner(key), count, flags); - } + public RedisValue[] SetPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.SetPop(ToInner(key), count, flags); - public RedisValue SetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.SetRandomMember(ToInner(key), flags); - } + public RedisValue SetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.SetRandomMember(ToInner(key), flags); - public RedisValue[] SetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) - { - return Inner.SetRandomMembers(ToInner(key), count, flags); - } + public RedisValue[] SetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.SetRandomMembers(ToInner(key), count, flags); - public long SetRemove(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) - { - return Inner.SetRemove(ToInner(key), values, flags); - } + public long SetRemove(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + Inner.SetRemove(ToInner(key), values, flags); - public bool SetRemove(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.SetRemove(ToInner(key), value, flags); - } + public bool SetRemove(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.SetRemove(ToInner(key), value, flags); - public long SortAndStore(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None) - { - return Inner.SortAndStore(ToInner(destination), ToInner(key), skip, take, order, sortType, SortByToInner(by), SortGetToInner(get), flags); - } + public long SortAndStore(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) => + Inner.SortAndStore(ToInner(destination), ToInner(key), skip, take, order, sortType, SortByToInner(by), SortGetToInner(get), flags); - public RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None) - { - return Inner.Sort(ToInner(key), skip, take, order, sortType, SortByToInner(by), SortGetToInner(get), flags); - } + public RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) => + Inner.Sort(ToInner(key), skip, take, order, sortType, SortByToInner(by), SortGetToInner(get), flags); - public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags flags) - { - return Inner.SortedSetAdd(ToInner(key), values, flags); - } + public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags flags) => + Inner.SortedSetAdd(ToInner(key), values, flags); - public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetAdd(ToInner(key), values, when, flags); - } + public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetAdd(ToInner(key), values, when, flags); - public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags) - { - return Inner.SortedSetAdd(ToInner(key), member, score, flags); - } + public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags) => + Inner.SortedSetAdd(ToInner(key), member, score, flags); - public bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetAdd(ToInner(key), member, score, when, flags); - } + public bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetAdd(ToInner(key), member, score, when, flags); - public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[] weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetCombineAndStore(operation, ToInner(destination), ToInner(keys), weights, aggregate, flags); - } + public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetCombineAndStore(operation, ToInner(destination), ToInner(keys), weights, aggregate, flags); - public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetCombineAndStore(operation, ToInner(destination), ToInner(first), ToInner(second), aggregate, flags); - } + public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetCombineAndStore(operation, ToInner(destination), ToInner(first), ToInner(second), aggregate, flags); - public double SortedSetDecrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetDecrement(ToInner(key), member, value, flags); - } + public double SortedSetDecrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetDecrement(ToInner(key), member, value, flags); - public double SortedSetIncrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetIncrement(ToInner(key), member, value, flags); - } + public double SortedSetIncrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetIncrement(ToInner(key), member, value, flags); - public long SortedSetLength(RedisKey key, double min = -1.0 / 0.0, double max = 1.0 / 0.0, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetLength(ToInner(key), min, max, exclude, flags); - } + public long SortedSetLength(RedisKey key, double min = -1.0 / 0.0, double max = 1.0 / 0.0, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetLength(ToInner(key), min, max, exclude, flags); - public long SortedSetLengthByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetLengthByValue(ToInner(key), min, max, exclude, flags); - } + public long SortedSetLengthByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetLengthByValue(ToInner(key), min, max, exclude, flags); - public RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRangeByRank(ToInner(key), start, stop, order, flags); - } + public RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRangeByRank(ToInner(key), start, stop, order, flags); public long SortedSetRangeAndStore( RedisKey destinationKey, @@ -586,350 +364,212 @@ public long SortedSetRangeAndStore( Order order = Order.Ascending, long skip = 0, long? take = null, - CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRangeAndStore(ToInner(sourceKey), ToInner(destinationKey), start, stop, sortedSetOrder, exclude, order, skip, take, flags); - } + CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRangeAndStore(ToInner(sourceKey), ToInner(destinationKey), start, stop, sortedSetOrder, exclude, order, skip, take, flags); - public SortedSetEntry[] SortedSetRangeByRankWithScores(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRangeByRankWithScores(ToInner(key), start, stop, order, flags); - } + public SortedSetEntry[] SortedSetRangeByRankWithScores(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRangeByRankWithScores(ToInner(key), start, stop, order, flags); - public RedisValue[] SortedSetRangeByScore(RedisKey key, double start = -1.0 / 0.0, double stop = 1.0 / 0.0, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRangeByScore(ToInner(key), start, stop, exclude, order, skip, take, flags); - } + public RedisValue[] SortedSetRangeByScore(RedisKey key, double start = -1.0 / 0.0, double stop = 1.0 / 0.0, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRangeByScore(ToInner(key), start, stop, exclude, order, skip, take, flags); - public SortedSetEntry[] SortedSetRangeByScoreWithScores(RedisKey key, double start = -1.0 / 0.0, double stop = 1.0 / 0.0, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRangeByScoreWithScores(ToInner(key), start, stop, exclude, order, skip, take, flags); - } + public SortedSetEntry[] SortedSetRangeByScoreWithScores(RedisKey key, double start = -1.0 / 0.0, double stop = 1.0 / 0.0, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRangeByScoreWithScores(ToInner(key), start, stop, exclude, order, skip, take, flags); - public RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude, long skip, long take, CommandFlags flags) - { - return Inner.SortedSetRangeByValue(ToInner(key), min, max, exclude, Order.Ascending, skip, take, flags); - } + public RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude, long skip, long take, CommandFlags flags) => + Inner.SortedSetRangeByValue(ToInner(key), min, max, exclude, Order.Ascending, skip, take, flags); - public RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min = default(RedisValue), RedisValue max = default(RedisValue), Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRangeByValue(ToInner(key), min, max, exclude, order, skip, take, flags); - } + public RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min = default, RedisValue max = default, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRangeByValue(ToInner(key), min, max, exclude, order, skip, take, flags); - public long? SortedSetRank(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRank(ToInner(key), member, order, flags); - } + public long? SortedSetRank(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRank(ToInner(key), member, order, flags); - public long SortedSetRemove(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRemove(ToInner(key), members, flags); - } + public long SortedSetRemove(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRemove(ToInner(key), members, flags); - public bool SortedSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRemove(ToInner(key), member, flags); - } + public bool SortedSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRemove(ToInner(key), member, flags); - public long SortedSetRemoveRangeByRank(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRemoveRangeByRank(ToInner(key), start, stop, flags); - } + public long SortedSetRemoveRangeByRank(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRemoveRangeByRank(ToInner(key), start, stop, flags); - public long SortedSetRemoveRangeByScore(RedisKey key, double start, double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRemoveRangeByScore(ToInner(key), start, stop, exclude, flags); - } + public long SortedSetRemoveRangeByScore(RedisKey key, double start, double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRemoveRangeByScore(ToInner(key), start, stop, exclude, flags); - public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRemoveRangeByValue(ToInner(key), min, max, exclude, flags); - } + public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRemoveRangeByValue(ToInner(key), min, max, exclude, flags); - public double? SortedSetScore(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetScore(ToInner(key), member, flags); - } + public double? SortedSetScore(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetScore(ToInner(key), member, flags); - public SortedSetEntry? SortedSetPop(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetPop(ToInner(key), order, flags); - } + public SortedSetEntry? SortedSetPop(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetPop(ToInner(key), order, flags); - public SortedSetEntry[] SortedSetPop(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetPop(ToInner(key), count, order, flags); - } + public SortedSetEntry[] SortedSetPop(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetPop(ToInner(key), count, order, flags); - public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamAcknowledge(ToInner(key), groupName, messageId, flags); - } + public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) => + Inner.StreamAcknowledge(ToInner(key), groupName, messageId, flags); - public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamAcknowledge(ToInner(key), groupName, messageIds, flags); - } + public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + Inner.StreamAcknowledge(ToInner(key), groupName, messageIds, flags); - public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamAdd(ToInner(key), streamField, streamValue, messageId, maxLength, useApproximateMaxLength, flags); - } + public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + Inner.StreamAdd(ToInner(key), streamField, streamValue, messageId, maxLength, useApproximateMaxLength, flags); - public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamAdd(ToInner(key), streamPairs, messageId, maxLength, useApproximateMaxLength, flags); - } + public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + Inner.StreamAdd(ToInner(key), streamPairs, messageId, maxLength, useApproximateMaxLength, flags); - public StreamEntry[] StreamClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamClaim(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, messageIds, flags); - } + public StreamEntry[] StreamClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + Inner.StreamClaim(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, messageIds, flags); - public RedisValue[] StreamClaimIdsOnly(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamClaimIdsOnly(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, messageIds, flags); - } + public RedisValue[] StreamClaimIdsOnly(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + Inner.StreamClaimIdsOnly(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, messageIds, flags); - public bool StreamConsumerGroupSetPosition(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamConsumerGroupSetPosition(ToInner(key), groupName, position, flags); - } + public bool StreamConsumerGroupSetPosition(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None) => + Inner.StreamConsumerGroupSetPosition(ToInner(key), groupName, position, flags); - public bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisValue? position, CommandFlags flags) - { - return Inner.StreamCreateConsumerGroup(ToInner(key), groupName, position, flags); - } + public bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisValue? position, CommandFlags flags) => + Inner.StreamCreateConsumerGroup(ToInner(key), groupName, position, flags); - public bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamCreateConsumerGroup(ToInner(key), groupName, position, createStream, flags); - } + public bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None) => + Inner.StreamCreateConsumerGroup(ToInner(key), groupName, position, createStream, flags); - public StreamInfo StreamInfo(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamInfo(ToInner(key), flags); - } + public StreamInfo StreamInfo(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StreamInfo(ToInner(key), flags); - public StreamGroupInfo[] StreamGroupInfo(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamGroupInfo(ToInner(key), flags); - } + public StreamGroupInfo[] StreamGroupInfo(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StreamGroupInfo(ToInner(key), flags); - public StreamConsumerInfo[] StreamConsumerInfo(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamConsumerInfo(ToInner(key), groupName, flags); - } + public StreamConsumerInfo[] StreamConsumerInfo(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => + Inner.StreamConsumerInfo(ToInner(key), groupName, flags); - public long StreamLength(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamLength(ToInner(key), flags); - } + public long StreamLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StreamLength(ToInner(key), flags); - public long StreamDelete(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamDelete(ToInner(key), messageIds, flags); - } + public long StreamDelete(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + Inner.StreamDelete(ToInner(key), messageIds, flags); - public long StreamDeleteConsumer(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamDeleteConsumer(ToInner(key), groupName, consumerName, flags); - } + public long StreamDeleteConsumer(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None) => + Inner.StreamDeleteConsumer(ToInner(key), groupName, consumerName, flags); - public bool StreamDeleteConsumerGroup(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamDeleteConsumerGroup(ToInner(key), groupName, flags); - } + public bool StreamDeleteConsumerGroup(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => + Inner.StreamDeleteConsumerGroup(ToInner(key), groupName, flags); - public StreamPendingInfo StreamPending(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamPending(ToInner(key), groupName, flags); - } + public StreamPendingInfo StreamPending(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => + Inner.StreamPending(ToInner(key), groupName, flags); - public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamPendingMessages(ToInner(key), groupName, count, consumerName, minId, maxId, flags); - } + public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamPendingMessages(ToInner(key), groupName, count, consumerName, minId, maxId, flags); - public StreamEntry[] StreamRange(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamRange(ToInner(key), minId, maxId, count, messageOrder, flags); - } + public StreamEntry[] StreamRange(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.StreamRange(ToInner(key), minId, maxId, count, messageOrder, flags); - public StreamEntry[] StreamRead(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamRead(ToInner(key), position, count, flags); - } + public StreamEntry[] StreamRead(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamRead(ToInner(key), position, count, flags); - public RedisStream[] StreamRead(StreamPosition[] streamPositions, int? countPerStream = null, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamRead(streamPositions, countPerStream, flags); - } + public RedisStream[] StreamRead(StreamPosition[] streamPositions, int? countPerStream = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamRead(streamPositions, countPerStream, flags); - public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, CommandFlags flags) - { - return Inner.StreamReadGroup(ToInner(key), groupName, consumerName, position, count, flags); - } + public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, CommandFlags flags) => + Inner.StreamReadGroup(ToInner(key), groupName, consumerName, position, count, flags); - public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamReadGroup(ToInner(key), groupName, consumerName, position, count, noAck, flags); - } + public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => + Inner.StreamReadGroup(ToInner(key), groupName, consumerName, position, count, noAck, flags); - public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags) - { - return Inner.StreamReadGroup(streamPositions, groupName, consumerName, countPerStream, flags); - } + public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags) => + Inner.StreamReadGroup(streamPositions, groupName, consumerName, countPerStream, flags); - public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamReadGroup(streamPositions, groupName, consumerName, countPerStream, noAck, flags); - } + public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => + Inner.StreamReadGroup(streamPositions, groupName, consumerName, countPerStream, noAck, flags); - public long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamTrim(ToInner(key), maxLength, useApproximateMaxLength, flags); - } + public long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + Inner.StreamTrim(ToInner(key), maxLength, useApproximateMaxLength, flags); - public long StringAppend(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.StringAppend(ToInner(key), value, flags); - } + public long StringAppend(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.StringAppend(ToInner(key), value, flags); - public long StringBitCount(RedisKey key, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) - { - return Inner.StringBitCount(ToInner(key), start, end, flags); - } + public long StringBitCount(RedisKey key, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) => + Inner.StringBitCount(ToInner(key), start, end, flags); - public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.StringBitOperation(operation, ToInner(destination), ToInner(keys), flags); - } + public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.StringBitOperation(operation, ToInner(destination), ToInner(keys), flags); - public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default(RedisKey), CommandFlags flags = CommandFlags.None) - { - return Inner.StringBitOperation(operation, ToInner(destination), ToInner(first), ToInnerOrDefault(second), flags); - } + public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, CommandFlags flags = CommandFlags.None) => + Inner.StringBitOperation(operation, ToInner(destination), ToInner(first), ToInnerOrDefault(second), flags); - public long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) - { - return Inner.StringBitPosition(ToInner(key), bit, start, end, flags); - } + public long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) => + Inner.StringBitPosition(ToInner(key), bit, start, end, flags); - public double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) - { - return Inner.StringDecrement(ToInner(key), value, flags); - } + public double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => + Inner.StringDecrement(ToInner(key), value, flags); - public long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) - { - return Inner.StringDecrement(ToInner(key), value, flags); - } + public long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => + Inner.StringDecrement(ToInner(key), value, flags); - public RedisValue[] StringGet(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGet(ToInner(keys), flags); - } + public RedisValue[] StringGet(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.StringGet(ToInner(keys), flags); - public RedisValue StringGet(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGet(ToInner(key), flags); - } + public RedisValue StringGet(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StringGet(ToInner(key), flags); - public RedisValue StringGetSetExpiry(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetSetExpiry(ToInner(key), expiry, flags); - } + public RedisValue StringGetSetExpiry(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => + Inner.StringGetSetExpiry(ToInner(key), expiry, flags); - public RedisValue StringGetSetExpiry(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetSetExpiry(ToInner(key), expiry, flags); - } + public RedisValue StringGetSetExpiry(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) => + Inner.StringGetSetExpiry(ToInner(key), expiry, flags); - public LeaseStringGetLease(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetLease(ToInner(key), flags); - } + public Lease? StringGetLease(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StringGetLease(ToInner(key), flags); - public bool StringGetBit(RedisKey key, long offset, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetBit(ToInner(key), offset, flags); - } + public bool StringGetBit(RedisKey key, long offset, CommandFlags flags = CommandFlags.None) => + Inner.StringGetBit(ToInner(key), offset, flags); - public RedisValue StringGetRange(RedisKey key, long start, long end, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetRange(ToInner(key), start, end, flags); - } + public RedisValue StringGetRange(RedisKey key, long start, long end, CommandFlags flags = CommandFlags.None) => + Inner.StringGetRange(ToInner(key), start, end, flags); - public RedisValue StringGetSet(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetSet(ToInner(key), value, flags); - } + public RedisValue StringGetSet(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.StringGetSet(ToInner(key), value, flags); - public RedisValue StringGetDelete(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetDelete(ToInner(key), flags); - } + public RedisValue StringGetDelete(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StringGetDelete(ToInner(key), flags); - public RedisValueWithExpiry StringGetWithExpiry(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetWithExpiry(ToInner(key), flags); - } + public RedisValueWithExpiry StringGetWithExpiry(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StringGetWithExpiry(ToInner(key), flags); - public double StringIncrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) - { - return Inner.StringIncrement(ToInner(key), value, flags); - } + public double StringIncrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => + Inner.StringIncrement(ToInner(key), value, flags); - public long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) - { - return Inner.StringIncrement(ToInner(key), value, flags); - } + public long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => + Inner.StringIncrement(ToInner(key), value, flags); - public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StringLength(ToInner(key), flags); - } + public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StringLength(ToInner(key), flags); - public bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.StringSet(ToInner(values), when, flags); - } + public bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.StringSet(ToInner(values), when, flags); - public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) - { - return Inner.StringSet(ToInner(key), value, expiry, when, flags); - } + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + Inner.StringSet(ToInner(key), value, expiry, when, flags); - public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.StringSet(ToInner(key), value, expiry, keepTtl, when, flags); - } + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.StringSet(ToInner(key), value, expiry, keepTtl, when, flags); - public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) - { - return Inner.StringSetAndGet(ToInner(key), value, expiry, when, flags); - } + public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + Inner.StringSetAndGet(ToInner(key), value, expiry, when, flags); - public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.StringSetAndGet(ToInner(key), value, expiry, keepTtl, when, flags); - } + public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.StringSetAndGet(ToInner(key), value, expiry, keepTtl, when, flags); - public bool StringSetBit(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) - { - return Inner.StringSetBit(ToInner(key), offset, bit, flags); - } + public bool StringSetBit(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) => + Inner.StringSetBit(ToInner(key), offset, bit, flags); - public RedisValue StringSetRange(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.StringSetRange(ToInner(key), offset, value, flags); - } + public RedisValue StringSetRange(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.StringSetRange(ToInner(key), offset, value, flags); - public TimeSpan Ping(CommandFlags flags = CommandFlags.None) - { - return Inner.Ping(flags); - } + public TimeSpan Ping(CommandFlags flags = CommandFlags.None) => + Inner.Ping(flags); IEnumerable IDatabase.HashScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => Inner.HashScan(ToInner(key), pattern, pageSize, flags); @@ -949,14 +589,10 @@ IEnumerable IDatabase.SortedSetScan(RedisKey key, RedisValue pat IEnumerable IDatabase.SortedSetScan(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => Inner.SortedSetScan(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); - public bool KeyTouch(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyTouch(ToInner(key), flags); - } + public bool KeyTouch(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyTouch(ToInner(key), flags); - public long KeyTouch(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyTouch(ToInner(keys), flags); - } + public long KeyTouch(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.KeyTouch(ToInner(keys), flags); } } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/TransactionWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/TransactionWrapper.cs index c3e8665bf..8e1ab1b7b 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/TransactionWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/TransactionWrapper.cs @@ -6,7 +6,7 @@ internal sealed class TransactionWrapper : WrapperBase, ITransacti { public TransactionWrapper(ITransaction inner, byte[] prefix) : base(inner, prefix) { } - public ConditionResult AddCondition(Condition condition) => Inner.AddCondition(condition?.MapKeys(GetMapFunction())); + public ConditionResult AddCondition(Condition condition) => Inner.AddCondition(condition.MapKeys(GetMapFunction())); public bool Execute(CommandFlags flags = CommandFlags.None) => Inner.Execute(flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index e1a5bc572..97d0abd6f 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Threading.Tasks; @@ -20,549 +21,348 @@ internal WrapperBase(TInner inner, byte[] keyPrefix) internal byte[] Prefix { get; } - public Task DebugObjectAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.DebugObjectAsync(ToInner(key), flags); - } + public Task DebugObjectAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.DebugObjectAsync(ToInner(key), flags); - public Task GeoAddAsync(RedisKey key, double longitude, double latitude, RedisValue member, CommandFlags flags = CommandFlags.None) - => Inner.GeoAddAsync(ToInner(key), longitude, latitude, member, flags); + public Task GeoAddAsync(RedisKey key, double longitude, double latitude, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.GeoAddAsync(ToInner(key), longitude, latitude, member, flags); - public Task GeoAddAsync(RedisKey key, GeoEntry value, CommandFlags flags = CommandFlags.None) - => Inner.GeoAddAsync(ToInner(key), value, flags); + public Task GeoAddAsync(RedisKey key, GeoEntry value, CommandFlags flags = CommandFlags.None) => + Inner.GeoAddAsync(ToInner(key), value, flags); - public Task GeoAddAsync(RedisKey key, GeoEntry[] values, CommandFlags flags = CommandFlags.None) - => Inner.GeoAddAsync(ToInner(key), values, flags); + public Task GeoAddAsync(RedisKey key, GeoEntry[] values, CommandFlags flags = CommandFlags.None) => + Inner.GeoAddAsync(ToInner(key), values, flags); - public Task GeoRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - => Inner.GeoRemoveAsync(ToInner(key), member, flags); + public Task GeoRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.GeoRemoveAsync(ToInner(key), member, flags); - public Task GeoDistanceAsync(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None) - => Inner.GeoDistanceAsync(ToInner(key), member1, member2, unit, flags); + public Task GeoDistanceAsync(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None) => + Inner.GeoDistanceAsync(ToInner(key), member1, member2, unit, flags); - public Task GeoHashAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) - => Inner.GeoHashAsync(ToInner(key), members, flags); + public Task GeoHashAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + Inner.GeoHashAsync(ToInner(key), members, flags); - public Task GeoHashAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - => Inner.GeoHashAsync(ToInner(key), member, flags); + public Task GeoHashAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.GeoHashAsync(ToInner(key), member, flags); - public Task GeoPositionAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) - => Inner.GeoPositionAsync(ToInner(key), members, flags); + public Task GeoPositionAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + Inner.GeoPositionAsync(ToInner(key), members, flags); - public Task GeoPositionAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - => Inner.GeoPositionAsync(ToInner(key), member, flags); + public Task GeoPositionAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.GeoPositionAsync(ToInner(key), member, flags); - public Task GeoRadiusAsync(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) - => Inner.GeoRadiusAsync(ToInner(key), member, radius, unit, count, order, options, flags); + public Task GeoRadiusAsync(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + Inner.GeoRadiusAsync(ToInner(key), member, radius, unit, count, order, options, flags); - public Task GeoRadiusAsync(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) - => Inner.GeoRadiusAsync(ToInner(key), longitude, latitude, radius, unit, count, order, options, flags); + public Task GeoRadiusAsync(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + Inner.GeoRadiusAsync(ToInner(key), longitude, latitude, radius, unit, count, order, options, flags); - public Task HashDecrementAsync(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) - { - return Inner.HashDecrementAsync(ToInner(key), hashField, value, flags); - } + public Task HashDecrementAsync(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) => + Inner.HashDecrementAsync(ToInner(key), hashField, value, flags); - public Task HashDecrementAsync(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) - { - return Inner.HashDecrementAsync(ToInner(key), hashField, value, flags); - } + public Task HashDecrementAsync(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) => + Inner.HashDecrementAsync(ToInner(key), hashField, value, flags); - public Task HashDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) - { - return Inner.HashDeleteAsync(ToInner(key), hashFields, flags); - } + public Task HashDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + Inner.HashDeleteAsync(ToInner(key), hashFields, flags); - public Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - { - return Inner.HashDeleteAsync(ToInner(key), hashField, flags); - } + public Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + Inner.HashDeleteAsync(ToInner(key), hashField, flags); - public Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - { - return Inner.HashExistsAsync(ToInner(key), hashField, flags); - } + public Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + Inner.HashExistsAsync(ToInner(key), hashField, flags); - public Task HashGetAllAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.HashGetAllAsync(ToInner(key), flags); - } + public Task HashGetAllAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.HashGetAllAsync(ToInner(key), flags); - public Task HashGetAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) - { - return Inner.HashGetAsync(ToInner(key), hashFields, flags); - } + public Task HashGetAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + Inner.HashGetAsync(ToInner(key), hashFields, flags); - public Task HashGetAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - { - return Inner.HashGetAsync(ToInner(key), hashField, flags); - } + public Task HashGetAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + Inner.HashGetAsync(ToInner(key), hashField, flags); - public Task> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - { - return Inner.HashGetLeaseAsync(ToInner(key), hashField, flags); - } + public Task?> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + Inner.HashGetLeaseAsync(ToInner(key), hashField, flags); - public Task HashIncrementAsync(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) - { - return Inner.HashIncrementAsync(ToInner(key), hashField, value, flags); - } + public Task HashIncrementAsync(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) => + Inner.HashIncrementAsync(ToInner(key), hashField, value, flags); - public Task HashIncrementAsync(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) - { - return Inner.HashIncrementAsync(ToInner(key), hashField, value, flags); - } + public Task HashIncrementAsync(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) => + Inner.HashIncrementAsync(ToInner(key), hashField, value, flags); - public Task HashKeysAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.HashKeysAsync(ToInner(key), flags); - } + public Task HashKeysAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.HashKeysAsync(ToInner(key), flags); - public Task HashLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.HashLengthAsync(ToInner(key), flags); - } + public Task HashLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.HashLengthAsync(ToInner(key), flags); - public IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) - => Inner.HashScanAsync(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); + public IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => + Inner.HashScanAsync(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); - public Task HashSetAsync(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.HashSetAsync(ToInner(key), hashField, value, when, flags); - } + public Task HashSetAsync(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashSetAsync(ToInner(key), hashField, value, when, flags); - public Task HashStringLengthAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) - { - return Inner.HashStringLengthAsync(ToInner(key), hashField, flags); - } + public Task HashStringLengthAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + Inner.HashStringLengthAsync(ToInner(key), hashField, flags); - public Task HashSetAsync(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None) - { - return Inner.HashSetAsync(ToInner(key), hashFields, flags); - } + public Task HashSetAsync(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None) => + Inner.HashSetAsync(ToInner(key), hashFields, flags); - public Task HashValuesAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.HashValuesAsync(ToInner(key), flags); - } + public Task HashValuesAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.HashValuesAsync(ToInner(key), flags); - public Task HyperLogLogAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) - { - return Inner.HyperLogLogAddAsync(ToInner(key), values, flags); - } + public Task HyperLogLogAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + Inner.HyperLogLogAddAsync(ToInner(key), values, flags); - public Task HyperLogLogAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.HyperLogLogAddAsync(ToInner(key), value, flags); - } + public Task HyperLogLogAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.HyperLogLogAddAsync(ToInner(key), value, flags); - public Task HyperLogLogLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.HyperLogLogLengthAsync(ToInner(key), flags); - } + public Task HyperLogLogLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.HyperLogLogLengthAsync(ToInner(key), flags); - public Task HyperLogLogLengthAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.HyperLogLogLengthAsync(ToInner(keys), flags); - } + public Task HyperLogLogLengthAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.HyperLogLogLengthAsync(ToInner(keys), flags); - public Task HyperLogLogMergeAsync(RedisKey destination, RedisKey[] sourceKeys, CommandFlags flags = CommandFlags.None) - { - return Inner.HyperLogLogMergeAsync(ToInner(destination), ToInner(sourceKeys), flags); - } + public Task HyperLogLogMergeAsync(RedisKey destination, RedisKey[] sourceKeys, CommandFlags flags = CommandFlags.None) => + Inner.HyperLogLogMergeAsync(ToInner(destination), ToInner(sourceKeys), flags); - public Task HyperLogLogMergeAsync(RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) - { - return Inner.HyperLogLogMergeAsync(ToInner(destination), ToInner(first), ToInner(second), flags); - } + public Task HyperLogLogMergeAsync(RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + Inner.HyperLogLogMergeAsync(ToInner(destination), ToInner(first), ToInner(second), flags); - public Task IdentifyEndpointAsync(RedisKey key = default(RedisKey), CommandFlags flags = CommandFlags.None) - { - return Inner.IdentifyEndpointAsync(ToInner(key), flags); - } + public Task IdentifyEndpointAsync(RedisKey key = default(RedisKey), CommandFlags flags = CommandFlags.None) => + Inner.IdentifyEndpointAsync(ToInner(key), flags); - public bool IsConnected(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.IsConnected(ToInner(key), flags); - } + public bool IsConnected(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.IsConnected(ToInner(key), flags); - public Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyDeleteAsync(ToInner(keys), flags); - } + public Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.KeyDeleteAsync(ToInner(keys), flags); - public Task KeyDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyDeleteAsync(ToInner(key), flags); - } + public Task KeyDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyDeleteAsync(ToInner(key), flags); - public Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyDumpAsync(ToInner(key), flags); - } + public Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyDumpAsync(ToInner(key), flags); - public Task KeyExistsAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyExistsAsync(ToInner(key), flags); - } + public Task KeyExistsAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyExistsAsync(ToInner(key), flags); - public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyExistsAsync(ToInner(keys), flags); - } + public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.KeyExistsAsync(ToInner(keys), flags); - public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyExpireAsync(ToInner(key), expiry, flags); - } + public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) => + Inner.KeyExpireAsync(ToInner(key), expiry, flags); - public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyExpireAsync(ToInner(key), expiry, flags); - } + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => + Inner.KeyExpireAsync(ToInner(key), expiry, flags); - public Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyIdleTimeAsync(ToInner(key), flags); - } + public Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyIdleTimeAsync(ToInner(key), flags); - public Task KeyMigrateAsync(RedisKey key, EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, MigrateOptions migrateOptions = MigrateOptions.None, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyMigrateAsync(ToInner(key), toServer, toDatabase, timeoutMilliseconds, migrateOptions, flags); - } + public Task KeyMigrateAsync(RedisKey key, EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, MigrateOptions migrateOptions = MigrateOptions.None, CommandFlags flags = CommandFlags.None) => + Inner.KeyMigrateAsync(ToInner(key), toServer, toDatabase, timeoutMilliseconds, migrateOptions, flags); - public Task KeyMoveAsync(RedisKey key, int database, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyMoveAsync(ToInner(key), database, flags); - } + public Task KeyMoveAsync(RedisKey key, int database, CommandFlags flags = CommandFlags.None) => + Inner.KeyMoveAsync(ToInner(key), database, flags); - public Task KeyPersistAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyPersistAsync(ToInner(key), flags); - } + public Task KeyPersistAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyPersistAsync(ToInner(key), flags); - public Task KeyRandomAsync(CommandFlags flags = CommandFlags.None) - { + public Task KeyRandomAsync(CommandFlags flags = CommandFlags.None) => throw new NotSupportedException("RANDOMKEY is not supported when a key-prefix is specified"); - } - public Task KeyRenameAsync(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyRenameAsync(ToInner(key), ToInner(newKey), when, flags); - } + public Task KeyRenameAsync(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.KeyRenameAsync(ToInner(key), ToInner(newKey), when, flags); - public Task KeyRestoreAsync(RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyRestoreAsync(ToInner(key), value, expiry, flags); - } + public Task KeyRestoreAsync(RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None) => + Inner.KeyRestoreAsync(ToInner(key), value, expiry, flags); - public Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyTimeToLiveAsync(ToInner(key), flags); - } + public Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyTimeToLiveAsync(ToInner(key), flags); - public Task KeyTypeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyTypeAsync(ToInner(key), flags); - } + public Task KeyTypeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyTypeAsync(ToInner(key), flags); - public Task ListGetByIndexAsync(RedisKey key, long index, CommandFlags flags = CommandFlags.None) - { - return Inner.ListGetByIndexAsync(ToInner(key), index, flags); - } + public Task ListGetByIndexAsync(RedisKey key, long index, CommandFlags flags = CommandFlags.None) => + Inner.ListGetByIndexAsync(ToInner(key), index, flags); - public Task ListInsertAfterAsync(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.ListInsertAfterAsync(ToInner(key), pivot, value, flags); - } + public Task ListInsertAfterAsync(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.ListInsertAfterAsync(ToInner(key), pivot, value, flags); - public Task ListInsertBeforeAsync(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.ListInsertBeforeAsync(ToInner(key), pivot, value, flags); - } + public Task ListInsertBeforeAsync(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.ListInsertBeforeAsync(ToInner(key), pivot, value, flags); - public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.ListLeftPopAsync(ToInner(key), flags); - } + public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.ListLeftPopAsync(ToInner(key), flags); - public Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) - { - return Inner.ListLeftPopAsync(ToInner(key), count, flags); - } + public Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.ListLeftPopAsync(ToInner(key), count, flags); - public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) - { - return Inner.ListLeftPushAsync(ToInner(key), values, flags); - } + public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + Inner.ListLeftPushAsync(ToInner(key), values, flags); - public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.ListLeftPushAsync(ToInner(key), values, when, flags); - } + public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.ListLeftPushAsync(ToInner(key), values, when, flags); - public Task ListLeftPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.ListLeftPushAsync(ToInner(key), value, when, flags); - } + public Task ListLeftPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.ListLeftPushAsync(ToInner(key), value, when, flags); - public Task ListLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.ListLengthAsync(ToInner(key), flags); - } + public Task ListLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.ListLengthAsync(ToInner(key), flags); - public Task ListRangeAsync(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRangeAsync(ToInner(key), start, stop, flags); - } + public Task ListRangeAsync(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) => + Inner.ListRangeAsync(ToInner(key), start, stop, flags); - public Task ListRemoveAsync(RedisKey key, RedisValue value, long count = 0, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRemoveAsync(ToInner(key), value, count, flags); - } + public Task ListRemoveAsync(RedisKey key, RedisValue value, long count = 0, CommandFlags flags = CommandFlags.None) => + Inner.ListRemoveAsync(ToInner(key), value, count, flags); - public Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRightPopAsync(ToInner(key), flags); - } + public Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPopAsync(ToInner(key), flags); - public Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRightPopAsync(ToInner(key), count, flags); - } + public Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPopAsync(ToInner(key), count, flags); - public Task ListRightPopLeftPushAsync(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRightPopLeftPushAsync(ToInner(source), ToInner(destination), flags); - } + public Task ListRightPopLeftPushAsync(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPopLeftPushAsync(ToInner(source), ToInner(destination), flags); - public Task ListRightPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRightPushAsync(ToInner(key), values, flags); - } + public Task ListRightPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPushAsync(ToInner(key), values, flags); - public Task ListRightPushAsync(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRightPushAsync(ToInner(key), values, when, flags); - } + public Task ListRightPushAsync(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPushAsync(ToInner(key), values, when, flags); - public Task ListRightPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.ListRightPushAsync(ToInner(key), value, when, flags); - } + public Task ListRightPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPushAsync(ToInner(key), value, when, flags); - public Task ListSetByIndexAsync(RedisKey key, long index, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.ListSetByIndexAsync(ToInner(key), index, value, flags); - } + public Task ListSetByIndexAsync(RedisKey key, long index, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.ListSetByIndexAsync(ToInner(key), index, value, flags); - public Task ListTrimAsync(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) - { - return Inner.ListTrimAsync(ToInner(key), start, stop, flags); - } + public Task ListTrimAsync(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) => + Inner.ListTrimAsync(ToInner(key), start, stop, flags); - public Task LockExtendAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) - { - return Inner.LockExtendAsync(ToInner(key), value, expiry, flags); - } + public Task LockExtendAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => + Inner.LockExtendAsync(ToInner(key), value, expiry, flags); - public Task LockQueryAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.LockQueryAsync(ToInner(key), flags); - } + public Task LockQueryAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.LockQueryAsync(ToInner(key), flags); - public Task LockReleaseAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.LockReleaseAsync(ToInner(key), value, flags); - } + public Task LockReleaseAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.LockReleaseAsync(ToInner(key), value, flags); - public Task LockTakeAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) - { - return Inner.LockTakeAsync(ToInner(key), value, expiry, flags); - } + public Task LockTakeAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => + Inner.LockTakeAsync(ToInner(key), value, expiry, flags); - public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) - { - return Inner.PublishAsync(ToInner(channel), message, flags); - } + public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) => + Inner.PublishAsync(ToInner(channel), message, flags); - public Task ExecuteAsync(string command, params object[] args) - => Inner.ExecuteAsync(command, ToInner(args), CommandFlags.None); + public Task ExecuteAsync(string command, params object[] args) => + Inner.ExecuteAsync(command, ToInner(args), CommandFlags.None); - public Task ExecuteAsync(string command, ICollection args, CommandFlags flags = CommandFlags.None) - => Inner.ExecuteAsync(command, ToInner(args), flags); + public Task ExecuteAsync(string command, ICollection? args, CommandFlags flags = CommandFlags.None) => + Inner.ExecuteAsync(command, ToInner(args), flags); - public Task ScriptEvaluateAsync(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None) - { + public Task ScriptEvaluateAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - return Inner.ScriptEvaluateAsync(hash, ToInner(keys), values, flags); - } + Inner.ScriptEvaluateAsync(hash, ToInner(keys), values, flags); - public Task ScriptEvaluateAsync(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None) - { + public Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - return Inner.ScriptEvaluateAsync(script, ToInner(keys), values, flags); - } + Inner.ScriptEvaluateAsync(script, ToInner(keys), values, flags); - public Task ScriptEvaluateAsync(LuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None) - { + public Task ScriptEvaluateAsync(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - return script.EvaluateAsync(Inner, parameters, Prefix, flags); - } + script.EvaluateAsync(Inner, parameters, Prefix, flags); - public Task ScriptEvaluateAsync(LoadedLuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None) - { + public Task ScriptEvaluateAsync(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - return script.EvaluateAsync(Inner, parameters, Prefix, flags); - } + script.EvaluateAsync(Inner, parameters, Prefix, flags); - public Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) - { - return Inner.SetAddAsync(ToInner(key), values, flags); - } + public Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + Inner.SetAddAsync(ToInner(key), values, flags); - public Task SetAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.SetAddAsync(ToInner(key), value, flags); - } + public Task SetAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.SetAddAsync(ToInner(key), value, flags); - public Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.SetCombineAndStoreAsync(operation, ToInner(destination), ToInner(keys), flags); - } + public Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.SetCombineAndStoreAsync(operation, ToInner(destination), ToInner(keys), flags); - public Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) - { - return Inner.SetCombineAndStoreAsync(operation, ToInner(destination), ToInner(first), ToInner(second), flags); - } + public Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + Inner.SetCombineAndStoreAsync(operation, ToInner(destination), ToInner(first), ToInner(second), flags); - public Task SetCombineAsync(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.SetCombineAsync(operation, ToInner(keys), flags); - } + public Task SetCombineAsync(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.SetCombineAsync(operation, ToInner(keys), flags); - public Task SetCombineAsync(SetOperation operation, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) - { - return Inner.SetCombineAsync(operation, ToInner(first), ToInner(second), flags); - } + public Task SetCombineAsync(SetOperation operation, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + Inner.SetCombineAsync(operation, ToInner(first), ToInner(second), flags); - public Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.SetContainsAsync(ToInner(key), value, flags); - } + public Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.SetContainsAsync(ToInner(key), value, flags); - public Task SetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.SetLengthAsync(ToInner(key), flags); - } + public Task SetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.SetLengthAsync(ToInner(key), flags); - public Task SetMembersAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.SetMembersAsync(ToInner(key), flags); - } + public Task SetMembersAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.SetMembersAsync(ToInner(key), flags); - public Task SetMoveAsync(RedisKey source, RedisKey destination, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.SetMoveAsync(ToInner(source), ToInner(destination), value, flags); - } + public Task SetMoveAsync(RedisKey source, RedisKey destination, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.SetMoveAsync(ToInner(source), ToInner(destination), value, flags); - public Task SetPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.SetPopAsync(ToInner(key), flags); - } + public Task SetPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.SetPopAsync(ToInner(key), flags); - public Task SetPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) - { - return Inner.SetPopAsync(ToInner(key), count, flags); - } + public Task SetPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.SetPopAsync(ToInner(key), count, flags); - public Task SetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.SetRandomMemberAsync(ToInner(key), flags); - } + public Task SetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.SetRandomMemberAsync(ToInner(key), flags); - public Task SetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) - { - return Inner.SetRandomMembersAsync(ToInner(key), count, flags); - } + public Task SetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.SetRandomMembersAsync(ToInner(key), count, flags); - public Task SetRemoveAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) - { - return Inner.SetRemoveAsync(ToInner(key), values, flags); - } + public Task SetRemoveAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + Inner.SetRemoveAsync(ToInner(key), values, flags); - public IAsyncEnumerable SetScanAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) - => Inner.SetScanAsync(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); + public IAsyncEnumerable SetScanAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => + Inner.SetScanAsync(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); - public Task SetRemoveAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.SetRemoveAsync(ToInner(key), value, flags); - } + public Task SetRemoveAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.SetRemoveAsync(ToInner(key), value, flags); - public Task SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None) - { - return Inner.SortAndStoreAsync(ToInner(destination), ToInner(key), skip, take, order, sortType, SortByToInner(by), SortGetToInner(get), flags); - } + public Task SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) => + Inner.SortAndStoreAsync(ToInner(destination), ToInner(key), skip, take, order, sortType, SortByToInner(by), SortGetToInner(get), flags); - public Task SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None) - { - return Inner.SortAsync(ToInner(key), skip, take, order, sortType, SortByToInner(by), SortGetToInner(get), flags); - } + public Task SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) => + Inner.SortAsync(ToInner(key), skip, take, order, sortType, SortByToInner(by), SortGetToInner(get), flags); - public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, CommandFlags flags) - { - return Inner.SortedSetAddAsync(ToInner(key), values, flags); - } + public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, CommandFlags flags) => + Inner.SortedSetAddAsync(ToInner(key), values, flags); - public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetAddAsync(ToInner(key), values, when, flags); - } + public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetAddAsync(ToInner(key), values, when, flags); - public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, CommandFlags flags) - { - return Inner.SortedSetAddAsync(ToInner(key), member, score, flags); - } + public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, CommandFlags flags) => + Inner.SortedSetAddAsync(ToInner(key), member, score, flags); - public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetAddAsync(ToInner(key), member, score, when, flags); - } + public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetAddAsync(ToInner(key), member, score, when, flags); - public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[] weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetCombineAndStoreAsync(operation, ToInner(destination), ToInner(keys), weights, aggregate, flags); - } + public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetCombineAndStoreAsync(operation, ToInner(destination), ToInner(keys), weights, aggregate, flags); - public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetCombineAndStoreAsync(operation, ToInner(destination), ToInner(first), ToInner(second), aggregate, flags); - } + public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetCombineAndStoreAsync(operation, ToInner(destination), ToInner(first), ToInner(second), aggregate, flags); - public Task SortedSetDecrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetDecrementAsync(ToInner(key), member, value, flags); - } + public Task SortedSetDecrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetDecrementAsync(ToInner(key), member, value, flags); - public Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetIncrementAsync(ToInner(key), member, value, flags); - } + public Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetIncrementAsync(ToInner(key), member, value, flags); - public Task SortedSetLengthAsync(RedisKey key, double min = -1.0 / 0.0, double max = 1.0 / 0.0, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetLengthAsync(ToInner(key), min, max, exclude, flags); - } - - public Task SortedSetLengthByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetLengthByValueAsync(ToInner(key), min, max, exclude, flags); - } + public Task SortedSetLengthAsync(RedisKey key, double min = -1.0 / 0.0, double max = 1.0 / 0.0, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetLengthAsync(ToInner(key), min, max, exclude, flags); - public Task SortedSetRangeByRankAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRangeByRankAsync(ToInner(key), start, stop, order, flags); - } + public Task SortedSetLengthByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetLengthByValueAsync(ToInner(key), min, max, exclude, flags); public Task SortedSetRangeAndStoreAsync( RedisKey sourceKey, @@ -574,401 +374,245 @@ public Task SortedSetRangeAndStoreAsync( Order order = Order.Ascending, long skip = 0, long? take = null, - CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRangeAndStoreAsync(ToInner(sourceKey), ToInner(destinationKey), start, stop, sortedSetOrder, exclude, order, skip, take, flags); - } + CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRangeAndStoreAsync(ToInner(sourceKey), ToInner(destinationKey), start, stop, sortedSetOrder, exclude, order, skip, take, flags); - public Task SortedSetRangeByRankWithScoresAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRangeByRankWithScoresAsync(ToInner(key), start, stop, order, flags); - } + public Task SortedSetRangeByRankAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRangeByRankAsync(ToInner(key), start, stop, order, flags); - public Task SortedSetRangeByScoreAsync(RedisKey key, double start = -1.0 / 0.0, double stop = 1.0 / 0.0, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRangeByScoreAsync(ToInner(key), start, stop, exclude, order, skip, take, flags); - } + public Task SortedSetRangeByRankWithScoresAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRangeByRankWithScoresAsync(ToInner(key), start, stop, order, flags); - public Task SortedSetRangeByScoreWithScoresAsync(RedisKey key, double start = -1.0 / 0.0, double stop = 1.0 / 0.0, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRangeByScoreWithScoresAsync(ToInner(key), start, stop, exclude, order, skip, take, flags); - } + public Task SortedSetRangeByScoreAsync(RedisKey key, double start = -1.0 / 0.0, double stop = 1.0 / 0.0, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRangeByScoreAsync(ToInner(key), start, stop, exclude, order, skip, take, flags); - public Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude, long skip, long take, CommandFlags flags) - { - return Inner.SortedSetRangeByValueAsync(ToInner(key), min, max, exclude, Order.Ascending, skip, take, flags); - } + public Task SortedSetRangeByScoreWithScoresAsync(RedisKey key, double start = -1.0 / 0.0, double stop = 1.0 / 0.0, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRangeByScoreWithScoresAsync(ToInner(key), start, stop, exclude, order, skip, take, flags); - public Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min = default(RedisValue), RedisValue max = default(RedisValue), Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRangeByValueAsync(ToInner(key), min, max, exclude, order, skip, take, flags); - } + public Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude, long skip, long take, CommandFlags flags) => + Inner.SortedSetRangeByValueAsync(ToInner(key), min, max, exclude, Order.Ascending, skip, take, flags); - public Task SortedSetRankAsync(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRankAsync(ToInner(key), member, order, flags); - } + public Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min = default, RedisValue max = default, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRangeByValueAsync(ToInner(key), min, max, exclude, order, skip, take, flags); - public Task SortedSetRemoveAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRemoveAsync(ToInner(key), members, flags); - } + public Task SortedSetRankAsync(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRankAsync(ToInner(key), member, order, flags); - public Task SortedSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRemoveAsync(ToInner(key), member, flags); - } + public Task SortedSetRemoveAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRemoveAsync(ToInner(key), members, flags); - public Task SortedSetRemoveRangeByRankAsync(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRemoveRangeByRankAsync(ToInner(key), start, stop, flags); - } + public Task SortedSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRemoveAsync(ToInner(key), member, flags); - public Task SortedSetRemoveRangeByScoreAsync(RedisKey key, double start, double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRemoveRangeByScoreAsync(ToInner(key), start, stop, exclude, flags); - } + public Task SortedSetRemoveRangeByRankAsync(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRemoveRangeByRankAsync(ToInner(key), start, stop, flags); - public Task SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetRemoveRangeByValueAsync(ToInner(key), min, max, exclude, flags); - } + public Task SortedSetRemoveRangeByScoreAsync(RedisKey key, double start, double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRemoveRangeByScoreAsync(ToInner(key), start, stop, exclude, flags); - public Task SortedSetScoreAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetScoreAsync(ToInner(key), member, flags); - } + public Task SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRemoveRangeByValueAsync(ToInner(key), min, max, exclude, flags); - public IAsyncEnumerable SortedSetScanAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) - => Inner.SortedSetScanAsync(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); + public Task SortedSetScoreAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetScoreAsync(ToInner(key), member, flags); - public Task SortedSetPopAsync(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetPopAsync(ToInner(key), order, flags); - } + public IAsyncEnumerable SortedSetScanAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => + Inner.SortedSetScanAsync(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); - public Task SortedSetPopAsync(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) - { - return Inner.SortedSetPopAsync(ToInner(key), count, order, flags); - } + public Task SortedSetPopAsync(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetPopAsync(ToInner(key), order, flags); - public Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamAcknowledgeAsync(ToInner(key), groupName, messageId, flags); - } + public Task SortedSetPopAsync(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetPopAsync(ToInner(key), count, order, flags); - public Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamAcknowledgeAsync(ToInner(key), groupName, messageIds, flags); - } + public Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) => + Inner.StreamAcknowledgeAsync(ToInner(key), groupName, messageId, flags); - public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamAddAsync(ToInner(key), streamField, streamValue, messageId, maxLength, useApproximateMaxLength, flags); - } + public Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + Inner.StreamAcknowledgeAsync(ToInner(key), groupName, messageIds, flags); - public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamAddAsync(ToInner(key), streamPairs, messageId, maxLength, useApproximateMaxLength, flags); - } + public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + Inner.StreamAddAsync(ToInner(key), streamField, streamValue, messageId, maxLength, useApproximateMaxLength, flags); - public Task StreamClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamClaimAsync(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, messageIds, flags); - } + public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + Inner.StreamAddAsync(ToInner(key), streamPairs, messageId, maxLength, useApproximateMaxLength, flags); - public Task StreamClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamClaimIdsOnlyAsync(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, messageIds, flags); - } + public Task StreamClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + Inner.StreamClaimAsync(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, messageIds, flags); - public Task StreamConsumerGroupSetPositionAsync(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamConsumerGroupSetPositionAsync(ToInner(key), groupName, position, flags); - } + public Task StreamClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + Inner.StreamClaimIdsOnlyAsync(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, messageIds, flags); - public Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position, CommandFlags flags) - { - return Inner.StreamCreateConsumerGroupAsync(ToInner(key), groupName, position, flags); - } + public Task StreamConsumerGroupSetPositionAsync(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None) => + Inner.StreamConsumerGroupSetPositionAsync(ToInner(key), groupName, position, flags); - public Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamCreateConsumerGroupAsync(ToInner(key), groupName, position, createStream, flags); - } + public Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position, CommandFlags flags) => + Inner.StreamCreateConsumerGroupAsync(ToInner(key), groupName, position, flags); - public Task StreamInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamInfoAsync(ToInner(key), flags); - } + public Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None) => + Inner.StreamCreateConsumerGroupAsync(ToInner(key), groupName, position, createStream, flags); - public Task StreamGroupInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamGroupInfoAsync(ToInner(key), flags); - } + public Task StreamInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StreamInfoAsync(ToInner(key), flags); - public Task StreamConsumerInfoAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamConsumerInfoAsync(ToInner(key), groupName, flags); - } + public Task StreamGroupInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StreamGroupInfoAsync(ToInner(key), flags); - public Task StreamLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamLengthAsync(ToInner(key), flags); - } + public Task StreamConsumerInfoAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => + Inner.StreamConsumerInfoAsync(ToInner(key), groupName, flags); - public Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamDeleteAsync(ToInner(key), messageIds, flags); - } + public Task StreamLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StreamLengthAsync(ToInner(key), flags); - public Task StreamDeleteConsumerAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamDeleteConsumerAsync(ToInner(key), groupName, consumerName, flags); - } + public Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + Inner.StreamDeleteAsync(ToInner(key), messageIds, flags); - public Task StreamDeleteConsumerGroupAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamDeleteConsumerGroupAsync(ToInner(key), groupName, flags); - } + public Task StreamDeleteConsumerAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None) => + Inner.StreamDeleteConsumerAsync(ToInner(key), groupName, consumerName, flags); - public Task StreamPendingAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamPendingAsync(ToInner(key), groupName, flags); - } + public Task StreamDeleteConsumerGroupAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => + Inner.StreamDeleteConsumerGroupAsync(ToInner(key), groupName, flags); - public Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamPendingMessagesAsync(ToInner(key), groupName, count, consumerName, minId, maxId, flags); - } + public Task StreamPendingAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => + Inner.StreamPendingAsync(ToInner(key), groupName, flags); - public Task StreamRangeAsync(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamRangeAsync(ToInner(key), minId, maxId, count, messageOrder, flags); - } + public Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamPendingMessagesAsync(ToInner(key), groupName, count, consumerName, minId, maxId, flags); - public Task StreamReadAsync(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamReadAsync(ToInner(key), position, count, flags); - } + public Task StreamRangeAsync(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.StreamRangeAsync(ToInner(key), minId, maxId, count, messageOrder, flags); - public Task StreamReadAsync(StreamPosition[] streamPositions, int? countPerStream = null, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamReadAsync(streamPositions, countPerStream, flags); - } + public Task StreamReadAsync(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamReadAsync(ToInner(key), position, count, flags); - public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, CommandFlags flags) - { - return Inner.StreamReadGroupAsync(ToInner(key), groupName, consumerName, position, count, flags); - } + public Task StreamReadAsync(StreamPosition[] streamPositions, int? countPerStream = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamReadAsync(streamPositions, countPerStream, flags); - public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamReadGroupAsync(ToInner(key), groupName, consumerName, position, count, noAck, flags); - } + public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, CommandFlags flags) => + Inner.StreamReadGroupAsync(ToInner(key), groupName, consumerName, position, count, flags); - public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags) - { - return Inner.StreamReadGroupAsync(streamPositions, groupName, consumerName, countPerStream, flags); - } + public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => + Inner.StreamReadGroupAsync(ToInner(key), groupName, consumerName, position, count, noAck, flags); - public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamReadGroupAsync(streamPositions, groupName, consumerName, countPerStream, noAck, flags); - } + public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags) => + Inner.StreamReadGroupAsync(streamPositions, groupName, consumerName, countPerStream, flags); - public Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) - { - return Inner.StreamTrimAsync(ToInner(key), maxLength, useApproximateMaxLength, flags); - } + public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => + Inner.StreamReadGroupAsync(streamPositions, groupName, consumerName, countPerStream, noAck, flags); - public Task StringAppendAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.StringAppendAsync(ToInner(key), value, flags); - } + public Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + Inner.StreamTrimAsync(ToInner(key), maxLength, useApproximateMaxLength, flags); - public Task StringBitCountAsync(RedisKey key, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) - { - return Inner.StringBitCountAsync(ToInner(key), start, end, flags); - } + public Task StringAppendAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.StringAppendAsync(ToInner(key), value, flags); - public Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.StringBitOperationAsync(operation, ToInner(destination), ToInner(keys), flags); - } + public Task StringBitCountAsync(RedisKey key, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) => + Inner.StringBitCountAsync(ToInner(key), start, end, flags); - public Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default(RedisKey), CommandFlags flags = CommandFlags.None) - { - return Inner.StringBitOperationAsync(operation, ToInner(destination), ToInner(first), ToInnerOrDefault(second), flags); - } + public Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.StringBitOperationAsync(operation, ToInner(destination), ToInner(keys), flags); - public Task StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) - { - return Inner.StringBitPositionAsync(ToInner(key), bit, start, end, flags); - } + public Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, CommandFlags flags = CommandFlags.None) => + Inner.StringBitOperationAsync(operation, ToInner(destination), ToInner(first), ToInnerOrDefault(second), flags); - public Task StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None) - { - return Inner.StringDecrementAsync(ToInner(key), value, flags); - } + public Task StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) => + Inner.StringBitPositionAsync(ToInner(key), bit, start, end, flags); - public Task StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) - { - return Inner.StringDecrementAsync(ToInner(key), value, flags); - } + public Task StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => + Inner.StringDecrementAsync(ToInner(key), value, flags); - public Task StringGetAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetAsync(ToInner(keys), flags); - } + public Task StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => + Inner.StringDecrementAsync(ToInner(key), value, flags); - public Task StringGetAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetAsync(ToInner(key), flags); - } + public Task StringGetAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.StringGetAsync(ToInner(keys), flags); - public Task StringGetSetExpiryAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetSetExpiryAsync(ToInner(key), expiry, flags); - } + public Task StringGetAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StringGetAsync(ToInner(key), flags); + + public Task StringGetSetExpiryAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => + Inner.StringGetSetExpiryAsync(ToInner(key), expiry, flags); - public Task StringGetSetExpiryAsync(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetSetExpiryAsync(ToInner(key), expiry, flags); - } + public Task StringGetSetExpiryAsync(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) => + Inner.StringGetSetExpiryAsync(ToInner(key), expiry, flags); - public Task> StringGetLeaseAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetLeaseAsync(ToInner(key), flags); - } + public Task?> StringGetLeaseAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StringGetLeaseAsync(ToInner(key), flags); - public Task StringGetBitAsync(RedisKey key, long offset, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetBitAsync(ToInner(key), offset, flags); - } + public Task StringGetBitAsync(RedisKey key, long offset, CommandFlags flags = CommandFlags.None) => + Inner.StringGetBitAsync(ToInner(key), offset, flags); - public Task StringGetRangeAsync(RedisKey key, long start, long end, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetRangeAsync(ToInner(key), start, end, flags); - } + public Task StringGetRangeAsync(RedisKey key, long start, long end, CommandFlags flags = CommandFlags.None) => + Inner.StringGetRangeAsync(ToInner(key), start, end, flags); - public Task StringGetSetAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetSetAsync(ToInner(key), value, flags); - } + public Task StringGetSetAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.StringGetSetAsync(ToInner(key), value, flags); - public Task StringGetDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetDeleteAsync(ToInner(key), flags); - } + public Task StringGetDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StringGetDeleteAsync(ToInner(key), flags); - public Task StringGetWithExpiryAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StringGetWithExpiryAsync(ToInner(key), flags); - } + public Task StringGetWithExpiryAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StringGetWithExpiryAsync(ToInner(key), flags); - public Task StringIncrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None) - { - return Inner.StringIncrementAsync(ToInner(key), value, flags); - } + public Task StringIncrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => + Inner.StringIncrementAsync(ToInner(key), value, flags); - public Task StringIncrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) - { - return Inner.StringIncrementAsync(ToInner(key), value, flags); - } + public Task StringIncrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => + Inner.StringIncrementAsync(ToInner(key), value, flags); - public Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.StringLengthAsync(ToInner(key), flags); - } + public Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StringLengthAsync(ToInner(key), flags); - public Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.StringSetAsync(ToInner(values), when, flags); - } + public Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.StringSetAsync(ToInner(values), when, flags); - public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) - { - return Inner.StringSetAsync(ToInner(key), value, expiry, when, flags); - } + public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + Inner.StringSetAsync(ToInner(key), value, expiry, when, flags); - public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.StringSetAsync(ToInner(key), value, expiry, keepTtl, when, flags); - } + public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.StringSetAsync(ToInner(key), value, expiry, keepTtl, when, flags); - public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) - { - return Inner.StringSetAndGetAsync(ToInner(key), value, expiry, when, flags); - } + public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => + Inner.StringSetAndGetAsync(ToInner(key), value, expiry, when, flags); - public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) - { - return Inner.StringSetAndGetAsync(ToInner(key), value, expiry, keepTtl, when, flags); - } + public Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.StringSetAndGetAsync(ToInner(key), value, expiry, keepTtl, when, flags); - public Task StringSetBitAsync(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) - { - return Inner.StringSetBitAsync(ToInner(key), offset, bit, flags); - } + public Task StringSetBitAsync(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None) => + Inner.StringSetBitAsync(ToInner(key), offset, bit, flags); - public Task StringSetRangeAsync(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None) - { - return Inner.StringSetRangeAsync(ToInner(key), offset, value, flags); - } + public Task StringSetRangeAsync(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None) => + Inner.StringSetRangeAsync(ToInner(key), offset, value, flags); - public Task PingAsync(CommandFlags flags = CommandFlags.None) - { - return Inner.PingAsync(flags); - } + public Task PingAsync(CommandFlags flags = CommandFlags.None) => + Inner.PingAsync(flags); - public Task KeyTouchAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyTouchAsync(ToInner(keys), flags); - } + public Task KeyTouchAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => + Inner.KeyTouchAsync(ToInner(keys), flags); - public Task KeyTouchAsync(RedisKey key, CommandFlags flags = CommandFlags.None) - { - return Inner.KeyTouchAsync(ToInner(key), flags); - } + public Task KeyTouchAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyTouchAsync(ToInner(key), flags); - public bool TryWait(Task task) - { - return Inner.TryWait(task); - } + public bool TryWait(Task task) => + Inner.TryWait(task); - public TResult Wait(Task task) - { - return Inner.Wait(task); - } + public TResult Wait(Task task) => + Inner.Wait(task); - public void Wait(Task task) - { + public void Wait(Task task) => Inner.Wait(task); - } - public void WaitAll(params Task[] tasks) - { + public void WaitAll(params Task[] tasks) => Inner.WaitAll(tasks); - } - protected internal RedisKey ToInner(RedisKey outer) - { - return RedisKey.WithPrefix(Prefix, outer); - } - protected RedisKey ToInnerOrDefault(RedisKey outer) - { - if (outer == default(RedisKey)) - { - return outer; - } - else - { - return ToInner(outer); - } - } + protected internal RedisKey ToInner(RedisKey outer) => + RedisKey.WithPrefix(Prefix, outer); + + protected RedisKey ToInnerOrDefault(RedisKey outer) => + (outer == default(RedisKey)) ? outer : ToInner(outer); - protected ICollection ToInner(ICollection args) + [return: NotNullIfNotNull("args")] + protected ICollection? ToInner(ICollection? args) { if (args?.Any(x => x is RedisKey || x is RedisChannel) == true) { @@ -996,7 +640,8 @@ protected ICollection ToInner(ICollection args) return args; } - protected RedisKey[] ToInner(RedisKey[] outer) + [return: NotNullIfNotNull("outer")] + protected RedisKey[]? ToInner(RedisKey[]? outer) { if (outer == null || outer.Length == 0) { @@ -1015,12 +660,11 @@ protected RedisKey[] ToInner(RedisKey[] outer) } } - protected KeyValuePair ToInner(KeyValuePair outer) - { - return new KeyValuePair(ToInner(outer.Key), outer.Value); - } + protected KeyValuePair ToInner(KeyValuePair outer) => + new KeyValuePair(ToInner(outer.Key), outer.Value); - protected KeyValuePair[] ToInner(KeyValuePair[] outer) + [return: NotNullIfNotNull("outer")] + protected KeyValuePair[]? ToInner(KeyValuePair[]? outer) { if (outer == null || outer.Length == 0) { @@ -1039,36 +683,17 @@ protected KeyValuePair[] ToInner(KeyValuePair + RedisKey.ConcatenateBytes(Prefix, null, (byte[]?)outer); - protected RedisValue SortByToInner(RedisValue outer) - { - if (outer == "nosort") - { - return outer; - } - else - { - return ToInner(outer); - } - } + protected RedisValue SortByToInner(RedisValue outer) => + (outer == "nosort") ? outer : ToInner(outer); - protected RedisValue SortGetToInner(RedisValue outer) - { - if (outer == "#") - { - return outer; - } - else - { - return ToInner(outer); - } - } + protected RedisValue SortGetToInner(RedisValue outer) => + (outer == "#") ? outer : ToInner(outer); - protected RedisValue[] SortGetToInner(RedisValue[] outer) + [return: NotNullIfNotNull("outer")] + protected RedisValue[]? SortGetToInner(RedisValue[]? outer) { if (outer == null || outer.Length == 0) { @@ -1087,16 +712,12 @@ protected RedisValue[] SortGetToInner(RedisValue[] outer) } } - protected RedisChannel ToInner(RedisChannel outer) - { - return RedisKey.ConcatenateBytes(Prefix, null, (byte[])outer); - } + protected RedisChannel ToInner(RedisChannel outer) => + RedisKey.ConcatenateBytes(Prefix, null, (byte[]?)outer); - private Func mapFunction; - protected Func GetMapFunction() - { + private Func? mapFunction; + protected Func GetMapFunction() => // create as a delegate when first required, then re-use - return mapFunction ??= new Func(ToInner); - } + mapFunction ??= new Func(ToInner); } } diff --git a/src/StackExchange.Redis/Lease.cs b/src/StackExchange.Redis/Lease.cs index 1dbbcea1b..91495dd08 100644 --- a/src/StackExchange.Redis/Lease.cs +++ b/src/StackExchange.Redis/Lease.cs @@ -16,7 +16,7 @@ public sealed class Lease : IMemoryOwner /// public static Lease Empty { get; } = new Lease(System.Array.Empty(), 0); - private T[] _arr; + private T[]? _arr; /// /// The length of the lease. diff --git a/src/StackExchange.Redis/LogProxy.cs b/src/StackExchange.Redis/LogProxy.cs index 6d24621ad..1e6fa7320 100644 --- a/src/StackExchange.Redis/LogProxy.cs +++ b/src/StackExchange.Redis/LogProxy.cs @@ -5,12 +5,12 @@ namespace StackExchange.Redis; internal sealed class LogProxy : IDisposable { - public static LogProxy TryCreate(TextWriter writer) + public static LogProxy? TryCreate(TextWriter? writer) => writer == null ? null : new LogProxy(writer); public override string ToString() { - string s = null; + string? s = null; if (_log != null) { lock (SyncLock) @@ -18,9 +18,9 @@ public override string ToString() s = _log?.ToString(); } } - return s ?? base.ToString(); + return s ?? base.ToString() ?? string.Empty; } - private TextWriter _log; + private TextWriter? _log; internal static Action NullWriter = _ => { }; public object SyncLock => this; @@ -35,7 +35,7 @@ public void WriteLine() } } } - public void WriteLine(string message = null) + public void WriteLine(string? message = null) { if (_log != null) // note: double-checked { diff --git a/src/StackExchange.Redis/LuaScript.cs b/src/StackExchange.Redis/LuaScript.cs index 9d85b3324..9c7d6553c 100644 --- a/src/StackExchange.Redis/LuaScript.cs +++ b/src/StackExchange.Redis/LuaScript.cs @@ -43,7 +43,7 @@ public sealed class LuaScript private bool HasArguments => Arguments?.Length > 0; - private readonly Hashtable ParameterMappers; + private readonly Hashtable? ParameterMappers; internal LuaScript(string originalScript, string executableScript, string[] arguments) { @@ -87,9 +87,7 @@ internal LuaScript(string originalScript, string executableScript, string[] argu /// The script to prepare. public static LuaScript Prepare(string script) { - LuaScript ret; - - if (!Cache.TryGetValue(script, out WeakReference weakRef) || (ret = (LuaScript)weakRef.Target) == null) + if (!Cache.TryGetValue(script, out WeakReference? weakRef) || weakRef.Target is not LuaScript ret) { ret = ScriptParameterMapper.PrepareScript(script); Cache[script] = new WeakReference(ret); @@ -98,22 +96,22 @@ public static LuaScript Prepare(string script) return ret; } - internal void ExtractParameters(object ps, RedisKey? keyPrefix, out RedisKey[] keys, out RedisValue[] args) + internal void ExtractParameters(object? ps, RedisKey? keyPrefix, out RedisKey[]? keys, out RedisValue[]? args) { if (HasArguments) { if (ps == null) throw new ArgumentNullException(nameof(ps), "Script requires parameters"); var psType = ps.GetType(); - var mapper = (Func)ParameterMappers[psType]; + var mapper = (Func?)ParameterMappers![psType]; if (mapper == null) { lock (ParameterMappers) { - mapper = (Func)ParameterMappers[psType]; + mapper = (Func?)ParameterMappers[psType]; if (mapper == null) { - if (!ScriptParameterMapper.IsValidParameterHash(psType, this, out string missingMember, out string badMemberType)) + if (!ScriptParameterMapper.IsValidParameterHash(psType, this, out string? missingMember, out string? badMemberType)) { if (missingMember != null) { @@ -146,9 +144,9 @@ internal void ExtractParameters(object ps, RedisKey? keyPrefix, out RedisKey[] k /// The parameter object to use. /// The key prefix to use, if any. /// The command flags to use. - public RedisResult Evaluate(IDatabase db, object ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None) + public RedisResult Evaluate(IDatabase db, object? ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None) { - ExtractParameters(ps, withKeyPrefix, out RedisKey[] keys, out RedisValue[] args); + ExtractParameters(ps, withKeyPrefix, out RedisKey[]? keys, out RedisValue[]? args); return db.ScriptEvaluate(ExecutableScript, keys, args, flags); } @@ -159,9 +157,9 @@ public RedisResult Evaluate(IDatabase db, object ps = null, RedisKey? withKeyPre /// The parameter object to use. /// The key prefix to use, if any. /// The command flags to use. - public Task EvaluateAsync(IDatabaseAsync db, object ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None) + public Task EvaluateAsync(IDatabaseAsync db, object? ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None) { - ExtractParameters(ps, withKeyPrefix, out RedisKey[] keys, out RedisValue[] args); + ExtractParameters(ps, withKeyPrefix, out RedisKey[]? keys, out RedisValue[]? args); return db.ScriptEvaluateAsync(ExecutableScript, keys, args, flags); } @@ -182,7 +180,7 @@ public LoadedLuaScript Load(IServer server, CommandFlags flags = CommandFlags.No } var hash = server.ScriptLoad(ExecutableScript, flags); - return new LoadedLuaScript(this, hash); + return new LoadedLuaScript(this, hash!); // not nullable because fire and forget is disabled } /// @@ -201,8 +199,8 @@ public async Task LoadAsync(IServer server, CommandFlags flags throw new ArgumentOutOfRangeException(nameof(flags), "Loading a script cannot be FireAndForget"); } - var hash = await server.ScriptLoadAsync(ExecutableScript, flags).ForAwait(); - return new LoadedLuaScript(this, hash); + var hash = await server.ScriptLoadAsync(ExecutableScript, flags).ForAwait()!; + return new LoadedLuaScript(this, hash!); // not nullable because fire and forget is disabled } } @@ -264,9 +262,9 @@ internal LoadedLuaScript(LuaScript original, byte[] hash) /// The parameter object to use. /// The key prefix to use, if any. /// The command flags to use. - public RedisResult Evaluate(IDatabase db, object ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None) + public RedisResult Evaluate(IDatabase db, object? ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None) { - Original.ExtractParameters(ps, withKeyPrefix, out RedisKey[] keys, out RedisValue[] args); + Original.ExtractParameters(ps, withKeyPrefix, out RedisKey[]? keys, out RedisValue[]? args); return db.ScriptEvaluate(Hash, keys, args, flags); } @@ -281,9 +279,9 @@ public RedisResult Evaluate(IDatabase db, object ps = null, RedisKey? withKeyPre /// The parameter object to use. /// The key prefix to use, if any. /// The command flags to use. - public Task EvaluateAsync(IDatabaseAsync db, object ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None) + public Task EvaluateAsync(IDatabaseAsync db, object? ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None) { - Original.ExtractParameters(ps, withKeyPrefix, out RedisKey[] keys, out RedisValue[] args); + Original.ExtractParameters(ps, withKeyPrefix, out RedisKey[]? keys, out RedisValue[]? args); return db.ScriptEvaluateAsync(Hash, keys, args, flags); } } diff --git a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs index 84d90ea85..5b0a6b126 100644 --- a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs @@ -114,7 +114,7 @@ internal AzureMaintenanceEvent(string azureEvent) } } - internal async static Task AddListenerAsync(ConnectionMultiplexer multiplexer, Action log = null) + internal async static Task AddListenerAsync(ConnectionMultiplexer multiplexer, Action? log = null) { if (!multiplexer.CommandMap.IsAvailable(RedisCommand.SUBSCRIBE)) { @@ -132,7 +132,7 @@ internal async static Task AddListenerAsync(ConnectionMultiplexer multiplexer, A await sub.SubscribeAsync(PubSubChannelName, async (_, message) => { - var newMessage = new AzureMaintenanceEvent(message); + var newMessage = new AzureMaintenanceEvent(message!); newMessage.NotifyMultiplexer(multiplexer); switch (newMessage.NotificationType) @@ -154,7 +154,7 @@ await sub.SubscribeAsync(PubSubChannelName, async (_, message) => /// /// Indicates the type of event (raw string form). /// - public string NotificationTypeString { get; } + public string NotificationTypeString { get; } = "Unknown"; /// /// The parsed version of for easier consumption. @@ -169,7 +169,7 @@ await sub.SubscribeAsync(PubSubChannelName, async (_, message) => /// /// IPAddress of the node event is intended for. /// - public IPAddress IPAddress { get; } + public IPAddress? IPAddress { get; } /// /// SSL Port. diff --git a/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs index dd18a803e..cb0d43c6c 100644 --- a/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/ServerMaintenanceEvent.cs @@ -15,7 +15,7 @@ internal ServerMaintenanceEvent() /// /// Raw message received from the server. /// - public string RawMessage { get; protected set; } + public string? RawMessage { get; protected set; } /// /// The time the event was received. If we know when the event is expected to start will be populated. @@ -30,7 +30,7 @@ internal ServerMaintenanceEvent() /// /// Returns a string representing the maintenance event with all of its properties. /// - public override string ToString() => RawMessage; + public override string? ToString() => RawMessage; /// /// Notifies a ConnectionMultiplexer of this event, for anyone observing its handler. diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 4a366449f..901f0173a 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -14,7 +14,7 @@ internal sealed class LoggingMessage : Message public readonly LogProxy log; private readonly Message tail; - public static Message Create(LogProxy log, Message tail) + public static Message Create(LogProxy? log, Message tail) { return log == null ? tail : new LoggingMessage(log, tail); } @@ -37,7 +37,7 @@ protected override void WriteImpl(PhysicalConnection physical) try { var bridge = physical.BridgeCouldBeNull; - log?.WriteLine($"{bridge.Name}: Writing: {tail.CommandAndKey}"); + log?.WriteLine($"{bridge?.Name}: Writing: {tail.CommandAndKey}"); } catch { } tail.WriteTo(physical); @@ -75,12 +75,12 @@ internal abstract class Message : ICompletable | CommandFlags.FireAndForget | CommandFlags.NoRedirect | CommandFlags.NoScriptCache; - private IResultBox resultBox; + private IResultBox? resultBox; - private ResultProcessor resultProcessor; + private ResultProcessor? resultProcessor; // All for profiling purposes - private ProfiledCommand performance; + private ProfiledCommand? performance; internal DateTime CreatedDateTime; internal long CreatedTimestamp; @@ -204,7 +204,7 @@ public bool IsAdmin public bool IsFireAndForget => (Flags & CommandFlags.FireAndForget) != 0; public bool IsInternalCall => (Flags & InternalCallFlag) != 0; - public IResultBox ResultBox => resultBox; + public IResultBox? ResultBox => resultBox; public abstract int ArgCount { get; } // note: over-estimate if necessary @@ -599,13 +599,13 @@ internal bool ComputeResult(PhysicalConnection connection, in RawResult result) } } - internal void Fail(ConnectionFailureType failure, Exception innerException, string annotation, ConnectionMultiplexer muxer) + internal void Fail(ConnectionFailureType failure, Exception? innerException, string? annotation, ConnectionMultiplexer? muxer) { PhysicalConnection.IdentifyFailureType(innerException, ref failure); resultProcessor?.ConnectionFail(this, failure, innerException, annotation, muxer); } - internal virtual void SetExceptionAndComplete(Exception exception, PhysicalBridge bridge) + internal virtual void SetExceptionAndComplete(Exception exception, PhysicalBridge? bridge) { resultBox?.SetException(exception); Complete(); @@ -621,7 +621,7 @@ internal bool TrySetResult(T value) return false; } - internal void SetEnqueued(PhysicalConnection connection) + internal void SetEnqueued(PhysicalConnection? connection) { SetWriteTime(); performance?.SetEnqueued(connection?.BridgeCouldBeNull?.ConnectionType); @@ -636,7 +636,7 @@ internal void SetEnqueued(PhysicalConnection connection) } } - internal void TryGetHeadMessages(out Message now, out Message next) + internal void TryGetHeadMessages(out Message? now, out Message? next) { now = next = null; _enqueuedTo?.GetHeadMessages(out now, out next); @@ -667,7 +667,7 @@ internal bool TryGetPhysicalState( } } - private PhysicalConnection _enqueuedTo; + private PhysicalConnection? _enqueuedTo; private long _queuedStampReceived, _queuedStampSent; internal void SetRequestSent() @@ -725,7 +725,7 @@ internal void SetPreferReplica() => /// /// Note order here is reversed to prevent overload resolution errors. /// - internal void SetSource(ResultProcessor resultProcessor, IResultBox resultBox) + internal void SetSource(ResultProcessor? resultProcessor, IResultBox? resultBox) { this.resultBox = resultBox; this.resultProcessor = resultProcessor; @@ -737,7 +737,7 @@ internal void SetSource(ResultProcessor resultProcessor, IResultBox resultBox) /// /// Note order here is reversed to prevent overload resolution errors. /// - internal void SetSource(IResultBox resultBox, ResultProcessor resultProcessor) + internal void SetSource(IResultBox resultBox, ResultProcessor? resultProcessor) { this.resultBox = resultBox; this.resultProcessor = resultProcessor; @@ -783,7 +783,7 @@ protected CommandKeyBase(int db, CommandFlags flags, RedisCommand command, in Re Key = key; } - public override string CommandAndKey => Command + " " + (string)Key; + public override string CommandAndKey => Command + " " + (string?)Key; public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(Key); } @@ -1447,7 +1447,7 @@ public CommandValueKeyMessage(int db, CommandFlags flags, RedisCommand command, public override void AppendStormLog(StringBuilder sb) { base.AppendStormLog(sb); - sb.Append(" (").Append((string)value).Append(')'); + sb.Append(" (").Append((string?)value).Append(')'); } protected override void WriteImpl(PhysicalConnection physical) diff --git a/src/StackExchange.Redis/MessageCompletable.cs b/src/StackExchange.Redis/MessageCompletable.cs index 7648bc42e..8f4737943 100644 --- a/src/StackExchange.Redis/MessageCompletable.cs +++ b/src/StackExchange.Redis/MessageCompletable.cs @@ -18,7 +18,7 @@ public MessageCompletable(RedisChannel channel, RedisValue message, Action (string)channel; + public override string? ToString() => (string?)channel; public bool TryComplete(bool isAsync) { @@ -26,7 +26,7 @@ public bool TryComplete(bool isAsync) { if (handler != null) { - ConnectionMultiplexer.TraceWithoutContext("Invoking (async)...: " + (string)channel, "Subscription"); + ConnectionMultiplexer.TraceWithoutContext("Invoking (async)...: " + (string?)channel, "Subscription"); if (handler.IsSingle()) { try { handler(channel, message); } catch { } @@ -48,6 +48,6 @@ public bool TryComplete(bool isAsync) } } - void ICompletable.AppendStormLog(StringBuilder sb) => sb.Append("event, pub/sub: ").Append((string)channel); + void ICompletable.AppendStormLog(StringBuilder sb) => sb.Append("event, pub/sub: ").Append((string?)channel); } } diff --git a/src/StackExchange.Redis/NameValueEntry.cs b/src/StackExchange.Redis/NameValueEntry.cs index 6756cfbb3..17cca1efc 100644 --- a/src/StackExchange.Redis/NameValueEntry.cs +++ b/src/StackExchange.Redis/NameValueEntry.cs @@ -57,7 +57,7 @@ public static implicit operator NameValueEntry(KeyValuePair /// The to compare to. - public override bool Equals(object obj) => obj is NameValueEntry heObj && Equals(heObj); + public override bool Equals(object? obj) => obj is NameValueEntry heObj && Equals(heObj); /// /// Compares two values for equality. diff --git a/src/StackExchange.Redis/NullableHacks.cs b/src/StackExchange.Redis/NullableHacks.cs new file mode 100644 index 000000000..5f8969c73 --- /dev/null +++ b/src/StackExchange.Redis/NullableHacks.cs @@ -0,0 +1,148 @@ +// https://github.com/dotnet/runtime/blob/527f9ae88a0ee216b44d556f9bdc84037fe0ebda/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs + +#pragma warning disable +#define INTERNAL_NULLABLE_ATTRIBUTES + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class AllowNullAttribute : Attribute { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class DisallowNullAttribute : Attribute { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class MaybeNullAttribute : Attribute { } + + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + internal sealed class DoesNotReturnAttribute : Attribute { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } +#endif + +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute + { + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = new[] { member }; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } + } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + } +#endif +} diff --git a/src/StackExchange.Redis/PerfCounterHelper.cs b/src/StackExchange.Redis/PerfCounterHelper.cs index 223e719c2..f7b5b5421 100644 --- a/src/StackExchange.Redis/PerfCounterHelper.cs +++ b/src/StackExchange.Redis/PerfCounterHelper.cs @@ -11,7 +11,7 @@ namespace StackExchange.Redis internal static class PerfCounterHelper { private static readonly object staticLock = new(); - private static volatile PerformanceCounter _cpu; + private static volatile PerformanceCounter? _cpu; private static volatile bool _disabled = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); #if NET5_0_OR_GREATER @@ -58,7 +58,7 @@ public static bool TryGetSystemCPU(out float value) internal static string GetThreadPoolAndCPUSummary(bool includePerformanceCounters) { - GetThreadPoolStats(out string iocp, out string worker, out string workItems); + GetThreadPoolStats(out string iocp, out string worker, out string? workItems); var cpu = includePerformanceCounters ? GetSystemCpuPercent() : "n/a"; return $"IOCP: {iocp}, WORKER: {worker}, POOL: {workItems ?? "n/a"}, Local-CPU: {cpu}"; } @@ -68,7 +68,7 @@ internal static string GetSystemCpuPercent() => ? Math.Round(systemCPU, 2) + "%" : "unavailable"; - internal static int GetThreadPoolStats(out string iocp, out string worker, out string workItems) + internal static int GetThreadPoolStats(out string iocp, out string worker, out string? workItems) { ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxIoThreads); ThreadPool.GetAvailableThreads(out int freeWorkerThreads, out int freeIoThreads); diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index fc8f58a6f..64080d07f 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text; using System.Threading; @@ -49,7 +50,7 @@ internal sealed class PhysicalBridge : IDisposable //private volatile int missedHeartbeats; private long operationCount, socketCount; - private volatile PhysicalConnection physical; + private volatile PhysicalConnection? physical; private long profileLastLog; private int profileLogIndex; @@ -64,7 +65,8 @@ internal sealed class PhysicalBridge : IDisposable private readonly MutexSlim _singleWriterMutex; #endif - internal string PhysicalName => physical?.ToString(); + internal string? PhysicalName => physical?.ToString(); + public PhysicalBridge(ServerEndPoint serverEndPoint, ConnectionType type, int timeoutMilliseconds) { ServerEndPoint = serverEndPoint; @@ -87,7 +89,7 @@ public enum State : byte Disconnected } - public Exception LastException { get; private set; } + public Exception? LastException { get; private set; } public ConnectionType ConnectionType { get; } @@ -336,7 +338,7 @@ internal void KeepAlive() if (!(physical?.IsIdle() ?? false)) return; // don't pile on if already doing something var commandMap = Multiplexer.CommandMap; - Message msg = null; + Message? msg = null; var features = ServerEndPoint.GetFeatures(); switch (ConnectionType) { @@ -364,7 +366,7 @@ internal void KeepAlive() { msg.SetInternalCall(); Multiplexer.Trace("Enqueue: " + msg); - Multiplexer.OnInfoMessage($"heartbeat ({physical?.LastWriteSecondsAgo}s >= {ServerEndPoint?.WriteEverySeconds}s, {physical?.GetSentAwaitingResponseCount()} waiting) '{msg.CommandAndKey}' on '{PhysicalName}' (v{features.Version})"); + Multiplexer.OnInfoMessage($"heartbeat ({physical?.LastWriteSecondsAgo}s >= {ServerEndPoint.WriteEverySeconds}s, {physical?.GetSentAwaitingResponseCount()} waiting) '{msg.CommandAndKey}' on '{PhysicalName}' (v{features.Version})"); physical?.UpdateLastWriteTime(); // preemptively #pragma warning disable CS0618 // Type or member is obsolete var result = TryWriteSync(msg, ServerEndPoint.IsReplica); @@ -378,7 +380,7 @@ internal void KeepAlive() } } - internal async Task OnConnectedAsync(PhysicalConnection connection, LogProxy log) + internal async Task OnConnectedAsync(PhysicalConnection connection, LogProxy? log) { Trace("OnConnected"); if (physical == connection && !isDisposed && ChangeState(State.Connecting, State.ConnectedEstablishing)) @@ -424,7 +426,7 @@ internal void OnConnectionFailed(PhysicalConnection connection, ConnectionFailur } } - internal void OnDisconnected(ConnectionFailureType failureType, PhysicalConnection connection, out bool isCurrent, out State oldState) + internal void OnDisconnected(ConnectionFailureType failureType, PhysicalConnection? connection, out bool isCurrent, out State oldState) { Trace($"OnDisconnected: {failureType}"); @@ -462,7 +464,7 @@ internal void OnDisconnected(ConnectionFailureType failureType, PhysicalConnecti private void AbandonPendingBacklog(Exception ex) { - while (BacklogTryDequeue(out Message next)) + while (BacklogTryDequeue(out Message? next)) { Multiplexer?.OnMessageFaulted(next, ex); next.SetExceptionAndComplete(ex, this); @@ -472,7 +474,7 @@ private void AbandonPendingBacklog(Exception ex) internal void OnFullyEstablished(PhysicalConnection connection, string source) { Trace("OnFullyEstablished"); - connection?.SetIdle(); + connection.SetIdle(); if (physical == connection && !isDisposed && ChangeState(State.ConnectedEstablishing, State.ConnectedEstablished)) { reportNextFailure = reconfigureNextFailure = true; @@ -584,7 +586,7 @@ internal void OnHeartbeat(bool ifConnectedOnly) if (!ifConnectedOnly) { Multiplexer.Trace("Resurrecting " + ToString()); - Multiplexer.OnResurrecting(ServerEndPoint?.EndPoint, ConnectionType); + Multiplexer.OnResurrecting(ServerEndPoint.EndPoint, ConnectionType); TryConnect(null); } break; @@ -642,7 +644,7 @@ internal bool TryEnqueue(List messages, bool isReplica) return true; } - private Message _activeMessage; + private Message? _activeMessage; private WriteResult WriteMessageInsideLock(PhysicalConnection physical, Message message) { @@ -800,7 +802,7 @@ private void BacklogEnqueue(Message message) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool BacklogTryDequeue(out Message message) + private bool BacklogTryDequeue([NotNullWhen(true)] out Message? message) { if (_backlog.TryDequeue(out message)) { @@ -827,7 +829,7 @@ private void StartBacklogProcessor() // to unblock the thread-pool when there could be sync-over-async callers. Note that in reality, // the initial "enough" of the back-log processor is typically sync, which means that the thread // we start is actually useful, despite thinking "but that will just go async and back to the pool" - var thread = new Thread(s => ((PhysicalBridge)s).ProcessBacklogAsync().RedisFireAndForget()) + var thread = new Thread(s => ((PhysicalBridge)s!).ProcessBacklogAsync().RedisFireAndForget()) { IsBackground = true, // don't keep process alive (also: act like the thread-pool used to) Name = "StackExchange.Redis Backlog", // help anyone looking at thread-dumps @@ -853,7 +855,7 @@ private void CheckBacklogForTimeouts() // Because peeking at the backlog, checking message and then dequeuing, is not thread-safe, we do have to use // a lock here, for mutual exclusion of backlog DEQUEUERS. Unfortunately. // But we reduce contention by only locking if we see something that looks timed out. - while (_backlog.TryPeek(out Message message)) + while (_backlog.TryPeek(out Message? message)) { // See if the message has pass our async timeout threshold // or has otherwise been completed (e.g. a sync wait timed out) which would have cleared the ResultBox @@ -1003,7 +1005,7 @@ private async Task ProcessBridgeBacklogAsync() // If we can't write them, abort and wait for the next heartbeat or activation to try this again. while (IsConnected && physical?.HasOutputPipe == true) { - Message message; + Message? message; _backlogStatus = BacklogStatus.CheckingForWork; lock (_backlog) @@ -1237,7 +1239,7 @@ private async ValueTask CompleteWriteAndReleaseLockAsync( try { var result = await flush.ForAwait(); - physical.SetIdle(); + physical?.SetIdle(); return result; } catch (Exception ex) @@ -1283,7 +1285,7 @@ private bool ChangeState(State oldState, State newState) return result; } - public PhysicalConnection TryConnect(LogProxy log) + public PhysicalConnection? TryConnect(LogProxy? log) { if (state == (int)State.Disconnected) { @@ -1335,7 +1337,7 @@ private void LogNonPreferred(CommandFlags flags, bool isReplica) } } - private void OnInternalError(Exception exception, [CallerMemberName] string origin = null) + private void OnInternalError(Exception exception, [CallerMemberName] string? origin = null) { Multiplexer.OnInternalError(exception, ServerEndPoint.EndPoint, ConnectionType, origin); } diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index d7c5fb991..19669cbe9 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -22,7 +22,7 @@ namespace StackExchange.Redis { internal sealed partial class PhysicalConnection : IDisposable { - internal readonly byte[] ChannelPrefix; + internal readonly byte[]? ChannelPrefix; private const int DefaultRedisDatabaseCount = 16; @@ -65,11 +65,15 @@ internal void GetBytes(out long sent, out long received) } } - private IDuplexPipe _ioPipe; + /// + /// Nullable because during simulation of failure, we'll null out. + /// ...but in those cases, we'll accept any null ref in a race - it's fine. + /// + private IDuplexPipe? _ioPipe; internal bool HasOutputPipe => _ioPipe?.Output != null; - private Socket _socket; - internal Socket VolatileSocket => Volatile.Read(ref _socket); + private Socket? _socket; + internal Socket? VolatileSocket => Volatile.Read(ref _socket); public PhysicalConnection(PhysicalBridge bridge) { @@ -85,11 +89,11 @@ public PhysicalConnection(PhysicalBridge bridge) OnCreateEcho(); } - internal async Task BeginConnectAsync(LogProxy log) + internal async Task BeginConnectAsync(LogProxy? log) { var bridge = BridgeCouldBeNull; var endpoint = bridge?.ServerEndPoint?.EndPoint; - if (endpoint == null) + if (bridge == null || endpoint == null) { log?.WriteLine("No endpoint"); return; @@ -101,7 +105,7 @@ internal async Task BeginConnectAsync(LogProxy log) bridge.Multiplexer.OnConnecting(endpoint, bridge.ConnectionType); log?.WriteLine($"{Format.ToString(endpoint)}: BeginConnectAsync"); - CancellationTokenSource timeoutSource = null; + CancellationTokenSource? timeoutSource = null; try { using (var args = new SocketAwaitableEventArgs @@ -141,7 +145,7 @@ internal async Task BeginConnectAsync(LogProxy log) { ConnectionMultiplexer.TraceWithoutContext("Socket was already aborted"); } - else if (await ConnectedAsync(x, log, bridge.Multiplexer.SocketManager).ForAwait()) + else if (await ConnectedAsync(x, log, bridge.Multiplexer.SocketManager!).ForAwait()) { log?.WriteLine($"{Format.ToString(endpoint)}: Starting read"); try @@ -199,7 +203,7 @@ private static CancellationTokenSource ConfigureTimeout(SocketAwaitableEventArgs { try { - var a = (SocketAwaitableEventArgs)state; + var a = (SocketAwaitableEventArgs)state!; a.Abort(SocketError.TimedOut); Socket.CancelConnectAsync(a); } @@ -216,7 +220,7 @@ private enum ReadMode : byte } private readonly WeakReference _bridge; - public PhysicalBridge BridgeCouldBeNull => (PhysicalBridge)_bridge.Target; + public PhysicalBridge? BridgeCouldBeNull => (PhysicalBridge?)_bridge.Target; public long LastWriteSecondsAgo => unchecked(Environment.TickCount - Thread.VolatileRead(ref lastWriteTickCount)) / 1000; @@ -330,18 +334,21 @@ internal void SimulateConnectionFailure(SimulatedFailureType failureType) public void RecordConnectionFailed( ConnectionFailureType failureType, - Exception innerException = null, - [CallerMemberName] string origin = null, + Exception? innerException = null, + [CallerMemberName] string? origin = null, bool isInitialConnect = false, - IDuplexPipe connectingPipe = null + IDuplexPipe? connectingPipe = null ) { - Exception outerException = innerException; + Exception? outerException = innerException; IdentifyFailureType(innerException, ref failureType); var bridge = BridgeCouldBeNull; if (_ioPipe != null || isInitialConnect) // if *we* didn't burn the pipe: flag it { - if (failureType == ConnectionFailureType.InternalFailure) OnInternalError(innerException, origin); + if (failureType == ConnectionFailureType.InternalFailure && innerException is not null) + { + OnInternalError(innerException, origin); + } // stop anything new coming in... bridge?.Trace("Failed: " + failureType); @@ -396,8 +403,8 @@ public void RecordConnectionFailed( else if (recd == 0) { exMessage.Append(" (0-read)"); } } - var data = new List>(); - void add(string lk, string sk, string v) + var data = new List>(); + void add(string lk, string sk, string? v) { if (lk != null) data.Add(Tuple.Create(lk, v)); if (sk != null) exMessage.Append(", ").Append(sk).Append(": ").Append(v); @@ -411,8 +418,8 @@ void add(string lk, string sk, string v) .Append(", ").Append(_writeStatus).Append('/').Append(_readStatus) .Append(", last: ").Append(bridge.LastCommand); - data.Add(Tuple.Create("FailureType", failureType.ToString())); - data.Add(Tuple.Create("EndPoint", Format.ToString(bridge.ServerEndPoint?.EndPoint))); + data.Add(Tuple.Create("FailureType", failureType.ToString())); + data.Add(Tuple.Create("EndPoint", Format.ToString(bridge.ServerEndPoint?.EndPoint))); add("Origin", "origin", origin); // add("Input-Buffer", "input-buffer", _ioPipe.Input); @@ -427,7 +434,7 @@ void add(string lk, string sk, string v) if (connStatus.BytesInReadPipe >= 0) add("Inbound-Pipe-Bytes", "in-pipe", connStatus.BytesInReadPipe.ToString()); if (connStatus.BytesInWritePipe >= 0) add("Outbound-Pipe-Bytes", "out-pipe", connStatus.BytesInWritePipe.ToString()); - add("Last-Heartbeat", "last-heartbeat", (lastBeat == 0 ? "never" : ((unchecked(now - lastBeat) / 1000) + "s ago")) + (BridgeCouldBeNull.IsBeating ? " (mid-beat)" : "")); + add("Last-Heartbeat", "last-heartbeat", (lastBeat == 0 ? "never" : ((unchecked(now - lastBeat) / 1000) + "s ago")) + (bridge.IsBeating ? " (mid-beat)" : "")); var mbeat = bridge.Multiplexer.LastHeartbeatSecondsAgo; if (mbeat >= 0) { @@ -470,7 +477,7 @@ void add(string lk, string sk, string v) bridge.Trace("Failing: " + next); bridge.Multiplexer?.OnMessageFaulted(next, ex, origin); } - next.SetExceptionAndComplete(ex, bridge); + next.SetExceptionAndComplete(ex!, bridge); } } } @@ -502,7 +509,7 @@ internal enum WriteStatus /// A string that represents the current object. public override string ToString() => $"{_physicalName} ({_writeStatus})"; - internal static void IdentifyFailureType(Exception exception, ref ConnectionFailureType failureType) + internal static void IdentifyFailureType(Exception? exception, ref ConnectionFailureType failureType) { if (exception != null && failureType == ConnectionFailureType.InternalFailure) { @@ -538,7 +545,7 @@ internal void GetCounters(ConnectionCounters counters) counters.Subscriptions = SubscriptionCount; } - internal Message GetReadModeCommand(bool isPrimaryOnly) + internal Message? GetReadModeCommand(bool isPrimaryOnly) { if (BridgeCouldBeNull?.ServerEndPoint?.RequiresReadMode == true) { @@ -563,7 +570,7 @@ internal Message GetReadModeCommand(bool isPrimaryOnly) return null; } - internal Message GetSelectDatabaseCommand(int targetDatabase, Message message) + internal Message? GetSelectDatabaseCommand(int targetDatabase, Message message) { if (targetDatabase < 0 || targetDatabase == currentDatabase) { @@ -651,8 +658,9 @@ internal void OnBridgeHeartbeat() { if (_writtenAwaitingResponse.Count != 0 && BridgeCouldBeNull is PhysicalBridge bridge) { - var server = bridge?.ServerEndPoint; - var timeout = bridge.Multiplexer.AsyncTimeoutMilliseconds; + var server = bridge.ServerEndPoint; + var multiplexer = bridge.Multiplexer; + var timeout = multiplexer.AsyncTimeoutMilliseconds; foreach (var msg in _writtenAwaitingResponse) { // We only handle async timeouts here, synchronous timeouts are handled upstream. @@ -660,12 +668,12 @@ internal void OnBridgeHeartbeat() if (msg.ResultBoxIsAsync && msg.HasTimedOut(now, timeout, out var elapsed)) { bool haveDeltas = msg.TryGetPhysicalState(out _, out _, out long sentDelta, out var receivedDelta) && sentDelta >= 0 && receivedDelta >= 0; - var timeoutEx = ExceptionFactory.Timeout(bridge.Multiplexer, haveDeltas + var timeoutEx = ExceptionFactory.Timeout(multiplexer, haveDeltas ? $"Timeout awaiting response (outbound={sentDelta >> 10}KiB, inbound={receivedDelta >> 10}KiB, {elapsed}ms elapsed, timeout is {timeout}ms)" : $"Timeout awaiting response ({elapsed}ms elapsed, timeout is {timeout}ms)", msg, server); - bridge.Multiplexer?.OnMessageFaulted(msg, timeoutEx); + multiplexer.OnMessageFaulted(msg, timeoutEx); msg.SetExceptionAndComplete(timeoutEx, bridge); // tell the message that it is doomed - bridge.Multiplexer.OnAsyncTimeout(); + multiplexer.OnAsyncTimeout(); } // Note: it is important that we **do not** remove the message unless we're tearing down the socket; that // would disrupt the chain for MatchResult; we just preemptively abort the message from the caller's @@ -675,7 +683,7 @@ internal void OnBridgeHeartbeat() } } - internal void OnInternalError(Exception exception, [CallerMemberName] string origin = null) + internal void OnInternalError(Exception exception, [CallerMemberName] string? origin = null) { if (BridgeCouldBeNull is PhysicalBridge bridge) { @@ -694,26 +702,26 @@ internal void Write(in RedisKey key) var val = key.KeyValue; if (val is string s) { - WriteUnifiedPrefixedString(_ioPipe.Output, key.KeyPrefix, s); + WriteUnifiedPrefixedString(_ioPipe!.Output, key.KeyPrefix, s); } else { - WriteUnifiedPrefixedBlob(_ioPipe.Output, key.KeyPrefix, (byte[])val); + WriteUnifiedPrefixedBlob(_ioPipe!.Output, key.KeyPrefix, (byte[]?)val); } } internal void Write(in RedisChannel channel) - => WriteUnifiedPrefixedBlob(_ioPipe.Output, ChannelPrefix, channel.Value); + => WriteUnifiedPrefixedBlob(_ioPipe!.Output, ChannelPrefix, channel.Value); [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void WriteBulkString(in RedisValue value) - => WriteBulkString(value, _ioPipe.Output); + => WriteBulkString(value, _ioPipe!.Output); internal static void WriteBulkString(in RedisValue value, PipeWriter output) { switch (value.Type) { case RedisValue.StorageType.Null: - WriteUnifiedBlob(output, (byte[])null); + WriteUnifiedBlob(output, (byte[]?)null); break; case RedisValue.StorageType.Int64: WriteUnifiedInt64(output, value.OverlappedValueInt64); @@ -723,7 +731,7 @@ internal static void WriteBulkString(in RedisValue value, PipeWriter output) break; case RedisValue.StorageType.Double: // use string case RedisValue.StorageType.String: - WriteUnifiedPrefixedString(output, null, (string)value); + WriteUnifiedPrefixedString(output, null, (string?)value); break; case RedisValue.StorageType.Raw: WriteUnifiedSpan(output, ((ReadOnlyMemory)value).Span); @@ -761,7 +769,7 @@ internal void WriteHeader(RedisCommand command, int arguments, CommandBytes comm // *{argCount}\r\n = 3 + MaxInt32TextLen // ${cmd-len}\r\n = 3 + MaxInt32TextLen // {cmd}\r\n = 2 + commandBytes.Length - var span = _ioPipe.Output.GetSpan(commandBytes.Length + 8 + MaxInt32TextLen + MaxInt32TextLen); + var span = _ioPipe!.Output.GetSpan(commandBytes.Length + 8 + MaxInt32TextLen + MaxInt32TextLen); span[0] = (byte)'*'; int offset = WriteRaw(span, arguments + 1, offset: 1); @@ -914,7 +922,7 @@ private async ValueTask FlushAsync_Awaited(PhysicalConnection conne } } - CancellationTokenSource _reusableFlushSyncTokenSource; + private CancellationTokenSource? _reusableFlushSyncTokenSource; [Obsolete("this is an anti-pattern; work to reduce reliance on this is in progress")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0062:Make local function 'static'", Justification = "DEBUG uses instance data")] internal WriteResult FlushSync(bool throwOnFailure, int millisecondsTimeout) @@ -969,7 +977,7 @@ internal ValueTask FlushAsync(bool throwOnFailure, CancellationToke private static readonly ReadOnlyMemory NullBulkString = Encoding.ASCII.GetBytes("$-1\r\n"), EmptyBulkString = Encoding.ASCII.GetBytes("$0\r\n\r\n"); - private static void WriteUnifiedBlob(PipeWriter writer, byte[] value) + private static void WriteUnifiedBlob(PipeWriter writer, byte[]? value) { if (value == null) { @@ -1034,7 +1042,7 @@ private static int AppendToSpan(Span span, ReadOnlySpan value, int o internal void WriteSha1AsHex(byte[] value) { - var writer = _ioPipe.Output; + var writer = _ioPipe!.Output; if (value == null) { writer.Write(NullBulkString.Span); @@ -1074,7 +1082,7 @@ internal static byte ToHexNibble(int value) return value < 10 ? (byte)('0' + value) : (byte)('a' - 10 + value); } - internal static void WriteUnifiedPrefixedString(PipeWriter writer, byte[] prefix, string value) + internal static void WriteUnifiedPrefixedString(PipeWriter writer, byte[]? prefix, string? value) { if (value == null) { @@ -1109,7 +1117,7 @@ internal static void WriteUnifiedPrefixedString(PipeWriter writer, byte[] prefix } [ThreadStatic] - private static Encoder s_PerThreadEncoder; + private static Encoder? s_PerThreadEncoder; internal static Encoder GetPerThreadEncoder() { var encoder = s_PerThreadEncoder; @@ -1124,7 +1132,7 @@ internal static Encoder GetPerThreadEncoder() return encoder; } - unsafe static internal void WriteRaw(PipeWriter writer, string value, int expectedLength) + internal static unsafe void WriteRaw(PipeWriter writer, string value, int expectedLength) { const int MaxQuickEncodeSize = 512; @@ -1177,7 +1185,7 @@ unsafe static internal void WriteRaw(PipeWriter writer, string value, int expect } } - private static void WriteUnifiedPrefixedBlob(PipeWriter writer, byte[] prefix, byte[] value) + private static void WriteUnifiedPrefixedBlob(PipeWriter writer, byte[]? prefix, byte[]? value) { // ${total-len}\r\n // {prefix}{value}\r\n @@ -1342,7 +1350,7 @@ public ConnectionStatus GetStatus() }; } - private static RemoteCertificateValidationCallback GetAmbientIssuerCertificateCallback() + private static RemoteCertificateValidationCallback? GetAmbientIssuerCertificateCallback() { try { @@ -1355,7 +1363,7 @@ private static RemoteCertificateValidationCallback GetAmbientIssuerCertificateCa } return null; } - private static LocalCertificateSelectionCallback GetAmbientClientCertificateCallback() + private static LocalCertificateSelectionCallback? GetAmbientClientCertificateCallback() { try { @@ -1381,12 +1389,12 @@ private static LocalCertificateSelectionCallback GetAmbientClientCertificateCall return null; } - internal async ValueTask ConnectedAsync(Socket socket, LogProxy log, SocketManager manager) + internal async ValueTask ConnectedAsync(Socket socket, LogProxy? log, SocketManager manager) { var bridge = BridgeCouldBeNull; if (bridge == null) return false; - IDuplexPipe pipe = null; + IDuplexPipe? pipe = null; try { // disallow connection in some cases @@ -1402,7 +1410,10 @@ internal async ValueTask ConnectedAsync(Socket socket, LogProxy log, Socke { log?.WriteLine("Configuring TLS"); var host = config.SslHost; - if (string.IsNullOrWhiteSpace(host)) host = Format.ToStringHostOnly(bridge.ServerEndPoint.EndPoint); + if (host.IsNullOrWhiteSpace()) + { + host = Format.ToStringHostOnly(bridge.ServerEndPoint.EndPoint); + } var ssl = new SslStream(new NetworkStream(socket), false, config.CertificateValidationCallback ?? GetAmbientIssuerCertificateCallback(), @@ -1438,7 +1449,7 @@ internal async ValueTask ConnectedAsync(Socket socket, LogProxy log, Socke _ioPipe = pipe; - log?.WriteLine($"{bridge?.Name}: Connected "); + log?.WriteLine($"{bridge.Name}: Connected "); await bridge.OnConnectedAsync(this, log).ForAwait(); return true; @@ -1469,12 +1480,13 @@ private void MatchResult(in RawResult result) var configChanged = muxer.ConfigurationChangedChannel; if (configChanged != null && items[1].IsEqual(configChanged)) { - EndPoint blame = null; + EndPoint? blame = null; try { if (!items[2].IsEqual(CommonReplies.wildcard)) { - blame = Format.TryParseEndPoint(items[2].GetString()); + // We don't want to fail here, just trying to identify + _ = Format.TryParseEndPoint(items[2].GetString(), out blame); } } catch { /* no biggie */ } @@ -1511,7 +1523,7 @@ private void MatchResult(in RawResult result) // if it didn't look like "[p]message", then we still need to process the pending queue } Trace("Matching result..."); - Message msg; + Message? msg; _readStatus = ReadStatus.DequeueResult; lock (_writtenAwaitingResponse) { @@ -1541,9 +1553,9 @@ private void MatchResult(in RawResult result) _activeMessage = null; } - private volatile Message _activeMessage; + private volatile Message? _activeMessage; - internal void GetHeadMessages(out Message now, out Message next) + internal void GetHeadMessages(out Message? now, out Message? next) { now = _activeMessage; lock(_writtenAwaitingResponse) @@ -1704,7 +1716,7 @@ private int ProcessBuffer(ref ReadOnlySequence buffer) // } //} - private static RawResult ReadArray(Arena arena, in ReadOnlySequence buffer, ref BufferReader reader, bool includeDetailInExceptions, ServerEndPoint server) + private static RawResult ReadArray(Arena arena, in ReadOnlySequence buffer, ref BufferReader reader, bool includeDetailInExceptions, ServerEndPoint? server) { var itemCount = ReadLineTerminatedString(ResultType.Integer, ref reader); if (itemCount.HasValue) @@ -1755,7 +1767,7 @@ private static RawResult ReadArray(Arena arena, in ReadOnlySequence ReadFromPipe().RedisFireAndForget(); internal static RawResult TryParseResult(Arena arena, in ReadOnlySequence buffer, ref BufferReader reader, - bool includeDetilInExceptions, ServerEndPoint server, bool allowInlineProtocol = false) + bool includeDetilInExceptions, ServerEndPoint? server, bool allowInlineProtocol = false) { var prefix = reader.PeekByte(); if (prefix < 0) return RawResult.Nil; // EOF diff --git a/src/StackExchange.Redis/Profiling/IProfiledCommand.cs b/src/StackExchange.Redis/Profiling/IProfiledCommand.cs index d088d4ab5..2f9c3cb54 100644 --- a/src/StackExchange.Redis/Profiling/IProfiledCommand.cs +++ b/src/StackExchange.Redis/Profiling/IProfiledCommand.cs @@ -80,7 +80,7 @@ public interface IProfiledCommand /// /// This can only be set if redis is configured as a cluster. /// - IProfiledCommand RetransmissionOf { get; } + IProfiledCommand? RetransmissionOf { get; } /// /// If RetransmissionOf is not null, this property will be set to either Ask or Moved to indicate diff --git a/src/StackExchange.Redis/Profiling/ProfiledCommand.cs b/src/StackExchange.Redis/Profiling/ProfiledCommand.cs index 4c1d366f9..dc31c5ed2 100644 --- a/src/StackExchange.Redis/Profiling/ProfiledCommand.cs +++ b/src/StackExchange.Redis/Profiling/ProfiledCommand.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Runtime.CompilerServices; using System.Threading; @@ -13,13 +14,13 @@ internal sealed class ProfiledCommand : IProfiledCommand #region IProfiledCommand Impl public EndPoint EndPoint => Server.EndPoint; - public int Db => Message.Db; + public int Db => Message!.Db; - public string Command => Message is RedisDatabase.ExecuteMessage em ? em.Command.ToString() : Message.Command.ToString(); + public string Command => Message is RedisDatabase.ExecuteMessage em ? em.Command.ToString() : Message!.Command.ToString(); - public CommandFlags Flags => Message.Flags; + public CommandFlags Flags => Message!.Flags; - public DateTime CommandCreated => MessageCreatedDateTime; + public DateTime CommandCreated { get; private set; } public TimeSpan CreationToEnqueued => GetElapsedTime(EnqueuedTimeStamp - MessageCreatedTimeStamp); @@ -37,19 +38,17 @@ private static TimeSpan GetElapsedTime(long timestampDelta) return new TimeSpan((long)(TimestampToTicks * timestampDelta)); } - public IProfiledCommand RetransmissionOf => OriginalProfiling; + public IProfiledCommand? RetransmissionOf => OriginalProfiling; public RetransmissionReasonType? RetransmissionReason { get; } #endregion - public ProfiledCommand NextElement { get; set; } + public ProfiledCommand? NextElement { get; set; } - private Message Message; + private Message? Message; private readonly ServerEndPoint Server; - private readonly ProfiledCommand OriginalProfiling; - - private DateTime MessageCreatedDateTime; + private readonly ProfiledCommand? OriginalProfiling; private long MessageCreatedTimeStamp; private long EnqueuedTimeStamp; private long RequestSentTimeStamp; @@ -59,7 +58,7 @@ private static TimeSpan GetElapsedTime(long timestampDelta) private readonly ProfilingSession PushToWhenFinished; - private ProfiledCommand(ProfilingSession pushTo, ServerEndPoint server, ProfiledCommand resentFor, RetransmissionReasonType? reason) + private ProfiledCommand(ProfilingSession pushTo, ServerEndPoint server, ProfiledCommand? resentFor, RetransmissionReasonType? reason) { PushToWhenFinished = pushTo; OriginalProfiling = resentFor; @@ -77,13 +76,17 @@ public static ProfiledCommand NewAttachedToSameContext(ProfiledCommand resentFor return new ProfiledCommand(resentFor.PushToWhenFinished, server, resentFor, isMoved ? RetransmissionReasonType.Moved : RetransmissionReasonType.Ask); } + [MemberNotNull(nameof(Message))] public void SetMessage(Message msg) { // This method should never be called twice - if (Message != null) throw new InvalidOperationException($"{nameof(SetMessage)} called more than once"); + if (Message is not null) + { + throw new InvalidOperationException($"{nameof(SetMessage)} called more than once"); + } Message = msg; - MessageCreatedDateTime = msg.CreatedDateTime; + CommandCreated = msg.CreatedDateTime; MessageCreatedTimeStamp = msg.CreatedTimestamp; } diff --git a/src/StackExchange.Redis/Profiling/ProfiledCommandEnumerable.cs b/src/StackExchange.Redis/Profiling/ProfiledCommandEnumerable.cs index 9ae3cd0d4..e5838f3ca 100644 --- a/src/StackExchange.Redis/Profiling/ProfiledCommandEnumerable.cs +++ b/src/StackExchange.Redis/Profiling/ProfiledCommandEnumerable.cs @@ -26,12 +26,12 @@ namespace StackExchange.Redis.Profiling /// public struct Enumerator : IEnumerator { - private ProfiledCommand Head, CurrentBacker; + private ProfiledCommand? Head, CurrentBacker; private bool IsEmpty => Head == null; private bool IsUnstartedOrFinished => CurrentBacker == null; - internal Enumerator(ProfiledCommand head) + internal Enumerator(ProfiledCommand? head) { Head = head; CurrentBacker = null; @@ -40,9 +40,9 @@ internal Enumerator(ProfiledCommand head) /// /// The current element. /// - public IProfiledCommand Current => CurrentBacker; + public IProfiledCommand Current => CurrentBacker!; - object System.Collections.IEnumerator.Current => CurrentBacker; + object System.Collections.IEnumerator.Current => CurrentBacker!; /// /// Advances the enumeration, returning true if there is a new element to consume and false @@ -58,7 +58,7 @@ public bool MoveNext() } else { - CurrentBacker = CurrentBacker.NextElement; + CurrentBacker = CurrentBacker!.NextElement; } return CurrentBacker != null; @@ -76,7 +76,7 @@ public bool MoveNext() public void Dispose() => CurrentBacker = Head = null; } - private readonly ProfiledCommand _head; + private readonly ProfiledCommand? _head; private readonly int _count; /// /// Returns the number of commands captured in this snapshot @@ -96,8 +96,8 @@ public int Count(Func predicate) var cur = _head; for (int i = 0; i < _count; i++) { - if (predicate(cur)) result++; - cur = cur.NextElement; + if (cur != null && predicate(cur)) result++; + cur = cur!.NextElement; } return result; } @@ -110,11 +110,11 @@ public IProfiledCommand[] ToArray() if (_count == 0) return Array.Empty(); var arr = new IProfiledCommand[_count]; - var cur = _head; - for(int i = 0; i < _count; i++) + ProfiledCommand? cur = _head; + for (int i = 0; i < _count; i++) { - arr[i] = cur; - cur = cur.NextElement; + arr[i] = cur!; + cur = cur!.NextElement; } return arr; } @@ -125,7 +125,7 @@ public IProfiledCommand[] ToArray() public List ToList() { // exploit the fact that we know the length var list = new List(_count); - var cur = _head; + ProfiledCommand? cur = _head; while (cur != null) { list.Add(cur); @@ -133,7 +133,7 @@ public List ToList() } return list; } - internal ProfiledCommandEnumerable(int count, ProfiledCommand head) + internal ProfiledCommandEnumerable(int count, ProfiledCommand? head) { _count = count; _head = head; diff --git a/src/StackExchange.Redis/Profiling/ProfilingSession.cs b/src/StackExchange.Redis/Profiling/ProfilingSession.cs index fd6b67def..83f1969bd 100644 --- a/src/StackExchange.Redis/Profiling/ProfilingSession.cs +++ b/src/StackExchange.Redis/Profiling/ProfilingSession.cs @@ -10,23 +10,23 @@ public sealed class ProfilingSession /// /// Caller-defined state object. /// - public object UserToken { get; } + public object? UserToken { get; } /// /// Create a new profiling session, optionally including a caller-defined state object. /// /// The state object to use for this session. - public ProfilingSession(object userToken = null) => UserToken = userToken; + public ProfilingSession(object? userToken = null) => UserToken = userToken; - private object _untypedHead; + private object? _untypedHead; internal void Add(ProfiledCommand command) { if (command == null) return; - object cur = Thread.VolatileRead(ref _untypedHead); + object? cur = Thread.VolatileRead(ref _untypedHead); while (true) { - command.NextElement = (ProfiledCommand)cur; + command.NextElement = (ProfiledCommand?)cur; var got = Interlocked.CompareExchange(ref _untypedHead, command, cur); if (ReferenceEquals(got, cur)) break; // successful update cur = got; // retry; no need to re-fetch the field, we just did that @@ -39,12 +39,12 @@ internal void Add(ProfiledCommand command) /// public ProfiledCommandEnumerable FinishProfiling() { - var head = (ProfiledCommand)Interlocked.Exchange(ref _untypedHead, null); + var head = (ProfiledCommand?)Interlocked.Exchange(ref _untypedHead, null); // reverse the list so everything is ordered the way the consumer expected them int count = 0; - ProfiledCommand previous = null, current = head, next; - while(current != null) + ProfiledCommand? previous = null, current = head, next; + while (current != null) { next = current.NextElement; current.NextElement = previous; diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 43e27c17a..991430e81 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -1,60 +1,61 @@ -abstract StackExchange.Redis.RedisResult.IsNull.get -> bool +#nullable enable +abstract StackExchange.Redis.RedisResult.IsNull.get -> bool abstract StackExchange.Redis.RedisResult.Type.get -> StackExchange.Redis.ResultType -override StackExchange.Redis.ChannelMessage.Equals(object obj) -> bool +override StackExchange.Redis.ChannelMessage.Equals(object? obj) -> bool override StackExchange.Redis.ChannelMessage.GetHashCode() -> int -override StackExchange.Redis.ChannelMessage.ToString() -> string -override StackExchange.Redis.ChannelMessageQueue.ToString() -> string -override StackExchange.Redis.ClientInfo.ToString() -> string -override StackExchange.Redis.ClusterNode.Equals(object obj) -> bool +override StackExchange.Redis.ChannelMessage.ToString() -> string! +override StackExchange.Redis.ChannelMessageQueue.ToString() -> string? +override StackExchange.Redis.ClientInfo.ToString() -> string! +override StackExchange.Redis.ClusterNode.Equals(object? obj) -> bool override StackExchange.Redis.ClusterNode.GetHashCode() -> int -override StackExchange.Redis.ClusterNode.ToString() -> string -override StackExchange.Redis.CommandMap.ToString() -> string +override StackExchange.Redis.ClusterNode.ToString() -> string! +override StackExchange.Redis.CommandMap.ToString() -> string! override StackExchange.Redis.Configuration.AzureOptionsProvider.AbortOnConnectFail.get -> bool -override StackExchange.Redis.Configuration.AzureOptionsProvider.AfterConnectAsync(StackExchange.Redis.ConnectionMultiplexer muxer, System.Action log) -> System.Threading.Tasks.Task -override StackExchange.Redis.Configuration.AzureOptionsProvider.DefaultVersion.get -> System.Version -override StackExchange.Redis.Configuration.AzureOptionsProvider.GetDefaultSsl(StackExchange.Redis.EndPointCollection endPoints) -> bool -override StackExchange.Redis.Configuration.AzureOptionsProvider.IsMatch(System.Net.EndPoint endpoint) -> bool -override StackExchange.Redis.ConfigurationOptions.ToString() -> string -override StackExchange.Redis.ConnectionCounters.ToString() -> string -override StackExchange.Redis.ConnectionFailedEventArgs.ToString() -> string -override StackExchange.Redis.ConnectionMultiplexer.ToString() -> string -override StackExchange.Redis.GeoEntry.Equals(object obj) -> bool +override StackExchange.Redis.Configuration.AzureOptionsProvider.AfterConnectAsync(StackExchange.Redis.ConnectionMultiplexer! muxer, System.Action! log) -> System.Threading.Tasks.Task! +override StackExchange.Redis.Configuration.AzureOptionsProvider.DefaultVersion.get -> System.Version! +override StackExchange.Redis.Configuration.AzureOptionsProvider.GetDefaultSsl(StackExchange.Redis.EndPointCollection! endPoints) -> bool +override StackExchange.Redis.Configuration.AzureOptionsProvider.IsMatch(System.Net.EndPoint! endpoint) -> bool +override StackExchange.Redis.ConfigurationOptions.ToString() -> string! +override StackExchange.Redis.ConnectionCounters.ToString() -> string! +override StackExchange.Redis.ConnectionFailedEventArgs.ToString() -> string! +override StackExchange.Redis.ConnectionMultiplexer.ToString() -> string! +override StackExchange.Redis.GeoEntry.Equals(object? obj) -> bool override StackExchange.Redis.GeoEntry.GetHashCode() -> int -override StackExchange.Redis.GeoEntry.ToString() -> string -override StackExchange.Redis.GeoPosition.Equals(object obj) -> bool +override StackExchange.Redis.GeoEntry.ToString() -> string! +override StackExchange.Redis.GeoPosition.Equals(object? obj) -> bool override StackExchange.Redis.GeoPosition.GetHashCode() -> int -override StackExchange.Redis.GeoPosition.ToString() -> string -override StackExchange.Redis.GeoRadiusResult.ToString() -> string -override StackExchange.Redis.HashEntry.Equals(object obj) -> bool +override StackExchange.Redis.GeoPosition.ToString() -> string! +override StackExchange.Redis.GeoRadiusResult.ToString() -> string! +override StackExchange.Redis.HashEntry.Equals(object? obj) -> bool override StackExchange.Redis.HashEntry.GetHashCode() -> int -override StackExchange.Redis.HashEntry.ToString() -> string -override StackExchange.Redis.Maintenance.ServerMaintenanceEvent.ToString() -> string -override StackExchange.Redis.NameValueEntry.Equals(object obj) -> bool +override StackExchange.Redis.HashEntry.ToString() -> string! +override StackExchange.Redis.Maintenance.ServerMaintenanceEvent.ToString() -> string? +override StackExchange.Redis.NameValueEntry.Equals(object? obj) -> bool override StackExchange.Redis.NameValueEntry.GetHashCode() -> int -override StackExchange.Redis.NameValueEntry.ToString() -> string -override StackExchange.Redis.RedisChannel.Equals(object obj) -> bool +override StackExchange.Redis.NameValueEntry.ToString() -> string! +override StackExchange.Redis.RedisChannel.Equals(object? obj) -> bool override StackExchange.Redis.RedisChannel.GetHashCode() -> int -override StackExchange.Redis.RedisChannel.ToString() -> string -override StackExchange.Redis.RedisConnectionException.GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) -> void -override StackExchange.Redis.RedisFeatures.Equals(object obj) -> bool +override StackExchange.Redis.RedisChannel.ToString() -> string! +override StackExchange.Redis.RedisConnectionException.GetObjectData(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext context) -> void +override StackExchange.Redis.RedisFeatures.Equals(object? obj) -> bool override StackExchange.Redis.RedisFeatures.GetHashCode() -> int -override StackExchange.Redis.RedisFeatures.ToString() -> string -override StackExchange.Redis.RedisKey.Equals(object obj) -> bool +override StackExchange.Redis.RedisFeatures.ToString() -> string! +override StackExchange.Redis.RedisKey.Equals(object? obj) -> bool override StackExchange.Redis.RedisKey.GetHashCode() -> int -override StackExchange.Redis.RedisKey.ToString() -> string -override StackExchange.Redis.RedisTimeoutException.GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) -> void -override StackExchange.Redis.RedisValue.Equals(object obj) -> bool +override StackExchange.Redis.RedisKey.ToString() -> string! +override StackExchange.Redis.RedisTimeoutException.GetObjectData(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext context) -> void +override StackExchange.Redis.RedisValue.Equals(object? obj) -> bool override StackExchange.Redis.RedisValue.GetHashCode() -> int -override StackExchange.Redis.RedisValue.ToString() -> string -override StackExchange.Redis.Role.ToString() -> string -override StackExchange.Redis.ServerCounters.ToString() -> string -override StackExchange.Redis.SlotRange.Equals(object obj) -> bool +override StackExchange.Redis.RedisValue.ToString() -> string! +override StackExchange.Redis.Role.ToString() -> string! +override StackExchange.Redis.ServerCounters.ToString() -> string! +override StackExchange.Redis.SlotRange.Equals(object? obj) -> bool override StackExchange.Redis.SlotRange.GetHashCode() -> int -override StackExchange.Redis.SlotRange.ToString() -> string -override StackExchange.Redis.SocketManager.ToString() -> string -override StackExchange.Redis.SortedSetEntry.Equals(object obj) -> bool +override StackExchange.Redis.SlotRange.ToString() -> string! +override StackExchange.Redis.SocketManager.ToString() -> string! +override StackExchange.Redis.SortedSetEntry.Equals(object? obj) -> bool override StackExchange.Redis.SortedSetEntry.GetHashCode() -> int -override StackExchange.Redis.SortedSetEntry.ToString() -> string +override StackExchange.Redis.SortedSetEntry.ToString() -> string! StackExchange.Redis.Aggregate StackExchange.Redis.Aggregate.Max = 2 -> StackExchange.Redis.Aggregate StackExchange.Redis.Aggregate.Min = 1 -> StackExchange.Redis.Aggregate @@ -77,14 +78,14 @@ StackExchange.Redis.ChannelMessage.Message.get -> StackExchange.Redis.RedisValue StackExchange.Redis.ChannelMessage.SubscriptionChannel.get -> StackExchange.Redis.RedisChannel StackExchange.Redis.ChannelMessageQueue StackExchange.Redis.ChannelMessageQueue.Channel.get -> StackExchange.Redis.RedisChannel -StackExchange.Redis.ChannelMessageQueue.Completion.get -> System.Threading.Tasks.Task -StackExchange.Redis.ChannelMessageQueue.OnMessage(System.Action handler) -> void -StackExchange.Redis.ChannelMessageQueue.OnMessage(System.Func handler) -> void +StackExchange.Redis.ChannelMessageQueue.Completion.get -> System.Threading.Tasks.Task! +StackExchange.Redis.ChannelMessageQueue.OnMessage(System.Action! handler) -> void +StackExchange.Redis.ChannelMessageQueue.OnMessage(System.Func! handler) -> void StackExchange.Redis.ChannelMessageQueue.ReadAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask StackExchange.Redis.ChannelMessageQueue.TryGetCount(out int count) -> bool StackExchange.Redis.ChannelMessageQueue.TryRead(out StackExchange.Redis.ChannelMessage item) -> bool StackExchange.Redis.ChannelMessageQueue.Unsubscribe(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.ChannelMessageQueue.UnsubscribeAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.ChannelMessageQueue.UnsubscribeAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.ClientFlags StackExchange.Redis.ClientFlags.Blocked = 16 -> StackExchange.Redis.ClientFlags StackExchange.Redis.ClientFlags.BroadcastTracking = 16384 -> StackExchange.Redis.ClientFlags @@ -105,21 +106,21 @@ StackExchange.Redis.ClientFlags.TransactionDoomed = 32 -> StackExchange.Redis.Cl StackExchange.Redis.ClientFlags.Unblocked = 128 -> StackExchange.Redis.ClientFlags StackExchange.Redis.ClientFlags.UnixDomainSocket = 2048 -> StackExchange.Redis.ClientFlags StackExchange.Redis.ClientInfo -StackExchange.Redis.ClientInfo.Address.get -> System.Net.EndPoint +StackExchange.Redis.ClientInfo.Address.get -> System.Net.EndPoint? StackExchange.Redis.ClientInfo.AgeSeconds.get -> int StackExchange.Redis.ClientInfo.ClientInfo() -> void StackExchange.Redis.ClientInfo.ClientType.get -> StackExchange.Redis.ClientType StackExchange.Redis.ClientInfo.Database.get -> int StackExchange.Redis.ClientInfo.Flags.get -> StackExchange.Redis.ClientFlags -StackExchange.Redis.ClientInfo.FlagsRaw.get -> string -StackExchange.Redis.ClientInfo.Host.get -> string +StackExchange.Redis.ClientInfo.FlagsRaw.get -> string? +StackExchange.Redis.ClientInfo.Host.get -> string? StackExchange.Redis.ClientInfo.Id.get -> long StackExchange.Redis.ClientInfo.IdleSeconds.get -> int -StackExchange.Redis.ClientInfo.LastCommand.get -> string -StackExchange.Redis.ClientInfo.Name.get -> string +StackExchange.Redis.ClientInfo.LastCommand.get -> string? +StackExchange.Redis.ClientInfo.Name.get -> string? StackExchange.Redis.ClientInfo.PatternSubscriptionCount.get -> int StackExchange.Redis.ClientInfo.Port.get -> int -StackExchange.Redis.ClientInfo.Raw.get -> string +StackExchange.Redis.ClientInfo.Raw.get -> string? StackExchange.Redis.ClientInfo.SubscriptionCount.get -> int StackExchange.Redis.ClientInfo.TransactionCommandLength.get -> int StackExchange.Redis.ClientType @@ -128,26 +129,26 @@ StackExchange.Redis.ClientType.PubSub = 2 -> StackExchange.Redis.ClientType StackExchange.Redis.ClientType.Replica = 1 -> StackExchange.Redis.ClientType StackExchange.Redis.ClientType.Slave = 1 -> StackExchange.Redis.ClientType StackExchange.Redis.ClusterConfiguration -StackExchange.Redis.ClusterConfiguration.GetBySlot(int slot) -> StackExchange.Redis.ClusterNode -StackExchange.Redis.ClusterConfiguration.GetBySlot(StackExchange.Redis.RedisKey key) -> StackExchange.Redis.ClusterNode -StackExchange.Redis.ClusterConfiguration.Nodes.get -> System.Collections.Generic.ICollection -StackExchange.Redis.ClusterConfiguration.Origin.get -> System.Net.EndPoint -StackExchange.Redis.ClusterConfiguration.this[System.Net.EndPoint endpoint].get -> StackExchange.Redis.ClusterNode +StackExchange.Redis.ClusterConfiguration.GetBySlot(int slot) -> StackExchange.Redis.ClusterNode? +StackExchange.Redis.ClusterConfiguration.GetBySlot(StackExchange.Redis.RedisKey key) -> StackExchange.Redis.ClusterNode? +StackExchange.Redis.ClusterConfiguration.Nodes.get -> System.Collections.Generic.ICollection! +StackExchange.Redis.ClusterConfiguration.Origin.get -> System.Net.EndPoint! +StackExchange.Redis.ClusterConfiguration.this[System.Net.EndPoint! endpoint].get -> StackExchange.Redis.ClusterNode? StackExchange.Redis.ClusterNode -StackExchange.Redis.ClusterNode.Children.get -> System.Collections.Generic.IList -StackExchange.Redis.ClusterNode.CompareTo(StackExchange.Redis.ClusterNode other) -> int -StackExchange.Redis.ClusterNode.EndPoint.get -> System.Net.EndPoint -StackExchange.Redis.ClusterNode.Equals(StackExchange.Redis.ClusterNode other) -> bool +StackExchange.Redis.ClusterNode.Children.get -> System.Collections.Generic.IList! +StackExchange.Redis.ClusterNode.CompareTo(StackExchange.Redis.ClusterNode? other) -> int +StackExchange.Redis.ClusterNode.EndPoint.get -> System.Net.EndPoint? +StackExchange.Redis.ClusterNode.Equals(StackExchange.Redis.ClusterNode? other) -> bool StackExchange.Redis.ClusterNode.IsConnected.get -> bool StackExchange.Redis.ClusterNode.IsMyself.get -> bool StackExchange.Redis.ClusterNode.IsNoAddr.get -> bool StackExchange.Redis.ClusterNode.IsReplica.get -> bool StackExchange.Redis.ClusterNode.IsSlave.get -> bool -StackExchange.Redis.ClusterNode.NodeId.get -> string -StackExchange.Redis.ClusterNode.Parent.get -> StackExchange.Redis.ClusterNode -StackExchange.Redis.ClusterNode.ParentNodeId.get -> string -StackExchange.Redis.ClusterNode.Raw.get -> string -StackExchange.Redis.ClusterNode.Slots.get -> System.Collections.Generic.IList +StackExchange.Redis.ClusterNode.NodeId.get -> string! +StackExchange.Redis.ClusterNode.Parent.get -> StackExchange.Redis.ClusterNode? +StackExchange.Redis.ClusterNode.ParentNodeId.get -> string? +StackExchange.Redis.ClusterNode.Raw.get -> string! +StackExchange.Redis.ClusterNode.Slots.get -> System.Collections.Generic.IList! StackExchange.Redis.CommandFlags StackExchange.Redis.CommandFlags.DemandMaster = 4 -> StackExchange.Redis.CommandFlags StackExchange.Redis.CommandFlags.DemandReplica = StackExchange.Redis.CommandFlags.DemandMaster | StackExchange.Redis.CommandFlags.PreferReplica -> StackExchange.Redis.CommandFlags @@ -166,9 +167,9 @@ StackExchange.Redis.CommandStatus.Sent = 2 -> StackExchange.Redis.CommandStatus StackExchange.Redis.CommandStatus.Unknown = 0 -> StackExchange.Redis.CommandStatus StackExchange.Redis.CommandStatus.WaitingToBeSent = 1 -> StackExchange.Redis.CommandStatus StackExchange.Redis.CommandTrace -StackExchange.Redis.CommandTrace.Arguments.get -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.CommandTrace.Arguments.get -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.CommandTrace.Duration.get -> System.TimeSpan -StackExchange.Redis.CommandTrace.GetHelpUrl() -> string +StackExchange.Redis.CommandTrace.GetHelpUrl() -> string? StackExchange.Redis.CommandTrace.Time.get -> System.DateTime StackExchange.Redis.CommandTrace.UniqueId.get -> long StackExchange.Redis.Condition @@ -177,34 +178,34 @@ StackExchange.Redis.ConditionResult.WasSatisfied.get -> bool StackExchange.Redis.Configuration.AzureOptionsProvider StackExchange.Redis.Configuration.AzureOptionsProvider.AzureOptionsProvider() -> void StackExchange.Redis.Configuration.DefaultOptionsProvider -StackExchange.Redis.Configuration.DefaultOptionsProvider.ClientName.get -> string +StackExchange.Redis.Configuration.DefaultOptionsProvider.ClientName.get -> string! StackExchange.Redis.Configuration.DefaultOptionsProvider.DefaultOptionsProvider() -> void StackExchange.Redis.ConfigurationOptions StackExchange.Redis.ConfigurationOptions.AbortOnConnectFail.get -> bool StackExchange.Redis.ConfigurationOptions.AbortOnConnectFail.set -> void StackExchange.Redis.ConfigurationOptions.AllowAdmin.get -> bool StackExchange.Redis.ConfigurationOptions.AllowAdmin.set -> void -StackExchange.Redis.ConfigurationOptions.Apply(System.Action configure) -> StackExchange.Redis.ConfigurationOptions +StackExchange.Redis.ConfigurationOptions.Apply(System.Action! configure) -> StackExchange.Redis.ConfigurationOptions! StackExchange.Redis.ConfigurationOptions.AsyncTimeout.get -> int StackExchange.Redis.ConfigurationOptions.AsyncTimeout.set -> void -StackExchange.Redis.ConfigurationOptions.BacklogPolicy.get -> StackExchange.Redis.BacklogPolicy +StackExchange.Redis.ConfigurationOptions.BacklogPolicy.get -> StackExchange.Redis.BacklogPolicy! StackExchange.Redis.ConfigurationOptions.BacklogPolicy.set -> void -StackExchange.Redis.ConfigurationOptions.BeforeSocketConnect.get -> System.Action +StackExchange.Redis.ConfigurationOptions.BeforeSocketConnect.get -> System.Action? StackExchange.Redis.ConfigurationOptions.BeforeSocketConnect.set -> void -StackExchange.Redis.ConfigurationOptions.CertificateSelection -> System.Net.Security.LocalCertificateSelectionCallback -StackExchange.Redis.ConfigurationOptions.CertificateValidation -> System.Net.Security.RemoteCertificateValidationCallback +StackExchange.Redis.ConfigurationOptions.CertificateSelection -> System.Net.Security.LocalCertificateSelectionCallback? +StackExchange.Redis.ConfigurationOptions.CertificateValidation -> System.Net.Security.RemoteCertificateValidationCallback? StackExchange.Redis.ConfigurationOptions.ChannelPrefix.get -> StackExchange.Redis.RedisChannel StackExchange.Redis.ConfigurationOptions.ChannelPrefix.set -> void StackExchange.Redis.ConfigurationOptions.CheckCertificateRevocation.get -> bool StackExchange.Redis.ConfigurationOptions.CheckCertificateRevocation.set -> void -StackExchange.Redis.ConfigurationOptions.ClientName.get -> string +StackExchange.Redis.ConfigurationOptions.ClientName.get -> string? StackExchange.Redis.ConfigurationOptions.ClientName.set -> void -StackExchange.Redis.ConfigurationOptions.Clone() -> StackExchange.Redis.ConfigurationOptions -StackExchange.Redis.ConfigurationOptions.CommandMap.get -> StackExchange.Redis.CommandMap +StackExchange.Redis.ConfigurationOptions.Clone() -> StackExchange.Redis.ConfigurationOptions! +StackExchange.Redis.ConfigurationOptions.CommandMap.get -> StackExchange.Redis.CommandMap! StackExchange.Redis.ConfigurationOptions.CommandMap.set -> void StackExchange.Redis.ConfigurationOptions.ConfigCheckSeconds.get -> int StackExchange.Redis.ConfigurationOptions.ConfigCheckSeconds.set -> void -StackExchange.Redis.ConfigurationOptions.ConfigurationChannel.get -> string +StackExchange.Redis.ConfigurationOptions.ConfigurationChannel.get -> string! StackExchange.Redis.ConfigurationOptions.ConfigurationChannel.set -> void StackExchange.Redis.ConfigurationOptions.ConfigurationOptions() -> void StackExchange.Redis.ConfigurationOptions.ConnectRetry.get -> int @@ -213,11 +214,11 @@ StackExchange.Redis.ConfigurationOptions.ConnectTimeout.get -> int StackExchange.Redis.ConfigurationOptions.ConnectTimeout.set -> void StackExchange.Redis.ConfigurationOptions.DefaultDatabase.get -> int? StackExchange.Redis.ConfigurationOptions.DefaultDatabase.set -> void -StackExchange.Redis.ConfigurationOptions.Defaults.get -> StackExchange.Redis.Configuration.DefaultOptionsProvider +StackExchange.Redis.ConfigurationOptions.Defaults.get -> StackExchange.Redis.Configuration.DefaultOptionsProvider! StackExchange.Redis.ConfigurationOptions.Defaults.set -> void -StackExchange.Redis.ConfigurationOptions.DefaultVersion.get -> System.Version +StackExchange.Redis.ConfigurationOptions.DefaultVersion.get -> System.Version! StackExchange.Redis.ConfigurationOptions.DefaultVersion.set -> void -StackExchange.Redis.ConfigurationOptions.EndPoints.get -> StackExchange.Redis.EndPointCollection +StackExchange.Redis.ConfigurationOptions.EndPoints.get -> StackExchange.Redis.EndPointCollection! StackExchange.Redis.ConfigurationOptions.EndPoints.init -> void StackExchange.Redis.ConfigurationOptions.HighPrioritySocketThreads.get -> bool StackExchange.Redis.ConfigurationOptions.HighPrioritySocketThreads.set -> void @@ -227,37 +228,37 @@ StackExchange.Redis.ConfigurationOptions.IncludePerformanceCountersInExceptions. StackExchange.Redis.ConfigurationOptions.IncludePerformanceCountersInExceptions.set -> void StackExchange.Redis.ConfigurationOptions.KeepAlive.get -> int StackExchange.Redis.ConfigurationOptions.KeepAlive.set -> void -StackExchange.Redis.ConfigurationOptions.Password.get -> string +StackExchange.Redis.ConfigurationOptions.Password.get -> string? StackExchange.Redis.ConfigurationOptions.Password.set -> void StackExchange.Redis.ConfigurationOptions.PreserveAsyncOrder.get -> bool StackExchange.Redis.ConfigurationOptions.PreserveAsyncOrder.set -> void StackExchange.Redis.ConfigurationOptions.Proxy.get -> StackExchange.Redis.Proxy StackExchange.Redis.ConfigurationOptions.Proxy.set -> void -StackExchange.Redis.ConfigurationOptions.ReconnectRetryPolicy.get -> StackExchange.Redis.IReconnectRetryPolicy +StackExchange.Redis.ConfigurationOptions.ReconnectRetryPolicy.get -> StackExchange.Redis.IReconnectRetryPolicy! StackExchange.Redis.ConfigurationOptions.ReconnectRetryPolicy.set -> void StackExchange.Redis.ConfigurationOptions.ResolveDns.get -> bool StackExchange.Redis.ConfigurationOptions.ResolveDns.set -> void StackExchange.Redis.ConfigurationOptions.ResponseTimeout.get -> int StackExchange.Redis.ConfigurationOptions.ResponseTimeout.set -> void -StackExchange.Redis.ConfigurationOptions.ServiceName.get -> string +StackExchange.Redis.ConfigurationOptions.ServiceName.get -> string? StackExchange.Redis.ConfigurationOptions.ServiceName.set -> void StackExchange.Redis.ConfigurationOptions.SetDefaultPorts() -> void -StackExchange.Redis.ConfigurationOptions.SocketManager.get -> StackExchange.Redis.SocketManager +StackExchange.Redis.ConfigurationOptions.SocketManager.get -> StackExchange.Redis.SocketManager? StackExchange.Redis.ConfigurationOptions.SocketManager.set -> void StackExchange.Redis.ConfigurationOptions.Ssl.get -> bool StackExchange.Redis.ConfigurationOptions.Ssl.set -> void -StackExchange.Redis.ConfigurationOptions.SslHost.get -> string +StackExchange.Redis.ConfigurationOptions.SslHost.get -> string? StackExchange.Redis.ConfigurationOptions.SslHost.set -> void StackExchange.Redis.ConfigurationOptions.SslProtocols.get -> System.Security.Authentication.SslProtocols? StackExchange.Redis.ConfigurationOptions.SslProtocols.set -> void StackExchange.Redis.ConfigurationOptions.SyncTimeout.get -> int StackExchange.Redis.ConfigurationOptions.SyncTimeout.set -> void -StackExchange.Redis.ConfigurationOptions.TieBreaker.get -> string +StackExchange.Redis.ConfigurationOptions.TieBreaker.get -> string! StackExchange.Redis.ConfigurationOptions.TieBreaker.set -> void -StackExchange.Redis.ConfigurationOptions.ToString(bool includePassword) -> string -StackExchange.Redis.ConfigurationOptions.TrustIssuer(string issuerCertificatePath) -> void -StackExchange.Redis.ConfigurationOptions.TrustIssuer(System.Security.Cryptography.X509Certificates.X509Certificate2 issuer) -> void -StackExchange.Redis.ConfigurationOptions.User.get -> string +StackExchange.Redis.ConfigurationOptions.ToString(bool includePassword) -> string! +StackExchange.Redis.ConfigurationOptions.TrustIssuer(string! issuerCertificatePath) -> void +StackExchange.Redis.ConfigurationOptions.TrustIssuer(System.Security.Cryptography.X509Certificates.X509Certificate2! issuer) -> void +StackExchange.Redis.ConfigurationOptions.User.get -> string? StackExchange.Redis.ConfigurationOptions.User.set -> void StackExchange.Redis.ConfigurationOptions.UseSsl.get -> bool StackExchange.Redis.ConfigurationOptions.UseSsl.set -> void @@ -279,10 +280,10 @@ StackExchange.Redis.ConnectionCounters.Subscriptions.get -> long StackExchange.Redis.ConnectionCounters.TotalOutstanding.get -> int StackExchange.Redis.ConnectionCounters.WriterCount.get -> int StackExchange.Redis.ConnectionFailedEventArgs -StackExchange.Redis.ConnectionFailedEventArgs.ConnectionFailedEventArgs(object sender, System.Net.EndPoint endPoint, StackExchange.Redis.ConnectionType connectionType, StackExchange.Redis.ConnectionFailureType failureType, System.Exception exception, string physicalName) -> void +StackExchange.Redis.ConnectionFailedEventArgs.ConnectionFailedEventArgs(object! sender, System.Net.EndPoint! endPoint, StackExchange.Redis.ConnectionType connectionType, StackExchange.Redis.ConnectionFailureType failureType, System.Exception! exception, string! physicalName) -> void StackExchange.Redis.ConnectionFailedEventArgs.ConnectionType.get -> StackExchange.Redis.ConnectionType -StackExchange.Redis.ConnectionFailedEventArgs.EndPoint.get -> System.Net.EndPoint -StackExchange.Redis.ConnectionFailedEventArgs.Exception.get -> System.Exception +StackExchange.Redis.ConnectionFailedEventArgs.EndPoint.get -> System.Net.EndPoint? +StackExchange.Redis.ConnectionFailedEventArgs.Exception.get -> System.Exception? StackExchange.Redis.ConnectionFailedEventArgs.FailureType.get -> StackExchange.Redis.ConnectionFailureType StackExchange.Redis.ConnectionFailureType StackExchange.Redis.ConnectionFailureType.AuthenticationFailure = 3 -> StackExchange.Redis.ConnectionFailureType @@ -296,71 +297,71 @@ StackExchange.Redis.ConnectionFailureType.SocketFailure = 2 -> StackExchange.Red StackExchange.Redis.ConnectionFailureType.UnableToConnect = 9 -> StackExchange.Redis.ConnectionFailureType StackExchange.Redis.ConnectionFailureType.UnableToResolvePhysicalConnection = 1 -> StackExchange.Redis.ConnectionFailureType StackExchange.Redis.ConnectionMultiplexer -StackExchange.Redis.ConnectionMultiplexer.ClientName.get -> string +StackExchange.Redis.ConnectionMultiplexer.ClientName.get -> string! StackExchange.Redis.ConnectionMultiplexer.Close(bool allowCommandsToComplete = true) -> void -StackExchange.Redis.ConnectionMultiplexer.CloseAsync(bool allowCommandsToComplete = true) -> System.Threading.Tasks.Task -StackExchange.Redis.ConnectionMultiplexer.Configuration.get -> string -StackExchange.Redis.ConnectionMultiplexer.ConfigurationChanged -> System.EventHandler -StackExchange.Redis.ConnectionMultiplexer.ConfigurationChangedBroadcast -> System.EventHandler -StackExchange.Redis.ConnectionMultiplexer.Configure(System.IO.TextWriter log = null) -> bool -StackExchange.Redis.ConnectionMultiplexer.ConfigureAsync(System.IO.TextWriter log = null) -> System.Threading.Tasks.Task -StackExchange.Redis.ConnectionMultiplexer.ConnectionFailed -> System.EventHandler -StackExchange.Redis.ConnectionMultiplexer.ConnectionRestored -> System.EventHandler +StackExchange.Redis.ConnectionMultiplexer.CloseAsync(bool allowCommandsToComplete = true) -> System.Threading.Tasks.Task! +StackExchange.Redis.ConnectionMultiplexer.Configuration.get -> string! +StackExchange.Redis.ConnectionMultiplexer.ConfigurationChanged -> System.EventHandler? +StackExchange.Redis.ConnectionMultiplexer.ConfigurationChangedBroadcast -> System.EventHandler? +StackExchange.Redis.ConnectionMultiplexer.Configure(System.IO.TextWriter? log = null) -> bool +StackExchange.Redis.ConnectionMultiplexer.ConfigureAsync(System.IO.TextWriter? log = null) -> System.Threading.Tasks.Task! +StackExchange.Redis.ConnectionMultiplexer.ConnectionFailed -> System.EventHandler? +StackExchange.Redis.ConnectionMultiplexer.ConnectionRestored -> System.EventHandler? StackExchange.Redis.ConnectionMultiplexer.Dispose() -> void -StackExchange.Redis.ConnectionMultiplexer.ErrorMessage -> System.EventHandler -StackExchange.Redis.ConnectionMultiplexer.ExportConfiguration(System.IO.Stream destination, StackExchange.Redis.ExportOptions options = (StackExchange.Redis.ExportOptions)-1) -> void -StackExchange.Redis.ConnectionMultiplexer.GetCounters() -> StackExchange.Redis.ServerCounters -StackExchange.Redis.ConnectionMultiplexer.GetDatabase(int db = -1, object asyncState = null) -> StackExchange.Redis.IDatabase -StackExchange.Redis.ConnectionMultiplexer.GetEndPoints(bool configuredOnly = false) -> System.Net.EndPoint[] +StackExchange.Redis.ConnectionMultiplexer.ErrorMessage -> System.EventHandler? +StackExchange.Redis.ConnectionMultiplexer.ExportConfiguration(System.IO.Stream! destination, StackExchange.Redis.ExportOptions options = (StackExchange.Redis.ExportOptions)-1) -> void +StackExchange.Redis.ConnectionMultiplexer.GetCounters() -> StackExchange.Redis.ServerCounters! +StackExchange.Redis.ConnectionMultiplexer.GetDatabase(int db = -1, object? asyncState = null) -> StackExchange.Redis.IDatabase! +StackExchange.Redis.ConnectionMultiplexer.GetEndPoints(bool configuredOnly = false) -> System.Net.EndPoint![]! StackExchange.Redis.ConnectionMultiplexer.GetHashSlot(StackExchange.Redis.RedisKey key) -> int -StackExchange.Redis.ConnectionMultiplexer.GetSentinelMasterConnection(StackExchange.Redis.ConfigurationOptions config, System.IO.TextWriter log = null) -> StackExchange.Redis.ConnectionMultiplexer -StackExchange.Redis.ConnectionMultiplexer.GetServer(string host, int port, object asyncState = null) -> StackExchange.Redis.IServer -StackExchange.Redis.ConnectionMultiplexer.GetServer(string hostAndPort, object asyncState = null) -> StackExchange.Redis.IServer -StackExchange.Redis.ConnectionMultiplexer.GetServer(System.Net.EndPoint endpoint, object asyncState = null) -> StackExchange.Redis.IServer -StackExchange.Redis.ConnectionMultiplexer.GetServer(System.Net.IPAddress host, int port) -> StackExchange.Redis.IServer -StackExchange.Redis.ConnectionMultiplexer.GetStatus() -> string -StackExchange.Redis.ConnectionMultiplexer.GetStatus(System.IO.TextWriter log) -> void -StackExchange.Redis.ConnectionMultiplexer.GetStormLog() -> string -StackExchange.Redis.ConnectionMultiplexer.GetSubscriber(object asyncState = null) -> StackExchange.Redis.ISubscriber +StackExchange.Redis.ConnectionMultiplexer.GetSentinelMasterConnection(StackExchange.Redis.ConfigurationOptions! config, System.IO.TextWriter? log = null) -> StackExchange.Redis.ConnectionMultiplexer! +StackExchange.Redis.ConnectionMultiplexer.GetServer(string! host, int port, object? asyncState = null) -> StackExchange.Redis.IServer! +StackExchange.Redis.ConnectionMultiplexer.GetServer(string! hostAndPort, object? asyncState = null) -> StackExchange.Redis.IServer! +StackExchange.Redis.ConnectionMultiplexer.GetServer(System.Net.EndPoint? endpoint, object? asyncState = null) -> StackExchange.Redis.IServer! +StackExchange.Redis.ConnectionMultiplexer.GetServer(System.Net.IPAddress! host, int port) -> StackExchange.Redis.IServer! +StackExchange.Redis.ConnectionMultiplexer.GetStatus() -> string! +StackExchange.Redis.ConnectionMultiplexer.GetStatus(System.IO.TextWriter! log) -> void +StackExchange.Redis.ConnectionMultiplexer.GetStormLog() -> string? +StackExchange.Redis.ConnectionMultiplexer.GetSubscriber(object? asyncState = null) -> StackExchange.Redis.ISubscriber! StackExchange.Redis.ConnectionMultiplexer.HashSlot(StackExchange.Redis.RedisKey key) -> int -StackExchange.Redis.ConnectionMultiplexer.HashSlotMoved -> System.EventHandler +StackExchange.Redis.ConnectionMultiplexer.HashSlotMoved -> System.EventHandler? StackExchange.Redis.ConnectionMultiplexer.IncludeDetailInExceptions.get -> bool StackExchange.Redis.ConnectionMultiplexer.IncludeDetailInExceptions.set -> void StackExchange.Redis.ConnectionMultiplexer.IncludePerformanceCountersInExceptions.get -> bool StackExchange.Redis.ConnectionMultiplexer.IncludePerformanceCountersInExceptions.set -> void -StackExchange.Redis.ConnectionMultiplexer.InternalError -> System.EventHandler +StackExchange.Redis.ConnectionMultiplexer.InternalError -> System.EventHandler? StackExchange.Redis.ConnectionMultiplexer.IsConnected.get -> bool StackExchange.Redis.ConnectionMultiplexer.IsConnecting.get -> bool StackExchange.Redis.ConnectionMultiplexer.OperationCount.get -> long StackExchange.Redis.ConnectionMultiplexer.PreserveAsyncOrder.get -> bool StackExchange.Redis.ConnectionMultiplexer.PreserveAsyncOrder.set -> void StackExchange.Redis.ConnectionMultiplexer.PublishReconfigure(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.ConnectionMultiplexer.PublishReconfigureAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.ConnectionMultiplexer.ReconfigureAsync(string reason) -> System.Threading.Tasks.Task -StackExchange.Redis.ConnectionMultiplexer.RegisterProfiler(System.Func profilingSessionProvider) -> void +StackExchange.Redis.ConnectionMultiplexer.PublishReconfigureAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.ConnectionMultiplexer.ReconfigureAsync(string! reason) -> System.Threading.Tasks.Task! +StackExchange.Redis.ConnectionMultiplexer.RegisterProfiler(System.Func! profilingSessionProvider) -> void StackExchange.Redis.ConnectionMultiplexer.ResetStormLog() -> void -StackExchange.Redis.ConnectionMultiplexer.ServerMaintenanceEvent -> System.EventHandler +StackExchange.Redis.ConnectionMultiplexer.ServerMaintenanceEvent -> System.EventHandler? StackExchange.Redis.ConnectionMultiplexer.StormLogThreshold.get -> int StackExchange.Redis.ConnectionMultiplexer.StormLogThreshold.set -> void StackExchange.Redis.ConnectionMultiplexer.TimeoutMilliseconds.get -> int -StackExchange.Redis.ConnectionMultiplexer.Wait(System.Threading.Tasks.Task task) -> void -StackExchange.Redis.ConnectionMultiplexer.Wait(System.Threading.Tasks.Task task) -> T -StackExchange.Redis.ConnectionMultiplexer.WaitAll(params System.Threading.Tasks.Task[] tasks) -> void +StackExchange.Redis.ConnectionMultiplexer.Wait(System.Threading.Tasks.Task! task) -> void +StackExchange.Redis.ConnectionMultiplexer.Wait(System.Threading.Tasks.Task! task) -> T +StackExchange.Redis.ConnectionMultiplexer.WaitAll(params System.Threading.Tasks.Task![]! tasks) -> void StackExchange.Redis.ConnectionType StackExchange.Redis.ConnectionType.Interactive = 1 -> StackExchange.Redis.ConnectionType StackExchange.Redis.ConnectionType.None = 0 -> StackExchange.Redis.ConnectionType StackExchange.Redis.ConnectionType.Subscription = 2 -> StackExchange.Redis.ConnectionType StackExchange.Redis.EndPointCollection -StackExchange.Redis.EndPointCollection.Add(string host, int port) -> void -StackExchange.Redis.EndPointCollection.Add(string hostAndPort) -> void -StackExchange.Redis.EndPointCollection.Add(System.Net.IPAddress host, int port) -> void +StackExchange.Redis.EndPointCollection.Add(string! host, int port) -> void +StackExchange.Redis.EndPointCollection.Add(string! hostAndPort) -> void +StackExchange.Redis.EndPointCollection.Add(System.Net.IPAddress! host, int port) -> void StackExchange.Redis.EndPointCollection.EndPointCollection() -> void -StackExchange.Redis.EndPointCollection.EndPointCollection(System.Collections.Generic.IList endpoints) -> void -StackExchange.Redis.EndPointCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator -StackExchange.Redis.EndPointCollection.TryAdd(System.Net.EndPoint endpoint) -> bool +StackExchange.Redis.EndPointCollection.EndPointCollection(System.Collections.Generic.IList! endpoints) -> void +StackExchange.Redis.EndPointCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator! +StackExchange.Redis.EndPointCollection.TryAdd(System.Net.EndPoint! endpoint) -> bool StackExchange.Redis.EndPointEventArgs -StackExchange.Redis.EndPointEventArgs.EndPoint.get -> System.Net.EndPoint -StackExchange.Redis.EndPointEventArgs.EndPointEventArgs(object sender, System.Net.EndPoint endpoint) -> void +StackExchange.Redis.EndPointEventArgs.EndPoint.get -> System.Net.EndPoint! +StackExchange.Redis.EndPointEventArgs.EndPointEventArgs(object! sender, System.Net.EndPoint! endpoint) -> void StackExchange.Redis.Exclude StackExchange.Redis.Exclude.Both = StackExchange.Redis.Exclude.Start | StackExchange.Redis.Exclude.Stop -> StackExchange.Redis.Exclude StackExchange.Redis.Exclude.None = 0 -> StackExchange.Redis.Exclude @@ -419,136 +420,136 @@ StackExchange.Redis.HashEntry.Name.get -> StackExchange.Redis.RedisValue StackExchange.Redis.HashEntry.Value.get -> StackExchange.Redis.RedisValue StackExchange.Redis.HashSlotMovedEventArgs StackExchange.Redis.HashSlotMovedEventArgs.HashSlot.get -> int -StackExchange.Redis.HashSlotMovedEventArgs.HashSlotMovedEventArgs(object sender, int hashSlot, System.Net.EndPoint old, System.Net.EndPoint new) -> void -StackExchange.Redis.HashSlotMovedEventArgs.NewEndPoint.get -> System.Net.EndPoint -StackExchange.Redis.HashSlotMovedEventArgs.OldEndPoint.get -> System.Net.EndPoint +StackExchange.Redis.HashSlotMovedEventArgs.HashSlotMovedEventArgs(object! sender, int hashSlot, System.Net.EndPoint! old, System.Net.EndPoint! new) -> void +StackExchange.Redis.HashSlotMovedEventArgs.NewEndPoint.get -> System.Net.EndPoint! +StackExchange.Redis.HashSlotMovedEventArgs.OldEndPoint.get -> System.Net.EndPoint? StackExchange.Redis.IBatch StackExchange.Redis.IBatch.Execute() -> void StackExchange.Redis.IConnectionMultiplexer -StackExchange.Redis.IConnectionMultiplexer.ClientName.get -> string +StackExchange.Redis.IConnectionMultiplexer.ClientName.get -> string! StackExchange.Redis.IConnectionMultiplexer.Close(bool allowCommandsToComplete = true) -> void -StackExchange.Redis.IConnectionMultiplexer.CloseAsync(bool allowCommandsToComplete = true) -> System.Threading.Tasks.Task -StackExchange.Redis.IConnectionMultiplexer.Configuration.get -> string -StackExchange.Redis.IConnectionMultiplexer.ConfigurationChanged -> System.EventHandler -StackExchange.Redis.IConnectionMultiplexer.ConfigurationChangedBroadcast -> System.EventHandler -StackExchange.Redis.IConnectionMultiplexer.Configure(System.IO.TextWriter log = null) -> bool -StackExchange.Redis.IConnectionMultiplexer.ConfigureAsync(System.IO.TextWriter log = null) -> System.Threading.Tasks.Task -StackExchange.Redis.IConnectionMultiplexer.ConnectionFailed -> System.EventHandler -StackExchange.Redis.IConnectionMultiplexer.ConnectionRestored -> System.EventHandler -StackExchange.Redis.IConnectionMultiplexer.ErrorMessage -> System.EventHandler -StackExchange.Redis.IConnectionMultiplexer.ExportConfiguration(System.IO.Stream destination, StackExchange.Redis.ExportOptions options = (StackExchange.Redis.ExportOptions)-1) -> void -StackExchange.Redis.IConnectionMultiplexer.GetCounters() -> StackExchange.Redis.ServerCounters -StackExchange.Redis.IConnectionMultiplexer.GetDatabase(int db = -1, object asyncState = null) -> StackExchange.Redis.IDatabase -StackExchange.Redis.IConnectionMultiplexer.GetEndPoints(bool configuredOnly = false) -> System.Net.EndPoint[] +StackExchange.Redis.IConnectionMultiplexer.CloseAsync(bool allowCommandsToComplete = true) -> System.Threading.Tasks.Task! +StackExchange.Redis.IConnectionMultiplexer.Configuration.get -> string! +StackExchange.Redis.IConnectionMultiplexer.ConfigurationChanged -> System.EventHandler! +StackExchange.Redis.IConnectionMultiplexer.ConfigurationChangedBroadcast -> System.EventHandler! +StackExchange.Redis.IConnectionMultiplexer.Configure(System.IO.TextWriter? log = null) -> bool +StackExchange.Redis.IConnectionMultiplexer.ConfigureAsync(System.IO.TextWriter? log = null) -> System.Threading.Tasks.Task! +StackExchange.Redis.IConnectionMultiplexer.ConnectionFailed -> System.EventHandler! +StackExchange.Redis.IConnectionMultiplexer.ConnectionRestored -> System.EventHandler! +StackExchange.Redis.IConnectionMultiplexer.ErrorMessage -> System.EventHandler! +StackExchange.Redis.IConnectionMultiplexer.ExportConfiguration(System.IO.Stream! destination, StackExchange.Redis.ExportOptions options = (StackExchange.Redis.ExportOptions)-1) -> void +StackExchange.Redis.IConnectionMultiplexer.GetCounters() -> StackExchange.Redis.ServerCounters! +StackExchange.Redis.IConnectionMultiplexer.GetDatabase(int db = -1, object? asyncState = null) -> StackExchange.Redis.IDatabase! +StackExchange.Redis.IConnectionMultiplexer.GetEndPoints(bool configuredOnly = false) -> System.Net.EndPoint![]! StackExchange.Redis.IConnectionMultiplexer.GetHashSlot(StackExchange.Redis.RedisKey key) -> int -StackExchange.Redis.IConnectionMultiplexer.GetServer(string host, int port, object asyncState = null) -> StackExchange.Redis.IServer -StackExchange.Redis.IConnectionMultiplexer.GetServer(string hostAndPort, object asyncState = null) -> StackExchange.Redis.IServer -StackExchange.Redis.IConnectionMultiplexer.GetServer(System.Net.EndPoint endpoint, object asyncState = null) -> StackExchange.Redis.IServer -StackExchange.Redis.IConnectionMultiplexer.GetServer(System.Net.IPAddress host, int port) -> StackExchange.Redis.IServer -StackExchange.Redis.IConnectionMultiplexer.GetStatus() -> string -StackExchange.Redis.IConnectionMultiplexer.GetStatus(System.IO.TextWriter log) -> void -StackExchange.Redis.IConnectionMultiplexer.GetStormLog() -> string -StackExchange.Redis.IConnectionMultiplexer.GetSubscriber(object asyncState = null) -> StackExchange.Redis.ISubscriber +StackExchange.Redis.IConnectionMultiplexer.GetServer(string! host, int port, object? asyncState = null) -> StackExchange.Redis.IServer! +StackExchange.Redis.IConnectionMultiplexer.GetServer(string! hostAndPort, object? asyncState = null) -> StackExchange.Redis.IServer! +StackExchange.Redis.IConnectionMultiplexer.GetServer(System.Net.EndPoint! endpoint, object? asyncState = null) -> StackExchange.Redis.IServer! +StackExchange.Redis.IConnectionMultiplexer.GetServer(System.Net.IPAddress! host, int port) -> StackExchange.Redis.IServer! +StackExchange.Redis.IConnectionMultiplexer.GetStatus() -> string! +StackExchange.Redis.IConnectionMultiplexer.GetStatus(System.IO.TextWriter! log) -> void +StackExchange.Redis.IConnectionMultiplexer.GetStormLog() -> string? +StackExchange.Redis.IConnectionMultiplexer.GetSubscriber(object? asyncState = null) -> StackExchange.Redis.ISubscriber! StackExchange.Redis.IConnectionMultiplexer.HashSlot(StackExchange.Redis.RedisKey key) -> int -StackExchange.Redis.IConnectionMultiplexer.HashSlotMoved -> System.EventHandler +StackExchange.Redis.IConnectionMultiplexer.HashSlotMoved -> System.EventHandler! StackExchange.Redis.IConnectionMultiplexer.IncludeDetailInExceptions.get -> bool StackExchange.Redis.IConnectionMultiplexer.IncludeDetailInExceptions.set -> void -StackExchange.Redis.IConnectionMultiplexer.InternalError -> System.EventHandler +StackExchange.Redis.IConnectionMultiplexer.InternalError -> System.EventHandler! StackExchange.Redis.IConnectionMultiplexer.IsConnected.get -> bool StackExchange.Redis.IConnectionMultiplexer.IsConnecting.get -> bool StackExchange.Redis.IConnectionMultiplexer.OperationCount.get -> long StackExchange.Redis.IConnectionMultiplexer.PreserveAsyncOrder.get -> bool StackExchange.Redis.IConnectionMultiplexer.PreserveAsyncOrder.set -> void StackExchange.Redis.IConnectionMultiplexer.PublishReconfigure(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IConnectionMultiplexer.PublishReconfigureAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IConnectionMultiplexer.RegisterProfiler(System.Func profilingSessionProvider) -> void +StackExchange.Redis.IConnectionMultiplexer.PublishReconfigureAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IConnectionMultiplexer.RegisterProfiler(System.Func! profilingSessionProvider) -> void StackExchange.Redis.IConnectionMultiplexer.ResetStormLog() -> void StackExchange.Redis.IConnectionMultiplexer.StormLogThreshold.get -> int StackExchange.Redis.IConnectionMultiplexer.StormLogThreshold.set -> void StackExchange.Redis.IConnectionMultiplexer.TimeoutMilliseconds.get -> int -StackExchange.Redis.IConnectionMultiplexer.ToString() -> string -StackExchange.Redis.IConnectionMultiplexer.Wait(System.Threading.Tasks.Task task) -> void -StackExchange.Redis.IConnectionMultiplexer.Wait(System.Threading.Tasks.Task task) -> T -StackExchange.Redis.IConnectionMultiplexer.WaitAll(params System.Threading.Tasks.Task[] tasks) -> void +StackExchange.Redis.IConnectionMultiplexer.ToString() -> string! +StackExchange.Redis.IConnectionMultiplexer.Wait(System.Threading.Tasks.Task! task) -> void +StackExchange.Redis.IConnectionMultiplexer.Wait(System.Threading.Tasks.Task! task) -> T +StackExchange.Redis.IConnectionMultiplexer.WaitAll(params System.Threading.Tasks.Task![]! tasks) -> void StackExchange.Redis.IDatabase -StackExchange.Redis.IDatabase.CreateBatch(object asyncState = null) -> StackExchange.Redis.IBatch -StackExchange.Redis.IDatabase.CreateTransaction(object asyncState = null) -> StackExchange.Redis.ITransaction +StackExchange.Redis.IDatabase.CreateBatch(object? asyncState = null) -> StackExchange.Redis.IBatch! +StackExchange.Redis.IDatabase.CreateTransaction(object? asyncState = null) -> StackExchange.Redis.ITransaction! StackExchange.Redis.IDatabase.Database.get -> int StackExchange.Redis.IDatabase.DebugObject(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue -StackExchange.Redis.IDatabase.Execute(string command, params object[] args) -> StackExchange.Redis.RedisResult -StackExchange.Redis.IDatabase.Execute(string command, System.Collections.Generic.ICollection args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult +StackExchange.Redis.IDatabase.Execute(string! command, params object![]! args) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.IDatabase.Execute(string! command, System.Collections.Generic.ICollection! args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! StackExchange.Redis.IDatabase.GeoAdd(StackExchange.Redis.RedisKey key, double longitude, double latitude, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.GeoAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.GeoEntry value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.GeoAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.GeoEntry[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.GeoAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.GeoEntry[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.GeoDistance(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member1, StackExchange.Redis.RedisValue member2, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double? -StackExchange.Redis.IDatabase.GeoHash(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string -StackExchange.Redis.IDatabase.GeoHash(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string[] +StackExchange.Redis.IDatabase.GeoHash(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? +StackExchange.Redis.IDatabase.GeoHash(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string?[]! StackExchange.Redis.IDatabase.GeoPosition(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoPosition? -StackExchange.Redis.IDatabase.GeoPosition(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoPosition?[] -StackExchange.Redis.IDatabase.GeoRadius(StackExchange.Redis.RedisKey key, double longitude, double latitude, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoRadiusResult[] -StackExchange.Redis.IDatabase.GeoRadius(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoRadiusResult[] +StackExchange.Redis.IDatabase.GeoPosition(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoPosition?[]! +StackExchange.Redis.IDatabase.GeoRadius(StackExchange.Redis.RedisKey key, double longitude, double latitude, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoRadiusResult[]! +StackExchange.Redis.IDatabase.GeoRadius(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoRadiusResult[]! StackExchange.Redis.IDatabase.GeoRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.HashDecrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double StackExchange.Redis.IDatabase.HashDecrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.HashDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.HashDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.HashDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.HashExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.HashGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue -StackExchange.Redis.IDatabase.HashGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] -StackExchange.Redis.IDatabase.HashGetAll(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.HashEntry[] -StackExchange.Redis.IDatabase.HashGetLease(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease +StackExchange.Redis.IDatabase.HashGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.IDatabase.HashGetAll(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.HashEntry[]! +StackExchange.Redis.IDatabase.HashGetLease(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? StackExchange.Redis.IDatabase.HashIncrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double StackExchange.Redis.IDatabase.HashIncrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.HashKeys(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.HashKeys(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.HashLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.HashScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable -StackExchange.Redis.IDatabase.HashScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable -StackExchange.Redis.IDatabase.HashSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[] hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IDatabase.HashScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! +StackExchange.Redis.IDatabase.HashScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable! +StackExchange.Redis.IDatabase.HashSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IDatabase.HashSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.HashStringLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.HashValues(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.HashValues(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.HyperLogLogAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.HyperLogLogAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.HyperLogLogAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.HyperLogLogLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.HyperLogLogLength(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.HyperLogLogLength(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.HyperLogLogMerge(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IDatabase.HyperLogLogMerge(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] sourceKeys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IDatabase.IdentifyEndpoint(StackExchange.Redis.RedisKey key = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint +StackExchange.Redis.IDatabase.HyperLogLogMerge(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! sourceKeys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IDatabase.IdentifyEndpoint(StackExchange.Redis.RedisKey key = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint? StackExchange.Redis.IDatabase.KeyDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.KeyDelete(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.KeyDump(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> byte[] +StackExchange.Redis.IDatabase.KeyDelete(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.KeyDump(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> byte[]? StackExchange.Redis.IDatabase.KeyExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.KeyExists(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.KeyExists(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyIdleTime(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.TimeSpan? -StackExchange.Redis.IDatabase.KeyMigrate(StackExchange.Redis.RedisKey key, System.Net.EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, StackExchange.Redis.MigrateOptions migrateOptions = StackExchange.Redis.MigrateOptions.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IDatabase.KeyMigrate(StackExchange.Redis.RedisKey key, System.Net.EndPoint! toServer, int toDatabase = 0, int timeoutMilliseconds = 0, StackExchange.Redis.MigrateOptions migrateOptions = StackExchange.Redis.MigrateOptions.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IDatabase.KeyMove(StackExchange.Redis.RedisKey key, int database, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyPersist(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyRandom(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisKey StackExchange.Redis.IDatabase.KeyRename(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisKey newKey, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.KeyRestore(StackExchange.Redis.RedisKey key, byte[] value, System.TimeSpan? expiry = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IDatabase.KeyRestore(StackExchange.Redis.RedisKey key, byte[]! value, System.TimeSpan? expiry = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IDatabase.KeyTimeToLive(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.TimeSpan? StackExchange.Redis.IDatabase.KeyTouch(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.KeyTouch(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.KeyTouch(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.KeyType(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisType StackExchange.Redis.IDatabase.ListGetByIndex(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.ListInsertAfter(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pivot, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.ListInsertBefore(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pivot, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.ListLeftPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.ListLeftPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.ListLeftPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags) -> long -StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags) -> long +StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.ListLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.ListRange(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.ListRange(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.ListRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, long count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.ListRightPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.ListRightPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.ListRightPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.ListRightPopLeftPush(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.ListRightPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.ListRightPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags) -> long -StackExchange.Redis.IDatabase.ListRightPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.ListRightPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags) -> long +StackExchange.Redis.IDatabase.ListRightPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.ListSetByIndex(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IDatabase.ListTrim(StackExchange.Redis.RedisKey key, long start, long stop, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IDatabase.LockExtend(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool @@ -556,96 +557,96 @@ StackExchange.Redis.IDatabase.LockQuery(StackExchange.Redis.RedisKey key, StackE StackExchange.Redis.IDatabase.LockRelease(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.LockTake(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.Publish(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.ScriptEvaluate(byte[] hash, StackExchange.Redis.RedisKey[] keys = null, StackExchange.Redis.RedisValue[] values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult -StackExchange.Redis.IDatabase.ScriptEvaluate(StackExchange.Redis.LoadedLuaScript script, object parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult -StackExchange.Redis.IDatabase.ScriptEvaluate(StackExchange.Redis.LuaScript script, object parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult -StackExchange.Redis.IDatabase.ScriptEvaluate(string script, StackExchange.Redis.RedisKey[] keys = null, StackExchange.Redis.RedisValue[] values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult +StackExchange.Redis.IDatabase.ScriptEvaluate(byte[]! hash, StackExchange.Redis.RedisKey[]? keys = null, StackExchange.Redis.RedisValue[]? values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.IDatabase.ScriptEvaluate(StackExchange.Redis.LoadedLuaScript! script, object? parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.IDatabase.ScriptEvaluate(StackExchange.Redis.LuaScript! script, object? parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.IDatabase.ScriptEvaluate(string! script, StackExchange.Redis.RedisKey[]? keys = null, StackExchange.Redis.RedisValue[]? values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! StackExchange.Redis.IDatabase.SetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.SetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.SetCombine(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] -StackExchange.Redis.IDatabase.SetCombine(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SetCombine(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.IDatabase.SetCombine(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.SetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.SetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.SetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.SetMembers(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SetMembers(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.SetMove(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.SetPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SetPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.SetPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.SetRandomMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue -StackExchange.Redis.IDatabase.SetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.SetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.SetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.SetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable -StackExchange.Redis.IDatabase.SetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable -StackExchange.Redis.IDatabase.Sort(StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[] get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] -StackExchange.Redis.IDatabase.SortAndStore(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[] get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! +StackExchange.Redis.IDatabase.SetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable! +StackExchange.Redis.IDatabase.Sort(StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[]? get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.IDatabase.SortAndStore(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[]? get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.CommandFlags flags) -> bool StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[] values, StackExchange.Redis.CommandFlags flags) -> long -StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.CommandFlags flags) -> long +StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.SortedSetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] keys, double[] weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetDecrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double StackExchange.Redis.IDatabase.SortedSetIncrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double StackExchange.Redis.IDatabase.SortedSetLength(StackExchange.Redis.RedisKey key, double min = -Infinity, double max = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetLengthByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[] +StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry? -StackExchange.Redis.IDatabase.SortedSetRangeByRank(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SortedSetRangeByRank(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.SortedSetRangeAndStore(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.RedisValue start, StackExchange.Redis.RedisValue stop, StackExchange.Redis.SortedSetOrder sortedSetOrder = StackExchange.Redis.SortedSetOrder.ByRank, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long? take = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.SortedSetRangeByRankWithScores(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[] -StackExchange.Redis.IDatabase.SortedSetRangeByScore(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] -StackExchange.Redis.IDatabase.SortedSetRangeByScoreWithScores(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[] -StackExchange.Redis.IDatabase.SortedSetRangeByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue max = default(StackExchange.Redis.RedisValue), StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] -StackExchange.Redis.IDatabase.SortedSetRangeByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude, long skip, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.SortedSetRangeByRankWithScores(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! +StackExchange.Redis.IDatabase.SortedSetRangeByScore(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.IDatabase.SortedSetRangeByScoreWithScores(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! +StackExchange.Redis.IDatabase.SortedSetRangeByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue max = default(StackExchange.Redis.RedisValue), StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.IDatabase.SortedSetRangeByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude, long skip, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.SortedSetRank(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long? StackExchange.Redis.IDatabase.SortedSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.SortedSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetRemoveRangeByRank(StackExchange.Redis.RedisKey key, long start, long stop, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetRemoveRangeByScore(StackExchange.Redis.RedisKey key, double start, double stop, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetRemoveRangeByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.SortedSetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable -StackExchange.Redis.IDatabase.SortedSetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable +StackExchange.Redis.IDatabase.SortedSetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! +StackExchange.Redis.IDatabase.SortedSetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable! StackExchange.Redis.IDatabase.SortedSetScore(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double? StackExchange.Redis.IDatabase.StreamAcknowledge(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue messageId, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.StreamAcknowledge(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[] streamPairs, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StreamAcknowledge(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue -StackExchange.Redis.IDatabase.StreamClaim(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[] -StackExchange.Redis.IDatabase.StreamClaimIdsOnly(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.StreamClaim(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[]! +StackExchange.Redis.IDatabase.StreamClaimIdsOnly(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.StreamConsumerGroupSetPosition(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue position, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.StreamConsumerInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamConsumerInfo[] +StackExchange.Redis.IDatabase.StreamConsumerInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamConsumerInfo[]! StackExchange.Redis.IDatabase.StreamCreateConsumerGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue? position = null, bool createStream = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.StreamCreateConsumerGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue? position, StackExchange.Redis.CommandFlags flags) -> bool -StackExchange.Redis.IDatabase.StreamDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StreamDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StreamDeleteConsumer(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StreamDeleteConsumerGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.StreamGroupInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamGroupInfo[] +StackExchange.Redis.IDatabase.StreamGroupInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamGroupInfo[]! StackExchange.Redis.IDatabase.StreamInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamInfo StackExchange.Redis.IDatabase.StreamLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StreamPending(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamPendingInfo -StackExchange.Redis.IDatabase.StreamPendingMessages(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, int count, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamPendingMessageInfo[] -StackExchange.Redis.IDatabase.StreamRange(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, int? count = null, StackExchange.Redis.Order messageOrder = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[] -StackExchange.Redis.IDatabase.StreamRead(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue position, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[] -StackExchange.Redis.IDatabase.StreamRead(StackExchange.Redis.StreamPosition[] streamPositions, int? countPerStream = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisStream[] -StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position = null, int? count = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[] -StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position, int? count, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.StreamEntry[] -StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.StreamPosition[] streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisStream[] -StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.StreamPosition[] streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.RedisStream[] +StackExchange.Redis.IDatabase.StreamPendingMessages(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, int count, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamPendingMessageInfo[]! +StackExchange.Redis.IDatabase.StreamRange(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, int? count = null, StackExchange.Redis.Order messageOrder = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[]! +StackExchange.Redis.IDatabase.StreamRead(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue position, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[]! +StackExchange.Redis.IDatabase.StreamRead(StackExchange.Redis.StreamPosition[]! streamPositions, int? countPerStream = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisStream[]! +StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position = null, int? count = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[]! +StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position, int? count, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.StreamEntry[]! +StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisStream[]! +StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.RedisStream[]! StackExchange.Redis.IDatabase.StreamTrim(StackExchange.Redis.RedisKey key, int maxLength, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringAppend(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringBitCount(StackExchange.Redis.RedisKey key, long start = 0, long end = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringBitOperation(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.StringBitOperation(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringBitOperation(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringBitPosition(StackExchange.Redis.RedisKey key, bool bit, long start = 0, long end = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringDecrement(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double StackExchange.Redis.IDatabase.StringDecrement(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue -StackExchange.Redis.IDatabase.StringGet(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[] +StackExchange.Redis.IDatabase.StringGet(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.StringGetBit(StackExchange.Redis.RedisKey key, long offset, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.StringGetDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue -StackExchange.Redis.IDatabase.StringGetLease(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease +StackExchange.Redis.IDatabase.StringGetLease(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? StackExchange.Redis.IDatabase.StringGetRange(StackExchange.Redis.RedisKey key, long start, long end, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.StringGetSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.StringGetSetExpiry(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue @@ -656,214 +657,214 @@ StackExchange.Redis.IDatabase.StringIncrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.IDatabase.StringLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> bool -StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.StringSetAndGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.StringSetAndGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.StringSetBit(StackExchange.Redis.RedisKey key, long offset, bool bit, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.StringSetRange(StackExchange.Redis.RedisKey key, long offset, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabaseAsync -StackExchange.Redis.IDatabaseAsync.DebugObjectAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ExecuteAsync(string command, params object[] args) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ExecuteAsync(string command, System.Collections.Generic.ICollection args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.GeoAddAsync(StackExchange.Redis.RedisKey key, double longitude, double latitude, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.GeoAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.GeoEntry value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.GeoAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.GeoEntry[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.GeoDistanceAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member1, StackExchange.Redis.RedisValue member2, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.GeoHashAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.GeoHashAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.GeoPositionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.GeoPositionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.GeoRadiusAsync(StackExchange.Redis.RedisKey key, double longitude, double latitude, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.GeoRadiusAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.GeoRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashDecrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashDecrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashExistsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashGetAllAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashGetLeaseAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task> -StackExchange.Redis.IDatabaseAsync.HashIncrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashIncrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashKeysAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable -StackExchange.Redis.IDatabaseAsync.HashSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[] hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashStringLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HashValuesAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HyperLogLogAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HyperLogLogAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HyperLogLogLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HyperLogLogLengthAsync(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HyperLogLogMergeAsync(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.HyperLogLogMergeAsync(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] sourceKeys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.IdentifyEndpointAsync(StackExchange.Redis.RedisKey key = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.DebugObjectAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ExecuteAsync(string! command, params object![]! args) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ExecuteAsync(string! command, System.Collections.Generic.ICollection? args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoAddAsync(StackExchange.Redis.RedisKey key, double longitude, double latitude, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.GeoEntry value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.GeoEntry[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoDistanceAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member1, StackExchange.Redis.RedisValue member2, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoHashAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoHashAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoPositionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoPositionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoRadiusAsync(StackExchange.Redis.RedisKey key, double longitude, double latitude, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoRadiusAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashDecrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashDecrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashExistsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashGetAllAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashGetLeaseAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +StackExchange.Redis.IDatabaseAsync.HashIncrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashIncrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashKeysAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! +StackExchange.Redis.IDatabaseAsync.HashSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashStringLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashValuesAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HyperLogLogAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HyperLogLogAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HyperLogLogLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HyperLogLogLengthAsync(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HyperLogLogMergeAsync(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HyperLogLogMergeAsync(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! sourceKeys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.IdentifyEndpointAsync(StackExchange.Redis.RedisKey key = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.IsConnected(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabaseAsync.KeyDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyDeleteAsync(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyDumpAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyExistsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyExistsAsync(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyIdleTimeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyMigrateAsync(StackExchange.Redis.RedisKey key, System.Net.EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, StackExchange.Redis.MigrateOptions migrateOptions = StackExchange.Redis.MigrateOptions.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyMoveAsync(StackExchange.Redis.RedisKey key, int database, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyPersistAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyRandomAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyRenameAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisKey newKey, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyRestoreAsync(StackExchange.Redis.RedisKey key, byte[] value, System.TimeSpan? expiry = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyTimeToLiveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyTouchAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyTouchAsync(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.KeyTypeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListGetByIndexAsync(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListInsertAfterAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pivot, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListInsertBeforeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pivot, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListLeftPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListLeftPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListRangeAsync(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, long count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListRightPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListRightPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListRightPopLeftPushAsync(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListRightPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListRightPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListRightPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListSetByIndexAsync(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ListTrimAsync(StackExchange.Redis.RedisKey key, long start, long stop, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.LockExtendAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.LockQueryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.LockReleaseAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.LockTakeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.PublishAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(byte[] hash, StackExchange.Redis.RedisKey[] keys = null, StackExchange.Redis.RedisValue[] values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(StackExchange.Redis.LoadedLuaScript script, object parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(StackExchange.Redis.LuaScript script, object parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(string script, StackExchange.Redis.RedisKey[] keys = null, StackExchange.Redis.RedisValue[] values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetMembersAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetMoveAsync(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetRandomMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SetScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable -StackExchange.Redis.IDatabaseAsync.SortAndStoreAsync(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[] get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortAsync(StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[] get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[] values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] keys, double[] weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetDecrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetIncrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetLengthAsync(StackExchange.Redis.RedisKey key, double min = -Infinity, double max = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetLengthByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetRangeByRankAsync(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetRangeAndStoreAsync(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.RedisValue start, StackExchange.Redis.RedisValue stop, StackExchange.Redis.SortedSetOrder sortedSetOrder = StackExchange.Redis.SortedSetOrder.ByRank, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long? take = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetRangeByRankWithScoresAsync(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetRangeByScoreAsync(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetRangeByScoreWithScoresAsync(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetRangeByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue max = default(StackExchange.Redis.RedisValue), StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetRangeByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude, long skip, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetRankAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetRemoveRangeByRankAsync(StackExchange.Redis.RedisKey key, long start, long stop, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetRemoveRangeByScoreAsync(StackExchange.Redis.RedisKey key, double start, double stop, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetRemoveRangeByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.SortedSetScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable -StackExchange.Redis.IDatabaseAsync.SortedSetScoreAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue messageId, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[] streamPairs, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamClaimAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamClaimIdsOnlyAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamConsumerGroupSetPositionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue position, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamConsumerInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamCreateConsumerGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue? position = null, bool createStream = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamCreateConsumerGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue? position, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[] messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamDeleteConsumerAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamDeleteConsumerGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamGroupInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamPendingAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamPendingMessagesAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, int count, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamRangeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, int? count = null, StackExchange.Redis.Order messageOrder = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamReadAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue position, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamReadAsync(StackExchange.Redis.StreamPosition[] streamPositions, int? countPerStream = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position = null, int? count = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position, int? count, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.StreamPosition[] streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.StreamPosition[] streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StreamTrimAsync(StackExchange.Redis.RedisKey key, int maxLength, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringAppendAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringBitCountAsync(StackExchange.Redis.RedisKey key, long start = 0, long end = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringBitOperationAsync(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringBitOperationAsync(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringBitPositionAsync(StackExchange.Redis.RedisKey key, bool bit, long start = 0, long end = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringDecrementAsync(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringDecrementAsync(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringGetAsync(StackExchange.Redis.RedisKey[] keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringGetBitAsync(StackExchange.Redis.RedisKey key, long offset, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringGetDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringGetLeaseAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task> -StackExchange.Redis.IDatabaseAsync.StringGetRangeAsync(StackExchange.Redis.RedisKey key, long start, long end, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringGetSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringGetSetExpiryAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringGetSetExpiryAsync(StackExchange.Redis.RedisKey key, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringGetWithExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringIncrementAsync(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringIncrementAsync(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringSetAsync(System.Collections.Generic.KeyValuePair[] values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringSetBitAsync(StackExchange.Redis.RedisKey key, long offset, bool bit, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IDatabaseAsync.StringSetRangeAsync(StackExchange.Redis.RedisKey key, long offset, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IDatabaseAsync.KeyDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyDeleteAsync(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyDumpAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyExistsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyExistsAsync(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyIdleTimeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyMigrateAsync(StackExchange.Redis.RedisKey key, System.Net.EndPoint! toServer, int toDatabase = 0, int timeoutMilliseconds = 0, StackExchange.Redis.MigrateOptions migrateOptions = StackExchange.Redis.MigrateOptions.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyMoveAsync(StackExchange.Redis.RedisKey key, int database, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyPersistAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyRandomAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyRenameAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisKey newKey, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyRestoreAsync(StackExchange.Redis.RedisKey key, byte[]! value, System.TimeSpan? expiry = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyTimeToLiveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyTouchAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyTouchAsync(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyTypeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListGetByIndexAsync(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListInsertAfterAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pivot, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListInsertBeforeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pivot, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListLeftPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListLeftPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListRangeAsync(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, long count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListRightPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListRightPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListRightPopLeftPushAsync(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListRightPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListRightPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListRightPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListSetByIndexAsync(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListTrimAsync(StackExchange.Redis.RedisKey key, long start, long stop, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.LockExtendAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.LockQueryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.LockReleaseAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.LockTakeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.PublishAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(byte[]! hash, StackExchange.Redis.RedisKey[]? keys = null, StackExchange.Redis.RedisValue[]? values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(StackExchange.Redis.LoadedLuaScript! script, object? parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(StackExchange.Redis.LuaScript! script, object? parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(string! script, StackExchange.Redis.RedisKey[]? keys = null, StackExchange.Redis.RedisValue[]? values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetMembersAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetMoveAsync(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetRandomMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! +StackExchange.Redis.IDatabaseAsync.SortAndStoreAsync(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[]? get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortAsync(StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[]? get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetDecrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetIncrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetLengthAsync(StackExchange.Redis.RedisKey key, double min = -Infinity, double max = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetLengthByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRangeAndStoreAsync(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.RedisValue start, StackExchange.Redis.RedisValue stop, StackExchange.Redis.SortedSetOrder sortedSetOrder = StackExchange.Redis.SortedSetOrder.ByRank, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long? take = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRangeByRankAsync(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRangeByRankWithScoresAsync(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRangeByScoreAsync(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRangeByScoreWithScoresAsync(StackExchange.Redis.RedisKey key, double start = -Infinity, double stop = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRangeByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue max = default(StackExchange.Redis.RedisValue), StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRangeByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude, long skip, long take = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRankAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRemoveRangeByRankAsync(StackExchange.Redis.RedisKey key, long start, long stop, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRemoveRangeByScoreAsync(StackExchange.Redis.RedisKey key, double start, double stop, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRemoveRangeByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! +StackExchange.Redis.IDatabaseAsync.SortedSetScoreAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue messageId, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamClaimAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamClaimIdsOnlyAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamConsumerGroupSetPositionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue position, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamConsumerInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamCreateConsumerGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue? position = null, bool createStream = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamCreateConsumerGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue? position, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamDeleteConsumerAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamDeleteConsumerGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamGroupInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamPendingAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamPendingMessagesAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, int count, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamRangeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, int? count = null, StackExchange.Redis.Order messageOrder = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamReadAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue position, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamReadAsync(StackExchange.Redis.StreamPosition[]! streamPositions, int? countPerStream = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position = null, int? count = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position, int? count, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamTrimAsync(StackExchange.Redis.RedisKey key, int maxLength, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringAppendAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringBitCountAsync(StackExchange.Redis.RedisKey key, long start = 0, long end = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringBitOperationAsync(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringBitOperationAsync(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringBitPositionAsync(StackExchange.Redis.RedisKey key, bool bit, long start = 0, long end = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringDecrementAsync(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringDecrementAsync(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringGetAsync(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringGetBitAsync(StackExchange.Redis.RedisKey key, long offset, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringGetDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringGetLeaseAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +StackExchange.Redis.IDatabaseAsync.StringGetRangeAsync(StackExchange.Redis.RedisKey key, long start, long end, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringGetSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringGetSetExpiryAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringGetSetExpiryAsync(StackExchange.Redis.RedisKey key, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringGetWithExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringIncrementAsync(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringIncrementAsync(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringSetAsync(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringSetBitAsync(StackExchange.Redis.RedisKey key, long offset, bool bit, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringSetRangeAsync(StackExchange.Redis.RedisKey key, long offset, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.InternalErrorEventArgs StackExchange.Redis.InternalErrorEventArgs.ConnectionType.get -> StackExchange.Redis.ConnectionType -StackExchange.Redis.InternalErrorEventArgs.EndPoint.get -> System.Net.EndPoint -StackExchange.Redis.InternalErrorEventArgs.Exception.get -> System.Exception -StackExchange.Redis.InternalErrorEventArgs.InternalErrorEventArgs(object sender, System.Net.EndPoint endpoint, StackExchange.Redis.ConnectionType connectionType, System.Exception exception, string origin) -> void -StackExchange.Redis.InternalErrorEventArgs.Origin.get -> string +StackExchange.Redis.InternalErrorEventArgs.EndPoint.get -> System.Net.EndPoint? +StackExchange.Redis.InternalErrorEventArgs.Exception.get -> System.Exception! +StackExchange.Redis.InternalErrorEventArgs.InternalErrorEventArgs(object! sender, System.Net.EndPoint! endpoint, StackExchange.Redis.ConnectionType connectionType, System.Exception! exception, string! origin) -> void +StackExchange.Redis.InternalErrorEventArgs.Origin.get -> string? StackExchange.Redis.IReconnectRetryPolicy StackExchange.Redis.IReconnectRetryPolicy.ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSinceLastRetry) -> bool StackExchange.Redis.IRedis StackExchange.Redis.IRedis.Ping(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.TimeSpan StackExchange.Redis.IRedisAsync -StackExchange.Redis.IRedisAsync.Multiplexer.get -> StackExchange.Redis.IConnectionMultiplexer -StackExchange.Redis.IRedisAsync.PingAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IRedisAsync.TryWait(System.Threading.Tasks.Task task) -> bool -StackExchange.Redis.IRedisAsync.Wait(System.Threading.Tasks.Task task) -> void -StackExchange.Redis.IRedisAsync.Wait(System.Threading.Tasks.Task task) -> T -StackExchange.Redis.IRedisAsync.WaitAll(params System.Threading.Tasks.Task[] tasks) -> void +StackExchange.Redis.IRedisAsync.Multiplexer.get -> StackExchange.Redis.IConnectionMultiplexer! +StackExchange.Redis.IRedisAsync.PingAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IRedisAsync.TryWait(System.Threading.Tasks.Task! task) -> bool +StackExchange.Redis.IRedisAsync.Wait(System.Threading.Tasks.Task! task) -> void +StackExchange.Redis.IRedisAsync.Wait(System.Threading.Tasks.Task! task) -> T +StackExchange.Redis.IRedisAsync.WaitAll(params System.Threading.Tasks.Task![]! tasks) -> void StackExchange.Redis.IScanningCursor StackExchange.Redis.IScanningCursor.Cursor.get -> long StackExchange.Redis.IScanningCursor.PageOffset.get -> int @@ -873,143 +874,143 @@ StackExchange.Redis.IServer.AllowReplicaWrites.get -> bool StackExchange.Redis.IServer.AllowReplicaWrites.set -> void StackExchange.Redis.IServer.AllowSlaveWrites.get -> bool StackExchange.Redis.IServer.AllowSlaveWrites.set -> void -StackExchange.Redis.IServer.ClientKill(long? id = null, StackExchange.Redis.ClientType? clientType = null, System.Net.EndPoint endpoint = null, bool skipMe = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IServer.ClientKill(System.Net.EndPoint endpoint, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.ClientKillAsync(long? id = null, StackExchange.Redis.ClientType? clientType = null, System.Net.EndPoint endpoint = null, bool skipMe = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.ClientKillAsync(System.Net.EndPoint endpoint, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.ClientList(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ClientInfo[] -StackExchange.Redis.IServer.ClientListAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.ClusterConfiguration.get -> StackExchange.Redis.ClusterConfiguration -StackExchange.Redis.IServer.ClusterNodes(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ClusterConfiguration -StackExchange.Redis.IServer.ClusterNodesAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.ClusterNodesRaw(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string -StackExchange.Redis.IServer.ClusterNodesRawAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.ConfigGet(StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[] -StackExchange.Redis.IServer.ConfigGetAsync(StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[]> +StackExchange.Redis.IServer.ClientKill(long? id = null, StackExchange.Redis.ClientType? clientType = null, System.Net.EndPoint? endpoint = null, bool skipMe = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IServer.ClientKill(System.Net.EndPoint! endpoint, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.ClientKillAsync(long? id = null, StackExchange.Redis.ClientType? clientType = null, System.Net.EndPoint? endpoint = null, bool skipMe = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.ClientKillAsync(System.Net.EndPoint! endpoint, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.ClientList(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ClientInfo![]! +StackExchange.Redis.IServer.ClientListAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.ClusterConfiguration.get -> StackExchange.Redis.ClusterConfiguration? +StackExchange.Redis.IServer.ClusterNodes(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ClusterConfiguration? +StackExchange.Redis.IServer.ClusterNodesAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.ClusterNodesRaw(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? +StackExchange.Redis.IServer.ClusterNodesRawAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.ConfigGet(StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[]! +StackExchange.Redis.IServer.ConfigGetAsync(StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[]!>! StackExchange.Redis.IServer.ConfigResetStatistics(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.ConfigResetStatisticsAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ConfigResetStatisticsAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.ConfigRewrite(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.ConfigRewriteAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ConfigRewriteAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.ConfigSet(StackExchange.Redis.RedisValue setting, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.ConfigSetAsync(StackExchange.Redis.RedisValue setting, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.ConfigSetAsync(StackExchange.Redis.RedisValue setting, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.DatabaseCount.get -> int StackExchange.Redis.IServer.DatabaseSize(int database = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IServer.DatabaseSizeAsync(int database = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.DatabaseSizeAsync(int database = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.Echo(StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue -StackExchange.Redis.IServer.EchoAsync(StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.EndPoint.get -> System.Net.EndPoint -StackExchange.Redis.IServer.Execute(string command, params object[] args) -> StackExchange.Redis.RedisResult -StackExchange.Redis.IServer.Execute(string command, System.Collections.Generic.ICollection args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult -StackExchange.Redis.IServer.ExecuteAsync(string command, params object[] args) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.ExecuteAsync(string command, System.Collections.Generic.ICollection args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.EchoAsync(StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.EndPoint.get -> System.Net.EndPoint! +StackExchange.Redis.IServer.Execute(string! command, params object![]! args) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.IServer.Execute(string! command, System.Collections.Generic.ICollection! args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.IServer.ExecuteAsync(string! command, params object![]! args) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.ExecuteAsync(string! command, System.Collections.Generic.ICollection! args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.Features.get -> StackExchange.Redis.RedisFeatures StackExchange.Redis.IServer.FlushAllDatabases(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.FlushAllDatabasesAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.FlushAllDatabasesAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.FlushDatabase(int database = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.FlushDatabaseAsync(int database = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.GetCounters() -> StackExchange.Redis.ServerCounters -StackExchange.Redis.IServer.Info(StackExchange.Redis.RedisValue section = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Linq.IGrouping>[] -StackExchange.Redis.IServer.InfoAsync(StackExchange.Redis.RedisValue section = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task>[]> -StackExchange.Redis.IServer.InfoRaw(StackExchange.Redis.RedisValue section = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string -StackExchange.Redis.IServer.InfoRawAsync(StackExchange.Redis.RedisValue section = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.FlushDatabaseAsync(int database = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.GetCounters() -> StackExchange.Redis.ServerCounters! +StackExchange.Redis.IServer.Info(StackExchange.Redis.RedisValue section = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Linq.IGrouping>![]! +StackExchange.Redis.IServer.InfoAsync(StackExchange.Redis.RedisValue section = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task>![]!>! +StackExchange.Redis.IServer.InfoRaw(StackExchange.Redis.RedisValue section = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? +StackExchange.Redis.IServer.InfoRawAsync(StackExchange.Redis.RedisValue section = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.IsConnected.get -> bool StackExchange.Redis.IServer.IsReplica.get -> bool StackExchange.Redis.IServer.IsSlave.get -> bool -StackExchange.Redis.IServer.Keys(int database = -1, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable -StackExchange.Redis.IServer.Keys(int database, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable -StackExchange.Redis.IServer.KeysAsync(int database = -1, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable +StackExchange.Redis.IServer.Keys(int database = -1, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! +StackExchange.Redis.IServer.Keys(int database, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable! +StackExchange.Redis.IServer.KeysAsync(int database = -1, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! StackExchange.Redis.IServer.LastSave(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.DateTime -StackExchange.Redis.IServer.LastSaveAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.LatencyDoctor(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string -StackExchange.Redis.IServer.LatencyDoctorAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.LatencyHistory(string eventName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LatencyHistoryEntry[] -StackExchange.Redis.IServer.LatencyHistoryAsync(string eventName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.LatencyLatest(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LatencyLatestEntry[] -StackExchange.Redis.IServer.LatencyLatestAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.LatencyReset(string[] eventNames = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IServer.LatencyResetAsync(string[] eventNames = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.MakeMaster(StackExchange.Redis.ReplicationChangeOptions options, System.IO.TextWriter log = null) -> void -StackExchange.Redis.IServer.MakePrimaryAsync(StackExchange.Redis.ReplicationChangeOptions options, System.IO.TextWriter log = null) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.MemoryAllocatorStats(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string -StackExchange.Redis.IServer.MemoryAllocatorStatsAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.MemoryDoctor(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string -StackExchange.Redis.IServer.MemoryDoctorAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.LastSaveAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.LatencyDoctor(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string! +StackExchange.Redis.IServer.LatencyDoctorAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.LatencyHistory(string! eventName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LatencyHistoryEntry[]! +StackExchange.Redis.IServer.LatencyHistoryAsync(string! eventName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.LatencyLatest(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LatencyLatestEntry[]! +StackExchange.Redis.IServer.LatencyLatestAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.LatencyReset(string![]? eventNames = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IServer.LatencyResetAsync(string![]? eventNames = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.MakeMaster(StackExchange.Redis.ReplicationChangeOptions options, System.IO.TextWriter? log = null) -> void +StackExchange.Redis.IServer.MakePrimaryAsync(StackExchange.Redis.ReplicationChangeOptions options, System.IO.TextWriter? log = null) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.MemoryAllocatorStats(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? +StackExchange.Redis.IServer.MemoryAllocatorStatsAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.MemoryDoctor(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string! +StackExchange.Redis.IServer.MemoryDoctorAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.MemoryPurge(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.MemoryPurgeAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.MemoryStats(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult -StackExchange.Redis.IServer.MemoryStatsAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.ReplicaOf(System.Net.EndPoint master, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.ReplicaOfAsync(System.Net.EndPoint master, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.Role(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Role -StackExchange.Redis.IServer.RoleAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.MemoryPurgeAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.MemoryStats(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.IServer.MemoryStatsAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.ReplicaOf(System.Net.EndPoint! master, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.ReplicaOfAsync(System.Net.EndPoint! master, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.Role(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Role! +StackExchange.Redis.IServer.RoleAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.Save(StackExchange.Redis.SaveType type, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.SaveAsync(StackExchange.Redis.SaveType type, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.ScriptExists(byte[] sha1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IServer.ScriptExists(string script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IServer.ScriptExistsAsync(byte[] sha1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.ScriptExistsAsync(string script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SaveAsync(StackExchange.Redis.SaveType type, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.ScriptExists(byte[]! sha1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IServer.ScriptExists(string! script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IServer.ScriptExistsAsync(byte[]! sha1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.ScriptExistsAsync(string! script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.ScriptFlush(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.ScriptFlushAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.ScriptLoad(StackExchange.Redis.LuaScript script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LoadedLuaScript -StackExchange.Redis.IServer.ScriptLoad(string script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> byte[] -StackExchange.Redis.IServer.ScriptLoadAsync(StackExchange.Redis.LuaScript script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.ScriptLoadAsync(string script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.SentinelFailover(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.SentinelFailoverAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.SentinelGetMasterAddressByName(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint -StackExchange.Redis.IServer.SentinelGetMasterAddressByNameAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.SentinelGetReplicaAddresses(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint[] -StackExchange.Redis.IServer.SentinelGetReplicaAddressesAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.SentinelGetSentinelAddresses(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint[] -StackExchange.Redis.IServer.SentinelGetSentinelAddressesAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.SentinelMaster(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[] -StackExchange.Redis.IServer.SentinelMasterAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[]> -StackExchange.Redis.IServer.SentinelMasters(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[][] -StackExchange.Redis.IServer.SentinelMastersAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[][]> -StackExchange.Redis.IServer.SentinelReplicas(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[][] -StackExchange.Redis.IServer.SentinelReplicasAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[][]> -StackExchange.Redis.IServer.SentinelSentinels(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[][] -StackExchange.Redis.IServer.SentinelSentinelsAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[][]> -StackExchange.Redis.IServer.SentinelSlaves(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[][] -StackExchange.Redis.IServer.SentinelSlavesAsync(string serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[][]> +StackExchange.Redis.IServer.ScriptFlushAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.ScriptLoad(StackExchange.Redis.LuaScript! script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LoadedLuaScript! +StackExchange.Redis.IServer.ScriptLoad(string! script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> byte[]! +StackExchange.Redis.IServer.ScriptLoadAsync(StackExchange.Redis.LuaScript! script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.ScriptLoadAsync(string! script, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.SentinelFailover(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.SentinelFailoverAsync(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.SentinelGetMasterAddressByName(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint? +StackExchange.Redis.IServer.SentinelGetMasterAddressByNameAsync(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.SentinelGetReplicaAddresses(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint![]! +StackExchange.Redis.IServer.SentinelGetReplicaAddressesAsync(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.SentinelGetSentinelAddresses(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint![]! +StackExchange.Redis.IServer.SentinelGetSentinelAddressesAsync(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.SentinelMaster(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[]! +StackExchange.Redis.IServer.SentinelMasterAsync(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[]!>! +StackExchange.Redis.IServer.SentinelMasters(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[]![]! +StackExchange.Redis.IServer.SentinelMastersAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[]![]!>! +StackExchange.Redis.IServer.SentinelReplicas(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[]![]! +StackExchange.Redis.IServer.SentinelReplicasAsync(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[]![]!>! +StackExchange.Redis.IServer.SentinelSentinels(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[]![]! +StackExchange.Redis.IServer.SentinelSentinelsAsync(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[]![]!>! +StackExchange.Redis.IServer.SentinelSlaves(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[]![]! +StackExchange.Redis.IServer.SentinelSlavesAsync(string! serviceName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[]![]!>! StackExchange.Redis.IServer.ServerType.get -> StackExchange.Redis.ServerType StackExchange.Redis.IServer.Shutdown(StackExchange.Redis.ShutdownMode shutdownMode = StackExchange.Redis.ShutdownMode.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.SlaveOf(System.Net.EndPoint master, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.SlaveOfAsync(System.Net.EndPoint master, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.SlowlogGet(int count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.CommandTrace[] -StackExchange.Redis.IServer.SlowlogGetAsync(int count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SlaveOf(System.Net.EndPoint! master, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.SlaveOfAsync(System.Net.EndPoint! master, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.SlowlogGet(int count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.CommandTrace![]! +StackExchange.Redis.IServer.SlowlogGetAsync(int count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.SlowlogReset(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.SlowlogResetAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.SubscriptionChannels(StackExchange.Redis.RedisChannel pattern = default(StackExchange.Redis.RedisChannel), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisChannel[] -StackExchange.Redis.IServer.SubscriptionChannelsAsync(StackExchange.Redis.RedisChannel pattern = default(StackExchange.Redis.RedisChannel), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SlowlogResetAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.SubscriptionChannels(StackExchange.Redis.RedisChannel pattern = default(StackExchange.Redis.RedisChannel), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisChannel[]! +StackExchange.Redis.IServer.SubscriptionChannelsAsync(StackExchange.Redis.RedisChannel pattern = default(StackExchange.Redis.RedisChannel), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.SubscriptionPatternCount(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IServer.SubscriptionPatternCountAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SubscriptionPatternCountAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.SubscriptionSubscriberCount(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IServer.SubscriptionSubscriberCountAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SubscriptionSubscriberCountAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.SwapDatabases(int first, int second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.IServer.SwapDatabasesAsync(int first, int second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.IServer.SwapDatabasesAsync(int first, int second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.Time(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.DateTime -StackExchange.Redis.IServer.TimeAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.IServer.Version.get -> System.Version +StackExchange.Redis.IServer.TimeAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.Version.get -> System.Version! StackExchange.Redis.ISubscriber -StackExchange.Redis.ISubscriber.IdentifyEndpoint(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint -StackExchange.Redis.ISubscriber.IdentifyEndpointAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.ISubscriber.IdentifyEndpoint(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint? +StackExchange.Redis.ISubscriber.IdentifyEndpointAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.ISubscriber.IsConnected(StackExchange.Redis.RedisChannel channel = default(StackExchange.Redis.RedisChannel)) -> bool StackExchange.Redis.ISubscriber.Publish(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.ISubscriber.PublishAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.ISubscriber.Subscribe(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ChannelMessageQueue -StackExchange.Redis.ISubscriber.Subscribe(StackExchange.Redis.RedisChannel channel, System.Action handler, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.ISubscriber.SubscribeAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.ISubscriber.SubscribeAsync(StackExchange.Redis.RedisChannel channel, System.Action handler, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.ISubscriber.SubscribedEndpoint(StackExchange.Redis.RedisChannel channel) -> System.Net.EndPoint -StackExchange.Redis.ISubscriber.Unsubscribe(StackExchange.Redis.RedisChannel channel, System.Action handler = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.ISubscriber.PublishAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.RedisValue message, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.ISubscriber.Subscribe(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ChannelMessageQueue! +StackExchange.Redis.ISubscriber.Subscribe(StackExchange.Redis.RedisChannel channel, System.Action! handler, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.ISubscriber.SubscribeAsync(StackExchange.Redis.RedisChannel channel, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.ISubscriber.SubscribeAsync(StackExchange.Redis.RedisChannel channel, System.Action! handler, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.ISubscriber.SubscribedEndpoint(StackExchange.Redis.RedisChannel channel) -> System.Net.EndPoint? +StackExchange.Redis.ISubscriber.Unsubscribe(StackExchange.Redis.RedisChannel channel, System.Action? handler = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.ISubscriber.UnsubscribeAll(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -StackExchange.Redis.ISubscriber.UnsubscribeAllAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.ISubscriber.UnsubscribeAsync(StackExchange.Redis.RedisChannel channel, System.Action handler = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.ISubscriber.UnsubscribeAllAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.ISubscriber.UnsubscribeAsync(StackExchange.Redis.RedisChannel channel, System.Action? handler = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.ITransaction -StackExchange.Redis.ITransaction.AddCondition(StackExchange.Redis.Condition condition) -> StackExchange.Redis.ConditionResult +StackExchange.Redis.ITransaction.AddCondition(StackExchange.Redis.Condition! condition) -> StackExchange.Redis.ConditionResult! StackExchange.Redis.ITransaction.Execute(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.ITransaction.ExecuteAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task +StackExchange.Redis.ITransaction.ExecuteAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.KeyspaceIsolation.DatabaseExtensions StackExchange.Redis.LatencyHistoryEntry StackExchange.Redis.LatencyHistoryEntry.DurationMilliseconds.get -> int @@ -1017,7 +1018,7 @@ StackExchange.Redis.LatencyHistoryEntry.LatencyHistoryEntry() -> void StackExchange.Redis.LatencyHistoryEntry.Timestamp.get -> System.DateTime StackExchange.Redis.LatencyLatestEntry StackExchange.Redis.LatencyLatestEntry.DurationMilliseconds.get -> int -StackExchange.Redis.LatencyLatestEntry.EventName.get -> string +StackExchange.Redis.LatencyLatestEntry.EventName.get -> string! StackExchange.Redis.LatencyLatestEntry.LatencyLatestEntry() -> void StackExchange.Redis.LatencyLatestEntry.MaxDurationMilliseconds.get -> int StackExchange.Redis.LatencyLatestEntry.Timestamp.get -> System.DateTime @@ -1031,24 +1032,24 @@ StackExchange.Redis.LinearRetry StackExchange.Redis.LinearRetry.LinearRetry(int maxRetryElapsedTimeAllowedMilliseconds) -> void StackExchange.Redis.LinearRetry.ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSinceLastRetry) -> bool StackExchange.Redis.LoadedLuaScript -StackExchange.Redis.LoadedLuaScript.Evaluate(StackExchange.Redis.IDatabase db, object ps = null, StackExchange.Redis.RedisKey? withKeyPrefix = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult -StackExchange.Redis.LoadedLuaScript.EvaluateAsync(StackExchange.Redis.IDatabaseAsync db, object ps = null, StackExchange.Redis.RedisKey? withKeyPrefix = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.LoadedLuaScript.ExecutableScript.get -> string -StackExchange.Redis.LoadedLuaScript.Hash.get -> byte[] -StackExchange.Redis.LoadedLuaScript.OriginalScript.get -> string +StackExchange.Redis.LoadedLuaScript.Evaluate(StackExchange.Redis.IDatabase! db, object? ps = null, StackExchange.Redis.RedisKey? withKeyPrefix = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.LoadedLuaScript.EvaluateAsync(StackExchange.Redis.IDatabaseAsync! db, object? ps = null, StackExchange.Redis.RedisKey? withKeyPrefix = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.LoadedLuaScript.ExecutableScript.get -> string! +StackExchange.Redis.LoadedLuaScript.Hash.get -> byte[]! +StackExchange.Redis.LoadedLuaScript.OriginalScript.get -> string! StackExchange.Redis.LuaScript -StackExchange.Redis.LuaScript.Evaluate(StackExchange.Redis.IDatabase db, object ps = null, StackExchange.Redis.RedisKey? withKeyPrefix = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult -StackExchange.Redis.LuaScript.EvaluateAsync(StackExchange.Redis.IDatabaseAsync db, object ps = null, StackExchange.Redis.RedisKey? withKeyPrefix = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.LuaScript.ExecutableScript.get -> string -StackExchange.Redis.LuaScript.Load(StackExchange.Redis.IServer server, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LoadedLuaScript -StackExchange.Redis.LuaScript.LoadAsync(StackExchange.Redis.IServer server, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task -StackExchange.Redis.LuaScript.OriginalScript.get -> string +StackExchange.Redis.LuaScript.Evaluate(StackExchange.Redis.IDatabase! db, object? ps = null, StackExchange.Redis.RedisKey? withKeyPrefix = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.LuaScript.EvaluateAsync(StackExchange.Redis.IDatabaseAsync! db, object? ps = null, StackExchange.Redis.RedisKey? withKeyPrefix = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.LuaScript.ExecutableScript.get -> string! +StackExchange.Redis.LuaScript.Load(StackExchange.Redis.IServer! server, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LoadedLuaScript! +StackExchange.Redis.LuaScript.LoadAsync(StackExchange.Redis.IServer! server, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.LuaScript.OriginalScript.get -> string! StackExchange.Redis.Maintenance.AzureMaintenanceEvent -StackExchange.Redis.Maintenance.AzureMaintenanceEvent.IPAddress.get -> System.Net.IPAddress +StackExchange.Redis.Maintenance.AzureMaintenanceEvent.IPAddress.get -> System.Net.IPAddress? StackExchange.Redis.Maintenance.AzureMaintenanceEvent.IsReplica.get -> bool StackExchange.Redis.Maintenance.AzureMaintenanceEvent.NonSslPort.get -> int StackExchange.Redis.Maintenance.AzureMaintenanceEvent.NotificationType.get -> StackExchange.Redis.Maintenance.AzureNotificationType -StackExchange.Redis.Maintenance.AzureMaintenanceEvent.NotificationTypeString.get -> string +StackExchange.Redis.Maintenance.AzureMaintenanceEvent.NotificationTypeString.get -> string! StackExchange.Redis.Maintenance.AzureMaintenanceEvent.SslPort.get -> int StackExchange.Redis.Maintenance.AzureNotificationType StackExchange.Redis.Maintenance.AzureNotificationType.NodeMaintenanceEnded = 4 -> StackExchange.Redis.Maintenance.AzureNotificationType @@ -1059,7 +1060,7 @@ StackExchange.Redis.Maintenance.AzureNotificationType.NodeMaintenanceStart = 3 - StackExchange.Redis.Maintenance.AzureNotificationType.NodeMaintenanceStarting = 2 -> StackExchange.Redis.Maintenance.AzureNotificationType StackExchange.Redis.Maintenance.AzureNotificationType.Unknown = 0 -> StackExchange.Redis.Maintenance.AzureNotificationType StackExchange.Redis.Maintenance.ServerMaintenanceEvent -StackExchange.Redis.Maintenance.ServerMaintenanceEvent.RawMessage.get -> string +StackExchange.Redis.Maintenance.ServerMaintenanceEvent.RawMessage.get -> string? StackExchange.Redis.Maintenance.ServerMaintenanceEvent.ReceivedTimeUtc.get -> System.DateTime StackExchange.Redis.Maintenance.ServerMaintenanceEvent.StartTimeUtc.get -> System.DateTime? StackExchange.Redis.MigrateOptions @@ -1076,35 +1077,35 @@ StackExchange.Redis.Order StackExchange.Redis.Order.Ascending = 0 -> StackExchange.Redis.Order StackExchange.Redis.Order.Descending = 1 -> StackExchange.Redis.Order StackExchange.Redis.Profiling.IProfiledCommand -StackExchange.Redis.Profiling.IProfiledCommand.Command.get -> string +StackExchange.Redis.Profiling.IProfiledCommand.Command.get -> string! StackExchange.Redis.Profiling.IProfiledCommand.CommandCreated.get -> System.DateTime StackExchange.Redis.Profiling.IProfiledCommand.CreationToEnqueued.get -> System.TimeSpan StackExchange.Redis.Profiling.IProfiledCommand.Db.get -> int StackExchange.Redis.Profiling.IProfiledCommand.ElapsedTime.get -> System.TimeSpan -StackExchange.Redis.Profiling.IProfiledCommand.EndPoint.get -> System.Net.EndPoint +StackExchange.Redis.Profiling.IProfiledCommand.EndPoint.get -> System.Net.EndPoint! StackExchange.Redis.Profiling.IProfiledCommand.EnqueuedToSending.get -> System.TimeSpan StackExchange.Redis.Profiling.IProfiledCommand.Flags.get -> StackExchange.Redis.CommandFlags StackExchange.Redis.Profiling.IProfiledCommand.ResponseToCompletion.get -> System.TimeSpan -StackExchange.Redis.Profiling.IProfiledCommand.RetransmissionOf.get -> StackExchange.Redis.Profiling.IProfiledCommand +StackExchange.Redis.Profiling.IProfiledCommand.RetransmissionOf.get -> StackExchange.Redis.Profiling.IProfiledCommand? StackExchange.Redis.Profiling.IProfiledCommand.RetransmissionReason.get -> StackExchange.Redis.RetransmissionReasonType? StackExchange.Redis.Profiling.IProfiledCommand.SentToResponse.get -> System.TimeSpan StackExchange.Redis.Profiling.ProfiledCommandEnumerable StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Count() -> int -StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Count(System.Func predicate) -> int +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Count(System.Func! predicate) -> int StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator -StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator.Current.get -> StackExchange.Redis.Profiling.IProfiledCommand +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator.Current.get -> StackExchange.Redis.Profiling.IProfiledCommand! StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator.Dispose() -> void StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator.Enumerator() -> void StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator.MoveNext() -> bool StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator.Reset() -> void StackExchange.Redis.Profiling.ProfiledCommandEnumerable.GetEnumerator() -> StackExchange.Redis.Profiling.ProfiledCommandEnumerable.Enumerator StackExchange.Redis.Profiling.ProfiledCommandEnumerable.ProfiledCommandEnumerable() -> void -StackExchange.Redis.Profiling.ProfiledCommandEnumerable.ToArray() -> StackExchange.Redis.Profiling.IProfiledCommand[] -StackExchange.Redis.Profiling.ProfiledCommandEnumerable.ToList() -> System.Collections.Generic.List +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.ToArray() -> StackExchange.Redis.Profiling.IProfiledCommand![]! +StackExchange.Redis.Profiling.ProfiledCommandEnumerable.ToList() -> System.Collections.Generic.List! StackExchange.Redis.Profiling.ProfilingSession StackExchange.Redis.Profiling.ProfilingSession.FinishProfiling() -> StackExchange.Redis.Profiling.ProfiledCommandEnumerable -StackExchange.Redis.Profiling.ProfilingSession.ProfilingSession(object userToken = null) -> void -StackExchange.Redis.Profiling.ProfilingSession.UserToken.get -> object +StackExchange.Redis.Profiling.ProfilingSession.ProfilingSession(object? userToken = null) -> void +StackExchange.Redis.Profiling.ProfilingSession.UserToken.get -> object? StackExchange.Redis.Proxy StackExchange.Redis.Proxy.Envoyproxy = 2 -> StackExchange.Redis.Proxy StackExchange.Redis.Proxy.None = 0 -> StackExchange.Redis.Proxy @@ -1117,25 +1118,25 @@ StackExchange.Redis.RedisChannel.PatternMode.Auto = 0 -> StackExchange.Redis.Red StackExchange.Redis.RedisChannel.PatternMode.Literal = 1 -> StackExchange.Redis.RedisChannel.PatternMode StackExchange.Redis.RedisChannel.PatternMode.Pattern = 2 -> StackExchange.Redis.RedisChannel.PatternMode StackExchange.Redis.RedisChannel.RedisChannel() -> void -StackExchange.Redis.RedisChannel.RedisChannel(byte[] value, StackExchange.Redis.RedisChannel.PatternMode mode) -> void -StackExchange.Redis.RedisChannel.RedisChannel(string value, StackExchange.Redis.RedisChannel.PatternMode mode) -> void +StackExchange.Redis.RedisChannel.RedisChannel(byte[]? value, StackExchange.Redis.RedisChannel.PatternMode mode) -> void +StackExchange.Redis.RedisChannel.RedisChannel(string! value, StackExchange.Redis.RedisChannel.PatternMode mode) -> void StackExchange.Redis.RedisCommandException -StackExchange.Redis.RedisCommandException.RedisCommandException(string message) -> void -StackExchange.Redis.RedisCommandException.RedisCommandException(string message, System.Exception innerException) -> void +StackExchange.Redis.RedisCommandException.RedisCommandException(string! message) -> void +StackExchange.Redis.RedisCommandException.RedisCommandException(string! message, System.Exception! innerException) -> void StackExchange.Redis.RedisConnectionException StackExchange.Redis.RedisConnectionException.CommandStatus.get -> StackExchange.Redis.CommandStatus StackExchange.Redis.RedisConnectionException.FailureType.get -> StackExchange.Redis.ConnectionFailureType -StackExchange.Redis.RedisConnectionException.RedisConnectionException(StackExchange.Redis.ConnectionFailureType failureType, string message) -> void -StackExchange.Redis.RedisConnectionException.RedisConnectionException(StackExchange.Redis.ConnectionFailureType failureType, string message, System.Exception innerException) -> void -StackExchange.Redis.RedisConnectionException.RedisConnectionException(StackExchange.Redis.ConnectionFailureType failureType, string message, System.Exception innerException, StackExchange.Redis.CommandStatus commandStatus) -> void +StackExchange.Redis.RedisConnectionException.RedisConnectionException(StackExchange.Redis.ConnectionFailureType failureType, string! message) -> void +StackExchange.Redis.RedisConnectionException.RedisConnectionException(StackExchange.Redis.ConnectionFailureType failureType, string! message, System.Exception? innerException) -> void +StackExchange.Redis.RedisConnectionException.RedisConnectionException(StackExchange.Redis.ConnectionFailureType failureType, string! message, System.Exception? innerException, StackExchange.Redis.CommandStatus commandStatus) -> void StackExchange.Redis.RedisErrorEventArgs -StackExchange.Redis.RedisErrorEventArgs.EndPoint.get -> System.Net.EndPoint -StackExchange.Redis.RedisErrorEventArgs.Message.get -> string -StackExchange.Redis.RedisErrorEventArgs.RedisErrorEventArgs(object sender, System.Net.EndPoint endpoint, string message) -> void +StackExchange.Redis.RedisErrorEventArgs.EndPoint.get -> System.Net.EndPoint! +StackExchange.Redis.RedisErrorEventArgs.Message.get -> string! +StackExchange.Redis.RedisErrorEventArgs.RedisErrorEventArgs(object! sender, System.Net.EndPoint! endpoint, string! message) -> void StackExchange.Redis.RedisException -StackExchange.Redis.RedisException.RedisException(string message) -> void -StackExchange.Redis.RedisException.RedisException(string message, System.Exception innerException) -> void -StackExchange.Redis.RedisException.RedisException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext ctx) -> void +StackExchange.Redis.RedisException.RedisException(string! message) -> void +StackExchange.Redis.RedisException.RedisException(string! message, System.Exception? innerException) -> void +StackExchange.Redis.RedisException.RedisException(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext ctx) -> void StackExchange.Redis.RedisFeatures StackExchange.Redis.RedisFeatures.BitwiseOperations.get -> bool StackExchange.Redis.RedisFeatures.ClientName.get -> bool @@ -1159,7 +1160,7 @@ StackExchange.Redis.RedisFeatures.Persist.get -> bool StackExchange.Redis.RedisFeatures.PushIfNotExists.get -> bool StackExchange.Redis.RedisFeatures.PushMultiple.get -> bool StackExchange.Redis.RedisFeatures.RedisFeatures() -> void -StackExchange.Redis.RedisFeatures.RedisFeatures(System.Version version) -> void +StackExchange.Redis.RedisFeatures.RedisFeatures(System.Version! version) -> void StackExchange.Redis.RedisFeatures.ReplicaCommands.get -> bool StackExchange.Redis.RedisFeatures.Scan.get -> bool StackExchange.Redis.RedisFeatures.Scripting.get -> bool @@ -1178,25 +1179,25 @@ StackExchange.Redis.RedisFeatures.StringSetRange.get -> bool StackExchange.Redis.RedisFeatures.SwapDB.get -> bool StackExchange.Redis.RedisFeatures.Time.get -> bool StackExchange.Redis.RedisFeatures.Unlink.get -> bool -StackExchange.Redis.RedisFeatures.Version.get -> System.Version +StackExchange.Redis.RedisFeatures.Version.get -> System.Version! StackExchange.Redis.RedisKey StackExchange.Redis.RedisKey.Append(StackExchange.Redis.RedisKey suffix) -> StackExchange.Redis.RedisKey StackExchange.Redis.RedisKey.Equals(StackExchange.Redis.RedisKey other) -> bool StackExchange.Redis.RedisKey.Prepend(StackExchange.Redis.RedisKey prefix) -> StackExchange.Redis.RedisKey StackExchange.Redis.RedisKey.RedisKey() -> void -StackExchange.Redis.RedisKey.RedisKey(string key) -> void +StackExchange.Redis.RedisKey.RedisKey(string? key) -> void StackExchange.Redis.RedisResult StackExchange.Redis.RedisResult.RedisResult() -> void -StackExchange.Redis.RedisResult.ToDictionary(System.Collections.Generic.IEqualityComparer comparer = null) -> System.Collections.Generic.Dictionary +StackExchange.Redis.RedisResult.ToDictionary(System.Collections.Generic.IEqualityComparer? comparer = null) -> System.Collections.Generic.Dictionary! StackExchange.Redis.RedisServerException -StackExchange.Redis.RedisServerException.RedisServerException(string message) -> void +StackExchange.Redis.RedisServerException.RedisServerException(string! message) -> void StackExchange.Redis.RedisStream -StackExchange.Redis.RedisStream.Entries.get -> StackExchange.Redis.StreamEntry[] +StackExchange.Redis.RedisStream.Entries.get -> StackExchange.Redis.StreamEntry[]! StackExchange.Redis.RedisStream.Key.get -> StackExchange.Redis.RedisKey StackExchange.Redis.RedisStream.RedisStream() -> void StackExchange.Redis.RedisTimeoutException StackExchange.Redis.RedisTimeoutException.Commandstatus.get -> StackExchange.Redis.CommandStatus -StackExchange.Redis.RedisTimeoutException.RedisTimeoutException(string message, StackExchange.Redis.CommandStatus commandStatus) -> void +StackExchange.Redis.RedisTimeoutException.RedisTimeoutException(string! message, StackExchange.Redis.CommandStatus commandStatus) -> void StackExchange.Redis.RedisType StackExchange.Redis.RedisType.Hash = 5 -> StackExchange.Redis.RedisType StackExchange.Redis.RedisType.List = 2 -> StackExchange.Redis.RedisType @@ -1207,7 +1208,7 @@ StackExchange.Redis.RedisType.Stream = 6 -> StackExchange.Redis.RedisType StackExchange.Redis.RedisType.String = 1 -> StackExchange.Redis.RedisType StackExchange.Redis.RedisType.Unknown = 7 -> StackExchange.Redis.RedisType StackExchange.Redis.RedisValue -StackExchange.Redis.RedisValue.Box() -> object +StackExchange.Redis.RedisValue.Box() -> object? StackExchange.Redis.RedisValue.CompareTo(StackExchange.Redis.RedisValue other) -> int StackExchange.Redis.RedisValue.Equals(StackExchange.Redis.RedisValue other) -> bool StackExchange.Redis.RedisValue.HasValue.get -> bool @@ -1216,7 +1217,7 @@ StackExchange.Redis.RedisValue.IsNull.get -> bool StackExchange.Redis.RedisValue.IsNullOrEmpty.get -> bool StackExchange.Redis.RedisValue.Length() -> long StackExchange.Redis.RedisValue.RedisValue() -> void -StackExchange.Redis.RedisValue.RedisValue(string value) -> void +StackExchange.Redis.RedisValue.RedisValue(string! value) -> void StackExchange.Redis.RedisValue.StartsWith(StackExchange.Redis.RedisValue value) -> bool StackExchange.Redis.RedisValue.TryParse(out double val) -> bool StackExchange.Redis.RedisValue.TryParse(out int val) -> bool @@ -1247,31 +1248,31 @@ StackExchange.Redis.RetransmissionReasonType.None = 0 -> StackExchange.Redis.Ret StackExchange.Redis.Role StackExchange.Redis.Role.Master StackExchange.Redis.Role.Master.Replica -StackExchange.Redis.Role.Master.Replica.Ip.get -> string +StackExchange.Redis.Role.Master.Replica.Ip.get -> string! StackExchange.Redis.Role.Master.Replica.Port.get -> int StackExchange.Redis.Role.Master.Replica.Replica() -> void StackExchange.Redis.Role.Master.Replica.ReplicationOffset.get -> long -StackExchange.Redis.Role.Master.Replicas.get -> System.Collections.Generic.ICollection +StackExchange.Redis.Role.Master.Replicas.get -> System.Collections.Generic.ICollection! StackExchange.Redis.Role.Master.ReplicationOffset.get -> long StackExchange.Redis.Role.Replica -StackExchange.Redis.Role.Replica.MasterIp.get -> string +StackExchange.Redis.Role.Replica.MasterIp.get -> string! StackExchange.Redis.Role.Replica.MasterPort.get -> int StackExchange.Redis.Role.Replica.ReplicationOffset.get -> long -StackExchange.Redis.Role.Replica.State.get -> string +StackExchange.Redis.Role.Replica.State.get -> string! StackExchange.Redis.Role.Sentinel -StackExchange.Redis.Role.Sentinel.MonitoredMasters.get -> System.Collections.Generic.ICollection +StackExchange.Redis.Role.Sentinel.MonitoredMasters.get -> System.Collections.Generic.ICollection! StackExchange.Redis.Role.Unknown -StackExchange.Redis.Role.Value.get -> string +StackExchange.Redis.Role.Value.get -> string! StackExchange.Redis.SaveType StackExchange.Redis.SaveType.BackgroundRewriteAppendOnlyFile = 0 -> StackExchange.Redis.SaveType StackExchange.Redis.SaveType.BackgroundSave = 1 -> StackExchange.Redis.SaveType StackExchange.Redis.SaveType.ForegroundSave = 2 -> StackExchange.Redis.SaveType StackExchange.Redis.ServerCounters -StackExchange.Redis.ServerCounters.EndPoint.get -> System.Net.EndPoint -StackExchange.Redis.ServerCounters.Interactive.get -> StackExchange.Redis.ConnectionCounters -StackExchange.Redis.ServerCounters.Other.get -> StackExchange.Redis.ConnectionCounters -StackExchange.Redis.ServerCounters.ServerCounters(System.Net.EndPoint endpoint) -> void -StackExchange.Redis.ServerCounters.Subscription.get -> StackExchange.Redis.ConnectionCounters +StackExchange.Redis.ServerCounters.EndPoint.get -> System.Net.EndPoint? +StackExchange.Redis.ServerCounters.Interactive.get -> StackExchange.Redis.ConnectionCounters! +StackExchange.Redis.ServerCounters.Other.get -> StackExchange.Redis.ConnectionCounters! +StackExchange.Redis.ServerCounters.ServerCounters(System.Net.EndPoint? endpoint) -> void +StackExchange.Redis.ServerCounters.Subscription.get -> StackExchange.Redis.ConnectionCounters! StackExchange.Redis.ServerCounters.TotalOutstanding.get -> long StackExchange.Redis.ServerType StackExchange.Redis.ServerType.Cluster = 2 -> StackExchange.Redis.ServerType @@ -1296,17 +1297,17 @@ StackExchange.Redis.SlotRange.SlotRange(int from, int to) -> void StackExchange.Redis.SlotRange.To.get -> int StackExchange.Redis.SocketManager StackExchange.Redis.SocketManager.Dispose() -> void -StackExchange.Redis.SocketManager.Name.get -> string -StackExchange.Redis.SocketManager.SocketManager(string name = null, int workerCount = 0, StackExchange.Redis.SocketManager.SocketManagerOptions options = StackExchange.Redis.SocketManager.SocketManagerOptions.None) -> void -StackExchange.Redis.SocketManager.SocketManager(string name) -> void -StackExchange.Redis.SocketManager.SocketManager(string name, bool useHighPrioritySocketThreads) -> void -StackExchange.Redis.SocketManager.SocketManager(string name, int workerCount, bool useHighPrioritySocketThreads) -> void +StackExchange.Redis.SocketManager.Name.get -> string! +StackExchange.Redis.SocketManager.SocketManager(string? name = null, int workerCount = 0, StackExchange.Redis.SocketManager.SocketManagerOptions options = StackExchange.Redis.SocketManager.SocketManagerOptions.None) -> void +StackExchange.Redis.SocketManager.SocketManager(string! name) -> void +StackExchange.Redis.SocketManager.SocketManager(string! name, bool useHighPrioritySocketThreads) -> void +StackExchange.Redis.SocketManager.SocketManager(string! name, int workerCount, bool useHighPrioritySocketThreads) -> void StackExchange.Redis.SocketManager.SocketManagerOptions StackExchange.Redis.SocketManager.SocketManagerOptions.None = 0 -> StackExchange.Redis.SocketManager.SocketManagerOptions StackExchange.Redis.SocketManager.SocketManagerOptions.UseHighPrioritySocketThreads = 1 -> StackExchange.Redis.SocketManager.SocketManagerOptions StackExchange.Redis.SocketManager.SocketManagerOptions.UseThreadPool = 2 -> StackExchange.Redis.SocketManager.SocketManagerOptions StackExchange.Redis.SortedSetEntry -StackExchange.Redis.SortedSetEntry.CompareTo(object obj) -> int +StackExchange.Redis.SortedSetEntry.CompareTo(object? obj) -> int StackExchange.Redis.SortedSetEntry.CompareTo(StackExchange.Redis.SortedSetEntry other) -> int StackExchange.Redis.SortedSetEntry.Element.get -> StackExchange.Redis.RedisValue StackExchange.Redis.SortedSetEntry.Equals(StackExchange.Redis.SortedSetEntry other) -> bool @@ -1328,20 +1329,20 @@ StackExchange.Redis.StreamConsumer.PendingMessageCount.get -> int StackExchange.Redis.StreamConsumer.StreamConsumer() -> void StackExchange.Redis.StreamConsumerInfo StackExchange.Redis.StreamConsumerInfo.IdleTimeInMilliseconds.get -> long -StackExchange.Redis.StreamConsumerInfo.Name.get -> string +StackExchange.Redis.StreamConsumerInfo.Name.get -> string! StackExchange.Redis.StreamConsumerInfo.PendingMessageCount.get -> int StackExchange.Redis.StreamConsumerInfo.StreamConsumerInfo() -> void StackExchange.Redis.StreamEntry StackExchange.Redis.StreamEntry.Id.get -> StackExchange.Redis.RedisValue StackExchange.Redis.StreamEntry.IsNull.get -> bool StackExchange.Redis.StreamEntry.StreamEntry() -> void -StackExchange.Redis.StreamEntry.StreamEntry(StackExchange.Redis.RedisValue id, StackExchange.Redis.NameValueEntry[] values) -> void +StackExchange.Redis.StreamEntry.StreamEntry(StackExchange.Redis.RedisValue id, StackExchange.Redis.NameValueEntry[]! values) -> void StackExchange.Redis.StreamEntry.this[StackExchange.Redis.RedisValue fieldName].get -> StackExchange.Redis.RedisValue -StackExchange.Redis.StreamEntry.Values.get -> StackExchange.Redis.NameValueEntry[] +StackExchange.Redis.StreamEntry.Values.get -> StackExchange.Redis.NameValueEntry[]! StackExchange.Redis.StreamGroupInfo StackExchange.Redis.StreamGroupInfo.ConsumerCount.get -> int -StackExchange.Redis.StreamGroupInfo.LastDeliveredId.get -> string -StackExchange.Redis.StreamGroupInfo.Name.get -> string +StackExchange.Redis.StreamGroupInfo.LastDeliveredId.get -> string? +StackExchange.Redis.StreamGroupInfo.Name.get -> string! StackExchange.Redis.StreamGroupInfo.PendingMessageCount.get -> int StackExchange.Redis.StreamGroupInfo.StreamGroupInfo() -> void StackExchange.Redis.StreamInfo @@ -1354,7 +1355,7 @@ StackExchange.Redis.StreamInfo.RadixTreeKeys.get -> int StackExchange.Redis.StreamInfo.RadixTreeNodes.get -> int StackExchange.Redis.StreamInfo.StreamInfo() -> void StackExchange.Redis.StreamPendingInfo -StackExchange.Redis.StreamPendingInfo.Consumers.get -> StackExchange.Redis.StreamConsumer[] +StackExchange.Redis.StreamPendingInfo.Consumers.get -> StackExchange.Redis.StreamConsumer[]! StackExchange.Redis.StreamPendingInfo.HighestPendingMessageId.get -> StackExchange.Redis.RedisValue StackExchange.Redis.StreamPendingInfo.LowestPendingMessageId.get -> StackExchange.Redis.RedisValue StackExchange.Redis.StreamPendingInfo.PendingMessageCount.get -> int @@ -1374,93 +1375,93 @@ StackExchange.Redis.When StackExchange.Redis.When.Always = 0 -> StackExchange.Redis.When StackExchange.Redis.When.Exists = 1 -> StackExchange.Redis.When StackExchange.Redis.When.NotExists = 2 -> StackExchange.Redis.When -static StackExchange.Redis.BacklogPolicy.Default.get -> StackExchange.Redis.BacklogPolicy -static StackExchange.Redis.BacklogPolicy.FailFast.get -> StackExchange.Redis.BacklogPolicy +static StackExchange.Redis.BacklogPolicy.Default.get -> StackExchange.Redis.BacklogPolicy! +static StackExchange.Redis.BacklogPolicy.FailFast.get -> StackExchange.Redis.BacklogPolicy! static StackExchange.Redis.ChannelMessage.operator !=(StackExchange.Redis.ChannelMessage left, StackExchange.Redis.ChannelMessage right) -> bool static StackExchange.Redis.ChannelMessage.operator ==(StackExchange.Redis.ChannelMessage left, StackExchange.Redis.ChannelMessage right) -> bool -static StackExchange.Redis.CommandMap.Create(System.Collections.Generic.Dictionary overrides) -> StackExchange.Redis.CommandMap -static StackExchange.Redis.CommandMap.Create(System.Collections.Generic.HashSet commands, bool available = true) -> StackExchange.Redis.CommandMap -static StackExchange.Redis.CommandMap.Default.get -> StackExchange.Redis.CommandMap -static StackExchange.Redis.CommandMap.Envoyproxy.get -> StackExchange.Redis.CommandMap -static StackExchange.Redis.CommandMap.Sentinel.get -> StackExchange.Redis.CommandMap -static StackExchange.Redis.CommandMap.SSDB.get -> StackExchange.Redis.CommandMap -static StackExchange.Redis.CommandMap.Twemproxy.get -> StackExchange.Redis.CommandMap -static StackExchange.Redis.Condition.HashEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.HashExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.HashLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.HashLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.HashLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.HashNotEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.HashNotExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.KeyExists(StackExchange.Redis.RedisKey key) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.KeyNotExists(StackExchange.Redis.RedisKey key) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.ListIndexEqual(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.ListIndexExists(StackExchange.Redis.RedisKey key, long index) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.ListIndexNotEqual(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.ListIndexNotExists(StackExchange.Redis.RedisKey key, long index) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.ListLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.ListLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.ListLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SetLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SetLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SetLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SetNotContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SortedSetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SortedSetEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.RedisValue score) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SortedSetLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SortedSetLengthEqual(StackExchange.Redis.RedisKey key, long length, double min = -Infinity, double max = Infinity) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SortedSetLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SortedSetLengthGreaterThan(StackExchange.Redis.RedisKey key, long length, double min = -Infinity, double max = Infinity) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SortedSetLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SortedSetLengthLessThan(StackExchange.Redis.RedisKey key, long length, double min = -Infinity, double max = Infinity) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SortedSetNotContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SortedSetNotEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.RedisValue score) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SortedSetScoreExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue score) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SortedSetScoreExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue score, StackExchange.Redis.RedisValue count) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SortedSetScoreNotExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue score) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.SortedSetScoreNotExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue score, StackExchange.Redis.RedisValue count) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.StreamLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.StreamLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.StreamLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.StringEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.StringLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.StringLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.StringLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition -static StackExchange.Redis.Condition.StringNotEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition -static StackExchange.Redis.Configuration.DefaultOptionsProvider.AddProvider(StackExchange.Redis.Configuration.DefaultOptionsProvider provider) -> void -static StackExchange.Redis.Configuration.DefaultOptionsProvider.ComputerName.get -> string -static StackExchange.Redis.Configuration.DefaultOptionsProvider.LibraryVersion.get -> string -static StackExchange.Redis.ConfigurationOptions.Parse(string configuration) -> StackExchange.Redis.ConfigurationOptions -static StackExchange.Redis.ConfigurationOptions.Parse(string configuration, bool ignoreUnknown) -> StackExchange.Redis.ConfigurationOptions -static StackExchange.Redis.ConnectionMultiplexer.Connect(StackExchange.Redis.ConfigurationOptions configuration, System.IO.TextWriter log = null) -> StackExchange.Redis.ConnectionMultiplexer -static StackExchange.Redis.ConnectionMultiplexer.Connect(string configuration, System.Action configure, System.IO.TextWriter log = null) -> StackExchange.Redis.ConnectionMultiplexer -static StackExchange.Redis.ConnectionMultiplexer.Connect(string configuration, System.IO.TextWriter log = null) -> StackExchange.Redis.ConnectionMultiplexer -static StackExchange.Redis.ConnectionMultiplexer.ConnectAsync(StackExchange.Redis.ConfigurationOptions configuration, System.IO.TextWriter log = null) -> System.Threading.Tasks.Task -static StackExchange.Redis.ConnectionMultiplexer.ConnectAsync(string configuration, System.Action configure, System.IO.TextWriter log = null) -> System.Threading.Tasks.Task -static StackExchange.Redis.ConnectionMultiplexer.ConnectAsync(string configuration, System.IO.TextWriter log = null) -> System.Threading.Tasks.Task -static StackExchange.Redis.ConnectionMultiplexer.Factory.get -> System.Threading.Tasks.TaskFactory +static StackExchange.Redis.CommandMap.Create(System.Collections.Generic.Dictionary? overrides) -> StackExchange.Redis.CommandMap! +static StackExchange.Redis.CommandMap.Create(System.Collections.Generic.HashSet! commands, bool available = true) -> StackExchange.Redis.CommandMap! +static StackExchange.Redis.CommandMap.Default.get -> StackExchange.Redis.CommandMap! +static StackExchange.Redis.CommandMap.Envoyproxy.get -> StackExchange.Redis.CommandMap! +static StackExchange.Redis.CommandMap.Sentinel.get -> StackExchange.Redis.CommandMap! +static StackExchange.Redis.CommandMap.SSDB.get -> StackExchange.Redis.CommandMap! +static StackExchange.Redis.CommandMap.Twemproxy.get -> StackExchange.Redis.CommandMap! +static StackExchange.Redis.Condition.HashEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.HashExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.HashLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.HashLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.HashLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.HashNotEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.HashNotExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.KeyExists(StackExchange.Redis.RedisKey key) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.KeyNotExists(StackExchange.Redis.RedisKey key) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.ListIndexEqual(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.ListIndexExists(StackExchange.Redis.RedisKey key, long index) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.ListIndexNotEqual(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.ListIndexNotExists(StackExchange.Redis.RedisKey key, long index) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.ListLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.ListLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.ListLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SetLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SetLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SetLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SetNotContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.RedisValue score) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetLengthEqual(StackExchange.Redis.RedisKey key, long length, double min = -Infinity, double max = Infinity) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetLengthGreaterThan(StackExchange.Redis.RedisKey key, long length, double min = -Infinity, double max = Infinity) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetLengthLessThan(StackExchange.Redis.RedisKey key, long length, double min = -Infinity, double max = Infinity) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetNotContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetNotEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.RedisValue score) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetScoreExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue score) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetScoreExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue score, StackExchange.Redis.RedisValue count) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetScoreNotExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue score) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetScoreNotExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue score, StackExchange.Redis.RedisValue count) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.StreamLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.StreamLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.StreamLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.StringEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.StringLengthEqual(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.StringLengthGreaterThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.StringLengthLessThan(StackExchange.Redis.RedisKey key, long length) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.StringNotEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Configuration.DefaultOptionsProvider.AddProvider(StackExchange.Redis.Configuration.DefaultOptionsProvider! provider) -> void +static StackExchange.Redis.Configuration.DefaultOptionsProvider.ComputerName.get -> string! +static StackExchange.Redis.Configuration.DefaultOptionsProvider.LibraryVersion.get -> string! +static StackExchange.Redis.ConfigurationOptions.Parse(string! configuration) -> StackExchange.Redis.ConfigurationOptions! +static StackExchange.Redis.ConfigurationOptions.Parse(string! configuration, bool ignoreUnknown) -> StackExchange.Redis.ConfigurationOptions! +static StackExchange.Redis.ConnectionMultiplexer.Connect(StackExchange.Redis.ConfigurationOptions! configuration, System.IO.TextWriter? log = null) -> StackExchange.Redis.ConnectionMultiplexer! +static StackExchange.Redis.ConnectionMultiplexer.Connect(string! configuration, System.Action! configure, System.IO.TextWriter? log = null) -> StackExchange.Redis.ConnectionMultiplexer! +static StackExchange.Redis.ConnectionMultiplexer.Connect(string! configuration, System.IO.TextWriter? log = null) -> StackExchange.Redis.ConnectionMultiplexer! +static StackExchange.Redis.ConnectionMultiplexer.ConnectAsync(StackExchange.Redis.ConfigurationOptions! configuration, System.IO.TextWriter? log = null) -> System.Threading.Tasks.Task! +static StackExchange.Redis.ConnectionMultiplexer.ConnectAsync(string! configuration, System.Action! configure, System.IO.TextWriter? log = null) -> System.Threading.Tasks.Task! +static StackExchange.Redis.ConnectionMultiplexer.ConnectAsync(string! configuration, System.IO.TextWriter? log = null) -> System.Threading.Tasks.Task! +static StackExchange.Redis.ConnectionMultiplexer.Factory.get -> System.Threading.Tasks.TaskFactory! static StackExchange.Redis.ConnectionMultiplexer.Factory.set -> void -static StackExchange.Redis.ConnectionMultiplexer.GetFeatureFlag(string flag) -> bool -static StackExchange.Redis.ConnectionMultiplexer.SentinelConnect(StackExchange.Redis.ConfigurationOptions configuration, System.IO.TextWriter log = null) -> StackExchange.Redis.ConnectionMultiplexer -static StackExchange.Redis.ConnectionMultiplexer.SentinelConnect(string configuration, System.IO.TextWriter log = null) -> StackExchange.Redis.ConnectionMultiplexer -static StackExchange.Redis.ConnectionMultiplexer.SentinelConnectAsync(StackExchange.Redis.ConfigurationOptions configuration, System.IO.TextWriter log = null) -> System.Threading.Tasks.Task -static StackExchange.Redis.ConnectionMultiplexer.SentinelConnectAsync(string configuration, System.IO.TextWriter log = null) -> System.Threading.Tasks.Task -static StackExchange.Redis.ConnectionMultiplexer.SetFeatureFlag(string flag, bool enabled) -> void -static StackExchange.Redis.EndPointCollection.ToString(System.Net.EndPoint endpoint) -> string -static StackExchange.Redis.EndPointCollection.TryParse(string endpoint) -> System.Net.EndPoint -static StackExchange.Redis.ExtensionMethods.AsStream(this StackExchange.Redis.Lease bytes, bool ownsLease = true) -> System.IO.Stream -static StackExchange.Redis.ExtensionMethods.DecodeLease(this StackExchange.Redis.Lease bytes, System.Text.Encoding encoding = null) -> StackExchange.Redis.Lease -static StackExchange.Redis.ExtensionMethods.DecodeString(this StackExchange.Redis.Lease bytes, System.Text.Encoding encoding = null) -> string -static StackExchange.Redis.ExtensionMethods.ToDictionary(this StackExchange.Redis.HashEntry[] hash) -> System.Collections.Generic.Dictionary -static StackExchange.Redis.ExtensionMethods.ToDictionary(this StackExchange.Redis.SortedSetEntry[] sortedSet) -> System.Collections.Generic.Dictionary -static StackExchange.Redis.ExtensionMethods.ToDictionary(this System.Collections.Generic.KeyValuePair[] pairs) -> System.Collections.Generic.Dictionary -static StackExchange.Redis.ExtensionMethods.ToDictionary(this System.Collections.Generic.KeyValuePair[] pairs) -> System.Collections.Generic.Dictionary -static StackExchange.Redis.ExtensionMethods.ToRedisValueArray(this string[] values) -> StackExchange.Redis.RedisValue[] -static StackExchange.Redis.ExtensionMethods.ToStringArray(this StackExchange.Redis.RedisValue[] values) -> string[] -static StackExchange.Redis.ExtensionMethods.ToStringDictionary(this StackExchange.Redis.HashEntry[] hash) -> System.Collections.Generic.Dictionary -static StackExchange.Redis.ExtensionMethods.ToStringDictionary(this StackExchange.Redis.SortedSetEntry[] sortedSet) -> System.Collections.Generic.Dictionary -static StackExchange.Redis.ExtensionMethods.ToStringDictionary(this System.Collections.Generic.KeyValuePair[] pairs) -> System.Collections.Generic.Dictionary +static StackExchange.Redis.ConnectionMultiplexer.GetFeatureFlag(string! flag) -> bool +static StackExchange.Redis.ConnectionMultiplexer.SentinelConnect(StackExchange.Redis.ConfigurationOptions! configuration, System.IO.TextWriter? log = null) -> StackExchange.Redis.ConnectionMultiplexer! +static StackExchange.Redis.ConnectionMultiplexer.SentinelConnect(string! configuration, System.IO.TextWriter? log = null) -> StackExchange.Redis.ConnectionMultiplexer! +static StackExchange.Redis.ConnectionMultiplexer.SentinelConnectAsync(StackExchange.Redis.ConfigurationOptions! configuration, System.IO.TextWriter? log = null) -> System.Threading.Tasks.Task! +static StackExchange.Redis.ConnectionMultiplexer.SentinelConnectAsync(string! configuration, System.IO.TextWriter? log = null) -> System.Threading.Tasks.Task! +static StackExchange.Redis.ConnectionMultiplexer.SetFeatureFlag(string! flag, bool enabled) -> void +static StackExchange.Redis.EndPointCollection.ToString(System.Net.EndPoint? endpoint) -> string! +static StackExchange.Redis.EndPointCollection.TryParse(string! endpoint) -> System.Net.EndPoint? +static StackExchange.Redis.ExtensionMethods.AsStream(this StackExchange.Redis.Lease? bytes, bool ownsLease = true) -> System.IO.Stream? +static StackExchange.Redis.ExtensionMethods.DecodeLease(this StackExchange.Redis.Lease? bytes, System.Text.Encoding? encoding = null) -> StackExchange.Redis.Lease? +static StackExchange.Redis.ExtensionMethods.DecodeString(this StackExchange.Redis.Lease! bytes, System.Text.Encoding? encoding = null) -> string? +static StackExchange.Redis.ExtensionMethods.ToDictionary(this StackExchange.Redis.HashEntry[]? hash) -> System.Collections.Generic.Dictionary? +static StackExchange.Redis.ExtensionMethods.ToDictionary(this StackExchange.Redis.SortedSetEntry[]? sortedSet) -> System.Collections.Generic.Dictionary? +static StackExchange.Redis.ExtensionMethods.ToDictionary(this System.Collections.Generic.KeyValuePair[]? pairs) -> System.Collections.Generic.Dictionary? +static StackExchange.Redis.ExtensionMethods.ToDictionary(this System.Collections.Generic.KeyValuePair[]? pairs) -> System.Collections.Generic.Dictionary? +static StackExchange.Redis.ExtensionMethods.ToRedisValueArray(this string![]? values) -> StackExchange.Redis.RedisValue[]? +static StackExchange.Redis.ExtensionMethods.ToStringArray(this StackExchange.Redis.RedisValue[]? values) -> string?[]? +static StackExchange.Redis.ExtensionMethods.ToStringDictionary(this StackExchange.Redis.HashEntry[]? hash) -> System.Collections.Generic.Dictionary? +static StackExchange.Redis.ExtensionMethods.ToStringDictionary(this StackExchange.Redis.SortedSetEntry[]? sortedSet) -> System.Collections.Generic.Dictionary? +static StackExchange.Redis.ExtensionMethods.ToStringDictionary(this System.Collections.Generic.KeyValuePair[]? pairs) -> System.Collections.Generic.Dictionary? static StackExchange.Redis.GeoEntry.operator !=(StackExchange.Redis.GeoEntry x, StackExchange.Redis.GeoEntry y) -> bool static StackExchange.Redis.GeoEntry.operator ==(StackExchange.Redis.GeoEntry x, StackExchange.Redis.GeoEntry y) -> bool static StackExchange.Redis.GeoPosition.operator !=(StackExchange.Redis.GeoPosition x, StackExchange.Redis.GeoPosition y) -> bool @@ -1469,77 +1470,77 @@ static StackExchange.Redis.HashEntry.implicit operator StackExchange.Redis.HashE static StackExchange.Redis.HashEntry.implicit operator System.Collections.Generic.KeyValuePair(StackExchange.Redis.HashEntry value) -> System.Collections.Generic.KeyValuePair static StackExchange.Redis.HashEntry.operator !=(StackExchange.Redis.HashEntry x, StackExchange.Redis.HashEntry y) -> bool static StackExchange.Redis.HashEntry.operator ==(StackExchange.Redis.HashEntry x, StackExchange.Redis.HashEntry y) -> bool -static StackExchange.Redis.KeyspaceIsolation.DatabaseExtensions.WithKeyPrefix(this StackExchange.Redis.IDatabase database, StackExchange.Redis.RedisKey keyPrefix) -> StackExchange.Redis.IDatabase -static StackExchange.Redis.Lease.Create(int length, bool clear = true) -> StackExchange.Redis.Lease -static StackExchange.Redis.Lease.Empty.get -> StackExchange.Redis.Lease +static StackExchange.Redis.KeyspaceIsolation.DatabaseExtensions.WithKeyPrefix(this StackExchange.Redis.IDatabase! database, StackExchange.Redis.RedisKey keyPrefix) -> StackExchange.Redis.IDatabase! +static StackExchange.Redis.Lease.Create(int length, bool clear = true) -> StackExchange.Redis.Lease! +static StackExchange.Redis.Lease.Empty.get -> StackExchange.Redis.Lease! static StackExchange.Redis.LuaScript.GetCachedScriptCount() -> int -static StackExchange.Redis.LuaScript.Prepare(string script) -> StackExchange.Redis.LuaScript +static StackExchange.Redis.LuaScript.Prepare(string! script) -> StackExchange.Redis.LuaScript! static StackExchange.Redis.LuaScript.PurgeCache() -> void static StackExchange.Redis.NameValueEntry.implicit operator StackExchange.Redis.NameValueEntry(System.Collections.Generic.KeyValuePair value) -> StackExchange.Redis.NameValueEntry static StackExchange.Redis.NameValueEntry.implicit operator System.Collections.Generic.KeyValuePair(StackExchange.Redis.NameValueEntry value) -> System.Collections.Generic.KeyValuePair static StackExchange.Redis.NameValueEntry.operator !=(StackExchange.Redis.NameValueEntry x, StackExchange.Redis.NameValueEntry y) -> bool static StackExchange.Redis.NameValueEntry.operator ==(StackExchange.Redis.NameValueEntry x, StackExchange.Redis.NameValueEntry y) -> bool -static StackExchange.Redis.RedisChannel.implicit operator byte[](StackExchange.Redis.RedisChannel key) -> byte[] -static StackExchange.Redis.RedisChannel.implicit operator StackExchange.Redis.RedisChannel(byte[] key) -> StackExchange.Redis.RedisChannel -static StackExchange.Redis.RedisChannel.implicit operator StackExchange.Redis.RedisChannel(string key) -> StackExchange.Redis.RedisChannel -static StackExchange.Redis.RedisChannel.implicit operator string(StackExchange.Redis.RedisChannel key) -> string -static StackExchange.Redis.RedisChannel.operator !=(byte[] x, StackExchange.Redis.RedisChannel y) -> bool -static StackExchange.Redis.RedisChannel.operator !=(StackExchange.Redis.RedisChannel x, byte[] y) -> bool +static StackExchange.Redis.RedisChannel.implicit operator byte[]?(StackExchange.Redis.RedisChannel key) -> byte[]? +static StackExchange.Redis.RedisChannel.implicit operator StackExchange.Redis.RedisChannel(byte[]? key) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.implicit operator StackExchange.Redis.RedisChannel(string! key) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.implicit operator string?(StackExchange.Redis.RedisChannel key) -> string? +static StackExchange.Redis.RedisChannel.operator !=(byte[]! x, StackExchange.Redis.RedisChannel y) -> bool +static StackExchange.Redis.RedisChannel.operator !=(StackExchange.Redis.RedisChannel x, byte[]! y) -> bool static StackExchange.Redis.RedisChannel.operator !=(StackExchange.Redis.RedisChannel x, StackExchange.Redis.RedisChannel y) -> bool -static StackExchange.Redis.RedisChannel.operator !=(StackExchange.Redis.RedisChannel x, string y) -> bool -static StackExchange.Redis.RedisChannel.operator !=(string x, StackExchange.Redis.RedisChannel y) -> bool -static StackExchange.Redis.RedisChannel.operator ==(byte[] x, StackExchange.Redis.RedisChannel y) -> bool -static StackExchange.Redis.RedisChannel.operator ==(StackExchange.Redis.RedisChannel x, byte[] y) -> bool +static StackExchange.Redis.RedisChannel.operator !=(StackExchange.Redis.RedisChannel x, string! y) -> bool +static StackExchange.Redis.RedisChannel.operator !=(string! x, StackExchange.Redis.RedisChannel y) -> bool +static StackExchange.Redis.RedisChannel.operator ==(byte[]! x, StackExchange.Redis.RedisChannel y) -> bool +static StackExchange.Redis.RedisChannel.operator ==(StackExchange.Redis.RedisChannel x, byte[]! y) -> bool static StackExchange.Redis.RedisChannel.operator ==(StackExchange.Redis.RedisChannel x, StackExchange.Redis.RedisChannel y) -> bool -static StackExchange.Redis.RedisChannel.operator ==(StackExchange.Redis.RedisChannel x, string y) -> bool -static StackExchange.Redis.RedisChannel.operator ==(string x, StackExchange.Redis.RedisChannel y) -> bool +static StackExchange.Redis.RedisChannel.operator ==(StackExchange.Redis.RedisChannel x, string! y) -> bool +static StackExchange.Redis.RedisChannel.operator ==(string! x, StackExchange.Redis.RedisChannel y) -> bool static StackExchange.Redis.RedisFeatures.operator !=(StackExchange.Redis.RedisFeatures left, StackExchange.Redis.RedisFeatures right) -> bool static StackExchange.Redis.RedisFeatures.operator ==(StackExchange.Redis.RedisFeatures left, StackExchange.Redis.RedisFeatures right) -> bool -static StackExchange.Redis.RedisKey.implicit operator byte[](StackExchange.Redis.RedisKey key) -> byte[] -static StackExchange.Redis.RedisKey.implicit operator StackExchange.Redis.RedisKey(byte[] key) -> StackExchange.Redis.RedisKey -static StackExchange.Redis.RedisKey.implicit operator StackExchange.Redis.RedisKey(string key) -> StackExchange.Redis.RedisKey -static StackExchange.Redis.RedisKey.implicit operator string(StackExchange.Redis.RedisKey key) -> string -static StackExchange.Redis.RedisKey.operator !=(byte[] x, StackExchange.Redis.RedisKey y) -> bool -static StackExchange.Redis.RedisKey.operator !=(StackExchange.Redis.RedisKey x, byte[] y) -> bool +static StackExchange.Redis.RedisKey.implicit operator byte[]?(StackExchange.Redis.RedisKey key) -> byte[]? +static StackExchange.Redis.RedisKey.implicit operator StackExchange.Redis.RedisKey(byte[]? key) -> StackExchange.Redis.RedisKey +static StackExchange.Redis.RedisKey.implicit operator StackExchange.Redis.RedisKey(string? key) -> StackExchange.Redis.RedisKey +static StackExchange.Redis.RedisKey.implicit operator string?(StackExchange.Redis.RedisKey key) -> string? +static StackExchange.Redis.RedisKey.operator !=(byte[]! x, StackExchange.Redis.RedisKey y) -> bool +static StackExchange.Redis.RedisKey.operator !=(StackExchange.Redis.RedisKey x, byte[]! y) -> bool static StackExchange.Redis.RedisKey.operator !=(StackExchange.Redis.RedisKey x, StackExchange.Redis.RedisKey y) -> bool -static StackExchange.Redis.RedisKey.operator !=(StackExchange.Redis.RedisKey x, string y) -> bool -static StackExchange.Redis.RedisKey.operator !=(string x, StackExchange.Redis.RedisKey y) -> bool +static StackExchange.Redis.RedisKey.operator !=(StackExchange.Redis.RedisKey x, string! y) -> bool +static StackExchange.Redis.RedisKey.operator !=(string! x, StackExchange.Redis.RedisKey y) -> bool static StackExchange.Redis.RedisKey.operator +(StackExchange.Redis.RedisKey x, StackExchange.Redis.RedisKey y) -> StackExchange.Redis.RedisKey -static StackExchange.Redis.RedisKey.operator ==(byte[] x, StackExchange.Redis.RedisKey y) -> bool -static StackExchange.Redis.RedisKey.operator ==(StackExchange.Redis.RedisKey x, byte[] y) -> bool +static StackExchange.Redis.RedisKey.operator ==(byte[]! x, StackExchange.Redis.RedisKey y) -> bool +static StackExchange.Redis.RedisKey.operator ==(StackExchange.Redis.RedisKey x, byte[]! y) -> bool static StackExchange.Redis.RedisKey.operator ==(StackExchange.Redis.RedisKey x, StackExchange.Redis.RedisKey y) -> bool -static StackExchange.Redis.RedisKey.operator ==(StackExchange.Redis.RedisKey x, string y) -> bool -static StackExchange.Redis.RedisKey.operator ==(string x, StackExchange.Redis.RedisKey y) -> bool -static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisChannel channel) -> StackExchange.Redis.RedisResult -static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisKey key) -> StackExchange.Redis.RedisResult -static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisResult[] values) -> StackExchange.Redis.RedisResult -static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue value, StackExchange.Redis.ResultType? resultType = null) -> StackExchange.Redis.RedisResult -static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[] values) -> StackExchange.Redis.RedisResult -static StackExchange.Redis.RedisResult.explicit operator bool(StackExchange.Redis.RedisResult result) -> bool -static StackExchange.Redis.RedisResult.explicit operator bool?(StackExchange.Redis.RedisResult result) -> bool? -static StackExchange.Redis.RedisResult.explicit operator bool[](StackExchange.Redis.RedisResult result) -> bool[] -static StackExchange.Redis.RedisResult.explicit operator byte[](StackExchange.Redis.RedisResult result) -> byte[] -static StackExchange.Redis.RedisResult.explicit operator byte[][](StackExchange.Redis.RedisResult result) -> byte[][] -static StackExchange.Redis.RedisResult.explicit operator double(StackExchange.Redis.RedisResult result) -> double -static StackExchange.Redis.RedisResult.explicit operator double?(StackExchange.Redis.RedisResult result) -> double? -static StackExchange.Redis.RedisResult.explicit operator double[](StackExchange.Redis.RedisResult result) -> double[] -static StackExchange.Redis.RedisResult.explicit operator int(StackExchange.Redis.RedisResult result) -> int -static StackExchange.Redis.RedisResult.explicit operator int?(StackExchange.Redis.RedisResult result) -> int? -static StackExchange.Redis.RedisResult.explicit operator int[](StackExchange.Redis.RedisResult result) -> int[] -static StackExchange.Redis.RedisResult.explicit operator long(StackExchange.Redis.RedisResult result) -> long -static StackExchange.Redis.RedisResult.explicit operator long?(StackExchange.Redis.RedisResult result) -> long? -static StackExchange.Redis.RedisResult.explicit operator long[](StackExchange.Redis.RedisResult result) -> long[] -static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisKey(StackExchange.Redis.RedisResult result) -> StackExchange.Redis.RedisKey -static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisKey[](StackExchange.Redis.RedisResult result) -> StackExchange.Redis.RedisKey[] -static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisResult[](StackExchange.Redis.RedisResult result) -> StackExchange.Redis.RedisResult[] -static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisValue(StackExchange.Redis.RedisResult result) -> StackExchange.Redis.RedisValue -static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisValue[](StackExchange.Redis.RedisResult result) -> StackExchange.Redis.RedisValue[] -static StackExchange.Redis.RedisResult.explicit operator string(StackExchange.Redis.RedisResult result) -> string -static StackExchange.Redis.RedisResult.explicit operator string[](StackExchange.Redis.RedisResult result) -> string[] -static StackExchange.Redis.RedisResult.explicit operator ulong(StackExchange.Redis.RedisResult result) -> ulong -static StackExchange.Redis.RedisResult.explicit operator ulong?(StackExchange.Redis.RedisResult result) -> ulong? -static StackExchange.Redis.RedisResult.explicit operator ulong[](StackExchange.Redis.RedisResult result) -> ulong[] -static StackExchange.Redis.RedisValue.CreateFrom(System.IO.MemoryStream stream) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisKey.operator ==(StackExchange.Redis.RedisKey x, string! y) -> bool +static StackExchange.Redis.RedisKey.operator ==(string! x, StackExchange.Redis.RedisKey y) -> bool +static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisChannel channel) -> StackExchange.Redis.RedisResult! +static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisKey key) -> StackExchange.Redis.RedisResult! +static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisResult![]! values) -> StackExchange.Redis.RedisResult! +static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue value, StackExchange.Redis.ResultType? resultType = null) -> StackExchange.Redis.RedisResult! +static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[]! values) -> StackExchange.Redis.RedisResult! +static StackExchange.Redis.RedisResult.explicit operator bool(StackExchange.Redis.RedisResult! result) -> bool +static StackExchange.Redis.RedisResult.explicit operator bool?(StackExchange.Redis.RedisResult? result) -> bool? +static StackExchange.Redis.RedisResult.explicit operator bool[]?(StackExchange.Redis.RedisResult? result) -> bool[]? +static StackExchange.Redis.RedisResult.explicit operator byte[]?(StackExchange.Redis.RedisResult? result) -> byte[]? +static StackExchange.Redis.RedisResult.explicit operator byte[]?[]?(StackExchange.Redis.RedisResult? result) -> byte[]?[]? +static StackExchange.Redis.RedisResult.explicit operator double(StackExchange.Redis.RedisResult! result) -> double +static StackExchange.Redis.RedisResult.explicit operator double?(StackExchange.Redis.RedisResult? result) -> double? +static StackExchange.Redis.RedisResult.explicit operator double[]?(StackExchange.Redis.RedisResult? result) -> double[]? +static StackExchange.Redis.RedisResult.explicit operator int(StackExchange.Redis.RedisResult! result) -> int +static StackExchange.Redis.RedisResult.explicit operator int?(StackExchange.Redis.RedisResult? result) -> int? +static StackExchange.Redis.RedisResult.explicit operator int[]?(StackExchange.Redis.RedisResult? result) -> int[]? +static StackExchange.Redis.RedisResult.explicit operator long(StackExchange.Redis.RedisResult! result) -> long +static StackExchange.Redis.RedisResult.explicit operator long?(StackExchange.Redis.RedisResult? result) -> long? +static StackExchange.Redis.RedisResult.explicit operator long[]?(StackExchange.Redis.RedisResult? result) -> long[]? +static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisKey(StackExchange.Redis.RedisResult? result) -> StackExchange.Redis.RedisKey +static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisKey[]?(StackExchange.Redis.RedisResult? result) -> StackExchange.Redis.RedisKey[]? +static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisResult![]?(StackExchange.Redis.RedisResult? result) -> StackExchange.Redis.RedisResult![]? +static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisValue(StackExchange.Redis.RedisResult? result) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisResult.explicit operator StackExchange.Redis.RedisValue[]?(StackExchange.Redis.RedisResult? result) -> StackExchange.Redis.RedisValue[]? +static StackExchange.Redis.RedisResult.explicit operator string?(StackExchange.Redis.RedisResult? result) -> string? +static StackExchange.Redis.RedisResult.explicit operator string?[]?(StackExchange.Redis.RedisResult? result) -> string?[]? +static StackExchange.Redis.RedisResult.explicit operator ulong(StackExchange.Redis.RedisResult! result) -> ulong +static StackExchange.Redis.RedisResult.explicit operator ulong?(StackExchange.Redis.RedisResult? result) -> ulong? +static StackExchange.Redis.RedisResult.explicit operator ulong[]?(StackExchange.Redis.RedisResult? result) -> ulong[]? +static StackExchange.Redis.RedisValue.CreateFrom(System.IO.MemoryStream! stream) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.EmptyString.get -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.explicit operator bool(StackExchange.Redis.RedisValue value) -> bool static StackExchange.Redis.RedisValue.explicit operator bool?(StackExchange.Redis.RedisValue value) -> bool? @@ -1557,34 +1558,34 @@ static StackExchange.Redis.RedisValue.explicit operator uint(StackExchange.Redis static StackExchange.Redis.RedisValue.explicit operator uint?(StackExchange.Redis.RedisValue value) -> uint? static StackExchange.Redis.RedisValue.explicit operator ulong(StackExchange.Redis.RedisValue value) -> ulong static StackExchange.Redis.RedisValue.explicit operator ulong?(StackExchange.Redis.RedisValue value) -> ulong? -static StackExchange.Redis.RedisValue.implicit operator byte[](StackExchange.Redis.RedisValue value) -> byte[] +static StackExchange.Redis.RedisValue.implicit operator byte[]?(StackExchange.Redis.RedisValue value) -> byte[]? static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(bool value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(bool? value) -> StackExchange.Redis.RedisValue -static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(byte[] value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(byte[]? value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(double value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(double? value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(int value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(int? value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(long value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(long? value) -> StackExchange.Redis.RedisValue -static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(string value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(string? value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(System.Memory value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(System.ReadOnlyMemory value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(uint value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(uint? value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(ulong value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.implicit operator StackExchange.Redis.RedisValue(ulong? value) -> StackExchange.Redis.RedisValue -static StackExchange.Redis.RedisValue.implicit operator string(StackExchange.Redis.RedisValue value) -> string +static StackExchange.Redis.RedisValue.implicit operator string?(StackExchange.Redis.RedisValue value) -> string? static StackExchange.Redis.RedisValue.implicit operator System.ReadOnlyMemory(StackExchange.Redis.RedisValue value) -> System.ReadOnlyMemory static StackExchange.Redis.RedisValue.Null.get -> StackExchange.Redis.RedisValue static StackExchange.Redis.RedisValue.operator !=(StackExchange.Redis.RedisValue x, StackExchange.Redis.RedisValue y) -> bool static StackExchange.Redis.RedisValue.operator ==(StackExchange.Redis.RedisValue x, StackExchange.Redis.RedisValue y) -> bool -static StackExchange.Redis.RedisValue.Unbox(object value) -> StackExchange.Redis.RedisValue +static StackExchange.Redis.RedisValue.Unbox(object? value) -> StackExchange.Redis.RedisValue static StackExchange.Redis.SlotRange.operator !=(StackExchange.Redis.SlotRange x, StackExchange.Redis.SlotRange y) -> bool static StackExchange.Redis.SlotRange.operator ==(StackExchange.Redis.SlotRange x, StackExchange.Redis.SlotRange y) -> bool -static StackExchange.Redis.SlotRange.TryParse(string range, out StackExchange.Redis.SlotRange value) -> bool -static StackExchange.Redis.SocketManager.Shared.get -> StackExchange.Redis.SocketManager -static StackExchange.Redis.SocketManager.ThreadPool.get -> StackExchange.Redis.SocketManager +static StackExchange.Redis.SlotRange.TryParse(string! range, out StackExchange.Redis.SlotRange value) -> bool +static StackExchange.Redis.SocketManager.Shared.get -> StackExchange.Redis.SocketManager! +static StackExchange.Redis.SocketManager.ThreadPool.get -> StackExchange.Redis.SocketManager! static StackExchange.Redis.SortedSetEntry.implicit operator StackExchange.Redis.SortedSetEntry(System.Collections.Generic.KeyValuePair value) -> StackExchange.Redis.SortedSetEntry static StackExchange.Redis.SortedSetEntry.implicit operator System.Collections.Generic.KeyValuePair(StackExchange.Redis.SortedSetEntry value) -> System.Collections.Generic.KeyValuePair static StackExchange.Redis.SortedSetEntry.operator !=(StackExchange.Redis.SortedSetEntry x, StackExchange.Redis.SortedSetEntry y) -> bool @@ -1593,25 +1594,25 @@ static StackExchange.Redis.StreamEntry.Null.get -> StackExchange.Redis.StreamEnt static StackExchange.Redis.StreamPosition.Beginning.get -> StackExchange.Redis.RedisValue static StackExchange.Redis.StreamPosition.NewMessages.get -> StackExchange.Redis.RedisValue virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.AbortOnConnectFail.get -> bool -virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.AfterConnectAsync(StackExchange.Redis.ConnectionMultiplexer multiplexer, System.Action log) -> System.Threading.Tasks.Task +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.AfterConnectAsync(StackExchange.Redis.ConnectionMultiplexer! multiplexer, System.Action! log) -> System.Threading.Tasks.Task! virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.AllowAdmin.get -> bool -virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.BacklogPolicy.get -> StackExchange.Redis.BacklogPolicy +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.BacklogPolicy.get -> StackExchange.Redis.BacklogPolicy! virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.CheckCertificateRevocation.get -> bool -virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.CommandMap.get -> StackExchange.Redis.CommandMap +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.CommandMap.get -> StackExchange.Redis.CommandMap? virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ConfigCheckInterval.get -> System.TimeSpan -virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ConfigurationChannel.get -> string +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ConfigurationChannel.get -> string! virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ConnectRetry.get -> int virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ConnectTimeout.get -> System.TimeSpan? -virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.DefaultVersion.get -> System.Version -virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetDefaultClientName() -> string -virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetDefaultSsl(StackExchange.Redis.EndPointCollection endPoints) -> bool -virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetSslHostFromEndpoints(StackExchange.Redis.EndPointCollection endPoints) -> string +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.DefaultVersion.get -> System.Version! +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetDefaultClientName() -> string! +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetDefaultSsl(StackExchange.Redis.EndPointCollection! endPoints) -> bool +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetSslHostFromEndpoints(StackExchange.Redis.EndPointCollection! endPoints) -> string? virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludeDetailInExceptions.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludePerformanceCountersInExceptions.get -> bool -virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IsMatch(System.Net.EndPoint endpoint) -> bool +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IsMatch(System.Net.EndPoint! endpoint) -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.KeepAliveInterval.get -> System.TimeSpan virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Proxy.get -> StackExchange.Redis.Proxy -virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ReconnectRetryPolicy.get -> StackExchange.Redis.IReconnectRetryPolicy +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ReconnectRetryPolicy.get -> StackExchange.Redis.IReconnectRetryPolicy? virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ResolveDns.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.SyncTimeout.get -> System.TimeSpan -virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.TieBreaker.get -> string +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.TieBreaker.get -> string! diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 991bcc92d..4620fe17e 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -117,7 +117,7 @@ public bool MoveNext() } public ReadOnlySequence Current { get; private set; } } - internal RedisChannel AsRedisChannel(byte[] channelPrefix, RedisChannel.PatternMode mode) + internal RedisChannel AsRedisChannel(byte[]? channelPrefix, RedisChannel.PatternMode mode) { switch (Type) { @@ -132,7 +132,7 @@ internal RedisChannel AsRedisChannel(byte[] channelPrefix, RedisChannel.PatternM byte[] copy = Payload.Slice(channelPrefix.Length).ToArray(); return new RedisChannel(copy, mode); } - return default(RedisChannel); + return default; default: throw new InvalidCastException("Cannot convert to RedisChannel: " + Type); } @@ -160,7 +160,7 @@ internal RedisValue AsRedisValue() throw new InvalidCastException("Cannot convert to RedisValue: " + Type); } - internal Lease AsLease() + internal Lease? AsLease() { if (IsNull) return null; switch (Type) @@ -181,7 +181,7 @@ internal bool IsEqual(in CommandBytes expected) return new CommandBytes(Payload).Equals(expected); } - internal unsafe bool IsEqual(byte[] expected) + internal unsafe bool IsEqual(byte[]? expected) { if (expected == null) throw new ArgumentNullException(nameof(expected)); @@ -230,7 +230,7 @@ internal bool StartsWith(byte[] expected) return true; } - internal byte[] GetBlob() + internal byte[]? GetBlob() { if (IsNull) return null; @@ -254,13 +254,13 @@ internal bool GetBoolean() internal Sequence GetItems() => _items.Cast(); [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal RedisKey[] GetItemsAsKeys() => this.ToArray((in RawResult x) => x.AsRedisKey()); + internal RedisKey[]? GetItemsAsKeys() => this.ToArray((in RawResult x) => x.AsRedisKey()); [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal RedisValue[] GetItemsAsValues() => this.ToArray((in RawResult x) => x.AsRedisValue()); + internal RedisValue[]? GetItemsAsValues() => this.ToArray((in RawResult x) => x.AsRedisValue()); [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal string[] GetItemsAsStrings() => this.ToArray((in RawResult x) => (string)x.AsRedisValue()); + internal string?[]? GetItemsAsStrings() => this.ToArray((in RawResult x) => (string?)x.AsRedisValue()); internal GeoPosition? GetItemsAsGeoPosition() { @@ -296,10 +296,10 @@ private static GeoPosition AsGeoPosition(in Sequence coords) return new GeoPosition(longitude, latitude); } - internal GeoPosition?[] GetItemsAsGeoPositionArray() - => this.ToArray((in RawResult item) => item.IsNull ? (GeoPosition?)null : AsGeoPosition(item.GetItems())); + internal GeoPosition?[]? GetItemsAsGeoPositionArray() + => this.ToArray((in RawResult item) => item.IsNull ? default : AsGeoPosition(item.GetItems())); - internal unsafe string GetString() + internal unsafe string? GetString() { if (IsNull) return null; if (Payload.IsEmpty) return ""; diff --git a/src/StackExchange.Redis/RedisBase.cs b/src/StackExchange.Redis/RedisBase.cs index 10544a024..787f18032 100644 --- a/src/StackExchange.Redis/RedisBase.cs +++ b/src/StackExchange.Redis/RedisBase.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace StackExchange.Redis @@ -7,9 +8,9 @@ internal abstract partial class RedisBase : IRedis { internal static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); internal readonly ConnectionMultiplexer multiplexer; - protected readonly object asyncState; + protected readonly object? asyncState; - internal RedisBase(ConnectionMultiplexer multiplexer, object asyncState) + internal RedisBase(ConnectionMultiplexer multiplexer, object? asyncState) { this.multiplexer = multiplexer; this.asyncState = asyncState; @@ -39,21 +40,30 @@ public virtual Task PingAsync(CommandFlags flags = CommandFlags.None) public void WaitAll(params Task[] tasks) => multiplexer.WaitAll(tasks); - internal virtual Task ExecuteAsync(Message message, ResultProcessor processor, ServerEndPoint server = null) + internal virtual Task ExecuteAsync(Message? message, ResultProcessor? processor, T defaultValue, ServerEndPoint? server = null) where T : class { - if (message == null) return CompletedTask.Default(asyncState); + if (message is null) return CompletedTask.FromDefault(defaultValue, asyncState); + multiplexer.CheckMessage(message); + return multiplexer.ExecuteAsyncImpl(message, processor, asyncState, server, defaultValue); + } + + [return: NotNullIfNotNull("defualtValue")] + internal virtual Task ExecuteAsync(Message? message, ResultProcessor? processor, ServerEndPoint? server = null) + { + if (message is null) return CompletedTask.Default(asyncState); multiplexer.CheckMessage(message); return multiplexer.ExecuteAsyncImpl(message, processor, asyncState, server); } - internal virtual T ExecuteSync(Message message, ResultProcessor processor, ServerEndPoint server = null) + [return: NotNullIfNotNull("defaultValue")] + internal virtual T? ExecuteSync(Message? message, ResultProcessor? processor, ServerEndPoint? server = null, T? defaultValue = default) { - if (message == null) return default(T); // no-op + if (message is null) return defaultValue; // no-op multiplexer.CheckMessage(message); - return multiplexer.ExecuteSyncImpl(message, processor, server); + return multiplexer.ExecuteSyncImpl(message, processor, server, defaultValue); } - internal virtual RedisFeatures GetFeatures(in RedisKey key, CommandFlags flags, out ServerEndPoint server) + internal virtual RedisFeatures GetFeatures(in RedisKey key, CommandFlags flags, out ServerEndPoint? server) { server = multiplexer.SelectServer(RedisCommand.PING, flags, key); var version = server == null ? multiplexer.RawConfig.DefaultVersion : server.Version; @@ -122,7 +132,7 @@ internal static bool IsNil(in RedisValue pattern) { if (pattern.IsNullOrEmpty) return true; if (pattern.IsInteger) return false; - byte[] rawValue = pattern; + byte[] rawValue = pattern!; return rawValue.Length == 1 && rawValue[0] == '*'; } } diff --git a/src/StackExchange.Redis/RedisBatch.cs b/src/StackExchange.Redis/RedisBatch.cs index e9790202d..3a4815a40 100644 --- a/src/StackExchange.Redis/RedisBatch.cs +++ b/src/StackExchange.Redis/RedisBatch.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis { internal class RedisBatch : RedisDatabase, IBatch { - private List pending; + private List? pending; - public RedisBatch(RedisDatabase wrapped, object asyncState) : base(wrapped.multiplexer, wrapped.Database, asyncState ?? wrapped.AsyncState) {} + public RedisBatch(RedisDatabase wrapped, object? asyncState) : base(wrapped.multiplexer, wrapped.Database, asyncState ?? wrapped.AsyncState) {} public void Execute() { @@ -20,8 +20,8 @@ public void Execute() var byBridge = new Dictionary>(); // optimisation: assume most things are in a single bridge - PhysicalBridge lastBridge = null; - List lastList = null; + PhysicalBridge? lastBridge = null; + List? lastList = null; foreach (var message in snapshot) { var server = multiplexer.SelectServer(message); @@ -38,10 +38,10 @@ public void Execute() } // identity a list - List list; + List? list; if (bridge == lastBridge) { - list = lastList; + list = lastList!; } else if (!byBridge.TryGetValue(bridge, out list)) { @@ -63,22 +63,22 @@ public void Execute() } } - internal override Task ExecuteAsync(Message message, ResultProcessor processor, ServerEndPoint server = null) + internal override Task ExecuteAsync(Message? message, ResultProcessor? processor, ServerEndPoint? server = null) where T : default { if (message == null) return CompletedTask.Default(asyncState); multiplexer.CheckMessage(message); // prepare the inner command as a task - Task task; + Task task; if (message.IsFireAndForget) { task = CompletedTask.Default(null); // F+F explicitly does not get async-state } else { - var source = TaskResultBox.Create(out var tcs, asyncState); + var source = TaskResultBox.Create(out var tcs, asyncState); task = tcs.Task; - message.SetSource(source, processor); + message.SetSource(source!, processor); } // store it @@ -86,7 +86,7 @@ internal override Task ExecuteAsync(Message message, ResultProcessor pr return task; } - internal override T ExecuteSync(Message message, ResultProcessor processor, ServerEndPoint server = null) => + internal override T ExecuteSync(Message? message, ResultProcessor? processor, ServerEndPoint? server = null, T? defaultValue = default) where T : default => throw new NotSupportedException("ExecuteSync cannot be used inside a batch"); private static void FailNoServer(ConnectionMultiplexer muxer, List messages) diff --git a/src/StackExchange.Redis/RedisChannel.cs b/src/StackExchange.Redis/RedisChannel.cs index ced2e0d04..ffd56aed4 100644 --- a/src/StackExchange.Redis/RedisChannel.cs +++ b/src/StackExchange.Redis/RedisChannel.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis /// public readonly struct RedisChannel : IEquatable { - internal readonly byte[] Value; + internal readonly byte[]? Value; internal readonly bool IsPatternBased; /// @@ -23,7 +23,7 @@ namespace StackExchange.Redis /// /// The name of the channel to create. /// The mode for name matching. - public RedisChannel(byte[] value, PatternMode mode) : this(value, DeterminePatternBased(value, mode)) {} + public RedisChannel(byte[]? value, PatternMode mode) : this(value, DeterminePatternBased(value, mode)) {} /// /// Create a new redis channel from a string, explicitly controlling the pattern mode. @@ -32,13 +32,13 @@ public RedisChannel(byte[] value, PatternMode mode) : this(value, DeterminePatte /// The mode for name matching. public RedisChannel(string value, PatternMode mode) : this(value == null ? null : Encoding.UTF8.GetBytes(value), mode) {} - private RedisChannel(byte[] value, bool isPatternBased) + private RedisChannel(byte[]? value, bool isPatternBased) { Value = value; IsPatternBased = isPatternBased; } - private static bool DeterminePatternBased(byte[] value, PatternMode mode) => mode switch + private static bool DeterminePatternBased(byte[]? value, PatternMode mode) => mode switch { PatternMode.Auto => value != null && Array.IndexOf(value, (byte)'*') >= 0, PatternMode.Literal => false, @@ -123,7 +123,7 @@ private RedisChannel(byte[] value, bool isPatternBased) /// See . /// /// The to compare to. - public override bool Equals(object obj) => obj switch + public override bool Equals(object? obj) => obj switch { RedisChannel rcObj => RedisValue.Equals(Value, rcObj.Value), string sObj => RedisValue.Equals(Value, Encoding.UTF8.GetBytes(sObj)), @@ -143,7 +143,7 @@ private RedisChannel(byte[] value, bool isPatternBased) /// /// Obtains a string representation of the channel name. /// - public override string ToString() => ((string)this) ?? "(null)"; + public override string ToString() => ((string?)this) ?? "(null)"; internal static bool AssertStarts(byte[] value, byte[] expected) { @@ -159,7 +159,7 @@ internal void AssertNotNull() if (IsNull) throw new ArgumentException("A null key is not valid in this context"); } - internal RedisChannel Clone() => (byte[])Value?.Clone(); + internal RedisChannel Clone() => (byte[]?)Value?.Clone() ?? default; /// /// The matching pattern for this channel. @@ -186,7 +186,7 @@ public enum PatternMode /// The string to get a channel from. public static implicit operator RedisChannel(string key) { - if (key == null) return default(RedisChannel); + if (key == null) return default; return new RedisChannel(Encoding.UTF8.GetBytes(key), PatternMode.Auto); } @@ -194,9 +194,9 @@ public static implicit operator RedisChannel(string key) /// Create a channel name from a . /// /// The byte array to get a channel from. - public static implicit operator RedisChannel(byte[] key) + public static implicit operator RedisChannel(byte[]? key) { - if (key == null) return default(RedisChannel); + if (key == null) return default; return new RedisChannel(key, PatternMode.Auto); } @@ -204,16 +204,19 @@ public static implicit operator RedisChannel(byte[] key) /// Obtain the channel name as a . /// /// The channel to get a byte[] from. - public static implicit operator byte[] (RedisChannel key) => key.Value; + public static implicit operator byte[]? (RedisChannel key) => key.Value; /// /// Obtain the channel name as a . /// /// The channel to get a string from. - public static implicit operator string (RedisChannel key) + public static implicit operator string? (RedisChannel key) { var arr = key.Value; - if (arr == null) return null; + if (arr == null) + { + return null; + } try { return Encoding.UTF8.GetString(arr); diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index c84c232c4..3820ed747 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -10,29 +10,29 @@ namespace StackExchange.Redis { internal class RedisDatabase : RedisBase, IDatabase { - internal RedisDatabase(ConnectionMultiplexer multiplexer, int db, object asyncState) + internal RedisDatabase(ConnectionMultiplexer multiplexer, int db, object? asyncState) : base(multiplexer, asyncState) { Database = db; } - public object AsyncState => asyncState; + public object? AsyncState => asyncState; public int Database { get; } - public IBatch CreateBatch(object asyncState) + public IBatch CreateBatch(object? asyncState) { if (this is IBatch) throw new NotSupportedException("Nested batches are not supported"); return new RedisBatch(this, asyncState); } - public ITransaction CreateTransaction(object asyncState) + public ITransaction CreateTransaction(object? asyncState) { if (this is IBatch) throw new NotSupportedException("Nested transactions are not supported"); return new RedisTransaction(this, asyncState); } - private ITransaction CreateTransactionIfAvailable(object asyncState) + private ITransaction? CreateTransactionIfAvailable(object? asyncState) { var map = multiplexer.CommandMap; if (!map.IsAvailable(RedisCommand.MULTI) || !map.IsAvailable(RedisCommand.EXEC)) @@ -110,31 +110,31 @@ public Task GeoRemoveAsync(RedisKey key, RedisValue member, CommandFlags f return ExecuteAsync(msg, ResultProcessor.NullableDouble); } - public string[] GeoHash(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) + public string?[] GeoHash(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) { if (members == null) throw new ArgumentNullException(nameof(members)); var redisValues = new RedisValue[members.Length]; for (var i = 0; i < members.Length; i++) redisValues[i] = members[i]; var msg = Message.Create(Database, flags, RedisCommand.GEOHASH, key, redisValues); - return ExecuteSync(msg, ResultProcessor.StringArray); + return ExecuteSync(msg, ResultProcessor.StringArray, defaultValue: Array.Empty()); } - public Task GeoHashAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) + public Task GeoHashAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) { if (members == null) throw new ArgumentNullException(nameof(members)); var redisValues = new RedisValue[members.Length]; for (var i = 0; i < members.Length; i++) redisValues[i] = members[i]; var msg = Message.Create(Database, flags, RedisCommand.GEOHASH, key, redisValues); - return ExecuteAsync(msg, ResultProcessor.StringArray); + return ExecuteAsync(msg, ResultProcessor.StringArray, defaultValue: Array.Empty()); } - public string GeoHash(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + public string? GeoHash(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.GEOHASH, key, member); return ExecuteSync(msg, ResultProcessor.String); } - public Task GeoHashAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + public Task GeoHashAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.GEOHASH, key, member); return ExecuteAsync(msg, ResultProcessor.String); @@ -146,7 +146,7 @@ public Task GeoHashAsync(RedisKey key, RedisValue member, CommandFlags f var redisValues = new RedisValue[members.Length]; for (var i = 0; i < members.Length; i++) redisValues[i] = members[i]; var msg = Message.Create(Database, flags, RedisCommand.GEOPOS, key, redisValues); - return ExecuteSync(msg, ResultProcessor.RedisGeoPositionArray); + return ExecuteSync(msg, ResultProcessor.RedisGeoPositionArray, defaultValue: Array.Empty()); } public Task GeoPositionAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) @@ -155,7 +155,7 @@ public Task GeoHashAsync(RedisKey key, RedisValue member, CommandFlags f var redisValues = new RedisValue[members.Length]; for (var i = 0; i < members.Length; i++) redisValues[i] = members[i]; var msg = Message.Create(Database, flags, RedisCommand.GEOPOS, key, redisValues); - return ExecuteAsync(msg, ResultProcessor.RedisGeoPositionArray); + return ExecuteAsync(msg, ResultProcessor.RedisGeoPositionArray, defaultValue: Array.Empty()); } public GeoPosition? GeoPosition(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) @@ -222,7 +222,7 @@ public GeoRadiusResult[] GeoRadius(RedisKey key, RedisValue member, double radiu { throw new ArgumentException("Member should not be a double, you likely want the GeoRadius(RedisKey, double, double, ...) overload.", nameof(member)); } - return ExecuteSync(GetGeoRadiusMessage(key, member, double.NaN, double.NaN, radius, unit, count, order, options, flags), ResultProcessor.GeoRadiusArray(options)); + return ExecuteSync(GetGeoRadiusMessage(key, member, double.NaN, double.NaN, radius, unit, count, order, options, flags), ResultProcessor.GeoRadiusArray(options), defaultValue: Array.Empty()); } public Task GeoRadiusAsync(RedisKey key, RedisValue member, double radius, GeoUnit unit, int count, Order? order, GeoRadiusOptions options, CommandFlags flags) @@ -232,17 +232,17 @@ public Task GeoRadiusAsync(RedisKey key, RedisValue member, d { throw new ArgumentException("Member should not be a double, you likely want the GeoRadius(RedisKey, double, double, ...) overload.", nameof(member)); } - return ExecuteAsync(GetGeoRadiusMessage(key, member, double.NaN, double.NaN, radius, unit, count, order, options, flags), ResultProcessor.GeoRadiusArray(options)); + return ExecuteAsync(GetGeoRadiusMessage(key, member, double.NaN, double.NaN, radius, unit, count, order, options, flags), ResultProcessor.GeoRadiusArray(options), defaultValue: Array.Empty()); } public GeoRadiusResult[] GeoRadius(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit, int count, Order? order, GeoRadiusOptions options, CommandFlags flags) { - return ExecuteSync(GetGeoRadiusMessage(key, null, longitude, latitude, radius, unit, count, order, options, flags), ResultProcessor.GeoRadiusArray(options)); + return ExecuteSync(GetGeoRadiusMessage(key, null, longitude, latitude, radius, unit, count, order, options, flags), ResultProcessor.GeoRadiusArray(options), defaultValue: Array.Empty()); } public Task GeoRadiusAsync(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit, int count, Order? order, GeoRadiusOptions options, CommandFlags flags) { - return ExecuteAsync(GetGeoRadiusMessage(key, null, longitude, latitude, radius, unit, count, order, options, flags), ResultProcessor.GeoRadiusArray(options)); + return ExecuteAsync(GetGeoRadiusMessage(key, null, longitude, latitude, radius, unit, count, order, options, flags), ResultProcessor.GeoRadiusArray(options), defaultValue: Array.Empty()); } public long HashDecrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) @@ -310,7 +310,7 @@ public RedisValue HashGet(RedisKey key, RedisValue hashField, CommandFlags flags return ExecuteSync(msg, ResultProcessor.RedisValue); } - public Lease HashGetLease(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + public Lease? HashGetLease(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.HGET, key, hashField); return ExecuteSync(msg, ResultProcessor.Lease); @@ -321,19 +321,19 @@ public RedisValue[] HashGet(RedisKey key, RedisValue[] hashFields, CommandFlags if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); if (hashFields.Length == 0) return Array.Empty(); var msg = Message.Create(Database, flags, RedisCommand.HMGET, key, hashFields); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.HGETALL, key); - return ExecuteSync(msg, ResultProcessor.HashEntryArray); + return ExecuteSync(msg, ResultProcessor.HashEntryArray, defaultValue: Array.Empty()); } public Task HashGetAllAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.HGETALL, key); - return ExecuteAsync(msg, ResultProcessor.HashEntryArray); + return ExecuteAsync(msg, ResultProcessor.HashEntryArray, defaultValue: Array.Empty()); } public Task HashGetAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) @@ -342,7 +342,7 @@ public Task HashGetAsync(RedisKey key, RedisValue hashField, Command return ExecuteAsync(msg, ResultProcessor.RedisValue); } - public Task> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + public Task?> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.HGET, key, hashField); return ExecuteAsync(msg, ResultProcessor.Lease); @@ -351,9 +351,9 @@ public Task> HashGetLeaseAsync(RedisKey key, RedisValue hashField, C public Task HashGetAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) { if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); - if (hashFields.Length == 0) return CompletedTask.FromResult(Array.Empty(), asyncState); + if (hashFields.Length == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState); var msg = Message.Create(Database, flags, RedisCommand.HMGET, key, hashFields); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public long HashIncrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) @@ -387,13 +387,13 @@ public Task HashIncrementAsync(RedisKey key, RedisValue hashField, doubl public RedisValue[] HashKeys(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.HKEYS, key); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public Task HashKeysAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.HKEYS, key); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public long HashLength(RedisKey key, CommandFlags flags = CommandFlags.None) @@ -480,13 +480,13 @@ public Task HashSetIfNotExistsAsync(RedisKey key, RedisValue hashField, Re public RedisValue[] HashValues(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.HVALS, key); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public Task HashValuesAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.HVALS, key); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public bool HyperLogLogAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) @@ -515,7 +515,7 @@ public Task HyperLogLogAddAsync(RedisKey key, RedisValue[] values, Command public long HyperLogLogLength(RedisKey key, CommandFlags flags = CommandFlags.None) { - var features = GetFeatures(key, flags, out ServerEndPoint server); + var features = GetFeatures(key, flags, out ServerEndPoint? server); var cmd = Message.Create(Database, flags, RedisCommand.PFCOUNT, key); // technically a write / primary-only command until 2.8.18 if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetPrimaryOnly(); @@ -525,7 +525,7 @@ public long HyperLogLogLength(RedisKey key, CommandFlags flags = CommandFlags.No public long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags.None) { if (keys == null) throw new ArgumentNullException(nameof(keys)); - ServerEndPoint server = null; + ServerEndPoint? server = null; var cmd = Message.Create(Database, flags, RedisCommand.PFCOUNT, keys); if (keys.Length != 0) { @@ -538,7 +538,7 @@ public long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags public Task HyperLogLogLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { - var features = GetFeatures(key, flags, out ServerEndPoint server); + var features = GetFeatures(key, flags, out ServerEndPoint? server); var cmd = Message.Create(Database, flags, RedisCommand.PFCOUNT, key); // technically a write / primary-only command until 2.8.18 if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetPrimaryOnly(); @@ -548,7 +548,7 @@ public Task HyperLogLogLengthAsync(RedisKey key, CommandFlags flags = Comm public Task HyperLogLogLengthAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) { if (keys == null) throw new ArgumentNullException(nameof(keys)); - ServerEndPoint server = null; + ServerEndPoint? server = null; var cmd = Message.Create(Database, flags, RedisCommand.PFCOUNT, keys); if (keys.Length != 0) { @@ -583,13 +583,13 @@ public Task HyperLogLogMergeAsync(RedisKey destination, RedisKey[] sourceKeys, C return ExecuteAsync(cmd, ResultProcessor.DemandOK); } - public EndPoint IdentifyEndpoint(RedisKey key = default(RedisKey), CommandFlags flags = CommandFlags.None) + public EndPoint? IdentifyEndpoint(RedisKey key = default, CommandFlags flags = CommandFlags.None) { var msg = key.IsNull ? Message.Create(-1, flags, RedisCommand.PING) : Message.Create(Database, flags, RedisCommand.EXISTS, key); return ExecuteSync(msg, ResultProcessor.ConnectionIdentity); } - public Task IdentifyEndpointAsync(RedisKey key = default(RedisKey), CommandFlags flags = CommandFlags.None) + public Task IdentifyEndpointAsync(RedisKey key = default, CommandFlags flags = CommandFlags.None) { var msg = key.IsNull ? Message.Create(-1, flags, RedisCommand.PING) : Message.Create(Database, flags, RedisCommand.EXISTS, key); return ExecuteAsync(msg, ResultProcessor.ConnectionIdentity); @@ -639,7 +639,7 @@ public Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFl return CompletedTask.Default(0); } - private RedisCommand GetDeleteCommand(RedisKey key, CommandFlags flags, out ServerEndPoint server) + private RedisCommand GetDeleteCommand(RedisKey key, CommandFlags flags, out ServerEndPoint? server) { var features = GetFeatures(key, flags, out server); if (server != null && features.Unlink && multiplexer.CommandMap.IsAvailable(RedisCommand.UNLINK)) @@ -649,13 +649,13 @@ private RedisCommand GetDeleteCommand(RedisKey key, CommandFlags flags, out Serv return RedisCommand.DEL; } - public byte[] KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None) + public byte[]? KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.DUMP, key); return ExecuteSync(msg, ResultProcessor.ByteArray); } - public Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + public Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.DUMP, key); return ExecuteAsync(msg, ResultProcessor.ByteArray); @@ -687,25 +687,25 @@ public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFl public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) { - var msg = GetExpiryMessage(key, flags, expiry, out ServerEndPoint server); + var msg = GetExpiryMessage(key, flags, expiry, out ServerEndPoint? server); return ExecuteSync(msg, ResultProcessor.Boolean, server: server); } public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) { - var msg = GetExpiryMessage(key, flags, expiry, out ServerEndPoint server); + var msg = GetExpiryMessage(key, flags, expiry, out ServerEndPoint? server); return ExecuteSync(msg, ResultProcessor.Boolean, server: server); } public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) { - var msg = GetExpiryMessage(key, flags, expiry, out ServerEndPoint server); + var msg = GetExpiryMessage(key, flags, expiry, out ServerEndPoint? server); return ExecuteAsync(msg, ResultProcessor.Boolean, server: server); } public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) { - var msg = GetExpiryMessage(key, flags, expiry, out ServerEndPoint server); + var msg = GetExpiryMessage(key, flags, expiry, out ServerEndPoint? server); return ExecuteAsync(msg, ResultProcessor.Boolean, server: server); } @@ -745,7 +745,7 @@ public KeyMigrateCommandMessage(int db, RedisKey key, EndPoint toServer, int toD : base(db, flags, RedisCommand.MIGRATE, key) { if (toServer == null) throw new ArgumentNullException(nameof(toServer)); - if (!Format.TryGetHostPort(toServer, out string toHost, out int toPort)) throw new ArgumentException($"Couldn't get host and port from {toServer}", nameof(toServer)); + if (!Format.TryGetHostPort(toServer, out string? toHost, out int? toPort)) throw new ArgumentException($"Couldn't get host and port from {toServer}", nameof(toServer)); this.toHost = toHost; this.toPort = toPort; if (toDatabase < 0) throw new ArgumentOutOfRangeException(nameof(toDatabase)); @@ -843,7 +843,7 @@ public Task KeyRestoreAsync(RedisKey key, byte[] value, TimeSpan? expiry = null, public TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None) { - var features = GetFeatures(key, flags, out ServerEndPoint server); + var features = GetFeatures(key, flags, out ServerEndPoint? server); Message msg; if (server != null && features.MillisecondExpiry && multiplexer.CommandMap.IsAvailable(RedisCommand.PTTL)) { @@ -856,7 +856,7 @@ public Task KeyRestoreAsync(RedisKey key, byte[] value, TimeSpan? expiry = null, public Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { - var features = GetFeatures(key, flags, out ServerEndPoint server); + var features = GetFeatures(key, flags, out ServerEndPoint? server); Message msg; if (server != null && features.MillisecondExpiry && multiplexer.CommandMap.IsAvailable(RedisCommand.PTTL)) { @@ -924,7 +924,7 @@ public RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.No public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.LPOP, key, count); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) @@ -936,7 +936,7 @@ public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = Comm public Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.LPOP, key, count); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public long ListLeftPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) @@ -1000,13 +1000,13 @@ public Task ListLengthAsync(RedisKey key, CommandFlags flags = CommandFlag public RedisValue[] ListRange(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.LRANGE, key, start, stop); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public Task ListRangeAsync(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.LRANGE, key, start, stop); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public long ListRemove(RedisKey key, RedisValue value, long count = 0, CommandFlags flags = CommandFlags.None) @@ -1030,7 +1030,7 @@ public RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.N public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.RPOP, key, count); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) @@ -1042,7 +1042,7 @@ public Task ListRightPopAsync(RedisKey key, CommandFlags flags = Com public Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.RPOP, key, count); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public RedisValue ListRightPopLeftPush(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) @@ -1204,17 +1204,17 @@ public Task PublishAsync(RedisChannel channel, RedisValue message, Command return ExecuteAsync(msg, ResultProcessor.Int64); } - public RedisResult ScriptEvaluate(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None) + public RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { var msg = new ScriptEvalMessage(Database, flags, script, keys, values); try { - return ExecuteSync(msg, ResultProcessor.ScriptResult); + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } catch (RedisServerException) when (msg.IsScriptUnavailable) { // could be a NOSCRIPT; for a sync call, we can re-issue that without problem - return ExecuteSync(msg, ResultProcessor.ScriptResult); + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } } @@ -1223,51 +1223,52 @@ public RedisResult Execute(string command, params object[] args) public RedisResult Execute(string command, ICollection args, CommandFlags flags = CommandFlags.None) { var msg = new ExecuteMessage(multiplexer?.CommandMap, Database, flags, command, args); - return ExecuteSync(msg, ResultProcessor.ScriptResult); + return ExecuteSync(msg, ResultProcessor.ScriptResult)!; } public Task ExecuteAsync(string command, params object[] args) => ExecuteAsync(command, args, CommandFlags.None); - public Task ExecuteAsync(string command, ICollection args, CommandFlags flags = CommandFlags.None) + + public Task ExecuteAsync(string command, ICollection? args, CommandFlags flags = CommandFlags.None) { var msg = new ExecuteMessage(multiplexer?.CommandMap, Database, flags, command, args); - return ExecuteAsync(msg, ResultProcessor.ScriptResult); + return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } - public RedisResult ScriptEvaluate(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None) + public RedisResult ScriptEvaluate(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { var msg = new ScriptEvalMessage(Database, flags, hash, keys, values); - return ExecuteSync(msg, ResultProcessor.ScriptResult); + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } - public RedisResult ScriptEvaluate(LuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None) + public RedisResult ScriptEvaluate(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) { return script.Evaluate(this, parameters, null, flags); } - public RedisResult ScriptEvaluate(LoadedLuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None) + public RedisResult ScriptEvaluate(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) { return script.Evaluate(this, parameters, null, flags); } - public Task ScriptEvaluateAsync(string script, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None) + public Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { var msg = new ScriptEvalMessage(Database, flags, script, keys, values); - return ExecuteAsync(msg, ResultProcessor.ScriptResult); + return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } - public Task ScriptEvaluateAsync(byte[] hash, RedisKey[] keys = null, RedisValue[] values = null, CommandFlags flags = CommandFlags.None) + public Task ScriptEvaluateAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { var msg = new ScriptEvalMessage(Database, flags, hash, keys, values); - return ExecuteAsync(msg, ResultProcessor.ScriptResult); + return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } - public Task ScriptEvaluateAsync(LuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None) + public Task ScriptEvaluateAsync(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) { return script.EvaluateAsync(this, parameters, null, flags); } - public Task ScriptEvaluateAsync(LoadedLuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None) + public Task ScriptEvaluateAsync(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) { return script.EvaluateAsync(this, parameters, null, flags); } @@ -1301,13 +1302,13 @@ public Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags fl public RedisValue[] SetCombine(SetOperation operation, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, SetOperationCommand(operation, false), first, second); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public RedisValue[] SetCombine(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, SetOperationCommand(operation, false), keys); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) @@ -1337,13 +1338,13 @@ public Task SetCombineAndStoreAsync(SetOperation operation, RedisKey desti public Task SetCombineAsync(SetOperation operation, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, SetOperationCommand(operation, false), first, second); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public Task SetCombineAsync(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, SetOperationCommand(operation, false), keys); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) @@ -1373,13 +1374,13 @@ public Task SetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags public RedisValue[] SetMembers(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.SMEMBERS, key); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public Task SetMembersAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.SMEMBERS, key); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public bool SetMove(RedisKey source, RedisKey destination, RedisValue value, CommandFlags flags = CommandFlags.None) @@ -1412,16 +1413,16 @@ public RedisValue[] SetPop(RedisKey key, long count, CommandFlags flags = Comman var msg = count == 1 ? Message.Create(Database, flags, RedisCommand.SPOP, key) : Message.Create(Database, flags, RedisCommand.SPOP, key, count); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public Task SetPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) { - if(count == 0) return Task.FromResult(Array.Empty()); + if(count == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState); var msg = count == 1 ? Message.Create(Database, flags, RedisCommand.SPOP, key) : Message.Create(Database, flags, RedisCommand.SPOP, key, count); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public RedisValue SetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) @@ -1439,13 +1440,13 @@ public Task SetRandomMemberAsync(RedisKey key, CommandFlags flags = public RedisValue[] SetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.SRANDMEMBER, key, count); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public Task SetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.SRANDMEMBER, key, count); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public bool SetRemove(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) @@ -1495,28 +1496,28 @@ private CursorEnumerable SetScanAsync(RedisKey key, RedisValue patte throw ExceptionFactory.NotSupported(true, RedisCommand.SSCAN); } - public RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None) + public RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) { var msg = GetSortedSetAddMessage(default(RedisKey), key, skip, take, order, sortType, by, get, flags); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } - public long SortAndStore(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None) + public long SortAndStore(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) { var msg = GetSortedSetAddMessage(destination, key, skip, take, order, sortType, by, get, flags); return ExecuteSync(msg, ResultProcessor.Int64); } - public Task SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None) + public Task SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) { var msg = GetSortedSetAddMessage(destination, key, skip, take, order, sortType, by, get, flags); return ExecuteAsync(msg, ResultProcessor.Int64); } - public Task SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default(RedisValue), RedisValue[] get = null, CommandFlags flags = CommandFlags.None) + public Task SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) { var msg = GetSortedSetAddMessage(default(RedisKey), key, skip, take, order, sortType, by, get, flags); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags) @@ -1573,7 +1574,7 @@ public long SortedSetCombineAndStore(SetOperation operation, RedisKey destinatio return ExecuteSync(msg, ResultProcessor.Int64); } - public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[] weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) + public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) { var msg = GetSortedSetCombineAndStoreCommandMessage(operation, destination, keys, weights, aggregate, flags); return ExecuteSync(msg, ResultProcessor.Int64); @@ -1585,7 +1586,7 @@ public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey return ExecuteAsync(msg, ResultProcessor.Int64); } - public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[] weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) + public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) { var msg = GetSortedSetCombineAndStoreCommandMessage(operation, destination, keys, weights, aggregate, flags); return ExecuteAsync(msg, ResultProcessor.Int64); @@ -1628,7 +1629,7 @@ public Task SortedSetLengthAsync(RedisKey key, double min = double.Negativ public RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, order == Order.Descending ? RedisCommand.ZREVRANGE : RedisCommand.ZRANGE, key, start, stop); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public long SortedSetRangeAndStore( @@ -1650,7 +1651,7 @@ public long SortedSetRangeAndStore( public Task SortedSetRangeByRankAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, order == Order.Descending ? RedisCommand.ZREVRANGE : RedisCommand.ZRANGE, key, start, stop); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public Task SortedSetRangeAndStoreAsync( @@ -1672,37 +1673,37 @@ public Task SortedSetRangeAndStoreAsync( public SortedSetEntry[] SortedSetRangeByRankWithScores(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, order == Order.Descending ? RedisCommand.ZREVRANGE : RedisCommand.ZRANGE, key, start, stop, RedisLiterals.WITHSCORES); - return ExecuteSync(msg, ResultProcessor.SortedSetWithScores); + return ExecuteSync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); } public Task SortedSetRangeByRankWithScoresAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, order == Order.Descending ? RedisCommand.ZREVRANGE : RedisCommand.ZRANGE, key, start, stop, RedisLiterals.WITHSCORES); - return ExecuteAsync(msg, ResultProcessor.SortedSetWithScores); + return ExecuteAsync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); } public RedisValue[] SortedSetRangeByScore(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) { var msg = GetSortedSetRangeByScoreMessage(key, start, stop, exclude, order, skip, take, flags, false); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public Task SortedSetRangeByScoreAsync(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) { var msg = GetSortedSetRangeByScoreMessage(key, start, stop, exclude, order, skip, take, flags, false); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public SortedSetEntry[] SortedSetRangeByScoreWithScores(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) { var msg = GetSortedSetRangeByScoreMessage(key, start, stop, exclude, order, skip, take, flags, true); - return ExecuteSync(msg, ResultProcessor.SortedSetWithScores); + return ExecuteSync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); } public Task SortedSetRangeByScoreWithScoresAsync(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) { var msg = GetSortedSetRangeByScoreMessage(key, start, stop, exclude, order, skip, take, flags, true); - return ExecuteAsync(msg, ResultProcessor.SortedSetWithScores); + return ExecuteAsync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); } public long? SortedSetRank(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) @@ -1814,16 +1815,16 @@ public SortedSetEntry[] SortedSetPop(RedisKey key, long count, Order order = Ord var msg = count == 1 ? Message.Create(Database, flags, order == Order.Descending ? RedisCommand.ZPOPMAX : RedisCommand.ZPOPMIN, key) : Message.Create(Database, flags, order == Order.Descending ? RedisCommand.ZPOPMAX : RedisCommand.ZPOPMIN, key, count); - return ExecuteSync(msg, ResultProcessor.SortedSetWithScores); + return ExecuteSync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); } public Task SortedSetPopAsync(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) { - if (count == 0) return Task.FromResult(Array.Empty()); + if (count == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState); var msg = count == 1 ? Message.Create(Database, flags, order == Order.Descending ? RedisCommand.ZPOPMAX : RedisCommand.ZPOPMIN, key) : Message.Create(Database, flags, order == Order.Descending ? RedisCommand.ZPOPMAX : RedisCommand.ZPOPMIN, key, count); - return ExecuteAsync(msg, ResultProcessor.SortedSetWithScores); + return ExecuteAsync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); } public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) @@ -1908,7 +1909,7 @@ public StreamEntry[] StreamClaim(RedisKey key, RedisValue consumerGroup, RedisVa returnJustIds: false, flags: flags); - return ExecuteSync(msg, ResultProcessor.SingleStream); + return ExecuteSync(msg, ResultProcessor.SingleStream, defaultValue: Array.Empty()); } public Task StreamClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) @@ -1921,7 +1922,7 @@ public Task StreamClaimAsync(RedisKey key, RedisValue consumerGro returnJustIds: false, flags: flags); - return ExecuteAsync(msg, ResultProcessor.SingleStream); + return ExecuteAsync(msg, ResultProcessor.SingleStream, defaultValue: Array.Empty()); } public RedisValue[] StreamClaimIdsOnly(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) @@ -1934,7 +1935,7 @@ public RedisValue[] StreamClaimIdsOnly(RedisKey key, RedisValue consumerGroup, R returnJustIds: true, flags: flags); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public Task StreamClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) @@ -1947,7 +1948,7 @@ public Task StreamClaimIdsOnlyAsync(RedisKey key, RedisValue consu returnJustIds: true, flags: flags); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public bool StreamConsumerGroupSetPosition(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None) @@ -2038,7 +2039,7 @@ public StreamConsumerInfo[] StreamConsumerInfo(RedisKey key, RedisValue groupNam groupName }); - return ExecuteSync(msg, ResultProcessor.StreamConsumerInfo); + return ExecuteSync(msg, ResultProcessor.StreamConsumerInfo, defaultValue: Array.Empty()); } public Task StreamConsumerInfoAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) @@ -2053,19 +2054,19 @@ public Task StreamConsumerInfoAsync(RedisKey key, RedisVal groupName }); - return ExecuteAsync(msg, ResultProcessor.StreamConsumerInfo); + return ExecuteAsync(msg, ResultProcessor.StreamConsumerInfo, defaultValue: Array.Empty()); } public StreamGroupInfo[] StreamGroupInfo(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.XINFO, StreamConstants.Groups, key); - return ExecuteSync(msg, ResultProcessor.StreamGroupInfo); + return ExecuteSync(msg, ResultProcessor.StreamGroupInfo, defaultValue: Array.Empty()); } public Task StreamGroupInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.XINFO, StreamConstants.Groups, key); - return ExecuteAsync(msg, ResultProcessor.StreamGroupInfo); + return ExecuteAsync(msg, ResultProcessor.StreamGroupInfo, defaultValue: Array.Empty()); } public StreamInfo StreamInfo(RedisKey key, CommandFlags flags = CommandFlags.None) @@ -2198,7 +2199,7 @@ public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue consumerName, flags); - return ExecuteSync(msg, ResultProcessor.StreamPendingMessages); + return ExecuteSync(msg, ResultProcessor.StreamPendingMessages, defaultValue: Array.Empty()); } public Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) @@ -2211,7 +2212,7 @@ public Task StreamPendingMessagesAsync(RedisKey key, consumerName, flags); - return ExecuteAsync(msg, ResultProcessor.StreamPendingMessages); + return ExecuteAsync(msg, ResultProcessor.StreamPendingMessages, defaultValue: Array.Empty()); } public StreamEntry[] StreamRange(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None) @@ -2223,7 +2224,7 @@ public StreamEntry[] StreamRange(RedisKey key, RedisValue? minId = null, RedisVa messageOrder, flags); - return ExecuteSync(msg, ResultProcessor.SingleStream); + return ExecuteSync(msg, ResultProcessor.SingleStream, defaultValue: Array.Empty()); } public Task StreamRangeAsync(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None) @@ -2235,7 +2236,7 @@ public Task StreamRangeAsync(RedisKey key, RedisValue? minId = nu messageOrder, flags); - return ExecuteAsync(msg, ResultProcessor.SingleStream); + return ExecuteAsync(msg, ResultProcessor.SingleStream, defaultValue: Array.Empty()); } public StreamEntry[] StreamRead(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None) @@ -2245,7 +2246,7 @@ public StreamEntry[] StreamRead(RedisKey key, RedisValue position, int? count = count, flags); - return ExecuteSync(msg, ResultProcessor.SingleStreamWithNameSkip); + return ExecuteSync(msg, ResultProcessor.SingleStreamWithNameSkip, defaultValue: Array.Empty()); } public Task StreamReadAsync(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None) @@ -2255,19 +2256,19 @@ public Task StreamReadAsync(RedisKey key, RedisValue position, in count, flags); - return ExecuteAsync(msg, ResultProcessor.SingleStreamWithNameSkip); + return ExecuteAsync(msg, ResultProcessor.SingleStreamWithNameSkip, defaultValue: Array.Empty()); } public RedisStream[] StreamRead(StreamPosition[] streamPositions, int? countPerStream = null, CommandFlags flags = CommandFlags.None) { var msg = GetMultiStreamReadMessage(streamPositions, countPerStream, flags); - return ExecuteSync(msg, ResultProcessor.MultiStream); + return ExecuteSync(msg, ResultProcessor.MultiStream, defaultValue: Array.Empty()); } public Task StreamReadAsync(StreamPosition[] streamPositions, int? countPerStream = null, CommandFlags flags = CommandFlags.None) { var msg = GetMultiStreamReadMessage(streamPositions, countPerStream, flags); - return ExecuteAsync(msg, ResultProcessor.MultiStream); + return ExecuteAsync(msg, ResultProcessor.MultiStream, defaultValue: Array.Empty()); } public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, CommandFlags flags) @@ -2293,7 +2294,7 @@ public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisVa noAck, flags); - return ExecuteSync(msg, ResultProcessor.SingleStreamWithNameSkip); + return ExecuteSync(msg, ResultProcessor.SingleStreamWithNameSkip, defaultValue: Array.Empty()); } public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, CommandFlags flags) @@ -2319,7 +2320,7 @@ public Task StreamReadGroupAsync(RedisKey key, RedisValue groupNa noAck, flags); - return ExecuteAsync(msg, ResultProcessor.SingleStreamWithNameSkip); + return ExecuteAsync(msg, ResultProcessor.SingleStreamWithNameSkip, defaultValue: Array.Empty()); } public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags) @@ -2341,7 +2342,7 @@ public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValu noAck, flags); - return ExecuteSync(msg, ResultProcessor.MultiStream); + return ExecuteSync(msg, ResultProcessor.MultiStream, defaultValue: Array.Empty()); } public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags) @@ -2363,7 +2364,7 @@ public Task StreamReadGroupAsync(StreamPosition[] streamPositions noAck, flags); - return ExecuteAsync(msg, ResultProcessor.MultiStream); + return ExecuteAsync(msg, ResultProcessor.MultiStream, defaultValue: Array.Empty()); } public long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) @@ -2493,10 +2494,10 @@ public RedisValue[] StringGet(RedisKey[] keys, CommandFlags flags = CommandFlags if (keys == null) throw new ArgumentNullException(nameof(keys)); if (keys.Length == 0) return Array.Empty(); var msg = Message.Create(Database, flags, RedisCommand.MGET, keys); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } - public Lease StringGetLease(RedisKey key, CommandFlags flags = CommandFlags.None) + public Lease? StringGetLease(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.GET, key); return ExecuteSync(msg, ResultProcessor.Lease); @@ -2508,7 +2509,7 @@ public Task StringGetAsync(RedisKey key, CommandFlags flags = Comman return ExecuteAsync(msg, ResultProcessor.RedisValue); } - public Task> StringGetLeaseAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + public Task?> StringGetLeaseAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.GET, key); return ExecuteAsync(msg, ResultProcessor.Lease); @@ -2517,9 +2518,9 @@ public Task> StringGetLeaseAsync(RedisKey key, CommandFlags flags = public Task StringGetAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) { if (keys == null) throw new ArgumentNullException(nameof(keys)); - if (keys.Length == 0) return CompletedTask.FromResult(Array.Empty(), asyncState); + if (keys.Length == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState); var msg = Message.Create(Database, flags, RedisCommand.MGET, keys); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public bool StringGetBit(RedisKey key, long offset, CommandFlags flags = CommandFlags.None) @@ -2572,13 +2573,13 @@ public Task StringGetDeleteAsync(RedisKey key, CommandFlags flags = public RedisValueWithExpiry StringGetWithExpiry(RedisKey key, CommandFlags flags = CommandFlags.None) { - var msg = GetStringGetWithExpiryMessage(key, flags, out ResultProcessor processor, out ServerEndPoint server); + var msg = GetStringGetWithExpiryMessage(key, flags, out ResultProcessor processor, out ServerEndPoint? server); return ExecuteSync(msg, processor, server); } public Task StringGetWithExpiryAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { - var msg = GetStringGetWithExpiryMessage(key, flags, out ResultProcessor processor, out ServerEndPoint server); + var msg = GetStringGetWithExpiryMessage(key, flags, out ResultProcessor processor, out ServerEndPoint? server); return ExecuteAsync(msg, processor, server); } @@ -2732,7 +2733,7 @@ public Task StringSetRangeAsync(RedisKey key, long offset, RedisValu _ => throw new ArgumentException("Expiry time must be either Utc or Local", nameof(when)), }; - private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, TimeSpan? expiry, out ServerEndPoint server) + private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, TimeSpan? expiry, out ServerEndPoint? server) { TimeSpan duration; if (expiry == null || (duration = expiry.Value) == TimeSpan.MaxValue) @@ -2754,7 +2755,7 @@ private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, TimeSpan? return Message.Create(Database, flags, RedisCommand.EXPIRE, key, seconds); } - private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, DateTime? expiry, out ServerEndPoint server) + private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, DateTime? expiry, out ServerEndPoint? server) { DateTime when; if (expiry == null || (when = expiry.Value) == DateTime.MaxValue) @@ -2776,7 +2777,7 @@ private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, DateTime? return Message.Create(Database, flags, RedisCommand.EXPIREAT, key, seconds); } - private Message GetHashSetMessage(RedisKey key, HashEntry[] hashFields, CommandFlags flags) + private Message? GetHashSetMessage(RedisKey key, HashEntry[] hashFields, CommandFlags flags) { if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); switch (hashFields.Length) @@ -2801,10 +2802,10 @@ private Message GetHashSetMessage(RedisKey key, HashEntry[] hashFields, CommandF } } - private ITransaction GetLockExtendTransaction(RedisKey key, RedisValue value, TimeSpan expiry) + private ITransaction? GetLockExtendTransaction(RedisKey key, RedisValue value, TimeSpan expiry) { var tran = CreateTransactionIfAvailable(asyncState); - if (tran != null) + if (tran is not null) { tran.AddCondition(Condition.StringEqual(key, value)); tran.KeyExpireAsync(key, expiry, CommandFlags.FireAndForget); @@ -2812,10 +2813,10 @@ private ITransaction GetLockExtendTransaction(RedisKey key, RedisValue value, Ti return tran; } - private ITransaction GetLockReleaseTransaction(RedisKey key, RedisValue value) + private ITransaction? GetLockReleaseTransaction(RedisKey key, RedisValue value) { var tran = CreateTransactionIfAvailable(asyncState); - if (tran != null) + if (tran is not null) { tran.AddCondition(Condition.StringEqual(key, value)); tran.KeyDeleteAsync(key, CommandFlags.FireAndForget); @@ -2829,7 +2830,7 @@ private static RedisValue GetLexRange(RedisValue value, Exclude exclude, bool is { return isStart ? RedisLiterals.MinusSymbol : RedisLiterals.PlusSumbol; } - byte[] orig = value; + byte[] orig = value!; byte[] result = new byte[orig.Length + 1]; // no defaults here; must always explicitly specify [ / ( @@ -2974,7 +2975,7 @@ private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double s }; } - private Message GetSortedSetAddMessage(RedisKey key, SortedSetEntry[] values, When when, CommandFlags flags) + private Message? GetSortedSetAddMessage(RedisKey key, SortedSetEntry[] values, When when, CommandFlags flags) { WhenAlwaysOrExistsOrNotExists(when); if (values == null) throw new ArgumentNullException(nameof(values)); @@ -3010,7 +3011,7 @@ private Message GetSortedSetAddMessage(RedisKey key, SortedSetEntry[] values, Wh } } - private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long skip, long take, Order order, SortType sortType, RedisValue by, RedisValue[] get, CommandFlags flags) + private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long skip, long take, Order order, SortType sortType, RedisValue by, RedisValue[]? get, CommandFlags flags) { // most common cases; no "get", no "by", no "destination", no "skip", no "take" if (destination.IsNull && skip == 0 && take == -1 && by.IsNull && (get == null || get.Length == 0)) @@ -3087,7 +3088,7 @@ private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long return Message.Create(Database, flags, RedisCommand.SORT, key, values.ToArray(), destination); } - private Message GetSortedSetCombineAndStoreCommandMessage(SetOperation operation, RedisKey destination, RedisKey[] keys, double[] weights, Aggregate aggregate, CommandFlags flags) + private Message GetSortedSetCombineAndStoreCommandMessage(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights, Aggregate aggregate, CommandFlags flags) { var command = operation switch { @@ -3097,7 +3098,7 @@ private Message GetSortedSetCombineAndStoreCommandMessage(SetOperation operation }; if (keys == null) throw new ArgumentNullException(nameof(keys)); - List values = null; + List? values = null; if (weights != null && weights.Length != 0) { (values ??= new List()).Add(RedisLiterals.WEIGHTS); @@ -3493,7 +3494,7 @@ private Message GetStreamTrimMessage(RedisKey key, int maxLength, bool useApprox values); } - private Message GetStringBitOperationMessage(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags) + private Message? GetStringBitOperationMessage(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags) { if (keys == null) throw new ArgumentNullException(nameof(keys)); if (keys.Length == 0) return null; @@ -3538,7 +3539,7 @@ private Message GetStringGetExMessage(in RedisKey key, DateTime expiry, CommandF ? Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PERSIST) : Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PXAT, GetMillisecondsUntil(expiry)); - private Message GetStringGetWithExpiryMessage(RedisKey key, CommandFlags flags, out ResultProcessor processor, out ServerEndPoint server) + private Message GetStringGetWithExpiryMessage(RedisKey key, CommandFlags flags, out ResultProcessor processor, out ServerEndPoint? server) { if (this is IBatch) { @@ -3555,7 +3556,7 @@ private Message GetStringGetWithExpiryMessage(RedisKey key, CommandFlags flags, return new StringGetWithExpiryMessage(Database, flags, RedisCommand.TTL, key); } - private Message GetStringSetMessage(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) + private Message? GetStringSetMessage(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) { if (values == null) throw new ArgumentNullException(nameof(values)); switch (values.Length) @@ -3675,7 +3676,7 @@ private Message GetStringSetAndGetMessage( }; } - private Message IncrMessage(RedisKey key, long value, CommandFlags flags) => value switch + private Message? IncrMessage(RedisKey key, long value, CommandFlags flags) => value switch { 0 => ((flags & CommandFlags.FireAndForget) != 0) ? null @@ -3694,7 +3695,7 @@ private Message GetStringSetAndGetMessage( _ => throw new ArgumentOutOfRangeException(nameof(operation)), }; - private CursorEnumerable TryScan(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags, RedisCommand command, ResultProcessor.ScanResult> processor, out ServerEndPoint server) + private CursorEnumerable? TryScan(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags, RedisCommand command, ResultProcessor.ScanResult> processor, out ServerEndPoint? server) { server = null; if (pageSize <= 0) @@ -3704,7 +3705,7 @@ private CursorEnumerable TryScan(RedisKey key, RedisValue pattern, int pag var features = GetFeatures(key, flags, out server); if (!features.Scan) return null; - if (CursorUtils.IsNil(pattern)) pattern = (byte[])null; + if (CursorUtils.IsNil(pattern)) pattern = (byte[]?)null; return new ScanEnumerable(this, server, key, pattern, pageSize, cursor, pageOffset, flags, command, processor); } @@ -3729,7 +3730,7 @@ public RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min, RedisVal private static void ReverseLimits(Order order, ref Exclude exclude, ref RedisValue start, ref RedisValue stop) { - bool reverseLimits = (order == Order.Ascending) == (stop != default(RedisValue) && start.CompareTo(stop) > 0); + bool reverseLimits = (order == Order.Ascending) == (stop != default && start.CompareTo(stop) > 0); if (reverseLimits) { var tmp = start; @@ -3742,12 +3743,12 @@ private static void ReverseLimits(Order order, ref Exclude exclude, ref RedisVal } } } - public RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min = default(RedisValue), RedisValue max = default(RedisValue), + public RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min = default, RedisValue max = default, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) { ReverseLimits(order, ref exclude, ref min, ref max); var msg = GetLexMessage(order == Order.Ascending ? RedisCommand.ZRANGEBYLEX : RedisCommand.ZREVRANGEBYLEX, key, min, max, exclude, skip, take, flags); - return ExecuteSync(msg, ResultProcessor.RedisValueArray); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) @@ -3765,12 +3766,12 @@ public Task SortedSetLengthByValueAsync(RedisKey key, RedisValue min, Redi public Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude, long skip, long take, CommandFlags flags) => SortedSetRangeByValueAsync(key, min, max, exclude, Order.Ascending, skip, take, flags); - public Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min = default(RedisValue), RedisValue max = default(RedisValue), + public Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min = default, RedisValue max = default, Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) { ReverseLimits(order, ref exclude, ref min, ref max); var msg = GetLexMessage(order == Order.Ascending ? RedisCommand.ZRANGEBYLEX : RedisCommand.ZREVRANGEBYLEX, key, min, max, exclude, skip, take, flags); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public Task SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) @@ -3785,7 +3786,7 @@ internal class ScanEnumerable : CursorEnumerable private readonly RedisValue pattern; private readonly RedisCommand command; - public ScanEnumerable(RedisDatabase database, ServerEndPoint server, RedisKey key, in RedisValue pattern, int pageSize, in RedisValue cursor, int pageOffset, CommandFlags flags, + public ScanEnumerable(RedisDatabase database, ServerEndPoint? server, RedisKey key, in RedisValue pattern, int pageSize, in RedisValue cursor, int pageOffset, CommandFlags flags, RedisCommand command, ResultProcessor processor) : base(database, server, database.Database, pageSize, cursor, pageOffset, flags) { @@ -3846,13 +3847,13 @@ private sealed class HashScanResultProcessor : ScanResultProcessor { public static readonly ResultProcessor.ScanResult> Default = new HashScanResultProcessor(); private HashScanResultProcessor() { } - protected override HashEntry[] Parse(in RawResult result, out int count) - => HashEntryArray.TryParse(result, out HashEntry[] pairs, true, out count) ? pairs : null; + protected override HashEntry[]? Parse(in RawResult result, out int count) + => HashEntryArray.TryParse(result, out HashEntry[]? pairs, true, out count) ? pairs : null; } private abstract class ScanResultProcessor : ResultProcessor.ScanResult> { - protected abstract T[] Parse(in RawResult result, out int count); + protected abstract T[]? Parse(in RawResult result, out int count); protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { @@ -3865,7 +3866,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes ref RawResult inner = ref arr[1]; if (inner.Type == ResultType.MultiBulk && arr[0].TryGetInt64(out var i64)) { - T[] oversized = Parse(inner, out int count); + T[]? oversized = Parse(inner, out int count); var sscanResult = new ScanEnumerable.ScanResult(i64, oversized, count, true); SetResult(message, sscanResult); return true; @@ -3882,7 +3883,7 @@ internal sealed class ExecuteMessage : Message private readonly ICollection _args; public new CommandBytes Command { get; } - public ExecuteMessage(CommandMap map, int db, CommandFlags flags, string command, ICollection args) : base(db, flags, RedisCommand.UNKNOWN) + public ExecuteMessage(CommandMap? map, int db, CommandFlags flags, string command, ICollection? args) : base(db, flags, RedisCommand.UNKNOWN) { if (args != null && args.Count >= PhysicalConnection.REDIS_MAX_ARGS) // using >= here because we will be adding 1 for the command itself (which is an arg for the purposes of the multi-bulk protocol) { @@ -3935,25 +3936,25 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) private sealed class ScriptEvalMessage : Message, IMultiMessage { private readonly RedisKey[] keys; - private readonly string script; + private readonly string? script; private readonly RedisValue[] values; - private byte[] asciiHash; - private readonly byte[] hexHash; + private byte[]? asciiHash; + private readonly byte[]? hexHash; - public ScriptEvalMessage(int db, CommandFlags flags, string script, RedisKey[] keys, RedisValue[] values) + public ScriptEvalMessage(int db, CommandFlags flags, string script, RedisKey[]? keys, RedisValue[]? values) : this(db, flags, ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA : RedisCommand.EVAL, script, null, keys, values) { if (script == null) throw new ArgumentNullException(nameof(script)); } - public ScriptEvalMessage(int db, CommandFlags flags, byte[] hash, RedisKey[] keys, RedisValue[] values) + public ScriptEvalMessage(int db, CommandFlags flags, byte[] hash, RedisKey[]? keys, RedisValue[]? values) : this(db, flags, RedisCommand.EVALSHA, null, hash, keys, values) { if (hash == null) throw new ArgumentNullException(nameof(hash)); if (hash.Length != ResultProcessor.ScriptLoadProcessor.Sha1HashLength) throw new ArgumentOutOfRangeException(nameof(hash), "Invalid hash length"); } - private ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, string script, byte[] hexHash, RedisKey[] keys, RedisValue[] values) + private ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, string? script, byte[]? hexHash, RedisKey[]? keys, RedisValue[]? values) : base(db, flags, command) { this.script = script; @@ -3979,7 +3980,7 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) public IEnumerable GetMessages(PhysicalConnection connection) { - PhysicalBridge bridge; + PhysicalBridge? bridge; if (script != null && (bridge = connection.BridgeCouldBeNull) != null && bridge.Multiplexer.CommandMap.IsAvailable(RedisCommand.SCRIPT) && (Flags & CommandFlags.NoScriptCache) == 0) @@ -4143,14 +4144,14 @@ private sealed class SortedSetScanResultProcessor : ScanResultProcessor.ScanResult> Default = new SortedSetScanResultProcessor(); private SortedSetScanResultProcessor() { } - protected override SortedSetEntry[] Parse(in RawResult result, out int count) - => SortedSetWithScores.TryParse(result, out SortedSetEntry[] pairs, true, out count) ? pairs : null; + protected override SortedSetEntry[]? Parse(in RawResult result, out int count) + => SortedSetWithScores.TryParse(result, out SortedSetEntry[]? pairs, true, out count) ? pairs : null; } private class StringGetWithExpiryMessage : Message.CommandKeyBase, IMultiMessage { private readonly RedisCommand ttlCommand; - private IResultBox box; + private IResultBox? box; public StringGetWithExpiryMessage(int db, CommandFlags flags, RedisCommand ttlCommand, in RedisKey key) : base(db, flags, RedisCommand.GET, key) @@ -4158,7 +4159,7 @@ public StringGetWithExpiryMessage(int db, CommandFlags flags, RedisCommand ttlCo this.ttlCommand = ttlCommand; } - public override string CommandAndKey => ttlCommand + "+" + RedisCommand.GET + " " + (string)Key; + public override string CommandAndKey => ttlCommand + "+" + RedisCommand.GET + " " + (string?)Key; public IEnumerable GetMessages(PhysicalConnection connection) { @@ -4170,7 +4171,7 @@ public IEnumerable GetMessages(PhysicalConnection connection) yield return this; } - public bool UnwrapValue(out TimeSpan? value, out Exception ex) + public bool UnwrapValue(out TimeSpan? value, out Exception? ex) { if (box != null) { diff --git a/src/StackExchange.Redis/RedisErrorEventArgs.cs b/src/StackExchange.Redis/RedisErrorEventArgs.cs index 92186d346..bfe114a7c 100644 --- a/src/StackExchange.Redis/RedisErrorEventArgs.cs +++ b/src/StackExchange.Redis/RedisErrorEventArgs.cs @@ -9,10 +9,10 @@ namespace StackExchange.Redis /// public class RedisErrorEventArgs : EventArgs, ICompletable { - private readonly EventHandler handler; + private readonly EventHandler? handler; private readonly object sender; internal RedisErrorEventArgs( - EventHandler handler, object sender, + EventHandler? handler, object sender, EndPoint endpoint, string message) { this.handler = handler; diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 077cb04e1..e44c43660 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -300,7 +300,7 @@ orderby prop.Name /// if and this instance are the same type and represent the same value, otherwise. /// /// The object to compare with the current instance. - public override bool Equals(object obj) => obj is RedisFeatures f && f.Version == Version; + public override bool Equals(object? obj) => obj is RedisFeatures f && f.Version == Version; /// /// Checks if 2 are .Equal(). diff --git a/src/StackExchange.Redis/RedisKey.cs b/src/StackExchange.Redis/RedisKey.cs index 6739250ef..5f0091b3a 100644 --- a/src/StackExchange.Redis/RedisKey.cs +++ b/src/StackExchange.Redis/RedisKey.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis /// public readonly struct RedisKey : IEquatable { - internal RedisKey(byte[] keyPrefix, object keyValue) + internal RedisKey(byte[]? keyPrefix, object? keyValue) { KeyPrefix = keyPrefix?.Length == 0 ? null : keyPrefix; KeyValue = keyValue; @@ -17,9 +17,9 @@ internal RedisKey(byte[] keyPrefix, object keyValue) /// /// Creates a from a string. /// - public RedisKey(string key) : this(null, key) { } + public RedisKey(string? key) : this(null, key) { } - internal RedisKey AsPrefix() => new RedisKey((byte[])this, null); + internal RedisKey AsPrefix() => new RedisKey((byte[]?)this, null); internal bool IsNull => KeyPrefix == null && KeyValue == null; @@ -34,8 +34,8 @@ internal bool IsEmpty } } - internal byte[] KeyPrefix { get; } - internal object KeyValue { get; } + internal byte[]? KeyPrefix { get; } + internal object? KeyValue { get; } /// /// Indicate whether two keys are not equal. @@ -111,7 +111,7 @@ internal bool IsEmpty /// See . /// /// The to compare to. - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is RedisKey other) { @@ -130,7 +130,7 @@ public override bool Equals(object obj) /// The to compare to. public bool Equals(RedisKey other) => CompositeEquals(KeyPrefix, KeyValue, other.KeyPrefix, other.KeyValue); - private static bool CompositeEquals(byte[] keyPrefix0, object keyValue0, byte[] keyPrefix1, object keyValue1) + private static bool CompositeEquals(byte[]? keyPrefix0, object? keyValue0, byte[]? keyPrefix1, object? keyValue1) { if (RedisValue.Equals(keyPrefix0, keyPrefix1)) { @@ -148,7 +148,7 @@ private static bool CompositeEquals(byte[] keyPrefix0, object keyValue0, byte[] public override int GetHashCode() { int chk0 = KeyPrefix == null ? 0 : RedisValue.GetHashCode(KeyPrefix), - chk1 = KeyValue is string ? KeyValue.GetHashCode() : RedisValue.GetHashCode((byte[])KeyValue); + chk1 = KeyValue is string ? KeyValue.GetHashCode() : RedisValue.GetHashCode((byte[]?)KeyValue); return unchecked((17 * chk0) + chk1); } @@ -156,12 +156,12 @@ public override int GetHashCode() /// /// Obtains a string representation of the key. /// - public override string ToString() => ((string)this) ?? "(null)"; + public override string ToString() => ((string?)this) ?? "(null)"; internal RedisValue AsRedisValue() { if (KeyPrefix == null && KeyValue is string keyString) return keyString; - return (byte[])this; + return (byte[]?)this; } internal void AssertNotNull() @@ -173,18 +173,18 @@ internal void AssertNotNull() /// Create a from a . /// /// The string to get a key from. - public static implicit operator RedisKey(string key) + public static implicit operator RedisKey(string? key) { - if (key == null) return default(RedisKey); + if (key == null) return default; return new RedisKey(null, key); } /// /// Create a from a . /// /// The byte array to get a key from. - public static implicit operator RedisKey(byte[] key) + public static implicit operator RedisKey(byte[]? key) { - if (key == null) return default(RedisKey); + if (key == null) return default; return new RedisKey(null, key); } @@ -192,15 +192,15 @@ public static implicit operator RedisKey(byte[] key) /// Obtain the as a . /// /// The key to get a byte array for. - public static implicit operator byte[] (RedisKey key) => ConcatenateBytes(key.KeyPrefix, key.KeyValue, null); + public static implicit operator byte[]? (RedisKey key) => ConcatenateBytes(key.KeyPrefix, key.KeyValue, null); /// /// Obtain the key as a . /// /// The key to get a string for. - public static implicit operator string(RedisKey key) + public static implicit operator string? (RedisKey key) { - byte[] arr; + byte[]? arr; if (key.KeyPrefix == null) { if (key.KeyValue == null) return null; @@ -211,7 +211,7 @@ public static implicit operator string(RedisKey key) } else { - arr = (byte[])key; + arr = (byte[]?)key; } if (arr == null) return null; try @@ -230,12 +230,10 @@ public static implicit operator string(RedisKey key) /// The first to add. /// The second to add. [Obsolete("Prefer WithPrefix")] - public static RedisKey operator +(RedisKey x, RedisKey y) - { - return new RedisKey(ConcatenateBytes(x.KeyPrefix, x.KeyValue, y.KeyPrefix), y.KeyValue); - } + public static RedisKey operator +(RedisKey x, RedisKey y) => + new RedisKey(ConcatenateBytes(x.KeyPrefix, x.KeyValue, y.KeyPrefix), y.KeyValue); - internal static RedisKey WithPrefix(byte[] prefix, RedisKey value) + internal static RedisKey WithPrefix(byte[]? prefix, RedisKey value) { if (prefix == null || prefix.Length == 0) return value; if (value.KeyPrefix == null) return new RedisKey(prefix, value.KeyValue); @@ -248,7 +246,7 @@ internal static RedisKey WithPrefix(byte[] prefix, RedisKey value) return new RedisKey(copy, value.KeyValue); } - internal static byte[] ConcatenateBytes(byte[] a, object b, byte[] c) + internal static byte[]? ConcatenateBytes(byte[]? a, object? b, byte[]? c) { if ((a == null || a.Length == 0) && (c == null || c.Length == 0)) { @@ -264,7 +262,7 @@ internal static byte[] ConcatenateBytes(byte[] a, object b, byte[] c) cLen = c?.Length ?? 0; var result = new byte[aLen + bLen + cLen]; - if (aLen != 0) Buffer.BlockCopy(a, 0, result, 0, aLen); + if (aLen != 0) Buffer.BlockCopy(a!, 0, result, 0, aLen); if (bLen != 0) { if (b is string s) @@ -273,10 +271,10 @@ internal static byte[] ConcatenateBytes(byte[] a, object b, byte[] c) } else { - Buffer.BlockCopy((byte[])b, 0, result, aLen, bLen); + Buffer.BlockCopy((byte[])b!, 0, result, aLen, bLen); } } - if (cLen != 0) Buffer.BlockCopy(c, 0, result, aLen + bLen, cLen); + if (cLen != 0) Buffer.BlockCopy(c!, 0, result, aLen + bLen, cLen); return result; } diff --git a/src/StackExchange.Redis/RedisResult.cs b/src/StackExchange.Redis/RedisResult.cs index b79d6ad7b..9935deee7 100644 --- a/src/StackExchange.Redis/RedisResult.cs +++ b/src/StackExchange.Redis/RedisResult.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace StackExchange.Redis @@ -44,11 +45,16 @@ public static RedisResult Create(RedisResult[] values) /// internal static RedisResult NullArray { get; } = new ArrayRedisResult(null); + /// + /// A null single result, to use as a default for invalid returns. + /// + internal static RedisResult NullSingle { get; } = new SingleRedisResult(RedisValue.Null, ResultType.None); + /// /// Internally, this is very similar to RawResult, except it is designed to be usable, /// outside of the IO-processing pipeline: the buffers are standalone, etc. /// - internal static RedisResult TryCreate(PhysicalConnection connection, in RawResult result) + internal static bool TryCreate(PhysicalConnection connection, in RawResult result, [NotNullWhen(true)] out RedisResult? redisResult) { try { @@ -57,30 +63,49 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul case ResultType.Integer: case ResultType.SimpleString: case ResultType.BulkString: - return new SingleRedisResult(result.AsRedisValue(), result.Type); + redisResult = new SingleRedisResult(result.AsRedisValue(), result.Type); + return true; case ResultType.MultiBulk: - if (result.IsNull) return NullArray; + if (result.IsNull) + { + redisResult = NullArray; + return true; + } var items = result.GetItems(); - if (items.Length == 0) return EmptyArray; + if (items.Length == 0) + { + redisResult = EmptyArray; + return true; + } var arr = new RedisResult[items.Length]; int i = 0; foreach (ref RawResult item in items) { - var next = TryCreate(connection, in item); - if (next == null) return null; // means we didn't understand - arr[i++] = next; + if (TryCreate(connection, in item, out var next)) + { + arr[i++] = next; + } + else + { + redisResult = null; + return false; + } } - return new ArrayRedisResult(arr); + redisResult = new ArrayRedisResult(arr); + return true; case ResultType.Error: - return new ErrorRedisResult(result.GetString()); + redisResult = new ErrorRedisResult(result.GetString()); + return true; default: - return null; + redisResult = null; + return false; } } catch (Exception ex) { connection?.OnInternalError(ex); - return null; // will be logged as a protocol fail by the processor + redisResult = null; + return false; // will be logged as a protocol fail by the processor } } @@ -98,12 +123,12 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator string(RedisResult result) => result?.AsString(); + public static explicit operator string?(RedisResult? result) => result?.AsString(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator byte[](RedisResult result) => result?.AsByteArray(); + public static explicit operator byte[]?(RedisResult? result) => result?.AsByteArray(); /// /// Interprets the result as a . /// @@ -134,137 +159,141 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator RedisValue(RedisResult result) => result?.AsRedisValue() ?? RedisValue.Null; + public static explicit operator RedisValue(RedisResult? result) => result?.AsRedisValue() ?? RedisValue.Null; /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator RedisKey(RedisResult result) => result?.AsRedisKey() ?? default; + public static explicit operator RedisKey(RedisResult? result) => result?.AsRedisKey() ?? default; /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator double?(RedisResult result) => result?.AsNullableDouble(); + public static explicit operator double?(RedisResult? result) => result?.AsNullableDouble(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator long?(RedisResult result) => result?.AsNullableInt64(); + public static explicit operator long?(RedisResult? result) => result?.AsNullableInt64(); /// /// Interprets the result as a . /// /// The result to convert to a . [CLSCompliant(false)] - public static explicit operator ulong?(RedisResult result) => result?.AsNullableUInt64(); + public static explicit operator ulong?(RedisResult? result) => result?.AsNullableUInt64(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator int?(RedisResult result) => result?.AsNullableInt32(); + public static explicit operator int?(RedisResult? result) => result?.AsNullableInt32(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator bool?(RedisResult result) => result?.AsNullableBoolean(); + public static explicit operator bool?(RedisResult? result) => result?.AsNullableBoolean(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator string[](RedisResult result) => result?.AsStringArray(); + public static explicit operator string?[]?(RedisResult? result) => result?.AsStringArray(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator byte[][](RedisResult result) => result?.AsByteArrayArray(); + public static explicit operator byte[]?[]?(RedisResult? result) => result?.AsByteArrayArray(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator double[](RedisResult result) => result?.AsDoubleArray(); + public static explicit operator double[]?(RedisResult? result) => result?.AsDoubleArray(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator long[](RedisResult result) => result?.AsInt64Array(); + public static explicit operator long[]?(RedisResult? result) => result?.AsInt64Array(); /// /// Interprets the result as a . /// /// The result to convert to a . [CLSCompliant(false)] - public static explicit operator ulong[](RedisResult result) => result?.AsUInt64Array(); + public static explicit operator ulong[]?(RedisResult? result) => result?.AsUInt64Array(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator int[](RedisResult result) => result?.AsInt32Array(); + public static explicit operator int[]?(RedisResult? result) => result?.AsInt32Array(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator bool[](RedisResult result) => result?.AsBooleanArray(); + public static explicit operator bool[]?(RedisResult? result) => result?.AsBooleanArray(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator RedisValue[](RedisResult result) => result?.AsRedisValueArray(); + public static explicit operator RedisValue[]?(RedisResult? result) => result?.AsRedisValueArray(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator RedisKey[](RedisResult result) => result?.AsRedisKeyArray(); + public static explicit operator RedisKey[]?(RedisResult? result) => result?.AsRedisKeyArray(); /// /// Interprets the result as a . /// /// The result to convert to a . - public static explicit operator RedisResult[](RedisResult result) => result?.AsRedisResultArray(); + public static explicit operator RedisResult[]?(RedisResult? result) => result?.AsRedisResultArray(); /// /// Interprets a multi-bulk result with successive key/name values as a dictionary keyed by name. /// /// The key comparator to use, or by default. - public Dictionary ToDictionary(IEqualityComparer comparer = null) + public Dictionary ToDictionary(IEqualityComparer? comparer = null) { var arr = AsRedisResultArray(); + if (arr is null) + { + return new Dictionary(); + } int len = arr.Length / 2; var result = new Dictionary(len, comparer ?? StringComparer.InvariantCultureIgnoreCase); for (int i = 0; i < arr.Length; i += 2) { - result.Add(arr[i].AsString(), arr[i + 1]); + result.Add(arr[i].AsString()!, arr[i + 1]); } return result; } internal abstract bool AsBoolean(); - internal abstract bool[] AsBooleanArray(); - internal abstract byte[] AsByteArray(); - internal abstract byte[][] AsByteArrayArray(); + internal abstract bool[]? AsBooleanArray(); + internal abstract byte[]? AsByteArray(); + internal abstract byte[][]? AsByteArrayArray(); internal abstract double AsDouble(); - internal abstract double[] AsDoubleArray(); + internal abstract double[]? AsDoubleArray(); internal abstract int AsInt32(); - internal abstract int[] AsInt32Array(); + internal abstract int[]? AsInt32Array(); internal abstract long AsInt64(); internal abstract ulong AsUInt64(); - internal abstract long[] AsInt64Array(); - internal abstract ulong[] AsUInt64Array(); + internal abstract long[]? AsInt64Array(); + internal abstract ulong[]? AsUInt64Array(); internal abstract bool? AsNullableBoolean(); internal abstract double? AsNullableDouble(); internal abstract int? AsNullableInt32(); internal abstract long? AsNullableInt64(); internal abstract ulong? AsNullableUInt64(); internal abstract RedisKey AsRedisKey(); - internal abstract RedisKey[] AsRedisKeyArray(); - internal abstract RedisResult[] AsRedisResultArray(); + internal abstract RedisKey[]? AsRedisKeyArray(); + internal abstract RedisResult[]? AsRedisResultArray(); internal abstract RedisValue AsRedisValue(); - internal abstract RedisValue[] AsRedisValueArray(); - internal abstract string AsString(); - internal abstract string[] AsStringArray(); + internal abstract RedisValue[]? AsRedisValueArray(); + internal abstract string? AsString(); + internal abstract string?[]? AsStringArray(); private sealed class ArrayRedisResult : RedisResult { public override bool IsNull => _value == null; - private readonly RedisResult[] _value; + private readonly RedisResult[]? _value; public override ResultType Type => ResultType.MultiBulk; - public ArrayRedisResult(RedisResult[] value) + public ArrayRedisResult(RedisResult[]? value) { _value = value; } @@ -273,131 +302,132 @@ public ArrayRedisResult(RedisResult[] value) internal override bool AsBoolean() { - if (IsSingleton) return _value[0].AsBoolean(); + if (IsSingleton) return _value![0].AsBoolean(); throw new InvalidCastException(); } - internal override bool[] AsBooleanArray() => IsNull ? null : Array.ConvertAll(_value, x => x.AsBoolean()); + internal override bool[]? AsBooleanArray() => IsNull ? null : Array.ConvertAll(_value!, x => x.AsBoolean()); - internal override byte[] AsByteArray() + internal override byte[]? AsByteArray() { - if (IsSingleton) return _value[0].AsByteArray(); + if (IsSingleton) return _value![0].AsByteArray(); throw new InvalidCastException(); } - internal override byte[][] AsByteArrayArray() + internal override byte[][]? AsByteArrayArray() => IsNull ? null - : _value.Length == 0 ? Array.Empty() - : Array.ConvertAll(_value, x => x.AsByteArray()); + : _value!.Length == 0 + ? Array.Empty() + : Array.ConvertAll(_value, x => x.AsByteArray()!); private bool IsSingleton => _value?.Length == 1; private bool IsEmpty => _value?.Length == 0; internal override double AsDouble() { - if (IsSingleton) return _value[0].AsDouble(); + if (IsSingleton) return _value![0].AsDouble(); throw new InvalidCastException(); } - internal override double[] AsDoubleArray() + internal override double[]? AsDoubleArray() => IsNull ? null : IsEmpty ? Array.Empty() - : Array.ConvertAll(_value, x => x.AsDouble()); + : Array.ConvertAll(_value!, x => x.AsDouble()); internal override int AsInt32() { - if (IsSingleton) return _value[0].AsInt32(); + if (IsSingleton) return _value![0].AsInt32(); throw new InvalidCastException(); } - internal override int[] AsInt32Array() + internal override int[]? AsInt32Array() => IsNull ? null : IsEmpty ? Array.Empty() - : Array.ConvertAll(_value, x => x.AsInt32()); + : Array.ConvertAll(_value!, x => x.AsInt32()); internal override long AsInt64() { - if (IsSingleton) return _value[0].AsInt64(); + if (IsSingleton) return _value![0].AsInt64(); throw new InvalidCastException(); } internal override ulong AsUInt64() { - if (IsSingleton) return _value[0].AsUInt64(); + if (IsSingleton) return _value![0].AsUInt64(); throw new InvalidCastException(); } - internal override long[] AsInt64Array() + internal override long[]? AsInt64Array() => IsNull ? null : IsEmpty ? Array.Empty() - : Array.ConvertAll(_value, x => x.AsInt64()); + : Array.ConvertAll(_value!, x => x.AsInt64()); - internal override ulong[] AsUInt64Array() + internal override ulong[]? AsUInt64Array() => IsNull ? null : IsEmpty ? Array.Empty() - : Array.ConvertAll(_value, x => x.AsUInt64()); + : Array.ConvertAll(_value!, x => x.AsUInt64()); internal override bool? AsNullableBoolean() { - if (IsSingleton) return _value[0].AsNullableBoolean(); + if (IsSingleton) return _value![0].AsNullableBoolean(); throw new InvalidCastException(); } internal override double? AsNullableDouble() { - if (IsSingleton) return _value[0].AsNullableDouble(); + if (IsSingleton) return _value![0].AsNullableDouble(); throw new InvalidCastException(); } internal override int? AsNullableInt32() { - if (IsSingleton) return _value[0].AsNullableInt32(); + if (IsSingleton) return _value![0].AsNullableInt32(); throw new InvalidCastException(); } internal override long? AsNullableInt64() { - if (IsSingleton) return _value[0].AsNullableInt64(); + if (IsSingleton) return _value![0].AsNullableInt64(); throw new InvalidCastException(); } internal override ulong? AsNullableUInt64() { - if (IsSingleton) return _value[0].AsNullableUInt64(); + if (IsSingleton) return _value![0].AsNullableUInt64(); throw new InvalidCastException(); } internal override RedisKey AsRedisKey() { - if (IsSingleton) return _value[0].AsRedisKey(); + if (IsSingleton) return _value![0].AsRedisKey(); throw new InvalidCastException(); } - internal override RedisKey[] AsRedisKeyArray() + internal override RedisKey[]? AsRedisKeyArray() => IsNull ? null : IsEmpty ? Array.Empty() - : Array.ConvertAll(_value, x => x.AsRedisKey()); + : Array.ConvertAll(_value!, x => x.AsRedisKey()); - internal override RedisResult[] AsRedisResultArray() => _value; + internal override RedisResult[]? AsRedisResultArray() => _value; internal override RedisValue AsRedisValue() { - if (IsSingleton) return _value[0].AsRedisValue(); + if (IsSingleton) return _value![0].AsRedisValue(); throw new InvalidCastException(); } - internal override RedisValue[] AsRedisValueArray() + internal override RedisValue[]? AsRedisValueArray() => IsNull ? null : IsEmpty ? Array.Empty() - : Array.ConvertAll(_value, x => x.AsRedisValue()); + : Array.ConvertAll(_value!, x => x.AsRedisValue()); - internal override string AsString() + internal override string? AsString() { - if (IsSingleton) return _value[0].AsString(); + if (IsSingleton) return _value![0].AsString(); throw new InvalidCastException(); } - internal override string[] AsStringArray() + internal override string?[]? AsStringArray() => IsNull ? null : IsEmpty ? Array.Empty() - : Array.ConvertAll(_value, x => x.AsString()); + : Array.ConvertAll(_value!, x => x.AsString()); } /// @@ -410,14 +440,14 @@ internal override string[] AsStringArray() /// Create a from a channel. /// /// The to create a from. - public static RedisResult Create(RedisChannel channel) => Create((byte[])channel, ResultType.BulkString); + public static RedisResult Create(RedisChannel channel) => Create((byte[]?)channel, ResultType.BulkString); private sealed class ErrorRedisResult : RedisResult { private readonly string value; public override ResultType Type => ResultType.Error; - public ErrorRedisResult(string value) + public ErrorRedisResult(string? value) { this.value = value ?? throw new ArgumentNullException(nameof(value)); } @@ -446,8 +476,8 @@ public ErrorRedisResult(string value) internal override RedisResult[] AsRedisResultArray() => throw new RedisServerException(value); internal override RedisValue AsRedisValue() => throw new RedisServerException(value); internal override RedisValue[] AsRedisValueArray() => throw new RedisServerException(value); - internal override string AsString() => throw new RedisServerException(value); - internal override string[] AsStringArray() => throw new RedisServerException(value); + internal override string? AsString() => throw new RedisServerException(value); + internal override string?[]? AsStringArray() => throw new RedisServerException(value); } private sealed class SingleRedisResult : RedisResult, IConvertible @@ -466,8 +496,8 @@ public SingleRedisResult(RedisValue value, ResultType? resultType) public override string ToString() => _value.ToString(); internal override bool AsBoolean() => (bool)_value; internal override bool[] AsBooleanArray() => new[] { AsBoolean() }; - internal override byte[] AsByteArray() => (byte[])_value; - internal override byte[][] AsByteArrayArray() => new[] { AsByteArray() }; + internal override byte[]? AsByteArray() => (byte[]?)_value; + internal override byte[][] AsByteArrayArray() => new[] { AsByteArray()! }; internal override double AsDouble() => (double)_value; internal override double[] AsDoubleArray() => new[] { AsDouble() }; internal override int AsInt32() => (int)_value; @@ -481,48 +511,48 @@ public SingleRedisResult(RedisValue value, ResultType? resultType) internal override int? AsNullableInt32() => (int?)_value; internal override long? AsNullableInt64() => (long?)_value; internal override ulong? AsNullableUInt64() => (ulong?)_value; - internal override RedisKey AsRedisKey() => (byte[])_value; + internal override RedisKey AsRedisKey() => (byte[]?)_value; internal override RedisKey[] AsRedisKeyArray() => new[] { AsRedisKey() }; internal override RedisResult[] AsRedisResultArray() => throw new InvalidCastException(); internal override RedisValue AsRedisValue() => _value; internal override RedisValue[] AsRedisValueArray() => new[] { AsRedisValue() }; - internal override string AsString() => (string)_value; - internal override string[] AsStringArray() => new[] { AsString() }; + internal override string? AsString() => (string?)_value; + internal override string?[]? AsStringArray() => new[] { AsString() }; TypeCode IConvertible.GetTypeCode() => TypeCode.Object; - bool IConvertible.ToBoolean(IFormatProvider provider) => AsBoolean(); - char IConvertible.ToChar(IFormatProvider provider) + bool IConvertible.ToBoolean(IFormatProvider? provider) => AsBoolean(); + char IConvertible.ToChar(IFormatProvider? provider) { checked { return (char)AsInt32(); } } - sbyte IConvertible.ToSByte(IFormatProvider provider) + sbyte IConvertible.ToSByte(IFormatProvider? provider) { checked { return (sbyte)AsInt32(); } } - byte IConvertible.ToByte(IFormatProvider provider) + byte IConvertible.ToByte(IFormatProvider? provider) { checked { return (byte)AsInt32(); } } - short IConvertible.ToInt16(IFormatProvider provider) + short IConvertible.ToInt16(IFormatProvider? provider) { checked { return (short)AsInt32(); } } - ushort IConvertible.ToUInt16(IFormatProvider provider) + ushort IConvertible.ToUInt16(IFormatProvider? provider) { checked { return (ushort)AsInt32(); } } - int IConvertible.ToInt32(IFormatProvider provider) => AsInt32(); - uint IConvertible.ToUInt32(IFormatProvider provider) + int IConvertible.ToInt32(IFormatProvider? provider) => AsInt32(); + uint IConvertible.ToUInt32(IFormatProvider? provider) { checked { return (uint)AsInt64(); } } - long IConvertible.ToInt64(IFormatProvider provider) => AsInt64(); - ulong IConvertible.ToUInt64(IFormatProvider provider) + long IConvertible.ToInt64(IFormatProvider? provider) => AsInt64(); + ulong IConvertible.ToUInt64(IFormatProvider? provider) { checked { return (ulong)AsInt64(); } } - float IConvertible.ToSingle(IFormatProvider provider) => (float)AsDouble(); - double IConvertible.ToDouble(IFormatProvider provider) => AsDouble(); - decimal IConvertible.ToDecimal(IFormatProvider provider) + float IConvertible.ToSingle(IFormatProvider? provider) => (float)AsDouble(); + double IConvertible.ToDouble(IFormatProvider? provider) => AsDouble(); + decimal IConvertible.ToDecimal(IFormatProvider? provider) { // we can do this safely *sometimes* if (Type == ResultType.Integer) return AsInt64(); @@ -530,9 +560,9 @@ decimal IConvertible.ToDecimal(IFormatProvider provider) ThrowNotSupported(); return default; } - DateTime IConvertible.ToDateTime(IFormatProvider provider) { ThrowNotSupported(); return default; } - string IConvertible.ToString(IFormatProvider provider) => AsString(); - object IConvertible.ToType(Type conversionType, IFormatProvider provider) + DateTime IConvertible.ToDateTime(IFormatProvider? provider) { ThrowNotSupported(); return default; } + string IConvertible.ToString(IFormatProvider? provider) => AsString()!; + object IConvertible.ToType(Type conversionType, IFormatProvider? provider) { switch (System.Type.GetTypeCode(conversionType)) { @@ -549,14 +579,15 @@ object IConvertible.ToType(Type conversionType, IFormatProvider provider) case TypeCode.Single: return (float)AsDouble(); case TypeCode.Double: return AsDouble(); case TypeCode.Decimal when Type == ResultType.Integer: return AsInt64(); - case TypeCode.String: return AsString(); + case TypeCode.String: return AsString()!; default: ThrowNotSupported(); return default; } } - void ThrowNotSupported([CallerMemberName] string caller = null) + [DoesNotReturn] + private void ThrowNotSupported([CallerMemberName] string? caller = null) => throw new NotSupportedException($"{typeof(SingleRedisResult).FullName} does not support {nameof(IConvertible)}.{caller} with value '{AsString()}'"); } } diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 9910f942c..758bd4105 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; @@ -15,14 +16,14 @@ internal sealed class RedisServer : RedisBase, IServer { private readonly ServerEndPoint server; - internal RedisServer(ConnectionMultiplexer multiplexer, ServerEndPoint server, object asyncState) : base(multiplexer, asyncState) + internal RedisServer(ConnectionMultiplexer multiplexer, ServerEndPoint server, object? asyncState) : base(multiplexer, asyncState) { this.server = server ?? throw new ArgumentNullException(nameof(server)); } int IServer.DatabaseCount => server.Databases; - public ClusterConfiguration ClusterConfiguration => server.ClusterConfiguration; + public ClusterConfiguration? ClusterConfiguration => server.ClusterConfiguration; public EndPoint EndPoint => server.EndPoint; @@ -60,19 +61,19 @@ public Task ClientKillAsync(EndPoint endpoint, CommandFlags flags = CommandFlags return ExecuteAsync(msg, ResultProcessor.DemandOK); } - public long ClientKill(long? id = null, ClientType? clientType = null, EndPoint endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None) + public long ClientKill(long? id = null, ClientType? clientType = null, EndPoint? endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None) { var msg = GetClientKillMessage(endpoint, id, clientType, skipMe, flags); return ExecuteSync(msg, ResultProcessor.Int64); } - public Task ClientKillAsync(long? id = null, ClientType? clientType = null, EndPoint endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None) + public Task ClientKillAsync(long? id = null, ClientType? clientType = null, EndPoint? endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None) { var msg = GetClientKillMessage(endpoint, id, clientType, skipMe, flags); return ExecuteAsync(msg, ResultProcessor.Int64); } - private Message GetClientKillMessage(EndPoint endpoint, long? id, ClientType? clientType, bool skipMe, CommandFlags flags) + private Message GetClientKillMessage(EndPoint? endpoint, long? id, ClientType? clientType, bool skipMe, CommandFlags flags) { var parts = new List(9) { @@ -117,51 +118,51 @@ private Message GetClientKillMessage(EndPoint endpoint, long? id, ClientType? cl public ClientInfo[] ClientList(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.CLIENT, RedisLiterals.LIST); - return ExecuteSync(msg, ClientInfo.Processor); + return ExecuteSync(msg, ClientInfo.Processor, defaultValue: Array.Empty()); } public Task ClientListAsync(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.CLIENT, RedisLiterals.LIST); - return ExecuteAsync(msg, ClientInfo.Processor); + return ExecuteAsync(msg, ClientInfo.Processor, defaultValue: Array.Empty()); } - public ClusterConfiguration ClusterNodes(CommandFlags flags = CommandFlags.None) + public ClusterConfiguration? ClusterNodes(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.CLUSTER, RedisLiterals.NODES); return ExecuteSync(msg, ResultProcessor.ClusterNodes); } - public Task ClusterNodesAsync(CommandFlags flags = CommandFlags.None) + public Task ClusterNodesAsync(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.CLUSTER, RedisLiterals.NODES); return ExecuteAsync(msg, ResultProcessor.ClusterNodes); } - public string ClusterNodesRaw(CommandFlags flags = CommandFlags.None) + public string? ClusterNodesRaw(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.CLUSTER, RedisLiterals.NODES); return ExecuteSync(msg, ResultProcessor.ClusterNodesRaw); } - public Task ClusterNodesRawAsync(CommandFlags flags = CommandFlags.None) + public Task ClusterNodesRawAsync(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.CLUSTER, RedisLiterals.NODES); return ExecuteAsync(msg, ResultProcessor.ClusterNodesRaw); } - public KeyValuePair[] ConfigGet(RedisValue pattern = default(RedisValue), CommandFlags flags = CommandFlags.None) + public KeyValuePair[] ConfigGet(RedisValue pattern = default, CommandFlags flags = CommandFlags.None) { if (pattern.IsNullOrEmpty) pattern = RedisLiterals.Wildcard; var msg = Message.Create(-1, flags, RedisCommand.CONFIG, RedisLiterals.GET, pattern); - return ExecuteSync(msg, ResultProcessor.StringPairInterleaved); + return ExecuteSync(msg, ResultProcessor.StringPairInterleaved, defaultValue: Array.Empty>()); } - public Task[]> ConfigGetAsync(RedisValue pattern = default(RedisValue), CommandFlags flags = CommandFlags.None) + public Task[]> ConfigGetAsync(RedisValue pattern = default, CommandFlags flags = CommandFlags.None) { if (pattern.IsNullOrEmpty) pattern = RedisLiterals.Wildcard; var msg = Message.Create(-1, flags, RedisCommand.CONFIG, RedisLiterals.GET, pattern); - return ExecuteAsync(msg, ResultProcessor.StringPairInterleaved); + return ExecuteAsync(msg, ResultProcessor.StringPairInterleaved, defaultValue: Array.Empty>()); } public void ConfigResetStatistics(CommandFlags flags = CommandFlags.None) @@ -253,25 +254,28 @@ public Task FlushDatabaseAsync(int database = -1, CommandFlags flags = CommandFl public ServerCounters GetCounters() => server.GetCounters(); - public IGrouping>[] Info(RedisValue section = default(RedisValue), CommandFlags flags = CommandFlags.None) + private static IGrouping>[] InfoDefault => + Enumerable.Empty>().GroupBy(k => k.Key).ToArray(); + + public IGrouping>[] Info(RedisValue section = default, CommandFlags flags = CommandFlags.None) { var msg = section.IsNullOrEmpty ? Message.Create(-1, flags, RedisCommand.INFO) : Message.Create(-1, flags, RedisCommand.INFO, section); - return ExecuteSync(msg, ResultProcessor.Info); + return ExecuteSync(msg, ResultProcessor.Info, defaultValue: InfoDefault); } - public Task>[]> InfoAsync(RedisValue section = default(RedisValue), CommandFlags flags = CommandFlags.None) + public Task>[]> InfoAsync(RedisValue section = default, CommandFlags flags = CommandFlags.None) { var msg = section.IsNullOrEmpty ? Message.Create(-1, flags, RedisCommand.INFO) : Message.Create(-1, flags, RedisCommand.INFO, section); - return ExecuteAsync(msg, ResultProcessor.Info); + return ExecuteAsync(msg, ResultProcessor.Info, defaultValue: InfoDefault); } - public string InfoRaw(RedisValue section = default(RedisValue), CommandFlags flags = CommandFlags.None) + public string? InfoRaw(RedisValue section = default, CommandFlags flags = CommandFlags.None) { var msg = section.IsNullOrEmpty ? Message.Create(-1, flags, RedisCommand.INFO) @@ -280,7 +284,7 @@ public Task FlushDatabaseAsync(int database = -1, CommandFlags flags = CommandFl return ExecuteSync(msg, ResultProcessor.String); } - public Task InfoRawAsync(RedisValue section = default(RedisValue), CommandFlags flags = CommandFlags.None) + public Task InfoRawAsync(RedisValue section = default, CommandFlags flags = CommandFlags.None) { var msg = section.IsNullOrEmpty ? Message.Create(-1, flags, RedisCommand.INFO) @@ -313,7 +317,7 @@ private CursorEnumerable KeysAsync(int database, RedisValue pattern, i if (cursor != 0) throw ExceptionFactory.NoCursor(RedisCommand.KEYS); Message msg = Message.Create(database, flags, RedisCommand.KEYS, pattern); - return CursorEnumerable.From(this, server, ExecuteAsync(msg, ResultProcessor.RedisKeyArray), pageOffset); + return CursorEnumerable.From(this, server, ExecuteAsync(msg, ResultProcessor.RedisKeyArray, defaultValue: Array.Empty()), pageOffset); } public DateTime LastSave(CommandFlags flags = CommandFlags.None) @@ -328,7 +332,7 @@ public Task LastSaveAsync(CommandFlags flags = CommandFlags.None) return ExecuteAsync(msg, ResultProcessor.DateTime); } - public void MakeMaster(ReplicationChangeOptions options, TextWriter log = null) + public void MakeMaster(ReplicationChangeOptions options, TextWriter? log = null) { using (var proxy = LogProxy.TryCreate(log)) { @@ -337,7 +341,7 @@ public void MakeMaster(ReplicationChangeOptions options, TextWriter log = null) } } - public async Task MakePrimaryAsync(ReplicationChangeOptions options, TextWriter log = null) + public async Task MakePrimaryAsync(ReplicationChangeOptions options, TextWriter? log = null) { using (var proxy = LogProxy.TryCreate(log)) { @@ -348,13 +352,13 @@ public async Task MakePrimaryAsync(ReplicationChangeOptions options, TextWriter public Role Role(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.ROLE); - return ExecuteSync(msg, ResultProcessor.Role); + return ExecuteSync(msg, ResultProcessor.Role, defaultValue: Redis.Role.Null); } public Task RoleAsync(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.ROLE); - return ExecuteAsync(msg, ResultProcessor.Role); + return ExecuteAsync(msg, ResultProcessor.Role, defaultValue: Redis.Role.Null); } public void Save(SaveType type, CommandFlags flags = CommandFlags.None) @@ -410,13 +414,13 @@ public Task ScriptFlushAsync(CommandFlags flags = CommandFlags.None) public byte[] ScriptLoad(string script, CommandFlags flags = CommandFlags.None) { var msg = new RedisDatabase.ScriptLoadMessage(flags, script); - return ExecuteSync(msg, ResultProcessor.ScriptLoad); + return ExecuteSync(msg, ResultProcessor.ScriptLoad, defaultValue: Array.Empty()); // Note: default isn't used on failure - we'll throw } public Task ScriptLoadAsync(string script, CommandFlags flags = CommandFlags.None) { var msg = new RedisDatabase.ScriptLoadMessage(flags, script); - return ExecuteAsync(msg, ResultProcessor.ScriptLoad); + return ExecuteAsync(msg, ResultProcessor.ScriptLoad, defaultValue: Array.Empty()); // Note: default isn't used on failure - we'll throw } public LoadedLuaScript ScriptLoad(LuaScript script, CommandFlags flags = CommandFlags.None) @@ -455,7 +459,7 @@ public CommandTrace[] SlowlogGet(int count = 0, CommandFlags flags = CommandFlag ? Message.Create(-1, flags, RedisCommand.SLOWLOG, RedisLiterals.GET, count) : Message.Create(-1, flags, RedisCommand.SLOWLOG, RedisLiterals.GET); - return ExecuteSync(msg, CommandTrace.Processor); + return ExecuteSync(msg, CommandTrace.Processor, defaultValue: Array.Empty()); } public Task SlowlogGetAsync(int count = 0, CommandFlags flags = CommandFlags.None) @@ -464,7 +468,7 @@ public Task SlowlogGetAsync(int count = 0, CommandFlags flags = ? Message.Create(-1, flags, RedisCommand.SLOWLOG, RedisLiterals.GET, count) : Message.Create(-1, flags, RedisCommand.SLOWLOG, RedisLiterals.GET); - return ExecuteAsync(msg, CommandTrace.Processor); + return ExecuteAsync(msg, CommandTrace.Processor, defaultValue: Array.Empty()); } public void SlowlogReset(CommandFlags flags = CommandFlags.None) @@ -491,18 +495,18 @@ public Task StringGetAsync(int db, RedisKey key, CommandFlags flags return ExecuteAsync(msg, ResultProcessor.RedisValue); } - public RedisChannel[] SubscriptionChannels(RedisChannel pattern = default(RedisChannel), CommandFlags flags = CommandFlags.None) + public RedisChannel[] SubscriptionChannels(RedisChannel pattern = default, CommandFlags flags = CommandFlags.None) { var msg = pattern.IsNullOrEmpty ? Message.Create(-1, flags, RedisCommand.PUBSUB, RedisLiterals.CHANNELS) : Message.Create(-1, flags, RedisCommand.PUBSUB, RedisLiterals.CHANNELS, pattern); - return ExecuteSync(msg, ResultProcessor.RedisChannelArrayLiteral); + return ExecuteSync(msg, ResultProcessor.RedisChannelArrayLiteral, defaultValue: Array.Empty()); } - public Task SubscriptionChannelsAsync(RedisChannel pattern = default(RedisChannel), CommandFlags flags = CommandFlags.None) + public Task SubscriptionChannelsAsync(RedisChannel pattern = default, CommandFlags flags = CommandFlags.None) { var msg = pattern.IsNullOrEmpty ? Message.Create(-1, flags, RedisCommand.PUBSUB, RedisLiterals.CHANNELS) : Message.Create(-1, flags, RedisCommand.PUBSUB, RedisLiterals.CHANNELS, pattern); - return ExecuteAsync(msg, ResultProcessor.RedisChannelArrayLiteral); + return ExecuteAsync(msg, ResultProcessor.RedisChannelArrayLiteral, defaultValue: Array.Empty()); } public long SubscriptionPatternCount(CommandFlags flags = CommandFlags.None) @@ -553,7 +557,7 @@ public Task TimeAsync(CommandFlags flags = CommandFlags.None) return ExecuteAsync(msg, ResultProcessor.DateTime); } - internal static Message CreateReplicaOfMessage(ServerEndPoint sendMessageTo, EndPoint primaryEndpoint, CommandFlags flags = CommandFlags.None) + internal static Message CreateReplicaOfMessage(ServerEndPoint sendMessageTo, EndPoint? primaryEndpoint, CommandFlags flags = CommandFlags.None) { RedisValue host, port; if (primaryEndpoint == null) @@ -563,7 +567,7 @@ internal static Message CreateReplicaOfMessage(ServerEndPoint sendMessageTo, End } else { - if (Format.TryGetHostPort(primaryEndpoint, out string hostRaw, out int portRaw)) + if (Format.TryGetHostPort(primaryEndpoint, out string? hostRaw, out int? portRaw)) { host = hostRaw; port = portRaw; @@ -576,7 +580,7 @@ internal static Message CreateReplicaOfMessage(ServerEndPoint sendMessageTo, End return Message.Create(-1, flags, sendMessageTo.GetFeatures().ReplicaCommands ? RedisCommand.REPLICAOF : RedisCommand.SLAVEOF, host, port); } - private Message GetTiebreakerRemovalMessage() + private Message? GetTiebreakerRemovalMessage() { var configuration = multiplexer.RawConfig; @@ -589,7 +593,7 @@ private Message GetTiebreakerRemovalMessage() return null; } - private Message GetConfigChangeMessage() + private Message? GetConfigChangeMessage() { // attempt to broadcast a reconfigure message to anybody listening to this server var channel = multiplexer.ConfigurationChangedChannel; @@ -602,44 +606,67 @@ private Message GetConfigChangeMessage() return null; } - internal override Task ExecuteAsync(Message message, ResultProcessor processor, ServerEndPoint server = null) + internal override Task ExecuteAsync(Message? message, ResultProcessor? processor, T defaultValue, ServerEndPoint? server = null) { // inject our expected server automatically - if (server == null) server = this.server; + server ??= this.server; + FixFlags(message, server); + if (!server.IsConnected) + { + if (message == null) return CompletedTask.FromDefault(defaultValue, asyncState); + if (message.IsFireAndForget) return CompletedTask.FromDefault(defaultValue, null); // F+F explicitly does not get async-state + + // After the "don't care" cases above, if we can't queue then it's time to error - otherwise call through to queuing. + if (!multiplexer.RawConfig.BacklogPolicy.QueueWhileDisconnected) + { + // no need to deny exec-sync here; will be complete before they see if + var tcs = TaskSource.Create(asyncState); + ConnectionMultiplexer.ThrowFailed(tcs, ExceptionFactory.NoConnectionAvailable(multiplexer, message, server)); + return tcs.Task; + } + } + return base.ExecuteAsync(message, processor, defaultValue, server); + } + + internal override Task ExecuteAsync(Message? message, ResultProcessor? processor, ServerEndPoint? server = null) where T : default + { + // inject our expected server automatically + server ??= this.server; FixFlags(message, server); if (!server.IsConnected) { if (message == null) return CompletedTask.Default(asyncState); if (message.IsFireAndForget) return CompletedTask.Default(null); // F+F explicitly does not get async-state - // After the "don't care" cases above, if we can't queue then it's time to error - otherwise call through to queueing. + // After the "don't care" cases above, if we can't queue then it's time to error - otherwise call through to queuing. if (!multiplexer.RawConfig.BacklogPolicy.QueueWhileDisconnected) { // no need to deny exec-sync here; will be complete before they see if - var tcs = TaskSource.Create(asyncState); + var tcs = TaskSource.Create(asyncState); ConnectionMultiplexer.ThrowFailed(tcs, ExceptionFactory.NoConnectionAvailable(multiplexer, message, server)); return tcs.Task; } } - return base.ExecuteAsync(message, processor, server); + return base.ExecuteAsync(message, processor, server); } - internal override T ExecuteSync(Message message, ResultProcessor processor, ServerEndPoint server = null) + [return: NotNullIfNotNull("defaultValue")] + internal override T? ExecuteSync(Message? message, ResultProcessor? processor, ServerEndPoint? server = null, T? defaultValue = default) where T : default { // inject our expected server automatically if (server == null) server = this.server; FixFlags(message, server); if (!server.IsConnected) { - if (message == null || message.IsFireAndForget) return default(T); + if (message == null || message.IsFireAndForget) return defaultValue; - // After the "don't care" cases above, if we can't queue then it's time to error - otherwise call through to queueing. + // After the "don't care" cases above, if we can't queue then it's time to error - otherwise call through to queuing. if (!multiplexer.RawConfig.BacklogPolicy.QueueWhileDisconnected) { throw ExceptionFactory.NoConnectionAvailable(multiplexer, message, server); } } - return base.ExecuteSync(message, processor, server); + return base.ExecuteSync(message, processor, server, defaultValue); } internal override RedisFeatures GetFeatures(in RedisKey key, CommandFlags flags, out ServerEndPoint server) @@ -663,7 +690,7 @@ public void ReplicaOf(EndPoint master, CommandFlags flags = CommandFlags.None) if (GetTiebreakerRemovalMessage() is Message tieBreakerRemoval) { tieBreakerRemoval.SetSource(ResultProcessor.Boolean, null); - server.GetBridge(tieBreakerRemoval).TryWriteSync(tieBreakerRemoval, server.IsReplica); + server.GetBridge(tieBreakerRemoval)?.TryWriteSync(tieBreakerRemoval, server.IsReplica); } var replicaOfMsg = CreateReplicaOfMessage(server, master, flags); @@ -673,14 +700,14 @@ public void ReplicaOf(EndPoint master, CommandFlags flags = CommandFlags.None) if (GetConfigChangeMessage() is Message configChangeMessage) { configChangeMessage.SetSource(ResultProcessor.Int64, null); - server.GetBridge(configChangeMessage).TryWriteSync(configChangeMessage, server.IsReplica); + server.GetBridge(configChangeMessage)?.TryWriteSync(configChangeMessage, server.IsReplica); } #pragma warning restore CS0618 } Task IServer.SlaveOfAsync(EndPoint master, CommandFlags flags) => ReplicaOfAsync(master, flags); - public async Task ReplicaOfAsync(EndPoint master, CommandFlags flags = CommandFlags.None) + public async Task ReplicaOfAsync(EndPoint? master, CommandFlags flags = CommandFlags.None) { if (master == server.EndPoint) { @@ -708,8 +735,13 @@ public async Task ReplicaOfAsync(EndPoint master, CommandFlags flags = CommandFl } } - private static void FixFlags(Message message, ServerEndPoint server) + private static void FixFlags(Message? message, ServerEndPoint server) { + if (message is null) + { + return; + } + // since the server is specified explicitly, we don't want defaults // to make the "non-preferred-endpoint" counters look artificially // inflated; note we only change *prefer* options @@ -749,7 +781,10 @@ private static class ScriptHash public static RedisValue Encode(byte[] value) { const string hex = "0123456789abcdef"; - if (value == null) return default(RedisValue); + if (value == null) + { + return default; + } var result = new byte[value.Length * 2]; int offset = 0; for (int i = 0; i < value.Length; i++) @@ -763,7 +798,7 @@ public static RedisValue Encode(byte[] value) public static RedisValue Hash(string value) { - if (value == null) return default(RedisValue); + if (value is null) return default; using (var sha1 = SHA1.Create()) { var bytes = sha1.ComputeHash(Encoding.UTF8.GetBytes(value)); @@ -849,13 +884,13 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes #region Sentinel - public EndPoint SentinelGetMasterAddressByName(string serviceName, CommandFlags flags = CommandFlags.None) + public EndPoint? SentinelGetMasterAddressByName(string serviceName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.GETMASTERADDRBYNAME, (RedisValue)serviceName); return ExecuteSync(msg, ResultProcessor.SentinelPrimaryEndpoint); } - public Task SentinelGetMasterAddressByNameAsync(string serviceName, CommandFlags flags = CommandFlags.None) + public Task SentinelGetMasterAddressByNameAsync(string serviceName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.GETMASTERADDRBYNAME, (RedisValue)serviceName); return ExecuteAsync(msg, ResultProcessor.SentinelPrimaryEndpoint); @@ -864,37 +899,37 @@ public Task SentinelGetMasterAddressByNameAsync(string serviceName, Co public EndPoint[] SentinelGetSentinelAddresses(string serviceName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SENTINELS, (RedisValue)serviceName); - return ExecuteSync(msg, ResultProcessor.SentinelAddressesEndPoints); + return ExecuteSync(msg, ResultProcessor.SentinelAddressesEndPoints, defaultValue: Array.Empty()); } public Task SentinelGetSentinelAddressesAsync(string serviceName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SENTINELS, (RedisValue)serviceName); - return ExecuteAsync(msg, ResultProcessor.SentinelAddressesEndPoints); + return ExecuteAsync(msg, ResultProcessor.SentinelAddressesEndPoints, defaultValue: Array.Empty()); } public EndPoint[] SentinelGetReplicaAddresses(string serviceName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SLAVES, (RedisValue)serviceName); - return ExecuteSync(msg, ResultProcessor.SentinelAddressesEndPoints); + return ExecuteSync(msg, ResultProcessor.SentinelAddressesEndPoints, defaultValue: Array.Empty()); } public Task SentinelGetReplicaAddressesAsync(string serviceName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SLAVES, (RedisValue)serviceName); - return ExecuteAsync(msg, ResultProcessor.SentinelAddressesEndPoints); + return ExecuteAsync(msg, ResultProcessor.SentinelAddressesEndPoints, defaultValue: Array.Empty()); } public KeyValuePair[] SentinelMaster(string serviceName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.MASTER, (RedisValue)serviceName); - return ExecuteSync(msg, ResultProcessor.StringPairInterleaved); + return ExecuteSync(msg, ResultProcessor.StringPairInterleaved, defaultValue: Array.Empty>()); } public Task[]> SentinelMasterAsync(string serviceName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.MASTER, (RedisValue)serviceName); - return ExecuteAsync(msg, ResultProcessor.StringPairInterleaved); + return ExecuteAsync(msg, ResultProcessor.StringPairInterleaved, defaultValue: Array.Empty>()); } public void SentinelFailover(string serviceName, CommandFlags flags = CommandFlags.None) @@ -912,13 +947,13 @@ public Task SentinelFailoverAsync(string serviceName, CommandFlags flags = Comma public KeyValuePair[][] SentinelMasters(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.MASTERS); - return ExecuteSync(msg, ResultProcessor.SentinelArrayOfArrays); + return ExecuteSync(msg, ResultProcessor.SentinelArrayOfArrays, defaultValue: Array.Empty[]>()); } public Task[][]> SentinelMastersAsync(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.MASTERS); - return ExecuteAsync(msg, ResultProcessor.SentinelArrayOfArrays); + return ExecuteAsync(msg, ResultProcessor.SentinelArrayOfArrays, defaultValue: Array.Empty[]>()); } // For previous compat only @@ -929,7 +964,7 @@ public KeyValuePair[][] SentinelReplicas(string serviceName, Com { // note: sentinel does not have "replicas" terminology at the current time var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SLAVES, (RedisValue)serviceName); - return ExecuteSync(msg, ResultProcessor.SentinelArrayOfArrays); + return ExecuteSync(msg, ResultProcessor.SentinelArrayOfArrays, defaultValue: Array.Empty[]>()); } // For previous compat only @@ -940,19 +975,19 @@ public Task[][]> SentinelReplicasAsync(string servi { // note: sentinel does not have "replicas" terminology at the current time var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SLAVES, (RedisValue)serviceName); - return ExecuteAsync(msg, ResultProcessor.SentinelArrayOfArrays); + return ExecuteAsync(msg, ResultProcessor.SentinelArrayOfArrays, defaultValue: Array.Empty[]>()); } public KeyValuePair[][] SentinelSentinels(string serviceName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SENTINELS, (RedisValue)serviceName); - return ExecuteSync(msg, ResultProcessor.SentinelArrayOfArrays); + return ExecuteSync(msg, ResultProcessor.SentinelArrayOfArrays, defaultValue: Array.Empty[]>()); } public Task[][]> SentinelSentinelsAsync(string serviceName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SENTINELS, (RedisValue)serviceName); - return ExecuteAsync(msg, ResultProcessor.SentinelArrayOfArrays); + return ExecuteAsync(msg, ResultProcessor.SentinelArrayOfArrays, defaultValue: Array.Empty[]>()); } #endregion @@ -962,7 +997,7 @@ public Task[][]> SentinelSentinelsAsync(string serv public RedisResult Execute(string command, ICollection args, CommandFlags flags = CommandFlags.None) { var msg = new RedisDatabase.ExecuteMessage(multiplexer?.CommandMap, -1, flags, command, args); - return ExecuteSync(msg, ResultProcessor.ScriptResult); + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } public Task ExecuteAsync(string command, params object[] args) => ExecuteAsync(command, args, CommandFlags.None); @@ -970,7 +1005,7 @@ public RedisResult Execute(string command, ICollection args, CommandFlag public Task ExecuteAsync(string command, ICollection args, CommandFlags flags = CommandFlags.None) { var msg = new RedisDatabase.ExecuteMessage(multiplexer?.CommandMap, -1, flags, command, args); - return ExecuteAsync(msg, ResultProcessor.ScriptResult); + return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } /// @@ -981,16 +1016,16 @@ public Task ExecuteAsync(string command, ICollection args, public Task LatencyDoctorAsync(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.DOCTOR); - return ExecuteAsync(msg, ResultProcessor.String); + return ExecuteAsync(msg, ResultProcessor.String!, defaultValue: string.Empty); } public string LatencyDoctor(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.DOCTOR); - return ExecuteSync(msg, ResultProcessor.String); + return ExecuteSync(msg, ResultProcessor.String, defaultValue: string.Empty); } - private static Message LatencyResetCommand(string[] eventNames, CommandFlags flags) + private static Message LatencyResetCommand(string[]? eventNames, CommandFlags flags) { if (eventNames == null) eventNames = Array.Empty(); switch (eventNames.Length) @@ -1007,13 +1042,13 @@ private static Message LatencyResetCommand(string[] eventNames, CommandFlags fla return Message.Create(-1, flags, RedisCommand.LATENCY, arr); } } - public Task LatencyResetAsync(string[] eventNames = null, CommandFlags flags = CommandFlags.None) + public Task LatencyResetAsync(string[]? eventNames = null, CommandFlags flags = CommandFlags.None) { var msg = LatencyResetCommand(eventNames, flags); return ExecuteAsync(msg, ResultProcessor.Int64); } - public long LatencyReset(string[] eventNames = null, CommandFlags flags = CommandFlags.None) + public long LatencyReset(string[]? eventNames = null, CommandFlags flags = CommandFlags.None) { var msg = LatencyResetCommand(eventNames, flags); return ExecuteSync(msg, ResultProcessor.Int64); @@ -1022,37 +1057,37 @@ public long LatencyReset(string[] eventNames = null, CommandFlags flags = Comman public Task LatencyHistoryAsync(string eventName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.HISTORY, (RedisValue)eventName); - return ExecuteAsync(msg, LatencyHistoryEntry.ToArray); + return ExecuteAsync(msg, LatencyHistoryEntry.ToArray, defaultValue: Array.Empty()); } public LatencyHistoryEntry[] LatencyHistory(string eventName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.HISTORY, (RedisValue)eventName); - return ExecuteSync(msg, LatencyHistoryEntry.ToArray); + return ExecuteSync(msg, LatencyHistoryEntry.ToArray, defaultValue: Array.Empty()); } public Task LatencyLatestAsync(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.LATEST); - return ExecuteAsync(msg, LatencyLatestEntry.ToArray); + return ExecuteAsync(msg, LatencyLatestEntry.ToArray, defaultValue: Array.Empty()); } public LatencyLatestEntry[] LatencyLatest(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.LATENCY, RedisLiterals.LATEST); - return ExecuteSync(msg, LatencyLatestEntry.ToArray); + return ExecuteSync(msg, LatencyLatestEntry.ToArray, defaultValue: Array.Empty()); } public Task MemoryDoctorAsync(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.MEMORY, RedisLiterals.DOCTOR); - return ExecuteAsync(msg, ResultProcessor.String); + return ExecuteAsync(msg, ResultProcessor.String!, defaultValue: string.Empty); } public string MemoryDoctor(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.MEMORY, RedisLiterals.DOCTOR); - return ExecuteSync(msg, ResultProcessor.String); + return ExecuteSync(msg, ResultProcessor.String, defaultValue: string.Empty); } public Task MemoryPurgeAsync(CommandFlags flags = CommandFlags.None) @@ -1067,13 +1102,13 @@ public void MemoryPurge(CommandFlags flags = CommandFlags.None) ExecuteSync(msg, ResultProcessor.DemandOK); } - public Task MemoryAllocatorStatsAsync(CommandFlags flags = CommandFlags.None) + public Task MemoryAllocatorStatsAsync(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.MEMORY, RedisLiterals.MALLOC_STATS); return ExecuteAsync(msg, ResultProcessor.String); } - public string MemoryAllocatorStats(CommandFlags flags = CommandFlags.None) + public string? MemoryAllocatorStats(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.MEMORY, RedisLiterals.MALLOC_STATS); return ExecuteSync(msg, ResultProcessor.String); @@ -1082,13 +1117,13 @@ public string MemoryAllocatorStats(CommandFlags flags = CommandFlags.None) public Task MemoryStatsAsync(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.MEMORY, RedisLiterals.STATS); - return ExecuteAsync(msg, ResultProcessor.ScriptResult); + return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullArray); } public RedisResult MemoryStats(CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.MEMORY, RedisLiterals.STATS); - return ExecuteSync(msg, ResultProcessor.ScriptResult); + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullArray); } } } diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index cb769a569..b87cb2ea3 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -10,7 +11,7 @@ namespace StackExchange.Redis { public partial class ConnectionMultiplexer { - private RedisSubscriber _defaultSubscriber; + private RedisSubscriber? _defaultSubscriber; private RedisSubscriber DefaultSubscriber => _defaultSubscriber ??= new RedisSubscriber(this, null); private readonly ConcurrentDictionary subscriptions = new(); @@ -30,8 +31,8 @@ internal Subscription GetOrAddSubscription(in RedisChannel channel, CommandFlags return sub; } } - internal bool TryGetSubscription(in RedisChannel channel, out Subscription sub) => subscriptions.TryGetValue(channel, out sub); - internal bool TryRemoveSubscription(in RedisChannel channel, out Subscription sub) + internal bool TryGetSubscription(in RedisChannel channel, [NotNullWhen(true)] out Subscription? sub) => subscriptions.TryGetValue(channel, out sub); + internal bool TryRemoveSubscription(in RedisChannel channel, [NotNullWhen(true)] out Subscription? sub) { lock (subscriptions) { @@ -61,9 +62,9 @@ internal bool GetSubscriberCounts(in RedisChannel channel, out int handlers, out /// This may be null if there is a subscription, but we don't have a connected server at the moment. /// This behavior is fine but IsConnected checks, but is a subtle difference in . /// - internal ServerEndPoint GetSubscribedServer(in RedisChannel channel) + internal ServerEndPoint? GetSubscribedServer(in RedisChannel channel) { - if (!channel.IsNullOrEmpty && subscriptions.TryGetValue(channel, out Subscription sub)) + if (!channel.IsNullOrEmpty && subscriptions.TryGetValue(channel, out Subscription? sub)) { return sub.GetCurrentServer(); } @@ -75,9 +76,9 @@ internal ServerEndPoint GetSubscribedServer(in RedisChannel channel) /// internal void OnMessage(in RedisChannel subscription, in RedisChannel channel, in RedisValue payload) { - ICompletable completable = null; - ChannelMessageQueue queues = null; - if (subscriptions.TryGetValue(subscription, out Subscription sub)) + ICompletable? completable = null; + ChannelMessageQueue? queues = null; + if (subscriptions.TryGetValue(subscription, out Subscription? sub)) { completable = sub.ForInvoke(channel, payload, out queues); } @@ -135,9 +136,9 @@ internal enum SubscriptionAction /// internal sealed class Subscription { - private Action _handlers; - private ChannelMessageQueue _queues; - private ServerEndPoint CurrentServer; + private Action? _handlers; + private ChannelMessageQueue? _queues; + private ServerEndPoint? CurrentServer; public CommandFlags Flags { get; } public ResultProcessor.TrackSubscriptionsProcessor Processor { get; } @@ -179,7 +180,7 @@ internal Message GetMessage(RedisChannel channel, SubscriptionAction action, Com return msg; } - public void Add(Action handler, ChannelMessageQueue queue) + public void Add(Action? handler, ChannelMessageQueue? queue) { if (handler != null) { @@ -191,7 +192,7 @@ public void Add(Action handler, ChannelMessageQueue qu } } - public bool Remove(Action handler, ChannelMessageQueue queue) + public bool Remove(Action? handler, ChannelMessageQueue? queue) { if (handler != null) { @@ -204,7 +205,7 @@ public bool Remove(Action handler, ChannelMessageQueue return _handlers == null & _queues == null; } - public ICompletable ForInvoke(in RedisChannel channel, in RedisValue message, out ChannelMessageQueue queues) + public ICompletable? ForInvoke(in RedisChannel channel, in RedisValue message, out ChannelMessageQueue? queues) { var handlers = _handlers; queues = Volatile.Read(ref _queues); @@ -236,8 +237,8 @@ internal void GetSubscriberCounts(out int handlers, out int queues) } } - internal ServerEndPoint GetCurrentServer() => Volatile.Read(ref CurrentServer); - internal void SetCurrentServer(ServerEndPoint server) => CurrentServer = server; + internal ServerEndPoint? GetCurrentServer() => Volatile.Read(ref CurrentServer); + internal void SetCurrentServer(ServerEndPoint? server) => CurrentServer = server; /// /// Evaluates state and if we're not currently connected, clears the server reference. @@ -261,18 +262,18 @@ internal void UpdateServer() /// internal sealed class RedisSubscriber : RedisBase, ISubscriber { - internal RedisSubscriber(ConnectionMultiplexer multiplexer, object asyncState) : base(multiplexer, asyncState) + internal RedisSubscriber(ConnectionMultiplexer multiplexer, object? asyncState) : base(multiplexer, asyncState) { } - public EndPoint IdentifyEndpoint(RedisChannel channel, CommandFlags flags = CommandFlags.None) + public EndPoint? IdentifyEndpoint(RedisChannel channel, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.PUBSUB, RedisLiterals.NUMSUB, channel); msg.SetInternalCall(); return ExecuteSync(msg, ResultProcessor.ConnectionIdentity); } - public Task IdentifyEndpointAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None) + public Task IdentifyEndpointAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.PUBSUB, RedisLiterals.NUMSUB, channel); msg.SetInternalCall(); @@ -283,7 +284,7 @@ public Task IdentifyEndpointAsync(RedisChannel channel, CommandFlags f /// This is *could* we be connected, as in "what's the theoretical endpoint for this channel?", /// rather than if we're actually connected and actually listening on that channel. /// - public bool IsConnected(RedisChannel channel = default(RedisChannel)) + public bool IsConnected(RedisChannel channel = default) { var server = multiplexer.GetSubscribedServer(channel) ?? multiplexer.SelectServer(RedisCommand.SUBSCRIBE, CommandFlags.DemandMaster, channel); return server?.IsConnected == true && server.IsSubscriberConnected; @@ -358,7 +359,7 @@ public ChannelMessageQueue Subscribe(RedisChannel channel, CommandFlags flags = return queue; } - public bool Subscribe(RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags) + public bool Subscribe(RedisChannel channel, Action? handler, ChannelMessageQueue? queue, CommandFlags flags) { ThrowIfNull(channel); if (handler == null && queue == null) { return true; } @@ -390,7 +391,7 @@ public async Task SubscribeAsync(RedisChannel channel, Comm return queue; } - public Task SubscribeAsync(RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags) + public Task SubscribeAsync(RedisChannel channel, Action? handler, ChannelMessageQueue? queue, CommandFlags flags) { ThrowIfNull(channel); if (handler == null && queue == null) { return CompletedTask.Default(null); } @@ -412,12 +413,12 @@ public Task EnsureSubscribedToServerAsync(Subscription sub, RedisChannel c return ExecuteAsync(message, sub.Processor, selected); } - public EndPoint SubscribedEndpoint(RedisChannel channel) => multiplexer.GetSubscribedServer(channel)?.EndPoint; + public EndPoint? SubscribedEndpoint(RedisChannel channel) => multiplexer.GetSubscribedServer(channel)?.EndPoint; - void ISubscriber.Unsubscribe(RedisChannel channel, Action handler, CommandFlags flags) + void ISubscriber.Unsubscribe(RedisChannel channel, Action? handler, CommandFlags flags) => Unsubscribe(channel, handler, null, flags); - public bool Unsubscribe(in RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags) + public bool Unsubscribe(in RedisChannel channel, Action? handler, ChannelMessageQueue? queue, CommandFlags flags) { ThrowIfNull(channel); // Unregister the subscription handler/queue, and if that returns true (last handler removed), also disconnect from the server @@ -436,10 +437,10 @@ private bool UnsubscribeFromServer(Subscription sub, RedisChannel channel, Comma return false; } - Task ISubscriber.UnsubscribeAsync(RedisChannel channel, Action handler, CommandFlags flags) + Task ISubscriber.UnsubscribeAsync(RedisChannel channel, Action? handler, CommandFlags flags) => UnsubscribeAsync(channel, handler, null, flags); - public Task UnsubscribeAsync(in RedisChannel channel, Action handler, ChannelMessageQueue queue, CommandFlags flags) + public Task UnsubscribeAsync(in RedisChannel channel, Action? handler, ChannelMessageQueue? queue, CommandFlags flags) { ThrowIfNull(channel); // Unregister the subscription handler/queue, and if that returns true (last handler removed), also disconnect from the server @@ -448,7 +449,7 @@ public Task UnsubscribeAsync(in RedisChannel channel, Action.Default(asyncState); } - private Task UnsubscribeFromServerAsync(Subscription sub, RedisChannel channel, CommandFlags flags, object asyncState, bool internalCall) + private Task UnsubscribeFromServerAsync(Subscription sub, RedisChannel channel, CommandFlags flags, object? asyncState, bool internalCall) { if (sub.GetCurrentServer() is ServerEndPoint oldOwner) { @@ -462,7 +463,7 @@ private Task UnsubscribeFromServerAsync(Subscription sub, RedisChannel cha /// Unregisters a handler or queue and returns if we should remove it from the server. /// /// if we should remove the subscription from the server, otherwise. - private bool UnregisterSubscription(in RedisChannel channel, Action handler, ChannelMessageQueue queue, out Subscription sub) + private bool UnregisterSubscription(in RedisChannel channel, Action? handler, ChannelMessageQueue? queue, [NotNullWhen(true)] out Subscription? sub) { ThrowIfNull(channel); if (multiplexer.TryGetSubscription(channel, out sub)) @@ -501,7 +502,7 @@ public void UnsubscribeAll(CommandFlags flags = CommandFlags.None) public Task UnsubscribeAllAsync(CommandFlags flags = CommandFlags.None) { // TODO: Unsubscribe variadic commands to reduce round trips - Task last = null; + Task? last = null; var subs = multiplexer.GetSubscriptions(); foreach (var pair in subs) { diff --git a/src/StackExchange.Redis/RedisTransaction.cs b/src/StackExchange.Redis/RedisTransaction.cs index 35943c80f..b1c901552 100644 --- a/src/StackExchange.Redis/RedisTransaction.cs +++ b/src/StackExchange.Redis/RedisTransaction.cs @@ -8,11 +8,11 @@ namespace StackExchange.Redis { internal class RedisTransaction : RedisDatabase, ITransaction { - private List _conditions; - private List _pending; + private List? _conditions; + private List? _pending; private object SyncLock => this; - public RedisTransaction(RedisDatabase wrapped, object asyncState) : base(wrapped.multiplexer, wrapped.Database, asyncState ?? wrapped.AsyncState) + public RedisTransaction(RedisDatabase wrapped, object? asyncState) : base(wrapped.multiplexer, wrapped.Database, asyncState ?? wrapped.AsyncState) { // need to check we can reliably do this... var commandMap = multiplexer.CommandMap; @@ -46,32 +46,32 @@ public ConditionResult AddCondition(Condition condition) public bool Execute(CommandFlags flags) { - var msg = CreateMessage(flags, out ResultProcessor proc); + var msg = CreateMessage(flags, out ResultProcessor? proc); return base.ExecuteSync(msg, proc); // need base to avoid our local "not supported" override } public Task ExecuteAsync(CommandFlags flags) { - var msg = CreateMessage(flags, out ResultProcessor proc); + var msg = CreateMessage(flags, out ResultProcessor? proc); return base.ExecuteAsync(msg, proc); // need base to avoid our local wrapping override } - internal override Task ExecuteAsync(Message message, ResultProcessor processor, ServerEndPoint server = null) + internal override Task ExecuteAsync(Message? message, ResultProcessor? processor, ServerEndPoint? server = null) where T : default { if (message == null) return CompletedTask.Default(asyncState); multiplexer.CheckMessage(message); multiplexer.Trace("Wrapping " + message.Command, "Transaction"); // prepare the inner command as a task - Task task; + Task task; if (message.IsFireAndForget) { task = CompletedTask.Default(null); // F+F explicitly does not get async-state } else { - var source = TaskResultBox.Create(out var tcs, asyncState); - message.SetSource(source, processor); + var source = TaskResultBox.Create(out var tcs, asyncState); + message.SetSource(source!, processor); task = tcs.Task; } @@ -105,15 +105,15 @@ internal override Task ExecuteAsync(Message message, ResultProcessor pr return task; } - internal override T ExecuteSync(Message message, ResultProcessor processor, ServerEndPoint server = null) + internal override T? ExecuteSync(Message? message, ResultProcessor? processor, ServerEndPoint? server = null, T? defaultValue = default) where T : default { throw new NotSupportedException("ExecuteSync cannot be used inside a transaction"); } - private Message CreateMessage(CommandFlags flags, out ResultProcessor processor) + private Message? CreateMessage(CommandFlags flags, out ResultProcessor? processor) { - List cond; - List work; + List? cond; + List? work; lock (SyncLock) { work = _pending; @@ -187,14 +187,14 @@ private class TransactionMessage : Message, IMultiMessage private readonly ConditionResult[] conditions; public QueuedMessage[] InnerOperations { get; } - public TransactionMessage(int db, CommandFlags flags, List conditions, List operations) + public TransactionMessage(int db, CommandFlags flags, List? conditions, List? operations) : base(db, flags, RedisCommand.EXEC) { InnerOperations = (operations?.Count > 0) ? operations.ToArray() : Array.Empty(); this.conditions = (conditions?.Count > 0) ? conditions.ToArray() : Array.Empty(); } - internal override void SetExceptionAndComplete(Exception exception, PhysicalBridge bridge) + internal override void SetExceptionAndComplete(Exception exception, PhysicalBridge? bridge) { var inner = InnerOperations; if (inner != null) @@ -239,7 +239,7 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) public IEnumerable GetMessages(PhysicalConnection connection) { - IResultBox lastBox = null; + IResultBox? lastBox = null; var bridge = connection.BridgeCouldBeNull; if (bridge == null) throw new ObjectDisposedException(connection.ToString()); @@ -267,7 +267,7 @@ public IEnumerable GetMessages(PhysicalConnection connection) { // need to have locked them before sending them // to guarantee that we see the pulse - IResultBox latestBox = conditions[i].GetBox(); + IResultBox latestBox = conditions[i].GetBox()!; Monitor.Enter(latestBox); if (lastBox != null) Monitor.Exit(lastBox); lastBox = latestBox; @@ -327,7 +327,7 @@ public IEnumerable GetMessages(PhysicalConnection connection) if (explicitCheckForQueued) { // need to have locked them before sending them // to guarantee that we see the pulse - IResultBox thisBox = op.ResultBox; + IResultBox? thisBox = op.ResultBox; if (thisBox != null) { Monitor.Enter(thisBox); @@ -438,7 +438,7 @@ public override bool SetResult(PhysicalConnection connection, Message message, i { if (result.IsError && message is TransactionMessage tran) { - string error = result.GetString(); + string error = result.GetString()!; foreach (var op in tran.InnerOperations) { var inner = op.Wrapped; @@ -451,7 +451,8 @@ public override bool SetResult(PhysicalConnection connection, Message message, i protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog($"got {result} for {message.CommandAndKey}"); + var muxer = connection.BridgeCouldBeNull?.Multiplexer; + muxer?.OnTransactionLog($"got {result} for {message.CommandAndKey}"); if (message is TransactionMessage tran) { var wrapped = tran.InnerOperations; @@ -485,7 +486,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var arr = result.GetItems(); if (result.IsNull) { - connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog("Aborting wrapped messages (failed watch)"); + muxer?.OnTransactionLog("Aborting wrapped messages (failed watch)"); connection.Trace("Server aborted due to failed WATCH"); foreach (var op in wrapped) { @@ -499,13 +500,13 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes else if (wrapped.Length == arr.Length) { connection.Trace("Server committed; processing nested replies"); - connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog($"Processing {arr.Length} wrapped messages"); + muxer?.OnTransactionLog($"Processing {arr.Length} wrapped messages"); int i = 0; foreach(ref RawResult item in arr) { var inner = wrapped[i++].Wrapped; - connection?.BridgeCouldBeNull?.Multiplexer?.OnTransactionLog($"> got {item} for {inner.CommandAndKey}"); + muxer?.OnTransactionLog($"> got {item} for {inner.CommandAndKey}"); if (inner.ComputeResult(connection, in item)) { inner.Complete(); @@ -523,7 +524,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { if (op?.Wrapped is Message inner) { - inner.Fail(ConnectionFailureType.ProtocolFailure, null, "Transaction failure", connection?.BridgeCouldBeNull?.Multiplexer); + inner.Fail(ConnectionFailureType.ProtocolFailure, null, "Transaction failure", muxer); inner.Complete(); } } diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index 8759385e8..9ff2188af 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -17,11 +17,11 @@ namespace StackExchange.Redis { internal static readonly RedisValue[] EmptyArray = Array.Empty(); - private readonly object _objectOrSentinel; + private readonly object? _objectOrSentinel; private readonly ReadOnlyMemory _memory; private readonly long _overlappedBits64; - private RedisValue(long overlappedValue64, ReadOnlyMemory memory, object objectOrSentinel) + private RedisValue(long overlappedValue64, ReadOnlyMemory memory, object? objectOrSentinel) { _overlappedBits64 = overlappedValue64; _memory = memory; @@ -42,19 +42,19 @@ internal RedisValue(object obj, long overlappedBits) public RedisValue(string value) : this(0, default, value) { } [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1085:Use auto-implemented property.", Justification = "Intentional field ref")] - internal object DirectObject => _objectOrSentinel; + internal object? DirectObject => _objectOrSentinel; [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1085:Use auto-implemented property.", Justification = "Intentional field ref")] internal long DirectOverlappedBits64 => _overlappedBits64; - private readonly static object Sentinel_SignedInteger = new(); - private readonly static object Sentinel_UnsignedInteger = new(); - private readonly static object Sentinel_Raw = new(); - private readonly static object Sentinel_Double = new(); + private static readonly object Sentinel_SignedInteger = new(); + private static readonly object Sentinel_UnsignedInteger = new(); + private static readonly object Sentinel_Raw = new(); + private static readonly object Sentinel_Double = new(); /// /// Obtain this value as an object - to be used alongside Unbox /// - public object Box() + public object? Box() { var obj = _objectOrSentinel; if (obj is null || obj is string || obj is byte[]) return obj; @@ -84,7 +84,7 @@ public object Box() /// Parse this object as a value - to be used alongside Box. /// /// The value to unbox. - public static RedisValue Unbox(object value) + public static RedisValue Unbox(object? value) { var val = TryParse(value, out var valid); if (!valid) throw new ArgumentException("Could not parse value", nameof(value)); @@ -97,9 +97,9 @@ public static RedisValue Unbox(object value) public static RedisValue EmptyString { get; } = new RedisValue(0, default, Sentinel_Raw); // note: it is *really important* that this s_EmptyString assignment happens *after* the EmptyString initializer above! - static readonly object s_DoubleNAN = double.NaN, s_DoublePosInf = double.PositiveInfinity, s_DoubleNegInf = double.NegativeInfinity, + private static readonly object s_DoubleNAN = double.NaN, s_DoublePosInf = double.PositiveInfinity, s_DoubleNegInf = double.NegativeInfinity, s_EmptyString = RedisValue.EmptyString; - static readonly object[] s_CommonInt32 = Enumerable.Range(-1, 22).Select(i => (object)i).ToArray(); // [-1,20] = 22 values + private static readonly object[] s_CommonInt32 = Enumerable.Range(-1, 22).Select(i => (object)i).ToArray(); // [-1,20] = 22 values /// /// A null value. @@ -185,7 +185,7 @@ internal ulong OverlappedValueUInt64 case StorageType.UInt64: // as long as xType == yType, only need to check the bits return x._overlappedBits64 == y._overlappedBits64; case StorageType.String: - return (string)x._objectOrSentinel == (string)y._objectOrSentinel; + return (string?)x._objectOrSentinel == (string?)y._objectOrSentinel; case StorageType.Raw: return x._memory.Span.SequenceEqual(y._memory.Span); } @@ -209,14 +209,14 @@ internal ulong OverlappedValueUInt64 } // otherwise, compare as strings - return (string)x == (string)y; + return (string?)x == (string?)y; } /// /// See . /// /// The other to compare. - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj == null) return IsNull; if (obj is RedisValue typed) return Equals(typed); @@ -240,19 +240,19 @@ private static int GetHashCode(RedisValue x) StorageType.Null => -1, StorageType.Double => x.OverlappedValueDouble.GetHashCode(), StorageType.Int64 or StorageType.UInt64 => x._overlappedBits64.GetHashCode(), - StorageType.Raw => ((string)x).GetHashCode(),// to match equality - _ => x._objectOrSentinel.GetHashCode(), + StorageType.Raw => ((string)x!).GetHashCode(),// to match equality + _ => x._objectOrSentinel!.GetHashCode(), }; } /// /// Returns a string representation of the value. /// - public override string ToString() => (string)this; + public override string ToString() => (string?)this ?? string.Empty; - internal static unsafe bool Equals(byte[] x, byte[] y) + internal static unsafe bool Equals(byte[]? x, byte[]? y) { - if ((object)x == (object)y) return true; // ref equals + if ((object?)x == (object?)y) return true; // ref equals if (x == null || y == null) return false; int len = x.Length; if (len != y.Length) return false; @@ -333,7 +333,7 @@ internal StorageType Type { StorageType.Null => 0, StorageType.Raw => _memory.Length, - StorageType.String => Encoding.UTF8.GetByteCount((string)_objectOrSentinel), + StorageType.String => Encoding.UTF8.GetByteCount((string)_objectOrSentinel!), _ => throw new InvalidOperationException("Unable to compute length of type: " + Type), }; @@ -365,7 +365,7 @@ private static int CompareTo(RedisValue x, RedisValue y) case StorageType.UInt64: return x.OverlappedValueUInt64.CompareTo(y.OverlappedValueUInt64); case StorageType.String: - return string.CompareOrdinal((string)x._objectOrSentinel, (string)y._objectOrSentinel); + return string.CompareOrdinal((string)x._objectOrSentinel!, (string)y._objectOrSentinel!); case StorageType.Raw: return x._memory.Span.SequenceCompareTo(y._memory.Span); } @@ -388,7 +388,7 @@ private static int CompareTo(RedisValue x, RedisValue y) } // otherwise, compare as strings - return string.CompareOrdinal((string)x, (string)y); + return string.CompareOrdinal((string?)x, (string?)y); } catch (Exception ex) { @@ -398,7 +398,7 @@ private static int CompareTo(RedisValue x, RedisValue y) return 0; } - int IComparable.CompareTo(object obj) + int IComparable.CompareTo(object? obj) { if (obj == null) return CompareTo(Null); @@ -408,7 +408,7 @@ int IComparable.CompareTo(object obj) return CompareTo(val); } - internal static RedisValue TryParse(object obj, out bool valid) + internal static RedisValue TryParse(object? obj, out bool valid) { valid = true; switch (obj) @@ -531,7 +531,7 @@ public static implicit operator RedisValue(ReadOnlyMemory value) /// Creates a new from an . /// /// The to convert to a . - public static implicit operator RedisValue(string value) + public static implicit operator RedisValue(string? value) { if (value == null) return Null; if (value.Length == 0) return EmptyString; @@ -542,7 +542,7 @@ public static implicit operator RedisValue(string value) /// Creates a new from an . /// /// The to convert to a . - public static implicit operator RedisValue(byte[] value) + public static implicit operator RedisValue(byte[]? value) { if (value == null) return Null; if (value.Length == 0) return EmptyString; @@ -754,7 +754,7 @@ private static bool TryParseDouble(ReadOnlySpan blob, out double value) /// Converts a to a . /// /// The to convert. - public static implicit operator string(RedisValue value) + public static implicit operator string? (RedisValue value) { switch (value.Type) { @@ -762,7 +762,7 @@ public static implicit operator string(RedisValue value) case StorageType.Double: return Format.ToString(value.OverlappedValueDouble); case StorageType.Int64: return Format.ToString(value.OverlappedValueInt64); case StorageType.UInt64: return Format.ToString(value.OverlappedValueUInt64); - case StorageType.String: return (string)value._objectOrSentinel; + case StorageType.String: return (string)value._objectOrSentinel!; case StorageType.Raw: var span = value._memory.Span; if (span.IsEmpty) return ""; @@ -808,7 +808,7 @@ private static string ToHex(ReadOnlySpan src) /// Converts a to a . /// /// The to convert. - public static implicit operator byte[] (RedisValue value) + public static implicit operator byte[]? (RedisValue value) { switch (value.Type) { @@ -840,7 +840,7 @@ public static implicit operator byte[] (RedisValue value) return arr; } // fallback: stringify and encode - return Encoding.UTF8.GetBytes((string)value); + return Encoding.UTF8.GetBytes((string)value!); } /// @@ -848,27 +848,27 @@ public static implicit operator byte[] (RedisValue value) /// /// The to convert. public static implicit operator ReadOnlyMemory(RedisValue value) - => value.Type == StorageType.Raw ? value._memory : (byte[])value; + => value.Type == StorageType.Raw ? value._memory : (byte[]?)value; TypeCode IConvertible.GetTypeCode() => TypeCode.Object; - bool IConvertible.ToBoolean(IFormatProvider provider) => (bool)this; - byte IConvertible.ToByte(IFormatProvider provider) => (byte)(uint)this; - char IConvertible.ToChar(IFormatProvider provider) => (char)(uint)this; - DateTime IConvertible.ToDateTime(IFormatProvider provider) => DateTime.Parse((string)this, provider); - decimal IConvertible.ToDecimal(IFormatProvider provider) => (decimal)this; - double IConvertible.ToDouble(IFormatProvider provider) => (double)this; - short IConvertible.ToInt16(IFormatProvider provider) => (short)this; - int IConvertible.ToInt32(IFormatProvider provider) => (int)this; - long IConvertible.ToInt64(IFormatProvider provider) => (long)this; - sbyte IConvertible.ToSByte(IFormatProvider provider) => (sbyte)this; - float IConvertible.ToSingle(IFormatProvider provider) => (float)this; - string IConvertible.ToString(IFormatProvider provider) => (string)this; - - object IConvertible.ToType(Type conversionType, IFormatProvider provider) + bool IConvertible.ToBoolean(IFormatProvider? provider) => (bool)this; + byte IConvertible.ToByte(IFormatProvider? provider) => (byte)(uint)this; + char IConvertible.ToChar(IFormatProvider? provider) => (char)(uint)this; + DateTime IConvertible.ToDateTime(IFormatProvider? provider) => DateTime.Parse(((string?)this)!, provider); + decimal IConvertible.ToDecimal(IFormatProvider? provider) => (decimal)this; + double IConvertible.ToDouble(IFormatProvider? provider) => (double)this; + short IConvertible.ToInt16(IFormatProvider? provider) => (short)this; + int IConvertible.ToInt32(IFormatProvider? provider) => (int)this; + long IConvertible.ToInt64(IFormatProvider? provider) => (long)this; + sbyte IConvertible.ToSByte(IFormatProvider? provider) => (sbyte)this; + float IConvertible.ToSingle(IFormatProvider? provider) => (float)this; + string IConvertible.ToString(IFormatProvider? provider) => ((string?)this)!; + + object IConvertible.ToType(Type conversionType, IFormatProvider? provider) { if (conversionType == null) throw new ArgumentNullException(nameof(conversionType)); - if (conversionType == typeof(byte[])) return (byte[])this; + if (conversionType == typeof(byte[])) return ((byte[]?)this)!; if (conversionType == typeof(ReadOnlyMemory)) return (ReadOnlyMemory)this; if (conversionType == typeof(RedisValue)) return this; return System.Type.GetTypeCode(conversionType) switch @@ -876,7 +876,7 @@ object IConvertible.ToType(Type conversionType, IFormatProvider provider) TypeCode.Boolean => (bool)this, TypeCode.Byte => checked((byte)(uint)this), TypeCode.Char => checked((char)(uint)this), - TypeCode.DateTime => DateTime.Parse((string)this, provider), + TypeCode.DateTime => DateTime.Parse(((string?)this)!, provider), TypeCode.Decimal => (decimal)this, TypeCode.Double => (double)this, TypeCode.Int16 => (short)this, @@ -884,7 +884,7 @@ object IConvertible.ToType(Type conversionType, IFormatProvider provider) TypeCode.Int64 => (long)this, TypeCode.SByte => (sbyte)this, TypeCode.Single => (float)this, - TypeCode.String => (string)this, + TypeCode.String => ((string?)this)!, TypeCode.UInt16 => checked((ushort)(uint)this), TypeCode.UInt32 => (uint)this, TypeCode.UInt64 => (ulong)this, @@ -893,9 +893,9 @@ object IConvertible.ToType(Type conversionType, IFormatProvider provider) }; } - ushort IConvertible.ToUInt16(IFormatProvider provider) => checked((ushort)(uint)this); - uint IConvertible.ToUInt32(IFormatProvider provider) => (uint)this; - ulong IConvertible.ToUInt64(IFormatProvider provider) => (ulong)this; + ushort IConvertible.ToUInt16(IFormatProvider? provider) => checked((ushort)(uint)this); + uint IConvertible.ToUInt32(IFormatProvider? provider) => (uint)this; + ulong IConvertible.ToUInt64(IFormatProvider? provider) => (ulong)this; /// /// Attempt to reduce to canonical terms ahead of time; parses integers, floats, etc @@ -912,7 +912,7 @@ internal RedisValue Simplify() switch (Type) { case StorageType.String: - string s = (string)_objectOrSentinel; + string s = (string)_objectOrSentinel!; if (Format.CouldBeInteger(s)) { if (Format.TryParseInt64(s, out i64)) return i64; @@ -955,7 +955,7 @@ public bool TryParse(out long val) val = default; return false; case StorageType.String: - return Format.TryParseInt64((string)_objectOrSentinel, out val); + return Format.TryParseInt64((string)_objectOrSentinel!, out val); case StorageType.Raw: return Format.TryParseInt64(_memory.Span, out val); case StorageType.Double: @@ -1008,7 +1008,7 @@ public bool TryParse(out double val) val = OverlappedValueDouble; return true; case StorageType.String: - return Format.TryParseDouble((string)_objectOrSentinel, out val); + return Format.TryParseDouble((string)_objectOrSentinel!, out val); case StorageType.Raw: return TryParseDouble(_memory.Span, out val); case StorageType.Null: @@ -1040,7 +1040,7 @@ public static RedisValue CreateFrom(MemoryStream stream) } } - private static readonly FieldInfo + private static readonly FieldInfo? s_origin = typeof(MemoryStream).GetField("_origin", BindingFlags.NonPublic | BindingFlags.Instance), s_buffer = typeof(MemoryStream).GetField("_buffer", BindingFlags.NonPublic | BindingFlags.Instance); @@ -1050,8 +1050,8 @@ private static bool ReflectionTryGetBuffer(MemoryStream ms, out ArraySegment(arr, offset, checked((int)ms.Length)); return true; } @@ -1078,8 +1078,8 @@ public bool StartsWith(RedisValue value) switch (thisType) { case StorageType.String: - var sThis = ((string)_objectOrSentinel); - var sOther = ((string)value._objectOrSentinel); + var sThis = ((string)_objectOrSentinel!); + var sOther = ((string)value._objectOrSentinel!); return sThis.StartsWith(sOther, StringComparison.Ordinal); case StorageType.Raw: rawThis = _memory; @@ -1087,7 +1087,7 @@ public bool StartsWith(RedisValue value) return rawThis.Span.StartsWith(rawOther.Span); } } - byte[] arr0 = null, arr1 = null; + byte[]? arr0 = null, arr1 = null; try { rawThis = AsMemory(out arr0); @@ -1102,7 +1102,7 @@ public bool StartsWith(RedisValue value) } } - private ReadOnlyMemory AsMemory(out byte[] leased) + private ReadOnlyMemory AsMemory(out byte[]? leased) { switch (Type) { @@ -1110,7 +1110,7 @@ private ReadOnlyMemory AsMemory(out byte[] leased) leased = null; return _memory; case StorageType.String: - string s = (string)_objectOrSentinel; + string s = (string)_objectOrSentinel!; HaveString: if (s.Length == 0) { diff --git a/src/StackExchange.Redis/ResultBox.cs b/src/StackExchange.Redis/ResultBox.cs index 53650ea89..111394321 100644 --- a/src/StackExchange.Redis/ResultBox.cs +++ b/src/StackExchange.Redis/ResultBox.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; @@ -14,13 +15,13 @@ internal interface IResultBox } internal interface IResultBox : IResultBox { - T GetResult(out Exception ex, bool canRecycle = false); + T? GetResult(out Exception? ex, bool canRecycle = false); void SetResult(T value); } internal abstract class SimpleResultBox : IResultBox { - private volatile Exception _exception; + private volatile Exception? _exception; bool IResultBox.IsAsync => false; bool IResultBox.IsFaulted => _exception != null; @@ -42,7 +43,7 @@ void IResultBox.ActivateContinuations() // about any confusion in stack-trace internal static readonly Exception CancelledException = new TaskCanceledException(); - protected Exception Exception + protected Exception? Exception { get => _exception; set => _exception = value; @@ -52,10 +53,10 @@ protected Exception Exception internal sealed class SimpleResultBox : SimpleResultBox, IResultBox { private SimpleResultBox() { } - private T _value; + private T? _value; [ThreadStatic] - private static SimpleResultBox _perThreadInstance; + private static SimpleResultBox? _perThreadInstance; public static IResultBox Create() => new SimpleResultBox(); public static IResultBox Get() // includes recycled boxes; used from sync, so makes re-use easy @@ -64,16 +65,17 @@ private SimpleResultBox() { } _perThreadInstance = null; // in case of oddness; only set back when recycled return obj; } + void IResultBox.SetResult(T value) => _value = value; - T IResultBox.GetResult(out Exception ex, bool canRecycle) + T? IResultBox.GetResult(out Exception? ex, bool canRecycle) { var value = _value; ex = Exception; if (canRecycle) { Exception = null; - _value = default; + _value = default!; _perThreadInstance = this; } return value; @@ -85,10 +87,10 @@ internal sealed class TaskResultBox : TaskCompletionSource, IResultBox // you might be asking "wait, doesn't the Task own these?", to which // I say: no; we can't set *immediately* due to thread-theft etc, hence // the fun TryComplete indirection - so we need somewhere to buffer them - private volatile Exception _exception; - private T _value; + private volatile Exception? _exception; + private T _value = default!; - private TaskResultBox(object asyncState, TaskCreationOptions creationOptions) : base(asyncState, creationOptions) + private TaskResultBox(object? asyncState, TaskCreationOptions creationOptions) : base(asyncState, creationOptions) { } bool IResultBox.IsAsync => true; @@ -101,14 +103,14 @@ private TaskResultBox(object asyncState, TaskCreationOptions creationOptions) : void IResultBox.SetResult(T value) => _value = value; - T IResultBox.GetResult(out Exception ex, bool _) + T? IResultBox.GetResult(out Exception? ex, bool _) { ex = _exception; return _value; // nothing to do re recycle: TaskCompletionSource cannot be recycled } - static readonly WaitCallback s_ActivateContinuations = state => ((TaskResultBox)state).ActivateContinuationsImpl(); + private static readonly WaitCallback s_ActivateContinuations = state => ((TaskResultBox)state!).ActivateContinuationsImpl(); void IResultBox.ActivateContinuations() { if ((Task.CreationOptions & TaskCreationOptions.RunContinuationsAsynchronously) == 0) @@ -137,7 +139,7 @@ private void ActivateContinuationsImpl() } } - public static IResultBox Create(out TaskCompletionSource source, object asyncState) + public static IResultBox Create(out TaskCompletionSource source, object? asyncState) { // it might look a little odd to return the same object as two different things, // but that's because it is serving two purposes, and I want to make it clear diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 89e630813..142ffc22e 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; @@ -25,8 +26,10 @@ public static readonly ResultProcessor BackgroundSaveStarted = new ExpectBasicStringProcessor(CommonReplies.backgroundSavingStarted_trimmed, startsWith: true), BackgroundSaveAOFStarted = new ExpectBasicStringProcessor(CommonReplies.backgroundSavingAOFStarted_trimmed, startsWith: true); + public static readonly ResultProcessor + ByteArray = new ByteArrayProcessor(); + public static readonly ResultProcessor - ByteArray = new ByteArrayProcessor(), ScriptLoad = new ScriptLoadProcessor(); public static readonly ResultProcessor @@ -76,7 +79,7 @@ public static readonly ResultProcessor> public static readonly ResultProcessor RedisValueArray = new RedisValueArrayProcessor(); - public static readonly ResultProcessor + public static readonly ResultProcessor StringArray = new StringArrayProcessor(); public static readonly ResultProcessor @@ -121,14 +124,14 @@ public static readonly StreamPendingMessagesProcessor public static ResultProcessor GeoRadiusArray(GeoRadiusOptions options) => GeoRadiusResultArrayProcessor.Get(options); - public static readonly ResultProcessor - String = new StringProcessor(), + public static readonly ResultProcessor + String = new StringProcessor(), TieBreaker = new TieBreakerProcessor(), ClusterNodesRaw = new ClusterNodesRawProcessor(); #region Sentinel - public static readonly ResultProcessor + public static readonly ResultProcessor SentinelPrimaryEndpoint = new SentinelGetPrimaryAddressByNameProcessor(); public static readonly ResultProcessor @@ -151,7 +154,7 @@ public static readonly HashEntryArrayProcessor HashEntryArray = new HashEntryArrayProcessor(); [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Conditionally run on instance")] - public void ConnectionFail(Message message, ConnectionFailureType fail, Exception innerException, string annotation, ConnectionMultiplexer muxer) + public void ConnectionFail(Message message, ConnectionFailureType fail, Exception? innerException, string? annotation, ConnectionMultiplexer? muxer) { PhysicalConnection.IdentifyFailureType(innerException, ref fail); @@ -170,17 +173,13 @@ public void ConnectionFail(Message message, ConnectionFailureType fail, Exceptio SetException(message, ex); } - public static void ConnectionFail(Message message, ConnectionFailureType fail, string errorMessage) - { + public static void ConnectionFail(Message message, ConnectionFailureType fail, string errorMessage) => SetException(message, new RedisConnectionException(fail, errorMessage)); - } - public static void ServerFail(Message message, string errorMessage) - { + public static void ServerFail(Message message, string errorMessage) => SetException(message, new RedisServerException(errorMessage)); - } - public static void SetException(Message message, Exception ex) + public static void SetException(Message? message, Exception ex) { var box = message?.ResultBox; box?.SetException(ex); @@ -201,27 +200,28 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in { if (result.StartsWith(CommonReplies.NOAUTH)) bridge?.Multiplexer?.SetAuthSuspect(); - var server = bridge.ServerEndPoint; + var server = bridge?.ServerEndPoint; bool log = !message.IsInternalCall; bool isMoved = result.StartsWith(CommonReplies.MOVED); bool wasNoRedirect = (message.Flags & CommandFlags.NoRedirect) != 0; - string err = string.Empty; + string? err = string.Empty; bool unableToConnectError = false; if (isMoved || result.StartsWith(CommonReplies.ASK)) { message.SetResponseReceived(); log = false; - string[] parts = result.GetString().Split(StringSplits.Space, 3); - EndPoint endpoint; + string[] parts = result.GetString()!.Split(StringSplits.Space, 3); if (Format.TryParseInt32(parts[1], out int hashSlot) - && (endpoint = Format.TryParseEndPoint(parts[2])) != null) + && Format.TryParseEndPoint(parts[2], out var endpoint)) { // no point sending back to same server, and no point sending to a dead server - if (!Equals(server.EndPoint, endpoint)) + if (!Equals(server?.EndPoint, endpoint)) { if (bridge == null) - { } // already toast + { + // already toast + } else if (bridge.Multiplexer.TryResend(hashSlot, message, endpoint, isMoved)) { bridge.Multiplexer.Trace(message.Command + " re-issued to " + endpoint, isMoved ? "MOVED" : "ASK"); @@ -246,12 +246,12 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in if (string.IsNullOrWhiteSpace(err)) { - err = result.GetString(); + err = result.GetString()!; } - if (log) + if (log && server != null) { - bridge.Multiplexer.OnErrorMessage(server.EndPoint, err); + bridge?.Multiplexer.OnErrorMessage(server.EndPoint, err); } bridge?.Multiplexer?.Trace("Completed with error: " + err + " (" + GetType().Name + ")", ToString()); if (unableToConnectError) @@ -341,10 +341,8 @@ public sealed class TimingProcessor : ResultProcessor { private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; - public static TimerMessage CreateMessage(int db, CommandFlags flags, RedisCommand command, RedisValue value = default(RedisValue)) - { - return new TimerMessage(db, flags, command, value); - } + public static TimerMessage CreateMessage(int db, CommandFlags flags, RedisCommand command, RedisValue value = default) => + new TimerMessage(db, flags, command, value); protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { @@ -400,8 +398,8 @@ protected override void WriteImpl(PhysicalConnection physical) public sealed class TrackSubscriptionsProcessor : ResultProcessor { - private ConnectionMultiplexer.Subscription Subscription { get; } - public TrackSubscriptionsProcessor(ConnectionMultiplexer.Subscription sub) => Subscription = sub; + private ConnectionMultiplexer.Subscription? Subscription { get; } + public TrackSubscriptionsProcessor(ConnectionMultiplexer.Subscription? sub) => Subscription = sub; protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { @@ -459,53 +457,37 @@ internal sealed class ScriptLoadProcessor : ResultProcessor { private static readonly Regex sha1 = new Regex("^[0-9a-f]{40}$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - internal static bool IsSHA1(string script) - { - return script != null && sha1.IsMatch(script); - } + internal static bool IsSHA1(string script) => script is not null && sha1.IsMatch(script); internal const int Sha1HashLength = 20; internal static byte[] ParseSHA1(byte[] value) { - if (value?.Length == Sha1HashLength * 2) + static int FromHex(char c) { - var tmp = new byte[Sha1HashLength]; - int charIndex = 0; - for (int i = 0; i < tmp.Length; i++) - { - int x = FromHex((char)value[charIndex++]), y = FromHex((char)value[charIndex++]); - if (x < 0 || y < 0) return null; - tmp[i] = (byte)((x << 4) | y); - } - return tmp; + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; } - return null; - } - internal static byte[] ParseSHA1(string value) - { - if (value?.Length == (Sha1HashLength * 2) && sha1.IsMatch(value)) + if (value?.Length == Sha1HashLength * 2) { var tmp = new byte[Sha1HashLength]; int charIndex = 0; for (int i = 0; i < tmp.Length; i++) { - int x = FromHex(value[charIndex++]), y = FromHex(value[charIndex++]); - if (x < 0 || y < 0) return null; + int x = FromHex((char)value[charIndex++]), y = FromHex((char)value[charIndex++]); + if (x < 0 || y < 0) + { + throw new ArgumentException("Unable to parse response as SHA1", nameof(value)); + } tmp[i] = (byte)((x << 4) | y); } return tmp; } - return null; + throw new ArgumentException("Unable to parse response as SHA1", nameof(value)); } - private static int FromHex(char c) - { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - return -1; - } // note that top-level error messages still get handled by SetResult, but nested errors // (is that a thing?) will be wrapped in the RedisResult protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) @@ -516,11 +498,10 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var asciiHash = result.GetBlob(); if (asciiHash == null || asciiHash.Length != (Sha1HashLength * 2)) return false; - byte[] hash = null; - if (!message.IsInternalCall) - { - hash = ParseSHA1(asciiHash); // external caller wants the hex bytes, not the ASCII bytes - } + // External caller wants the hex bytes, not the ASCII bytes + // For nullability/consistency reasons, we always do the parse here. + byte[] hash = ParseSHA1(asciiHash); + if (message is RedisDatabase.ScriptLoadMessage sl) { connection.BridgeCouldBeNull?.ServerEndPoint?.AddScript(sl.Script, asciiHash); @@ -568,26 +549,22 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal sealed class SortedSetEntryArrayProcessor : ValuePairInterleavedProcessorBase { - protected override SortedSetEntry Parse(in RawResult first, in RawResult second) - { - return new SortedSetEntry(first.AsRedisValue(), second.TryGetDouble(out double val) ? val : double.NaN); - } + protected override SortedSetEntry Parse(in RawResult first, in RawResult second) => + new SortedSetEntry(first.AsRedisValue(), second.TryGetDouble(out double val) ? val : double.NaN); } internal sealed class HashEntryArrayProcessor : ValuePairInterleavedProcessorBase { - protected override HashEntry Parse(in RawResult first, in RawResult second) - { - return new HashEntry(first.AsRedisValue(), second.AsRedisValue()); - } + protected override HashEntry Parse(in RawResult first, in RawResult second) => + new HashEntry(first.AsRedisValue(), second.AsRedisValue()); } internal abstract class ValuePairInterleavedProcessorBase : ResultProcessor { - public bool TryParse(in RawResult result, out T[] pairs) + public bool TryParse(in RawResult result, out T[]? pairs) => TryParse(result, out pairs, false, out _); - public bool TryParse(in RawResult result, out T[] pairs, bool allowOversized, out int count) + public bool TryParse(in RawResult result, out T[]? pairs, bool allowOversized, out int count) { count = 0; switch (result.Type) @@ -637,9 +614,9 @@ public bool TryParse(in RawResult result, out T[] pairs, bool allowOversized, ou protected abstract T Parse(in RawResult first, in RawResult second); protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - if (TryParse(result, out T[] arr)) + if (TryParse(result, out T[]? arr)) { - SetResult(message, arr); + SetResult(message, arr!); return true; } return false; @@ -648,8 +625,8 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal sealed class AutoConfigureProcessor : ResultProcessor { - private LogProxy Log { get; } - public AutoConfigureProcessor(LogProxy log = null) => Log = log; + private LogProxy? Log { get; } + public AutoConfigureProcessor(LogProxy? log = null) => Log = log; public override bool SetResult(PhysicalConnection connection, Message message, in RawResult result) { @@ -675,21 +652,24 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes case ResultType.BulkString: if (message?.Command == RedisCommand.INFO) { - string info = result.GetString(), line; + string? info = result.GetString(); if (string.IsNullOrWhiteSpace(info)) { SetResult(message, true); return true; } - string primaryHost = null, primaryPort = null; + string? primaryHost = null, primaryPort = null; bool roleSeen = false; using (var reader = new StringReader(info)) { - while ((line = reader.ReadLine()) != null) + while (reader.ReadLine() is string line) { - if (string.IsNullOrWhiteSpace(line) || line.StartsWith("# ")) continue; + if (string.IsNullOrWhiteSpace(line) || line.StartsWith("# ")) + { + continue; + } - string val; + string? val; if ((val = Extract(line, "role:")) != null) { roleSeen = true; @@ -716,7 +696,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } else if ((val = Extract(line, "redis_version:")) != null) { - if (Version.TryParse(val, out Version version)) + if (Version.TryParse(val, out Version? version)) { server.Version = version; Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (INFO) version: " + version); @@ -745,10 +725,10 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes server.RunId = val; } } - if (roleSeen) + if (roleSeen && Format.TryParseEndPoint(primaryHost!, primaryPort, out var sep)) { // These are in the same section, if present - server.PrimaryEndPoint = Format.TryParseEndPoint(primaryHost, primaryPort); + server.PrimaryEndPoint = sep; } } } @@ -819,7 +799,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return false; } - private static string Extract(string line, string prefix) + private static string? Extract(string line, string prefix) { if (line.StartsWith(prefix)) return line.Substring(prefix.Length).Trim(); return null; @@ -864,7 +844,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - private sealed class ByteArrayProcessor : ResultProcessor + private sealed class ByteArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { @@ -895,7 +875,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes switch (result.Type) { case ResultType.BulkString: - string nodes = result.GetString(); + string nodes = result.GetString()!; var bridge = connection.BridgeCouldBeNull; if (bridge != null) bridge.ServerEndPoint.ServerType = ServerType.Cluster; var config = Parse(connection, nodes); @@ -906,7 +886,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - private sealed class ClusterNodesRawProcessor : ResultProcessor + private sealed class ClusterNodesRawProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { @@ -915,7 +895,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes case ResultType.Integer: case ResultType.SimpleString: case ResultType.BulkString: - string nodes = result.GetString(); + string nodes = result.GetString()!; try { ClusterNodesProcessor.Parse(connection, nodes); } catch @@ -931,8 +911,12 @@ private sealed class ConnectionIdentityProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - SetResult(message, connection.BridgeCouldBeNull?.ServerEndPoint?.EndPoint); - return true; + if (connection.BridgeCouldBeNull is PhysicalBridge bridge) + { + SetResult(message, bridge.ServerEndPoint.EndPoint); + return true; + } + return false; } } @@ -1034,11 +1018,11 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { if (result.Type == ResultType.BulkString) { - string category = Normalize(null), line; + string category = Normalize(null); var list = new List>>(); - using (var reader = new StringReader(result.GetString())) + using (var reader = new StringReader(result.GetString()!)) { - while ((line = reader.ReadLine()) != null) + while (reader.ReadLine() is string line) { if (string.IsNullOrWhiteSpace(line)) continue; if (line.StartsWith("# ")) @@ -1061,10 +1045,8 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return false; } - private static string Normalize(string category) - { - return string.IsNullOrWhiteSpace(category) ? "miscellaneous" : category.Trim(); - } + private static string Normalize(string? category) => + category.IsNullOrWhiteSpace() ? "miscellaneous" : category.Trim(); } private class Int64Processor : ResultProcessor @@ -1167,9 +1149,9 @@ public RedisChannelArrayProcessor(RedisChannel.PatternMode mode) private readonly struct ChannelState // I would use a value-tuple here, but that is binding hell { - public readonly byte[] Prefix; + public readonly byte[]? Prefix; public readonly RedisChannel.PatternMode Mode; - public ChannelState(byte[] prefix, RedisChannel.PatternMode mode) + public ChannelState(byte[]? prefix, RedisChannel.PatternMode mode) { Prefix = prefix; Mode = mode; @@ -1182,7 +1164,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes case ResultType.MultiBulk: var final = result.ToArray( (in RawResult item, in ChannelState state) => item.AsRedisChannel(state.Prefix, state.Mode), - new ChannelState(connection.ChannelPrefix, mode)); + new ChannelState(connection.ChannelPrefix, mode))!; SetResult(message, final); return true; @@ -1198,7 +1180,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes switch (result.Type) { case ResultType.MultiBulk: - var arr = result.GetItemsAsKeys(); + var arr = result.GetItemsAsKeys()!; SetResult(message, arr); return true; } @@ -1230,7 +1212,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { case ResultType.SimpleString: case ResultType.BulkString: - string s = result.GetString(); + string s = result.GetString()!; RedisType value; if (string.Equals(s, "zset", StringComparison.OrdinalIgnoreCase)) value = Redis.RedisType.SortedSet; else if (!Enum.TryParse(s, true, out value)) value = global::StackExchange.Redis.RedisType.Unknown; @@ -1256,7 +1238,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes SetResult(message, arr); return true; case ResultType.MultiBulk: - arr = result.GetItemsAsValues(); + arr = result.GetItemsAsValues()!; SetResult(message, arr); return true; } @@ -1264,14 +1246,14 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - private sealed class StringArrayProcessor : ResultProcessor + private sealed class StringArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { switch (result.Type) { case ResultType.MultiBulk: - var arr = result.GetItemsAsStrings(); + var arr = result.GetItemsAsStrings()!; SetResult(message, arr); return true; @@ -1303,7 +1285,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes switch (result.Type) { case ResultType.MultiBulk: - var arr = result.GetItemsAsGeoPositionArray(); + var arr = result.GetItemsAsGeoPositionArray()!; SetResult(message, arr); return true; @@ -1341,7 +1323,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { case ResultType.MultiBulk: var typed = result.ToArray( - (in RawResult item, in GeoRadiusOptions radiusOptions) => Parse(item, radiusOptions), options); + (in RawResult item, in GeoRadiusOptions radiusOptions) => Parse(item, radiusOptions), options)!; SetResult(message, typed); return true; } @@ -1408,19 +1390,19 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } ref var val = ref items[0]; - Role role; + Role? role; if (val.IsEqual(RedisLiterals.master)) role = ParsePrimary(items); - else if (val.IsEqual(RedisLiterals.slave)) role = ParseReplica(items, RedisLiterals.slave); - else if (val.IsEqual(RedisLiterals.replica)) role = ParseReplica(items, RedisLiterals.replica); // for when "slave" is deprecated + else if (val.IsEqual(RedisLiterals.slave)) role = ParseReplica(items, RedisLiterals.slave!); + else if (val.IsEqual(RedisLiterals.replica)) role = ParseReplica(items, RedisLiterals.replica!); // for when "slave" is deprecated else if (val.IsEqual(RedisLiterals.sentinel)) role = ParseSentinel(items); - else role = new Role.Unknown(val.GetString()); + else role = new Role.Unknown(val.GetString()!); if (role is null) return false; SetResult(message, role); return true; } - private static Role ParsePrimary(in Sequence items) + private static Role? ParsePrimary(in Sequence items) { if (items.Length < 3) { @@ -1465,7 +1447,7 @@ private static bool TryParsePrimaryReplica(in Sequence items, out Rol return false; } - var primaryIp = items[0].GetString(); + var primaryIp = items[0].GetString()!; if (!items[1].TryGetInt64(out var primaryPort) || primaryPort > int.MaxValue) { @@ -1483,14 +1465,14 @@ private static bool TryParsePrimaryReplica(in Sequence items, out Rol return true; } - private static Role ParseReplica(in Sequence items, string role) + private static Role? ParseReplica(in Sequence items, string role) { if (items.Length < 5) { return null; } - var primaryIp = items[1].GetString(); + var primaryIp = items[1].GetString()!; if (!items[2].TryGetInt64(out var primaryPort) || primaryPort > int.MaxValue) { @@ -1499,13 +1481,13 @@ private static Role ParseReplica(in Sequence items, string role) ref var val = ref items[3]; string replicationState; - if (val.IsEqual(RedisLiterals.connect)) replicationState = RedisLiterals.connect; - else if (val.IsEqual(RedisLiterals.connecting)) replicationState = RedisLiterals.connecting; - else if (val.IsEqual(RedisLiterals.sync)) replicationState = RedisLiterals.sync; - else if (val.IsEqual(RedisLiterals.connected)) replicationState = RedisLiterals.connected; - else if (val.IsEqual(RedisLiterals.none)) replicationState = RedisLiterals.none; - else if (val.IsEqual(RedisLiterals.handshake)) replicationState = RedisLiterals.handshake; - else replicationState = val.GetString(); + if (val.IsEqual(RedisLiterals.connect)) replicationState = RedisLiterals.connect!; + else if (val.IsEqual(RedisLiterals.connecting)) replicationState = RedisLiterals.connecting!; + else if (val.IsEqual(RedisLiterals.sync)) replicationState = RedisLiterals.sync!; + else if (val.IsEqual(RedisLiterals.connected)) replicationState = RedisLiterals.connected!; + else if (val.IsEqual(RedisLiterals.none)) replicationState = RedisLiterals.none!; + else if (val.IsEqual(RedisLiterals.handshake)) replicationState = RedisLiterals.handshake!; + else replicationState = val.GetString()!; if (!items[4].TryGetInt64(out var replicationOffset)) { @@ -1515,13 +1497,13 @@ private static Role ParseReplica(in Sequence items, string role) return new Role.Replica(role, primaryIp, (int)primaryPort, replicationState, replicationOffset); } - private static Role ParseSentinel(in Sequence items) + private static Role? ParseSentinel(in Sequence items) { if (items.Length < 2) { return null; } - var primaries = items[1].GetItemsAsStrings(); + var primaries = items[1].GetItemsAsStrings()!; return new Role.Sentinel(primaries); } } @@ -1535,7 +1517,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes case ResultType.Integer: case ResultType.SimpleString: case ResultType.BulkString: - SetResult(message, result.AsLease()); + SetResult(message, result.AsLease()!); return true; } return false; @@ -1559,8 +1541,7 @@ public override bool SetResult(PhysicalConnection connection, Message message, i // (is that a thing?) will be wrapped in the RedisResult protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - var value = Redis.RedisResult.TryCreate(connection, result); - if (value != null) + if (RedisResult.TryCreate(connection, result, out var value)) { SetResult(message, value); return true; @@ -1682,7 +1663,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes // details[0] = Name of the Stream // details[1] = Multibulk Array of Stream Entries return new RedisStream(key: details[0].AsRedisKey(), - entries: obj.ParseRedisStreamEntries(details[1])); + entries: obj.ParseRedisStreamEntries(details[1])!); }, this); SetResult(message, streams); @@ -1712,7 +1693,7 @@ protected override StreamConsumerInfo ParseItem(in RawResult result) // 6) (integer)83841983 var arr = result.GetItems(); - string name = default; + string? name = default; int pendingMessageCount = default; long idleTimeInMilliseconds = default; @@ -1720,7 +1701,7 @@ protected override StreamConsumerInfo ParseItem(in RawResult result) KeyValuePairParser.TryRead(arr, KeyValuePairParser.Pending, ref pendingMessageCount); KeyValuePairParser.TryRead(arr, KeyValuePairParser.Idle, ref idleTimeInMilliseconds); - return new StreamConsumerInfo(name, pendingMessageCount, idleTimeInMilliseconds); + return new StreamConsumerInfo(name!, pendingMessageCount, idleTimeInMilliseconds); } } @@ -1759,14 +1740,14 @@ internal static bool TryRead(Sequence pairs, in CommandBytes key, ref return false; } - internal static bool TryRead(Sequence pairs, in CommandBytes key, ref string value) + internal static bool TryRead(Sequence pairs, in CommandBytes key, [NotNullWhen(true)] ref string? value) { var len = pairs.Length / 2; for (int i = 0; i < len; i++) { if (pairs[i * 2].IsEqual(key)) { - value = pairs[(i * 2) + 1].GetString(); + value = pairs[(i * 2) + 1].GetString()!; return true; } } @@ -1800,7 +1781,7 @@ protected override StreamGroupInfo ParseItem(in RawResult result) // 8) "1588152498034-0" var arr = result.GetItems(); - string name = default, lastDeliveredId = default; + string? name = default, lastDeliveredId = default; int consumerCount = default, pendingMessageCount = default; KeyValuePairParser.TryRead(arr, KeyValuePairParser.Name, ref name); @@ -1808,7 +1789,7 @@ protected override StreamGroupInfo ParseItem(in RawResult result) KeyValuePairParser.TryRead(arr, KeyValuePairParser.Pending, ref pendingMessageCount); KeyValuePairParser.TryRead(arr, KeyValuePairParser.LastDeliveredId, ref lastDeliveredId); - return new StreamGroupInfo(name, consumerCount, pendingMessageCount, lastDeliveredId); + return new StreamGroupInfo(name!, consumerCount, pendingMessageCount, lastDeliveredId); } } @@ -1943,7 +1924,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return false; } - StreamConsumer[] consumers = null; + StreamConsumer[]? consumers = null; // If there are no consumers as of yet for the given group, the last // item in the response array will be null. @@ -2013,16 +1994,8 @@ protected static StreamEntry ParseRedisStreamEntry(in RawResult item) return new StreamEntry(id: entryDetails[0].AsRedisValue(), values: ParseStreamEntryValues(entryDetails[1])); } - protected StreamEntry[] ParseRedisStreamEntries(in RawResult result) - { - if (result.Type != ResultType.MultiBulk) - { - return null; - } - - return result.GetItems().ToArray( - (in RawResult item, in StreamProcessorBase _) => ParseRedisStreamEntry(item), this); - } + protected StreamEntry[] ParseRedisStreamEntries(in RawResult result) => + result.GetItems().ToArray((in RawResult item, in StreamProcessorBase _) => ParseRedisStreamEntry(item), this); protected static NameValueEntry[] ParseStreamEntryValues(in RawResult result) { @@ -2043,7 +2016,7 @@ protected static NameValueEntry[] ParseStreamEntryValues(in RawResult result) if (result.Type != ResultType.MultiBulk || result.IsNull) { - return null; + return Array.Empty(); } var arr = result.GetItems(); @@ -2068,13 +2041,11 @@ protected static NameValueEntry[] ParseStreamEntryValues(in RawResult result) private sealed class StringPairInterleavedProcessor : ValuePairInterleavedProcessorBase> { - protected override KeyValuePair Parse(in RawResult first, in RawResult second) - { - return new KeyValuePair(first.GetString(), second.GetString()); - } + protected override KeyValuePair Parse(in RawResult first, in RawResult second) => + new KeyValuePair(first.GetString()!, second.GetString()!); } - private sealed class StringProcessor : ResultProcessor + private sealed class StringProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { @@ -2098,7 +2069,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - private sealed class TieBreakerProcessor : ResultProcessor + private sealed class TieBreakerProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { @@ -2106,7 +2077,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { case ResultType.SimpleString: case ResultType.BulkString: - var tieBreaker = result.GetString(); + var tieBreaker = result.GetString()!; SetResult(message, tieBreaker); try @@ -2135,7 +2106,7 @@ public TracerProcessor(bool establishConnection) public override bool SetResult(PhysicalConnection connection, Message message, in RawResult result) { - connection?.BridgeCouldBeNull?.Multiplexer.OnInfoMessage($"got '{result}' for '{message.CommandAndKey}' on '{connection}'"); + connection.BridgeCouldBeNull?.Multiplexer.OnInfoMessage($"got '{result}' for '{message.CommandAndKey}' on '{connection}'"); var final = base.SetResult(connection, message, result); if (result.IsError) { @@ -2219,7 +2190,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes #region Sentinel - private sealed class SentinelGetPrimaryAddressByNameProcessor : ResultProcessor + private sealed class SentinelGetPrimaryAddressByNameProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { @@ -2233,7 +2204,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } else if (items.Length == 2 && items[1].TryGetInt64(out var port)) { - SetResult(message, Format.ParseEndPoint(items[0].GetString(), checked((int)port))); + SetResult(message, Format.ParseEndPoint(items[0].GetString()!, checked((int)port))); return true; } else if (items.Length == 0) @@ -2259,7 +2230,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes foreach (RawResult item in result.GetItems()) { var pairs = item.GetItems(); - string ip = null; + string? ip = null; int port = default; if (KeyValuePairParser.TryRead(pairs, in KeyValuePairParser.IP, ref ip) && KeyValuePairParser.TryRead(pairs, in KeyValuePairParser.Port, ref port)) @@ -2293,7 +2264,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes foreach (RawResult item in result.GetItems()) { var pairs = item.GetItems(); - string ip = null; + string? ip = null; int port = default; if (KeyValuePairParser.TryRead(pairs, in KeyValuePairParser.IP, ref ip) && KeyValuePairParser.TryRead(pairs, in KeyValuePairParser.Port, ref port)) @@ -2337,15 +2308,15 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var returnArray = result.ToArray[], StringPairInterleavedProcessor>( (in RawResult rawInnerArray, in StringPairInterleavedProcessor proc) => { - if (proc.TryParse(rawInnerArray, out KeyValuePair[] kvpArray)) + if (proc.TryParse(rawInnerArray, out KeyValuePair[]? kvpArray)) { - return kvpArray; + return kvpArray!; } else { throw new ArgumentOutOfRangeException(nameof(rawInnerArray), $"Error processing {message.CommandAndKey}, could not decode array '{rawInnerArray}'"); } - }, innerProcessor); + }, innerProcessor)!; SetResult(message, returnArray); return true; @@ -2359,7 +2330,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal abstract class ResultProcessor : ResultProcessor { - protected static void SetResult(Message message, T value) + protected static void SetResult(Message? message, T value) { if (message == null) return; var box = message.ResultBox as IResultBox; diff --git a/src/StackExchange.Redis/Role.cs b/src/StackExchange.Redis/Role.cs index c2b1650c4..f87f763c0 100644 --- a/src/StackExchange.Redis/Role.cs +++ b/src/StackExchange.Redis/Role.cs @@ -8,6 +8,8 @@ namespace StackExchange.Redis /// https://redis.io/commands/role public abstract class Role { + internal static Unknown Null { get; } = new Unknown(""); + /// /// One of "master", "slave" (aka replica), or "sentinel". /// @@ -62,7 +64,7 @@ internal Replica(string ip, int port, long offset) } } - internal Master(long offset, ICollection replicas) : base(RedisLiterals.master) + internal Master(long offset, ICollection replicas) : base(RedisLiterals.master!) { ReplicationOffset = offset; Replicas = replicas; @@ -113,9 +115,9 @@ public sealed class Sentinel : Role /// /// Primary names monitored by this sentinel node. /// - public ICollection MonitoredMasters { get; } + public ICollection MonitoredMasters { get; } - internal Sentinel(ICollection primaries) : base(RedisLiterals.sentinel) + internal Sentinel(ICollection primaries) : base(RedisLiterals.sentinel!) { MonitoredMasters = primaries; } diff --git a/src/StackExchange.Redis/ScriptParameterMapper.cs b/src/StackExchange.Redis/ScriptParameterMapper.cs index 314f6a835..17920bcb5 100644 --- a/src/StackExchange.Redis/ScriptParameterMapper.cs +++ b/src/StackExchange.Redis/ScriptParameterMapper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -15,7 +16,7 @@ public readonly struct ScriptParameters public readonly RedisKey[] Keys; public readonly RedisValue[] Arguments; - public static readonly ConstructorInfo Cons = typeof(ScriptParameters).GetConstructor(new[] { typeof(RedisKey[]), typeof(RedisValue[]) }); + public static readonly ConstructorInfo Cons = typeof(ScriptParameters).GetConstructor(new[] { typeof(RedisKey[]), typeof(RedisValue[]) })!; public ScriptParameters(RedisKey[] keys, RedisValue[] args) { Keys = keys; @@ -24,10 +25,15 @@ public ScriptParameters(RedisKey[] keys, RedisValue[] args) } private static readonly Regex ParameterExtractor = new Regex(@"@(? ([a-z]|_) ([a-z]|_|\d)*)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - private static string[] ExtractParameters(string script) + + private static bool TryExtractParameters(string script, [NotNullWhen(true)] out string[]? parameters) { var ps = ParameterExtractor.Matches(script); - if (ps.Count == 0) return null; + if (ps.Count == 0) + { + parameters = null; + return false; + } var ret = new HashSet(); @@ -50,7 +56,8 @@ private static string[] ExtractParameters(string script) if (!ret.Contains(n)) ret.Add(n); } - return ret.ToArray(); + parameters = ret.ToArray(); + return true; } private static string MakeOrdinalScriptWithoutKeys(string rawScript, string[] args) @@ -130,10 +137,12 @@ static ScriptParameterMapper() /// The script to prepare. public static LuaScript PrepareScript(string script) { - var ps = ExtractParameters(script); - var ordinalScript = MakeOrdinalScriptWithoutKeys(script, ps); - - return new LuaScript(script, ordinalScript, ps); + if (TryExtractParameters(script, out var ps)) + { + var ordinalScript = MakeOrdinalScriptWithoutKeys(script, ps); + return new LuaScript(script, ordinalScript, ps); + } + throw new ArgumentException("Count not parse script: " + script); } private static readonly HashSet ConvertableTypes = new() @@ -161,7 +170,7 @@ public static LuaScript PrepareScript(string script) /// The script to match against. /// The first missing member, if any. /// The first type mismatched member, if any. - public static bool IsValidParameterHash(Type t, LuaScript script, out string missingMember, out string badTypeMember) + public static bool IsValidParameterHash(Type t, LuaScript script, out string? missingMember, out string? badTypeMember) { for (var i = 0; i < script.Arguments.Length; i++) { @@ -221,6 +230,10 @@ public static bool IsValidParameterHash(Type t, LuaScript script, out string mis { var argName = script.Arguments[i]; var member = t.GetMember(argName).SingleOrDefault(m => m is PropertyInfo || m is FieldInfo); + if (member is null) + { + throw new ArgumentException($"There was no member found for {argName}"); + } var memberType = member is FieldInfo memberFieldInfo ? memberFieldInfo.FieldType : ((PropertyInfo)member).PropertyType; @@ -240,8 +253,8 @@ public static bool IsValidParameterHash(Type t, LuaScript script, out string mis var keyPrefix = Expression.Parameter(typeof(RedisKey?), "keyPrefix"); Expression keysResult, valuesResult; - MethodInfo asRedisValue = null; - Expression[] keysResultArr = null; + MethodInfo? asRedisValue = null; + Expression[]? keysResultArr = null; if (keys.Count == 0) { // if there are no keys, don't allocate @@ -253,9 +266,9 @@ public static bool IsValidParameterHash(Type t, LuaScript script, out string mis var keyPrefixValueArr = new[] { Expression.Call(keyPrefix, nameof(Nullable.GetValueOrDefault), null, null) }; var prepend = typeof(RedisKey).GetMethod(nameof(RedisKey.Prepend), - BindingFlags.Public | BindingFlags.Instance); + BindingFlags.Public | BindingFlags.Instance)!; asRedisValue = typeof(RedisKey).GetMethod(nameof(RedisKey.AsRedisValue), - BindingFlags.NonPublic | BindingFlags.Instance); + BindingFlags.NonPublic | BindingFlags.Instance)!; keysResultArr = new Expression[keys.Count]; for (int i = 0; i < keysResultArr.Length; i++) @@ -281,8 +294,8 @@ public static bool IsValidParameterHash(Type t, LuaScript script, out string mis if (member.Type == typeof(RedisValue)) return member; // pass-through if (member.Type == typeof(RedisKey)) { // need to apply prefix (note we can re-use the body from earlier) - var val = keysResultArr[keys.IndexOf(arg)]; - return Expression.Call(val, asRedisValue); + var val = keysResultArr![keys.IndexOf(arg)]; + return Expression.Call(val, asRedisValue!); } // otherwise: use the conversion operator diff --git a/src/StackExchange.Redis/ServerCounters.cs b/src/StackExchange.Redis/ServerCounters.cs index f3f29cde2..f6d96c6a6 100644 --- a/src/StackExchange.Redis/ServerCounters.cs +++ b/src/StackExchange.Redis/ServerCounters.cs @@ -12,7 +12,7 @@ public class ServerCounters /// Creates a instance for an . /// /// The to create counters for. - public ServerCounters(EndPoint endpoint) + public ServerCounters(EndPoint? endpoint) { EndPoint = endpoint; Interactive = new ConnectionCounters(ConnectionType.Interactive); @@ -23,7 +23,7 @@ public ServerCounters(EndPoint endpoint) /// /// The endpoint to which this data relates (this can be null if the data represents all servers). /// - public EndPoint EndPoint { get; } + public EndPoint? EndPoint { get; } /// /// Counters associated with the interactive (non pub-sub) connection. diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 8151212c2..de9fb50c8 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -23,14 +23,14 @@ internal enum UnselectableFlags internal sealed partial class ServerEndPoint : IDisposable { - internal volatile ServerEndPoint Primary; + internal volatile ServerEndPoint? Primary; internal volatile ServerEndPoint[] Replicas = Array.Empty(); private static readonly Regex nameSanitizer = new Regex("[^!-~]", RegexOptions.Compiled); private readonly Hashtable knownScripts = new Hashtable(StringComparer.Ordinal); private int databases, writeEverySeconds; - private PhysicalBridge interactive, subscription; + private PhysicalBridge? interactive, subscription; private bool isDisposed, replicaReadOnly, isReplica, allowReplicaWrites; private bool? supportsDatabases, supportsPrimaryWrites; private ServerType serverType; @@ -72,7 +72,7 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) public EndPoint EndPoint { get; } - public ClusterConfiguration ClusterConfiguration { get; private set; } + public ClusterConfiguration? ClusterConfiguration { get; private set; } /// /// Whether this endpoint supports databases at all. @@ -103,9 +103,9 @@ public int Databases /// /// Awaitable state seeing if this endpoint is connected. /// - public Task OnConnectedAsync(LogProxy log = null, bool sendTracerIfConnected = false, bool autoConfigureIfConnected = false) + public Task OnConnectedAsync(LogProxy? log = null, bool sendTracerIfConnected = false, bool autoConfigureIfConnected = false) { - async Task IfConnectedAsync(LogProxy log, bool sendTracerIfConnected, bool autoConfigureIfConnected) + async Task IfConnectedAsync(LogProxy? log, bool sendTracerIfConnected, bool autoConfigureIfConnected) { log?.WriteLine($"{Format.ToString(this)}: OnConnectedAsync already connected start"); if (autoConfigureIfConnected) @@ -140,7 +140,7 @@ async Task IfConnectedAsync(LogProxy log, bool sendTracerIfConnected, bo return IfConnectedAsync(log, sendTracerIfConnected, autoConfigureIfConnected); } - internal Exception LastException + internal Exception? LastException { get { @@ -217,7 +217,7 @@ public void Dispose() tmp?.Dispose(); } - public PhysicalBridge GetBridge(ConnectionType type, bool create = true, LogProxy log = null) + public PhysicalBridge? GetBridge(ConnectionType type, bool create = true, LogProxy? log = null) { if (isDisposed) return null; return type switch @@ -228,7 +228,7 @@ public PhysicalBridge GetBridge(ConnectionType type, bool create = true, LogProx }; } - public PhysicalBridge GetBridge(Message message, bool create = true) + public PhysicalBridge? GetBridge(Message message) { if (isDisposed) return null; @@ -246,11 +246,11 @@ public PhysicalBridge GetBridge(Message message, bool create = true) } return message.IsForSubscriptionBridge - ? subscription ?? (create ? subscription = CreateBridge(ConnectionType.Subscription, null) : null) - : interactive ?? (create ? interactive = CreateBridge(ConnectionType.Interactive, null) : null); + ? subscription ??= CreateBridge(ConnectionType.Subscription, null) + : interactive ??= CreateBridge(ConnectionType.Interactive, null); } - public PhysicalBridge GetBridge(RedisCommand command, bool create = true) + public PhysicalBridge? GetBridge(RedisCommand command, bool create = true) { if (isDisposed) return null; switch (command) @@ -283,19 +283,19 @@ public void SetClusterConfiguration(ClusterConfiguration configuration) public void UpdateNodeRelations(ClusterConfiguration configuration) { - var thisNode = configuration.Nodes.FirstOrDefault(x => x.EndPoint.Equals(EndPoint)); + var thisNode = configuration.Nodes.FirstOrDefault(x => x.EndPoint?.Equals(EndPoint) == true); if (thisNode != null) { Multiplexer.Trace($"Updating node relations for {Format.ToString(thisNode.EndPoint)}..."); - List replicas = null; - ServerEndPoint primary = null; + List? replicas = null; + ServerEndPoint? primary = null; foreach (var node in configuration.Nodes) { if (node.NodeId == thisNode.ParentNodeId) { primary = Multiplexer.GetServerEndPoint(node.EndPoint); } - else if (node.ParentNodeId == thisNode.NodeId) + else if (node.ParentNodeId == thisNode.NodeId && node.EndPoint is not null) { (replicas ??= new List()).Add(Multiplexer.GetServerEndPoint(node.EndPoint)); } @@ -338,7 +338,7 @@ public void ClearUnselectable(UnselectableFlags flags) public ValueTask TryWriteAsync(Message message) => GetBridge(message)?.TryWriteAsync(message, isReplica) ?? new ValueTask(WriteResult.NoConnectionAvailable); - internal void Activate(ConnectionType type, LogProxy log) => GetBridge(type, true, log); + internal void Activate(ConnectionType type, LogProxy? log) => GetBridge(type, true, log); internal void AddScript(string script, byte[] hash) { @@ -348,7 +348,7 @@ internal void AddScript(string script, byte[] hash) } } - internal async Task AutoConfigureAsync(PhysicalConnection connection, LogProxy log = null) + internal async Task AutoConfigureAsync(PhysicalConnection? connection, LogProxy? log = null) { if (!serverType.SupportsAutoConfigure()) { @@ -471,8 +471,8 @@ internal void FlushScriptCache() } } - private string runId; - internal string RunId + private string? runId; + internal string? RunId { get => runId; set @@ -523,9 +523,9 @@ internal string GetProfile() return sb.ToString(); } - internal byte[] GetScriptHash(string script, RedisCommand command) + internal byte[]? GetScriptHash(string script, RedisCommand command) { - var found = (byte[])knownScripts[script]; + var found = (byte[]?)knownScripts[script]; if (found == null && command == RedisCommand.EVALSHA) { // The script provided is a hex SHA - store and re-use the ASCii for that @@ -538,7 +538,7 @@ internal byte[] GetScriptHash(string script, RedisCommand command) return found; } - internal string GetStormLog(Message message) => GetBridge(message)?.GetStormLog(); + internal string? GetStormLog(Message message) => GetBridge(message)?.GetStormLog(); internal Message GetTracerMessage(bool assertIdentity) { @@ -611,7 +611,7 @@ internal void OnDisconnected(PhysicalBridge bridge) } } - internal Task OnEstablishingAsync(PhysicalConnection connection, LogProxy log) + internal Task OnEstablishingAsync(PhysicalConnection connection, LogProxy? log) { static async Task OnEstablishingAsyncAwaited(PhysicalConnection connection, Task handshake) { @@ -676,8 +676,8 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) internal int LastInfoReplicationCheckSecondsAgo => unchecked(Environment.TickCount - Thread.VolatileRead(ref lastInfoReplicationCheckTicks)) / 1000; - private EndPoint primaryEndPoint; - public EndPoint PrimaryEndPoint + private EndPoint? primaryEndPoint; + public EndPoint? PrimaryEndPoint { get => primaryEndPoint; set => SetConfig(ref primaryEndPoint, value); @@ -686,7 +686,7 @@ public EndPoint PrimaryEndPoint /// /// Result of the latest tie breaker (from the last reconfigure). /// - internal string TieBreakerResult { get; set; } + internal string? TieBreakerResult { get; set; } internal bool CheckInfoReplication() { @@ -710,7 +710,7 @@ internal bool CheckInfoReplication() private int lastInfoReplicationCheckTicks; internal volatile int ConfigCheckSeconds; [ThreadStatic] - private static Random r; + private static Random? r; /// /// Forces frequent replication check starting from 1 second up to max ConfigCheckSeconds with an exponential increment. @@ -753,9 +753,9 @@ internal void OnHeartbeat() } } - internal Task WriteDirectAsync(Message message, ResultProcessor processor, PhysicalBridge bridge = null) + internal Task WriteDirectAsync(Message message, ResultProcessor processor, PhysicalBridge? bridge = null) { - static async Task Awaited(ServerEndPoint @this, Message message, ValueTask write, TaskCompletionSource tcs) + static async Task Awaited(ServerEndPoint @this, Message message, ValueTask write, TaskCompletionSource tcs) { var result = await write.ForAwait(); if (result != WriteResult.Success) @@ -766,7 +766,7 @@ static async Task Awaited(ServerEndPoint @this, Message message, ValueTask.Create(out var tcs, null); + var source = TaskResultBox.Create(out var tcs, null); message.SetSource(processor, source); if (bridge == null) bridge = GetBridge(message); @@ -799,7 +799,7 @@ internal void ReportNextFailure() subscription?.ReportNextFailure(); } - internal Task SendTracerAsync(LogProxy log = null) + internal Task SendTracerAsync(LogProxy? log = null) { var msg = GetTracerMessage(false); msg = LoggingMessage.Create(log, msg); @@ -842,7 +842,7 @@ internal string Summary() /// /// Write the message directly to the pipe or fail...will not queue. /// - internal ValueTask WriteDirectOrQueueFireAndForgetAsync(PhysicalConnection connection, Message message, ResultProcessor processor) + internal ValueTask WriteDirectOrQueueFireAndForgetAsync(PhysicalConnection? connection, Message message, ResultProcessor processor) { static async ValueTask Awaited(ValueTask l_result) => await l_result.ForAwait(); @@ -853,7 +853,8 @@ internal ValueTask WriteDirectOrQueueFireAndForgetAsync(PhysicalConnection co if (connection == null) { Multiplexer.Trace($"{Format.ToString(this)}: Enqueue (async): " + message); - result = GetBridge(message).TryWriteAsync(message, isReplica); + // A bridge will be created if missing, so not nullable here + result = GetBridge(message)!.TryWriteAsync(message, isReplica); } else { @@ -877,7 +878,7 @@ internal ValueTask WriteDirectOrQueueFireAndForgetAsync(PhysicalConnection co return default; } - private PhysicalBridge CreateBridge(ConnectionType type, LogProxy log) + private PhysicalBridge? CreateBridge(ConnectionType type, LogProxy? log) { if (Multiplexer.IsDisposed) return null; Multiplexer.Trace(type.ToString()); @@ -886,7 +887,7 @@ private PhysicalBridge CreateBridge(ConnectionType type, LogProxy log) return bridge; } - private async Task HandshakeAsync(PhysicalConnection connection, LogProxy log) + private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) { log?.WriteLine($"{Format.ToString(this)}: Server handshake"); if (connection == null) @@ -896,7 +897,8 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy log) } Message msg; // Note that we need "" (not null) for password in the case of 'nopass' logins - string user = Multiplexer.RawConfig.User, password = Multiplexer.RawConfig.Password ?? ""; + string? user = Multiplexer.RawConfig.User; + string password = Multiplexer.RawConfig.Password ?? ""; if (!string.IsNullOrWhiteSpace(user)) { log?.WriteLine($"{Format.ToString(this)}: Authenticating (user/password)"); @@ -961,14 +963,14 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy log) await connection.FlushAsync().ForAwait(); } - private void SetConfig(ref T field, T value, [CallerMemberName] string caller = null) + private void SetConfig(ref T field, T value, [CallerMemberName] string? caller = null) { if (!EqualityComparer.Default.Equals(field, value)) { Multiplexer.Trace(caller + " changed from " + field + " to " + value, "Configuration"); field = value; ClearMemoized(); - Multiplexer.ReconfigureIfNeeded(EndPoint, false, caller); + Multiplexer.ReconfigureIfNeeded(EndPoint, false, caller!); } } diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index 37792e47a..f654c0142 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -47,7 +47,7 @@ internal sealed class ServerSelectionStrategy private readonly ConnectionMultiplexer multiplexer; private int anyStartOffset; - private ServerEndPoint[] map; + private ServerEndPoint[]? map; public ServerSelectionStrategy(ConnectionMultiplexer multiplexer) => this.multiplexer = multiplexer; @@ -59,14 +59,14 @@ internal sealed class ServerSelectionStrategy /// /// The to determine a slot ID for. public int HashSlot(in RedisKey key) - => ServerType == ServerType.Standalone || key.IsNull ? NoSlot : GetClusterSlot((byte[])key); + => ServerType == ServerType.Standalone || key.IsNull ? NoSlot : GetClusterSlot((byte[])key!); /// /// Computes the hash-slot that would be used by the given channel. /// /// The to determine a slot ID for. public int HashSlot(in RedisChannel channel) - => ServerType == ServerType.Standalone || channel.IsNull ? NoSlot : GetClusterSlot((byte[])channel); + => ServerType == ServerType.Standalone || channel.IsNull ? NoSlot : GetClusterSlot((byte[])channel!); /// /// Gets the hashslot for a given byte sequence. @@ -100,9 +100,8 @@ private static unsafe int GetClusterSlot(byte[] blob) } } - public ServerEndPoint Select(Message message, bool allowDisconnected = false) + public ServerEndPoint? Select(Message message, bool allowDisconnected = false) { - if (message == null) throw new ArgumentNullException(nameof(message)); int slot = NoSlot; switch (ServerType) { @@ -118,13 +117,13 @@ public ServerEndPoint Select(Message message, bool allowDisconnected = false) return Select(slot, message.Command, message.Flags, allowDisconnected); } - public ServerEndPoint Select(RedisCommand command, in RedisKey key, CommandFlags flags, bool allowDisconnected = false) + public ServerEndPoint? Select(RedisCommand command, in RedisKey key, CommandFlags flags, bool allowDisconnected = false) { int slot = ServerType == ServerType.Cluster ? HashSlot(key) : NoSlot; return Select(slot, command, flags, allowDisconnected); } - public ServerEndPoint Select(RedisCommand command, in RedisChannel channel, CommandFlags flags, bool allowDisconnected = false) + public ServerEndPoint? Select(RedisCommand command, in RedisChannel channel, CommandFlags flags, bool allowDisconnected = false) { int slot = ServerType == ServerType.Cluster ? HashSlot(channel) : NoSlot; return Select(slot, command, flags, allowDisconnected); @@ -148,7 +147,7 @@ public bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isM // Note that everything so far is talking about PRIMARY nodes // We might be wanting a REPLICA, so we'll check - ServerEndPoint resendVia = null; + ServerEndPoint? resendVia = null; var command = message.Command; switch (Message.GetPrimaryReplicaFlags(message.Flags)) { @@ -241,22 +240,23 @@ private static unsafe int IndexOf(byte* ptr, byte value, int start, int end) return -1; } - private ServerEndPoint Any(RedisCommand command, CommandFlags flags, bool allowDisconnected) => + private ServerEndPoint? Any(RedisCommand command, CommandFlags flags, bool allowDisconnected) => multiplexer.AnyServer(ServerType, (uint)Interlocked.Increment(ref anyStartOffset), command, flags, allowDisconnected); - private static ServerEndPoint FindPrimary(ServerEndPoint endpoint, RedisCommand command) + private static ServerEndPoint? FindPrimary(ServerEndPoint endpoint, RedisCommand command) { + ServerEndPoint? cursor = endpoint; int max = 5; do { - if (!endpoint.IsReplica && endpoint.IsSelectable(command)) return endpoint; + if (!cursor.IsReplica && cursor.IsSelectable(command)) return cursor; - endpoint = endpoint.Primary; - } while (endpoint != null && --max != 0); + cursor = cursor.Primary; + } while (cursor != null && --max != 0); return null; } - private static ServerEndPoint FindReplica(ServerEndPoint endpoint, RedisCommand command, bool allowDisconnected = false) + private static ServerEndPoint? FindReplica(ServerEndPoint endpoint, RedisCommand command, bool allowDisconnected = false) { if (endpoint.IsReplica && endpoint.IsSelectable(command, allowDisconnected)) return endpoint; @@ -285,15 +285,16 @@ private ServerEndPoint[] MapForMutation() return arr; } - private ServerEndPoint Select(int slot, RedisCommand command, CommandFlags flags, bool allowDisconnected) + private ServerEndPoint? Select(int slot, RedisCommand command, CommandFlags flags, bool allowDisconnected) { // Only interested in primary/replica preferences flags = Message.GetPrimaryReplicaFlags(flags); - ServerEndPoint[] arr; + ServerEndPoint[]? arr; if (slot == NoSlot || (arr = map) == null) return Any(command, flags, allowDisconnected); - ServerEndPoint endpoint = arr[slot], testing; + ServerEndPoint endpoint = arr[slot]; + ServerEndPoint? testing; // but: ^^^ is the PRIMARY slots; if we want a replica, we need to do some thinking if (endpoint != null) @@ -304,13 +305,13 @@ private ServerEndPoint Select(int slot, RedisCommand command, CommandFlags flags return FindReplica(endpoint, command) ?? Any(command, flags, allowDisconnected); case CommandFlags.PreferReplica: testing = FindReplica(endpoint, command); - if (testing != null) return testing; + if (testing is not null) return testing; break; case CommandFlags.DemandMaster: return FindPrimary(endpoint, command) ?? Any(command, flags, allowDisconnected); case CommandFlags.PreferMaster: testing = FindPrimary(endpoint, command); - if (testing != null) return testing; + if (testing is not null) return testing; break; } if (endpoint.IsSelectable(command, allowDisconnected)) return endpoint; diff --git a/src/StackExchange.Redis/SocketManager.cs b/src/StackExchange.Redis/SocketManager.cs index 2478f725a..bf1918660 100644 --- a/src/StackExchange.Redis/SocketManager.cs +++ b/src/StackExchange.Redis/SocketManager.cs @@ -72,9 +72,9 @@ public enum SocketManagerOptions /// The name for this . /// the number of dedicated workers for this . /// - public SocketManager(string name = null, int workerCount = 0, SocketManagerOptions options = SocketManagerOptions.None) + public SocketManager(string? name = null, int workerCount = 0, SocketManagerOptions options = SocketManagerOptions.None) { - if (string.IsNullOrWhiteSpace(name)) name = GetType().Name; + if (name.IsNullOrWhiteSpace()) name = GetType().Name; if (workerCount <= 0) workerCount = DEFAULT_WORKERS; Name = name; bool useHighPrioritySocketThreads = (options & SocketManagerOptions.UseHighPrioritySocketThreads) != 0, @@ -170,7 +170,7 @@ public override string ToString() return $"{Name} - queue: {scheduler?.TotalServicedByQueue}, pool: {scheduler?.TotalServicedByPool}"; } - private static SocketManager s_shared, s_threadPool; + private static SocketManager? s_shared, s_threadPool; private const int DEFAULT_WORKERS = 5, MINIMUM_SEGMENT_SIZE = 8 * 1024; @@ -178,7 +178,7 @@ public override string ToString() internal PipeScheduler Scheduler { get; private set; } - internal DedicatedThreadPoolPipeScheduler SchedulerPool => Scheduler as DedicatedThreadPoolPipeScheduler; + internal DedicatedThreadPoolPipeScheduler? SchedulerPool => Scheduler as DedicatedThreadPoolPipeScheduler; private enum CallbackOperation { @@ -228,7 +228,7 @@ internal static Socket CreateSocket(EndPoint endpoint) partial void OnDispose(); - internal string GetState() + internal string? GetState() { var s = SchedulerPool; return s == null ? null : $"{s.AvailableCount} of {s.WorkerCount} available"; diff --git a/src/StackExchange.Redis/SortedSetEntry.cs b/src/StackExchange.Redis/SortedSetEntry.cs index 9214b5485..eb07f10b9 100644 --- a/src/StackExchange.Redis/SortedSetEntry.cs +++ b/src/StackExchange.Redis/SortedSetEntry.cs @@ -71,7 +71,7 @@ public SortedSetEntry(RedisValue element, double score) /// Compares two values for equality. /// /// The to compare to. - public override bool Equals(object obj) => obj is SortedSetEntry ssObj && Equals(ssObj); + public override bool Equals(object? obj) => obj is SortedSetEntry ssObj && Equals(ssObj); /// /// Compares two values for equality. @@ -89,7 +89,7 @@ public SortedSetEntry(RedisValue element, double score) /// Compares two values by score. /// /// The to compare to. - public int CompareTo(object obj) => obj is SortedSetEntry ssObj ? CompareTo(ssObj) : -1; + public int CompareTo(object? obj) => obj is SortedSetEntry ssObj ? CompareTo(ssObj) : -1; /// /// Compares two values for equality. diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index b7cde797f..41a90fb89 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -1,5 +1,6 @@  + enable net461;netstandard2.0;net472;netcoreapp3.1;net5.0 diff --git a/src/StackExchange.Redis/StreamEntry.cs b/src/StackExchange.Redis/StreamEntry.cs index fb9238da3..5752de520 100644 --- a/src/StackExchange.Redis/StreamEntry.cs +++ b/src/StackExchange.Redis/StreamEntry.cs @@ -19,7 +19,7 @@ public StreamEntry(RedisValue id, NameValueEntry[] values) /// /// A null stream entry. /// - public static StreamEntry Null { get; } = new StreamEntry(RedisValue.Null, null); + public static StreamEntry Null { get; } = new StreamEntry(RedisValue.Null, Array.Empty()); /// /// The ID assigned to the message. @@ -54,6 +54,6 @@ public RedisValue this[RedisValue fieldName] /// /// Indicates that the Redis Stream Entry is null. /// - public bool IsNull => Id == RedisValue.Null && Values == null; + public bool IsNull => Id == RedisValue.Null && Values == Array.Empty(); } } diff --git a/src/StackExchange.Redis/StreamGroupInfo.cs b/src/StackExchange.Redis/StreamGroupInfo.cs index d854a0039..052dffca1 100644 --- a/src/StackExchange.Redis/StreamGroupInfo.cs +++ b/src/StackExchange.Redis/StreamGroupInfo.cs @@ -6,7 +6,7 @@ namespace StackExchange.Redis /// public readonly struct StreamGroupInfo { - internal StreamGroupInfo(string name, int consumerCount, int pendingMessageCount, string lastDeliveredId) + internal StreamGroupInfo(string name, int consumerCount, int pendingMessageCount, string? lastDeliveredId) { Name = name; ConsumerCount = consumerCount; @@ -33,6 +33,6 @@ internal StreamGroupInfo(string name, int consumerCount, int pendingMessageCount /// /// The Id of the last message delivered to the group. /// - public string LastDeliveredId { get; } + public string? LastDeliveredId { get; } } } diff --git a/src/StackExchange.Redis/TaskExtensions.cs b/src/StackExchange.Redis/TaskExtensions.cs index 208e10e5e..397422064 100644 --- a/src/StackExchange.Redis/TaskExtensions.cs +++ b/src/StackExchange.Redis/TaskExtensions.cs @@ -15,13 +15,13 @@ private static void ObverveErrors(this Task task) public static Task ObserveErrors(this Task task) { - task?.ContinueWith(observeErrors, TaskContinuationOptions.OnlyOnFaulted); + task.ContinueWith(observeErrors, TaskContinuationOptions.OnlyOnFaulted); return task; } public static Task ObserveErrors(this Task task) { - task?.ContinueWith(observeErrors, TaskContinuationOptions.OnlyOnFaulted); + task.ContinueWith(observeErrors, TaskContinuationOptions.OnlyOnFaulted); return task; } diff --git a/src/StackExchange.Redis/TaskSource.cs b/src/StackExchange.Redis/TaskSource.cs index 5df86af89..737366c03 100644 --- a/src/StackExchange.Redis/TaskSource.cs +++ b/src/StackExchange.Redis/TaskSource.cs @@ -10,7 +10,7 @@ internal static class TaskSource /// The type for the created . /// The state for the created . /// The options to apply to the task. - public static TaskCompletionSource Create(object asyncState, TaskCreationOptions options = TaskCreationOptions.None) + public static TaskCompletionSource Create(object? asyncState, TaskCreationOptions options = TaskCreationOptions.None) => new TaskCompletionSource(asyncState, options); } } diff --git a/src/StackExchange.Redis/Utils.cs b/src/StackExchange.Redis/Utils.cs index b5ec20670..a4beb0295 100644 --- a/src/StackExchange.Redis/Utils.cs +++ b/src/StackExchange.Redis/Utils.cs @@ -5,14 +5,14 @@ namespace StackExchange.Redis; internal static class Utils { - private static string _libVersion; + private static string? _libVersion; internal static string GetLibVersion() { if (_libVersion == null) { var assembly = typeof(ConnectionMultiplexer).Assembly; - _libVersion = ((AssemblyFileVersionAttribute)Attribute.GetCustomAttribute(assembly, typeof(AssemblyFileVersionAttribute)))?.Version - ?? assembly.GetName().Version.ToString(); + _libVersion = ((AssemblyFileVersionAttribute)Attribute.GetCustomAttribute(assembly, typeof(AssemblyFileVersionAttribute))!)?.Version + ?? assembly.GetName().Version!.ToString(); } return _libVersion; } diff --git a/tests/BasicTest/Program.cs b/tests/BasicTest/Program.cs index c324fb59e..5c8ef687c 100644 --- a/tests/BasicTest/Program.cs +++ b/tests/BasicTest/Program.cs @@ -231,31 +231,31 @@ public Issue898() db = mux.GetDatabase(); } - private const int max = 100000; - [Benchmark(OperationsPerInvoke = max)] + private const int Max = 100000; + [Benchmark(OperationsPerInvoke = Max)] public void Load() { - for (int i = 0; i < max; ++i) + for (int i = 0; i < Max; ++i) { db.StringSet(i.ToString(), i); } } - [Benchmark(OperationsPerInvoke = max)] + [Benchmark(OperationsPerInvoke = Max)] public async Task LoadAsync() { - for (int i = 0; i < max; ++i) + for (int i = 0; i < Max; ++i) { await db.StringSetAsync(i.ToString(), i).ConfigureAwait(false); } } - [Benchmark(OperationsPerInvoke = max)] + [Benchmark(OperationsPerInvoke = Max)] public void Sample() { var rnd = new Random(); - for (int i = 0; i < max; ++i) + for (int i = 0; i < Max; ++i) { - var r = rnd.Next(0, max - 1); + var r = rnd.Next(0, Max - 1); var rv = db.StringGet(r.ToString()); if (rv != r) @@ -265,14 +265,14 @@ public void Sample() } } - [Benchmark(OperationsPerInvoke = max)] + [Benchmark(OperationsPerInvoke = Max)] public async Task SampleAsync() { var rnd = new Random(); - for (int i = 0; i < max; ++i) + for (int i = 0; i < Max; ++i) { - var r = rnd.Next(0, max - 1); + var r = rnd.Next(0, Max - 1); var rv = await db.StringGetAsync(r.ToString()).ConfigureAwait(false); if (rv != r) diff --git a/tests/StackExchange.Redis.Tests/BacklogTests.cs b/tests/StackExchange.Redis.Tests/BacklogTests.cs index 6d34b3209..aa4c54d9b 100644 --- a/tests/StackExchange.Redis.Tests/BacklogTests.cs +++ b/tests/StackExchange.Redis.Tests/BacklogTests.cs @@ -29,8 +29,8 @@ void PrintSnapshot(ConnectionMultiplexer muxer) Writer.WriteLine($" UnselectableFlags: {server.GetUnselectableFlags()}"); var bridge = server.GetBridge(RedisCommand.PING, create: false); Writer.WriteLine($" GetBridge: {bridge}"); - Writer.WriteLine($" IsConnected: {bridge.IsConnected}"); - Writer.WriteLine($" ConnectionState: {bridge.ConnectionState}"); + Writer.WriteLine($" IsConnected: {bridge?.IsConnected}"); + Writer.WriteLine($" ConnectionState: {bridge?.ConnectionState}"); } } diff --git a/tests/StackExchange.Redis.Tests/BasicOps.cs b/tests/StackExchange.Redis.Tests/BasicOps.cs index ed15d1f06..fb7012162 100644 --- a/tests/StackExchange.Redis.Tests/BasicOps.cs +++ b/tests/StackExchange.Redis.Tests/BasicOps.cs @@ -83,7 +83,7 @@ public void SetWithNullKey() { var db = muxer.GetDatabase(); const string? key = null, value = "abc"; - var ex = Assert.Throws(() => db.StringSet(key, value)); + var ex = Assert.Throws(() => db.StringSet(key!, value)); Assert.Equal("A null key is not valid in this context", ex.Message); } } @@ -95,14 +95,14 @@ public void SetWithNullValue() { var db = muxer.GetDatabase(); string key = Me(); - string? value = null; + const string? value = null; db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); Assert.True(db.KeyExists(key)); db.StringSet(key, value, flags: CommandFlags.FireAndForget); - var actual = (string)db.StringGet(key); + var actual = (string?)db.StringGet(key); Assert.Null(actual); Assert.False(db.KeyExists(key)); } @@ -122,7 +122,7 @@ public void SetWithDefaultValue() Assert.True(db.KeyExists(key)); db.StringSet(key, value, flags: CommandFlags.FireAndForget); - var actual = (string)db.StringGet(key); + var actual = (string?)db.StringGet(key); Assert.Null(actual); Assert.False(db.KeyExists(key)); } @@ -142,7 +142,7 @@ public void SetWithZeroValue() Assert.True(db.KeyExists(key)); db.StringSet(key, value, flags: CommandFlags.FireAndForget); - var actual = (string)db.StringGet(key); + var actual = (string?)db.StringGet(key); Assert.Equal("0", actual); Assert.True(db.KeyExists(key)); } @@ -165,7 +165,7 @@ public async Task GetSetAsync() await d0; Assert.False(await d1); - Assert.Null((string)(await g1)); + Assert.Null((string?)(await g1)); Assert.True((await g1).IsNull); await s1; Assert.Equal("123", await g2); @@ -191,7 +191,7 @@ public void GetSetSync() var d2 = conn.KeyDelete(key); Assert.False(d1); - Assert.Null((string)g1); + Assert.Null((string?)g1); Assert.True(g1.IsNull); Assert.Equal("123", g2); @@ -255,7 +255,7 @@ public async Task GetWithExpiryWrongTypeAsync() { try { - Log("Key: " + (string)key); + Log("Key: " + (string?)key); await db.StringGetWithExpiryAsync(key).ForAwait(); } catch (AggregateException e) diff --git a/tests/StackExchange.Redis.Tests/BoxUnbox.cs b/tests/StackExchange.Redis.Tests/BoxUnbox.cs index 9ae082d84..3da861477 100644 --- a/tests/StackExchange.Redis.Tests/BoxUnbox.cs +++ b/tests/StackExchange.Redis.Tests/BoxUnbox.cs @@ -28,7 +28,7 @@ public void UnboxCommonValues(object value, RedisValue expected) [MemberData(nameof(InternedValues))] public void ReturnInternedBoxesForCommonValues(RedisValue value, bool expectSameReference) { - object x = value.Box(), y = value.Box(); + object? x = value.Box(), y = value.Box(); Assert.Equal(expectSameReference, ReferenceEquals(x, y)); // check we got the right values! AssertEqualGiveOrTakeNaN(value, RedisValue.Unbox(x)); diff --git a/tests/StackExchange.Redis.Tests/Cluster.cs b/tests/StackExchange.Redis.Tests/Cluster.cs index d76570482..902d2c095 100644 --- a/tests/StackExchange.Redis.Tests/Cluster.cs +++ b/tests/StackExchange.Redis.Tests/Cluster.cs @@ -133,15 +133,16 @@ public void TestIdentity() { RedisKey key = Guid.NewGuid().ToByteArray(); var ep = conn.GetDatabase().IdentifyEndpoint(key); - Assert.Equal(ep, conn.GetServer(ep).ClusterConfiguration.GetBySlot(key).EndPoint); + Assert.NotNull(ep); + Assert.Equal(ep, conn.GetServer(ep).ClusterConfiguration?.GetBySlot(key)?.EndPoint); } } [Fact] public void IntentionalWrongServer() { - static string StringGet(IServer server, RedisKey key, CommandFlags flags = CommandFlags.None) - => (string)server.Execute("GET", new object[] { key }, flags); + static string? StringGet(IServer server, RedisKey key, CommandFlags flags = CommandFlags.None) + => (string?)server.Execute("GET", new object[] { key }, flags); using (var conn = Create()) { @@ -161,14 +162,16 @@ static string StringGet(IServer server, RedisKey key, CommandFlags flags = Comma Assert.NotNull(rightPrimaryNode); Log("Right Primary: {0} {1}", rightPrimaryNode.EndPoint, rightPrimaryNode.NodeId); - string a = StringGet(conn.GetServer(rightPrimaryNode.EndPoint), key); + Assert.NotNull(rightPrimaryNode.EndPoint); + string? a = StringGet(conn.GetServer(rightPrimaryNode.EndPoint), key); Assert.Equal(value, a); // right primary var node = config.Nodes.FirstOrDefault(x => !x.IsReplica && x.NodeId != rightPrimaryNode.NodeId); Assert.NotNull(node); Log("Using Primary: {0}", node.EndPoint, node.NodeId); { - string b = StringGet(conn.GetServer(node.EndPoint), key); + Assert.NotNull(node.EndPoint); + string? b = StringGet(conn.GetServer(node.EndPoint), key); Assert.Equal(value, b); // wrong primary, allow redirect var ex = Assert.Throws(() => StringGet(conn.GetServer(node.EndPoint), key, CommandFlags.NoRedirect)); @@ -178,14 +181,16 @@ static string StringGet(IServer server, RedisKey key, CommandFlags flags = Comma node = config.Nodes.FirstOrDefault(x => x.IsReplica && x.ParentNodeId == rightPrimaryNode.NodeId); Assert.NotNull(node); { - string d = StringGet(conn.GetServer(node.EndPoint), key); + Assert.NotNull(node.EndPoint); + string? d = StringGet(conn.GetServer(node.EndPoint), key); Assert.Equal(value, d); // right replica } node = config.Nodes.FirstOrDefault(x => x.IsReplica && x.ParentNodeId != rightPrimaryNode.NodeId); Assert.NotNull(node); { - string e = StringGet(conn.GetServer(node.EndPoint), key); + Assert.NotNull(node.EndPoint); + string? e = StringGet(conn.GetServer(node.EndPoint), key); Assert.Equal(value, e); // wrong replica, allow redirect var ex = Assert.Throws(() => StringGet(conn.GetServer(node.EndPoint), key, CommandFlags.NoRedirect)); @@ -212,6 +217,7 @@ public void TransactionWithMultiServerKeys() // invent 2 keys that we believe are served by different nodes string x = Guid.NewGuid().ToString(), y; var xNode = config.GetBySlot(x); + Assert.NotNull(xNode); int abort = 1000; do { @@ -219,6 +225,7 @@ public void TransactionWithMultiServerKeys() } while (--abort > 0 && config.GetBySlot(y) == xNode); if (abort == 0) Skip.Inconclusive("failed to find a different node to use"); var yNode = config.GetBySlot(y); + Assert.NotNull(yNode); Log("x={0}, served by {1}", x, xNode.NodeId); Log("y={0}, served by {1}", y, yNode.NodeId); Assert.NotEqual(xNode.NodeId, yNode.NodeId); @@ -275,7 +282,9 @@ public void TransactionWithSameServerKeys() } while (--abort > 0 && config.GetBySlot(y) != xNode); if (abort == 0) Skip.Inconclusive("failed to find a key with the same node to use"); var yNode = config.GetBySlot(y); + Assert.NotNull(xNode); Log("x={0}, served by {1}", x, xNode.NodeId); + Assert.NotNull(yNode); Log("y={0}, served by {1}", y, yNode.NodeId); Assert.Equal(xNode.NodeId, yNode.NodeId); @@ -326,7 +335,9 @@ public void TransactionWithSameSlotKeys() Assert.Equal(muxer.HashSlot(x), muxer.HashSlot(y)); var xNode = config.GetBySlot(x); var yNode = config.GetBySlot(y); + Assert.NotNull(xNode); Log("x={0}, served by {1}", x, xNode.NodeId); + Assert.NotNull(yNode); Log("y={0}, served by {1}", y, yNode.NodeId); Assert.Equal(xNode.NodeId, yNode.NodeId); @@ -443,6 +454,7 @@ public void GetConfig() var endpoints = muxer.GetEndPoints(); var server = muxer.GetServer(endpoints[0]); var nodes = server.ClusterNodes(); + Assert.NotNull(nodes); Log("Endpoints:"); foreach (var endpoint in endpoints) @@ -469,6 +481,7 @@ public void AccessRandomKeys() int slotMovedCount = 0; conn.HashSlotMoved += (s, a) => { + Assert.NotNull(a.OldEndPoint); Log("{0} moved from {1} to {2}", a.HashSlot, Describe(a.OldEndPoint), Describe(a.NewEndPoint)); Interlocked.Increment(ref slotMovedCount); }; @@ -545,6 +558,7 @@ public void GetFromRightNodeBasedOnFlags(CommandFlags flags, bool isReplica) { var key = Guid.NewGuid().ToString(); var endpoint = db.IdentifyEndpoint(key, flags); + Assert.NotNull(endpoint); var server = muxer.GetServer(endpoint); Assert.Equal(isReplica, server.IsReplica); } @@ -672,13 +686,15 @@ public void MovedProfiling() var rightPrimaryNode = config.GetBySlot(Key); Assert.NotNull(rightPrimaryNode); - string a = (string)conn.GetServer(rightPrimaryNode.EndPoint).Execute("GET", Key); + Assert.NotNull(rightPrimaryNode.EndPoint); + string? a = (string?)conn.GetServer(rightPrimaryNode.EndPoint).Execute("GET", Key); Assert.Equal(Value, a); // right primary var wrongPrimaryNode = config.Nodes.FirstOrDefault(x => !x.IsReplica && x.NodeId != rightPrimaryNode.NodeId); Assert.NotNull(wrongPrimaryNode); - string b = (string)conn.GetServer(wrongPrimaryNode.EndPoint).Execute("GET", Key); + Assert.NotNull(wrongPrimaryNode.EndPoint); + string? b = (string?)conn.GetServer(wrongPrimaryNode.EndPoint).Execute("GET", Key); Assert.Equal(Value, b); // wrong primary, allow redirect var msgs = profiler.GetSession().FinishProfiling().ToList(); diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index 546600cf2..222f29393 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -235,7 +235,7 @@ public void ClientName() var conn = muxer.GetDatabase(); conn.Ping(); - var name = (string)GetAnyPrimary(muxer).Execute("CLIENT", "GETNAME"); + var name = (string?)GetAnyPrimary(muxer).Execute("CLIENT", "GETNAME"); Assert.Equal("TestRig", name); } } @@ -249,7 +249,7 @@ public void DefaultClientName() var conn = muxer.GetDatabase(); conn.Ping(); - var name = (string)GetAnyPrimary(muxer).Execute("CLIENT", "GETNAME"); + var name = (string?)GetAnyPrimary(muxer).Execute("CLIENT", "GETNAME"); Assert.Equal($"{Environment.MachineName}(SE.Redis-v{Utils.GetLibVersion()})", name); } } @@ -325,7 +325,7 @@ public void DebugObject() RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); db.StringIncrement(key, flags: CommandFlags.FireAndForget); - var debug = (string)db.DebugObject(key); + var debug = (string?)db.DebugObject(key); Assert.NotNull(debug); Assert.Contains("encoding:int serializedlength:2", debug); } @@ -463,7 +463,7 @@ public void ThreadPoolManagerIsDetected() SocketManager = SocketManager.ThreadPool }; using var muxer = ConnectionMultiplexer.Connect(config); - Assert.Same(PipeScheduler.ThreadPool, muxer.SocketManager.Scheduler); + Assert.Same(PipeScheduler.ThreadPool, muxer.SocketManager?.Scheduler); } [Fact] @@ -474,7 +474,7 @@ public void DefaultThreadPoolManagerIsDetected() EndPoints = { { IPAddress.Loopback, 6379 } }, }; using var muxer = ConnectionMultiplexer.Connect(config); - Assert.Same(ConnectionMultiplexer.GetDefaultSocketManager().Scheduler, muxer.SocketManager.Scheduler); + Assert.Same(ConnectionMultiplexer.GetDefaultSocketManager().Scheduler, muxer.SocketManager?.Scheduler); } [Theory] @@ -519,7 +519,7 @@ public void NullApply() Assert.Equal("FooApply", options.ClientName); // Doesn't go boom - var result = options.Apply(null); + var result = options.Apply(null!); Assert.Equal("FooApply", options.ClientName); Assert.Equal(result, options); } @@ -555,8 +555,8 @@ public void BeforeSocketConnect() Assert.Equal(2, count); var endpoint = muxer.GetServerSnapshot()[0]; - var interactivePhysical = endpoint.GetBridge(ConnectionType.Interactive).TryConnect(null); - var subscriptionPhysical = endpoint.GetBridge(ConnectionType.Subscription).TryConnect(null); + var interactivePhysical = endpoint.GetBridge(ConnectionType.Interactive)?.TryConnect(null); + var subscriptionPhysical = endpoint.GetBridge(ConnectionType.Subscription)?.TryConnect(null); Assert.NotNull(interactivePhysical); Assert.NotNull(subscriptionPhysical); @@ -616,6 +616,7 @@ public async Task MutableOptions() const string newConfigChannel = "newConfig"; options.ConfigurationChannel = newConfigChannel; Assert.Equal(newConfigChannel, options.ConfigurationChannel); + Assert.NotNull(muxer.ConfigurationChangedChannel); Assert.Equal(Encoding.UTF8.GetString(muxer.ConfigurationChangedChannel), originalConfigChannel); Assert.Equal(originalUser, muxer.RawConfig.User); diff --git a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs b/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs index 606099c07..c67b840b5 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs @@ -189,7 +189,7 @@ void innerScenario() server.SimulateConnectionFailure(SimulatedFailureType.All); - var lastFailure = ((RedisConnectionException)muxer.GetServerSnapshot()[0].LastException).FailureType; + var lastFailure = ((RedisConnectionException?)muxer.GetServerSnapshot()[0].LastException)!.FailureType; // Depending on heartbat races, the last exception will be a socket failure or an internal (follow-up) failure Assert.Contains(lastFailure, new[] { ConnectionFailureType.SocketFailure, ConnectionFailureType.InternalFailure }); diff --git a/tests/StackExchange.Redis.Tests/EventArgsTests.cs b/tests/StackExchange.Redis.Tests/EventArgsTests.cs index bd1041e8e..227197c96 100644 --- a/tests/StackExchange.Redis.Tests/EventArgsTests.cs +++ b/tests/StackExchange.Redis.Tests/EventArgsTests.cs @@ -16,16 +16,13 @@ RedisErrorEventArgs redisErrorArgsMock = Substitute.For(default, default, default); ConnectionFailedEventArgs connectionFailedArgsMock - = Substitute.For( - default, default, default, default, default, default); + = Substitute.For(default, default, default, default, default, default); InternalErrorEventArgs internalErrorArgsMock - = Substitute.For( - default, default, default, default, default); + = Substitute.For(default, default, default, default, default); HashSlotMovedEventArgs hashSlotMovedArgsMock - = Substitute.For( - default, default, default, default); + = Substitute.For(default, default, default, default); DiagnosticStub stub = new DiagnosticStub(); @@ -53,94 +50,33 @@ HashSlotMovedEventArgs hashSlotMovedArgsMock public class DiagnosticStub { - public const string ConfigurationChangedBroadcastHandlerMessage - = "ConfigurationChangedBroadcastHandler invoked"; - - public const string ErrorMessageHandlerMessage - = "ErrorMessageHandler invoked"; - - public const string ConnectionFailedHandlerMessage - = "ConnectionFailedHandler invoked"; - - public const string InternalErrorHandlerMessage - = "InternalErrorHandler invoked"; - - public const string ConnectionRestoredHandlerMessage - = "ConnectionRestoredHandler invoked"; - - public const string ConfigurationChangedHandlerMessage - = "ConfigurationChangedHandler invoked"; - - public const string HashSlotMovedHandlerMessage - = "HashSlotMovedHandler invoked"; + public const string ConfigurationChangedBroadcastHandlerMessage = "ConfigurationChangedBroadcastHandler invoked"; + public const string ErrorMessageHandlerMessage = "ErrorMessageHandler invoked"; + public const string ConnectionFailedHandlerMessage = "ConnectionFailedHandler invoked"; + public const string InternalErrorHandlerMessage = "InternalErrorHandler invoked"; + public const string ConnectionRestoredHandlerMessage = "ConnectionRestoredHandler invoked"; + public const string ConfigurationChangedHandlerMessage = "ConfigurationChangedHandler invoked"; + public const string HashSlotMovedHandlerMessage = "HashSlotMovedHandler invoked"; public DiagnosticStub() { - ConfigurationChangedBroadcastHandler - = (obj, args) => Message = ConfigurationChangedBroadcastHandlerMessage; - - ErrorMessageHandler - = (obj, args) => Message = ErrorMessageHandlerMessage; - - ConnectionFailedHandler - = (obj, args) => Message = ConnectionFailedHandlerMessage; - - InternalErrorHandler - = (obj, args) => Message = InternalErrorHandlerMessage; - - ConnectionRestoredHandler - = (obj, args) => Message = ConnectionRestoredHandlerMessage; - - ConfigurationChangedHandler - = (obj, args) => Message = ConfigurationChangedHandlerMessage; - - HashSlotMovedHandler - = (obj, args) => Message = HashSlotMovedHandlerMessage; + ConfigurationChangedBroadcastHandler = (obj, args) => Message = ConfigurationChangedBroadcastHandlerMessage; + ErrorMessageHandler = (obj, args) => Message = ErrorMessageHandlerMessage; + ConnectionFailedHandler = (obj, args) => Message = ConnectionFailedHandlerMessage; + InternalErrorHandler = (obj, args) => Message = InternalErrorHandlerMessage; + ConnectionRestoredHandler = (obj, args) => Message = ConnectionRestoredHandlerMessage; + ConfigurationChangedHandler = (obj, args) => Message = ConfigurationChangedHandlerMessage; + HashSlotMovedHandler = (obj, args) => Message = HashSlotMovedHandlerMessage; } public string? Message { get; private set; } - - public Action ConfigurationChangedBroadcastHandler - { - get; - private set; - } - - public Action ErrorMessageHandler - { - get; - private set; - } - - public Action ConnectionFailedHandler - { - get; - private set; - } - - public Action InternalErrorHandler - { - get; - private set; - } - - public Action ConnectionRestoredHandler - { - get; - private set; - } - - public Action ConfigurationChangedHandler - { - get; - private set; - } - - public Action HashSlotMovedHandler - { - get; - private set; - } + public Action ConfigurationChangedBroadcastHandler { get; } + public Action ErrorMessageHandler { get; } + public Action ConnectionFailedHandler { get; } + public Action InternalErrorHandler { get; } + public Action ConnectionRestoredHandler { get; } + public Action ConfigurationChangedHandler { get; } + public Action HashSlotMovedHandler { get; } } } } diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 60f7728da..ff319be13 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -15,7 +15,7 @@ public void NullLastException() { muxer.GetDatabase(); Assert.Null(muxer.GetServerSnapshot()[0].LastException); - var ex = ExceptionFactory.NoConnectionAvailable(muxer as ConnectionMultiplexer, null, null); + var ex = ExceptionFactory.NoConnectionAvailable((muxer as ConnectionMultiplexer)!, null, null); Assert.Null(ex.InnerException); } } @@ -43,7 +43,7 @@ public void MultipleEndpointsThrowConnectionException() muxer.GetServer(endpoint).SimulateConnectionFailure(SimulatedFailureType.All); } - var ex = ExceptionFactory.NoConnectionAvailable(muxer as ConnectionMultiplexer, null, null); + var ex = ExceptionFactory.NoConnectionAvailable((muxer as ConnectionMultiplexer)!, null, null); var outer = Assert.IsType(ex); Assert.Equal(ConnectionFailureType.UnableToResolvePhysicalConnection, outer.FailureType); var inner = Assert.IsType(outer.InnerException); @@ -70,7 +70,7 @@ public void ServerTakesPrecendenceOverSnapshot() muxer.GetServer(muxer.GetEndPoints()[0]).SimulateConnectionFailure(SimulatedFailureType.All); - var ex = ExceptionFactory.NoConnectionAvailable(muxer as ConnectionMultiplexer, null, muxer.GetServerSnapshot()[0]); + var ex = ExceptionFactory.NoConnectionAvailable((muxer as ConnectionMultiplexer)!, null, muxer.GetServerSnapshot()[0]); Assert.IsType(ex); Assert.IsType(ex.InnerException); Assert.Equal(ex.InnerException, muxer.GetServerSnapshot()[0].LastException); @@ -91,7 +91,7 @@ public void NullInnerExceptionForMultipleEndpointsWithNoLastException() { muxer.GetDatabase(); muxer.AllowConnect = false; - var ex = ExceptionFactory.NoConnectionAvailable(muxer as ConnectionMultiplexer, null, null); + var ex = ExceptionFactory.NoConnectionAvailable((muxer as ConnectionMultiplexer)!, null, null); Assert.IsType(ex); Assert.Null(ex.InnerException); } diff --git a/tests/StackExchange.Redis.Tests/Execute.cs b/tests/StackExchange.Redis.Tests/Execute.cs index de58a671e..99cb25488 100644 --- a/tests/StackExchange.Redis.Tests/Execute.cs +++ b/tests/StackExchange.Redis.Tests/Execute.cs @@ -19,10 +19,10 @@ public async Task DBExecute() RedisKey key = Me(); db.StringSet(key, "some value"); - var actual = (string)db.Execute("GET", key); + var actual = (string?)db.Execute("GET", key); Assert.Equal("some value", actual); - actual = (string)await db.ExecuteAsync("GET", key).ForAwait(); + actual = (string?)await db.ExecuteAsync("GET", key).ForAwait(); Assert.Equal("some value", actual); } } @@ -33,10 +33,10 @@ public async Task ServerExecute() using (var conn = Create()) { var server = conn.GetServer(conn.GetEndPoints().First()); - var actual = (string)server.Execute("echo", "some value"); + var actual = (string?)server.Execute("echo", "some value"); Assert.Equal("some value", actual); - actual = (string)await server.ExecuteAsync("echo", "some value").ForAwait(); + actual = (string?)await server.ExecuteAsync("echo", "some value").ForAwait(); Assert.Equal("some value", actual); } } diff --git a/tests/StackExchange.Redis.Tests/FloatingPoint.cs b/tests/StackExchange.Redis.Tests/FloatingPoint.cs index 7f5df8ea4..9d7ad770b 100644 --- a/tests/StackExchange.Redis.Tests/FloatingPoint.cs +++ b/tests/StackExchange.Redis.Tests/FloatingPoint.cs @@ -10,10 +10,7 @@ public class FloatingPoint : TestBase { public FloatingPoint(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } - private static bool Within(double x, double y, double delta) - { - return Math.Abs(x - y) <= delta; - } + private static bool Within(double x, double y, double delta) => Math.Abs(x - y) <= delta; [Fact] public void IncrDecrFloatingPoint() diff --git a/tests/StackExchange.Redis.Tests/FormatTests.cs b/tests/StackExchange.Redis.Tests/FormatTests.cs index 02f532698..757b9d314 100644 --- a/tests/StackExchange.Redis.Tests/FormatTests.cs +++ b/tests/StackExchange.Redis.Tests/FormatTests.cs @@ -39,7 +39,7 @@ public static IEnumerable EndpointData() [MemberData(nameof(EndpointData))] public void ParseEndPoint(string data, EndPoint expected) { - var result = Format.TryParseEndPoint(data); + _ = Format.TryParseEndPoint(data, out var result); Assert.Equal(expected, result); } diff --git a/tests/StackExchange.Redis.Tests/GeoTests.cs b/tests/StackExchange.Redis.Tests/GeoTests.cs index 3a9008c07..cbb135546 100644 --- a/tests/StackExchange.Redis.Tests/GeoTests.cs +++ b/tests/StackExchange.Redis.Tests/GeoTests.cs @@ -10,12 +10,12 @@ public class GeoTests : TestBase { public GeoTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } - private readonly static GeoEntry + private static readonly GeoEntry palermo = new GeoEntry(13.361389, 38.115556, "Palermo"), catania = new GeoEntry(15.087269, 37.502669, "Catania"), agrigento = new GeoEntry(13.5765, 37.311, "Agrigento"), cefalù = new GeoEntry(14.0188, 38.0084, "Cefalù"); - private readonly static GeoEntry[] all = { palermo, catania, agrigento, cefalù }; + private static readonly GeoEntry[] all = { palermo, catania, agrigento, cefalù }; [Fact] public void GeoAdd() @@ -76,6 +76,7 @@ public void GeoHash() db.GeoAdd(key, all, CommandFlags.FireAndForget); var hashes = db.GeoHash(key, new RedisValue[] { palermo.Member, "Nowhere", agrigento.Member }); + Assert.NotNull(hashes); Assert.Equal(3, hashes.Length); Assert.Equal("sqc8b49rny0", hashes[0]); Assert.Null(hashes[1]); diff --git a/tests/StackExchange.Redis.Tests/Hashes.cs b/tests/StackExchange.Redis.Tests/Hashes.cs index 415146f65..9673a2fe0 100644 --- a/tests/StackExchange.Redis.Tests/Hashes.cs +++ b/tests/StackExchange.Redis.Tests/Hashes.cs @@ -188,7 +188,7 @@ public async Task TestGetAll() } var inRedis = (await conn.HashGetAllAsync(key).ForAwait()).ToDictionary( - x => Guid.Parse(x.Name), x => int.Parse(x.Value)); + x => Guid.Parse(x.Name!), x => int.Parse(x.Value!)); Assert.Equal(shouldMatch.Count, inRedis.Count); @@ -222,7 +222,7 @@ public async Task TestGet() foreach (var k in shouldMatch.Keys) { var inRedis = await conn.HashGetAsync(key, k.ToString()).ForAwait(); - var num = int.Parse(inRedis); + var num = int.Parse(inRedis!); Assert.Equal(shouldMatch[k], num); } @@ -253,7 +253,7 @@ public async Task TestSet() // https://redis.io/commands/hset var val5 = conn.HashGetAsync(hashkey, "empty_type2").ForAwait(); await del; - Assert.Null((string)(await val0)); + Assert.Null((string?)(await val0)); Assert.True(await set0); Assert.Equal("value1", await val1); Assert.False(await set1); @@ -289,7 +289,7 @@ public async Task TestSetNotExists() // https://redis.io/commands/hsetnx var set3 = conn.HashSetAsync(hashkey, "field-blob", Encoding.UTF8.GetBytes("value3"), When.NotExists).ForAwait(); await del; - Assert.Null((string)(await val0)); + Assert.Null((string?)(await val0)); Assert.True(await set0); Assert.Equal("value1", await val1); Assert.False(await set1); @@ -461,8 +461,8 @@ public async Task TestHashValues() // https://redis.io/commands/hvals var arr = await keys1; Assert.Equal(2, arr.Length); - Assert.Equal("abc", Encoding.UTF8.GetString(arr[0])); - Assert.Equal("def", Encoding.UTF8.GetString(arr[1])); + Assert.Equal("abc", Encoding.UTF8.GetString(arr[0]!)); + Assert.Equal("def", Encoding.UTF8.GetString(arr[1]!)); } } @@ -506,19 +506,19 @@ public async Task TestGetMulti() // https://redis.io/commands/hmget var arr2 = await conn.HashGetAsync(hashkey, fields).ForAwait(); Assert.Equal(3, arr0.Length); - Assert.Null((string)arr0[0]); - Assert.Null((string)arr0[1]); - Assert.Null((string)arr0[2]); + Assert.Null((string?)arr0[0]); + Assert.Null((string?)arr0[1]); + Assert.Null((string?)arr0[2]); Assert.Equal(3, arr1.Length); Assert.Equal("abc", arr1[0]); Assert.Equal("def", arr1[1]); - Assert.Null((string)arr1[2]); + Assert.Null((string?)arr1[2]); Assert.Equal(3, arr2.Length); Assert.Equal("abc", arr2[0]); Assert.Equal("def", arr2[1]); - Assert.Null((string)arr2[2]); + Assert.Null((string?)arr2[2]); } } diff --git a/tests/StackExchange.Redis.Tests/Helpers/Skip.cs b/tests/StackExchange.Redis.Tests/Helpers/Skip.cs index bf51afe3a..2a39c985f 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Skip.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Skip.cs @@ -8,23 +8,15 @@ public static class Skip { public static void Inconclusive(string message) => throw new SkipTestException(message); - public static void IfNoConfig(string prop, -#if NETCOREAPP - [NotNull] -#endif - string? value) + public static void IfNoConfig(string prop, [NotNull] string? value) { - if (string.IsNullOrEmpty(value)) + if (value.IsNullOrEmpty()) { throw new SkipTestException($"Config.{prop} is not set, skipping test."); } } - public static void IfNoConfig(string prop, -#if NETCOREAPP - [NotNull] -#endif - List? values) + public static void IfNoConfig(string prop, [NotNull] List? values) { if (values == null || values.Count == 0) { diff --git a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs index 02b6b6f01..0a0f93411 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs @@ -545,13 +545,7 @@ public int TimeToLive(string key) return SendExpectInt("TTL {0}\r\n", key); } - public int DbSize - { - get - { - return SendExpectInt("DBSIZE\r\n"); - } - } + public int DbSize => SendExpectInt("DBSIZE\r\n"); public string Save() { diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs b/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs index d92be44f3..bbfd2c126 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs @@ -36,7 +36,7 @@ public async Task ExecuteWithUnsubscribeViaChannel() var first = await pubsub.SubscribeAsync(name); var second = await pubsub.SubscribeAsync(name); AssertCounts(pubsub, name, true, 0, 2); - List values = new List(); + var values = new List(); int i = 0; first.OnMessage(x => { @@ -102,7 +102,7 @@ public async Task ExecuteWithUnsubscribeViaSubscriber() var first = await pubsub.SubscribeAsync(name); var second = await pubsub.SubscribeAsync(name); AssertCounts(pubsub, name, true, 0, 2); - List values = new List(); + var values = new List(); int i = 0; first.OnMessage(x => { @@ -154,7 +154,7 @@ public async Task ExecuteWithUnsubscribeViaClearAll() var first = await pubsub.SubscribeAsync(name); var second = await pubsub.SubscribeAsync(name); AssertCounts(pubsub, name, true, 0, 2); - List values = new List(); + var values = new List(); int i = 0; first.OnMessage(x => { diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue182.cs b/tests/StackExchange.Redis.Tests/Issues/Issue182.cs index 1b75c1240..04dfc8950 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue182.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue182.cs @@ -20,8 +20,8 @@ public async Task SetMembers() conn.ConnectionFailed += (s, a) => { Log(a.FailureType.ToString()); - Log(a.Exception.Message); - Log(a.Exception.StackTrace); + Log(a.Exception?.Message); + Log(a.Exception?.StackTrace); }; var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs b/tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs index 0067c1522..5b366d07d 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs @@ -25,7 +25,7 @@ private void Prep(int db, string key) conn.StringSetAsync(iKey, iKey); last = conn.SetAddAsync(key, iKey); } - conn.Wait(last); + conn.Wait(last!); } } @@ -51,13 +51,13 @@ public async Task ExecuteMassiveDelete() throttle.Release(); if (task.IsCompleted) { - if ((string)task.Result == null) + if ((string?)task.Result == null) { Volatile.Write(ref keepChecking, 0); } else { - last = conn.KeyDeleteAsync((string)task.Result); + last = conn.KeyDeleteAsync((string?)task.Result); } } }); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO10504853.cs b/tests/StackExchange.Redis.Tests/Issues/SO10504853.cs index d7b7bb7df..bd20cf6d2 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO10504853.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO10504853.cs @@ -53,7 +53,7 @@ public void ExecuteWithEmptyStartingPoint() conn.Wait(taskResult); - var priority = int.Parse(taskResult.Result); + var priority = int.Parse(taskResult.Result!); Assert.Equal(3, priority); } diff --git a/tests/StackExchange.Redis.Tests/Issues/SO11766033.cs b/tests/StackExchange.Redis.Tests/Issues/SO11766033.cs index de3a9c3e9..cf88d9cdf 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO11766033.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO11766033.cs @@ -17,7 +17,7 @@ public void TestNullString() var uid = Me(); redis.StringSetAsync(uid, "abc"); redis.StringSetAsync(uid, expectedTestValue); - string testValue = redis.StringGet(uid); + string? testValue = redis.StringGet(uid); Assert.Null(testValue); } } @@ -32,7 +32,7 @@ public void TestEmptyString() var uid = Me(); redis.StringSetAsync(uid, expectedTestValue); - string testValue = redis.StringGet(uid); + string? testValue = redis.StringGet(uid); Assert.Equal(expectedTestValue, testValue); } diff --git a/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs b/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs index 130b340cd..b8d1793af 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs @@ -42,7 +42,7 @@ public async Task Exec() Assert.Null(ttl); var r = await fullWait; Assert.True(r.IsNull); - Assert.Null((string)r); + Assert.Null((string?)r); } } } diff --git a/tests/StackExchange.Redis.Tests/Keys.cs b/tests/StackExchange.Redis.Tests/Keys.cs index 0edaa1939..15663b692 100644 --- a/tests/StackExchange.Redis.Tests/Keys.cs +++ b/tests/StackExchange.Redis.Tests/Keys.cs @@ -42,12 +42,13 @@ public void FlushFetchRandomKey() var db = conn.GetDatabase(dbId); var prefix = Me(); conn.GetServer(TestConfig.Current.PrimaryServerAndPort).FlushDatabase(dbId, CommandFlags.FireAndForget); - string anyKey = db.KeyRandom(); + string? anyKey = db.KeyRandom(); Assert.Null(anyKey); db.StringSet(prefix + "abc", "def"); - byte[] keyBytes = db.KeyRandom(); + byte[]? keyBytes = db.KeyRandom(); + Assert.NotNull(keyBytes); Assert.Equal(prefix + "abc", Encoding.UTF8.GetString(keyBytes)); } } diff --git a/tests/StackExchange.Redis.Tests/KeysAndValues.cs b/tests/StackExchange.Redis.Tests/KeysAndValues.cs index b2c8011dc..b57cbe690 100644 --- a/tests/StackExchange.Redis.Tests/KeysAndValues.cs +++ b/tests/StackExchange.Redis.Tests/KeysAndValues.cs @@ -95,12 +95,12 @@ private static void CheckNotSame(RedisValue x, RedisValue y) private static void CheckNotNull(RedisValue value) { Assert.False(value.IsNull); - Assert.NotNull((byte[])value); - Assert.NotNull((string)value); + Assert.NotNull((byte[]?)value); + Assert.NotNull((string?)value); Assert.NotEqual(-1, value.GetHashCode()); - Assert.NotNull((string)value); - Assert.NotNull((byte[])value); + Assert.NotNull((string?)value); + Assert.NotNull((byte[]?)value); CheckSame(value, value); CheckNotSame(value, default(RedisValue)); @@ -115,8 +115,8 @@ internal static void CheckNull(RedisValue value) Assert.False(value.IsInteger); Assert.Equal(-1, value.GetHashCode()); - Assert.Null((string)value); - Assert.Null((byte[])value); + Assert.Null((string?)value); + Assert.Null((byte[]?)value); Assert.Equal(0, (int)value); Assert.Equal(0L, (long)value); diff --git a/tests/StackExchange.Redis.Tests/Latency.cs b/tests/StackExchange.Redis.Tests/Latency.cs index d3eafcf5c..31c1f3c1d 100644 --- a/tests/StackExchange.Redis.Tests/Latency.cs +++ b/tests/StackExchange.Redis.Tests/Latency.cs @@ -15,7 +15,7 @@ public async Task CanCallDoctor() using (var conn = Create()) { var server = conn.GetServer(conn.GetEndPoints()[0]); - string doctor = server.LatencyDoctor(); + string? doctor = server.LatencyDoctor(); Assert.NotNull(doctor); Assert.NotEqual("", doctor); diff --git a/tests/StackExchange.Redis.Tests/Locking.cs b/tests/StackExchange.Redis.Tests/Locking.cs index 2afcd0461..0c782f20d 100644 --- a/tests/StackExchange.Redis.Tests/Locking.cs +++ b/tests/StackExchange.Redis.Tests/Locking.cs @@ -159,7 +159,7 @@ public async Task TakeLockAndExtend(TestMode mode) ttl = (await t10).Value.TotalSeconds; Assert.True(ttl > 50 && ttl <= 60, "10"); Assert.True(await t11, "11"); - Assert.Null((string)await t12); + Assert.Null((string?)await t12); Assert.True(await t13, "13"); } } diff --git a/tests/StackExchange.Redis.Tests/Memory.cs b/tests/StackExchange.Redis.Tests/Memory.cs index ba0d37674..113576ee6 100644 --- a/tests/StackExchange.Redis.Tests/Memory.cs +++ b/tests/StackExchange.Redis.Tests/Memory.cs @@ -16,7 +16,7 @@ public async Task CanCallDoctor() { Skip.IfMissingFeature(conn, nameof(RedisFeatures.Memory), r => r.Streams); var server = conn.GetServer(conn.GetEndPoints()[0]); - string doctor = server.MemoryDoctor(); + string? doctor = server.MemoryDoctor(); Assert.NotNull(doctor); Assert.NotEqual("", doctor); @@ -64,6 +64,7 @@ public async Task GetStats() Skip.IfMissingFeature(conn, nameof(RedisFeatures.Memory), r => r.Streams); var server = conn.GetServer(conn.GetEndPoints()[0]); var stats = server.MemoryStats(); + Assert.NotNull(stats); Assert.Equal(ResultType.MultiBulk, stats.Type); var parsed = stats.ToDictionary(); @@ -73,6 +74,7 @@ public async Task GetStats() Assert.True(alloc.AsInt64() > 0); stats = await server.MemoryStatsAsync(); + Assert.NotNull(stats); Assert.Equal(ResultType.MultiBulk, stats.Type); alloc = parsed["total.allocated"]; diff --git a/tests/StackExchange.Redis.Tests/Migrate.cs b/tests/StackExchange.Redis.Tests/Migrate.cs index 9a89e006e..bf9a454bf 100644 --- a/tests/StackExchange.Redis.Tests/Migrate.cs +++ b/tests/StackExchange.Redis.Tests/Migrate.cs @@ -40,7 +40,7 @@ public async Task Basic() Assert.False(fromDb.KeyExists(key), "Exists at source"); Assert.True(toDb.KeyExists(key), "Exists at destination"); - string s = toDb.StringGet(key); + string? s = toDb.StringGet(key); Assert.Equal("foo", s); } } diff --git a/tests/StackExchange.Redis.Tests/Profiling.cs b/tests/StackExchange.Redis.Tests/Profiling.cs index 613972187..159d737dc 100644 --- a/tests/StackExchange.Redis.Tests/Profiling.cs +++ b/tests/StackExchange.Redis.Tests/Profiling.cs @@ -33,12 +33,14 @@ public void Simple() var db = conn.GetDatabase(dbId); db.StringSet(key, "world"); var result = db.ScriptEvaluate(script, new { key = (RedisKey)key }); + Assert.NotNull(result); Assert.Equal("world", result.AsString()); var loadedResult = db.ScriptEvaluate(loaded, new { key = (RedisKey)key }); + Assert.NotNull(loadedResult); Assert.Equal("world", loadedResult.AsString()); var val = db.StringGet(key); Assert.Equal("world", val); - var s = (string)db.Execute("ECHO", "fii"); + var s = (string?)db.Execute("ECHO", "fii"); Assert.Equal("fii", s); var cmds = session.FinishProfiling(); @@ -242,14 +244,14 @@ public void LowAllocationEnumerable() var prefix = Me(); var db = conn.GetDatabase(1); - var allTasks = new List>(); + var allTasks = new List>(); foreach (var i in Enumerable.Range(0, OuterLoop)) { var t = db.StringSetAsync(prefix + i, "bar" + i) .ContinueWith( - async _ => (string)(await db.StringGetAsync(prefix + i).ForAwait()) + async _ => (string?)(await db.StringGetAsync(prefix + i).ForAwait()) ); var finalResult = t.Unwrap(); diff --git a/tests/StackExchange.Redis.Tests/PubSub.cs b/tests/StackExchange.Redis.Tests/PubSub.cs index 502e3ffe4..d20b3a81a 100644 --- a/tests/StackExchange.Redis.Tests/PubSub.cs +++ b/tests/StackExchange.Redis.Tests/PubSub.cs @@ -59,7 +59,7 @@ public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string br var pub = GetAnyPrimary(muxer); var sub = muxer.GetSubscriber(); await PingAsync(pub, sub).ForAwait(); - HashSet received = new(); + HashSet received = new(); int secondHandler = 0; string subChannel = (wildCard ? "a*c" : "abc") + breaker; string pubChannel = "abc" + breaker; @@ -140,7 +140,7 @@ public async Task TestBasicPubSubFireAndForget() var sub = muxer.GetSubscriber(); RedisChannel key = Me() + Guid.NewGuid(); - HashSet received = new(); + HashSet received = new(); int secondHandler = 0; await PingAsync(pub, sub).ForAwait(); sub.Subscribe(key, (channel, payload) => @@ -211,7 +211,7 @@ public async Task TestPatternPubSub() var pub = GetAnyPrimary(muxer); var sub = muxer.GetSubscriber(); - HashSet received = new(); + HashSet received = new(); int secondHandler = 0; sub.Subscribe("a*c", (channel, payload) => { @@ -332,7 +332,7 @@ await sub.SubscribeAsync(channel, (_, val) => bool pulse; lock (data) { - data.Add(int.Parse(Encoding.UTF8.GetString(val))); + data.Add(int.Parse(Encoding.UTF8.GetString(val!))); pulse = data.Count == count; if ((data.Count % 100) == 99) Log(data.Count.ToString()); } @@ -384,7 +384,7 @@ async Task RunLoop() while (!subChannel.Completion.IsCompleted) { var work = await subChannel.ReadAsync().ForAwait(); - int i = int.Parse(Encoding.UTF8.GetString(work.Message)); + int i = int.Parse(Encoding.UTF8.GetString(work.Message!)); lock (data) { data.Add(i); @@ -451,7 +451,7 @@ public async Task PubSubGetAllCorrectOrder_OnMessage_Sync() var subChannel = await sub.SubscribeAsync(channel).ForAwait(); subChannel.OnMessage(msg => { - int i = int.Parse(Encoding.UTF8.GetString(msg.Message)); + int i = int.Parse(Encoding.UTF8.GetString(msg.Message!)); bool pulse = false; lock (data) { @@ -520,7 +520,7 @@ public async Task PubSubGetAllCorrectOrder_OnMessage_Async() var subChannel = await sub.SubscribeAsync(channel).ForAwait(); subChannel.OnMessage(msg => { - int i = int.Parse(Encoding.UTF8.GetString(msg.Message)); + int i = int.Parse(Encoding.UTF8.GetString(msg.Message!)); bool pulse = false; lock (data) { @@ -535,7 +535,8 @@ public async Task PubSubGetAllCorrectOrder_OnMessage_Async() Monitor.PulseAll(syncLock); } } - return i % 2 == 0 ? null : Task.CompletedTask; + // Making sure we cope with null being returned here by a handler + return i % 2 == 0 ? null! : Task.CompletedTask; }); await sub.PingAsync().ForAwait(); diff --git a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs index 4e55b3dbd..32812022e 100644 --- a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs +++ b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs @@ -57,6 +57,7 @@ await sub.SubscribeAsync(channel, (_, val) => var subscribedServerEndpoint = muxer.GetServerEndPoint(endpoint); Assert.True(subscribedServer.IsConnected, "subscribedServer.IsConnected"); + Assert.NotNull(subscribedServerEndpoint); Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); Assert.True(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); @@ -127,6 +128,7 @@ await sub.SubscribeAsync(channel, (_, val) => var subscribedServerEndpoint = muxer.GetServerEndPoint(endpoint); Assert.True(subscribedServer.IsConnected, "subscribedServer.IsConnected"); + Assert.NotNull(subscribedServerEndpoint); Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); Assert.True(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); diff --git a/tests/StackExchange.Redis.Tests/RawResultTests.cs b/tests/StackExchange.Redis.Tests/RawResultTests.cs index accc2fe34..69d85ff61 100644 --- a/tests/StackExchange.Redis.Tests/RawResultTests.cs +++ b/tests/StackExchange.Redis.Tests/RawResultTests.cs @@ -21,10 +21,10 @@ public void NullWorks() var value = result.AsRedisValue(); Assert.True(value.IsNull); - string s = value; + string? s = value; Assert.Null(s); - byte[] arr = (byte[])value; + byte[]? arr = (byte[]?)value; Assert.Null(arr); } @@ -38,10 +38,10 @@ public void DefaultWorks() var value = result.AsRedisValue(); Assert.True(value.IsNull); - var s = (string)value; + var s = (string?)value; Assert.Null(s); - var arr = (byte[])value; + var arr = (byte[]?)value; Assert.Null(arr); } @@ -55,10 +55,10 @@ public void NilWorks() var value = result.AsRedisValue(); Assert.True(value.IsNull); - var s = (string)value; + var s = (string?)value; Assert.Null(s); - var arr = (byte[])value; + var arr = (byte[]?)value; Assert.Null(arr); } } diff --git a/tests/StackExchange.Redis.Tests/SSL.cs b/tests/StackExchange.Redis.Tests/SSL.cs index 52b982d5c..c2fe69b14 100644 --- a/tests/StackExchange.Redis.Tests/SSL.cs +++ b/tests/StackExchange.Redis.Tests/SSL.cs @@ -216,7 +216,7 @@ public void RedisLabsSSL() { var db = conn.GetDatabase(); db.KeyDelete(key, CommandFlags.FireAndForget); - string s = db.StringGet(key); + string? s = db.StringGet(key); Assert.Null(s); db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); s = db.StringGet(key); @@ -274,7 +274,7 @@ public void RedisLabsEnvironmentVariableClientCertificate(bool setEnv) var db = conn.GetDatabase(); db.KeyDelete(key, CommandFlags.FireAndForget); - string s = db.StringGet(key); + string? s = db.StringGet(key); Assert.Null(s); db.StringSet(key, "abc"); s = db.StringGet(key); diff --git a/tests/StackExchange.Redis.Tests/Scans.cs b/tests/StackExchange.Redis.Tests/Scans.cs index 18a1f1308..da6a92020 100644 --- a/tests/StackExchange.Redis.Tests/Scans.cs +++ b/tests/StackExchange.Redis.Tests/Scans.cs @@ -117,7 +117,7 @@ public void ScanResume() db.StringSet(prefix + ":" + i, Guid.NewGuid().ToString()); } - var expected = new HashSet(); + var expected = new HashSet(); long snapCursor = 0; int snapOffset = 0, snapPageSize = 0; diff --git a/tests/StackExchange.Redis.Tests/Scripting.cs b/tests/StackExchange.Redis.Tests/Scripting.cs index 56caaef6f..b157eb8a2 100644 --- a/tests/StackExchange.Redis.Tests/Scripting.cs +++ b/tests/StackExchange.Redis.Tests/Scripting.cs @@ -43,14 +43,16 @@ public async Task BasicScripting() new RedisKey[] { "key1", "key2" }, new RedisValue[] { "first", "second" }); var cache = conn.ScriptEvaluateAsync("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", new RedisKey[] { "key1", "key2" }, new RedisValue[] { "first", "second" }); - var results = (string[])await noCache; + var results = (string[]?)await noCache; + Assert.NotNull(results); Assert.Equal(4, results.Length); Assert.Equal("key1", results[0]); Assert.Equal("key2", results[1]); Assert.Equal("first", results[2]); Assert.Equal("second", results[3]); - results = (string[])await cache; + results = (string[]?)await cache; + Assert.NotNull(results); Assert.Equal(4, results.Length); Assert.Equal("key1", results[0]); Assert.Equal("key2", results[1]); @@ -67,7 +69,7 @@ public void KeysScripting() var conn = muxer.GetDatabase(); var key = Me(); conn.StringSet(key, "bar", flags: CommandFlags.FireAndForget); - var result = (string)conn.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); + var result = (string?)conn.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); Assert.Equal("bar", result); } } @@ -149,7 +151,9 @@ public async Task MultiIncrWithoutReplies() var b = conn.StringGetAsync(prefix + "b").ForAwait(); var c = conn.StringGetAsync(prefix + "c").ForAwait(); - Assert.True((await result).IsNull, "result"); + var r = await result; + Assert.NotNull(r); + Assert.True(r.IsNull, "result"); Assert.Equal(1, (long)await a); Assert.Equal(2, (long)await b); Assert.Equal(4, (long)await c); @@ -196,7 +200,8 @@ public void DisableStringInference() var conn = muxer.GetDatabase(); var key = Me(); conn.StringSet(key, "bar", flags: CommandFlags.FireAndForget); - var result = (byte[])conn.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }); + var result = (byte[]?)conn.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }); + Assert.NotNull(result); Assert.Equal("bar", Encoding.UTF8.GetString(result)); } } @@ -209,7 +214,7 @@ public void FlushDetection() var conn = muxer.GetDatabase(); var key = Me(); conn.StringSet(key, "bar", flags: CommandFlags.FireAndForget); - var result = (string)conn.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); + var result = (string?)conn.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); Assert.Equal("bar", result); // now cause all kinds of problems @@ -218,7 +223,7 @@ public void FlushDetection() //expect this one to fail just work fine (self-fix) conn.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); - result = (string)conn.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); + result = (string?)conn.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); Assert.Equal("bar", result); } } @@ -267,7 +272,7 @@ public void NonAsciiScripts() var conn = muxer.GetDatabase(); GetServer(muxer).ScriptLoad(evil); - var result = (string)conn.ScriptEvaluate(evil, null, null); + var result = (string?)conn.ScriptEvaluate(evil, null, null); Assert.Equal("僕", result); } } @@ -300,7 +305,7 @@ public void ScriptThrowsErrorInsideTransaction() var key = Me(); var conn = muxer.GetDatabase(); conn.KeyDelete(key, CommandFlags.FireAndForget); - var beforeTran = (string)conn.StringGet(key); + var beforeTran = (string?)conn.StringGet(key); Assert.Null(beforeTran); var tran = conn.CreateTransaction(); { @@ -350,7 +355,7 @@ public async Task ChangeDbInScript() return redis.call('get','" + key + "')", null, null); var getResult = conn.StringGetAsync(key); - Assert.Equal("db 1", (string)await evalResult); + Assert.Equal("db 1", (string?)await evalResult); // now, our connection thought it was in db 2, but the script changed to db 1 Assert.Equal("db 2", await getResult); } @@ -372,7 +377,7 @@ public async Task ChangeDbInTranScript() var getResult = tran.StringGetAsync(key); Assert.True(tran.Execute()); - Assert.Equal("db 1", (string)await evalResult); + Assert.Equal("db 1", (string?)await evalResult); // now, our connection thought it was in db 2, but the script changed to db 1 Assert.Equal("db 2", await getResult); } @@ -515,7 +520,8 @@ public void TestCallByHash() var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); - byte[] hash = server.ScriptLoad(Script); + byte[]? hash = server.ScriptLoad(Script); + Assert.NotNull(hash); var db = conn.GetDatabase(); var key = Me(); @@ -550,7 +556,7 @@ public void SimpleLuaScript() { var val = prepared.Evaluate(db, new { ident = "hello" }); - Assert.Equal("hello", (string)val); + Assert.Equal("hello", (string?)val); } { @@ -575,12 +581,16 @@ public void SimpleLuaScript() { var val = prepared.Evaluate(db, new { ident = new byte[] { 4, 5, 6 } }); - Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual((byte[])val)); + var valArray = (byte[]?)val; + Assert.NotNull(valArray); + Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); } { var val = prepared.Evaluate(db, new { ident = new ReadOnlyMemory(new byte[] { 4, 5, 6 }) }); - Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual((byte[])val)); + var valArray = (byte[]?)val; + Assert.NotNull(valArray); + Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); } } } @@ -600,7 +610,7 @@ public void SimpleRawScriptEvaluate() { var val = db.ScriptEvaluate(Script, values: new RedisValue[] { "hello" }); - Assert.Equal("hello", (string)val); + Assert.Equal("hello", (string?)val); } { @@ -625,12 +635,16 @@ public void SimpleRawScriptEvaluate() { var val = db.ScriptEvaluate(Script, values: new RedisValue[] { new byte[] { 4, 5, 6 } }); - Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual((byte[])val)); + var valArray = (byte[]?)val; + Assert.NotNull(valArray); + Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); } { var val = db.ScriptEvaluate(Script, values: new RedisValue[] { new ReadOnlyMemory(new byte[] { 4, 5, 6 }) }); - Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual((byte[])val)); + var valArray = (byte[]?)val; + Assert.NotNull(valArray); + Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); } } } @@ -659,7 +673,7 @@ public void LuaScriptWithKeys() Assert.Equal(123, (int)val); // no super clean way to extract this; so just abuse InternalsVisibleTo - script.ExtractParameters(p, null, out RedisKey[] keys, out _); + script.ExtractParameters(p, null, out RedisKey[]? keys, out _); Assert.NotNull(keys); Assert.Single(keys); Assert.Equal(key, keys[0]); @@ -719,7 +733,7 @@ public void SimpleLoadedLuaScript() { var val = loaded.Evaluate(db, new { ident = "hello" }); - Assert.Equal("hello", (string)val); + Assert.Equal("hello", (string?)val); } { @@ -744,12 +758,16 @@ public void SimpleLoadedLuaScript() { var val = loaded.Evaluate(db, new { ident = new byte[] { 4, 5, 6 } }); - Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual((byte[])val)); + var valArray = (byte[]?)val; + Assert.NotNull(valArray); + Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); } { var val = loaded.Evaluate(db, new { ident = new ReadOnlyMemory(new byte[] { 4, 5, 6 }) }); - Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual((byte[])val)); + var valArray = (byte[]?)val; + Assert.NotNull(valArray); + Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); } } } @@ -779,7 +797,7 @@ public void LoadedLuaScriptWithKeys() Assert.Equal(123, (int)val); // no super clean way to extract this; so just abuse InternalsVisibleTo - prepared.Original.ExtractParameters(p, null, out RedisKey[] keys, out _); + prepared.Original.ExtractParameters(p, null, out RedisKey[]? keys, out _); Assert.NotNull(keys); Assert.Single(keys); Assert.Equal(key, keys[0]); @@ -882,10 +900,11 @@ public void LuaScriptPrefixedKeys() var p = new { key = (RedisKey)key, value = "hello" }; // no super clean way to extract this; so just abuse InternalsVisibleTo - prepared.ExtractParameters(p, "prefix-", out RedisKey[] keys, out RedisValue[] args); + prepared.ExtractParameters(p, "prefix-", out RedisKey[]? keys, out RedisValue[]? args); Assert.NotNull(keys); Assert.Single(keys); Assert.Equal("prefix-" + key, keys[0]); + Assert.NotNull(args); Assert.Equal(2, args.Length); Assert.Equal("prefix-" + key, args[0]); Assert.Equal("hello", args[1]); @@ -1012,7 +1031,8 @@ public void ScriptWithKeyPrefixViaTokens() arr[3] = @z; return arr; "); - var result = (RedisValue[])p.ScriptEvaluate(script, args); + var result = (RedisValue[]?)p.ScriptEvaluate(script, args); + Assert.NotNull(result); Assert.Equal("abc", result[0]); Assert.Equal("prefix/def", result[1]); Assert.Equal("123", result[2]); @@ -1033,7 +1053,8 @@ public void ScriptWithKeyPrefixViaArrays() arr[3] = ARGV[2]; return arr; "; - var result = (RedisValue[])p.ScriptEvaluate(script, new RedisKey[] { "def" }, new RedisValue[] { "abc", 123 }); + var result = (RedisValue[]?)p.ScriptEvaluate(script, new RedisKey[] { "def" }, new RedisValue[] { "abc", 123 }); + Assert.NotNull(result); Assert.Equal("abc", result[0]); Assert.Equal("prefix/def", result[1]); Assert.Equal("123", result[2]); @@ -1048,9 +1069,11 @@ public void ScriptWithKeyPrefixCompare() var p = conn.GetDatabase().WithKeyPrefix("prefix/"); var args = new { k = (RedisKey)"key", s = "str", v = 123 }; LuaScript lua = LuaScript.Prepare("return {@k, @s, @v}"); - var viaArgs = (RedisValue[])p.ScriptEvaluate(lua, args); + var viaArgs = (RedisValue[]?)p.ScriptEvaluate(lua, args); - var viaArr = (RedisValue[])p.ScriptEvaluate("return {KEYS[1], ARGV[1], ARGV[2]}", new[] { args.k }, new RedisValue[] { args.s, args.v }); + var viaArr = (RedisValue[]?)p.ScriptEvaluate("return {KEYS[1], ARGV[1], ARGV[2]}", new[] { args.k }, new RedisValue[] { args.s, args.v }); + Assert.NotNull(viaArr); + Assert.NotNull(viaArgs); Assert.Equal(string.Join(",", viaArr), string.Join(",", viaArgs)); } } @@ -1064,16 +1087,16 @@ private static void TestNullArray(RedisResult? value) { Assert.True(value == null || value.IsNull); - Assert.Null((RedisValue[])value); - Assert.Null((RedisKey[])value); - Assert.Null((bool[])value); - Assert.Null((long[])value); - Assert.Null((ulong[])value); - Assert.Null((string[])value); - Assert.Null((int[])value); - Assert.Null((double[])value); - Assert.Null((byte[][])value); - Assert.Null((RedisResult[])value); + Assert.Null((RedisValue[]?)value); + Assert.Null((RedisKey[]?)value); + Assert.Null((bool[]?)value); + Assert.Null((long[]?)value); + Assert.Null((ulong[]?)value); + Assert.Null((string[]?)value); + Assert.Null((int[]?)value); + Assert.Null((double[]?)value); + Assert.Null((byte[][]?)value); + Assert.Null((RedisResult[]?)value); } [Fact] @@ -1090,9 +1113,9 @@ private static void TestNullValue(RedisResult? value) Assert.Null((bool?)value); Assert.Null((long?)value); Assert.Null((ulong?)value); - Assert.Null((string)value); + Assert.Null((string?)value); Assert.Null((double?)value); - Assert.Null((byte[])value); + Assert.Null((byte[]?)value); } } } diff --git a/tests/StackExchange.Redis.Tests/Sentinel.cs b/tests/StackExchange.Redis.Tests/Sentinel.cs index 821be3258..0f1cd06f8 100644 --- a/tests/StackExchange.Redis.Tests/Sentinel.cs +++ b/tests/StackExchange.Redis.Tests/Sentinel.cs @@ -118,6 +118,7 @@ public void SentinelRole() foreach (var server in SentinelsServers) { var role = server.Role(); + Assert.NotNull(role); Assert.Equal(role.Value, RedisLiterals.sentinel); var sentinel = role as Role.Sentinel; Assert.NotNull(sentinel); diff --git a/tests/StackExchange.Redis.Tests/SentinelBase.cs b/tests/StackExchange.Redis.Tests/SentinelBase.cs index 2b4528bf4..51d083d26 100644 --- a/tests/StackExchange.Redis.Tests/SentinelBase.cs +++ b/tests/StackExchange.Redis.Tests/SentinelBase.cs @@ -79,7 +79,7 @@ protected async Task WaitForReadyAsync(EndPoint? expectedPrimary = null, bool wa // wait until we have 1 primary and 1 replica and have verified their roles var primary = SentinelServerA.SentinelGetMasterAddressByName(ServiceName); - if (expectedPrimary != null && expectedPrimary.ToString() != primary.ToString()) + if (expectedPrimary != null && expectedPrimary.ToString() != primary?.ToString()) { while (sw.Elapsed < duration.Value) { @@ -87,7 +87,7 @@ protected async Task WaitForReadyAsync(EndPoint? expectedPrimary = null, bool wa try { primary = SentinelServerA.SentinelGetMasterAddressByName(ServiceName); - if (expectedPrimary.ToString() == primary.ToString()) + if (expectedPrimary.ToString() == primary?.ToString()) break; } catch (Exception) @@ -96,7 +96,7 @@ protected async Task WaitForReadyAsync(EndPoint? expectedPrimary = null, bool wa } } } - if (expectedPrimary != null && expectedPrimary.ToString() != primary.ToString()) + if (expectedPrimary != null && expectedPrimary.ToString() != primary?.ToString()) throw new RedisException($"Primary was expected to be {expectedPrimary}"); Log($"Primary is {primary}"); @@ -105,7 +105,7 @@ protected async Task WaitForReadyAsync(EndPoint? expectedPrimary = null, bool wa await WaitForRoleAsync(checkConn.GetServer(primary), "master", duration.Value.Subtract(sw.Elapsed)).ForAwait(); var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); - if (replicas.Length > 0) + if (replicas?.Length > 0) { await Task.Delay(1000).ForAwait(); replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); @@ -128,7 +128,7 @@ protected async Task WaitForRoleAsync(IServer server, string role, TimeSpan? dur { try { - if (server.Role().Value == role) + if (server.Role()?.Value == role) { Log($"Done waiting for server ({server.EndPoint}) role to be \"{role}\""); return; diff --git a/tests/StackExchange.Redis.Tests/Sets.cs b/tests/StackExchange.Redis.Tests/Sets.cs index 67bd76ce6..41fe8d94f 100644 --- a/tests/StackExchange.Redis.Tests/Sets.cs +++ b/tests/StackExchange.Redis.Tests/Sets.cs @@ -47,8 +47,8 @@ public async Task SetRemoveArgTests() var key = Me(); RedisValue[]? values = null; - Assert.Throws(() => db.SetRemove(key, values)); - await Assert.ThrowsAsync(async () => await db.SetRemoveAsync(key, values).ForAwait()).ForAwait(); + Assert.Throws(() => db.SetRemove(key, values!)); + await Assert.ThrowsAsync(async () => await db.SetRemoveAsync(key, values!).ForAwait()).ForAwait(); values = Array.Empty(); Assert.Equal(0, db.SetRemove(key, values)); diff --git a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs index 97d8aa87a..279b34b42 100644 --- a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs @@ -148,7 +148,7 @@ public event EventHandler HashSlotMoved public void GetStatus(TextWriter log) => _inner.GetStatus(log); - public string GetStormLog() => _inner.GetStormLog(); + public string? GetStormLog() => _inner.GetStormLog(); public ISubscriber GetSubscriber(object? asyncState = null) => _inner.GetSubscriber(asyncState); @@ -170,6 +170,8 @@ public event EventHandler HashSlotMoved public void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All) => _inner.ExportConfiguration(destination, options); + + public override string ToString() => _inner.ToString(); } public void Dispose() @@ -219,10 +221,10 @@ public void Teardown(TextWriter output) foreach (var ep in _actualConnection.GetServerSnapshot()) { var interactive = ep.GetBridge(ConnectionType.Interactive); - TestBase.Log(output, $" {Format.ToString(interactive)}: " + interactive.GetStatus()); + TestBase.Log(output, $" {Format.ToString(interactive)}: " + interactive?.GetStatus()); var subscription = ep.GetBridge(ConnectionType.Subscription); - TestBase.Log(output, $" {Format.ToString(subscription)}: " + subscription.GetStatus()); + TestBase.Log(output, $" {Format.ToString(subscription)}: " + subscription?.GetStatus()); } } } diff --git a/tests/StackExchange.Redis.Tests/SortedSets.cs b/tests/StackExchange.Redis.Tests/SortedSets.cs index b950fc250..abfd33664 100644 --- a/tests/StackExchange.Redis.Tests/SortedSets.cs +++ b/tests/StackExchange.Redis.Tests/SortedSets.cs @@ -170,6 +170,7 @@ public async Task SortedSetPopMulti_Zero_Async() var t = db.SortedSetPopAsync(key, count: 0); Assert.True(t.IsCompleted); // sync var arr = await t; + Assert.NotNull(arr); Assert.Empty(arr); Assert.Equal(10, db.SortedSetLength(key)); diff --git a/tests/StackExchange.Redis.Tests/Streams.cs b/tests/StackExchange.Redis.Tests/Streams.cs index 9379d4998..297db18a0 100644 --- a/tests/StackExchange.Redis.Tests/Streams.cs +++ b/tests/StackExchange.Redis.Tests/Streams.cs @@ -40,7 +40,7 @@ public void StreamAddSinglePairWithAutoId() var db = conn.GetDatabase(); var messageId = db.StreamAdd(GetUniqueKey("auto_id"), "field1", "value1"); - Assert.True(messageId != RedisValue.Null && ((string)messageId).Length > 0); + Assert.True(messageId != RedisValue.Null && ((string?)messageId)?.Length > 0); } } @@ -66,11 +66,13 @@ public void StreamAddMultipleValuePairsWithAutoId() Assert.Single(entries); Assert.Equal(messageId, entries[0].Id); - Assert.Equal(2, entries[0].Values.Length); - Assert.Equal("field1", entries[0].Values[0].Name); - Assert.Equal("value1", entries[0].Values[0].Value); - Assert.Equal("field2", entries[0].Values[1].Name); - Assert.Equal("value2", entries[0].Values[1].Value); + var vals = entries[0].Values; + Assert.NotNull(vals); + Assert.Equal(2, vals.Length); + Assert.Equal("field1", vals[0].Name); + Assert.Equal("value1", vals[0].Value); + Assert.Equal("field2", vals[1].Name); + Assert.Equal("value2", vals[1].Value); } } @@ -238,15 +240,12 @@ public void StreamCreateConsumerGroupFailsIfKeyDoesntExist() var db = conn.GetDatabase(); // Pass 'false' for 'createStream' to ensure that an - // execption is thrown when the stream doesn't exist. - Assert.ThrowsAny(() => - { - db.StreamCreateConsumerGroup( + // exception is thrown when the stream doesn't exist. + Assert.ThrowsAny(() => db.StreamCreateConsumerGroup( key, "consumerGroup", StreamPosition.NewMessages, - createStream: false); - }); + createStream: false)); } } @@ -999,9 +998,9 @@ public void StreamGroupInfoGet() Assert.True(IsMessageId(groupInfoList[1].LastDeliveredId)); // can't test actual - will vary } - static bool IsMessageId(string value) + static bool IsMessageId(string? value) { - if (string.IsNullOrWhiteSpace(value)) return false; + if (value.IsNullOrWhiteSpace()) return false; return value.Length >= 3 && value.Contains('-'); } } @@ -1304,7 +1303,7 @@ public void StreamReadExpectedExceptionNullStreamList() Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); var db = conn.GetDatabase(); - Assert.Throws(() => db.StreamRead(null)); + Assert.Throws(() => db.StreamRead(null!)); } } @@ -1807,13 +1806,13 @@ public async Task StreamReadIndexerUsage() await db.StreamAddAsync(streamName, new[] { new NameValueEntry("x", "blah"), - new NameValueEntry("msg", @"{""name"":""test"",""id"":123}"), + new NameValueEntry("msg", /*lang=json,strict*/ @"{""name"":""test"",""id"":123}"), new NameValueEntry("y", "more blah"), }); var streamResult = await db.StreamRangeAsync(streamName, count: 1000); var evntJson = streamResult - .Select(x => (dynamic?)JsonConvert.DeserializeObject(x["msg"])) + .Select(x => (dynamic?)JsonConvert.DeserializeObject(x["msg"]!)) .ToList(); var obj = Assert.Single(evntJson); Assert.Equal(123, (int)obj!.id); diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index adac8d87b..713402912 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -157,7 +157,7 @@ public async Task GetLease() conn.StringSet(key, "abc", flags: CommandFlags.FireAndForget); using (var v1 = await conn.StringGetLeaseAsync(key).ConfigureAwait(false)) { - string s = v1.DecodeString(); + string? s = v1?.DecodeString(); Assert.Equal("abc", s); } } @@ -173,7 +173,9 @@ public async Task GetLeaseAsStream() conn.KeyDelete(key, CommandFlags.FireAndForget); conn.StringSet(key, "abc", flags: CommandFlags.FireAndForget); - using (var v1 = (await conn.StringGetLeaseAsync(key).ConfigureAwait(false)).AsStream()) + var lease = await conn.StringGetLeaseAsync(key).ConfigureAwait(false); + Assert.NotNull(lease); + using (var v1 = lease.AsStream()) { using (var sr = new StreamReader(v1)) { @@ -558,10 +560,10 @@ public async Task BitOp() Assert.Equal(1, await len_xor); Assert.Equal(1, await len_not); - var r_and = ((byte[])(await conn.StringGetAsync("and").ForAwait())).Single(); - var r_or = ((byte[])(await conn.StringGetAsync("or").ForAwait())).Single(); - var r_xor = ((byte[])(await conn.StringGetAsync("xor").ForAwait())).Single(); - var r_not = ((byte[])(await conn.StringGetAsync("not").ForAwait())).Single(); + var r_and = ((byte[]?)(await conn.StringGetAsync("and").ForAwait()))?.Single(); + var r_or = ((byte[]?)(await conn.StringGetAsync("or").ForAwait()))?.Single(); + var r_xor = ((byte[]?)(await conn.StringGetAsync("xor").ForAwait()))?.Single(); + var r_not = ((byte[]?)(await conn.StringGetAsync("not").ForAwait()))?.Single(); Assert.Equal((byte)(3 & 6 & 12), r_and); Assert.Equal((byte)(3 | 6 | 12), r_or); @@ -616,6 +618,6 @@ public void HashStringLength() } private static byte[] Encode(string value) => Encoding.UTF8.GetBytes(value); - private static string Decode(byte[] value) => Encoding.UTF8.GetString(value); + private static string? Decode(byte[]? value) => value is null ? null : Encoding.UTF8.GetString(value); } } diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 7dfec3994..b47c2f507 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -297,12 +297,12 @@ internal virtual IInternalConnectionMultiplexer Create( var muxer = CreateDefault( Writer, + configuration ?? GetConfiguration(), clientName, syncTimeout, allowAdmin, keepAlive, connectTimeout, password, tieBreaker, log, fail, disabledCommands, enabledCommands, checkConnect, failMessage, channelPrefix, proxy, - configuration ?? GetConfiguration(), logTransactionData, defaultDatabase, backlogPolicy, caller); @@ -314,6 +314,7 @@ internal virtual IInternalConnectionMultiplexer Create( public static ConnectionMultiplexer CreateDefault( TextWriter? output, + string configuration, string? clientName = null, int? syncTimeout = null, bool? allowAdmin = null, @@ -329,7 +330,6 @@ public static ConnectionMultiplexer CreateDefault( string? failMessage = null, string? channelPrefix = null, Proxy? proxy = null, - string? configuration = null, bool logTransactionData = true, int? defaultDatabase = null, BacklogPolicy? backlogPolicy = null, diff --git a/tests/StackExchange.Redis.Tests/Transactions.cs b/tests/StackExchange.Redis.Tests/Transactions.cs index 4b23c0019..a8c148d7a 100644 --- a/tests/StackExchange.Redis.Tests/Transactions.cs +++ b/tests/StackExchange.Redis.Tests/Transactions.cs @@ -279,7 +279,7 @@ public async Task BasicTranWithListExistsCondition(bool demandKeyExists, bool ke Assert.False(await exec, "neq: exec"); Assert.False(cond.WasSatisfied, "neq: was satisfied"); Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push - Assert.Null((string)get); // neq: get + Assert.Null((string?)get); // neq: get } } } @@ -328,7 +328,7 @@ public async Task BasicTranWithListEqualsCondition(string expected, string value Assert.False(await exec, "neq: exec"); Assert.False(cond.WasSatisfied, "neq: was satisfied"); Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push - Assert.Null((string)get); // neq: get + Assert.Null((string?)get); // neq: get } } } diff --git a/tests/StackExchange.Redis.Tests/Values.cs b/tests/StackExchange.Redis.Tests/Values.cs index ecf702136..4d8732780 100644 --- a/tests/StackExchange.Redis.Tests/Values.cs +++ b/tests/StackExchange.Redis.Tests/Values.cs @@ -19,7 +19,7 @@ public void NullValueChecks() Assert.True(four.HasValue); Assert.False(four.IsNullOrEmpty); - RedisValue n = default(RedisValue); + RedisValue n = default; Assert.True(n.IsNull); Assert.False(n.IsInteger); Assert.False(n.HasValue); diff --git a/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs b/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs index bd32c235c..b27ef389a 100644 --- a/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs +++ b/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs @@ -63,7 +63,7 @@ public void NullDatabaseIsError(string prefix) Assert.Throws(() => { IDatabase? raw = null; - raw.WithKeyPrefix(prefix); + raw!.WithKeyPrefix(prefix); }); } @@ -83,7 +83,7 @@ public void BasicSmokeTest() string s = Guid.NewGuid().ToString(), t = Guid.NewGuid().ToString(); foo.StringSet(key, s, flags: CommandFlags.FireAndForget); - var val = (string)foo.StringGet(key); + var val = (string?)foo.StringGet(key); Assert.Equal(s, val); // fooBasicSmokeTest foobar.StringSet(key, t, flags: CommandFlags.FireAndForget); diff --git a/toys/KestrelRedisServer/Program.cs b/toys/KestrelRedisServer/Program.cs index 3695b3b95..5139db291 100644 --- a/toys/KestrelRedisServer/Program.cs +++ b/toys/KestrelRedisServer/Program.cs @@ -6,10 +6,7 @@ namespace KestrelRedisServer { public static class Program { - public static void Main(string[] args) - { - CreateWebHostBuilder(args).Build().Run(); - } + public static void Main(string[] args) => CreateWebHostBuilder(args).Build().Run(); public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) diff --git a/toys/StackExchange.Redis.Server/RedisServer.cs b/toys/StackExchange.Redis.Server/RedisServer.cs index 16d76ac42..8d3607e28 100644 --- a/toys/StackExchange.Redis.Server/RedisServer.cs +++ b/toys/StackExchange.Redis.Server/RedisServer.cs @@ -8,11 +8,10 @@ namespace StackExchange.Redis.Server { public abstract class RedisServer : RespServer { - public static bool IsMatch(string pattern, string key) - { - // non-trivial wildcards not implemented yet! - return pattern == "*" || string.Equals(pattern, key, StringComparison.OrdinalIgnoreCase); - } + // non-trivial wildcards not implemented yet! + public static bool IsMatch(string pattern, string key) => + pattern == "*" || string.Equals(pattern, key, StringComparison.OrdinalIgnoreCase); + protected RedisServer(int databases = 16, TextWriter output = null) : base(output) { if (databases < 1) throw new ArgumentOutOfRangeException(nameof(databases)); diff --git a/toys/StackExchange.Redis.Server/RespServer.cs b/toys/StackExchange.Redis.Server/RespServer.cs index f8111647c..174eb10fc 100644 --- a/toys/StackExchange.Redis.Server/RespServer.cs +++ b/toys/StackExchange.Redis.Server/RespServer.cs @@ -70,13 +70,11 @@ public string GetStats() AppendStats(sb); return sb.ToString(); } - protected virtual void AppendStats(StringBuilder sb) - { + protected virtual void AppendStats(StringBuilder sb) => sb.Append("Current clients:\t").Append(ClientCount).AppendLine() - .Append("Total clients:\t").Append(TotalClientCount).AppendLine() - .Append("Total operations:\t").Append(TotalCommandsProcesed).AppendLine() - .Append("Error replies:\t").Append(TotalErrorCount).AppendLine(); - } + .Append("Total clients:\t").Append(TotalClientCount).AppendLine() + .Append("Total operations:\t").Append(TotalCommandsProcesed).AppendLine() + .Append("Error replies:\t").Append(TotalErrorCount).AppendLine(); [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] protected sealed class RedisCommandAttribute : Attribute diff --git a/version.json b/version.json index 2d8ba31f6..f5ce755a1 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "2.5", + "version": "2.6", "versionHeightOffset": -1, "assemblyVersion": "2.0", "publicReleaseRefSpec": [ From 58cbef02fe1ec3e2358706daf019543e0eab78e6 Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Fri, 8 Apr 2022 05:14:14 +0300 Subject: [PATCH 117/435] Support COPY (#2064) Adds support for https://redis.io/commands/copy/ Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 1 + .../Interfaces/IDatabase.cs | 12 +++ .../Interfaces/IDatabaseAsync.cs | 12 +++ .../KeyspaceIsolation/DatabaseWrapper.cs | 3 + .../KeyspaceIsolation/WrapperBase.cs | 3 + src/StackExchange.Redis/PublicAPI.Shipped.txt | 2 + src/StackExchange.Redis/RedisDatabase.cs | 22 ++++++ src/StackExchange.Redis/RedisFeatures.cs | 5 -- src/StackExchange.Redis/RedisLiterals.cs | 2 +- tests/StackExchange.Redis.Tests/Copy.cs | 73 +++++++++++++++++++ .../DatabaseWrapperTests.cs | 7 ++ .../StackExchange.Redis.Tests/Helpers/Skip.cs | 12 +++ tests/StackExchange.Redis.Tests/Strings.cs | 9 ++- .../WrapperBaseTests.cs | 7 ++ 15 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/Copy.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 59ac059c9..4b7a77c9e 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,6 +7,7 @@ - Fixes a few internal edge cases that will now throw proper errors (rather than a downstream null reference) - Fixes inconsistencies with `null` vs. empty array returns (preferring an not-null empty array in those edge cases) - Note: does *not* increment a major version (as these are warnings to consumers), because: they're warnings (errors are opt-in), removing obsolete types with a 3.0 rev _would_ be binary breaking (this isn't), and reving to 3.0 would cause binding redirect pain for consumers. Bumping from 2.5 to 2.6 only for this change. +- Adds: Support for `COPY` ([#2064 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2064)) ## 2.5.61 diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 30a93ccf4..09d067184 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -20,6 +20,7 @@ internal enum RedisCommand CLIENT, CLUSTER, CONFIG, + COPY, DBSIZE, DEBUG, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 213a7daf7..0c4c4acce 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -472,6 +472,18 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The endpoint serving the key. EndPoint? IdentifyEndpoint(RedisKey key = default, CommandFlags flags = CommandFlags.None); + /// + /// Copies the value from the to the specified . + /// + /// The key of the source value to copy. + /// The destination key to copy the source to. + /// The database ID to store in. If default (-1), current database is used. + /// Whether to overwrite an existing values at . If and the key exists, the copy will not succeed. + /// The flags to use for this operation. + /// if key was copied. if key was not copied. + /// https://redis.io/commands/copy + bool KeyCopy(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None); + /// /// Removes the specified key. A key is ignored if it does not exist. /// If UNLINK is available (Redis 4.0+), it will be used. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 0a9eb3ace..81d4d55af 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -448,6 +448,18 @@ public interface IDatabaseAsync : IRedisAsync /// The endpoint serving the key. Task IdentifyEndpointAsync(RedisKey key = default, CommandFlags flags = CommandFlags.None); + /// + /// Copies the value from the to the specified . + /// + /// The key of the source value to copy. + /// The destination key to copy the source to. + /// The database ID to store in. If default (-1), current database is used. + /// Whether to overwrite an existing values at . If and the key exists, the copy will not succeed. + /// The flags to use for this operation. + /// if key was copied. if key was not copied. + /// https://redis.io/commands/copy + Task KeyCopyAsync(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None); + /// /// Removes the specified key. A key is ignored if it does not exist. /// If UNLINK is available (Redis 4.0+), it will be used. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 411c86af0..e755c2d34 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -126,6 +126,9 @@ public void HyperLogLogMerge(RedisKey destination, RedisKey first, RedisKey seco public EndPoint? IdentifyEndpoint(RedisKey key = default, CommandFlags flags = CommandFlags.None) => Inner.IdentifyEndpoint(ToInner(key), flags); + public bool KeyCopy(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None) => + Inner.KeyCopy(ToInner(sourceKey), ToInner(destinationKey), destinationDatabase, replace, flags); + public long KeyDelete(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Inner.KeyDelete(ToInner(keys), flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 97d0abd6f..2da36d504 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -135,6 +135,9 @@ public Task HyperLogLogMergeAsync(RedisKey destination, RedisKey first, RedisKey public bool IsConnected(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.IsConnected(ToInner(key), flags); + public Task KeyCopyAsync(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None) => + Inner.KeyCopyAsync(ToInner(sourceKey), ToInner(destinationKey), destinationDatabase, replace, flags); + public Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Inner.KeyDeleteAsync(ToInner(keys), flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 991430e81..008027d61 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -515,6 +515,7 @@ StackExchange.Redis.IDatabase.HyperLogLogLength(StackExchange.Redis.RedisKey[]! StackExchange.Redis.IDatabase.HyperLogLogMerge(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IDatabase.HyperLogLogMerge(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! sourceKeys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IDatabase.IdentifyEndpoint(StackExchange.Redis.RedisKey key = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Net.EndPoint? +StackExchange.Redis.IDatabase.KeyCopy(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyDelete(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.KeyDump(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> byte[]? @@ -703,6 +704,7 @@ StackExchange.Redis.IDatabaseAsync.HyperLogLogMergeAsync(StackExchange.Redis.Red StackExchange.Redis.IDatabaseAsync.HyperLogLogMergeAsync(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! sourceKeys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.IdentifyEndpointAsync(StackExchange.Redis.RedisKey key = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.IsConnected(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabaseAsync.KeyCopyAsync(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyDeleteAsync(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyDumpAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 3820ed747..6eb4ed0bf 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -601,6 +601,18 @@ public bool IsConnected(RedisKey key, CommandFlags flags = CommandFlags.None) return server?.IsConnected == true; } + public bool KeyCopy(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None) + { + var msg = GetCopyMessage(sourceKey, destinationKey, destinationDatabase, replace, flags); + return ExecuteSync(msg, ResultProcessor.Boolean); + } + + public Task KeyCopyAsync(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None) + { + var msg = GetCopyMessage(sourceKey, destinationKey, destinationDatabase, replace, flags); + return ExecuteAsync(msg, ResultProcessor.Boolean); + } + public bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None) { var cmd = GetDeleteCommand(key, flags, out var server); @@ -2733,6 +2745,16 @@ public Task StringSetRangeAsync(RedisKey key, long offset, RedisValu _ => throw new ArgumentException("Expiry time must be either Utc or Local", nameof(when)), }; + private Message GetCopyMessage(in RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase, bool replace, CommandFlags flags) => + destinationDatabase switch + { + < -1 => throw new ArgumentOutOfRangeException(nameof(destinationDatabase)), + -1 when replace => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.REPLACE), + -1 => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey), + _ when replace => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.DB, destinationDatabase, RedisLiterals.REPLACE), + _ => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.DB, destinationDatabase), + }; + private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, TimeSpan? expiry, out ServerEndPoint? server) { TimeSpan duration; diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index e44c43660..81dc29d00 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -76,11 +76,6 @@ public RedisFeatures(Version version) /// public bool GetDelete => Version >= v6_2_0; - /// - /// Does GETEX exist? - /// - internal bool GetEx => Version >= v6_2_0; - /// /// Is HSTRLEN available? /// diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 4dc2b6657..a31fbdf25 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -1,5 +1,4 @@ using System; -using System.Text; namespace StackExchange.Redis { @@ -54,6 +53,7 @@ public static readonly RedisValue CHANNELS = "CHANNELS", COPY = "COPY", COUNT = "COUNT", + DB = "DB", DESC = "DESC", DOCTOR = "DOCTOR", EX = "EX", diff --git a/tests/StackExchange.Redis.Tests/Copy.cs b/tests/StackExchange.Redis.Tests/Copy.cs new file mode 100644 index 000000000..1d7c05b62 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Copy.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests +{ + [Collection(SharedConnectionFixture.Key)] + public class Copy : TestBase + { + public Copy(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + + [Fact] + public async Task Basic() + { + using var muxer = Create(); + Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + + var db = muxer.GetDatabase(); + var src = Me(); + var dest = Me() + "2"; + _ = db.KeyDelete(dest); + + _ = db.StringSetAsync(src, "Heyyyyy"); + var ke1 = db.KeyCopyAsync(src, dest).ForAwait(); + var ku1 = db.StringGet(dest); + Assert.True(await ke1); + Assert.True(ku1.Equals("Heyyyyy")); + } + + [Fact] + public async Task CrossDB() + { + using var muxer = Create(); + Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + + var db = muxer.GetDatabase(); + var dbDestId = TestConfig.GetDedicatedDB(muxer); + var dbDest = muxer.GetDatabase(dbDestId); + + var src = Me(); + var dest = Me() + "2"; + dbDest.KeyDelete(dest); + + _ = db.StringSetAsync(src, "Heyyyyy"); + var ke1 = db.KeyCopyAsync(src, dest, dbDestId).ForAwait(); + var ku1 = dbDest.StringGet(dest); + Assert.True(await ke1); + Assert.True(ku1.Equals("Heyyyyy")); + + await Assert.ThrowsAsync(() => db.KeyCopyAsync(src, dest, destinationDatabase: -10)); + } + + [Fact] + public async Task WithReplace() + { + using var muxer = Create(); + Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + + var db = muxer.GetDatabase(); + var src = Me(); + var dest = Me() + "2"; + _ = db.StringSetAsync(src, "foo1"); + _ = db.StringSetAsync(dest, "foo2"); + var ke1 = db.KeyCopyAsync(src, dest).ForAwait(); + var ke2 = db.KeyCopyAsync(src, dest, replace: true).ForAwait(); + var ku1 = db.StringGet(dest); + Assert.False(await ke1); // Should fail when not using replace and destination key exist + Assert.True(await ke2); + Assert.True(ku1.Equals("foo1")); + } + } +} diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 061e782a9..0ddd9981b 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -236,6 +236,13 @@ public void IdentifyEndpoint() mock.Verify(_ => _.IdentifyEndpoint("prefix:key", CommandFlags.None)); } + [Fact] + public void KeyCopy() + { + wrapper.KeyCopy("key", "destination", flags: CommandFlags.None); + mock.Verify(_ => _.KeyCopy("prefix:key", "prefix:destination", -1, false, CommandFlags.None)); + } + [Fact] public void KeyDelete_1() { diff --git a/tests/StackExchange.Redis.Tests/Helpers/Skip.cs b/tests/StackExchange.Redis.Tests/Helpers/Skip.cs index 2a39c985f..b1cdffa30 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Skip.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Skip.cs @@ -24,6 +24,18 @@ public static void IfNoConfig(string prop, [NotNull] List? values) } } + public static void IfBelow(IConnectionMultiplexer conn, Version minVersion) + { + var serverVersion = conn.GetServer(conn.GetEndPoints()[0]).Version; + if (minVersion > serverVersion) + { + throw new SkipTestException($"Requires server version {minVersion}, but server is only {serverVersion}.") + { + MissingFeatures = $"Server version >= {minVersion}." + }; + } + } + public static void IfMissingFeature(IConnectionMultiplexer conn, string feature, Func check) { var features = conn.GetServer(conn.GetEndPoints()[0]).Features; diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index 713402912..7ca5648c2 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -72,7 +72,7 @@ public async Task Set() public async Task StringGetSetExpiryNoValue() { using var muxer = Create(); - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetEx), r => r.GetEx); + Skip.IfBelow(muxer, RedisFeatures.v6_2_0); var conn = muxer.GetDatabase(); var key = Me(); @@ -87,7 +87,7 @@ public async Task StringGetSetExpiryNoValue() public async Task StringGetSetExpiryRelative() { using var muxer = Create(); - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetEx), r => r.GetEx); + Skip.IfBelow(muxer, RedisFeatures.v6_2_0); var conn = muxer.GetDatabase(); var key = Me(); @@ -107,7 +107,8 @@ public async Task StringGetSetExpiryRelative() public async Task StringGetSetExpiryAbsolute() { using var muxer = Create(); - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetEx), r => r.GetEx); + Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + var conn = muxer.GetDatabase(); var key = Me(); conn.KeyDelete(key, CommandFlags.FireAndForget); @@ -131,7 +132,7 @@ public async Task StringGetSetExpiryAbsolute() public async Task StringGetSetExpiryPersist() { using var muxer = Create(); - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetEx), r => r.GetEx); + Skip.IfBelow(muxer, RedisFeatures.v6_2_0); var conn = muxer.GetDatabase(); var key = Me(); diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index c747d403f..13b5fd8cd 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -196,6 +196,13 @@ public void IsConnected() mock.Verify(_ => _.IsConnected("prefix:key", CommandFlags.None)); } + [Fact] + public void KeyCopyAsync() + { + wrapper.KeyCopyAsync("key", "destination", flags: CommandFlags.None); + mock.Verify(_ => _.KeyCopyAsync("prefix:key", "prefix:destination", -1, false, CommandFlags.None)); + } + [Fact] public void KeyDeleteAsync_1() { From 8ec67140b55fdaa403049136f7ed0f454d262d38 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 8 Apr 2022 10:23:25 -0400 Subject: [PATCH 118/435] Tests: move to skip by version (#2072) This should again make PRs more straightforward by having 1 way to do things, and doesn't leave us appending kind of random surface area to `RedisFeatures`. Also tidies up a few `RedisFeatures` versions to be what docs reflect. --- src/StackExchange.Redis/RedisFeatures.cs | 12 +- tests/StackExchange.Redis.Tests/Databases.cs | 4 +- tests/StackExchange.Redis.Tests/GeoTests.cs | 14 +- tests/StackExchange.Redis.Tests/Hashes.cs | 8 +- tests/StackExchange.Redis.Tests/Keys.cs | 4 +- tests/StackExchange.Redis.Tests/Lists.cs | 8 +- tests/StackExchange.Redis.Tests/Memory.cs | 8 +- tests/StackExchange.Redis.Tests/Scans.cs | 4 +- tests/StackExchange.Redis.Tests/Scripting.cs | 50 ++++--- tests/StackExchange.Redis.Tests/Sets.cs | 6 +- tests/StackExchange.Redis.Tests/SortedSets.cs | 54 ++++---- tests/StackExchange.Redis.Tests/Streams.cs | 125 +++++++++--------- tests/StackExchange.Redis.Tests/Strings.cs | 25 ++-- .../StackExchange.Redis.Tests/Transactions.cs | 2 +- 14 files changed, 172 insertions(+), 152 deletions(-) diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 81dc29d00..171492855 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -38,7 +38,7 @@ public readonly struct RedisFeatures v5_0_0 = new Version(5, 0, 0), v6_0_0 = new Version(6, 0, 0), v6_2_0 = new Version(6, 2, 0), - v6_9_240 = new Version(6, 9, 240); // 7.0 RC1 is version 6.9.240 + v7_0_0_rc1 = new Version(6, 9, 240); // 7.0 RC1 is version 6.9.240 private readonly Version version; @@ -54,7 +54,7 @@ public RedisFeatures(Version version) /// /// Does BITOP / BITCOUNT exist? /// - public bool BitwiseOperations => Version >= v2_5_10; + public bool BitwiseOperations => Version >= v2_6_0; /// /// Is CLIENT SETNAME available? @@ -89,7 +89,7 @@ public RedisFeatures(Version version) /// /// Does INCRBYFLOAT / HINCRBYFLOAT exist? /// - public bool IncrementFloat => Version >= v2_5_7; + public bool IncrementFloat => Version >= v2_6_0; /// /// Does INFO support sections? @@ -139,7 +139,7 @@ public RedisFeatures(Version version) /// /// Does EVAL / EVALSHA / etc exist? /// - public bool Scripting => Version >= v2_5_7; + public bool Scripting => Version >= v2_6_0; /// /// Does SET support the GET option? @@ -159,7 +159,7 @@ public RedisFeatures(Version version) /// /// Does SET allow the NX and GET options to be used together? /// - public bool SetNotExistsAndGet => Version >= v6_9_240; + public bool SetNotExistsAndGet => Version >= v7_0_0_rc1; /// /// Does SADD support variadic usage? @@ -169,7 +169,7 @@ public RedisFeatures(Version version) /// /// Is ZPOPMAX and ZPOPMIN available? /// - public bool SortedSetPop => Version >= v4_9_1; + public bool SortedSetPop => Version >= v5_0_0; /// /// Is ZRANGESTORE available? diff --git a/tests/StackExchange.Redis.Tests/Databases.cs b/tests/StackExchange.Redis.Tests/Databases.cs index 678defe38..dee605312 100644 --- a/tests/StackExchange.Redis.Tests/Databases.cs +++ b/tests/StackExchange.Redis.Tests/Databases.cs @@ -91,7 +91,7 @@ public async Task SwapDatabases() { using (var muxer = Create(allowAdmin: true)) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.SwapDB), r => r.SwapDB); + Skip.IfBelow(muxer, RedisFeatures.v4_0_0); RedisKey key = Me(); var db0id = TestConfig.GetDedicatedDB(muxer); @@ -127,7 +127,7 @@ public async Task SwapDatabasesAsync() { using (var muxer = Create(allowAdmin: true)) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.SwapDB), r => r.SwapDB); + Skip.IfBelow(muxer, RedisFeatures.v4_0_0); RedisKey key = Me(); var db0id = TestConfig.GetDedicatedDB(muxer); diff --git a/tests/StackExchange.Redis.Tests/GeoTests.cs b/tests/StackExchange.Redis.Tests/GeoTests.cs index cbb135546..71f40d1f2 100644 --- a/tests/StackExchange.Redis.Tests/GeoTests.cs +++ b/tests/StackExchange.Redis.Tests/GeoTests.cs @@ -22,7 +22,7 @@ public void GeoAdd() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Geo), r => r.Geo); + Skip.IfBelow(conn, RedisFeatures.v3_2_0); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -50,7 +50,7 @@ public void GetDistance() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Geo), r => r.Geo); + Skip.IfBelow(conn, RedisFeatures.v3_2_0); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -69,7 +69,7 @@ public void GeoHash() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Geo), r => r.Geo); + Skip.IfBelow(conn, RedisFeatures.v3_2_0); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -95,7 +95,7 @@ public void GeoGetPosition() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Geo), r => r.Geo); + Skip.IfBelow(conn, RedisFeatures.v3_2_0); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -116,7 +116,7 @@ public void GeoRemove() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Geo), r => r.Geo); + Skip.IfBelow(conn, RedisFeatures.v3_2_0); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -139,7 +139,7 @@ public void GeoRadius() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Geo), r => r.Geo); + Skip.IfBelow(conn, RedisFeatures.v3_2_0); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -185,7 +185,7 @@ public async Task GeoRadiusOverloads() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Geo), r => r.Geo); + Skip.IfBelow(conn, RedisFeatures.v3_2_0); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); diff --git a/tests/StackExchange.Redis.Tests/Hashes.cs b/tests/StackExchange.Redis.Tests/Hashes.cs index 9673a2fe0..e15d68480 100644 --- a/tests/StackExchange.Redis.Tests/Hashes.cs +++ b/tests/StackExchange.Redis.Tests/Hashes.cs @@ -44,7 +44,8 @@ public async Task ScanAsync() { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.Scan), r => r.Scan); + Skip.IfBelow(muxer, RedisFeatures.v2_8_0); + var conn = muxer.GetDatabase(); var key = Me(); await conn.KeyDeleteAsync(key); @@ -92,7 +93,8 @@ public void Scan() { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.Scan), r => r.Scan); + Skip.IfBelow(muxer, RedisFeatures.v2_8_0); + var conn = muxer.GetDatabase(); var key = Me(); @@ -146,7 +148,7 @@ public async Task TestIncrByFloat() { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.IncrementFloat), r => r.IncrementFloat); + Skip.IfBelow(muxer, RedisFeatures.v2_6_0); var conn = muxer.GetDatabase(); var key = Me(); _ = conn.KeyDeleteAsync(key).ForAwait(); diff --git a/tests/StackExchange.Redis.Tests/Keys.cs b/tests/StackExchange.Redis.Tests/Keys.cs index 15663b692..ed41d5815 100644 --- a/tests/StackExchange.Redis.Tests/Keys.cs +++ b/tests/StackExchange.Redis.Tests/Keys.cs @@ -205,7 +205,7 @@ public async Task TouchIdleTime() { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.KeyTouch), r => r.KeyTouch); + Skip.IfBelow(muxer, RedisFeatures.v3_2_1); RedisKey key = Me(); var db = muxer.GetDatabase(); @@ -249,7 +249,7 @@ public async Task TouchIdleTimeAsync() { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.KeyTouch), r => r.KeyTouch); + Skip.IfBelow(muxer, RedisFeatures.v3_2_1); RedisKey key = Me(); var db = muxer.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/Lists.cs b/tests/StackExchange.Redis.Tests/Lists.cs index 8fa212c43..0667b6266 100644 --- a/tests/StackExchange.Redis.Tests/Lists.cs +++ b/tests/StackExchange.Redis.Tests/Lists.cs @@ -86,7 +86,7 @@ public void ListLeftPushMultipleToExisitingKey() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.PushMultiple), f => f.PushMultiple); + Skip.IfBelow(conn, RedisFeatures.v4_0_0); var db = conn.GetDatabase(); RedisKey key = "testlist"; db.KeyDelete(key, CommandFlags.FireAndForget); @@ -156,7 +156,7 @@ public async Task ListLeftPushAsyncMultipleToExisitingKey() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.PushMultiple), f => f.PushMultiple); + Skip.IfBelow(conn, RedisFeatures.v4_0_0); var db = conn.GetDatabase(); RedisKey key = "testlist"; db.KeyDelete(key, CommandFlags.FireAndForget); @@ -226,7 +226,7 @@ public void ListRightPushMultipleToExisitingKey() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.PushMultiple), f => f.PushMultiple); + Skip.IfBelow(conn, RedisFeatures.v4_0_0); var db = conn.GetDatabase(); RedisKey key = "testlist"; db.KeyDelete(key, CommandFlags.FireAndForget); @@ -296,7 +296,7 @@ public async Task ListRightPushAsyncMultipleToExisitingKey() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.PushMultiple), f => f.PushMultiple); + Skip.IfBelow(conn, RedisFeatures.v4_0_0); var db = conn.GetDatabase(); RedisKey key = "testlist"; db.KeyDelete(key, CommandFlags.FireAndForget); diff --git a/tests/StackExchange.Redis.Tests/Memory.cs b/tests/StackExchange.Redis.Tests/Memory.cs index 113576ee6..52277e478 100644 --- a/tests/StackExchange.Redis.Tests/Memory.cs +++ b/tests/StackExchange.Redis.Tests/Memory.cs @@ -14,7 +14,7 @@ public async Task CanCallDoctor() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Memory), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v4_0_0); var server = conn.GetServer(conn.GetEndPoints()[0]); string? doctor = server.MemoryDoctor(); Assert.NotNull(doctor); @@ -31,7 +31,7 @@ public async Task CanPurge() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Memory), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v4_0_0); var server = conn.GetServer(conn.GetEndPoints()[0]); server.MemoryPurge(); await server.MemoryPurgeAsync(); @@ -45,7 +45,7 @@ public async Task GetAllocatorStats() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Memory), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v4_0_0); var server = conn.GetServer(conn.GetEndPoints()[0]); var stats = server.MemoryAllocatorStats(); @@ -61,7 +61,7 @@ public async Task GetStats() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Memory), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v4_0_0); var server = conn.GetServer(conn.GetEndPoints()[0]); var stats = server.MemoryStats(); Assert.NotNull(stats); diff --git a/tests/StackExchange.Redis.Tests/Scans.cs b/tests/StackExchange.Redis.Tests/Scans.cs index da6a92020..8089de2ef 100644 --- a/tests/StackExchange.Redis.Tests/Scans.cs +++ b/tests/StackExchange.Redis.Tests/Scans.cs @@ -104,8 +104,8 @@ public void ScanResume() { using (var conn = Create(allowAdmin: true)) { - // only goes up to 3.*, so... - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scan), x => x.Scan); + Skip.IfBelow(conn, RedisFeatures.v2_8_0); + var dbId = TestConfig.GetDedicatedDB(conn); var db = conn.GetDatabase(dbId); var prefix = Me(); diff --git a/tests/StackExchange.Redis.Tests/Scripting.cs b/tests/StackExchange.Redis.Tests/Scripting.cs index b157eb8a2..2ca786d8a 100644 --- a/tests/StackExchange.Redis.Tests/Scripting.cs +++ b/tests/StackExchange.Redis.Tests/Scripting.cs @@ -20,7 +20,7 @@ private IConnectionMultiplexer GetScriptConn(bool allowAdmin = false) if (Debugger.IsAttached) syncTimeout = 500000; var muxer = Create(allowAdmin: allowAdmin, syncTimeout: syncTimeout); - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.Scripting), r => r.Scripting); + Skip.IfBelow(muxer, RedisFeatures.v2_6_0); return muxer; } @@ -388,7 +388,8 @@ public void TestBasicScripting() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + RedisValue newId = Guid.NewGuid().ToString(); RedisKey key = Me(); var db = conn.GetDatabase(); @@ -414,7 +415,8 @@ public async Task CheckLoads(bool async) using (var conn0 = Create(allowAdmin: true)) using (var conn1 = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn0, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn0, RedisFeatures.v2_6_0); + // note that these are on different connections (so we wouldn't expect // the flush to drop the local cache - assume it is a surprise!) var server = conn0.GetServer(TestConfig.Current.PrimaryServerAndPort); @@ -465,7 +467,8 @@ public void CompareScriptToDirect() using (var conn = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); @@ -516,7 +519,8 @@ public void TestCallByHash() using (var conn = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); @@ -546,7 +550,8 @@ public void SimpleLuaScript() using (var conn = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); @@ -602,7 +607,8 @@ public void SimpleRawScriptEvaluate() using (var conn = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); @@ -656,7 +662,8 @@ public void LuaScriptWithKeys() using (var conn = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); @@ -686,7 +693,8 @@ public void NoInlineReplacement() const string Script = "redis.call('set', @key, 'hello@example')"; using (var conn = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); @@ -722,7 +730,8 @@ public void SimpleLoadedLuaScript() using (var conn = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); @@ -779,7 +788,8 @@ public void LoadedLuaScriptWithKeys() using (var conn = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); @@ -852,7 +862,8 @@ public void IDatabaseLuaScriptConvenienceMethods() using (var conn = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var script = LuaScript.Prepare(Script); var db = conn.GetDatabase(); var key = Me(); @@ -876,7 +887,8 @@ public void IServerLuaScriptConvenienceMethods() using (var conn = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var script = LuaScript.Prepare(Script); var server = conn.GetServer(conn.GetEndPoints()[0]); var db = conn.GetDatabase(); @@ -917,7 +929,8 @@ public void LuaScriptWithWrappedDatabase() using (var conn = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var db = conn.GetDatabase(); var wrappedDb = db.WithKeyPrefix("prefix-"); var key = Me(); @@ -943,7 +956,8 @@ public async Task AsyncLuaScriptWithWrappedDatabase() using (var conn = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var db = conn.GetDatabase(); var wrappedDb = db.WithKeyPrefix("prefix-"); var key = Me(); @@ -969,7 +983,8 @@ public void LoadedLuaScriptWithWrappedDatabase() using (var conn = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var db = conn.GetDatabase(); var wrappedDb = db.WithKeyPrefix("prefix2-"); var key = Me(); @@ -996,7 +1011,8 @@ public async Task AsyncLoadedLuaScriptWithWrappedDatabase() using (var conn = Create(allowAdmin: true)) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Scripting), f => f.Scripting); + Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var db = conn.GetDatabase(); var wrappedDb = db.WithKeyPrefix("prefix2-"); var key = Me(); diff --git a/tests/StackExchange.Redis.Tests/Sets.cs b/tests/StackExchange.Redis.Tests/Sets.cs index 41fe8d94f..3036a7930 100644 --- a/tests/StackExchange.Redis.Tests/Sets.cs +++ b/tests/StackExchange.Redis.Tests/Sets.cs @@ -61,7 +61,7 @@ public void SetPopMulti_Multi() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SetPopMultiple), r => r.SetPopMultiple); + Skip.IfBelow(conn, RedisFeatures.v3_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -117,7 +117,7 @@ public async Task SetPopMulti_Multi_Async() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SetPopMultiple), r => r.SetPopMultiple); + Skip.IfBelow(conn, RedisFeatures.v3_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -232,7 +232,7 @@ public void SetPopMulti_Nil() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SetPopMultiple), r => r.SetPopMultiple); + Skip.IfBelow(conn, RedisFeatures.v3_2_0); var db = conn.GetDatabase(); var key = Me(); diff --git a/tests/StackExchange.Redis.Tests/SortedSets.cs b/tests/StackExchange.Redis.Tests/SortedSets.cs index abfd33664..e7cdcc59c 100644 --- a/tests/StackExchange.Redis.Tests/SortedSets.cs +++ b/tests/StackExchange.Redis.Tests/SortedSets.cs @@ -57,7 +57,7 @@ public void SortedSetPopMulti_Multi() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetPop), r => r.SortedSetPop); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -83,7 +83,7 @@ public void SortedSetPopMulti_Single() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetPop), r => r.SortedSetPop); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -108,7 +108,7 @@ public async Task SortedSetPopMulti_Multi_Async() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetPop), r => r.SortedSetPop); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -134,7 +134,7 @@ public async Task SortedSetPopMulti_Single_Async() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetPop), r => r.SortedSetPop); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -159,7 +159,7 @@ public async Task SortedSetPopMulti_Zero_Async() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetPop), r => r.SortedSetPop); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -181,7 +181,7 @@ public async Task SortedSetPopMulti_Zero_Async() public async Task SortedSetRangeStoreByRankAsync() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; @@ -197,7 +197,7 @@ public async Task SortedSetRangeStoreByRankAsync() public async Task SortedSetRangeStoreByRankLimitedAsync() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -219,7 +219,7 @@ public async Task SortedSetRangeStoreByRankLimitedAsync() public async Task SortedSetRangeStoreByScoreAsync() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -241,7 +241,7 @@ public async Task SortedSetRangeStoreByScoreAsync() public async Task SortedSetRangeStoreByScoreAsyncDefault() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -263,7 +263,7 @@ public async Task SortedSetRangeStoreByScoreAsyncDefault() public async Task SortedSetRangeStoreByScoreAsyncLimited() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -285,7 +285,7 @@ public async Task SortedSetRangeStoreByScoreAsyncLimited() public async Task SortedSetRangeStoreByScoreAsyncExclusiveRange() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -307,7 +307,7 @@ public async Task SortedSetRangeStoreByScoreAsyncExclusiveRange() public async Task SortedSetRangeStoreByScoreAsyncReverse() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -329,7 +329,7 @@ public async Task SortedSetRangeStoreByScoreAsyncReverse() public async Task SortedSetRangeStoreByLexAsync() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -351,7 +351,7 @@ public async Task SortedSetRangeStoreByLexAsync() public async Task SortedSetRangeStoreByLexExclusiveRangeAsync() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -373,7 +373,7 @@ public async Task SortedSetRangeStoreByLexExclusiveRangeAsync() public async Task SortedSetRangeStoreByLexRevRangeAsync() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -395,7 +395,7 @@ public async Task SortedSetRangeStoreByLexRevRangeAsync() public void SortedSetRangeStoreByRank() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -412,7 +412,7 @@ public void SortedSetRangeStoreByRank() public void SortedSetRangeStoreByRankLimited() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -434,7 +434,7 @@ public void SortedSetRangeStoreByRankLimited() public void SortedSetRangeStoreByScore() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -456,7 +456,7 @@ public void SortedSetRangeStoreByScore() public void SortedSetRangeStoreByScoreDefault() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -478,7 +478,7 @@ public void SortedSetRangeStoreByScoreDefault() public void SortedSetRangeStoreByScoreLimited() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -500,7 +500,7 @@ public void SortedSetRangeStoreByScoreLimited() public void SortedSetRangeStoreByScoreExclusiveRange() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -522,7 +522,7 @@ public void SortedSetRangeStoreByScoreExclusiveRange() public void SortedSetRangeStoreByScoreReverse() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -544,7 +544,7 @@ public void SortedSetRangeStoreByScoreReverse() public void SortedSetRangeStoreByLex() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -566,7 +566,7 @@ public void SortedSetRangeStoreByLex() public void SortedSetRangeStoreByLexExclusiveRange() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -588,7 +588,7 @@ public void SortedSetRangeStoreByLexExclusiveRange() public void SortedSetRangeStoreByLexRevRange() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -610,7 +610,7 @@ public void SortedSetRangeStoreByLexRevRange() public void SortedSetRangeStoreFailErroneousTake() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); @@ -627,7 +627,7 @@ public void SortedSetRangeStoreFailErroneousTake() public void SortedSetRangeStoreFailExclude() { using var conn = Create(); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.SortedSetRangeStore), r=> r.SortedSetRangeStore); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); diff --git a/tests/StackExchange.Redis.Tests/Streams.cs b/tests/StackExchange.Redis.Tests/Streams.cs index 297db18a0..0008291c1 100644 --- a/tests/StackExchange.Redis.Tests/Streams.cs +++ b/tests/StackExchange.Redis.Tests/Streams.cs @@ -17,10 +17,9 @@ public void IsStreamType() { using (var conn = Create()) { + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var key = GetUniqueKey("type_check"); - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); - var db = conn.GetDatabase(); db.StreamAdd(key, "field1", "value1"); @@ -35,7 +34,7 @@ public void StreamAddSinglePairWithAutoId() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var messageId = db.StreamAdd(GetUniqueKey("auto_id"), "field1", "value1"); @@ -49,7 +48,7 @@ public void StreamAddMultipleValuePairsWithAutoId() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var key = GetUniqueKey("multiple_value_pairs"); @@ -84,7 +83,7 @@ public void StreamAddWithManualId() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var messageId = db.StreamAdd(key, "field1", "value1", id); @@ -101,7 +100,7 @@ public void StreamAddMultipleValuePairsWithManualId() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -130,7 +129,7 @@ public void StreamConsumerGroupSetId() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -164,7 +163,7 @@ public void StreamConsumerGroupWithNoConsumers() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -189,7 +188,7 @@ public void StreamCreateConsumerGroup() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -210,7 +209,7 @@ public void StreamCreateConsumerGroupBeforeCreatingStream() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -235,7 +234,7 @@ public void StreamCreateConsumerGroupFailsIfKeyDoesntExist() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -256,7 +255,7 @@ public void StreamCreateConsumerGroupSucceedsWhenKeyExists() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -282,7 +281,7 @@ public void StreamConsumerGroupReadOnlyNewMessagesWithEmptyResponse() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -308,7 +307,7 @@ public void StreamConsumerGroupReadFromStreamBeginning() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -333,7 +332,7 @@ public void StreamConsumerGroupReadFromStreamBeginningWithCount() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -363,7 +362,7 @@ public void StreamConsumerGroupAcknowledgeMessage() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -406,7 +405,7 @@ public void StreamConsumerGroupClaimMessages() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -457,7 +456,7 @@ public void StreamConsumerGroupClaimMessagesReturningIds() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -511,7 +510,7 @@ public void StreamConsumerGroupReadMultipleOneReadBeginningOneReadNew() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -553,7 +552,7 @@ public void StreamConsumerGroupReadMultipleOnlyNewMessagesExpectNoResult() var stream2 = GetUniqueKey("stream2b"); using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -589,7 +588,7 @@ public void StreamConsumerGroupReadMultipleOnlyNewMessagesExpect1Result() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -632,7 +631,7 @@ public void StreamConsumerGroupReadMultipleRestrictCount() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -673,7 +672,7 @@ public void StreamConsumerGroupViewPendingInfoNoConsumers() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -699,7 +698,7 @@ public void StreamConsumerGroupViewPendingInfoWhenNothingPending() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -727,7 +726,7 @@ public void StreamConsumerGroupViewPendingInfoSummary() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -769,7 +768,7 @@ public async Task StreamConsumerGroupViewPendingMessageInfo() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -810,7 +809,7 @@ public void StreamConsumerGroupViewPendingMessageInfoForConsumer() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -847,7 +846,7 @@ public void StreamDeleteConsumer() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -882,7 +881,7 @@ public void StreamDeleteConsumerGroup() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -913,7 +912,7 @@ public void StreamDeleteMessage() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -937,7 +936,7 @@ public void StreamDeleteMessages() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -965,7 +964,7 @@ public void StreamGroupInfoGet() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); db.KeyDelete(key); @@ -1015,7 +1014,7 @@ public void StreamGroupConsumerInfoGet() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1048,7 +1047,7 @@ public void StreamInfoGet() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1074,7 +1073,7 @@ public void StreamInfoGetWithEmptyStream() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1101,7 +1100,7 @@ public void StreamNoConsumerGroups() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1122,7 +1121,7 @@ public void StreamPendingNoMessagesOrConsumers() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1184,7 +1183,7 @@ public void StreamRead() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1209,7 +1208,7 @@ public void StreamReadEmptyStream() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1236,7 +1235,7 @@ public void StreamReadEmptyStreams() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1268,7 +1267,7 @@ public void StreamReadExpectedExceptionInvalidCountMultipleStream() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var streamPositions = new [] { @@ -1288,7 +1287,7 @@ public void StreamReadExpectedExceptionInvalidCountSingleStream() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); Assert.Throws(() => db.StreamRead(key, "0-0", 0)); @@ -1300,7 +1299,7 @@ public void StreamReadExpectedExceptionNullStreamList() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); Assert.Throws(() => db.StreamRead(null!)); @@ -1312,7 +1311,7 @@ public void StreamReadExpectedExceptionEmptyStreamList() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1330,7 +1329,7 @@ public void StreamReadMultipleStreams() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1370,7 +1369,7 @@ public void StreamReadMultipleStreamsWithCount() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1408,7 +1407,7 @@ public void StreamReadMultipleStreamsWithReadPastSecondStream() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1443,7 +1442,7 @@ public void StreamReadMultipleStreamsWithEmptyResponse() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1473,7 +1472,7 @@ public void StreamReadPastEndOfStream() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1495,7 +1494,7 @@ public void StreamReadRange() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1517,7 +1516,7 @@ public void StreamReadRangeOfEmptyStream() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1541,7 +1540,7 @@ public void StreamReadRangeWithCount() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1562,7 +1561,7 @@ public void StreamReadRangeReverse() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1584,7 +1583,7 @@ public void StreamReadRangeReverseWithCount() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1605,7 +1604,7 @@ public void StreamReadWithAfterIdAndCount_1() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1628,7 +1627,7 @@ public void StreamReadWithAfterIdAndCount_2() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1653,7 +1652,7 @@ public void StreamTrimLength() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1678,7 +1677,7 @@ public void StreamVerifyLength() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1699,7 +1698,7 @@ public async Task AddWithApproxCountAsync() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); await db.StreamAddAsync(key, "field", "value", maxLength: 10, useApproximateMaxLength: true, flags: CommandFlags.None).ConfigureAwait(false); @@ -1713,7 +1712,7 @@ public void AddWithApproxCount() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); db.StreamAdd(key, "field", "value", maxLength: 10, useApproximateMaxLength: true, flags: CommandFlags.None); @@ -1729,7 +1728,7 @@ public void StreamReadGroupWithNoAckShowsNoPendingMessages() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1760,7 +1759,7 @@ public void StreamReadGroupMultiStreamWithNoAckShowsNoPendingMessages() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -1800,7 +1799,7 @@ public async Task StreamReadIndexerUsage() using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(conn, RedisFeatures.v5_0_0); var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index 7ca5648c2..9f166ee8e 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -192,7 +192,7 @@ public void GetDelete() { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetDelete), r => r.GetDelete); + Skip.IfBelow(muxer, RedisFeatures.v6_2_0); var conn = muxer.GetDatabase(); var prefix = Me(); @@ -217,7 +217,7 @@ public async Task GetDeleteAsync() { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.GetDelete), r => r.GetDelete); + Skip.IfBelow(muxer, RedisFeatures.v6_2_0); var conn = muxer.GetDatabase(); var prefix = Me(); @@ -279,7 +279,7 @@ public async Task SetKeepTtl() { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.SetKeepTtl), r => r.SetKeepTtl); + Skip.IfBelow(muxer, RedisFeatures.v6_0_0); var conn = muxer.GetDatabase(); var prefix = Me(); @@ -320,7 +320,7 @@ public async Task SetAndGet() { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.SetAndGet), r => r.SetAndGet); + Skip.IfBelow(muxer, RedisFeatures.v6_2_0); var conn = muxer.GetDatabase(); var prefix = Me(); @@ -391,7 +391,7 @@ public async Task SetNotExistsAndGet() { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.SetNotExistsAndGet), r => r.SetNotExistsAndGet); + Skip.IfBelow(muxer, RedisFeatures.v7_0_0_rc1); var conn = muxer.GetDatabase(); var prefix = Me(); @@ -424,7 +424,7 @@ public async Task Ranges() { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.StringSetRange), r => r.StringSetRange); + Skip.IfBelow(muxer, RedisFeatures.v2_1_8); var conn = muxer.GetDatabase(); var key = Me(); @@ -473,7 +473,7 @@ public async Task IncrDecrFloat() { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.IncrementFloat), r => r.IncrementFloat); + Skip.IfBelow(muxer, RedisFeatures.v2_6_0); var conn = muxer.GetDatabase(); var key = Me(); conn.KeyDelete(key, CommandFlags.FireAndForget); @@ -521,7 +521,7 @@ public async Task BitCount() { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.BitwiseOperations), r => r.BitwiseOperations); + Skip.IfBelow(muxer, RedisFeatures.v2_6_0); var conn = muxer.GetDatabase(); var key = Me(); @@ -541,7 +541,8 @@ public async Task BitOp() { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.BitwiseOperations), r => r.BitwiseOperations); + Skip.IfBelow(muxer, RedisFeatures.v2_6_0); + var conn = muxer.GetDatabase(); var prefix = Me(); var key1 = prefix + "1"; @@ -591,7 +592,8 @@ public async Task HashStringLengthAsync() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.HashStringLength), r => r.HashStringLength); + Skip.IfBelow(conn, RedisFeatures.v3_2_0); + var database = conn.GetDatabase(); var key = Me(); const string value = "hello world"; @@ -608,7 +610,8 @@ public void HashStringLength() { using (var conn = Create()) { - Skip.IfMissingFeature(conn, nameof(RedisFeatures.HashStringLength), r => r.HashStringLength); + Skip.IfBelow(conn, RedisFeatures.v3_2_0); + var database = conn.GetDatabase(); var key = Me(); const string value = "hello world"; diff --git a/tests/StackExchange.Redis.Tests/Transactions.cs b/tests/StackExchange.Redis.Tests/Transactions.cs index a8c148d7a..f370a46c8 100644 --- a/tests/StackExchange.Redis.Tests/Transactions.cs +++ b/tests/StackExchange.Redis.Tests/Transactions.cs @@ -1110,7 +1110,7 @@ public async Task BasicTranWithStreamLengthCondition(string value, ComparisonTyp { using (var muxer = Create()) { - Skip.IfMissingFeature(muxer, nameof(RedisFeatures.Streams), r => r.Streams); + Skip.IfBelow(muxer, RedisFeatures.v5_0_0); RedisKey key = Me(), key2 = Me() + "2"; var db = muxer.GetDatabase(); From 35fab74c373a3f1ebbc5c179566d738a719f323c Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Sun, 10 Apr 2022 16:32:13 +0300 Subject: [PATCH 119/435] Support LMOVE (#2065) Adds support for https://redis.io/commands/lmove/ Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 3 +- src/StackExchange.Redis/Enums/ListSide.cs | 29 ++++++++ src/StackExchange.Redis/Enums/RedisCommand.cs | 1 + .../Interfaces/IDatabase.cs | 13 ++++ .../Interfaces/IDatabaseAsync.cs | 13 ++++ .../KeyspaceIsolation/DatabaseWrapper.cs | 3 + .../KeyspaceIsolation/WrapperBase.cs | 5 +- src/StackExchange.Redis/PublicAPI.Shipped.txt | 5 ++ src/StackExchange.Redis/RedisDatabase.cs | 12 ++++ src/StackExchange.Redis/RedisLiterals.cs | 2 + .../DatabaseWrapperTests.cs | 7 ++ tests/StackExchange.Redis.Tests/Lists.cs | 71 ++++++++++++++----- .../WrapperBaseTests.cs | 7 ++ 13 files changed, 153 insertions(+), 18 deletions(-) create mode 100644 src/StackExchange.Redis/Enums/ListSide.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 4b7a77c9e..56c31a1a1 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,7 +7,8 @@ - Fixes a few internal edge cases that will now throw proper errors (rather than a downstream null reference) - Fixes inconsistencies with `null` vs. empty array returns (preferring an not-null empty array in those edge cases) - Note: does *not* increment a major version (as these are warnings to consumers), because: they're warnings (errors are opt-in), removing obsolete types with a 3.0 rev _would_ be binary breaking (this isn't), and reving to 3.0 would cause binding redirect pain for consumers. Bumping from 2.5 to 2.6 only for this change. -- Adds: Support for `COPY` ([#2064 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2064)) +- Adds: Support for `COPY` with `.KeyCopy()`/`.KeyCopyAsync()` ([#2064 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2064)) +- Adds: Support for `LMOVE` with `.ListMove()`/`.ListMoveAsync()` ([#2065 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2065)) ## 2.5.61 diff --git a/src/StackExchange.Redis/Enums/ListSide.cs b/src/StackExchange.Redis/Enums/ListSide.cs new file mode 100644 index 000000000..0e71f91e8 --- /dev/null +++ b/src/StackExchange.Redis/Enums/ListSide.cs @@ -0,0 +1,29 @@ +using System; + +namespace StackExchange.Redis +{ + /// + /// Specifies what side of the list to refer to. + /// + public enum ListSide + { + /// + /// The head of the list. + /// + Left, + /// + /// The tail of the list. + /// + Right, + } + + internal static class ListSideExtensions + { + public static RedisValue ToLiteral(this ListSide side) => side switch + { + ListSide.Left => RedisLiterals.LEFT, + ListSide.Right => RedisLiterals.RIGHT, + _ => throw new ArgumentOutOfRangeException(nameof(side)) + }; + } +} diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 09d067184..5e7de00c1 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -83,6 +83,7 @@ internal enum RedisCommand LINDEX, LINSERT, LLEN, + LMOVE, LPOP, LPUSH, LPUSHX, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 0c4c4acce..92853a20c 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -769,6 +769,19 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/llen long ListLength(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Returns and removes the first or last element of the list stored at , and pushes the element + /// as the first or last element of the list stored at . + /// + /// The key of the list to remove from. + /// The key of the list to move to. + /// What side of the list to remove from. + /// What side of the list to move to. + /// The flags to use for this operation. + /// The element being popped and pushed or if there is no element to move. + /// https://redis.io/commands/lmove + RedisValue ListMove(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, CommandFlags flags = CommandFlags.None); + /// /// Returns the specified elements of the list stored at key. /// The offsets start and stop are zero-based indexes, with 0 being the first element of the list (the head of the list), 1 being the next element and so on. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 81d4d55af..7bb26ed1b 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -745,6 +745,19 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/llen Task ListLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Returns and removes the first or last element of the list stored at , and pushes the element + /// as the first or last element of the list stored at . + /// + /// The key of the list to remove from. + /// The key of the list to move to. + /// What side of the list to remove from. + /// What side of the list to move to. + /// The flags to use for this operation. + /// The element being popped and pushed or if there is no element to move. + /// https://redis.io/commands/lmove + Task ListMoveAsync(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, CommandFlags flags = CommandFlags.None); + /// /// Returns the specified elements of the list stored at key. /// The offsets start and stop are zero-based indexes, with 0 being the first element of the list (the head of the list), 1 being the next element and so on. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index e755c2d34..a669cee57 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -203,6 +203,9 @@ public long ListLeftPush(RedisKey key, RedisValue value, When when = When.Always public long ListLength(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.ListLength(ToInner(key), flags); + public RedisValue ListMove(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, CommandFlags flags = CommandFlags.None) => + Inner.ListMove(ToInner(sourceKey), ToInner(destinationKey), sourceSide, destinationSide); + public RedisValue[] ListRange(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) => Inner.ListRange(ToInner(key), start, stop, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 2da36d504..694087e75 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -213,6 +213,9 @@ public Task ListLeftPushAsync(RedisKey key, RedisValue value, When when = public Task ListLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.ListLengthAsync(ToInner(key), flags); + public Task ListMoveAsync(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, CommandFlags flags = CommandFlags.None) => + Inner.ListMoveAsync(ToInner(sourceKey), ToInner(destinationKey), sourceSide, destinationSide); + public Task ListRangeAsync(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) => Inner.ListRangeAsync(ToInner(key), start, stop, flags); @@ -532,7 +535,7 @@ public Task StringGetAsync(RedisKey[] keys, CommandFlags flags = C public Task StringGetAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.StringGetAsync(ToInner(key), flags); - + public Task StringGetSetExpiryAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => Inner.StringGetSetExpiryAsync(ToInner(key), expiry, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 008027d61..da09e4044 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -411,6 +411,9 @@ StackExchange.Redis.GeoUnit.Feet = 3 -> StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit.Kilometers = 1 -> StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit.Meters = 0 -> StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit.Miles = 2 -> StackExchange.Redis.GeoUnit +StackExchange.Redis.ListSide +StackExchange.Redis.ListSide.Left = 0 -> StackExchange.Redis.ListSide +StackExchange.Redis.ListSide.Right = 1 -> StackExchange.Redis.ListSide StackExchange.Redis.HashEntry StackExchange.Redis.HashEntry.Equals(StackExchange.Redis.HashEntry other) -> bool StackExchange.Redis.HashEntry.HashEntry() -> void @@ -537,6 +540,7 @@ StackExchange.Redis.IDatabase.KeyType(StackExchange.Redis.RedisKey key, StackExc StackExchange.Redis.IDatabase.ListGetByIndex(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.ListInsertAfter(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pivot, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.ListInsertBefore(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pivot, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.ListMove(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.ListSide sourceSide, StackExchange.Redis.ListSide destinationSide, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.ListLeftPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.ListLeftPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long @@ -726,6 +730,7 @@ StackExchange.Redis.IDatabaseAsync.KeyTypeAsync(StackExchange.Redis.RedisKey key StackExchange.Redis.IDatabaseAsync.ListGetByIndexAsync(StackExchange.Redis.RedisKey key, long index, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListInsertAfterAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pivot, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListInsertBeforeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pivot, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListMoveAsync(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.ListSide sourceSide, StackExchange.Redis.ListSide destinationSide, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLeftPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLeftPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 6eb4ed0bf..cc8fa6acd 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1009,6 +1009,18 @@ public Task ListLengthAsync(RedisKey key, CommandFlags flags = CommandFlag return ExecuteAsync(msg, ResultProcessor.Int64); } + public RedisValue ListMove(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.LMOVE, sourceKey, destinationKey, sourceSide.ToLiteral(), destinationSide.ToLiteral()); + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + + public Task ListMoveAsync(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.LMOVE, sourceKey, destinationKey, sourceSide.ToLiteral(), destinationSide.ToLiteral()); + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + public RedisValue[] ListRange(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.LRANGE, key, start, stop); diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index a31fbdf25..ca3a79357 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -68,6 +68,7 @@ public static readonly RedisValue KEEPTTL = "KEEPTTL", KILL = "KILL", LATEST = "LATEST", + LEFT = "LEFT", LIMIT = "LIMIT", LIST = "LIST", LOAD = "LOAD", @@ -89,6 +90,7 @@ public static readonly RedisValue PURGE = "PURGE", PX = "PX", PXAT = "PXAT", + RIGHT = "RIGHT", REPLACE = "REPLACE", RESET = "RESET", RESETSTAT = "RESETSTAT", diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 0ddd9981b..53a7437f6 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -412,6 +412,13 @@ public void ListLength() mock.Verify(_ => _.ListLength("prefix:key", CommandFlags.None)); } + [Fact] + public void ListMove() + { + wrapper.ListMove("key", "destination", ListSide.Left, ListSide.Right, CommandFlags.None); + mock.Verify(_ => _.ListMove("prefix:key", "prefix:destination", ListSide.Left, ListSide.Right, CommandFlags.None)); + } + [Fact] public void ListRange() { diff --git a/tests/StackExchange.Redis.Tests/Lists.cs b/tests/StackExchange.Redis.Tests/Lists.cs index 0667b6266..a3daeb9a5 100644 --- a/tests/StackExchange.Redis.Tests/Lists.cs +++ b/tests/StackExchange.Redis.Tests/Lists.cs @@ -40,7 +40,7 @@ public void ListLeftPushEmptyValues() using (var conn = Create()) { var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var result = db.ListLeftPush(key, Array.Empty(), When.Always, CommandFlags.None); Assert.Equal(0, result); @@ -53,7 +53,7 @@ public void ListLeftPushKeyDoesNotExists() using (var conn = Create()) { var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var result = db.ListLeftPush(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); Assert.Equal(0, result); @@ -66,7 +66,7 @@ public void ListLeftPushToExisitingKey() using (var conn = Create()) { var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var pushResult = db.ListLeftPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); @@ -88,7 +88,7 @@ public void ListLeftPushMultipleToExisitingKey() { Skip.IfBelow(conn, RedisFeatures.v4_0_0); var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var pushResult = db.ListLeftPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); @@ -110,7 +110,7 @@ public async Task ListLeftPushAsyncEmptyValues() using (var conn = Create()) { var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var result = await db.ListLeftPushAsync(key, Array.Empty(), When.Always, CommandFlags.None); Assert.Equal(0, result); @@ -123,7 +123,7 @@ public async Task ListLeftPushAsyncKeyDoesNotExists() using (var conn = Create()) { var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var result = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); Assert.Equal(0, result); @@ -136,7 +136,7 @@ public async Task ListLeftPushAsyncToExisitingKey() using (var conn = Create()) { var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var pushResult = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); @@ -158,7 +158,7 @@ public async Task ListLeftPushAsyncMultipleToExisitingKey() { Skip.IfBelow(conn, RedisFeatures.v4_0_0); var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var pushResult = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); @@ -180,7 +180,7 @@ public void ListRightPushEmptyValues() using (var conn = Create()) { var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var result = db.ListRightPush(key, Array.Empty(), When.Always, CommandFlags.None); Assert.Equal(0, result); @@ -193,7 +193,7 @@ public void ListRightPushKeyDoesNotExists() using (var conn = Create()) { var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var result = db.ListRightPush(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); Assert.Equal(0, result); @@ -206,7 +206,7 @@ public void ListRightPushToExisitingKey() using (var conn = Create()) { var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var pushResult = db.ListRightPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); @@ -228,7 +228,7 @@ public void ListRightPushMultipleToExisitingKey() { Skip.IfBelow(conn, RedisFeatures.v4_0_0); var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var pushResult = db.ListRightPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); @@ -250,7 +250,7 @@ public async Task ListRightPushAsyncEmptyValues() using (var conn = Create()) { var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var result = await db.ListRightPushAsync(key, Array.Empty(), When.Always, CommandFlags.None); Assert.Equal(0, result); @@ -263,7 +263,7 @@ public async Task ListRightPushAsyncKeyDoesNotExists() using (var conn = Create()) { var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var result = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); Assert.Equal(0, result); @@ -276,7 +276,7 @@ public async Task ListRightPushAsyncToExisitingKey() using (var conn = Create()) { var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var pushResult = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); @@ -298,7 +298,7 @@ public async Task ListRightPushAsyncMultipleToExisitingKey() { Skip.IfBelow(conn, RedisFeatures.v4_0_0); var db = conn.GetDatabase(); - RedisKey key = "testlist"; + RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); var pushResult = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); @@ -313,5 +313,44 @@ public async Task ListRightPushAsyncMultipleToExisitingKey() Assert.Equal("testvalue3", rangeResult[2]); } } + + [Fact] + public async Task ListMove() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + RedisKey src = Me(); + RedisKey dest = Me() + "dest"; + db.KeyDelete(src, CommandFlags.FireAndForget); + + var pushResult = await db.ListRightPushAsync(src, new RedisValue[] { "testvalue1", "testvalue2" }); + Assert.Equal(2, pushResult); + + var rangeResult1 = db.ListMove(src, dest, ListSide.Left, ListSide.Right); + var rangeResult2 = db.ListMove(src, dest, ListSide.Left, ListSide.Left); + var rangeResult3 = db.ListMove(dest, src, ListSide.Right, ListSide.Right); + var rangeResult4 = db.ListMove(dest, src, ListSide.Right, ListSide.Left); + Assert.Equal("testvalue1", rangeResult1); + Assert.Equal("testvalue2", rangeResult2); + Assert.Equal("testvalue1", rangeResult3); + Assert.Equal("testvalue2", rangeResult4); + } + + [Fact] + public void ListMoveKeyDoesNotExist() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + RedisKey src = Me(); + RedisKey dest = Me() + "dest"; + db.KeyDelete(src, CommandFlags.FireAndForget); + + var rangeResult1 = db.ListMove(src, dest, ListSide.Left, ListSide.Right); + Assert.True(rangeResult1.IsNull); + } } } diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index 13b5fd8cd..1197a636d 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -372,6 +372,13 @@ public void ListLengthAsync() mock.Verify(_ => _.ListLengthAsync("prefix:key", CommandFlags.None)); } + [Fact] + public void ListMoveAsync() + { + wrapper.ListMoveAsync("key", "destination", ListSide.Left, ListSide.Right, CommandFlags.None); + mock.Verify(_ => _.ListMoveAsync("prefix:key", "prefix:destination", ListSide.Left, ListSide.Right, CommandFlags.None)); + } + [Fact] public void ListRangeAsync() { From c2a734886d137119da59e6fc195a2b499bf80665 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 11 Apr 2022 09:47:50 -0400 Subject: [PATCH 120/435] Redis 7.0: Compatibility (#2079) This ups the Docker image to 7.0-rc3 and fixes the few things broken in 7.0. Overall: - `SENTINEL SLAVES` -> `SENTINEL REPLICAS` (been a thing since 5.0 according to docs: https://redis.io/docs/manual/sentinel/#sentinel-commands) - `enable-debug-command yes` config needed for `DEBUG` tests - Slight error message difference on script throws This is needed to actually be testing the 7.0-RC contributions in PRs and locally. Be sure to: ```bash docker compose down docker compose up --build ``` ...to test this locally or going forward, to get the new version and configs. --- .github/workflows/CI.yml | 2 +- appveyor.yml | 2 +- src/StackExchange.Redis/RedisLiterals.cs | 1 + src/StackExchange.Redis/RedisServer.cs | 10 ++++------ tests/RedisConfigs/Basic/primary-6379-3.0.conf | 9 +++++++++ tests/RedisConfigs/Basic/primary-6379.conf | 3 ++- tests/RedisConfigs/Dockerfile | 2 +- tests/RedisConfigs/start-basic.cmd | 2 +- tests/StackExchange.Redis.Tests/Scripting.cs | 3 ++- 9 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 tests/RedisConfigs/Basic/primary-6379-3.0.conf diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 098dc0ced..35b9c8daf 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -56,7 +56,7 @@ jobs: - name: Start Redis Services (v3.0.503) working-directory: .\tests\RedisConfigs\3.0.503 run: | - .\redis-server.exe --service-install --service-name "redis-6379" "..\Basic\primary-6379.conf" + .\redis-server.exe --service-install --service-name "redis-6379" "..\Basic\primary-6379-3.0.conf" .\redis-server.exe --service-install --service-name "redis-6380" "..\Basic\replica-6380.conf" .\redis-server.exe --service-install --service-name "redis-6381" "..\Basic\secure-6381.conf" .\redis-server.exe --service-install --service-name "redis-6382" "..\Failover\primary-6382.conf" diff --git a/appveyor.yml b/appveyor.yml index 5c6123e37..8e774d89c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,7 +10,7 @@ install: cd tests\RedisConfigs\3.0.503 - redis-server.exe --service-install --service-name "redis-6379" "..\Basic\primary-6379.conf" + redis-server.exe --service-install --service-name "redis-6379" "..\Basic\primary-6379-3.0.conf" redis-server.exe --service-install --service-name "redis-6380" "..\Basic\replica-6380.conf" diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index ca3a79357..221e16c00 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -112,6 +112,7 @@ public static readonly RedisValue // Sentinel Literals MASTERS = "MASTERS", MASTER = "MASTER", + REPLICAS = "REPLICAS", SLAVES = "SLAVES", GETMASTERADDRBYNAME = "GET-MASTER-ADDR-BY-NAME", // RESET = "RESET", diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 758bd4105..a71aa9c4b 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -910,13 +910,13 @@ public Task SentinelGetSentinelAddressesAsync(string serviceName, Co public EndPoint[] SentinelGetReplicaAddresses(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SLAVES, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, Features.ReplicaCommands ? RedisLiterals.REPLICAS : RedisLiterals.SLAVES, (RedisValue)serviceName); return ExecuteSync(msg, ResultProcessor.SentinelAddressesEndPoints, defaultValue: Array.Empty()); } public Task SentinelGetReplicaAddressesAsync(string serviceName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SLAVES, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, Features.ReplicaCommands ? RedisLiterals.REPLICAS : RedisLiterals.SLAVES, (RedisValue)serviceName); return ExecuteAsync(msg, ResultProcessor.SentinelAddressesEndPoints, defaultValue: Array.Empty()); } @@ -962,8 +962,7 @@ KeyValuePair[][] IServer.SentinelSlaves(string serviceName, Comm public KeyValuePair[][] SentinelReplicas(string serviceName, CommandFlags flags = CommandFlags.None) { - // note: sentinel does not have "replicas" terminology at the current time - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SLAVES, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, Features.ReplicaCommands ? RedisLiterals.REPLICAS : RedisLiterals.SLAVES, (RedisValue)serviceName); return ExecuteSync(msg, ResultProcessor.SentinelArrayOfArrays, defaultValue: Array.Empty[]>()); } @@ -973,8 +972,7 @@ Task[][]> IServer.SentinelSlavesAsync(string servic public Task[][]> SentinelReplicasAsync(string serviceName, CommandFlags flags = CommandFlags.None) { - // note: sentinel does not have "replicas" terminology at the current time - var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.SLAVES, (RedisValue)serviceName); + var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, Features.ReplicaCommands ? RedisLiterals.REPLICAS : RedisLiterals.SLAVES, (RedisValue)serviceName); return ExecuteAsync(msg, ResultProcessor.SentinelArrayOfArrays, defaultValue: Array.Empty[]>()); } diff --git a/tests/RedisConfigs/Basic/primary-6379-3.0.conf b/tests/RedisConfigs/Basic/primary-6379-3.0.conf new file mode 100644 index 000000000..1f4d96da5 --- /dev/null +++ b/tests/RedisConfigs/Basic/primary-6379-3.0.conf @@ -0,0 +1,9 @@ +port 6379 +repl-diskless-sync yes +repl-diskless-sync-delay 0 +databases 2000 +maxmemory 6gb +dir "../Temp" +appendonly no +dbfilename "primary-6379.rdb" +save "" \ No newline at end of file diff --git a/tests/RedisConfigs/Basic/primary-6379.conf b/tests/RedisConfigs/Basic/primary-6379.conf index 1f4d96da5..dee83828c 100644 --- a/tests/RedisConfigs/Basic/primary-6379.conf +++ b/tests/RedisConfigs/Basic/primary-6379.conf @@ -6,4 +6,5 @@ maxmemory 6gb dir "../Temp" appendonly no dbfilename "primary-6379.rdb" -save "" \ No newline at end of file +save "" +enable-debug-command yes \ No newline at end of file diff --git a/tests/RedisConfigs/Dockerfile b/tests/RedisConfigs/Dockerfile index a37c99ca2..cba8d6af5 100644 --- a/tests/RedisConfigs/Dockerfile +++ b/tests/RedisConfigs/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:6.2.6 +FROM redis:7.0-rc3 COPY Basic /data/Basic/ COPY Failover /data/Failover/ diff --git a/tests/RedisConfigs/start-basic.cmd b/tests/RedisConfigs/start-basic.cmd index 586d38c33..723e30f98 100644 --- a/tests/RedisConfigs/start-basic.cmd +++ b/tests/RedisConfigs/start-basic.cmd @@ -2,7 +2,7 @@ echo Starting Basic: pushd %~dp0\Basic echo Primary: 6379 -@start "Redis (Primary): 6379" /min ..\3.0.503\redis-server.exe primary-6379.conf +@start "Redis (Primary): 6379" /min ..\3.0.503\redis-server.exe primary-6379-3.0.conf echo Replica: 6380 @start "Redis (Replica): 6380" /min ..\3.0.503\redis-server.exe replica-6380.conf echo Secure: 6381 diff --git a/tests/StackExchange.Redis.Tests/Scripting.cs b/tests/StackExchange.Redis.Tests/Scripting.cs index 2ca786d8a..09689c8fa 100644 --- a/tests/StackExchange.Redis.Tests/Scripting.cs +++ b/tests/StackExchange.Redis.Tests/Scripting.cs @@ -325,7 +325,8 @@ public void ScriptThrowsErrorInsideTransaction() Assert.Single(b.Exception.InnerExceptions); var ex = b.Exception.InnerExceptions.Single(); Assert.IsType(ex); - Assert.Equal("oops", ex.Message); + // 7.0 slightly changes the error format, accept either. + Assert.Contains(ex.Message, new[] { "ERR oops", "oops" }); } var afterTran = conn.StringGetAsync(key); Assert.Equal(2L, (long)conn.Wait(afterTran)); From ba352d27b5772c56ab67f3a0ecfcd7eb300a38f4 Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Mon, 11 Apr 2022 23:34:36 +0300 Subject: [PATCH 121/435] Support SINTERCARD (#2078) Adds support for https://redis-stack.io/commands/sintercard/ (#2055) Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 3 +- src/StackExchange.Redis/Enums/RedisCommand.cs | 1 + .../Interfaces/IDatabase.cs | 16 ++++++ .../Interfaces/IDatabaseAsync.cs | 16 ++++++ .../KeyspaceIsolation/DatabaseWrapper.cs | 3 ++ .../KeyspaceIsolation/WrapperBase.cs | 3 ++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 2 + src/StackExchange.Redis/RedisDatabase.cs | 32 +++++++++++ .../DatabaseWrapperTests.cs | 8 +++ tests/StackExchange.Redis.Tests/Sets.cs | 54 ++++++++++++++++++- .../WrapperBaseTests.cs | 8 +++ 11 files changed, 144 insertions(+), 2 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 56c31a1a1..00ef24fb2 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ - Note: does *not* increment a major version (as these are warnings to consumers), because: they're warnings (errors are opt-in), removing obsolete types with a 3.0 rev _would_ be binary breaking (this isn't), and reving to 3.0 would cause binding redirect pain for consumers. Bumping from 2.5 to 2.6 only for this change. - Adds: Support for `COPY` with `.KeyCopy()`/`.KeyCopyAsync()` ([#2064 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2064)) - Adds: Support for `LMOVE` with `.ListMove()`/`.ListMoveAsync()` ([#2065 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2065)) +- Adds: Support for `SINTERCARD` with `.SetIntersectionLength()`/`.SetIntersectionLengthAsync()` ([#2078 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2078)) ## 2.5.61 @@ -25,7 +26,7 @@ ## 2.5.43 -- Adds: Bounds checking for `ExponentialRetry` backoff policy ([#1921 by gliljas](https://github.com/StackExchange/StackExchange.Redis/pull/1921)) +- Adds: Bounds checking for `ExponentialRetry` backoff policy ([#1921 by gliljas](https://github.com/StackExchange/StackExchange.Redis/pull/1921)) - Adds: `DefaultOptionsProvider` support for endpoint-based defaults configuration ([#1987 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1987)) - Adds: Envoy proxy support ([#1989 by rkarthick](https://github.com/StackExchange/StackExchange.Redis/pull/1989)) - Performance: When `SUBSCRIBE` is disabled, give proper errors and connect faster ([#2001 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2001)) diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 5e7de00c1..4b7ec84da 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -148,6 +148,7 @@ internal enum RedisCommand SETRANGE, SHUTDOWN, SINTER, + SINTERCARD, SINTERSTORE, SISMEMBER, SLAVEOF, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 92853a20c..e29e8b1d9 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1115,6 +1115,22 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/sismember bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); + /// + /// + /// Returns the set cardinality (number of elements) of the intersection between the sets stored at the given . + /// + /// + /// If the intersection cardinality reaches partway through the computation, + /// the algorithm will exit and yield as the cardinality. + /// + /// + /// The keys of the sets. + /// The number of elements to check (defaults to 0 and means unlimited). + /// The flags to use for this operation. + /// The cardinality (number of elements) of the set, or 0 if key does not exist. + /// https://redis.io/commands/scard + long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None); + /// /// Returns the set cardinality (number of elements) of the set stored at key. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 7bb26ed1b..1a02671f2 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1091,6 +1091,22 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/sismember Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); + /// + /// + /// Returns the set cardinality (number of elements) of the intersection between the sets stored at the given . + /// + /// + /// If the intersection cardinality reaches partway through the computation, + /// the algorithm will exit and yield as the cardinality. + /// + /// + /// The keys of the sets. + /// The number of elements to check (defaults to 0 and means unlimited). + /// The flags to use for this operation. + /// The cardinality (number of elements) of the set, or 0 if key does not exist. + /// https://redis.io/commands/scard + Task SetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None); + /// /// Returns the set cardinality (number of elements) of the set stored at key. /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index a669cee57..a35ec0c5e 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -294,6 +294,9 @@ public RedisValue[] SetCombine(SetOperation operation, RedisKey first, RedisKey public bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => Inner.SetContains(ToInner(key), value, flags); + public long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => + Inner.SetIntersectionLength(keys, limit, flags); + public long SetLength(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.SetLength(ToInner(key), flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 694087e75..798524999 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -304,6 +304,9 @@ public Task SetCombineAsync(SetOperation operation, RedisKey first public Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => Inner.SetContainsAsync(ToInner(key), value, flags); + public Task SetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => + Inner.SetIntersectionLengthAsync(keys, limit, flags); + public Task SetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.SetLengthAsync(ToInner(key), flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index da09e4044..ab7b9031e 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -573,6 +573,7 @@ StackExchange.Redis.IDatabase.SetCombine(StackExchange.Redis.SetOperation operat StackExchange.Redis.IDatabase.SetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.SetIntersectionLength(StackExchange.Redis.RedisKey[]! keys, long limit = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SetMembers(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.SetMove(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool @@ -763,6 +764,7 @@ StackExchange.Redis.IDatabaseAsync.SetCombineAndStoreAsync(StackExchange.Redis.S StackExchange.Redis.IDatabaseAsync.SetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetIntersectionLengthAsync(StackExchange.Redis.RedisKey[]! keys, long limit = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetMembersAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetMoveAsync(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index cc8fa6acd..6602f2060 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1383,6 +1383,18 @@ public Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags return ExecuteAsync(msg, ResultProcessor.Boolean); } + public long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) + { + var msg = GetSetIntersectionLengthMessage(keys, limit, flags); + return ExecuteSync(msg, ResultProcessor.Int64); + } + + public Task SetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) + { + var msg = GetSetIntersectionLengthMessage(keys, limit, flags); + return ExecuteAsync(msg, ResultProcessor.Int64); + } + public long SetLength(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.SCARD, key); @@ -2997,6 +3009,26 @@ private Message GetRestoreMessage(RedisKey key, byte[] value, TimeSpan? expiry, return Message.Create(Database, flags, RedisCommand.RESTORE, key, pttl, value); } + private Message GetSetIntersectionLengthMessage(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) + { + if (keys == null) throw new ArgumentNullException(nameof(keys)); + + var values = new RedisValue[1 + keys.Length + (limit > 0 ? 2 : 0)]; + int i = 0; + values[i++] = keys.Length; + for (var j = 0; j < keys.Length; j++) + { + values[i++] = keys[j].AsRedisValue(); + } + if (limit > 0) + { + values[i++] = RedisLiterals.LIMIT; + values[i] = limit; + } + + return Message.Create(Database, flags, RedisCommand.SINTERCARD, values); + } + private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double score, When when, CommandFlags flags) { WhenAlwaysOrExistsOrNotExists(when); diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 53a7437f6..5a48e6809 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -603,6 +603,14 @@ public void SetContains() mock.Verify(_ => _.SetContains("prefix:key", "value", CommandFlags.None)); } + [Fact] + public void SetIntersectionLength() + { + var keys = new RedisKey[] { "key1", "key2" }; + wrapper.SetIntersectionLength(keys); + mock.Verify(_ => _.SetIntersectionLength(keys, 0, CommandFlags.None)); + } + [Fact] public void SetLength() { diff --git a/tests/StackExchange.Redis.Tests/Sets.cs b/tests/StackExchange.Redis.Tests/Sets.cs index 3036a7930..745eac87c 100644 --- a/tests/StackExchange.Redis.Tests/Sets.cs +++ b/tests/StackExchange.Redis.Tests/Sets.cs @@ -11,6 +11,58 @@ public class Sets : TestBase { public Sets(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + [Fact] + public void SetIntersectionLength() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v7_0_0_rc1); + var db = conn.GetDatabase(); + + var key1 = Me() + "1"; + db.KeyDelete(key1, CommandFlags.FireAndForget); + db.SetAdd(key1, new RedisValue[] { 0, 1, 2, 3, 4 }, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + db.SetAdd(key2, new RedisValue[] { 1, 2, 3, 4, 5 }, CommandFlags.FireAndForget); + + Assert.Equal(4, db.SetIntersectionLength(new RedisKey[]{ key1, key2})); + // with limit + Assert.Equal(3, db.SetIntersectionLength(new RedisKey[]{ key1, key2}, 3)); + + // Missing keys should be 0 + var key3 = Me() + "3"; + var key4 = Me() + "4"; + db.KeyDelete(key3, CommandFlags.FireAndForget); + Assert.Equal(0, db.SetIntersectionLength(new RedisKey[] { key1, key3 })); + Assert.Equal(0, db.SetIntersectionLength(new RedisKey[] { key3, key4 })); + } + + [Fact] + public async Task SetIntersectionLengthAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v7_0_0_rc1); + var db = conn.GetDatabase(); + + var key1 = Me() + "1"; + db.KeyDelete(key1, CommandFlags.FireAndForget); + db.SetAdd(key1, new RedisValue[] { 0, 1, 2, 3, 4 }, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + db.SetAdd(key2, new RedisValue[] { 1, 2, 3, 4, 5 }, CommandFlags.FireAndForget); + + Assert.Equal(4, await db.SetIntersectionLengthAsync(new RedisKey[]{ key1, key2})); + // with limit + Assert.Equal(3, await db.SetIntersectionLengthAsync(new RedisKey[]{ key1, key2}, 3)); + + // Missing keys should be 0 + var key3 = Me() + "3"; + var key4 = Me() + "4"; + db.KeyDelete(key3, CommandFlags.FireAndForget); + Assert.Equal(0, await db.SetIntersectionLengthAsync(new RedisKey[] { key1, key3 })); + Assert.Equal(0, await db.SetIntersectionLengthAsync(new RedisKey[] { key3, key4 })); + } + [Fact] public void SScan() { @@ -18,7 +70,7 @@ public void SScan() { var server = GetAnyPrimary(conn); - RedisKey key = Me(); + var key = Me(); var db = conn.GetDatabase(); int totalUnfiltered = 0, totalFiltered = 0; for (int i = 1; i < 1001; i++) diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index 1197a636d..fac9fce28 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -563,6 +563,14 @@ public void SetContainsAsync() mock.Verify(_ => _.SetContainsAsync("prefix:key", "value", CommandFlags.None)); } + [Fact] + public void SetIntersectionLengthAsync() + { + var keys = new RedisKey[] { "key1", "key2" }; + wrapper.SetIntersectionLengthAsync(keys); + mock.Verify(_ => _.SetIntersectionLengthAsync(keys, 0, CommandFlags.None)); + } + [Fact] public void SetLengthAsync() { From a64f74f281d133fb3a492063077c998ebf6e7fd5 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 11 Apr 2022 22:09:50 -0400 Subject: [PATCH 122/435] Cleanup: removing evil (#2081) Removes the #regions that divide us! (I couldn't find an .editorconfig for this, but removing from existing codebase helps makes it clear for future PRs!) --- src/StackExchange.Redis/Interfaces/IServer.cs | 4 ---- src/StackExchange.Redis/Profiling/ProfiledCommand.cs | 3 --- src/StackExchange.Redis/RedisServer.cs | 4 ---- src/StackExchange.Redis/ResultProcessor.cs | 8 -------- tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs | 4 ---- 5 files changed, 23 deletions(-) diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 6941e895c..4df6b937d 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -839,8 +839,6 @@ public partial interface IServer : IRedis /// https://redis.io/commands/memory-malloc-stats string? MemoryAllocatorStats(CommandFlags flags = CommandFlags.None); - #region Sentinel - /// /// Returns the IP and port number of the primary with that name. /// If a failover is in progress or terminated successfully for this primary it returns the address and port of the promoted replica. @@ -1000,8 +998,6 @@ public partial interface IServer : IRedis /// The command flags to use. /// https://redis.io/topics/sentinel Task[][]> SentinelSentinelsAsync(string serviceName, CommandFlags flags = CommandFlags.None); - - #endregion } /// diff --git a/src/StackExchange.Redis/Profiling/ProfiledCommand.cs b/src/StackExchange.Redis/Profiling/ProfiledCommand.cs index dc31c5ed2..e4037902e 100644 --- a/src/StackExchange.Redis/Profiling/ProfiledCommand.cs +++ b/src/StackExchange.Redis/Profiling/ProfiledCommand.cs @@ -11,7 +11,6 @@ internal sealed class ProfiledCommand : IProfiledCommand { private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; - #region IProfiledCommand Impl public EndPoint EndPoint => Server.EndPoint; public int Db => Message!.Db; @@ -42,8 +41,6 @@ private static TimeSpan GetElapsedTime(long timestampDelta) public RetransmissionReasonType? RetransmissionReason { get; } - #endregion - public ProfiledCommand? NextElement { get; set; } private Message? Message; diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index a71aa9c4b..cf094d1fd 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -882,8 +882,6 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - #region Sentinel - public EndPoint? SentinelGetMasterAddressByName(string serviceName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(-1, flags, RedisCommand.SENTINEL, RedisLiterals.GETMASTERADDRBYNAME, (RedisValue)serviceName); @@ -988,8 +986,6 @@ public Task[][]> SentinelSentinelsAsync(string serv return ExecuteAsync(msg, ResultProcessor.SentinelArrayOfArrays, defaultValue: Array.Empty[]>()); } - #endregion - public RedisResult Execute(string command, params object[] args) => Execute(command, args, CommandFlags.None); public RedisResult Execute(string command, ICollection args, CommandFlags flags = CommandFlags.None) diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 142ffc22e..e9ab9026c 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -129,8 +129,6 @@ public static readonly ResultProcessor TieBreaker = new TieBreakerProcessor(), ClusterNodesRaw = new ClusterNodesRawProcessor(); - #region Sentinel - public static readonly ResultProcessor SentinelPrimaryEndpoint = new SentinelGetPrimaryAddressByNameProcessor(); @@ -143,8 +141,6 @@ public static readonly ResultProcessor public static readonly ResultProcessor[][]> SentinelArrayOfArrays = new SentinelArrayOfArraysProcessor(); - #endregion - public static readonly ResultProcessor[]> StringPairInterleaved = new StringPairInterleavedProcessor(); public static readonly TimeSpanProcessor @@ -2188,8 +2184,6 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - #region Sentinel - private sealed class SentinelGetPrimaryAddressByNameProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) @@ -2324,8 +2318,6 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return false; } } - - #endregion } internal abstract class ResultProcessor : ResultProcessor diff --git a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs index 0a0f93411..bd6e99b55 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs @@ -658,7 +658,6 @@ public byte[][] SendDataCommandExpectMultiBulkReply(byte[] data, string command, throw new ResponseException("Unknown reply on multi-request: " + c + s); } - #region List commands public byte[][] ListRange(string key, int start, int end) { return SendDataCommandExpectMultiBulkReply(null, "LRANGE {0} {1} {2}\r\n", key, start, end); @@ -685,9 +684,7 @@ public byte[] LeftPop(string key) SendCommand("LPOP {0}\r\n", key); return ReadData(); } - #endregion - #region Set commands public bool AddToSet(string key, byte[] member) { return SendDataExpectInt(member, "SADD {0} {1}\r\n", key, member.Length) > 0; @@ -795,7 +792,6 @@ public bool MoveMemberToSet(string srcKey, string destKey, byte[] member) { return SendDataExpectInt(member, "SMOVE {0} {1} {2}\r\n", srcKey, destKey, member.Length) > 0; } - #endregion public void Dispose() { From 0e9e69834464f6a0a00d05b995a99e9d4a5f53ac Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Tue, 12 Apr 2022 15:36:58 +0300 Subject: [PATCH 123/435] Support SMISMEMBER (#2077) Adds support for https://redis-stack.io/commands/smismember/ (#2055) Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 1 + .../Interfaces/IDatabase.cs | 18 ++++- .../Interfaces/IDatabaseAsync.cs | 17 ++++- .../KeyspaceIsolation/DatabaseWrapper.cs | 3 + .../KeyspaceIsolation/WrapperBase.cs | 3 + src/StackExchange.Redis/PublicAPI.Shipped.txt | 2 + src/StackExchange.Redis/RawResult.cs | 3 + src/StackExchange.Redis/RedisDatabase.cs | 12 ++++ src/StackExchange.Redis/ResultProcessor.cs | 17 +++++ .../DatabaseWrapperTests.cs | 8 +++ tests/StackExchange.Redis.Tests/Sets.cs | 66 +++++++++++++++++++ .../WrapperBaseTests.cs | 8 +++ 13 files changed, 155 insertions(+), 4 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 00ef24fb2..b3bc9845e 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ - Note: does *not* increment a major version (as these are warnings to consumers), because: they're warnings (errors are opt-in), removing obsolete types with a 3.0 rev _would_ be binary breaking (this isn't), and reving to 3.0 would cause binding redirect pain for consumers. Bumping from 2.5 to 2.6 only for this change. - Adds: Support for `COPY` with `.KeyCopy()`/`.KeyCopyAsync()` ([#2064 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2064)) - Adds: Support for `LMOVE` with `.ListMove()`/`.ListMoveAsync()` ([#2065 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2065)) +- Adds: Support for `SMISMEMBER` with `.SetContains()`/`.SetContainsAsync()` ([#2077 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2077)) - Adds: Support for `SINTERCARD` with `.SetIntersectionLength()`/`.SetIntersectionLengthAsync()` ([#2078 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2078)) ## 2.5.61 diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 4b7ec84da..b4d3c5fd7 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -154,6 +154,7 @@ internal enum RedisCommand SLAVEOF, SLOWLOG, SMEMBERS, + SMISMEMBER, SMOVE, SORT, SPOP, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index e29e8b1d9..c6887608f 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1103,10 +1103,10 @@ public interface IDatabase : IRedis, IDatabaseAsync long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Returns if member is a member of the set stored at key. + /// Returns whether is a member of the set stored at . /// /// The key of the set. - /// The value to check for . + /// The value to check for. /// The flags to use for this operation. /// /// if the element is a member of the set. @@ -1115,6 +1115,20 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/sismember bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); + /// + /// Returns whether each of is a member of the set stored at . + /// + /// The key of the set. + /// The members to check for. + /// The flags to use for this operation. + /// + /// An array of booleans corresponding to , for each: + /// if the element is a member of the set. + /// if the element is not a member of the set, or if key does not exist. + /// + /// https://redis.io/commands/smismember + bool[] SetContains(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); + /// /// /// Returns the set cardinality (number of elements) of the intersection between the sets stored at the given . diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 1a02671f2..45074dd6d 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1079,10 +1079,10 @@ public interface IDatabaseAsync : IRedisAsync Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Returns if member is a member of the set stored at key. + /// Returns whether is a member of the set stored at . /// /// The key of the set. - /// The value to check for . + /// The value to check for. /// The flags to use for this operation. /// /// if the element is a member of the set. @@ -1091,6 +1091,19 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/sismember Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); + /// + /// Returns whether each of is a member of the set stored at . + /// + /// The key of the set. + /// The members to check for. + /// The flags to use for this operation. + /// + /// if the element is a member of the set. + /// if the element is not a member of the set, or if key does not exist. + /// + /// https://redis.io/commands/smismember + Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); + /// /// /// Returns the set cardinality (number of elements) of the intersection between the sets stored at the given . diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index a35ec0c5e..9563d4d13 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -294,6 +294,9 @@ public RedisValue[] SetCombine(SetOperation operation, RedisKey first, RedisKey public bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => Inner.SetContains(ToInner(key), value, flags); + public bool[] SetContains(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + Inner.SetContains(ToInner(key), values, flags); + public long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => Inner.SetIntersectionLength(keys, limit, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 798524999..87f40b608 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -304,6 +304,9 @@ public Task SetCombineAsync(SetOperation operation, RedisKey first public Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => Inner.SetContainsAsync(ToInner(key), value, flags); + public Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => + Inner.SetContainsAsync(ToInner(key), values, flags); + public Task SetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => Inner.SetIntersectionLengthAsync(keys, limit, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index ab7b9031e..4f07e98e2 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -573,6 +573,7 @@ StackExchange.Redis.IDatabase.SetCombine(StackExchange.Redis.SetOperation operat StackExchange.Redis.IDatabase.SetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.SetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool[]! StackExchange.Redis.IDatabase.SetIntersectionLength(StackExchange.Redis.RedisKey[]! keys, long limit = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SetMembers(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! @@ -764,6 +765,7 @@ StackExchange.Redis.IDatabaseAsync.SetCombineAndStoreAsync(StackExchange.Redis.S StackExchange.Redis.IDatabaseAsync.SetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetIntersectionLengthAsync(StackExchange.Redis.RedisKey[]! keys, long limit = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetMembersAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 4620fe17e..ef449c101 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -262,6 +262,9 @@ internal bool GetBoolean() [MethodImpl(MethodImplOptions.AggressiveInlining)] internal string?[]? GetItemsAsStrings() => this.ToArray((in RawResult x) => (string?)x.AsRedisValue()); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool[]? GetItemsAsBooleans() => this.ToArray((in RawResult x) => (bool)x.AsRedisValue()); + internal GeoPosition? GetItemsAsGeoPosition() { var items = GetItems(); diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 6602f2060..54181f982 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1383,6 +1383,18 @@ public Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags return ExecuteAsync(msg, ResultProcessor.Boolean); } + public bool[] SetContains(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.SMISMEMBER, key, values); + return ExecuteSync(msg, ResultProcessor.BooleanArray, defaultValue: Array.Empty()); + } + + public Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.SMISMEMBER, key, values); + return ExecuteAsync(msg, ResultProcessor.BooleanArray, defaultValue: Array.Empty()); + } + public long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) { var msg = GetSetIntersectionLengthMessage(keys, limit, flags); diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index e9ab9026c..583595377 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -82,6 +82,9 @@ public static readonly ResultProcessor public static readonly ResultProcessor StringArray = new StringArrayProcessor(); + public static readonly ResultProcessor + BooleanArray = new BooleanArrayProcessor(); + public static readonly ResultProcessor RedisGeoPositionArray = new RedisValueGeoPositionArrayProcessor(); public static readonly ResultProcessor @@ -1258,6 +1261,20 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + private sealed class BooleanArrayProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Type == ResultType.MultiBulk && !result.IsNull) + { + var arr = result.GetItemsAsBooleans()!; + SetResult(message, arr); + return true; + } + return false; + } + } + private sealed class RedisValueGeoPositionProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 5a48e6809..78db3c07a 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -603,6 +603,14 @@ public void SetContains() mock.Verify(_ => _.SetContains("prefix:key", "value", CommandFlags.None)); } + [Fact] + public void SetContains_2() + { + RedisValue[] values = new RedisValue[] { "value1", "value2" }; + wrapper.SetContains("key", values, CommandFlags.None); + mock.Verify(_ => _.SetContains("prefix:key", values, CommandFlags.None)); + } + [Fact] public void SetIntersectionLength() { diff --git a/tests/StackExchange.Redis.Tests/Sets.cs b/tests/StackExchange.Redis.Tests/Sets.cs index 745eac87c..215885051 100644 --- a/tests/StackExchange.Redis.Tests/Sets.cs +++ b/tests/StackExchange.Redis.Tests/Sets.cs @@ -11,6 +11,72 @@ public class Sets : TestBase { public Sets(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + [Fact] + public void SetContains() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key); + for (int i = 1; i < 1001; i++) + { + db.SetAdd(key, i, CommandFlags.FireAndForget); + } + + // Single member + var isMemeber = db.SetContains(key, 1); + Assert.True(isMemeber); + + // Multi members + var areMemebers = db.SetContains(key, new RedisValue[] { 0, 1, 2 }); + Assert.Equal(3, areMemebers.Length); + Assert.False(areMemebers[0]); + Assert.True(areMemebers[1]); + + // key not exists + db.KeyDelete(key); + isMemeber = db.SetContains(key, 1); + Assert.False(isMemeber); + areMemebers = db.SetContains(key, new RedisValue[] { 0, 1, 2 }); + Assert.Equal(3, areMemebers.Length); + Assert.True(areMemebers.All(i => !i)); // Check that all the elements are False + } + + [Fact] + public async Task SetContainsAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await db.KeyDeleteAsync(key); + for (int i = 1; i < 1001; i++) + { + db.SetAdd(key, i, CommandFlags.FireAndForget); + } + + // Single member + var isMemeber = await db.SetContainsAsync(key, 1); + Assert.True(isMemeber); + + // Multi members + var areMemebers = await db.SetContainsAsync(key, new RedisValue[] { 0, 1, 2 }); + Assert.Equal(3, areMemebers.Length); + Assert.False(areMemebers[0]); + Assert.True(areMemebers[1]); + + // key not exists + await db.KeyDeleteAsync(key); + isMemeber = await db.SetContainsAsync(key, 1); + Assert.False(isMemeber); + areMemebers = await db.SetContainsAsync(key, new RedisValue[] { 0, 1, 2 }); + Assert.Equal(3, areMemebers.Length); + Assert.True(areMemebers.All(i => !i)); // Check that all the elements are False + } + [Fact] public void SetIntersectionLength() { diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index fac9fce28..322f71a3f 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -563,6 +563,14 @@ public void SetContainsAsync() mock.Verify(_ => _.SetContainsAsync("prefix:key", "value", CommandFlags.None)); } + [Fact] + public void SetContainsAsync_2() + { + RedisValue[] values = new RedisValue[] { "value1", "value2" }; + wrapper.SetContainsAsync("key", values, CommandFlags.None); + mock.Verify(_ => _.SetContainsAsync("prefix:key", values, CommandFlags.None)); + } + [Fact] public void SetIntersectionLengthAsync() { From cd33bebc67383fbc15af097705b1285727e6ce72 Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Tue, 12 Apr 2022 15:56:24 +0300 Subject: [PATCH 124/435] Support ZRANDMEMBER (#2076) Adds support for https://redis-stack.io/commands/zrandmember/ (#2055) Co-authored-by: Nick Craver Co-authored-by: Steve Lorello <42971704+slorello89@users.noreply.github.com> --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 1 + .../Interfaces/IDatabase.cs | 53 ++++++++++- .../Interfaces/IDatabaseAsync.cs | 47 ++++++++++ .../KeyspaceIsolation/DatabaseWrapper.cs | 9 ++ .../KeyspaceIsolation/WrapperBase.cs | 9 ++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 6 ++ src/StackExchange.Redis/RedisDatabase.cs | 36 ++++++++ .../DatabaseWrapperTests.cs | 21 +++++ tests/StackExchange.Redis.Tests/SortedSets.cs | 89 +++++++++++++++++++ .../WrapperBaseTests.cs | 21 +++++ 11 files changed, 290 insertions(+), 3 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b3bc9845e..38d9abf19 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ - Note: does *not* increment a major version (as these are warnings to consumers), because: they're warnings (errors are opt-in), removing obsolete types with a 3.0 rev _would_ be binary breaking (this isn't), and reving to 3.0 would cause binding redirect pain for consumers. Bumping from 2.5 to 2.6 only for this change. - Adds: Support for `COPY` with `.KeyCopy()`/`.KeyCopyAsync()` ([#2064 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2064)) - Adds: Support for `LMOVE` with `.ListMove()`/`.ListMoveAsync()` ([#2065 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2065)) +- Adds: Support for `ZRANDMEMBER` with `.SortedSetRandomMember()`/`.SortedSetRandomMemberAsync()`, `.SortedSetRandomMembers()`/`.SortedSetRandomMembersAsync()`, and `.SortedSetRandomMembersWithScores()`/`.SortedSetRandomMembersWithScoresAsync()` ([#2076 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2076)) - Adds: Support for `SMISMEMBER` with `.SetContains()`/`.SetContainsAsync()` ([#2077 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2077)) - Adds: Support for `SINTERCARD` with `.SetIntersectionLength()`/`.SetIntersectionLengthAsync()` ([#2078 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2078)) diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index b4d3c5fd7..f0020cda4 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -201,6 +201,7 @@ internal enum RedisCommand ZLEXCOUNT, ZPOPMAX, ZPOPMIN, + ZRANDMEMBER, ZRANGE, ZRANGEBYLEX, ZRANGEBYSCORE, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index c6887608f..3eef153e4 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1199,11 +1199,11 @@ public interface IDatabase : IRedis, IDatabaseAsync RedisValue[] SetPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// - /// Return a random element from the set value stored at key. + /// Return a random element from the set value stored at . /// /// The key of the set. /// The flags to use for this operation. - /// The randomly selected element, or nil when key does not exist. + /// The randomly selected element, or when does not exist. /// https://redis.io/commands/srandmember RedisValue SetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -1215,7 +1215,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the set. /// The count of members to get. /// The flags to use for this operation. - /// An array of elements, or an empty array when key does not exist. + /// An array of elements, or an empty array when does not exist. /// https://redis.io/commands/srandmember RedisValue[] SetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None); @@ -1433,6 +1433,53 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/zlexcount long SortedSetLengthByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); + /// + /// Returns a random element from the sorted set value stored at . + /// + /// The key of the sorted set. + /// The flags to use for this operation. + /// The randomly selected element, or when does not exist. + /// https://redis.io/commands/zrandmember + RedisValue SortedSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Returns an array of random elements from the sorted set value stored at . + /// + /// The key of the sorted set. + /// + /// + /// If the provided count argument is positive, returns an array of distinct elements. + /// The array's length is either or the sorted set's cardinality (ZCARD), whichever is lower. + /// + /// + /// If called with a negative count, the behavior changes and the command is allowed to return the same element multiple times. + /// In this case, the number of returned elements is the absolute value of the specified count. + /// + /// + /// The flags to use for this operation. + /// The randomly selected elements, or an empty array when does not exist. + /// https://redis.io/commands/zrandmember + RedisValue[] SortedSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + + /// + /// Returns an array of random elements from the sorted set value stored at . + /// + /// The key of the sorted set. + /// + /// + /// If the provided count argument is positive, returns an array of distinct elements. + /// The array's length is either or the sorted set's cardinality (ZCARD), whichever is lower. + /// + /// + /// If called with a negative count, the behavior changes and the command is allowed to return the same element multiple times. + /// In this case, the number of returned elements is the absolute value of the specified count. + /// + /// + /// The flags to use for this operation. + /// The randomly selected elements with scores, or an empty array when does not exist. + /// https://redis.io/commands/zrandmember + SortedSetEntry[] SortedSetRandomMembersWithScores(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// /// Returns the specified range of elements in the sorted set stored at key. /// By default the elements are considered to be ordered from the lowest to the highest score. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 45074dd6d..1ec8d7e30 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1397,6 +1397,53 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/zlexcount Task SortedSetLengthByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); + /// + /// Returns a random element from the sorted set value stored at . + /// + /// The key of the sorted set. + /// The flags to use for this operation. + /// The randomly selected element, or when does not exist. + /// https://redis.io/commands/zrandmember + Task SortedSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Returns an array of random elements from the sorted set value stored at . + /// + /// The key of the sorted set. + /// + /// + /// If the provided count argument is positive, returns an array of distinct elements. + /// The array's length is either or the sorted set's cardinality (ZCARD), whichever is lower. + /// + /// + /// If called with a negative count, the behavior changes and the command is allowed to return the same element multiple times. + /// In this case, the number of returned elements is the absolute value of the specified count. + /// + /// + /// The flags to use for this operation. + /// The randomly selected elements, or an empty array when does not exist. + /// https://redis.io/commands/zrandmember + Task SortedSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + + /// + /// Returns an array of random elements from the sorted set value stored at . + /// + /// The key of the sorted set. + /// + /// + /// If the provided count argument is positive, returns an array of distinct elements. + /// The array's length is either or the sorted set's cardinality (ZCARD), whichever is lower. + /// + /// + /// If called with a negative count, the behavior changes and the command is allowed to return the same element multiple times. + /// In this case, the number of returned elements is the absolute value of the specified count. + /// + /// + /// The flags to use for this operation. + /// The randomly selected elements with scores, or an empty array when does not exist. + /// https://redis.io/commands/zrandmember + Task SortedSetRandomMembersWithScoresAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// /// Returns the specified range of elements in the sorted set stored at key. /// By default the elements are considered to be ordered from the lowest to the highest score. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 9563d4d13..c56209de1 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -363,6 +363,15 @@ public long SortedSetLength(RedisKey key, double min = -1.0 / 0.0, double max = public long SortedSetLengthByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => Inner.SortedSetLengthByValue(ToInner(key), min, max, exclude, flags); + public RedisValue SortedSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRandomMember(ToInner(key), flags); + + public RedisValue[] SortedSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRandomMembers(ToInner(key), count, flags); + + public SortedSetEntry[] SortedSetRandomMembersWithScores(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRandomMembersWithScores(ToInner(key), count, flags); + public RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => Inner.SortedSetRangeByRank(ToInner(key), start, stop, order, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 87f40b608..969637abd 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -376,6 +376,15 @@ public Task SortedSetLengthAsync(RedisKey key, double min = -1.0 / 0.0, do public Task SortedSetLengthByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => Inner.SortedSetLengthByValueAsync(ToInner(key), min, max, exclude, flags); + public Task SortedSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRandomMemberAsync(ToInner(key), flags); + + public Task SortedSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRandomMembersAsync(ToInner(key), count, flags); + + public Task SortedSetRandomMembersWithScoresAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetRandomMembersWithScoresAsync(ToInner(key), count, flags); + public Task SortedSetRangeAndStoreAsync( RedisKey sourceKey, RedisKey destinationKey, diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 4f07e98e2..b31fa1a9e 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -600,6 +600,9 @@ StackExchange.Redis.IDatabase.SortedSetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.IDatabase.SortedSetLengthByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry? +StackExchange.Redis.IDatabase.SortedSetRandomMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.SortedSetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.IDatabase.SortedSetRandomMembersWithScores(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! StackExchange.Redis.IDatabase.SortedSetRangeByRank(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.SortedSetRangeAndStore(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.RedisValue start, StackExchange.Redis.RedisValue stop, StackExchange.Redis.SortedSetOrder sortedSetOrder = StackExchange.Redis.SortedSetOrder.ByRank, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long? take = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetRangeByRankWithScores(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! @@ -791,6 +794,9 @@ StackExchange.Redis.IDatabaseAsync.SortedSetLengthAsync(StackExchange.Redis.Redi StackExchange.Redis.IDatabaseAsync.SortedSetLengthByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRandomMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetRandomMembersWithScoresAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetRangeAndStoreAsync(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.RedisValue start, StackExchange.Redis.RedisValue stop, StackExchange.Redis.SortedSetOrder sortedSetOrder = StackExchange.Redis.SortedSetOrder.ByRank, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, long skip = 0, long? take = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetRangeByRankAsync(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetRangeByRankWithScoresAsync(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 54181f982..aaa647e19 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1674,6 +1674,42 @@ public Task SortedSetLengthAsync(RedisKey key, double min = double.Negativ return ExecuteAsync(msg, ResultProcessor.Int64); } + public RedisValue SortedSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.ZRANDMEMBER, key); + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + + public RedisValue[] SortedSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.ZRANDMEMBER, key, count); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public SortedSetEntry[] SortedSetRandomMembersWithScores(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.ZRANDMEMBER, key, count, RedisLiterals.WITHSCORES); + return ExecuteSync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); + } + + public Task SortedSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.ZRANDMEMBER, key); + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + + public Task SortedSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.ZRANDMEMBER, key, count); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public Task SortedSetRandomMembersWithScoresAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.ZRANDMEMBER, key, count, RedisLiterals.WITHSCORES); + return ExecuteAsync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); + } + public RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, order == Order.Descending ? RedisCommand.ZREVRANGE : RedisCommand.ZRANGE, key, start, stop); diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 78db3c07a..5b2018858 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -778,6 +778,27 @@ public void SortedSetLength() mock.Verify(_ => _.SortedSetLength("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None)); } + [Fact] + public void SortedSetRandomMember() + { + wrapper.SortedSetRandomMember("key", CommandFlags.None); + mock.Verify(_ => _.SortedSetRandomMember("prefix:key", CommandFlags.None)); + } + + [Fact] + public void SortedSetRandomMembers() + { + wrapper.SortedSetRandomMembers("key", 2, CommandFlags.None); + mock.Verify(_ => _.SortedSetRandomMembers("prefix:key", 2, CommandFlags.None)); + } + + [Fact] + public void SortedSetRandomMembersWithScores() + { + wrapper.SortedSetRandomMembersWithScores("key", 2, CommandFlags.None); + mock.Verify(_ => _.SortedSetRandomMembersWithScores("prefix:key", 2, CommandFlags.None)); + } + [Fact] public void SortedSetLengthByValue() { diff --git a/tests/StackExchange.Redis.Tests/SortedSets.cs b/tests/StackExchange.Redis.Tests/SortedSets.cs index e7cdcc59c..f15779d78 100644 --- a/tests/StackExchange.Redis.Tests/SortedSets.cs +++ b/tests/StackExchange.Redis.Tests/SortedSets.cs @@ -177,6 +177,95 @@ public async Task SortedSetPopMulti_Zero_Async() } } + [Fact] + public void SortedSetRandomMembers() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + var key0 = Me() + "non-existing"; + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key0, CommandFlags.FireAndForget); + db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); + + // single member + var randMember = db.SortedSetRandomMember(key); + Assert.True(Array.Exists(entries, element => element.Element.Equals(randMember))); + + // with count + var randMemberArray = db.SortedSetRandomMembers(key, 5); + Assert.Equal(5, randMemberArray.Length); + randMemberArray = db.SortedSetRandomMembers(key, 15); + Assert.Equal(10, randMemberArray.Length); + randMemberArray = db.SortedSetRandomMembers(key, -5); + Assert.Equal(5, randMemberArray.Length); + randMemberArray = db.SortedSetRandomMembers(key, -15); + Assert.Equal(15, randMemberArray.Length); + + // with scores + var randMemberArray2 = db.SortedSetRandomMembersWithScores(key, 2); + Assert.Equal(2, randMemberArray2.Length); + foreach (var member in randMemberArray2) + { + Assert.Contains(member, entries); + } + + // check missing key case + randMember = db.SortedSetRandomMember(key0); + Assert.True(randMember.IsNull); + randMemberArray = db.SortedSetRandomMembers(key0, 2); + Assert.True(randMemberArray.Length == 0); + randMemberArray2 = db.SortedSetRandomMembersWithScores(key0, 2); + Assert.True(randMemberArray2.Length == 0); + } + + [Fact] + public async Task SortedSetRandomMembersAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + var key0 = Me() + "non-existing"; + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key0, CommandFlags.FireAndForget); + db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); + + var randMember = await db.SortedSetRandomMemberAsync(key); + Assert.True(Array.Exists(entries, element => element.Element.Equals(randMember))); + + // with count + var randMemberArray = await db.SortedSetRandomMembersAsync(key, 5); + Assert.Equal(5, randMemberArray.Length); + randMemberArray = await db.SortedSetRandomMembersAsync(key, 15); + Assert.Equal(10, randMemberArray.Length); + randMemberArray = await db.SortedSetRandomMembersAsync(key, -5); + Assert.Equal(5, randMemberArray.Length); + randMemberArray = await db.SortedSetRandomMembersAsync(key, -15); + Assert.Equal(15, randMemberArray.Length); + + // with scores + var randMemberArray2 = await db.SortedSetRandomMembersWithScoresAsync(key, 2); + Assert.Equal(2, randMemberArray2.Length); + foreach (var member in randMemberArray2) + { + Assert.Contains(member, entries); + } + + // check missing key case + randMember = await db.SortedSetRandomMemberAsync(key0); + Assert.True(randMember.IsNull); + randMemberArray = await db.SortedSetRandomMembersAsync(key0, 2); + Assert.True(randMemberArray.Length == 0); + randMemberArray2 = await db.SortedSetRandomMembersWithScoresAsync(key0, 2); + Assert.True(randMemberArray2.Length == 0); + } + [Fact] public async Task SortedSetRangeStoreByRankAsync() { diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index 322f71a3f..1f293266d 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -731,6 +731,27 @@ public void SortedSetLengthByValueAsync() mock.Verify(_ => _.SortedSetLengthByValueAsync("prefix:key", "min", "max", Exclude.Start, CommandFlags.None)); } + [Fact] + public void SortedSetRandomMemberAsync() + { + wrapper.SortedSetRandomMemberAsync("key", CommandFlags.None); + mock.Verify(_ => _.SortedSetRandomMemberAsync("prefix:key", CommandFlags.None)); + } + + [Fact] + public void SortedSetRandomMembersAsync() + { + wrapper.SortedSetRandomMembersAsync("key", 2, CommandFlags.None); + mock.Verify(_ => _.SortedSetRandomMembersAsync("prefix:key", 2, CommandFlags.None)); + } + + [Fact] + public void SortedSetRandomMemberWithScoresAsync() + { + wrapper.SortedSetRandomMembersWithScoresAsync("key", 2, CommandFlags.None); + mock.Verify(_ => _.SortedSetRandomMembersWithScoresAsync("prefix:key", 2, CommandFlags.None)); + } + [Fact] public void SortedSetRangeByRankAsync() { From fffb5c15dda7d56aeb2401ecc0822c114c903d02 Mon Sep 17 00:00:00 2001 From: Steve Lorello <42971704+slorello89@users.noreply.github.com> Date: Tue, 12 Apr 2022 09:01:25 -0400 Subject: [PATCH 125/435] LPOS feature (#2080) Implements the [LPOS](https://redis.io/commands/lpos/) command for #2055 Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 1 + .../Interfaces/IDatabase.cs | 25 + .../Interfaces/IDatabaseAsync.cs | 25 + .../KeyspaceIsolation/DatabaseWrapper.cs | 6 + .../KeyspaceIsolation/WrapperBase.cs | 6 + src/StackExchange.Redis/Message.cs | 40 ++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 4 + src/StackExchange.Redis/RedisBase.cs | 3 +- src/StackExchange.Redis/RedisDatabase.cs | 29 + src/StackExchange.Redis/RedisFeatures.cs | 1 + src/StackExchange.Redis/RedisLiterals.cs | 2 + src/StackExchange.Redis/ResultProcessor.cs | 43 +- tests/StackExchange.Redis.Tests/Lists.cs | 522 ++++++++++++++++++ 14 files changed, 705 insertions(+), 3 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 38d9abf19..be940e0a8 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -12,6 +12,7 @@ - Adds: Support for `ZRANDMEMBER` with `.SortedSetRandomMember()`/`.SortedSetRandomMemberAsync()`, `.SortedSetRandomMembers()`/`.SortedSetRandomMembersAsync()`, and `.SortedSetRandomMembersWithScores()`/`.SortedSetRandomMembersWithScoresAsync()` ([#2076 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2076)) - Adds: Support for `SMISMEMBER` with `.SetContains()`/`.SetContainsAsync()` ([#2077 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2077)) - Adds: Support for `SINTERCARD` with `.SetIntersectionLength()`/`.SetIntersectionLengthAsync()` ([#2078 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2078)) +- Adds: Support for `LPOS` with `.ListPosition()`/`.ListPositionAsync()` and `.ListPositions()`/`.ListPositionsAsync()` ([#2080 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2080)) ## 2.5.61 diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index f0020cda4..2ee9fa480 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -85,6 +85,7 @@ internal enum RedisCommand LLEN, LMOVE, LPOP, + LPOS, LPUSH, LPUSHX, LRANGE, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 3eef153e4..8401d5abe 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -721,6 +721,31 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/lpop RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// + /// Scans through the list stored at looking for , returning the 0-based + /// index of the first matching element. + /// + /// The key of the list. + /// The element to search for. + /// The rank of the first element to return, within the sub-list of matching indexes in the case of multiple matches. + /// The maximum number of elements to scan through before stopping, defaults to 0 (a full scan of the list.) + /// The flags to use for this operation. + /// The 0-based index of the first matching element, or -1 if not found. + long ListPosition(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None); + + /// + /// Scans through the list stored at looking for instances of , returning the 0-based + /// indexes of any matching elements. + /// + /// The key of the list. + /// The element to search for. + /// The number of matches to find. A count of 0 will return the indexes of all occurrences of the element. + /// The rank of the first element to return, within the sub-list of matching indexes in the case of multiple matches. + /// The maximum number of elements to scan through before stopping, defaults to 0 (a full scan of the list.) + /// The flags to use for this operation. + /// An array of at most of indexes of matching elements. If none are found, and empty array is returned. + long[] ListPositions(RedisKey key, RedisValue element, long count, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None); + /// /// Insert the specified value at the head of the list stored at key. /// If key does not exist, it is created as empty list before performing the push operations. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 1ec8d7e30..43d3d041a 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -697,6 +697,31 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/lpop Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// + /// Scans through the list stored at looking for , returning the 0-based + /// index of the first matching element. + /// + /// The key of the list. + /// The element to search for. + /// The rank of the first element to return, within the sub-list of matching indexes in the case of multiple matches. + /// The maximum number of elements to scan through before stopping, defaults to 0 (a full scan of the list.) + /// The flags to use for this operation. + /// The 0-based index of the first matching element, or -1 if not found. + Task ListPositionAsync(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None); + + /// + /// Scans through the list stored at looking for instances of , returning the 0-based + /// indexes of any matching elements. + /// + /// The key of the list. + /// The element to search for. + /// The number of matches to find. A count of 0 will return the indexes of all occurrences of the element. + /// The rank of the first element to return, within the sub-list of matching indexes in the case of multiple matches. + /// The maximum number of elements to scan through before stopping, defaults to 0 (a full scan of the list.) + /// The flags to use for this operation. + /// An array of at most of indexes of matching elements. If none are found, and empty array is returned. + Task ListPositionsAsync(RedisKey key, RedisValue element, long count, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None); + /// /// Insert the specified value at the head of the list stored at key. /// If key does not exist, it is created as empty list before performing the push operations. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index c56209de1..f25ce3321 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -191,6 +191,12 @@ public RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.No public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => Inner.ListLeftPop(ToInner(key), count, flags); + public long ListPosition(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => + Inner.ListPosition(ToInner(key), element, rank, maxLength, flags); + + public long[] ListPositions(RedisKey key, RedisValue element, long count, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => + Inner.ListPositions(ToInner(key), element, count, rank, maxLength, flags); + public long ListLeftPush(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => Inner.ListLeftPush(ToInner(key), values, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 969637abd..bbcdb3e97 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -201,6 +201,12 @@ public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = Comm public Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => Inner.ListLeftPopAsync(ToInner(key), count, flags); + public Task ListPositionAsync(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => + Inner.ListPositionAsync(ToInner(key), element, rank, maxLength, flags); + + public Task ListPositionsAsync(RedisKey key, RedisValue element, long count, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => + Inner.ListPositionsAsync(ToInner(key), element, count, rank, maxLength, flags); + public Task ListLeftPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => Inner.ListLeftPushAsync(ToInner(key), values, flags); diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 901f0173a..f1760f27c 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -281,6 +281,9 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) => new CommandKeyValueValueValueValueValueMessage(db, flags, command, key, value0, value1, value2, value3, value4); + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4, in RedisValue value5, in RedisValue value6) => + new CommandKeyValueValueValueValueValueValueValueMessage(db, flags, command, key, value0, value1, value2, value3, value4, value5, value6); + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value0, in RedisValue value1) => new CommandValueValueMessage(db, flags, command, value0, value1); @@ -1164,6 +1167,43 @@ protected override void WriteImpl(PhysicalConnection physical) public override int ArgCount => 6; } + private sealed class CommandKeyValueValueValueValueValueValueValueMessage : CommandKeyBase + { + private readonly RedisValue value0, value1, value2, value3, value4, value5, value6; + + public CommandKeyValueValueValueValueValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4, in RedisValue value5, in RedisValue value6) : base(db, flags, command, key) + { + value0.AssertNotNull(); + value1.AssertNotNull(); + value2.AssertNotNull(); + value3.AssertNotNull(); + value4.AssertNotNull(); + value5.AssertNotNull(); + value6.AssertNotNull(); + this.value0 = value0; + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.value4 = value4; + this.value5 = value5; + this.value6 = value6; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, ArgCount); + physical.Write(Key); + physical.WriteBulkString(value0); + physical.WriteBulkString(value1); + physical.WriteBulkString(value2); + physical.WriteBulkString(value3); + physical.WriteBulkString(value4); + physical.WriteBulkString(value5); + physical.WriteBulkString(value6); + } + public override int ArgCount => 8; + } + private sealed class CommandKeyKeyValueValueMessage : CommandKeyBase { private readonly RedisValue value0, value1; diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index b31fa1a9e..9a3b7246f 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -547,6 +547,8 @@ StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, Sta StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags) -> long StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.ListLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.ListPosition(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, long rank = 1, long maxLength = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.ListPositions(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, long count, long rank = 1, long maxLength = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long[]! StackExchange.Redis.IDatabase.ListRange(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.ListRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, long count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.ListRightPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! @@ -742,6 +744,8 @@ StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKe StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListPositionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, long rank = 1, long maxLength = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListPositionsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue element, long count, long rank = 1, long maxLength = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListRangeAsync(StackExchange.Redis.RedisKey key, long start = 0, long stop = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, long count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListRightPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisBase.cs b/src/StackExchange.Redis/RedisBase.cs index 787f18032..0f0952607 100644 --- a/src/StackExchange.Redis/RedisBase.cs +++ b/src/StackExchange.Redis/RedisBase.cs @@ -40,14 +40,13 @@ public virtual Task PingAsync(CommandFlags flags = CommandFlags.None) public void WaitAll(params Task[] tasks) => multiplexer.WaitAll(tasks); - internal virtual Task ExecuteAsync(Message? message, ResultProcessor? processor, T defaultValue, ServerEndPoint? server = null) where T : class + internal virtual Task ExecuteAsync(Message? message, ResultProcessor? processor, T defaultValue, ServerEndPoint? server = null) { if (message is null) return CompletedTask.FromDefault(defaultValue, asyncState); multiplexer.CheckMessage(message); return multiplexer.ExecuteAsyncImpl(message, processor, asyncState, server, defaultValue); } - [return: NotNullIfNotNull("defualtValue")] internal virtual Task ExecuteAsync(Message? message, ResultProcessor? processor, ServerEndPoint? server = null) { if (message is null) return CompletedTask.Default(asyncState); diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index aaa647e19..3275864db 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -939,6 +939,18 @@ public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = C return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } + public long ListPosition(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) + { + var msg = CreateListPositionMessage(Database, flags, key, element, rank, maxLength); + return ExecuteSync(msg, ResultProcessor.Int64DefaultNegativeOne, defaultValue: -1); + } + + public long[] ListPositions(RedisKey key, RedisValue element, long count, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) + { + var msg = CreateListPositionMessage(Database, flags, key, element, rank, maxLength, count); + return ExecuteSync(msg, ResultProcessor.Int64Array, defaultValue: Array.Empty()); + } + public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.LPOP, key); @@ -951,6 +963,18 @@ public Task ListLeftPopAsync(RedisKey key, long count, CommandFlag return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } + public Task ListPositionAsync(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) + { + var msg = CreateListPositionMessage(Database, flags, key, element, rank, maxLength); + return ExecuteAsync(msg, ResultProcessor.Int64DefaultNegativeOne, defaultValue: -1); + } + + public Task ListPositionsAsync(RedisKey key, RedisValue element, long count, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) + { + var msg = CreateListPositionMessage(Database, flags, key, element, rank, maxLength, count); + return ExecuteAsync(msg, ResultProcessor.Int64Array, defaultValue: Array.Empty()); + } + public long ListLeftPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) { WhenAlwaysOrExists(when); @@ -4158,6 +4182,11 @@ protected override RedisValue[] Parse(in RawResult result, out int count) } } + private static Message CreateListPositionMessage(int db, CommandFlags flags, RedisKey key, RedisValue element, long rank, long maxLen, long? count = null) => + count != null + ? Message.Create(db, flags, RedisCommand.LPOS, key, element, RedisLiterals.RANK, rank, RedisLiterals.MAXLEN, maxLen, RedisLiterals.COUNT, count) + : Message.Create(db, flags, RedisCommand.LPOS, key, element, RedisLiterals.RANK, rank, RedisLiterals.MAXLEN, maxLen); + private static Message CreateSortedSetRangeStoreMessage( int db, CommandFlags flags, diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 171492855..180e1ee88 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -37,6 +37,7 @@ public readonly struct RedisFeatures v4_9_1 = new Version(4, 9, 1), // 5.0 RC1 is version 4.9.1; // 5.0 RC1 is version 4.9.1 v5_0_0 = new Version(5, 0, 0), v6_0_0 = new Version(6, 0, 0), + v6_0_6 = new Version(6, 0, 6), v6_2_0 = new Version(6, 2, 0), v7_0_0_rc1 = new Version(6, 9, 240); // 7.0 RC1 is version 6.9.240 diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 221e16c00..2afbd443c 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -75,6 +75,7 @@ public static readonly RedisValue MATCH = "MATCH", MALLOC_STATS = "MALLOC-STATS", MAX = "MAX", + MAXLEN = "MAXLEN", MIN = "MIN", NODES = "NODES", NOSAVE = "NOSAVE", @@ -90,6 +91,7 @@ public static readonly RedisValue PURGE = "PURGE", PX = "PX", PXAT = "PXAT", + RANK = "RANK", RIGHT = "RIGHT", REPLACE = "REPLACE", RESET = "RESET", diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 583595377..9db002a0c 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -51,7 +51,8 @@ public static readonly MultiStreamProcessor public static readonly ResultProcessor Int64 = new Int64Processor(), - PubSubNumSub = new PubSubNumSubProcessor(); + PubSubNumSub = new PubSubNumSubProcessor(), + Int64DefaultNegativeOne = new Int64DefaultValueProcessor(-1); public static readonly ResultProcessor NullableDouble = new NullableDoubleProcessor(); @@ -79,6 +80,9 @@ public static readonly ResultProcessor> public static readonly ResultProcessor RedisValueArray = new RedisValueArrayProcessor(); + public static readonly ResultProcessor + Int64Array = new Int64ArrayProcessor(); + public static readonly ResultProcessor StringArray = new StringArrayProcessor(); @@ -1048,6 +1052,28 @@ private static string Normalize(string? category) => category.IsNullOrWhiteSpace() ? "miscellaneous" : category.Trim(); } + private class Int64DefaultValueProcessor : ResultProcessor + { + private readonly long _defaultValue; + + public Int64DefaultValueProcessor(long defaultValue) => _defaultValue = defaultValue; + + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.IsNull) + { + SetResult(message, _defaultValue); + return true; + } + if (result.Type == ResultType.Integer && result.TryGetInt64(out var i64)) + { + SetResult(message, i64); + return true; + } + return false; + } + } + private class Int64Processor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) @@ -1245,6 +1271,21 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + private sealed class Int64ArrayProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Type == ResultType.MultiBulk && !result.IsNull) + { + var arr = result.ToArray((in RawResult x) => (long)x.AsRedisValue())!; + SetResult(message, arr); + return true; + } + + return false; + } + } + private sealed class StringArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) diff --git a/tests/StackExchange.Redis.Tests/Lists.cs b/tests/StackExchange.Redis.Tests/Lists.cs index a3daeb9a5..4f2b9c481 100644 --- a/tests/StackExchange.Redis.Tests/Lists.cs +++ b/tests/StackExchange.Redis.Tests/Lists.cs @@ -352,5 +352,527 @@ public void ListMoveKeyDoesNotExist() var rangeResult1 = db.ListMove(src, dest, ListSide.Left, ListSide.Right); Assert.True(rangeResult1.IsNull); } + + [Fact] + public void ListPositionHappyPath() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var val = "foo"; + db.KeyDelete(key); + + db.ListLeftPush(key, val); + var res = db.ListPosition(key, val); + + Assert.Equal(0, res); + } + + [Fact] + public void ListPositionEmpty() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var val = "foo"; + db.KeyDelete(key); + + var res = db.ListPosition(key, val); + + Assert.Equal(-1, res); + } + + [Fact] + public void ListPositionsHappyPath() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + db.KeyDelete(key); + + for (var i = 0; i < 10; i++) + { + db.ListLeftPush(key, foo); + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } + + var res = db.ListPositions(key, foo, 5); + + foreach (var item in res) + { + Assert.Equal(2, item % 3); + } + + Assert.Equal(5,res.Count()); + } + + [Fact] + public void ListPositionsTooFew() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + db.KeyDelete(key); + + for (var i = 0; i < 10; i++) + { + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } + + db.ListLeftPush(key, foo); + + var res = db.ListPositions(key, foo, 5); + Assert.Single(res); + Assert.Equal(0, res.Single()); + } + + [Fact] + public void ListPositionsAll() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + db.KeyDelete(key); + + for (var i = 0; i < 10; i++) + { + db.ListLeftPush(key, foo); + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } + + var res = db.ListPositions(key, foo, 0); + + foreach (var item in res) + { + Assert.Equal(2, item % 3); + } + + Assert.Equal(10,res.Count()); + } + + [Fact] + public void ListPositionsAllLimitLength() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + db.KeyDelete(key); + + for (var i = 0; i < 10; i++) + { + db.ListLeftPush(key, foo); + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } + + var res = db.ListPositions(key, foo, 0, maxLength: 15); + + foreach (var item in res) + { + Assert.Equal(2, item % 3); + } + + Assert.Equal(5,res.Count()); + } + + [Fact] + public void ListPositionsEmpty() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + db.KeyDelete(key); + + for (var i = 0; i < 10; i++) + { + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } + + var res = db.ListPositions(key, foo, 5); + + Assert.Empty(res); + } + + [Fact] + public void ListPositionByRank() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + db.KeyDelete(key); + + for (var i = 0; i < 10; i++) + { + db.ListLeftPush(key, foo); + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } + + var rank = 6; + + var res = db.ListPosition(key, foo, rank: rank); + + Assert.Equal(3*rank-1, res); + } + + [Fact] + public void ListPositionLimitSoNull() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + db.KeyDelete(key); + + for (var i = 0; i < 10; i++) + { + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } + + db.ListRightPush(key, foo); + + var res = db.ListPosition(key, foo, maxLength: 20); + + Assert.Equal(-1, res); + } + + [Fact] + public async Task ListPositionHappyPathAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var val = "foo"; + await db.KeyDeleteAsync(key); + + await db.ListLeftPushAsync(key, val); + var res = await db.ListPositionAsync(key, val); + + Assert.Equal(0, res); + } + + [Fact] + public async Task ListPositionEmptyAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var val = "foo"; + await db.KeyDeleteAsync(key); + + var res = await db.ListPositionAsync(key, val); + + Assert.Equal(-1, res); + } + + [Fact] + public async Task ListPositionsHappyPathAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + await db.KeyDeleteAsync(key); + + for (var i = 0; i < 10; i++) + { + await db.ListLeftPushAsync(key, foo); + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); + } + + var res = await db.ListPositionsAsync(key, foo, 5); + + foreach (var item in res) + { + Assert.Equal(2, item % 3); + } + + Assert.Equal(5,res.Count()); + } + + [Fact] + public async Task ListPositionsTooFewAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + await db.KeyDeleteAsync(key); + + for (var i = 0; i < 10; i++) + { + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); + } + + db.ListLeftPush(key, foo); + + var res = await db.ListPositionsAsync(key, foo, 5); + Assert.Single(res); + Assert.Equal(0, res.Single()); + } + + [Fact] + public async Task ListPositionsAllAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + await db.KeyDeleteAsync(key); + + for (var i = 0; i < 10; i++) + { + await db.ListLeftPushAsync(key, foo); + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); + } + + var res = await db.ListPositionsAsync(key, foo, 0); + + foreach (var item in res) + { + Assert.Equal(2, item % 3); + } + + Assert.Equal(10,res.Count()); + } + + [Fact] + public async Task ListPositionsAllLimitLengthAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + await db.KeyDeleteAsync(key); + + for (var i = 0; i < 10; i++) + { + await db.ListLeftPushAsync(key, foo); + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); + } + + var res = await db.ListPositionsAsync(key, foo, 0, maxLength: 15); + + foreach (var item in res) + { + Assert.Equal(2, item % 3); + } + + Assert.Equal(5,res.Count()); + } + + [Fact] + public async Task ListPositionsEmptyAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + await db.KeyDeleteAsync(key); + + for (var i = 0; i < 10; i++) + { + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); + } + + var res = await db.ListPositionsAsync(key, foo, 5); + + Assert.Empty(res); + } + + [Fact] + public async Task ListPositionByRankAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + await db.KeyDeleteAsync(key); + + for (var i = 0; i < 10; i++) + { + await db.ListLeftPushAsync(key, foo); + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); + } + + var rank = 6; + + var res = await db.ListPositionAsync(key, foo, rank: rank); + + Assert.Equal(3 * rank - 1, res); + } + + [Fact] + public async Task ListPositionLimitSoNullAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + await db.KeyDeleteAsync(key); + + for (var i = 0; i < 10; i++) + { + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); + } + + await db.ListRightPushAsync(key, foo); + + var res = await db.ListPositionAsync(key, foo, maxLength: 20); + + Assert.Equal(-1, res); + } + + [Fact] + public async Task ListPositionFireAndForgetAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + await db.KeyDeleteAsync(key); + + for (var i = 0; i < 10; i++) + { + await db.ListLeftPushAsync(key, foo); + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); + } + + await db.ListRightPushAsync(key, foo); + + var res = await db.ListPositionAsync(key, foo, maxLength: 20, flags: CommandFlags.FireAndForget); + + Assert.Equal(-1, res); + } + + [Fact] + public void ListPositionFireAndForget() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + var foo = "foo"; + var bar = "bar"; + var baz = "baz"; + + db.KeyDelete(key); + + for (var i = 0; i < 10; i++) + { + db.ListLeftPush(key, foo); + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } + + db.ListRightPush(key, foo); + + var res = db.ListPosition(key, foo, maxLength: 20, flags: CommandFlags.FireAndForget); + + Assert.Equal(-1, res); + } } } From e18b2c1637ea3e075c2da69db9396f17f52328ab Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Tue, 12 Apr 2022 20:51:47 +0300 Subject: [PATCH 126/435] Sorted set new commands: ZDIFF, ZDIFFSTORE, ZINTER, ZINTERCARD, and ZUNION (#2075) Adds support for ZDIFF, ZDIFFSTORE, ZINTER, ZINTERCARD, ZUNION (#2055) Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 5 + src/StackExchange.Redis/Enums/SetOperation.cs | 18 +- .../Interfaces/IDatabase.cs | 46 +++ .../Interfaces/IDatabaseAsync.cs | 46 +++ .../KeyspaceIsolation/DatabaseWrapper.cs | 9 + .../KeyspaceIsolation/WrapperBase.cs | 9 + src/StackExchange.Redis/PublicAPI.Shipped.txt | 6 + src/StackExchange.Redis/RedisDatabase.cs | 142 ++++++++- .../DatabaseWrapperTests.cs | 24 ++ tests/StackExchange.Redis.Tests/SortedSets.cs | 287 ++++++++++++++++++ .../WrapperBaseTests.cs | 24 ++ 12 files changed, 600 insertions(+), 17 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index be940e0a8..4c2c40658 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -11,6 +11,7 @@ - Adds: Support for `LMOVE` with `.ListMove()`/`.ListMoveAsync()` ([#2065 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2065)) - Adds: Support for `ZRANDMEMBER` with `.SortedSetRandomMember()`/`.SortedSetRandomMemberAsync()`, `.SortedSetRandomMembers()`/`.SortedSetRandomMembersAsync()`, and `.SortedSetRandomMembersWithScores()`/`.SortedSetRandomMembersWithScoresAsync()` ([#2076 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2076)) - Adds: Support for `SMISMEMBER` with `.SetContains()`/`.SetContainsAsync()` ([#2077 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2077)) +- Adds: Support for `ZDIFF`, `ZDIFFSTORE`, `ZINTER`, `ZINTERCARD`, and `ZUNION` with `.SortedSetCombine()`/`.SortedSetCombineAsync()`, `.SortedSetCombineWithScores()`/`.SortedSetCombineWithScoresAsync()`, and `.SortedSetIntersectionLength()`/`.SortedSetIntersectionLengthAsync()` ([#2075 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2075)) - Adds: Support for `SINTERCARD` with `.SetIntersectionLength()`/`.SetIntersectionLengthAsync()` ([#2078 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2078)) - Adds: Support for `LPOS` with `.ListPosition()`/`.ListPositionAsync()` and `.ListPositions()`/`.ListPositionsAsync()` ([#2080 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2080)) diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 2ee9fa480..f40d3ff49 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -197,7 +197,11 @@ internal enum RedisCommand ZADD, ZCARD, ZCOUNT, + ZDIFF, + ZDIFFSTORE, ZINCRBY, + ZINTER, + ZINTERCARD, ZINTERSTORE, ZLEXCOUNT, ZPOPMAX, @@ -218,6 +222,7 @@ internal enum RedisCommand ZREVRANK, ZSCAN, ZSCORE, + ZUNION, ZUNIONSTORE, UNKNOWN, diff --git a/src/StackExchange.Redis/Enums/SetOperation.cs b/src/StackExchange.Redis/Enums/SetOperation.cs index b88b0c8f0..f7cf4f1df 100644 --- a/src/StackExchange.Redis/Enums/SetOperation.cs +++ b/src/StackExchange.Redis/Enums/SetOperation.cs @@ -1,4 +1,6 @@ -namespace StackExchange.Redis +using System; + +namespace StackExchange.Redis { /// /// Describes an algebraic set operation that can be performed to combine multiple sets. @@ -18,4 +20,18 @@ public enum SetOperation /// Difference, } + + internal static class SetOperationExtensions + { + public static RedisCommand ToCommand(this SetOperation operation, bool store) => operation switch + { + SetOperation.Intersect when store => RedisCommand.ZINTERSTORE, + SetOperation.Intersect => RedisCommand.ZINTER, + SetOperation.Union when store => RedisCommand.ZUNIONSTORE, + SetOperation.Union => RedisCommand.ZUNION, + SetOperation.Difference when store => RedisCommand.ZDIFFSTORE, + SetOperation.Difference => RedisCommand.ZDIFF, + _ => throw new ArgumentOutOfRangeException(nameof(operation)), + }; + } } diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 8401d5abe..ad5595800 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1380,9 +1380,42 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/zadd long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); + /// + /// Computes a set operation for multiple sorted sets (optionally using per-set ), + /// optionally performing a specific aggregation (defaults to ). + /// cannot be used with weights or aggregation. + /// + /// The operation to perform. + /// The keys of the sorted sets. + /// The optional weights per set that correspond to . + /// The aggregation method (defaults to ). + /// The flags to use for this operation. + /// https://redis.io/commands/zunion + /// https://redis.io/commands/zinter + /// https://redis.io/commands/zdiff + /// The resulting sorted set. + RedisValue[] SortedSetCombine(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + + /// + /// Computes a set operation for multiple sorted sets (optionally using per-set ), + /// optionally performing a specific aggregation (defaults to ). + /// cannot be used with weights or aggregation. + /// + /// The operation to perform. + /// The keys of the sorted sets. + /// The optional weights per set that correspond to . + /// The aggregation method (defaults to ). + /// The flags to use for this operation. + /// https://redis.io/commands/zunion + /// https://redis.io/commands/zinter + /// https://redis.io/commands/zdiff + /// The resulting sorted set with scores. + SortedSetEntry[] SortedSetCombineWithScores(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + /// /// Computes a set operation over two sorted sets, and stores the result in destination, optionally performing /// a specific aggregation (defaults to sum). + /// cannot be used with aggregation. /// /// The operation to perform. /// The key to store the results in. @@ -1392,12 +1425,14 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// https://redis.io/commands/zunionstore /// https://redis.io/commands/zinterstore + /// https://redis.io/commands/zdiffstore /// The number of elements in the resulting sorted set at destination. long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// /// Computes a set operation over multiple sorted sets (optionally using per-set weights), and stores the result in destination, optionally performing /// a specific aggregation (defaults to sum). + /// cannot be used with aggregation. /// /// The operation to perform. /// The key to store the results in. @@ -1407,6 +1442,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// https://redis.io/commands/zunionstore /// https://redis.io/commands/zinterstore + /// https://redis.io/commands/zdiffstore /// The number of elements in the resulting sorted set at destination. long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); @@ -1433,6 +1469,16 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/zincrby double SortedSetIncrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None); + /// + /// Returns the cardinality of the intersection of the sorted sets at . + /// + /// The keys of the sorted sets. + /// If the intersection cardinality reaches partway through the computation, the algorithm will exit and yield as the cardinality (defaults to 0 meaning unlimited). + /// The flags to use for this operation. + /// The number of elements in the resulting intersection. + /// https://redis.io/commands/zintercard + long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None); + /// /// Returns the sorted set cardinality (number of elements) of the sorted set stored at key. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 43d3d041a..29bdd2d52 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1344,9 +1344,42 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/zadd Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); + /// + /// Computes a set operation for multiple sorted sets (optionally using per-set ), + /// optionally performing a specific aggregation (defaults to ). + /// cannot be used with weights or aggregation. + /// + /// The operation to perform. + /// The keys of the sorted sets. + /// The optional weights per set that correspond to . + /// The aggregation method (defaults to ). + /// The flags to use for this operation. + /// https://redis.io/commands/zunion + /// https://redis.io/commands/zinter + /// https://redis.io/commands/zdiff + /// The resulting sorted set. + Task SortedSetCombineAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + + /// + /// Computes a set operation for multiple sorted sets (optionally using per-set ), + /// optionally performing a specific aggregation (defaults to ). + /// cannot be used with weights or aggregation. + /// + /// The operation to perform. + /// The keys of the sorted sets. + /// The optional weights per set that correspond to . + /// The aggregation method (defaults to ). + /// The flags to use for this operation. + /// https://redis.io/commands/zunion + /// https://redis.io/commands/zinter + /// https://redis.io/commands/zdiff + /// The resulting sorted set with scores. + Task SortedSetCombineWithScoresAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + /// /// Computes a set operation over two sorted sets, and stores the result in destination, optionally performing /// a specific aggregation (defaults to sum). + /// cannot be used with aggregation. /// /// The operation to perform. /// The key to store the results in. @@ -1356,12 +1389,14 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// https://redis.io/commands/zunionstore /// https://redis.io/commands/zinterstore + /// https://redis.io/commands/zdiffstore /// The number of elements in the resulting sorted set at destination. Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// /// Computes a set operation over multiple sorted sets (optionally using per-set weights), and stores the result in destination, optionally performing /// a specific aggregation (defaults to sum). + /// cannot be used with aggregation. /// /// The operation to perform. /// The key to store the results in. @@ -1371,6 +1406,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// https://redis.io/commands/zunionstore /// https://redis.io/commands/zinterstore + /// https://redis.io/commands/zdiffstore /// The number of elements in the resulting sorted set at destination. Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); @@ -1397,6 +1433,16 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/zincrby Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None); + /// + /// Returns the cardinality of the intersection of the sorted sets at . + /// + /// The keys of the sorted sets. + /// If the intersection cardinality reaches partway through the computation, the algorithm will exit and yield as the cardinality (defaults to 0 meaning unlimited). + /// The flags to use for this operation. + /// The number of elements in the resulting intersection. + /// https://redis.io/commands/zintercard + Task SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None); + /// /// Returns the sorted set cardinality (number of elements) of the sorted set stored at key. /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index f25ce3321..69c9b5cf7 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -351,6 +351,12 @@ public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandF public bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetAdd(ToInner(key), member, score, when, flags); + public RedisValue[] SortedSetCombine(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetCombine(operation, keys, weights, aggregate, flags); + + public SortedSetEntry[] SortedSetCombineWithScores(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetCombineWithScores(operation, keys, weights, aggregate, flags); + public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => Inner.SortedSetCombineAndStore(operation, ToInner(destination), ToInner(keys), weights, aggregate, flags); @@ -363,6 +369,9 @@ public double SortedSetDecrement(RedisKey key, RedisValue member, double value, public double SortedSetIncrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => Inner.SortedSetIncrement(ToInner(key), member, value, flags); + public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetIntersectionLength(keys, limit, flags); + public long SortedSetLength(RedisKey key, double min = -1.0 / 0.0, double max = 1.0 / 0.0, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => Inner.SortedSetLength(ToInner(key), min, max, exclude, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index bbcdb3e97..a5999039e 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -364,6 +364,12 @@ public Task SortedSetAddAsync(RedisKey key, RedisValue member, double scor public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetAddAsync(ToInner(key), member, score, when, flags); + public Task SortedSetCombineAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetCombineAsync(operation, keys, weights, aggregate, flags); + + public Task SortedSetCombineWithScoresAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetCombineWithScoresAsync(operation, keys, weights, aggregate, flags); + public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => Inner.SortedSetCombineAndStoreAsync(operation, ToInner(destination), ToInner(keys), weights, aggregate, flags); @@ -376,6 +382,9 @@ public Task SortedSetDecrementAsync(RedisKey key, RedisValue member, dou public Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None) => Inner.SortedSetIncrementAsync(ToInner(key), member, value, flags); + public Task SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetIntersectionLengthAsync(keys, limit, flags); + public Task SortedSetLengthAsync(RedisKey key, double min = -1.0 / 0.0, double max = 1.0 / 0.0, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => Inner.SortedSetLengthAsync(ToInner(key), min, max, exclude, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 9a3b7246f..6ddf9d892 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -594,10 +594,13 @@ StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, Sta StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.CommandFlags flags) -> long StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetCombine(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.IDatabase.SortedSetCombineWithScores(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! StackExchange.Redis.IDatabase.SortedSetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetDecrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double StackExchange.Redis.IDatabase.SortedSetIncrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double +StackExchange.Redis.IDatabase.SortedSetIntersectionLength(StackExchange.Redis.RedisKey[]! keys, long limit = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetLength(StackExchange.Redis.RedisKey key, double min = -Infinity, double max = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetLengthByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! @@ -790,10 +793,13 @@ StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKe StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetCombineWithScoresAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetDecrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetIncrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetIntersectionLengthAsync(StackExchange.Redis.RedisKey[]! keys, long limit = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetLengthAsync(StackExchange.Redis.RedisKey key, double min = -Infinity, double max = Infinity, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetLengthByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 3275864db..03707579a 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1640,6 +1640,30 @@ public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When return ExecuteAsync(msg, ResultProcessor.Int64); } + public RedisValue[] SortedSetCombine(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetCombineCommandMessage(operation, keys, weights, aggregate, withScores: false, flags); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public Task SortedSetCombineAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetCombineCommandMessage(operation, keys, weights, aggregate, withScores: false, flags); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public SortedSetEntry[] SortedSetCombineWithScores(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetCombineCommandMessage(operation, keys, weights, aggregate, withScores: true, flags); + return ExecuteSync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); + } + + public Task SortedSetCombineWithScoresAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetCombineCommandMessage(operation, keys, weights, aggregate, withScores: true, flags); + return ExecuteAsync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); + } + public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) { var msg = GetSortedSetCombineAndStoreCommandMessage(operation, destination, new[] { first, second }, null, aggregate, flags); @@ -1686,6 +1710,18 @@ public Task SortedSetIncrementAsync(RedisKey key, RedisValue member, dou return ExecuteAsync(msg, ResultProcessor.Double); } + public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetIntersectionLengthMessage(keys, limit, flags); + return ExecuteSync(msg, ResultProcessor.Int64); + } + + public Task SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetIntersectionLengthMessage(keys, limit, flags); + return ExecuteAsync(msg, ResultProcessor.Int64); + } + public long SortedSetLength(RedisKey key, double min = double.NegativeInfinity, double max = double.PositiveInfinity, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) { var msg = GetSortedSetLengthMessage(key, min, max, exclude, flags); @@ -3228,36 +3264,91 @@ private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long private Message GetSortedSetCombineAndStoreCommandMessage(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights, Aggregate aggregate, CommandFlags flags) { - var command = operation switch + var command = operation.ToCommand(store: true); + if (keys == null) { - SetOperation.Intersect => RedisCommand.ZINTERSTORE, - SetOperation.Union => RedisCommand.ZUNIONSTORE, - _ => throw new ArgumentOutOfRangeException(nameof(operation)), - }; - if (keys == null) throw new ArgumentNullException(nameof(keys)); + throw new ArgumentNullException(nameof(keys)); + } + if (command == RedisCommand.ZDIFFSTORE && (weights != null || aggregate != Aggregate.Sum)) + { + throw new ArgumentException("ZDIFFSTORE cannot be used with weights or aggregation."); + } + if (weights != null && keys.Length != weights.Length) + { + throw new ArgumentException("Keys and weights should have the same number of elements.", nameof(weights)); + } - List? values = null; - if (weights != null && weights.Length != 0) + RedisValue[] values = RedisValue.EmptyArray; + + var argsLength = (weights?.Length > 0 ? 1 + weights.Length : 0) + (aggregate != Aggregate.Sum ? 2 : 0); + if (argsLength > 0) { - (values ??= new List()).Add(RedisLiterals.WEIGHTS); + values = new RedisValue[argsLength]; + AddWeightsAggregationAndScore(values, weights, aggregate); + } + return new SortedSetCombineAndStoreCommandMessage(Database, flags, command, destination, keys, values); + } + + private Message GetSortedSetCombineCommandMessage(SetOperation operation, RedisKey[] keys, double[]? weights, Aggregate aggregate, bool withScores, CommandFlags flags) + { + var command = operation.ToCommand(store: false); + if (keys == null) + { + throw new ArgumentNullException(nameof(keys)); + } + if (command == RedisCommand.ZDIFF && (weights != null || aggregate != Aggregate.Sum)) + { + throw new ArgumentException("ZDIFF cannot be used with weights or aggregation."); + } + if (weights != null && keys.Length != weights.Length) + { + throw new ArgumentException("Keys and weights should have the same number of elements.", nameof(weights)); + } + + var i = 0; + var values = new RedisValue[1 + keys.Length + + (weights?.Length > 0 ? 1 + weights.Length : 0) + + (aggregate != Aggregate.Sum ? 2 : 0) + + (withScores ? 1 : 0)]; + values[i++] = keys.Length; + foreach (var key in keys) + { + values[i++] = key.AsRedisValue(); + } + AddWeightsAggregationAndScore(values.AsSpan(i), weights, aggregate, withScores: withScores); + return Message.Create(Database, flags, command, values ?? RedisValue.EmptyArray); + } + + private void AddWeightsAggregationAndScore(Span values, double[]? weights, Aggregate aggregate, bool withScores = false) + { + int i = 0; + if (weights?.Length > 0) + { + values[i++] = RedisLiterals.WEIGHTS; foreach (var weight in weights) - values.Add(weight); + { + values[i++] = weight; + } } switch (aggregate) { - case Aggregate.Sum: break; // default + case Aggregate.Sum: + break; // add nothing - Redis default case Aggregate.Min: - (values ??= new List()).Add(RedisLiterals.AGGREGATE); - values.Add(RedisLiterals.MIN); + values[i++] = RedisLiterals.AGGREGATE; + values[i++] = RedisLiterals.MIN; break; case Aggregate.Max: - (values ??= new List()).Add(RedisLiterals.AGGREGATE); - values.Add(RedisLiterals.MAX); + values[i++] = RedisLiterals.AGGREGATE; + values[i++] = RedisLiterals.MAX; break; default: throw new ArgumentOutOfRangeException(nameof(aggregate)); } - return new SortedSetCombineAndStoreCommandMessage(Database, flags, command, destination, keys, values?.ToArray() ?? RedisValue.EmptyArray); + if (withScores) + { + values[i++] = RedisLiterals.WITHSCORES; + } } private Message GetSortedSetLengthMessage(RedisKey key, double min, double max, Exclude exclude, CommandFlags flags) @@ -3270,6 +3361,25 @@ private Message GetSortedSetLengthMessage(RedisKey key, double min, double max, return Message.Create(Database, flags, RedisCommand.ZCOUNT, key, from, to); } + private Message GetSortedSetIntersectionLengthMessage(RedisKey[] keys, long limit, CommandFlags flags) + { + if (keys == null) throw new ArgumentNullException(nameof(keys)); + + var i = 0; + var values = new RedisValue[1 + keys.Length + (limit > 0 ? 2 : 0)]; + values[i++] = keys.Length; + foreach (var key in keys) + { + values[i++] = key.AsRedisValue(); + } + if (limit > 0) + { + values[i++] = RedisLiterals.LIMIT; + values[i++] = limit; + } + return Message.Create(Database, flags, RedisCommand.ZINTERCARD, values); + } + private Message GetSortedSetRangeByScoreMessage(RedisKey key, double start, double stop, Exclude exclude, Order order, long skip, long take, CommandFlags flags, bool withScores) { // usage: {ZRANGEBYSCORE|ZREVRANGEBYSCORE} key from to [WITHSCORES] [LIMIT offset count] diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 5b2018858..fa3a88851 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -741,6 +741,22 @@ public void SortedSetAdd_2() mock.Verify(_ => _.SortedSetAdd("prefix:key", values, When.Exists, CommandFlags.None)); } + [Fact] + public void SortedSetCombine() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetCombine(SetOperation.Intersect, keys); + mock.Verify(_ => _.SortedSetCombine(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); + } + + [Fact] + public void SortedSetCombineWithScores() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetCombineWithScores(SetOperation.Intersect, keys); + mock.Verify(_ => _.SortedSetCombineWithScores(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); + } + [Fact] public void SortedSetCombineAndStore_1() { @@ -771,6 +787,14 @@ public void SortedSetIncrement() mock.Verify(_ => _.SortedSetIncrement("prefix:key", "member", 1.23, CommandFlags.None)); } + [Fact] + public void SortedSetIntersectionLength() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetIntersectionLength(keys, 1, CommandFlags.None); + mock.Verify(_ => _.SortedSetIntersectionLength(keys, 1, CommandFlags.None)); + } + [Fact] public void SortedSetLength() { diff --git a/tests/StackExchange.Redis.Tests/SortedSets.cs b/tests/StackExchange.Redis.Tests/SortedSets.cs index f15779d78..c20464240 100644 --- a/tests/StackExchange.Redis.Tests/SortedSets.cs +++ b/tests/StackExchange.Redis.Tests/SortedSets.cs @@ -38,6 +38,15 @@ public SortedSets(ITestOutputHelper output, SharedConnectionFixture fixture) : b new SortedSetEntry("j", 512) }; + private static readonly SortedSetEntry[] entriesPow3 = new SortedSetEntry[] + { + new SortedSetEntry("a", 1), + new SortedSetEntry("c", 4), + new SortedSetEntry("e", 16), + new SortedSetEntry("g", 64), + new SortedSetEntry("i", 256), + }; + private static readonly SortedSetEntry[] lexEntries = new SortedSetEntry[] { new SortedSetEntry("a", 0), @@ -52,6 +61,284 @@ public SortedSets(ITestOutputHelper output, SharedConnectionFixture fixture) : b new SortedSetEntry("j", 0) }; + [Fact] + public void SortedSetCombine() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var diff = db.SortedSetCombine(SetOperation.Difference, new RedisKey[]{ key1, key2}); + Assert.Equal(5, diff.Length); + Assert.Equal("b", diff[0]); + + var inter = db.SortedSetCombine(SetOperation.Intersect, new RedisKey[]{ key1, key2}); + Assert.Equal(5, inter.Length); + Assert.Equal("a", inter[0]); + + var union = db.SortedSetCombine(SetOperation.Union, new RedisKey[]{ key1, key2}); + Assert.Equal(10, union.Length); + Assert.Equal("a", union[0]); + } + + + [Fact] + public async Task SortedSetCombineAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var diff = await db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }); + Assert.Equal(5, diff.Length); + Assert.Equal("b", diff[0]); + + var inter = await db.SortedSetCombineAsync(SetOperation.Intersect, new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter.Length); + Assert.Equal("a", inter[0]); + + var union = await db.SortedSetCombineAsync(SetOperation.Union, new RedisKey[] { key1, key2 }); + Assert.Equal(10, union.Length); + Assert.Equal("a", union[0]); + } + + [Fact] + public void SortedSetCombineWithScores() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var diff = db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[]{ key1, key2}); + Assert.Equal(5, diff.Length); + Assert.Equal(new SortedSetEntry("b", 2), diff[0]); + + var inter = db.SortedSetCombineWithScores(SetOperation.Intersect, new RedisKey[]{ key1, key2}); + Assert.Equal(5, inter.Length); + Assert.Equal(new SortedSetEntry("a", 2), inter[0]); + + var union = db.SortedSetCombineWithScores(SetOperation.Union, new RedisKey[]{ key1, key2}); + Assert.Equal(10, union.Length); + Assert.Equal(new SortedSetEntry("a", 2), union[0]); + } + + + [Fact] + public async Task SortedSetCombineWithScoresAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var diff = await db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }); + Assert.Equal(5, diff.Length); + Assert.Equal(new SortedSetEntry("b", 2), diff[0]); + + var inter = await db.SortedSetCombineWithScoresAsync(SetOperation.Intersect, new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter.Length); + Assert.Equal(new SortedSetEntry("a", 2), inter[0]); + + var union = await db.SortedSetCombineWithScoresAsync(SetOperation.Union, new RedisKey[] { key1, key2 }); + Assert.Equal(10, union.Length); + Assert.Equal(new SortedSetEntry("a", 2), union[0]); + } + + [Fact] + public void SortedSetCombineAndStore() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + var destination = Me() + "dest"; + db.KeyDelete(destination, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var diff = db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[]{ key1, key2}); + Assert.Equal(5, diff); + + var inter = db.SortedSetCombineAndStore(SetOperation.Intersect, destination, new RedisKey[]{ key1, key2}); + Assert.Equal(5, inter); + + var union = db.SortedSetCombineAndStore(SetOperation.Union, destination, new RedisKey[]{ key1, key2}); + Assert.Equal(10, union); + } + + + [Fact] + public async Task SortedSetCombineAndStoreAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + var destination = Me() + "dest"; + db.KeyDelete(destination, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var diff = await db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }); + Assert.Equal(5, diff); + + var inter = await db.SortedSetCombineAndStoreAsync(SetOperation.Intersect, destination, new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter); + + var union = await db.SortedSetCombineAndStoreAsync(SetOperation.Union, destination, new RedisKey[] { key1, key2 }); + Assert.Equal(10, union); + } + + [Fact] + public async Task SortedSetCombineErrors() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + var destination = Me() + "dest"; + db.KeyDelete(destination, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + // ZDIFF can't be used with weights + var ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); + // and Async... + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); + + // ZDIFF can't be used with aggregation + ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); + // and Async... + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); + + // Too many weights + ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Union, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + // and Async... + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Union, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + } + + [Fact] + public void SortedSetIntersectionLength() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var inter = db.SortedSetIntersectionLength(new RedisKey[]{ key1, key2}); + Assert.Equal(5, inter); + + // with limit + inter = db.SortedSetIntersectionLength(new RedisKey[]{ key1, key2}, 3); + Assert.Equal(3, inter); + } + + [Fact] + public async Task SortedSetIntersectionLengthAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + var inter = await db.SortedSetIntersectionLengthAsync(new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter); + + // with limit + inter = await db.SortedSetIntersectionLengthAsync(new RedisKey[] { key1, key2 }, 3); + Assert.Equal(3, inter); + } + [Fact] public void SortedSetPopMulti_Multi() { diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index 1f293266d..dfb6968ed 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -687,6 +687,22 @@ public void SortedSetAddAsync_2() mock.Verify(_ => _.SortedSetAddAsync("prefix:key", values, When.Exists, CommandFlags.None)); } + [Fact] + public void SortedSetCombineAsync() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetCombineAsync(SetOperation.Intersect, keys); + mock.Verify(_ => _.SortedSetCombineAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); + } + + [Fact] + public void SortedSetCombineWithScoresAsync() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys); + mock.Verify(_ => _.SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); + } + [Fact] public void SortedSetCombineAndStoreAsync_1() { @@ -717,6 +733,14 @@ public void SortedSetIncrementAsync() mock.Verify(_ => _.SortedSetIncrementAsync("prefix:key", "member", 1.23, CommandFlags.None)); } + [Fact] + public void SortedSetIntersectionLengthAsync() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None); + mock.Verify(_ => _.SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None)); + } + [Fact] public void SortedSetLengthAsync() { From 9047c5b86212caa7aa89ce2809db12c4c759c3c3 Mon Sep 17 00:00:00 2001 From: nielsderdaele Date: Wed, 13 Apr 2022 14:34:32 +0200 Subject: [PATCH 127/435] Fix hashslot calculation for XACK, XCLAIM and XPENDING (#2085) The current implementation of XACK, XCLAIM and XPENDING uses a ComandValuesMessage. Due to this the GetHashSlot() method always returns -1. Using these commands in a redis cluster environment will result in MOVED responses as the commands are send to incorrect endpoints. Co-authored-by: Niels Derdaele Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/RedisDatabase.cs | 27 +++++++++++------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 4c2c40658..dc572d616 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -14,6 +14,7 @@ - Adds: Support for `ZDIFF`, `ZDIFFSTORE`, `ZINTER`, `ZINTERCARD`, and `ZUNION` with `.SortedSetCombine()`/`.SortedSetCombineAsync()`, `.SortedSetCombineWithScores()`/`.SortedSetCombineWithScoresAsync()`, and `.SortedSetIntersectionLength()`/`.SortedSetIntersectionLengthAsync()` ([#2075 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2075)) - Adds: Support for `SINTERCARD` with `.SetIntersectionLength()`/`.SetIntersectionLengthAsync()` ([#2078 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2078)) - Adds: Support for `LPOS` with `.ListPosition()`/`.ListPositionAsync()` and `.ListPositions()`/`.ListPositionsAsync()` ([#2080 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2080)) +- Fix: For streams, properly hash `XACK`, `XCLAIM`, and `XPENDING` in cluster scenarios to eliminate `MOVED` retries ([#2085 by nielsderdaele](https://github.com/StackExchange/StackExchange.Redis/pull/2085)) ## 2.5.61 diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 03707579a..89e89a370 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -3423,12 +3423,11 @@ private Message GetStreamAcknowledgeMessage(RedisKey key, RedisValue groupName, { var values = new RedisValue[] { - key.AsRedisValue(), groupName, messageId }; - return Message.Create(Database, flags, RedisCommand.XACK, values); + return Message.Create(Database, flags, RedisCommand.XACK, key, values); } private Message GetStreamAcknowledgeMessage(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags) @@ -3436,11 +3435,10 @@ private Message GetStreamAcknowledgeMessage(RedisKey key, RedisValue groupName, if (messageIds == null) throw new ArgumentNullException(nameof(messageIds)); if (messageIds.Length == 0) throw new ArgumentOutOfRangeException(nameof(messageIds), "messageIds must contain at least one item."); - var values = new RedisValue[messageIds.Length + 2]; + var values = new RedisValue[messageIds.Length + 1]; var offset = 0; - values[offset++] = key.AsRedisValue(); values[offset++] = groupName; for (var i = 0; i < messageIds.Length; i++) @@ -3448,7 +3446,7 @@ private Message GetStreamAcknowledgeMessage(RedisKey key, RedisValue groupName, values[offset++] = messageIds[i]; } - return Message.Create(Database, flags, RedisCommand.XACK, values); + return Message.Create(Database, flags, RedisCommand.XACK, key, values); } private Message GetStreamAddMessage(RedisKey key, RedisValue messageId, int? maxLength, bool useApproximateMaxLength, NameValueEntry streamPair, CommandFlags flags) @@ -3539,11 +3537,10 @@ private Message GetStreamClaimMessage(RedisKey key, RedisValue consumerGroup, Re if (messageIds.Length == 0) throw new ArgumentOutOfRangeException(nameof(messageIds), "messageIds must contain at least one item."); // XCLAIM ... - var values = new RedisValue[4 + messageIds.Length + (returnJustIds ? 1 : 0)]; + var values = new RedisValue[3 + messageIds.Length + (returnJustIds ? 1 : 0)]; var offset = 0; - values[offset++] = key.AsRedisValue(); values[offset++] = consumerGroup; values[offset++] = assignToConsumer; values[offset++] = minIdleTimeInMs; @@ -3558,7 +3555,7 @@ private Message GetStreamClaimMessage(RedisKey key, RedisValue consumerGroup, Re values[offset] = StreamConstants.JustId; } - return Message.Create(Database, flags, RedisCommand.XCLAIM, values); + return Message.Create(Database, flags, RedisCommand.XCLAIM, key, values); } private Message GetStreamCreateConsumerGroupMessage(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None) @@ -3602,22 +3599,22 @@ private Message GetStreamPendingMessagesMessage(RedisKey key, RedisValue groupNa throw new ArgumentOutOfRangeException(nameof(count), "count must be greater than 0."); } - var values = new RedisValue[consumerName == RedisValue.Null ? 5 : 6]; + var values = new RedisValue[consumerName == RedisValue.Null ? 4 : 5]; - values[0] = key.AsRedisValue(); - values[1] = groupName; - values[2] = minId ?? StreamConstants.ReadMinValue; - values[3] = maxId ?? StreamConstants.ReadMaxValue; - values[4] = count; + values[0] = groupName; + values[1] = minId ?? StreamConstants.ReadMinValue; + values[2] = maxId ?? StreamConstants.ReadMaxValue; + values[3] = count; if (consumerName != RedisValue.Null) { - values[5] = consumerName; + values[4] = consumerName; } return Message.Create(Database, flags, RedisCommand.XPENDING, + key, values); } From 311f898fad5d35d7da3742c0bb36122a6e1e2ad1 Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Wed, 13 Apr 2022 16:45:14 +0300 Subject: [PATCH 128/435] Support OBJECT REFCOUNT (#2087) https://redis.io/commands/object-refcount/ (#2055) Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Interfaces/IDatabase.cs | 9 +++++++++ .../Interfaces/IDatabaseAsync.cs | 9 +++++++++ .../KeyspaceIsolation/DatabaseWrapper.cs | 3 +++ .../KeyspaceIsolation/WrapperBase.cs | 3 +++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 2 ++ src/StackExchange.Redis/RedisDatabase.cs | 12 ++++++++++++ src/StackExchange.Redis/RedisLiterals.cs | 3 ++- .../DatabaseWrapperTests.cs | 7 +++++++ tests/StackExchange.Redis.Tests/Keys.cs | 17 +++++++++++++++++ .../WrapperBaseTests.cs | 7 +++++++ 11 files changed, 72 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index dc572d616..4f7001a06 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -15,6 +15,7 @@ - Adds: Support for `SINTERCARD` with `.SetIntersectionLength()`/`.SetIntersectionLengthAsync()` ([#2078 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2078)) - Adds: Support for `LPOS` with `.ListPosition()`/`.ListPositionAsync()` and `.ListPositions()`/`.ListPositionsAsync()` ([#2080 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2080)) - Fix: For streams, properly hash `XACK`, `XCLAIM`, and `XPENDING` in cluster scenarios to eliminate `MOVED` retries ([#2085 by nielsderdaele](https://github.com/StackExchange/StackExchange.Redis/pull/2085)) +- Adds: Support for `OBJECT REFCOUNT` with `.KeyRefCount()`/`.KeyRefCountAsync()` ([#2087 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2087)) ## 2.5.61 diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index ad5595800..d40721650 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -620,6 +620,15 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/randomkey RedisKey KeyRandom(CommandFlags flags = CommandFlags.None); + /// + /// Returns the reference count of the object stored at . + /// + /// The key to get a reference count for. + /// The flags to use for this operation. + /// The number of references ( if the key does not exist). + /// https://redis.io/commands/object-refcount + long? KeyRefCount(RedisKey key, CommandFlags flags = CommandFlags.None); + /// /// Renames to . /// It returns an error when the source and destination names are the same, or when key does not exist. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 29bdd2d52..d4b0dac74 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -596,6 +596,15 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/randomkey Task KeyRandomAsync(CommandFlags flags = CommandFlags.None); + /// + /// Returns the reference count of the object stored at . + /// + /// The key to get a reference count for. + /// The flags to use for this operation. + /// The number of references ( if the key does not exist). + /// https://redis.io/commands/object-refcount + Task KeyRefCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// /// Renames to . /// It returns an error when the source and destination names are the same, or when key does not exist. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 69c9b5cf7..959ecaa70 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -164,6 +164,9 @@ public bool KeyPersist(RedisKey key, CommandFlags flags = CommandFlags.None) => public RedisKey KeyRandom(CommandFlags flags = CommandFlags.None) => throw new NotSupportedException("RANDOMKEY is not supported when a key-prefix is specified"); + public long? KeyRefCount(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyRefCount(ToInner(key), flags); + public bool KeyRename(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.KeyRename(ToInner(key), ToInner(newKey), when, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index a5999039e..b26a66622 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -174,6 +174,9 @@ public Task KeyPersistAsync(RedisKey key, CommandFlags flags = CommandFlag public Task KeyRandomAsync(CommandFlags flags = CommandFlags.None) => throw new NotSupportedException("RANDOMKEY is not supported when a key-prefix is specified"); + public Task KeyRefCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyRefCountAsync(ToInner(key), flags); + public Task KeyRenameAsync(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.KeyRenameAsync(ToInner(key), ToInner(newKey), when, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 6ddf9d892..91d53c8ad 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -531,6 +531,7 @@ StackExchange.Redis.IDatabase.KeyMigrate(StackExchange.Redis.RedisKey key, Syste StackExchange.Redis.IDatabase.KeyMove(StackExchange.Redis.RedisKey key, int database, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyPersist(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyRandom(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisKey +StackExchange.Redis.IDatabase.KeyRefCount(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long? StackExchange.Redis.IDatabase.KeyRename(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisKey newKey, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyRestore(StackExchange.Redis.RedisKey key, byte[]! value, System.TimeSpan? expiry = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IDatabase.KeyTimeToLive(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.TimeSpan? @@ -731,6 +732,7 @@ StackExchange.Redis.IDatabaseAsync.KeyMigrateAsync(StackExchange.Redis.RedisKey StackExchange.Redis.IDatabaseAsync.KeyMoveAsync(StackExchange.Redis.RedisKey key, int database, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyPersistAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyRandomAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyRefCountAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyRenameAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisKey newKey, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyRestoreAsync(StackExchange.Redis.RedisKey key, byte[]! value, System.TimeSpan? expiry = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyTimeToLiveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 89e89a370..2bfc4cbe1 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -827,6 +827,18 @@ public Task KeyRandomAsync(CommandFlags flags = CommandFlags.None) return ExecuteAsync(msg, ResultProcessor.RedisKey); } + public long? KeyRefCount(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.OBJECT, RedisLiterals.REFCOUNT, key); + return ExecuteSync(msg, ResultProcessor.NullableInt64); + } + + public Task KeyRefCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.OBJECT, RedisLiterals.REFCOUNT, key); + return ExecuteAsync(msg, ResultProcessor.NullableInt64); + } + public bool KeyRename(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None) { WhenAlwaysOrNotExists(when); diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 2afbd443c..14a1a3cd2 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -92,12 +92,13 @@ public static readonly RedisValue PX = "PX", PXAT = "PXAT", RANK = "RANK", - RIGHT = "RIGHT", + REFCOUNT = "REFCOUNT", REPLACE = "REPLACE", RESET = "RESET", RESETSTAT = "RESETSTAT", REV = "REV", REWRITE = "REWRITE", + RIGHT = "RIGHT", SAVE = "SAVE", SEGFAULT = "SEGFAULT", SET = "SET", diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index fa3a88851..aaefbe2e2 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -317,6 +317,13 @@ public void KeyRandom() Assert.Throws(() => wrapper.KeyRandom()); } + [Fact] + public void KeyRefCount() + { + wrapper.KeyRefCount("key", CommandFlags.None); + mock.Verify(_ => _.KeyRefCount("prefix:key", CommandFlags.None)); + } + [Fact] public void KeyRename() { diff --git a/tests/StackExchange.Redis.Tests/Keys.cs b/tests/StackExchange.Redis.Tests/Keys.cs index ed41d5815..85612b8f4 100644 --- a/tests/StackExchange.Redis.Tests/Keys.cs +++ b/tests/StackExchange.Redis.Tests/Keys.cs @@ -264,5 +264,22 @@ public async Task TouchIdleTimeAsync() Assert.True(idleTime1 < idleTime); } } + + [Fact] + public async Task KeyRefCount() + { + using var muxer = Create(); + var key = Me(); + var db = muxer.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + + Assert.Equal(1, db.KeyRefCount(key)); + Assert.Equal(1, await db.KeyRefCountAsync(key)); + + var keyNotExists = key + "no-exist"; + Assert.Null(db.KeyRefCount(keyNotExists)); + Assert.Null(await db.KeyRefCountAsync(keyNotExists)); + } } } diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index dfb6968ed..4017fc7cf 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -277,6 +277,13 @@ public Task KeyRandomAsync() return Assert.ThrowsAsync(() => wrapper.KeyRandomAsync()); } + [Fact] + public void KeyRefCountAsync() + { + wrapper.KeyRefCountAsync("key", CommandFlags.None); + mock.Verify(_ => _.KeyRefCountAsync("prefix:key", CommandFlags.None)); + } + [Fact] public void KeyRenameAsync() { From 567527d251f75cb0e6689783f8bd57122f48878f Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Thu, 14 Apr 2022 16:24:54 +0300 Subject: [PATCH 129/435] Support OBJECT ENCODING (#2088) Support https://redis.io/commands/object-encoding/ (#2055) Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + .../Interfaces/IDatabase.cs | 9 +++++++ .../Interfaces/IDatabaseAsync.cs | 9 +++++++ .../KeyspaceIsolation/DatabaseWrapper.cs | 3 +++ .../KeyspaceIsolation/WrapperBase.cs | 3 +++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 2 ++ src/StackExchange.Redis/RedisDatabase.cs | 14 +++++++++- src/StackExchange.Redis/RedisLiterals.cs | 1 + .../DatabaseWrapperTests.cs | 7 +++++ tests/StackExchange.Redis.Tests/Keys.cs | 26 +++++++++++++++++++ .../WrapperBaseTests.cs | 8 ++++++ 11 files changed, 82 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 4f7001a06..8a87fd3a1 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -16,6 +16,7 @@ - Adds: Support for `LPOS` with `.ListPosition()`/`.ListPositionAsync()` and `.ListPositions()`/`.ListPositionsAsync()` ([#2080 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2080)) - Fix: For streams, properly hash `XACK`, `XCLAIM`, and `XPENDING` in cluster scenarios to eliminate `MOVED` retries ([#2085 by nielsderdaele](https://github.com/StackExchange/StackExchange.Redis/pull/2085)) - Adds: Support for `OBJECT REFCOUNT` with `.KeyRefCount()`/`.KeyRefCountAsync()` ([#2087 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2087)) +- Adds: Support for `OBJECT ENCODIND` with `.KeyEncoding()`/`.KeyEncodingAsync()` ([#2088 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2088)) ## 2.5.61 diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index d40721650..627ab3d81 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -516,6 +516,15 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/dump byte[]? KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Returns the internal encoding for the Redis object stored at . + /// + /// The key to dump. + /// The flags to use for this operation. + /// The Redis encoding for the value or is the key does not exist. + /// https://redis.io/commands/object-encoding + string? KeyEncoding(RedisKey key, CommandFlags flags = CommandFlags.None); + /// /// Returns if key exists. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index d4b0dac74..00e1e5a8b 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -492,6 +492,15 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/dump Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Returns the internal encoding for the Redis object stored at . + /// + /// The key to dump. + /// The flags to use for this operation. + /// The Redis encoding for the value or is the key does not exist. + /// https://redis.io/commands/object-encoding + Task KeyEncodingAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// /// Returns if key exists. /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 959ecaa70..df54dd8fd 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -138,6 +138,9 @@ public bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None) => public byte[]? KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.KeyDump(ToInner(key), flags); + public string? KeyEncoding(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyEncoding(ToInner(key), flags); + public bool KeyExists(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.KeyExists(ToInner(key), flags); public long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index b26a66622..0821c9a2b 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -147,6 +147,9 @@ public Task KeyDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags public Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.KeyDumpAsync(ToInner(key), flags); + public Task KeyEncodingAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyEncodingAsync(ToInner(key), flags); + public Task KeyExistsAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.KeyExistsAsync(ToInner(key), flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 91d53c8ad..d7f806828 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -522,6 +522,7 @@ StackExchange.Redis.IDatabase.KeyCopy(StackExchange.Redis.RedisKey sourceKey, St StackExchange.Redis.IDatabase.KeyDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyDelete(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.KeyDump(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> byte[]? +StackExchange.Redis.IDatabase.KeyEncoding(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? StackExchange.Redis.IDatabase.KeyExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyExists(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool @@ -723,6 +724,7 @@ StackExchange.Redis.IDatabaseAsync.KeyCopyAsync(StackExchange.Redis.RedisKey sou StackExchange.Redis.IDatabaseAsync.KeyDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyDeleteAsync(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyDumpAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyEncodingAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyExistsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyExistsAsync(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 2bfc4cbe1..d243e4a30 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -673,6 +673,18 @@ private RedisCommand GetDeleteCommand(RedisKey key, CommandFlags flags, out Serv return ExecuteAsync(msg, ResultProcessor.ByteArray); } + public string? KeyEncoding(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.OBJECT, RedisLiterals.ENCODING, key); + return ExecuteSync(msg, ResultProcessor.String); + } + + public Task KeyEncodingAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.OBJECT, RedisLiterals.ENCODING, key); + return ExecuteAsync(msg, ResultProcessor.String); + } + public bool KeyExists(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.EXISTS, key); @@ -3620,7 +3632,7 @@ private Message GetStreamPendingMessagesMessage(RedisKey key, RedisValue groupNa if (consumerName != RedisValue.Null) { - values[4] = consumerName; + values[4] = consumerName; } return Message.Create(Database, diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 14a1a3cd2..f56fc79dd 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -56,6 +56,7 @@ public static readonly RedisValue DB = "DB", DESC = "DESC", DOCTOR = "DOCTOR", + ENCODING = "ENCODING", EX = "EX", EXAT = "EXAT", EXISTS = "EXISTS", diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index aaefbe2e2..64cf898b5 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -266,6 +266,13 @@ public void KeyDump() mock.Verify(_ => _.KeyDump("prefix:key", CommandFlags.None)); } + [Fact] + public void KeyEncoding() + { + wrapper.KeyEncoding("key", CommandFlags.None); + mock.Verify(_ => _.KeyEncoding("prefix:key", CommandFlags.None)); + } + [Fact] public void KeyExists() { diff --git a/tests/StackExchange.Redis.Tests/Keys.cs b/tests/StackExchange.Redis.Tests/Keys.cs index 85612b8f4..0032611a3 100644 --- a/tests/StackExchange.Redis.Tests/Keys.cs +++ b/tests/StackExchange.Redis.Tests/Keys.cs @@ -265,6 +265,32 @@ public async Task TouchIdleTimeAsync() } } + [Fact] + public async Task KeyEncoding() + { + using var muxer = Create(); + var key = Me(); + var db = muxer.GetDatabase(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + + Assert.Equal("embstr", db.KeyEncoding(key)); + Assert.Equal("embstr", await db.KeyEncodingAsync(key)); + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.ListLeftPush(key, "new value", flags: CommandFlags.FireAndForget); + + // Depending on server version, this is going to vary - we're sanity checking here. + var listTypes = new [] { "ziplist", "quicklist" }; + Assert.Contains(db.KeyEncoding(key), listTypes); + Assert.Contains(await db.KeyEncodingAsync(key), listTypes); + + var keyNotExists = key + "no-exist"; + Assert.Null(db.KeyEncoding(keyNotExists)); + Assert.Null(await db.KeyEncodingAsync(keyNotExists)); + } + [Fact] public async Task KeyRefCount() { diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index 4017fc7cf..471b6f184 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -226,6 +226,14 @@ public void KeyDumpAsync() mock.Verify(_ => _.KeyDumpAsync("prefix:key", CommandFlags.None)); } + [Fact] + public void KeyEncodingAsync() + { + wrapper.KeyEncodingAsync("key", CommandFlags.None); + mock.Verify(_ => _.KeyEncodingAsync("prefix:key", CommandFlags.None)); + } + + [Fact] public void KeyExistsAsync() { From 6b371169ce51c77b52ca790aba487911d69ad6c9 Mon Sep 17 00:00:00 2001 From: Steve Lorello <42971704+slorello89@users.noreply.github.com> Date: Thu, 14 Apr 2022 09:34:03 -0400 Subject: [PATCH 130/435] HRANDFIELD feature (#2090) Implements [`HRANDFIELD`](https://redis.io/commands/hrandfield/) as a part of #2055. Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 3 +- src/StackExchange.Redis/Enums/RedisCommand.cs | 1 + .../Interfaces/IDatabase.cs | 29 +++++++ .../Interfaces/IDatabaseAsync.cs | 29 +++++++ .../KeyspaceIsolation/DatabaseWrapper.cs | 9 +++ .../KeyspaceIsolation/WrapperBase.cs | 10 +++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 6 ++ src/StackExchange.Redis/RedisDatabase.cs | 36 +++++++++ src/StackExchange.Redis/RedisLiterals.cs | 1 + tests/StackExchange.Redis.Tests/Hashes.cs | 76 +++++++++++++++++++ 10 files changed, 199 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 8a87fd3a1..f0f828ccf 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -16,7 +16,8 @@ - Adds: Support for `LPOS` with `.ListPosition()`/`.ListPositionAsync()` and `.ListPositions()`/`.ListPositionsAsync()` ([#2080 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2080)) - Fix: For streams, properly hash `XACK`, `XCLAIM`, and `XPENDING` in cluster scenarios to eliminate `MOVED` retries ([#2085 by nielsderdaele](https://github.com/StackExchange/StackExchange.Redis/pull/2085)) - Adds: Support for `OBJECT REFCOUNT` with `.KeyRefCount()`/`.KeyRefCountAsync()` ([#2087 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2087)) -- Adds: Support for `OBJECT ENCODIND` with `.KeyEncoding()`/`.KeyEncodingAsync()` ([#2088 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2088)) +- Adds: Support for `OBJECT ENCODING` with `.KeyEncoding()`/`.KeyEncodingAsync()` ([#2088 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2088)) +- Adds: Support for `HRANDFIELD` with `.HashRandomField()`/`.HashRandomFieldAsync()`, `.HashRandomFields()`/`.HashRandomFieldsAsync()`, and `.HashRandomFieldsWithValues()`/`.HashRandomFieldsWithValuesAsync()` ([#2090 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2090)) ## 2.5.61 diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index f40d3ff49..b3b4887c1 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -65,6 +65,7 @@ internal enum RedisCommand HLEN, HMGET, HMSET, + HRANDFIELD, HSCAN, HSET, HSETNX, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 627ab3d81..849a78988 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -337,6 +337,35 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/hlen long HashLength(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Gets a random field from the hash at . + /// + /// The key of the hash. + /// The flags to use for this operation. + /// A random hash field name or if the hash does not exist. + /// https://redis.io/commands/hrandfield + RedisValue HashRandomField(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Gets field names from the hash at . + /// + /// The key of the hash. + /// The number of fields to return. + /// The flags to use for this operation. + /// An array of hash field names of size of at most , or if the hash does not exist. + /// https://redis.io/commands/hrandfield + RedisValue[] HashRandomFields(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + + /// + /// Gets field names and values from the hash at . + /// + /// The key of the hash. + /// The number of fields to return. + /// The flags to use for this operation. + /// An array of hash entries of size of at most , or if the hash does not exist. + /// https://redis.io/commands/hrandfield + HashEntry[] HashRandomFieldsWithValues(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// /// The HSCAN command is used to incrementally iterate over a hash. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 00e1e5a8b..254841ff1 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -324,6 +324,35 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/hlen Task HashLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Gets a random field from the hash at . + /// + /// The key of the hash. + /// The flags to use for this operation. + /// A random hash field name or if the hash does not exist. + /// https://redis.io/commands/hrandfield + Task HashRandomFieldAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Gets field names from the hash at . + /// + /// The key of the hash. + /// The number of fields to return. + /// The flags to use for this operation. + /// An array of hash field names of size of at most , or if the hash does not exist. + /// https://redis.io/commands/hrandfield + Task HashRandomFieldsAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + + /// + /// Gets field names and values from the hash at . + /// + /// The key of the hash. + /// The number of fields to return. + /// The flags to use for this operation. + /// An array of hash entries of size of at most , or if the hash does not exist. + /// https://redis.io/commands/hrandfield + Task HashRandomFieldsWithValuesAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// /// The HSCAN command is used to incrementally iterate over a hash. /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to . diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index df54dd8fd..b49805841 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -93,6 +93,15 @@ public RedisValue[] HashKeys(RedisKey key, CommandFlags flags = CommandFlags.Non public long HashLength(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.HashLength(ToInner(key), flags); + public RedisValue HashRandomField(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.HashRandomField(ToInner(key), flags); + + public RedisValue[] HashRandomFields(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.HashRandomFields(ToInner(key), count, flags); + + public HashEntry[] HashRandomFieldsWithValues(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.HashRandomFieldsWithValues(ToInner(key), count, flags); + public bool HashSet(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.HashSet(ToInner(key), hashField, value, when, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 0821c9a2b..0d9181f49 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -96,6 +96,16 @@ public Task HashKeysAsync(RedisKey key, CommandFlags flags = Comma public Task HashLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.HashLengthAsync(ToInner(key), flags); + public Task HashRandomFieldAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.HashRandomFieldAsync(ToInner(key), flags); + + public Task HashRandomFieldsAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.HashRandomFieldsAsync(ToInner(key), count, flags); + + public Task HashRandomFieldsWithValuesAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.HashRandomFieldsWithValuesAsync(ToInner(key), count, flags); + + public IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => Inner.HashScanAsync(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index d7f806828..93411f4c5 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -505,6 +505,9 @@ StackExchange.Redis.IDatabase.HashIncrement(StackExchange.Redis.RedisKey key, St StackExchange.Redis.IDatabase.HashIncrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.HashKeys(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.HashLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.HashRandomField(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.HashRandomFields(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.IDatabase.HashRandomFieldsWithValues(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.HashEntry[]! StackExchange.Redis.IDatabase.HashScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! StackExchange.Redis.IDatabase.HashScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable! StackExchange.Redis.IDatabase.HashSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void @@ -707,6 +710,9 @@ StackExchange.Redis.IDatabaseAsync.HashIncrementAsync(StackExchange.Redis.RedisK StackExchange.Redis.IDatabaseAsync.HashIncrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashKeysAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashRandomFieldAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashRandomFieldsAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashRandomFieldsWithValuesAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! StackExchange.Redis.IDatabaseAsync.HashSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index d243e4a30..2ea8cd94e 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -402,12 +402,48 @@ public long HashLength(RedisKey key, CommandFlags flags = CommandFlags.None) return ExecuteSync(msg, ResultProcessor.Int64); } + public RedisValue HashRandomField(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.HRANDFIELD, key); + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + + public RedisValue[] HashRandomFields(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.HRANDFIELD, key, count); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public HashEntry[] HashRandomFieldsWithValues(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.HRANDFIELD, key, count, RedisLiterals.WITHVALUES); + return ExecuteSync(msg, ResultProcessor.HashEntryArray, defaultValue: Array.Empty()); + } + public Task HashLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.HLEN, key); return ExecuteAsync(msg, ResultProcessor.Int64); } + public Task HashRandomFieldAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.HRANDFIELD, key); + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + + public Task HashRandomFieldsAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.HRANDFIELD, key, count); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public Task HashRandomFieldsWithValuesAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.HRANDFIELD, key, count, RedisLiterals.WITHVALUES); + return ExecuteAsync(msg, ResultProcessor.HashEntryArray, defaultValue: Array.Empty()); + } + IEnumerable IDatabase.HashScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => HashScanAsync(key, pattern, pageSize, CursorUtils.Origin, 0, flags); diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index f56fc79dd..52329f8e5 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -110,6 +110,7 @@ public static readonly RedisValue TYPE = "TYPE", WEIGHTS = "WEIGHTS", WITHSCORES = "WITHSCORES", + WITHVALUES = "WITHVALUES", XOR = "XOR", XX = "XX", diff --git a/tests/StackExchange.Redis.Tests/Hashes.cs b/tests/StackExchange.Redis.Tests/Hashes.cs index e15d68480..972b842de 100644 --- a/tests/StackExchange.Redis.Tests/Hashes.cs +++ b/tests/StackExchange.Redis.Tests/Hashes.cs @@ -596,5 +596,81 @@ public async Task TestWhenAlwaysAsync() Assert.False(result4, "Duplicate se key 1 variant"); } } + + [Fact] + public async Task HashRandomFieldAsync() + { + using var muxer = Create(); + Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + + var db = muxer.GetDatabase(); + var hashKey = Me(); + var items = new HashEntry[] { new("new york", "yankees"), new("baltimore", "orioles"), new("boston", "red sox"), new("Tampa Bay", "rays"), new("Toronto", "blue jays") }; + await db.HashSetAsync(hashKey, items); + + var singleField = await db.HashRandomFieldAsync(hashKey); + var multiFields = await db.HashRandomFieldsAsync(hashKey, 3); + var withValues = await db.HashRandomFieldsWithValuesAsync(hashKey, 3); + Assert.Equal(3, multiFields.Length); + Assert.Equal(3, withValues.Length); + Assert.Contains(items, x => x.Name == singleField); + + foreach (var field in multiFields) + { + Assert.Contains(items, x => x.Name == field); + } + + foreach (var field in withValues) + { + Assert.Contains(items, x => x.Name == field.Name); + } + } + + [Fact] + public void HashRandomField() + { + using var muxer = Create(); + Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + + var db = muxer.GetDatabase(); + var hashKey = Me(); + var items = new HashEntry[] { new("new york", "yankees"), new("baltimore", "orioles"), new("boston", "red sox"), new("Tampa Bay", "rays"), new("Toronto", "blue jays") }; + db.HashSet(hashKey, items); + + var singleField = db.HashRandomField(hashKey); + var multiFields = db.HashRandomFields(hashKey, 3); + var withValues = db.HashRandomFieldsWithValues(hashKey, 3); + Assert.Equal(3, multiFields.Length); + Assert.Equal(3, withValues.Length); + Assert.Contains(items, x => x.Name == singleField); + + foreach (var field in multiFields) + { + Assert.Contains(items, x => x.Name == field); + } + + foreach (var field in withValues) + { + Assert.Contains(items, x => x.Name == field.Name); + } + } + + [Fact] + public void HashRandomFieldEmptyHash() + { + using var muxer = Create(); + Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + + var db = muxer.GetDatabase(); + var hashKey = Me(); + + var singleField = db.HashRandomField(hashKey); + var multiFields = db.HashRandomFields(hashKey, 3); + var withValues = db.HashRandomFieldsWithValues(hashKey, 3); + + Assert.Equal(RedisValue.Null, singleField); + Assert.Empty(multiFields); + Assert.Empty(withValues); + } } } From cb571bd8bd6a93617fe424eead4ca1b0bce89156 Mon Sep 17 00:00:00 2001 From: Todd Tingen Date: Thu, 14 Apr 2022 09:38:49 -0400 Subject: [PATCH 131/435] Support the ZMSCORE command. (#2082) Support for https://redis.io/commands/zmscore/ (#2055) Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 1 + .../Interfaces/IDatabase.cs | 14 + .../Interfaces/IDatabaseAsync.cs | 14 + .../KeyspaceIsolation/DatabaseWrapper.cs | 3 + .../KeyspaceIsolation/WrapperBase.cs | 3 + src/StackExchange.Redis/PublicAPI.Shipped.txt | 2 + src/StackExchange.Redis/RawResult.cs | 3 + src/StackExchange.Redis/RedisDatabase.cs | 12 + src/StackExchange.Redis/ResultProcessor.cs | 18 ++ .../DatabaseWrapperTests.cs | 7 + tests/StackExchange.Redis.Tests/SortedSets.cs | 260 ++++++++++++++++++ .../WrapperBaseTests.cs | 7 + 13 files changed, 345 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index f0f828ccf..1ddfeab3a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -14,6 +14,7 @@ - Adds: Support for `ZDIFF`, `ZDIFFSTORE`, `ZINTER`, `ZINTERCARD`, and `ZUNION` with `.SortedSetCombine()`/`.SortedSetCombineAsync()`, `.SortedSetCombineWithScores()`/`.SortedSetCombineWithScoresAsync()`, and `.SortedSetIntersectionLength()`/`.SortedSetIntersectionLengthAsync()` ([#2075 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2075)) - Adds: Support for `SINTERCARD` with `.SetIntersectionLength()`/`.SetIntersectionLengthAsync()` ([#2078 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2078)) - Adds: Support for `LPOS` with `.ListPosition()`/`.ListPositionAsync()` and `.ListPositions()`/`.ListPositionsAsync()` ([#2080 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2080)) +- Adds: Support for `ZMSCORE` with `.SortedSetScores()`/.`SortedSetScoresAsync()` ([#2082 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2082)) - Fix: For streams, properly hash `XACK`, `XCLAIM`, and `XPENDING` in cluster scenarios to eliminate `MOVED` retries ([#2085 by nielsderdaele](https://github.com/StackExchange/StackExchange.Redis/pull/2085)) - Adds: Support for `OBJECT REFCOUNT` with `.KeyRefCount()`/`.KeyRefCountAsync()` ([#2087 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2087)) - Adds: Support for `OBJECT ENCODING` with `.KeyEncoding()`/`.KeyEncodingAsync()` ([#2088 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2088)) diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index b3b4887c1..ab016961f 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -205,6 +205,7 @@ internal enum RedisCommand ZINTERCARD, ZINTERSTORE, ZLEXCOUNT, + ZMSCORE, ZPOPMAX, ZPOPMIN, ZRANDMEMBER, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 849a78988..a988c7220 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1876,6 +1876,20 @@ IEnumerable SortedSetScan(RedisKey key, /// https://redis.io/commands/zscore double? SortedSetScore(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + /// + /// Returns the scores of members in the sorted set at . + /// If a member does not exist in the sorted set, or key does not exist, is returned. + /// + /// The key of the sorted set. + /// The members to get a score for. + /// The flags to use for this operation. + /// + /// The scores of the members in the same order as the array. + /// If a member does not exist in the set, is returned. + /// + /// https://redis.io/commands/zmscore + double?[] SortedSetScores(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); + /// /// Removes and returns the first element from the sorted set stored at key, by default with the scores ordered from low to high. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 254841ff1..020f06979 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1828,6 +1828,20 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// https://redis.io/commands/zscore Task SortedSetScoreAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + /// + /// Returns the scores of members in the sorted set at . + /// If a member does not exist in the sorted set, or key does not exist, is returned. + /// + /// The key of the sorted set. + /// The members to get a score for. + /// The flags to use for this operation. + /// + /// The scores of the members in the same order as the array. + /// If a member does not exist in the set, is returned. + /// + /// https://redis.io/commands/zmscore + Task SortedSetScoresAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); + /// /// Removes and returns the first element from the sorted set stored at key, by default with the scores ordered from low to high. /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index b49805841..0b28037d9 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -454,6 +454,9 @@ public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue public double? SortedSetScore(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => Inner.SortedSetScore(ToInner(key), member, flags); + public double?[] SortedSetScores(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetScores(ToInner(key), members, flags); + public SortedSetEntry? SortedSetPop(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => Inner.SortedSetPop(ToInner(key), order, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 0d9181f49..033757262 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -468,6 +468,9 @@ public Task SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min, public Task SortedSetScoreAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => Inner.SortedSetScoreAsync(ToInner(key), member, flags); + public Task SortedSetScoresAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetScoresAsync(ToInner(key), members, flags); + public IAsyncEnumerable SortedSetScanAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => Inner.SortedSetScanAsync(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 93411f4c5..56d738de5 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -629,6 +629,7 @@ StackExchange.Redis.IDatabase.SortedSetRemoveRangeByValue(StackExchange.Redis.Re StackExchange.Redis.IDatabase.SortedSetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! StackExchange.Redis.IDatabase.SortedSetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable! StackExchange.Redis.IDatabase.SortedSetScore(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double? +StackExchange.Redis.IDatabase.SortedSetScores(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double?[]! StackExchange.Redis.IDatabase.StreamAcknowledge(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue messageId, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StreamAcknowledge(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue @@ -832,6 +833,7 @@ StackExchange.Redis.IDatabaseAsync.SortedSetRemoveRangeByScoreAsync(StackExchang StackExchange.Redis.IDatabaseAsync.SortedSetRemoveRangeByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! StackExchange.Redis.IDatabaseAsync.SortedSetScoreAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetScoresAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue messageId, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index ef449c101..919054411 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -253,6 +253,9 @@ internal bool GetBoolean() [MethodImpl(MethodImplOptions.AggressiveInlining)] internal Sequence GetItems() => _items.Cast(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal double?[]? GetItemsAsDoubles() => this.ToArray((in RawResult x) => x.TryGetDouble(out double val) ? val : null); + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal RedisKey[]? GetItemsAsKeys() => this.ToArray((in RawResult x) => x.AsRedisKey()); diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 2ea8cd94e..db04c6ba8 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1995,12 +1995,24 @@ private CursorEnumerable SortedSetScanAsync(RedisKey key, RedisV return ExecuteSync(msg, ResultProcessor.NullableDouble); } + public double?[] SortedSetScores(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.ZMSCORE, key, members); + return ExecuteSync(msg, ResultProcessor.NullableDoubleArray, defaultValue: Array.Empty()); + } + public Task SortedSetScoreAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.ZSCORE, key, member); return ExecuteAsync(msg, ResultProcessor.NullableDouble); } + public Task SortedSetScoresAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.ZMSCORE, key, members); + return ExecuteAsync(msg, ResultProcessor.NullableDoubleArray, defaultValue: Array.Empty()); + } + public SortedSetEntry? SortedSetPop(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, order == Order.Descending ? RedisCommand.ZPOPMAX : RedisCommand.ZPOPMIN, key); diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 9db002a0c..4f611354d 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -56,6 +56,10 @@ public static readonly ResultProcessor public static readonly ResultProcessor NullableDouble = new NullableDoubleProcessor(); + + public static readonly ResultProcessor + NullableDoubleArray = new NullableDoubleArrayProcessor(); + public static readonly ResultProcessor NullableInt64 = new NullableInt64Processor(); @@ -1112,6 +1116,20 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + private sealed class NullableDoubleArrayProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Type == ResultType.MultiBulk && !result.IsNull) + { + var arr = result.GetItemsAsDoubles()!; + SetResult(message, arr); + return true; + } + return false; + } + } + private sealed class NullableDoubleProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 64cf898b5..0c6e41b97 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -950,6 +950,13 @@ public void SortedSetScore() mock.Verify(_ => _.SortedSetScore("prefix:key", "member", CommandFlags.None)); } + [Fact] + public void SortedSetScore_Multiple() + { + wrapper.SortedSetScores("key", new RedisValue[] { "member1", "member2" }, CommandFlags.None); + mock.Verify(_ => _.SortedSetScores("prefix:key", new RedisValue[] { "member1", "member2" }, CommandFlags.None)); + } + [Fact] public void StreamAcknowledge_1() { diff --git a/tests/StackExchange.Redis.Tests/SortedSets.cs b/tests/StackExchange.Redis.Tests/SortedSets.cs index c20464240..a6b29e64f 100644 --- a/tests/StackExchange.Redis.Tests/SortedSets.cs +++ b/tests/StackExchange.Redis.Tests/SortedSets.cs @@ -1015,5 +1015,265 @@ public void SortedSetRangeStoreFailExclude() var exception = Assert.Throws(()=>db.SortedSetRangeAndStore(sourceKey, destinationKey,0,-1, exclude: Exclude.Both)); Assert.Equal("exclude", exception.ParamName); } + + [Fact] + public void SortedSetScoresSingle() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v2_1_0); + + var db = conn.GetDatabase(); + var key = Me(); + var memberName = "member"; + + db.KeyDelete(key); + db.SortedSetAdd(key, memberName, 1.5); + + var score = db.SortedSetScore(key, memberName); + + Assert.NotNull(score); + Assert.Equal((double)1.5, score.Value); + } + + [Fact] + public async Task SortedSetScoresSingleAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v2_1_0); + + var db = conn.GetDatabase(); + var key = Me(); + var memberName = "member"; + + await db.KeyDeleteAsync(key); + await db.SortedSetAddAsync(key, memberName, 1.5); + + var score = await db.SortedSetScoreAsync(key, memberName); + + Assert.NotNull(score); + Assert.Equal((double)1.5, score.Value); + } + + [Fact] + public void SortedSetScoresSingle_MissingSetStillReturnsNull() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v2_1_0); + + var db = conn.GetDatabase(); + var key = Me(); + + db.KeyDelete(key); + + // Attempt to retrieve score for a missing set, should still return null. + var score = db.SortedSetScore(key, "bogusMemberName"); + + Assert.Null(score); + } + + [Fact] + public async Task SortedSetScoresSingle_MissingSetStillReturnsNullAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v2_1_0); + + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + // Attempt to retrieve score for a missing set, should still return null. + var score = await db.SortedSetScoreAsync(key, "bogusMemberName"); + + Assert.Null(score); + } + + [Fact] + public void SortedSetScoresSingle_ReturnsNullForMissingMember() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v2_1_0); + + var db = conn.GetDatabase(); + var key = Me(); + + db.KeyDelete(key); + db.SortedSetAdd(key, "member1", 1.5); + + // Attempt to retrieve score for a missing member, should return null. + var score = db.SortedSetScore(key, "bogusMemberName"); + + Assert.Null(score); + } + + [Fact] + public async Task SortedSetScoresSingle_ReturnsNullForMissingMemberAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v2_1_0); + + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + await db.SortedSetAddAsync(key, "member1", 1.5); + + // Attempt to retrieve score for a missing member, should return null. + var score = await db.SortedSetScoreAsync(key, "bogusMemberName"); + + Assert.Null(score); + } + + [Fact] + public void SortedSetScoresMultiple() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + var member1 = "member1"; + var member2 = "member2"; + var member3 = "member3"; + + db.KeyDelete(key); + db.SortedSetAdd(key, member1, 1.5); + db.SortedSetAdd(key, member2, 1.75); + db.SortedSetAdd(key, member3, 2); + + var scores = db.SortedSetScores(key, new RedisValue[] { member1, member2, member3 }); + + Assert.NotNull(scores); + Assert.Equal(3, scores.Length); + Assert.Equal((double)1.5, scores[0]); + Assert.Equal((double)1.75, scores[1]); + Assert.Equal(2, scores[2]); + } + + [Fact] + public async Task SortedSetScoresMultipleAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + var member1 = "member1"; + var member2 = "member2"; + var member3 = "member3"; + + await db.KeyDeleteAsync(key); + await db.SortedSetAddAsync(key, member1, 1.5); + await db.SortedSetAddAsync(key, member2, 1.75); + await db.SortedSetAddAsync(key, member3, 2); + + var scores = await db.SortedSetScoresAsync(key, new RedisValue[] { member1, member2, member3 }); + + Assert.NotNull(scores); + Assert.Equal(3, scores.Length); + Assert.Equal((double)1.5, scores[0]); + Assert.Equal((double)1.75, scores[1]); + Assert.Equal(2, scores[2]); + } + + [Fact] + public void SortedSetScoresMultiple_ReturnsNullItemsForMissingSet() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + + db.KeyDelete(key); + + // Missing set but should still return an array of nulls. + var scores = db.SortedSetScores(key, new RedisValue[] { "bogus1", "bogus2", "bogus3" }); + + Assert.NotNull(scores); + Assert.Equal(3, scores.Length); + Assert.Null(scores[0]); + Assert.Null(scores[1]); + Assert.Null(scores[2]); + } + + [Fact] + public async Task SortedSetScoresMultiple_ReturnsNullItemsForMissingSetAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + // Missing set but should still return an array of nulls. + var scores = await db.SortedSetScoresAsync(key, new RedisValue[] { "bogus1", "bogus2", "bogus3" }); + + Assert.NotNull(scores); + Assert.Equal(3, scores.Length); + Assert.Null(scores[0]); + Assert.Null(scores[1]); + Assert.Null(scores[2]); + } + + [Fact] + public void SortedSetScoresMultiple_ReturnsScoresAndNullItems() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + var member1 = "member1"; + var member2 = "member2"; + var member3 = "member3"; + var bogusMember = "bogusMember"; + + db.KeyDelete(key); + + db.SortedSetAdd(key, member1, 1.5); + db.SortedSetAdd(key, member2, 1.75); + db.SortedSetAdd(key, member3, 2); + + var scores = db.SortedSetScores(key, new RedisValue[] { member1, bogusMember, member2, member3 }); + + Assert.NotNull(scores); + Assert.Equal(4, scores.Length); + Assert.Null(scores[1]); + Assert.Equal((double)1.5, scores[0]); + Assert.Equal((double)1.75, scores[2]); + Assert.Equal(2, scores[3]); + } + + [Fact] + public async Task SortedSetScoresMultiple_ReturnsScoresAndNullItemsAsync() + { + using var conn = Create(); + Skip.IfBelow(conn, RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + var member1 = "member1"; + var member2 = "member2"; + var member3 = "member3"; + var bogusMember = "bogusMember"; + + await db.KeyDeleteAsync(key); + + await db.SortedSetAddAsync(key, member1, 1.5); + await db.SortedSetAddAsync(key, member2, 1.75); + await db.SortedSetAddAsync(key, member3, 2); + + var scores = await db.SortedSetScoresAsync(key, new RedisValue[] { member1, bogusMember, member2, member3 }); + + Assert.NotNull(scores); + Assert.Equal(4, scores.Length); + Assert.Null(scores[1]); + Assert.Equal((double)1.5, scores[0]); + Assert.Equal((double)1.75, scores[2]); + Assert.Equal(2, scores[3]); + } } } diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index 471b6f184..b6204de3c 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -883,6 +883,13 @@ public void SortedSetScoreAsync() mock.Verify(_ => _.SortedSetScoreAsync("prefix:key", "member", CommandFlags.None)); } + [Fact] + public void SortedSetScoreAsync_Multiple() + { + wrapper.SortedSetScoresAsync("key", new RedisValue[] { "member1", "member2" }, CommandFlags.None); + mock.Verify(_ => _.SortedSetScoresAsync("prefix:key", new RedisValue[] { "member1", "member2" }, CommandFlags.None)); + } + [Fact] public void StreamAcknowledgeAsync_1() { From d81ec2c37fca1c47867cd80f5f35c5a2474df1fe Mon Sep 17 00:00:00 2001 From: Steve Lorello <42971704+slorello89@users.noreply.github.com> Date: Fri, 15 Apr 2022 20:17:04 -0400 Subject: [PATCH 132/435] GEOSEARCH & GEOSEARCHSTORE (#2089) This PR implements [`GEOSEARCH`](https://redis.io/commands/geosearch/) and [`GEOSEARCHSTORE`](https://redis.io/commands/geosearchstore/) for #2055 To abstract the box/circle sub-options from GEOSEARCH I added a new abstract class `GeoSearchShape` who's children are responsible for maintaining the bounding shape, its unit of measurement, the number of arguments required for the sub-option, and of course the sub-option name. Rather than casting/extracting the arguments, I have it use an IEnumerable state-machine with yield/return. Wasn't sure which was the better option, the IEnumerable seemed cleaner, open to whichever you want. I changed the `GEORADIUS` pattern of having a `GeoSearch(key, member, args. . .)` and a `GeoSearch(key, lon, lat, args. . .)`, and instead have `GeoSearchByMember` and `GeoSearchByCoordinates`. If I'm honest, it was because my IDE was complaining about breaking compatibility rules by having more than 1 override with optional parameters, not sure if you have a strong feeling on this. Co-authored-by: Nick Craver --- src/StackExchange.Redis/Enums/GeoUnit.cs | 16 +- src/StackExchange.Redis/Enums/Order.cs | 14 +- src/StackExchange.Redis/Enums/RedisCommand.cs | 2 + src/StackExchange.Redis/GeoEntry.cs | 22 +- src/StackExchange.Redis/GeoSearchShape.cs | 93 ++++ .../Interfaces/IDatabase.cs | 68 +++ .../Interfaces/IDatabaseAsync.cs | 68 +++ .../KeyspaceIsolation/DatabaseWrapper.cs | 12 + .../KeyspaceIsolation/WrapperBase.cs | 12 + src/StackExchange.Redis/Message.cs | 47 ++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 15 + src/StackExchange.Redis/RedisDatabase.cs | 122 ++++- src/StackExchange.Redis/RedisKey.cs | 2 + src/StackExchange.Redis/RedisLiterals.cs | 18 + tests/StackExchange.Redis.Tests/GeoTests.cs | 428 ++++++++++++++++++ tests/StackExchange.Redis.Tests/TestBase.cs | 14 +- 16 files changed, 930 insertions(+), 23 deletions(-) create mode 100644 src/StackExchange.Redis/GeoSearchShape.cs diff --git a/src/StackExchange.Redis/Enums/GeoUnit.cs b/src/StackExchange.Redis/Enums/GeoUnit.cs index 2d1c599c8..3f5104742 100644 --- a/src/StackExchange.Redis/Enums/GeoUnit.cs +++ b/src/StackExchange.Redis/Enums/GeoUnit.cs @@ -1,4 +1,6 @@ -namespace StackExchange.Redis +using System; + +namespace StackExchange.Redis { /// /// Units associated with Geo Commands. @@ -22,4 +24,16 @@ public enum GeoUnit /// Feet, } + + internal static class GeoUnitExtensions + { + internal static RedisValue ToLiteral(this GeoUnit unit) => unit switch + { + GeoUnit.Feet => RedisLiterals.ft, + GeoUnit.Kilometers => RedisLiterals.km, + GeoUnit.Meters => RedisLiterals.m, + GeoUnit.Miles => RedisLiterals.mi, + _ => throw new ArgumentOutOfRangeException(nameof(unit)) + }; + } } diff --git a/src/StackExchange.Redis/Enums/Order.cs b/src/StackExchange.Redis/Enums/Order.cs index 48364a7dd..be3dd0a8b 100644 --- a/src/StackExchange.Redis/Enums/Order.cs +++ b/src/StackExchange.Redis/Enums/Order.cs @@ -1,4 +1,6 @@ -namespace StackExchange.Redis +using System; + +namespace StackExchange.Redis { /// /// The direction in which to sequence elements. @@ -14,4 +16,14 @@ public enum Order /// Descending, } + + internal static class OrderExtensions + { + internal static RedisValue ToLiteral(this Order order) => order switch + { + Order.Ascending => RedisLiterals.ASC, + Order.Descending => RedisLiterals.DESC, + _ => throw new ArgumentOutOfRangeException(nameof(order)) + }; + } } diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index ab016961f..1a78b0458 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -47,6 +47,8 @@ internal enum RedisCommand GEOPOS, GEORADIUS, GEORADIUSBYMEMBER, + GEOSEARCH, + GEOSEARCHSTORE, GET, GETBIT, diff --git a/src/StackExchange.Redis/GeoEntry.cs b/src/StackExchange.Redis/GeoEntry.cs index 2926b9c3d..a7adf8ae1 100644 --- a/src/StackExchange.Redis/GeoEntry.cs +++ b/src/StackExchange.Redis/GeoEntry.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace StackExchange.Redis { @@ -27,7 +28,26 @@ public enum GeoRadiusOptions /// /// Populates the commonly used values from the entry (the integer hash is not returned as it is not commonly useful). /// - Default = WithCoordinates | GeoRadiusOptions.WithDistance + Default = WithCoordinates | WithDistance + } + + internal static class GeoRadiusOptionsExtensions + { + internal static void AddArgs(this GeoRadiusOptions options, List values) + { + if ((options & GeoRadiusOptions.WithCoordinates) != 0) + { + values.Add(RedisLiterals.WITHCOORD); + } + if ((options & GeoRadiusOptions.WithDistance) != 0) + { + values.Add(RedisLiterals.WITHDIST); + } + if ((options & GeoRadiusOptions.WithGeoHash) != 0) + { + values.Add(RedisLiterals.WITHHASH); + } + } } /// diff --git a/src/StackExchange.Redis/GeoSearchShape.cs b/src/StackExchange.Redis/GeoSearchShape.cs new file mode 100644 index 000000000..68f1ee754 --- /dev/null +++ b/src/StackExchange.Redis/GeoSearchShape.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; + +namespace StackExchange.Redis; + +/// +/// A Shape that you can use for a GeoSearch +/// +public abstract class GeoSearchShape +{ + /// + /// The unit to use for creating the shape. + /// + protected GeoUnit Unit { get; } + + /// + /// The number of shape arguments. + /// + internal abstract int ArgCount { get; } + + /// + /// constructs a + /// + /// + public GeoSearchShape(GeoUnit unit) + { + Unit = unit; + } + + internal abstract void AddArgs(List args); +} + +/// +/// A circle drawn on a map bounding +/// +public class GeoSearchCircle : GeoSearchShape +{ + private readonly double _radius; + + /// + /// Creates a Shape. + /// + /// The radius of the circle. + /// The distance unit the circle will use, defaults to Meters. + public GeoSearchCircle(double radius, GeoUnit unit = GeoUnit.Meters) : base (unit) + { + _radius = radius; + } + + internal override int ArgCount => 3; + + /// + /// Gets the s for this shape + /// + /// + internal override void AddArgs(List args) + { + args.Add(RedisLiterals.BYRADIUS); + args.Add(_radius); + args.Add(Unit.ToLiteral()); + } +} + +/// +/// A box drawn on a map +/// +public class GeoSearchBox : GeoSearchShape +{ + private readonly double _height; + + private readonly double _width; + + /// + /// Initializes a GeoBox. + /// + /// The height of the box. + /// The width of the box. + /// The distance unit the box will use, defaults to Meters. + public GeoSearchBox(double height, double width, GeoUnit unit = GeoUnit.Meters) : base(unit) + { + _height = height; + _width = width; + } + + internal override int ArgCount => 4; + + internal override void AddArgs(List args) + { + args.Add(RedisLiterals.BYBOX); + args.Add(_width); + args.Add(_height); + args.Add(Unit.ToLiteral()); + } +} diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index a988c7220..9aba1118a 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -193,6 +193,74 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/georadius GeoRadiusResult[] GeoRadius(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); + /// + /// Return the members of the geo-encoded sorted set stored at bounded by the provided + /// , centered at the provided set . + /// + /// The key of the set. + /// The set member to use as the center of the shape. + /// The shape to use to bound the geo search. + /// The maximum number of results to pull back. + /// Whether or not to terminate the search after finding results. Must be true of count is -1. + /// The order to sort by (defaults to unordered). + /// The search options to use + /// The flags for this operation. + /// The results found within the shape, if any. + /// https://redis.io/commands/geosearch + GeoRadiusResult[] GeoSearch(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); + + /// + /// Return the members of the geo-encoded sorted set stored at bounded by the provided + /// , centered at the point provided by the and . + /// + /// The key of the set. + /// The longitude of the center point. + /// The latitude of the center point. + /// The shape to use to bound the geo search. + /// The maximum number of results to pull back. + /// Whether or not to terminate the search after finding results. Must be true of count is -1. + /// The order to sort by (defaults to unordered). + /// The search options to use + /// The flags for this operation. + /// The results found within the shape, if any. + /// /// https://redis.io/commands/geosearch + GeoRadiusResult[] GeoSearch(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); + + /// + /// Stores the members of the geo-encoded sorted set stored at bounded by the provided + /// , centered at the provided set . + /// + /// The key of the set. + /// The key to store the result at. + /// The set member to use as the center of the shape. + /// The shape to use to bound the geo search. + /// The maximum number of results to pull back. + /// Whether or not to terminate the search after finding results. Must be true of count is -1. + /// The order to sort by (defaults to unordered). + /// If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set. + /// The flags for this operation. + /// The size of the set stored at . + /// https://redis.io/commands/geosearchstore + long GeoSearchAndStore(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); + + /// + /// Stores the members of the geo-encoded sorted set stored at bounded by the provided + /// , centered at the point provided by the and . + /// + /// The key of the set. + /// The key to store the result at. + /// The longitude of the center point. + /// The latitude of the center point. + /// The shape to use to bound the geo search. + /// The maximum number of results to pull back. + /// Whether or not to terminate the search after finding results. Must be true of count is -1. + /// The order to sort by (defaults to unordered). + /// If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set. + /// The flags for this operation. + /// The size of the set stored at . + /// https://redis.io/commands/geosearchstore + long GeoSearchAndStore(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); + /// /// Decrements the number stored at field in the hash stored at key by decrement. /// If key does not exist, a new key holding a hash is created. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 020f06979..74aceb368 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -180,6 +180,74 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/georadius Task GeoRadiusAsync(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); + /// + /// Return the members of the geo-encoded sorted set stored at bounded by the provided + /// , centered at the provided set . + /// + /// The key of the set. + /// The set member to use as the center of the shape. + /// The shape to use to bound the geo search. + /// The maximum number of results to pull back. + /// Whether or not to terminate the search after finding results. Must be true of count is -1. + /// The order to sort by (defaults to unordered). + /// The search options to use. + /// The flags for this operation. + /// The results found within the shape, if any. + /// https://redis.io/commands/geosearch + Task GeoSearchAsync(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); + + /// + /// Return the members of the geo-encoded sorted set stored at bounded by the provided + /// , centered at the point provided by the and . + /// + /// The key of the set. + /// The longitude of the center point. + /// The latitude of the center point. + /// The shape to use to bound the geo search. + /// The maximum number of results to pull back. + /// Whether or not to terminate the search after finding results. Must be true of count is -1. + /// The order to sort by (defaults to unordered). + /// The search options to use. + /// The flags for this operation. + /// The results found within the shape, if any. + /// /// https://redis.io/commands/geosearch + Task GeoSearchAsync(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); + + /// + /// Stores the members of the geo-encoded sorted set stored at bounded by the provided + /// , centered at the provided set . + /// + /// The key of the set. + /// The key to store the result at. + /// The set member to use as the center of the shape. + /// The shape to use to bound the geo search. + /// The maximum number of results to pull back. + /// Whether or not to terminate the search after finding results. Must be true of count is -1. + /// The order to sort by (defaults to unordered). + /// If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set. + /// The flags for this operation. + /// The size of the set stored at . + /// https://redis.io/commands/geosearchstore + Task GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); + + /// + /// Stores the members of the geo-encoded sorted set stored at bounded by the provided + /// , centered at the point provided by the and . + /// + /// The key of the set. + /// The key to store the result at. + /// The longitude of the center point. + /// The latitude of the center point. + /// The shape to use to bound the geo search. + /// The maximum number of results to pull back. + /// Whether or not to terminate the search after finding results. Must be true of count is -1. + /// The order to sort by (defaults to unordered). + /// If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set. + /// The flags for this operation. + /// The size of the set stored at . + /// https://redis.io/commands/geosearchstore + Task GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); + /// /// Decrements the number stored at field in the hash stored at key by decrement. /// If key does not exist, a new key holding a hash is created. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 0b28037d9..3b4448e2b 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -54,6 +54,18 @@ public GeoRadiusResult[] GeoRadius(RedisKey key, RedisValue member, double radiu public GeoRadiusResult[] GeoRadius(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => Inner.GeoRadius(ToInner(key), longitude, latitude, radius, unit, count, order, options, flags); + public GeoRadiusResult[] GeoSearch(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + Inner.GeoSearch(ToInner(key), member, shape, count, demandClosest, order, options, flags); + + public GeoRadiusResult[] GeoSearch(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + Inner.GeoSearch(ToInner(key), longitude, latitude, shape, count, demandClosest, order, options, flags); + + public long GeoSearchAndStore(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) => + Inner.GeoSearchAndStore(ToInner(sourceKey), ToInner(destinationKey), member, shape, count, demandClosest, order, storeDistances, flags); + + public long GeoSearchAndStore(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) => + Inner.GeoSearchAndStore(ToInner(sourceKey), ToInner(destinationKey), longitude, latitude, shape, count, demandClosest, order, storeDistances, flags); + public double HashDecrement(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) => Inner.HashDecrement(ToInner(key), hashField, value, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 033757262..bcbf19aad 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -57,6 +57,18 @@ public Task GeoRadiusAsync(RedisKey key, RedisValue member, d public Task GeoRadiusAsync(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => Inner.GeoRadiusAsync(ToInner(key), longitude, latitude, radius, unit, count, order, options, flags); + public Task GeoSearchAsync(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + Inner.GeoSearchAsync(ToInner(key), member, shape, count, demandClosest, order, options, flags); + + public Task GeoSearchAsync(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + Inner.GeoSearchAsync(ToInner(key), longitude, latitude, shape, count, demandClosest, order, options, flags); + + public Task GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) => + Inner.GeoSearchAndStoreAsync(ToInner(sourceKey), ToInner(destinationKey), member, shape, count, demandClosest, order, storeDistances, flags); + + public Task GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) => + Inner.GeoSearchAndStoreAsync(ToInner(sourceKey), ToInner(destinationKey), longitude, latitude, shape, count, demandClosest, order, storeDistances, flags); + public Task HashDecrementAsync(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None) => Inner.HashDecrementAsync(ToInner(key), hashField, value, flags); diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index f1760f27c..7efd5fe6c 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -509,6 +509,27 @@ internal static Message Create(int db, CommandFlags flags, RedisCommand command, }; } + internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, in RedisKey key1, RedisValue[] values) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(values); +#else + if (values == null) throw new ArgumentNullException(nameof(values)); +#endif + return values.Length switch + { + 0 => new CommandKeyKeyMessage(db, flags, command, key0, key1), + 1 => new CommandKeyKeyValueMessage(db, flags, command, key0, key1, values[0]), + 2 => new CommandKeyKeyValueValueMessage(db, flags, command, key0, key1, values[0], values[1]), + 3 => new CommandKeyKeyValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2]), + 4 => new CommandKeyKeyValueValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2], values[3]), + 5 => new CommandKeyKeyValueValueValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2], values[3], values[4]), + 6 => new CommandKeyKeyValueValueValueValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2], values[3],values[4],values[5]), + 7 => new CommandKeyKeyValueValueValueValueValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2], values[3], values[4], values[5], values[6]), + _ => new CommandKeyKeyValuesMessage(db, flags, command, key0, key1, values), + }; + } + internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, RedisValue[] values, in RedisKey key1) { #if NET6_0_OR_GREATER @@ -1065,6 +1086,32 @@ protected override void WriteImpl(PhysicalConnection physical) public override int ArgCount => values.Length + 1; } + private sealed class CommandKeyKeyValuesMessage : CommandKeyBase + { + private readonly RedisKey key1; + private readonly RedisValue[] values; + public CommandKeyKeyValuesMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisKey key1, RedisValue[] values) : base(db, flags, command, key) + { + for (int i = 0; i < values.Length; i++) + { + values[i].AssertNotNull(); + } + + key1.AssertNotNull(); + this.key1 = key1; + this.values = values; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, values.Length + 2); + physical.Write(Key); + physical.Write(key1); + for (int i = 0; i < values.Length; i++) physical.WriteBulkString(values[i]); + } + public override int ArgCount => values.Length + 1; + } + private sealed class CommandKeyValueValueMessage : CommandKeyBase { private readonly RedisValue value0, value1; diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 56d738de5..b9297aec0 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -406,6 +406,13 @@ StackExchange.Redis.GeoRadiusResult.GeoRadiusResult(in StackExchange.Redis.Redis StackExchange.Redis.GeoRadiusResult.Hash.get -> long? StackExchange.Redis.GeoRadiusResult.Member.get -> StackExchange.Redis.RedisValue StackExchange.Redis.GeoRadiusResult.Position.get -> StackExchange.Redis.GeoPosition? +StackExchange.Redis.GeoSearchBox +StackExchange.Redis.GeoSearchBox.GeoSearchBox(double height, double width, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters) -> void +StackExchange.Redis.GeoSearchCircle +StackExchange.Redis.GeoSearchCircle.GeoSearchCircle(double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters) -> void +StackExchange.Redis.GeoSearchShape +StackExchange.Redis.GeoSearchShape.GeoSearchShape(StackExchange.Redis.GeoUnit unit) -> void +StackExchange.Redis.GeoSearchShape.Unit.get -> StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit.Feet = 3 -> StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit.Kilometers = 1 -> StackExchange.Redis.GeoUnit @@ -492,6 +499,10 @@ StackExchange.Redis.IDatabase.GeoPosition(StackExchange.Redis.RedisKey key, Stac StackExchange.Redis.IDatabase.GeoRadius(StackExchange.Redis.RedisKey key, double longitude, double latitude, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoRadiusResult[]! StackExchange.Redis.IDatabase.GeoRadius(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoRadiusResult[]! StackExchange.Redis.IDatabase.GeoRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.GeoSearch(StackExchange.Redis.RedisKey key, double longitude, double latitude, StackExchange.Redis.GeoSearchShape! shape, int count = -1, bool demandClosest = true, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoRadiusResult[]! +StackExchange.Redis.IDatabase.GeoSearch(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.GeoSearchShape! shape, int count = -1, bool demandClosest = true, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.GeoRadiusResult[]! +StackExchange.Redis.IDatabase.GeoSearchAndStore(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, double longitude, double latitude, StackExchange.Redis.GeoSearchShape! shape, int count = -1, bool demandClosest = true, StackExchange.Redis.Order? order = null, bool storeDistances = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.GeoSearchAndStore(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.RedisValue member, StackExchange.Redis.GeoSearchShape! shape, int count = -1, bool demandClosest = true, StackExchange.Redis.Order? order = null, bool storeDistances = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.HashDecrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double StackExchange.Redis.IDatabase.HashDecrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.HashDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool @@ -698,6 +709,10 @@ StackExchange.Redis.IDatabaseAsync.GeoPositionAsync(StackExchange.Redis.RedisKey StackExchange.Redis.IDatabaseAsync.GeoRadiusAsync(StackExchange.Redis.RedisKey key, double longitude, double latitude, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.GeoRadiusAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double radius, StackExchange.Redis.GeoUnit unit = StackExchange.Redis.GeoUnit.Meters, int count = -1, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.GeoRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoSearchAsync(StackExchange.Redis.RedisKey key, double longitude, double latitude, StackExchange.Redis.GeoSearchShape! shape, int count = -1, bool demandClosest = true, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoSearchAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.GeoSearchShape! shape, int count = -1, bool demandClosest = true, StackExchange.Redis.Order? order = null, StackExchange.Redis.GeoRadiusOptions options = StackExchange.Redis.GeoRadiusOptions.Default, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoSearchAndStoreAsync(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, double longitude, double latitude, StackExchange.Redis.GeoSearchShape! shape, int count = -1, bool demandClosest = true, StackExchange.Redis.Order? order = null, bool storeDistances = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.GeoSearchAndStoreAsync(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.RedisValue member, StackExchange.Redis.GeoSearchShape! shape, int count = -1, bool demandClosest = true, StackExchange.Redis.Order? order = null, bool storeDistances = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashDecrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashDecrementAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index db04c6ba8..adeae6103 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -170,16 +170,57 @@ public Task GeoRemoveAsync(RedisKey key, RedisValue member, CommandFlags f return ExecuteAsync(msg, ResultProcessor.RedisGeoPosition); } - private static readonly RedisValue - WITHCOORD = Encoding.ASCII.GetBytes("WITHCOORD"), - WITHDIST = Encoding.ASCII.GetBytes("WITHDIST"), - WITHHASH = Encoding.ASCII.GetBytes("WITHHASH"), - COUNT = Encoding.ASCII.GetBytes("COUNT"), - ASC = Encoding.ASCII.GetBytes("ASC"), - DESC = Encoding.ASCII.GetBytes("DESC"); + private Message GetGeoSearchMessage(in RedisKey sourceKey, in RedisKey destinationKey, RedisValue? member, double longitude, double latitude, GeoSearchShape shape, int count, bool demandClosest, bool storeDistances, Order? order, GeoRadiusOptions options, CommandFlags flags) + { + var redisValues = new List(15); + if (member != null) + { + redisValues.Add(RedisLiterals.FROMMEMBER); + redisValues.Add(member.Value); + } + else + { + redisValues.Add(RedisLiterals.FROMLONLAT); + redisValues.Add(longitude); + redisValues.Add(latitude); + } + + shape.AddArgs(redisValues); + + if (order != null) + { + redisValues.Add(order.Value.ToLiteral()); + } + if (count >= 0) + { + redisValues.Add(RedisLiterals.COUNT); + redisValues.Add(count); + } + + if (!demandClosest) + { + if (count < 0) + { + throw new ArgumentException($"{nameof(demandClosest)} must be true if you are not limiting the count for a GEOSEARCH"); + } + redisValues.Add(RedisLiterals.ANY); + } + + options.AddArgs(redisValues); + + if (storeDistances) + { + redisValues.Add(RedisLiterals.STOREDIST); + } + + return destinationKey.IsNull + ? Message.Create(Database, flags, RedisCommand.GEOSEARCH, sourceKey, redisValues.ToArray()) + : Message.Create(Database, flags, RedisCommand.GEOSEARCHSTORE, destinationKey, sourceKey, redisValues.ToArray()); + } + private Message GetGeoRadiusMessage(in RedisKey key, RedisValue? member, double longitude, double latitude, double radius, GeoUnit unit, int count, Order? order, GeoRadiusOptions options, CommandFlags flags) { - var redisValues = new List(); + var redisValues = new List(10); RedisCommand command; if (member == null) { @@ -192,24 +233,19 @@ private Message GetGeoRadiusMessage(in RedisKey key, RedisValue? member, double redisValues.Add(member.Value); command = RedisCommand.GEORADIUSBYMEMBER; } + redisValues.Add(radius); - redisValues.Add(StackExchange.Redis.GeoPosition.GetRedisUnit(unit)); - if ((options & GeoRadiusOptions.WithCoordinates) != 0) redisValues.Add(WITHCOORD); - if ((options & GeoRadiusOptions.WithDistance) != 0) redisValues.Add(WITHDIST); - if ((options & GeoRadiusOptions.WithGeoHash) != 0) redisValues.Add(WITHHASH); + redisValues.Add(Redis.GeoPosition.GetRedisUnit(unit)); + options.AddArgs(redisValues); + if (count > 0) { - redisValues.Add(COUNT); + redisValues.Add(RedisLiterals.COUNT); redisValues.Add(count); } if (order != null) { - switch (order.Value) - { - case Order.Ascending: redisValues.Add(ASC); break; - case Order.Descending: redisValues.Add(DESC); break; - default: throw new ArgumentOutOfRangeException(nameof(order)); - } + redisValues.Add(order.Value.ToLiteral()); } return Message.Create(Database, flags, command, key, redisValues.ToArray()); @@ -245,6 +281,54 @@ public Task GeoRadiusAsync(RedisKey key, double longitude, do return ExecuteAsync(GetGeoRadiusMessage(key, null, longitude, latitude, radius, unit, count, order, options, flags), ResultProcessor.GeoRadiusArray(options), defaultValue: Array.Empty()); } + public GeoRadiusResult[] GeoSearch(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) + { + var msg = GetGeoSearchMessage(key, RedisKey.Null, member, double.NaN, double.NaN, shape, count, demandClosest, false, order, options, flags); + return ExecuteSync(msg, ResultProcessor.GeoRadiusArray(options), defaultValue: Array.Empty()); + } + + public GeoRadiusResult[] GeoSearch(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) + { + var msg = GetGeoSearchMessage(key, RedisKey.Null, null, longitude, latitude, shape, count, demandClosest, false, order, options, flags); + return ExecuteSync(msg, ResultProcessor.GeoRadiusArray(options), defaultValue: Array.Empty()); + } + + public Task GeoSearchAsync(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) + { + var msg = GetGeoSearchMessage(key, RedisKey.Null, member, double.NaN, double.NaN, shape, count, demandClosest, false, order, options, flags); + return ExecuteAsync(msg, ResultProcessor.GeoRadiusArray(options), defaultValue: Array.Empty()); + } + + public Task GeoSearchAsync(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) + { + var msg = GetGeoSearchMessage(key, RedisKey.Null, null, longitude, latitude, shape, count, demandClosest, false, order, options, flags); + return ExecuteAsync(msg, ResultProcessor.GeoRadiusArray(options), defaultValue: Array.Empty()); + } + + public long GeoSearchAndStore(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) + { + var msg = GetGeoSearchMessage(sourceKey, destinationKey, member, double.NaN, double.NaN, shape, count, demandClosest, storeDistances, order, GeoRadiusOptions.None, flags); + return ExecuteSync(msg, ResultProcessor.Int64); + } + + public long GeoSearchAndStore(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) + { + var msg = GetGeoSearchMessage(sourceKey, destinationKey, null, longitude, latitude, shape, count, demandClosest, storeDistances, order, GeoRadiusOptions.None, flags); + return ExecuteSync(msg, ResultProcessor.Int64); + } + + public Task GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) + { + var msg = GetGeoSearchMessage(sourceKey, destinationKey, member, double.NaN, double.NaN, shape, count, demandClosest, storeDistances, order, GeoRadiusOptions.None, flags); + return ExecuteAsync(msg, ResultProcessor.Int64); + } + + public Task GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None) + { + var msg = GetGeoSearchMessage(sourceKey, destinationKey, null, longitude, latitude, shape, count, demandClosest, storeDistances, order, GeoRadiusOptions.None, flags); + return ExecuteAsync(msg, ResultProcessor.Int64); + } + public long HashDecrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None) { return HashIncrement(key, hashField, -value, flags); diff --git a/src/StackExchange.Redis/RedisKey.cs b/src/StackExchange.Redis/RedisKey.cs index 5f0091b3a..113a3e646 100644 --- a/src/StackExchange.Redis/RedisKey.cs +++ b/src/StackExchange.Redis/RedisKey.cs @@ -23,6 +23,8 @@ public RedisKey(string? key) : this(null, key) { } internal bool IsNull => KeyPrefix == null && KeyValue == null; + internal static RedisKey Null { get; } = new RedisKey(null, null); + internal bool IsEmpty { get diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 52329f8e5..03c5d68f5 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -46,6 +46,8 @@ public static readonly RedisValue AGGREGATE = "AGGREGATE", ALPHA = "ALPHA", AND = "AND", + ANY = "ANY", + ASC = "ASC", BEFORE = "BEFORE", BY = "BY", BYLEX = "BYLEX", @@ -141,6 +143,22 @@ public static readonly RedisValue PlusSumbol = "+", Wildcard = "*", + // Geo Radius/Search Literals + BYBOX = "BYBOX", + BYRADIUS = "BYRADIUS", + FROMMEMBER = "FROMMEMBER", + FROMLONLAT = "FROMLONLAT", + STOREDIST = "STOREDIST", + WITHCOORD = "WITHCOORD", + WITHDIST = "WITHDIST", + WITHHASH = "WITHHASH", + + // geo units + ft = "ft", + km = "km", + m = "m", + mi = "mi", + // misc (config, etc) databases = "databases", master = "master", diff --git a/tests/StackExchange.Redis.Tests/GeoTests.cs b/tests/StackExchange.Redis.Tests/GeoTests.cs index 71f40d1f2..fa3a3e4a1 100644 --- a/tests/StackExchange.Redis.Tests/GeoTests.cs +++ b/tests/StackExchange.Redis.Tests/GeoTests.cs @@ -210,5 +210,433 @@ public async Task GeoRadiusOverloads() Assert.NotNull(result); } } + + private async Task GeoSearchSetupAsync(RedisKey key, IDatabase db) + { + await db.KeyDeleteAsync(key); + await db.GeoAddAsync(key, 82.6534, 27.7682, "rays"); + await db.GeoAddAsync(key, 79.3891, 43.6418, "blue jays"); + await db.GeoAddAsync(key, 76.6217, 39.2838, "orioles"); + await db.GeoAddAsync(key, 71.0927, 42.3467, "red sox"); + await db.GeoAddAsync(key, 73.9262, 40.8296, "yankees"); + } + + private void GeoSearchSetup(RedisKey key, IDatabase db) + { + db.KeyDelete(key); + db.GeoAdd(key, 82.6534, 27.7682, "rays"); + db.GeoAdd(key, 79.3891, 43.6418, "blue jays"); + db.GeoAdd(key, 76.6217, 39.2838, "orioles"); + db.GeoAdd(key, 71.0927, 42.3467, "red sox"); + db.GeoAdd(key, 73.9262, 40.8296, "yankees"); + } + + [Fact] + public async Task GeoSearchCircleMemberAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await GeoSearchSetupAsync(key, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Miles); + var res = await db.GeoSearchAsync(key, "yankees", circle); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Contains(res, x => x.Member == "blue jays"); + Assert.NotNull(res[0].Distance); + Assert.NotNull(res[0].Position); + Assert.Null(res[0].Hash); + Assert.Equal(4, res.Length); + } + + [Fact] + public async Task GeoSearchCircleMemberAsyncOnlyHash() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await GeoSearchSetupAsync(key, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Miles); + var res = await db.GeoSearchAsync(key, "yankees", circle, options: GeoRadiusOptions.WithGeoHash); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Contains(res, x => x.Member == "blue jays"); + Assert.Null(res[0].Distance); + Assert.Null(res[0].Position); + Assert.NotNull(res[0].Hash); + Assert.Equal(4, res.Length); + } + + [Fact] + public async Task GeoSearchCircleMemberAsyncHashAndDistance() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await GeoSearchSetupAsync(key, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Miles); + var res = await db.GeoSearchAsync(key, "yankees", circle, options: GeoRadiusOptions.WithGeoHash | GeoRadiusOptions.WithDistance); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Contains(res, x => x.Member == "blue jays"); + Assert.NotNull(res[0].Distance); + Assert.Null(res[0].Position); + Assert.NotNull(res[0].Hash); + Assert.Equal(4, res.Length); + } + + [Fact] + public async Task GeoSearchCircleLonLatAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await GeoSearchSetupAsync(key, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Miles); + var res = await db.GeoSearchAsync(key, 73.9262, 40.8296, circle); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Contains(res, x => x.Member == "blue jays"); + Assert.Equal(4, res.Length); + } + + [Fact] + public void GeoSearchCircleMember() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + GeoSearchSetup(key, db); + + var circle = new GeoSearchCircle(500 * 1609); + var res = db.GeoSearch(key, "yankees", circle); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Contains(res, x => x.Member == "blue jays"); + Assert.Equal(4, res.Length); + } + + [Fact] + public void GeoSearchCircleLonLat() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + GeoSearchSetup(key, db); + + var circle = new GeoSearchCircle(500 * 5280, GeoUnit.Feet); + var res = db.GeoSearch(key, 73.9262, 40.8296, circle); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Contains(res, x => x.Member == "blue jays"); + Assert.Equal(4, res.Length); + } + + [Fact] + public async Task GeoSearchBoxMemberAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await GeoSearchSetupAsync(key, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = await db.GeoSearchAsync(key, "yankees", box); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Equal(3, res.Length); + } + + [Fact] + public async Task GeoSearchBoxLonLatAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await GeoSearchSetupAsync(key, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = await db.GeoSearchAsync(key, 73.9262, 40.8296, box); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Equal(3, res.Length); + } + + [Fact] + public void GeoSearchBoxMember() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + GeoSearchSetup(key, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = db.GeoSearch(key, "yankees", box); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Equal(3, res.Length); + } + + [Fact] + public void GeoSearchBoxLonLat() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + GeoSearchSetup(key, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = db.GeoSearch(key, 73.9262, 40.8296, box); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Equal(3, res.Length); + } + + [Fact] + public void GeoSearchLimitCount() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + GeoSearchSetup(key, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = db.GeoSearch(key, 73.9262, 40.8296, box, count: 2); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Equal(2, res.Length); + } + + [Fact] + public void GeoSearchLimitCountMakeNoDemands() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + GeoSearchSetup(key, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = db.GeoSearch(key, 73.9262, 40.8296, box, count: 2, demandClosest: false); + Assert.Contains(res, x => x.Member == "red sox"); // this order MIGHT not be fully deterministic, seems to work for our purposes. + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Equal(2, res.Length); + } + + [Fact] + public async Task GeoSearchBoxLonLatDescending() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await GeoSearchSetupAsync(key, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = await db.GeoSearchAsync(key, 73.9262, 40.8296, box, order: Order.Descending); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Equal(3, res.Length); + Assert.Equal("red sox", res[0].Member); + } + + [Fact] + public async Task GeoSearchBoxMemberAndStoreAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var me = Me(); + var db = conn.GetDatabase(); + RedisKey sourceKey = $"{me}:source"; + RedisKey destinationKey = $"{me}:destination"; + await db.KeyDeleteAsync(destinationKey); + await GeoSearchSetupAsync(sourceKey, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = await db.GeoSearchAndStoreAsync(sourceKey, destinationKey, "yankees", box); + var set = await db.GeoSearchAsync(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); + Assert.Contains(set, x => x.Member == "yankees"); + Assert.Contains(set, x => x.Member == "red sox"); + Assert.Contains(set, x => x.Member == "orioles"); + Assert.Equal(3, set.Length); + Assert.Equal(3, res); + } + + [Fact] + public async Task GeoSearchBoxLonLatAndStoreAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var me = Me(); + var db = conn.GetDatabase(); + RedisKey sourceKey = $"{me}:source"; + RedisKey destinationKey = $"{me}:destination"; + await db.KeyDeleteAsync(destinationKey); + await GeoSearchSetupAsync(sourceKey, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = await db.GeoSearchAndStoreAsync(sourceKey, destinationKey, 73.9262, 40.8296, box); + var set = await db.GeoSearchAsync(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); + Assert.Contains(set, x => x.Member == "yankees"); + Assert.Contains(set, x => x.Member == "red sox"); + Assert.Contains(set, x => x.Member == "orioles"); + Assert.Equal(3, set.Length); + Assert.Equal(3, res); + } + + [Fact] + public async Task GeoSearchCircleMemberAndStoreAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var me = Me(); + var db = conn.GetDatabase(); + RedisKey sourceKey = $"{me}:source"; + RedisKey destinationKey = $"{me}:destination"; + await db.KeyDeleteAsync(destinationKey); + await GeoSearchSetupAsync(sourceKey, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); + var res = await db.GeoSearchAndStoreAsync(sourceKey, destinationKey, "yankees", circle); + var set = await db.GeoSearchAsync(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); + Assert.Contains(set, x => x.Member == "yankees"); + Assert.Contains(set, x => x.Member == "red sox"); + Assert.Contains(set, x => x.Member == "orioles"); + Assert.Equal(3, set.Length); + Assert.Equal(3, res); + } + + [Fact] + public async Task GeoSearchCircleLonLatAndStoreAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var me = Me(); + var db = conn.GetDatabase(); + RedisKey sourceKey = $"{me}:source"; + RedisKey destinationKey = $"{me}:destination"; + await db.KeyDeleteAsync(destinationKey); + await GeoSearchSetupAsync(sourceKey, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); + var res = await db.GeoSearchAndStoreAsync(sourceKey, destinationKey, 73.9262, 40.8296, circle); + var set = await db.GeoSearchAsync(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); + Assert.Contains(set, x => x.Member == "yankees"); + Assert.Contains(set, x => x.Member == "red sox"); + Assert.Contains(set, x => x.Member == "orioles"); + Assert.Equal(3, set.Length); + Assert.Equal(3, res); + } + + [Fact] + public void GeoSearchCircleMemberAndStore() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var me = Me(); + var db = conn.GetDatabase(); + RedisKey sourceKey = $"{me}:source"; + RedisKey destinationKey = $"{me}:destination"; + db.KeyDelete(destinationKey); + GeoSearchSetup(sourceKey, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); + var res = db.GeoSearchAndStore(sourceKey, destinationKey, "yankees", circle); + var set = db.GeoSearch(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); + Assert.Contains(set, x => x.Member == "yankees"); + Assert.Contains(set, x => x.Member == "red sox"); + Assert.Contains(set, x => x.Member == "orioles"); + Assert.Equal(3, set.Length); + Assert.Equal(3, res); + } + + [Fact] + public void GeoSearchCircleLonLatAndStore() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var me = Me(); + var db = conn.GetDatabase(); + RedisKey sourceKey = $"{me}:source"; + RedisKey destinationKey = $"{me}:destination"; + db.KeyDelete(destinationKey); + GeoSearchSetup(sourceKey, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); + var res = db.GeoSearchAndStore(sourceKey, destinationKey, 73.9262, 40.8296, circle); + var set = db.GeoSearch(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); + Assert.Contains(set, x => x.Member == "yankees"); + Assert.Contains(set, x => x.Member == "red sox"); + Assert.Contains(set, x => x.Member == "orioles"); + Assert.Equal(3, set.Length); + Assert.Equal(3, res); + } + + [Fact] + public void GeoSearchCircleAndStoreDistOnly() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var me = Me(); + var db = conn.GetDatabase(); + RedisKey sourceKey = $"{me}:source"; + RedisKey destinationKey = $"{me}:destination"; + db.KeyDelete(destinationKey); + GeoSearchSetup(sourceKey, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); + var res = db.GeoSearchAndStore(sourceKey, destinationKey, 73.9262, 40.8296, circle, storeDistances: true); + var set = db.SortedSetRangeByRankWithScores(destinationKey); + Assert.Contains(set, x => x.Element == "yankees"); + Assert.Contains(set, x => x.Element == "red sox"); + Assert.Contains(set, x => x.Element == "orioles"); + Assert.InRange(Array.Find(set, x => x.Element == "yankees").Score, 0, .2); + Assert.InRange(Array.Find(set, x => x.Element == "orioles").Score, 286, 287); + Assert.InRange(Array.Find(set, x => x.Element == "red sox").Score, 289, 290); + Assert.Equal(3, set.Length); + Assert.Equal(3, res); + } + + [Fact] + public void GeoSearchBadArgs() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key); + var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); + var exception = Assert.Throws(() => + db.GeoSearch(key, "irrelevant", circle, demandClosest: false)); + + Assert.Contains("demandClosest must be true if you are not limiting the count for a GEOSEARCH", + exception.Message); + } } } diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index b47c2f507..70a98ded7 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -265,6 +265,7 @@ internal virtual IInternalConnectionMultiplexer Create( bool shared = true, int? defaultDatabase = null, BacklogPolicy? backlogPolicy = null, + Version? require = null, [CallerMemberName] string? caller = null) { if (Output == null) @@ -290,7 +291,12 @@ internal virtual IInternalConnectionMultiplexer Create( { configuration = GetConfiguration(); if (configuration == _fixture.Configuration) - { // only if the + { + // Only return if we match + if (require != null) + { + Skip.IfBelow(_fixture.Connection, require); + } return _fixture.Connection; } } @@ -306,6 +312,12 @@ internal virtual IInternalConnectionMultiplexer Create( logTransactionData, defaultDatabase, backlogPolicy, caller); + + if (require != null) + { + Skip.IfBelow(muxer, require); + } + muxer.InternalError += OnInternalError; muxer.ConnectionFailed += OnConnectionFailed; muxer.ConnectionRestored += (s, e) => Log($"Connection Restored ({e.ConnectionType},{e.FailureType}): {e.Exception}"); From 8dc6456ccb32f33cd5cd922e881561e7bea9d991 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 15 Apr 2022 20:22:22 -0400 Subject: [PATCH 133/435] Release notes for #2089 --- docs/ReleaseNotes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 1ddfeab3a..8d59c8e2b 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -18,6 +18,8 @@ - Fix: For streams, properly hash `XACK`, `XCLAIM`, and `XPENDING` in cluster scenarios to eliminate `MOVED` retries ([#2085 by nielsderdaele](https://github.com/StackExchange/StackExchange.Redis/pull/2085)) - Adds: Support for `OBJECT REFCOUNT` with `.KeyRefCount()`/`.KeyRefCountAsync()` ([#2087 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2087)) - Adds: Support for `OBJECT ENCODING` with `.KeyEncoding()`/`.KeyEncodingAsync()` ([#2088 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2088)) +- Adds: Support for `GEOSEARCH` with `.GeoSearch()`/`.GeoSearchAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089)) +- Adds: Support for `GEOSEARCHSTORE` with `.GeoSearchAndStore()`/`.GeoSearchAndStoreAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089)) - Adds: Support for `HRANDFIELD` with `.HashRandomField()`/`.HashRandomFieldAsync()`, `.HashRandomFields()`/`.HashRandomFieldsAsync()`, and `.HashRandomFieldsWithValues()`/`.HashRandomFieldsWithValuesAsync()` ([#2090 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2090)) ## 2.5.61 From bcc647bbc40f918638e6d5058ea4893f576785a6 Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Sat, 16 Apr 2022 05:15:05 +0300 Subject: [PATCH 134/435] Support EXPIRETIME and PEXPIRETIME (#2083) Adds support for https://redis.io/commands/expiretime/ https://redis.io/commands/pexpiretime/ (#2055) Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 2 + src/StackExchange.Redis/Enums/ExpireWhen.cs | 42 +++++++ src/StackExchange.Redis/Enums/RedisCommand.cs | 2 + .../Interfaces/IDatabase.cs | 44 ++++++- .../Interfaces/IDatabaseAsync.cs | 50 +++++++- .../KeyspaceIsolation/DatabaseWrapper.cs | 9 ++ .../KeyspaceIsolation/WrapperBase.cs | 9 ++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 12 ++ src/StackExchange.Redis/RedisDatabase.cs | 107 ++++++++++++------ src/StackExchange.Redis/RedisLiterals.cs | 2 + src/StackExchange.Redis/ResultProcessor.cs | 32 ++++++ .../DatabaseWrapperTests.cs | 23 ++++ tests/StackExchange.Redis.Tests/Expiry.cs | 91 +++++++++++++++ .../WrapperBaseTests.cs | 23 ++++ 14 files changed, 410 insertions(+), 38 deletions(-) create mode 100644 src/StackExchange.Redis/Enums/ExpireWhen.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 8d59c8e2b..857cd5166 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -15,6 +15,8 @@ - Adds: Support for `SINTERCARD` with `.SetIntersectionLength()`/`.SetIntersectionLengthAsync()` ([#2078 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2078)) - Adds: Support for `LPOS` with `.ListPosition()`/`.ListPositionAsync()` and `.ListPositions()`/`.ListPositionsAsync()` ([#2080 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2080)) - Adds: Support for `ZMSCORE` with `.SortedSetScores()`/.`SortedSetScoresAsync()` ([#2082 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2082)) +- Adds: Support for `NX | XX | GT | LT` to `EXPIRE`, `EXPIREAT`, `PEXPIRE`, and `PEXPIREAT` with `.KeyExpire()`/`.KeyExpireAsync()` ([#2083 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2083)) +- Adds: Support for `EXPIRETIME`, and `PEXPIRETIME` with `.KeyExpireTime()`/`.KeyExpireTimeAsync()` ([#2083 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2083)) - Fix: For streams, properly hash `XACK`, `XCLAIM`, and `XPENDING` in cluster scenarios to eliminate `MOVED` retries ([#2085 by nielsderdaele](https://github.com/StackExchange/StackExchange.Redis/pull/2085)) - Adds: Support for `OBJECT REFCOUNT` with `.KeyRefCount()`/`.KeyRefCountAsync()` ([#2087 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2087)) - Adds: Support for `OBJECT ENCODING` with `.KeyEncoding()`/`.KeyEncodingAsync()` ([#2088 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2088)) diff --git a/src/StackExchange.Redis/Enums/ExpireWhen.cs b/src/StackExchange.Redis/Enums/ExpireWhen.cs new file mode 100644 index 000000000..7e6552a40 --- /dev/null +++ b/src/StackExchange.Redis/Enums/ExpireWhen.cs @@ -0,0 +1,42 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// Specifies when to set the expiry for a key. +/// +public enum ExpireWhen +{ + /// + /// Set expiry whether or not there is an existing expiry. + /// + Always, + /// + /// Set expiry only when the new expiry is greater than current one. + /// + GreaterThanCurrentExpiry, + /// + /// Set expiry only when the key has an existing expiry. + /// + HasExpiry, + /// + /// Set expiry only when the key has no expiry. + /// + HasNoExpiry, + /// + /// Set expiry only when the new expiry is less than current one + /// + LessThanCurrentExpiry, +} + +internal static class ExpiryOptionExtensions +{ + public static RedisValue ToLiteral(this ExpireWhen op) => op switch + { + ExpireWhen.HasNoExpiry => RedisLiterals.NX, + ExpireWhen.HasExpiry => RedisLiterals.XX, + ExpireWhen.GreaterThanCurrentExpiry => RedisLiterals.GT, + ExpireWhen.LessThanCurrentExpiry => RedisLiterals.LT, + _ => throw new ArgumentOutOfRangeException(nameof(op)) + }; +} diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 1a78b0458..f7ceb8a1b 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -37,6 +37,7 @@ internal enum RedisCommand EXISTS, EXPIRE, EXPIREAT, + EXPIRETIME, FLUSHALL, FLUSHDB, @@ -110,6 +111,7 @@ internal enum RedisCommand PERSIST, PEXPIRE, PEXPIREAT, + PEXPIRETIME, PFADD, PFCOUNT, PFMERGE, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 9aba1118a..4c42fa498 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -641,7 +641,8 @@ public interface IDatabase : IRedis, IDatabaseAsync long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Set a timeout on key. After the timeout has expired, the key will automatically be deleted. + /// Set a timeout on . + /// After the timeout has expired, the key will automatically be deleted. /// A key with an associated timeout is said to be volatile in Redis terminology. /// /// The key to set the expiration for. @@ -665,7 +666,22 @@ public interface IDatabase : IRedis, IDatabaseAsync bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); /// - /// Set a timeout on key. After the timeout has expired, the key will automatically be deleted. + /// Set a timeout on . + /// After the timeout has expired, the key will automatically be deleted. + /// A key with an associated timeout is said to be volatile in Redis terminology. + /// + /// The key to set the expiration for. + /// The timeout to set. + /// Since Redis 7.0.0, you can choose under which condition the expiration will be set using . + /// The flags to use for this operation. + /// if the timeout was set. if key does not exist or the timeout could not be set. + /// https://redis.io/commands/expire + /// https://redis.io/commands/pexpire + bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None); + + /// + /// Set a timeout on . + /// After the timeout has expired, the key will automatically be deleted. /// A key with an associated timeout is said to be volatile in Redis terminology. /// /// The key to set the expiration for. @@ -688,6 +704,30 @@ public interface IDatabase : IRedis, IDatabaseAsync /// https://redis.io/commands/persist bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None); + /// + /// Set a timeout on . + /// After the timeout has expired, the key will automatically be deleted. + /// A key with an associated timeout is said to be volatile in Redis terminology. + /// + /// The key to set the expiration for. + /// The timeout to set. + /// Since Redis 7.0.0, you can choose under which condition the expiration will be set using . + /// The flags to use for this operation. + /// if the timeout was set. if key does not exist or the timeout could not be set. + /// https://redis.io/commands/expire + /// https://redis.io/commands/pexpire + bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the absolute time at which the given will expire, if it exists and has an expiration. + /// + /// The key to get the expiration for. + /// The flags to use for this operation. + /// The time at which the given key will expire, or if the key does not exist or has no associated expiration time. + /// https://redis.io/commands/expiretime + /// https://redis.io/commands/pexpiretime + DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None); + /// /// Returns the time since the object stored at the specified key is idle (not requested by read or write operations). /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 74aceb368..a54681ce5 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -617,7 +617,8 @@ public interface IDatabaseAsync : IRedisAsync Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Set a timeout on key. After the timeout has expired, the key will automatically be deleted. + /// Set a timeout on . + /// After the timeout has expired, the key will automatically be deleted. /// A key with an associated timeout is said to be volatile in Redis terminology. /// /// The key to set the expiration for. @@ -632,7 +633,8 @@ public interface IDatabaseAsync : IRedisAsync /// /// /// Since Redis 2.1.3, you can update the timeout of a key. - /// It is also possible to remove the timeout using the PERSIST command. See the page on key expiry for more information. + /// It is also possible to remove the timeout using the PERSIST command. + /// See the page on key expiry for more information. /// /// /// https://redis.io/commands/expire @@ -641,7 +643,22 @@ public interface IDatabaseAsync : IRedisAsync Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); /// - /// Set a timeout on key. After the timeout has expired, the key will automatically be deleted. + /// Set a timeout on . + /// After the timeout has expired, the key will automatically be deleted. + /// A key with an associated timeout is said to be volatile in Redis terminology. + /// + /// The key to set the expiration for. + /// The timeout to set. + /// Since Redis 7.0.0, you can choose under which condition the expiration will be set using . + /// The flags to use for this operation. + /// if the timeout was set. if key does not exist or the timeout could not be set. + /// https://redis.io/commands/expire + /// https://redis.io/commands/pexpire + Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None); + + /// + /// Set a timeout on . + /// After the timeout has expired, the key will automatically be deleted. /// A key with an associated timeout is said to be volatile in Redis terminology. /// /// The key to set the expiration for. @@ -656,7 +673,8 @@ public interface IDatabaseAsync : IRedisAsync /// /// /// Since Redis 2.1.3, you can update the timeout of a key. - /// It is also possible to remove the timeout using the PERSIST command. See the page on key expiry for more information. + /// It is also possible to remove the timeout using the PERSIST command. + /// See the page on key expiry for more information. /// /// /// https://redis.io/commands/expireat @@ -664,6 +682,30 @@ public interface IDatabaseAsync : IRedisAsync /// https://redis.io/commands/persist Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None); + /// + /// Set a timeout on . + /// After the timeout has expired, the key will automatically be deleted. + /// A key with an associated timeout is said to be volatile in Redis terminology. + /// + /// The key to set the expiration for. + /// The timeout to set. + /// Since Redis 7.0.0, you can choose under which condition the expiration will be set using . + /// The flags to use for this operation. + /// if the timeout was set. if key does not exist or the timeout could not be set. + /// https://redis.io/commands/expire + /// https://redis.io/commands/pexpire + Task KeyExpireAsync(RedisKey key, DateTime? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the absolute time at which the given will expire, if it exists and has an expiration. + /// + /// The key to get the expiration for. + /// The flags to use for this operation. + /// The time at which the given key will expire, or if the key does not exist or has no associated expiration time. + /// https://redis.io/commands/expiretime + /// https://redis.io/commands/pexpiretime + Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// /// Returns the time since the object stored at the specified key is idle (not requested by read or write operations). /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 3b4448e2b..7def559b2 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -170,9 +170,18 @@ public long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None) = public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) => Inner.KeyExpire(ToInner(key), expiry, flags); + public bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) => + Inner.KeyExpire(ToInner(key), expiry, when, flags); + public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => Inner.KeyExpire(ToInner(key), expiry, flags); + public bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) => + Inner.KeyExpire(ToInner(key), expiry, when, flags); + + public DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyExpireTime(ToInner(key), flags); + public TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.KeyIdleTime(ToInner(key), flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index bcbf19aad..5a1437e33 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -181,9 +181,18 @@ public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFl public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) => Inner.KeyExpireAsync(ToInner(key), expiry, flags); + public Task KeyExpireAsync(RedisKey key, DateTime? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) => + Inner.KeyExpireAsync(ToInner(key), expiry, when, flags); + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => Inner.KeyExpireAsync(ToInner(key), expiry, flags); + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) => + Inner.KeyExpireAsync(ToInner(key), expiry, when, flags); + + public Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyExpireTimeAsync(ToInner(key), flags); + public Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.KeyIdleTimeAsync(ToInner(key), flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index b9297aec0..7b4dfb2df 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -367,6 +367,12 @@ StackExchange.Redis.Exclude.Both = StackExchange.Redis.Exclude.Start | StackExch StackExchange.Redis.Exclude.None = 0 -> StackExchange.Redis.Exclude StackExchange.Redis.Exclude.Start = 1 -> StackExchange.Redis.Exclude StackExchange.Redis.Exclude.Stop = 2 -> StackExchange.Redis.Exclude +StackExchange.Redis.ExpireWhen +StackExchange.Redis.ExpireWhen.Always = 0 -> StackExchange.Redis.ExpireWhen +StackExchange.Redis.ExpireWhen.GreaterThanCurrentExpiry = 1 -> StackExchange.Redis.ExpireWhen +StackExchange.Redis.ExpireWhen.HasExpiry = 2 -> StackExchange.Redis.ExpireWhen +StackExchange.Redis.ExpireWhen.HasNoExpiry = 3 -> StackExchange.Redis.ExpireWhen +StackExchange.Redis.ExpireWhen.LessThanCurrentExpiry = 4 -> StackExchange.Redis.ExpireWhen StackExchange.Redis.ExponentialRetry StackExchange.Redis.ExponentialRetry.ExponentialRetry(int deltaBackOffMilliseconds) -> void StackExchange.Redis.ExponentialRetry.ExponentialRetry(int deltaBackOffMilliseconds, int maxDeltaBackOffMilliseconds) -> void @@ -540,7 +546,10 @@ StackExchange.Redis.IDatabase.KeyEncoding(StackExchange.Redis.RedisKey key, Stac StackExchange.Redis.IDatabase.KeyExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyExists(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.ExpireWhen when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.ExpireWhen when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.KeyExpireTime(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.DateTime? StackExchange.Redis.IDatabase.KeyIdleTime(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.TimeSpan? StackExchange.Redis.IDatabase.KeyMigrate(StackExchange.Redis.RedisKey key, System.Net.EndPoint! toServer, int toDatabase = 0, int timeoutMilliseconds = 0, StackExchange.Redis.MigrateOptions migrateOptions = StackExchange.Redis.MigrateOptions.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IDatabase.KeyMove(StackExchange.Redis.RedisKey key, int database, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool @@ -750,7 +759,10 @@ StackExchange.Redis.IDatabaseAsync.KeyEncodingAsync(StackExchange.Redis.RedisKey StackExchange.Redis.IDatabaseAsync.KeyExistsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyExistsAsync(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.ExpireWhen when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.ExpireWhen when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyExpireTimeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyIdleTimeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyMigrateAsync(StackExchange.Redis.RedisKey key, System.Net.EndPoint! toServer, int toDatabase = 0, int timeoutMilliseconds = 0, StackExchange.Redis.MigrateOptions migrateOptions = StackExchange.Redis.MigrateOptions.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyMoveAsync(StackExchange.Redis.RedisKey key, int database, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index adeae6103..500fcc963 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -829,30 +829,54 @@ public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFl return ExecuteAsync(msg, ResultProcessor.Int64); } - public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) + public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => + KeyExpire(key, expiry, ExpireWhen.Always, flags); + + public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) => + KeyExpire(key, expiry, ExpireWhen.Always, flags); + + public bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) { - var msg = GetExpiryMessage(key, flags, expiry, out ServerEndPoint? server); + var msg = GetExpiryMessage(key, flags, expiry, when, out ServerEndPoint? server); return ExecuteSync(msg, ResultProcessor.Boolean, server: server); } - public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) + public bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) { - var msg = GetExpiryMessage(key, flags, expiry, out ServerEndPoint? server); + var msg = GetExpiryMessage(key, flags, expiry, when, out ServerEndPoint? server); return ExecuteSync(msg, ResultProcessor.Boolean, server: server); } - public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => + KeyExpireAsync(key, expiry, ExpireWhen.Always, flags); + + public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) => + KeyExpireAsync(key, expiry, ExpireWhen.Always, flags); + + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) { - var msg = GetExpiryMessage(key, flags, expiry, out ServerEndPoint? server); + var msg = GetExpiryMessage(key, flags, expiry, when, out ServerEndPoint? server); return ExecuteAsync(msg, ResultProcessor.Boolean, server: server); } - public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) + public Task KeyExpireAsync(RedisKey key, DateTime? expire, ExpireWhen when, CommandFlags flags = CommandFlags.None) { - var msg = GetExpiryMessage(key, flags, expiry, out ServerEndPoint? server); + var msg = GetExpiryMessage(key, flags, expire, when, out ServerEndPoint? server); return ExecuteAsync(msg, ResultProcessor.Boolean, server: server); } + public DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.PEXPIRETIME, key); + return ExecuteSync(msg, ResultProcessor.NullableDateTimeFromMilliseconds); + } + + public Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.PEXPIRETIME, key); + return ExecuteAsync(msg, ResultProcessor.NullableDateTimeFromMilliseconds); + } + public TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.OBJECT, RedisLiterals.IDLETIME, key); @@ -3043,48 +3067,67 @@ private Message GetCopyMessage(in RedisKey sourceKey, RedisKey destinationKey, i _ => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.DB, destinationDatabase), }; - private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, TimeSpan? expiry, out ServerEndPoint? server) + private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, TimeSpan? expiry, ExpireWhen when, out ServerEndPoint? server) { - TimeSpan duration; - if (expiry == null || (duration = expiry.Value) == TimeSpan.MaxValue) + if (expiry is null || expiry.Value == TimeSpan.MaxValue) { server = null; - return Message.Create(Database, flags, RedisCommand.PERSIST, key); - } - long milliseconds = duration.Ticks / TimeSpan.TicksPerMillisecond; - if ((milliseconds % 1000) != 0) - { - var features = GetFeatures(key, flags, out server); - if (server != null && features.MillisecondExpiry && multiplexer.CommandMap.IsAvailable(RedisCommand.PEXPIRE)) + return when switch { - return Message.Create(Database, flags, RedisCommand.PEXPIRE, key, milliseconds); - } + ExpireWhen.Always => Message.Create(Database, flags, RedisCommand.PERSIST, key), + _ => throw new ArgumentException("PERSIST cannot be used with when.") + }; } - server = null; - long seconds = milliseconds / 1000; - return Message.Create(Database, flags, RedisCommand.EXPIRE, key, seconds); + + long milliseconds = expiry.Value.Ticks / TimeSpan.TicksPerMillisecond; + return GetExpiryMessage(key, RedisCommand.PEXPIRE, RedisCommand.EXPIRE, milliseconds, when, flags, out server); } - private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, DateTime? expiry, out ServerEndPoint? server) + private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, DateTime? expiry, ExpireWhen when, out ServerEndPoint? server) { - DateTime when; - if (expiry == null || (when = expiry.Value) == DateTime.MaxValue) + if (expiry is null || expiry == DateTime.MaxValue) { server = null; - return Message.Create(Database, flags, RedisCommand.PERSIST, key); + return when switch + { + ExpireWhen.Always => Message.Create(Database, flags, RedisCommand.PERSIST, key), + _ => throw new ArgumentException("PERSIST cannot be used with when.") + }; } - long milliseconds = GetMillisecondsUntil(when); + + long milliseconds = GetMillisecondsUntil(expiry.Value); + return GetExpiryMessage(key, RedisCommand.PEXPIREAT, RedisCommand.EXPIREAT, milliseconds, when, flags, out server); + } + + private Message GetExpiryMessage(in RedisKey key, + RedisCommand millisecondsCommand, + RedisCommand secondsCommand, + long milliseconds, + ExpireWhen when, + CommandFlags flags, + out ServerEndPoint? server) + { + server = null; if ((milliseconds % 1000) != 0) { var features = GetFeatures(key, flags, out server); - if (server != null && features.MillisecondExpiry && multiplexer.CommandMap.IsAvailable(RedisCommand.PEXPIREAT)) + if (server is not null && features.MillisecondExpiry && multiplexer.CommandMap.IsAvailable(millisecondsCommand)) { - return Message.Create(Database, flags, RedisCommand.PEXPIREAT, key, milliseconds); + return when switch + { + ExpireWhen.Always => Message.Create(Database, flags, millisecondsCommand, key, milliseconds), + _ => Message.Create(Database, flags, millisecondsCommand, key, milliseconds, when.ToLiteral()) + }; } + server = null; } - server = null; + long seconds = milliseconds / 1000; - return Message.Create(Database, flags, RedisCommand.EXPIREAT, key, seconds); + return when switch + { + ExpireWhen.Always => Message.Create(Database, flags, secondsCommand, key, seconds), + _ => Message.Create(Database, flags, secondsCommand, key, seconds, when.ToLiteral()) + }; } private Message? GetHashSetMessage(RedisKey key, HashEntry[] hashFields, CommandFlags flags) diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 03c5d68f5..3b405ae8b 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -65,6 +65,7 @@ public static readonly RedisValue FLUSH = "FLUSH", GET = "GET", GETNAME = "GETNAME", + GT = "GT", HISTORY = "HISTORY", ID = "ID", IDLETIME = "IDLETIME", @@ -75,6 +76,7 @@ public static readonly RedisValue LIMIT = "LIMIT", LIST = "LIST", LOAD = "LOAD", + LT = "LT", MATCH = "MATCH", MALLOC_STATS = "MALLOC-STATS", MAX = "MAX", diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 4f611354d..fedaa1982 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -41,6 +41,10 @@ public static readonly ResultProcessor public static readonly ResultProcessor DateTime = new DateTimeProcessor(); + public static readonly ResultProcessor + NullableDateTimeFromMilliseconds = new NullableDateTimeProcessor(fromMilliseconds: true), + NullableDateTimeFromSeconds = new NullableDateTimeProcessor(fromMilliseconds: false); + public static readonly ResultProcessor Double = new DoubleProcessor(); public static readonly ResultProcessor>[]> @@ -969,6 +973,34 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + public sealed class NullableDateTimeProcessor : ResultProcessor + { + private readonly bool isMilliseconds; + public NullableDateTimeProcessor(bool fromMilliseconds) => isMilliseconds = fromMilliseconds; + + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Type) + { + case ResultType.Integer when result.TryGetInt64(out var duration): + DateTime? expiry = duration switch + { + // -1 means no expiry and -2 means key does not exist + < 0 => null, + _ when isMilliseconds => RedisBase.UnixEpoch.AddMilliseconds(duration), + _ => RedisBase.UnixEpoch.AddSeconds(duration) + }; + SetResult(message, expiry); + return true; + + case ResultType.BulkString when result.IsNull: + SetResult(message, null); + return true; + } + return false; + } + } + private sealed class DoubleProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 0c6e41b97..5d5b1b6b2 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -296,6 +296,29 @@ public void KeyExpire_2() mock.Verify(_ => _.KeyExpire("prefix:key", expiry, CommandFlags.None)); } + [Fact] + public void KeyExpire_3() + { + TimeSpan expiry = TimeSpan.FromSeconds(123); + wrapper.KeyExpire("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); + mock.Verify(_ => _.KeyExpire("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); + } + + [Fact] + public void KeyExpire_4() + { + DateTime expiry = DateTime.Now; + wrapper.KeyExpire("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); + mock.Verify(_ => _.KeyExpire("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); + } + + [Fact] + public void KeyExpireTime() + { + wrapper.KeyExpireTime("key", CommandFlags.None); + mock.Verify(_ => _.KeyExpireTime("prefix:key", CommandFlags.None)); + } + [Fact] public void KeyMigrate() { diff --git a/tests/StackExchange.Redis.Tests/Expiry.cs b/tests/StackExchange.Redis.Tests/Expiry.cs index 7902c7a60..922dbaf91 100644 --- a/tests/StackExchange.Redis.Tests/Expiry.cs +++ b/tests/StackExchange.Redis.Tests/Expiry.cs @@ -49,6 +49,35 @@ public async Task TestBasicExpiryTimeSpan(bool disablePTimes) } } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestExpiryOptions(bool disablePTimes) + { + using var muxer = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); + + var key = Me(); + var conn = muxer.GetDatabase(); + conn.KeyDelete(key, CommandFlags.FireAndForget); + conn.StringSet(key, "value", flags: CommandFlags.FireAndForget); + + // The key has no expiry + Assert.False(conn.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasExpiry)); + Assert.True(conn.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasNoExpiry)); + + // The key has an existing expiry + Assert.True(conn.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasExpiry)); + Assert.False(conn.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasNoExpiry)); + + // Set only when the new expiry is greater than current one + Assert.True(conn.KeyExpire(key, TimeSpan.FromHours(1.5), ExpireWhen.GreaterThanCurrentExpiry)); + Assert.False(conn.KeyExpire(key, TimeSpan.FromHours(0.5), ExpireWhen.GreaterThanCurrentExpiry)); + + // Set only when the new expiry is less than current one + Assert.True(conn.KeyExpire(key, TimeSpan.FromHours(0.5), ExpireWhen.LessThanCurrentExpiry)); + Assert.False(conn.KeyExpire(key, TimeSpan.FromHours(1.5), ExpireWhen.LessThanCurrentExpiry)); + } + [Theory] [InlineData(true, true)] [InlineData(false, true)] @@ -104,5 +133,67 @@ public async Task TestBasicExpiryDateTime(bool disablePTimes, bool utc) Assert.Null(await f); } } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void KeyExpiryTime(bool disablePTimes) + { + using var muxer = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); + + var key = Me(); + var conn = muxer.GetDatabase(); + conn.KeyDelete(key, CommandFlags.FireAndForget); + + var expireTime = DateTime.UtcNow.AddHours(1); + conn.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + conn.KeyExpire(key, expireTime, CommandFlags.FireAndForget); + + var time = conn.KeyExpireTime(key); + Assert.NotNull(time); + Assert.Equal(expireTime, time.Value, TimeSpan.FromSeconds(30)); + + // Without associated expiration time + conn.KeyDelete(key, CommandFlags.FireAndForget); + conn.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + time = conn.KeyExpireTime(key); + Assert.Null(time); + + // Non existing key + conn.KeyDelete(key, CommandFlags.FireAndForget); + time = conn.KeyExpireTime(key); + Assert.Null(time); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task KeyExpiryTimeAsync(bool disablePTimes) + { + using var muxer = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); + + var key = Me(); + var conn = muxer.GetDatabase(); + conn.KeyDelete(key, CommandFlags.FireAndForget); + + var expireTime = DateTime.UtcNow.AddHours(1); + conn.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + conn.KeyExpire(key, expireTime, CommandFlags.FireAndForget); + + var time = await conn.KeyExpireTimeAsync(key); + Assert.NotNull(time); + Assert.Equal(expireTime, time.Value, TimeSpan.FromSeconds(30)); + + // Without associated expiration time + conn.KeyDelete(key, CommandFlags.FireAndForget); + conn.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + time = await conn.KeyExpireTimeAsync(key); + Assert.Null(time); + + // Non existing key + conn.KeyDelete(key, CommandFlags.FireAndForget); + time = await conn.KeyExpireTimeAsync(key); + Assert.Null(time); + } } } diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index b6204de3c..3fb8b7b73 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -257,6 +257,29 @@ public void KeyExpireAsync_2() mock.Verify(_ => _.KeyExpireAsync("prefix:key", expiry, CommandFlags.None)); } + [Fact] + public void KeyExpireAsync_3() + { + TimeSpan expiry = TimeSpan.FromSeconds(123); + wrapper.KeyExpireAsync("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); + mock.Verify(_ => _.KeyExpireAsync("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); + } + + [Fact] + public void KeyExpireAsync_4() + { + DateTime expiry = DateTime.Now; + wrapper.KeyExpireAsync("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); + mock.Verify(_ => _.KeyExpireAsync("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); + } + + [Fact] + public void KeyExpireTimeAsync() + { + wrapper.KeyExpireTimeAsync("key", CommandFlags.None); + mock.Verify(_ => _.KeyExpireTimeAsync("prefix:key", CommandFlags.None)); + } + [Fact] public void KeyMigrateAsync() { From ef131d8a47131f52d0f1c8ec355e4fb9179df5cc Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 16 Apr 2022 12:24:34 -0400 Subject: [PATCH 135/435] Types: re-locate the API specific types (#2096) Specific commands have optimize result types, especially Geo and Streams for example - let's move all those out of the main file list to make maintenance easier. --- src/StackExchange.Redis/APITypes/GeoEntry.cs | 76 ++++++ .../APITypes/GeoPosition.cs | 77 ++++++ .../APITypes/GeoRadiusOptions.cs | 51 ++++ .../APITypes/GeoRadiusResult.cs | 48 ++++ .../{ => APITypes}/GeoSearchShape.cs | 0 src/StackExchange.Redis/APITypes/HashEntry.cs | 89 +++++++ .../APITypes/NameValueEntry.cs | 81 ++++++ .../APITypes/RedisStream.cs | 23 ++ .../APITypes/RedisValueWithExpiry.cs | 28 ++ .../APITypes/SortedSetEntry.cs | 107 ++++++++ .../APITypes/StreamConsumer.cs | 23 ++ .../APITypes/StreamConsumerInfo.cs | 30 +++ .../APITypes/StreamEntry.cs | 58 +++++ .../APITypes/StreamGroupInfo.cs | 36 +++ .../APITypes/StreamInfo.cs | 53 ++++ .../APITypes/StreamPendingInfo.cs | 35 +++ .../APITypes/StreamPendingMessageInfo.cs | 37 +++ .../APITypes/StreamPosition.cs | 66 +++++ .../{ => Enums}/SimulatedFailureType.cs | 0 src/StackExchange.Redis/GeoEntry.cs | 246 ------------------ src/StackExchange.Redis/HashEntry.cs | 90 ------- src/StackExchange.Redis/NameValueEntry.cs | 82 ------ src/StackExchange.Redis/RedisStream.cs | 24 -- .../RedisValueWithExpiry.cs | 29 --- src/StackExchange.Redis/SortedSetEntry.cs | 108 -------- .../StackExchange.Redis.csproj | 4 +- src/StackExchange.Redis/StreamConsumer.cs | 25 -- src/StackExchange.Redis/StreamConsumerInfo.cs | 32 --- src/StackExchange.Redis/StreamEntry.cs | 59 ----- src/StackExchange.Redis/StreamGroupInfo.cs | 38 --- src/StackExchange.Redis/StreamInfo.cs | 62 ----- src/StackExchange.Redis/StreamPendingInfo.cs | 40 --- .../StreamPendingMessageInfo.cs | 41 --- src/StackExchange.Redis/StreamPosition.cs | 67 ----- 34 files changed, 920 insertions(+), 945 deletions(-) create mode 100644 src/StackExchange.Redis/APITypes/GeoEntry.cs create mode 100644 src/StackExchange.Redis/APITypes/GeoPosition.cs create mode 100644 src/StackExchange.Redis/APITypes/GeoRadiusOptions.cs create mode 100644 src/StackExchange.Redis/APITypes/GeoRadiusResult.cs rename src/StackExchange.Redis/{ => APITypes}/GeoSearchShape.cs (100%) create mode 100644 src/StackExchange.Redis/APITypes/HashEntry.cs create mode 100644 src/StackExchange.Redis/APITypes/NameValueEntry.cs create mode 100644 src/StackExchange.Redis/APITypes/RedisStream.cs create mode 100644 src/StackExchange.Redis/APITypes/RedisValueWithExpiry.cs create mode 100644 src/StackExchange.Redis/APITypes/SortedSetEntry.cs create mode 100644 src/StackExchange.Redis/APITypes/StreamConsumer.cs create mode 100644 src/StackExchange.Redis/APITypes/StreamConsumerInfo.cs create mode 100644 src/StackExchange.Redis/APITypes/StreamEntry.cs create mode 100644 src/StackExchange.Redis/APITypes/StreamGroupInfo.cs create mode 100644 src/StackExchange.Redis/APITypes/StreamInfo.cs create mode 100644 src/StackExchange.Redis/APITypes/StreamPendingInfo.cs create mode 100644 src/StackExchange.Redis/APITypes/StreamPendingMessageInfo.cs create mode 100644 src/StackExchange.Redis/APITypes/StreamPosition.cs rename src/StackExchange.Redis/{ => Enums}/SimulatedFailureType.cs (100%) delete mode 100644 src/StackExchange.Redis/GeoEntry.cs delete mode 100644 src/StackExchange.Redis/HashEntry.cs delete mode 100644 src/StackExchange.Redis/NameValueEntry.cs delete mode 100644 src/StackExchange.Redis/RedisStream.cs delete mode 100644 src/StackExchange.Redis/RedisValueWithExpiry.cs delete mode 100644 src/StackExchange.Redis/SortedSetEntry.cs delete mode 100644 src/StackExchange.Redis/StreamConsumer.cs delete mode 100644 src/StackExchange.Redis/StreamConsumerInfo.cs delete mode 100644 src/StackExchange.Redis/StreamEntry.cs delete mode 100644 src/StackExchange.Redis/StreamGroupInfo.cs delete mode 100644 src/StackExchange.Redis/StreamInfo.cs delete mode 100644 src/StackExchange.Redis/StreamPendingInfo.cs delete mode 100644 src/StackExchange.Redis/StreamPendingMessageInfo.cs delete mode 100644 src/StackExchange.Redis/StreamPosition.cs diff --git a/src/StackExchange.Redis/APITypes/GeoEntry.cs b/src/StackExchange.Redis/APITypes/GeoEntry.cs new file mode 100644 index 000000000..b9ecb8d5b --- /dev/null +++ b/src/StackExchange.Redis/APITypes/GeoEntry.cs @@ -0,0 +1,76 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// Describes a GeoEntry element with the corresponding value. +/// GeoEntries are stored in redis as SortedSetEntries. +/// +public readonly struct GeoEntry : IEquatable +{ + /// + /// The name of the GeoEntry. + /// + public RedisValue Member { get; } + + /// + /// Describes the longitude and latitude of a GeoEntry. + /// + public GeoPosition Position { get; } + + /// + /// Initializes a GeoEntry value. + /// + /// The longitude position to use. + /// The latitude position to use. + /// The value to store for this position. + public GeoEntry(double longitude, double latitude, RedisValue member) + { + Member = member; + Position = new GeoPosition(longitude, latitude); + } + + /// + /// The longitude of the GeoEntry. + /// + public double Longitude => Position.Longitude; + + /// + /// The latitude of the GeoEntry. + /// + public double Latitude => Position.Latitude; + + /// + /// A "({Longitude},{Latitude})={Member}" string representation of this entry. + /// + public override string ToString() => $"({Longitude},{Latitude})={Member}"; + + /// + public override int GetHashCode() => Position.GetHashCode() ^ Member.GetHashCode(); + + /// + /// Compares two values for equality. + /// + /// The to compare to. + public override bool Equals(object? obj) => obj is GeoEntry geObj && Equals(geObj); + + /// + /// Compares two values for equality. + /// + /// The to compare to. + public bool Equals(GeoEntry other) => this == other; + + /// + /// Compares two values for equality. + /// + /// The first entry to compare. + /// The second entry to compare. + public static bool operator ==(GeoEntry x, GeoEntry y) => x.Position == y.Position && x.Member == y.Member; + + /// + /// Compares two values for non-equality. + /// + /// The first entry to compare. + /// The second entry to compare. + public static bool operator !=(GeoEntry x, GeoEntry y) => x.Position != y.Position || x.Member != y.Member; +} diff --git a/src/StackExchange.Redis/APITypes/GeoPosition.cs b/src/StackExchange.Redis/APITypes/GeoPosition.cs new file mode 100644 index 000000000..6e53b3e32 --- /dev/null +++ b/src/StackExchange.Redis/APITypes/GeoPosition.cs @@ -0,0 +1,77 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// Describes the longitude and latitude of a GeoEntry. +/// +public readonly struct GeoPosition : IEquatable +{ + internal static string GetRedisUnit(GeoUnit unit) => unit switch + { + GeoUnit.Meters => "m", + GeoUnit.Kilometers => "km", + GeoUnit.Miles => "mi", + GeoUnit.Feet => "ft", + _ => throw new ArgumentOutOfRangeException(nameof(unit)), + }; + + /// + /// The Latitude of the GeoPosition. + /// + public double Latitude { get; } + + /// + /// The Longitude of the GeoPosition. + /// + public double Longitude { get; } + + /// + /// Creates a new GeoPosition. + /// + public GeoPosition(double longitude, double latitude) + { + Longitude = longitude; + Latitude = latitude; + } + + /// + /// A "{long} {lat}" string representation of this position. + /// + public override string ToString() => string.Format("{0} {1}", Longitude, Latitude); + + /// + /// See . + /// Diagonals not an issue in the case of lat/long. + /// + /// + /// Diagonals are not an issue in the case of lat/long. + /// + public override int GetHashCode() => Longitude.GetHashCode() ^ Latitude.GetHashCode(); + + /// + /// Compares two values for equality. + /// + /// The to compare to. + public override bool Equals(object? obj) => obj is GeoPosition gpObj && Equals(gpObj); + + /// + /// Compares two values for equality. + /// + /// The to compare to. + public bool Equals(GeoPosition other) => this == other; + + /// + /// Compares two values for equality. + /// + /// The first position to compare. + /// The second position to compare. + public static bool operator ==(GeoPosition x, GeoPosition y) => x.Longitude == y.Longitude && x.Latitude == y.Latitude; + + /// + /// Compares two values for non-equality. + /// + /// The first position to compare. + /// The second position to compare. + public static bool operator !=(GeoPosition x, GeoPosition y) => x.Longitude != y.Longitude || x.Latitude != y.Latitude; +} diff --git a/src/StackExchange.Redis/APITypes/GeoRadiusOptions.cs b/src/StackExchange.Redis/APITypes/GeoRadiusOptions.cs new file mode 100644 index 000000000..f9f182f5b --- /dev/null +++ b/src/StackExchange.Redis/APITypes/GeoRadiusOptions.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace StackExchange.Redis; + +/// +/// GeoRadius command options. +/// +[Flags] +public enum GeoRadiusOptions +{ + /// + /// No Options. + /// + None = 0, + /// + /// Redis will return the coordinates of any results. + /// + WithCoordinates = 1, + /// + /// Redis will return the distance from center for all results. + /// + WithDistance = 2, + /// + /// Redis will return the geo hash value as an integer. (This is the score in the sorted set). + /// + WithGeoHash = 4, + /// + /// Populates the commonly used values from the entry (the integer hash is not returned as it is not commonly useful). + /// + Default = WithCoordinates | WithDistance +} + +internal static class GeoRadiusOptionsExtensions +{ + internal static void AddArgs(this GeoRadiusOptions options, List values) + { + if ((options & GeoRadiusOptions.WithCoordinates) != 0) + { + values.Add(RedisLiterals.WITHCOORD); + } + if ((options & GeoRadiusOptions.WithDistance) != 0) + { + values.Add(RedisLiterals.WITHDIST); + } + if ((options & GeoRadiusOptions.WithGeoHash) != 0) + { + values.Add(RedisLiterals.WITHHASH); + } + } +} diff --git a/src/StackExchange.Redis/APITypes/GeoRadiusResult.cs b/src/StackExchange.Redis/APITypes/GeoRadiusResult.cs new file mode 100644 index 000000000..d4cdbe8f8 --- /dev/null +++ b/src/StackExchange.Redis/APITypes/GeoRadiusResult.cs @@ -0,0 +1,48 @@ +namespace StackExchange.Redis; + +/// +/// The result of a GeoRadius command. +/// +public readonly struct GeoRadiusResult +{ + /// + /// Indicate the member being represented. + /// + public override string ToString() => Member.ToString(); + + /// + /// The matched member. + /// + public RedisValue Member { get; } + + /// + /// The distance of the matched member from the center of the geo radius command. + /// + public double? Distance { get; } + + /// + /// The hash value of the matched member as an integer. (The key in the sorted set). + /// + /// Note that this is not the same as the hash returned from GeoHash + public long? Hash { get; } + + /// + /// The coordinates of the matched member. + /// + public GeoPosition? Position { get; } + + /// + /// Returns a new GeoRadiusResult. + /// + /// The value from the result. + /// The distance from the result. + /// The hash of the result. + /// The GeoPosition of the result. + public GeoRadiusResult(in RedisValue member, double? distance, long? hash, GeoPosition? position) + { + Member = member; + Distance = distance; + Hash = hash; + Position = position; + } +} diff --git a/src/StackExchange.Redis/GeoSearchShape.cs b/src/StackExchange.Redis/APITypes/GeoSearchShape.cs similarity index 100% rename from src/StackExchange.Redis/GeoSearchShape.cs rename to src/StackExchange.Redis/APITypes/GeoSearchShape.cs diff --git a/src/StackExchange.Redis/APITypes/HashEntry.cs b/src/StackExchange.Redis/APITypes/HashEntry.cs new file mode 100644 index 000000000..c985aeb68 --- /dev/null +++ b/src/StackExchange.Redis/APITypes/HashEntry.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace StackExchange.Redis; + +/// +/// Describes a hash-field (a name/value pair). +/// +public readonly struct HashEntry : IEquatable +{ + internal readonly RedisValue name, value; + + /// + /// Initializes a value. + /// + /// The name for this hash entry. + /// The value for this hash entry. + public HashEntry(RedisValue name, RedisValue value) + { + this.name = name; + this.value = value; + } + + /// + /// The name of the hash field. + /// + public RedisValue Name => name; + + /// + /// The value of the hash field. + /// + public RedisValue Value => value; + + /// + /// The name of the hash field. + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Please use Name", false)] + public RedisValue Key => name; + + /// + /// Converts to a key/value pair. + /// + /// The to create a from. + public static implicit operator KeyValuePair(HashEntry value) => + new KeyValuePair(value.name, value.value); + + /// + /// Converts from a key/value pair. + /// + /// The to get a from. + public static implicit operator HashEntry(KeyValuePair value) => + new HashEntry(value.Key, value.Value); + + /// + /// A "{name}: {value}" string representation of this entry. + /// + public override string ToString() => name + ": " + value; + + /// + public override int GetHashCode() => name.GetHashCode() ^ value.GetHashCode(); + + /// + /// Compares two values for equality. + /// + /// The to compare to. + public override bool Equals(object? obj) => obj is HashEntry heObj && Equals(heObj); + + /// + /// Compares two values for equality. + /// + /// The to compare to. + public bool Equals(HashEntry other) => name == other.name && value == other.value; + + /// + /// Compares two values for equality. + /// + /// The first to compare. + /// The second to compare. + public static bool operator ==(HashEntry x, HashEntry y) => x.name == y.name && x.value == y.value; + + /// + /// Compares two values for non-equality. + /// + /// The first to compare. + /// The second to compare. + public static bool operator !=(HashEntry x, HashEntry y) => x.name != y.name || x.value != y.value; +} diff --git a/src/StackExchange.Redis/APITypes/NameValueEntry.cs b/src/StackExchange.Redis/APITypes/NameValueEntry.cs new file mode 100644 index 000000000..3fbafa86e --- /dev/null +++ b/src/StackExchange.Redis/APITypes/NameValueEntry.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; + +namespace StackExchange.Redis; + +/// +/// Describes a value contained in a stream (a name/value pair). +/// +public readonly struct NameValueEntry : IEquatable +{ + internal readonly RedisValue name, value; + + /// + /// Initializes a value. + /// + /// The name for this entry. + /// The value for this entry. + public NameValueEntry(RedisValue name, RedisValue value) + { + this.name = name; + this.value = value; + } + + /// + /// The name of the field. + /// + public RedisValue Name => name; + + /// + /// The value of the field. + /// + public RedisValue Value => value; + + /// + /// Converts to a key/value pair. + /// + /// The to create a from. + public static implicit operator KeyValuePair(NameValueEntry value) => + new KeyValuePair(value.name, value.value); + + /// + /// Converts from a key/value pair. + /// + /// The to get a from. + public static implicit operator NameValueEntry(KeyValuePair value) => + new NameValueEntry(value.Key, value.Value); + + /// + /// The "{name}: {value}" string representation. + /// + public override string ToString() => name + ": " + value; + + /// + public override int GetHashCode() => name.GetHashCode() ^ value.GetHashCode(); + + /// + /// Compares two values for equality. + /// + /// The to compare to. + public override bool Equals(object? obj) => obj is NameValueEntry heObj && Equals(heObj); + + /// + /// Compares two values for equality. + /// + /// The to compare to. + public bool Equals(NameValueEntry other) => name == other.name && value == other.value; + + /// + /// Compares two values for equality. + /// + /// The first to compare. + /// The second to compare. + public static bool operator ==(NameValueEntry x, NameValueEntry y) => x.name == y.name && x.value == y.value; + + /// + /// Compares two values for non-equality. + /// + /// The first to compare. + /// The second to compare. + public static bool operator !=(NameValueEntry x, NameValueEntry y) => x.name != y.name || x.value != y.value; +} diff --git a/src/StackExchange.Redis/APITypes/RedisStream.cs b/src/StackExchange.Redis/APITypes/RedisStream.cs new file mode 100644 index 000000000..fbefa1240 --- /dev/null +++ b/src/StackExchange.Redis/APITypes/RedisStream.cs @@ -0,0 +1,23 @@ +namespace StackExchange.Redis; + +/// +/// Describes a Redis Stream with an associated array of entries. +/// +public readonly struct RedisStream +{ + internal RedisStream(RedisKey key, StreamEntry[] entries) + { + Key = key; + Entries = entries; + } + + /// + /// The key for the stream. + /// + public RedisKey Key { get; } + + /// + /// An array of entries contained within the stream. + /// + public StreamEntry[] Entries { get; } +} diff --git a/src/StackExchange.Redis/APITypes/RedisValueWithExpiry.cs b/src/StackExchange.Redis/APITypes/RedisValueWithExpiry.cs new file mode 100644 index 000000000..c64e9aca9 --- /dev/null +++ b/src/StackExchange.Redis/APITypes/RedisValueWithExpiry.cs @@ -0,0 +1,28 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// Describes a value/expiry pair. +/// +public readonly struct RedisValueWithExpiry +{ + /// + /// Creates a from a and a . + /// + public RedisValueWithExpiry(RedisValue value, TimeSpan? expiry) + { + Value = value; + Expiry = expiry; + } + + /// + /// The expiry of this record. + /// + public TimeSpan? Expiry { get; } + + /// + /// The value of this record. + /// + public RedisValue Value { get; } +} diff --git a/src/StackExchange.Redis/APITypes/SortedSetEntry.cs b/src/StackExchange.Redis/APITypes/SortedSetEntry.cs new file mode 100644 index 000000000..e61dc05ed --- /dev/null +++ b/src/StackExchange.Redis/APITypes/SortedSetEntry.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace StackExchange.Redis; + +/// +/// Describes a sorted-set element with the corresponding value. +/// +public readonly struct SortedSetEntry : IEquatable, IComparable, IComparable +{ + internal readonly RedisValue element; + internal readonly double score; + + /// + /// Initializes a value. + /// + /// The to get an entry for. + /// The redis score for . + public SortedSetEntry(RedisValue element, double score) + { + this.element = element; + this.score = score; + } + + /// + /// The unique element stored in the sorted set. + /// + public RedisValue Element => element; + + /// + /// The score against the element. + /// + public double Score => score; + + /// + /// The score against the element. + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Please use Score", false)] + public double Value => score; + + /// + /// The unique element stored in the sorted set. + /// + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Please use Element", false)] + public RedisValue Key => element; + + /// + /// Converts to a key/value pair. + /// + /// The to get a for. + public static implicit operator KeyValuePair(SortedSetEntry value) => new KeyValuePair(value.element, value.score); + + /// + /// Converts from a key/value pair. + /// + /// The to get a for. + public static implicit operator SortedSetEntry(KeyValuePair value) => new SortedSetEntry(value.Key, value.Value); + + /// + /// A "{element}: {score}" string representation of the entry. + /// + public override string ToString() => element + ": " + score; + + /// + public override int GetHashCode() => element.GetHashCode() ^ score.GetHashCode(); + + /// + /// Compares two values for equality. + /// + /// The to compare to. + public override bool Equals(object? obj) => obj is SortedSetEntry ssObj && Equals(ssObj); + + /// + /// Compares two values for equality. + /// + /// The to compare to. + public bool Equals(SortedSetEntry other) => score == other.score && element == other.element; + + /// + /// Compares two values by score. + /// + /// The to compare to. + public int CompareTo(SortedSetEntry other) => score.CompareTo(other.score); + + /// + /// Compares two values by score. + /// + /// The to compare to. + public int CompareTo(object? obj) => obj is SortedSetEntry ssObj ? CompareTo(ssObj) : -1; + + /// + /// Compares two values for equality. + /// + /// The first to compare. + /// The second to compare. + public static bool operator ==(SortedSetEntry x, SortedSetEntry y) => x.score == y.score && x.element == y.element; + + /// + /// Compares two values for non-equality. + /// + /// The first to compare. + /// The second to compare. + public static bool operator !=(SortedSetEntry x, SortedSetEntry y) => x.score != y.score || x.element != y.element; +} diff --git a/src/StackExchange.Redis/APITypes/StreamConsumer.cs b/src/StackExchange.Redis/APITypes/StreamConsumer.cs new file mode 100644 index 000000000..a99778707 --- /dev/null +++ b/src/StackExchange.Redis/APITypes/StreamConsumer.cs @@ -0,0 +1,23 @@ +namespace StackExchange.Redis; + +/// +/// Describes a consumer off a Redis Stream. +/// +public readonly struct StreamConsumer +{ + internal StreamConsumer(RedisValue name, int pendingMessageCount) + { + Name = name; + PendingMessageCount = pendingMessageCount; + } + + /// + /// The name of the consumer. + /// + public RedisValue Name { get; } + + /// + /// The number of messages that have been delivered by not yet acknowledged by the consumer. + /// + public int PendingMessageCount { get; } +} diff --git a/src/StackExchange.Redis/APITypes/StreamConsumerInfo.cs b/src/StackExchange.Redis/APITypes/StreamConsumerInfo.cs new file mode 100644 index 000000000..ab7cd9af1 --- /dev/null +++ b/src/StackExchange.Redis/APITypes/StreamConsumerInfo.cs @@ -0,0 +1,30 @@ +namespace StackExchange.Redis; + +/// +/// Describes a consumer within a consumer group, retrieved using the XINFO CONSUMERS command. . +/// +public readonly struct StreamConsumerInfo +{ + internal StreamConsumerInfo(string name, int pendingMessageCount, long idleTimeInMilliseconds) + { + Name = name; + PendingMessageCount = pendingMessageCount; + IdleTimeInMilliseconds = idleTimeInMilliseconds; + } + + /// + /// The name of the consumer. + /// + public string Name { get; } + + /// + /// The number of pending messages for the consumer. A pending message is one that has been + /// received by the consumer but not yet acknowledged. + /// + public int PendingMessageCount { get; } + + /// + /// The idle time, if any, for the consumer. + /// + public long IdleTimeInMilliseconds { get; } +} diff --git a/src/StackExchange.Redis/APITypes/StreamEntry.cs b/src/StackExchange.Redis/APITypes/StreamEntry.cs new file mode 100644 index 000000000..3f37b9430 --- /dev/null +++ b/src/StackExchange.Redis/APITypes/StreamEntry.cs @@ -0,0 +1,58 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// Describes an entry contained in a Redis Stream. +/// +public readonly struct StreamEntry +{ + /// + /// Creates an stream entry. + /// + public StreamEntry(RedisValue id, NameValueEntry[] values) + { + Id = id; + Values = values; + } + + /// + /// A null stream entry. + /// + public static StreamEntry Null { get; } = new StreamEntry(RedisValue.Null, Array.Empty()); + + /// + /// The ID assigned to the message. + /// + public RedisValue Id { get; } + + /// + /// The values contained within the message. + /// + public NameValueEntry[] Values { get; } + + /// + /// Search for a specific field by name, returning the value. + /// + public RedisValue this[RedisValue fieldName] + { + get + { + var values = Values; + if (values != null) + { + for (int i = 0; i < values.Length; i++) + { + if (values[i].name == fieldName) + return values[i].value; + } + } + return RedisValue.Null; + } + } + + /// + /// Indicates that the Redis Stream Entry is null. + /// + public bool IsNull => Id == RedisValue.Null && Values == Array.Empty(); +} diff --git a/src/StackExchange.Redis/APITypes/StreamGroupInfo.cs b/src/StackExchange.Redis/APITypes/StreamGroupInfo.cs new file mode 100644 index 000000000..33281737f --- /dev/null +++ b/src/StackExchange.Redis/APITypes/StreamGroupInfo.cs @@ -0,0 +1,36 @@ +namespace StackExchange.Redis; + +/// +/// Describes a consumer group retrieved using the XINFO GROUPS command. . +/// +public readonly struct StreamGroupInfo +{ + internal StreamGroupInfo(string name, int consumerCount, int pendingMessageCount, string? lastDeliveredId) + { + Name = name; + ConsumerCount = consumerCount; + PendingMessageCount = pendingMessageCount; + LastDeliveredId = lastDeliveredId; + } + + /// + /// The name of the consumer group. + /// + public string Name { get; } + + /// + /// The number of consumers within the consumer group. + /// + public int ConsumerCount { get; } + + /// + /// The total number of pending messages for the consumer group. A pending message is one that has been + /// received by a consumer but not yet acknowledged. + /// + public int PendingMessageCount { get; } + + /// + /// The Id of the last message delivered to the group. + /// + public string? LastDeliveredId { get; } +} diff --git a/src/StackExchange.Redis/APITypes/StreamInfo.cs b/src/StackExchange.Redis/APITypes/StreamInfo.cs new file mode 100644 index 000000000..230ea47fb --- /dev/null +++ b/src/StackExchange.Redis/APITypes/StreamInfo.cs @@ -0,0 +1,53 @@ +namespace StackExchange.Redis; + +/// +/// Describes stream information retrieved using the XINFO STREAM command. . +/// +public readonly struct StreamInfo +{ + internal StreamInfo(int length, int radixTreeKeys, int radixTreeNodes, int groups, StreamEntry firstEntry, StreamEntry lastEntry, RedisValue lastGeneratedId) + { + Length = length; + RadixTreeKeys = radixTreeKeys; + RadixTreeNodes = radixTreeNodes; + ConsumerGroupCount = groups; + FirstEntry = firstEntry; + LastEntry = lastEntry; + LastGeneratedId = lastGeneratedId; + } + + /// + /// The number of entries in the stream. + /// + public int Length { get; } + + /// + /// The number of radix tree keys in the stream. + /// + public int RadixTreeKeys { get; } + + /// + /// The number of radix tree nodes in the stream. + /// + public int RadixTreeNodes { get; } + + /// + /// The number of consumers groups in the stream. + /// + public int ConsumerGroupCount { get; } + + /// + /// The first entry in the stream. + /// + public StreamEntry FirstEntry { get; } + + /// + /// The last entry in the stream. + /// + public StreamEntry LastEntry { get; } + + /// + /// The last generated id. + /// + public RedisValue LastGeneratedId { get; } +} diff --git a/src/StackExchange.Redis/APITypes/StreamPendingInfo.cs b/src/StackExchange.Redis/APITypes/StreamPendingInfo.cs new file mode 100644 index 000000000..b55696f8c --- /dev/null +++ b/src/StackExchange.Redis/APITypes/StreamPendingInfo.cs @@ -0,0 +1,35 @@ +namespace StackExchange.Redis; + +/// +/// Describes basic information about pending messages for a consumer group. +/// +public readonly struct StreamPendingInfo +{ + internal StreamPendingInfo(int pendingMessageCount, RedisValue lowestId, RedisValue highestId, StreamConsumer[] consumers) + { + PendingMessageCount = pendingMessageCount; + LowestPendingMessageId = lowestId; + HighestPendingMessageId = highestId; + Consumers = consumers; + } + + /// + /// The number of pending messages. A pending message is a message that has been consumed but not yet acknowledged. + /// + public int PendingMessageCount { get; } + + /// + /// The lowest message ID in the set of pending messages. + /// + public RedisValue LowestPendingMessageId { get; } + + /// + /// The highest message ID in the set of pending messages. + /// + public RedisValue HighestPendingMessageId { get; } + + /// + /// An array of consumers within the consumer group that have pending messages. + /// + public StreamConsumer[] Consumers { get; } +} diff --git a/src/StackExchange.Redis/APITypes/StreamPendingMessageInfo.cs b/src/StackExchange.Redis/APITypes/StreamPendingMessageInfo.cs new file mode 100644 index 000000000..95f545ca5 --- /dev/null +++ b/src/StackExchange.Redis/APITypes/StreamPendingMessageInfo.cs @@ -0,0 +1,37 @@ + +namespace StackExchange.Redis; + +/// +/// Describes properties of a pending message. +/// A pending message is one that has been received by a consumer but has not yet been acknowledged. +/// +public readonly struct StreamPendingMessageInfo +{ + internal StreamPendingMessageInfo(RedisValue messageId, RedisValue consumerName, long idleTimeInMs, int deliveryCount) + { + MessageId = messageId; + ConsumerName = consumerName; + IdleTimeInMilliseconds = idleTimeInMs; + DeliveryCount = deliveryCount; + } + + /// + /// The ID of the pending message. + /// + public RedisValue MessageId { get; } + + /// + /// The consumer that received the pending message. + /// + public RedisValue ConsumerName { get; } + + /// + /// The time that has passed since the message was last delivered to a consumer. + /// + public long IdleTimeInMilliseconds { get; } + + /// + /// The number of times the message has been delivered to a consumer. + /// + public int DeliveryCount { get; } +} diff --git a/src/StackExchange.Redis/APITypes/StreamPosition.cs b/src/StackExchange.Redis/APITypes/StreamPosition.cs new file mode 100644 index 000000000..b58dfd599 --- /dev/null +++ b/src/StackExchange.Redis/APITypes/StreamPosition.cs @@ -0,0 +1,66 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// Describes a pair consisting of the Stream Key and the from which to begin reading a stream. +/// +public struct StreamPosition +{ + /// + /// Read from the beginning of a stream. + /// + public static RedisValue Beginning => StreamConstants.ReadMinValue; + + /// + /// Read new messages. + /// + public static RedisValue NewMessages => StreamConstants.NewMessages; + + /// + /// Initializes a value. + /// + /// The key for the stream. + /// The position from which to begin reading the stream. + public StreamPosition(RedisKey key, RedisValue position) + { + Key = key; + Position = position; + } + + /// + /// The stream key. + /// + public RedisKey Key { get; } + + /// + /// The offset at which to begin reading the stream. + /// + public RedisValue Position { get; } + + internal static RedisValue Resolve(RedisValue value, RedisCommand command) + { + if (value == NewMessages) + { + return command switch + { + RedisCommand.XREAD => throw new InvalidOperationException("StreamPosition.NewMessages cannot be used with StreamRead."), + RedisCommand.XREADGROUP => StreamConstants.UndeliveredMessages, + RedisCommand.XGROUP => StreamConstants.NewMessages, + // new is only valid for the above + _ => throw new ArgumentException($"Unsupported command in StreamPosition.Resolve: {command}.", nameof(command)), + }; + } + else if (value == StreamPosition.Beginning) + { + switch (command) + { + case RedisCommand.XREAD: + case RedisCommand.XREADGROUP: + case RedisCommand.XGROUP: + return StreamConstants.AllMessages; + } + } + return value; + } +} diff --git a/src/StackExchange.Redis/SimulatedFailureType.cs b/src/StackExchange.Redis/Enums/SimulatedFailureType.cs similarity index 100% rename from src/StackExchange.Redis/SimulatedFailureType.cs rename to src/StackExchange.Redis/Enums/SimulatedFailureType.cs diff --git a/src/StackExchange.Redis/GeoEntry.cs b/src/StackExchange.Redis/GeoEntry.cs deleted file mode 100644 index a7adf8ae1..000000000 --- a/src/StackExchange.Redis/GeoEntry.cs +++ /dev/null @@ -1,246 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace StackExchange.Redis -{ - /// - /// GeoRadius command options. - /// - [Flags] - public enum GeoRadiusOptions - { - /// - /// No Options. - /// - None = 0, - /// - /// Redis will return the coordinates of any results. - /// - WithCoordinates = 1, - /// - /// Redis will return the distance from center for all results. - /// - WithDistance = 2, - /// - /// Redis will return the geo hash value as an integer. (This is the score in the sorted set). - /// - WithGeoHash = 4, - /// - /// Populates the commonly used values from the entry (the integer hash is not returned as it is not commonly useful). - /// - Default = WithCoordinates | WithDistance - } - - internal static class GeoRadiusOptionsExtensions - { - internal static void AddArgs(this GeoRadiusOptions options, List values) - { - if ((options & GeoRadiusOptions.WithCoordinates) != 0) - { - values.Add(RedisLiterals.WITHCOORD); - } - if ((options & GeoRadiusOptions.WithDistance) != 0) - { - values.Add(RedisLiterals.WITHDIST); - } - if ((options & GeoRadiusOptions.WithGeoHash) != 0) - { - values.Add(RedisLiterals.WITHHASH); - } - } - } - - /// - /// The result of a GeoRadius command. - /// - public readonly struct GeoRadiusResult - { - /// - /// Indicate the member being represented. - /// - public override string ToString() => Member.ToString(); - - /// - /// The matched member. - /// - public RedisValue Member { get; } - - /// - /// The distance of the matched member from the center of the geo radius command. - /// - public double? Distance { get; } - - /// - /// The hash value of the matched member as an integer. (The key in the sorted set). - /// - /// Note that this is not the same as the hash returned from GeoHash - public long? Hash { get; } - - /// - /// The coordinates of the matched member. - /// - public GeoPosition? Position { get; } - - /// - /// Returns a new GeoRadiusResult. - /// - /// The value from the result. - /// The distance from the result. - /// The hash of the result. - /// The GeoPosition of the result. - public GeoRadiusResult(in RedisValue member, double? distance, long? hash, GeoPosition? position) - { - Member = member; - Distance = distance; - Hash = hash; - Position = position; - } - } - - /// - /// Describes the longitude and latitude of a GeoEntry. - /// - public readonly struct GeoPosition : IEquatable - { - internal static string GetRedisUnit(GeoUnit unit) => unit switch - { - GeoUnit.Meters => "m", - GeoUnit.Kilometers => "km", - GeoUnit.Miles => "mi", - GeoUnit.Feet => "ft", - _ => throw new ArgumentOutOfRangeException(nameof(unit)), - }; - - /// - /// The Latitude of the GeoPosition. - /// - public double Latitude { get; } - - /// - /// The Longitude of the GeoPosition. - /// - public double Longitude { get; } - - /// - /// Creates a new GeoPosition. - /// - public GeoPosition(double longitude, double latitude) - { - Longitude = longitude; - Latitude = latitude; - } - - /// - /// A "{long} {lat}" string representation of this position. - /// - public override string ToString() => string.Format("{0} {1}", Longitude, Latitude); - - /// - /// See . - /// Diagonals not an issue in the case of lat/long. - /// - /// - /// Diagonals are not an issue in the case of lat/long. - /// - public override int GetHashCode() => Longitude.GetHashCode() ^ Latitude.GetHashCode(); - - /// - /// Compares two values for equality. - /// - /// The to compare to. - public override bool Equals(object? obj) => obj is GeoPosition gpObj && Equals(gpObj); - - /// - /// Compares two values for equality. - /// - /// The to compare to. - public bool Equals(GeoPosition other) => this == other; - - /// - /// Compares two values for equality. - /// - /// The first position to compare. - /// The second position to compare. - public static bool operator ==(GeoPosition x, GeoPosition y) => x.Longitude == y.Longitude && x.Latitude == y.Latitude; - - /// - /// Compares two values for non-equality. - /// - /// The first position to compare. - /// The second position to compare. - public static bool operator !=(GeoPosition x, GeoPosition y) => x.Longitude != y.Longitude || x.Latitude != y.Latitude; - } - - /// - /// Describes a GeoEntry element with the corresponding value. - /// GeoEntries are stored in redis as SortedSetEntries. - /// - public readonly struct GeoEntry : IEquatable - { - /// - /// The name of the GeoEntry. - /// - public RedisValue Member { get; } - - /// - /// Describes the longitude and latitude of a GeoEntry. - /// - public GeoPosition Position { get; } - - /// - /// Initializes a GeoEntry value. - /// - /// The longitude position to use. - /// The latitude position to use. - /// The value to store for this position. - public GeoEntry(double longitude, double latitude, RedisValue member) - { - Member = member; - Position = new GeoPosition(longitude, latitude); - } - - /// - /// The longitude of the GeoEntry. - /// - public double Longitude => Position.Longitude; - - /// - /// The latitude of the GeoEntry. - /// - public double Latitude => Position.Latitude; - - /// - /// A "({Longitude},{Latitude})={Member}" string representation of this entry. - /// - public override string ToString() => $"({Longitude},{Latitude})={Member}"; - - /// - public override int GetHashCode() => Position.GetHashCode() ^ Member.GetHashCode(); - - /// - /// Compares two values for equality. - /// - /// The to compare to. - public override bool Equals(object? obj) => obj is GeoEntry geObj && Equals(geObj); - - /// - /// Compares two values for equality. - /// - /// The to compare to. - public bool Equals(GeoEntry other) => this == other; - - /// - /// Compares two values for equality. - /// - /// The first entry to compare. - /// The second entry to compare. - public static bool operator ==(GeoEntry x, GeoEntry y) => x.Position == y.Position && x.Member == y.Member; - - /// - /// Compares two values for non-equality. - /// - /// The first entry to compare. - /// The second entry to compare. - public static bool operator !=(GeoEntry x, GeoEntry y) => x.Position != y.Position || x.Member != y.Member; - } -} diff --git a/src/StackExchange.Redis/HashEntry.cs b/src/StackExchange.Redis/HashEntry.cs deleted file mode 100644 index e1a62380e..000000000 --- a/src/StackExchange.Redis/HashEntry.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; - -namespace StackExchange.Redis -{ - /// - /// Describes a hash-field (a name/value pair). - /// - public readonly struct HashEntry : IEquatable - { - internal readonly RedisValue name, value; - - /// - /// Initializes a value. - /// - /// The name for this hash entry. - /// The value for this hash entry. - public HashEntry(RedisValue name, RedisValue value) - { - this.name = name; - this.value = value; - } - - /// - /// The name of the hash field. - /// - public RedisValue Name => name; - - /// - /// The value of the hash field. - /// - public RedisValue Value => value; - - /// - /// The name of the hash field. - /// - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Please use Name", false)] - public RedisValue Key => name; - - /// - /// Converts to a key/value pair. - /// - /// The to create a from. - public static implicit operator KeyValuePair(HashEntry value) => - new KeyValuePair(value.name, value.value); - - /// - /// Converts from a key/value pair. - /// - /// The to get a from. - public static implicit operator HashEntry(KeyValuePair value) => - new HashEntry(value.Key, value.Value); - - /// - /// A "{name}: {value}" string representation of this entry. - /// - public override string ToString() => name + ": " + value; - - /// - public override int GetHashCode() => name.GetHashCode() ^ value.GetHashCode(); - - /// - /// Compares two values for equality. - /// - /// The to compare to. - public override bool Equals(object? obj) => obj is HashEntry heObj && Equals(heObj); - - /// - /// Compares two values for equality. - /// - /// The to compare to. - public bool Equals(HashEntry other) => name == other.name && value == other.value; - - /// - /// Compares two values for equality. - /// - /// The first to compare. - /// The second to compare. - public static bool operator ==(HashEntry x, HashEntry y) => x.name == y.name && x.value == y.value; - - /// - /// Compares two values for non-equality. - /// - /// The first to compare. - /// The second to compare. - public static bool operator !=(HashEntry x, HashEntry y) => x.name != y.name || x.value != y.value; - } -} diff --git a/src/StackExchange.Redis/NameValueEntry.cs b/src/StackExchange.Redis/NameValueEntry.cs deleted file mode 100644 index 17cca1efc..000000000 --- a/src/StackExchange.Redis/NameValueEntry.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace StackExchange.Redis -{ - /// - /// Describes a value contained in a stream (a name/value pair). - /// - public readonly struct NameValueEntry : IEquatable - { - internal readonly RedisValue name, value; - - /// - /// Initializes a value. - /// - /// The name for this entry. - /// The value for this entry. - public NameValueEntry(RedisValue name, RedisValue value) - { - this.name = name; - this.value = value; - } - - /// - /// The name of the field. - /// - public RedisValue Name => name; - - /// - /// The value of the field. - /// - public RedisValue Value => value; - - /// - /// Converts to a key/value pair. - /// - /// The to create a from. - public static implicit operator KeyValuePair(NameValueEntry value) => - new KeyValuePair(value.name, value.value); - - /// - /// Converts from a key/value pair. - /// - /// The to get a from. - public static implicit operator NameValueEntry(KeyValuePair value) => - new NameValueEntry(value.Key, value.Value); - - /// - /// The "{name}: {value}" string representation. - /// - public override string ToString() => name + ": " + value; - - /// - public override int GetHashCode() => name.GetHashCode() ^ value.GetHashCode(); - - /// - /// Compares two values for equality. - /// - /// The to compare to. - public override bool Equals(object? obj) => obj is NameValueEntry heObj && Equals(heObj); - - /// - /// Compares two values for equality. - /// - /// The to compare to. - public bool Equals(NameValueEntry other) => name == other.name && value == other.value; - - /// - /// Compares two values for equality. - /// - /// The first to compare. - /// The second to compare. - public static bool operator ==(NameValueEntry x, NameValueEntry y) => x.name == y.name && x.value == y.value; - - /// - /// Compares two values for non-equality. - /// - /// The first to compare. - /// The second to compare. - public static bool operator !=(NameValueEntry x, NameValueEntry y) => x.name != y.name || x.value != y.value; - } -} diff --git a/src/StackExchange.Redis/RedisStream.cs b/src/StackExchange.Redis/RedisStream.cs deleted file mode 100644 index eb3c97967..000000000 --- a/src/StackExchange.Redis/RedisStream.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace StackExchange.Redis -{ - /// - /// Describes a Redis Stream with an associated array of entries. - /// - public readonly struct RedisStream - { - internal RedisStream(RedisKey key, StreamEntry[] entries) - { - Key = key; - Entries = entries; - } - - /// - /// The key for the stream. - /// - public RedisKey Key { get; } - - /// - /// An array of entries contained within the stream. - /// - public StreamEntry[] Entries { get; } - } -} diff --git a/src/StackExchange.Redis/RedisValueWithExpiry.cs b/src/StackExchange.Redis/RedisValueWithExpiry.cs deleted file mode 100644 index d2ebbeb56..000000000 --- a/src/StackExchange.Redis/RedisValueWithExpiry.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace StackExchange.Redis -{ - /// - /// Describes a value/expiry pair. - /// - public readonly struct RedisValueWithExpiry - { - /// - /// Creates a from a and a . - /// - public RedisValueWithExpiry(RedisValue value, TimeSpan? expiry) - { - Value = value; - Expiry = expiry; - } - - /// - /// The expiry of this record. - /// - public TimeSpan? Expiry { get; } - - /// - /// The value of this record. - /// - public RedisValue Value { get; } - } -} diff --git a/src/StackExchange.Redis/SortedSetEntry.cs b/src/StackExchange.Redis/SortedSetEntry.cs deleted file mode 100644 index eb07f10b9..000000000 --- a/src/StackExchange.Redis/SortedSetEntry.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; - -namespace StackExchange.Redis -{ - /// - /// Describes a sorted-set element with the corresponding value. - /// - public readonly struct SortedSetEntry : IEquatable, IComparable, IComparable - { - internal readonly RedisValue element; - internal readonly double score; - - /// - /// Initializes a value. - /// - /// The to get an entry for. - /// The redis score for . - public SortedSetEntry(RedisValue element, double score) - { - this.element = element; - this.score = score; - } - - /// - /// The unique element stored in the sorted set. - /// - public RedisValue Element => element; - - /// - /// The score against the element. - /// - public double Score => score; - - /// - /// The score against the element. - /// - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Please use Score", false)] - public double Value => score; - - /// - /// The unique element stored in the sorted set. - /// - [Browsable(false)] - [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Please use Element", false)] - public RedisValue Key => element; - - /// - /// Converts to a key/value pair. - /// - /// The to get a for. - public static implicit operator KeyValuePair(SortedSetEntry value) => new KeyValuePair(value.element, value.score); - - /// - /// Converts from a key/value pair. - /// - /// The to get a for. - public static implicit operator SortedSetEntry(KeyValuePair value) => new SortedSetEntry(value.Key, value.Value); - - /// - /// A "{element}: {score}" string representation of the entry. - /// - public override string ToString() => element + ": " + score; - - /// - public override int GetHashCode() => element.GetHashCode() ^ score.GetHashCode(); - - /// - /// Compares two values for equality. - /// - /// The to compare to. - public override bool Equals(object? obj) => obj is SortedSetEntry ssObj && Equals(ssObj); - - /// - /// Compares two values for equality. - /// - /// The to compare to. - public bool Equals(SortedSetEntry other) => score == other.score && element == other.element; - - /// - /// Compares two values by score. - /// - /// The to compare to. - public int CompareTo(SortedSetEntry other) => score.CompareTo(other.score); - - /// - /// Compares two values by score. - /// - /// The to compare to. - public int CompareTo(object? obj) => obj is SortedSetEntry ssObj ? CompareTo(ssObj) : -1; - - /// - /// Compares two values for equality. - /// - /// The first to compare. - /// The second to compare. - public static bool operator ==(SortedSetEntry x, SortedSetEntry y) => x.score == y.score && x.element == y.element; - - /// - /// Compares two values for non-equality. - /// - /// The first to compare. - /// The second to compare. - public static bool operator !=(SortedSetEntry x, SortedSetEntry y) => x.score != y.score || x.element != y.element; - } -} diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 41a90fb89..30c639a18 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -25,10 +25,10 @@ - + - + diff --git a/src/StackExchange.Redis/StreamConsumer.cs b/src/StackExchange.Redis/StreamConsumer.cs deleted file mode 100644 index f92933180..000000000 --- a/src/StackExchange.Redis/StreamConsumer.cs +++ /dev/null @@ -1,25 +0,0 @@ - -namespace StackExchange.Redis -{ - /// - /// Describes a consumer off a Redis Stream. - /// - public readonly struct StreamConsumer - { - internal StreamConsumer(RedisValue name, int pendingMessageCount) - { - Name = name; - PendingMessageCount = pendingMessageCount; - } - - /// - /// The name of the consumer. - /// - public RedisValue Name { get; } - - /// - /// The number of messages that have been delivered by not yet acknowledged by the consumer. - /// - public int PendingMessageCount { get; } - } -} diff --git a/src/StackExchange.Redis/StreamConsumerInfo.cs b/src/StackExchange.Redis/StreamConsumerInfo.cs deleted file mode 100644 index a23e27bd0..000000000 --- a/src/StackExchange.Redis/StreamConsumerInfo.cs +++ /dev/null @@ -1,32 +0,0 @@ - -namespace StackExchange.Redis -{ - /// - /// Describes a consumer within a consumer group, retrieved using the XINFO CONSUMERS command. . - /// - public readonly struct StreamConsumerInfo - { - internal StreamConsumerInfo(string name, int pendingMessageCount, long idleTimeInMilliseconds) - { - Name = name; - PendingMessageCount = pendingMessageCount; - IdleTimeInMilliseconds = idleTimeInMilliseconds; - } - - /// - /// The name of the consumer. - /// - public string Name { get; } - - /// - /// The number of pending messages for the consumer. A pending message is one that has been - /// received by the consumer but not yet acknowledged. - /// - public int PendingMessageCount { get; } - - /// - /// The idle time, if any, for the consumer. - /// - public long IdleTimeInMilliseconds { get; } - } -} diff --git a/src/StackExchange.Redis/StreamEntry.cs b/src/StackExchange.Redis/StreamEntry.cs deleted file mode 100644 index 5752de520..000000000 --- a/src/StackExchange.Redis/StreamEntry.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; - -namespace StackExchange.Redis -{ - /// - /// Describes an entry contained in a Redis Stream. - /// - public readonly struct StreamEntry - { - /// - /// Creates an stream entry. - /// - public StreamEntry(RedisValue id, NameValueEntry[] values) - { - Id = id; - Values = values; - } - - /// - /// A null stream entry. - /// - public static StreamEntry Null { get; } = new StreamEntry(RedisValue.Null, Array.Empty()); - - /// - /// The ID assigned to the message. - /// - public RedisValue Id { get; } - - /// - /// The values contained within the message. - /// - public NameValueEntry[] Values { get; } - - /// - /// Search for a specific field by name, returning the value. - /// - public RedisValue this[RedisValue fieldName] - { - get - { - var values = Values; - if (values != null) - { - for (int i = 0; i < values.Length; i++) - { - if (values[i].name == fieldName) - return values[i].value; - } - } - return RedisValue.Null; - } - } - - /// - /// Indicates that the Redis Stream Entry is null. - /// - public bool IsNull => Id == RedisValue.Null && Values == Array.Empty(); - } -} diff --git a/src/StackExchange.Redis/StreamGroupInfo.cs b/src/StackExchange.Redis/StreamGroupInfo.cs deleted file mode 100644 index 052dffca1..000000000 --- a/src/StackExchange.Redis/StreamGroupInfo.cs +++ /dev/null @@ -1,38 +0,0 @@ - -namespace StackExchange.Redis -{ - /// - /// Describes a consumer group retrieved using the XINFO GROUPS command. . - /// - public readonly struct StreamGroupInfo - { - internal StreamGroupInfo(string name, int consumerCount, int pendingMessageCount, string? lastDeliveredId) - { - Name = name; - ConsumerCount = consumerCount; - PendingMessageCount = pendingMessageCount; - LastDeliveredId = lastDeliveredId; - } - - /// - /// The name of the consumer group. - /// - public string Name { get; } - - /// - /// The number of consumers within the consumer group. - /// - public int ConsumerCount { get; } - - /// - /// The total number of pending messages for the consumer group. A pending message is one that has been - /// received by a consumer but not yet acknowledged. - /// - public int PendingMessageCount { get; } - - /// - /// The Id of the last message delivered to the group. - /// - public string? LastDeliveredId { get; } - } -} diff --git a/src/StackExchange.Redis/StreamInfo.cs b/src/StackExchange.Redis/StreamInfo.cs deleted file mode 100644 index 77c0dcf86..000000000 --- a/src/StackExchange.Redis/StreamInfo.cs +++ /dev/null @@ -1,62 +0,0 @@ - -namespace StackExchange.Redis -{ - /// - /// Describes stream information retrieved using the XINFO STREAM command. . - /// - public readonly struct StreamInfo - { - internal StreamInfo( - int length, - int radixTreeKeys, - int radixTreeNodes, - int groups, - StreamEntry firstEntry, - StreamEntry lastEntry, - RedisValue lastGeneratedId) - { - Length = length; - RadixTreeKeys = radixTreeKeys; - RadixTreeNodes = radixTreeNodes; - ConsumerGroupCount = groups; - FirstEntry = firstEntry; - LastEntry = lastEntry; - LastGeneratedId = lastGeneratedId; - } - - /// - /// The number of entries in the stream. - /// - public int Length { get; } - - /// - /// The number of radix tree keys in the stream. - /// - public int RadixTreeKeys { get; } - - /// - /// The number of radix tree nodes in the stream. - /// - public int RadixTreeNodes { get; } - - /// - /// The number of consumers groups in the stream. - /// - public int ConsumerGroupCount { get; } - - /// - /// The first entry in the stream. - /// - public StreamEntry FirstEntry { get; } - - /// - /// The last entry in the stream. - /// - public StreamEntry LastEntry { get; } - - /// - /// The last generated id. - /// - public RedisValue LastGeneratedId { get; } - } -} diff --git a/src/StackExchange.Redis/StreamPendingInfo.cs b/src/StackExchange.Redis/StreamPendingInfo.cs deleted file mode 100644 index d9393c88e..000000000 --- a/src/StackExchange.Redis/StreamPendingInfo.cs +++ /dev/null @@ -1,40 +0,0 @@ - -namespace StackExchange.Redis -{ - /// - /// Describes basic information about pending messages for a consumer group. - /// - public readonly struct StreamPendingInfo - { - internal StreamPendingInfo(int pendingMessageCount, - RedisValue lowestId, - RedisValue highestId, - StreamConsumer[] consumers) - { - PendingMessageCount = pendingMessageCount; - LowestPendingMessageId = lowestId; - HighestPendingMessageId = highestId; - Consumers = consumers; - } - - /// - /// The number of pending messages. A pending message is a message that has been consumed but not yet acknowledged. - /// - public int PendingMessageCount { get; } - - /// - /// The lowest message ID in the set of pending messages. - /// - public RedisValue LowestPendingMessageId { get; } - - /// - /// The highest message ID in the set of pending messages. - /// - public RedisValue HighestPendingMessageId { get; } - - /// - /// An array of consumers within the consumer group that have pending messages. - /// - public StreamConsumer[] Consumers { get; } - } -} diff --git a/src/StackExchange.Redis/StreamPendingMessageInfo.cs b/src/StackExchange.Redis/StreamPendingMessageInfo.cs deleted file mode 100644 index 84241116d..000000000 --- a/src/StackExchange.Redis/StreamPendingMessageInfo.cs +++ /dev/null @@ -1,41 +0,0 @@ - -namespace StackExchange.Redis -{ - /// - /// Describes properties of a pending message. A pending message is one that has - /// been received by a consumer but has not yet been acknowledged. - /// - public readonly struct StreamPendingMessageInfo - { - internal StreamPendingMessageInfo(RedisValue messageId, - RedisValue consumerName, - long idleTimeInMs, - int deliveryCount) - { - MessageId = messageId; - ConsumerName = consumerName; - IdleTimeInMilliseconds = idleTimeInMs; - DeliveryCount = deliveryCount; - } - - /// - /// The ID of the pending message. - /// - public RedisValue MessageId { get; } - - /// - /// The consumer that received the pending message. - /// - public RedisValue ConsumerName { get; } - - /// - /// The time that has passed since the message was last delivered to a consumer. - /// - public long IdleTimeInMilliseconds { get; } - - /// - /// The number of times the message has been delivered to a consumer. - /// - public int DeliveryCount { get; } - } -} diff --git a/src/StackExchange.Redis/StreamPosition.cs b/src/StackExchange.Redis/StreamPosition.cs deleted file mode 100644 index b58b77304..000000000 --- a/src/StackExchange.Redis/StreamPosition.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; - -namespace StackExchange.Redis -{ - /// - /// Describes a pair consisting of the Stream Key and the from which to begin reading a stream. - /// - public struct StreamPosition - { - /// - /// Read from the beginning of a stream. - /// - public static RedisValue Beginning => StreamConstants.ReadMinValue; - - /// - /// Read new messages. - /// - public static RedisValue NewMessages => StreamConstants.NewMessages; - - /// - /// Initializes a value. - /// - /// The key for the stream. - /// The position from which to begin reading the stream. - public StreamPosition(RedisKey key, RedisValue position) - { - Key = key; - Position = position; - } - - /// - /// The stream key. - /// - public RedisKey Key { get; } - - /// - /// The offset at which to begin reading the stream. - /// - public RedisValue Position { get; } - - internal static RedisValue Resolve(RedisValue value, RedisCommand command) - { - if (value == NewMessages) - { - return command switch - { - RedisCommand.XREAD => throw new InvalidOperationException("StreamPosition.NewMessages cannot be used with StreamRead."), - RedisCommand.XREADGROUP => StreamConstants.UndeliveredMessages, - RedisCommand.XGROUP => StreamConstants.NewMessages, - // new is only valid for the above - _ => throw new ArgumentException($"Unsupported command in StreamPosition.Resolve: {command}.", nameof(command)), - }; - } - else if (value == StreamPosition.Beginning) - { - switch (command) - { - case RedisCommand.XREAD: - case RedisCommand.XREADGROUP: - case RedisCommand.XGROUP: - return StreamConstants.AllMessages; - } - } - return value; - } - } -} From cf3e30bfb263734c124e007a500b7792e724a778 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 16 Apr 2022 19:56:39 -0400 Subject: [PATCH 136/435] Tests: normalize all the things (#2097) In recent PRs, through no fault of their own, I can see a lot of inconsistency because they're based on inconsistency in the existing test suite. This is one major overhaul to unify how things are laid out, connection naming, etc. Once this builds, I'll merge in then tidy up PRs in progress to be on the common scheme and have less confusion for contributors going forward. --- tests/StackExchange.Redis.Tests/AdhocTests.cs | 45 +- .../AggresssiveTests.cs | 481 ++- tests/StackExchange.Redis.Tests/AsyncTests.cs | 129 +- .../AzureMaintenanceEventTests.cs | 73 +- .../StackExchange.Redis.Tests/BacklogTests.cs | 746 +++-- tests/StackExchange.Redis.Tests/BasicOps.cs | 889 +++--- .../BatchWrapperTests.cs | 33 +- tests/StackExchange.Redis.Tests/Batches.cs | 93 +- tests/StackExchange.Redis.Tests/Bits.cs | 33 +- tests/StackExchange.Redis.Tests/BoxUnbox.cs | 291 +- tests/StackExchange.Redis.Tests/Cluster.cs | 1281 ++++---- tests/StackExchange.Redis.Tests/Commands.cs | 84 +- tests/StackExchange.Redis.Tests/Config.cs | 1050 ++++--- .../StackExchange.Redis.Tests/ConnectByIP.cs | 147 +- .../ConnectCustomConfig.cs | 145 +- .../ConnectFailTimeout.cs | 72 +- .../ConnectToUnexistingHost.cs | 121 +- .../ConnectingFailDetection.cs | 269 +- .../ConnectionFailedErrors.cs | 316 +- .../ConnectionReconnectRetryPolicyTests.cs | 89 +- .../ConnectionShutdown.cs | 80 +- .../StackExchange.Redis.Tests/Constraints.cs | 70 +- tests/StackExchange.Redis.Tests/Copy.cs | 106 +- .../DatabaseWrapperTests.cs | 2601 ++++++++-------- tests/StackExchange.Redis.Tests/Databases.cs | 251 +- .../DefaultOptions.cs | 247 +- .../StackExchange.Redis.Tests/DefaultPorts.cs | 89 +- tests/StackExchange.Redis.Tests/Deprecated.cs | 129 +- tests/StackExchange.Redis.Tests/EnvoyTests.cs | 74 +- .../EventArgsTests.cs | 117 +- .../ExceptionFactoryTests.cs | 379 ++- tests/StackExchange.Redis.Tests/Execute.cs | 67 +- tests/StackExchange.Redis.Tests/Expiry.cs | 377 ++- .../StackExchange.Redis.Tests/FSharpCompat.cs | 35 +- tests/StackExchange.Redis.Tests/Failover.cs | 604 ++-- .../StackExchange.Redis.Tests/FeatureFlags.cs | 37 +- .../FloatingPoint.cs | 283 +- .../StackExchange.Redis.Tests/FormatTests.cs | 131 +- .../GarbageCollectionTests.cs | 57 +- tests/StackExchange.Redis.Tests/GeoTests.cs | 1251 ++++---- tests/StackExchange.Redis.Tests/Hashes.cs | 1135 ++++--- .../Helpers/Attributes.cs | 275 +- .../Helpers/Extensions.cs | 35 +- .../Helpers/NonParallelCollection.cs | 11 +- .../StackExchange.Redis.Tests/Helpers/Skip.cs | 70 +- .../Helpers/TestConfig.cs | 151 +- .../Helpers/TextWriterOutputHelper.cs | 149 +- .../StackExchange.Redis.Tests/HyperLogLog.cs | 69 +- .../Issues/BgSaveResponse.cs | 28 +- .../Issues/DefaultDatabase.cs | 87 +- .../Issues/Issue10.cs | 42 +- .../Issues/Issue1101.cs | 340 ++- .../Issues/Issue1103.cs | 100 +- .../Issues/Issue182.cs | 111 +- .../Issues/Issue25.cs | 65 +- .../Issues/Issue6.cs | 26 +- .../Issues/Massive Delete.cs | 116 +- .../Issues/SO10504853.cs | 137 +- .../Issues/SO10825542.cs | 41 +- .../Issues/SO11766033.cs | 65 +- .../Issues/SO22786599.cs | 54 +- .../Issues/SO23949477.cs | 62 +- .../Issues/SO24807536.cs | 78 +- .../Issues/SO25113323.cs | 64 +- .../Issues/SO25567566.cs | 98 +- tests/StackExchange.Redis.Tests/Keys.cs | 538 ++-- .../KeysAndValues.cs | 335 ++- tests/StackExchange.Redis.Tests/Latency.cs | 153 +- tests/StackExchange.Redis.Tests/Lex.cs | 185 +- tests/StackExchange.Redis.Tests/Lists.cs | 1360 +++++---- tests/StackExchange.Redis.Tests/Locking.cs | 391 ++- tests/StackExchange.Redis.Tests/MassiveOps.cs | 185 +- tests/StackExchange.Redis.Tests/Memory.cs | 147 +- tests/StackExchange.Redis.Tests/Migrate.cs | 95 +- tests/StackExchange.Redis.Tests/MultiAdd.cs | 194 +- .../StackExchange.Redis.Tests/MultiPrimary.cs | 136 +- tests/StackExchange.Redis.Tests/Naming.cs | 368 ++- tests/StackExchange.Redis.Tests/Parse.cs | 141 +- .../StackExchange.Redis.Tests/Performance.cs | 198 +- .../PreserveOrder.cs | 100 +- tests/StackExchange.Redis.Tests/Profiling.cs | 616 ++-- tests/StackExchange.Redis.Tests/PubSub.cs | 1419 +++++---- .../PubSubCommand.cs | 129 +- .../PubSubMultiserver.cs | 323 +- .../RawResultTests.cs | 94 +- tests/StackExchange.Redis.Tests/RealWorld.cs | 39 +- .../RedisFeaturesTests.cs | 41 +- .../RedisResultTests.cs | 289 +- .../RedisValueEquivalency.cs | 453 ++- tests/StackExchange.Redis.Tests/Roles.cs | 75 +- tests/StackExchange.Redis.Tests/SSDB.cs | 39 +- tests/StackExchange.Redis.Tests/SSL.cs | 750 +++-- .../StackExchange.Redis.Tests/SanityChecks.cs | 43 +- tests/StackExchange.Redis.Tests/Scans.cs | 736 +++-- tests/StackExchange.Redis.Tests/Scripting.cs | 1810 ++++++------ tests/StackExchange.Redis.Tests/Secure.cs | 119 +- tests/StackExchange.Redis.Tests/Sentinel.cs | 678 +++-- .../StackExchange.Redis.Tests/SentinelBase.cs | 268 +- .../SentinelFailover.cs | 185 +- tests/StackExchange.Redis.Tests/Sets.cs | 609 ++-- .../SharedConnectionFixture.cs | 311 +- tests/StackExchange.Redis.Tests/Sockets.cs | 41 +- tests/StackExchange.Redis.Tests/SortedSets.cs | 2143 +++++++------- tests/StackExchange.Redis.Tests/Streams.cs | 2614 ++++++++--------- tests/StackExchange.Redis.Tests/Strings.cs | 1070 ++++--- tests/StackExchange.Redis.Tests/TestBase.cs | 796 ++--- .../TestExtensions.cs | 18 +- .../TestInfoReplicationChecks.cs | 36 +- .../TransactionWrapperTests.cs | 247 +- .../StackExchange.Redis.Tests/Transactions.cs | 2379 ++++++++------- tests/StackExchange.Redis.Tests/Values.cs | 83 +- .../WithKeyPrefixTests.cs | 231 +- .../WrapperBaseTests.cs | 1 - 113 files changed, 19484 insertions(+), 20508 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/AdhocTests.cs b/tests/StackExchange.Redis.Tests/AdhocTests.cs index 924669ee5..50ceb88c4 100644 --- a/tests/StackExchange.Redis.Tests/AdhocTests.cs +++ b/tests/StackExchange.Redis.Tests/AdhocTests.cs @@ -1,34 +1,31 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class AdhocTests : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class AdhocTests : TestBase - { - public AdhocTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public AdhocTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } - [Fact] - public void TestAdhocCommandsAPI() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); + [Fact] + public void TestAdhocCommandsAPI() + { + using var conn = Create(); + var db = conn.GetDatabase(); - // needs explicit RedisKey type for key-based - // sharding to work; will still work with strings, - // but no key-based sharding support - RedisKey key = Me(); + // needs explicit RedisKey type for key-based + // sharding to work; will still work with strings, + // but no key-based sharding support + RedisKey key = Me(); - // note: if command renames are configured in - // the API, they will still work automatically - db.Execute("del", key); - db.Execute("set", key, "12"); - db.Execute("incrby", key, 4); - int i = (int)db.Execute("get", key); + // note: if command renames are configured in + // the API, they will still work automatically + db.Execute("del", key); + db.Execute("set", key, "12"); + db.Execute("incrby", key, 4); + int i = (int)db.Execute("get", key); - Assert.Equal(16, i); - } - } + Assert.Equal(16, i); } } diff --git a/tests/StackExchange.Redis.Tests/AggresssiveTests.cs b/tests/StackExchange.Redis.Tests/AggresssiveTests.cs index fd2a45cc2..04d472965 100644 --- a/tests/StackExchange.Redis.Tests/AggresssiveTests.cs +++ b/tests/StackExchange.Redis.Tests/AggresssiveTests.cs @@ -3,321 +3,312 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(NonParallelCollection.Name)] +public class AggresssiveTests : TestBase { - [Collection(NonParallelCollection.Name)] - public class AggresssiveTests : TestBase + public AggresssiveTests(ITestOutputHelper output) : base(output) { } + + [FactLongRunning] + public async Task ParallelTransactionsWithConditions() { - public AggresssiveTests(ITestOutputHelper output) : base(output) { } + const int Muxers = 4, Workers = 20, PerThread = 250; - [FactLongRunning] - public async Task ParallelTransactionsWithConditions() + var muxers = new IConnectionMultiplexer[Muxers]; + try { - const int Muxers = 4, Workers = 20, PerThread = 250; - - var muxers = new IConnectionMultiplexer[Muxers]; - try - { - for (int i = 0; i < Muxers; i++) - muxers[i] = Create(); + for (int i = 0; i < Muxers; i++) + muxers[i] = Create(); - RedisKey hits = Me(), trigger = Me() + "3"; - int expectedSuccess = 0; + RedisKey hits = Me(), trigger = Me() + "3"; + int expectedSuccess = 0; - await muxers[0].GetDatabase().KeyDeleteAsync(new[] { hits, trigger }).ForAwait(); + await muxers[0].GetDatabase().KeyDeleteAsync(new[] { hits, trigger }).ForAwait(); - Task[] tasks = new Task[Workers]; - for (int i = 0; i < tasks.Length; i++) + Task[] tasks = new Task[Workers]; + for (int i = 0; i < tasks.Length; i++) + { + var scopedDb = muxers[i % Muxers].GetDatabase(); + tasks[i] = Task.Run(async () => { - var scopedDb = muxers[i % Muxers].GetDatabase(); - tasks[i] = Task.Run(async () => + for (int j = 0; j < PerThread; j++) { - for (int j = 0; j < PerThread; j++) + var oldVal = await scopedDb.StringGetAsync(trigger).ForAwait(); + var tran = scopedDb.CreateTransaction(); + tran.AddCondition(Condition.StringEqual(trigger, oldVal)); + var x = tran.StringIncrementAsync(trigger); + var y = tran.StringIncrementAsync(hits); + if (await tran.ExecuteAsync().ForAwait()) { - var oldVal = await scopedDb.StringGetAsync(trigger).ForAwait(); - var tran = scopedDb.CreateTransaction(); - tran.AddCondition(Condition.StringEqual(trigger, oldVal)); - var x = tran.StringIncrementAsync(trigger); - var y = tran.StringIncrementAsync(hits); - if (await tran.ExecuteAsync().ForAwait()) - { - Interlocked.Increment(ref expectedSuccess); - await x; - await y; - } - else - { - await Assert.ThrowsAsync(() => x).ForAwait(); - await Assert.ThrowsAsync(() => y).ForAwait(); - } + Interlocked.Increment(ref expectedSuccess); + await x; + await y; } - }); - } - for (int i = tasks.Length - 1; i >= 0; i--) - { - await tasks[i]; - } - var actual = (int)await muxers[0].GetDatabase().StringGetAsync(hits).ForAwait(); - Assert.Equal(expectedSuccess, actual); - Writer.WriteLine($"success: {actual} out of {Workers * PerThread} attempts"); + else + { + await Assert.ThrowsAsync(() => x).ForAwait(); + await Assert.ThrowsAsync(() => y).ForAwait(); + } + } + }); } - finally + for (int i = tasks.Length - 1; i >= 0; i--) { - for (int i = 0; i < muxers.Length; i++) - { - try { muxers[i]?.Dispose(); } - catch { /* Don't care */ } - } + await tasks[i]; } + var actual = (int)await muxers[0].GetDatabase().StringGetAsync(hits).ForAwait(); + Assert.Equal(expectedSuccess, actual); + Writer.WriteLine($"success: {actual} out of {Workers * PerThread} attempts"); } - - private const int IterationCount = 5000, InnerCount = 20; - - [FactLongRunning] - public void RunCompetingBatchesOnSameMuxer() + finally { - using (var muxer = Create()) + for (int i = 0; i < muxers.Length; i++) { - var db = muxer.GetDatabase(); + try { muxers[i]?.Dispose(); } + catch { /* Don't care */ } + } + } + } - Thread x = new Thread(state => BatchRunPings((IDatabase)state!)) - { - Name = nameof(BatchRunPings) - }; - Thread y = new Thread(state => BatchRunIntegers((IDatabase)state!)) - { - Name = nameof(BatchRunIntegers) - }; + private const int IterationCount = 5000, InnerCount = 20; - x.Start(db); - y.Start(db); - x.Join(); - y.Join(); + [FactLongRunning] + public void RunCompetingBatchesOnSameMuxer() + { + using var conn = Create(); + var db = conn.GetDatabase(); - Writer.WriteLine(muxer.GetCounters().Interactive); - } - } + Thread x = new Thread(state => BatchRunPings((IDatabase)state!)) + { + Name = nameof(BatchRunPings) + }; + Thread y = new Thread(state => BatchRunIntegers((IDatabase)state!)) + { + Name = nameof(BatchRunIntegers) + }; - private void BatchRunIntegers(IDatabase db) + x.Start(db); + y.Start(db); + x.Join(); + y.Join(); + + Writer.WriteLine(conn.GetCounters().Interactive); + } + + private void BatchRunIntegers(IDatabase db) + { + var key = Me(); + db.KeyDelete(key); + db.StringSet(key, 1); + Task[] tasks = new Task[InnerCount]; + for(int i = 0; i < IterationCount; i++) { - var key = Me(); - db.KeyDelete(key); - db.StringSet(key, 1); - Task[] tasks = new Task[InnerCount]; - for(int i = 0; i < IterationCount; i++) + var batch = db.CreateBatch(); + for (int j = 0; j < tasks.Length; j++) { - var batch = db.CreateBatch(); - for (int j = 0; j < tasks.Length; j++) - { - tasks[j] = batch.StringIncrementAsync(key); - } - batch.Execute(); - db.Multiplexer.WaitAll(tasks); + tasks[j] = batch.StringIncrementAsync(key); } - - var count = (long)db.StringGet(key); - Writer.WriteLine($"tally: {count}"); + batch.Execute(); + db.Multiplexer.WaitAll(tasks); } - private static void BatchRunPings(IDatabase db) + var count = (long)db.StringGet(key); + Writer.WriteLine($"tally: {count}"); + } + + private static void BatchRunPings(IDatabase db) + { + Task[] tasks = new Task[InnerCount]; + for (int i = 0; i < IterationCount; i++) { - Task[] tasks = new Task[InnerCount]; - for (int i = 0; i < IterationCount; i++) + var batch = db.CreateBatch(); + for (int j = 0; j < tasks.Length; j++) { - var batch = db.CreateBatch(); - for (int j = 0; j < tasks.Length; j++) - { - tasks[j] = batch.PingAsync(); - } - batch.Execute(); - db.Multiplexer.WaitAll(tasks); + tasks[j] = batch.PingAsync(); } + batch.Execute(); + db.Multiplexer.WaitAll(tasks); } + } - [FactLongRunning] - public async Task RunCompetingBatchesOnSameMuxerAsync() - { - using (var muxer = Create()) - { - var db = muxer.GetDatabase(); + [FactLongRunning] + public async Task RunCompetingBatchesOnSameMuxerAsync() + { + using var conn = Create(); + var db = conn.GetDatabase(); - var x = Task.Run(() => BatchRunPingsAsync(db)); - var y = Task.Run(() => BatchRunIntegersAsync(db)); + var x = Task.Run(() => BatchRunPingsAsync(db)); + var y = Task.Run(() => BatchRunIntegersAsync(db)); - await x; - await y; + await x; + await y; - Writer.WriteLine(muxer.GetCounters().Interactive); - } - } + Writer.WriteLine(conn.GetCounters().Interactive); + } - private async Task BatchRunIntegersAsync(IDatabase db) + private async Task BatchRunIntegersAsync(IDatabase db) + { + var key = Me(); + await db.KeyDeleteAsync(key).ForAwait(); + await db.StringSetAsync(key, 1).ForAwait(); + Task[] tasks = new Task[InnerCount]; + for (int i = 0; i < IterationCount; i++) { - var key = Me(); - await db.KeyDeleteAsync(key).ForAwait(); - await db.StringSetAsync(key, 1).ForAwait(); - Task[] tasks = new Task[InnerCount]; - for (int i = 0; i < IterationCount; i++) + var batch = db.CreateBatch(); + for (int j = 0; j < tasks.Length; j++) { - var batch = db.CreateBatch(); - for (int j = 0; j < tasks.Length; j++) - { - tasks[j] = batch.StringIncrementAsync(key); - } - batch.Execute(); - for(int j = tasks.Length - 1; j >= 0;j--) - { - await tasks[j]; - } + tasks[j] = batch.StringIncrementAsync(key); + } + batch.Execute(); + for(int j = tasks.Length - 1; j >= 0;j--) + { + await tasks[j]; } - - var count = (long)await db.StringGetAsync(key).ForAwait(); - Writer.WriteLine($"tally: {count}"); } - private static async Task BatchRunPingsAsync(IDatabase db) + var count = (long)await db.StringGetAsync(key).ForAwait(); + Writer.WriteLine($"tally: {count}"); + } + + private static async Task BatchRunPingsAsync(IDatabase db) + { + Task[] tasks = new Task[InnerCount]; + for (int i = 0; i < IterationCount; i++) { - Task[] tasks = new Task[InnerCount]; - for (int i = 0; i < IterationCount; i++) + var batch = db.CreateBatch(); + for (int j = 0; j < tasks.Length; j++) { - var batch = db.CreateBatch(); - for (int j = 0; j < tasks.Length; j++) - { - tasks[j] = batch.PingAsync(); - } - batch.Execute(); - for (int j = tasks.Length - 1; j >= 0; j--) - { - await tasks[j]; - } + tasks[j] = batch.PingAsync(); + } + batch.Execute(); + for (int j = tasks.Length - 1; j >= 0; j--) + { + await tasks[j]; } } + } - [FactLongRunning] - public void RunCompetingTransactionsOnSameMuxer() - { - using (var muxer = Create(logTransactionData: false)) - { - var db = muxer.GetDatabase(); + [FactLongRunning] + public void RunCompetingTransactionsOnSameMuxer() + { + using var conn = Create(logTransactionData: false); + var db = conn.GetDatabase(); - Thread x = new Thread(state => TranRunPings((IDatabase)state!)) - { - Name = nameof(BatchRunPings) - }; - Thread y = new Thread(state => TranRunIntegers((IDatabase)state!)) - { - Name = nameof(BatchRunIntegers) - }; + Thread x = new Thread(state => TranRunPings((IDatabase)state!)) + { + Name = nameof(BatchRunPings) + }; + Thread y = new Thread(state => TranRunIntegers((IDatabase)state!)) + { + Name = nameof(BatchRunIntegers) + }; - x.Start(db); - y.Start(db); - x.Join(); - y.Join(); + x.Start(db); + y.Start(db); + x.Join(); + y.Join(); - Writer.WriteLine(muxer.GetCounters().Interactive); - } - } + Writer.WriteLine(conn.GetCounters().Interactive); + } - private void TranRunIntegers(IDatabase db) + private void TranRunIntegers(IDatabase db) + { + var key = Me(); + db.KeyDelete(key); + db.StringSet(key, 1); + Task[] tasks = new Task[InnerCount]; + for (int i = 0; i < IterationCount; i++) { - var key = Me(); - db.KeyDelete(key); - db.StringSet(key, 1); - Task[] tasks = new Task[InnerCount]; - for (int i = 0; i < IterationCount; i++) + var batch = db.CreateTransaction(); + batch.AddCondition(Condition.KeyExists(key)); + for (int j = 0; j < tasks.Length; j++) { - var batch = db.CreateTransaction(); - batch.AddCondition(Condition.KeyExists(key)); - for (int j = 0; j < tasks.Length; j++) - { - tasks[j] = batch.StringIncrementAsync(key); - } - batch.Execute(); - db.Multiplexer.WaitAll(tasks); + tasks[j] = batch.StringIncrementAsync(key); } - - var count = (long)db.StringGet(key); - Writer.WriteLine($"tally: {count}"); + batch.Execute(); + db.Multiplexer.WaitAll(tasks); } - private static void TranRunPings(IDatabase db) + var count = (long)db.StringGet(key); + Writer.WriteLine($"tally: {count}"); + } + + private static void TranRunPings(IDatabase db) + { + var key = Me(); + db.KeyDelete(key); + Task[] tasks = new Task[InnerCount]; + for (int i = 0; i < IterationCount; i++) { - var key = Me(); - db.KeyDelete(key); - Task[] tasks = new Task[InnerCount]; - for (int i = 0; i < IterationCount; i++) + var batch = db.CreateTransaction(); + batch.AddCondition(Condition.KeyNotExists(key)); + for (int j = 0; j < tasks.Length; j++) { - var batch = db.CreateTransaction(); - batch.AddCondition(Condition.KeyNotExists(key)); - for (int j = 0; j < tasks.Length; j++) - { - tasks[j] = batch.PingAsync(); - } - batch.Execute(); - db.Multiplexer.WaitAll(tasks); + tasks[j] = batch.PingAsync(); } + batch.Execute(); + db.Multiplexer.WaitAll(tasks); } + } - [FactLongRunning] - public async Task RunCompetingTransactionsOnSameMuxerAsync() - { - using (var muxer = Create(logTransactionData: false)) - { - var db = muxer.GetDatabase(); + [FactLongRunning] + public async Task RunCompetingTransactionsOnSameMuxerAsync() + { + using var conn = Create(logTransactionData: false); + var db = conn.GetDatabase(); - var x = Task.Run(() => TranRunPingsAsync(db)); - var y = Task.Run(() => TranRunIntegersAsync(db)); + var x = Task.Run(() => TranRunPingsAsync(db)); + var y = Task.Run(() => TranRunIntegersAsync(db)); - await x; - await y; + await x; + await y; - Writer.WriteLine(muxer.GetCounters().Interactive); - } - } + Writer.WriteLine(conn.GetCounters().Interactive); + } - private async Task TranRunIntegersAsync(IDatabase db) + private async Task TranRunIntegersAsync(IDatabase db) + { + var key = Me(); + await db.KeyDeleteAsync(key).ForAwait(); + await db.StringSetAsync(key, 1).ForAwait(); + Task[] tasks = new Task[InnerCount]; + for (int i = 0; i < IterationCount; i++) { - var key = Me(); - await db.KeyDeleteAsync(key).ForAwait(); - await db.StringSetAsync(key, 1).ForAwait(); - Task[] tasks = new Task[InnerCount]; - for (int i = 0; i < IterationCount; i++) + var batch = db.CreateTransaction(); + batch.AddCondition(Condition.KeyExists(key)); + for (int j = 0; j < tasks.Length; j++) { - var batch = db.CreateTransaction(); - batch.AddCondition(Condition.KeyExists(key)); - for (int j = 0; j < tasks.Length; j++) - { - tasks[j] = batch.StringIncrementAsync(key); - } - await batch.ExecuteAsync().ForAwait(); - for (int j = tasks.Length - 1; j >= 0; j--) - { - await tasks[j]; - } + tasks[j] = batch.StringIncrementAsync(key); + } + await batch.ExecuteAsync().ForAwait(); + for (int j = tasks.Length - 1; j >= 0; j--) + { + await tasks[j]; } - - var count = (long)await db.StringGetAsync(key).ForAwait(); - Writer.WriteLine($"tally: {count}"); } - private static async Task TranRunPingsAsync(IDatabase db) + var count = (long)await db.StringGetAsync(key).ForAwait(); + Writer.WriteLine($"tally: {count}"); + } + + private static async Task TranRunPingsAsync(IDatabase db) + { + var key = Me(); + db.KeyDelete(key); + Task[] tasks = new Task[InnerCount]; + for (int i = 0; i < IterationCount; i++) { - var key = Me(); - db.KeyDelete(key); - Task[] tasks = new Task[InnerCount]; - for (int i = 0; i < IterationCount; i++) + var batch = db.CreateTransaction(); + batch.AddCondition(Condition.KeyNotExists(key)); + for (int j = 0; j < tasks.Length; j++) { - var batch = db.CreateTransaction(); - batch.AddCondition(Condition.KeyNotExists(key)); - for (int j = 0; j < tasks.Length; j++) - { - tasks[j] = batch.PingAsync(); - } - await batch.ExecuteAsync().ForAwait(); - for (int j = tasks.Length - 1; j >= 0; j--) - { - await tasks[j]; - } + tasks[j] = batch.PingAsync(); + } + await batch.ExecuteAsync().ForAwait(); + for (int j = tasks.Length - 1; j >= 0; j--) + { + await tasks[j]; } } } diff --git a/tests/StackExchange.Redis.Tests/AsyncTests.cs b/tests/StackExchange.Redis.Tests/AsyncTests.cs index 91098ebf1..e754cda7c 100644 --- a/tests/StackExchange.Redis.Tests/AsyncTests.cs +++ b/tests/StackExchange.Redis.Tests/AsyncTests.cs @@ -5,82 +5,77 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(NonParallelCollection.Name)] +public class AsyncTests : TestBase { - [Collection(NonParallelCollection.Name)] - public class AsyncTests : TestBase + public AsyncTests(ITestOutputHelper output) : base(output) { } + + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; + + [Fact] + public void AsyncTasksReportFailureIfServerUnavailable() { - public AsyncTests(ITestOutputHelper output) : base(output) { } + SetExpectedAmbientFailureCount(-1); // this will get messy - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; + using var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); + var server = conn.GetServer(TestConfig.Current.PrimaryServer, TestConfig.Current.PrimaryPort); - [Fact] - public void AsyncTasksReportFailureIfServerUnavailable() - { - SetExpectedAmbientFailureCount(-1); // this will get messy - - using (var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast)) - { - var server = conn.GetServer(TestConfig.Current.PrimaryServer, TestConfig.Current.PrimaryPort); - - RedisKey key = Me(); - var db = conn.GetDatabase(); - db.KeyDelete(key); - var a = db.SetAddAsync(key, "a"); - var b = db.SetAddAsync(key, "b"); - - Assert.True(conn.Wait(a)); - Assert.True(conn.Wait(b)); - - conn.AllowConnect = false; - server.SimulateConnectionFailure(SimulatedFailureType.All); - var c = db.SetAddAsync(key, "c"); - - Assert.True(c.IsFaulted, "faulted"); - Assert.NotNull(c.Exception); - var ex = c.Exception.InnerExceptions.Single(); - Assert.IsType(ex); - Assert.StartsWith("No connection is active/available to service this operation: SADD " + key.ToString(), ex.Message); - } + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key); + var a = db.SetAddAsync(key, "a"); + var b = db.SetAddAsync(key, "b"); + + Assert.True(conn.Wait(a)); + Assert.True(conn.Wait(b)); + + conn.AllowConnect = false; + server.SimulateConnectionFailure(SimulatedFailureType.All); + var c = db.SetAddAsync(key, "c"); + + Assert.True(c.IsFaulted, "faulted"); + Assert.NotNull(c.Exception); + var ex = c.Exception.InnerExceptions.Single(); + Assert.IsType(ex); + Assert.StartsWith("No connection is active/available to service this operation: SADD " + key.ToString(), ex.Message); + } + + [Fact] + public async Task AsyncTimeoutIsNoticed() + { + using var conn = Create(syncTimeout: 1000); + var opt = ConfigurationOptions.Parse(conn.Configuration); + if (!Debugger.IsAttached) + { // we max the timeouts if a degugger is detected + Assert.Equal(1000, opt.AsyncTimeout); } - [Fact] - public async Task AsyncTimeoutIsNoticed() + RedisKey key = Me(); + var val = Guid.NewGuid().ToString(); + var db = conn.GetDatabase(); + db.StringSet(key, val); + + Assert.Contains("; async timeouts: 0;", conn.GetStatus()); + + await db.ExecuteAsync("client", "pause", 4000).ForAwait(); // client pause returns immediately + + var ms = Stopwatch.StartNew(); + var ex = await Assert.ThrowsAsync(async () => { - using (var conn = Create(syncTimeout: 1000)) - { - var opt = ConfigurationOptions.Parse(conn.Configuration); - if (!Debugger.IsAttached) - { // we max the timeouts if a degugger is detected - Assert.Equal(1000, opt.AsyncTimeout); - } - - RedisKey key = Me(); - var val = Guid.NewGuid().ToString(); - var db = conn.GetDatabase(); - db.StringSet(key, val); - - Assert.Contains("; async timeouts: 0;", conn.GetStatus()); - - await db.ExecuteAsync("client", "pause", 4000).ForAwait(); // client pause returns immediately - - var ms = Stopwatch.StartNew(); - var ex = await Assert.ThrowsAsync(async () => - { - await db.StringGetAsync(key).ForAwait(); // but *subsequent* operations are paused - ms.Stop(); - Writer.WriteLine($"Unexpectedly succeeded after {ms.ElapsedMilliseconds}ms"); - }).ForAwait(); + await db.StringGetAsync(key).ForAwait(); // but *subsequent* operations are paused ms.Stop(); - Writer.WriteLine($"Timed out after {ms.ElapsedMilliseconds}ms"); + Writer.WriteLine($"Unexpectedly succeeded after {ms.ElapsedMilliseconds}ms"); + }).ForAwait(); + ms.Stop(); + Writer.WriteLine($"Timed out after {ms.ElapsedMilliseconds}ms"); - Assert.Contains("Timeout awaiting response", ex.Message); - Writer.WriteLine(ex.Message); + Assert.Contains("Timeout awaiting response", ex.Message); + Writer.WriteLine(ex.Message); - string status = conn.GetStatus(); - Writer.WriteLine(status); - Assert.Contains("; async timeouts: 1;", status); - } - } + string status = conn.GetStatus(); + Writer.WriteLine(status); + Assert.Contains("; async timeouts: 1;", status); } } diff --git a/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs b/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs index 81359be79..4e3bdcbd6 100644 --- a/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs +++ b/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs @@ -5,49 +5,46 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class AzureMaintenanceEventTests : TestBase { - public class AzureMaintenanceEventTests : TestBase + public AzureMaintenanceEventTests(ITestOutputHelper output) : base(output) { } + + [Theory] + [InlineData("NotificationType|NodeMaintenanceStarting|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress||SSLPort|15001|NonSSLPort|13001", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, null, 15001, 13001)] + [InlineData("NotificationType|NodeMaintenanceFailover|StartTimeInUTC||IsReplica|False|IPAddress||SSLPort|15001|NonSSLPort|13001", AzureNotificationType.NodeMaintenanceFailoverComplete, null, false, null, 15001, 13001)] + [InlineData("NotificationType|NodeMaintenanceFailover|StartTimeInUTC||IsReplica|True|IPAddress||SSLPort|15001|NonSSLPort|13001", AzureNotificationType.NodeMaintenanceFailoverComplete, null, true, null, 15001, 13001)] + [InlineData("NotificationType|NodeMaintenanceStarting|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|j|IPAddress||SSLPort|char|NonSSLPort|char", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, null, 0, 0)] + [InlineData("NotificationType|NodeMaintenanceStarting|somejunkkey|somejunkvalue|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress||SSLPort|15999|NonSSLPort|139991", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, null, 15999, 139991)] + [InlineData("NotificationType|NodeMaintenanceStarting|somejunkkey|somejunkvalue|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress|127.0.0.1|SSLPort|15999|NonSSLPort|139991", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, "127.0.0.1", 15999, 139991)] + [InlineData("NotificationType|NodeMaintenanceScaleComplete|somejunkkey|somejunkvalue|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress|127.0.0.1|SSLPort|15999|NonSSLPort|139991", AzureNotificationType.NodeMaintenanceScaleComplete, "2021-03-02T23:26:57", false, "127.0.0.1", 15999, 139991)] + [InlineData("NotificationTypeNodeMaintenanceStartingsomejunkkeysomejunkvalueStartTimeInUTC2021-03-02T23:26:57IsReplicaFalseIPAddress127.0.0.1SSLPort15999NonSSLPort139991", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("NotificationType|", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("NotificationType|NodeMaintenanceStarting1", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("1|2|3", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("StartTimeInUTC|", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("IsReplica|", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("SSLPort|", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("NonSSLPort |", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData("StartTimeInUTC|thisisthestart", AzureNotificationType.Unknown, null, false, null, 0, 0)] + [InlineData(null, AzureNotificationType.Unknown, null, false, null, 0, 0)] + public void TestAzureMaintenanceEventStrings(string message, AzureNotificationType expectedEventType, string expectedStart, bool expectedIsReplica, string expectedIP, int expectedSSLPort, int expectedNonSSLPort) { - public AzureMaintenanceEventTests(ITestOutputHelper output) : base(output) + DateTime? expectedStartTimeUtc = null; + if (expectedStart != null && DateTime.TryParseExact(expectedStart, "s", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTime startTimeUtc)) { + expectedStartTimeUtc = DateTime.SpecifyKind(startTimeUtc, DateTimeKind.Utc); } + _ = IPAddress.TryParse(expectedIP, out IPAddress? expectedIPAddress); - [Theory] - [InlineData("NotificationType|NodeMaintenanceStarting|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress||SSLPort|15001|NonSSLPort|13001", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, null, 15001, 13001)] - [InlineData("NotificationType|NodeMaintenanceFailover|StartTimeInUTC||IsReplica|False|IPAddress||SSLPort|15001|NonSSLPort|13001", AzureNotificationType.NodeMaintenanceFailoverComplete, null, false, null, 15001, 13001)] - [InlineData("NotificationType|NodeMaintenanceFailover|StartTimeInUTC||IsReplica|True|IPAddress||SSLPort|15001|NonSSLPort|13001", AzureNotificationType.NodeMaintenanceFailoverComplete, null, true, null, 15001, 13001)] - [InlineData("NotificationType|NodeMaintenanceStarting|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|j|IPAddress||SSLPort|char|NonSSLPort|char", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, null, 0, 0)] - [InlineData("NotificationType|NodeMaintenanceStarting|somejunkkey|somejunkvalue|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress||SSLPort|15999|NonSSLPort|139991", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, null, 15999, 139991)] - [InlineData("NotificationType|NodeMaintenanceStarting|somejunkkey|somejunkvalue|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress|127.0.0.1|SSLPort|15999|NonSSLPort|139991", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, "127.0.0.1", 15999, 139991)] - [InlineData("NotificationType|NodeMaintenanceScaleComplete|somejunkkey|somejunkvalue|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress|127.0.0.1|SSLPort|15999|NonSSLPort|139991", AzureNotificationType.NodeMaintenanceScaleComplete, "2021-03-02T23:26:57", false, "127.0.0.1", 15999, 139991)] - [InlineData("NotificationTypeNodeMaintenanceStartingsomejunkkeysomejunkvalueStartTimeInUTC2021-03-02T23:26:57IsReplicaFalseIPAddress127.0.0.1SSLPort15999NonSSLPort139991", AzureNotificationType.Unknown, null, false, null, 0, 0)] - [InlineData("NotificationType|", AzureNotificationType.Unknown, null, false, null, 0, 0)] - [InlineData("NotificationType|NodeMaintenanceStarting1", AzureNotificationType.Unknown, null, false, null, 0, 0)] - [InlineData("1|2|3", AzureNotificationType.Unknown, null, false, null, 0, 0)] - [InlineData("StartTimeInUTC|", AzureNotificationType.Unknown, null, false, null, 0, 0)] - [InlineData("IsReplica|", AzureNotificationType.Unknown, null, false, null, 0, 0)] - [InlineData("SSLPort|", AzureNotificationType.Unknown, null, false, null, 0, 0)] - [InlineData("NonSSLPort |", AzureNotificationType.Unknown, null, false, null, 0, 0)] - [InlineData("StartTimeInUTC|thisisthestart", AzureNotificationType.Unknown, null, false, null, 0, 0)] - [InlineData(null, AzureNotificationType.Unknown, null, false, null, 0, 0)] - public void TestAzureMaintenanceEventStrings(string message, AzureNotificationType expectedEventType, string expectedStart, bool expectedIsReplica, string expectedIP, int expectedSSLPort, int expectedNonSSLPort) - { - DateTime? expectedStartTimeUtc = null; - if (expectedStart != null && DateTime.TryParseExact(expectedStart, "s", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTime startTimeUtc)) - { - expectedStartTimeUtc = DateTime.SpecifyKind(startTimeUtc, DateTimeKind.Utc); - } - _ = IPAddress.TryParse(expectedIP, out IPAddress? expectedIPAddress); + var azureMaintenance = new AzureMaintenanceEvent(message); - var azureMaintenance = new AzureMaintenanceEvent(message); - - Assert.Equal(expectedEventType, azureMaintenance.NotificationType); - Assert.Equal(expectedStartTimeUtc, azureMaintenance.StartTimeUtc); - Assert.Equal(expectedIsReplica, azureMaintenance.IsReplica); - Assert.Equal(expectedIPAddress, azureMaintenance.IPAddress); - Assert.Equal(expectedSSLPort, azureMaintenance.SslPort); - Assert.Equal(expectedNonSSLPort, azureMaintenance.NonSslPort); - } + Assert.Equal(expectedEventType, azureMaintenance.NotificationType); + Assert.Equal(expectedStartTimeUtc, azureMaintenance.StartTimeUtc); + Assert.Equal(expectedIsReplica, azureMaintenance.IsReplica); + Assert.Equal(expectedIPAddress, azureMaintenance.IPAddress); + Assert.Equal(expectedSSLPort, azureMaintenance.SslPort); + Assert.Equal(expectedNonSSLPort, azureMaintenance.NonSslPort); } } diff --git a/tests/StackExchange.Redis.Tests/BacklogTests.cs b/tests/StackExchange.Redis.Tests/BacklogTests.cs index aa4c54d9b..30ad1cad9 100644 --- a/tests/StackExchange.Redis.Tests/BacklogTests.cs +++ b/tests/StackExchange.Redis.Tests/BacklogTests.cs @@ -1,405 +1,403 @@ using System; -using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class BacklogTests : TestBase { - public class BacklogTests : TestBase - { - public BacklogTests(ITestOutputHelper output) : base (output) { } + public BacklogTests(ITestOutputHelper output) : base (output) { } - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; - [Fact] - public async Task FailFast() + [Fact] + public async Task FailFast() + { + void PrintSnapshot(ConnectionMultiplexer muxer) { - void PrintSnapshot(ConnectionMultiplexer muxer) + Writer.WriteLine("Snapshot summary:"); + foreach (var server in muxer.GetServerSnapshot()) { - Writer.WriteLine("Snapshot summary:"); - foreach (var server in muxer.GetServerSnapshot()) - { - Writer.WriteLine($" {server.EndPoint}: "); - Writer.WriteLine($" Type: {server.ServerType}"); - Writer.WriteLine($" IsConnected: {server.IsConnected}"); - Writer.WriteLine($" IsConnecting: {server.IsConnecting}"); - Writer.WriteLine($" IsSelectable(allowDisconnected: true): {server.IsSelectable(RedisCommand.PING, true)}"); - Writer.WriteLine($" IsSelectable(allowDisconnected: false): {server.IsSelectable(RedisCommand.PING, false)}"); - Writer.WriteLine($" UnselectableFlags: {server.GetUnselectableFlags()}"); - var bridge = server.GetBridge(RedisCommand.PING, create: false); - Writer.WriteLine($" GetBridge: {bridge}"); - Writer.WriteLine($" IsConnected: {bridge?.IsConnected}"); - Writer.WriteLine($" ConnectionState: {bridge?.ConnectionState}"); - } + Writer.WriteLine($" {server.EndPoint}: "); + Writer.WriteLine($" Type: {server.ServerType}"); + Writer.WriteLine($" IsConnected: {server.IsConnected}"); + Writer.WriteLine($" IsConnecting: {server.IsConnecting}"); + Writer.WriteLine($" IsSelectable(allowDisconnected: true): {server.IsSelectable(RedisCommand.PING, true)}"); + Writer.WriteLine($" IsSelectable(allowDisconnected: false): {server.IsSelectable(RedisCommand.PING, false)}"); + Writer.WriteLine($" UnselectableFlags: {server.GetUnselectableFlags()}"); + var bridge = server.GetBridge(RedisCommand.PING, create: false); + Writer.WriteLine($" GetBridge: {bridge}"); + Writer.WriteLine($" IsConnected: {bridge?.IsConnected}"); + Writer.WriteLine($" ConnectionState: {bridge?.ConnectionState}"); } + } - try - { - // Ensuring the FailFast policy errors immediate with no connection available exceptions - var options = new ConfigurationOptions() - { - BacklogPolicy = BacklogPolicy.FailFast, - AbortOnConnectFail = false, - ConnectTimeout = 1000, - ConnectRetry = 2, - SyncTimeout = 10000, - KeepAlive = 10000, - AsyncTimeout = 5000, - AllowAdmin = true, - }; - options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); - - using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); - - var db = muxer.GetDatabase(); - Writer.WriteLine("Test: Initial (connected) ping"); - await db.PingAsync(); - - var server = muxer.GetServerSnapshot()[0]; - var stats = server.GetBridgeStatus(ConnectionType.Interactive); - Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal - - // Fail the connection - Writer.WriteLine("Test: Simulating failure"); - muxer.AllowConnect = false; - server.SimulateConnectionFailure(SimulatedFailureType.All); - Assert.False(muxer.IsConnected); - - // Queue up some commands - Writer.WriteLine("Test: Disconnected pings"); - await Assert.ThrowsAsync(() => db.PingAsync()); - - var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); - Assert.False(muxer.IsConnected); - Assert.Equal(0, disconnectedStats.BacklogMessagesPending); - - Writer.WriteLine("Test: Allowing reconnect"); - muxer.AllowConnect = true; - Writer.WriteLine("Test: Awaiting reconnect"); - await UntilConditionAsync(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); - - Writer.WriteLine("Test: Reconnecting"); - Assert.True(muxer.IsConnected); - Assert.True(server.IsConnected); - var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); - Assert.Equal(0, reconnectedStats.BacklogMessagesPending); - - _ = db.PingAsync(); - _ = db.PingAsync(); - var lastPing = db.PingAsync(); - - // For debug, print out the snapshot and server states - PrintSnapshot(muxer); - - Assert.NotNull(muxer.SelectServer(Message.Create(-1, CommandFlags.None, RedisCommand.PING))); - - // We should see none queued - Assert.Equal(0, stats.BacklogMessagesPending); - await lastPing; - } - finally + try + { + // Ensuring the FailFast policy errors immediate with no connection available exceptions + var options = new ConfigurationOptions() { - ClearAmbientFailures(); - } + BacklogPolicy = BacklogPolicy.FailFast, + AbortOnConnectFail = false, + ConnectTimeout = 1000, + ConnectRetry = 2, + SyncTimeout = 10000, + KeepAlive = 10000, + AsyncTimeout = 5000, + AllowAdmin = true, + }; + options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); + + using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + + var db = conn.GetDatabase(); + Writer.WriteLine("Test: Initial (connected) ping"); + await db.PingAsync(); + + var server = conn.GetServerSnapshot()[0]; + var stats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal + + // Fail the connection + Writer.WriteLine("Test: Simulating failure"); + conn.AllowConnect = false; + server.SimulateConnectionFailure(SimulatedFailureType.All); + Assert.False(conn.IsConnected); + + // Queue up some commands + Writer.WriteLine("Test: Disconnected pings"); + await Assert.ThrowsAsync(() => db.PingAsync()); + + var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.False(conn.IsConnected); + Assert.Equal(0, disconnectedStats.BacklogMessagesPending); + + Writer.WriteLine("Test: Allowing reconnect"); + conn.AllowConnect = true; + Writer.WriteLine("Test: Awaiting reconnect"); + await UntilConditionAsync(TimeSpan.FromSeconds(3), () => conn.IsConnected).ForAwait(); + + Writer.WriteLine("Test: Reconnecting"); + Assert.True(conn.IsConnected); + Assert.True(server.IsConnected); + var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, reconnectedStats.BacklogMessagesPending); + + _ = db.PingAsync(); + _ = db.PingAsync(); + var lastPing = db.PingAsync(); + + // For debug, print out the snapshot and server states + PrintSnapshot(conn); + + Assert.NotNull(conn.SelectServer(Message.Create(-1, CommandFlags.None, RedisCommand.PING))); + + // We should see none queued + Assert.Equal(0, stats.BacklogMessagesPending); + await lastPing; } + finally + { + ClearAmbientFailures(); + } + } - [Fact] - public async Task QueuesAndFlushesAfterReconnectingAsync() + [Fact] + public async Task QueuesAndFlushesAfterReconnectingAsync() + { + try { - try + var options = new ConfigurationOptions() { - var options = new ConfigurationOptions() - { - BacklogPolicy = BacklogPolicy.Default, - AbortOnConnectFail = false, - ConnectTimeout = 1000, - ConnectRetry = 2, - SyncTimeout = 10000, - KeepAlive = 10000, - AsyncTimeout = 5000, - AllowAdmin = true, - SocketManager = SocketManager.ThreadPool, - }; - options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); - - using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); - muxer.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); - muxer.InternalError += (s, e) => Log($"Internal Error {e.EndPoint}: {e.Exception.Message}"); - muxer.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); - muxer.ConnectionRestored += (s, a) => Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); - - var db = muxer.GetDatabase(); - Writer.WriteLine("Test: Initial (connected) ping"); - await db.PingAsync(); - - var server = muxer.GetServerSnapshot()[0]; - var stats = server.GetBridgeStatus(ConnectionType.Interactive); - Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal - - // Fail the connection - Writer.WriteLine("Test: Simulating failure"); - muxer.AllowConnect = false; - server.SimulateConnectionFailure(SimulatedFailureType.All); - Assert.False(muxer.IsConnected); - - // Queue up some commands - Writer.WriteLine("Test: Disconnected pings"); - var ignoredA = db.PingAsync(); - var ignoredB = db.PingAsync(); - var lastPing = db.PingAsync(); - - // TODO: Add specific server call - - var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); - Assert.False(muxer.IsConnected); - Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); - - Writer.WriteLine("Test: Allowing reconnect"); - muxer.AllowConnect = true; - Writer.WriteLine("Test: Awaiting reconnect"); - await UntilConditionAsync(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); - - Writer.WriteLine("Test: Checking reconnected 1"); - Assert.True(muxer.IsConnected); - - Writer.WriteLine("Test: ignoredA Status: " + ignoredA.Status); - Writer.WriteLine("Test: ignoredB Status: " + ignoredB.Status); - Writer.WriteLine("Test: lastPing Status: " + lastPing.Status); - var afterConnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); - Writer.WriteLine($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); - - Writer.WriteLine("Test: Awaiting lastPing 1"); - await lastPing; - - Writer.WriteLine("Test: Checking reconnected 2"); - Assert.True(muxer.IsConnected); - var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); - Assert.Equal(0, reconnectedStats.BacklogMessagesPending); - - Writer.WriteLine("Test: Pinging again..."); - _ = db.PingAsync(); - _ = db.PingAsync(); - Writer.WriteLine("Test: Last Ping issued"); - lastPing = db.PingAsync(); - - // We should see none queued - Writer.WriteLine("Test: BacklogMessagesPending check"); - Assert.Equal(0, stats.BacklogMessagesPending); - Writer.WriteLine("Test: Awaiting lastPing 2"); - await lastPing; - Writer.WriteLine("Test: Done"); - } - finally - { - ClearAmbientFailures(); - } + BacklogPolicy = BacklogPolicy.Default, + AbortOnConnectFail = false, + ConnectTimeout = 1000, + ConnectRetry = 2, + SyncTimeout = 10000, + KeepAlive = 10000, + AsyncTimeout = 5000, + AllowAdmin = true, + SocketManager = SocketManager.ThreadPool, + }; + options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); + + using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + conn.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); + conn.InternalError += (s, e) => Log($"Internal Error {e.EndPoint}: {e.Exception.Message}"); + conn.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); + conn.ConnectionRestored += (s, a) => Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); + + var db = conn.GetDatabase(); + Writer.WriteLine("Test: Initial (connected) ping"); + await db.PingAsync(); + + var server = conn.GetServerSnapshot()[0]; + var stats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal + + // Fail the connection + Writer.WriteLine("Test: Simulating failure"); + conn.AllowConnect = false; + server.SimulateConnectionFailure(SimulatedFailureType.All); + Assert.False(conn.IsConnected); + + // Queue up some commands + Writer.WriteLine("Test: Disconnected pings"); + var ignoredA = db.PingAsync(); + var ignoredB = db.PingAsync(); + var lastPing = db.PingAsync(); + + // TODO: Add specific server call + + var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.False(conn.IsConnected); + Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); + + Writer.WriteLine("Test: Allowing reconnect"); + conn.AllowConnect = true; + Writer.WriteLine("Test: Awaiting reconnect"); + await UntilConditionAsync(TimeSpan.FromSeconds(3), () => conn.IsConnected).ForAwait(); + + Writer.WriteLine("Test: Checking reconnected 1"); + Assert.True(conn.IsConnected); + + Writer.WriteLine("Test: ignoredA Status: " + ignoredA.Status); + Writer.WriteLine("Test: ignoredB Status: " + ignoredB.Status); + Writer.WriteLine("Test: lastPing Status: " + lastPing.Status); + var afterConnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Writer.WriteLine($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); + + Writer.WriteLine("Test: Awaiting lastPing 1"); + await lastPing; + + Writer.WriteLine("Test: Checking reconnected 2"); + Assert.True(conn.IsConnected); + var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, reconnectedStats.BacklogMessagesPending); + + Writer.WriteLine("Test: Pinging again..."); + _ = db.PingAsync(); + _ = db.PingAsync(); + Writer.WriteLine("Test: Last Ping issued"); + lastPing = db.PingAsync(); + + // We should see none queued + Writer.WriteLine("Test: BacklogMessagesPending check"); + Assert.Equal(0, stats.BacklogMessagesPending); + Writer.WriteLine("Test: Awaiting lastPing 2"); + await lastPing; + Writer.WriteLine("Test: Done"); } + finally + { + ClearAmbientFailures(); + } + } - [Fact] - public async Task QueuesAndFlushesAfterReconnecting() + [Fact] + public async Task QueuesAndFlushesAfterReconnecting() + { + try { - try + var options = new ConfigurationOptions() { - var options = new ConfigurationOptions() - { - BacklogPolicy = BacklogPolicy.Default, - AbortOnConnectFail = false, - ConnectTimeout = 1000, - ConnectRetry = 2, - SyncTimeout = 10000, - KeepAlive = 10000, - AsyncTimeout = 5000, - AllowAdmin = true, - SocketManager = SocketManager.ThreadPool, - }; - options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); - - using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); - muxer.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); - muxer.InternalError += (s, e) => Log($"Internal Error {e.EndPoint}: {e.Exception.Message}"); - muxer.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); - muxer.ConnectionRestored += (s, a) => Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); - - var db = muxer.GetDatabase(); - Writer.WriteLine("Test: Initial (connected) ping"); - await db.PingAsync(); - - var server = muxer.GetServerSnapshot()[0]; - var stats = server.GetBridgeStatus(ConnectionType.Interactive); - Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal - - // Fail the connection - Writer.WriteLine("Test: Simulating failure"); - muxer.AllowConnect = false; - server.SimulateConnectionFailure(SimulatedFailureType.All); - Assert.False(muxer.IsConnected); - - // Queue up some commands - Writer.WriteLine("Test: Disconnected pings"); - - Task[] pings = new Task[3]; - pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(1)); - pings[1] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(2)); - pings[2] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(3)); - void disconnectedPings(int id) - { - // No need to delay, we're going to try a disconnected connection immediately so it'll fail... - Log($"Pinging (disconnected - {id})"); - var result = db.Ping(); - Log($"Pinging (disconnected - {id}) - result: " + result); - } - Writer.WriteLine("Test: Disconnected pings issued"); - - Assert.False(muxer.IsConnected); - // Give the tasks time to queue - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => server.GetBridgeStatus(ConnectionType.Interactive).BacklogMessagesPending >= 3); - - var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); - Log($"Test Stats: (BacklogMessagesPending: {disconnectedStats.BacklogMessagesPending}, TotalBacklogMessagesQueued: {disconnectedStats.TotalBacklogMessagesQueued})"); - Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); - - Writer.WriteLine("Test: Allowing reconnect"); - muxer.AllowConnect = true; - Writer.WriteLine("Test: Awaiting reconnect"); - await UntilConditionAsync(TimeSpan.FromSeconds(3), () => muxer.IsConnected).ForAwait(); - - Writer.WriteLine("Test: Checking reconnected 1"); - Assert.True(muxer.IsConnected); - - var afterConnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); - Writer.WriteLine($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); - - Writer.WriteLine("Test: Awaiting 3 pings"); - await Task.WhenAll(pings); - - Writer.WriteLine("Test: Checking reconnected 2"); - Assert.True(muxer.IsConnected); - var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); - Assert.Equal(0, reconnectedStats.BacklogMessagesPending); - - Writer.WriteLine("Test: Pinging again..."); - pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(4)); - pings[1] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(5)); - pings[2] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(6)); - Writer.WriteLine("Test: Last Ping queued"); - - // We should see none queued - Writer.WriteLine("Test: BacklogMessagesPending check"); - Assert.Equal(0, stats.BacklogMessagesPending); - Writer.WriteLine("Test: Awaiting 3 more pings"); - await Task.WhenAll(pings); - Writer.WriteLine("Test: Done"); - } - finally + BacklogPolicy = BacklogPolicy.Default, + AbortOnConnectFail = false, + ConnectTimeout = 1000, + ConnectRetry = 2, + SyncTimeout = 10000, + KeepAlive = 10000, + AsyncTimeout = 5000, + AllowAdmin = true, + SocketManager = SocketManager.ThreadPool, + }; + options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); + + using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + conn.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); + conn.InternalError += (s, e) => Log($"Internal Error {e.EndPoint}: {e.Exception.Message}"); + conn.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); + conn.ConnectionRestored += (s, a) => Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); + + var db = conn.GetDatabase(); + Writer.WriteLine("Test: Initial (connected) ping"); + await db.PingAsync(); + + var server = conn.GetServerSnapshot()[0]; + var stats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal + + // Fail the connection + Writer.WriteLine("Test: Simulating failure"); + conn.AllowConnect = false; + server.SimulateConnectionFailure(SimulatedFailureType.All); + Assert.False(conn.IsConnected); + + // Queue up some commands + Writer.WriteLine("Test: Disconnected pings"); + + Task[] pings = new Task[3]; + pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(1)); + pings[1] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(2)); + pings[2] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(3)); + void disconnectedPings(int id) { - ClearAmbientFailures(); + // No need to delay, we're going to try a disconnected connection immediately so it'll fail... + Log($"Pinging (disconnected - {id})"); + var result = db.Ping(); + Log($"Pinging (disconnected - {id}) - result: " + result); } + Writer.WriteLine("Test: Disconnected pings issued"); + + Assert.False(conn.IsConnected); + // Give the tasks time to queue + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => server.GetBridgeStatus(ConnectionType.Interactive).BacklogMessagesPending >= 3); + + var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Log($"Test Stats: (BacklogMessagesPending: {disconnectedStats.BacklogMessagesPending}, TotalBacklogMessagesQueued: {disconnectedStats.TotalBacklogMessagesQueued})"); + Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); + + Writer.WriteLine("Test: Allowing reconnect"); + conn.AllowConnect = true; + Writer.WriteLine("Test: Awaiting reconnect"); + await UntilConditionAsync(TimeSpan.FromSeconds(3), () => conn.IsConnected).ForAwait(); + + Writer.WriteLine("Test: Checking reconnected 1"); + Assert.True(conn.IsConnected); + + var afterConnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Writer.WriteLine($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); + + Writer.WriteLine("Test: Awaiting 3 pings"); + await Task.WhenAll(pings); + + Writer.WriteLine("Test: Checking reconnected 2"); + Assert.True(conn.IsConnected); + var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, reconnectedStats.BacklogMessagesPending); + + Writer.WriteLine("Test: Pinging again..."); + pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(4)); + pings[1] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(5)); + pings[2] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(6)); + Writer.WriteLine("Test: Last Ping queued"); + + // We should see none queued + Writer.WriteLine("Test: BacklogMessagesPending check"); + Assert.Equal(0, stats.BacklogMessagesPending); + Writer.WriteLine("Test: Awaiting 3 more pings"); + await Task.WhenAll(pings); + Writer.WriteLine("Test: Done"); + } + finally + { + ClearAmbientFailures(); } + } - [Fact] - public async Task QueuesAndFlushesAfterReconnectingClusterAsync() + [Fact] + public async Task QueuesAndFlushesAfterReconnectingClusterAsync() + { + try { - try - { - var options = ConfigurationOptions.Parse(TestConfig.Current.ClusterServersAndPorts); - options.BacklogPolicy = BacklogPolicy.Default; - options.AbortOnConnectFail = false; - options.ConnectTimeout = 1000; - options.ConnectRetry = 2; - options.SyncTimeout = 10000; - options.KeepAlive = 10000; - options.AsyncTimeout = 5000; - options.AllowAdmin = true; - options.SocketManager = SocketManager.ThreadPool; - - using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); - muxer.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); - muxer.InternalError += (s, e) => Log($"Internal Error {e.EndPoint}: {e.Exception.Message}"); - muxer.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); - muxer.ConnectionRestored += (s, a) => Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); - - var db = muxer.GetDatabase(); - Writer.WriteLine("Test: Initial (connected) ping"); - await db.PingAsync(); - - RedisKey meKey = Me(); - var getMsg = Message.Create(0, CommandFlags.None, RedisCommand.GET, meKey); - - ServerEndPoint? server = null; // Get the server specifically for this message's hash slot - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => (server = muxer.SelectServer(getMsg)) != null); - - Assert.NotNull(server); - var stats = server.GetBridgeStatus(ConnectionType.Interactive); - Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal - - static Task PingAsync(ServerEndPoint server, CommandFlags flags = CommandFlags.None) - { - var message = ResultProcessor.TimingProcessor.CreateMessage(-1, flags, RedisCommand.PING); - - server.Multiplexer.CheckMessage(message); - return server.Multiplexer.ExecuteAsyncImpl(message, ResultProcessor.ResponseTimer, null, server); - } - - // Fail the connection - Writer.WriteLine("Test: Simulating failure"); - muxer.AllowConnect = false; - server.SimulateConnectionFailure(SimulatedFailureType.All); - Assert.False(server.IsConnected); // Server isn't connected - Assert.True(muxer.IsConnected); // ...but the multiplexer is - - // Queue up some commands - Writer.WriteLine("Test: Disconnected pings"); - var ignoredA = PingAsync(server); - var ignoredB = PingAsync(server); - var lastPing = PingAsync(server); - - var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); - Assert.False(server.IsConnected); - Assert.True(muxer.IsConnected); - Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); - - Writer.WriteLine("Test: Allowing reconnect"); - muxer.AllowConnect = true; - Writer.WriteLine("Test: Awaiting reconnect"); - await UntilConditionAsync(TimeSpan.FromSeconds(3), () => server.IsConnected).ForAwait(); - - Writer.WriteLine("Test: Checking reconnected 1"); - Assert.True(server.IsConnected); - Assert.True(muxer.IsConnected); - - Writer.WriteLine("Test: ignoredA Status: " + ignoredA.Status); - Writer.WriteLine("Test: ignoredB Status: " + ignoredB.Status); - Writer.WriteLine("Test: lastPing Status: " + lastPing.Status); - var afterConnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); - Writer.WriteLine($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); - - Writer.WriteLine("Test: Awaiting lastPing 1"); - await lastPing; - - Writer.WriteLine("Test: Checking reconnected 2"); - Assert.True(server.IsConnected); - Assert.True(muxer.IsConnected); - var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); - Assert.Equal(0, reconnectedStats.BacklogMessagesPending); - - Writer.WriteLine("Test: Pinging again..."); - _ = PingAsync(server); - _ = PingAsync(server); - Writer.WriteLine("Test: Last Ping issued"); - lastPing = PingAsync(server); - - // We should see none queued - Writer.WriteLine("Test: BacklogMessagesPending check"); - Assert.Equal(0, stats.BacklogMessagesPending); - Writer.WriteLine("Test: Awaiting lastPing 2"); - await lastPing; - Writer.WriteLine("Test: Done"); - } - finally + var options = ConfigurationOptions.Parse(TestConfig.Current.ClusterServersAndPorts); + options.BacklogPolicy = BacklogPolicy.Default; + options.AbortOnConnectFail = false; + options.ConnectTimeout = 1000; + options.ConnectRetry = 2; + options.SyncTimeout = 10000; + options.KeepAlive = 10000; + options.AsyncTimeout = 5000; + options.AllowAdmin = true; + options.SocketManager = SocketManager.ThreadPool; + + using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + conn.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); + conn.InternalError += (s, e) => Log($"Internal Error {e.EndPoint}: {e.Exception.Message}"); + conn.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); + conn.ConnectionRestored += (s, a) => Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); + + var db = conn.GetDatabase(); + Writer.WriteLine("Test: Initial (connected) ping"); + await db.PingAsync(); + + RedisKey meKey = Me(); + var getMsg = Message.Create(0, CommandFlags.None, RedisCommand.GET, meKey); + + ServerEndPoint? server = null; // Get the server specifically for this message's hash slot + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => (server = conn.SelectServer(getMsg)) != null); + + Assert.NotNull(server); + var stats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal + + static Task PingAsync(ServerEndPoint server, CommandFlags flags = CommandFlags.None) { - ClearAmbientFailures(); + var message = ResultProcessor.TimingProcessor.CreateMessage(-1, flags, RedisCommand.PING); + + server.Multiplexer.CheckMessage(message); + return server.Multiplexer.ExecuteAsyncImpl(message, ResultProcessor.ResponseTimer, null, server); } + + // Fail the connection + Writer.WriteLine("Test: Simulating failure"); + conn.AllowConnect = false; + server.SimulateConnectionFailure(SimulatedFailureType.All); + Assert.False(server.IsConnected); // Server isn't connected + Assert.True(conn.IsConnected); // ...but the multiplexer is + + // Queue up some commands + Writer.WriteLine("Test: Disconnected pings"); + var ignoredA = PingAsync(server); + var ignoredB = PingAsync(server); + var lastPing = PingAsync(server); + + var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.False(server.IsConnected); + Assert.True(conn.IsConnected); + Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); + + Writer.WriteLine("Test: Allowing reconnect"); + conn.AllowConnect = true; + Writer.WriteLine("Test: Awaiting reconnect"); + await UntilConditionAsync(TimeSpan.FromSeconds(3), () => server.IsConnected).ForAwait(); + + Writer.WriteLine("Test: Checking reconnected 1"); + Assert.True(server.IsConnected); + Assert.True(conn.IsConnected); + + Writer.WriteLine("Test: ignoredA Status: " + ignoredA.Status); + Writer.WriteLine("Test: ignoredB Status: " + ignoredB.Status); + Writer.WriteLine("Test: lastPing Status: " + lastPing.Status); + var afterConnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Writer.WriteLine($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); + + Writer.WriteLine("Test: Awaiting lastPing 1"); + await lastPing; + + Writer.WriteLine("Test: Checking reconnected 2"); + Assert.True(server.IsConnected); + Assert.True(conn.IsConnected); + var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); + Assert.Equal(0, reconnectedStats.BacklogMessagesPending); + + Writer.WriteLine("Test: Pinging again..."); + _ = PingAsync(server); + _ = PingAsync(server); + Writer.WriteLine("Test: Last Ping issued"); + lastPing = PingAsync(server); + + // We should see none queued + Writer.WriteLine("Test: BacklogMessagesPending check"); + Assert.Equal(0, stats.BacklogMessagesPending); + Writer.WriteLine("Test: Awaiting lastPing 2"); + await lastPing; + Writer.WriteLine("Test: Done"); + } + finally + { + ClearAmbientFailures(); } } } diff --git a/tests/StackExchange.Redis.Tests/BasicOps.cs b/tests/StackExchange.Redis.Tests/BasicOps.cs index fb7012162..63ae438f1 100644 --- a/tests/StackExchange.Redis.Tests/BasicOps.cs +++ b/tests/StackExchange.Redis.Tests/BasicOps.cs @@ -5,523 +5,468 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class BasicOpsTests : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class BasicOpsTests : TestBase + public BasicOpsTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + + [Fact] + public async Task PingOnce() { - public BasicOpsTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + using var conn = Create(); + var db = conn.GetDatabase(); - [Fact] - public async Task PingOnce() - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); + var duration = await db.PingAsync().ForAwait(); + Log("Ping took: " + duration); + Assert.True(duration.TotalMilliseconds > 0); + } - var duration = await conn.PingAsync().ForAwait(); - Log("Ping took: " + duration); - Assert.True(duration.TotalMilliseconds > 0); - } - } + [Fact] + public async Task RapidDispose() + { + using var primary = Create(); + var db = primary.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - [Fact] - public async Task RapidDispose() + for (int i = 0; i < 10; i++) { - RedisKey key = Me(); - using (var primary = Create()) - { - var conn = primary.GetDatabase(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - - for (int i = 0; i < 10; i++) - { - using (var secondary = Create(fail: true, shared: false)) - { - secondary.GetDatabase().StringIncrement(key, flags: CommandFlags.FireAndForget); - } - } - // Give it a moment to get through the pipe...they were fire and forget - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => 10 == (int)conn.StringGet(key)); - Assert.Equal(10, (int)conn.StringGet(key)); - } + using var secondary = Create(fail: true, shared: false); + secondary.GetDatabase().StringIncrement(key, flags: CommandFlags.FireAndForget); } + // Give it a moment to get through the pipe...they were fire and forget + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => 10 == (int)db.StringGet(key)); + Assert.Equal(10, (int)db.StringGet(key)); + } - [Fact] - public async Task PingMany() + [Fact] + public async Task PingMany() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var tasks = new Task[100]; + for (int i = 0; i < tasks.Length; i++) { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var tasks = new Task[100]; - for (int i = 0; i < tasks.Length; i++) - { - tasks[i] = conn.PingAsync(); - } - await Task.WhenAll(tasks).ForAwait(); - Assert.True(tasks[0].Result.TotalMilliseconds > 0); - Assert.True(tasks[tasks.Length - 1].Result.TotalMilliseconds > 0); - } + tasks[i] = db.PingAsync(); } + await Task.WhenAll(tasks).ForAwait(); + Assert.True(tasks[0].Result.TotalMilliseconds > 0); + Assert.True(tasks[tasks.Length - 1].Result.TotalMilliseconds > 0); + } - [Fact] - public void GetWithNullKey() - { - using (var muxer = Create()) - { - var db = muxer.GetDatabase(); - const string? key = null; - var ex = Assert.Throws(() => db.StringGet(key)); - Assert.Equal("A null key is not valid in this context", ex.Message); - } - } + [Fact] + public void GetWithNullKey() + { + using var conn = Create(); + var db = conn.GetDatabase(); + const string? key = null; + var ex = Assert.Throws(() => db.StringGet(key)); + Assert.Equal("A null key is not valid in this context", ex.Message); + } - [Fact] - public void SetWithNullKey() - { - using (var muxer = Create()) - { - var db = muxer.GetDatabase(); - const string? key = null, value = "abc"; - var ex = Assert.Throws(() => db.StringSet(key!, value)); - Assert.Equal("A null key is not valid in this context", ex.Message); - } - } + [Fact] + public void SetWithNullKey() + { + using var conn = Create(); + var db = conn.GetDatabase(); + const string? key = null, value = "abc"; + var ex = Assert.Throws(() => db.StringSet(key!, value)); + Assert.Equal("A null key is not valid in this context", ex.Message); + } - [Fact] - public void SetWithNullValue() - { - using (var muxer = Create()) - { - var db = muxer.GetDatabase(); - string key = Me(); - const string? value = null; - db.KeyDelete(key, CommandFlags.FireAndForget); - - db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); - Assert.True(db.KeyExists(key)); - db.StringSet(key, value, flags: CommandFlags.FireAndForget); - - var actual = (string?)db.StringGet(key); - Assert.Null(actual); - Assert.False(db.KeyExists(key)); - } - } + [Fact] + public void SetWithNullValue() + { + using var conn = Create(); + var db = conn.GetDatabase(); + string key = Me(); + const string? value = null; + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); + Assert.True(db.KeyExists(key)); + db.StringSet(key, value, flags: CommandFlags.FireAndForget); + + var actual = (string?)db.StringGet(key); + Assert.Null(actual); + Assert.False(db.KeyExists(key)); + } - [Fact] - public void SetWithDefaultValue() - { - using (var muxer = Create()) - { - var db = muxer.GetDatabase(); - string key = Me(); - var value = default(RedisValue); // this is kinda 0... ish - db.KeyDelete(key, CommandFlags.FireAndForget); - - db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); - Assert.True(db.KeyExists(key)); - db.StringSet(key, value, flags: CommandFlags.FireAndForget); - - var actual = (string?)db.StringGet(key); - Assert.Null(actual); - Assert.False(db.KeyExists(key)); - } - } + [Fact] + public void SetWithDefaultValue() + { + using var conn = Create(); + var db = conn.GetDatabase(); + string key = Me(); + var value = default(RedisValue); // this is kinda 0... ish + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); + Assert.True(db.KeyExists(key)); + db.StringSet(key, value, flags: CommandFlags.FireAndForget); + + var actual = (string?)db.StringGet(key); + Assert.Null(actual); + Assert.False(db.KeyExists(key)); + } + + [Fact] + public void SetWithZeroValue() + { + using var conn = Create(); + var db = conn.GetDatabase(); + string key = Me(); + const long value = 0; + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); + Assert.True(db.KeyExists(key)); + db.StringSet(key, value, flags: CommandFlags.FireAndForget); + + var actual = (string?)db.StringGet(key); + Assert.Equal("0", actual); + Assert.True(db.KeyExists(key)); + } - [Fact] - public void SetWithZeroValue() + [Fact] + public async Task GetSetAsync() + { + using var conn = Create(); + var db = conn.GetDatabase(); + + RedisKey key = Me(); + var d0 = db.KeyDeleteAsync(key); + var d1 = db.KeyDeleteAsync(key); + var g1 = db.StringGetAsync(key); + var s1 = db.StringSetAsync(key, "123"); + var g2 = db.StringGetAsync(key); + var d2 = db.KeyDeleteAsync(key); + + await d0; + Assert.False(await d1); + Assert.Null((string?)(await g1)); + Assert.True((await g1).IsNull); + await s1; + Assert.Equal("123", await g2); + Assert.Equal(123, (int)(await g2)); + Assert.False((await g2).IsNull); + Assert.True(await d2); + } + + [Fact] + public void GetSetSync() + { + using var conn = Create(); + var db = conn.GetDatabase(); + + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + var d1 = db.KeyDelete(key); + var g1 = db.StringGet(key); + db.StringSet(key, "123", flags: CommandFlags.FireAndForget); + var g2 = db.StringGet(key); + var d2 = db.KeyDelete(key); + + Assert.False(d1); + Assert.Null((string?)g1); + Assert.True(g1.IsNull); + + Assert.Equal("123", g2); + Assert.Equal(123, (int)g2); + Assert.False(g2.IsNull); + Assert.True(d2); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(true, false)] + public async Task GetWithExpiry(bool exists, bool hasExpiry) + { + using var conn = Create(); + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + if (exists) { - using (var muxer = Create()) - { - var db = muxer.GetDatabase(); - string key = Me(); - const long value = 0; - db.KeyDelete(key, CommandFlags.FireAndForget); - - db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); - Assert.True(db.KeyExists(key)); - db.StringSet(key, value, flags: CommandFlags.FireAndForget); - - var actual = (string?)db.StringGet(key); - Assert.Equal("0", actual); - Assert.True(db.KeyExists(key)); - } + if (hasExpiry) + db.StringSet(key, "val", TimeSpan.FromMinutes(5), flags: CommandFlags.FireAndForget); + else + db.StringSet(key, "val", flags: CommandFlags.FireAndForget); } + var async = db.StringGetWithExpiryAsync(key); + var syncResult = db.StringGetWithExpiry(key); + var asyncResult = await async; - [Fact] - public async Task GetSetAsync() + if (exists) { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - - RedisKey key = Me(); - var d0 = conn.KeyDeleteAsync(key); - var d1 = conn.KeyDeleteAsync(key); - var g1 = conn.StringGetAsync(key); - var s1 = conn.StringSetAsync(key, "123"); - var g2 = conn.StringGetAsync(key); - var d2 = conn.KeyDeleteAsync(key); - - await d0; - Assert.False(await d1); - Assert.Null((string?)(await g1)); - Assert.True((await g1).IsNull); - await s1; - Assert.Equal("123", await g2); - Assert.Equal(123, (int)(await g2)); - Assert.False((await g2).IsNull); - Assert.True(await d2); - } + Assert.Equal("val", asyncResult.Value); + Assert.Equal(hasExpiry, asyncResult.Expiry.HasValue); + if (hasExpiry) Assert.True(asyncResult.Expiry!.Value.TotalMinutes >= 4.9 && asyncResult.Expiry.Value.TotalMinutes <= 5); + Assert.Equal("val", syncResult.Value); + Assert.Equal(hasExpiry, syncResult.Expiry.HasValue); + if (hasExpiry) Assert.True(syncResult.Expiry!.Value.TotalMinutes >= 4.9 && syncResult.Expiry.Value.TotalMinutes <= 5); } - - [Fact] - public void GetSetSync() + else { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - - RedisKey key = Me(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - var d1 = conn.KeyDelete(key); - var g1 = conn.StringGet(key); - conn.StringSet(key, "123", flags: CommandFlags.FireAndForget); - var g2 = conn.StringGet(key); - var d2 = conn.KeyDelete(key); - - Assert.False(d1); - Assert.Null((string?)g1); - Assert.True(g1.IsNull); - - Assert.Equal("123", g2); - Assert.Equal(123, (int)g2); - Assert.False(g2.IsNull); - Assert.True(d2); - } + Assert.True(asyncResult.Value.IsNull); + Assert.False(asyncResult.Expiry.HasValue); + Assert.True(syncResult.Value.IsNull); + Assert.False(syncResult.Expiry.HasValue); } + } - [Theory] - [InlineData(false, false)] - [InlineData(true, true)] - [InlineData(true, false)] - public async Task GetWithExpiry(bool exists, bool hasExpiry) + [Fact] + public async Task GetWithExpiryWrongTypeAsync() + { + using var conn = Create(); + var db = conn.GetDatabase(); + RedisKey key = Me(); + _ = db.KeyDeleteAsync(key); + _ = db.SetAddAsync(key, "abc"); + var ex = await Assert.ThrowsAsync(async () => { - using (var conn = Create()) + try { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - if (exists) - { - if (hasExpiry) - db.StringSet(key, "val", TimeSpan.FromMinutes(5), flags: CommandFlags.FireAndForget); - else - db.StringSet(key, "val", flags: CommandFlags.FireAndForget); - } - var async = db.StringGetWithExpiryAsync(key); - var syncResult = db.StringGetWithExpiry(key); - var asyncResult = await async; - - if (exists) - { - Assert.Equal("val", asyncResult.Value); - Assert.Equal(hasExpiry, asyncResult.Expiry.HasValue); - if (hasExpiry) Assert.True(asyncResult.Expiry!.Value.TotalMinutes >= 4.9 && asyncResult.Expiry.Value.TotalMinutes <= 5); - Assert.Equal("val", syncResult.Value); - Assert.Equal(hasExpiry, syncResult.Expiry.HasValue); - if (hasExpiry) Assert.True(syncResult.Expiry!.Value.TotalMinutes >= 4.9 && syncResult.Expiry.Value.TotalMinutes <= 5); - } - else - { - Assert.True(asyncResult.Value.IsNull); - Assert.False(asyncResult.Expiry.HasValue); - Assert.True(syncResult.Value.IsNull); - Assert.False(syncResult.Expiry.HasValue); - } + Log("Key: " + (string?)key); + await db.StringGetWithExpiryAsync(key).ForAwait(); } - } - - [Fact] - public async Task GetWithExpiryWrongTypeAsync() - { - using (var conn = Create()) + catch (AggregateException e) { - var db = conn.GetDatabase(); - RedisKey key = Me(); - _ = db.KeyDeleteAsync(key); - _ = db.SetAddAsync(key, "abc"); - var ex = await Assert.ThrowsAsync(async () => - { - try - { - Log("Key: " + (string?)key); - await db.StringGetWithExpiryAsync(key).ForAwait(); - } - catch (AggregateException e) - { - throw e.InnerExceptions[0]; - } - }).ForAwait(); - Assert.Equal("WRONGTYPE Operation against a key holding the wrong kind of value", ex.Message); + throw e.InnerExceptions[0]; } - } + }).ForAwait(); + Assert.Equal("WRONGTYPE Operation against a key holding the wrong kind of value", ex.Message); + } - [Fact] - public void GetWithExpiryWrongTypeSync() + [Fact] + public void GetWithExpiryWrongTypeSync() + { + RedisKey key = Me(); + var ex = Assert.Throws(() => { - RedisKey key = Me(); - var ex = Assert.Throws(() => - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.SetAdd(key, "abc", CommandFlags.FireAndForget); - db.StringGetWithExpiry(key); - } - }); - Assert.Equal("WRONGTYPE Operation against a key holding the wrong kind of value", ex.Message); - } + using var conn = Create(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.SetAdd(key, "abc", CommandFlags.FireAndForget); + db.StringGetWithExpiry(key); + }); + Assert.Equal("WRONGTYPE Operation against a key holding the wrong kind of value", ex.Message); + } #if DEBUG - [Fact] - public async Task TestSevered() - { - SetExpectedAmbientFailureCount(2); - using (var muxer = Create(allowAdmin: true, shared: false)) - { - var db = muxer.GetDatabase(); - string key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, key, flags: CommandFlags.FireAndForget); - var server = GetServer(muxer); - server.SimulateConnectionFailure(SimulatedFailureType.All); - var watch = Stopwatch.StartNew(); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => server.IsConnected); - watch.Stop(); - Log("Time to re-establish: {0}ms (any order)", watch.ElapsedMilliseconds); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => key == db.StringGet(key)); - Debug.WriteLine("Pinging..."); - Assert.Equal(key, db.StringGet(key)); - } - } + [Fact] + public async Task TestSevered() + { + SetExpectedAmbientFailureCount(2); + using var conn = Create(allowAdmin: true, shared: false); + var db = conn.GetDatabase(); + string key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, key, flags: CommandFlags.FireAndForget); + var server = GetServer(conn); + server.SimulateConnectionFailure(SimulatedFailureType.All); + var watch = Stopwatch.StartNew(); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => server.IsConnected); + watch.Stop(); + Log("Time to re-establish: {0}ms (any order)", watch.ElapsedMilliseconds); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => key == db.StringGet(key)); + Debug.WriteLine("Pinging..."); + Assert.Equal(key, db.StringGet(key)); + } #endif - [Fact] - public async Task IncrAsync() - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - RedisKey key = Me(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - var nix = conn.KeyExistsAsync(key).ForAwait(); - var a = conn.StringGetAsync(key).ForAwait(); - var b = conn.StringIncrementAsync(key).ForAwait(); - var c = conn.StringGetAsync(key).ForAwait(); - var d = conn.StringIncrementAsync(key, 10).ForAwait(); - var e = conn.StringGetAsync(key).ForAwait(); - var f = conn.StringDecrementAsync(key, 11).ForAwait(); - var g = conn.StringGetAsync(key).ForAwait(); - var h = conn.KeyExistsAsync(key).ForAwait(); - Assert.False(await nix); - Assert.True((await a).IsNull); - Assert.Equal(0, (long)(await a)); - Assert.Equal(1, await b); - Assert.Equal(1, (long)(await c)); - Assert.Equal(11, await d); - Assert.Equal(11, (long)(await e)); - Assert.Equal(0, await f); - Assert.Equal(0, (long)(await g)); - Assert.True(await h); - } - } + [Fact] + public async Task IncrAsync() + { + using var conn = Create(); + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + var nix = db.KeyExistsAsync(key).ForAwait(); + var a = db.StringGetAsync(key).ForAwait(); + var b = db.StringIncrementAsync(key).ForAwait(); + var c = db.StringGetAsync(key).ForAwait(); + var d = db.StringIncrementAsync(key, 10).ForAwait(); + var e = db.StringGetAsync(key).ForAwait(); + var f = db.StringDecrementAsync(key, 11).ForAwait(); + var g = db.StringGetAsync(key).ForAwait(); + var h = db.KeyExistsAsync(key).ForAwait(); + Assert.False(await nix); + Assert.True((await a).IsNull); + Assert.Equal(0, (long)(await a)); + Assert.Equal(1, await b); + Assert.Equal(1, (long)(await c)); + Assert.Equal(11, await d); + Assert.Equal(11, (long)(await e)); + Assert.Equal(0, await f); + Assert.Equal(0, (long)(await g)); + Assert.True(await h); + } - [Fact] - public void IncrSync() - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - RedisKey key = Me(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - var nix = conn.KeyExists(key); - var a = conn.StringGet(key); - var b = conn.StringIncrement(key); - var c = conn.StringGet(key); - var d = conn.StringIncrement(key, 10); - var e = conn.StringGet(key); - var f = conn.StringDecrement(key, 11); - var g = conn.StringGet(key); - var h = conn.KeyExists(key); - Assert.False(nix); - Assert.True(a.IsNull); - Assert.Equal(0, (long)a); - Assert.Equal(1, b); - Assert.Equal(1, (long)c); - Assert.Equal(11, d); - Assert.Equal(11, (long)e); - Assert.Equal(0, f); - Assert.Equal(0, (long)g); - Assert.True(h); - } - } + [Fact] + public void IncrSync() + { + using var conn = Create(); + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + var nix = db.KeyExists(key); + var a = db.StringGet(key); + var b = db.StringIncrement(key); + var c = db.StringGet(key); + var d = db.StringIncrement(key, 10); + var e = db.StringGet(key); + var f = db.StringDecrement(key, 11); + var g = db.StringGet(key); + var h = db.KeyExists(key); + Assert.False(nix); + Assert.True(a.IsNull); + Assert.Equal(0, (long)a); + Assert.Equal(1, b); + Assert.Equal(1, (long)c); + Assert.Equal(11, d); + Assert.Equal(11, (long)e); + Assert.Equal(0, f); + Assert.Equal(0, (long)g); + Assert.True(h); + } - [Fact] - public void IncrDifferentSizes() - { - using (var muxer = Create()) - { - var db = muxer.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - int expected = 0; - Incr(db, key, -129019, ref expected); - Incr(db, key, -10023, ref expected); - Incr(db, key, -9933, ref expected); - Incr(db, key, -23, ref expected); - Incr(db, key, -7, ref expected); - Incr(db, key, -1, ref expected); - Incr(db, key, 0, ref expected); - Incr(db, key, 1, ref expected); - Incr(db, key, 9, ref expected); - Incr(db, key, 11, ref expected); - Incr(db, key, 345, ref expected); - Incr(db, key, 4982, ref expected); - Incr(db, key, 13091, ref expected); - Incr(db, key, 324092, ref expected); - Assert.NotEqual(0, expected); - var sum = (long)db.StringGet(key); - Assert.Equal(expected, sum); - } - } + [Fact] + public void IncrDifferentSizes() + { + using var conn = Create(); + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + int expected = 0; + Incr(db, key, -129019, ref expected); + Incr(db, key, -10023, ref expected); + Incr(db, key, -9933, ref expected); + Incr(db, key, -23, ref expected); + Incr(db, key, -7, ref expected); + Incr(db, key, -1, ref expected); + Incr(db, key, 0, ref expected); + Incr(db, key, 1, ref expected); + Incr(db, key, 9, ref expected); + Incr(db, key, 11, ref expected); + Incr(db, key, 345, ref expected); + Incr(db, key, 4982, ref expected); + Incr(db, key, 13091, ref expected); + Incr(db, key, 324092, ref expected); + Assert.NotEqual(0, expected); + var sum = (long)db.StringGet(key); + Assert.Equal(expected, sum); + } - private static void Incr(IDatabase database, RedisKey key, int delta, ref int total) - { - database.StringIncrement(key, delta, CommandFlags.FireAndForget); - total += delta; - } + private static void Incr(IDatabase database, RedisKey key, int delta, ref int total) + { + database.StringIncrement(key, delta, CommandFlags.FireAndForget); + total += delta; + } - [Fact] - public void ShouldUseSharedMuxer() + [Fact] + public void ShouldUseSharedMuxer() + { + Writer.WriteLine($"Shared: {SharedFixtureAvailable}"); + if (SharedFixtureAvailable) { - Writer.WriteLine($"Shared: {SharedFixtureAvailable}"); - if (SharedFixtureAvailable) - { - using (var a = Create()) - { - Assert.IsNotType(a); - using (var b = Create()) - { - Assert.Same(a, b); - } - } - } - else - { - using (var a = Create()) - { - Assert.IsType(a); - using (var b = Create()) - { - Assert.NotSame(a, b); - } - } - } + using var a = Create(); + Assert.IsNotType(a); + using var b = Create(); + Assert.Same(a, b); } - - [Fact] - public async Task Delete() + else { - using (var muxer = Create()) - { - var db = muxer.GetDatabase(); - var key = Me(); - _ = db.StringSetAsync(key, "Heyyyyy"); - var ke1 = db.KeyExistsAsync(key).ForAwait(); - var ku1 = db.KeyDelete(key); - var ke2 = db.KeyExistsAsync(key).ForAwait(); - Assert.True(await ke1); - Assert.True(ku1); - Assert.False(await ke2); - } + using var a = Create(); + Assert.IsType(a); + using var b = Create(); + Assert.NotSame(a, b); } + } - [Fact] - public async Task DeleteAsync() - { - using (var muxer = Create()) - { - var db = muxer.GetDatabase(); - var key = Me(); - _ = db.StringSetAsync(key, "Heyyyyy"); - var ke1 = db.KeyExistsAsync(key).ForAwait(); - var ku1 = db.KeyDeleteAsync(key).ForAwait(); - var ke2 = db.KeyExistsAsync(key).ForAwait(); - Assert.True(await ke1); - Assert.True(await ku1); - Assert.False(await ke2); - } - } + [Fact] + public async Task Delete() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + _ = db.StringSetAsync(key, "Heyyyyy"); + var ke1 = db.KeyExistsAsync(key).ForAwait(); + var ku1 = db.KeyDelete(key); + var ke2 = db.KeyExistsAsync(key).ForAwait(); + Assert.True(await ke1); + Assert.True(ku1); + Assert.False(await ke2); + } - [Fact] - public async Task DeleteMany() - { - using (var muxer = Create()) - { - var db = muxer.GetDatabase(); - var key1 = Me(); - var key2 = Me() + "2"; - var key3 = Me() + "3"; - _ = db.StringSetAsync(key1, "Heyyyyy"); - _ = db.StringSetAsync(key2, "Heyyyyy"); - // key 3 not set - var ku1 = db.KeyDelete(new RedisKey[] { key1, key2, key3 }); - var ke1 = db.KeyExistsAsync(key1).ForAwait(); - var ke2 = db.KeyExistsAsync(key2).ForAwait(); - Assert.Equal(2, ku1); - Assert.False(await ke1); - Assert.False(await ke2); - } - } + [Fact] + public async Task DeleteAsync() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + _ = db.StringSetAsync(key, "Heyyyyy"); + var ke1 = db.KeyExistsAsync(key).ForAwait(); + var ku1 = db.KeyDeleteAsync(key).ForAwait(); + var ke2 = db.KeyExistsAsync(key).ForAwait(); + Assert.True(await ke1); + Assert.True(await ku1); + Assert.False(await ke2); + } - [Fact] - public async Task DeleteManyAsync() - { - using (var muxer = Create()) - { - var db = muxer.GetDatabase(); - var key1 = Me(); - var key2 = Me() + "2"; - var key3 = Me() + "3"; - _ = db.StringSetAsync(key1, "Heyyyyy"); - _ = db.StringSetAsync(key2, "Heyyyyy"); - // key 3 not set - var ku1 = db.KeyDeleteAsync(new RedisKey[] { key1, key2, key3 }).ForAwait(); - var ke1 = db.KeyExistsAsync(key1).ForAwait(); - var ke2 = db.KeyExistsAsync(key2).ForAwait(); - Assert.Equal(2, await ku1); - Assert.False(await ke1); - Assert.False(await ke2); - } - } + [Fact] + public async Task DeleteMany() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key1 = Me(); + var key2 = Me() + "2"; + var key3 = Me() + "3"; + _ = db.StringSetAsync(key1, "Heyyyyy"); + _ = db.StringSetAsync(key2, "Heyyyyy"); + // key 3 not set + var ku1 = db.KeyDelete(new RedisKey[] { key1, key2, key3 }); + var ke1 = db.KeyExistsAsync(key1).ForAwait(); + var ke2 = db.KeyExistsAsync(key2).ForAwait(); + Assert.Equal(2, ku1); + Assert.False(await ke1); + Assert.False(await ke2); + } - [Fact] - public void WrappedDatabasePrefixIntegration() - { - var key = Me(); - using (var conn = Create()) - { - var db = conn.GetDatabase().WithKeyPrefix("abc"); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringIncrement(key, flags: CommandFlags.FireAndForget); - db.StringIncrement(key, flags: CommandFlags.FireAndForget); - db.StringIncrement(key, flags: CommandFlags.FireAndForget); - - int count = (int)conn.GetDatabase().StringGet("abc" + key); - Assert.Equal(3, count); - } - } + [Fact] + public async Task DeleteManyAsync() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key1 = Me(); + var key2 = Me() + "2"; + var key3 = Me() + "3"; + _ = db.StringSetAsync(key1, "Heyyyyy"); + _ = db.StringSetAsync(key2, "Heyyyyy"); + // key 3 not set + var ku1 = db.KeyDeleteAsync(new RedisKey[] { key1, key2, key3 }).ForAwait(); + var ke1 = db.KeyExistsAsync(key1).ForAwait(); + var ke2 = db.KeyExistsAsync(key2).ForAwait(); + Assert.Equal(2, await ku1); + Assert.False(await ke1); + Assert.False(await ke2); + } + + [Fact] + public void WrappedDatabasePrefixIntegration() + { + var key = Me(); + using var conn = Create(); + var db = conn.GetDatabase().WithKeyPrefix("abc"); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringIncrement(key, flags: CommandFlags.FireAndForget); + db.StringIncrement(key, flags: CommandFlags.FireAndForget); + db.StringIncrement(key, flags: CommandFlags.FireAndForget); + + int count = (int)conn.GetDatabase().StringGet("abc" + key); + Assert.Equal(3, count); } } diff --git a/tests/StackExchange.Redis.Tests/BatchWrapperTests.cs b/tests/StackExchange.Redis.Tests/BatchWrapperTests.cs index 1bb41c53b..e06d478a5 100644 --- a/tests/StackExchange.Redis.Tests/BatchWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/BatchWrapperTests.cs @@ -3,25 +3,24 @@ using System.Text; using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(nameof(MoqDependentCollection))] +public sealed class BatchWrapperTests { - [Collection(nameof(MoqDependentCollection))] - public sealed class BatchWrapperTests - { - private readonly Mock mock; - private readonly BatchWrapper wrapper; + private readonly Mock mock; + private readonly BatchWrapper wrapper; - public BatchWrapperTests() - { - mock = new Mock(); - wrapper = new BatchWrapper(mock.Object, Encoding.UTF8.GetBytes("prefix:")); - } + public BatchWrapperTests() + { + mock = new Mock(); + wrapper = new BatchWrapper(mock.Object, Encoding.UTF8.GetBytes("prefix:")); + } - [Fact] - public void Execute() - { - wrapper.Execute(); - mock.Verify(_ => _.Execute(), Times.Once()); - } + [Fact] + public void Execute() + { + wrapper.Execute(); + mock.Verify(_ => _.Execute(), Times.Once()); } } diff --git a/tests/StackExchange.Redis.Tests/Batches.cs b/tests/StackExchange.Redis.Tests/Batches.cs index 04174cad0..8590e15b8 100644 --- a/tests/StackExchange.Redis.Tests/Batches.cs +++ b/tests/StackExchange.Redis.Tests/Batches.cs @@ -4,61 +4,56 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Batches : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Batches : TestBase - { - public Batches(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public Batches(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void TestBatchNotSent() - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var key = Me(); - conn.KeyDeleteAsync(key); - conn.StringSetAsync(key, "batch-not-sent"); - var batch = conn.CreateBatch(); + [Fact] + public void TestBatchNotSent() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDeleteAsync(key); + db.StringSetAsync(key, "batch-not-sent"); + var batch = db.CreateBatch(); - batch.KeyDeleteAsync(key); - batch.SetAddAsync(key, "a"); - batch.SetAddAsync(key, "b"); - batch.SetAddAsync(key, "c"); + batch.KeyDeleteAsync(key); + batch.SetAddAsync(key, "a"); + batch.SetAddAsync(key, "b"); + batch.SetAddAsync(key, "c"); - Assert.Equal("batch-not-sent", conn.StringGet(key)); - } - } + Assert.Equal("batch-not-sent", db.StringGet(key)); + } - [Fact] - public void TestBatchSent() - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var key = Me(); - conn.KeyDeleteAsync(key); - conn.StringSetAsync(key, "batch-sent"); - var tasks = new List(); - var batch = conn.CreateBatch(); - tasks.Add(batch.KeyDeleteAsync(key)); - tasks.Add(batch.SetAddAsync(key, "a")); - tasks.Add(batch.SetAddAsync(key, "b")); - tasks.Add(batch.SetAddAsync(key, "c")); - batch.Execute(); + [Fact] + public void TestBatchSent() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDeleteAsync(key); + db.StringSetAsync(key, "batch-sent"); + var tasks = new List(); + var batch = db.CreateBatch(); + tasks.Add(batch.KeyDeleteAsync(key)); + tasks.Add(batch.SetAddAsync(key, "a")); + tasks.Add(batch.SetAddAsync(key, "b")); + tasks.Add(batch.SetAddAsync(key, "c")); + batch.Execute(); - var result = conn.SetMembersAsync(key); - tasks.Add(result); - Task.WhenAll(tasks.ToArray()); + var result = db.SetMembersAsync(key); + tasks.Add(result); + Task.WhenAll(tasks.ToArray()); - var arr = result.Result; - Array.Sort(arr, (x, y) => string.Compare(x, y)); - Assert.Equal(3, arr.Length); - Assert.Equal("a", arr[0]); - Assert.Equal("b", arr[1]); - Assert.Equal("c", arr[2]); - } - } + var arr = result.Result; + Array.Sort(arr, (x, y) => string.Compare(x, y)); + Assert.Equal(3, arr.Length); + Assert.Equal("a", arr[0]); + Assert.Equal("b", arr[1]); + Assert.Equal("c", arr[2]); } } diff --git a/tests/StackExchange.Redis.Tests/Bits.cs b/tests/StackExchange.Redis.Tests/Bits.cs index 054d7661c..e78b09dfa 100644 --- a/tests/StackExchange.Redis.Tests/Bits.cs +++ b/tests/StackExchange.Redis.Tests/Bits.cs @@ -1,26 +1,23 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Bits : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Bits : TestBase - { - public Bits(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public Bits(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } - [Fact] - public void BasicOps() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); + [Fact] + public void BasicOps() + { + using var conn = Create(); + var db = conn.GetDatabase(); + RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSetBit(key, 10, true); - Assert.True(db.StringGetBit(key, 10)); - Assert.False(db.StringGetBit(key, 11)); - } - } + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSetBit(key, 10, true); + Assert.True(db.StringGetBit(key, 10)); + Assert.False(db.StringGetBit(key, 11)); } } diff --git a/tests/StackExchange.Redis.Tests/BoxUnbox.cs b/tests/StackExchange.Redis.Tests/BoxUnbox.cs index 3da861477..123a33a88 100644 --- a/tests/StackExchange.Redis.Tests/BoxUnbox.cs +++ b/tests/StackExchange.Redis.Tests/BoxUnbox.cs @@ -3,167 +3,166 @@ using System.Text; using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class BoxUnboxTests { - public class BoxUnboxTests + [Theory] + [MemberData(nameof(RoundTripValues))] + public void RoundTripRedisValue(RedisValue value) { - [Theory] - [MemberData(nameof(RoundTripValues))] - public void RoundTripRedisValue(RedisValue value) - { - var boxed = value.Box(); - var unboxed = RedisValue.Unbox(boxed); - AssertEqualGiveOrTakeNaN(value, unboxed); - } + var boxed = value.Box(); + var unboxed = RedisValue.Unbox(boxed); + AssertEqualGiveOrTakeNaN(value, unboxed); + } - [Theory] - [MemberData(nameof(UnboxValues))] - public void UnboxCommonValues(object value, RedisValue expected) - { - var unboxed = RedisValue.Unbox(value); - AssertEqualGiveOrTakeNaN(expected, unboxed); - } + [Theory] + [MemberData(nameof(UnboxValues))] + public void UnboxCommonValues(object value, RedisValue expected) + { + var unboxed = RedisValue.Unbox(value); + AssertEqualGiveOrTakeNaN(expected, unboxed); + } - [Theory] - [MemberData(nameof(InternedValues))] - public void ReturnInternedBoxesForCommonValues(RedisValue value, bool expectSameReference) - { - object? x = value.Box(), y = value.Box(); - Assert.Equal(expectSameReference, ReferenceEquals(x, y)); - // check we got the right values! - AssertEqualGiveOrTakeNaN(value, RedisValue.Unbox(x)); - AssertEqualGiveOrTakeNaN(value, RedisValue.Unbox(y)); - } + [Theory] + [MemberData(nameof(InternedValues))] + public void ReturnInternedBoxesForCommonValues(RedisValue value, bool expectSameReference) + { + object? x = value.Box(), y = value.Box(); + Assert.Equal(expectSameReference, ReferenceEquals(x, y)); + // check we got the right values! + AssertEqualGiveOrTakeNaN(value, RedisValue.Unbox(x)); + AssertEqualGiveOrTakeNaN(value, RedisValue.Unbox(y)); + } - private static void AssertEqualGiveOrTakeNaN(RedisValue expected, RedisValue actual) + private static void AssertEqualGiveOrTakeNaN(RedisValue expected, RedisValue actual) + { + if (expected.Type == RedisValue.StorageType.Double && actual.Type == expected.Type) { - if (expected.Type == RedisValue.StorageType.Double && actual.Type == expected.Type) + // because NaN != NaN, we need to special-case this scenario + bool enan = double.IsNaN((double)expected), anan = double.IsNaN((double)actual); + if (enan | anan) { - // because NaN != NaN, we need to special-case this scenario - bool enan = double.IsNaN((double)expected), anan = double.IsNaN((double)actual); - if (enan | anan) - { - Assert.Equal(enan, anan); - return; // and that's all - } + Assert.Equal(enan, anan); + return; // and that's all } - Assert.Equal(expected, actual); } + Assert.Equal(expected, actual); + } - private static readonly byte[] s_abc = Encoding.UTF8.GetBytes("abc"); - public static IEnumerable RoundTripValues - => new [] - { - new object[] { RedisValue.Null }, - new object[] { RedisValue.EmptyString }, - new object[] { (RedisValue)0L }, - new object[] { (RedisValue)1L }, - new object[] { (RedisValue)18L }, - new object[] { (RedisValue)19L }, - new object[] { (RedisValue)20L }, - new object[] { (RedisValue)21L }, - new object[] { (RedisValue)22L }, - new object[] { (RedisValue)(-1L) }, - new object[] { (RedisValue)0 }, - new object[] { (RedisValue)1 }, - new object[] { (RedisValue)18 }, - new object[] { (RedisValue)19 }, - new object[] { (RedisValue)20 }, - new object[] { (RedisValue)21 }, - new object[] { (RedisValue)22 }, - new object[] { (RedisValue)(-1) }, - new object[] { (RedisValue)0F }, - new object[] { (RedisValue)1F }, - new object[] { (RedisValue)(-1F) }, - new object[] { (RedisValue)0D }, - new object[] { (RedisValue)1D }, - new object[] { (RedisValue)(-1D) }, - new object[] { (RedisValue)float.PositiveInfinity }, - new object[] { (RedisValue)float.NegativeInfinity }, - new object[] { (RedisValue)float.NaN }, - new object[] { (RedisValue)double.PositiveInfinity }, - new object[] { (RedisValue)double.NegativeInfinity }, - new object[] { (RedisValue)double.NaN }, - new object[] { (RedisValue)true }, - new object[] { (RedisValue)false }, - new object[] { (RedisValue)(string?)null }, - new object[] { (RedisValue)"abc" }, - new object[] { (RedisValue)s_abc }, - new object[] { (RedisValue)new Memory(s_abc) }, - new object[] { (RedisValue)new ReadOnlyMemory(s_abc) }, - }; + private static readonly byte[] s_abc = Encoding.UTF8.GetBytes("abc"); + public static IEnumerable RoundTripValues + => new [] + { + new object[] { RedisValue.Null }, + new object[] { RedisValue.EmptyString }, + new object[] { (RedisValue)0L }, + new object[] { (RedisValue)1L }, + new object[] { (RedisValue)18L }, + new object[] { (RedisValue)19L }, + new object[] { (RedisValue)20L }, + new object[] { (RedisValue)21L }, + new object[] { (RedisValue)22L }, + new object[] { (RedisValue)(-1L) }, + new object[] { (RedisValue)0 }, + new object[] { (RedisValue)1 }, + new object[] { (RedisValue)18 }, + new object[] { (RedisValue)19 }, + new object[] { (RedisValue)20 }, + new object[] { (RedisValue)21 }, + new object[] { (RedisValue)22 }, + new object[] { (RedisValue)(-1) }, + new object[] { (RedisValue)0F }, + new object[] { (RedisValue)1F }, + new object[] { (RedisValue)(-1F) }, + new object[] { (RedisValue)0D }, + new object[] { (RedisValue)1D }, + new object[] { (RedisValue)(-1D) }, + new object[] { (RedisValue)float.PositiveInfinity }, + new object[] { (RedisValue)float.NegativeInfinity }, + new object[] { (RedisValue)float.NaN }, + new object[] { (RedisValue)double.PositiveInfinity }, + new object[] { (RedisValue)double.NegativeInfinity }, + new object[] { (RedisValue)double.NaN }, + new object[] { (RedisValue)true }, + new object[] { (RedisValue)false }, + new object[] { (RedisValue)(string?)null }, + new object[] { (RedisValue)"abc" }, + new object[] { (RedisValue)s_abc }, + new object[] { (RedisValue)new Memory(s_abc) }, + new object[] { (RedisValue)new ReadOnlyMemory(s_abc) }, + }; - public static IEnumerable UnboxValues - => new [] - { - new object?[] { null, RedisValue.Null }, - new object[] { "", RedisValue.EmptyString }, - new object[] { 0, (RedisValue)0 }, - new object[] { 1, (RedisValue)1 }, - new object[] { 18, (RedisValue)18 }, - new object[] { 19, (RedisValue)19 }, - new object[] { 20, (RedisValue)20 }, - new object[] { 21, (RedisValue)21 }, - new object[] { 22, (RedisValue)22 }, - new object[] { -1, (RedisValue)(-1) }, - new object[] { 18L, (RedisValue)18 }, - new object[] { 19L, (RedisValue)19 }, - new object[] { 20L, (RedisValue)20 }, - new object[] { 21L, (RedisValue)21 }, - new object[] { 22L, (RedisValue)22 }, - new object[] { -1L, (RedisValue)(-1) }, - new object[] { 0F, (RedisValue)0 }, - new object[] { 1F, (RedisValue)1 }, - new object[] { -1F, (RedisValue)(-1) }, - new object[] { 0D, (RedisValue)0 }, - new object[] { 1D, (RedisValue)1 }, - new object[] { -1D, (RedisValue)(-1) }, - new object[] { float.PositiveInfinity, (RedisValue)double.PositiveInfinity }, - new object[] { float.NegativeInfinity, (RedisValue)double.NegativeInfinity }, - new object[] { float.NaN, (RedisValue)double.NaN }, - new object[] { double.PositiveInfinity, (RedisValue)double.PositiveInfinity }, - new object[] { double.NegativeInfinity, (RedisValue)double.NegativeInfinity }, - new object[] { double.NaN, (RedisValue)double.NaN }, - new object[] { true, (RedisValue)true }, - new object[] { false, (RedisValue)false}, - new object[] { "abc", (RedisValue)"abc" }, - new object[] { s_abc, (RedisValue)s_abc }, - new object[] { new Memory(s_abc), (RedisValue)s_abc }, - new object[] { new ReadOnlyMemory(s_abc), (RedisValue)s_abc }, - new object[] { (RedisValue)1234, (RedisValue)1234 }, - }; + public static IEnumerable UnboxValues + => new [] + { + new object?[] { null, RedisValue.Null }, + new object[] { "", RedisValue.EmptyString }, + new object[] { 0, (RedisValue)0 }, + new object[] { 1, (RedisValue)1 }, + new object[] { 18, (RedisValue)18 }, + new object[] { 19, (RedisValue)19 }, + new object[] { 20, (RedisValue)20 }, + new object[] { 21, (RedisValue)21 }, + new object[] { 22, (RedisValue)22 }, + new object[] { -1, (RedisValue)(-1) }, + new object[] { 18L, (RedisValue)18 }, + new object[] { 19L, (RedisValue)19 }, + new object[] { 20L, (RedisValue)20 }, + new object[] { 21L, (RedisValue)21 }, + new object[] { 22L, (RedisValue)22 }, + new object[] { -1L, (RedisValue)(-1) }, + new object[] { 0F, (RedisValue)0 }, + new object[] { 1F, (RedisValue)1 }, + new object[] { -1F, (RedisValue)(-1) }, + new object[] { 0D, (RedisValue)0 }, + new object[] { 1D, (RedisValue)1 }, + new object[] { -1D, (RedisValue)(-1) }, + new object[] { float.PositiveInfinity, (RedisValue)double.PositiveInfinity }, + new object[] { float.NegativeInfinity, (RedisValue)double.NegativeInfinity }, + new object[] { float.NaN, (RedisValue)double.NaN }, + new object[] { double.PositiveInfinity, (RedisValue)double.PositiveInfinity }, + new object[] { double.NegativeInfinity, (RedisValue)double.NegativeInfinity }, + new object[] { double.NaN, (RedisValue)double.NaN }, + new object[] { true, (RedisValue)true }, + new object[] { false, (RedisValue)false}, + new object[] { "abc", (RedisValue)"abc" }, + new object[] { s_abc, (RedisValue)s_abc }, + new object[] { new Memory(s_abc), (RedisValue)s_abc }, + new object[] { new ReadOnlyMemory(s_abc), (RedisValue)s_abc }, + new object[] { (RedisValue)1234, (RedisValue)1234 }, + }; - public static IEnumerable InternedValues() + public static IEnumerable InternedValues() + { + for(int i = -20; i <= 40; i++) { - for(int i = -20; i <= 40; i++) - { - bool expectInterned = i >= -1 & i <= 20; - yield return new object[] { (RedisValue)i, expectInterned }; - yield return new object[] { (RedisValue)(long)i, expectInterned }; - yield return new object[] { (RedisValue)(float)i, expectInterned }; - yield return new object[] { (RedisValue)(double)i, expectInterned }; - } + bool expectInterned = i >= -1 & i <= 20; + yield return new object[] { (RedisValue)i, expectInterned }; + yield return new object[] { (RedisValue)(long)i, expectInterned }; + yield return new object[] { (RedisValue)(float)i, expectInterned }; + yield return new object[] { (RedisValue)(double)i, expectInterned }; + } - yield return new object[] { (RedisValue)float.NegativeInfinity, true }; - yield return new object[] { (RedisValue)(-0.5F), false }; - yield return new object[] { (RedisValue)(0.5F), false }; - yield return new object[] { (RedisValue)float.PositiveInfinity, true }; - yield return new object[] { (RedisValue)float.NaN, true }; + yield return new object[] { (RedisValue)float.NegativeInfinity, true }; + yield return new object[] { (RedisValue)(-0.5F), false }; + yield return new object[] { (RedisValue)(0.5F), false }; + yield return new object[] { (RedisValue)float.PositiveInfinity, true }; + yield return new object[] { (RedisValue)float.NaN, true }; - yield return new object[] { (RedisValue)double.NegativeInfinity, true }; - yield return new object[] { (RedisValue)(-0.5D), false }; - yield return new object[] { (RedisValue)(0.5D), false }; - yield return new object[] { (RedisValue)double.PositiveInfinity, true }; - yield return new object[] { (RedisValue)double.NaN, true }; + yield return new object[] { (RedisValue)double.NegativeInfinity, true }; + yield return new object[] { (RedisValue)(-0.5D), false }; + yield return new object[] { (RedisValue)(0.5D), false }; + yield return new object[] { (RedisValue)double.PositiveInfinity, true }; + yield return new object[] { (RedisValue)double.NaN, true }; - yield return new object[] { (RedisValue)true, true }; - yield return new object[] { (RedisValue)false, true }; - yield return new object[] { RedisValue.Null, true }; - yield return new object[] { RedisValue.EmptyString, true }; - yield return new object[] { (RedisValue)"abc", true }; - yield return new object[] { (RedisValue)s_abc, true }; - yield return new object[] { (RedisValue)new Memory(s_abc), false }; - yield return new object[] { (RedisValue)new ReadOnlyMemory(s_abc), false }; - } + yield return new object[] { (RedisValue)true, true }; + yield return new object[] { (RedisValue)false, true }; + yield return new object[] { RedisValue.Null, true }; + yield return new object[] { RedisValue.EmptyString, true }; + yield return new object[] { (RedisValue)"abc", true }; + yield return new object[] { (RedisValue)s_abc, true }; + yield return new object[] { (RedisValue)new Memory(s_abc), false }; + yield return new object[] { (RedisValue)new ReadOnlyMemory(s_abc), false }; } } diff --git a/tests/StackExchange.Redis.Tests/Cluster.cs b/tests/StackExchange.Redis.Tests/Cluster.cs index 902d2c095..f81375c97 100644 --- a/tests/StackExchange.Redis.Tests/Cluster.cs +++ b/tests/StackExchange.Redis.Tests/Cluster.cs @@ -9,739 +9,722 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class Cluster : TestBase { - public class Cluster : TestBase - { - public Cluster(ITestOutputHelper output) : base (output) { } - protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; + public Cluster(ITestOutputHelper output) : base (output) { } + protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; - [Fact] - public void ExportConfiguration() + [Fact] + public void ExportConfiguration() + { + if (File.Exists("cluster.zip")) File.Delete("cluster.zip"); + Assert.False(File.Exists("cluster.zip")); + using (var conn = Create(allowAdmin: true)) + using (var file = File.Create("cluster.zip")) { - if (File.Exists("cluster.zip")) File.Delete("cluster.zip"); - Assert.False(File.Exists("cluster.zip")); - using (var muxer = Create(allowAdmin: true)) - using (var file = File.Create("cluster.zip")) - { - muxer.ExportConfiguration(file); - } - Assert.True(File.Exists("cluster.zip")); + conn.ExportConfiguration(file); } + Assert.True(File.Exists("cluster.zip")); + } - [Fact] - public void ConnectUsesSingleSocket() + [Fact] + public void ConnectUsesSingleSocket() + { + for (int i = 0; i < 5; i++) { - for (int i = 0; i < 5; i++) + using var conn = Create(failMessage: i + ": ", log: Writer); + + foreach (var ep in conn.GetEndPoints()) + { + var srv = conn.GetServer(ep); + var counters = srv.GetCounters(); + Log($"{i}; interactive, {ep}, count: {counters.Interactive.SocketCount}"); + Log($"{i}; subscription, {ep}, count: {counters.Subscription.SocketCount}"); + } + foreach (var ep in conn.GetEndPoints()) { - using (var muxer = Create(failMessage: i + ": ", log: Writer)) - { - foreach (var ep in muxer.GetEndPoints()) - { - var srv = muxer.GetServer(ep); - var counters = srv.GetCounters(); - Log($"{i}; interactive, {ep}, count: {counters.Interactive.SocketCount}"); - Log($"{i}; subscription, {ep}, count: {counters.Subscription.SocketCount}"); - } - foreach (var ep in muxer.GetEndPoints()) - { - var srv = muxer.GetServer(ep); - var counters = srv.GetCounters(); - Assert.Equal(1, counters.Interactive.SocketCount); - Assert.Equal(1, counters.Subscription.SocketCount); - } - } + var srv = conn.GetServer(ep); + var counters = srv.GetCounters(); + Assert.Equal(1, counters.Interactive.SocketCount); + Assert.Equal(1, counters.Subscription.SocketCount); } } + } + + [Fact] + public void CanGetTotalStats() + { + using var conn = Create(); + + var counters = conn.GetCounters(); + Log(counters.ToString()); + } - [Fact] - public void CanGetTotalStats() + private void PrintEndpoints(EndPoint[] endpoints) + { + Log($"Endpoints Expected: {TestConfig.Current.ClusterStartPort}+{TestConfig.Current.ClusterServerCount}"); + Log("Endpoints Found:"); + foreach (var endpoint in endpoints) { - using (var muxer = Create()) - { - var counters = muxer.GetCounters(); - Log(counters.ToString()); - } + Log(" Endpoint: " + endpoint); } + } - private void PrintEndpoints(EndPoint[] endpoints) + [Fact] + public void Connect() + { + using var conn = Create(log: Writer); + + var expectedPorts = new HashSet(Enumerable.Range(TestConfig.Current.ClusterStartPort, TestConfig.Current.ClusterServerCount)); + var endpoints = conn.GetEndPoints(); + if (TestConfig.Current.ClusterServerCount != endpoints.Length) { - Log($"Endpoints Expected: {TestConfig.Current.ClusterStartPort}+{TestConfig.Current.ClusterServerCount}"); - Log("Endpoints Found:"); - foreach (var endpoint in endpoints) - { - Log(" Endpoint: " + endpoint); - } + PrintEndpoints(endpoints); } - [Fact] - public void Connect() + Assert.Equal(TestConfig.Current.ClusterServerCount, endpoints.Length); + int primaries = 0, replicas = 0; + var failed = new List(); + foreach (var endpoint in endpoints) { - var expectedPorts = new HashSet(Enumerable.Range(TestConfig.Current.ClusterStartPort, TestConfig.Current.ClusterServerCount)); - using (var muxer = Create(log: Writer)) + var server = conn.GetServer(endpoint); + if (!server.IsConnected) { - var endpoints = muxer.GetEndPoints(); - if (TestConfig.Current.ClusterServerCount != endpoints.Length) - { - PrintEndpoints(endpoints); - } - - Assert.Equal(TestConfig.Current.ClusterServerCount, endpoints.Length); - int primaries = 0, replicas = 0; - var failed = new List(); - foreach (var endpoint in endpoints) - { - var server = muxer.GetServer(endpoint); - if (!server.IsConnected) - { - failed.Add(endpoint); - } - Log("endpoint:" + endpoint); - Assert.Equal(endpoint, server.EndPoint); - - Log("endpoint-type:" + endpoint); - Assert.IsType(endpoint); - - Log("port:" + endpoint); - Assert.True(expectedPorts.Remove(((IPEndPoint)endpoint).Port)); - - Log("server-type:" + endpoint); - Assert.Equal(ServerType.Cluster, server.ServerType); - - if (server.IsReplica) replicas++; - else primaries++; - } - if (failed.Count != 0) - { - Log("{0} failues", failed.Count); - foreach (var fail in failed) - { - Log(fail.ToString()); - } - Assert.True(false, "not all servers connected"); - } - - Assert.Equal(TestConfig.Current.ClusterServerCount / 2, replicas); - Assert.Equal(TestConfig.Current.ClusterServerCount / 2, primaries); + failed.Add(endpoint); } - } + Log("endpoint:" + endpoint); + Assert.Equal(endpoint, server.EndPoint); - [Fact] - public void TestIdentity() + Log("endpoint-type:" + endpoint); + Assert.IsType(endpoint); + + Log("port:" + endpoint); + Assert.True(expectedPorts.Remove(((IPEndPoint)endpoint).Port)); + + Log("server-type:" + endpoint); + Assert.Equal(ServerType.Cluster, server.ServerType); + + if (server.IsReplica) replicas++; + else primaries++; + } + if (failed.Count != 0) { - using (var conn = Create()) + Log("{0} failues", failed.Count); + foreach (var fail in failed) { - RedisKey key = Guid.NewGuid().ToByteArray(); - var ep = conn.GetDatabase().IdentifyEndpoint(key); - Assert.NotNull(ep); - Assert.Equal(ep, conn.GetServer(ep).ClusterConfiguration?.GetBySlot(key)?.EndPoint); + Log(fail.ToString()); } + Assert.True(false, "not all servers connected"); } - [Fact] - public void IntentionalWrongServer() + Assert.Equal(TestConfig.Current.ClusterServerCount / 2, replicas); + Assert.Equal(TestConfig.Current.ClusterServerCount / 2, primaries); + } + + [Fact] + public void TestIdentity() + { + using var conn = Create(); + + RedisKey key = Guid.NewGuid().ToByteArray(); + var ep = conn.GetDatabase().IdentifyEndpoint(key); + Assert.NotNull(ep); + Assert.Equal(ep, conn.GetServer(ep).ClusterConfiguration?.GetBySlot(key)?.EndPoint); + } + + [Fact] + public void IntentionalWrongServer() + { + static string? StringGet(IServer server, RedisKey key, CommandFlags flags = CommandFlags.None) + => (string?)server.Execute("GET", new object[] { key }, flags); + + using var conn = Create(); + + var endpoints = conn.GetEndPoints(); + var servers = endpoints.Select(e => conn.GetServer(e)).ToList(); + + var key = Me(); + const string value = "abc"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, value, flags: CommandFlags.FireAndForget); + servers[0].Ping(); + var config = servers[0].ClusterConfiguration; + Assert.NotNull(config); + int slot = conn.HashSlot(key); + var rightPrimaryNode = config.GetBySlot(key); + Assert.NotNull(rightPrimaryNode); + Log("Right Primary: {0} {1}", rightPrimaryNode.EndPoint, rightPrimaryNode.NodeId); + + Assert.NotNull(rightPrimaryNode.EndPoint); + string? a = StringGet(conn.GetServer(rightPrimaryNode.EndPoint), key); + Assert.Equal(value, a); // right primary + + var node = config.Nodes.FirstOrDefault(x => !x.IsReplica && x.NodeId != rightPrimaryNode.NodeId); + Assert.NotNull(node); + Log("Using Primary: {0}", node.EndPoint, node.NodeId); { - static string? StringGet(IServer server, RedisKey key, CommandFlags flags = CommandFlags.None) - => (string?)server.Execute("GET", new object[] { key }, flags); + Assert.NotNull(node.EndPoint); + string? b = StringGet(conn.GetServer(node.EndPoint), key); + Assert.Equal(value, b); // wrong primary, allow redirect - using (var conn = Create()) - { - var endpoints = conn.GetEndPoints(); - var servers = endpoints.Select(e => conn.GetServer(e)).ToList(); - - var key = Me(); - const string value = "abc"; - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, value, flags: CommandFlags.FireAndForget); - servers[0].Ping(); - var config = servers[0].ClusterConfiguration; - Assert.NotNull(config); - int slot = conn.HashSlot(key); - var rightPrimaryNode = config.GetBySlot(key); - Assert.NotNull(rightPrimaryNode); - Log("Right Primary: {0} {1}", rightPrimaryNode.EndPoint, rightPrimaryNode.NodeId); - - Assert.NotNull(rightPrimaryNode.EndPoint); - string? a = StringGet(conn.GetServer(rightPrimaryNode.EndPoint), key); - Assert.Equal(value, a); // right primary - - var node = config.Nodes.FirstOrDefault(x => !x.IsReplica && x.NodeId != rightPrimaryNode.NodeId); - Assert.NotNull(node); - Log("Using Primary: {0}", node.EndPoint, node.NodeId); - { - Assert.NotNull(node.EndPoint); - string? b = StringGet(conn.GetServer(node.EndPoint), key); - Assert.Equal(value, b); // wrong primary, allow redirect - - var ex = Assert.Throws(() => StringGet(conn.GetServer(node.EndPoint), key, CommandFlags.NoRedirect)); - Assert.StartsWith($"Key has MOVED to Endpoint {rightPrimaryNode.EndPoint} and hashslot {slot}", ex.Message); - } - - node = config.Nodes.FirstOrDefault(x => x.IsReplica && x.ParentNodeId == rightPrimaryNode.NodeId); - Assert.NotNull(node); - { - Assert.NotNull(node.EndPoint); - string? d = StringGet(conn.GetServer(node.EndPoint), key); - Assert.Equal(value, d); // right replica - } - - node = config.Nodes.FirstOrDefault(x => x.IsReplica && x.ParentNodeId != rightPrimaryNode.NodeId); - Assert.NotNull(node); - { - Assert.NotNull(node.EndPoint); - string? e = StringGet(conn.GetServer(node.EndPoint), key); - Assert.Equal(value, e); // wrong replica, allow redirect - - var ex = Assert.Throws(() => StringGet(conn.GetServer(node.EndPoint), key, CommandFlags.NoRedirect)); - Assert.StartsWith($"Key has MOVED to Endpoint {rightPrimaryNode.EndPoint} and hashslot {slot}", ex.Message); - } - } + var ex = Assert.Throws(() => StringGet(conn.GetServer(node.EndPoint), key, CommandFlags.NoRedirect)); + Assert.StartsWith($"Key has MOVED to Endpoint {rightPrimaryNode.EndPoint} and hashslot {slot}", ex.Message); } - [Fact] - public void TransactionWithMultiServerKeys() + node = config.Nodes.FirstOrDefault(x => x.IsReplica && x.ParentNodeId == rightPrimaryNode.NodeId); + Assert.NotNull(node); { - var ex = Assert.Throws(() => - { - using (var muxer = Create()) - { - // connect - var cluster = muxer.GetDatabase(); - var anyServer = muxer.GetServer(muxer.GetEndPoints()[0]); - anyServer.Ping(); - Assert.Equal(ServerType.Cluster, anyServer.ServerType); - var config = anyServer.ClusterConfiguration; - Assert.NotNull(config); - - // invent 2 keys that we believe are served by different nodes - string x = Guid.NewGuid().ToString(), y; - var xNode = config.GetBySlot(x); - Assert.NotNull(xNode); - int abort = 1000; - do - { - y = Guid.NewGuid().ToString(); - } while (--abort > 0 && config.GetBySlot(y) == xNode); - if (abort == 0) Skip.Inconclusive("failed to find a different node to use"); - var yNode = config.GetBySlot(y); - Assert.NotNull(yNode); - Log("x={0}, served by {1}", x, xNode.NodeId); - Log("y={0}, served by {1}", y, yNode.NodeId); - Assert.NotEqual(xNode.NodeId, yNode.NodeId); - - // wipe those keys - cluster.KeyDelete(x, CommandFlags.FireAndForget); - cluster.KeyDelete(y, CommandFlags.FireAndForget); - - // create a transaction that attempts to assign both keys - var tran = cluster.CreateTransaction(); - tran.AddCondition(Condition.KeyNotExists(x)); - tran.AddCondition(Condition.KeyNotExists(y)); - _ = tran.StringSetAsync(x, "x-val"); - _ = tran.StringSetAsync(y, "y-val"); - tran.Execute(); - - Assert.True(false, "Expected single-slot rules to apply"); - // the rest no longer applies while we are following single-slot rules - - //// check that everything was aborted - //Assert.False(success, "tran aborted"); - //Assert.True(setX.IsCanceled, "set x cancelled"); - //Assert.True(setY.IsCanceled, "set y cancelled"); - //var existsX = cluster.KeyExistsAsync(x); - //var existsY = cluster.KeyExistsAsync(y); - //Assert.False(cluster.Wait(existsX), "x exists"); - //Assert.False(cluster.Wait(existsY), "y exists"); - } - }); - Assert.Equal("Multi-key operations must involve a single slot; keys can use 'hash tags' to help this, i.e. '{/users/12345}/account' and '{/users/12345}/contacts' will always be in the same slot", ex.Message); + Assert.NotNull(node.EndPoint); + string? d = StringGet(conn.GetServer(node.EndPoint), key); + Assert.Equal(value, d); // right replica } - [Fact] - public void TransactionWithSameServerKeys() + node = config.Nodes.FirstOrDefault(x => x.IsReplica && x.ParentNodeId != rightPrimaryNode.NodeId); + Assert.NotNull(node); { - var ex = Assert.Throws(() => - { - using (var muxer = Create()) - { - // connect - var cluster = muxer.GetDatabase(); - var anyServer = muxer.GetServer(muxer.GetEndPoints()[0]); - anyServer.Ping(); - var config = anyServer.ClusterConfiguration; - Assert.NotNull(config); - - // invent 2 keys that we believe are served by different nodes - string x = Guid.NewGuid().ToString(), y; - var xNode = config.GetBySlot(x); - int abort = 1000; - do - { - y = Guid.NewGuid().ToString(); - } while (--abort > 0 && config.GetBySlot(y) != xNode); - if (abort == 0) Skip.Inconclusive("failed to find a key with the same node to use"); - var yNode = config.GetBySlot(y); - Assert.NotNull(xNode); - Log("x={0}, served by {1}", x, xNode.NodeId); - Assert.NotNull(yNode); - Log("y={0}, served by {1}", y, yNode.NodeId); - Assert.Equal(xNode.NodeId, yNode.NodeId); - - // wipe those keys - cluster.KeyDelete(x, CommandFlags.FireAndForget); - cluster.KeyDelete(y, CommandFlags.FireAndForget); - - // create a transaction that attempts to assign both keys - var tran = cluster.CreateTransaction(); - tran.AddCondition(Condition.KeyNotExists(x)); - tran.AddCondition(Condition.KeyNotExists(y)); - _ = tran.StringSetAsync(x, "x-val"); - _ = tran.StringSetAsync(y, "y-val"); - tran.Execute(); - - Assert.True(false, "Expected single-slot rules to apply"); - // the rest no longer applies while we are following single-slot rules - - //// check that everything was aborted - //Assert.True(success, "tran aborted"); - //Assert.False(setX.IsCanceled, "set x cancelled"); - //Assert.False(setY.IsCanceled, "set y cancelled"); - //var existsX = cluster.KeyExistsAsync(x); - //var existsY = cluster.KeyExistsAsync(y); - //Assert.True(cluster.Wait(existsX), "x exists"); - //Assert.True(cluster.Wait(existsY), "y exists"); - } - }); - Assert.Equal("Multi-key operations must involve a single slot; keys can use 'hash tags' to help this, i.e. '{/users/12345}/account' and '{/users/12345}/contacts' will always be in the same slot", ex.Message); + Assert.NotNull(node.EndPoint); + string? e = StringGet(conn.GetServer(node.EndPoint), key); + Assert.Equal(value, e); // wrong replica, allow redirect + + var ex = Assert.Throws(() => StringGet(conn.GetServer(node.EndPoint), key, CommandFlags.NoRedirect)); + Assert.StartsWith($"Key has MOVED to Endpoint {rightPrimaryNode.EndPoint} and hashslot {slot}", ex.Message); } + } - [Fact] - public void TransactionWithSameSlotKeys() + [Fact] + public void TransactionWithMultiServerKeys() + { + var ex = Assert.Throws(() => { - using (var muxer = Create()) + using var conn = Create(); + + // connect + var cluster = conn.GetDatabase(); + var anyServer = conn.GetServer(conn.GetEndPoints()[0]); + anyServer.Ping(); + Assert.Equal(ServerType.Cluster, anyServer.ServerType); + var config = anyServer.ClusterConfiguration; + Assert.NotNull(config); + + // invent 2 keys that we believe are served by different nodes + string x = Guid.NewGuid().ToString(), y; + var xNode = config.GetBySlot(x); + Assert.NotNull(xNode); + int abort = 1000; + do { - // connect - var cluster = muxer.GetDatabase(); - var anyServer = muxer.GetServer(muxer.GetEndPoints()[0]); - anyServer.Ping(); - var config = anyServer.ClusterConfiguration; - Assert.NotNull(config); - - // invent 2 keys that we believe are in the same slot - var guid = Guid.NewGuid().ToString(); - string x = "/{" + guid + "}/foo", y = "/{" + guid + "}/bar"; - - Assert.Equal(muxer.HashSlot(x), muxer.HashSlot(y)); - var xNode = config.GetBySlot(x); - var yNode = config.GetBySlot(y); - Assert.NotNull(xNode); - Log("x={0}, served by {1}", x, xNode.NodeId); - Assert.NotNull(yNode); - Log("y={0}, served by {1}", y, yNode.NodeId); - Assert.Equal(xNode.NodeId, yNode.NodeId); - - // wipe those keys - cluster.KeyDelete(x, CommandFlags.FireAndForget); - cluster.KeyDelete(y, CommandFlags.FireAndForget); - - // create a transaction that attempts to assign both keys - var tran = cluster.CreateTransaction(); - tran.AddCondition(Condition.KeyNotExists(x)); - tran.AddCondition(Condition.KeyNotExists(y)); - var setX = tran.StringSetAsync(x, "x-val"); - var setY = tran.StringSetAsync(y, "y-val"); - bool success = tran.Execute(); - - // check that everything was aborted - Assert.True(success, "tran aborted"); - Assert.False(setX.IsCanceled, "set x cancelled"); - Assert.False(setY.IsCanceled, "set y cancelled"); - var existsX = cluster.KeyExistsAsync(x); - var existsY = cluster.KeyExistsAsync(y); - Assert.True(cluster.Wait(existsX), "x exists"); - Assert.True(cluster.Wait(existsY), "y exists"); - } - } - - [Theory] - [InlineData(null, 10)] - [InlineData(null, 100)] - [InlineData("abc", 10)] - [InlineData("abc", 100)] + y = Guid.NewGuid().ToString(); + } while (--abort > 0 && config.GetBySlot(y) == xNode); + if (abort == 0) Skip.Inconclusive("failed to find a different node to use"); + var yNode = config.GetBySlot(y); + Assert.NotNull(yNode); + Log("x={0}, served by {1}", x, xNode.NodeId); + Log("y={0}, served by {1}", y, yNode.NodeId); + Assert.NotEqual(xNode.NodeId, yNode.NodeId); + + // wipe those keys + cluster.KeyDelete(x, CommandFlags.FireAndForget); + cluster.KeyDelete(y, CommandFlags.FireAndForget); + + // create a transaction that attempts to assign both keys + var tran = cluster.CreateTransaction(); + tran.AddCondition(Condition.KeyNotExists(x)); + tran.AddCondition(Condition.KeyNotExists(y)); + _ = tran.StringSetAsync(x, "x-val"); + _ = tran.StringSetAsync(y, "y-val"); + tran.Execute(); + + Assert.True(false, "Expected single-slot rules to apply"); + // the rest no longer applies while we are following single-slot rules + + //// check that everything was aborted + //Assert.False(success, "tran aborted"); + //Assert.True(setX.IsCanceled, "set x cancelled"); + //Assert.True(setY.IsCanceled, "set y cancelled"); + //var existsX = cluster.KeyExistsAsync(x); + //var existsY = cluster.KeyExistsAsync(y); + //Assert.False(cluster.Wait(existsX), "x exists"); + //Assert.False(cluster.Wait(existsY), "y exists"); + }); + Assert.Equal("Multi-key operations must involve a single slot; keys can use 'hash tags' to help this, i.e. '{/users/12345}/account' and '{/users/12345}/contacts' will always be in the same slot", ex.Message); + } - public void Keys(string pattern, int pageSize) + [Fact] + public void TransactionWithSameServerKeys() + { + var ex = Assert.Throws(() => { - using (var conn = Create(allowAdmin: true)) + using var conn = Create(); + + // connect + var cluster = conn.GetDatabase(); + var anyServer = conn.GetServer(conn.GetEndPoints()[0]); + anyServer.Ping(); + var config = anyServer.ClusterConfiguration; + Assert.NotNull(config); + + // invent 2 keys that we believe are served by different nodes + string x = Guid.NewGuid().ToString(), y; + var xNode = config.GetBySlot(x); + int abort = 1000; + do { - _ = conn.GetDatabase(); - var server = conn.GetEndPoints().Select(x => conn.GetServer(x)).First(x => !x.IsReplica); - server.FlushAllDatabases(); - try - { - Assert.False(server.Keys(pattern: pattern, pageSize: pageSize).Any()); - Log("Complete: '{0}' / {1}", pattern, pageSize); - } - catch - { - Log("Failed: '{0}' / {1}", pattern, pageSize); - throw; - } - } + y = Guid.NewGuid().ToString(); + } while (--abort > 0 && config.GetBySlot(y) != xNode); + if (abort == 0) Skip.Inconclusive("failed to find a key with the same node to use"); + var yNode = config.GetBySlot(y); + Assert.NotNull(xNode); + Log("x={0}, served by {1}", x, xNode.NodeId); + Assert.NotNull(yNode); + Log("y={0}, served by {1}", y, yNode.NodeId); + Assert.Equal(xNode.NodeId, yNode.NodeId); + + // wipe those keys + cluster.KeyDelete(x, CommandFlags.FireAndForget); + cluster.KeyDelete(y, CommandFlags.FireAndForget); + + // create a transaction that attempts to assign both keys + var tran = cluster.CreateTransaction(); + tran.AddCondition(Condition.KeyNotExists(x)); + tran.AddCondition(Condition.KeyNotExists(y)); + _ = tran.StringSetAsync(x, "x-val"); + _ = tran.StringSetAsync(y, "y-val"); + tran.Execute(); + + Assert.True(false, "Expected single-slot rules to apply"); + // the rest no longer applies while we are following single-slot rules + + //// check that everything was aborted + //Assert.True(success, "tran aborted"); + //Assert.False(setX.IsCanceled, "set x cancelled"); + //Assert.False(setY.IsCanceled, "set y cancelled"); + //var existsX = cluster.KeyExistsAsync(x); + //var existsY = cluster.KeyExistsAsync(y); + //Assert.True(cluster.Wait(existsX), "x exists"); + //Assert.True(cluster.Wait(existsY), "y exists"); + }); + Assert.Equal("Multi-key operations must involve a single slot; keys can use 'hash tags' to help this, i.e. '{/users/12345}/account' and '{/users/12345}/contacts' will always be in the same slot", ex.Message); + } + + [Fact] + public void TransactionWithSameSlotKeys() + { + using var conn = Create(); + + // connect + var cluster = conn.GetDatabase(); + var anyServer = conn.GetServer(conn.GetEndPoints()[0]); + anyServer.Ping(); + var config = anyServer.ClusterConfiguration; + Assert.NotNull(config); + + // invent 2 keys that we believe are in the same slot + var guid = Guid.NewGuid().ToString(); + string x = "/{" + guid + "}/foo", y = "/{" + guid + "}/bar"; + + Assert.Equal(conn.HashSlot(x), conn.HashSlot(y)); + var xNode = config.GetBySlot(x); + var yNode = config.GetBySlot(y); + Assert.NotNull(xNode); + Log("x={0}, served by {1}", x, xNode.NodeId); + Assert.NotNull(yNode); + Log("y={0}, served by {1}", y, yNode.NodeId); + Assert.Equal(xNode.NodeId, yNode.NodeId); + + // wipe those keys + cluster.KeyDelete(x, CommandFlags.FireAndForget); + cluster.KeyDelete(y, CommandFlags.FireAndForget); + + // create a transaction that attempts to assign both keys + var tran = cluster.CreateTransaction(); + tran.AddCondition(Condition.KeyNotExists(x)); + tran.AddCondition(Condition.KeyNotExists(y)); + var setX = tran.StringSetAsync(x, "x-val"); + var setY = tran.StringSetAsync(y, "y-val"); + bool success = tran.Execute(); + + // check that everything was aborted + Assert.True(success, "tran aborted"); + Assert.False(setX.IsCanceled, "set x cancelled"); + Assert.False(setY.IsCanceled, "set y cancelled"); + var existsX = cluster.KeyExistsAsync(x); + var existsY = cluster.KeyExistsAsync(y); + Assert.True(cluster.Wait(existsX), "x exists"); + Assert.True(cluster.Wait(existsY), "y exists"); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "Because.")] + [Theory (Skip = "FlushAllDatabases")] + [InlineData(null, 10)] + [InlineData(null, 100)] + [InlineData("abc", 10)] + [InlineData("abc", 100)] + public void Keys(string pattern, int pageSize) + { + using var conn = Create(allowAdmin: true); + + _ = conn.GetDatabase(); + var server = conn.GetEndPoints().Select(x => conn.GetServer(x)).First(x => !x.IsReplica); + server.FlushAllDatabases(); + try + { + Assert.False(server.Keys(pattern: pattern, pageSize: pageSize).Any()); + Log("Complete: '{0}' / {1}", pattern, pageSize); + } + catch + { + Log("Failed: '{0}' / {1}", pattern, pageSize); + throw; } + } + + [Theory] + [InlineData("", 0)] + [InlineData("abc", 7638)] + [InlineData("{abc}", 7638)] + [InlineData("abcdef", 15101)] + [InlineData("abc{abc}def", 7638)] + [InlineData("c", 7365)] + [InlineData("g", 7233)] + [InlineData("d", 11298)] - [Theory] - [InlineData("", 0)] - [InlineData("abc", 7638)] - [InlineData("{abc}", 7638)] - [InlineData("abcdef", 15101)] - [InlineData("abc{abc}def", 7638)] - [InlineData("c", 7365)] - [InlineData("g", 7233)] - [InlineData("d", 11298)] + [InlineData("user1000", 3443)] + [InlineData("{user1000}", 3443)] + [InlineData("abc{user1000}", 3443)] + [InlineData("abc{user1000}def", 3443)] + [InlineData("{user1000}.following", 3443)] + [InlineData("{user1000}.followers", 3443)] - [InlineData("user1000", 3443)] - [InlineData("{user1000}", 3443)] - [InlineData("abc{user1000}", 3443)] - [InlineData("abc{user1000}def", 3443)] - [InlineData("{user1000}.following", 3443)] - [InlineData("{user1000}.followers", 3443)] + [InlineData("foo{}{bar}", 8363)] + + [InlineData("foo{{bar}}zap", 4015)] + [InlineData("{bar", 4015)] + + [InlineData("foo{bar}{zap}", 5061)] + [InlineData("bar", 5061)] + + public void HashSlots(string key, int slot) + { + using var conn = Create(connectTimeout: 5000); - [InlineData("foo{}{bar}", 8363)] + Assert.Equal(slot, conn.HashSlot(key)); + } - [InlineData("foo{{bar}}zap", 4015)] - [InlineData("{bar", 4015)] + [Fact] + public void SScan() + { + using var conn = Create(); - [InlineData("foo{bar}{zap}", 5061)] - [InlineData("bar", 5061)] + RedisKey key = "a"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); - public void HashSlots(string key, int slot) + int totalUnfiltered = 0, totalFiltered = 0; + for (int i = 0; i < 1000; i++) { - using (var muxer = Create(connectTimeout: 5000)) - { - Assert.Equal(slot, muxer.HashSlot(key)); - } + db.SetAdd(key, i, CommandFlags.FireAndForget); + totalUnfiltered += i; + if (i.ToString().Contains('3')) totalFiltered += i; } + var unfilteredActual = db.SetScan(key).Select(x => (int)x).Sum(); + var filteredActual = db.SetScan(key, "*3*").Select(x => (int)x).Sum(); + Assert.Equal(totalUnfiltered, unfilteredActual); + Assert.Equal(totalFiltered, filteredActual); + } - [Fact] - public void SScan() + [Fact] + public void GetConfig() + { + using var conn = Create(allowAdmin: true, log: Writer); + + var endpoints = conn.GetEndPoints(); + var server = conn.GetServer(endpoints[0]); + var nodes = server.ClusterNodes(); + Assert.NotNull(nodes); + + Log("Endpoints:"); + foreach (var endpoint in endpoints) { - using (var conn = Create()) - { - RedisKey key = "a"; - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - int totalUnfiltered = 0, totalFiltered = 0; - for (int i = 0; i < 1000; i++) - { - db.SetAdd(key, i, CommandFlags.FireAndForget); - totalUnfiltered += i; - if (i.ToString().Contains('3')) totalFiltered += i; - } - var unfilteredActual = db.SetScan(key).Select(x => (int)x).Sum(); - var filteredActual = db.SetScan(key, "*3*").Select(x => (int)x).Sum(); - Assert.Equal(totalUnfiltered, unfilteredActual); - Assert.Equal(totalFiltered, filteredActual); - } + Log(endpoint.ToString()); } - - [Fact] - public void GetConfig() + Log("Nodes:"); + foreach (var node in nodes.Nodes.OrderBy(x => x)) { - using (var muxer = Create(allowAdmin: true, log: Writer)) - { - var endpoints = muxer.GetEndPoints(); - var server = muxer.GetServer(endpoints[0]); - var nodes = server.ClusterNodes(); - Assert.NotNull(nodes); - - Log("Endpoints:"); - foreach (var endpoint in endpoints) - { - Log(endpoint.ToString()); - } - Log("Nodes:"); - foreach (var node in nodes.Nodes.OrderBy(x => x)) - { - Log(node.ToString()); - } - - Assert.Equal(TestConfig.Current.ClusterServerCount, endpoints.Length); - Assert.Equal(TestConfig.Current.ClusterServerCount, nodes.Nodes.Count); - } + Log(node.ToString()); } - [Fact] - public void AccessRandomKeys() + Assert.Equal(TestConfig.Current.ClusterServerCount, endpoints.Length); + Assert.Equal(TestConfig.Current.ClusterServerCount, nodes.Nodes.Count); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "Because.")] + [Fact(Skip = "FlushAllDatabases")] + public void AccessRandomKeys() + { + using var conn = Create(allowAdmin: true); + + var cluster = conn.GetDatabase(); + int slotMovedCount = 0; + conn.HashSlotMoved += (s, a) => { - using (var conn = Create(allowAdmin: true)) + Assert.NotNull(a.OldEndPoint); + Log("{0} moved from {1} to {2}", a.HashSlot, Describe(a.OldEndPoint), Describe(a.NewEndPoint)); + Interlocked.Increment(ref slotMovedCount); + }; + var pairs = new Dictionary(); + const int COUNT = 500; + int index = 0; + + var servers = conn.GetEndPoints().Select(x => conn.GetServer(x)).ToList(); + foreach (var server in servers) + { + if (!server.IsReplica) { - var cluster = conn.GetDatabase(); - int slotMovedCount = 0; - conn.HashSlotMoved += (s, a) => - { - Assert.NotNull(a.OldEndPoint); - Log("{0} moved from {1} to {2}", a.HashSlot, Describe(a.OldEndPoint), Describe(a.NewEndPoint)); - Interlocked.Increment(ref slotMovedCount); - }; - var pairs = new Dictionary(); - const int COUNT = 500; - int index = 0; - - var servers = conn.GetEndPoints().Select(x => conn.GetServer(x)).ToList(); - foreach (var server in servers) - { - if (!server.IsReplica) - { - server.Ping(); - server.FlushAllDatabases(); - } - } - - for (int i = 0; i < COUNT; i++) - { - var key = Guid.NewGuid().ToString(); - var value = Guid.NewGuid().ToString(); - pairs.Add(key, value); - cluster.StringSet(key, value, flags: CommandFlags.FireAndForget); - } - - var expected = new string[COUNT]; - var actual = new Task[COUNT]; - index = 0; - foreach (var pair in pairs) - { - expected[index] = pair.Value; - actual[index] = cluster.StringGetAsync(pair.Key); - index++; - } - cluster.WaitAll(actual); - for (int i = 0; i < COUNT; i++) - { - Assert.Equal(expected[i], actual[i].Result); - } - - int total = 0; - Parallel.ForEach(servers, server => - { - if (!server.IsReplica) - { - int count = server.Keys(pageSize: 100).Count(); - Log("{0} has {1} keys", server.EndPoint, count); - Interlocked.Add(ref total, count); - } - }); - - foreach (var server in servers) - { - var counters = server.GetCounters(); - Log(counters.ToString()); - } - int final = Interlocked.CompareExchange(ref total, 0, 0); - Assert.Equal(COUNT, final); - Assert.Equal(0, Interlocked.CompareExchange(ref slotMovedCount, 0, 0)); + server.Ping(); + server.FlushAllDatabases(); } } - [Theory] - [InlineData(CommandFlags.DemandMaster, false)] - [InlineData(CommandFlags.DemandReplica, true)] - [InlineData(CommandFlags.PreferMaster, false)] - [InlineData(CommandFlags.PreferReplica, true)] - public void GetFromRightNodeBasedOnFlags(CommandFlags flags, bool isReplica) + for (int i = 0; i < COUNT; i++) { - using (var muxer = Create(allowAdmin: true)) - { - var db = muxer.GetDatabase(); - for (int i = 0; i < 1000; i++) - { - var key = Guid.NewGuid().ToString(); - var endpoint = db.IdentifyEndpoint(key, flags); - Assert.NotNull(endpoint); - var server = muxer.GetServer(endpoint); - Assert.Equal(isReplica, server.IsReplica); - } - } + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + pairs.Add(key, value); + cluster.StringSet(key, value, flags: CommandFlags.FireAndForget); } - private static string Describe(EndPoint endpoint) => endpoint?.ToString() ?? "(unknown)"; + var expected = new string[COUNT]; + var actual = new Task[COUNT]; + index = 0; + foreach (var pair in pairs) + { + expected[index] = pair.Value; + actual[index] = cluster.StringGetAsync(pair.Key); + index++; + } + cluster.WaitAll(actual); + for (int i = 0; i < COUNT; i++) + { + Assert.Equal(expected[i], actual[i].Result); + } - [Fact] - public void SimpleProfiling() + int total = 0; + Parallel.ForEach(servers, server => { - using (var conn = Create(log: Writer)) + if (!server.IsReplica) { - var profiler = new ProfilingSession(); - var key = Me(); - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - conn.RegisterProfiler(() => profiler); - db.StringSet(key, "world"); - var val = db.StringGet(key); - Assert.Equal("world", val); - - var msgs = profiler.FinishProfiling().Where(m => m.Command == "GET" || m.Command == "SET").ToList(); - foreach (var msg in msgs) - { - Log("Profiler Message: " + Environment.NewLine + msg); - } - Log("Checking GET..."); - Assert.Contains(msgs, m => m.Command == "GET"); - Log("Checking SET..."); - Assert.Contains(msgs, m => m.Command == "SET"); - Assert.Equal(2, msgs.Count(m => m.RetransmissionOf is null)); - - var arr = msgs.Where(m => m.RetransmissionOf is null).ToArray(); - Assert.Equal("SET", arr[0].Command); - Assert.Equal("GET", arr[1].Command); + int count = server.Keys(pageSize: 100).Count(); + Log("{0} has {1} keys", server.EndPoint, count); + Interlocked.Add(ref total, count); } + }); + + foreach (var server in servers) + { + var counters = server.GetCounters(); + Log(counters.ToString()); } + int final = Interlocked.CompareExchange(ref total, 0, 0); + Assert.Equal(COUNT, final); + Assert.Equal(0, Interlocked.CompareExchange(ref slotMovedCount, 0, 0)); + } - [Fact] - public void MultiKeyQueryFails() + [Theory] + [InlineData(CommandFlags.DemandMaster, false)] + [InlineData(CommandFlags.DemandReplica, true)] + [InlineData(CommandFlags.PreferMaster, false)] + [InlineData(CommandFlags.PreferReplica, true)] + public void GetFromRightNodeBasedOnFlags(CommandFlags flags, bool isReplica) + { + using var conn = Create(allowAdmin: true); + + var db = conn.GetDatabase(); + for (int i = 0; i < 1000; i++) { - var keys = InventKeys(); // note the rules expected of this data are enforced in GroupedQueriesWork + var key = Guid.NewGuid().ToString(); + var endpoint = db.IdentifyEndpoint(key, flags); + Assert.NotNull(endpoint); + var server = conn.GetServer(endpoint); + Assert.Equal(isReplica, server.IsReplica); + } + } - using (var conn = Create()) - { - var ex = Assert.Throws(() => conn.GetDatabase(0).StringGet(keys)); - Assert.Contains("Multi-key operations must involve a single slot", ex.Message); - } + private static string Describe(EndPoint endpoint) => endpoint?.ToString() ?? "(unknown)"; + + [Fact] + public void SimpleProfiling() + { + using var conn = Create(log: Writer); + + var profiler = new ProfilingSession(); + var key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + conn.RegisterProfiler(() => profiler); + db.StringSet(key, "world"); + var val = db.StringGet(key); + Assert.Equal("world", val); + + var msgs = profiler.FinishProfiling().Where(m => m.Command == "GET" || m.Command == "SET").ToList(); + foreach (var msg in msgs) + { + Log("Profiler Message: " + Environment.NewLine + msg); } + Log("Checking GET..."); + Assert.Contains(msgs, m => m.Command == "GET"); + Log("Checking SET..."); + Assert.Contains(msgs, m => m.Command == "SET"); + Assert.Equal(2, msgs.Count(m => m.RetransmissionOf is null)); + + var arr = msgs.Where(m => m.RetransmissionOf is null).ToArray(); + Assert.Equal("SET", arr[0].Command); + Assert.Equal("GET", arr[1].Command); + } + + [Fact] + public void MultiKeyQueryFails() + { + var keys = InventKeys(); // note the rules expected of this data are enforced in GroupedQueriesWork + + using var conn = Create(); + + var ex = Assert.Throws(() => conn.GetDatabase(0).StringGet(keys)); + Assert.Contains("Multi-key operations must involve a single slot", ex.Message); + } - private static RedisKey[] InventKeys() + private static RedisKey[] InventKeys() + { + RedisKey[] keys = new RedisKey[256]; + Random rand = new Random(12324); + string InventString() { - RedisKey[] keys = new RedisKey[256]; - Random rand = new Random(12324); - string InventString() - { - const string alphabet = "abcdefghijklmnopqrstuvwxyz012345689"; - var len = rand.Next(10, 50); - char[] chars = new char[len]; - for (int i = 0; i < len; i++) - chars[i] = alphabet[rand.Next(alphabet.Length)]; - return new string(chars); - } + const string alphabet = "abcdefghijklmnopqrstuvwxyz012345689"; + var len = rand.Next(10, 50); + char[] chars = new char[len]; + for (int i = 0; i < len; i++) + chars[i] = alphabet[rand.Next(alphabet.Length)]; + return new string(chars); + } - for (int i = 0; i < keys.Length; i++) - { - keys[i] = InventString(); - } - return keys; + for (int i = 0; i < keys.Length; i++) + { + keys[i] = InventString(); } + return keys; + } + + [Fact] + public void GroupedQueriesWork() + { + // note it doesn't matter that the data doesn't exist for this; + // the point here is that the entire thing *won't work* otherwise, + // as per above test - [Fact] - public void GroupedQueriesWork() + var keys = InventKeys(); + using var conn = Create(); + + var grouped = keys.GroupBy(key => conn.GetHashSlot(key)).ToList(); + Assert.True(grouped.Count > 1); // check not all a super-group + Assert.True(grouped.Count < keys.Length); // check not all singleton groups + Assert.Equal(keys.Length, grouped.Sum(x => x.Count())); // check they're all there + Assert.Contains(grouped, x => x.Count() > 1); // check at least one group with multiple items (redundant from above, but... meh) + + Log($"{grouped.Count} groups, min: {grouped.Min(x => x.Count())}, max: {grouped.Max(x => x.Count())}, avg: {grouped.Average(x => x.Count())}"); + + var db = conn.GetDatabase(0); + var all = grouped.SelectMany(grp => { - // note it doesn't matter that the data doesn't exist for this; - // the point here is that the entire thing *won't work* otherwise, - // as per above test + var grpKeys = grp.ToArray(); + var values = db.StringGet(grpKeys); + return grpKeys.Zip(values, (key, val) => new { key, val }); + }).ToDictionary(x => x.key, x => x.val); - var keys = InventKeys(); - using (var conn = Create()) - { - var grouped = keys.GroupBy(key => conn.GetHashSlot(key)).ToList(); - Assert.True(grouped.Count > 1); // check not all a super-group - Assert.True(grouped.Count < keys.Length); // check not all singleton groups - Assert.Equal(keys.Length, grouped.Sum(x => x.Count())); // check they're all there - Assert.Contains(grouped, x => x.Count() > 1); // check at least one group with multiple items (redundant from above, but... meh) - - Log($"{grouped.Count} groups, min: {grouped.Min(x => x.Count())}, max: {grouped.Max(x => x.Count())}, avg: {grouped.Average(x => x.Count())}"); - - var db = conn.GetDatabase(0); - var all = grouped.SelectMany(grp => { - var grpKeys = grp.ToArray(); - var values = db.StringGet(grpKeys); - return grpKeys.Zip(values, (key, val) => new { key, val }); - }).ToDictionary(x => x.key, x => x.val); - - Assert.Equal(keys.Length, all.Count); - } - } + Assert.Equal(keys.Length, all.Count); + } + + [Fact] + public void MovedProfiling() + { + var Key = Me(); + const string Value = "redirected-value"; - [Fact] - public void MovedProfiling() + var profiler = new Profiling.PerThreadProfiler(); + + using var conn = Create(); + + conn.RegisterProfiler(profiler.GetSession); + + var endpoints = conn.GetEndPoints(); + var servers = endpoints.Select(e => conn.GetServer(e)); + + var db = conn.GetDatabase(); + db.KeyDelete(Key); + db.StringSet(Key, Value); + var config = servers.First().ClusterConfiguration; + Assert.NotNull(config); + + //int slot = conn.HashSlot(Key); + var rightPrimaryNode = config.GetBySlot(Key); + Assert.NotNull(rightPrimaryNode); + + Assert.NotNull(rightPrimaryNode.EndPoint); + string? a = (string?)conn.GetServer(rightPrimaryNode.EndPoint).Execute("GET", Key); + Assert.Equal(Value, a); // right primary + + var wrongPrimaryNode = config.Nodes.FirstOrDefault(x => !x.IsReplica && x.NodeId != rightPrimaryNode.NodeId); + Assert.NotNull(wrongPrimaryNode); + + Assert.NotNull(wrongPrimaryNode.EndPoint); + string? b = (string?)conn.GetServer(wrongPrimaryNode.EndPoint).Execute("GET", Key); + Assert.Equal(Value, b); // wrong primary, allow redirect + + var msgs = profiler.GetSession().FinishProfiling().ToList(); + + // verify that things actually got recorded properly, and the retransmission profilings are connected as expected { - var Key = Me(); - const string Value = "redirected-value"; + // expect 1 DEL, 1 SET, 1 GET (to right primary), 1 GET (to wrong primary) that was responded to by an ASK, and 1 GET (to right primary or a replica of it) + Assert.Equal(5, msgs.Count); + Assert.Equal(1, msgs.Count(c => c.Command == "DEL" || c.Command == "UNLINK")); + Assert.Equal(1, msgs.Count(c => c.Command == "SET")); + Assert.Equal(3, msgs.Count(c => c.Command == "GET")); + + var toRightPrimaryNotRetransmission = msgs.Where(m => m.Command == "GET" && m.EndPoint.Equals(rightPrimaryNode.EndPoint) && m.RetransmissionOf == null); + Assert.Single(toRightPrimaryNotRetransmission); + + var toWrongPrimaryWithoutRetransmission = msgs.Where(m => m.Command == "GET" && m.EndPoint.Equals(wrongPrimaryNode.EndPoint) && m.RetransmissionOf == null).ToList(); + Assert.Single(toWrongPrimaryWithoutRetransmission); - var profiler = new Profiling.PerThreadProfiler(); + var toRightPrimaryOrReplicaAsRetransmission = msgs.Where(m => m.Command == "GET" && (m.EndPoint.Equals(rightPrimaryNode.EndPoint) || rightPrimaryNode.Children.Any(c => m.EndPoint.Equals(c.EndPoint))) && m.RetransmissionOf != null).ToList(); + Assert.Single(toRightPrimaryOrReplicaAsRetransmission); - using (var conn = Create()) + var originalWrongPrimary = toWrongPrimaryWithoutRetransmission.Single(); + var retransmissionToRight = toRightPrimaryOrReplicaAsRetransmission.Single(); + + Assert.True(ReferenceEquals(originalWrongPrimary, retransmissionToRight.RetransmissionOf)); + } + + foreach (var msg in msgs) + { + Assert.True(msg.CommandCreated != default(DateTime)); + Assert.True(msg.CreationToEnqueued > TimeSpan.Zero); + Assert.True(msg.EnqueuedToSending > TimeSpan.Zero); + Assert.True(msg.SentToResponse > TimeSpan.Zero); + Assert.True(msg.ResponseToCompletion >= TimeSpan.Zero); // this can be immeasurably fast + Assert.True(msg.ElapsedTime > TimeSpan.Zero); + + if (msg.RetransmissionOf != null) + { + // imprecision of DateTime.UtcNow makes this pretty approximate + Assert.True(msg.RetransmissionOf.CommandCreated <= msg.CommandCreated); + Assert.Equal(RetransmissionReasonType.Moved, msg.RetransmissionReason); + } + else { - conn.RegisterProfiler(profiler.GetSession); - - var endpoints = conn.GetEndPoints(); - var servers = endpoints.Select(e => conn.GetServer(e)); - - var db = conn.GetDatabase(); - db.KeyDelete(Key); - db.StringSet(Key, Value); - var config = servers.First().ClusterConfiguration; - Assert.NotNull(config); - - //int slot = conn.HashSlot(Key); - var rightPrimaryNode = config.GetBySlot(Key); - Assert.NotNull(rightPrimaryNode); - - Assert.NotNull(rightPrimaryNode.EndPoint); - string? a = (string?)conn.GetServer(rightPrimaryNode.EndPoint).Execute("GET", Key); - Assert.Equal(Value, a); // right primary - - var wrongPrimaryNode = config.Nodes.FirstOrDefault(x => !x.IsReplica && x.NodeId != rightPrimaryNode.NodeId); - Assert.NotNull(wrongPrimaryNode); - - Assert.NotNull(wrongPrimaryNode.EndPoint); - string? b = (string?)conn.GetServer(wrongPrimaryNode.EndPoint).Execute("GET", Key); - Assert.Equal(Value, b); // wrong primary, allow redirect - - var msgs = profiler.GetSession().FinishProfiling().ToList(); - - // verify that things actually got recorded properly, and the retransmission profilings are connected as expected - { - // expect 1 DEL, 1 SET, 1 GET (to right primary), 1 GET (to wrong primary) that was responded to by an ASK, and 1 GET (to right primary or a replica of it) - Assert.Equal(5, msgs.Count); - Assert.Equal(1, msgs.Count(c => c.Command == "DEL" || c.Command == "UNLINK")); - Assert.Equal(1, msgs.Count(c => c.Command == "SET")); - Assert.Equal(3, msgs.Count(c => c.Command == "GET")); - - var toRightPrimaryNotRetransmission = msgs.Where(m => m.Command == "GET" && m.EndPoint.Equals(rightPrimaryNode.EndPoint) && m.RetransmissionOf == null); - Assert.Single(toRightPrimaryNotRetransmission); - - var toWrongPrimaryWithoutRetransmission = msgs.Where(m => m.Command == "GET" && m.EndPoint.Equals(wrongPrimaryNode.EndPoint) && m.RetransmissionOf == null).ToList(); - Assert.Single(toWrongPrimaryWithoutRetransmission); - - var toRightPrimaryOrReplicaAsRetransmission = msgs.Where(m => m.Command == "GET" && (m.EndPoint.Equals(rightPrimaryNode.EndPoint) || rightPrimaryNode.Children.Any(c => m.EndPoint.Equals(c.EndPoint))) && m.RetransmissionOf != null).ToList(); - Assert.Single(toRightPrimaryOrReplicaAsRetransmission); - - var originalWrongPrimary = toWrongPrimaryWithoutRetransmission.Single(); - var retransmissionToRight = toRightPrimaryOrReplicaAsRetransmission.Single(); - - Assert.True(ReferenceEquals(originalWrongPrimary, retransmissionToRight.RetransmissionOf)); - } - - foreach (var msg in msgs) - { - Assert.True(msg.CommandCreated != default(DateTime)); - Assert.True(msg.CreationToEnqueued > TimeSpan.Zero); - Assert.True(msg.EnqueuedToSending > TimeSpan.Zero); - Assert.True(msg.SentToResponse > TimeSpan.Zero); - Assert.True(msg.ResponseToCompletion >= TimeSpan.Zero); // this can be immeasurably fast - Assert.True(msg.ElapsedTime > TimeSpan.Zero); - - if (msg.RetransmissionOf != null) - { - // imprecision of DateTime.UtcNow makes this pretty approximate - Assert.True(msg.RetransmissionOf.CommandCreated <= msg.CommandCreated); - Assert.Equal(RetransmissionReasonType.Moved, msg.RetransmissionReason); - } - else - { - Assert.False(msg.RetransmissionReason.HasValue); - } - } + Assert.False(msg.RetransmissionReason.HasValue); } } } diff --git a/tests/StackExchange.Redis.Tests/Commands.cs b/tests/StackExchange.Redis.Tests/Commands.cs index 036477d90..c63dbba04 100644 --- a/tests/StackExchange.Redis.Tests/Commands.cs +++ b/tests/StackExchange.Redis.Tests/Commands.cs @@ -1,58 +1,56 @@ -using System; -using System.Net; +using System.Net; using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class Commands { - public class Commands + [Fact] + public void CommandByteLength() { - [Fact] - public void CommandByteLength() - { - Assert.Equal(31, CommandBytes.MaxLength); - } + Assert.Equal(31, CommandBytes.MaxLength); + } - [Fact] - public void CheckCommandContents() + [Fact] + public void CheckCommandContents() + { + for (int len = 0; len <= CommandBytes.MaxLength; len++) { - for (int len = 0; len <= CommandBytes.MaxLength; len++) - { - var s = new string('A', len); - CommandBytes b = s; - Assert.Equal(len, b.Length); + var s = new string('A', len); + CommandBytes b = s; + Assert.Equal(len, b.Length); - var t = b.ToString(); - Assert.Equal(s, t); + var t = b.ToString(); + Assert.Equal(s, t); - CommandBytes b2 = t; - Assert.Equal(b, b2); + CommandBytes b2 = t; + Assert.Equal(b, b2); - Assert.Equal(len == 0, ReferenceEquals(s, t)); - } + Assert.Equal(len == 0, ReferenceEquals(s, t)); } + } - [Fact] - public void Basic() - { - var config = ConfigurationOptions.Parse(".,$PING=p"); - Assert.Single(config.EndPoints); - config.SetDefaultPorts(); - Assert.Contains(new DnsEndPoint(".", 6379), config.EndPoints); - var map = config.CommandMap; - Assert.Equal("$PING=P", map.ToString()); - Assert.Equal(".:6379,$PING=P", config.ToString()); - } + [Fact] + public void Basic() + { + var config = ConfigurationOptions.Parse(".,$PING=p"); + Assert.Single(config.EndPoints); + config.SetDefaultPorts(); + Assert.Contains(new DnsEndPoint(".", 6379), config.EndPoints); + var map = config.CommandMap; + Assert.Equal("$PING=P", map.ToString()); + Assert.Equal(".:6379,$PING=P", config.ToString()); + } - [Theory] - [InlineData("redisql.CREATE_STATEMENT")] - [InlineData("INSERTINTOTABLE1STMT")] - public void CanHandleNonTrivialCommands(string command) - { - var cmd = new CommandBytes(command); - Assert.Equal(command.Length, cmd.Length); - Assert.Equal(command.ToUpperInvariant(), cmd.ToString()); + [Theory] + [InlineData("redisql.CREATE_STATEMENT")] + [InlineData("INSERTINTOTABLE1STMT")] + public void CanHandleNonTrivialCommands(string command) + { + var cmd = new CommandBytes(command); + Assert.Equal(command.Length, cmd.Length); + Assert.Equal(command.ToUpperInvariant(), cmd.ToString()); - Assert.Equal(31, CommandBytes.MaxLength); - } + Assert.Equal(31, CommandBytes.MaxLength); } } diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index 222f29393..5198a0e6c 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -12,617 +12,603 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class Config : TestBase { - public class Config : TestBase - { - public Version DefaultVersion = new (3, 0, 0); - public Version DefaultAzureVersion = new (4, 0, 0); + public Version DefaultVersion = new (3, 0, 0); + public Version DefaultAzureVersion = new (4, 0, 0); - public Config(ITestOutputHelper output) : base(output) { } + public Config(ITestOutputHelper output) : base(output) { } - [Fact] - public void SslProtocols_SingleValue() - { - var options = ConfigurationOptions.Parse("myhost,sslProtocols=Tls11"); - Assert.Equal(SslProtocols.Tls11, options.SslProtocols.GetValueOrDefault()); - } + [Fact] + public void SslProtocols_SingleValue() + { + var options = ConfigurationOptions.Parse("myhost,sslProtocols=Tls11"); + Assert.Equal(SslProtocols.Tls11, options.SslProtocols.GetValueOrDefault()); + } - [Fact] - public void SslProtocols_MultipleValues() - { - var options = ConfigurationOptions.Parse("myhost,sslProtocols=Tls11|Tls12"); - Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, options.SslProtocols.GetValueOrDefault()); - } + [Fact] + public void SslProtocols_MultipleValues() + { + var options = ConfigurationOptions.Parse("myhost,sslProtocols=Tls11|Tls12"); + Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, options.SslProtocols.GetValueOrDefault()); + } - [Theory] - [InlineData("checkCertificateRevocation=false", false)] - [InlineData("checkCertificateRevocation=true", true)] - [InlineData("", true)] - public void ConfigurationOption_CheckCertificateRevocation(string conString, bool expectedValue) - { - var options = ConfigurationOptions.Parse($"host,{conString}"); - Assert.Equal(expectedValue, options.CheckCertificateRevocation); - var toString = options.ToString(); - Assert.Contains(conString, toString, StringComparison.CurrentCultureIgnoreCase); - } + [Theory] + [InlineData("checkCertificateRevocation=false", false)] + [InlineData("checkCertificateRevocation=true", true)] + [InlineData("", true)] + public void ConfigurationOption_CheckCertificateRevocation(string conString, bool expectedValue) + { + var options = ConfigurationOptions.Parse($"host,{conString}"); + Assert.Equal(expectedValue, options.CheckCertificateRevocation); + var toString = options.ToString(); + Assert.Contains(conString, toString, StringComparison.CurrentCultureIgnoreCase); + } - [Fact] - public void SslProtocols_UsingIntegerValue() - { - // The below scenario is for cases where the *targeted* - // .NET framework version (e.g. .NET 4.0) doesn't define an enum value (e.g. Tls11) - // but the OS has been patched with support - const int integerValue = (int)(SslProtocols.Tls11 | SslProtocols.Tls12); - var options = ConfigurationOptions.Parse("myhost,sslProtocols=" + integerValue); - Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, options.SslProtocols.GetValueOrDefault()); - } + [Fact] + public void SslProtocols_UsingIntegerValue() + { + // The below scenario is for cases where the *targeted* + // .NET framework version (e.g. .NET 4.0) doesn't define an enum value (e.g. Tls11) + // but the OS has been patched with support + const int integerValue = (int)(SslProtocols.Tls11 | SslProtocols.Tls12); + var options = ConfigurationOptions.Parse("myhost,sslProtocols=" + integerValue); + Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, options.SslProtocols.GetValueOrDefault()); + } - [Fact] - public void SslProtocols_InvalidValue() - { - Assert.Throws(() => ConfigurationOptions.Parse("myhost,sslProtocols=InvalidSslProtocol")); - } + [Fact] + public void SslProtocols_InvalidValue() + { + Assert.Throws(() => ConfigurationOptions.Parse("myhost,sslProtocols=InvalidSslProtocol")); + } - [Fact] - public void ConfigurationOptionsDefaultForAzure() - { - var options = ConfigurationOptions.Parse("contoso.redis.cache.windows.net"); - Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); - Assert.False(options.AbortOnConnectFail); - } + [Fact] + public void ConfigurationOptionsDefaultForAzure() + { + var options = ConfigurationOptions.Parse("contoso.redis.cache.windows.net"); + Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); + Assert.False(options.AbortOnConnectFail); + } - [Fact] - public void ConfigurationOptionsForAzureWhenSpecified() - { - var options = ConfigurationOptions.Parse("contoso.redis.cache.windows.net,abortConnect=true, version=2.1.1"); - Assert.True(options.DefaultVersion.Equals(new Version(2, 1, 1))); - Assert.True(options.AbortOnConnectFail); - } + [Fact] + public void ConfigurationOptionsForAzureWhenSpecified() + { + var options = ConfigurationOptions.Parse("contoso.redis.cache.windows.net,abortConnect=true, version=2.1.1"); + Assert.True(options.DefaultVersion.Equals(new Version(2, 1, 1))); + Assert.True(options.AbortOnConnectFail); + } - [Fact] - public void ConfigurationOptionsDefaultForAzureChina() - { - // added a few upper case chars to validate comparison - var options = ConfigurationOptions.Parse("contoso.REDIS.CACHE.chinacloudapi.cn"); - Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); - Assert.False(options.AbortOnConnectFail); - } + [Fact] + public void ConfigurationOptionsDefaultForAzureChina() + { + // added a few upper case chars to validate comparison + var options = ConfigurationOptions.Parse("contoso.REDIS.CACHE.chinacloudapi.cn"); + Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); + Assert.False(options.AbortOnConnectFail); + } - [Fact] - public void ConfigurationOptionsDefaultForAzureGermany() - { - var options = ConfigurationOptions.Parse("contoso.redis.cache.cloudapi.de"); - Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); - Assert.False(options.AbortOnConnectFail); - } + [Fact] + public void ConfigurationOptionsDefaultForAzureGermany() + { + var options = ConfigurationOptions.Parse("contoso.redis.cache.cloudapi.de"); + Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); + Assert.False(options.AbortOnConnectFail); + } - [Fact] - public void ConfigurationOptionsDefaultForAzureUSGov() - { - var options = ConfigurationOptions.Parse("contoso.redis.cache.usgovcloudapi.net"); - Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); - Assert.False(options.AbortOnConnectFail); - } + [Fact] + public void ConfigurationOptionsDefaultForAzureUSGov() + { + var options = ConfigurationOptions.Parse("contoso.redis.cache.usgovcloudapi.net"); + Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); + Assert.False(options.AbortOnConnectFail); + } - [Fact] - public void ConfigurationOptionsDefaultForNonAzure() - { - var options = ConfigurationOptions.Parse("redis.contoso.com"); - Assert.True(options.DefaultVersion.Equals(DefaultVersion)); - Assert.True(options.AbortOnConnectFail); - } + [Fact] + public void ConfigurationOptionsDefaultForNonAzure() + { + var options = ConfigurationOptions.Parse("redis.contoso.com"); + Assert.True(options.DefaultVersion.Equals(DefaultVersion)); + Assert.True(options.AbortOnConnectFail); + } - [Fact] - public void ConfigurationOptionsDefaultWhenNoEndpointsSpecifiedYet() - { - var options = new ConfigurationOptions(); - Assert.True(options.DefaultVersion.Equals(DefaultVersion)); - Assert.True(options.AbortOnConnectFail); - } + [Fact] + public void ConfigurationOptionsDefaultWhenNoEndpointsSpecifiedYet() + { + var options = new ConfigurationOptions(); + Assert.True(options.DefaultVersion.Equals(DefaultVersion)); + Assert.True(options.AbortOnConnectFail); + } - [Fact] - public void ConfigurationOptionsSyncTimeout() - { - // Default check - var options = new ConfigurationOptions(); - Assert.Equal(5000, options.SyncTimeout); + [Fact] + public void ConfigurationOptionsSyncTimeout() + { + // Default check + var options = new ConfigurationOptions(); + Assert.Equal(5000, options.SyncTimeout); - options = ConfigurationOptions.Parse("syncTimeout=20"); - Assert.Equal(20, options.SyncTimeout); - } + options = ConfigurationOptions.Parse("syncTimeout=20"); + Assert.Equal(20, options.SyncTimeout); + } - [Theory] - [InlineData("127.1:6379", AddressFamily.InterNetwork, "127.0.0.1", 6379)] - [InlineData("127.0.0.1:6379", AddressFamily.InterNetwork, "127.0.0.1", 6379)] - [InlineData("2a01:9820:1:24::1:1:6379", AddressFamily.InterNetworkV6, "2a01:9820:1:24:0:1:1:6379", 0)] - [InlineData("[2a01:9820:1:24::1:1]:6379", AddressFamily.InterNetworkV6, "2a01:9820:1:24::1:1", 6379)] - public void ConfigurationOptionsIPv6Parsing(string configString, AddressFamily family, string address, int port) - { - var options = ConfigurationOptions.Parse(configString); - Assert.Single(options.EndPoints); - var ep = Assert.IsType(options.EndPoints[0]); - Assert.Equal(family, ep.AddressFamily); - Assert.Equal(address, ep.Address.ToString()); - Assert.Equal(port, ep.Port); - } + [Theory] + [InlineData("127.1:6379", AddressFamily.InterNetwork, "127.0.0.1", 6379)] + [InlineData("127.0.0.1:6379", AddressFamily.InterNetwork, "127.0.0.1", 6379)] + [InlineData("2a01:9820:1:24::1:1:6379", AddressFamily.InterNetworkV6, "2a01:9820:1:24:0:1:1:6379", 0)] + [InlineData("[2a01:9820:1:24::1:1]:6379", AddressFamily.InterNetworkV6, "2a01:9820:1:24::1:1", 6379)] + public void ConfigurationOptionsIPv6Parsing(string configString, AddressFamily family, string address, int port) + { + var options = ConfigurationOptions.Parse(configString); + Assert.Single(options.EndPoints); + var ep = Assert.IsType(options.EndPoints[0]); + Assert.Equal(family, ep.AddressFamily); + Assert.Equal(address, ep.Address.ToString()); + Assert.Equal(port, ep.Port); + } - [Fact] - public void CanParseAndFormatUnixDomainSocket() - { - const string ConfigString = "!/some/path,allowAdmin=True"; + [Fact] + public void CanParseAndFormatUnixDomainSocket() + { + const string ConfigString = "!/some/path,allowAdmin=True"; #if NET472 - var ex = Assert.Throws(() => ConfigurationOptions.Parse(ConfigString)); - Assert.Equal("Unix domain sockets require .NET Core 3 or above", ex.Message); + var ex = Assert.Throws(() => ConfigurationOptions.Parse(ConfigString)); + Assert.Equal("Unix domain sockets require .NET Core 3 or above", ex.Message); #else - var config = ConfigurationOptions.Parse(ConfigString); - Assert.True(config.AllowAdmin); - var ep = Assert.IsType(Assert.Single(config.EndPoints)); - Assert.Equal("/some/path", ep.ToString()); - Assert.Equal(ConfigString, config.ToString()); + var config = ConfigurationOptions.Parse(ConfigString); + Assert.True(config.AllowAdmin); + var ep = Assert.IsType(Assert.Single(config.EndPoints)); + Assert.Equal("/some/path", ep.ToString()); + Assert.Equal(ConfigString, config.ToString()); #endif - } + } - [Fact] - public void TalkToNonsenseServer() + [Fact] + public void TalkToNonsenseServer() + { + var config = new ConfigurationOptions { - var config = new ConfigurationOptions + AbortOnConnectFail = false, + EndPoints = { - AbortOnConnectFail = false, - EndPoints = - { - { "127.0.0.1:1234" } - }, - ConnectTimeout = 200 - }; - var log = new StringWriter(); - using (var conn = ConnectionMultiplexer.Connect(config, log)) - { - Log(log.ToString()); - Assert.False(conn.IsConnected); - } + { "127.0.0.1:1234" } + }, + ConnectTimeout = 200 + }; + var log = new StringWriter(); + using (var conn = ConnectionMultiplexer.Connect(config, log)) + { + Log(log.ToString()); + Assert.False(conn.IsConnected); } + } - [Fact] - public async Task TestManaulHeartbeat() - { - using (var muxer = Create(keepAlive: 2)) - { - var conn = muxer.GetDatabase(); - conn.Ping(); + [Fact] + public async Task TestManaulHeartbeat() + { + using var conn = Create(keepAlive: 2); - var before = muxer.OperationCount; + var db = conn.GetDatabase(); + db.Ping(); - Log("sleeping to test heartbeat..."); - await Task.Delay(5000).ForAwait(); + var before = conn.OperationCount; - var after = muxer.OperationCount; + Log("sleeping to test heartbeat..."); + await Task.Delay(5000).ForAwait(); - Assert.True(after >= before + 2, $"after: {after}, before: {before}"); - } - } + var after = conn.OperationCount; - [Theory] - [InlineData(0)] - [InlineData(10)] - [InlineData(100)] - [InlineData(200)] - public void GetSlowlog(int count) - { - using (var muxer = Create(allowAdmin: true)) - { - var rows = GetAnyPrimary(muxer).SlowlogGet(count); - Assert.NotNull(rows); - } - } + Assert.True(after >= before + 2, $"after: {after}, before: {before}"); + } - [Fact] - public void ClearSlowlog() - { - using (var muxer = Create(allowAdmin: true)) - { - GetAnyPrimary(muxer).SlowlogReset(); - } - } + [Theory] + [InlineData(0)] + [InlineData(10)] + [InlineData(100)] + [InlineData(200)] + public void GetSlowlog(int count) + { + using var conn = Create(allowAdmin: true); - [Fact] - public void ClientName() - { - using (var muxer = Create(clientName: "Test Rig", allowAdmin: true)) - { - Assert.Equal("Test Rig", muxer.ClientName); + var rows = GetAnyPrimary(conn).SlowlogGet(count); + Assert.NotNull(rows); + } - var conn = muxer.GetDatabase(); - conn.Ping(); + [Fact] + public void ClearSlowlog() + { + using var conn = Create(allowAdmin: true); - var name = (string?)GetAnyPrimary(muxer).Execute("CLIENT", "GETNAME"); - Assert.Equal("TestRig", name); - } - } + GetAnyPrimary(conn).SlowlogReset(); + } - [Fact] - public void DefaultClientName() - { - using (var muxer = Create(allowAdmin: true, caller: null)) // force default naming to kick in - { - Assert.Equal($"{Environment.MachineName}(SE.Redis-v{Utils.GetLibVersion()})", muxer.ClientName); - var conn = muxer.GetDatabase(); - conn.Ping(); + [Fact] + public void ClientName() + { + using var conn = Create(clientName: "Test Rig", allowAdmin: true); - var name = (string?)GetAnyPrimary(muxer).Execute("CLIENT", "GETNAME"); - Assert.Equal($"{Environment.MachineName}(SE.Redis-v{Utils.GetLibVersion()})", name); - } - } + Assert.Equal("Test Rig", conn.ClientName); - [Fact] - public void ReadConfigWithConfigDisabled() - { - using (var muxer = Create(allowAdmin: true, disabledCommands: new[] { "config", "info" })) - { - var conn = GetAnyPrimary(muxer); - var ex = Assert.Throws(() => conn.ConfigGet()); - Assert.Equal("This operation has been disabled in the command-map and cannot be used: CONFIG", ex.Message); - } - } + var db = conn.GetDatabase(); + db.Ping(); - [Fact] - public void ConnectWithSubscribeDisabled() - { - using (var muxer = Create(allowAdmin: true, disabledCommands: new[] { "subscribe" })) - { - Assert.True(muxer.IsConnected); - var servers = muxer.GetServerSnapshot(); - Assert.True(servers[0].IsConnected); - Assert.False(servers[0].IsSubscriberConnected); + var name = (string?)GetAnyPrimary(conn).Execute("CLIENT", "GETNAME"); + Assert.Equal("TestRig", name); + } - var ex = Assert.Throws(() => muxer.GetSubscriber().Subscribe(Me(), (_, _) => GC.KeepAlive(this))); - Assert.Equal("This operation has been disabled in the command-map and cannot be used: SUBSCRIBE", ex.Message); - } - } + [Fact] + public void DefaultClientName() + { + using var conn = Create(allowAdmin: true, caller: null); // force default naming to kick in - [Fact] - public void ReadConfig() - { - using (var muxer = Create(allowAdmin: true)) - { - Log("about to get config"); - var conn = GetAnyPrimary(muxer); - var all = conn.ConfigGet(); - Assert.True(all.Length > 0, "any"); + Assert.Equal($"{Environment.MachineName}(SE.Redis-v{Utils.GetLibVersion()})", conn.ClientName); + var db = conn.GetDatabase(); + db.Ping(); + + var name = (string?)GetAnyPrimary(conn).Execute("CLIENT", "GETNAME"); + Assert.Equal($"{Environment.MachineName}(SE.Redis-v{Utils.GetLibVersion()})", name); + } - var pairs = all.ToDictionary(x => (string)x.Key, x => (string)x.Value, StringComparer.InvariantCultureIgnoreCase); + [Fact] + public void ReadConfigWithConfigDisabled() + { + using var conn = Create(allowAdmin: true, disabledCommands: new[] { "config", "info" }); - Assert.Equal(all.Length, pairs.Count); - Assert.True(pairs.ContainsKey("timeout"), "timeout"); - var val = int.Parse(pairs["timeout"]); + var server = GetAnyPrimary(conn); + var ex = Assert.Throws(() => server.ConfigGet()); + Assert.Equal("This operation has been disabled in the command-map and cannot be used: CONFIG", ex.Message); + } - Assert.True(pairs.ContainsKey("port"), "port"); - val = int.Parse(pairs["port"]); - Assert.Equal(TestConfig.Current.PrimaryPort, val); - } - } + [Fact] + public void ConnectWithSubscribeDisabled() + { + using var conn = Create(allowAdmin: true, disabledCommands: new[] { "subscribe" }); - [Fact] - public void GetTime() - { - using (var muxer = Create()) - { - var server = GetAnyPrimary(muxer); - var serverTime = server.Time(); - var localTime = DateTime.UtcNow; - Log("Server: " + serverTime.ToString(CultureInfo.InvariantCulture)); - Log("Local: " + localTime.ToString(CultureInfo.InvariantCulture)); - Assert.Equal(localTime, serverTime, TimeSpan.FromSeconds(10)); - } - } + Assert.True(conn.IsConnected); + var servers = conn.GetServerSnapshot(); + Assert.True(servers[0].IsConnected); + Assert.False(servers[0].IsSubscriberConnected); - [Fact] - public void DebugObject() - { - using (var muxer = Create(allowAdmin: true)) - { - var db = muxer.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringIncrement(key, flags: CommandFlags.FireAndForget); - var debug = (string?)db.DebugObject(key); - Assert.NotNull(debug); - Assert.Contains("encoding:int serializedlength:2", debug); - } - } + var ex = Assert.Throws(() => conn.GetSubscriber().Subscribe(Me(), (_, _) => GC.KeepAlive(this))); + Assert.Equal("This operation has been disabled in the command-map and cannot be used: SUBSCRIBE", ex.Message); + } - [Fact] - public void GetInfo() - { - using (var muxer = Create(allowAdmin: true)) - { - var server = GetAnyPrimary(muxer); - var info1 = server.Info(); - Assert.True(info1.Length > 5); - Log("All sections"); - foreach (var group in info1) - { - Log(group.Key); - } - var first = info1[0]; - Log("Full info for: " + first.Key); - foreach (var setting in first) - { - Log("{0} ==> {1}", setting.Key, setting.Value); - } - - var info2 = server.Info("cpu"); - Assert.Single(info2); - var cpu = info2.Single(); - var cpuCount = cpu.Count(); - Assert.True(cpuCount > 2); - Assert.Equal("CPU", cpu.Key); - Assert.Contains(cpu, x => x.Key == "used_cpu_sys"); - Assert.Contains(cpu, x => x.Key == "used_cpu_user"); - } - } + [Fact] + public void ReadConfig() + { + using var conn = Create(allowAdmin: true); + + Log("about to get config"); + var server = GetAnyPrimary(conn); + var all = server.ConfigGet(); + Assert.True(all.Length > 0, "any"); + + var pairs = all.ToDictionary(x => (string)x.Key, x => (string)x.Value, StringComparer.InvariantCultureIgnoreCase); + + Assert.Equal(all.Length, pairs.Count); + Assert.True(pairs.ContainsKey("timeout"), "timeout"); + var val = int.Parse(pairs["timeout"]); + + Assert.True(pairs.ContainsKey("port"), "port"); + val = int.Parse(pairs["port"]); + Assert.Equal(TestConfig.Current.PrimaryPort, val); + } + + [Fact] + public void GetTime() + { + using var conn = Create(); + + var server = GetAnyPrimary(conn); + var serverTime = server.Time(); + var localTime = DateTime.UtcNow; + Log("Server: " + serverTime.ToString(CultureInfo.InvariantCulture)); + Log("Local: " + localTime.ToString(CultureInfo.InvariantCulture)); + Assert.Equal(localTime, serverTime, TimeSpan.FromSeconds(10)); + } + + [Fact] + public void DebugObject() + { + using var conn = Create(allowAdmin: true); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringIncrement(key, flags: CommandFlags.FireAndForget); + var debug = (string?)db.DebugObject(key); + Assert.NotNull(debug); + Assert.Contains("encoding:int serializedlength:2", debug); + } - [Fact] - public void GetInfoRaw() + [Fact] + public void GetInfo() + { + using var conn = Create(allowAdmin: true); + + var server = GetAnyPrimary(conn); + var info1 = server.Info(); + Assert.True(info1.Length > 5); + Log("All sections"); + foreach (var group in info1) { - using (var muxer = Create(allowAdmin: true)) - { - var server = GetAnyPrimary(muxer); - var info = server.InfoRaw(); - Assert.Contains("used_cpu_sys", info); - Assert.Contains("used_cpu_user", info); - } + Log(group.Key); } - - [Fact] - public void GetClients() + var first = info1[0]; + Log("Full info for: " + first.Key); + foreach (var setting in first) { - var name = Guid.NewGuid().ToString(); - using (var muxer = Create(clientName: name, allowAdmin: true)) - { - var server = GetAnyPrimary(muxer); - var clients = server.ClientList(); - Assert.True(clients.Length > 0, "no clients"); // ourselves! - Assert.True(clients.Any(x => x.Name == name), "expected: " + name); - } + Log("{0} ==> {1}", setting.Key, setting.Value); } - [Fact] - public void SlowLog() + var info2 = server.Info("cpu"); + Assert.Single(info2); + var cpu = info2.Single(); + var cpuCount = cpu.Count(); + Assert.True(cpuCount > 2); + Assert.Equal("CPU", cpu.Key); + Assert.Contains(cpu, x => x.Key == "used_cpu_sys"); + Assert.Contains(cpu, x => x.Key == "used_cpu_user"); + } + + [Fact] + public void GetInfoRaw() + { + using var conn = Create(allowAdmin: true); + + var server = GetAnyPrimary(conn); + var info = server.InfoRaw(); + Assert.Contains("used_cpu_sys", info); + Assert.Contains("used_cpu_user", info); + } + + [Fact] + public void GetClients() + { + var name = Guid.NewGuid().ToString(); + using var conn = Create(clientName: name, allowAdmin: true); + + var server = GetAnyPrimary(conn); + var clients = server.ClientList(); + Assert.True(clients.Length > 0, "no clients"); // ourselves! + Assert.True(clients.Any(x => x.Name == name), "expected: " + name); + } + + [Fact] + public void SlowLog() + { + using var conn = Create(allowAdmin: true); + + var server = GetAnyPrimary(conn); + server.SlowlogGet(); + server.SlowlogReset(); + } + + [Fact] + public async Task TestAutomaticHeartbeat() + { + RedisValue oldTimeout = RedisValue.Null; + using var configConn = Create(allowAdmin: true); + + try { - using (var muxer = Create(allowAdmin: true)) - { - var server = GetAnyPrimary(muxer); - server.SlowlogGet(); - server.SlowlogReset(); - } - } + configConn.GetDatabase(); + var srv = GetAnyPrimary(configConn); + oldTimeout = srv.ConfigGet("timeout")[0].Value; + srv.ConfigSet("timeout", 5); + + using var innerConn = Create(); + var innerDb = innerConn.GetDatabase(); + innerDb.Ping(); // need to wait to pick up configuration etc - [Fact] - public async Task TestAutomaticHeartbeat() + var before = innerConn.OperationCount; + + Log("sleeping to test heartbeat..."); + await Task.Delay(8000).ForAwait(); + + var after = innerConn.OperationCount; + Assert.True(after >= before + 2, $"after: {after}, before: {before}"); + } + finally { - RedisValue oldTimeout = RedisValue.Null; - using (var configMuxer = Create(allowAdmin: true)) + if (!oldTimeout.IsNull) { - try - { - configMuxer.GetDatabase(); - var srv = GetAnyPrimary(configMuxer); - oldTimeout = srv.ConfigGet("timeout")[0].Value; - srv.ConfigSet("timeout", 5); - - using (var innerMuxer = Create()) - { - var innerConn = innerMuxer.GetDatabase(); - innerConn.Ping(); // need to wait to pick up configuration etc - - var before = innerMuxer.OperationCount; - - Log("sleeping to test heartbeat..."); - await Task.Delay(8000).ForAwait(); - - var after = innerMuxer.OperationCount; - Assert.True(after >= before + 2, $"after: {after}, before: {before}"); - } - } - finally - { - if (!oldTimeout.IsNull) - { - var srv = GetAnyPrimary(configMuxer); - srv.ConfigSet("timeout", oldTimeout); - } - } + var srv = GetAnyPrimary(configConn); + srv.ConfigSet("timeout", oldTimeout); } } + } - [Fact] - public void EndpointIteratorIsReliableOverChanges() - { - var eps = new EndPointCollection - { - { IPAddress.Loopback, 7999 }, - { IPAddress.Loopback, 8000 }, - }; - - using var iter = eps.GetEnumerator(); - Assert.True(iter.MoveNext()); - Assert.Equal(7999, ((IPEndPoint)iter.Current).Port); - eps[1] = new IPEndPoint(IPAddress.Loopback, 8001); // boom - Assert.True(iter.MoveNext()); - Assert.Equal(8001, ((IPEndPoint)iter.Current).Port); - Assert.False(iter.MoveNext()); - } + [Fact] + public void EndpointIteratorIsReliableOverChanges() + { + var eps = new EndPointCollection + { + { IPAddress.Loopback, 7999 }, + { IPAddress.Loopback, 8000 }, + }; + + using var iter = eps.GetEnumerator(); + Assert.True(iter.MoveNext()); + Assert.Equal(7999, ((IPEndPoint)iter.Current).Port); + eps[1] = new IPEndPoint(IPAddress.Loopback, 8001); // boom + Assert.True(iter.MoveNext()); + Assert.Equal(8001, ((IPEndPoint)iter.Current).Port); + Assert.False(iter.MoveNext()); + } - [Fact] - public void ThreadPoolManagerIsDetected() + [Fact] + public void ThreadPoolManagerIsDetected() + { + var config = new ConfigurationOptions { - var config = new ConfigurationOptions - { - EndPoints = { { IPAddress.Loopback, 6379 } }, - SocketManager = SocketManager.ThreadPool - }; - using var muxer = ConnectionMultiplexer.Connect(config); - Assert.Same(PipeScheduler.ThreadPool, muxer.SocketManager?.Scheduler); - } + EndPoints = { { IPAddress.Loopback, 6379 } }, + SocketManager = SocketManager.ThreadPool + }; - [Fact] - public void DefaultThreadPoolManagerIsDetected() + using var conn = ConnectionMultiplexer.Connect(config); + + Assert.Same(PipeScheduler.ThreadPool, conn.SocketManager?.Scheduler); + } + + [Fact] + public void DefaultThreadPoolManagerIsDetected() + { + var config = new ConfigurationOptions { - var config = new ConfigurationOptions - { - EndPoints = { { IPAddress.Loopback, 6379 } }, - }; - using var muxer = ConnectionMultiplexer.Connect(config); - Assert.Same(ConnectionMultiplexer.GetDefaultSocketManager().Scheduler, muxer.SocketManager?.Scheduler); - } + EndPoints = { { IPAddress.Loopback, 6379 } }, + }; + + using var conn = ConnectionMultiplexer.Connect(config); + + Assert.Same(ConnectionMultiplexer.GetDefaultSocketManager().Scheduler, conn.SocketManager?.Scheduler); + } - [Theory] - [InlineData("myDNS:myPort,password=myPassword,connectRetry=3,connectTimeout=15000,syncTimeout=15000,defaultDatabase=0,abortConnect=false,ssl=true,sslProtocols=Tls12", SslProtocols.Tls12)] - [InlineData("myDNS:myPort,password=myPassword,abortConnect=false,ssl=true,sslProtocols=Tls12", SslProtocols.Tls12)] + [Theory] + [InlineData("myDNS:myPort,password=myPassword,connectRetry=3,connectTimeout=15000,syncTimeout=15000,defaultDatabase=0,abortConnect=false,ssl=true,sslProtocols=Tls12", SslProtocols.Tls12)] + [InlineData("myDNS:myPort,password=myPassword,abortConnect=false,ssl=true,sslProtocols=Tls12", SslProtocols.Tls12)] #pragma warning disable CS0618 // Type or member is obsolete - [InlineData("myDNS:myPort,password=myPassword,abortConnect=false,ssl=true,sslProtocols=Ssl3", SslProtocols.Ssl3)] + [InlineData("myDNS:myPort,password=myPassword,abortConnect=false,ssl=true,sslProtocols=Ssl3", SslProtocols.Ssl3)] #pragma warning restore CS0618 - [InlineData("myDNS:myPort,password=myPassword,abortConnect=false,ssl=true,sslProtocols=Tls12 ", SslProtocols.Tls12)] - public void ParseTlsWithoutTrailingComma(string configString, SslProtocols expected) - { - var config = ConfigurationOptions.Parse(configString); - Assert.Equal(expected, config.SslProtocols); - } + [InlineData("myDNS:myPort,password=myPassword,abortConnect=false,ssl=true,sslProtocols=Tls12 ", SslProtocols.Tls12)] + public void ParseTlsWithoutTrailingComma(string configString, SslProtocols expected) + { + var config = ConfigurationOptions.Parse(configString); + Assert.Equal(expected, config.SslProtocols); + } - [Theory] - [InlineData("foo,sslProtocols=NotAThing", "Keyword 'sslProtocols' requires an SslProtocol value (multiple values separated by '|'); the value 'NotAThing' is not recognised.", "sslProtocols")] - [InlineData("foo,SyncTimeout=ten", "Keyword 'SyncTimeout' requires an integer value; the value 'ten' is not recognised.", "SyncTimeout")] - [InlineData("foo,syncTimeout=-42", "Keyword 'syncTimeout' has a minimum value of '1'; the value '-42' is not permitted.", "syncTimeout")] - [InlineData("foo,AllowAdmin=maybe", "Keyword 'AllowAdmin' requires a boolean value; the value 'maybe' is not recognised.", "AllowAdmin")] - [InlineData("foo,Version=current", "Keyword 'Version' requires a version value; the value 'current' is not recognised.", "Version")] - [InlineData("foo,proxy=epoxy", "Keyword 'proxy' requires a proxy value; the value 'epoxy' is not recognised.", "proxy")] - public void ConfigStringErrorsGiveMeaningfulMessages(string configString, string expected, string paramName) - { - var ex = Assert.Throws(() => ConfigurationOptions.Parse(configString)); - Assert.StartsWith(expected, ex.Message); // param name gets concatenated sometimes - Assert.Equal(paramName, ex.ParamName); // param name gets concatenated sometimes - } + [Theory] + [InlineData("foo,sslProtocols=NotAThing", "Keyword 'sslProtocols' requires an SslProtocol value (multiple values separated by '|'); the value 'NotAThing' is not recognised.", "sslProtocols")] + [InlineData("foo,SyncTimeout=ten", "Keyword 'SyncTimeout' requires an integer value; the value 'ten' is not recognised.", "SyncTimeout")] + [InlineData("foo,syncTimeout=-42", "Keyword 'syncTimeout' has a minimum value of '1'; the value '-42' is not permitted.", "syncTimeout")] + [InlineData("foo,AllowAdmin=maybe", "Keyword 'AllowAdmin' requires a boolean value; the value 'maybe' is not recognised.", "AllowAdmin")] + [InlineData("foo,Version=current", "Keyword 'Version' requires a version value; the value 'current' is not recognised.", "Version")] + [InlineData("foo,proxy=epoxy", "Keyword 'proxy' requires a proxy value; the value 'epoxy' is not recognised.", "proxy")] + public void ConfigStringErrorsGiveMeaningfulMessages(string configString, string expected, string paramName) + { + var ex = Assert.Throws(() => ConfigurationOptions.Parse(configString)); + Assert.StartsWith(expected, ex.Message); // param name gets concatenated sometimes + Assert.Equal(paramName, ex.ParamName); // param name gets concatenated sometimes + } - [Fact] - public void ConfigStringInvalidOptionErrorGiveMeaningfulMessages() - { - var ex = Assert.Throws(() => ConfigurationOptions.Parse("foo,flibble=value")); - Assert.StartsWith("Keyword 'flibble' is not supported.", ex.Message); // param name gets concatenated sometimes - Assert.Equal("flibble", ex.ParamName); - } + [Fact] + public void ConfigStringInvalidOptionErrorGiveMeaningfulMessages() + { + var ex = Assert.Throws(() => ConfigurationOptions.Parse("foo,flibble=value")); + Assert.StartsWith("Keyword 'flibble' is not supported.", ex.Message); // param name gets concatenated sometimes + Assert.Equal("flibble", ex.ParamName); + } - [Fact] - public void NullApply() - { - var options = ConfigurationOptions.Parse("127.0.0.1,name=FooApply"); - Assert.Equal("FooApply", options.ClientName); + [Fact] + public void NullApply() + { + var options = ConfigurationOptions.Parse("127.0.0.1,name=FooApply"); + Assert.Equal("FooApply", options.ClientName); - // Doesn't go boom - var result = options.Apply(null!); - Assert.Equal("FooApply", options.ClientName); - Assert.Equal(result, options); - } + // Doesn't go boom + var result = options.Apply(null!); + Assert.Equal("FooApply", options.ClientName); + Assert.Equal(result, options); + } - [Fact] - public void Apply() - { - var options = ConfigurationOptions.Parse("127.0.0.1,name=FooApply"); - Assert.Equal("FooApply", options.ClientName); + [Fact] + public void Apply() + { + var options = ConfigurationOptions.Parse("127.0.0.1,name=FooApply"); + Assert.Equal("FooApply", options.ClientName); - var randomName = Guid.NewGuid().ToString(); - var result = options.Apply(options => options.ClientName = randomName); + var randomName = Guid.NewGuid().ToString(); + var result = options.Apply(options => options.ClientName = randomName); - Assert.Equal(randomName, options.ClientName); - Assert.Equal(randomName, result.ClientName); - Assert.Equal(result, options); - } + Assert.Equal(randomName, options.ClientName); + Assert.Equal(randomName, result.ClientName); + Assert.Equal(result, options); + } - [Fact] - public void BeforeSocketConnect() - { - var options = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort); - int count = 0; - options.BeforeSocketConnect = (endpoint, connType, socket) => - { - Interlocked.Increment(ref count); - Log($"Endpoint: {endpoint}, ConnType: {connType}, Socket: {socket}"); - socket.DontFragment = true; - socket.Ttl = (short)(connType == ConnectionType.Interactive ? 12 : 123); - }; - var muxer = ConnectionMultiplexer.Connect(options); - Assert.True(muxer.IsConnected); - Assert.Equal(2, count); - - var endpoint = muxer.GetServerSnapshot()[0]; - var interactivePhysical = endpoint.GetBridge(ConnectionType.Interactive)?.TryConnect(null); - var subscriptionPhysical = endpoint.GetBridge(ConnectionType.Subscription)?.TryConnect(null); - Assert.NotNull(interactivePhysical); - Assert.NotNull(subscriptionPhysical); - - var interactiveSocket = interactivePhysical.VolatileSocket; - var subscriptionSocket = subscriptionPhysical.VolatileSocket; - Assert.NotNull(interactiveSocket); - Assert.NotNull(subscriptionSocket); - - Assert.Equal(12, interactiveSocket.Ttl); - Assert.Equal(123, subscriptionSocket.Ttl); - Assert.True(interactiveSocket.DontFragment); - Assert.True(subscriptionSocket.DontFragment); - } + [Fact] + public void BeforeSocketConnect() + { + var options = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort); + int count = 0; + options.BeforeSocketConnect = (endpoint, connType, socket) => + { + Interlocked.Increment(ref count); + Log($"Endpoint: {endpoint}, ConnType: {connType}, Socket: {socket}"); + socket.DontFragment = true; + socket.Ttl = (short)(connType == ConnectionType.Interactive ? 12 : 123); + }; + using var conn = ConnectionMultiplexer.Connect(options); + Assert.True(conn.IsConnected); + Assert.Equal(2, count); + + var endpoint = conn.GetServerSnapshot()[0]; + var interactivePhysical = endpoint.GetBridge(ConnectionType.Interactive)?.TryConnect(null); + var subscriptionPhysical = endpoint.GetBridge(ConnectionType.Subscription)?.TryConnect(null); + Assert.NotNull(interactivePhysical); + Assert.NotNull(subscriptionPhysical); + + var interactiveSocket = interactivePhysical.VolatileSocket; + var subscriptionSocket = subscriptionPhysical.VolatileSocket; + Assert.NotNull(interactiveSocket); + Assert.NotNull(subscriptionSocket); + + Assert.Equal(12, interactiveSocket.Ttl); + Assert.Equal(123, subscriptionSocket.Ttl); + Assert.True(interactiveSocket.DontFragment); + Assert.True(subscriptionSocket.DontFragment); + } - [Fact] - public async Task MutableOptions() - { - var options = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort + ",name=Details"); - var originalConfigChannel = options.ConfigurationChannel = "originalConfig"; - var originalUser = options.User = "originalUser"; - var originalPassword = options.Password = "originalPassword"; - Assert.Equal("Details", options.ClientName); - using var muxer = await ConnectionMultiplexer.ConnectAsync(options); - - // Same instance - Assert.Same(options, muxer.RawConfig); - // Copies - Assert.NotSame(options.EndPoints, muxer.EndPoints); - - // Same until forked - it's not cloned - Assert.Same(options.CommandMap, muxer.CommandMap); - options.CommandMap = CommandMap.Envoyproxy; - Assert.NotSame(options.CommandMap, muxer.CommandMap); + [Fact] + public async Task MutableOptions() + { + var options = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort + ",name=Details"); + var originalConfigChannel = options.ConfigurationChannel = "originalConfig"; + var originalUser = options.User = "originalUser"; + var originalPassword = options.Password = "originalPassword"; + Assert.Equal("Details", options.ClientName); + using var conn = await ConnectionMultiplexer.ConnectAsync(options); + + // Same instance + Assert.Same(options, conn.RawConfig); + // Copies + Assert.NotSame(options.EndPoints, conn.EndPoints); + + // Same until forked - it's not cloned + Assert.Same(options.CommandMap, conn.CommandMap); + options.CommandMap = CommandMap.Envoyproxy; + Assert.NotSame(options.CommandMap, conn.CommandMap); #pragma warning disable CS0618 // Type or member is obsolete - // Defaults true - Assert.True(options.IncludeDetailInExceptions); - Assert.True(muxer.IncludeDetailInExceptions); - options.IncludeDetailInExceptions = false; - Assert.False(options.IncludeDetailInExceptions); - Assert.False(muxer.IncludeDetailInExceptions); - - // Defaults false - Assert.False(options.IncludePerformanceCountersInExceptions); - Assert.False(muxer.IncludePerformanceCountersInExceptions); - options.IncludePerformanceCountersInExceptions = true; - Assert.True(options.IncludePerformanceCountersInExceptions); - Assert.True(muxer.IncludePerformanceCountersInExceptions); + // Defaults true + Assert.True(options.IncludeDetailInExceptions); + Assert.True(conn.IncludeDetailInExceptions); + options.IncludeDetailInExceptions = false; + Assert.False(options.IncludeDetailInExceptions); + Assert.False(conn.IncludeDetailInExceptions); + + // Defaults false + Assert.False(options.IncludePerformanceCountersInExceptions); + Assert.False(conn.IncludePerformanceCountersInExceptions); + options.IncludePerformanceCountersInExceptions = true; + Assert.True(options.IncludePerformanceCountersInExceptions); + Assert.True(conn.IncludePerformanceCountersInExceptions); #pragma warning restore CS0618 - var newName = Guid.NewGuid().ToString(); - options.ClientName = newName; - Assert.Equal(newName, muxer.ClientName); - - // TODO: This forks due to memoization of the byte[] for efficiency - // If we could cheaply detect change it'd be good to let this change - const string newConfigChannel = "newConfig"; - options.ConfigurationChannel = newConfigChannel; - Assert.Equal(newConfigChannel, options.ConfigurationChannel); - Assert.NotNull(muxer.ConfigurationChangedChannel); - Assert.Equal(Encoding.UTF8.GetString(muxer.ConfigurationChangedChannel), originalConfigChannel); - - Assert.Equal(originalUser, muxer.RawConfig.User); - Assert.Equal(originalPassword, muxer.RawConfig.Password); - var newPass = options.Password = "newPassword"; - Assert.Equal(newPass, muxer.RawConfig.Password); - } + var newName = Guid.NewGuid().ToString(); + options.ClientName = newName; + Assert.Equal(newName, conn.ClientName); + + // TODO: This forks due to memoization of the byte[] for efficiency + // If we could cheaply detect change it'd be good to let this change + const string newConfigChannel = "newConfig"; + options.ConfigurationChannel = newConfigChannel; + Assert.Equal(newConfigChannel, options.ConfigurationChannel); + Assert.NotNull(conn.ConfigurationChangedChannel); + Assert.Equal(Encoding.UTF8.GetString(conn.ConfigurationChangedChannel), originalConfigChannel); + + Assert.Equal(originalUser, conn.RawConfig.User); + Assert.Equal(originalPassword, conn.RawConfig.Password); + var newPass = options.Password = "newPassword"; + Assert.Equal(newPass, conn.RawConfig.Password); } } diff --git a/tests/StackExchange.Redis.Tests/ConnectByIP.cs b/tests/StackExchange.Redis.Tests/ConnectByIP.cs index 4295e0f7d..73a0b9825 100644 --- a/tests/StackExchange.Redis.Tests/ConnectByIP.cs +++ b/tests/StackExchange.Redis.Tests/ConnectByIP.cs @@ -5,104 +5,101 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class ConnectByIP : TestBase { - public class ConnectByIP : TestBase + public ConnectByIP(ITestOutputHelper output) : base (output) { } + + [Fact] + public void ParseEndpoints() { - public ConnectByIP(ITestOutputHelper output) : base (output) { } + var eps = new EndPointCollection + { + { "127.0.0.1", 1000 }, + { "::1", 1001 }, + { "localhost", 1002 } + }; + + Assert.Equal(AddressFamily.InterNetwork, eps[0].AddressFamily); + Assert.Equal(AddressFamily.InterNetworkV6, eps[1].AddressFamily); + Assert.Equal(AddressFamily.Unspecified, eps[2].AddressFamily); + + Assert.Equal("127.0.0.1:1000", eps[0].ToString()); + Assert.Equal("[::1]:1001", eps[1].ToString()); + Assert.Equal("Unspecified/localhost:1002", eps[2].ToString()); + } - [Fact] - public void ParseEndpoints() + [Fact] + public void IPv4Connection() + { + var config = new ConfigurationOptions { - var eps = new EndPointCollection - { - { "127.0.0.1", 1000 }, - { "::1", 1001 }, - { "localhost", 1002 } - }; - - Assert.Equal(AddressFamily.InterNetwork, eps[0].AddressFamily); - Assert.Equal(AddressFamily.InterNetworkV6, eps[1].AddressFamily); - Assert.Equal(AddressFamily.Unspecified, eps[2].AddressFamily); - - Assert.Equal("127.0.0.1:1000", eps[0].ToString()); - Assert.Equal("[::1]:1001", eps[1].ToString()); - Assert.Equal("Unspecified/localhost:1002", eps[2].ToString()); - } + EndPoints = { { TestConfig.Current.IPv4Server, TestConfig.Current.IPv4Port } } + }; + using var conn = ConnectionMultiplexer.Connect(config); + + var server = conn.GetServer(config.EndPoints[0]); + Assert.Equal(AddressFamily.InterNetwork, server.EndPoint.AddressFamily); + server.Ping(); + } - [Fact] - public void IPv4Connection() + [Fact] + public void IPv6Connection() + { + var config = new ConfigurationOptions { - var config = new ConfigurationOptions - { - EndPoints = { { TestConfig.Current.IPv4Server, TestConfig.Current.IPv4Port } } - }; - using (var conn = ConnectionMultiplexer.Connect(config)) - { - var server = conn.GetServer(config.EndPoints[0]); - Assert.Equal(AddressFamily.InterNetwork, server.EndPoint.AddressFamily); - server.Ping(); - } - } + EndPoints = { { TestConfig.Current.IPv6Server, TestConfig.Current.IPv6Port } } + }; + using var conn = ConnectionMultiplexer.Connect(config); - [Fact] - public void IPv6Connection() + var server = conn.GetServer(config.EndPoints[0]); + Assert.Equal(AddressFamily.InterNetworkV6, server.EndPoint.AddressFamily); + server.Ping(); + } + + [Theory] + [MemberData(nameof(ConnectByVariousEndpointsData))] + public void ConnectByVariousEndpoints(EndPoint ep, AddressFamily expectedFamily) + { + Assert.Equal(expectedFamily, ep.AddressFamily); + var config = new ConfigurationOptions + { + EndPoints = { ep } + }; + if (ep.AddressFamily != AddressFamily.InterNetworkV6) // I don't have IPv6 servers { - var config = new ConfigurationOptions - { - EndPoints = { { TestConfig.Current.IPv6Server, TestConfig.Current.IPv6Port } } - }; using (var conn = ConnectionMultiplexer.Connect(config)) { - var server = conn.GetServer(config.EndPoints[0]); - Assert.Equal(AddressFamily.InterNetworkV6, server.EndPoint.AddressFamily); + var actual = conn.GetEndPoints().Single(); + var server = conn.GetServer(actual); server.Ping(); } } + } - [Theory] - [MemberData(nameof(ConnectByVariousEndpointsData))] - public void ConnectByVariousEndpoints(EndPoint ep, AddressFamily expectedFamily) - { - Assert.Equal(expectedFamily, ep.AddressFamily); - var config = new ConfigurationOptions - { - EndPoints = { ep } - }; - if (ep.AddressFamily != AddressFamily.InterNetworkV6) // I don't have IPv6 servers - { - using (var conn = ConnectionMultiplexer.Connect(config)) - { - var actual = conn.GetEndPoints().Single(); - var server = conn.GetServer(actual); - server.Ping(); - } - } - } - - public static IEnumerable ConnectByVariousEndpointsData() - { - yield return new object[] { new IPEndPoint(IPAddress.Loopback, 6379), AddressFamily.InterNetwork }; + public static IEnumerable ConnectByVariousEndpointsData() + { + yield return new object[] { new IPEndPoint(IPAddress.Loopback, 6379), AddressFamily.InterNetwork }; - yield return new object[] { new IPEndPoint(IPAddress.IPv6Loopback, 6379), AddressFamily.InterNetworkV6 }; + yield return new object[] { new IPEndPoint(IPAddress.IPv6Loopback, 6379), AddressFamily.InterNetworkV6 }; - yield return new object[] { new DnsEndPoint("localhost", 6379), AddressFamily.Unspecified }; + yield return new object[] { new DnsEndPoint("localhost", 6379), AddressFamily.Unspecified }; - yield return new object[] { new DnsEndPoint("localhost", 6379, AddressFamily.InterNetwork), AddressFamily.InterNetwork }; + yield return new object[] { new DnsEndPoint("localhost", 6379, AddressFamily.InterNetwork), AddressFamily.InterNetwork }; - yield return new object[] { new DnsEndPoint("localhost", 6379, AddressFamily.InterNetworkV6), AddressFamily.InterNetworkV6 }; + yield return new object[] { new DnsEndPoint("localhost", 6379, AddressFamily.InterNetworkV6), AddressFamily.InterNetworkV6 }; - yield return new object[] { ConfigurationOptions.Parse("localhost:6379").EndPoints.Single(), AddressFamily.Unspecified }; + yield return new object[] { ConfigurationOptions.Parse("localhost:6379").EndPoints.Single(), AddressFamily.Unspecified }; - yield return new object[] { ConfigurationOptions.Parse("localhost").EndPoints.Single(), AddressFamily.Unspecified }; + yield return new object[] { ConfigurationOptions.Parse("localhost").EndPoints.Single(), AddressFamily.Unspecified }; - yield return new object[] { ConfigurationOptions.Parse("127.0.0.1:6379").EndPoints.Single(), AddressFamily.InterNetwork }; + yield return new object[] { ConfigurationOptions.Parse("127.0.0.1:6379").EndPoints.Single(), AddressFamily.InterNetwork }; - yield return new object[] { ConfigurationOptions.Parse("127.0.0.1").EndPoints.Single(), AddressFamily.InterNetwork }; + yield return new object[] { ConfigurationOptions.Parse("127.0.0.1").EndPoints.Single(), AddressFamily.InterNetwork }; - yield return new object[] { ConfigurationOptions.Parse("[::1]").EndPoints.Single(), AddressFamily.InterNetworkV6 }; + yield return new object[] { ConfigurationOptions.Parse("[::1]").EndPoints.Single(), AddressFamily.InterNetworkV6 }; - yield return new object[] { ConfigurationOptions.Parse("[::1]:6379").EndPoints.Single(), AddressFamily.InterNetworkV6 }; - } + yield return new object[] { ConfigurationOptions.Parse("[::1]:6379").EndPoints.Single(), AddressFamily.InterNetworkV6 }; } } diff --git a/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs b/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs index 5ba33e872..c7c2b6045 100644 --- a/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs +++ b/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs @@ -1,95 +1,92 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class ConnectCustomConfig : TestBase { - public class ConnectCustomConfig : TestBase + public ConnectCustomConfig(ITestOutputHelper output) : base (output) { } + + // So we're triggering tiebreakers here + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + + [Theory] + [InlineData("config")] + [InlineData("info")] + [InlineData("get")] + [InlineData("config,get")] + [InlineData("info,get")] + [InlineData("config,info,get")] + public void DisabledCommandsStillConnect(string disabledCommands) { - public ConnectCustomConfig(ITestOutputHelper output) : base (output) { } - - // So we're triggering tiebreakers here - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; - - [Theory] - [InlineData("config")] - [InlineData("info")] - [InlineData("get")] - [InlineData("config,get")] - [InlineData("info,get")] - [InlineData("config,info,get")] - public void DisabledCommandsStillConnect(string disabledCommands) - { - using var muxer = Create(allowAdmin: true, disabledCommands: disabledCommands.Split(','), log: Writer); + using var conn = Create(allowAdmin: true, disabledCommands: disabledCommands.Split(','), log: Writer); - var db = muxer.GetDatabase(); - db.Ping(); - Assert.True(db.IsConnected(default(RedisKey))); - } + var db = conn.GetDatabase(); + db.Ping(); + Assert.True(db.IsConnected(default(RedisKey))); + } - [Theory] - [InlineData("config")] - [InlineData("info")] - [InlineData("get")] - [InlineData("cluster")] - [InlineData("config,get")] - [InlineData("info,get")] - [InlineData("config,info,get")] - [InlineData("config,info,get,cluster")] - public void DisabledCommandsStillConnectCluster(string disabledCommands) - { - using var muxer = Create(allowAdmin: true, configuration: TestConfig.Current.ClusterServersAndPorts, disabledCommands: disabledCommands.Split(','), log: Writer); + [Theory] + [InlineData("config")] + [InlineData("info")] + [InlineData("get")] + [InlineData("cluster")] + [InlineData("config,get")] + [InlineData("info,get")] + [InlineData("config,info,get")] + [InlineData("config,info,get,cluster")] + public void DisabledCommandsStillConnectCluster(string disabledCommands) + { + using var conn = Create(allowAdmin: true, configuration: TestConfig.Current.ClusterServersAndPorts, disabledCommands: disabledCommands.Split(','), log: Writer); - var db = muxer.GetDatabase(); - db.Ping(); - Assert.True(db.IsConnected(default(RedisKey))); - } + var db = conn.GetDatabase(); + db.Ping(); + Assert.True(db.IsConnected(default(RedisKey))); + } - [Fact] - public void TieBreakerIntact() - { - using var muxer = (Create(allowAdmin: true, log: Writer) as ConnectionMultiplexer)!; + [Fact] + public void TieBreakerIntact() + { + using var conn = (Create(allowAdmin: true, log: Writer) as ConnectionMultiplexer)!; - var tiebreaker = muxer.GetDatabase().StringGet(muxer.RawConfig.TieBreaker); - Log($"Tiebreaker: {tiebreaker}"); + var tiebreaker = conn.GetDatabase().StringGet(conn.RawConfig.TieBreaker); + Log($"Tiebreaker: {tiebreaker}"); - var snapshot = muxer.GetServerSnapshot(); - foreach (var server in snapshot) - { - Assert.Equal(tiebreaker, server.TieBreakerResult); - } + foreach (var server in conn.GetServerSnapshot()) + { + Assert.Equal(tiebreaker, server.TieBreakerResult); } + } + + [Fact] + public void TieBreakerSkips() + { + using var conn = (Create(allowAdmin: true, disabledCommands: new[] { "get" }, log: Writer) as ConnectionMultiplexer)!; + Assert.Throws(() => conn.GetDatabase().StringGet(conn.RawConfig.TieBreaker)); - [Fact] - public void TieBreakerSkips() + foreach (var server in conn.GetServerSnapshot()) { - using var muxer = (Create(allowAdmin: true, disabledCommands: new[] { "get" }, log: Writer) as ConnectionMultiplexer)!; - Assert.Throws(() => muxer.GetDatabase().StringGet(muxer.RawConfig.TieBreaker)); - - var snapshot = muxer.GetServerSnapshot(); - foreach (var server in snapshot) - { - Assert.True(server.IsConnected); - Assert.Null(server.TieBreakerResult); - } + Assert.True(server.IsConnected); + Assert.Null(server.TieBreakerResult); } + } - [Fact] - public void TiebreakerIncorrectType() - { - var tiebreakerKey = Me(); - using var fubarMuxer = Create(allowAdmin: true, log: Writer); - // Store something nonsensical in the tiebreaker key: - fubarMuxer.GetDatabase().HashSet(tiebreakerKey, "foo", "bar"); + [Fact] + public void TiebreakerIncorrectType() + { + var tiebreakerKey = Me(); + using var fubarConn = Create(allowAdmin: true, log: Writer); + // Store something nonsensical in the tiebreaker key: + fubarConn.GetDatabase().HashSet(tiebreakerKey, "foo", "bar"); - // Ensure the next connection getting an invalid type still connects - using var muxer = Create(allowAdmin: true, tieBreaker: tiebreakerKey, log: Writer); + // Ensure the next connection getting an invalid type still connects + using var conn = Create(allowAdmin: true, tieBreaker: tiebreakerKey, log: Writer); - var db = muxer.GetDatabase(); - db.Ping(); - Assert.True(db.IsConnected(default(RedisKey))); + var db = conn.GetDatabase(); + db.Ping(); + Assert.True(db.IsConnected(default(RedisKey))); - var ex = Assert.Throws(() => db.StringGet(tiebreakerKey)); - Assert.Contains("WRONGTYPE", ex.Message); - } + var ex = Assert.Throws(() => db.StringGet(tiebreakerKey)); + Assert.Contains("WRONGTYPE", ex.Message); } } diff --git a/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs b/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs index 014223a24..1362e92a8 100644 --- a/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs +++ b/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs @@ -3,47 +3,45 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class ConnectFailTimeout : TestBase { - public class ConnectFailTimeout : TestBase + public ConnectFailTimeout(ITestOutputHelper output) : base (output) { } + + [Fact] + public async Task NoticesConnectFail() { - public ConnectFailTimeout(ITestOutputHelper output) : base (output) { } + SetExpectedAmbientFailureCount(-1); + using var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); + + var server = conn.GetServer(conn.GetEndPoints()[0]); - [Fact] - public async Task NoticesConnectFail() + await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); + void innerScenario() { - SetExpectedAmbientFailureCount(-1); - using (var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast)) - { - var server = conn.GetServer(conn.GetEndPoints()[0]); - - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() - { - conn.ConnectionFailed += (s, a) => - Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); - conn.ConnectionRestored += (s, a) => - Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); - - // No need to delay, we're going to try a disconnected connection immediately so it'll fail... - conn.IgnoreConnect = true; - Log("simulating failure"); - server.SimulateConnectionFailure(SimulatedFailureType.All); - Log("simulated failure"); - conn.IgnoreConnect = false; - Log("pinging - expect failure"); - Assert.Throws(() => server.Ping()); - Log("pinged"); - } - - // Heartbeat should reconnect by now - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => server.IsConnected); - - Log("pinging - expect success"); - var time = server.Ping(); - Log("pinged"); - Log(time.ToString()); - } + conn.ConnectionFailed += (s, a) => + Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); + conn.ConnectionRestored += (s, a) => + Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); + + // No need to delay, we're going to try a disconnected connection immediately so it'll fail... + conn.IgnoreConnect = true; + Log("simulating failure"); + server.SimulateConnectionFailure(SimulatedFailureType.All); + Log("simulated failure"); + conn.IgnoreConnect = false; + Log("pinging - expect failure"); + Assert.Throws(() => server.Ping()); + Log("pinged"); } + + // Heartbeat should reconnect by now + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => server.IsConnected); + + Log("pinging - expect success"); + var time = server.Ping(); + Log("pinged"); + Log(time.ToString()); } } diff --git a/tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs b/tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs index d648b5784..17f260ede 100644 --- a/tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs +++ b/tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs @@ -5,90 +5,89 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class ConnectToUnexistingHost : TestBase { - public class ConnectToUnexistingHost : TestBase - { - public ConnectToUnexistingHost(ITestOutputHelper output) : base (output) { } + public ConnectToUnexistingHost(ITestOutputHelper output) : base (output) { } - [Fact] - public async Task FailsWithinTimeout() + [Fact] + public async Task FailsWithinTimeout() + { + const int timeout = 1000; + var sw = Stopwatch.StartNew(); + try { - const int timeout = 1000; - var sw = Stopwatch.StartNew(); - try + var config = new ConfigurationOptions { - var config = new ConfigurationOptions - { - EndPoints = { { "invalid", 1234 } }, - ConnectTimeout = timeout - }; + EndPoints = { { "invalid", 1234 } }, + ConnectTimeout = timeout + }; - using (ConnectionMultiplexer.Connect(config, Writer)) - { - await Task.Delay(10000).ForAwait(); - } - - Assert.True(false, "Connect should fail with RedisConnectionException exception"); - } - catch (RedisConnectionException) + using (ConnectionMultiplexer.Connect(config, Writer)) { - var elapsed = sw.ElapsedMilliseconds; - Log("Elapsed time: " + elapsed); - Log("Timeout: " + timeout); - Assert.True(elapsed < 9000, "Connect should fail within ConnectTimeout, ElapsedMs: " + elapsed); + await Task.Delay(10000).ForAwait(); } - } - [Fact] - public async Task CanNotOpenNonsenseConnection_IP() + Assert.True(false, "Connect should fail with RedisConnectionException exception"); + } + catch (RedisConnectionException) { - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() - { - var ex = Assert.Throws(() => - { - using (ConnectionMultiplexer.Connect(TestConfig.Current.PrimaryServer + ":6500,connectTimeout=1000,connectRetry=0", Writer)) { } - }); - Log(ex.ToString()); - } + var elapsed = sw.ElapsedMilliseconds; + Log("Elapsed time: " + elapsed); + Log("Timeout: " + timeout); + Assert.True(elapsed < 9000, "Connect should fail within ConnectTimeout, ElapsedMs: " + elapsed); } + } - [Fact] - public async Task CanNotOpenNonsenseConnection_DNS() + [Fact] + public async Task CanNotOpenNonsenseConnection_IP() + { + await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); + void innerScenario() { - var ex = await Assert.ThrowsAsync(async () => + var ex = Assert.Throws(() => { - using (await ConnectionMultiplexer.ConnectAsync($"doesnot.exist.ds.{Guid.NewGuid():N}.com:6500,connectTimeout=1000,connectRetry=0", Writer).ForAwait()) { } - }).ForAwait(); + using (ConnectionMultiplexer.Connect(TestConfig.Current.PrimaryServer + ":6500,connectTimeout=1000,connectRetry=0", Writer)) { } + }); Log(ex.ToString()); } + } - [Fact] - public async Task CreateDisconnectedNonsenseConnection_IP() + [Fact] + public async Task CanNotOpenNonsenseConnection_DNS() + { + var ex = await Assert.ThrowsAsync(async () => { - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() + using (await ConnectionMultiplexer.ConnectAsync($"doesnot.exist.ds.{Guid.NewGuid():N}.com:6500,connectTimeout=1000,connectRetry=0", Writer).ForAwait()) { } + }).ForAwait(); + Log(ex.ToString()); + } + + [Fact] + public async Task CreateDisconnectedNonsenseConnection_IP() + { + await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); + void innerScenario() + { + using (var conn = ConnectionMultiplexer.Connect(TestConfig.Current.PrimaryServer + ":6500,abortConnect=false,connectTimeout=1000,connectRetry=0", Writer)) { - using (var conn = ConnectionMultiplexer.Connect(TestConfig.Current.PrimaryServer + ":6500,abortConnect=false,connectTimeout=1000,connectRetry=0", Writer)) - { - Assert.False(conn.GetServer(conn.GetEndPoints().Single()).IsConnected); - Assert.False(conn.GetDatabase().IsConnected(default(RedisKey))); - } + Assert.False(conn.GetServer(conn.GetEndPoints().Single()).IsConnected); + Assert.False(conn.GetDatabase().IsConnected(default(RedisKey))); } } + } - [Fact] - public async Task CreateDisconnectedNonsenseConnection_DNS() + [Fact] + public async Task CreateDisconnectedNonsenseConnection_DNS() + { + await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); + void innerScenario() { - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() + using (var conn = ConnectionMultiplexer.Connect($"doesnot.exist.ds.{Guid.NewGuid():N}.com:6500,abortConnect=false,connectTimeout=1000,connectRetry=0", Writer)) { - using (var conn = ConnectionMultiplexer.Connect($"doesnot.exist.ds.{Guid.NewGuid():N}.com:6500,abortConnect=false,connectTimeout=1000,connectRetry=0", Writer)) - { - Assert.False(conn.GetServer(conn.GetEndPoints().Single()).IsConnected); - Assert.False(conn.GetDatabase().IsConnected(default(RedisKey))); - } + Assert.False(conn.GetServer(conn.GetEndPoints().Single()).IsConnected); + Assert.False(conn.GetDatabase().IsConnected(default(RedisKey))); } } } diff --git a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs index 9dd0ad570..d32066bf3 100644 --- a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs +++ b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs @@ -4,156 +4,151 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class ConnectingFailDetection : TestBase { - public class ConnectingFailDetection : TestBase + public ConnectingFailDetection(ITestOutputHelper output) : base (output) { } + + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + + [Fact] + public async Task FastNoticesFailOnConnectingSyncCompletion() { - public ConnectingFailDetection(ITestOutputHelper output) : base (output) { } + try + { + using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false); + + var db = conn.GetDatabase(); + db.Ping(); - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + var server = conn.GetServer(conn.GetEndPoints()[0]); + var server2 = conn.GetServer(conn.GetEndPoints()[1]); - [Fact] - public async Task FastNoticesFailOnConnectingSyncCompletion() + conn.AllowConnect = false; + + // muxer.IsConnected is true of *any* are connected, simulate failure for all cases. + server.SimulateConnectionFailure(SimulatedFailureType.All); + Assert.False(server.IsConnected); + Assert.True(server2.IsConnected); + Assert.True(conn.IsConnected); + + server2.SimulateConnectionFailure(SimulatedFailureType.All); + Assert.False(server.IsConnected); + Assert.False(server2.IsConnected); + Assert.False(conn.IsConnected); + + // should reconnect within 1 keepalive interval + conn.AllowConnect = true; + Log("Waiting for reconnect"); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => conn.IsConnected).ForAwait(); + + Assert.True(conn.IsConnected); + } + finally { - try - { - using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false)) - { - var conn = muxer.GetDatabase(); - conn.Ping(); - - var server = muxer.GetServer(muxer.GetEndPoints()[0]); - var server2 = muxer.GetServer(muxer.GetEndPoints()[1]); - - muxer.AllowConnect = false; - - // muxer.IsConnected is true of *any* are connected, simulate failure for all cases. - server.SimulateConnectionFailure(SimulatedFailureType.All); - Assert.False(server.IsConnected); - Assert.True(server2.IsConnected); - Assert.True(muxer.IsConnected); - - server2.SimulateConnectionFailure(SimulatedFailureType.All); - Assert.False(server.IsConnected); - Assert.False(server2.IsConnected); - Assert.False(muxer.IsConnected); - - // should reconnect within 1 keepalive interval - muxer.AllowConnect = true; - Log("Waiting for reconnect"); - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => muxer.IsConnected).ForAwait(); - - Assert.True(muxer.IsConnected); - } - } - finally - { - ClearAmbientFailures(); - } + ClearAmbientFailures(); } + } - [Fact] - public async Task FastNoticesFailOnConnectingAsyncCompletion() + [Fact] + public async Task FastNoticesFailOnConnectingAsyncCompletion() + { + try { - try - { - using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false)) - { - var conn = muxer.GetDatabase(); - conn.Ping(); - - var server = muxer.GetServer(muxer.GetEndPoints()[0]); - var server2 = muxer.GetServer(muxer.GetEndPoints()[1]); - - muxer.AllowConnect = false; - - // muxer.IsConnected is true of *any* are connected, simulate failure for all cases. - server.SimulateConnectionFailure(SimulatedFailureType.All); - Assert.False(server.IsConnected); - Assert.True(server2.IsConnected); - Assert.True(muxer.IsConnected); - - server2.SimulateConnectionFailure(SimulatedFailureType.All); - Assert.False(server.IsConnected); - Assert.False(server2.IsConnected); - Assert.False(muxer.IsConnected); - - // should reconnect within 1 keepalive interval - muxer.AllowConnect = true; - Log("Waiting for reconnect"); - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => muxer.IsConnected).ForAwait(); - - Assert.True(muxer.IsConnected); - } - } - finally - { - ClearAmbientFailures(); - } - } + using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false); + + var db = conn.GetDatabase(); + db.Ping(); + + var server = conn.GetServer(conn.GetEndPoints()[0]); + var server2 = conn.GetServer(conn.GetEndPoints()[1]); - [Fact] - public async Task Issue922_ReconnectRaised() + conn.AllowConnect = false; + + // muxer.IsConnected is true of *any* are connected, simulate failure for all cases. + server.SimulateConnectionFailure(SimulatedFailureType.All); + Assert.False(server.IsConnected); + Assert.True(server2.IsConnected); + Assert.True(conn.IsConnected); + + server2.SimulateConnectionFailure(SimulatedFailureType.All); + Assert.False(server.IsConnected); + Assert.False(server2.IsConnected); + Assert.False(conn.IsConnected); + + // should reconnect within 1 keepalive interval + conn.AllowConnect = true; + Log("Waiting for reconnect"); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => conn.IsConnected).ForAwait(); + + Assert.True(conn.IsConnected); + } + finally { - var config = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort); - config.AbortOnConnectFail = true; - config.KeepAlive = 1; - config.SyncTimeout = 1000; - config.AsyncTimeout = 1000; - config.ReconnectRetryPolicy = new ExponentialRetry(5000); - config.AllowAdmin = true; - config.BacklogPolicy = BacklogPolicy.FailFast; - - int failCount = 0, restoreCount = 0; - - using (var muxer = ConnectionMultiplexer.Connect(config)) - { - muxer.ConnectionFailed += (s, e) => - { - Interlocked.Increment(ref failCount); - Log($"Connection Failed ({e.ConnectionType}, {e.FailureType}): {e.Exception}"); - }; - muxer.ConnectionRestored += (s, e) => - { - Interlocked.Increment(ref restoreCount); - Log($"Connection Restored ({e.ConnectionType}, {e.FailureType})"); - }; - - muxer.GetDatabase(); - Assert.Equal(0, Volatile.Read(ref failCount)); - Assert.Equal(0, Volatile.Read(ref restoreCount)); - - var server = muxer.GetServer(TestConfig.Current.PrimaryServerAndPort); - server.SimulateConnectionFailure(SimulatedFailureType.All); - - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Volatile.Read(ref failCount) >= 2 && Volatile.Read(ref restoreCount) >= 2); - - // interactive+subscriber = 2 - var failCountSnapshot = Volatile.Read(ref failCount); - Assert.True(failCountSnapshot >= 2, $"failCount {failCountSnapshot} >= 2"); - - var restoreCountSnapshot = Volatile.Read(ref restoreCount); - Assert.True(restoreCountSnapshot >= 2, $"restoreCount ({restoreCountSnapshot}) >= 2"); - } + ClearAmbientFailures(); } + } + + [Fact] + public async Task Issue922_ReconnectRaised() + { + var config = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort); + config.AbortOnConnectFail = true; + config.KeepAlive = 1; + config.SyncTimeout = 1000; + config.AsyncTimeout = 1000; + config.ReconnectRetryPolicy = new ExponentialRetry(5000); + config.AllowAdmin = true; + config.BacklogPolicy = BacklogPolicy.FailFast; + + int failCount = 0, restoreCount = 0; + + using var conn = ConnectionMultiplexer.Connect(config); + + conn.ConnectionFailed += (s, e) => + { + Interlocked.Increment(ref failCount); + Log($"Connection Failed ({e.ConnectionType}, {e.FailureType}): {e.Exception}"); + }; + conn.ConnectionRestored += (s, e) => + { + Interlocked.Increment(ref restoreCount); + Log($"Connection Restored ({e.ConnectionType}, {e.FailureType})"); + }; + + conn.GetDatabase(); + Assert.Equal(0, Volatile.Read(ref failCount)); + Assert.Equal(0, Volatile.Read(ref restoreCount)); + + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); + server.SimulateConnectionFailure(SimulatedFailureType.All); - [Fact] - public void ConnectsWhenBeginConnectCompletesSynchronously() + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Volatile.Read(ref failCount) >= 2 && Volatile.Read(ref restoreCount) >= 2); + + // interactive+subscriber = 2 + var failCountSnapshot = Volatile.Read(ref failCount); + Assert.True(failCountSnapshot >= 2, $"failCount {failCountSnapshot} >= 2"); + + var restoreCountSnapshot = Volatile.Read(ref restoreCount); + Assert.True(restoreCountSnapshot >= 2, $"restoreCount ({restoreCountSnapshot}) >= 2"); + } + + [Fact] + public void ConnectsWhenBeginConnectCompletesSynchronously() + { + try + { + using var conn = Create(keepAlive: 1, connectTimeout: 3000); + + var db = conn.GetDatabase(); + db.Ping(); + + Assert.True(conn.IsConnected); + } + finally { - try - { - using (var muxer = Create(keepAlive: 1, connectTimeout: 3000)) - { - var conn = muxer.GetDatabase(); - conn.Ping(); - - Assert.True(muxer.IsConnected); - } - } - finally - { - ClearAmbientFailures(); - } + ClearAmbientFailures(); } } } diff --git a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs b/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs index c67b840b5..791bbd44e 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs @@ -6,206 +6,202 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class ConnectionFailedErrors : TestBase { - public class ConnectionFailedErrors : TestBase + public ConnectionFailedErrors(ITestOutputHelper output) : base (output) { } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SSLCertificateValidationError(bool isCertValidationSucceeded) { - public ConnectionFailedErrors(ITestOutputHelper output) : base (output) { } + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task SSLCertificateValidationError(bool isCertValidationSucceeded) - { - Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); - Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); + var options = new ConfigurationOptions(); + options.EndPoints.Add(TestConfig.Current.AzureCacheServer); + options.Ssl = true; + options.Password = TestConfig.Current.AzureCachePassword; + options.CertificateValidation += (sender, cert, chain, errors) => isCertValidationSucceeded; + options.AbortOnConnectFail = false; - var options = new ConfigurationOptions(); - options.EndPoints.Add(TestConfig.Current.AzureCacheServer); - options.Ssl = true; - options.Password = TestConfig.Current.AzureCachePassword; - options.CertificateValidation += (sender, cert, chain, errors) => isCertValidationSucceeded; - options.AbortOnConnectFail = false; + using var conn = ConnectionMultiplexer.Connect(options); - using (var connection = ConnectionMultiplexer.Connect(options)) + await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); + void innerScenario() + { + conn.ConnectionFailed += (sender, e) => + Assert.Equal(ConnectionFailureType.AuthenticationFailure, e.FailureType); + if (!isCertValidationSucceeded) { - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() - { - connection.ConnectionFailed += (sender, e) => - Assert.Equal(ConnectionFailureType.AuthenticationFailure, e.FailureType); - if (!isCertValidationSucceeded) - { - //validate that in this case it throws an certificatevalidation exception - var outer = Assert.Throws(() => connection.GetDatabase().Ping()); - Assert.Equal(ConnectionFailureType.UnableToResolvePhysicalConnection, outer.FailureType); - - Assert.NotNull(outer.InnerException); - var inner = Assert.IsType(outer.InnerException); - Assert.Equal(ConnectionFailureType.AuthenticationFailure, inner.FailureType); + //validate that in this case it throws an certificatevalidation exception + var outer = Assert.Throws(() => conn.GetDatabase().Ping()); + Assert.Equal(ConnectionFailureType.UnableToResolvePhysicalConnection, outer.FailureType); - Assert.NotNull(inner.InnerException); - var innerMost = Assert.IsType(inner.InnerException); - Assert.Equal("The remote certificate is invalid according to the validation procedure.", innerMost.Message); - } - else - { - connection.GetDatabase().Ping(); - } - } + Assert.NotNull(outer.InnerException); + var inner = Assert.IsType(outer.InnerException); + Assert.Equal(ConnectionFailureType.AuthenticationFailure, inner.FailureType); - // wait for a second for connectionfailed event to fire - await Task.Delay(1000).ForAwait(); + Assert.NotNull(inner.InnerException); + var innerMost = Assert.IsType(inner.InnerException); + Assert.Equal("The remote certificate is invalid according to the validation procedure.", innerMost.Message); + } + else + { + conn.GetDatabase().Ping(); } } - [Fact] - public async Task AuthenticationFailureError() + // wait for a second for connectionfailed event to fire + await Task.Delay(1000).ForAwait(); + } + + [Fact] + public async Task AuthenticationFailureError() + { + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); + + var options = new ConfigurationOptions(); + options.EndPoints.Add(TestConfig.Current.AzureCacheServer); + options.Ssl = true; + options.Password = ""; + options.AbortOnConnectFail = false; + options.CertificateValidation += SSL.ShowCertFailures(Writer); + + using var conn = ConnectionMultiplexer.Connect(options); + + await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); + void innerScenario() { - Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); + conn.ConnectionFailed += (sender, e) => + { + if (e.FailureType == ConnectionFailureType.SocketFailure) Skip.Inconclusive("socket fail"); // this is OK too + Assert.Equal(ConnectionFailureType.AuthenticationFailure, e.FailureType); + }; + var ex = Assert.Throws(() => conn.GetDatabase().Ping()); + + Assert.NotNull(ex.InnerException); + var rde = Assert.IsType(ex.InnerException); + Assert.Equal(CommandStatus.WaitingToBeSent, ex.CommandStatus); + Assert.Equal(ConnectionFailureType.AuthenticationFailure, rde.FailureType); + Assert.NotNull(rde.InnerException); + Assert.Equal("Error: NOAUTH Authentication required. Verify if the Redis password provided is correct.", rde.InnerException.Message); + } + + //wait for a second for connectionfailed event to fire + await Task.Delay(1000).ForAwait(); + } + [Fact] + public async Task SocketFailureError() + { + await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); + void innerScenario() + { var options = new ConfigurationOptions(); - options.EndPoints.Add(TestConfig.Current.AzureCacheServer); + options.EndPoints.Add($"{Guid.NewGuid():N}.redis.cache.windows.net"); options.Ssl = true; options.Password = ""; options.AbortOnConnectFail = false; - options.CertificateValidation += SSL.ShowCertFailures(Writer); - using (var muxer = ConnectionMultiplexer.Connect(options)) + options.ConnectTimeout = 1000; + options.BacklogPolicy = BacklogPolicy.FailFast; + var outer = Assert.Throws(() => { - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() - { - muxer.ConnectionFailed += (sender, e) => - { - if (e.FailureType == ConnectionFailureType.SocketFailure) Skip.Inconclusive("socket fail"); // this is OK too - Assert.Equal(ConnectionFailureType.AuthenticationFailure, e.FailureType); - }; - var ex = Assert.Throws(() => muxer.GetDatabase().Ping()); - - Assert.NotNull(ex.InnerException); - var rde = Assert.IsType(ex.InnerException); - Assert.Equal(CommandStatus.WaitingToBeSent, ex.CommandStatus); - Assert.Equal(ConnectionFailureType.AuthenticationFailure, rde.FailureType); - Assert.NotNull(rde.InnerException); - Assert.Equal("Error: NOAUTH Authentication required. Verify if the Redis password provided is correct.", rde.InnerException.Message); - } + using var conn = ConnectionMultiplexer.Connect(options); - //wait for a second for connectionfailed event to fire - await Task.Delay(1000).ForAwait(); - } - } + conn.GetDatabase().Ping(); + }); + Assert.Equal(ConnectionFailureType.UnableToResolvePhysicalConnection, outer.FailureType); - [Fact] - public async Task SocketFailureError() - { - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() + Assert.NotNull(outer.InnerException); + if (outer.InnerException is RedisConnectionException rce) { - var options = new ConfigurationOptions(); - options.EndPoints.Add($"{Guid.NewGuid():N}.redis.cache.windows.net"); - options.Ssl = true; - options.Password = ""; - options.AbortOnConnectFail = false; - options.ConnectTimeout = 1000; - options.BacklogPolicy = BacklogPolicy.FailFast; - var outer = Assert.Throws(() => - { - using (var muxer = ConnectionMultiplexer.Connect(options)) - { - muxer.GetDatabase().Ping(); - } - }); - Assert.Equal(ConnectionFailureType.UnableToResolvePhysicalConnection, outer.FailureType); - - Assert.NotNull(outer.InnerException); - if (outer.InnerException is RedisConnectionException rce) - { - Assert.Equal(ConnectionFailureType.UnableToConnect, rce.FailureType); - } - else if (outer.InnerException is AggregateException ae - && ae.InnerExceptions.Any(e => e is RedisConnectionException rce2 - && rce2.FailureType == ConnectionFailureType.UnableToConnect)) - { - // fine; at least *one* of them is the one we were hoping to see - } - else + Assert.Equal(ConnectionFailureType.UnableToConnect, rce.FailureType); + } + else if (outer.InnerException is AggregateException ae + && ae.InnerExceptions.Any(e => e is RedisConnectionException rce2 + && rce2.FailureType == ConnectionFailureType.UnableToConnect)) + { + // fine; at least *one* of them is the one we were hoping to see + } + else + { + Writer.WriteLine(outer.InnerException.ToString()); + if (outer.InnerException is AggregateException inner) { - Writer.WriteLine(outer.InnerException.ToString()); - if (outer.InnerException is AggregateException inner) + foreach (var ex in inner.InnerExceptions) { - foreach (var ex in inner.InnerExceptions) - { - Writer.WriteLine(ex.ToString()); - } + Writer.WriteLine(ex.ToString()); } - Assert.False(true); // force fail } + Assert.False(true); // force fail } } + } - [Fact] - public async Task AbortOnConnectFailFalseConnectTimeoutError() + [Fact] + public async Task AbortOnConnectFailFalseConnectTimeoutError() + { + await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); + void innerScenario() { - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() - { - Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); - Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); - - var options = new ConfigurationOptions(); - options.EndPoints.Add(TestConfig.Current.AzureCacheServer); - options.Ssl = true; - options.ConnectTimeout = 0; - options.Password = TestConfig.Current.AzureCachePassword; - using (var muxer = ConnectionMultiplexer.Connect(options)) - { - var ex = Assert.Throws(() => muxer.GetDatabase().Ping()); - Assert.Contains("ConnectTimeout", ex.Message); - } - } - } + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); - [Fact] - public void TryGetAzureRoleInstanceIdNoThrow() - { - Assert.Null(DefaultOptionsProvider.TryGetAzureRoleInstanceIdNoThrow()); + var options = new ConfigurationOptions(); + options.EndPoints.Add(TestConfig.Current.AzureCacheServer); + options.Ssl = true; + options.ConnectTimeout = 0; + options.Password = TestConfig.Current.AzureCachePassword; + + using var conn = ConnectionMultiplexer.Connect(options); + + var ex = Assert.Throws(() => conn.GetDatabase().Ping()); + Assert.Contains("ConnectTimeout", ex.Message); } + } + + [Fact] + public void TryGetAzureRoleInstanceIdNoThrow() + { + Assert.Null(DefaultOptionsProvider.TryGetAzureRoleInstanceIdNoThrow()); + } #if DEBUG - [Fact] - public async Task CheckFailureRecovered() + [Fact] + public async Task CheckFailureRecovered() + { + try { - try - { - using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, log: Writer, shared: false)) - { - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() - { - muxer.GetDatabase(); - var server = muxer.GetServer(muxer.GetEndPoints()[0]); + using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, log: Writer, shared: false); - muxer.AllowConnect = false; + await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); + void innerScenario() + { + conn.GetDatabase(); + var server = conn.GetServer(conn.GetEndPoints()[0]); - server.SimulateConnectionFailure(SimulatedFailureType.All); + conn.AllowConnect = false; - var lastFailure = ((RedisConnectionException?)muxer.GetServerSnapshot()[0].LastException)!.FailureType; - // Depending on heartbat races, the last exception will be a socket failure or an internal (follow-up) failure - Assert.Contains(lastFailure, new[] { ConnectionFailureType.SocketFailure, ConnectionFailureType.InternalFailure }); + server.SimulateConnectionFailure(SimulatedFailureType.All); - // should reconnect within 1 keepalive interval - muxer.AllowConnect = true; - } - await Task.Delay(2000).ForAwait(); + var lastFailure = ((RedisConnectionException?)conn.GetServerSnapshot()[0].LastException)!.FailureType; + // Depending on heartbeat races, the last exception will be a socket failure or an internal (follow-up) failure + Assert.Contains(lastFailure, new[] { ConnectionFailureType.SocketFailure, ConnectionFailureType.InternalFailure }); - Assert.Null(muxer.GetServerSnapshot()[0].LastException); - } - } - finally - { - ClearAmbientFailures(); + // should reconnect within 1 keepalive interval + conn.AllowConnect = true; } + await Task.Delay(2000).ForAwait(); + + Assert.Null(conn.GetServerSnapshot()[0].LastException); + } + finally + { + ClearAmbientFailures(); } -#endif } +#endif } diff --git a/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs b/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs index 5f9fe64dc..2ce71f2f7 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs @@ -2,52 +2,51 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class TransientErrorTests : TestBase { - public class TransientErrorTests : TestBase + public TransientErrorTests(ITestOutputHelper output) : base (output) { } + + [Fact] + public void TestExponentialRetry() + { + IReconnectRetryPolicy exponentialRetry = new ExponentialRetry(5000); + Assert.False(exponentialRetry.ShouldRetry(0, 0)); + Assert.True(exponentialRetry.ShouldRetry(1, 5600)); + Assert.True(exponentialRetry.ShouldRetry(2, 6050)); + Assert.False(exponentialRetry.ShouldRetry(2, 4050)); + } + + [Fact] + public void TestExponentialMaxRetry() + { + IReconnectRetryPolicy exponentialRetry = new ExponentialRetry(5000); + Assert.True(exponentialRetry.ShouldRetry(long.MaxValue, (int)TimeSpan.FromSeconds(30).TotalMilliseconds)); + } + + [Fact] + public void TestExponentialRetryArgs() + { + _ = new ExponentialRetry(5000); + _ = new ExponentialRetry(5000, 10000); + + var ex = Assert.Throws(() => new ExponentialRetry(-1)); + Assert.Equal("deltaBackOffMilliseconds", ex.ParamName); + + ex = Assert.Throws(() => new ExponentialRetry(5000, -1)); + Assert.Equal("maxDeltaBackOffMilliseconds", ex.ParamName); + + ex = Assert.Throws(() => new ExponentialRetry(10000, 5000)); + Assert.Equal("maxDeltaBackOffMilliseconds", ex.ParamName); + } + + [Fact] + public void TestLinearRetry() { - public TransientErrorTests(ITestOutputHelper output) : base (output) { } - - [Fact] - public void TestExponentialRetry() - { - IReconnectRetryPolicy exponentialRetry = new ExponentialRetry(5000); - Assert.False(exponentialRetry.ShouldRetry(0, 0)); - Assert.True(exponentialRetry.ShouldRetry(1, 5600)); - Assert.True(exponentialRetry.ShouldRetry(2, 6050)); - Assert.False(exponentialRetry.ShouldRetry(2, 4050)); - } - - [Fact] - public void TestExponentialMaxRetry() - { - IReconnectRetryPolicy exponentialRetry = new ExponentialRetry(5000); - Assert.True(exponentialRetry.ShouldRetry(long.MaxValue, (int)TimeSpan.FromSeconds(30).TotalMilliseconds)); - } - - [Fact] - public void TestExponentialRetryArgs() - { - _ = new ExponentialRetry(5000); - _ = new ExponentialRetry(5000, 10000); - - var ex = Assert.Throws(() => new ExponentialRetry(-1)); - Assert.Equal("deltaBackOffMilliseconds", ex.ParamName); - - ex = Assert.Throws(() => new ExponentialRetry(5000, -1)); - Assert.Equal("maxDeltaBackOffMilliseconds", ex.ParamName); - - ex = Assert.Throws(() => new ExponentialRetry(10000, 5000)); - Assert.Equal("maxDeltaBackOffMilliseconds", ex.ParamName); - } - - [Fact] - public void TestLinearRetry() - { - IReconnectRetryPolicy linearRetry = new LinearRetry(5000); - Assert.False(linearRetry.ShouldRetry(0, 0)); - Assert.False(linearRetry.ShouldRetry(2, 4999)); - Assert.True(linearRetry.ShouldRetry(1, 5000)); - } + IReconnectRetryPolicy linearRetry = new LinearRetry(5000); + Assert.False(linearRetry.ShouldRetry(0, 0)); + Assert.False(linearRetry.ShouldRetry(2, 4999)); + Assert.True(linearRetry.ShouldRetry(1, 5000)); } } diff --git a/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs b/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs index df85d2818..13d06cbfd 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs @@ -4,53 +4,51 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class ConnectionShutdown : TestBase { - public class ConnectionShutdown : TestBase + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; + public ConnectionShutdown(ITestOutputHelper output) : base(output) { } + + [Fact(Skip = "Unfriendly")] + public async Task ShutdownRaisesConnectionFailedAndRestore() { - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public ConnectionShutdown(ITestOutputHelper output) : base(output) { } + using var conn = Create(allowAdmin: true, shared: false); - [Fact(Skip = "Unfriendly")] - public async Task ShutdownRaisesConnectionFailedAndRestore() + int failed = 0, restored = 0; + Stopwatch watch = Stopwatch.StartNew(); + conn.ConnectionFailed += (sender, args) => + { + Log(watch.Elapsed + ": failed: " + EndPointCollection.ToString(args.EndPoint) + "/" + args.ConnectionType + ": " + args); + Interlocked.Increment(ref failed); + }; + conn.ConnectionRestored += (sender, args) => { - using (var conn = Create(allowAdmin: true, shared: false)) - { - int failed = 0, restored = 0; - Stopwatch watch = Stopwatch.StartNew(); - conn.ConnectionFailed += (sender, args) => - { - Log(watch.Elapsed + ": failed: " + EndPointCollection.ToString(args.EndPoint) + "/" + args.ConnectionType + ": " + args); - Interlocked.Increment(ref failed); - }; - conn.ConnectionRestored += (sender, args) => - { - Log(watch.Elapsed + ": restored: " + EndPointCollection.ToString(args.EndPoint) + "/" + args.ConnectionType + ": " + args); - Interlocked.Increment(ref restored); - }; - var db = conn.GetDatabase(); - db.Ping(); - Assert.Equal(0, Interlocked.CompareExchange(ref failed, 0, 0)); - Assert.Equal(0, Interlocked.CompareExchange(ref restored, 0, 0)); - await Task.Delay(1).ForAwait(); // To make compiler happy in Release + Log(watch.Elapsed + ": restored: " + EndPointCollection.ToString(args.EndPoint) + "/" + args.ConnectionType + ": " + args); + Interlocked.Increment(ref restored); + }; + var db = conn.GetDatabase(); + db.Ping(); + Assert.Equal(0, Interlocked.CompareExchange(ref failed, 0, 0)); + Assert.Equal(0, Interlocked.CompareExchange(ref restored, 0, 0)); + await Task.Delay(1).ForAwait(); // To make compiler happy in Release - conn.AllowConnect = false; - var server = conn.GetServer(TestConfig.Current.PrimaryServer, TestConfig.Current.PrimaryPort); + conn.AllowConnect = false; + var server = conn.GetServer(TestConfig.Current.PrimaryServer, TestConfig.Current.PrimaryPort); - SetExpectedAmbientFailureCount(2); - server.SimulateConnectionFailure(SimulatedFailureType.All); + SetExpectedAmbientFailureCount(2); + server.SimulateConnectionFailure(SimulatedFailureType.All); - db.Ping(CommandFlags.FireAndForget); - await Task.Delay(250).ForAwait(); - Assert.Equal(2, Interlocked.CompareExchange(ref failed, 0, 0)); - Assert.Equal(0, Interlocked.CompareExchange(ref restored, 0, 0)); - conn.AllowConnect = true; - db.Ping(CommandFlags.FireAndForget); - await Task.Delay(1500).ForAwait(); - Assert.Equal(2, Interlocked.CompareExchange(ref failed, 0, 0)); - Assert.Equal(2, Interlocked.CompareExchange(ref restored, 0, 0)); - watch.Stop(); - } - } + db.Ping(CommandFlags.FireAndForget); + await Task.Delay(250).ForAwait(); + Assert.Equal(2, Interlocked.CompareExchange(ref failed, 0, 0)); + Assert.Equal(0, Interlocked.CompareExchange(ref restored, 0, 0)); + conn.AllowConnect = true; + db.Ping(CommandFlags.FireAndForget); + await Task.Delay(1500).ForAwait(); + Assert.Equal(2, Interlocked.CompareExchange(ref failed, 0, 0)); + Assert.Equal(2, Interlocked.CompareExchange(ref restored, 0, 0)); + watch.Stop(); } } diff --git a/tests/StackExchange.Redis.Tests/Constraints.cs b/tests/StackExchange.Redis.Tests/Constraints.cs index 5727127e6..614c6b4a1 100644 --- a/tests/StackExchange.Redis.Tests/Constraints.cs +++ b/tests/StackExchange.Redis.Tests/Constraints.cs @@ -2,49 +2,47 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Constraints : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Constraints : TestBase + public Constraints(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public void ValueEquals() { - public Constraints(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + RedisValue x = 1, y = "1"; + Assert.True(x.Equals(y), "equals"); + Assert.True(x == y, "operator"); + } - [Fact] - public void ValueEquals() - { - RedisValue x = 1, y = "1"; - Assert.True(x.Equals(y), "equals"); - Assert.True(x == y, "operator"); - } + [Fact] + public async Task TestManualIncr() + { + using var conn = Create(syncTimeout: 120000); // big timeout while debugging - [Fact] - public async Task TestManualIncr() + var key = Me(); + var db = conn.GetDatabase(); + for (int i = 0; i < 10; i++) { - using (var muxer = Create(syncTimeout: 120000)) // big timeout while debugging - { - var key = Me(); - var conn = muxer.GetDatabase(); - for (int i = 0; i < 10; i++) - { - conn.KeyDelete(key, CommandFlags.FireAndForget); - Assert.Equal(1, await ManualIncrAsync(conn, key).ForAwait()); - Assert.Equal(2, await ManualIncrAsync(conn, key).ForAwait()); - Assert.Equal(2, (long)conn.StringGet(key)); - } - } + db.KeyDelete(key, CommandFlags.FireAndForget); + Assert.Equal(1, await ManualIncrAsync(db, key).ForAwait()); + Assert.Equal(2, await ManualIncrAsync(db, key).ForAwait()); + Assert.Equal(2, (long)db.StringGet(key)); } + } - public static async Task ManualIncrAsync(IDatabase connection, RedisKey key) - { - var oldVal = (long?)await connection.StringGetAsync(key).ForAwait(); - var newVal = (oldVal ?? 0) + 1; - var tran = connection.CreateTransaction(); - { // check hasn't changed - tran.AddCondition(Condition.StringEqual(key, oldVal)); - _ = tran.StringSetAsync(key, newVal); - if (!await tran.ExecuteAsync().ForAwait()) return null; // aborted - return newVal; - } + public static async Task ManualIncrAsync(IDatabase connection, RedisKey key) + { + var oldVal = (long?)await connection.StringGetAsync(key).ForAwait(); + var newVal = (oldVal ?? 0) + 1; + var tran = connection.CreateTransaction(); + { // check hasn't changed + tran.AddCondition(Condition.StringEqual(key, oldVal)); + _ = tran.StringSetAsync(key, newVal); + if (!await tran.ExecuteAsync().ForAwait()) return null; // aborted + return newVal; } } } diff --git a/tests/StackExchange.Redis.Tests/Copy.cs b/tests/StackExchange.Redis.Tests/Copy.cs index 1d7c05b62..45b683b35 100644 --- a/tests/StackExchange.Redis.Tests/Copy.cs +++ b/tests/StackExchange.Redis.Tests/Copy.cs @@ -3,71 +3,67 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Copy : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Copy : TestBase - { - public Copy(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public Copy(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } - [Fact] - public async Task Basic() - { - using var muxer = Create(); - Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + [Fact] + public async Task Basic() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - var db = muxer.GetDatabase(); - var src = Me(); - var dest = Me() + "2"; - _ = db.KeyDelete(dest); + var db = conn.GetDatabase(); + var src = Me(); + var dest = Me() + "2"; + _ = db.KeyDelete(dest); - _ = db.StringSetAsync(src, "Heyyyyy"); - var ke1 = db.KeyCopyAsync(src, dest).ForAwait(); - var ku1 = db.StringGet(dest); - Assert.True(await ke1); - Assert.True(ku1.Equals("Heyyyyy")); - } + _ = db.StringSetAsync(src, "Heyyyyy"); + var ke1 = db.KeyCopyAsync(src, dest).ForAwait(); + var ku1 = db.StringGet(dest); + Assert.True(await ke1); + Assert.True(ku1.Equals("Heyyyyy")); + } - [Fact] - public async Task CrossDB() - { - using var muxer = Create(); - Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + [Fact] + public async Task CrossDB() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - var db = muxer.GetDatabase(); - var dbDestId = TestConfig.GetDedicatedDB(muxer); - var dbDest = muxer.GetDatabase(dbDestId); + var db = conn.GetDatabase(); + var dbDestId = TestConfig.GetDedicatedDB(conn); + var dbDest = conn.GetDatabase(dbDestId); - var src = Me(); - var dest = Me() + "2"; - dbDest.KeyDelete(dest); + var src = Me(); + var dest = Me() + "2"; + dbDest.KeyDelete(dest); - _ = db.StringSetAsync(src, "Heyyyyy"); - var ke1 = db.KeyCopyAsync(src, dest, dbDestId).ForAwait(); - var ku1 = dbDest.StringGet(dest); - Assert.True(await ke1); - Assert.True(ku1.Equals("Heyyyyy")); + _ = db.StringSetAsync(src, "Heyyyyy"); + var ke1 = db.KeyCopyAsync(src, dest, dbDestId).ForAwait(); + var ku1 = dbDest.StringGet(dest); + Assert.True(await ke1); + Assert.True(ku1.Equals("Heyyyyy")); - await Assert.ThrowsAsync(() => db.KeyCopyAsync(src, dest, destinationDatabase: -10)); - } + await Assert.ThrowsAsync(() => db.KeyCopyAsync(src, dest, destinationDatabase: -10)); + } - [Fact] - public async Task WithReplace() - { - using var muxer = Create(); - Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + [Fact] + public async Task WithReplace() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - var db = muxer.GetDatabase(); - var src = Me(); - var dest = Me() + "2"; - _ = db.StringSetAsync(src, "foo1"); - _ = db.StringSetAsync(dest, "foo2"); - var ke1 = db.KeyCopyAsync(src, dest).ForAwait(); - var ke2 = db.KeyCopyAsync(src, dest, replace: true).ForAwait(); - var ku1 = db.StringGet(dest); - Assert.False(await ke1); // Should fail when not using replace and destination key exist - Assert.True(await ke2); - Assert.True(ku1.Equals("foo1")); - } + var db = conn.GetDatabase(); + var src = Me(); + var dest = Me() + "2"; + _ = db.StringSetAsync(src, "foo1"); + _ = db.StringSetAsync(dest, "foo2"); + var ke1 = db.KeyCopyAsync(src, dest).ForAwait(); + var ke2 = db.KeyCopyAsync(src, dest, replace: true).ForAwait(); + var ku1 = db.StringGet(dest); + Assert.False(await ke1); // Should fail when not using replace and destination key exist + Assert.True(await ke2); + Assert.True(ku1.Equals("foo1")); } } diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 5d5b1b6b2..e51805da3 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -7,1307 +7,1306 @@ using StackExchange.Redis.KeyspaceIsolation; using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[CollectionDefinition(nameof(MoqDependentCollection), DisableParallelization = true)] +public class MoqDependentCollection { } + +[Collection(nameof(MoqDependentCollection))] +public sealed class DatabaseWrapperTests { - [CollectionDefinition(nameof(MoqDependentCollection), DisableParallelization = true)] - public class MoqDependentCollection { } - - [Collection(nameof(MoqDependentCollection))] - public sealed class DatabaseWrapperTests - { - private readonly Mock mock; - private readonly IDatabase wrapper; - - public DatabaseWrapperTests() - { - mock = new Mock(); - wrapper = new DatabaseWrapper(mock.Object, Encoding.UTF8.GetBytes("prefix:")); - } - - [Fact] - public void CreateBatch() - { - object asyncState = new(); - IBatch innerBatch = new Mock().Object; - mock.Setup(_ => _.CreateBatch(asyncState)).Returns(innerBatch); - IBatch wrappedBatch = wrapper.CreateBatch(asyncState); - mock.Verify(_ => _.CreateBatch(asyncState)); - Assert.IsType(wrappedBatch); - Assert.Same(innerBatch, ((BatchWrapper)wrappedBatch).Inner); - } - - [Fact] - public void CreateTransaction() - { - object asyncState = new(); - ITransaction innerTransaction = new Mock().Object; - mock.Setup(_ => _.CreateTransaction(asyncState)).Returns(innerTransaction); - ITransaction wrappedTransaction = wrapper.CreateTransaction(asyncState); - mock.Verify(_ => _.CreateTransaction(asyncState)); - Assert.IsType(wrappedTransaction); - Assert.Same(innerTransaction, ((TransactionWrapper)wrappedTransaction).Inner); - } - - [Fact] - public void DebugObject() - { - wrapper.DebugObject("key", CommandFlags.None); - mock.Verify(_ => _.DebugObject("prefix:key", CommandFlags.None)); - } - - [Fact] - public void Get_Database() - { - mock.SetupGet(_ => _.Database).Returns(123); - Assert.Equal(123, wrapper.Database); - } - - [Fact] - public void HashDecrement_1() - { - wrapper.HashDecrement("key", "hashField", 123, CommandFlags.None); - mock.Verify(_ => _.HashDecrement("prefix:key", "hashField", 123, CommandFlags.None)); - } - - [Fact] - public void HashDecrement_2() - { - wrapper.HashDecrement("key", "hashField", 1.23, CommandFlags.None); - mock.Verify(_ => _.HashDecrement("prefix:key", "hashField", 1.23, CommandFlags.None)); - } - - [Fact] - public void HashDelete_1() - { - wrapper.HashDelete("key", "hashField", CommandFlags.None); - mock.Verify(_ => _.HashDelete("prefix:key", "hashField", CommandFlags.None)); - } - - [Fact] - public void HashDelete_2() - { - RedisValue[] hashFields = Array.Empty(); - wrapper.HashDelete("key", hashFields, CommandFlags.None); - mock.Verify(_ => _.HashDelete("prefix:key", hashFields, CommandFlags.None)); - } - - [Fact] - public void HashExists() - { - wrapper.HashExists("key", "hashField", CommandFlags.None); - mock.Verify(_ => _.HashExists("prefix:key", "hashField", CommandFlags.None)); - } - - [Fact] - public void HashGet_1() - { - wrapper.HashGet("key", "hashField", CommandFlags.None); - mock.Verify(_ => _.HashGet("prefix:key", "hashField", CommandFlags.None)); - } - - [Fact] - public void HashGet_2() - { - RedisValue[] hashFields = Array.Empty(); - wrapper.HashGet("key", hashFields, CommandFlags.None); - mock.Verify(_ => _.HashGet("prefix:key", hashFields, CommandFlags.None)); - } - - [Fact] - public void HashGetAll() - { - wrapper.HashGetAll("key", CommandFlags.None); - mock.Verify(_ => _.HashGetAll("prefix:key", CommandFlags.None)); - } - - [Fact] - public void HashIncrement_1() - { - wrapper.HashIncrement("key", "hashField", 123, CommandFlags.None); - mock.Verify(_ => _.HashIncrement("prefix:key", "hashField", 123, CommandFlags.None)); - } - - [Fact] - public void HashIncrement_2() - { - wrapper.HashIncrement("key", "hashField", 1.23, CommandFlags.None); - mock.Verify(_ => _.HashIncrement("prefix:key", "hashField", 1.23, CommandFlags.None)); - } - - [Fact] - public void HashKeys() - { - wrapper.HashKeys("key", CommandFlags.None); - mock.Verify(_ => _.HashKeys("prefix:key", CommandFlags.None)); - } - - [Fact] - public void HashLength() - { - wrapper.HashLength("key", CommandFlags.None); - mock.Verify(_ => _.HashLength("prefix:key", CommandFlags.None)); - } - - [Fact] - public void HashScan() - { - wrapper.HashScan("key", "pattern", 123, flags: CommandFlags.None); - mock.Verify(_ => _.HashScan("prefix:key", "pattern", 123, CommandFlags.None)); - } - - [Fact] - public void HashScan_Full() - { - wrapper.HashScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); - mock.Verify(_ => _.HashScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None)); - } - - [Fact] - public void HashSet_1() - { - HashEntry[] hashFields = Array.Empty(); - wrapper.HashSet("key", hashFields, CommandFlags.None); - mock.Verify(_ => _.HashSet("prefix:key", hashFields, CommandFlags.None)); - } - - [Fact] - public void HashSet_2() - { - wrapper.HashSet("key", "hashField", "value", When.Exists, CommandFlags.None); - mock.Verify(_ => _.HashSet("prefix:key", "hashField", "value", When.Exists, CommandFlags.None)); - } - - [Fact] - public void HashStringLength() - { - wrapper.HashStringLength("key","field", CommandFlags.None); - mock.Verify(_ => _.HashStringLength("prefix:key", "field", CommandFlags.None)); - } - - [Fact] - public void HashValues() - { - wrapper.HashValues("key", CommandFlags.None); - mock.Verify(_ => _.HashValues("prefix:key", CommandFlags.None)); - } - - [Fact] - public void HyperLogLogAdd_1() - { - wrapper.HyperLogLogAdd("key", "value", CommandFlags.None); - mock.Verify(_ => _.HyperLogLogAdd("prefix:key", "value", CommandFlags.None)); - } - - [Fact] - public void HyperLogLogAdd_2() - { - RedisValue[] values = Array.Empty(); - wrapper.HyperLogLogAdd("key", values, CommandFlags.None); - mock.Verify(_ => _.HyperLogLogAdd("prefix:key", values, CommandFlags.None)); - } - - [Fact] - public void HyperLogLogLength() - { - wrapper.HyperLogLogLength("key", CommandFlags.None); - mock.Verify(_ => _.HyperLogLogLength("prefix:key", CommandFlags.None)); - } - - [Fact] - public void HyperLogLogMerge_1() - { - wrapper.HyperLogLogMerge("destination", "first", "second", CommandFlags.None); - mock.Verify(_ => _.HyperLogLogMerge("prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); - } - - [Fact] - public void HyperLogLogMerge_2() - { - RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.HyperLogLogMerge("destination", keys, CommandFlags.None); - mock.Verify(_ => _.HyperLogLogMerge("prefix:destination", It.Is(valid), CommandFlags.None)); - } - - [Fact] - public void IdentifyEndpoint() - { - wrapper.IdentifyEndpoint("key", CommandFlags.None); - mock.Verify(_ => _.IdentifyEndpoint("prefix:key", CommandFlags.None)); - } - - [Fact] - public void KeyCopy() - { - wrapper.KeyCopy("key", "destination", flags: CommandFlags.None); - mock.Verify(_ => _.KeyCopy("prefix:key", "prefix:destination", -1, false, CommandFlags.None)); - } - - [Fact] - public void KeyDelete_1() - { - wrapper.KeyDelete("key", CommandFlags.None); - mock.Verify(_ => _.KeyDelete("prefix:key", CommandFlags.None)); - } - - [Fact] - public void KeyDelete_2() - { - RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.KeyDelete(keys, CommandFlags.None); - mock.Verify(_ => _.KeyDelete(It.Is(valid), CommandFlags.None)); - } - - [Fact] - public void KeyDump() - { - wrapper.KeyDump("key", CommandFlags.None); - mock.Verify(_ => _.KeyDump("prefix:key", CommandFlags.None)); - } - - [Fact] - public void KeyEncoding() - { - wrapper.KeyEncoding("key", CommandFlags.None); - mock.Verify(_ => _.KeyEncoding("prefix:key", CommandFlags.None)); - } - - [Fact] - public void KeyExists() - { - wrapper.KeyExists("key", CommandFlags.None); - mock.Verify(_ => _.KeyExists("prefix:key", CommandFlags.None)); - } - - [Fact] - public void KeyExpire_1() - { - TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.KeyExpire("key", expiry, CommandFlags.None); - mock.Verify(_ => _.KeyExpire("prefix:key", expiry, CommandFlags.None)); - } - - [Fact] - public void KeyExpire_2() - { - DateTime expiry = DateTime.Now; - wrapper.KeyExpire("key", expiry, CommandFlags.None); - mock.Verify(_ => _.KeyExpire("prefix:key", expiry, CommandFlags.None)); - } - - [Fact] - public void KeyExpire_3() - { - TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.KeyExpire("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); - mock.Verify(_ => _.KeyExpire("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); - } - - [Fact] - public void KeyExpire_4() - { - DateTime expiry = DateTime.Now; - wrapper.KeyExpire("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); - mock.Verify(_ => _.KeyExpire("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); - } - - [Fact] - public void KeyExpireTime() - { - wrapper.KeyExpireTime("key", CommandFlags.None); - mock.Verify(_ => _.KeyExpireTime("prefix:key", CommandFlags.None)); - } - - [Fact] - public void KeyMigrate() - { - EndPoint toServer = new IPEndPoint(IPAddress.Loopback, 123); - wrapper.KeyMigrate("key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None); - mock.Verify(_ => _.KeyMigrate("prefix:key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None)); - } - - [Fact] - public void KeyMove() - { - wrapper.KeyMove("key", 123, CommandFlags.None); - mock.Verify(_ => _.KeyMove("prefix:key", 123, CommandFlags.None)); - } - - [Fact] - public void KeyPersist() - { - wrapper.KeyPersist("key", CommandFlags.None); - mock.Verify(_ => _.KeyPersist("prefix:key", CommandFlags.None)); - } - - [Fact] - public void KeyRandom() - { - Assert.Throws(() => wrapper.KeyRandom()); - } - - [Fact] - public void KeyRefCount() - { - wrapper.KeyRefCount("key", CommandFlags.None); - mock.Verify(_ => _.KeyRefCount("prefix:key", CommandFlags.None)); - } - - [Fact] - public void KeyRename() - { - wrapper.KeyRename("key", "newKey", When.Exists, CommandFlags.None); - mock.Verify(_ => _.KeyRename("prefix:key", "prefix:newKey", When.Exists, CommandFlags.None)); - } - - [Fact] - public void KeyRestore() - { - byte[] value = Array.Empty(); - TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.KeyRestore("key", value, expiry, CommandFlags.None); - mock.Verify(_ => _.KeyRestore("prefix:key", value, expiry, CommandFlags.None)); - } - - [Fact] - public void KeyTimeToLive() - { - wrapper.KeyTimeToLive("key", CommandFlags.None); - mock.Verify(_ => _.KeyTimeToLive("prefix:key", CommandFlags.None)); - } - - [Fact] - public void KeyType() - { - wrapper.KeyType("key", CommandFlags.None); - mock.Verify(_ => _.KeyType("prefix:key", CommandFlags.None)); - } - - [Fact] - public void ListGetByIndex() - { - wrapper.ListGetByIndex("key", 123, CommandFlags.None); - mock.Verify(_ => _.ListGetByIndex("prefix:key", 123, CommandFlags.None)); - } - - [Fact] - public void ListInsertAfter() - { - wrapper.ListInsertAfter("key", "pivot", "value", CommandFlags.None); - mock.Verify(_ => _.ListInsertAfter("prefix:key", "pivot", "value", CommandFlags.None)); - } - - [Fact] - public void ListInsertBefore() - { - wrapper.ListInsertBefore("key", "pivot", "value", CommandFlags.None); - mock.Verify(_ => _.ListInsertBefore("prefix:key", "pivot", "value", CommandFlags.None)); - } - - [Fact] - public void ListLeftPop() - { - wrapper.ListLeftPop("key", CommandFlags.None); - mock.Verify(_ => _.ListLeftPop("prefix:key", CommandFlags.None)); - } - - [Fact] - public void ListLeftPop_1() - { - wrapper.ListLeftPop("key", 123, CommandFlags.None); - mock.Verify(_ => _.ListLeftPop("prefix:key", 123, CommandFlags.None)); - } - - [Fact] - public void ListLeftPush_1() - { - wrapper.ListLeftPush("key", "value", When.Exists, CommandFlags.None); - mock.Verify(_ => _.ListLeftPush("prefix:key", "value", When.Exists, CommandFlags.None)); - } - - [Fact] - public void ListLeftPush_2() - { - RedisValue[] values = Array.Empty(); - wrapper.ListLeftPush("key", values, CommandFlags.None); - mock.Verify(_ => _.ListLeftPush("prefix:key", values, CommandFlags.None)); - } - - [Fact] - public void ListLeftPush_3() - { - RedisValue[] values = new RedisValue[] { "value1", "value2" }; - wrapper.ListLeftPush("key", values, When.Exists, CommandFlags.None); - mock.Verify(_ => _.ListLeftPush("prefix:key", values, When.Exists, CommandFlags.None)); - } - - [Fact] - public void ListLength() - { - wrapper.ListLength("key", CommandFlags.None); - mock.Verify(_ => _.ListLength("prefix:key", CommandFlags.None)); - } - - [Fact] - public void ListMove() - { - wrapper.ListMove("key", "destination", ListSide.Left, ListSide.Right, CommandFlags.None); - mock.Verify(_ => _.ListMove("prefix:key", "prefix:destination", ListSide.Left, ListSide.Right, CommandFlags.None)); - } - - [Fact] - public void ListRange() - { - wrapper.ListRange("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.ListRange("prefix:key", 123, 456, CommandFlags.None)); - } - - [Fact] - public void ListRemove() - { - wrapper.ListRemove("key", "value", 123, CommandFlags.None); - mock.Verify(_ => _.ListRemove("prefix:key", "value", 123, CommandFlags.None)); - } - - [Fact] - public void ListRightPop() - { - wrapper.ListRightPop("key", CommandFlags.None); - mock.Verify(_ => _.ListRightPop("prefix:key", CommandFlags.None)); - } - - [Fact] - public void ListRightPop_1() - { - wrapper.ListRightPop("key", 123, CommandFlags.None); - mock.Verify(_ => _.ListRightPop("prefix:key", 123, CommandFlags.None)); - } - - [Fact] - public void ListRightPopLeftPush() - { - wrapper.ListRightPopLeftPush("source", "destination", CommandFlags.None); - mock.Verify(_ => _.ListRightPopLeftPush("prefix:source", "prefix:destination", CommandFlags.None)); - } - - [Fact] - public void ListRightPush_1() - { - wrapper.ListRightPush("key", "value", When.Exists, CommandFlags.None); - mock.Verify(_ => _.ListRightPush("prefix:key", "value", When.Exists, CommandFlags.None)); - } - - [Fact] - public void ListRightPush_2() - { - RedisValue[] values = Array.Empty(); - wrapper.ListRightPush("key", values, CommandFlags.None); - mock.Verify(_ => _.ListRightPush("prefix:key", values, CommandFlags.None)); - } - - [Fact] - public void ListRightPush_3() - { - RedisValue[] values = new RedisValue[] { "value1", "value2" }; - wrapper.ListRightPush("key", values, When.Exists, CommandFlags.None); - mock.Verify(_ => _.ListRightPush("prefix:key", values, When.Exists, CommandFlags.None)); - } - - [Fact] - public void ListSetByIndex() - { - wrapper.ListSetByIndex("key", 123, "value", CommandFlags.None); - mock.Verify(_ => _.ListSetByIndex("prefix:key", 123, "value", CommandFlags.None)); - } - - [Fact] - public void ListTrim() - { - wrapper.ListTrim("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.ListTrim("prefix:key", 123, 456, CommandFlags.None)); - } - - [Fact] - public void LockExtend() - { - TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.LockExtend("key", "value", expiry, CommandFlags.None); - mock.Verify(_ => _.LockExtend("prefix:key", "value", expiry, CommandFlags.None)); - } - - [Fact] - public void LockQuery() - { - wrapper.LockQuery("key", CommandFlags.None); - mock.Verify(_ => _.LockQuery("prefix:key", CommandFlags.None)); - } - - [Fact] - public void LockRelease() - { - wrapper.LockRelease("key", "value", CommandFlags.None); - mock.Verify(_ => _.LockRelease("prefix:key", "value", CommandFlags.None)); - } - - [Fact] - public void LockTake() - { - TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.LockTake("key", "value", expiry, CommandFlags.None); - mock.Verify(_ => _.LockTake("prefix:key", "value", expiry, CommandFlags.None)); - } - - [Fact] - public void Publish() - { - wrapper.Publish("channel", "message", CommandFlags.None); - mock.Verify(_ => _.Publish("prefix:channel", "message", CommandFlags.None)); - } - - [Fact] - public void ScriptEvaluate_1() - { - byte[] hash = Array.Empty(); - RedisValue[] values = Array.Empty(); - RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.ScriptEvaluate(hash, keys, values, CommandFlags.None); - mock.Verify(_ => _.ScriptEvaluate(hash, It.Is(valid), values, CommandFlags.None)); - } - - [Fact] - public void ScriptEvaluate_2() - { - RedisValue[] values = Array.Empty(); - RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.ScriptEvaluate("script", keys, values, CommandFlags.None); - mock.Verify(_ => _.ScriptEvaluate("script", It.Is(valid), values, CommandFlags.None)); - } - - [Fact] - public void SetAdd_1() - { - wrapper.SetAdd("key", "value", CommandFlags.None); - mock.Verify(_ => _.SetAdd("prefix:key", "value", CommandFlags.None)); - } - - [Fact] - public void SetAdd_2() - { - RedisValue[] values = Array.Empty(); - wrapper.SetAdd("key", values, CommandFlags.None); - mock.Verify(_ => _.SetAdd("prefix:key", values, CommandFlags.None)); - } - - [Fact] - public void SetCombine_1() - { - wrapper.SetCombine(SetOperation.Intersect, "first", "second", CommandFlags.None); - mock.Verify(_ => _.SetCombine(SetOperation.Intersect, "prefix:first", "prefix:second", CommandFlags.None)); - } - - [Fact] - public void SetCombine_2() - { - RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.SetCombine(SetOperation.Intersect, keys, CommandFlags.None); - mock.Verify(_ => _.SetCombine(SetOperation.Intersect, It.Is(valid), CommandFlags.None)); - } - - [Fact] - public void SetCombineAndStore_1() - { - wrapper.SetCombineAndStore(SetOperation.Intersect, "destination", "first", "second", CommandFlags.None); - mock.Verify(_ => _.SetCombineAndStore(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); - } - - [Fact] - public void SetCombineAndStore_2() - { - RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None); - mock.Verify(_ => _.SetCombineAndStore(SetOperation.Intersect, "prefix:destination", It.Is(valid), CommandFlags.None)); - } - - [Fact] - public void SetContains() - { - wrapper.SetContains("key", "value", CommandFlags.None); - mock.Verify(_ => _.SetContains("prefix:key", "value", CommandFlags.None)); - } - - [Fact] - public void SetContains_2() - { - RedisValue[] values = new RedisValue[] { "value1", "value2" }; - wrapper.SetContains("key", values, CommandFlags.None); - mock.Verify(_ => _.SetContains("prefix:key", values, CommandFlags.None)); - } - - [Fact] - public void SetIntersectionLength() - { - var keys = new RedisKey[] { "key1", "key2" }; - wrapper.SetIntersectionLength(keys); - mock.Verify(_ => _.SetIntersectionLength(keys, 0, CommandFlags.None)); - } - - [Fact] - public void SetLength() - { - wrapper.SetLength("key", CommandFlags.None); - mock.Verify(_ => _.SetLength("prefix:key", CommandFlags.None)); - } - - [Fact] - public void SetMembers() - { - wrapper.SetMembers("key", CommandFlags.None); - mock.Verify(_ => _.SetMembers("prefix:key", CommandFlags.None)); - } - - [Fact] - public void SetMove() - { - wrapper.SetMove("source", "destination", "value", CommandFlags.None); - mock.Verify(_ => _.SetMove("prefix:source", "prefix:destination", "value", CommandFlags.None)); - } - - [Fact] - public void SetPop_1() - { - wrapper.SetPop("key", CommandFlags.None); - mock.Verify(_ => _.SetPop("prefix:key", CommandFlags.None)); - - wrapper.SetPop("key", 5, CommandFlags.None); - mock.Verify(_ => _.SetPop("prefix:key", 5, CommandFlags.None)); - } - - [Fact] - public void SetPop_2() - { - wrapper.SetPop("key", 5, CommandFlags.None); - mock.Verify(_ => _.SetPop("prefix:key", 5, CommandFlags.None)); - } - - [Fact] - public void SetRandomMember() - { - wrapper.SetRandomMember("key", CommandFlags.None); - mock.Verify(_ => _.SetRandomMember("prefix:key", CommandFlags.None)); - } - - [Fact] - public void SetRandomMembers() - { - wrapper.SetRandomMembers("key", 123, CommandFlags.None); - mock.Verify(_ => _.SetRandomMembers("prefix:key", 123, CommandFlags.None)); - } - - [Fact] - public void SetRemove_1() - { - wrapper.SetRemove("key", "value", CommandFlags.None); - mock.Verify(_ => _.SetRemove("prefix:key", "value", CommandFlags.None)); - } - - [Fact] - public void SetRemove_2() - { - RedisValue[] values = Array.Empty(); - wrapper.SetRemove("key", values, CommandFlags.None); - mock.Verify(_ => _.SetRemove("prefix:key", values, CommandFlags.None)); - } - - [Fact] - public void SetScan() - { - wrapper.SetScan("key", "pattern", 123, flags: CommandFlags.None); - mock.Verify(_ => _.SetScan("prefix:key", "pattern", 123, CommandFlags.None)); - } - - [Fact] - public void SetScan_Full() - { - wrapper.SetScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); - mock.Verify(_ => _.SetScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None)); - } - - [Fact] - public void Sort() - { - RedisValue[] get = new RedisValue[] { "a", "#" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; - - wrapper.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); - wrapper.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); - - mock.Verify(_ => _.Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", It.Is(valid), CommandFlags.None)); - mock.Verify(_ => _.Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", It.Is(valid), CommandFlags.None)); - } - - [Fact] - public void SortAndStore() - { - RedisValue[] get = new RedisValue[] { "a", "#" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; - - wrapper.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); - wrapper.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); - - mock.Verify(_ => _.SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", It.Is(valid), CommandFlags.None)); - mock.Verify(_ => _.SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", It.Is(valid), CommandFlags.None)); - } - - [Fact] - public void SortedSetAdd_1() - { - wrapper.SortedSetAdd("key", "member", 1.23, When.Exists, CommandFlags.None); - mock.Verify(_ => _.SortedSetAdd("prefix:key", "member", 1.23, When.Exists, CommandFlags.None)); - } - - [Fact] - public void SortedSetAdd_2() - { - SortedSetEntry[] values = Array.Empty(); - wrapper.SortedSetAdd("key", values, When.Exists, CommandFlags.None); - mock.Verify(_ => _.SortedSetAdd("prefix:key", values, When.Exists, CommandFlags.None)); - } - - [Fact] - public void SortedSetCombine() - { - RedisKey[] keys = new RedisKey[] { "a", "b" }; - wrapper.SortedSetCombine(SetOperation.Intersect, keys); - mock.Verify(_ => _.SortedSetCombine(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); - } - - [Fact] - public void SortedSetCombineWithScores() - { - RedisKey[] keys = new RedisKey[] { "a", "b" }; - wrapper.SortedSetCombineWithScores(SetOperation.Intersect, keys); - mock.Verify(_ => _.SortedSetCombineWithScores(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); - } - - [Fact] - public void SortedSetCombineAndStore_1() - { - wrapper.SortedSetCombineAndStore(SetOperation.Intersect, "destination", "first", "second", Aggregate.Max, CommandFlags.None); - mock.Verify(_ => _.SortedSetCombineAndStore(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", Aggregate.Max, CommandFlags.None)); - } - - [Fact] - public void SortedSetCombineAndStore_2() - { - RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None); - mock.Verify(_ => _.SetCombineAndStore(SetOperation.Intersect, "prefix:destination", It.Is(valid), CommandFlags.None)); - } - - [Fact] - public void SortedSetDecrement() - { - wrapper.SortedSetDecrement("key", "member", 1.23, CommandFlags.None); - mock.Verify(_ => _.SortedSetDecrement("prefix:key", "member", 1.23, CommandFlags.None)); - } - - [Fact] - public void SortedSetIncrement() - { - wrapper.SortedSetIncrement("key", "member", 1.23, CommandFlags.None); - mock.Verify(_ => _.SortedSetIncrement("prefix:key", "member", 1.23, CommandFlags.None)); - } - - [Fact] - public void SortedSetIntersectionLength() - { - RedisKey[] keys = new RedisKey[] { "a", "b" }; - wrapper.SortedSetIntersectionLength(keys, 1, CommandFlags.None); - mock.Verify(_ => _.SortedSetIntersectionLength(keys, 1, CommandFlags.None)); - } - - [Fact] - public void SortedSetLength() - { - wrapper.SortedSetLength("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); - mock.Verify(_ => _.SortedSetLength("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None)); - } - - [Fact] - public void SortedSetRandomMember() - { - wrapper.SortedSetRandomMember("key", CommandFlags.None); - mock.Verify(_ => _.SortedSetRandomMember("prefix:key", CommandFlags.None)); - } - - [Fact] - public void SortedSetRandomMembers() - { - wrapper.SortedSetRandomMembers("key", 2, CommandFlags.None); - mock.Verify(_ => _.SortedSetRandomMembers("prefix:key", 2, CommandFlags.None)); - } - - [Fact] - public void SortedSetRandomMembersWithScores() - { - wrapper.SortedSetRandomMembersWithScores("key", 2, CommandFlags.None); - mock.Verify(_ => _.SortedSetRandomMembersWithScores("prefix:key", 2, CommandFlags.None)); - } - - [Fact] - public void SortedSetLengthByValue() - { - wrapper.SortedSetLengthByValue("key", "min", "max", Exclude.Start, CommandFlags.None); - mock.Verify(_ => _.SortedSetLengthByValue("prefix:key", "min", "max", Exclude.Start, CommandFlags.None)); - } - - [Fact] - public void SortedSetRangeByRank() - { - wrapper.SortedSetRangeByRank("key", 123, 456, Order.Descending, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByRank("prefix:key", 123, 456, Order.Descending, CommandFlags.None)); - } - - [Fact] - public void SortedSetRangeByRankWithScores() - { - wrapper.SortedSetRangeByRankWithScores("key", 123, 456, Order.Descending, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByRankWithScores("prefix:key", 123, 456, Order.Descending, CommandFlags.None)); - } - - [Fact] - public void SortedSetRangeByScore() - { - wrapper.SortedSetRangeByScore("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByScore("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); - } - - [Fact] - public void SortedSetRangeByScoreWithScores() - { - wrapper.SortedSetRangeByScoreWithScores("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByScoreWithScores("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); - } - - [Fact] - public void SortedSetRangeByValue() - { - wrapper.SortedSetRangeByValue("key", "min", "max", Exclude.Start, 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByValue("prefix:key", "min", "max", Exclude.Start, Order.Ascending, 123, 456, CommandFlags.None)); - } - - [Fact] - public void SortedSetRangeByValueDesc() - { - wrapper.SortedSetRangeByValue("key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByValue("prefix:key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); - } - - [Fact] - public void SortedSetRank() - { - wrapper.SortedSetRank("key", "member", Order.Descending, CommandFlags.None); - mock.Verify(_ => _.SortedSetRank("prefix:key", "member", Order.Descending, CommandFlags.None)); - } - - [Fact] - public void SortedSetRemove_1() - { - wrapper.SortedSetRemove("key", "member", CommandFlags.None); - mock.Verify(_ => _.SortedSetRemove("prefix:key", "member", CommandFlags.None)); - } - - [Fact] - public void SortedSetRemove_2() - { - RedisValue[] members = Array.Empty(); - wrapper.SortedSetRemove("key", members, CommandFlags.None); - mock.Verify(_ => _.SortedSetRemove("prefix:key", members, CommandFlags.None)); - } - - [Fact] - public void SortedSetRemoveRangeByRank() - { - wrapper.SortedSetRemoveRangeByRank("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRemoveRangeByRank("prefix:key", 123, 456, CommandFlags.None)); - } - - [Fact] - public void SortedSetRemoveRangeByScore() - { - wrapper.SortedSetRemoveRangeByScore("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); - mock.Verify(_ => _.SortedSetRemoveRangeByScore("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None)); - } - - [Fact] - public void SortedSetRemoveRangeByValue() - { - wrapper.SortedSetRemoveRangeByValue("key", "min", "max", Exclude.Start, CommandFlags.None); - mock.Verify(_ => _.SortedSetRemoveRangeByValue("prefix:key", "min", "max", Exclude.Start, CommandFlags.None)); - } - - [Fact] - public void SortedSetScan() - { - wrapper.SortedSetScan("key", "pattern", 123, flags: CommandFlags.None); - mock.Verify(_ => _.SortedSetScan("prefix:key", "pattern", 123, CommandFlags.None)); - } - - [Fact] - public void SortedSetScan_Full() - { - wrapper.SortedSetScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); - mock.Verify(_ => _.SortedSetScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None)); - } - - [Fact] - public void SortedSetScore() - { - wrapper.SortedSetScore("key", "member", CommandFlags.None); - mock.Verify(_ => _.SortedSetScore("prefix:key", "member", CommandFlags.None)); - } - - [Fact] - public void SortedSetScore_Multiple() - { - wrapper.SortedSetScores("key", new RedisValue[] { "member1", "member2" }, CommandFlags.None); - mock.Verify(_ => _.SortedSetScores("prefix:key", new RedisValue[] { "member1", "member2" }, CommandFlags.None)); - } - - [Fact] - public void StreamAcknowledge_1() - { - wrapper.StreamAcknowledge("key", "group", "0-0", CommandFlags.None); - mock.Verify(_ => _.StreamAcknowledge("prefix:key", "group", "0-0", CommandFlags.None)); - } - - [Fact] - public void StreamAcknowledge_2() - { - var messageIds = new RedisValue[] { "0-0", "0-1", "0-2" }; - wrapper.StreamAcknowledge("key", "group", messageIds, CommandFlags.None); - mock.Verify(_ => _.StreamAcknowledge("prefix:key", "group", messageIds, CommandFlags.None)); - } - - [Fact] - public void StreamAdd_1() - { - wrapper.StreamAdd("key", "field1", "value1", "*", 1000, true, CommandFlags.None); - mock.Verify(_ => _.StreamAdd("prefix:key", "field1", "value1", "*", 1000, true, CommandFlags.None)); - } - - [Fact] - public void StreamAdd_2() - { - var fields = Array.Empty(); - wrapper.StreamAdd("key", fields, "*", 1000, true, CommandFlags.None); - mock.Verify(_ => _.StreamAdd("prefix:key", fields, "*", 1000, true, CommandFlags.None)); - } - - [Fact] - public void StreamClaimMessages() - { - var messageIds = Array.Empty(); - wrapper.StreamClaim("key", "group", "consumer", 1000, messageIds, CommandFlags.None); - mock.Verify(_ => _.StreamClaim("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); - } - - [Fact] - public void StreamClaimMessagesReturningIds() - { - var messageIds = Array.Empty(); - wrapper.StreamClaimIdsOnly("key", "group", "consumer", 1000, messageIds, CommandFlags.None); - mock.Verify(_ => _.StreamClaimIdsOnly("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); - } - - [Fact] - public void StreamConsumerGroupSetPosition() - { - wrapper.StreamConsumerGroupSetPosition("key", "group", StreamPosition.Beginning, CommandFlags.None); - mock.Verify(_ => _.StreamConsumerGroupSetPosition("prefix:key", "group", StreamPosition.Beginning, CommandFlags.None)); - } - - [Fact] - public void StreamConsumerInfoGet() - { - wrapper.StreamConsumerInfo("key", "group", CommandFlags.None); - mock.Verify(_ => _.StreamConsumerInfo("prefix:key", "group", CommandFlags.None)); - } - - [Fact] - public void StreamCreateConsumerGroup() - { - wrapper.StreamCreateConsumerGroup("key", "group", StreamPosition.Beginning, false, CommandFlags.None); - mock.Verify(_ => _.StreamCreateConsumerGroup("prefix:key", "group", StreamPosition.Beginning, false, CommandFlags.None)); - } - - [Fact] - public void StreamGroupInfoGet() - { - wrapper.StreamGroupInfo("key", CommandFlags.None); - mock.Verify(_ => _.StreamGroupInfo("prefix:key", CommandFlags.None)); - } - - [Fact] - public void StreamInfoGet() - { - wrapper.StreamInfo("key", CommandFlags.None); - mock.Verify(_ => _.StreamInfo("prefix:key", CommandFlags.None)); - } - - [Fact] - public void StreamLength() - { - wrapper.StreamLength("key", CommandFlags.None); - mock.Verify(_ => _.StreamLength("prefix:key", CommandFlags.None)); - } - - [Fact] - public void StreamMessagesDelete() - { - var messageIds = Array.Empty(); - wrapper.StreamDelete("key", messageIds, CommandFlags.None); - mock.Verify(_ => _.StreamDelete("prefix:key", messageIds, CommandFlags.None)); - } - - [Fact] - public void StreamDeleteConsumer() - { - wrapper.StreamDeleteConsumer("key", "group", "consumer", CommandFlags.None); - mock.Verify(_ => _.StreamDeleteConsumer("prefix:key", "group", "consumer", CommandFlags.None)); - } - - [Fact] - public void StreamDeleteConsumerGroup() - { - wrapper.StreamDeleteConsumerGroup("key", "group", CommandFlags.None); - mock.Verify(_ => _.StreamDeleteConsumerGroup("prefix:key", "group", CommandFlags.None)); - } - - [Fact] - public void StreamPendingInfoGet() - { - wrapper.StreamPending("key", "group", CommandFlags.None); - mock.Verify(_ => _.StreamPending("prefix:key", "group", CommandFlags.None)); - } - - [Fact] - public void StreamPendingMessageInfoGet() - { - wrapper.StreamPendingMessages("key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); - mock.Verify(_ => _.StreamPendingMessages("prefix:key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None)); - } - - [Fact] - public void StreamRange() - { - wrapper.StreamRange("key", "-", "+", null, Order.Ascending, CommandFlags.None); - mock.Verify(_ => _.StreamRange("prefix:key", "-", "+", null, Order.Ascending, CommandFlags.None)); - } - - [Fact] - public void StreamRead_1() - { - var streamPositions = Array.Empty(); - wrapper.StreamRead(streamPositions, null, CommandFlags.None); - mock.Verify(_ => _.StreamRead(streamPositions, null, CommandFlags.None)); - } - - [Fact] - public void StreamRead_2() - { - wrapper.StreamRead("key", "0-0", null, CommandFlags.None); - mock.Verify(_ => _.StreamRead("prefix:key", "0-0", null, CommandFlags.None)); - } - - [Fact] - public void StreamStreamReadGroup_1() - { - wrapper.StreamReadGroup("key", "group", "consumer", "0-0", 10, false, CommandFlags.None); - mock.Verify(_ => _.StreamReadGroup("prefix:key", "group", "consumer", "0-0", 10, false, CommandFlags.None)); - } - - [Fact] - public void StreamStreamReadGroup_2() - { - var streamPositions = Array.Empty(); - wrapper.StreamReadGroup(streamPositions, "group", "consumer", 10, false, CommandFlags.None); - mock.Verify(_ => _.StreamReadGroup(streamPositions, "group", "consumer", 10, false, CommandFlags.None)); - } - - [Fact] - public void StreamTrim() - { - wrapper.StreamTrim("key", 1000, true, CommandFlags.None); - mock.Verify(_ => _.StreamTrim("prefix:key", 1000, true, CommandFlags.None)); - } - - [Fact] - public void StringAppend() - { - wrapper.StringAppend("key", "value", CommandFlags.None); - mock.Verify(_ => _.StringAppend("prefix:key", "value", CommandFlags.None)); - } - - [Fact] - public void StringBitCount() - { - wrapper.StringBitCount("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.StringBitCount("prefix:key", 123, 456, CommandFlags.None)); - } - - [Fact] - public void StringBitOperation_1() - { - wrapper.StringBitOperation(Bitwise.Xor, "destination", "first", "second", CommandFlags.None); - mock.Verify(_ => _.StringBitOperation(Bitwise.Xor, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); - } - - [Fact] - public void StringBitOperation_2() - { - RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.StringBitOperation(Bitwise.Xor, "destination", keys, CommandFlags.None); - mock.Verify(_ => _.StringBitOperation(Bitwise.Xor, "prefix:destination", It.Is(valid), CommandFlags.None)); - } - - [Fact] - public void StringBitPosition() - { - wrapper.StringBitPosition("key", true, 123, 456, CommandFlags.None); - mock.Verify(_ => _.StringBitPosition("prefix:key", true, 123, 456, CommandFlags.None)); - } - - [Fact] - public void StringDecrement_1() - { - wrapper.StringDecrement("key", 123, CommandFlags.None); - mock.Verify(_ => _.StringDecrement("prefix:key", 123, CommandFlags.None)); - } - - [Fact] - public void StringDecrement_2() - { - wrapper.StringDecrement("key", 1.23, CommandFlags.None); - mock.Verify(_ => _.StringDecrement("prefix:key", 1.23, CommandFlags.None)); - } - - [Fact] - public void StringGet_1() - { - wrapper.StringGet("key", CommandFlags.None); - mock.Verify(_ => _.StringGet("prefix:key", CommandFlags.None)); - } - - [Fact] - public void StringGet_2() - { - RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.StringGet(keys, CommandFlags.None); - mock.Verify(_ => _.StringGet(It.Is(valid), CommandFlags.None)); - } - - [Fact] - public void StringGetBit() - { - wrapper.StringGetBit("key", 123, CommandFlags.None); - mock.Verify(_ => _.StringGetBit("prefix:key", 123, CommandFlags.None)); - } - - [Fact] - public void StringGetRange() - { - wrapper.StringGetRange("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.StringGetRange("prefix:key", 123, 456, CommandFlags.None)); - } - - [Fact] - public void StringGetSet() - { - wrapper.StringGetSet("key", "value", CommandFlags.None); - mock.Verify(_ => _.StringGetSet("prefix:key", "value", CommandFlags.None)); - } - - [Fact] - public void StringGetDelete() - { - wrapper.StringGetDelete("key", CommandFlags.None); - mock.Verify(_ => _.StringGetDelete("prefix:key", CommandFlags.None)); - } - - [Fact] - public void StringGetWithExpiry() - { - wrapper.StringGetWithExpiry("key", CommandFlags.None); - mock.Verify(_ => _.StringGetWithExpiry("prefix:key", CommandFlags.None)); - } - - [Fact] - public void StringIncrement_1() - { - wrapper.StringIncrement("key", 123, CommandFlags.None); - mock.Verify(_ => _.StringIncrement("prefix:key", 123, CommandFlags.None)); - } - - [Fact] - public void StringIncrement_2() - { - wrapper.StringIncrement("key", 1.23, CommandFlags.None); - mock.Verify(_ => _.StringIncrement("prefix:key", 1.23, CommandFlags.None)); - } - - [Fact] - public void StringLength() - { - wrapper.StringLength("key", CommandFlags.None); - mock.Verify(_ => _.StringLength("prefix:key", CommandFlags.None)); - } - - [Fact] - public void StringSet_1() - { - TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.StringSet("key", "value", expiry, When.Exists, CommandFlags.None); - mock.Verify(_ => _.StringSet("prefix:key", "value", expiry, When.Exists, CommandFlags.None)); - } - - [Fact] - public void StringSet_2() - { - TimeSpan? expiry = null; - wrapper.StringSet("key", "value", expiry, true, When.Exists, CommandFlags.None); - mock.Verify(_ => _.StringSet("prefix:key", "value", expiry, true, When.Exists, CommandFlags.None)); - } - - [Fact] - public void StringSet_3() - { - KeyValuePair[] values = new KeyValuePair[] { new KeyValuePair("a", "x"), new KeyValuePair("b", "y") }; - Expression[], bool>> valid = _ => _.Length == 2 && _[0].Key == "prefix:a" && _[0].Value == "x" && _[1].Key == "prefix:b" && _[1].Value == "y"; - wrapper.StringSet(values, When.Exists, CommandFlags.None); - mock.Verify(_ => _.StringSet(It.Is(valid), When.Exists, CommandFlags.None)); - } - - [Fact] - public void StringSetBit() - { - wrapper.StringSetBit("key", 123, true, CommandFlags.None); - mock.Verify(_ => _.StringSetBit("prefix:key", 123, true, CommandFlags.None)); - } - - [Fact] - public void StringSetRange() - { - wrapper.StringSetRange("key", 123, "value", CommandFlags.None); - mock.Verify(_ => _.StringSetRange("prefix:key", 123, "value", CommandFlags.None)); - } + private readonly Mock mock; + private readonly IDatabase wrapper; + + public DatabaseWrapperTests() + { + mock = new Mock(); + wrapper = new DatabaseWrapper(mock.Object, Encoding.UTF8.GetBytes("prefix:")); + } + + [Fact] + public void CreateBatch() + { + object asyncState = new(); + IBatch innerBatch = new Mock().Object; + mock.Setup(_ => _.CreateBatch(asyncState)).Returns(innerBatch); + IBatch wrappedBatch = wrapper.CreateBatch(asyncState); + mock.Verify(_ => _.CreateBatch(asyncState)); + Assert.IsType(wrappedBatch); + Assert.Same(innerBatch, ((BatchWrapper)wrappedBatch).Inner); + } + + [Fact] + public void CreateTransaction() + { + object asyncState = new(); + ITransaction innerTransaction = new Mock().Object; + mock.Setup(_ => _.CreateTransaction(asyncState)).Returns(innerTransaction); + ITransaction wrappedTransaction = wrapper.CreateTransaction(asyncState); + mock.Verify(_ => _.CreateTransaction(asyncState)); + Assert.IsType(wrappedTransaction); + Assert.Same(innerTransaction, ((TransactionWrapper)wrappedTransaction).Inner); + } + + [Fact] + public void DebugObject() + { + wrapper.DebugObject("key", CommandFlags.None); + mock.Verify(_ => _.DebugObject("prefix:key", CommandFlags.None)); + } + + [Fact] + public void Get_Database() + { + mock.SetupGet(_ => _.Database).Returns(123); + Assert.Equal(123, wrapper.Database); + } + + [Fact] + public void HashDecrement_1() + { + wrapper.HashDecrement("key", "hashField", 123, CommandFlags.None); + mock.Verify(_ => _.HashDecrement("prefix:key", "hashField", 123, CommandFlags.None)); + } + + [Fact] + public void HashDecrement_2() + { + wrapper.HashDecrement("key", "hashField", 1.23, CommandFlags.None); + mock.Verify(_ => _.HashDecrement("prefix:key", "hashField", 1.23, CommandFlags.None)); + } + + [Fact] + public void HashDelete_1() + { + wrapper.HashDelete("key", "hashField", CommandFlags.None); + mock.Verify(_ => _.HashDelete("prefix:key", "hashField", CommandFlags.None)); + } + + [Fact] + public void HashDelete_2() + { + RedisValue[] hashFields = Array.Empty(); + wrapper.HashDelete("key", hashFields, CommandFlags.None); + mock.Verify(_ => _.HashDelete("prefix:key", hashFields, CommandFlags.None)); + } + + [Fact] + public void HashExists() + { + wrapper.HashExists("key", "hashField", CommandFlags.None); + mock.Verify(_ => _.HashExists("prefix:key", "hashField", CommandFlags.None)); + } + + [Fact] + public void HashGet_1() + { + wrapper.HashGet("key", "hashField", CommandFlags.None); + mock.Verify(_ => _.HashGet("prefix:key", "hashField", CommandFlags.None)); + } + + [Fact] + public void HashGet_2() + { + RedisValue[] hashFields = Array.Empty(); + wrapper.HashGet("key", hashFields, CommandFlags.None); + mock.Verify(_ => _.HashGet("prefix:key", hashFields, CommandFlags.None)); + } + + [Fact] + public void HashGetAll() + { + wrapper.HashGetAll("key", CommandFlags.None); + mock.Verify(_ => _.HashGetAll("prefix:key", CommandFlags.None)); + } + + [Fact] + public void HashIncrement_1() + { + wrapper.HashIncrement("key", "hashField", 123, CommandFlags.None); + mock.Verify(_ => _.HashIncrement("prefix:key", "hashField", 123, CommandFlags.None)); + } + + [Fact] + public void HashIncrement_2() + { + wrapper.HashIncrement("key", "hashField", 1.23, CommandFlags.None); + mock.Verify(_ => _.HashIncrement("prefix:key", "hashField", 1.23, CommandFlags.None)); + } + + [Fact] + public void HashKeys() + { + wrapper.HashKeys("key", CommandFlags.None); + mock.Verify(_ => _.HashKeys("prefix:key", CommandFlags.None)); + } + + [Fact] + public void HashLength() + { + wrapper.HashLength("key", CommandFlags.None); + mock.Verify(_ => _.HashLength("prefix:key", CommandFlags.None)); + } + + [Fact] + public void HashScan() + { + wrapper.HashScan("key", "pattern", 123, flags: CommandFlags.None); + mock.Verify(_ => _.HashScan("prefix:key", "pattern", 123, CommandFlags.None)); + } + + [Fact] + public void HashScan_Full() + { + wrapper.HashScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); + mock.Verify(_ => _.HashScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None)); + } + + [Fact] + public void HashSet_1() + { + HashEntry[] hashFields = Array.Empty(); + wrapper.HashSet("key", hashFields, CommandFlags.None); + mock.Verify(_ => _.HashSet("prefix:key", hashFields, CommandFlags.None)); + } + + [Fact] + public void HashSet_2() + { + wrapper.HashSet("key", "hashField", "value", When.Exists, CommandFlags.None); + mock.Verify(_ => _.HashSet("prefix:key", "hashField", "value", When.Exists, CommandFlags.None)); + } + + [Fact] + public void HashStringLength() + { + wrapper.HashStringLength("key", "field", CommandFlags.None); + mock.Verify(_ => _.HashStringLength("prefix:key", "field", CommandFlags.None)); + } + + [Fact] + public void HashValues() + { + wrapper.HashValues("key", CommandFlags.None); + mock.Verify(_ => _.HashValues("prefix:key", CommandFlags.None)); + } + + [Fact] + public void HyperLogLogAdd_1() + { + wrapper.HyperLogLogAdd("key", "value", CommandFlags.None); + mock.Verify(_ => _.HyperLogLogAdd("prefix:key", "value", CommandFlags.None)); + } + + [Fact] + public void HyperLogLogAdd_2() + { + RedisValue[] values = Array.Empty(); + wrapper.HyperLogLogAdd("key", values, CommandFlags.None); + mock.Verify(_ => _.HyperLogLogAdd("prefix:key", values, CommandFlags.None)); + } + + [Fact] + public void HyperLogLogLength() + { + wrapper.HyperLogLogLength("key", CommandFlags.None); + mock.Verify(_ => _.HyperLogLogLength("prefix:key", CommandFlags.None)); + } + + [Fact] + public void HyperLogLogMerge_1() + { + wrapper.HyperLogLogMerge("destination", "first", "second", CommandFlags.None); + mock.Verify(_ => _.HyperLogLogMerge("prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); + } + + [Fact] + public void HyperLogLogMerge_2() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + wrapper.HyperLogLogMerge("destination", keys, CommandFlags.None); + mock.Verify(_ => _.HyperLogLogMerge("prefix:destination", It.Is(valid), CommandFlags.None)); + } + + [Fact] + public void IdentifyEndpoint() + { + wrapper.IdentifyEndpoint("key", CommandFlags.None); + mock.Verify(_ => _.IdentifyEndpoint("prefix:key", CommandFlags.None)); + } + + [Fact] + public void KeyCopy() + { + wrapper.KeyCopy("key", "destination", flags: CommandFlags.None); + mock.Verify(_ => _.KeyCopy("prefix:key", "prefix:destination", -1, false, CommandFlags.None)); + } + + [Fact] + public void KeyDelete_1() + { + wrapper.KeyDelete("key", CommandFlags.None); + mock.Verify(_ => _.KeyDelete("prefix:key", CommandFlags.None)); + } + + [Fact] + public void KeyDelete_2() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + wrapper.KeyDelete(keys, CommandFlags.None); + mock.Verify(_ => _.KeyDelete(It.Is(valid), CommandFlags.None)); + } + + [Fact] + public void KeyDump() + { + wrapper.KeyDump("key", CommandFlags.None); + mock.Verify(_ => _.KeyDump("prefix:key", CommandFlags.None)); + } + + [Fact] + public void KeyEncoding() + { + wrapper.KeyEncoding("key", CommandFlags.None); + mock.Verify(_ => _.KeyEncoding("prefix:key", CommandFlags.None)); + } + + [Fact] + public void KeyExists() + { + wrapper.KeyExists("key", CommandFlags.None); + mock.Verify(_ => _.KeyExists("prefix:key", CommandFlags.None)); + } + + [Fact] + public void KeyExpire_1() + { + TimeSpan expiry = TimeSpan.FromSeconds(123); + wrapper.KeyExpire("key", expiry, CommandFlags.None); + mock.Verify(_ => _.KeyExpire("prefix:key", expiry, CommandFlags.None)); + } + + [Fact] + public void KeyExpire_2() + { + DateTime expiry = DateTime.Now; + wrapper.KeyExpire("key", expiry, CommandFlags.None); + mock.Verify(_ => _.KeyExpire("prefix:key", expiry, CommandFlags.None)); + } + + [Fact] + public void KeyExpire_3() + { + TimeSpan expiry = TimeSpan.FromSeconds(123); + wrapper.KeyExpire("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); + mock.Verify(_ => _.KeyExpire("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); + } + + [Fact] + public void KeyExpire_4() + { + DateTime expiry = DateTime.Now; + wrapper.KeyExpire("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); + mock.Verify(_ => _.KeyExpire("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); + } + + [Fact] + public void KeyExpireTime() + { + wrapper.KeyExpireTime("key", CommandFlags.None); + mock.Verify(_ => _.KeyExpireTime("prefix:key", CommandFlags.None)); + } + + [Fact] + public void KeyMigrate() + { + EndPoint toServer = new IPEndPoint(IPAddress.Loopback, 123); + wrapper.KeyMigrate("key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None); + mock.Verify(_ => _.KeyMigrate("prefix:key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None)); + } + + [Fact] + public void KeyMove() + { + wrapper.KeyMove("key", 123, CommandFlags.None); + mock.Verify(_ => _.KeyMove("prefix:key", 123, CommandFlags.None)); + } + + [Fact] + public void KeyPersist() + { + wrapper.KeyPersist("key", CommandFlags.None); + mock.Verify(_ => _.KeyPersist("prefix:key", CommandFlags.None)); + } + + [Fact] + public void KeyRandom() + { + Assert.Throws(() => wrapper.KeyRandom()); + } + + [Fact] + public void KeyRefCount() + { + wrapper.KeyRefCount("key", CommandFlags.None); + mock.Verify(_ => _.KeyRefCount("prefix:key", CommandFlags.None)); + } + + [Fact] + public void KeyRename() + { + wrapper.KeyRename("key", "newKey", When.Exists, CommandFlags.None); + mock.Verify(_ => _.KeyRename("prefix:key", "prefix:newKey", When.Exists, CommandFlags.None)); + } + + [Fact] + public void KeyRestore() + { + byte[] value = Array.Empty(); + TimeSpan expiry = TimeSpan.FromSeconds(123); + wrapper.KeyRestore("key", value, expiry, CommandFlags.None); + mock.Verify(_ => _.KeyRestore("prefix:key", value, expiry, CommandFlags.None)); + } + + [Fact] + public void KeyTimeToLive() + { + wrapper.KeyTimeToLive("key", CommandFlags.None); + mock.Verify(_ => _.KeyTimeToLive("prefix:key", CommandFlags.None)); + } + + [Fact] + public void KeyType() + { + wrapper.KeyType("key", CommandFlags.None); + mock.Verify(_ => _.KeyType("prefix:key", CommandFlags.None)); + } + + [Fact] + public void ListGetByIndex() + { + wrapper.ListGetByIndex("key", 123, CommandFlags.None); + mock.Verify(_ => _.ListGetByIndex("prefix:key", 123, CommandFlags.None)); + } + + [Fact] + public void ListInsertAfter() + { + wrapper.ListInsertAfter("key", "pivot", "value", CommandFlags.None); + mock.Verify(_ => _.ListInsertAfter("prefix:key", "pivot", "value", CommandFlags.None)); + } + + [Fact] + public void ListInsertBefore() + { + wrapper.ListInsertBefore("key", "pivot", "value", CommandFlags.None); + mock.Verify(_ => _.ListInsertBefore("prefix:key", "pivot", "value", CommandFlags.None)); + } + + [Fact] + public void ListLeftPop() + { + wrapper.ListLeftPop("key", CommandFlags.None); + mock.Verify(_ => _.ListLeftPop("prefix:key", CommandFlags.None)); + } + + [Fact] + public void ListLeftPop_1() + { + wrapper.ListLeftPop("key", 123, CommandFlags.None); + mock.Verify(_ => _.ListLeftPop("prefix:key", 123, CommandFlags.None)); + } + + [Fact] + public void ListLeftPush_1() + { + wrapper.ListLeftPush("key", "value", When.Exists, CommandFlags.None); + mock.Verify(_ => _.ListLeftPush("prefix:key", "value", When.Exists, CommandFlags.None)); + } + + [Fact] + public void ListLeftPush_2() + { + RedisValue[] values = Array.Empty(); + wrapper.ListLeftPush("key", values, CommandFlags.None); + mock.Verify(_ => _.ListLeftPush("prefix:key", values, CommandFlags.None)); + } + + [Fact] + public void ListLeftPush_3() + { + RedisValue[] values = new RedisValue[] { "value1", "value2" }; + wrapper.ListLeftPush("key", values, When.Exists, CommandFlags.None); + mock.Verify(_ => _.ListLeftPush("prefix:key", values, When.Exists, CommandFlags.None)); + } + + [Fact] + public void ListLength() + { + wrapper.ListLength("key", CommandFlags.None); + mock.Verify(_ => _.ListLength("prefix:key", CommandFlags.None)); + } + + [Fact] + public void ListMove() + { + wrapper.ListMove("key", "destination", ListSide.Left, ListSide.Right, CommandFlags.None); + mock.Verify(_ => _.ListMove("prefix:key", "prefix:destination", ListSide.Left, ListSide.Right, CommandFlags.None)); + } + + [Fact] + public void ListRange() + { + wrapper.ListRange("key", 123, 456, CommandFlags.None); + mock.Verify(_ => _.ListRange("prefix:key", 123, 456, CommandFlags.None)); + } + + [Fact] + public void ListRemove() + { + wrapper.ListRemove("key", "value", 123, CommandFlags.None); + mock.Verify(_ => _.ListRemove("prefix:key", "value", 123, CommandFlags.None)); + } + + [Fact] + public void ListRightPop() + { + wrapper.ListRightPop("key", CommandFlags.None); + mock.Verify(_ => _.ListRightPop("prefix:key", CommandFlags.None)); + } + + [Fact] + public void ListRightPop_1() + { + wrapper.ListRightPop("key", 123, CommandFlags.None); + mock.Verify(_ => _.ListRightPop("prefix:key", 123, CommandFlags.None)); + } + + [Fact] + public void ListRightPopLeftPush() + { + wrapper.ListRightPopLeftPush("source", "destination", CommandFlags.None); + mock.Verify(_ => _.ListRightPopLeftPush("prefix:source", "prefix:destination", CommandFlags.None)); + } + + [Fact] + public void ListRightPush_1() + { + wrapper.ListRightPush("key", "value", When.Exists, CommandFlags.None); + mock.Verify(_ => _.ListRightPush("prefix:key", "value", When.Exists, CommandFlags.None)); + } + + [Fact] + public void ListRightPush_2() + { + RedisValue[] values = Array.Empty(); + wrapper.ListRightPush("key", values, CommandFlags.None); + mock.Verify(_ => _.ListRightPush("prefix:key", values, CommandFlags.None)); + } + + [Fact] + public void ListRightPush_3() + { + RedisValue[] values = new RedisValue[] { "value1", "value2" }; + wrapper.ListRightPush("key", values, When.Exists, CommandFlags.None); + mock.Verify(_ => _.ListRightPush("prefix:key", values, When.Exists, CommandFlags.None)); + } + + [Fact] + public void ListSetByIndex() + { + wrapper.ListSetByIndex("key", 123, "value", CommandFlags.None); + mock.Verify(_ => _.ListSetByIndex("prefix:key", 123, "value", CommandFlags.None)); + } + + [Fact] + public void ListTrim() + { + wrapper.ListTrim("key", 123, 456, CommandFlags.None); + mock.Verify(_ => _.ListTrim("prefix:key", 123, 456, CommandFlags.None)); + } + + [Fact] + public void LockExtend() + { + TimeSpan expiry = TimeSpan.FromSeconds(123); + wrapper.LockExtend("key", "value", expiry, CommandFlags.None); + mock.Verify(_ => _.LockExtend("prefix:key", "value", expiry, CommandFlags.None)); + } + + [Fact] + public void LockQuery() + { + wrapper.LockQuery("key", CommandFlags.None); + mock.Verify(_ => _.LockQuery("prefix:key", CommandFlags.None)); + } + + [Fact] + public void LockRelease() + { + wrapper.LockRelease("key", "value", CommandFlags.None); + mock.Verify(_ => _.LockRelease("prefix:key", "value", CommandFlags.None)); + } + + [Fact] + public void LockTake() + { + TimeSpan expiry = TimeSpan.FromSeconds(123); + wrapper.LockTake("key", "value", expiry, CommandFlags.None); + mock.Verify(_ => _.LockTake("prefix:key", "value", expiry, CommandFlags.None)); + } + + [Fact] + public void Publish() + { + wrapper.Publish("channel", "message", CommandFlags.None); + mock.Verify(_ => _.Publish("prefix:channel", "message", CommandFlags.None)); + } + + [Fact] + public void ScriptEvaluate_1() + { + byte[] hash = Array.Empty(); + RedisValue[] values = Array.Empty(); + RedisKey[] keys = new RedisKey[] { "a", "b" }; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + wrapper.ScriptEvaluate(hash, keys, values, CommandFlags.None); + mock.Verify(_ => _.ScriptEvaluate(hash, It.Is(valid), values, CommandFlags.None)); + } + + [Fact] + public void ScriptEvaluate_2() + { + RedisValue[] values = Array.Empty(); + RedisKey[] keys = new RedisKey[] { "a", "b" }; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + wrapper.ScriptEvaluate("script", keys, values, CommandFlags.None); + mock.Verify(_ => _.ScriptEvaluate("script", It.Is(valid), values, CommandFlags.None)); + } + + [Fact] + public void SetAdd_1() + { + wrapper.SetAdd("key", "value", CommandFlags.None); + mock.Verify(_ => _.SetAdd("prefix:key", "value", CommandFlags.None)); + } + + [Fact] + public void SetAdd_2() + { + RedisValue[] values = Array.Empty(); + wrapper.SetAdd("key", values, CommandFlags.None); + mock.Verify(_ => _.SetAdd("prefix:key", values, CommandFlags.None)); + } + + [Fact] + public void SetCombine_1() + { + wrapper.SetCombine(SetOperation.Intersect, "first", "second", CommandFlags.None); + mock.Verify(_ => _.SetCombine(SetOperation.Intersect, "prefix:first", "prefix:second", CommandFlags.None)); + } + + [Fact] + public void SetCombine_2() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + wrapper.SetCombine(SetOperation.Intersect, keys, CommandFlags.None); + mock.Verify(_ => _.SetCombine(SetOperation.Intersect, It.Is(valid), CommandFlags.None)); + } + + [Fact] + public void SetCombineAndStore_1() + { + wrapper.SetCombineAndStore(SetOperation.Intersect, "destination", "first", "second", CommandFlags.None); + mock.Verify(_ => _.SetCombineAndStore(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); + } + + [Fact] + public void SetCombineAndStore_2() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + wrapper.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None); + mock.Verify(_ => _.SetCombineAndStore(SetOperation.Intersect, "prefix:destination", It.Is(valid), CommandFlags.None)); + } + + [Fact] + public void SetContains() + { + wrapper.SetContains("key", "value", CommandFlags.None); + mock.Verify(_ => _.SetContains("prefix:key", "value", CommandFlags.None)); + } + + [Fact] + public void SetContains_2() + { + RedisValue[] values = new RedisValue[] { "value1", "value2" }; + wrapper.SetContains("key", values, CommandFlags.None); + mock.Verify(_ => _.SetContains("prefix:key", values, CommandFlags.None)); + } + + [Fact] + public void SetIntersectionLength() + { + var keys = new RedisKey[] { "key1", "key2" }; + wrapper.SetIntersectionLength(keys); + mock.Verify(_ => _.SetIntersectionLength(keys, 0, CommandFlags.None)); + } + + [Fact] + public void SetLength() + { + wrapper.SetLength("key", CommandFlags.None); + mock.Verify(_ => _.SetLength("prefix:key", CommandFlags.None)); + } + + [Fact] + public void SetMembers() + { + wrapper.SetMembers("key", CommandFlags.None); + mock.Verify(_ => _.SetMembers("prefix:key", CommandFlags.None)); + } + + [Fact] + public void SetMove() + { + wrapper.SetMove("source", "destination", "value", CommandFlags.None); + mock.Verify(_ => _.SetMove("prefix:source", "prefix:destination", "value", CommandFlags.None)); + } + + [Fact] + public void SetPop_1() + { + wrapper.SetPop("key", CommandFlags.None); + mock.Verify(_ => _.SetPop("prefix:key", CommandFlags.None)); + + wrapper.SetPop("key", 5, CommandFlags.None); + mock.Verify(_ => _.SetPop("prefix:key", 5, CommandFlags.None)); + } + + [Fact] + public void SetPop_2() + { + wrapper.SetPop("key", 5, CommandFlags.None); + mock.Verify(_ => _.SetPop("prefix:key", 5, CommandFlags.None)); + } + + [Fact] + public void SetRandomMember() + { + wrapper.SetRandomMember("key", CommandFlags.None); + mock.Verify(_ => _.SetRandomMember("prefix:key", CommandFlags.None)); + } + + [Fact] + public void SetRandomMembers() + { + wrapper.SetRandomMembers("key", 123, CommandFlags.None); + mock.Verify(_ => _.SetRandomMembers("prefix:key", 123, CommandFlags.None)); + } + + [Fact] + public void SetRemove_1() + { + wrapper.SetRemove("key", "value", CommandFlags.None); + mock.Verify(_ => _.SetRemove("prefix:key", "value", CommandFlags.None)); + } + + [Fact] + public void SetRemove_2() + { + RedisValue[] values = Array.Empty(); + wrapper.SetRemove("key", values, CommandFlags.None); + mock.Verify(_ => _.SetRemove("prefix:key", values, CommandFlags.None)); + } + + [Fact] + public void SetScan() + { + wrapper.SetScan("key", "pattern", 123, flags: CommandFlags.None); + mock.Verify(_ => _.SetScan("prefix:key", "pattern", 123, CommandFlags.None)); + } + + [Fact] + public void SetScan_Full() + { + wrapper.SetScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); + mock.Verify(_ => _.SetScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None)); + } + + [Fact] + public void Sort() + { + RedisValue[] get = new RedisValue[] { "a", "#" }; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; + + wrapper.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); + wrapper.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); + + mock.Verify(_ => _.Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", It.Is(valid), CommandFlags.None)); + mock.Verify(_ => _.Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", It.Is(valid), CommandFlags.None)); + } + + [Fact] + public void SortAndStore() + { + RedisValue[] get = new RedisValue[] { "a", "#" }; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; + + wrapper.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); + wrapper.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); + + mock.Verify(_ => _.SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", It.Is(valid), CommandFlags.None)); + mock.Verify(_ => _.SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", It.Is(valid), CommandFlags.None)); + } + + [Fact] + public void SortedSetAdd_1() + { + wrapper.SortedSetAdd("key", "member", 1.23, When.Exists, CommandFlags.None); + mock.Verify(_ => _.SortedSetAdd("prefix:key", "member", 1.23, When.Exists, CommandFlags.None)); + } + + [Fact] + public void SortedSetAdd_2() + { + SortedSetEntry[] values = Array.Empty(); + wrapper.SortedSetAdd("key", values, When.Exists, CommandFlags.None); + mock.Verify(_ => _.SortedSetAdd("prefix:key", values, When.Exists, CommandFlags.None)); + } + + [Fact] + public void SortedSetCombine() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetCombine(SetOperation.Intersect, keys); + mock.Verify(_ => _.SortedSetCombine(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); + } + + [Fact] + public void SortedSetCombineWithScores() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetCombineWithScores(SetOperation.Intersect, keys); + mock.Verify(_ => _.SortedSetCombineWithScores(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); + } + + [Fact] + public void SortedSetCombineAndStore_1() + { + wrapper.SortedSetCombineAndStore(SetOperation.Intersect, "destination", "first", "second", Aggregate.Max, CommandFlags.None); + mock.Verify(_ => _.SortedSetCombineAndStore(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", Aggregate.Max, CommandFlags.None)); + } + + [Fact] + public void SortedSetCombineAndStore_2() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + wrapper.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None); + mock.Verify(_ => _.SetCombineAndStore(SetOperation.Intersect, "prefix:destination", It.Is(valid), CommandFlags.None)); + } + + [Fact] + public void SortedSetDecrement() + { + wrapper.SortedSetDecrement("key", "member", 1.23, CommandFlags.None); + mock.Verify(_ => _.SortedSetDecrement("prefix:key", "member", 1.23, CommandFlags.None)); + } + + [Fact] + public void SortedSetIncrement() + { + wrapper.SortedSetIncrement("key", "member", 1.23, CommandFlags.None); + mock.Verify(_ => _.SortedSetIncrement("prefix:key", "member", 1.23, CommandFlags.None)); + } + + [Fact] + public void SortedSetIntersectionLength() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + wrapper.SortedSetIntersectionLength(keys, 1, CommandFlags.None); + mock.Verify(_ => _.SortedSetIntersectionLength(keys, 1, CommandFlags.None)); + } + + [Fact] + public void SortedSetLength() + { + wrapper.SortedSetLength("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); + mock.Verify(_ => _.SortedSetLength("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None)); + } + + [Fact] + public void SortedSetRandomMember() + { + wrapper.SortedSetRandomMember("key", CommandFlags.None); + mock.Verify(_ => _.SortedSetRandomMember("prefix:key", CommandFlags.None)); + } + + [Fact] + public void SortedSetRandomMembers() + { + wrapper.SortedSetRandomMembers("key", 2, CommandFlags.None); + mock.Verify(_ => _.SortedSetRandomMembers("prefix:key", 2, CommandFlags.None)); + } + + [Fact] + public void SortedSetRandomMembersWithScores() + { + wrapper.SortedSetRandomMembersWithScores("key", 2, CommandFlags.None); + mock.Verify(_ => _.SortedSetRandomMembersWithScores("prefix:key", 2, CommandFlags.None)); + } + + [Fact] + public void SortedSetLengthByValue() + { + wrapper.SortedSetLengthByValue("key", "min", "max", Exclude.Start, CommandFlags.None); + mock.Verify(_ => _.SortedSetLengthByValue("prefix:key", "min", "max", Exclude.Start, CommandFlags.None)); + } + + [Fact] + public void SortedSetRangeByRank() + { + wrapper.SortedSetRangeByRank("key", 123, 456, Order.Descending, CommandFlags.None); + mock.Verify(_ => _.SortedSetRangeByRank("prefix:key", 123, 456, Order.Descending, CommandFlags.None)); + } + + [Fact] + public void SortedSetRangeByRankWithScores() + { + wrapper.SortedSetRangeByRankWithScores("key", 123, 456, Order.Descending, CommandFlags.None); + mock.Verify(_ => _.SortedSetRangeByRankWithScores("prefix:key", 123, 456, Order.Descending, CommandFlags.None)); + } + + [Fact] + public void SortedSetRangeByScore() + { + wrapper.SortedSetRangeByScore("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); + mock.Verify(_ => _.SortedSetRangeByScore("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); + } + + [Fact] + public void SortedSetRangeByScoreWithScores() + { + wrapper.SortedSetRangeByScoreWithScores("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); + mock.Verify(_ => _.SortedSetRangeByScoreWithScores("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); + } + + [Fact] + public void SortedSetRangeByValue() + { + wrapper.SortedSetRangeByValue("key", "min", "max", Exclude.Start, 123, 456, CommandFlags.None); + mock.Verify(_ => _.SortedSetRangeByValue("prefix:key", "min", "max", Exclude.Start, Order.Ascending, 123, 456, CommandFlags.None)); + } + + [Fact] + public void SortedSetRangeByValueDesc() + { + wrapper.SortedSetRangeByValue("key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); + mock.Verify(_ => _.SortedSetRangeByValue("prefix:key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); + } + + [Fact] + public void SortedSetRank() + { + wrapper.SortedSetRank("key", "member", Order.Descending, CommandFlags.None); + mock.Verify(_ => _.SortedSetRank("prefix:key", "member", Order.Descending, CommandFlags.None)); + } + + [Fact] + public void SortedSetRemove_1() + { + wrapper.SortedSetRemove("key", "member", CommandFlags.None); + mock.Verify(_ => _.SortedSetRemove("prefix:key", "member", CommandFlags.None)); + } + + [Fact] + public void SortedSetRemove_2() + { + RedisValue[] members = Array.Empty(); + wrapper.SortedSetRemove("key", members, CommandFlags.None); + mock.Verify(_ => _.SortedSetRemove("prefix:key", members, CommandFlags.None)); + } + + [Fact] + public void SortedSetRemoveRangeByRank() + { + wrapper.SortedSetRemoveRangeByRank("key", 123, 456, CommandFlags.None); + mock.Verify(_ => _.SortedSetRemoveRangeByRank("prefix:key", 123, 456, CommandFlags.None)); + } + + [Fact] + public void SortedSetRemoveRangeByScore() + { + wrapper.SortedSetRemoveRangeByScore("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); + mock.Verify(_ => _.SortedSetRemoveRangeByScore("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None)); + } + + [Fact] + public void SortedSetRemoveRangeByValue() + { + wrapper.SortedSetRemoveRangeByValue("key", "min", "max", Exclude.Start, CommandFlags.None); + mock.Verify(_ => _.SortedSetRemoveRangeByValue("prefix:key", "min", "max", Exclude.Start, CommandFlags.None)); + } + + [Fact] + public void SortedSetScan() + { + wrapper.SortedSetScan("key", "pattern", 123, flags: CommandFlags.None); + mock.Verify(_ => _.SortedSetScan("prefix:key", "pattern", 123, CommandFlags.None)); + } + + [Fact] + public void SortedSetScan_Full() + { + wrapper.SortedSetScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); + mock.Verify(_ => _.SortedSetScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None)); + } + + [Fact] + public void SortedSetScore() + { + wrapper.SortedSetScore("key", "member", CommandFlags.None); + mock.Verify(_ => _.SortedSetScore("prefix:key", "member", CommandFlags.None)); + } + + [Fact] + public void SortedSetScore_Multiple() + { + wrapper.SortedSetScores("key", new RedisValue[] { "member1", "member2" }, CommandFlags.None); + mock.Verify(_ => _.SortedSetScores("prefix:key", new RedisValue[] { "member1", "member2" }, CommandFlags.None)); + } + + [Fact] + public void StreamAcknowledge_1() + { + wrapper.StreamAcknowledge("key", "group", "0-0", CommandFlags.None); + mock.Verify(_ => _.StreamAcknowledge("prefix:key", "group", "0-0", CommandFlags.None)); + } + + [Fact] + public void StreamAcknowledge_2() + { + var messageIds = new RedisValue[] { "0-0", "0-1", "0-2" }; + wrapper.StreamAcknowledge("key", "group", messageIds, CommandFlags.None); + mock.Verify(_ => _.StreamAcknowledge("prefix:key", "group", messageIds, CommandFlags.None)); + } + + [Fact] + public void StreamAdd_1() + { + wrapper.StreamAdd("key", "field1", "value1", "*", 1000, true, CommandFlags.None); + mock.Verify(_ => _.StreamAdd("prefix:key", "field1", "value1", "*", 1000, true, CommandFlags.None)); + } + + [Fact] + public void StreamAdd_2() + { + var fields = Array.Empty(); + wrapper.StreamAdd("key", fields, "*", 1000, true, CommandFlags.None); + mock.Verify(_ => _.StreamAdd("prefix:key", fields, "*", 1000, true, CommandFlags.None)); + } + + [Fact] + public void StreamClaimMessages() + { + var messageIds = Array.Empty(); + wrapper.StreamClaim("key", "group", "consumer", 1000, messageIds, CommandFlags.None); + mock.Verify(_ => _.StreamClaim("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); + } + + [Fact] + public void StreamClaimMessagesReturningIds() + { + var messageIds = Array.Empty(); + wrapper.StreamClaimIdsOnly("key", "group", "consumer", 1000, messageIds, CommandFlags.None); + mock.Verify(_ => _.StreamClaimIdsOnly("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); + } + + [Fact] + public void StreamConsumerGroupSetPosition() + { + wrapper.StreamConsumerGroupSetPosition("key", "group", StreamPosition.Beginning, CommandFlags.None); + mock.Verify(_ => _.StreamConsumerGroupSetPosition("prefix:key", "group", StreamPosition.Beginning, CommandFlags.None)); + } + + [Fact] + public void StreamConsumerInfoGet() + { + wrapper.StreamConsumerInfo("key", "group", CommandFlags.None); + mock.Verify(_ => _.StreamConsumerInfo("prefix:key", "group", CommandFlags.None)); + } + + [Fact] + public void StreamCreateConsumerGroup() + { + wrapper.StreamCreateConsumerGroup("key", "group", StreamPosition.Beginning, false, CommandFlags.None); + mock.Verify(_ => _.StreamCreateConsumerGroup("prefix:key", "group", StreamPosition.Beginning, false, CommandFlags.None)); + } + + [Fact] + public void StreamGroupInfoGet() + { + wrapper.StreamGroupInfo("key", CommandFlags.None); + mock.Verify(_ => _.StreamGroupInfo("prefix:key", CommandFlags.None)); + } + + [Fact] + public void StreamInfoGet() + { + wrapper.StreamInfo("key", CommandFlags.None); + mock.Verify(_ => _.StreamInfo("prefix:key", CommandFlags.None)); + } + + [Fact] + public void StreamLength() + { + wrapper.StreamLength("key", CommandFlags.None); + mock.Verify(_ => _.StreamLength("prefix:key", CommandFlags.None)); + } + + [Fact] + public void StreamMessagesDelete() + { + var messageIds = Array.Empty(); + wrapper.StreamDelete("key", messageIds, CommandFlags.None); + mock.Verify(_ => _.StreamDelete("prefix:key", messageIds, CommandFlags.None)); + } + + [Fact] + public void StreamDeleteConsumer() + { + wrapper.StreamDeleteConsumer("key", "group", "consumer", CommandFlags.None); + mock.Verify(_ => _.StreamDeleteConsumer("prefix:key", "group", "consumer", CommandFlags.None)); + } + + [Fact] + public void StreamDeleteConsumerGroup() + { + wrapper.StreamDeleteConsumerGroup("key", "group", CommandFlags.None); + mock.Verify(_ => _.StreamDeleteConsumerGroup("prefix:key", "group", CommandFlags.None)); + } + + [Fact] + public void StreamPendingInfoGet() + { + wrapper.StreamPending("key", "group", CommandFlags.None); + mock.Verify(_ => _.StreamPending("prefix:key", "group", CommandFlags.None)); + } + + [Fact] + public void StreamPendingMessageInfoGet() + { + wrapper.StreamPendingMessages("key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); + mock.Verify(_ => _.StreamPendingMessages("prefix:key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None)); + } + + [Fact] + public void StreamRange() + { + wrapper.StreamRange("key", "-", "+", null, Order.Ascending, CommandFlags.None); + mock.Verify(_ => _.StreamRange("prefix:key", "-", "+", null, Order.Ascending, CommandFlags.None)); + } + + [Fact] + public void StreamRead_1() + { + var streamPositions = Array.Empty(); + wrapper.StreamRead(streamPositions, null, CommandFlags.None); + mock.Verify(_ => _.StreamRead(streamPositions, null, CommandFlags.None)); + } + + [Fact] + public void StreamRead_2() + { + wrapper.StreamRead("key", "0-0", null, CommandFlags.None); + mock.Verify(_ => _.StreamRead("prefix:key", "0-0", null, CommandFlags.None)); + } + + [Fact] + public void StreamStreamReadGroup_1() + { + wrapper.StreamReadGroup("key", "group", "consumer", "0-0", 10, false, CommandFlags.None); + mock.Verify(_ => _.StreamReadGroup("prefix:key", "group", "consumer", "0-0", 10, false, CommandFlags.None)); + } + + [Fact] + public void StreamStreamReadGroup_2() + { + var streamPositions = Array.Empty(); + wrapper.StreamReadGroup(streamPositions, "group", "consumer", 10, false, CommandFlags.None); + mock.Verify(_ => _.StreamReadGroup(streamPositions, "group", "consumer", 10, false, CommandFlags.None)); + } + + [Fact] + public void StreamTrim() + { + wrapper.StreamTrim("key", 1000, true, CommandFlags.None); + mock.Verify(_ => _.StreamTrim("prefix:key", 1000, true, CommandFlags.None)); + } + + [Fact] + public void StringAppend() + { + wrapper.StringAppend("key", "value", CommandFlags.None); + mock.Verify(_ => _.StringAppend("prefix:key", "value", CommandFlags.None)); + } + + [Fact] + public void StringBitCount() + { + wrapper.StringBitCount("key", 123, 456, CommandFlags.None); + mock.Verify(_ => _.StringBitCount("prefix:key", 123, 456, CommandFlags.None)); + } + + [Fact] + public void StringBitOperation_1() + { + wrapper.StringBitOperation(Bitwise.Xor, "destination", "first", "second", CommandFlags.None); + mock.Verify(_ => _.StringBitOperation(Bitwise.Xor, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); + } + + [Fact] + public void StringBitOperation_2() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + wrapper.StringBitOperation(Bitwise.Xor, "destination", keys, CommandFlags.None); + mock.Verify(_ => _.StringBitOperation(Bitwise.Xor, "prefix:destination", It.Is(valid), CommandFlags.None)); + } + + [Fact] + public void StringBitPosition() + { + wrapper.StringBitPosition("key", true, 123, 456, CommandFlags.None); + mock.Verify(_ => _.StringBitPosition("prefix:key", true, 123, 456, CommandFlags.None)); + } + + [Fact] + public void StringDecrement_1() + { + wrapper.StringDecrement("key", 123, CommandFlags.None); + mock.Verify(_ => _.StringDecrement("prefix:key", 123, CommandFlags.None)); + } + + [Fact] + public void StringDecrement_2() + { + wrapper.StringDecrement("key", 1.23, CommandFlags.None); + mock.Verify(_ => _.StringDecrement("prefix:key", 1.23, CommandFlags.None)); + } + + [Fact] + public void StringGet_1() + { + wrapper.StringGet("key", CommandFlags.None); + mock.Verify(_ => _.StringGet("prefix:key", CommandFlags.None)); + } + + [Fact] + public void StringGet_2() + { + RedisKey[] keys = new RedisKey[] { "a", "b" }; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + wrapper.StringGet(keys, CommandFlags.None); + mock.Verify(_ => _.StringGet(It.Is(valid), CommandFlags.None)); + } + + [Fact] + public void StringGetBit() + { + wrapper.StringGetBit("key", 123, CommandFlags.None); + mock.Verify(_ => _.StringGetBit("prefix:key", 123, CommandFlags.None)); + } + + [Fact] + public void StringGetRange() + { + wrapper.StringGetRange("key", 123, 456, CommandFlags.None); + mock.Verify(_ => _.StringGetRange("prefix:key", 123, 456, CommandFlags.None)); + } + + [Fact] + public void StringGetSet() + { + wrapper.StringGetSet("key", "value", CommandFlags.None); + mock.Verify(_ => _.StringGetSet("prefix:key", "value", CommandFlags.None)); + } + + [Fact] + public void StringGetDelete() + { + wrapper.StringGetDelete("key", CommandFlags.None); + mock.Verify(_ => _.StringGetDelete("prefix:key", CommandFlags.None)); + } + + [Fact] + public void StringGetWithExpiry() + { + wrapper.StringGetWithExpiry("key", CommandFlags.None); + mock.Verify(_ => _.StringGetWithExpiry("prefix:key", CommandFlags.None)); + } + + [Fact] + public void StringIncrement_1() + { + wrapper.StringIncrement("key", 123, CommandFlags.None); + mock.Verify(_ => _.StringIncrement("prefix:key", 123, CommandFlags.None)); + } + + [Fact] + public void StringIncrement_2() + { + wrapper.StringIncrement("key", 1.23, CommandFlags.None); + mock.Verify(_ => _.StringIncrement("prefix:key", 1.23, CommandFlags.None)); + } + + [Fact] + public void StringLength() + { + wrapper.StringLength("key", CommandFlags.None); + mock.Verify(_ => _.StringLength("prefix:key", CommandFlags.None)); + } + + [Fact] + public void StringSet_1() + { + TimeSpan expiry = TimeSpan.FromSeconds(123); + wrapper.StringSet("key", "value", expiry, When.Exists, CommandFlags.None); + mock.Verify(_ => _.StringSet("prefix:key", "value", expiry, When.Exists, CommandFlags.None)); + } + + [Fact] + public void StringSet_2() + { + TimeSpan? expiry = null; + wrapper.StringSet("key", "value", expiry, true, When.Exists, CommandFlags.None); + mock.Verify(_ => _.StringSet("prefix:key", "value", expiry, true, When.Exists, CommandFlags.None)); + } + + [Fact] + public void StringSet_3() + { + KeyValuePair[] values = new KeyValuePair[] { new KeyValuePair("a", "x"), new KeyValuePair("b", "y") }; + Expression[], bool>> valid = _ => _.Length == 2 && _[0].Key == "prefix:a" && _[0].Value == "x" && _[1].Key == "prefix:b" && _[1].Value == "y"; + wrapper.StringSet(values, When.Exists, CommandFlags.None); + mock.Verify(_ => _.StringSet(It.Is(valid), When.Exists, CommandFlags.None)); + } + + [Fact] + public void StringSetBit() + { + wrapper.StringSetBit("key", 123, true, CommandFlags.None); + mock.Verify(_ => _.StringSetBit("prefix:key", 123, true, CommandFlags.None)); + } + + [Fact] + public void StringSetRange() + { + wrapper.StringSetRange("key", 123, "value", CommandFlags.None); + mock.Verify(_ => _.StringSetRange("prefix:key", 123, "value", CommandFlags.None)); } } diff --git a/tests/StackExchange.Redis.Tests/Databases.cs b/tests/StackExchange.Redis.Tests/Databases.cs index dee605312..1dfffb4e0 100644 --- a/tests/StackExchange.Redis.Tests/Databases.cs +++ b/tests/StackExchange.Redis.Tests/Databases.cs @@ -2,160 +2,151 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Databases : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Databases : TestBase - { - public Databases(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public Databases(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } - [Fact] - public async Task CountKeys() + [Fact] + public async Task CountKeys() + { + var db1Id = TestConfig.GetDedicatedDB(); + var db2Id = TestConfig.GetDedicatedDB(); + using (var conn = Create(allowAdmin: true)) { - var db1Id = TestConfig.GetDedicatedDB(); - var db2Id = TestConfig.GetDedicatedDB(); - using (var muxer = Create(allowAdmin: true)) - { - Skip.IfMissingDatabase(muxer, db1Id); - Skip.IfMissingDatabase(muxer, db2Id); - var server = GetAnyPrimary(muxer); - server.FlushDatabase(db1Id, CommandFlags.FireAndForget); - server.FlushDatabase(db2Id, CommandFlags.FireAndForget); - } - using (var muxer = Create(defaultDatabase: db2Id)) - { - Skip.IfMissingDatabase(muxer, db1Id); - Skip.IfMissingDatabase(muxer, db2Id); - RedisKey key = Me(); - var dba = muxer.GetDatabase(db1Id); - var dbb = muxer.GetDatabase(db2Id); - dba.StringSet("abc", "def", flags: CommandFlags.FireAndForget); - dba.StringIncrement(key, flags: CommandFlags.FireAndForget); - dbb.StringIncrement(key, flags: CommandFlags.FireAndForget); - - var server = GetAnyPrimary(muxer); - var c0 = server.DatabaseSizeAsync(db1Id); - var c1 = server.DatabaseSizeAsync(db2Id); - var c2 = server.DatabaseSizeAsync(); // using default DB, which is db2Id - - Assert.Equal(2, await c0); - Assert.Equal(1, await c1); - Assert.Equal(1, await c2); - } + Skip.IfMissingDatabase(conn, db1Id); + Skip.IfMissingDatabase(conn, db2Id); + var server = GetAnyPrimary(conn); + server.FlushDatabase(db1Id, CommandFlags.FireAndForget); + server.FlushDatabase(db2Id, CommandFlags.FireAndForget); } - - [Fact] - public void DatabaseCount() + using (var conn = Create(defaultDatabase: db2Id)) { - using (var muxer = Create(allowAdmin: true)) - { - var server = GetAnyPrimary(muxer); - var count = server.DatabaseCount; - Log("Count: " + count); - var configVal = server.ConfigGet("databases")[0].Value; - Log("Config databases: " + configVal); - Assert.Equal(int.Parse(configVal), count); - } + Skip.IfMissingDatabase(conn, db1Id); + Skip.IfMissingDatabase(conn, db2Id); + RedisKey key = Me(); + var dba = conn.GetDatabase(db1Id); + var dbb = conn.GetDatabase(db2Id); + dba.StringSet("abc", "def", flags: CommandFlags.FireAndForget); + dba.StringIncrement(key, flags: CommandFlags.FireAndForget); + dbb.StringIncrement(key, flags: CommandFlags.FireAndForget); + + var server = GetAnyPrimary(conn); + var c0 = server.DatabaseSizeAsync(db1Id); + var c1 = server.DatabaseSizeAsync(db2Id); + var c2 = server.DatabaseSizeAsync(); // using default DB, which is db2Id + + Assert.Equal(2, await c0); + Assert.Equal(1, await c1); + Assert.Equal(1, await c2); } + } - [Fact] - public async Task MultiDatabases() - { - using (var muxer = Create()) - { - RedisKey key = Me(); - var db0 = muxer.GetDatabase(TestConfig.GetDedicatedDB(muxer)); - var db1 = muxer.GetDatabase(TestConfig.GetDedicatedDB(muxer)); - var db2 = muxer.GetDatabase(TestConfig.GetDedicatedDB(muxer)); - - db0.KeyDelete(key, CommandFlags.FireAndForget); - db1.KeyDelete(key, CommandFlags.FireAndForget); - db2.KeyDelete(key, CommandFlags.FireAndForget); - - db0.StringSet(key, "a", flags: CommandFlags.FireAndForget); - db1.StringSet(key, "b", flags: CommandFlags.FireAndForget); - db2.StringSet(key, "c", flags: CommandFlags.FireAndForget); - - var a = db0.StringGetAsync(key); - var b = db1.StringGetAsync(key); - var c = db2.StringGetAsync(key); - - Assert.Equal("a", await a); // db:0 - Assert.Equal("b", await b); // db:1 - Assert.Equal("c", await c); // db:2 - } - } + [Fact] + public void DatabaseCount() + { + using var conn = Create(allowAdmin: true); + + var server = GetAnyPrimary(conn); + var count = server.DatabaseCount; + Log("Count: " + count); + var configVal = server.ConfigGet("databases")[0].Value; + Log("Config databases: " + configVal); + Assert.Equal(int.Parse(configVal), count); + } - [Fact] - public async Task SwapDatabases() - { - using (var muxer = Create(allowAdmin: true)) - { - Skip.IfBelow(muxer, RedisFeatures.v4_0_0); + [Fact] + public async Task MultiDatabases() + { + using var conn = Create(); + + RedisKey key = Me(); + var db0 = conn.GetDatabase(TestConfig.GetDedicatedDB(conn)); + var db1 = conn.GetDatabase(TestConfig.GetDedicatedDB(conn)); + var db2 = conn.GetDatabase(TestConfig.GetDedicatedDB(conn)); - RedisKey key = Me(); - var db0id = TestConfig.GetDedicatedDB(muxer); - var db0 = muxer.GetDatabase(db0id); - var db1id = TestConfig.GetDedicatedDB(muxer); - var db1 = muxer.GetDatabase(db1id); + db0.KeyDelete(key, CommandFlags.FireAndForget); + db1.KeyDelete(key, CommandFlags.FireAndForget); + db2.KeyDelete(key, CommandFlags.FireAndForget); - db0.KeyDelete(key, CommandFlags.FireAndForget); - db1.KeyDelete(key, CommandFlags.FireAndForget); + db0.StringSet(key, "a", flags: CommandFlags.FireAndForget); + db1.StringSet(key, "b", flags: CommandFlags.FireAndForget); + db2.StringSet(key, "c", flags: CommandFlags.FireAndForget); - db0.StringSet(key, "a", flags: CommandFlags.FireAndForget); - db1.StringSet(key, "b", flags: CommandFlags.FireAndForget); + var a = db0.StringGetAsync(key); + var b = db1.StringGetAsync(key); + var c = db2.StringGetAsync(key); - var a = db0.StringGetAsync(key); - var b = db1.StringGetAsync(key); + Assert.Equal("a", await a); // db:0 + Assert.Equal("b", await b); // db:1 + Assert.Equal("c", await c); // db:2 + } - Assert.Equal("a", await a); // db:0 - Assert.Equal("b", await b); // db:1 + [Fact] + public async Task SwapDatabases() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v4_0_0); - var server = GetServer(muxer); - server.SwapDatabases(db0id, db1id); + RedisKey key = Me(); + var db0id = TestConfig.GetDedicatedDB(conn); + var db0 = conn.GetDatabase(db0id); + var db1id = TestConfig.GetDedicatedDB(conn); + var db1 = conn.GetDatabase(db1id); - var aNew = db1.StringGetAsync(key); - var bNew = db0.StringGetAsync(key); + db0.KeyDelete(key, CommandFlags.FireAndForget); + db1.KeyDelete(key, CommandFlags.FireAndForget); - Assert.Equal("a", await aNew); // db:1 - Assert.Equal("b", await bNew); // db:0 - } - } + db0.StringSet(key, "a", flags: CommandFlags.FireAndForget); + db1.StringSet(key, "b", flags: CommandFlags.FireAndForget); - [Fact] - public async Task SwapDatabasesAsync() - { - using (var muxer = Create(allowAdmin: true)) - { - Skip.IfBelow(muxer, RedisFeatures.v4_0_0); + var a = db0.StringGetAsync(key); + var b = db1.StringGetAsync(key); + + Assert.Equal("a", await a); // db:0 + Assert.Equal("b", await b); // db:1 + + var server = GetServer(conn); + server.SwapDatabases(db0id, db1id); - RedisKey key = Me(); - var db0id = TestConfig.GetDedicatedDB(muxer); - var db0 = muxer.GetDatabase(db0id); - var db1id = TestConfig.GetDedicatedDB(muxer); - var db1 = muxer.GetDatabase(db1id); + var aNew = db1.StringGetAsync(key); + var bNew = db0.StringGetAsync(key); - db0.KeyDelete(key, CommandFlags.FireAndForget); - db1.KeyDelete(key, CommandFlags.FireAndForget); + Assert.Equal("a", await aNew); // db:1 + Assert.Equal("b", await bNew); // db:0 + } - db0.StringSet(key, "a", flags: CommandFlags.FireAndForget); - db1.StringSet(key, "b", flags: CommandFlags.FireAndForget); + [Fact] + public async Task SwapDatabasesAsync() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v4_0_0); - var a = db0.StringGetAsync(key); - var b = db1.StringGetAsync(key); + RedisKey key = Me(); + var db0id = TestConfig.GetDedicatedDB(conn); + var db0 = conn.GetDatabase(db0id); + var db1id = TestConfig.GetDedicatedDB(conn); + var db1 = conn.GetDatabase(db1id); - Assert.Equal("a", await a); // db:0 - Assert.Equal("b", await b); // db:1 + db0.KeyDelete(key, CommandFlags.FireAndForget); + db1.KeyDelete(key, CommandFlags.FireAndForget); - var server = GetServer(muxer); - _ = server.SwapDatabasesAsync(db0id, db1id).ForAwait(); + db0.StringSet(key, "a", flags: CommandFlags.FireAndForget); + db1.StringSet(key, "b", flags: CommandFlags.FireAndForget); - var aNew = db1.StringGetAsync(key); - var bNew = db0.StringGetAsync(key); + var a = db0.StringGetAsync(key); + var b = db1.StringGetAsync(key); - Assert.Equal("a", await aNew); // db:1 - Assert.Equal("b", await bNew); // db:0 - } - } + Assert.Equal("a", await a); // db:0 + Assert.Equal("b", await b); // db:1 + + var server = GetServer(conn); + _ = server.SwapDatabasesAsync(db0id, db1id).ForAwait(); + + var aNew = db1.StringGetAsync(key); + var bNew = db0.StringGetAsync(key); + + Assert.Equal("a", await aNew); // db:1 + Assert.Equal("b", await bNew); // db:0 } } diff --git a/tests/StackExchange.Redis.Tests/DefaultOptions.cs b/tests/StackExchange.Redis.Tests/DefaultOptions.cs index d5317cfbb..7a9a97543 100644 --- a/tests/StackExchange.Redis.Tests/DefaultOptions.cs +++ b/tests/StackExchange.Redis.Tests/DefaultOptions.cs @@ -7,152 +7,151 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class DefaultOptions : TestBase { - public class DefaultOptions : TestBase + public DefaultOptions(ITestOutputHelper output) : base(output) { } + + public class TestOptionsProvider : DefaultOptionsProvider { - public DefaultOptions(ITestOutputHelper output) : base(output) { } + private readonly string _domainSuffix; + public TestOptionsProvider(string domainSuffix) => _domainSuffix = domainSuffix; + + public override bool AbortOnConnectFail => true; + public override TimeSpan? ConnectTimeout => TimeSpan.FromSeconds(123); + public override bool AllowAdmin => true; + public override BacklogPolicy BacklogPolicy => BacklogPolicy.FailFast; + public override bool CheckCertificateRevocation => true; + public override CommandMap CommandMap => CommandMap.Create(new HashSet() { "SELECT" }); + public override TimeSpan ConfigCheckInterval => TimeSpan.FromSeconds(124); + public override string ConfigurationChannel => "TestConfigChannel"; + public override int ConnectRetry => 123; + public override Version DefaultVersion => new Version(1, 2, 3, 4); + protected override string GetDefaultClientName() => "TestPrefix-" + base.GetDefaultClientName(); + public override bool IsMatch(EndPoint endpoint) => endpoint is DnsEndPoint dnsep && dnsep.Host.EndsWith(_domainSuffix); + public override TimeSpan KeepAliveInterval => TimeSpan.FromSeconds(125); + public override Proxy Proxy => Proxy.Twemproxy; + public override IReconnectRetryPolicy ReconnectRetryPolicy => new TestRetryPolicy(); + public override bool ResolveDns => true; + public override TimeSpan SyncTimeout => TimeSpan.FromSeconds(126); + public override string TieBreaker => "TestTiebreaker"; + } - public class TestOptionsProvider : DefaultOptionsProvider - { - private readonly string _domainSuffix; - public TestOptionsProvider(string domainSuffix) => _domainSuffix = domainSuffix; - - public override bool AbortOnConnectFail => true; - public override TimeSpan? ConnectTimeout => TimeSpan.FromSeconds(123); - public override bool AllowAdmin => true; - public override BacklogPolicy BacklogPolicy => BacklogPolicy.FailFast; - public override bool CheckCertificateRevocation => true; - public override CommandMap CommandMap => CommandMap.Create(new HashSet() { "SELECT" }); - public override TimeSpan ConfigCheckInterval => TimeSpan.FromSeconds(124); - public override string ConfigurationChannel => "TestConfigChannel"; - public override int ConnectRetry => 123; - public override Version DefaultVersion => new Version(1, 2, 3, 4); - protected override string GetDefaultClientName() => "TestPrefix-" + base.GetDefaultClientName(); - public override bool IsMatch(EndPoint endpoint) => endpoint is DnsEndPoint dnsep && dnsep.Host.EndsWith(_domainSuffix); - public override TimeSpan KeepAliveInterval => TimeSpan.FromSeconds(125); - public override Proxy Proxy => Proxy.Twemproxy; - public override IReconnectRetryPolicy ReconnectRetryPolicy => new TestRetryPolicy(); - public override bool ResolveDns => true; - public override TimeSpan SyncTimeout => TimeSpan.FromSeconds(126); - public override string TieBreaker => "TestTiebreaker"; - } + public class TestRetryPolicy : IReconnectRetryPolicy + { + public bool ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSinceLastRetry) => false; + } - public class TestRetryPolicy : IReconnectRetryPolicy - { - public bool ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSinceLastRetry) => false; - } + [Fact] + public void IsMatchOnDomain() + { + DefaultOptionsProvider.AddProvider(new TestOptionsProvider(".testdomain")); - [Fact] - public void IsMatchOnDomain() - { - DefaultOptionsProvider.AddProvider(new TestOptionsProvider(".testdomain")); + var epc = new EndPointCollection(new List() { new DnsEndPoint("local.testdomain", 0) }); + var provider = DefaultOptionsProvider.GetForEndpoints(epc); + Assert.IsType(provider); - var epc = new EndPointCollection(new List() { new DnsEndPoint("local.testdomain", 0) }); - var provider = DefaultOptionsProvider.GetForEndpoints(epc); - Assert.IsType(provider); + epc = new EndPointCollection(new List() { new DnsEndPoint("local.nottestdomain", 0) }); + provider = DefaultOptionsProvider.GetForEndpoints(epc); + Assert.IsType(provider); + } - epc = new EndPointCollection(new List() { new DnsEndPoint("local.nottestdomain", 0) }); - provider = DefaultOptionsProvider.GetForEndpoints(epc); - Assert.IsType(provider); - } + [Fact] + public void AllOverridesFromDefaultsProp() + { + var options = ConfigurationOptions.Parse("localhost"); + Assert.IsType(options.Defaults); + options.Defaults = new TestOptionsProvider(""); + Assert.IsType(options.Defaults); + AssertAllOverrides(options); + } - [Fact] - public void AllOverridesFromDefaultsProp() - { - var options = ConfigurationOptions.Parse("localhost"); - Assert.IsType(options.Defaults); - options.Defaults = new TestOptionsProvider(""); - Assert.IsType(options.Defaults); - AssertAllOverrides(options); - } + [Fact] + public void AllOverridesFromEndpointsParse() + { + DefaultOptionsProvider.AddProvider(new TestOptionsProvider(".parse")); + var options = ConfigurationOptions.Parse("localhost.parse:6379"); + Assert.IsType(options.Defaults); + AssertAllOverrides(options); + } - [Fact] - public void AllOverridesFromEndpointsParse() - { - DefaultOptionsProvider.AddProvider(new TestOptionsProvider(".parse")); - var options = ConfigurationOptions.Parse("localhost.parse:6379"); - Assert.IsType(options.Defaults); - AssertAllOverrides(options); - } + private static void AssertAllOverrides(ConfigurationOptions options) + { + Assert.True(options.AbortOnConnectFail); + Assert.Equal(TimeSpan.FromSeconds(123), TimeSpan.FromMilliseconds(options.ConnectTimeout)); + + Assert.True(options.AllowAdmin); + Assert.Equal(BacklogPolicy.FailFast, options.BacklogPolicy); + Assert.True(options.CheckCertificateRevocation); + + Assert.True(options.CommandMap.IsAvailable(RedisCommand.SELECT)); + Assert.False(options.CommandMap.IsAvailable(RedisCommand.GET)); + + Assert.Equal(TimeSpan.FromSeconds(124), TimeSpan.FromSeconds(options.ConfigCheckSeconds)); + Assert.Equal("TestConfigChannel", options.ConfigurationChannel); + Assert.Equal(123, options.ConnectRetry); + Assert.Equal(new Version(1, 2, 3, 4), options.DefaultVersion); + + Assert.Equal(TimeSpan.FromSeconds(125), TimeSpan.FromSeconds(options.KeepAlive)); + Assert.Equal(Proxy.Twemproxy, options.Proxy); + Assert.IsType(options.ReconnectRetryPolicy); + Assert.True(options.ResolveDns); + Assert.Equal(TimeSpan.FromSeconds(126), TimeSpan.FromMilliseconds(options.SyncTimeout)); + Assert.Equal("TestTiebreaker", options.TieBreaker); + } - private static void AssertAllOverrides(ConfigurationOptions options) - { - Assert.True(options.AbortOnConnectFail); - Assert.Equal(TimeSpan.FromSeconds(123), TimeSpan.FromMilliseconds(options.ConnectTimeout)); - - Assert.True(options.AllowAdmin); - Assert.Equal(BacklogPolicy.FailFast, options.BacklogPolicy); - Assert.True(options.CheckCertificateRevocation); - - Assert.True(options.CommandMap.IsAvailable(RedisCommand.SELECT)); - Assert.False(options.CommandMap.IsAvailable(RedisCommand.GET)); - - Assert.Equal(TimeSpan.FromSeconds(124), TimeSpan.FromSeconds(options.ConfigCheckSeconds)); - Assert.Equal("TestConfigChannel", options.ConfigurationChannel); - Assert.Equal(123, options.ConnectRetry); - Assert.Equal(new Version(1, 2, 3, 4), options.DefaultVersion); - - Assert.Equal(TimeSpan.FromSeconds(125), TimeSpan.FromSeconds(options.KeepAlive)); - Assert.Equal(Proxy.Twemproxy, options.Proxy); - Assert.IsType(options.ReconnectRetryPolicy); - Assert.True(options.ResolveDns); - Assert.Equal(TimeSpan.FromSeconds(126), TimeSpan.FromMilliseconds(options.SyncTimeout)); - Assert.Equal("TestTiebreaker", options.TieBreaker); - } + public class TestAfterConnectOptionsProvider : DefaultOptionsProvider + { + public int Calls; - public class TestAfterConnectOptionsProvider : DefaultOptionsProvider + public override Task AfterConnectAsync(ConnectionMultiplexer muxer, Action log) { - public int Calls; - - public override Task AfterConnectAsync(ConnectionMultiplexer muxer, Action log) - { - Interlocked.Increment(ref Calls); - log("TestAfterConnectOptionsProvider.AfterConnectAsync!"); - return Task.CompletedTask; - } + Interlocked.Increment(ref Calls); + log("TestAfterConnectOptionsProvider.AfterConnectAsync!"); + return Task.CompletedTask; } + } - [Fact] - public async Task AfterConnectAsyncHandler() - { - var options = ConfigurationOptions.Parse(GetConfiguration()); - var provider = new TestAfterConnectOptionsProvider(); - options.Defaults = provider; + [Fact] + public async Task AfterConnectAsyncHandler() + { + var options = ConfigurationOptions.Parse(GetConfiguration()); + var provider = new TestAfterConnectOptionsProvider(); + options.Defaults = provider; - using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); + using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); - Assert.True(muxer.IsConnected); - Assert.Equal(1, provider.Calls); - } + Assert.True(conn.IsConnected); + Assert.Equal(1, provider.Calls); + } - public class TestClientNameOptionsProvider : DefaultOptionsProvider - { - protected override string GetDefaultClientName() => "Hey there"; - } + public class TestClientNameOptionsProvider : DefaultOptionsProvider + { + protected override string GetDefaultClientName() => "Hey there"; + } - [Fact] - public async Task ClientNameOverride() - { - var options = ConfigurationOptions.Parse(GetConfiguration()); - options.Defaults = new TestClientNameOptionsProvider(); + [Fact] + public async Task ClientNameOverride() + { + var options = ConfigurationOptions.Parse(GetConfiguration()); + options.Defaults = new TestClientNameOptionsProvider(); - using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); + using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); - Assert.True(muxer.IsConnected); - Assert.Equal("Hey there", muxer.ClientName); - } + Assert.True(conn.IsConnected); + Assert.Equal("Hey there", conn.ClientName); + } - [Fact] - public async Task ClientNameExplicitWins() - { - var options = ConfigurationOptions.Parse(GetConfiguration() + ",name=FooBar"); - options.Defaults = new TestClientNameOptionsProvider(); + [Fact] + public async Task ClientNameExplicitWins() + { + var options = ConfigurationOptions.Parse(GetConfiguration() + ",name=FooBar"); + options.Defaults = new TestClientNameOptionsProvider(); - using var muxer = await ConnectionMultiplexer.ConnectAsync(options, Writer); + using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); - Assert.True(muxer.IsConnected); - Assert.Equal("FooBar", muxer.ClientName); - } + Assert.True(conn.IsConnected); + Assert.Equal("FooBar", conn.ClientName); } } diff --git a/tests/StackExchange.Redis.Tests/DefaultPorts.cs b/tests/StackExchange.Redis.Tests/DefaultPorts.cs index 0e5b9cbcd..ef2c2d699 100644 --- a/tests/StackExchange.Redis.Tests/DefaultPorts.cs +++ b/tests/StackExchange.Redis.Tests/DefaultPorts.cs @@ -2,57 +2,56 @@ using System.Net; using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class DefaultPorts { - public class DefaultPorts + [Theory] + [InlineData("foo", 6379)] + [InlineData("foo:6379", 6379)] + [InlineData("foo:6380", 6380)] + [InlineData("foo,ssl=false", 6379)] + [InlineData("foo:6379,ssl=false", 6379)] + [InlineData("foo:6380,ssl=false", 6380)] + + [InlineData("foo,ssl=true", 6380)] + [InlineData("foo:6379,ssl=true", 6379)] + [InlineData("foo:6380,ssl=true", 6380)] + [InlineData("foo:6381,ssl=true", 6381)] + public void ConfigStringRoundTripWithDefaultPorts(string config, int expectedPort) { - [Theory] - [InlineData("foo", 6379)] - [InlineData("foo:6379", 6379)] - [InlineData("foo:6380", 6380)] - [InlineData("foo,ssl=false", 6379)] - [InlineData("foo:6379,ssl=false", 6379)] - [InlineData("foo:6380,ssl=false", 6380)] - - [InlineData("foo,ssl=true", 6380)] - [InlineData("foo:6379,ssl=true", 6379)] - [InlineData("foo:6380,ssl=true", 6380)] - [InlineData("foo:6381,ssl=true", 6381)] - public void ConfigStringRoundTripWithDefaultPorts(string config, int expectedPort) - { - var options = ConfigurationOptions.Parse(config); - string backAgain = options.ToString(); - Assert.Equal(config, backAgain.Replace("=True", "=true").Replace("=False", "=false")); + var options = ConfigurationOptions.Parse(config); + string backAgain = options.ToString(); + Assert.Equal(config, backAgain.Replace("=True", "=true").Replace("=False", "=false")); - options.SetDefaultPorts(); // normally it is the multiplexer that calls this, not us - Assert.Equal(expectedPort, ((DnsEndPoint)options.EndPoints.Single()).Port); - } + options.SetDefaultPorts(); // normally it is the multiplexer that calls this, not us + Assert.Equal(expectedPort, ((DnsEndPoint)options.EndPoints.Single()).Port); + } - [Theory] - [InlineData("foo", 0, false, 6379)] - [InlineData("foo", 6379, false, 6379)] - [InlineData("foo", 6380, false, 6380)] + [Theory] + [InlineData("foo", 0, false, 6379)] + [InlineData("foo", 6379, false, 6379)] + [InlineData("foo", 6380, false, 6380)] - [InlineData("foo", 0, true, 6380)] - [InlineData("foo", 6379, true, 6379)] - [InlineData("foo", 6380, true, 6380)] - [InlineData("foo", 6381, true, 6381)] + [InlineData("foo", 0, true, 6380)] + [InlineData("foo", 6379, true, 6379)] + [InlineData("foo", 6380, true, 6380)] + [InlineData("foo", 6381, true, 6381)] - public void ConfigManualWithDefaultPorts(string host, int port, bool useSsl, int expectedPort) + public void ConfigManualWithDefaultPorts(string host, int port, bool useSsl, int expectedPort) + { + var options = new ConfigurationOptions(); + if (port == 0) + { + options.EndPoints.Add(host); + } + else { - var options = new ConfigurationOptions(); - if (port == 0) - { - options.EndPoints.Add(host); - } - else - { - options.EndPoints.Add(host, port); - } - if (useSsl) options.Ssl = true; - - options.SetDefaultPorts(); // normally it is the multiplexer that calls this, not us - Assert.Equal(expectedPort, ((DnsEndPoint)options.EndPoints.Single()).Port); + options.EndPoints.Add(host, port); } + if (useSsl) options.Ssl = true; + + options.SetDefaultPorts(); // normally it is the multiplexer that calls this, not us + Assert.Equal(expectedPort, ((DnsEndPoint)options.EndPoints.Single()).Port); } -} \ No newline at end of file +} diff --git a/tests/StackExchange.Redis.Tests/Deprecated.cs b/tests/StackExchange.Redis.Tests/Deprecated.cs index 274f40a69..83efced7f 100644 --- a/tests/StackExchange.Redis.Tests/Deprecated.cs +++ b/tests/StackExchange.Redis.Tests/Deprecated.cs @@ -2,73 +2,72 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +/// +/// Testing that things we deprecate still parse, but are otherwise defaults. +/// +public class Deprecated : TestBase { - /// - /// Testing that things we deprecate still parse, but are otherwise defaults. - /// - public class Deprecated : TestBase - { - public Deprecated(ITestOutputHelper output) : base(output) { } + public Deprecated(ITestOutputHelper output) : base(output) { } #pragma warning disable CS0618 // Type or member is obsolete - [Fact] - public void HighPrioritySocketThreads() - { - Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.HighPrioritySocketThreads))!, typeof(ObsoleteAttribute))); - - var options = ConfigurationOptions.Parse("name=Hello"); - Assert.False(options.HighPrioritySocketThreads); - - options = ConfigurationOptions.Parse("highPriorityThreads=true"); - Assert.Equal("", options.ToString()); - Assert.False(options.HighPrioritySocketThreads); - - options = ConfigurationOptions.Parse("highPriorityThreads=false"); - Assert.Equal("", options.ToString()); - Assert.False(options.HighPrioritySocketThreads); - } - - [Fact] - public void PreserveAsyncOrder() - { - Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.PreserveAsyncOrder))!, typeof(ObsoleteAttribute))); - - var options = ConfigurationOptions.Parse("name=Hello"); - Assert.False(options.PreserveAsyncOrder); - - options = ConfigurationOptions.Parse("preserveAsyncOrder=true"); - Assert.Equal("", options.ToString()); - Assert.False(options.PreserveAsyncOrder); - - options = ConfigurationOptions.Parse("preserveAsyncOrder=false"); - Assert.Equal("", options.ToString()); - Assert.False(options.PreserveAsyncOrder); - } - - [Fact] - public void WriteBufferParse() - { - Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.WriteBuffer))!, typeof(ObsoleteAttribute))); - - var options = ConfigurationOptions.Parse("name=Hello"); - Assert.Equal(0, options.WriteBuffer); - - options = ConfigurationOptions.Parse("writeBuffer=8092"); - Assert.Equal(0, options.WriteBuffer); - } - - [Fact] - public void ResponseTimeout() - { - Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.ResponseTimeout))!, typeof(ObsoleteAttribute))); - - var options = ConfigurationOptions.Parse("name=Hello"); - Assert.Equal(0, options.ResponseTimeout); - - options = ConfigurationOptions.Parse("responseTimeout=1000"); - Assert.Equal(0, options.ResponseTimeout); - } -#pragma warning restore CS0618 + [Fact] + public void HighPrioritySocketThreads() + { + Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.HighPrioritySocketThreads))!, typeof(ObsoleteAttribute))); + + var options = ConfigurationOptions.Parse("name=Hello"); + Assert.False(options.HighPrioritySocketThreads); + + options = ConfigurationOptions.Parse("highPriorityThreads=true"); + Assert.Equal("", options.ToString()); + Assert.False(options.HighPrioritySocketThreads); + + options = ConfigurationOptions.Parse("highPriorityThreads=false"); + Assert.Equal("", options.ToString()); + Assert.False(options.HighPrioritySocketThreads); + } + + [Fact] + public void PreserveAsyncOrder() + { + Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.PreserveAsyncOrder))!, typeof(ObsoleteAttribute))); + + var options = ConfigurationOptions.Parse("name=Hello"); + Assert.False(options.PreserveAsyncOrder); + + options = ConfigurationOptions.Parse("preserveAsyncOrder=true"); + Assert.Equal("", options.ToString()); + Assert.False(options.PreserveAsyncOrder); + + options = ConfigurationOptions.Parse("preserveAsyncOrder=false"); + Assert.Equal("", options.ToString()); + Assert.False(options.PreserveAsyncOrder); + } + + [Fact] + public void WriteBufferParse() + { + Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.WriteBuffer))!, typeof(ObsoleteAttribute))); + + var options = ConfigurationOptions.Parse("name=Hello"); + Assert.Equal(0, options.WriteBuffer); + + options = ConfigurationOptions.Parse("writeBuffer=8092"); + Assert.Equal(0, options.WriteBuffer); } + + [Fact] + public void ResponseTimeout() + { + Assert.True(Attribute.IsDefined(typeof(ConfigurationOptions).GetProperty(nameof(ConfigurationOptions.ResponseTimeout))!, typeof(ObsoleteAttribute))); + + var options = ConfigurationOptions.Parse("name=Hello"); + Assert.Equal(0, options.ResponseTimeout); + + options = ConfigurationOptions.Parse("responseTimeout=1000"); + Assert.Equal(0, options.ResponseTimeout); + } +#pragma warning restore CS0618 } diff --git a/tests/StackExchange.Redis.Tests/EnvoyTests.cs b/tests/StackExchange.Redis.Tests/EnvoyTests.cs index e3654a570..fac91496c 100644 --- a/tests/StackExchange.Redis.Tests/EnvoyTests.cs +++ b/tests/StackExchange.Redis.Tests/EnvoyTests.cs @@ -3,49 +3,47 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class EnvoyTests : TestBase { - public class EnvoyTests : TestBase + public EnvoyTests(ITestOutputHelper output) : base(output) { } + + protected override string GetConfiguration() => TestConfig.Current.ProxyServerAndPort; + + /// + /// Tests basic envoy connection with the ability to set and get a key. + /// + [Fact] + public void TestBasicEnvoyConnection() { - public EnvoyTests(ITestOutputHelper output) : base(output) { } + var sb = new StringBuilder(); + Writer.EchoTo(sb); + try + { + using var conn = Create(configuration: GetConfiguration(), keepAlive: 1, connectTimeout: 2000, allowAdmin: true, shared: false, proxy: Proxy.Envoyproxy, log: Writer); + + var db = conn.GetDatabase(); + + const string key = "foobar"; + const string value = "barfoo"; + db.StringSet(key, value); - protected override string GetConfiguration() => TestConfig.Current.ProxyServerAndPort; + var expectedVal = db.StringGet(key); - /// - /// Tests basic envoy connection with the ability to set and get a key. - /// - [Fact] - public void TestBasicEnvoyConnection() + Assert.Equal(expectedVal, value); + } + catch (TimeoutException ex) when (ex.Message == "Connect timeout" || sb.ToString().Contains("Returned, but incorrectly")) + { + Skip.Inconclusive("Envoy server not found."); + } + catch (AggregateException) + { + Skip.Inconclusive("Envoy server not found."); + } + catch (RedisConnectionException) when (sb.ToString().Contains("It was not possible to connect to the redis server(s)")) { - var sb = new StringBuilder(); - Writer.EchoTo(sb); - try - { - using (var muxer = Create(configuration: GetConfiguration(), keepAlive: 1, connectTimeout: 2000, allowAdmin: true, shared: false, proxy: Proxy.Envoyproxy, log: Writer)) - { - var db = muxer.GetDatabase(); - - const string key = "foobar"; - const string value = "barfoo"; - db.StringSet(key, value); - - var expectedVal = db.StringGet(key); - - Assert.Equal(expectedVal, value); - } - } - catch (TimeoutException ex) when (ex.Message == "Connect timeout" || sb.ToString().Contains("Returned, but incorrectly")) - { - Skip.Inconclusive("Envoy server not found."); - } - catch (AggregateException) - { - Skip.Inconclusive("Envoy server not found."); - } - catch (RedisConnectionException) when (sb.ToString().Contains("It was not possible to connect to the redis server(s)")) - { - Skip.Inconclusive("Envoy server not found."); - } + Skip.Inconclusive("Envoy server not found."); } } } diff --git a/tests/StackExchange.Redis.Tests/EventArgsTests.cs b/tests/StackExchange.Redis.Tests/EventArgsTests.cs index 227197c96..27245f1ec 100644 --- a/tests/StackExchange.Redis.Tests/EventArgsTests.cs +++ b/tests/StackExchange.Redis.Tests/EventArgsTests.cs @@ -2,81 +2,80 @@ using NSubstitute; using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class EventArgsTests { - public class EventArgsTests + [Fact] + public void EventArgsCanBeSubstituted() { - [Fact] - public void EventArgsCanBeSubstituted() - { - EndPointEventArgs endpointArgsMock - = Substitute.For(default, default); + EndPointEventArgs endpointArgsMock + = Substitute.For(default, default); - RedisErrorEventArgs redisErrorArgsMock - = Substitute.For(default, default, default); + RedisErrorEventArgs redisErrorArgsMock + = Substitute.For(default, default, default); - ConnectionFailedEventArgs connectionFailedArgsMock - = Substitute.For(default, default, default, default, default, default); + ConnectionFailedEventArgs connectionFailedArgsMock + = Substitute.For(default, default, default, default, default, default); - InternalErrorEventArgs internalErrorArgsMock - = Substitute.For(default, default, default, default, default); + InternalErrorEventArgs internalErrorArgsMock + = Substitute.For(default, default, default, default, default); - HashSlotMovedEventArgs hashSlotMovedArgsMock - = Substitute.For(default, default, default, default); + HashSlotMovedEventArgs hashSlotMovedArgsMock + = Substitute.For(default, default, default, default); - DiagnosticStub stub = new DiagnosticStub(); + DiagnosticStub stub = new DiagnosticStub(); - stub.ConfigurationChangedBroadcastHandler(default, endpointArgsMock); - Assert.Equal(stub.Message,DiagnosticStub.ConfigurationChangedBroadcastHandlerMessage); + stub.ConfigurationChangedBroadcastHandler(default, endpointArgsMock); + Assert.Equal(stub.Message,DiagnosticStub.ConfigurationChangedBroadcastHandlerMessage); - stub.ErrorMessageHandler(default, redisErrorArgsMock); - Assert.Equal(stub.Message, DiagnosticStub.ErrorMessageHandlerMessage); + stub.ErrorMessageHandler(default, redisErrorArgsMock); + Assert.Equal(stub.Message, DiagnosticStub.ErrorMessageHandlerMessage); - stub.ConnectionFailedHandler(default, connectionFailedArgsMock); - Assert.Equal(stub.Message, DiagnosticStub.ConnectionFailedHandlerMessage); + stub.ConnectionFailedHandler(default, connectionFailedArgsMock); + Assert.Equal(stub.Message, DiagnosticStub.ConnectionFailedHandlerMessage); - stub.InternalErrorHandler(default, internalErrorArgsMock); - Assert.Equal(stub.Message, DiagnosticStub.InternalErrorHandlerMessage); + stub.InternalErrorHandler(default, internalErrorArgsMock); + Assert.Equal(stub.Message, DiagnosticStub.InternalErrorHandlerMessage); - stub.ConnectionRestoredHandler(default, connectionFailedArgsMock); - Assert.Equal(stub.Message, DiagnosticStub.ConnectionRestoredHandlerMessage); + stub.ConnectionRestoredHandler(default, connectionFailedArgsMock); + Assert.Equal(stub.Message, DiagnosticStub.ConnectionRestoredHandlerMessage); - stub.ConfigurationChangedHandler(default, endpointArgsMock); - Assert.Equal(stub.Message, DiagnosticStub.ConfigurationChangedHandlerMessage); + stub.ConfigurationChangedHandler(default, endpointArgsMock); + Assert.Equal(stub.Message, DiagnosticStub.ConfigurationChangedHandlerMessage); - stub.HashSlotMovedHandler(default, hashSlotMovedArgsMock); - Assert.Equal(stub.Message, DiagnosticStub.HashSlotMovedHandlerMessage); - } + stub.HashSlotMovedHandler(default, hashSlotMovedArgsMock); + Assert.Equal(stub.Message, DiagnosticStub.HashSlotMovedHandlerMessage); + } - public class DiagnosticStub + public class DiagnosticStub + { + public const string ConfigurationChangedBroadcastHandlerMessage = "ConfigurationChangedBroadcastHandler invoked"; + public const string ErrorMessageHandlerMessage = "ErrorMessageHandler invoked"; + public const string ConnectionFailedHandlerMessage = "ConnectionFailedHandler invoked"; + public const string InternalErrorHandlerMessage = "InternalErrorHandler invoked"; + public const string ConnectionRestoredHandlerMessage = "ConnectionRestoredHandler invoked"; + public const string ConfigurationChangedHandlerMessage = "ConfigurationChangedHandler invoked"; + public const string HashSlotMovedHandlerMessage = "HashSlotMovedHandler invoked"; + + public DiagnosticStub() { - public const string ConfigurationChangedBroadcastHandlerMessage = "ConfigurationChangedBroadcastHandler invoked"; - public const string ErrorMessageHandlerMessage = "ErrorMessageHandler invoked"; - public const string ConnectionFailedHandlerMessage = "ConnectionFailedHandler invoked"; - public const string InternalErrorHandlerMessage = "InternalErrorHandler invoked"; - public const string ConnectionRestoredHandlerMessage = "ConnectionRestoredHandler invoked"; - public const string ConfigurationChangedHandlerMessage = "ConfigurationChangedHandler invoked"; - public const string HashSlotMovedHandlerMessage = "HashSlotMovedHandler invoked"; - - public DiagnosticStub() - { - ConfigurationChangedBroadcastHandler = (obj, args) => Message = ConfigurationChangedBroadcastHandlerMessage; - ErrorMessageHandler = (obj, args) => Message = ErrorMessageHandlerMessage; - ConnectionFailedHandler = (obj, args) => Message = ConnectionFailedHandlerMessage; - InternalErrorHandler = (obj, args) => Message = InternalErrorHandlerMessage; - ConnectionRestoredHandler = (obj, args) => Message = ConnectionRestoredHandlerMessage; - ConfigurationChangedHandler = (obj, args) => Message = ConfigurationChangedHandlerMessage; - HashSlotMovedHandler = (obj, args) => Message = HashSlotMovedHandlerMessage; - } - - public string? Message { get; private set; } - public Action ConfigurationChangedBroadcastHandler { get; } - public Action ErrorMessageHandler { get; } - public Action ConnectionFailedHandler { get; } - public Action InternalErrorHandler { get; } - public Action ConnectionRestoredHandler { get; } - public Action ConfigurationChangedHandler { get; } - public Action HashSlotMovedHandler { get; } + ConfigurationChangedBroadcastHandler = (obj, args) => Message = ConfigurationChangedBroadcastHandlerMessage; + ErrorMessageHandler = (obj, args) => Message = ErrorMessageHandlerMessage; + ConnectionFailedHandler = (obj, args) => Message = ConnectionFailedHandlerMessage; + InternalErrorHandler = (obj, args) => Message = InternalErrorHandlerMessage; + ConnectionRestoredHandler = (obj, args) => Message = ConnectionRestoredHandlerMessage; + ConfigurationChangedHandler = (obj, args) => Message = ConfigurationChangedHandlerMessage; + HashSlotMovedHandler = (obj, args) => Message = HashSlotMovedHandlerMessage; } + + public string? Message { get; private set; } + public Action ConfigurationChangedBroadcastHandler { get; } + public Action ErrorMessageHandler { get; } + public Action ConnectionFailedHandler { get; } + public Action InternalErrorHandler { get; } + public Action ConnectionRestoredHandler { get; } + public Action ConfigurationChangedHandler { get; } + public Action HashSlotMovedHandler { get; } } } diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index ff319be13..1098a034c 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -2,237 +2,232 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class ExceptionFactoryTests : TestBase { - public class ExceptionFactoryTests : TestBase + public ExceptionFactoryTests(ITestOutputHelper output) : base (output) { } + + [Fact] + public void NullLastException() { - public ExceptionFactoryTests(ITestOutputHelper output) : base (output) { } + using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true); - [Fact] - public void NullLastException() - { - using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true)) - { - muxer.GetDatabase(); - Assert.Null(muxer.GetServerSnapshot()[0].LastException); - var ex = ExceptionFactory.NoConnectionAvailable((muxer as ConnectionMultiplexer)!, null, null); - Assert.Null(ex.InnerException); - } - } + conn.GetDatabase(); + Assert.Null(conn.GetServerSnapshot()[0].LastException); + var ex = ExceptionFactory.NoConnectionAvailable((conn as ConnectionMultiplexer)!, null, null); + Assert.Null(ex.InnerException); + } - [Fact] - public void CanGetVersion() - { - var libVer = Utils.GetLibVersion(); - Assert.Matches(@"2\.[0-9]+\.[0-9]+(\.[0-9]+)?", libVer); - } + [Fact] + public void CanGetVersion() + { + var libVer = Utils.GetLibVersion(); + Assert.Matches(@"2\.[0-9]+\.[0-9]+(\.[0-9]+)?", libVer); + } #if DEBUG - [Fact] - public void MultipleEndpointsThrowConnectionException() + [Fact] + public void MultipleEndpointsThrowConnectionException() + { + try { - try - { - using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false)) - { - muxer.GetDatabase(); - muxer.AllowConnect = false; - - foreach (var endpoint in muxer.GetEndPoints()) - { - muxer.GetServer(endpoint).SimulateConnectionFailure(SimulatedFailureType.All); - } - - var ex = ExceptionFactory.NoConnectionAvailable((muxer as ConnectionMultiplexer)!, null, null); - var outer = Assert.IsType(ex); - Assert.Equal(ConnectionFailureType.UnableToResolvePhysicalConnection, outer.FailureType); - var inner = Assert.IsType(outer.InnerException); - Assert.True(inner.FailureType == ConnectionFailureType.SocketFailure - || inner.FailureType == ConnectionFailureType.InternalFailure); - } - } - finally + using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false); + + conn.GetDatabase(); + conn.AllowConnect = false; + + foreach (var endpoint in conn.GetEndPoints()) { - ClearAmbientFailures(); + conn.GetServer(endpoint).SimulateConnectionFailure(SimulatedFailureType.All); } + + var ex = ExceptionFactory.NoConnectionAvailable((conn as ConnectionMultiplexer)!, null, null); + var outer = Assert.IsType(ex); + Assert.Equal(ConnectionFailureType.UnableToResolvePhysicalConnection, outer.FailureType); + var inner = Assert.IsType(outer.InnerException); + Assert.True(inner.FailureType == ConnectionFailureType.SocketFailure + || inner.FailureType == ConnectionFailureType.InternalFailure); } + finally + { + ClearAmbientFailures(); + } + } #endif - [Fact] - public void ServerTakesPrecendenceOverSnapshot() + [Fact] + public void ServerTakesPrecendenceOverSnapshot() + { + try { - try - { - using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast)) - { - muxer.GetDatabase(); - muxer.AllowConnect = false; + using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); - muxer.GetServer(muxer.GetEndPoints()[0]).SimulateConnectionFailure(SimulatedFailureType.All); + conn.GetDatabase(); + conn.AllowConnect = false; - var ex = ExceptionFactory.NoConnectionAvailable((muxer as ConnectionMultiplexer)!, null, muxer.GetServerSnapshot()[0]); - Assert.IsType(ex); - Assert.IsType(ex.InnerException); - Assert.Equal(ex.InnerException, muxer.GetServerSnapshot()[0].LastException); - } - } - finally - { - ClearAmbientFailures(); - } + conn.GetServer(conn.GetEndPoints()[0]).SimulateConnectionFailure(SimulatedFailureType.All); + + var ex = ExceptionFactory.NoConnectionAvailable((conn as ConnectionMultiplexer)!, null, conn.GetServerSnapshot()[0]); + Assert.IsType(ex); + Assert.IsType(ex.InnerException); + Assert.Equal(ex.InnerException, conn.GetServerSnapshot()[0].LastException); } + finally + { + ClearAmbientFailures(); + } + } - [Fact] - public void NullInnerExceptionForMultipleEndpointsWithNoLastException() + [Fact] + public void NullInnerExceptionForMultipleEndpointsWithNoLastException() + { + try { - try - { - using (var muxer = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true)) - { - muxer.GetDatabase(); - muxer.AllowConnect = false; - var ex = ExceptionFactory.NoConnectionAvailable((muxer as ConnectionMultiplexer)!, null, null); - Assert.IsType(ex); - Assert.Null(ex.InnerException); - } - } - finally - { - ClearAmbientFailures(); - } + using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true); + + conn.GetDatabase(); + conn.AllowConnect = false; + var ex = ExceptionFactory.NoConnectionAvailable((conn as ConnectionMultiplexer)!, null, null); + Assert.IsType(ex); + Assert.Null(ex.InnerException); + } + finally + { + ClearAmbientFailures(); } + } - [Fact] - public void TimeoutException() + [Fact] + public void TimeoutException() + { + try { - try - { - using (var muxer = (Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false) as ConnectionMultiplexer)!) - { - var server = GetServer(muxer); - muxer.AllowConnect = false; - var msg = Message.Create(-1, CommandFlags.None, RedisCommand.PING); - var rawEx = ExceptionFactory.Timeout(muxer, "Test Timeout", msg, new ServerEndPoint(muxer, server.EndPoint)); - var ex = Assert.IsType(rawEx); - Writer.WriteLine("Exception: " + ex.Message); - - // Example format: "Test Timeout, command=PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, serverEndpoint: 127.0.0.1:6379, mgr: 10 of 10 available, clientName: TimeoutException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), v: 2.1.0 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)"; - Assert.StartsWith("Test Timeout, command=PING", ex.Message); - Assert.Contains("clientName: " + nameof(TimeoutException), ex.Message); - // Ensure our pipe numbers are in place - Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); - Assert.Contains("mc: 1/1/0", ex.Message); - Assert.Contains("serverEndpoint: " + server.EndPoint, ex.Message); - Assert.Contains("IOCP: ", ex.Message); - Assert.Contains("WORKER: ", ex.Message); + using var conn = (Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false) as ConnectionMultiplexer)!; + + var server = GetServer(conn); + conn.AllowConnect = false; + var msg = Message.Create(-1, CommandFlags.None, RedisCommand.PING); + var rawEx = ExceptionFactory.Timeout(conn, "Test Timeout", msg, new ServerEndPoint(conn, server.EndPoint)); + var ex = Assert.IsType(rawEx); + Writer.WriteLine("Exception: " + ex.Message); + + // Example format: "Test Timeout, command=PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, serverEndpoint: 127.0.0.1:6379, mgr: 10 of 10 available, clientName: TimeoutException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), v: 2.1.0 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)"; + Assert.StartsWith("Test Timeout, command=PING", ex.Message); + Assert.Contains("clientName: " + nameof(TimeoutException), ex.Message); + // Ensure our pipe numbers are in place + Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); + Assert.Contains("mc: 1/1/0", ex.Message); + Assert.Contains("serverEndpoint: " + server.EndPoint, ex.Message); + Assert.Contains("IOCP: ", ex.Message); + Assert.Contains("WORKER: ", ex.Message); #if NETCOREAPP - Assert.Contains("POOL: ", ex.Message); + Assert.Contains("POOL: ", ex.Message); #endif - Assert.DoesNotContain("Unspecified/", ex.Message); - Assert.EndsWith(" (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)", ex.Message); - Assert.Null(ex.InnerException); - } + Assert.DoesNotContain("Unspecified/", ex.Message); + Assert.EndsWith(" (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)", ex.Message); + Assert.Null(ex.InnerException); + } + finally + { + ClearAmbientFailures(); + } + } + + [Theory] + [InlineData(false, 0, 0, true, "Connection to Redis never succeeded (attempts: 0 - connection likely in-progress), unable to service operation: PING")] + [InlineData(false, 1, 0, true, "Connection to Redis never succeeded (attempts: 1 - connection likely in-progress), unable to service operation: PING")] + [InlineData(false, 12, 0, true, "Connection to Redis never succeeded (attempts: 12 - check your config), unable to service operation: PING")] + [InlineData(false, 0, 0, false, "Connection to Redis never succeeded (attempts: 0 - connection likely in-progress), unable to service operation: PING")] + [InlineData(false, 1, 0, false, "Connection to Redis never succeeded (attempts: 1 - connection likely in-progress), unable to service operation: PING")] + [InlineData(false, 12, 0, false, "Connection to Redis never succeeded (attempts: 12 - check your config), unable to service operation: PING")] + [InlineData(true, 0, 0, true, "No connection is active/available to service this operation: PING")] + [InlineData(true, 1, 0, true, "No connection is active/available to service this operation: PING")] + [InlineData(true, 12, 0, true, "No connection is active/available to service this operation: PING")] + public void NoConnectionException(bool abortOnConnect, int connCount, int completeCount, bool hasDetail, string messageStart) + { + try + { + var options = new ConfigurationOptions() + { + AbortOnConnectFail = abortOnConnect, + BacklogPolicy = BacklogPolicy.FailFast, + ConnectTimeout = 1000, + SyncTimeout = 500, + KeepAlive = 5000 + }; + + ConnectionMultiplexer conn; + if (abortOnConnect) + { + options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); + conn = ConnectionMultiplexer.Connect(options, Writer); } - finally + else { - ClearAmbientFailures(); + options.EndPoints.Add($"doesnot.exist.{Guid.NewGuid():N}:6379"); + conn = ConnectionMultiplexer.Connect(options, Writer); } - } - [Theory] - [InlineData(false, 0, 0, true, "Connection to Redis never succeeded (attempts: 0 - connection likely in-progress), unable to service operation: PING")] - [InlineData(false, 1, 0, true, "Connection to Redis never succeeded (attempts: 1 - connection likely in-progress), unable to service operation: PING")] - [InlineData(false, 12, 0, true, "Connection to Redis never succeeded (attempts: 12 - check your config), unable to service operation: PING")] - [InlineData(false, 0, 0, false, "Connection to Redis never succeeded (attempts: 0 - connection likely in-progress), unable to service operation: PING")] - [InlineData(false, 1, 0, false, "Connection to Redis never succeeded (attempts: 1 - connection likely in-progress), unable to service operation: PING")] - [InlineData(false, 12, 0, false, "Connection to Redis never succeeded (attempts: 12 - check your config), unable to service operation: PING")] - [InlineData(true, 0, 0, true, "No connection is active/available to service this operation: PING")] - [InlineData(true, 1, 0, true, "No connection is active/available to service this operation: PING")] - [InlineData(true, 12, 0, true, "No connection is active/available to service this operation: PING")] - public void NoConnectionException(bool abortOnConnect, int connCount, int completeCount, bool hasDetail, string messageStart) - { - try + using (conn) { - var options = new ConfigurationOptions() - { - AbortOnConnectFail = abortOnConnect, - BacklogPolicy = BacklogPolicy.FailFast, - ConnectTimeout = 1000, - SyncTimeout = 500, - KeepAlive = 5000 - }; - - ConnectionMultiplexer muxer; - if (abortOnConnect) + var server = conn.GetServer(conn.GetEndPoints()[0]); + conn.AllowConnect = false; + conn._connectAttemptCount = connCount; + conn._connectCompletedCount = completeCount; + options.IncludeDetailInExceptions = hasDetail; + options.IncludePerformanceCountersInExceptions = hasDetail; + + var msg = Message.Create(-1, CommandFlags.None, RedisCommand.PING); + var rawEx = ExceptionFactory.NoConnectionAvailable(conn, msg, new ServerEndPoint(conn, server.EndPoint)); + var ex = Assert.IsType(rawEx); + Writer.WriteLine("Exception: " + ex.Message); + + // Example format: "Exception: No connection is active/available to service this operation: PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, serverEndpoint: 127.0.0.1:6379, mc: 1/1/0, mgr: 10 of 10 available, clientName: NoConnectionException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), Local-CPU: 100%, v: 2.1.0.5"; + Assert.StartsWith(messageStart, ex.Message); + + // Ensure our pipe numbers are in place if they should be + if (hasDetail) { - options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); - muxer = ConnectionMultiplexer.Connect(options, Writer); + Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); + Assert.Contains($"mc: {connCount}/{completeCount}/0", ex.Message); + Assert.Contains("serverEndpoint: " + server.EndPoint.ToString()?.Replace("Unspecified/", ""), ex.Message); } else { - options.EndPoints.Add($"doesnot.exist.{Guid.NewGuid():N}:6379"); - muxer = ConnectionMultiplexer.Connect(options, Writer); - } - - using (muxer) - { - var server = muxer.GetServer(muxer.GetEndPoints()[0]); - muxer.AllowConnect = false; - muxer._connectAttemptCount = connCount; - muxer._connectCompletedCount = completeCount; - options.IncludeDetailInExceptions = hasDetail; - options.IncludePerformanceCountersInExceptions = hasDetail; - - var msg = Message.Create(-1, CommandFlags.None, RedisCommand.PING); - var rawEx = ExceptionFactory.NoConnectionAvailable(muxer, msg, new ServerEndPoint(muxer, server.EndPoint)); - var ex = Assert.IsType(rawEx); - Writer.WriteLine("Exception: " + ex.Message); - - // Example format: "Exception: No connection is active/available to service this operation: PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, serverEndpoint: 127.0.0.1:6379, mc: 1/1/0, mgr: 10 of 10 available, clientName: NoConnectionException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), Local-CPU: 100%, v: 2.1.0.5"; - Assert.StartsWith(messageStart, ex.Message); - - // Ensure our pipe numbers are in place if they should be - if (hasDetail) - { - Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); - Assert.Contains($"mc: {connCount}/{completeCount}/0", ex.Message); - Assert.Contains("serverEndpoint: " + server.EndPoint.ToString()?.Replace("Unspecified/", ""), ex.Message); - } - else - { - Assert.DoesNotContain("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); - Assert.DoesNotContain($"mc: {connCount}/{completeCount}/0", ex.Message); - Assert.DoesNotContain("serverEndpoint: " + server.EndPoint.ToString()?.Replace("Unspecified/", ""), ex.Message); - } - Assert.DoesNotContain("Unspecified/", ex.Message); + Assert.DoesNotContain("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); + Assert.DoesNotContain($"mc: {connCount}/{completeCount}/0", ex.Message); + Assert.DoesNotContain("serverEndpoint: " + server.EndPoint.ToString()?.Replace("Unspecified/", ""), ex.Message); } - } - finally - { - ClearAmbientFailures(); + Assert.DoesNotContain("Unspecified/", ex.Message); } } - - [Theory] - [InlineData(true, ConnectionFailureType.ProtocolFailure, "ProtocolFailure on [0]:GET myKey (StringProcessor), my annotation")] - [InlineData(true, ConnectionFailureType.ConnectionDisposed, "ConnectionDisposed on [0]:GET myKey (StringProcessor), my annotation")] - [InlineData(false, ConnectionFailureType.ProtocolFailure, "ProtocolFailure on [0]:GET (StringProcessor), my annotation")] - [InlineData(false, ConnectionFailureType.ConnectionDisposed, "ConnectionDisposed on [0]:GET (StringProcessor), my annotation")] - public void MessageFail(bool includeDetail, ConnectionFailureType failType, string messageStart) + finally { - using var muxer = Create(shared: false); - muxer.RawConfig.IncludeDetailInExceptions = includeDetail; + ClearAmbientFailures(); + } + } - var message = Message.Create(0, CommandFlags.None, RedisCommand.GET, (RedisKey)"myKey"); - var resultBox = SimpleResultBox.Create(); - message.SetSource(ResultProcessor.String, resultBox); + [Theory] + [InlineData(true, ConnectionFailureType.ProtocolFailure, "ProtocolFailure on [0]:GET myKey (StringProcessor), my annotation")] + [InlineData(true, ConnectionFailureType.ConnectionDisposed, "ConnectionDisposed on [0]:GET myKey (StringProcessor), my annotation")] + [InlineData(false, ConnectionFailureType.ProtocolFailure, "ProtocolFailure on [0]:GET (StringProcessor), my annotation")] + [InlineData(false, ConnectionFailureType.ConnectionDisposed, "ConnectionDisposed on [0]:GET (StringProcessor), my annotation")] + public void MessageFail(bool includeDetail, ConnectionFailureType failType, string messageStart) + { + using var conn = Create(shared: false); - message.Fail(failType, null, "my annotation", muxer as ConnectionMultiplexer); + conn.RawConfig.IncludeDetailInExceptions = includeDetail; - resultBox.GetResult(out var ex); - Assert.NotNull(ex); + var message = Message.Create(0, CommandFlags.None, RedisCommand.GET, (RedisKey)"myKey"); + var resultBox = SimpleResultBox.Create(); + message.SetSource(ResultProcessor.String, resultBox); - Assert.StartsWith(messageStart, ex.Message); - } + message.Fail(failType, null, "my annotation", conn as ConnectionMultiplexer); + + resultBox.GetResult(out var ex); + Assert.NotNull(ex); + + Assert.StartsWith(messageStart, ex.Message); } } diff --git a/tests/StackExchange.Redis.Tests/Execute.cs b/tests/StackExchange.Redis.Tests/Execute.cs index 99cb25488..30012001a 100644 --- a/tests/StackExchange.Redis.Tests/Execute.cs +++ b/tests/StackExchange.Redis.Tests/Execute.cs @@ -3,42 +3,39 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class ExecuteTests : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class ExecuteTests : TestBase + public ExecuteTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public async Task DBExecute() + { + using var conn = Create(); + + var db = conn.GetDatabase(4); + RedisKey key = Me(); + db.StringSet(key, "some value"); + + var actual = (string?)db.Execute("GET", key); + Assert.Equal("some value", actual); + + actual = (string?)await db.ExecuteAsync("GET", key).ForAwait(); + Assert.Equal("some value", actual); + } + + [Fact] + public async Task ServerExecute() { - public ExecuteTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - - [Fact] - public async Task DBExecute() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(4); - RedisKey key = Me(); - db.StringSet(key, "some value"); - - var actual = (string?)db.Execute("GET", key); - Assert.Equal("some value", actual); - - actual = (string?)await db.ExecuteAsync("GET", key).ForAwait(); - Assert.Equal("some value", actual); - } - } - - [Fact] - public async Task ServerExecute() - { - using (var conn = Create()) - { - var server = conn.GetServer(conn.GetEndPoints().First()); - var actual = (string?)server.Execute("echo", "some value"); - Assert.Equal("some value", actual); - - actual = (string?)await server.ExecuteAsync("echo", "some value").ForAwait(); - Assert.Equal("some value", actual); - } - } + using var conn = Create(); + + var server = conn.GetServer(conn.GetEndPoints().First()); + var actual = (string?)server.Execute("echo", "some value"); + Assert.Equal("some value", actual); + + actual = (string?)await server.ExecuteAsync("echo", "some value").ForAwait(); + Assert.Equal("some value", actual); } } diff --git a/tests/StackExchange.Redis.Tests/Expiry.cs b/tests/StackExchange.Redis.Tests/Expiry.cs index 922dbaf91..f29cd617d 100644 --- a/tests/StackExchange.Redis.Tests/Expiry.cs +++ b/tests/StackExchange.Redis.Tests/Expiry.cs @@ -3,197 +3,194 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Expiry : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Expiry : TestBase + public Expiry(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + + private static string[]? GetMap(bool disablePTimes) => disablePTimes ? (new[] { "pexpire", "pexpireat", "pttl" }) : null; + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task TestBasicExpiryTimeSpan(bool disablePTimes) + { + using var conn = Create(disabledCommands: GetMap(disablePTimes)); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + var a = db.KeyTimeToLiveAsync(key); + db.KeyExpire(key, TimeSpan.FromHours(1), CommandFlags.FireAndForget); + var b = db.KeyTimeToLiveAsync(key); + db.KeyExpire(key, (TimeSpan?)null, CommandFlags.FireAndForget); + var c = db.KeyTimeToLiveAsync(key); + db.KeyExpire(key, TimeSpan.FromHours(1.5), CommandFlags.FireAndForget); + var d = db.KeyTimeToLiveAsync(key); + db.KeyExpire(key, TimeSpan.MaxValue, CommandFlags.FireAndForget); + var e = db.KeyTimeToLiveAsync(key); + db.KeyDelete(key, CommandFlags.FireAndForget); + var f = db.KeyTimeToLiveAsync(key); + + Assert.Null(await a); + var time = await b; + Assert.NotNull(time); + Assert.True(time > TimeSpan.FromMinutes(59.9) && time <= TimeSpan.FromMinutes(60)); + Assert.Null(await c); + time = await d; + Assert.NotNull(time); + Assert.True(time > TimeSpan.FromMinutes(89.9) && time <= TimeSpan.FromMinutes(90)); + Assert.Null(await e); + Assert.Null(await f); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestExpiryOptions(bool disablePTimes) + { + using var conn = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); + + var key = Me(); + var cb = conn.GetDatabase(); + cb.KeyDelete(key, CommandFlags.FireAndForget); + cb.StringSet(key, "value", flags: CommandFlags.FireAndForget); + + // The key has no expiry + Assert.False(cb.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasExpiry)); + Assert.True(cb.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasNoExpiry)); + + // The key has an existing expiry + Assert.True(cb.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasExpiry)); + Assert.False(cb.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasNoExpiry)); + + // Set only when the new expiry is greater than current one + Assert.True(cb.KeyExpire(key, TimeSpan.FromHours(1.5), ExpireWhen.GreaterThanCurrentExpiry)); + Assert.False(cb.KeyExpire(key, TimeSpan.FromHours(0.5), ExpireWhen.GreaterThanCurrentExpiry)); + + // Set only when the new expiry is less than current one + Assert.True(cb.KeyExpire(key, TimeSpan.FromHours(0.5), ExpireWhen.LessThanCurrentExpiry)); + Assert.False(cb.KeyExpire(key, TimeSpan.FromHours(1.5), ExpireWhen.LessThanCurrentExpiry)); + } + + [Theory] + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public async Task TestBasicExpiryDateTime(bool disablePTimes, bool utc) + { + using var conn = Create(disabledCommands: GetMap(disablePTimes)); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + var now = utc ? DateTime.UtcNow : DateTime.Now; + var serverTime = GetServer(conn).Time(); + Log("Server time: {0}", serverTime); + var offset = DateTime.UtcNow - serverTime; + + Log("Now (local time): {0}", now); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + var a = db.KeyTimeToLiveAsync(key); + db.KeyExpire(key, now.AddHours(1), CommandFlags.FireAndForget); + var b = db.KeyTimeToLiveAsync(key); + db.KeyExpire(key, (DateTime?)null, CommandFlags.FireAndForget); + var c = db.KeyTimeToLiveAsync(key); + db.KeyExpire(key, now.AddHours(1.5), CommandFlags.FireAndForget); + var d = db.KeyTimeToLiveAsync(key); + db.KeyExpire(key, DateTime.MaxValue, CommandFlags.FireAndForget); + var e = db.KeyTimeToLiveAsync(key); + db.KeyDelete(key, CommandFlags.FireAndForget); + var f = db.KeyTimeToLiveAsync(key); + + Assert.Null(await a); + var timeResult = await b; + Assert.NotNull(timeResult); + TimeSpan time = timeResult.Value; + + // Adjust for server time offset, if any when checking expectations + time -= offset; + + Log("Time: {0}, Expected: {1}-{2}", time, TimeSpan.FromMinutes(59), TimeSpan.FromMinutes(60)); + Assert.True(time >= TimeSpan.FromMinutes(59)); + Assert.True(time <= TimeSpan.FromMinutes(60.1)); + Assert.Null(await c); + + timeResult = await d; + Assert.NotNull(timeResult); + time = timeResult.Value; + + Assert.True(time >= TimeSpan.FromMinutes(89)); + Assert.True(time <= TimeSpan.FromMinutes(90.1)); + Assert.Null(await e); + Assert.Null(await f); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void KeyExpiryTime(bool disablePTimes) + { + using var conn = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); + + var key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + var expireTime = DateTime.UtcNow.AddHours(1); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + db.KeyExpire(key, expireTime, CommandFlags.FireAndForget); + + var time = db.KeyExpireTime(key); + Assert.NotNull(time); + Assert.Equal(expireTime, time.Value, TimeSpan.FromSeconds(30)); + + // Without associated expiration time + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + time = db.KeyExpireTime(key); + Assert.Null(time); + + // Non existing key + db.KeyDelete(key, CommandFlags.FireAndForget); + time = db.KeyExpireTime(key); + Assert.Null(time); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task KeyExpiryTimeAsync(bool disablePTimes) { - public Expiry(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } - - private static string[]? GetMap(bool disablePTimes) => disablePTimes ? (new[] { "pexpire", "pexpireat", "pttl" }) : null; - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task TestBasicExpiryTimeSpan(bool disablePTimes) - { - using (var muxer = Create(disabledCommands: GetMap(disablePTimes))) - { - RedisKey key = Me(); - var conn = muxer.GetDatabase(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - - conn.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - var a = conn.KeyTimeToLiveAsync(key); - conn.KeyExpire(key, TimeSpan.FromHours(1), CommandFlags.FireAndForget); - var b = conn.KeyTimeToLiveAsync(key); - conn.KeyExpire(key, (TimeSpan?)null, CommandFlags.FireAndForget); - var c = conn.KeyTimeToLiveAsync(key); - conn.KeyExpire(key, TimeSpan.FromHours(1.5), CommandFlags.FireAndForget); - var d = conn.KeyTimeToLiveAsync(key); - conn.KeyExpire(key, TimeSpan.MaxValue, CommandFlags.FireAndForget); - var e = conn.KeyTimeToLiveAsync(key); - conn.KeyDelete(key, CommandFlags.FireAndForget); - var f = conn.KeyTimeToLiveAsync(key); - - Assert.Null(await a); - var time = await b; - Assert.NotNull(time); - Assert.True(time > TimeSpan.FromMinutes(59.9) && time <= TimeSpan.FromMinutes(60)); - Assert.Null(await c); - time = await d; - Assert.NotNull(time); - Assert.True(time > TimeSpan.FromMinutes(89.9) && time <= TimeSpan.FromMinutes(90)); - Assert.Null(await e); - Assert.Null(await f); - } - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void TestExpiryOptions(bool disablePTimes) - { - using var muxer = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); - - var key = Me(); - var conn = muxer.GetDatabase(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - conn.StringSet(key, "value", flags: CommandFlags.FireAndForget); - - // The key has no expiry - Assert.False(conn.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasExpiry)); - Assert.True(conn.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasNoExpiry)); - - // The key has an existing expiry - Assert.True(conn.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasExpiry)); - Assert.False(conn.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasNoExpiry)); - - // Set only when the new expiry is greater than current one - Assert.True(conn.KeyExpire(key, TimeSpan.FromHours(1.5), ExpireWhen.GreaterThanCurrentExpiry)); - Assert.False(conn.KeyExpire(key, TimeSpan.FromHours(0.5), ExpireWhen.GreaterThanCurrentExpiry)); - - // Set only when the new expiry is less than current one - Assert.True(conn.KeyExpire(key, TimeSpan.FromHours(0.5), ExpireWhen.LessThanCurrentExpiry)); - Assert.False(conn.KeyExpire(key, TimeSpan.FromHours(1.5), ExpireWhen.LessThanCurrentExpiry)); - } - - [Theory] - [InlineData(true, true)] - [InlineData(false, true)] - [InlineData(true, false)] - [InlineData(false, false)] - public async Task TestBasicExpiryDateTime(bool disablePTimes, bool utc) - { - using (var muxer = Create(disabledCommands: GetMap(disablePTimes))) - { - RedisKey key = Me(); - var conn = muxer.GetDatabase(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - - var now = utc ? DateTime.UtcNow : DateTime.Now; - var serverTime = GetServer(muxer).Time(); - Log("Server time: {0}", serverTime); - var offset = DateTime.UtcNow - serverTime; - - Log("Now (local time): {0}", now); - conn.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - var a = conn.KeyTimeToLiveAsync(key); - conn.KeyExpire(key, now.AddHours(1), CommandFlags.FireAndForget); - var b = conn.KeyTimeToLiveAsync(key); - conn.KeyExpire(key, (DateTime?)null, CommandFlags.FireAndForget); - var c = conn.KeyTimeToLiveAsync(key); - conn.KeyExpire(key, now.AddHours(1.5), CommandFlags.FireAndForget); - var d = conn.KeyTimeToLiveAsync(key); - conn.KeyExpire(key, DateTime.MaxValue, CommandFlags.FireAndForget); - var e = conn.KeyTimeToLiveAsync(key); - conn.KeyDelete(key, CommandFlags.FireAndForget); - var f = conn.KeyTimeToLiveAsync(key); - - Assert.Null(await a); - var timeResult = await b; - Assert.NotNull(timeResult); - TimeSpan time = timeResult.Value; - - // Adjust for server time offset, if any when checking expectations - time -= offset; - - Log("Time: {0}, Expected: {1}-{2}", time, TimeSpan.FromMinutes(59), TimeSpan.FromMinutes(60)); - Assert.True(time >= TimeSpan.FromMinutes(59)); - Assert.True(time <= TimeSpan.FromMinutes(60.1)); - Assert.Null(await c); - - timeResult = await d; - Assert.NotNull(timeResult); - time = timeResult.Value; - - Assert.True(time >= TimeSpan.FromMinutes(89)); - Assert.True(time <= TimeSpan.FromMinutes(90.1)); - Assert.Null(await e); - Assert.Null(await f); - } - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void KeyExpiryTime(bool disablePTimes) - { - using var muxer = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); - - var key = Me(); - var conn = muxer.GetDatabase(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - - var expireTime = DateTime.UtcNow.AddHours(1); - conn.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - conn.KeyExpire(key, expireTime, CommandFlags.FireAndForget); - - var time = conn.KeyExpireTime(key); - Assert.NotNull(time); - Assert.Equal(expireTime, time.Value, TimeSpan.FromSeconds(30)); - - // Without associated expiration time - conn.KeyDelete(key, CommandFlags.FireAndForget); - conn.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - time = conn.KeyExpireTime(key); - Assert.Null(time); - - // Non existing key - conn.KeyDelete(key, CommandFlags.FireAndForget); - time = conn.KeyExpireTime(key); - Assert.Null(time); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task KeyExpiryTimeAsync(bool disablePTimes) - { - using var muxer = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); - - var key = Me(); - var conn = muxer.GetDatabase(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - - var expireTime = DateTime.UtcNow.AddHours(1); - conn.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - conn.KeyExpire(key, expireTime, CommandFlags.FireAndForget); - - var time = await conn.KeyExpireTimeAsync(key); - Assert.NotNull(time); - Assert.Equal(expireTime, time.Value, TimeSpan.FromSeconds(30)); - - // Without associated expiration time - conn.KeyDelete(key, CommandFlags.FireAndForget); - conn.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - time = await conn.KeyExpireTimeAsync(key); - Assert.Null(time); - - // Non existing key - conn.KeyDelete(key, CommandFlags.FireAndForget); - time = await conn.KeyExpireTimeAsync(key); - Assert.Null(time); - } + using var conn = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); + + var key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + var expireTime = DateTime.UtcNow.AddHours(1); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + db.KeyExpire(key, expireTime, CommandFlags.FireAndForget); + + var time = await db.KeyExpireTimeAsync(key); + Assert.NotNull(time); + Assert.Equal(expireTime, time.Value, TimeSpan.FromSeconds(30)); + + // Without associated expiration time + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + time = await db.KeyExpireTimeAsync(key); + Assert.Null(time); + + // Non existing key + db.KeyDelete(key, CommandFlags.FireAndForget); + time = await db.KeyExpireTimeAsync(key); + Assert.Null(time); } } diff --git a/tests/StackExchange.Redis.Tests/FSharpCompat.cs b/tests/StackExchange.Redis.Tests/FSharpCompat.cs index a9b68b8d5..3a1366326 100644 --- a/tests/StackExchange.Redis.Tests/FSharpCompat.cs +++ b/tests/StackExchange.Redis.Tests/FSharpCompat.cs @@ -1,26 +1,25 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class FSharpCompat : TestBase { - public class FSharpCompat : TestBase - { - public FSharpCompat(ITestOutputHelper output) : base (output) { } + public FSharpCompat(ITestOutputHelper output) : base (output) { } - [Fact] - public void RedisKeyConstructor() - { - Assert.Equal(default, new RedisKey()); - Assert.Equal((RedisKey)"MyKey", new RedisKey("MyKey")); - Assert.Equal((RedisKey)"MyKey2", new RedisKey(null, "MyKey2")); - } + [Fact] + public void RedisKeyConstructor() + { + Assert.Equal(default, new RedisKey()); + Assert.Equal((RedisKey)"MyKey", new RedisKey("MyKey")); + Assert.Equal((RedisKey)"MyKey2", new RedisKey(null, "MyKey2")); + } - [Fact] - public void RedisValueConstructor() - { - Assert.Equal(default, new RedisValue()); - Assert.Equal((RedisValue)"MyKey", new RedisValue("MyKey")); - Assert.Equal((RedisValue)"MyKey2", new RedisValue("MyKey2", 0)); - } + [Fact] + public void RedisValueConstructor() + { + Assert.Equal(default, new RedisValue()); + Assert.Equal((RedisValue)"MyKey", new RedisValue("MyKey")); + Assert.Equal((RedisValue)"MyKey2", new RedisValue("MyKey2", 0)); } } diff --git a/tests/StackExchange.Redis.Tests/Failover.cs b/tests/StackExchange.Redis.Tests/Failover.cs index b0257b76c..b9b23cd21 100644 --- a/tests/StackExchange.Redis.Tests/Failover.cs +++ b/tests/StackExchange.Redis.Tests/Failover.cs @@ -5,361 +5,353 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests -{ - public class Failover : TestBase, IAsyncLifetime - { - protected override string GetConfiguration() => GetPrimaryReplicaConfig().ToString(); +namespace StackExchange.Redis.Tests; - public Failover(ITestOutputHelper output) : base(output) - { - } +public class Failover : TestBase, IAsyncLifetime +{ + protected override string GetConfiguration() => GetPrimaryReplicaConfig().ToString(); - public Task DisposeAsync() => Task.CompletedTask; + public Failover(ITestOutputHelper output) : base(output) { } - public async Task InitializeAsync() - { - using (var mutex = Create()) - { - var shouldBePrimary = mutex.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort); - if (shouldBePrimary.IsReplica) - { - Log(shouldBePrimary.EndPoint + " should be primary, fixing..."); - await shouldBePrimary.MakePrimaryAsync(ReplicationChangeOptions.SetTiebreaker); - } + public Task DisposeAsync() => Task.CompletedTask; - var shouldBeReplica = mutex.GetServer(TestConfig.Current.FailoverReplicaServerAndPort); - if (!shouldBeReplica.IsReplica) - { - Log(shouldBeReplica.EndPoint + " should be a replica, fixing..."); - await shouldBeReplica.ReplicaOfAsync(shouldBePrimary.EndPoint); - await Task.Delay(2000).ForAwait(); - } - } - } + public async Task InitializeAsync() + { + using var conn = Create(); - private static ConfigurationOptions GetPrimaryReplicaConfig() + var shouldBePrimary = conn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort); + if (shouldBePrimary.IsReplica) { - return new ConfigurationOptions - { - AllowAdmin = true, - SyncTimeout = 100000, - EndPoints = - { - { TestConfig.Current.FailoverPrimaryServer, TestConfig.Current.FailoverPrimaryPort }, - { TestConfig.Current.FailoverReplicaServer, TestConfig.Current.FailoverReplicaPort }, - } - }; + Log(shouldBePrimary.EndPoint + " should be primary, fixing..."); + await shouldBePrimary.MakePrimaryAsync(ReplicationChangeOptions.SetTiebreaker); } - [Fact] - public async Task ConfigureAsync() + var shouldBeReplica = conn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort); + if (!shouldBeReplica.IsReplica) { - using (var muxer = Create()) - { - await Task.Delay(1000).ForAwait(); - Log("About to reconfigure....."); - await muxer.ConfigureAsync().ForAwait(); - Log("Reconfigured"); - } + Log(shouldBeReplica.EndPoint + " should be a replica, fixing..."); + await shouldBeReplica.ReplicaOfAsync(shouldBePrimary.EndPoint); + await Task.Delay(2000).ForAwait(); } + } - [Fact] - public async Task ConfigureSync() + private static ConfigurationOptions GetPrimaryReplicaConfig() + { + return new ConfigurationOptions { - using (var muxer = Create()) + AllowAdmin = true, + SyncTimeout = 100000, + EndPoints = { - await Task.Delay(1000).ForAwait(); - Log("About to reconfigure....."); - muxer.Configure(); - Log("Reconfigured"); + { TestConfig.Current.FailoverPrimaryServer, TestConfig.Current.FailoverPrimaryPort }, + { TestConfig.Current.FailoverReplicaServer, TestConfig.Current.FailoverReplicaPort }, } - } + }; + } - [Fact] - public async Task ConfigVerifyReceiveConfigChangeBroadcast() - { - _ = GetConfiguration(); - using (var sender = Create(allowAdmin: true)) - using (var receiver = Create(syncTimeout: 2000)) - { - int total = 0; - receiver.ConfigurationChangedBroadcast += (s, a) => - { - Log("Config changed: " + (a.EndPoint == null ? "(none)" : a.EndPoint.ToString())); - Interlocked.Increment(ref total); - }; - // send a reconfigure/reconnect message - long count = sender.PublishReconfigure(); - GetServer(receiver).Ping(); - GetServer(receiver).Ping(); - await Task.Delay(1000).ConfigureAwait(false); - Assert.True(count == -1 || count >= 2, "subscribers"); - Assert.True(Interlocked.CompareExchange(ref total, 0, 0) >= 1, "total (1st)"); - - Interlocked.Exchange(ref total, 0); - - // and send a second time via a re-primary operation - var server = GetServer(sender); - if (server.IsReplica) Skip.Inconclusive("didn't expect a replica"); - await server.MakePrimaryAsync(ReplicationChangeOptions.Broadcast); - await Task.Delay(1000).ConfigureAwait(false); - GetServer(receiver).Ping(); - GetServer(receiver).Ping(); - Assert.True(Interlocked.CompareExchange(ref total, 0, 0) >= 1, "total (2nd)"); - } - } + [Fact] + public async Task ConfigureAsync() + { + using var conn = Create(); + + await Task.Delay(1000).ForAwait(); + Log("About to reconfigure....."); + await conn.ConfigureAsync().ForAwait(); + Log("Reconfigured"); + } + + [Fact] + public async Task ConfigureSync() + { + using var conn = Create(); + + await Task.Delay(1000).ForAwait(); + Log("About to reconfigure....."); + conn.Configure(); + Log("Reconfigured"); + } - [Fact] - public async Task DereplicateGoesToPrimary() + [Fact] + public async Task ConfigVerifyReceiveConfigChangeBroadcast() + { + _ = GetConfiguration(); + using var senderConn = Create(allowAdmin: true); + using var receiverConn = Create(syncTimeout: 2000); + + int total = 0; + receiverConn.ConfigurationChangedBroadcast += (s, a) => { - ConfigurationOptions config = GetPrimaryReplicaConfig(); - config.ConfigCheckSeconds = 5; - using (var conn = ConnectionMultiplexer.Connect(config)) - { - var primary = conn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort); - var secondary = conn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort); + Log("Config changed: " + (a.EndPoint == null ? "(none)" : a.EndPoint.ToString())); + Interlocked.Increment(ref total); + }; + // send a reconfigure/reconnect message + long count = senderConn.PublishReconfigure(); + GetServer(receiverConn).Ping(); + GetServer(receiverConn).Ping(); + await Task.Delay(1000).ConfigureAwait(false); + Assert.True(count == -1 || count >= 2, "subscribers"); + Assert.True(Interlocked.CompareExchange(ref total, 0, 0) >= 1, "total (1st)"); + + Interlocked.Exchange(ref total, 0); + + // and send a second time via a re-primary operation + var server = GetServer(senderConn); + if (server.IsReplica) Skip.Inconclusive("didn't expect a replica"); + await server.MakePrimaryAsync(ReplicationChangeOptions.Broadcast); + await Task.Delay(1000).ConfigureAwait(false); + GetServer(receiverConn).Ping(); + GetServer(receiverConn).Ping(); + Assert.True(Interlocked.CompareExchange(ref total, 0, 0) >= 1, "total (2nd)"); + } - primary.Ping(); - secondary.Ping(); + [Fact] + public async Task DereplicateGoesToPrimary() + { + ConfigurationOptions config = GetPrimaryReplicaConfig(); + config.ConfigCheckSeconds = 5; - await primary.MakePrimaryAsync(ReplicationChangeOptions.SetTiebreaker); - await secondary.MakePrimaryAsync(ReplicationChangeOptions.None); + using var conn = ConnectionMultiplexer.Connect(config); - await Task.Delay(100).ConfigureAwait(false); + var primary = conn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort); + var secondary = conn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort); - primary.Ping(); - secondary.Ping(); + primary.Ping(); + secondary.Ping(); - using (var writer = new StringWriter()) - { - conn.Configure(writer); - string log = writer.ToString(); - Writer.WriteLine(log); - bool isUnanimous = log.Contains("tie-break is unanimous at " + TestConfig.Current.FailoverPrimaryServerAndPort); - if (!isUnanimous) Skip.Inconclusive("this is timing sensitive; unable to verify this time"); - } - // k, so we know everyone loves 6379; is that what we get? + await primary.MakePrimaryAsync(ReplicationChangeOptions.SetTiebreaker); + await secondary.MakePrimaryAsync(ReplicationChangeOptions.None); - var db = conn.GetDatabase(); - RedisKey key = Me(); + await Task.Delay(100).ConfigureAwait(false); - Assert.Equal(primary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.PreferMaster)); - Assert.Equal(primary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.DemandMaster)); - Assert.Equal(primary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.PreferReplica)); + primary.Ping(); + secondary.Ping(); - var ex = Assert.Throws(() => db.IdentifyEndpoint(key, CommandFlags.DemandReplica)); - Assert.StartsWith("No connection is active/available to service this operation: EXISTS " + Me(), ex.Message); - Writer.WriteLine("Invoking MakePrimaryAsync()..."); - await primary.MakePrimaryAsync(ReplicationChangeOptions.Broadcast | ReplicationChangeOptions.ReplicateToOtherEndpoints | ReplicationChangeOptions.SetTiebreaker, Writer); - Writer.WriteLine("Finished MakePrimaryAsync() call."); + using (var writer = new StringWriter()) + { + conn.Configure(writer); + string log = writer.ToString(); + Writer.WriteLine(log); + bool isUnanimous = log.Contains("tie-break is unanimous at " + TestConfig.Current.FailoverPrimaryServerAndPort); + if (!isUnanimous) Skip.Inconclusive("this is timing sensitive; unable to verify this time"); + } + // k, so we know everyone loves 6379; is that what we get? - await Task.Delay(100).ConfigureAwait(false); + var db = conn.GetDatabase(); + RedisKey key = Me(); - Writer.WriteLine("Invoking Ping() (post-primary)"); - primary.Ping(); - secondary.Ping(); - Writer.WriteLine("Finished Ping() (post-primary)"); + Assert.Equal(primary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.PreferMaster)); + Assert.Equal(primary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.DemandMaster)); + Assert.Equal(primary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.PreferReplica)); - Assert.True(primary.IsConnected, $"{primary.EndPoint} is not connected."); - Assert.True(secondary.IsConnected, $"{secondary.EndPoint} is not connected."); + var ex = Assert.Throws(() => db.IdentifyEndpoint(key, CommandFlags.DemandReplica)); + Assert.StartsWith("No connection is active/available to service this operation: EXISTS " + Me(), ex.Message); + Writer.WriteLine("Invoking MakePrimaryAsync()..."); + await primary.MakePrimaryAsync(ReplicationChangeOptions.Broadcast | ReplicationChangeOptions.ReplicateToOtherEndpoints | ReplicationChangeOptions.SetTiebreaker, Writer); + Writer.WriteLine("Finished MakePrimaryAsync() call."); - Writer.WriteLine($"{primary.EndPoint}: {primary.ServerType}, Mode: {(primary.IsReplica ? "Replica" : "Primary")}"); - Writer.WriteLine($"{secondary.EndPoint}: {secondary.ServerType}, Mode: {(secondary.IsReplica ? "Replica" : "Primary")}"); + await Task.Delay(100).ConfigureAwait(false); - // Create a separate multiplexer with a valid view of the world to distinguish between failures of - // server topology changes from failures to recognize those changes - Writer.WriteLine("Connecting to secondary validation connection."); - using (var conn2 = ConnectionMultiplexer.Connect(config)) - { - var primary2 = conn2.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort); - var secondary2 = conn2.GetServer(TestConfig.Current.FailoverReplicaServerAndPort); + Writer.WriteLine("Invoking Ping() (post-primary)"); + primary.Ping(); + secondary.Ping(); + Writer.WriteLine("Finished Ping() (post-primary)"); - Writer.WriteLine($"Check: {primary2.EndPoint}: {primary2.ServerType}, Mode: {(primary2.IsReplica ? "Replica" : "Primary")}"); - Writer.WriteLine($"Check: {secondary2.EndPoint}: {secondary2.ServerType}, Mode: {(secondary2.IsReplica ? "Replica" : "Primary")}"); + Assert.True(primary.IsConnected, $"{primary.EndPoint} is not connected."); + Assert.True(secondary.IsConnected, $"{secondary.EndPoint} is not connected."); - Assert.False(primary2.IsReplica, $"{primary2.EndPoint} should be a primary (verification connection)."); - Assert.True(secondary2.IsReplica, $"{secondary2.EndPoint} should be a replica (verification connection)."); + Writer.WriteLine($"{primary.EndPoint}: {primary.ServerType}, Mode: {(primary.IsReplica ? "Replica" : "Primary")}"); + Writer.WriteLine($"{secondary.EndPoint}: {secondary.ServerType}, Mode: {(secondary.IsReplica ? "Replica" : "Primary")}"); - var db2 = conn2.GetDatabase(); + // Create a separate multiplexer with a valid view of the world to distinguish between failures of + // server topology changes from failures to recognize those changes + Writer.WriteLine("Connecting to secondary validation connection."); + using (var conn2 = ConnectionMultiplexer.Connect(config)) + { + var primary2 = conn2.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort); + var secondary2 = conn2.GetServer(TestConfig.Current.FailoverReplicaServerAndPort); - Assert.Equal(primary2.EndPoint, db2.IdentifyEndpoint(key, CommandFlags.PreferMaster)); - Assert.Equal(primary2.EndPoint, db2.IdentifyEndpoint(key, CommandFlags.DemandMaster)); - Assert.Equal(secondary2.EndPoint, db2.IdentifyEndpoint(key, CommandFlags.PreferReplica)); - Assert.Equal(secondary2.EndPoint, db2.IdentifyEndpoint(key, CommandFlags.DemandReplica)); - } + Writer.WriteLine($"Check: {primary2.EndPoint}: {primary2.ServerType}, Mode: {(primary2.IsReplica ? "Replica" : "Primary")}"); + Writer.WriteLine($"Check: {secondary2.EndPoint}: {secondary2.ServerType}, Mode: {(secondary2.IsReplica ? "Replica" : "Primary")}"); - await UntilConditionAsync(TimeSpan.FromSeconds(20), () => !primary.IsReplica && secondary.IsReplica); + Assert.False(primary2.IsReplica, $"{primary2.EndPoint} should be a primary (verification connection)."); + Assert.True(secondary2.IsReplica, $"{secondary2.EndPoint} should be a replica (verification connection)."); - Assert.False(primary.IsReplica, $"{primary.EndPoint} should be a primary."); - Assert.True(secondary.IsReplica, $"{secondary.EndPoint} should be a replica."); + var db2 = conn2.GetDatabase(); - Assert.Equal(primary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.PreferMaster)); - Assert.Equal(primary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.DemandMaster)); - Assert.Equal(secondary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.PreferReplica)); - Assert.Equal(secondary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.DemandReplica)); - } + Assert.Equal(primary2.EndPoint, db2.IdentifyEndpoint(key, CommandFlags.PreferMaster)); + Assert.Equal(primary2.EndPoint, db2.IdentifyEndpoint(key, CommandFlags.DemandMaster)); + Assert.Equal(secondary2.EndPoint, db2.IdentifyEndpoint(key, CommandFlags.PreferReplica)); + Assert.Equal(secondary2.EndPoint, db2.IdentifyEndpoint(key, CommandFlags.DemandReplica)); } + await UntilConditionAsync(TimeSpan.FromSeconds(20), () => !primary.IsReplica && secondary.IsReplica); + + Assert.False(primary.IsReplica, $"{primary.EndPoint} should be a primary."); + Assert.True(secondary.IsReplica, $"{secondary.EndPoint} should be a replica."); + + Assert.Equal(primary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.PreferMaster)); + Assert.Equal(primary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.DemandMaster)); + Assert.Equal(secondary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.PreferReplica)); + Assert.Equal(secondary.EndPoint, db.IdentifyEndpoint(key, CommandFlags.DemandReplica)); + } + #if DEBUG - [Fact] - public async Task SubscriptionsSurvivePrimarySwitchAsync() + [Fact] + public async Task SubscriptionsSurvivePrimarySwitchAsync() + { + static void TopologyFail() => Skip.Inconclusive("Replication topology change failed...and that's both inconsistent and not what we're testing."); + + if (RunningInCI) { - static void TopologyFail() => Skip.Inconclusive("Replication topology change failed...and that's both inconsistent and not what we're testing."); + Skip.Inconclusive("TODO: Fix race in broadcast reconfig a zero latency."); + } - if (RunningInCI) + using var aConn = Create(allowAdmin: true, shared: false); + using var bConn = Create(allowAdmin: true, shared: false); + + RedisChannel channel = Me(); + Log("Using Channel: " + channel); + var subA = aConn.GetSubscriber(); + var subB = bConn.GetSubscriber(); + + long primaryChanged = 0, aCount = 0, bCount = 0; + aConn.ConfigurationChangedBroadcast += delegate + { + Log("A noticed config broadcast: " + Interlocked.Increment(ref primaryChanged)); + }; + bConn.ConfigurationChangedBroadcast += delegate + { + Log("B noticed config broadcast: " + Interlocked.Increment(ref primaryChanged)); + }; + subA.Subscribe(channel, (_, message) => + { + Log("A got message: " + message); + Interlocked.Increment(ref aCount); + }); + subB.Subscribe(channel, (_, message) => + { + Log("B got message: " + message); + Interlocked.Increment(ref bCount); + }); + + Assert.False(aConn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverPrimaryServerAndPort} should be a primary"); + if (!aConn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica) + { + TopologyFail(); + } + Assert.True(aConn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a replica"); + Assert.False(bConn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica, $"B Connection: {TestConfig.Current.FailoverPrimaryServerAndPort} should be a primary"); + Assert.True(bConn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"B Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a replica"); + + Log("Failover 1 Complete"); + var epA = subA.SubscribedEndpoint(channel); + var epB = subB.SubscribedEndpoint(channel); + Log(" A: " + EndPointCollection.ToString(epA)); + Log(" B: " + EndPointCollection.ToString(epB)); + subA.Publish(channel, "A1"); + subB.Publish(channel, "B1"); + Log(" SubA ping: " + subA.Ping()); + Log(" SubB ping: " + subB.Ping()); + // If redis is under load due to this suite, it may take a moment to send across. + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => Interlocked.Read(ref aCount) == 2 && Interlocked.Read(ref bCount) == 2).ForAwait(); + + Assert.Equal(2, Interlocked.Read(ref aCount)); + Assert.Equal(2, Interlocked.Read(ref bCount)); + Assert.Equal(0, Interlocked.Read(ref primaryChanged)); + + try + { + Interlocked.Exchange(ref primaryChanged, 0); + Interlocked.Exchange(ref aCount, 0); + Interlocked.Exchange(ref bCount, 0); + Log("Changing primary..."); + using (var sw = new StringWriter()) { - Skip.Inconclusive("TODO: Fix race in broadcast reconfig a zero latency."); + await aConn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).MakePrimaryAsync(ReplicationChangeOptions.All, sw); + Log(sw.ToString()); } - - using (var a = Create(allowAdmin: true, shared: false)) - using (var b = Create(allowAdmin: true, shared: false)) + Log("Waiting for connection B to detect..."); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => bConn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica).ForAwait(); + subA.Ping(); + subB.Ping(); + Log("Failover 2 Attempted. Pausing..."); + Log(" A " + TestConfig.Current.FailoverPrimaryServerAndPort + " status: " + (aConn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica ? "Replica" : "Primary")); + Log(" A " + TestConfig.Current.FailoverReplicaServerAndPort + " status: " + (aConn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica ? "Replica" : "Primary")); + Log(" B " + TestConfig.Current.FailoverPrimaryServerAndPort + " status: " + (bConn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica ? "Replica" : "Primary")); + Log(" B " + TestConfig.Current.FailoverReplicaServerAndPort + " status: " + (bConn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica ? "Replica" : "Primary")); + + if (!aConn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica) { - RedisChannel channel = Me(); - Log("Using Channel: " + channel); - var subA = a.GetSubscriber(); - var subB = b.GetSubscriber(); - - long primaryChanged = 0, aCount = 0, bCount = 0; - a.ConfigurationChangedBroadcast += delegate - { - Log("A noticed config broadcast: " + Interlocked.Increment(ref primaryChanged)); - }; - b.ConfigurationChangedBroadcast += delegate - { - Log("B noticed config broadcast: " + Interlocked.Increment(ref primaryChanged)); - }; - subA.Subscribe(channel, (_, message) => - { - Log("A got message: " + message); - Interlocked.Increment(ref aCount); - }); - subB.Subscribe(channel, (_, message) => - { - Log("B got message: " + message); - Interlocked.Increment(ref bCount); - }); + TopologyFail(); + } + Log("Failover 2 Complete."); - Assert.False(a.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverPrimaryServerAndPort} should be a primary"); - if (!a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica) - { - TopologyFail(); - } - Assert.True(a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a replica"); - Assert.False(b.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica, $"B Connection: {TestConfig.Current.FailoverPrimaryServerAndPort} should be a primary"); - Assert.True(b.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"B Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a replica"); - - Log("Failover 1 Complete"); - var epA = subA.SubscribedEndpoint(channel); - var epB = subB.SubscribedEndpoint(channel); - Log(" A: " + EndPointCollection.ToString(epA)); - Log(" B: " + EndPointCollection.ToString(epB)); - subA.Publish(channel, "A1"); - subB.Publish(channel, "B1"); - Log(" SubA ping: " + subA.Ping()); - Log(" SubB ping: " + subB.Ping()); - // If redis is under load due to this suite, it may take a moment to send across. - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => Interlocked.Read(ref aCount) == 2 && Interlocked.Read(ref bCount) == 2).ForAwait(); - - Assert.Equal(2, Interlocked.Read(ref aCount)); - Assert.Equal(2, Interlocked.Read(ref bCount)); - Assert.Equal(0, Interlocked.Read(ref primaryChanged)); - - try - { - Interlocked.Exchange(ref primaryChanged, 0); - Interlocked.Exchange(ref aCount, 0); - Interlocked.Exchange(ref bCount, 0); - Log("Changing primary..."); - using (var sw = new StringWriter()) - { - await a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).MakePrimaryAsync(ReplicationChangeOptions.All, sw); - Log(sw.ToString()); - } - Log("Waiting for connection B to detect..."); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => b.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica).ForAwait(); - subA.Ping(); - subB.Ping(); - Log("Failover 2 Attempted. Pausing..."); - Log(" A " + TestConfig.Current.FailoverPrimaryServerAndPort + " status: " + (a.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica ? "Replica" : "Primary")); - Log(" A " + TestConfig.Current.FailoverReplicaServerAndPort + " status: " + (a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica ? "Replica" : "Primary")); - Log(" B " + TestConfig.Current.FailoverPrimaryServerAndPort + " status: " + (b.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica ? "Replica" : "Primary")); - Log(" B " + TestConfig.Current.FailoverReplicaServerAndPort + " status: " + (b.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica ? "Replica" : "Primary")); - - if (!a.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica) - { - TopologyFail(); - } - Log("Failover 2 Complete."); - - Assert.True(a.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverPrimaryServerAndPort} should be a replica"); - Assert.False(a.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a primary"); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => b.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica).ForAwait(); - var sanityCheck = b.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica; - if (!sanityCheck) - { - Log("FAILURE: B has not detected the topology change."); - foreach (var server in b.GetServerSnapshot().ToArray()) - { - Log(" Server" + server.EndPoint); - Log(" State: " + server.ConnectionState); - Log(" IsReplica: " + !server.IsReplica); - Log(" Type: " + server.ServerType); - } - //Skip.Inconclusive("Not enough latency."); - } - Assert.True(sanityCheck, $"B Connection: {TestConfig.Current.FailoverPrimaryServerAndPort} should be a replica"); - Assert.False(b.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"B Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a primary"); - - Log("Pause complete"); - Log(" A outstanding: " + a.GetCounters().TotalOutstanding); - Log(" B outstanding: " + b.GetCounters().TotalOutstanding); - subA.Ping(); - subB.Ping(); - await Task.Delay(5000).ForAwait(); - epA = subA.SubscribedEndpoint(channel); - epB = subB.SubscribedEndpoint(channel); - Log("Subscription complete"); - Log(" A: " + EndPointCollection.ToString(epA)); - Log(" B: " + EndPointCollection.ToString(epB)); - var aSentTo = subA.Publish(channel, "A2"); - var bSentTo = subB.Publish(channel, "B2"); - Log(" A2 sent to: " + aSentTo); - Log(" B2 sent to: " + bSentTo); - subA.Ping(); - subB.Ping(); - Log("Ping Complete. Checking..."); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Interlocked.Read(ref aCount) == 2 && Interlocked.Read(ref bCount) == 2).ForAwait(); - - Log("Counts so far:"); - Log(" aCount: " + Interlocked.Read(ref aCount)); - Log(" bCount: " + Interlocked.Read(ref bCount)); - Log(" primaryChanged: " + Interlocked.Read(ref primaryChanged)); - - Assert.Equal(2, Interlocked.Read(ref aCount)); - Assert.Equal(2, Interlocked.Read(ref bCount)); - // Expect 12, because a sees a, but b sees a and b due to replication - Assert.Equal(12, Interlocked.CompareExchange(ref primaryChanged, 0, 0)); - } - catch - { - LogNoTime(""); - Log("ERROR: Something went bad - see above! Roooooolling back. Back it up. Baaaaaack it on up."); - LogNoTime(""); - throw; - } - finally + Assert.True(aConn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverPrimaryServerAndPort} should be a replica"); + Assert.False(aConn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"A Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a primary"); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => bConn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica).ForAwait(); + var sanityCheck = bConn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica; + if (!sanityCheck) + { + Log("FAILURE: B has not detected the topology change."); + foreach (var server in bConn.GetServerSnapshot().ToArray()) { - Log("Restoring configuration..."); - try - { - await a.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).MakePrimaryAsync(ReplicationChangeOptions.All); - await Task.Delay(1000).ForAwait(); - } - catch { /* Don't bomb here */ } + Log(" Server" + server.EndPoint); + Log(" State: " + server.ConnectionState); + Log(" IsReplica: " + !server.IsReplica); + Log(" Type: " + server.ServerType); } + //Skip.Inconclusive("Not enough latency."); } + Assert.True(sanityCheck, $"B Connection: {TestConfig.Current.FailoverPrimaryServerAndPort} should be a replica"); + Assert.False(bConn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"B Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a primary"); + + Log("Pause complete"); + Log(" A outstanding: " + aConn.GetCounters().TotalOutstanding); + Log(" B outstanding: " + bConn.GetCounters().TotalOutstanding); + subA.Ping(); + subB.Ping(); + await Task.Delay(5000).ForAwait(); + epA = subA.SubscribedEndpoint(channel); + epB = subB.SubscribedEndpoint(channel); + Log("Subscription complete"); + Log(" A: " + EndPointCollection.ToString(epA)); + Log(" B: " + EndPointCollection.ToString(epB)); + var aSentTo = subA.Publish(channel, "A2"); + var bSentTo = subB.Publish(channel, "B2"); + Log(" A2 sent to: " + aSentTo); + Log(" B2 sent to: " + bSentTo); + subA.Ping(); + subB.Ping(); + Log("Ping Complete. Checking..."); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Interlocked.Read(ref aCount) == 2 && Interlocked.Read(ref bCount) == 2).ForAwait(); + + Log("Counts so far:"); + Log(" aCount: " + Interlocked.Read(ref aCount)); + Log(" bCount: " + Interlocked.Read(ref bCount)); + Log(" primaryChanged: " + Interlocked.Read(ref primaryChanged)); + + Assert.Equal(2, Interlocked.Read(ref aCount)); + Assert.Equal(2, Interlocked.Read(ref bCount)); + // Expect 12, because a sees a, but b sees a and b due to replication + Assert.Equal(12, Interlocked.CompareExchange(ref primaryChanged, 0, 0)); + } + catch + { + LogNoTime(""); + Log("ERROR: Something went bad - see above! Roooooolling back. Back it up. Baaaaaack it on up."); + LogNoTime(""); + throw; + } + finally + { + Log("Restoring configuration..."); + try + { + await aConn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).MakePrimaryAsync(ReplicationChangeOptions.All); + await Task.Delay(1000).ForAwait(); + } + catch { /* Don't bomb here */ } } -#endif } +#endif } diff --git a/tests/StackExchange.Redis.Tests/FeatureFlags.cs b/tests/StackExchange.Redis.Tests/FeatureFlags.cs index 5d0b66d19..966c2df43 100644 --- a/tests/StackExchange.Redis.Tests/FeatureFlags.cs +++ b/tests/StackExchange.Redis.Tests/FeatureFlags.cs @@ -1,26 +1,25 @@ using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(NonParallelCollection.Name)] +public class FeatureFlags { - [Collection(NonParallelCollection.Name)] - public class FeatureFlags + [Fact] + public void UnknownFlagToggle() { - [Fact] - public void UnknownFlagToggle() - { - Assert.False(ConnectionMultiplexer.GetFeatureFlag("nope")); - ConnectionMultiplexer.SetFeatureFlag("nope", true); - Assert.False(ConnectionMultiplexer.GetFeatureFlag("nope")); - } + Assert.False(ConnectionMultiplexer.GetFeatureFlag("nope")); + ConnectionMultiplexer.SetFeatureFlag("nope", true); + Assert.False(ConnectionMultiplexer.GetFeatureFlag("nope")); + } - [Fact] - public void KnownFlagToggle() - { - Assert.False(ConnectionMultiplexer.GetFeatureFlag("preventthreadtheft")); - ConnectionMultiplexer.SetFeatureFlag("preventthreadtheft", true); - Assert.True(ConnectionMultiplexer.GetFeatureFlag("preventthreadtheft")); - ConnectionMultiplexer.SetFeatureFlag("preventthreadtheft", false); - Assert.False(ConnectionMultiplexer.GetFeatureFlag("preventthreadtheft")); - } + [Fact] + public void KnownFlagToggle() + { + Assert.False(ConnectionMultiplexer.GetFeatureFlag("preventthreadtheft")); + ConnectionMultiplexer.SetFeatureFlag("preventthreadtheft", true); + Assert.True(ConnectionMultiplexer.GetFeatureFlag("preventthreadtheft")); + ConnectionMultiplexer.SetFeatureFlag("preventthreadtheft", false); + Assert.False(ConnectionMultiplexer.GetFeatureFlag("preventthreadtheft")); } } diff --git a/tests/StackExchange.Redis.Tests/FloatingPoint.cs b/tests/StackExchange.Redis.Tests/FloatingPoint.cs index 9d7ad770b..029a32696 100644 --- a/tests/StackExchange.Redis.Tests/FloatingPoint.cs +++ b/tests/StackExchange.Redis.Tests/FloatingPoint.cs @@ -3,163 +3,158 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class FloatingPoint : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class FloatingPoint : TestBase - { - public FloatingPoint(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public FloatingPoint(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } - private static bool Within(double x, double y, double delta) => Math.Abs(x - y) <= delta; + private static bool Within(double x, double y, double delta) => Math.Abs(x - y) <= delta; - [Fact] - public void IncrDecrFloatingPoint() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - double[] incr = - { - 12.134, - -14561.0000002, - 125.3421, - -2.49892498 - }, decr = - { - 99.312, - 12, - -35 - }; - double sum = 0; - foreach (var value in incr) - { - db.StringIncrement(key, value, CommandFlags.FireAndForget); - sum += value; - } - foreach (var value in decr) - { - db.StringDecrement(key, value, CommandFlags.FireAndForget); - sum -= value; - } - var val = (double)db.StringGet(key); + [Fact] + public void IncrDecrFloatingPoint() + { + using var conn = Create(); - Assert.True(Within(sum, val, 0.0001)); - } + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + double[] incr = + { + 12.134, + -14561.0000002, + 125.3421, + -2.49892498 + }, decr = + { + 99.312, + 12, + -35 + }; + double sum = 0; + foreach (var value in incr) + { + db.StringIncrement(key, value, CommandFlags.FireAndForget); + sum += value; + } + foreach (var value in decr) + { + db.StringDecrement(key, value, CommandFlags.FireAndForget); + sum -= value; } + var val = (double)db.StringGet(key); - [Fact] - public async Task IncrDecrFloatingPointAsync() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - double[] incr = - { - 12.134, - -14561.0000002, - 125.3421, - -2.49892498 - }, decr = - { - 99.312, - 12, - -35 - }; - double sum = 0; - foreach (var value in incr) - { - await db.StringIncrementAsync(key, value).ForAwait(); - sum += value; - } - foreach (var value in decr) - { - await db.StringDecrementAsync(key, value).ForAwait(); - sum -= value; - } - var val = (double)await db.StringGetAsync(key).ForAwait(); + Assert.True(Within(sum, val, 0.0001)); + } - Assert.True(Within(sum, val, 0.0001)); - } + [Fact] + public async Task IncrDecrFloatingPointAsync() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + double[] incr = + { + 12.134, + -14561.0000002, + 125.3421, + -2.49892498 + }, decr = + { + 99.312, + 12, + -35 + }; + double sum = 0; + foreach (var value in incr) + { + await db.StringIncrementAsync(key, value).ForAwait(); + sum += value; + } + foreach (var value in decr) + { + await db.StringDecrementAsync(key, value).ForAwait(); + sum -= value; } + var val = (double)await db.StringGetAsync(key).ForAwait(); - [Fact] - public void HashIncrDecrFloatingPoint() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - RedisValue field = "foo"; - db.KeyDelete(key, CommandFlags.FireAndForget); - double[] incr = - { - 12.134, - -14561.0000002, - 125.3421, - -2.49892498 - }, decr = - { - 99.312, - 12, - -35 - }; - double sum = 0; - foreach (var value in incr) - { - db.HashIncrement(key, field, value, CommandFlags.FireAndForget); - sum += value; - } - foreach (var value in decr) - { - db.HashDecrement(key, field, value, CommandFlags.FireAndForget); - sum -= value; - } - var val = (double)db.HashGet(key, field); + Assert.True(Within(sum, val, 0.0001)); + } - Assert.True(Within(sum, val, 0.0001), $"{sum} not within 0.0001 of {val}"); - } + [Fact] + public void HashIncrDecrFloatingPoint() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + RedisValue field = "foo"; + db.KeyDelete(key, CommandFlags.FireAndForget); + double[] incr = + { + 12.134, + -14561.0000002, + 125.3421, + -2.49892498 + }, decr = + { + 99.312, + 12, + -35 + }; + double sum = 0; + foreach (var value in incr) + { + db.HashIncrement(key, field, value, CommandFlags.FireAndForget); + sum += value; + } + foreach (var value in decr) + { + db.HashDecrement(key, field, value, CommandFlags.FireAndForget); + sum -= value; } + var val = (double)db.HashGet(key, field); + + Assert.True(Within(sum, val, 0.0001), $"{sum} not within 0.0001 of {val}"); + } - [Fact] - public async Task HashIncrDecrFloatingPointAsync() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - RedisValue field = "bar"; - db.KeyDelete(key, CommandFlags.FireAndForget); - double[] incr = - { - 12.134, - -14561.0000002, - 125.3421, - -2.49892498 - }, decr = - { - 99.312, - 12, - -35 - }; - double sum = 0; - foreach (var value in incr) - { - _ = db.HashIncrementAsync(key, field, value); - sum += value; - } - foreach (var value in decr) - { - _ = db.HashDecrementAsync(key, field, value); - sum -= value; - } - var val = (double)await db.HashGetAsync(key, field).ForAwait(); + [Fact] + public async Task HashIncrDecrFloatingPointAsync() + { + using var conn = Create(); - Assert.True(Within(sum, val, 0.0001)); - } + var db = conn.GetDatabase(); + RedisKey key = Me(); + RedisValue field = "bar"; + db.KeyDelete(key, CommandFlags.FireAndForget); + double[] incr = + { + 12.134, + -14561.0000002, + 125.3421, + -2.49892498 + }, decr = + { + 99.312, + 12, + -35 + }; + double sum = 0; + foreach (var value in incr) + { + _ = db.HashIncrementAsync(key, field, value); + sum += value; + } + foreach (var value in decr) + { + _ = db.HashDecrementAsync(key, field, value); + sum -= value; } + var val = (double)await db.HashGetAsync(key, field).ForAwait(); + + Assert.True(Within(sum, val, 0.0001)); } } diff --git a/tests/StackExchange.Redis.Tests/FormatTests.cs b/tests/StackExchange.Redis.Tests/FormatTests.cs index 757b9d314..8da9262fc 100644 --- a/tests/StackExchange.Redis.Tests/FormatTests.cs +++ b/tests/StackExchange.Redis.Tests/FormatTests.cs @@ -3,80 +3,79 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class FormatTests : TestBase { - public class FormatTests : TestBase - { - public FormatTests(ITestOutputHelper output) : base(output) { } + public FormatTests(ITestOutputHelper output) : base(output) { } - public static IEnumerable EndpointData() - { - // DNS - yield return new object[] { "localhost", new DnsEndPoint("localhost", 0) }; - yield return new object[] { "localhost:6390", new DnsEndPoint("localhost", 6390) }; - yield return new object[] { "bob.the.builder.com", new DnsEndPoint("bob.the.builder.com", 0) }; - yield return new object[] { "bob.the.builder.com:6390", new DnsEndPoint("bob.the.builder.com", 6390) }; - // IPv4 - yield return new object[] { "0.0.0.0", new IPEndPoint(IPAddress.Parse("0.0.0.0"), 0) }; - yield return new object[] { "127.0.0.1", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 0) }; - yield return new object[] { "127.1", new IPEndPoint(IPAddress.Parse("127.1"), 0) }; - yield return new object[] { "127.1:6389", new IPEndPoint(IPAddress.Parse("127.1"), 6389) }; - yield return new object[] { "127.0.0.1:6389", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 6389) }; - yield return new object[] { "127.0.0.1:1", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1) }; - yield return new object[] { "127.0.0.1:2", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 2) }; - yield return new object[] { "10.10.9.18:2", new IPEndPoint(IPAddress.Parse("10.10.9.18"), 2) }; - // IPv6 - yield return new object[] { "::1", new IPEndPoint(IPAddress.Parse("::1"), 0) }; - yield return new object[] { "::1:6379", new IPEndPoint(IPAddress.Parse("::0.1.99.121"), 0) }; // remember your brackets! - yield return new object[] { "[::1]:6379", new IPEndPoint(IPAddress.Parse("::1"), 6379) }; - yield return new object[] { "[::1]", new IPEndPoint(IPAddress.Parse("::1"), 0) }; - yield return new object[] { "[::1]:1000", new IPEndPoint(IPAddress.Parse("::1"), 1000) }; - yield return new object[] { "[2001:db7:85a3:8d2:1319:8a2e:370:7348]", new IPEndPoint(IPAddress.Parse("2001:db7:85a3:8d2:1319:8a2e:370:7348"), 0) }; - yield return new object[] { "[2001:db7:85a3:8d2:1319:8a2e:370:7348]:1000", new IPEndPoint(IPAddress.Parse("2001:db7:85a3:8d2:1319:8a2e:370:7348"), 1000) }; - } + public static IEnumerable EndpointData() + { + // DNS + yield return new object[] { "localhost", new DnsEndPoint("localhost", 0) }; + yield return new object[] { "localhost:6390", new DnsEndPoint("localhost", 6390) }; + yield return new object[] { "bob.the.builder.com", new DnsEndPoint("bob.the.builder.com", 0) }; + yield return new object[] { "bob.the.builder.com:6390", new DnsEndPoint("bob.the.builder.com", 6390) }; + // IPv4 + yield return new object[] { "0.0.0.0", new IPEndPoint(IPAddress.Parse("0.0.0.0"), 0) }; + yield return new object[] { "127.0.0.1", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 0) }; + yield return new object[] { "127.1", new IPEndPoint(IPAddress.Parse("127.1"), 0) }; + yield return new object[] { "127.1:6389", new IPEndPoint(IPAddress.Parse("127.1"), 6389) }; + yield return new object[] { "127.0.0.1:6389", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 6389) }; + yield return new object[] { "127.0.0.1:1", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1) }; + yield return new object[] { "127.0.0.1:2", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 2) }; + yield return new object[] { "10.10.9.18:2", new IPEndPoint(IPAddress.Parse("10.10.9.18"), 2) }; + // IPv6 + yield return new object[] { "::1", new IPEndPoint(IPAddress.Parse("::1"), 0) }; + yield return new object[] { "::1:6379", new IPEndPoint(IPAddress.Parse("::0.1.99.121"), 0) }; // remember your brackets! + yield return new object[] { "[::1]:6379", new IPEndPoint(IPAddress.Parse("::1"), 6379) }; + yield return new object[] { "[::1]", new IPEndPoint(IPAddress.Parse("::1"), 0) }; + yield return new object[] { "[::1]:1000", new IPEndPoint(IPAddress.Parse("::1"), 1000) }; + yield return new object[] { "[2001:db7:85a3:8d2:1319:8a2e:370:7348]", new IPEndPoint(IPAddress.Parse("2001:db7:85a3:8d2:1319:8a2e:370:7348"), 0) }; + yield return new object[] { "[2001:db7:85a3:8d2:1319:8a2e:370:7348]:1000", new IPEndPoint(IPAddress.Parse("2001:db7:85a3:8d2:1319:8a2e:370:7348"), 1000) }; + } - [Theory] - [MemberData(nameof(EndpointData))] - public void ParseEndPoint(string data, EndPoint expected) - { - _ = Format.TryParseEndPoint(data, out var result); - Assert.Equal(expected, result); - } + [Theory] + [MemberData(nameof(EndpointData))] + public void ParseEndPoint(string data, EndPoint expected) + { + _ = Format.TryParseEndPoint(data, out var result); + Assert.Equal(expected, result); + } - [Theory] - [InlineData(CommandFlags.None, "None")] + [Theory] + [InlineData(CommandFlags.None, "None")] #if NET472 - [InlineData(CommandFlags.PreferReplica, "PreferMaster, PreferReplica")] // 2-bit flag is hit-and-miss - [InlineData(CommandFlags.DemandReplica, "PreferMaster, DemandReplica")] // 2-bit flag is hit-and-miss + [InlineData(CommandFlags.PreferReplica, "PreferMaster, PreferReplica")] // 2-bit flag is hit-and-miss + [InlineData(CommandFlags.DemandReplica, "PreferMaster, DemandReplica")] // 2-bit flag is hit-and-miss #else - [InlineData(CommandFlags.PreferReplica, "PreferReplica")] // 2-bit flag is hit-and-miss - [InlineData(CommandFlags.DemandReplica, "DemandReplica")] // 2-bit flag is hit-and-miss + [InlineData(CommandFlags.PreferReplica, "PreferReplica")] // 2-bit flag is hit-and-miss + [InlineData(CommandFlags.DemandReplica, "DemandReplica")] // 2-bit flag is hit-and-miss #endif - [InlineData(CommandFlags.PreferReplica | CommandFlags.FireAndForget, "PreferMaster, FireAndForget, PreferReplica")] // 2-bit flag is hit-and-miss - [InlineData(CommandFlags.DemandReplica | CommandFlags.FireAndForget, "PreferMaster, FireAndForget, DemandReplica")] // 2-bit flag is hit-and-miss - public void CommandFlagsFormatting(CommandFlags value, string expected) - => Assert.Equal(expected, value.ToString()); + [InlineData(CommandFlags.PreferReplica | CommandFlags.FireAndForget, "PreferMaster, FireAndForget, PreferReplica")] // 2-bit flag is hit-and-miss + [InlineData(CommandFlags.DemandReplica | CommandFlags.FireAndForget, "PreferMaster, FireAndForget, DemandReplica")] // 2-bit flag is hit-and-miss + public void CommandFlagsFormatting(CommandFlags value, string expected) + => Assert.Equal(expected, value.ToString()); - [Theory] - [InlineData(ClientType.Normal, "Normal")] - [InlineData(ClientType.Replica, "Replica")] - [InlineData(ClientType.PubSub, "PubSub")] - public void ClientTypeFormatting(ClientType value, string expected) - => Assert.Equal(expected, value.ToString()); + [Theory] + [InlineData(ClientType.Normal, "Normal")] + [InlineData(ClientType.Replica, "Replica")] + [InlineData(ClientType.PubSub, "PubSub")] + public void ClientTypeFormatting(ClientType value, string expected) + => Assert.Equal(expected, value.ToString()); - [Theory] - [InlineData(ClientFlags.None, "None")] - [InlineData(ClientFlags.Replica | ClientFlags.Transaction, "Replica, Transaction")] - [InlineData(ClientFlags.Transaction | ClientFlags.ReplicaMonitor | ClientFlags.UnixDomainSocket, "ReplicaMonitor, Transaction, UnixDomainSocket")] - public void ClientFlagsFormatting(ClientFlags value, string expected) - => Assert.Equal(expected, value.ToString()); + [Theory] + [InlineData(ClientFlags.None, "None")] + [InlineData(ClientFlags.Replica | ClientFlags.Transaction, "Replica, Transaction")] + [InlineData(ClientFlags.Transaction | ClientFlags.ReplicaMonitor | ClientFlags.UnixDomainSocket, "ReplicaMonitor, Transaction, UnixDomainSocket")] + public void ClientFlagsFormatting(ClientFlags value, string expected) + => Assert.Equal(expected, value.ToString()); - [Theory] - [InlineData(ReplicationChangeOptions.None, "None")] - [InlineData(ReplicationChangeOptions.ReplicateToOtherEndpoints, "ReplicateToOtherEndpoints")] - [InlineData(ReplicationChangeOptions.SetTiebreaker | ReplicationChangeOptions.ReplicateToOtherEndpoints, "SetTiebreaker, ReplicateToOtherEndpoints")] - [InlineData(ReplicationChangeOptions.Broadcast | ReplicationChangeOptions.SetTiebreaker | ReplicationChangeOptions.ReplicateToOtherEndpoints, "All")] - public void ReplicationChangeOptionsFormatting(ReplicationChangeOptions value, string expected) - => Assert.Equal(expected, value.ToString()); - } + [Theory] + [InlineData(ReplicationChangeOptions.None, "None")] + [InlineData(ReplicationChangeOptions.ReplicateToOtherEndpoints, "ReplicateToOtherEndpoints")] + [InlineData(ReplicationChangeOptions.SetTiebreaker | ReplicationChangeOptions.ReplicateToOtherEndpoints, "SetTiebreaker, ReplicateToOtherEndpoints")] + [InlineData(ReplicationChangeOptions.Broadcast | ReplicationChangeOptions.SetTiebreaker | ReplicationChangeOptions.ReplicateToOtherEndpoints, "All")] + public void ReplicationChangeOptionsFormatting(ReplicationChangeOptions value, string expected) + => Assert.Equal(expected, value.ToString()); } diff --git a/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs b/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs index 142b77c91..0bff4abc5 100644 --- a/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs +++ b/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs @@ -3,54 +3,53 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(NonParallelCollection.Name)] // because I need to measure some things that could get confused +public class GarbageCollectionTests : TestBase { - [Collection(NonParallelCollection.Name)] // because I need to measure some things that could get confused - public class GarbageCollectionTests : TestBase - { - public GarbageCollectionTests(ITestOutputHelper helper) : base(helper) { } + public GarbageCollectionTests(ITestOutputHelper helper) : base(helper) { } - private static void ForceGC() + private static void ForceGC() + { + for (int i = 0; i < 3; i++) { - for (int i = 0; i < 3; i++) - { - GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); - GC.WaitForPendingFinalizers(); - } + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); + GC.WaitForPendingFinalizers(); } + } - [Fact] - public async Task MuxerIsCollected() - { + [Fact] + public async Task MuxerIsCollected() + { #if DEBUG - Skip.Inconclusive("Only predictable in release builds"); + Skip.Inconclusive("Only predictable in release builds"); #endif - // this is more nuanced than it looks; multiple sockets with - // async callbacks, plus a heartbeat on a timer + // this is more nuanced than it looks; multiple sockets with + // async callbacks, plus a heartbeat on a timer - // deliberately not "using" - we *want* to leak this - var muxer = Create(); - muxer.GetDatabase().Ping(); // smoke-test + // deliberately not "using" - we *want* to leak this + var conn = Create(); + conn.GetDatabase().Ping(); // smoke-test - ForceGC(); + ForceGC(); //#if DEBUG // this counter only exists in debug // int before = ConnectionMultiplexer.CollectedWithoutDispose; //#endif - var wr = new WeakReference(muxer); - muxer = null; + var wr = new WeakReference(conn); + conn = null; - ForceGC(); - await Task.Delay(2000).ForAwait(); // GC is twitchy - ForceGC(); + ForceGC(); + await Task.Delay(2000).ForAwait(); // GC is twitchy + ForceGC(); - // should be collectable - Assert.Null(wr.Target); + // should be collectable + Assert.Null(wr.Target); //#if DEBUG // this counter only exists in debug // int after = ConnectionMultiplexer.CollectedWithoutDispose; // Assert.Equal(before + 1, after); //#endif - } } } diff --git a/tests/StackExchange.Redis.Tests/GeoTests.cs b/tests/StackExchange.Redis.Tests/GeoTests.cs index fa3a3e4a1..562bd1f5b 100644 --- a/tests/StackExchange.Redis.Tests/GeoTests.cs +++ b/tests/StackExchange.Redis.Tests/GeoTests.cs @@ -3,640 +3,625 @@ using Xunit.Abstractions; using System.Threading.Tasks; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class GeoTests : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class GeoTests : TestBase + public GeoTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + + private static readonly GeoEntry + palermo = new GeoEntry(13.361389, 38.115556, "Palermo"), + catania = new GeoEntry(15.087269, 37.502669, "Catania"), + agrigento = new GeoEntry(13.5765, 37.311, "Agrigento"), + cefalù = new GeoEntry(14.0188, 38.0084, "Cefalù"); + private static readonly GeoEntry[] all = { palermo, catania, agrigento, cefalù }; + + [Fact] + public void GeoAdd() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + // add while not there + Assert.True(db.GeoAdd(key, cefalù.Longitude, cefalù.Latitude, cefalù.Member)); + Assert.Equal(2, db.GeoAdd(key, new[] { palermo, catania })); + Assert.True(db.GeoAdd(key, agrigento)); + + // now add again + Assert.False(db.GeoAdd(key, cefalù.Longitude, cefalù.Latitude, cefalù.Member)); + Assert.Equal(0, db.GeoAdd(key, new[] { palermo, catania })); + Assert.False(db.GeoAdd(key, agrigento)); + + // Validate + var pos = db.GeoPosition(key, palermo.Member); + Assert.NotNull(pos); + Assert.Equal(palermo.Longitude, pos.Value.Longitude, 5); + Assert.Equal(palermo.Latitude, pos.Value.Latitude, 5); + } + + [Fact] + public void GetDistance() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.GeoAdd(key, all, CommandFlags.FireAndForget); + var val = db.GeoDistance(key, "Palermo", "Catania", GeoUnit.Meters); + Assert.True(val.HasValue); + Assert.Equal(166274.1516, val); + + val = db.GeoDistance(key, "Palermo", "Nowhere", GeoUnit.Meters); + Assert.False(val.HasValue); + } + + [Fact] + public void GeoHash() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.GeoAdd(key, all, CommandFlags.FireAndForget); + + var hashes = db.GeoHash(key, new RedisValue[] { palermo.Member, "Nowhere", agrigento.Member }); + Assert.NotNull(hashes); + Assert.Equal(3, hashes.Length); + Assert.Equal("sqc8b49rny0", hashes[0]); + Assert.Null(hashes[1]); + Assert.Equal("sq9skbq0760", hashes[2]); + + var hash = db.GeoHash(key, "Palermo"); + Assert.Equal("sqc8b49rny0", hash); + + hash = db.GeoHash(key, "Nowhere"); + Assert.Null(hash); + } + + [Fact] + public void GeoGetPosition() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.GeoAdd(key, all, CommandFlags.FireAndForget); + + var pos = db.GeoPosition(key, palermo.Member); + Assert.True(pos.HasValue); + Assert.Equal(Math.Round(palermo.Longitude, 6), Math.Round(pos.Value.Longitude, 6)); + Assert.Equal(Math.Round(palermo.Latitude, 6), Math.Round(pos.Value.Latitude, 6)); + + pos = db.GeoPosition(key, "Nowhere"); + Assert.False(pos.HasValue); + } + + [Fact] + public void GeoRemove() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.GeoAdd(key, all, CommandFlags.FireAndForget); + + var pos = db.GeoPosition(key, "Palermo"); + Assert.True(pos.HasValue); + + Assert.False(db.GeoRemove(key, "Nowhere")); + Assert.True(db.GeoRemove(key, "Palermo")); + Assert.False(db.GeoRemove(key, "Palermo")); + + pos = db.GeoPosition(key, "Palermo"); + Assert.False(pos.HasValue); + } + + [Fact] + public void GeoRadius() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.GeoAdd(key, all, CommandFlags.FireAndForget); + + var results = db.GeoRadius(key, cefalù.Member, 60, GeoUnit.Miles, 2, Order.Ascending); + Assert.Equal(2, results.Length); + + Assert.Equal(results[0].Member, cefalù.Member); + Assert.Equal(0, results[0].Distance); + var position0 = results[0].Position; + Assert.NotNull(position0); + Assert.Equal(Math.Round(position0.Value.Longitude, 5), Math.Round(cefalù.Position.Longitude, 5)); + Assert.Equal(Math.Round(position0.Value.Latitude, 5), Math.Round(cefalù.Position.Latitude, 5)); + Assert.False(results[0].Hash.HasValue); + + Assert.Equal(results[1].Member, palermo.Member); + var distance1 = results[1].Distance; + Assert.NotNull(distance1); + Assert.Equal(Math.Round(36.5319, 6), Math.Round(distance1.Value, 6)); + var position1 = results[1].Position; + Assert.NotNull(position1); + Assert.Equal(Math.Round(position1.Value.Longitude, 5), Math.Round(palermo.Position.Longitude, 5)); + Assert.Equal(Math.Round(position1.Value.Latitude, 5), Math.Round(palermo.Position.Latitude, 5)); + Assert.False(results[1].Hash.HasValue); + + results = db.GeoRadius(key, cefalù.Member, 60, GeoUnit.Miles, 2, Order.Ascending, GeoRadiusOptions.None); + Assert.Equal(2, results.Length); + Assert.Equal(results[0].Member, cefalù.Member); + Assert.False(results[0].Position.HasValue); + Assert.False(results[0].Distance.HasValue); + Assert.False(results[0].Hash.HasValue); + + Assert.Equal(results[1].Member, palermo.Member); + Assert.False(results[1].Position.HasValue); + Assert.False(results[1].Distance.HasValue); + Assert.False(results[1].Hash.HasValue); + } + + [Fact] + public async Task GeoRadiusOverloads() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + Assert.True(db.GeoAdd(key, -1.759925, 52.19493, "steve")); + Assert.True(db.GeoAdd(key, -3.360655, 54.66395, "dave")); + + // Invalid overload + // Since this would throw ERR could not decode requested zset member, we catch and return something more useful to the user earlier. + var ex = Assert.Throws(() => db.GeoRadius(key, -1.759925, 52.19493, GeoUnit.Miles, 500, Order.Ascending, GeoRadiusOptions.WithDistance)); + Assert.StartsWith("Member should not be a double, you likely want the GeoRadius(RedisKey, double, double, ...) overload.", ex.Message); + Assert.Equal("member", ex.ParamName); + ex = await Assert.ThrowsAsync(() => db.GeoRadiusAsync(key, -1.759925, 52.19493, GeoUnit.Miles, 500, Order.Ascending, GeoRadiusOptions.WithDistance)).ForAwait(); + Assert.StartsWith("Member should not be a double, you likely want the GeoRadius(RedisKey, double, double, ...) overload.", ex.Message); + Assert.Equal("member", ex.ParamName); + + // The good stuff + GeoRadiusResult[] result = db.GeoRadius(key, -1.759925, 52.19493, 500, unit: GeoUnit.Miles, order: Order.Ascending, options: GeoRadiusOptions.WithDistance); + Assert.NotNull(result); + + result = await db.GeoRadiusAsync(key, -1.759925, 52.19493, 500, unit: GeoUnit.Miles, order: Order.Ascending, options: GeoRadiusOptions.WithDistance).ForAwait(); + Assert.NotNull(result); + } + + private async Task GeoSearchSetupAsync(RedisKey key, IDatabase db) + { + await db.KeyDeleteAsync(key); + await db.GeoAddAsync(key, 82.6534, 27.7682, "rays"); + await db.GeoAddAsync(key, 79.3891, 43.6418, "blue jays"); + await db.GeoAddAsync(key, 76.6217, 39.2838, "orioles"); + await db.GeoAddAsync(key, 71.0927, 42.3467, "red sox"); + await db.GeoAddAsync(key, 73.9262, 40.8296, "yankees"); + } + + private void GeoSearchSetup(RedisKey key, IDatabase db) + { + db.KeyDelete(key); + db.GeoAdd(key, 82.6534, 27.7682, "rays"); + db.GeoAdd(key, 79.3891, 43.6418, "blue jays"); + db.GeoAdd(key, 76.6217, 39.2838, "orioles"); + db.GeoAdd(key, 71.0927, 42.3467, "red sox"); + db.GeoAdd(key, 73.9262, 40.8296, "yankees"); + } + + [Fact] + public async Task GeoSearchCircleMemberAsync() { - public GeoTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } - - private static readonly GeoEntry - palermo = new GeoEntry(13.361389, 38.115556, "Palermo"), - catania = new GeoEntry(15.087269, 37.502669, "Catania"), - agrigento = new GeoEntry(13.5765, 37.311, "Agrigento"), - cefalù = new GeoEntry(14.0188, 38.0084, "Cefalù"); - private static readonly GeoEntry[] all = { palermo, catania, agrigento, cefalù }; - - [Fact] - public void GeoAdd() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v3_2_0); - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - // add while not there - Assert.True(db.GeoAdd(key, cefalù.Longitude, cefalù.Latitude, cefalù.Member)); - Assert.Equal(2, db.GeoAdd(key, new [] { palermo, catania })); - Assert.True(db.GeoAdd(key, agrigento)); - - // now add again - Assert.False(db.GeoAdd(key, cefalù.Longitude, cefalù.Latitude, cefalù.Member)); - Assert.Equal(0, db.GeoAdd(key, new [] { palermo, catania })); - Assert.False(db.GeoAdd(key, agrigento)); - - // Validate - var pos = db.GeoPosition(key, palermo.Member); - Assert.NotNull(pos); - Assert.Equal(palermo.Longitude, pos.Value.Longitude, 5); - Assert.Equal(palermo.Latitude, pos.Value.Latitude, 5); - } - } - - [Fact] - public void GetDistance() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v3_2_0); - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.GeoAdd(key, all, CommandFlags.FireAndForget); - var val = db.GeoDistance(key, "Palermo", "Catania", GeoUnit.Meters); - Assert.True(val.HasValue); - Assert.Equal(166274.1516, val); - - val = db.GeoDistance(key, "Palermo", "Nowhere", GeoUnit.Meters); - Assert.False(val.HasValue); - } - } - - [Fact] - public void GeoHash() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v3_2_0); - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.GeoAdd(key, all, CommandFlags.FireAndForget); - - var hashes = db.GeoHash(key, new RedisValue[] { palermo.Member, "Nowhere", agrigento.Member }); - Assert.NotNull(hashes); - Assert.Equal(3, hashes.Length); - Assert.Equal("sqc8b49rny0", hashes[0]); - Assert.Null(hashes[1]); - Assert.Equal("sq9skbq0760", hashes[2]); - - var hash = db.GeoHash(key, "Palermo"); - Assert.Equal("sqc8b49rny0", hash); - - hash = db.GeoHash(key, "Nowhere"); - Assert.Null(hash); - } - } - - [Fact] - public void GeoGetPosition() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v3_2_0); - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.GeoAdd(key, all, CommandFlags.FireAndForget); - - var pos = db.GeoPosition(key, palermo.Member); - Assert.True(pos.HasValue); - Assert.Equal(Math.Round(palermo.Longitude, 6), Math.Round(pos.Value.Longitude, 6)); - Assert.Equal(Math.Round(palermo.Latitude, 6), Math.Round(pos.Value.Latitude, 6)); - - pos = db.GeoPosition(key, "Nowhere"); - Assert.False(pos.HasValue); - } - } - - [Fact] - public void GeoRemove() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v3_2_0); - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.GeoAdd(key, all, CommandFlags.FireAndForget); - - var pos = db.GeoPosition(key, "Palermo"); - Assert.True(pos.HasValue); - - Assert.False(db.GeoRemove(key, "Nowhere")); - Assert.True(db.GeoRemove(key, "Palermo")); - Assert.False(db.GeoRemove(key, "Palermo")); - - pos = db.GeoPosition(key, "Palermo"); - Assert.False(pos.HasValue); - } - } - - [Fact] - public void GeoRadius() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v3_2_0); - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.GeoAdd(key, all, CommandFlags.FireAndForget); - - var results = db.GeoRadius(key, cefalù.Member, 60, GeoUnit.Miles, 2, Order.Ascending); - Assert.Equal(2, results.Length); - - Assert.Equal(results[0].Member, cefalù.Member); - Assert.Equal(0, results[0].Distance); - var position0 = results[0].Position; - Assert.NotNull(position0); - Assert.Equal(Math.Round(position0.Value.Longitude, 5), Math.Round(cefalù.Position.Longitude, 5)); - Assert.Equal(Math.Round(position0.Value.Latitude, 5), Math.Round(cefalù.Position.Latitude, 5)); - Assert.False(results[0].Hash.HasValue); - - Assert.Equal(results[1].Member, palermo.Member); - var distance1 = results[1].Distance; - Assert.NotNull(distance1); - Assert.Equal(Math.Round(36.5319, 6), Math.Round(distance1.Value, 6)); - var position1 = results[1].Position; - Assert.NotNull(position1); - Assert.Equal(Math.Round(position1.Value.Longitude, 5), Math.Round(palermo.Position.Longitude, 5)); - Assert.Equal(Math.Round(position1.Value.Latitude, 5), Math.Round(palermo.Position.Latitude, 5)); - Assert.False(results[1].Hash.HasValue); - - results = db.GeoRadius(key, cefalù.Member, 60, GeoUnit.Miles, 2, Order.Ascending, GeoRadiusOptions.None); - Assert.Equal(2, results.Length); - Assert.Equal(results[0].Member, cefalù.Member); - Assert.False(results[0].Position.HasValue); - Assert.False(results[0].Distance.HasValue); - Assert.False(results[0].Hash.HasValue); - - Assert.Equal(results[1].Member, palermo.Member); - Assert.False(results[1].Position.HasValue); - Assert.False(results[1].Distance.HasValue); - Assert.False(results[1].Hash.HasValue); - } - } - - [Fact] - public async Task GeoRadiusOverloads() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v3_2_0); - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - Assert.True(db.GeoAdd(key, -1.759925, 52.19493, "steve")); - Assert.True(db.GeoAdd(key, -3.360655, 54.66395, "dave")); - - // Invalid overload - // Since this would throw ERR could not decode requested zset member, we catch and return something more useful to the user earlier. - var ex = Assert.Throws(() => db.GeoRadius(key, -1.759925, 52.19493, GeoUnit.Miles, 500, Order.Ascending, GeoRadiusOptions.WithDistance)); - Assert.StartsWith("Member should not be a double, you likely want the GeoRadius(RedisKey, double, double, ...) overload.", ex.Message); - Assert.Equal("member", ex.ParamName); - ex = await Assert.ThrowsAsync(() => db.GeoRadiusAsync(key, -1.759925, 52.19493, GeoUnit.Miles, 500, Order.Ascending, GeoRadiusOptions.WithDistance)).ForAwait(); - Assert.StartsWith("Member should not be a double, you likely want the GeoRadius(RedisKey, double, double, ...) overload.", ex.Message); - Assert.Equal("member", ex.ParamName); - - // The good stuff - GeoRadiusResult[] result = db.GeoRadius(key, -1.759925, 52.19493, 500, unit: GeoUnit.Miles, order: Order.Ascending, options: GeoRadiusOptions.WithDistance); - Assert.NotNull(result); - - result = await db.GeoRadiusAsync(key, -1.759925, 52.19493, 500, unit: GeoUnit.Miles, order: Order.Ascending, options: GeoRadiusOptions.WithDistance).ForAwait(); - Assert.NotNull(result); - } - } - - private async Task GeoSearchSetupAsync(RedisKey key, IDatabase db) - { - await db.KeyDeleteAsync(key); - await db.GeoAddAsync(key, 82.6534, 27.7682, "rays"); - await db.GeoAddAsync(key, 79.3891, 43.6418, "blue jays"); - await db.GeoAddAsync(key, 76.6217, 39.2838, "orioles"); - await db.GeoAddAsync(key, 71.0927, 42.3467, "red sox"); - await db.GeoAddAsync(key, 73.9262, 40.8296, "yankees"); - } - - private void GeoSearchSetup(RedisKey key, IDatabase db) - { - db.KeyDelete(key); - db.GeoAdd(key, 82.6534, 27.7682, "rays"); - db.GeoAdd(key, 79.3891, 43.6418, "blue jays"); - db.GeoAdd(key, 76.6217, 39.2838, "orioles"); - db.GeoAdd(key, 71.0927, 42.3467, "red sox"); - db.GeoAdd(key, 73.9262, 40.8296, "yankees"); - } - - [Fact] - public async Task GeoSearchCircleMemberAsync() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - await GeoSearchSetupAsync(key, db); - - var circle = new GeoSearchCircle(500, GeoUnit.Miles); - var res = await db.GeoSearchAsync(key, "yankees", circle); - Assert.Contains(res, x => x.Member == "yankees"); - Assert.Contains(res, x => x.Member == "red sox"); - Assert.Contains(res, x => x.Member == "orioles"); - Assert.Contains(res, x => x.Member == "blue jays"); - Assert.NotNull(res[0].Distance); - Assert.NotNull(res[0].Position); - Assert.Null(res[0].Hash); - Assert.Equal(4, res.Length); - } - - [Fact] - public async Task GeoSearchCircleMemberAsyncOnlyHash() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - await GeoSearchSetupAsync(key, db); - - var circle = new GeoSearchCircle(500, GeoUnit.Miles); - var res = await db.GeoSearchAsync(key, "yankees", circle, options: GeoRadiusOptions.WithGeoHash); - Assert.Contains(res, x => x.Member == "yankees"); - Assert.Contains(res, x => x.Member == "red sox"); - Assert.Contains(res, x => x.Member == "orioles"); - Assert.Contains(res, x => x.Member == "blue jays"); - Assert.Null(res[0].Distance); - Assert.Null(res[0].Position); - Assert.NotNull(res[0].Hash); - Assert.Equal(4, res.Length); - } - - [Fact] - public async Task GeoSearchCircleMemberAsyncHashAndDistance() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - await GeoSearchSetupAsync(key, db); - - var circle = new GeoSearchCircle(500, GeoUnit.Miles); - var res = await db.GeoSearchAsync(key, "yankees", circle, options: GeoRadiusOptions.WithGeoHash | GeoRadiusOptions.WithDistance); - Assert.Contains(res, x => x.Member == "yankees"); - Assert.Contains(res, x => x.Member == "red sox"); - Assert.Contains(res, x => x.Member == "orioles"); - Assert.Contains(res, x => x.Member == "blue jays"); - Assert.NotNull(res[0].Distance); - Assert.Null(res[0].Position); - Assert.NotNull(res[0].Hash); - Assert.Equal(4, res.Length); - } - - [Fact] - public async Task GeoSearchCircleLonLatAsync() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - await GeoSearchSetupAsync(key, db); - - var circle = new GeoSearchCircle(500, GeoUnit.Miles); - var res = await db.GeoSearchAsync(key, 73.9262, 40.8296, circle); - Assert.Contains(res, x => x.Member == "yankees"); - Assert.Contains(res, x => x.Member == "red sox"); - Assert.Contains(res, x => x.Member == "orioles"); - Assert.Contains(res, x => x.Member == "blue jays"); - Assert.Equal(4, res.Length); - } - - [Fact] - public void GeoSearchCircleMember() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - GeoSearchSetup(key, db); - - var circle = new GeoSearchCircle(500 * 1609); - var res = db.GeoSearch(key, "yankees", circle); - Assert.Contains(res, x => x.Member == "yankees"); - Assert.Contains(res, x => x.Member == "red sox"); - Assert.Contains(res, x => x.Member == "orioles"); - Assert.Contains(res, x => x.Member == "blue jays"); - Assert.Equal(4, res.Length); - } - - [Fact] - public void GeoSearchCircleLonLat() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - GeoSearchSetup(key, db); - - var circle = new GeoSearchCircle(500 * 5280, GeoUnit.Feet); - var res = db.GeoSearch(key, 73.9262, 40.8296, circle); - Assert.Contains(res, x => x.Member == "yankees"); - Assert.Contains(res, x => x.Member == "red sox"); - Assert.Contains(res, x => x.Member == "orioles"); - Assert.Contains(res, x => x.Member == "blue jays"); - Assert.Equal(4, res.Length); - } - - [Fact] - public async Task GeoSearchBoxMemberAsync() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - await GeoSearchSetupAsync(key, db); - - var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); - var res = await db.GeoSearchAsync(key, "yankees", box); - Assert.Contains(res, x => x.Member == "yankees"); - Assert.Contains(res, x => x.Member == "red sox"); - Assert.Contains(res, x => x.Member == "orioles"); - Assert.Equal(3, res.Length); - } - - [Fact] - public async Task GeoSearchBoxLonLatAsync() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - await GeoSearchSetupAsync(key, db); - - var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); - var res = await db.GeoSearchAsync(key, 73.9262, 40.8296, box); - Assert.Contains(res, x => x.Member == "yankees"); - Assert.Contains(res, x => x.Member == "red sox"); - Assert.Contains(res, x => x.Member == "orioles"); - Assert.Equal(3, res.Length); - } - - [Fact] - public void GeoSearchBoxMember() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - GeoSearchSetup(key, db); - - var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); - var res = db.GeoSearch(key, "yankees", box); - Assert.Contains(res, x => x.Member == "yankees"); - Assert.Contains(res, x => x.Member == "red sox"); - Assert.Contains(res, x => x.Member == "orioles"); - Assert.Equal(3, res.Length); - } - - [Fact] - public void GeoSearchBoxLonLat() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - GeoSearchSetup(key, db); - - var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); - var res = db.GeoSearch(key, 73.9262, 40.8296, box); - Assert.Contains(res, x => x.Member == "yankees"); - Assert.Contains(res, x => x.Member == "red sox"); - Assert.Contains(res, x => x.Member == "orioles"); - Assert.Equal(3, res.Length); - } - - [Fact] - public void GeoSearchLimitCount() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - GeoSearchSetup(key, db); - - var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); - var res = db.GeoSearch(key, 73.9262, 40.8296, box, count: 2); - Assert.Contains(res, x => x.Member == "yankees"); - Assert.Contains(res, x => x.Member == "orioles"); - Assert.Equal(2, res.Length); - } - - [Fact] - public void GeoSearchLimitCountMakeNoDemands() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - GeoSearchSetup(key, db); - - var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); - var res = db.GeoSearch(key, 73.9262, 40.8296, box, count: 2, demandClosest: false); - Assert.Contains(res, x => x.Member == "red sox"); // this order MIGHT not be fully deterministic, seems to work for our purposes. - Assert.Contains(res, x => x.Member == "orioles"); - Assert.Equal(2, res.Length); - } - - [Fact] - public async Task GeoSearchBoxLonLatDescending() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - await GeoSearchSetupAsync(key, db); - - var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); - var res = await db.GeoSearchAsync(key, 73.9262, 40.8296, box, order: Order.Descending); - Assert.Contains(res, x => x.Member == "yankees"); - Assert.Contains(res, x => x.Member == "red sox"); - Assert.Contains(res, x => x.Member == "orioles"); - Assert.Equal(3, res.Length); - Assert.Equal("red sox", res[0].Member); - } - - [Fact] - public async Task GeoSearchBoxMemberAndStoreAsync() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var me = Me(); - var db = conn.GetDatabase(); - RedisKey sourceKey = $"{me}:source"; - RedisKey destinationKey = $"{me}:destination"; - await db.KeyDeleteAsync(destinationKey); - await GeoSearchSetupAsync(sourceKey, db); - - var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); - var res = await db.GeoSearchAndStoreAsync(sourceKey, destinationKey, "yankees", box); - var set = await db.GeoSearchAsync(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); - Assert.Contains(set, x => x.Member == "yankees"); - Assert.Contains(set, x => x.Member == "red sox"); - Assert.Contains(set, x => x.Member == "orioles"); - Assert.Equal(3, set.Length); - Assert.Equal(3, res); - } - - [Fact] - public async Task GeoSearchBoxLonLatAndStoreAsync() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var me = Me(); - var db = conn.GetDatabase(); - RedisKey sourceKey = $"{me}:source"; - RedisKey destinationKey = $"{me}:destination"; - await db.KeyDeleteAsync(destinationKey); - await GeoSearchSetupAsync(sourceKey, db); - - var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); - var res = await db.GeoSearchAndStoreAsync(sourceKey, destinationKey, 73.9262, 40.8296, box); - var set = await db.GeoSearchAsync(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); - Assert.Contains(set, x => x.Member == "yankees"); - Assert.Contains(set, x => x.Member == "red sox"); - Assert.Contains(set, x => x.Member == "orioles"); - Assert.Equal(3, set.Length); - Assert.Equal(3, res); - } - - [Fact] - public async Task GeoSearchCircleMemberAndStoreAsync() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var me = Me(); - var db = conn.GetDatabase(); - RedisKey sourceKey = $"{me}:source"; - RedisKey destinationKey = $"{me}:destination"; - await db.KeyDeleteAsync(destinationKey); - await GeoSearchSetupAsync(sourceKey, db); - - var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); - var res = await db.GeoSearchAndStoreAsync(sourceKey, destinationKey, "yankees", circle); - var set = await db.GeoSearchAsync(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); - Assert.Contains(set, x => x.Member == "yankees"); - Assert.Contains(set, x => x.Member == "red sox"); - Assert.Contains(set, x => x.Member == "orioles"); - Assert.Equal(3, set.Length); - Assert.Equal(3, res); - } - - [Fact] - public async Task GeoSearchCircleLonLatAndStoreAsync() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var me = Me(); - var db = conn.GetDatabase(); - RedisKey sourceKey = $"{me}:source"; - RedisKey destinationKey = $"{me}:destination"; - await db.KeyDeleteAsync(destinationKey); - await GeoSearchSetupAsync(sourceKey, db); - - var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); - var res = await db.GeoSearchAndStoreAsync(sourceKey, destinationKey, 73.9262, 40.8296, circle); - var set = await db.GeoSearchAsync(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); - Assert.Contains(set, x => x.Member == "yankees"); - Assert.Contains(set, x => x.Member == "red sox"); - Assert.Contains(set, x => x.Member == "orioles"); - Assert.Equal(3, set.Length); - Assert.Equal(3, res); - } - - [Fact] - public void GeoSearchCircleMemberAndStore() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var me = Me(); - var db = conn.GetDatabase(); - RedisKey sourceKey = $"{me}:source"; - RedisKey destinationKey = $"{me}:destination"; - db.KeyDelete(destinationKey); - GeoSearchSetup(sourceKey, db); - - var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); - var res = db.GeoSearchAndStore(sourceKey, destinationKey, "yankees", circle); - var set = db.GeoSearch(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); - Assert.Contains(set, x => x.Member == "yankees"); - Assert.Contains(set, x => x.Member == "red sox"); - Assert.Contains(set, x => x.Member == "orioles"); - Assert.Equal(3, set.Length); - Assert.Equal(3, res); - } - - [Fact] - public void GeoSearchCircleLonLatAndStore() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var me = Me(); - var db = conn.GetDatabase(); - RedisKey sourceKey = $"{me}:source"; - RedisKey destinationKey = $"{me}:destination"; - db.KeyDelete(destinationKey); - GeoSearchSetup(sourceKey, db); - - var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); - var res = db.GeoSearchAndStore(sourceKey, destinationKey, 73.9262, 40.8296, circle); - var set = db.GeoSearch(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); - Assert.Contains(set, x => x.Member == "yankees"); - Assert.Contains(set, x => x.Member == "red sox"); - Assert.Contains(set, x => x.Member == "orioles"); - Assert.Equal(3, set.Length); - Assert.Equal(3, res); - } - - [Fact] - public void GeoSearchCircleAndStoreDistOnly() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var me = Me(); - var db = conn.GetDatabase(); - RedisKey sourceKey = $"{me}:source"; - RedisKey destinationKey = $"{me}:destination"; - db.KeyDelete(destinationKey); - GeoSearchSetup(sourceKey, db); - - var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); - var res = db.GeoSearchAndStore(sourceKey, destinationKey, 73.9262, 40.8296, circle, storeDistances: true); - var set = db.SortedSetRangeByRankWithScores(destinationKey); - Assert.Contains(set, x => x.Element == "yankees"); - Assert.Contains(set, x => x.Element == "red sox"); - Assert.Contains(set, x => x.Element == "orioles"); - Assert.InRange(Array.Find(set, x => x.Element == "yankees").Score, 0, .2); - Assert.InRange(Array.Find(set, x => x.Element == "orioles").Score, 286, 287); - Assert.InRange(Array.Find(set, x => x.Element == "red sox").Score, 289, 290); - Assert.Equal(3, set.Length); - Assert.Equal(3, res); - } - - [Fact] - public void GeoSearchBadArgs() - { - using var conn = Create(require: RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - db.KeyDelete(key); - var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); - var exception = Assert.Throws(() => - db.GeoSearch(key, "irrelevant", circle, demandClosest: false)); - - Assert.Contains("demandClosest must be true if you are not limiting the count for a GEOSEARCH", - exception.Message); - } + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await GeoSearchSetupAsync(key, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Miles); + var res = await db.GeoSearchAsync(key, "yankees", circle); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Contains(res, x => x.Member == "blue jays"); + Assert.NotNull(res[0].Distance); + Assert.NotNull(res[0].Position); + Assert.Null(res[0].Hash); + Assert.Equal(4, res.Length); + } + + [Fact] + public async Task GeoSearchCircleMemberAsyncOnlyHash() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await GeoSearchSetupAsync(key, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Miles); + var res = await db.GeoSearchAsync(key, "yankees", circle, options: GeoRadiusOptions.WithGeoHash); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Contains(res, x => x.Member == "blue jays"); + Assert.Null(res[0].Distance); + Assert.Null(res[0].Position); + Assert.NotNull(res[0].Hash); + Assert.Equal(4, res.Length); + } + + [Fact] + public async Task GeoSearchCircleMemberAsyncHashAndDistance() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await GeoSearchSetupAsync(key, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Miles); + var res = await db.GeoSearchAsync(key, "yankees", circle, options: GeoRadiusOptions.WithGeoHash | GeoRadiusOptions.WithDistance); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Contains(res, x => x.Member == "blue jays"); + Assert.NotNull(res[0].Distance); + Assert.Null(res[0].Position); + Assert.NotNull(res[0].Hash); + Assert.Equal(4, res.Length); + } + + [Fact] + public async Task GeoSearchCircleLonLatAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await GeoSearchSetupAsync(key, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Miles); + var res = await db.GeoSearchAsync(key, 73.9262, 40.8296, circle); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Contains(res, x => x.Member == "blue jays"); + Assert.Equal(4, res.Length); + } + + [Fact] + public void GeoSearchCircleMember() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + GeoSearchSetup(key, db); + + var circle = new GeoSearchCircle(500 * 1609); + var res = db.GeoSearch(key, "yankees", circle); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Contains(res, x => x.Member == "blue jays"); + Assert.Equal(4, res.Length); + } + + [Fact] + public void GeoSearchCircleLonLat() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + GeoSearchSetup(key, db); + + var circle = new GeoSearchCircle(500 * 5280, GeoUnit.Feet); + var res = db.GeoSearch(key, 73.9262, 40.8296, circle); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Contains(res, x => x.Member == "blue jays"); + Assert.Equal(4, res.Length); + } + + [Fact] + public async Task GeoSearchBoxMemberAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await GeoSearchSetupAsync(key, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = await db.GeoSearchAsync(key, "yankees", box); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Equal(3, res.Length); + } + + [Fact] + public async Task GeoSearchBoxLonLatAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await GeoSearchSetupAsync(key, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = await db.GeoSearchAsync(key, 73.9262, 40.8296, box); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Equal(3, res.Length); + } + + [Fact] + public void GeoSearchBoxMember() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + GeoSearchSetup(key, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = db.GeoSearch(key, "yankees", box); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Equal(3, res.Length); + } + + [Fact] + public void GeoSearchBoxLonLat() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + GeoSearchSetup(key, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = db.GeoSearch(key, 73.9262, 40.8296, box); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Equal(3, res.Length); + } + + [Fact] + public void GeoSearchLimitCount() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + GeoSearchSetup(key, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = db.GeoSearch(key, 73.9262, 40.8296, box, count: 2); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Equal(2, res.Length); + } + + [Fact] + public void GeoSearchLimitCountMakeNoDemands() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + GeoSearchSetup(key, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = db.GeoSearch(key, 73.9262, 40.8296, box, count: 2, demandClosest: false); + Assert.Contains(res, x => x.Member == "red sox"); // this order MIGHT not be fully deterministic, seems to work for our purposes. + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Equal(2, res.Length); + } + + [Fact] + public async Task GeoSearchBoxLonLatDescending() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + await GeoSearchSetupAsync(key, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = await db.GeoSearchAsync(key, 73.9262, 40.8296, box, order: Order.Descending); + Assert.Contains(res, x => x.Member == "yankees"); + Assert.Contains(res, x => x.Member == "red sox"); + Assert.Contains(res, x => x.Member == "orioles"); + Assert.Equal(3, res.Length); + Assert.Equal("red sox", res[0].Member); + } + + [Fact] + public async Task GeoSearchBoxMemberAndStoreAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var me = Me(); + var db = conn.GetDatabase(); + RedisKey sourceKey = $"{me}:source"; + RedisKey destinationKey = $"{me}:destination"; + await db.KeyDeleteAsync(destinationKey); + await GeoSearchSetupAsync(sourceKey, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = await db.GeoSearchAndStoreAsync(sourceKey, destinationKey, "yankees", box); + var set = await db.GeoSearchAsync(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); + Assert.Contains(set, x => x.Member == "yankees"); + Assert.Contains(set, x => x.Member == "red sox"); + Assert.Contains(set, x => x.Member == "orioles"); + Assert.Equal(3, set.Length); + Assert.Equal(3, res); + } + + [Fact] + public async Task GeoSearchBoxLonLatAndStoreAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var me = Me(); + var db = conn.GetDatabase(); + RedisKey sourceKey = $"{me}:source"; + RedisKey destinationKey = $"{me}:destination"; + await db.KeyDeleteAsync(destinationKey); + await GeoSearchSetupAsync(sourceKey, db); + + var box = new GeoSearchBox(500, 500, GeoUnit.Kilometers); + var res = await db.GeoSearchAndStoreAsync(sourceKey, destinationKey, 73.9262, 40.8296, box); + var set = await db.GeoSearchAsync(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); + Assert.Contains(set, x => x.Member == "yankees"); + Assert.Contains(set, x => x.Member == "red sox"); + Assert.Contains(set, x => x.Member == "orioles"); + Assert.Equal(3, set.Length); + Assert.Equal(3, res); + } + + [Fact] + public async Task GeoSearchCircleMemberAndStoreAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var me = Me(); + var db = conn.GetDatabase(); + RedisKey sourceKey = $"{me}:source"; + RedisKey destinationKey = $"{me}:destination"; + await db.KeyDeleteAsync(destinationKey); + await GeoSearchSetupAsync(sourceKey, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); + var res = await db.GeoSearchAndStoreAsync(sourceKey, destinationKey, "yankees", circle); + var set = await db.GeoSearchAsync(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); + Assert.Contains(set, x => x.Member == "yankees"); + Assert.Contains(set, x => x.Member == "red sox"); + Assert.Contains(set, x => x.Member == "orioles"); + Assert.Equal(3, set.Length); + Assert.Equal(3, res); + } + + [Fact] + public async Task GeoSearchCircleLonLatAndStoreAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var me = Me(); + var db = conn.GetDatabase(); + RedisKey sourceKey = $"{me}:source"; + RedisKey destinationKey = $"{me}:destination"; + await db.KeyDeleteAsync(destinationKey); + await GeoSearchSetupAsync(sourceKey, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); + var res = await db.GeoSearchAndStoreAsync(sourceKey, destinationKey, 73.9262, 40.8296, circle); + var set = await db.GeoSearchAsync(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); + Assert.Contains(set, x => x.Member == "yankees"); + Assert.Contains(set, x => x.Member == "red sox"); + Assert.Contains(set, x => x.Member == "orioles"); + Assert.Equal(3, set.Length); + Assert.Equal(3, res); + } + + [Fact] + public void GeoSearchCircleMemberAndStore() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var me = Me(); + var db = conn.GetDatabase(); + RedisKey sourceKey = $"{me}:source"; + RedisKey destinationKey = $"{me}:destination"; + db.KeyDelete(destinationKey); + GeoSearchSetup(sourceKey, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); + var res = db.GeoSearchAndStore(sourceKey, destinationKey, "yankees", circle); + var set = db.GeoSearch(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); + Assert.Contains(set, x => x.Member == "yankees"); + Assert.Contains(set, x => x.Member == "red sox"); + Assert.Contains(set, x => x.Member == "orioles"); + Assert.Equal(3, set.Length); + Assert.Equal(3, res); + } + + [Fact] + public void GeoSearchCircleLonLatAndStore() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var me = Me(); + var db = conn.GetDatabase(); + RedisKey sourceKey = $"{me}:source"; + RedisKey destinationKey = $"{me}:destination"; + db.KeyDelete(destinationKey); + GeoSearchSetup(sourceKey, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); + var res = db.GeoSearchAndStore(sourceKey, destinationKey, 73.9262, 40.8296, circle); + var set = db.GeoSearch(destinationKey, "yankees", new GeoSearchCircle(10000, GeoUnit.Miles)); + Assert.Contains(set, x => x.Member == "yankees"); + Assert.Contains(set, x => x.Member == "red sox"); + Assert.Contains(set, x => x.Member == "orioles"); + Assert.Equal(3, set.Length); + Assert.Equal(3, res); + } + + [Fact] + public void GeoSearchCircleAndStoreDistOnly() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var me = Me(); + var db = conn.GetDatabase(); + RedisKey sourceKey = $"{me}:source"; + RedisKey destinationKey = $"{me}:destination"; + db.KeyDelete(destinationKey); + GeoSearchSetup(sourceKey, db); + + var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); + var res = db.GeoSearchAndStore(sourceKey, destinationKey, 73.9262, 40.8296, circle, storeDistances: true); + var set = db.SortedSetRangeByRankWithScores(destinationKey); + Assert.Contains(set, x => x.Element == "yankees"); + Assert.Contains(set, x => x.Element == "red sox"); + Assert.Contains(set, x => x.Element == "orioles"); + Assert.InRange(Array.Find(set, x => x.Element == "yankees").Score, 0, .2); + Assert.InRange(Array.Find(set, x => x.Element == "orioles").Score, 286, 287); + Assert.InRange(Array.Find(set, x => x.Element == "red sox").Score, 289, 290); + Assert.Equal(3, set.Length); + Assert.Equal(3, res); + } + + [Fact] + public void GeoSearchBadArgs() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key); + var circle = new GeoSearchCircle(500, GeoUnit.Kilometers); + var exception = Assert.Throws(() => + db.GeoSearch(key, "irrelevant", circle, demandClosest: false)); + + Assert.Contains("demandClosest must be true if you are not limiting the count for a GEOSEARCH", + exception.Message); } } diff --git a/tests/StackExchange.Redis.Tests/Hashes.cs b/tests/StackExchange.Redis.Tests/Hashes.cs index 972b842de..671364eae 100644 --- a/tests/StackExchange.Redis.Tests/Hashes.cs +++ b/tests/StackExchange.Redis.Tests/Hashes.cs @@ -6,671 +6,642 @@ using Xunit.Abstractions; using System.Threading.Tasks; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Hashes : TestBase // https://redis.io/commands#hash { - [Collection(SharedConnectionFixture.Key)] - public class Hashes : TestBase // https://redis.io/commands#hash + public Hashes(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public async Task TestIncrBy() { - public Hashes(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + using var conn = Create(); - [Fact] - public async Task TestIncrBy() + var db = conn.GetDatabase(); + var key = Me(); + _ = db.KeyDeleteAsync(key).ForAwait(); + + const int iterations = 100; + var aTasks = new Task[iterations]; + var bTasks = new Task[iterations]; + for (int i = 1; i < iterations + 1; i++) + { + aTasks[i - 1] = db.HashIncrementAsync(key, "a", 1); + bTasks[i - 1] = db.HashIncrementAsync(key, "b", -1); + } + await Task.WhenAll(bTasks).ForAwait(); + for (int i = 1; i < iterations + 1; i++) { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var key = Me(); - _ = conn.KeyDeleteAsync(key).ForAwait(); - - const int iterations = 100; - var aTasks = new Task[iterations]; - var bTasks = new Task[iterations]; - for (int i = 1; i < iterations + 1; i++) - { - aTasks[i - 1] = conn.HashIncrementAsync(key, "a", 1); - bTasks[i - 1] = conn.HashIncrementAsync(key, "b", -1); - } - await Task.WhenAll(bTasks).ForAwait(); - for (int i = 1; i < iterations + 1; i++) - { - Assert.Equal(i, aTasks[i - 1].Result); - Assert.Equal(-i, bTasks[i - 1].Result); - } - } + Assert.Equal(i, aTasks[i - 1].Result); + Assert.Equal(-i, bTasks[i - 1].Result); } + } + + [Fact] + public async Task ScanAsync() + { + using var conn = Create(require: RedisFeatures.v2_8_0); - [Fact] - public async Task ScanAsync() + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key); + for (int i = 0; i < 200; i++) { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v2_8_0); - - var conn = muxer.GetDatabase(); - var key = Me(); - await conn.KeyDeleteAsync(key); - for(int i = 0; i < 200; i++) - { - await conn.HashSetAsync(key, "key" + i, "value " + i); - } - - int count = 0; - // works for async - await foreach(var _ in conn.HashScanAsync(key, pageSize: 20)) - { - count++; - } - Assert.Equal(200, count); - - // and sync=>async (via cast) - count = 0; - await foreach (var _ in (IAsyncEnumerable)conn.HashScan(key, pageSize: 20)) - { - count++; - } - Assert.Equal(200, count); - - // and sync (native) - count = 0; - foreach (var _ in conn.HashScan(key, pageSize: 20)) - { - count++; - } - Assert.Equal(200, count); - - // and async=>sync (via cast) - count = 0; - foreach (var _ in (IEnumerable)conn.HashScanAsync(key, pageSize: 20)) - { - count++; - } - Assert.Equal(200, count); - } + await db.HashSetAsync(key, "key" + i, "value " + i); } - [Fact] - public void Scan() + int count = 0; + // works for async + await foreach (var _ in db.HashScanAsync(key, pageSize: 20)) { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v2_8_0); - - var conn = muxer.GetDatabase(); - - var key = Me(); - conn.KeyDeleteAsync(key); - conn.HashSetAsync(key, "abc", "def"); - conn.HashSetAsync(key, "ghi", "jkl"); - conn.HashSetAsync(key, "mno", "pqr"); - - var t1 = conn.HashScan(key); - var t2 = conn.HashScan(key, "*h*"); - var t3 = conn.HashScan(key); - var t4 = conn.HashScan(key, "*h*"); - - var v1 = t1.ToArray(); - var v2 = t2.ToArray(); - var v3 = t3.ToArray(); - var v4 = t4.ToArray(); - - Assert.Equal(3, v1.Length); - Assert.Single(v2); - Assert.Equal(3, v3.Length); - Assert.Single(v4); - Array.Sort(v1, (x, y) => string.Compare(x.Name, y.Name)); - Array.Sort(v2, (x, y) => string.Compare(x.Name, y.Name)); - Array.Sort(v3, (x, y) => string.Compare(x.Name, y.Name)); - Array.Sort(v4, (x, y) => string.Compare(x.Name, y.Name)); - - Assert.Equal("abc=def,ghi=jkl,mno=pqr", string.Join(",", v1.Select(pair => pair.Name + "=" + pair.Value))); - Assert.Equal("ghi=jkl", string.Join(",", v2.Select(pair => pair.Name + "=" + pair.Value))); - Assert.Equal("abc=def,ghi=jkl,mno=pqr", string.Join(",", v3.Select(pair => pair.Name + "=" + pair.Value))); - Assert.Equal("ghi=jkl", string.Join(",", v4.Select(pair => pair.Name + "=" + pair.Value))); - } + count++; } + Assert.Equal(200, count); - [Fact] - public void TestIncrementOnHashThatDoesntExist() + // and sync=>async (via cast) + count = 0; + await foreach (var _ in (IAsyncEnumerable)db.HashScan(key, pageSize: 20)) { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - conn.KeyDeleteAsync("keynotexist"); - var result1 = conn.Wait(conn.HashIncrementAsync("keynotexist", "fieldnotexist", 1)); - var result2 = conn.Wait(conn.HashIncrementAsync("keynotexist", "anotherfieldnotexist", 1)); - Assert.Equal(1, result1); - Assert.Equal(1, result2); - } + count++; } + Assert.Equal(200, count); - [Fact] - public async Task TestIncrByFloat() + // and sync (native) + count = 0; + foreach (var _ in db.HashScan(key, pageSize: 20)) { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v2_6_0); - var conn = muxer.GetDatabase(); - var key = Me(); - _ = conn.KeyDeleteAsync(key).ForAwait(); - var aTasks = new Task[1000]; - var bTasks = new Task[1000]; - for (int i = 1; i < 1001; i++) - { - aTasks[i-1] = conn.HashIncrementAsync(key, "a", 1.0); - bTasks[i-1] = conn.HashIncrementAsync(key, "b", -1.0); - } - await Task.WhenAll(bTasks).ForAwait(); - for (int i = 1; i < 1001; i++) - { - Assert.Equal(i, aTasks[i-1].Result); - Assert.Equal(-i, bTasks[i-1].Result); - } - } + count++; } + Assert.Equal(200, count); - [Fact] - public async Task TestGetAll() + // and async=>sync (via cast) + count = 0; + foreach (var _ in (IEnumerable)db.HashScanAsync(key, pageSize: 20)) { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var key = Me(); - await conn.KeyDeleteAsync(key).ForAwait(); - var shouldMatch = new Dictionary(); - var random = new Random(); - - for (int i = 0; i < 1000; i++) - { - var guid = Guid.NewGuid(); - var value = random.Next(int.MaxValue); - - shouldMatch[guid] = value; - - _ = conn.HashIncrementAsync(key, guid.ToString(), value); - } - - var inRedis = (await conn.HashGetAllAsync(key).ForAwait()).ToDictionary( - x => Guid.Parse(x.Name!), x => int.Parse(x.Value!)); - - Assert.Equal(shouldMatch.Count, inRedis.Count); - - foreach (var k in shouldMatch.Keys) - { - Assert.Equal(shouldMatch[k], inRedis[k]); - } - } + count++; } + Assert.Equal(200, count); + } + + [Fact] + public void Scan() + { + using var conn = Create(require: RedisFeatures.v2_8_0); + + var db = conn.GetDatabase(); + + var key = Me(); + db.KeyDeleteAsync(key); + db.HashSetAsync(key, "abc", "def"); + db.HashSetAsync(key, "ghi", "jkl"); + db.HashSetAsync(key, "mno", "pqr"); + + var t1 = db.HashScan(key); + var t2 = db.HashScan(key, "*h*"); + var t3 = db.HashScan(key); + var t4 = db.HashScan(key, "*h*"); + + var v1 = t1.ToArray(); + var v2 = t2.ToArray(); + var v3 = t3.ToArray(); + var v4 = t4.ToArray(); + + Assert.Equal(3, v1.Length); + Assert.Single(v2); + Assert.Equal(3, v3.Length); + Assert.Single(v4); + Array.Sort(v1, (x, y) => string.Compare(x.Name, y.Name)); + Array.Sort(v2, (x, y) => string.Compare(x.Name, y.Name)); + Array.Sort(v3, (x, y) => string.Compare(x.Name, y.Name)); + Array.Sort(v4, (x, y) => string.Compare(x.Name, y.Name)); + + Assert.Equal("abc=def,ghi=jkl,mno=pqr", string.Join(",", v1.Select(pair => pair.Name + "=" + pair.Value))); + Assert.Equal("ghi=jkl", string.Join(",", v2.Select(pair => pair.Name + "=" + pair.Value))); + Assert.Equal("abc=def,ghi=jkl,mno=pqr", string.Join(",", v3.Select(pair => pair.Name + "=" + pair.Value))); + Assert.Equal("ghi=jkl", string.Join(",", v4.Select(pair => pair.Name + "=" + pair.Value))); + } - [Fact] - public async Task TestGet() + [Fact] + public void TestIncrementOnHashThatDoesntExist() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + db.KeyDeleteAsync("keynotexist"); + var result1 = db.Wait(db.HashIncrementAsync("keynotexist", "fieldnotexist", 1)); + var result2 = db.Wait(db.HashIncrementAsync("keynotexist", "anotherfieldnotexist", 1)); + Assert.Equal(1, result1); + Assert.Equal(1, result2); + } + + [Fact] + public async Task TestIncrByFloat() + { + using var conn = Create(require: RedisFeatures.v2_6_0); + + var db = conn.GetDatabase(); + var key = Me(); + _ = db.KeyDeleteAsync(key).ForAwait(); + var aTasks = new Task[1000]; + var bTasks = new Task[1000]; + for (int i = 1; i < 1001; i++) { - using (var muxer = Create()) - { - var key = Me(); - var conn = muxer.GetDatabase(); - var shouldMatch = new Dictionary(); - var random = new Random(); - - for (int i = 1; i < 1000; i++) - { - var guid = Guid.NewGuid(); - var value = random.Next(int.MaxValue); - - shouldMatch[guid] = value; - - _ = conn.HashIncrementAsync(key, guid.ToString(), value); - } - - foreach (var k in shouldMatch.Keys) - { - var inRedis = await conn.HashGetAsync(key, k.ToString()).ForAwait(); - var num = int.Parse(inRedis!); - - Assert.Equal(shouldMatch[k], num); - } - } + aTasks[i - 1] = db.HashIncrementAsync(key, "a", 1.0); + bTasks[i - 1] = db.HashIncrementAsync(key, "b", -1.0); } + await Task.WhenAll(bTasks).ForAwait(); + for (int i = 1; i < 1001; i++) + { + Assert.Equal(i, aTasks[i - 1].Result); + Assert.Equal(-i, bTasks[i - 1].Result); + } + } + + [Fact] + public async Task TestGetAll() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key).ForAwait(); + var shouldMatch = new Dictionary(); + var random = new Random(); - [Fact] - public async Task TestSet() // https://redis.io/commands/hset + for (int i = 0; i < 1000; i++) { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var hashkey = Me(); - var del = conn.KeyDeleteAsync(hashkey).ForAwait(); - - var val0 = conn.HashGetAsync(hashkey, "field").ForAwait(); - var set0 = conn.HashSetAsync(hashkey, "field", "value1").ForAwait(); - var val1 = conn.HashGetAsync(hashkey, "field").ForAwait(); - var set1 = conn.HashSetAsync(hashkey, "field", "value2").ForAwait(); - var val2 = conn.HashGetAsync(hashkey, "field").ForAwait(); - - var set2 = conn.HashSetAsync(hashkey, "field-blob", Encoding.UTF8.GetBytes("value3")).ForAwait(); - var val3 = conn.HashGetAsync(hashkey, "field-blob").ForAwait(); - - var set3 = conn.HashSetAsync(hashkey, "empty_type1", "").ForAwait(); - var val4 = conn.HashGetAsync(hashkey, "empty_type1").ForAwait(); - var set4 = conn.HashSetAsync(hashkey, "empty_type2", RedisValue.EmptyString).ForAwait(); - var val5 = conn.HashGetAsync(hashkey, "empty_type2").ForAwait(); - - await del; - Assert.Null((string?)(await val0)); - Assert.True(await set0); - Assert.Equal("value1", await val1); - Assert.False(await set1); - Assert.Equal("value2", await val2); - - Assert.True(await set2); - Assert.Equal("value3", await val3); - - Assert.True(await set3); - Assert.Equal("", await val4); - Assert.True(await set4); - Assert.Equal("", await val5); - } + var guid = Guid.NewGuid(); + var value = random.Next(int.MaxValue); + + shouldMatch[guid] = value; + + _ = db.HashIncrementAsync(key, guid.ToString(), value); } - [Fact] - public async Task TestSetNotExists() // https://redis.io/commands/hsetnx + var inRedis = (await db.HashGetAllAsync(key).ForAwait()).ToDictionary( + x => Guid.Parse(x.Name!), x => int.Parse(x.Value!)); + + Assert.Equal(shouldMatch.Count, inRedis.Count); + + foreach (var k in shouldMatch.Keys) { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var hashkey = Me(); - var del = conn.KeyDeleteAsync(hashkey).ForAwait(); - - var val0 = conn.HashGetAsync(hashkey, "field").ForAwait(); - var set0 = conn.HashSetAsync(hashkey, "field", "value1", When.NotExists).ForAwait(); - var val1 = conn.HashGetAsync(hashkey, "field").ForAwait(); - var set1 = conn.HashSetAsync(hashkey, "field", "value2", When.NotExists).ForAwait(); - var val2 = conn.HashGetAsync(hashkey, "field").ForAwait(); - - var set2 = conn.HashSetAsync(hashkey, "field-blob", Encoding.UTF8.GetBytes("value3"), When.NotExists).ForAwait(); - var val3 = conn.HashGetAsync(hashkey, "field-blob").ForAwait(); - var set3 = conn.HashSetAsync(hashkey, "field-blob", Encoding.UTF8.GetBytes("value3"), When.NotExists).ForAwait(); - - await del; - Assert.Null((string?)(await val0)); - Assert.True(await set0); - Assert.Equal("value1", await val1); - Assert.False(await set1); - Assert.Equal("value1", await val2); - - Assert.True(await set2); - Assert.Equal("value3", await val3); - Assert.False(await set3); - } + Assert.Equal(shouldMatch[k], inRedis[k]); } + } - [Fact] - public async Task TestDelSingle() // https://redis.io/commands/hdel + [Fact] + public async Task TestGet() + { + using var conn = Create(); + + var key = Me(); + var db = conn.GetDatabase(); + var shouldMatch = new Dictionary(); + var random = new Random(); + + for (int i = 1; i < 1000; i++) { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var hashkey = Me(); - await conn.KeyDeleteAsync(hashkey).ForAwait(); - var del0 = conn.HashDeleteAsync(hashkey, "field").ForAwait(); - - await conn.HashSetAsync(hashkey, "field", "value").ForAwait(); - - var del1 = conn.HashDeleteAsync(hashkey, "field").ForAwait(); - var del2 = conn.HashDeleteAsync(hashkey, "field").ForAwait(); - - Assert.False(await del0); - Assert.True(await del1); - Assert.False(await del2); - } + var guid = Guid.NewGuid(); + var value = random.Next(int.MaxValue); + + shouldMatch[guid] = value; + + _ = db.HashIncrementAsync(key, guid.ToString(), value); } - [Fact] - public async Task TestDelMulti() // https://redis.io/commands/hdel + foreach (var k in shouldMatch.Keys) { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var hashkey = Me(); - conn.HashSet(hashkey, "key1", "val1", flags: CommandFlags.FireAndForget); - conn.HashSet(hashkey, "key2", "val2", flags: CommandFlags.FireAndForget); - conn.HashSet(hashkey, "key3", "val3", flags: CommandFlags.FireAndForget); + var inRedis = await db.HashGetAsync(key, k.ToString()).ForAwait(); + var num = int.Parse(inRedis!); - var s1 = conn.HashExistsAsync(hashkey, "key1"); - var s2 = conn.HashExistsAsync(hashkey, "key2"); - var s3 = conn.HashExistsAsync(hashkey, "key3"); + Assert.Equal(shouldMatch[k], num); + } + } - var removed = conn.HashDeleteAsync(hashkey, new RedisValue[] { "key1", "key3" }); + [Fact] + public async Task TestSet() // https://redis.io/commands/hset + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var hashkey = Me(); + var del = db.KeyDeleteAsync(hashkey).ForAwait(); + + var val0 = db.HashGetAsync(hashkey, "field").ForAwait(); + var set0 = db.HashSetAsync(hashkey, "field", "value1").ForAwait(); + var val1 = db.HashGetAsync(hashkey, "field").ForAwait(); + var set1 = db.HashSetAsync(hashkey, "field", "value2").ForAwait(); + var val2 = db.HashGetAsync(hashkey, "field").ForAwait(); + + var set2 = db.HashSetAsync(hashkey, "field-blob", Encoding.UTF8.GetBytes("value3")).ForAwait(); + var val3 = db.HashGetAsync(hashkey, "field-blob").ForAwait(); + + var set3 = db.HashSetAsync(hashkey, "empty_type1", "").ForAwait(); + var val4 = db.HashGetAsync(hashkey, "empty_type1").ForAwait(); + var set4 = db.HashSetAsync(hashkey, "empty_type2", RedisValue.EmptyString).ForAwait(); + var val5 = db.HashGetAsync(hashkey, "empty_type2").ForAwait(); + + await del; + Assert.Null((string?)(await val0)); + Assert.True(await set0); + Assert.Equal("value1", await val1); + Assert.False(await set1); + Assert.Equal("value2", await val2); + + Assert.True(await set2); + Assert.Equal("value3", await val3); + + Assert.True(await set3); + Assert.Equal("", await val4); + Assert.True(await set4); + Assert.Equal("", await val5); + } - var d1 = conn.HashExistsAsync(hashkey, "key1"); - var d2 = conn.HashExistsAsync(hashkey, "key2"); - var d3 = conn.HashExistsAsync(hashkey, "key3"); + [Fact] + public async Task TestSetNotExists() // https://redis.io/commands/hsetnx + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var hashkey = Me(); + var del = db.KeyDeleteAsync(hashkey).ForAwait(); + + var val0 = db.HashGetAsync(hashkey, "field").ForAwait(); + var set0 = db.HashSetAsync(hashkey, "field", "value1", When.NotExists).ForAwait(); + var val1 = db.HashGetAsync(hashkey, "field").ForAwait(); + var set1 = db.HashSetAsync(hashkey, "field", "value2", When.NotExists).ForAwait(); + var val2 = db.HashGetAsync(hashkey, "field").ForAwait(); + + var set2 = db.HashSetAsync(hashkey, "field-blob", Encoding.UTF8.GetBytes("value3"), When.NotExists).ForAwait(); + var val3 = db.HashGetAsync(hashkey, "field-blob").ForAwait(); + var set3 = db.HashSetAsync(hashkey, "field-blob", Encoding.UTF8.GetBytes("value3"), When.NotExists).ForAwait(); + + await del; + Assert.Null((string?)(await val0)); + Assert.True(await set0); + Assert.Equal("value1", await val1); + Assert.False(await set1); + Assert.Equal("value1", await val2); + + Assert.True(await set2); + Assert.Equal("value3", await val3); + Assert.False(await set3); + } - Assert.True(await s1); - Assert.True(await s2); - Assert.True(await s3); + [Fact] + public async Task TestDelSingle() // https://redis.io/commands/hdel + { + using var conn = Create(); - Assert.Equal(2, await removed); + var db = conn.GetDatabase(); + var hashkey = Me(); + await db.KeyDeleteAsync(hashkey).ForAwait(); + var del0 = db.HashDeleteAsync(hashkey, "field").ForAwait(); - Assert.False(await d1); - Assert.True(await d2); - Assert.False(await d3); + await db.HashSetAsync(hashkey, "field", "value").ForAwait(); - var removeFinal = conn.HashDeleteAsync(hashkey, new RedisValue[] { "key2" }); + var del1 = db.HashDeleteAsync(hashkey, "field").ForAwait(); + var del2 = db.HashDeleteAsync(hashkey, "field").ForAwait(); - Assert.Equal(0, await conn.HashLengthAsync(hashkey).ForAwait()); - Assert.Equal(1, await removeFinal); - } - } + Assert.False(await del0); + Assert.True(await del1); + Assert.False(await del2); + } - [Fact] - public async Task TestDelMultiInsideTransaction() // https://redis.io/commands/hdel - { - using (var outer = Create()) - { - var conn = outer.GetDatabase().CreateTransaction(); - { - var hashkey = Me(); - _ = conn.HashSetAsync(hashkey, "key1", "val1"); - _ = conn.HashSetAsync(hashkey, "key2", "val2"); - _ = conn.HashSetAsync(hashkey, "key3", "val3"); - - var s1 = conn.HashExistsAsync(hashkey, "key1"); - var s2 = conn.HashExistsAsync(hashkey, "key2"); - var s3 = conn.HashExistsAsync(hashkey, "key3"); - - var removed = conn.HashDeleteAsync(hashkey, new RedisValue[] { "key1", "key3" }); - - var d1 = conn.HashExistsAsync(hashkey, "key1"); - var d2 = conn.HashExistsAsync(hashkey, "key2"); - var d3 = conn.HashExistsAsync(hashkey, "key3"); - - conn.Execute(); - - Assert.True(await s1); - Assert.True(await s2); - Assert.True(await s3); - - Assert.Equal(2, await removed); - - Assert.False(await d1); - Assert.True(await d2); - Assert.False(await d3); - } - } - } + [Fact] + public async Task TestDelMulti() // https://redis.io/commands/hdel + { + using var conn = Create(); - [Fact] - public async Task TestExists() // https://redis.io/commands/hexists - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var hashkey = Me(); - _ = conn.KeyDeleteAsync(hashkey).ForAwait(); - var ex0 = conn.HashExistsAsync(hashkey, "field").ForAwait(); - _ = conn.HashSetAsync(hashkey, "field", "value").ForAwait(); - var ex1 = conn.HashExistsAsync(hashkey, "field").ForAwait(); - _ = conn.HashDeleteAsync(hashkey, "field").ForAwait(); - _ = conn.HashExistsAsync(hashkey, "field").ForAwait(); - - Assert.False(await ex0); - Assert.True(await ex1); - Assert.False(await ex0); - } - } + var db = conn.GetDatabase(); + var hashkey = Me(); + db.HashSet(hashkey, "key1", "val1", flags: CommandFlags.FireAndForget); + db.HashSet(hashkey, "key2", "val2", flags: CommandFlags.FireAndForget); + db.HashSet(hashkey, "key3", "val3", flags: CommandFlags.FireAndForget); - [Fact] - public async Task TestHashKeys() // https://redis.io/commands/hkeys - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var hashKey = Me(); - await conn.KeyDeleteAsync(hashKey).ForAwait(); + var s1 = db.HashExistsAsync(hashkey, "key1"); + var s2 = db.HashExistsAsync(hashkey, "key2"); + var s3 = db.HashExistsAsync(hashkey, "key3"); - var keys0 = await conn.HashKeysAsync(hashKey).ForAwait(); - Assert.Empty(keys0); + var removed = db.HashDeleteAsync(hashkey, new RedisValue[] { "key1", "key3" }); - await conn.HashSetAsync(hashKey, "foo", "abc").ForAwait(); - await conn.HashSetAsync(hashKey, "bar", "def").ForAwait(); + var d1 = db.HashExistsAsync(hashkey, "key1"); + var d2 = db.HashExistsAsync(hashkey, "key2"); + var d3 = db.HashExistsAsync(hashkey, "key3"); - var keys1 = conn.HashKeysAsync(hashKey); + Assert.True(await s1); + Assert.True(await s2); + Assert.True(await s3); - var arr = await keys1; - Assert.Equal(2, arr.Length); - Assert.Equal("foo", arr[0]); - Assert.Equal("bar", arr[1]); - } - } + Assert.Equal(2, await removed); + + Assert.False(await d1); + Assert.True(await d2); + Assert.False(await d3); + + var removeFinal = db.HashDeleteAsync(hashkey, new RedisValue[] { "key2" }); + + Assert.Equal(0, await db.HashLengthAsync(hashkey).ForAwait()); + Assert.Equal(1, await removeFinal); + } + + [Fact] + public async Task TestDelMultiInsideTransaction() // https://redis.io/commands/hdel + { + using var conn = Create(); - [Fact] - public async Task TestHashValues() // https://redis.io/commands/hvals + var tran = conn.GetDatabase().CreateTransaction(); { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var hashkey = Me(); - await conn.KeyDeleteAsync(hashkey).ForAwait(); + var hashkey = Me(); + _ = tran.HashSetAsync(hashkey, "key1", "val1"); + _ = tran.HashSetAsync(hashkey, "key2", "val2"); + _ = tran.HashSetAsync(hashkey, "key3", "val3"); - var keys0 = await conn.HashValuesAsync(hashkey).ForAwait(); + var s1 = tran.HashExistsAsync(hashkey, "key1"); + var s2 = tran.HashExistsAsync(hashkey, "key2"); + var s3 = tran.HashExistsAsync(hashkey, "key3"); - await conn.HashSetAsync(hashkey, "foo", "abc").ForAwait(); - await conn.HashSetAsync(hashkey, "bar", "def").ForAwait(); + var removed = tran.HashDeleteAsync(hashkey, new RedisValue[] { "key1", "key3" }); - var keys1 = conn.HashValuesAsync(hashkey).ForAwait(); + var d1 = tran.HashExistsAsync(hashkey, "key1"); + var d2 = tran.HashExistsAsync(hashkey, "key2"); + var d3 = tran.HashExistsAsync(hashkey, "key3"); - Assert.Empty(keys0); + tran.Execute(); - var arr = await keys1; - Assert.Equal(2, arr.Length); - Assert.Equal("abc", Encoding.UTF8.GetString(arr[0]!)); - Assert.Equal("def", Encoding.UTF8.GetString(arr[1]!)); - } + Assert.True(await s1); + Assert.True(await s2); + Assert.True(await s3); + + Assert.Equal(2, await removed); + + Assert.False(await d1); + Assert.True(await d2); + Assert.False(await d3); } + } - [Fact] - public async Task TestHashLength() // https://redis.io/commands/hlen - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var hashkey = Me(); - conn.KeyDelete(hashkey, CommandFlags.FireAndForget); + [Fact] + public async Task TestExists() // https://redis.io/commands/hexists + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var hashkey = Me(); + _ = db.KeyDeleteAsync(hashkey).ForAwait(); + var ex0 = db.HashExistsAsync(hashkey, "field").ForAwait(); + _ = db.HashSetAsync(hashkey, "field", "value").ForAwait(); + var ex1 = db.HashExistsAsync(hashkey, "field").ForAwait(); + _ = db.HashDeleteAsync(hashkey, "field").ForAwait(); + _ = db.HashExistsAsync(hashkey, "field").ForAwait(); + + Assert.False(await ex0); + Assert.True(await ex1); + Assert.False(await ex0); + } + + [Fact] + public async Task TestHashKeys() // https://redis.io/commands/hkeys + { + using var conn = Create(); - var len0 = conn.HashLengthAsync(hashkey); + var db = conn.GetDatabase(); + var hashKey = Me(); + await db.KeyDeleteAsync(hashKey).ForAwait(); - conn.HashSet(hashkey, "foo", "abc", flags: CommandFlags.FireAndForget); - conn.HashSet(hashkey, "bar", "def", flags: CommandFlags.FireAndForget); + var keys0 = await db.HashKeysAsync(hashKey).ForAwait(); + Assert.Empty(keys0); - var len1 = conn.HashLengthAsync(hashkey); + await db.HashSetAsync(hashKey, "foo", "abc").ForAwait(); + await db.HashSetAsync(hashKey, "bar", "def").ForAwait(); - Assert.Equal(0, await len0); - Assert.Equal(2, await len1); - } - } + var keys1 = db.HashKeysAsync(hashKey); - [Fact] - public async Task TestGetMulti() // https://redis.io/commands/hmget - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var hashkey = Me(); - conn.KeyDelete(hashkey, CommandFlags.FireAndForget); - - RedisValue[] fields = { "foo", "bar", "blop" }; - var arr0 = await conn.HashGetAsync(hashkey, fields).ForAwait(); - - conn.HashSet(hashkey, "foo", "abc", flags: CommandFlags.FireAndForget); - conn.HashSet(hashkey, "bar", "def", flags: CommandFlags.FireAndForget); - - var arr1 = await conn.HashGetAsync(hashkey, fields).ForAwait(); - var arr2 = await conn.HashGetAsync(hashkey, fields).ForAwait(); - - Assert.Equal(3, arr0.Length); - Assert.Null((string?)arr0[0]); - Assert.Null((string?)arr0[1]); - Assert.Null((string?)arr0[2]); - - Assert.Equal(3, arr1.Length); - Assert.Equal("abc", arr1[0]); - Assert.Equal("def", arr1[1]); - Assert.Null((string?)arr1[2]); - - Assert.Equal(3, arr2.Length); - Assert.Equal("abc", arr2[0]); - Assert.Equal("def", arr2[1]); - Assert.Null((string?)arr2[2]); - } - } + var arr = await keys1; + Assert.Equal(2, arr.Length); + Assert.Equal("foo", arr[0]); + Assert.Equal("bar", arr[1]); + } - [Fact] - public void TestGetPairs() // https://redis.io/commands/hgetall - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var hashkey = Me(); - conn.KeyDeleteAsync(hashkey); + [Fact] + public async Task TestHashValues() // https://redis.io/commands/hvals + { + using var conn = Create(); - var result0 = conn.HashGetAllAsync(hashkey); + var db = conn.GetDatabase(); + var hashkey = Me(); + await db.KeyDeleteAsync(hashkey).ForAwait(); - conn.HashSetAsync(hashkey, "foo", "abc"); - conn.HashSetAsync(hashkey, "bar", "def"); + var keys0 = await db.HashValuesAsync(hashkey).ForAwait(); - var result1 = conn.HashGetAllAsync(hashkey); + await db.HashSetAsync(hashkey, "foo", "abc").ForAwait(); + await db.HashSetAsync(hashkey, "bar", "def").ForAwait(); - Assert.Empty(muxer.Wait(result0)); - var result = muxer.Wait(result1).ToStringDictionary(); - Assert.Equal(2, result.Count); - Assert.Equal("abc", result["foo"]); - Assert.Equal("def", result["bar"]); - } - } + var keys1 = db.HashValuesAsync(hashkey).ForAwait(); + + Assert.Empty(keys0); + + var arr = await keys1; + Assert.Equal(2, arr.Length); + Assert.Equal("abc", Encoding.UTF8.GetString(arr[0]!)); + Assert.Equal("def", Encoding.UTF8.GetString(arr[1]!)); + } + + [Fact] + public async Task TestHashLength() // https://redis.io/commands/hlen + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var hashkey = Me(); + db.KeyDelete(hashkey, CommandFlags.FireAndForget); + + var len0 = db.HashLengthAsync(hashkey); + + db.HashSet(hashkey, "foo", "abc", flags: CommandFlags.FireAndForget); + db.HashSet(hashkey, "bar", "def", flags: CommandFlags.FireAndForget); + + var len1 = db.HashLengthAsync(hashkey); + + Assert.Equal(0, await len0); + Assert.Equal(2, await len1); + } + + [Fact] + public async Task TestGetMulti() // https://redis.io/commands/hmget + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var hashkey = Me(); + db.KeyDelete(hashkey, CommandFlags.FireAndForget); + + RedisValue[] fields = { "foo", "bar", "blop" }; + var arr0 = await db.HashGetAsync(hashkey, fields).ForAwait(); + + db.HashSet(hashkey, "foo", "abc", flags: CommandFlags.FireAndForget); + db.HashSet(hashkey, "bar", "def", flags: CommandFlags.FireAndForget); + + var arr1 = await db.HashGetAsync(hashkey, fields).ForAwait(); + var arr2 = await db.HashGetAsync(hashkey, fields).ForAwait(); + + Assert.Equal(3, arr0.Length); + Assert.Null((string?)arr0[0]); + Assert.Null((string?)arr0[1]); + Assert.Null((string?)arr0[2]); + + Assert.Equal(3, arr1.Length); + Assert.Equal("abc", arr1[0]); + Assert.Equal("def", arr1[1]); + Assert.Null((string?)arr1[2]); + + Assert.Equal(3, arr2.Length); + Assert.Equal("abc", arr2[0]); + Assert.Equal("def", arr2[1]); + Assert.Null((string?)arr2[2]); + } + + [Fact] + public void TestGetPairs() // https://redis.io/commands/hgetall + { + using var conn = Create(); - [Fact] - public void TestSetPairs() // https://redis.io/commands/hmset + var db = conn.GetDatabase(); + var hashkey = Me(); + db.KeyDeleteAsync(hashkey); + + var result0 = db.HashGetAllAsync(hashkey); + + db.HashSetAsync(hashkey, "foo", "abc"); + db.HashSetAsync(hashkey, "bar", "def"); + + var result1 = db.HashGetAllAsync(hashkey); + + Assert.Empty(conn.Wait(result0)); + var result = conn.Wait(result1).ToStringDictionary(); + Assert.Equal(2, result.Count); + Assert.Equal("abc", result["foo"]); + Assert.Equal("def", result["bar"]); + } + + [Fact] + public void TestSetPairs() // https://redis.io/commands/hmset + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var hashkey = Me(); + db.KeyDeleteAsync(hashkey).ForAwait(); + + var result0 = db.HashGetAllAsync(hashkey); + + var data = new[] { + new HashEntry("foo", Encoding.UTF8.GetBytes("abc")), + new HashEntry("bar", Encoding.UTF8.GetBytes("def")) + }; + db.HashSetAsync(hashkey, data).ForAwait(); + + var result1 = db.Wait(db.HashGetAllAsync(hashkey)); + + Assert.Empty(result0.Result); + var result = result1.ToStringDictionary(); + Assert.Equal(2, result.Count); + Assert.Equal("abc", result["foo"]); + Assert.Equal("def", result["bar"]); + } + + [Fact] + public async Task TestWhenAlwaysAsync() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var hashkey = Me(); + db.KeyDelete(hashkey, CommandFlags.FireAndForget); + + var result1 = await db.HashSetAsync(hashkey, "foo", "bar", When.Always, CommandFlags.None); + var result2 = await db.HashSetAsync(hashkey, "foo2", "bar", When.Always, CommandFlags.None); + var result3 = await db.HashSetAsync(hashkey, "foo", "bar", When.Always, CommandFlags.None); + var result4 = await db.HashSetAsync(hashkey, "foo", "bar2", When.Always, CommandFlags.None); + + Assert.True(result1, "Initial set key 1"); + Assert.True(result2, "Initial set key 2"); + // Fields modified *but not added* should be a zero/false. That's the behavior of HSET + Assert.False(result3, "Duplicate set key 1"); + Assert.False(result4, "Duplicate se key 1 variant"); + } + + [Fact] + public async Task HashRandomFieldAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var hashKey = Me(); + var items = new HashEntry[] { new("new york", "yankees"), new("baltimore", "orioles"), new("boston", "red sox"), new("Tampa Bay", "rays"), new("Toronto", "blue jays") }; + await db.HashSetAsync(hashKey, items); + + var singleField = await db.HashRandomFieldAsync(hashKey); + var multiFields = await db.HashRandomFieldsAsync(hashKey, 3); + var withValues = await db.HashRandomFieldsWithValuesAsync(hashKey, 3); + Assert.Equal(3, multiFields.Length); + Assert.Equal(3, withValues.Length); + Assert.Contains(items, x => x.Name == singleField); + + foreach (var field in multiFields) { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var hashkey = Me(); - conn.KeyDeleteAsync(hashkey).ForAwait(); - - var result0 = conn.HashGetAllAsync(hashkey); - - var data = new [] { - new HashEntry("foo", Encoding.UTF8.GetBytes("abc")), - new HashEntry("bar", Encoding.UTF8.GetBytes("def")) - }; - conn.HashSetAsync(hashkey, data).ForAwait(); - - var result1 = conn.Wait(conn.HashGetAllAsync(hashkey)); - - Assert.Empty(result0.Result); - var result = result1.ToStringDictionary(); - Assert.Equal(2, result.Count); - Assert.Equal("abc", result["foo"]); - Assert.Equal("def", result["bar"]); - } + Assert.Contains(items, x => x.Name == field); } - [Fact] - public async Task TestWhenAlwaysAsync() + foreach (var field in withValues) { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var hashkey = Me(); - conn.KeyDelete(hashkey, CommandFlags.FireAndForget); - - var result1 = await conn.HashSetAsync(hashkey, "foo", "bar", When.Always, CommandFlags.None); - var result2 = await conn.HashSetAsync(hashkey, "foo2", "bar", When.Always, CommandFlags.None); - var result3 = await conn.HashSetAsync(hashkey, "foo", "bar", When.Always, CommandFlags.None); - var result4 = await conn.HashSetAsync(hashkey, "foo", "bar2", When.Always, CommandFlags.None); - - Assert.True(result1, "Initial set key 1"); - Assert.True(result2, "Initial set key 2"); - // Fields modified *but not added* should be a zero/false. That's the behavior of HSET - Assert.False(result3, "Duplicate set key 1"); - Assert.False(result4, "Duplicate se key 1 variant"); - } + Assert.Contains(items, x => x.Name == field.Name); } + } - [Fact] - public async Task HashRandomFieldAsync() + [Fact] + public void HashRandomField() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var hashKey = Me(); + var items = new HashEntry[] { new("new york", "yankees"), new("baltimore", "orioles"), new("boston", "red sox"), new("Tampa Bay", "rays"), new("Toronto", "blue jays") }; + db.HashSet(hashKey, items); + + var singleField = db.HashRandomField(hashKey); + var multiFields = db.HashRandomFields(hashKey, 3); + var withValues = db.HashRandomFieldsWithValues(hashKey, 3); + Assert.Equal(3, multiFields.Length); + Assert.Equal(3, withValues.Length); + Assert.Contains(items, x => x.Name == singleField); + + foreach (var field in multiFields) { - using var muxer = Create(); - Skip.IfBelow(muxer, RedisFeatures.v6_2_0); - - var db = muxer.GetDatabase(); - var hashKey = Me(); - var items = new HashEntry[] { new("new york", "yankees"), new("baltimore", "orioles"), new("boston", "red sox"), new("Tampa Bay", "rays"), new("Toronto", "blue jays") }; - await db.HashSetAsync(hashKey, items); - - var singleField = await db.HashRandomFieldAsync(hashKey); - var multiFields = await db.HashRandomFieldsAsync(hashKey, 3); - var withValues = await db.HashRandomFieldsWithValuesAsync(hashKey, 3); - Assert.Equal(3, multiFields.Length); - Assert.Equal(3, withValues.Length); - Assert.Contains(items, x => x.Name == singleField); - - foreach (var field in multiFields) - { - Assert.Contains(items, x => x.Name == field); - } - - foreach (var field in withValues) - { - Assert.Contains(items, x => x.Name == field.Name); - } + Assert.Contains(items, x => x.Name == field); } - [Fact] - public void HashRandomField() + foreach (var field in withValues) { - using var muxer = Create(); - Skip.IfBelow(muxer, RedisFeatures.v6_2_0); - - var db = muxer.GetDatabase(); - var hashKey = Me(); - var items = new HashEntry[] { new("new york", "yankees"), new("baltimore", "orioles"), new("boston", "red sox"), new("Tampa Bay", "rays"), new("Toronto", "blue jays") }; - db.HashSet(hashKey, items); - - var singleField = db.HashRandomField(hashKey); - var multiFields = db.HashRandomFields(hashKey, 3); - var withValues = db.HashRandomFieldsWithValues(hashKey, 3); - Assert.Equal(3, multiFields.Length); - Assert.Equal(3, withValues.Length); - Assert.Contains(items, x => x.Name == singleField); - - foreach (var field in multiFields) - { - Assert.Contains(items, x => x.Name == field); - } - - foreach (var field in withValues) - { - Assert.Contains(items, x => x.Name == field.Name); - } + Assert.Contains(items, x => x.Name == field.Name); } + } - [Fact] - public void HashRandomFieldEmptyHash() - { - using var muxer = Create(); - Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + [Fact] + public void HashRandomFieldEmptyHash() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - var db = muxer.GetDatabase(); - var hashKey = Me(); + var db = conn.GetDatabase(); + var hashKey = Me(); - var singleField = db.HashRandomField(hashKey); - var multiFields = db.HashRandomFields(hashKey, 3); - var withValues = db.HashRandomFieldsWithValues(hashKey, 3); + var singleField = db.HashRandomField(hashKey); + var multiFields = db.HashRandomFields(hashKey, 3); + var withValues = db.HashRandomFieldsWithValues(hashKey, 3); - Assert.Equal(RedisValue.Null, singleField); - Assert.Empty(multiFields); - Assert.Empty(withValues); - } + Assert.Equal(RedisValue.Null, singleField); + Assert.Empty(multiFields); + Assert.Empty(withValues); } } diff --git a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs index a2081644c..aa285c15a 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs @@ -6,184 +6,181 @@ using Xunit.Abstractions; using Xunit.Sdk; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +/// +/// Override for that truncates our DisplayName down. +/// +/// Attribute that is applied to a method to indicate that it is a fact that should +/// be run by the test runner. It can also be extended to support a customized definition +/// of a test method. +/// +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +[XunitTestCaseDiscoverer("StackExchange.Redis.Tests.FactDiscoverer", "StackExchange.Redis.Tests")] +public class FactAttribute : Xunit.FactAttribute { } + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public class FactLongRunningAttribute : FactAttribute { - /// - /// Override for that truncates our DisplayName down. - /// - /// Attribute that is applied to a method to indicate that it is a fact that should - /// be run by the test runner. It can also be extended to support a customized definition - /// of a test method. - /// - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - [XunitTestCaseDiscoverer("StackExchange.Redis.Tests.FactDiscoverer", "StackExchange.Redis.Tests")] - public class FactAttribute : Xunit.FactAttribute + public override string Skip { + get => TestConfig.Current.RunLongRunning ? base.Skip : "Config.RunLongRunning is false - skipping long test."; + set => base.Skip = value; } +} - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class FactLongRunningAttribute : FactAttribute +/// +/// Override for that truncates our DisplayName down. +/// +/// Marks a test method as being a data theory. Data theories are tests which are +/// fed various bits of data from a data source, mapping to parameters on the test +/// method. If the data source contains multiple rows, then the test method is executed +/// multiple times (once with each data row). Data is provided by attributes which +/// derive from Xunit.Sdk.DataAttribute (notably, Xunit.InlineDataAttribute and Xunit.MemberDataAttribute). +/// +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +[XunitTestCaseDiscoverer("StackExchange.Redis.Tests.TheoryDiscoverer", "StackExchange.Redis.Tests")] +public class TheoryAttribute : Xunit.TheoryAttribute { } + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public class TheoryLongRunningAttribute : Xunit.TheoryAttribute +{ + public override string Skip { - public override string Skip - { - get => TestConfig.Current.RunLongRunning ? base.Skip : "Config.RunLongRunning is false - skipping long test."; - set => base.Skip = value; - } + get => TestConfig.Current.RunLongRunning ? base.Skip : "Config.RunLongRunning is false - skipping long test."; + set => base.Skip = value; } +} - /// - /// Override for that truncates our DisplayName down. - /// - /// Marks a test method as being a data theory. Data theories are tests which are - /// fed various bits of data from a data source, mapping to parameters on the test - /// method. If the data source contains multiple rows, then the test method is executed - /// multiple times (once with each data row). Data is provided by attributes which - /// derive from Xunit.Sdk.DataAttribute (notably, Xunit.InlineDataAttribute and Xunit.MemberDataAttribute). - /// - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - [XunitTestCaseDiscoverer("StackExchange.Redis.Tests.TheoryDiscoverer", "StackExchange.Redis.Tests")] - public class TheoryAttribute : Xunit.TheoryAttribute { } - - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class TheoryLongRunningAttribute : Xunit.TheoryAttribute - { - public override string Skip - { - get => TestConfig.Current.RunLongRunning ? base.Skip : "Config.RunLongRunning is false - skipping long test."; - set => base.Skip = value; - } - } +public class FactDiscoverer : Xunit.Sdk.FactDiscoverer +{ + public FactDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) { } - public class FactDiscoverer : Xunit.Sdk.FactDiscoverer - { - public FactDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) { } + protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + => new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod); +} - protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) - => new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod); - } +public class TheoryDiscoverer : Xunit.Sdk.TheoryDiscoverer +{ + public TheoryDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) { } - public class TheoryDiscoverer : Xunit.Sdk.TheoryDiscoverer - { - public TheoryDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) { } + protected override IEnumerable CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow) + => new[] { new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, dataRow) }; + + protected override IEnumerable CreateTestCasesForSkip(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, string skipReason) + => new[] { new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod) }; - protected override IEnumerable CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow) - => new[] { new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, dataRow) }; + protected override IEnumerable CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) + => new[] { new SkippableTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod) }; - protected override IEnumerable CreateTestCasesForSkip(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, string skipReason) - => new[] { new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod) }; + protected override IEnumerable CreateTestCasesForSkippedDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow, string skipReason) + => new[] { new NamedSkippedDataRowTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, skipReason, dataRow) }; +} + +public class SkippableTestCase : XunitTestCase +{ + protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => + base.GetDisplayName(factAttribute, displayName).StripName(); - protected override IEnumerable CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) - => new[] { new SkippableTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod) }; + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public SkippableTestCase() { } - protected override IEnumerable CreateTestCasesForSkippedDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow, string skipReason) - => new[] { new NamedSkippedDataRowTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, skipReason, dataRow) }; + public SkippableTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[]? testMethodArguments = null) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) + { } - public class SkippableTestCase : XunitTestCase + public override async Task RunAsync( + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) { - protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => - base.GetDisplayName(factAttribute, displayName).StripName(); + var skipMessageBus = new SkippableMessageBus(messageBus); + var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource).ForAwait(); + return result.Update(skipMessageBus); + } +} - [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] - public SkippableTestCase() { } +public class SkippableTheoryTestCase : XunitTheoryTestCase +{ + protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => + base.GetDisplayName(factAttribute, displayName).StripName(); - public SkippableTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[]? testMethodArguments = null) - : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) - { - } + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public SkippableTheoryTestCase() { } - public override async Task RunAsync( - IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - object[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) - { - var skipMessageBus = new SkippableMessageBus(messageBus); - var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource).ForAwait(); - return result.Update(skipMessageBus); - } - } + public SkippableTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) { } - public class SkippableTheoryTestCase : XunitTheoryTestCase + public override async Task RunAsync( + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) { - protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => - base.GetDisplayName(factAttribute, displayName).StripName(); + var skipMessageBus = new SkippableMessageBus(messageBus); + var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource).ForAwait(); + return result.Update(skipMessageBus); + } +} - [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] - public SkippableTheoryTestCase() { } +public class NamedSkippedDataRowTestCase : XunitSkippedDataRowTestCase +{ + protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => + base.GetDisplayName(factAttribute, displayName).StripName(); - public SkippableTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod) - : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) { } + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public NamedSkippedDataRowTestCase() { } - public override async Task RunAsync( - IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - object[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) - { - var skipMessageBus = new SkippableMessageBus(messageBus); - var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource).ForAwait(); - return result.Update(skipMessageBus); - } - } + public NamedSkippedDataRowTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, string skipReason, object[]? testMethodArguments = null) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, skipReason, testMethodArguments) { } +} - public class NamedSkippedDataRowTestCase : XunitSkippedDataRowTestCase - { - protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => - base.GetDisplayName(factAttribute, displayName).StripName(); +public class SkippableMessageBus : IMessageBus +{ + private readonly IMessageBus InnerBus; + public SkippableMessageBus(IMessageBus innerBus) => InnerBus = innerBus; - [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] - public NamedSkippedDataRowTestCase() { } + public int DynamicallySkippedTestCount { get; private set; } - public NamedSkippedDataRowTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, string skipReason, object[]? testMethodArguments = null) - : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, skipReason, testMethodArguments) { } + public void Dispose() + { + InnerBus.Dispose(); + GC.SuppressFinalize(this); } - public class SkippableMessageBus : IMessageBus + public bool QueueMessage(IMessageSinkMessage message) { - private readonly IMessageBus InnerBus; - public SkippableMessageBus(IMessageBus innerBus) => InnerBus = innerBus; - - public int DynamicallySkippedTestCount { get; private set; } - - public void Dispose() - { - InnerBus.Dispose(); - GC.SuppressFinalize(this); - } - - public bool QueueMessage(IMessageSinkMessage message) + if (message is ITestFailed testFailed) { - if (message is ITestFailed testFailed) + var exceptionType = testFailed.ExceptionTypes.FirstOrDefault(); + if (exceptionType == typeof(SkipTestException).FullName) { - var exceptionType = testFailed.ExceptionTypes.FirstOrDefault(); - if (exceptionType == typeof(SkipTestException).FullName) - { - DynamicallySkippedTestCount++; - return InnerBus.QueueMessage(new TestSkipped(testFailed.Test, testFailed.Messages.FirstOrDefault())); - } + DynamicallySkippedTestCount++; + return InnerBus.QueueMessage(new TestSkipped(testFailed.Test, testFailed.Messages.FirstOrDefault())); } - return InnerBus.QueueMessage(message); } + return InnerBus.QueueMessage(message); } +} - internal static class XUnitExtensions - { - internal static string StripName(this string name) => - name.Replace("StackExchange.Redis.Tests.", ""); +internal static class XUnitExtensions +{ + internal static string StripName(this string name) => + name.Replace("StackExchange.Redis.Tests.", ""); - public static RunSummary Update(this RunSummary summary, SkippableMessageBus bus) + public static RunSummary Update(this RunSummary summary, SkippableMessageBus bus) + { + if (bus.DynamicallySkippedTestCount > 0) { - if (bus.DynamicallySkippedTestCount > 0) - { - summary.Failed -= bus.DynamicallySkippedTestCount; - summary.Skipped += bus.DynamicallySkippedTestCount; - } - return summary; + summary.Failed -= bus.DynamicallySkippedTestCount; + summary.Skipped += bus.DynamicallySkippedTestCount; } + return summary; } } diff --git a/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs b/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs index ffde26249..25fd219ad 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs @@ -2,29 +2,28 @@ using System.Runtime.InteropServices; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Helpers +namespace StackExchange.Redis.Tests.Helpers; + +public static class Extensions { - public static class Extensions - { - private static string VersionInfo { get; } + private static string VersionInfo { get; } - static Extensions() - { + static Extensions() + { #if NET462 - VersionInfo = "Compiled under .NET 4.6.2"; + VersionInfo = "Compiled under .NET 4.6.2"; #else - VersionInfo = $"Running under {RuntimeInformation.FrameworkDescription} ({Environment.Version})"; + VersionInfo = $"Running under {RuntimeInformation.FrameworkDescription} ({Environment.Version})"; #endif - try - { - VersionInfo += "\n Running on: " + RuntimeInformation.OSDescription; - } - catch (Exception) - { - VersionInfo += "\n Failed to get OS version"; - } + try + { + VersionInfo += "\n Running on: " + RuntimeInformation.OSDescription; + } + catch (Exception) + { + VersionInfo += "\n Failed to get OS version"; } - - public static void WriteFrameworkVersion(this ITestOutputHelper output) => output.WriteLine(VersionInfo); } + + public static void WriteFrameworkVersion(this ITestOutputHelper output) => output.WriteLine(VersionInfo); } diff --git a/tests/StackExchange.Redis.Tests/Helpers/NonParallelCollection.cs b/tests/StackExchange.Redis.Tests/Helpers/NonParallelCollection.cs index 3932a34e1..ef623c337 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/NonParallelCollection.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/NonParallelCollection.cs @@ -1,10 +1,9 @@ using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[CollectionDefinition(Name, DisableParallelization = true)] +public static class NonParallelCollection { - [CollectionDefinition(Name, DisableParallelization = true)] - public static class NonParallelCollection - { - public const string Name = "NonParallel"; - } + public const string Name = "NonParallel"; } diff --git a/tests/StackExchange.Redis.Tests/Helpers/Skip.cs b/tests/StackExchange.Redis.Tests/Helpers/Skip.cs index b1cdffa30..8627dbda2 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Skip.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Skip.cs @@ -1,64 +1,30 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -namespace StackExchange.Redis.Tests -{ - public static class Skip - { - public static void Inconclusive(string message) => throw new SkipTestException(message); - - public static void IfNoConfig(string prop, [NotNull] string? value) - { - if (value.IsNullOrEmpty()) - { - throw new SkipTestException($"Config.{prop} is not set, skipping test."); - } - } - - public static void IfNoConfig(string prop, [NotNull] List? values) - { - if (values == null || values.Count == 0) - { - throw new SkipTestException($"Config.{prop} is not set, skipping test."); - } - } - - public static void IfBelow(IConnectionMultiplexer conn, Version minVersion) - { - var serverVersion = conn.GetServer(conn.GetEndPoints()[0]).Version; - if (minVersion > serverVersion) - { - throw new SkipTestException($"Requires server version {minVersion}, but server is only {serverVersion}.") - { - MissingFeatures = $"Server version >= {minVersion}." - }; - } - } +namespace StackExchange.Redis.Tests; - public static void IfMissingFeature(IConnectionMultiplexer conn, string feature, Func check) - { - var features = conn.GetServer(conn.GetEndPoints()[0]).Features; - if (!check(features)) - { - throw new SkipTestException($"'{feature}' is not supported on this server.") - { - MissingFeatures = feature - }; - } - } +public static class Skip +{ + public static void Inconclusive(string message) => throw new SkipTestException(message); - internal static void IfMissingDatabase(IConnectionMultiplexer conn, int dbId) + public static void IfNoConfig(string prop, [NotNull] string? value) + { + if (value.IsNullOrEmpty()) { - var dbCount = conn.GetServer(conn.GetEndPoints()[0]).DatabaseCount; - if (dbId >= dbCount) throw new SkipTestException($"Database '{dbId}' is not supported on this server."); + throw new SkipTestException($"Config.{prop} is not set, skipping test."); } } - public class SkipTestException : Exception + internal static void IfMissingDatabase(IConnectionMultiplexer conn, int dbId) { - public string? MissingFeatures { get; set; } - - public SkipTestException(string reason) : base(reason) { } + var dbCount = conn.GetServer(conn.GetEndPoints()[0]).DatabaseCount; + if (dbId >= dbCount) throw new SkipTestException($"Database '{dbId}' is not supported on this server."); } } + +public class SkipTestException : Exception +{ + public string? MissingFeatures { get; set; } + + public SkipTestException(string reason) : base(reason) { } +} diff --git a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs index a6c94611f..f7d5c3c63 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs @@ -4,109 +4,108 @@ using System.Threading; using System.Linq; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public static class TestConfig { - public static class TestConfig - { - private const string FileName = "TestConfig.json"; + private const string FileName = "TestConfig.json"; - public static Config Current { get; } + public static Config Current { get; } - private static int _db = 17; - public static int GetDedicatedDB(IConnectionMultiplexer? conn = null) - { - int db = Interlocked.Increment(ref _db); - if (conn != null) Skip.IfMissingDatabase(conn, db); - return db; - } + private static int _db = 17; + public static int GetDedicatedDB(IConnectionMultiplexer? conn = null) + { + int db = Interlocked.Increment(ref _db); + if (conn != null) Skip.IfMissingDatabase(conn, db); + return db; + } - static TestConfig() + static TestConfig() + { + Current = new Config(); + try { - Current = new Config(); - try + using (var stream = typeof(TestConfig).Assembly.GetManifestResourceStream("StackExchange.Redis.Tests." + FileName)) { - using (var stream = typeof(TestConfig).Assembly.GetManifestResourceStream("StackExchange.Redis.Tests." + FileName)) + if (stream != null) { - if (stream != null) + using (var reader = new StreamReader(stream)) { - using (var reader = new StreamReader(stream)) - { - Current = JsonConvert.DeserializeObject(reader.ReadToEnd()) ?? new Config(); - } + Current = JsonConvert.DeserializeObject(reader.ReadToEnd()) ?? new Config(); } } } - catch (Exception ex) - { - Console.WriteLine("Error Deserializing TestConfig.json: " + ex); - } } - - public class Config + catch (Exception ex) { - public bool UseSharedConnection { get; set; } = true; - public bool RunLongRunning { get; set; } - public bool LogToConsole { get; set; } + Console.WriteLine("Error Deserializing TestConfig.json: " + ex); + } + } - public string PrimaryServer { get; set; } = "127.0.0.1"; - public int PrimaryPort { get; set; } = 6379; - public string PrimaryServerAndPort => PrimaryServer + ":" + PrimaryPort.ToString(); + public class Config + { + public bool UseSharedConnection { get; set; } = true; + public bool RunLongRunning { get; set; } + public bool LogToConsole { get; set; } - public string ReplicaServer { get; set; } = "127.0.0.1"; - public int ReplicaPort { get; set; } = 6380; - public string ReplicaServerAndPort => ReplicaServer + ":" + ReplicaPort.ToString(); + public string PrimaryServer { get; set; } = "127.0.0.1"; + public int PrimaryPort { get; set; } = 6379; + public string PrimaryServerAndPort => PrimaryServer + ":" + PrimaryPort.ToString(); - public string SecureServer { get; set; } = "127.0.0.1"; - public int SecurePort { get; set; } = 6381; - public string SecurePassword { get; set; } = "changeme"; - public string SecureServerAndPort => SecureServer + ":" + SecurePort.ToString(); + public string ReplicaServer { get; set; } = "127.0.0.1"; + public int ReplicaPort { get; set; } = 6380; + public string ReplicaServerAndPort => ReplicaServer + ":" + ReplicaPort.ToString(); - // Separate servers for failover tests, so they don't wreak havoc on all others - public string FailoverPrimaryServer { get; set; } = "127.0.0.1"; - public int FailoverPrimaryPort { get; set; } = 6382; - public string FailoverPrimaryServerAndPort => FailoverPrimaryServer + ":" + FailoverPrimaryPort.ToString(); + public string SecureServer { get; set; } = "127.0.0.1"; + public int SecurePort { get; set; } = 6381; + public string SecurePassword { get; set; } = "changeme"; + public string SecureServerAndPort => SecureServer + ":" + SecurePort.ToString(); - public string FailoverReplicaServer { get; set; } = "127.0.0.1"; - public int FailoverReplicaPort { get; set; } = 6383; - public string FailoverReplicaServerAndPort => FailoverReplicaServer + ":" + FailoverReplicaPort.ToString(); + // Separate servers for failover tests, so they don't wreak havoc on all others + public string FailoverPrimaryServer { get; set; } = "127.0.0.1"; + public int FailoverPrimaryPort { get; set; } = 6382; + public string FailoverPrimaryServerAndPort => FailoverPrimaryServer + ":" + FailoverPrimaryPort.ToString(); - public string IPv4Server { get; set; } = "127.0.0.1"; - public int IPv4Port { get; set; } = 6379; - public string IPv6Server { get; set; } = "::1"; - public int IPv6Port { get; set; } = 6379; + public string FailoverReplicaServer { get; set; } = "127.0.0.1"; + public int FailoverReplicaPort { get; set; } = 6383; + public string FailoverReplicaServerAndPort => FailoverReplicaServer + ":" + FailoverReplicaPort.ToString(); - public string RemoteServer { get; set; } = "127.0.0.1"; - public int RemotePort { get; set; } = 6379; - public string RemoteServerAndPort => RemoteServer + ":" + RemotePort.ToString(); + public string IPv4Server { get; set; } = "127.0.0.1"; + public int IPv4Port { get; set; } = 6379; + public string IPv6Server { get; set; } = "::1"; + public int IPv6Port { get; set; } = 6379; - public string SentinelServer { get; set; } = "127.0.0.1"; - public int SentinelPortA { get; set; } = 26379; - public int SentinelPortB { get; set; } = 26380; - public int SentinelPortC { get; set; } = 26381; - public string SentinelSeviceName { get; set; } = "myprimary"; + public string RemoteServer { get; set; } = "127.0.0.1"; + public int RemotePort { get; set; } = 6379; + public string RemoteServerAndPort => RemoteServer + ":" + RemotePort.ToString(); - public string ClusterServer { get; set; } = "127.0.0.1"; - public int ClusterStartPort { get; set; } = 7000; - public int ClusterServerCount { get; set; } = 6; - public string ClusterServersAndPorts => string.Join(",", Enumerable.Range(ClusterStartPort, ClusterServerCount).Select(port => ClusterServer + ":" + port)); + public string SentinelServer { get; set; } = "127.0.0.1"; + public int SentinelPortA { get; set; } = 26379; + public int SentinelPortB { get; set; } = 26380; + public int SentinelPortC { get; set; } = 26381; + public string SentinelSeviceName { get; set; } = "myprimary"; - public string? SslServer { get; set; } - public int SslPort { get; set; } + public string ClusterServer { get; set; } = "127.0.0.1"; + public int ClusterStartPort { get; set; } = 7000; + public int ClusterServerCount { get; set; } = 6; + public string ClusterServersAndPorts => string.Join(",", Enumerable.Range(ClusterStartPort, ClusterServerCount).Select(port => ClusterServer + ":" + port)); - public string? RedisLabsSslServer { get; set; } - public int RedisLabsSslPort { get; set; } = 6379; - public string? RedisLabsPfxPath { get; set; } + public string? SslServer { get; set; } + public int SslPort { get; set; } - public string? AzureCacheServer { get; set; } - public string? AzureCachePassword { get; set; } + public string? RedisLabsSslServer { get; set; } + public int RedisLabsSslPort { get; set; } = 6379; + public string? RedisLabsPfxPath { get; set; } - public string? SSDBServer { get; set; } - public int SSDBPort { get; set; } = 8888; + public string? AzureCacheServer { get; set; } + public string? AzureCachePassword { get; set; } - public string ProxyServer { get; set; } = "127.0.0.1"; - public int ProxyPort { get; set; } = 7015; + public string? SSDBServer { get; set; } + public int SSDBPort { get; set; } = 8888; - public string ProxyServerAndPort => ProxyServer + ":" + ProxyPort.ToString(); - } + public string ProxyServer { get; set; } = "127.0.0.1"; + public int ProxyPort { get; set; } = 7015; + + public string ProxyServerAndPort => ProxyServer + ":" + ProxyPort.ToString(); } } diff --git a/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs b/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs index 6ccbb478b..5a9d267d3 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs @@ -3,104 +3,103 @@ using System.Text; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Helpers +namespace StackExchange.Redis.Tests.Helpers; + +public class TextWriterOutputHelper : TextWriter { - public class TextWriterOutputHelper : TextWriter + private StringBuilder Buffer { get; } = new StringBuilder(2048); + private StringBuilder? Echo { get; set; } + public override Encoding Encoding => Encoding.UTF8; + private readonly ITestOutputHelper Output; + private readonly bool ToConsole; + public TextWriterOutputHelper(ITestOutputHelper outputHelper, bool echoToConsole) { - private StringBuilder Buffer { get; } = new StringBuilder(2048); - private StringBuilder? Echo { get; set; } - public override Encoding Encoding => Encoding.UTF8; - private readonly ITestOutputHelper Output; - private readonly bool ToConsole; - public TextWriterOutputHelper(ITestOutputHelper outputHelper, bool echoToConsole) - { - Output = outputHelper; - ToConsole = echoToConsole; - } + Output = outputHelper; + ToConsole = echoToConsole; + } - public void EchoTo(StringBuilder sb) => Echo = sb; + public void EchoTo(StringBuilder sb) => Echo = sb; - public void WriteLineNoTime(string? value) + public void WriteLineNoTime(string? value) + { + try { - try - { - base.WriteLine(value); - } - catch (Exception ex) - { - Console.Write("Attempted to write: "); - Console.WriteLine(value); - Console.WriteLine(ex); - } + base.WriteLine(value); } - - public override void WriteLine(string? value) + catch (Exception ex) { - if (value is null) - { - return; - } + Console.Write("Attempted to write: "); + Console.WriteLine(value); + Console.WriteLine(ex); + } + } - try - { - // Prevent double timestamps - if (value.Length < "HH:mm:ss.ffff:".Length || value["HH:mm:ss.ffff:".Length - 1] != ':') - { - base.Write(TestBase.Time()); - base.Write(": "); - } - base.WriteLine(value); - } - catch (Exception ex) - { - Console.Write("Attempted to write: "); - Console.WriteLine(value); - Console.WriteLine(ex); - } + public override void WriteLine(string? value) + { + if (value is null) + { + return; } - public override void Write(char value) + try { - if (value == '\n' || value == '\r') + // Prevent double timestamps + if (value.Length < "HH:mm:ss.ffff:".Length || value["HH:mm:ss.ffff:".Length - 1] != ':') { - // Ignore empty lines - if (Buffer.Length > 0) - { - FlushBuffer(); - } - } - else - { - Buffer.Append(value); + base.Write(TestBase.Time()); + base.Write(": "); } + base.WriteLine(value); } + catch (Exception ex) + { + Console.Write("Attempted to write: "); + Console.WriteLine(value); + Console.WriteLine(ex); + } + } - protected override void Dispose(bool disposing) + public override void Write(char value) + { + if (value == '\n' || value == '\r') { + // Ignore empty lines if (Buffer.Length > 0) { FlushBuffer(); } - base.Dispose(disposing); } + else + { + Buffer.Append(value); + } + } - private void FlushBuffer() + protected override void Dispose(bool disposing) + { + if (Buffer.Length > 0) { - var text = Buffer.ToString(); - try - { - Output.WriteLine(text); - } - catch (InvalidOperationException) - { - // Thrown when writing from a handler after a test has ended - just bail in this case - } - Echo?.AppendLine(text); - if (ToConsole) - { - Console.WriteLine(text); - } - Buffer.Clear(); + FlushBuffer(); + } + base.Dispose(disposing); + } + + private void FlushBuffer() + { + var text = Buffer.ToString(); + try + { + Output.WriteLine(text); + } + catch (InvalidOperationException) + { + // Thrown when writing from a handler after a test has ended - just bail in this case + } + Echo?.AppendLine(text); + if (ToConsole) + { + Console.WriteLine(text); } + Buffer.Clear(); } } diff --git a/tests/StackExchange.Redis.Tests/HyperLogLog.cs b/tests/StackExchange.Redis.Tests/HyperLogLog.cs index ed883bf2d..27d39feec 100644 --- a/tests/StackExchange.Redis.Tests/HyperLogLog.cs +++ b/tests/StackExchange.Redis.Tests/HyperLogLog.cs @@ -1,43 +1,40 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class HyperLogLog : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class HyperLogLog : TestBase + public HyperLogLog(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + + [Fact] + public void SingleKeyLength() { - public HyperLogLog(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } - - [Fact] - public void SingleKeyLength() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = "hll1"; - - db.HyperLogLogAdd(key, "a"); - db.HyperLogLogAdd(key, "b"); - db.HyperLogLogAdd(key, "c"); - - Assert.True(db.HyperLogLogLength(key) > 0); - } - } - - [Fact] - public void MultiKeyLength() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey[] keys = { "hll1", "hll2", "hll3" }; - - db.HyperLogLogAdd(keys[0], "a"); - db.HyperLogLogAdd(keys[1], "b"); - db.HyperLogLogAdd(keys[2], "c"); - - Assert.True(db.HyperLogLogLength(keys) > 0); - } - } + using var conn = Create(); + + var db = conn.GetDatabase(); + RedisKey key = "hll1"; + + db.HyperLogLogAdd(key, "a"); + db.HyperLogLogAdd(key, "b"); + db.HyperLogLogAdd(key, "c"); + + Assert.True(db.HyperLogLogLength(key) > 0); + } + + [Fact] + public void MultiKeyLength() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + RedisKey[] keys = { "hll1", "hll2", "hll3" }; + + db.HyperLogLogAdd(keys[0], "a"); + db.HyperLogLogAdd(keys[1], "b"); + db.HyperLogLogAdd(keys[2], "c"); + + Assert.True(db.HyperLogLogLength(keys) > 0); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/BgSaveResponse.cs b/tests/StackExchange.Redis.Tests/Issues/BgSaveResponse.cs index e6983f561..b40bf63fe 100644 --- a/tests/StackExchange.Redis.Tests/Issues/BgSaveResponse.cs +++ b/tests/StackExchange.Redis.Tests/Issues/BgSaveResponse.cs @@ -2,23 +2,21 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class BgSaveResponse : TestBase { - public class BgSaveResponse : TestBase + public BgSaveResponse(ITestOutputHelper output) : base (output) { } + + [Theory (Skip = "We don't need to test this, and it really screws local testing hard.")] + [InlineData(SaveType.BackgroundSave)] + [InlineData(SaveType.BackgroundRewriteAppendOnlyFile)] + public async Task ShouldntThrowException(SaveType saveType) { - public BgSaveResponse(ITestOutputHelper output) : base (output) { } + using var conn = Create(null, null, true); - [Theory (Skip = "We don't need to test this, and it really screws local testing hard.")] - [InlineData(SaveType.BackgroundSave)] - [InlineData(SaveType.BackgroundRewriteAppendOnlyFile)] - public async Task ShouldntThrowException(SaveType saveType) - { - using (var conn = Create(null, null, true)) - { - var Server = GetServer(conn); - Server.Save(saveType); - await Task.Delay(1000); - } - } + var Server = GetServer(conn); + Server.Save(saveType); + await Task.Delay(1000); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/DefaultDatabase.cs b/tests/StackExchange.Redis.Tests/Issues/DefaultDatabase.cs index 9bd4e17b6..5f9f37aeb 100644 --- a/tests/StackExchange.Redis.Tests/Issues/DefaultDatabase.cs +++ b/tests/StackExchange.Redis.Tests/Issues/DefaultDatabase.cs @@ -2,58 +2,55 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class DefaultDatabase : TestBase { - public class DefaultDatabase : TestBase - { - public DefaultDatabase(ITestOutputHelper output) : base (output) { } + public DefaultDatabase(ITestOutputHelper output) : base(output) { } - [Fact] - public void UnspecifiedDbId_ReturnsNull() - { - var config = ConfigurationOptions.Parse("localhost"); - Assert.Null(config.DefaultDatabase); - } + [Fact] + public void UnspecifiedDbId_ReturnsNull() + { + var config = ConfigurationOptions.Parse("localhost"); + Assert.Null(config.DefaultDatabase); + } - [Fact] - public void SpecifiedDbId_ReturnsExpected() - { - var config = ConfigurationOptions.Parse("localhost,defaultDatabase=3"); - Assert.Equal(3, config.DefaultDatabase); - } + [Fact] + public void SpecifiedDbId_ReturnsExpected() + { + var config = ConfigurationOptions.Parse("localhost,defaultDatabase=3"); + Assert.Equal(3, config.DefaultDatabase); + } - [Fact] - public void ConfigurationOptions_UnspecifiedDefaultDb() + [Fact] + public void ConfigurationOptions_UnspecifiedDefaultDb() + { + var log = new StringWriter(); + try { - var log = new StringWriter(); - try - { - using (var conn = ConnectionMultiplexer.Connect(TestConfig.Current.PrimaryServerAndPort, log)) { - var db = conn.GetDatabase(); - Assert.Equal(0, db.Database); - } - } - finally - { - Log(log.ToString()); - } + using var conn = ConnectionMultiplexer.Connect(TestConfig.Current.PrimaryServerAndPort, log); + var db = conn.GetDatabase(); + Assert.Equal(0, db.Database); } + finally + { + Log(log.ToString()); + } + } - [Fact] - public void ConfigurationOptions_SpecifiedDefaultDb() + [Fact] + public void ConfigurationOptions_SpecifiedDefaultDb() + { + var log = new StringWriter(); + try + { + using var conn = ConnectionMultiplexer.Connect($"{TestConfig.Current.PrimaryServerAndPort},defaultDatabase=3", log); + var db = conn.GetDatabase(); + Assert.Equal(3, db.Database); + } + finally { - var log = new StringWriter(); - try - { - using (var conn = ConnectionMultiplexer.Connect($"{TestConfig.Current.PrimaryServerAndPort},defaultDatabase=3", log)) { - var db = conn.GetDatabase(); - Assert.Equal(3, db.Database); - } - } - finally - { - Log(log.ToString()); - } + Log(log.ToString()); } - } + } } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue10.cs b/tests/StackExchange.Redis.Tests/Issues/Issue10.cs index 252fefa51..2a9d693eb 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue10.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue10.cs @@ -1,31 +1,29 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class Issue10 : TestBase { - public class Issue10 : TestBase + public Issue10(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Execute() { - public Issue10(ITestOutputHelper output) : base(output) { } + using var conn = Create(); - [Fact] - public void Execute() - { - using (var muxer = Create()) - { - var key = Me(); - var conn = muxer.GetDatabase(); - conn.KeyDeleteAsync(key); // contents: nil - conn.ListLeftPushAsync(key, "abc"); // "abc" - conn.ListLeftPushAsync(key, "def"); // "def", "abc" - conn.ListLeftPushAsync(key, "ghi"); // "ghi", "def", "abc", - conn.ListSetByIndexAsync(key, 1, "jkl"); // "ghi", "jkl", "abc" + var key = Me(); + var db = conn.GetDatabase(); + db.KeyDeleteAsync(key); // contents: nil + db.ListLeftPushAsync(key, "abc"); // "abc" + db.ListLeftPushAsync(key, "def"); // "def", "abc" + db.ListLeftPushAsync(key, "ghi"); // "ghi", "def", "abc", + db.ListSetByIndexAsync(key, 1, "jkl"); // "ghi", "jkl", "abc" - var contents = conn.Wait(conn.ListRangeAsync(key, 0, -1)); - Assert.Equal(3, contents.Length); - Assert.Equal("ghi", contents[0]); - Assert.Equal("jkl", contents[1]); - Assert.Equal("abc", contents[2]); - } - } + var contents = db.Wait(db.ListRangeAsync(key, 0, -1)); + Assert.Equal(3, contents.Length); + Assert.Equal("ghi", contents[0]); + Assert.Equal("jkl", contents[1]); + Assert.Equal("abc", contents[2]); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs b/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs index bbfd2c126..ebefcd53b 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs @@ -6,190 +6,186 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class Issue1101 : TestBase { - public class Issue1101 : TestBase + public Issue1101(ITestOutputHelper output) : base(output) { } + + private static void AssertCounts(ISubscriber pubsub, in RedisChannel channel, bool has, int handlers, int queues) { - public Issue1101(ITestOutputHelper output) : base(output) { } + if (pubsub.Multiplexer is ConnectionMultiplexer muxer) + { + var aHas = muxer.GetSubscriberCounts(channel, out var ah, out var aq); + Assert.Equal(has, aHas); + Assert.Equal(handlers, ah); + Assert.Equal(queues, aq); + } + } - private static void AssertCounts(ISubscriber pubsub, in RedisChannel channel, - bool has, int handlers, int queues) + [Fact] + public async Task ExecuteWithUnsubscribeViaChannel() + { + using var conn = Create(log: Writer); + + RedisChannel name = Me(); + var pubsub = conn.GetSubscriber(); + AssertCounts(pubsub, name, false, 0, 0); + + // subscribe and check we get data + var first = await pubsub.SubscribeAsync(name); + var second = await pubsub.SubscribeAsync(name); + AssertCounts(pubsub, name, true, 0, 2); + var values = new List(); + int i = 0; + first.OnMessage(x => + { + lock (values) { values.Add(x.Message); } + return Task.CompletedTask; + }); + second.OnMessage(_ => Interlocked.Increment(ref i)); + await Task.Delay(200); + await pubsub.PublishAsync(name, "abc"); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); + lock (values) { - if (pubsub.Multiplexer is ConnectionMultiplexer muxer) - { - var aHas = muxer.GetSubscriberCounts(channel, out var ah, out var aq); - Assert.Equal(has, aHas); - Assert.Equal(handlers, ah); - Assert.Equal(queues, aq); - } + Assert.Equal("abc", Assert.Single(values)); } - [Fact] - public async Task ExecuteWithUnsubscribeViaChannel() + var subs = conn.GetServer(conn.GetEndPoints().Single()).SubscriptionSubscriberCount(name); + Assert.Equal(1, subs); + Assert.False(first.Completion.IsCompleted, "completed"); + Assert.False(second.Completion.IsCompleted, "completed"); + + await first.UnsubscribeAsync(); + await Task.Delay(200); + await pubsub.PublishAsync(name, "def"); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1 && Volatile.Read(ref i) == 2); + lock (values) { - using (var muxer = Create(log: Writer)) - { - RedisChannel name = Me(); - var pubsub = muxer.GetSubscriber(); - AssertCounts(pubsub, name, false, 0, 0); - - // subscribe and check we get data - var first = await pubsub.SubscribeAsync(name); - var second = await pubsub.SubscribeAsync(name); - AssertCounts(pubsub, name, true, 0, 2); - var values = new List(); - int i = 0; - first.OnMessage(x => - { - lock (values) { values.Add(x.Message); } - return Task.CompletedTask; - }); - second.OnMessage(_ => Interlocked.Increment(ref i)); - await Task.Delay(200); - await pubsub.PublishAsync(name, "abc"); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); - lock (values) - { - Assert.Equal("abc", Assert.Single(values)); - } - var subs = muxer.GetServer(muxer.GetEndPoints().Single()).SubscriptionSubscriberCount(name); - Assert.Equal(1, subs); - Assert.False(first.Completion.IsCompleted, "completed"); - Assert.False(second.Completion.IsCompleted, "completed"); - - await first.UnsubscribeAsync(); - await Task.Delay(200); - await pubsub.PublishAsync(name, "def"); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1 && Volatile.Read(ref i) == 2); - lock (values) - { - Assert.Equal("abc", Assert.Single(values)); - } - Assert.Equal(2, Volatile.Read(ref i)); - Assert.True(first.Completion.IsCompleted, "completed"); - Assert.False(second.Completion.IsCompleted, "completed"); - AssertCounts(pubsub, name, true, 0, 1); - - await second.UnsubscribeAsync(); - await Task.Delay(200); - await pubsub.PublishAsync(name, "ghi"); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); - lock (values) - { - Assert.Equal("abc", Assert.Single(values)); - } - Assert.Equal(2, Volatile.Read(ref i)); - Assert.True(first.Completion.IsCompleted, "completed"); - Assert.True(second.Completion.IsCompleted, "completed"); - AssertCounts(pubsub, name, false, 0, 0); - - subs = muxer.GetServer(muxer.GetEndPoints().Single()).SubscriptionSubscriberCount(name); - Assert.Equal(0, subs); - Assert.True(first.Completion.IsCompleted, "completed"); - Assert.True(second.Completion.IsCompleted, "completed"); - } + Assert.Equal("abc", Assert.Single(values)); } + Assert.Equal(2, Volatile.Read(ref i)); + Assert.True(first.Completion.IsCompleted, "completed"); + Assert.False(second.Completion.IsCompleted, "completed"); + AssertCounts(pubsub, name, true, 0, 1); - [Fact] - public async Task ExecuteWithUnsubscribeViaSubscriber() + await second.UnsubscribeAsync(); + await Task.Delay(200); + await pubsub.PublishAsync(name, "ghi"); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); + lock (values) { - using (var muxer = Create(shared: false, log: Writer)) - { - RedisChannel name = Me(); - var pubsub = muxer.GetSubscriber(); - AssertCounts(pubsub, name, false, 0, 0); - - // subscribe and check we get data - var first = await pubsub.SubscribeAsync(name); - var second = await pubsub.SubscribeAsync(name); - AssertCounts(pubsub, name, true, 0, 2); - var values = new List(); - int i = 0; - first.OnMessage(x => - { - lock (values) { values.Add(x.Message); } - return Task.CompletedTask; - }); - second.OnMessage(_ => Interlocked.Increment(ref i)); - - await Task.Delay(100); - await pubsub.PublishAsync(name, "abc"); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); - lock (values) - { - Assert.Equal("abc", Assert.Single(values)); - } - var subs = muxer.GetServer(muxer.GetEndPoints().Single()).SubscriptionSubscriberCount(name); - Assert.Equal(1, subs); - Assert.False(first.Completion.IsCompleted, "completed"); - Assert.False(second.Completion.IsCompleted, "completed"); - - await pubsub.UnsubscribeAsync(name); - await Task.Delay(100); - await pubsub.PublishAsync(name, "def"); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); - lock (values) - { - Assert.Equal("abc", Assert.Single(values)); - } - Assert.Equal(1, Volatile.Read(ref i)); - - subs = muxer.GetServer(muxer.GetEndPoints().Single()).SubscriptionSubscriberCount(name); - Assert.Equal(0, subs); - Assert.True(first.Completion.IsCompleted, "completed"); - Assert.True(second.Completion.IsCompleted, "completed"); - AssertCounts(pubsub, name, false, 0, 0); - } + Assert.Equal("abc", Assert.Single(values)); } + Assert.Equal(2, Volatile.Read(ref i)); + Assert.True(first.Completion.IsCompleted, "completed"); + Assert.True(second.Completion.IsCompleted, "completed"); + AssertCounts(pubsub, name, false, 0, 0); + + subs = conn.GetServer(conn.GetEndPoints().Single()).SubscriptionSubscriberCount(name); + Assert.Equal(0, subs); + Assert.True(first.Completion.IsCompleted, "completed"); + Assert.True(second.Completion.IsCompleted, "completed"); + } + + [Fact] + public async Task ExecuteWithUnsubscribeViaSubscriber() + { + using var conn = Create(shared: false, log: Writer); + + RedisChannel name = Me(); + var pubsub = conn.GetSubscriber(); + AssertCounts(pubsub, name, false, 0, 0); + + // subscribe and check we get data + var first = await pubsub.SubscribeAsync(name); + var second = await pubsub.SubscribeAsync(name); + AssertCounts(pubsub, name, true, 0, 2); + var values = new List(); + int i = 0; + first.OnMessage(x => + { + lock (values) { values.Add(x.Message); } + return Task.CompletedTask; + }); + second.OnMessage(_ => Interlocked.Increment(ref i)); - [Fact] - public async Task ExecuteWithUnsubscribeViaClearAll() + await Task.Delay(100); + await pubsub.PublishAsync(name, "abc"); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); + lock (values) { - using (var muxer = Create(log: Writer)) - { - RedisChannel name = Me(); - var pubsub = muxer.GetSubscriber(); - AssertCounts(pubsub, name, false, 0, 0); - - // subscribe and check we get data - var first = await pubsub.SubscribeAsync(name); - var second = await pubsub.SubscribeAsync(name); - AssertCounts(pubsub, name, true, 0, 2); - var values = new List(); - int i = 0; - first.OnMessage(x => - { - lock (values) { values.Add(x.Message); } - return Task.CompletedTask; - }); - second.OnMessage(_ => Interlocked.Increment(ref i)); - await Task.Delay(100); - await pubsub.PublishAsync(name, "abc"); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); - lock (values) - { - Assert.Equal("abc", Assert.Single(values)); - } - var subs = muxer.GetServer(muxer.GetEndPoints().Single()).SubscriptionSubscriberCount(name); - Assert.Equal(1, subs); - Assert.False(first.Completion.IsCompleted, "completed"); - Assert.False(second.Completion.IsCompleted, "completed"); - - await pubsub.UnsubscribeAllAsync(); - await Task.Delay(100); - await pubsub.PublishAsync(name, "def"); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); - lock (values) - { - Assert.Equal("abc", Assert.Single(values)); - } - Assert.Equal(1, Volatile.Read(ref i)); - - subs = muxer.GetServer(muxer.GetEndPoints().Single()).SubscriptionSubscriberCount(name); - Assert.Equal(0, subs); - Assert.True(first.Completion.IsCompleted, "completed"); - Assert.True(second.Completion.IsCompleted, "completed"); - AssertCounts(pubsub, name, false, 0, 0); - } + Assert.Equal("abc", Assert.Single(values)); } + var subs = conn.GetServer(conn.GetEndPoints().Single()).SubscriptionSubscriberCount(name); + Assert.Equal(1, subs); + Assert.False(first.Completion.IsCompleted, "completed"); + Assert.False(second.Completion.IsCompleted, "completed"); + + await pubsub.UnsubscribeAsync(name); + await Task.Delay(100); + await pubsub.PublishAsync(name, "def"); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); + lock (values) + { + Assert.Equal("abc", Assert.Single(values)); + } + Assert.Equal(1, Volatile.Read(ref i)); + + subs = conn.GetServer(conn.GetEndPoints().Single()).SubscriptionSubscriberCount(name); + Assert.Equal(0, subs); + Assert.True(first.Completion.IsCompleted, "completed"); + Assert.True(second.Completion.IsCompleted, "completed"); + AssertCounts(pubsub, name, false, 0, 0); + } + + [Fact] + public async Task ExecuteWithUnsubscribeViaClearAll() + { + using var conn = Create(log: Writer); + + RedisChannel name = Me(); + var pubsub = conn.GetSubscriber(); + AssertCounts(pubsub, name, false, 0, 0); + + // subscribe and check we get data + var first = await pubsub.SubscribeAsync(name); + var second = await pubsub.SubscribeAsync(name); + AssertCounts(pubsub, name, true, 0, 2); + var values = new List(); + int i = 0; + first.OnMessage(x => + { + lock (values) { values.Add(x.Message); } + return Task.CompletedTask; + }); + second.OnMessage(_ => Interlocked.Increment(ref i)); + await Task.Delay(100); + await pubsub.PublishAsync(name, "abc"); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); + lock (values) + { + Assert.Equal("abc", Assert.Single(values)); + } + var subs = conn.GetServer(conn.GetEndPoints().Single()).SubscriptionSubscriberCount(name); + Assert.Equal(1, subs); + Assert.False(first.Completion.IsCompleted, "completed"); + Assert.False(second.Completion.IsCompleted, "completed"); + + await pubsub.UnsubscribeAllAsync(); + await Task.Delay(100); + await pubsub.PublishAsync(name, "def"); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => values.Count == 1); + lock (values) + { + Assert.Equal("abc", Assert.Single(values)); + } + Assert.Equal(1, Volatile.Read(ref i)); + + subs = conn.GetServer(conn.GetEndPoints().Single()).SubscriptionSubscriberCount(name); + Assert.Equal(0, subs); + Assert.True(first.Completion.IsCompleted, "completed"); + Assert.True(second.Completion.IsCompleted, "completed"); + AssertCounts(pubsub, name, false, 0, 0); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue1103.cs b/tests/StackExchange.Redis.Tests/Issues/Issue1103.cs index 20f1832e6..d712b0dee 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue1103.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue1103.cs @@ -3,67 +3,65 @@ using Xunit.Abstractions; using static StackExchange.Redis.RedisValue; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class Issue1103 : TestBase { - public class Issue1103 : TestBase + public Issue1103(ITestOutputHelper output) : base(output) { } + + [Theory] + [InlineData(142205255210238005UL, (int)StorageType.Int64)] + [InlineData(ulong.MaxValue, (int)StorageType.UInt64)] + [InlineData(ulong.MinValue, (int)StorageType.Int64)] + [InlineData(0x8000000000000000UL, (int)StorageType.UInt64)] + [InlineData(0x8000000000000001UL, (int)StorageType.UInt64)] + [InlineData(0x7FFFFFFFFFFFFFFFUL, (int)StorageType.Int64)] + public void LargeUInt64StoredCorrectly(ulong value, int storageType) { - public Issue1103(ITestOutputHelper output) : base(output) { } + using var conn = Create(); - [Theory] - [InlineData(142205255210238005UL, (int)StorageType.Int64)] - [InlineData(ulong.MaxValue, (int)StorageType.UInt64)] - [InlineData(ulong.MinValue, (int)StorageType.Int64)] - [InlineData(0x8000000000000000UL, (int)StorageType.UInt64)] - [InlineData(0x8000000000000001UL, (int)StorageType.UInt64)] - [InlineData(0x7FFFFFFFFFFFFFFFUL, (int)StorageType.Int64)] - public void LargeUInt64StoredCorrectly(ulong value, int storageType) - { - RedisKey key = Me(); - using (var muxer = Create()) - { - var db = muxer.GetDatabase(); - RedisValue typed = value; + RedisKey key = Me(); + var db = conn.GetDatabase(); + RedisValue typed = value; - // only need UInt64 for 64-bits - Assert.Equal((StorageType)storageType, typed.Type); - db.StringSet(key, typed); - var fromRedis = db.StringGet(key); + // only need UInt64 for 64-bits + Assert.Equal((StorageType)storageType, typed.Type); + db.StringSet(key, typed); + var fromRedis = db.StringGet(key); - Log($"{fromRedis.Type}: {fromRedis}"); - Assert.Equal(StorageType.Raw, fromRedis.Type); - Assert.Equal(value, (ulong)fromRedis); - Assert.Equal(value.ToString(CultureInfo.InvariantCulture), fromRedis.ToString()); + Log($"{fromRedis.Type}: {fromRedis}"); + Assert.Equal(StorageType.Raw, fromRedis.Type); + Assert.Equal(value, (ulong)fromRedis); + Assert.Equal(value.ToString(CultureInfo.InvariantCulture), fromRedis.ToString()); - var simplified = fromRedis.Simplify(); - Log($"{simplified.Type}: {simplified}"); - Assert.Equal((StorageType)storageType, typed.Type); - Assert.Equal(value, (ulong)simplified); - Assert.Equal(value.ToString(CultureInfo.InvariantCulture), fromRedis.ToString()); - } - } + var simplified = fromRedis.Simplify(); + Log($"{simplified.Type}: {simplified}"); + Assert.Equal((StorageType)storageType, typed.Type); + Assert.Equal(value, (ulong)simplified); + Assert.Equal(value.ToString(CultureInfo.InvariantCulture), fromRedis.ToString()); + } - [Fact] - public void UnusualRedisValueOddities() // things we found while doing this - { - RedisValue x = 0, y = "0"; - Assert.Equal(x, y); - Assert.Equal(y, x); + [Fact] + public void UnusualRedisValueOddities() // things we found while doing this + { + RedisValue x = 0, y = "0"; + Assert.Equal(x, y); + Assert.Equal(y, x); - y = "-0"; - Assert.Equal(x, y); - Assert.Equal(y, x); + y = "-0"; + Assert.Equal(x, y); + Assert.Equal(y, x); - y = "-"; // this is the oddness; this used to return true - Assert.NotEqual(x, y); - Assert.NotEqual(y, x); + y = "-"; // this is the oddness; this used to return true + Assert.NotEqual(x, y); + Assert.NotEqual(y, x); - y = "+"; - Assert.NotEqual(x, y); - Assert.NotEqual(y, x); + y = "+"; + Assert.NotEqual(x, y); + Assert.NotEqual(y, x); - y = "."; - Assert.NotEqual(x, y); - Assert.NotEqual(y, x); - } + y = "."; + Assert.NotEqual(x, y); + Assert.NotEqual(y, x); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue182.cs b/tests/StackExchange.Redis.Tests/Issues/Issue182.cs index 04dfc8950..c932cfa12 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue182.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue182.cs @@ -4,78 +4,75 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class Issue182 : TestBase { - public class Issue182 : TestBase - { - protected override string GetConfiguration() => $"{TestConfig.Current.PrimaryServerAndPort},responseTimeout=10000"; + protected override string GetConfiguration() => $"{TestConfig.Current.PrimaryServerAndPort},responseTimeout=10000"; - public Issue182(ITestOutputHelper output) : base (output) { } + public Issue182(ITestOutputHelper output) : base (output) { } + + [FactLongRunning] + public async Task SetMembers() + { + using var conn = Create(syncTimeout: 20000); - [FactLongRunning] - public async Task SetMembers() + conn.ConnectionFailed += (s, a) => { - using (var conn = Create(syncTimeout: 20000)) - { - conn.ConnectionFailed += (s, a) => - { - Log(a.FailureType.ToString()); - Log(a.Exception?.Message); - Log(a.Exception?.StackTrace); - }; - var db = conn.GetDatabase(); + Log(a.FailureType.ToString()); + Log(a.Exception?.Message); + Log(a.Exception?.StackTrace); + }; + var db = conn.GetDatabase(); - var key = Me(); - const int count = (int)5e6; - var len = await db.SetLengthAsync(key).ForAwait(); + var key = Me(); + const int count = (int)5e6; + var len = await db.SetLengthAsync(key).ForAwait(); - if (len != count) - { - await db.KeyDeleteAsync(key).ForAwait(); - foreach (var _ in Enumerable.Range(0, count)) - db.SetAdd(key, Guid.NewGuid().ToByteArray(), CommandFlags.FireAndForget); + if (len != count) + { + await db.KeyDeleteAsync(key).ForAwait(); + foreach (var _ in Enumerable.Range(0, count)) + db.SetAdd(key, Guid.NewGuid().ToByteArray(), CommandFlags.FireAndForget); - Assert.Equal(count, await db.SetLengthAsync(key).ForAwait()); // SCARD for set - } - var result = await db.SetMembersAsync(key).ForAwait(); - Assert.Equal(count, result.Length); // SMEMBERS result length - } + Assert.Equal(count, await db.SetLengthAsync(key).ForAwait()); // SCARD for set } + var result = await db.SetMembersAsync(key).ForAwait(); + Assert.Equal(count, result.Length); // SMEMBERS result length + } - [FactLongRunning] - public async Task SetUnion() - { - using (var conn = Create(syncTimeout: 10000)) - { - var db = conn.GetDatabase(); + [FactLongRunning] + public async Task SetUnion() + { + using var conn = Create(syncTimeout: 10000); - var key1 = Me() + ":1"; - var key2 = Me() + ":2"; - var dstkey = Me() + ":dst"; + var db = conn.GetDatabase(); - const int count = (int)5e6; + var key1 = Me() + ":1"; + var key2 = Me() + ":2"; + var dstkey = Me() + ":dst"; - var len1 = await db.SetLengthAsync(key1).ForAwait(); - var len2 = await db.SetLengthAsync(key2).ForAwait(); - await db.KeyDeleteAsync(dstkey).ForAwait(); + const int count = (int)5e6; - if (len1 != count || len2 != count) - { - await db.KeyDeleteAsync(key1).ForAwait(); - await db.KeyDeleteAsync(key2).ForAwait(); + var len1 = await db.SetLengthAsync(key1).ForAwait(); + var len2 = await db.SetLengthAsync(key2).ForAwait(); + await db.KeyDeleteAsync(dstkey).ForAwait(); - foreach (var _ in Enumerable.Range(0, count)) - { - db.SetAdd(key1, Guid.NewGuid().ToByteArray(), CommandFlags.FireAndForget); - db.SetAdd(key2, Guid.NewGuid().ToByteArray(), CommandFlags.FireAndForget); - } - Assert.Equal(count, await db.SetLengthAsync(key1).ForAwait()); // SCARD for set 1 - Assert.Equal(count, await db.SetLengthAsync(key2).ForAwait()); // SCARD for set 2 - } - await db.SetCombineAndStoreAsync(SetOperation.Union, dstkey, key1, key2).ForAwait(); - var dstLen = db.SetLength(dstkey); - Assert.Equal(count * 2, dstLen); // SCARD for destination set + if (len1 != count || len2 != count) + { + await db.KeyDeleteAsync(key1).ForAwait(); + await db.KeyDeleteAsync(key2).ForAwait(); + + foreach (var _ in Enumerable.Range(0, count)) + { + db.SetAdd(key1, Guid.NewGuid().ToByteArray(), CommandFlags.FireAndForget); + db.SetAdd(key2, Guid.NewGuid().ToByteArray(), CommandFlags.FireAndForget); } + Assert.Equal(count, await db.SetLengthAsync(key1).ForAwait()); // SCARD for set 1 + Assert.Equal(count, await db.SetLengthAsync(key2).ForAwait()); // SCARD for set 2 } + await db.SetCombineAndStoreAsync(SetOperation.Union, dstkey, key1, key2).ForAwait(); + var dstLen = db.SetLength(dstkey); + Assert.Equal(count * 2, dstLen); // SCARD for destination set } } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue25.cs b/tests/StackExchange.Redis.Tests/Issues/Issue25.cs index 6ea9dc283..090a7ae8c 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue25.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue25.cs @@ -2,44 +2,43 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class Issue25 : TestBase { - public class Issue25 : TestBase - { - public Issue25(ITestOutputHelper output) : base (output) { } + public Issue25(ITestOutputHelper output) : base (output) { } - [Fact] - public void CaseInsensitive() - { - var options = ConfigurationOptions.Parse("ssl=true"); - Assert.True(options.Ssl); - Assert.Equal("ssl=True", options.ToString()); + [Fact] + public void CaseInsensitive() + { + var options = ConfigurationOptions.Parse("ssl=true"); + Assert.True(options.Ssl); + Assert.Equal("ssl=True", options.ToString()); - options = ConfigurationOptions.Parse("SSL=TRUE"); - Assert.True(options.Ssl); - Assert.Equal("ssl=True", options.ToString()); - } + options = ConfigurationOptions.Parse("SSL=TRUE"); + Assert.True(options.Ssl); + Assert.Equal("ssl=True", options.ToString()); + } - [Fact] - public void UnkonwnKeywordHandling_Ignore() - { - ConfigurationOptions.Parse("ssl2=true", true); - } + [Fact] + public void UnkonwnKeywordHandling_Ignore() + { + ConfigurationOptions.Parse("ssl2=true", true); + } - [Fact] - public void UnkonwnKeywordHandling_ExplicitFail() - { - var ex = Assert.Throws(() => ConfigurationOptions.Parse("ssl2=true", false)); - Assert.StartsWith("Keyword 'ssl2' is not supported", ex.Message); - Assert.Equal("ssl2", ex.ParamName); - } + [Fact] + public void UnkonwnKeywordHandling_ExplicitFail() + { + var ex = Assert.Throws(() => ConfigurationOptions.Parse("ssl2=true", false)); + Assert.StartsWith("Keyword 'ssl2' is not supported", ex.Message); + Assert.Equal("ssl2", ex.ParamName); + } - [Fact] - public void UnkonwnKeywordHandling_ImplicitFail() - { - var ex = Assert.Throws(() => ConfigurationOptions.Parse("ssl2=true")); - Assert.StartsWith("Keyword 'ssl2' is not supported", ex.Message); - Assert.Equal("ssl2", ex.ParamName); - } + [Fact] + public void UnkonwnKeywordHandling_ImplicitFail() + { + var ex = Assert.Throws(() => ConfigurationOptions.Parse("ssl2=true")); + Assert.StartsWith("Keyword 'ssl2' is not supported", ex.Message); + Assert.Equal("ssl2", ex.ParamName); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue6.cs b/tests/StackExchange.Redis.Tests/Issues/Issue6.cs index 9b9776099..87299aeb1 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue6.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue6.cs @@ -1,21 +1,19 @@ using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class Issue6 : TestBase { - public class Issue6 : TestBase + public Issue6(ITestOutputHelper output) : base (output) { } + + [Fact] + public void ShouldWorkWithoutEchoOrPing() { - public Issue6(ITestOutputHelper output) : base (output) { } + using var conn = Create(proxy: Proxy.Twemproxy); - [Fact] - public void ShouldWorkWithoutEchoOrPing() - { - using(var conn = Create(proxy: Proxy.Twemproxy)) - { - Log("config: " + conn.Configuration); - var db = conn.GetDatabase(); - var time = db.Ping(); - Log("ping time: " + time); - } - } + Log("config: " + conn.Configuration); + var db = conn.GetDatabase(); + var time = db.Ping(); + Log("ping time: " + time); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs b/tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs index 5b366d07d..e742690ca 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs @@ -4,77 +4,73 @@ using System.Threading.Tasks; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class Massive_Delete : TestBase { - public class Massive_Delete : TestBase + public Massive_Delete(ITestOutputHelper output) : base(output) { } + + private void Prep(int dbId, string key) { - public Massive_Delete(ITestOutputHelper output) : base(output) { } + using var conn = Create(allowAdmin: true); - private void Prep(int db, string key) + var prefix = Me(); + Skip.IfMissingDatabase(conn, dbId); + GetServer(conn).FlushDatabase(dbId); + Task? last = null; + var db = conn.GetDatabase(dbId); + for (int i = 0; i < 10000; i++) { - var prefix = Me(); - using (var muxer = Create(allowAdmin: true)) - { - Skip.IfMissingDatabase(muxer, db); - GetServer(muxer).FlushDatabase(db); - Task? last = null; - var conn = muxer.GetDatabase(db); - for (int i = 0; i < 10000; i++) - { - string iKey = prefix + i; - conn.StringSetAsync(iKey, iKey); - last = conn.SetAddAsync(key, iKey); - } - conn.Wait(last!); - } + string iKey = prefix + i; + db.StringSetAsync(iKey, iKey); + last = db.SetAddAsync(key, iKey); } + db.Wait(last!); + } - [FactLongRunning] - public async Task ExecuteMassiveDelete() + [FactLongRunning] + public async Task ExecuteMassiveDelete() + { + var dbId = TestConfig.GetDedicatedDB(); + var key = Me(); + Prep(dbId, key); + var watch = Stopwatch.StartNew(); + using var conn = Create(); + using var throttle = new SemaphoreSlim(1); + var db = conn.GetDatabase(dbId); + var originally = await db.SetLengthAsync(key).ForAwait(); + int keepChecking = 1; + Task? last = null; + while (Volatile.Read(ref keepChecking) == 1) { - var dbId = TestConfig.GetDedicatedDB(); - var key = Me(); - Prep(dbId, key); - var watch = Stopwatch.StartNew(); - using (var muxer = Create()) - using (var throttle = new SemaphoreSlim(1)) + throttle.Wait(); // acquire + var x = db.SetPopAsync(key).ContinueWith(task => { - var conn = muxer.GetDatabase(dbId); - var originally = await conn.SetLengthAsync(key).ForAwait(); - int keepChecking = 1; - Task? last = null; - while (Volatile.Read(ref keepChecking) == 1) + throttle.Release(); + if (task.IsCompleted) { - throttle.Wait(); // acquire - var x = conn.SetPopAsync(key).ContinueWith(task => + if ((string?)task.Result == null) { - throttle.Release(); - if (task.IsCompleted) - { - if ((string?)task.Result == null) - { - Volatile.Write(ref keepChecking, 0); - } - else - { - last = conn.KeyDeleteAsync((string?)task.Result); - } - } - }); - GC.KeepAlive(x); - } - if (last != null) - { - await last; + Volatile.Write(ref keepChecking, 0); + } + else + { + last = db.KeyDeleteAsync((string?)task.Result); + } } - watch.Stop(); - long remaining = await conn.SetLengthAsync(key).ForAwait(); - Log("From {0} to {1}; {2}ms", originally, remaining, - watch.ElapsedMilliseconds); - - var counters = GetServer(muxer).GetCounters(); - Log("Completions: {0} sync, {1} async", counters.Interactive.CompletedSynchronously, counters.Interactive.CompletedAsynchronously); - } + }); + GC.KeepAlive(x); } + if (last != null) + { + await last; + } + watch.Stop(); + long remaining = await db.SetLengthAsync(key).ForAwait(); + Log("From {0} to {1}; {2}ms", originally, remaining, + watch.ElapsedMilliseconds); + + var counters = GetServer(conn).GetCounters(); + Log("Completions: {0} sync, {1} async", counters.Interactive.CompletedSynchronously, counters.Interactive.CompletedAsynchronously); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/SO10504853.cs b/tests/StackExchange.Redis.Tests/Issues/SO10504853.cs index bd20cf6d2..9f8e71e4d 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO10504853.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO10504853.cs @@ -3,89 +3,84 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class SO10504853 : TestBase { - public class SO10504853 : TestBase - { - public SO10504853(ITestOutputHelper output) : base(output) { } + public SO10504853(ITestOutputHelper output) : base(output) { } - [Fact] - public void LoopLotsOfTrivialStuff() + [Fact] + public void LoopLotsOfTrivialStuff() + { + var key = Me(); + Trace.WriteLine("### init"); + using (var conn = Create()) { - var key = Me(); - Trace.WriteLine("### init"); - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - } - const int COUNT = 2; - for (int i = 0; i < COUNT; i++) - { - Trace.WriteLine("### incr:" + i); - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - Assert.Equal(i + 1, conn.StringIncrement(key)); - } - } - Trace.WriteLine("### close"); - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - Assert.Equal(COUNT, (long)conn.StringGet(key)); - } + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); } - - [Fact] - public void ExecuteWithEmptyStartingPoint() + const int COUNT = 2; + for (int i = 0; i < COUNT; i++) { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var key = Me(); - var task = new { priority = 3 }; - conn.KeyDeleteAsync(key); - conn.HashSetAsync(key, "something else", "abc"); - conn.HashSetAsync(key, "priority", task.priority.ToString()); + Trace.WriteLine("### incr:" + i); + using var conn = Create(); + var db = conn.GetDatabase(); + Assert.Equal(i + 1, db.StringIncrement(key)); + } + Trace.WriteLine("### close"); + using (var conn = Create()) + { + var db = conn.GetDatabase(); + Assert.Equal(COUNT, (long)db.StringGet(key)); + } + } - var taskResult = conn.HashGetAsync(key, "priority"); + [Fact] + public void ExecuteWithEmptyStartingPoint() + { + using var conn = Create(); - conn.Wait(taskResult); + var db = conn.GetDatabase(); + var key = Me(); + var task = new { priority = 3 }; + db.KeyDeleteAsync(key); + db.HashSetAsync(key, "something else", "abc"); + db.HashSetAsync(key, "priority", task.priority.ToString()); - var priority = int.Parse(taskResult.Result!); + var taskResult = db.HashGetAsync(key, "priority"); - Assert.Equal(3, priority); - } - } + db.Wait(taskResult); + + var priority = int.Parse(taskResult.Result!); - [Fact] - public void ExecuteWithNonHashStartingPoint() + Assert.Equal(3, priority); + } + + [Fact] + public void ExecuteWithNonHashStartingPoint() + { + var key = Me(); + Assert.Throws(() => { - var key = Me(); - Assert.Throws(() => - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var task = new { priority = 3 }; - conn.KeyDeleteAsync(key); - conn.StringSetAsync(key, "not a hash"); - conn.HashSetAsync(key, "priority", task.priority.ToString()); + using var conn = Create(); - var taskResult = conn.HashGetAsync(key, "priority"); + var db = conn.GetDatabase(); + var task = new { priority = 3 }; + db.KeyDeleteAsync(key); + db.StringSetAsync(key, "not a hash"); + db.HashSetAsync(key, "priority", task.priority.ToString()); - try - { - conn.Wait(taskResult); - Assert.True(false, "Should throw a WRONGTYPE"); - } - catch (AggregateException ex) - { - throw ex.InnerExceptions[0]; - } - } - }); // WRONGTYPE Operation against a key holding the wrong kind of value - } + var taskResult = db.HashGetAsync(key, "priority"); + + try + { + db.Wait(taskResult); + Assert.True(false, "Should throw a WRONGTYPE"); + } + catch (AggregateException ex) + { + throw ex.InnerExceptions[0]; + } + }); // WRONGTYPE Operation against a key holding the wrong kind of value } } diff --git a/tests/StackExchange.Redis.Tests/Issues/SO10825542.cs b/tests/StackExchange.Redis.Tests/Issues/SO10825542.cs index 5f12ebe42..c4c20a210 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO10825542.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO10825542.cs @@ -4,31 +4,28 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class SO10825542 : TestBase { - public class SO10825542 : TestBase - { - public SO10825542(ITestOutputHelper output) : base(output) { } + public SO10825542(ITestOutputHelper output) : base(output) { } - [Fact] - public async Task Execute() - { - using (var muxer = Create()) - { - var key = Me(); + [Fact] + public async Task Execute() + { + using var conn = Create(); + var key = Me(); - var con = muxer.GetDatabase(); - // set the field value and expiration - _ = con.HashSetAsync(key, "field1", Encoding.UTF8.GetBytes("hello world")); - _ = con.KeyExpireAsync(key, TimeSpan.FromSeconds(7200)); - _ = con.HashSetAsync(key, "field2", "fooobar"); - var result = await con.HashGetAllAsync(key).ForAwait(); + var db = conn.GetDatabase(); + // set the field value and expiration + _ = db.HashSetAsync(key, "field1", Encoding.UTF8.GetBytes("hello world")); + _ = db.KeyExpireAsync(key, TimeSpan.FromSeconds(7200)); + _ = db.HashSetAsync(key, "field2", "fooobar"); + var result = await db.HashGetAllAsync(key).ForAwait(); - Assert.Equal(2, result.Length); - var dict = result.ToStringDictionary(); - Assert.Equal("hello world", dict["field1"]); - Assert.Equal("fooobar", dict["field2"]); - } - } + Assert.Equal(2, result.Length); + var dict = result.ToStringDictionary(); + Assert.Equal("hello world", dict["field1"]); + Assert.Equal("fooobar", dict["field2"]); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/SO11766033.cs b/tests/StackExchange.Redis.Tests/Issues/SO11766033.cs index cf88d9cdf..4cc7596e5 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO11766033.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO11766033.cs @@ -1,41 +1,38 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class SO11766033 : TestBase { - public class SO11766033 : TestBase + public SO11766033(ITestOutputHelper output) : base(output) { } + + [Fact] + public void TestNullString() { - public SO11766033(ITestOutputHelper output) : base(output) { } - - [Fact] - public void TestNullString() - { - using (var muxer = Create()) - { - var redis = muxer.GetDatabase(); - const string? expectedTestValue = null; - var uid = Me(); - redis.StringSetAsync(uid, "abc"); - redis.StringSetAsync(uid, expectedTestValue); - string? testValue = redis.StringGet(uid); - Assert.Null(testValue); - } - } - - [Fact] - public void TestEmptyString() - { - using (var muxer = Create()) - { - var redis = muxer.GetDatabase(); - const string expectedTestValue = ""; - var uid = Me(); - - redis.StringSetAsync(uid, expectedTestValue); - string? testValue = redis.StringGet(uid); - - Assert.Equal(expectedTestValue, testValue); - } - } + using var conn = Create(); + + var db = conn.GetDatabase(); + const string? expectedTestValue = null; + var uid = Me(); + db.StringSetAsync(uid, "abc"); + db.StringSetAsync(uid, expectedTestValue); + string? testValue = db.StringGet(uid); + Assert.Null(testValue); + } + + [Fact] + public void TestEmptyString() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + const string expectedTestValue = ""; + var uid = Me(); + + db.StringSetAsync(uid, expectedTestValue); + string? testValue = db.StringGet(uid); + + Assert.Equal(expectedTestValue, testValue); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/SO22786599.cs b/tests/StackExchange.Redis.Tests/Issues/SO22786599.cs index 893870550..5726f2bcd 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO22786599.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO22786599.cs @@ -3,35 +3,33 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class SO22786599 : TestBase { - public class SO22786599 : TestBase + public SO22786599(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Execute() { - public SO22786599(ITestOutputHelper output) : base(output) { } - - [Fact] - public void Execute() - { - string CurrentIdsSetDbKey = Me() + ".x"; - string CurrentDetailsSetDbKey = Me() + ".y"; - - RedisValue[] stringIds = Enumerable.Range(1, 750).Select(i => (RedisValue)(i + " id")).ToArray(); - RedisValue[] stringDetails = Enumerable.Range(1, 750).Select(i => (RedisValue)(i + " detail")).ToArray(); - - using (var conn = Create()) - { - var db = conn.GetDatabase(); - var tran = db.CreateTransaction(); - - tran.SetAddAsync(CurrentIdsSetDbKey, stringIds); - tran.SetAddAsync(CurrentDetailsSetDbKey, stringDetails); - - var watch = Stopwatch.StartNew(); - var isOperationSuccessful = tran.Execute(); - watch.Stop(); - Log("{0}ms", watch.ElapsedMilliseconds); - Assert.True(isOperationSuccessful); - } - } + string CurrentIdsSetDbKey = Me() + ".x"; + string CurrentDetailsSetDbKey = Me() + ".y"; + + RedisValue[] stringIds = Enumerable.Range(1, 750).Select(i => (RedisValue)(i + " id")).ToArray(); + RedisValue[] stringDetails = Enumerable.Range(1, 750).Select(i => (RedisValue)(i + " detail")).ToArray(); + + using var conn = Create(); + + var db = conn.GetDatabase(); + var tran = db.CreateTransaction(); + + tran.SetAddAsync(CurrentIdsSetDbKey, stringIds); + tran.SetAddAsync(CurrentDetailsSetDbKey, stringDetails); + + var watch = Stopwatch.StartNew(); + var isOperationSuccessful = tran.Execute(); + watch.Stop(); + Log("{0}ms", watch.ElapsedMilliseconds); + Assert.True(isOperationSuccessful); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/SO23949477.cs b/tests/StackExchange.Redis.Tests/Issues/SO23949477.cs index 25435443f..f8b7baa2a 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO23949477.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO23949477.cs @@ -1,40 +1,38 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class SO23949477 : TestBase { - public class SO23949477 : TestBase + public SO23949477(ITestOutputHelper output) : base (output) { } + + [Fact] + public void Execute() { - public SO23949477(ITestOutputHelper output) : base (output) { } + using var conn = Create(); - [Fact] - public void Execute() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.SortedSetAdd(key, "c", 3, When.Always, CommandFlags.FireAndForget); - db.SortedSetAdd(key, - new[] { - new SortedSetEntry("a", 1), - new SortedSetEntry("b", 2), - new SortedSetEntry("d", 4), - new SortedSetEntry("e", 5) - }, - When.Always, - CommandFlags.FireAndForget); - var pairs = db.SortedSetRangeByScoreWithScores( - key, order: Order.Descending, take: 3); - Assert.Equal(3, pairs.Length); - Assert.Equal(5, pairs[0].Score); - Assert.Equal("e", pairs[0].Element); - Assert.Equal(4, pairs[1].Score); - Assert.Equal("d", pairs[1].Element); - Assert.Equal(3, pairs[2].Score); - Assert.Equal("c", pairs[2].Element); - } - } + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.SortedSetAdd(key, "c", 3, When.Always, CommandFlags.FireAndForget); + db.SortedSetAdd(key, + new[] { + new SortedSetEntry("a", 1), + new SortedSetEntry("b", 2), + new SortedSetEntry("d", 4), + new SortedSetEntry("e", 5) + }, + When.Always, + CommandFlags.FireAndForget); + var pairs = db.SortedSetRangeByScoreWithScores( + key, order: Order.Descending, take: 3); + Assert.Equal(3, pairs.Length); + Assert.Equal(5, pairs[0].Score); + Assert.Equal("e", pairs[0].Element); + Assert.Equal(4, pairs[1].Score); + Assert.Equal("d", pairs[1].Element); + Assert.Equal(3, pairs[2].Score); + Assert.Equal("c", pairs[2].Element); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs b/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs index b8d1793af..954bf9fee 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs @@ -3,47 +3,45 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class SO24807536 : TestBase { - public class SO24807536 : TestBase + public SO24807536(ITestOutputHelper output) : base (output) { } + + [Fact] + public async Task Exec() { - public SO24807536(ITestOutputHelper output) : base (output) { } - - [Fact] - public async Task Exec() - { - var key = Me(); - using(var conn = Create()) - { - var cache = conn.GetDatabase(); - - // setup some data - cache.KeyDelete(key, CommandFlags.FireAndForget); - cache.HashSet(key, "full", "some value", flags: CommandFlags.FireAndForget); - cache.KeyExpire(key, TimeSpan.FromSeconds(4), CommandFlags.FireAndForget); - - // test while exists - var keyExists = cache.KeyExists(key); - var ttl = cache.KeyTimeToLive(key); - var fullWait = cache.HashGetAsync(key, "full", flags: CommandFlags.None); - Assert.True(keyExists, "key exists"); - Assert.NotNull(ttl); - Assert.Equal("some value", fullWait.Result); - - // wait for expiry - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => !cache.KeyExists(key)).ForAwait(); - - // test once expired - keyExists = cache.KeyExists(key); - ttl = cache.KeyTimeToLive(key); - fullWait = cache.HashGetAsync(key, "full", flags: CommandFlags.None); - - Assert.False(keyExists); - Assert.Null(ttl); - var r = await fullWait; - Assert.True(r.IsNull); - Assert.Null((string?)r); - } - } + using var conn = Create(); + + var key = Me(); + var db = conn.GetDatabase(); + + // setup some data + db.KeyDelete(key, CommandFlags.FireAndForget); + db.HashSet(key, "full", "some value", flags: CommandFlags.FireAndForget); + db.KeyExpire(key, TimeSpan.FromSeconds(4), CommandFlags.FireAndForget); + + // test while exists + var keyExists = db.KeyExists(key); + var ttl = db.KeyTimeToLive(key); + var fullWait = db.HashGetAsync(key, "full", flags: CommandFlags.None); + Assert.True(keyExists, "key exists"); + Assert.NotNull(ttl); + Assert.Equal("some value", fullWait.Result); + + // wait for expiry + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => !db.KeyExists(key)).ForAwait(); + + // test once expired + keyExists = db.KeyExists(key); + ttl = db.KeyTimeToLive(key); + fullWait = db.HashGetAsync(key, "full", flags: CommandFlags.None); + + Assert.False(keyExists); + Assert.Null(ttl); + var r = await fullWait; + Assert.True(r.IsNull); + Assert.Null((string?)r); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/SO25113323.cs b/tests/StackExchange.Redis.Tests/Issues/SO25113323.cs index fb72d2007..b38743848 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO25113323.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO25113323.cs @@ -3,40 +3,38 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class SO25113323 : TestBase { - public class SO25113323 : TestBase + public SO25113323(ITestOutputHelper output) : base (output) { } + + [Fact] + public async Task SetExpirationToPassed() { - public SO25113323(ITestOutputHelper output) : base (output) { } - - [Fact] - public async Task SetExpirationToPassed() - { - var key = Me(); - using (var conn = Create()) - { - // Given - var cache = conn.GetDatabase(); - cache.KeyDelete(key, CommandFlags.FireAndForget); - cache.HashSet(key, "full", "test", When.NotExists, CommandFlags.PreferMaster); - - await Task.Delay(2000).ForAwait(); - - // When - var serverTime = GetServer(conn).Time(); - var expiresOn = serverTime.AddSeconds(-2); - - var firstResult = cache.KeyExpire(key, expiresOn, CommandFlags.PreferMaster); - var secondResult = cache.KeyExpire(key, expiresOn, CommandFlags.PreferMaster); - var exists = cache.KeyExists(key); - var ttl = cache.KeyTimeToLive(key); - - // Then - Assert.True(firstResult); // could set the first time, but this nukes the key - Assert.False(secondResult); // can't set, since nuked - Assert.False(exists); // does not exist since nuked - Assert.Null(ttl); // no expiry since nuked - } - } + using var conn = Create(); + + // Given + var key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.HashSet(key, "full", "test", When.NotExists, CommandFlags.PreferMaster); + + await Task.Delay(2000).ForAwait(); + + // When + var serverTime = GetServer(conn).Time(); + var expiresOn = serverTime.AddSeconds(-2); + + var firstResult = db.KeyExpire(key, expiresOn, CommandFlags.PreferMaster); + var secondResult = db.KeyExpire(key, expiresOn, CommandFlags.PreferMaster); + var exists = db.KeyExists(key); + var ttl = db.KeyTimeToLive(key); + + // Then + Assert.True(firstResult); // could set the first time, but this nukes the key + Assert.False(secondResult); // can't set, since nuked + Assert.False(exists); // does not exist since nuked + Assert.Null(ttl); // no expiry since nuked } } diff --git a/tests/StackExchange.Redis.Tests/Issues/SO25567566.cs b/tests/StackExchange.Redis.Tests/Issues/SO25567566.cs index 363419ae8..86be32862 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO25567566.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO25567566.cs @@ -3,73 +3,71 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +public class SO25567566 : TestBase { - public class SO25567566 : TestBase + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; + public SO25567566(ITestOutputHelper output) : base(output) { } + + [FactLongRunning] + public async Task Execute() { - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public SO25567566(ITestOutputHelper output) : base(output) { } + using var conn = ConnectionMultiplexer.Connect(GetConfiguration()); - [FactLongRunning] - public async Task Execute() + for (int i = 0; i < 100; i++) { - using (var conn = ConnectionMultiplexer.Connect(GetConfiguration())) // Create()) - { - for (int i = 0; i < 100; i++) - { - Assert.Equal("ok", await DoStuff(conn).ForAwait()); - } - } + Assert.Equal("ok", await DoStuff(conn).ForAwait()); } + } - private static async Task DoStuff(ConnectionMultiplexer conn) - { - var db = conn.GetDatabase(); + private static async Task DoStuff(ConnectionMultiplexer conn) + { + var db = conn.GetDatabase(); - var timeout = Task.Delay(5000); - var key = Me(); - var key2 = key + "2"; - var len = db.ListLengthAsync(key); + var timeout = Task.Delay(5000); + var key = Me(); + var key2 = key + "2"; + var len = db.ListLengthAsync(key); - if (await Task.WhenAny(timeout, len).ForAwait() != len) - { - return "Timeout getting length"; - } + if (await Task.WhenAny(timeout, len).ForAwait() != len) + { + return "Timeout getting length"; + } - if ((await len.ForAwait()) == 0) - { - db.ListRightPush(key, "foo", flags: CommandFlags.FireAndForget); - } - var tran = db.CreateTransaction(); - var x = tran.ListRightPopLeftPushAsync(key, key2); - var y = tran.SetAddAsync(key + "set", "bar"); - var z = tran.KeyExpireAsync(key2, TimeSpan.FromSeconds(60)); - timeout = Task.Delay(5000); + if ((await len.ForAwait()) == 0) + { + db.ListRightPush(key, "foo", flags: CommandFlags.FireAndForget); + } + var tran = db.CreateTransaction(); + var x = tran.ListRightPopLeftPushAsync(key, key2); + var y = tran.SetAddAsync(key + "set", "bar"); + var z = tran.KeyExpireAsync(key2, TimeSpan.FromSeconds(60)); + timeout = Task.Delay(5000); - var exec = tran.ExecuteAsync(); - // SWAP THESE TWO - bool ok = await Task.WhenAny(exec, timeout).ForAwait() == exec; - //bool ok = true; + var exec = tran.ExecuteAsync(); + // SWAP THESE TWO + bool ok = await Task.WhenAny(exec, timeout).ForAwait() == exec; + //bool ok = true; - if (ok) + if (ok) + { + if (await exec.ForAwait()) { - if (await exec.ForAwait()) - { - await Task.WhenAll(x, y, z).ForAwait(); + await Task.WhenAll(x, y, z).ForAwait(); - var db2 = conn.GetDatabase(); - db2.HashGet(key + "hash", "whatever"); - return "ok"; - } - else - { - return "Transaction aborted"; - } + var db2 = conn.GetDatabase(); + db2.HashGet(key + "hash", "whatever"); + return "ok"; } else { - return "Timeout during exec"; + return "Transaction aborted"; } } + else + { + return "Timeout during exec"; + } } } diff --git a/tests/StackExchange.Redis.Tests/Keys.cs b/tests/StackExchange.Redis.Tests/Keys.cs index 0032611a3..65dd48c51 100644 --- a/tests/StackExchange.Redis.Tests/Keys.cs +++ b/tests/StackExchange.Redis.Tests/Keys.cs @@ -5,307 +5,295 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Keys : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Keys : TestBase + public Keys(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + + [Fact] + public void TestScan() { - public Keys(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + using var conn = Create(allowAdmin: true); - [Fact] - public void TestScan() - { - using (var muxer = Create(allowAdmin: true)) - { - var dbId = TestConfig.GetDedicatedDB(); - var db = muxer.GetDatabase(dbId); - var server = GetAnyPrimary(muxer); - var prefix = Me(); - server.FlushDatabase(dbId, flags: CommandFlags.FireAndForget); - - const int Count = 1000; - for (int i = 0; i < Count; i++) - db.StringSet(prefix + "x" + i, "y" + i, flags: CommandFlags.FireAndForget); - - var count = server.Keys(dbId, prefix + "*").Count(); - Assert.Equal(Count, count); - } - } + var dbId = TestConfig.GetDedicatedDB(); + var db = conn.GetDatabase(dbId); + var server = GetAnyPrimary(conn); + var prefix = Me(); + server.FlushDatabase(dbId, flags: CommandFlags.FireAndForget); - [Fact] - public void FlushFetchRandomKey() - { - using (var conn = Create(allowAdmin: true)) - { - var dbId = TestConfig.GetDedicatedDB(conn); - Skip.IfMissingDatabase(conn, dbId); - var db = conn.GetDatabase(dbId); - var prefix = Me(); - conn.GetServer(TestConfig.Current.PrimaryServerAndPort).FlushDatabase(dbId, CommandFlags.FireAndForget); - string? anyKey = db.KeyRandom(); - - Assert.Null(anyKey); - db.StringSet(prefix + "abc", "def"); - byte[]? keyBytes = db.KeyRandom(); - - Assert.NotNull(keyBytes); - Assert.Equal(prefix + "abc", Encoding.UTF8.GetString(keyBytes)); - } - } + const int Count = 1000; + for (int i = 0; i < Count; i++) + db.StringSet(prefix + "x" + i, "y" + i, flags: CommandFlags.FireAndForget); - [Fact] - public void Zeros() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - var key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, 123, flags: CommandFlags.FireAndForget); - int k = (int)db.StringGet(key); - Assert.Equal(123, k); - - db.KeyDelete(key, CommandFlags.FireAndForget); - int i = (int)db.StringGet(key); - Assert.Equal(0, i); - - Assert.True(db.StringGet(key).IsNull); - int? value = (int?)db.StringGet(key); - Assert.False(value.HasValue); - } - } + var count = server.Keys(dbId, prefix + "*").Count(); + Assert.Equal(Count, count); + } - [Fact] - public void PrependAppend() - { - { - // simple - RedisKey key = "world"; - var ret = key.Prepend("hello"); - Assert.Equal("helloworld", ret); - } - - { - RedisKey key1 = "world"; - RedisKey key2 = Encoding.UTF8.GetBytes("hello"); - var key3 = key1.Prepend(key2); - Assert.True(ReferenceEquals(key1.KeyValue, key3.KeyValue)); - Assert.True(ReferenceEquals(key2.KeyValue, key3.KeyPrefix)); - Assert.Equal("helloworld", key3); - } - - { - RedisKey key = "hello"; - var ret = key.Append("world"); - Assert.Equal("helloworld", ret); - } - - { - RedisKey key1 = Encoding.UTF8.GetBytes("hello"); - RedisKey key2 = "world"; - var key3 = key1.Append(key2); - Assert.True(ReferenceEquals(key2.KeyValue, key3.KeyValue)); - Assert.True(ReferenceEquals(key1.KeyValue, key3.KeyPrefix)); - Assert.Equal("helloworld", key3); - } - } + [Fact] + public void FlushFetchRandomKey() + { + using var conn = Create(allowAdmin: true); - [Fact] - public void Exists() - { - using (var muxer = Create()) - { - RedisKey key = Me(); - RedisKey key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - - Assert.False(db.KeyExists(key)); - Assert.False(db.KeyExists(key2)); - Assert.Equal(0, db.KeyExists(new[] { key, key2 })); - - db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - Assert.True(db.KeyExists(key)); - Assert.False(db.KeyExists(key2)); - Assert.Equal(1, db.KeyExists(new[] { key, key2 })); - - db.StringSet(key2, "new value", flags: CommandFlags.FireAndForget); - Assert.True(db.KeyExists(key)); - Assert.True(db.KeyExists(key2)); - Assert.Equal(2, db.KeyExists(new[] { key, key2 })); - } - } + var dbId = TestConfig.GetDedicatedDB(conn); + Skip.IfMissingDatabase(conn, dbId); + var db = conn.GetDatabase(dbId); + var prefix = Me(); + conn.GetServer(TestConfig.Current.PrimaryServerAndPort).FlushDatabase(dbId, CommandFlags.FireAndForget); + string? anyKey = db.KeyRandom(); - [Fact] - public async Task ExistsAsync() - { - using (var muxer = Create()) - { - RedisKey key = Me(); - RedisKey key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - var a1 = db.KeyExistsAsync(key).ForAwait(); - var a2 = db.KeyExistsAsync(key2).ForAwait(); - var a3 = db.KeyExistsAsync(new[] { key, key2 }).ForAwait(); - - db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - - var b1 = db.KeyExistsAsync(key).ForAwait(); - var b2 = db.KeyExistsAsync(key2).ForAwait(); - var b3 = db.KeyExistsAsync(new[] { key, key2 }).ForAwait(); - - db.StringSet(key2, "new value", flags: CommandFlags.FireAndForget); - - var c1 = db.KeyExistsAsync(key).ForAwait(); - var c2 = db.KeyExistsAsync(key2).ForAwait(); - var c3 = db.KeyExistsAsync(new[] { key, key2 }).ForAwait(); - - Assert.False(await a1); - Assert.False(await a2); - Assert.Equal(0, await a3); - - Assert.True(await b1); - Assert.False(await b2); - Assert.Equal(1, await b3); - - Assert.True(await c1); - Assert.True(await c2); - Assert.Equal(2, await c3); - } - } + Assert.Null(anyKey); + db.StringSet(prefix + "abc", "def"); + byte[]? keyBytes = db.KeyRandom(); + + Assert.NotNull(keyBytes); + Assert.Equal(prefix + "abc", Encoding.UTF8.GetString(keyBytes)); + } + + [Fact] + public void Zeros() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, 123, flags: CommandFlags.FireAndForget); + int k = (int)db.StringGet(key); + Assert.Equal(123, k); + + db.KeyDelete(key, CommandFlags.FireAndForget); + int i = (int)db.StringGet(key); + Assert.Equal(0, i); + + Assert.True(db.StringGet(key).IsNull); + int? value = (int?)db.StringGet(key); + Assert.False(value.HasValue); + } - [Fact] - public async Task IdleTime() + [Fact] + public void PrependAppend() + { { - using (var muxer = Create()) - { - RedisKey key = Me(); - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - await Task.Delay(2000).ForAwait(); - var idleTime = db.KeyIdleTime(key); - Assert.True(idleTime > TimeSpan.Zero); - - db.StringSet(key, "new value2", flags: CommandFlags.FireAndForget); - var idleTime2 = db.KeyIdleTime(key); - Assert.True(idleTime2 < idleTime); - - db.KeyDelete(key); - var idleTime3 = db.KeyIdleTime(key); - Assert.Null(idleTime3); - } + // simple + RedisKey key = "world"; + var ret = key.Prepend("hello"); + Assert.Equal("helloworld", ret); } - [Fact] - public async Task TouchIdleTime() { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v3_2_1); - - RedisKey key = Me(); - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - await Task.Delay(2000).ForAwait(); - var idleTime = db.KeyIdleTime(key); - Assert.True(idleTime > TimeSpan.Zero); - - Assert.True(db.KeyTouch(key)); - var idleTime1 = db.KeyIdleTime(key); - Assert.True(idleTime1 < idleTime); - } + RedisKey key1 = "world"; + RedisKey key2 = Encoding.UTF8.GetBytes("hello"); + var key3 = key1.Prepend(key2); + Assert.True(ReferenceEquals(key1.KeyValue, key3.KeyValue)); + Assert.True(ReferenceEquals(key2.KeyValue, key3.KeyPrefix)); + Assert.Equal("helloworld", key3); } - [Fact] - public async Task IdleTimeAsync() { - using (var muxer = Create()) - { - RedisKey key = Me(); - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - await Task.Delay(2000).ForAwait(); - var idleTime = await db.KeyIdleTimeAsync(key).ForAwait(); - Assert.True(idleTime > TimeSpan.Zero); - - db.StringSet(key, "new value2", flags: CommandFlags.FireAndForget); - var idleTime2 = await db.KeyIdleTimeAsync(key).ForAwait(); - Assert.True(idleTime2 < idleTime); - - db.KeyDelete(key); - var idleTime3 = await db.KeyIdleTimeAsync(key).ForAwait(); - Assert.Null(idleTime3); - } + RedisKey key = "hello"; + var ret = key.Append("world"); + Assert.Equal("helloworld", ret); } - [Fact] - public async Task TouchIdleTimeAsync() { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v3_2_1); - - RedisKey key = Me(); - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - await Task.Delay(2000).ForAwait(); - var idleTime = await db.KeyIdleTimeAsync(key).ForAwait(); - Assert.True(idleTime > TimeSpan.Zero); - - Assert.True(await db.KeyTouchAsync(key).ForAwait()); - var idleTime1 = await db.KeyIdleTimeAsync(key).ForAwait(); - Assert.True(idleTime1 < idleTime); - } + RedisKey key1 = Encoding.UTF8.GetBytes("hello"); + RedisKey key2 = "world"; + var key3 = key1.Append(key2); + Assert.True(ReferenceEquals(key2.KeyValue, key3.KeyValue)); + Assert.True(ReferenceEquals(key1.KeyValue, key3.KeyPrefix)); + Assert.Equal("helloworld", key3); } + } - [Fact] - public async Task KeyEncoding() - { - using var muxer = Create(); - var key = Me(); - var db = muxer.GetDatabase(); + [Fact] + public void Exists() + { + using var conn = Create(); + + RedisKey key = Me(); + RedisKey key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + + Assert.False(db.KeyExists(key)); + Assert.False(db.KeyExists(key2)); + Assert.Equal(0, db.KeyExists(new[] { key, key2 })); + + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + Assert.True(db.KeyExists(key)); + Assert.False(db.KeyExists(key2)); + Assert.Equal(1, db.KeyExists(new[] { key, key2 })); + + db.StringSet(key2, "new value", flags: CommandFlags.FireAndForget); + Assert.True(db.KeyExists(key)); + Assert.True(db.KeyExists(key2)); + Assert.Equal(2, db.KeyExists(new[] { key, key2 })); + } - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + [Fact] + public async Task ExistsAsync() + { + using var conn = Create(); - Assert.Equal("embstr", db.KeyEncoding(key)); - Assert.Equal("embstr", await db.KeyEncodingAsync(key)); + RedisKey key = Me(); + RedisKey key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + var a1 = db.KeyExistsAsync(key).ForAwait(); + var a2 = db.KeyExistsAsync(key2).ForAwait(); + var a3 = db.KeyExistsAsync(new[] { key, key2 }).ForAwait(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.ListLeftPush(key, "new value", flags: CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - // Depending on server version, this is going to vary - we're sanity checking here. - var listTypes = new [] { "ziplist", "quicklist" }; - Assert.Contains(db.KeyEncoding(key), listTypes); - Assert.Contains(await db.KeyEncodingAsync(key), listTypes); + var b1 = db.KeyExistsAsync(key).ForAwait(); + var b2 = db.KeyExistsAsync(key2).ForAwait(); + var b3 = db.KeyExistsAsync(new[] { key, key2 }).ForAwait(); - var keyNotExists = key + "no-exist"; - Assert.Null(db.KeyEncoding(keyNotExists)); - Assert.Null(await db.KeyEncodingAsync(keyNotExists)); - } + db.StringSet(key2, "new value", flags: CommandFlags.FireAndForget); - [Fact] - public async Task KeyRefCount() - { - using var muxer = Create(); - var key = Me(); - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - - Assert.Equal(1, db.KeyRefCount(key)); - Assert.Equal(1, await db.KeyRefCountAsync(key)); - - var keyNotExists = key + "no-exist"; - Assert.Null(db.KeyRefCount(keyNotExists)); - Assert.Null(await db.KeyRefCountAsync(keyNotExists)); - } + var c1 = db.KeyExistsAsync(key).ForAwait(); + var c2 = db.KeyExistsAsync(key2).ForAwait(); + var c3 = db.KeyExistsAsync(new[] { key, key2 }).ForAwait(); + + Assert.False(await a1); + Assert.False(await a2); + Assert.Equal(0, await a3); + + Assert.True(await b1); + Assert.False(await b2); + Assert.Equal(1, await b3); + + Assert.True(await c1); + Assert.True(await c2); + Assert.Equal(2, await c3); + } + + [Fact] + public async Task IdleTime() + { + using var conn = Create(); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + await Task.Delay(2000).ForAwait(); + var idleTime = db.KeyIdleTime(key); + Assert.True(idleTime > TimeSpan.Zero); + + db.StringSet(key, "new value2", flags: CommandFlags.FireAndForget); + var idleTime2 = db.KeyIdleTime(key); + Assert.True(idleTime2 < idleTime); + + db.KeyDelete(key); + var idleTime3 = db.KeyIdleTime(key); + Assert.Null(idleTime3); + } + + [Fact] + public async Task TouchIdleTime() + { + using var conn = Create(require: RedisFeatures.v3_2_1); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + await Task.Delay(2000).ForAwait(); + var idleTime = db.KeyIdleTime(key); + Assert.True(idleTime > TimeSpan.Zero); + + Assert.True(db.KeyTouch(key)); + var idleTime1 = db.KeyIdleTime(key); + Assert.True(idleTime1 < idleTime); + } + + [Fact] + public async Task IdleTimeAsync() + { + using var conn = Create(); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + await Task.Delay(2000).ForAwait(); + var idleTime = await db.KeyIdleTimeAsync(key).ForAwait(); + Assert.True(idleTime > TimeSpan.Zero); + + db.StringSet(key, "new value2", flags: CommandFlags.FireAndForget); + var idleTime2 = await db.KeyIdleTimeAsync(key).ForAwait(); + Assert.True(idleTime2 < idleTime); + + db.KeyDelete(key); + var idleTime3 = await db.KeyIdleTimeAsync(key).ForAwait(); + Assert.Null(idleTime3); + } + + [Fact] + public async Task TouchIdleTimeAsync() + { + using var conn = Create(require: RedisFeatures.v3_2_1); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + await Task.Delay(2000).ForAwait(); + var idleTime = await db.KeyIdleTimeAsync(key).ForAwait(); + Assert.True(idleTime > TimeSpan.Zero); + + Assert.True(await db.KeyTouchAsync(key).ForAwait()); + var idleTime1 = await db.KeyIdleTimeAsync(key).ForAwait(); + Assert.True(idleTime1 < idleTime); + } + + [Fact] + public async Task KeyEncoding() + { + using var conn = Create(); + + var key = Me(); + var db = conn.GetDatabase(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + + Assert.Equal("embstr", db.KeyEncoding(key)); + Assert.Equal("embstr", await db.KeyEncodingAsync(key)); + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.ListLeftPush(key, "new value", flags: CommandFlags.FireAndForget); + + // Depending on server version, this is going to vary - we're sanity checking here. + var listTypes = new [] { "ziplist", "quicklist" }; + Assert.Contains(db.KeyEncoding(key), listTypes); + Assert.Contains(await db.KeyEncodingAsync(key), listTypes); + + var keyNotExists = key + "no-exist"; + Assert.Null(db.KeyEncoding(keyNotExists)); + Assert.Null(await db.KeyEncodingAsync(keyNotExists)); + } + + [Fact] + public async Task KeyRefCount() + { + using var conn = Create(); + + var key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + + Assert.Equal(1, db.KeyRefCount(key)); + Assert.Equal(1, await db.KeyRefCountAsync(key)); + + var keyNotExists = key + "no-exist"; + Assert.Null(db.KeyRefCount(keyNotExists)); + Assert.Null(await db.KeyRefCountAsync(keyNotExists)); } } diff --git a/tests/StackExchange.Redis.Tests/KeysAndValues.cs b/tests/StackExchange.Redis.Tests/KeysAndValues.cs index b57cbe690..ad8cc7ae7 100644 --- a/tests/StackExchange.Redis.Tests/KeysAndValues.cs +++ b/tests/StackExchange.Redis.Tests/KeysAndValues.cs @@ -3,175 +3,174 @@ using System.Globalization; using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class KeysAndValues { - public class KeysAndValues + [Fact] + public void TestValues() + { + RedisValue @default = default(RedisValue); + CheckNull(@default); + + RedisValue nullString = (string?)null; + CheckNull(nullString); + + RedisValue nullBlob = (byte[]?)null; + CheckNull(nullBlob); + + RedisValue emptyString = ""; + CheckNotNull(emptyString); + + RedisValue emptyBlob = Array.Empty(); + CheckNotNull(emptyBlob); + + RedisValue a0 = new string('a', 1); + CheckNotNull(a0); + RedisValue a1 = new string('a', 1); + CheckNotNull(a1); + RedisValue b0 = new[] { (byte)'b' }; + CheckNotNull(b0); + RedisValue b1 = new[] { (byte)'b' }; + CheckNotNull(b1); + + RedisValue i4 = 1; + CheckNotNull(i4); + RedisValue i8 = 1L; + CheckNotNull(i8); + + RedisValue bool1 = true; + CheckNotNull(bool1); + RedisValue bool2 = false; + CheckNotNull(bool2); + RedisValue bool3 = true; + CheckNotNull(bool3); + + CheckSame(a0, a0); + CheckSame(a1, a1); + CheckSame(a0, a1); + + CheckSame(b0, b0); + CheckSame(b1, b1); + CheckSame(b0, b1); + + CheckSame(i4, i4); + CheckSame(i8, i8); + CheckSame(i4, i8); + + CheckSame(bool1, bool3); + CheckNotSame(bool1, bool2); + } + + internal static void CheckSame(RedisValue x, RedisValue y) + { + Assert.True(Equals(x, y), "Equals(x, y)"); + Assert.True(Equals(y, x), "Equals(y, x)"); + Assert.True(EqualityComparer.Default.Equals(x, y), "EQ(x,y)"); + Assert.True(EqualityComparer.Default.Equals(y, x), "EQ(y,x)"); + Assert.True(x == y, "x==y"); + Assert.True(y == x, "y==x"); + Assert.False(x != y, "x!=y"); + Assert.False(y != x, "y!=x"); + Assert.True(x.Equals(y),"x.EQ(y)"); + Assert.True(y.Equals(x), "y.EQ(x)"); + Assert.True(x.GetHashCode() == y.GetHashCode(), "GetHashCode"); + } + + private static void CheckNotSame(RedisValue x, RedisValue y) + { + Assert.False(Equals(x, y)); + Assert.False(Equals(y, x)); + Assert.False(EqualityComparer.Default.Equals(x, y)); + Assert.False(EqualityComparer.Default.Equals(y, x)); + Assert.False(x == y); + Assert.False(y == x); + Assert.True(x != y); + Assert.True(y != x); + Assert.False(x.Equals(y)); + Assert.False(y.Equals(x)); + Assert.False(x.GetHashCode() == y.GetHashCode()); // well, very unlikely + } + + private static void CheckNotNull(RedisValue value) + { + Assert.False(value.IsNull); + Assert.NotNull((byte[]?)value); + Assert.NotNull((string?)value); + Assert.NotEqual(-1, value.GetHashCode()); + + Assert.NotNull((string?)value); + Assert.NotNull((byte[]?)value); + + CheckSame(value, value); + CheckNotSame(value, default(RedisValue)); + CheckNotSame(value, (string?)null); + CheckNotSame(value, (byte[]?)null); + } + + internal static void CheckNull(RedisValue value) + { + Assert.True(value.IsNull); + Assert.True(value.IsNullOrEmpty); + Assert.False(value.IsInteger); + Assert.Equal(-1, value.GetHashCode()); + + Assert.Null((string?)value); + Assert.Null((byte[]?)value); + + Assert.Equal(0, (int)value); + Assert.Equal(0L, (long)value); + + CheckSame(value, value); + //CheckSame(value, default(RedisValue)); + //CheckSame(value, (string)null); + //CheckSame(value, (byte[])null); + } + + [Fact] + public void ValuesAreConvertible() + { + RedisValue val = 123; + object o = val; + byte[] blob = (byte[])Convert.ChangeType(o, typeof(byte[])); + + Assert.Equal(3, blob.Length); + Assert.Equal((byte)'1', blob[0]); + Assert.Equal((byte)'2', blob[1]); + Assert.Equal((byte)'3', blob[2]); + + Assert.Equal(123, Convert.ToDouble(o)); + + IConvertible c = (IConvertible)o; + // ReSharper disable RedundantCast + Assert.Equal((short)123, c.ToInt16(CultureInfo.InvariantCulture)); + Assert.Equal((int)123, c.ToInt32(CultureInfo.InvariantCulture)); + Assert.Equal((long)123, c.ToInt64(CultureInfo.InvariantCulture)); + Assert.Equal((float)123, c.ToSingle(CultureInfo.InvariantCulture)); + Assert.Equal("123", c.ToString(CultureInfo.InvariantCulture)); + Assert.Equal((double)123, c.ToDouble(CultureInfo.InvariantCulture)); + Assert.Equal((decimal)123, c.ToDecimal(CultureInfo.InvariantCulture)); + Assert.Equal((ushort)123, c.ToUInt16(CultureInfo.InvariantCulture)); + Assert.Equal((uint)123, c.ToUInt32(CultureInfo.InvariantCulture)); + Assert.Equal((ulong)123, c.ToUInt64(CultureInfo.InvariantCulture)); + + blob = (byte[])c.ToType(typeof(byte[]), CultureInfo.InvariantCulture); + Assert.Equal(3, blob.Length); + Assert.Equal((byte)'1', blob[0]); + Assert.Equal((byte)'2', blob[1]); + Assert.Equal((byte)'3', blob[2]); + } + + [Fact] + public void CanBeDynamic() { - [Fact] - public void TestValues() - { - RedisValue @default = default(RedisValue); - CheckNull(@default); - - RedisValue nullString = (string?)null; - CheckNull(nullString); - - RedisValue nullBlob = (byte[]?)null; - CheckNull(nullBlob); - - RedisValue emptyString = ""; - CheckNotNull(emptyString); - - RedisValue emptyBlob = Array.Empty(); - CheckNotNull(emptyBlob); - - RedisValue a0 = new string('a', 1); - CheckNotNull(a0); - RedisValue a1 = new string('a', 1); - CheckNotNull(a1); - RedisValue b0 = new[] { (byte)'b' }; - CheckNotNull(b0); - RedisValue b1 = new[] { (byte)'b' }; - CheckNotNull(b1); - - RedisValue i4 = 1; - CheckNotNull(i4); - RedisValue i8 = 1L; - CheckNotNull(i8); - - RedisValue bool1 = true; - CheckNotNull(bool1); - RedisValue bool2 = false; - CheckNotNull(bool2); - RedisValue bool3 = true; - CheckNotNull(bool3); - - CheckSame(a0, a0); - CheckSame(a1, a1); - CheckSame(a0, a1); - - CheckSame(b0, b0); - CheckSame(b1, b1); - CheckSame(b0, b1); - - CheckSame(i4, i4); - CheckSame(i8, i8); - CheckSame(i4, i8); - - CheckSame(bool1, bool3); - CheckNotSame(bool1, bool2); - } - - internal static void CheckSame(RedisValue x, RedisValue y) - { - Assert.True(Equals(x, y), "Equals(x, y)"); - Assert.True(Equals(y, x), "Equals(y, x)"); - Assert.True(EqualityComparer.Default.Equals(x, y), "EQ(x,y)"); - Assert.True(EqualityComparer.Default.Equals(y, x), "EQ(y,x)"); - Assert.True(x == y, "x==y"); - Assert.True(y == x, "y==x"); - Assert.False(x != y, "x!=y"); - Assert.False(y != x, "y!=x"); - Assert.True(x.Equals(y),"x.EQ(y)"); - Assert.True(y.Equals(x), "y.EQ(x)"); - Assert.True(x.GetHashCode() == y.GetHashCode(), "GetHashCode"); - } - - private static void CheckNotSame(RedisValue x, RedisValue y) - { - Assert.False(Equals(x, y)); - Assert.False(Equals(y, x)); - Assert.False(EqualityComparer.Default.Equals(x, y)); - Assert.False(EqualityComparer.Default.Equals(y, x)); - Assert.False(x == y); - Assert.False(y == x); - Assert.True(x != y); - Assert.True(y != x); - Assert.False(x.Equals(y)); - Assert.False(y.Equals(x)); - Assert.False(x.GetHashCode() == y.GetHashCode()); // well, very unlikely - } - - private static void CheckNotNull(RedisValue value) - { - Assert.False(value.IsNull); - Assert.NotNull((byte[]?)value); - Assert.NotNull((string?)value); - Assert.NotEqual(-1, value.GetHashCode()); - - Assert.NotNull((string?)value); - Assert.NotNull((byte[]?)value); - - CheckSame(value, value); - CheckNotSame(value, default(RedisValue)); - CheckNotSame(value, (string?)null); - CheckNotSame(value, (byte[]?)null); - } - - internal static void CheckNull(RedisValue value) - { - Assert.True(value.IsNull); - Assert.True(value.IsNullOrEmpty); - Assert.False(value.IsInteger); - Assert.Equal(-1, value.GetHashCode()); - - Assert.Null((string?)value); - Assert.Null((byte[]?)value); - - Assert.Equal(0, (int)value); - Assert.Equal(0L, (long)value); - - CheckSame(value, value); - //CheckSame(value, default(RedisValue)); - //CheckSame(value, (string)null); - //CheckSame(value, (byte[])null); - } - - [Fact] - public void ValuesAreConvertible() - { - RedisValue val = 123; - object o = val; - byte[] blob = (byte[])Convert.ChangeType(o, typeof(byte[])); - - Assert.Equal(3, blob.Length); - Assert.Equal((byte)'1', blob[0]); - Assert.Equal((byte)'2', blob[1]); - Assert.Equal((byte)'3', blob[2]); - - Assert.Equal(123, Convert.ToDouble(o)); - - IConvertible c = (IConvertible)o; - // ReSharper disable RedundantCast - Assert.Equal((short)123, c.ToInt16(CultureInfo.InvariantCulture)); - Assert.Equal((int)123, c.ToInt32(CultureInfo.InvariantCulture)); - Assert.Equal((long)123, c.ToInt64(CultureInfo.InvariantCulture)); - Assert.Equal((float)123, c.ToSingle(CultureInfo.InvariantCulture)); - Assert.Equal("123", c.ToString(CultureInfo.InvariantCulture)); - Assert.Equal((double)123, c.ToDouble(CultureInfo.InvariantCulture)); - Assert.Equal((decimal)123, c.ToDecimal(CultureInfo.InvariantCulture)); - Assert.Equal((ushort)123, c.ToUInt16(CultureInfo.InvariantCulture)); - Assert.Equal((uint)123, c.ToUInt32(CultureInfo.InvariantCulture)); - Assert.Equal((ulong)123, c.ToUInt64(CultureInfo.InvariantCulture)); - - blob = (byte[])c.ToType(typeof(byte[]), CultureInfo.InvariantCulture); - Assert.Equal(3, blob.Length); - Assert.Equal((byte)'1', blob[0]); - Assert.Equal((byte)'2', blob[1]); - Assert.Equal((byte)'3', blob[2]); - } - - [Fact] - public void CanBeDynamic() - { - RedisValue val = "abc"; - object o = val; - dynamic d = o; - byte[] blob = (byte[])d; // could be in a try/catch - Assert.Equal(3, blob.Length); - Assert.Equal((byte)'a', blob[0]); - Assert.Equal((byte)'b', blob[1]); - Assert.Equal((byte)'c', blob[2]); - } + RedisValue val = "abc"; + object o = val; + dynamic d = o; + byte[] blob = (byte[])d; // could be in a try/catch + Assert.Equal(3, blob.Length); + Assert.Equal((byte)'a', blob[0]); + Assert.Equal((byte)'b', blob[1]); + Assert.Equal((byte)'c', blob[2]); } } diff --git a/tests/StackExchange.Redis.Tests/Latency.cs b/tests/StackExchange.Redis.Tests/Latency.cs index 31c1f3c1d..1d49300c4 100644 --- a/tests/StackExchange.Redis.Tests/Latency.cs +++ b/tests/StackExchange.Redis.Tests/Latency.cs @@ -2,86 +2,81 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Latency : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Latency : TestBase + public Latency(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public async Task CanCallDoctor() { - public Latency(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - - [Fact] - public async Task CanCallDoctor() - { - using (var conn = Create()) - { - var server = conn.GetServer(conn.GetEndPoints()[0]); - string? doctor = server.LatencyDoctor(); - Assert.NotNull(doctor); - Assert.NotEqual("", doctor); - - doctor = await server.LatencyDoctorAsync(); - Assert.NotNull(doctor); - Assert.NotEqual("", doctor); - } - } - - [Fact] - public async Task CanReset() - { - using (var conn = Create()) - { - var server = conn.GetServer(conn.GetEndPoints()[0]); - _ = server.LatencyReset(); - var count = await server.LatencyResetAsync(new [] { "command" }); - Assert.Equal(0, count); - - count = await server.LatencyResetAsync(new [] { "command", "fast-command" }); - Assert.Equal(0, count); - } - } - - [Fact] - public async Task GetLatest() - { - using (var conn = Create(allowAdmin: true)) - { - var server = conn.GetServer(conn.GetEndPoints()[0]); - server.ConfigSet("latency-monitor-threshold", 100); - server.LatencyReset(); - var arr = server.LatencyLatest(); - Assert.Empty(arr); - - var now = await server.TimeAsync(); - server.Execute("debug", "sleep", "0.5"); // cause something to be slow - - arr = await server.LatencyLatestAsync(); - var item = Assert.Single(arr); - Assert.Equal("command", item.EventName); - Assert.True(item.DurationMilliseconds >= 400 && item.DurationMilliseconds <= 600); - Assert.Equal(item.DurationMilliseconds, item.MaxDurationMilliseconds); - Assert.True(item.Timestamp >= now.AddSeconds(-2) && item.Timestamp <= now.AddSeconds(2)); - } - } - - [Fact] - public async Task GetHistory() - { - using (var conn = Create(allowAdmin: true)) - { - var server = conn.GetServer(conn.GetEndPoints()[0]); - server.ConfigSet("latency-monitor-threshold", 100); - server.LatencyReset(); - var arr = server.LatencyHistory("command"); - Assert.Empty(arr); - - var now = await server.TimeAsync(); - server.Execute("debug", "sleep", "0.5"); // cause something to be slow - - arr = await server.LatencyHistoryAsync("command"); - var item = Assert.Single(arr); - Assert.True(item.DurationMilliseconds >= 400 && item.DurationMilliseconds <= 600); - Assert.True(item.Timestamp >= now.AddSeconds(-2) && item.Timestamp <= now.AddSeconds(2)); - } - } + using var conn = Create(); + + var server = conn.GetServer(conn.GetEndPoints()[0]); + string? doctor = server.LatencyDoctor(); + Assert.NotNull(doctor); + Assert.NotEqual("", doctor); + + doctor = await server.LatencyDoctorAsync(); + Assert.NotNull(doctor); + Assert.NotEqual("", doctor); + } + + [Fact] + public async Task CanReset() + { + using var conn = Create(); + + var server = conn.GetServer(conn.GetEndPoints()[0]); + _ = server.LatencyReset(); + var count = await server.LatencyResetAsync(new[] { "command" }); + Assert.Equal(0, count); + + count = await server.LatencyResetAsync(new[] { "command", "fast-command" }); + Assert.Equal(0, count); + } + + [Fact] + public async Task GetLatest() + { + using var conn = Create(allowAdmin: true); + + var server = conn.GetServer(conn.GetEndPoints()[0]); + server.ConfigSet("latency-monitor-threshold", 100); + server.LatencyReset(); + var arr = server.LatencyLatest(); + Assert.Empty(arr); + + var now = await server.TimeAsync(); + server.Execute("debug", "sleep", "0.5"); // cause something to be slow + + arr = await server.LatencyLatestAsync(); + var item = Assert.Single(arr); + Assert.Equal("command", item.EventName); + Assert.True(item.DurationMilliseconds >= 400 && item.DurationMilliseconds <= 600); + Assert.Equal(item.DurationMilliseconds, item.MaxDurationMilliseconds); + Assert.True(item.Timestamp >= now.AddSeconds(-2) && item.Timestamp <= now.AddSeconds(2)); + } + + [Fact] + public async Task GetHistory() + { + using var conn = Create(allowAdmin: true); + + var server = conn.GetServer(conn.GetEndPoints()[0]); + server.ConfigSet("latency-monitor-threshold", 100); + server.LatencyReset(); + var arr = server.LatencyHistory("command"); + Assert.Empty(arr); + + var now = await server.TimeAsync(); + server.Execute("debug", "sleep", "0.5"); // cause something to be slow + + arr = await server.LatencyHistoryAsync("command"); + var item = Assert.Single(arr); + Assert.True(item.DurationMilliseconds >= 400 && item.DurationMilliseconds <= 600); + Assert.True(item.Timestamp >= now.AddSeconds(-2) && item.Timestamp <= now.AddSeconds(2)); } } diff --git a/tests/StackExchange.Redis.Tests/Lex.cs b/tests/StackExchange.Redis.Tests/Lex.cs index 0e56b7dea..3c5c32250 100644 --- a/tests/StackExchange.Redis.Tests/Lex.cs +++ b/tests/StackExchange.Redis.Tests/Lex.cs @@ -1,108 +1,105 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Lex : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Lex : TestBase + public Lex(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public void QueryRangeAndLengthByLex() { - public Lex(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + using var conn = Create(); - [Fact] - public void QueryRangeAndLengthByLex() + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.SortedSetAdd(key, + new[] { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - db.SortedSetAdd(key, - new [] - { - new SortedSetEntry("a", 0), - new SortedSetEntry("b", 0), - new SortedSetEntry("c", 0), - new SortedSetEntry("d", 0), - new SortedSetEntry("e", 0), - new SortedSetEntry("f", 0), - new SortedSetEntry("g", 0), - }, CommandFlags.FireAndForget); - - var set = db.SortedSetRangeByValue(key, default(RedisValue), "c"); - var count = db.SortedSetLengthByValue(key, default(RedisValue), "c"); - Equate(set, count, "a", "b", "c"); - - set = db.SortedSetRangeByValue(key, default(RedisValue), "c", Exclude.Stop); - count = db.SortedSetLengthByValue(key, default(RedisValue), "c", Exclude.Stop); - Equate(set, count, "a", "b"); - - set = db.SortedSetRangeByValue(key, "aaa", "g", Exclude.Stop); - count = db.SortedSetLengthByValue(key, "aaa", "g", Exclude.Stop); - Equate(set, count, "b", "c", "d", "e", "f"); - - set = db.SortedSetRangeByValue(key, "aaa", "g", Exclude.Stop, 1, 3); - Equate(set, set.Length, "c", "d", "e"); - - set = db.SortedSetRangeByValue(key, "aaa", "g", Exclude.Stop, Order.Descending, 1, 3); - Equate(set, set.Length, "e", "d", "c"); - - set = db.SortedSetRangeByValue(key, "g", "aaa", Exclude.Start, Order.Descending, 1, 3); - Equate(set, set.Length, "e", "d", "c"); - - set = db.SortedSetRangeByValue(key, "e", default(RedisValue)); - count = db.SortedSetLengthByValue(key, "e", default(RedisValue)); - Equate(set, count, "e", "f", "g"); - } - } + new SortedSetEntry("a", 0), + new SortedSetEntry("b", 0), + new SortedSetEntry("c", 0), + new SortedSetEntry("d", 0), + new SortedSetEntry("e", 0), + new SortedSetEntry("f", 0), + new SortedSetEntry("g", 0), + }, CommandFlags.FireAndForget); + + var set = db.SortedSetRangeByValue(key, default(RedisValue), "c"); + var count = db.SortedSetLengthByValue(key, default(RedisValue), "c"); + Equate(set, count, "a", "b", "c"); + + set = db.SortedSetRangeByValue(key, default(RedisValue), "c", Exclude.Stop); + count = db.SortedSetLengthByValue(key, default(RedisValue), "c", Exclude.Stop); + Equate(set, count, "a", "b"); + + set = db.SortedSetRangeByValue(key, "aaa", "g", Exclude.Stop); + count = db.SortedSetLengthByValue(key, "aaa", "g", Exclude.Stop); + Equate(set, count, "b", "c", "d", "e", "f"); + + set = db.SortedSetRangeByValue(key, "aaa", "g", Exclude.Stop, 1, 3); + Equate(set, set.Length, "c", "d", "e"); + + set = db.SortedSetRangeByValue(key, "aaa", "g", Exclude.Stop, Order.Descending, 1, 3); + Equate(set, set.Length, "e", "d", "c"); + + set = db.SortedSetRangeByValue(key, "g", "aaa", Exclude.Start, Order.Descending, 1, 3); + Equate(set, set.Length, "e", "d", "c"); + + set = db.SortedSetRangeByValue(key, "e", default(RedisValue)); + count = db.SortedSetLengthByValue(key, "e", default(RedisValue)); + Equate(set, count, "e", "f", "g"); + } + + [Fact] + public void RemoveRangeByLex() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - [Fact] - public void RemoveRangeByLex() + db.SortedSetAdd(key, + new[] { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - db.SortedSetAdd(key, - new [] - { - new SortedSetEntry("aaaa", 0), - new SortedSetEntry("b", 0), - new SortedSetEntry("c", 0), - new SortedSetEntry("d", 0), - new SortedSetEntry("e", 0), - }, CommandFlags.FireAndForget); - db.SortedSetAdd(key, - new [] - { - new SortedSetEntry("foo", 0), - new SortedSetEntry("zap", 0), - new SortedSetEntry("zip", 0), - new SortedSetEntry("ALPHA", 0), - new SortedSetEntry("alpha", 0), - }, CommandFlags.FireAndForget); - - var set = db.SortedSetRangeByRank(key); - Equate(set, set.Length, "ALPHA", "aaaa", "alpha", "b", "c", "d", "e", "foo", "zap", "zip"); - - long removed = db.SortedSetRemoveRangeByValue(key, "alpha", "omega"); - Assert.Equal(6, removed); - - set = db.SortedSetRangeByRank(key); - Equate(set, set.Length, "ALPHA", "aaaa", "zap", "zip"); - } - } + new SortedSetEntry("aaaa", 0), + new SortedSetEntry("b", 0), + new SortedSetEntry("c", 0), + new SortedSetEntry("d", 0), + new SortedSetEntry("e", 0), + }, CommandFlags.FireAndForget); + db.SortedSetAdd(key, + new[] + { + new SortedSetEntry("foo", 0), + new SortedSetEntry("zap", 0), + new SortedSetEntry("zip", 0), + new SortedSetEntry("ALPHA", 0), + new SortedSetEntry("alpha", 0), + }, CommandFlags.FireAndForget); + + var set = db.SortedSetRangeByRank(key); + Equate(set, set.Length, "ALPHA", "aaaa", "alpha", "b", "c", "d", "e", "foo", "zap", "zip"); + + long removed = db.SortedSetRemoveRangeByValue(key, "alpha", "omega"); + Assert.Equal(6, removed); - private static void Equate(RedisValue[] actual, long count, params string[] expected) + set = db.SortedSetRangeByRank(key); + Equate(set, set.Length, "ALPHA", "aaaa", "zap", "zip"); + } + + private static void Equate(RedisValue[] actual, long count, params string[] expected) + { + Assert.Equal(expected.Length, count); + Assert.Equal(expected.Length, actual.Length); + for (int i = 0; i < actual.Length; i++) { - Assert.Equal(expected.Length, count); - Assert.Equal(expected.Length, actual.Length); - for (int i = 0; i < actual.Length; i++) - { - Assert.Equal(expected[i], actual[i]); - } + Assert.Equal(expected[i], actual[i]); } } } diff --git a/tests/StackExchange.Redis.Tests/Lists.cs b/tests/StackExchange.Redis.Tests/Lists.cs index 4f2b9c481..8263d5971 100644 --- a/tests/StackExchange.Redis.Tests/Lists.cs +++ b/tests/StackExchange.Redis.Tests/Lists.cs @@ -4,875 +4,831 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Lists : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Lists : TestBase + public Lists(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public void Ranges() { - public Lists(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + using var conn = Create(); - [Fact] - public void Ranges() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); + var db = conn.GetDatabase(); + RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.ListRightPush(key, "abcdefghijklmnopqrstuvwxyz".Select(x => (RedisValue)x.ToString()).ToArray(), CommandFlags.FireAndForget); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.ListRightPush(key, "abcdefghijklmnopqrstuvwxyz".Select(x => (RedisValue)x.ToString()).ToArray(), CommandFlags.FireAndForget); - Assert.Equal(26, db.ListLength(key)); - Assert.Equal("abcdefghijklmnopqrstuvwxyz", string.Concat(db.ListRange(key))); + Assert.Equal(26, db.ListLength(key)); + Assert.Equal("abcdefghijklmnopqrstuvwxyz", string.Concat(db.ListRange(key))); - var last10 = db.ListRange(key, -10, -1); - Assert.Equal("qrstuvwxyz", string.Concat(last10)); - db.ListTrim(key, 0, -11, CommandFlags.FireAndForget); + var last10 = db.ListRange(key, -10, -1); + Assert.Equal("qrstuvwxyz", string.Concat(last10)); + db.ListTrim(key, 0, -11, CommandFlags.FireAndForget); - Assert.Equal(16, db.ListLength(key)); - Assert.Equal("abcdefghijklmnop", string.Concat(db.ListRange(key))); - } - } + Assert.Equal(16, db.ListLength(key)); + Assert.Equal("abcdefghijklmnop", string.Concat(db.ListRange(key))); + } - [Fact] - public void ListLeftPushEmptyValues() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - var result = db.ListLeftPush(key, Array.Empty(), When.Always, CommandFlags.None); - Assert.Equal(0, result); - } - } + [Fact] + public void ListLeftPushEmptyValues() + { + using var conn = Create(); - [Fact] - public void ListLeftPushKeyDoesNotExists() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - var result = db.ListLeftPush(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); - Assert.Equal(0, result); - } - } + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + var result = db.ListLeftPush(key, Array.Empty(), When.Always, CommandFlags.None); + Assert.Equal(0, result); + } - [Fact] - public void ListLeftPushToExisitingKey() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - var pushResult = db.ListLeftPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); - Assert.Equal(1, pushResult); - var pushXResult = db.ListLeftPush(key, new RedisValue[] { "testvalue2" }, When.Exists, CommandFlags.None); - Assert.Equal(2, pushXResult); - - var rangeResult = db.ListRange(key, 0, -1); - Assert.Equal(2, rangeResult.Length); - Assert.Equal("testvalue2", rangeResult[0]); - Assert.Equal("testvalue1", rangeResult[1]); - } - } + [Fact] + public void ListLeftPushKeyDoesNotExists() + { + using var conn = Create(); - [Fact] - public void ListLeftPushMultipleToExisitingKey() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v4_0_0); - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - var pushResult = db.ListLeftPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); - Assert.Equal(1, pushResult); - var pushXResult = db.ListLeftPush(key, new RedisValue[] { "testvalue2", "testvalue3" }, When.Exists, CommandFlags.None); - Assert.Equal(3, pushXResult); - - var rangeResult = db.ListRange(key, 0, -1); - Assert.Equal(3, rangeResult.Length); - Assert.Equal("testvalue3", rangeResult[0]); - Assert.Equal("testvalue2", rangeResult[1]); - Assert.Equal("testvalue1", rangeResult[2]); - } - } + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + var result = db.ListLeftPush(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); + Assert.Equal(0, result); + } - [Fact] - public async Task ListLeftPushAsyncEmptyValues() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - var result = await db.ListLeftPushAsync(key, Array.Empty(), When.Always, CommandFlags.None); - Assert.Equal(0, result); - } - } + [Fact] + public void ListLeftPushToExisitingKey() + { + using var conn = Create(); - [Fact] - public async Task ListLeftPushAsyncKeyDoesNotExists() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - var result = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); - Assert.Equal(0, result); - } - } + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - [Fact] - public async Task ListLeftPushAsyncToExisitingKey() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - var pushResult = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); - Assert.Equal(1, pushResult); - var pushXResult = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue2" }, When.Exists, CommandFlags.None); - Assert.Equal(2, pushXResult); - - var rangeResult = db.ListRange(key, 0, -1); - Assert.Equal(2, rangeResult.Length); - Assert.Equal("testvalue2", rangeResult[0]); - Assert.Equal("testvalue1", rangeResult[1]); - } - } + var pushResult = db.ListLeftPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + Assert.Equal(1, pushResult); + var pushXResult = db.ListLeftPush(key, new RedisValue[] { "testvalue2" }, When.Exists, CommandFlags.None); + Assert.Equal(2, pushXResult); - [Fact] - public async Task ListLeftPushAsyncMultipleToExisitingKey() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v4_0_0); - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - var pushResult = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); - Assert.Equal(1, pushResult); - var pushXResult = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue2", "testvalue3" }, When.Exists, CommandFlags.None); - Assert.Equal(3, pushXResult); - - var rangeResult = db.ListRange(key, 0, -1); - Assert.Equal(3, rangeResult.Length); - Assert.Equal("testvalue3", rangeResult[0]); - Assert.Equal("testvalue2", rangeResult[1]); - Assert.Equal("testvalue1", rangeResult[2]); - } - } + var rangeResult = db.ListRange(key, 0, -1); + Assert.Equal(2, rangeResult.Length); + Assert.Equal("testvalue2", rangeResult[0]); + Assert.Equal("testvalue1", rangeResult[1]); + } - [Fact] - public void ListRightPushEmptyValues() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - var result = db.ListRightPush(key, Array.Empty(), When.Always, CommandFlags.None); - Assert.Equal(0, result); - } - } + [Fact] + public void ListLeftPushMultipleToExisitingKey() + { + using var conn = Create(require: RedisFeatures.v4_0_0); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + var pushResult = db.ListLeftPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + Assert.Equal(1, pushResult); + var pushXResult = db.ListLeftPush(key, new RedisValue[] { "testvalue2", "testvalue3" }, When.Exists, CommandFlags.None); + Assert.Equal(3, pushXResult); + + var rangeResult = db.ListRange(key, 0, -1); + Assert.Equal(3, rangeResult.Length); + Assert.Equal("testvalue3", rangeResult[0]); + Assert.Equal("testvalue2", rangeResult[1]); + Assert.Equal("testvalue1", rangeResult[2]); + } - [Fact] - public void ListRightPushKeyDoesNotExists() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - var result = db.ListRightPush(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); - Assert.Equal(0, result); - } - } + [Fact] + public async Task ListLeftPushAsyncEmptyValues() + { + using var conn = Create(); - [Fact] - public void ListRightPushToExisitingKey() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - var pushResult = db.ListRightPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); - Assert.Equal(1, pushResult); - var pushXResult = db.ListRightPush(key, new RedisValue[] { "testvalue2" }, When.Exists, CommandFlags.None); - Assert.Equal(2, pushXResult); - - var rangeResult = db.ListRange(key, 0, -1); - Assert.Equal(2, rangeResult.Length); - Assert.Equal("testvalue1", rangeResult[0]); - Assert.Equal("testvalue2", rangeResult[1]); - } - } + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + var result = await db.ListLeftPushAsync(key, Array.Empty(), When.Always, CommandFlags.None); + Assert.Equal(0, result); + } - [Fact] - public void ListRightPushMultipleToExisitingKey() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v4_0_0); - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - var pushResult = db.ListRightPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); - Assert.Equal(1, pushResult); - var pushXResult = db.ListRightPush(key, new RedisValue[] { "testvalue2", "testvalue3" }, When.Exists, CommandFlags.None); - Assert.Equal(3, pushXResult); - - var rangeResult = db.ListRange(key, 0, -1); - Assert.Equal(3, rangeResult.Length); - Assert.Equal("testvalue1", rangeResult[0]); - Assert.Equal("testvalue2", rangeResult[1]); - Assert.Equal("testvalue3", rangeResult[2]); - } - } + [Fact] + public async Task ListLeftPushAsyncKeyDoesNotExists() + { + using var conn = Create(); - [Fact] - public async Task ListRightPushAsyncEmptyValues() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - var result = await db.ListRightPushAsync(key, Array.Empty(), When.Always, CommandFlags.None); - Assert.Equal(0, result); - } - } + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + var result = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); + Assert.Equal(0, result); + } - [Fact] - public async Task ListRightPushAsyncKeyDoesNotExists() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - var result = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); - Assert.Equal(0, result); - } - } + [Fact] + public async Task ListLeftPushAsyncToExisitingKey() + { + using var conn = Create(); - [Fact] - public async Task ListRightPushAsyncToExisitingKey() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - var pushResult = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); - Assert.Equal(1, pushResult); - var pushXResult = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue2" }, When.Exists, CommandFlags.None); - Assert.Equal(2, pushXResult); - - var rangeResult = db.ListRange(key, 0, -1); - Assert.Equal(2, rangeResult.Length); - Assert.Equal("testvalue1", rangeResult[0]); - Assert.Equal("testvalue2", rangeResult[1]); - } - } + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - [Fact] - public async Task ListRightPushAsyncMultipleToExisitingKey() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v4_0_0); - var db = conn.GetDatabase(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - var pushResult = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); - Assert.Equal(1, pushResult); - var pushXResult = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue2", "testvalue3" }, When.Exists, CommandFlags.None); - Assert.Equal(3, pushXResult); - - var rangeResult = db.ListRange(key, 0, -1); - Assert.Equal(3, rangeResult.Length); - Assert.Equal("testvalue1", rangeResult[0]); - Assert.Equal("testvalue2", rangeResult[1]); - Assert.Equal("testvalue3", rangeResult[2]); - } - } + var pushResult = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + Assert.Equal(1, pushResult); + var pushXResult = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue2" }, When.Exists, CommandFlags.None); + Assert.Equal(2, pushXResult); - [Fact] - public async Task ListMove() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - RedisKey src = Me(); - RedisKey dest = Me() + "dest"; - db.KeyDelete(src, CommandFlags.FireAndForget); - - var pushResult = await db.ListRightPushAsync(src, new RedisValue[] { "testvalue1", "testvalue2" }); - Assert.Equal(2, pushResult); - - var rangeResult1 = db.ListMove(src, dest, ListSide.Left, ListSide.Right); - var rangeResult2 = db.ListMove(src, dest, ListSide.Left, ListSide.Left); - var rangeResult3 = db.ListMove(dest, src, ListSide.Right, ListSide.Right); - var rangeResult4 = db.ListMove(dest, src, ListSide.Right, ListSide.Left); - Assert.Equal("testvalue1", rangeResult1); - Assert.Equal("testvalue2", rangeResult2); - Assert.Equal("testvalue1", rangeResult3); - Assert.Equal("testvalue2", rangeResult4); - } + var rangeResult = db.ListRange(key, 0, -1); + Assert.Equal(2, rangeResult.Length); + Assert.Equal("testvalue2", rangeResult[0]); + Assert.Equal("testvalue1", rangeResult[1]); + } - [Fact] - public void ListMoveKeyDoesNotExist() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); + [Fact] + public async Task ListLeftPushAsyncMultipleToExisitingKey() + { + using var conn = Create(require: RedisFeatures.v4_0_0); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + var pushResult = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + Assert.Equal(1, pushResult); + var pushXResult = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue2", "testvalue3" }, When.Exists, CommandFlags.None); + Assert.Equal(3, pushXResult); + + var rangeResult = db.ListRange(key, 0, -1); + Assert.Equal(3, rangeResult.Length); + Assert.Equal("testvalue3", rangeResult[0]); + Assert.Equal("testvalue2", rangeResult[1]); + Assert.Equal("testvalue1", rangeResult[2]); + } - var db = conn.GetDatabase(); - RedisKey src = Me(); - RedisKey dest = Me() + "dest"; - db.KeyDelete(src, CommandFlags.FireAndForget); + [Fact] + public void ListRightPushEmptyValues() + { + using var conn = Create(); - var rangeResult1 = db.ListMove(src, dest, ListSide.Left, ListSide.Right); - Assert.True(rangeResult1.IsNull); - } + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + var result = db.ListRightPush(key, Array.Empty(), When.Always, CommandFlags.None); + Assert.Equal(0, result); + } - [Fact] - public void ListPositionHappyPath() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + [Fact] + public void ListRightPushKeyDoesNotExists() + { + using var conn = Create(); - var db = conn.GetDatabase(); - var key = Me(); - var val = "foo"; - db.KeyDelete(key); + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + var result = db.ListRightPush(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); + Assert.Equal(0, result); + } - db.ListLeftPush(key, val); - var res = db.ListPosition(key, val); + [Fact] + public void ListRightPushToExisitingKey() + { + using var conn = Create(); - Assert.Equal(0, res); - } + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - [Fact] - public void ListPositionEmpty() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + var pushResult = db.ListRightPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + Assert.Equal(1, pushResult); + var pushXResult = db.ListRightPush(key, new RedisValue[] { "testvalue2" }, When.Exists, CommandFlags.None); + Assert.Equal(2, pushXResult); + + var rangeResult = db.ListRange(key, 0, -1); + Assert.Equal(2, rangeResult.Length); + Assert.Equal("testvalue1", rangeResult[0]); + Assert.Equal("testvalue2", rangeResult[1]); + } - var db = conn.GetDatabase(); - var key = Me(); - var val = "foo"; - db.KeyDelete(key); + [Fact] + public void ListRightPushMultipleToExisitingKey() + { + using var conn = Create(require: RedisFeatures.v4_0_0); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + var pushResult = db.ListRightPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + Assert.Equal(1, pushResult); + var pushXResult = db.ListRightPush(key, new RedisValue[] { "testvalue2", "testvalue3" }, When.Exists, CommandFlags.None); + Assert.Equal(3, pushXResult); + + var rangeResult = db.ListRange(key, 0, -1); + Assert.Equal(3, rangeResult.Length); + Assert.Equal("testvalue1", rangeResult[0]); + Assert.Equal("testvalue2", rangeResult[1]); + Assert.Equal("testvalue3", rangeResult[2]); + } - var res = db.ListPosition(key, val); + [Fact] + public async Task ListRightPushAsyncEmptyValues() + { + using var conn = Create(); - Assert.Equal(-1, res); - } + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + var result = await db.ListRightPushAsync(key, Array.Empty(), When.Always, CommandFlags.None); + Assert.Equal(0, result); + } - [Fact] - public void ListPositionsHappyPath() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + [Fact] + public async Task ListRightPushAsyncKeyDoesNotExists() + { + using var conn = Create(); - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + var result = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); + Assert.Equal(0, result); + } - db.KeyDelete(key); + [Fact] + public async Task ListRightPushAsyncToExisitingKey() + { + using var conn = Create(); - for (var i = 0; i < 10; i++) - { - db.ListLeftPush(key, foo); - db.ListLeftPush(key, bar); - db.ListLeftPush(key, baz); - } + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - var res = db.ListPositions(key, foo, 5); + var pushResult = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + Assert.Equal(1, pushResult); + var pushXResult = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue2" }, When.Exists, CommandFlags.None); + Assert.Equal(2, pushXResult); - foreach (var item in res) - { - Assert.Equal(2, item % 3); - } + var rangeResult = db.ListRange(key, 0, -1); + Assert.Equal(2, rangeResult.Length); + Assert.Equal("testvalue1", rangeResult[0]); + Assert.Equal("testvalue2", rangeResult[1]); + } - Assert.Equal(5,res.Count()); - } + [Fact] + public async Task ListRightPushAsyncMultipleToExisitingKey() + { + using var conn = Create(require: RedisFeatures.v4_0_0); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + var pushResult = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + Assert.Equal(1, pushResult); + var pushXResult = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue2", "testvalue3" }, When.Exists, CommandFlags.None); + Assert.Equal(3, pushXResult); + + var rangeResult = db.ListRange(key, 0, -1); + Assert.Equal(3, rangeResult.Length); + Assert.Equal("testvalue1", rangeResult[0]); + Assert.Equal("testvalue2", rangeResult[1]); + Assert.Equal("testvalue3", rangeResult[2]); + } - [Fact] - public void ListPositionsTooFew() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + [Fact] + public async Task ListMove() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + RedisKey src = Me(); + RedisKey dest = Me() + "dest"; + db.KeyDelete(src, CommandFlags.FireAndForget); + + var pushResult = await db.ListRightPushAsync(src, new RedisValue[] { "testvalue1", "testvalue2" }); + Assert.Equal(2, pushResult); + + var rangeResult1 = db.ListMove(src, dest, ListSide.Left, ListSide.Right); + var rangeResult2 = db.ListMove(src, dest, ListSide.Left, ListSide.Left); + var rangeResult3 = db.ListMove(dest, src, ListSide.Right, ListSide.Right); + var rangeResult4 = db.ListMove(dest, src, ListSide.Right, ListSide.Left); + Assert.Equal("testvalue1", rangeResult1); + Assert.Equal("testvalue2", rangeResult2); + Assert.Equal("testvalue1", rangeResult3); + Assert.Equal("testvalue2", rangeResult4); + } + + [Fact] + public void ListMoveKeyDoesNotExist() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + var db = conn.GetDatabase(); + RedisKey src = Me(); + RedisKey dest = Me() + "dest"; + db.KeyDelete(src, CommandFlags.FireAndForget); - db.KeyDelete(key); + var rangeResult1 = db.ListMove(src, dest, ListSide.Left, ListSide.Right); + Assert.True(rangeResult1.IsNull); + } - for (var i = 0; i < 10; i++) - { - db.ListLeftPush(key, bar); - db.ListLeftPush(key, baz); - } + [Fact] + public void ListPositionHappyPath() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - db.ListLeftPush(key, foo); + var db = conn.GetDatabase(); + var key = Me(); + const string val = "foo"; + db.KeyDelete(key); - var res = db.ListPositions(key, foo, 5); - Assert.Single(res); - Assert.Equal(0, res.Single()); - } + db.ListLeftPush(key, val); + var res = db.ListPosition(key, val); - [Fact] - public void ListPositionsAll() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + Assert.Equal(0, res); + } - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + [Fact] + public void ListPositionEmpty() + { + using var conn = Create(require: RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + const string val = "foo"; + db.KeyDelete(key); - db.KeyDelete(key); + var res = db.ListPosition(key, val); - for (var i = 0; i < 10; i++) - { - db.ListLeftPush(key, foo); - db.ListLeftPush(key, bar); - db.ListLeftPush(key, baz); - } + Assert.Equal(-1, res); + } - var res = db.ListPositions(key, foo, 0); + [Fact] + public void ListPositionsHappyPath() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - foreach (var item in res) - { - Assert.Equal(2, item % 3); - } + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; - Assert.Equal(10,res.Count()); - } + db.KeyDelete(key); - [Fact] - public void ListPositionsAllLimitLength() + for (var i = 0; i < 10; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + db.ListLeftPush(key, foo); + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + var res = db.ListPositions(key, foo, 5); - db.KeyDelete(key); + foreach (var item in res) + { + Assert.Equal(2, item % 3); + } - for (var i = 0; i < 10; i++) - { - db.ListLeftPush(key, foo); - db.ListLeftPush(key, bar); - db.ListLeftPush(key, baz); - } + Assert.Equal(5, res.Length); + } - var res = db.ListPositions(key, foo, 0, maxLength: 15); + [Fact] + public void ListPositionsTooFew() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - foreach (var item in res) - { - Assert.Equal(2, item % 3); - } + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; - Assert.Equal(5,res.Count()); - } + db.KeyDelete(key); - [Fact] - public void ListPositionsEmpty() + for (var i = 0; i < 10; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + db.ListLeftPush(key, foo); - db.KeyDelete(key); + var res = db.ListPositions(key, foo, 5); + Assert.Single(res); + Assert.Equal(0, res.Single()); + } - for (var i = 0; i < 10; i++) - { - db.ListLeftPush(key, bar); - db.ListLeftPush(key, baz); - } + [Fact] + public void ListPositionsAll() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - var res = db.ListPositions(key, foo, 5); + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; - Assert.Empty(res); - } + db.KeyDelete(key); - [Fact] - public void ListPositionByRank() + for (var i = 0; i < 10; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + db.ListLeftPush(key, foo); + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + var res = db.ListPositions(key, foo, 0); - db.KeyDelete(key); + foreach (var item in res) + { + Assert.Equal(2, item % 3); + } - for (var i = 0; i < 10; i++) - { - db.ListLeftPush(key, foo); - db.ListLeftPush(key, bar); - db.ListLeftPush(key, baz); - } + Assert.Equal(10, res.Length); + } - var rank = 6; + [Fact] + public void ListPositionsAllLimitLength() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - var res = db.ListPosition(key, foo, rank: rank); + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; - Assert.Equal(3*rank-1, res); - } + db.KeyDelete(key); - [Fact] - public void ListPositionLimitSoNull() + for (var i = 0; i < 10; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + db.ListLeftPush(key, foo); + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + var res = db.ListPositions(key, foo, 0, maxLength: 15); - db.KeyDelete(key); + foreach (var item in res) + { + Assert.Equal(2, item % 3); + } - for (var i = 0; i < 10; i++) - { - db.ListLeftPush(key, bar); - db.ListLeftPush(key, baz); - } + Assert.Equal(5, res.Length); + } - db.ListRightPush(key, foo); + [Fact] + public void ListPositionsEmpty() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - var res = db.ListPosition(key, foo, maxLength: 20); + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; - Assert.Equal(-1, res); - } + db.KeyDelete(key); - [Fact] - public async Task ListPositionHappyPathAsync() + for (var i = 0; i < 10; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } - var db = conn.GetDatabase(); - var key = Me(); - var val = "foo"; - await db.KeyDeleteAsync(key); + var res = db.ListPositions(key, foo, 5); - await db.ListLeftPushAsync(key, val); - var res = await db.ListPositionAsync(key, val); + Assert.Empty(res); + } - Assert.Equal(0, res); - } + [Fact] + public void ListPositionByRank() + { + using var conn = Create(require: RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; + + db.KeyDelete(key); - [Fact] - public async Task ListPositionEmptyAsync() + for (var i = 0; i < 10; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + db.ListLeftPush(key, foo); + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } - var db = conn.GetDatabase(); - var key = Me(); - var val = "foo"; - await db.KeyDeleteAsync(key); + const int rank = 6; - var res = await db.ListPositionAsync(key, val); + var res = db.ListPosition(key, foo, rank: rank); - Assert.Equal(-1, res); - } + Assert.Equal((3 * rank) - 1, res); + } + + [Fact] + public void ListPositionLimitSoNull() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - [Fact] - public async Task ListPositionsHappyPathAsync() + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; + + db.KeyDelete(key); + + for (var i = 0; i < 10; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); + } - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + db.ListRightPush(key, foo); - await db.KeyDeleteAsync(key); + var res = db.ListPosition(key, foo, maxLength: 20); - for (var i = 0; i < 10; i++) - { - await db.ListLeftPushAsync(key, foo); - await db.ListLeftPushAsync(key, bar); - await db.ListLeftPushAsync(key, baz); - } + Assert.Equal(-1, res); + } - var res = await db.ListPositionsAsync(key, foo, 5); + [Fact] + public async Task ListPositionHappyPathAsync() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - foreach (var item in res) - { - Assert.Equal(2, item % 3); - } + var db = conn.GetDatabase(); + var key = Me(); + const string val = "foo"; + await db.KeyDeleteAsync(key); - Assert.Equal(5,res.Count()); - } + await db.ListLeftPushAsync(key, val); + var res = await db.ListPositionAsync(key, val); - [Fact] - public async Task ListPositionsTooFewAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + Assert.Equal(0, res); + } - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + [Fact] + public async Task ListPositionEmptyAsync() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - await db.KeyDeleteAsync(key); + var db = conn.GetDatabase(); + var key = Me(); + const string val = "foo"; + await db.KeyDeleteAsync(key); - for (var i = 0; i < 10; i++) - { - await db.ListLeftPushAsync(key, bar); - await db.ListLeftPushAsync(key, baz); - } + var res = await db.ListPositionAsync(key, val); - db.ListLeftPush(key, foo); + Assert.Equal(-1, res); + } - var res = await db.ListPositionsAsync(key, foo, 5); - Assert.Single(res); - Assert.Equal(0, res.Single()); - } + [Fact] + public async Task ListPositionsHappyPathAsync() + { + using var conn = Create(require: RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; + + await db.KeyDeleteAsync(key); - [Fact] - public async Task ListPositionsAllAsync() + for (var i = 0; i < 10; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + await db.ListLeftPushAsync(key, foo); + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); + } - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + var res = await db.ListPositionsAsync(key, foo, 5); - await db.KeyDeleteAsync(key); + foreach (var item in res) + { + Assert.Equal(2, item % 3); + } - for (var i = 0; i < 10; i++) - { - await db.ListLeftPushAsync(key, foo); - await db.ListLeftPushAsync(key, bar); - await db.ListLeftPushAsync(key, baz); - } + Assert.Equal(5, res.Length); + } - var res = await db.ListPositionsAsync(key, foo, 0); + [Fact] + public async Task ListPositionsTooFewAsync() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - foreach (var item in res) - { - Assert.Equal(2, item % 3); - } + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; - Assert.Equal(10,res.Count()); - } + await db.KeyDeleteAsync(key); - [Fact] - public async Task ListPositionsAllLimitLengthAsync() + for (var i = 0; i < 10; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); + } - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + db.ListLeftPush(key, foo); - await db.KeyDeleteAsync(key); + var res = await db.ListPositionsAsync(key, foo, 5); + Assert.Single(res); + Assert.Equal(0, res.Single()); + } - for (var i = 0; i < 10; i++) - { - await db.ListLeftPushAsync(key, foo); - await db.ListLeftPushAsync(key, bar); - await db.ListLeftPushAsync(key, baz); - } + [Fact] + public async Task ListPositionsAllAsync() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - var res = await db.ListPositionsAsync(key, foo, 0, maxLength: 15); + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; - foreach (var item in res) - { - Assert.Equal(2, item % 3); - } + await db.KeyDeleteAsync(key); - Assert.Equal(5,res.Count()); + for (var i = 0; i < 10; i++) + { + await db.ListLeftPushAsync(key, foo); + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); } - [Fact] - public async Task ListPositionsEmptyAsync() + var res = await db.ListPositionsAsync(key, foo, 0); + + foreach (var item in res) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + Assert.Equal(2, item % 3); + } - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + Assert.Equal(10, res.Length); + } - await db.KeyDeleteAsync(key); + [Fact] + public async Task ListPositionsAllLimitLengthAsync() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - for (var i = 0; i < 10; i++) - { - await db.ListLeftPushAsync(key, bar); - await db.ListLeftPushAsync(key, baz); - } + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; - var res = await db.ListPositionsAsync(key, foo, 5); + await db.KeyDeleteAsync(key); - Assert.Empty(res); + for (var i = 0; i < 10; i++) + { + await db.ListLeftPushAsync(key, foo); + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); } - [Fact] - public async Task ListPositionByRankAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + var res = await db.ListPositionsAsync(key, foo, 0, maxLength: 15); - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + foreach (var item in res) + { + Assert.Equal(2, item % 3); + } - await db.KeyDeleteAsync(key); + Assert.Equal(5, res.Length); + } - for (var i = 0; i < 10; i++) - { - await db.ListLeftPushAsync(key, foo); - await db.ListLeftPushAsync(key, bar); - await db.ListLeftPushAsync(key, baz); - } + [Fact] + public async Task ListPositionsEmptyAsync() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - var rank = 6; + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; - var res = await db.ListPositionAsync(key, foo, rank: rank); + await db.KeyDeleteAsync(key); - Assert.Equal(3 * rank - 1, res); + for (var i = 0; i < 10; i++) + { + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); } - [Fact] - public async Task ListPositionLimitSoNullAsync() + var res = await db.ListPositionsAsync(key, foo, 5); + + Assert.Empty(res); + } + + [Fact] + public async Task ListPositionByRankAsync() + { + using var conn = Create(require: RedisFeatures.v6_0_6); + + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; + + await db.KeyDeleteAsync(key); + + for (var i = 0; i < 10; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + await db.ListLeftPushAsync(key, foo); + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); + } - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + const int rank = 6; - await db.KeyDeleteAsync(key); + var res = await db.ListPositionAsync(key, foo, rank: rank); - for (var i = 0; i < 10; i++) - { - await db.ListLeftPushAsync(key, bar); - await db.ListLeftPushAsync(key, baz); - } + Assert.Equal((3 * rank) - 1, res); + } - await db.ListRightPushAsync(key, foo); + [Fact] + public async Task ListPositionLimitSoNullAsync() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - var res = await db.ListPositionAsync(key, foo, maxLength: 20); + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; - Assert.Equal(-1, res); - } + await db.KeyDeleteAsync(key); - [Fact] - public async Task ListPositionFireAndForgetAsync() + for (var i = 0; i < 10; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); + } - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + await db.ListRightPushAsync(key, foo); - await db.KeyDeleteAsync(key); + var res = await db.ListPositionAsync(key, foo, maxLength: 20); - for (var i = 0; i < 10; i++) - { - await db.ListLeftPushAsync(key, foo); - await db.ListLeftPushAsync(key, bar); - await db.ListLeftPushAsync(key, baz); - } + Assert.Equal(-1, res); + } - await db.ListRightPushAsync(key, foo); + [Fact] + public async Task ListPositionFireAndForgetAsync() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - var res = await db.ListPositionAsync(key, foo, maxLength: 20, flags: CommandFlags.FireAndForget); + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; - Assert.Equal(-1, res); - } + await db.KeyDeleteAsync(key); - [Fact] - public void ListPositionFireAndForget() + for (var i = 0; i < 10; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_0_6); + await db.ListLeftPushAsync(key, foo); + await db.ListLeftPushAsync(key, bar); + await db.ListLeftPushAsync(key, baz); + } + + await db.ListRightPushAsync(key, foo); - var db = conn.GetDatabase(); - var key = Me(); - var foo = "foo"; - var bar = "bar"; - var baz = "baz"; + var res = await db.ListPositionAsync(key, foo, maxLength: 20, flags: CommandFlags.FireAndForget); - db.KeyDelete(key); + Assert.Equal(-1, res); + } - for (var i = 0; i < 10; i++) - { - db.ListLeftPush(key, foo); - db.ListLeftPush(key, bar); - db.ListLeftPush(key, baz); - } + [Fact] + public void ListPositionFireAndForget() + { + using var conn = Create(require: RedisFeatures.v6_0_6); - db.ListRightPush(key, foo); + var db = conn.GetDatabase(); + var key = Me(); + const string foo = "foo", + bar = "bar", + baz = "baz"; - var res = db.ListPosition(key, foo, maxLength: 20, flags: CommandFlags.FireAndForget); + db.KeyDelete(key); - Assert.Equal(-1, res); + for (var i = 0; i < 10; i++) + { + db.ListLeftPush(key, foo); + db.ListLeftPush(key, bar); + db.ListLeftPush(key, baz); } + + db.ListRightPush(key, foo); + + var res = db.ListPosition(key, foo, maxLength: 20, flags: CommandFlags.FireAndForget); + + Assert.Equal(-1, res); } } diff --git a/tests/StackExchange.Redis.Tests/Locking.cs b/tests/StackExchange.Redis.Tests/Locking.cs index 0c782f20d..08dc7db48 100644 --- a/tests/StackExchange.Redis.Tests/Locking.cs +++ b/tests/StackExchange.Redis.Tests/Locking.cs @@ -5,241 +5,208 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(NonParallelCollection.Name)] +public class Locking : TestBase { - [Collection(NonParallelCollection.Name)] - public class Locking : TestBase - { - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public Locking(ITestOutputHelper output) : base (output) { } + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; + public Locking(ITestOutputHelper output) : base (output) { } - public enum TestMode - { - MultiExec, - NoMultiExec, - Twemproxy - } + public enum TestMode + { + MultiExec, + NoMultiExec, + Twemproxy + } - public static IEnumerable TestModes() - { - yield return new object[] { TestMode.MultiExec }; - yield return new object[] { TestMode.NoMultiExec }; - yield return new object[] { TestMode.Twemproxy }; - } + public static IEnumerable TestModes() + { + yield return new object[] { TestMode.MultiExec }; + yield return new object[] { TestMode.NoMultiExec }; + yield return new object[] { TestMode.Twemproxy }; + } - [Theory, MemberData(nameof(TestModes))] - public void AggressiveParallel(TestMode testMode) + [Theory, MemberData(nameof(TestModes))] + public void AggressiveParallel(TestMode testMode) + { + int count = 2; + int errorCount = 0; + int bgErrorCount = 0; + var evt = new ManualResetEvent(false); + var key = Me(); + using (var conn1 = Create(testMode)) + using (var conn2 = Create(testMode)) { - int count = 2; - int errorCount = 0; - int bgErrorCount = 0; - var evt = new ManualResetEvent(false); - var key = Me(); - using (var c1 = Create(testMode)) - using (var c2 = Create(testMode)) + void cb(object? obj) { - void cb(object? obj) + try { - try - { - var conn = (IDatabase?)obj!; - conn.Multiplexer.ErrorMessage += delegate { Interlocked.Increment(ref errorCount); }; - - for (int i = 0; i < 1000; i++) - { - conn.LockTakeAsync(key, "def", TimeSpan.FromSeconds(5)); - } - conn.Ping(); - if (Interlocked.Decrement(ref count) == 0) evt.Set(); - } - catch + var conn = (IDatabase?)obj!; + conn.Multiplexer.ErrorMessage += delegate { Interlocked.Increment(ref errorCount); }; + + for (int i = 0; i < 1000; i++) { - Interlocked.Increment(ref bgErrorCount); + conn.LockTakeAsync(key, "def", TimeSpan.FromSeconds(5)); } + conn.Ping(); + if (Interlocked.Decrement(ref count) == 0) evt.Set(); + } + catch + { + Interlocked.Increment(ref bgErrorCount); } - int db = testMode == TestMode.Twemproxy ? 0 : 2; - ThreadPool.QueueUserWorkItem(cb, c1.GetDatabase(db)); - ThreadPool.QueueUserWorkItem(cb, c2.GetDatabase(db)); - evt.WaitOne(8000); } - Assert.Equal(0, Interlocked.CompareExchange(ref errorCount, 0, 0)); - Assert.Equal(0, bgErrorCount); + int db = testMode == TestMode.Twemproxy ? 0 : 2; + ThreadPool.QueueUserWorkItem(cb, conn1.GetDatabase(db)); + ThreadPool.QueueUserWorkItem(cb, conn2.GetDatabase(db)); + evt.WaitOne(8000); } + Assert.Equal(0, Interlocked.CompareExchange(ref errorCount, 0, 0)); + Assert.Equal(0, bgErrorCount); + } + + [Fact] + public void TestOpCountByVersionLocal_UpLevel() + { + using var conn = Create(shared: false); - [Fact] - public void TestOpCountByVersionLocal_UpLevel() + TestLockOpCountByVersion(conn, 1, false); + TestLockOpCountByVersion(conn, 1, true); + } + + private static void TestLockOpCountByVersion(IConnectionMultiplexer conn, int expectedOps, bool existFirst) + { + const int LockDuration = 30; + RedisKey Key = Me(); + + var db = conn.GetDatabase(); + db.KeyDelete(Key, CommandFlags.FireAndForget); + RedisValue newVal = "us:" + Guid.NewGuid().ToString(); + RedisValue expectedVal = newVal; + if (existFirst) { - using (var conn = Create(shared: false)) - { - TestLockOpCountByVersion(conn, 1, false); - TestLockOpCountByVersion(conn, 1, true); - } + expectedVal = "other:" + Guid.NewGuid().ToString(); + db.StringSet(Key, expectedVal, TimeSpan.FromSeconds(LockDuration), flags: CommandFlags.FireAndForget); } + long countBefore = GetServer(conn).GetCounters().Interactive.OperationCount; - private static void TestLockOpCountByVersion(IConnectionMultiplexer conn, int expectedOps, bool existFirst) - { - const int LockDuration = 30; - RedisKey Key = Me(); - - var db = conn.GetDatabase(); - db.KeyDelete(Key, CommandFlags.FireAndForget); - RedisValue newVal = "us:" + Guid.NewGuid().ToString(); - RedisValue expectedVal = newVal; - if (existFirst) - { - expectedVal = "other:" + Guid.NewGuid().ToString(); - db.StringSet(Key, expectedVal, TimeSpan.FromSeconds(LockDuration), flags: CommandFlags.FireAndForget); - } - long countBefore = GetServer(conn).GetCounters().Interactive.OperationCount; + var taken = db.LockTake(Key, newVal, TimeSpan.FromSeconds(LockDuration)); - var taken = db.LockTake(Key, newVal, TimeSpan.FromSeconds(LockDuration)); + long countAfter = GetServer(conn).GetCounters().Interactive.OperationCount; + var valAfter = db.StringGet(Key); - long countAfter = GetServer(conn).GetCounters().Interactive.OperationCount; - var valAfter = db.StringGet(Key); + Assert.Equal(!existFirst, taken); + Assert.Equal(expectedVal, valAfter); + // note we get a ping from GetCounters + Assert.True(countAfter - countBefore >= expectedOps, $"({countAfter} - {countBefore}) >= {expectedOps}"); + } - Assert.Equal(!existFirst, taken); - Assert.Equal(expectedVal, valAfter); - // note we get a ping from GetCounters - Assert.True(countAfter - countBefore >= expectedOps, $"({countAfter} - {countBefore}) >= {expectedOps}"); - } + private IConnectionMultiplexer Create(TestMode mode) => mode switch + { + TestMode.MultiExec => Create(), + TestMode.NoMultiExec => Create(disabledCommands: new[] { "multi", "exec" }), + TestMode.Twemproxy => Create(proxy: Proxy.Twemproxy), + _ => throw new NotSupportedException(mode.ToString()), + }; + + [Theory, MemberData(nameof(TestModes))] + public async Task TakeLockAndExtend(TestMode mode) + { + using var conn = Create(mode); + + RedisValue right = Guid.NewGuid().ToString(), + wrong = Guid.NewGuid().ToString(); + + int DB = mode == TestMode.Twemproxy ? 0 : 7; + RedisKey Key = Me(); + + var db = conn.GetDatabase(DB); + + db.KeyDelete(Key, CommandFlags.FireAndForget); + + bool withTran = mode == TestMode.MultiExec; + var t1 = db.LockTakeAsync(Key, right, TimeSpan.FromSeconds(20)); + var t1b = db.LockTakeAsync(Key, wrong, TimeSpan.FromSeconds(10)); + var t2 = db.LockQueryAsync(Key); + var t3 = withTran ? db.LockReleaseAsync(Key, wrong) : null; + var t4 = db.LockQueryAsync(Key); + var t5 = withTran ? db.LockExtendAsync(Key, wrong, TimeSpan.FromSeconds(60)) : null; + var t6 = db.LockQueryAsync(Key); + var t7 = db.KeyTimeToLiveAsync(Key); + var t8 = db.LockExtendAsync(Key, right, TimeSpan.FromSeconds(60)); + var t9 = db.LockQueryAsync(Key); + var t10 = db.KeyTimeToLiveAsync(Key); + var t11 = db.LockReleaseAsync(Key, right); + var t12 = db.LockQueryAsync(Key); + var t13 = db.LockTakeAsync(Key, wrong, TimeSpan.FromSeconds(10)); + + Assert.NotEqual(default(RedisValue), right); + Assert.NotEqual(default(RedisValue), wrong); + Assert.NotEqual(right, wrong); + Assert.True(await t1, "1"); + Assert.False(await t1b, "1b"); + Assert.Equal(right, await t2); + if (withTran) Assert.False(await t3!, "3"); + Assert.Equal(right, await t4); + if (withTran) Assert.False(await t5!, "5"); + Assert.Equal(right, await t6); + var ttl = (await t7).Value.TotalSeconds; + Assert.True(ttl > 0 && ttl <= 20, "7"); + Assert.True(await t8, "8"); + Assert.Equal(right, await t9); + ttl = (await t10).Value.TotalSeconds; + Assert.True(ttl > 50 && ttl <= 60, "10"); + Assert.True(await t11, "11"); + Assert.Null((string?)await t12); + Assert.True(await t13, "13"); + } - private IConnectionMultiplexer Create(TestMode mode) => mode switch - { - TestMode.MultiExec => Create(), - TestMode.NoMultiExec => Create(disabledCommands: new[] { "multi", "exec" }), - TestMode.Twemproxy => Create(proxy: Proxy.Twemproxy), - _ => throw new NotSupportedException(mode.ToString()), - }; - - [Theory, MemberData(nameof(TestModes))] - public async Task TakeLockAndExtend(TestMode mode) + [Theory, MemberData(nameof(TestModes))] + public async Task TestBasicLockNotTaken(TestMode testMode) + { + using var conn = Create(testMode); + + int errorCount = 0; + conn.ErrorMessage += delegate { Interlocked.Increment(ref errorCount); }; + Task? taken = null; + Task? newValue = null; + Task? ttl = null; + + const int LOOP = 50; + var db = conn.GetDatabase(); + var key = Me(); + for (int i = 0; i < LOOP; i++) { - bool withTran = mode == TestMode.MultiExec; - using (var conn = Create(mode)) - { - RedisValue right = Guid.NewGuid().ToString(), - wrong = Guid.NewGuid().ToString(); - - int DB = mode == TestMode.Twemproxy ? 0 : 7; - RedisKey Key = Me(); - - var db = conn.GetDatabase(DB); - - db.KeyDelete(Key, CommandFlags.FireAndForget); - - var t1 = db.LockTakeAsync(Key, right, TimeSpan.FromSeconds(20)); - var t1b = db.LockTakeAsync(Key, wrong, TimeSpan.FromSeconds(10)); - var t2 = db.LockQueryAsync(Key); - var t3 = withTran ? db.LockReleaseAsync(Key, wrong) : null; - var t4 = db.LockQueryAsync(Key); - var t5 = withTran ? db.LockExtendAsync(Key, wrong, TimeSpan.FromSeconds(60)) : null; - var t6 = db.LockQueryAsync(Key); - var t7 = db.KeyTimeToLiveAsync(Key); - var t8 = db.LockExtendAsync(Key, right, TimeSpan.FromSeconds(60)); - var t9 = db.LockQueryAsync(Key); - var t10 = db.KeyTimeToLiveAsync(Key); - var t11 = db.LockReleaseAsync(Key, right); - var t12 = db.LockQueryAsync(Key); - var t13 = db.LockTakeAsync(Key, wrong, TimeSpan.FromSeconds(10)); - - Assert.NotEqual(default(RedisValue), right); - Assert.NotEqual(default(RedisValue), wrong); - Assert.NotEqual(right, wrong); - Assert.True(await t1, "1"); - Assert.False(await t1b, "1b"); - Assert.Equal(right, await t2); - if (withTran) Assert.False(await t3!, "3"); - Assert.Equal(right, await t4); - if (withTran) Assert.False(await t5!, "5"); - Assert.Equal(right, await t6); - var ttl = (await t7).Value.TotalSeconds; - Assert.True(ttl > 0 && ttl <= 20, "7"); - Assert.True(await t8, "8"); - Assert.Equal(right, await t9); - ttl = (await t10).Value.TotalSeconds; - Assert.True(ttl > 50 && ttl <= 60, "10"); - Assert.True(await t11, "11"); - Assert.Null((string?)await t12); - Assert.True(await t13, "13"); - } + _ = db.KeyDeleteAsync(key); + taken = db.LockTakeAsync(key, "new-value", TimeSpan.FromSeconds(10)); + newValue = db.StringGetAsync(key); + ttl = db.KeyTimeToLiveAsync(key); } + Assert.True(await taken!, "taken"); + Assert.Equal("new-value", await newValue!); + var ttlValue = (await ttl!).Value.TotalSeconds; + Assert.True(ttlValue >= 8 && ttlValue <= 10, "ttl"); - //public void TestManualLockOpCountByVersion(RedisConnection conn, int expected, bool existFirst) - //{ - // const int DB = 0, LockDuration = 30; - // const string Key = "TestManualLockOpCountByVersion"; - // conn.Wait(conn.Open()); - // conn.Keys.Remove(DB, Key); - // var newVal = "us:" + CreateUniqueName(); - // string expectedVal = newVal; - // if (existFirst) - // { - // expectedVal = "other:" + CreateUniqueName(); - // conn.Strings.Set(DB, Key, expectedVal, LockDuration); - // } - // int countBefore = conn.GetCounters().MessagesSent; - - // var tran = conn.CreateTransaction(); - // tran.AddCondition(Condition.KeyNotExists(DB, Key)); - // tran.Strings.Set(DB, Key, newVal, LockDuration); - // var taken = conn.Wait(tran.Execute()); - - // int countAfter = conn.GetCounters().MessagesSent; - // var valAfter = conn.Wait(conn.Strings.GetString(DB, Key)); - // Assert.Equal(!existFirst, taken, "lock taken (manual)"); - // Assert.Equal(expectedVal, valAfter, "taker (manual)"); - // Assert.Equal(expected, (countAfter - countBefore) - 1, "expected ops (including ping) (manual)"); - // // note we get a ping from GetCounters - //} - - [Theory, MemberData(nameof(TestModes))] - public async Task TestBasicLockNotTaken(TestMode testMode) - { - using (var conn = Create(testMode)) - { - int errorCount = 0; - conn.ErrorMessage += delegate { Interlocked.Increment(ref errorCount); }; - Task? taken = null; - Task? newValue = null; - Task? ttl = null; - - const int LOOP = 50; - var db = conn.GetDatabase(); - var key = Me(); - for (int i = 0; i < LOOP; i++) - { - _ = db.KeyDeleteAsync(key); - taken = db.LockTakeAsync(key, "new-value", TimeSpan.FromSeconds(10)); - newValue = db.StringGetAsync(key); - ttl = db.KeyTimeToLiveAsync(key); - } - Assert.True(await taken!, "taken"); - Assert.Equal("new-value", await newValue!); - var ttlValue = (await ttl!).Value.TotalSeconds; - Assert.True(ttlValue >= 8 && ttlValue <= 10, "ttl"); - - Assert.Equal(0, errorCount); - } - } + Assert.Equal(0, errorCount); + } - [Theory, MemberData(nameof(TestModes))] - public async Task TestBasicLockTaken(TestMode testMode) - { - using (var conn = Create(testMode)) - { - var db = conn.GetDatabase(); - var key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, "old-value", TimeSpan.FromSeconds(20), flags: CommandFlags.FireAndForget); - var taken = db.LockTakeAsync(key, "new-value", TimeSpan.FromSeconds(10)); - var newValue = db.StringGetAsync(key); - var ttl = db.KeyTimeToLiveAsync(key); - - Assert.False(await taken, "taken"); - Assert.Equal("old-value", await newValue); - var ttlValue = (await ttl).Value.TotalSeconds; - Assert.True(ttlValue >= 18 && ttlValue <= 20, "ttl"); - } - } + [Theory, MemberData(nameof(TestModes))] + public async Task TestBasicLockTaken(TestMode testMode) + { + using var conn = Create(testMode); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "old-value", TimeSpan.FromSeconds(20), flags: CommandFlags.FireAndForget); + var taken = db.LockTakeAsync(key, "new-value", TimeSpan.FromSeconds(10)); + var newValue = db.StringGetAsync(key); + var ttl = db.KeyTimeToLiveAsync(key); + + Assert.False(await taken, "taken"); + Assert.Equal("old-value", await newValue); + var ttlValue = (await ttl).Value.TotalSeconds; + Assert.True(ttlValue >= 18 && ttlValue <= 20, "ttl"); } } diff --git a/tests/StackExchange.Redis.Tests/MassiveOps.cs b/tests/StackExchange.Redis.Tests/MassiveOps.cs index a78aa44e1..000f22fe2 100644 --- a/tests/StackExchange.Redis.Tests/MassiveOps.cs +++ b/tests/StackExchange.Redis.Tests/MassiveOps.cs @@ -4,118 +4,113 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(NonParallelCollection.Name)] +public class MassiveOps : TestBase { - [Collection(NonParallelCollection.Name)] - public class MassiveOps : TestBase + public MassiveOps(ITestOutputHelper output) : base(output) { } + + [FactLongRunning] + public async Task LongRunning() { - public MassiveOps(ITestOutputHelper output) : base(output) { } + using var conn = Create(); - [FactLongRunning] - public async Task LongRunning() + var key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "test value", flags: CommandFlags.FireAndForget); + for (var i = 0; i < 200; i++) { - var key = Me(); - using (var conn = Create()) - { - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, "test value", flags: CommandFlags.FireAndForget); - for (var i = 0; i < 200; i++) - { - var val = await db.StringGetAsync(key).ForAwait(); - Assert.Equal("test value", val); - await Task.Delay(50).ForAwait(); - } - } + var val = await db.StringGetAsync(key).ForAwait(); + Assert.Equal("test value", val); + await Task.Delay(50).ForAwait(); } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MassiveBulkOpsAsync(bool withContinuation) + { + using var conn = Create(); - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task MassiveBulkOpsAsync(bool withContinuation) + RedisKey key = Me(); + var db = conn.GetDatabase(); + await db.PingAsync().ForAwait(); + static void nonTrivial(Task _) { - using (var muxer = Create()) - { - RedisKey key = Me(); - var conn = muxer.GetDatabase(); - await conn.PingAsync().ForAwait(); - static void nonTrivial(Task _) - { - Thread.SpinWait(5); - } - var watch = Stopwatch.StartNew(); - for (int i = 0; i <= AsyncOpsQty; i++) - { - var t = conn.StringSetAsync(key, i); - if (withContinuation) - { - // Intentionally unawaited - _ = t.ContinueWith(nonTrivial); - } - } - Assert.Equal(AsyncOpsQty, await conn.StringGetAsync(key).ForAwait()); - watch.Stop(); - Log("{2}: Time for {0} ops: {1}ms ({3}, any order); ops/s: {4}", AsyncOpsQty, watch.ElapsedMilliseconds, Me(), - withContinuation ? "with continuation" : "no continuation", AsyncOpsQty / watch.Elapsed.TotalSeconds); - } + Thread.SpinWait(5); } - - [TheoryLongRunning] - [InlineData(1)] - [InlineData(5)] - [InlineData(10)] - [InlineData(50)] - public void MassiveBulkOpsSync(int threads) + var watch = Stopwatch.StartNew(); + for (int i = 0; i <= AsyncOpsQty; i++) { - int workPerThread = SyncOpsQty / threads; - using (var muxer = Create(syncTimeout: 30000)) + var t = db.StringSetAsync(key, i); + if (withContinuation) { - RedisKey key = Me(); - var conn = muxer.GetDatabase(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - var timeTaken = RunConcurrent(delegate - { - for (int i = 0; i < workPerThread; i++) - { - conn.StringIncrement(key, flags: CommandFlags.FireAndForget); - } - }, threads); - - int val = (int)conn.StringGet(key); - Assert.Equal(workPerThread * threads, val); - Log("{2}: Time for {0} ops on {3} threads: {1}ms (any order); ops/s: {4}", - threads * workPerThread, timeTaken.TotalMilliseconds, Me(), threads, (workPerThread * threads) / timeTaken.TotalSeconds); + // Intentionally unawaited + _ = t.ContinueWith(nonTrivial); } } + Assert.Equal(AsyncOpsQty, await db.StringGetAsync(key).ForAwait()); + watch.Stop(); + Log("{2}: Time for {0} ops: {1}ms ({3}, any order); ops/s: {4}", AsyncOpsQty, watch.ElapsedMilliseconds, Me(), + withContinuation ? "with continuation" : "no continuation", AsyncOpsQty / watch.Elapsed.TotalSeconds); + } - [Theory] - [InlineData(1)] - [InlineData(5)] - public void MassiveBulkOpsFireAndForget(int threads) + [TheoryLongRunning] + [InlineData(1)] + [InlineData(5)] + [InlineData(10)] + [InlineData(50)] + public void MassiveBulkOpsSync(int threads) + { + using var conn = Create(syncTimeout: 30000); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + int workPerThread = SyncOpsQty / threads; + var timeTaken = RunConcurrent(delegate { - using (var muxer = Create(syncTimeout: 30000)) + for (int i = 0; i < workPerThread; i++) { - RedisKey key = Me(); - var conn = muxer.GetDatabase(); - conn.Ping(); + db.StringIncrement(key, flags: CommandFlags.FireAndForget); + } + }, threads); - conn.KeyDelete(key, CommandFlags.FireAndForget); - int perThread = AsyncOpsQty / threads; - var elapsed = RunConcurrent(delegate - { - for (int i = 0; i < perThread; i++) - { - conn.StringIncrement(key, flags: CommandFlags.FireAndForget); - } - conn.Ping(); - }, threads); - var val = (long)conn.StringGet(key); - Assert.Equal(perThread * threads, val); + int val = (int)db.StringGet(key); + Assert.Equal(workPerThread * threads, val); + Log("{2}: Time for {0} ops on {3} threads: {1}ms (any order); ops/s: {4}", + threads * workPerThread, timeTaken.TotalMilliseconds, Me(), threads, (workPerThread * threads) / timeTaken.TotalSeconds); + } + + [Theory] + [InlineData(1)] + [InlineData(5)] + public void MassiveBulkOpsFireAndForget(int threads) + { + using var conn = Create(syncTimeout: 30000); - Log("{2}: Time for {0} ops over {4} threads: {1:###,###}ms (any order); ops/s: {3:###,###,##0}", - val, elapsed.TotalMilliseconds, Me(), - val / elapsed.TotalSeconds, threads); + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.Ping(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + int perThread = AsyncOpsQty / threads; + var elapsed = RunConcurrent(delegate + { + for (int i = 0; i < perThread; i++) + { + db.StringIncrement(key, flags: CommandFlags.FireAndForget); } - } + db.Ping(); + }, threads); + var val = (long)db.StringGet(key); + Assert.Equal(perThread * threads, val); + + Log("{2}: Time for {0} ops over {4} threads: {1:###,###}ms (any order); ops/s: {3:###,###,##0}", + val, elapsed.TotalMilliseconds, Me(), + val / elapsed.TotalSeconds, threads); } } diff --git a/tests/StackExchange.Redis.Tests/Memory.cs b/tests/StackExchange.Redis.Tests/Memory.cs index 52277e478..b8fb7ea02 100644 --- a/tests/StackExchange.Redis.Tests/Memory.cs +++ b/tests/StackExchange.Redis.Tests/Memory.cs @@ -2,85 +2,76 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Memory : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Memory : TestBase + public Memory(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public async Task CanCallDoctor() + { + using var conn = Create(require: RedisFeatures.v4_0_0); + + var server = conn.GetServer(conn.GetEndPoints()[0]); + string? doctor = server.MemoryDoctor(); + Assert.NotNull(doctor); + Assert.NotEqual("", doctor); + + doctor = await server.MemoryDoctorAsync(); + Assert.NotNull(doctor); + Assert.NotEqual("", doctor); + } + + [Fact] + public async Task CanPurge() + { + using var conn = Create(require: RedisFeatures.v4_0_0); + + var server = conn.GetServer(conn.GetEndPoints()[0]); + server.MemoryPurge(); + await server.MemoryPurgeAsync(); + + await server.MemoryPurgeAsync(); + } + + [Fact] + public async Task GetAllocatorStats() { - public Memory(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - - [Fact] - public async Task CanCallDoctor() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v4_0_0); - var server = conn.GetServer(conn.GetEndPoints()[0]); - string? doctor = server.MemoryDoctor(); - Assert.NotNull(doctor); - Assert.NotEqual("", doctor); - - doctor = await server.MemoryDoctorAsync(); - Assert.NotNull(doctor); - Assert.NotEqual("", doctor); - } - } - - [Fact] - public async Task CanPurge() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v4_0_0); - var server = conn.GetServer(conn.GetEndPoints()[0]); - server.MemoryPurge(); - await server.MemoryPurgeAsync(); - - await server.MemoryPurgeAsync(); - } - } - - [Fact] - public async Task GetAllocatorStats() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v4_0_0); - var server = conn.GetServer(conn.GetEndPoints()[0]); - - var stats = server.MemoryAllocatorStats(); - Assert.False(string.IsNullOrWhiteSpace(stats)); - - stats = await server.MemoryAllocatorStatsAsync(); - Assert.False(string.IsNullOrWhiteSpace(stats)); - } - } - - [Fact] - public async Task GetStats() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v4_0_0); - var server = conn.GetServer(conn.GetEndPoints()[0]); - var stats = server.MemoryStats(); - Assert.NotNull(stats); - Assert.Equal(ResultType.MultiBulk, stats.Type); - - var parsed = stats.ToDictionary(); - - var alloc = parsed["total.allocated"]; - Assert.Equal(ResultType.Integer, alloc.Type); - Assert.True(alloc.AsInt64() > 0); - - stats = await server.MemoryStatsAsync(); - Assert.NotNull(stats); - Assert.Equal(ResultType.MultiBulk, stats.Type); - - alloc = parsed["total.allocated"]; - Assert.Equal(ResultType.Integer, alloc.Type); - Assert.True(alloc.AsInt64() > 0); - } - } + using var conn = Create(require: RedisFeatures.v4_0_0); + + var server = conn.GetServer(conn.GetEndPoints()[0]); + + var stats = server.MemoryAllocatorStats(); + Assert.False(string.IsNullOrWhiteSpace(stats)); + + stats = await server.MemoryAllocatorStatsAsync(); + Assert.False(string.IsNullOrWhiteSpace(stats)); + } + + [Fact] + public async Task GetStats() + { + using var conn = Create(require: RedisFeatures.v4_0_0); + + var server = conn.GetServer(conn.GetEndPoints()[0]); + var stats = server.MemoryStats(); + Assert.NotNull(stats); + Assert.Equal(ResultType.MultiBulk, stats.Type); + + var parsed = stats.ToDictionary(); + + var alloc = parsed["total.allocated"]; + Assert.Equal(ResultType.Integer, alloc.Type); + Assert.True(alloc.AsInt64() > 0); + + stats = await server.MemoryStatsAsync(); + Assert.NotNull(stats); + Assert.Equal(ResultType.MultiBulk, stats.Type); + + alloc = parsed["total.allocated"]; + Assert.Equal(ResultType.Integer, alloc.Type); + Assert.True(alloc.AsInt64() > 0); } } diff --git a/tests/StackExchange.Redis.Tests/Migrate.cs b/tests/StackExchange.Redis.Tests/Migrate.cs index bf9a454bf..9a497b4f5 100644 --- a/tests/StackExchange.Redis.Tests/Migrate.cs +++ b/tests/StackExchange.Redis.Tests/Migrate.cs @@ -4,55 +4,54 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class Migrate : TestBase { - public class Migrate : TestBase + public Migrate(ITestOutputHelper output) : base (output) { } + + [FactLongRunning] + public async Task Basic() + { + var fromConfig = new ConfigurationOptions { EndPoints = { { TestConfig.Current.SecureServer, TestConfig.Current.SecurePort } }, Password = TestConfig.Current.SecurePassword, AllowAdmin = true }; + var toConfig = new ConfigurationOptions { EndPoints = { { TestConfig.Current.PrimaryServer, TestConfig.Current.PrimaryPort } }, AllowAdmin = true }; + + using var fromConn = ConnectionMultiplexer.Connect(fromConfig, Writer); + using var toConn = ConnectionMultiplexer.Connect(toConfig, Writer); + + if (await IsWindows(fromConn) || await IsWindows(toConn)) + Skip.Inconclusive("'migrate' is unreliable on redis-64"); + + RedisKey key = Me(); + var fromDb = fromConn.GetDatabase(); + var toDb = toConn.GetDatabase(); + fromDb.KeyDelete(key, CommandFlags.FireAndForget); + toDb.KeyDelete(key, CommandFlags.FireAndForget); + fromDb.StringSet(key, "foo", flags: CommandFlags.FireAndForget); + var dest = toConn.GetEndPoints(true).Single(); + Log("Migrating key..."); + fromDb.KeyMigrate(key, dest, migrateOptions: MigrateOptions.Replace); + Log("Migration command complete"); + + // this is *meant* to be synchronous at the redis level, but + // we keep seeing it fail on the CI server where the key has *left* the origin, but + // has *not* yet arrived at the destination; adding a pause while we investigate with + // the redis folks + await UntilConditionAsync(TimeSpan.FromSeconds(15), () => !fromDb.KeyExists(key) && toDb.KeyExists(key)); + + Assert.False(fromDb.KeyExists(key), "Exists at source"); + Assert.True(toDb.KeyExists(key), "Exists at destination"); + string? s = toDb.StringGet(key); + Assert.Equal("foo", s); + } + + private static async Task IsWindows(ConnectionMultiplexer conn) { - public Migrate(ITestOutputHelper output) : base (output) { } - - [FactLongRunning] - public async Task Basic() - { - var fromConfig = new ConfigurationOptions { EndPoints = { { TestConfig.Current.SecureServer, TestConfig.Current.SecurePort } }, Password = TestConfig.Current.SecurePassword, AllowAdmin = true }; - var toConfig = new ConfigurationOptions { EndPoints = { { TestConfig.Current.PrimaryServer, TestConfig.Current.PrimaryPort } }, AllowAdmin = true }; - using (var from = ConnectionMultiplexer.Connect(fromConfig, Writer)) - using (var to = ConnectionMultiplexer.Connect(toConfig, Writer)) - { - if (await IsWindows(from) || await IsWindows(to)) - Skip.Inconclusive("'migrate' is unreliable on redis-64"); - - RedisKey key = Me(); - var fromDb = from.GetDatabase(); - var toDb = to.GetDatabase(); - fromDb.KeyDelete(key, CommandFlags.FireAndForget); - toDb.KeyDelete(key, CommandFlags.FireAndForget); - fromDb.StringSet(key, "foo", flags: CommandFlags.FireAndForget); - var dest = to.GetEndPoints(true).Single(); - Log("Migrating key..."); - fromDb.KeyMigrate(key, dest, migrateOptions: MigrateOptions.Replace); - Log("Migration command complete"); - - // this is *meant* to be synchronous at the redis level, but - // we keep seeing it fail on the CI server where the key has *left* the origin, but - // has *not* yet arrived at the destination; adding a pause while we investigate with - // the redis folks - await UntilConditionAsync(TimeSpan.FromSeconds(15), () => !fromDb.KeyExists(key) && toDb.KeyExists(key)); - - Assert.False(fromDb.KeyExists(key), "Exists at source"); - Assert.True(toDb.KeyExists(key), "Exists at destination"); - string? s = toDb.StringGet(key); - Assert.Equal("foo", s); - } - } - - private static async Task IsWindows(ConnectionMultiplexer conn) - { - var server = conn.GetServer(conn.GetEndPoints().First()); - var section = (await server.InfoAsync("server")).Single(); - var os = section.FirstOrDefault( - x => string.Equals("os", x.Key, StringComparison.OrdinalIgnoreCase)); - // note: WSL returns things like "os:Linux 4.4.0-17134-Microsoft x86_64" - return string.Equals("windows", os.Value, StringComparison.OrdinalIgnoreCase); - } + var server = conn.GetServer(conn.GetEndPoints().First()); + var section = (await server.InfoAsync("server")).Single(); + var os = section.FirstOrDefault( + x => string.Equals("os", x.Key, StringComparison.OrdinalIgnoreCase)); + // note: WSL returns things like "os:Linux 4.4.0-17134-Microsoft x86_64" + return string.Equals("windows", os.Value, StringComparison.OrdinalIgnoreCase); } } diff --git a/tests/StackExchange.Redis.Tests/MultiAdd.cs b/tests/StackExchange.Redis.Tests/MultiAdd.cs index 7bf48ec51..5f8dee77e 100644 --- a/tests/StackExchange.Redis.Tests/MultiAdd.cs +++ b/tests/StackExchange.Redis.Tests/MultiAdd.cs @@ -2,117 +2,111 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class MultiAdd : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class MultiAdd : TestBase + public MultiAdd(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public void AddSortedSetEveryWay() { - public MultiAdd(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + using var conn = Create(); + + var db = conn.GetDatabase(); + RedisKey key = Me(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.SortedSetAdd(key, "a", 1, CommandFlags.FireAndForget); + db.SortedSetAdd(key, new[] { + new SortedSetEntry("b", 2) }, CommandFlags.FireAndForget); + db.SortedSetAdd(key, new[] { + new SortedSetEntry("c", 3), + new SortedSetEntry("d", 4)}, CommandFlags.FireAndForget); + db.SortedSetAdd(key, new[] { + new SortedSetEntry("e", 5), + new SortedSetEntry("f", 6), + new SortedSetEntry("g", 7)}, CommandFlags.FireAndForget); + db.SortedSetAdd(key, new[] { + new SortedSetEntry("h", 8), + new SortedSetEntry("i", 9), + new SortedSetEntry("j", 10), + new SortedSetEntry("k", 11)}, CommandFlags.FireAndForget); + var vals = db.SortedSetRangeByScoreWithScores(key); + string s = string.Join(",", vals.OrderByDescending(x => x.Score).Select(x => x.Element)); + Assert.Equal("k,j,i,h,g,f,e,d,c,b,a", s); + s = string.Join(",", vals.OrderBy(x => x.Score).Select(x => x.Score)); + Assert.Equal("1,2,3,4,5,6,7,8,9,10,11", s); + } - [Fact] - public void AddSortedSetEveryWay() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); + [Fact] + public void AddHashEveryWay() + { + using var conn = Create(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.SortedSetAdd(key, "a", 1, CommandFlags.FireAndForget); - db.SortedSetAdd(key, new[] { - new SortedSetEntry("b", 2) }, CommandFlags.FireAndForget); - db.SortedSetAdd(key, new[] { - new SortedSetEntry("c", 3), - new SortedSetEntry("d", 4)}, CommandFlags.FireAndForget); - db.SortedSetAdd(key, new[] { - new SortedSetEntry("e", 5), - new SortedSetEntry("f", 6), - new SortedSetEntry("g", 7)}, CommandFlags.FireAndForget); - db.SortedSetAdd(key, new[] { - new SortedSetEntry("h", 8), - new SortedSetEntry("i", 9), - new SortedSetEntry("j", 10), - new SortedSetEntry("k", 11)}, CommandFlags.FireAndForget); - var vals = db.SortedSetRangeByScoreWithScores(key); - string s = string.Join(",", vals.OrderByDescending(x => x.Score).Select(x => x.Element)); - Assert.Equal("k,j,i,h,g,f,e,d,c,b,a", s); - s = string.Join(",", vals.OrderBy(x => x.Score).Select(x => x.Score)); - Assert.Equal("1,2,3,4,5,6,7,8,9,10,11", s); - } - } + var db = conn.GetDatabase(); + RedisKey key = Me(); - [Fact] - public void AddHashEveryWay() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.HashSet(key, "a", 1, flags: CommandFlags.FireAndForget); + db.HashSet(key, new[] { + new HashEntry("b", 2) }, CommandFlags.FireAndForget); + db.HashSet(key, new[] { + new HashEntry("c", 3), + new HashEntry("d", 4)}, CommandFlags.FireAndForget); + db.HashSet(key, new[] { + new HashEntry("e", 5), + new HashEntry("f", 6), + new HashEntry("g", 7)}, CommandFlags.FireAndForget); + db.HashSet(key, new[] { + new HashEntry("h", 8), + new HashEntry("i", 9), + new HashEntry("j", 10), + new HashEntry("k", 11)}, CommandFlags.FireAndForget); + var vals = db.HashGetAll(key); + string s = string.Join(",", vals.OrderByDescending(x => (double)x.Value).Select(x => x.Name)); + Assert.Equal("k,j,i,h,g,f,e,d,c,b,a", s); + s = string.Join(",", vals.OrderBy(x => (double)x.Value).Select(x => x.Value)); + Assert.Equal("1,2,3,4,5,6,7,8,9,10,11", s); + } - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.HashSet(key, "a", 1, flags: CommandFlags.FireAndForget); - db.HashSet(key, new[] { - new HashEntry("b", 2) }, CommandFlags.FireAndForget); - db.HashSet(key, new[] { - new HashEntry("c", 3), - new HashEntry("d", 4)}, CommandFlags.FireAndForget); - db.HashSet(key, new[] { - new HashEntry("e", 5), - new HashEntry("f", 6), - new HashEntry("g", 7)}, CommandFlags.FireAndForget); - db.HashSet(key, new[] { - new HashEntry("h", 8), - new HashEntry("i", 9), - new HashEntry("j", 10), - new HashEntry("k", 11)}, CommandFlags.FireAndForget); - var vals = db.HashGetAll(key); - string s = string.Join(",", vals.OrderByDescending(x => (double)x.Value).Select(x => x.Name)); - Assert.Equal("k,j,i,h,g,f,e,d,c,b,a", s); - s = string.Join(",", vals.OrderBy(x => (double)x.Value).Select(x => x.Value)); - Assert.Equal("1,2,3,4,5,6,7,8,9,10,11", s); - } - } + [Fact] + public void AddSetEveryWay() + { + using var conn = Create(); - [Fact] - public void AddSetEveryWay() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); + var db = conn.GetDatabase(); + RedisKey key = Me(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.SetAdd(key, "a", CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "b" }, CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "c", "d" }, CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "e", "f", "g" }, CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "h", "i", "j", "k" }, CommandFlags.FireAndForget); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.SetAdd(key, "a", CommandFlags.FireAndForget); + db.SetAdd(key, new RedisValue[] { "b" }, CommandFlags.FireAndForget); + db.SetAdd(key, new RedisValue[] { "c", "d" }, CommandFlags.FireAndForget); + db.SetAdd(key, new RedisValue[] { "e", "f", "g" }, CommandFlags.FireAndForget); + db.SetAdd(key, new RedisValue[] { "h", "i", "j", "k" }, CommandFlags.FireAndForget); - var vals = db.SetMembers(key); - string s = string.Join(",", vals.OrderByDescending(x => x)); - Assert.Equal("k,j,i,h,g,f,e,d,c,b,a", s); - } - } + var vals = db.SetMembers(key); + string s = string.Join(",", vals.OrderByDescending(x => x)); + Assert.Equal("k,j,i,h,g,f,e,d,c,b,a", s); + } - [Fact] - public void AddSetEveryWayNumbers() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); + [Fact] + public void AddSetEveryWayNumbers() + { + using var conn = Create(); + var db = conn.GetDatabase(); + RedisKey key = Me(); - RedisKey key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.SetAdd(key, "a", CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "1" }, CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "11", "2" }, CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "10", "3", "1.5" }, CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "2.2", "-1", "s", "t" }, CommandFlags.FireAndForget); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.SetAdd(key, "a", CommandFlags.FireAndForget); + db.SetAdd(key, new RedisValue[] { "1" }, CommandFlags.FireAndForget); + db.SetAdd(key, new RedisValue[] { "11", "2" }, CommandFlags.FireAndForget); + db.SetAdd(key, new RedisValue[] { "10", "3", "1.5" }, CommandFlags.FireAndForget); + db.SetAdd(key, new RedisValue[] { "2.2", "-1", "s", "t" }, CommandFlags.FireAndForget); - var vals = db.SetMembers(key); - string s = string.Join(",", vals.OrderByDescending(x => x)); - Assert.Equal("t,s,a,11,10,3,2.2,2,1.5,1,-1", s); - } - } + var vals = db.SetMembers(key); + string s = string.Join(",", vals.OrderByDescending(x => x)); + Assert.Equal("t,s,a,11,10,3,2.2,2,1.5,1,-1", s); } } diff --git a/tests/StackExchange.Redis.Tests/MultiPrimary.cs b/tests/StackExchange.Redis.Tests/MultiPrimary.cs index 022fd3d5f..240213798 100644 --- a/tests/StackExchange.Redis.Tests/MultiPrimary.cs +++ b/tests/StackExchange.Redis.Tests/MultiPrimary.cs @@ -4,92 +4,90 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class MultiPrimary : TestBase { - public class MultiPrimary : TestBase + protected override string GetConfiguration() => + TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword; + public MultiPrimary(ITestOutputHelper output) : base (output) { } + + [Fact] + public void CannotFlushReplica() { - protected override string GetConfiguration() => - TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword; - public MultiPrimary(ITestOutputHelper output) : base (output) { } + var ex = Assert.Throws(() => + { + using var conn = ConnectionMultiplexer.Connect(TestConfig.Current.ReplicaServerAndPort + ",allowAdmin=true"); + + var servers = conn.GetEndPoints().Select(e => conn.GetServer(e)); + var replica = servers.FirstOrDefault(x => x.IsReplica); + Assert.NotNull(replica); // replica not found, ruh roh + replica.FlushDatabase(); + }); + Assert.Equal("Command cannot be issued to a replica: FLUSHDB", ex.Message); + } - [Fact] - public void CannotFlushReplica() + [Fact] + public void TestMultiNoTieBreak() + { + var log = new StringBuilder(); + Writer.EchoTo(log); + using (Create(log: Writer, tieBreaker: "")) { - var ex = Assert.Throws(() => - { - using (var conn = ConnectionMultiplexer.Connect(TestConfig.Current.ReplicaServerAndPort + ",allowAdmin=true")) - { - var servers = conn.GetEndPoints().Select(e => conn.GetServer(e)); - var replica = servers.FirstOrDefault(x => x.IsReplica); - Assert.NotNull(replica); // replica not found, ruh roh - replica.FlushDatabase(); - } - }); - Assert.Equal("Command cannot be issued to a replica: FLUSHDB", ex.Message); + Assert.Contains("Choosing primary arbitrarily", log.ToString()); } + } + + public static IEnumerable GetConnections() + { + yield return new object[] { TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.PrimaryServerAndPort }; + yield return new object[] { TestConfig.Current.SecureServerAndPort, TestConfig.Current.SecureServerAndPort, TestConfig.Current.SecureServerAndPort }; + yield return new object?[] { TestConfig.Current.SecureServerAndPort, TestConfig.Current.PrimaryServerAndPort, null }; + yield return new object?[] { TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.SecureServerAndPort, null }; - [Fact] - public void TestMultiNoTieBreak() + yield return new object?[] { null, TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.PrimaryServerAndPort }; + yield return new object?[] { TestConfig.Current.PrimaryServerAndPort, null, TestConfig.Current.PrimaryServerAndPort }; + yield return new object?[] { null, TestConfig.Current.SecureServerAndPort, TestConfig.Current.SecureServerAndPort }; + yield return new object?[] { TestConfig.Current.SecureServerAndPort, null, TestConfig.Current.SecureServerAndPort }; + yield return new object?[] { null, null, null }; + } + + [Theory, MemberData(nameof(GetConnections))] + public void TestMultiWithTiebreak(string a, string b, string elected) + { + const string TieBreak = "__tie__"; + // set the tie-breakers to the expected state + using (var aConn = ConnectionMultiplexer.Connect(TestConfig.Current.PrimaryServerAndPort)) { - var log = new StringBuilder(); - Writer.EchoTo(log); - using (Create(log: Writer, tieBreaker: "")) - { - Assert.Contains("Choosing primary arbitrarily", log.ToString()); - } + aConn.GetDatabase().StringSet(TieBreak, a); } - - public static IEnumerable GetConnections() + using (var aConn = ConnectionMultiplexer.Connect(TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword)) { - yield return new object[] { TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.PrimaryServerAndPort }; - yield return new object[] { TestConfig.Current.SecureServerAndPort, TestConfig.Current.SecureServerAndPort, TestConfig.Current.SecureServerAndPort }; - yield return new object?[] { TestConfig.Current.SecureServerAndPort, TestConfig.Current.PrimaryServerAndPort, null }; - yield return new object?[] { TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.SecureServerAndPort, null }; - - yield return new object?[] { null, TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.PrimaryServerAndPort }; - yield return new object?[] { TestConfig.Current.PrimaryServerAndPort, null, TestConfig.Current.PrimaryServerAndPort }; - yield return new object?[] { null, TestConfig.Current.SecureServerAndPort, TestConfig.Current.SecureServerAndPort }; - yield return new object?[] { TestConfig.Current.SecureServerAndPort, null, TestConfig.Current.SecureServerAndPort }; - yield return new object?[] { null, null, null }; + aConn.GetDatabase().StringSet(TieBreak, b); } - [Theory, MemberData(nameof(GetConnections))] - public void TestMultiWithTiebreak(string a, string b, string elected) + // see what happens + var log = new StringBuilder(); + Writer.EchoTo(log); + + using (Create(log: Writer, tieBreaker: TieBreak)) { - const string TieBreak = "__tie__"; - // set the tie-breakers to the expected state - using (var aConn = ConnectionMultiplexer.Connect(TestConfig.Current.PrimaryServerAndPort)) + string text = log.ToString(); + Assert.False(text.Contains("failed to nominate"), "failed to nominate"); + if (elected != null) { - aConn.GetDatabase().StringSet(TieBreak, a); + Assert.True(text.Contains("Elected: " + elected), "elected"); } - using (var aConn = ConnectionMultiplexer.Connect(TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword)) + int nullCount = (a == null ? 1 : 0) + (b == null ? 1 : 0); + if ((a == b && nullCount == 0) || nullCount == 1) { - aConn.GetDatabase().StringSet(TieBreak, b); + Assert.True(text.Contains("Election: Tie-breaker unanimous"), "unanimous"); + Assert.False(text.Contains("Election: Choosing primary arbitrarily"), "arbitrarily"); } - - // see what happens - var log = new StringBuilder(); - Writer.EchoTo(log); - - using (Create(log: Writer, tieBreaker: TieBreak)) + else { - string text = log.ToString(); - Assert.False(text.Contains("failed to nominate"), "failed to nominate"); - if (elected != null) - { - Assert.True(text.Contains("Elected: " + elected), "elected"); - } - int nullCount = (a == null ? 1 : 0) + (b == null ? 1 : 0); - if ((a == b && nullCount == 0) || nullCount == 1) - { - Assert.True(text.Contains("Election: Tie-breaker unanimous"), "unanimous"); - Assert.False(text.Contains("Election: Choosing primary arbitrarily"), "arbitrarily"); - } - else - { - Assert.False(text.Contains("Election: Tie-breaker unanimous"), "unanimous"); - Assert.True(text.Contains("Election: Choosing primary arbitrarily"), "arbitrarily"); - } + Assert.False(text.Contains("Election: Tie-breaker unanimous"), "unanimous"); + Assert.True(text.Contains("Election: Choosing primary arbitrarily"), "arbitrarily"); } } } diff --git a/tests/StackExchange.Redis.Tests/Naming.cs b/tests/StackExchange.Redis.Tests/Naming.cs index 6305bc6b9..224d09b5f 100644 --- a/tests/StackExchange.Redis.Tests/Naming.cs +++ b/tests/StackExchange.Redis.Tests/Naming.cs @@ -6,232 +6,220 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class Naming : TestBase { - public class Naming : TestBase - { - public Naming(ITestOutputHelper output) : base(output) { } + public Naming(ITestOutputHelper output) : base(output) { } - [Theory] - [InlineData(typeof(IDatabase), false)] - [InlineData(typeof(IDatabaseAsync), true)] - [InlineData(typeof(Condition), false)] - public void CheckSignatures(Type type, bool isAsync) + [Theory] + [InlineData(typeof(IDatabase), false)] + [InlineData(typeof(IDatabaseAsync), true)] + [InlineData(typeof(Condition), false)] + public void CheckSignatures(Type type, bool isAsync) + { + // check that all methods and interfaces look appropriate for their sync/async nature + CheckName(type, isAsync); + var members = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); + foreach (var member in members) { - // check that all methods and interfaces look appropriate for their sync/async nature - CheckName(type, isAsync); - var members = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); - foreach (var member in members) - { - if (member.Name.StartsWith("get_") || member.Name.StartsWith("set_") || member.Name.StartsWith("add_") || member.Name.StartsWith("remove_")) continue; - CheckMethod(member, isAsync); - } + if (member.Name.StartsWith("get_") || member.Name.StartsWith("set_") || member.Name.StartsWith("add_") || member.Name.StartsWith("remove_")) continue; + CheckMethod(member, isAsync); } + } - [Fact] - public void ShowReadOnlyOperations() + [Fact] + public void ShowReadOnlyOperations() + { + List primaryReplica = new List(); + List primaryOnly = new List(); + foreach (var val in (RedisCommand[])Enum.GetValues(typeof(RedisCommand))) { - List primaryReplica = new List(); - List primaryOnly = new List(); - foreach (var val in (RedisCommand[])Enum.GetValues(typeof(RedisCommand))) - { - bool isPrimaryOnly = Message.IsPrimaryOnly(val); - (isPrimaryOnly ? primaryOnly : primaryReplica).Add(val); + bool isPrimaryOnly = Message.IsPrimaryOnly(val); + (isPrimaryOnly ? primaryOnly : primaryReplica).Add(val); - if (!isPrimaryOnly) - { - Log(val.ToString()); - } - } - Log("primary-only: {0}, vs primary/replica: {1}", primaryOnly.Count, primaryReplica.Count); - Log(""); - Log("primary-only:"); - foreach (var val in primaryOnly) - { - Log(val?.ToString()); - } - Log(""); - Log("primary/replica:"); - foreach (var val in primaryReplica) + if (!isPrimaryOnly) { - Log(val?.ToString()); + Log(val.ToString()); } } - - [Theory] - [InlineData(typeof(IDatabase))] - [InlineData(typeof(IDatabaseAsync))] - public void CheckDatabaseMethodsUseKeys(Type type) + Log("primary-only: {0}, vs primary/replica: {1}", primaryOnly.Count, primaryReplica.Count); + Log(""); + Log("primary-only:"); + foreach (var val in primaryOnly) { - foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) - { - if (IgnoreMethodConventions(method)) continue; - - switch (method.Name) - { - case nameof(IDatabase.KeyRandom): - case nameof(IDatabaseAsync.KeyRandomAsync): - case nameof(IDatabase.Publish): - case nameof(IDatabaseAsync.PublishAsync): - case nameof(IDatabase.Execute): - case nameof(IDatabaseAsync.ExecuteAsync): - case nameof(IDatabase.ScriptEvaluate): - case nameof(IDatabaseAsync.ScriptEvaluateAsync): - case nameof(IDatabase.StreamRead): - case nameof(IDatabase.StreamReadAsync): - case nameof(IDatabase.StreamReadGroup): - case nameof(IDatabase.StreamReadGroupAsync): - continue; // they're fine, but don't want to widen check to return type - } - - bool usesKey = method.GetParameters().Any(p => UsesKey(p.ParameterType)); - Assert.True(usesKey, type.Name + ":" + method.Name); - } + Log(val?.ToString()); } - - private static bool UsesKey(Type type) + Log(""); + Log("primary/replica:"); + foreach (var val in primaryReplica) { - if (type == typeof(RedisKey)) return true; - - if (type.IsArray) - { - if (UsesKey(type.GetElementType()!)) return true; - } - if (type.IsGenericType) // KVP, etc - { - var args = type.GetGenericArguments(); - if (args.Any(UsesKey)) return true; - } - return false; + Log(val?.ToString()); } + } - private static bool IgnoreMethodConventions(MethodInfo method) + [Theory] + [InlineData(typeof(IDatabase))] + [InlineData(typeof(IDatabaseAsync))] + public void CheckDatabaseMethodsUseKeys(Type type) + { + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) { - string name = method.Name; - if (name.StartsWith("get_") || name.StartsWith("set_") || name.StartsWith("add_") || name.StartsWith("remove_")) return true; - switch (name) + if (IgnoreMethodConventions(method)) continue; + + switch (method.Name) { - case nameof(IDatabase.CreateBatch): - case nameof(IDatabase.CreateTransaction): + case nameof(IDatabase.KeyRandom): + case nameof(IDatabaseAsync.KeyRandomAsync): + case nameof(IDatabase.Publish): + case nameof(IDatabaseAsync.PublishAsync): case nameof(IDatabase.Execute): case nameof(IDatabaseAsync.ExecuteAsync): - case nameof(IDatabase.IsConnected): - case nameof(IDatabase.SetScan): - case nameof(IDatabase.SortedSetScan): - case nameof(IDatabase.HashScan): - case nameof(ISubscriber.SubscribedEndpoint): - return true; + case nameof(IDatabase.ScriptEvaluate): + case nameof(IDatabaseAsync.ScriptEvaluateAsync): + case nameof(IDatabase.StreamRead): + case nameof(IDatabase.StreamReadAsync): + case nameof(IDatabase.StreamReadGroup): + case nameof(IDatabase.StreamReadGroupAsync): + continue; // they're fine, but don't want to widen check to return type } - return false; + + bool usesKey = method.GetParameters().Any(p => UsesKey(p.ParameterType)); + Assert.True(usesKey, type.Name + ":" + method.Name); } + } - [Theory] - [InlineData(typeof(IDatabase), typeof(IDatabaseAsync))] - [InlineData(typeof(IDatabaseAsync), typeof(IDatabase))] - public void CheckSyncAsyncMethodsMatch(Type from, Type to) + private static bool UsesKey(Type type) => + type == typeof(RedisKey) + || (type.IsArray && UsesKey(type.GetElementType()!)) + || (type.IsGenericType && type.GetGenericArguments().Any(UsesKey)); + + private static bool IgnoreMethodConventions(MethodInfo method) + { + string name = method.Name; + if (name.StartsWith("get_") || name.StartsWith("set_") || name.StartsWith("add_") || name.StartsWith("remove_")) return true; + switch (name) { - const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; - int count = 0; - foreach (var method in from.GetMethods(flags)) - { - if (IgnoreMethodConventions(method)) continue; - - string name = method.Name, huntName; - - if (name.EndsWith("Async")) huntName = name.Substring(0, name.Length - 5); - else huntName = name + "Async"; - var pFrom = method.GetParameters(); - Type[] args = pFrom.Select(x => x.ParameterType).ToArray(); - Log("Checking: {0}.{1}", from.Name, method.Name); - Assert.Equal(typeof(CommandFlags), args.Last()); - var found = to.GetMethod(huntName, flags, null, method.CallingConvention, args, null); - Assert.NotNull(found); // "Found " + name + ", no " + huntName - var pTo = found.GetParameters(); - - for (int i = 0; i < pFrom.Length; i++) - { - Assert.Equal(pFrom[i].Name, pTo[i].Name); // method.Name + ":" + pFrom[i].Name - Assert.Equal(pFrom[i].ParameterType, pTo[i].ParameterType); // method.Name + ":" + pFrom[i].Name - } - - count++; - } - Log("Validated: {0} ({1} methods)", from.Name, count); + case nameof(IDatabase.CreateBatch): + case nameof(IDatabase.CreateTransaction): + case nameof(IDatabase.Execute): + case nameof(IDatabaseAsync.ExecuteAsync): + case nameof(IDatabase.IsConnected): + case nameof(IDatabase.SetScan): + case nameof(IDatabase.SortedSetScan): + case nameof(IDatabase.HashScan): + case nameof(ISubscriber.SubscribedEndpoint): + return true; } + return false; + } - private void CheckMethod(MethodInfo method, bool isAsync) + [Theory] + [InlineData(typeof(IDatabase), typeof(IDatabaseAsync))] + [InlineData(typeof(IDatabaseAsync), typeof(IDatabase))] + public void CheckSyncAsyncMethodsMatch(Type from, Type to) + { + const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + int count = 0; + foreach (var method in from.GetMethods(flags)) { - string shortName = method.Name, fullName = method.DeclaringType?.Name + "." + shortName; + if (IgnoreMethodConventions(method)) continue; + + string name = method.Name, huntName; - switch (shortName) + if (name.EndsWith("Async")) huntName = name.Substring(0, name.Length - 5); + else huntName = name + "Async"; + var pFrom = method.GetParameters(); + Type[] args = pFrom.Select(x => x.ParameterType).ToArray(); + Log("Checking: {0}.{1}", from.Name, method.Name); + Assert.Equal(typeof(CommandFlags), args.Last()); + var found = to.GetMethod(huntName, flags, null, method.CallingConvention, args, null); + Assert.NotNull(found); // "Found " + name + ", no " + huntName + var pTo = found.GetParameters(); + + for (int i = 0; i < pFrom.Length; i++) { - case nameof(IDatabaseAsync.IsConnected): - return; - case nameof(IDatabase.CreateBatch): - case nameof(IDatabase.CreateTransaction): - case nameof(IDatabase.IdentifyEndpoint): - case nameof(IDatabase.Sort): - case nameof(IDatabase.SortAndStore): - case nameof(IDatabaseAsync.IdentifyEndpointAsync): - case nameof(IDatabaseAsync.SortAsync): - case nameof(IDatabaseAsync.SortAndStoreAsync): - CheckName(method, isAsync); - break; - default: - CheckName(method, isAsync); - var isValid = shortName.StartsWith("Debug") - || shortName.StartsWith("Execute") - || shortName.StartsWith("Geo") - || shortName.StartsWith("Hash") - || shortName.StartsWith("HyperLogLog") - || shortName.StartsWith("Key") - || shortName.StartsWith("List") - || shortName.StartsWith("Lock") - || shortName.StartsWith("Publish") - || shortName.StartsWith("Set") - || shortName.StartsWith("Script") - || shortName.StartsWith("SortedSet") - || shortName.StartsWith("String") - || shortName.StartsWith("Stream"); - Log(fullName + ": " + (isValid ? "valid" : "invalid")); - Assert.True(isValid, fullName + ":Prefix"); - break; + Assert.Equal(pFrom[i].Name, pTo[i].Name); // method.Name + ":" + pFrom[i].Name + Assert.Equal(pFrom[i].ParameterType, pTo[i].ParameterType); // method.Name + ":" + pFrom[i].Name } - Assert.False(shortName.Contains("If"), fullName + ":If"); // should probably be a When option + count++; + } + Log("Validated: {0} ({1} methods)", from.Name, count); + } - var returnType = method.ReturnType ?? typeof(void); + private void CheckMethod(MethodInfo method, bool isAsync) + { + string shortName = method.Name, fullName = method.DeclaringType?.Name + "." + shortName; - if (isAsync) - { - Assert.True(IsAsyncMethod(returnType), fullName + ":Task"); - } - else - { - Assert.False(IsAsyncMethod(returnType), fullName + ":Task"); - } + switch (shortName) + { + case nameof(IDatabaseAsync.IsConnected): + return; + case nameof(IDatabase.CreateBatch): + case nameof(IDatabase.CreateTransaction): + case nameof(IDatabase.IdentifyEndpoint): + case nameof(IDatabase.Sort): + case nameof(IDatabase.SortAndStore): + case nameof(IDatabaseAsync.IdentifyEndpointAsync): + case nameof(IDatabaseAsync.SortAsync): + case nameof(IDatabaseAsync.SortAndStoreAsync): + CheckName(method, isAsync); + break; + default: + CheckName(method, isAsync); + var isValid = shortName.StartsWith("Debug") + || shortName.StartsWith("Execute") + || shortName.StartsWith("Geo") + || shortName.StartsWith("Hash") + || shortName.StartsWith("HyperLogLog") + || shortName.StartsWith("Key") + || shortName.StartsWith("List") + || shortName.StartsWith("Lock") + || shortName.StartsWith("Publish") + || shortName.StartsWith("Set") + || shortName.StartsWith("Script") + || shortName.StartsWith("SortedSet") + || shortName.StartsWith("String") + || shortName.StartsWith("Stream"); + Log(fullName + ": " + (isValid ? "valid" : "invalid")); + Assert.True(isValid, fullName + ":Prefix"); + break; + } - static bool IsAsyncMethod(Type returnType) - { - if (returnType == typeof(Task)) return true; - if (returnType == typeof(ValueTask)) return true; - - if (returnType.IsGenericType) - { - var genDef = returnType.GetGenericTypeDefinition(); - if (genDef == typeof(Task<>)) return true; - if (genDef == typeof(ValueTask<>)) return true; - if (genDef == typeof(IAsyncEnumerable<>)) return true; - } - - return false; - } + Assert.False(shortName.Contains("If"), fullName + ":If"); // should probably be a When option + + var returnType = method.ReturnType ?? typeof(void); + + if (isAsync) + { + Assert.True(IsAsyncMethod(returnType), fullName + ":Task"); + } + else + { + Assert.False(IsAsyncMethod(returnType), fullName + ":Task"); } - private static void CheckName(MemberInfo member, bool isAsync) + static bool IsAsyncMethod(Type returnType) { - if (isAsync) Assert.True(member.Name.EndsWith("Async"), member.Name + ":Name - end *Async"); - else Assert.False(member.Name.EndsWith("Async"), member.Name + ":Name - don't end *Async"); + if (returnType == typeof(Task)) return true; + if (returnType == typeof(ValueTask)) return true; + + if (returnType.IsGenericType) + { + var genDef = returnType.GetGenericTypeDefinition(); + if (genDef == typeof(Task<>)) return true; + if (genDef == typeof(ValueTask<>)) return true; + if (genDef == typeof(IAsyncEnumerable<>)) return true; + } + + return false; } } + + private static void CheckName(MemberInfo member, bool isAsync) + { + if (isAsync) Assert.True(member.Name.EndsWith("Async"), member.Name + ":Name - end *Async"); + else Assert.False(member.Name.EndsWith("Async"), member.Name + ":Name - don't end *Async"); + } } diff --git a/tests/StackExchange.Redis.Tests/Parse.cs b/tests/StackExchange.Redis.Tests/Parse.cs index 27bdea01a..e0a46a2ce 100644 --- a/tests/StackExchange.Redis.Tests/Parse.cs +++ b/tests/StackExchange.Redis.Tests/Parse.cs @@ -6,95 +6,94 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class ParseTests : TestBase { - public class ParseTests : TestBase + public ParseTests(ITestOutputHelper output) : base(output) { } + + public static IEnumerable GetTestData() { - public ParseTests(ITestOutputHelper output) : base(output) { } + yield return new object[] { "$4\r\nPING\r\n$4\r\nPON", 1 }; + yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG", 1 }; + yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r", 1 }; + yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n", 2 }; + yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4", 2 }; + yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r", 2 }; + yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\n", 2 }; + yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\nP", 2 }; + yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\nPO", 2 }; + yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\nPON", 2 }; + yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\nPONG", 2 }; + yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\nPONG\r", 2 }; + yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\nPONG\r\n", 3 }; + yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\nPONG\r\n$", 3 }; + } - public static IEnumerable GetTestData() + [Theory] + [MemberData(nameof(GetTestData))] + public void ParseAsSingleChunk(string ascii, int expected) + { + var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(ascii)); + using (var arena = new Arena()) { - yield return new object[] { "$4\r\nPING\r\n$4\r\nPON", 1 }; - yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG", 1 }; - yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r", 1 }; - yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n", 2 }; - yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4", 2 }; - yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r", 2 }; - yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\n", 2 }; - yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\nP", 2 }; - yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\nPO", 2 }; - yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\nPON", 2 }; - yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\nPONG", 2 }; - yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\nPONG\r", 2 }; - yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\nPONG\r\n", 3 }; - yield return new object[] { "$4\r\nPING\r\n$4\r\nPONG\r\n$4\r\nPONG\r\n$", 3 }; - } - - [Theory] - [MemberData(nameof(GetTestData))] - public void ParseAsSingleChunk(string ascii, int expected) - { - var buffer = new ReadOnlySequence(Encoding.ASCII.GetBytes(ascii)); - using (var arena = new Arena()) - { - ProcessMessages(arena, buffer, expected); - } + ProcessMessages(arena, buffer, expected); } + } - [Theory] - [MemberData(nameof(GetTestData))] - public void ParseAsLotsOfChunks(string ascii, int expected) + [Theory] + [MemberData(nameof(GetTestData))] + public void ParseAsLotsOfChunks(string ascii, int expected) + { + var bytes = Encoding.ASCII.GetBytes(ascii); + FragmentedSegment? chain = null, tail = null; + for (int i = 0; i < bytes.Length; i++) { - var bytes = Encoding.ASCII.GetBytes(ascii); - FragmentedSegment? chain = null, tail = null; - for (int i = 0; i < bytes.Length; i++) + var next = new FragmentedSegment(i, new ReadOnlyMemory(bytes, i, 1)); + if (tail == null) { - var next = new FragmentedSegment(i, new ReadOnlyMemory(bytes, i, 1)); - if (tail == null) - { - chain = next; - } - else - { - tail.Next = next; - } - tail = next; + chain = next; } - var buffer = new ReadOnlySequence(chain!, 0, tail!, 1); - Assert.Equal(bytes.Length, buffer.Length); - using (var arena = new Arena()) + else { - ProcessMessages(arena, buffer, expected); + tail.Next = next; } + tail = next; + } + var buffer = new ReadOnlySequence(chain!, 0, tail!, 1); + Assert.Equal(bytes.Length, buffer.Length); + using (var arena = new Arena()) + { + ProcessMessages(arena, buffer, expected); } + } - private void ProcessMessages(Arena arena, ReadOnlySequence buffer, int expected) + private void ProcessMessages(Arena arena, ReadOnlySequence buffer, int expected) + { + Writer.WriteLine($"chain: {buffer.Length}"); + var reader = new BufferReader(buffer); + RawResult result; + int found = 0; + while (!(result = PhysicalConnection.TryParseResult(arena, buffer, ref reader, false, null, false)).IsNull) { - Writer.WriteLine($"chain: {buffer.Length}"); - var reader = new BufferReader(buffer); - RawResult result; - int found = 0; - while (!(result = PhysicalConnection.TryParseResult(arena, buffer, ref reader, false, null, false)).IsNull) - { - Writer.WriteLine($"{result} - {result.GetString()}"); - found++; - } - Assert.Equal(expected, found); + Writer.WriteLine($"{result} - {result.GetString()}"); + found++; } + Assert.Equal(expected, found); + } - private class FragmentedSegment : ReadOnlySequenceSegment + private class FragmentedSegment : ReadOnlySequenceSegment + { + public FragmentedSegment(long runningIndex, ReadOnlyMemory memory) { - public FragmentedSegment(long runningIndex, ReadOnlyMemory memory) - { - RunningIndex = runningIndex; - Memory = memory; - } + RunningIndex = runningIndex; + Memory = memory; + } - public new FragmentedSegment? Next - { - get => (FragmentedSegment?)base.Next; - set => base.Next = value; - } + public new FragmentedSegment? Next + { + get => (FragmentedSegment?)base.Next; + set => base.Next = value; } } } diff --git a/tests/StackExchange.Redis.Tests/Performance.cs b/tests/StackExchange.Redis.Tests/Performance.cs index bab2b6412..34807aab3 100644 --- a/tests/StackExchange.Redis.Tests/Performance.cs +++ b/tests/StackExchange.Redis.Tests/Performance.cs @@ -4,129 +4,127 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(NonParallelCollection.Name)] +public class Performance : TestBase { - [Collection(NonParallelCollection.Name)] - public class Performance : TestBase - { - public Performance(ITestOutputHelper output) : base(output) { } + public Performance(ITestOutputHelper output) : base(output) { } - [FactLongRunning] - public void VerifyPerformanceImprovement() + [FactLongRunning] + public void VerifyPerformanceImprovement() + { + int asyncTimer, sync, op = 0, asyncFaF, syncFaF; + var key = Me(); + using (var conn = Create()) { - int asyncTimer, sync, op = 0, asyncFaF, syncFaF; - var key = Me(); - using (var muxer = Create()) + // do these outside the timings, just to ensure the core methods are JITted etc + for (int dbId = 0; dbId < 5; dbId++) { - // do these outside the timings, just to ensure the core methods are JITted etc - for (int db = 0; db < 5; db++) - { - muxer.GetDatabase(db).KeyDeleteAsync(key); - } + conn.GetDatabase(dbId).KeyDeleteAsync(key); + } - var timer = Stopwatch.StartNew(); - for (int i = 0; i < 100; i++) - { - // want to test multiplex scenario; test each db, but to make it fair we'll - // do in batches of 10 on each - for (int db = 0; db < 5; db++) - { - var conn = muxer.GetDatabase(db); - for (int j = 0; j < 10; j++) - conn.StringIncrementAsync(key); - } - } - asyncFaF = (int)timer.ElapsedMilliseconds; - var final = new Task[5]; - for (int db = 0; db < 5; db++) - final[db] = muxer.GetDatabase(db).StringGetAsync(key); - muxer.WaitAll(final); - timer.Stop(); - asyncTimer = (int)timer.ElapsedMilliseconds; - Log("async to completion (local): {0}ms", timer.ElapsedMilliseconds); - for (int db = 0; db < 5; db++) + var timer = Stopwatch.StartNew(); + for (int i = 0; i < 100; i++) + { + // want to test multiplex scenario; test each db, but to make it fair we'll + // do in batches of 10 on each + for (int dbId = 0; dbId < 5; dbId++) { - Assert.Equal(1000, (long)final[db].Result); // "async, db:" + db + var db = conn.GetDatabase(dbId); + for (int j = 0; j < 10; j++) + db.StringIncrementAsync(key); } } + asyncFaF = (int)timer.ElapsedMilliseconds; + var final = new Task[5]; + for (int db = 0; db < 5; db++) + final[db] = conn.GetDatabase(db).StringGetAsync(key); + conn.WaitAll(final); + timer.Stop(); + asyncTimer = (int)timer.ElapsedMilliseconds; + Log("async to completion (local): {0}ms", timer.ElapsedMilliseconds); + for (int db = 0; db < 5; db++) + { + Assert.Equal(1000, (long)final[db].Result); // "async, db:" + db + } + } + + using (var conn = new RedisSharp.Redis(TestConfig.Current.PrimaryServer, TestConfig.Current.PrimaryPort)) + { + // do these outside the timings, just to ensure the core methods are JITted etc + for (int db = 0; db < 5; db++) + { + conn.Db = db; + conn.Remove(key); + } - using (var conn = new RedisSharp.Redis(TestConfig.Current.PrimaryServer, TestConfig.Current.PrimaryPort)) + var timer = Stopwatch.StartNew(); + for (int i = 0; i < 100; i++) { - // do these outside the timings, just to ensure the core methods are JITted etc + // want to test multiplex scenario; test each db, but to make it fair we'll + // do in batches of 10 on each for (int db = 0; db < 5; db++) { conn.Db = db; - conn.Remove(key); - } - - var timer = Stopwatch.StartNew(); - for (int i = 0; i < 100; i++) - { - // want to test multiplex scenario; test each db, but to make it fair we'll - // do in batches of 10 on each - for (int db = 0; db < 5; db++) + op++; + for (int j = 0; j < 10; j++) { - conn.Db = db; + conn.Increment(key); op++; - for (int j = 0; j < 10; j++) - { - conn.Increment(key); - op++; - } } } - syncFaF = (int)timer.ElapsedMilliseconds; - string[] final = new string[5]; - for (int db = 0; db < 5; db++) - { - conn.Db = db; - final[db] = Encoding.ASCII.GetString(conn.Get(key)); - } - timer.Stop(); - sync = (int)timer.ElapsedMilliseconds; - Log("sync to completion (local): {0}ms", timer.ElapsedMilliseconds); - for (int db = 0; db < 5; db++) - { - Assert.Equal("1000", final[db]); // "async, db:" + db - } } - int effectiveAsync = ((10 * asyncTimer) + 3) / 10; - int effectiveSync = ((10 * sync) + (op * 3)) / 10; - Log("async to completion with assumed 0.3ms LAN latency: " + effectiveAsync); - Log("sync to completion with assumed 0.3ms LAN latency: " + effectiveSync); - Log("fire-and-forget: {0}ms sync vs {1}ms async ", syncFaF, asyncFaF); - Assert.True(effectiveAsync < effectiveSync, "Everything"); - Assert.True(asyncFaF < syncFaF, "Fire and Forget"); + syncFaF = (int)timer.ElapsedMilliseconds; + string[] final = new string[5]; + for (int db = 0; db < 5; db++) + { + conn.Db = db; + final[db] = Encoding.ASCII.GetString(conn.Get(key)); + } + timer.Stop(); + sync = (int)timer.ElapsedMilliseconds; + Log("sync to completion (local): {0}ms", timer.ElapsedMilliseconds); + for (int db = 0; db < 5; db++) + { + Assert.Equal("1000", final[db]); // "async, db:" + db + } } + int effectiveAsync = ((10 * asyncTimer) + 3) / 10; + int effectiveSync = ((10 * sync) + (op * 3)) / 10; + Log("async to completion with assumed 0.3ms LAN latency: " + effectiveAsync); + Log("sync to completion with assumed 0.3ms LAN latency: " + effectiveSync); + Log("fire-and-forget: {0}ms sync vs {1}ms async ", syncFaF, asyncFaF); + Assert.True(effectiveAsync < effectiveSync, "Everything"); + Assert.True(asyncFaF < syncFaF, "Fire and Forget"); + } - [Fact] - public async Task BasicStringGetPerf() - { - using (var conn = Create()) - { - RedisKey key = Me(); - var db = conn.GetDatabase(); - await db.StringSetAsync(key, "some value").ForAwait(); + [Fact] + public async Task BasicStringGetPerf() + { + using var conn = Create(); - // this is just to JIT everything before we try testing - var syncVal = db.StringGet(key); - var asyncVal = await db.StringGetAsync(key).ForAwait(); + RedisKey key = Me(); + var db = conn.GetDatabase(); + await db.StringSetAsync(key, "some value").ForAwait(); - var syncTimer = Stopwatch.StartNew(); - syncVal = db.StringGet(key); - syncTimer.Stop(); + // this is just to JIT everything before we try testing + var syncVal = db.StringGet(key); + var asyncVal = await db.StringGetAsync(key).ForAwait(); - var asyncTimer = Stopwatch.StartNew(); - asyncVal = await db.StringGetAsync(key).ForAwait(); - asyncTimer.Stop(); + var syncTimer = Stopwatch.StartNew(); + syncVal = db.StringGet(key); + syncTimer.Stop(); - Log($"Sync: {syncTimer.ElapsedMilliseconds}; Async: {asyncTimer.ElapsedMilliseconds}"); - Assert.Equal("some value", syncVal); - Assert.Equal("some value", asyncVal); - // let's allow 20% async overhead - // But with a floor, since the base can often be zero - Assert.True(asyncTimer.ElapsedMilliseconds <= System.Math.Max(syncTimer.ElapsedMilliseconds * 1.2M, 50)); - } - } + var asyncTimer = Stopwatch.StartNew(); + asyncVal = await db.StringGetAsync(key).ForAwait(); + asyncTimer.Stop(); + + Log($"Sync: {syncTimer.ElapsedMilliseconds}; Async: {asyncTimer.ElapsedMilliseconds}"); + Assert.Equal("some value", syncVal); + Assert.Equal("some value", asyncVal); + // let's allow 20% async overhead + // But with a floor, since the base can often be zero + Assert.True(asyncTimer.ElapsedMilliseconds <= System.Math.Max(syncTimer.ElapsedMilliseconds * 1.2M, 50)); } } diff --git a/tests/StackExchange.Redis.Tests/PreserveOrder.cs b/tests/StackExchange.Redis.Tests/PreserveOrder.cs index ac2bd9361..f794a3650 100644 --- a/tests/StackExchange.Redis.Tests/PreserveOrder.cs +++ b/tests/StackExchange.Redis.Tests/PreserveOrder.cs @@ -5,65 +5,63 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class PreserveOrder : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class PreserveOrder : TestBase + public PreserveOrder(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + + [Fact] + public void Execute() { - public PreserveOrder(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + using var conn = Create(); - [Fact] - public void Execute() + var sub = conn.GetSubscriber(); + var channel = Me(); + var received = new List(); + Log("Subscribing..."); + const int COUNT = 500; + sub.Subscribe(channel, (_, message) => { - using (var conn = Create()) + lock (received) { - var sub = conn.GetSubscriber(); - var channel = Me(); - var received = new List(); - Log("Subscribing..."); - const int COUNT = 500; - sub.Subscribe(channel, (_, message) => - { - lock (received) - { - received.Add((int)message); - if (received.Count == COUNT) - Monitor.PulseAll(received); // wake the test rig - } - }); - Log(""); - Log("Sending (any order)..."); - lock (received) - { - received.Clear(); - // we'll also use received as a wait-detection mechanism; sneaky + received.Add((int)message); + if (received.Count == COUNT) + Monitor.PulseAll(received); // wake the test rig + } + }); + Log(""); + Log("Sending (any order)..."); + lock (received) + { + received.Clear(); + // we'll also use received as a wait-detection mechanism; sneaky - // note: this does not do any cheating; - // it all goes to the server and back - for (int i = 0; i < COUNT; i++) - { - sub.Publish(channel, i, CommandFlags.FireAndForget); - } + // note: this does not do any cheating; + // it all goes to the server and back + for (int i = 0; i < COUNT; i++) + { + sub.Publish(channel, i, CommandFlags.FireAndForget); + } - Log("Allowing time for delivery etc..."); - var watch = Stopwatch.StartNew(); - if (!Monitor.Wait(received, 10000)) - { - Log("Timed out; expect less data"); - } - watch.Stop(); - Log("Checking..."); - lock (received) - { - Log("Received: {0} in {1}ms", received.Count, watch.ElapsedMilliseconds); - int wrongOrder = 0; - for (int i = 0; i < Math.Min(COUNT, received.Count); i++) - { - if (received[i] != i) wrongOrder++; - } - Log("Out of order: " + wrongOrder); - } + Log("Allowing time for delivery etc..."); + var watch = Stopwatch.StartNew(); + if (!Monitor.Wait(received, 10000)) + { + Log("Timed out; expect less data"); + } + watch.Stop(); + Log("Checking..."); + lock (received) + { + Log("Received: {0} in {1}ms", received.Count, watch.ElapsedMilliseconds); + int wrongOrder = 0; + for (int i = 0; i < Math.Min(COUNT, received.Count); i++) + { + if (received[i] != i) wrongOrder++; } + Log("Out of order: " + wrongOrder); } } } diff --git a/tests/StackExchange.Redis.Tests/Profiling.cs b/tests/StackExchange.Redis.Tests/Profiling.cs index 159d737dc..5a7718017 100644 --- a/tests/StackExchange.Redis.Tests/Profiling.cs +++ b/tests/StackExchange.Redis.Tests/Profiling.cs @@ -8,413 +8,401 @@ using Xunit.Abstractions; using StackExchange.Redis.Profiling; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(NonParallelCollection.Name)] +public class Profiling : TestBase { - [Collection(NonParallelCollection.Name)] - public class Profiling : TestBase - { - public Profiling(ITestOutputHelper output) : base(output) { } + public Profiling(ITestOutputHelper output) : base(output) { } - [Fact] - public void Simple() + [Fact] + public void Simple() + { + using var conn = Create(); + + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); + var script = LuaScript.Prepare("return redis.call('get', @key)"); + var loaded = script.Load(server); + var key = Me(); + + var session = new ProfilingSession(); + + conn.RegisterProfiler(() => session); + + var dbId = TestConfig.GetDedicatedDB(); + var db = conn.GetDatabase(dbId); + db.StringSet(key, "world"); + var result = db.ScriptEvaluate(script, new { key = (RedisKey)key }); + Assert.NotNull(result); + Assert.Equal("world", result.AsString()); + var loadedResult = db.ScriptEvaluate(loaded, new { key = (RedisKey)key }); + Assert.NotNull(loadedResult); + Assert.Equal("world", loadedResult.AsString()); + var val = db.StringGet(key); + Assert.Equal("world", val); + var s = (string?)db.Execute("ECHO", "fii"); + Assert.Equal("fii", s); + + var cmds = session.FinishProfiling(); + var i = 0; + foreach (var cmd in cmds) { - using (var conn = Create()) - { - var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); - var script = LuaScript.Prepare("return redis.call('get', @key)"); - var loaded = script.Load(server); - var key = Me(); - - var session = new ProfilingSession(); - - conn.RegisterProfiler(() => session); - - var dbId = TestConfig.GetDedicatedDB(); - var db = conn.GetDatabase(dbId); - db.StringSet(key, "world"); - var result = db.ScriptEvaluate(script, new { key = (RedisKey)key }); - Assert.NotNull(result); - Assert.Equal("world", result.AsString()); - var loadedResult = db.ScriptEvaluate(loaded, new { key = (RedisKey)key }); - Assert.NotNull(loadedResult); - Assert.Equal("world", loadedResult.AsString()); - var val = db.StringGet(key); - Assert.Equal("world", val); - var s = (string?)db.Execute("ECHO", "fii"); - Assert.Equal("fii", s); - - var cmds = session.FinishProfiling(); - var i = 0; - foreach (var cmd in cmds) - { - Log("Command {0} (DB: {1}): {2}", i++, cmd.Db, cmd?.ToString()?.Replace("\n", ", ")); - } + Log("Command {0} (DB: {1}): {2}", i++, cmd.Db, cmd?.ToString()?.Replace("\n", ", ")); + } - var all = string.Join(",", cmds.Select(x => x.Command)); - Assert.Equal("SET,EVAL,EVALSHA,GET,ECHO", all); - Log("Checking for SET"); - var set = cmds.SingleOrDefault(cmd => cmd.Command == "SET"); - Assert.NotNull(set); - Log("Checking for GET"); - var get = cmds.SingleOrDefault(cmd => cmd.Command == "GET"); - Assert.NotNull(get); - Log("Checking for EVAL"); - var eval = cmds.SingleOrDefault(cmd => cmd.Command == "EVAL"); - Assert.NotNull(eval); - Log("Checking for EVALSHA"); - var evalSha = cmds.SingleOrDefault(cmd => cmd.Command == "EVALSHA"); - Assert.NotNull(evalSha); - Log("Checking for ECHO"); - var echo = cmds.SingleOrDefault(cmd => cmd.Command == "ECHO"); - Assert.NotNull(echo); + var all = string.Join(",", cmds.Select(x => x.Command)); + Assert.Equal("SET,EVAL,EVALSHA,GET,ECHO", all); + Log("Checking for SET"); + var set = cmds.SingleOrDefault(cmd => cmd.Command == "SET"); + Assert.NotNull(set); + Log("Checking for GET"); + var get = cmds.SingleOrDefault(cmd => cmd.Command == "GET"); + Assert.NotNull(get); + Log("Checking for EVAL"); + var eval = cmds.SingleOrDefault(cmd => cmd.Command == "EVAL"); + Assert.NotNull(eval); + Log("Checking for EVALSHA"); + var evalSha = cmds.SingleOrDefault(cmd => cmd.Command == "EVALSHA"); + Assert.NotNull(evalSha); + Log("Checking for ECHO"); + var echo = cmds.SingleOrDefault(cmd => cmd.Command == "ECHO"); + Assert.NotNull(echo); - Assert.Equal(5, cmds.Count()); + Assert.Equal(5, cmds.Count()); - Assert.True(set.CommandCreated <= eval.CommandCreated); - Assert.True(eval.CommandCreated <= evalSha.CommandCreated); - Assert.True(evalSha.CommandCreated <= get.CommandCreated); + Assert.True(set.CommandCreated <= eval.CommandCreated); + Assert.True(eval.CommandCreated <= evalSha.CommandCreated); + Assert.True(evalSha.CommandCreated <= get.CommandCreated); - AssertProfiledCommandValues(set, conn, dbId); + AssertProfiledCommandValues(set, conn, dbId); - AssertProfiledCommandValues(get, conn, dbId); + AssertProfiledCommandValues(get, conn, dbId); - AssertProfiledCommandValues(eval, conn, dbId); + AssertProfiledCommandValues(eval, conn, dbId); - AssertProfiledCommandValues(evalSha, conn, dbId); + AssertProfiledCommandValues(evalSha, conn, dbId); - AssertProfiledCommandValues(echo, conn, dbId); - } - } + AssertProfiledCommandValues(echo, conn, dbId); + } - private static void AssertProfiledCommandValues(IProfiledCommand command, IConnectionMultiplexer conn, int dbId) - { - Assert.Equal(dbId, command.Db); - Assert.Equal(conn.GetEndPoints()[0], command.EndPoint); - Assert.True(command.CreationToEnqueued > TimeSpan.Zero, nameof(command.CreationToEnqueued)); - Assert.True(command.EnqueuedToSending > TimeSpan.Zero, nameof(command.EnqueuedToSending)); - Assert.True(command.SentToResponse > TimeSpan.Zero, nameof(command.SentToResponse)); - Assert.True(command.ResponseToCompletion >= TimeSpan.Zero, nameof(command.ResponseToCompletion)); - Assert.True(command.ElapsedTime > TimeSpan.Zero, nameof(command.ElapsedTime)); - Assert.True(command.ElapsedTime > command.CreationToEnqueued && command.ElapsedTime > command.EnqueuedToSending && command.ElapsedTime > command.SentToResponse, "Comparisons"); - Assert.True(command.RetransmissionOf == null, nameof(command.RetransmissionOf)); - Assert.True(command.RetransmissionReason == null, nameof(command.RetransmissionReason)); - } + private static void AssertProfiledCommandValues(IProfiledCommand command, IConnectionMultiplexer conn, int dbId) + { + Assert.Equal(dbId, command.Db); + Assert.Equal(conn.GetEndPoints()[0], command.EndPoint); + Assert.True(command.CreationToEnqueued > TimeSpan.Zero, nameof(command.CreationToEnqueued)); + Assert.True(command.EnqueuedToSending > TimeSpan.Zero, nameof(command.EnqueuedToSending)); + Assert.True(command.SentToResponse > TimeSpan.Zero, nameof(command.SentToResponse)); + Assert.True(command.ResponseToCompletion >= TimeSpan.Zero, nameof(command.ResponseToCompletion)); + Assert.True(command.ElapsedTime > TimeSpan.Zero, nameof(command.ElapsedTime)); + Assert.True(command.ElapsedTime > command.CreationToEnqueued && command.ElapsedTime > command.EnqueuedToSending && command.ElapsedTime > command.SentToResponse, "Comparisons"); + Assert.True(command.RetransmissionOf == null, nameof(command.RetransmissionOf)); + Assert.True(command.RetransmissionReason == null, nameof(command.RetransmissionReason)); + } - [FactLongRunning] - public void ManyThreads() - { - using (var conn = Create()) - { - var session = new ProfilingSession(); - var prefix = Me(); + [FactLongRunning] + public void ManyThreads() + { + using var conn = Create(); - conn.RegisterProfiler(() => session); + var session = new ProfilingSession(); + var prefix = Me(); - var threads = new List(); - const int CountPer = 100; - for (var i = 1; i <= 16; i++) - { - var db = conn.GetDatabase(i); + conn.RegisterProfiler(() => session); - threads.Add(new Thread(() => - { - var threadTasks = new List(); + var threads = new List(); + const int CountPer = 100; + for (var i = 1; i <= 16; i++) + { + var db = conn.GetDatabase(i); - for (var j = 0; j < CountPer; j++) - { - var task = db.StringSetAsync(prefix + j, "" + j); - threadTasks.Add(task); - } + threads.Add(new Thread(() => + { + var threadTasks = new List(); - Task.WaitAll(threadTasks.ToArray()); - })); + for (var j = 0; j < CountPer; j++) + { + var task = db.StringSetAsync(prefix + j, "" + j); + threadTasks.Add(task); } - threads.ForEach(thread => thread.Start()); - threads.ForEach(thread => thread.Join()); + Task.WaitAll(threadTasks.ToArray()); + })); + } - var allVals = session.FinishProfiling(); - var relevant = allVals.Where(cmd => cmd.Db > 0).ToList(); + threads.ForEach(thread => thread.Start()); + threads.ForEach(thread => thread.Join()); - var kinds = relevant.Select(cmd => cmd.Command).Distinct().ToList(); - foreach (var k in kinds) - { - Log("Kind Seen: " + k); - } - Assert.True(kinds.Count <= 2); - Assert.Contains("SET", kinds); - if (kinds.Count == 2 && !kinds.Contains("SELECT") && !kinds.Contains("GET")) - { - Assert.True(false, "Non-SET, Non-SELECT, Non-GET command seen"); - } - - Assert.Equal(16 * CountPer, relevant.Count); - Assert.Equal(16, relevant.Select(cmd => cmd.Db).Distinct().Count()); + var allVals = session.FinishProfiling(); + var relevant = allVals.Where(cmd => cmd.Db > 0).ToList(); - for (var i = 1; i <= 16; i++) - { - var setsInDb = relevant.Count(cmd => cmd.Db == i); - Assert.Equal(CountPer, setsInDb); - } - } + var kinds = relevant.Select(cmd => cmd.Command).Distinct().ToList(); + foreach (var k in kinds) + { + Log("Kind Seen: " + k); } + Assert.True(kinds.Count <= 2); + Assert.Contains("SET", kinds); + if (kinds.Count == 2 && !kinds.Contains("SELECT") && !kinds.Contains("GET")) + { + Assert.True(false, "Non-SET, Non-SELECT, Non-GET command seen"); + } + + Assert.Equal(16 * CountPer, relevant.Count); + Assert.Equal(16, relevant.Select(cmd => cmd.Db).Distinct().Count()); - [FactLongRunning] - public void ManyContexts() + for (var i = 1; i <= 16; i++) { - using (var conn = Create()) - { - var profiler = new AsyncLocalProfiler(); - var prefix = Me(); - conn.RegisterProfiler(profiler.GetSession); + var setsInDb = relevant.Count(cmd => cmd.Db == i); + Assert.Equal(CountPer, setsInDb); + } + } - var tasks = new Task[16]; + [FactLongRunning] + public void ManyContexts() + { + using var conn = Create(); - var results = new ProfiledCommandEnumerable[tasks.Length]; + var profiler = new AsyncLocalProfiler(); + var prefix = Me(); + conn.RegisterProfiler(profiler.GetSession); - for (var i = 0; i < tasks.Length; i++) - { - var ix = i; - tasks[ix] = Task.Run(async () => - { - var db = conn.GetDatabase(ix); - - var allTasks = new List(); - - for (var j = 0; j < 1000; j++) - { - var g = db.StringGetAsync(prefix + ix); - var s = db.StringSetAsync(prefix + ix, "world" + ix); - // overlap the g+s, just for fun - await g; - await s; - } - - results[ix] = profiler.GetSession().FinishProfiling(); - }); - } - Task.WhenAll(tasks).Wait(); + var tasks = new Task[16]; - for (var i = 0; i < results.Length; i++) - { - var res = results[i]; + var results = new ProfiledCommandEnumerable[tasks.Length]; - var numGets = res.Count(r => r.Command == "GET"); - var numSets = res.Count(r => r.Command == "SET"); + for (var i = 0; i < tasks.Length; i++) + { + var ix = i; + tasks[ix] = Task.Run(async () => + { + var db = conn.GetDatabase(ix); + + var allTasks = new List(); - Assert.Equal(1000, numGets); - Assert.Equal(1000, numSets); - Assert.True(res.All(cmd => cmd.Db == i)); + for (var j = 0; j < 1000; j++) + { + var g = db.StringGetAsync(prefix + ix); + var s = db.StringSetAsync(prefix + ix, "world" + ix); + // overlap the g+s, just for fun + await g; + await s; } - } + + results[ix] = profiler.GetSession().FinishProfiling(); + }); } + Task.WhenAll(tasks).Wait(); - internal class PerThreadProfiler + for (var i = 0; i < results.Length; i++) { - private readonly ThreadLocal perThreadSession = new ThreadLocal(() => new ProfilingSession()); + var res = results[i]; + + var numGets = res.Count(r => r.Command == "GET"); + var numSets = res.Count(r => r.Command == "SET"); - public ProfilingSession GetSession() => perThreadSession.Value!; + Assert.Equal(1000, numGets); + Assert.Equal(1000, numSets); + Assert.True(res.All(cmd => cmd.Db == i)); } + } - internal class AsyncLocalProfiler - { - private readonly AsyncLocal perThreadSession = new AsyncLocal(); + internal class PerThreadProfiler + { + private readonly ThreadLocal perThreadSession = new ThreadLocal(() => new ProfilingSession()); + + public ProfilingSession GetSession() => perThreadSession.Value!; + } + + internal class AsyncLocalProfiler + { + private readonly AsyncLocal perThreadSession = new AsyncLocal(); - public ProfilingSession GetSession() + public ProfilingSession GetSession() + { + var val = perThreadSession.Value; + if (val == null) { - var val = perThreadSession.Value; - if (val == null) - { - perThreadSession.Value = val = new ProfilingSession(); - } - return val; + perThreadSession.Value = val = new ProfilingSession(); } + return val; } + } - [Fact] - public void LowAllocationEnumerable() - { - const int OuterLoop = 1000; - - using (var conn = Create()) - { - var session = new ProfilingSession(); - conn.RegisterProfiler(() => session); + [Fact] + public void LowAllocationEnumerable() + { + using var conn = Create(); - var prefix = Me(); - var db = conn.GetDatabase(1); + const int OuterLoop = 1000; + var session = new ProfilingSession(); + conn.RegisterProfiler(() => session); - var allTasks = new List>(); + var prefix = Me(); + var db = conn.GetDatabase(1); - foreach (var i in Enumerable.Range(0, OuterLoop)) - { - var t = - db.StringSetAsync(prefix + i, "bar" + i) - .ContinueWith( - async _ => (string?)(await db.StringGetAsync(prefix + i).ForAwait()) - ); - - var finalResult = t.Unwrap(); - allTasks.Add(finalResult); - } + var allTasks = new List>(); - conn.WaitAll(allTasks.ToArray()); + foreach (var i in Enumerable.Range(0, OuterLoop)) + { + var t = + db.StringSetAsync(prefix + i, "bar" + i) + .ContinueWith( + async _ => (string?)(await db.StringGetAsync(prefix + i).ForAwait()) + ); + + var finalResult = t.Unwrap(); + allTasks.Add(finalResult); + } - var res = session.FinishProfiling(); - Assert.True(res.GetType().IsValueType); + conn.WaitAll(allTasks.ToArray()); - using (var e = res.GetEnumerator()) - { - Assert.True(e.GetType().IsValueType); + var res = session.FinishProfiling(); + Assert.True(res.GetType().IsValueType); - Assert.True(e.MoveNext()); - var i = e.Current; + using (var e = res.GetEnumerator()) + { + Assert.True(e.GetType().IsValueType); - e.Reset(); - Assert.True(e.MoveNext()); - var j = e.Current; + Assert.True(e.MoveNext()); + var i = e.Current; - Assert.True(ReferenceEquals(i, j)); - } + e.Reset(); + Assert.True(e.MoveNext()); + var j = e.Current; - Assert.Equal(OuterLoop, res.Count(r => r.Command == "GET" && r.Db > 0)); - Assert.Equal(OuterLoop, res.Count(r => r.Command == "SET" && r.Db > 0)); - Assert.Equal(OuterLoop * 2, res.Count(r => r.Db > 0)); - } + Assert.True(ReferenceEquals(i, j)); } - [FactLongRunning] - public void ProfilingMD_Ex1() - { - using (var c = Create()) - { - IConnectionMultiplexer conn = c; - var session = new ProfilingSession(); - var prefix = Me(); + Assert.Equal(OuterLoop, res.Count(r => r.Command == "GET" && r.Db > 0)); + Assert.Equal(OuterLoop, res.Count(r => r.Command == "SET" && r.Db > 0)); + Assert.Equal(OuterLoop * 2, res.Count(r => r.Db > 0)); + } - conn.RegisterProfiler(() => session); + [FactLongRunning] + public void ProfilingMD_Ex1() + { + using var conn = Create(); - var threads = new List(); + var session = new ProfilingSession(); + var prefix = Me(); - for (var i = 0; i < 16; i++) - { - var db = conn.GetDatabase(i); + conn.RegisterProfiler(() => session); - var thread = new Thread(() => - { - var threadTasks = new List(); + var threads = new List(); - for (var j = 0; j < 1000; j++) - { - var task = db.StringSetAsync(prefix + j, "" + j); - threadTasks.Add(task); - } + for (var i = 0; i < 16; i++) + { + var db = conn.GetDatabase(i); - Task.WaitAll(threadTasks.ToArray()); - }); + var thread = new Thread(() => + { + var threadTasks = new List(); - threads.Add(thread); + for (var j = 0; j < 1000; j++) + { + var task = db.StringSetAsync(prefix + j, "" + j); + threadTasks.Add(task); } - threads.ForEach(thread => thread.Start()); - threads.ForEach(thread => thread.Join()); - - IEnumerable timings = session.FinishProfiling(); + Task.WaitAll(threadTasks.ToArray()); + }); - Assert.Equal(16000, timings.Count()); - } + threads.Add(thread); } - [FactLongRunning] - public void ProfilingMD_Ex2() - { - using (var c = Create()) - { - IConnectionMultiplexer conn = c; - var profiler = new PerThreadProfiler(); - var prefix = Me(); + threads.ForEach(thread => thread.Start()); + threads.ForEach(thread => thread.Join()); - conn.RegisterProfiler(profiler.GetSession); + IEnumerable timings = session.FinishProfiling(); - var threads = new List(); + Assert.Equal(16000, timings.Count()); + } - var perThreadTimings = new ConcurrentDictionary>(); + [FactLongRunning] + public void ProfilingMD_Ex2() + { + using var conn = Create(); - for (var i = 0; i < 16; i++) - { - var db = conn.GetDatabase(i); + var profiler = new PerThreadProfiler(); + var prefix = Me(); - var thread = new Thread(() => - { - var threadTasks = new List(); + conn.RegisterProfiler(profiler.GetSession); - for (var j = 0; j < 1000; j++) - { - var task = db.StringSetAsync(prefix + j, "" + j); - threadTasks.Add(task); - } + var threads = new List(); - Task.WaitAll(threadTasks.ToArray()); + var perThreadTimings = new ConcurrentDictionary>(); - perThreadTimings[Thread.CurrentThread] = profiler.GetSession().FinishProfiling().ToList(); - }); + for (var i = 0; i < 16; i++) + { + var db = conn.GetDatabase(i); - threads.Add(thread); + var thread = new Thread(() => + { + var threadTasks = new List(); + + for (var j = 0; j < 1000; j++) + { + var task = db.StringSetAsync(prefix + j, "" + j); + threadTasks.Add(task); } - threads.ForEach(thread => thread.Start()); - threads.ForEach(thread => thread.Join()); + Task.WaitAll(threadTasks.ToArray()); - Assert.Equal(16, perThreadTimings.Count); - Assert.True(perThreadTimings.All(kv => kv.Value.Count == 1000)); - } + perThreadTimings[Thread.CurrentThread] = profiler.GetSession().FinishProfiling().ToList(); + }); + + threads.Add(thread); } - [FactLongRunning] - public async Task ProfilingMD_Ex2_Async() - { - using (var c = Create()) - { - IConnectionMultiplexer conn = c; - var profiler = new AsyncLocalProfiler(); - var prefix = Me(); + threads.ForEach(thread => thread.Start()); + threads.ForEach(thread => thread.Join()); - conn.RegisterProfiler(profiler.GetSession); + Assert.Equal(16, perThreadTimings.Count); + Assert.True(perThreadTimings.All(kv => kv.Value.Count == 1000)); + } - var tasks = new List(); + [FactLongRunning] + public async Task ProfilingMD_Ex2_Async() + { + using var conn = Create(); - var perThreadTimings = new ConcurrentBag>(); + var profiler = new AsyncLocalProfiler(); + var prefix = Me(); - for (var i = 0; i < 16; i++) - { - var db = conn.GetDatabase(i); + conn.RegisterProfiler(profiler.GetSession); - var task = Task.Run(async () => - { - for (var j = 0; j < 100; j++) - { - await db.StringSetAsync(prefix + j, "" + j).ForAwait(); - } + var tasks = new List(); - perThreadTimings.Add(profiler.GetSession().FinishProfiling().ToList()); - }); + var perThreadTimings = new ConcurrentBag>(); - tasks.Add(task); - } + for (var i = 0; i < 16; i++) + { + var db = conn.GetDatabase(i); - var timeout = Task.Delay(10000); - var complete = Task.WhenAll(tasks); - if (timeout == await Task.WhenAny(timeout, complete).ForAwait()) + var task = Task.Run(async () => + { + for (var j = 0; j < 100; j++) { - throw new TimeoutException(); + await db.StringSetAsync(prefix + j, "" + j).ForAwait(); } - Assert.Equal(16, perThreadTimings.Count); - foreach (var item in perThreadTimings) - { - Assert.Equal(100, item.Count); - } - } + perThreadTimings.Add(profiler.GetSession().FinishProfiling().ToList()); + }); + + tasks.Add(task); + } + + var timeout = Task.Delay(10000); + var complete = Task.WhenAll(tasks); + if (timeout == await Task.WhenAny(timeout, complete).ForAwait()) + { + throw new TimeoutException(); + } + + Assert.Equal(16, perThreadTimings.Count); + foreach (var item in perThreadTimings) + { + Assert.Equal(100, item.Count); } } } diff --git a/tests/StackExchange.Redis.Tests/PubSub.cs b/tests/StackExchange.Redis.Tests/PubSub.cs index d20b3a81a..c7486f074 100644 --- a/tests/StackExchange.Redis.Tests/PubSub.cs +++ b/tests/StackExchange.Redis.Tests/PubSub.cs @@ -10,854 +10,839 @@ using Xunit.Abstractions; // ReSharper disable AccessToModifiedClosure -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class PubSub : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class PubSub : TestBase + public PubSub(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public async Task ExplicitPublishMode() { - public PubSub(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + using var conn = Create(channelPrefix: "foo:", log: Writer); + + var pub = conn.GetSubscriber(); + int a = 0, b = 0, c = 0, d = 0; + pub.Subscribe(new RedisChannel("*bcd", RedisChannel.PatternMode.Literal), (x, y) => Interlocked.Increment(ref a)); + pub.Subscribe(new RedisChannel("a*cd", RedisChannel.PatternMode.Pattern), (x, y) => Interlocked.Increment(ref b)); + pub.Subscribe(new RedisChannel("ab*d", RedisChannel.PatternMode.Auto), (x, y) => Interlocked.Increment(ref c)); + pub.Subscribe("abc*", (x, y) => Interlocked.Increment(ref d)); + + pub.Publish("abcd", "efg"); + await UntilConditionAsync(TimeSpan.FromSeconds(10), + () => Thread.VolatileRead(ref b) == 1 + && Thread.VolatileRead(ref c) == 1 + && Thread.VolatileRead(ref d) == 1); + Assert.Equal(0, Thread.VolatileRead(ref a)); + Assert.Equal(1, Thread.VolatileRead(ref b)); + Assert.Equal(1, Thread.VolatileRead(ref c)); + Assert.Equal(1, Thread.VolatileRead(ref d)); + + pub.Publish("*bcd", "efg"); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Thread.VolatileRead(ref a) == 1); + Assert.Equal(1, Thread.VolatileRead(ref a)); + } - [Fact] - public async Task ExplicitPublishMode() + [Theory] + [InlineData(null, false, "a")] + [InlineData("", false, "b")] + [InlineData("Foo:", false, "c")] + [InlineData(null, true, "d")] + [InlineData("", true, "e")] + [InlineData("Foo:", true, "f")] + public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string breaker) + { + using var conn = Create(channelPrefix: channelPrefix, shared: false, log: Writer); + + var pub = GetAnyPrimary(conn); + var sub = conn.GetSubscriber(); + await PingAsync(pub, sub).ForAwait(); + HashSet received = new(); + int secondHandler = 0; + string subChannel = (wildCard ? "a*c" : "abc") + breaker; + string pubChannel = "abc" + breaker; + Action handler1 = (channel, payload) => { - using (var mx = Create(channelPrefix: "foo:", log: Writer)) + lock (received) { - var pub = mx.GetSubscriber(); - int a = 0, b = 0, c = 0, d = 0; - pub.Subscribe(new RedisChannel("*bcd", RedisChannel.PatternMode.Literal), (x, y) => Interlocked.Increment(ref a)); - pub.Subscribe(new RedisChannel("a*cd", RedisChannel.PatternMode.Pattern), (x, y) => Interlocked.Increment(ref b)); - pub.Subscribe(new RedisChannel("ab*d", RedisChannel.PatternMode.Auto), (x, y) => Interlocked.Increment(ref c)); - pub.Subscribe("abc*", (x, y) => Interlocked.Increment(ref d)); - - pub.Publish("abcd", "efg"); - await UntilConditionAsync(TimeSpan.FromSeconds(10), - () => Thread.VolatileRead(ref b) == 1 - && Thread.VolatileRead(ref c) == 1 - && Thread.VolatileRead(ref d) == 1); - Assert.Equal(0, Thread.VolatileRead(ref a)); - Assert.Equal(1, Thread.VolatileRead(ref b)); - Assert.Equal(1, Thread.VolatileRead(ref c)); - Assert.Equal(1, Thread.VolatileRead(ref d)); - - pub.Publish("*bcd", "efg"); - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Thread.VolatileRead(ref a) == 1); - Assert.Equal(1, Thread.VolatileRead(ref a)); + if (channel == pubChannel) + { + received.Add(payload); + } + else + { + Log(channel); + } } } + , handler2 = (_, __) => Interlocked.Increment(ref secondHandler); + sub.Subscribe(subChannel, handler1); + sub.Subscribe(subChannel, handler2); - [Theory] - [InlineData(null, false, "a")] - [InlineData("", false, "b")] - [InlineData("Foo:", false, "c")] - [InlineData(null, true, "d")] - [InlineData("", true, "e")] - [InlineData("Foo:", true, "f")] - public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string breaker) + lock (received) { - using (var muxer = Create(channelPrefix: channelPrefix, shared: false, log: Writer)) - { - var pub = GetAnyPrimary(muxer); - var sub = muxer.GetSubscriber(); - await PingAsync(pub, sub).ForAwait(); - HashSet received = new(); - int secondHandler = 0; - string subChannel = (wildCard ? "a*c" : "abc") + breaker; - string pubChannel = "abc" + breaker; - Action handler1 = (channel, payload) => - { - lock (received) - { - if (channel == pubChannel) - { - received.Add(payload); - } - else - { - Log(channel); - } - } - } - , handler2 = (_, __) => Interlocked.Increment(ref secondHandler); - sub.Subscribe(subChannel, handler1); - sub.Subscribe(subChannel, handler2); + Assert.Empty(received); + } + Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); + var count = sub.Publish(pubChannel, "def"); - lock (received) - { - Assert.Empty(received); - } - Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); - var count = sub.Publish(pubChannel, "def"); + await PingAsync(pub, sub, 3).ForAwait(); - await PingAsync(pub, sub, 3).ForAwait(); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => received.Count == 1); + lock (received) + { + Assert.Single(received); + } + // Give handler firing a moment + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); + Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); + + // unsubscribe from first; should still see second + sub.Unsubscribe(subChannel, handler1); + count = sub.Publish(pubChannel, "ghi"); + await PingAsync(pub, sub).ForAwait(); + lock (received) + { + Assert.Single(received); + } - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => received.Count == 1); - lock (received) - { - Assert.Single(received); - } - // Give handler firing a moment - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); - Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); - - // unsubscribe from first; should still see second - sub.Unsubscribe(subChannel, handler1); - count = sub.Publish(pubChannel, "ghi"); - await PingAsync(pub, sub).ForAwait(); - lock (received) - { - Assert.Single(received); - } + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 2); + + var secondHandlerCount = Thread.VolatileRead(ref secondHandler); + Log("Expecting 2 from second handler, got: " + secondHandlerCount); + Assert.Equal(2, secondHandlerCount); + Assert.Equal(1, count); - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 2); + // unsubscribe from second; should see nothing this time + sub.Unsubscribe(subChannel, handler2); + count = sub.Publish(pubChannel, "ghi"); + await PingAsync(pub, sub).ForAwait(); + lock (received) + { + Assert.Single(received); + } + secondHandlerCount = Thread.VolatileRead(ref secondHandler); + Log("Expecting 2 from second handler, got: " + secondHandlerCount); + Assert.Equal(2, secondHandlerCount); + Assert.Equal(0, count); + } + + [Fact] + public async Task TestBasicPubSubFireAndForget() + { + using var conn = Create(shared: false, log: Writer); - var secondHandlerCount = Thread.VolatileRead(ref secondHandler); - Log("Expecting 2 from second handler, got: " + secondHandlerCount); - Assert.Equal(2, secondHandlerCount); - Assert.Equal(1, count); + var profiler = conn.AddProfiler(); + var pub = GetAnyPrimary(conn); + var sub = conn.GetSubscriber(); - // unsubscribe from second; should see nothing this time - sub.Unsubscribe(subChannel, handler2); - count = sub.Publish(pubChannel, "ghi"); - await PingAsync(pub, sub).ForAwait(); - lock (received) + RedisChannel key = Me() + Guid.NewGuid(); + HashSet received = new(); + int secondHandler = 0; + await PingAsync(pub, sub).ForAwait(); + sub.Subscribe(key, (channel, payload) => + { + lock (received) + { + if (channel == key) { - Assert.Single(received); + received.Add(payload); } - secondHandlerCount = Thread.VolatileRead(ref secondHandler); - Log("Expecting 2 from second handler, got: " + secondHandlerCount); - Assert.Equal(2, secondHandlerCount); - Assert.Equal(0, count); } + }, CommandFlags.FireAndForget); + + sub.Subscribe(key, (_, __) => Interlocked.Increment(ref secondHandler), CommandFlags.FireAndForget); + Log(profiler); + + lock (received) + { + Assert.Empty(received); } + Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); + await PingAsync(pub, sub).ForAwait(); + var count = sub.Publish(key, "def", CommandFlags.FireAndForget); + await PingAsync(pub, sub).ForAwait(); + + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => received.Count == 1); + Log(profiler); - [Fact] - public async Task TestBasicPubSubFireAndForget() + lock (received) { - using (var muxer = Create(shared: false, log: Writer)) - { - var profiler = muxer.AddProfiler(); - var pub = GetAnyPrimary(muxer); - var sub = muxer.GetSubscriber(); - - RedisChannel key = Me() + Guid.NewGuid(); - HashSet received = new(); - int secondHandler = 0; - await PingAsync(pub, sub).ForAwait(); - sub.Subscribe(key, (channel, payload) => - { - lock (received) - { - if (channel == key) - { - received.Add(payload); - } - } - }, CommandFlags.FireAndForget); + Assert.Single(received); + } + Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); - sub.Subscribe(key, (_, __) => Interlocked.Increment(ref secondHandler), CommandFlags.FireAndForget); - Log(profiler); + sub.Unsubscribe(key); + count = sub.Publish(key, "ghi", CommandFlags.FireAndForget); - lock (received) - { - Assert.Empty(received); - } - Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); - await PingAsync(pub, sub).ForAwait(); - var count = sub.Publish(key, "def", CommandFlags.FireAndForget); - await PingAsync(pub, sub).ForAwait(); + await PingAsync(pub, sub).ForAwait(); + Log(profiler); + lock (received) + { + Assert.Single(received); + } + Assert.Equal(0, count); + } - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => received.Count == 1); - Log(profiler); + private async Task PingAsync(IServer pub, ISubscriber sub, int times = 1) + { + while (times-- > 0) + { + // both use async because we want to drain the completion managers, and the only + // way to prove that is to use TPL objects + var subTask = sub.PingAsync(); + var pubTask = pub.PingAsync(); + await Task.WhenAll(subTask, pubTask).ForAwait(); + + Log($"Sub PING time: {subTask.Result.TotalMilliseconds} ms"); + Log($"Pub PING time: {pubTask.Result.TotalMilliseconds} ms"); + } + } - lock (received) - { - Assert.Single(received); - } - Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); + [Fact] + public async Task TestPatternPubSub() + { + using var conn = Create(shared: false, log: Writer); - sub.Unsubscribe(key); - count = sub.Publish(key, "ghi", CommandFlags.FireAndForget); + var pub = GetAnyPrimary(conn); + var sub = conn.GetSubscriber(); - await PingAsync(pub, sub).ForAwait(); - Log(profiler); - lock (received) + HashSet received = new(); + int secondHandler = 0; + sub.Subscribe("a*c", (channel, payload) => + { + lock (received) + { + if (channel == "abc") { - Assert.Single(received); + received.Add(payload); } - Assert.Equal(0, count); } + }); + + sub.Subscribe("a*c", (_, __) => Interlocked.Increment(ref secondHandler)); + lock (received) + { + Assert.Empty(received); } + Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); + + await PingAsync(pub, sub).ForAwait(); + var count = sub.Publish("abc", "def"); + await PingAsync(pub, sub).ForAwait(); - private async Task PingAsync(IServer pub, ISubscriber sub, int times = 1) + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => received.Count == 1); + lock (received) { - while (times-- > 0) - { - // both use async because we want to drain the completion managers, and the only - // way to prove that is to use TPL objects - var subTask = sub.PingAsync(); - var pubTask = pub.PingAsync(); - await Task.WhenAll(subTask, pubTask).ForAwait(); - - Log($"Sub PING time: {subTask.Result.TotalMilliseconds} ms"); - Log($"Pub PING time: {pubTask.Result.TotalMilliseconds} ms"); - } + Assert.Single(received); } - [Fact] - public async Task TestPatternPubSub() + // Give reception a bit, the handler could be delayed under load + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); + Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); + + sub.Unsubscribe("a*c"); + count = sub.Publish("abc", "ghi"); + + await PingAsync(pub, sub).ForAwait(); + + lock (received) { - using (var muxer = Create(shared: false, log: Writer)) - { - var pub = GetAnyPrimary(muxer); - var sub = muxer.GetSubscriber(); + Assert.Single(received); + } + } - HashSet received = new(); - int secondHandler = 0; - sub.Subscribe("a*c", (channel, payload) => - { - lock (received) - { - if (channel == "abc") - { - received.Add(payload); - } - } - }); + [Fact] + public void TestPublishWithNoSubscribers() + { + using var conn = Create(); - sub.Subscribe("a*c", (_, __) => Interlocked.Increment(ref secondHandler)); - lock (received) - { - Assert.Empty(received); - } - Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); + var sub = conn.GetSubscriber(); + Assert.Equal(0, sub.Publish(Me() + "channel", "message")); + } - await PingAsync(pub, sub).ForAwait(); - var count = sub.Publish("abc", "def"); - await PingAsync(pub, sub).ForAwait(); + [FactLongRunning] + public void TestMassivePublishWithWithoutFlush_Local() + { + using var conn = Create(); - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => received.Count == 1); - lock (received) - { - Assert.Single(received); - } + var sub = conn.GetSubscriber(); + TestMassivePublish(sub, Me(), "local"); + } - // Give reception a bit, the handler could be delayed under load - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); - Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); + [FactLongRunning] + public void TestMassivePublishWithWithoutFlush_Remote() + { + using var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort); - sub.Unsubscribe("a*c"); - count = sub.Publish("abc", "ghi"); + var sub = conn.GetSubscriber(); + TestMassivePublish(sub, Me(), "remote"); + } - await PingAsync(pub, sub).ForAwait(); + private void TestMassivePublish(ISubscriber sub, string channel, string caption) + { + const int loop = 10000; - lock (received) - { - Assert.Single(received); - } - } - } + var tasks = new Task[loop]; - [Fact] - public void TestPublishWithNoSubscribers() + var withFAF = Stopwatch.StartNew(); + for (int i = 0; i < loop; i++) { - using (var muxer = Create()) - { - var conn = muxer.GetSubscriber(); - Assert.Equal(0, conn.Publish(Me() + "channel", "message")); - } + sub.Publish(channel, "bar", CommandFlags.FireAndForget); } + withFAF.Stop(); - [FactLongRunning] - public void TestMassivePublishWithWithoutFlush_Local() + var withAsync = Stopwatch.StartNew(); + for (int i = 0; i < loop; i++) { - using (var muxer = Create()) - { - var conn = muxer.GetSubscriber(); - TestMassivePublish(conn, Me(), "local"); - } + tasks[i] = sub.PublishAsync(channel, "bar"); } + sub.WaitAll(tasks); + withAsync.Stop(); + + Log("{2}: {0}ms (F+F) vs {1}ms (async)", + withFAF.ElapsedMilliseconds, withAsync.ElapsedMilliseconds, caption); + // We've made async so far, this test isn't really valid anymore + // So let's check they're at least within a few seconds. + Assert.True(withFAF.ElapsedMilliseconds < withAsync.ElapsedMilliseconds + 3000, caption); + } + + [Fact] + public async Task PubSubGetAllAnyOrder() + { + using var sonn = Create(syncTimeout: 20000, shared: false, log: Writer); - [FactLongRunning] - public void TestMassivePublishWithWithoutFlush_Remote() + var sub = sonn.GetSubscriber(); + RedisChannel channel = Me(); + const int count = 1000; + var syncLock = new object(); + + Assert.True(sub.IsConnected()); + var data = new HashSet(); + await sub.SubscribeAsync(channel, (_, val) => { - using (var muxer = Create(configuration: TestConfig.Current.RemoteServerAndPort)) + bool pulse; + lock (data) { - var conn = muxer.GetSubscriber(); - TestMassivePublish(conn, Me(), "remote"); + data.Add(int.Parse(Encoding.UTF8.GetString(val!))); + pulse = data.Count == count; + if ((data.Count % 100) == 99) Log(data.Count.ToString()); } - } + if (pulse) + { + lock (syncLock) + { + Monitor.PulseAll(syncLock); + } + } + }).ForAwait(); - private void TestMassivePublish(ISubscriber conn, string channel, string caption) + lock (syncLock) { - const int loop = 10000; - - var tasks = new Task[loop]; - - var withFAF = Stopwatch.StartNew(); - for (int i = 0; i < loop; i++) + for (int i = 0; i < count; i++) { - conn.Publish(channel, "bar", CommandFlags.FireAndForget); + sub.Publish(channel, i.ToString(), CommandFlags.FireAndForget); } - withFAF.Stop(); - - var withAsync = Stopwatch.StartNew(); - for (int i = 0; i < loop; i++) + sub.Ping(); + if (!Monitor.Wait(syncLock, 20000)) { - tasks[i] = conn.PublishAsync(channel, "bar"); + throw new TimeoutException("Items: " + data.Count); + } + for (int i = 0; i < count; i++) + { + Assert.Contains(i, data); } - conn.WaitAll(tasks); - withAsync.Stop(); - - Log("{2}: {0}ms (F+F) vs {1}ms (async)", - withFAF.ElapsedMilliseconds, withAsync.ElapsedMilliseconds, caption); - // We've made async so far, this test isn't really valid anymore - // So let's check they're at least within a few seconds. - Assert.True(withFAF.ElapsedMilliseconds < withAsync.ElapsedMilliseconds + 3000, caption); } + } - [Fact] - public async Task PubSubGetAllAnyOrder() + [Fact] + public async Task PubSubGetAllCorrectOrder() + { + using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { - using (var muxer = Create(syncTimeout: 20000, shared: false, log: Writer)) + var sub = conn.GetSubscriber(); + RedisChannel channel = Me(); + const int count = 250; + var syncLock = new object(); + + var data = new List(count); + var subChannel = await sub.SubscribeAsync(channel).ForAwait(); + + await sub.PingAsync().ForAwait(); + + async Task RunLoop() { - var sub = muxer.GetSubscriber(); - RedisChannel channel = Me(); - const int count = 1000; - var syncLock = new object(); - - Assert.True(sub.IsConnected()); - var data = new HashSet(); - await sub.SubscribeAsync(channel, (_, val) => + while (!subChannel.Completion.IsCompleted) { - bool pulse; + var work = await subChannel.ReadAsync().ForAwait(); + int i = int.Parse(Encoding.UTF8.GetString(work.Message!)); lock (data) { - data.Add(int.Parse(Encoding.UTF8.GetString(val!))); - pulse = data.Count == count; - if ((data.Count % 100) == 99) Log(data.Count.ToString()); - } - if (pulse) - { - lock (syncLock) - { - Monitor.PulseAll(syncLock); - } + data.Add(i); + if (data.Count == count) break; + if ((data.Count % 100) == 99) Log("Received: " + data.Count.ToString()); } - }).ForAwait(); - + } lock (syncLock) { - for (int i = 0; i < count; i++) - { - sub.Publish(channel, i.ToString(), CommandFlags.FireAndForget); - } - sub.Ping(); - if (!Monitor.Wait(syncLock, 20000)) - { - throw new TimeoutException("Items: " + data.Count); - } - for (int i = 0; i < count; i++) - { - Assert.Contains(i, data); - } + Log("PulseAll."); + Monitor.PulseAll(syncLock); } } - } - [Fact] - public async Task PubSubGetAllCorrectOrder() - { - using (var muxer = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) + lock (syncLock) { - var sub = muxer.GetSubscriber(); - RedisChannel channel = Me(); - const int count = 250; - var syncLock = new object(); - - var data = new List(count); - var subChannel = await sub.SubscribeAsync(channel).ForAwait(); - - await sub.PingAsync().ForAwait(); - - async Task RunLoop() + // Intentionally not awaited - running in parallel + _ = Task.Run(RunLoop); + for (int i = 0; i < count; i++) { - while (!subChannel.Completion.IsCompleted) - { - var work = await subChannel.ReadAsync().ForAwait(); - int i = int.Parse(Encoding.UTF8.GetString(work.Message!)); - lock (data) - { - data.Add(i); - if (data.Count == count) break; - if ((data.Count % 100) == 99) Log("Received: " + data.Count.ToString()); - } - } - lock (syncLock) - { - Log("PulseAll."); - Monitor.PulseAll(syncLock); - } + sub.Publish(channel, i.ToString()); + if ((i % 100) == 99) Log("Published: " + i.ToString()); } - - lock (syncLock) + Log("Send loop complete."); + if (!Monitor.Wait(syncLock, 20000)) { - // Intentionally not awaited - running in parallel - _ = Task.Run(RunLoop); - for (int i = 0; i < count; i++) - { - sub.Publish(channel, i.ToString()); - if ((i % 100) == 99) Log("Published: " + i.ToString()); - } - Log("Send loop complete."); - if (!Monitor.Wait(syncLock, 20000)) - { - throw new TimeoutException("Items: " + data.Count); - } - Log("Unsubscribe."); - subChannel.Unsubscribe(); - Log("Sub Ping."); - sub.Ping(); - Log("Database Ping."); - muxer.GetDatabase().Ping(); - for (int i = 0; i < count; i++) - { - Assert.Equal(i, data[i]); - } + throw new TimeoutException("Items: " + data.Count); } - - Log("Awaiting completion."); - await subChannel.Completion; - Log("Completion awaited."); - await Assert.ThrowsAsync(async delegate + Log("Unsubscribe."); + subChannel.Unsubscribe(); + Log("Sub Ping."); + sub.Ping(); + Log("Database Ping."); + conn.GetDatabase().Ping(); + for (int i = 0; i < count; i++) { - await subChannel.ReadAsync().ForAwait(); - }).ForAwait(); - Log("End of muxer."); + Assert.Equal(i, data[i]); + } } - Log("End of test."); + + Log("Awaiting completion."); + await subChannel.Completion; + Log("Completion awaited."); + await Assert.ThrowsAsync(async delegate + { + await subChannel.ReadAsync().ForAwait(); + }).ForAwait(); + Log("End of muxer."); } + Log("End of test."); + } - [Fact] - public async Task PubSubGetAllCorrectOrder_OnMessage_Sync() + [Fact] + public async Task PubSubGetAllCorrectOrder_OnMessage_Sync() + { + using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { - using (var muxer = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) + var sub = conn.GetSubscriber(); + RedisChannel channel = Me(); + const int count = 1000; + var syncLock = new object(); + + var data = new List(count); + var subChannel = await sub.SubscribeAsync(channel).ForAwait(); + subChannel.OnMessage(msg => { - var sub = muxer.GetSubscriber(); - RedisChannel channel = Me(); - const int count = 1000; - var syncLock = new object(); - - var data = new List(count); - var subChannel = await sub.SubscribeAsync(channel).ForAwait(); - subChannel.OnMessage(msg => + int i = int.Parse(Encoding.UTF8.GetString(msg.Message!)); + bool pulse = false; + lock (data) { - int i = int.Parse(Encoding.UTF8.GetString(msg.Message!)); - bool pulse = false; - lock (data) - { - data.Add(i); - if (data.Count == count) pulse = true; - if ((data.Count % 100) == 99) Log("Received: " + data.Count.ToString()); - } - if (pulse) - { - lock (syncLock) - { - Monitor.PulseAll(syncLock); - } - } - }); - await sub.PingAsync().ForAwait(); - - lock (syncLock) + data.Add(i); + if (data.Count == count) pulse = true; + if ((data.Count % 100) == 99) Log("Received: " + data.Count.ToString()); + } + if (pulse) { - for (int i = 0; i < count; i++) - { - sub.Publish(channel, i.ToString(), CommandFlags.FireAndForget); - if ((i % 100) == 99) Log("Published: " + i.ToString()); - } - Log("Send loop complete."); - if (!Monitor.Wait(syncLock, 20000)) - { - throw new TimeoutException("Items: " + data.Count); - } - Log("Unsubscribe."); - subChannel.Unsubscribe(); - Log("Sub Ping."); - sub.Ping(); - Log("Database Ping."); - muxer.GetDatabase().Ping(); - for (int i = 0; i < count; i++) + lock (syncLock) { - Assert.Equal(i, data[i]); + Monitor.PulseAll(syncLock); } } + }); + await sub.PingAsync().ForAwait(); - Log("Awaiting completion."); - await subChannel.Completion; - Log("Completion awaited."); - Assert.True(subChannel.Completion.IsCompleted); - await Assert.ThrowsAsync(async delegate + lock (syncLock) + { + for (int i = 0; i < count; i++) + { + sub.Publish(channel, i.ToString(), CommandFlags.FireAndForget); + if ((i % 100) == 99) Log("Published: " + i.ToString()); + } + Log("Send loop complete."); + if (!Monitor.Wait(syncLock, 20000)) + { + throw new TimeoutException("Items: " + data.Count); + } + Log("Unsubscribe."); + subChannel.Unsubscribe(); + Log("Sub Ping."); + sub.Ping(); + Log("Database Ping."); + conn.GetDatabase().Ping(); + for (int i = 0; i < count; i++) { - await subChannel.ReadAsync().ForAwait(); - }).ForAwait(); - Log("End of muxer."); + Assert.Equal(i, data[i]); + } } - Log("End of test."); + + Log("Awaiting completion."); + await subChannel.Completion; + Log("Completion awaited."); + Assert.True(subChannel.Completion.IsCompleted); + await Assert.ThrowsAsync(async delegate + { + await subChannel.ReadAsync().ForAwait(); + }).ForAwait(); + Log("End of muxer."); } + Log("End of test."); + } - [Fact] - public async Task PubSubGetAllCorrectOrder_OnMessage_Async() + [Fact] + public async Task PubSubGetAllCorrectOrder_OnMessage_Async() + { + using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { - using (var muxer = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) + var sub = conn.GetSubscriber(); + RedisChannel channel = Me(); + const int count = 1000; + var syncLock = new object(); + + var data = new List(count); + var subChannel = await sub.SubscribeAsync(channel).ForAwait(); + subChannel.OnMessage(msg => { - var sub = muxer.GetSubscriber(); - RedisChannel channel = Me(); - const int count = 1000; - var syncLock = new object(); - - var data = new List(count); - var subChannel = await sub.SubscribeAsync(channel).ForAwait(); - subChannel.OnMessage(msg => + int i = int.Parse(Encoding.UTF8.GetString(msg.Message!)); + bool pulse = false; + lock (data) { - int i = int.Parse(Encoding.UTF8.GetString(msg.Message!)); - bool pulse = false; - lock (data) - { - data.Add(i); - if (data.Count == count) pulse = true; - if ((data.Count % 100) == 99) Log("Received: " + data.Count.ToString()); - } - if (pulse) + data.Add(i); + if (data.Count == count) pulse = true; + if ((data.Count % 100) == 99) Log("Received: " + data.Count.ToString()); + } + if (pulse) + { + lock (syncLock) { - lock (syncLock) - { - Monitor.PulseAll(syncLock); - } + Monitor.PulseAll(syncLock); } - // Making sure we cope with null being returned here by a handler - return i % 2 == 0 ? null! : Task.CompletedTask; - }); - await sub.PingAsync().ForAwait(); + } + // Making sure we cope with null being returned here by a handler + return i % 2 == 0 ? null! : Task.CompletedTask; + }); + await sub.PingAsync().ForAwait(); - // Give a delay between subscriptions and when we try to publish to be safe - await Task.Delay(1000).ForAwait(); + // Give a delay between subscriptions and when we try to publish to be safe + await Task.Delay(1000).ForAwait(); - lock (syncLock) + lock (syncLock) + { + for (int i = 0; i < count; i++) { - for (int i = 0; i < count; i++) - { - sub.Publish(channel, i.ToString(), CommandFlags.FireAndForget); - if ((i % 100) == 99) Log("Published: " + i.ToString()); - } - Log("Send loop complete."); - if (!Monitor.Wait(syncLock, 20000)) - { - throw new TimeoutException("Items: " + data.Count); - } - Log("Unsubscribe."); - subChannel.Unsubscribe(); - Log("Sub Ping."); - sub.Ping(); - Log("Database Ping."); - muxer.GetDatabase().Ping(); - for (int i = 0; i < count; i++) - { - Assert.Equal(i, data[i]); - } + sub.Publish(channel, i.ToString(), CommandFlags.FireAndForget); + if ((i % 100) == 99) Log("Published: " + i.ToString()); } - - Log("Awaiting completion."); - await subChannel.Completion; - Log("Completion awaited."); - Assert.True(subChannel.Completion.IsCompleted); - await Assert.ThrowsAsync(async delegate + Log("Send loop complete."); + if (!Monitor.Wait(syncLock, 20000)) + { + throw new TimeoutException("Items: " + data.Count); + } + Log("Unsubscribe."); + subChannel.Unsubscribe(); + Log("Sub Ping."); + sub.Ping(); + Log("Database Ping."); + conn.GetDatabase().Ping(); + for (int i = 0; i < count; i++) { - await subChannel.ReadAsync().ForAwait(); - }).ForAwait(); - Log("End of muxer."); + Assert.Equal(i, data[i]); + } } - Log("End of test."); - } - [Fact] - public async Task TestPublishWithSubscribers() - { - var channel = Me(); - using (var muxerA = Create(shared: false, log: Writer)) - using (var muxerB = Create(shared: false, log: Writer)) - using (var conn = Create()) + Log("Awaiting completion."); + await subChannel.Completion; + Log("Completion awaited."); + Assert.True(subChannel.Completion.IsCompleted); + await Assert.ThrowsAsync(async delegate { - var listenA = muxerA.GetSubscriber(); - var listenB = muxerB.GetSubscriber(); - var t1 = listenA.SubscribeAsync(channel, delegate { }); - var t2 = listenB.SubscribeAsync(channel, delegate { }); + await subChannel.ReadAsync().ForAwait(); + }).ForAwait(); + Log("End of muxer."); + } + Log("End of test."); + } - await Task.WhenAll(t1, t2).ForAwait(); + [Fact] + public async Task TestPublishWithSubscribers() + { + using var connA = Create(shared: false, log: Writer); + using var connB = Create(shared: false, log: Writer); + using var connPub = Create(); - // subscribe is just a thread-race-mess - await listenA.PingAsync(); - await listenB.PingAsync(); + var channel = Me(); + var listenA = connA.GetSubscriber(); + var listenB = connB.GetSubscriber(); + var t1 = listenA.SubscribeAsync(channel, delegate { }); + var t2 = listenB.SubscribeAsync(channel, delegate { }); - var pub = conn.GetSubscriber().PublishAsync(channel, "message"); - Assert.Equal(2, await pub); // delivery count - } - } + await Task.WhenAll(t1, t2).ForAwait(); - [Fact] - public async Task TestMultipleSubscribersGetMessage() - { - var channel = Me(); - using (var muxerA = Create(shared: false, log: Writer)) - using (var muxerB = Create(shared: false, log: Writer)) - using (var conn = Create()) - { - var listenA = muxerA.GetSubscriber(); - var listenB = muxerB.GetSubscriber(); - conn.GetDatabase().Ping(); - var pub = conn.GetSubscriber(); - int gotA = 0, gotB = 0; - var tA = listenA.SubscribeAsync(channel, (_, msg) => { if (msg == "message") Interlocked.Increment(ref gotA); }); - var tB = listenB.SubscribeAsync(channel, (_, msg) => { if (msg == "message") Interlocked.Increment(ref gotB); }); - await Task.WhenAll(tA, tB).ForAwait(); - Assert.Equal(2, pub.Publish(channel, "message")); - await AllowReasonableTimeToPublishAndProcess().ForAwait(); - Assert.Equal(1, Interlocked.CompareExchange(ref gotA, 0, 0)); - Assert.Equal(1, Interlocked.CompareExchange(ref gotB, 0, 0)); - - // and unsubscibe... - tA = listenA.UnsubscribeAsync(channel); - await tA; - Assert.Equal(1, pub.Publish(channel, "message")); - await AllowReasonableTimeToPublishAndProcess().ForAwait(); - Assert.Equal(1, Interlocked.CompareExchange(ref gotA, 0, 0)); - Assert.Equal(2, Interlocked.CompareExchange(ref gotB, 0, 0)); - } - } + // subscribe is just a thread-race-mess + await listenA.PingAsync(); + await listenB.PingAsync(); - [Fact] - public async Task Issue38() - { - // https://code.google.com/p/booksleeve/issues/detail?id=38 - using (var pub = Create(log: Writer)) - { - var sub = pub.GetSubscriber(); - int count = 0; - var prefix = Me(); - void handler(RedisChannel _, RedisValue __) => Interlocked.Increment(ref count); - var a0 = sub.SubscribeAsync(prefix + "foo", handler); - var a1 = sub.SubscribeAsync(prefix + "bar", handler); - var b0 = sub.SubscribeAsync(prefix + "f*o", handler); - var b1 = sub.SubscribeAsync(prefix + "b*r", handler); - await Task.WhenAll(a0, a1, b0, b1).ForAwait(); - - var c = sub.PublishAsync(prefix + "foo", "foo"); - var d = sub.PublishAsync(prefix + "f@o", "f@o"); - var e = sub.PublishAsync(prefix + "bar", "bar"); - var f = sub.PublishAsync(prefix + "b@r", "b@r"); - await Task.WhenAll(c, d, e, f).ForAwait(); - - long total = c.Result + d.Result + e.Result + f.Result; - - await AllowReasonableTimeToPublishAndProcess().ForAwait(); - - Assert.Equal(6, total); // sent - Assert.Equal(6, Interlocked.CompareExchange(ref count, 0, 0)); // received - } - } - - internal static Task AllowReasonableTimeToPublishAndProcess() => Task.Delay(500); + var pub = connPub.GetSubscriber().PublishAsync(channel, "message"); + Assert.Equal(2, await pub); // delivery count + } - [Fact] - public async Task TestPartialSubscriberGetMessage() - { - using (var muxerA = Create()) - using (var muxerB = Create()) - using (var conn = Create()) - { - int gotA = 0, gotB = 0; - var listenA = muxerA.GetSubscriber(); - var listenB = muxerB.GetSubscriber(); - var pub = conn.GetSubscriber(); - var prefix = Me(); - var tA = listenA.SubscribeAsync(prefix + "channel", (s, msg) => { if (s == prefix + "channel" && msg == "message") Interlocked.Increment(ref gotA); }); - var tB = listenB.SubscribeAsync(prefix + "chann*", (s, msg) => { if (s == prefix + "channel" && msg == "message") Interlocked.Increment(ref gotB); }); - await Task.WhenAll(tA, tB).ForAwait(); - Assert.Equal(2, pub.Publish(prefix + "channel", "message")); - await AllowReasonableTimeToPublishAndProcess().ForAwait(); - Assert.Equal(1, Interlocked.CompareExchange(ref gotA, 0, 0)); - Assert.Equal(1, Interlocked.CompareExchange(ref gotB, 0, 0)); - - // and unsubscibe... - tB = listenB.UnsubscribeAsync(prefix + "chann*", null); - await tB; - Assert.Equal(1, pub.Publish(prefix + "channel", "message")); - await AllowReasonableTimeToPublishAndProcess().ForAwait(); - Assert.Equal(2, Interlocked.CompareExchange(ref gotA, 0, 0)); - Assert.Equal(1, Interlocked.CompareExchange(ref gotB, 0, 0)); - } - } + [Fact] + public async Task TestMultipleSubscribersGetMessage() + { + using var connA = Create(shared: false, log: Writer); + using var connB = Create(shared: false, log: Writer); + using var connPub = Create(); + + var channel = Me(); + var listenA = connA.GetSubscriber(); + var listenB = connB.GetSubscriber(); + connPub.GetDatabase().Ping(); + var pub = connPub.GetSubscriber(); + int gotA = 0, gotB = 0; + var tA = listenA.SubscribeAsync(channel, (_, msg) => { if (msg == "message") Interlocked.Increment(ref gotA); }); + var tB = listenB.SubscribeAsync(channel, (_, msg) => { if (msg == "message") Interlocked.Increment(ref gotB); }); + await Task.WhenAll(tA, tB).ForAwait(); + Assert.Equal(2, pub.Publish(channel, "message")); + await AllowReasonableTimeToPublishAndProcess().ForAwait(); + Assert.Equal(1, Interlocked.CompareExchange(ref gotA, 0, 0)); + Assert.Equal(1, Interlocked.CompareExchange(ref gotB, 0, 0)); + + // and unsubscribe... + tA = listenA.UnsubscribeAsync(channel); + await tA; + Assert.Equal(1, pub.Publish(channel, "message")); + await AllowReasonableTimeToPublishAndProcess().ForAwait(); + Assert.Equal(1, Interlocked.CompareExchange(ref gotA, 0, 0)); + Assert.Equal(2, Interlocked.CompareExchange(ref gotB, 0, 0)); + } - [Fact] - public async Task TestSubscribeUnsubscribeAndSubscribeAgain() - { - using (var pubMuxer = Create()) - using (var subMuxer = Create()) - { - var prefix = Me(); - var pub = pubMuxer.GetSubscriber(); - var sub = subMuxer.GetSubscriber(); - int x = 0, y = 0; - var t1 = sub.SubscribeAsync(prefix + "abc", delegate { Interlocked.Increment(ref x); }); - var t2 = sub.SubscribeAsync(prefix + "ab*", delegate { Interlocked.Increment(ref y); }); - await Task.WhenAll(t1, t2).ForAwait(); - pub.Publish(prefix + "abc", ""); - await AllowReasonableTimeToPublishAndProcess().ForAwait(); - Assert.Equal(1, Volatile.Read(ref x)); - Assert.Equal(1, Volatile.Read(ref y)); - t1 = sub.UnsubscribeAsync(prefix + "abc", null); - t2 = sub.UnsubscribeAsync(prefix + "ab*", null); - await Task.WhenAll(t1, t2).ForAwait(); - pub.Publish(prefix + "abc", ""); - Assert.Equal(1, Volatile.Read(ref x)); - Assert.Equal(1, Volatile.Read(ref y)); - t1 = sub.SubscribeAsync(prefix + "abc", delegate { Interlocked.Increment(ref x); }); - t2 = sub.SubscribeAsync(prefix + "ab*", delegate { Interlocked.Increment(ref y); }); - await Task.WhenAll(t1, t2).ForAwait(); - pub.Publish(prefix + "abc", ""); - await AllowReasonableTimeToPublishAndProcess().ForAwait(); - Assert.Equal(2, Volatile.Read(ref x)); - Assert.Equal(2, Volatile.Read(ref y)); - } - } + [Fact] + public async Task Issue38() + { + // https://code.google.com/p/booksleeve/issues/detail?id=38 + using var conn = Create(log: Writer); + + var sub = conn.GetSubscriber(); + int count = 0; + var prefix = Me(); + void handler(RedisChannel _, RedisValue __) => Interlocked.Increment(ref count); + var a0 = sub.SubscribeAsync(prefix + "foo", handler); + var a1 = sub.SubscribeAsync(prefix + "bar", handler); + var b0 = sub.SubscribeAsync(prefix + "f*o", handler); + var b1 = sub.SubscribeAsync(prefix + "b*r", handler); + await Task.WhenAll(a0, a1, b0, b1).ForAwait(); + + var c = sub.PublishAsync(prefix + "foo", "foo"); + var d = sub.PublishAsync(prefix + "f@o", "f@o"); + var e = sub.PublishAsync(prefix + "bar", "bar"); + var f = sub.PublishAsync(prefix + "b@r", "b@r"); + await Task.WhenAll(c, d, e, f).ForAwait(); + + long total = c.Result + d.Result + e.Result + f.Result; + + await AllowReasonableTimeToPublishAndProcess().ForAwait(); + + Assert.Equal(6, total); // sent + Assert.Equal(6, Interlocked.CompareExchange(ref count, 0, 0)); // received + } - [Fact] - public async Task AzureRedisEventsAutomaticSubscribe() - { - Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); - Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); + internal static Task AllowReasonableTimeToPublishAndProcess() => Task.Delay(500); - bool didUpdate = false; - var options = new ConfigurationOptions() - { - EndPoints = { TestConfig.Current.AzureCacheServer }, - Password = TestConfig.Current.AzureCachePassword, - Ssl = true - }; + [Fact] + public async Task TestPartialSubscriberGetMessage() + { + using var connA = Create(); + using var connB = Create(); + using var connPub = Create(); + + int gotA = 0, gotB = 0; + var listenA = connA.GetSubscriber(); + var listenB = connB.GetSubscriber(); + var pub = connPub.GetSubscriber(); + var prefix = Me(); + var tA = listenA.SubscribeAsync(prefix + "channel", (s, msg) => { if (s == prefix + "channel" && msg == "message") Interlocked.Increment(ref gotA); }); + var tB = listenB.SubscribeAsync(prefix + "chann*", (s, msg) => { if (s == prefix + "channel" && msg == "message") Interlocked.Increment(ref gotB); }); + await Task.WhenAll(tA, tB).ForAwait(); + Assert.Equal(2, pub.Publish(prefix + "channel", "message")); + await AllowReasonableTimeToPublishAndProcess().ForAwait(); + Assert.Equal(1, Interlocked.CompareExchange(ref gotA, 0, 0)); + Assert.Equal(1, Interlocked.CompareExchange(ref gotB, 0, 0)); + + // and unsubscibe... + tB = listenB.UnsubscribeAsync(prefix + "chann*", null); + await tB; + Assert.Equal(1, pub.Publish(prefix + "channel", "message")); + await AllowReasonableTimeToPublishAndProcess().ForAwait(); + Assert.Equal(2, Interlocked.CompareExchange(ref gotA, 0, 0)); + Assert.Equal(1, Interlocked.CompareExchange(ref gotB, 0, 0)); + } - using (var connection = await ConnectionMultiplexer.ConnectAsync(options)) - { - connection.ServerMaintenanceEvent += (object? _, ServerMaintenanceEvent e) => - { - if (e is AzureMaintenanceEvent) - { - didUpdate = true; - } - }; + [Fact] + public async Task TestSubscribeUnsubscribeAndSubscribeAgain() + { + using var connPub = Create(); + using var connSub = Create(); + + var prefix = Me(); + var pub = connPub.GetSubscriber(); + var sub = connSub.GetSubscriber(); + int x = 0, y = 0; + var t1 = sub.SubscribeAsync(prefix + "abc", delegate { Interlocked.Increment(ref x); }); + var t2 = sub.SubscribeAsync(prefix + "ab*", delegate { Interlocked.Increment(ref y); }); + await Task.WhenAll(t1, t2).ForAwait(); + pub.Publish(prefix + "abc", ""); + await AllowReasonableTimeToPublishAndProcess().ForAwait(); + Assert.Equal(1, Volatile.Read(ref x)); + Assert.Equal(1, Volatile.Read(ref y)); + t1 = sub.UnsubscribeAsync(prefix + "abc", null); + t2 = sub.UnsubscribeAsync(prefix + "ab*", null); + await Task.WhenAll(t1, t2).ForAwait(); + pub.Publish(prefix + "abc", ""); + Assert.Equal(1, Volatile.Read(ref x)); + Assert.Equal(1, Volatile.Read(ref y)); + t1 = sub.SubscribeAsync(prefix + "abc", delegate { Interlocked.Increment(ref x); }); + t2 = sub.SubscribeAsync(prefix + "ab*", delegate { Interlocked.Increment(ref y); }); + await Task.WhenAll(t1, t2).ForAwait(); + pub.Publish(prefix + "abc", ""); + await AllowReasonableTimeToPublishAndProcess().ForAwait(); + Assert.Equal(2, Volatile.Read(ref x)); + Assert.Equal(2, Volatile.Read(ref y)); + } - var pubSub = connection.GetSubscriber(); - await pubSub.PublishAsync("AzureRedisEvents", "HI"); - await Task.Delay(100); + [Fact] + public async Task AzureRedisEventsAutomaticSubscribe() + { + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); - Assert.True(didUpdate); - } - } + bool didUpdate = false; + var options = new ConfigurationOptions() + { + EndPoints = { TestConfig.Current.AzureCacheServer }, + Password = TestConfig.Current.AzureCachePassword, + Ssl = true + }; - [Fact] - public async Task SubscriptionsSurviveConnectionFailureAsync() + using (var connection = await ConnectionMultiplexer.ConnectAsync(options)) { - using (var muxer = (Create(allowAdmin: true, shared: false, log: Writer, syncTimeout: 1000) as ConnectionMultiplexer)!) + connection.ServerMaintenanceEvent += (object? _, ServerMaintenanceEvent e) => { - var profiler = muxer.AddProfiler(); - RedisChannel channel = Me(); - var sub = muxer.GetSubscriber(); - int counter = 0; - Assert.True(sub.IsConnected()); - await sub.SubscribeAsync(channel, delegate + if (e is AzureMaintenanceEvent) { - Interlocked.Increment(ref counter); - }).ConfigureAwait(false); - - var profile1 = Log(profiler); - - Assert.Equal(1, muxer.GetSubscriptionsCount()); - - await Task.Delay(200).ConfigureAwait(false); + didUpdate = true; + } + }; - await sub.PublishAsync(channel, "abc").ConfigureAwait(false); - sub.Ping(); - await Task.Delay(200).ConfigureAwait(false); - - var counter1 = Thread.VolatileRead(ref counter); - Log($"Expecting 1 message, got {counter1}"); - Assert.Equal(1, counter1); - - var server = GetServer(muxer); - var socketCount = server.GetCounters().Subscription.SocketCount; - Log($"Expecting 1 socket, got {socketCount}"); - Assert.Equal(1, socketCount); - - // We might fail both connections or just the primary in the time period - SetExpectedAmbientFailureCount(-1); - - // Make sure we fail all the way - muxer.AllowConnect = false; - Log("Failing connection"); - // Fail all connections - server.SimulateConnectionFailure(SimulatedFailureType.All); - // Trigger failure (RedisTimeoutException because of backlog behavior) - Assert.Throws(() => sub.Ping()); - Assert.False(sub.IsConnected(channel)); - - // Now reconnect... - muxer.AllowConnect = true; - Log("Waiting on reconnect"); - // Wait until we're reconnected - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => sub.IsConnected(channel)); - Log("Reconnected"); - // Ensure we're reconnected - Assert.True(sub.IsConnected(channel)); - - // Ensure we've sent the subscribe command after reconnecting - var profile2 = Log(profiler); - //Assert.Equal(1, profile2.Count(p => p.Command == nameof(RedisCommand.SUBSCRIBE))); - - Log("Issuing ping after reconnected"); - sub.Ping(); + var pubSub = connection.GetSubscriber(); + await pubSub.PublishAsync("AzureRedisEvents", "HI"); + await Task.Delay(100); - var muxerSubCount = muxer.GetSubscriptionsCount(); - Log($"Muxer thinks we have {muxerSubCount} subscriber(s)."); - Assert.Equal(1, muxerSubCount); + Assert.True(didUpdate); + } + } - var muxerSubs = muxer.GetSubscriptions(); - foreach (var pair in muxerSubs) - { - var muxerSub = pair.Value; - Log($" Muxer Sub: {pair.Key}: (EndPoint: {muxerSub.GetCurrentServer()}, Connected: {muxerSub.IsConnected})"); - } + [Fact] + public async Task SubscriptionsSurviveConnectionFailureAsync() + { + using var conn = (Create(allowAdmin: true, shared: false, log: Writer, syncTimeout: 1000) as ConnectionMultiplexer)!; + + var profiler = conn.AddProfiler(); + RedisChannel channel = Me(); + var sub = conn.GetSubscriber(); + int counter = 0; + Assert.True(sub.IsConnected()); + await sub.SubscribeAsync(channel, delegate + { + Interlocked.Increment(ref counter); + }).ConfigureAwait(false); + + var profile1 = Log(profiler); + + Assert.Equal(1, conn.GetSubscriptionsCount()); + + await Task.Delay(200).ConfigureAwait(false); + + await sub.PublishAsync(channel, "abc").ConfigureAwait(false); + sub.Ping(); + await Task.Delay(200).ConfigureAwait(false); + + var counter1 = Thread.VolatileRead(ref counter); + Log($"Expecting 1 message, got {counter1}"); + Assert.Equal(1, counter1); + + var server = GetServer(conn); + var socketCount = server.GetCounters().Subscription.SocketCount; + Log($"Expecting 1 socket, got {socketCount}"); + Assert.Equal(1, socketCount); + + // We might fail both connections or just the primary in the time period + SetExpectedAmbientFailureCount(-1); + + // Make sure we fail all the way + conn.AllowConnect = false; + Log("Failing connection"); + // Fail all connections + server.SimulateConnectionFailure(SimulatedFailureType.All); + // Trigger failure (RedisTimeoutException because of backlog behavior) + Assert.Throws(() => sub.Ping()); + Assert.False(sub.IsConnected(channel)); + + // Now reconnect... + conn.AllowConnect = true; + Log("Waiting on reconnect"); + // Wait until we're reconnected + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => sub.IsConnected(channel)); + Log("Reconnected"); + // Ensure we're reconnected + Assert.True(sub.IsConnected(channel)); + + // Ensure we've sent the subscribe command after reconnecting + var profile2 = Log(profiler); + //Assert.Equal(1, profile2.Count(p => p.Command == nameof(RedisCommand.SUBSCRIBE))); + + Log("Issuing ping after reconnected"); + sub.Ping(); + + var muxerSubCount = conn.GetSubscriptionsCount(); + Log($"Muxer thinks we have {muxerSubCount} subscriber(s)."); + Assert.Equal(1, muxerSubCount); + + var muxerSubs = conn.GetSubscriptions(); + foreach (var pair in muxerSubs) + { + var muxerSub = pair.Value; + Log($" Muxer Sub: {pair.Key}: (EndPoint: {muxerSub.GetCurrentServer()}, Connected: {muxerSub.IsConnected})"); + } - Log("Publishing"); - var published = await sub.PublishAsync(channel, "abc").ConfigureAwait(false); + Log("Publishing"); + var published = await sub.PublishAsync(channel, "abc").ConfigureAwait(false); - Log($"Published to {published} subscriber(s)."); - Assert.Equal(1, published); + Log($"Published to {published} subscriber(s)."); + Assert.Equal(1, published); - // Give it a few seconds to get our messages - Log("Waiting for 2 messages"); - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => Thread.VolatileRead(ref counter) == 2); + // Give it a few seconds to get our messages + Log("Waiting for 2 messages"); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => Thread.VolatileRead(ref counter) == 2); - var counter2 = Thread.VolatileRead(ref counter); - Log($"Expecting 2 messages, got {counter2}"); - Assert.Equal(2, counter2); + var counter2 = Thread.VolatileRead(ref counter); + Log($"Expecting 2 messages, got {counter2}"); + Assert.Equal(2, counter2); - // Log all commands at the end - Log("All commands since connecting:"); - var profile3 = profiler.FinishProfiling(); - foreach (var command in profile3) - { - Log($"{command.EndPoint}: {command}"); - } - } + // Log all commands at the end + Log("All commands since connecting:"); + var profile3 = profiler.FinishProfiling(); + foreach (var command in profile3) + { + Log($"{command.EndPoint}: {command}"); } } } diff --git a/tests/StackExchange.Redis.Tests/PubSubCommand.cs b/tests/StackExchange.Redis.Tests/PubSubCommand.cs index 1a3f775f2..12ab1f17e 100644 --- a/tests/StackExchange.Redis.Tests/PubSubCommand.cs +++ b/tests/StackExchange.Redis.Tests/PubSubCommand.cs @@ -5,88 +5,85 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class PubSubCommand : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class PubSubCommand : TestBase + public PubSubCommand(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + + [Fact] + public void SubscriberCount() { - public PubSubCommand(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + using var conn = Create(); - [Fact] - public void SubscriberCount() - { - using (var conn = Create()) - { - RedisChannel channel = Me() + Guid.NewGuid(); - var server = conn.GetServer(conn.GetEndPoints()[0]); + RedisChannel channel = Me() + Guid.NewGuid(); + var server = conn.GetServer(conn.GetEndPoints()[0]); - var channels = server.SubscriptionChannels(Me() + "*"); - Assert.DoesNotContain(channel, channels); + var channels = server.SubscriptionChannels(Me() + "*"); + Assert.DoesNotContain(channel, channels); - _ = server.SubscriptionPatternCount(); - var count = server.SubscriptionSubscriberCount(channel); - Assert.Equal(0, count); - conn.GetSubscriber().Subscribe(channel, delegate { }); - count = server.SubscriptionSubscriberCount(channel); - Assert.Equal(1, count); + _ = server.SubscriptionPatternCount(); + var count = server.SubscriptionSubscriberCount(channel); + Assert.Equal(0, count); + conn.GetSubscriber().Subscribe(channel, delegate { }); + count = server.SubscriptionSubscriberCount(channel); + Assert.Equal(1, count); - channels = server.SubscriptionChannels(Me() + "*"); - Assert.Contains(channel, channels); - } - } + channels = server.SubscriptionChannels(Me() + "*"); + Assert.Contains(channel, channels); + } - [Fact] - public async Task SubscriberCountAsync() - { - using (var conn = Create()) - { - RedisChannel channel = Me() + Guid.NewGuid(); - var server = conn.GetServer(conn.GetEndPoints()[0]); + [Fact] + public async Task SubscriberCountAsync() + { + using var conn = Create(); + + RedisChannel channel = Me() + Guid.NewGuid(); + var server = conn.GetServer(conn.GetEndPoints()[0]); - var channels = await server.SubscriptionChannelsAsync(Me() + "*").WithTimeout(2000); - Assert.DoesNotContain(channel, channels); + var channels = await server.SubscriptionChannelsAsync(Me() + "*").WithTimeout(2000); + Assert.DoesNotContain(channel, channels); - _ = await server.SubscriptionPatternCountAsync().WithTimeout(2000); - var count = await server.SubscriptionSubscriberCountAsync(channel).WithTimeout(2000); - Assert.Equal(0, count); - await conn.GetSubscriber().SubscribeAsync(channel, delegate { }).WithTimeout(2000); - count = await server.SubscriptionSubscriberCountAsync(channel).WithTimeout(2000); - Assert.Equal(1, count); + _ = await server.SubscriptionPatternCountAsync().WithTimeout(2000); + var count = await server.SubscriptionSubscriberCountAsync(channel).WithTimeout(2000); + Assert.Equal(0, count); + await conn.GetSubscriber().SubscribeAsync(channel, delegate { }).WithTimeout(2000); + count = await server.SubscriptionSubscriberCountAsync(channel).WithTimeout(2000); + Assert.Equal(1, count); - channels = await server.SubscriptionChannelsAsync(Me() + "*").WithTimeout(2000); - Assert.Contains(channel, channels); - } + channels = await server.SubscriptionChannelsAsync(Me() + "*").WithTimeout(2000); + Assert.Contains(channel, channels); + } +} +internal static class Util +{ + public static async Task WithTimeout(this Task task, int timeoutMs, + [CallerMemberName] string? caller = null, [CallerLineNumber] int line = 0) + { + var cts = new CancellationTokenSource(); + if (task == await Task.WhenAny(task, Task.Delay(timeoutMs, cts.Token)).ForAwait()) + { + cts.Cancel(); + await task.ForAwait(); + } + else + { + throw new TimeoutException($"timout from {caller} line {line}"); } } - internal static class Util + public static async Task WithTimeout(this Task task, int timeoutMs, + [CallerMemberName] string? caller = null, [CallerLineNumber] int line = 0) { - public static async Task WithTimeout(this Task task, int timeoutMs, - [CallerMemberName] string? caller = null, [CallerLineNumber] int line = 0) + var cts = new CancellationTokenSource(); + if (task == await Task.WhenAny(task, Task.Delay(timeoutMs, cts.Token)).ForAwait()) { - var cts = new CancellationTokenSource(); - if (task == await Task.WhenAny(task, Task.Delay(timeoutMs, cts.Token)).ForAwait()) - { - cts.Cancel(); - await task.ForAwait(); - } - else - { - throw new TimeoutException($"timout from {caller} line {line}"); - } + cts.Cancel(); + return await task.ForAwait(); } - public static async Task WithTimeout(this Task task, int timeoutMs, - [CallerMemberName] string? caller = null, [CallerLineNumber] int line = 0) + else { - var cts = new CancellationTokenSource(); - if (task == await Task.WhenAny(task, Task.Delay(timeoutMs, cts.Token)).ForAwait()) - { - cts.Cancel(); - return await task.ForAwait(); - } - else - { - throw new TimeoutException($"timout from {caller} line {line}"); - } + throw new TimeoutException($"timout from {caller} line {line}"); } } } diff --git a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs index 32812022e..d58982ec1 100644 --- a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs +++ b/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs @@ -4,75 +4,152 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class PubSubMultiserver : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class PubSubMultiserver : TestBase + public PubSubMultiserver(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; + + [Fact] + public void ChannelSharding() { - public PubSubMultiserver(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; + using var conn = (Create(channelPrefix: Me()) as ConnectionMultiplexer)!; - [Fact] - public void ChannelSharding() - { - using var muxer = (Create(channelPrefix: Me()) as ConnectionMultiplexer)!; + var defaultSlot = conn.ServerSelectionStrategy.HashSlot(default(RedisChannel)); + var slot1 = conn.ServerSelectionStrategy.HashSlot((RedisChannel)"hey"); + var slot2 = conn.ServerSelectionStrategy.HashSlot((RedisChannel)"hey2"); - var defaultSlot = muxer.ServerSelectionStrategy.HashSlot(default(RedisChannel)); - var slot1 = muxer.ServerSelectionStrategy.HashSlot((RedisChannel)"hey"); - var slot2 = muxer.ServerSelectionStrategy.HashSlot((RedisChannel)"hey2"); + Assert.NotEqual(defaultSlot, slot1); + Assert.NotEqual(ServerSelectionStrategy.NoSlot, slot1); + Assert.NotEqual(slot1, slot2); + } - Assert.NotEqual(defaultSlot, slot1); - Assert.NotEqual(ServerSelectionStrategy.NoSlot, slot1); - Assert.NotEqual(slot1, slot2); - } + [Fact] + public async Task ClusterNodeSubscriptionFailover() + { + Log("Connecting..."); + + using var conn = (Create(allowAdmin: true) as ConnectionMultiplexer)!; - [Fact] - public async Task ClusterNodeSubscriptionFailover() + var sub = conn.GetSubscriber(); + var channel = (RedisChannel)Me(); + + var count = 0; + Log("Subscribing..."); + await sub.SubscribeAsync(channel, (_, val) => { - Log("Connecting..."); - using var muxer = (Create(allowAdmin: true) as ConnectionMultiplexer)!; - var sub = muxer.GetSubscriber(); - var channel = (RedisChannel)Me(); - - var count = 0; - Log("Subscribing..."); - await sub.SubscribeAsync(channel, (_, val) => - { - Interlocked.Increment(ref count); - Log("Message: " + val); - }); - Assert.True(sub.IsConnected(channel)); - - Log("Publishing (1)..."); - Assert.Equal(0, count); - var publishedTo = await sub.PublishAsync(channel, "message1"); - // Client -> Redis -> Client -> handler takes just a moment - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); - Assert.Equal(1, count); - Log($" Published (1) to {publishedTo} subscriber(s)."); - Assert.Equal(1, publishedTo); - - var endpoint = sub.SubscribedEndpoint(channel); - var subscribedServer = muxer.GetServer(endpoint); - var subscribedServerEndpoint = muxer.GetServerEndPoint(endpoint); - - Assert.True(subscribedServer.IsConnected, "subscribedServer.IsConnected"); - Assert.NotNull(subscribedServerEndpoint); - Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); - Assert.True(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); - - Assert.True(muxer.TryGetSubscription(channel, out var subscription)); - var initialServer = subscription.GetCurrentServer(); - Assert.NotNull(initialServer); - Assert.True(initialServer.IsConnected); - Log("Connected to: " + initialServer); - - muxer.AllowConnect = false; - subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.AllSubscription); - - Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); - Assert.False(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + Interlocked.Increment(ref count); + Log("Message: " + val); + }); + Assert.True(sub.IsConnected(channel)); + + Log("Publishing (1)..."); + Assert.Equal(0, count); + var publishedTo = await sub.PublishAsync(channel, "message1"); + // Client -> Redis -> Client -> handler takes just a moment + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); + Assert.Equal(1, count); + Log($" Published (1) to {publishedTo} subscriber(s)."); + Assert.Equal(1, publishedTo); + + var endpoint = sub.SubscribedEndpoint(channel); + var subscribedServer = conn.GetServer(endpoint); + var subscribedServerEndpoint = conn.GetServerEndPoint(endpoint); + + Assert.True(subscribedServer.IsConnected, "subscribedServer.IsConnected"); + Assert.NotNull(subscribedServerEndpoint); + Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); + Assert.True(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + + Assert.True(conn.TryGetSubscription(channel, out var subscription)); + var initialServer = subscription.GetCurrentServer(); + Assert.NotNull(initialServer); + Assert.True(initialServer.IsConnected); + Log("Connected to: " + initialServer); + + conn.AllowConnect = false; + subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.AllSubscription); + + Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); + Assert.False(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); + Assert.True(subscription.IsConnected); + + var newServer = subscription.GetCurrentServer(); + Assert.NotNull(newServer); + Assert.NotEqual(newServer, initialServer); + Log("Now connected to: " + newServer); + + count = 0; + Log("Publishing (2)..."); + Assert.Equal(0, count); + publishedTo = await sub.PublishAsync(channel, "message2"); + // Client -> Redis -> Client -> handler takes just a moment + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); + Assert.Equal(1, count); + Log($" Published (2) to {publishedTo} subscriber(s)."); + + ClearAmbientFailures(); + } + + [Theory] + [InlineData(CommandFlags.PreferMaster, true)] + [InlineData(CommandFlags.PreferReplica, true)] + [InlineData(CommandFlags.DemandMaster, false)] + [InlineData(CommandFlags.DemandReplica, false)] + public async Task PrimaryReplicaSubscriptionFailover(CommandFlags flags, bool expectSuccess) + { + var config = TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + Log("Connecting..."); + + using var conn = (Create(configuration: config, shared: false, allowAdmin: true) as ConnectionMultiplexer)!; + + var sub = conn.GetSubscriber(); + var channel = (RedisChannel)(Me() + flags.ToString()); // Individual channel per case to not overlap publishers + var count = 0; + Log("Subscribing..."); + await sub.SubscribeAsync(channel, (_, val) => + { + Interlocked.Increment(ref count); + Log("Message: " + val); + }, flags); + Assert.True(sub.IsConnected(channel)); + + Log("Publishing (1)..."); + Assert.Equal(0, count); + var publishedTo = await sub.PublishAsync(channel, "message1"); + // Client -> Redis -> Client -> handler takes just a moment + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); + Assert.Equal(1, count); + Log($" Published (1) to {publishedTo} subscriber(s)."); + + var endpoint = sub.SubscribedEndpoint(channel); + var subscribedServer = conn.GetServer(endpoint); + var subscribedServerEndpoint = conn.GetServerEndPoint(endpoint); + + Assert.True(subscribedServer.IsConnected, "subscribedServer.IsConnected"); + Assert.NotNull(subscribedServerEndpoint); + Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); + Assert.True(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + + Assert.True(conn.TryGetSubscription(channel, out var subscription)); + var initialServer = subscription.GetCurrentServer(); + Assert.NotNull(initialServer); + Assert.True(initialServer.IsConnected); + Log("Connected to: " + initialServer); + + conn.AllowConnect = false; + subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.AllSubscription); + + Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); + Assert.False(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + + if (expectSuccess) + { await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); Assert.True(subscription.IsConnected); @@ -80,108 +157,34 @@ await sub.SubscribeAsync(channel, (_, val) => Assert.NotNull(newServer); Assert.NotEqual(newServer, initialServer); Log("Now connected to: " + newServer); + } + else + { + // This subscription shouldn't be able to reconnect by flags (demanding an unavailable server) + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); + Assert.False(subscription.IsConnected); + Log("Unable to reconnect (as expected)"); - count = 0; - Log("Publishing (2)..."); - Assert.Equal(0, count); - publishedTo = await sub.PublishAsync(channel, "message2"); - // Client -> Redis -> Client -> handler takes just a moment - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); - Assert.Equal(1, count); - Log($" Published (2) to {publishedTo} subscriber(s)."); + // Allow connecting back to the original + conn.AllowConnect = true; + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); + Assert.True(subscription.IsConnected); - ClearAmbientFailures(); + var newServer = subscription.GetCurrentServer(); + Assert.NotNull(newServer); + Assert.Equal(newServer, initialServer); + Log("Now connected to: " + newServer); } - [Theory] - [InlineData(CommandFlags.PreferMaster, true)] - [InlineData(CommandFlags.PreferReplica, true)] - [InlineData(CommandFlags.DemandMaster, false)] - [InlineData(CommandFlags.DemandReplica, false)] - public async Task PrimaryReplicaSubscriptionFailover(CommandFlags flags, bool expectSuccess) - { - var config = TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; - Log("Connecting..."); - using var muxer = (Create(configuration: config, shared: false, allowAdmin: true) as ConnectionMultiplexer)!; - var sub = muxer.GetSubscriber(); - var channel = (RedisChannel)(Me() + flags.ToString()); // Individual channel per case to not overlap publishers - - var count = 0; - Log("Subscribing..."); - await sub.SubscribeAsync(channel, (_, val) => - { - Interlocked.Increment(ref count); - Log("Message: " + val); - }, flags); - Assert.True(sub.IsConnected(channel)); - - Log("Publishing (1)..."); - Assert.Equal(0, count); - var publishedTo = await sub.PublishAsync(channel, "message1"); - // Client -> Redis -> Client -> handler takes just a moment - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); - Assert.Equal(1, count); - Log($" Published (1) to {publishedTo} subscriber(s)."); - - var endpoint = sub.SubscribedEndpoint(channel); - var subscribedServer = muxer.GetServer(endpoint); - var subscribedServerEndpoint = muxer.GetServerEndPoint(endpoint); - - Assert.True(subscribedServer.IsConnected, "subscribedServer.IsConnected"); - Assert.NotNull(subscribedServerEndpoint); - Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); - Assert.True(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); - - Assert.True(muxer.TryGetSubscription(channel, out var subscription)); - var initialServer = subscription.GetCurrentServer(); - Assert.NotNull(initialServer); - Assert.True(initialServer.IsConnected); - Log("Connected to: " + initialServer); - - muxer.AllowConnect = false; - subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.AllSubscription); - - Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); - Assert.False(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); - - if (expectSuccess) - { - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); - Assert.True(subscription.IsConnected); - - var newServer = subscription.GetCurrentServer(); - Assert.NotNull(newServer); - Assert.NotEqual(newServer, initialServer); - Log("Now connected to: " + newServer); - } - else - { - // This subscription shouldn't be able to reconnect by flags (demanding an unavailable server) - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); - Assert.False(subscription.IsConnected); - Log("Unable to reconnect (as expected)"); - - // Allow connecting back to the original - muxer.AllowConnect = true; - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); - Assert.True(subscription.IsConnected); - - var newServer = subscription.GetCurrentServer(); - Assert.NotNull(newServer); - Assert.Equal(newServer, initialServer); - Log("Now connected to: " + newServer); - } - - count = 0; - Log("Publishing (2)..."); - Assert.Equal(0, count); - publishedTo = await sub.PublishAsync(channel, "message2"); - // Client -> Redis -> Client -> handler takes just a moment - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); - Assert.Equal(1, count); - Log($" Published (2) to {publishedTo} subscriber(s)."); - - ClearAmbientFailures(); - } + count = 0; + Log("Publishing (2)..."); + Assert.Equal(0, count); + publishedTo = await sub.PublishAsync(channel, "message2"); + // Client -> Redis -> Client -> handler takes just a moment + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref count) == 1); + Assert.Equal(1, count); + Log($" Published (2) to {publishedTo} subscriber(s)."); + + ClearAmbientFailures(); } } diff --git a/tests/StackExchange.Redis.Tests/RawResultTests.cs b/tests/StackExchange.Redis.Tests/RawResultTests.cs index 69d85ff61..895ec4ec1 100644 --- a/tests/StackExchange.Redis.Tests/RawResultTests.cs +++ b/tests/StackExchange.Redis.Tests/RawResultTests.cs @@ -1,65 +1,65 @@ using System.Buffers; using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class RawResultTests { - public class RawResultTests + [Fact] + public void TypeLoads() + { + var type = typeof(RawResult); + Assert.Equal(nameof(RawResult), type.Name); + } + + [Fact] + public void NullWorks() { - [Fact] - public void TypeLoads() - { - var type = typeof(RawResult); - Assert.Equal(nameof(RawResult), type.Name); - } - [Fact] - public void NullWorks() - { - var result = new RawResult(ResultType.BulkString, ReadOnlySequence.Empty, true); - Assert.Equal(ResultType.BulkString, result.Type); - Assert.True(result.IsNull); + var result = new RawResult(ResultType.BulkString, ReadOnlySequence.Empty, true); + Assert.Equal(ResultType.BulkString, result.Type); + Assert.True(result.IsNull); - var value = result.AsRedisValue(); + var value = result.AsRedisValue(); - Assert.True(value.IsNull); - string? s = value; - Assert.Null(s); + Assert.True(value.IsNull); + string? s = value; + Assert.Null(s); - byte[]? arr = (byte[]?)value; - Assert.Null(arr); - } + byte[]? arr = (byte[]?)value; + Assert.Null(arr); + } - [Fact] - public void DefaultWorks() - { - var result = default(RawResult); - Assert.Equal(ResultType.None, result.Type); - Assert.True(result.IsNull); + [Fact] + public void DefaultWorks() + { + var result = default(RawResult); + Assert.Equal(ResultType.None, result.Type); + Assert.True(result.IsNull); - var value = result.AsRedisValue(); + var value = result.AsRedisValue(); - Assert.True(value.IsNull); - var s = (string?)value; - Assert.Null(s); + Assert.True(value.IsNull); + var s = (string?)value; + Assert.Null(s); - var arr = (byte[]?)value; - Assert.Null(arr); - } + var arr = (byte[]?)value; + Assert.Null(arr); + } - [Fact] - public void NilWorks() - { - var result = RawResult.Nil; - Assert.Equal(ResultType.None, result.Type); - Assert.True(result.IsNull); + [Fact] + public void NilWorks() + { + var result = RawResult.Nil; + Assert.Equal(ResultType.None, result.Type); + Assert.True(result.IsNull); - var value = result.AsRedisValue(); + var value = result.AsRedisValue(); - Assert.True(value.IsNull); - var s = (string?)value; - Assert.Null(s); + Assert.True(value.IsNull); + var s = (string?)value; + Assert.Null(s); - var arr = (byte[]?)value; - Assert.Null(arr); - } + var arr = (byte[]?)value; + Assert.Null(arr); } } diff --git a/tests/StackExchange.Redis.Tests/RealWorld.cs b/tests/StackExchange.Redis.Tests/RealWorld.cs index 2d6760ae4..246a64462 100644 --- a/tests/StackExchange.Redis.Tests/RealWorld.cs +++ b/tests/StackExchange.Redis.Tests/RealWorld.cs @@ -2,31 +2,30 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class RealWorld : TestBase { - public class RealWorld : TestBase + public RealWorld(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task WhyDoesThisNotWork() { - public RealWorld(ITestOutputHelper output) : base(output) { } + Log("first:"); + var config = ConfigurationOptions.Parse("localhost:6379,localhost:6380,name=Core (Q&A),tiebreaker=:RedisPrimary,abortConnect=False"); + Assert.Equal(2, config.EndPoints.Count); + Log("Endpoint 0: {0} (AddressFamily: {1})", config.EndPoints[0], config.EndPoints[0].AddressFamily); + Log("Endpoint 1: {0} (AddressFamily: {1})", config.EndPoints[1], config.EndPoints[1].AddressFamily); - [Fact] - public async Task WhyDoesThisNotWork() + using (var conn = ConnectionMultiplexer.Connect("localhost:6379,localhost:6380,name=Core (Q&A),tiebreaker=:RedisPrimary,abortConnect=False", Writer)) { - Log("first:"); - var config = ConfigurationOptions.Parse("localhost:6379,localhost:6380,name=Core (Q&A),tiebreaker=:RedisPrimary,abortConnect=False"); - Assert.Equal(2, config.EndPoints.Count); - Log("Endpoint 0: {0} (AddressFamily: {1})", config.EndPoints[0], config.EndPoints[0].AddressFamily); - Log("Endpoint 1: {0} (AddressFamily: {1})", config.EndPoints[1], config.EndPoints[1].AddressFamily); - - using (var conn = ConnectionMultiplexer.Connect("localhost:6379,localhost:6380,name=Core (Q&A),tiebreaker=:RedisPrimary,abortConnect=False", Writer)) - { - Log(""); - Log("pausing..."); - await Task.Delay(200).ForAwait(); - Log("second:"); + Log(""); + Log("pausing..."); + await Task.Delay(200).ForAwait(); + Log("second:"); - bool result = conn.Configure(Writer); - Log("Returned: {0}", result); - } + bool result = conn.Configure(Writer); + Log("Returned: {0}", result); } } } diff --git a/tests/StackExchange.Redis.Tests/RedisFeaturesTests.cs b/tests/StackExchange.Redis.Tests/RedisFeaturesTests.cs index 2bd492ab9..2cabb90b4 100644 --- a/tests/StackExchange.Redis.Tests/RedisFeaturesTests.cs +++ b/tests/StackExchange.Redis.Tests/RedisFeaturesTests.cs @@ -1,30 +1,29 @@ using System; using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class RedisFeaturesTests { - public class RedisFeaturesTests + [Fact] + public void ExecAbort() // a random one because it is fun { - [Fact] - public void ExecAbort() // a random one because it is fun - { - var features = new RedisFeatures(new Version(2, 9)); - var s = features.ToString(); - Assert.True(features.ExecAbort); - Assert.StartsWith("Features in 2.9" + Environment.NewLine, s); - Assert.Contains("ExecAbort: True" + Environment.NewLine, s); + var features = new RedisFeatures(new Version(2, 9)); + var s = features.ToString(); + Assert.True(features.ExecAbort); + Assert.StartsWith("Features in 2.9" + Environment.NewLine, s); + Assert.Contains("ExecAbort: True" + Environment.NewLine, s); - features = new RedisFeatures(new Version(2, 9, 5)); - s = features.ToString(); - Assert.False(features.ExecAbort); - Assert.StartsWith("Features in 2.9.5" + Environment.NewLine, s); - Assert.Contains("ExecAbort: False" + Environment.NewLine, s); + features = new RedisFeatures(new Version(2, 9, 5)); + s = features.ToString(); + Assert.False(features.ExecAbort); + Assert.StartsWith("Features in 2.9.5" + Environment.NewLine, s); + Assert.Contains("ExecAbort: False" + Environment.NewLine, s); - features = new RedisFeatures(new Version(3, 0)); - s = features.ToString(); - Assert.True(features.ExecAbort); - Assert.StartsWith("Features in 3.0" + Environment.NewLine, s); - Assert.Contains("ExecAbort: True" + Environment.NewLine, s); - } + features = new RedisFeatures(new Version(3, 0)); + s = features.ToString(); + Assert.True(features.ExecAbort); + Assert.StartsWith("Features in 3.0" + Environment.NewLine, s); + Assert.Contains("ExecAbort: True" + Environment.NewLine, s); } } diff --git a/tests/StackExchange.Redis.Tests/RedisResultTests.cs b/tests/StackExchange.Redis.Tests/RedisResultTests.cs index fb27c6745..27ceba755 100644 --- a/tests/StackExchange.Redis.Tests/RedisResultTests.cs +++ b/tests/StackExchange.Redis.Tests/RedisResultTests.cs @@ -2,154 +2,153 @@ using System.Collections.Generic; using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +/// +/// Tests for +/// +public sealed class RedisResultTests { /// - /// Tests for + /// Tests the basic functionality of + /// + [Fact] + public void ToDictionaryWorks() + { + var redisArrayResult = RedisResult.Create( + new RedisValue[] { "one", 1, "two", 2, "three", 3, "four", 4 }); + + var dict = redisArrayResult.ToDictionary(); + + Assert.Equal(4, dict.Count); + Assert.Equal(1, (RedisValue)dict["one"]); + Assert.Equal(2, (RedisValue)dict["two"]); + Assert.Equal(3, (RedisValue)dict["three"]); + Assert.Equal(4, (RedisValue)dict["four"]); + } + + /// + /// Tests the basic functionality of + /// when the results contain a nested results array, which is common for lua script results + /// + [Fact] + public void ToDictionaryWorksWhenNested() + { + var redisArrayResult = RedisResult.Create( + new [] + { + RedisResult.Create((RedisValue)"one"), + RedisResult.Create(new RedisValue[]{"two", 2, "three", 3}), + + RedisResult.Create((RedisValue)"four"), + RedisResult.Create(new RedisValue[] { "five", 5, "six", 6 }), + }); + + var dict = redisArrayResult.ToDictionary(); + var nestedDict = dict["one"].ToDictionary(); + + Assert.Equal(2, dict.Count); + Assert.Equal(2, nestedDict.Count); + Assert.Equal(2, (RedisValue)nestedDict["two"]); + Assert.Equal(3, (RedisValue)nestedDict["three"]); + } + + /// + /// Tests that fails when a duplicate key is encountered. + /// This also tests that the default comparator is case-insensitive. + /// + [Fact] + public void ToDictionaryFailsWithDuplicateKeys() + { + var redisArrayResult = RedisResult.Create( + new RedisValue[] { "banana", 1, "BANANA", 2, "orange", 3, "apple", 4 }); + + Assert.Throws(() => redisArrayResult.ToDictionary(/* Use default comparer, causes collision of banana */)); + } + + /// + /// Tests that correctly uses the provided comparator /// - public sealed class RedisResultTests + [Fact] + public void ToDictionaryWorksWithCustomComparator() + { + var redisArrayResult = RedisResult.Create( + new RedisValue[] { "banana", 1, "BANANA", 2, "orange", 3, "apple", 4 }); + + var dict = redisArrayResult.ToDictionary(StringComparer.Ordinal); + + Assert.Equal(4, dict.Count); + Assert.Equal(1, (RedisValue)dict["banana"]); + Assert.Equal(2, (RedisValue)dict["BANANA"]); + } + + /// + /// Tests that fails when the redis results array contains an odd number + /// of elements. In other words, it's not actually a Key,Value,Key,Value... etc. array + /// + [Fact] + public void ToDictionaryFailsOnMishapenResults() + { + var redisArrayResult = RedisResult.Create( + new RedisValue[] { "one", 1, "two", 2, "three", 3, "four" /* missing 4 */ }); + + Assert.Throws(()=>redisArrayResult.ToDictionary(StringComparer.Ordinal)); + } + + [Fact] + public void SingleResultConvertibleViaTo() + { + var value = RedisResult.Create(123); + Assert.StrictEqual((int)123, Convert.ToInt32(value)); + Assert.StrictEqual((uint)123U, Convert.ToUInt32(value)); + Assert.StrictEqual((long)123, Convert.ToInt64(value)); + Assert.StrictEqual((ulong)123U, Convert.ToUInt64(value)); + Assert.StrictEqual((byte)123, Convert.ToByte(value)); + Assert.StrictEqual((sbyte)123, Convert.ToSByte(value)); + Assert.StrictEqual((short)123, Convert.ToInt16(value)); + Assert.StrictEqual((ushort)123, Convert.ToUInt16(value)); + Assert.Equal("123", Convert.ToString(value)); + Assert.StrictEqual(123M, Convert.ToDecimal(value)); + Assert.StrictEqual((char)123, Convert.ToChar(value)); + Assert.StrictEqual(123f, Convert.ToSingle(value)); + Assert.StrictEqual(123d, Convert.ToDouble(value)); + } + + [Fact] + public void SingleResultConvertibleDirectViaChangeType_Type() + { + var value = RedisResult.Create(123); + Assert.StrictEqual((int)123, Convert.ChangeType(value, typeof(int))); + Assert.StrictEqual((uint)123U, Convert.ChangeType(value, typeof(uint))); + Assert.StrictEqual((long)123, Convert.ChangeType(value, typeof(long))); + Assert.StrictEqual((ulong)123U, Convert.ChangeType(value, typeof(ulong))); + Assert.StrictEqual((byte)123, Convert.ChangeType(value, typeof(byte))); + Assert.StrictEqual((sbyte)123, Convert.ChangeType(value, typeof(sbyte))); + Assert.StrictEqual((short)123, Convert.ChangeType(value, typeof(short))); + Assert.StrictEqual((ushort)123, Convert.ChangeType(value, typeof(ushort))); + Assert.Equal("123", Convert.ChangeType(value, typeof(string))); + Assert.StrictEqual(123M, Convert.ChangeType(value, typeof(decimal))); + Assert.StrictEqual((char)123, Convert.ChangeType(value, typeof(char))); + Assert.StrictEqual(123f, Convert.ChangeType(value, typeof(float))); + Assert.StrictEqual(123d, Convert.ChangeType(value, typeof(double))); + } + + [Fact] + public void SingleResultConvertibleDirectViaChangeType_TypeCode() { - /// - /// Tests the basic functionality of - /// - [Fact] - public void ToDictionaryWorks() - { - var redisArrayResult = RedisResult.Create( - new RedisValue[] { "one", 1, "two", 2, "three", 3, "four", 4 }); - - var dict = redisArrayResult.ToDictionary(); - - Assert.Equal(4, dict.Count); - Assert.Equal(1, (RedisValue)dict["one"]); - Assert.Equal(2, (RedisValue)dict["two"]); - Assert.Equal(3, (RedisValue)dict["three"]); - Assert.Equal(4, (RedisValue)dict["four"]); - } - - /// - /// Tests the basic functionality of - /// when the results contain a nested results array, which is common for lua script results - /// - [Fact] - public void ToDictionaryWorksWhenNested() - { - var redisArrayResult = RedisResult.Create( - new [] - { - RedisResult.Create((RedisValue)"one"), - RedisResult.Create(new RedisValue[]{"two", 2, "three", 3}), - - RedisResult.Create((RedisValue)"four"), - RedisResult.Create(new RedisValue[] { "five", 5, "six", 6 }), - }); - - var dict = redisArrayResult.ToDictionary(); - var nestedDict = dict["one"].ToDictionary(); - - Assert.Equal(2, dict.Count); - Assert.Equal(2, nestedDict.Count); - Assert.Equal(2, (RedisValue)nestedDict["two"]); - Assert.Equal(3, (RedisValue)nestedDict["three"]); - } - - /// - /// Tests that fails when a duplicate key is encountered. - /// This also tests that the default comparator is case-insensitive. - /// - [Fact] - public void ToDictionaryFailsWithDuplicateKeys() - { - var redisArrayResult = RedisResult.Create( - new RedisValue[] { "banana", 1, "BANANA", 2, "orange", 3, "apple", 4 }); - - Assert.Throws(() => redisArrayResult.ToDictionary(/* Use default comparer, causes collision of banana */)); - } - - /// - /// Tests that correctly uses the provided comparator - /// - [Fact] - public void ToDictionaryWorksWithCustomComparator() - { - var redisArrayResult = RedisResult.Create( - new RedisValue[] { "banana", 1, "BANANA", 2, "orange", 3, "apple", 4 }); - - var dict = redisArrayResult.ToDictionary(StringComparer.Ordinal); - - Assert.Equal(4, dict.Count); - Assert.Equal(1, (RedisValue)dict["banana"]); - Assert.Equal(2, (RedisValue)dict["BANANA"]); - } - - /// - /// Tests that fails when the redis results array contains an odd number - /// of elements. In other words, it's not actually a Key,Value,Key,Value... etc. array - /// - [Fact] - public void ToDictionaryFailsOnMishapenResults() - { - var redisArrayResult = RedisResult.Create( - new RedisValue[] { "one", 1, "two", 2, "three", 3, "four" /* missing 4 */ }); - - Assert.Throws(()=>redisArrayResult.ToDictionary(StringComparer.Ordinal)); - } - - [Fact] - public void SingleResultConvertibleViaTo() - { - var value = RedisResult.Create(123); - Assert.StrictEqual((int)123, Convert.ToInt32(value)); - Assert.StrictEqual((uint)123U, Convert.ToUInt32(value)); - Assert.StrictEqual((long)123, Convert.ToInt64(value)); - Assert.StrictEqual((ulong)123U, Convert.ToUInt64(value)); - Assert.StrictEqual((byte)123, Convert.ToByte(value)); - Assert.StrictEqual((sbyte)123, Convert.ToSByte(value)); - Assert.StrictEqual((short)123, Convert.ToInt16(value)); - Assert.StrictEqual((ushort)123, Convert.ToUInt16(value)); - Assert.Equal("123", Convert.ToString(value)); - Assert.StrictEqual(123M, Convert.ToDecimal(value)); - Assert.StrictEqual((char)123, Convert.ToChar(value)); - Assert.StrictEqual(123f, Convert.ToSingle(value)); - Assert.StrictEqual(123d, Convert.ToDouble(value)); - } - - [Fact] - public void SingleResultConvertibleDirectViaChangeType_Type() - { - var value = RedisResult.Create(123); - Assert.StrictEqual((int)123, Convert.ChangeType(value, typeof(int))); - Assert.StrictEqual((uint)123U, Convert.ChangeType(value, typeof(uint))); - Assert.StrictEqual((long)123, Convert.ChangeType(value, typeof(long))); - Assert.StrictEqual((ulong)123U, Convert.ChangeType(value, typeof(ulong))); - Assert.StrictEqual((byte)123, Convert.ChangeType(value, typeof(byte))); - Assert.StrictEqual((sbyte)123, Convert.ChangeType(value, typeof(sbyte))); - Assert.StrictEqual((short)123, Convert.ChangeType(value, typeof(short))); - Assert.StrictEqual((ushort)123, Convert.ChangeType(value, typeof(ushort))); - Assert.Equal("123", Convert.ChangeType(value, typeof(string))); - Assert.StrictEqual(123M, Convert.ChangeType(value, typeof(decimal))); - Assert.StrictEqual((char)123, Convert.ChangeType(value, typeof(char))); - Assert.StrictEqual(123f, Convert.ChangeType(value, typeof(float))); - Assert.StrictEqual(123d, Convert.ChangeType(value, typeof(double))); - } - - [Fact] - public void SingleResultConvertibleDirectViaChangeType_TypeCode() - { - var value = RedisResult.Create(123); - Assert.StrictEqual((int)123, Convert.ChangeType(value, TypeCode.Int32)); - Assert.StrictEqual((uint)123U, Convert.ChangeType(value, TypeCode.UInt32)); - Assert.StrictEqual((long)123, Convert.ChangeType(value, TypeCode.Int64)); - Assert.StrictEqual((ulong)123U, Convert.ChangeType(value, TypeCode.UInt64)); - Assert.StrictEqual((byte)123, Convert.ChangeType(value, TypeCode.Byte)); - Assert.StrictEqual((sbyte)123, Convert.ChangeType(value, TypeCode.SByte)); - Assert.StrictEqual((short)123, Convert.ChangeType(value, TypeCode.Int16)); - Assert.StrictEqual((ushort)123, Convert.ChangeType(value, TypeCode.UInt16)); - Assert.Equal("123", Convert.ChangeType(value, TypeCode.String)); - Assert.StrictEqual(123M, Convert.ChangeType(value, TypeCode.Decimal)); - Assert.StrictEqual((char)123, Convert.ChangeType(value, TypeCode.Char)); - Assert.StrictEqual(123f, Convert.ChangeType(value, TypeCode.Single)); - Assert.StrictEqual(123d, Convert.ChangeType(value, TypeCode.Double)); - } + var value = RedisResult.Create(123); + Assert.StrictEqual((int)123, Convert.ChangeType(value, TypeCode.Int32)); + Assert.StrictEqual((uint)123U, Convert.ChangeType(value, TypeCode.UInt32)); + Assert.StrictEqual((long)123, Convert.ChangeType(value, TypeCode.Int64)); + Assert.StrictEqual((ulong)123U, Convert.ChangeType(value, TypeCode.UInt64)); + Assert.StrictEqual((byte)123, Convert.ChangeType(value, TypeCode.Byte)); + Assert.StrictEqual((sbyte)123, Convert.ChangeType(value, TypeCode.SByte)); + Assert.StrictEqual((short)123, Convert.ChangeType(value, TypeCode.Int16)); + Assert.StrictEqual((ushort)123, Convert.ChangeType(value, TypeCode.UInt16)); + Assert.Equal("123", Convert.ChangeType(value, TypeCode.String)); + Assert.StrictEqual(123M, Convert.ChangeType(value, TypeCode.Decimal)); + Assert.StrictEqual((char)123, Convert.ChangeType(value, TypeCode.Char)); + Assert.StrictEqual(123f, Convert.ChangeType(value, TypeCode.Single)); + Assert.StrictEqual(123d, Convert.ChangeType(value, TypeCode.Double)); } } diff --git a/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs b/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs index 87e503883..195c971ad 100644 --- a/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs +++ b/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs @@ -2,281 +2,280 @@ using System.Text; using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class RedisValueEquivalency { - public class RedisValueEquivalency - { - // internal storage types: null, integer, double, string, raw - // public perceived types: int, long, double, bool, memory / byte[] + // internal storage types: null, integer, double, string, raw + // public perceived types: int, long, double, bool, memory / byte[] - [Fact] - public void Int32_Matrix() + [Fact] + public void Int32_Matrix() + { + static void Check(RedisValue known, RedisValue test) { - static void Check(RedisValue known, RedisValue test) + KeysAndValues.CheckSame(known, test); + if (known.IsNull) + { + Assert.True(test.IsNull); + Assert.False(((int?)test).HasValue); + } + else { - KeysAndValues.CheckSame(known, test); - if (known.IsNull) - { - Assert.True(test.IsNull); - Assert.False(((int?)test).HasValue); - } - else - { - Assert.False(test.IsNull); - Assert.Equal((int)known, ((int?)test)!.Value); - Assert.Equal((int)known, (int)test); - } + Assert.False(test.IsNull); + Assert.Equal((int)known, ((int?)test)!.Value); Assert.Equal((int)known, (int)test); } - Check(42, 42); - Check(42, 42.0); - Check(42, "42"); - Check(42, "42.0"); - Check(42, Bytes("42")); - Check(42, Bytes("42.0")); - CheckString(42, "42"); - - Check(-42, -42); - Check(-42, -42.0); - Check(-42, "-42"); - Check(-42, "-42.0"); - Check(-42, Bytes("-42")); - Check(-42, Bytes("-42.0")); - CheckString(-42, "-42"); - - Check(1, true); - Check(0, false); + Assert.Equal((int)known, (int)test); } + Check(42, 42); + Check(42, 42.0); + Check(42, "42"); + Check(42, "42.0"); + Check(42, Bytes("42")); + Check(42, Bytes("42.0")); + CheckString(42, "42"); + + Check(-42, -42); + Check(-42, -42.0); + Check(-42, "-42"); + Check(-42, "-42.0"); + Check(-42, Bytes("-42")); + Check(-42, Bytes("-42.0")); + CheckString(-42, "-42"); + + Check(1, true); + Check(0, false); + } - [Fact] - public void Int64_Matrix() + [Fact] + public void Int64_Matrix() + { + static void Check(RedisValue known, RedisValue test) { - static void Check(RedisValue known, RedisValue test) + KeysAndValues.CheckSame(known, test); + if (known.IsNull) + { + Assert.True(test.IsNull); + Assert.False(((long?)test).HasValue); + } + else { - KeysAndValues.CheckSame(known, test); - if (known.IsNull) - { - Assert.True(test.IsNull); - Assert.False(((long?)test).HasValue); - } - else - { - Assert.False(test.IsNull); - Assert.Equal((long)known, ((long?)test!).Value); - Assert.Equal((long)known, (long)test); - } + Assert.False(test.IsNull); + Assert.Equal((long)known, ((long?)test!).Value); Assert.Equal((long)known, (long)test); } - Check(1099511627848, 1099511627848); - Check(1099511627848, 1099511627848.0); - Check(1099511627848, "1099511627848"); - Check(1099511627848, "1099511627848.0"); - Check(1099511627848, Bytes("1099511627848")); - Check(1099511627848, Bytes("1099511627848.0")); - CheckString(1099511627848, "1099511627848"); - - Check(-1099511627848, -1099511627848); - Check(-1099511627848, -1099511627848); - Check(-1099511627848, "-1099511627848"); - Check(-1099511627848, "-1099511627848.0"); - Check(-1099511627848, Bytes("-1099511627848")); - Check(-1099511627848, Bytes("-1099511627848.0")); - CheckString(-1099511627848, "-1099511627848"); - - Check(1L, true); - Check(0L, false); + Assert.Equal((long)known, (long)test); } + Check(1099511627848, 1099511627848); + Check(1099511627848, 1099511627848.0); + Check(1099511627848, "1099511627848"); + Check(1099511627848, "1099511627848.0"); + Check(1099511627848, Bytes("1099511627848")); + Check(1099511627848, Bytes("1099511627848.0")); + CheckString(1099511627848, "1099511627848"); + + Check(-1099511627848, -1099511627848); + Check(-1099511627848, -1099511627848); + Check(-1099511627848, "-1099511627848"); + Check(-1099511627848, "-1099511627848.0"); + Check(-1099511627848, Bytes("-1099511627848")); + Check(-1099511627848, Bytes("-1099511627848.0")); + CheckString(-1099511627848, "-1099511627848"); + + Check(1L, true); + Check(0L, false); + } - [Fact] - public void Double_Matrix() + [Fact] + public void Double_Matrix() + { + static void Check(RedisValue known, RedisValue test) { - static void Check(RedisValue known, RedisValue test) + KeysAndValues.CheckSame(known, test); + if (known.IsNull) + { + Assert.True(test.IsNull); + Assert.False(((double?)test).HasValue); + } + else { - KeysAndValues.CheckSame(known, test); - if (known.IsNull) - { - Assert.True(test.IsNull); - Assert.False(((double?)test).HasValue); - } - else - { - Assert.False(test.IsNull); - Assert.Equal((double)known, ((double?)test)!.Value); - Assert.Equal((double)known, (double)test); - } + Assert.False(test.IsNull); + Assert.Equal((double)known, ((double?)test)!.Value); Assert.Equal((double)known, (double)test); } - Check(1099511627848.0, 1099511627848); - Check(1099511627848.0, 1099511627848.0); - Check(1099511627848.0, "1099511627848"); - Check(1099511627848.0, "1099511627848.0"); - Check(1099511627848.0, Bytes("1099511627848")); - Check(1099511627848.0, Bytes("1099511627848.0")); - CheckString(1099511627848.0, "1099511627848"); - - Check(-1099511627848.0, -1099511627848); - Check(-1099511627848.0, -1099511627848); - Check(-1099511627848.0, "-1099511627848"); - Check(-1099511627848.0, "-1099511627848.0"); - Check(-1099511627848.0, Bytes("-1099511627848")); - Check(-1099511627848.0, Bytes("-1099511627848.0")); - CheckString(-1099511627848.0, "-1099511627848"); - - Check(1.0, true); - Check(0.0, false); - - Check(1099511627848.6001, 1099511627848.6001); - Check(1099511627848.6001, "1099511627848.6001"); - Check(1099511627848.6001, Bytes("1099511627848.6001")); - CheckString(1099511627848.6001, "1099511627848.6001"); - - Check(-1099511627848.6001, -1099511627848.6001); - Check(-1099511627848.6001, "-1099511627848.6001"); - Check(-1099511627848.6001, Bytes("-1099511627848.6001")); - CheckString(-1099511627848.6001, "-1099511627848.6001"); - - Check(double.NegativeInfinity, double.NegativeInfinity); - Check(double.NegativeInfinity, "-inf"); - CheckString(double.NegativeInfinity, "-inf"); - - Check(double.PositiveInfinity, double.PositiveInfinity); - Check(double.PositiveInfinity, "+inf"); - CheckString(double.PositiveInfinity, "+inf"); + Assert.Equal((double)known, (double)test); } + Check(1099511627848.0, 1099511627848); + Check(1099511627848.0, 1099511627848.0); + Check(1099511627848.0, "1099511627848"); + Check(1099511627848.0, "1099511627848.0"); + Check(1099511627848.0, Bytes("1099511627848")); + Check(1099511627848.0, Bytes("1099511627848.0")); + CheckString(1099511627848.0, "1099511627848"); + + Check(-1099511627848.0, -1099511627848); + Check(-1099511627848.0, -1099511627848); + Check(-1099511627848.0, "-1099511627848"); + Check(-1099511627848.0, "-1099511627848.0"); + Check(-1099511627848.0, Bytes("-1099511627848")); + Check(-1099511627848.0, Bytes("-1099511627848.0")); + CheckString(-1099511627848.0, "-1099511627848"); + + Check(1.0, true); + Check(0.0, false); + + Check(1099511627848.6001, 1099511627848.6001); + Check(1099511627848.6001, "1099511627848.6001"); + Check(1099511627848.6001, Bytes("1099511627848.6001")); + CheckString(1099511627848.6001, "1099511627848.6001"); + + Check(-1099511627848.6001, -1099511627848.6001); + Check(-1099511627848.6001, "-1099511627848.6001"); + Check(-1099511627848.6001, Bytes("-1099511627848.6001")); + CheckString(-1099511627848.6001, "-1099511627848.6001"); + + Check(double.NegativeInfinity, double.NegativeInfinity); + Check(double.NegativeInfinity, "-inf"); + CheckString(double.NegativeInfinity, "-inf"); + + Check(double.PositiveInfinity, double.PositiveInfinity); + Check(double.PositiveInfinity, "+inf"); + CheckString(double.PositiveInfinity, "+inf"); + } - private static void CheckString(RedisValue value, string expected) - { - var s = value.ToString(); - Assert.True(s == expected, $"'{s}' vs '{expected}'"); - } + private static void CheckString(RedisValue value, string expected) + { + var s = value.ToString(); + Assert.True(s == expected, $"'{s}' vs '{expected}'"); + } - private static byte[]? Bytes(string? s) => s == null ? null : Encoding.UTF8.GetBytes(s); + private static byte[]? Bytes(string? s) => s == null ? null : Encoding.UTF8.GetBytes(s); - private static string LineNumber([CallerLineNumber] int lineNumber = 0) => lineNumber.ToString(); + private static string LineNumber([CallerLineNumber] int lineNumber = 0) => lineNumber.ToString(); - [Fact] - public void RedisValueStartsWith() - { - // test strings - RedisValue x = "abc"; - Assert.True(x.StartsWith("a"), LineNumber()); - Assert.True(x.StartsWith("ab"), LineNumber()); - Assert.True(x.StartsWith("abc"), LineNumber()); - Assert.False(x.StartsWith("abd"), LineNumber()); - Assert.False(x.StartsWith("abcd"), LineNumber()); - Assert.False(x.StartsWith(123), LineNumber()); - Assert.False(x.StartsWith(false), LineNumber()); - - // test binary - x = Encoding.ASCII.GetBytes("abc"); - Assert.True(x.StartsWith("a"), LineNumber()); - Assert.True(x.StartsWith("ab"), LineNumber()); - Assert.True(x.StartsWith("abc"), LineNumber()); - Assert.False(x.StartsWith("abd"), LineNumber()); - Assert.False(x.StartsWith("abcd"), LineNumber()); - Assert.False(x.StartsWith(123), LineNumber()); - Assert.False(x.StartsWith(false), LineNumber()); - - Assert.True(x.StartsWith(Encoding.ASCII.GetBytes("a")), LineNumber()); - Assert.True(x.StartsWith(Encoding.ASCII.GetBytes("ab")), LineNumber()); - Assert.True(x.StartsWith(Encoding.ASCII.GetBytes("abc")), LineNumber()); - Assert.False(x.StartsWith(Encoding.ASCII.GetBytes("abd")), LineNumber()); - Assert.False(x.StartsWith(Encoding.ASCII.GetBytes("abcd")), LineNumber()); - - x = 10; // integers are effectively strings in this context - Assert.True(x.StartsWith(1), LineNumber()); - Assert.True(x.StartsWith(10), LineNumber()); - Assert.False(x.StartsWith(100), LineNumber()); - } + [Fact] + public void RedisValueStartsWith() + { + // test strings + RedisValue x = "abc"; + Assert.True(x.StartsWith("a"), LineNumber()); + Assert.True(x.StartsWith("ab"), LineNumber()); + Assert.True(x.StartsWith("abc"), LineNumber()); + Assert.False(x.StartsWith("abd"), LineNumber()); + Assert.False(x.StartsWith("abcd"), LineNumber()); + Assert.False(x.StartsWith(123), LineNumber()); + Assert.False(x.StartsWith(false), LineNumber()); + + // test binary + x = Encoding.ASCII.GetBytes("abc"); + Assert.True(x.StartsWith("a"), LineNumber()); + Assert.True(x.StartsWith("ab"), LineNumber()); + Assert.True(x.StartsWith("abc"), LineNumber()); + Assert.False(x.StartsWith("abd"), LineNumber()); + Assert.False(x.StartsWith("abcd"), LineNumber()); + Assert.False(x.StartsWith(123), LineNumber()); + Assert.False(x.StartsWith(false), LineNumber()); + + Assert.True(x.StartsWith(Encoding.ASCII.GetBytes("a")), LineNumber()); + Assert.True(x.StartsWith(Encoding.ASCII.GetBytes("ab")), LineNumber()); + Assert.True(x.StartsWith(Encoding.ASCII.GetBytes("abc")), LineNumber()); + Assert.False(x.StartsWith(Encoding.ASCII.GetBytes("abd")), LineNumber()); + Assert.False(x.StartsWith(Encoding.ASCII.GetBytes("abcd")), LineNumber()); + + x = 10; // integers are effectively strings in this context + Assert.True(x.StartsWith(1), LineNumber()); + Assert.True(x.StartsWith(10), LineNumber()); + Assert.False(x.StartsWith(100), LineNumber()); + } - [Fact] - public void TryParseInt64() - { - Assert.True(((RedisValue)123).TryParse(out long l)); - Assert.Equal(123, l); + [Fact] + public void TryParseInt64() + { + Assert.True(((RedisValue)123).TryParse(out long l)); + Assert.Equal(123, l); - Assert.True(((RedisValue)123.0).TryParse(out l)); - Assert.Equal(123, l); + Assert.True(((RedisValue)123.0).TryParse(out l)); + Assert.Equal(123, l); - Assert.True(((RedisValue)(int.MaxValue + 123L)).TryParse(out l)); - Assert.Equal(int.MaxValue + 123L, l); + Assert.True(((RedisValue)(int.MaxValue + 123L)).TryParse(out l)); + Assert.Equal(int.MaxValue + 123L, l); - Assert.True(((RedisValue)"123").TryParse(out l)); - Assert.Equal(123, l); + Assert.True(((RedisValue)"123").TryParse(out l)); + Assert.Equal(123, l); - Assert.True(((RedisValue)(-123)).TryParse(out l)); - Assert.Equal(-123, l); + Assert.True(((RedisValue)(-123)).TryParse(out l)); + Assert.Equal(-123, l); - Assert.True(default(RedisValue).TryParse(out l)); - Assert.Equal(0, l); + Assert.True(default(RedisValue).TryParse(out l)); + Assert.Equal(0, l); - Assert.True(((RedisValue)123.0).TryParse(out l)); - Assert.Equal(123, l); + Assert.True(((RedisValue)123.0).TryParse(out l)); + Assert.Equal(123, l); - Assert.False(((RedisValue)"abc").TryParse(out long _)); - Assert.False(((RedisValue)"123.1").TryParse(out long _)); - Assert.False(((RedisValue)123.1).TryParse(out long _)); - } + Assert.False(((RedisValue)"abc").TryParse(out long _)); + Assert.False(((RedisValue)"123.1").TryParse(out long _)); + Assert.False(((RedisValue)123.1).TryParse(out long _)); + } - [Fact] - public void TryParseInt32() - { - Assert.True(((RedisValue)123).TryParse(out int i)); - Assert.Equal(123, i); + [Fact] + public void TryParseInt32() + { + Assert.True(((RedisValue)123).TryParse(out int i)); + Assert.Equal(123, i); - Assert.True(((RedisValue)123.0).TryParse(out i)); - Assert.Equal(123, i); + Assert.True(((RedisValue)123.0).TryParse(out i)); + Assert.Equal(123, i); - Assert.False(((RedisValue)(int.MaxValue + 123L)).TryParse(out int _)); + Assert.False(((RedisValue)(int.MaxValue + 123L)).TryParse(out int _)); - Assert.True(((RedisValue)"123").TryParse(out i)); - Assert.Equal(123, i); + Assert.True(((RedisValue)"123").TryParse(out i)); + Assert.Equal(123, i); - Assert.True(((RedisValue)(-123)).TryParse(out i)); - Assert.Equal(-123, i); + Assert.True(((RedisValue)(-123)).TryParse(out i)); + Assert.Equal(-123, i); - Assert.True(default(RedisValue).TryParse(out i)); - Assert.Equal(0, i); + Assert.True(default(RedisValue).TryParse(out i)); + Assert.Equal(0, i); - Assert.True(((RedisValue)123.0).TryParse(out i)); - Assert.Equal(123, i); + Assert.True(((RedisValue)123.0).TryParse(out i)); + Assert.Equal(123, i); - Assert.False(((RedisValue)"abc").TryParse(out int _)); - Assert.False(((RedisValue)"123.1").TryParse(out int _)); - Assert.False(((RedisValue)123.1).TryParse(out int _)); - } + Assert.False(((RedisValue)"abc").TryParse(out int _)); + Assert.False(((RedisValue)"123.1").TryParse(out int _)); + Assert.False(((RedisValue)123.1).TryParse(out int _)); + } - [Fact] - public void TryParseDouble() - { - Assert.True(((RedisValue)123).TryParse(out double d)); - Assert.Equal(123, d); + [Fact] + public void TryParseDouble() + { + Assert.True(((RedisValue)123).TryParse(out double d)); + Assert.Equal(123, d); - Assert.True(((RedisValue)123.0).TryParse(out d)); - Assert.Equal(123.0, d); + Assert.True(((RedisValue)123.0).TryParse(out d)); + Assert.Equal(123.0, d); - Assert.True(((RedisValue)123.1).TryParse(out d)); - Assert.Equal(123.1, d); + Assert.True(((RedisValue)123.1).TryParse(out d)); + Assert.Equal(123.1, d); - Assert.True(((RedisValue)(int.MaxValue + 123L)).TryParse(out d)); - Assert.Equal(int.MaxValue + 123L, d); + Assert.True(((RedisValue)(int.MaxValue + 123L)).TryParse(out d)); + Assert.Equal(int.MaxValue + 123L, d); - Assert.True(((RedisValue)"123").TryParse(out d)); - Assert.Equal(123.0, d); + Assert.True(((RedisValue)"123").TryParse(out d)); + Assert.Equal(123.0, d); - Assert.True(((RedisValue)(-123)).TryParse(out d)); - Assert.Equal(-123.0, d); + Assert.True(((RedisValue)(-123)).TryParse(out d)); + Assert.Equal(-123.0, d); - Assert.True(default(RedisValue).TryParse(out d)); - Assert.Equal(0.0, d); + Assert.True(default(RedisValue).TryParse(out d)); + Assert.Equal(0.0, d); - Assert.True(((RedisValue)123.0).TryParse(out d)); - Assert.Equal(123.0, d); + Assert.True(((RedisValue)123.0).TryParse(out d)); + Assert.Equal(123.0, d); - Assert.True(((RedisValue)"123.1").TryParse(out d)); - Assert.Equal(123.1, d); + Assert.True(((RedisValue)"123.1").TryParse(out d)); + Assert.Equal(123.1, d); - Assert.False(((RedisValue)"abc").TryParse(out double _)); - } + Assert.False(((RedisValue)"abc").TryParse(out double _)); } } diff --git a/tests/StackExchange.Redis.Tests/Roles.cs b/tests/StackExchange.Redis.Tests/Roles.cs index 21cee3a8d..09e88ce77 100644 --- a/tests/StackExchange.Redis.Tests/Roles.cs +++ b/tests/StackExchange.Redis.Tests/Roles.cs @@ -1,50 +1,43 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; +using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Roles : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Roles : TestBase - { - public Roles(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public Roles(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void PrimaryRole(bool allowAdmin) // should work with or without admin now - { - using var muxer = Create(allowAdmin: allowAdmin); - var server = muxer.GetServer(TestConfig.Current.PrimaryServerAndPort); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void PrimaryRole(bool allowAdmin) // should work with or without admin now + { + using var conn = Create(allowAdmin: allowAdmin); + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); - var role = server.Role(); - Assert.NotNull(role); - Assert.Equal(role.Value, RedisLiterals.master); - var primary = role as Role.Master; - Assert.NotNull(primary); - Assert.NotNull(primary.Replicas); - Assert.Contains(primary.Replicas, r => - r.Ip == TestConfig.Current.ReplicaServer && - r.Port == TestConfig.Current.ReplicaPort); - } + var role = server.Role(); + Assert.NotNull(role); + Assert.Equal(role.Value, RedisLiterals.master); + var primary = role as Role.Master; + Assert.NotNull(primary); + Assert.NotNull(primary.Replicas); + Assert.Contains(primary.Replicas, r => + r.Ip == TestConfig.Current.ReplicaServer && + r.Port == TestConfig.Current.ReplicaPort); + } - [Fact] - public void ReplicaRole() - { - var connString = $"{TestConfig.Current.ReplicaServerAndPort},allowAdmin=true"; - using var muxer = ConnectionMultiplexer.Connect(connString); - var server = muxer.GetServer(TestConfig.Current.ReplicaServerAndPort); + [Fact] + public void ReplicaRole() + { + using var conn = ConnectionMultiplexer.Connect($"{TestConfig.Current.ReplicaServerAndPort},allowAdmin=true"); + var server = conn.GetServer(TestConfig.Current.ReplicaServerAndPort); - var role = server.Role(); - Assert.NotNull(role); - var replica = role as Role.Replica; - Assert.NotNull(replica); - Assert.Equal(replica.MasterIp, TestConfig.Current.PrimaryServer); - Assert.Equal(replica.MasterPort, TestConfig.Current.PrimaryPort); - } + var role = server.Role(); + Assert.NotNull(role); + var replica = role as Role.Replica; + Assert.NotNull(replica); + Assert.Equal(replica.MasterIp, TestConfig.Current.PrimaryServer); + Assert.Equal(replica.MasterPort, TestConfig.Current.PrimaryPort); } } diff --git a/tests/StackExchange.Redis.Tests/SSDB.cs b/tests/StackExchange.Redis.Tests/SSDB.cs index 23bdd684d..982d61244 100644 --- a/tests/StackExchange.Redis.Tests/SSDB.cs +++ b/tests/StackExchange.Redis.Tests/SSDB.cs @@ -1,31 +1,28 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class SSDB : TestBase { - public class SSDB : TestBase + public SSDB(ITestOutputHelper output) : base (output) { } + + [Fact] + public void ConnectToSSDB() { - public SSDB(ITestOutputHelper output) : base (output) { } + Skip.IfNoConfig(nameof(TestConfig.Config.SSDBServer), TestConfig.Current.SSDBServer); - [Fact] - public void ConnectToSSDB() + using var conn = ConnectionMultiplexer.Connect(new ConfigurationOptions { - Skip.IfNoConfig(nameof(TestConfig.Config.SSDBServer), TestConfig.Current.SSDBServer); + EndPoints = { { TestConfig.Current.SSDBServer, TestConfig.Current.SSDBPort } }, + CommandMap = CommandMap.SSDB + }); - var config = new ConfigurationOptions - { - EndPoints = { { TestConfig.Current.SSDBServer, TestConfig.Current.SSDBPort } }, - CommandMap = CommandMap.SSDB - }; - RedisKey key = Me(); - using (var conn = ConnectionMultiplexer.Connect(config)) - { - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - Assert.True(db.StringGet(key).IsNull); - db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); - Assert.Equal("abc", db.StringGet(key)); - } - } + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + Assert.True(db.StringGet(key).IsNull); + db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); + Assert.Equal("abc", db.StringGet(key)); } } diff --git a/tests/StackExchange.Redis.Tests/SSL.cs b/tests/StackExchange.Redis.Tests/SSL.cs index c2fe69b14..ad09b3fe0 100644 --- a/tests/StackExchange.Redis.Tests/SSL.cs +++ b/tests/StackExchange.Redis.Tests/SSL.cs @@ -14,180 +14,221 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class SSL : TestBase { - public class SSL : TestBase + public SSL(ITestOutputHelper output) : base(output) { } + + [Theory] + [InlineData(null, true)] // auto-infer port (but specify 6380) + [InlineData(6380, true)] // all explicit + // (note the 6379 port is closed) + public void ConnectToAzure(int? port, bool ssl) { - public SSL(ITestOutputHelper output) : base (output) { } + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); - [Theory] - [InlineData(null, true)] // auto-infer port (but specify 6380) - [InlineData(6380, true)] // all explicit - // (note the 6379 port is closed) - public void ConnectToAzure(int? port, bool ssl) + var options = new ConfigurationOptions(); + options.CertificateValidation += ShowCertFailures(Writer); + if (port == null) + { + options.EndPoints.Add(TestConfig.Current.AzureCacheServer); + } + else + { + options.EndPoints.Add(TestConfig.Current.AzureCacheServer, port.Value); + } + options.Ssl = ssl; + options.Password = TestConfig.Current.AzureCachePassword; + Log(options.ToString()); + using (var connection = ConnectionMultiplexer.Connect(options)) { - Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); - Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); + var ttl = connection.GetDatabase().Ping(); + Log(ttl.ToString()); + } + } - var options = new ConfigurationOptions(); - options.CertificateValidation += ShowCertFailures(Writer); - if (port == null) + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task ConnectToSSLServer(bool useSsl, bool specifyHost) + { + var server = TestConfig.Current.SslServer; + int? port = TestConfig.Current.SslPort; + string? password = ""; + bool isAzure = false; + if (string.IsNullOrWhiteSpace(server) && useSsl) + { + // we can bounce it past azure instead? + server = TestConfig.Current.AzureCacheServer; + password = TestConfig.Current.AzureCachePassword; + port = null; + isAzure = true; + } + Skip.IfNoConfig(nameof(TestConfig.Config.SslServer), server); + + var config = new ConfigurationOptions + { + AllowAdmin = true, + SyncTimeout = Debugger.IsAttached ? int.MaxValue : 5000, + Password = password, + }; + var map = new Dictionary + { + ["config"] = null // don't rely on config working + }; + if (!isAzure) map["cluster"] = null; + config.CommandMap = CommandMap.Create(map); + if (port != null) config.EndPoints.Add(server, port.Value); + else config.EndPoints.Add(server); + + if (useSsl) + { + config.Ssl = useSsl; + if (specifyHost) { - options.EndPoints.Add(TestConfig.Current.AzureCacheServer); + config.SslHost = server; } - else + config.CertificateValidation += (sender, cert, chain, errors) => + { + Log("errors: " + errors); + Log("cert issued to: " + cert?.Subject); + return true; // fingers in ears, pretend we don't know this is wrong + }; + } + + var configString = config.ToString(); + Log("config: " + configString); + var clone = ConfigurationOptions.Parse(configString); + Assert.Equal(configString, clone.ToString()); + + using var log = new StringWriter(); + using var conn = ConnectionMultiplexer.Connect(config, log); + + Log("Connect log:"); + lock (log) + { + Log(log.ToString()); + } + Log("===="); + conn.ConnectionFailed += OnConnectionFailed; + conn.InternalError += OnInternalError; + var db = conn.GetDatabase(); + await db.PingAsync().ForAwait(); + using (var file = File.Create("ssl-" + useSsl + "-" + specifyHost + ".zip")) + { + conn.ExportConfiguration(file); + } + RedisKey key = "SE.Redis"; + + const int AsyncLoop = 2000; + // perf; async + await db.KeyDeleteAsync(key).ForAwait(); + var watch = Stopwatch.StartNew(); + for (int i = 0; i < AsyncLoop; i++) + { + try { - options.EndPoints.Add(TestConfig.Current.AzureCacheServer, port.Value); + await db.StringIncrementAsync(key, flags: CommandFlags.FireAndForget).ForAwait(); } - options.Ssl = ssl; - options.Password = TestConfig.Current.AzureCachePassword; - Log(options.ToString()); - using (var connection = ConnectionMultiplexer.Connect(options)) + catch (Exception ex) { - var ttl = connection.GetDatabase().Ping(); - Log(ttl.ToString()); + Log($"Failure on i={i}: {ex.Message}"); + throw; } } + // need to do this inside the timer to measure the TTLB + long value = (long)await db.StringGetAsync(key).ForAwait(); + watch.Stop(); + Assert.Equal(AsyncLoop, value); + Log("F&F: {0} INCR, {1:###,##0}ms, {2} ops/s; final value: {3}", + AsyncLoop, + watch.ElapsedMilliseconds, + (long)(AsyncLoop / watch.Elapsed.TotalSeconds), + value); + + // perf: sync/multi-threaded + // TestConcurrent(db, key, 30, 10); + //TestConcurrent(db, key, 30, 20); + //TestConcurrent(db, key, 30, 30); + //TestConcurrent(db, key, 30, 40); + //TestConcurrent(db, key, 30, 50); + } + + [Fact] + public void RedisLabsSSL() + { + Skip.IfNoConfig(nameof(TestConfig.Config.RedisLabsSslServer), TestConfig.Current.RedisLabsSslServer); + Skip.IfNoConfig(nameof(TestConfig.Config.RedisLabsPfxPath), TestConfig.Current.RedisLabsPfxPath); + + var cert = new X509Certificate2(TestConfig.Current.RedisLabsPfxPath, ""); + Assert.NotNull(cert); + Writer.WriteLine("Thumbprint: " + cert.Thumbprint); - [Theory] - [InlineData(false, false)] - [InlineData(true, false)] - [InlineData(true, true)] - public async Task ConnectToSSLServer(bool useSsl, bool specifyHost) + int timeout = 5000; + if (Debugger.IsAttached) timeout *= 100; + var options = new ConfigurationOptions { - var server = TestConfig.Current.SslServer; - int? port = TestConfig.Current.SslPort; - string? password = ""; - bool isAzure = false; - if (string.IsNullOrWhiteSpace(server) && useSsl) - { - // we can bounce it past azure instead? - server = TestConfig.Current.AzureCacheServer; - password = TestConfig.Current.AzureCachePassword; - port = null; - isAzure = true; - } - Skip.IfNoConfig(nameof(TestConfig.Config.SslServer), server); + EndPoints = { { TestConfig.Current.RedisLabsSslServer, TestConfig.Current.RedisLabsSslPort } }, + ConnectTimeout = timeout, + AllowAdmin = true, + CommandMap = CommandMap.Create(new HashSet { + "subscribe", "unsubscribe", "cluster" + }, false) + }; - var config = new ConfigurationOptions - { - AllowAdmin = true, - SyncTimeout = Debugger.IsAttached ? int.MaxValue : 5000, - Password = password, - }; - var map = new Dictionary - { - ["config"] = null // don't rely on config working - }; - if (!isAzure) map["cluster"] = null; - config.CommandMap = CommandMap.Create(map); - if (port != null) config.EndPoints.Add(server, port.Value); - else config.EndPoints.Add(server); + options.TrustIssuer("redislabs_ca.pem"); - if (useSsl) - { - config.Ssl = useSsl; - if (specifyHost) - { - config.SslHost = server; - } - config.CertificateValidation += (sender, cert, chain, errors) => - { - Log("errors: " + errors); - Log("cert issued to: " + cert?.Subject); - return true; // fingers in ears, pretend we don't know this is wrong - }; - } + if (!Directory.Exists(Me())) Directory.CreateDirectory(Me()); +#if LOGOUTPUT + ConnectionMultiplexer.EchoPath = Me(); +#endif + options.Ssl = true; + options.CertificateSelection += delegate + { + return cert; + }; - var configString = config.ToString(); - Log("config: " + configString); - var clone = ConfigurationOptions.Parse(configString); - Assert.Equal(configString, clone.ToString()); + using var conn = ConnectionMultiplexer.Connect(options); - using (var log = new StringWriter()) - using (var muxer = ConnectionMultiplexer.Connect(config, log)) - { - Log("Connect log:"); - lock (log) - { - Log(log.ToString()); - } - Log("===="); - muxer.ConnectionFailed += OnConnectionFailed; - muxer.InternalError += OnInternalError; - var db = muxer.GetDatabase(); - await db.PingAsync().ForAwait(); - using (var file = File.Create("ssl-" + useSsl + "-" + specifyHost + ".zip")) - { - muxer.ExportConfiguration(file); - } - RedisKey key = "SE.Redis"; + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + string? s = db.StringGet(key); + Assert.Null(s); + db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); + s = db.StringGet(key); + Assert.Equal("abc", s); - const int AsyncLoop = 2000; - // perf; async - await db.KeyDeleteAsync(key).ForAwait(); - var watch = Stopwatch.StartNew(); - for (int i = 0; i < AsyncLoop; i++) - { - try - { - await db.StringIncrementAsync(key, flags: CommandFlags.FireAndForget).ForAwait(); - } - catch (Exception ex) - { - Log($"Failure on i={i}: {ex.Message}"); - throw; - } - } - // need to do this inside the timer to measure the TTLB - long value = (long)await db.StringGetAsync(key).ForAwait(); - watch.Stop(); - Assert.Equal(AsyncLoop, value); - Log("F&F: {0} INCR, {1:###,##0}ms, {2} ops/s; final value: {3}", - AsyncLoop, - watch.ElapsedMilliseconds, - (long)(AsyncLoop / watch.Elapsed.TotalSeconds), - value); - - // perf: sync/multi-threaded - // TestConcurrent(db, key, 30, 10); - //TestConcurrent(db, key, 30, 20); - //TestConcurrent(db, key, 30, 30); - //TestConcurrent(db, key, 30, 40); - //TestConcurrent(db, key, 30, 50); - } + var latency = db.Ping(); + Log("RedisLabs latency: {0:###,##0.##}ms", latency.TotalMilliseconds); + + using (var file = File.Create("RedisLabs.zip")) + { + conn.ExportConfiguration(file); } + } - //private void TestConcurrent(IDatabase db, RedisKey key, int SyncLoop, int Threads) - //{ - // long value; - // db.KeyDelete(key, CommandFlags.FireAndForget); - // var time = RunConcurrent(delegate - // { - // for (int i = 0; i < SyncLoop; i++) - // { - // db.StringIncrement(key); - // } - // }, Threads, timeout: 45000); - // value = (long)db.StringGet(key); - // Assert.Equal(SyncLoop * Threads, value); - // Log("Sync: {0} INCR using {1} threads, {2:###,##0}ms, {3} ops/s; final value: {4}", - // SyncLoop * Threads, Threads, - // (long)time.TotalMilliseconds, - // (long)((SyncLoop * Threads) / time.TotalSeconds), - // value); - //} - - [Fact] - public void RedisLabsSSL() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RedisLabsEnvironmentVariableClientCertificate(bool setEnv) + { + try { Skip.IfNoConfig(nameof(TestConfig.Config.RedisLabsSslServer), TestConfig.Current.RedisLabsSslServer); Skip.IfNoConfig(nameof(TestConfig.Config.RedisLabsPfxPath), TestConfig.Current.RedisLabsPfxPath); - var cert = new X509Certificate2(TestConfig.Current.RedisLabsPfxPath, ""); - Assert.NotNull(cert); - Writer.WriteLine("Thumbprint: " + cert.Thumbprint); - + if (setEnv) + { + Environment.SetEnvironmentVariable("SERedis_ClientCertPfxPath", TestConfig.Current.RedisLabsPfxPath); + Environment.SetEnvironmentVariable("SERedis_IssuerCertPath", "redislabs_ca.pem"); + // check env worked + Assert.Equal(TestConfig.Current.RedisLabsPfxPath, Environment.GetEnvironmentVariable("SERedis_ClientCertPfxPath")); + Assert.Equal("redislabs_ca.pem", Environment.GetEnvironmentVariable("SERedis_IssuerCertPath")); + } int timeout = 5000; if (Debugger.IsAttached) timeout *= 100; var options = new ConfigurationOptions @@ -200,286 +241,223 @@ public void RedisLabsSSL() }, false) }; - options.TrustIssuer("redislabs_ca.pem"); - if (!Directory.Exists(Me())) Directory.CreateDirectory(Me()); #if LOGOUTPUT - ConnectionMultiplexer.EchoPath = Me(); + ConnectionMultiplexer.EchoPath = Me(); #endif options.Ssl = true; - options.CertificateSelection += delegate - { - return cert; - }; - RedisKey key = Me(); - using (var conn = ConnectionMultiplexer.Connect(options)) - { - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - string? s = db.StringGet(key); - Assert.Null(s); - db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); - s = db.StringGet(key); - Assert.Equal("abc", s); - - var latency = db.Ping(); - Log("RedisLabs latency: {0:###,##0.##}ms", latency.TotalMilliseconds); - - using (var file = File.Create("RedisLabs.zip")) - { - conn.ExportConfiguration(file); - } - } - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void RedisLabsEnvironmentVariableClientCertificate(bool setEnv) - { - try - { - Skip.IfNoConfig(nameof(TestConfig.Config.RedisLabsSslServer), TestConfig.Current.RedisLabsSslServer); - Skip.IfNoConfig(nameof(TestConfig.Config.RedisLabsPfxPath), TestConfig.Current.RedisLabsPfxPath); - if (setEnv) - { - Environment.SetEnvironmentVariable("SERedis_ClientCertPfxPath", TestConfig.Current.RedisLabsPfxPath); - Environment.SetEnvironmentVariable("SERedis_IssuerCertPath", "redislabs_ca.pem"); - // check env worked - Assert.Equal(TestConfig.Current.RedisLabsPfxPath, Environment.GetEnvironmentVariable("SERedis_ClientCertPfxPath")); - Assert.Equal("redislabs_ca.pem", Environment.GetEnvironmentVariable("SERedis_IssuerCertPath")); - } - int timeout = 5000; - if (Debugger.IsAttached) timeout *= 100; - var options = new ConfigurationOptions - { - EndPoints = { { TestConfig.Current.RedisLabsSslServer, TestConfig.Current.RedisLabsSslPort } }, - ConnectTimeout = timeout, - AllowAdmin = true, - CommandMap = CommandMap.Create(new HashSet { - "subscribe", "unsubscribe", "cluster" - }, false) - }; + using var conn = ConnectionMultiplexer.Connect(options); - if (!Directory.Exists(Me())) Directory.CreateDirectory(Me()); -#if LOGOUTPUT - ConnectionMultiplexer.EchoPath = Me(); -#endif - options.Ssl = true; - RedisKey key = Me(); - using (var conn = ConnectionMultiplexer.Connect(options)) - { - if (!setEnv) Assert.True(false, "Could not set environment"); + RedisKey key = Me(); + if (!setEnv) Assert.True(false, "Could not set environment"); - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - string? s = db.StringGet(key); - Assert.Null(s); - db.StringSet(key, "abc"); - s = db.StringGet(key); - Assert.Equal("abc", s); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + string? s = db.StringGet(key); + Assert.Null(s); + db.StringSet(key, "abc"); + s = db.StringGet(key); + Assert.Equal("abc", s); - var latency = db.Ping(); - Log("RedisLabs latency: {0:###,##0.##}ms", latency.TotalMilliseconds); + var latency = db.Ping(); + Log("RedisLabs latency: {0:###,##0.##}ms", latency.TotalMilliseconds); - using (var file = File.Create("RedisLabs.zip")) - { - conn.ExportConfiguration(file); - } - } - } - catch (RedisConnectionException ex) when (!setEnv && ex.FailureType == ConnectionFailureType.UnableToConnect) - { - } - finally + using (var file = File.Create("RedisLabs.zip")) { - Environment.SetEnvironmentVariable("SERedis_ClientCertPfxPath", null); + conn.ExportConfiguration(file); } } - - [Fact] - public void SSLHostInferredFromEndpoints() + catch (RedisConnectionException ex) when (!setEnv && ex.FailureType == ConnectionFailureType.UnableToConnect) { - var options = new ConfigurationOptions() - { - EndPoints = { - { "mycache.rediscache.windows.net", 15000}, - { "mycache.rediscache.windows.net", 15001 }, - { "mycache.rediscache.windows.net", 15002 }, - } - }; - options.Ssl = true; - Assert.True(options.SslHost == "mycache.rediscache.windows.net"); - options = new ConfigurationOptions() - { - EndPoints = { - { "121.23.23.45", 15000}, - } - }; - Assert.True(options.SslHost == null); } - - private void Check(string name, object? x, object? y) + finally { - Writer.WriteLine($"{name}: {(x == null ? "(null)" : x.ToString())} vs {(y == null ? "(null)" : y.ToString())}"); - Assert.Equal(x, y); + Environment.SetEnvironmentVariable("SERedis_ClientCertPfxPath", null); } + } - [Fact] - public void Issue883_Exhaustive() + [Fact] + public void SSLHostInferredFromEndpoints() + { + var options = new ConfigurationOptions() { - var old = CultureInfo.CurrentCulture; - try + EndPoints = { + { "mycache.rediscache.windows.net", 15000}, + { "mycache.rediscache.windows.net", 15001 }, + { "mycache.rediscache.windows.net", 15002 }, + } + }; + options.Ssl = true; + Assert.True(options.SslHost == "mycache.rediscache.windows.net"); + options = new ConfigurationOptions() + { + EndPoints = { + { "121.23.23.45", 15000}, + } + }; + Assert.True(options.SslHost == null); + } + + private void Check(string name, object? x, object? y) + { + Writer.WriteLine($"{name}: {(x == null ? "(null)" : x.ToString())} vs {(y == null ? "(null)" : y.ToString())}"); + Assert.Equal(x, y); + } + + [Fact] + public void Issue883_Exhaustive() + { + var old = CultureInfo.CurrentCulture; + try + { + var all = CultureInfo.GetCultures(CultureTypes.AllCultures); + Writer.WriteLine($"Checking {all.Length} cultures..."); + foreach (var ci in all) { - var all = CultureInfo.GetCultures(CultureTypes.AllCultures); - Writer.WriteLine($"Checking {all.Length} cultures..."); - foreach (var ci in all) - { - Writer.WriteLine("Testing: " + ci.Name); - CultureInfo.CurrentCulture = ci; + Writer.WriteLine("Testing: " + ci.Name); + CultureInfo.CurrentCulture = ci; - var a = ConfigurationOptions.Parse("myDNS:883,password=mypassword,connectRetry=3,connectTimeout=5000,syncTimeout=5000,defaultDatabase=0,ssl=true,abortConnect=false"); - var b = new ConfigurationOptions - { - EndPoints = { { "myDNS", 883 } }, - Password = "mypassword", - ConnectRetry = 3, - ConnectTimeout = 5000, - SyncTimeout = 5000, - DefaultDatabase = 0, - Ssl = true, - AbortOnConnectFail = false, - }; - Writer.WriteLine($"computed: {b.ToString(true)}"); - - Writer.WriteLine("Checking endpoints..."); - var c = a.EndPoints.Cast().Single(); - var d = b.EndPoints.Cast().Single(); - Check(nameof(c.Host), c.Host, d.Host); - Check(nameof(c.Port), c.Port, d.Port); - Check(nameof(c.AddressFamily), c.AddressFamily, d.AddressFamily); - - var fields = typeof(ConfigurationOptions).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - Writer.WriteLine($"Comparing {fields.Length} fields..."); - Array.Sort(fields, (x, y) => string.CompareOrdinal(x.Name, y.Name)); - foreach (var field in fields) - { - Check(field.Name, field.GetValue(a), field.GetValue(b)); - } + var a = ConfigurationOptions.Parse("myDNS:883,password=mypassword,connectRetry=3,connectTimeout=5000,syncTimeout=5000,defaultDatabase=0,ssl=true,abortConnect=false"); + var b = new ConfigurationOptions + { + EndPoints = { { "myDNS", 883 } }, + Password = "mypassword", + ConnectRetry = 3, + ConnectTimeout = 5000, + SyncTimeout = 5000, + DefaultDatabase = 0, + Ssl = true, + AbortOnConnectFail = false, + }; + Writer.WriteLine($"computed: {b.ToString(true)}"); + + Writer.WriteLine("Checking endpoints..."); + var c = a.EndPoints.Cast().Single(); + var d = b.EndPoints.Cast().Single(); + Check(nameof(c.Host), c.Host, d.Host); + Check(nameof(c.Port), c.Port, d.Port); + Check(nameof(c.AddressFamily), c.AddressFamily, d.AddressFamily); + + var fields = typeof(ConfigurationOptions).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + Writer.WriteLine($"Comparing {fields.Length} fields..."); + Array.Sort(fields, (x, y) => string.CompareOrdinal(x.Name, y.Name)); + foreach (var field in fields) + { + Check(field.Name, field.GetValue(a), field.GetValue(b)); } } - finally - { - CultureInfo.CurrentCulture = old; - } } + finally + { + CultureInfo.CurrentCulture = old; + } + } + + [Fact] + public void SSLParseViaConfig_Issue883_ConfigObject() + { + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); - [Fact] - public void SSLParseViaConfig_Issue883_ConfigObject() + var options = new ConfigurationOptions { - Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); - Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); + AbortOnConnectFail = false, + Ssl = true, + ConnectRetry = 3, + ConnectTimeout = 5000, + SyncTimeout = 5000, + DefaultDatabase = 0, + EndPoints = { { TestConfig.Current.AzureCacheServer, 6380 } }, + Password = TestConfig.Current.AzureCachePassword + }; + options.CertificateValidation += ShowCertFailures(Writer); + + using var conn = ConnectionMultiplexer.Connect(options); + + conn.GetDatabase().Ping(); + } - var options = new ConfigurationOptions - { - AbortOnConnectFail = false, - Ssl = true, - ConnectRetry = 3, - ConnectTimeout = 5000, - SyncTimeout = 5000, - DefaultDatabase = 0, - EndPoints = { { TestConfig.Current.AzureCacheServer, 6380 } }, - Password = TestConfig.Current.AzureCachePassword - }; - options.CertificateValidation += ShowCertFailures(Writer); - using (var conn = ConnectionMultiplexer.Connect(options)) - { - conn.GetDatabase().Ping(); - } + public static RemoteCertificateValidationCallback? ShowCertFailures(TextWriterOutputHelper output) + { + if (output == null) + { + return null; } - public static RemoteCertificateValidationCallback? ShowCertFailures(TextWriterOutputHelper output) + return (sender, certificate, chain, sslPolicyErrors) => { - if (output == null) + void WriteStatus(X509ChainStatus[] status) { - return null; - } - - return (sender, certificate, chain, sslPolicyErrors) => - { - void WriteStatus(X509ChainStatus[] status) + if (status != null) { - if (status != null) + for (int i = 0; i < status.Length; i++) { - for (int i = 0; i < status.Length; i++) - { - var item = status[i]; - output.WriteLine($"\tstatus {i}: {item.Status}, {item.StatusInformation}"); - } + var item = status[i]; + output.WriteLine($"\tstatus {i}: {item.Status}, {item.StatusInformation}"); } } - lock (output) + } + lock (output) + { + if (certificate != null) { - if (certificate != null) - { - output.WriteLine($"Subject: {certificate.Subject}"); - } - output.WriteLine($"Policy errors: {sslPolicyErrors}"); - if (chain != null) - { - WriteStatus(chain.ChainStatus); + output.WriteLine($"Subject: {certificate.Subject}"); + } + output.WriteLine($"Policy errors: {sslPolicyErrors}"); + if (chain != null) + { + WriteStatus(chain.ChainStatus); - var elements = chain.ChainElements; - if (elements != null) + var elements = chain.ChainElements; + if (elements != null) + { + int index = 0; + foreach (var item in elements) { - int index = 0; - foreach (var item in elements) - { - output.WriteLine($"{index++}: {item.Certificate.Subject}; {item.Information}"); - WriteStatus(item.ChainElementStatus); - } + output.WriteLine($"{index++}: {item.Certificate.Subject}; {item.Information}"); + WriteStatus(item.ChainElementStatus); } } } - return sslPolicyErrors == SslPolicyErrors.None; - }; - } + } + return sslPolicyErrors == SslPolicyErrors.None; + }; + } - [Fact] - public void SSLParseViaConfig_Issue883_ConfigString() - { - Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); - Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); + [Fact] + public void SSLParseViaConfig_Issue883_ConfigString() + { + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); + Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); - var configString = $"{TestConfig.Current.AzureCacheServer}:6380,password={TestConfig.Current.AzureCachePassword},connectRetry=3,connectTimeout=5000,syncTimeout=5000,defaultDatabase=0,ssl=true,abortConnect=false"; - var options = ConfigurationOptions.Parse(configString); - options.CertificateValidation += ShowCertFailures(Writer); - using (var conn = ConnectionMultiplexer.Connect(options)) - { - conn.GetDatabase().Ping(); - } - } + var configString = $"{TestConfig.Current.AzureCacheServer}:6380,password={TestConfig.Current.AzureCachePassword},connectRetry=3,connectTimeout=5000,syncTimeout=5000,defaultDatabase=0,ssl=true,abortConnect=false"; + var options = ConfigurationOptions.Parse(configString); + options.CertificateValidation += ShowCertFailures(Writer); - [Fact] - public void ConfigObject_Issue1407_ToStringIncludesSslProtocols() - { - const SslProtocols sslProtocols = SslProtocols.Tls12 | SslProtocols.Tls; - var sourceOptions = new ConfigurationOptions - { - AbortOnConnectFail = false, - Ssl = true, - SslProtocols = sslProtocols, - ConnectRetry = 3, - ConnectTimeout = 5000, - SyncTimeout = 5000, - DefaultDatabase = 0, - EndPoints = { { "endpoint.test", 6380 } }, - Password = "123456" - }; + using var conn = ConnectionMultiplexer.Connect(options); - var targetOptions = ConfigurationOptions.Parse(sourceOptions.ToString()); - Assert.Equal(sourceOptions.SslProtocols, targetOptions.SslProtocols); - } + conn.GetDatabase().Ping(); + } + + [Fact] + public void ConfigObject_Issue1407_ToStringIncludesSslProtocols() + { + const SslProtocols sslProtocols = SslProtocols.Tls12 | SslProtocols.Tls; + var sourceOptions = new ConfigurationOptions + { + AbortOnConnectFail = false, + Ssl = true, + SslProtocols = sslProtocols, + ConnectRetry = 3, + ConnectTimeout = 5000, + SyncTimeout = 5000, + DefaultDatabase = 0, + EndPoints = { { "endpoint.test", 6380 } }, + Password = "123456" + }; + + var targetOptions = ConfigurationOptions.Parse(sourceOptions.ToString()); + Assert.Equal(sourceOptions.SslProtocols, targetOptions.SslProtocols); } } diff --git a/tests/StackExchange.Redis.Tests/SanityChecks.cs b/tests/StackExchange.Redis.Tests/SanityChecks.cs index 55571fa2d..2ed194c47 100644 --- a/tests/StackExchange.Redis.Tests/SanityChecks.cs +++ b/tests/StackExchange.Redis.Tests/SanityChecks.cs @@ -4,32 +4,31 @@ using System.Reflection.Metadata; using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public sealed class SanityChecks { - public sealed class SanityChecks + /// + /// Ensure we don't reference System.ValueTuple as it causes issues with .NET Full Framework + /// + /// + /// Modified from https://github.com/ltrzesniewski/InlineIL.Fody/blob/137e8b57f78b08cdc3abdaaf50ac01af50c58759/src/InlineIL.Tests/AssemblyTests.cs#L14 + /// Thanks Lucas Trzesniewski! + /// + [Fact] + public void ValueTupleNotReferenced() { - /// - /// Ensure we don't reference System.ValueTuple as it causes issues with .NET Full Framework - /// - /// - /// Modified from https://github.com/ltrzesniewski/InlineIL.Fody/blob/137e8b57f78b08cdc3abdaaf50ac01af50c58759/src/InlineIL.Tests/AssemblyTests.cs#L14 - /// Thanks Lucas Trzesniewski! - /// - [Fact] - public void ValueTupleNotReferenced() - { - using var fileStream = File.OpenRead(typeof(RedisValue).Assembly.Location); - using var peReader = new PEReader(fileStream); - var metadataReader = peReader.GetMetadataReader(); + using var fileStream = File.OpenRead(typeof(RedisValue).Assembly.Location); + using var peReader = new PEReader(fileStream); + var metadataReader = peReader.GetMetadataReader(); - foreach (var typeRefHandle in metadataReader.TypeReferences) + foreach (var typeRefHandle in metadataReader.TypeReferences) + { + var typeRef = metadataReader.GetTypeReference(typeRefHandle); + if (metadataReader.GetString(typeRef.Namespace) == typeof(ValueTuple).Namespace) { - var typeRef = metadataReader.GetTypeReference(typeRefHandle); - if (metadataReader.GetString(typeRef.Namespace) == typeof(ValueTuple).Namespace) - { - var typeName = metadataReader.GetString(typeRef.Name); - Assert.DoesNotContain(nameof(ValueTuple), typeName); - } + var typeName = metadataReader.GetString(typeRef.Name); + Assert.DoesNotContain(nameof(ValueTuple), typeName); } } } diff --git a/tests/StackExchange.Redis.Tests/Scans.cs b/tests/StackExchange.Redis.Tests/Scans.cs index 8089de2ef..7f705c3b8 100644 --- a/tests/StackExchange.Redis.Tests/Scans.cs +++ b/tests/StackExchange.Redis.Tests/Scans.cs @@ -5,418 +5,408 @@ using Xunit.Abstractions; // ReSharper disable PossibleMultipleEnumeration -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Scans : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Scans : TestBase - { - public Scans(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public Scans(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void KeysScan(bool supported) + [Theory] + [InlineData(true)] + [InlineData(false)] + public void KeysScan(bool supported) + { + string[]? disabledCommands = supported ? null : new[] { "scan" }; + using var conn = Create(disabledCommands: disabledCommands, allowAdmin: true); + + var dbId = TestConfig.GetDedicatedDB(conn); + var db = conn.GetDatabase(dbId); + var prefix = Me() + ":"; + var server = GetServer(conn); + server.FlushDatabase(dbId); + for (int i = 0; i < 100; i++) { - string[]? disabledCommands = supported ? null : new[] { "scan" }; - using (var conn = Create(disabledCommands: disabledCommands, allowAdmin: true)) - { - var dbId = TestConfig.GetDedicatedDB(conn); - var db = conn.GetDatabase(dbId); - var prefix = Me() + ":"; - var server = GetServer(conn); - server.FlushDatabase(dbId); - for (int i = 0; i < 100; i++) - { - db.StringSet(prefix + i, Guid.NewGuid().ToString(), flags: CommandFlags.FireAndForget); - } - var seq = server.Keys(dbId, pageSize: 50); - var cur = seq as IScanningCursor; - Assert.NotNull(cur); - Log($"Cursor: {cur.Cursor}, PageOffset: {cur.PageOffset}, PageSize: {cur.PageSize}"); - Assert.Equal(0, cur.PageOffset); - Assert.Equal(0, cur.Cursor); - if (supported) - { - Assert.Equal(50, cur.PageSize); - } - else - { - Assert.Equal(int.MaxValue, cur.PageSize); - } - Assert.Equal(100, seq.Distinct().Count()); - Assert.Equal(100, seq.Distinct().Count()); - Assert.Equal(100, server.Keys(dbId, prefix + "*").Distinct().Count()); - // 7, 70, 71, ..., 79 - Assert.Equal(11, server.Keys(dbId, prefix + "7*").Distinct().Count()); - } + db.StringSet(prefix + i, Guid.NewGuid().ToString(), flags: CommandFlags.FireAndForget); } - - [Fact] - public void ScansIScanning() + var seq = server.Keys(dbId, pageSize: 50); + var cur = seq as IScanningCursor; + Assert.NotNull(cur); + Log($"Cursor: {cur.Cursor}, PageOffset: {cur.PageOffset}, PageSize: {cur.PageSize}"); + Assert.Equal(0, cur.PageOffset); + Assert.Equal(0, cur.Cursor); + if (supported) { - using (var conn = Create(allowAdmin: true)) - { - var prefix = Me() + Guid.NewGuid(); - var dbId = TestConfig.GetDedicatedDB(conn); - var db = conn.GetDatabase(dbId); - var server = GetServer(conn); - server.FlushDatabase(dbId); - for (int i = 0; i < 100; i++) - { - db.StringSet(prefix + i, Guid.NewGuid().ToString(), flags: CommandFlags.FireAndForget); - } - var seq = server.Keys(dbId, prefix + "*", pageSize: 15); - using (var iter = seq.GetEnumerator()) - { - IScanningCursor s0 = (IScanningCursor)seq, s1 = (IScanningCursor)iter; - - Assert.Equal(15, s0.PageSize); - Assert.Equal(15, s1.PageSize); - - // start at zero - Assert.Equal(0, s0.Cursor); - Assert.Equal(s0.Cursor, s1.Cursor); - - for (int i = 0; i < 47; i++) - { - Assert.True(iter.MoveNext()); - } - - // non-zero in the middle - Assert.NotEqual(0, s0.Cursor); - Assert.Equal(s0.Cursor, s1.Cursor); - - for (int i = 0; i < 53; i++) - { - Assert.True(iter.MoveNext()); - } - - // zero "next" at the end - Assert.False(iter.MoveNext()); - Assert.NotEqual(0, s0.Cursor); - Assert.NotEqual(0, s1.Cursor); - } - } + Assert.Equal(50, cur.PageSize); + } + else + { + Assert.Equal(int.MaxValue, cur.PageSize); } + Assert.Equal(100, seq.Distinct().Count()); + Assert.Equal(100, seq.Distinct().Count()); + Assert.Equal(100, server.Keys(dbId, prefix + "*").Distinct().Count()); + // 7, 70, 71, ..., 79 + Assert.Equal(11, server.Keys(dbId, prefix + "7*").Distinct().Count()); + } - [Fact] - public void ScanResume() + [Fact] + public void ScansIScanning() + { + using var conn = Create(allowAdmin: true); + + var prefix = Me() + Guid.NewGuid(); + var dbId = TestConfig.GetDedicatedDB(conn); + var db = conn.GetDatabase(dbId); + var server = GetServer(conn); + server.FlushDatabase(dbId); + for (int i = 0; i < 100; i++) { - using (var conn = Create(allowAdmin: true)) + db.StringSet(prefix + i, Guid.NewGuid().ToString(), flags: CommandFlags.FireAndForget); + } + var seq = server.Keys(dbId, prefix + "*", pageSize: 15); + using (var iter = seq.GetEnumerator()) + { + IScanningCursor s0 = (IScanningCursor)seq, s1 = (IScanningCursor)iter; + + Assert.Equal(15, s0.PageSize); + Assert.Equal(15, s1.PageSize); + + // start at zero + Assert.Equal(0, s0.Cursor); + Assert.Equal(s0.Cursor, s1.Cursor); + + for (int i = 0; i < 47; i++) { - Skip.IfBelow(conn, RedisFeatures.v2_8_0); - - var dbId = TestConfig.GetDedicatedDB(conn); - var db = conn.GetDatabase(dbId); - var prefix = Me(); - var server = GetServer(conn); - server.FlushDatabase(dbId); - int i; - for (i = 0; i < 100; i++) - { - db.StringSet(prefix + ":" + i, Guid.NewGuid().ToString()); - } - - var expected = new HashSet(); - long snapCursor = 0; - int snapOffset = 0, snapPageSize = 0; - - i = 0; - var seq = server.Keys(dbId, prefix + ":*", pageSize: 15); - foreach (var key in seq) - { - if (i == 57) - { - snapCursor = ((IScanningCursor)seq).Cursor; - snapOffset = ((IScanningCursor)seq).PageOffset; - snapPageSize = ((IScanningCursor)seq).PageSize; - Log($"i: {i}, Cursor: {snapCursor}, Offset: {snapOffset}, PageSize: {snapPageSize}"); - } - if (i >= 57) - { - expected.Add(key); - } - i++; - } - Log($"Expected: 43, Actual: {expected.Count}, Cursor: {snapCursor}, Offset: {snapOffset}, PageSize: {snapPageSize}"); - Assert.Equal(43, expected.Count); - Assert.NotEqual(0, snapCursor); - Assert.Equal(15, snapPageSize); - - // note: you might think that we can say "hmmm, 57 when using page-size 15 on an empty (flushed) db (so: no skipped keys); that'll be - // offset 12 in the 4th page; you'd be wrong, though; page size doesn't *actually* mean page size; it is a rough analogue for - // page size, with zero guarantees; in this particular test, the first page actually has 19 elements, for example. So: we cannot - // make the following assertion: - // Assert.Equal(12, snapOffset); - - seq = server.Keys(dbId, prefix + ":*", pageSize: 15, cursor: snapCursor, pageOffset: snapOffset); - var seqCur = (IScanningCursor)seq; - Assert.Equal(snapCursor, seqCur.Cursor); - Assert.Equal(snapPageSize, seqCur.PageSize); - Assert.Equal(snapOffset, seqCur.PageOffset); - using (var iter = seq.GetEnumerator()) - { - var iterCur = (IScanningCursor)iter; - Assert.Equal(snapCursor, iterCur.Cursor); - Assert.Equal(snapOffset, iterCur.PageOffset); - Assert.Equal(snapCursor, seqCur.Cursor); - Assert.Equal(snapOffset, seqCur.PageOffset); - - Assert.True(iter.MoveNext()); - Assert.Equal(snapCursor, iterCur.Cursor); - Assert.Equal(snapOffset, iterCur.PageOffset); - Assert.Equal(snapCursor, seqCur.Cursor); - Assert.Equal(snapOffset, seqCur.PageOffset); - - Assert.True(iter.MoveNext()); - Assert.Equal(snapCursor, iterCur.Cursor); - Assert.Equal(snapOffset + 1, iterCur.PageOffset); - Assert.Equal(snapCursor, seqCur.Cursor); - Assert.Equal(snapOffset + 1, seqCur.PageOffset); - } - - int count = 0; - foreach (var key in seq) - { - expected.Remove(key); - count++; - } - Assert.Empty(expected); - Assert.Equal(43, count); + Assert.True(iter.MoveNext()); } - } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void SetScan(bool supported) - { - string[]? disabledCommands = supported ? null : new[] { "sscan" }; - using (var conn = Create(disabledCommands: disabledCommands)) + // non-zero in the middle + Assert.NotEqual(0, s0.Cursor); + Assert.Equal(s0.Cursor, s1.Cursor); + + for (int i = 0; i < 53; i++) { - RedisKey key = Me(); - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - db.SetAdd(key, "a", CommandFlags.FireAndForget); - db.SetAdd(key, "b", CommandFlags.FireAndForget); - db.SetAdd(key, "c", CommandFlags.FireAndForget); - var arr = db.SetScan(key).ToArray(); - Assert.Equal(3, arr.Length); - Assert.Contains((RedisValue)"a", arr); - Assert.Contains((RedisValue)"b", arr); - Assert.Contains((RedisValue)"c", arr); + Assert.True(iter.MoveNext()); } + + // zero "next" at the end + Assert.False(iter.MoveNext()); + Assert.NotEqual(0, s0.Cursor); + Assert.NotEqual(0, s1.Cursor); } + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void SortedSetScan(bool supported) + [Fact] + public void ScanResume() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_8_0); + + var dbId = TestConfig.GetDedicatedDB(conn); + var db = conn.GetDatabase(dbId); + var prefix = Me(); + var server = GetServer(conn); + server.FlushDatabase(dbId); + int i; + for (i = 0; i < 100; i++) { - string[]? disabledCommands = supported ? null : new[] { "zscan" }; - using (var conn = Create(disabledCommands: disabledCommands)) - { - RedisKey key = Me() + supported; - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - db.SortedSetAdd(key, "a", 1, CommandFlags.FireAndForget); - db.SortedSetAdd(key, "b", 2, CommandFlags.FireAndForget); - db.SortedSetAdd(key, "c", 3, CommandFlags.FireAndForget); - - var arr = db.SortedSetScan(key).ToArray(); - Assert.Equal(3, arr.Length); - Assert.True(arr.Any(x => x.Element == "a" && x.Score == 1), "a"); - Assert.True(arr.Any(x => x.Element == "b" && x.Score == 2), "b"); - Assert.True(arr.Any(x => x.Element == "c" && x.Score == 3), "c"); - - var dictionary = arr.ToDictionary(); - Assert.Equal(1, dictionary["a"]); - Assert.Equal(2, dictionary["b"]); - Assert.Equal(3, dictionary["c"]); - - var sDictionary = arr.ToStringDictionary(); - Assert.Equal(1, sDictionary["a"]); - Assert.Equal(2, sDictionary["b"]); - Assert.Equal(3, sDictionary["c"]); - - var basic = db.SortedSetRangeByRankWithScores(key, order: Order.Ascending).ToDictionary(); - Assert.Equal(3, basic.Count); - Assert.Equal(1, basic["a"]); - Assert.Equal(2, basic["b"]); - Assert.Equal(3, basic["c"]); - - basic = db.SortedSetRangeByRankWithScores(key, order: Order.Descending).ToDictionary(); - Assert.Equal(3, basic.Count); - Assert.Equal(1, basic["a"]); - Assert.Equal(2, basic["b"]); - Assert.Equal(3, basic["c"]); - - var basicArr = db.SortedSetRangeByScoreWithScores(key, order: Order.Ascending); - Assert.Equal(3, basicArr.Length); - Assert.Equal(1, basicArr[0].Score); - Assert.Equal(2, basicArr[1].Score); - Assert.Equal(3, basicArr[2].Score); - basic = basicArr.ToDictionary(); - Assert.Equal(3, basic.Count); //asc - Assert.Equal(1, basic["a"]); - Assert.Equal(2, basic["b"]); - Assert.Equal(3, basic["c"]); - - basicArr = db.SortedSetRangeByScoreWithScores(key, order: Order.Descending); - Assert.Equal(3, basicArr.Length); - Assert.Equal(3, basicArr[0].Score); - Assert.Equal(2, basicArr[1].Score); - Assert.Equal(1, basicArr[2].Score); - basic = basicArr.ToDictionary(); - Assert.Equal(3, basic.Count); // desc - Assert.Equal(1, basic["a"]); - Assert.Equal(2, basic["b"]); - Assert.Equal(3, basic["c"]); - } + db.StringSet(prefix + ":" + i, Guid.NewGuid().ToString()); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void HashScan(bool supported) + var expected = new HashSet(); + long snapCursor = 0; + int snapOffset = 0, snapPageSize = 0; + + i = 0; + var seq = server.Keys(dbId, prefix + ":*", pageSize: 15); + foreach (var key in seq) { - string[]? disabledCommands = supported ? null : new[] { "hscan" }; - using (var conn = Create(disabledCommands: disabledCommands)) + if (i == 57) { - RedisKey key = Me(); - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - db.HashSet(key, "a", "1", flags: CommandFlags.FireAndForget); - db.HashSet(key, "b", "2", flags: CommandFlags.FireAndForget); - db.HashSet(key, "c", "3", flags: CommandFlags.FireAndForget); - - var arr = db.HashScan(key).ToArray(); - Assert.Equal(3, arr.Length); - Assert.True(arr.Any(x => x.Name == "a" && x.Value == "1"), "a"); - Assert.True(arr.Any(x => x.Name == "b" && x.Value == "2"), "b"); - Assert.True(arr.Any(x => x.Name == "c" && x.Value == "3"), "c"); - - var dictionary = arr.ToDictionary(); - Assert.Equal(1, (long)dictionary["a"]); - Assert.Equal(2, (long)dictionary["b"]); - Assert.Equal(3, (long)dictionary["c"]); - - var sDictionary = arr.ToStringDictionary(); - Assert.Equal("1", sDictionary["a"]); - Assert.Equal("2", sDictionary["b"]); - Assert.Equal("3", sDictionary["c"]); - - var basic = db.HashGetAll(key).ToDictionary(); - Assert.Equal(3, basic.Count); - Assert.Equal(1, (long)basic["a"]); - Assert.Equal(2, (long)basic["b"]); - Assert.Equal(3, (long)basic["c"]); + snapCursor = ((IScanningCursor)seq).Cursor; + snapOffset = ((IScanningCursor)seq).PageOffset; + snapPageSize = ((IScanningCursor)seq).PageSize; + Log($"i: {i}, Cursor: {snapCursor}, Offset: {snapOffset}, PageSize: {snapPageSize}"); } + if (i >= 57) + { + expected.Add(key); + } + i++; + } + Log($"Expected: 43, Actual: {expected.Count}, Cursor: {snapCursor}, Offset: {snapOffset}, PageSize: {snapPageSize}"); + Assert.Equal(43, expected.Count); + Assert.NotEqual(0, snapCursor); + Assert.Equal(15, snapPageSize); + + // note: you might think that we can say "hmmm, 57 when using page-size 15 on an empty (flushed) db (so: no skipped keys); that'll be + // offset 12 in the 4th page; you'd be wrong, though; page size doesn't *actually* mean page size; it is a rough analogue for + // page size, with zero guarantees; in this particular test, the first page actually has 19 elements, for example. So: we cannot + // make the following assertion: + // Assert.Equal(12, snapOffset); + + seq = server.Keys(dbId, prefix + ":*", pageSize: 15, cursor: snapCursor, pageOffset: snapOffset); + var seqCur = (IScanningCursor)seq; + Assert.Equal(snapCursor, seqCur.Cursor); + Assert.Equal(snapPageSize, seqCur.PageSize); + Assert.Equal(snapOffset, seqCur.PageOffset); + using (var iter = seq.GetEnumerator()) + { + var iterCur = (IScanningCursor)iter; + Assert.Equal(snapCursor, iterCur.Cursor); + Assert.Equal(snapOffset, iterCur.PageOffset); + Assert.Equal(snapCursor, seqCur.Cursor); + Assert.Equal(snapOffset, seqCur.PageOffset); + + Assert.True(iter.MoveNext()); + Assert.Equal(snapCursor, iterCur.Cursor); + Assert.Equal(snapOffset, iterCur.PageOffset); + Assert.Equal(snapCursor, seqCur.Cursor); + Assert.Equal(snapOffset, seqCur.PageOffset); + + Assert.True(iter.MoveNext()); + Assert.Equal(snapCursor, iterCur.Cursor); + Assert.Equal(snapOffset + 1, iterCur.PageOffset); + Assert.Equal(snapCursor, seqCur.Cursor); + Assert.Equal(snapOffset + 1, seqCur.PageOffset); } - [Theory] - [InlineData(10)] - [InlineData(100)] - [InlineData(1000)] - [InlineData(10000)] - public void HashScanLarge(int pageSize) + int count = 0; + foreach (var key in seq) { - using (var conn = Create()) - { - RedisKey key = Me() + pageSize; - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); + expected.Remove(key); + count++; + } + Assert.Empty(expected); + Assert.Equal(43, count); + } - for (int i = 0; i < 2000; i++) - db.HashSet(key, "k" + i, "v" + i, flags: CommandFlags.FireAndForget); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SetScan(bool supported) + { + string[]? disabledCommands = supported ? null : new[] { "sscan" }; + + using var conn = Create(disabledCommands: disabledCommands); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.SetAdd(key, "a", CommandFlags.FireAndForget); + db.SetAdd(key, "b", CommandFlags.FireAndForget); + db.SetAdd(key, "c", CommandFlags.FireAndForget); + var arr = db.SetScan(key).ToArray(); + Assert.Equal(3, arr.Length); + Assert.Contains((RedisValue)"a", arr); + Assert.Contains((RedisValue)"b", arr); + Assert.Contains((RedisValue)"c", arr); + } - int count = db.HashScan(key, pageSize: pageSize).Count(); - Assert.Equal(2000, count); - } - } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SortedSetScan(bool supported) + { + string[]? disabledCommands = supported ? null : new[] { "zscan" }; + + using var conn = Create(disabledCommands: disabledCommands); + + RedisKey key = Me() + supported; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.SortedSetAdd(key, "a", 1, CommandFlags.FireAndForget); + db.SortedSetAdd(key, "b", 2, CommandFlags.FireAndForget); + db.SortedSetAdd(key, "c", 3, CommandFlags.FireAndForget); + + var arr = db.SortedSetScan(key).ToArray(); + Assert.Equal(3, arr.Length); + Assert.True(arr.Any(x => x.Element == "a" && x.Score == 1), "a"); + Assert.True(arr.Any(x => x.Element == "b" && x.Score == 2), "b"); + Assert.True(arr.Any(x => x.Element == "c" && x.Score == 3), "c"); + + var dictionary = arr.ToDictionary(); + Assert.Equal(1, dictionary["a"]); + Assert.Equal(2, dictionary["b"]); + Assert.Equal(3, dictionary["c"]); + + var sDictionary = arr.ToStringDictionary(); + Assert.Equal(1, sDictionary["a"]); + Assert.Equal(2, sDictionary["b"]); + Assert.Equal(3, sDictionary["c"]); + + var basic = db.SortedSetRangeByRankWithScores(key, order: Order.Ascending).ToDictionary(); + Assert.Equal(3, basic.Count); + Assert.Equal(1, basic["a"]); + Assert.Equal(2, basic["b"]); + Assert.Equal(3, basic["c"]); + + basic = db.SortedSetRangeByRankWithScores(key, order: Order.Descending).ToDictionary(); + Assert.Equal(3, basic.Count); + Assert.Equal(1, basic["a"]); + Assert.Equal(2, basic["b"]); + Assert.Equal(3, basic["c"]); + + var basicArr = db.SortedSetRangeByScoreWithScores(key, order: Order.Ascending); + Assert.Equal(3, basicArr.Length); + Assert.Equal(1, basicArr[0].Score); + Assert.Equal(2, basicArr[1].Score); + Assert.Equal(3, basicArr[2].Score); + basic = basicArr.ToDictionary(); + Assert.Equal(3, basic.Count); //asc + Assert.Equal(1, basic["a"]); + Assert.Equal(2, basic["b"]); + Assert.Equal(3, basic["c"]); + + basicArr = db.SortedSetRangeByScoreWithScores(key, order: Order.Descending); + Assert.Equal(3, basicArr.Length); + Assert.Equal(3, basicArr[0].Score); + Assert.Equal(2, basicArr[1].Score); + Assert.Equal(1, basicArr[2].Score); + basic = basicArr.ToDictionary(); + Assert.Equal(3, basic.Count); // desc + Assert.Equal(1, basic["a"]); + Assert.Equal(2, basic["b"]); + Assert.Equal(3, basic["c"]); + } - [Fact] // See https://github.com/StackExchange/StackExchange.Redis/issues/729 - public void HashScanThresholds() - { - using (var conn = Create(allowAdmin: true)) - { - var config = conn.GetServer(conn.GetEndPoints(true)[0]).ConfigGet("hash-max-ziplist-entries").First(); - var threshold = int.Parse(config.Value); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void HashScan(bool supported) + { + string[]? disabledCommands = supported ? null : new[] { "hscan" }; + + using var conn = Create(disabledCommands: disabledCommands); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.HashSet(key, "a", "1", flags: CommandFlags.FireAndForget); + db.HashSet(key, "b", "2", flags: CommandFlags.FireAndForget); + db.HashSet(key, "c", "3", flags: CommandFlags.FireAndForget); + + var arr = db.HashScan(key).ToArray(); + Assert.Equal(3, arr.Length); + Assert.True(arr.Any(x => x.Name == "a" && x.Value == "1"), "a"); + Assert.True(arr.Any(x => x.Name == "b" && x.Value == "2"), "b"); + Assert.True(arr.Any(x => x.Name == "c" && x.Value == "3"), "c"); + + var dictionary = arr.ToDictionary(); + Assert.Equal(1, (long)dictionary["a"]); + Assert.Equal(2, (long)dictionary["b"]); + Assert.Equal(3, (long)dictionary["c"]); + + var sDictionary = arr.ToStringDictionary(); + Assert.Equal("1", sDictionary["a"]); + Assert.Equal("2", sDictionary["b"]); + Assert.Equal("3", sDictionary["c"]); + + var basic = db.HashGetAll(key).ToDictionary(); + Assert.Equal(3, basic.Count); + Assert.Equal(1, (long)basic["a"]); + Assert.Equal(2, (long)basic["b"]); + Assert.Equal(3, (long)basic["c"]); + } - RedisKey key = Me(); - Assert.False(GotCursors(conn, key, threshold - 1)); - Assert.True(GotCursors(conn, key, threshold + 1)); - } - } + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1000)] + [InlineData(10000)] + public void HashScanLarge(int pageSize) + { + using var conn = Create(); - private static bool GotCursors(IConnectionMultiplexer conn, RedisKey key, int count) - { - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); + RedisKey key = Me() + pageSize; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); - var entries = new HashEntry[count]; - for (var i = 0; i < count; i++) - { - entries[i] = new HashEntry("Item:" + i, i); - } - db.HashSet(key, entries, CommandFlags.FireAndForget); + for (int i = 0; i < 2000; i++) + db.HashSet(key, "k" + i, "v" + i, flags: CommandFlags.FireAndForget); - var found = false; - var response = db.HashScan(key); - var cursor = ((IScanningCursor)response); - foreach (var _ in response) - { - if (cursor.Cursor > 0) - { - found = true; - } - } - return found; - } + int count = db.HashScan(key, pageSize: pageSize).Count(); + Assert.Equal(2000, count); + } - [Theory] - [InlineData(10)] - [InlineData(100)] - [InlineData(1000)] - [InlineData(10000)] - public void SetScanLarge(int pageSize) - { - using (var conn = Create()) - { - RedisKey key = Me() + pageSize; - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); + [Fact] // See https://github.com/StackExchange/StackExchange.Redis/issues/729 + public void HashScanThresholds() + { + using var conn = Create(allowAdmin: true); - for (int i = 0; i < 2000; i++) - db.SetAdd(key, "s" + i, flags: CommandFlags.FireAndForget); + var config = conn.GetServer(conn.GetEndPoints(true)[0]).ConfigGet("hash-max-ziplist-entries").First(); + var threshold = int.Parse(config.Value); - int count = db.SetScan(key, pageSize: pageSize).Count(); - Assert.Equal(2000, count); - } + RedisKey key = Me(); + Assert.False(GotCursors(conn, key, threshold - 1)); + Assert.True(GotCursors(conn, key, threshold + 1)); + } + + private static bool GotCursors(IConnectionMultiplexer conn, RedisKey key, int count) + { + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + var entries = new HashEntry[count]; + for (var i = 0; i < count; i++) + { + entries[i] = new HashEntry("Item:" + i, i); } + db.HashSet(key, entries, CommandFlags.FireAndForget); - [Theory] - [InlineData(10)] - [InlineData(100)] - [InlineData(1000)] - [InlineData(10000)] - public void SortedSetScanLarge(int pageSize) + var found = false; + var response = db.HashScan(key); + var cursor = ((IScanningCursor)response); + foreach (var _ in response) { - using (var conn = Create()) + if (cursor.Cursor > 0) { - RedisKey key = Me() + pageSize; - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - - for (int i = 0; i < 2000; i++) - db.SortedSetAdd(key, "z" + i, i, flags: CommandFlags.FireAndForget); - - int count = db.SortedSetScan(key, pageSize: pageSize).Count(); - Assert.Equal(2000, count); + found = true; } } + return found; + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1000)] + [InlineData(10000)] + public void SetScanLarge(int pageSize) + { + using var conn = Create(); + + RedisKey key = Me() + pageSize; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + for (int i = 0; i < 2000; i++) + db.SetAdd(key, "s" + i, flags: CommandFlags.FireAndForget); + + int count = db.SetScan(key, pageSize: pageSize).Count(); + Assert.Equal(2000, count); + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1000)] + [InlineData(10000)] + public void SortedSetScanLarge(int pageSize) + { + using var conn = Create(); + + RedisKey key = Me() + pageSize; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + for (int i = 0; i < 2000; i++) + db.SortedSetAdd(key, "z" + i, i, flags: CommandFlags.FireAndForget); + + int count = db.SortedSetScan(key, pageSize: pageSize).Count(); + Assert.Equal(2000, count); } } diff --git a/tests/StackExchange.Redis.Tests/Scripting.cs b/tests/StackExchange.Redis.Tests/Scripting.cs index 09689c8fa..0e80ff08a 100644 --- a/tests/StackExchange.Redis.Tests/Scripting.cs +++ b/tests/StackExchange.Redis.Tests/Scripting.cs @@ -7,112 +7,104 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Scripting : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Scripting : TestBase - { - public Scripting(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public Scripting(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } - private IConnectionMultiplexer GetScriptConn(bool allowAdmin = false) - { - int syncTimeout = 5000; - if (Debugger.IsAttached) syncTimeout = 500000; - var muxer = Create(allowAdmin: allowAdmin, syncTimeout: syncTimeout); + private IConnectionMultiplexer GetScriptConn(bool allowAdmin = false) + { + int syncTimeout = 5000; + if (Debugger.IsAttached) syncTimeout = 500000; + return Create(allowAdmin: allowAdmin, syncTimeout: syncTimeout, require: RedisFeatures.v2_6_0); + } - Skip.IfBelow(muxer, RedisFeatures.v2_6_0); - return muxer; - } + [Fact] + public void ClientScripting() + { + using var conn = GetScriptConn(); + _ = conn.GetDatabase().ScriptEvaluate("return redis.call('info','server')", null, null); + } - [Fact] - public void ClientScripting() - { - using (var conn = GetScriptConn()) - { - _ = conn.GetDatabase().ScriptEvaluate("return redis.call('info','server')", null, null); - } - } + [Fact] + public async Task BasicScripting() + { + using var conn = GetScriptConn(); + + var db = conn.GetDatabase(); + var noCache = db.ScriptEvaluateAsync("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", + new RedisKey[] { "key1", "key2" }, new RedisValue[] { "first", "second" }); + var cache = db.ScriptEvaluateAsync("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", + new RedisKey[] { "key1", "key2" }, new RedisValue[] { "first", "second" }); + var results = (string[]?)await noCache; + Assert.NotNull(results); + Assert.Equal(4, results.Length); + Assert.Equal("key1", results[0]); + Assert.Equal("key2", results[1]); + Assert.Equal("first", results[2]); + Assert.Equal("second", results[3]); + + results = (string[]?)await cache; + Assert.NotNull(results); + Assert.Equal(4, results.Length); + Assert.Equal("key1", results[0]); + Assert.Equal("key2", results[1]); + Assert.Equal("first", results[2]); + Assert.Equal("second", results[3]); + } - [Fact] - public async Task BasicScripting() - { - using (var muxer = GetScriptConn()) - { - var conn = muxer.GetDatabase(); - var noCache = conn.ScriptEvaluateAsync("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", - new RedisKey[] { "key1", "key2" }, new RedisValue[] { "first", "second" }); - var cache = conn.ScriptEvaluateAsync("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", - new RedisKey[] { "key1", "key2" }, new RedisValue[] { "first", "second" }); - var results = (string[]?)await noCache; - Assert.NotNull(results); - Assert.Equal(4, results.Length); - Assert.Equal("key1", results[0]); - Assert.Equal("key2", results[1]); - Assert.Equal("first", results[2]); - Assert.Equal("second", results[3]); - - results = (string[]?)await cache; - Assert.NotNull(results); - Assert.Equal(4, results.Length); - Assert.Equal("key1", results[0]); - Assert.Equal("key2", results[1]); - Assert.Equal("first", results[2]); - Assert.Equal("second", results[3]); - } - } + [Fact] + public void KeysScripting() + { + using var conn = GetScriptConn(); - [Fact] - public void KeysScripting() - { - using (var muxer = GetScriptConn()) - { - var conn = muxer.GetDatabase(); - var key = Me(); - conn.StringSet(key, "bar", flags: CommandFlags.FireAndForget); - var result = (string?)conn.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); - Assert.Equal("bar", result); - } - } + var db = conn.GetDatabase(); + var key = Me(); + db.StringSet(key, "bar", flags: CommandFlags.FireAndForget); + var result = (string?)db.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); + Assert.Equal("bar", result); + } - [Fact] - public async Task TestRandomThingFromForum() - { - const string script = @"local currentVal = tonumber(redis.call('GET', KEYS[1])); + [Fact] + public async Task TestRandomThingFromForum() + { + const string script = @"local currentVal = tonumber(redis.call('GET', KEYS[1])); if (currentVal <= 0 ) then return 1 elseif (currentVal - (tonumber(ARGV[1])) < 0 ) then return 0 end; return redis.call('INCRBY', KEYS[1], -tonumber(ARGV[1]));"; - using (var muxer = GetScriptConn()) - { - var prefix = Me(); - var conn = muxer.GetDatabase(); - conn.StringSet(prefix + "A", "0", flags: CommandFlags.FireAndForget); - conn.StringSet(prefix + "B", "5", flags: CommandFlags.FireAndForget); - conn.StringSet(prefix + "C", "10", flags: CommandFlags.FireAndForget); - - var a = conn.ScriptEvaluateAsync(script, new RedisKey[] { prefix + "A" }, new RedisValue[] { 6 }).ForAwait(); - var b = conn.ScriptEvaluateAsync(script, new RedisKey[] { prefix + "B" }, new RedisValue[] { 6 }).ForAwait(); - var c = conn.ScriptEvaluateAsync(script, new RedisKey[] { prefix + "C" }, new RedisValue[] { 6 }).ForAwait(); - - var vals = await conn.StringGetAsync(new RedisKey[] { prefix + "A", prefix + "B", prefix + "C" }).ForAwait(); - - Assert.Equal(1, (long)await a); // exit code when current val is non-positive - Assert.Equal(0, (long)await b); // exit code when result would be negative - Assert.Equal(4, (long)await c); // 10 - 6 = 4 - Assert.Equal("0", vals[0]); - Assert.Equal("5", vals[1]); - Assert.Equal("4", vals[2]); - } - } + using var conn = GetScriptConn(); - [Fact] - public void HackyGetPerf() - { - using (var muxer = GetScriptConn()) - { - var key = Me(); - var conn = muxer.GetDatabase(); - conn.StringSet(key + "foo", "bar", flags: CommandFlags.FireAndForget); - var result = (long)conn.ScriptEvaluate(@" + var prefix = Me(); + var db = conn.GetDatabase(); + db.StringSet(prefix + "A", "0", flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "B", "5", flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "C", "10", flags: CommandFlags.FireAndForget); + + var a = db.ScriptEvaluateAsync(script, new RedisKey[] { prefix + "A" }, new RedisValue[] { 6 }).ForAwait(); + var b = db.ScriptEvaluateAsync(script, new RedisKey[] { prefix + "B" }, new RedisValue[] { 6 }).ForAwait(); + var c = db.ScriptEvaluateAsync(script, new RedisKey[] { prefix + "C" }, new RedisValue[] { 6 }).ForAwait(); + + var vals = await db.StringGetAsync(new RedisKey[] { prefix + "A", prefix + "B", prefix + "C" }).ForAwait(); + + Assert.Equal(1, (long)await a); // exit code when current val is non-positive + Assert.Equal(0, (long)await b); // exit code when result would be negative + Assert.Equal(4, (long)await c); // 10 - 6 = 4 + Assert.Equal("0", vals[0]); + Assert.Equal("5", vals[1]); + Assert.Equal("4", vals[2]); + } + + [Fact] + public void HackyGetPerf() + { + using var conn = GetScriptConn(); + + var key = Me(); + var db = conn.GetDatabase(); + db.StringSet(key + "foo", "bar", flags: CommandFlags.FireAndForget); + var result = (long)db.ScriptEvaluate(@" redis.call('psetex', KEYS[1], 60000, 'timing') for i = 1,5000 do redis.call('set', 'ignore','abc') @@ -121,1018 +113,944 @@ public void HackyGetPerf() redis.call('del', KEYS[1]) return timeTaken ", new RedisKey[] { key }, null); - Log(result.ToString()); - Assert.True(result > 0); - } - } + Log(result.ToString()); + Assert.True(result > 0); + } - [Fact] - public async Task MultiIncrWithoutReplies() - { - using (var muxer = GetScriptConn()) - { - var conn = muxer.GetDatabase(); - var prefix = Me(); - // prime some initial values - conn.KeyDelete(new RedisKey[] { prefix + "a", prefix + "b", prefix + "c" }, CommandFlags.FireAndForget); - conn.StringIncrement(prefix + "b", flags: CommandFlags.FireAndForget); - conn.StringIncrement(prefix + "c", flags: CommandFlags.FireAndForget); - conn.StringIncrement(prefix + "c", flags: CommandFlags.FireAndForget); - - // run the script, passing "a", "b", "c", "c" to - // increment a & b by 1, c twice - var result = conn.ScriptEvaluateAsync( - "for i,key in ipairs(KEYS) do redis.call('incr', key) end", - new RedisKey[] { prefix + "a", prefix + "b", prefix + "c", prefix + "c" }, // <== aka "KEYS" in the script - null).ForAwait(); // <== aka "ARGV" in the script - - // check the incremented values - var a = conn.StringGetAsync(prefix + "a").ForAwait(); - var b = conn.StringGetAsync(prefix + "b").ForAwait(); - var c = conn.StringGetAsync(prefix + "c").ForAwait(); - - var r = await result; - Assert.NotNull(r); - Assert.True(r.IsNull, "result"); - Assert.Equal(1, (long)await a); - Assert.Equal(2, (long)await b); - Assert.Equal(4, (long)await c); - } - } + [Fact] + public async Task MultiIncrWithoutReplies() + { + using var conn = GetScriptConn(); + + var db = conn.GetDatabase(); + var prefix = Me(); + // prime some initial values + db.KeyDelete(new RedisKey[] { prefix + "a", prefix + "b", prefix + "c" }, CommandFlags.FireAndForget); + db.StringIncrement(prefix + "b", flags: CommandFlags.FireAndForget); + db.StringIncrement(prefix + "c", flags: CommandFlags.FireAndForget); + db.StringIncrement(prefix + "c", flags: CommandFlags.FireAndForget); + + // run the script, passing "a", "b", "c", "c" to + // increment a & b by 1, c twice + var result = db.ScriptEvaluateAsync( + "for i,key in ipairs(KEYS) do redis.call('incr', key) end", + new RedisKey[] { prefix + "a", prefix + "b", prefix + "c", prefix + "c" }, // <== aka "KEYS" in the script + null).ForAwait(); // <== aka "ARGV" in the script + + // check the incremented values + var a = db.StringGetAsync(prefix + "a").ForAwait(); + var b = db.StringGetAsync(prefix + "b").ForAwait(); + var c = db.StringGetAsync(prefix + "c").ForAwait(); + + var r = await result; + Assert.NotNull(r); + Assert.True(r.IsNull, "result"); + Assert.Equal(1, (long)await a); + Assert.Equal(2, (long)await b); + Assert.Equal(4, (long)await c); + } - [Fact] - public async Task MultiIncrByWithoutReplies() - { - using (var muxer = GetScriptConn()) - { - var conn = muxer.GetDatabase(); - var prefix = Me(); - // prime some initial values - conn.KeyDelete(new RedisKey[] { prefix + "a", prefix + "b", prefix + "c" }, CommandFlags.FireAndForget); - conn.StringIncrement(prefix + "b", flags: CommandFlags.FireAndForget); - conn.StringIncrement(prefix + "c", flags: CommandFlags.FireAndForget); - conn.StringIncrement(prefix + "c", flags: CommandFlags.FireAndForget); - - //run the script, passing "a", "b", "c" and 1,2,3 - // increment a &b by 1, c twice - var result = conn.ScriptEvaluateAsync( - "for i,key in ipairs(KEYS) do redis.call('incrby', key, ARGV[i]) end", - new RedisKey[] { prefix + "a", prefix + "b", prefix + "c" }, // <== aka "KEYS" in the script - new RedisValue[] { 1, 1, 2 }).ForAwait(); // <== aka "ARGV" in the script - - // check the incremented values - var a = conn.StringGetAsync(prefix + "a").ForAwait(); - var b = conn.StringGetAsync(prefix + "b").ForAwait(); - var c = conn.StringGetAsync(prefix + "c").ForAwait(); - - Assert.True((await result).IsNull, "result"); - Assert.Equal(1, (long)await a); - Assert.Equal(2, (long)await b); - Assert.Equal(4, (long)await c); - } - } + [Fact] + public async Task MultiIncrByWithoutReplies() + { + using var conn = GetScriptConn(); + + var db = conn.GetDatabase(); + var prefix = Me(); + // prime some initial values + db.KeyDelete(new RedisKey[] { prefix + "a", prefix + "b", prefix + "c" }, CommandFlags.FireAndForget); + db.StringIncrement(prefix + "b", flags: CommandFlags.FireAndForget); + db.StringIncrement(prefix + "c", flags: CommandFlags.FireAndForget); + db.StringIncrement(prefix + "c", flags: CommandFlags.FireAndForget); + + //run the script, passing "a", "b", "c" and 1,2,3 + // increment a &b by 1, c twice + var result = db.ScriptEvaluateAsync( + "for i,key in ipairs(KEYS) do redis.call('incrby', key, ARGV[i]) end", + new RedisKey[] { prefix + "a", prefix + "b", prefix + "c" }, // <== aka "KEYS" in the script + new RedisValue[] { 1, 1, 2 }).ForAwait(); // <== aka "ARGV" in the script + + // check the incremented values + var a = db.StringGetAsync(prefix + "a").ForAwait(); + var b = db.StringGetAsync(prefix + "b").ForAwait(); + var c = db.StringGetAsync(prefix + "c").ForAwait(); + + Assert.True((await result).IsNull, "result"); + Assert.Equal(1, (long)await a); + Assert.Equal(2, (long)await b); + Assert.Equal(4, (long)await c); + } - [Fact] - public void DisableStringInference() - { - using (var muxer = GetScriptConn()) - { - var conn = muxer.GetDatabase(); - var key = Me(); - conn.StringSet(key, "bar", flags: CommandFlags.FireAndForget); - var result = (byte[]?)conn.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }); - Assert.NotNull(result); - Assert.Equal("bar", Encoding.UTF8.GetString(result)); - } - } + [Fact] + public void DisableStringInference() + { + using var conn = GetScriptConn(); + + var db = conn.GetDatabase(); + var key = Me(); + db.StringSet(key, "bar", flags: CommandFlags.FireAndForget); + var result = (byte[]?)db.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }); + Assert.NotNull(result); + Assert.Equal("bar", Encoding.UTF8.GetString(result)); + } - [Fact] - public void FlushDetection() - { // we don't expect this to handle everything; we just expect it to be predictable - using (var muxer = GetScriptConn(allowAdmin: true)) - { - var conn = muxer.GetDatabase(); - var key = Me(); - conn.StringSet(key, "bar", flags: CommandFlags.FireAndForget); - var result = (string?)conn.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); - Assert.Equal("bar", result); + [Fact] + public void FlushDetection() + { + // we don't expect this to handle everything; we just expect it to be predictable + using var conn = GetScriptConn(allowAdmin: true); - // now cause all kinds of problems - GetServer(muxer).ScriptFlush(); + var db = conn.GetDatabase(); + var key = Me(); + db.StringSet(key, "bar", flags: CommandFlags.FireAndForget); + var result = (string?)db.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); + Assert.Equal("bar", result); - //expect this one to fail just work fine (self-fix) - conn.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); + // now cause all kinds of problems + GetServer(conn).ScriptFlush(); - result = (string?)conn.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); - Assert.Equal("bar", result); - } - } + //expect this one to fail just work fine (self-fix) + db.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); + + result = (string?)db.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); + Assert.Equal("bar", result); + } - [Fact] - public void PrepareScript() + [Fact] + public void PrepareScript() + { + string[] scripts = { "return redis.call('get', KEYS[1])", "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" }; + using (var conn = GetScriptConn(allowAdmin: true)) { - string[] scripts = { "return redis.call('get', KEYS[1])", "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" }; - using (var muxer = GetScriptConn(allowAdmin: true)) - { - var server = GetServer(muxer); - server.ScriptFlush(); + var server = GetServer(conn); + server.ScriptFlush(); - // when vanilla - server.ScriptLoad(scripts[0]); - server.ScriptLoad(scripts[1]); + // when vanilla + server.ScriptLoad(scripts[0]); + server.ScriptLoad(scripts[1]); - //when known to exist - server.ScriptLoad(scripts[0]); - server.ScriptLoad(scripts[1]); - } - using (var muxer = GetScriptConn()) - { - var server = GetServer(muxer); + //when known to exist + server.ScriptLoad(scripts[0]); + server.ScriptLoad(scripts[1]); + } + using (var conn = GetScriptConn()) + { + var server = GetServer(conn); - //when vanilla - server.ScriptLoad(scripts[0]); - server.ScriptLoad(scripts[1]); + //when vanilla + server.ScriptLoad(scripts[0]); + server.ScriptLoad(scripts[1]); - //when known to exist - server.ScriptLoad(scripts[0]); - server.ScriptLoad(scripts[1]); + //when known to exist + server.ScriptLoad(scripts[0]); + server.ScriptLoad(scripts[1]); - //when known to exist - server.ScriptLoad(scripts[0]); - server.ScriptLoad(scripts[1]); - } + //when known to exist + server.ScriptLoad(scripts[0]); + server.ScriptLoad(scripts[1]); } + } - [Fact] - public void NonAsciiScripts() - { - using (var muxer = GetScriptConn()) - { - const string evil = "return '僕'"; - var conn = muxer.GetDatabase(); - GetServer(muxer).ScriptLoad(evil); + [Fact] + public void NonAsciiScripts() + { + using var conn = GetScriptConn(); - var result = (string?)conn.ScriptEvaluate(evil, null, null); - Assert.Equal("僕", result); - } - } + const string evil = "return '僕'"; + var db = conn.GetDatabase(); + GetServer(conn).ScriptLoad(evil); - [Fact] - public async Task ScriptThrowsError() - { - await Assert.ThrowsAsync(async () => - { - using (var muxer = GetScriptConn()) - { - var conn = muxer.GetDatabase(); - try - { - await conn.ScriptEvaluateAsync("return redis.error_reply('oops')", null, null).ForAwait(); - } - catch (AggregateException ex) - { - throw ex.InnerExceptions[0]; - } - } - }).ForAwait(); - } + var result = (string?)db.ScriptEvaluate(evil, null, null); + Assert.Equal("僕", result); + } - [Fact] - public void ScriptThrowsErrorInsideTransaction() + [Fact] + public async Task ScriptThrowsError() + { + await Assert.ThrowsAsync(async () => { - using (var muxer = GetScriptConn()) + using var conn = GetScriptConn(); + + var db = conn.GetDatabase(); + try { - var key = Me(); - var conn = muxer.GetDatabase(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - var beforeTran = (string?)conn.StringGet(key); - Assert.Null(beforeTran); - var tran = conn.CreateTransaction(); - { - var a = tran.StringIncrementAsync(key); - var b = tran.ScriptEvaluateAsync("return redis.error_reply('oops')", null, null); - var c = tran.StringIncrementAsync(key); - var complete = tran.ExecuteAsync(); - - Assert.True(muxer.Wait(complete)); - Assert.True(QuickWait(a).IsCompleted, a.Status.ToString()); - Assert.True(QuickWait(c).IsCompleted, "State: " + c.Status); - Assert.Equal(1L, a.Result); - Assert.Equal(2L, c.Result); - - Assert.True(QuickWait(b).IsFaulted, "should be faulted"); - Assert.NotNull(b.Exception); - Assert.Single(b.Exception.InnerExceptions); - var ex = b.Exception.InnerExceptions.Single(); - Assert.IsType(ex); - // 7.0 slightly changes the error format, accept either. - Assert.Contains(ex.Message, new[] { "ERR oops", "oops" }); - } - var afterTran = conn.StringGetAsync(key); - Assert.Equal(2L, (long)conn.Wait(afterTran)); + await db.ScriptEvaluateAsync("return redis.error_reply('oops')", null, null).ForAwait(); } - } - private static Task QuickWait(Task task) - { - if (!task.IsCompleted) + catch (AggregateException ex) { - try { task.Wait(200); } catch { /* But don't error */ } + throw ex.InnerExceptions[0]; } - return task; - } + }).ForAwait(); + } - [Fact] - public async Task ChangeDbInScript() + [Fact] + public void ScriptThrowsErrorInsideTransaction() + { + using var conn = GetScriptConn(); + + var key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + var beforeTran = (string?)db.StringGet(key); + Assert.Null(beforeTran); + var tran = db.CreateTransaction(); { - using (var muxer = GetScriptConn()) - { - var key = Me(); - muxer.GetDatabase(1).StringSet(key, "db 1", flags: CommandFlags.FireAndForget); - muxer.GetDatabase(2).StringSet(key, "db 2", flags: CommandFlags.FireAndForget); + var a = tran.StringIncrementAsync(key); + var b = tran.ScriptEvaluateAsync("return redis.error_reply('oops')", null, null); + var c = tran.StringIncrementAsync(key); + var complete = tran.ExecuteAsync(); + + Assert.True(conn.Wait(complete)); + Assert.True(QuickWait(a).IsCompleted, a.Status.ToString()); + Assert.True(QuickWait(c).IsCompleted, "State: " + c.Status); + Assert.Equal(1L, a.Result); + Assert.Equal(2L, c.Result); + + Assert.True(QuickWait(b).IsFaulted, "should be faulted"); + Assert.NotNull(b.Exception); + Assert.Single(b.Exception.InnerExceptions); + var ex = b.Exception.InnerExceptions.Single(); + Assert.IsType(ex); + // 7.0 slightly changes the error format, accept either. + Assert.Contains(ex.Message, new[] { "ERR oops", "oops" }); + } + var afterTran = db.StringGetAsync(key); + Assert.Equal(2L, (long)db.Wait(afterTran)); + } + private static Task QuickWait(Task task) + { + if (!task.IsCompleted) + { + try { task.Wait(200); } catch { /* But don't error */ } + } + return task; + } + + [Fact] + public async Task ChangeDbInScript() + { + using var conn = GetScriptConn(); + + var key = Me(); + conn.GetDatabase(1).StringSet(key, "db 1", flags: CommandFlags.FireAndForget); + conn.GetDatabase(2).StringSet(key, "db 2", flags: CommandFlags.FireAndForget); - Log("Key: " + key); - var conn = muxer.GetDatabase(2); - var evalResult = conn.ScriptEvaluateAsync(@"redis.call('select', 1) + Log("Key: " + key); + var db = conn.GetDatabase(2); + var evalResult = db.ScriptEvaluateAsync(@"redis.call('select', 1) return redis.call('get','" + key + "')", null, null); - var getResult = conn.StringGetAsync(key); + var getResult = db.StringGetAsync(key); - Assert.Equal("db 1", (string?)await evalResult); - // now, our connection thought it was in db 2, but the script changed to db 1 - Assert.Equal("db 2", await getResult); - } - } + Assert.Equal("db 1", (string?)await evalResult); + // now, our connection thought it was in db 2, but the script changed to db 1 + Assert.Equal("db 2", await getResult); + } - [Fact] - public async Task ChangeDbInTranScript() - { - using (var muxer = GetScriptConn()) - { - var key = Me(); - muxer.GetDatabase(1).StringSet(key, "db 1", flags: CommandFlags.FireAndForget); - muxer.GetDatabase(2).StringSet(key, "db 2", flags: CommandFlags.FireAndForget); + [Fact] + public async Task ChangeDbInTranScript() + { + using var conn = GetScriptConn(); + + var key = Me(); + conn.GetDatabase(1).StringSet(key, "db 1", flags: CommandFlags.FireAndForget); + conn.GetDatabase(2).StringSet(key, "db 2", flags: CommandFlags.FireAndForget); - var conn = muxer.GetDatabase(2); - var tran = conn.CreateTransaction(); - var evalResult = tran.ScriptEvaluateAsync(@"redis.call('select', 1) + var db = conn.GetDatabase(2); + var tran = db.CreateTransaction(); + var evalResult = tran.ScriptEvaluateAsync(@"redis.call('select', 1) return redis.call('get','" + key + "')", null, null); - var getResult = tran.StringGetAsync(key); - Assert.True(tran.Execute()); + var getResult = tran.StringGetAsync(key); + Assert.True(tran.Execute()); - Assert.Equal("db 1", (string?)await evalResult); - // now, our connection thought it was in db 2, but the script changed to db 1 - Assert.Equal("db 2", await getResult); - } - } + Assert.Equal("db 1", (string?)await evalResult); + // now, our connection thought it was in db 2, but the script changed to db 1 + Assert.Equal("db 2", await getResult); + } - [Fact] - public void TestBasicScripting() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); + [Fact] + public void TestBasicScripting() + { + using var conn = Create(require: RedisFeatures.v2_6_0); - RedisValue newId = Guid.NewGuid().ToString(); - RedisKey key = Me(); - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.HashSet(key, "id", 123, flags: CommandFlags.FireAndForget); + RedisValue newId = Guid.NewGuid().ToString(); + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.HashSet(key, "id", 123, flags: CommandFlags.FireAndForget); - var wasSet = (bool)db.ScriptEvaluate("if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", - new [] { key }, new [] { newId }); + var wasSet = (bool)db.ScriptEvaluate("if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", + new[] { key }, new[] { newId }); - Assert.True(wasSet); + Assert.True(wasSet); - wasSet = (bool)db.ScriptEvaluate("if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", - new [] { key }, new [] { newId }); - Assert.False(wasSet); - } - } + wasSet = (bool)db.ScriptEvaluate("if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", + new[] { key }, new[] { newId }); + Assert.False(wasSet); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CheckLoads(bool async) + { + using var conn0 = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + using var conn1 = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + + // note that these are on different connections (so we wouldn't expect + // the flush to drop the local cache - assume it is a surprise!) + var server = conn0.GetServer(TestConfig.Current.PrimaryServerAndPort); + var db = conn1.GetDatabase(); + const string script = "return 1;"; + + // start empty + server.ScriptFlush(); + Assert.False(server.ScriptExists(script)); + + // run once, causes to be cached + Assert.True((bool)db.ScriptEvaluate(script)); + Assert.True(server.ScriptExists(script)); + + // can run again + Assert.True((bool)db.ScriptEvaluate(script)); + + // ditch the scripts; should no longer exist + db.Ping(); + server.ScriptFlush(); + Assert.False(server.ScriptExists(script)); + db.Ping(); - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task CheckLoads(bool async) + if (async) { - using (var conn0 = Create(allowAdmin: true)) - using (var conn1 = Create(allowAdmin: true)) - { - Skip.IfBelow(conn0, RedisFeatures.v2_6_0); - - // note that these are on different connections (so we wouldn't expect - // the flush to drop the local cache - assume it is a surprise!) - var server = conn0.GetServer(TestConfig.Current.PrimaryServerAndPort); - var db = conn1.GetDatabase(); - const string script = "return 1;"; - - // start empty - server.ScriptFlush(); - Assert.False(server.ScriptExists(script)); - - // run once, causes to be cached - Assert.True((bool)db.ScriptEvaluate(script)); - Assert.True(server.ScriptExists(script)); - - // can run again - Assert.True((bool)db.ScriptEvaluate(script)); - - // ditch the scripts; should no longer exist - db.Ping(); - server.ScriptFlush(); - Assert.False(server.ScriptExists(script)); - db.Ping(); - - if (async) - { - // now: fails the first time - var ex = await Assert.ThrowsAsync(async () => await db.ScriptEvaluateAsync(script).ForAwait()).ForAwait(); - Assert.Equal("NOSCRIPT No matching script. Please use EVAL.", ex.Message); - } - else - { - // just works; magic - Assert.True((bool)db.ScriptEvaluate(script)); - } - - // but gets marked as unloaded, so we can use it again... - Assert.True((bool)db.ScriptEvaluate(script)); - - // which will cause it to be cached - Assert.True(server.ScriptExists(script)); - } + // now: fails the first time + var ex = await Assert.ThrowsAsync(async () => await db.ScriptEvaluateAsync(script).ForAwait()).ForAwait(); + Assert.Equal("NOSCRIPT No matching script. Please use EVAL.", ex.Message); } - - [Fact] - public void CompareScriptToDirect() + else { - const string Script = "return redis.call('incr', KEYS[1])"; - - using (var conn = Create(allowAdmin: true)) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); - - var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); - server.ScriptFlush(); - - server.ScriptLoad(Script); - var db = conn.GetDatabase(); - db.Ping(); // k, we're all up to date now; clean db, minimal script cache - - // we're using a pipeline here, so send 1000 messages, but for timing: only care about the last - const int LOOP = 5000; - RedisKey key = Me(); - RedisKey[] keys = new[] { key }; // script takes an array - - // run via script - db.KeyDelete(key, CommandFlags.FireAndForget); - var watch = Stopwatch.StartNew(); - for (int i = 1; i < LOOP; i++) // the i=1 is to do all-but-one - { - db.ScriptEvaluate(Script, keys, flags: CommandFlags.FireAndForget); - } - var scriptResult = db.ScriptEvaluate(Script, keys); // last one we wait for (no F+F) - watch.Stop(); - TimeSpan scriptTime = watch.Elapsed; - - // run via raw op - db.KeyDelete(key, CommandFlags.FireAndForget); - watch = Stopwatch.StartNew(); - for (int i = 1; i < LOOP; i++) // the i=1 is to do all-but-one - { - db.StringIncrement(key, flags: CommandFlags.FireAndForget); - } - var directResult = db.StringIncrement(key); // last one we wait for (no F+F) - watch.Stop(); - TimeSpan directTime = watch.Elapsed; - - Assert.Equal(LOOP, (long)scriptResult); - Assert.Equal(LOOP, directResult); - - Log("script: {0}ms; direct: {1}ms", - scriptTime.TotalMilliseconds, - directTime.TotalMilliseconds); - } + // just works; magic + Assert.True((bool)db.ScriptEvaluate(script)); } - [Fact] - public void TestCallByHash() + // but gets marked as unloaded, so we can use it again... + Assert.True((bool)db.ScriptEvaluate(script)); + + // which will cause it to be cached + Assert.True(server.ScriptExists(script)); + } + + [Fact] + public void CompareScriptToDirect() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + + const string Script = "return redis.call('incr', KEYS[1])"; + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); + server.ScriptFlush(); + + server.ScriptLoad(Script); + var db = conn.GetDatabase(); + db.Ping(); // k, we're all up to date now; clean db, minimal script cache + + // we're using a pipeline here, so send 1000 messages, but for timing: only care about the last + const int LOOP = 5000; + RedisKey key = Me(); + RedisKey[] keys = new[] { key }; // script takes an array + + // run via script + db.KeyDelete(key, CommandFlags.FireAndForget); + var watch = Stopwatch.StartNew(); + for (int i = 1; i < LOOP; i++) // the i=1 is to do all-but-one { - const string Script = "return redis.call('incr', KEYS[1])"; + db.ScriptEvaluate(Script, keys, flags: CommandFlags.FireAndForget); + } + var scriptResult = db.ScriptEvaluate(Script, keys); // last one we wait for (no F+F) + watch.Stop(); + TimeSpan scriptTime = watch.Elapsed; + + // run via raw op + db.KeyDelete(key, CommandFlags.FireAndForget); + watch = Stopwatch.StartNew(); + for (int i = 1; i < LOOP; i++) // the i=1 is to do all-but-one + { + db.StringIncrement(key, flags: CommandFlags.FireAndForget); + } + var directResult = db.StringIncrement(key); // last one we wait for (no F+F) + watch.Stop(); + TimeSpan directTime = watch.Elapsed; - using (var conn = Create(allowAdmin: true)) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); + Assert.Equal(LOOP, (long)scriptResult); + Assert.Equal(LOOP, directResult); - var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); - server.ScriptFlush(); + Log("script: {0}ms; direct: {1}ms", + scriptTime.TotalMilliseconds, + directTime.TotalMilliseconds); + } - byte[]? hash = server.ScriptLoad(Script); - Assert.NotNull(hash); + [Fact] + public void TestCallByHash() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); - var db = conn.GetDatabase(); - var key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - RedisKey[] keys = { key }; + const string Script = "return redis.call('incr', KEYS[1])"; + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); + server.ScriptFlush(); - string hexHash = string.Concat(hash.Select(x => x.ToString("X2"))); - Assert.Equal("2BAB3B661081DB58BD2341920E0BA7CF5DC77B25", hexHash); + byte[]? hash = server.ScriptLoad(Script); + Assert.NotNull(hash); - db.ScriptEvaluate(hexHash, keys, flags: CommandFlags.FireAndForget); - db.ScriptEvaluate(hash, keys, flags: CommandFlags.FireAndForget); + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + RedisKey[] keys = { key }; - var count = (int)db.StringGet(keys)[0]; - Assert.Equal(2, count); - } + string hexHash = string.Concat(hash.Select(x => x.ToString("X2"))); + Assert.Equal("2BAB3B661081DB58BD2341920E0BA7CF5DC77B25", hexHash); + + db.ScriptEvaluate(hexHash, keys, flags: CommandFlags.FireAndForget); + db.ScriptEvaluate(hash, keys, flags: CommandFlags.FireAndForget); + + var count = (int)db.StringGet(keys)[0]; + Assert.Equal(2, count); + } + + [Fact] + public void SimpleLuaScript() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + + const string Script = "return @ident"; + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); + server.ScriptFlush(); + + var prepared = LuaScript.Prepare(Script); + + var db = conn.GetDatabase(); + + { + var val = prepared.Evaluate(db, new { ident = "hello" }); + Assert.Equal("hello", (string?)val); } - [Fact] - public void SimpleLuaScript() { - const string Script = "return @ident"; + var val = prepared.Evaluate(db, new { ident = 123 }); + Assert.Equal(123, (int)val); + } - using (var conn = Create(allowAdmin: true)) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); - - var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); - server.ScriptFlush(); - - var prepared = LuaScript.Prepare(Script); - - var db = conn.GetDatabase(); - - { - var val = prepared.Evaluate(db, new { ident = "hello" }); - Assert.Equal("hello", (string?)val); - } - - { - var val = prepared.Evaluate(db, new { ident = 123 }); - Assert.Equal(123, (int)val); - } - - { - var val = prepared.Evaluate(db, new { ident = 123L }); - Assert.Equal(123L, (long)val); - } - - { - var val = prepared.Evaluate(db, new { ident = 1.1 }); - Assert.Equal(1.1, (double)val); - } - - { - var val = prepared.Evaluate(db, new { ident = true }); - Assert.True((bool)val); - } - - { - var val = prepared.Evaluate(db, new { ident = new byte[] { 4, 5, 6 } }); - var valArray = (byte[]?)val; - Assert.NotNull(valArray); - Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); - } - - { - var val = prepared.Evaluate(db, new { ident = new ReadOnlyMemory(new byte[] { 4, 5, 6 }) }); - var valArray = (byte[]?)val; - Assert.NotNull(valArray); - Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); - } - } + { + var val = prepared.Evaluate(db, new { ident = 123L }); + Assert.Equal(123L, (long)val); } - [Fact] - public void SimpleRawScriptEvaluate() { - const string Script = "return ARGV[1]"; + var val = prepared.Evaluate(db, new { ident = 1.1 }); + Assert.Equal(1.1, (double)val); + } - using (var conn = Create(allowAdmin: true)) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); - - var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); - server.ScriptFlush(); - - var db = conn.GetDatabase(); - - { - var val = db.ScriptEvaluate(Script, values: new RedisValue[] { "hello" }); - Assert.Equal("hello", (string?)val); - } - - { - var val = db.ScriptEvaluate(Script, values: new RedisValue[] { 123 }); - Assert.Equal(123, (int)val); - } - - { - var val = db.ScriptEvaluate(Script, values: new RedisValue[] { 123L }); - Assert.Equal(123L, (long)val); - } - - { - var val = db.ScriptEvaluate(Script, values: new RedisValue[] { 1.1 }); - Assert.Equal(1.1, (double)val); - } - - { - var val = db.ScriptEvaluate(Script, values: new RedisValue[] { true }); - Assert.True((bool)val); - } - - { - var val = db.ScriptEvaluate(Script, values: new RedisValue[] { new byte[] { 4, 5, 6 } }); - var valArray = (byte[]?)val; - Assert.NotNull(valArray); - Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); - } - - { - var val = db.ScriptEvaluate(Script, values: new RedisValue[] { new ReadOnlyMemory(new byte[] { 4, 5, 6 }) }); - var valArray = (byte[]?)val; - Assert.NotNull(valArray); - Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); - } - } + { + var val = prepared.Evaluate(db, new { ident = true }); + Assert.True((bool)val); } - [Fact] - public void LuaScriptWithKeys() { - const string Script = "redis.call('set', @key, @value)"; + var val = prepared.Evaluate(db, new { ident = new byte[] { 4, 5, 6 } }); + var valArray = (byte[]?)val; + Assert.NotNull(valArray); + Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); + } - using (var conn = Create(allowAdmin: true)) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); + { + var val = prepared.Evaluate(db, new { ident = new ReadOnlyMemory(new byte[] { 4, 5, 6 }) }); + var valArray = (byte[]?)val; + Assert.NotNull(valArray); + Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); + } + } - var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); - server.ScriptFlush(); + [Fact] + public void SimpleRawScriptEvaluate() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); - var script = LuaScript.Prepare(Script); + const string Script = "return ARGV[1]"; + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); + server.ScriptFlush(); - var db = conn.GetDatabase(); - var key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); + var db = conn.GetDatabase(); - var p = new { key = (RedisKey)key, value = 123 }; + { + var val = db.ScriptEvaluate(Script, values: new RedisValue[] { "hello" }); + Assert.Equal("hello", (string?)val); + } - script.Evaluate(db, p); - var val = db.StringGet(key); - Assert.Equal(123, (int)val); + { + var val = db.ScriptEvaluate(Script, values: new RedisValue[] { 123 }); + Assert.Equal(123, (int)val); + } - // no super clean way to extract this; so just abuse InternalsVisibleTo - script.ExtractParameters(p, null, out RedisKey[]? keys, out _); - Assert.NotNull(keys); - Assert.Single(keys); - Assert.Equal(key, keys[0]); - } + { + var val = db.ScriptEvaluate(Script, values: new RedisValue[] { 123L }); + Assert.Equal(123L, (long)val); } - [Fact] - public void NoInlineReplacement() { - const string Script = "redis.call('set', @key, 'hello@example')"; - using (var conn = Create(allowAdmin: true)) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var val = db.ScriptEvaluate(Script, values: new RedisValue[] { 1.1 }); + Assert.Equal(1.1, (double)val); + } - var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); - server.ScriptFlush(); + { + var val = db.ScriptEvaluate(Script, values: new RedisValue[] { true }); + Assert.True((bool)val); + } - var script = LuaScript.Prepare(Script); + { + var val = db.ScriptEvaluate(Script, values: new RedisValue[] { new byte[] { 4, 5, 6 } }); + var valArray = (byte[]?)val; + Assert.NotNull(valArray); + Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); + } - Assert.Equal("redis.call('set', ARGV[1], 'hello@example')", script.ExecutableScript); + { + var val = db.ScriptEvaluate(Script, values: new RedisValue[] { new ReadOnlyMemory(new byte[] { 4, 5, 6 }) }); + var valArray = (byte[]?)val; + Assert.NotNull(valArray); + Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); + } + } - var db = conn.GetDatabase(); - var key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); + [Fact] + public void LuaScriptWithKeys() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); - var p = new { key }; + const string Script = "redis.call('set', @key, @value)"; + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); + server.ScriptFlush(); - script.Evaluate(db, p, flags: CommandFlags.FireAndForget); - var val = db.StringGet(key); - Assert.Equal("hello@example", val); - } - } + var script = LuaScript.Prepare(Script); - [Fact] - public void EscapeReplacement() - { - const string Script = "redis.call('set', @key, @@escapeMe)"; - var script = LuaScript.Prepare(Script); + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - Assert.Equal("redis.call('set', ARGV[1], @escapeMe)", script.ExecutableScript); - } + var p = new { key = (RedisKey)key, value = 123 }; - [Fact] - public void SimpleLoadedLuaScript() - { - const string Script = "return @ident"; + script.Evaluate(db, p); + var val = db.StringGet(key); + Assert.Equal(123, (int)val); - using (var conn = Create(allowAdmin: true)) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); - - var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); - server.ScriptFlush(); - - var prepared = LuaScript.Prepare(Script); - var loaded = prepared.Load(server); - - var db = conn.GetDatabase(); - - { - var val = loaded.Evaluate(db, new { ident = "hello" }); - Assert.Equal("hello", (string?)val); - } - - { - var val = loaded.Evaluate(db, new { ident = 123 }); - Assert.Equal(123, (int)val); - } - - { - var val = loaded.Evaluate(db, new { ident = 123L }); - Assert.Equal(123L, (long)val); - } - - { - var val = loaded.Evaluate(db, new { ident = 1.1 }); - Assert.Equal(1.1, (double)val); - } - - { - var val = loaded.Evaluate(db, new { ident = true }); - Assert.True((bool)val); - } - - { - var val = loaded.Evaluate(db, new { ident = new byte[] { 4, 5, 6 } }); - var valArray = (byte[]?)val; - Assert.NotNull(valArray); - Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); - } - - { - var val = loaded.Evaluate(db, new { ident = new ReadOnlyMemory(new byte[] { 4, 5, 6 }) }); - var valArray = (byte[]?)val; - Assert.NotNull(valArray); - Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); - } - } - } + // no super clean way to extract this; so just abuse InternalsVisibleTo + script.ExtractParameters(p, null, out RedisKey[]? keys, out _); + Assert.NotNull(keys); + Assert.Single(keys); + Assert.Equal(key, keys[0]); + } - [Fact] - public void LoadedLuaScriptWithKeys() - { - const string Script = "redis.call('set', @key, @value)"; + [Fact] + public void NoInlineReplacement() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); - using (var conn = Create(allowAdmin: true)) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); + const string Script = "redis.call('set', @key, 'hello@example')"; + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); + server.ScriptFlush(); - var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); - server.ScriptFlush(); + var script = LuaScript.Prepare(Script); - var script = LuaScript.Prepare(Script); - var prepared = script.Load(server); + Assert.Equal("redis.call('set', ARGV[1], 'hello@example')", script.ExecutableScript); - var db = conn.GetDatabase(); - var key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - var p = new { key = (RedisKey)key, value = 123 }; + var p = new { key }; - prepared.Evaluate(db, p, flags: CommandFlags.FireAndForget); - var val = db.StringGet(key); - Assert.Equal(123, (int)val); + script.Evaluate(db, p, flags: CommandFlags.FireAndForget); + var val = db.StringGet(key); + Assert.Equal("hello@example", val); + } - // no super clean way to extract this; so just abuse InternalsVisibleTo - prepared.Original.ExtractParameters(p, null, out RedisKey[]? keys, out _); - Assert.NotNull(keys); - Assert.Single(keys); - Assert.Equal(key, keys[0]); - } - } + [Fact] + public void EscapeReplacement() + { + const string Script = "redis.call('set', @key, @@escapeMe)"; + var script = LuaScript.Prepare(Script); - [Fact] - public void PurgeLuaScriptCache() - { - const string Script = "redis.call('set', @PurgeLuaScriptCacheKey, @PurgeLuaScriptCacheValue)"; - var first = LuaScript.Prepare(Script); - var fromCache = LuaScript.Prepare(Script); + Assert.Equal("redis.call('set', ARGV[1], @escapeMe)", script.ExecutableScript); + } - Assert.True(ReferenceEquals(first, fromCache)); + [Fact] + public void SimpleLoadedLuaScript() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + + const string Script = "return @ident"; + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); + server.ScriptFlush(); - LuaScript.PurgeCache(); - var shouldBeNew = LuaScript.Prepare(Script); + var prepared = LuaScript.Prepare(Script); + var loaded = prepared.Load(server); - Assert.False(ReferenceEquals(first, shouldBeNew)); + var db = conn.GetDatabase(); + + { + var val = loaded.Evaluate(db, new { ident = "hello" }); + Assert.Equal("hello", (string?)val); } - private static void PurgeLuaScriptOnFinalizeImpl(string script) { - var first = LuaScript.Prepare(script); - var fromCache = LuaScript.Prepare(script); - Assert.True(ReferenceEquals(first, fromCache)); - Assert.Equal(1, LuaScript.GetCachedScriptCount()); + var val = loaded.Evaluate(db, new { ident = 123 }); + Assert.Equal(123, (int)val); } - [FactLongRunning] - public void PurgeLuaScriptOnFinalize() { - const string Script = "redis.call('set', @PurgeLuaScriptOnFinalizeKey, @PurgeLuaScriptOnFinalizeValue)"; - LuaScript.PurgeCache(); - Assert.Equal(0, LuaScript.GetCachedScriptCount()); + var val = loaded.Evaluate(db, new { ident = 123L }); + Assert.Equal(123L, (long)val); + } - // This has to be a separate method to guarantee that the created LuaScript objects go out of scope, - // and are thus available to be GC'd - PurgeLuaScriptOnFinalizeImpl(Script); - CollectGarbage(); + { + var val = loaded.Evaluate(db, new { ident = 1.1 }); + Assert.Equal(1.1, (double)val); + } - Assert.Equal(0, LuaScript.GetCachedScriptCount()); + { + var val = loaded.Evaluate(db, new { ident = true }); + Assert.True((bool)val); + } - LuaScript.Prepare(Script); - Assert.Equal(1, LuaScript.GetCachedScriptCount()); + { + var val = loaded.Evaluate(db, new { ident = new byte[] { 4, 5, 6 } }); + var valArray = (byte[]?)val; + Assert.NotNull(valArray); + Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); } - [Fact] - public void IDatabaseLuaScriptConvenienceMethods() { - const string Script = "redis.call('set', @key, @value)"; + var val = loaded.Evaluate(db, new { ident = new ReadOnlyMemory(new byte[] { 4, 5, 6 }) }); + var valArray = (byte[]?)val; + Assert.NotNull(valArray); + Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); + } + } - using (var conn = Create(allowAdmin: true)) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); + [Fact] + public void LoadedLuaScriptWithKeys() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); - var script = LuaScript.Prepare(Script); - var db = conn.GetDatabase(); - var key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.ScriptEvaluate(script, new { key = (RedisKey)key, value = "value" }, flags: CommandFlags.FireAndForget); - var val = db.StringGet(key); - Assert.Equal("value", val); + const string Script = "redis.call('set', @key, @value)"; + var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); + server.ScriptFlush(); - var prepared = script.Load(conn.GetServer(conn.GetEndPoints()[0])); + var script = LuaScript.Prepare(Script); + var prepared = script.Load(server); - db.ScriptEvaluate(prepared, new { key = (RedisKey)(key + "2"), value = "value2" }, flags: CommandFlags.FireAndForget); - var val2 = db.StringGet(key + "2"); - Assert.Equal("value2", val2); - } - } + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - [Fact] - public void IServerLuaScriptConvenienceMethods() - { - const string Script = "redis.call('set', @key, @value)"; + var p = new { key = (RedisKey)key, value = 123 }; - using (var conn = Create(allowAdmin: true)) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); + prepared.Evaluate(db, p, flags: CommandFlags.FireAndForget); + var val = db.StringGet(key); + Assert.Equal(123, (int)val); - var script = LuaScript.Prepare(Script); - var server = conn.GetServer(conn.GetEndPoints()[0]); - var db = conn.GetDatabase(); - var key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); + // no super clean way to extract this; so just abuse InternalsVisibleTo + prepared.Original.ExtractParameters(p, null, out RedisKey[]? keys, out _); + Assert.NotNull(keys); + Assert.Single(keys); + Assert.Equal(key, keys[0]); + } - var prepared = server.ScriptLoad(script); + [Fact] + public void PurgeLuaScriptCache() + { + const string Script = "redis.call('set', @PurgeLuaScriptCacheKey, @PurgeLuaScriptCacheValue)"; + var first = LuaScript.Prepare(Script); + var fromCache = LuaScript.Prepare(Script); - db.ScriptEvaluate(prepared, new { key = (RedisKey)key, value = "value3" }); - var val = db.StringGet(key); - Assert.Equal("value3", val); - } - } + Assert.True(ReferenceEquals(first, fromCache)); - [Fact] - public void LuaScriptPrefixedKeys() - { - const string Script = "redis.call('set', @key, @value)"; - var prepared = LuaScript.Prepare(Script); - var key = Me(); - var p = new { key = (RedisKey)key, value = "hello" }; - - // no super clean way to extract this; so just abuse InternalsVisibleTo - prepared.ExtractParameters(p, "prefix-", out RedisKey[]? keys, out RedisValue[]? args); - Assert.NotNull(keys); - Assert.Single(keys); - Assert.Equal("prefix-" + key, keys[0]); - Assert.NotNull(args); - Assert.Equal(2, args.Length); - Assert.Equal("prefix-" + key, args[0]); - Assert.Equal("hello", args[1]); - } + LuaScript.PurgeCache(); + var shouldBeNew = LuaScript.Prepare(Script); - [Fact] - public void LuaScriptWithWrappedDatabase() - { - const string Script = "redis.call('set', @key, @value)"; + Assert.False(ReferenceEquals(first, shouldBeNew)); + } - using (var conn = Create(allowAdmin: true)) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); + private static void PurgeLuaScriptOnFinalizeImpl(string script) + { + var first = LuaScript.Prepare(script); + var fromCache = LuaScript.Prepare(script); + Assert.True(ReferenceEquals(first, fromCache)); + Assert.Equal(1, LuaScript.GetCachedScriptCount()); + } - var db = conn.GetDatabase(); - var wrappedDb = db.WithKeyPrefix("prefix-"); - var key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); + [FactLongRunning] + public void PurgeLuaScriptOnFinalize() + { + const string Script = "redis.call('set', @PurgeLuaScriptOnFinalizeKey, @PurgeLuaScriptOnFinalizeValue)"; + LuaScript.PurgeCache(); + Assert.Equal(0, LuaScript.GetCachedScriptCount()); - var prepared = LuaScript.Prepare(Script); - wrappedDb.ScriptEvaluate(prepared, new { key = (RedisKey)key, value = 123 }); - var val1 = wrappedDb.StringGet(key); - Assert.Equal(123, (int)val1); + // This has to be a separate method to guarantee that the created LuaScript objects go out of scope, + // and are thus available to be GC'd + PurgeLuaScriptOnFinalizeImpl(Script); + CollectGarbage(); - var val2 = db.StringGet("prefix-" + key); - Assert.Equal(123, (int)val2); + Assert.Equal(0, LuaScript.GetCachedScriptCount()); - var val3 = db.StringGet(key); - Assert.True(val3.IsNull); - } - } + LuaScript.Prepare(Script); + Assert.Equal(1, LuaScript.GetCachedScriptCount()); + } - [Fact] - public async Task AsyncLuaScriptWithWrappedDatabase() - { - const string Script = "redis.call('set', @key, @value)"; + [Fact] + public void IDatabaseLuaScriptConvenienceMethods() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + + const string Script = "redis.call('set', @key, @value)"; + var script = LuaScript.Prepare(Script); + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.ScriptEvaluate(script, new { key = (RedisKey)key, value = "value" }, flags: CommandFlags.FireAndForget); + var val = db.StringGet(key); + Assert.Equal("value", val); + + var prepared = script.Load(conn.GetServer(conn.GetEndPoints()[0])); + + db.ScriptEvaluate(prepared, new { key = (RedisKey)(key + "2"), value = "value2" }, flags: CommandFlags.FireAndForget); + var val2 = db.StringGet(key + "2"); + Assert.Equal("value2", val2); + } - using (var conn = Create(allowAdmin: true)) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); + [Fact] + public void IServerLuaScriptConvenienceMethods() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); - var db = conn.GetDatabase(); - var wrappedDb = db.WithKeyPrefix("prefix-"); - var key = Me(); - await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + const string Script = "redis.call('set', @key, @value)"; + var script = LuaScript.Prepare(Script); + var server = conn.GetServer(conn.GetEndPoints()[0]); + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - var prepared = LuaScript.Prepare(Script); - await wrappedDb.ScriptEvaluateAsync(prepared, new { key = (RedisKey)key, value = 123 }); - var val1 = await wrappedDb.StringGetAsync(key); - Assert.Equal(123, (int)val1); + var prepared = server.ScriptLoad(script); - var val2 = await db.StringGetAsync("prefix-" + key); - Assert.Equal(123, (int)val2); + db.ScriptEvaluate(prepared, new { key = (RedisKey)key, value = "value3" }); + var val = db.StringGet(key); + Assert.Equal("value3", val); + } - var val3 = await db.StringGetAsync(key); - Assert.True(val3.IsNull); - } - } + [Fact] + public void LuaScriptPrefixedKeys() + { + const string Script = "redis.call('set', @key, @value)"; + var prepared = LuaScript.Prepare(Script); + var key = Me(); + var p = new { key = (RedisKey)key, value = "hello" }; + + // no super clean way to extract this; so just abuse InternalsVisibleTo + prepared.ExtractParameters(p, "prefix-", out RedisKey[]? keys, out RedisValue[]? args); + Assert.NotNull(keys); + Assert.Single(keys); + Assert.Equal("prefix-" + key, keys[0]); + Assert.NotNull(args); + Assert.Equal(2, args.Length); + Assert.Equal("prefix-" + key, args[0]); + Assert.Equal("hello", args[1]); + } - [Fact] - public void LoadedLuaScriptWithWrappedDatabase() - { - const string Script = "redis.call('set', @key, @value)"; + [Fact] + public void LuaScriptWithWrappedDatabase() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); - using (var conn = Create(allowAdmin: true)) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); + const string Script = "redis.call('set', @key, @value)"; + var db = conn.GetDatabase(); + var wrappedDb = db.WithKeyPrefix("prefix-"); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - var db = conn.GetDatabase(); - var wrappedDb = db.WithKeyPrefix("prefix2-"); - var key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); + var prepared = LuaScript.Prepare(Script); + wrappedDb.ScriptEvaluate(prepared, new { key = (RedisKey)key, value = 123 }); + var val1 = wrappedDb.StringGet(key); + Assert.Equal(123, (int)val1); - var server = conn.GetServer(conn.GetEndPoints()[0]); - var prepared = LuaScript.Prepare(Script).Load(server); - wrappedDb.ScriptEvaluate(prepared, new { key = (RedisKey)key, value = 123 }, flags: CommandFlags.FireAndForget); - var val1 = wrappedDb.StringGet(key); - Assert.Equal(123, (int)val1); + var val2 = db.StringGet("prefix-" + key); + Assert.Equal(123, (int)val2); - var val2 = db.StringGet("prefix2-" + key); - Assert.Equal(123, (int)val2); + var val3 = db.StringGet(key); + Assert.True(val3.IsNull); + } - var val3 = db.StringGet(key); - Assert.True(val3.IsNull); - } - } + [Fact] + public async Task AsyncLuaScriptWithWrappedDatabase() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); - [Fact] - public async Task AsyncLoadedLuaScriptWithWrappedDatabase() - { - const string Script = "redis.call('set', @key, @value)"; + const string Script = "redis.call('set', @key, @value)"; + var db = conn.GetDatabase(); + var wrappedDb = db.WithKeyPrefix("prefix-"); + var key = Me(); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - using (var conn = Create(allowAdmin: true)) - { - Skip.IfBelow(conn, RedisFeatures.v2_6_0); + var prepared = LuaScript.Prepare(Script); + await wrappedDb.ScriptEvaluateAsync(prepared, new { key = (RedisKey)key, value = 123 }); + var val1 = await wrappedDb.StringGetAsync(key); + Assert.Equal(123, (int)val1); - var db = conn.GetDatabase(); - var wrappedDb = db.WithKeyPrefix("prefix2-"); - var key = Me(); - await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + var val2 = await db.StringGetAsync("prefix-" + key); + Assert.Equal(123, (int)val2); - var server = conn.GetServer(conn.GetEndPoints()[0]); - var prepared = await LuaScript.Prepare(Script).LoadAsync(server); - await wrappedDb.ScriptEvaluateAsync(prepared, new { key = (RedisKey)key, value = 123 }, flags: CommandFlags.FireAndForget); - var val1 = await wrappedDb.StringGetAsync(key); - Assert.Equal(123, (int)val1); + var val3 = await db.StringGetAsync(key); + Assert.True(val3.IsNull); + } - var val2 = await db.StringGetAsync("prefix2-" + key); - Assert.Equal(123, (int)val2); + [Fact] + public void LoadedLuaScriptWithWrappedDatabase() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); - var val3 = await db.StringGetAsync(key); - Assert.True(val3.IsNull); - } - } + const string Script = "redis.call('set', @key, @value)"; + var db = conn.GetDatabase(); + var wrappedDb = db.WithKeyPrefix("prefix2-"); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - [Fact] - public void ScriptWithKeyPrefixViaTokens() - { - using (var conn = Create()) - { - var p = conn.GetDatabase().WithKeyPrefix("prefix/"); + var server = conn.GetServer(conn.GetEndPoints()[0]); + var prepared = LuaScript.Prepare(Script).Load(server); + wrappedDb.ScriptEvaluate(prepared, new { key = (RedisKey)key, value = 123 }, flags: CommandFlags.FireAndForget); + var val1 = wrappedDb.StringGet(key); + Assert.Equal(123, (int)val1); + + var val2 = db.StringGet("prefix2-" + key); + Assert.Equal(123, (int)val2); - var args = new { x = "abc", y = (RedisKey)"def", z = 123 }; - var script = LuaScript.Prepare(@" + var val3 = db.StringGet(key); + Assert.True(val3.IsNull); + } + + [Fact] + public async Task AsyncLoadedLuaScriptWithWrappedDatabase() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + + const string Script = "redis.call('set', @key, @value)"; + var db = conn.GetDatabase(); + var wrappedDb = db.WithKeyPrefix("prefix2-"); + var key = Me(); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var server = conn.GetServer(conn.GetEndPoints()[0]); + var prepared = await LuaScript.Prepare(Script).LoadAsync(server); + await wrappedDb.ScriptEvaluateAsync(prepared, new { key = (RedisKey)key, value = 123 }, flags: CommandFlags.FireAndForget); + var val1 = await wrappedDb.StringGetAsync(key); + Assert.Equal(123, (int)val1); + + var val2 = await db.StringGetAsync("prefix2-" + key); + Assert.Equal(123, (int)val2); + + var val3 = await db.StringGetAsync(key); + Assert.True(val3.IsNull); + } + + [Fact] + public void ScriptWithKeyPrefixViaTokens() + { + using var conn = Create(); + + var p = conn.GetDatabase().WithKeyPrefix("prefix/"); + + var args = new { x = "abc", y = (RedisKey)"def", z = 123 }; + var script = LuaScript.Prepare(@" local arr = {}; arr[1] = @x; arr[2] = @y; arr[3] = @z; return arr; "); - var result = (RedisValue[]?)p.ScriptEvaluate(script, args); - Assert.NotNull(result); - Assert.Equal("abc", result[0]); - Assert.Equal("prefix/def", result[1]); - Assert.Equal("123", result[2]); - } - } + var result = (RedisValue[]?)p.ScriptEvaluate(script, args); + Assert.NotNull(result); + Assert.Equal("abc", result[0]); + Assert.Equal("prefix/def", result[1]); + Assert.Equal("123", result[2]); + } - [Fact] - public void ScriptWithKeyPrefixViaArrays() - { - using (var conn = Create()) - { - var p = conn.GetDatabase().WithKeyPrefix("prefix/"); + [Fact] + public void ScriptWithKeyPrefixViaArrays() + { + using var conn = Create(); + + var p = conn.GetDatabase().WithKeyPrefix("prefix/"); - const string script = @" + const string script = @" local arr = {}; arr[1] = ARGV[1]; arr[2] = KEYS[1]; arr[3] = ARGV[2]; return arr; "; - var result = (RedisValue[]?)p.ScriptEvaluate(script, new RedisKey[] { "def" }, new RedisValue[] { "abc", 123 }); - Assert.NotNull(result); - Assert.Equal("abc", result[0]); - Assert.Equal("prefix/def", result[1]); - Assert.Equal("123", result[2]); - } - } + var result = (RedisValue[]?)p.ScriptEvaluate(script, new RedisKey[] { "def" }, new RedisValue[] { "abc", 123 }); + Assert.NotNull(result); + Assert.Equal("abc", result[0]); + Assert.Equal("prefix/def", result[1]); + Assert.Equal("123", result[2]); + } - [Fact] - public void ScriptWithKeyPrefixCompare() - { - using (var conn = Create()) - { - var p = conn.GetDatabase().WithKeyPrefix("prefix/"); - var args = new { k = (RedisKey)"key", s = "str", v = 123 }; - LuaScript lua = LuaScript.Prepare("return {@k, @s, @v}"); - var viaArgs = (RedisValue[]?)p.ScriptEvaluate(lua, args); - - var viaArr = (RedisValue[]?)p.ScriptEvaluate("return {KEYS[1], ARGV[1], ARGV[2]}", new[] { args.k }, new RedisValue[] { args.s, args.v }); - Assert.NotNull(viaArr); - Assert.NotNull(viaArgs); - Assert.Equal(string.Join(",", viaArr), string.Join(",", viaArgs)); - } - } + [Fact] + public void ScriptWithKeyPrefixCompare() + { + using var conn = Create(); - [Fact] - public void RedisResultUnderstandsNullArrayArray() => TestNullArray(RedisResult.NullArray); - [Fact] - public void RedisResultUnderstandsNullArrayNull() => TestNullArray(null); + var p = conn.GetDatabase().WithKeyPrefix("prefix/"); + var args = new { k = (RedisKey)"key", s = "str", v = 123 }; + LuaScript lua = LuaScript.Prepare("return {@k, @s, @v}"); + var viaArgs = (RedisValue[]?)p.ScriptEvaluate(lua, args); - private static void TestNullArray(RedisResult? value) - { - Assert.True(value == null || value.IsNull); - - Assert.Null((RedisValue[]?)value); - Assert.Null((RedisKey[]?)value); - Assert.Null((bool[]?)value); - Assert.Null((long[]?)value); - Assert.Null((ulong[]?)value); - Assert.Null((string[]?)value); - Assert.Null((int[]?)value); - Assert.Null((double[]?)value); - Assert.Null((byte[][]?)value); - Assert.Null((RedisResult[]?)value); - } + var viaArr = (RedisValue[]?)p.ScriptEvaluate("return {KEYS[1], ARGV[1], ARGV[2]}", new[] { args.k }, new RedisValue[] { args.s, args.v }); + Assert.NotNull(viaArr); + Assert.NotNull(viaArgs); + Assert.Equal(string.Join(",", viaArr), string.Join(",", viaArgs)); + } - [Fact] - public void RedisResultUnderstandsNullNull() => TestNullValue(null); - [Fact] - public void RedisResultUnderstandsNullValue() => TestNullValue(RedisResult.Create(RedisValue.Null, ResultType.None)); + [Fact] + public void RedisResultUnderstandsNullArrayArray() => TestNullArray(RedisResult.NullArray); + [Fact] + public void RedisResultUnderstandsNullArrayNull() => TestNullArray(null); - private static void TestNullValue(RedisResult? value) - { - Assert.True(value == null || value.IsNull); - - Assert.True(((RedisValue)value).IsNull); - Assert.True(((RedisKey)value).IsNull); - Assert.Null((bool?)value); - Assert.Null((long?)value); - Assert.Null((ulong?)value); - Assert.Null((string?)value); - Assert.Null((double?)value); - Assert.Null((byte[]?)value); - } + private static void TestNullArray(RedisResult? value) + { + Assert.True(value == null || value.IsNull); + + Assert.Null((RedisValue[]?)value); + Assert.Null((RedisKey[]?)value); + Assert.Null((bool[]?)value); + Assert.Null((long[]?)value); + Assert.Null((ulong[]?)value); + Assert.Null((string[]?)value); + Assert.Null((int[]?)value); + Assert.Null((double[]?)value); + Assert.Null((byte[][]?)value); + Assert.Null((RedisResult[]?)value); + } + + [Fact] + public void RedisResultUnderstandsNullNull() => TestNullValue(null); + [Fact] + public void RedisResultUnderstandsNullValue() => TestNullValue(RedisResult.Create(RedisValue.Null, ResultType.None)); + + private static void TestNullValue(RedisResult? value) + { + Assert.True(value == null || value.IsNull); + + Assert.True(((RedisValue)value).IsNull); + Assert.True(((RedisKey)value).IsNull); + Assert.Null((bool?)value); + Assert.Null((long?)value); + Assert.Null((ulong?)value); + Assert.Null((string?)value); + Assert.Null((double?)value); + Assert.Null((byte[]?)value); } } diff --git a/tests/StackExchange.Redis.Tests/Secure.cs b/tests/StackExchange.Redis.Tests/Secure.cs index 79cba81b8..fe4418c69 100644 --- a/tests/StackExchange.Redis.Tests/Secure.cs +++ b/tests/StackExchange.Redis.Tests/Secure.cs @@ -3,80 +3,77 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(NonParallelCollection.Name)] +public class Secure : TestBase { - [Collection(NonParallelCollection.Name)] - public class Secure : TestBase - { - protected override string GetConfiguration() => - TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword + ",name=MyClient"; + protected override string GetConfiguration() => + TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword + ",name=MyClient"; - public Secure(ITestOutputHelper output) : base (output) { } + public Secure(ITestOutputHelper output) : base (output) { } - [Fact] - public void MassiveBulkOpsFireAndForgetSecure() - { - using (var muxer = Create()) - { - RedisKey key = Me(); - var conn = muxer.GetDatabase(); - conn.Ping(); + [Fact] + public void MassiveBulkOpsFireAndForgetSecure() + { + using var conn = Create(); - var watch = Stopwatch.StartNew(); + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.Ping(); - for (int i = 0; i <= AsyncOpsQty; i++) - { - conn.StringSet(key, i, flags: CommandFlags.FireAndForget); - } - int val = (int)conn.StringGet(key); - Assert.Equal(AsyncOpsQty, val); - watch.Stop(); - Log("{2}: Time for {0} ops: {1}ms (any order); ops/s: {3}", AsyncOpsQty, watch.ElapsedMilliseconds, Me(), - AsyncOpsQty / watch.Elapsed.TotalSeconds); - } - } + var watch = Stopwatch.StartNew(); - [Fact] - public void CheckConfig() + for (int i = 0; i <= AsyncOpsQty; i++) { - var config = ConfigurationOptions.Parse(GetConfiguration()); - foreach (var ep in config.EndPoints) - { - Log(ep.ToString()); - } - Assert.Single(config.EndPoints); - Assert.Equal("changeme", config.Password); + db.StringSet(key, i, flags: CommandFlags.FireAndForget); } + int val = (int)db.StringGet(key); + Assert.Equal(AsyncOpsQty, val); + watch.Stop(); + Log("{2}: Time for {0} ops: {1}ms (any order); ops/s: {3}", AsyncOpsQty, watch.ElapsedMilliseconds, Me(), + AsyncOpsQty / watch.Elapsed.TotalSeconds); + } - [Fact] - public void Connect() + [Fact] + public void CheckConfig() + { + var config = ConfigurationOptions.Parse(GetConfiguration()); + foreach (var ep in config.EndPoints) { - using (var server = Create()) - { - server.GetDatabase().Ping(); - } + Log(ep.ToString()); } + Assert.Single(config.EndPoints); + Assert.Equal("changeme", config.Password); + } - [Theory] - [InlineData("wrong")] - [InlineData("")] - public async Task ConnectWithWrongPassword(string password) + [Fact] + public void Connect() + { + using var server = Create(); + + server.GetDatabase().Ping(); + } + + [Theory] + [InlineData("wrong")] + [InlineData("")] + public async Task ConnectWithWrongPassword(string password) + { + var config = ConfigurationOptions.Parse(GetConfiguration()); + config.Password = password; + config.ConnectRetry = 0; // we don't want to retry on closed sockets in this case. + config.BacklogPolicy = BacklogPolicy.FailFast; + + var ex = await Assert.ThrowsAsync(async () => { - var config = ConfigurationOptions.Parse(GetConfiguration()); - config.Password = password; - config.ConnectRetry = 0; // we don't want to retry on closed sockets in this case. - config.BacklogPolicy = BacklogPolicy.FailFast; + SetExpectedAmbientFailureCount(-1); - var ex = await Assert.ThrowsAsync(async () => - { - SetExpectedAmbientFailureCount(-1); - using (var conn = await ConnectionMultiplexer.ConnectAsync(config, Writer).ConfigureAwait(false)) - { - conn.GetDatabase().Ping(); - } - }).ConfigureAwait(false); - Log("Exception: " + ex.Message); - Assert.StartsWith("It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly.", ex.Message); - } + using var conn = await ConnectionMultiplexer.ConnectAsync(config, Writer).ConfigureAwait(false); + + conn.GetDatabase().Ping(); + }).ConfigureAwait(false); + Log("Exception: " + ex.Message); + Assert.StartsWith("It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly.", ex.Message); } } diff --git a/tests/StackExchange.Redis.Tests/Sentinel.cs b/tests/StackExchange.Redis.Tests/Sentinel.cs index 0f1cd06f8..e277c4883 100644 --- a/tests/StackExchange.Redis.Tests/Sentinel.cs +++ b/tests/StackExchange.Redis.Tests/Sentinel.cs @@ -6,434 +6,432 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class Sentinel : SentinelBase { - public class Sentinel : SentinelBase - { - public Sentinel(ITestOutputHelper output) : base(output) { } + public Sentinel(ITestOutputHelper output) : base(output) { } - [Fact] - public async Task PrimaryConnectTest() - { - var connectionString = $"{TestConfig.Current.SentinelServer},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; - var conn = ConnectionMultiplexer.Connect(connectionString); + [Fact] + public async Task PrimaryConnectTest() + { + var connectionString = $"{TestConfig.Current.SentinelServer},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; - var db = conn.GetDatabase(); - db.Ping(); + var conn = ConnectionMultiplexer.Connect(connectionString); - var endpoints = conn.GetEndPoints(); - Assert.Equal(2, endpoints.Length); + var db = conn.GetDatabase(); + db.Ping(); - var servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); - Assert.Equal(2, servers.Length); + var endpoints = conn.GetEndPoints(); + Assert.Equal(2, endpoints.Length); - var primary = servers.FirstOrDefault(s => !s.IsReplica); - Assert.NotNull(primary); - var replica = servers.FirstOrDefault(s => s.IsReplica); - Assert.NotNull(replica); - Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString()); + var servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); + Assert.Equal(2, servers.Length); - var expected = DateTime.Now.Ticks.ToString(); - Log("Tick Key: " + expected); - var key = Me(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, expected); + var primary = servers.FirstOrDefault(s => !s.IsReplica); + Assert.NotNull(primary); + var replica = servers.FirstOrDefault(s => s.IsReplica); + Assert.NotNull(replica); + Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString()); - var value = db.StringGet(key); - Assert.Equal(expected, value); + var expected = DateTime.Now.Ticks.ToString(); + Log("Tick Key: " + expected); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, expected); - // force read from replica, replication has some lag - await WaitForReplicationAsync(servers[0], TimeSpan.FromSeconds(10)).ForAwait(); - value = db.StringGet(key, CommandFlags.DemandReplica); - Assert.Equal(expected, value); - } + var value = db.StringGet(key); + Assert.Equal(expected, value); - [Fact] - public async Task PrimaryConnectAsyncTest() - { - var connectionString = $"{TestConfig.Current.SentinelServer},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; - var conn = await ConnectionMultiplexer.ConnectAsync(connectionString); + // force read from replica, replication has some lag + await WaitForReplicationAsync(servers[0], TimeSpan.FromSeconds(10)).ForAwait(); + value = db.StringGet(key, CommandFlags.DemandReplica); + Assert.Equal(expected, value); + } - var db = conn.GetDatabase(); - await db.PingAsync(); + [Fact] + public async Task PrimaryConnectAsyncTest() + { + var connectionString = $"{TestConfig.Current.SentinelServer},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; + var conn = await ConnectionMultiplexer.ConnectAsync(connectionString); - var endpoints = conn.GetEndPoints(); - Assert.Equal(2, endpoints.Length); + var db = conn.GetDatabase(); + await db.PingAsync(); - var servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); - Assert.Equal(2, servers.Length); + var endpoints = conn.GetEndPoints(); + Assert.Equal(2, endpoints.Length); - var primary = servers.FirstOrDefault(s => !s.IsReplica); - Assert.NotNull(primary); - var replica = servers.FirstOrDefault(s => s.IsReplica); - Assert.NotNull(replica); - Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString()); + var servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); + Assert.Equal(2, servers.Length); - var expected = DateTime.Now.Ticks.ToString(); - Log("Tick Key: " + expected); - var key = Me(); - await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - await db.StringSetAsync(key, expected); + var primary = servers.FirstOrDefault(s => !s.IsReplica); + Assert.NotNull(primary); + var replica = servers.FirstOrDefault(s => s.IsReplica); + Assert.NotNull(replica); + Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString()); - var value = await db.StringGetAsync(key); - Assert.Equal(expected, value); + var expected = DateTime.Now.Ticks.ToString(); + Log("Tick Key: " + expected); + var key = Me(); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + await db.StringSetAsync(key, expected); - // force read from replica, replication has some lag - await WaitForReplicationAsync(servers[0], TimeSpan.FromSeconds(10)).ForAwait(); - value = await db.StringGetAsync(key, CommandFlags.DemandReplica); - Assert.Equal(expected, value); - } + var value = await db.StringGetAsync(key); + Assert.Equal(expected, value); - [Fact] - public void SentinelConnectTest() - { - var options = ServiceOptions.Clone(); - options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA); + // force read from replica, replication has some lag + await WaitForReplicationAsync(servers[0], TimeSpan.FromSeconds(10)).ForAwait(); + value = await db.StringGetAsync(key, CommandFlags.DemandReplica); + Assert.Equal(expected, value); + } - var conn = ConnectionMultiplexer.SentinelConnect(options); - var db = conn.GetDatabase(); + [Fact] + public void SentinelConnectTest() + { + var options = ServiceOptions.Clone(); + options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA); + var conn = ConnectionMultiplexer.SentinelConnect(options); + + var db = conn.GetDatabase(); + var test = db.Ping(); + Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, + TestConfig.Current.SentinelPortA, test.TotalMilliseconds); + } - var test = db.Ping(); - Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, - TestConfig.Current.SentinelPortA, test.TotalMilliseconds); - } + [Fact] + public async Task SentinelConnectAsyncTest() + { + var options = ServiceOptions.Clone(); + options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA); + var conn = await ConnectionMultiplexer.SentinelConnectAsync(options); + + var db = conn.GetDatabase(); + var test = await db.PingAsync(); + Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, + TestConfig.Current.SentinelPortA, test.TotalMilliseconds); + } - [Fact] - public async Task SentinelConnectAsyncTest() + [Fact] + public void SentinelRole() + { + foreach (var server in SentinelsServers) { - var options = ServiceOptions.Clone(); - options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA); + var role = server.Role(); + Assert.NotNull(role); + Assert.Equal(role.Value, RedisLiterals.sentinel); + var sentinel = role as Role.Sentinel; + Assert.NotNull(sentinel); + } + } - var conn = await ConnectionMultiplexer.SentinelConnectAsync(options); - var db = conn.GetDatabase(); + [Fact] + public void PingTest() + { + var test = SentinelServerA.Ping(); + Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, + TestConfig.Current.SentinelPortA, test.TotalMilliseconds); + test = SentinelServerB.Ping(); + Log("ping to sentinel {0}:{1} took {1} ms", TestConfig.Current.SentinelServer, + TestConfig.Current.SentinelPortB, test.TotalMilliseconds); + test = SentinelServerC.Ping(); + Log("ping to sentinel {0}:{1} took {1} ms", TestConfig.Current.SentinelServer, + TestConfig.Current.SentinelPortC, test.TotalMilliseconds); + } - var test = await db.PingAsync(); - Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, - TestConfig.Current.SentinelPortA, test.TotalMilliseconds); + [Fact] + public void SentinelGetPrimaryAddressByNameTest() + { + foreach (var server in SentinelsServers) + { + var primary = server.SentinelMaster(ServiceName); + var endpoint = server.SentinelGetMasterAddressByName(ServiceName); + Assert.NotNull(endpoint); + var ipEndPoint = endpoint as IPEndPoint; + Assert.NotNull(ipEndPoint); + Assert.Equal(primary.ToDictionary()["ip"], ipEndPoint.Address.ToString()); + Assert.Equal(primary.ToDictionary()["port"], ipEndPoint.Port.ToString()); + Log("{0}:{1}", ipEndPoint.Address, ipEndPoint.Port); } + } - [Fact] - public void SentinelRole() + [Fact] + public async Task SentinelGetPrimaryAddressByNameAsyncTest() + { + foreach (var server in SentinelsServers) { - foreach (var server in SentinelsServers) - { - var role = server.Role(); - Assert.NotNull(role); - Assert.Equal(role.Value, RedisLiterals.sentinel); - var sentinel = role as Role.Sentinel; - Assert.NotNull(sentinel); - } + var primary = server.SentinelMaster(ServiceName); + var endpoint = await server.SentinelGetMasterAddressByNameAsync(ServiceName).ForAwait(); + Assert.NotNull(endpoint); + var ipEndPoint = endpoint as IPEndPoint; + Assert.NotNull(ipEndPoint); + Assert.Equal(primary.ToDictionary()["ip"], ipEndPoint.Address.ToString()); + Assert.Equal(primary.ToDictionary()["port"], ipEndPoint.Port.ToString()); + Log("{0}:{1}", ipEndPoint.Address, ipEndPoint.Port); } + } - [Fact] - public void PingTest() + [Fact] + public void SentinelGetMasterAddressByNameNegativeTest() + { + foreach (var server in SentinelsServers) { - var test = SentinelServerA.Ping(); - Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, - TestConfig.Current.SentinelPortA, test.TotalMilliseconds); - test = SentinelServerB.Ping(); - Log("ping to sentinel {0}:{1} took {1} ms", TestConfig.Current.SentinelServer, - TestConfig.Current.SentinelPortB, test.TotalMilliseconds); - test = SentinelServerC.Ping(); - Log("ping to sentinel {0}:{1} took {1} ms", TestConfig.Current.SentinelServer, - TestConfig.Current.SentinelPortC, test.TotalMilliseconds); + var endpoint = server.SentinelGetMasterAddressByName("FakeServiceName"); + Assert.Null(endpoint); } + } - [Fact] - public void SentinelGetPrimaryAddressByNameTest() + [Fact] + public async Task SentinelGetMasterAddressByNameAsyncNegativeTest() + { + foreach (var server in SentinelsServers) { - foreach (var server in SentinelsServers) - { - var primary = server.SentinelMaster(ServiceName); - var endpoint = server.SentinelGetMasterAddressByName(ServiceName); - Assert.NotNull(endpoint); - var ipEndPoint = endpoint as IPEndPoint; - Assert.NotNull(ipEndPoint); - Assert.Equal(primary.ToDictionary()["ip"], ipEndPoint.Address.ToString()); - Assert.Equal(primary.ToDictionary()["port"], ipEndPoint.Port.ToString()); - Log("{0}:{1}", ipEndPoint.Address, ipEndPoint.Port); - } + var endpoint = await server.SentinelGetMasterAddressByNameAsync("FakeServiceName").ForAwait(); + Assert.Null(endpoint); } + } - [Fact] - public async Task SentinelGetPrimaryAddressByNameAsyncTest() + [Fact] + public void SentinelPrimaryTest() + { + foreach (var server in SentinelsServers) { - foreach (var server in SentinelsServers) + var dict = server.SentinelMaster(ServiceName).ToDictionary(); + Assert.Equal(ServiceName, dict["name"]); + Assert.StartsWith("master", dict["flags"]); + foreach (var kvp in dict) { - var primary = server.SentinelMaster(ServiceName); - var endpoint = await server.SentinelGetMasterAddressByNameAsync(ServiceName).ForAwait(); - Assert.NotNull(endpoint); - var ipEndPoint = endpoint as IPEndPoint; - Assert.NotNull(ipEndPoint); - Assert.Equal(primary.ToDictionary()["ip"], ipEndPoint.Address.ToString()); - Assert.Equal(primary.ToDictionary()["port"], ipEndPoint.Port.ToString()); - Log("{0}:{1}", ipEndPoint.Address, ipEndPoint.Port); + Log("{0}:{1}", kvp.Key, kvp.Value); } } + } - [Fact] - public void SentinelGetMasterAddressByNameNegativeTest() + [Fact] + public async Task SentinelPrimaryAsyncTest() + { + foreach (var server in SentinelsServers) { - foreach (var server in SentinelsServers) + var results = await server.SentinelMasterAsync(ServiceName).ForAwait(); + Assert.Equal(ServiceName, results.ToDictionary()["name"]); + Assert.StartsWith("master", results.ToDictionary()["flags"]); + foreach (var kvp in results) { - var endpoint = server.SentinelGetMasterAddressByName("FakeServiceName"); - Assert.Null(endpoint); + Log("{0}:{1}", kvp.Key, kvp.Value); } } + } + + [Fact] + public void SentinelSentinelsTest() + { + var sentinels = SentinelServerA.SentinelSentinels(ServiceName); + + var expected = new List { + SentinelServerB.EndPoint.ToString(), + SentinelServerC.EndPoint.ToString() + }; - [Fact] - public async Task SentinelGetMasterAddressByNameAsyncNegativeTest() + var actual = new List(); + foreach (var kv in sentinels) { - foreach (var server in SentinelsServers) - { - var endpoint = await server.SentinelGetMasterAddressByNameAsync("FakeServiceName").ForAwait(); - Assert.Null(endpoint); - } + var data = kv.ToDictionary(); + actual.Add(data["ip"] + ":" + data["port"]); } - [Fact] - public void SentinelPrimaryTest() + Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerA.EndPoint.ToString())); + Assert.True(sentinels.Length == 2); + Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); + + sentinels = SentinelServerB.SentinelSentinels(ServiceName); + foreach (var kv in sentinels) { - foreach (var server in SentinelsServers) - { - var dict = server.SentinelMaster(ServiceName).ToDictionary(); - Assert.Equal(ServiceName, dict["name"]); - Assert.StartsWith("master", dict["flags"]); - foreach (var kvp in dict) - { - Log("{0}:{1}", kvp.Key, kvp.Value); - } - } + var data = kv.ToDictionary(); + actual.Add(data["ip"] + ":" + data["port"]); } + expected = new List { + SentinelServerA.EndPoint.ToString(), + SentinelServerC.EndPoint.ToString() + }; + + Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerB.EndPoint.ToString())); + Assert.True(sentinels.Length == 2); + Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); - [Fact] - public async Task SentinelPrimaryAsyncTest() + sentinels = SentinelServerC.SentinelSentinels(ServiceName); + foreach (var kv in sentinels) { - foreach (var server in SentinelsServers) - { - var results = await server.SentinelMasterAsync(ServiceName).ForAwait(); - Assert.Equal(ServiceName, results.ToDictionary()["name"]); - Assert.StartsWith("master", results.ToDictionary()["flags"]); - foreach (var kvp in results) - { - Log("{0}:{1}", kvp.Key, kvp.Value); - } - } + var data = kv.ToDictionary(); + actual.Add(data["ip"] + ":" + data["port"]); } + expected = new List { + SentinelServerA.EndPoint.ToString(), + SentinelServerB.EndPoint.ToString() + }; + + Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerC.EndPoint.ToString())); + Assert.True(sentinels.Length == 2); + Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); + } - [Fact] - public void SentinelSentinelsTest() + [Fact] + public async Task SentinelSentinelsAsyncTest() + { + var sentinels = await SentinelServerA.SentinelSentinelsAsync(ServiceName).ForAwait(); + var expected = new List { + SentinelServerB.EndPoint.ToString(), + SentinelServerC.EndPoint.ToString() + }; + + var actual = new List(); + foreach (var kv in sentinels) { - var sentinels = SentinelServerA.SentinelSentinels(ServiceName); - - var expected = new List { - SentinelServerB.EndPoint.ToString(), - SentinelServerC.EndPoint.ToString() - }; - - var actual = new List(); - foreach (var kv in sentinels) - { - var data = kv.ToDictionary(); - actual.Add(data["ip"] + ":" + data["port"]); - } - - Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerA.EndPoint.ToString())); - Assert.True(sentinels.Length == 2); - Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); + var data = kv.ToDictionary(); + actual.Add(data["ip"] + ":" + data["port"]); + } + Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerA.EndPoint.ToString())); + Assert.True(sentinels.Length == 2); + Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); - sentinels = SentinelServerB.SentinelSentinels(ServiceName); - foreach (var kv in sentinels) - { - var data = kv.ToDictionary(); - actual.Add(data["ip"] + ":" + data["port"]); - } - expected = new List { - SentinelServerA.EndPoint.ToString(), - SentinelServerC.EndPoint.ToString() - }; + sentinels = await SentinelServerB.SentinelSentinelsAsync(ServiceName).ForAwait(); - Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerB.EndPoint.ToString())); - Assert.True(sentinels.Length == 2); - Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); + expected = new List { + SentinelServerA.EndPoint.ToString(), + SentinelServerC.EndPoint.ToString() + }; - sentinels = SentinelServerC.SentinelSentinels(ServiceName); - foreach (var kv in sentinels) - { - var data = kv.ToDictionary(); - actual.Add(data["ip"] + ":" + data["port"]); - } - expected = new List { - SentinelServerA.EndPoint.ToString(), - SentinelServerB.EndPoint.ToString() - }; - - Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerC.EndPoint.ToString())); - Assert.True(sentinels.Length == 2); - Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); + actual = new List(); + foreach (var kv in sentinels) + { + var data = kv.ToDictionary(); + actual.Add(data["ip"] + ":" + data["port"]); } - - [Fact] - public async Task SentinelSentinelsAsyncTest() + Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerB.EndPoint.ToString())); + Assert.True(sentinels.Length == 2); + Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); + + sentinels = await SentinelServerC.SentinelSentinelsAsync(ServiceName).ForAwait(); + expected = new List { + SentinelServerA.EndPoint.ToString(), + SentinelServerB.EndPoint.ToString() + }; + actual = new List(); + foreach (var kv in sentinels) { - var sentinels = await SentinelServerA.SentinelSentinelsAsync(ServiceName).ForAwait(); - var expected = new List { - SentinelServerB.EndPoint.ToString(), - SentinelServerC.EndPoint.ToString() - }; - - var actual = new List(); - foreach (var kv in sentinels) - { - var data = kv.ToDictionary(); - actual.Add(data["ip"] + ":" + data["port"]); - } - Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerA.EndPoint.ToString())); - Assert.True(sentinels.Length == 2); - Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); - - sentinels = await SentinelServerB.SentinelSentinelsAsync(ServiceName).ForAwait(); - - expected = new List { - SentinelServerA.EndPoint.ToString(), - SentinelServerC.EndPoint.ToString() - }; - - actual = new List(); - foreach (var kv in sentinels) - { - var data = kv.ToDictionary(); - actual.Add(data["ip"] + ":" + data["port"]); - } - Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerB.EndPoint.ToString())); - Assert.True(sentinels.Length == 2); - Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); - - sentinels = await SentinelServerC.SentinelSentinelsAsync(ServiceName).ForAwait(); - expected = new List { - SentinelServerA.EndPoint.ToString(), - SentinelServerB.EndPoint.ToString() - }; - actual = new List(); - foreach (var kv in sentinels) - { - var data = kv.ToDictionary(); - actual.Add(data["ip"] + ":" + data["port"]); - } - Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerC.EndPoint.ToString())); - Assert.True(sentinels.Length == 2); - Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); + var data = kv.ToDictionary(); + actual.Add(data["ip"] + ":" + data["port"]); } + Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerC.EndPoint.ToString())); + Assert.True(sentinels.Length == 2); + Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); + } - [Fact] - public void SentinelPrimariesTest() + [Fact] + public void SentinelPrimariesTest() + { + var primaryConfigs = SentinelServerA.SentinelMasters(); + Assert.Single(primaryConfigs); + Assert.True(primaryConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); + Assert.Equal(ServiceName, primaryConfigs[0].ToDictionary()["name"]); + Assert.StartsWith("master", primaryConfigs[0].ToDictionary()["flags"]); + foreach (var config in primaryConfigs) { - var primaryConfigs = SentinelServerA.SentinelMasters(); - Assert.Single(primaryConfigs); - Assert.True(primaryConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); - Assert.Equal(ServiceName, primaryConfigs[0].ToDictionary()["name"]); - Assert.StartsWith("master", primaryConfigs[0].ToDictionary()["flags"]); - foreach (var config in primaryConfigs) + foreach (var kvp in config) { - foreach (var kvp in config) - { - Log("{0}:{1}", kvp.Key, kvp.Value); - } + Log("{0}:{1}", kvp.Key, kvp.Value); } } + } - [Fact] - public async Task SentinelPrimariesAsyncTest() + [Fact] + public async Task SentinelPrimariesAsyncTest() + { + var primaryConfigs = await SentinelServerA.SentinelMastersAsync().ForAwait(); + Assert.Single(primaryConfigs); + Assert.True(primaryConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); + Assert.Equal(ServiceName, primaryConfigs[0].ToDictionary()["name"]); + Assert.StartsWith("master", primaryConfigs[0].ToDictionary()["flags"]); + foreach (var config in primaryConfigs) { - var primaryConfigs = await SentinelServerA.SentinelMastersAsync().ForAwait(); - Assert.Single(primaryConfigs); - Assert.True(primaryConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); - Assert.Equal(ServiceName, primaryConfigs[0].ToDictionary()["name"]); - Assert.StartsWith("master", primaryConfigs[0].ToDictionary()["flags"]); - foreach (var config in primaryConfigs) + foreach (var kvp in config) { - foreach (var kvp in config) - { - Log("{0}:{1}", kvp.Key, kvp.Value); - } + Log("{0}:{1}", kvp.Key, kvp.Value); } } + } - [Fact] - public async Task SentinelReplicasTest() - { - // Give previous test run a moment to reset when multi-framework failover is in play. - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => SentinelServerA.SentinelReplicas(ServiceName).Length > 0); + [Fact] + public async Task SentinelReplicasTest() + { + // Give previous test run a moment to reset when multi-framework failover is in play. + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => SentinelServerA.SentinelReplicas(ServiceName).Length > 0); - var replicaConfigs = SentinelServerA.SentinelReplicas(ServiceName); - Assert.True(replicaConfigs.Length > 0, "Has replicaConfigs"); - Assert.True(replicaConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); - Assert.StartsWith("slave", replicaConfigs[0].ToDictionary()["flags"]); + var replicaConfigs = SentinelServerA.SentinelReplicas(ServiceName); + Assert.True(replicaConfigs.Length > 0, "Has replicaConfigs"); + Assert.True(replicaConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); + Assert.StartsWith("slave", replicaConfigs[0].ToDictionary()["flags"]); - foreach (var config in replicaConfigs) + foreach (var config in replicaConfigs) + { + foreach (var kvp in config) { - foreach (var kvp in config) - { - Log("{0}:{1}", kvp.Key, kvp.Value); - } + Log("{0}:{1}", kvp.Key, kvp.Value); } } + } - [Fact] - public async Task SentinelReplicasAsyncTest() + [Fact] + public async Task SentinelReplicasAsyncTest() + { + // Give previous test run a moment to reset when multi-framework failover is in play. + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => SentinelServerA.SentinelReplicas(ServiceName).Length > 0); + + var replicaConfigs = await SentinelServerA.SentinelReplicasAsync(ServiceName).ForAwait(); + Assert.True(replicaConfigs.Length > 0, "Has replicaConfigs"); + Assert.True(replicaConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); + Assert.StartsWith("slave", replicaConfigs[0].ToDictionary()["flags"]); + foreach (var config in replicaConfigs) { - // Give previous test run a moment to reset when multi-framework failover is in play. - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => SentinelServerA.SentinelReplicas(ServiceName).Length > 0); - - var replicaConfigs = await SentinelServerA.SentinelReplicasAsync(ServiceName).ForAwait(); - Assert.True(replicaConfigs.Length > 0, "Has replicaConfigs"); - Assert.True(replicaConfigs[0].ToDictionary().ContainsKey("name"), "replicaConfigs contains 'name'"); - Assert.StartsWith("slave", replicaConfigs[0].ToDictionary()["flags"]); - foreach (var config in replicaConfigs) + foreach (var kvp in config) { - foreach (var kvp in config) - { - Log("{0}:{1}", kvp.Key, kvp.Value); - } + Log("{0}:{1}", kvp.Key, kvp.Value); } } + } - [Fact] - public async Task SentinelGetSentinelAddressesTest() - { - var addresses = await SentinelServerA.SentinelGetSentinelAddressesAsync(ServiceName).ForAwait(); - Assert.Contains(SentinelServerB.EndPoint, addresses); - Assert.Contains(SentinelServerC.EndPoint, addresses); + [Fact] + public async Task SentinelGetSentinelAddressesTest() + { + var addresses = await SentinelServerA.SentinelGetSentinelAddressesAsync(ServiceName).ForAwait(); + Assert.Contains(SentinelServerB.EndPoint, addresses); + Assert.Contains(SentinelServerC.EndPoint, addresses); - addresses = await SentinelServerB.SentinelGetSentinelAddressesAsync(ServiceName).ForAwait(); - Assert.Contains(SentinelServerA.EndPoint, addresses); - Assert.Contains(SentinelServerC.EndPoint, addresses); + addresses = await SentinelServerB.SentinelGetSentinelAddressesAsync(ServiceName).ForAwait(); + Assert.Contains(SentinelServerA.EndPoint, addresses); + Assert.Contains(SentinelServerC.EndPoint, addresses); - addresses = await SentinelServerC.SentinelGetSentinelAddressesAsync(ServiceName).ForAwait(); - Assert.Contains(SentinelServerA.EndPoint, addresses); - Assert.Contains(SentinelServerB.EndPoint, addresses); - } + addresses = await SentinelServerC.SentinelGetSentinelAddressesAsync(ServiceName).ForAwait(); + Assert.Contains(SentinelServerA.EndPoint, addresses); + Assert.Contains(SentinelServerB.EndPoint, addresses); + } - [Fact] - public async Task ReadOnlyConnectionReplicasTest() - { - var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); - var config = new ConfigurationOptions(); + [Fact] + public async Task ReadOnlyConnectionReplicasTest() + { + var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); + var config = new ConfigurationOptions(); - foreach (var replica in replicas) - { - config.EndPoints.Add(replica); - } + foreach (var replica in replicas) + { + config.EndPoints.Add(replica); + } - var readonlyConn = await ConnectionMultiplexer.ConnectAsync(config); + var readonlyConn = await ConnectionMultiplexer.ConnectAsync(config); - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => readonlyConn.IsConnected); - Assert.True(readonlyConn.IsConnected); - var db = readonlyConn.GetDatabase(); - var s = db.StringGet("test"); - Assert.True(s.IsNullOrEmpty); - //var ex = Assert.Throws(() => db.StringSet("test", "try write to read only instance")); - //Assert.StartsWith("No connection is available to service this operation", ex.Message); - } + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => readonlyConn.IsConnected); + Assert.True(readonlyConn.IsConnected); + var db = readonlyConn.GetDatabase(); + var s = db.StringGet("test"); + Assert.True(s.IsNullOrEmpty); + //var ex = Assert.Throws(() => db.StringSet("test", "try write to read only instance")); + //Assert.StartsWith("No connection is available to service this operation", ex.Message); } } diff --git a/tests/StackExchange.Redis.Tests/SentinelBase.cs b/tests/StackExchange.Redis.Tests/SentinelBase.cs index 51d083d26..6b967c914 100644 --- a/tests/StackExchange.Redis.Tests/SentinelBase.cs +++ b/tests/StackExchange.Redis.Tests/SentinelBase.cs @@ -1,192 +1,190 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class SentinelBase : TestBase, IAsyncLifetime { - public class SentinelBase : TestBase, IAsyncLifetime - { - protected static string ServiceName => TestConfig.Current.SentinelSeviceName; - protected static ConfigurationOptions ServiceOptions => new ConfigurationOptions { ServiceName = ServiceName, AllowAdmin = true }; + protected static string ServiceName => TestConfig.Current.SentinelSeviceName; + protected static ConfigurationOptions ServiceOptions => new ConfigurationOptions { ServiceName = ServiceName, AllowAdmin = true }; - protected ConnectionMultiplexer Conn { get; set; } - protected IServer SentinelServerA { get; set; } - protected IServer SentinelServerB { get; set; } - protected IServer SentinelServerC { get; set; } - public IServer[] SentinelsServers { get; set; } + protected ConnectionMultiplexer Conn { get; set; } + protected IServer SentinelServerA { get; set; } + protected IServer SentinelServerB { get; set; } + protected IServer SentinelServerC { get; set; } + public IServer[] SentinelsServers { get; set; } #nullable disable - public SentinelBase(ITestOutputHelper output) : base(output) - { - Skip.IfNoConfig(nameof(TestConfig.Config.SentinelServer), TestConfig.Current.SentinelServer); - Skip.IfNoConfig(nameof(TestConfig.Config.SentinelSeviceName), TestConfig.Current.SentinelSeviceName); - } + public SentinelBase(ITestOutputHelper output) : base(output) + { + Skip.IfNoConfig(nameof(TestConfig.Config.SentinelServer), TestConfig.Current.SentinelServer); + Skip.IfNoConfig(nameof(TestConfig.Config.SentinelSeviceName), TestConfig.Current.SentinelSeviceName); + } #nullable enable - public Task DisposeAsync() => Task.CompletedTask; - - public async Task InitializeAsync() - { - var options = ServiceOptions.Clone(); - options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA); - options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortB); - options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortC); - Conn = ConnectionMultiplexer.SentinelConnect(options, Writer); - - for (var i = 0; i < 150; i++) - { - await Task.Delay(100).ForAwait(); - if (Conn.IsConnected) - { - using var checkConn = Conn.GetSentinelMasterConnection(options, Writer); - if (checkConn.IsConnected) - { - break; - } - } - } - Assert.True(Conn.IsConnected); - SentinelServerA = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA)!; - SentinelServerB = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortB)!; - SentinelServerC = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortC)!; - SentinelsServers = new[] { SentinelServerA, SentinelServerB, SentinelServerC }; - - SentinelServerA.AllowReplicaWrites = true; - // Wait until we are in a state of a single primary and replica - await WaitForReadyAsync(); - } + public Task DisposeAsync() => Task.CompletedTask; - // Sometimes it's global, sometimes it's local - // Depends what mood Redis is in but they're equal and not the point of our tests - protected static readonly IpComparer _ipComparer = new IpComparer(); - protected class IpComparer : IEqualityComparer - { - public bool Equals(string? x, string? y) => x == y || x?.Replace("0.0.0.0", "127.0.0.1") == y?.Replace("0.0.0.0", "127.0.0.1"); - public int GetHashCode(string? obj) => obj?.GetHashCode() ?? 0; - } + public async Task InitializeAsync() + { + var options = ServiceOptions.Clone(); + options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA); + options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortB); + options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortC); + Conn = ConnectionMultiplexer.SentinelConnect(options, Writer); - protected async Task WaitForReadyAsync(EndPoint? expectedPrimary = null, bool waitForReplication = false, TimeSpan? duration = null) + for (var i = 0; i < 150; i++) { - duration ??= TimeSpan.FromSeconds(30); - - var sw = Stopwatch.StartNew(); - - // wait until we have 1 primary and 1 replica and have verified their roles - var primary = SentinelServerA.SentinelGetMasterAddressByName(ServiceName); - if (expectedPrimary != null && expectedPrimary.ToString() != primary?.ToString()) + await Task.Delay(100).ForAwait(); + if (Conn.IsConnected) { - while (sw.Elapsed < duration.Value) + using var checkConn = Conn.GetSentinelMasterConnection(options, Writer); + if (checkConn.IsConnected) { - await Task.Delay(1000).ForAwait(); - try - { - primary = SentinelServerA.SentinelGetMasterAddressByName(ServiceName); - if (expectedPrimary.ToString() == primary?.ToString()) - break; - } - catch (Exception) - { - // ignore - } + break; } } - if (expectedPrimary != null && expectedPrimary.ToString() != primary?.ToString()) - throw new RedisException($"Primary was expected to be {expectedPrimary}"); - Log($"Primary is {primary}"); - - using var checkConn = Conn.GetSentinelMasterConnection(ServiceOptions); + } + Assert.True(Conn.IsConnected); + SentinelServerA = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA)!; + SentinelServerB = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortB)!; + SentinelServerC = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortC)!; + SentinelsServers = new[] { SentinelServerA, SentinelServerB, SentinelServerC }; + + SentinelServerA.AllowReplicaWrites = true; + // Wait until we are in a state of a single primary and replica + await WaitForReadyAsync(); + } - await WaitForRoleAsync(checkConn.GetServer(primary), "master", duration.Value.Subtract(sw.Elapsed)).ForAwait(); + // Sometimes it's global, sometimes it's local + // Depends what mood Redis is in but they're equal and not the point of our tests + protected static readonly IpComparer _ipComparer = new IpComparer(); + protected class IpComparer : IEqualityComparer + { + public bool Equals(string? x, string? y) => x == y || x?.Replace("0.0.0.0", "127.0.0.1") == y?.Replace("0.0.0.0", "127.0.0.1"); + public int GetHashCode(string? obj) => obj?.GetHashCode() ?? 0; + } - var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); - if (replicas?.Length > 0) - { - await Task.Delay(1000).ForAwait(); - replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); - await WaitForRoleAsync(checkConn.GetServer(replicas[0]), "slave", duration.Value.Subtract(sw.Elapsed)).ForAwait(); - } + protected async Task WaitForReadyAsync(EndPoint? expectedPrimary = null, bool waitForReplication = false, TimeSpan? duration = null) + { + duration ??= TimeSpan.FromSeconds(30); - if (waitForReplication) - { - await WaitForReplicationAsync(checkConn.GetServer(primary), duration.Value.Subtract(sw.Elapsed)).ForAwait(); - } - } + var sw = Stopwatch.StartNew(); - protected async Task WaitForRoleAsync(IServer server, string role, TimeSpan? duration = null) + // wait until we have 1 primary and 1 replica and have verified their roles + var primary = SentinelServerA.SentinelGetMasterAddressByName(ServiceName); + if (expectedPrimary != null && expectedPrimary.ToString() != primary?.ToString()) { - duration ??= TimeSpan.FromSeconds(30); - - Log($"Waiting for server ({server.EndPoint}) role to be \"{role}\"..."); - var sw = Stopwatch.StartNew(); while (sw.Elapsed < duration.Value) { + await Task.Delay(1000).ForAwait(); try { - if (server.Role()?.Value == role) - { - Log($"Done waiting for server ({server.EndPoint}) role to be \"{role}\""); - return; - } + primary = SentinelServerA.SentinelGetMasterAddressByName(ServiceName); + if (expectedPrimary.ToString() == primary?.ToString()) + break; } catch (Exception) { // ignore } - - await Task.Delay(500).ForAwait(); } + } + if (expectedPrimary != null && expectedPrimary.ToString() != primary?.ToString()) + throw new RedisException($"Primary was expected to be {expectedPrimary}"); + Log($"Primary is {primary}"); + + using var checkConn = Conn.GetSentinelMasterConnection(ServiceOptions); - throw new RedisException($"Timeout waiting for server ({server.EndPoint}) to have expected role (\"{role}\") assigned"); + await WaitForRoleAsync(checkConn.GetServer(primary), "master", duration.Value.Subtract(sw.Elapsed)).ForAwait(); + + var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); + if (replicas?.Length > 0) + { + await Task.Delay(1000).ForAwait(); + replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); + await WaitForRoleAsync(checkConn.GetServer(replicas[0]), "slave", duration.Value.Subtract(sw.Elapsed)).ForAwait(); } - protected async Task WaitForReplicationAsync(IServer primary, TimeSpan? duration = null) + if (waitForReplication) { - duration ??= TimeSpan.FromSeconds(10); + await WaitForReplicationAsync(checkConn.GetServer(primary), duration.Value.Subtract(sw.Elapsed)).ForAwait(); + } + } + + protected async Task WaitForRoleAsync(IServer server, string role, TimeSpan? duration = null) + { + duration ??= TimeSpan.FromSeconds(30); - static void LogEndpoints(IServer primary, Action log) + Log($"Waiting for server ({server.EndPoint}) role to be \"{role}\"..."); + var sw = Stopwatch.StartNew(); + while (sw.Elapsed < duration.Value) + { + try { - if (primary.Multiplexer is ConnectionMultiplexer muxer) + if (server.Role()?.Value == role) { - var serverEndpoints = muxer.GetServerSnapshot(); - log("Endpoints:"); - foreach (var serverEndpoint in serverEndpoints) - { - log($" {serverEndpoint}:"); - var server = primary.Multiplexer.GetServer(serverEndpoint.EndPoint); - log($" Server: (Connected={server.IsConnected}, Type={server.ServerType}, IsReplica={server.IsReplica}, Unselectable={serverEndpoint.GetUnselectableFlags()})"); - } + Log($"Done waiting for server ({server.EndPoint}) role to be \"{role}\""); + return; } } - - Log("Waiting for primary/replica replication to be in sync..."); - var sw = Stopwatch.StartNew(); - while (sw.Elapsed < duration.Value) + catch (Exception) { - var info = primary.Info("replication"); - var replicationInfo = info.FirstOrDefault(f => f.Key == "Replication")?.ToArray().ToDictionary(); - var replicaInfo = replicationInfo?.FirstOrDefault(i => i.Key.StartsWith("slave")).Value?.Split(',').ToDictionary(i => i.Split('=').First(), i => i.Split('=').Last()); - var replicaOffset = replicaInfo?["offset"]; - var primaryOffset = replicationInfo?["master_repl_offset"]; + // ignore + } + + await Task.Delay(500).ForAwait(); + } + + throw new RedisException($"Timeout waiting for server ({server.EndPoint}) to have expected role (\"{role}\") assigned"); + } + + protected async Task WaitForReplicationAsync(IServer primary, TimeSpan? duration = null) + { + duration ??= TimeSpan.FromSeconds(10); - if (replicaOffset == primaryOffset) + static void LogEndpoints(IServer primary, Action log) + { + if (primary.Multiplexer is ConnectionMultiplexer muxer) + { + var serverEndpoints = muxer.GetServerSnapshot(); + log("Endpoints:"); + foreach (var serverEndpoint in serverEndpoints) { - Log($"Done waiting for primary ({primaryOffset}) / replica ({replicaOffset}) replication to be in sync"); - LogEndpoints(primary, Log); - return; + log($" {serverEndpoint}:"); + var server = primary.Multiplexer.GetServer(serverEndpoint.EndPoint); + log($" Server: (Connected={server.IsConnected}, Type={server.ServerType}, IsReplica={server.IsReplica}, Unselectable={serverEndpoint.GetUnselectableFlags()})"); } + } + } - Log($"Waiting for primary ({primaryOffset}) / replica ({replicaOffset}) replication to be in sync..."); + Log("Waiting for primary/replica replication to be in sync..."); + var sw = Stopwatch.StartNew(); + while (sw.Elapsed < duration.Value) + { + var info = primary.Info("replication"); + var replicationInfo = info.FirstOrDefault(f => f.Key == "Replication")?.ToArray().ToDictionary(); + var replicaInfo = replicationInfo?.FirstOrDefault(i => i.Key.StartsWith("slave")).Value?.Split(',').ToDictionary(i => i.Split('=').First(), i => i.Split('=').Last()); + var replicaOffset = replicaInfo?["offset"]; + var primaryOffset = replicationInfo?["master_repl_offset"]; - await Task.Delay(250).ForAwait(); + if (replicaOffset == primaryOffset) + { + Log($"Done waiting for primary ({primaryOffset}) / replica ({replicaOffset}) replication to be in sync"); + LogEndpoints(primary, Log); + return; } - throw new RedisException("Timeout waiting for test servers primary/replica replication to be in sync."); + Log($"Waiting for primary ({primaryOffset}) / replica ({replicaOffset}) replication to be in sync..."); + + await Task.Delay(250).ForAwait(); } + + throw new RedisException("Timeout waiting for test servers primary/replica replication to be in sync."); } } diff --git a/tests/StackExchange.Redis.Tests/SentinelFailover.cs b/tests/StackExchange.Redis.Tests/SentinelFailover.cs index 81971925a..25b3bf6fd 100644 --- a/tests/StackExchange.Redis.Tests/SentinelFailover.cs +++ b/tests/StackExchange.Redis.Tests/SentinelFailover.cs @@ -5,99 +5,100 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(NonParallelCollection.Name)] +public class SentinelFailover : SentinelBase { - [Collection(NonParallelCollection.Name)] - public class SentinelFailover : SentinelBase, IAsyncLifetime + public SentinelFailover(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task ManagedPrimaryConnectionEndToEndWithFailoverTest() { - public SentinelFailover(ITestOutputHelper output) : base(output) { } - - [Fact] - public async Task ManagedPrimaryConnectionEndToEndWithFailoverTest() - { - var connectionString = $"{TestConfig.Current.SentinelServer}:{TestConfig.Current.SentinelPortA},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; - var conn = await ConnectionMultiplexer.ConnectAsync(connectionString); - conn.ConfigurationChanged += (s, e) => Log($"Configuration changed: {e.EndPoint}"); - var sub = conn.GetSubscriber(); - sub.Subscribe("*", (channel, message) => Log($"Sub: {channel}, message:{message}")); - - var db = conn.GetDatabase(); - await db.PingAsync(); - - var endpoints = conn.GetEndPoints(); - Assert.Equal(2, endpoints.Length); - - var servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); - Assert.Equal(2, servers.Length); - - var primary = servers.FirstOrDefault(s => !s.IsReplica); - Assert.NotNull(primary); - var replica = servers.FirstOrDefault(s => s.IsReplica); - Assert.NotNull(replica); - Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString()); - - // Set string value on current primary - var expected = DateTime.Now.Ticks.ToString(); - Log("Tick Key: " + expected); - var key = Me(); - await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); - await db.StringSetAsync(key, expected); - - var value = await db.StringGetAsync(key); - Assert.Equal(expected, value); - - Log("Waiting for first replication check..."); - // force read from replica, replication has some lag - await WaitForReplicationAsync(servers[0]).ForAwait(); - value = await db.StringGetAsync(key, CommandFlags.DemandReplica); - Assert.Equal(expected, value); - - Log("Waiting for ready pre-failover..."); - await WaitForReadyAsync(); - - // capture current replica - var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); - - Log("Starting failover..."); - var sw = Stopwatch.StartNew(); - SentinelServerA.SentinelFailover(ServiceName); - - // There's no point in doing much for 10 seconds - this is a built-in delay of how Sentinel works. - // The actual completion invoking the replication of the former primary is handled via - // https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L4799-L4808 - // ...which is invoked by INFO polls every 10 seconds (https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L81) - // ...which is calling https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L2666 - // However, the quicker iteration on INFO during an o_down does not apply here: https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L3089-L3104 - // So...we're waiting 10 seconds, no matter what. Might as well just idle to be more stable. - await Task.Delay(TimeSpan.FromSeconds(10)); - - // wait until the replica becomes the primary - Log("Waiting for ready post-failover..."); - await WaitForReadyAsync(expectedPrimary: replicas[0]); - Log($"Time to failover: {sw.Elapsed}"); - - endpoints = conn.GetEndPoints(); - Assert.Equal(2, endpoints.Length); - - servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); - Assert.Equal(2, servers.Length); - - var newPrimary = servers.FirstOrDefault(s => !s.IsReplica); - Assert.NotNull(newPrimary); - Assert.Equal(replica.EndPoint.ToString(), newPrimary.EndPoint.ToString()); - var newReplica = servers.FirstOrDefault(s => s.IsReplica); - Assert.NotNull(newReplica); - Assert.Equal(primary.EndPoint.ToString(), newReplica.EndPoint.ToString()); - Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString()); - - value = await db.StringGetAsync(key); - Assert.Equal(expected, value); - - Log("Waiting for second replication check..."); - // force read from replica, replication has some lag - await WaitForReplicationAsync(newPrimary).ForAwait(); - value = await db.StringGetAsync(key, CommandFlags.DemandReplica); - Assert.Equal(expected, value); - } + var connectionString = $"{TestConfig.Current.SentinelServer}:{TestConfig.Current.SentinelPortA},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; + using var conn = await ConnectionMultiplexer.ConnectAsync(connectionString); + + conn.ConfigurationChanged += (s, e) => Log($"Configuration changed: {e.EndPoint}"); + + var sub = conn.GetSubscriber(); + sub.Subscribe("*", (channel, message) => Log($"Sub: {channel}, message:{message}")); + + var db = conn.GetDatabase(); + await db.PingAsync(); + + var endpoints = conn.GetEndPoints(); + Assert.Equal(2, endpoints.Length); + + var servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); + Assert.Equal(2, servers.Length); + + var primary = servers.FirstOrDefault(s => !s.IsReplica); + Assert.NotNull(primary); + var replica = servers.FirstOrDefault(s => s.IsReplica); + Assert.NotNull(replica); + Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString()); + + // Set string value on current primary + var expected = DateTime.Now.Ticks.ToString(); + Log("Tick Key: " + expected); + var key = Me(); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + await db.StringSetAsync(key, expected); + + var value = await db.StringGetAsync(key); + Assert.Equal(expected, value); + + Log("Waiting for first replication check..."); + // force read from replica, replication has some lag + await WaitForReplicationAsync(servers[0]).ForAwait(); + value = await db.StringGetAsync(key, CommandFlags.DemandReplica); + Assert.Equal(expected, value); + + Log("Waiting for ready pre-failover..."); + await WaitForReadyAsync(); + + // capture current replica + var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); + + Log("Starting failover..."); + var sw = Stopwatch.StartNew(); + SentinelServerA.SentinelFailover(ServiceName); + + // There's no point in doing much for 10 seconds - this is a built-in delay of how Sentinel works. + // The actual completion invoking the replication of the former primary is handled via + // https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L4799-L4808 + // ...which is invoked by INFO polls every 10 seconds (https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L81) + // ...which is calling https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L2666 + // However, the quicker iteration on INFO during an o_down does not apply here: https://github.com/redis/redis/blob/f233c4c59d24828c77eb1118f837eaee14695f7f/src/sentinel.c#L3089-L3104 + // So...we're waiting 10 seconds, no matter what. Might as well just idle to be more stable. + await Task.Delay(TimeSpan.FromSeconds(10)); + + // wait until the replica becomes the primary + Log("Waiting for ready post-failover..."); + await WaitForReadyAsync(expectedPrimary: replicas[0]); + Log($"Time to failover: {sw.Elapsed}"); + + endpoints = conn.GetEndPoints(); + Assert.Equal(2, endpoints.Length); + + servers = endpoints.Select(e => conn.GetServer(e)).ToArray(); + Assert.Equal(2, servers.Length); + + var newPrimary = servers.FirstOrDefault(s => !s.IsReplica); + Assert.NotNull(newPrimary); + Assert.Equal(replica.EndPoint.ToString(), newPrimary.EndPoint.ToString()); + var newReplica = servers.FirstOrDefault(s => s.IsReplica); + Assert.NotNull(newReplica); + Assert.Equal(primary.EndPoint.ToString(), newReplica.EndPoint.ToString()); + Assert.NotEqual(primary.EndPoint.ToString(), replica.EndPoint.ToString()); + + value = await db.StringGetAsync(key); + Assert.Equal(expected, value); + + Log("Waiting for second replication check..."); + // force read from replica, replication has some lag + await WaitForReplicationAsync(newPrimary).ForAwait(); + value = await db.StringGetAsync(key, CommandFlags.DemandReplica); + Assert.Equal(expected, value); } } diff --git a/tests/StackExchange.Redis.Tests/Sets.cs b/tests/StackExchange.Redis.Tests/Sets.cs index 215885051..d32e12573 100644 --- a/tests/StackExchange.Redis.Tests/Sets.cs +++ b/tests/StackExchange.Redis.Tests/Sets.cs @@ -4,362 +4,343 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Sets : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Sets : TestBase + public Sets(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + + [Fact] + public void SetContains() { - public Sets(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + using var conn = Create(require: RedisFeatures.v6_2_0); - [Fact] - public void SetContains() + var key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key); + for (int i = 1; i < 1001; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - db.KeyDelete(key); - for (int i = 1; i < 1001; i++) - { - db.SetAdd(key, i, CommandFlags.FireAndForget); - } - - // Single member - var isMemeber = db.SetContains(key, 1); - Assert.True(isMemeber); - - // Multi members - var areMemebers = db.SetContains(key, new RedisValue[] { 0, 1, 2 }); - Assert.Equal(3, areMemebers.Length); - Assert.False(areMemebers[0]); - Assert.True(areMemebers[1]); - - // key not exists - db.KeyDelete(key); - isMemeber = db.SetContains(key, 1); - Assert.False(isMemeber); - areMemebers = db.SetContains(key, new RedisValue[] { 0, 1, 2 }); - Assert.Equal(3, areMemebers.Length); - Assert.True(areMemebers.All(i => !i)); // Check that all the elements are False + db.SetAdd(key, i, CommandFlags.FireAndForget); } - [Fact] - public async Task SetContainsAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var key = Me(); - var db = conn.GetDatabase(); - await db.KeyDeleteAsync(key); - for (int i = 1; i < 1001; i++) - { - db.SetAdd(key, i, CommandFlags.FireAndForget); - } - - // Single member - var isMemeber = await db.SetContainsAsync(key, 1); - Assert.True(isMemeber); - - // Multi members - var areMemebers = await db.SetContainsAsync(key, new RedisValue[] { 0, 1, 2 }); - Assert.Equal(3, areMemebers.Length); - Assert.False(areMemebers[0]); - Assert.True(areMemebers[1]); - - // key not exists - await db.KeyDeleteAsync(key); - isMemeber = await db.SetContainsAsync(key, 1); - Assert.False(isMemeber); - areMemebers = await db.SetContainsAsync(key, new RedisValue[] { 0, 1, 2 }); - Assert.Equal(3, areMemebers.Length); - Assert.True(areMemebers.All(i => !i)); // Check that all the elements are False - } + // Single member + var isMemeber = db.SetContains(key, 1); + Assert.True(isMemeber); + + // Multi members + var areMemebers = db.SetContains(key, new RedisValue[] { 0, 1, 2 }); + Assert.Equal(3, areMemebers.Length); + Assert.False(areMemebers[0]); + Assert.True(areMemebers[1]); + + // key not exists + db.KeyDelete(key); + isMemeber = db.SetContains(key, 1); + Assert.False(isMemeber); + areMemebers = db.SetContains(key, new RedisValue[] { 0, 1, 2 }); + Assert.Equal(3, areMemebers.Length); + Assert.True(areMemebers.All(i => !i)); // Check that all the elements are False + } - [Fact] - public void SetIntersectionLength() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v7_0_0_rc1); - var db = conn.GetDatabase(); - - var key1 = Me() + "1"; - db.KeyDelete(key1, CommandFlags.FireAndForget); - db.SetAdd(key1, new RedisValue[] { 0, 1, 2, 3, 4 }, CommandFlags.FireAndForget); - var key2 = Me() + "2"; - db.KeyDelete(key2, CommandFlags.FireAndForget); - db.SetAdd(key2, new RedisValue[] { 1, 2, 3, 4, 5 }, CommandFlags.FireAndForget); - - Assert.Equal(4, db.SetIntersectionLength(new RedisKey[]{ key1, key2})); - // with limit - Assert.Equal(3, db.SetIntersectionLength(new RedisKey[]{ key1, key2}, 3)); - - // Missing keys should be 0 - var key3 = Me() + "3"; - var key4 = Me() + "4"; - db.KeyDelete(key3, CommandFlags.FireAndForget); - Assert.Equal(0, db.SetIntersectionLength(new RedisKey[] { key1, key3 })); - Assert.Equal(0, db.SetIntersectionLength(new RedisKey[] { key3, key4 })); - } + [Fact] + public async Task SetContainsAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - [Fact] - public async Task SetIntersectionLengthAsync() + var key = Me(); + var db = conn.GetDatabase(); + await db.KeyDeleteAsync(key); + for (int i = 1; i < 1001; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v7_0_0_rc1); - var db = conn.GetDatabase(); - - var key1 = Me() + "1"; - db.KeyDelete(key1, CommandFlags.FireAndForget); - db.SetAdd(key1, new RedisValue[] { 0, 1, 2, 3, 4 }, CommandFlags.FireAndForget); - var key2 = Me() + "2"; - db.KeyDelete(key2, CommandFlags.FireAndForget); - db.SetAdd(key2, new RedisValue[] { 1, 2, 3, 4, 5 }, CommandFlags.FireAndForget); - - Assert.Equal(4, await db.SetIntersectionLengthAsync(new RedisKey[]{ key1, key2})); - // with limit - Assert.Equal(3, await db.SetIntersectionLengthAsync(new RedisKey[]{ key1, key2}, 3)); - - // Missing keys should be 0 - var key3 = Me() + "3"; - var key4 = Me() + "4"; - db.KeyDelete(key3, CommandFlags.FireAndForget); - Assert.Equal(0, await db.SetIntersectionLengthAsync(new RedisKey[] { key1, key3 })); - Assert.Equal(0, await db.SetIntersectionLengthAsync(new RedisKey[] { key3, key4 })); + db.SetAdd(key, i, CommandFlags.FireAndForget); } - [Fact] - public void SScan() + // Single member + var isMemeber = await db.SetContainsAsync(key, 1); + Assert.True(isMemeber); + + // Multi members + var areMemebers = await db.SetContainsAsync(key, new RedisValue[] { 0, 1, 2 }); + Assert.Equal(3, areMemebers.Length); + Assert.False(areMemebers[0]); + Assert.True(areMemebers[1]); + + // key not exists + await db.KeyDeleteAsync(key); + isMemeber = await db.SetContainsAsync(key, 1); + Assert.False(isMemeber); + areMemebers = await db.SetContainsAsync(key, new RedisValue[] { 0, 1, 2 }); + Assert.Equal(3, areMemebers.Length); + Assert.True(areMemebers.All(i => !i)); // Check that all the elements are False + } + + [Fact] + public void SetIntersectionLength() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + + var key1 = Me() + "1"; + db.KeyDelete(key1, CommandFlags.FireAndForget); + db.SetAdd(key1, new RedisValue[] { 0, 1, 2, 3, 4 }, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + db.SetAdd(key2, new RedisValue[] { 1, 2, 3, 4, 5 }, CommandFlags.FireAndForget); + + Assert.Equal(4, db.SetIntersectionLength(new RedisKey[]{ key1, key2})); + // with limit + Assert.Equal(3, db.SetIntersectionLength(new RedisKey[]{ key1, key2}, 3)); + + // Missing keys should be 0 + var key3 = Me() + "3"; + var key4 = Me() + "4"; + db.KeyDelete(key3, CommandFlags.FireAndForget); + Assert.Equal(0, db.SetIntersectionLength(new RedisKey[] { key1, key3 })); + Assert.Equal(0, db.SetIntersectionLength(new RedisKey[] { key3, key4 })); + } + + [Fact] + public async Task SetIntersectionLengthAsync() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + + var key1 = Me() + "1"; + db.KeyDelete(key1, CommandFlags.FireAndForget); + db.SetAdd(key1, new RedisValue[] { 0, 1, 2, 3, 4 }, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + db.SetAdd(key2, new RedisValue[] { 1, 2, 3, 4, 5 }, CommandFlags.FireAndForget); + + Assert.Equal(4, await db.SetIntersectionLengthAsync(new RedisKey[]{ key1, key2})); + // with limit + Assert.Equal(3, await db.SetIntersectionLengthAsync(new RedisKey[]{ key1, key2}, 3)); + + // Missing keys should be 0 + var key3 = Me() + "3"; + var key4 = Me() + "4"; + db.KeyDelete(key3, CommandFlags.FireAndForget); + Assert.Equal(0, await db.SetIntersectionLengthAsync(new RedisKey[] { key1, key3 })); + Assert.Equal(0, await db.SetIntersectionLengthAsync(new RedisKey[] { key3, key4 })); + } + + [Fact] + public void SScan() + { + using var conn = Create(); + + var server = GetAnyPrimary(conn); + + var key = Me(); + var db = conn.GetDatabase(); + int totalUnfiltered = 0, totalFiltered = 0; + for (int i = 1; i < 1001; i++) { - using (var conn = Create()) - { - var server = GetAnyPrimary(conn); - - var key = Me(); - var db = conn.GetDatabase(); - int totalUnfiltered = 0, totalFiltered = 0; - for (int i = 1; i < 1001; i++) - { - db.SetAdd(key, i, CommandFlags.FireAndForget); - totalUnfiltered += i; - if (i.ToString().Contains('3')) totalFiltered += i; - } - - var unfilteredActual = db.SetScan(key).Select(x => (int)x).Sum(); - Assert.Equal(totalUnfiltered, unfilteredActual); - if (server.Features.Scan) - { - var filteredActual = db.SetScan(key, "*3*").Select(x => (int)x).Sum(); - Assert.Equal(totalFiltered, filteredActual); - } - } + db.SetAdd(key, i, CommandFlags.FireAndForget); + totalUnfiltered += i; + if (i.ToString().Contains('3')) totalFiltered += i; } - [Fact] - public async Task SetRemoveArgTests() + var unfilteredActual = db.SetScan(key).Select(x => (int)x).Sum(); + Assert.Equal(totalUnfiltered, unfilteredActual); + if (server.Features.Scan) { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - var key = Me(); - - RedisValue[]? values = null; - Assert.Throws(() => db.SetRemove(key, values!)); - await Assert.ThrowsAsync(async () => await db.SetRemoveAsync(key, values!).ForAwait()).ForAwait(); - - values = Array.Empty(); - Assert.Equal(0, db.SetRemove(key, values)); - Assert.Equal(0, await db.SetRemoveAsync(key, values).ForAwait()); - } + var filteredActual = db.SetScan(key, "*3*").Select(x => (int)x).Sum(); + Assert.Equal(totalFiltered, filteredActual); } + } + + [Fact] + public async Task SetRemoveArgTests() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var key = Me(); - [Fact] - public void SetPopMulti_Multi() + RedisValue[]? values = null; + Assert.Throws(() => db.SetRemove(key, values!)); + await Assert.ThrowsAsync(async () => await db.SetRemoveAsync(key, values!).ForAwait()).ForAwait(); + + values = Array.Empty(); + Assert.Equal(0, db.SetRemove(key, values)); + Assert.Equal(0, await db.SetRemoveAsync(key, values).ForAwait()); + } + + [Fact] + public void SetPopMulti_Multi() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + for (int i = 1; i < 11; i++) { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v3_2_0); - - var db = conn.GetDatabase(); - var key = Me(); - - db.KeyDelete(key, CommandFlags.FireAndForget); - for (int i = 1; i < 11; i++) - { - db.SetAddAsync(key, i, CommandFlags.FireAndForget); - } - - var random = db.SetPop(key); - Assert.False(random.IsNull); - Assert.True((int)random > 0); - Assert.True((int)random <= 10); - Assert.Equal(9, db.SetLength(key)); - - var moreRandoms = db.SetPop(key, 2); - Assert.Equal(2, moreRandoms.Length); - Assert.False(moreRandoms[0].IsNull); - Assert.Equal(7, db.SetLength(key)); - } + db.SetAddAsync(key, i, CommandFlags.FireAndForget); } - [Fact] - public void SetPopMulti_Single() + var random = db.SetPop(key); + Assert.False(random.IsNull); + Assert.True((int)random > 0); + Assert.True((int)random <= 10); + Assert.Equal(9, db.SetLength(key)); + + var moreRandoms = db.SetPop(key, 2); + Assert.Equal(2, moreRandoms.Length); + Assert.False(moreRandoms[0].IsNull); + Assert.Equal(7, db.SetLength(key)); + } + + [Fact] + public void SetPopMulti_Single() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var key = Me(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + for (int i = 1; i < 11; i++) { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - var key = Me(); - - db.KeyDelete(key, CommandFlags.FireAndForget); - for (int i = 1; i < 11; i++) - { - db.SetAdd(key, i, CommandFlags.FireAndForget); - } - - var random = db.SetPop(key); - Assert.False(random.IsNull); - Assert.True((int)random > 0); - Assert.True((int)random <= 10); - Assert.Equal(9, db.SetLength(key)); - - var moreRandoms = db.SetPop(key, 1); - Assert.Single(moreRandoms); - Assert.False(moreRandoms[0].IsNull); - Assert.Equal(8, db.SetLength(key)); - } + db.SetAdd(key, i, CommandFlags.FireAndForget); } - [Fact] - public async Task SetPopMulti_Multi_Async() + var random = db.SetPop(key); + Assert.False(random.IsNull); + Assert.True((int)random > 0); + Assert.True((int)random <= 10); + Assert.Equal(9, db.SetLength(key)); + + var moreRandoms = db.SetPop(key, 1); + Assert.Single(moreRandoms); + Assert.False(moreRandoms[0].IsNull); + Assert.Equal(8, db.SetLength(key)); + } + + [Fact] + public async Task SetPopMulti_Multi_Async() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + for (int i = 1; i < 11; i++) { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v3_2_0); - - var db = conn.GetDatabase(); - var key = Me(); - - db.KeyDelete(key, CommandFlags.FireAndForget); - for (int i = 1; i < 11; i++) - { - db.SetAdd(key, i, CommandFlags.FireAndForget); - } - - var random = await db.SetPopAsync(key).ForAwait(); - Assert.False(random.IsNull); - Assert.True((int)random > 0); - Assert.True((int)random <= 10); - Assert.Equal(9, db.SetLength(key)); - - var moreRandoms = await db.SetPopAsync(key, 2).ForAwait(); - Assert.Equal(2, moreRandoms.Length); - Assert.False(moreRandoms[0].IsNull); - Assert.Equal(7, db.SetLength(key)); - } + db.SetAdd(key, i, CommandFlags.FireAndForget); } - [Fact] - public async Task SetPopMulti_Single_Async() + var random = await db.SetPopAsync(key).ForAwait(); + Assert.False(random.IsNull); + Assert.True((int)random > 0); + Assert.True((int)random <= 10); + Assert.Equal(9, db.SetLength(key)); + + var moreRandoms = await db.SetPopAsync(key, 2).ForAwait(); + Assert.Equal(2, moreRandoms.Length); + Assert.False(moreRandoms[0].IsNull); + Assert.Equal(7, db.SetLength(key)); + } + + [Fact] + public async Task SetPopMulti_Single_Async() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var key = Me(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + for (int i = 1; i < 11; i++) { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - var key = Me(); - - db.KeyDelete(key, CommandFlags.FireAndForget); - for (int i = 1; i < 11; i++) - { - db.SetAdd(key, i, CommandFlags.FireAndForget); - } - - var random = await db.SetPopAsync(key).ForAwait(); - Assert.False(random.IsNull); - Assert.True((int)random > 0); - Assert.True((int)random <= 10); - Assert.Equal(9, db.SetLength(key)); - - var moreRandoms = db.SetPop(key, 1); - Assert.Single(moreRandoms); - Assert.False(moreRandoms[0].IsNull); - Assert.Equal(8, db.SetLength(key)); - } + db.SetAdd(key, i, CommandFlags.FireAndForget); } - [Fact] - public async Task SetPopMulti_Zero_Async() + var random = await db.SetPopAsync(key).ForAwait(); + Assert.False(random.IsNull); + Assert.True((int)random > 0); + Assert.True((int)random <= 10); + Assert.Equal(9, db.SetLength(key)); + + var moreRandoms = db.SetPop(key, 1); + Assert.Single(moreRandoms); + Assert.False(moreRandoms[0].IsNull); + Assert.Equal(8, db.SetLength(key)); + } + + [Fact] + public async Task SetPopMulti_Zero_Async() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var key = Me(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + for (int i = 1; i < 11; i++) { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - var key = Me(); - - db.KeyDelete(key, CommandFlags.FireAndForget); - for (int i = 1; i < 11; i++) - { - db.SetAdd(key, i, CommandFlags.FireAndForget); - } - - var t = db.SetPopAsync(key, count: 0); - Assert.True(t.IsCompleted); // sync - var arr = await t; - Assert.Empty(arr); - - Assert.Equal(10, db.SetLength(key)); - } + db.SetAdd(key, i, CommandFlags.FireAndForget); } - [Fact] - public void SetAdd_Zero() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - var key = Me(); + var t = db.SetPopAsync(key, count: 0); + Assert.True(t.IsCompleted); // sync + var arr = await t; + Assert.Empty(arr); - db.KeyDelete(key, CommandFlags.FireAndForget); + Assert.Equal(10, db.SetLength(key)); + } - var result = db.SetAdd(key, Array.Empty()); - Assert.Equal(0, result); + [Fact] + public void SetAdd_Zero() + { + using var conn = Create(); - Assert.Equal(0, db.SetLength(key)); - } - } + var db = conn.GetDatabase(); + var key = Me(); - [Fact] - public async Task SetAdd_Zero_Async() - { - using (var conn = Create()) - { - var db = conn.GetDatabase(); - var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key, CommandFlags.FireAndForget); + var result = db.SetAdd(key, Array.Empty()); + Assert.Equal(0, result); - var t = db.SetAddAsync(key, Array.Empty()); - Assert.True(t.IsCompleted); // sync - var count = await t; - Assert.Equal(0, count); + Assert.Equal(0, db.SetLength(key)); + } - Assert.Equal(0, db.SetLength(key)); - } - } + [Fact] + public async Task SetAdd_Zero_Async() + { + using var conn = Create(); - [Fact] - public void SetPopMulti_Nil() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v3_2_0); + var db = conn.GetDatabase(); + var key = Me(); - var db = conn.GetDatabase(); - var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key, CommandFlags.FireAndForget); + var t = db.SetAddAsync(key, Array.Empty()); + Assert.True(t.IsCompleted); // sync + var count = await t; + Assert.Equal(0, count); - var arr = db.SetPop(key, 1); - Assert.Empty(arr); - } - } + Assert.Equal(0, db.SetLength(key)); + } + + [Fact] + public void SetPopMulti_Nil() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + + var arr = db.SetPop(key, 1); + Assert.Empty(arr); } } diff --git a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs index 279b34b42..8e561aa07 100644 --- a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs @@ -8,234 +8,233 @@ using StackExchange.Redis.Profiling; using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class SharedConnectionFixture : IDisposable { - public class SharedConnectionFixture : IDisposable - { - public bool IsEnabled { get; } + public bool IsEnabled { get; } + + public const string Key = "Shared Muxer"; + private readonly ConnectionMultiplexer _actualConnection; + internal IInternalConnectionMultiplexer Connection { get; } + public string Configuration { get; } - public const string Key = "Shared Muxer"; - private readonly ConnectionMultiplexer _actualConnection; - internal IInternalConnectionMultiplexer Connection { get; } - public string Configuration { get; } + public SharedConnectionFixture() + { + IsEnabled = TestConfig.Current.UseSharedConnection; + Configuration = TestBase.GetDefaultConfiguration(); + _actualConnection = TestBase.CreateDefault( + output: null, + clientName: nameof(SharedConnectionFixture), + configuration: Configuration, + allowAdmin: true + ); + _actualConnection.InternalError += OnInternalError; + _actualConnection.ConnectionFailed += OnConnectionFailed; + + Connection = new NonDisposingConnection(_actualConnection); + } - public SharedConnectionFixture() + private class NonDisposingConnection : IInternalConnectionMultiplexer + { + public bool AllowConnect { - IsEnabled = TestConfig.Current.UseSharedConnection; - Configuration = TestBase.GetDefaultConfiguration(); - _actualConnection = TestBase.CreateDefault( - output: null, - clientName: nameof(SharedConnectionFixture), - configuration: Configuration, - allowAdmin: true - ); - _actualConnection.InternalError += OnInternalError; - _actualConnection.ConnectionFailed += OnConnectionFailed; - - Connection = new NonDisposingConnection(_actualConnection); + get => _inner.AllowConnect; + set => _inner.AllowConnect = value; } - private class NonDisposingConnection : IInternalConnectionMultiplexer + public bool IgnoreConnect { - public bool AllowConnect - { - get => _inner.AllowConnect; - set => _inner.AllowConnect = value; - } - - public bool IgnoreConnect - { - get => _inner.IgnoreConnect; - set => _inner.IgnoreConnect = value; - } + get => _inner.IgnoreConnect; + set => _inner.IgnoreConnect = value; + } - public ReadOnlySpan GetServerSnapshot() => _inner.GetServerSnapshot(); + public ReadOnlySpan GetServerSnapshot() => _inner.GetServerSnapshot(); - private readonly IInternalConnectionMultiplexer _inner; - public NonDisposingConnection(IInternalConnectionMultiplexer inner) => _inner = inner; + private readonly IInternalConnectionMultiplexer _inner; + public NonDisposingConnection(IInternalConnectionMultiplexer inner) => _inner = inner; - public string ClientName => _inner.ClientName; + public string ClientName => _inner.ClientName; - public string Configuration => _inner.Configuration; + public string Configuration => _inner.Configuration; - public int TimeoutMilliseconds => _inner.TimeoutMilliseconds; + public int TimeoutMilliseconds => _inner.TimeoutMilliseconds; - public long OperationCount => _inner.OperationCount; + public long OperationCount => _inner.OperationCount; #pragma warning disable CS0618 // Type or member is obsolete - public bool PreserveAsyncOrder { get => false; set { } } + public bool PreserveAsyncOrder { get => false; set { } } #pragma warning restore CS0618 - public bool IsConnected => _inner.IsConnected; + public bool IsConnected => _inner.IsConnected; - public bool IsConnecting => _inner.IsConnecting; + public bool IsConnecting => _inner.IsConnecting; - public ConfigurationOptions RawConfig => _inner.RawConfig; + public ConfigurationOptions RawConfig => _inner.RawConfig; - public bool IncludeDetailInExceptions { get => _inner.RawConfig.IncludeDetailInExceptions; set => _inner.RawConfig.IncludeDetailInExceptions = value; } + public bool IncludeDetailInExceptions { get => _inner.RawConfig.IncludeDetailInExceptions; set => _inner.RawConfig.IncludeDetailInExceptions = value; } - public int StormLogThreshold { get => _inner.StormLogThreshold; set => _inner.StormLogThreshold = value; } + public int StormLogThreshold { get => _inner.StormLogThreshold; set => _inner.StormLogThreshold = value; } - public event EventHandler ErrorMessage - { - add => _inner.ErrorMessage += value; - remove => _inner.ErrorMessage -= value; - } + public event EventHandler ErrorMessage + { + add => _inner.ErrorMessage += value; + remove => _inner.ErrorMessage -= value; + } - public event EventHandler ConnectionFailed - { - add => _inner.ConnectionFailed += value; - remove => _inner.ConnectionFailed -= value; - } + public event EventHandler ConnectionFailed + { + add => _inner.ConnectionFailed += value; + remove => _inner.ConnectionFailed -= value; + } - public event EventHandler InternalError - { - add => _inner.InternalError += value; - remove => _inner.InternalError -= value; - } + public event EventHandler InternalError + { + add => _inner.InternalError += value; + remove => _inner.InternalError -= value; + } - public event EventHandler ConnectionRestored - { - add => _inner.ConnectionRestored += value; - remove => _inner.ConnectionRestored -= value; - } + public event EventHandler ConnectionRestored + { + add => _inner.ConnectionRestored += value; + remove => _inner.ConnectionRestored -= value; + } - public event EventHandler ConfigurationChanged - { - add => _inner.ConfigurationChanged += value; - remove => _inner.ConfigurationChanged -= value; - } + public event EventHandler ConfigurationChanged + { + add => _inner.ConfigurationChanged += value; + remove => _inner.ConfigurationChanged -= value; + } - public event EventHandler ConfigurationChangedBroadcast - { - add => _inner.ConfigurationChangedBroadcast += value; - remove => _inner.ConfigurationChangedBroadcast -= value; - } + public event EventHandler ConfigurationChangedBroadcast + { + add => _inner.ConfigurationChangedBroadcast += value; + remove => _inner.ConfigurationChangedBroadcast -= value; + } - public event EventHandler HashSlotMoved - { - add => _inner.HashSlotMoved += value; - remove => _inner.HashSlotMoved -= value; - } + public event EventHandler HashSlotMoved + { + add => _inner.HashSlotMoved += value; + remove => _inner.HashSlotMoved -= value; + } - public void Close(bool allowCommandsToComplete = true) => _inner.Close(allowCommandsToComplete); + public void Close(bool allowCommandsToComplete = true) => _inner.Close(allowCommandsToComplete); - public Task CloseAsync(bool allowCommandsToComplete = true) => _inner.CloseAsync(allowCommandsToComplete); + public Task CloseAsync(bool allowCommandsToComplete = true) => _inner.CloseAsync(allowCommandsToComplete); - public bool Configure(TextWriter? log = null) => _inner.Configure(log); + public bool Configure(TextWriter? log = null) => _inner.Configure(log); - public Task ConfigureAsync(TextWriter? log = null) => _inner.ConfigureAsync(log); + public Task ConfigureAsync(TextWriter? log = null) => _inner.ConfigureAsync(log); - public void Dispose() { } // DO NOT call _inner.Dispose(); + public void Dispose() { } // DO NOT call _inner.Dispose(); - public ServerCounters GetCounters() => _inner.GetCounters(); + public ServerCounters GetCounters() => _inner.GetCounters(); - public IDatabase GetDatabase(int db = -1, object? asyncState = null) => _inner.GetDatabase(db, asyncState); + public IDatabase GetDatabase(int db = -1, object? asyncState = null) => _inner.GetDatabase(db, asyncState); - public EndPoint[] GetEndPoints(bool configuredOnly = false) => _inner.GetEndPoints(configuredOnly); + public EndPoint[] GetEndPoints(bool configuredOnly = false) => _inner.GetEndPoints(configuredOnly); - public int GetHashSlot(RedisKey key) => _inner.GetHashSlot(key); + public int GetHashSlot(RedisKey key) => _inner.GetHashSlot(key); - public IServer GetServer(string host, int port, object? asyncState = null) => _inner.GetServer(host, port, asyncState); + public IServer GetServer(string host, int port, object? asyncState = null) => _inner.GetServer(host, port, asyncState); - public IServer GetServer(string hostAndPort, object? asyncState = null) => _inner.GetServer(hostAndPort, asyncState); + public IServer GetServer(string hostAndPort, object? asyncState = null) => _inner.GetServer(hostAndPort, asyncState); - public IServer GetServer(IPAddress host, int port) => _inner.GetServer(host, port); + public IServer GetServer(IPAddress host, int port) => _inner.GetServer(host, port); - public IServer GetServer(EndPoint endpoint, object? asyncState = null) => _inner.GetServer(endpoint, asyncState); + public IServer GetServer(EndPoint endpoint, object? asyncState = null) => _inner.GetServer(endpoint, asyncState); - public string GetStatus() => _inner.GetStatus(); + public string GetStatus() => _inner.GetStatus(); - public void GetStatus(TextWriter log) => _inner.GetStatus(log); + public void GetStatus(TextWriter log) => _inner.GetStatus(log); - public string? GetStormLog() => _inner.GetStormLog(); + public string? GetStormLog() => _inner.GetStormLog(); - public ISubscriber GetSubscriber(object? asyncState = null) => _inner.GetSubscriber(asyncState); + public ISubscriber GetSubscriber(object? asyncState = null) => _inner.GetSubscriber(asyncState); - public int HashSlot(RedisKey key) => _inner.HashSlot(key); + public int HashSlot(RedisKey key) => _inner.HashSlot(key); - public long PublishReconfigure(CommandFlags flags = CommandFlags.None) => _inner.PublishReconfigure(flags); + public long PublishReconfigure(CommandFlags flags = CommandFlags.None) => _inner.PublishReconfigure(flags); - public Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None) => _inner.PublishReconfigureAsync(flags); + public Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None) => _inner.PublishReconfigureAsync(flags); - public void RegisterProfiler(Func profilingSessionProvider) => _inner.RegisterProfiler(profilingSessionProvider); + public void RegisterProfiler(Func profilingSessionProvider) => _inner.RegisterProfiler(profilingSessionProvider); - public void ResetStormLog() => _inner.ResetStormLog(); + public void ResetStormLog() => _inner.ResetStormLog(); - public void Wait(Task task) => _inner.Wait(task); + public void Wait(Task task) => _inner.Wait(task); - public T Wait(Task task) => _inner.Wait(task); + public T Wait(Task task) => _inner.Wait(task); - public void WaitAll(params Task[] tasks) => _inner.WaitAll(tasks); + public void WaitAll(params Task[] tasks) => _inner.WaitAll(tasks); - public void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All) - => _inner.ExportConfiguration(destination, options); + public void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All) + => _inner.ExportConfiguration(destination, options); - public override string ToString() => _inner.ToString(); - } + public override string ToString() => _inner.ToString(); + } - public void Dispose() - { - _actualConnection.Dispose(); - GC.SuppressFinalize(this); - } + public void Dispose() + { + _actualConnection.Dispose(); + GC.SuppressFinalize(this); + } - protected void OnInternalError(object? sender, InternalErrorEventArgs e) + protected void OnInternalError(object? sender, InternalErrorEventArgs e) + { + Interlocked.Increment(ref privateFailCount); + lock (privateExceptions) { - Interlocked.Increment(ref privateFailCount); - lock (privateExceptions) - { - privateExceptions.Add(TestBase.Time() + ": Internal error: " + e.Origin + ", " + EndPointCollection.ToString(e.EndPoint) + "/" + e.ConnectionType); - } + privateExceptions.Add(TestBase.Time() + ": Internal error: " + e.Origin + ", " + EndPointCollection.ToString(e.EndPoint) + "/" + e.ConnectionType); } - protected void OnConnectionFailed(object? sender, ConnectionFailedEventArgs e) + } + protected void OnConnectionFailed(object? sender, ConnectionFailedEventArgs e) + { + Interlocked.Increment(ref privateFailCount); + lock (privateExceptions) { - Interlocked.Increment(ref privateFailCount); - lock (privateExceptions) - { - privateExceptions.Add($"{TestBase.Time()}: Connection failed ({e.FailureType}): {EndPointCollection.ToString(e.EndPoint)}/{e.ConnectionType}: {e.Exception}"); - } + privateExceptions.Add($"{TestBase.Time()}: Connection failed ({e.FailureType}): {EndPointCollection.ToString(e.EndPoint)}/{e.ConnectionType}: {e.Exception}"); } - private readonly List privateExceptions = new List(); - private int privateFailCount; + } + private readonly List privateExceptions = new List(); + private int privateFailCount; - public void Teardown(TextWriter output) + public void Teardown(TextWriter output) + { + var innerPrivateFailCount = Interlocked.Exchange(ref privateFailCount, 0); + if (innerPrivateFailCount != 0) { - var innerPrivateFailCount = Interlocked.Exchange(ref privateFailCount, 0); - if (innerPrivateFailCount != 0) + lock (privateExceptions) { - lock (privateExceptions) + foreach (var item in privateExceptions.Take(5)) { - foreach (var item in privateExceptions.Take(5)) - { - TestBase.LogNoTime(output, item); - } - privateExceptions.Clear(); + TestBase.LogNoTime(output, item); } - //Assert.True(false, $"There were {privateFailCount} private ambient exceptions."); + privateExceptions.Clear(); } + //Assert.True(false, $"There were {privateFailCount} private ambient exceptions."); + } - if (_actualConnection != null) + if (_actualConnection != null) + { + TestBase.Log(output, "Connection Counts: " + _actualConnection.GetCounters().ToString()); + foreach (var ep in _actualConnection.GetServerSnapshot()) { - TestBase.Log(output, "Connection Counts: " + _actualConnection.GetCounters().ToString()); - foreach (var ep in _actualConnection.GetServerSnapshot()) - { - var interactive = ep.GetBridge(ConnectionType.Interactive); - TestBase.Log(output, $" {Format.ToString(interactive)}: " + interactive?.GetStatus()); + var interactive = ep.GetBridge(ConnectionType.Interactive); + TestBase.Log(output, $" {Format.ToString(interactive)}: " + interactive?.GetStatus()); - var subscription = ep.GetBridge(ConnectionType.Subscription); - TestBase.Log(output, $" {Format.ToString(subscription)}: " + subscription?.GetStatus()); - } + var subscription = ep.GetBridge(ConnectionType.Subscription); + TestBase.Log(output, $" {Format.ToString(subscription)}: " + subscription?.GetStatus()); } } } +} - // https://stackoverflow.com/questions/13829737/xunit-net-run-code-once-before-and-after-all-tests - [CollectionDefinition(SharedConnectionFixture.Key)] - public class ConnectionCollection : ICollectionFixture - { - // This class has no code, and is never created. Its purpose is simply - // to be the place to apply [CollectionDefinition] and all the - // ICollectionFixture<> interfaces. - } +// https://stackoverflow.com/questions/13829737/xunit-net-run-code-once-before-and-after-all-tests +[CollectionDefinition(SharedConnectionFixture.Key)] +public class ConnectionCollection : ICollectionFixture +{ + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. } diff --git a/tests/StackExchange.Redis.Tests/Sockets.cs b/tests/StackExchange.Redis.Tests/Sockets.cs index 67b5b573f..a4d415237 100644 --- a/tests/StackExchange.Redis.Tests/Sockets.cs +++ b/tests/StackExchange.Redis.Tests/Sockets.cs @@ -1,32 +1,29 @@ using System.Diagnostics; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class Sockets : TestBase { - public class Sockets : TestBase - { - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public Sockets(ITestOutputHelper output) : base (output) { } + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; + public Sockets(ITestOutputHelper output) : base (output) { } - [FactLongRunning] - public void CheckForSocketLeaks() + [FactLongRunning] + public void CheckForSocketLeaks() + { + const int count = 2000; + for (var i = 0; i < count; i++) { - const int count = 2000; - for (var i = 0; i < count; i++) - { - using (var _ = Create(clientName: "Test: " + i)) - { - // Intentionally just creating and disposing to leak sockets here - // ...so we can figure out what's happening. - } - } - // Force GC before memory dump in debug below... - CollectGarbage(); + using var _ = Create(clientName: "Test: " + i); + // Intentionally just creating and disposing to leak sockets here + // ...so we can figure out what's happening. + } + // Force GC before memory dump in debug below... + CollectGarbage(); - if (Debugger.IsAttached) - { - Debugger.Break(); - } + if (Debugger.IsAttached) + { + Debugger.Break(); } } } diff --git a/tests/StackExchange.Redis.Tests/SortedSets.cs b/tests/StackExchange.Redis.Tests/SortedSets.cs index a6b29e64f..0a7f76d9d 100644 --- a/tests/StackExchange.Redis.Tests/SortedSets.cs +++ b/tests/StackExchange.Redis.Tests/SortedSets.cs @@ -3,1277 +3,1214 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class SortedSets : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class SortedSets : TestBase + public SortedSets(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + private static readonly SortedSetEntry[] entries = new SortedSetEntry[] + { + new SortedSetEntry("a", 1), + new SortedSetEntry("b", 2), + new SortedSetEntry("c", 3), + new SortedSetEntry("d", 4), + new SortedSetEntry("e", 5), + new SortedSetEntry("f", 6), + new SortedSetEntry("g", 7), + new SortedSetEntry("h", 8), + new SortedSetEntry("i", 9), + new SortedSetEntry("j", 10) + }; + + private static readonly SortedSetEntry[] entriesPow2 = new SortedSetEntry[] + { + new SortedSetEntry("a", 1), + new SortedSetEntry("b", 2), + new SortedSetEntry("c", 4), + new SortedSetEntry("d", 8), + new SortedSetEntry("e", 16), + new SortedSetEntry("f", 32), + new SortedSetEntry("g", 64), + new SortedSetEntry("h", 128), + new SortedSetEntry("i", 256), + new SortedSetEntry("j", 512) + }; + + private static readonly SortedSetEntry[] entriesPow3 = new SortedSetEntry[] { - public SortedSets(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + new SortedSetEntry("a", 1), + new SortedSetEntry("c", 4), + new SortedSetEntry("e", 16), + new SortedSetEntry("g", 64), + new SortedSetEntry("i", 256), + }; + + private static readonly SortedSetEntry[] lexEntries = new SortedSetEntry[] + { + new SortedSetEntry("a", 0), + new SortedSetEntry("b", 0), + new SortedSetEntry("c", 0), + new SortedSetEntry("d", 0), + new SortedSetEntry("e", 0), + new SortedSetEntry("f", 0), + new SortedSetEntry("g", 0), + new SortedSetEntry("h", 0), + new SortedSetEntry("i", 0), + new SortedSetEntry("j", 0) + }; + + [Fact] + public void SortedSetCombine() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - private static readonly SortedSetEntry[] entries = new SortedSetEntry[] - { - new SortedSetEntry("a", 1), - new SortedSetEntry("b", 2), - new SortedSetEntry("c", 3), - new SortedSetEntry("d", 4), - new SortedSetEntry("e", 5), - new SortedSetEntry("f", 6), - new SortedSetEntry("g", 7), - new SortedSetEntry("h", 8), - new SortedSetEntry("i", 9), - new SortedSetEntry("j", 10) - }; - - private static readonly SortedSetEntry[] entriesPow2 = new SortedSetEntry[] - { - new SortedSetEntry("a", 1), - new SortedSetEntry("b", 2), - new SortedSetEntry("c", 4), - new SortedSetEntry("d", 8), - new SortedSetEntry("e", 16), - new SortedSetEntry("f", 32), - new SortedSetEntry("g", 64), - new SortedSetEntry("h", 128), - new SortedSetEntry("i", 256), - new SortedSetEntry("j", 512) - }; - - private static readonly SortedSetEntry[] entriesPow3 = new SortedSetEntry[] - { - new SortedSetEntry("a", 1), - new SortedSetEntry("c", 4), - new SortedSetEntry("e", 16), - new SortedSetEntry("g", 64), - new SortedSetEntry("i", 256), - }; - - private static readonly SortedSetEntry[] lexEntries = new SortedSetEntry[] - { - new SortedSetEntry("a", 0), - new SortedSetEntry("b", 0), - new SortedSetEntry("c", 0), - new SortedSetEntry("d", 0), - new SortedSetEntry("e", 0), - new SortedSetEntry("f", 0), - new SortedSetEntry("g", 0), - new SortedSetEntry("h", 0), - new SortedSetEntry("i", 0), - new SortedSetEntry("j", 0) - }; - - [Fact] - public void SortedSetCombine() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); - var db = conn.GetDatabase(); - var key1 = Me(); - db.KeyDelete(key1, CommandFlags.FireAndForget); - var key2 = Me() + "2"; - db.KeyDelete(key2, CommandFlags.FireAndForget); + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); - db.SortedSetAdd(key1, entries); - db.SortedSetAdd(key2, entriesPow3); + var diff = db.SortedSetCombine(SetOperation.Difference, new RedisKey[] { key1, key2 }); + Assert.Equal(5, diff.Length); + Assert.Equal("b", diff[0]); - var diff = db.SortedSetCombine(SetOperation.Difference, new RedisKey[]{ key1, key2}); - Assert.Equal(5, diff.Length); - Assert.Equal("b", diff[0]); + var inter = db.SortedSetCombine(SetOperation.Intersect, new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter.Length); + Assert.Equal("a", inter[0]); - var inter = db.SortedSetCombine(SetOperation.Intersect, new RedisKey[]{ key1, key2}); - Assert.Equal(5, inter.Length); - Assert.Equal("a", inter[0]); + var union = db.SortedSetCombine(SetOperation.Union, new RedisKey[] { key1, key2 }); + Assert.Equal(10, union.Length); + Assert.Equal("a", union[0]); + } - var union = db.SortedSetCombine(SetOperation.Union, new RedisKey[]{ key1, key2}); - Assert.Equal(10, union.Length); - Assert.Equal("a", union[0]); - } + [Fact] + public async Task SortedSetCombineAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); - [Fact] - public async Task SortedSetCombineAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); - var db = conn.GetDatabase(); - var key1 = Me(); - db.KeyDelete(key1, CommandFlags.FireAndForget); - var key2 = Me() + "2"; - db.KeyDelete(key2, CommandFlags.FireAndForget); + var diff = await db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }); + Assert.Equal(5, diff.Length); + Assert.Equal("b", diff[0]); - db.SortedSetAdd(key1, entries); - db.SortedSetAdd(key2, entriesPow3); + var inter = await db.SortedSetCombineAsync(SetOperation.Intersect, new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter.Length); + Assert.Equal("a", inter[0]); - var diff = await db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }); - Assert.Equal(5, diff.Length); - Assert.Equal("b", diff[0]); + var union = await db.SortedSetCombineAsync(SetOperation.Union, new RedisKey[] { key1, key2 }); + Assert.Equal(10, union.Length); + Assert.Equal("a", union[0]); + } - var inter = await db.SortedSetCombineAsync(SetOperation.Intersect, new RedisKey[] { key1, key2 }); - Assert.Equal(5, inter.Length); - Assert.Equal("a", inter[0]); + [Fact] + public void SortedSetCombineWithScores() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - var union = await db.SortedSetCombineAsync(SetOperation.Union, new RedisKey[] { key1, key2 }); - Assert.Equal(10, union.Length); - Assert.Equal("a", union[0]); - } + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); - [Fact] - public void SortedSetCombineWithScores() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); - var db = conn.GetDatabase(); - var key1 = Me(); - db.KeyDelete(key1, CommandFlags.FireAndForget); - var key2 = Me() + "2"; - db.KeyDelete(key2, CommandFlags.FireAndForget); + var diff = db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[] { key1, key2 }); + Assert.Equal(5, diff.Length); + Assert.Equal(new SortedSetEntry("b", 2), diff[0]); - db.SortedSetAdd(key1, entries); - db.SortedSetAdd(key2, entriesPow3); + var inter = db.SortedSetCombineWithScores(SetOperation.Intersect, new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter.Length); + Assert.Equal(new SortedSetEntry("a", 2), inter[0]); - var diff = db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[]{ key1, key2}); - Assert.Equal(5, diff.Length); - Assert.Equal(new SortedSetEntry("b", 2), diff[0]); + var union = db.SortedSetCombineWithScores(SetOperation.Union, new RedisKey[] { key1, key2 }); + Assert.Equal(10, union.Length); + Assert.Equal(new SortedSetEntry("a", 2), union[0]); + } - var inter = db.SortedSetCombineWithScores(SetOperation.Intersect, new RedisKey[]{ key1, key2}); - Assert.Equal(5, inter.Length); - Assert.Equal(new SortedSetEntry("a", 2), inter[0]); + [Fact] + public async Task SortedSetCombineWithScoresAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - var union = db.SortedSetCombineWithScores(SetOperation.Union, new RedisKey[]{ key1, key2}); - Assert.Equal(10, union.Length); - Assert.Equal(new SortedSetEntry("a", 2), union[0]); - } + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); - [Fact] - public async Task SortedSetCombineWithScoresAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); + var diff = await db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }); + Assert.Equal(5, diff.Length); + Assert.Equal(new SortedSetEntry("b", 2), diff[0]); - var db = conn.GetDatabase(); - var key1 = Me(); - db.KeyDelete(key1, CommandFlags.FireAndForget); - var key2 = Me() + "2"; - db.KeyDelete(key2, CommandFlags.FireAndForget); + var inter = await db.SortedSetCombineWithScoresAsync(SetOperation.Intersect, new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter.Length); + Assert.Equal(new SortedSetEntry("a", 2), inter[0]); - db.SortedSetAdd(key1, entries); - db.SortedSetAdd(key2, entriesPow3); + var union = await db.SortedSetCombineWithScoresAsync(SetOperation.Union, new RedisKey[] { key1, key2 }); + Assert.Equal(10, union.Length); + Assert.Equal(new SortedSetEntry("a", 2), union[0]); + } - var diff = await db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }); - Assert.Equal(5, diff.Length); - Assert.Equal(new SortedSetEntry("b", 2), diff[0]); + [Fact] + public void SortedSetCombineAndStore() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - var inter = await db.SortedSetCombineWithScoresAsync(SetOperation.Intersect, new RedisKey[] { key1, key2 }); - Assert.Equal(5, inter.Length); - Assert.Equal(new SortedSetEntry("a", 2), inter[0]); + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + var destination = Me() + "dest"; + db.KeyDelete(destination, CommandFlags.FireAndForget); - var union = await db.SortedSetCombineWithScoresAsync(SetOperation.Union, new RedisKey[] { key1, key2 }); - Assert.Equal(10, union.Length); - Assert.Equal(new SortedSetEntry("a", 2), union[0]); - } + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); - [Fact] - public void SortedSetCombineAndStore() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); + var diff = db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }); + Assert.Equal(5, diff); - var db = conn.GetDatabase(); - var key1 = Me(); - db.KeyDelete(key1, CommandFlags.FireAndForget); - var key2 = Me() + "2"; - db.KeyDelete(key2, CommandFlags.FireAndForget); - var destination = Me() + "dest"; - db.KeyDelete(destination, CommandFlags.FireAndForget); + var inter = db.SortedSetCombineAndStore(SetOperation.Intersect, destination, new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter); - db.SortedSetAdd(key1, entries); - db.SortedSetAdd(key2, entriesPow3); + var union = db.SortedSetCombineAndStore(SetOperation.Union, destination, new RedisKey[] { key1, key2 }); + Assert.Equal(10, union); + } - var diff = db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[]{ key1, key2}); - Assert.Equal(5, diff); + [Fact] + public async Task SortedSetCombineAndStoreAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - var inter = db.SortedSetCombineAndStore(SetOperation.Intersect, destination, new RedisKey[]{ key1, key2}); - Assert.Equal(5, inter); + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + var destination = Me() + "dest"; + db.KeyDelete(destination, CommandFlags.FireAndForget); - var union = db.SortedSetCombineAndStore(SetOperation.Union, destination, new RedisKey[]{ key1, key2}); - Assert.Equal(10, union); - } + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + var diff = await db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }); + Assert.Equal(5, diff); - [Fact] - public async Task SortedSetCombineAndStoreAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); + var inter = await db.SortedSetCombineAndStoreAsync(SetOperation.Intersect, destination, new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter); - var db = conn.GetDatabase(); - var key1 = Me(); - db.KeyDelete(key1, CommandFlags.FireAndForget); - var key2 = Me() + "2"; - db.KeyDelete(key2, CommandFlags.FireAndForget); - var destination = Me() + "dest"; - db.KeyDelete(destination, CommandFlags.FireAndForget); + var union = await db.SortedSetCombineAndStoreAsync(SetOperation.Union, destination, new RedisKey[] { key1, key2 }); + Assert.Equal(10, union); + } - db.SortedSetAdd(key1, entries); - db.SortedSetAdd(key2, entriesPow3); + [Fact] + public async Task SortedSetCombineErrors() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); + var destination = Me() + "dest"; + db.KeyDelete(destination, CommandFlags.FireAndForget); + + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); + + // ZDIFF can't be used with weights + var ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); + // and Async... + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); + + // ZDIFF can't be used with aggregation + ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); + // and Async... + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); + + // Too many weights + ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Union, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + // and Async... + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Union, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); + } - var diff = await db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }); - Assert.Equal(5, diff); + [Fact] + public void SortedSetIntersectionLength() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); - var inter = await db.SortedSetCombineAndStoreAsync(SetOperation.Intersect, destination, new RedisKey[] { key1, key2 }); - Assert.Equal(5, inter); + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); - var union = await db.SortedSetCombineAndStoreAsync(SetOperation.Union, destination, new RedisKey[] { key1, key2 }); - Assert.Equal(10, union); - } + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); - [Fact] - public async Task SortedSetCombineErrors() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var key1 = Me(); - db.KeyDelete(key1, CommandFlags.FireAndForget); - var key2 = Me() + "2"; - db.KeyDelete(key2, CommandFlags.FireAndForget); - var destination = Me() + "dest"; - db.KeyDelete(destination, CommandFlags.FireAndForget); - - db.SortedSetAdd(key1, entries); - db.SortedSetAdd(key2, entriesPow3); - - // ZDIFF can't be used with weights - var ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); - Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); - Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); - Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); - // and Async... - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); - Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); - Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); - Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); - - // ZDIFF can't be used with aggregation - ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); - Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); - Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); - Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); - // and Async... - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); - Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); - Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); - Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); - - // Too many weights - ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); - Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); - ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); - Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); - ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Union, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); - Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); - // and Async... - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); - Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); - Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Union, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); - Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); - } + var inter = db.SortedSetIntersectionLength(new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter); - [Fact] - public void SortedSetIntersectionLength() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v7_0_0_rc1); + // with limit + inter = db.SortedSetIntersectionLength(new RedisKey[] { key1, key2 }, 3); + Assert.Equal(3, inter); + } - var db = conn.GetDatabase(); - var key1 = Me(); - db.KeyDelete(key1, CommandFlags.FireAndForget); - var key2 = Me() + "2"; - db.KeyDelete(key2, CommandFlags.FireAndForget); + [Fact] + public async Task SortedSetIntersectionLengthAsync() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); - db.SortedSetAdd(key1, entries); - db.SortedSetAdd(key2, entriesPow3); + var db = conn.GetDatabase(); + var key1 = Me(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + var key2 = Me() + "2"; + db.KeyDelete(key2, CommandFlags.FireAndForget); - var inter = db.SortedSetIntersectionLength(new RedisKey[]{ key1, key2}); - Assert.Equal(5, inter); + db.SortedSetAdd(key1, entries); + db.SortedSetAdd(key2, entriesPow3); - // with limit - inter = db.SortedSetIntersectionLength(new RedisKey[]{ key1, key2}, 3); - Assert.Equal(3, inter); - } + var inter = await db.SortedSetIntersectionLengthAsync(new RedisKey[] { key1, key2 }); + Assert.Equal(5, inter); - [Fact] - public async Task SortedSetIntersectionLengthAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v7_0_0_rc1); + // with limit + inter = await db.SortedSetIntersectionLengthAsync(new RedisKey[] { key1, key2 }, 3); + Assert.Equal(3, inter); + } - var db = conn.GetDatabase(); - var key1 = Me(); - db.KeyDelete(key1, CommandFlags.FireAndForget); - var key2 = Me() + "2"; - db.KeyDelete(key2, CommandFlags.FireAndForget); + [Fact] + public void SortedSetPopMulti_Multi() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - db.SortedSetAdd(key1, entries); - db.SortedSetAdd(key2, entriesPow3); + var db = conn.GetDatabase(); + var key = Me(); - var inter = await db.SortedSetIntersectionLengthAsync(new RedisKey[] { key1, key2 }); - Assert.Equal(5, inter); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); - // with limit - inter = await db.SortedSetIntersectionLengthAsync(new RedisKey[] { key1, key2 }, 3); - Assert.Equal(3, inter); - } + var first = db.SortedSetPop(key, Order.Ascending); + Assert.True(first.HasValue); + Assert.Equal(entries[0], first.Value); + Assert.Equal(9, db.SortedSetLength(key)); - [Fact] - public void SortedSetPopMulti_Multi() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - var key = Me(); - - db.KeyDelete(key, CommandFlags.FireAndForget); - db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); - - var first = db.SortedSetPop(key, Order.Ascending); - Assert.True(first.HasValue); - Assert.Equal(entries[0], first.Value); - Assert.Equal(9, db.SortedSetLength(key)); - - var lasts = db.SortedSetPop(key, 2, Order.Descending); - Assert.Equal(2, lasts.Length); - Assert.Equal(entries[9], lasts[0]); - Assert.Equal(entries[8], lasts[1]); - Assert.Equal(7, db.SortedSetLength(key)); - } - } + var lasts = db.SortedSetPop(key, 2, Order.Descending); + Assert.Equal(2, lasts.Length); + Assert.Equal(entries[9], lasts[0]); + Assert.Equal(entries[8], lasts[1]); + Assert.Equal(7, db.SortedSetLength(key)); + } - [Fact] - public void SortedSetPopMulti_Single() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - var key = Me(); - - db.KeyDelete(key, CommandFlags.FireAndForget); - db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); - - var last = db.SortedSetPop(key, Order.Descending); - Assert.True(last.HasValue); - Assert.Equal(entries[9], last.Value); - Assert.Equal(9, db.SortedSetLength(key)); - - var firsts = db.SortedSetPop(key, 1, Order.Ascending); - Assert.Single(firsts); - Assert.Equal(entries[0], firsts[0]); - Assert.Equal(8, db.SortedSetLength(key)); - } - } + [Fact] + public void SortedSetPopMulti_Single() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - [Fact] - public async Task SortedSetPopMulti_Multi_Async() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - var key = Me(); - - db.KeyDelete(key, CommandFlags.FireAndForget); - db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); - - var last = await db.SortedSetPopAsync(key, Order.Descending).ForAwait(); - Assert.True(last.HasValue); - Assert.Equal(entries[9], last.Value); - Assert.Equal(9, db.SortedSetLength(key)); - - var moreLasts = await db.SortedSetPopAsync(key, 2, Order.Descending).ForAwait(); - Assert.Equal(2, moreLasts.Length); - Assert.Equal(entries[8], moreLasts[0]); - Assert.Equal(entries[7], moreLasts[1]); - Assert.Equal(7, db.SortedSetLength(key)); - } - } + var db = conn.GetDatabase(); + var key = Me(); - [Fact] - public async Task SortedSetPopMulti_Single_Async() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - var key = Me(); - - db.KeyDelete(key, CommandFlags.FireAndForget); - db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); - - var first = await db.SortedSetPopAsync(key).ForAwait(); - Assert.True(first.HasValue); - Assert.Equal(entries[0], first.Value); - Assert.Equal(9, db.SortedSetLength(key)); - - var moreFirsts = await db.SortedSetPopAsync(key, 1).ForAwait(); - Assert.Single(moreFirsts); - Assert.Equal(entries[1], moreFirsts[0]); - Assert.Equal(8, db.SortedSetLength(key)); - } - } + db.KeyDelete(key, CommandFlags.FireAndForget); + db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); - [Fact] - public async Task SortedSetPopMulti_Zero_Async() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var last = db.SortedSetPop(key, Order.Descending); + Assert.True(last.HasValue); + Assert.Equal(entries[9], last.Value); + Assert.Equal(9, db.SortedSetLength(key)); - var db = conn.GetDatabase(); - var key = Me(); + var firsts = db.SortedSetPop(key, 1, Order.Ascending); + Assert.Single(firsts); + Assert.Equal(entries[0], firsts[0]); + Assert.Equal(8, db.SortedSetLength(key)); + } - db.KeyDelete(key, CommandFlags.FireAndForget); - db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); + [Fact] + public async Task SortedSetPopMulti_Multi_Async() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var t = db.SortedSetPopAsync(key, count: 0); - Assert.True(t.IsCompleted); // sync - var arr = await t; - Assert.NotNull(arr); - Assert.Empty(arr); + var db = conn.GetDatabase(); + var key = Me(); - Assert.Equal(10, db.SortedSetLength(key)); - } - } + db.KeyDelete(key, CommandFlags.FireAndForget); + db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); - [Fact] - public void SortedSetRandomMembers() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var key = Me(); - var key0 = Me() + "non-existing"; - - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key0, CommandFlags.FireAndForget); - db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); - - // single member - var randMember = db.SortedSetRandomMember(key); - Assert.True(Array.Exists(entries, element => element.Element.Equals(randMember))); - - // with count - var randMemberArray = db.SortedSetRandomMembers(key, 5); - Assert.Equal(5, randMemberArray.Length); - randMemberArray = db.SortedSetRandomMembers(key, 15); - Assert.Equal(10, randMemberArray.Length); - randMemberArray = db.SortedSetRandomMembers(key, -5); - Assert.Equal(5, randMemberArray.Length); - randMemberArray = db.SortedSetRandomMembers(key, -15); - Assert.Equal(15, randMemberArray.Length); - - // with scores - var randMemberArray2 = db.SortedSetRandomMembersWithScores(key, 2); - Assert.Equal(2, randMemberArray2.Length); - foreach (var member in randMemberArray2) - { - Assert.Contains(member, entries); - } - - // check missing key case - randMember = db.SortedSetRandomMember(key0); - Assert.True(randMember.IsNull); - randMemberArray = db.SortedSetRandomMembers(key0, 2); - Assert.True(randMemberArray.Length == 0); - randMemberArray2 = db.SortedSetRandomMembersWithScores(key0, 2); - Assert.True(randMemberArray2.Length == 0); - } + var last = await db.SortedSetPopAsync(key, Order.Descending).ForAwait(); + Assert.True(last.HasValue); + Assert.Equal(entries[9], last.Value); + Assert.Equal(9, db.SortedSetLength(key)); - [Fact] - public async Task SortedSetRandomMembersAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var key = Me(); - var key0 = Me() + "non-existing"; - - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key0, CommandFlags.FireAndForget); - db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); - - var randMember = await db.SortedSetRandomMemberAsync(key); - Assert.True(Array.Exists(entries, element => element.Element.Equals(randMember))); - - // with count - var randMemberArray = await db.SortedSetRandomMembersAsync(key, 5); - Assert.Equal(5, randMemberArray.Length); - randMemberArray = await db.SortedSetRandomMembersAsync(key, 15); - Assert.Equal(10, randMemberArray.Length); - randMemberArray = await db.SortedSetRandomMembersAsync(key, -5); - Assert.Equal(5, randMemberArray.Length); - randMemberArray = await db.SortedSetRandomMembersAsync(key, -15); - Assert.Equal(15, randMemberArray.Length); - - // with scores - var randMemberArray2 = await db.SortedSetRandomMembersWithScoresAsync(key, 2); - Assert.Equal(2, randMemberArray2.Length); - foreach (var member in randMemberArray2) - { - Assert.Contains(member, entries); - } - - // check missing key case - randMember = await db.SortedSetRandomMemberAsync(key0); - Assert.True(randMember.IsNull); - randMemberArray = await db.SortedSetRandomMembersAsync(key0, 2); - Assert.True(randMemberArray.Length == 0); - randMemberArray2 = await db.SortedSetRandomMembersWithScoresAsync(key0, 2); - Assert.True(randMemberArray2.Length == 0); - } + var moreLasts = await db.SortedSetPopAsync(key, 2, Order.Descending).ForAwait(); + Assert.Equal(2, moreLasts.Length); + Assert.Equal(entries[8], moreLasts[0]); + Assert.Equal(entries[7], moreLasts[1]); + Assert.Equal(7, db.SortedSetLength(key)); + } - [Fact] - public async Task SortedSetRangeStoreByRankAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - var db = conn.GetDatabase(); - var me = Me(); - var sourceKey = $"{me}:ZSetSource"; - var destinationKey = $"{me}:ZSetDestination"; - - db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); - await db.SortedSetAddAsync(sourceKey, entries, CommandFlags.FireAndForget); - var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 0, -1); - Assert.Equal(entries.Length, res); - } + [Fact] + public async Task SortedSetPopMulti_Single_Async() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - [Fact] - public async Task SortedSetRangeStoreByRankLimitedAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var me = Me(); - var sourceKey = $"{me}:ZSetSource"; - var destinationKey = $"{me}:ZSetDestination"; - - db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); - await db.SortedSetAddAsync(sourceKey, entries, CommandFlags.FireAndForget); - var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 1, 4); - var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); - Assert.Equal(4, res); - for (var i = 1; i < 5; i++) - { - Assert.Equal(entries[i], range[i-1]); - } - } + var db = conn.GetDatabase(); + var key = Me(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); + + var first = await db.SortedSetPopAsync(key).ForAwait(); + Assert.True(first.HasValue); + Assert.Equal(entries[0], first.Value); + Assert.Equal(9, db.SortedSetLength(key)); + + var moreFirsts = await db.SortedSetPopAsync(key, 1).ForAwait(); + Assert.Single(moreFirsts); + Assert.Equal(entries[1], moreFirsts[0]); + Assert.Equal(8, db.SortedSetLength(key)); + } + + [Fact] + public async Task SortedSetPopMulti_Zero_Async() + { + using var conn = Create(require: RedisFeatures.v5_0_0); + + var db = conn.GetDatabase(); + var key = Me(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); + + var t = db.SortedSetPopAsync(key, count: 0); + Assert.True(t.IsCompleted); // sync + var arr = await t; + Assert.NotNull(arr); + Assert.Empty(arr); - [Fact] - public async Task SortedSetRangeStoreByScoreAsync() + Assert.Equal(10, db.SortedSetLength(key)); + } + + [Fact] + public void SortedSetRandomMembers() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + var key0 = Me() + "non-existing"; + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key0, CommandFlags.FireAndForget); + db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); + + // single member + var randMember = db.SortedSetRandomMember(key); + Assert.True(Array.Exists(entries, element => element.Element.Equals(randMember))); + + // with count + var randMemberArray = db.SortedSetRandomMembers(key, 5); + Assert.Equal(5, randMemberArray.Length); + randMemberArray = db.SortedSetRandomMembers(key, 15); + Assert.Equal(10, randMemberArray.Length); + randMemberArray = db.SortedSetRandomMembers(key, -5); + Assert.Equal(5, randMemberArray.Length); + randMemberArray = db.SortedSetRandomMembers(key, -15); + Assert.Equal(15, randMemberArray.Length); + + // with scores + var randMemberArray2 = db.SortedSetRandomMembersWithScores(key, 2); + Assert.Equal(2, randMemberArray2.Length); + foreach (var member in randMemberArray2) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var me = Me(); - var sourceKey = $"{me}:ZSetSource"; - var destinationKey = $"{me}:ZSetDestination"; - - db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); - await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); - var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 64, 128, SortedSetOrder.ByScore); - var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); - Assert.Equal(2, res); - for (var i = 6; i < 8; i++) - { - Assert.Equal(entriesPow2[i], range[i-6]); - } + Assert.Contains(member, entries); } - [Fact] - public async Task SortedSetRangeStoreByScoreAsyncDefault() + // check missing key case + randMember = db.SortedSetRandomMember(key0); + Assert.True(randMember.IsNull); + randMemberArray = db.SortedSetRandomMembers(key0, 2); + Assert.True(randMemberArray.Length == 0); + randMemberArray2 = db.SortedSetRandomMembersWithScores(key0, 2); + Assert.True(randMemberArray2.Length == 0); + } + + [Fact] + public async Task SortedSetRandomMembersAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + var key0 = Me() + "non-existing"; + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key0, CommandFlags.FireAndForget); + db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); + + var randMember = await db.SortedSetRandomMemberAsync(key); + Assert.True(Array.Exists(entries, element => element.Element.Equals(randMember))); + + // with count + var randMemberArray = await db.SortedSetRandomMembersAsync(key, 5); + Assert.Equal(5, randMemberArray.Length); + randMemberArray = await db.SortedSetRandomMembersAsync(key, 15); + Assert.Equal(10, randMemberArray.Length); + randMemberArray = await db.SortedSetRandomMembersAsync(key, -5); + Assert.Equal(5, randMemberArray.Length); + randMemberArray = await db.SortedSetRandomMembersAsync(key, -15); + Assert.Equal(15, randMemberArray.Length); + + // with scores + var randMemberArray2 = await db.SortedSetRandomMembersWithScoresAsync(key, 2); + Assert.Equal(2, randMemberArray2.Length); + foreach (var member in randMemberArray2) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var me = Me(); - var sourceKey = $"{me}:ZSetSource"; - var destinationKey = $"{me}:ZSetDestination"; - - db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); - await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); - var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, double.NegativeInfinity, double.PositiveInfinity, SortedSetOrder.ByScore); - var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); - Assert.Equal(10, res); - for (var i = 0; i < entriesPow2.Length; i++) - { - Assert.Equal(entriesPow2[i], range[i]); - } + Assert.Contains(member, entries); } - [Fact] - public async Task SortedSetRangeStoreByScoreAsyncLimited() + // check missing key case + randMember = await db.SortedSetRandomMemberAsync(key0); + Assert.True(randMember.IsNull); + randMemberArray = await db.SortedSetRandomMembersAsync(key0, 2); + Assert.True(randMemberArray.Length == 0); + randMemberArray2 = await db.SortedSetRandomMembersWithScoresAsync(key0, 2); + Assert.True(randMemberArray2.Length == 0); + } + + [Fact] + public async Task SortedSetRangeStoreByRankAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, entries, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 0, -1); + Assert.Equal(entries.Length, res); + } + + [Fact] + public async Task SortedSetRangeStoreByRankLimitedAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, entries, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 1, 4); + var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); + Assert.Equal(4, res); + for (var i = 1; i < 5; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var me = Me(); - var sourceKey = $"{me}:ZSetSource"; - var destinationKey = $"{me}:ZSetDestination"; - - db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); - await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); - var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, double.NegativeInfinity, double.PositiveInfinity, SortedSetOrder.ByScore, skip: 1, take: 6); - var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); - Assert.Equal(6, res); - for (var i = 1; i < 7; i++) - { - Assert.Equal(entriesPow2[i], range[i-1]); - } + Assert.Equal(entries[i], range[i - 1]); } + } - [Fact] - public async Task SortedSetRangeStoreByScoreAsyncExclusiveRange() + [Fact] + public async Task SortedSetRangeStoreByScoreAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 64, 128, SortedSetOrder.ByScore); + var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); + Assert.Equal(2, res); + for (var i = 6; i < 8; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var me = Me(); - var sourceKey = $"{me}:ZSetSource"; - var destinationKey = $"{me}:ZSetDestination"; - - db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); - await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); - var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 32, 256, SortedSetOrder.ByScore, exclude: Exclude.Both); - var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); - Assert.Equal(2, res); - for (var i = 6; i < 8; i++) - { - Assert.Equal(entriesPow2[i], range[i-6]); - } + Assert.Equal(entriesPow2[i], range[i - 6]); } + } - [Fact] - public async Task SortedSetRangeStoreByScoreAsyncReverse() + [Fact] + public async Task SortedSetRangeStoreByScoreAsyncDefault() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, double.NegativeInfinity, double.PositiveInfinity, SortedSetOrder.ByScore); + var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); + Assert.Equal(10, res); + for (var i = 0; i < entriesPow2.Length; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var me = Me(); - var sourceKey = $"{me}:ZSetSource"; - var destinationKey = $"{me}:ZSetDestination"; - - db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); - await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); - var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, start: double.PositiveInfinity, double.NegativeInfinity, SortedSetOrder.ByScore, order: Order.Descending); - var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); - Assert.Equal(10, res); - for (var i = 0; i < entriesPow2.Length; i++) - { - Assert.Equal(entriesPow2[i], range[i]); - } + Assert.Equal(entriesPow2[i], range[i]); } + } - [Fact] - public async Task SortedSetRangeStoreByLexAsync() + [Fact] + public async Task SortedSetRangeStoreByScoreAsyncLimited() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); + var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, double.NegativeInfinity, double.PositiveInfinity, SortedSetOrder.ByScore, skip: 1, take: 6); + var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); + Assert.Equal(6, res); + for (var i = 1; i < 7; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var me = Me(); - var sourceKey = $"{me}:ZSetSource"; - var destinationKey = $"{me}:ZSetDestination"; - - db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); - await db.SortedSetAddAsync(sourceKey, lexEntries, CommandFlags.FireAndForget); - var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, "a", "j", SortedSetOrder.ByLex); - var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); - Assert.Equal(10, res); - for (var i = 0; i (()=>db.SortedSetRangeAndStore(sourceKey, destinationKey,0,-1, take:5)); - Assert.Equal("take", exception.ParamName); + Assert.Equal(lexEntries[i], range[i - 1]); } + } - [Fact] - public void SortedSetRangeStoreFailExclude() + [Fact] + public void SortedSetRangeStoreByLexRevRange() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); + var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, "j", "a", SortedSetOrder.ByLex, Exclude.None, Order.Descending); + var range = db.SortedSetRangeByRankWithScores(destinationKey); + Assert.Equal(10, res); + for (var i = 0; i < lexEntries.Length; i++) { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var me = Me(); - var sourceKey = $"{me}:ZSetSource"; - var destinationKey = $"{me}:ZSetDestination"; - - db.KeyDelete(new RedisKey[] {sourceKey, destinationKey}, CommandFlags.FireAndForget); - db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); - var exception = Assert.Throws(()=>db.SortedSetRangeAndStore(sourceKey, destinationKey,0,-1, exclude: Exclude.Both)); - Assert.Equal("exclude", exception.ParamName); + Assert.Equal(lexEntries[i], range[i]); } + } - [Fact] - public void SortedSetScoresSingle() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v2_1_0); + [Fact] + public void SortedSetRangeStoreFailErroneousTake() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - var db = conn.GetDatabase(); - var key = Me(); - var memberName = "member"; + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(key); - db.SortedSetAdd(key, memberName, 1.5); + db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); + var exception = Assert.Throws(() => db.SortedSetRangeAndStore(sourceKey, destinationKey, 0, -1, take: 5)); + Assert.Equal("take", exception.ParamName); + } - var score = db.SortedSetScore(key, memberName); + [Fact] + public void SortedSetRangeStoreFailExclude() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - Assert.NotNull(score); - Assert.Equal((double)1.5, score.Value); - } + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; - [Fact] - public async Task SortedSetScoresSingleAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v2_1_0); + db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); + var exception = Assert.Throws(() => db.SortedSetRangeAndStore(sourceKey, destinationKey, 0, -1, exclude: Exclude.Both)); + Assert.Equal("exclude", exception.ParamName); + } - var db = conn.GetDatabase(); - var key = Me(); - var memberName = "member"; + [Fact] + public void SortedSetScoresSingle() + { + using var conn = Create(require: RedisFeatures.v2_1_0); - await db.KeyDeleteAsync(key); - await db.SortedSetAddAsync(key, memberName, 1.5); + var db = conn.GetDatabase(); + var key = Me(); + const string memberName = "member"; - var score = await db.SortedSetScoreAsync(key, memberName); + db.KeyDelete(key); + db.SortedSetAdd(key, memberName, 1.5); - Assert.NotNull(score); - Assert.Equal((double)1.5, score.Value); - } + var score = db.SortedSetScore(key, memberName); - [Fact] - public void SortedSetScoresSingle_MissingSetStillReturnsNull() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v2_1_0); + Assert.NotNull(score); + Assert.Equal((double)1.5, score.Value); + } - var db = conn.GetDatabase(); - var key = Me(); + [Fact] + public async Task SortedSetScoresSingleAsync() + { + using var conn = Create(require: RedisFeatures.v2_1_0); - db.KeyDelete(key); + var db = conn.GetDatabase(); + var key = Me(); + const string memberName = "member"; - // Attempt to retrieve score for a missing set, should still return null. - var score = db.SortedSetScore(key, "bogusMemberName"); + await db.KeyDeleteAsync(key); + await db.SortedSetAddAsync(key, memberName, 1.5); - Assert.Null(score); - } + var score = await db.SortedSetScoreAsync(key, memberName); - [Fact] - public async Task SortedSetScoresSingle_MissingSetStillReturnsNullAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v2_1_0); + Assert.NotNull(score); + Assert.Equal((double)1.5, score.Value); + } - var db = conn.GetDatabase(); - var key = Me(); + [Fact] + public void SortedSetScoresSingle_MissingSetStillReturnsNull() + { + using var conn = Create(require: RedisFeatures.v2_1_0); - await db.KeyDeleteAsync(key); + var db = conn.GetDatabase(); + var key = Me(); - // Attempt to retrieve score for a missing set, should still return null. - var score = await db.SortedSetScoreAsync(key, "bogusMemberName"); + db.KeyDelete(key); - Assert.Null(score); - } + // Attempt to retrieve score for a missing set, should still return null. + var score = db.SortedSetScore(key, "bogusMemberName"); - [Fact] - public void SortedSetScoresSingle_ReturnsNullForMissingMember() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v2_1_0); + Assert.Null(score); + } - var db = conn.GetDatabase(); - var key = Me(); + [Fact] + public async Task SortedSetScoresSingle_MissingSetStillReturnsNullAsync() + { + using var conn = Create(require: RedisFeatures.v2_1_0); - db.KeyDelete(key); - db.SortedSetAdd(key, "member1", 1.5); + var db = conn.GetDatabase(); + var key = Me(); - // Attempt to retrieve score for a missing member, should return null. - var score = db.SortedSetScore(key, "bogusMemberName"); + await db.KeyDeleteAsync(key); - Assert.Null(score); - } + // Attempt to retrieve score for a missing set, should still return null. + var score = await db.SortedSetScoreAsync(key, "bogusMemberName"); - [Fact] - public async Task SortedSetScoresSingle_ReturnsNullForMissingMemberAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v2_1_0); + Assert.Null(score); + } - var db = conn.GetDatabase(); - var key = Me(); + [Fact] + public void SortedSetScoresSingle_ReturnsNullForMissingMember() + { + using var conn = Create(require: RedisFeatures.v2_1_0); - await db.KeyDeleteAsync(key); - await db.SortedSetAddAsync(key, "member1", 1.5); + var db = conn.GetDatabase(); + var key = Me(); - // Attempt to retrieve score for a missing member, should return null. - var score = await db.SortedSetScoreAsync(key, "bogusMemberName"); + db.KeyDelete(key); + db.SortedSetAdd(key, "member1", 1.5); - Assert.Null(score); - } + // Attempt to retrieve score for a missing member, should return null. + var score = db.SortedSetScore(key, "bogusMemberName"); - [Fact] - public void SortedSetScoresMultiple() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var key = Me(); - var member1 = "member1"; - var member2 = "member2"; - var member3 = "member3"; - - db.KeyDelete(key); - db.SortedSetAdd(key, member1, 1.5); - db.SortedSetAdd(key, member2, 1.75); - db.SortedSetAdd(key, member3, 2); - - var scores = db.SortedSetScores(key, new RedisValue[] { member1, member2, member3 }); - - Assert.NotNull(scores); - Assert.Equal(3, scores.Length); - Assert.Equal((double)1.5, scores[0]); - Assert.Equal((double)1.75, scores[1]); - Assert.Equal(2, scores[2]); - } + Assert.Null(score); + } - [Fact] - public async Task SortedSetScoresMultipleAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var key = Me(); - var member1 = "member1"; - var member2 = "member2"; - var member3 = "member3"; - - await db.KeyDeleteAsync(key); - await db.SortedSetAddAsync(key, member1, 1.5); - await db.SortedSetAddAsync(key, member2, 1.75); - await db.SortedSetAddAsync(key, member3, 2); - - var scores = await db.SortedSetScoresAsync(key, new RedisValue[] { member1, member2, member3 }); - - Assert.NotNull(scores); - Assert.Equal(3, scores.Length); - Assert.Equal((double)1.5, scores[0]); - Assert.Equal((double)1.75, scores[1]); - Assert.Equal(2, scores[2]); - } + [Fact] + public async Task SortedSetScoresSingle_ReturnsNullForMissingMemberAsync() + { + using var conn = Create(require: RedisFeatures.v2_1_0); - [Fact] - public void SortedSetScoresMultiple_ReturnsNullItemsForMissingSet() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); + var db = conn.GetDatabase(); + var key = Me(); - var db = conn.GetDatabase(); - var key = Me(); + await db.KeyDeleteAsync(key); + await db.SortedSetAddAsync(key, "member1", 1.5); - db.KeyDelete(key); + // Attempt to retrieve score for a missing member, should return null. + var score = await db.SortedSetScoreAsync(key, "bogusMemberName"); - // Missing set but should still return an array of nulls. - var scores = db.SortedSetScores(key, new RedisValue[] { "bogus1", "bogus2", "bogus3" }); + Assert.Null(score); + } - Assert.NotNull(scores); - Assert.Equal(3, scores.Length); - Assert.Null(scores[0]); - Assert.Null(scores[1]); - Assert.Null(scores[2]); - } + [Fact] + public void SortedSetScoresMultiple() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + const string member1 = "member1", + member2 = "member2", + member3 = "member3"; + + db.KeyDelete(key); + db.SortedSetAdd(key, member1, 1.5); + db.SortedSetAdd(key, member2, 1.75); + db.SortedSetAdd(key, member3, 2); + + var scores = db.SortedSetScores(key, new RedisValue[] { member1, member2, member3 }); + + Assert.NotNull(scores); + Assert.Equal(3, scores.Length); + Assert.Equal((double)1.5, scores[0]); + Assert.Equal((double)1.75, scores[1]); + Assert.Equal(2, scores[2]); + } - [Fact] - public async Task SortedSetScoresMultiple_ReturnsNullItemsForMissingSetAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); + [Fact] + public async Task SortedSetScoresMultipleAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + const string member1 = "member1", + member2 = "member2", + member3 = "member3"; + + await db.KeyDeleteAsync(key); + await db.SortedSetAddAsync(key, member1, 1.5); + await db.SortedSetAddAsync(key, member2, 1.75); + await db.SortedSetAddAsync(key, member3, 2); + + var scores = await db.SortedSetScoresAsync(key, new RedisValue[] { member1, member2, member3 }); + + Assert.NotNull(scores); + Assert.Equal(3, scores.Length); + Assert.Equal((double)1.5, scores[0]); + Assert.Equal((double)1.75, scores[1]); + Assert.Equal(2, scores[2]); + } - var db = conn.GetDatabase(); - var key = Me(); + [Fact] + public void SortedSetScoresMultiple_ReturnsNullItemsForMissingSet() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - await db.KeyDeleteAsync(key); + var db = conn.GetDatabase(); + var key = Me(); - // Missing set but should still return an array of nulls. - var scores = await db.SortedSetScoresAsync(key, new RedisValue[] { "bogus1", "bogus2", "bogus3" }); + db.KeyDelete(key); - Assert.NotNull(scores); - Assert.Equal(3, scores.Length); - Assert.Null(scores[0]); - Assert.Null(scores[1]); - Assert.Null(scores[2]); - } + // Missing set but should still return an array of nulls. + var scores = db.SortedSetScores(key, new RedisValue[] { "bogus1", "bogus2", "bogus3" }); - [Fact] - public void SortedSetScoresMultiple_ReturnsScoresAndNullItems() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var key = Me(); - var member1 = "member1"; - var member2 = "member2"; - var member3 = "member3"; - var bogusMember = "bogusMember"; - - db.KeyDelete(key); - - db.SortedSetAdd(key, member1, 1.5); - db.SortedSetAdd(key, member2, 1.75); - db.SortedSetAdd(key, member3, 2); - - var scores = db.SortedSetScores(key, new RedisValue[] { member1, bogusMember, member2, member3 }); - - Assert.NotNull(scores); - Assert.Equal(4, scores.Length); - Assert.Null(scores[1]); - Assert.Equal((double)1.5, scores[0]); - Assert.Equal((double)1.75, scores[2]); - Assert.Equal(2, scores[3]); - } + Assert.NotNull(scores); + Assert.Equal(3, scores.Length); + Assert.Null(scores[0]); + Assert.Null(scores[1]); + Assert.Null(scores[2]); + } - [Fact] - public async Task SortedSetScoresMultiple_ReturnsScoresAndNullItemsAsync() - { - using var conn = Create(); - Skip.IfBelow(conn, RedisFeatures.v6_2_0); - - var db = conn.GetDatabase(); - var key = Me(); - var member1 = "member1"; - var member2 = "member2"; - var member3 = "member3"; - var bogusMember = "bogusMember"; - - await db.KeyDeleteAsync(key); - - await db.SortedSetAddAsync(key, member1, 1.5); - await db.SortedSetAddAsync(key, member2, 1.75); - await db.SortedSetAddAsync(key, member3, 2); - - var scores = await db.SortedSetScoresAsync(key, new RedisValue[] { member1, bogusMember, member2, member3 }); - - Assert.NotNull(scores); - Assert.Equal(4, scores.Length); - Assert.Null(scores[1]); - Assert.Equal((double)1.5, scores[0]); - Assert.Equal((double)1.75, scores[2]); - Assert.Equal(2, scores[3]); - } + [Fact] + public async Task SortedSetScoresMultiple_ReturnsNullItemsForMissingSetAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key); + + // Missing set but should still return an array of nulls. + var scores = await db.SortedSetScoresAsync(key, new RedisValue[] { "bogus1", "bogus2", "bogus3" }); + + Assert.NotNull(scores); + Assert.Equal(3, scores.Length); + Assert.Null(scores[0]); + Assert.Null(scores[1]); + Assert.Null(scores[2]); + } + + [Fact] + public void SortedSetScoresMultiple_ReturnsScoresAndNullItems() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + const string member1 = "member1", + member2 = "member2", + member3 = "member3", + bogusMember = "bogusMember"; + + db.KeyDelete(key); + + db.SortedSetAdd(key, member1, 1.5); + db.SortedSetAdd(key, member2, 1.75); + db.SortedSetAdd(key, member3, 2); + + var scores = db.SortedSetScores(key, new RedisValue[] { member1, bogusMember, member2, member3 }); + + Assert.NotNull(scores); + Assert.Equal(4, scores.Length); + Assert.Null(scores[1]); + Assert.Equal((double)1.5, scores[0]); + Assert.Equal((double)1.75, scores[2]); + Assert.Equal(2, scores[3]); + } + + [Fact] + public async Task SortedSetScoresMultiple_ReturnsScoresAndNullItemsAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + const string member1 = "member1", + member2 = "member2", + member3 = "member3", + bogusMember = "bogusMember"; + + await db.KeyDeleteAsync(key); + + await db.SortedSetAddAsync(key, member1, 1.5); + await db.SortedSetAddAsync(key, member2, 1.75); + await db.SortedSetAddAsync(key, member3, 2); + + var scores = await db.SortedSetScoresAsync(key, new RedisValue[] { member1, bogusMember, member2, member3 }); + + Assert.NotNull(scores); + Assert.Equal(4, scores.Length); + Assert.Null(scores[1]); + Assert.Equal((double)1.5, scores[0]); + Assert.Equal((double)1.75, scores[2]); + Assert.Equal(2, scores[3]); } } diff --git a/tests/StackExchange.Redis.Tests/Streams.cs b/tests/StackExchange.Redis.Tests/Streams.cs index 0008291c1..adb5511dd 100644 --- a/tests/StackExchange.Redis.Tests/Streams.cs +++ b/tests/StackExchange.Redis.Tests/Streams.cs @@ -5,1818 +5,1574 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Streams : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Streams : TestBase + public Streams(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public void IsStreamType() { - public Streams(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + using var conn = Create(require: RedisFeatures.v5_0_0); - [Fact] - public void IsStreamType() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - var key = GetUniqueKey("type_check"); + var db = conn.GetDatabase(); + var key = GetUniqueKey("type_check"); + db.StreamAdd(key, "field1", "value1"); - var db = conn.GetDatabase(); - db.StreamAdd(key, "field1", "value1"); + var keyType = db.KeyType(key); - var keyType = db.KeyType(key); + Assert.Equal(RedisType.Stream, keyType); + } - Assert.Equal(RedisType.Stream, keyType); - } - } + [Fact] + public void StreamAddSinglePairWithAutoId() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - [Fact] - public void StreamAddSinglePairWithAutoId() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var db = conn.GetDatabase(); + var messageId = db.StreamAdd(GetUniqueKey("auto_id"), "field1", "value1"); - var db = conn.GetDatabase(); - var messageId = db.StreamAdd(GetUniqueKey("auto_id"), "field1", "value1"); + Assert.True(messageId != RedisValue.Null && ((string?)messageId)?.Length > 0); + } - Assert.True(messageId != RedisValue.Null && ((string?)messageId)?.Length > 0); - } - } + [Fact] + public void StreamAddMultipleValuePairsWithAutoId() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - [Fact] - public void StreamAddMultipleValuePairsWithAutoId() + var db = conn.GetDatabase(); + var key = GetUniqueKey("multiple_value_pairs"); + var fields = new[] { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var key = GetUniqueKey("multiple_value_pairs"); - - var fields = new [] - { - new NameValueEntry("field1", "value1"), - new NameValueEntry("field2", "value2") - }; - - var db = conn.GetDatabase(); - var messageId = db.StreamAdd(key, fields); - - var entries = db.StreamRange(key); - - Assert.Single(entries); - Assert.Equal(messageId, entries[0].Id); - var vals = entries[0].Values; - Assert.NotNull(vals); - Assert.Equal(2, vals.Length); - Assert.Equal("field1", vals[0].Name); - Assert.Equal("value1", vals[0].Value); - Assert.Equal("field2", vals[1].Name); - Assert.Equal("value2", vals[1].Value); - } - } + new NameValueEntry("field1", "value1"), + new NameValueEntry("field2", "value2"), + }; - [Fact] - public void StreamAddWithManualId() - { - const string id = "42-0"; - var key = GetUniqueKey("manual_id"); + var messageId = db.StreamAdd(key, fields); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var entries = db.StreamRange(key); - var db = conn.GetDatabase(); - var messageId = db.StreamAdd(key, "field1", "value1", id); + Assert.Single(entries); + Assert.Equal(messageId, entries[0].Id); + var vals = entries[0].Values; + Assert.NotNull(vals); + Assert.Equal(2, vals.Length); + Assert.Equal("field1", vals[0].Name); + Assert.Equal("value1", vals[0].Value); + Assert.Equal("field2", vals[1].Name); + Assert.Equal("value2", vals[1].Value); + } - Assert.Equal(id, messageId); - } - } + [Fact] + public void StreamAddWithManualId() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - [Fact] - public void StreamAddMultipleValuePairsWithManualId() - { - const string id = "42-0"; - var key = GetUniqueKey("manual_id_multiple_values"); + var db = conn.GetDatabase(); + const string id = "42-0"; + var key = GetUniqueKey("manual_id"); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var messageId = db.StreamAdd(key, "field1", "value1", id); - var db = conn.GetDatabase(); + Assert.Equal(id, messageId); + } - var fields = new [] - { - new NameValueEntry("field1", "value1"), - new NameValueEntry("field2", "value2") - }; + [Fact] + public void StreamAddMultipleValuePairsWithManualId() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var messageId = db.StreamAdd(key, fields, id); - var entries = db.StreamRange(key); + var db = conn.GetDatabase(); + const string id = "42-0"; + var key = GetUniqueKey("manual_id_multiple_values"); - Assert.Equal(id, messageId); - Assert.NotNull(entries); - Assert.Single(entries); - Assert.Equal(id, entries[0].Id); - } - } - - [Fact] - public void StreamConsumerGroupSetId() + var fields = new[] { - var key = GetUniqueKey("group_set_id"); - const string groupName = "test_group"; - const string consumer = "consumer"; - - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - - // Create a stream - db.StreamAdd(key, "field1", "value1"); - db.StreamAdd(key, "field2", "value2"); - - // Create a group and set the position to deliver new messages only. - db.StreamCreateConsumerGroup(key, groupName, StreamPosition.NewMessages); + new NameValueEntry("field1", "value1"), + new NameValueEntry("field2", "value2") + }; - // Read into the group, expect nothing - var firstRead = db.StreamReadGroup(key, groupName, consumer, StreamPosition.NewMessages); - - // Reset the ID back to read from the beginning. - db.StreamConsumerGroupSetPosition(key, groupName, StreamPosition.Beginning); - - var secondRead = db.StreamReadGroup(key, groupName, consumer, StreamPosition.NewMessages); - - Assert.NotNull(firstRead); - Assert.NotNull(secondRead); - Assert.Empty(firstRead); - Assert.Equal(2, secondRead.Length); - } - } - - [Fact] - public void StreamConsumerGroupWithNoConsumers() - { - var key = GetUniqueKey("group_with_no_consumers"); - const string groupName = "test_group"; + var messageId = db.StreamAdd(key, fields, id); + var entries = db.StreamRange(key); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + Assert.Equal(id, messageId); + Assert.NotNull(entries); + Assert.Single(entries); + Assert.Equal(id, entries[0].Id); + } - var db = conn.GetDatabase(); + [Fact] + public void StreamConsumerGroupSetId() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - // Create a stream - db.StreamAdd(key, "field1", "value1"); + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_set_id"); + const string groupName = "test_group", + consumer = "consumer"; - // Create a group - db.StreamCreateConsumerGroup(key, groupName, "0-0"); + // Create a stream + db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); - // Query redis for the group consumers, expect an empty list in response. - var consumers = db.StreamConsumerInfo(key, groupName); + // Create a group and set the position to deliver new messages only. + db.StreamCreateConsumerGroup(key, groupName, StreamPosition.NewMessages); - Assert.Empty(consumers); - } - } + // Read into the group, expect nothing + var firstRead = db.StreamReadGroup(key, groupName, consumer, StreamPosition.NewMessages); - [Fact] - public void StreamCreateConsumerGroup() - { - var key = GetUniqueKey("group_create"); - const string groupName = "test_group"; + // Reset the ID back to read from the beginning. + db.StreamConsumerGroupSetPosition(key, groupName, StreamPosition.Beginning); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var secondRead = db.StreamReadGroup(key, groupName, consumer, StreamPosition.NewMessages); - var db = conn.GetDatabase(); + Assert.NotNull(firstRead); + Assert.NotNull(secondRead); + Assert.Empty(firstRead); + Assert.Equal(2, secondRead.Length); + } - // Create a stream - db.StreamAdd(key, "field1", "value1"); + [Fact] + public void StreamConsumerGroupWithNoConsumers() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - // Create a group - var result = db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_with_no_consumers"); + const string groupName = "test_group"; - Assert.True(result); - } - } + // Create a stream + db.StreamAdd(key, "field1", "value1"); - [Fact] - public void StreamCreateConsumerGroupBeforeCreatingStream() - { - var key = GetUniqueKey("group_create_before_stream"); + // Create a group + db.StreamCreateConsumerGroup(key, groupName, "0-0"); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + // Query redis for the group consumers, expect an empty list in response. + var consumers = db.StreamConsumerInfo(key, groupName); - var db = conn.GetDatabase(); - - // Ensure the key doesn't exist. - var keyExistsBeforeCreate = db.KeyExists(key); + Assert.Empty(consumers); + } - // The 'createStream' parameter is 'true' by default. - var groupCreated = db.StreamCreateConsumerGroup(key, "consumerGroup", StreamPosition.NewMessages); + [Fact] + public void StreamCreateConsumerGroup() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var keyExistsAfterCreate = db.KeyExists(key); + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_create"); + const string groupName = "test_group"; - Assert.False(keyExistsBeforeCreate); - Assert.True(groupCreated); - Assert.True(keyExistsAfterCreate); - } - } + // Create a stream + db.StreamAdd(key, "field1", "value1"); - [Fact] - public void StreamCreateConsumerGroupFailsIfKeyDoesntExist() - { - var key = GetUniqueKey("group_create_before_stream_should_fail"); + // Create a group + var result = db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - - // Pass 'false' for 'createStream' to ensure that an - // exception is thrown when the stream doesn't exist. - Assert.ThrowsAny(() => db.StreamCreateConsumerGroup( - key, - "consumerGroup", - StreamPosition.NewMessages, - createStream: false)); - } - } + Assert.True(result); + } - [Fact] - public void StreamCreateConsumerGroupSucceedsWhenKeyExists() - { - var key = GetUniqueKey("group_create_after_stream"); + [Fact] + public void StreamCreateConsumerGroupBeforeCreatingStream() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_create_before_stream"); - var db = conn.GetDatabase(); + // Ensure the key doesn't exist. + var keyExistsBeforeCreate = db.KeyExists(key); - db.StreamAdd(key, "f1", "v1"); + // The 'createStream' parameter is 'true' by default. + var groupCreated = db.StreamCreateConsumerGroup(key, "consumerGroup", StreamPosition.NewMessages); - // Pass 'false' for 'createStream', should create the consumer group - // without issue since the stream already exists. - var groupCreated = db.StreamCreateConsumerGroup( - key, - "consumerGroup", - StreamPosition.NewMessages, - createStream: false); + var keyExistsAfterCreate = db.KeyExists(key); - Assert.True(groupCreated); - } - } - - [Fact] - public void StreamConsumerGroupReadOnlyNewMessagesWithEmptyResponse() - { - var key = GetUniqueKey("group_read"); - const string groupName = "test_group"; + Assert.False(keyExistsBeforeCreate); + Assert.True(groupCreated); + Assert.True(keyExistsAfterCreate); + } - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + [Fact] + public void StreamCreateConsumerGroupFailsIfKeyDoesntExist() + { + using var conn = Create(require: RedisFeatures.v5_0_0); + + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_create_before_stream_should_fail"); + + // Pass 'false' for 'createStream' to ensure that an + // exception is thrown when the stream doesn't exist. + Assert.ThrowsAny(() => db.StreamCreateConsumerGroup( + key, + "consumerGroup", + StreamPosition.NewMessages, + createStream: false)); + } - var db = conn.GetDatabase(); + [Fact] + public void StreamCreateConsumerGroupSucceedsWhenKeyExists() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - // Create a stream - db.StreamAdd(key, "field1", "value1"); - db.StreamAdd(key, "field2", "value2"); + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_create_after_stream"); - // Create a group. - db.StreamCreateConsumerGroup(key, groupName); + db.StreamAdd(key, "f1", "v1"); - // Read, expect no messages - var entries = db.StreamReadGroup(key, groupName, "test_consumer", "0-0"); + // Pass 'false' for 'createStream', should create the consumer group + // without issue since the stream already exists. + var groupCreated = db.StreamCreateConsumerGroup( + key, + "consumerGroup", + StreamPosition.NewMessages, + createStream: false); - Assert.Empty(entries); - } - } + Assert.True(groupCreated); + } - [Fact] - public void StreamConsumerGroupReadFromStreamBeginning() - { - var key = GetUniqueKey("group_read_beginning"); - const string groupName = "test_group"; + [Fact] + public void StreamConsumerGroupReadOnlyNewMessagesWithEmptyResponse() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_read"); + const string groupName = "test_group"; - var db = conn.GetDatabase(); + // Create a stream + db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); - var id1 = db.StreamAdd(key, "field1", "value1"); - var id2 = db.StreamAdd(key, "field2", "value2"); + // Create a group. + db.StreamCreateConsumerGroup(key, groupName); - db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); + // Read, expect no messages + var entries = db.StreamReadGroup(key, groupName, "test_consumer", "0-0"); - var entries = db.StreamReadGroup(key, groupName, "test_consumer", StreamPosition.NewMessages); + Assert.Empty(entries); + } - Assert.Equal(2, entries.Length); - Assert.True(id1 == entries[0].Id); - Assert.True(id2 == entries[1].Id); - } - } + [Fact] + public void StreamConsumerGroupReadFromStreamBeginning() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - [Fact] - public void StreamConsumerGroupReadFromStreamBeginningWithCount() - { - var key = GetUniqueKey("group_read_with_count"); - const string groupName = "test_group"; + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_read_beginning"); + const string groupName = "test_group"; - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var id1 = db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); - var db = conn.GetDatabase(); + db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); - var id1 = db.StreamAdd(key, "field1", "value1"); - var id2 = db.StreamAdd(key, "field2", "value2"); - var id3 = db.StreamAdd(key, "field3", "value3"); - _ = db.StreamAdd(key, "field4", "value4"); + var entries = db.StreamReadGroup(key, groupName, "test_consumer", StreamPosition.NewMessages); - // Start reading after id1. - db.StreamCreateConsumerGroup(key, groupName, id1); + Assert.Equal(2, entries.Length); + Assert.True(id1 == entries[0].Id); + Assert.True(id2 == entries[1].Id); + } - var entries = db.StreamReadGroup(key, groupName, "test_consumer", StreamPosition.NewMessages, 2); + [Fact] + public void StreamConsumerGroupReadFromStreamBeginningWithCount() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - // Ensure we only received the requested count and that the IDs match the expected values. - Assert.Equal(2, entries.Length); - Assert.True(id2 == entries[0].Id); - Assert.True(id3 == entries[1].Id); - } - } + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_read_with_count"); + const string groupName = "test_group"; - [Fact] - public void StreamConsumerGroupAcknowledgeMessage() - { - var key = GetUniqueKey("group_ack"); - const string groupName = "test_group"; - const string consumer = "test_consumer"; + var id1 = db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); + var id3 = db.StreamAdd(key, "field3", "value3"); + _ = db.StreamAdd(key, "field4", "value4"); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + // Start reading after id1. + db.StreamCreateConsumerGroup(key, groupName, id1); - var db = conn.GetDatabase(); + var entries = db.StreamReadGroup(key, groupName, "test_consumer", StreamPosition.NewMessages, 2); - var id1 = db.StreamAdd(key, "field1", "value1"); - var id2 = db.StreamAdd(key, "field2", "value2"); - var id3 = db.StreamAdd(key, "field3", "value3"); - var id4 = db.StreamAdd(key, "field4", "value4"); + // Ensure we only received the requested count and that the IDs match the expected values. + Assert.Equal(2, entries.Length); + Assert.True(id2 == entries[0].Id); + Assert.True(id3 == entries[1].Id); + } - db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); + [Fact] + public void StreamConsumerGroupAcknowledgeMessage() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - // Read all 4 messages, they will be assigned to the consumer - var entries = db.StreamReadGroup(key, groupName, consumer, StreamPosition.NewMessages); + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_ack"); + const string groupName = "test_group", + consumer = "test_consumer"; - // Send XACK for 3 of the messages + var id1 = db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); + var id3 = db.StreamAdd(key, "field3", "value3"); + var id4 = db.StreamAdd(key, "field4", "value4"); - // Single message Id overload. - var oneAck = db.StreamAcknowledge(key, groupName, id1); + db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); - // Multiple message Id overload. - var twoAck = db.StreamAcknowledge(key, groupName, new [] { id3, id4 }); + // Read all 4 messages, they will be assigned to the consumer + var entries = db.StreamReadGroup(key, groupName, consumer, StreamPosition.NewMessages); - // Read the group again, it should only return the unacknowledged message. - var notAcknowledged = db.StreamReadGroup(key, groupName, consumer, "0-0"); + // Send XACK for 3 of the messages - Assert.Equal(4, entries.Length); - Assert.Equal(1, oneAck); - Assert.Equal(2, twoAck); - Assert.Single(notAcknowledged); - Assert.Equal(id2, notAcknowledged[0].Id); - } - } + // Single message Id overload. + var oneAck = db.StreamAcknowledge(key, groupName, id1); - [Fact] - public void StreamConsumerGroupClaimMessages() - { - var key = GetUniqueKey("group_claim"); - const string groupName = "test_group"; - const string consumer1 = "test_consumer_1"; - const string consumer2 = "test_consumer_2"; + // Multiple message Id overload. + var twoAck = db.StreamAcknowledge(key, groupName, new[] { id3, id4 }); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + // Read the group again, it should only return the unacknowledged message. + var notAcknowledged = db.StreamReadGroup(key, groupName, consumer, "0-0"); - var db = conn.GetDatabase(); + Assert.Equal(4, entries.Length); + Assert.Equal(1, oneAck); + Assert.Equal(2, twoAck); + Assert.Single(notAcknowledged); + Assert.Equal(id2, notAcknowledged[0].Id); + } - _ = db.StreamAdd(key, "field1", "value1"); - _ = db.StreamAdd(key, "field2", "value2"); - _ = db.StreamAdd(key, "field3", "value3"); - _ = db.StreamAdd(key, "field4", "value4"); + [Fact] + public void StreamConsumerGroupClaimMessages() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - db.StreamCreateConsumerGroup(key, groupName, "0-0"); + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_claim"); + const string groupName = "test_group", + consumer1 = "test_consumer_1", + consumer2 = "test_consumer_2"; - // Read a single message into the first consumer. - db.StreamReadGroup(key, groupName, consumer1, count: 1); + _ = db.StreamAdd(key, "field1", "value1"); + _ = db.StreamAdd(key, "field2", "value2"); + _ = db.StreamAdd(key, "field3", "value3"); + _ = db.StreamAdd(key, "field4", "value4"); - // Read the remaining messages into the second consumer. - db.StreamReadGroup(key, groupName, consumer2); + db.StreamCreateConsumerGroup(key, groupName, "0-0"); - // Claim the 3 messages consumed by consumer2 for consumer1. + // Read a single message into the first consumer. + db.StreamReadGroup(key, groupName, consumer1, count: 1); - // Get the pending messages for consumer2. - var pendingMessages = db.StreamPendingMessages(key, groupName, - 10, - consumer2); + // Read the remaining messages into the second consumer. + db.StreamReadGroup(key, groupName, consumer2); - // Claim the messages for consumer1. - var messages = db.StreamClaim(key, - groupName, - consumer1, - 0, // Min message idle time - messageIds: pendingMessages.Select(pm => pm.MessageId).ToArray()); + // Claim the 3 messages consumed by consumer2 for consumer1. - // Now see how many messages are pending for each consumer - var pendingSummary = db.StreamPending(key, groupName); + // Get the pending messages for consumer2. + var pendingMessages = db.StreamPendingMessages(key, groupName, + 10, + consumer2); - Assert.NotNull(pendingSummary.Consumers); - Assert.Single(pendingSummary.Consumers); - Assert.Equal(4, pendingSummary.Consumers[0].PendingMessageCount); - Assert.Equal(pendingMessages.Length, messages.Length); - } - } + // Claim the messages for consumer1. + var messages = db.StreamClaim(key, + groupName, + consumer1, + 0, // Min message idle time + messageIds: pendingMessages.Select(pm => pm.MessageId).ToArray()); - [Fact] - public void StreamConsumerGroupClaimMessagesReturningIds() - { - var key = GetUniqueKey("group_claim_view_ids"); - const string groupName = "test_group"; - const string consumer1 = "test_consumer_1"; - const string consumer2 = "test_consumer_2"; + // Now see how many messages are pending for each consumer + var pendingSummary = db.StreamPending(key, groupName); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + Assert.NotNull(pendingSummary.Consumers); + Assert.Single(pendingSummary.Consumers); + Assert.Equal(4, pendingSummary.Consumers[0].PendingMessageCount); + Assert.Equal(pendingMessages.Length, messages.Length); + } - var db = conn.GetDatabase(); + [Fact] + public void StreamConsumerGroupClaimMessagesReturningIds() + { + using var conn = Create(require: RedisFeatures.v5_0_0); + + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_claim_view_ids"); + const string groupName = "test_group", + consumer1 = "test_consumer_1", + consumer2 = "test_consumer_2"; + + _ = db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); + var id3 = db.StreamAdd(key, "field3", "value3"); + var id4 = db.StreamAdd(key, "field4", "value4"); + + db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); + + // Read a single message into the first consumer. + _ = db.StreamReadGroup(key, groupName, consumer1, StreamPosition.NewMessages, 1); + + // Read the remaining messages into the second consumer. + _ = db.StreamReadGroup(key, groupName, consumer2); + + // Claim the 3 messages consumed by consumer2 for consumer1. + + // Get the pending messages for consumer2. + var pendingMessages = db.StreamPendingMessages(key, groupName, + 10, + consumer2); + + // Claim the messages for consumer1. + var messageIds = db.StreamClaimIdsOnly(key, + groupName, + consumer1, + 0, // Min message idle time + messageIds: pendingMessages.Select(pm => pm.MessageId).ToArray()); + + // We should get an array of 3 message IDs. + Assert.Equal(3, messageIds.Length); + Assert.Equal(id2, messageIds[0]); + Assert.Equal(id3, messageIds[1]); + Assert.Equal(id4, messageIds[2]); + } - _ = db.StreamAdd(key, "field1", "value1"); - var id2 = db.StreamAdd(key, "field2", "value2"); - var id3 = db.StreamAdd(key, "field3", "value3"); - var id4 = db.StreamAdd(key, "field4", "value4"); + [Fact] + public void StreamConsumerGroupReadMultipleOneReadBeginningOneReadNew() + { + // Create a group for each stream. One set to read from the beginning of the + // stream and the other to begin reading only new messages. - db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); + // Ask redis to read from the beginning of both stream, expect messages + // for only the stream set to read from the beginning. - // Read a single message into the first consumer. - _ = db.StreamReadGroup(key, groupName, consumer1, StreamPosition.NewMessages, 1); + using var conn = Create(require: RedisFeatures.v5_0_0); - // Read the remaining messages into the second consumer. - _ = db.StreamReadGroup(key, groupName, consumer2); + var db = conn.GetDatabase(); + const string groupName = "test_group"; + var stream1 = GetUniqueKey("stream1a"); + var stream2 = GetUniqueKey("stream2a"); - // Claim the 3 messages consumed by consumer2 for consumer1. + db.StreamAdd(stream1, "field1-1", "value1-1"); + db.StreamAdd(stream1, "field1-2", "value1-2"); - // Get the pending messages for consumer2. - var pendingMessages = db.StreamPendingMessages(key, groupName, - 10, - consumer2); + db.StreamAdd(stream2, "field2-1", "value2-1"); + db.StreamAdd(stream2, "field2-2", "value2-2"); + db.StreamAdd(stream2, "field2-3", "value2-3"); - // Claim the messages for consumer1. - var messageIds = db.StreamClaimIdsOnly(key, - groupName, - consumer1, - 0, // Min message idle time - messageIds: pendingMessages.Select(pm => pm.MessageId).ToArray()); + // stream1 set up to read only new messages. + db.StreamCreateConsumerGroup(stream1, groupName, StreamPosition.NewMessages); - // We should get an array of 3 message IDs. - Assert.Equal(3, messageIds.Length); - Assert.Equal(id2, messageIds[0]); - Assert.Equal(id3, messageIds[1]); - Assert.Equal(id4, messageIds[2]); - } - } + // stream2 set up to read from the beginning of the stream + db.StreamCreateConsumerGroup(stream2, groupName, StreamPosition.Beginning); - [Fact] - public void StreamConsumerGroupReadMultipleOneReadBeginningOneReadNew() + // Read for both streams from the beginning. We shouldn't get anything back for stream1. + var pairs = new[] { - // Create a group for each stream. One set to read from the beginning of the - // stream and the other to begin reading only new messages. - - // Ask redis to read from the beginning of both stream, expect messages - // for only the stream set to read from the beginning. - - const string groupName = "test_group"; - var stream1 = GetUniqueKey("stream1a"); - var stream2 = GetUniqueKey("stream2a"); - - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - - db.StreamAdd(stream1, "field1-1", "value1-1"); - db.StreamAdd(stream1, "field1-2", "value1-2"); + // StreamPosition.NewMessages will send ">" which indicates "Undelivered" messages. + new StreamPosition(stream1, StreamPosition.NewMessages), + new StreamPosition(stream2, StreamPosition.NewMessages) + }; - db.StreamAdd(stream2, "field2-1", "value2-1"); - db.StreamAdd(stream2, "field2-2", "value2-2"); - db.StreamAdd(stream2, "field2-3", "value2-3"); + var streams = db.StreamReadGroup(pairs, groupName, "test_consumer"); - // stream1 set up to read only new messages. - db.StreamCreateConsumerGroup(stream1, groupName, StreamPosition.NewMessages); + Assert.NotNull(streams); + Assert.Single(streams); + Assert.Equal(stream2, streams[0].Key); + Assert.Equal(3, streams[0].Entries.Length); + } - // stream2 set up to read from the beginning of the stream - db.StreamCreateConsumerGroup(stream2, groupName, StreamPosition.Beginning); + [Fact] + public void StreamConsumerGroupReadMultipleOnlyNewMessagesExpectNoResult() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - // Read for both streams from the beginning. We shouldn't get anything back for stream1. - var pairs = new [] - { - // StreamPosition.NewMessages will send ">" which indicates "Undelivered" messages. - new StreamPosition(stream1, StreamPosition.NewMessages), - new StreamPosition(stream2, StreamPosition.NewMessages) - }; + var db = conn.GetDatabase(); + const string groupName = "test_group"; + var stream1 = GetUniqueKey("stream1b"); + var stream2 = GetUniqueKey("stream2b"); - var streams = db.StreamReadGroup(pairs, groupName, "test_consumer"); + db.StreamAdd(stream1, "field1-1", "value1-1"); + db.StreamAdd(stream2, "field2-1", "value2-1"); - Assert.NotNull(streams); - Assert.Single(streams); - Assert.Equal(stream2, streams[0].Key); - Assert.Equal(3, streams[0].Entries.Length); - } - } + // set both streams to read only new messages (default behavior). + db.StreamCreateConsumerGroup(stream1, groupName); + db.StreamCreateConsumerGroup(stream2, groupName); - [Fact] - public void StreamConsumerGroupReadMultipleOnlyNewMessagesExpectNoResult() + // We shouldn't get anything for either stream. + var pairs = new[] { - const string groupName = "test_group"; - var stream1 = GetUniqueKey("stream1b"); - var stream2 = GetUniqueKey("stream2b"); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); + new StreamPosition(stream1, StreamPosition.Beginning), + new StreamPosition(stream2, StreamPosition.Beginning) + }; - db.StreamAdd(stream1, "field1-1", "value1-1"); - db.StreamAdd(stream2, "field2-1", "value2-1"); + var streams = db.StreamReadGroup(pairs, groupName, "test_consumer"); - // set both streams to read only new messages (default behavior). - db.StreamCreateConsumerGroup(stream1, groupName); - db.StreamCreateConsumerGroup(stream2, groupName); + Assert.NotNull(streams); + Assert.Equal(2, streams.Length); + Assert.Empty(streams[0].Entries); + Assert.Empty(streams[1].Entries); + } - // We shouldn't get anything for either stream. - var pairs = new [] - { - new StreamPosition(stream1, StreamPosition.Beginning), - new StreamPosition(stream2, StreamPosition.Beginning) - }; + [Fact] + public void StreamConsumerGroupReadMultipleOnlyNewMessagesExpect1Result() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var streams = db.StreamReadGroup(pairs, groupName, "test_consumer"); + var db = conn.GetDatabase(); + const string groupName = "test_group"; + var stream1 = GetUniqueKey("stream1c"); + var stream2 = GetUniqueKey("stream2c"); - Assert.NotNull(streams); - Assert.Equal(2, streams.Length); - Assert.Empty(streams[0].Entries); - Assert.Empty(streams[1].Entries); - } - } + // These messages won't be read. + db.StreamAdd(stream1, "field1-1", "value1-1"); + db.StreamAdd(stream2, "field2-1", "value2-1"); - [Fact] - public void StreamConsumerGroupReadMultipleOnlyNewMessagesExpect1Result() - { - const string groupName = "test_group"; - var stream1 = GetUniqueKey("stream1c"); - var stream2 = GetUniqueKey("stream2c"); + // set both streams to read only new messages (default behavior). + db.StreamCreateConsumerGroup(stream1, groupName); + db.StreamCreateConsumerGroup(stream2, groupName); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - - // These messages won't be read. - db.StreamAdd(stream1, "field1-1", "value1-1"); - db.StreamAdd(stream2, "field2-1", "value2-1"); - - // set both streams to read only new messages (default behavior). - db.StreamCreateConsumerGroup(stream1, groupName); - db.StreamCreateConsumerGroup(stream2, groupName); - - // We should read these though. - var id1 = db.StreamAdd(stream1, "field1-2", "value1-2"); - var id2 = db.StreamAdd(stream2, "field2-2", "value2-2"); - - // Read the new messages (messages created after the group was created). - var pairs = new [] - { - new StreamPosition(stream1, StreamPosition.NewMessages), - new StreamPosition(stream2, StreamPosition.NewMessages) - }; - - var streams = db.StreamReadGroup(pairs, groupName, "test_consumer"); - - Assert.NotNull(streams); - Assert.Equal(2, streams.Length); - Assert.Single(streams[0].Entries); - Assert.Single(streams[1].Entries); - Assert.Equal(id1, streams[0].Entries[0].Id); - Assert.Equal(id2, streams[1].Entries[0].Id); - } - } + // We should read these though. + var id1 = db.StreamAdd(stream1, "field1-2", "value1-2"); + var id2 = db.StreamAdd(stream2, "field2-2", "value2-2"); - [Fact] - public void StreamConsumerGroupReadMultipleRestrictCount() + // Read the new messages (messages created after the group was created). + var pairs = new[] { - const string groupName = "test_group"; - var stream1 = GetUniqueKey("stream1d"); - var stream2 = GetUniqueKey("stream2d"); + new StreamPosition(stream1, StreamPosition.NewMessages), + new StreamPosition(stream2, StreamPosition.NewMessages) + }; - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var streams = db.StreamReadGroup(pairs, groupName, "test_consumer"); - var db = conn.GetDatabase(); + Assert.NotNull(streams); + Assert.Equal(2, streams.Length); + Assert.Single(streams[0].Entries); + Assert.Single(streams[1].Entries); + Assert.Equal(id1, streams[0].Entries[0].Id); + Assert.Equal(id2, streams[1].Entries[0].Id); + } - var id1_1 = db.StreamAdd(stream1, "field1-1", "value1-1"); - var id1_2 = db.StreamAdd(stream1, "field1-2", "value1-2"); + [Fact] + public void StreamConsumerGroupReadMultipleRestrictCount() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var id2_1 = db.StreamAdd(stream2, "field2-1", "value2-1"); - _ = db.StreamAdd(stream2, "field2-2", "value2-2"); - _ = db.StreamAdd(stream2, "field2-3", "value2-3"); + var db = conn.GetDatabase(); + const string groupName = "test_group"; + var stream1 = GetUniqueKey("stream1d"); + var stream2 = GetUniqueKey("stream2d"); - // Set the initial read point in each stream, *after* the first ID in both streams. - db.StreamCreateConsumerGroup(stream1, groupName, id1_1); - db.StreamCreateConsumerGroup(stream2, groupName, id2_1); + var id1_1 = db.StreamAdd(stream1, "field1-1", "value1-1"); + var id1_2 = db.StreamAdd(stream1, "field1-2", "value1-2"); - var pairs = new [] - { - // Read after the first id in both streams - new StreamPosition(stream1, StreamPosition.NewMessages), - new StreamPosition(stream2, StreamPosition.NewMessages) - }; + var id2_1 = db.StreamAdd(stream2, "field2-1", "value2-1"); + _ = db.StreamAdd(stream2, "field2-2", "value2-2"); + _ = db.StreamAdd(stream2, "field2-3", "value2-3"); - // Restrict the count to 2 (expect only 1 message from first stream, 2 from the second). - var streams = db.StreamReadGroup(pairs, groupName, "test_consumer", 2); + // Set the initial read point in each stream, *after* the first ID in both streams. + db.StreamCreateConsumerGroup(stream1, groupName, id1_1); + db.StreamCreateConsumerGroup(stream2, groupName, id2_1); - Assert.NotNull(streams); - Assert.Equal(2, streams.Length); - Assert.Single(streams[0].Entries); - Assert.Equal(2, streams[1].Entries.Length); - Assert.Equal(id1_2, streams[0].Entries[0].Id); - } - } - - [Fact] - public void StreamConsumerGroupViewPendingInfoNoConsumers() + var pairs = new[] { - var key = GetUniqueKey("group_pending_info_no_consumers"); - const string groupName = "test_group"; - - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - - db.StreamAdd(key, "field1", "value1"); - - db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); + // Read after the first id in both streams + new StreamPosition(stream1, StreamPosition.NewMessages), + new StreamPosition(stream2, StreamPosition.NewMessages) + }; - var pendingInfo = db.StreamPending(key, groupName); + // Restrict the count to 2 (expect only 1 message from first stream, 2 from the second). + var streams = db.StreamReadGroup(pairs, groupName, "test_consumer", 2); - Assert.Equal(0, pendingInfo.PendingMessageCount); - Assert.Equal(RedisValue.Null, pendingInfo.LowestPendingMessageId); - Assert.Equal(RedisValue.Null, pendingInfo.HighestPendingMessageId); - Assert.NotNull(pendingInfo.Consumers); - Assert.Empty(pendingInfo.Consumers); - } - } - - [Fact] - public void StreamConsumerGroupViewPendingInfoWhenNothingPending() - { - var key = GetUniqueKey("group_pending_info_nothing_pending"); - const string groupName = "test_group"; + Assert.NotNull(streams); + Assert.Equal(2, streams.Length); + Assert.Single(streams[0].Entries); + Assert.Equal(2, streams[1].Entries.Length); + Assert.Equal(id1_2, streams[0].Entries[0].Id); + } - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + [Fact] + public void StreamConsumerGroupViewPendingInfoNoConsumers() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var db = conn.GetDatabase(); + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_pending_info_no_consumers"); + const string groupName = "test_group"; - db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field1", "value1"); - db.StreamCreateConsumerGroup(key, groupName, "0-0"); + db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); - var pendingMessages = db.StreamPendingMessages(key, - groupName, - 10, - consumerName: RedisValue.Null); + var pendingInfo = db.StreamPending(key, groupName); - Assert.NotNull(pendingMessages); - Assert.Empty(pendingMessages); - } - } + Assert.Equal(0, pendingInfo.PendingMessageCount); + Assert.Equal(RedisValue.Null, pendingInfo.LowestPendingMessageId); + Assert.Equal(RedisValue.Null, pendingInfo.HighestPendingMessageId); + Assert.NotNull(pendingInfo.Consumers); + Assert.Empty(pendingInfo.Consumers); + } - [Fact] - public void StreamConsumerGroupViewPendingInfoSummary() - { - var key = GetUniqueKey("group_pending_info"); - const string groupName = "test_group"; - const string consumer1 = "test_consumer_1"; - const string consumer2 = "test_consumer_2"; + [Fact] + public void StreamConsumerGroupViewPendingInfoWhenNothingPending() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_pending_info_nothing_pending"); + const string groupName = "test_group"; - var db = conn.GetDatabase(); + db.StreamAdd(key, "field1", "value1"); - var id1 = db.StreamAdd(key, "field1", "value1"); - db.StreamAdd(key, "field2", "value2"); - db.StreamAdd(key, "field3", "value3"); - var id4 = db.StreamAdd(key, "field4", "value4"); + db.StreamCreateConsumerGroup(key, groupName, "0-0"); - db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); + var pendingMessages = db.StreamPendingMessages(key, + groupName, + 10, + consumerName: RedisValue.Null); - // Read a single message into the first consumer. - db.StreamReadGroup(key, groupName, consumer1, StreamPosition.NewMessages, 1); + Assert.NotNull(pendingMessages); + Assert.Empty(pendingMessages); + } - // Read the remaining messages into the second consumer. - db.StreamReadGroup(key, groupName, consumer2); + [Fact] + public void StreamConsumerGroupViewPendingInfoSummary() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var pendingInfo = db.StreamPending(key, groupName); + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_pending_info"); + const string groupName = "test_group", + consumer1 = "test_consumer_1", + consumer2 = "test_consumer_2"; - Assert.Equal(4, pendingInfo.PendingMessageCount); - Assert.Equal(id1, pendingInfo.LowestPendingMessageId); - Assert.Equal(id4, pendingInfo.HighestPendingMessageId); - Assert.True(pendingInfo.Consumers.Length == 2); + var id1 = db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); + db.StreamAdd(key, "field3", "value3"); + var id4 = db.StreamAdd(key, "field4", "value4"); - var consumer1Count = pendingInfo.Consumers.First(c => c.Name == consumer1).PendingMessageCount; - var consumer2Count = pendingInfo.Consumers.First(c => c.Name == consumer2).PendingMessageCount; + db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); - Assert.Equal(1, consumer1Count); - Assert.Equal(3, consumer2Count); - } - } + // Read a single message into the first consumer. + db.StreamReadGroup(key, groupName, consumer1, StreamPosition.NewMessages, 1); - [Fact] - public async Task StreamConsumerGroupViewPendingMessageInfo() - { - var key = GetUniqueKey("group_pending_messages"); - const string groupName = "test_group"; - const string consumer1 = "test_consumer_1"; - const string consumer2 = "test_consumer_2"; + // Read the remaining messages into the second consumer. + db.StreamReadGroup(key, groupName, consumer2); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var pendingInfo = db.StreamPending(key, groupName); - var db = conn.GetDatabase(); + Assert.Equal(4, pendingInfo.PendingMessageCount); + Assert.Equal(id1, pendingInfo.LowestPendingMessageId); + Assert.Equal(id4, pendingInfo.HighestPendingMessageId); + Assert.True(pendingInfo.Consumers.Length == 2); - var id1 = db.StreamAdd(key, "field1", "value1"); - db.StreamAdd(key, "field2", "value2"); - db.StreamAdd(key, "field3", "value3"); - db.StreamAdd(key, "field4", "value4"); + var consumer1Count = pendingInfo.Consumers.First(c => c.Name == consumer1).PendingMessageCount; + var consumer2Count = pendingInfo.Consumers.First(c => c.Name == consumer2).PendingMessageCount; - db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); + Assert.Equal(1, consumer1Count); + Assert.Equal(3, consumer2Count); + } - // Read a single message into the first consumer. - db.StreamReadGroup(key, groupName, consumer1, count: 1); + [Fact] + public async Task StreamConsumerGroupViewPendingMessageInfo() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - // Read the remaining messages into the second consumer. - _ = db.StreamReadGroup(key, groupName, consumer2) ?? throw new ArgumentNullException(nameof(consumer2), "db.StreamReadGroup(key, groupName, consumer2)"); + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_pending_messages"); + const string groupName = "test_group", + consumer1 = "test_consumer_1", + consumer2 = "test_consumer_2"; - await Task.Delay(10).ForAwait(); + var id1 = db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); + db.StreamAdd(key, "field3", "value3"); + db.StreamAdd(key, "field4", "value4"); - // Get the pending info about the messages themselves. - var pendingMessageInfoList = db.StreamPendingMessages(key, groupName, 10, RedisValue.Null); + db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); - Assert.NotNull(pendingMessageInfoList); - Assert.Equal(4, pendingMessageInfoList.Length); - Assert.Equal(consumer1, pendingMessageInfoList[0].ConsumerName); - Assert.Equal(1, pendingMessageInfoList[0].DeliveryCount); - Assert.True((int)pendingMessageInfoList[0].IdleTimeInMilliseconds > 0); - Assert.Equal(id1, pendingMessageInfoList[0].MessageId); - } - } + // Read a single message into the first consumer. + db.StreamReadGroup(key, groupName, consumer1, count: 1); - [Fact] - public void StreamConsumerGroupViewPendingMessageInfoForConsumer() - { - var key = GetUniqueKey("group_pending_for_consumer"); - const string groupName = "test_group"; - const string consumer1 = "test_consumer_1"; - const string consumer2 = "test_consumer_2"; + // Read the remaining messages into the second consumer. + _ = db.StreamReadGroup(key, groupName, consumer2) ?? throw new ArgumentNullException(nameof(consumer2), "db.StreamReadGroup(key, groupName, consumer2)"); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + await Task.Delay(10).ForAwait(); - var db = conn.GetDatabase(); + // Get the pending info about the messages themselves. + var pendingMessageInfoList = db.StreamPendingMessages(key, groupName, 10, RedisValue.Null); - db.StreamAdd(key, "field1", "value1"); - db.StreamAdd(key, "field2", "value2"); - db.StreamAdd(key, "field3", "value3"); - db.StreamAdd(key, "field4", "value4"); + Assert.NotNull(pendingMessageInfoList); + Assert.Equal(4, pendingMessageInfoList.Length); + Assert.Equal(consumer1, pendingMessageInfoList[0].ConsumerName); + Assert.Equal(1, pendingMessageInfoList[0].DeliveryCount); + Assert.True((int)pendingMessageInfoList[0].IdleTimeInMilliseconds > 0); + Assert.Equal(id1, pendingMessageInfoList[0].MessageId); + } - db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); + [Fact] + public void StreamConsumerGroupViewPendingMessageInfoForConsumer() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - // Read a single message into the first consumer. - db.StreamReadGroup(key, groupName, consumer1, count: 1); + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_pending_for_consumer"); + const string groupName = "test_group", + consumer1 = "test_consumer_1", + consumer2 = "test_consumer_2"; - // Read the remaining messages into the second consumer. - db.StreamReadGroup(key, groupName, consumer2); + db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); + db.StreamAdd(key, "field3", "value3"); + db.StreamAdd(key, "field4", "value4"); - // Get the pending info about the messages themselves. - var pendingMessageInfoList = db.StreamPendingMessages(key, - groupName, - 10, - consumer2); + db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); - Assert.NotNull(pendingMessageInfoList); - Assert.Equal(3, pendingMessageInfoList.Length); - } - } + // Read a single message into the first consumer. + db.StreamReadGroup(key, groupName, consumer1, count: 1); - [Fact] - public void StreamDeleteConsumer() - { - var key = GetUniqueKey("delete_consumer"); - const string groupName = "test_group"; - const string consumer = "test_consumer"; + // Read the remaining messages into the second consumer. + db.StreamReadGroup(key, groupName, consumer2); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + // Get the pending info about the messages themselves. + var pendingMessageInfoList = db.StreamPendingMessages(key, + groupName, + 10, + consumer2); - var db = conn.GetDatabase(); + Assert.NotNull(pendingMessageInfoList); + Assert.Equal(3, pendingMessageInfoList.Length); + } - // Add a message to create the stream. - db.StreamAdd(key, "field1", "value1"); - db.StreamAdd(key, "field2", "value2"); + [Fact] + public void StreamDeleteConsumer() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - // Create a consumer group and read the message. - db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); - db.StreamReadGroup(key, groupName, consumer, StreamPosition.NewMessages); + var db = conn.GetDatabase(); + var key = GetUniqueKey("delete_consumer"); + const string groupName = "test_group", + consumer = "test_consumer"; - var preDeleteConsumers = db.StreamConsumerInfo(key, groupName); + // Add a message to create the stream. + db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); - // Delete the consumer. - var deleteResult = db.StreamDeleteConsumer(key, groupName, consumer); + // Create a consumer group and read the message. + db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); + db.StreamReadGroup(key, groupName, consumer, StreamPosition.NewMessages); - // Should get 2 messages in the deleteResult. - var postDeleteConsumers = db.StreamConsumerInfo(key, groupName); + var preDeleteConsumers = db.StreamConsumerInfo(key, groupName); - Assert.Equal(2, deleteResult); - Assert.Single(preDeleteConsumers); - Assert.Empty(postDeleteConsumers); - } - } + // Delete the consumer. + var deleteResult = db.StreamDeleteConsumer(key, groupName, consumer); - [Fact] - public void StreamDeleteConsumerGroup() - { - var key = GetUniqueKey("delete_consumer_group"); - const string groupName = "test_group"; - const string consumer = "test_consumer"; + // Should get 2 messages in the deleteResult. + var postDeleteConsumers = db.StreamConsumerInfo(key, groupName); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + Assert.Equal(2, deleteResult); + Assert.Single(preDeleteConsumers); + Assert.Empty(postDeleteConsumers); + } - var db = conn.GetDatabase(); + [Fact] + public void StreamDeleteConsumerGroup() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - // Add a message to create the stream. - db.StreamAdd(key, "field1", "value1"); + var db = conn.GetDatabase(); + var key = GetUniqueKey("delete_consumer_group"); + const string groupName = "test_group", + consumer = "test_consumer"; - // Create a consumer group and read the messages. - db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); - db.StreamReadGroup(key, groupName, consumer, StreamPosition.Beginning); + // Add a message to create the stream. + db.StreamAdd(key, "field1", "value1"); - var preDeleteInfo = db.StreamInfo(key); + // Create a consumer group and read the messages. + db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); + db.StreamReadGroup(key, groupName, consumer, StreamPosition.Beginning); - // Now delete the group. - var deleteResult = db.StreamDeleteConsumerGroup(key, groupName); + var preDeleteInfo = db.StreamInfo(key); - var postDeleteInfo = db.StreamInfo(key); + // Now delete the group. + var deleteResult = db.StreamDeleteConsumerGroup(key, groupName); - Assert.True(deleteResult); - Assert.Equal(1, preDeleteInfo.ConsumerGroupCount); - Assert.Equal(0, postDeleteInfo.ConsumerGroupCount); - } - } + var postDeleteInfo = db.StreamInfo(key); - [Fact] - public void StreamDeleteMessage() - { - var key = GetUniqueKey("delete_msg"); + Assert.True(deleteResult); + Assert.Equal(1, preDeleteInfo.ConsumerGroupCount); + Assert.Equal(0, postDeleteInfo.ConsumerGroupCount); + } - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + [Fact] + public void StreamDeleteMessage() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var db = conn.GetDatabase(); + var db = conn.GetDatabase(); + var key = GetUniqueKey("delete_msg"); - db.StreamAdd(key, "field1", "value1"); - db.StreamAdd(key, "field2", "value2"); - var id3 = db.StreamAdd(key, "field3", "value3"); - db.StreamAdd(key, "field4", "value4"); + db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); + var id3 = db.StreamAdd(key, "field3", "value3"); + db.StreamAdd(key, "field4", "value4"); - var deletedCount = db.StreamDelete(key, new [] { id3 }); - var messages = db.StreamRange(key); + var deletedCount = db.StreamDelete(key, new[] { id3 }); + var messages = db.StreamRange(key); - Assert.Equal(1, deletedCount); - Assert.Equal(3, messages.Length); - } - } + Assert.Equal(1, deletedCount); + Assert.Equal(3, messages.Length); + } - [Fact] - public void StreamDeleteMessages() - { - var key = GetUniqueKey("delete_msgs"); + [Fact] + public void StreamDeleteMessages() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var db = conn.GetDatabase(); + var key = GetUniqueKey("delete_msgs"); - var db = conn.GetDatabase(); + db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); + var id3 = db.StreamAdd(key, "field3", "value3"); + db.StreamAdd(key, "field4", "value4"); - db.StreamAdd(key, "field1", "value1"); - var id2 = db.StreamAdd(key, "field2", "value2"); - var id3 = db.StreamAdd(key, "field3", "value3"); - db.StreamAdd(key, "field4", "value4"); + var deletedCount = db.StreamDelete(key, new[] { id2, id3 }, CommandFlags.None); + var messages = db.StreamRange(key); - var deletedCount = db.StreamDelete(key, new [] { id2, id3 }, CommandFlags.None); - var messages = db.StreamRange(key); + Assert.Equal(2, deletedCount); + Assert.Equal(2, messages.Length); + } - Assert.Equal(2, deletedCount); - Assert.Equal(2, messages.Length); - } - } + [Fact] + public void StreamGroupInfoGet() + { + var key = GetUniqueKey("group_info"); + const string group1 = "test_group_1", + group2 = "test_group_2", + consumer1 = "test_consumer_1", + consumer2 = "test_consumer_2"; - [Fact] - public void StreamGroupInfoGet() + using (var conn = Create(require: RedisFeatures.v5_0_0)) { - var key = GetUniqueKey("group_info"); - const string group1 = "test_group_1"; - const string group2 = "test_group_2"; - const string consumer1 = "test_consumer_1"; - const string consumer2 = "test_consumer_2"; + var db = conn.GetDatabase(); + db.KeyDelete(key); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - db.KeyDelete(key); - - db.StreamAdd(key, "field1", "value1"); - db.StreamAdd(key, "field2", "value2"); - db.StreamAdd(key, "field3", "value3"); - db.StreamAdd(key, "field4", "value4"); + db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); + db.StreamAdd(key, "field3", "value3"); + db.StreamAdd(key, "field4", "value4"); - db.StreamCreateConsumerGroup(key, group1, StreamPosition.Beginning); - db.StreamCreateConsumerGroup(key, group2, StreamPosition.Beginning); + db.StreamCreateConsumerGroup(key, group1, StreamPosition.Beginning); + db.StreamCreateConsumerGroup(key, group2, StreamPosition.Beginning); - // Read a single message into the first consumer. - db.StreamReadGroup(key, group1, consumer1, count: 1); + // Read a single message into the first consumer. + db.StreamReadGroup(key, group1, consumer1, count: 1); - // Read the remaining messages into the second consumer. - db.StreamReadGroup(key, group2, consumer2); + // Read the remaining messages into the second consumer. + db.StreamReadGroup(key, group2, consumer2); - var groupInfoList = db.StreamGroupInfo(key); + var groupInfoList = db.StreamGroupInfo(key); - Assert.NotNull(groupInfoList); - Assert.Equal(2, groupInfoList.Length); + Assert.NotNull(groupInfoList); + Assert.Equal(2, groupInfoList.Length); - Assert.Equal(group1, groupInfoList[0].Name); - Assert.Equal(1, groupInfoList[0].PendingMessageCount); - Assert.True(IsMessageId(groupInfoList[0].LastDeliveredId)); // can't test actual - will vary + Assert.Equal(group1, groupInfoList[0].Name); + Assert.Equal(1, groupInfoList[0].PendingMessageCount); + Assert.True(IsMessageId(groupInfoList[0].LastDeliveredId)); // can't test actual - will vary - Assert.Equal(group2, groupInfoList[1].Name); - Assert.Equal(4, groupInfoList[1].PendingMessageCount); - Assert.True(IsMessageId(groupInfoList[1].LastDeliveredId)); // can't test actual - will vary - } - - static bool IsMessageId(string? value) - { - if (value.IsNullOrWhiteSpace()) return false; - return value.Length >= 3 && value.Contains('-'); - } + Assert.Equal(group2, groupInfoList[1].Name); + Assert.Equal(4, groupInfoList[1].PendingMessageCount); + Assert.True(IsMessageId(groupInfoList[1].LastDeliveredId)); // can't test actual - will vary } - [Fact] - public void StreamGroupConsumerInfoGet() + static bool IsMessageId(string? value) { - var key = GetUniqueKey("group_consumer_info"); - const string group = "test_group"; - const string consumer1 = "test_consumer_1"; - const string consumer2 = "test_consumer_2"; - - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - - db.StreamAdd(key, "field1", "value1"); - db.StreamAdd(key, "field2", "value2"); - db.StreamAdd(key, "field3", "value3"); - db.StreamAdd(key, "field4", "value4"); - - db.StreamCreateConsumerGroup(key, group, StreamPosition.Beginning); - db.StreamReadGroup(key, group, consumer1, count: 1); - db.StreamReadGroup(key, group, consumer2); - - var consumerInfoList = db.StreamConsumerInfo(key, group); - - Assert.NotNull(consumerInfoList); - Assert.Equal(2, consumerInfoList.Length); - - Assert.Equal(consumer1, consumerInfoList[0].Name); - Assert.Equal(consumer2, consumerInfoList[1].Name); - - Assert.Equal(1, consumerInfoList[0].PendingMessageCount); - Assert.Equal(3, consumerInfoList[1].PendingMessageCount); - } + if (value.IsNullOrWhiteSpace()) return false; + return value.Length >= 3 && value.Contains('-'); } + } - [Fact] - public void StreamInfoGet() - { - var key = GetUniqueKey("stream_info"); - - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - - var id1 = db.StreamAdd(key, "field1", "value1"); - db.StreamAdd(key, "field2", "value2"); - db.StreamAdd(key, "field3", "value3"); - var id4 = db.StreamAdd(key, "field4", "value4"); - - var streamInfo = db.StreamInfo(key); + [Fact] + public void StreamGroupConsumerInfoGet() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - Assert.Equal(4, streamInfo.Length); - Assert.True(streamInfo.RadixTreeKeys > 0); - Assert.True(streamInfo.RadixTreeNodes > 0); - Assert.Equal(id1, streamInfo.FirstEntry.Id); - Assert.Equal(id4, streamInfo.LastEntry.Id); - } - } + var db = conn.GetDatabase(); + var key = GetUniqueKey("group_consumer_info"); + const string group = "test_group", + consumer1 = "test_consumer_1", + consumer2 = "test_consumer_2"; - [Fact] - public void StreamInfoGetWithEmptyStream() - { - var key = GetUniqueKey("stream_info_empty"); + db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); + db.StreamAdd(key, "field3", "value3"); + db.StreamAdd(key, "field4", "value4"); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + db.StreamCreateConsumerGroup(key, group, StreamPosition.Beginning); + db.StreamReadGroup(key, group, consumer1, count: 1); + db.StreamReadGroup(key, group, consumer2); - var db = conn.GetDatabase(); + var consumerInfoList = db.StreamConsumerInfo(key, group); - // Add an entry and then delete it so the stream is empty, then run streaminfo - // to ensure it functions properly on an empty stream. Namely, the first-entry - // and last-entry messages should be null. + Assert.NotNull(consumerInfoList); + Assert.Equal(2, consumerInfoList.Length); - var id = db.StreamAdd(key, "field1", "value1"); - db.StreamDelete(key, new [] { id }); + Assert.Equal(consumer1, consumerInfoList[0].Name); + Assert.Equal(consumer2, consumerInfoList[1].Name); - Assert.Equal(0, db.StreamLength(key)); + Assert.Equal(1, consumerInfoList[0].PendingMessageCount); + Assert.Equal(3, consumerInfoList[1].PendingMessageCount); + } - var streamInfo = db.StreamInfo(key); + [Fact] + public void StreamInfoGet() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - Assert.True(streamInfo.FirstEntry.IsNull); - Assert.True(streamInfo.LastEntry.IsNull); - } - } + var db = conn.GetDatabase(); + var key = GetUniqueKey("stream_info"); - [Fact] - public void StreamNoConsumerGroups() - { - var key = GetUniqueKey("stream_with_no_consumers"); + var id1 = db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); + db.StreamAdd(key, "field3", "value3"); + var id4 = db.StreamAdd(key, "field4", "value4"); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var streamInfo = db.StreamInfo(key); - var db = conn.GetDatabase(); + Assert.Equal(4, streamInfo.Length); + Assert.True(streamInfo.RadixTreeKeys > 0); + Assert.True(streamInfo.RadixTreeNodes > 0); + Assert.Equal(id1, streamInfo.FirstEntry.Id); + Assert.Equal(id4, streamInfo.LastEntry.Id); + } - db.StreamAdd(key, "field1", "value1"); + [Fact] + public void StreamInfoGetWithEmptyStream() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var groups = db.StreamGroupInfo(key); + var db = conn.GetDatabase(); + var key = GetUniqueKey("stream_info_empty"); - Assert.NotNull(groups); - Assert.Empty(groups); - } - } + // Add an entry and then delete it so the stream is empty, then run streaminfo + // to ensure it functions properly on an empty stream. Namely, the first-entry + // and last-entry messages should be null. - [Fact] - public void StreamPendingNoMessagesOrConsumers() - { - var key = GetUniqueKey("stream_pending_empty"); - const string groupName = "test_group"; + var id = db.StreamAdd(key, "field1", "value1"); + db.StreamDelete(key, new[] { id }); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + Assert.Equal(0, db.StreamLength(key)); - var db = conn.GetDatabase(); + var streamInfo = db.StreamInfo(key); - var id = db.StreamAdd(key, "field1", "value1"); - db.StreamDelete(key, new [] { id }); + Assert.True(streamInfo.FirstEntry.IsNull); + Assert.True(streamInfo.LastEntry.IsNull); + } - db.StreamCreateConsumerGroup(key, groupName, "0-0"); + [Fact] + public void StreamNoConsumerGroups() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var pendingInfo = db.StreamPending(key, "test_group"); + var db = conn.GetDatabase(); + var key = GetUniqueKey("stream_with_no_consumers"); - Assert.Equal(0, pendingInfo.PendingMessageCount); - Assert.Equal(RedisValue.Null, pendingInfo.LowestPendingMessageId); - Assert.Equal(RedisValue.Null, pendingInfo.HighestPendingMessageId); - Assert.NotNull(pendingInfo.Consumers); - Assert.Empty(pendingInfo.Consumers); - } - } + db.StreamAdd(key, "field1", "value1"); - [Fact] - public void StreamPositionDefaultValueIsBeginning() - { - RedisValue position = StreamPosition.Beginning; - Assert.Equal(StreamConstants.AllMessages, StreamPosition.Resolve(position, RedisCommand.XREAD)); - Assert.Equal(StreamConstants.AllMessages, StreamPosition.Resolve(position, RedisCommand.XREADGROUP)); - Assert.Equal(StreamConstants.AllMessages, StreamPosition.Resolve(position, RedisCommand.XGROUP)); - } + var groups = db.StreamGroupInfo(key); - [Fact] - public void StreamPositionValidateBeginning() - { - var position = StreamPosition.Beginning; - - Assert.Equal(StreamConstants.AllMessages, StreamPosition.Resolve(position, RedisCommand.XREAD)); - } - - [Fact] - public void StreamPositionValidateExplicit() - { - const string explicitValue = "1-0"; - const string position = explicitValue; + Assert.NotNull(groups); + Assert.Empty(groups); + } - Assert.Equal(explicitValue, StreamPosition.Resolve(position, RedisCommand.XREAD)); - } + [Fact] + public void StreamPendingNoMessagesOrConsumers() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - [Fact] - public void StreamPositionValidateNew() - { - var position = StreamPosition.NewMessages; + var db = conn.GetDatabase(); + var key = GetUniqueKey("stream_pending_empty"); + const string groupName = "test_group"; - Assert.Equal(StreamConstants.NewMessages, StreamPosition.Resolve(position, RedisCommand.XGROUP)); - Assert.Equal(StreamConstants.UndeliveredMessages, StreamPosition.Resolve(position, RedisCommand.XREADGROUP)); - Assert.ThrowsAny(() => StreamPosition.Resolve(position, RedisCommand.XREAD)); - } + var id = db.StreamAdd(key, "field1", "value1"); + db.StreamDelete(key, new[] { id }); - [Fact] - public void StreamRead() - { - var key = GetUniqueKey("read"); + db.StreamCreateConsumerGroup(key, groupName, "0-0"); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var pendingInfo = db.StreamPending(key, "test_group"); - var db = conn.GetDatabase(); + Assert.Equal(0, pendingInfo.PendingMessageCount); + Assert.Equal(RedisValue.Null, pendingInfo.LowestPendingMessageId); + Assert.Equal(RedisValue.Null, pendingInfo.HighestPendingMessageId); + Assert.NotNull(pendingInfo.Consumers); + Assert.Empty(pendingInfo.Consumers); + } - var id1 = db.StreamAdd(key, "field1", "value1"); - var id2 = db.StreamAdd(key, "field2", "value2"); - var id3 = db.StreamAdd(key, "field3", "value3"); + [Fact] + public void StreamPositionDefaultValueIsBeginning() + { + RedisValue position = StreamPosition.Beginning; + Assert.Equal(StreamConstants.AllMessages, StreamPosition.Resolve(position, RedisCommand.XREAD)); + Assert.Equal(StreamConstants.AllMessages, StreamPosition.Resolve(position, RedisCommand.XREADGROUP)); + Assert.Equal(StreamConstants.AllMessages, StreamPosition.Resolve(position, RedisCommand.XGROUP)); + } - // Read the entire stream from the beginning. - var entries = db.StreamRead(key, "0-0"); + [Fact] + public void StreamPositionValidateBeginning() + { + var position = StreamPosition.Beginning; - Assert.Equal(3, entries.Length); - Assert.Equal(id1, entries[0].Id); - Assert.Equal(id2, entries[1].Id); - Assert.Equal(id3, entries[2].Id); - } - } + Assert.Equal(StreamConstants.AllMessages, StreamPosition.Resolve(position, RedisCommand.XREAD)); + } - [Fact] - public void StreamReadEmptyStream() - { - var key = GetUniqueKey("read_empty_stream"); + [Fact] + public void StreamPositionValidateExplicit() + { + const string explicitValue = "1-0"; + const string position = explicitValue; - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + Assert.Equal(explicitValue, StreamPosition.Resolve(position, RedisCommand.XREAD)); + } - var db = conn.GetDatabase(); + [Fact] + public void StreamPositionValidateNew() + { + var position = StreamPosition.NewMessages; - // Write to a stream to create the key. - var id1 = db.StreamAdd(key, "field1", "value1"); + Assert.Equal(StreamConstants.NewMessages, StreamPosition.Resolve(position, RedisCommand.XGROUP)); + Assert.Equal(StreamConstants.UndeliveredMessages, StreamPosition.Resolve(position, RedisCommand.XREADGROUP)); + Assert.ThrowsAny(() => StreamPosition.Resolve(position, RedisCommand.XREAD)); + } - // Delete the key to empty the stream. - db.StreamDelete(key, new [] { id1 }); - var len = db.StreamLength(key); + [Fact] + public void StreamRead() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - // Read the entire stream from the beginning. - var entries = db.StreamRead(key, "0-0"); + var db = conn.GetDatabase(); + var key = GetUniqueKey("read"); - Assert.Empty(entries); - Assert.Equal(0, len); - } - } + var id1 = db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); + var id3 = db.StreamAdd(key, "field3", "value3"); - [Fact] - public void StreamReadEmptyStreams() - { - var key1 = GetUniqueKey("read_empty_stream_1"); - var key2 = GetUniqueKey("read_empty_stream_2"); + // Read the entire stream from the beginning. + var entries = db.StreamRead(key, "0-0"); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + Assert.Equal(3, entries.Length); + Assert.Equal(id1, entries[0].Id); + Assert.Equal(id2, entries[1].Id); + Assert.Equal(id3, entries[2].Id); + } - var db = conn.GetDatabase(); + [Fact] + public void StreamReadEmptyStream() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - // Write to a stream to create the key. - var id1 = db.StreamAdd(key1, "field1", "value1"); - var id2 = db.StreamAdd(key2, "field2", "value2"); + var db = conn.GetDatabase(); + var key = GetUniqueKey("read_empty_stream"); - // Delete the key to empty the stream. - db.StreamDelete(key1, new [] { id1 }); - db.StreamDelete(key2, new [] { id2 }); + // Write to a stream to create the key. + var id1 = db.StreamAdd(key, "field1", "value1"); - var len1 = db.StreamLength(key1); - var len2 = db.StreamLength(key2); + // Delete the key to empty the stream. + db.StreamDelete(key, new[] { id1 }); + var len = db.StreamLength(key); - // Read the entire stream from the beginning. - var entries1 = db.StreamRead(key1, "0-0"); - var entries2 = db.StreamRead(key2, "0-0"); + // Read the entire stream from the beginning. + var entries = db.StreamRead(key, "0-0"); - Assert.Empty(entries1); - Assert.Empty(entries2); + Assert.Empty(entries); + Assert.Equal(0, len); + } - Assert.Equal(0, len1); - Assert.Equal(0, len2); - } - } + [Fact] + public void StreamReadEmptyStreams() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - [Fact] - public void StreamReadExpectedExceptionInvalidCountMultipleStream() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var db = conn.GetDatabase(); + var key1 = GetUniqueKey("read_empty_stream_1"); + var key2 = GetUniqueKey("read_empty_stream_2"); - var streamPositions = new [] - { - new StreamPosition("key1", "0-0"), - new StreamPosition("key2", "0-0") - }; + // Write to a stream to create the key. + var id1 = db.StreamAdd(key1, "field1", "value1"); + var id2 = db.StreamAdd(key2, "field2", "value2"); - var db = conn.GetDatabase(); - Assert.Throws(() => db.StreamRead(streamPositions, 0)); - } - } + // Delete the key to empty the stream. + db.StreamDelete(key1, new[] { id1 }); + db.StreamDelete(key2, new[] { id2 }); - [Fact] - public void StreamReadExpectedExceptionInvalidCountSingleStream() - { - var key = GetUniqueKey("read_exception_invalid_count_single"); + var len1 = db.StreamLength(key1); + var len2 = db.StreamLength(key2); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + // Read the entire stream from the beginning. + var entries1 = db.StreamRead(key1, "0-0"); + var entries2 = db.StreamRead(key2, "0-0"); - var db = conn.GetDatabase(); - Assert.Throws(() => db.StreamRead(key, "0-0", 0)); - } - } + Assert.Empty(entries1); + Assert.Empty(entries2); - [Fact] - public void StreamReadExpectedExceptionNullStreamList() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + Assert.Equal(0, len1); + Assert.Equal(0, len2); + } - var db = conn.GetDatabase(); - Assert.Throws(() => db.StreamRead(null!)); - } - } + [Fact] + public void StreamReadExpectedExceptionInvalidCountMultipleStream() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - [Fact] - public void StreamReadExpectedExceptionEmptyStreamList() + var db = conn.GetDatabase(); + var streamPositions = new[] { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - - var emptyList = Array.Empty(); + new StreamPosition("key1", "0-0"), + new StreamPosition("key2", "0-0") + }; + Assert.Throws(() => db.StreamRead(streamPositions, 0)); + } - Assert.Throws(() => db.StreamRead(emptyList)); - } - } + [Fact] + public void StreamReadExpectedExceptionInvalidCountSingleStream() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - [Fact] - public void StreamReadMultipleStreams() - { - var key1 = GetUniqueKey("read_multi_1a"); - var key2 = GetUniqueKey("read_multi_2a"); + var db = conn.GetDatabase(); + var key = GetUniqueKey("read_exception_invalid_count_single"); + Assert.Throws(() => db.StreamRead(key, "0-0", 0)); + } - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + [Fact] + public void StreamReadExpectedExceptionNullStreamList() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var db = conn.GetDatabase(); + var db = conn.GetDatabase(); + Assert.Throws(() => db.StreamRead(null!)); + } - var id1 = db.StreamAdd(key1, "field1", "value1"); - var id2 = db.StreamAdd(key1, "field2", "value2"); - var id3 = db.StreamAdd(key2, "field3", "value3"); - var id4 = db.StreamAdd(key2, "field4", "value4"); + [Fact] + public void StreamReadExpectedExceptionEmptyStreamList() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - // Read from both streams at the same time. - var streamList = new [] - { - new StreamPosition(key1, "0-0"), - new StreamPosition(key2, "0-0") - }; + var db = conn.GetDatabase(); + var emptyList = Array.Empty(); + Assert.Throws(() => db.StreamRead(emptyList)); + } - var streams = db.StreamRead(streamList); + [Fact] + public void StreamReadMultipleStreams() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - Assert.True(streams.Length == 2); + var db = conn.GetDatabase(); + var key1 = GetUniqueKey("read_multi_1a"); + var key2 = GetUniqueKey("read_multi_2a"); - Assert.Equal(key1, streams[0].Key); - Assert.Equal(2, streams[0].Entries.Length); - Assert.Equal(id1, streams[0].Entries[0].Id); - Assert.Equal(id2, streams[0].Entries[1].Id); + var id1 = db.StreamAdd(key1, "field1", "value1"); + var id2 = db.StreamAdd(key1, "field2", "value2"); + var id3 = db.StreamAdd(key2, "field3", "value3"); + var id4 = db.StreamAdd(key2, "field4", "value4"); - Assert.Equal(key2, streams[1].Key); - Assert.Equal(2, streams[1].Entries.Length); - Assert.Equal(id3, streams[1].Entries[0].Id); - Assert.Equal(id4, streams[1].Entries[1].Id); - } - } - - [Fact] - public void StreamReadMultipleStreamsWithCount() + // Read from both streams at the same time. + var streamList = new[] { - var key1 = GetUniqueKey("read_multi_count_1"); - var key2 = GetUniqueKey("read_multi_count_2"); - - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + new StreamPosition(key1, "0-0"), + new StreamPosition(key2, "0-0") + }; - var db = conn.GetDatabase(); + var streams = db.StreamRead(streamList); - var id1 = db.StreamAdd(key1, "field1", "value1"); - db.StreamAdd(key1, "field2", "value2"); - var id3 = db.StreamAdd(key2, "field3", "value3"); - db.StreamAdd(key2, "field4", "value4"); + Assert.True(streams.Length == 2); - var streamList = new [] - { - new StreamPosition(key1, "0-0"), - new StreamPosition(key2, "0-0") - }; + Assert.Equal(key1, streams[0].Key); + Assert.Equal(2, streams[0].Entries.Length); + Assert.Equal(id1, streams[0].Entries[0].Id); + Assert.Equal(id2, streams[0].Entries[1].Id); - var streams = db.StreamRead(streamList, countPerStream: 1); + Assert.Equal(key2, streams[1].Key); + Assert.Equal(2, streams[1].Entries.Length); + Assert.Equal(id3, streams[1].Entries[0].Id); + Assert.Equal(id4, streams[1].Entries[1].Id); + } - // We should get both streams back. - Assert.Equal(2, streams.Length); + [Fact] + public void StreamReadMultipleStreamsWithCount() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - // Ensure we only got one message per stream. - Assert.Single(streams[0].Entries); - Assert.Single(streams[1].Entries); + var db = conn.GetDatabase(); + var key1 = GetUniqueKey("read_multi_count_1"); + var key2 = GetUniqueKey("read_multi_count_2"); - // Check the message IDs as well. - Assert.Equal(id1, streams[0].Entries[0].Id); - Assert.Equal(id3, streams[1].Entries[0].Id); - } - } + var id1 = db.StreamAdd(key1, "field1", "value1"); + db.StreamAdd(key1, "field2", "value2"); + var id3 = db.StreamAdd(key2, "field3", "value3"); + db.StreamAdd(key2, "field4", "value4"); - [Fact] - public void StreamReadMultipleStreamsWithReadPastSecondStream() + var streamList = new[] { - var key1 = GetUniqueKey("read_multi_1b"); - var key2 = GetUniqueKey("read_multi_2b"); - - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - - db.StreamAdd(key1, "field1", "value1"); - db.StreamAdd(key1, "field2", "value2"); - db.StreamAdd(key2, "field3", "value3"); - var id4 = db.StreamAdd(key2, "field4", "value4"); - - var streamList = new [] - { - new StreamPosition(key1, "0-0"), + new StreamPosition(key1, "0-0"), + new StreamPosition(key2, "0-0") + }; - // read past the end of stream # 2 - new StreamPosition(key2, id4) - }; + var streams = db.StreamRead(streamList, countPerStream: 1); - var streams = db.StreamRead(streamList); + // We should get both streams back. + Assert.Equal(2, streams.Length); - // We should only get the first stream back. - Assert.Single(streams); + // Ensure we only got one message per stream. + Assert.Single(streams[0].Entries); + Assert.Single(streams[1].Entries); - Assert.Equal(key1, streams[0].Key); - Assert.Equal(2, streams[0].Entries.Length); - } - } - - [Fact] - public void StreamReadMultipleStreamsWithEmptyResponse() - { - var key1 = GetUniqueKey("read_multi_1c"); - var key2 = GetUniqueKey("read_multi_2c"); - - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - - db.StreamAdd(key1, "field1", "value1"); - var id2 = db.StreamAdd(key1, "field2", "value2"); - db.StreamAdd(key2, "field3", "value3"); - var id4 = db.StreamAdd(key2, "field4", "value4"); + // Check the message IDs as well. + Assert.Equal(id1, streams[0].Entries[0].Id); + Assert.Equal(id3, streams[1].Entries[0].Id); + } - var streamList = new [] - { - // Read past the end of both streams. - new StreamPosition(key1, id2), - new StreamPosition(key2, id4) - }; + [Fact] + public void StreamReadMultipleStreamsWithReadPastSecondStream() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var streams = db.StreamRead(streamList); + var db = conn.GetDatabase(); + var key1 = GetUniqueKey("read_multi_1b"); + var key2 = GetUniqueKey("read_multi_2b"); - // We expect an empty response. - Assert.Empty(streams); - } - } + db.StreamAdd(key1, "field1", "value1"); + db.StreamAdd(key1, "field2", "value2"); + db.StreamAdd(key2, "field3", "value3"); + var id4 = db.StreamAdd(key2, "field4", "value4"); - [Fact] - public void StreamReadPastEndOfStream() + var streamList = new[] { - var key = GetUniqueKey("read_empty"); - - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + new StreamPosition(key1, "0-0"), - var db = conn.GetDatabase(); + // read past the end of stream # 2 + new StreamPosition(key2, id4) + }; - db.StreamAdd(key, "field1", "value1"); - var id2 = db.StreamAdd(key, "field2", "value2"); + var streams = db.StreamRead(streamList); - // Read after the final ID in the stream, we expect an empty array as a response. + // We should only get the first stream back. + Assert.Single(streams); - var entries = db.StreamRead(key, id2); - - Assert.Empty(entries); - } - } - - [Fact] - public void StreamReadRange() - { - var key = GetUniqueKey("range"); - - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + Assert.Equal(key1, streams[0].Key); + Assert.Equal(2, streams[0].Entries.Length); + } - var db = conn.GetDatabase(); + [Fact] + public void StreamReadMultipleStreamsWithEmptyResponse() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var id1 = db.StreamAdd(key, "field1", "value1"); - var id2 = db.StreamAdd(key, "field2", "value2"); + var db = conn.GetDatabase(); + var key1 = GetUniqueKey("read_multi_1c"); + var key2 = GetUniqueKey("read_multi_2c"); - var entries = db.StreamRange(key); + db.StreamAdd(key1, "field1", "value1"); + var id2 = db.StreamAdd(key1, "field2", "value2"); + db.StreamAdd(key2, "field3", "value3"); + var id4 = db.StreamAdd(key2, "field4", "value4"); - Assert.Equal(2, entries.Length); - Assert.Equal(id1, entries[0].Id); - Assert.Equal(id2, entries[1].Id); - } - } - - [Fact] - public void StreamReadRangeOfEmptyStream() + var streamList = new[] { - var key = GetUniqueKey("range_empty"); - - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + // Read past the end of both streams. + new StreamPosition(key1, id2), + new StreamPosition(key2, id4) + }; - var db = conn.GetDatabase(); + var streams = db.StreamRead(streamList); - var id1 = db.StreamAdd(key, "field1", "value1"); - var id2 = db.StreamAdd(key, "field2", "value2"); + // We expect an empty response. + Assert.Empty(streams); + } - var deleted = db.StreamDelete(key, new [] { id1, id2 }); + [Fact] + public void StreamReadPastEndOfStream() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var entries = db.StreamRange(key); + var db = conn.GetDatabase(); + var key = GetUniqueKey("read_empty"); - Assert.Equal(2, deleted); - Assert.NotNull(entries); - Assert.Empty(entries); - } - } + db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); - [Fact] - public void StreamReadRangeWithCount() - { - var key = GetUniqueKey("range_count"); + // Read after the final ID in the stream, we expect an empty array as a response. - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var entries = db.StreamRead(key, id2); - var db = conn.GetDatabase(); + Assert.Empty(entries); + } - var id1 = db.StreamAdd(key, "field1", "value1"); - db.StreamAdd(key, "field2", "value2"); + [Fact] + public void StreamReadRange() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var entries = db.StreamRange(key, count: 1); + var db = conn.GetDatabase(); + var key = GetUniqueKey("range"); - Assert.Single(entries); - Assert.Equal(id1, entries[0].Id); - } - } + var id1 = db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); - [Fact] - public void StreamReadRangeReverse() - { - var key = GetUniqueKey("rangerev"); + var entries = db.StreamRange(key); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + Assert.Equal(2, entries.Length); + Assert.Equal(id1, entries[0].Id); + Assert.Equal(id2, entries[1].Id); + } - var db = conn.GetDatabase(); + [Fact] + public void StreamReadRangeOfEmptyStream() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var id1 = db.StreamAdd(key, "field1", "value1"); - var id2 = db.StreamAdd(key, "field2", "value2"); + var db = conn.GetDatabase(); + var key = GetUniqueKey("range_empty"); - var entries = db.StreamRange(key, messageOrder: Order.Descending); + var id1 = db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); - Assert.Equal(2, entries.Length); - Assert.Equal(id2, entries[0].Id); - Assert.Equal(id1, entries[1].Id); - } - } + var deleted = db.StreamDelete(key, new[] { id1, id2 }); - [Fact] - public void StreamReadRangeReverseWithCount() - { - var key = GetUniqueKey("rangerev_count"); + var entries = db.StreamRange(key); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + Assert.Equal(2, deleted); + Assert.NotNull(entries); + Assert.Empty(entries); + } - var db = conn.GetDatabase(); + [Fact] + public void StreamReadRangeWithCount() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var id1 = db.StreamAdd(key, "field1", "value1"); - var id2 = db.StreamAdd(key, "field2", "value2"); + var db = conn.GetDatabase(); + var key = GetUniqueKey("range_count"); - var entries = db.StreamRange(key, id1, id2, 1, Order.Descending); + var id1 = db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); - Assert.Single(entries); - Assert.Equal(id2, entries[0].Id); - } - } + var entries = db.StreamRange(key, count: 1); - [Fact] - public void StreamReadWithAfterIdAndCount_1() - { - var key = GetUniqueKey("read1"); + Assert.Single(entries); + Assert.Equal(id1, entries[0].Id); + } - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + [Fact] + public void StreamReadRangeReverse() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var db = conn.GetDatabase(); + var db = conn.GetDatabase(); + var key = GetUniqueKey("rangerev"); - var id1 = db.StreamAdd(key, "field1", "value1"); - var id2 = db.StreamAdd(key, "field2", "value2"); - db.StreamAdd(key, "field3", "value3"); + var id1 = db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); - // Only read a single item from the stream. - var entries = db.StreamRead(key, id1, 1); + var entries = db.StreamRange(key, messageOrder: Order.Descending); - Assert.Single(entries); - Assert.Equal(id2, entries[0].Id); - } - } + Assert.Equal(2, entries.Length); + Assert.Equal(id2, entries[0].Id); + Assert.Equal(id1, entries[1].Id); + } - [Fact] - public void StreamReadWithAfterIdAndCount_2() - { - var key = GetUniqueKey("read2"); + [Fact] + public void StreamReadRangeReverseWithCount() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var db = conn.GetDatabase(); + var key = GetUniqueKey("rangerev_count"); - var db = conn.GetDatabase(); + var id1 = db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); - var id1 = db.StreamAdd(key, "field1", "value1"); - var id2 = db.StreamAdd(key, "field2", "value2"); - var id3 = db.StreamAdd(key, "field3", "value3"); - db.StreamAdd(key, "field4", "value4"); + var entries = db.StreamRange(key, id1, id2, 1, Order.Descending); - // Read multiple items from the stream. - var entries = db.StreamRead(key, id1, 2); + Assert.Single(entries); + Assert.Equal(id2, entries[0].Id); + } - Assert.Equal(2, entries.Length); - Assert.Equal(id2, entries[0].Id); - Assert.Equal(id3, entries[1].Id); - } - } + [Fact] + public void StreamReadWithAfterIdAndCount_1() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - [Fact] - public void StreamTrimLength() - { - var key = GetUniqueKey("trimlen"); + var db = conn.GetDatabase(); + var key = GetUniqueKey("read1"); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + var id1 = db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); + db.StreamAdd(key, "field3", "value3"); - var db = conn.GetDatabase(); + // Only read a single item from the stream. + var entries = db.StreamRead(key, id1, 1); - // Add a couple items and check length. - db.StreamAdd(key, "field1", "value1"); - db.StreamAdd(key, "field2", "value2"); - db.StreamAdd(key, "field3", "value3"); - db.StreamAdd(key, "field4", "value4"); + Assert.Single(entries); + Assert.Equal(id2, entries[0].Id); + } - var numRemoved = db.StreamTrim(key, 1); - var len = db.StreamLength(key); + [Fact] + public void StreamReadWithAfterIdAndCount_2() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - Assert.Equal(3, numRemoved); - Assert.Equal(1, len); - } - } + var db = conn.GetDatabase(); + var key = GetUniqueKey("read2"); - [Fact] - public void StreamVerifyLength() - { - var key = GetUniqueKey("len"); + var id1 = db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); + var id3 = db.StreamAdd(key, "field3", "value3"); + db.StreamAdd(key, "field4", "value4"); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + // Read multiple items from the stream. + var entries = db.StreamRead(key, id1, 2); - var db = conn.GetDatabase(); + Assert.Equal(2, entries.Length); + Assert.Equal(id2, entries[0].Id); + Assert.Equal(id3, entries[1].Id); + } - // Add a couple items and check length. - db.StreamAdd(key, "field1", "value1"); - db.StreamAdd(key, "field2", "value2"); + [Fact] + public void StreamTrimLength() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var len = db.StreamLength(key); + var db = conn.GetDatabase(); + var key = GetUniqueKey("trimlen"); - Assert.Equal(2, len); - } - } + // Add a couple items and check length. + db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); + db.StreamAdd(key, "field3", "value3"); + db.StreamAdd(key, "field4", "value4"); - [Fact] - public async Task AddWithApproxCountAsync() - { - var key = GetUniqueKey("approx-async"); + var numRemoved = db.StreamTrim(key, 1); + var len = db.StreamLength(key); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + Assert.Equal(3, numRemoved); + Assert.Equal(1, len); + } - var db = conn.GetDatabase(); - await db.StreamAddAsync(key, "field", "value", maxLength: 10, useApproximateMaxLength: true, flags: CommandFlags.None).ConfigureAwait(false); - } - } + [Fact] + public void StreamVerifyLength() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - [Fact] - public void AddWithApproxCount() - { - var key = GetUniqueKey("approx"); + var db = conn.GetDatabase(); + var key = GetUniqueKey("len"); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + // Add a couple items and check length. + db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); - var db = conn.GetDatabase(); - db.StreamAdd(key, "field", "value", maxLength: 10, useApproximateMaxLength: true, flags: CommandFlags.None); - } - } + var len = db.StreamLength(key); - [Fact] - public void StreamReadGroupWithNoAckShowsNoPendingMessages() - { - var key = GetUniqueKey("read_group_noack"); - const string groupName = "test_group"; - const string consumer = "consumer"; + Assert.Equal(2, len); + } - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + [Fact] + public async Task AddWithApproxCountAsync() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var db = conn.GetDatabase(); + var db = conn.GetDatabase(); + var key = GetUniqueKey("approx-async"); + await db.StreamAddAsync(key, "field", "value", maxLength: 10, useApproximateMaxLength: true, flags: CommandFlags.None).ConfigureAwait(false); + } - db.StreamAdd(key, "field1", "value1"); - db.StreamAdd(key, "field2", "value2"); + [Fact] + public void AddWithApproxCount() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - db.StreamCreateConsumerGroup(key, groupName, StreamPosition.NewMessages); + var db = conn.GetDatabase(); + var key = GetUniqueKey("approx"); + db.StreamAdd(key, "field", "value", maxLength: 10, useApproximateMaxLength: true, flags: CommandFlags.None); + } - db.StreamReadGroup(key, - groupName, - consumer, - StreamPosition.NewMessages, - noAck: true); + [Fact] + public void StreamReadGroupWithNoAckShowsNoPendingMessages() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - var pendingInfo = db.StreamPending(key, groupName); + var db = conn.GetDatabase(); + var key = GetUniqueKey("read_group_noack"); + const string groupName = "test_group", + consumer = "consumer"; - Assert.Equal(0, pendingInfo.PendingMessageCount); - } - } + db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); - [Fact] - public void StreamReadGroupMultiStreamWithNoAckShowsNoPendingMessages() - { - var key1 = GetUniqueKey("read_group_noack1"); - var key2 = GetUniqueKey("read_group_noack2"); - const string groupName = "test_group"; - const string consumer = "consumer"; + db.StreamCreateConsumerGroup(key, groupName, StreamPosition.NewMessages); - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); + db.StreamReadGroup(key, + groupName, + consumer, + StreamPosition.NewMessages, + noAck: true); - var db = conn.GetDatabase(); + var pendingInfo = db.StreamPending(key, groupName); - db.StreamAdd(key1, "field1", "value1"); - db.StreamAdd(key1, "field2", "value2"); + Assert.Equal(0, pendingInfo.PendingMessageCount); + } - db.StreamAdd(key2, "field3", "value3"); - db.StreamAdd(key2, "field4", "value4"); + [Fact] + public void StreamReadGroupMultiStreamWithNoAckShowsNoPendingMessages() + { + using var conn = Create(require: RedisFeatures.v5_0_0); - db.StreamCreateConsumerGroup(key1, groupName, StreamPosition.NewMessages); - db.StreamCreateConsumerGroup(key2, groupName, StreamPosition.NewMessages); + var db = conn.GetDatabase(); + var key1 = GetUniqueKey("read_group_noack1"); + var key2 = GetUniqueKey("read_group_noack2"); + const string groupName = "test_group", + consumer = "consumer"; - db.StreamReadGroup( - new [] - { - new StreamPosition(key1, StreamPosition.NewMessages), - new StreamPosition(key2, StreamPosition.NewMessages) - }, - groupName, - consumer, - noAck: true); + db.StreamAdd(key1, "field1", "value1"); + db.StreamAdd(key1, "field2", "value2"); - var pending1 = db.StreamPending(key1, groupName); - var pending2 = db.StreamPending(key2, groupName); + db.StreamAdd(key2, "field3", "value3"); + db.StreamAdd(key2, "field4", "value4"); - Assert.Equal(0, pending1.PendingMessageCount); - Assert.Equal(0, pending2.PendingMessageCount); - } - } + db.StreamCreateConsumerGroup(key1, groupName, StreamPosition.NewMessages); + db.StreamCreateConsumerGroup(key2, groupName, StreamPosition.NewMessages); - private static RedisKey GetUniqueKey(string type) => $"{type}_stream_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; + db.StreamReadGroup(new[] + { + new StreamPosition(key1, StreamPosition.NewMessages), + new StreamPosition(key2, StreamPosition.NewMessages) + }, + groupName, + consumer, + noAck: true); + + var pending1 = db.StreamPending(key1, groupName); + var pending2 = db.StreamPending(key2, groupName); + + Assert.Equal(0, pending1.PendingMessageCount); + Assert.Equal(0, pending2.PendingMessageCount); + } - [Fact] - public async Task StreamReadIndexerUsage() - { - var streamName = GetUniqueKey("read-group-indexer"); + private static RedisKey GetUniqueKey(string type) => $"{type}_stream_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - - await db.StreamAddAsync(streamName, new[] { - new NameValueEntry("x", "blah"), - new NameValueEntry("msg", /*lang=json,strict*/ @"{""name"":""test"",""id"":123}"), - new NameValueEntry("y", "more blah"), - }); - - var streamResult = await db.StreamRangeAsync(streamName, count: 1000); - var evntJson = streamResult - .Select(x => (dynamic?)JsonConvert.DeserializeObject(x["msg"]!)) - .ToList(); - var obj = Assert.Single(evntJson); - Assert.Equal(123, (int)obj!.id); - Assert.Equal("test", (string)obj.name); - } - } + [Fact] + public async Task StreamReadIndexerUsage() + { + using var conn = Create(require: RedisFeatures.v5_0_0); + + var db = conn.GetDatabase(); + var streamName = GetUniqueKey("read-group-indexer"); + + await db.StreamAddAsync(streamName, new[] { + new NameValueEntry("x", "blah"), + new NameValueEntry("msg", /*lang=json,strict*/ @"{""name"":""test"",""id"":123}"), + new NameValueEntry("y", "more blah"), + }); + + var streamResult = await db.StreamRangeAsync(streamName, count: 1000); + var evntJson = streamResult + .Select(x => (dynamic?)JsonConvert.DeserializeObject(x["msg"]!)) + .ToList(); + var obj = Assert.Single(evntJson); + Assert.Equal(123, (int)obj!.id); + Assert.Equal("test", (string)obj.name); } } diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index 9f166ee8e..f1ae1f049 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -6,622 +6,578 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Strings : TestBase // https://redis.io/commands#string { - [Collection(SharedConnectionFixture.Key)] - public class Strings : TestBase // https://redis.io/commands#string + public Strings(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public async Task Append() { - public Strings(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + using var conn = Create(); + + var db = conn.GetDatabase(); + var server = GetServer(conn); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + var l0 = server.Features.StringLength ? db.StringLengthAsync(key) : null; + + var s0 = db.StringGetAsync(key); + + db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); + var s1 = db.StringGetAsync(key); + var l1 = server.Features.StringLength ? db.StringLengthAsync(key) : null; - [Fact] - public async Task Append() + var result = db.StringAppendAsync(key, Encode("defgh")); + var s3 = db.StringGetAsync(key); + var l2 = server.Features.StringLength ? db.StringLengthAsync(key) : null; + + Assert.Null((string?)await s0); + Assert.Equal("abc", await s1); + Assert.Equal(8, await result); + Assert.Equal("abcdefgh", await s3); + + if (server.Features.StringLength) { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var server = GetServer(muxer); - var key = Me(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - var l0 = server.Features.StringLength ? conn.StringLengthAsync(key) : null; - - var s0 = conn.StringGetAsync(key); - - conn.StringSet(key, "abc", flags: CommandFlags.FireAndForget); - var s1 = conn.StringGetAsync(key); - var l1 = server.Features.StringLength ? conn.StringLengthAsync(key) : null; - - var result = conn.StringAppendAsync(key, Encode("defgh")); - var s3 = conn.StringGetAsync(key); - var l2 = server.Features.StringLength ? conn.StringLengthAsync(key) : null; - - Assert.Null((string?)await s0); - Assert.Equal("abc", await s1); - Assert.Equal(8, await result); - Assert.Equal("abcdefgh", await s3); - - if (server.Features.StringLength) - { - Assert.Equal(0, await l0!); - Assert.Equal(3, await l1!); - Assert.Equal(8, await l2!); - } - } + Assert.Equal(0, await l0!); + Assert.Equal(3, await l1!); + Assert.Equal(8, await l2!); } + } - [Fact] - public async Task Set() - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var key = Me(); - conn.KeyDelete(key, CommandFlags.FireAndForget); + [Fact] + public async Task Set() + { + using var conn = Create(); - conn.StringSet(key, "abc", flags: CommandFlags.FireAndForget); - var v1 = conn.StringGetAsync(key); + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - conn.StringSet(key, Encode("def"), flags: CommandFlags.FireAndForget); - var v2 = conn.StringGetAsync(key); + db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); + var v1 = db.StringGetAsync(key); - Assert.Equal("abc", await v1); - Assert.Equal("def", Decode(await v2)); - } - } + db.StringSet(key, Encode("def"), flags: CommandFlags.FireAndForget); + var v2 = db.StringGetAsync(key); - [Fact] - public async Task StringGetSetExpiryNoValue() - { - using var muxer = Create(); - Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + Assert.Equal("abc", await v1); + Assert.Equal("def", Decode(await v2)); + } - var conn = muxer.GetDatabase(); - var key = Me(); - conn.KeyDelete(key, CommandFlags.FireAndForget); + [Fact] + public async Task StringGetSetExpiryNoValue() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - var emptyVal = await conn.StringGetSetExpiryAsync(key, TimeSpan.FromHours(1)); + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - Assert.Equal(RedisValue.Null, emptyVal); - } + var emptyVal = await db.StringGetSetExpiryAsync(key, TimeSpan.FromHours(1)); - [Fact] - public async Task StringGetSetExpiryRelative() - { - using var muxer = Create(); - Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + Assert.Equal(RedisValue.Null, emptyVal); + } - var conn = muxer.GetDatabase(); - var key = Me(); - conn.KeyDelete(key, CommandFlags.FireAndForget); + [Fact] + public async Task StringGetSetExpiryRelative() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - conn.StringSet(key, "abc", TimeSpan.FromHours(1)); - var relativeSec = conn.StringGetSetExpiryAsync(key, TimeSpan.FromMinutes(30)); - var relativeSecTtl = conn.KeyTimeToLiveAsync(key); + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - Assert.Equal("abc", await relativeSec); - var time = await relativeSecTtl; - Assert.NotNull(time); - Assert.InRange(time.Value, TimeSpan.FromMinutes(29.8), TimeSpan.FromMinutes(30.2)); - } + db.StringSet(key, "abc", TimeSpan.FromHours(1)); + var relativeSec = db.StringGetSetExpiryAsync(key, TimeSpan.FromMinutes(30)); + var relativeSecTtl = db.KeyTimeToLiveAsync(key); - [Fact] - public async Task StringGetSetExpiryAbsolute() - { - using var muxer = Create(); - Skip.IfBelow(muxer, RedisFeatures.v6_2_0); - - var conn = muxer.GetDatabase(); - var key = Me(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - - conn.StringSet(key, "abc", TimeSpan.FromHours(1)); - var newDate = DateTime.UtcNow.AddMinutes(30); - var val = conn.StringGetSetExpiryAsync(key, newDate); - var valTtl = conn.KeyTimeToLiveAsync(key); - - Assert.Equal("abc", await val); - var time = await valTtl; - Assert.NotNull(time); - Assert.InRange(time.Value, TimeSpan.FromMinutes(29.8), TimeSpan.FromMinutes(30.2)); - - // And ensure our type checking works - var ex = await Assert.ThrowsAsync(() => conn.StringGetSetExpiryAsync(key, new DateTime(100, DateTimeKind.Unspecified))); - Assert.NotNull(ex); - } + Assert.Equal("abc", await relativeSec); + var time = await relativeSecTtl; + Assert.NotNull(time); + Assert.InRange(time.Value, TimeSpan.FromMinutes(29.8), TimeSpan.FromMinutes(30.2)); + } - [Fact] - public async Task StringGetSetExpiryPersist() - { - using var muxer = Create(); - Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + [Fact] + public async Task StringGetSetExpiryAbsolute() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - var conn = muxer.GetDatabase(); - var key = Me(); - conn.KeyDelete(key, CommandFlags.FireAndForget); + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - conn.StringSet(key, "abc", TimeSpan.FromHours(1)); - var val = conn.StringGetSetExpiryAsync(key, null); - var valTtl = conn.KeyTimeToLiveAsync(key); + db.StringSet(key, "abc", TimeSpan.FromHours(1)); + var newDate = DateTime.UtcNow.AddMinutes(30); + var val = db.StringGetSetExpiryAsync(key, newDate); + var valTtl = db.KeyTimeToLiveAsync(key); - Assert.Equal("abc", await val); - Assert.Null(await valTtl); - } + Assert.Equal("abc", await val); + var time = await valTtl; + Assert.NotNull(time); + Assert.InRange(time.Value, TimeSpan.FromMinutes(29.8), TimeSpan.FromMinutes(30.2)); + + // And ensure our type checking works + var ex = await Assert.ThrowsAsync(() => db.StringGetSetExpiryAsync(key, new DateTime(100, DateTimeKind.Unspecified))); + Assert.NotNull(ex); + } + + [Fact] + public async Task StringGetSetExpiryPersist() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.StringSet(key, "abc", TimeSpan.FromHours(1)); + var val = db.StringGetSetExpiryAsync(key, null); + var valTtl = db.KeyTimeToLiveAsync(key); - [Fact] - public async Task GetLease() + Assert.Equal("abc", await val); + Assert.Null(await valTtl); + } + + [Fact] + public async Task GetLease() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); + using (var v1 = await db.StringGetLeaseAsync(key).ConfigureAwait(false)) { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var key = Me(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - - conn.StringSet(key, "abc", flags: CommandFlags.FireAndForget); - using (var v1 = await conn.StringGetLeaseAsync(key).ConfigureAwait(false)) - { - string? s = v1?.DecodeString(); - Assert.Equal("abc", s); - } - } + string? s = v1?.DecodeString(); + Assert.Equal("abc", s); } + } + + [Fact] + public async Task GetLeaseAsStream() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - [Fact] - public async Task GetLeaseAsStream() + db.StringSet(key, "abc", flags: CommandFlags.FireAndForget); + var lease = await db.StringGetLeaseAsync(key).ConfigureAwait(false); + Assert.NotNull(lease); + using (var v1 = lease.AsStream()) { - using (var muxer = Create()) + using (var sr = new StreamReader(v1)) { - var conn = muxer.GetDatabase(); - var key = Me(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - - conn.StringSet(key, "abc", flags: CommandFlags.FireAndForget); - var lease = await conn.StringGetLeaseAsync(key).ConfigureAwait(false); - Assert.NotNull(lease); - using (var v1 = lease.AsStream()) - { - using (var sr = new StreamReader(v1)) - { - string s = sr.ReadToEnd(); - Assert.Equal("abc", s); - } - } + string s = sr.ReadToEnd(); + Assert.Equal("abc", s); } } + } - [Fact] - public void GetDelete() - { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + [Fact] + public void GetDelete() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - var conn = muxer.GetDatabase(); - var prefix = Me(); - conn.KeyDelete(prefix + "1", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "2", CommandFlags.FireAndForget); - conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); + var db = conn.GetDatabase(); + var prefix = Me(); + db.KeyDelete(prefix + "1", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "2", CommandFlags.FireAndForget); + db.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); - Assert.True(conn.KeyExists(prefix + "1")); - Assert.False(conn.KeyExists(prefix + "2")); + Assert.True(db.KeyExists(prefix + "1")); + Assert.False(db.KeyExists(prefix + "2")); - var s0 = conn.StringGetDelete(prefix + "1"); - var s2 = conn.StringGetDelete(prefix + "2"); + var s0 = db.StringGetDelete(prefix + "1"); + var s2 = db.StringGetDelete(prefix + "2"); - Assert.False(conn.KeyExists(prefix + "1")); - Assert.Equal("abc", s0); - Assert.Equal(RedisValue.Null, s2); - } - } + Assert.False(db.KeyExists(prefix + "1")); + Assert.Equal("abc", s0); + Assert.Equal(RedisValue.Null, s2); + } - [Fact] - public async Task GetDeleteAsync() - { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v6_2_0); + [Fact] + public async Task GetDeleteAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); - var conn = muxer.GetDatabase(); - var prefix = Me(); - conn.KeyDelete(prefix + "1", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "2", CommandFlags.FireAndForget); - conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); + var db = conn.GetDatabase(); + var prefix = Me(); + db.KeyDelete(prefix + "1", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "2", CommandFlags.FireAndForget); + db.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); - Assert.True(conn.KeyExists(prefix + "1")); - Assert.False(conn.KeyExists(prefix + "2")); + Assert.True(db.KeyExists(prefix + "1")); + Assert.False(db.KeyExists(prefix + "2")); - var s0 = conn.StringGetDeleteAsync(prefix + "1"); - var s2 = conn.StringGetDeleteAsync(prefix + "2"); + var s0 = db.StringGetDeleteAsync(prefix + "1"); + var s2 = db.StringGetDeleteAsync(prefix + "2"); - Assert.False(conn.KeyExists(prefix + "1")); - Assert.Equal("abc", await s0); - Assert.Equal(RedisValue.Null, await s2); - } - } + Assert.False(db.KeyExists(prefix + "1")); + Assert.Equal("abc", await s0); + Assert.Equal(RedisValue.Null, await s2); + } - [Fact] - public async Task SetNotExists() - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var prefix = Me(); - conn.KeyDelete(prefix + "1", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "2", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "3", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "4", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "5", CommandFlags.FireAndForget); - conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); - - var x0 = conn.StringSetAsync(prefix + "1", "def", when: When.NotExists); - var x1 = conn.StringSetAsync(prefix + "1", Encode("def"), when: When.NotExists); - var x2 = conn.StringSetAsync(prefix + "2", "def", when: When.NotExists); - var x3 = conn.StringSetAsync(prefix + "3", Encode("def"), when: When.NotExists); - var x4 = conn.StringSetAsync(prefix + "4", "def", expiry: TimeSpan.FromSeconds(4), when: When.NotExists); - var x5 = conn.StringSetAsync(prefix + "5", "def", expiry: TimeSpan.FromMilliseconds(4001), when: When.NotExists); - - var s0 = conn.StringGetAsync(prefix + "1"); - var s2 = conn.StringGetAsync(prefix + "2"); - var s3 = conn.StringGetAsync(prefix + "3"); - - Assert.False(await x0); - Assert.False(await x1); - Assert.True(await x2); - Assert.True(await x3); - Assert.True(await x4); - Assert.True(await x5); - Assert.Equal("abc", await s0); - Assert.Equal("def", await s2); - Assert.Equal("def", await s3); - } - } + [Fact] + public async Task SetNotExists() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var prefix = Me(); + db.KeyDelete(prefix + "1", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "2", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "3", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "4", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "5", CommandFlags.FireAndForget); + db.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); + + var x0 = db.StringSetAsync(prefix + "1", "def", when: When.NotExists); + var x1 = db.StringSetAsync(prefix + "1", Encode("def"), when: When.NotExists); + var x2 = db.StringSetAsync(prefix + "2", "def", when: When.NotExists); + var x3 = db.StringSetAsync(prefix + "3", Encode("def"), when: When.NotExists); + var x4 = db.StringSetAsync(prefix + "4", "def", expiry: TimeSpan.FromSeconds(4), when: When.NotExists); + var x5 = db.StringSetAsync(prefix + "5", "def", expiry: TimeSpan.FromMilliseconds(4001), when: When.NotExists); + + var s0 = db.StringGetAsync(prefix + "1"); + var s2 = db.StringGetAsync(prefix + "2"); + var s3 = db.StringGetAsync(prefix + "3"); + + Assert.False(await x0); + Assert.False(await x1); + Assert.True(await x2); + Assert.True(await x3); + Assert.True(await x4); + Assert.True(await x5); + Assert.Equal("abc", await s0); + Assert.Equal("def", await s2); + Assert.Equal("def", await s3); + } - [Fact] - public async Task SetKeepTtl() - { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v6_0_0); - - var conn = muxer.GetDatabase(); - var prefix = Me(); - conn.KeyDelete(prefix + "1", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "2", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "3", CommandFlags.FireAndForget); - conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); - conn.StringSet(prefix + "2", "abc", expiry: TimeSpan.FromMinutes(5), flags: CommandFlags.FireAndForget); - conn.StringSet(prefix + "3", "abc", expiry: TimeSpan.FromMinutes(10), flags: CommandFlags.FireAndForget); - - var x0 = conn.KeyTimeToLiveAsync(prefix + "1"); - var x1 = conn.KeyTimeToLiveAsync(prefix + "2"); - var x2 = conn.KeyTimeToLiveAsync(prefix + "3"); - - Assert.Null(await x0); - Assert.True(await x1 > TimeSpan.FromMinutes(4), "Over 4"); - Assert.True(await x1 <= TimeSpan.FromMinutes(5), "Under 5"); - Assert.True(await x2 > TimeSpan.FromMinutes(9), "Over 9"); - Assert.True(await x2 <= TimeSpan.FromMinutes(10), "Under 10"); - - conn.StringSet(prefix + "1", "def", keepTtl: true, flags: CommandFlags.FireAndForget); - conn.StringSet(prefix + "2", "def", flags: CommandFlags.FireAndForget); - conn.StringSet(prefix + "3", "def", keepTtl: true, flags: CommandFlags.FireAndForget); - - var y0 = conn.KeyTimeToLiveAsync(prefix + "1"); - var y1 = conn.KeyTimeToLiveAsync(prefix + "2"); - var y2 = conn.KeyTimeToLiveAsync(prefix + "3"); - - Assert.Null(await y0); - Assert.Null(await y1); - Assert.True(await y2 > TimeSpan.FromMinutes(9), "Over 9"); - Assert.True(await y2 <= TimeSpan.FromMinutes(10), "Under 10"); - } - } + [Fact] + public async Task SetKeepTtl() + { + using var conn = Create(require: RedisFeatures.v6_0_0); + + var db = conn.GetDatabase(); + var prefix = Me(); + db.KeyDelete(prefix + "1", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "2", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "3", CommandFlags.FireAndForget); + db.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "2", "abc", expiry: TimeSpan.FromMinutes(5), flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "3", "abc", expiry: TimeSpan.FromMinutes(10), flags: CommandFlags.FireAndForget); + + var x0 = db.KeyTimeToLiveAsync(prefix + "1"); + var x1 = db.KeyTimeToLiveAsync(prefix + "2"); + var x2 = db.KeyTimeToLiveAsync(prefix + "3"); + + Assert.Null(await x0); + Assert.True(await x1 > TimeSpan.FromMinutes(4), "Over 4"); + Assert.True(await x1 <= TimeSpan.FromMinutes(5), "Under 5"); + Assert.True(await x2 > TimeSpan.FromMinutes(9), "Over 9"); + Assert.True(await x2 <= TimeSpan.FromMinutes(10), "Under 10"); + + db.StringSet(prefix + "1", "def", keepTtl: true, flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "2", "def", flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "3", "def", keepTtl: true, flags: CommandFlags.FireAndForget); + + var y0 = db.KeyTimeToLiveAsync(prefix + "1"); + var y1 = db.KeyTimeToLiveAsync(prefix + "2"); + var y2 = db.KeyTimeToLiveAsync(prefix + "3"); + + Assert.Null(await y0); + Assert.Null(await y1); + Assert.True(await y2 > TimeSpan.FromMinutes(9), "Over 9"); + Assert.True(await y2 <= TimeSpan.FromMinutes(10), "Under 10"); + } - [Fact] - public async Task SetAndGet() - { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v6_2_0); - - var conn = muxer.GetDatabase(); - var prefix = Me(); - conn.KeyDelete(prefix + "1", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "2", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "3", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "4", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "5", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "6", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "7", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "8", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "9", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "10", CommandFlags.FireAndForget); - conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); - conn.StringSet(prefix + "2", "abc", flags: CommandFlags.FireAndForget); - conn.StringSet(prefix + "4", "abc", flags: CommandFlags.FireAndForget); - conn.StringSet(prefix + "6", "abc", flags: CommandFlags.FireAndForget); - conn.StringSet(prefix + "7", "abc", flags: CommandFlags.FireAndForget); - conn.StringSet(prefix + "8", "abc", flags: CommandFlags.FireAndForget); - conn.StringSet(prefix + "9", "abc", flags: CommandFlags.FireAndForget); - conn.StringSet(prefix + "10", "abc", expiry: TimeSpan.FromMinutes(10), flags: CommandFlags.FireAndForget); - - var x0 = conn.StringSetAndGetAsync(prefix + "1", RedisValue.Null); - var x1 = conn.StringSetAndGetAsync(prefix + "2", "def"); - var x2 = conn.StringSetAndGetAsync(prefix + "3", "def"); - var x3 = conn.StringSetAndGetAsync(prefix + "4", "def", when: When.Exists); - var x4 = conn.StringSetAndGetAsync(prefix + "5", "def", when: When.Exists); - var x5 = conn.StringSetAndGetAsync(prefix + "6", "def", expiry: TimeSpan.FromSeconds(4)); - var x6 = conn.StringSetAndGetAsync(prefix + "7", "def", expiry: TimeSpan.FromMilliseconds(4001)); - var x7 = conn.StringSetAndGetAsync(prefix + "8", "def", expiry: TimeSpan.FromSeconds(4), when: When.Exists); - var x8 = conn.StringSetAndGetAsync(prefix + "9", "def", expiry: TimeSpan.FromMilliseconds(4001), when: When.Exists); - - var y0 = conn.StringSetAndGetAsync(prefix + "10", "def", keepTtl: true); - var y1 = conn.KeyTimeToLiveAsync(prefix + "10"); - var y2 = conn.StringGetAsync(prefix + "10"); - - var s0 = conn.StringGetAsync(prefix + "1"); - var s1 = conn.StringGetAsync(prefix + "2"); - var s2 = conn.StringGetAsync(prefix + "3"); - var s3 = conn.StringGetAsync(prefix + "4"); - var s4 = conn.StringGetAsync(prefix + "5"); - - Assert.Equal("abc", await x0); - Assert.Equal("abc", await x1); - Assert.Equal(RedisValue.Null, await x2); - Assert.Equal("abc", await x3); - Assert.Equal(RedisValue.Null, await x4); - Assert.Equal("abc", await x5); - Assert.Equal("abc", await x6); - Assert.Equal("abc", await x7); - Assert.Equal("abc", await x8); - - Assert.Equal("abc", await y0); - Assert.True(await y1 <= TimeSpan.FromMinutes(10), "Under 10 min"); - Assert.True(await y1 >= TimeSpan.FromMinutes(8), "Over 8 min"); - Assert.Equal("def", await y2); - - Assert.Equal(RedisValue.Null, await s0); - Assert.Equal("def", await s1); - Assert.Equal("def", await s2); - Assert.Equal("def", await s3); - Assert.Equal(RedisValue.Null, await s4); - } - } + [Fact] + public async Task SetAndGet() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var prefix = Me(); + db.KeyDelete(prefix + "1", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "2", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "3", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "4", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "5", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "6", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "7", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "8", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "9", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "10", CommandFlags.FireAndForget); + db.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "2", "abc", flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "4", "abc", flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "6", "abc", flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "7", "abc", flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "8", "abc", flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "9", "abc", flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "10", "abc", expiry: TimeSpan.FromMinutes(10), flags: CommandFlags.FireAndForget); + + var x0 = db.StringSetAndGetAsync(prefix + "1", RedisValue.Null); + var x1 = db.StringSetAndGetAsync(prefix + "2", "def"); + var x2 = db.StringSetAndGetAsync(prefix + "3", "def"); + var x3 = db.StringSetAndGetAsync(prefix + "4", "def", when: When.Exists); + var x4 = db.StringSetAndGetAsync(prefix + "5", "def", when: When.Exists); + var x5 = db.StringSetAndGetAsync(prefix + "6", "def", expiry: TimeSpan.FromSeconds(4)); + var x6 = db.StringSetAndGetAsync(prefix + "7", "def", expiry: TimeSpan.FromMilliseconds(4001)); + var x7 = db.StringSetAndGetAsync(prefix + "8", "def", expiry: TimeSpan.FromSeconds(4), when: When.Exists); + var x8 = db.StringSetAndGetAsync(prefix + "9", "def", expiry: TimeSpan.FromMilliseconds(4001), when: When.Exists); + + var y0 = db.StringSetAndGetAsync(prefix + "10", "def", keepTtl: true); + var y1 = db.KeyTimeToLiveAsync(prefix + "10"); + var y2 = db.StringGetAsync(prefix + "10"); + + var s0 = db.StringGetAsync(prefix + "1"); + var s1 = db.StringGetAsync(prefix + "2"); + var s2 = db.StringGetAsync(prefix + "3"); + var s3 = db.StringGetAsync(prefix + "4"); + var s4 = db.StringGetAsync(prefix + "5"); + + Assert.Equal("abc", await x0); + Assert.Equal("abc", await x1); + Assert.Equal(RedisValue.Null, await x2); + Assert.Equal("abc", await x3); + Assert.Equal(RedisValue.Null, await x4); + Assert.Equal("abc", await x5); + Assert.Equal("abc", await x6); + Assert.Equal("abc", await x7); + Assert.Equal("abc", await x8); + + Assert.Equal("abc", await y0); + Assert.True(await y1 <= TimeSpan.FromMinutes(10), "Under 10 min"); + Assert.True(await y1 >= TimeSpan.FromMinutes(8), "Over 8 min"); + Assert.Equal("def", await y2); + + Assert.Equal(RedisValue.Null, await s0); + Assert.Equal("def", await s1); + Assert.Equal("def", await s2); + Assert.Equal("def", await s3); + Assert.Equal(RedisValue.Null, await s4); + } - [Fact] - public async Task SetNotExistsAndGet() - { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v7_0_0_rc1); - - var conn = muxer.GetDatabase(); - var prefix = Me(); - conn.KeyDelete(prefix + "1", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "2", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "3", CommandFlags.FireAndForget); - conn.KeyDelete(prefix + "4", CommandFlags.FireAndForget); - conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); - - var x0 = conn.StringSetAndGetAsync(prefix + "1", "def", when: When.NotExists); - var x1 = conn.StringSetAndGetAsync(prefix + "2", "def", when: When.NotExists); - var x2 = conn.StringSetAndGetAsync(prefix + "3", "def", expiry: TimeSpan.FromSeconds(4), when: When.NotExists); - var x3 = conn.StringSetAndGetAsync(prefix + "4", "def", expiry: TimeSpan.FromMilliseconds(4001), when: When.NotExists); - - var s0 = conn.StringGetAsync(prefix + "1"); - var s1 = conn.StringGetAsync(prefix + "2"); - - Assert.Equal("abc", await x0); - Assert.Equal(RedisValue.Null, await x1); - Assert.Equal(RedisValue.Null, await x2); - Assert.Equal(RedisValue.Null, await x3); - - Assert.Equal("abc", await s0); - Assert.Equal("def", await s1); - } - } + [Fact] + public async Task SetNotExistsAndGet() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var prefix = Me(); + db.KeyDelete(prefix + "1", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "2", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "3", CommandFlags.FireAndForget); + db.KeyDelete(prefix + "4", CommandFlags.FireAndForget); + db.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget); + + var x0 = db.StringSetAndGetAsync(prefix + "1", "def", when: When.NotExists); + var x1 = db.StringSetAndGetAsync(prefix + "2", "def", when: When.NotExists); + var x2 = db.StringSetAndGetAsync(prefix + "3", "def", expiry: TimeSpan.FromSeconds(4), when: When.NotExists); + var x3 = db.StringSetAndGetAsync(prefix + "4", "def", expiry: TimeSpan.FromMilliseconds(4001), when: When.NotExists); + + var s0 = db.StringGetAsync(prefix + "1"); + var s1 = db.StringGetAsync(prefix + "2"); + + Assert.Equal("abc", await x0); + Assert.Equal(RedisValue.Null, await x1); + Assert.Equal(RedisValue.Null, await x2); + Assert.Equal(RedisValue.Null, await x3); + + Assert.Equal("abc", await s0); + Assert.Equal("def", await s1); + } - [Fact] - public async Task Ranges() - { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v2_1_8); - var conn = muxer.GetDatabase(); - var key = Me(); + [Fact] + public async Task Ranges() + { + using var conn = Create(require: RedisFeatures.v2_1_8); - conn.KeyDelete(key, CommandFlags.FireAndForget); + var db = conn.GetDatabase(); + var key = Me(); - conn.StringSet(key, "abcdefghi", flags: CommandFlags.FireAndForget); - conn.StringSetRange(key, 2, "xy", CommandFlags.FireAndForget); - conn.StringSetRange(key, 4, Encode("z"), CommandFlags.FireAndForget); + db.KeyDelete(key, CommandFlags.FireAndForget); - var val = conn.StringGetAsync(key); + db.StringSet(key, "abcdefghi", flags: CommandFlags.FireAndForget); + db.StringSetRange(key, 2, "xy", CommandFlags.FireAndForget); + db.StringSetRange(key, 4, Encode("z"), CommandFlags.FireAndForget); - Assert.Equal("abxyzfghi", await val); - } - } + var val = db.StringGetAsync(key); - [Fact] - public async Task IncrDecr() - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var key = Me(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - - conn.StringSet(key, "2", flags: CommandFlags.FireAndForget); - var v1 = conn.StringIncrementAsync(key); - var v2 = conn.StringIncrementAsync(key, 5); - var v3 = conn.StringIncrementAsync(key, -2); - var v4 = conn.StringDecrementAsync(key); - var v5 = conn.StringDecrementAsync(key, 5); - var v6 = conn.StringDecrementAsync(key, -2); - var s = conn.StringGetAsync(key); - - Assert.Equal(3, await v1); - Assert.Equal(8, await v2); - Assert.Equal(6, await v3); - Assert.Equal(5, await v4); - Assert.Equal(0, await v5); - Assert.Equal(2, await v6); - Assert.Equal("2", await s); - } - } + Assert.Equal("abxyzfghi", await val); + } - [Fact] - public async Task IncrDecrFloat() - { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v2_6_0); - var conn = muxer.GetDatabase(); - var key = Me(); - conn.KeyDelete(key, CommandFlags.FireAndForget); - - conn.StringSet(key, "2", flags: CommandFlags.FireAndForget); - var v1 = conn.StringIncrementAsync(key, 1.1); - var v2 = conn.StringIncrementAsync(key, 5.0); - var v3 = conn.StringIncrementAsync(key, -2.0); - var v4 = conn.StringIncrementAsync(key, -1.0); - var v5 = conn.StringIncrementAsync(key, -5.0); - var v6 = conn.StringIncrementAsync(key, 2.0); - - var s = conn.StringGetAsync(key); - - Assert.Equal(3.1, await v1, 5); - Assert.Equal(8.1, await v2, 5); - Assert.Equal(6.1, await v3, 5); - Assert.Equal(5.1, await v4, 5); - Assert.Equal(0.1, await v5, 5); - Assert.Equal(2.1, await v6, 5); - Assert.Equal(2.1, (double)await s, 5); - } - } + [Fact] + public async Task IncrDecr() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.StringSet(key, "2", flags: CommandFlags.FireAndForget); + var v1 = db.StringIncrementAsync(key); + var v2 = db.StringIncrementAsync(key, 5); + var v3 = db.StringIncrementAsync(key, -2); + var v4 = db.StringDecrementAsync(key); + var v5 = db.StringDecrementAsync(key, 5); + var v6 = db.StringDecrementAsync(key, -2); + var s = db.StringGetAsync(key); + + Assert.Equal(3, await v1); + Assert.Equal(8, await v2); + Assert.Equal(6, await v3); + Assert.Equal(5, await v4); + Assert.Equal(0, await v5); + Assert.Equal(2, await v6); + Assert.Equal("2", await s); + } - [Fact] - public async Task GetRange() - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var key = Me(); - conn.KeyDelete(key, CommandFlags.FireAndForget); + [Fact] + public async Task IncrDecrFloat() + { + using var conn = Create(require: RedisFeatures.v2_6_0); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.StringSet(key, "2", flags: CommandFlags.FireAndForget); + var v1 = db.StringIncrementAsync(key, 1.1); + var v2 = db.StringIncrementAsync(key, 5.0); + var v3 = db.StringIncrementAsync(key, -2.0); + var v4 = db.StringIncrementAsync(key, -1.0); + var v5 = db.StringIncrementAsync(key, -5.0); + var v6 = db.StringIncrementAsync(key, 2.0); + + var s = db.StringGetAsync(key); + + Assert.Equal(3.1, await v1, 5); + Assert.Equal(8.1, await v2, 5); + Assert.Equal(6.1, await v3, 5); + Assert.Equal(5.1, await v4, 5); + Assert.Equal(0.1, await v5, 5); + Assert.Equal(2.1, await v6, 5); + Assert.Equal(2.1, (double)await s, 5); + } - conn.StringSet(key, "abcdefghi", flags: CommandFlags.FireAndForget); - var s = conn.StringGetRangeAsync(key, 2, 4); - var b = conn.StringGetRangeAsync(key, 2, 4); + [Fact] + public async Task GetRange() + { + using var conn = Create(); - Assert.Equal("cde", await s); - Assert.Equal("cde", Decode(await b)); - } - } + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); - [Fact] - public async Task BitCount() - { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v2_6_0); - - var conn = muxer.GetDatabase(); - var key = Me(); - conn.StringSet(key, "foobar", flags: CommandFlags.FireAndForget); - var r1 = conn.StringBitCountAsync(key); - var r2 = conn.StringBitCountAsync(key, 0, 0); - var r3 = conn.StringBitCountAsync(key, 1, 1); - - Assert.Equal(26, await r1); - Assert.Equal(4, await r2); - Assert.Equal(6, await r3); - } - } + db.StringSet(key, "abcdefghi", flags: CommandFlags.FireAndForget); + var s = db.StringGetRangeAsync(key, 2, 4); + var b = db.StringGetRangeAsync(key, 2, 4); - [Fact] - public async Task BitOp() - { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v2_6_0); - - var conn = muxer.GetDatabase(); - var prefix = Me(); - var key1 = prefix + "1"; - var key2 = prefix + "2"; - var key3 = prefix + "3"; - conn.StringSet(key1, new byte[] { 3 }, flags: CommandFlags.FireAndForget); - conn.StringSet(key2, new byte[] { 6 }, flags: CommandFlags.FireAndForget); - conn.StringSet(key3, new byte[] { 12 }, flags: CommandFlags.FireAndForget); - - var len_and = conn.StringBitOperationAsync(Bitwise.And, "and", new RedisKey[] { key1, key2, key3 }); - var len_or = conn.StringBitOperationAsync(Bitwise.Or, "or", new RedisKey[] { key1, key2, key3 }); - var len_xor = conn.StringBitOperationAsync(Bitwise.Xor, "xor", new RedisKey[] { key1, key2, key3 }); - var len_not = conn.StringBitOperationAsync(Bitwise.Not, "not", key1); - - Assert.Equal(1, await len_and); - Assert.Equal(1, await len_or); - Assert.Equal(1, await len_xor); - Assert.Equal(1, await len_not); - - var r_and = ((byte[]?)(await conn.StringGetAsync("and").ForAwait()))?.Single(); - var r_or = ((byte[]?)(await conn.StringGetAsync("or").ForAwait()))?.Single(); - var r_xor = ((byte[]?)(await conn.StringGetAsync("xor").ForAwait()))?.Single(); - var r_not = ((byte[]?)(await conn.StringGetAsync("not").ForAwait()))?.Single(); - - Assert.Equal((byte)(3 & 6 & 12), r_and); - Assert.Equal((byte)(3 | 6 | 12), r_or); - Assert.Equal((byte)(3 ^ 6 ^ 12), r_xor); - Assert.Equal(unchecked((byte)(~3)), r_not); - } - } + Assert.Equal("cde", await s); + Assert.Equal("cde", Decode(await b)); + } - [Fact] - public async Task RangeString() - { - using (var muxer = Create()) - { - var conn = muxer.GetDatabase(); - var key = Me(); - conn.StringSet(key, "hello world", flags: CommandFlags.FireAndForget); - var result = conn.StringGetRangeAsync(key, 2, 6); - Assert.Equal("llo w", await result); - } - } + [Fact] + public async Task BitCount() + { + using var conn = Create(require: RedisFeatures.v2_6_0); + + var db = conn.GetDatabase(); + var key = Me(); + db.StringSet(key, "foobar", flags: CommandFlags.FireAndForget); + var r1 = db.StringBitCountAsync(key); + var r2 = db.StringBitCountAsync(key, 0, 0); + var r3 = db.StringBitCountAsync(key, 1, 1); + + Assert.Equal(26, await r1); + Assert.Equal(4, await r2); + Assert.Equal(6, await r3); + } - [Fact] - public async Task HashStringLengthAsync() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v3_2_0); - - var database = conn.GetDatabase(); - var key = Me(); - const string value = "hello world"; - database.HashSet(key, "field", value); - var resAsync = database.HashStringLengthAsync(key, "field"); - var resNonExistingAsync = database.HashStringLengthAsync(key, "non-existing-field"); - Assert.Equal(value.Length, await resAsync); - Assert.Equal(0, await resNonExistingAsync); - } - } + [Fact] + public async Task BitOp() + { + using var conn = Create(require: RedisFeatures.v2_6_0); + + var db = conn.GetDatabase(); + var prefix = Me(); + var key1 = prefix + "1"; + var key2 = prefix + "2"; + var key3 = prefix + "3"; + db.StringSet(key1, new byte[] { 3 }, flags: CommandFlags.FireAndForget); + db.StringSet(key2, new byte[] { 6 }, flags: CommandFlags.FireAndForget); + db.StringSet(key3, new byte[] { 12 }, flags: CommandFlags.FireAndForget); + + var len_and = db.StringBitOperationAsync(Bitwise.And, "and", new RedisKey[] { key1, key2, key3 }); + var len_or = db.StringBitOperationAsync(Bitwise.Or, "or", new RedisKey[] { key1, key2, key3 }); + var len_xor = db.StringBitOperationAsync(Bitwise.Xor, "xor", new RedisKey[] { key1, key2, key3 }); + var len_not = db.StringBitOperationAsync(Bitwise.Not, "not", key1); + + Assert.Equal(1, await len_and); + Assert.Equal(1, await len_or); + Assert.Equal(1, await len_xor); + Assert.Equal(1, await len_not); + + var r_and = ((byte[]?)(await db.StringGetAsync("and").ForAwait()))?.Single(); + var r_or = ((byte[]?)(await db.StringGetAsync("or").ForAwait()))?.Single(); + var r_xor = ((byte[]?)(await db.StringGetAsync("xor").ForAwait()))?.Single(); + var r_not = ((byte[]?)(await db.StringGetAsync("not").ForAwait()))?.Single(); + + Assert.Equal((byte)(3 & 6 & 12), r_and); + Assert.Equal((byte)(3 | 6 | 12), r_or); + Assert.Equal((byte)(3 ^ 6 ^ 12), r_xor); + Assert.Equal(unchecked((byte)(~3)), r_not); + } - [Fact] - public void HashStringLength() - { - using (var conn = Create()) - { - Skip.IfBelow(conn, RedisFeatures.v3_2_0); - - var database = conn.GetDatabase(); - var key = Me(); - const string value = "hello world"; - database.HashSet(key, "field", value); - Assert.Equal(value.Length, database.HashStringLength(key, "field")); - Assert.Equal(0, database.HashStringLength(key, "non-existing-field")); - } - } + [Fact] + public async Task RangeString() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var key = Me(); + db.StringSet(key, "hello world", flags: CommandFlags.FireAndForget); + var result = db.StringGetRangeAsync(key, 2, 6); + Assert.Equal("llo w", await result); + } + + [Fact] + public async Task HashStringLengthAsync() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + + var database = conn.GetDatabase(); + var key = Me(); + const string value = "hello world"; + database.HashSet(key, "field", value); + var resAsync = database.HashStringLengthAsync(key, "field"); + var resNonExistingAsync = database.HashStringLengthAsync(key, "non-existing-field"); + Assert.Equal(value.Length, await resAsync); + Assert.Equal(0, await resNonExistingAsync); + } - private static byte[] Encode(string value) => Encoding.UTF8.GetBytes(value); - private static string? Decode(byte[]? value) => value is null ? null : Encoding.UTF8.GetString(value); + [Fact] + public void HashStringLength() + { + using var conn = Create(require: RedisFeatures.v3_2_0); + + var database = conn.GetDatabase(); + var key = Me(); + const string value = "hello world"; + database.HashSet(key, "field", value); + Assert.Equal(value.Length, database.HashStringLength(key, "field")); + Assert.Equal(0, database.HashStringLength(key, "non-existing-field")); } + + private static byte[] Encode(string value) => Encoding.UTF8.GetBytes(value); + private static string? Decode(byte[]? value) => value is null ? null : Encoding.UTF8.GetString(value); } diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 70a98ded7..a57cdaf14 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -13,498 +13,508 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public abstract class TestBase : IDisposable { - public abstract class TestBase : IDisposable - { - private ITestOutputHelper Output { get; } - protected TextWriterOutputHelper Writer { get; } - protected static bool RunningInCI { get; } = Environment.GetEnvironmentVariable("APPVEYOR") != null; - protected virtual string GetConfiguration() => GetDefaultConfiguration(); - internal static string GetDefaultConfiguration() => TestConfig.Current.PrimaryServerAndPort; + private ITestOutputHelper Output { get; } + protected TextWriterOutputHelper Writer { get; } + protected static bool RunningInCI { get; } = Environment.GetEnvironmentVariable("APPVEYOR") != null; + protected virtual string GetConfiguration() => GetDefaultConfiguration(); + internal static string GetDefaultConfiguration() => TestConfig.Current.PrimaryServerAndPort; + + private readonly SharedConnectionFixture? _fixture; - private readonly SharedConnectionFixture? _fixture; + protected bool SharedFixtureAvailable => _fixture != null && _fixture.IsEnabled; - protected bool SharedFixtureAvailable => _fixture != null && _fixture.IsEnabled; + protected TestBase(ITestOutputHelper output, SharedConnectionFixture? fixture = null) + { + Output = output; + Output.WriteFrameworkVersion(); + Writer = new TextWriterOutputHelper(output, TestConfig.Current.LogToConsole); + _fixture = fixture; + ClearAmbientFailures(); + } - protected TestBase(ITestOutputHelper output, SharedConnectionFixture? fixture = null) + /// + /// Useful to temporarily get extra worker threads for an otherwise synchronous test case which will 'block' the thread, + /// on a synchronous API like or . + /// + /// + /// Must NOT be used for test cases which *goes async*, as then the inferred return type will become 'async void', + /// and we will fail to observe the result of the async part. + /// + /// See 'ConnectFailTimeout' class for example usage. + protected static Task RunBlockingSynchronousWithExtraThreadAsync(Action testScenario) => Task.Factory.StartNew(testScenario, CancellationToken.None, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + + protected void LogNoTime(string message) => LogNoTime(Writer, message); + internal static void LogNoTime(TextWriter output, string? message) + { + lock (output) { - Output = output; - Output.WriteFrameworkVersion(); - Writer = new TextWriterOutputHelper(output, TestConfig.Current.LogToConsole); - _fixture = fixture; - ClearAmbientFailures(); + output.WriteLine(message); } - - /// - /// Useful to temporarily get extra worker threads for an otherwise synchronous test case which will 'block' the thread, - /// on a synchronous API like or . - /// - /// - /// Must NOT be used for test cases which *goes async*, as then the inferred return type will become 'async void', - /// and we will fail to observe the result of the async part. - /// - /// See 'ConnectFailTimeout' class for example usage. - protected static Task RunBlockingSynchronousWithExtraThreadAsync(Action testScenario) => Task.Factory.StartNew(testScenario, CancellationToken.None, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); - - protected void LogNoTime(string message) => LogNoTime(Writer, message); - internal static void LogNoTime(TextWriter output, string? message) + if (TestConfig.Current.LogToConsole) { - lock (output) - { - output.WriteLine(message); - } - if (TestConfig.Current.LogToConsole) - { - Console.WriteLine(message); - } + Console.WriteLine(message); } - protected void Log(string? message) => LogNoTime(Writer, message); - public static void Log(TextWriter output, string message) + } + protected void Log(string? message) => LogNoTime(Writer, message); + public static void Log(TextWriter output, string message) + { + lock (output) { - lock (output) - { - output?.WriteLine(Time() + ": " + message); - } - if (TestConfig.Current.LogToConsole) - { - Console.WriteLine(message); - } + output?.WriteLine(Time() + ": " + message); } - protected void Log(string message, params object?[] args) + if (TestConfig.Current.LogToConsole) { - lock (Output) - { - Output.WriteLine(Time() + ": " + message, args); - } - if (TestConfig.Current.LogToConsole) - { - Console.WriteLine(message, args); - } + Console.WriteLine(message); } - - protected ProfiledCommandEnumerable Log(ProfilingSession session) + } + protected void Log(string message, params object?[] args) + { + lock (Output) { - var profile = session.FinishProfiling(); - foreach (var command in profile) - { - Writer.WriteLineNoTime(command.ToString()); - } - return profile; + Output.WriteLine(Time() + ": " + message, args); } - - protected static void CollectGarbage() + if (TestConfig.Current.LogToConsole) { - GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); - GC.WaitForPendingFinalizers(); - GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); + Console.WriteLine(message, args); } + } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly")] - public void Dispose() + protected ProfiledCommandEnumerable Log(ProfilingSession session) + { + var profile = session.FinishProfiling(); + foreach (var command in profile) { - _fixture?.Teardown(Writer); - Teardown(); - Writer.Dispose(); - GC.SuppressFinalize(this); + Writer.WriteLineNoTime(command.ToString()); } + return profile; + } + + protected static void CollectGarbage() + { + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); + GC.WaitForPendingFinalizers(); + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly")] + public void Dispose() + { + _fixture?.Teardown(Writer); + Teardown(); + Writer.Dispose(); + GC.SuppressFinalize(this); + } #if VERBOSE - protected const int AsyncOpsQty = 100, SyncOpsQty = 10; + protected const int AsyncOpsQty = 100, SyncOpsQty = 10; #else - protected const int AsyncOpsQty = 2000, SyncOpsQty = 2000; + protected const int AsyncOpsQty = 2000, SyncOpsQty = 2000; #endif - static TestBase() + static TestBase() + { + TaskScheduler.UnobservedTaskException += (sender, args) => { - TaskScheduler.UnobservedTaskException += (sender, args) => + Console.WriteLine("Unobserved: " + args.Exception); + args.SetObserved(); + lock (sharedFailCount) { - Console.WriteLine("Unobserved: " + args.Exception); - args.SetObserved(); - lock (sharedFailCount) + if (sharedFailCount != null) { - if (sharedFailCount != null) - { - sharedFailCount.Value++; - } - } - lock (backgroundExceptions) - { - backgroundExceptions.Add(args.Exception.ToString()); + sharedFailCount.Value++; } - }; - Console.WriteLine("Setup information:"); - Console.WriteLine(" GC IsServer: " + GCSettings.IsServerGC); - Console.WriteLine(" GC LOH Mode: " + GCSettings.LargeObjectHeapCompactionMode); - Console.WriteLine(" GC Latency Mode: " + GCSettings.LatencyMode); - } - - internal static string Time() => DateTime.UtcNow.ToString("HH:mm:ss.ffff"); - protected void OnConnectionFailed(object? sender, ConnectionFailedEventArgs e) - { - Interlocked.Increment(ref privateFailCount); - lock (privateExceptions) + } + lock (backgroundExceptions) { - privateExceptions.Add($"{Time()}: Connection failed ({e.FailureType}): {EndPointCollection.ToString(e.EndPoint)}/{e.ConnectionType}: {e.Exception}"); + backgroundExceptions.Add(args.Exception.ToString()); } - Log($"Connection Failed ({e.ConnectionType},{e.FailureType}): {e.Exception}"); + }; + Console.WriteLine("Setup information:"); + Console.WriteLine(" GC IsServer: " + GCSettings.IsServerGC); + Console.WriteLine(" GC LOH Mode: " + GCSettings.LargeObjectHeapCompactionMode); + Console.WriteLine(" GC Latency Mode: " + GCSettings.LatencyMode); + } + + internal static string Time() => DateTime.UtcNow.ToString("HH:mm:ss.ffff"); + protected void OnConnectionFailed(object? sender, ConnectionFailedEventArgs e) + { + Interlocked.Increment(ref privateFailCount); + lock (privateExceptions) + { + privateExceptions.Add($"{Time()}: Connection failed ({e.FailureType}): {EndPointCollection.ToString(e.EndPoint)}/{e.ConnectionType}: {e.Exception}"); } + Log($"Connection Failed ({e.ConnectionType},{e.FailureType}): {e.Exception}"); + } - protected void OnInternalError(object? sender, InternalErrorEventArgs e) + protected void OnInternalError(object? sender, InternalErrorEventArgs e) + { + Interlocked.Increment(ref privateFailCount); + lock (privateExceptions) { - Interlocked.Increment(ref privateFailCount); - lock (privateExceptions) - { - privateExceptions.Add(Time() + ": Internal error: " + e.Origin + ", " + EndPointCollection.ToString(e.EndPoint) + "/" + e.ConnectionType); - } + privateExceptions.Add(Time() + ": Internal error: " + e.Origin + ", " + EndPointCollection.ToString(e.EndPoint) + "/" + e.ConnectionType); } + } - private int privateFailCount; - private static readonly AsyncLocal sharedFailCount = new AsyncLocal(); - private volatile int expectedFailCount; + private int privateFailCount; + private static readonly AsyncLocal sharedFailCount = new AsyncLocal(); + private volatile int expectedFailCount; - private readonly List privateExceptions = new List(); - private static readonly List backgroundExceptions = new List(); + private readonly List privateExceptions = new List(); + private static readonly List backgroundExceptions = new List(); - public void ClearAmbientFailures() + public void ClearAmbientFailures() + { + Interlocked.Exchange(ref privateFailCount, 0); + lock (sharedFailCount) + { + sharedFailCount.Value = 0; + } + expectedFailCount = 0; + lock (privateExceptions) + { + privateExceptions.Clear(); + } + lock (backgroundExceptions) + { + backgroundExceptions.Clear(); + } + } + + public void SetExpectedAmbientFailureCount(int count) + { + expectedFailCount = count; + } + + public void Teardown() + { + int sharedFails; + lock (sharedFailCount) + { + sharedFails = sharedFailCount.Value; + sharedFailCount.Value = 0; + } + if (expectedFailCount >= 0 && (sharedFails + privateFailCount) != expectedFailCount) { - Interlocked.Exchange(ref privateFailCount, 0); - lock (sharedFailCount) - { - sharedFailCount.Value = 0; - } - expectedFailCount = 0; lock (privateExceptions) { - privateExceptions.Clear(); + foreach (var item in privateExceptions.Take(5)) + { + LogNoTime(item); + } } lock (backgroundExceptions) { - backgroundExceptions.Clear(); + foreach (var item in backgroundExceptions.Take(5)) + { + LogNoTime(item); + } } + Skip.Inconclusive($"There were {privateFailCount} private and {sharedFailCount.Value} ambient exceptions; expected {expectedFailCount}."); } + var pool = SocketManager.Shared?.SchedulerPool; + Log($"Service Counts: (Scheduler) Queue: {pool?.TotalServicedByQueue.ToString()}, Pool: {pool?.TotalServicedByPool.ToString()}, Workers: {pool?.WorkerCount.ToString()}, Available: {pool?.AvailableCount.ToString()}"); + } - public void SetExpectedAmbientFailureCount(int count) + protected static IServer GetServer(IConnectionMultiplexer muxer) + { + EndPoint[] endpoints = muxer.GetEndPoints(); + IServer? result = null; + foreach (var endpoint in endpoints) { - expectedFailCount = count; + var server = muxer.GetServer(endpoint); + if (server.IsReplica || !server.IsConnected) continue; + if (result != null) throw new InvalidOperationException("Requires exactly one primary endpoint (found " + server.EndPoint + " and " + result.EndPoint + ")"); + result = server; } + if (result == null) throw new InvalidOperationException("Requires exactly one primary endpoint (found none)"); + return result; + } - public void Teardown() + protected static IServer GetAnyPrimary(IConnectionMultiplexer muxer) + { + foreach (var endpoint in muxer.GetEndPoints()) { - int sharedFails; - lock (sharedFailCount) - { - sharedFails = sharedFailCount.Value; - sharedFailCount.Value = 0; - } - if (expectedFailCount >= 0 && (sharedFails + privateFailCount) != expectedFailCount) - { - lock (privateExceptions) - { - foreach (var item in privateExceptions.Take(5)) - { - LogNoTime(item); - } - } - lock (backgroundExceptions) - { - foreach (var item in backgroundExceptions.Take(5)) - { - LogNoTime(item); - } - } - Skip.Inconclusive($"There were {privateFailCount} private and {sharedFailCount.Value} ambient exceptions; expected {expectedFailCount}."); - } - var pool = SocketManager.Shared?.SchedulerPool; - Log($"Service Counts: (Scheduler) Queue: {pool?.TotalServicedByQueue.ToString()}, Pool: {pool?.TotalServicedByPool.ToString()}, Workers: {pool?.WorkerCount.ToString()}, Available: {pool?.AvailableCount.ToString()}"); + var server = muxer.GetServer(endpoint); + if (!server.IsReplica) return server; + } + throw new InvalidOperationException("Requires a primary endpoint (found none)"); + } + + internal virtual IInternalConnectionMultiplexer Create( + string? clientName = null, + int? syncTimeout = null, + bool? allowAdmin = null, + int? keepAlive = null, + int? connectTimeout = null, + string? password = null, + string? tieBreaker = null, + TextWriter? log = null, + bool fail = true, + string[]? disabledCommands = null, + string[]? enabledCommands = null, + bool checkConnect = true, + string? failMessage = null, + string? channelPrefix = null, + Proxy? proxy = null, + string? configuration = null, + bool logTransactionData = true, + bool shared = true, + int? defaultDatabase = null, + BacklogPolicy? backlogPolicy = null, + Version? require = null, + [CallerMemberName] string? caller = null) + { + if (Output == null) + { + Assert.True(false, "Failure: Be sure to call the TestBase constuctor like this: BasicOpsTests(ITestOutputHelper output) : base(output) { }"); } - protected static IServer GetServer(IConnectionMultiplexer muxer) + // Share a connection if instructed to and we can - many specifics mean no sharing + if (shared + && _fixture != null && _fixture.IsEnabled + && enabledCommands == null + && disabledCommands == null + && fail + && channelPrefix == null + && proxy == null + && configuration == null + && password == null + && tieBreaker == null + && defaultDatabase == null + && (allowAdmin == null || allowAdmin == true) + && expectedFailCount == 0 + && backlogPolicy == null) { - EndPoint[] endpoints = muxer.GetEndPoints(); - IServer? result = null; - foreach (var endpoint in endpoints) + configuration = GetConfiguration(); + // Only return if we match + if (configuration == _fixture.Configuration) { - var server = muxer.GetServer(endpoint); - if (server.IsReplica || !server.IsConnected) continue; - if (result != null) throw new InvalidOperationException("Requires exactly one primary endpoint (found " + server.EndPoint + " and " + result.EndPoint + ")"); - result = server; + ThrowIfBelowMinVersion(_fixture.Connection, require); + return _fixture.Connection; } - if (result == null) throw new InvalidOperationException("Requires exactly one primary endpoint (found none)"); - return result; } - protected static IServer GetAnyPrimary(IConnectionMultiplexer muxer) + var conn = CreateDefault( + Writer, + configuration ?? GetConfiguration(), + clientName, syncTimeout, allowAdmin, keepAlive, + connectTimeout, password, tieBreaker, log, + fail, disabledCommands, enabledCommands, + checkConnect, failMessage, + channelPrefix, proxy, + logTransactionData, defaultDatabase, + backlogPolicy, + caller); + + ThrowIfBelowMinVersion(conn, require); + + conn.InternalError += OnInternalError; + conn.ConnectionFailed += OnConnectionFailed; + conn.ConnectionRestored += (s, e) => Log($"Connection Restored ({e.ConnectionType},{e.FailureType}): {e.Exception}"); + return conn; + } + + private void ThrowIfBelowMinVersion(IInternalConnectionMultiplexer conn, Version? requiredVersion) + { + if (requiredVersion is null) + { + return; + } + + var serverVersion = conn.GetServer(conn.GetEndPoints()[0]).Version; + if (requiredVersion > serverVersion) { - foreach (var endpoint in muxer.GetEndPoints()) + throw new SkipTestException($"Requires server version {requiredVersion}, but server is only {serverVersion}.") { - var server = muxer.GetServer(endpoint); - if (!server.IsReplica) return server; - } - throw new InvalidOperationException("Requires a primary endpoint (found none)"); + MissingFeatures = $"Server version >= {requiredVersion}." + }; } + } - internal virtual IInternalConnectionMultiplexer Create( - string? clientName = null, - int? syncTimeout = null, - bool? allowAdmin = null, - int? keepAlive = null, - int? connectTimeout = null, - string? password = null, - string? tieBreaker = null, - TextWriter? log = null, - bool fail = true, - string[]? disabledCommands = null, - string[]? enabledCommands = null, - bool checkConnect = true, - string? failMessage = null, - string? channelPrefix = null, - Proxy? proxy = null, - string? configuration = null, - bool logTransactionData = true, - bool shared = true, - int? defaultDatabase = null, - BacklogPolicy? backlogPolicy = null, - Version? require = null, - [CallerMemberName] string? caller = null) + public static ConnectionMultiplexer CreateDefault( + TextWriter? output, + string configuration, + string? clientName = null, + int? syncTimeout = null, + bool? allowAdmin = null, + int? keepAlive = null, + int? connectTimeout = null, + string? password = null, + string? tieBreaker = null, + TextWriter? log = null, + bool fail = true, + string[]? disabledCommands = null, + string[]? enabledCommands = null, + bool checkConnect = true, + string? failMessage = null, + string? channelPrefix = null, + Proxy? proxy = null, + bool logTransactionData = true, + int? defaultDatabase = null, + BacklogPolicy? backlogPolicy = null, + [CallerMemberName] string? caller = null) + { + StringWriter? localLog = null; + if (log == null) + { + log = localLog = new StringWriter(); + } + try { - if (Output == null) + var config = ConfigurationOptions.Parse(configuration); + if (disabledCommands != null && disabledCommands.Length != 0) + { + config.CommandMap = CommandMap.Create(new HashSet(disabledCommands), false); + } + else if (enabledCommands != null && enabledCommands.Length != 0) + { + config.CommandMap = CommandMap.Create(new HashSet(enabledCommands), true); + } + + if (Debugger.IsAttached) { - Assert.True(false, "Failure: Be sure to call the TestBase constuctor like this: BasicOpsTests(ITestOutputHelper output) : base(output) { }"); + syncTimeout = int.MaxValue; } - // Share a connection if instructed to and we can - many specifics mean no sharing - if (shared - && _fixture != null && _fixture.IsEnabled - && enabledCommands == null - && disabledCommands == null - && fail - && channelPrefix == null - && proxy == null - && configuration == null - && password == null - && tieBreaker == null - && defaultDatabase == null - && (allowAdmin == null || allowAdmin == true) - && expectedFailCount == 0 - && backlogPolicy == null) + if (channelPrefix != null) config.ChannelPrefix = channelPrefix; + if (tieBreaker != null) config.TieBreaker = tieBreaker; + if (password != null) config.Password = string.IsNullOrEmpty(password) ? null : password; + if (clientName != null) config.ClientName = clientName; + else if (caller != null) config.ClientName = caller; + if (syncTimeout != null) config.SyncTimeout = syncTimeout.Value; + if (allowAdmin != null) config.AllowAdmin = allowAdmin.Value; + if (keepAlive != null) config.KeepAlive = keepAlive.Value; + if (connectTimeout != null) config.ConnectTimeout = connectTimeout.Value; + if (proxy != null) config.Proxy = proxy.Value; + if (defaultDatabase != null) config.DefaultDatabase = defaultDatabase.Value; + if (backlogPolicy != null) config.BacklogPolicy = backlogPolicy; + var watch = Stopwatch.StartNew(); + var task = ConnectionMultiplexer.ConnectAsync(config, log); + if (!task.Wait(config.ConnectTimeout >= (int.MaxValue / 2) ? int.MaxValue : config.ConnectTimeout * 2)) { - configuration = GetConfiguration(); - if (configuration == _fixture.Configuration) + task.ContinueWith(x => { - // Only return if we match - if (require != null) + try { - Skip.IfBelow(_fixture.Connection, require); + GC.KeepAlive(x.Exception); } - return _fixture.Connection; - } + catch { /* No boom */ } + }, TaskContinuationOptions.OnlyOnFaulted); + throw new TimeoutException("Connect timeout"); } - - var muxer = CreateDefault( - Writer, - configuration ?? GetConfiguration(), - clientName, syncTimeout, allowAdmin, keepAlive, - connectTimeout, password, tieBreaker, log, - fail, disabledCommands, enabledCommands, - checkConnect, failMessage, - channelPrefix, proxy, - logTransactionData, defaultDatabase, - backlogPolicy, - caller); - - if (require != null) + watch.Stop(); + if (output != null) { - Skip.IfBelow(muxer, require); + Log(output, "Connect took: " + watch.ElapsedMilliseconds + "ms"); } - - muxer.InternalError += OnInternalError; - muxer.ConnectionFailed += OnConnectionFailed; - muxer.ConnectionRestored += (s, e) => Log($"Connection Restored ({e.ConnectionType},{e.FailureType}): {e.Exception}"); - return muxer; - } - - public static ConnectionMultiplexer CreateDefault( - TextWriter? output, - string configuration, - string? clientName = null, - int? syncTimeout = null, - bool? allowAdmin = null, - int? keepAlive = null, - int? connectTimeout = null, - string? password = null, - string? tieBreaker = null, - TextWriter? log = null, - bool fail = true, - string[]? disabledCommands = null, - string[]? enabledCommands = null, - bool checkConnect = true, - string? failMessage = null, - string? channelPrefix = null, - Proxy? proxy = null, - bool logTransactionData = true, - int? defaultDatabase = null, - BacklogPolicy? backlogPolicy = null, - [CallerMemberName] string? caller = null) - { - StringWriter? localLog = null; - if (log == null) + var conn = task.Result; + if (checkConnect && !conn.IsConnected) { - log = localLog = new StringWriter(); + // If fail is true, we throw. + Assert.False(fail, failMessage + "Server is not available"); + Skip.Inconclusive(failMessage + "Server is not available"); } - try + if (output != null) { - var config = ConfigurationOptions.Parse(configuration); - if (disabledCommands != null && disabledCommands.Length != 0) - { - config.CommandMap = CommandMap.Create(new HashSet(disabledCommands), false); - } - else if (enabledCommands != null && enabledCommands.Length != 0) - { - config.CommandMap = CommandMap.Create(new HashSet(enabledCommands), true); - } - - if (Debugger.IsAttached) + conn.MessageFaulted += (msg, ex, origin) => { - syncTimeout = int.MaxValue; - } - - if (channelPrefix != null) config.ChannelPrefix = channelPrefix; - if (tieBreaker != null) config.TieBreaker = tieBreaker; - if (password != null) config.Password = string.IsNullOrEmpty(password) ? null : password; - if (clientName != null) config.ClientName = clientName; - else if (caller != null) config.ClientName = caller; - if (syncTimeout != null) config.SyncTimeout = syncTimeout.Value; - if (allowAdmin != null) config.AllowAdmin = allowAdmin.Value; - if (keepAlive != null) config.KeepAlive = keepAlive.Value; - if (connectTimeout != null) config.ConnectTimeout = connectTimeout.Value; - if (proxy != null) config.Proxy = proxy.Value; - if (defaultDatabase != null) config.DefaultDatabase = defaultDatabase.Value; - if (backlogPolicy != null) config.BacklogPolicy = backlogPolicy; - var watch = Stopwatch.StartNew(); - var task = ConnectionMultiplexer.ConnectAsync(config, log); - if (!task.Wait(config.ConnectTimeout >= (int.MaxValue / 2) ? int.MaxValue : config.ConnectTimeout * 2)) - { - task.ContinueWith(x => - { - try - { - GC.KeepAlive(x.Exception); - } - catch { /* No boom */ } - }, TaskContinuationOptions.OnlyOnFaulted); - throw new TimeoutException("Connect timeout"); - } - watch.Stop(); - if (output != null) - { - Log(output, "Connect took: " + watch.ElapsedMilliseconds + "ms"); - } - var muxer = task.Result; - if (checkConnect && !muxer.IsConnected) - { - // If fail is true, we throw. - Assert.False(fail, failMessage + "Server is not available"); - Skip.Inconclusive(failMessage + "Server is not available"); - } - if (output != null) - { - muxer.MessageFaulted += (msg, ex, origin) => - { - output?.WriteLine($"Faulted from '{origin}': '{msg}' - '{(ex == null ? "(null)" : ex.Message)}'"); - if (ex != null && ex.Data.Contains("got")) - { - output?.WriteLine($"Got: '{ex.Data["got"]}'"); - } - }; - muxer.Connecting += (e, t) => output?.WriteLine($"Connecting to {Format.ToString(e)} as {t}"); - if (logTransactionData) + output?.WriteLine($"Faulted from '{origin}': '{msg}' - '{(ex == null ? "(null)" : ex.Message)}'"); + if (ex != null && ex.Data.Contains("got")) { - muxer.TransactionLog += msg => output?.WriteLine("tran: " + msg); + output?.WriteLine($"Got: '{ex.Data["got"]}'"); } - muxer.InfoMessage += msg => output?.WriteLine(msg); - muxer.Resurrecting += (e, t) => output?.WriteLine($"Resurrecting {Format.ToString(e)} as {t}"); - muxer.Closing += complete => output?.WriteLine(complete ? "Closed" : "Closing..."); + }; + conn.Connecting += (e, t) => output?.WriteLine($"Connecting to {Format.ToString(e)} as {t}"); + if (logTransactionData) + { + conn.TransactionLog += msg => output?.WriteLine("tran: " + msg); } - return muxer; - } - catch - { - if (localLog != null) output?.WriteLine(localLog.ToString()); - throw; + conn.InfoMessage += msg => output?.WriteLine(msg); + conn.Resurrecting += (e, t) => output?.WriteLine($"Resurrecting {Format.ToString(e)} as {t}"); + conn.Closing += complete => output?.WriteLine(complete ? "Closed" : "Closing..."); } + return conn; + } + catch + { + if (localLog != null) output?.WriteLine(localLog.ToString()); + throw; } + } - public static string Me([CallerFilePath] string? filePath = null, [CallerMemberName] string? caller = null) => - Environment.Version.ToString() + Path.GetFileNameWithoutExtension(filePath) + "-" + caller; + public static string Me([CallerFilePath] string? filePath = null, [CallerMemberName] string? caller = null) => + Environment.Version.ToString() + Path.GetFileNameWithoutExtension(filePath) + "-" + caller; - protected static TimeSpan RunConcurrent(Action work, int threads, int timeout = 10000, [CallerMemberName] string? caller = null) + protected static TimeSpan RunConcurrent(Action work, int threads, int timeout = 10000, [CallerMemberName] string? caller = null) + { + if (work == null) throw new ArgumentNullException(nameof(work)); + if (threads < 1) throw new ArgumentOutOfRangeException(nameof(threads)); + if (string.IsNullOrWhiteSpace(caller)) caller = Me(); + Stopwatch? watch = null; + ManualResetEvent allDone = new ManualResetEvent(false); + object token = new object(); + int active = 0; + void callback() { - if (work == null) throw new ArgumentNullException(nameof(work)); - if (threads < 1) throw new ArgumentOutOfRangeException(nameof(threads)); - if (string.IsNullOrWhiteSpace(caller)) caller = Me(); - Stopwatch? watch = null; - ManualResetEvent allDone = new ManualResetEvent(false); - object token = new object(); - int active = 0; - void callback() + lock (token) { - lock (token) + int nowActive = Interlocked.Increment(ref active); + if (nowActive == threads) { - int nowActive = Interlocked.Increment(ref active); - if (nowActive == threads) - { - watch = Stopwatch.StartNew(); - Monitor.PulseAll(token); - } - else - { - Monitor.Wait(token); - } + watch = Stopwatch.StartNew(); + Monitor.PulseAll(token); } - work(); - if (Interlocked.Decrement(ref active) == 0) + else { - watch?.Stop(); - allDone.Set(); + Monitor.Wait(token); } } - - var threadArr = new Thread[threads]; - for (int i = 0; i < threads; i++) + work(); + if (Interlocked.Decrement(ref active) == 0) { - var thd = new Thread(callback) - { - Name = caller - }; - threadArr[i] = thd; - thd.Start(); + watch?.Stop(); + allDone.Set(); } - if (!allDone.WaitOne(timeout)) + } + + var threadArr = new Thread[threads]; + for (int i = 0; i < threads; i++) + { + var thd = new Thread(callback) { - for (int i = 0; i < threads; i++) - { - var thd = threadArr[i]; + Name = caller + }; + threadArr[i] = thd; + thd.Start(); + } + if (!allDone.WaitOne(timeout)) + { + for (int i = 0; i < threads; i++) + { + var thd = threadArr[i]; #if !NET6_0_OR_GREATER - if (thd.IsAlive) thd.Abort(); + if (thd.IsAlive) thd.Abort(); #endif - } - throw new TimeoutException(); } - - return watch?.Elapsed ?? TimeSpan.Zero; + throw new TimeoutException(); } - private static readonly TimeSpan DefaultWaitPerLoop = TimeSpan.FromMilliseconds(50); - protected static async Task UntilConditionAsync(TimeSpan maxWaitTime, Func predicate, TimeSpan? waitPerLoop = null) + return watch?.Elapsed ?? TimeSpan.Zero; + } + + private static readonly TimeSpan DefaultWaitPerLoop = TimeSpan.FromMilliseconds(50); + protected static async Task UntilConditionAsync(TimeSpan maxWaitTime, Func predicate, TimeSpan? waitPerLoop = null) + { + TimeSpan spent = TimeSpan.Zero; + while (spent < maxWaitTime && !predicate()) { - TimeSpan spent = TimeSpan.Zero; - while (spent < maxWaitTime && !predicate()) - { - var wait = waitPerLoop ?? DefaultWaitPerLoop; - await Task.Delay(wait).ForAwait(); - spent += wait; - } + var wait = waitPerLoop ?? DefaultWaitPerLoop; + await Task.Delay(wait).ForAwait(); + spent += wait; } } } diff --git a/tests/StackExchange.Redis.Tests/TestExtensions.cs b/tests/StackExchange.Redis.Tests/TestExtensions.cs index b4c9707fd..504f260a4 100644 --- a/tests/StackExchange.Redis.Tests/TestExtensions.cs +++ b/tests/StackExchange.Redis.Tests/TestExtensions.cs @@ -1,15 +1,13 @@ -using System; -using StackExchange.Redis.Profiling; +using StackExchange.Redis.Profiling; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public static class TestExtensions { - public static class TestExtensions + public static ProfilingSession AddProfiler(this IConnectionMultiplexer mutex) { - public static ProfilingSession AddProfiler(this IConnectionMultiplexer mutex) - { - var session = new ProfilingSession(); - mutex.RegisterProfiler(() => session); - return session; - } + var session = new ProfilingSession(); + mutex.RegisterProfiler(() => session); + return session; } } diff --git a/tests/StackExchange.Redis.Tests/TestInfoReplicationChecks.cs b/tests/StackExchange.Redis.Tests/TestInfoReplicationChecks.cs index 169246894..10873e7ef 100644 --- a/tests/StackExchange.Redis.Tests/TestInfoReplicationChecks.cs +++ b/tests/StackExchange.Redis.Tests/TestInfoReplicationChecks.cs @@ -2,28 +2,26 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class TestInfoReplicationChecks : TestBase { - public class TestInfoReplicationChecks : TestBase + protected override string GetConfiguration() => base.GetConfiguration() + ",configCheckSeconds=2"; + public TestInfoReplicationChecks(ITestOutputHelper output) : base (output) { } + + [Fact] + public async Task Exec() { - protected override string GetConfiguration() => base.GetConfiguration() + ",configCheckSeconds=2"; - public TestInfoReplicationChecks(ITestOutputHelper output) : base (output) { } + Skip.Inconclusive("need to think about CompletedSynchronously"); - [Fact] - public async Task Exec() - { - Skip.Inconclusive("need to think about CompletedSynchronously"); + using var conn = Create(); - using(var conn = Create()) - { - var parsed = ConfigurationOptions.Parse(conn.Configuration); - Assert.Equal(2, parsed.ConfigCheckSeconds); - var before = conn.GetCounters(); - await Task.Delay(7000).ForAwait(); - var after = conn.GetCounters(); - int done = (int)(after.Interactive.CompletedSynchronously - before.Interactive.CompletedSynchronously); - Assert.True(done >= 2, $"expected >=2, got {done}"); - } - } + var parsed = ConfigurationOptions.Parse(conn.Configuration); + Assert.Equal(2, parsed.ConfigCheckSeconds); + var before = conn.GetCounters(); + await Task.Delay(7000).ForAwait(); + var after = conn.GetCounters(); + int done = (int)(after.Interactive.CompletedSynchronously - before.Interactive.CompletedSynchronously); + Assert.True(done >= 2, $"expected >=2, got {done}"); } } diff --git a/tests/StackExchange.Redis.Tests/TransactionWrapperTests.cs b/tests/StackExchange.Redis.Tests/TransactionWrapperTests.cs index 0b76e309a..9fccd26d8 100644 --- a/tests/StackExchange.Redis.Tests/TransactionWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/TransactionWrapperTests.cs @@ -4,130 +4,129 @@ using StackExchange.Redis.KeyspaceIsolation; using Xunit; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(nameof(MoqDependentCollection))] +public sealed class TransactionWrapperTests { - [Collection(nameof(MoqDependentCollection))] - public sealed class TransactionWrapperTests - { - private readonly Mock mock; - private readonly TransactionWrapper wrapper; - - public TransactionWrapperTests() - { - mock = new Mock(); - wrapper = new TransactionWrapper(mock.Object, Encoding.UTF8.GetBytes("prefix:")); - } - - [Fact] - public void AddCondition_HashEqual() - { - wrapper.AddCondition(Condition.HashEqual("key", "field", "value")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field == value" == value.ToString()))); - } - - [Fact] - public void AddCondition_HashNotEqual() - { - wrapper.AddCondition(Condition.HashNotEqual("key", "field", "value")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field != value" == value.ToString()))); - } - - [Fact] - public void AddCondition_HashExists() - { - wrapper.AddCondition(Condition.HashExists("key", "field")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field exists" == value.ToString()))); - } - - [Fact] - public void AddCondition_HashNotExists() - { - wrapper.AddCondition(Condition.HashNotExists("key", "field")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field does not exists" == value.ToString()))); - } - - [Fact] - public void AddCondition_KeyExists() - { - wrapper.AddCondition(Condition.KeyExists("key")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key exists" == value.ToString()))); - } - - [Fact] - public void AddCondition_KeyNotExists() - { - wrapper.AddCondition(Condition.KeyNotExists("key")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key does not exists" == value.ToString()))); - } - - [Fact] - public void AddCondition_StringEqual() - { - wrapper.AddCondition(Condition.StringEqual("key", "value")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key == value" == value.ToString()))); - } - - [Fact] - public void AddCondition_StringNotEqual() - { - wrapper.AddCondition(Condition.StringNotEqual("key", "value")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key != value" == value.ToString()))); - } - - [Fact] - public void AddCondition_SortedSetEqual() - { - wrapper.AddCondition(Condition.SortedSetEqual("key", "member", "score")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key SortedSet > member == score" == value.ToString()))); - } - - [Fact] - public void AddCondition_SortedSetNotEqual() - { - wrapper.AddCondition(Condition.SortedSetNotEqual("key", "member", "score")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key SortedSet > member != score" == value.ToString()))); - } - - [Fact] - public void AddCondition_SortedSetScoreExists() - { - wrapper.AddCondition(Condition.SortedSetScoreExists("key", "score")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key not contains 0 members with score: score" == value.ToString()))); - } - - [Fact] - public void AddCondition_SortedSetScoreNotExists() - { - wrapper.AddCondition(Condition.SortedSetScoreNotExists("key", "score")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key contains 0 members with score: score" == value.ToString()))); - } - - [Fact] - public void AddCondition_SortedSetScoreCountExists() - { - wrapper.AddCondition(Condition.SortedSetScoreExists("key", "score", "count")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key contains count members with score: score" == value.ToString()))); - } - - [Fact] - public void AddCondition_SortedSetScoreCountNotExists() - { - wrapper.AddCondition(Condition.SortedSetScoreNotExists("key", "score", "count")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key not contains count members with score: score" == value.ToString()))); - } - - [Fact] - public async Task ExecuteAsync() - { - await wrapper.ExecuteAsync(CommandFlags.None); - mock.Verify(_ => _.ExecuteAsync(CommandFlags.None), Times.Once()); - } - - [Fact] - public void Execute() - { - wrapper.Execute(CommandFlags.None); - mock.Verify(_ => _.Execute(CommandFlags.None), Times.Once()); - } + private readonly Mock mock; + private readonly TransactionWrapper wrapper; + + public TransactionWrapperTests() + { + mock = new Mock(); + wrapper = new TransactionWrapper(mock.Object, Encoding.UTF8.GetBytes("prefix:")); + } + + [Fact] + public void AddCondition_HashEqual() + { + wrapper.AddCondition(Condition.HashEqual("key", "field", "value")); + mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field == value" == value.ToString()))); + } + + [Fact] + public void AddCondition_HashNotEqual() + { + wrapper.AddCondition(Condition.HashNotEqual("key", "field", "value")); + mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field != value" == value.ToString()))); + } + + [Fact] + public void AddCondition_HashExists() + { + wrapper.AddCondition(Condition.HashExists("key", "field")); + mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field exists" == value.ToString()))); + } + + [Fact] + public void AddCondition_HashNotExists() + { + wrapper.AddCondition(Condition.HashNotExists("key", "field")); + mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field does not exists" == value.ToString()))); + } + + [Fact] + public void AddCondition_KeyExists() + { + wrapper.AddCondition(Condition.KeyExists("key")); + mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key exists" == value.ToString()))); + } + + [Fact] + public void AddCondition_KeyNotExists() + { + wrapper.AddCondition(Condition.KeyNotExists("key")); + mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key does not exists" == value.ToString()))); + } + + [Fact] + public void AddCondition_StringEqual() + { + wrapper.AddCondition(Condition.StringEqual("key", "value")); + mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key == value" == value.ToString()))); + } + + [Fact] + public void AddCondition_StringNotEqual() + { + wrapper.AddCondition(Condition.StringNotEqual("key", "value")); + mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key != value" == value.ToString()))); + } + + [Fact] + public void AddCondition_SortedSetEqual() + { + wrapper.AddCondition(Condition.SortedSetEqual("key", "member", "score")); + mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key SortedSet > member == score" == value.ToString()))); + } + + [Fact] + public void AddCondition_SortedSetNotEqual() + { + wrapper.AddCondition(Condition.SortedSetNotEqual("key", "member", "score")); + mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key SortedSet > member != score" == value.ToString()))); + } + + [Fact] + public void AddCondition_SortedSetScoreExists() + { + wrapper.AddCondition(Condition.SortedSetScoreExists("key", "score")); + mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key not contains 0 members with score: score" == value.ToString()))); + } + + [Fact] + public void AddCondition_SortedSetScoreNotExists() + { + wrapper.AddCondition(Condition.SortedSetScoreNotExists("key", "score")); + mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key contains 0 members with score: score" == value.ToString()))); + } + + [Fact] + public void AddCondition_SortedSetScoreCountExists() + { + wrapper.AddCondition(Condition.SortedSetScoreExists("key", "score", "count")); + mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key contains count members with score: score" == value.ToString()))); + } + + [Fact] + public void AddCondition_SortedSetScoreCountNotExists() + { + wrapper.AddCondition(Condition.SortedSetScoreNotExists("key", "score", "count")); + mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key not contains count members with score: score" == value.ToString()))); + } + + [Fact] + public async Task ExecuteAsync() + { + await wrapper.ExecuteAsync(CommandFlags.None); + mock.Verify(_ => _.ExecuteAsync(CommandFlags.None), Times.Once()); + } + + [Fact] + public void Execute() + { + wrapper.Execute(CommandFlags.None); + mock.Verify(_ => _.Execute(CommandFlags.None), Times.Once()); } } diff --git a/tests/StackExchange.Redis.Tests/Transactions.cs b/tests/StackExchange.Redis.Tests/Transactions.cs index f370a46c8..1b1279136 100644 --- a/tests/StackExchange.Redis.Tests/Transactions.cs +++ b/tests/StackExchange.Redis.Tests/Transactions.cs @@ -3,1330 +3,1303 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class Transactions : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class Transactions : TestBase + public Transactions(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public void BasicEmptyTran() { - public Transactions(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + using var conn = Create(); - [Fact] - public void BasicEmptyTran() - { - using (var muxer = Create()) - { - RedisKey key = Me(); - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - Assert.False(db.KeyExists(key)); + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); - var tran = db.CreateTransaction(); + var tran = db.CreateTransaction(); - var result = tran.Execute(); - Assert.True(result); - } - } + var result = tran.Execute(); + Assert.True(result); + } - [Fact] - public void NestedTransactionThrows() + [Fact] + public void NestedTransactionThrows() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var tran = db.CreateTransaction(); + var redisTransaction = Assert.IsType(tran); + Assert.Throws(() => redisTransaction.CreateTransaction(null)); + } + + [Theory] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(true, false, false)] + [InlineData(true, true, true)] + public async Task BasicTranWithExistsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) + { + using var conn = Create(disabledCommands: new[] { "info", "config" }); + + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + if (keyExists) db.StringSet(key2, "any value", flags: CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); + Assert.Equal(keyExists, db.KeyExists(key2)); + + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(demandKeyExists ? Condition.KeyExists(key2) : Condition.KeyNotExists(key2)); + var incr = tran.StringIncrementAsync(key); + var exec = tran.ExecuteAsync(); + var get = db.StringGet(key); + + Assert.Equal(expectTranResult, await exec); + if (demandKeyExists == keyExists) { - using (var muxer = Create()) - { - var db = muxer.GetDatabase(); - var tran = db.CreateTransaction(); - var redisTransaction = Assert.IsType(tran); - Assert.Throws(() => redisTransaction.CreateTransaction(null)); - } + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.Equal(1, await incr); // eq: incr + Assert.Equal(1, (long)get); // eq: get } - - [Theory] - [InlineData(false, false, true)] - [InlineData(false, true, false)] - [InlineData(true, false, false)] - [InlineData(true, true, true)] - public async Task BasicTranWithExistsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) + else { - using (var muxer = Create(disabledCommands: new[] { "info", "config" })) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - if (keyExists) db.StringSet(key2, "any value", flags: CommandFlags.FireAndForget); - Assert.False(db.KeyExists(key)); - Assert.Equal(keyExists, db.KeyExists(key2)); - - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(demandKeyExists ? Condition.KeyExists(key2) : Condition.KeyNotExists(key2)); - var incr = tran.StringIncrementAsync(key); - var exec = tran.ExecuteAsync(); - var get = db.StringGet(key); - - Assert.Equal(expectTranResult, await exec); - if (demandKeyExists == keyExists) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.Equal(1, await incr); // eq: incr - Assert.Equal(1, (long)get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr - Assert.Equal(0, (long)get); // neq: get - } - } + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr + Assert.Equal(0, (long)get); // neq: get } + } + + [Theory] + [InlineData("same", "same", true, true)] + [InlineData("x", "y", true, false)] + [InlineData("x", null, true, false)] + [InlineData(null, "y", true, false)] + [InlineData(null, null, true, true)] + + [InlineData("same", "same", false, false)] + [InlineData("x", "y", false, true)] + [InlineData("x", null, false, true)] + [InlineData(null, "y", false, true)] + [InlineData(null, null, false, false)] + public async Task BasicTranWithEqualsCondition(string expected, string value, bool expectEqual, bool expectTranResult) + { + using var conn = Create(); + + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + + if (value != null) db.StringSet(key2, value, flags: CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); + Assert.Equal(value, db.StringGet(key2)); - [Theory] - [InlineData("same", "same", true, true)] - [InlineData("x", "y", true, false)] - [InlineData("x", null, true, false)] - [InlineData(null, "y", true, false)] - [InlineData(null, null, true, true)] - - [InlineData("same", "same", false, false)] - [InlineData("x", "y", false, true)] - [InlineData("x", null, false, true)] - [InlineData(null, "y", false, true)] - [InlineData(null, null, false, false)] - public async Task BasicTranWithEqualsCondition(string expected, string value, bool expectEqual, bool expectTranResult) + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(expectEqual ? Condition.StringEqual(key2, expected) : Condition.StringNotEqual(key2, expected)); + var incr = tran.StringIncrementAsync(key); + var exec = tran.ExecuteAsync(); + var get = db.StringGet(key); + + Assert.Equal(expectTranResult, await exec); + if (expectEqual == (value == expected)) { - using (var muxer = Create()) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - - if (value != null) db.StringSet(key2, value, flags: CommandFlags.FireAndForget); - Assert.False(db.KeyExists(key)); - Assert.Equal(value, db.StringGet(key2)); - - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(expectEqual ? Condition.StringEqual(key2, expected) : Condition.StringNotEqual(key2, expected)); - var incr = tran.StringIncrementAsync(key); - var exec = tran.ExecuteAsync(); - var get = db.StringGet(key); - - Assert.Equal(expectTranResult, await exec); - if (expectEqual == (value == expected)) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.Equal(1, await incr); // eq: incr - Assert.Equal(1, (long)get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr - Assert.Equal(0, (long)get); // neq: get - } - } + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.Equal(1, await incr); // eq: incr + Assert.Equal(1, (long)get); // eq: get } - - [Theory] - [InlineData(false, false, true)] - [InlineData(false, true, false)] - [InlineData(true, false, false)] - [InlineData(true, true, true)] - public async Task BasicTranWithHashExistsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) + else { - using (var muxer = Create(disabledCommands: new[] { "info", "config" })) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - RedisValue hashField = "field"; - if (keyExists) db.HashSet(key2, hashField, "any value", flags: CommandFlags.FireAndForget); - Assert.False(db.KeyExists(key)); - Assert.Equal(keyExists, db.HashExists(key2, hashField)); - - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(demandKeyExists ? Condition.HashExists(key2, hashField) : Condition.HashNotExists(key2, hashField)); - var incr = tran.StringIncrementAsync(key); - var exec = tran.ExecuteAsync(); - var get = db.StringGet(key); - - Assert.Equal(expectTranResult, await exec); - if (demandKeyExists == keyExists) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.Equal(1, await incr); // eq: incr - Assert.Equal(1, (long)get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr - Assert.Equal(0, (long)get); // neq: get - } - } + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr + Assert.Equal(0, (long)get); // neq: get } + } - [Theory] - [InlineData("same", "same", true, true)] - [InlineData("x", "y", true, false)] - [InlineData("x", null, true, false)] - [InlineData(null, "y", true, false)] - [InlineData(null, null, true, true)] - - [InlineData("same", "same", false, false)] - [InlineData("x", "y", false, true)] - [InlineData("x", null, false, true)] - [InlineData(null, "y", false, true)] - [InlineData(null, null, false, false)] - public async Task BasicTranWithHashEqualsCondition(string expected, string value, bool expectEqual, bool expectedTranResult) + [Theory] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(true, false, false)] + [InlineData(true, true, true)] + public async Task BasicTranWithHashExistsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) + { + using var conn = Create(disabledCommands: new[] { "info", "config" }); + + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + RedisValue hashField = "field"; + if (keyExists) db.HashSet(key2, hashField, "any value", flags: CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); + Assert.Equal(keyExists, db.HashExists(key2, hashField)); + + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(demandKeyExists ? Condition.HashExists(key2, hashField) : Condition.HashNotExists(key2, hashField)); + var incr = tran.StringIncrementAsync(key); + var exec = tran.ExecuteAsync(); + var get = db.StringGet(key); + + Assert.Equal(expectTranResult, await exec); + if (demandKeyExists == keyExists) { - using (var muxer = Create()) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - - RedisValue hashField = "field"; - if (value != null) db.HashSet(key2, hashField, value, flags: CommandFlags.FireAndForget); - Assert.False(db.KeyExists(key)); - Assert.Equal(value, db.HashGet(key2, hashField)); - - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(expectEqual ? Condition.HashEqual(key2, hashField, expected) : Condition.HashNotEqual(key2, hashField, expected)); - var incr = tran.StringIncrementAsync(key); - var exec = tran.ExecuteAsync(); - var get = db.StringGet(key); - - Assert.Equal(expectedTranResult, await exec); - if (expectEqual == (value == expected)) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.Equal(1, await incr); // eq: incr - Assert.Equal(1, (long)get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr - Assert.Equal(0, (long)get); // neq: get - } - } + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.Equal(1, await incr); // eq: incr + Assert.Equal(1, (long)get); // eq: get } - - private static TaskStatus SafeStatus(Task task) + else { - if (task.Status == TaskStatus.WaitingForActivation) - { - try - { - if (!task.Wait(1000)) throw new TimeoutException("timeout waiting for task to complete"); - } - catch (AggregateException ex) - when (ex.InnerException is TaskCanceledException - || (ex.InnerExceptions.Count == 1 && ex.InnerException is TaskCanceledException)) - { - return TaskStatus.Canceled; - } - catch (TaskCanceledException) - { - return TaskStatus.Canceled; - } - } - return task.Status; + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr + Assert.Equal(0, (long)get); // neq: get } + } - [Theory] - [InlineData(false, false, true)] - [InlineData(false, true, false)] - [InlineData(true, false, false)] - [InlineData(true, true, true)] - public async Task BasicTranWithListExistsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) + [Theory] + [InlineData("same", "same", true, true)] + [InlineData("x", "y", true, false)] + [InlineData("x", null, true, false)] + [InlineData(null, "y", true, false)] + [InlineData(null, null, true, true)] + + [InlineData("same", "same", false, false)] + [InlineData("x", "y", false, true)] + [InlineData("x", null, false, true)] + [InlineData(null, "y", false, true)] + [InlineData(null, null, false, false)] + public async Task BasicTranWithHashEqualsCondition(string expected, string value, bool expectEqual, bool expectedTranResult) + { + using var conn = Create(); + + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + + RedisValue hashField = "field"; + if (value != null) db.HashSet(key2, hashField, value, flags: CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); + Assert.Equal(value, db.HashGet(key2, hashField)); + + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(expectEqual ? Condition.HashEqual(key2, hashField, expected) : Condition.HashNotEqual(key2, hashField, expected)); + var incr = tran.StringIncrementAsync(key); + var exec = tran.ExecuteAsync(); + var get = db.StringGet(key); + + Assert.Equal(expectedTranResult, await exec); + if (expectEqual == (value == expected)) { - using (var muxer = Create(disabledCommands: new[] { "info", "config" })) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - if (keyExists) db.ListRightPush(key2, "any value", flags: CommandFlags.FireAndForget); - Assert.False(db.KeyExists(key)); - Assert.Equal(keyExists, db.KeyExists(key2)); - - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(demandKeyExists ? Condition.ListIndexExists(key2, 0) : Condition.ListIndexNotExists(key2, 0)); - var push = tran.ListRightPushAsync(key, "any value"); - var exec = tran.ExecuteAsync(); - var get = db.ListGetByIndex(key, 0); - - Assert.Equal(expectTranResult, await exec); - if (demandKeyExists == keyExists) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.Equal(1, await push); // eq: push - Assert.Equal("any value", get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push - Assert.Null((string?)get); // neq: get - } - } + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.Equal(1, await incr); // eq: incr + Assert.Equal(1, (long)get); // eq: get } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr + Assert.Equal(0, (long)get); // neq: get + } + } - [Theory] - [InlineData("same", "same", true, true)] - [InlineData("x", "y", true, false)] - [InlineData("x", null, true, false)] - [InlineData(null, "y", true, false)] - [InlineData(null, null, true, true)] - - [InlineData("same", "same", false, false)] - [InlineData("x", "y", false, true)] - [InlineData("x", null, false, true)] - [InlineData(null, "y", false, true)] - [InlineData(null, null, false, false)] - public async Task BasicTranWithListEqualsCondition(string expected, string value, bool expectEqual, bool expectTranResult) + private static TaskStatus SafeStatus(Task task) + { + if (task.Status == TaskStatus.WaitingForActivation) { - using (var muxer = Create()) + try { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - - if (value != null) db.ListRightPush(key2, value, flags: CommandFlags.FireAndForget); - Assert.False(db.KeyExists(key)); - Assert.Equal(value, db.ListGetByIndex(key2, 0)); - - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(expectEqual ? Condition.ListIndexEqual(key2, 0, expected) : Condition.ListIndexNotEqual(key2, 0, expected)); - var push = tran.ListRightPushAsync(key, "any value"); - var exec = tran.ExecuteAsync(); - var get = db.ListGetByIndex(key, 0); - - Assert.Equal(expectTranResult, await exec); - if (expectEqual == (value == expected)) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.Equal(1, await push); // eq: push - Assert.Equal("any value", get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push - Assert.Null((string?)get); // neq: get - } + if (!task.Wait(1000)) throw new TimeoutException("timeout waiting for task to complete"); + } + catch (AggregateException ex) + when (ex.InnerException is TaskCanceledException + || (ex.InnerExceptions.Count == 1 && ex.InnerException is TaskCanceledException)) + { + return TaskStatus.Canceled; + } + catch (TaskCanceledException) + { + return TaskStatus.Canceled; } } + return task.Status; + } - public enum ComparisonType + [Theory] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(true, false, false)] + [InlineData(true, true, true)] + public async Task BasicTranWithListExistsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) + { + using var conn = Create(disabledCommands: new[] { "info", "config" }); + + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + if (keyExists) db.ListRightPush(key2, "any value", flags: CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); + Assert.Equal(keyExists, db.KeyExists(key2)); + + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(demandKeyExists ? Condition.ListIndexExists(key2, 0) : Condition.ListIndexNotExists(key2, 0)); + var push = tran.ListRightPushAsync(key, "any value"); + var exec = tran.ExecuteAsync(); + var get = db.ListGetByIndex(key, 0); + + Assert.Equal(expectTranResult, await exec); + if (demandKeyExists == keyExists) { - Equal, - LessThan, - GreaterThan + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.Equal(1, await push); // eq: push + Assert.Equal("any value", get); // eq: get } - - [Theory] - [InlineData("five", ComparisonType.Equal, 5L, false)] - [InlineData("four", ComparisonType.Equal, 4L, true)] - [InlineData("three", ComparisonType.Equal, 3L, false)] - [InlineData("", ComparisonType.Equal, 2L, false)] - [InlineData("", ComparisonType.Equal, 0L, true)] - [InlineData(null, ComparisonType.Equal, 1L, false)] - [InlineData(null, ComparisonType.Equal, 0L, true)] - - [InlineData("five", ComparisonType.LessThan, 5L, true)] - [InlineData("four", ComparisonType.LessThan, 4L, false)] - [InlineData("three", ComparisonType.LessThan, 3L, false)] - [InlineData("", ComparisonType.LessThan, 2L, true)] - [InlineData("", ComparisonType.LessThan, 0L, false)] - [InlineData(null, ComparisonType.LessThan, 1L, true)] - [InlineData(null, ComparisonType.LessThan, 0L, false)] - - [InlineData("five", ComparisonType.GreaterThan, 5L, false)] - [InlineData("four", ComparisonType.GreaterThan, 4L, false)] - [InlineData("three", ComparisonType.GreaterThan, 3L, true)] - [InlineData("", ComparisonType.GreaterThan, 2L, false)] - [InlineData("", ComparisonType.GreaterThan, 0L, false)] - [InlineData(null, ComparisonType.GreaterThan, 1L, false)] - [InlineData(null, ComparisonType.GreaterThan, 0L, false)] - public async Task BasicTranWithStringLengthCondition(string value, ComparisonType type, long length, bool expectTranResult) + else { - using (var muxer = Create()) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - - var expectSuccess = false; - Condition? condition = null; - var valueLength = value?.Length ?? 0; - switch (type) - { - case ComparisonType.Equal: - expectSuccess = valueLength == length; - condition = Condition.StringLengthEqual(key2, length); - Assert.Contains("String length == " + length, condition.ToString()); - break; - case ComparisonType.GreaterThan: - expectSuccess = valueLength > length; - condition = Condition.StringLengthGreaterThan(key2, length); - Assert.Contains("String length > " + length, condition.ToString()); - break; - case ComparisonType.LessThan: - expectSuccess = valueLength < length; - condition = Condition.StringLengthLessThan(key2, length); - Assert.Contains("String length < " + length, condition.ToString()); - break; - default: - throw new ArgumentOutOfRangeException(nameof(type)); - } + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push + Assert.Null((string?)get); // neq: get + } + } - if (value != null) db.StringSet(key2, value, flags: CommandFlags.FireAndForget); - Assert.False(db.KeyExists(key)); - Assert.Equal(value, db.StringGet(key2)); + [Theory] + [InlineData("same", "same", true, true)] + [InlineData("x", "y", true, false)] + [InlineData("x", null, true, false)] + [InlineData(null, "y", true, false)] + [InlineData(null, null, true, true)] + + [InlineData("same", "same", false, false)] + [InlineData("x", "y", false, true)] + [InlineData("x", null, false, true)] + [InlineData(null, "y", false, true)] + [InlineData(null, null, false, false)] + public async Task BasicTranWithListEqualsCondition(string expected, string value, bool expectEqual, bool expectTranResult) + { + using var conn = Create(); - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(condition); - var push = tran.StringSetAsync(key, "any value"); - var exec = tran.ExecuteAsync(); - var get = db.StringLength(key); + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); - Assert.Equal(expectTranResult, await exec); + if (value != null) db.ListRightPush(key2, value, flags: CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); + Assert.Equal(value, db.ListGetByIndex(key2, 0)); - if (expectSuccess) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.True(await push); // eq: push - Assert.Equal("any value".Length, get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push - Assert.Equal(0, get); // neq: get - } - } - } + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(expectEqual ? Condition.ListIndexEqual(key2, 0, expected) : Condition.ListIndexNotEqual(key2, 0, expected)); + var push = tran.ListRightPushAsync(key, "any value"); + var exec = tran.ExecuteAsync(); + var get = db.ListGetByIndex(key, 0); - [Theory] - [InlineData("five", ComparisonType.Equal, 5L, false)] - [InlineData("four", ComparisonType.Equal, 4L, true)] - [InlineData("three", ComparisonType.Equal, 3L, false)] - [InlineData("", ComparisonType.Equal, 2L, false)] - [InlineData("", ComparisonType.Equal, 0L, true)] - - [InlineData("five", ComparisonType.LessThan, 5L, true)] - [InlineData("four", ComparisonType.LessThan, 4L, false)] - [InlineData("three", ComparisonType.LessThan, 3L, false)] - [InlineData("", ComparisonType.LessThan, 2L, true)] - [InlineData("", ComparisonType.LessThan, 0L, false)] - - [InlineData("five", ComparisonType.GreaterThan, 5L, false)] - [InlineData("four", ComparisonType.GreaterThan, 4L, false)] - [InlineData("three", ComparisonType.GreaterThan, 3L, true)] - [InlineData("", ComparisonType.GreaterThan, 2L, false)] - [InlineData("", ComparisonType.GreaterThan, 0L, false)] - public async Task BasicTranWithHashLengthCondition(string value, ComparisonType type, long length, bool expectTranResult) + Assert.Equal(expectTranResult, await exec); + if (expectEqual == (value == expected)) { - using (var muxer = Create()) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - - var expectSuccess = false; - Condition? condition = null; - var valueLength = value?.Length ?? 0; - switch (type) - { - case ComparisonType.Equal: - expectSuccess = valueLength == length; - condition = Condition.HashLengthEqual(key2, length); - break; - case ComparisonType.GreaterThan: - expectSuccess = valueLength > length; - condition = Condition.HashLengthGreaterThan(key2, length); - break; - case ComparisonType.LessThan: - expectSuccess = valueLength < length; - condition = Condition.HashLengthLessThan(key2, length); - break; - default: - throw new ArgumentOutOfRangeException(nameof(type)); - } + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.Equal(1, await push); // eq: push + Assert.Equal("any value", get); // eq: get + } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push + Assert.Null((string?)get); // neq: get + } + } - for (var i = 0; i < valueLength; i++) - { - db.HashSet(key2, i, value![i].ToString(), flags: CommandFlags.FireAndForget); - } - Assert.False(db.KeyExists(key)); - Assert.Equal(valueLength, db.HashLength(key2)); + public enum ComparisonType + { + Equal, + LessThan, + GreaterThan + } - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(condition); - var push = tran.StringSetAsync(key, "any value"); - var exec = tran.ExecuteAsync(); - var get = db.StringLength(key); + [Theory] + [InlineData("five", ComparisonType.Equal, 5L, false)] + [InlineData("four", ComparisonType.Equal, 4L, true)] + [InlineData("three", ComparisonType.Equal, 3L, false)] + [InlineData("", ComparisonType.Equal, 2L, false)] + [InlineData("", ComparisonType.Equal, 0L, true)] + [InlineData(null, ComparisonType.Equal, 1L, false)] + [InlineData(null, ComparisonType.Equal, 0L, true)] + + [InlineData("five", ComparisonType.LessThan, 5L, true)] + [InlineData("four", ComparisonType.LessThan, 4L, false)] + [InlineData("three", ComparisonType.LessThan, 3L, false)] + [InlineData("", ComparisonType.LessThan, 2L, true)] + [InlineData("", ComparisonType.LessThan, 0L, false)] + [InlineData(null, ComparisonType.LessThan, 1L, true)] + [InlineData(null, ComparisonType.LessThan, 0L, false)] + + [InlineData("five", ComparisonType.GreaterThan, 5L, false)] + [InlineData("four", ComparisonType.GreaterThan, 4L, false)] + [InlineData("three", ComparisonType.GreaterThan, 3L, true)] + [InlineData("", ComparisonType.GreaterThan, 2L, false)] + [InlineData("", ComparisonType.GreaterThan, 0L, false)] + [InlineData(null, ComparisonType.GreaterThan, 1L, false)] + [InlineData(null, ComparisonType.GreaterThan, 0L, false)] + public async Task BasicTranWithStringLengthCondition(string value, ComparisonType type, long length, bool expectTranResult) + { + using var conn = Create(); - Assert.Equal(expectTranResult, await exec); + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); - if (expectSuccess) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.True(await push); // eq: push - Assert.Equal("any value".Length, get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push - Assert.Equal(0, get); // neq: get - } - } + var expectSuccess = false; + Condition? condition = null; + var valueLength = value?.Length ?? 0; + switch (type) + { + case ComparisonType.Equal: + expectSuccess = valueLength == length; + condition = Condition.StringLengthEqual(key2, length); + Assert.Contains("String length == " + length, condition.ToString()); + break; + case ComparisonType.GreaterThan: + expectSuccess = valueLength > length; + condition = Condition.StringLengthGreaterThan(key2, length); + Assert.Contains("String length > " + length, condition.ToString()); + break; + case ComparisonType.LessThan: + expectSuccess = valueLength < length; + condition = Condition.StringLengthLessThan(key2, length); + Assert.Contains("String length < " + length, condition.ToString()); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); } - [Theory] - [InlineData("five", ComparisonType.Equal, 5L, false)] - [InlineData("four", ComparisonType.Equal, 4L, true)] - [InlineData("three", ComparisonType.Equal, 3L, false)] - [InlineData("", ComparisonType.Equal, 2L, false)] - [InlineData("", ComparisonType.Equal, 0L, true)] - - [InlineData("five", ComparisonType.LessThan, 5L, true)] - [InlineData("four", ComparisonType.LessThan, 4L, false)] - [InlineData("three", ComparisonType.LessThan, 3L, false)] - [InlineData("", ComparisonType.LessThan, 2L, true)] - [InlineData("", ComparisonType.LessThan, 0L, false)] - - [InlineData("five", ComparisonType.GreaterThan, 5L, false)] - [InlineData("four", ComparisonType.GreaterThan, 4L, false)] - [InlineData("three", ComparisonType.GreaterThan, 3L, true)] - [InlineData("", ComparisonType.GreaterThan, 2L, false)] - [InlineData("", ComparisonType.GreaterThan, 0L, false)] - public async Task BasicTranWithSetCardinalityCondition(string value, ComparisonType type, long length, bool expectTranResult) - { - using (var muxer = Create()) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - - var expectSuccess = false; - Condition? condition = null; - var valueLength = value?.Length ?? 0; - switch (type) - { - case ComparisonType.Equal: - expectSuccess = valueLength == length; - condition = Condition.SetLengthEqual(key2, length); - break; - case ComparisonType.GreaterThan: - expectSuccess = valueLength > length; - condition = Condition.SetLengthGreaterThan(key2, length); - break; - case ComparisonType.LessThan: - expectSuccess = valueLength < length; - condition = Condition.SetLengthLessThan(key2, length); - break; - default: - throw new ArgumentOutOfRangeException(nameof(type)); - } + if (value != null) db.StringSet(key2, value, flags: CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); + Assert.Equal(value, db.StringGet(key2)); - for (var i = 0; i < valueLength; i++) - { - db.SetAdd(key2, i, flags: CommandFlags.FireAndForget); - } - Assert.False(db.KeyExists(key)); - Assert.Equal(valueLength, db.SetLength(key2)); + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(condition); + var push = tran.StringSetAsync(key, "any value"); + var exec = tran.ExecuteAsync(); + var get = db.StringLength(key); - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(condition); - var push = tran.StringSetAsync(key, "any value"); - var exec = tran.ExecuteAsync(); - var get = db.StringLength(key); + Assert.Equal(expectTranResult, await exec); - Assert.Equal(expectTranResult, await exec); + if (expectSuccess) + { + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.True(await push); // eq: push + Assert.Equal("any value".Length, get); // eq: get + } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push + Assert.Equal(0, get); // neq: get + } + } - if (expectSuccess) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.True(await push); // eq: push - Assert.Equal("any value".Length, get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push - Assert.Equal(0, get); // neq: get - } - } + [Theory] + [InlineData("five", ComparisonType.Equal, 5L, false)] + [InlineData("four", ComparisonType.Equal, 4L, true)] + [InlineData("three", ComparisonType.Equal, 3L, false)] + [InlineData("", ComparisonType.Equal, 2L, false)] + [InlineData("", ComparisonType.Equal, 0L, true)] + + [InlineData("five", ComparisonType.LessThan, 5L, true)] + [InlineData("four", ComparisonType.LessThan, 4L, false)] + [InlineData("three", ComparisonType.LessThan, 3L, false)] + [InlineData("", ComparisonType.LessThan, 2L, true)] + [InlineData("", ComparisonType.LessThan, 0L, false)] + + [InlineData("five", ComparisonType.GreaterThan, 5L, false)] + [InlineData("four", ComparisonType.GreaterThan, 4L, false)] + [InlineData("three", ComparisonType.GreaterThan, 3L, true)] + [InlineData("", ComparisonType.GreaterThan, 2L, false)] + [InlineData("", ComparisonType.GreaterThan, 0L, false)] + public async Task BasicTranWithHashLengthCondition(string value, ComparisonType type, long length, bool expectTranResult) + { + using var conn = Create(); + + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + + var expectSuccess = false; + Condition? condition = null; + var valueLength = value?.Length ?? 0; + switch (type) + { + case ComparisonType.Equal: + expectSuccess = valueLength == length; + condition = Condition.HashLengthEqual(key2, length); + break; + case ComparisonType.GreaterThan: + expectSuccess = valueLength > length; + condition = Condition.HashLengthGreaterThan(key2, length); + break; + case ComparisonType.LessThan: + expectSuccess = valueLength < length; + condition = Condition.HashLengthLessThan(key2, length); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); } - [Theory] - [InlineData(false, false, true)] - [InlineData(false, true, false)] - [InlineData(true, false, false)] - [InlineData(true, true, true)] - public async Task BasicTranWithSetContainsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) + for (var i = 0; i < valueLength; i++) { - using (var muxer = Create(disabledCommands: new[] { "info", "config" })) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - RedisValue member = "value"; - if (keyExists) db.SetAdd(key2, member, flags: CommandFlags.FireAndForget); - Assert.False(db.KeyExists(key)); - Assert.Equal(keyExists, db.SetContains(key2, member)); - - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(demandKeyExists ? Condition.SetContains(key2, member) : Condition.SetNotContains(key2, member)); - var incr = tran.StringIncrementAsync(key); - var exec = tran.ExecuteAsync(); - var get = db.StringGet(key); - - Assert.Equal(expectTranResult, await exec); - if (demandKeyExists == keyExists) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.Equal(1, await incr); // eq: incr - Assert.Equal(1, (long)get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr - Assert.Equal(0, (long)get); // neq: get - } - } + db.HashSet(key2, i, value![i].ToString(), flags: CommandFlags.FireAndForget); } + Assert.False(db.KeyExists(key)); + Assert.Equal(valueLength, db.HashLength(key2)); - [Theory] - [InlineData("five", ComparisonType.Equal, 5L, false)] - [InlineData("four", ComparisonType.Equal, 4L, true)] - [InlineData("three", ComparisonType.Equal, 3L, false)] - [InlineData("", ComparisonType.Equal, 2L, false)] - [InlineData("", ComparisonType.Equal, 0L, true)] - - [InlineData("five", ComparisonType.LessThan, 5L, true)] - [InlineData("four", ComparisonType.LessThan, 4L, false)] - [InlineData("three", ComparisonType.LessThan, 3L, false)] - [InlineData("", ComparisonType.LessThan, 2L, true)] - [InlineData("", ComparisonType.LessThan, 0L, false)] - - [InlineData("five", ComparisonType.GreaterThan, 5L, false)] - [InlineData("four", ComparisonType.GreaterThan, 4L, false)] - [InlineData("three", ComparisonType.GreaterThan, 3L, true)] - [InlineData("", ComparisonType.GreaterThan, 2L, false)] - [InlineData("", ComparisonType.GreaterThan, 0L, false)] - public async Task BasicTranWithSortedSetCardinalityCondition(string value, ComparisonType type, long length, bool expectTranResult) + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(condition); + var push = tran.StringSetAsync(key, "any value"); + var exec = tran.ExecuteAsync(); + var get = db.StringLength(key); + + Assert.Equal(expectTranResult, await exec); + + if (expectSuccess) { - using (var muxer = Create()) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - - var expectSuccess = false; - Condition? condition = null; - var valueLength = value?.Length ?? 0; - switch (type) - { - case ComparisonType.Equal: - expectSuccess = valueLength == length; - condition = Condition.SortedSetLengthEqual(key2, length); - break; - case ComparisonType.GreaterThan: - expectSuccess = valueLength > length; - condition = Condition.SortedSetLengthGreaterThan(key2, length); - break; - case ComparisonType.LessThan: - expectSuccess = valueLength < length; - condition = Condition.SortedSetLengthLessThan(key2, length); - break; - default: - throw new ArgumentOutOfRangeException(nameof(type)); - } + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.True(await push); // eq: push + Assert.Equal("any value".Length, get); // eq: get + } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push + Assert.Equal(0, get); // neq: get + } + } - for (var i = 0; i < valueLength; i++) - { - db.SortedSetAdd(key2, i, i, flags: CommandFlags.FireAndForget); - } - Assert.False(db.KeyExists(key)); - Assert.Equal(valueLength, db.SortedSetLength(key2)); + [Theory] + [InlineData("five", ComparisonType.Equal, 5L, false)] + [InlineData("four", ComparisonType.Equal, 4L, true)] + [InlineData("three", ComparisonType.Equal, 3L, false)] + [InlineData("", ComparisonType.Equal, 2L, false)] + [InlineData("", ComparisonType.Equal, 0L, true)] + + [InlineData("five", ComparisonType.LessThan, 5L, true)] + [InlineData("four", ComparisonType.LessThan, 4L, false)] + [InlineData("three", ComparisonType.LessThan, 3L, false)] + [InlineData("", ComparisonType.LessThan, 2L, true)] + [InlineData("", ComparisonType.LessThan, 0L, false)] + + [InlineData("five", ComparisonType.GreaterThan, 5L, false)] + [InlineData("four", ComparisonType.GreaterThan, 4L, false)] + [InlineData("three", ComparisonType.GreaterThan, 3L, true)] + [InlineData("", ComparisonType.GreaterThan, 2L, false)] + [InlineData("", ComparisonType.GreaterThan, 0L, false)] + public async Task BasicTranWithSetCardinalityCondition(string value, ComparisonType type, long length, bool expectTranResult) + { + using var conn = Create(); - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(condition); - var push = tran.StringSetAsync(key, "any value"); - var exec = tran.ExecuteAsync(); - var get = db.StringLength(key); + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); - Assert.Equal(expectTranResult, await exec); + var expectSuccess = false; + Condition? condition = null; + var valueLength = value?.Length ?? 0; + switch (type) + { + case ComparisonType.Equal: + expectSuccess = valueLength == length; + condition = Condition.SetLengthEqual(key2, length); + break; + case ComparisonType.GreaterThan: + expectSuccess = valueLength > length; + condition = Condition.SetLengthGreaterThan(key2, length); + break; + case ComparisonType.LessThan: + expectSuccess = valueLength < length; + condition = Condition.SetLengthLessThan(key2, length); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); + } - if (expectSuccess) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.True(await push); // eq: push - Assert.Equal("any value".Length, get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push - Assert.Equal(0, get); // neq: get - } - } + for (var i = 0; i < valueLength; i++) + { + db.SetAdd(key2, i, flags: CommandFlags.FireAndForget); } + Assert.False(db.KeyExists(key)); + Assert.Equal(valueLength, db.SetLength(key2)); - [Theory] - [InlineData(1, 4, ComparisonType.Equal, 5L, false)] - [InlineData(1, 4, ComparisonType.Equal, 4L, true)] - [InlineData(1, 2, ComparisonType.Equal, 3L, false)] - [InlineData(1, 1, ComparisonType.Equal, 2L, false)] - [InlineData(0, 0, ComparisonType.Equal, 0L, false)] - - [InlineData(1, 4, ComparisonType.LessThan, 5L, true)] - [InlineData(1, 4, ComparisonType.LessThan, 4L, false)] - [InlineData(1, 3, ComparisonType.LessThan, 3L, false)] - [InlineData(1, 1, ComparisonType.LessThan, 2L, true)] - [InlineData(0, 0, ComparisonType.LessThan, 0L, false)] - - [InlineData(1, 5, ComparisonType.GreaterThan, 5L, false)] - [InlineData(1, 4, ComparisonType.GreaterThan, 4L, false)] - [InlineData(1, 4, ComparisonType.GreaterThan, 3L, true)] - [InlineData(1, 2, ComparisonType.GreaterThan, 2L, false)] - [InlineData(0, 0, ComparisonType.GreaterThan, 0L, true)] - public async Task BasicTranWithSortedSetRangeCountCondition(double min, double max, ComparisonType type, long length, bool expectTranResult) + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(condition); + var push = tran.StringSetAsync(key, "any value"); + var exec = tran.ExecuteAsync(); + var get = db.StringLength(key); + + Assert.Equal(expectTranResult, await exec); + + if (expectSuccess) { - using (var muxer = Create()) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - - var expectSuccess = false; - Condition? condition = null; - var valueLength = (int)(max - min) + 1; - switch (type) - { - case ComparisonType.Equal: - expectSuccess = valueLength == length; - condition = Condition.SortedSetLengthEqual(key2, length, min, max); - break; - case ComparisonType.GreaterThan: - expectSuccess = valueLength > length; - condition = Condition.SortedSetLengthGreaterThan(key2, length, min, max); - break; - case ComparisonType.LessThan: - expectSuccess = valueLength < length; - condition = Condition.SortedSetLengthLessThan(key2, length, min, max); - break; - default: - throw new ArgumentOutOfRangeException(nameof(type)); - } + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.True(await push); // eq: push + Assert.Equal("any value".Length, get); // eq: get + } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push + Assert.Equal(0, get); // neq: get + } + } - for (var i = 0; i < 5; i++) - { - db.SortedSetAdd(key2, i, i, flags: CommandFlags.FireAndForget); - } - Assert.False(db.KeyExists(key)); - Assert.Equal(5, db.SortedSetLength(key2)); + [Theory] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(true, false, false)] + [InlineData(true, true, true)] + public async Task BasicTranWithSetContainsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) + { + using var conn = Create(disabledCommands: new[] { "info", "config" }); + + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + RedisValue member = "value"; + if (keyExists) db.SetAdd(key2, member, flags: CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); + Assert.Equal(keyExists, db.SetContains(key2, member)); + + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(demandKeyExists ? Condition.SetContains(key2, member) : Condition.SetNotContains(key2, member)); + var incr = tran.StringIncrementAsync(key); + var exec = tran.ExecuteAsync(); + var get = db.StringGet(key); + + Assert.Equal(expectTranResult, await exec); + if (demandKeyExists == keyExists) + { + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.Equal(1, await incr); // eq: incr + Assert.Equal(1, (long)get); // eq: get + } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr + Assert.Equal(0, (long)get); // neq: get + } + } - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(condition); - var push = tran.StringSetAsync(key, "any value"); - var exec = tran.ExecuteAsync(); - var get = db.StringLength(key); + [Theory] + [InlineData("five", ComparisonType.Equal, 5L, false)] + [InlineData("four", ComparisonType.Equal, 4L, true)] + [InlineData("three", ComparisonType.Equal, 3L, false)] + [InlineData("", ComparisonType.Equal, 2L, false)] + [InlineData("", ComparisonType.Equal, 0L, true)] + + [InlineData("five", ComparisonType.LessThan, 5L, true)] + [InlineData("four", ComparisonType.LessThan, 4L, false)] + [InlineData("three", ComparisonType.LessThan, 3L, false)] + [InlineData("", ComparisonType.LessThan, 2L, true)] + [InlineData("", ComparisonType.LessThan, 0L, false)] + + [InlineData("five", ComparisonType.GreaterThan, 5L, false)] + [InlineData("four", ComparisonType.GreaterThan, 4L, false)] + [InlineData("three", ComparisonType.GreaterThan, 3L, true)] + [InlineData("", ComparisonType.GreaterThan, 2L, false)] + [InlineData("", ComparisonType.GreaterThan, 0L, false)] + public async Task BasicTranWithSortedSetCardinalityCondition(string value, ComparisonType type, long length, bool expectTranResult) + { + using var conn = Create(); - Assert.Equal(expectTranResult, await exec); + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); - if (expectSuccess) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.True(await push); // eq: push - Assert.Equal("any value".Length, get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push - Assert.Equal(0, get); // neq: get - } - } + var expectSuccess = false; + Condition? condition = null; + var valueLength = value?.Length ?? 0; + switch (type) + { + case ComparisonType.Equal: + expectSuccess = valueLength == length; + condition = Condition.SortedSetLengthEqual(key2, length); + break; + case ComparisonType.GreaterThan: + expectSuccess = valueLength > length; + condition = Condition.SortedSetLengthGreaterThan(key2, length); + break; + case ComparisonType.LessThan: + expectSuccess = valueLength < length; + condition = Condition.SortedSetLengthLessThan(key2, length); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); } - [Theory] - [InlineData(false, false, true)] - [InlineData(false, true, false)] - [InlineData(true, false, false)] - [InlineData(true, true, true)] - public async Task BasicTranWithSortedSetContainsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) + for (var i = 0; i < valueLength; i++) { - using (var muxer = Create(disabledCommands: new[] { "info", "config" })) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - RedisValue member = "value"; - if (keyExists) db.SortedSetAdd(key2, member, 0.0, flags: CommandFlags.FireAndForget); - Assert.False(db.KeyExists(key)); - Assert.Equal(keyExists, db.SortedSetScore(key2, member).HasValue); - - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(demandKeyExists ? Condition.SortedSetContains(key2, member) : Condition.SortedSetNotContains(key2, member)); - var incr = tran.StringIncrementAsync(key); - var exec = tran.ExecuteAsync(); - var get = db.StringGet(key); - - Assert.Equal(expectTranResult, await exec); - if (demandKeyExists == keyExists) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.Equal(1, await incr); // eq: incr - Assert.Equal(1, (long)get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr - Assert.Equal(0, (long)get); // neq: get - } - } + db.SortedSetAdd(key2, i, i, flags: CommandFlags.FireAndForget); } + Assert.False(db.KeyExists(key)); + Assert.Equal(valueLength, db.SortedSetLength(key2)); + + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(condition); + var push = tran.StringSetAsync(key, "any value"); + var exec = tran.ExecuteAsync(); + var get = db.StringLength(key); - [Theory] - [InlineData(4D, 4D, true, true)] - [InlineData(4D, 5D, true, false)] - [InlineData(4D, null, true, false)] - [InlineData(null, 5D, true, false)] - [InlineData(null, null, true, true)] - - [InlineData(4D, 4D, false, false)] - [InlineData(4D, 5D, false, true)] - [InlineData(4D, null, false, true)] - [InlineData(null, 5D, false, true)] - [InlineData(null, null, false, false)] - public async Task BasicTranWithSortedSetEqualCondition(double? expected, double? value, bool expectEqual, bool expectedTranResult) + Assert.Equal(expectTranResult, await exec); + + if (expectSuccess) { - using (var muxer = Create()) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - - RedisValue member = "member"; - if (value != null) db.SortedSetAdd(key2, member, value.Value, flags: CommandFlags.FireAndForget); - Assert.False(db.KeyExists(key)); - Assert.Equal(value, db.SortedSetScore(key2, member)); - - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(expectEqual ? Condition.SortedSetEqual(key2, member, expected) : Condition.SortedSetNotEqual(key2, member, expected)); - var incr = tran.StringIncrementAsync(key); - var exec = tran.ExecuteAsync(); - var get = db.StringGet(key); - - Assert.Equal(expectedTranResult, await exec); - if (expectEqual == (value == expected)) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.Equal(1, await incr); // eq: incr - Assert.Equal(1, (long)get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr - Assert.Equal(0, (long)get); // neq: get - } - } + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.True(await push); // eq: push + Assert.Equal("any value".Length, get); // eq: get + } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push + Assert.Equal(0, get); // neq: get } + } - [Theory] - [InlineData(true, true, true, true)] - [InlineData(true, false, true, true)] - [InlineData(false, true, true, true)] - [InlineData(true, true, false, false)] - [InlineData(true, false, false, false)] - [InlineData(false, true, false, false)] - [InlineData(false, false, true, false)] - [InlineData(false, false, false, true)] - public async Task BasicTranWithSortedSetScoreExistsCondition(bool member1HasScore, bool member2HasScore, bool demandScoreExists, bool expectedTranResult) + [Theory] + [InlineData(1, 4, ComparisonType.Equal, 5L, false)] + [InlineData(1, 4, ComparisonType.Equal, 4L, true)] + [InlineData(1, 2, ComparisonType.Equal, 3L, false)] + [InlineData(1, 1, ComparisonType.Equal, 2L, false)] + [InlineData(0, 0, ComparisonType.Equal, 0L, false)] + + [InlineData(1, 4, ComparisonType.LessThan, 5L, true)] + [InlineData(1, 4, ComparisonType.LessThan, 4L, false)] + [InlineData(1, 3, ComparisonType.LessThan, 3L, false)] + [InlineData(1, 1, ComparisonType.LessThan, 2L, true)] + [InlineData(0, 0, ComparisonType.LessThan, 0L, false)] + + [InlineData(1, 5, ComparisonType.GreaterThan, 5L, false)] + [InlineData(1, 4, ComparisonType.GreaterThan, 4L, false)] + [InlineData(1, 4, ComparisonType.GreaterThan, 3L, true)] + [InlineData(1, 2, ComparisonType.GreaterThan, 2L, false)] + [InlineData(0, 0, ComparisonType.GreaterThan, 0L, true)] + public async Task BasicTranWithSortedSetRangeCountCondition(double min, double max, ComparisonType type, long length, bool expectTranResult) + { + using var conn = Create(); + + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + + var expectSuccess = false; + Condition? condition = null; + var valueLength = (int)(max - min) + 1; + switch (type) { - using (var muxer = Create()) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - - const double Score = 4D; - RedisValue member1 = "member1"; - RedisValue member2 = "member2"; - if (member1HasScore) - { - db.SortedSetAdd(key2, member1, Score, flags: CommandFlags.FireAndForget); - } + case ComparisonType.Equal: + expectSuccess = valueLength == length; + condition = Condition.SortedSetLengthEqual(key2, length, min, max); + break; + case ComparisonType.GreaterThan: + expectSuccess = valueLength > length; + condition = Condition.SortedSetLengthGreaterThan(key2, length, min, max); + break; + case ComparisonType.LessThan: + expectSuccess = valueLength < length; + condition = Condition.SortedSetLengthLessThan(key2, length, min, max); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); + } - if (member2HasScore) - { - db.SortedSetAdd(key2, member2, Score, flags: CommandFlags.FireAndForget); - } + for (var i = 0; i < 5; i++) + { + db.SortedSetAdd(key2, i, i, flags: CommandFlags.FireAndForget); + } + Assert.False(db.KeyExists(key)); + Assert.Equal(5, db.SortedSetLength(key2)); - Assert.False(db.KeyExists(key)); - Assert.Equal(member1HasScore ? (double?)Score : null, db.SortedSetScore(key2, member1)); - Assert.Equal(member2HasScore ? (double?)Score : null, db.SortedSetScore(key2, member2)); + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(condition); + var push = tran.StringSetAsync(key, "any value"); + var exec = tran.ExecuteAsync(); + var get = db.StringLength(key); - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(demandScoreExists ? Condition.SortedSetScoreExists(key2, Score) : Condition.SortedSetScoreNotExists(key2, Score)); - var incr = tran.StringIncrementAsync(key); - var exec = tran.ExecuteAsync(); - var get = db.StringGet(key); + Assert.Equal(expectTranResult, await exec); - Assert.Equal(expectedTranResult, await exec); - if ((member1HasScore || member2HasScore) == demandScoreExists) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.Equal(1, await incr); // eq: incr - Assert.Equal(1, (long)get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr - Assert.Equal(0, (long)get); // neq: get - } - } + if (expectSuccess) + { + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.True(await push); // eq: push + Assert.Equal("any value".Length, get); // eq: get } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push + Assert.Equal(0, get); // neq: get + } + } - [Theory] - [InlineData(true, true, 2L, true, true)] - [InlineData(true, true, 2L, false, false)] - [InlineData(true, true, 1L, true, false)] - [InlineData(true, true, 1L, false, true)] - [InlineData(true, false, 2L, true, false)] - [InlineData(true, false, 2L, false, true)] - [InlineData(true, false, 1L, true, true)] - [InlineData(true, false, 1L, false, false)] - [InlineData(false, true, 2L, true, false)] - [InlineData(false, true, 2L, false, true)] - [InlineData(false, true, 1L, true, true)] - [InlineData(false, true, 1L, false, false)] - [InlineData(false, false, 2L, true, false)] - [InlineData(false, false, 2L, false, true)] - [InlineData(false, false, 1L, true, false)] - [InlineData(false, false, 1L, false, true)] - public async Task BasicTranWithSortedSetScoreCountExistsCondition(bool member1HasScore, bool member2HasScore, long expectedLength, bool expectEqual, bool expectedTranResult) + [Theory] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(true, false, false)] + [InlineData(true, true, true)] + public async Task BasicTranWithSortedSetContainsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) + { + using var conn = Create(disabledCommands: new[] { "info", "config" }); + + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + RedisValue member = "value"; + if (keyExists) db.SortedSetAdd(key2, member, 0.0, flags: CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); + Assert.Equal(keyExists, db.SortedSetScore(key2, member).HasValue); + + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(demandKeyExists ? Condition.SortedSetContains(key2, member) : Condition.SortedSetNotContains(key2, member)); + var incr = tran.StringIncrementAsync(key); + var exec = tran.ExecuteAsync(); + var get = db.StringGet(key); + + Assert.Equal(expectTranResult, await exec); + if (demandKeyExists == keyExists) { - using (var muxer = Create()) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - - const double Score = 4D; - var length = 0L; - RedisValue member1 = "member1"; - RedisValue member2 = "member2"; - if (member1HasScore) - { - db.SortedSetAdd(key2, member1, Score, flags: CommandFlags.FireAndForget); - length++; - } + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.Equal(1, await incr); // eq: incr + Assert.Equal(1, (long)get); // eq: get + } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr + Assert.Equal(0, (long)get); // neq: get + } + } - if (member2HasScore) - { - db.SortedSetAdd(key2, member2, Score, flags: CommandFlags.FireAndForget); - length++; - } + [Theory] + [InlineData(4D, 4D, true, true)] + [InlineData(4D, 5D, true, false)] + [InlineData(4D, null, true, false)] + [InlineData(null, 5D, true, false)] + [InlineData(null, null, true, true)] + + [InlineData(4D, 4D, false, false)] + [InlineData(4D, 5D, false, true)] + [InlineData(4D, null, false, true)] + [InlineData(null, 5D, false, true)] + [InlineData(null, null, false, false)] + public async Task BasicTranWithSortedSetEqualCondition(double? expected, double? value, bool expectEqual, bool expectedTranResult) + { + using var conn = Create(); + + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + + RedisValue member = "member"; + if (value != null) db.SortedSetAdd(key2, member, value.Value, flags: CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); + Assert.Equal(value, db.SortedSetScore(key2, member)); + + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(expectEqual ? Condition.SortedSetEqual(key2, member, expected) : Condition.SortedSetNotEqual(key2, member, expected)); + var incr = tran.StringIncrementAsync(key); + var exec = tran.ExecuteAsync(); + var get = db.StringGet(key); + + Assert.Equal(expectedTranResult, await exec); + if (expectEqual == (value == expected)) + { + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.Equal(1, await incr); // eq: incr + Assert.Equal(1, (long)get); // eq: get + } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr + Assert.Equal(0, (long)get); // neq: get + } + } - Assert.False(db.KeyExists(key)); - Assert.Equal(length, db.SortedSetLength(key2, Score, Score)); + [Theory] + [InlineData(true, true, true, true)] + [InlineData(true, false, true, true)] + [InlineData(false, true, true, true)] + [InlineData(true, true, false, false)] + [InlineData(true, false, false, false)] + [InlineData(false, true, false, false)] + [InlineData(false, false, true, false)] + [InlineData(false, false, false, true)] + public async Task BasicTranWithSortedSetScoreExistsCondition(bool member1HasScore, bool member2HasScore, bool demandScoreExists, bool expectedTranResult) + { + using var conn = Create(); - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(expectEqual ? Condition.SortedSetScoreExists(key2, Score, expectedLength) : Condition.SortedSetScoreNotExists(key2, Score, expectedLength)); - var incr = tran.StringIncrementAsync(key); - var exec = tran.ExecuteAsync(); - var get = db.StringGet(key); + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); - Assert.Equal(expectedTranResult, await exec); - if (expectEqual == (length == expectedLength)) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.Equal(1, await incr); // eq: incr - Assert.Equal(1, (long)get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr - Assert.Equal(0, (long)get); // neq: get - } - } + const double Score = 4D; + RedisValue member1 = "member1"; + RedisValue member2 = "member2"; + if (member1HasScore) + { + db.SortedSetAdd(key2, member1, Score, flags: CommandFlags.FireAndForget); } - [Theory] - [InlineData("five", ComparisonType.Equal, 5L, false)] - [InlineData("four", ComparisonType.Equal, 4L, true)] - [InlineData("three", ComparisonType.Equal, 3L, false)] - [InlineData("", ComparisonType.Equal, 2L, false)] - [InlineData("", ComparisonType.Equal, 0L, true)] - - [InlineData("five", ComparisonType.LessThan, 5L, true)] - [InlineData("four", ComparisonType.LessThan, 4L, false)] - [InlineData("three", ComparisonType.LessThan, 3L, false)] - [InlineData("", ComparisonType.LessThan, 2L, true)] - [InlineData("", ComparisonType.LessThan, 0L, false)] - - [InlineData("five", ComparisonType.GreaterThan, 5L, false)] - [InlineData("four", ComparisonType.GreaterThan, 4L, false)] - [InlineData("three", ComparisonType.GreaterThan, 3L, true)] - [InlineData("", ComparisonType.GreaterThan, 2L, false)] - [InlineData("", ComparisonType.GreaterThan, 0L, false)] - public async Task BasicTranWithListLengthCondition(string value, ComparisonType type, long length, bool expectTranResult) + if (member2HasScore) { - using (var muxer = Create()) - { - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); - - var expectSuccess = false; - Condition? condition = null; - var valueLength = value?.Length ?? 0; - switch (type) - { - case ComparisonType.Equal: - expectSuccess = valueLength == length; - condition = Condition.ListLengthEqual(key2, length); - break; - case ComparisonType.GreaterThan: - expectSuccess = valueLength > length; - condition = Condition.ListLengthGreaterThan(key2, length); - break; - case ComparisonType.LessThan: - expectSuccess = valueLength < length; - condition = Condition.ListLengthLessThan(key2, length); - break; - default: - throw new ArgumentOutOfRangeException(nameof(type)); - } + db.SortedSetAdd(key2, member2, Score, flags: CommandFlags.FireAndForget); + } - for (var i = 0; i < valueLength; i++) - { - db.ListRightPush(key2, i, flags: CommandFlags.FireAndForget); - } - Assert.False(db.KeyExists(key)); - Assert.Equal(valueLength, db.ListLength(key2)); + Assert.False(db.KeyExists(key)); + Assert.Equal(member1HasScore ? (double?)Score : null, db.SortedSetScore(key2, member1)); + Assert.Equal(member2HasScore ? (double?)Score : null, db.SortedSetScore(key2, member2)); - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(condition); - var push = tran.StringSetAsync(key, "any value"); - var exec = tran.ExecuteAsync(); - var get = db.StringLength(key); + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(demandScoreExists ? Condition.SortedSetScoreExists(key2, Score) : Condition.SortedSetScoreNotExists(key2, Score)); + var incr = tran.StringIncrementAsync(key); + var exec = tran.ExecuteAsync(); + var get = db.StringGet(key); - Assert.Equal(expectTranResult, await exec); + Assert.Equal(expectedTranResult, await exec); + if ((member1HasScore || member2HasScore) == demandScoreExists) + { + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.Equal(1, await incr); // eq: incr + Assert.Equal(1, (long)get); // eq: get + } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr + Assert.Equal(0, (long)get); // neq: get + } + } - if (expectSuccess) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.True(await push); // eq: push - Assert.Equal("any value".Length, get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push - Assert.Equal(0, get); // neq: get - } - } + [Theory] + [InlineData(true, true, 2L, true, true)] + [InlineData(true, true, 2L, false, false)] + [InlineData(true, true, 1L, true, false)] + [InlineData(true, true, 1L, false, true)] + [InlineData(true, false, 2L, true, false)] + [InlineData(true, false, 2L, false, true)] + [InlineData(true, false, 1L, true, true)] + [InlineData(true, false, 1L, false, false)] + [InlineData(false, true, 2L, true, false)] + [InlineData(false, true, 2L, false, true)] + [InlineData(false, true, 1L, true, true)] + [InlineData(false, true, 1L, false, false)] + [InlineData(false, false, 2L, true, false)] + [InlineData(false, false, 2L, false, true)] + [InlineData(false, false, 1L, true, false)] + [InlineData(false, false, 1L, false, true)] + public async Task BasicTranWithSortedSetScoreCountExistsCondition(bool member1HasScore, bool member2HasScore, long expectedLength, bool expectEqual, bool expectedTranResult) + { + using var conn = Create(); + + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + + const double Score = 4D; + var length = 0L; + RedisValue member1 = "member1"; + RedisValue member2 = "member2"; + if (member1HasScore) + { + db.SortedSetAdd(key2, member1, Score, flags: CommandFlags.FireAndForget); + length++; } - [Theory] - [InlineData("five", ComparisonType.Equal, 5L, false)] - [InlineData("four", ComparisonType.Equal, 4L, true)] - [InlineData("three", ComparisonType.Equal, 3L, false)] - [InlineData("", ComparisonType.Equal, 2L, false)] - [InlineData("", ComparisonType.Equal, 0L, true)] - - [InlineData("five", ComparisonType.LessThan, 5L, true)] - [InlineData("four", ComparisonType.LessThan, 4L, false)] - [InlineData("three", ComparisonType.LessThan, 3L, false)] - [InlineData("", ComparisonType.LessThan, 2L, true)] - [InlineData("", ComparisonType.LessThan, 0L, false)] - - [InlineData("five", ComparisonType.GreaterThan, 5L, false)] - [InlineData("four", ComparisonType.GreaterThan, 4L, false)] - [InlineData("three", ComparisonType.GreaterThan, 3L, true)] - [InlineData("", ComparisonType.GreaterThan, 2L, false)] - [InlineData("", ComparisonType.GreaterThan, 0L, false)] - public async Task BasicTranWithStreamLengthCondition(string value, ComparisonType type, long length, bool expectTranResult) + if (member2HasScore) { - using (var muxer = Create()) - { - Skip.IfBelow(muxer, RedisFeatures.v5_0_0); + db.SortedSetAdd(key2, member2, Score, flags: CommandFlags.FireAndForget); + length++; + } - RedisKey key = Me(), key2 = Me() + "2"; - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.KeyDelete(key2, CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); + Assert.Equal(length, db.SortedSetLength(key2, Score, Score)); - var expectSuccess = false; - Condition? condition = null; - var valueLength = value?.Length ?? 0; - switch (type) - { - case ComparisonType.Equal: - expectSuccess = valueLength == length; - condition = Condition.StreamLengthEqual(key2, length); - break; - case ComparisonType.GreaterThan: - expectSuccess = valueLength > length; - condition = Condition.StreamLengthGreaterThan(key2, length); - break; - case ComparisonType.LessThan: - expectSuccess = valueLength < length; - condition = Condition.StreamLengthLessThan(key2, length); - break; - default: - throw new ArgumentOutOfRangeException(nameof(type)); - } - RedisValue fieldName = "Test"; - for (var i = 0; i < valueLength; i++) - { - db.StreamAdd(key2, fieldName, i, flags: CommandFlags.FireAndForget); - } - Assert.False(db.KeyExists(key)); - Assert.Equal(valueLength, db.StreamLength(key2)); + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(expectEqual ? Condition.SortedSetScoreExists(key2, Score, expectedLength) : Condition.SortedSetScoreNotExists(key2, Score, expectedLength)); + var incr = tran.StringIncrementAsync(key); + var exec = tran.ExecuteAsync(); + var get = db.StringGet(key); + + Assert.Equal(expectedTranResult, await exec); + if (expectEqual == (length == expectedLength)) + { + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.Equal(1, await incr); // eq: incr + Assert.Equal(1, (long)get); // eq: get + } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr + Assert.Equal(0, (long)get); // neq: get + } + } - var tran = db.CreateTransaction(); - var cond = tran.AddCondition(condition); - var push = tran.StringSetAsync(key, "any value"); - var exec = tran.ExecuteAsync(); - var get = db.StringLength(key); + [Theory] + [InlineData("five", ComparisonType.Equal, 5L, false)] + [InlineData("four", ComparisonType.Equal, 4L, true)] + [InlineData("three", ComparisonType.Equal, 3L, false)] + [InlineData("", ComparisonType.Equal, 2L, false)] + [InlineData("", ComparisonType.Equal, 0L, true)] + + [InlineData("five", ComparisonType.LessThan, 5L, true)] + [InlineData("four", ComparisonType.LessThan, 4L, false)] + [InlineData("three", ComparisonType.LessThan, 3L, false)] + [InlineData("", ComparisonType.LessThan, 2L, true)] + [InlineData("", ComparisonType.LessThan, 0L, false)] + + [InlineData("five", ComparisonType.GreaterThan, 5L, false)] + [InlineData("four", ComparisonType.GreaterThan, 4L, false)] + [InlineData("three", ComparisonType.GreaterThan, 3L, true)] + [InlineData("", ComparisonType.GreaterThan, 2L, false)] + [InlineData("", ComparisonType.GreaterThan, 0L, false)] + public async Task BasicTranWithListLengthCondition(string value, ComparisonType type, long length, bool expectTranResult) + { + using var conn = Create(); - Assert.Equal(expectTranResult, await exec); + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); - if (expectSuccess) - { - Assert.True(await exec, "eq: exec"); - Assert.True(cond.WasSatisfied, "eq: was satisfied"); - Assert.True(await push); // eq: push - Assert.Equal("any value".Length, get); // eq: get - } - else - { - Assert.False(await exec, "neq: exec"); - Assert.False(cond.WasSatisfied, "neq: was satisfied"); - Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push - Assert.Equal(0, get); // neq: get - } - } + var expectSuccess = false; + Condition? condition = null; + var valueLength = value?.Length ?? 0; + switch (type) + { + case ComparisonType.Equal: + expectSuccess = valueLength == length; + condition = Condition.ListLengthEqual(key2, length); + break; + case ComparisonType.GreaterThan: + expectSuccess = valueLength > length; + condition = Condition.ListLengthGreaterThan(key2, length); + break; + case ComparisonType.LessThan: + expectSuccess = valueLength < length; + condition = Condition.ListLengthLessThan(key2, length); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); } - [Fact] - public async Task BasicTran() + for (var i = 0; i < valueLength; i++) { - using (var muxer = Create()) - { - RedisKey key = Me(); - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - Assert.False(db.KeyExists(key)); - - var tran = db.CreateTransaction(); - var a = tran.StringIncrementAsync(key, 10); - var b = tran.StringIncrementAsync(key, 5); - var c = tran.StringGetAsync(key); - var d = tran.KeyExistsAsync(key); - var e = tran.KeyDeleteAsync(key); - var f = tran.KeyExistsAsync(key); - Assert.False(a.IsCompleted); - Assert.False(b.IsCompleted); - Assert.False(c.IsCompleted); - Assert.False(d.IsCompleted); - Assert.False(e.IsCompleted); - Assert.False(f.IsCompleted); - var result = await tran.ExecuteAsync().ForAwait(); - Assert.True(result, "result"); - await Task.WhenAll(a, b, c, d, e, f).ForAwait(); - Assert.True(a.IsCompleted, "a"); - Assert.True(b.IsCompleted, "b"); - Assert.True(c.IsCompleted, "c"); - Assert.True(d.IsCompleted, "d"); - Assert.True(e.IsCompleted, "e"); - Assert.True(f.IsCompleted, "f"); - - var g = db.KeyExists(key); - - Assert.Equal(10, await a.ForAwait()); - Assert.Equal(15, await b.ForAwait()); - Assert.Equal(15, (long)await c.ForAwait()); - Assert.True(await d.ForAwait()); - Assert.True(await e.ForAwait()); - Assert.False(await f.ForAwait()); - Assert.False(g); - } + db.ListRightPush(key2, i, flags: CommandFlags.FireAndForget); } + Assert.False(db.KeyExists(key)); + Assert.Equal(valueLength, db.ListLength(key2)); + + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(condition); + var push = tran.StringSetAsync(key, "any value"); + var exec = tran.ExecuteAsync(); + var get = db.StringLength(key); - [Fact] - public async Task CombineFireAndForgetAndRegularAsyncInTransaction() + Assert.Equal(expectTranResult, await exec); + + if (expectSuccess) { - using (var muxer = Create()) - { - RedisKey key = Me(); - var db = muxer.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - Assert.False(db.KeyExists(key)); - - var tran = db.CreateTransaction("state"); - var a = tran.StringIncrementAsync(key, 5); - var b = tran.StringIncrementAsync(key, 10, CommandFlags.FireAndForget); - var c = tran.StringIncrementAsync(key, 15); - Assert.True(tran.Execute()); - var count = (long)db.StringGet(key); - - Assert.Equal(5, await a); - Assert.Equal("state", a.AsyncState); - Assert.Equal(0, await b); - Assert.Null(b.AsyncState); - Assert.Equal(30, await c); - Assert.Equal("state", a.AsyncState); - Assert.Equal(30, count); - } + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.True(await push); // eq: push + Assert.Equal("any value".Length, get); // eq: get + } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push + Assert.Equal(0, get); // neq: get } + } -#if VERBOSE - [Fact] - public async Task WatchAbort_StringEqual() + [Theory] + [InlineData("five", ComparisonType.Equal, 5L, false)] + [InlineData("four", ComparisonType.Equal, 4L, true)] + [InlineData("three", ComparisonType.Equal, 3L, false)] + [InlineData("", ComparisonType.Equal, 2L, false)] + [InlineData("", ComparisonType.Equal, 0L, true)] + + [InlineData("five", ComparisonType.LessThan, 5L, true)] + [InlineData("four", ComparisonType.LessThan, 4L, false)] + [InlineData("three", ComparisonType.LessThan, 3L, false)] + [InlineData("", ComparisonType.LessThan, 2L, true)] + [InlineData("", ComparisonType.LessThan, 0L, false)] + + [InlineData("five", ComparisonType.GreaterThan, 5L, false)] + [InlineData("four", ComparisonType.GreaterThan, 4L, false)] + [InlineData("three", ComparisonType.GreaterThan, 3L, true)] + [InlineData("", ComparisonType.GreaterThan, 2L, false)] + [InlineData("", ComparisonType.GreaterThan, 0L, false)] + public async Task BasicTranWithStreamLengthCondition(string value, ComparisonType type, long length, bool expectTranResult) + { + using var conn = Create(require: RedisFeatures.v5_0_0); + + RedisKey key = Me(), key2 = Me() + "2"; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + + var expectSuccess = false; + Condition? condition = null; + var valueLength = value?.Length ?? 0; + switch (type) { - using (var vic = Create()) - using (var perp = Create()) - { - var key = Me(); - var db = vic.GetDatabase(); + case ComparisonType.Equal: + expectSuccess = valueLength == length; + condition = Condition.StreamLengthEqual(key2, length); + break; + case ComparisonType.GreaterThan: + expectSuccess = valueLength > length; + condition = Condition.StreamLengthGreaterThan(key2, length); + break; + case ComparisonType.LessThan: + expectSuccess = valueLength < length; + condition = Condition.StreamLengthLessThan(key2, length); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); + } + RedisValue fieldName = "Test"; + for (var i = 0; i < valueLength; i++) + { + db.StreamAdd(key2, fieldName, i, flags: CommandFlags.FireAndForget); + } + Assert.False(db.KeyExists(key)); + Assert.Equal(valueLength, db.StreamLength(key2)); - // expect foo, change to bar at the last minute - vic.PreTransactionExec += cmd => - { - Writer.WriteLine($"'{cmd}' detected; changing it..."); - perp.GetDatabase().StringSet(key, "bar"); - }; - db.KeyDelete(key); - db.StringSet(key, "foo"); - var tran = db.CreateTransaction(); - tran.AddCondition(Condition.StringEqual(key, "foo")); - var pong = tran.PingAsync(); - Assert.False(await tran.ExecuteAsync(), "expected abort"); - await Assert.ThrowsAsync(() => pong); - } + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(condition); + var push = tran.StringSetAsync(key, "any value"); + var exec = tran.ExecuteAsync(); + var get = db.StringLength(key); + + Assert.Equal(expectTranResult, await exec); + + if (expectSuccess) + { + Assert.True(await exec, "eq: exec"); + Assert.True(cond.WasSatisfied, "eq: was satisfied"); + Assert.True(await push); // eq: push + Assert.Equal("any value".Length, get); // eq: get } + else + { + Assert.False(await exec, "neq: exec"); + Assert.False(cond.WasSatisfied, "neq: was satisfied"); + Assert.Equal(TaskStatus.Canceled, SafeStatus(push)); // neq: push + Assert.Equal(0, get); // neq: get + } + } + + [Fact] + public async Task BasicTran() + { + using var conn = Create(); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); + + var tran = db.CreateTransaction(); + var a = tran.StringIncrementAsync(key, 10); + var b = tran.StringIncrementAsync(key, 5); + var c = tran.StringGetAsync(key); + var d = tran.KeyExistsAsync(key); + var e = tran.KeyDeleteAsync(key); + var f = tran.KeyExistsAsync(key); + Assert.False(a.IsCompleted); + Assert.False(b.IsCompleted); + Assert.False(c.IsCompleted); + Assert.False(d.IsCompleted); + Assert.False(e.IsCompleted); + Assert.False(f.IsCompleted); + var result = await tran.ExecuteAsync().ForAwait(); + Assert.True(result, "result"); + await Task.WhenAll(a, b, c, d, e, f).ForAwait(); + Assert.True(a.IsCompleted, "a"); + Assert.True(b.IsCompleted, "b"); + Assert.True(c.IsCompleted, "c"); + Assert.True(d.IsCompleted, "d"); + Assert.True(e.IsCompleted, "e"); + Assert.True(f.IsCompleted, "f"); + + var g = db.KeyExists(key); + + Assert.Equal(10, await a.ForAwait()); + Assert.Equal(15, await b.ForAwait()); + Assert.Equal(15, (long)await c.ForAwait()); + Assert.True(await d.ForAwait()); + Assert.True(await e.ForAwait()); + Assert.False(await f.ForAwait()); + Assert.False(g); + } - [Fact] - public async Task WatchAbort_HashLengthEqual() + [Fact] + public async Task CombineFireAndForgetAndRegularAsyncInTransaction() + { + using var conn = Create(); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); + + var tran = db.CreateTransaction("state"); + var a = tran.StringIncrementAsync(key, 5); + var b = tran.StringIncrementAsync(key, 10, CommandFlags.FireAndForget); + var c = tran.StringIncrementAsync(key, 15); + Assert.True(tran.Execute()); + var count = (long)db.StringGet(key); + + Assert.Equal(5, await a); + Assert.Equal("state", a.AsyncState); + Assert.Equal(0, await b); + Assert.Null(b.AsyncState); + Assert.Equal(30, await c); + Assert.Equal("state", a.AsyncState); + Assert.Equal(30, count); + } + +#if VERBOSE + [Fact] + public async Task WatchAbort_StringEqual() + { + using var vicConn = Create(); + using var perpConn = Create(); + + var key = Me(); + var db = vicConn.GetDatabase(); + + // expect foo, change to bar at the last minute + vicConn.PreTransactionExec += cmd => { - using (var vic = Create()) - using (var perp = Create()) - { - var key = Me(); - var db = vic.GetDatabase(); + Writer.WriteLine($"'{cmd}' detected; changing it..."); + perpConn.GetDatabase().StringSet(key, "bar"); + }; + db.KeyDelete(key); + db.StringSet(key, "foo"); + var tran = db.CreateTransaction(); + tran.AddCondition(Condition.StringEqual(key, "foo")); + var pong = tran.PingAsync(); + Assert.False(await tran.ExecuteAsync(), "expected abort"); + await Assert.ThrowsAsync(() => pong); + } - // expect foo, change to bar at the last minute - vic.PreTransactionExec += cmd => - { - Writer.WriteLine($"'{cmd}' detected; changing it..."); - perp.GetDatabase().HashSet(key, "bar", "def"); - }; - db.KeyDelete(key); - db.HashSet(key, "foo", "abc"); - var tran = db.CreateTransaction(); - tran.AddCondition(Condition.HashLengthEqual(key, 1)); - var pong = tran.PingAsync(); - Assert.False(await tran.ExecuteAsync()); - await Assert.ThrowsAsync(() => pong); - } - } + [Fact] + public async Task WatchAbort_HashLengthEqual() + { + using var vicConn = Create(); + using var perpConn = Create(); + + var key = Me(); + var db = vicConn.GetDatabase(); + + // expect foo, change to bar at the last minute + vicConn.PreTransactionExec += cmd => + { + Writer.WriteLine($"'{cmd}' detected; changing it..."); + perpConn.GetDatabase().HashSet(key, "bar", "def"); + }; + db.KeyDelete(key); + db.HashSet(key, "foo", "abc"); + var tran = db.CreateTransaction(); + tran.AddCondition(Condition.HashLengthEqual(key, 1)); + var pong = tran.PingAsync(); + Assert.False(await tran.ExecuteAsync()); + await Assert.ThrowsAsync(() => pong); + } #endif - [FactLongRunning] - public async Task ExecCompletes_Issue943() + [FactLongRunning] + public async Task ExecCompletes_Issue943() + { + int hashHit = 0, hashMiss = 0, expireHit = 0, expireMiss = 0; + using (var conn = Create()) { - int hashHit = 0, hashMiss = 0, expireHit = 0, expireMiss = 0; - using (var conn = Create()) + var db = conn.GetDatabase(); + for (int i = 0; i < 40000; i++) { - var db = conn.GetDatabase(); - for (int i = 0; i < 40000; i++) + RedisKey key = Me(); + await db.KeyDeleteAsync(key); + HashEntry[] hashEntries = new [] + { + new HashEntry("blah", DateTime.UtcNow.ToString("R")) + }; + ITransaction transaction = db.CreateTransaction(); + transaction.AddCondition(Condition.KeyNotExists(key)); + Task hashSetTask = transaction.HashSetAsync(key, hashEntries); + Task expireTask = transaction.KeyExpireAsync(key, TimeSpan.FromSeconds(30)); + bool committed = await transaction.ExecuteAsync(); + if (committed) { - RedisKey key = Me(); - await db.KeyDeleteAsync(key); - HashEntry[] hashEntries = new [] - { - new HashEntry("blah", DateTime.UtcNow.ToString("R")) - }; - ITransaction transaction = db.CreateTransaction(); - transaction.AddCondition(Condition.KeyNotExists(key)); - Task hashSetTask = transaction.HashSetAsync(key, hashEntries); - Task expireTask = transaction.KeyExpireAsync(key, TimeSpan.FromSeconds(30)); - bool committed = await transaction.ExecuteAsync(); - if (committed) - { - if (hashSetTask.IsCompleted) hashHit++; else hashMiss++; - if (expireTask.IsCompleted) expireHit++; else expireMiss++; - await hashSetTask; - await expireTask; - } + if (hashSetTask.IsCompleted) hashHit++; else hashMiss++; + if (expireTask.IsCompleted) expireHit++; else expireMiss++; + await hashSetTask; + await expireTask; } } - - Writer.WriteLine($"hash hit: {hashHit}, miss: {hashMiss}; expire hit: {expireHit}, miss: {expireMiss}"); - Assert.Equal(0, hashMiss); - Assert.Equal(0, expireMiss); } + + Writer.WriteLine($"hash hit: {hashHit}, miss: {hashMiss}; expire hit: {expireHit}, miss: {expireMiss}"); + Assert.Equal(0, hashMiss); + Assert.Equal(0, expireMiss); } } diff --git a/tests/StackExchange.Redis.Tests/Values.cs b/tests/StackExchange.Redis.Tests/Values.cs index 4d8732780..0d0b18c35 100644 --- a/tests/StackExchange.Redis.Tests/Values.cs +++ b/tests/StackExchange.Redis.Tests/Values.cs @@ -4,49 +4,48 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +public class Values : TestBase { - public class Values : TestBase + public Values(ITestOutputHelper output) : base (output) { } + + [Fact] + public void NullValueChecks() { - public Values(ITestOutputHelper output) : base (output) { } - - [Fact] - public void NullValueChecks() - { - RedisValue four = 4; - Assert.False(four.IsNull); - Assert.True(four.IsInteger); - Assert.True(four.HasValue); - Assert.False(four.IsNullOrEmpty); - - RedisValue n = default; - Assert.True(n.IsNull); - Assert.False(n.IsInteger); - Assert.False(n.HasValue); - Assert.True(n.IsNullOrEmpty); - - RedisValue emptyArr = Array.Empty(); - Assert.False(emptyArr.IsNull); - Assert.False(emptyArr.IsInteger); - Assert.False(emptyArr.HasValue); - Assert.True(emptyArr.IsNullOrEmpty); - } - - [Fact] - public void FromStream() - { - var arr = Encoding.UTF8.GetBytes("hello world"); - var ms = new MemoryStream(arr); - var val = RedisValue.CreateFrom(ms); - Assert.Equal("hello world", val); - - ms = new MemoryStream(arr, 1, 6, false, false); - val = RedisValue.CreateFrom(ms); - Assert.Equal("ello w", val); - - ms = new MemoryStream(arr, 2, 6, false, true); - val = RedisValue.CreateFrom(ms); - Assert.Equal("llo wo", val); - } + RedisValue four = 4; + Assert.False(four.IsNull); + Assert.True(four.IsInteger); + Assert.True(four.HasValue); + Assert.False(four.IsNullOrEmpty); + + RedisValue n = default; + Assert.True(n.IsNull); + Assert.False(n.IsInteger); + Assert.False(n.HasValue); + Assert.True(n.IsNullOrEmpty); + + RedisValue emptyArr = Array.Empty(); + Assert.False(emptyArr.IsNull); + Assert.False(emptyArr.IsInteger); + Assert.False(emptyArr.HasValue); + Assert.True(emptyArr.IsNullOrEmpty); + } + + [Fact] + public void FromStream() + { + var arr = Encoding.UTF8.GetBytes("hello world"); + var ms = new MemoryStream(arr); + var val = RedisValue.CreateFrom(ms); + Assert.Equal("hello world", val); + + ms = new MemoryStream(arr, 1, 6, false, false); + val = RedisValue.CreateFrom(ms); + Assert.Equal("ello w", val); + + ms = new MemoryStream(arr, 2, 6, false, true); + val = RedisValue.CreateFrom(ms); + Assert.Equal("llo wo", val); } } diff --git a/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs b/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs index b27ef389a..60cccdc7d 100644 --- a/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs +++ b/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs @@ -3,137 +3,134 @@ using Xunit; using Xunit.Abstractions; -namespace StackExchange.Redis.Tests +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class WithKeyPrefixTests : TestBase { - [Collection(SharedConnectionFixture.Key)] - public class WithKeyPrefixTests : TestBase + public WithKeyPrefixTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public void BlankPrefixYieldsSame_Bytes() { - public WithKeyPrefixTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + using var conn = Create(); - [Fact] - public void BlankPrefixYieldsSame_Bytes() - { - using (var conn = Create()) - { - var raw = conn.GetDatabase(); - var prefixed = raw.WithKeyPrefix(Array.Empty()); - Assert.Same(raw, prefixed); - } - } - - [Fact] - public void BlankPrefixYieldsSame_String() - { - using (var conn = Create()) - { - var raw = conn.GetDatabase(); - var prefixed = raw.WithKeyPrefix(""); - Assert.Same(raw, prefixed); - } - } - - [Fact] - public void NullPrefixIsError_Bytes() - { - Assert.Throws(() => - { - using var conn = Create(); - var raw = conn.GetDatabase(); - raw.WithKeyPrefix((byte[]?)null); - }); - } - - [Fact] - public void NullPrefixIsError_String() + var raw = conn.GetDatabase(); + var prefixed = raw.WithKeyPrefix(Array.Empty()); + Assert.Same(raw, prefixed); + } + + [Fact] + public void BlankPrefixYieldsSame_String() + { + using var conn = Create(); + + var raw = conn.GetDatabase(); + var prefixed = raw.WithKeyPrefix(""); + Assert.Same(raw, prefixed); + } + + [Fact] + public void NullPrefixIsError_Bytes() + { + Assert.Throws(() => { - Assert.Throws(() => - { - using var conn = Create(); - var raw = conn.GetDatabase(); - raw.WithKeyPrefix((string?)null); - }); - } - - [Theory] - [InlineData("abc")] - [InlineData("")] - [InlineData(null)] - public void NullDatabaseIsError(string prefix) + using var conn = Create(); + + var raw = conn.GetDatabase(); + raw.WithKeyPrefix((byte[]?)null); + }); + } + + [Fact] + public void NullPrefixIsError_String() + { + Assert.Throws(() => { - Assert.Throws(() => - { - IDatabase? raw = null; - raw!.WithKeyPrefix(prefix); - }); - } - - [Fact] - public void BasicSmokeTest() + using var conn = Create(); + + var raw = conn.GetDatabase(); + raw.WithKeyPrefix((string?)null); + }); + } + + [Theory] + [InlineData("abc")] + [InlineData("")] + [InlineData(null)] + public void NullDatabaseIsError(string prefix) + { + Assert.Throws(() => { - using (var conn = Create()) - { - var raw = conn.GetDatabase(); + IDatabase? raw = null; + raw!.WithKeyPrefix(prefix); + }); + } - var prefix = Me(); - var foo = raw.WithKeyPrefix(prefix); - var foobar = foo.WithKeyPrefix("bar"); + [Fact] + public void BasicSmokeTest() + { + using var conn = Create(); - string key = Me(); + var raw = conn.GetDatabase(); - string s = Guid.NewGuid().ToString(), t = Guid.NewGuid().ToString(); + var prefix = Me(); + var foo = raw.WithKeyPrefix(prefix); + var foobar = foo.WithKeyPrefix("bar"); - foo.StringSet(key, s, flags: CommandFlags.FireAndForget); - var val = (string?)foo.StringGet(key); - Assert.Equal(s, val); // fooBasicSmokeTest + string key = Me(); - foobar.StringSet(key, t, flags: CommandFlags.FireAndForget); - val = foobar.StringGet(key); - Assert.Equal(t, val); // foobarBasicSmokeTest + string s = Guid.NewGuid().ToString(), t = Guid.NewGuid().ToString(); - val = foo.StringGet("bar" + key); - Assert.Equal(t, val); // foobarBasicSmokeTest + foo.StringSet(key, s, flags: CommandFlags.FireAndForget); + var val = (string?)foo.StringGet(key); + Assert.Equal(s, val); // fooBasicSmokeTest - val = raw.StringGet(prefix + key); - Assert.Equal(s, val); // fooBasicSmokeTest + foobar.StringSet(key, t, flags: CommandFlags.FireAndForget); + val = foobar.StringGet(key); + Assert.Equal(t, val); // foobarBasicSmokeTest - val = raw.StringGet(prefix + "bar" + key); - Assert.Equal(t, val); // foobarBasicSmokeTest - } - } + val = foo.StringGet("bar" + key); + Assert.Equal(t, val); // foobarBasicSmokeTest - [Fact] - public void ConditionTest() - { - using (var conn = Create()) - { - var raw = conn.GetDatabase(); - - var prefix = Me() + ":"; - var foo = raw.WithKeyPrefix(prefix); - - raw.KeyDelete(prefix + "abc", CommandFlags.FireAndForget); - raw.KeyDelete(prefix + "i", CommandFlags.FireAndForget); - - // execute while key exists - raw.StringSet(prefix + "abc", "def", flags: CommandFlags.FireAndForget); - var tran = foo.CreateTransaction(); - tran.AddCondition(Condition.KeyExists("abc")); - tran.StringIncrementAsync("i"); - tran.Execute(); - - int i = (int)raw.StringGet(prefix + "i"); - Assert.Equal(1, i); - - // repeat without key - raw.KeyDelete(prefix + "abc", CommandFlags.FireAndForget); - tran = foo.CreateTransaction(); - tran.AddCondition(Condition.KeyExists("abc")); - tran.StringIncrementAsync("i"); - tran.Execute(); - - i = (int)raw.StringGet(prefix + "i"); - Assert.Equal(1, i); - } - } + val = raw.StringGet(prefix + key); + Assert.Equal(s, val); // fooBasicSmokeTest + + val = raw.StringGet(prefix + "bar" + key); + Assert.Equal(t, val); // foobarBasicSmokeTest + } + + [Fact] + public void ConditionTest() + { + using var conn = Create(); + + var raw = conn.GetDatabase(); + + var prefix = Me() + ":"; + var foo = raw.WithKeyPrefix(prefix); + + raw.KeyDelete(prefix + "abc", CommandFlags.FireAndForget); + raw.KeyDelete(prefix + "i", CommandFlags.FireAndForget); + + // execute while key exists + raw.StringSet(prefix + "abc", "def", flags: CommandFlags.FireAndForget); + var tran = foo.CreateTransaction(); + tran.AddCondition(Condition.KeyExists("abc")); + tran.StringIncrementAsync("i"); + tran.Execute(); + + int i = (int)raw.StringGet(prefix + "i"); + Assert.Equal(1, i); + + // repeat without key + raw.KeyDelete(prefix + "abc", CommandFlags.FireAndForget); + tran = foo.CreateTransaction(); + tran.AddCondition(Condition.KeyExists("abc")); + tran.StringIncrementAsync("i"); + tran.Execute(); + + i = (int)raw.StringGet(prefix + "i"); + Assert.Equal(1, i); } } diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index 3fb8b7b73..b29d36b1a 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -1258,6 +1258,5 @@ public void KeyTouchAsync_2() wrapper.KeyTouchAsync(keys, CommandFlags.None); mock.Verify(_ => _.KeyTouchAsync(It.Is(valid), CommandFlags.None)); } -#pragma warning restore RCS1047 // Non-asynchronous method name should not end with 'Async'. } } From 686c391675899e808d1acd2bc959c10bacfcef77 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 18 Apr 2022 10:19:39 -0400 Subject: [PATCH 137/435] Link to fuget.org on docs site (#2099) Adds a link to fuget.org for people who want to quickly go search/browse the latest package's API. --- docs/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index a57c3134f..f25ba5dea 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,8 @@ StackExchange.Redis =================== -[Release Notes](ReleaseNotes) +- [Release Notes](ReleaseNotes) +- [API Browser (via furget.org)](https://www.fuget.org/packages/StackExchange.Redis/) ## Overview From b8027fb26536727d1084160f67ca356912315e22 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 18 Apr 2022 22:29:40 -0400 Subject: [PATCH 138/435] Extensions: make internals...internal (#2102) This isn't a visibility change, just making public members of internal classes internal themselves to be clearer. Note: no public API changes in this...so loving that analyzer right now :) --- src/StackExchange.Redis/Enums/ExpireWhen.cs | 2 +- src/StackExchange.Redis/Enums/ListSide.cs | 2 +- src/StackExchange.Redis/Enums/Proxy.cs | 6 +++--- src/StackExchange.Redis/Enums/ServerType.cs | 4 ++-- src/StackExchange.Redis/Enums/SetOperation.cs | 2 +- src/StackExchange.Redis/Enums/SortedSetOrder.cs | 2 +- .../ExtensionMethods.Internal.cs | 4 ++-- src/StackExchange.Redis/Interfaces/IServer.cs | 2 +- src/StackExchange.Redis/TaskExtensions.cs | 14 +++++++------- src/StackExchange.Redis/TaskSource.cs | 2 +- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/StackExchange.Redis/Enums/ExpireWhen.cs b/src/StackExchange.Redis/Enums/ExpireWhen.cs index 7e6552a40..0ed3782bc 100644 --- a/src/StackExchange.Redis/Enums/ExpireWhen.cs +++ b/src/StackExchange.Redis/Enums/ExpireWhen.cs @@ -31,7 +31,7 @@ public enum ExpireWhen internal static class ExpiryOptionExtensions { - public static RedisValue ToLiteral(this ExpireWhen op) => op switch + internal static RedisValue ToLiteral(this ExpireWhen op) => op switch { ExpireWhen.HasNoExpiry => RedisLiterals.NX, ExpireWhen.HasExpiry => RedisLiterals.XX, diff --git a/src/StackExchange.Redis/Enums/ListSide.cs b/src/StackExchange.Redis/Enums/ListSide.cs index 0e71f91e8..dfb74383d 100644 --- a/src/StackExchange.Redis/Enums/ListSide.cs +++ b/src/StackExchange.Redis/Enums/ListSide.cs @@ -19,7 +19,7 @@ public enum ListSide internal static class ListSideExtensions { - public static RedisValue ToLiteral(this ListSide side) => side switch + internal static RedisValue ToLiteral(this ListSide side) => side switch { ListSide.Left => RedisLiterals.LEFT, ListSide.Right => RedisLiterals.RIGHT, diff --git a/src/StackExchange.Redis/Enums/Proxy.cs b/src/StackExchange.Redis/Enums/Proxy.cs index 59ab941cf..f529ac123 100644 --- a/src/StackExchange.Redis/Enums/Proxy.cs +++ b/src/StackExchange.Redis/Enums/Proxy.cs @@ -24,7 +24,7 @@ internal static class ProxyExtensions /// /// Whether a proxy supports databases (e.g. database > 0). /// - public static bool SupportsDatabases(this Proxy proxy) => proxy switch + internal static bool SupportsDatabases(this Proxy proxy) => proxy switch { Proxy.Twemproxy => false, Proxy.Envoyproxy => false, @@ -34,7 +34,7 @@ internal static class ProxyExtensions /// /// Whether a proxy supports pub/sub. /// - public static bool SupportsPubSub(this Proxy proxy) => proxy switch + internal static bool SupportsPubSub(this Proxy proxy) => proxy switch { Proxy.Twemproxy => false, Proxy.Envoyproxy => false, @@ -44,7 +44,7 @@ internal static class ProxyExtensions /// /// Whether a proxy supports the ConnectionMultiplexer.GetServer. /// - public static bool SupportsServerApi(this Proxy proxy) => proxy switch + internal static bool SupportsServerApi(this Proxy proxy) => proxy switch { Proxy.Twemproxy => false, Proxy.Envoyproxy => false, diff --git a/src/StackExchange.Redis/Enums/ServerType.cs b/src/StackExchange.Redis/Enums/ServerType.cs index 6414fc5ea..580bd8cee 100644 --- a/src/StackExchange.Redis/Enums/ServerType.cs +++ b/src/StackExchange.Redis/Enums/ServerType.cs @@ -32,7 +32,7 @@ internal static class ServerTypeExtensions /// /// Whether a server type can have only a single primary, meaning an election if multiple are found. /// - public static bool HasSinglePrimary(this ServerType type) => type switch + internal static bool HasSinglePrimary(this ServerType type) => type switch { ServerType.Envoyproxy => false, _ => true @@ -41,7 +41,7 @@ internal static class ServerTypeExtensions /// /// Whether a server type supports . /// - public static bool SupportsAutoConfigure(this ServerType type) => type switch + internal static bool SupportsAutoConfigure(this ServerType type) => type switch { ServerType.Twemproxy => false, ServerType.Envoyproxy => false, diff --git a/src/StackExchange.Redis/Enums/SetOperation.cs b/src/StackExchange.Redis/Enums/SetOperation.cs index f7cf4f1df..7e649847f 100644 --- a/src/StackExchange.Redis/Enums/SetOperation.cs +++ b/src/StackExchange.Redis/Enums/SetOperation.cs @@ -23,7 +23,7 @@ public enum SetOperation internal static class SetOperationExtensions { - public static RedisCommand ToCommand(this SetOperation operation, bool store) => operation switch + internal static RedisCommand ToCommand(this SetOperation operation, bool store) => operation switch { SetOperation.Intersect when store => RedisCommand.ZINTERSTORE, SetOperation.Intersect => RedisCommand.ZINTER, diff --git a/src/StackExchange.Redis/Enums/SortedSetOrder.cs b/src/StackExchange.Redis/Enums/SortedSetOrder.cs index afb389d91..6c205bae0 100644 --- a/src/StackExchange.Redis/Enums/SortedSetOrder.cs +++ b/src/StackExchange.Redis/Enums/SortedSetOrder.cs @@ -23,7 +23,7 @@ public enum SortedSetOrder internal static class SortedSetOrderByExtensions { - public static RedisValue GetLiteral(this SortedSetOrder sortedSetOrder) => sortedSetOrder switch + internal static RedisValue GetLiteral(this SortedSetOrder sortedSetOrder) => sortedSetOrder switch { SortedSetOrder.ByLex => RedisLiterals.BYLEX, SortedSetOrder.ByScore => RedisLiterals.BYSCORE, diff --git a/src/StackExchange.Redis/ExtensionMethods.Internal.cs b/src/StackExchange.Redis/ExtensionMethods.Internal.cs index e4903583b..6b29c4d45 100644 --- a/src/StackExchange.Redis/ExtensionMethods.Internal.cs +++ b/src/StackExchange.Redis/ExtensionMethods.Internal.cs @@ -4,10 +4,10 @@ namespace StackExchange.Redis { internal static class ExtensionMethodsInternal { - public static bool IsNullOrEmpty([NotNullWhen(false)] this string? s) => + internal static bool IsNullOrEmpty([NotNullWhen(false)] this string? s) => string.IsNullOrEmpty(s); - public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? s) => + internal static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? s) => string.IsNullOrWhiteSpace(s); } } diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 4df6b937d..377f52358 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -1108,6 +1108,6 @@ internal static class IServerExtensions /// /// The server to simulate failure on. /// The type of failure(s) to simulate. - public static void SimulateConnectionFailure(this IServer server, SimulatedFailureType failureType) => (server as RedisServer)?.SimulateConnectionFailure(failureType); + internal static void SimulateConnectionFailure(this IServer server, SimulatedFailureType failureType) => (server as RedisServer)?.SimulateConnectionFailure(failureType); } } diff --git a/src/StackExchange.Redis/TaskExtensions.cs b/src/StackExchange.Redis/TaskExtensions.cs index 397422064..12682e21e 100644 --- a/src/StackExchange.Redis/TaskExtensions.cs +++ b/src/StackExchange.Redis/TaskExtensions.cs @@ -13,33 +13,33 @@ private static void ObverveErrors(this Task task) if (task != null) GC.KeepAlive(task.Exception); } - public static Task ObserveErrors(this Task task) + internal static Task ObserveErrors(this Task task) { task.ContinueWith(observeErrors, TaskContinuationOptions.OnlyOnFaulted); return task; } - public static Task ObserveErrors(this Task task) + internal static Task ObserveErrors(this Task task) { task.ContinueWith(observeErrors, TaskContinuationOptions.OnlyOnFaulted); return task; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ConfiguredTaskAwaitable ForAwait(this Task task) => task.ConfigureAwait(false); + internal static ConfiguredTaskAwaitable ForAwait(this Task task) => task.ConfigureAwait(false); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ConfiguredValueTaskAwaitable ForAwait(this in ValueTask task) => task.ConfigureAwait(false); + internal static ConfiguredValueTaskAwaitable ForAwait(this in ValueTask task) => task.ConfigureAwait(false); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ConfiguredTaskAwaitable ForAwait(this Task task) => task.ConfigureAwait(false); + internal static ConfiguredTaskAwaitable ForAwait(this Task task) => task.ConfigureAwait(false); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ConfiguredValueTaskAwaitable ForAwait(this in ValueTask task) => task.ConfigureAwait(false); + internal static ConfiguredValueTaskAwaitable ForAwait(this in ValueTask task) => task.ConfigureAwait(false); internal static void RedisFireAndForget(this Task task) => task?.ContinueWith(t => GC.KeepAlive(t.Exception), TaskContinuationOptions.OnlyOnFaulted); // Inspired from https://github.com/dotnet/corefx/blob/81a246f3adf1eece3d981f1d8bb8ae9de12de9c6/src/Common/tests/System/Threading/Tasks/TaskTimeoutExtensions.cs#L15-L43 // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - public static async Task TimeoutAfter(this Task task, int timeoutMs) + internal static async Task TimeoutAfter(this Task task, int timeoutMs) { var cts = new CancellationTokenSource(); if (task == await Task.WhenAny(task, Task.Delay(timeoutMs, cts.Token)).ForAwait()) diff --git a/src/StackExchange.Redis/TaskSource.cs b/src/StackExchange.Redis/TaskSource.cs index 737366c03..00f83cb04 100644 --- a/src/StackExchange.Redis/TaskSource.cs +++ b/src/StackExchange.Redis/TaskSource.cs @@ -10,7 +10,7 @@ internal static class TaskSource /// The type for the created . /// The state for the created . /// The options to apply to the task. - public static TaskCompletionSource Create(object? asyncState, TaskCreationOptions options = TaskCreationOptions.None) + internal static TaskCompletionSource Create(object? asyncState, TaskCreationOptions options = TaskCreationOptions.None) => new TaskCompletionSource(asyncState, options); } } From ecea7f4ff003934498f843d06634848bf70a6544 Mon Sep 17 00:00:00 2001 From: Steve Lorello <42971704+slorello89@users.noreply.github.com> Date: Tue, 19 Apr 2022 11:06:55 -0400 Subject: [PATCH 139/435] Updating IsPrimaryOnly command map for new commands (#2101) @NickCraver, working on `BITFIELD`/`BITFIELD_RO` and I realized that several of the new commands we've been adding are write-commands and therefore won't work on replicas, opening this to fix the command-map in Message.cs (pretty sure this is the right place but LMK if I'm offbase here) will need to push an update to #2095 #2094 to reflect this as well. Co-authored-by: Nick Craver --- src/StackExchange.Redis/Enums/RedisCommand.cs | 698 ++++++++++++------ src/StackExchange.Redis/ExceptionFactory.cs | 5 + src/StackExchange.Redis/Message.cs | 85 +-- .../ExceptionFactoryTests.cs | 15 + tests/StackExchange.Redis.Tests/Naming.cs | 18 +- tests/StackExchange.Redis.Tests/SortedSets.cs | 16 + tests/StackExchange.Redis.Tests/Streams.cs | 88 +++ 7 files changed, 603 insertions(+), 322 deletions(-) diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index f7ceb8a1b..e2cc9603c 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -1,236 +1,470 @@ -namespace StackExchange.Redis +using System; + +namespace StackExchange.Redis; + +internal enum RedisCommand +{ + NONE, // must be first for "zero reasons" + + APPEND, + ASKING, + AUTH, + + BGREWRITEAOF, + BGSAVE, + BITCOUNT, + BITOP, + BITPOS, + BLPOP, + BRPOP, + BRPOPLPUSH, + + CLIENT, + CLUSTER, + CONFIG, + COPY, + + DBSIZE, + DEBUG, + DECR, + DECRBY, + DEL, + DISCARD, + DUMP, + + ECHO, + EVAL, + EVALSHA, + EXEC, + EXISTS, + EXPIRE, + EXPIREAT, + EXPIRETIME, + + FLUSHALL, + FLUSHDB, + + GEOADD, + GEODIST, + GEOHASH, + GEOPOS, + GEORADIUS, + GEORADIUSBYMEMBER, + GEOSEARCH, + GEOSEARCHSTORE, + + GET, + GETBIT, + GETDEL, + GETEX, + GETRANGE, + GETSET, + + HDEL, + HEXISTS, + HGET, + HGETALL, + HINCRBY, + HINCRBYFLOAT, + HKEYS, + HLEN, + HMGET, + HMSET, + HRANDFIELD, + HSCAN, + HSET, + HSETNX, + HSTRLEN, + HVALS, + + INCR, + INCRBY, + INCRBYFLOAT, + INFO, + + KEYS, + + LASTSAVE, + LATENCY, + LINDEX, + LINSERT, + LLEN, + LMOVE, + LPOP, + LPOS, + LPUSH, + LPUSHX, + LRANGE, + LREM, + LSET, + LTRIM, + + MEMORY, + MGET, + MIGRATE, + MONITOR, + MOVE, + MSET, + MSETNX, + MULTI, + + OBJECT, + + PERSIST, + PEXPIRE, + PEXPIREAT, + PEXPIRETIME, + PFADD, + PFCOUNT, + PFMERGE, + PING, + PSETEX, + PSUBSCRIBE, + PTTL, + PUBLISH, + PUBSUB, + PUNSUBSCRIBE, + + QUIT, + + RANDOMKEY, + READONLY, + READWRITE, + RENAME, + RENAMENX, + REPLICAOF, + RESTORE, + ROLE, + RPOP, + RPOPLPUSH, + RPUSH, + RPUSHX, + + SADD, + SAVE, + SCAN, + SCARD, + SCRIPT, + SDIFF, + SDIFFSTORE, + SELECT, + SENTINEL, + SET, + SETBIT, + SETEX, + SETNX, + SETRANGE, + SHUTDOWN, + SINTER, + SINTERCARD, + SINTERSTORE, + SISMEMBER, + SLAVEOF, + SLOWLOG, + SMEMBERS, + SMISMEMBER, + SMOVE, + SORT, + SPOP, + SRANDMEMBER, + SREM, + STRLEN, + SUBSCRIBE, + SUNION, + SUNIONSTORE, + SSCAN, + SWAPDB, + SYNC, + + TIME, + TOUCH, + TTL, + TYPE, + + UNLINK, + UNSUBSCRIBE, + UNWATCH, + + WATCH, + + XACK, + XADD, + XCLAIM, + XDEL, + XGROUP, + XINFO, + XLEN, + XPENDING, + XRANGE, + XREAD, + XREADGROUP, + XREVRANGE, + XTRIM, + + ZADD, + ZCARD, + ZCOUNT, + ZDIFF, + ZDIFFSTORE, + ZINCRBY, + ZINTER, + ZINTERCARD, + ZINTERSTORE, + ZLEXCOUNT, + ZMSCORE, + ZPOPMAX, + ZPOPMIN, + ZRANDMEMBER, + ZRANGE, + ZRANGEBYLEX, + ZRANGEBYSCORE, + ZRANGESTORE, + ZRANK, + ZREM, + ZREMRANGEBYLEX, + ZREMRANGEBYRANK, + ZREMRANGEBYSCORE, + ZREVRANGE, + ZREVRANGEBYLEX, + ZREVRANGEBYSCORE, + ZREVRANK, + ZSCAN, + ZSCORE, + ZUNION, + ZUNIONSTORE, + + UNKNOWN, +} + +internal static class RedisCommandExtensions { - internal enum RedisCommand + /// + /// Gets whether a given command can be issued only to a primary, or if any server is eligible. + /// + /// The to check. + /// if the command is primary-only, otherwise. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "No, it'd be ridiculous.")] + internal static bool IsPrimaryOnly(this RedisCommand command) { - NONE, // must be first for "zero reasons" - - APPEND, - ASKING, - AUTH, - - BGREWRITEAOF, - BGSAVE, - BITCOUNT, - BITOP, - BITPOS, - BLPOP, - BRPOP, - BRPOPLPUSH, - - CLIENT, - CLUSTER, - CONFIG, - COPY, - - DBSIZE, - DEBUG, - DECR, - DECRBY, - DEL, - DISCARD, - DUMP, - - ECHO, - EVAL, - EVALSHA, - EXEC, - EXISTS, - EXPIRE, - EXPIREAT, - EXPIRETIME, - - FLUSHALL, - FLUSHDB, - - GEOADD, - GEODIST, - GEOHASH, - GEOPOS, - GEORADIUS, - GEORADIUSBYMEMBER, - GEOSEARCH, - GEOSEARCHSTORE, - - GET, - GETBIT, - GETDEL, - GETEX, - GETRANGE, - GETSET, - - HDEL, - HEXISTS, - HGET, - HGETALL, - HINCRBY, - HINCRBYFLOAT, - HKEYS, - HLEN, - HMGET, - HMSET, - HRANDFIELD, - HSCAN, - HSET, - HSETNX, - HSTRLEN, - HVALS, - - INCR, - INCRBY, - INCRBYFLOAT, - INFO, - - KEYS, - - LASTSAVE, - LATENCY, - LINDEX, - LINSERT, - LLEN, - LMOVE, - LPOP, - LPOS, - LPUSH, - LPUSHX, - LRANGE, - LREM, - LSET, - LTRIM, - - MEMORY, - MGET, - MIGRATE, - MONITOR, - MOVE, - MSET, - MSETNX, - MULTI, - - OBJECT, - - PERSIST, - PEXPIRE, - PEXPIREAT, - PEXPIRETIME, - PFADD, - PFCOUNT, - PFMERGE, - PING, - PSETEX, - PSUBSCRIBE, - PTTL, - PUBLISH, - PUBSUB, - PUNSUBSCRIBE, - - QUIT, - - RANDOMKEY, - READONLY, - READWRITE, - RENAME, - RENAMENX, - REPLICAOF, - RESTORE, - ROLE, - RPOP, - RPOPLPUSH, - RPUSH, - RPUSHX, - - SADD, - SAVE, - SCAN, - SCARD, - SCRIPT, - SDIFF, - SDIFFSTORE, - SELECT, - SENTINEL, - SET, - SETBIT, - SETEX, - SETNX, - SETRANGE, - SHUTDOWN, - SINTER, - SINTERCARD, - SINTERSTORE, - SISMEMBER, - SLAVEOF, - SLOWLOG, - SMEMBERS, - SMISMEMBER, - SMOVE, - SORT, - SPOP, - SRANDMEMBER, - SREM, - STRLEN, - SUBSCRIBE, - SUNION, - SUNIONSTORE, - SSCAN, - SWAPDB, - SYNC, - - TIME, - TOUCH, - TTL, - TYPE, - - UNLINK, - UNSUBSCRIBE, - UNWATCH, - - WATCH, - - XACK, - XADD, - XCLAIM, - XDEL, - XGROUP, - XINFO, - XLEN, - XPENDING, - XRANGE, - XREAD, - XREADGROUP, - XREVRANGE, - XTRIM, - - ZADD, - ZCARD, - ZCOUNT, - ZDIFF, - ZDIFFSTORE, - ZINCRBY, - ZINTER, - ZINTERCARD, - ZINTERSTORE, - ZLEXCOUNT, - ZMSCORE, - ZPOPMAX, - ZPOPMIN, - ZRANDMEMBER, - ZRANGE, - ZRANGEBYLEX, - ZRANGEBYSCORE, - ZRANGESTORE, - ZRANK, - ZREM, - ZREMRANGEBYLEX, - ZREMRANGEBYRANK, - ZREMRANGEBYSCORE, - ZREVRANGE, - ZREVRANGEBYLEX, - ZREVRANGEBYSCORE, - ZREVRANK, - ZSCAN, - ZSCORE, - ZUNION, - ZUNIONSTORE, - - UNKNOWN, + switch (command) + { + // Commands that can only be issued to a primary (writable) server + // If a command *may* be writable (e.g. an EVAL script), it should *not* be primary-only + // because that'd block a legitimate use case of a read-only script on replica servers, + // for example spreading load via a .DemandReplica flag in the caller. + // Basically: would it fail on a read-only replica in 100% of cases? Then it goes in the list. + case RedisCommand.APPEND: + case RedisCommand.BITOP: + case RedisCommand.BLPOP: + case RedisCommand.BRPOP: + case RedisCommand.BRPOPLPUSH: + case RedisCommand.COPY: + case RedisCommand.DECR: + case RedisCommand.DECRBY: + case RedisCommand.DEL: + case RedisCommand.EXPIRE: + case RedisCommand.EXPIREAT: + case RedisCommand.EXPIRETIME: + case RedisCommand.FLUSHALL: + case RedisCommand.FLUSHDB: + case RedisCommand.GEOADD: + case RedisCommand.GEOSEARCHSTORE: + case RedisCommand.GETDEL: + case RedisCommand.GETEX: + case RedisCommand.GETSET: + case RedisCommand.HDEL: + case RedisCommand.HINCRBY: + case RedisCommand.HINCRBYFLOAT: + case RedisCommand.HMSET: + case RedisCommand.HSET: + case RedisCommand.HSETNX: + case RedisCommand.INCR: + case RedisCommand.INCRBY: + case RedisCommand.INCRBYFLOAT: + case RedisCommand.LINSERT: + case RedisCommand.LMOVE: + case RedisCommand.LPOP: + case RedisCommand.LPUSH: + case RedisCommand.LPUSHX: + case RedisCommand.LREM: + case RedisCommand.LSET: + case RedisCommand.LTRIM: + case RedisCommand.MIGRATE: + case RedisCommand.MOVE: + case RedisCommand.MSET: + case RedisCommand.MSETNX: + case RedisCommand.PERSIST: + case RedisCommand.PEXPIRE: + case RedisCommand.PEXPIREAT: + case RedisCommand.PEXPIRETIME: + case RedisCommand.PFADD: + case RedisCommand.PFMERGE: + case RedisCommand.PSETEX: + case RedisCommand.RENAME: + case RedisCommand.RENAMENX: + case RedisCommand.RESTORE: + case RedisCommand.RPOP: + case RedisCommand.RPOPLPUSH: + case RedisCommand.RPUSH: + case RedisCommand.RPUSHX: + case RedisCommand.SADD: + case RedisCommand.SDIFFSTORE: + case RedisCommand.SET: + case RedisCommand.SETBIT: + case RedisCommand.SETEX: + case RedisCommand.SETNX: + case RedisCommand.SETRANGE: + case RedisCommand.SINTERSTORE: + case RedisCommand.SMOVE: + case RedisCommand.SPOP: + case RedisCommand.SREM: + case RedisCommand.SUNIONSTORE: + case RedisCommand.SWAPDB: + case RedisCommand.TOUCH: + case RedisCommand.UNLINK: + case RedisCommand.XACK: + case RedisCommand.XADD: + case RedisCommand.XCLAIM: + case RedisCommand.XDEL: + case RedisCommand.XGROUP: + case RedisCommand.XREADGROUP: + case RedisCommand.XTRIM: + case RedisCommand.ZADD: + case RedisCommand.ZDIFFSTORE: + case RedisCommand.ZINTERSTORE: + case RedisCommand.ZINCRBY: + case RedisCommand.ZPOPMAX: + case RedisCommand.ZPOPMIN: + case RedisCommand.ZRANGESTORE: + case RedisCommand.ZREM: + case RedisCommand.ZREMRANGEBYLEX: + case RedisCommand.ZREMRANGEBYRANK: + case RedisCommand.ZREMRANGEBYSCORE: + case RedisCommand.ZUNIONSTORE: + return true; + // Commands that can be issued anywhere + case RedisCommand.NONE: + case RedisCommand.ASKING: + case RedisCommand.AUTH: + case RedisCommand.BGREWRITEAOF: + case RedisCommand.BGSAVE: + case RedisCommand.BITCOUNT: + case RedisCommand.BITPOS: + case RedisCommand.CLIENT: + case RedisCommand.CLUSTER: + case RedisCommand.CONFIG: + case RedisCommand.DBSIZE: + case RedisCommand.DEBUG: + case RedisCommand.DISCARD: + case RedisCommand.DUMP: + case RedisCommand.ECHO: + case RedisCommand.EVAL: + case RedisCommand.EVALSHA: + case RedisCommand.EXEC: + case RedisCommand.EXISTS: + case RedisCommand.GEODIST: + case RedisCommand.GEOHASH: + case RedisCommand.GEOPOS: + case RedisCommand.GEORADIUS: + case RedisCommand.GEORADIUSBYMEMBER: + case RedisCommand.GEOSEARCH: + case RedisCommand.GET: + case RedisCommand.GETBIT: + case RedisCommand.GETRANGE: + case RedisCommand.HEXISTS: + case RedisCommand.HGET: + case RedisCommand.HGETALL: + case RedisCommand.HKEYS: + case RedisCommand.HLEN: + case RedisCommand.HMGET: + case RedisCommand.HRANDFIELD: + case RedisCommand.HSCAN: + case RedisCommand.HSTRLEN: + case RedisCommand.HVALS: + case RedisCommand.INFO: + case RedisCommand.KEYS: + case RedisCommand.LASTSAVE: + case RedisCommand.LATENCY: + case RedisCommand.LINDEX: + case RedisCommand.LLEN: + case RedisCommand.LPOS: + case RedisCommand.LRANGE: + case RedisCommand.MEMORY: + case RedisCommand.MGET: + case RedisCommand.MONITOR: + case RedisCommand.MULTI: + case RedisCommand.OBJECT: + case RedisCommand.PFCOUNT: + case RedisCommand.PING: + case RedisCommand.PSUBSCRIBE: + case RedisCommand.PTTL: + case RedisCommand.PUBLISH: + case RedisCommand.PUBSUB: + case RedisCommand.PUNSUBSCRIBE: + case RedisCommand.QUIT: + case RedisCommand.RANDOMKEY: + case RedisCommand.READONLY: + case RedisCommand.READWRITE: + case RedisCommand.REPLICAOF: + case RedisCommand.ROLE: + case RedisCommand.SAVE: + case RedisCommand.SCAN: + case RedisCommand.SCARD: + case RedisCommand.SCRIPT: + case RedisCommand.SDIFF: + case RedisCommand.SELECT: + case RedisCommand.SENTINEL: + case RedisCommand.SHUTDOWN: + case RedisCommand.SINTER: + case RedisCommand.SINTERCARD: + case RedisCommand.SISMEMBER: + case RedisCommand.SLAVEOF: + case RedisCommand.SLOWLOG: + case RedisCommand.SMEMBERS: + case RedisCommand.SMISMEMBER: + case RedisCommand.SORT: + case RedisCommand.SRANDMEMBER: + case RedisCommand.STRLEN: + case RedisCommand.SUBSCRIBE: + case RedisCommand.SUNION: + case RedisCommand.SSCAN: + case RedisCommand.SYNC: + case RedisCommand.TIME: + case RedisCommand.TTL: + case RedisCommand.TYPE: + case RedisCommand.UNSUBSCRIBE: + case RedisCommand.UNWATCH: + case RedisCommand.WATCH: + // Stream commands verified working on replicas + case RedisCommand.XINFO: + case RedisCommand.XLEN: + case RedisCommand.XPENDING: + case RedisCommand.XRANGE: + case RedisCommand.XREAD: + case RedisCommand.XREVRANGE: + case RedisCommand.ZCARD: + case RedisCommand.ZCOUNT: + case RedisCommand.ZDIFF: + case RedisCommand.ZINTER: + case RedisCommand.ZINTERCARD: + case RedisCommand.ZLEXCOUNT: + case RedisCommand.ZMSCORE: + case RedisCommand.ZRANDMEMBER: + case RedisCommand.ZRANGE: + case RedisCommand.ZRANGEBYLEX: + case RedisCommand.ZRANGEBYSCORE: + case RedisCommand.ZRANK: + case RedisCommand.ZREVRANGE: + case RedisCommand.ZREVRANGEBYLEX: + case RedisCommand.ZREVRANGEBYSCORE: + case RedisCommand.ZREVRANK: + case RedisCommand.ZSCAN: + case RedisCommand.ZSCORE: + case RedisCommand.ZUNION: + case RedisCommand.UNKNOWN: + return false; + default: + throw new ArgumentOutOfRangeException(nameof(command), $"Every RedisCommand must be defined in Message.IsPrimaryOnly, unknown command '{command}' encountered."); + } } } diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 7200a3a06..2c0f2b5ca 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -124,6 +124,11 @@ internal static Exception NoConnectionAvailable( // This can happen in cloud environments often, where user disables abort and has the wrong config initialMessage = $"Connection to Redis never succeeded (attempts: {attempts} - check your config), unable to service operation: "; } + else if (message is not null && message.IsPrimaryOnly() && multiplexer.IsConnected) + { + // If we know it's a primary-only command, indicate that in the error message + initialMessage = "No connection (requires writable - not eligible for replica) is active/available to service this operation: "; + } else { // Default if we don't have a more useful error message here based on circumstances diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 7efd5fe6c..f44a395d5 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -106,7 +106,7 @@ protected Message(int db, CommandFlags flags, RedisCommand command) } } - bool primaryOnly = IsPrimaryOnly(command); + bool primaryOnly = command.IsPrimaryOnly(); Db = db; this.command = command; Flags = flags & UserSelectableFlags; @@ -324,89 +324,6 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i public static Message CreateInSlot(int db, int slot, CommandFlags flags, RedisCommand command, RedisValue[] values) => new CommandSlotValuesMessage(db, slot, flags, command, values); - public static bool IsPrimaryOnly(RedisCommand command) - { - switch (command) - { - case RedisCommand.APPEND: - case RedisCommand.BITOP: - case RedisCommand.BLPOP: - case RedisCommand.BRPOP: - case RedisCommand.BRPOPLPUSH: - case RedisCommand.DECR: - case RedisCommand.DECRBY: - case RedisCommand.DEL: - case RedisCommand.EXPIRE: - case RedisCommand.EXPIREAT: - case RedisCommand.FLUSHALL: - case RedisCommand.FLUSHDB: - case RedisCommand.GETDEL: - case RedisCommand.GETEX: - case RedisCommand.GETSET: - case RedisCommand.HDEL: - case RedisCommand.HINCRBY: - case RedisCommand.HINCRBYFLOAT: - case RedisCommand.HMSET: - case RedisCommand.HSET: - case RedisCommand.HSETNX: - case RedisCommand.INCR: - case RedisCommand.INCRBY: - case RedisCommand.INCRBYFLOAT: - case RedisCommand.LINSERT: - case RedisCommand.LPOP: - case RedisCommand.LPUSH: - case RedisCommand.LPUSHX: - case RedisCommand.LREM: - case RedisCommand.LSET: - case RedisCommand.LTRIM: - case RedisCommand.MIGRATE: - case RedisCommand.MOVE: - case RedisCommand.MSET: - case RedisCommand.MSETNX: - case RedisCommand.PERSIST: - case RedisCommand.PEXPIRE: - case RedisCommand.PEXPIREAT: - case RedisCommand.PFADD: - case RedisCommand.PFMERGE: - case RedisCommand.PSETEX: - case RedisCommand.RENAME: - case RedisCommand.RENAMENX: - case RedisCommand.RESTORE: - case RedisCommand.RPOP: - case RedisCommand.RPOPLPUSH: - case RedisCommand.RPUSH: - case RedisCommand.RPUSHX: - case RedisCommand.SADD: - case RedisCommand.SDIFFSTORE: - case RedisCommand.SET: - case RedisCommand.SETBIT: - case RedisCommand.SETEX: - case RedisCommand.SETNX: - case RedisCommand.SETRANGE: - case RedisCommand.SINTERSTORE: - case RedisCommand.SMOVE: - case RedisCommand.SPOP: - case RedisCommand.SREM: - case RedisCommand.SUNIONSTORE: - case RedisCommand.SWAPDB: - case RedisCommand.TOUCH: - case RedisCommand.UNLINK: - case RedisCommand.ZADD: - case RedisCommand.ZINTERSTORE: - case RedisCommand.ZINCRBY: - case RedisCommand.ZPOPMAX: - case RedisCommand.ZPOPMIN: - case RedisCommand.ZREM: - case RedisCommand.ZREMRANGEBYLEX: - case RedisCommand.ZREMRANGEBYRANK: - case RedisCommand.ZREMRANGEBYSCORE: - case RedisCommand.ZUNIONSTORE: - return true; - default: - return false; - } - } - /// Gets whether this is primary-only. /// /// Note that the constructor runs the switch statement above, so diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 1098a034c..cb34aa19c 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -208,6 +208,21 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple } } + [Fact] + public void NoConnectionPrimaryOnlyException() + { + using var conn = ConnectionMultiplexer.Connect(TestConfig.Current.ReplicaServerAndPort, Writer); + + var msg = Message.Create(0, CommandFlags.None, RedisCommand.SET, (RedisKey)Me(), (RedisValue)"test"); + Assert.True(msg.IsPrimaryOnly()); + var rawEx = ExceptionFactory.NoConnectionAvailable(conn, msg, null); + var ex = Assert.IsType(rawEx); + Writer.WriteLine("Exception: " + ex.Message); + + // Ensure a primary-only operation like SET gives the additional context + Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available to service this operation: SET", ex.Message); + } + [Theory] [InlineData(true, ConnectionFailureType.ProtocolFailure, "ProtocolFailure on [0]:GET myKey (StringProcessor), my annotation")] [InlineData(true, ConnectionFailureType.ConnectionDisposed, "ConnectionDisposed on [0]:GET myKey (StringProcessor), my annotation")] diff --git a/tests/StackExchange.Redis.Tests/Naming.cs b/tests/StackExchange.Redis.Tests/Naming.cs index 224d09b5f..55997848c 100644 --- a/tests/StackExchange.Redis.Tests/Naming.cs +++ b/tests/StackExchange.Redis.Tests/Naming.cs @@ -28,14 +28,17 @@ public void CheckSignatures(Type type, bool isAsync) } } + /// + /// This test iterates over all s to ensure we have everything accounted for as primary-only or not. + /// [Fact] - public void ShowReadOnlyOperations() + public void CheckReadOnlyOperations() { - List primaryReplica = new List(); - List primaryOnly = new List(); + List primaryReplica = new(), + primaryOnly = new(); foreach (var val in (RedisCommand[])Enum.GetValues(typeof(RedisCommand))) { - bool isPrimaryOnly = Message.IsPrimaryOnly(val); + bool isPrimaryOnly = val.IsPrimaryOnly(); (isPrimaryOnly ? primaryOnly : primaryReplica).Add(val); if (!isPrimaryOnly) @@ -43,18 +46,21 @@ public void ShowReadOnlyOperations() Log(val.ToString()); } } + // Ensure an unknown command from nowhere would violate the check above, as any not-yet-added one would. + Assert.Throws(() => ((RedisCommand)99999).IsPrimaryOnly()); + Log("primary-only: {0}, vs primary/replica: {1}", primaryOnly.Count, primaryReplica.Count); Log(""); Log("primary-only:"); foreach (var val in primaryOnly) { - Log(val?.ToString()); + Log(val.ToString()); } Log(""); Log("primary/replica:"); foreach (var val in primaryReplica) { - Log(val?.ToString()); + Log(val.ToString()); } } diff --git a/tests/StackExchange.Redis.Tests/SortedSets.cs b/tests/StackExchange.Redis.Tests/SortedSets.cs index 0a7f76d9d..b0eca7a87 100644 --- a/tests/StackExchange.Redis.Tests/SortedSets.cs +++ b/tests/StackExchange.Redis.Tests/SortedSets.cs @@ -966,6 +966,22 @@ public void SortedSetRangeStoreFailExclude() Assert.Equal("exclude", exception.ParamName); } + [Fact] + public void SortedSetRangeStoreFailForReplica() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var me = Me(); + var sourceKey = $"{me}:ZSetSource"; + var destinationKey = $"{me}:ZSetDestination"; + + db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); + var exception = Assert.Throws(() => db.SortedSetRangeAndStore(sourceKey, destinationKey, 0, -1, flags: CommandFlags.DemandReplica)); + Assert.Contains("Command cannot be issued to a replica", exception.Message); + } + [Fact] public void SortedSetScoresSingle() { diff --git a/tests/StackExchange.Redis.Tests/Streams.cs b/tests/StackExchange.Redis.Tests/Streams.cs index adb5511dd..c946b1cf7 100644 --- a/tests/StackExchange.Redis.Tests/Streams.cs +++ b/tests/StackExchange.Redis.Tests/Streams.cs @@ -26,6 +26,94 @@ public void IsStreamType() Assert.Equal(RedisType.Stream, keyType); } + [Fact] + public void StreamOpsFailOnReplica() + { + using var conn = Create(configuration: TestConfig.Current.PrimaryServerAndPort, require: RedisFeatures.v5_0_0); + using var replicaConn = Create(configuration: TestConfig.Current.ReplicaServerAndPort, require: RedisFeatures.v5_0_0); + + var db = conn.GetDatabase(); + var replicaDb = replicaConn.GetDatabase(); + + // XADD: Works on primary, not secondary + db.StreamAdd(GetUniqueKey("auto_id"), "field1", "value1"); + var ex = Assert.Throws(() => replicaDb.StreamAdd(GetUniqueKey("auto_id"), "field1", "value1")); + Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available", ex.Message); + + // Add stream content to primary + var key = GetUniqueKey("group_ack"); + const string groupName1 = "test_group1", + groupName2 = "test_group2", + consumer1 = "test_consumer1", + consumer2 = "test_consumer2"; + + // Add for primary + var id1 = db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); + var id3 = db.StreamAdd(key, "field3", "value3"); + var id4 = db.StreamAdd(key, "field4", "value4"); + + // XGROUP: Works on primary, not replica + db.StreamCreateConsumerGroup(key, groupName1, StreamPosition.Beginning); + ex = Assert.Throws(() => replicaDb.StreamCreateConsumerGroup(key, groupName2, StreamPosition.Beginning)); + Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available", ex.Message); + + // Create the second group on the primary, for the rest of the tests. + db.StreamCreateConsumerGroup(key, groupName2, StreamPosition.Beginning); + + // XREADGROUP: Works on primary, not replica + // Read all 4 messages, they will be assigned to the consumer + var entries = db.StreamReadGroup(key, groupName1, consumer1, StreamPosition.NewMessages); + ex = Assert.Throws(() => replicaDb.StreamReadGroup(key, groupName2, consumer2, StreamPosition.NewMessages)); + Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available", ex.Message); + + // XACK: Works on primary, not secondary + var oneAck = db.StreamAcknowledge(key, groupName1, id1); + ex = Assert.Throws(() => replicaDb.StreamAcknowledge(key, groupName2, id1)); + Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available", ex.Message); + + // XPENDING: Works on primary and replica + // Get the pending messages for consumer2. + var pendingMessages = db.StreamPendingMessages(key, groupName1, 10, consumer1); + var pendingMessages2 = replicaDb.StreamPendingMessages(key, groupName2, 10, consumer2); + + // XCLAIM: Works on primary, not replica + // Claim the messages for consumer1. + var messages = db.StreamClaim(key, groupName1, consumer1, 0, messageIds: pendingMessages.Select(pm => pm.MessageId).ToArray()); + ex = Assert.Throws(() => replicaDb.StreamClaim(key, groupName2, consumer2, 0, messageIds: pendingMessages.Select(pm => pm.MessageId).ToArray())); + Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available", ex.Message); + + // XDEL: Works on primary, not replica + db.StreamDelete(key, new RedisValue[] { id4 }); + ex = Assert.Throws(() => replicaDb.StreamDelete(key, new RedisValue[] { id3 })); + Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available", ex.Message); + + // XINFO: Works on primary and replica + db.StreamInfo(key); + replicaDb.StreamInfo(key); + + // XLEN: Works on primary and replica + db.StreamLength(key); + replicaDb.StreamLength(key); + + // XRANGE: Works on primary and replica + db.StreamRange(key); + replicaDb.StreamRange(key); + + // XREVRANGE: Works on primary and replica + db.StreamRange(key, messageOrder: Order.Descending); + replicaDb.StreamRange(key, messageOrder: Order.Descending); + + // XREAD: Works on primary and replica + db.StreamRead(key, "0-1"); + replicaDb.StreamRead(key, "0-1"); + + // XTRIM: Works on primary, not replica + db.StreamTrim(key, 10); + ex = Assert.Throws(() => replicaDb.StreamTrim(key, 10)); + Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available", ex.Message); + } + [Fact] public void StreamAddSinglePairWithAutoId() { From 75471fbb3fdee148627919f385a8301642ea9e72 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 19 Apr 2022 11:56:08 -0400 Subject: [PATCH 140/435] Repeat: Docs: Love! Linking, fixes, code formatting, etc. (#2109) Repeat of #2100, for after #2071 goes in. This does a few things globally to the interfaces: - De-dupes `` since evidently past the first one doesn't count/render - Links our redis command links (and all others) so they're easily clickable! - Moves a few types to proper class files - In places sync/async methods are adjacent, utilizes ` to de-dupe - ...and some other misc URL cleanup throughout. In general: docs only change - I think we should merge this as-is to help PRs coming in, then I'll continue to iterate on docs. --- .../APITypes/LatencyHistoryEntry.cs | 47 ++ .../APITypes/LatencyLatestEntry.cs | 60 ++ src/StackExchange.Redis/ClientInfo.cs | 2 +- .../ClusterConfiguration.cs | 2 +- src/StackExchange.Redis/CommandMap.cs | 10 +- .../ConnectionMultiplexer.cs | 2 +- src/StackExchange.Redis/Enums/ClientFlags.cs | 2 +- src/StackExchange.Redis/Enums/RedisType.cs | 14 +- src/StackExchange.Redis/Enums/SaveType.cs | 6 +- src/StackExchange.Redis/Format.cs | 15 +- .../Interfaces/IDatabase.cs | 654 ++++++++------- .../Interfaces/IDatabaseAsync.cs | 643 ++++++++------- src/StackExchange.Redis/Interfaces/IRedis.cs | 2 +- .../Interfaces/IRedisAsync.cs | 2 +- src/StackExchange.Redis/Interfaces/IServer.cs | 744 +++++------------- .../Interfaces/ISubscriber.cs | 89 +-- .../Interfaces/ITransaction.cs | 10 +- .../Maintenance/AzureMaintenanceEvent.cs | 2 +- src/StackExchange.Redis/RedisDatabase.cs | 11 +- src/StackExchange.Redis/RedisFeatures.cs | 78 +- src/StackExchange.Redis/ResultProcessor.cs | 16 +- src/StackExchange.Redis/Role.cs | 8 +- src/StackExchange.Redis/TaskExtensions.cs | 8 +- src/StackExchange.Redis/ValueStopwatch.cs | 3 +- tests/StackExchange.Redis.Tests/Hashes.cs | 65 +- tests/StackExchange.Redis.Tests/PubSub.cs | 1 - .../StackExchange.Redis.Tests/SanityChecks.cs | 2 +- tests/StackExchange.Redis.Tests/Scans.cs | 5 +- .../SharedConnectionFixture.cs | 4 +- tests/StackExchange.Redis.Tests/Strings.cs | 5 +- 30 files changed, 1226 insertions(+), 1286 deletions(-) create mode 100644 src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs create mode 100644 src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs diff --git a/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs b/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs new file mode 100644 index 000000000..e07d89342 --- /dev/null +++ b/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs @@ -0,0 +1,47 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// A latency entry as reported by the built-in LATENCY HISTORY command +/// +public readonly struct LatencyHistoryEntry +{ + internal static readonly ResultProcessor ToArray = new Processor(); + + private sealed class Processor : ArrayResultProcessor + { + protected override bool TryParse(in RawResult raw, out LatencyHistoryEntry parsed) + { + if (raw.Type == ResultType.MultiBulk) + { + var items = raw.GetItems(); + if (items.Length >= 2 + && items[0].TryGetInt64(out var timestamp) + && items[1].TryGetInt64(out var duration)) + { + parsed = new LatencyHistoryEntry(timestamp, duration); + return true; + } + } + parsed = default; + return false; + } + } + + /// + /// The time at which this entry was recorded + /// + public DateTime Timestamp { get; } + + /// + /// The latency recorded for this event + /// + public int DurationMilliseconds { get; } + + internal LatencyHistoryEntry(long timestamp, long duration) + { + Timestamp = RedisBase.UnixEpoch.AddSeconds(timestamp); + DurationMilliseconds = checked((int)duration); + } +} diff --git a/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs b/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs new file mode 100644 index 000000000..739d1c71d --- /dev/null +++ b/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs @@ -0,0 +1,60 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// A latency entry as reported by the built-in LATENCY LATEST command +/// +public readonly struct LatencyLatestEntry +{ + internal static readonly ResultProcessor ToArray = new Processor(); + + private sealed class Processor : ArrayResultProcessor + { + protected override bool TryParse(in RawResult raw, out LatencyLatestEntry parsed) + { + if (raw.Type == ResultType.MultiBulk) + { + var items = raw.GetItems(); + if (items.Length >= 4 + && items[1].TryGetInt64(out var timestamp) + && items[2].TryGetInt64(out var duration) + && items[3].TryGetInt64(out var maxDuration)) + { + parsed = new LatencyLatestEntry(items[0].GetString()!, timestamp, duration, maxDuration); + return true; + } + } + parsed = default; + return false; + } + } + + /// + /// The name of this event + /// + public string EventName { get; } + + /// + /// The time at which this entry was recorded + /// + public DateTime Timestamp { get; } + + /// + /// The latency recorded for this event + /// + public int DurationMilliseconds { get; } + + /// + /// The max latency recorded for all events + /// + public int MaxDurationMilliseconds { get; } + + internal LatencyLatestEntry(string eventName, long timestamp, long duration, long maxDuration) + { + EventName = eventName; + Timestamp = RedisBase.UnixEpoch.AddSeconds(timestamp); + DurationMilliseconds = checked((int)duration); + MaxDurationMilliseconds = checked((int)maxDuration); + } +} diff --git a/src/StackExchange.Redis/ClientInfo.cs b/src/StackExchange.Redis/ClientInfo.cs index 539f72bce..272bd97da 100644 --- a/src/StackExchange.Redis/ClientInfo.cs +++ b/src/StackExchange.Redis/ClientInfo.cs @@ -105,7 +105,7 @@ public sealed class ClientInfo /// /// /// - /// https://redis.io/commands/client-list + /// public string? FlagsRaw { get; private set; } /// diff --git a/src/StackExchange.Redis/ClusterConfiguration.cs b/src/StackExchange.Redis/ClusterConfiguration.cs index 2592e6a22..92cbec872 100644 --- a/src/StackExchange.Redis/ClusterConfiguration.cs +++ b/src/StackExchange.Redis/ClusterConfiguration.cs @@ -264,6 +264,7 @@ internal ClusterNode? this[string nodeId] /// /// Represents the configuration of a single node in a cluster configuration. /// + /// public sealed class ClusterNode : IEquatable, IComparable, IComparable { private readonly ClusterConfiguration configuration; @@ -273,7 +274,6 @@ public sealed class ClusterNode : IEquatable, IComparable /// The commands available to twemproxy. /// - /// https://github.com/twitter/twemproxy/blob/master/notes/redis.md + /// public static CommandMap Twemproxy { get; } = CreateImpl(null, exclusions: new HashSet { - // see https://github.com/twitter/twemproxy/blob/master/notes/redis.md RedisCommand.KEYS, RedisCommand.MIGRATE, RedisCommand.MOVE, RedisCommand.OBJECT, RedisCommand.RANDOMKEY, RedisCommand.RENAME, RedisCommand.RENAMENX, RedisCommand.SCAN, @@ -48,9 +47,9 @@ public sealed class CommandMap /// /// The commands available to envoyproxy. /// + /// public static CommandMap Envoyproxy { get; } = CreateImpl(null, exclusions: new HashSet { - // see https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_protocols/redis.html?highlight=redis RedisCommand.KEYS, RedisCommand.MIGRATE, RedisCommand.MOVE, RedisCommand.OBJECT, RedisCommand.RANDOMKEY, RedisCommand.RENAME, RedisCommand.RENAMENX, RedisCommand.SORT, RedisCommand.SCAN, @@ -80,7 +79,7 @@ public sealed class CommandMap /// /// The commands available to SSDB. /// - /// https://ssdb.io/docs/redis-to-ssdb.html + /// public static CommandMap SSDB { get; } = Create(new HashSet { "ping", "get", "set", "del", "incr", "incrby", "mget", "mset", "keys", "getset", "setnx", @@ -92,9 +91,8 @@ public sealed class CommandMap /// /// The commands available to Sentinel. /// - /// https://redis.io/topics/sentinel + /// public static CommandMap Sentinel { get; } = Create(new HashSet { - // see https://redis.io/topics/sentinel "auth", "ping", "info", "role", "sentinel", "subscribe", "shutdown", "psubscribe", "unsubscribe", "punsubscribe" }, true); /// diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 484f802f6..099c46071 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -19,7 +19,7 @@ namespace StackExchange.Redis /// Represents an inter-related group of connections to redis servers. /// A reference to this should be held and re-used. /// - /// https://stackexchange.github.io/StackExchange.Redis/PipelinesMultiplexers + /// public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplexer // implies : IConnectionMultiplexer and : IDisposable { internal const int MillisecondsPerHeartbeat = 1000; diff --git a/src/StackExchange.Redis/Enums/ClientFlags.cs b/src/StackExchange.Redis/Enums/ClientFlags.cs index 42baa49ae..50d32261b 100644 --- a/src/StackExchange.Redis/Enums/ClientFlags.cs +++ b/src/StackExchange.Redis/Enums/ClientFlags.cs @@ -76,7 +76,7 @@ namespace StackExchange.Redis /// /// /// - /// https://redis.io/commands/client-list + /// [Flags] [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "Compatibility")] public enum ClientFlags : long diff --git a/src/StackExchange.Redis/Enums/RedisType.cs b/src/StackExchange.Redis/Enums/RedisType.cs index 5072918d0..b061dc906 100644 --- a/src/StackExchange.Redis/Enums/RedisType.cs +++ b/src/StackExchange.Redis/Enums/RedisType.cs @@ -3,7 +3,7 @@ /// /// The intrinsic data-types supported by redis. /// - /// https://redis.io/topics/data-types + /// public enum RedisType { /// @@ -15,14 +15,14 @@ public enum RedisType /// a Redis string can contain any kind of data, for instance a JPEG image or a serialized Ruby object. /// A String value can be at max 512 Megabytes in length. /// - /// https://redis.io/commands#string + /// String, /// /// Redis Lists are simply lists of strings, sorted by insertion order. /// It is possible to add elements to a Redis List pushing new elements on the head (on the left) or /// on the tail (on the right) of the list. /// - /// https://redis.io/commands#list + /// List, /// /// Redis Sets are an unordered collection of Strings. It is possible to add, remove, and test for @@ -31,7 +31,7 @@ public enum RedisType /// Adding the same element multiple times will result in a set having a single copy of this element. /// Practically speaking this means that adding a member does not require a check if exists then add operation. /// - /// https://redis.io/commands#set + /// Set, /// /// Redis Sorted Sets are, similarly to Redis Sets, non repeating collections of Strings. @@ -39,20 +39,20 @@ public enum RedisType /// in order to take the sorted set ordered, from the smallest to the greatest score. /// While members are unique, scores may be repeated. /// - /// https://redis.io/commands#sorted_set + /// SortedSet, /// /// Redis Hashes are maps between string fields and string values, so they are the perfect data type /// to represent objects (e.g. A User with a number of fields like name, surname, age, and so forth). /// - /// https://redis.io/commands#hash + /// Hash, /// /// A Redis Stream is a data structure which models the behavior of an append only log but it has more /// advanced features for manipulating the data contained within the stream. Each entry in a /// stream contains a unique message ID and a list of name/value pairs containing the entry's data. /// - /// https://redis.io/commands#stream + /// Stream, /// /// The data-type was not recognised by the client library. diff --git a/src/StackExchange.Redis/Enums/SaveType.cs b/src/StackExchange.Redis/Enums/SaveType.cs index ac2a88335..740e262a9 100644 --- a/src/StackExchange.Redis/Enums/SaveType.cs +++ b/src/StackExchange.Redis/Enums/SaveType.cs @@ -11,21 +11,21 @@ public enum SaveType /// Instruct Redis to start an Append Only File rewrite process. /// The rewrite will create a small optimized version of the current Append Only File. /// - /// https://redis.io/commands/bgrewriteaof + /// BackgroundRewriteAppendOnlyFile, /// /// Save the DB in background. The OK code is immediately returned. /// Redis forks, the parent continues to serve the clients, the child saves the DB on disk then exits. /// A client my be able to check if the operation succeeded using the LASTSAVE command. /// - /// https://redis.io/commands/bgsave + /// BackgroundSave, /// /// Save the DB in foreground. /// This is almost never a good thing to do, and could cause significant blocking. /// Only do this if you know you need to save. /// - /// https://redis.io/commands/save + /// [Obsolete("Saving on the foreground can cause significant blocking; use with extreme caution")] ForegroundSave, } diff --git a/src/StackExchange.Redis/Format.cs b/src/StackExchange.Redis/Format.cs index 040545855..cdf1aef80 100644 --- a/src/StackExchange.Redis/Format.cs +++ b/src/StackExchange.Redis/Format.cs @@ -234,12 +234,19 @@ private static bool CaseInsensitiveASCIIEqual(string xLowerCase, ReadOnlySpan + /// + /// Adapted from IPEndPointParser in Microsoft.AspNetCore + /// Link: + /// + /// + /// Copyright (c) .NET Foundation. All rights reserved. + /// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + /// + /// + /// If Unix sockets are attempted but not supported. internal static bool TryParseEndPoint(string? addressWithPort, [NotNullWhen(true)] out EndPoint? endpoint) { - // Adapted from IPEndPointParser in Microsoft.AspNetCore - // Link: https://github.com/aspnet/BasicMiddleware/blob/f320511b63da35571e890d53f3906c7761cd00a1/src/Microsoft.AspNetCore.HttpOverrides/Internal/IPEndPointParser.cs#L8 - // Copyright (c) .NET Foundation. All rights reserved. - // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. string addressPart; string? portPart = null; if (addressWithPort.IsNullOrEmpty()) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 4c42fa498..f95c06325 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -40,7 +40,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The timeout to use for the transfer. /// The options to use for this migration. /// The flags to use for this operation. - /// https://redis.io/commands/MIGRATE + /// void KeyMigrate(RedisKey key, EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, MigrateOptions migrateOptions = MigrateOptions.None, CommandFlags flags = CommandFlags.None); /// @@ -50,7 +50,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key to debug. /// The flags to use for this migration. /// The raw output from DEBUG OBJECT. - /// https://redis.io/commands/debug-object + /// RedisValue DebugObject(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -64,7 +64,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The value to set at this entry. /// The flags to use for this operation. /// if the specified member was not already present in the set, else . - /// https://redis.io/commands/geoadd + /// bool GeoAdd(RedisKey key, double longitude, double latitude, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -76,7 +76,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The geo value to store. /// The flags to use for this operation. /// if the specified member was not already present in the set, else . - /// https://redis.io/commands/geoadd + /// bool GeoAdd(RedisKey key, GeoEntry value, CommandFlags flags = CommandFlags.None); /// @@ -88,7 +88,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The geo values add to the set. /// The flags to use for this operation. /// The number of elements that were added to the set, not including all the elements already present into the set. - /// https://redis.io/commands/geoadd + /// long GeoAdd(RedisKey key, GeoEntry[] values, CommandFlags flags = CommandFlags.None); /// @@ -99,7 +99,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The geo value to remove. /// The flags to use for this operation. /// if the member existed in the sorted set and was removed, else . - /// https://redis.io/commands/zrem + /// bool GeoRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -111,7 +111,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The unit of distance to return (defaults to meters). /// The flags to use for this operation. /// The command returns the distance as a double (represented as a string) in the specified unit, or if one or both the elements are missing. - /// https://redis.io/commands/geodist + /// double? GeoDistance(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None); /// @@ -121,7 +121,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The members to get. /// The flags to use for this operation. /// The command returns an array where each element is the Geohash corresponding to each member name passed as argument to the command. - /// https://redis.io/commands/geohash + /// string?[] GeoHash(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); /// @@ -131,7 +131,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The member to get. /// The flags to use for this operation. /// The command returns an array where each element is the Geohash corresponding to each member name passed as argument to the command. - /// https://redis.io/commands/geohash + /// string? GeoHash(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -144,7 +144,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command. /// Non existing elements are reported as NULL elements of the array. /// - /// https://redis.io/commands/geopos + /// GeoPosition?[] GeoPosition(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); /// @@ -157,7 +157,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command. /// Non existing elements are reported as NULL elements of the array. /// - /// https://redis.io/commands/geopos + /// GeoPosition? GeoPosition(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -173,11 +173,11 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The search options to use. /// The flags to use for this operation. /// The results found within the radius, if any. - /// https://redis.io/commands/georadius + /// GeoRadiusResult[] GeoRadius(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); /// - /// Return the members of a sorted set populated with geospatial information using GEOADD, which are + /// Return the members of a sorted set populated with geospatial information using GEOADD, which are /// within the borders of the area specified with the center location and the maximum distance from the center (the radius). /// /// The key of the set. @@ -190,7 +190,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The search options to use. /// The flags to use for this operation. /// The results found within the radius, if any. - /// https://redis.io/commands/georadius + /// GeoRadiusResult[] GeoRadius(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); /// @@ -203,10 +203,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The maximum number of results to pull back. /// Whether or not to terminate the search after finding results. Must be true of count is -1. /// The order to sort by (defaults to unordered). - /// The search options to use + /// The search options to use. /// The flags for this operation. /// The results found within the shape, if any. - /// https://redis.io/commands/geosearch + /// GeoRadiusResult[] GeoSearch(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); /// @@ -220,10 +220,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The maximum number of results to pull back. /// Whether or not to terminate the search after finding results. Must be true of count is -1. /// The order to sort by (defaults to unordered). - /// The search options to use + /// The search options to use. /// The flags for this operation. /// The results found within the shape, if any. - /// /// https://redis.io/commands/geosearch + /// GeoRadiusResult[] GeoSearch(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); /// @@ -240,7 +240,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set. /// The flags for this operation. /// The size of the set stored at . - /// https://redis.io/commands/geosearchstore + /// long GeoSearchAndStore(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); /// @@ -258,7 +258,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set. /// The flags for this operation. /// The size of the set stored at . - /// https://redis.io/commands/geosearchstore + /// long GeoSearchAndStore(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); /// @@ -271,8 +271,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The amount to decrement by. /// The flags to use for this operation. /// The value at field after the decrement operation. - /// The range of values supported by HINCRBY is limited to 64 bit signed integers. - /// https://redis.io/commands/hincrby + /// + /// The range of values supported by HINCRBY is limited to 64 bit signed integers. + /// + /// long HashDecrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None); /// @@ -284,8 +286,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The amount to decrement by. /// The flags to use for this operation. /// The value at field after the decrement operation. - /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. - /// https://redis.io/commands/hincrbyfloat + /// + /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. + /// + /// double HashDecrement(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None); /// @@ -296,7 +300,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The field in the hash to delete. /// The flags to use for this operation. /// The number of fields that were removed. - /// https://redis.io/commands/hdel + /// bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// @@ -307,7 +311,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The fields in the hash to delete. /// The flags to use for this operation. /// The number of fields that were removed. - /// https://redis.io/commands/hdel + /// long HashDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); /// @@ -317,7 +321,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The field in the hash to check. /// The flags to use for this operation. /// if the hash contains field, if the hash does not contain field, or key does not exist. - /// https://redis.io/commands/hexists + /// bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// @@ -327,7 +331,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The field in the hash to get. /// The flags to use for this operation. /// The value associated with field, or nil when field is not present in the hash or key does not exist. - /// https://redis.io/commands/hget + /// RedisValue HashGet(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// @@ -337,7 +341,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The field in the hash to get. /// The flags to use for this operation. /// The value associated with field, or nil when field is not present in the hash or key does not exist. - /// https://redis.io/commands/hget + /// Lease? HashGetLease(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// @@ -348,7 +352,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The fields in the hash to get. /// The flags to use for this operation. /// List of values associated with the given fields, in the same order as they are requested. - /// https://redis.io/commands/hmget + /// RedisValue[] HashGet(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); /// @@ -357,7 +361,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the hash to get all entries from. /// The flags to use for this operation. /// List of fields and their values stored in the hash, or an empty list when key does not exist. - /// https://redis.io/commands/hgetall + /// HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -370,8 +374,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The amount to increment by. /// The flags to use for this operation. /// The value at field after the increment operation. - /// The range of values supported by HINCRBY is limited to 64 bit signed integers. - /// https://redis.io/commands/hincrby + /// + /// The range of values supported by HINCRBY is limited to 64 bit signed integers. + /// + /// long HashIncrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None); /// @@ -383,8 +389,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The amount to increment by. /// The flags to use for this operation. /// The value at field after the increment operation. - /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. - /// https://redis.io/commands/hincrbyfloat + /// + /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. + /// + /// double HashIncrement(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None); /// @@ -393,7 +401,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the hash. /// The flags to use for this operation. /// List of fields in the hash, or an empty list when key does not exist. - /// https://redis.io/commands/hkeys + /// RedisValue[] HashKeys(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -402,7 +410,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the hash. /// The flags to use for this operation. /// The number of fields in the hash, or 0 when key does not exist. - /// https://redis.io/commands/hlen + /// long HashLength(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -411,7 +419,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the hash. /// The flags to use for this operation. /// A random hash field name or if the hash does not exist. - /// https://redis.io/commands/hrandfield + /// RedisValue HashRandomField(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -421,7 +429,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The number of fields to return. /// The flags to use for this operation. /// An array of hash field names of size of at most , or if the hash does not exist. - /// https://redis.io/commands/hrandfield + /// RedisValue[] HashRandomFields(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -431,7 +439,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The number of fields to return. /// The flags to use for this operation. /// An array of hash entries of size of at most , or if the hash does not exist. - /// https://redis.io/commands/hrandfield + /// HashEntry[] HashRandomFieldsWithValues(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -442,7 +450,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The page size to iterate by. /// The flags to use for this operation. /// Yields all elements of the hash matching the pattern. - /// https://redis.io/commands/hscan + /// IEnumerable HashScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags); /// @@ -456,7 +464,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The page offset to start at. /// The flags to use for this operation. /// Yields all elements of the hash matching the pattern. - /// https://redis.io/commands/hscan + /// IEnumerable HashScan(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// @@ -467,7 +475,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the hash. /// The entries to set in the hash. /// The flags to use for this operation. - /// https://redis.io/commands/hmset + /// void HashSet(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None); /// @@ -481,8 +489,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// Which conditions under which to set the field value (defaults to always). /// The flags to use for this operation. /// if field is a new field in the hash and value was set, if field already exists in the hash and the value was updated. - /// https://redis.io/commands/hset - /// https://redis.io/commands/hsetnx + /// + /// , + /// + /// bool HashSet(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -492,7 +502,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The field containing the string /// The flags to use for this operation. /// The length of the string at field, or 0 when key does not exist. - /// https://redis.io/commands/hstrlen + /// long HashStringLength(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// @@ -501,7 +511,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the hash. /// The flags to use for this operation. /// List of values in the hash, or an empty list when key does not exist. - /// https://redis.io/commands/hvals + /// RedisValue[] HashValues(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -511,7 +521,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The value to add. /// The flags to use for this operation. /// if at least 1 HyperLogLog internal register was altered, otherwise. - /// https://redis.io/commands/pfadd + /// bool HyperLogLogAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -521,7 +531,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The values to add. /// The flags to use for this operation. /// if at least 1 HyperLogLog internal register was altered, otherwise. - /// https://redis.io/commands/pfadd + /// bool HyperLogLogAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); /// @@ -530,7 +540,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the hyperloglog. /// The flags to use for this operation. /// The approximated number of unique elements observed via HyperLogLogAdd. - /// https://redis.io/commands/pfcount + /// long HyperLogLogLength(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -539,7 +549,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The keys of the hyperloglogs. /// The flags to use for this operation. /// The approximated number of unique elements observed via HyperLogLogAdd. - /// https://redis.io/commands/pfcount + /// long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// @@ -549,7 +559,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the first hyperloglog to merge. /// The key of the first hyperloglog to merge. /// The flags to use for this operation. - /// https://redis.io/commands/pfmerge + /// void HyperLogLogMerge(RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); /// @@ -558,7 +568,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the merged hyperloglog. /// The keys of the hyperloglogs to merge. /// The flags to use for this operation. - /// https://redis.io/commands/pfmerge + /// void HyperLogLogMerge(RedisKey destination, RedisKey[] sourceKeys, CommandFlags flags = CommandFlags.None); /// @@ -578,7 +588,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// Whether to overwrite an existing values at . If and the key exists, the copy will not succeed. /// The flags to use for this operation. /// if key was copied. if key was not copied. - /// https://redis.io/commands/copy + /// bool KeyCopy(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None); /// @@ -588,8 +598,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key to delete. /// The flags to use for this operation. /// if the key was removed. - /// https://redis.io/commands/del - /// https://redis.io/commands/unlink + /// + /// , + /// + /// bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -599,8 +611,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The keys to delete. /// The flags to use for this operation. /// The number of keys that were removed. - /// https://redis.io/commands/del - /// https://redis.io/commands/unlink + /// + /// , + /// + /// long KeyDelete(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// @@ -610,7 +624,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key to dump. /// The flags to use for this operation. /// The serialized value. - /// https://redis.io/commands/dump + /// byte[]? KeyDump(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -619,7 +633,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key to dump. /// The flags to use for this operation. /// The Redis encoding for the value or is the key does not exist. - /// https://redis.io/commands/object-encoding + /// string? KeyEncoding(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -628,7 +642,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key to check. /// The flags to use for this operation. /// if the key exists. if the key does not exist. - /// https://redis.io/commands/exists + /// bool KeyExists(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -637,7 +651,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The keys to check. /// The flags to use for this operation. /// The number of keys that existed. - /// https://redis.io/commands/exists + /// long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// @@ -657,12 +671,15 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// /// Since Redis 2.1.3, you can update the timeout of a key. - /// It is also possible to remove the timeout using the PERSIST command. See the page on key expiry for more information. + /// It is also possible to remove the timeout using the PERSIST command. + /// See the page on key expiry for more information. + /// + /// + /// , + /// , + /// /// /// - /// https://redis.io/commands/expire - /// https://redis.io/commands/pexpire - /// https://redis.io/commands/persist bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); /// @@ -675,8 +692,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// Since Redis 7.0.0, you can choose under which condition the expiration will be set using . /// The flags to use for this operation. /// if the timeout was set. if key does not exist or the timeout could not be set. - /// https://redis.io/commands/expire - /// https://redis.io/commands/pexpire + /// + /// , + /// + /// bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None); /// @@ -696,12 +715,15 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// /// Since Redis 2.1.3, you can update the timeout of a key. - /// It is also possible to remove the timeout using the PERSIST command. See the page on key expiry for more information. + /// It is also possible to remove the timeout using the PERSIST command. + /// See the page on key expiry for more information. + /// + /// + /// , + /// , + /// /// /// - /// https://redis.io/commands/expireat - /// https://redis.io/commands/pexpireat - /// https://redis.io/commands/persist bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None); /// @@ -714,8 +736,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// Since Redis 7.0.0, you can choose under which condition the expiration will be set using . /// The flags to use for this operation. /// if the timeout was set. if key does not exist or the timeout could not be set. - /// https://redis.io/commands/expire - /// https://redis.io/commands/pexpire + /// + /// , + /// + /// bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None); /// @@ -724,8 +748,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key to get the expiration for. /// The flags to use for this operation. /// The time at which the given key will expire, or if the key does not exist or has no associated expiration time. - /// https://redis.io/commands/expiretime - /// https://redis.io/commands/pexpiretime + /// + /// , + /// + /// DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -734,7 +760,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key to get the time of. /// The flags to use for this operation. /// The time since the object stored at the specified key is idle. - /// https://redis.io/commands/object + /// TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -746,7 +772,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The database to move the key to. /// The flags to use for this operation. /// if key was moved. if key was not moved. - /// https://redis.io/commands/move + /// bool KeyMove(RedisKey key, int database, CommandFlags flags = CommandFlags.None); /// @@ -755,7 +781,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key to persist. /// The flags to use for this operation. /// if the timeout was removed. if key does not exist or does not have an associated timeout. - /// https://redis.io/commands/persist + /// bool KeyPersist(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -763,7 +789,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The flags to use for this operation. /// The random key, or nil when the database is empty. - /// https://redis.io/commands/randomkey + /// RedisKey KeyRandom(CommandFlags flags = CommandFlags.None); /// @@ -772,7 +798,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key to get a reference count for. /// The flags to use for this operation. /// The number of references ( if the key does not exist). - /// https://redis.io/commands/object-refcount + /// long? KeyRefCount(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -784,8 +810,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// What conditions to rename under (defaults to always). /// The flags to use for this operation. /// if the key was renamed, otherwise. - /// https://redis.io/commands/rename - /// https://redis.io/commands/renamenx + /// + /// , + /// + /// bool KeyRename(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -796,7 +824,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The value of the key. /// The expiry to set. /// The flags to use for this operation. - /// https://redis.io/commands/restore + /// void KeyRestore(RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None); /// @@ -806,9 +834,27 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key to check. /// The flags to use for this operation. /// TTL, or nil when key does not exist or does not have a timeout. - /// https://redis.io/commands/ttl + /// TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Alters the last access time of a key. + /// + /// The key to touch. + /// The flags to use for this operation. + /// if the key was touched, otherwise. + /// + bool KeyTouch(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Alters the last access time of the specified . A key is ignored if it does not exist. + /// + /// The keys to touch. + /// The flags to use for this operation. + /// The number of keys that were touched. + /// + long KeyTouch(RedisKey[] keys, CommandFlags flags = CommandFlags.None); + /// /// Returns the string representation of the type of the value stored at key. /// The different types that can be returned are: string, list, set, zset and hash. @@ -816,7 +862,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key to get the type of. /// The flags to use for this operation. /// Type of key, or none when key does not exist. - /// https://redis.io/commands/type + /// RedisType KeyType(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -829,7 +875,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The index position to get the value at. /// The flags to use for this operation. /// The requested element, or nil when index is out of range. - /// https://redis.io/commands/lindex + /// RedisValue ListGetByIndex(RedisKey key, long index, CommandFlags flags = CommandFlags.None); /// @@ -841,7 +887,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The value to insert. /// The flags to use for this operation. /// The length of the list after the insert operation, or -1 when the value pivot was not found. - /// https://redis.io/commands/linsert + /// long ListInsertAfter(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -853,7 +899,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The value to insert. /// The flags to use for this operation. /// The length of the list after the insert operation, or -1 when the value pivot was not found. - /// https://redis.io/commands/linsert + /// long ListInsertBefore(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -862,7 +908,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the list. /// The flags to use for this operation. /// The value of the first element, or nil when key does not exist. - /// https://redis.io/commands/lpop + /// RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -873,7 +919,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The number of elements to remove /// The flags to use for this operation. /// Array of values that were popped, or nil if the key doesn't exist. - /// https://redis.io/commands/lpop + /// RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -886,6 +932,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The maximum number of elements to scan through before stopping, defaults to 0 (a full scan of the list.) /// The flags to use for this operation. /// The 0-based index of the first matching element, or -1 if not found. + /// long ListPosition(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None); /// @@ -899,6 +946,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The maximum number of elements to scan through before stopping, defaults to 0 (a full scan of the list.) /// The flags to use for this operation. /// An array of at most of indexes of matching elements. If none are found, and empty array is returned. + /// long[] ListPositions(RedisKey key, RedisValue element, long count, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None); /// @@ -910,8 +958,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// Which conditions to add to the list under (defaults to always). /// The flags to use for this operation. /// The length of the list after the push operations. - /// https://redis.io/commands/lpush - /// https://redis.io/commands/lpushx + /// + /// , + /// + /// long ListLeftPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -923,21 +973,23 @@ public interface IDatabase : IRedis, IDatabaseAsync /// Which conditions to add to the list under (defaults to always). /// The flags to use for this operation. /// The length of the list after the push operations. - /// https://redis.io/commands/lpush - /// https://redis.io/commands/lpushx + /// + /// , + /// + /// long ListLeftPush(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); /// /// Insert all the specified values at the head of the list stored at key. /// If key does not exist, it is created as empty list before performing the push operations. /// Elements are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. - /// So for instance the command LPUSH mylist a b c will result into a list containing c as first element, b as second element and a as third element. + /// So for instance the command LPUSH mylist a b c will result into a list containing c as first element, b as second element and a as third element. /// /// The key of the list. /// The values to add to the head of the list. /// The flags to use for this operation. /// The length of the list after the push operations. - /// https://redis.io/commands/lpush + /// long ListLeftPush(RedisKey key, RedisValue[] values, CommandFlags flags); /// @@ -946,7 +998,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the list. /// The flags to use for this operation. /// The length of the list at key. - /// https://redis.io/commands/llen + /// long ListLength(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -959,7 +1011,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// What side of the list to move to. /// The flags to use for this operation. /// The element being popped and pushed or if there is no element to move. - /// https://redis.io/commands/lmove + /// RedisValue ListMove(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, CommandFlags flags = CommandFlags.None); /// @@ -973,7 +1025,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The stop index of the list. /// The flags to use for this operation. /// List of elements in the specified range. - /// https://redis.io/commands/lrange + /// RedisValue[] ListRange(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None); /// @@ -990,7 +1042,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The count behavior (see method summary). /// The flags to use for this operation. /// The number of removed elements. - /// https://redis.io/commands/lrem + /// long ListRemove(RedisKey key, RedisValue value, long count = 0, CommandFlags flags = CommandFlags.None); /// @@ -999,7 +1051,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the list. /// The flags to use for this operation. /// The element being popped. - /// https://redis.io/commands/rpop + /// RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -1010,7 +1062,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The number of elements to pop /// The flags to use for this operation. /// Array of values that were popped, or nil if the key doesn't exist. - /// https://redis.io/commands/rpop + /// RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -1020,7 +1072,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the destination list. /// The flags to use for this operation. /// The element being popped and pushed. - /// https://redis.io/commands/rpoplpush + /// RedisValue ListRightPopLeftPush(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None); /// @@ -1032,8 +1084,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// Which conditions to add to the list under. /// The flags to use for this operation. /// The length of the list after the push operation. - /// https://redis.io/commands/rpush - /// https://redis.io/commands/rpushx + /// + /// , + /// + /// long ListRightPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -1045,21 +1099,23 @@ public interface IDatabase : IRedis, IDatabaseAsync /// Which conditions to add to the list under. /// The flags to use for this operation. /// The length of the list after the push operation. - /// https://redis.io/commands/rpush - /// https://redis.io/commands/rpushx + /// + /// , + /// + /// long ListRightPush(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); /// /// Insert all the specified values at the tail of the list stored at key. /// If key does not exist, it is created as empty list before performing the push operation. /// Elements are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. - /// So for instance the command RPUSH mylist a b c will result into a list containing a as first element, b as second element and c as third element. + /// So for instance the command RPUSH mylist a b c will result into a list containing a as first element, b as second element and c as third element. /// /// The key of the list. /// The values to add to the tail of the list. /// The flags to use for this operation. /// The length of the list after the push operation. - /// https://redis.io/commands/rpush + /// long ListRightPush(RedisKey key, RedisValue[] values, CommandFlags flags); /// @@ -1071,20 +1127,20 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The index to set the value at. /// The values to add to the list. /// The flags to use for this operation. - /// https://redis.io/commands/lset + /// void ListSetByIndex(RedisKey key, long index, RedisValue value, CommandFlags flags = CommandFlags.None); /// /// Trim an existing list so that it will contain only the specified range of elements specified. /// Both start and stop are zero-based indexes, where 0 is the first element of the list (the head), 1 the next element and so on. - /// For example: LTRIM foobar 0 2 will modify the list stored at foobar so that only the first three elements of the list will remain. + /// For example: LTRIM foobar 0 2 will modify the list stored at foobar so that only the first three elements of the list will remain. /// start and end can also be negative numbers indicating offsets from the end of the list, where -1 is the last element of the list, -2 the penultimate element and so on. /// /// The key of the list. /// The start index of the list to trim to. /// The end index of the list to trim to. /// The flags to use for this operation. - /// https://redis.io/commands/ltrim + /// void ListTrim(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None); /// @@ -1134,7 +1190,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The number of clients that received the message *on the destination server*, /// note that this doesn't mean much in a cluster as clients can get the message through other nodes. /// - /// https://redis.io/commands/publish + /// long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None); /// @@ -1143,8 +1199,8 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The command to run. /// The arguments to pass for the command. - /// This API should be considered an advanced feature; inappropriate use can be harmful. /// A dynamic representation of the command's result. + /// This API should be considered an advanced feature; inappropriate use can be harmful. RedisResult Execute(string command, params object[] args); /// @@ -1154,8 +1210,8 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The command to run. /// The arguments to pass for the command. /// The flags to use for this operation. - /// This API should be considered an advanced feature; inappropriate use can be harmful. /// A dynamic representation of the command's result. + /// This API should be considered an advanced feature; inappropriate use can be harmful. RedisResult Execute(string command, ICollection args, CommandFlags flags = CommandFlags.None); /// @@ -1166,8 +1222,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The values to execute against. /// The flags to use for this operation. /// A dynamic representation of the script's result. - /// https://redis.io/commands/eval - /// https://redis.io/commands/evalsha + /// + /// , + /// + /// RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); /// @@ -1178,7 +1236,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The values to execute against. /// The flags to use for this operation. /// A dynamic representation of the script's result. - /// https://redis.io/commands/evalsha + /// RedisResult ScriptEvaluate(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); /// @@ -1189,7 +1247,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The parameters to pass to the script. /// The flags to use for this operation. /// A dynamic representation of the script's result. - /// https://redis.io/commands/eval + /// RedisResult ScriptEvaluate(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None); /// @@ -1201,7 +1259,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The parameters to pass to the script. /// The flags to use for this operation. /// A dynamic representation of the script's result. - /// https://redis.io/commands/eval + /// RedisResult ScriptEvaluate(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None); /// @@ -1213,7 +1271,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The value to add to the set. /// The flags to use for this operation. /// if the specified member was not already present in the set, else . - /// https://redis.io/commands/sadd + /// bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -1225,7 +1283,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The values to add to the set. /// The flags to use for this operation. /// The number of elements that were added to the set, not including all the elements already present into the set. - /// https://redis.io/commands/sadd + /// long SetAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); /// @@ -1236,9 +1294,11 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the second set. /// The flags to use for this operation. /// List with members of the resulting set. - /// https://redis.io/commands/sunion - /// https://redis.io/commands/sinter - /// https://redis.io/commands/sdiff + /// + /// , + /// , + /// + /// RedisValue[] SetCombine(SetOperation operation, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); /// @@ -1248,9 +1308,11 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The keys of the sets to operate on. /// The flags to use for this operation. /// List with members of the resulting set. - /// https://redis.io/commands/sunion - /// https://redis.io/commands/sinter - /// https://redis.io/commands/sdiff + /// + /// , + /// , + /// + /// RedisValue[] SetCombine(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// @@ -1263,9 +1325,11 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the second set. /// The flags to use for this operation. /// The number of elements in the resulting set. - /// https://redis.io/commands/sunionstore - /// https://redis.io/commands/sinterstore - /// https://redis.io/commands/sdiffstore + /// + /// , + /// , + /// + /// long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); /// @@ -1277,9 +1341,11 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The keys of the sets to operate on. /// The flags to use for this operation. /// The number of elements in the resulting set. - /// https://redis.io/commands/sunionstore - /// https://redis.io/commands/sinterstore - /// https://redis.io/commands/sdiffstore + /// + /// , + /// , + /// + /// long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// @@ -1292,7 +1358,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// if the element is a member of the set. /// if the element is not a member of the set, or if key does not exist. /// - /// https://redis.io/commands/sismember + /// bool SetContains(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -1302,11 +1368,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The members to check for. /// The flags to use for this operation. /// - /// An array of booleans corresponding to , for each: /// if the element is a member of the set. /// if the element is not a member of the set, or if key does not exist. /// - /// https://redis.io/commands/smismember + /// bool[] SetContains(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); /// @@ -1322,7 +1387,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The number of elements to check (defaults to 0 and means unlimited). /// The flags to use for this operation. /// The cardinality (number of elements) of the set, or 0 if key does not exist. - /// https://redis.io/commands/scard + /// long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None); /// @@ -1331,7 +1396,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the set. /// The flags to use for this operation. /// The cardinality (number of elements) of the set, or 0 if key does not exist. - /// https://redis.io/commands/scard + /// long SetLength(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -1340,7 +1405,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the set. /// The flags to use for this operation. /// All elements of the set. - /// https://redis.io/commands/smembers + /// RedisValue[] SetMembers(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -1356,7 +1421,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// if the element is moved. /// if the element is not a member of source and no operation was performed. /// - /// https://redis.io/commands/smove + /// bool SetMove(RedisKey source, RedisKey destination, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -1365,7 +1430,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the set. /// The flags to use for this operation. /// The removed element, or nil when key does not exist. - /// https://redis.io/commands/spop + /// RedisValue SetPop(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -1375,7 +1440,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The number of elements to return. /// The flags to use for this operation. /// An array of elements, or an empty array when key does not exist. - /// https://redis.io/commands/spop + /// RedisValue[] SetPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -1384,7 +1449,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the set. /// The flags to use for this operation. /// The randomly selected element, or when does not exist. - /// https://redis.io/commands/srandmember + /// RedisValue SetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -1396,7 +1461,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The count of members to get. /// The flags to use for this operation. /// An array of elements, or an empty array when does not exist. - /// https://redis.io/commands/srandmember + /// RedisValue[] SetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -1407,7 +1472,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The value to remove. /// The flags to use for this operation. /// if the specified member was already present in the set, otherwise. - /// https://redis.io/commands/srem + /// bool SetRemove(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -1418,7 +1483,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The values to remove. /// The flags to use for this operation. /// The number of members that were removed from the set, not including non existing members. - /// https://redis.io/commands/srem + /// long SetRemove(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); /// @@ -1429,7 +1494,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The page size to iterate by. /// The flags to use for this operation. /// Yields all matching elements of the set. - /// https://redis.io/commands/sscan + /// IEnumerable SetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags); /// @@ -1443,7 +1508,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The page offset to start at. /// The flags to use for this operation. /// Yields all matching elements of the set. - /// https://redis.io/commands/sscan + /// IEnumerable SetScan(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// @@ -1463,7 +1528,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key pattern to sort by, if any e.g. ExternalKey_* would return the value of ExternalKey_{listvalue} for each entry. /// The flags to use for this operation. /// The sorted elements, or the external values if get is specified. - /// https://redis.io/commands/sort + /// RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); /// @@ -1471,7 +1536,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// By default, the elements themselves are compared, but the values can also be used to perform external key-lookups using the by parameter. /// By default, the elements themselves are returned, but external key-lookups (one or many) can be performed instead by specifying /// the get parameter (note that # specifies the element itself, when used in get). - /// Referring to the redis SORT documentation for examples is recommended. + /// Referring to the redis SORT documentation for examples is recommended. /// When used in hashes, by and get can be used to specify fields using -> notation (again, refer to redis documentation). /// /// The destination key to store results in. @@ -1484,7 +1549,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key pattern to sort by, if any e.g. ExternalKey_* would return the value of ExternalKey_{listvalue} for each entry. /// The flags to use for this operation. /// The number of elements stored in the new list. - /// https://redis.io/commands/sort + /// long SortAndStore(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); /// @@ -1496,7 +1561,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The score for the member to add to the sorted set. /// The flags to use for this operation. /// if the value was added. if it already existed (the score is still updated). - /// https://redis.io/commands/zadd + /// bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags); /// @@ -1509,7 +1574,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// What conditions to add the element under (defaults to always). /// The flags to use for this operation. /// if the value was added. if it already existed (the score is still updated). - /// https://redis.io/commands/zadd + /// bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -1520,7 +1585,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The members and values to add to the sorted set. /// The flags to use for this operation. /// The number of elements added to the sorted sets, not including elements already existing for which the score was updated. - /// https://redis.io/commands/zadd + /// long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags flags); /// @@ -1532,7 +1597,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// What conditions to add the element under (defaults to always). /// The flags to use for this operation. /// The number of elements added to the sorted sets, not including elements already existing for which the score was updated. - /// https://redis.io/commands/zadd + /// long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -1545,10 +1610,12 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The optional weights per set that correspond to . /// The aggregation method (defaults to ). /// The flags to use for this operation. - /// https://redis.io/commands/zunion - /// https://redis.io/commands/zinter - /// https://redis.io/commands/zdiff /// The resulting sorted set. + /// + /// , + /// , + /// + /// RedisValue[] SortedSetCombine(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// @@ -1561,10 +1628,12 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The optional weights per set that correspond to . /// The aggregation method (defaults to ). /// The flags to use for this operation. - /// https://redis.io/commands/zunion - /// https://redis.io/commands/zinter - /// https://redis.io/commands/zdiff /// The resulting sorted set with scores. + /// + /// , + /// , + /// + /// SortedSetEntry[] SortedSetCombineWithScores(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// @@ -1578,10 +1647,12 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the second sorted set. /// The aggregation method (defaults to sum). /// The flags to use for this operation. - /// https://redis.io/commands/zunionstore - /// https://redis.io/commands/zinterstore - /// https://redis.io/commands/zdiffstore /// The number of elements in the resulting sorted set at destination. + /// + /// , + /// , + /// + /// long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// @@ -1595,10 +1666,12 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The optional weights per set that correspond to . /// The aggregation method (defaults to sum). /// The flags to use for this operation. - /// https://redis.io/commands/zunionstore - /// https://redis.io/commands/zinterstore - /// https://redis.io/commands/zdiffstore /// The number of elements in the resulting sorted set at destination. + /// + /// , + /// , + /// + /// long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// @@ -1610,7 +1683,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The amount to decrement by. /// The flags to use for this operation. /// The new score of member. - /// https://redis.io/commands/zincrby + /// double SortedSetDecrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None); /// @@ -1621,7 +1694,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The amount to increment by. /// The flags to use for this operation. /// The new score of member. - /// https://redis.io/commands/zincrby + /// double SortedSetIncrement(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None); /// @@ -1631,7 +1704,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// If the intersection cardinality reaches partway through the computation, the algorithm will exit and yield as the cardinality (defaults to 0 meaning unlimited). /// The flags to use for this operation. /// The number of elements in the resulting intersection. - /// https://redis.io/commands/zintercard + /// long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None); /// @@ -1643,7 +1716,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// Whether to exclude and from the range check (defaults to both inclusive). /// The flags to use for this operation. /// The cardinality (number of elements) of the sorted set, or 0 if key does not exist. - /// https://redis.io/commands/zcard + /// long SortedSetLength(RedisKey key, double min = double.NegativeInfinity, double max = double.PositiveInfinity, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// @@ -1656,7 +1729,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// Whether to exclude and from the range check (defaults to both inclusive). /// The flags to use for this operation. /// The number of elements in the specified score range. - /// https://redis.io/commands/zlexcount + /// long SortedSetLengthByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// @@ -1665,7 +1738,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the sorted set. /// The flags to use for this operation. /// The randomly selected element, or when does not exist. - /// https://redis.io/commands/zrandmember + /// RedisValue SortedSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -1684,7 +1757,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The flags to use for this operation. /// The randomly selected elements, or an empty array when does not exist. - /// https://redis.io/commands/zrandmember + /// RedisValue[] SortedSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -1703,7 +1776,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The flags to use for this operation. /// The randomly selected elements with scores, or an empty array when does not exist. - /// https://redis.io/commands/zrandmember + /// SortedSetEntry[] SortedSetRandomMembersWithScores(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -1719,8 +1792,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The order to sort by (defaults to ascending). /// The flags to use for this operation. /// List of elements in the specified range. - /// https://redis.io/commands/zrange - /// https://redis.io/commands/zrevrange + /// + /// , + /// + /// RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); /// @@ -1741,8 +1816,8 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The number of elements into the sorted set to skip. Note: this iterates after sorting so incurs O(n) cost for large values. /// The maximum number of elements to pull into the new () set. /// The flags to use for this operation. - /// https://redis.io/commands/zrangestore /// The cardinality of (number of elements in) the newly created sorted set. + /// long SortedSetRangeAndStore( RedisKey sourceKey, RedisKey destinationKey, @@ -1768,8 +1843,10 @@ long SortedSetRangeAndStore( /// The order to sort by (defaults to ascending). /// The flags to use for this operation. /// List of elements in the specified range. - /// https://redis.io/commands/zrange - /// https://redis.io/commands/zrevrange + /// + /// , + /// + /// SortedSetEntry[] SortedSetRangeByRankWithScores(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); /// @@ -1788,8 +1865,10 @@ long SortedSetRangeAndStore( /// How many items to take. /// The flags to use for this operation. /// List of elements in the specified score range. - /// https://redis.io/commands/zrangebyscore - /// https://redis.io/commands/zrevrangebyscore + /// + /// , + /// + /// RedisValue[] SortedSetRangeByScore(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, @@ -1815,8 +1894,10 @@ RedisValue[] SortedSetRangeByScore(RedisKey key, /// How many items to take. /// The flags to use for this operation. /// List of elements in the specified score range. - /// https://redis.io/commands/zrangebyscore - /// https://redis.io/commands/zrevrangebyscore + /// + /// , + /// + /// SortedSetEntry[] SortedSetRangeByScoreWithScores(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, @@ -1837,8 +1918,8 @@ SortedSetEntry[] SortedSetRangeByScoreWithScores(RedisKey key, /// How many items to skip. /// How many items to take. /// The flags to use for this operation. - /// https://redis.io/commands/zrangebylex /// List of elements in the specified score range. + /// RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min, RedisValue max, @@ -1859,9 +1940,11 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// How many items to skip. /// How many items to take. /// The flags to use for this operation. - /// https://redis.io/commands/zrangebylex - /// https://redis.io/commands/zrevrangebylex /// List of elements in the specified score range. + /// + /// , + /// + /// RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min = default, RedisValue max = default, @@ -1880,8 +1963,10 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// The order to sort by (defaults to ascending). /// The flags to use for this operation. /// If member exists in the sorted set, the rank of member. If member does not exist in the sorted set or key does not exist, . - /// https://redis.io/commands/zrank - /// https://redis.io/commands/zrevrank + /// + /// , + /// + /// long? SortedSetRank(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); /// @@ -1891,7 +1976,7 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// The member to remove. /// The flags to use for this operation. /// if the member existed in the sorted set and was removed. otherwise. - /// https://redis.io/commands/zrem + /// bool SortedSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -1901,7 +1986,7 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// The members to remove. /// The flags to use for this operation. /// The number of members removed from the sorted set, not including non existing members. - /// https://redis.io/commands/zrem + /// long SortedSetRemove(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); /// @@ -1915,7 +2000,7 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// The maximum rank to remove. /// The flags to use for this operation. /// The number of elements removed. - /// https://redis.io/commands/zremrangebyrank + /// long SortedSetRemoveRangeByRank(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None); /// @@ -1927,7 +2012,7 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// Which of and to exclude (defaults to both inclusive). /// The flags to use for this operation. /// The number of elements removed. - /// https://redis.io/commands/zremrangebyscore + /// long SortedSetRemoveRangeByScore(RedisKey key, double start, double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// @@ -1940,7 +2025,7 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// Which of and to exclude (defaults to both inclusive). /// The flags to use for this operation. /// The number of elements removed. - /// https://redis.io/commands/zremrangebylex + /// long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// @@ -1951,7 +2036,7 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// The page size to iterate by. /// The flags to use for this operation. /// Yields all matching elements of the sorted set. - /// https://redis.io/commands/zscan + /// IEnumerable SortedSetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags); /// @@ -1965,7 +2050,7 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// The page offset to start at. /// The flags to use for this operation. /// Yields all matching elements of the sorted set. - /// https://redis.io/commands/zscan + /// IEnumerable SortedSetScan(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, @@ -1981,7 +2066,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The member to get a score for. /// The flags to use for this operation. /// The score of the member. - /// https://redis.io/commands/zscore + /// double? SortedSetScore(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -1995,7 +2080,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The scores of the members in the same order as the array. /// If a member does not exist in the set, is returned. /// - /// https://redis.io/commands/zmscore + /// double?[] SortedSetScores(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); /// @@ -2005,8 +2090,10 @@ IEnumerable SortedSetScan(RedisKey key, /// The order to sort by (defaults to ascending). /// The flags to use for this operation. /// The removed element, or nil when key does not exist. - /// https://redis.io/commands/zpopmin - /// https://redis.io/commands/zpopmax + /// + /// , + /// + /// SortedSetEntry? SortedSetPop(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); /// @@ -2017,8 +2104,10 @@ IEnumerable SortedSetScan(RedisKey key, /// The order to sort by (defaults to ascending). /// The flags to use for this operation. /// An array of elements, or an empty array when key does not exist. - /// https://redis.io/commands/zpopmin - /// https://redis.io/commands/zpopmax + /// + /// , + /// + /// SortedSetEntry[] SortedSetPop(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); /// @@ -2029,7 +2118,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The ID of the message to acknowledge. /// The flags to use for this operation. /// The number of messages acknowledged. - /// https://redis.io/topics/streams-intro + /// long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None); /// @@ -2040,7 +2129,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The IDs of the messages to acknowledge. /// The flags to use for this operation. /// The number of messages acknowledged. - /// https://redis.io/topics/streams-intro + /// long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); /// @@ -2056,7 +2145,7 @@ IEnumerable SortedSetScan(RedisKey key, /// If true, the "~" argument is used to allow the stream to exceed max length by a small number. This improves performance when removing messages. /// The flags to use for this operation. /// The ID of the newly created message. - /// https://redis.io/commands/xadd + /// RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); /// @@ -2071,7 +2160,7 @@ IEnumerable SortedSetScan(RedisKey key, /// If true, the "~" argument is used to allow the stream to exceed max length by a small number. This improves performance when removing messages. /// The flags to use for this operation. /// The ID of the newly created message. - /// https://redis.io/commands/xadd + /// RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); /// @@ -2085,7 +2174,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The IDs of the messages to claim for the given consumer. /// The flags to use for this operation. /// The messages successfully claimed by the given consumer. - /// https://redis.io/topics/streams-intro + /// StreamEntry[] StreamClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); /// @@ -2099,7 +2188,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The IDs of the messages to claim for the given consumer. /// The flags to use for this operation. /// The message IDs for the messages successfully claimed by the given consumer. - /// https://redis.io/topics/streams-intro + /// RedisValue[] StreamClaimIdsOnly(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); /// @@ -2110,6 +2199,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The position from which to read for the consumer group. /// The flags to use for this operation. /// if successful, otherwise. + /// bool StreamConsumerGroupSetPosition(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None); /// @@ -2120,7 +2210,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The consumer group name. /// The flags to use for this operation. /// An instance of for each of the consumer group's consumers. - /// https://redis.io/topics/streams-intro + /// StreamConsumerInfo[] StreamConsumerInfo(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); /// @@ -2131,7 +2221,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The position to begin reading the stream. Defaults to . /// The flags to use for this operation. /// if the group was created, otherwise. - /// https://redis.io/topics/streams-intro + /// bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisValue? position, CommandFlags flags); /// @@ -2143,7 +2233,7 @@ IEnumerable SortedSetScan(RedisKey key, /// Create the stream if it does not already exist. /// The flags to use for this operation. /// if the group was created, otherwise. - /// https://redis.io/topics/streams-intro + /// bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None); /// @@ -2153,7 +2243,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The IDs of the messages to delete. /// The flags to use for this operation. /// Returns the number of messages successfully deleted from the stream. - /// https://redis.io/topics/streams-intro + /// long StreamDelete(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); /// @@ -2164,6 +2254,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The name of the consumer. /// The flags to use for this operation. /// The number of messages that were pending for the deleted consumer. + /// long StreamDeleteConsumer(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None); /// @@ -2173,6 +2264,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The name of the consumer group. /// The flags to use for this operation. /// if deleted, otherwise. + /// bool StreamDeleteConsumerGroup(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); /// @@ -2181,7 +2273,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the stream. /// The flags to use for this operation. /// An instance of for each of the stream's groups. - /// https://redis.io/topics/streams-intro + /// StreamGroupInfo[] StreamGroupInfo(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2190,7 +2282,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the stream. /// The flags to use for this operation. /// A instance with information about the stream. - /// https://redis.io/topics/streams-intro + /// StreamInfo StreamInfo(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2199,7 +2291,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the stream. /// The flags to use for this operation. /// The number of entries inside the given stream. - /// https://redis.io/commands/xlen + /// long StreamLength(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2215,7 +2307,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The highest and lowest ID of the pending messages, and the consumers with their pending message count. /// /// The equivalent of calling XPENDING key group. - /// https://redis.io/commands/xpending + /// StreamPendingInfo StreamPending(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); /// @@ -2230,7 +2322,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The flags to use for this operation. /// An instance of for each pending message. /// Equivalent of calling XPENDING key group start-id end-id count consumer-name. - /// https://redis.io/commands/xpending + /// StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None); /// @@ -2243,7 +2335,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The order of the messages. will execute XRANGE and will execute XREVRANGE. /// The flags to use for this operation. /// Returns an instance of for each message returned. - /// https://redis.io/commands/xrange + /// StreamEntry[] StreamRange(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None); /// @@ -2254,8 +2346,10 @@ IEnumerable SortedSetScan(RedisKey key, /// The maximum number of messages to return. /// The flags to use for this operation. /// Returns an instance of for each message returned. - /// Equivalent of calling XREAD COUNT num STREAMS key id. - /// https://redis.io/commands/xread + /// + /// Equivalent of calling XREAD COUNT num STREAMS key id. + /// + /// StreamEntry[] StreamRead(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None); /// @@ -2265,8 +2359,10 @@ IEnumerable SortedSetScan(RedisKey key, /// The maximum number of messages to return from each stream. /// The flags to use for this operation. /// A value of for each stream. - /// Equivalent of calling XREAD COUNT num STREAMS key1 key2 id1 id2. - /// https://redis.io/commands/xread + /// + /// Equivalent of calling XREAD COUNT num STREAMS key1 key2 id1 id2. + /// + /// RedisStream[] StreamRead(StreamPosition[] streamPositions, int? countPerStream = null, CommandFlags flags = CommandFlags.None); /// @@ -2279,7 +2375,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The maximum number of messages to return. /// The flags to use for this operation. /// Returns a value of for each message returned. - /// https://redis.io/commands/xreadgroup + /// StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, CommandFlags flags); /// @@ -2293,7 +2389,7 @@ IEnumerable SortedSetScan(RedisKey key, /// When true, the message will not be added to the pending message list. /// The flags to use for this operation. /// Returns a value of for each message returned. - /// https://redis.io/commands/xreadgroup + /// StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None); /// @@ -2306,8 +2402,10 @@ IEnumerable SortedSetScan(RedisKey key, /// The maximum number of messages to return from each stream. /// The flags to use for this operation. /// A value of for each stream. - /// Equivalent of calling XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2 - /// https://redis.io/commands/xreadgroup + /// + /// Equivalent of calling XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2. + /// + /// RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags); /// @@ -2321,8 +2419,10 @@ IEnumerable SortedSetScan(RedisKey key, /// When true, the message will not be added to the pending message list. /// The flags to use for this operation. /// A value of for each stream. - /// Equivalent of calling XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2 - /// https://redis.io/commands/xreadgroup + /// + /// Equivalent of calling XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2. + /// + /// RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None); /// @@ -2333,7 +2433,7 @@ IEnumerable SortedSetScan(RedisKey key, /// If true, the "~" argument is used to allow the stream to exceed max length by a small number. This improves performance when removing messages. /// The flags to use for this operation. /// The number of messages removed from the stream. - /// https://redis.io/topics/streams-intro + /// long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); /// @@ -2344,7 +2444,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The value to append to the string. /// The flags to use for this operation. /// The length of the string after the append operation. - /// https://redis.io/commands/append + /// long StringAppend(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -2358,7 +2458,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The end byte to count at. /// The flags to use for this operation. /// The number of bits set to 1. - /// https://redis.io/commands/bitcount + /// long StringBitCount(RedisKey key, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None); /// @@ -2373,7 +2473,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The second key to get the bit value from. /// The flags to use for this operation. /// The size of the string stored in the destination key, that is equal to the size of the longest input string. - /// https://redis.io/commands/bitop + /// long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, CommandFlags flags = CommandFlags.None); /// @@ -2386,7 +2486,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The keys to get the bit values from. /// The flags to use for this operation. /// The size of the string stored in the destination key, that is equal to the size of the longest input string. - /// https://redis.io/commands/bitop + /// long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// @@ -2400,9 +2500,11 @@ IEnumerable SortedSetScan(RedisKey key, /// The position to start looking (defaults to 0). /// The position to stop looking (defaults to -1, unlimited). /// The flags to use for this operation. - /// The command returns the position of the first bit set to 1 or 0 according to the request. - /// If we look for set bits(the bit argument is 1) and the string is empty or composed of just zero bytes, -1 is returned. - /// https://redis.io/commands/bitpos + /// + /// The command returns the position of the first bit set to 1 or 0 according to the request. + /// If we look for set bits(the bit argument is 1) and the string is empty or composed of just zero bytes, -1 is returned. + /// + /// long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None); /// @@ -2415,8 +2517,10 @@ IEnumerable SortedSetScan(RedisKey key, /// The amount to decrement by (defaults to 1). /// The flags to use for this operation. /// The value of key after the decrement. - /// https://redis.io/commands/decrby - /// https://redis.io/commands/decr + /// + /// , + /// + /// long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); /// @@ -2428,7 +2532,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The amount to decrement by (defaults to 1). /// The flags to use for this operation. /// The value of key after the decrement. - /// https://redis.io/commands/incrbyfloat + /// double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None); /// @@ -2438,7 +2542,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the string. /// The flags to use for this operation. /// The value of key, or nil when key does not exist. - /// https://redis.io/commands/get + /// RedisValue StringGet(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2448,7 +2552,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The keys of the strings. /// The flags to use for this operation. /// The values of the strings with nil for keys do not exist. - /// https://redis.io/commands/mget + /// RedisValue[] StringGet(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// @@ -2458,7 +2562,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the string. /// The flags to use for this operation. /// The value of key, or nil when key does not exist. - /// https://redis.io/commands/get + /// Lease? StringGetLease(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2469,7 +2573,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The offset in the string to get a bit at. /// The flags to use for this operation. /// The bit value stored at offset. - /// https://redis.io/commands/getbit + /// bool StringGetBit(RedisKey key, long offset, CommandFlags flags = CommandFlags.None); /// @@ -2482,7 +2586,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The end index of the substring to get. /// The flags to use for this operation. /// The substring of the string value stored at key. - /// https://redis.io/commands/getrange + /// RedisValue StringGetRange(RedisKey key, long start, long end, CommandFlags flags = CommandFlags.None); /// @@ -2492,7 +2596,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The value to replace the existing value with. /// The flags to use for this operation. /// The old value stored at key, or nil when key did not exist. - /// https://redis.io/commands/getset + /// RedisValue StringGetSet(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -2503,7 +2607,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The expiry to set. will remove expiry. /// The flags to use for this operation. /// The value of key, or nil when key does not exist. - /// https://redis.io/commands/getex + /// RedisValue StringGetSetExpiry(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); /// @@ -2514,7 +2618,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The exact date and time to expire at. will remove expiry. /// The flags to use for this operation. /// The value of key, or nil when key does not exist. - /// https://redis.io/commands/getex + /// RedisValue StringGetSetExpiry(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None); /// @@ -2525,7 +2629,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the string. /// The flags to use for this operation. /// The value of key, or nil when key does not exist. - /// https://redis.io/commands/getdelete + /// RedisValue StringGetDelete(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2536,7 +2640,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the string. /// The flags to use for this operation. /// The value of key and its expiry, or nil when key does not exist. - /// https://redis.io/commands/get + /// RedisValueWithExpiry StringGetWithExpiry(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2549,8 +2653,10 @@ IEnumerable SortedSetScan(RedisKey key, /// The amount to increment by (defaults to 1). /// The flags to use for this operation. /// The value of key after the increment. - /// https://redis.io/commands/incrby - /// https://redis.io/commands/incr + /// + /// , + /// + /// long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); /// @@ -2562,7 +2668,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The amount to increment by (defaults to 1). /// The flags to use for this operation. /// The value of key after the increment. - /// https://redis.io/commands/incrbyfloat + /// double StringIncrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None); /// @@ -2571,7 +2677,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the string. /// The flags to use for this operation. /// The length of the string at key, or 0 when key does not exist. - /// https://redis.io/commands/strlen + /// long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2596,7 +2702,7 @@ IEnumerable SortedSetScan(RedisKey key, /// Which condition to set the value under (defaults to always). /// The flags to use for this operation. /// if the string was set, otherwise. - /// https://redis.io/commands/set + /// bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -2607,8 +2713,10 @@ IEnumerable SortedSetScan(RedisKey key, /// Which condition to set the value under (defaults to always). /// The flags to use for this operation. /// if the keys were set, otherwise. - /// https://redis.io/commands/mset - /// https://redis.io/commands/msetnx + /// + /// , + /// + /// bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -2620,8 +2728,10 @@ IEnumerable SortedSetScan(RedisKey key, /// Which condition to set the value under (defaults to ). /// The flags to use for this operation. /// The previous value stored at , or nil when key did not exist. - /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. - /// https://redis.io/commands/set + /// + /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. + /// + /// RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags); /// @@ -2635,7 +2745,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The flags to use for this operation. /// The previous value stored at , or nil when key did not exist. /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. - /// https://redis.io/commands/set + /// RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -2648,7 +2758,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The bit value to set, true for 1, false for 0. /// The flags to use for this operation. /// The original bit value stored at offset. - /// https://redis.io/commands/setbit + /// bool StringSetBit(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None); /// @@ -2661,25 +2771,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The value to overwrite with. /// The flags to use for this operation. /// The length of the string after it was modified by the command. - /// https://redis.io/commands/setrange + /// RedisValue StringSetRange(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None); - - /// - /// Alters the last access time of a key. - /// - /// The key to touch. - /// The flags to use for this operation. - /// if the key was touched, otherwise. - /// https://redis.io/commands/touch - bool KeyTouch(RedisKey key, CommandFlags flags = CommandFlags.None); - - /// - /// Alters the last access time of the specified . A key is ignored if it does not exist. - /// - /// The keys to touch. - /// The flags to use for this operation. - /// The number of keys that were touched. - /// https://redis.io/commands/touch - long KeyTouch(RedisKey[] keys, CommandFlags flags = CommandFlags.None); } } diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index a54681ce5..3400010cf 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -27,7 +27,7 @@ public interface IDatabaseAsync : IRedisAsync /// The timeout to use for the transfer. /// The options to use for this migration. /// The flags to use for this operation. - /// https://redis.io/commands/MIGRATE + /// Task KeyMigrateAsync(RedisKey key, EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, MigrateOptions migrateOptions = MigrateOptions.None, CommandFlags flags = CommandFlags.None); /// @@ -37,7 +37,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key to debug. /// The flags to use for this migration. /// The raw output from DEBUG OBJECT. - /// https://redis.io/commands/debug-object + /// Task DebugObjectAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -51,7 +51,7 @@ public interface IDatabaseAsync : IRedisAsync /// The value to set at this entry. /// The flags to use for this operation. /// if the specified member was not already present in the set, else . - /// https://redis.io/commands/geoadd + /// Task GeoAddAsync(RedisKey key, double longitude, double latitude, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -63,8 +63,8 @@ public interface IDatabaseAsync : IRedisAsync /// The geo value to store. /// The flags to use for this operation. /// if the specified member was not already present in the set, else . - /// https://redis.io/commands/geoadd - Task GeoAddAsync(RedisKey key, StackExchange.Redis.GeoEntry value, CommandFlags flags = CommandFlags.None); + /// + Task GeoAddAsync(RedisKey key, GeoEntry value, CommandFlags flags = CommandFlags.None); /// /// Add the specified members to the set stored at key. @@ -75,7 +75,7 @@ public interface IDatabaseAsync : IRedisAsync /// The geo values add to the set. /// The flags to use for this operation. /// The number of elements that were added to the set, not including all the elements already present into the set. - /// https://redis.io/commands/geoadd + /// Task GeoAddAsync(RedisKey key, GeoEntry[] values, CommandFlags flags = CommandFlags.None); /// @@ -86,7 +86,7 @@ public interface IDatabaseAsync : IRedisAsync /// The geo value to remove. /// The flags to use for this operation. /// if the member existed in the sorted set and was removed, else . - /// https://redis.io/commands/zrem + /// Task GeoRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -98,7 +98,7 @@ public interface IDatabaseAsync : IRedisAsync /// The unit of distance to return (defaults to meters). /// The flags to use for this operation. /// The command returns the distance as a double (represented as a string) in the specified unit, or if one or both the elements are missing. - /// https://redis.io/commands/geodist + /// Task GeoDistanceAsync(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None); /// @@ -108,7 +108,7 @@ public interface IDatabaseAsync : IRedisAsync /// The members to get. /// The flags to use for this operation. /// The command returns an array where each element is the Geohash corresponding to each member name passed as argument to the command. - /// https://redis.io/commands/geohash + /// Task GeoHashAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); /// @@ -118,7 +118,7 @@ public interface IDatabaseAsync : IRedisAsync /// The member to get. /// The flags to use for this operation. /// The command returns an array where each element is the Geohash corresponding to each member name passed as argument to the command. - /// https://redis.io/commands/geohash + /// Task GeoHashAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -131,7 +131,7 @@ public interface IDatabaseAsync : IRedisAsync /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command. /// Non existing elements are reported as NULL elements of the array. /// - /// https://redis.io/commands/geopos + /// Task GeoPositionAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); /// @@ -144,7 +144,7 @@ public interface IDatabaseAsync : IRedisAsync /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command. /// Non existing elements are reported as NULL elements of the array. /// - /// https://redis.io/commands/geopos + /// Task GeoPositionAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -160,7 +160,7 @@ public interface IDatabaseAsync : IRedisAsync /// The search options to use. /// The flags to use for this operation. /// The results found within the radius, if any. - /// https://redis.io/commands/georadius + /// Task GeoRadiusAsync(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); /// @@ -177,7 +177,7 @@ public interface IDatabaseAsync : IRedisAsync /// The search options to use. /// The flags to use for this operation. /// The results found within the radius, if any. - /// https://redis.io/commands/georadius + /// Task GeoRadiusAsync(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); /// @@ -193,7 +193,7 @@ public interface IDatabaseAsync : IRedisAsync /// The search options to use. /// The flags for this operation. /// The results found within the shape, if any. - /// https://redis.io/commands/geosearch + /// Task GeoSearchAsync(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); /// @@ -210,7 +210,7 @@ public interface IDatabaseAsync : IRedisAsync /// The search options to use. /// The flags for this operation. /// The results found within the shape, if any. - /// /// https://redis.io/commands/geosearch + /// Task GeoSearchAsync(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); /// @@ -227,7 +227,7 @@ public interface IDatabaseAsync : IRedisAsync /// If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set. /// The flags for this operation. /// The size of the set stored at . - /// https://redis.io/commands/geosearchstore + /// Task GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); /// @@ -245,7 +245,7 @@ public interface IDatabaseAsync : IRedisAsync /// If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set. /// The flags for this operation. /// The size of the set stored at . - /// https://redis.io/commands/geosearchstore + /// Task GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); /// @@ -258,8 +258,10 @@ public interface IDatabaseAsync : IRedisAsync /// The amount to decrement by. /// The flags to use for this operation. /// The value at field after the decrement operation. - /// The range of values supported by HINCRBY is limited to 64 bit signed integers. - /// https://redis.io/commands/hincrby + /// + /// The range of values supported by HINCRBY is limited to 64 bit signed integers. + /// + /// Task HashDecrementAsync(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None); /// @@ -271,8 +273,10 @@ public interface IDatabaseAsync : IRedisAsync /// The amount to decrement by. /// The flags to use for this operation. /// The value at field after the decrement operation. - /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. - /// https://redis.io/commands/hincrbyfloat + /// + /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. + /// + /// Task HashDecrementAsync(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None); /// @@ -283,7 +287,7 @@ public interface IDatabaseAsync : IRedisAsync /// The field in the hash to delete. /// The flags to use for this operation. /// The number of fields that were removed. - /// https://redis.io/commands/hdel + /// Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// @@ -294,7 +298,7 @@ public interface IDatabaseAsync : IRedisAsync /// The fields in the hash to delete. /// The flags to use for this operation. /// The number of fields that were removed. - /// https://redis.io/commands/hdel + /// Task HashDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); /// @@ -304,7 +308,7 @@ public interface IDatabaseAsync : IRedisAsync /// The field in the hash to check. /// The flags to use for this operation. /// if the hash contains field, if the hash does not contain field, or key does not exist. - /// https://redis.io/commands/hexists + /// Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// @@ -314,7 +318,7 @@ public interface IDatabaseAsync : IRedisAsync /// The field in the hash to get. /// The flags to use for this operation. /// The value associated with field, or nil when field is not present in the hash or key does not exist. - /// https://redis.io/commands/hget + /// Task HashGetAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// @@ -324,7 +328,7 @@ public interface IDatabaseAsync : IRedisAsync /// The field in the hash to get. /// The flags to use for this operation. /// The value associated with field, or nil when field is not present in the hash or key does not exist. - /// https://redis.io/commands/hget + /// Task?> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// @@ -335,7 +339,7 @@ public interface IDatabaseAsync : IRedisAsync /// The fields in the hash to get. /// The flags to use for this operation. /// List of values associated with the given fields, in the same order as they are requested. - /// https://redis.io/commands/hmget + /// Task HashGetAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); /// @@ -344,7 +348,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the hash to get all entries from. /// The flags to use for this operation. /// List of fields and their values stored in the hash, or an empty list when key does not exist. - /// https://redis.io/commands/hgetall + /// Task HashGetAllAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -357,8 +361,10 @@ public interface IDatabaseAsync : IRedisAsync /// The amount to increment by. /// The flags to use for this operation. /// The value at field after the increment operation. - /// The range of values supported by HINCRBY is limited to 64 bit signed integers. - /// https://redis.io/commands/hincrby + /// + /// The range of values supported by HINCRBY is limited to 64 bit signed integers. + /// + /// Task HashIncrementAsync(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None); /// @@ -370,8 +376,10 @@ public interface IDatabaseAsync : IRedisAsync /// The amount to increment by. /// The flags to use for this operation. /// The value at field after the increment operation. - /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. - /// https://redis.io/commands/hincrbyfloat + /// + /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. + /// + /// Task HashIncrementAsync(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None); /// @@ -380,7 +388,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the hash. /// The flags to use for this operation. /// List of fields in the hash, or an empty list when key does not exist. - /// https://redis.io/commands/hkeys + /// Task HashKeysAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -389,7 +397,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the hash. /// The flags to use for this operation. /// The number of fields in the hash, or 0 when key does not exist. - /// https://redis.io/commands/hlen + /// Task HashLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -398,7 +406,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the hash. /// The flags to use for this operation. /// A random hash field name or if the hash does not exist. - /// https://redis.io/commands/hrandfield + /// Task HashRandomFieldAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -408,7 +416,7 @@ public interface IDatabaseAsync : IRedisAsync /// The number of fields to return. /// The flags to use for this operation. /// An array of hash field names of size of at most , or if the hash does not exist. - /// https://redis.io/commands/hrandfield + /// Task HashRandomFieldsAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -418,7 +426,7 @@ public interface IDatabaseAsync : IRedisAsync /// The number of fields to return. /// The flags to use for this operation. /// An array of hash entries of size of at most , or if the hash does not exist. - /// https://redis.io/commands/hrandfield + /// Task HashRandomFieldsWithValuesAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -432,7 +440,7 @@ public interface IDatabaseAsync : IRedisAsync /// The page offset to start at. /// The flags to use for this operation. /// Yields all elements of the hash matching the pattern. - /// https://redis.io/commands/hscan + /// IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// @@ -443,7 +451,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the hash. /// The entries to set in the hash. /// The flags to use for this operation. - /// https://redis.io/commands/hmset + /// Task HashSetAsync(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None); /// @@ -457,8 +465,10 @@ public interface IDatabaseAsync : IRedisAsync /// Which conditions under which to set the field value (defaults to always). /// The flags to use for this operation. /// if field is a new field in the hash and value was set, if field already exists in the hash and the value was updated. - /// https://redis.io/commands/hset - /// https://redis.io/commands/hsetnx + /// + /// , + /// + /// Task HashSetAsync(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -468,7 +478,7 @@ public interface IDatabaseAsync : IRedisAsync /// The field containing the string /// The flags to use for this operation. /// The length of the string at field, or 0 when key does not exist. - /// https://redis.io/commands/hstrlen + /// Task HashStringLengthAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// @@ -477,7 +487,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the hash. /// The flags to use for this operation. /// List of values in the hash, or an empty list when key does not exist. - /// https://redis.io/commands/hvals + /// Task HashValuesAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -487,7 +497,7 @@ public interface IDatabaseAsync : IRedisAsync /// The value to add. /// The flags to use for this operation. /// if at least 1 HyperLogLog internal register was altered, otherwise. - /// https://redis.io/commands/pfadd + /// Task HyperLogLogAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -497,7 +507,7 @@ public interface IDatabaseAsync : IRedisAsync /// The values to add. /// The flags to use for this operation. /// if at least 1 HyperLogLog internal register was altered, otherwise. - /// https://redis.io/commands/pfadd + /// Task HyperLogLogAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); /// @@ -506,7 +516,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the hyperloglog. /// The flags to use for this operation. /// The approximated number of unique elements observed via HyperLogLogAdd. - /// https://redis.io/commands/pfcount + /// Task HyperLogLogLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -515,7 +525,7 @@ public interface IDatabaseAsync : IRedisAsync /// The keys of the hyperloglogs. /// The flags to use for this operation. /// The approximated number of unique elements observed via HyperLogLogAdd. - /// https://redis.io/commands/pfcount + /// Task HyperLogLogLengthAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// @@ -525,7 +535,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the first hyperloglog to merge. /// The key of the first hyperloglog to merge. /// The flags to use for this operation. - /// https://redis.io/commands/pfmerge + /// Task HyperLogLogMergeAsync(RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); /// @@ -534,7 +544,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the merged hyperloglog. /// The keys of the hyperloglogs to merge. /// The flags to use for this operation. - /// https://redis.io/commands/pfmerge + /// Task HyperLogLogMergeAsync(RedisKey destination, RedisKey[] sourceKeys, CommandFlags flags = CommandFlags.None); /// @@ -554,7 +564,7 @@ public interface IDatabaseAsync : IRedisAsync /// Whether to overwrite an existing values at . If and the key exists, the copy will not succeed. /// The flags to use for this operation. /// if key was copied. if key was not copied. - /// https://redis.io/commands/copy + /// Task KeyCopyAsync(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None); /// @@ -564,8 +574,10 @@ public interface IDatabaseAsync : IRedisAsync /// The key to delete. /// The flags to use for this operation. /// if the key was removed. - /// https://redis.io/commands/del - /// https://redis.io/commands/unlink + /// + /// , + /// + /// Task KeyDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -575,8 +587,10 @@ public interface IDatabaseAsync : IRedisAsync /// The keys to delete. /// The flags to use for this operation. /// The number of keys that were removed. - /// https://redis.io/commands/del - /// https://redis.io/commands/unlink + /// + /// , + /// + /// Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// @@ -586,7 +600,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key to dump. /// The flags to use for this operation. /// The serialized value. - /// https://redis.io/commands/dump + /// Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -595,7 +609,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key to dump. /// The flags to use for this operation. /// The Redis encoding for the value or is the key does not exist. - /// https://redis.io/commands/object-encoding + /// Task KeyEncodingAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -604,7 +618,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key to check. /// The flags to use for this operation. /// if the key exists. if the key does not exist. - /// https://redis.io/commands/exists + /// Task KeyExistsAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -613,7 +627,7 @@ public interface IDatabaseAsync : IRedisAsync /// The keys to check. /// The flags to use for this operation. /// The number of keys that existed. - /// https://redis.io/commands/exists + /// Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// @@ -636,10 +650,12 @@ public interface IDatabaseAsync : IRedisAsync /// It is also possible to remove the timeout using the PERSIST command. /// See the page on key expiry for more information. /// + /// + /// , + /// , + /// + /// /// - /// https://redis.io/commands/expire - /// https://redis.io/commands/pexpire - /// https://redis.io/commands/persist Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); /// @@ -652,8 +668,10 @@ public interface IDatabaseAsync : IRedisAsync /// Since Redis 7.0.0, you can choose under which condition the expiration will be set using . /// The flags to use for this operation. /// if the timeout was set. if key does not exist or the timeout could not be set. - /// https://redis.io/commands/expire - /// https://redis.io/commands/pexpire + /// + /// , + /// + /// Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None); /// @@ -676,10 +694,12 @@ public interface IDatabaseAsync : IRedisAsync /// It is also possible to remove the timeout using the PERSIST command. /// See the page on key expiry for more information. /// + /// + /// , + /// , + /// + /// /// - /// https://redis.io/commands/expireat - /// https://redis.io/commands/pexpireat - /// https://redis.io/commands/persist Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None); /// @@ -692,8 +712,10 @@ public interface IDatabaseAsync : IRedisAsync /// Since Redis 7.0.0, you can choose under which condition the expiration will be set using . /// The flags to use for this operation. /// if the timeout was set. if key does not exist or the timeout could not be set. - /// https://redis.io/commands/expire - /// https://redis.io/commands/pexpire + /// + /// , + /// + /// Task KeyExpireAsync(RedisKey key, DateTime? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None); /// @@ -702,8 +724,10 @@ public interface IDatabaseAsync : IRedisAsync /// The key to get the expiration for. /// The flags to use for this operation. /// The time at which the given key will expire, or if the key does not exist or has no associated expiration time. - /// https://redis.io/commands/expiretime - /// https://redis.io/commands/pexpiretime + /// + /// , + /// + /// Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -712,7 +736,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key to get the time of. /// The flags to use for this operation. /// The time since the object stored at the specified key is idle. - /// https://redis.io/commands/object + /// Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -724,7 +748,7 @@ public interface IDatabaseAsync : IRedisAsync /// The database to move the key to. /// The flags to use for this operation. /// if key was moved. if key was not moved. - /// https://redis.io/commands/move + /// Task KeyMoveAsync(RedisKey key, int database, CommandFlags flags = CommandFlags.None); /// @@ -733,7 +757,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key to persist. /// The flags to use for this operation. /// if the timeout was removed. if key does not exist or does not have an associated timeout. - /// https://redis.io/commands/persist + /// Task KeyPersistAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -741,7 +765,7 @@ public interface IDatabaseAsync : IRedisAsync /// /// The flags to use for this operation. /// The random key, or nil when the database is empty. - /// https://redis.io/commands/randomkey + /// Task KeyRandomAsync(CommandFlags flags = CommandFlags.None); /// @@ -750,7 +774,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key to get a reference count for. /// The flags to use for this operation. /// The number of references ( if the key does not exist). - /// https://redis.io/commands/object-refcount + /// Task KeyRefCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -762,8 +786,10 @@ public interface IDatabaseAsync : IRedisAsync /// What conditions to rename under (defaults to always). /// The flags to use for this operation. /// if the key was renamed, otherwise. - /// https://redis.io/commands/rename - /// https://redis.io/commands/renamenx + /// + /// , + /// + /// Task KeyRenameAsync(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -774,7 +800,7 @@ public interface IDatabaseAsync : IRedisAsync /// The value of the key. /// The expiry to set. /// The flags to use for this operation. - /// https://redis.io/commands/restore + /// Task KeyRestoreAsync(RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None); /// @@ -784,9 +810,27 @@ public interface IDatabaseAsync : IRedisAsync /// The key to check. /// The flags to use for this operation. /// TTL, or nil when key does not exist or does not have a timeout. - /// https://redis.io/commands/ttl + /// Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Alters the last access time of a key. + /// + /// The key to touch. + /// The flags to use for this operation. + /// if the key was touched, otherwise. + /// + Task KeyTouchAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Alters the last access time of the specified . A key is ignored if it does not exist. + /// + /// The keys to touch. + /// The flags to use for this operation. + /// The number of keys that were touched. + /// + Task KeyTouchAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); + /// /// Returns the string representation of the type of the value stored at key. /// The different types that can be returned are: string, list, set, zset and hash. @@ -794,7 +838,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key to get the type of. /// The flags to use for this operation. /// Type of key, or none when key does not exist. - /// https://redis.io/commands/type + /// Task KeyTypeAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -807,7 +851,7 @@ public interface IDatabaseAsync : IRedisAsync /// The index position to get the value at. /// The flags to use for this operation. /// The requested element, or nil when index is out of range. - /// https://redis.io/commands/lindex + /// Task ListGetByIndexAsync(RedisKey key, long index, CommandFlags flags = CommandFlags.None); /// @@ -819,7 +863,7 @@ public interface IDatabaseAsync : IRedisAsync /// The value to insert. /// The flags to use for this operation. /// The length of the list after the insert operation, or -1 when the value pivot was not found. - /// https://redis.io/commands/linsert + /// Task ListInsertAfterAsync(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -831,7 +875,7 @@ public interface IDatabaseAsync : IRedisAsync /// The value to insert. /// The flags to use for this operation. /// The length of the list after the insert operation, or -1 when the value pivot was not found. - /// https://redis.io/commands/linsert + /// Task ListInsertBeforeAsync(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -840,7 +884,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the list. /// The flags to use for this operation. /// The value of the first element, or nil when key does not exist. - /// https://redis.io/commands/lpop + /// Task ListLeftPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -851,7 +895,7 @@ public interface IDatabaseAsync : IRedisAsync /// The number of elements to remove /// The flags to use for this operation. /// Array of values that were popped, or nil if the key doesn't exist. - /// https://redis.io/commands/lpop + /// Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -864,6 +908,7 @@ public interface IDatabaseAsync : IRedisAsync /// The maximum number of elements to scan through before stopping, defaults to 0 (a full scan of the list.) /// The flags to use for this operation. /// The 0-based index of the first matching element, or -1 if not found. + /// Task ListPositionAsync(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None); /// @@ -877,6 +922,7 @@ public interface IDatabaseAsync : IRedisAsync /// The maximum number of elements to scan through before stopping, defaults to 0 (a full scan of the list.) /// The flags to use for this operation. /// An array of at most of indexes of matching elements. If none are found, and empty array is returned. + /// Task ListPositionsAsync(RedisKey key, RedisValue element, long count, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None); /// @@ -888,8 +934,10 @@ public interface IDatabaseAsync : IRedisAsync /// Which conditions to add to the list under (defaults to always). /// The flags to use for this operation. /// The length of the list after the push operations. - /// https://redis.io/commands/lpush - /// https://redis.io/commands/lpushx + /// + /// , + /// + /// Task ListLeftPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -901,21 +949,23 @@ public interface IDatabaseAsync : IRedisAsync /// Which conditions to add to the list under (defaults to always). /// The flags to use for this operation. /// The length of the list after the push operations. - /// https://redis.io/commands/lpush - /// https://redis.io/commands/lpushx + /// + /// , + /// + /// Task ListLeftPushAsync(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); /// /// Insert all the specified values at the head of the list stored at key. /// If key does not exist, it is created as empty list before performing the push operations. /// Elements are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. - /// So for instance the command LPUSH mylist a b c will result into a list containing c as first element, b as second element and a as third element. + /// So for instance the command LPUSH mylist a b c will result into a list containing c as first element, b as second element and a as third element. /// /// The key of the list. /// The values to add to the head of the list. /// The flags to use for this operation. /// The length of the list after the push operations. - /// https://redis.io/commands/lpush + /// Task ListLeftPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags); /// @@ -924,7 +974,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the list. /// The flags to use for this operation. /// The length of the list at key. - /// https://redis.io/commands/llen + /// Task ListLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -937,7 +987,7 @@ public interface IDatabaseAsync : IRedisAsync /// What side of the list to move to. /// The flags to use for this operation. /// The element being popped and pushed or if there is no element to move. - /// https://redis.io/commands/lmove + /// Task ListMoveAsync(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, CommandFlags flags = CommandFlags.None); /// @@ -951,7 +1001,7 @@ public interface IDatabaseAsync : IRedisAsync /// The stop index of the list. /// The flags to use for this operation. /// List of elements in the specified range. - /// https://redis.io/commands/lrange + /// Task ListRangeAsync(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None); /// @@ -968,7 +1018,7 @@ public interface IDatabaseAsync : IRedisAsync /// The count behavior (see method summary). /// The flags to use for this operation. /// The number of removed elements. - /// https://redis.io/commands/lrem + /// Task ListRemoveAsync(RedisKey key, RedisValue value, long count = 0, CommandFlags flags = CommandFlags.None); /// @@ -977,7 +1027,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the list. /// The flags to use for this operation. /// The element being popped. - /// https://redis.io/commands/rpop + /// Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -988,7 +1038,7 @@ public interface IDatabaseAsync : IRedisAsync /// The number of elements to pop /// The flags to use for this operation. /// Array of values that were popped, or nil if the key doesn't exist. - /// https://redis.io/commands/rpop + /// Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -998,7 +1048,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the destination list. /// The flags to use for this operation. /// The element being popped and pushed. - /// https://redis.io/commands/rpoplpush + /// Task ListRightPopLeftPushAsync(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None); /// @@ -1010,8 +1060,10 @@ public interface IDatabaseAsync : IRedisAsync /// Which conditions to add to the list under. /// The flags to use for this operation. /// The length of the list after the push operation. - /// https://redis.io/commands/rpush - /// https://redis.io/commands/rpushx + /// + /// , + /// + /// Task ListRightPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -1023,21 +1075,23 @@ public interface IDatabaseAsync : IRedisAsync /// Which conditions to add to the list under. /// The flags to use for this operation. /// The length of the list after the push operation. - /// https://redis.io/commands/rpush - /// https://redis.io/commands/rpushx + /// + /// , + /// + /// Task ListRightPushAsync(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); /// /// Insert all the specified values at the tail of the list stored at key. /// If key does not exist, it is created as empty list before performing the push operation. /// Elements are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. - /// So for instance the command RPUSH mylist a b c will result into a list containing a as first element, b as second element and c as third element. + /// So for instance the command RPUSH mylist a b c will result into a list containing a as first element, b as second element and c as third element. /// /// The key of the list. /// The values to add to the tail of the list. /// The flags to use for this operation. /// The length of the list after the push operation. - /// https://redis.io/commands/rpush + /// Task ListRightPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags); /// @@ -1049,20 +1103,20 @@ public interface IDatabaseAsync : IRedisAsync /// The index to set the value at. /// The values to add to the list. /// The flags to use for this operation. - /// https://redis.io/commands/lset + /// Task ListSetByIndexAsync(RedisKey key, long index, RedisValue value, CommandFlags flags = CommandFlags.None); /// /// Trim an existing list so that it will contain only the specified range of elements specified. /// Both start and stop are zero-based indexes, where 0 is the first element of the list (the head), 1 the next element and so on. - /// For example: LTRIM foobar 0 2 will modify the list stored at foobar so that only the first three elements of the list will remain. + /// For example: LTRIM foobar 0 2 will modify the list stored at foobar so that only the first three elements of the list will remain. /// start and end can also be negative numbers indicating offsets from the end of the list, where -1 is the last element of the list, -2 the penultimate element and so on. /// /// The key of the list. /// The start index of the list to trim to. /// The end index of the list to trim to. /// The flags to use for this operation. - /// https://redis.io/commands/ltrim + /// Task ListTrimAsync(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None); /// @@ -1112,7 +1166,7 @@ public interface IDatabaseAsync : IRedisAsync /// The number of clients that received the message *on the destination server*, /// note that this doesn't mean much in a cluster as clients can get the message through other nodes. /// - /// https://redis.io/commands/publish + /// Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None); /// @@ -1121,8 +1175,8 @@ public interface IDatabaseAsync : IRedisAsync /// /// The command to run. /// The arguments to pass for the command. - /// This API should be considered an advanced feature; inappropriate use can be harmful. /// A dynamic representation of the command's result. + /// This API should be considered an advanced feature; inappropriate use can be harmful. Task ExecuteAsync(string command, params object[] args); /// @@ -1132,8 +1186,8 @@ public interface IDatabaseAsync : IRedisAsync /// The command to run. /// The arguments to pass for the command. /// The flags to use for this operation. - /// This API should be considered an advanced feature; inappropriate use can be harmful. /// A dynamic representation of the command's result. + /// This API should be considered an advanced feature; inappropriate use can be harmful. Task ExecuteAsync(string command, ICollection? args, CommandFlags flags = CommandFlags.None); /// @@ -1144,8 +1198,10 @@ public interface IDatabaseAsync : IRedisAsync /// The values to execute against. /// The flags to use for this operation. /// A dynamic representation of the script's result. - /// https://redis.io/commands/eval - /// https://redis.io/commands/evalsha + /// + /// , + /// + /// Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); /// @@ -1156,7 +1212,7 @@ public interface IDatabaseAsync : IRedisAsync /// The values to execute against. /// The flags to use for this operation. /// A dynamic representation of the script's result. - /// https://redis.io/commands/evalsha + /// Task ScriptEvaluateAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); /// @@ -1167,7 +1223,7 @@ public interface IDatabaseAsync : IRedisAsync /// The parameters to pass to the script. /// The flags to use for this operation. /// A dynamic representation of the script's result. - /// https://redis.io/commands/eval + /// Task ScriptEvaluateAsync(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None); /// @@ -1179,7 +1235,7 @@ public interface IDatabaseAsync : IRedisAsync /// The parameters to pass to the script. /// The flags to use for this operation. /// A dynamic representation of the script's result. - /// https://redis.io/commands/eval + /// Task ScriptEvaluateAsync(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None); /// @@ -1191,7 +1247,7 @@ public interface IDatabaseAsync : IRedisAsync /// The value to add to the set. /// The flags to use for this operation. /// if the specified member was not already present in the set, else . - /// https://redis.io/commands/sadd + /// Task SetAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -1203,7 +1259,7 @@ public interface IDatabaseAsync : IRedisAsync /// The values to add to the set. /// The flags to use for this operation. /// The number of elements that were added to the set, not including all the elements already present into the set. - /// https://redis.io/commands/sadd + /// Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); /// @@ -1214,9 +1270,11 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the second set. /// The flags to use for this operation. /// List with members of the resulting set. - /// https://redis.io/commands/sunion - /// https://redis.io/commands/sinter - /// https://redis.io/commands/sdiff + /// + /// , + /// , + /// + /// Task SetCombineAsync(SetOperation operation, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); /// @@ -1226,9 +1284,11 @@ public interface IDatabaseAsync : IRedisAsync /// The keys of the sets to operate on. /// The flags to use for this operation. /// List with members of the resulting set. - /// https://redis.io/commands/sunion - /// https://redis.io/commands/sinter - /// https://redis.io/commands/sdiff + /// + /// , + /// , + /// + /// Task SetCombineAsync(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// @@ -1241,9 +1301,11 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the second set. /// The flags to use for this operation. /// The number of elements in the resulting set. - /// https://redis.io/commands/sunionstore - /// https://redis.io/commands/sinterstore - /// https://redis.io/commands/sdiffstore + /// + /// , + /// , + /// + /// Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); /// @@ -1255,9 +1317,11 @@ public interface IDatabaseAsync : IRedisAsync /// The keys of the sets to operate on. /// The flags to use for this operation. /// The number of elements in the resulting set. - /// https://redis.io/commands/sunionstore - /// https://redis.io/commands/sinterstore - /// https://redis.io/commands/sdiffstore + /// + /// , + /// , + /// + /// Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// @@ -1270,7 +1334,7 @@ public interface IDatabaseAsync : IRedisAsync /// if the element is a member of the set. /// if the element is not a member of the set, or if key does not exist. /// - /// https://redis.io/commands/sismember + /// Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -1283,7 +1347,7 @@ public interface IDatabaseAsync : IRedisAsync /// if the element is a member of the set. /// if the element is not a member of the set, or if key does not exist. /// - /// https://redis.io/commands/smismember + /// Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); /// @@ -1299,7 +1363,7 @@ public interface IDatabaseAsync : IRedisAsync /// The number of elements to check (defaults to 0 and means unlimited). /// The flags to use for this operation. /// The cardinality (number of elements) of the set, or 0 if key does not exist. - /// https://redis.io/commands/scard + /// Task SetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None); /// @@ -1308,7 +1372,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the set. /// The flags to use for this operation. /// The cardinality (number of elements) of the set, or 0 if key does not exist. - /// https://redis.io/commands/scard + /// Task SetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -1317,7 +1381,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the set. /// The flags to use for this operation. /// All elements of the set. - /// https://redis.io/commands/smembers + /// Task SetMembersAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -1333,7 +1397,7 @@ public interface IDatabaseAsync : IRedisAsync /// if the element is moved. /// if the element is not a member of source and no operation was performed. /// - /// https://redis.io/commands/smove + /// Task SetMoveAsync(RedisKey source, RedisKey destination, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -1342,7 +1406,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the set. /// The flags to use for this operation. /// The removed element, or nil when key does not exist. - /// https://redis.io/commands/spop + /// Task SetPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -1352,16 +1416,16 @@ public interface IDatabaseAsync : IRedisAsync /// The number of elements to return. /// The flags to use for this operation. /// An array of elements, or an empty array when key does not exist. - /// https://redis.io/commands/spop + /// Task SetPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// - /// Return a random element from the set value stored at key. + /// Return a random element from the set value stored at . /// /// The key of the set. /// The flags to use for this operation. - /// The randomly selected element, or nil when key does not exist. - /// https://redis.io/commands/srandmember + /// The randomly selected element, or when does not exist. + /// Task SetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -1372,8 +1436,8 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the set. /// The count of members to get. /// The flags to use for this operation. - /// An array of elements, or an empty array when key does not exist. - /// https://redis.io/commands/srandmember + /// An array of elements, or an empty array when does not exist. + /// Task SetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -1384,7 +1448,7 @@ public interface IDatabaseAsync : IRedisAsync /// The value to remove. /// The flags to use for this operation. /// if the specified member was already present in the set, otherwise. - /// https://redis.io/commands/srem + /// Task SetRemoveAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -1395,7 +1459,7 @@ public interface IDatabaseAsync : IRedisAsync /// The values to remove. /// The flags to use for this operation. /// The number of members that were removed from the set, not including non existing members. - /// https://redis.io/commands/srem + /// Task SetRemoveAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); /// @@ -1409,7 +1473,7 @@ public interface IDatabaseAsync : IRedisAsync /// The page offset to start at. /// The flags to use for this operation. /// Yields all matching elements of the set. - /// https://redis.io/commands/sscan + /// IAsyncEnumerable SetScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// @@ -1429,7 +1493,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key pattern to sort by, if any e.g. ExternalKey_* would return the value of ExternalKey_{listvalue} for each entry. /// The flags to use for this operation. /// The sorted elements, or the external values if get is specified. - /// https://redis.io/commands/sort + /// Task SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); /// @@ -1437,7 +1501,7 @@ public interface IDatabaseAsync : IRedisAsync /// By default, the elements themselves are compared, but the values can also be used to perform external key-lookups using the by parameter. /// By default, the elements themselves are returned, but external key-lookups (one or many) can be performed instead by specifying /// the get parameter (note that # specifies the element itself, when used in get). - /// Referring to the redis SORT documentation for examples is recommended. + /// Referring to the redis SORT documentation for examples is recommended. /// When used in hashes, by and get can be used to specify fields using -> notation (again, refer to redis documentation). /// /// The destination key to store results in. @@ -1450,7 +1514,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key pattern to sort by, if any e.g. ExternalKey_* would return the value of ExternalKey_{listvalue} for each entry. /// The flags to use for this operation. /// The number of elements stored in the new list. - /// https://redis.io/commands/sort + /// Task SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); /// @@ -1462,7 +1526,7 @@ public interface IDatabaseAsync : IRedisAsync /// The score for the member to add to the sorted set. /// The flags to use for this operation. /// if the value was added. if it already existed (the score is still updated). - /// https://redis.io/commands/zadd + /// Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, CommandFlags flags); /// @@ -1475,7 +1539,7 @@ public interface IDatabaseAsync : IRedisAsync /// What conditions to add the element under (defaults to always). /// The flags to use for this operation. /// if the value was added. if it already existed (the score is still updated). - /// https://redis.io/commands/zadd + /// Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -1486,7 +1550,7 @@ public interface IDatabaseAsync : IRedisAsync /// The members and values to add to the sorted set. /// The flags to use for this operation. /// The number of elements added to the sorted sets, not including elements already existing for which the score was updated. - /// https://redis.io/commands/zadd + /// Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, CommandFlags flags); /// @@ -1498,7 +1562,7 @@ public interface IDatabaseAsync : IRedisAsync /// What conditions to add the element under (defaults to always). /// The flags to use for this operation. /// The number of elements added to the sorted sets, not including elements already existing for which the score was updated. - /// https://redis.io/commands/zadd + /// Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -1511,10 +1575,12 @@ public interface IDatabaseAsync : IRedisAsync /// The optional weights per set that correspond to . /// The aggregation method (defaults to ). /// The flags to use for this operation. - /// https://redis.io/commands/zunion - /// https://redis.io/commands/zinter - /// https://redis.io/commands/zdiff /// The resulting sorted set. + /// + /// , + /// , + /// + /// Task SortedSetCombineAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// @@ -1527,10 +1593,12 @@ public interface IDatabaseAsync : IRedisAsync /// The optional weights per set that correspond to . /// The aggregation method (defaults to ). /// The flags to use for this operation. - /// https://redis.io/commands/zunion - /// https://redis.io/commands/zinter - /// https://redis.io/commands/zdiff /// The resulting sorted set with scores. + /// + /// , + /// , + /// + /// Task SortedSetCombineWithScoresAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// @@ -1544,10 +1612,12 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the second sorted set. /// The aggregation method (defaults to sum). /// The flags to use for this operation. - /// https://redis.io/commands/zunionstore - /// https://redis.io/commands/zinterstore - /// https://redis.io/commands/zdiffstore /// The number of elements in the resulting sorted set at destination. + /// + /// , + /// , + /// + /// Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// @@ -1561,10 +1631,12 @@ public interface IDatabaseAsync : IRedisAsync /// The optional weights per set that correspond to . /// The aggregation method (defaults to sum). /// The flags to use for this operation. - /// https://redis.io/commands/zunionstore - /// https://redis.io/commands/zinterstore - /// https://redis.io/commands/zdiffstore /// The number of elements in the resulting sorted set at destination. + /// + /// , + /// , + /// + /// Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); /// @@ -1576,7 +1648,7 @@ public interface IDatabaseAsync : IRedisAsync /// The amount to decrement by. /// The flags to use for this operation. /// The new score of member. - /// https://redis.io/commands/zincrby + /// Task SortedSetDecrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None); /// @@ -1587,7 +1659,7 @@ public interface IDatabaseAsync : IRedisAsync /// The amount to increment by. /// The flags to use for this operation. /// The new score of member. - /// https://redis.io/commands/zincrby + /// Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None); /// @@ -1597,7 +1669,7 @@ public interface IDatabaseAsync : IRedisAsync /// If the intersection cardinality reaches partway through the computation, the algorithm will exit and yield as the cardinality (defaults to 0 meaning unlimited). /// The flags to use for this operation. /// The number of elements in the resulting intersection. - /// https://redis.io/commands/zintercard + /// Task SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None); /// @@ -1609,7 +1681,7 @@ public interface IDatabaseAsync : IRedisAsync /// Whether to exclude and from the range check (defaults to both inclusive). /// The flags to use for this operation. /// The cardinality (number of elements) of the sorted set, or 0 if key does not exist. - /// https://redis.io/commands/zcard + /// Task SortedSetLengthAsync(RedisKey key, double min = double.NegativeInfinity, double max = double.PositiveInfinity, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// @@ -1622,7 +1694,7 @@ public interface IDatabaseAsync : IRedisAsync /// Whether to exclude and from the range check (defaults to both inclusive). /// The flags to use for this operation. /// The number of elements in the specified score range. - /// https://redis.io/commands/zlexcount + /// Task SortedSetLengthByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// @@ -1631,7 +1703,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the sorted set. /// The flags to use for this operation. /// The randomly selected element, or when does not exist. - /// https://redis.io/commands/zrandmember + /// Task SortedSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -1650,7 +1722,7 @@ public interface IDatabaseAsync : IRedisAsync /// /// The flags to use for this operation. /// The randomly selected elements, or an empty array when does not exist. - /// https://redis.io/commands/zrandmember + /// Task SortedSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -1669,7 +1741,7 @@ public interface IDatabaseAsync : IRedisAsync /// /// The flags to use for this operation. /// The randomly selected elements with scores, or an empty array when does not exist. - /// https://redis.io/commands/zrandmember + /// Task SortedSetRandomMembersWithScoresAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); /// @@ -1685,8 +1757,10 @@ public interface IDatabaseAsync : IRedisAsync /// The order to sort by (defaults to ascending). /// The flags to use for this operation. /// List of elements in the specified range. - /// https://redis.io/commands/zrange - /// https://redis.io/commands/zrevrange + /// + /// , + /// + /// Task SortedSetRangeByRankAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); /// @@ -1707,8 +1781,8 @@ public interface IDatabaseAsync : IRedisAsync /// The number of elements into the sorted set to skip. Note: this iterates after sorting so incurs O(n) cost for large values. /// The maximum number of elements to pull into the new () set. /// The flags to use for this operation. - /// https://redis.io/commands/zrangestore /// The cardinality of (number of elements in) the newly created sorted set. + /// Task SortedSetRangeAndStoreAsync( RedisKey sourceKey, RedisKey destinationKey, @@ -1734,8 +1808,10 @@ Task SortedSetRangeAndStoreAsync( /// The order to sort by (defaults to ascending). /// The flags to use for this operation. /// List of elements in the specified range. - /// https://redis.io/commands/zrange - /// https://redis.io/commands/zrevrange + /// + /// , + /// + /// Task SortedSetRangeByRankWithScoresAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); /// @@ -1754,8 +1830,10 @@ Task SortedSetRangeAndStoreAsync( /// How many items to take. /// The flags to use for this operation. /// List of elements in the specified score range. - /// https://redis.io/commands/zrangebyscore - /// https://redis.io/commands/zrevrangebyscore + /// + /// , + /// + /// Task SortedSetRangeByScoreAsync(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, @@ -1781,8 +1859,10 @@ Task SortedSetRangeByScoreAsync(RedisKey key, /// How many items to take. /// The flags to use for this operation. /// List of elements in the specified score range. - /// https://redis.io/commands/zrangebyscore - /// https://redis.io/commands/zrevrangebyscore + /// + /// , + /// + /// Task SortedSetRangeByScoreWithScoresAsync(RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, @@ -1803,8 +1883,8 @@ Task SortedSetRangeByScoreWithScoresAsync(RedisKey key, /// How many items to skip. /// How many items to take. /// The flags to use for this operation. - /// https://redis.io/commands/zrangebylex /// List of elements in the specified score range. + /// Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, @@ -1825,9 +1905,11 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// How many items to skip. /// How many items to take. /// The flags to use for this operation. - /// https://redis.io/commands/zrangebylex - /// https://redis.io/commands/zrevrangebylex /// List of elements in the specified score range. + /// + /// , + /// + /// Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min = default, RedisValue max = default, @@ -1846,8 +1928,10 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The order to sort by (defaults to ascending). /// The flags to use for this operation. /// If member exists in the sorted set, the rank of member. If member does not exist in the sorted set or key does not exist, . - /// https://redis.io/commands/zrank - /// https://redis.io/commands/zrevrank + /// + /// , + /// + /// Task SortedSetRankAsync(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); /// @@ -1857,7 +1941,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The member to remove. /// The flags to use for this operation. /// if the member existed in the sorted set and was removed. otherwise. - /// https://redis.io/commands/zrem + /// Task SortedSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -1867,7 +1951,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The members to remove. /// The flags to use for this operation. /// The number of members removed from the sorted set, not including non existing members. - /// https://redis.io/commands/zrem + /// Task SortedSetRemoveAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); /// @@ -1881,7 +1965,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The maximum rank to remove. /// The flags to use for this operation. /// The number of elements removed. - /// https://redis.io/commands/zremrangebyrank + /// Task SortedSetRemoveRangeByRankAsync(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None); /// @@ -1893,7 +1977,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// Which of and to exclude (defaults to both inclusive). /// The flags to use for this operation. /// The number of elements removed. - /// https://redis.io/commands/zremrangebyscore + /// Task SortedSetRemoveRangeByScoreAsync(RedisKey key, double start, double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// @@ -1906,7 +1990,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// Which of and to exclude (defaults to both inclusive). /// The flags to use for this operation. /// The number of elements removed. - /// https://redis.io/commands/zremrangebylex + /// Task SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); /// @@ -1919,7 +2003,7 @@ Task SortedSetRangeByValueAsync(RedisKey key, /// The page offset to start at. /// The flags to use for this operation. /// Yields all matching elements of the sorted set. - /// https://redis.io/commands/zscan + /// IAsyncEnumerable SortedSetScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, @@ -1935,7 +2019,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The member to get a score for. /// The flags to use for this operation. /// The score of the member. - /// https://redis.io/commands/zscore + /// Task SortedSetScoreAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); /// @@ -1949,7 +2033,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The scores of the members in the same order as the array. /// If a member does not exist in the set, is returned. /// - /// https://redis.io/commands/zmscore + /// Task SortedSetScoresAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); /// @@ -1959,8 +2043,10 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The order to sort by (defaults to ascending). /// The flags to use for this operation. /// The removed element, or nil when key does not exist. - /// https://redis.io/commands/zpopmin - /// https://redis.io/commands/zpopmax + /// + /// , + /// + /// Task SortedSetPopAsync(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); /// @@ -1971,8 +2057,10 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The order to sort by (defaults to ascending). /// The flags to use for this operation. /// An array of elements, or an empty array when key does not exist. - /// https://redis.io/commands/zpopmin - /// https://redis.io/commands/zpopmax + /// + /// , + /// + /// Task SortedSetPopAsync(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); /// @@ -1983,7 +2071,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The ID of the message to acknowledge. /// The flags to use for this operation. /// The number of messages acknowledged. - /// https://redis.io/topics/streams-intro + /// Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None); /// @@ -1994,7 +2082,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The IDs of the messages to acknowledge. /// The flags to use for this operation. /// The number of messages acknowledged. - /// https://redis.io/topics/streams-intro + /// Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); /// @@ -2010,7 +2098,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// If true, the "~" argument is used to allow the stream to exceed max length by a small number. This improves performance when removing messages. /// The flags to use for this operation. /// The ID of the newly created message. - /// https://redis.io/commands/xadd + /// Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); /// @@ -2025,7 +2113,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// If true, the "~" argument is used to allow the stream to exceed max length by a small number. This improves performance when removing messages. /// The flags to use for this operation. /// The ID of the newly created message. - /// https://redis.io/commands/xadd + /// Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); /// @@ -2039,7 +2127,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The IDs of the messages to claim for the given consumer. /// The flags to use for this operation. /// The messages successfully claimed by the given consumer. - /// https://redis.io/topics/streams-intro + /// Task StreamClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); /// @@ -2053,7 +2141,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The IDs of the messages to claim for the given consumer. /// The flags to use for this operation. /// The message IDs for the messages successfully claimed by the given consumer. - /// https://redis.io/topics/streams-intro + /// Task StreamClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); /// @@ -2064,6 +2152,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The position from which to read for the consumer group. /// The flags to use for this operation. /// if successful, otherwise. + /// Task StreamConsumerGroupSetPositionAsync(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None); /// @@ -2074,7 +2163,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The consumer group name. /// The flags to use for this operation. /// An instance of for each of the consumer group's consumers. - /// https://redis.io/topics/streams-intro + /// Task StreamConsumerInfoAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); /// @@ -2085,7 +2174,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The position to begin reading the stream. Defaults to . /// The flags to use for this operation. /// if the group was created, otherwise. - /// https://redis.io/topics/streams-intro + /// Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position, CommandFlags flags); /// @@ -2097,7 +2186,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// Create the stream if it does not already exist. /// The flags to use for this operation. /// if the group was created, otherwise. - /// https://redis.io/topics/streams-intro + /// Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None); /// @@ -2107,7 +2196,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The IDs of the messages to delete. /// The flags to use for this operation. /// Returns the number of messages successfully deleted from the stream. - /// https://redis.io/topics/streams-intro + /// Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); /// @@ -2118,6 +2207,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The name of the consumer. /// The flags to use for this operation. /// The number of messages that were pending for the deleted consumer. + /// Task StreamDeleteConsumerAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None); /// @@ -2127,6 +2217,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The name of the consumer group. /// The flags to use for this operation. /// if deleted, otherwise. + /// Task StreamDeleteConsumerGroupAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); /// @@ -2135,7 +2226,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The key of the stream. /// The flags to use for this operation. /// An instance of for each of the stream's groups. - /// https://redis.io/topics/streams-intro + /// Task StreamGroupInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2144,7 +2235,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The key of the stream. /// The flags to use for this operation. /// A instance with information about the stream. - /// https://redis.io/topics/streams-intro + /// Task StreamInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2153,7 +2244,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The key of the stream. /// The flags to use for this operation. /// The number of entries inside the given stream. - /// https://redis.io/commands/xlen + /// Task StreamLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2169,7 +2260,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The highest and lowest ID of the pending messages, and the consumers with their pending message count. /// /// The equivalent of calling XPENDING key group. - /// https://redis.io/commands/xpending + /// Task StreamPendingAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); /// @@ -2184,7 +2275,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The flags to use for this operation. /// An instance of for each pending message. /// Equivalent of calling XPENDING key group start-id end-id count consumer-name. - /// https://redis.io/commands/xpending + /// Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None); /// @@ -2197,7 +2288,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The order of the messages. will execute XRANGE and will execute XREVRANGE. /// The flags to use for this operation. /// Returns an instance of for each message returned. - /// https://redis.io/commands/xrange + /// Task StreamRangeAsync(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None); /// @@ -2208,8 +2299,10 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The maximum number of messages to return. /// The flags to use for this operation. /// Returns an instance of for each message returned. - /// Equivalent of calling XREAD COUNT num STREAMS key id. - /// https://redis.io/commands/xread + /// + /// Equivalent of calling XREAD COUNT num STREAMS key id. + /// + /// Task StreamReadAsync(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None); /// @@ -2219,8 +2312,10 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The maximum number of messages to return from each stream. /// The flags to use for this operation. /// A value of for each stream. - /// Equivalent of calling XREAD COUNT num STREAMS key1 key2 id1 id2. - /// https://redis.io/commands/xread + /// + /// Equivalent of calling XREAD COUNT num STREAMS key1 key2 id1 id2. + /// + /// Task StreamReadAsync(StreamPosition[] streamPositions, int? countPerStream = null, CommandFlags flags = CommandFlags.None); /// @@ -2233,7 +2328,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The maximum number of messages to return. /// The flags to use for this operation. /// Returns a value of for each message returned. - /// https://redis.io/commands/xreadgroup + /// Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, CommandFlags flags); /// @@ -2247,7 +2342,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// When true, the message will not be added to the pending message list. /// The flags to use for this operation. /// Returns a value of for each message returned. - /// https://redis.io/commands/xreadgroup + /// Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None); /// @@ -2260,8 +2355,10 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The maximum number of messages to return from each stream. /// The flags to use for this operation. /// A value of for each stream. - /// Equivalent of calling XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2 - /// https://redis.io/commands/xreadgroup + /// + /// Equivalent of calling XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2. + /// + /// Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags); /// @@ -2275,8 +2372,10 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// When true, the message will not be added to the pending message list. /// The flags to use for this operation. /// A value of for each stream. - /// Equivalent of calling XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2 - /// https://redis.io/commands/xreadgroup + /// + /// Equivalent of calling XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2. + /// + /// Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None); /// @@ -2287,7 +2386,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// If true, the "~" argument is used to allow the stream to exceed max length by a small number. This improves performance when removing messages. /// The flags to use for this operation. /// The number of messages removed from the stream. - /// https://redis.io/topics/streams-intro + /// Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); /// @@ -2298,7 +2397,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The value to append to the string. /// The flags to use for this operation. /// The length of the string after the append operation. - /// https://redis.io/commands/append + /// Task StringAppendAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -2312,7 +2411,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The end byte to count at. /// The flags to use for this operation. /// The number of bits set to 1. - /// https://redis.io/commands/bitcount + /// Task StringBitCountAsync(RedisKey key, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None); /// @@ -2327,7 +2426,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The second key to get the bit value from. /// The flags to use for this operation. /// The size of the string stored in the destination key, that is equal to the size of the longest input string. - /// https://redis.io/commands/bitop + /// Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, CommandFlags flags = CommandFlags.None); /// @@ -2340,7 +2439,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The keys to get the bit values from. /// The flags to use for this operation. /// The size of the string stored in the destination key, that is equal to the size of the longest input string. - /// https://redis.io/commands/bitop + /// Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// @@ -2354,9 +2453,11 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The position to start looking (defaults to 0). /// The position to stop looking (defaults to -1, unlimited). /// The flags to use for this operation. - /// The command returns the position of the first bit set to 1 or 0 according to the request. - /// If we look for set bits(the bit argument is 1) and the string is empty or composed of just zero bytes, -1 is returned. - /// https://redis.io/commands/bitpos + /// + /// The command returns the position of the first bit set to 1 or 0 according to the request. + /// If we look for set bits(the bit argument is 1) and the string is empty or composed of just zero bytes, -1 is returned. + /// + /// Task StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None); /// @@ -2369,8 +2470,10 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The amount to decrement by (defaults to 1). /// The flags to use for this operation. /// The value of key after the decrement. - /// https://redis.io/commands/decrby - /// https://redis.io/commands/decr + /// + /// , + /// + /// Task StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); /// @@ -2382,7 +2485,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The amount to decrement by (defaults to 1). /// The flags to use for this operation. /// The value of key after the decrement. - /// https://redis.io/commands/incrbyfloat + /// Task StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None); /// @@ -2392,7 +2495,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The key of the string. /// The flags to use for this operation. /// The value of key, or nil when key does not exist. - /// https://redis.io/commands/get + /// Task StringGetAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2402,7 +2505,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The keys of the strings. /// The flags to use for this operation. /// The values of the strings with nil for keys do not exist. - /// https://redis.io/commands/mget + /// Task StringGetAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// @@ -2412,7 +2515,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The key of the string. /// The flags to use for this operation. /// The value of key, or nil when key does not exist. - /// https://redis.io/commands/get + /// Task?> StringGetLeaseAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2423,7 +2526,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The offset in the string to get a bit at. /// The flags to use for this operation. /// The bit value stored at offset. - /// https://redis.io/commands/getbit + /// Task StringGetBitAsync(RedisKey key, long offset, CommandFlags flags = CommandFlags.None); /// @@ -2436,7 +2539,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The end index of the substring to get. /// The flags to use for this operation. /// The substring of the string value stored at key. - /// https://redis.io/commands/getrange + /// Task StringGetRangeAsync(RedisKey key, long start, long end, CommandFlags flags = CommandFlags.None); /// @@ -2446,7 +2549,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The value to replace the existing value with. /// The flags to use for this operation. /// The old value stored at key, or nil when key did not exist. - /// https://redis.io/commands/getset + /// Task StringGetSetAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -2457,7 +2560,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The expiry to set. will remove expiry. /// The flags to use for this operation. /// The value of key, or nil when key does not exist. - /// https://redis.io/commands/getex + /// Task StringGetSetExpiryAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); /// @@ -2468,7 +2571,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The exact date and time to expire at. will remove expiry. /// The flags to use for this operation. /// The value of key, or nil when key does not exist. - /// https://redis.io/commands/getex + /// Task StringGetSetExpiryAsync(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None); /// @@ -2479,7 +2582,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The key of the string. /// The flags to use for this operation. /// The value of key, or nil when key does not exist. - /// https://redis.io/commands/getdelete + /// Task StringGetDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2490,7 +2593,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The key of the string. /// The flags to use for this operation. /// The value of key and its expiry, or nil when key does not exist. - /// https://redis.io/commands/get + /// Task StringGetWithExpiryAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2503,8 +2606,10 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The amount to increment by (defaults to 1). /// The flags to use for this operation. /// The value of key after the increment. - /// https://redis.io/commands/incrby - /// https://redis.io/commands/incr + /// + /// , + /// + /// Task StringIncrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); /// @@ -2516,7 +2621,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The amount to increment by (defaults to 1). /// The flags to use for this operation. /// The value of key after the increment. - /// https://redis.io/commands/incrbyfloat + /// Task StringIncrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None); /// @@ -2525,7 +2630,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The key of the string. /// The flags to use for this operation. /// The length of the string at key, or 0 when key does not exist. - /// https://redis.io/commands/strlen + /// Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// @@ -2550,7 +2655,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// Which condition to set the value under (defaults to always). /// The flags to use for this operation. /// if the string was set, otherwise. - /// https://redis.io/commands/set + /// Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -2561,8 +2666,10 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// Which condition to set the value under (defaults to always). /// The flags to use for this operation. /// if the keys were set, otherwise. - /// https://redis.io/commands/mset - /// https://redis.io/commands/msetnx + /// + /// , + /// + /// Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -2574,8 +2681,10 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// Which condition to set the value under (defaults to ). /// The flags to use for this operation. /// The previous value stored at , or nil when key did not exist. - /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. - /// https://redis.io/commands/set + /// + /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. + /// + /// Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags); /// @@ -2589,7 +2698,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The flags to use for this operation. /// The previous value stored at , or nil when key did not exist. /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. - /// https://redis.io/commands/set + /// Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); /// @@ -2602,7 +2711,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The bit value to set, true for 1, false for 0. /// The flags to use for this operation. /// The original bit value stored at offset. - /// https://redis.io/commands/setbit + /// Task StringSetBitAsync(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None); /// @@ -2615,25 +2724,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The value to overwrite with. /// The flags to use for this operation. /// The length of the string after it was modified by the command. - /// https://redis.io/commands/setrange + /// Task StringSetRangeAsync(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None); - - /// - /// Alters the last access time of a key. - /// - /// The key to touch. - /// The flags to use for this operation. - /// if the key was touched, otherwise. - /// https://redis.io/commands/touch - Task KeyTouchAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - - /// - /// Alters the last access time of the specified . A key is ignored if it does not exist. - /// - /// The keys to touch. - /// The flags to use for this operation. - /// The number of keys that were touched. - /// https://redis.io/commands/touch - Task KeyTouchAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); } } diff --git a/src/StackExchange.Redis/Interfaces/IRedis.cs b/src/StackExchange.Redis/Interfaces/IRedis.cs index c945e01ec..3507aa433 100644 --- a/src/StackExchange.Redis/Interfaces/IRedis.cs +++ b/src/StackExchange.Redis/Interfaces/IRedis.cs @@ -12,7 +12,7 @@ public partial interface IRedis : IRedisAsync /// /// The command flags to use when pinging. /// The observed latency. - /// https://redis.io/commands/ping + /// TimeSpan Ping(CommandFlags flags = CommandFlags.None); } } diff --git a/src/StackExchange.Redis/Interfaces/IRedisAsync.cs b/src/StackExchange.Redis/Interfaces/IRedisAsync.cs index 9f5b7a701..4c20d5e72 100644 --- a/src/StackExchange.Redis/Interfaces/IRedisAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IRedisAsync.cs @@ -18,7 +18,7 @@ public partial interface IRedisAsync /// /// The command flags to use. /// The observed latency. - /// https://redis.io/commands/ping + /// Task PingAsync(CommandFlags flags = CommandFlags.None); /// diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 377f52358..3d9eb8065 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -73,27 +73,18 @@ public partial interface IServer : IRedis int DatabaseCount { get; } /// - /// The CLIENT KILL command closes a given client connection identified by ip:port. - /// The ip:port should match a line returned by the CLIENT LIST command. + /// The CLIENT KILL command closes a given client connection identified by ip:port. + /// The ip:port should match a line returned by the CLIENT LIST command. /// Due to the single-threaded nature of Redis, it is not possible to kill a client connection while it is executing a command. /// From the client point of view, the connection can never be closed in the middle of the execution of a command. /// However, the client will notice the connection has been closed only when the next command is sent (and results in network error). /// /// The endpoint of the client to kill. /// The command flags to use. - /// https://redis.io/commands/client-kill + /// void ClientKill(EndPoint endpoint, CommandFlags flags = CommandFlags.None); - /// - /// The CLIENT KILL command closes a given client connection identified by ip:port. - /// The ip:port should match a line returned by the CLIENT LIST command. - /// Due to the single-threaded nature of Redis, it is not possible to kill a client connection while it is executing a command. - /// From the client point of view, the connection can never be closed in the middle of the execution of a command. - /// However, the client will notice the connection has been closed only when the next command is sent (and results in network error). - /// - /// The endpoint of the client to kill. - /// The command flags to use. - /// https://redis.io/commands/client-kill + /// Task ClientKillAsync(EndPoint endpoint, CommandFlags flags = CommandFlags.None); /// @@ -105,61 +96,40 @@ public partial interface IServer : IRedis /// Whether to skip the current connection. /// The command flags to use. /// the number of clients killed. - /// https://redis.io/commands/client-kill + /// long ClientKill(long? id = null, ClientType? clientType = null, EndPoint? endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None); - /// - /// The CLIENT KILL command closes multiple connections that match the specified filters. - /// - /// The ID of the client to kill. - /// The type of client. - /// The endpoint to kill. - /// Whether to skip the current connection. - /// The command flags to use. - /// the number of clients killed. - /// https://redis.io/commands/client-kill + /// Task ClientKillAsync(long? id = null, ClientType? clientType = null, EndPoint? endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None); /// - /// The CLIENT LIST command returns information and statistics about the client connections server in a mostly human readable format. + /// The CLIENT LIST command returns information and statistics about the client connections server in a mostly human readable format. /// /// The command flags to use. - /// https://redis.io/commands/client-list + /// ClientInfo[] ClientList(CommandFlags flags = CommandFlags.None); - /// - /// The CLIENT LIST command returns information and statistics about the client connections server in a mostly human readable format. - /// - /// The command flags to use. - /// https://redis.io/commands/client-list + /// Task ClientListAsync(CommandFlags flags = CommandFlags.None); /// /// Obtains the current CLUSTER NODES output from a cluster server. /// /// The command flags to use. - /// https://redis.io/commands/cluster-nodes/ + /// ClusterConfiguration? ClusterNodes(CommandFlags flags = CommandFlags.None); - /// - /// Obtains the current CLUSTER NODES output from a cluster server. - /// - /// The command flags to use. - /// https://redis.io/commands/cluster-nodes/ + /// Task ClusterNodesAsync(CommandFlags flags = CommandFlags.None); /// /// Obtains the current raw CLUSTER NODES output from a cluster server. /// /// The command flags to use. - /// https://redis.io/commands/cluster-nodes/ + /// string? ClusterNodesRaw(CommandFlags flags = CommandFlags.None); - /// - /// Obtains the current raw CLUSTER NODES output from a cluster server. - /// - /// The command flags to use. - /// https://redis.io/commands/cluster-nodes/ + /// Task ClusterNodesRawAsync(CommandFlags flags = CommandFlags.None); /// @@ -168,30 +138,20 @@ public partial interface IServer : IRedis /// The pattern of config values to get. /// The command flags to use. /// All matching configuration parameters. - /// https://redis.io/commands/config-get + /// KeyValuePair[] ConfigGet(RedisValue pattern = default, CommandFlags flags = CommandFlags.None); - /// - /// Get all configuration parameters matching the specified pattern. - /// - /// The pattern of config values to get. - /// The command flags to use. - /// All matching configuration parameters. - /// https://redis.io/commands/config-get + /// Task[]> ConfigGetAsync(RedisValue pattern = default, CommandFlags flags = CommandFlags.None); /// /// Resets the statistics reported by Redis using the INFO command. /// /// The command flags to use. - /// https://redis.io/commands/config-resetstat + /// void ConfigResetStatistics(CommandFlags flags = CommandFlags.None); - /// - /// Resets the statistics reported by Redis using the INFO command. - /// - /// The command flags to use. - /// https://redis.io/commands/config-resetstat + /// Task ConfigResetStatisticsAsync(CommandFlags flags = CommandFlags.None); /// @@ -200,16 +160,10 @@ public partial interface IServer : IRedis /// used by the server, that may be different compared to the original one because of the use of the CONFIG SET command. /// /// The command flags to use. - /// https://redis.io/commands/config-rewrite + /// void ConfigRewrite(CommandFlags flags = CommandFlags.None); - /// - /// The CONFIG REWRITE command rewrites the redis.conf file the server was started with, - /// applying the minimal changes needed to make it reflecting the configuration currently - /// used by the server, that may be different compared to the original one because of the use of the CONFIG SET command. - /// - /// The command flags to use. - /// https://redis.io/commands/config-rewrite + /// Task ConfigRewriteAsync(CommandFlags flags = CommandFlags.None); /// @@ -219,17 +173,10 @@ public partial interface IServer : IRedis /// The setting name. /// The new setting value. /// The command flags to use. - /// https://redis.io/commands/config-set + /// void ConfigSet(RedisValue setting, RedisValue value, CommandFlags flags = CommandFlags.None); - /// - /// The CONFIG SET command is used in order to reconfigure the server at runtime without the need to restart Redis. - /// You can change both trivial parameters or switch from one to another persistence option using this command. - /// - /// The setting name. - /// The new setting value. - /// The command flags to use. - /// https://redis.io/commands/config-set + /// Task ConfigSetAsync(RedisValue setting, RedisValue value, CommandFlags flags = CommandFlags.None); /// @@ -237,15 +184,10 @@ public partial interface IServer : IRedis /// /// The database ID. /// The command flags to use. - /// https://redis.io/commands/dbsize + /// long DatabaseSize(int database = -1, CommandFlags flags = CommandFlags.None); - /// - /// Return the number of keys in the database. - /// - /// The database ID. - /// The command flags to use. - /// https://redis.io/commands/dbsize + /// Task DatabaseSizeAsync(int database = -1, CommandFlags flags = CommandFlags.None); /// @@ -253,15 +195,10 @@ public partial interface IServer : IRedis /// /// The message to echo. /// The command flags to use. - /// https://redis.io/commands/echo + /// RedisValue Echo(RedisValue message, CommandFlags flags = CommandFlags.None); - /// - /// Return the same message passed in. - /// - /// The message to echo. - /// The command flags to use. - /// https://redis.io/commands/echo + /// Task EchoAsync(RedisValue message, CommandFlags flags = CommandFlags.None); /// @@ -271,31 +208,11 @@ public partial interface IServer : IRedis /// /// The command to run. /// The arguments to pass for the command. - /// This API should be considered an advanced feature; inappropriate use can be harmful - /// A dynamic representation of the command's result + /// A dynamic representation of the command's result. + /// This API should be considered an advanced feature; inappropriate use can be harmful. RedisResult Execute(string command, params object[] args); - /// - /// Execute an arbitrary command against the server; this is primarily intended for - /// executing modules, but may also be used to provide access to new features that lack - /// a direct API. - /// - /// The command to run. - /// The arguments to pass for the command. - /// The flags to use for this operation. - /// This API should be considered an advanced feature; inappropriate use can be harmful - /// A dynamic representation of the command's result - RedisResult Execute(string command, ICollection args, CommandFlags flags = CommandFlags.None); - - /// - /// Execute an arbitrary command against the server; this is primarily intended for - /// executing modules, but may also be used to provide access to new features that lack - /// a direct API. - /// - /// The command to run. - /// The arguments to pass for the command. - /// This API should be considered an advanced feature; inappropriate use can be harmful - /// A dynamic representation of the command's result + /// Task ExecuteAsync(string command, params object[] args); /// @@ -306,22 +223,21 @@ public partial interface IServer : IRedis /// The command to run. /// The arguments to pass for the command. /// The flags to use for this operation. - /// This API should be considered an advanced feature; inappropriate use can be harmful - /// A dynamic representation of the command's result + /// A dynamic representation of the command's result. + /// This API should be considered an advanced feature; inappropriate use can be harmful. + RedisResult Execute(string command, ICollection args, CommandFlags flags = CommandFlags.None); + + /// Task ExecuteAsync(string command, ICollection args, CommandFlags flags = CommandFlags.None); /// /// Delete all the keys of all databases on the server. /// /// The command flags to use. - /// https://redis.io/commands/flushall + /// void FlushAllDatabases(CommandFlags flags = CommandFlags.None); - /// - /// Delete all the keys of all databases on the server. - /// - /// The command flags to use. - /// https://redis.io/commands/flushall + /// Task FlushAllDatabasesAsync(CommandFlags flags = CommandFlags.None); /// @@ -329,15 +245,10 @@ public partial interface IServer : IRedis /// /// The database ID. /// The command flags to use. - /// https://redis.io/commands/flushdb + /// void FlushDatabase(int database = -1, CommandFlags flags = CommandFlags.None); - /// - /// Delete all the keys of the database. - /// - /// The database ID. - /// The command flags to use. - /// https://redis.io/commands/flushdb + /// Task FlushDatabaseAsync(int database = -1, CommandFlags flags = CommandFlags.None); /// @@ -350,15 +261,11 @@ public partial interface IServer : IRedis /// /// The info section to get, if getting a specific one. /// The command flags to use. - /// https://redis.io/commands/info + /// A grouping of key/value pairs, grouped by their section header. + /// IGrouping>[] Info(RedisValue section = default, CommandFlags flags = CommandFlags.None); - /// - /// The INFO command returns information and statistics about the server in a format that is simple to parse by computers and easy to read by humans. - /// - /// The info section to get, if getting a specific one. - /// The command flags to use. - /// https://redis.io/commands/info + /// Task>[]> InfoAsync(RedisValue section = default, CommandFlags flags = CommandFlags.None); /// @@ -366,33 +273,20 @@ public partial interface IServer : IRedis /// /// The info section to get, if getting a specific one. /// The command flags to use. - /// https://redis.io/commands/info + /// The entire raw INFO string. + /// string? InfoRaw(RedisValue section = default, CommandFlags flags = CommandFlags.None); - /// - /// The INFO command returns information and statistics about the server in a format that is simple to parse by computers and easy to read by humans. - /// - /// The info section to get, if getting a specific one. - /// The command flags to use. - /// https://redis.io/commands/info + /// Task InfoRawAsync(RedisValue section = default, CommandFlags flags = CommandFlags.None); - /// - /// Returns all keys matching pattern; the KEYS or SCAN commands will be used based on the server capabilities. - /// - /// The database ID. - /// The pattern to use. - /// The page size to iterate by. - /// The command flags to use. - /// Warning: consider KEYS as a command that should only be used in production environments with extreme care. - /// https://redis.io/commands/keys - /// https://redis.io/commands/scan + /// IEnumerable Keys(int database, RedisValue pattern, int pageSize, CommandFlags flags); /// - /// Returns all keys matching pattern. - /// The KEYS or SCAN commands will be used based on the server capabilities. - /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to IScanningCursor. + /// Returns all keys matching . + /// The KEYS or SCAN commands will be used based on the server capabilities. + /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to . /// /// The database ID. /// The pattern to use. @@ -400,51 +294,33 @@ public partial interface IServer : IRedis /// The cursor position to resume at. /// The page offset to start at. /// The command flags to use. - /// Warning: consider KEYS as a command that should only be used in production environments with extreme care. - /// https://redis.io/commands/keys - /// https://redis.io/commands/scan + /// An enumeration of matching redis keys. + /// + /// Warning: consider KEYS as a command that should only be used in production environments with extreme care. + /// + /// , + /// + /// + /// IEnumerable Keys(int database = -1, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); - /// - /// Returns all keys matching pattern. - /// The KEYS or SCAN commands will be used based on the server capabilities. - /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to IScanningCursor. - /// - /// The database ID. - /// The pattern to use. - /// The page size to iterate by. - /// The cursor position to resume at. - /// The page offset to start at. - /// The command flags to use. - /// Warning: consider KEYS as a command that should only be used in production environments with extreme care. - /// https://redis.io/commands/keys - /// https://redis.io/commands/scan + /// IAsyncEnumerable KeysAsync(int database = -1, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); /// /// Return the time of the last DB save executed with success. - /// A client may check if a BGSAVE command succeeded reading the LASTSAVE value, then issuing a BGSAVE command - /// and checking at regular intervals every N seconds if LASTSAVE changed. + /// A client may check if a BGSAVE command succeeded reading the LASTSAVE value, then issuing a BGSAVE command + /// and checking at regular intervals every N seconds if LASTSAVE changed. /// /// The command flags to use. - /// https://redis.io/commands/lastsave + /// The last time a save was performed. + /// DateTime LastSave(CommandFlags flags = CommandFlags.None); - /// - /// Return the time of the last DB save executed with success. - /// A client may check if a BGSAVE command succeeded reading the LASTSAVE value, then issuing a BGSAVE command - /// and checking at regular intervals every N seconds if LASTSAVE changed. - /// - /// The command flags to use. - /// https://redis.io/commands/lastsave + /// Task LastSaveAsync(CommandFlags flags = CommandFlags.None); - /// - /// Promote the selected node to be primary. - /// - /// The options to use for this topology change. - /// The log to write output to. - /// https://redis.io/commands/replicaof/ + /// [Obsolete("Please use " + nameof(MakePrimaryAsync) + ", this will be removed in 3.0.")] void MakeMaster(ReplicationChangeOptions options, TextWriter? log = null); @@ -453,19 +329,16 @@ public partial interface IServer : IRedis /// /// The options to use for this topology change. /// The log to write output to. - /// https://redis.io/commands/replicaof/ + /// Task MakePrimaryAsync(ReplicationChangeOptions options, TextWriter? log = null); /// /// Returns the role info for the current server. /// - /// https://redis.io/commands/role + /// Role Role(CommandFlags flags = CommandFlags.None); - /// - /// Returns the role info for the current server. - /// - /// https://redis.io/commands/role + /// Task RoleAsync(CommandFlags flags = CommandFlags.None); /// @@ -473,21 +346,15 @@ public partial interface IServer : IRedis /// /// The method of the save (e.g. background or foreground). /// The command flags to use. - /// https://redis.io/commands/bgrewriteaof - /// https://redis.io/commands/bgsave - /// https://redis.io/commands/save - /// https://redis.io/topics/persistence + /// + /// , + /// , + /// , + /// + /// void Save(SaveType type, CommandFlags flags = CommandFlags.None); - /// - /// Explicitly request the database to persist the current state to disk. - /// - /// The method of the save (e.g. background or foreground). - /// The command flags to use. - /// https://redis.io/commands/bgrewriteaof - /// https://redis.io/commands/bgsave - /// https://redis.io/commands/save - /// https://redis.io/topics/persistence + /// Task SaveAsync(SaveType type, CommandFlags flags = CommandFlags.None); /// @@ -495,23 +362,10 @@ public partial interface IServer : IRedis /// /// The text of the script to check for on the server. /// The command flags to use. - /// https://redis.io/commands/script-exists/ + /// bool ScriptExists(string script, CommandFlags flags = CommandFlags.None); - /// - /// Indicates whether the specified script hash is defined on the server. - /// - /// The SHA1 of the script to check for on the server. - /// The command flags to use. - /// https://redis.io/commands/script-exists/ - bool ScriptExists(byte[] sha1, CommandFlags flags = CommandFlags.None); - - /// - /// Indicates whether the specified script is defined on the server. - /// - /// The text of the script to check for on the server. - /// The command flags to use. - /// https://redis.io/commands/script-exists/ + /// Task ScriptExistsAsync(string script, CommandFlags flags = CommandFlags.None); /// @@ -519,21 +373,20 @@ public partial interface IServer : IRedis /// /// The SHA1 of the script to check for on the server. /// The command flags to use. - /// https://redis.io/commands/script-exists/ + /// + bool ScriptExists(byte[] sha1, CommandFlags flags = CommandFlags.None); + + /// Task ScriptExistsAsync(byte[] sha1, CommandFlags flags = CommandFlags.None); /// /// Removes all cached scripts on this server. /// /// The command flags to use. - /// https://redis.io/commands/script-flush/ + /// void ScriptFlush(CommandFlags flags = CommandFlags.None); - /// - /// Removes all cached scripts on this server. - /// - /// The command flags to use. - /// https://redis.io/commands/script-flush/ + /// Task ScriptFlushAsync(CommandFlags flags = CommandFlags.None); /// @@ -541,23 +394,11 @@ public partial interface IServer : IRedis /// /// The script to load. /// The command flags to use. - /// https://redis.io/commands/script-load/ + /// The SHA1 of the loaded script. + /// byte[] ScriptLoad(string script, CommandFlags flags = CommandFlags.None); - /// - /// Explicitly defines a script on the server. - /// - /// The script to load. - /// The command flags to use. - /// https://redis.io/commands/script-load/ - LoadedLuaScript ScriptLoad(LuaScript script, CommandFlags flags = CommandFlags.None); - - /// - /// Explicitly defines a script on the server. - /// - /// The script to load. - /// The command flags to use. - /// https://redis.io/commands/script-load/ + /// Task ScriptLoadAsync(string script, CommandFlags flags = CommandFlags.None); /// @@ -565,55 +406,35 @@ public partial interface IServer : IRedis /// /// The script to load. /// The command flags to use. - /// https://redis.io/commands/script-load/ + /// The loaded script, ready for rapid reuse based on the SHA1. + /// + LoadedLuaScript ScriptLoad(LuaScript script, CommandFlags flags = CommandFlags.None); + + /// Task ScriptLoadAsync(LuaScript script, CommandFlags flags = CommandFlags.None); /// - /// Asks the redis server to shutdown, killing all connections. Please FULLY read the notes on the SHUTDOWN command. + /// Asks the redis server to shutdown, killing all connections. Please FULLY read the notes on the SHUTDOWN command. /// /// The mode of the shutdown. /// The command flags to use. - /// https://redis.io/commands/shutdown + /// void Shutdown(ShutdownMode shutdownMode = ShutdownMode.Default, CommandFlags flags = CommandFlags.None); - /// - /// The REPLICAOF command can change the replication settings of a replica on the fly. - /// If a Redis server is already acting as replica, specifying a null primary will turn off the replication, - /// turning the Redis server into a PRIMARY. Specifying a non-null primary will make the server a replica of - /// another server listening at the specified hostname and port. - /// - /// Endpoint of the new primary to replicate from. - /// The command flags to use. - /// https://redis.io/commands/replicaof + /// [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicaOfAsync) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] void SlaveOf(EndPoint master, CommandFlags flags = CommandFlags.None); - /// - /// The REPLICAOF command can change the replication settings of a replica on the fly. - /// If a Redis server is already acting as replica, specifying a null primary will turn off the replication, - /// turning the Redis server into a PRIMARY. Specifying a non-null primary will make the server a replica of - /// another server listening at the specified hostname and port. - /// - /// Endpoint of the new primary to replicate from. - /// The command flags to use. - /// https://redis.io/commands/replicaof - [Obsolete("Please use " + nameof(ReplicaOfAsync) + ", this will be removed in 3.0.")] - void ReplicaOf(EndPoint master, CommandFlags flags = CommandFlags.None); - - /// - /// The REPLICAOF command can change the replication settings of a replica on the fly. - /// If a Redis server is already acting as replica, specifying a null primary will turn off the replication, - /// turning the Redis server into a PRIMARY. Specifying a non-null primary will make the server a replica of - /// another server listening at the specified hostname and port. - /// - /// Endpoint of the new primary to replicate from. - /// The command flags to use. - /// https://redis.io/commands/replicaof + /// [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicaOfAsync) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task SlaveOfAsync(EndPoint master, CommandFlags flags = CommandFlags.None); + /// + [Obsolete("Please use " + nameof(ReplicaOfAsync) + ", this will be removed in 3.0.")] + void ReplicaOf(EndPoint master, CommandFlags flags = CommandFlags.None); + /// /// The REPLICAOF command can change the replication settings of a replica on the fly. /// If a Redis server is already acting as replica, specifying a null primary will turn off the replication, @@ -622,7 +443,7 @@ public partial interface IServer : IRedis /// /// Endpoint of the new primary to replicate from. /// The command flags to use. - /// https://redis.io/commands/replicaof + /// Task ReplicaOfAsync(EndPoint master, CommandFlags flags = CommandFlags.None); /// @@ -631,30 +452,21 @@ public partial interface IServer : IRedis /// /// The count of items to get. /// The command flags to use. - /// https://redis.io/commands/slowlog + /// The slow command traces as recorded by the Redis server. + /// CommandTrace[] SlowlogGet(int count = 0, CommandFlags flags = CommandFlags.None); - /// - /// To read the slow log the SLOWLOG GET command is used, that returns every entry in the slow log. - /// It is possible to return only the N most recent entries passing an additional argument to the command (for instance SLOWLOG GET 10). - /// - /// The count of items to get. - /// The command flags to use. - /// https://redis.io/commands/slowlog + /// Task SlowlogGetAsync(int count = 0, CommandFlags flags = CommandFlags.None); /// /// You can reset the slow log using the SLOWLOG RESET command. Once deleted the information is lost forever. /// /// The command flags to use. - /// https://redis.io/commands/slowlog + /// void SlowlogReset(CommandFlags flags = CommandFlags.None); - /// - /// You can reset the slow log using the SLOWLOG RESET command. Once deleted the information is lost forever. - /// - /// The command flags to use. - /// https://redis.io/commands/slowlog + /// Task SlowlogResetAsync(CommandFlags flags = CommandFlags.None); /// @@ -664,17 +476,10 @@ public partial interface IServer : IRedis /// The channel name pattern to get channels for. /// The command flags to use. /// a list of active channels, optionally matching the specified pattern. - /// https://redis.io/commands/pubsub + /// RedisChannel[] SubscriptionChannels(RedisChannel pattern = default, CommandFlags flags = CommandFlags.None); - /// - /// Lists the currently active channels. - /// An active channel is a Pub/Sub channel with one ore more subscribers (not including clients subscribed to patterns). - /// - /// The channel name pattern to get channels for. - /// The command flags to use. - /// a list of active channels, optionally matching the specified pattern. - /// https://redis.io/commands/pubsub + /// Task SubscriptionChannelsAsync(RedisChannel pattern = default, CommandFlags flags = CommandFlags.None); /// @@ -683,16 +488,10 @@ public partial interface IServer : IRedis /// /// The command flags to use. /// the number of patterns all the clients are subscribed to. - /// https://redis.io/commands/pubsub + /// long SubscriptionPatternCount(CommandFlags flags = CommandFlags.None); - /// - /// Returns the number of subscriptions to patterns (that are performed using the PSUBSCRIBE command). - /// Note that this is not just the count of clients subscribed to patterns but the total number of patterns all the clients are subscribed to. - /// - /// The command flags to use. - /// the number of patterns all the clients are subscribed to. - /// https://redis.io/commands/pubsub + /// Task SubscriptionPatternCountAsync(CommandFlags flags = CommandFlags.None); /// @@ -700,145 +499,128 @@ public partial interface IServer : IRedis /// /// The channel to get a subscriber count for. /// The command flags to use. - /// https://redis.io/commands/pubsub + /// The number of subscribers on this server. + /// long SubscriptionSubscriberCount(RedisChannel channel, CommandFlags flags = CommandFlags.None); - /// - /// Returns the number of subscribers (not counting clients subscribed to patterns) for the specified channel. - /// - /// The channel to get a subscriber count for. - /// The command flags to use. - /// https://redis.io/commands/pubsub + /// Task SubscriptionSubscriberCountAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None); /// - /// Swaps two Redis databases, so that immediately all the clients connected to a given database will see the data of the other database, and the other way around + /// Swaps two Redis databases, so that immediately all the clients connected to a given database will see the data of the other database, and the other way around. /// /// The ID of the first database. /// The ID of the second database. /// The command flags to use. - /// https://redis.io/commands/swapdb + /// void SwapDatabases(int first, int second, CommandFlags flags = CommandFlags.None); - /// - /// Swaps two Redis databases, so that immediately all the clients connected to a given database will see the data of the other database, and the other way around - /// - /// The ID of the first database. - /// The ID of the second database. - /// The command flags to use. - /// https://redis.io/commands/swapdb + /// Task SwapDatabasesAsync(int first, int second, CommandFlags flags = CommandFlags.None); /// - /// The TIME command returns the current server time in UTC format. - /// Use the DateTime.ToLocalTime() method to get local time. + /// The TIME command returns the current server time in UTC format. + /// Use the method to get local time. /// /// The command flags to use. /// The server's current time. - /// https://redis.io/commands/time + /// DateTime Time(CommandFlags flags = CommandFlags.None); - /// - /// The TIME command returns the current server time in UTC format. - /// Use the DateTime.ToLocalTime() method to get local time. - /// - /// The command flags to use. - /// The server's current time. - /// https://redis.io/commands/time + /// Task TimeAsync(CommandFlags flags = CommandFlags.None); /// /// Gets a text-based latency diagnostic. /// - /// https://redis.io/topics/latency-monitor - Task LatencyDoctorAsync(CommandFlags flags = CommandFlags.None); - /// - /// Gets a text-based latency diagnostic. - /// - /// https://redis.io/topics/latency-monitor + /// The full text result of latency doctor. + /// + /// , + /// + /// string LatencyDoctor(CommandFlags flags = CommandFlags.None); + /// + Task LatencyDoctorAsync(CommandFlags flags = CommandFlags.None); + /// /// Resets the given events (or all if none are specified), discarding the currently logged latency spike events, and resetting the maximum event time register. /// - /// https://redis.io/topics/latency-monitor - Task LatencyResetAsync(string[]? eventNames = null, CommandFlags flags = CommandFlags.None); - /// - /// Resets the given events (or all if none are specified), discarding the currently logged latency spike events, and resetting the maximum event time register. - /// - /// https://redis.io/topics/latency-monitor + /// The number of events that were reset. + /// + /// , + /// + /// long LatencyReset(string[]? eventNames = null, CommandFlags flags = CommandFlags.None); + /// + Task LatencyResetAsync(string[]? eventNames = null, CommandFlags flags = CommandFlags.None); + /// - /// Fetch raw latency data from the event time series, as timestamp-latency pairs - /// - /// https://redis.io/topics/latency-monitor - Task LatencyHistoryAsync(string eventName, CommandFlags flags = CommandFlags.None); - /// - /// Fetch raw latency data from the event time series, as timestamp-latency pairs + /// Fetch raw latency data from the event time series, as timestamp-latency pairs. /// - /// https://redis.io/topics/latency-monitor + /// An array of latency history entries. + /// + /// , + /// + /// LatencyHistoryEntry[] LatencyHistory(string eventName, CommandFlags flags = CommandFlags.None); + /// + Task LatencyHistoryAsync(string eventName, CommandFlags flags = CommandFlags.None); + /// - /// Fetch raw latency data from the event time series, as timestamp-latency pairs - /// - /// https://redis.io/topics/latency-monitor - Task LatencyLatestAsync(CommandFlags flags = CommandFlags.None); - /// - /// Fetch raw latency data from the event time series, as timestamp-latency pairs + /// Fetch raw latency data from the event time series, as timestamp-latency pairs. /// - /// https://redis.io/topics/latency-monitor + /// An array of the latest latency history entries. + /// + /// , + /// + /// LatencyLatestEntry[] LatencyLatest(CommandFlags flags = CommandFlags.None); - /// - /// Reports about different memory-related issues that the Redis server experiences, and advises about possible remedies. - /// - /// https://redis.io/commands/memory-doctor - Task MemoryDoctorAsync(CommandFlags flags = CommandFlags.None); + /// + Task LatencyLatestAsync(CommandFlags flags = CommandFlags.None); /// /// Reports about different memory-related issues that the Redis server experiences, and advises about possible remedies. /// - /// https://redis.io/commands/memory-doctor + /// The full text result of memory doctor. + /// string MemoryDoctor(CommandFlags flags = CommandFlags.None); - /// - /// Attempts to purge dirty pages so these can be reclaimed by the allocator. - /// - /// https://redis.io/commands/memory-purge - Task MemoryPurgeAsync(CommandFlags flags = CommandFlags.None); + /// + Task MemoryDoctorAsync(CommandFlags flags = CommandFlags.None); /// /// Attempts to purge dirty pages so these can be reclaimed by the allocator. /// - /// https://redis.io/commands/memory-purge + /// void MemoryPurge(CommandFlags flags = CommandFlags.None); - /// - /// Returns an array reply about the memory usage of the server. - /// - /// https://redis.io/commands/memory-stats - Task MemoryStatsAsync(CommandFlags flags = CommandFlags.None); + /// + Task MemoryPurgeAsync(CommandFlags flags = CommandFlags.None); /// /// Returns an array reply about the memory usage of the server. /// - /// https://redis.io/commands/memory-stats + /// An array reply of memory stat metrics and values. + /// RedisResult MemoryStats(CommandFlags flags = CommandFlags.None); - /// - /// Provides an internal statistics report from the memory allocator. - /// - /// https://redis.io/commands/memory-malloc-stats - Task MemoryAllocatorStatsAsync(CommandFlags flags = CommandFlags.None); + /// + Task MemoryStatsAsync(CommandFlags flags = CommandFlags.None); /// /// Provides an internal statistics report from the memory allocator. /// - /// https://redis.io/commands/memory-malloc-stats + /// The full text result of memory allocation stats. + /// string? MemoryAllocatorStats(CommandFlags flags = CommandFlags.None); + /// + Task MemoryAllocatorStatsAsync(CommandFlags flags = CommandFlags.None); + /// /// Returns the IP and port number of the primary with that name. /// If a failover is in progress or terminated successfully for this primary it returns the address and port of the promoted replica. @@ -846,17 +628,10 @@ public partial interface IServer : IRedis /// The sentinel service name. /// The command flags to use. /// The primary IP and port. - /// https://redis.io/topics/sentinel + /// EndPoint? SentinelGetMasterAddressByName(string serviceName, CommandFlags flags = CommandFlags.None); - /// - /// Returns the IP and port number of the primary with that name. - /// If a failover is in progress or terminated successfully for this primary it returns the address and port of the promoted replica. - /// - /// The sentinel service name. - /// The command flags to use. - /// The primary IP and port. - /// https://redis.io/topics/sentinel + /// Task SentinelGetMasterAddressByNameAsync(string serviceName, CommandFlags flags = CommandFlags.None); /// @@ -865,14 +640,10 @@ public partial interface IServer : IRedis /// The sentinel service name. /// The command flags to use. /// A list of the sentinel IPs and ports. + /// EndPoint[] SentinelGetSentinelAddresses(string serviceName, CommandFlags flags = CommandFlags.None); - /// - /// Returns the IP and port numbers of all known Sentinels for the given service name. - /// - /// The sentinel service name. - /// The command flags to use. - /// A list of the sentinel IPs and ports. + /// Task SentinelGetSentinelAddressesAsync(string serviceName, CommandFlags flags = CommandFlags.None); /// @@ -881,14 +652,10 @@ public partial interface IServer : IRedis /// The sentinel service name. /// The command flags to use. /// A list of the replica IPs and ports. + /// EndPoint[] SentinelGetReplicaAddresses(string serviceName, CommandFlags flags = CommandFlags.None); - /// - /// Returns the IP and port numbers of all known Sentinel replicas for the given service name. - /// - /// The sentinel service name. - /// The command flags to use. - /// A list of the replica IPs and ports. + /// Task SentinelGetReplicaAddressesAsync(string serviceName, CommandFlags flags = CommandFlags.None); /// @@ -897,16 +664,10 @@ public partial interface IServer : IRedis /// The sentinel service name. /// The command flags to use. /// The primaries state as KeyValuePairs. - /// https://redis.io/topics/sentinel + /// KeyValuePair[] SentinelMaster(string serviceName, CommandFlags flags = CommandFlags.None); - /// - /// Show the state and info of the specified primary. - /// - /// The sentinel service name. - /// The command flags to use. - /// The primaries state as KeyValuePairs. - /// https://redis.io/topics/sentinel + /// Task[]> SentinelMasterAsync(string serviceName, CommandFlags flags = CommandFlags.None); /// @@ -914,44 +675,18 @@ public partial interface IServer : IRedis /// /// The command flags to use. /// An array of primaries state KeyValuePair arrays. - /// https://redis.io/topics/sentinel + /// KeyValuePair[][] SentinelMasters(CommandFlags flags = CommandFlags.None); - /// - /// Show a list of monitored primaries and their state. - /// - /// The command flags to use. - /// An array of primaries state KeyValuePair arrays. - /// https://redis.io/topics/sentinel + /// Task[][]> SentinelMastersAsync(CommandFlags flags = CommandFlags.None); - /// - /// Show a list of replicas for this primary, and their state. - /// - /// The sentinel service name. - /// The command flags to use. - /// An array of replica state KeyValuePair arrays. - /// https://redis.io/topics/sentinel + /// [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(SentinelReplicas) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] KeyValuePair[][] SentinelSlaves(string serviceName, CommandFlags flags = CommandFlags.None); - /// - /// Show a list of replicas for this primary, and their state. - /// - /// The sentinel service name. - /// The command flags to use. - /// An array of replica state KeyValuePair arrays. - /// https://redis.io/topics/sentinel - KeyValuePair[][] SentinelReplicas(string serviceName, CommandFlags flags = CommandFlags.None); - - /// - /// Show a list of replicas for this primary, and their state. - /// - /// The sentinel service name. - /// The command flags to use. - /// An array of replica state KeyValuePair arrays. - /// https://redis.io/topics/sentinel + /// [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(SentinelReplicasAsync) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task[][]> SentinelSlavesAsync(string serviceName, CommandFlags flags = CommandFlags.None); @@ -962,7 +697,10 @@ public partial interface IServer : IRedis /// The sentinel service name. /// The command flags to use. /// An array of replica state KeyValuePair arrays. - /// https://redis.io/topics/sentinel + /// + KeyValuePair[][] SentinelReplicas(string serviceName, CommandFlags flags = CommandFlags.None); + + /// Task[][]> SentinelReplicasAsync(string serviceName, CommandFlags flags = CommandFlags.None); /// @@ -971,16 +709,10 @@ public partial interface IServer : IRedis /// /// The sentinel service name. /// The command flags to use. - /// https://redis.io/topics/sentinel + /// void SentinelFailover(string serviceName, CommandFlags flags = CommandFlags.None); - /// - /// Force a failover as if the primary was not reachable, and without asking for agreement to other Sentinels - /// (however a new version of the configuration will be published so that the other Sentinels will update their configurations). - /// - /// The sentinel service name. - /// The command flags to use. - /// https://redis.io/topics/sentinel + /// Task SentinelFailoverAsync(string serviceName, CommandFlags flags = CommandFlags.None); /// @@ -988,119 +720,13 @@ public partial interface IServer : IRedis /// /// The sentinel service name. /// The command flags to use. - /// https://redis.io/topics/sentinel + /// KeyValuePair[][] SentinelSentinels(string serviceName, CommandFlags flags = CommandFlags.None); - /// - /// Show a list of sentinels for a primary, and their state. - /// - /// The sentinel service name. - /// The command flags to use. - /// https://redis.io/topics/sentinel + /// Task[][]> SentinelSentinelsAsync(string serviceName, CommandFlags flags = CommandFlags.None); } - /// - /// A latency entry as reported by the built-in LATENCY HISTORY command - /// - public readonly struct LatencyHistoryEntry - { - internal static readonly ResultProcessor ToArray = new Processor(); - - private sealed class Processor : ArrayResultProcessor - { - protected override bool TryParse(in RawResult raw, out LatencyHistoryEntry parsed) - { - if (raw.Type == ResultType.MultiBulk) - { - var items = raw.GetItems(); - if (items.Length >= 2 - && items[0].TryGetInt64(out var timestamp) - && items[1].TryGetInt64(out var duration)) - { - parsed = new LatencyHistoryEntry(timestamp, duration); - return true; - } - } - parsed = default; - return false; - } - } - - /// - /// The time at which this entry was recorded - /// - public DateTime Timestamp { get; } - - /// - /// The latency recorded for this event - /// - public int DurationMilliseconds { get; } - - internal LatencyHistoryEntry(long timestamp, long duration) - { - Timestamp = RedisBase.UnixEpoch.AddSeconds(timestamp); - DurationMilliseconds = checked((int)duration); - } - } - - /// - /// A latency entry as reported by the built-in LATENCY LATEST command - /// - public readonly struct LatencyLatestEntry - { - internal static readonly ResultProcessor ToArray = new Processor(); - - private sealed class Processor : ArrayResultProcessor - { - protected override bool TryParse(in RawResult raw, out LatencyLatestEntry parsed) - { - if (raw.Type == ResultType.MultiBulk) - { - var items = raw.GetItems(); - if (items.Length >= 4 - && items[1].TryGetInt64(out var timestamp) - && items[2].TryGetInt64(out var duration) - && items[3].TryGetInt64(out var maxDuration)) - { - parsed = new LatencyLatestEntry(items[0].GetString()!, timestamp, duration, maxDuration); - return true; - } - } - parsed = default; - return false; - } - } - - /// - /// The name of this event - /// - public string EventName { get; } - - /// - /// The time at which this entry was recorded - /// - public DateTime Timestamp { get; } - - /// - /// The latency recorded for this event - /// - public int DurationMilliseconds { get; } - - /// - /// The max latency recorded for all events - /// - public int MaxDurationMilliseconds { get; } - - internal LatencyLatestEntry(string eventName, long timestamp, long duration, long maxDuration) - { - EventName = eventName; - Timestamp = RedisBase.UnixEpoch.AddSeconds(timestamp); - DurationMilliseconds = checked((int)duration); - MaxDurationMilliseconds = checked((int)maxDuration); - } - } - internal static class IServerExtensions { /// diff --git a/src/StackExchange.Redis/Interfaces/ISubscriber.cs b/src/StackExchange.Redis/Interfaces/ISubscriber.cs index 87a2b20a7..b81ed3af4 100644 --- a/src/StackExchange.Redis/Interfaces/ISubscriber.cs +++ b/src/StackExchange.Redis/Interfaces/ISubscriber.cs @@ -16,11 +16,7 @@ public interface ISubscriber : IRedis /// The command flags to use. EndPoint? IdentifyEndpoint(RedisChannel channel, CommandFlags flags = CommandFlags.None); - /// - /// Indicate exactly which redis server we are talking to. - /// - /// The channel to identify the server endpoint by. - /// The command flags to use. + /// Task IdentifyEndpointAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None); /// @@ -30,6 +26,7 @@ public interface ISubscriber : IRedis /// server is chosen arbitrarily from the primaries. /// /// The channel to identify the server endpoint by. + /// if connected, otherwise. bool IsConnected(RedisChannel channel = default); /// @@ -42,20 +39,10 @@ public interface ISubscriber : IRedis /// The number of clients that received the message *on the destination server*, /// note that this doesn't mean much in a cluster as clients can get the message through other nodes. /// - /// https://redis.io/commands/publish + /// long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None); - /// - /// Posts a message to the given channel. - /// - /// The channel to publish to. - /// The message to publish. - /// The command flags to use. - /// - /// The number of clients that received the message *on the destination server*, - /// note that this doesn't mean much in a cluster as clients can get the message through other nodes. - /// - /// https://redis.io/commands/publish + /// Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None); /// @@ -64,38 +51,28 @@ public interface ISubscriber : IRedis /// The channel to subscribe to. /// The handler to invoke when a message is received on . /// The command flags to use. - /// https://redis.io/commands/subscribe - /// https://redis.io/commands/psubscribe + /// + /// , + /// + /// void Subscribe(RedisChannel channel, Action handler, CommandFlags flags = CommandFlags.None); + /// + Task SubscribeAsync(RedisChannel channel, Action handler, CommandFlags flags = CommandFlags.None); + /// /// Subscribe to perform some operation when a message to the preferred/active node is broadcast, as a queue that guarantees ordered handling. /// /// The redis channel to subscribe to. /// The command flags to use. /// A channel that represents this source - /// https://redis.io/commands/subscribe - /// https://redis.io/commands/psubscribe + /// + /// , + /// + /// ChannelMessageQueue Subscribe(RedisChannel channel, CommandFlags flags = CommandFlags.None); - /// - /// Subscribe to perform some operation when a change to the preferred/active node is broadcast. - /// - /// The channel to subscribe to. - /// The handler to invoke when a message is received on . - /// The command flags to use. - /// https://redis.io/commands/subscribe - /// https://redis.io/commands/psubscribe - Task SubscribeAsync(RedisChannel channel, Action handler, CommandFlags flags = CommandFlags.None); - - /// - /// Subscribe to perform some operation when a change to the preferred/active node is broadcast, as a channel. - /// - /// The redis channel to subscribe to. - /// The command flags to use. - /// A channel that represents this source - /// https://redis.io/commands/subscribe - /// https://redis.io/commands/psubscribe + /// Task SubscribeAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None); /// @@ -113,36 +90,26 @@ public interface ISubscriber : IRedis /// The channel that was subscribed to. /// The handler to no longer invoke when a message is received on . /// The command flags to use. - /// https://redis.io/commands/unsubscribe - /// https://redis.io/commands/punsubscribe + /// + /// , + /// + /// void Unsubscribe(RedisChannel channel, Action? handler = null, CommandFlags flags = CommandFlags.None); + /// + Task UnsubscribeAsync(RedisChannel channel, Action? handler = null, CommandFlags flags = CommandFlags.None); + /// /// Unsubscribe all subscriptions on this instance. /// /// The command flags to use. - /// https://redis.io/commands/unsubscribe - /// https://redis.io/commands/punsubscribe + /// + /// , + /// + /// void UnsubscribeAll(CommandFlags flags = CommandFlags.None); - /// - /// Unsubscribe all subscriptions on this instance. - /// - /// The command flags to use. - /// https://redis.io/commands/unsubscribe - /// https://redis.io/commands/punsubscribe + /// Task UnsubscribeAllAsync(CommandFlags flags = CommandFlags.None); - - /// - /// Unsubscribe from a specified message channel. - /// Note: if no handler is specified, the subscription is canceled regardless of the subscribers. - /// If a handler is specified, the subscription is only canceled if this handler is the last handler remaining against the channel. - /// - /// The channel that was subscribed to. - /// The handler to no longer invoke when a message is received on . - /// The command flags to use. - /// https://redis.io/commands/unsubscribe - /// https://redis.io/commands/punsubscribe - Task UnsubscribeAsync(RedisChannel channel, Action? handler = null, CommandFlags flags = CommandFlags.None); } } diff --git a/src/StackExchange.Redis/Interfaces/ITransaction.cs b/src/StackExchange.Redis/Interfaces/ITransaction.cs index 0b8a25c6e..21c66968a 100644 --- a/src/StackExchange.Redis/Interfaces/ITransaction.cs +++ b/src/StackExchange.Redis/Interfaces/ITransaction.cs @@ -5,14 +5,14 @@ namespace StackExchange.Redis /// /// Represents a group of operations that will be sent to the server as a single unit, /// and processed on the server as a single unit. Transactions can also include constraints - /// (implemented via WATCH), but note that constraint checking involves will (very briefly) - /// block the connection, since the transaction cannot be correctly committed (EXEC), - /// aborted (DISCARD) or not applied in the first place (UNWATCH) until the responses from + /// (implemented via WATCH), but note that constraint checking involves will (very briefly) + /// block the connection, since the transaction cannot be correctly committed (EXEC), + /// aborted (DISCARD) or not applied in the first place (UNWATCH) until the responses from /// the constraint checks have arrived. /// - /// https://redis.io/topics/transactions /// - /// Note that on a cluster, it may be required that all keys involved in the transaction (including constraints) are in the same hash-slot. + /// Note that on a cluster, it may be required that all keys involved in the transaction (including constraints) are in the same hash-slot. + /// /// public interface ITransaction : IBatch { diff --git a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs index 5b0a6b126..c965e1231 100644 --- a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs @@ -9,7 +9,7 @@ namespace StackExchange.Redis.Maintenance { /// - /// Azure node maintenance event. For more information, please see: https://aka.ms/redis/maintenanceevents + /// Azure node maintenance event. For more information, please see: . /// public sealed class AzureMaintenanceEvent : ServerMaintenanceEvent { diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 500fcc963..7cdf173a3 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -3683,10 +3683,11 @@ private Message GetStreamAddMessage(RedisKey key, RedisValue messageId, int? max return Message.Create(Database, flags, RedisCommand.XADD, key, values); } + /// + /// Gets message for . + /// private Message GetStreamAddMessage(RedisKey key, RedisValue entryId, int? maxLength, bool useApproximateMaxLength, NameValueEntry[] streamPairs, CommandFlags flags) { - // See https://redis.io/commands/xadd. - if (streamPairs == null) throw new ArgumentNullException(nameof(streamPairs)); if (streamPairs.Length == 0) throw new ArgumentOutOfRangeException(nameof(streamPairs), "streamPairs must contain at least one item."); @@ -3779,6 +3780,10 @@ private Message GetStreamCreateConsumerGroupMessage(RedisKey key, RedisValue gro values); } + /// + /// Gets a message for + /// + /// private Message GetStreamPendingMessagesMessage(RedisKey key, RedisValue groupName, RedisValue? minId, RedisValue? maxId, int count, RedisValue consumerName, CommandFlags flags) { // > XPENDING mystream mygroup - + 10 [consumer name] @@ -3791,8 +3796,6 @@ private Message GetStreamPendingMessagesMessage(RedisKey key, RedisValue groupNa // 3) (integer)74170458 // 4) (integer)1 - // See https://redis.io/topics/streams-intro. - if (count <= 0) { throw new ArgumentOutOfRangeException(nameof(count), "count must be greater than 0."); diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 180e1ee88..a2dcee19e 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -53,157 +53,157 @@ public RedisFeatures(Version version) } /// - /// Does BITOP / BITCOUNT exist? + /// Are BITOP and BITCOUNT available? /// public bool BitwiseOperations => Version >= v2_6_0; /// - /// Is CLIENT SETNAME available? + /// Is CLIENT SETNAME available? /// public bool ClientName => Version >= v2_6_9; /// - /// Does EXEC support EXECABORT if there are errors? + /// Does EXEC support EXECABORT if there are errors? /// public bool ExecAbort => Version >= v2_6_5 && Version != v2_9_5; /// - /// Can EXPIRE be used to set expiration on a key that is already volatile (i.e. has an expiration)? + /// Can EXPIRE be used to set expiration on a key that is already volatile (i.e. has an expiration)? /// public bool ExpireOverwrite => Version >= v2_1_3; /// - /// Is GETDEL available? + /// Is GETDEL available? /// public bool GetDelete => Version >= v6_2_0; /// - /// Is HSTRLEN available? + /// Is HSTRLEN available? /// public bool HashStringLength => Version >= v3_2_0; /// - /// Does HDEL support variadic usage? + /// Does HDEL support variadic usage? /// public bool HashVaradicDelete => Version >= v2_4_0; /// - /// Does INCRBYFLOAT / HINCRBYFLOAT exist? + /// Are INCRBYFLOAT and HINCRBYFLOAT available? /// public bool IncrementFloat => Version >= v2_6_0; /// - /// Does INFO support sections? + /// Does INFO support sections? /// public bool InfoSections => Version >= v2_8_0; /// - /// Is LINSERT available? + /// Is LINSERT available? /// public bool ListInsert => Version >= v2_1_1; /// - /// Is MEMORY available? + /// Is MEMORY available? /// public bool Memory => Version >= v4_0_0; /// - /// Indicates whether PEXPIRE and PTTL are supported + /// Are PEXPIRE and PTTL available? /// public bool MillisecondExpiry => Version >= v2_6_0; /// - /// Is MODULE available? + /// Is MODULE available? /// public bool Module => Version >= v4_0_0; /// - /// Does SRANDMEMBER support "count"? + /// Does SRANDMEMBER support the "count" option? /// public bool MultipleRandom => Version >= v2_5_14; /// - /// Is the PERSIST operation supported? + /// Is PERSIST available? /// public bool Persist => Version >= v2_1_2; /// - /// Is RPUSHX and LPUSHX available? + /// Are LPUSHX and RPUSHX available? /// public bool PushIfNotExists => Version >= v2_1_1; /// - /// Are cursor-based scans available? + /// Is SCAN (cursor-based scanning) available? /// public bool Scan => Version >= v2_8_0; /// - /// Does EVAL / EVALSHA / etc exist? + /// Are EVAL, EVALSHA, and other script commands available? /// public bool Scripting => Version >= v2_6_0; /// - /// Does SET support the GET option? + /// Does SET support the GET option? /// public bool SetAndGet => Version >= v6_2_0; /// - /// Does SET have the EX|PX|NX|XX extensions? + /// Does SET support the EX, PX, NX, and XX options? /// public bool SetConditional => Version >= v2_6_12; /// - /// Does SET have the KEEPTTL extension? + /// Does SET have the KEEPTTL option? /// public bool SetKeepTtl => Version >= v6_0_0; /// - /// Does SET allow the NX and GET options to be used together? + /// Does SET allow the NX and GET options to be used together? /// public bool SetNotExistsAndGet => Version >= v7_0_0_rc1; /// - /// Does SADD support variadic usage? + /// Does SADD support variadic usage? /// public bool SetVaradicAddRemove => Version >= v2_4_0; /// - /// Is ZPOPMAX and ZPOPMIN available? + /// Are ZPOPMIN and ZPOPMAX available? /// public bool SortedSetPop => Version >= v5_0_0; /// - /// Is ZRANGESTORE available? + /// Is ZRANGESTORE available? /// public bool SortedSetRangeStore => Version >= v6_2_0; /// - /// Are Redis Streams available? + /// Are Redis Streams available? /// public bool Streams => Version >= v4_9_1; /// - /// Is STRLEN available? + /// Is STRLEN available? /// public bool StringLength => Version >= v2_1_2; /// - /// Is SETRANGE available? + /// Is SETRANGE available? /// public bool StringSetRange => Version >= v2_1_8; /// - /// Is SWAPDB available? + /// Is SWAPDB available? /// public bool SwapDB => Version >= v4_0_0; /// - /// Does TIME exist? + /// Is TIME available? /// public bool Time => Version >= v2_6_0; /// - /// Does UNLINK exist? + /// Is UNLINK available? /// public bool Unlink => Version >= v4_0_0; @@ -212,40 +212,38 @@ public RedisFeatures(Version version) /// public bool ScriptingDatabaseSafe => Version >= v2_8_12; - /// - /// Is PFCOUNT supported on replicas? - /// + /// [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(HyperLogLogCountReplicaSafe) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public bool HyperLogLogCountSlaveSafe => HyperLogLogCountReplicaSafe; /// - /// Is PFCOUNT supported on replicas? + /// Is PFCOUNT available on replicas? /// public bool HyperLogLogCountReplicaSafe => Version >= v2_8_18; /// - /// Are the GEO commands available? + /// Are geospatial commands available? /// public bool Geo => Version >= v3_2_0; /// - /// Can PING be used on a subscription connection? + /// Can PING be used on a subscription connection? /// internal bool PingOnSubscriber => Version >= v3_0_0; /// - /// Does SetPop support popping multiple items? + /// Does SPOP support popping multiple items? /// public bool SetPopMultiple => Version >= v3_2_0; /// - /// Are the Touch command available? + /// Is TOUCH available? /// public bool KeyTouch => Version >= v3_2_1; /// - /// Does the server prefer 'replica' terminology - 'REPLICAOF', etc? + /// Does the server prefer 'replica' terminology - 'REPLICAOF', etc? /// public bool ReplicaCommands => Version >= v5_0_0; diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index fedaa1982..f75c7045a 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -1663,6 +1663,9 @@ public SingleStreamProcessor(bool skipStreamName = false) this.skipStreamName = skipStreamName; } + /// + /// Handles . + /// protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { if (result.IsNull) @@ -1681,9 +1684,6 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (skipStreamName) { - // Skip the first element in the array (i.e., the stream name). - // See https://redis.io/commands/xread. - // > XREAD COUNT 2 STREAMS mystream 0 // 1) 1) "mystream" <== Skip the stream name // 2) 1) 1) 1519073278252 - 0 <== Index 1 contains the array of stream entries @@ -1712,6 +1712,9 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + /// + /// Handles . + /// internal sealed class MultiStreamProcessor : StreamProcessorBase { /* @@ -1719,8 +1722,6 @@ The result is similar to the XRANGE result (see SingleStreamProcessor) with the addition of the stream name as the first element of top level Multibulk array. - See https://redis.io/commands/xread. - > XREAD COUNT 2 STREAMS mystream writers 0-0 0-0 1) 1) "mystream" 2) 1) 1) 1526984818136-0 @@ -2080,10 +2081,11 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + /// + /// Handles stream responses. For formats, see . + /// internal abstract class StreamProcessorBase : ResultProcessor { - // For command response formats see https://redis.io/topics/streams-intro. - protected static StreamEntry ParseRedisStreamEntry(in RawResult item) { if (item.IsNull || item.Type != ResultType.MultiBulk) diff --git a/src/StackExchange.Redis/Role.cs b/src/StackExchange.Redis/Role.cs index f87f763c0..7f26220fb 100644 --- a/src/StackExchange.Redis/Role.cs +++ b/src/StackExchange.Redis/Role.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis /// /// Result of the ROLE command. Values depend on the role: master, replica, or sentinel. /// - /// https://redis.io/commands/role + /// public abstract class Role { internal static Unknown Null { get; } = new Unknown(""); @@ -23,7 +23,7 @@ public abstract class Role /// /// Result of the ROLE command for a primary node. /// - /// https://redis.io/commands/role#master-output + /// public sealed class Master : Role { /// @@ -74,7 +74,7 @@ internal Master(long offset, ICollection replicas) : base(RedisLiterals /// /// Result of the ROLE command for a replica node. /// - /// https://redis.io/commands/role#output-of-the-command-on-replicas + /// public sealed class Replica : Role { /// @@ -109,7 +109,7 @@ internal Replica(string role, string ip, int port, string state, long offset) : /// /// Result of the ROLE command for a sentinel node. /// - /// https://redis.io/commands/role#sentinel-output + /// public sealed class Sentinel : Role { /// diff --git a/src/StackExchange.Redis/TaskExtensions.cs b/src/StackExchange.Redis/TaskExtensions.cs index 12682e21e..0a70cfc09 100644 --- a/src/StackExchange.Redis/TaskExtensions.cs +++ b/src/StackExchange.Redis/TaskExtensions.cs @@ -36,9 +36,11 @@ internal static Task ObserveErrors(this Task task) internal static void RedisFireAndForget(this Task task) => task?.ContinueWith(t => GC.KeepAlive(t.Exception), TaskContinuationOptions.OnlyOnFaulted); - // Inspired from https://github.com/dotnet/corefx/blob/81a246f3adf1eece3d981f1d8bb8ae9de12de9c6/src/Common/tests/System/Threading/Tasks/TaskTimeoutExtensions.cs#L15-L43 - // Licensed to the .NET Foundation under one or more agreements. - // The .NET Foundation licenses this file to you under the MIT license. + /// + /// Licensed to the .NET Foundation under one or more agreements. + /// The .NET Foundation licenses this file to you under the MIT license. + /// + /// Inspired from internal static async Task TimeoutAfter(this Task task, int timeoutMs) { var cts = new CancellationTokenSource(); diff --git a/src/StackExchange.Redis/ValueStopwatch.cs b/src/StackExchange.Redis/ValueStopwatch.cs index f29738aaf..e7f93b102 100644 --- a/src/StackExchange.Redis/ValueStopwatch.cs +++ b/src/StackExchange.Redis/ValueStopwatch.cs @@ -4,8 +4,9 @@ namespace StackExchange.Redis; /// -/// Optimization over , from https://github.com/dotnet/aspnetcore/blob/main/src/Shared/ValueStopwatch/ValueStopwatch.cs +/// Optimization over . /// +/// From . internal struct ValueStopwatch { private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; diff --git a/tests/StackExchange.Redis.Tests/Hashes.cs b/tests/StackExchange.Redis.Tests/Hashes.cs index 671364eae..dac10d936 100644 --- a/tests/StackExchange.Redis.Tests/Hashes.cs +++ b/tests/StackExchange.Redis.Tests/Hashes.cs @@ -8,8 +8,11 @@ namespace StackExchange.Redis.Tests; +/// +/// Tests for . +/// [Collection(SharedConnectionFixture.Key)] -public class Hashes : TestBase // https://redis.io/commands#hash +public class Hashes : TestBase { public Hashes(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } @@ -219,8 +222,11 @@ public async Task TestGet() } } + /// + /// Tests for . + /// [Fact] - public async Task TestSet() // https://redis.io/commands/hset + public async Task TestSet() { using var conn = Create(); @@ -258,8 +264,11 @@ public async Task TestSet() // https://redis.io/commands/hset Assert.Equal("", await val5); } + /// + /// Tests for . + /// [Fact] - public async Task TestSetNotExists() // https://redis.io/commands/hsetnx + public async Task TestSetNotExists() { using var conn = Create(); @@ -289,8 +298,11 @@ public async Task TestSetNotExists() // https://redis.io/commands/hsetnx Assert.False(await set3); } + /// + /// Tests for . + /// [Fact] - public async Task TestDelSingle() // https://redis.io/commands/hdel + public async Task TestDelSingle() { using var conn = Create(); @@ -309,8 +321,11 @@ public async Task TestDelSingle() // https://redis.io/commands/hdel Assert.False(await del2); } + /// + /// Tests for . + /// [Fact] - public async Task TestDelMulti() // https://redis.io/commands/hdel + public async Task TestDelMulti() { using var conn = Create(); @@ -346,8 +361,11 @@ public async Task TestDelMulti() // https://redis.io/commands/hdel Assert.Equal(1, await removeFinal); } + /// + /// Tests for . + /// [Fact] - public async Task TestDelMultiInsideTransaction() // https://redis.io/commands/hdel + public async Task TestDelMultiInsideTransaction() { using var conn = Create(); @@ -382,8 +400,11 @@ public async Task TestDelMultiInsideTransaction() // https://redis.io/commands/h } } + /// + /// Tests for . + /// [Fact] - public async Task TestExists() // https://redis.io/commands/hexists + public async Task TestExists() { using var conn = Create(); @@ -401,8 +422,11 @@ public async Task TestExists() // https://redis.io/commands/hexists Assert.False(await ex0); } + /// + /// Tests for . + /// [Fact] - public async Task TestHashKeys() // https://redis.io/commands/hkeys + public async Task TestHashKeys() { using var conn = Create(); @@ -424,8 +448,11 @@ public async Task TestHashKeys() // https://redis.io/commands/hkeys Assert.Equal("bar", arr[1]); } + /// + /// Tests for . + /// [Fact] - public async Task TestHashValues() // https://redis.io/commands/hvals + public async Task TestHashValues() { using var conn = Create(); @@ -448,8 +475,11 @@ public async Task TestHashValues() // https://redis.io/commands/hvals Assert.Equal("def", Encoding.UTF8.GetString(arr[1]!)); } + /// + /// Tests for . + /// [Fact] - public async Task TestHashLength() // https://redis.io/commands/hlen + public async Task TestHashLength() { using var conn = Create(); @@ -468,8 +498,11 @@ public async Task TestHashLength() // https://redis.io/commands/hlen Assert.Equal(2, await len1); } + /// + /// Tests for . + /// [Fact] - public async Task TestGetMulti() // https://redis.io/commands/hmget + public async Task TestGetMulti() { using var conn = Create(); @@ -502,8 +535,11 @@ public async Task TestGetMulti() // https://redis.io/commands/hmget Assert.Null((string?)arr2[2]); } + /// + /// Tests for . + /// [Fact] - public void TestGetPairs() // https://redis.io/commands/hgetall + public void TestGetPairs() { using var conn = Create(); @@ -525,8 +561,11 @@ public void TestGetPairs() // https://redis.io/commands/hgetall Assert.Equal("def", result["bar"]); } + /// + /// Tests for . + /// [Fact] - public void TestSetPairs() // https://redis.io/commands/hmset + public void TestSetPairs() { using var conn = Create(); diff --git a/tests/StackExchange.Redis.Tests/PubSub.cs b/tests/StackExchange.Redis.Tests/PubSub.cs index c7486f074..0cb9efcad 100644 --- a/tests/StackExchange.Redis.Tests/PubSub.cs +++ b/tests/StackExchange.Redis.Tests/PubSub.cs @@ -628,7 +628,6 @@ public async Task TestMultipleSubscribersGetMessage() [Fact] public async Task Issue38() { - // https://code.google.com/p/booksleeve/issues/detail?id=38 using var conn = Create(log: Writer); var sub = conn.GetSubscriber(); diff --git a/tests/StackExchange.Redis.Tests/SanityChecks.cs b/tests/StackExchange.Redis.Tests/SanityChecks.cs index 2ed194c47..e1a2de977 100644 --- a/tests/StackExchange.Redis.Tests/SanityChecks.cs +++ b/tests/StackExchange.Redis.Tests/SanityChecks.cs @@ -12,7 +12,7 @@ public sealed class SanityChecks /// Ensure we don't reference System.ValueTuple as it causes issues with .NET Full Framework /// /// - /// Modified from https://github.com/ltrzesniewski/InlineIL.Fody/blob/137e8b57f78b08cdc3abdaaf50ac01af50c58759/src/InlineIL.Tests/AssemblyTests.cs#L14 + /// Modified from . /// Thanks Lucas Trzesniewski! /// [Fact] diff --git a/tests/StackExchange.Redis.Tests/Scans.cs b/tests/StackExchange.Redis.Tests/Scans.cs index 7f705c3b8..b8b98f22c 100644 --- a/tests/StackExchange.Redis.Tests/Scans.cs +++ b/tests/StackExchange.Redis.Tests/Scans.cs @@ -332,7 +332,10 @@ public void HashScanLarge(int pageSize) Assert.Equal(2000, count); } - [Fact] // See https://github.com/StackExchange/StackExchange.Redis/issues/729 + /// + /// See . + /// + [Fact] public void HashScanThresholds() { using var conn = Create(allowAdmin: true); diff --git a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs index 8e561aa07..eb199e625 100644 --- a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs @@ -230,7 +230,9 @@ public void Teardown(TextWriter output) } } -// https://stackoverflow.com/questions/13829737/xunit-net-run-code-once-before-and-after-all-tests +/// +/// See . +/// [CollectionDefinition(SharedConnectionFixture.Key)] public class ConnectionCollection : ICollectionFixture { diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index f1ae1f049..2a095ab59 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -8,8 +8,11 @@ namespace StackExchange.Redis.Tests; +/// +/// Tests for . +/// [Collection(SharedConnectionFixture.Key)] -public class Strings : TestBase // https://redis.io/commands#string +public class Strings : TestBase { public Strings(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } From e4a88ddc9f6c830e69ba34f03425bd00870c40f1 Mon Sep 17 00:00:00 2001 From: Todd Tingen Date: Tue, 19 Apr 2022 12:25:25 -0400 Subject: [PATCH 141/435] Support the XAUTOCLAIM command. (#2095) Added [XAUTOCLAIM](https://redis.io/commands/xautoclaim/) support as part of #2055 The XCLAIM command has two methods, StreamClaim & StreamClaimIdsOnly. Since the XAUTOCLAIM result is a bit more complex than the XCLAIM command, I opted to consolidate to a single method and single result type. To return just the message IDs in the result, the `idsOnly` parameter should be set to `true`. The caveat here is that the `StreamEntry` instances will only have `Id` populated when `idsOnly` is `true`, the `Values` array will be empty. This behavior is called out in the XML docs for the method. I wasn't sure if this is the proper "feel" you want for this command or not. Let me know if you want to break it up like `StreamClaim`. Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + .../APITypes/StreamAutoClaimIdsOnlyResult.cs | 41 ++ .../APITypes/StreamAutoClaimResult.cs | 41 ++ src/StackExchange.Redis/Enums/RedisCommand.cs | 2 + .../Interfaces/IDatabase.cs | 31 ++ .../Interfaces/IDatabaseAsync.cs | 31 ++ .../KeyspaceIsolation/DatabaseWrapper.cs | 6 + .../KeyspaceIsolation/WrapperBase.cs | 6 + src/StackExchange.Redis/PublicAPI.Shipped.txt | 18 + src/StackExchange.Redis/RedisDatabase.cs | 50 +++ src/StackExchange.Redis/ResultProcessor.cs | 64 +++ .../DatabaseWrapperTests.cs | 14 + tests/StackExchange.Redis.Tests/Streams.cs | 383 ++++++++++++++++++ .../WrapperBaseTests.cs | 14 + 14 files changed, 702 insertions(+) create mode 100644 src/StackExchange.Redis/APITypes/StreamAutoClaimIdsOnlyResult.cs create mode 100644 src/StackExchange.Redis/APITypes/StreamAutoClaimResult.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 857cd5166..8cec3ee88 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -23,6 +23,7 @@ - Adds: Support for `GEOSEARCH` with `.GeoSearch()`/`.GeoSearchAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089)) - Adds: Support for `GEOSEARCHSTORE` with `.GeoSearchAndStore()`/`.GeoSearchAndStoreAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089)) - Adds: Support for `HRANDFIELD` with `.HashRandomField()`/`.HashRandomFieldAsync()`, `.HashRandomFields()`/`.HashRandomFieldsAsync()`, and `.HashRandomFieldsWithValues()`/`.HashRandomFieldsWithValuesAsync()` ([#2090 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2090)) +- Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2095)) ## 2.5.61 diff --git a/src/StackExchange.Redis/APITypes/StreamAutoClaimIdsOnlyResult.cs b/src/StackExchange.Redis/APITypes/StreamAutoClaimIdsOnlyResult.cs new file mode 100644 index 000000000..114763129 --- /dev/null +++ b/src/StackExchange.Redis/APITypes/StreamAutoClaimIdsOnlyResult.cs @@ -0,0 +1,41 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// Result of the XAUTOCLAIM command with the JUSTID option. +/// +public readonly struct StreamAutoClaimIdsOnlyResult +{ + internal StreamAutoClaimIdsOnlyResult(RedisValue nextStartId, RedisValue[] claimedIds, RedisValue[] deletedIds) + { + NextStartId = nextStartId; + ClaimedIds = claimedIds; + DeletedIds = deletedIds; + } + + /// + /// A null , indicating no results. + /// + public static StreamAutoClaimIdsOnlyResult Null { get; } = new StreamAutoClaimIdsOnlyResult(RedisValue.Null, Array.Empty(), Array.Empty()); + + /// + /// Whether this object is null/empty. + /// + public bool IsNull => NextStartId.IsNull && ClaimedIds == Array.Empty() && DeletedIds == Array.Empty(); + + /// + /// The stream ID to be used in the next call to StreamAutoClaim. + /// + public RedisValue NextStartId { get; } + + /// + /// Array of IDs claimed by the command. + /// + public RedisValue[] ClaimedIds { get; } + + /// + /// Array of message IDs deleted from the stream. + /// + public RedisValue[] DeletedIds { get; } +} diff --git a/src/StackExchange.Redis/APITypes/StreamAutoClaimResult.cs b/src/StackExchange.Redis/APITypes/StreamAutoClaimResult.cs new file mode 100644 index 000000000..09f607f3d --- /dev/null +++ b/src/StackExchange.Redis/APITypes/StreamAutoClaimResult.cs @@ -0,0 +1,41 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// Result of the XAUTOCLAIM command. +/// +public readonly struct StreamAutoClaimResult +{ + internal StreamAutoClaimResult(RedisValue nextStartId, StreamEntry[] claimedEntries, RedisValue[] deletedIds) + { + NextStartId = nextStartId; + ClaimedEntries = claimedEntries; + DeletedIds = deletedIds; + } + + /// + /// A null , indicating no results. + /// + public static StreamAutoClaimResult Null { get; } = new StreamAutoClaimResult(RedisValue.Null, Array.Empty(), Array.Empty()); + + /// + /// Whether this object is null/empty. + /// + public bool IsNull => NextStartId.IsNull && ClaimedEntries == Array.Empty() && DeletedIds == Array.Empty(); + + /// + /// The stream ID to be used in the next call to StreamAutoClaim. + /// + public RedisValue NextStartId { get; } + + /// + /// An array of for the successfully claimed entries. + /// + public StreamEntry[] ClaimedEntries { get; } + + /// + /// An array of message IDs deleted from the stream. + /// + public RedisValue[] DeletedIds { get; } +} diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index e2cc9603c..6ec77ea01 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -189,6 +189,7 @@ internal enum RedisCommand XACK, XADD, + XAUTOCLAIM, XCLAIM, XDEL, XGROUP, @@ -324,6 +325,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.UNLINK: case RedisCommand.XACK: case RedisCommand.XADD: + case RedisCommand.XAUTOCLAIM: case RedisCommand.XCLAIM: case RedisCommand.XDEL: case RedisCommand.XGROUP: diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index f95c06325..16975beb5 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -2163,6 +2163,37 @@ IEnumerable SortedSetScan(RedisKey key, /// RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); + /// + /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. + /// Messages that have been idle for more than will be claimed. + /// + /// The key of the stream. + /// The consumer group. + /// The consumer claiming the messages(s). + /// The minimum idle time threshold for pending messages to be claimed. + /// The starting ID to scan for pending messages that have an idle time greater than . + /// The upper limit of the number of entries that the command attempts to claim. If , Redis will default the value to 100. + /// The flags to use for this operation. + /// An instance of . + /// + StreamAutoClaimResult StreamAutoClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None); + + /// + /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. + /// Messages that have been idle for more than will be claimed. + /// The result will contain the claimed message IDs instead of a instance. + /// + /// The key of the stream. + /// The consumer group. + /// The consumer claiming the messages(s). + /// The minimum idle time threshold for pending messages to be claimed. + /// The starting ID to scan for pending messages that have an idle time greater than . + /// The upper limit of the number of entries that the command attempts to claim. If , Redis will default the value to 100. + /// The flags to use for this operation. + /// An instance of . + /// + StreamAutoClaimIdsOnlyResult StreamAutoClaimIdsOnly(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None); + /// /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. /// This method returns the complete message for the claimed message(s). diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 3400010cf..846e5ba2f 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -2116,6 +2116,37 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); + /// + /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. + /// Messages that have been idle for more than will be claimed. + /// + /// The key of the stream. + /// The consumer group. + /// The consumer claiming the messages(s). + /// The minimum idle time threshold for pending messages to be claimed. + /// The starting ID to scan for pending messages that have an idle time greater than . + /// The upper limit of the number of entries that the command attempts to claim. If , Redis will default the value to 100. + /// The flags to use for this operation. + /// An instance of . + /// + Task StreamAutoClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None); + + /// + /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. + /// Messages that have been idle for more than will be claimed. + /// The result will contain the claimed message IDs instead of a instance. + /// + /// The key of the stream. + /// The consumer group. + /// The consumer claiming the messages(s). + /// The minimum idle time threshold for pending messages to be claimed. + /// The starting ID to scan for pending messages that have an idle time greater than . + /// The upper limit of the number of entries that the command attempts to claim. If , Redis will default the value to 100. + /// The flags to use for this operation. + /// An instance of . + /// + Task StreamAutoClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None); + /// /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. /// This method returns the complete message for the claimed message(s). diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 7def559b2..d08834c08 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -496,6 +496,12 @@ public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue str public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => Inner.StreamAdd(ToInner(key), streamPairs, messageId, maxLength, useApproximateMaxLength, flags); + public StreamAutoClaimResult StreamAutoClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamAutoClaim(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, startAtId, count, flags); + + public StreamAutoClaimIdsOnlyResult StreamAutoClaimIdsOnly(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamAutoClaimIdsOnly(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, startAtId, count, flags); + public StreamEntry[] StreamClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => Inner.StreamClaim(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, messageIds, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 5a1437e33..0ccaed1fa 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -513,6 +513,12 @@ public Task StreamAddAsync(RedisKey key, RedisValue streamField, Red public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => Inner.StreamAddAsync(ToInner(key), streamPairs, messageId, maxLength, useApproximateMaxLength, flags); + public Task StreamAutoClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamAutoClaimAsync(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, startAtId, count, flags); + + public Task StreamAutoClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamAutoClaimIdsOnlyAsync(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, startAtId, count, flags); + public Task StreamClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => Inner.StreamClaimAsync(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, messageIds, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 7b4dfb2df..8347638de 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -654,6 +654,8 @@ StackExchange.Redis.IDatabase.StreamAcknowledge(StackExchange.Redis.RedisKey key StackExchange.Redis.IDatabase.StreamAcknowledge(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StreamAutoClaim(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue startAtId, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamAutoClaimResult +StackExchange.Redis.IDatabase.StreamAutoClaimIdsOnly(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue startAtId, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamAutoClaimIdsOnlyResult StackExchange.Redis.IDatabase.StreamClaim(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[]! StackExchange.Redis.IDatabase.StreamClaimIdsOnly(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.StreamConsumerGroupSetPosition(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue position, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool @@ -865,6 +867,8 @@ StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAsync(StackExchange.Redis.Re StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamAutoClaimAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue startAtId, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamAutoClaimIdsOnlyAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue startAtId, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamClaimAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamClaimIdsOnlyAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamConsumerGroupSetPositionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue position, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -1389,6 +1393,18 @@ StackExchange.Redis.SortedSetOrder.ByScore = 1 -> StackExchange.Redis.SortedSetO StackExchange.Redis.SortType StackExchange.Redis.SortType.Alphabetic = 1 -> StackExchange.Redis.SortType StackExchange.Redis.SortType.Numeric = 0 -> StackExchange.Redis.SortType +StackExchange.Redis.StreamAutoClaimIdsOnlyResult +StackExchange.Redis.StreamAutoClaimIdsOnlyResult.ClaimedIds.get -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.StreamAutoClaimIdsOnlyResult.DeletedIds.get -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.StreamAutoClaimIdsOnlyResult.IsNull.get -> bool +StackExchange.Redis.StreamAutoClaimIdsOnlyResult.NextStartId.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.StreamAutoClaimIdsOnlyResult.StreamAutoClaimIdsOnlyResult() -> void +StackExchange.Redis.StreamAutoClaimResult +StackExchange.Redis.StreamAutoClaimResult.ClaimedEntries.get -> StackExchange.Redis.StreamEntry[]! +StackExchange.Redis.StreamAutoClaimResult.DeletedIds.get -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.StreamAutoClaimResult.IsNull.get -> bool +StackExchange.Redis.StreamAutoClaimResult.NextStartId.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.StreamAutoClaimResult.StreamAutoClaimResult() -> void StackExchange.Redis.StreamConsumer StackExchange.Redis.StreamConsumer.Name.get -> StackExchange.Redis.RedisValue StackExchange.Redis.StreamConsumer.PendingMessageCount.get -> int @@ -1656,6 +1672,8 @@ static StackExchange.Redis.SortedSetEntry.implicit operator StackExchange.Redis. static StackExchange.Redis.SortedSetEntry.implicit operator System.Collections.Generic.KeyValuePair(StackExchange.Redis.SortedSetEntry value) -> System.Collections.Generic.KeyValuePair static StackExchange.Redis.SortedSetEntry.operator !=(StackExchange.Redis.SortedSetEntry x, StackExchange.Redis.SortedSetEntry y) -> bool static StackExchange.Redis.SortedSetEntry.operator ==(StackExchange.Redis.SortedSetEntry x, StackExchange.Redis.SortedSetEntry y) -> bool +static StackExchange.Redis.StreamAutoClaimIdsOnlyResult.Null.get -> StackExchange.Redis.StreamAutoClaimIdsOnlyResult +static StackExchange.Redis.StreamAutoClaimResult.Null.get -> StackExchange.Redis.StreamAutoClaimResult static StackExchange.Redis.StreamEntry.Null.get -> StackExchange.Redis.StreamEntry static StackExchange.Redis.StreamPosition.Beginning.get -> StackExchange.Redis.RedisValue static StackExchange.Redis.StreamPosition.NewMessages.get -> StackExchange.Redis.RedisValue diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 7cdf173a3..7d05e46da 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -2223,6 +2223,30 @@ public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPair return ExecuteAsync(msg, ResultProcessor.RedisValue); } + public StreamAutoClaimResult StreamAutoClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamAutoClaimMessage(key, consumerGroup, claimingConsumer, minIdleTimeInMs, startAtId, count, idsOnly: false, flags); + return ExecuteSync(msg, ResultProcessor.StreamAutoClaim, defaultValue: StreamAutoClaimResult.Null); + } + + public Task StreamAutoClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamAutoClaimMessage(key, consumerGroup, claimingConsumer, minIdleTimeInMs, startAtId, count, idsOnly: false, flags); + return ExecuteAsync(msg, ResultProcessor.StreamAutoClaim, defaultValue: StreamAutoClaimResult.Null); + } + + public StreamAutoClaimIdsOnlyResult StreamAutoClaimIdsOnly(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamAutoClaimMessage(key, consumerGroup, claimingConsumer, minIdleTimeInMs, startAtId, count, idsOnly: true, flags); + return ExecuteSync(msg, ResultProcessor.StreamAutoClaimIdsOnly, defaultValue: StreamAutoClaimIdsOnlyResult.Null); + } + + public Task StreamAutoClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamAutoClaimMessage(key, consumerGroup, claimingConsumer, minIdleTimeInMs, startAtId, count, idsOnly: true, flags); + return ExecuteAsync(msg, ResultProcessor.StreamAutoClaimIdsOnly, defaultValue: StreamAutoClaimIdsOnlyResult.Null); + } + public StreamEntry[] StreamClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) { var msg = GetStreamClaimMessage(key, @@ -3731,6 +3755,32 @@ private Message GetStreamAddMessage(RedisKey key, RedisValue entryId, int? maxLe return Message.Create(Database, flags, RedisCommand.XADD, key, values); } + private Message GetStreamAutoClaimMessage(RedisKey key, RedisValue consumerGroup, RedisValue assignToConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count, bool idsOnly, CommandFlags flags) + { + // XAUTOCLAIM [COUNT count] [JUSTID] + var values = new RedisValue[4 + (count is null ? 0 : 2) + (idsOnly ? 1 : 0)]; + + var offset = 0; + + values[offset++] = consumerGroup; + values[offset++] = assignToConsumer; + values[offset++] = minIdleTimeInMs; + values[offset++] = startAtId; + + if (count is not null) + { + values[offset++] = StreamConstants.Count; + values[offset++] = count.Value; + } + + if (idsOnly) + { + values[offset++] = StreamConstants.JustId; + } + + return Message.Create(Database, flags, RedisCommand.XAUTOCLAIM, key, values); + } + private Message GetStreamClaimMessage(RedisKey key, RedisValue consumerGroup, RedisValue assignToConsumer, long minIdleTimeInMs, RedisValue[] messageIds, bool returnJustIds, CommandFlags flags) { if (messageIds == null) throw new ArgumentNullException(nameof(messageIds)); diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index f75c7045a..9ae426e7d 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -122,6 +122,12 @@ public static readonly SingleStreamProcessor public static readonly SingleStreamProcessor SingleStreamWithNameSkip = new SingleStreamProcessor(skipStreamName: true); + public static readonly StreamAutoClaimProcessor + StreamAutoClaim = new StreamAutoClaimProcessor(); + + public static readonly StreamAutoClaimIdsOnlyProcessor + StreamAutoClaimIdsOnly = new StreamAutoClaimIdsOnlyProcessor(); + public static readonly StreamConsumerInfoProcessor StreamConsumerInfo = new StreamConsumerInfoProcessor(); @@ -1776,6 +1782,64 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + /// + /// This processor is for *without* the option. + /// + internal sealed class StreamAutoClaimProcessor : StreamProcessorBase + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + // See https://redis.io/commands/xautoclaim for command documentation. + // Note that the result should never be null, so intentionally treating it as a failure to parse here + if (result.Type == ResultType.MultiBulk && !result.IsNull) + { + var items = result.GetItems(); + + // [0] The next start ID. + var nextStartId = items[0].AsRedisValue(); + // [1] The array of StreamEntry's. + var entries = ParseRedisStreamEntries(items[1]); + // [2] The array of message IDs deleted from the stream that were in the PEL. + // This is not available in 6.2 so we need to be defensive when reading this part of the response. + var deletedIds = (items.Length == 3 ? items[2].GetItemsAsValues() : null) ?? Array.Empty(); + + SetResult(message, new StreamAutoClaimResult(nextStartId, entries, deletedIds)); + return true; + } + + return false; + } + } + + /// + /// This processor is for *with* the option. + /// + internal sealed class StreamAutoClaimIdsOnlyProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + // See https://redis.io/commands/xautoclaim for command documentation. + // Note that the result should never be null, so intentionally treating it as a failure to parse here + if (result.Type == ResultType.MultiBulk && !result.IsNull) + { + var items = result.GetItems(); + + // [0] The next start ID. + var nextStartId = items[0].AsRedisValue(); + // [1] The array of claimed message IDs. + var claimedIds = items[1].GetItemsAsValues() ?? Array.Empty(); + // [2] The array of message IDs deleted from the stream that were in the PEL. + // This is not available in 6.2 so we need to be defensive when reading this part of the response. + var deletedIds = (items.Length == 3 ? items[2].GetItemsAsValues() : null) ?? Array.Empty(); + + SetResult(message, new StreamAutoClaimIdsOnlyResult(nextStartId, claimedIds, deletedIds)); + return true; + } + + return false; + } + } + internal sealed class StreamConsumerInfoProcessor : InterleavedStreamInfoProcessorBase { protected override StreamConsumerInfo ParseItem(in RawResult result) diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index e51805da3..f73162634 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -1010,6 +1010,20 @@ public void StreamAdd_2() mock.Verify(_ => _.StreamAdd("prefix:key", fields, "*", 1000, true, CommandFlags.None)); } + [Fact] + public void StreamAutoClaim() + { + wrapper.StreamAutoClaim("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); + mock.Verify(_ => _.StreamAutoClaim("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None)); + } + + [Fact] + public void StreamAutoClaimIdsOnly() + { + wrapper.StreamAutoClaimIdsOnly("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); + mock.Verify(_ => _.StreamAutoClaimIdsOnly("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None)); + } + [Fact] public void StreamClaimMessages() { diff --git a/tests/StackExchange.Redis.Tests/Streams.cs b/tests/StackExchange.Redis.Tests/Streams.cs index c946b1cf7..b24d7cfd7 100644 --- a/tests/StackExchange.Redis.Tests/Streams.cs +++ b/tests/StackExchange.Redis.Tests/Streams.cs @@ -191,6 +191,389 @@ public void StreamAddMultipleValuePairsWithManualId() Assert.Equal(id, entries[0].Id); } + [Fact] + public async Task StreamAutoClaim_MissingKey() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup", + consumer = "consumer"; + + db.KeyDelete(key); + + var ex = Assert.Throws(() => db.StreamAutoClaim(key, group, consumer, 0, "0-0")); + Assert.StartsWith("NOGROUP No such key", ex.Message); + + ex = await Assert.ThrowsAsync(() => db.StreamAutoClaimAsync(key, group, consumer, 0, "0-0")); + Assert.StartsWith("NOGROUP No such key", ex.Message); + } + + [Fact] + public void StreamAutoClaim_ClaimsPendingMessages() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup", + consumer1 = "c1", + consumer2 = "c2"; + + // Create Consumer Group, add messages, and read messages into a consumer. + _ = StreamAutoClaim_PrepareTestData(db, key, group, consumer1); + + // Claim any pending messages and reassign them to consumer2. + var result = db.StreamAutoClaim(key, group, consumer2, 0, "0-0"); + + Assert.Equal("0-0", result.NextStartId); + Assert.NotEmpty(result.ClaimedEntries); + Assert.Empty(result.DeletedIds); + Assert.True(result.ClaimedEntries.Length == 2); + Assert.Equal("value1", result.ClaimedEntries[0].Values[0].Value); + Assert.Equal("value2", result.ClaimedEntries[1].Values[0].Value); + } + + [Fact] + public async Task StreamAutoClaim_ClaimsPendingMessagesAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup", + consumer1 = "c1", + consumer2 = "c2"; + + // Create Consumer Group, add messages, and read messages into a consumer. + _ = StreamAutoClaim_PrepareTestData(db, key, group, consumer1); + + // Claim any pending messages and reassign them to consumer2. + var result = await db.StreamAutoClaimAsync(key, group, consumer2, 0, "0-0"); + + Assert.Equal("0-0", result.NextStartId); + Assert.NotEmpty(result.ClaimedEntries); + Assert.Empty(result.DeletedIds); + Assert.True(result.ClaimedEntries.Length == 2); + Assert.Equal("value1", result.ClaimedEntries[0].Values[0].Value); + Assert.Equal("value2", result.ClaimedEntries[1].Values[0].Value); + } + + [Fact] + public void StreamAutoClaim_ClaimsSingleMessageWithCountOption() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup", + consumer1 = "c1", + consumer2 = "c2"; + + // Create Consumer Group, add messages, and read messages into a consumer. + var messageIds = StreamAutoClaim_PrepareTestData(db, key, group, consumer1); + + // Claim a single pending message and reassign it to consumer2. + var result = db.StreamAutoClaim(key, group, consumer2, 0, "0-0", count: 1); + + // Should be the second message ID from the call to prepare. + Assert.Equal(messageIds[1], result.NextStartId); + Assert.NotEmpty(result.ClaimedEntries); + Assert.Empty(result.DeletedIds); + Assert.True(result.ClaimedEntries.Length == 1); + Assert.Equal("value1", result.ClaimedEntries[0].Values[0].Value); + } + + [Fact] + public void StreamAutoClaim_ClaimsSingleMessageWithCountOptionIdsOnly() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup", + consumer1 = "c1", + consumer2 = "c2"; + + // Create Consumer Group, add messages, and read messages into a consumer. + var messageIds = StreamAutoClaim_PrepareTestData(db, key, group, consumer1); + + // Claim a single pending message and reassign it to consumer2. + var result = db.StreamAutoClaimIdsOnly(key, group, consumer2, 0, "0-0", count: 1); + + // Should be the second message ID from the call to prepare. + Assert.Equal(messageIds[1], result.NextStartId); + Assert.NotEmpty(result.ClaimedIds); + Assert.True(result.ClaimedIds.Length == 1); + Assert.Equal(messageIds[0], result.ClaimedIds[0]); + Assert.Empty(result.DeletedIds); + } + + [Fact] + public async Task StreamAutoClaim_ClaimsSingleMessageWithCountOptionAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup", + consumer1 = "c1", + consumer2 = "c2"; + + // Create Consumer Group, add messages, and read messages into a consumer. + var messageIds = StreamAutoClaim_PrepareTestData(db, key, group, consumer1); + + // Claim a single pending message and reassign it to consumer2. + var result = await db.StreamAutoClaimAsync(key, group, consumer2, 0, "0-0", count: 1); + + // Should be the second message ID from the call to prepare. + Assert.Equal(messageIds[1], result.NextStartId); + Assert.NotEmpty(result.ClaimedEntries); + Assert.Empty(result.DeletedIds); + Assert.True(result.ClaimedEntries.Length == 1); + Assert.Equal("value1", result.ClaimedEntries[0].Values[0].Value); + } + + [Fact] + public async Task StreamAutoClaim_ClaimsSingleMessageWithCountOptionIdsOnlyAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup", + consumer1 = "c1", + consumer2 = "c2"; + + // Create Consumer Group, add messages, and read messages into a consumer. + var messageIds = StreamAutoClaim_PrepareTestData(db, key, group, consumer1); + + // Claim a single pending message and reassign it to consumer2. + var result = await db.StreamAutoClaimIdsOnlyAsync(key, group, consumer2, 0, "0-0", count: 1); + + // Should be the second message ID from the call to prepare. + Assert.Equal(messageIds[1], result.NextStartId); + Assert.NotEmpty(result.ClaimedIds); + Assert.True(result.ClaimedIds.Length == 1); + Assert.Equal(messageIds[0], result.ClaimedIds[0]); + Assert.Empty(result.DeletedIds); + } + + [Fact] + public void StreamAutoClaim_IncludesDeletedMessageId() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup", + consumer1 = "c1", + consumer2 = "c2"; + + // Create Consumer Group, add messages, and read messages into a consumer. + var messageIds = StreamAutoClaim_PrepareTestData(db, key, group, consumer1); + + // Delete one of the messages, it should be included in the deleted message ID array. + db.StreamDelete(key, new RedisValue[] { messageIds[0] }); + + // Claim a single pending message and reassign it to consumer2. + var result = db.StreamAutoClaim(key, group, consumer2, 0, "0-0", count: 1); + + Assert.Equal("0-0", result.NextStartId); + Assert.NotEmpty(result.ClaimedEntries); + Assert.NotEmpty(result.DeletedIds); + Assert.True(result.ClaimedEntries.Length == 1); + Assert.True(result.DeletedIds.Length == 1); + Assert.Equal(messageIds[0], result.DeletedIds[0]); + } + + [Fact] + public async Task StreamAutoClaim_IncludesDeletedMessageIdAsync() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup", + consumer1 = "c1", + consumer2 = "c2"; + + // Create Consumer Group, add messages, and read messages into a consumer. + var messageIds = StreamAutoClaim_PrepareTestData(db, key, group, consumer1); + + // Delete one of the messages, it should be included in the deleted message ID array. + db.StreamDelete(key, new RedisValue[] { messageIds[0] }); + + // Claim a single pending message and reassign it to consumer2. + var result = await db.StreamAutoClaimAsync(key, group, consumer2, 0, "0-0", count: 1); + + Assert.Equal("0-0", result.NextStartId); + Assert.NotEmpty(result.ClaimedEntries); + Assert.NotEmpty(result.DeletedIds); + Assert.True(result.ClaimedEntries.Length == 1); + Assert.True(result.DeletedIds.Length == 1); + Assert.Equal(messageIds[0], result.DeletedIds[0]); + } + + [Fact] + public void StreamAutoClaim_NoMessagesToClaim() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup"; + + // Create the group. + db.KeyDelete(key); + db.StreamCreateConsumerGroup(key, group, createStream: true); + + // **Don't add any messages to the stream** + + // Claim any pending messages (there aren't any) and reassign them to consumer2. + var result = db.StreamAutoClaim(key, group, "consumer1", 0, "0-0"); + + // Claimed entries should be empty + Assert.Equal("0-0", result.NextStartId); + Assert.Empty(result.ClaimedEntries); + Assert.Empty(result.DeletedIds); + } + + [Fact] + public async Task StreamAutoClaim_NoMessagesToClaimAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup"; + + // Create the group. + db.KeyDelete(key); + db.StreamCreateConsumerGroup(key, group, createStream: true); + + // **Don't add any messages to the stream** + + // Claim any pending messages (there aren't any) and reassign them to consumer2. + var result = await db.StreamAutoClaimAsync(key, group, "consumer1", 0, "0-0"); + + // Claimed entries should be empty + Assert.Equal("0-0", result.NextStartId); + Assert.Empty(result.ClaimedEntries); + Assert.Empty(result.DeletedIds); + } + + [Fact] + public void StreamAutoClaim_NoMessageMeetsMinIdleTime() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup", + consumer1 = "c1", + consumer2 = "c2"; + + // Create Consumer Group, add messages, and read messages into a consumer. + _ = StreamAutoClaim_PrepareTestData(db, key, group, consumer1); + + // Claim messages idle for more than 5 minutes, should return an empty array. + var result = db.StreamAutoClaim(key, group, consumer2, 300000, "0-0"); + + Assert.Equal("0-0", result.NextStartId); + Assert.Empty(result.ClaimedEntries); + Assert.Empty(result.DeletedIds); + } + + [Fact] + public async Task StreamAutoClaim_NoMessageMeetsMinIdleTimeAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup", + consumer1 = "c1", + consumer2 = "c2"; + + // Create Consumer Group, add messages, and read messages into a consumer. + _ = StreamAutoClaim_PrepareTestData(db, key, group, consumer1); + + // Claim messages idle for more than 5 minutes, should return an empty array. + var result = await db.StreamAutoClaimAsync(key, group, consumer2, 300000, "0-0"); + + Assert.Equal("0-0", result.NextStartId); + Assert.Empty(result.ClaimedEntries); + Assert.Empty(result.DeletedIds); + } + + [Fact] + public void StreamAutoClaim_ReturnsMessageIdOnly() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup", + consumer1 = "c1", + consumer2 = "c2"; + + // Create Consumer Group, add messages, and read messages into a consumer. + var messageIds = StreamAutoClaim_PrepareTestData(db, key, group, consumer1); + + // Claim any pending messages and reassign them to consumer2. + var result = db.StreamAutoClaimIdsOnly(key, group, consumer2, 0, "0-0"); + + Assert.Equal("0-0", result.NextStartId); + Assert.NotEmpty(result.ClaimedIds); + Assert.Empty(result.DeletedIds); + Assert.True(result.ClaimedIds.Length == 2); + Assert.Equal(messageIds[0], result.ClaimedIds[0]); + Assert.Equal(messageIds[1], result.ClaimedIds[1]); + } + + [Fact] + public async Task StreamAutoClaim_ReturnsMessageIdOnlyAsync() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var key = Me(); + var db = conn.GetDatabase(); + const string group = "consumerGroup", + consumer1 = "c1", + consumer2 = "c2"; + + // Create Consumer Group, add messages, and read messages into a consumer. + var messageIds = StreamAutoClaim_PrepareTestData(db, key, group, consumer1); + + // Claim any pending messages and reassign them to consumer2. + var result = await db.StreamAutoClaimIdsOnlyAsync(key, group, consumer2, 0, "0-0"); + + Assert.Equal("0-0", result.NextStartId); + Assert.NotEmpty(result.ClaimedIds); + Assert.Empty(result.DeletedIds); + Assert.True(result.ClaimedIds.Length == 2); + Assert.Equal(messageIds[0], result.ClaimedIds[0]); + Assert.Equal(messageIds[1], result.ClaimedIds[1]); + } + + private RedisValue[] StreamAutoClaim_PrepareTestData(IDatabase db, RedisKey key, RedisValue group, RedisValue consumer) + { + // Create the group. + db.KeyDelete(key); + db.StreamCreateConsumerGroup(key, group, createStream: true); + + // Add some messages + var id1 = db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); + + // Read the messages into the "c1" + db.StreamReadGroup(key, group, consumer); + + return new RedisValue[2] { id1, id2 }; + } + [Fact] public void StreamConsumerGroupSetId() { diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index b29d36b1a..1d1058008 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -943,6 +943,20 @@ public void StreamAddAsync_2() mock.Verify(_ => _.StreamAddAsync("prefix:key", fields, "*", 1000, true, CommandFlags.None)); } + [Fact] + public void StreamAutoClaimAsync() + { + wrapper.StreamAutoClaimAsync("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); + mock.Verify(_ => _.StreamAutoClaimAsync("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None)); + } + + [Fact] + public void StreamAutoClaimIdsOnlyAsync() + { + wrapper.StreamAutoClaimIdsOnlyAsync("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); + mock.Verify(_ => _.StreamAutoClaimIdsOnlyAsync("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None)); + } + [Fact] public void StreamClaimMessagesAsync() { From dbd52f1807cfa513cb9a470232ef05442a02581c Mon Sep 17 00:00:00 2001 From: Steve Lorello <42971704+slorello89@users.noreply.github.com> Date: Tue, 19 Apr 2022 12:45:11 -0400 Subject: [PATCH 142/435] ZMPOP and LMPOP (#2094) Implementing [`ZMPOP`](https://redis.io/commands/zmpop/) and [`LMPOP`](https://redis.io/commands/lmpop/) as part of #2055 Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 2 + .../APITypes/ListPopResult.cs | 35 +++++ .../APITypes/SortedSetPopResult.cs | 35 +++++ src/StackExchange.Redis/Enums/RedisCommand.cs | 4 + .../Interfaces/IDatabase.cs | 34 +++++ .../Interfaces/IDatabaseAsync.cs | 34 +++++ .../KeyspaceIsolation/DatabaseWrapper.cs | 9 ++ .../KeyspaceIsolation/WrapperBase.cs | 9 ++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 18 +++ src/StackExchange.Redis/RawResult.cs | 15 ++ src/StackExchange.Redis/RedisDatabase.cs | 92 ++++++++++++ src/StackExchange.Redis/ResultProcessor.cs | 49 +++++++ tests/StackExchange.Redis.Tests/Lists.cs | 126 ++++++++++++++++ tests/StackExchange.Redis.Tests/SortedSets.cs | 137 ++++++++++++++++++ 14 files changed, 599 insertions(+) create mode 100644 src/StackExchange.Redis/APITypes/ListPopResult.cs create mode 100644 src/StackExchange.Redis/APITypes/SortedSetPopResult.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 8cec3ee88..c21d6b6d4 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -23,6 +23,8 @@ - Adds: Support for `GEOSEARCH` with `.GeoSearch()`/`.GeoSearchAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089)) - Adds: Support for `GEOSEARCHSTORE` with `.GeoSearchAndStore()`/`.GeoSearchAndStoreAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089)) - Adds: Support for `HRANDFIELD` with `.HashRandomField()`/`.HashRandomFieldAsync()`, `.HashRandomFields()`/`.HashRandomFieldsAsync()`, and `.HashRandomFieldsWithValues()`/`.HashRandomFieldsWithValuesAsync()` ([#2090 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2090)) +- Adds: Support for `LMPOP` with `.ListLeftPop()`/`.ListLeftPopAsync()` and `.ListRightPop()`/`.ListRightPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094)) +- Adds: Support for `ZMPOP` with `.SortedSetPop()`/`.SortedSetPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094)) - Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2095)) ## 2.5.61 diff --git a/src/StackExchange.Redis/APITypes/ListPopResult.cs b/src/StackExchange.Redis/APITypes/ListPopResult.cs new file mode 100644 index 000000000..149bed68a --- /dev/null +++ b/src/StackExchange.Redis/APITypes/ListPopResult.cs @@ -0,0 +1,35 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// A contiguous portion of a redis list. +/// +public readonly struct ListPopResult +{ + /// + /// A null ListPopResult, indicating no results. + /// + public static ListPopResult Null { get; } = new ListPopResult(RedisKey.Null, Array.Empty()); + + /// + /// Whether this object is null/empty. + /// + public bool IsNull => Key.IsNull && Values == Array.Empty(); + + /// + /// The key of the list that this set of entries came form. + /// + public RedisKey Key { get; } + + /// + /// The values from the list. + /// + public RedisValue[] Values { get; } + + internal ListPopResult(RedisKey key, RedisValue[] values) + { + Key = key; + Values = values; + } +} diff --git a/src/StackExchange.Redis/APITypes/SortedSetPopResult.cs b/src/StackExchange.Redis/APITypes/SortedSetPopResult.cs new file mode 100644 index 000000000..dcdc4c01e --- /dev/null +++ b/src/StackExchange.Redis/APITypes/SortedSetPopResult.cs @@ -0,0 +1,35 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// A contiguous portion of a redis sorted set. +/// +public readonly struct SortedSetPopResult +{ + /// + /// A null SortedSetPopResult, indicating no results. + /// + public static SortedSetPopResult Null { get; } = new SortedSetPopResult(RedisKey.Null, Array.Empty()); + + /// + /// Whether this object is null/empty. + /// + public bool IsNull => Key.IsNull && Entries == Array.Empty(); + + /// + /// The key of the sorted set these entries came form. + /// + public RedisKey Key { get; } + + /// + /// The provided entries of the sorted set. + /// + public SortedSetEntry[] Entries { get; } + + internal SortedSetPopResult(RedisKey key, SortedSetEntry[] entries) + { + Key = key; + Entries = entries; + } +} diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 6ec77ea01..9c8a6aa16 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -90,6 +90,7 @@ internal enum RedisCommand LINSERT, LLEN, LMOVE, + LMPOP, LPOP, LPOS, LPUSH, @@ -212,6 +213,7 @@ internal enum RedisCommand ZINTERCARD, ZINTERSTORE, ZLEXCOUNT, + ZMPOP, ZMSCORE, ZPOPMAX, ZPOPMIN, @@ -284,6 +286,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.INCRBYFLOAT: case RedisCommand.LINSERT: case RedisCommand.LMOVE: + case RedisCommand.LMPOP: case RedisCommand.LPOP: case RedisCommand.LPUSH: case RedisCommand.LPUSHX: @@ -335,6 +338,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.ZDIFFSTORE: case RedisCommand.ZINTERSTORE: case RedisCommand.ZINCRBY: + case RedisCommand.ZMPOP: case RedisCommand.ZPOPMAX: case RedisCommand.ZPOPMIN: case RedisCommand.ZRANGESTORE: diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 16975beb5..780ae0a47 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -922,6 +922,17 @@ public interface IDatabase : IRedis, IDatabaseAsync /// RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns at most elements from the first non-empty list in . + /// Starts on the left side of the list. + /// + /// The keys to look through for elements to pop. + /// The maximum number of elements to pop from the list. + /// The flags to use for this operation. + /// A span of contiguous elements from the list, or if no non-empty lists are found. + /// + ListPopResult ListLeftPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None); + /// /// Scans through the list stored at looking for , returning the 0-based /// index of the first matching element. @@ -1065,6 +1076,17 @@ public interface IDatabase : IRedis, IDatabaseAsync /// RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns at most elements from the first non-empty list in . + /// Starts on the right side of the list. + /// + /// The keys to look through for elements to pop. + /// The maximum number of elements to pop from the list. + /// The flags to use for this operation. + /// A span of contiguous elements from the list, or if no non-empty lists are found. + /// + ListPopResult ListRightPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None); + /// /// Atomically returns and removes the last element (tail) of the list stored at source, and pushes the element at the first element (head) of the list stored at destination. /// @@ -2110,6 +2132,18 @@ IEnumerable SortedSetScan(RedisKey key, /// SortedSetEntry[] SortedSetPop(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns up to entries from the first non-empty sorted set in . + /// Returns if none of the sets exist or contain any elements. + /// + /// The keys to check. + /// The maximum number of records to pop out of the sorted set. + /// The order to sort by when popping items out of the set. + /// The flags to use for the operation. + /// A contiguous collection of sorted set entries with the key they were popped from, or if no non-empty sorted sets are found. + /// + SortedSetPopResult SortedSetPop(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); + /// /// Allow the consumer to mark a pending message as correctly processed. Returns the number of messages acknowledged. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 846e5ba2f..d9a53ee2d 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -898,6 +898,17 @@ public interface IDatabaseAsync : IRedisAsync /// Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns at most elements from the first non-empty list in . + /// Starts on the left side of the list. + /// + /// The keys to look through for elements to pop. + /// The maximum number of elements to pop from the list. + /// The flags to use for this operation. + /// A span of contiguous elements from the list, or if no non-empty lists are found. + /// + Task ListLeftPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None); + /// /// Scans through the list stored at looking for , returning the 0-based /// index of the first matching element. @@ -1041,6 +1052,17 @@ public interface IDatabaseAsync : IRedisAsync /// Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns at most elements from the first non-empty list in . + /// Starts on the right side of the list. + /// + /// The keys to look through for elements to pop. + /// The maximum number of elements to pop from the list. + /// The flags to use for this operation. + /// A span of contiguous elements from the list, or if no non-empty lists are found. + /// + Task ListRightPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None); + /// /// Atomically returns and removes the last element (tail) of the list stored at source, and pushes the element at the first element (head) of the list stored at destination. /// @@ -2063,6 +2085,18 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// Task SortedSetPopAsync(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); + /// + /// Removes and returns up to entries from the first non-empty sorted set in . + /// Returns if none of the sets exist or contain any elements. + /// + /// The keys to check. + /// The maximum number of records to pop out of the sorted set. + /// The order to sort by when popping items out of the set. + /// The flags to use for the operation. + /// A contiguous collection of sorted set entries with the key they were popped from, or if no non-empty sorted sets are found. + /// + Task SortedSetPopAsync(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); + /// /// Allow the consumer to mark a pending message as correctly processed. Returns the number of messages acknowledged. /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index d08834c08..ad58d37b1 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -227,6 +227,9 @@ public RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.No public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => Inner.ListLeftPop(ToInner(key), count, flags); + public ListPopResult ListLeftPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + Inner.ListLeftPop(ToInner(keys), count, flags); + public long ListPosition(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => Inner.ListPosition(ToInner(key), element, rank, maxLength, flags); @@ -260,6 +263,9 @@ public RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.N public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => Inner.ListRightPop(ToInner(key), count, flags); + public ListPopResult ListRightPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPop(ToInner(keys), count, flags); + public RedisValue ListRightPopLeftPush(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) => Inner.ListRightPopLeftPush(ToInner(source), ToInner(destination), flags); @@ -484,6 +490,9 @@ public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue public SortedSetEntry[] SortedSetPop(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => Inner.SortedSetPop(ToInner(key), count, order, flags); + public SortedSetPopResult SortedSetPop(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetPop(ToInner(keys), count, order, flags); + public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) => Inner.StreamAcknowledge(ToInner(key), groupName, messageId, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 0ccaed1fa..ec5b44539 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -238,6 +238,9 @@ public Task ListLeftPopAsync(RedisKey key, CommandFlags flags = Comm public Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => Inner.ListLeftPopAsync(ToInner(key), count, flags); + public Task ListLeftPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + Inner.ListLeftPopAsync(ToInner(keys), count, flags); + public Task ListPositionAsync(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) => Inner.ListPositionAsync(ToInner(key), element, rank, maxLength, flags); @@ -271,6 +274,9 @@ public Task ListRightPopAsync(RedisKey key, CommandFlags flags = Com public Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => Inner.ListRightPopAsync(ToInner(key), count, flags); + public Task ListRightPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) => + Inner.ListRightPopAsync(ToInner(keys), count, flags); + public Task ListRightPopLeftPushAsync(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) => Inner.ListRightPopLeftPushAsync(ToInner(source), ToInner(destination), flags); @@ -501,6 +507,9 @@ public IAsyncEnumerable SortedSetScanAsync(RedisKey key, RedisVa public Task SortedSetPopAsync(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => Inner.SortedSetPopAsync(ToInner(key), count, order, flags); + public Task SortedSetPopAsync(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetPopAsync(ToInner(keys), count, order, flags); + public Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) => Inner.StreamAcknowledgeAsync(ToInner(key), groupName, messageId, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 8347638de..d98c12330 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -424,6 +424,11 @@ StackExchange.Redis.GeoUnit.Feet = 3 -> StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit.Kilometers = 1 -> StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit.Meters = 0 -> StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit.Miles = 2 -> StackExchange.Redis.GeoUnit +StackExchange.Redis.ListPopResult +StackExchange.Redis.ListPopResult.IsNull.get -> bool +StackExchange.Redis.ListPopResult.Key.get -> StackExchange.Redis.RedisKey +StackExchange.Redis.ListPopResult.ListPopResult() -> void +StackExchange.Redis.ListPopResult.Values.get -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.ListSide StackExchange.Redis.ListSide.Left = 0 -> StackExchange.Redis.ListSide StackExchange.Redis.ListSide.Right = 1 -> StackExchange.Redis.ListSide @@ -568,6 +573,7 @@ StackExchange.Redis.IDatabase.ListInsertBefore(StackExchange.Redis.RedisKey key, StackExchange.Redis.IDatabase.ListMove(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.ListSide sourceSide, StackExchange.Redis.ListSide destinationSide, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.ListLeftPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.ListLeftPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.ListLeftPop(StackExchange.Redis.RedisKey[]! keys, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ListPopResult StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags) -> long StackExchange.Redis.IDatabase.ListLeftPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long @@ -578,6 +584,7 @@ StackExchange.Redis.IDatabase.ListRange(StackExchange.Redis.RedisKey key, long s StackExchange.Redis.IDatabase.ListRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, long count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.ListRightPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.ListRightPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.ListRightPop(StackExchange.Redis.RedisKey[]! keys, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ListPopResult StackExchange.Redis.IDatabase.ListRightPopLeftPush(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.ListRightPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.ListRightPush(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags) -> long @@ -630,6 +637,7 @@ StackExchange.Redis.IDatabase.SortedSetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.IDatabase.SortedSetLengthByValue(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey key, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry? +StackExchange.Redis.IDatabase.SortedSetPop(StackExchange.Redis.RedisKey[]! keys, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetPopResult StackExchange.Redis.IDatabase.SortedSetRandomMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.SortedSetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.SortedSetRandomMembersWithScores(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! @@ -783,6 +791,7 @@ StackExchange.Redis.IDatabaseAsync.ListInsertBeforeAsync(StackExchange.Redis.Red StackExchange.Redis.IDatabaseAsync.ListMoveAsync(StackExchange.Redis.RedisKey sourceKey, StackExchange.Redis.RedisKey destinationKey, StackExchange.Redis.ListSide sourceSide, StackExchange.Redis.ListSide destinationSide, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLeftPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLeftPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListLeftPopAsync(StackExchange.Redis.RedisKey[]! keys, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListLeftPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -793,6 +802,7 @@ StackExchange.Redis.IDatabaseAsync.ListRangeAsync(StackExchange.Redis.RedisKey k StackExchange.Redis.IDatabaseAsync.ListRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, long count = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListRightPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListRightPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ListRightPopAsync(StackExchange.Redis.RedisKey[]! keys, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListRightPopLeftPushAsync(StackExchange.Redis.RedisKey source, StackExchange.Redis.RedisKey destination, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListRightPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ListRightPushAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! @@ -844,6 +854,7 @@ StackExchange.Redis.IDatabaseAsync.SortedSetLengthAsync(StackExchange.Redis.Redi StackExchange.Redis.IDatabaseAsync.SortedSetLengthByValueAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue min, StackExchange.Redis.RedisValue max, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetPopAsync(StackExchange.Redis.RedisKey[]! keys, long count, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetRandomMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetRandomMembersWithScoresAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -1390,6 +1401,11 @@ StackExchange.Redis.SortedSetOrder StackExchange.Redis.SortedSetOrder.ByLex = 2 -> StackExchange.Redis.SortedSetOrder StackExchange.Redis.SortedSetOrder.ByRank = 0 -> StackExchange.Redis.SortedSetOrder StackExchange.Redis.SortedSetOrder.ByScore = 1 -> StackExchange.Redis.SortedSetOrder +StackExchange.Redis.SortedSetPopResult +StackExchange.Redis.SortedSetPopResult.Entries.get -> StackExchange.Redis.SortedSetEntry[]! +StackExchange.Redis.SortedSetPopResult.IsNull.get -> bool +StackExchange.Redis.SortedSetPopResult.Key.get -> StackExchange.Redis.RedisKey +StackExchange.Redis.SortedSetPopResult.SortedSetPopResult() -> void StackExchange.Redis.SortType StackExchange.Redis.SortType.Alphabetic = 1 -> StackExchange.Redis.SortType StackExchange.Redis.SortType.Numeric = 0 -> StackExchange.Redis.SortType @@ -1555,6 +1571,7 @@ static StackExchange.Redis.HashEntry.operator ==(StackExchange.Redis.HashEntry x static StackExchange.Redis.KeyspaceIsolation.DatabaseExtensions.WithKeyPrefix(this StackExchange.Redis.IDatabase! database, StackExchange.Redis.RedisKey keyPrefix) -> StackExchange.Redis.IDatabase! static StackExchange.Redis.Lease.Create(int length, bool clear = true) -> StackExchange.Redis.Lease! static StackExchange.Redis.Lease.Empty.get -> StackExchange.Redis.Lease! +static StackExchange.Redis.ListPopResult.Null.get -> StackExchange.Redis.ListPopResult static StackExchange.Redis.LuaScript.GetCachedScriptCount() -> int static StackExchange.Redis.LuaScript.Prepare(string! script) -> StackExchange.Redis.LuaScript! static StackExchange.Redis.LuaScript.PurgeCache() -> void @@ -1672,6 +1689,7 @@ static StackExchange.Redis.SortedSetEntry.implicit operator StackExchange.Redis. static StackExchange.Redis.SortedSetEntry.implicit operator System.Collections.Generic.KeyValuePair(StackExchange.Redis.SortedSetEntry value) -> System.Collections.Generic.KeyValuePair static StackExchange.Redis.SortedSetEntry.operator !=(StackExchange.Redis.SortedSetEntry x, StackExchange.Redis.SortedSetEntry y) -> bool static StackExchange.Redis.SortedSetEntry.operator ==(StackExchange.Redis.SortedSetEntry x, StackExchange.Redis.SortedSetEntry y) -> bool +static StackExchange.Redis.SortedSetPopResult.Null.get -> StackExchange.Redis.SortedSetPopResult static StackExchange.Redis.StreamAutoClaimIdsOnlyResult.Null.get -> StackExchange.Redis.StreamAutoClaimIdsOnlyResult static StackExchange.Redis.StreamAutoClaimResult.Null.get -> StackExchange.Redis.StreamAutoClaimResult static StackExchange.Redis.StreamEntry.Null.get -> StackExchange.Redis.StreamEntry diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 919054411..11ca450af 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -284,6 +284,21 @@ internal bool GetBoolean() return AsGeoPosition(root.GetItems()); } + internal SortedSetEntry[]? GetItemsAsSortedSetEntryArray() => this.ToArray((in RawResult item) => AsSortedSetEntry(item.GetItems())); + + private static SortedSetEntry AsSortedSetEntry(in Sequence elements) + { + if (elements.IsSingleSegment) + { + var span = elements.FirstSpan; + return new SortedSetEntry(span[0].AsRedisValue(), span[1].TryGetDouble(out double val) ? val : double.NaN); + } + else + { + return new SortedSetEntry(elements[0].AsRedisValue(), elements[1].TryGetDouble(out double val) ? val : double.NaN); + } + } + private static GeoPosition AsGeoPosition(in Sequence coords) { double longitude, latitude; diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 7d05e46da..4f302f223 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1107,6 +1107,12 @@ public RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = C return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } + public ListPopResult ListLeftPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) + { + var msg = GetListMultiPopMessage(keys, RedisLiterals.LEFT, count, flags); + return ExecuteSync(msg, ResultProcessor.ListPopResult, defaultValue: ListPopResult.Null); + } + public long ListPosition(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) { var msg = CreateListPositionMessage(Database, flags, key, element, rank, maxLength); @@ -1131,6 +1137,12 @@ public Task ListLeftPopAsync(RedisKey key, long count, CommandFlag return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } + public Task ListLeftPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) + { + var msg = GetListMultiPopMessage(keys, RedisLiterals.LEFT, count, flags); + return ExecuteAsync(msg, ResultProcessor.ListPopResult, defaultValue: ListPopResult.Null); + } + public Task ListPositionAsync(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None) { var msg = CreateListPositionMessage(Database, flags, key, element, rank, maxLength); @@ -1249,6 +1261,12 @@ public RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } + public ListPopResult ListRightPop(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) + { + var msg = GetListMultiPopMessage(keys, RedisLiterals.RIGHT, count, flags); + return ExecuteSync(msg, ResultProcessor.ListPopResult, defaultValue: ListPopResult.Null); + } + public Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.RPOP, key); @@ -1261,6 +1279,12 @@ public Task ListRightPopAsync(RedisKey key, long count, CommandFla return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } + public Task ListRightPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None) + { + var msg = GetListMultiPopMessage(keys, RedisLiterals.RIGHT, count, flags); + return ExecuteAsync(msg, ResultProcessor.ListPopResult, defaultValue: ListPopResult.Null); + } + public RedisValue ListRightPopLeftPush(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.RPOPLPUSH, source, destination); @@ -2142,6 +2166,12 @@ public SortedSetEntry[] SortedSetPop(RedisKey key, long count, Order order = Ord return ExecuteSync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); } + public SortedSetPopResult SortedSetPop(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetMultiPopMessage(keys, order, count, flags); + return ExecuteSync(msg, ResultProcessor.SortedSetPopResult, defaultValue: SortedSetPopResult.Null); + } + public Task SortedSetPopAsync(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) { if (count == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState); @@ -2151,6 +2181,12 @@ public Task SortedSetPopAsync(RedisKey key, long count, Order return ExecuteAsync(msg, ResultProcessor.SortedSetWithScores, defaultValue: Array.Empty()); } + public Task SortedSetPopAsync(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) + { + var msg = GetSortedSetMultiPopMessage(keys, order, count, flags); + return ExecuteAsync(msg, ResultProcessor.SortedSetPopResult, defaultValue: SortedSetPopResult.Null); + } + public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None) { var msg = GetStreamAcknowledgeMessage(key, groupName, messageId, flags); @@ -3154,6 +3190,62 @@ private Message GetExpiryMessage(in RedisKey key, }; } + private Message GetListMultiPopMessage(RedisKey[] keys, RedisValue side, long count, CommandFlags flags) + { + if (keys is null || keys.Length == 0) + { + throw new ArgumentOutOfRangeException(nameof(keys), "keys must have a size of at least 1"); + } + + var slot = multiplexer.ServerSelectionStrategy.HashSlot(keys[0]); + + var args = new RedisValue[2 + keys.Length + (count == 1 ? 0 : 2)]; + var i = 0; + args[i++] = keys.Length; + foreach (var key in keys) + { + args[i++] = key.AsRedisValue(); + } + + args[i++] = side; + + if (count != 1) + { + args[i++] = RedisLiterals.COUNT; + args[i++] = count; + } + + return Message.CreateInSlot(Database, slot, flags, RedisCommand.LMPOP, args); + } + + private Message GetSortedSetMultiPopMessage(RedisKey[] keys, Order order, long count, CommandFlags flags) + { + if (keys is null || keys.Length == 0) + { + throw new ArgumentOutOfRangeException(nameof(keys), "keys must have a size of at least 1"); + } + + var slot = multiplexer.ServerSelectionStrategy.HashSlot(keys[0]); + + var args = new RedisValue[2 + keys.Length + (count == 1 ? 0 : 2)]; + var i = 0; + args[i++] = keys.Length; + foreach (var key in keys) + { + args[i++] = key.AsRedisValue(); + } + + args[i++] = order == Order.Ascending ? RedisLiterals.MIN : RedisLiterals.MAX; + + if (count != 1) + { + args[i++] = RedisLiterals.COUNT; + args[i++] = count; + } + + return Message.CreateInSlot(Database, slot, flags, RedisCommand.ZMPOP, args); + } + private Message? GetHashSetMessage(RedisKey key, HashEntry[] hashFields, CommandFlags flags) { if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 9ae426e7d..9b3fce521 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -116,6 +116,12 @@ public static readonly SortedSetEntryProcessor public static readonly SortedSetEntryArrayProcessor SortedSetWithScores = new SortedSetEntryArrayProcessor(); + public static readonly SortedSetPopResultProcessor + SortedSetPopResult = new SortedSetPopResultProcessor(); + + public static readonly ListPopResultProcessor + ListPopResult = new ListPopResultProcessor(); + public static readonly SingleStreamProcessor SingleStream = new SingleStreamProcessor(); @@ -570,6 +576,49 @@ protected override SortedSetEntry Parse(in RawResult first, in RawResult second) new SortedSetEntry(first.AsRedisValue(), second.TryGetDouble(out double val) ? val : double.NaN); } + internal sealed class SortedSetPopResultProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Type == ResultType.MultiBulk) + { + if (result.IsNull) + { + SetResult(message, Redis.SortedSetPopResult.Null); + return true; + } + + var arr = result.GetItems(); + SetResult(message, new SortedSetPopResult(arr[0].AsRedisKey(), arr[1].GetItemsAsSortedSetEntryArray()!)); + return true; + } + + return false; + } + } + + internal sealed class ListPopResultProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Type == ResultType.MultiBulk) + { + if (result.IsNull) + { + SetResult(message, Redis.ListPopResult.Null); + return true; + } + + var arr = result.GetItems(); + SetResult(message, new ListPopResult(arr[0].AsRedisKey(), arr[1].GetItemsAsValues()!)); + return true; + } + + return false; + } + } + + internal sealed class HashEntryArrayProcessor : ValuePairInterleavedProcessorBase { protected override HashEntry Parse(in RawResult first, in RawResult second) => diff --git a/tests/StackExchange.Redis.Tests/Lists.cs b/tests/StackExchange.Redis.Tests/Lists.cs index 8263d5971..119df434e 100644 --- a/tests/StackExchange.Redis.Tests/Lists.cs +++ b/tests/StackExchange.Redis.Tests/Lists.cs @@ -831,4 +831,130 @@ public void ListPositionFireAndForget() Assert.Equal(-1, res); } + + [Fact] + public async Task ListMultiPopSingleKeyAsync() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key); + + db.ListLeftPush(key, "yankees"); + db.ListLeftPush(key, "blue jays"); + db.ListLeftPush(key, "orioles"); + db.ListLeftPush(key, "red sox"); + db.ListLeftPush(key, "rays"); + + var res = await db.ListLeftPopAsync(new RedisKey[] { key }, 1); + + Assert.False(res.IsNull); + Assert.Single(res.Values); + Assert.Equal("rays", res.Values[0]); + + res = await db.ListRightPopAsync(new RedisKey[] { key }, 2); + + Assert.False(res.IsNull); + Assert.Equal(2, res.Values.Length); + Assert.Equal("yankees", res.Values[0]); + Assert.Equal("blue jays", res.Values[1]); + } + + [Fact] + public async Task ListMultiPopMultipleKeysAsync() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key); + + db.ListLeftPush(key, "yankees"); + db.ListLeftPush(key, "blue jays"); + db.ListLeftPush(key, "orioles"); + db.ListLeftPush(key, "red sox"); + db.ListLeftPush(key, "rays"); + + var res = await db.ListLeftPopAsync(new RedisKey[] { "empty-key", key, "also-empty" }, 2); + + Assert.False(res.IsNull); + Assert.Equal(2, res.Values.Length); + Assert.Equal("rays", res.Values[0]); + Assert.Equal("red sox", res.Values[1]); + + res = await db.ListRightPopAsync(new RedisKey[] { "empty-key", key, "also-empty" }, 1); + + Assert.False(res.IsNull); + Assert.Single(res.Values); + Assert.Equal("yankees", res.Values[0]); + } + + [Fact] + public void ListMultiPopSingleKey() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + + db.ListLeftPush(key, "yankees"); + db.ListLeftPush(key, "blue jays"); + db.ListLeftPush(key, "orioles"); + db.ListLeftPush(key, "red sox"); + db.ListLeftPush(key, "rays"); + + var res = db.ListLeftPop(new RedisKey[] { key }, 1); + + Assert.False(res.IsNull); + Assert.Single(res.Values); + Assert.Equal("rays", res.Values[0]); + + res = db.ListRightPop(new RedisKey[] { key }, 2); + + Assert.False(res.IsNull); + Assert.Equal(2, res.Values.Length); + Assert.Equal("yankees", res.Values[0]); + Assert.Equal("blue jays", res.Values[1]); + } + + [Fact] + public async Task ListMultiPopZeroCount() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + + var exception = await Assert.ThrowsAsync(() => db.ListLeftPopAsync(new RedisKey[] { key }, 0)); + Assert.Contains("ERR count should be greater than 0", exception.Message); + } + + [Fact] + public async Task ListMultiPopEmpty() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + + var res = await db.ListLeftPopAsync(new RedisKey[] { key }, 1); + Assert.True(res.IsNull); + } + + [Fact] + public void ListMultiPopEmptyKeys() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var exception = Assert.Throws(() => db.ListRightPop(Array.Empty(), 5)); + Assert.Contains("keys must have a size of at least 1", exception.Message); + + exception = Assert.Throws(() => db.ListLeftPop(Array.Empty(), 5)); + Assert.Contains("keys must have a size of at least 1", exception.Message); + } } diff --git a/tests/StackExchange.Redis.Tests/SortedSets.cs b/tests/StackExchange.Redis.Tests/SortedSets.cs index b0eca7a87..94c21a996 100644 --- a/tests/StackExchange.Redis.Tests/SortedSets.cs +++ b/tests/StackExchange.Redis.Tests/SortedSets.cs @@ -966,6 +966,143 @@ public void SortedSetRangeStoreFailExclude() Assert.Equal("exclude", exception.ParamName); } + [Fact] + public void SortedSetMultiPopSingleKey() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + + db.SortedSetAdd(key, new SortedSetEntry[] { + new SortedSetEntry("rays", 100), + new SortedSetEntry("yankees", 92), + new SortedSetEntry("red sox", 92), + new SortedSetEntry("blue jays", 91), + new SortedSetEntry("orioles", 52), + }); + + var highest = db.SortedSetPop(new RedisKey[] { key }, 1, order: Order.Descending); + Assert.False(highest.IsNull); + Assert.Equal(key, highest.Key); + var entry = Assert.Single(highest.Entries); + Assert.Equal("rays", entry.Element); + Assert.Equal(100, entry.Score); + + var bottom2 = db.SortedSetPop(new RedisKey[] { key }, 2); + Assert.False(bottom2.IsNull); + Assert.Equal(key, bottom2.Key); + Assert.Equal(2, bottom2.Entries.Length); + Assert.Equal("orioles", bottom2.Entries[0].Element); + Assert.Equal(52, bottom2.Entries[0].Score); + Assert.Equal("blue jays", bottom2.Entries[1].Element); + Assert.Equal(91, bottom2.Entries[1].Score); + } + + [Fact] + public void SortedSetMultiPopMultiKey() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + + db.SortedSetAdd(key, new SortedSetEntry[] { + new SortedSetEntry("rays", 100), + new SortedSetEntry("yankees", 92), + new SortedSetEntry("red sox", 92), + new SortedSetEntry("blue jays", 91), + new SortedSetEntry("orioles", 52), + }); + + var highest = db.SortedSetPop(new RedisKey[] { "not a real key", key, "yet another not a real key" }, 1, order: Order.Descending); + Assert.False(highest.IsNull); + Assert.Equal(key, highest.Key); + var entry = Assert.Single(highest.Entries); + Assert.Equal("rays", entry.Element); + Assert.Equal(100, entry.Score); + + var bottom2 = db.SortedSetPop(new RedisKey[] { "not a real key", key, "yet another not a real key" }, 2); + Assert.False(bottom2.IsNull); + Assert.Equal(key, bottom2.Key); + Assert.Equal(2, bottom2.Entries.Length); + Assert.Equal("orioles", bottom2.Entries[0].Element); + Assert.Equal(52, bottom2.Entries[0].Score); + Assert.Equal("blue jays", bottom2.Entries[1].Element); + Assert.Equal(91, bottom2.Entries[1].Score); + } + + [Fact] + public void SortedSetMultiPopNoSet() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + var res = db.SortedSetPop(new RedisKey[] { key }, 1); + Assert.True(res.IsNull); + } + + [Fact] + public void SortedSetMultiPopCount0() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + var exception = Assert.Throws(() => db.SortedSetPop(new RedisKey[] { key }, 0)); + Assert.Contains("ERR count should be greater than 0", exception.Message); + } + + [Fact] + public async Task SortedSetMultiPopAsync() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key); + + db.SortedSetAdd(key, new SortedSetEntry[] { + new SortedSetEntry("rays", 100), + new SortedSetEntry("yankees", 92), + new SortedSetEntry("red sox", 92), + new SortedSetEntry("blue jays", 91), + new SortedSetEntry("orioles", 52), + }); + + var highest = await db.SortedSetPopAsync( + new RedisKey[] { "not a real key", key, "yet another not a real key" }, 1, order: Order.Descending); + Assert.False(highest.IsNull); + Assert.Equal(key, highest.Key); + var entry = Assert.Single(highest.Entries); + Assert.Equal("rays", entry.Element); + Assert.Equal(100, entry.Score); + + var bottom2 = await db.SortedSetPopAsync(new RedisKey[] { "not a real key", key, "yet another not a real key" }, 2); + Assert.False(bottom2.IsNull); + Assert.Equal(key, bottom2.Key); + Assert.Equal(2, bottom2.Entries.Length); + Assert.Equal("orioles", bottom2.Entries[0].Element); + Assert.Equal(52, bottom2.Entries[0].Score); + Assert.Equal("blue jays", bottom2.Entries[1].Element); + Assert.Equal(91, bottom2.Entries[1].Score); + } + + [Fact] + public void SortedSetMultiPopEmptyKeys() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var exception = Assert.Throws(() => db.SortedSetPop(Array.Empty(), 5)); + Assert.Contains("keys must have a size of at least 1", exception.Message); + } + [Fact] public void SortedSetRangeStoreFailForReplica() { From f0fa79c59700a58a1cb848175c544e82ff0d2180 Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Tue, 19 Apr 2022 20:34:33 +0300 Subject: [PATCH 143/435] Support OBJECT FREQ (#2105) Adds support for https://redis.io/commands/object-freq/ (#2055) Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + .../Interfaces/IDatabase.cs | 11 +++++ .../Interfaces/IDatabaseAsync.cs | 11 +++++ .../KeyspaceIsolation/DatabaseWrapper.cs | 3 ++ .../KeyspaceIsolation/WrapperBase.cs | 3 ++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 2 + src/StackExchange.Redis/RedisDatabase.cs | 13 ++++++ src/StackExchange.Redis/RedisLiterals.cs | 1 + .../DatabaseWrapperTests.cs | 7 +++ tests/StackExchange.Redis.Tests/Keys.cs | 43 +++++++++++++++++++ .../WrapperBaseTests.cs | 7 +++ 11 files changed, 102 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index c21d6b6d4..463da7c7f 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -26,6 +26,7 @@ - Adds: Support for `LMPOP` with `.ListLeftPop()`/`.ListLeftPopAsync()` and `.ListRightPop()`/`.ListRightPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094)) - Adds: Support for `ZMPOP` with `.SortedSetPop()`/`.SortedSetPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094)) - Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2095)) +- Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105)) ## 2.5.61 diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 780ae0a47..5ca31542f 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -754,6 +754,17 @@ public interface IDatabase : IRedis, IDatabaseAsync /// DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Returns the logarithmic access frequency counter of the object stored at . + /// The command is only available when the maxmemory-policy configuration directive is set to + /// one of the LFU policies. + /// + /// The key to get a frequency count for. + /// The flags to use for this operation. + /// The number of logarithmic access frequency counter, ( if the key does not exist). + /// + long? KeyFrequency(RedisKey key, CommandFlags flags = CommandFlags.None); + /// /// Returns the time since the object stored at the specified key is idle (not requested by read or write operations). /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index d9a53ee2d..724be9caa 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -730,6 +730,17 @@ public interface IDatabaseAsync : IRedisAsync /// Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Returns the logarithmic access frequency counter of the object stored at . + /// The command is only available when the maxmemory-policy configuration directive is set to + /// one of the LFU policies. + /// + /// The key to get a frequency count for. + /// The flags to use for this operation. + /// The number of logarithmic access frequency counter, ( if the key does not exist). + /// + Task KeyFrequencyAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// /// Returns the time since the object stored at the specified key is idle (not requested by read or write operations). /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index ad58d37b1..62a9ae6fb 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -182,6 +182,9 @@ public bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFl public DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.KeyExpireTime(ToInner(key), flags); + public long? KeyFrequency(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyFrequency(ToInner(key), flags); + public TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.KeyIdleTime(ToInner(key), flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index ec5b44539..200cd67b5 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -193,6 +193,9 @@ public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when public Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.KeyExpireTimeAsync(ToInner(key), flags); + public Task KeyFrequencyAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.KeyFrequencyAsync(ToInner(key), flags); + public Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.KeyIdleTimeAsync(ToInner(key), flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index d98c12330..517c48d3a 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -555,6 +555,7 @@ StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.ExpireWhen when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyExpireTime(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.DateTime? +StackExchange.Redis.IDatabase.KeyFrequency(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long? StackExchange.Redis.IDatabase.KeyIdleTime(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.TimeSpan? StackExchange.Redis.IDatabase.KeyMigrate(StackExchange.Redis.RedisKey key, System.Net.EndPoint! toServer, int toDatabase = 0, int timeoutMilliseconds = 0, StackExchange.Redis.MigrateOptions migrateOptions = StackExchange.Redis.MigrateOptions.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IDatabase.KeyMove(StackExchange.Redis.RedisKey key, int database, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool @@ -773,6 +774,7 @@ StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey k StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.ExpireWhen when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyExpireTimeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyFrequencyAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyIdleTimeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyMigrateAsync(StackExchange.Redis.RedisKey key, System.Net.EndPoint! toServer, int toDatabase = 0, int timeoutMilliseconds = 0, StackExchange.Redis.MigrateOptions migrateOptions = StackExchange.Redis.MigrateOptions.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyMoveAsync(StackExchange.Redis.RedisKey key, int database, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 4f302f223..47553653e 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -877,11 +877,24 @@ public Task KeyExpireAsync(RedisKey key, DateTime? expire, ExpireWhen when return ExecuteAsync(msg, ResultProcessor.NullableDateTimeFromMilliseconds); } + public long? KeyFrequency(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.OBJECT, RedisLiterals.FREQ, key); + return ExecuteSync(msg, ResultProcessor.NullableInt64); + } + + public Task KeyFrequencyAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.OBJECT, RedisLiterals.FREQ, key); + return ExecuteAsync(msg, ResultProcessor.NullableInt64); + } + public TimeSpan? KeyIdleTime(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.OBJECT, RedisLiterals.IDLETIME, key); return ExecuteSync(msg, ResultProcessor.TimeSpanFromSeconds); } + public Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.OBJECT, RedisLiterals.IDLETIME, key); diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 3b405ae8b..44d4c60ac 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -63,6 +63,7 @@ public static readonly RedisValue EXAT = "EXAT", EXISTS = "EXISTS", FLUSH = "FLUSH", + FREQ = "FREQ", GET = "GET", GETNAME = "GETNAME", GT = "GT", diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index f73162634..1f47d5d9f 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -319,6 +319,13 @@ public void KeyExpireTime() mock.Verify(_ => _.KeyExpireTime("prefix:key", CommandFlags.None)); } + [Fact] + public void KeyFrequency() + { + wrapper.KeyFrequency("key", CommandFlags.None); + mock.Verify(_ => _.KeyFrequency("prefix:key", CommandFlags.None)); + } + [Fact] public void KeyMigrate() { diff --git a/tests/StackExchange.Redis.Tests/Keys.cs b/tests/StackExchange.Redis.Tests/Keys.cs index 65dd48c51..cd09cf9de 100644 --- a/tests/StackExchange.Redis.Tests/Keys.cs +++ b/tests/StackExchange.Redis.Tests/Keys.cs @@ -296,4 +296,47 @@ public async Task KeyRefCount() Assert.Null(db.KeyRefCount(keyNotExists)); Assert.Null(await db.KeyRefCountAsync(keyNotExists)); } + + [Fact] + public async Task KeyFrequency() + { + using var conn = Create(allowAdmin: true, require: RedisFeatures.v4_0_0); + + var key = Me(); + var db = conn.GetDatabase(); + var server = GetServer(conn); + + var serverConfig = server.ConfigGet("maxmemory-policy"); + var maxMemoryPolicy = serverConfig.Length == 1 ? serverConfig[0].Value : ""; + Log($"maxmemory-policy detected as {maxMemoryPolicy}"); + var isLfu = maxMemoryPolicy.Contains("lfu"); + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + db.StringGet(key); + + if (isLfu) + { + var count = db.KeyFrequency(key); + Assert.True(count > 0); + + count = await db.KeyFrequencyAsync(key); + Assert.True(count > 0); + + // Key not exists + db.KeyDelete(key, CommandFlags.FireAndForget); + var res = db.KeyFrequency(key); + Assert.Null(res); + + res = await db.KeyFrequencyAsync(key); + Assert.Null(res); + } + else + { + var ex = Assert.Throws(() => db.KeyFrequency(key)); + Assert.Contains("An LFU maxmemory policy is not selected", ex.Message); + ex = await Assert.ThrowsAsync(() => db.KeyFrequencyAsync(key)); + Assert.Contains("An LFU maxmemory policy is not selected", ex.Message); + } + } } diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index 1d1058008..6acbb41f9 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -280,6 +280,13 @@ public void KeyExpireTimeAsync() mock.Verify(_ => _.KeyExpireTimeAsync("prefix:key", CommandFlags.None)); } + [Fact] + public void KeyFrequencyAsync() + { + wrapper.KeyFrequencyAsync("key", CommandFlags.None); + mock.Verify(_ => _.KeyFrequencyAsync("prefix:key", CommandFlags.None)); + } + [Fact] public void KeyMigrateAsync() { From 989528f498d5861e8af11cdea7f6e8d841525532 Mon Sep 17 00:00:00 2001 From: Steve Lorello <42971704+slorello89@users.noreply.github.com> Date: Wed, 20 Apr 2022 09:41:06 -0400 Subject: [PATCH 144/435] Add support for SORT_RO (#2111) Introducing [`SORT_RO`](https://redis.io/commands/sort_ro/) as part of #2055 @NickCraver - I modified the path for message creation for the Sort command to point to using `SORT_RO` when possible, this again had to do a version-check against the multiplexer, which I'm not certain is the correct way - thoughts? Also, SORT is considered a write command and will be rejected out of hand by a replica (so I'm moving that as well) ```text 127.0.0.1:6378> SORT test (error) READONLY You can't write against a read only replica. ``` Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 4 +- .../Interfaces/IDatabase.cs | 2 + .../Interfaces/IDatabaseAsync.cs | 1 + src/StackExchange.Redis/RedisDatabase.cs | 49 +++++++++---------- src/StackExchange.Redis/RedisFeatures.cs | 5 ++ tests/StackExchange.Redis.Tests/Sets.cs | 46 +++++++++++++++++ 7 files changed, 81 insertions(+), 27 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 463da7c7f..9b366cd7e 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -27,6 +27,7 @@ - Adds: Support for `ZMPOP` with `.SortedSetPop()`/`.SortedSetPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094)) - Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2095)) - Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105)) +- Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111)) ## 2.5.61 diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 9c8a6aa16..786316303 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -166,6 +166,7 @@ internal enum RedisCommand SMISMEMBER, SMOVE, SORT, + SORT_RO, SPOP, SRANDMEMBER, SREM, @@ -320,6 +321,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.SETRANGE: case RedisCommand.SINTERSTORE: case RedisCommand.SMOVE: + case RedisCommand.SORT: case RedisCommand.SPOP: case RedisCommand.SREM: case RedisCommand.SUNIONSTORE: @@ -428,7 +430,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.SLOWLOG: case RedisCommand.SMEMBERS: case RedisCommand.SMISMEMBER: - case RedisCommand.SORT: + case RedisCommand.SORT_RO: case RedisCommand.SRANDMEMBER: case RedisCommand.STRLEN: case RedisCommand.SUBSCRIBE: diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 5ca31542f..2485ad27f 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1551,6 +1551,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// the get parameter (note that # specifies the element itself, when used in get). /// Referring to the redis SORT documentation for examples is recommended. /// When used in hashes, by and get can be used to specify fields using -> notation (again, refer to redis documentation). + /// Uses SORT_RO when possible. /// /// The key of the list, set, or sorted set. /// How many entries to skip on the return. @@ -1562,6 +1563,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The sorted elements, or the external values if get is specified. /// + /// RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 724be9caa..50d129eea 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1516,6 +1516,7 @@ public interface IDatabaseAsync : IRedisAsync /// the get parameter (note that # specifies the element itself, when used in get). /// Referring to the redis SORT documentation for examples is recommended. /// When used in hashes, by and get can be used to specify fields using -> notation (again, refer to redis documentation). + /// Uses SORT_RO when possible. /// /// The key of the list, set, or sorted set. /// How many entries to skip on the return. diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 47553653e..c3c189909 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1775,26 +1775,26 @@ private CursorEnumerable SetScanAsync(RedisKey key, RedisValue patte public RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) { - var msg = GetSortedSetAddMessage(default(RedisKey), key, skip, take, order, sortType, by, get, flags); - return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + var msg = GetSortMessage(RedisKey.Null, key, skip, take, order, sortType, by, get, flags, out var server); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, server: server, defaultValue: Array.Empty()); } public long SortAndStore(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) { - var msg = GetSortedSetAddMessage(destination, key, skip, take, order, sortType, by, get, flags); - return ExecuteSync(msg, ResultProcessor.Int64); + var msg = GetSortMessage(destination, key, skip, take, order, sortType, by, get, flags, out var server); + return ExecuteSync(msg, ResultProcessor.Int64, server); } public Task SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) { - var msg = GetSortedSetAddMessage(destination, key, skip, take, order, sortType, by, get, flags); - return ExecuteAsync(msg, ResultProcessor.Int64); + var msg = GetSortMessage(destination, key, skip, take, order, sortType, by, get, flags, out var server); + return ExecuteAsync(msg, ResultProcessor.Int64, server); } public Task SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None) { - var msg = GetSortedSetAddMessage(default(RedisKey), key, skip, take, order, sortType, by, get, flags); - return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + var msg = GetSortMessage(RedisKey.Null, key, skip, take, order, sortType, by, get, flags, out var server); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty(), server: server); } public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags) @@ -3513,28 +3513,25 @@ private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double s } } - private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long skip, long take, Order order, SortType sortType, RedisValue by, RedisValue[]? get, CommandFlags flags) + private Message GetSortMessage(RedisKey destination, RedisKey key, long skip, long take, Order order, SortType sortType, RedisValue by, RedisValue[]? get, CommandFlags flags, out ServerEndPoint? server) { + server = null; + var command = destination.IsNull && GetFeatures(key, flags, out server).ReadOnlySort + ? RedisCommand.SORT_RO + : RedisCommand.SORT; + // most common cases; no "get", no "by", no "destination", no "skip", no "take" if (destination.IsNull && skip == 0 && take == -1 && by.IsNull && (get == null || get.Length == 0)) { - switch (order) + return order switch { - case Order.Ascending: - switch (sortType) - { - case SortType.Numeric: return Message.Create(Database, flags, RedisCommand.SORT, key); - case SortType.Alphabetic: return Message.Create(Database, flags, RedisCommand.SORT, key, RedisLiterals.ALPHA); - } - break; - case Order.Descending: - switch (sortType) - { - case SortType.Numeric: return Message.Create(Database, flags, RedisCommand.SORT, key, RedisLiterals.DESC); - case SortType.Alphabetic: return Message.Create(Database, flags, RedisCommand.SORT, key, RedisLiterals.DESC, RedisLiterals.ALPHA); - } - break; - } + Order.Ascending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key), + Order.Ascending when sortType == SortType.Alphabetic => Message.Create(Database, flags, command, key, RedisLiterals.ALPHA), + Order.Descending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key, RedisLiterals.DESC), + Order.Descending when sortType == SortType.Alphabetic => Message.Create(Database, flags, command, key, RedisLiterals.DESC, RedisLiterals.ALPHA), + Order.Ascending or Order.Descending => throw new ArgumentOutOfRangeException(nameof(sortType)), + _ => throw new ArgumentOutOfRangeException(nameof(order)), + }; } // and now: more complicated scenarios... @@ -3578,7 +3575,7 @@ private Message GetSortedSetAddMessage(RedisKey destination, RedisKey key, long values.Add(item); } } - if (destination.IsNull) return Message.Create(Database, flags, RedisCommand.SORT, key, values.ToArray()); + if (destination.IsNull) return Message.Create(Database, flags, command, key, values.ToArray()); // Because we are using STORE, we need to push this to a primary if (Message.GetPrimaryReplicaFlags(flags) == CommandFlags.DemandReplica) diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index a2dcee19e..1a5bd9b98 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -132,6 +132,11 @@ public RedisFeatures(Version version) /// public bool PushIfNotExists => Version >= v2_1_1; + /// + /// Does this support SORT_RO? + /// + internal bool ReadOnlySort => Version >= v7_0_0_rc1; + /// /// Is SCAN (cursor-based scanning) available? /// diff --git a/tests/StackExchange.Redis.Tests/Sets.cs b/tests/StackExchange.Redis.Tests/Sets.cs index d32e12573..436691eff 100644 --- a/tests/StackExchange.Redis.Tests/Sets.cs +++ b/tests/StackExchange.Redis.Tests/Sets.cs @@ -343,4 +343,50 @@ public void SetPopMulti_Nil() var arr = db.SetPop(key, 1); Assert.Empty(arr); } + + [Fact] + public async Task TestSortReadonlyPrimary() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key); + + var random = new Random(); + var items = Enumerable.Repeat(0, 200).Select(_ => random.Next()).ToList(); + await db.SetAddAsync(key, items.Select(x=>(RedisValue)x).ToArray()); + items.Sort(); + + var result = db.Sort(key).Select(x=>(int)x); + Assert.Equal(items, result); + + result = (await db.SortAsync(key)).Select(x => (int)x); + Assert.Equal(items, result); + } + + [Fact] + public async Task TestSortReadonlyReplica() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key); + + var random = new Random(); + var items = Enumerable.Repeat(0, 200).Select(_ => random.Next()).ToList(); + await db.SetAddAsync(key, items.Select(x=>(RedisValue)x).ToArray()); + + using var readonlyConn = Create(configuration: TestConfig.Current.ReplicaServerAndPort, require: RedisFeatures.v7_0_0_rc1); + var readonlyDb = conn.GetDatabase(); + + items.Sort(); + + var result = readonlyDb.Sort(key).Select(x => (int)x); + Assert.Equal(items, result); + + result = (await readonlyDb.SortAsync(key)).Select(x => (int)x); + Assert.Equal(items, result); + } } From e74076de9333d462ba2c11d4fe323cd5574a956c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 21 Apr 2022 11:09:44 +0100 Subject: [PATCH 145/435] avoid byte[] allocations when calculating cluster slot (#2110) * avoid byte[] allocations when calculating cluster slot * release notes * Quick style fixes * 0. use RedisKey.CopyTo when building byte[] 1. remove RedisKey.CompositeEquals - prefer CopyTo 2. use [SkipLocalsInit] * 1. add tests for key equality 2. fix broken RedisKey.GetHashCode() !!! * we now pass GetHashCode() for null vs "" etc * tweak release notes * Misc tidy * use compiler magic for CRLF - not stackalloc * add Nick into Co-authored-by: Nick Craver --- Directory.Build.props | 2 +- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ExtensionMethods.cs | 9 +- src/StackExchange.Redis/RedisKey.cs | 232 ++++++++++++++---- src/StackExchange.Redis/RedisValue.cs | 9 +- .../ServerSelectionStrategy.cs | 35 ++- src/StackExchange.Redis/SkipLocalsInit.cs | 14 ++ tests/StackExchange.Redis.Tests/Keys.cs | 197 +++++++++++++++ 8 files changed, 437 insertions(+), 62 deletions(-) create mode 100644 src/StackExchange.Redis/SkipLocalsInit.cs diff --git a/Directory.Build.props b/Directory.Build.props index b01dc7c53..2c67e1a02 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,7 @@ $(MSBuildThisFileDirectory)StackExchange.Redis.snk $(AssemblyName) strict - Stack Exchange, Inc.; marc.gravell + Stack Exchange, Inc.; Marc Gravell; Nick Craver true $(MSBuildThisFileDirectory)Shared.ruleset NETSDK1069 diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 9b366cd7e..94b775a8d 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -27,6 +27,7 @@ - Adds: Support for `ZMPOP` with `.SortedSetPop()`/`.SortedSetPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094)) - Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2095)) - Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105)) +- Performance: Avoids allocations when computing cluster hash slots or testing key equality ([#2110 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2110)) - Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111)) ## 2.5.61 diff --git a/src/StackExchange.Redis/ExtensionMethods.cs b/src/StackExchange.Redis/ExtensionMethods.cs index b84381d4c..89b9a0e21 100644 --- a/src/StackExchange.Redis/ExtensionMethods.cs +++ b/src/StackExchange.Redis/ExtensionMethods.cs @@ -310,10 +310,11 @@ internal static int VectorSafeIndexOf(this ReadOnlySpan span, byte value) [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int VectorSafeIndexOfCRLF(this ReadOnlySpan span) - { - ReadOnlySpan CRLF = stackalloc byte[2] { (byte)'\r', (byte)'\n' }; - return span.IndexOf(CRLF); - } + => span.IndexOf(CRLF); + + // note that this is *not* actually an array; this is compiled into a .data section + // (confirmed down to net472, which is the lowest TFM that uses this branch) + private static ReadOnlySpan CRLF => new byte[] { (byte)'\r', (byte)'\n' }; #else internal static int VectorSafeIndexOf(this ReadOnlySpan span, byte value) { diff --git a/src/StackExchange.Redis/RedisKey.cs b/src/StackExchange.Redis/RedisKey.cs index 113a3e646..28378d116 100644 --- a/src/StackExchange.Redis/RedisKey.cs +++ b/src/StackExchange.Redis/RedisKey.cs @@ -1,4 +1,7 @@ using System; +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text; namespace StackExchange.Redis @@ -44,115 +47,171 @@ internal bool IsEmpty /// /// The first to compare. /// The second to compare. - public static bool operator !=(RedisKey x, RedisKey y) => !(x == y); + public static bool operator !=(RedisKey x, RedisKey y) => !x.EqualsImpl(in y); /// /// Indicate whether two keys are not equal. /// /// The first to compare. /// The second to compare. - public static bool operator !=(string x, RedisKey y) => !(x == y); + public static bool operator !=(string x, RedisKey y) => !y.EqualsImpl(new RedisKey(x)); /// /// Indicate whether two keys are not equal. /// /// The first to compare. /// The second to compare. - public static bool operator !=(byte[] x, RedisKey y) => !(x == y); + public static bool operator !=(byte[] x, RedisKey y) => !y.EqualsImpl(new RedisKey(null, x)); /// /// Indicate whether two keys are not equal. /// /// The first to compare. /// The second to compare. - public static bool operator !=(RedisKey x, string y) => !(x == y); + public static bool operator !=(RedisKey x, string y) => !x.EqualsImpl(new RedisKey(y)); /// /// Indicate whether two keys are not equal. /// /// The first to compare. /// The second to compare. - public static bool operator !=(RedisKey x, byte[] y) => !(x == y); + public static bool operator !=(RedisKey x, byte[] y) => !x.EqualsImpl(new RedisKey(null, y)); /// /// Indicate whether two keys are equal. /// /// The first to compare. /// The second to compare. - public static bool operator ==(RedisKey x, RedisKey y) => CompositeEquals(x.KeyPrefix, x.KeyValue, y.KeyPrefix, y.KeyValue); + public static bool operator ==(RedisKey x, RedisKey y) => x.EqualsImpl(in y); /// /// Indicate whether two keys are equal. /// /// The first to compare. /// The second to compare. - public static bool operator ==(string x, RedisKey y) => CompositeEquals(null, x, y.KeyPrefix, y.KeyValue); + public static bool operator ==(string x, RedisKey y) => y.EqualsImpl(new RedisKey(x)); /// /// Indicate whether two keys are equal. /// /// The first to compare. /// The second to compare. - public static bool operator ==(byte[] x, RedisKey y) => CompositeEquals(null, x, y.KeyPrefix, y.KeyValue); + public static bool operator ==(byte[] x, RedisKey y) => y.EqualsImpl(new RedisKey(null, x)); /// /// Indicate whether two keys are equal. /// /// The first to compare. /// The second to compare. - public static bool operator ==(RedisKey x, string y) => CompositeEquals(x.KeyPrefix, x.KeyValue, null, y); + public static bool operator ==(RedisKey x, string y) => x.EqualsImpl(new RedisKey(y)); /// /// Indicate whether two keys are equal. /// /// The first to compare. /// The second to compare. - public static bool operator ==(RedisKey x, byte[] y) => CompositeEquals(x.KeyPrefix, x.KeyValue, null, y); + public static bool operator ==(RedisKey x, byte[] y) => x.EqualsImpl(new RedisKey(null, y)); /// /// See . /// /// The to compare to. - public override bool Equals(object? obj) + public override bool Equals(object? obj) => obj switch { - if (obj is RedisKey other) - { - return CompositeEquals(KeyPrefix, KeyValue, other.KeyPrefix, other.KeyValue); - } - if (obj is string || obj is byte[]) - { - return CompositeEquals(KeyPrefix, KeyValue, null, obj); - } - return false; - } + null => IsNull, + RedisKey key => EqualsImpl(in key), + string s => EqualsImpl(new RedisKey(s)), + byte[] b => EqualsImpl(new RedisKey(null, b)), + _ => false, + }; /// /// Indicate whether two keys are equal. /// /// The to compare to. - public bool Equals(RedisKey other) => CompositeEquals(KeyPrefix, KeyValue, other.KeyPrefix, other.KeyValue); + public bool Equals(RedisKey other) => EqualsImpl(in other); - private static bool CompositeEquals(byte[]? keyPrefix0, object? keyValue0, byte[]? keyPrefix1, object? keyValue1) + private bool EqualsImpl(in RedisKey other) { - if (RedisValue.Equals(keyPrefix0, keyPrefix1)) + if (IsNull) + { + return other.IsNull; + } + else if (other.IsNull) { - if (keyValue0 == keyValue1) return true; // ref equal - if (keyValue0 == null || keyValue1 == null) return false; // null vs non-null + return false; + } + + // if there's no prefix, we might be able to do a simple compare + if (RedisValue.Equals(KeyPrefix, other.KeyPrefix)) + { + if ((object?)KeyValue == (object?)other.KeyValue) return true; // ref equal - if (keyValue0 is string keyString1 && keyValue1 is string keyString2) return keyString1 == keyString2; - if (keyValue0 is byte[] keyBytes1 && keyValue1 is byte[] keyBytes2) return RedisValue.Equals(keyBytes1, keyBytes2); + if (KeyValue is string keyString1 && other.KeyValue is string keyString2) return keyString1 == keyString2; + if (KeyValue is byte[] keyBytes1 && other.KeyValue is byte[] keyBytes2) return RedisValue.Equals(keyBytes1, keyBytes2); + } + + int len = TotalLength(); + if (len != other.TotalLength()) + { + return false; // different length; can't be equal + } + if (len == 0) + { + return true; // both empty + } + if (len <= 128) + { + return CopyCompare(in this, in other, len, stackalloc byte[len * 2]); + } + else + { + byte[] arr = ArrayPool.Shared.Rent(len * 2); + var result = CopyCompare(in this, in other, len, arr); + ArrayPool.Shared.Return(arr); + return result; } - return RedisValue.Equals(ConcatenateBytes(keyPrefix0, keyValue0, null), ConcatenateBytes(keyPrefix1, keyValue1, null)); + static bool CopyCompare(in RedisKey x, in RedisKey y, int length, Span span) + { + Span span1 = span.Slice(0, length), span2 = span.Slice(length, length); + var written = x.CopyTo(span1); + Debug.Assert(written == length, "length error (1)"); + written = y.CopyTo(span2); + Debug.Assert(written == length, "length error (2)"); + return span1.SequenceEqual(span2); + } } /// public override int GetHashCode() { - int chk0 = KeyPrefix == null ? 0 : RedisValue.GetHashCode(KeyPrefix), - chk1 = KeyValue is string ? KeyValue.GetHashCode() : RedisValue.GetHashCode((byte[]?)KeyValue); + // note that we need need eaulity-like behavior, regardless of whether the + // parts look like bytes or strings, and with/without prefix + + // the simplest way to do this is to use the CopyTo version, which normalizes that + if (IsNull) return -1; + if (TryGetSimpleBuffer(out var buffer)) return RedisValue.GetHashCode(buffer); + var len = TotalLength(); + if (len == 0) return 0; - return unchecked((17 * chk0) + chk1); + if (len <= 256) + { + Span span = stackalloc byte[len]; + var written = CopyTo(span); + Debug.Assert(written == len); + return RedisValue.GetHashCode(span); + } + else + { + var arr = ArrayPool.Shared.Rent(len); + var span = new Span(arr, 0, len); + var written = CopyTo(span); + Debug.Assert(written == len); + var result = RedisValue.GetHashCode(span); + ArrayPool.Shared.Return(arr); + return result; + } } /// @@ -194,7 +253,18 @@ public static implicit operator RedisKey(byte[]? key) /// Obtain the as a . /// /// The key to get a byte array for. - public static implicit operator byte[]? (RedisKey key) => ConcatenateBytes(key.KeyPrefix, key.KeyValue, null); + public static implicit operator byte[]? (RedisKey key) + { + if (key.IsNull) return null; + if (key.TryGetSimpleBuffer(out var arr)) return arr; + + var len = key.TotalLength(); + if (len == 0) return Array.Empty(); + arr = new byte[len]; + var written = key.CopyTo(arr); + Debug.Assert(written == len, "length/copyto error"); + return arr; + } /// /// Obtain the key as a . @@ -202,27 +272,36 @@ public static implicit operator RedisKey(byte[]? key) /// The key to get a string for. public static implicit operator string? (RedisKey key) { - byte[]? arr; - if (key.KeyPrefix == null) + if (key.KeyPrefix is null) { - if (key.KeyValue == null) return null; + return key.KeyValue switch + { + null => null, + string s => s, + object o => Get((byte[])o, -1), + }; + } - if (key.KeyValue is string keyString) return keyString; + var len = key.TotalLength(); + var arr = ArrayPool.Shared.Rent(len); + var written = key.CopyTo(arr); + Debug.Assert(written == len, "length error"); + var result = Get(arr, len); + ArrayPool.Shared.Return(arr); + return result; - arr = (byte[])key.KeyValue; - } - else + static string? Get(byte[] arr, int length) { - arr = (byte[]?)key; - } - if (arr == null) return null; - try - { - return Encoding.UTF8.GetString(arr); - } - catch - { - return BitConverter.ToString(arr); + if (length == -1) length = arr.Length; + if (length == 0) return ""; + try + { + return Encoding.UTF8.GetString(arr, 0, length); + } + catch + { + return BitConverter.ToString(arr, 0, length); + } } } @@ -297,5 +376,58 @@ internal static RedisKey WithPrefix(byte[]? prefix, RedisKey value) /// /// The suffix to append. public RedisKey Append(RedisKey suffix) => WithPrefix(this, suffix); + + internal bool TryGetSimpleBuffer([NotNullWhen(true)] out byte[]? arr) + { + arr = KeyValue is null ? Array.Empty() : KeyValue as byte[]; + return arr is not null && (KeyPrefix is null || KeyPrefix.Length == 0); + } + + internal int TotalLength() => + (KeyPrefix is null ? 0 : KeyPrefix.Length) + KeyValue switch + { + null => 0, + string s => Encoding.UTF8.GetByteCount(s), + _ => ((byte[])KeyValue).Length, + }; + + internal int CopyTo(Span destination) + { + int written = 0; + if (KeyPrefix is not null && KeyPrefix.Length != 0) + { + KeyPrefix.CopyTo(destination); + written += KeyPrefix.Length; + destination = destination.Slice(KeyPrefix.Length); + } + switch (KeyValue) + { + case null: + break; // nothing to do + case string s: + if (s.Length != 0) + { +#if NETCOREAPP + written += Encoding.UTF8.GetBytes(s, destination); +#else + unsafe + { + fixed (byte* bPtr = destination) + fixed (char* cPtr = s) + { + written += Encoding.UTF8.GetBytes(cPtr, s.Length, bPtr, destination.Length); + } + } +#endif + } + break; + default: + var arr = (byte[])KeyValue; + arr.CopyTo(destination); + written += arr.Length; + break; + } + return written; + } } } diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index 9ff2188af..ba16b6d42 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -274,17 +274,16 @@ internal static unsafe bool Equals(byte[]? x, byte[]? y) return true; } - internal static unsafe int GetHashCode(ReadOnlyMemory memory) + internal static unsafe int GetHashCode(ReadOnlySpan span) { unchecked { - var span8 = memory.Span; - int len = span8.Length; + int len = span.Length; if (len == 0) return 0; int acc = 728271210; - var span64 = MemoryMarshal.Cast(span8); + var span64 = MemoryMarshal.Cast(span); for (int i = 0; i < span64.Length; i++) { var val = span64[i]; @@ -294,7 +293,7 @@ internal static unsafe int GetHashCode(ReadOnlyMemory memory) int spare = len % 8, offset = len - spare; while (spare-- != 0) { - acc = (((acc << 5) + acc) ^ span8[offset++]); + acc = (((acc << 5) + acc) ^ span[offset++]); } return acc; } diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index f654c0142..a4dbb92a7 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -1,4 +1,6 @@ using System; +using System.Buffers; +using System.Diagnostics; using System.Net; using System.Threading; @@ -59,13 +61,42 @@ internal sealed class ServerSelectionStrategy /// /// The to determine a slot ID for. public int HashSlot(in RedisKey key) - => ServerType == ServerType.Standalone || key.IsNull ? NoSlot : GetClusterSlot((byte[])key!); + { + if (ServerType == ServerType.Standalone || key.IsNull) return NoSlot; + if (key.TryGetSimpleBuffer(out var arr)) // key was constructed from a byte[] + { + return GetClusterSlot(arr); + } + else + { + var length = key.TotalLength(); + if (length <= 256) + { + Span span = stackalloc byte[length]; + var written = key.CopyTo(span); + Debug.Assert(written == length, "key length/write error"); + return GetClusterSlot(span); + } + else + { + arr = ArrayPool.Shared.Rent(length); + var span = new Span(arr, 0, length); + var written = key.CopyTo(span); + Debug.Assert(written == length, "key length/write error"); + var result = GetClusterSlot(span); + ArrayPool.Shared.Return(arr); + return result; + } + } + } /// /// Computes the hash-slot that would be used by the given channel. /// /// The to determine a slot ID for. public int HashSlot(in RedisChannel channel) + // note that the RedisChannel->byte[] converter is always direct, so this is not an alloc + // (we deal with channels far less frequently, so pay the encoding cost up-front) => ServerType == ServerType.Standalone || channel.IsNull ? NoSlot : GetClusterSlot((byte[])channel!); /// @@ -74,7 +105,7 @@ public int HashSlot(in RedisChannel channel) /// /// HASH_SLOT = CRC16(key) mod 16384 /// - private static unsafe int GetClusterSlot(byte[] blob) + private static unsafe int GetClusterSlot(ReadOnlySpan blob) { unchecked { diff --git a/src/StackExchange.Redis/SkipLocalsInit.cs b/src/StackExchange.Redis/SkipLocalsInit.cs new file mode 100644 index 000000000..66f84567e --- /dev/null +++ b/src/StackExchange.Redis/SkipLocalsInit.cs @@ -0,0 +1,14 @@ +// turn off ".locals init"; this gives a small perf boost, but is particularly relevant when stackalloc is used +// side-effects: locals don't have defined zero values; normally this doesn't matter, due to "definite assignment", +// but it *can* be observed when using unsafe code, any "out" method that cheats, or "stackalloc" - the last is +// the most relevant to us, so we have audited that no "stackalloc" use expects the buffers to be zero'd initially +[module:System.Runtime.CompilerServices.SkipLocalsInit] + +#if !NET5_0_OR_GREATER +// when not available, we can spoof it in a private type +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Interface, Inherited = false)] + internal sealed class SkipLocalsInitAttribute : Attribute {} +} +#endif diff --git a/tests/StackExchange.Redis.Tests/Keys.cs b/tests/StackExchange.Redis.Tests/Keys.cs index cd09cf9de..e6b4c6395 100644 --- a/tests/StackExchange.Redis.Tests/Keys.cs +++ b/tests/StackExchange.Redis.Tests/Keys.cs @@ -1,4 +1,6 @@ using System; +using System.Buffers; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -339,4 +341,199 @@ public async Task KeyFrequency() Assert.Contains("An LFU maxmemory policy is not selected", ex.Message); } } + + private static void TestTotalLengthAndCopyTo(in RedisKey key, int expectedLength) + { + var length = key.TotalLength(); + Assert.Equal(expectedLength, length); + var arr = ArrayPool.Shared.Rent(length + 20); // deliberately over-sized + try + { + var written = key.CopyTo(arr); + Assert.Equal(length, written); + + var viaCast = (byte[]?)key; + ReadOnlySpan x = viaCast, y = new ReadOnlySpan(arr, 0, length); + Assert.True(x.SequenceEqual(y)); + Assert.True(key.IsNull == viaCast is null); + } + finally + { + ArrayPool.Shared.Return(arr); + } + } + + [Fact] + public void NullKeySlot() + { + RedisKey key = RedisKey.Null; + Assert.True(key.TryGetSimpleBuffer(out var buffer)); + Assert.Empty(buffer); + TestTotalLengthAndCopyTo(key, 0); + + Assert.Equal(-1, GetHashSlot(key)); + } + + private static readonly byte[] KeyPrefix = Encoding.UTF8.GetBytes("abcde"); + + private static int GetHashSlot(in RedisKey key) + { + var strategy = new ServerSelectionStrategy(null!) + { + ServerType = ServerType.Cluster + }; + return strategy.HashSlot(key); + } + + [Theory] + [InlineData(false, null, -1)] + [InlineData(false, "", 0)] + [InlineData(false, "f", 3168)] + [InlineData(false, "abcde", 16097)] + [InlineData(false, "abcdef", 15101)] + [InlineData(false, "abcdeffsdkjhsdfgkjh sdkjhsdkjf hsdkjfh skudrfy7 348iu yksef78 dssdhkfh ##$OIU", 5073)] + [InlineData(false, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras lobortis quam ac molestie ultricies. Duis maximus, nunc a auctor faucibus, risus turpis porttitor nibh, sit amet consequat lacus nibh quis nisi. Aliquam ipsum quam, dapibus ut ex eu, efficitur vestibulum dui. Sed a nibh ut felis congue tempor vel vel lectus. Phasellus a neque placerat, blandit massa sed, imperdiet urna. Praesent scelerisque lorem ipsum, non facilisis libero hendrerit quis. Nullam sit amet malesuada velit, ac lacinia lacus. Donec mollis a massa sed egestas. Suspendisse vitae augue quis erat gravida consectetur. Aenean interdum neque id lacinia eleifend.", 4954)] + [InlineData(true, null, 16097)] + [InlineData(true, "", 16097)] // note same as false/abcde + [InlineData(true, "f", 15101)] // note same as false/abcdef + [InlineData(true, "abcde", 4089)] + [InlineData(true, "abcdef", 1167)] + [InlineData(true, "👻👩‍👩‍👦‍👦", 8494)] + [InlineData(true, "abcdeffsdkjhsdfgkjh sdkjhsdkjf hsdkjfh skudrfy7 348iu yksef78 dssdhkfh ##$OIU", 10923)] + [InlineData(true, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras lobortis quam ac molestie ultricies. Duis maximus, nunc a auctor faucibus, risus turpis porttitor nibh, sit amet consequat lacus nibh quis nisi. Aliquam ipsum quam, dapibus ut ex eu, efficitur vestibulum dui. Sed a nibh ut felis congue tempor vel vel lectus. Phasellus a neque placerat, blandit massa sed, imperdiet urna. Praesent scelerisque lorem ipsum, non facilisis libero hendrerit quis. Nullam sit amet malesuada velit, ac lacinia lacus. Donec mollis a massa sed egestas. Suspendisse vitae augue quis erat gravida consectetur. Aenean interdum neque id lacinia eleifend.", 4452)] + public void TestStringKeySlot(bool prefixed, string? s, int slot) + { + RedisKey key = prefixed ? new RedisKey(KeyPrefix, s) : s; + if (s is null && !prefixed) + { + Assert.True(key.TryGetSimpleBuffer(out var buffer)); + Assert.Empty(buffer); + TestTotalLengthAndCopyTo(key, 0); + } + else + { + Assert.False(key.TryGetSimpleBuffer(out var _)); + } + TestTotalLengthAndCopyTo(key, Encoding.UTF8.GetByteCount(s ?? "") + (prefixed ? KeyPrefix.Length : 0)); + + Assert.Equal(slot, GetHashSlot(key)); + } + + [Theory] + [InlineData(false, -1, -1)] + [InlineData(false, 0, 0)] + [InlineData(false, 1, 10242)] + [InlineData(false, 6, 10015)] + [InlineData(false, 47, 849)] + [InlineData(false, 14123, 2356)] + [InlineData(true, -1, 16097)] + [InlineData(true, 0, 16097)] + [InlineData(true, 1, 7839)] + [InlineData(true, 6, 6509)] + [InlineData(true, 47, 2217)] + [InlineData(true, 14123, 6773)] + public void TestBlobKeySlot(bool prefixed, int count, int slot) + { + byte[]? blob = null; + if (count >= 0) + { + blob = new byte[count]; + new Random(count).NextBytes(blob); + for (int i = 0; i < blob.Length; i++) + { + if (blob[i] == (byte)'{') blob[i] = (byte)'!'; // avoid unexpected hash tags + } + } + RedisKey key = prefixed ? new RedisKey(KeyPrefix, blob) : blob; + if (prefixed) + { + Assert.False(key.TryGetSimpleBuffer(out _)); + } + else + { + Assert.True(key.TryGetSimpleBuffer(out var buffer)); + if (blob is null) + { + Assert.Empty(buffer); + } + else + { + Assert.Same(blob, buffer); + } + } + TestTotalLengthAndCopyTo(key, (blob?.Length ?? 0) + (prefixed ? KeyPrefix.Length : 0)); + + Assert.Equal(slot, GetHashSlot(key)); + } + + [Theory] + [MemberData(nameof(KeyEqualityData))] + public void KeyEquality(RedisKey x, RedisKey y, bool equal) + { + if (equal) + { + Assert.Equal(x, y); + Assert.True(x == y); + Assert.False(x != y); + Assert.True(x.Equals(y)); + Assert.True(x.Equals((object)y)); + Assert.Equal(x.GetHashCode(), y.GetHashCode()); + } + else + { + Assert.NotEqual(x, y); + Assert.False(x == y); + Assert.True(x != y); + Assert.False(x.Equals(y)); + Assert.False(x.Equals((object)y)); + // note that this last one is not strictly required, but: we pass, so: yay! + Assert.NotEqual(x.GetHashCode(), y.GetHashCode()); + } + } + + public static IEnumerable KeyEqualityData() + { + RedisKey abcString = "abc", abcBytes = Encoding.UTF8.GetBytes("abc"); + RedisKey abcdefString = "abcdef", abcdefBytes = Encoding.UTF8.GetBytes("abcdef"); + + yield return new object[] { RedisKey.Null, abcString, false }; + yield return new object[] { RedisKey.Null, abcBytes, false }; + yield return new object[] { abcString, RedisKey.Null, false }; + yield return new object[] { abcBytes, RedisKey.Null, false }; + yield return new object[] { RedisKey.Null, RedisKey.Null, true }; + yield return new object[] { new RedisKey((string?)null), RedisKey.Null, true }; + yield return new object[] { new RedisKey(null, (byte[]?)null), RedisKey.Null, true }; + yield return new object[] { new RedisKey(""), RedisKey.Null, false }; + yield return new object[] { new RedisKey(null, Array.Empty()), RedisKey.Null, false }; + + yield return new object[] { abcString, abcString, true }; + yield return new object[] { abcBytes, abcBytes, true }; + yield return new object[] { abcString, abcBytes, true }; + yield return new object[] { abcBytes, abcString, true }; + + yield return new object[] { abcdefString, abcdefString, true }; + yield return new object[] { abcdefBytes, abcdefBytes, true }; + yield return new object[] { abcdefString, abcdefBytes, true }; + yield return new object[] { abcdefBytes, abcdefString, true }; + + yield return new object[] { abcString, abcdefString, false }; + yield return new object[] { abcBytes, abcdefBytes, false }; + yield return new object[] { abcString, abcdefBytes, false }; + yield return new object[] { abcBytes, abcdefString, false }; + + yield return new object[] { abcdefString, abcString, false }; + yield return new object[] { abcdefBytes, abcBytes, false }; + yield return new object[] { abcdefString, abcBytes, false }; + yield return new object[] { abcdefBytes, abcString, false }; + + var x = abcString.Append("def"); + yield return new object[] { abcdefString, x, true }; + yield return new object[] { abcdefBytes, x, true }; + yield return new object[] { x, abcdefBytes, true }; + yield return new object[] { x, abcdefString, true }; + yield return new object[] { abcString, x, false }; + yield return new object[] { abcString, x, false }; + yield return new object[] { x, abcString, false }; + yield return new object[] { x, abcString, false }; + } } From d3e26c4024457bb3d0708436539f29740e79f879 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 21 Apr 2022 14:45:29 -0400 Subject: [PATCH 146/435] Add .NET 6.0 env variable for color output (#2113) See https://github.com/dotnet/runtime/pull/47935 for details, basically enables color output even though console output is redirected for a better time in GitHub actions. Co-authored-by: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com> --- .github/workflows/CI.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 35b9c8daf..382d72520 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,6 +13,9 @@ jobs: main: name: StackExchange.Redis (Ubuntu) runs-on: ubuntu-latest + env: + DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: "1" # Enable color output, even though the console output is redirected in Actions + TERM: xterm # Enable color output in GitHub Actions steps: - name: Checkout code uses: actions/checkout@v1 @@ -43,6 +46,8 @@ jobs: runs-on: windows-2022 env: NUGET_CERT_REVOCATION_MODE: offline # Disabling signing because of massive perf hit, see https://github.com/NuGet/Home/issues/11548 + DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: "1" # Note this doesn't work yet for Windows - see https://github.com/dotnet/runtime/issues/68340 + TERM: xterm steps: - name: Checkout code uses: actions/checkout@v1 From 77a159c358b7c94e66f3507724febec7c5411bc4 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 26 Apr 2022 11:47:55 +0100 Subject: [PATCH 147/435] for pub/sub, treat length-one arrays comparably to simple values (#2118) * fix #2117 for pub/sub, treak length-one arrays comparably to simple values * update release notes * actually check whether we're meant to be allowing arrays --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/PhysicalConnection.cs | 29 ++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 94b775a8d..e4197240a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -29,6 +29,7 @@ - Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105)) - Performance: Avoids allocations when computing cluster hash slots or testing key equality ([#2110 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2110)) - Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111)) +- Adds: Support for pub/sub payloads that are unary arrays ([#2118 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2118)) ## 2.5.61 diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 19669cbe9..a6647134c 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -1498,10 +1498,10 @@ private void MatchResult(in RawResult result) // invoke the handlers var channel = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.PatternMode.Literal); Trace("MESSAGE: " + channel); - if (!channel.IsNull) + if (!channel.IsNull && TryGetPubSubPayload(items[2], out var payload)) { _readStatus = ReadStatus.InvokePubSub; - muxer.OnMessage(channel, channel, items[2].AsRedisValue()); + muxer.OnMessage(channel, channel, payload); } return; // AND STOP PROCESSING! } @@ -1511,11 +1511,11 @@ private void MatchResult(in RawResult result) var channel = items[2].AsRedisChannel(ChannelPrefix, RedisChannel.PatternMode.Literal); Trace("PMESSAGE: " + channel); - if (!channel.IsNull) + if (!channel.IsNull && TryGetPubSubPayload(items[3], out var payload)) { var sub = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.PatternMode.Pattern); _readStatus = ReadStatus.InvokePubSub; - muxer.OnMessage(sub, channel, items[3].AsRedisValue()); + muxer.OnMessage(sub, channel, payload); } return; // AND STOP PROCESSING! } @@ -1551,6 +1551,27 @@ private void MatchResult(in RawResult result) } _readStatus = ReadStatus.MatchResultComplete; _activeMessage = null; + + static bool TryGetPubSubPayload(in RawResult value, out RedisValue parsed, bool allowArraySingleton = true) + { + if (value.IsNull) + { + parsed = RedisValue.Null; + return true; + } + switch (value.Type) + { + case ResultType.Integer: + case ResultType.SimpleString: + case ResultType.BulkString: + parsed = value.AsRedisValue(); + return true; + case ResultType.MultiBulk when allowArraySingleton && value.ItemsCount == 1: + return TryGetPubSubPayload(in value[0], out parsed, allowArraySingleton: false); + } + parsed = default; + return false; + } } private volatile Message? _activeMessage; From c1aaf4f990544b17a7cdcf773bbef1309c88d73c Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 26 Apr 2022 10:26:12 -0400 Subject: [PATCH 148/435] Fix for #2071: StringSet compatibility (#2098) I missed an overload case being an idiot - adding the missing source break for the case in #2071. ...also fixing KeyTouch ordering while in here. Note that adding `CommandFlags` back optional seems like a quick fix and I did try that route, but in a full test suite here it became apparent that created other ambiguous overload cases, so went this route. --- docs/ReleaseNotes.md | 1 + .../Interfaces/IDatabase.cs | 17 +++--- .../Interfaces/IDatabaseAsync.cs | 17 +++--- .../KeyspaceIsolation/DatabaseWrapper.cs | 3 +- .../KeyspaceIsolation/WrapperBase.cs | 3 +- src/StackExchange.Redis/PublicAPI.Shipped.txt | 2 + src/StackExchange.Redis/RedisDatabase.cs | 6 ++ .../DatabaseWrapperTests.cs | 8 +++ tests/StackExchange.Redis.Tests/Naming.cs | 10 +++- .../OverloadCompat.cs | 56 +++++++++++++++++++ .../WrapperBaseTests.cs | 8 +++ 11 files changed, 108 insertions(+), 23 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/OverloadCompat.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index e4197240a..a3bf5e25f 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -26,6 +26,7 @@ - Adds: Support for `LMPOP` with `.ListLeftPop()`/`.ListLeftPopAsync()` and `.ListRightPop()`/`.ListRightPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094)) - Adds: Support for `ZMPOP` with `.SortedSetPop()`/`.SortedSetPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094)) - Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2095)) +- Fix [#2071](https://github.com/StackExchange/StackExchange.Redis/issues/2071): Add `.StringSet()`/`.StringSetAsync()` overloads for source compat broken for 1 case in 2.5.61 ([#2098 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2098)) - Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105)) - Performance: Avoids allocations when computing cluster hash slots or testing key equality ([#2110 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2110)) - Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111)) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 2485ad27f..d80603dee 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Net; namespace StackExchange.Redis @@ -2758,16 +2759,12 @@ IEnumerable SortedSetScan(RedisKey key, /// long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Set key to hold the string value. If key already holds a value, it is overwritten, regardless of its type. - /// - /// The key of the string. - /// The value to set. - /// The expiry to set. - /// Which condition to set the value under (defaults to always). - /// The flags to use for this operation. - /// if the string was set, otherwise. - /// https://redis.io/commands/set + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when); + + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags); /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 50d129eea..d0fcaa355 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Net; using System.Threading.Tasks; @@ -2710,16 +2711,12 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Set key to hold the string value. If key already holds a value, it is overwritten, regardless of its type. - /// - /// The key of the string. - /// The value to set. - /// The expiry to set. - /// Which condition to set the value under (defaults to always). - /// The flags to use for this operation. - /// if the string was set, otherwise. - /// https://redis.io/commands/set + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when); + + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags); /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 62a9ae6fb..c2a9fff8a 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -643,9 +643,10 @@ public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) = public bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.StringSet(ToInner(values), when, flags); + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when) => + Inner.StringSet(ToInner(key), value, expiry, when); public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => Inner.StringSet(ToInner(key), value, expiry, when, flags); - public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.StringSet(ToInner(key), value, expiry, keepTtl, when, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 200cd67b5..c7b0d929f 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -660,9 +660,10 @@ public Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFl public Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.StringSetAsync(ToInner(values), when, flags); + public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when) => + Inner.StringSetAsync(ToInner(key), value, expiry, when); public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => Inner.StringSetAsync(ToInner(key), value, expiry, when, flags); - public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.StringSetAsync(ToInner(key), value, expiry, keepTtl, when, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 517c48d3a..290c1a2ca 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -708,6 +708,7 @@ StackExchange.Redis.IDatabase.StringIncrement(StackExchange.Redis.RedisKey key, StackExchange.Redis.IDatabase.StringIncrement(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when) -> bool StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> bool StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.StringSetAndGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue @@ -927,6 +928,7 @@ StackExchange.Redis.IDatabaseAsync.StringLengthAsync(StackExchange.Redis.RedisKe StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAsync(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetBitAsync(StackExchange.Redis.RedisKey key, long offset, bool bit, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index c3c189909..d12bd4a0f 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -3018,6 +3018,9 @@ public Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFl return ExecuteAsync(msg, ResultProcessor.Int64); } + // Backwards compatibility overloads: + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when) => + StringSet(key, value, expiry, false, when, CommandFlags.None); public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => StringSet(key, value, expiry, false, when, flags); @@ -3033,6 +3036,9 @@ public bool StringSet(KeyValuePair[] values, When when = W return ExecuteSync(msg, ResultProcessor.Boolean); } + // Backwards compatibility overloads: + public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when) => + StringSetAsync(key, value, expiry, false, when); public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => StringSetAsync(key, value, expiry, false, when, flags); diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 1f47d5d9f..87b12949e 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -1317,6 +1317,14 @@ public void StringSet_3() mock.Verify(_ => _.StringSet(It.Is(valid), When.Exists, CommandFlags.None)); } + [Fact] + public void StringSet_Compat() + { + TimeSpan? expiry = null; + wrapper.StringSet("key", "value", expiry, When.Exists); + mock.Verify(_ => _.StringSet("prefix:key", "value", expiry, When.Exists)); + } + [Fact] public void StringSetBit() { diff --git a/tests/StackExchange.Redis.Tests/Naming.cs b/tests/StackExchange.Redis.Tests/Naming.cs index 55997848c..7540eb8a6 100644 --- a/tests/StackExchange.Redis.Tests/Naming.cs +++ b/tests/StackExchange.Redis.Tests/Naming.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -138,7 +139,14 @@ public void CheckSyncAsyncMethodsMatch(Type from, Type to) var pFrom = method.GetParameters(); Type[] args = pFrom.Select(x => x.ParameterType).ToArray(); Log("Checking: {0}.{1}", from.Name, method.Name); - Assert.Equal(typeof(CommandFlags), args.Last()); + if (method.GetCustomAttribute() is EditorBrowsableAttribute attr && attr.State == EditorBrowsableState.Never) + { + // For compatibility overloads, explicitly don't ensure CommandFlags is last + } + else + { + Assert.Equal(typeof(CommandFlags), args.Last()); + } var found = to.GetMethod(huntName, flags, null, method.CallingConvention, args, null); Assert.NotNull(found); // "Found " + name + ", no " + huntName var pTo = found.GetParameters(); diff --git a/tests/StackExchange.Redis.Tests/OverloadCompat.cs b/tests/StackExchange.Redis.Tests/OverloadCompat.cs new file mode 100644 index 000000000..ffe8958c2 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/OverloadCompat.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests; + +/// +/// This test set is for when we add an overload, to making sure all +/// past versions work correctly and aren't source breaking. +/// +[Collection(SharedConnectionFixture.Key)] +public class OverloadCompat : TestBase +{ + public OverloadCompat(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + + [Fact] + public async Task StringGet() + { + using var conn = Create(); + var db = conn.GetDatabase(); + RedisKey key = Me(); + RedisValue val = "myval"; + var expiresIn = TimeSpan.FromSeconds(10); + var when = When.Always; + var flags = CommandFlags.None; + + db.StringSet(key, val); + db.StringSet(key, val, expiry: expiresIn); + db.StringSet(key, val, when: when); + db.StringSet(key, val, flags: flags); + db.StringSet(key, val, expiry: expiresIn, when: when); + db.StringSet(key, val, expiry: expiresIn, when: when, flags: flags); + db.StringSet(key, val, expiry: expiresIn, when: when, flags: flags); + + db.StringSet(key, val, expiresIn, When.NotExists); + db.StringSet(key, val, expiresIn, When.NotExists, flags); + db.StringSet(key, val, null); + db.StringSet(key, val, null, When.NotExists); + db.StringSet(key, val, null, When.NotExists, flags); + + await db.StringSetAsync(key, val); + await db.StringSetAsync(key, val, expiry: expiresIn); + await db.StringSetAsync(key, val, when: when); + await db.StringSetAsync(key, val, flags: flags); + await db.StringSetAsync(key, val, expiry: expiresIn, when: when); + await db.StringSetAsync(key, val, expiry: expiresIn, when: when, flags: flags); + await db.StringSetAsync(key, val, expiry: expiresIn, when: when, flags: flags); + + await db.StringSetAsync(key, val, expiresIn, When.NotExists); + await db.StringSetAsync(key, val, expiresIn, When.NotExists, flags); + await db.StringSetAsync(key, val, null); + await db.StringSetAsync(key, val, null, When.NotExists); + await db.StringSetAsync(key, val, null, When.NotExists, flags); + } +} diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index 6acbb41f9..122185c37 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -1250,6 +1250,14 @@ public void StringSetAsync_3() mock.Verify(_ => _.StringSetAsync(It.Is(valid), When.Exists, CommandFlags.None)); } + [Fact] + public void StringSetAsync_Compat() + { + TimeSpan expiry = TimeSpan.FromSeconds(123); + wrapper.StringSetAsync("key", "value", expiry, When.Exists); + mock.Verify(_ => _.StringSetAsync("prefix:key", "value", expiry, When.Exists)); + } + [Fact] public void StringSetBitAsync() { From d68ec8ea53cd218f777821c30aca726d80c366b3 Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Thu, 28 Apr 2022 16:28:42 +0300 Subject: [PATCH 149/435] Expiry.TestExpiryOptions FAILED (might be flaky) (#2123) https://github.com/StackExchange/StackExchange.Redis/runs/6199961801?check_suite_focus=true Co-authored-by: Nick Craver --- tests/StackExchange.Redis.Tests/Expiry.cs | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/Expiry.cs b/tests/StackExchange.Redis.Tests/Expiry.cs index f29cd617d..c87fb542b 100644 --- a/tests/StackExchange.Redis.Tests/Expiry.cs +++ b/tests/StackExchange.Redis.Tests/Expiry.cs @@ -51,30 +51,30 @@ public async Task TestBasicExpiryTimeSpan(bool disablePTimes) [Theory] [InlineData(true)] [InlineData(false)] - public void TestExpiryOptions(bool disablePTimes) + public async Task TestExpiryOptions(bool disablePTimes) { using var conn = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); var key = Me(); - var cb = conn.GetDatabase(); - cb.KeyDelete(key, CommandFlags.FireAndForget); - cb.StringSet(key, "value", flags: CommandFlags.FireAndForget); + var db = conn.GetDatabase(); + db.KeyDelete(key); + db.StringSet(key, "value"); // The key has no expiry - Assert.False(cb.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasExpiry)); - Assert.True(cb.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasNoExpiry)); + Assert.False(await db.KeyExpireAsync(key, TimeSpan.FromHours(1), ExpireWhen.HasExpiry)); + Assert.True(await db.KeyExpireAsync(key, TimeSpan.FromHours(1), ExpireWhen.HasNoExpiry)); // The key has an existing expiry - Assert.True(cb.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasExpiry)); - Assert.False(cb.KeyExpire(key, TimeSpan.FromHours(1), ExpireWhen.HasNoExpiry)); + Assert.True(await db.KeyExpireAsync(key, TimeSpan.FromHours(1), ExpireWhen.HasExpiry)); + Assert.False(await db.KeyExpireAsync(key, TimeSpan.FromHours(1), ExpireWhen.HasNoExpiry)); // Set only when the new expiry is greater than current one - Assert.True(cb.KeyExpire(key, TimeSpan.FromHours(1.5), ExpireWhen.GreaterThanCurrentExpiry)); - Assert.False(cb.KeyExpire(key, TimeSpan.FromHours(0.5), ExpireWhen.GreaterThanCurrentExpiry)); + Assert.True(await db.KeyExpireAsync(key, TimeSpan.FromHours(1.5), ExpireWhen.GreaterThanCurrentExpiry)); + Assert.False(await db.KeyExpireAsync(key, TimeSpan.FromHours(0.5), ExpireWhen.GreaterThanCurrentExpiry)); // Set only when the new expiry is less than current one - Assert.True(cb.KeyExpire(key, TimeSpan.FromHours(0.5), ExpireWhen.LessThanCurrentExpiry)); - Assert.False(cb.KeyExpire(key, TimeSpan.FromHours(1.5), ExpireWhen.LessThanCurrentExpiry)); + Assert.True(await db.KeyExpireAsync(key, TimeSpan.FromHours(0.5), ExpireWhen.LessThanCurrentExpiry)); + Assert.False(await db.KeyExpireAsync(key, TimeSpan.FromHours(1.5), ExpireWhen.LessThanCurrentExpiry)); } [Theory] From c83443aaaf6497e5175690075a5934aa28c79ee7 Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Thu, 28 Apr 2022 16:51:44 +0300 Subject: [PATCH 150/435] Support BYTE|BIT in BITCOUNT and BITPOS commands (#2116) The new features were added to Redis 7: now the indexes of both commands ([BITCOUNT](https://redis.io/commands/bitcount/) and [BITPOS](https://redis.io/commands/bitpos/)) can be specified in BITs instead of BYTEs. (Also added tests for BitPosition since it was missing) Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + .../Enums/StringIndexType.cs | 28 ++++++ .../Interfaces/IDatabase.cs | 14 ++- .../Interfaces/IDatabaseAsync.cs | 14 ++- .../KeyspaceIsolation/DatabaseWrapper.cs | 10 +- .../KeyspaceIsolation/WrapperBase.cs | 10 +- src/StackExchange.Redis/PublicAPI.Shipped.txt | 15 ++- src/StackExchange.Redis/RedisDatabase.cs | 44 +++++++-- src/StackExchange.Redis/RedisLiterals.cs | 2 + .../DatabaseWrapperTests.cs | 14 +++ .../OverloadCompat.cs | 98 +++++++++++++++++++ tests/StackExchange.Redis.Tests/Strings.cs | 95 ++++++++++++++++-- .../WrapperBaseTests.cs | 14 +++ 13 files changed, 333 insertions(+), 26 deletions(-) create mode 100644 src/StackExchange.Redis/Enums/StringIndexType.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index a3bf5e25f..74accb188 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -30,6 +30,7 @@ - Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105)) - Performance: Avoids allocations when computing cluster hash slots or testing key equality ([#2110 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2110)) - Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111)) +- Adds: Support for `BIT | BYTE` to `BITCOUNT` and `BITPOS` with `.StringBitCount()`/`.StringBitCountAsync()` and `.StringBitPosition()`/`.StringBitPositionAsync()` [#2116 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2116)) - Adds: Support for pub/sub payloads that are unary arrays ([#2118 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2118)) ## 2.5.61 diff --git a/src/StackExchange.Redis/Enums/StringIndexType.cs b/src/StackExchange.Redis/Enums/StringIndexType.cs new file mode 100644 index 000000000..deb180404 --- /dev/null +++ b/src/StackExchange.Redis/Enums/StringIndexType.cs @@ -0,0 +1,28 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// Indicates if we index into a string based on bits or bytes. +/// +public enum StringIndexType +{ + /// + /// Indicates the index is the number of bytes into a string. + /// + Byte, + /// + /// Indicates the index is the number of bits into a string. + /// + Bit, +} + +internal static class StringIndexTypeExtensions +{ + internal static RedisValue ToLiteral(this StringIndexType indexType) => indexType switch + { + StringIndexType.Bit => RedisLiterals.BIT, + StringIndexType.Byte => RedisLiterals.BYTE, + _ => throw new ArgumentOutOfRangeException(nameof(indexType)) + }; +} diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index d80603dee..016469808 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -2526,6 +2526,10 @@ IEnumerable SortedSetScan(RedisKey key, /// long StringAppend(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + long StringBitCount(RedisKey key, long start, long end, CommandFlags flags); + /// /// Count the number of set bits (population counting) in a string. /// By default all the bytes contained in the string are examined. @@ -2535,10 +2539,11 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the string. /// The start byte to count at. /// The end byte to count at. + /// In Redis 7+, we can choose if and specify a bit index or byte index (defaults to ). /// The flags to use for this operation. /// The number of bits set to 1. /// - long StringBitCount(RedisKey key, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None); + long StringBitCount(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None); /// /// Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key. @@ -2568,6 +2573,10 @@ IEnumerable SortedSetScan(RedisKey key, /// long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None); + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + long StringBitPosition(RedisKey key, bool bit, long start, long end, CommandFlags flags); + /// /// Return the position of the first bit set to 1 or 0 in a string. /// The position is returned thinking at the string as an array of bits from left to right where the first byte most significant bit is at position 0, the second byte most significant bit is at position 8 and so forth. @@ -2578,13 +2587,14 @@ IEnumerable SortedSetScan(RedisKey key, /// True to check for the first 1 bit, false to check for the first 0 bit. /// The position to start looking (defaults to 0). /// The position to stop looking (defaults to -1, unlimited). + /// In Redis 7+, we can choose if and specify a bit index or byte index (defaults to ). /// The flags to use for this operation. /// /// The command returns the position of the first bit set to 1 or 0 according to the request. /// If we look for set bits(the bit argument is 1) and the string is empty or composed of just zero bytes, -1 is returned. /// /// - long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None); + long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None); /// /// Decrements the number stored at key by decrement. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index d0fcaa355..08d500ea3 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -2478,6 +2478,10 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// Task StringAppendAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + Task StringBitCountAsync(RedisKey key, long start, long end, CommandFlags flags); + /// /// Count the number of set bits (population counting) in a string. /// By default all the bytes contained in the string are examined. @@ -2487,10 +2491,11 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The key of the string. /// The start byte to count at. /// The end byte to count at. + /// In Redis 7+, we can choose if and specify a bit index or byte index (defaults to ). /// The flags to use for this operation. /// The number of bits set to 1. /// - Task StringBitCountAsync(RedisKey key, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None); + Task StringBitCountAsync(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None); /// /// Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key. @@ -2520,6 +2525,10 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None); + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + Task StringBitPositionAsync(RedisKey key, bool bit, long start, long end, CommandFlags flags); + /// /// Return the position of the first bit set to 1 or 0 in a string. /// The position is returned thinking at the string as an array of bits from left to right where the first byte most significant bit is at position 0, the second byte most significant bit is at position 8 and so forth. @@ -2530,13 +2539,14 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// True to check for the first 1 bit, false to check for the first 0 bit. /// The position to start looking (defaults to 0). /// The position to stop looking (defaults to -1, unlimited). + /// In Redis 7+, we can choose if and specify a bit index or byte index (defaults to ). /// The flags to use for this operation. /// /// The command returns the position of the first bit set to 1 or 0 according to the request. /// If we look for set bits(the bit argument is 1) and the string is empty or composed of just zero bytes, -1 is returned. /// /// - Task StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None); + Task StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None); /// /// Decrements the number stored at key by decrement. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index c2a9fff8a..395bd7010 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -583,18 +583,24 @@ public long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength public long StringAppend(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => Inner.StringAppend(ToInner(key), value, flags); - public long StringBitCount(RedisKey key, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) => + public long StringBitCount(RedisKey key, long start, long end, CommandFlags flags) => Inner.StringBitCount(ToInner(key), start, end, flags); + public long StringBitCount(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => + Inner.StringBitCount(ToInner(key), start, end, indexType, flags); + public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Inner.StringBitOperation(operation, ToInner(destination), ToInner(keys), flags); public long StringBitOperation(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, CommandFlags flags = CommandFlags.None) => Inner.StringBitOperation(operation, ToInner(destination), ToInner(first), ToInnerOrDefault(second), flags); - public long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) => + public long StringBitPosition(RedisKey key, bool bit, long start, long end, CommandFlags flags) => Inner.StringBitPosition(ToInner(key), bit, start, end, flags); + public long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => + Inner.StringBitPosition(ToInner(key), bit, start, end, indexType, flags); + public double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => Inner.StringDecrement(ToInner(key), value, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index c7b0d929f..5d603f663 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -600,18 +600,24 @@ public Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproxima public Task StringAppendAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => Inner.StringAppendAsync(ToInner(key), value, flags); - public Task StringBitCountAsync(RedisKey key, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) => + public Task StringBitCountAsync(RedisKey key, long start, long end, CommandFlags flags) => Inner.StringBitCountAsync(ToInner(key), start, end, flags); + public Task StringBitCountAsync(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => + Inner.StringBitCountAsync(ToInner(key), start, end, indexType, flags); + public Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Inner.StringBitOperationAsync(operation, ToInner(destination), ToInner(keys), flags); public Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, CommandFlags flags = CommandFlags.None) => Inner.StringBitOperationAsync(operation, ToInner(destination), ToInner(first), ToInnerOrDefault(second), flags); - public Task StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) => + public Task StringBitPositionAsync(RedisKey key, bool bit, long start, long end, CommandFlags flags) => Inner.StringBitPositionAsync(ToInner(key), bit, start, end, flags); + public Task StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => + Inner.StringBitPositionAsync(ToInner(key), bit, start, end, indexType, flags); + public Task StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => Inner.StringDecrementAsync(ToInner(key), value, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 290c1a2ca..1c47392db 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -688,10 +688,12 @@ StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.StreamPosition StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.RedisStream[]! StackExchange.Redis.IDatabase.StreamTrim(StackExchange.Redis.RedisKey key, int maxLength, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringAppend(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.StringBitCount(StackExchange.Redis.RedisKey key, long start = 0, long end = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringBitCount(StackExchange.Redis.RedisKey key, long start, long end, StackExchange.Redis.CommandFlags flags) -> long +StackExchange.Redis.IDatabase.StringBitCount(StackExchange.Redis.RedisKey key, long start = 0, long end = -1, StackExchange.Redis.StringIndexType indexType = StackExchange.Redis.StringIndexType.Byte, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringBitOperation(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringBitOperation(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.StringBitPosition(StackExchange.Redis.RedisKey key, bool bit, long start = 0, long end = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringBitPosition(StackExchange.Redis.RedisKey key, bool bit, long start, long end, StackExchange.Redis.CommandFlags flags) -> long +StackExchange.Redis.IDatabase.StringBitPosition(StackExchange.Redis.RedisKey key, bool bit, long start = 0, long end = -1, StackExchange.Redis.StringIndexType indexType = StackExchange.Redis.StringIndexType.Byte, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringDecrement(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double StackExchange.Redis.IDatabase.StringDecrement(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue @@ -906,10 +908,12 @@ StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.Stre StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamTrimAsync(StackExchange.Redis.RedisKey key, int maxLength, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringAppendAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.StringBitCountAsync(StackExchange.Redis.RedisKey key, long start = 0, long end = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringBitCountAsync(StackExchange.Redis.RedisKey key, long start, long end, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringBitCountAsync(StackExchange.Redis.RedisKey key, long start = 0, long end = -1, StackExchange.Redis.StringIndexType indexType = StackExchange.Redis.StringIndexType.Byte, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringBitOperationAsync(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second = default(StackExchange.Redis.RedisKey), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringBitOperationAsync(StackExchange.Redis.Bitwise operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.StringBitPositionAsync(StackExchange.Redis.RedisKey key, bool bit, long start = 0, long end = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringBitPositionAsync(StackExchange.Redis.RedisKey key, bool bit, long start, long end, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringBitPositionAsync(StackExchange.Redis.RedisKey key, bool bit, long start = 0, long end = -1, StackExchange.Redis.StringIndexType indexType = StackExchange.Redis.StringIndexType.Byte, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringDecrementAsync(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringDecrementAsync(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -1473,6 +1477,9 @@ StackExchange.Redis.StreamPosition.Key.get -> StackExchange.Redis.RedisKey StackExchange.Redis.StreamPosition.Position.get -> StackExchange.Redis.RedisValue StackExchange.Redis.StreamPosition.StreamPosition() -> void StackExchange.Redis.StreamPosition.StreamPosition(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue position) -> void +StackExchange.Redis.StringIndexType +StackExchange.Redis.StringIndexType.Byte = 0 -> StackExchange.Redis.StringIndexType +StackExchange.Redis.StringIndexType.Bit = 1 -> StackExchange.Redis.StringIndexType StackExchange.Redis.When StackExchange.Redis.When.Always = 0 -> StackExchange.Redis.When StackExchange.Redis.When.Exists = 1 -> StackExchange.Redis.When diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index d12bd4a0f..2f3766771 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -2788,15 +2788,29 @@ public Task StringAppendAsync(RedisKey key, RedisValue value, CommandFlags return ExecuteAsync(msg, ResultProcessor.Int64); } - public long StringBitCount(RedisKey key, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) + public long StringBitCount(RedisKey key, long start, long end, CommandFlags flags) => + StringBitCount(key, start, end, StringIndexType.Byte, flags); + + public long StringBitCount(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, flags, RedisCommand.BITCOUNT, key, start, end); + var msg = indexType switch + { + StringIndexType.Byte => Message.Create(Database, flags, RedisCommand.BITCOUNT, key, start, end), + _ => Message.Create(Database, flags, RedisCommand.BITCOUNT, key, start, end, indexType.ToLiteral()), + }; return ExecuteSync(msg, ResultProcessor.Int64); } - public Task StringBitCountAsync(RedisKey key, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) + public Task StringBitCountAsync(RedisKey key, long start, long end, CommandFlags flags) => + StringBitCountAsync(key, start, end, StringIndexType.Byte, flags); + + public Task StringBitCountAsync(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, flags, RedisCommand.BITCOUNT, key, start, end); + var msg = indexType switch + { + StringIndexType.Byte => Message.Create(Database, flags, RedisCommand.BITCOUNT, key, start, end), + _ => Message.Create(Database, flags, RedisCommand.BITCOUNT, key, start, end, indexType.ToLiteral()), + }; return ExecuteAsync(msg, ResultProcessor.Int64); } @@ -2824,15 +2838,29 @@ public Task StringBitOperationAsync(Bitwise operation, RedisKey destinatio return ExecuteAsync(msg, ResultProcessor.Int64); } - public long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) + public long StringBitPosition(RedisKey key, bool bit, long start, long end, CommandFlags flags) => + StringBitPosition(key, bit, start, end, StringIndexType.Byte, flags); + + public long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, flags, RedisCommand.BITPOS, key, bit, start, end); + var msg = indexType switch + { + StringIndexType.Byte => Message.Create(Database, flags, RedisCommand.BITPOS, key, bit, start, end), + _ => Message.Create(Database, flags, RedisCommand.BITPOS, key, bit, start, end, indexType.ToLiteral()), + }; return ExecuteSync(msg, ResultProcessor.Int64); } - public Task StringBitPositionAsync(RedisKey key, bool value, long start = 0, long end = -1, CommandFlags flags = CommandFlags.None) + public Task StringBitPositionAsync(RedisKey key, bool bit, long start, long end, CommandFlags flags) => + StringBitPositionAsync(key, bit, start, end, StringIndexType.Byte, flags); + + public Task StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, flags, RedisCommand.BITPOS, key, value, start, end); + var msg = indexType switch + { + StringIndexType.Byte => Message.Create(Database, flags, RedisCommand.BITPOS, key, bit, start, end), + _ => Message.Create(Database, flags, RedisCommand.BITPOS, key, bit, start, end, indexType.ToLiteral()), + }; return ExecuteAsync(msg, ResultProcessor.Int64); } diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 44d4c60ac..295b0ef04 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -49,9 +49,11 @@ public static readonly RedisValue ANY = "ANY", ASC = "ASC", BEFORE = "BEFORE", + BIT = "BIT", BY = "BY", BYLEX = "BYLEX", BYSCORE = "BYSCORE", + BYTE = "BYTE", CHANNELS = "CHANNELS", COPY = "COPY", COUNT = "COUNT", diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 87b12949e..95c11fad2 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -1183,6 +1183,13 @@ public void StringBitCount() mock.Verify(_ => _.StringBitCount("prefix:key", 123, 456, CommandFlags.None)); } + [Fact] + public void StringBitCount_2() + { + wrapper.StringBitCount("key", 123, 456, StringIndexType.Byte, CommandFlags.None); + mock.Verify(_ => _.StringBitCount("prefix:key", 123, 456, StringIndexType.Byte, CommandFlags.None)); + } + [Fact] public void StringBitOperation_1() { @@ -1206,6 +1213,13 @@ public void StringBitPosition() mock.Verify(_ => _.StringBitPosition("prefix:key", true, 123, 456, CommandFlags.None)); } + [Fact] + public void StringBitPosition_2() + { + wrapper.StringBitPosition("key", true, 123, 456, StringIndexType.Byte, CommandFlags.None); + mock.Verify(_ => _.StringBitPosition("prefix:key", true, 123, 456, StringIndexType.Byte, CommandFlags.None)); + } + [Fact] public void StringDecrement_1() { diff --git a/tests/StackExchange.Redis.Tests/OverloadCompat.cs b/tests/StackExchange.Redis.Tests/OverloadCompat.cs index ffe8958c2..7f9b4b98a 100644 --- a/tests/StackExchange.Redis.Tests/OverloadCompat.cs +++ b/tests/StackExchange.Redis.Tests/OverloadCompat.cs @@ -14,6 +14,102 @@ public class OverloadCompat : TestBase { public OverloadCompat(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + [Fact] + public async Task StringBitCount() + { + using var conn = Create(require: RedisFeatures.v2_6_0); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, flags: CommandFlags.FireAndForget); + db.StringSet(key, "foobar", flags: CommandFlags.FireAndForget); + + db.StringBitCount(key); + db.StringBitCount(key, 1); + db.StringBitCount(key, 0, 0); + db.StringBitCount(key, start: 1); + db.StringBitCount(key, end: 1); + db.StringBitCount(key, start: 1, end: 1); + + var flags = CommandFlags.None; + db.StringBitCount(key, flags: flags); + db.StringBitCount(key, 0, 0, flags); + db.StringBitCount(key, 1, flags: flags); + db.StringBitCount(key, 1, 1, flags: flags); + db.StringBitCount(key, start: 1, flags: flags); + db.StringBitCount(key, end: 1, flags: flags); + db.StringBitCount(key, start: 1, end: 1, flags); + db.StringBitCount(key, start: 1, end: 1, flags: flags); + + // Async + + await db.StringBitCountAsync(key); + await db.StringBitCountAsync(key, 1); + await db.StringBitCountAsync(key, 0, 0); + await db.StringBitCountAsync(key, start: 1); + await db.StringBitCountAsync(key, end: 1); + await db.StringBitCountAsync(key, start: 1, end: 1); + + await db.StringBitCountAsync(key, flags: flags); + await db.StringBitCountAsync(key, 0, 0, flags); + await db.StringBitCountAsync(key, 1, flags: flags); + await db.StringBitCountAsync(key, 1, 1, flags: flags); + await db.StringBitCountAsync(key, start: 1, flags: flags); + await db.StringBitCountAsync(key, end: 1, flags: flags); + await db.StringBitCountAsync(key, start: 1, end: 1, flags); + await db.StringBitCountAsync(key, start: 1, end: 1, flags: flags); + } + + [Fact] + public async Task StringBitPosition() + { + using var conn = Create(require: RedisFeatures.v2_6_0); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, flags: CommandFlags.FireAndForget); + db.StringSet(key, "foo", flags: CommandFlags.FireAndForget); + + db.StringBitPosition(key, true); + db.StringBitPosition(key, true, 1); + db.StringBitPosition(key, true, 1, 3); + db.StringBitPosition(key, bit: true); + db.StringBitPosition(key, bit: true, start: 1); + db.StringBitPosition(key, bit: true, end: 1); + db.StringBitPosition(key, bit: true, start: 1, end: 1); + db.StringBitPosition(key, true, start: 1, end: 1); + + var flags = CommandFlags.None; + db.StringBitPosition(key, true, flags: flags); + db.StringBitPosition(key, true, 1, 3, flags); + db.StringBitPosition(key, true, 1, flags: flags); + db.StringBitPosition(key, bit: true, flags: flags); + db.StringBitPosition(key, bit: true, start: 1, flags: flags); + db.StringBitPosition(key, bit: true, end: 1, flags: flags); + db.StringBitPosition(key, bit: true, start: 1, end: 1, flags: flags); + db.StringBitPosition(key, true, start: 1, end: 1, flags: flags); + + // Async + + await db.StringBitPositionAsync(key, true); + await db.StringBitPositionAsync(key, true, 1); + await db.StringBitPositionAsync(key, true, 1, 3); + await db.StringBitPositionAsync(key, bit: true); + await db.StringBitPositionAsync(key, bit: true, start: 1); + await db.StringBitPositionAsync(key, bit: true, end: 1); + await db.StringBitPositionAsync(key, bit: true, start: 1, end: 1); + await db.StringBitPositionAsync(key, true, start: 1, end: 1); + + await db.StringBitPositionAsync(key, true, flags: flags); + await db.StringBitPositionAsync(key, true, 1, 3, flags); + await db.StringBitPositionAsync(key, true, 1, flags: flags); + await db.StringBitPositionAsync(key, bit: true, flags: flags); + await db.StringBitPositionAsync(key, bit: true, start: 1, flags: flags); + await db.StringBitPositionAsync(key, bit: true, end: 1, flags: flags); + await db.StringBitPositionAsync(key, bit: true, start: 1, end: 1, flags: flags); + await db.StringBitPositionAsync(key, true, start: 1, end: 1, flags: flags); + } + [Fact] public async Task StringGet() { @@ -39,6 +135,8 @@ public async Task StringGet() db.StringSet(key, val, null, When.NotExists); db.StringSet(key, val, null, When.NotExists, flags); + // Async + await db.StringSetAsync(key, val); await db.StringSetAsync(key, val, expiry: expiresIn); await db.StringSetAsync(key, val, when: when); diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index 2a095ab59..7fe1be74a 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -496,14 +496,51 @@ public async Task BitCount() var db = conn.GetDatabase(); var key = Me(); + db.KeyDelete(key, flags: CommandFlags.FireAndForget); db.StringSet(key, "foobar", flags: CommandFlags.FireAndForget); - var r1 = db.StringBitCountAsync(key); - var r2 = db.StringBitCountAsync(key, 0, 0); - var r3 = db.StringBitCountAsync(key, 1, 1); - Assert.Equal(26, await r1); - Assert.Equal(4, await r2); - Assert.Equal(6, await r3); + var r1 = db.StringBitCount(key); + var r2 = db.StringBitCount(key, 0, 0); + var r3 = db.StringBitCount(key, 1, 1); + + Assert.Equal(26, r1); + Assert.Equal(4, r2); + Assert.Equal(6, r3); + + // Async + + r1 = await db.StringBitCountAsync(key); + r2 = await db.StringBitCountAsync(key, 0, 0); + r3 = await db.StringBitCountAsync(key, 1, 1); + + Assert.Equal(26, r1); + Assert.Equal(4, r2); + Assert.Equal(6, r3); + } + + [Fact] + public async Task BitCountWithBitUnit() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, flags: CommandFlags.FireAndForget); + db.StringSet(key, "foobar", flags: CommandFlags.FireAndForget); + + var r1 = db.StringBitCount(key, 1, 1); // Using default byte + var r2 = db.StringBitCount(key, 1, 1, StringIndexType.Bit); + + Assert.Equal(6, r1); + Assert.Equal(1, r2); + + // Async + + r1 = await db.StringBitCountAsync(key, 1, 1); // Using default byte + r2 = await db.StringBitCountAsync(key, 1, 1, StringIndexType.Bit); + + Assert.Equal(6, r1); + Assert.Equal(1, r2); } [Fact] @@ -541,6 +578,52 @@ public async Task BitOp() Assert.Equal(unchecked((byte)(~3)), r_not); } + [Fact] + public async Task BitPosition() + { + using var conn = Create(require: RedisFeatures.v2_6_0); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, flags: CommandFlags.FireAndForget); + db.StringSet(key, "foo", flags: CommandFlags.FireAndForget); + + var r1 = db.StringBitPosition(key, true); + var r2 = db.StringBitPosition(key, true, 10, 10); + var r3 = db.StringBitPosition(key, true, 1, 3); + + Assert.Equal(1, r1); + Assert.Equal(-1, r2); + Assert.Equal(9, r3); + + // Async + + r1 = await db.StringBitPositionAsync(key, true); + r2 = await db.StringBitPositionAsync(key, true, 10, 10); + r3 = await db.StringBitPositionAsync(key, true, 1, 3); + + Assert.Equal(1, r1); + Assert.Equal(-1, r2); + Assert.Equal(9, r3); + } + + [Fact] + public async Task BitPositionWithBitUnit() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, flags: CommandFlags.FireAndForget); + db.StringSet(key, "foo", flags: CommandFlags.FireAndForget); + + var r1 = db.StringBitPositionAsync(key, true, 1, 3); // Using default byte + var r2 = db.StringBitPositionAsync(key, true, 1, 3, StringIndexType.Bit); + + Assert.Equal(9, await r1); + Assert.Equal(1, await r2); + } + [Fact] public async Task RangeString() { diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index 122185c37..d63921d70 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -1116,6 +1116,13 @@ public void StringBitCountAsync() mock.Verify(_ => _.StringBitCountAsync("prefix:key", 123, 456, CommandFlags.None)); } + [Fact] + public void StringBitCountAsync_2() + { + wrapper.StringBitCountAsync("key", 123, 456, StringIndexType.Byte, CommandFlags.None); + mock.Verify(_ => _.StringBitCountAsync("prefix:key", 123, 456, StringIndexType.Byte, CommandFlags.None)); + } + [Fact] public void StringBitOperationAsync_1() { @@ -1139,6 +1146,13 @@ public void StringBitPositionAsync() mock.Verify(_ => _.StringBitPositionAsync("prefix:key", true, 123, 456, CommandFlags.None)); } + [Fact] + public void StringBitPositionAsync_2() + { + wrapper.StringBitPositionAsync("key", true, 123, 456, StringIndexType.Byte, CommandFlags.None); + mock.Verify(_ => _.StringBitPositionAsync("prefix:key", true, 123, 456, StringIndexType.Byte, CommandFlags.None)); + } + [Fact] public void StringDecrementAsync_1() { From 90dff5cd9f5b904b2c2e7cb2407b1e0882f03806 Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Tue, 10 May 2022 18:48:18 +0300 Subject: [PATCH 151/435] Overload old versions of KeyExpire and KeyExpireAsync (#2124) Continue #2083 Now when we can overload function - we can do it also to `KeyExpire` and `KeyExpireAsync` --- .../Interfaces/IDatabase.cs | 14 ++--- .../Interfaces/IDatabaseAsync.cs | 14 ++--- .../KeyspaceIsolation/DatabaseWrapper.cs | 8 +-- .../KeyspaceIsolation/WrapperBase.cs | 8 +-- src/StackExchange.Redis/PublicAPI.Shipped.txt | 16 +++--- src/StackExchange.Redis/RedisDatabase.cs | 16 +++--- .../OverloadCompat.cs | 51 +++++++++++++++++-- 7 files changed, 88 insertions(+), 39 deletions(-) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 016469808..9d8cd53f6 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -681,7 +681,8 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// /// - bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags); /// /// Set a timeout on . @@ -690,14 +691,14 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The key to set the expiration for. /// The timeout to set. - /// Since Redis 7.0.0, you can choose under which condition the expiration will be set using . + /// In Redis 7+, we can choose under which condition the expiration will be set using . /// The flags to use for this operation. /// if the timeout was set. if key does not exist or the timeout could not be set. /// /// , /// /// - bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None); + bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); /// /// Set a timeout on . @@ -725,7 +726,8 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// /// - bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None); + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags); /// /// Set a timeout on . @@ -734,14 +736,14 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The key to set the expiration for. /// The timeout to set. - /// Since Redis 7.0.0, you can choose under which condition the expiration will be set using . + /// In Redis 7+, we choose under which condition the expiration will be set using . /// The flags to use for this operation. /// if the timeout was set. if key does not exist or the timeout could not be set. /// /// , /// /// - bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None); + bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); /// /// Returns the absolute time at which the given will expire, if it exists and has an expiration. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 08d500ea3..fd56deca1 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -657,7 +657,8 @@ public interface IDatabaseAsync : IRedisAsync /// /// /// - Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags); /// /// Set a timeout on . @@ -666,14 +667,14 @@ public interface IDatabaseAsync : IRedisAsync /// /// The key to set the expiration for. /// The timeout to set. - /// Since Redis 7.0.0, you can choose under which condition the expiration will be set using . + /// In Redis 7+, we choose under which condition the expiration will be set using . /// The flags to use for this operation. /// if the timeout was set. if key does not exist or the timeout could not be set. /// /// , /// /// - Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None); + Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); /// /// Set a timeout on . @@ -701,7 +702,8 @@ public interface IDatabaseAsync : IRedisAsync /// /// /// - Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None); + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags); /// /// Set a timeout on . @@ -710,14 +712,14 @@ public interface IDatabaseAsync : IRedisAsync /// /// The key to set the expiration for. /// The timeout to set. - /// Since Redis 7.0.0, you can choose under which condition the expiration will be set using . + /// In Redis 7+, we choose under which condition the expiration will be set using . /// The flags to use for this operation. /// if the timeout was set. if key does not exist or the timeout could not be set. /// /// , /// /// - Task KeyExpireAsync(RedisKey key, DateTime? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None); + Task KeyExpireAsync(RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); /// /// Returns the absolute time at which the given will expire, if it exists and has an expiration. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 395bd7010..1671c77a8 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -167,16 +167,16 @@ public bool KeyExists(RedisKey key, CommandFlags flags = CommandFlags.None) => public long KeyExists(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Inner.KeyExists(ToInner(keys), flags); - public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) => + public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags) => Inner.KeyExpire(ToInner(key), expiry, flags); - public bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) => + public bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => Inner.KeyExpire(ToInner(key), expiry, when, flags); - public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => + public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags) => Inner.KeyExpire(ToInner(key), expiry, flags); - public bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) => + public bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => Inner.KeyExpire(ToInner(key), expiry, when, flags); public DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None) => diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 5d603f663..250b00a38 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -178,16 +178,16 @@ public Task KeyExistsAsync(RedisKey key, CommandFlags flags = CommandFlags public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None) => Inner.KeyExistsAsync(ToInner(keys), flags); - public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) => + public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags) => Inner.KeyExpireAsync(ToInner(key), expiry, flags); - public Task KeyExpireAsync(RedisKey key, DateTime? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) => + public Task KeyExpireAsync(RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => Inner.KeyExpireAsync(ToInner(key), expiry, when, flags); - public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags) => Inner.KeyExpireAsync(ToInner(key), expiry, flags); - public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) => + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => Inner.KeyExpireAsync(ToInner(key), expiry, when, flags); public Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 1c47392db..33bb46777 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -550,10 +550,10 @@ StackExchange.Redis.IDatabase.KeyDump(StackExchange.Redis.RedisKey key, StackExc StackExchange.Redis.IDatabase.KeyEncoding(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? StackExchange.Redis.IDatabase.KeyExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyExists(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.ExpireWhen when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.ExpireWhen when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.CommandFlags flags) -> bool +StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.ExpireWhen when = StackExchange.Redis.ExpireWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags) -> bool +StackExchange.Redis.IDatabase.KeyExpire(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.ExpireWhen when = StackExchange.Redis.ExpireWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.KeyExpireTime(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.DateTime? StackExchange.Redis.IDatabase.KeyFrequency(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long? StackExchange.Redis.IDatabase.KeyIdleTime(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.TimeSpan? @@ -772,10 +772,10 @@ StackExchange.Redis.IDatabaseAsync.KeyDumpAsync(StackExchange.Redis.RedisKey key StackExchange.Redis.IDatabaseAsync.KeyEncodingAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyExistsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyExistsAsync(StackExchange.Redis.RedisKey[]! keys, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.ExpireWhen when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.ExpireWhen when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.DateTime? expiry, StackExchange.Redis.ExpireWhen when = StackExchange.Redis.ExpireWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.KeyExpireAsync(StackExchange.Redis.RedisKey key, System.TimeSpan? expiry, StackExchange.Redis.ExpireWhen when = StackExchange.Redis.ExpireWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyExpireTimeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyFrequencyAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.KeyIdleTimeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 2f3766771..9b699d10c 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -829,37 +829,37 @@ public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFl return ExecuteAsync(msg, ResultProcessor.Int64); } - public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => + public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags) => KeyExpire(key, expiry, ExpireWhen.Always, flags); - public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) => + public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags) => KeyExpire(key, expiry, ExpireWhen.Always, flags); - public bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) + public bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) { var msg = GetExpiryMessage(key, flags, expiry, when, out ServerEndPoint? server); return ExecuteSync(msg, ResultProcessor.Boolean, server: server); } - public bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) + public bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) { var msg = GetExpiryMessage(key, flags, expiry, when, out ServerEndPoint? server); return ExecuteSync(msg, ResultProcessor.Boolean, server: server); } - public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags) => KeyExpireAsync(key, expiry, ExpireWhen.Always, flags); - public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) => + public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags) => KeyExpireAsync(key, expiry, ExpireWhen.Always, flags); - public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) { var msg = GetExpiryMessage(key, flags, expiry, when, out ServerEndPoint? server); return ExecuteAsync(msg, ResultProcessor.Boolean, server: server); } - public Task KeyExpireAsync(RedisKey key, DateTime? expire, ExpireWhen when, CommandFlags flags = CommandFlags.None) + public Task KeyExpireAsync(RedisKey key, DateTime? expire, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) { var msg = GetExpiryMessage(key, flags, expire, when, out ServerEndPoint? server); return ExecuteAsync(msg, ResultProcessor.Boolean, server: server); diff --git a/tests/StackExchange.Redis.Tests/OverloadCompat.cs b/tests/StackExchange.Redis.Tests/OverloadCompat.cs index 7f9b4b98a..81d8bad82 100644 --- a/tests/StackExchange.Redis.Tests/OverloadCompat.cs +++ b/tests/StackExchange.Redis.Tests/OverloadCompat.cs @@ -15,6 +15,51 @@ public class OverloadCompat : TestBase public OverloadCompat(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } [Fact] + public async Task KeyExpire() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var key = Me(); + var expiresIn = TimeSpan.FromSeconds(10); + var expireTime = DateTime.UtcNow.AddHours(1); + var when = ExpireWhen.Always; + var flags = CommandFlags.None; + + db.KeyExpire(key, expiresIn); + db.KeyExpire(key, expiresIn, when); + db.KeyExpire(key, expiresIn, when: when); + db.KeyExpire(key, expiresIn, flags); + db.KeyExpire(key, expiresIn, flags: flags); + db.KeyExpire(key, expiresIn, when, flags); + db.KeyExpire(key, expiresIn, when: when, flags: flags); + + db.KeyExpire(key, expireTime); + db.KeyExpire(key, expireTime, when); + db.KeyExpire(key, expireTime, when: when); + db.KeyExpire(key, expireTime, flags); + db.KeyExpire(key, expireTime, flags: flags); + db.KeyExpire(key, expireTime, when, flags); + db.KeyExpire(key, expireTime, when: when, flags: flags); + + // Async + + await db.KeyExpireAsync(key, expiresIn); + await db.KeyExpireAsync(key, expiresIn, when); + await db.KeyExpireAsync(key, expiresIn, when: when); + await db.KeyExpireAsync(key, expiresIn, flags); + await db.KeyExpireAsync(key, expiresIn, flags: flags); + await db.KeyExpireAsync(key, expiresIn, when, flags); + await db.KeyExpireAsync(key, expiresIn, when: when, flags: flags); + + await db.KeyExpireAsync(key, expireTime); + await db.KeyExpireAsync(key, expireTime, when); + await db.KeyExpireAsync(key, expireTime, when: when); + await db.KeyExpireAsync(key, expireTime, flags); + await db.KeyExpireAsync(key, expireTime, flags: flags); + await db.KeyExpireAsync(key, expireTime, when, flags); + await db.KeyExpireAsync(key, expireTime, when: when, flags: flags); + } + public async Task StringBitCount() { using var conn = Create(require: RedisFeatures.v2_6_0); @@ -111,12 +156,12 @@ public async Task StringBitPosition() } [Fact] - public async Task StringGet() + public async Task StringSet() { using var conn = Create(); var db = conn.GetDatabase(); - RedisKey key = Me(); - RedisValue val = "myval"; + var key = Me(); + var val = "myval"; var expiresIn = TimeSpan.FromSeconds(10); var when = When.Always; var flags = CommandFlags.None; From dc9a9b60af3c3610eaff5a16ed299a5a0566cf3d Mon Sep 17 00:00:00 2001 From: Erik Wisuri Date: Tue, 10 May 2022 12:54:32 -0500 Subject: [PATCH 152/435] Fix race condition in Sentinel connection multiplexer (#2133) In the Sentinel Connection Multiplexer, since `OnManagedConnectionRestored` can be executed on a different thread, it's possible for it to dispose the timer just before `OnManagedConnectionFailed` calls `.Change()` on the timer, which leads to an `ObjectDisposedException` being thrown in `OnManagedConnectionFailed`. Since the connection was restored, it seems safe to suppress these exceptions, instead of requiring locking around the reference to `connection.sentinelPrimaryReconnectTimer`. --- docs/ReleaseNotes.md | 1 + .../ConnectionMultiplexer.Sentinel.cs | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 74accb188..b0a2b79bd 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -32,6 +32,7 @@ - Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111)) - Adds: Support for `BIT | BYTE` to `BITCOUNT` and `BITPOS` with `.StringBitCount()`/`.StringBitCountAsync()` and `.StringBitPosition()`/`.StringBitPositionAsync()` [#2116 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2116)) - Adds: Support for pub/sub payloads that are unary arrays ([#2118 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2118)) +- Fix: Sentinel timer race during dispose ([#2133 by ewisuri](https://github.com/StackExchange/StackExchange.Redis/pull/2133)) ## 2.5.61 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs index f1c50a56a..05305cc47 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs @@ -331,7 +331,15 @@ internal void OnManagedConnectionFailed(object? sender, ConnectionFailedEventArg } finally { - connection.sentinelPrimaryReconnectTimer?.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan); + try + { + connection.sentinelPrimaryReconnectTimer?.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan); + } + catch (ObjectDisposedException) + { + // If we get here the managed connection was restored and the timer was + // disposed by another thread, so there's no need to run the timer again. + } } }, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan); } From 116132cae68023ff3693fb6649ae423939af4587 Mon Sep 17 00:00:00 2001 From: Philo Date: Sun, 15 May 2022 17:48:10 -0700 Subject: [PATCH 153/435] Update IncludePerformanceCountersInExceptions refs to new location (#2141) --- src/StackExchange.Redis/ExceptionFactory.cs | 4 ++-- src/StackExchange.Redis/ResultProcessor.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 2c0f2b5ca..4c3a95b2a 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -154,7 +154,7 @@ internal static Exception NoConnectionAvailable( if (multiplexer.RawConfig.IncludeDetailInExceptions) { CopyDataToException(data, ex); - sb.Append("; ").Append(PerfCounterHelper.GetThreadPoolAndCPUSummary(multiplexer.IncludePerformanceCountersInExceptions)); + sb.Append("; ").Append(PerfCounterHelper.GetThreadPoolAndCPUSummary(multiplexer.RawConfig.IncludePerformanceCountersInExceptions)); AddExceptionDetail(ex, message, server, commandLabel); } return ex; @@ -349,7 +349,7 @@ private static void AddCommonDetail( } data.Add(Tuple.Create("Busy-Workers", busyWorkerCount.ToString())); - if (multiplexer.IncludePerformanceCountersInExceptions) + if (multiplexer.RawConfig.IncludePerformanceCountersInExceptions) { Add(data, sb, "Local-CPU", "Local-CPU", PerfCounterHelper.GetSystemCpuPercent()); } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 9b3fce521..e7afa7cff 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -260,7 +260,7 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in { unableToConnectError = true; err = $"Endpoint {endpoint} serving hashslot {hashSlot} is not reachable at this point of time. Please check connectTimeout value. If it is low, try increasing it to give the ConnectionMultiplexer a chance to recover from the network disconnect. " - + PerfCounterHelper.GetThreadPoolAndCPUSummary(bridge.Multiplexer.IncludePerformanceCountersInExceptions); + + PerfCounterHelper.GetThreadPoolAndCPUSummary(bridge.Multiplexer.RawConfig.IncludePerformanceCountersInExceptions); } } } From 09a067c2ee30d52fdfb477d9af7b6bb5d77f84f0 Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Mon, 16 May 2022 03:55:14 +0300 Subject: [PATCH 154/435] Support LCS (#2104) Adds support for https://redis.io/commands/lcs/ (#2055) Co-authored-by: slorello89 <42971704+slorello89@users.noreply.github.com> Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + .../APITypes/LCSMatchResult.cs | 67 +++++++++++++ src/StackExchange.Redis/Enums/RedisCommand.cs | 2 + .../Interfaces/IDatabase.cs | 40 ++++++++ .../Interfaces/IDatabaseAsync.cs | 40 ++++++++ .../KeyspaceIsolation/DatabaseWrapper.cs | 9 ++ .../KeyspaceIsolation/WrapperBase.cs | 9 ++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 31 ++++-- src/StackExchange.Redis/RedisDatabase.cs | 37 +++++++ src/StackExchange.Redis/RedisLiterals.cs | 4 + src/StackExchange.Redis/ResultProcessor.cs | 52 ++++++++++ tests/StackExchange.Redis.Tests/Strings.cs | 97 +++++++++++++++++-- 12 files changed, 373 insertions(+), 16 deletions(-) create mode 100644 src/StackExchange.Redis/APITypes/LCSMatchResult.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b0a2b79bd..001c8104f 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -27,6 +27,7 @@ - Adds: Support for `ZMPOP` with `.SortedSetPop()`/`.SortedSetPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094)) - Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2095)) - Fix [#2071](https://github.com/StackExchange/StackExchange.Redis/issues/2071): Add `.StringSet()`/`.StringSetAsync()` overloads for source compat broken for 1 case in 2.5.61 ([#2098 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2098)) +- Adds: Support for `LCS` with `.StringLongestCommonSubsequence()`/`.StringLongestCommonSubsequence()`, `.StringLongestCommonSubsequenceLength()`/`.StringLongestCommonSubsequenceLengthAsync()`, and `.StringLongestCommonSubsequenceWithMatches()`/`.StringLongestCommonSubsequenceWithMatchesAsync()` ([#2104 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2104)) - Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105)) - Performance: Avoids allocations when computing cluster hash slots or testing key equality ([#2110 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2110)) - Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111)) diff --git a/src/StackExchange.Redis/APITypes/LCSMatchResult.cs b/src/StackExchange.Redis/APITypes/LCSMatchResult.cs new file mode 100644 index 000000000..97ff8c1cd --- /dev/null +++ b/src/StackExchange.Redis/APITypes/LCSMatchResult.cs @@ -0,0 +1,67 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// The result of a LongestCommonSubsequence command with IDX feature. +/// Returns a list of the positions of each sub-match. +/// +public readonly struct LCSMatchResult +{ + internal static LCSMatchResult Null { get; } = new LCSMatchResult(Array.Empty(), 0); + + /// + /// The matched positions of all the sub-matched strings. + /// + public LCSMatch[] Matches { get; } + + /// + /// The length of the longest match. + /// + public long LongestMatchLength { get; } + + /// + /// Returns a new . + /// + /// The matched positions in each string. + /// The length of the match. + internal LCSMatchResult(LCSMatch[] matches, long matchLength) + { + Matches = matches; + LongestMatchLength = matchLength; + } + + /// + /// Represents a sub-match of the longest match. i.e first indexes the matched substring in each string. + /// + public readonly struct LCSMatch + { + /// + /// The first index of the matched substring in the first string. + /// + public long FirstStringIndex { get; } + + /// + /// The first index of the matched substring in the second string. + /// + public long SecondStringIndex { get; } + + /// + /// The length of the match. + /// + public long Length { get; } + + /// + /// Returns a new Match. + /// + /// The first index of the matched substring in the first string. + /// The first index of the matched substring in the second string. + /// The length of the match. + internal LCSMatch(long firstStringIndex, long secondStringIndex, long length) + { + FirstStringIndex = firstStringIndex; + SecondStringIndex = secondStringIndex; + Length = length; + } + } +} diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 786316303..8587ceedf 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -86,6 +86,7 @@ internal enum RedisCommand LASTSAVE, LATENCY, + LCS, LINDEX, LINSERT, LLEN, @@ -393,6 +394,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.KEYS: case RedisCommand.LASTSAVE: case RedisCommand.LATENCY: + case RedisCommand.LCS: case RedisCommand.LINDEX: case RedisCommand.LLEN: case RedisCommand.LPOS: diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 9d8cd53f6..ee4dc0c4b 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -2771,6 +2771,46 @@ IEnumerable SortedSetScan(RedisKey key, /// long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Implements the longest common subsequence algorithm between the values at and , + /// returning a string containing the common sequence. + /// Note that this is different than the longest common string algorithm, + /// since matching characters in the string does not need to be contiguous. + /// + /// The key of the first string. + /// The key of the second string. + /// The flags to use for this operation. + /// A string (sequence of characters) of the LCS match. + /// + string? StringLongestCommonSubsequence(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); + + /// + /// Implements the longest common subsequence algorithm between the values at and , + /// returning the legnth of the common sequence. + /// Note that this is different than the longest common string algorithm, + /// since matching characters in the string does not need to be contiguous. + /// + /// The key of the first string. + /// The key of the second string. + /// The flags to use for this operation. + /// The length of the LCS match. + /// + long StringLongestCommonSubsequenceLength(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); + + /// + /// Implements the longest common subsequence algorithm between the values at and , + /// returning a list of all common sequences. + /// Note that this is different than the longest common string algorithm, + /// since matching characters in the string does not need to be contiguous. + /// + /// The key of the first string. + /// The key of the second string. + /// Can be used to restrict the list of matches to the ones of a given minimum length. + /// The flags to use for this operation. + /// The result of LCS algorithm, based on the given parameters. + /// + LCSMatchResult StringLongestCommonSubsequenceWithMatches(RedisKey first, RedisKey second, long minLength = 0, CommandFlags flags = CommandFlags.None); + /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when); diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index fd56deca1..b3950743e 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -2723,6 +2723,46 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// + /// Implements the longest common subsequence algorithm between the values at and , + /// returning a string containing the common sequence. + /// Note that this is different than the longest common string algorithm, + /// since matching characters in the string does not need to be contiguous. + /// + /// The key of the first string. + /// The key of the second string. + /// The flags to use for this operation. + /// A string (sequence of characters) of the LCS match. + /// + Task StringLongestCommonSubsequenceAsync(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); + + /// + /// Implements the longest common subsequence algorithm between the values at and , + /// returning the legnth of the common sequence. + /// Note that this is different than the longest common string algorithm, + /// since matching characters in the string does not need to be contiguous. + /// + /// The key of the first string. + /// The key of the second string. + /// The flags to use for this operation. + /// The length of the LCS match. + /// + Task StringLongestCommonSubsequenceLengthAsync(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); + + /// + /// Implements the longest common subsequence algorithm between the values at and , + /// returning a list of all common sequences. + /// Note that this is different than the longest common string algorithm, + /// since matching characters in the string does not need to be contiguous. + /// + /// The key of the first string. + /// The key of the second string. + /// Can be used to restrict the list of matches to the ones of a given minimum length. + /// The flags to use for this operation. + /// The result of LCS algorithm, based on the given parameters. + /// + Task StringLongestCommonSubsequenceWithMatchesAsync(RedisKey first, RedisKey second, long minLength = 0, CommandFlags flags = CommandFlags.None); + /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 1671c77a8..6fa9bfbc0 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -299,6 +299,15 @@ public bool LockRelease(RedisKey key, RedisValue value, CommandFlags flags = Com public bool LockTake(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => Inner.LockTake(ToInner(key), value, expiry, flags); + public string? StringLongestCommonSubsequence(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + Inner.StringLongestCommonSubsequence(ToInner(first), ToInner(second), flags); + + public long StringLongestCommonSubsequenceLength(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + Inner.StringLongestCommonSubsequenceLength(ToInner(first), ToInner(second), flags); + + public LCSMatchResult StringLongestCommonSubsequenceWithMatches(RedisKey first, RedisKey second, long minLength = 0, CommandFlags flags = CommandFlags.None) => + Inner.StringLongestCommonSubsequenceWithMatches(ToInner(first), ToInner(second), minLength, flags); + public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) => Inner.Publish(ToInner(channel), message, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index 250b00a38..c604a21c9 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -310,6 +310,15 @@ public Task LockReleaseAsync(RedisKey key, RedisValue value, CommandFlags public Task LockTakeAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) => Inner.LockTakeAsync(ToInner(key), value, expiry, flags); + public Task StringLongestCommonSubsequenceAsync(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + Inner.StringLongestCommonSubsequenceAsync(ToInner(first), ToInner(second), flags); + + public Task StringLongestCommonSubsequenceLengthAsync(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None) => + Inner.StringLongestCommonSubsequenceLengthAsync(ToInner(first), ToInner(second), flags); + + public Task StringLongestCommonSubsequenceWithMatchesAsync(RedisKey first, RedisKey second, long minLength = 0, CommandFlags flags = CommandFlags.None) => + Inner.StringLongestCommonSubsequenceWithMatchesAsync(ToInner(first), ToInner(second), minLength, flags); + public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) => Inner.PublishAsync(ToInner(channel), message, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 33bb46777..0b4924949 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -424,14 +424,6 @@ StackExchange.Redis.GeoUnit.Feet = 3 -> StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit.Kilometers = 1 -> StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit.Meters = 0 -> StackExchange.Redis.GeoUnit StackExchange.Redis.GeoUnit.Miles = 2 -> StackExchange.Redis.GeoUnit -StackExchange.Redis.ListPopResult -StackExchange.Redis.ListPopResult.IsNull.get -> bool -StackExchange.Redis.ListPopResult.Key.get -> StackExchange.Redis.RedisKey -StackExchange.Redis.ListPopResult.ListPopResult() -> void -StackExchange.Redis.ListPopResult.Values.get -> StackExchange.Redis.RedisValue[]! -StackExchange.Redis.ListSide -StackExchange.Redis.ListSide.Left = 0 -> StackExchange.Redis.ListSide -StackExchange.Redis.ListSide.Right = 1 -> StackExchange.Redis.ListSide StackExchange.Redis.HashEntry StackExchange.Redis.HashEntry.Equals(StackExchange.Redis.HashEntry other) -> bool StackExchange.Redis.HashEntry.HashEntry() -> void @@ -709,6 +701,9 @@ StackExchange.Redis.IDatabase.StringGetWithExpiry(StackExchange.Redis.RedisKey k StackExchange.Redis.IDatabase.StringIncrement(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double StackExchange.Redis.IDatabase.StringIncrement(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringLongestCommonSubsequence(StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? +StackExchange.Redis.IDatabase.StringLongestCommonSubsequenceLength(StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StringLongestCommonSubsequenceWithMatches(StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, long minLength = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LCSMatchResult StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when) -> bool StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> bool @@ -929,6 +924,9 @@ StackExchange.Redis.IDatabaseAsync.StringGetWithExpiryAsync(StackExchange.Redis. StackExchange.Redis.IDatabaseAsync.StringIncrementAsync(StackExchange.Redis.RedisKey key, double value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringIncrementAsync(StackExchange.Redis.RedisKey key, long value = 1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringLongestCommonSubsequenceAsync(StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringLongestCommonSubsequenceLengthAsync(StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringLongestCommonSubsequenceWithMatchesAsync(StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, long minLength = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -1126,6 +1124,23 @@ StackExchange.Redis.LoadedLuaScript.EvaluateAsync(StackExchange.Redis.IDatabaseA StackExchange.Redis.LoadedLuaScript.ExecutableScript.get -> string! StackExchange.Redis.LoadedLuaScript.Hash.get -> byte[]! StackExchange.Redis.LoadedLuaScript.OriginalScript.get -> string! +StackExchange.Redis.LCSMatchResult +StackExchange.Redis.LCSMatchResult.LCSMatchResult() -> void +StackExchange.Redis.LCSMatchResult.LongestMatchLength.get -> long +StackExchange.Redis.LCSMatchResult.Matches.get -> StackExchange.Redis.LCSMatchResult.LCSMatch[]! +StackExchange.Redis.LCSMatchResult.LCSMatch +StackExchange.Redis.LCSMatchResult.LCSMatch.LCSMatch() -> void +StackExchange.Redis.LCSMatchResult.LCSMatch.FirstStringIndex.get -> long +StackExchange.Redis.LCSMatchResult.LCSMatch.SecondStringIndex.get -> long +StackExchange.Redis.LCSMatchResult.LCSMatch.Length.get -> long +StackExchange.Redis.ListPopResult +StackExchange.Redis.ListPopResult.IsNull.get -> bool +StackExchange.Redis.ListPopResult.Key.get -> StackExchange.Redis.RedisKey +StackExchange.Redis.ListPopResult.ListPopResult() -> void +StackExchange.Redis.ListPopResult.Values.get -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.ListSide +StackExchange.Redis.ListSide.Left = 0 -> StackExchange.Redis.ListSide +StackExchange.Redis.ListSide.Right = 1 -> StackExchange.Redis.ListSide StackExchange.Redis.LuaScript StackExchange.Redis.LuaScript.Evaluate(StackExchange.Redis.IDatabase! db, object? ps = null, StackExchange.Redis.RedisKey? withKeyPrefix = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! StackExchange.Redis.LuaScript.EvaluateAsync(StackExchange.Redis.IDatabaseAsync! db, object? ps = null, StackExchange.Redis.RedisKey? withKeyPrefix = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 9b699d10c..7a4e7af98 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1443,6 +1443,42 @@ public Task LockTakeAsync(RedisKey key, RedisValue value, TimeSpan expiry, return StringSetAsync(key, value, expiry, When.NotExists, flags); } + public string? StringLongestCommonSubsequence(RedisKey key1, RedisKey key2, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.LCS, key1, key2); + return ExecuteSync(msg, ResultProcessor.String); + } + + public Task StringLongestCommonSubsequenceAsync(RedisKey key1, RedisKey key2, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.LCS, key1, key2); + return ExecuteAsync(msg, ResultProcessor.String); + } + + public long StringLongestCommonSubsequenceLength(RedisKey key1, RedisKey key2, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.LCS, key1, key2, RedisLiterals.LEN); + return ExecuteSync(msg, ResultProcessor.Int64); + } + + public Task StringLongestCommonSubsequenceLengthAsync(RedisKey key1, RedisKey key2, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.LCS, key1, key2, RedisLiterals.LEN); + return ExecuteAsync(msg, ResultProcessor.Int64); + } + + public LCSMatchResult StringLongestCommonSubsequenceWithMatches(RedisKey key1, RedisKey key2, long minSubMatchLength = 0, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.LCS, key1, key2, RedisLiterals.IDX, RedisLiterals.MINMATCHLEN, minSubMatchLength, RedisLiterals.WITHMATCHLEN); + return ExecuteSync(msg, ResultProcessor.LCSMatchResult, defaultValue: LCSMatchResult.Null); + } + + public Task StringLongestCommonSubsequenceWithMatchesAsync(RedisKey key1, RedisKey key2, long minSubMatchLength = 0, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.LCS, key1, key2, RedisLiterals.IDX, RedisLiterals.MINMATCHLEN, minSubMatchLength, RedisLiterals.WITHMATCHLEN); + return ExecuteAsync(msg, ResultProcessor.LCSMatchResult, defaultValue: LCSMatchResult.Null); + } + public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { if (channel.IsNullOrEmpty) throw new ArgumentNullException(nameof(channel)); @@ -1473,6 +1509,7 @@ public RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisV public RedisResult Execute(string command, params object[] args) => Execute(command, args, CommandFlags.None); + public RedisResult Execute(string command, ICollection args, CommandFlags flags = CommandFlags.None) { var msg = new ExecuteMessage(multiplexer?.CommandMap, Database, flags, command, args); diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 295b0ef04..846969d15 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -71,11 +71,13 @@ public static readonly RedisValue GT = "GT", HISTORY = "HISTORY", ID = "ID", + IDX = "IDX", IDLETIME = "IDLETIME", KEEPTTL = "KEEPTTL", KILL = "KILL", LATEST = "LATEST", LEFT = "LEFT", + LEN = "LEN", LIMIT = "LIMIT", LIST = "LIST", LOAD = "LOAD", @@ -85,6 +87,7 @@ public static readonly RedisValue MAX = "MAX", MAXLEN = "MAXLEN", MIN = "MIN", + MINMATCHLEN = "MINMATCHLEN", NODES = "NODES", NOSAVE = "NOSAVE", NOT = "NOT", @@ -116,6 +119,7 @@ public static readonly RedisValue STORE = "STORE", TYPE = "TYPE", WEIGHTS = "WEIGHTS", + WITHMATCHLEN = "WITHMATCHLEN", WITHSCORES = "WITHSCORES", WITHVALUES = "WITHVALUES", XOR = "XOR", diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index e7afa7cff..8b15a8762 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -151,6 +151,9 @@ public static readonly StreamPendingMessagesProcessor public static ResultProcessor GeoRadiusArray(GeoRadiusOptions options) => GeoRadiusResultArrayProcessor.Get(options); + public static readonly ResultProcessor + LCSMatchResult = new LongestCommonSubsequenceProcessor(); + public static readonly ResultProcessor String = new StringProcessor(), TieBreaker = new TieBreakerProcessor(), @@ -1522,6 +1525,55 @@ The geohash integer. } } + /// + /// Parser for the https://redis.io/commands/lcs/ format with the and arguments. + /// + /// + /// Example response: + /// 1) "matches" + /// 2) 1) 1) 1) (integer) 4 + /// 2) (integer) 7 + /// 2) 1) (integer) 5 + /// 2) (integer) 8 + /// 3) (integer) 4 + /// 3) "len" + /// 4) (integer) 6 + /// + private sealed class LongestCommonSubsequenceProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Type) + { + case ResultType.BulkString: + case ResultType.MultiBulk: + SetResult(message, Parse(result)); + return true; + } + return false; + } + + private static LCSMatchResult Parse(in RawResult result) + { + var topItems = result.GetItems(); + var matches = new LCSMatchResult.LCSMatch[topItems[1].GetItems().Length]; + int i = 0; + var matchesRawArray = topItems[1]; // skip the first element (title "matches") + foreach (var match in matchesRawArray.GetItems()) + { + var matchItems = match.GetItems(); + + matches[i++] = new LCSMatchResult.LCSMatch( + firstStringIndex: (long)matchItems[0].GetItems()[0].AsRedisValue(), + secondStringIndex: (long)matchItems[1].GetItems()[0].AsRedisValue(), + length: (long)matchItems[2].AsRedisValue()); + } + var len = (long)topItems[3].AsRedisValue(); + + return new LCSMatchResult(matches, len); + } + } + private sealed class RedisValueProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index 7fe1be74a..54dfcce0d 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -641,27 +641,108 @@ public async Task HashStringLengthAsync() { using var conn = Create(require: RedisFeatures.v3_2_0); - var database = conn.GetDatabase(); + var db = conn.GetDatabase(); var key = Me(); const string value = "hello world"; - database.HashSet(key, "field", value); - var resAsync = database.HashStringLengthAsync(key, "field"); - var resNonExistingAsync = database.HashStringLengthAsync(key, "non-existing-field"); + db.HashSet(key, "field", value); + var resAsync = db.HashStringLengthAsync(key, "field"); + var resNonExistingAsync = db.HashStringLengthAsync(key, "non-existing-field"); Assert.Equal(value.Length, await resAsync); Assert.Equal(0, await resNonExistingAsync); } + [Fact] public void HashStringLength() { using var conn = Create(require: RedisFeatures.v3_2_0); - var database = conn.GetDatabase(); + var db = conn.GetDatabase(); var key = Me(); const string value = "hello world"; - database.HashSet(key, "field", value); - Assert.Equal(value.Length, database.HashStringLength(key, "field")); - Assert.Equal(0, database.HashStringLength(key, "non-existing-field")); + db.HashSet(key, "field", value); + Assert.Equal(value.Length, db.HashStringLength(key, "field")); + Assert.Equal(0, db.HashStringLength(key, "non-existing-field")); + } + + [Fact] + public void LongestCommonSubsequence() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key1 = Me() + "1"; + var key2 = Me() + "2"; + db.KeyDelete(key1); + db.KeyDelete(key2); + db.StringSet(key1, "ohmytext"); + db.StringSet(key2, "mynewtext"); + + Assert.Equal("mytext", db.StringLongestCommonSubsequence(key1, key2)); + Assert.Equal(6, db.StringLongestCommonSubsequenceLength(key1, key2)); + + var stringMatchResult = db.StringLongestCommonSubsequenceWithMatches(key1, key2); + Assert.Equal(2, stringMatchResult.Matches.Length); // "my" and "text" are the two matches of the result + Assert.Equivalent(new LCSMatchResult.LCSMatch(4, 5, length: 4), stringMatchResult.Matches[0]); // the string "text" starts at index 4 in the first string and at index 5 in the second string + Assert.Equivalent(new LCSMatchResult.LCSMatch(2, 0, length: 2), stringMatchResult.Matches[1]); // the string "my" starts at index 2 in the first string and at index 0 in the second string + + stringMatchResult = db.StringLongestCommonSubsequenceWithMatches(key1, key2, 5); + Assert.Empty(stringMatchResult.Matches); // no matches longer than 5 characters + Assert.Equal(6, stringMatchResult.LongestMatchLength); + + // Missing keys + db.KeyDelete(key1); + Assert.Equal(string.Empty, db.StringLongestCommonSubsequence(key1, key2)); + db.KeyDelete(key2); + Assert.Equal(string.Empty, db.StringLongestCommonSubsequence(key1, key2)); + stringMatchResult = db.StringLongestCommonSubsequenceWithMatches(key1, key2); + Assert.NotNull(stringMatchResult.Matches); + Assert.Empty(stringMatchResult.Matches); + Assert.Equal(0, stringMatchResult.LongestMatchLength); + + // Default value + stringMatchResult = db.StringLongestCommonSubsequenceWithMatches(key1, key2, flags: CommandFlags.FireAndForget); + Assert.Equal(stringMatchResult, LCSMatchResult.Null); + } + + [Fact] + public async Task LongestCommonSubsequenceAsync() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var db = conn.GetDatabase(); + var key1 = Me() + "1"; + var key2 = Me() + "2"; + db.KeyDelete(key1); + db.KeyDelete(key2); + db.StringSet(key1, "ohmytext"); + db.StringSet(key2, "mynewtext"); + + Assert.Equal("mytext", await db.StringLongestCommonSubsequenceAsync(key1, key2)); + Assert.Equal(6, await db.StringLongestCommonSubsequenceLengthAsync(key1, key2)); + + var stringMatchResult = await db.StringLongestCommonSubsequenceWithMatchesAsync(key1, key2); + Assert.Equal(2, stringMatchResult.Matches.Length); // "my" and "text" are the two matches of the result + Assert.Equivalent(new LCSMatchResult.LCSMatch(4, 5, length: 4), stringMatchResult.Matches[0]); // the string "text" starts at index 4 in the first string and at index 5 in the second string + Assert.Equivalent(new LCSMatchResult.LCSMatch(2, 0, length: 2), stringMatchResult.Matches[1]); // the string "my" starts at index 2 in the first string and at index 0 in the second string + + stringMatchResult = await db.StringLongestCommonSubsequenceWithMatchesAsync(key1, key2, 5); + Assert.Empty(stringMatchResult.Matches); // no matches longer than 5 characters + Assert.Equal(6, stringMatchResult.LongestMatchLength); + + // Missing keys + db.KeyDelete(key1); + Assert.Equal(string.Empty, await db.StringLongestCommonSubsequenceAsync(key1, key2)); + db.KeyDelete(key2); + Assert.Equal(string.Empty, await db.StringLongestCommonSubsequenceAsync(key1, key2)); + stringMatchResult = await db.StringLongestCommonSubsequenceWithMatchesAsync(key1, key2); + Assert.NotNull(stringMatchResult.Matches); + Assert.Empty(stringMatchResult.Matches); + Assert.Equal(0, stringMatchResult.LongestMatchLength); + + // Default value + stringMatchResult = await db.StringLongestCommonSubsequenceWithMatchesAsync(key1, key2, flags: CommandFlags.FireAndForget); + Assert.Equal(stringMatchResult, LCSMatchResult.Null); } private static byte[] Encode(string value) => Encoding.UTF8.GetBytes(value); From a3a49ca8c095fff98f07b5b2119d7abedd3c27e3 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 17 May 2022 09:08:03 +0100 Subject: [PATCH 155/435] fix #2144 - incorrect variable used in RedisValue null/non-equality test --- src/StackExchange.Redis/RedisValue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index ba16b6d42..45f23474b 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -173,7 +173,7 @@ internal ulong OverlappedValueUInt64 StorageType xType = x.Type, yType = y.Type; if (xType == StorageType.Null) return yType == StorageType.Null; - if (xType == StorageType.Null) return false; + if (yType == StorageType.Null) return false; if (xType == yType) { From 2f0c4770df7747d65a3bb44083fd15da58a80923 Mon Sep 17 00:00:00 2001 From: Reece Russell Date: Tue, 24 May 2022 14:55:14 +0100 Subject: [PATCH 156/435] fix: corrected fuget.org typo (#2145) --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index f25ba5dea..d1711e346 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,7 +2,7 @@ StackExchange.Redis =================== - [Release Notes](ReleaseNotes) -- [API Browser (via furget.org)](https://www.fuget.org/packages/StackExchange.Redis/) +- [API Browser (via fuget.org)](https://www.fuget.org/packages/StackExchange.Redis/) ## Overview From 4e8431ea85a318e9f3d5ed92a4ec4044b89a5df0 Mon Sep 17 00:00:00 2001 From: shacharPash <93581407+shacharPash@users.noreply.github.com> Date: Tue, 24 May 2022 18:31:43 +0300 Subject: [PATCH 157/435] Support COMMAND [...] (#2143) Add support for `COMMAND` commands (part of #2055): COMMAND COUNT - https://redis.io/commands/command-count/ COMMAND GETKEYS - https://redis.io/commands/command-getkeys/ COMMAND LIST - https://redis.io/commands/command-list/ Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 2 + .../APITypes/LCSMatchResult.cs | 5 ++ src/StackExchange.Redis/Enums/RedisCommand.cs | 2 + src/StackExchange.Redis/Interfaces/IServer.cs | 35 +++++++++ src/StackExchange.Redis/Message.cs | 1 + src/StackExchange.Redis/PublicAPI.Shipped.txt | 7 ++ src/StackExchange.Redis/RawResult.cs | 3 + src/StackExchange.Redis/RedisDatabase.cs | 24 +++--- src/StackExchange.Redis/RedisLiterals.cs | 5 ++ src/StackExchange.Redis/RedisServer.cs | 73 +++++++++++++++++++ src/StackExchange.Redis/ResultProcessor.cs | 20 ++++- tests/StackExchange.Redis.Tests/Databases.cs | 63 +++++++++++++++- tests/StackExchange.Redis.Tests/Strings.cs | 4 +- 13 files changed, 227 insertions(+), 17 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 001c8104f..e92a4084d 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -34,6 +34,8 @@ - Adds: Support for `BIT | BYTE` to `BITCOUNT` and `BITPOS` with `.StringBitCount()`/`.StringBitCountAsync()` and `.StringBitPosition()`/`.StringBitPositionAsync()` [#2116 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2116)) - Adds: Support for pub/sub payloads that are unary arrays ([#2118 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2118)) - Fix: Sentinel timer race during dispose ([#2133 by ewisuri](https://github.com/StackExchange/StackExchange.Redis/pull/2133)) +- Adds: Support for `COMMAND COUNT`, `COMMAND GETKEYS`, and `COMMAND LIST`, with `.CommandCount()`/`.CommandCountAsync()`, `.CommandGetKeys()`/`.CommandGetKeysAsync()`, and `.CommandList()`/`.CommandListAsync()` ([#2143 by shacharPash](https://github.com/StackExchange/StackExchange.Redis/pull/2143)) + ## 2.5.61 diff --git a/src/StackExchange.Redis/APITypes/LCSMatchResult.cs b/src/StackExchange.Redis/APITypes/LCSMatchResult.cs index 97ff8c1cd..fdeea89ff 100644 --- a/src/StackExchange.Redis/APITypes/LCSMatchResult.cs +++ b/src/StackExchange.Redis/APITypes/LCSMatchResult.cs @@ -10,6 +10,11 @@ public readonly struct LCSMatchResult { internal static LCSMatchResult Null { get; } = new LCSMatchResult(Array.Empty(), 0); + /// + /// Whether this match result contains any matches. + /// + public bool IsEmpty => LongestMatchLength == 0 && (Matches is null || Matches.Length == 0); + /// /// The matched positions of all the sub-matched strings. /// diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 8587ceedf..6eded0e78 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -23,6 +23,7 @@ internal enum RedisCommand CLUSTER, CONFIG, COPY, + COMMAND, DBSIZE, DEBUG, @@ -361,6 +362,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.BITPOS: case RedisCommand.CLIENT: case RedisCommand.CLUSTER: + case RedisCommand.COMMAND: case RedisCommand.CONFIG: case RedisCommand.DBSIZE: case RedisCommand.DEBUG: diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 3d9eb8065..7319f7feb 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -179,6 +179,41 @@ public partial interface IServer : IRedis /// Task ConfigSetAsync(RedisValue setting, RedisValue value, CommandFlags flags = CommandFlags.None); + /// + /// Returns the number of total commands available in this Redis server. + /// + /// The command flags to use. + /// + long CommandCount(CommandFlags flags = CommandFlags.None); + + /// + Task CommandCountAsync(CommandFlags flags = CommandFlags.None); + + /// + /// Returns list of keys from a full Redis command. + /// + /// The command to get keys from. + /// The command flags to use. + /// + RedisKey[] CommandGetKeys(RedisValue[] command, CommandFlags flags = CommandFlags.None); + + /// + Task CommandGetKeysAsync(RedisValue[] command, CommandFlags flags = CommandFlags.None); + + /// + /// Returns a list of command names available on this Redis server. + /// Only one of the filter options is usable at a time. + /// + /// The module name to filter the command list by. + /// The category to filter the command list by. + /// The pattern to filter the command list by. + /// The command flags to use. + /// + string[] CommandList(RedisValue? moduleName = null, RedisValue? category = null, RedisValue? pattern = null, CommandFlags flags = CommandFlags.None); + + /// + Task CommandListAsync(RedisValue? moduleName = null, RedisValue? category = null, RedisValue? pattern = null, CommandFlags flags = CommandFlags.None); + /// /// Return the number of keys in the database. /// diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index f44a395d5..e9c35925a 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -473,6 +473,7 @@ internal static bool RequiresDatabase(RedisCommand command) case RedisCommand.BGSAVE: case RedisCommand.CLIENT: case RedisCommand.CLUSTER: + case RedisCommand.COMMAND: case RedisCommand.CONFIG: case RedisCommand.DISCARD: case RedisCommand.ECHO: diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 0b4924949..9f7168a51 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -980,6 +980,12 @@ StackExchange.Redis.IServer.ConfigRewrite(StackExchange.Redis.CommandFlags flags StackExchange.Redis.IServer.ConfigRewriteAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.ConfigSet(StackExchange.Redis.RedisValue setting, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IServer.ConfigSetAsync(StackExchange.Redis.RedisValue setting, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.CommandCount(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IServer.CommandCountAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.CommandGetKeys(StackExchange.Redis.RedisValue[]! command, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisKey[]! +StackExchange.Redis.IServer.CommandGetKeysAsync(StackExchange.Redis.RedisValue[]! command, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.CommandList(StackExchange.Redis.RedisValue? moduleName = null, StackExchange.Redis.RedisValue? category = null, StackExchange.Redis.RedisValue? pattern = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string![]! +StackExchange.Redis.IServer.CommandListAsync(StackExchange.Redis.RedisValue? moduleName = null, StackExchange.Redis.RedisValue? category = null, StackExchange.Redis.RedisValue? pattern = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.DatabaseCount.get -> int StackExchange.Redis.IServer.DatabaseSize(int database = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IServer.DatabaseSizeAsync(int database = -1, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -1125,6 +1131,7 @@ StackExchange.Redis.LoadedLuaScript.ExecutableScript.get -> string! StackExchange.Redis.LoadedLuaScript.Hash.get -> byte[]! StackExchange.Redis.LoadedLuaScript.OriginalScript.get -> string! StackExchange.Redis.LCSMatchResult +StackExchange.Redis.LCSMatchResult.IsEmpty.get -> bool StackExchange.Redis.LCSMatchResult.LCSMatchResult() -> void StackExchange.Redis.LCSMatchResult.LongestMatchLength.get -> long StackExchange.Redis.LCSMatchResult.Matches.get -> StackExchange.Redis.LCSMatchResult.LCSMatch[]! diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 11ca450af..8a4f4cf88 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -265,6 +265,9 @@ internal bool GetBoolean() [MethodImpl(MethodImplOptions.AggressiveInlining)] internal string?[]? GetItemsAsStrings() => this.ToArray((in RawResult x) => (string?)x.AsRedisValue()); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal string[]? GetItemsAsStringsNotNullable() => this.ToArray((in RawResult x) => (string)x.AsRedisValue()!); + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool[]? GetItemsAsBooleans() => this.ToArray((in RawResult x) => (bool)x.AsRedisValue()); diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 7a4e7af98..e713eb80e 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -116,7 +116,7 @@ public Task GeoRemoveAsync(RedisKey key, RedisValue member, CommandFlags f var redisValues = new RedisValue[members.Length]; for (var i = 0; i < members.Length; i++) redisValues[i] = members[i]; var msg = Message.Create(Database, flags, RedisCommand.GEOHASH, key, redisValues); - return ExecuteSync(msg, ResultProcessor.StringArray, defaultValue: Array.Empty()); + return ExecuteSync(msg, ResultProcessor.NullableStringArray, defaultValue: Array.Empty()); } public Task GeoHashAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) @@ -125,7 +125,7 @@ public Task GeoRemoveAsync(RedisKey key, RedisValue member, CommandFlags f var redisValues = new RedisValue[members.Length]; for (var i = 0; i < members.Length; i++) redisValues[i] = members[i]; var msg = Message.Create(Database, flags, RedisCommand.GEOHASH, key, redisValues); - return ExecuteAsync(msg, ResultProcessor.StringArray, defaultValue: Array.Empty()); + return ExecuteAsync(msg, ResultProcessor.NullableStringArray, defaultValue: Array.Empty()); } public string? GeoHash(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) @@ -829,37 +829,37 @@ public Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFl return ExecuteAsync(msg, ResultProcessor.Int64); } - public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags) => + public bool KeyExpire(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => KeyExpire(key, expiry, ExpireWhen.Always, flags); - public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags) => + public bool KeyExpire(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) => KeyExpire(key, expiry, ExpireWhen.Always, flags); - public bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + public bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) { var msg = GetExpiryMessage(key, flags, expiry, when, out ServerEndPoint? server); return ExecuteSync(msg, ResultProcessor.Boolean, server: server); } - public bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + public bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) { var msg = GetExpiryMessage(key, flags, expiry, when, out ServerEndPoint? server); return ExecuteSync(msg, ResultProcessor.Boolean, server: server); } - public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags) => + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => KeyExpireAsync(key, expiry, ExpireWhen.Always, flags); - public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags) => + public Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags = CommandFlags.None) => KeyExpireAsync(key, expiry, ExpireWhen.Always, flags); - public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + public Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when, CommandFlags flags = CommandFlags.None) { var msg = GetExpiryMessage(key, flags, expiry, when, out ServerEndPoint? server); return ExecuteAsync(msg, ResultProcessor.Boolean, server: server); } - public Task KeyExpireAsync(RedisKey key, DateTime? expire, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + public Task KeyExpireAsync(RedisKey key, DateTime? expire, ExpireWhen when, CommandFlags flags = CommandFlags.None) { var msg = GetExpiryMessage(key, flags, expire, when, out ServerEndPoint? server); return ExecuteAsync(msg, ResultProcessor.Boolean, server: server); @@ -1470,13 +1470,13 @@ public Task StringLongestCommonSubsequenceLengthAsync(RedisKey key1, Redis public LCSMatchResult StringLongestCommonSubsequenceWithMatches(RedisKey key1, RedisKey key2, long minSubMatchLength = 0, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.LCS, key1, key2, RedisLiterals.IDX, RedisLiterals.MINMATCHLEN, minSubMatchLength, RedisLiterals.WITHMATCHLEN); - return ExecuteSync(msg, ResultProcessor.LCSMatchResult, defaultValue: LCSMatchResult.Null); + return ExecuteSync(msg, ResultProcessor.LCSMatchResult); } public Task StringLongestCommonSubsequenceWithMatchesAsync(RedisKey key1, RedisKey key2, long minSubMatchLength = 0, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.LCS, key1, key2, RedisLiterals.IDX, RedisLiterals.MINMATCHLEN, minSubMatchLength, RedisLiterals.WITHMATCHLEN); - return ExecuteAsync(msg, ResultProcessor.LCSMatchResult, defaultValue: LCSMatchResult.Null); + return ExecuteAsync(msg, ResultProcessor.LCSMatchResult); } public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 846969d15..9747a141a 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -41,6 +41,7 @@ internal static class RedisLiterals // unlike primary commands, these do not get altered by the command-map; we may as // well compute the bytes once and share them public static readonly RedisValue + ACLCAT = "ACLCAT", ADDR = "ADDR", AFTER = "AFTER", AGGREGATE = "AGGREGATE", @@ -64,9 +65,11 @@ public static readonly RedisValue EX = "EX", EXAT = "EXAT", EXISTS = "EXISTS", + FILTERBY = "FILTERBY", FLUSH = "FLUSH", FREQ = "FREQ", GET = "GET", + GETKEYS = "GETKEYS", GETNAME = "GETNAME", GT = "GT", HISTORY = "HISTORY", @@ -88,6 +91,7 @@ public static readonly RedisValue MAXLEN = "MAXLEN", MIN = "MIN", MINMATCHLEN = "MINMATCHLEN", + MODULE = "MODULE", NODES = "NODES", NOSAVE = "NOSAVE", NOT = "NOT", @@ -96,6 +100,7 @@ public static readonly RedisValue NX = "NX", OBJECT = "OBJECT", OR = "OR", + PATTERN = "PATTERN", PAUSE = "PAUSE", PERSIST = "PERSIST", PING = "PING", diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index cf094d1fd..dea6d586d 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -204,6 +204,79 @@ public Task ConfigSetAsync(RedisValue setting, RedisValue value, CommandFlags fl return task; } + public long CommandCount(CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.COMMAND, RedisLiterals.COUNT); + return ExecuteSync(msg, ResultProcessor.Int64); + } + + public Task CommandCountAsync(CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.COMMAND, RedisLiterals.COUNT); + return ExecuteAsync(msg, ResultProcessor.Int64); + } + + public RedisKey[] CommandGetKeys(RedisValue[] command, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.COMMAND, AddValueToArray(RedisLiterals.GETKEYS, command)); + return ExecuteSync(msg, ResultProcessor.RedisKeyArray, defaultValue: Array.Empty()); + } + + public Task CommandGetKeysAsync(RedisValue[] command, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.COMMAND, AddValueToArray(RedisLiterals.GETKEYS, command)); + return ExecuteAsync(msg, ResultProcessor.RedisKeyArray, defaultValue: Array.Empty()); + } + + public string[] CommandList(RedisValue? moduleName = null, RedisValue? category = null, RedisValue? pattern = null, CommandFlags flags = CommandFlags.None) + { + var msg = GetCommandListMessage(moduleName, category, pattern, flags); + return ExecuteSync(msg, ResultProcessor.StringArray, defaultValue: Array.Empty()); + } + + public Task CommandListAsync(RedisValue? moduleName = null, RedisValue? category = null, RedisValue? pattern = null, CommandFlags flags = CommandFlags.None) + { + var msg = GetCommandListMessage(moduleName, category, pattern, flags); + return ExecuteAsync(msg, ResultProcessor.StringArray, defaultValue: Array.Empty()); + } + + private Message GetCommandListMessage(RedisValue? moduleName = null, RedisValue? category = null, RedisValue? pattern = null, CommandFlags flags = CommandFlags.None) + { + if (moduleName == null && category == null && pattern == null) + { + return Message.Create(-1, flags, RedisCommand.COMMAND, RedisLiterals.LIST); + } + + else if (moduleName != null && category == null && pattern == null) + { + return Message.Create(-1, flags, RedisCommand.COMMAND, MakeArray(RedisLiterals.LIST, RedisLiterals.FILTERBY, RedisLiterals.MODULE, (RedisValue)moduleName)); + } + + else if (moduleName == null && category != null && pattern == null) + { + return Message.Create(-1, flags, RedisCommand.COMMAND, MakeArray(RedisLiterals.LIST, RedisLiterals.FILTERBY, RedisLiterals.ACLCAT, (RedisValue)category)); + } + + else if (moduleName == null && category == null && pattern != null) + { + return Message.Create(-1, flags, RedisCommand.COMMAND, MakeArray(RedisLiterals.LIST, RedisLiterals.FILTERBY, RedisLiterals.PATTERN, (RedisValue)pattern)); + } + + else + throw new ArgumentException("More then one filter is not allwed"); + } + + private RedisValue[] AddValueToArray(RedisValue val, RedisValue[] arr) + { + var result = new RedisValue[arr.Length + 1]; + var i = 0; + result[i++] = val; + foreach (var item in arr) result[i++] = item; + return result; + } + + private RedisValue[] MakeArray(params RedisValue[] redisValues) { return redisValues; } + public long DatabaseSize(int database = -1, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(multiplexer.ApplyDefaultDatabase(database), flags, RedisCommand.DBSIZE); diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 8b15a8762..17b6d383e 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -92,6 +92,9 @@ public static readonly ResultProcessor Int64Array = new Int64ArrayProcessor(); public static readonly ResultProcessor + NullableStringArray = new NullableStringArrayProcessor(); + + public static readonly ResultProcessor StringArray = new StringArrayProcessor(); public static readonly ResultProcessor @@ -1394,7 +1397,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - private sealed class StringArrayProcessor : ResultProcessor + private sealed class NullableStringArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { @@ -1410,6 +1413,21 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + private sealed class StringArrayProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Type) + { + case ResultType.MultiBulk: + var arr = result.GetItemsAsStringsNotNullable()!; + SetResult(message, arr); + return true; + } + return false; + } + } + private sealed class BooleanArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) diff --git a/tests/StackExchange.Redis.Tests/Databases.cs b/tests/StackExchange.Redis.Tests/Databases.cs index 1dfffb4e0..bacce1cd7 100644 --- a/tests/StackExchange.Redis.Tests/Databases.cs +++ b/tests/StackExchange.Redis.Tests/Databases.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -7,7 +8,65 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] public class Databases : TestBase { - public Databases(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public Databases(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public async Task CommandCount() + { + using var conn = Create(); + var server = GetAnyPrimary(conn); + var count = server.CommandCount(); + Assert.True(count > 100); + + count = await server.CommandCountAsync(); + Assert.True(count > 100); + } + + [Fact] + public async Task CommandGetKeys() + { + using var conn = Create(); + var server = GetAnyPrimary(conn); + + RedisValue[] command = { "MSET", "a", "b", "c", "d", "e", "f" }; + + RedisKey[] keys = server.CommandGetKeys(command); + RedisKey[] expected = { "a", "c", "e" }; + Assert.Equal(keys, expected); + + keys = await server.CommandGetKeysAsync(command); + Assert.Equal(keys, expected); + } + + [Fact] + public async Task CommandList() + { + using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + var server = GetAnyPrimary(conn); + + var commands = server.CommandList(); + Assert.True(commands.Length > 100); + commands = await server.CommandListAsync(); + Assert.True(commands.Length > 100); + + commands = server.CommandList(moduleName: "JSON"); + Assert.Empty(commands); + commands = await server.CommandListAsync(moduleName: "JSON"); + Assert.Empty(commands); + + commands = server.CommandList(category: "admin"); + Assert.True(commands.Length > 10); + commands = await server.CommandListAsync(category: "admin"); + Assert.True(commands.Length > 10); + + commands = server.CommandList(pattern: "a*"); + Assert.True(commands.Length > 10); + commands = await server.CommandListAsync(pattern: "a*"); + Assert.True(commands.Length > 10); + + Assert.Throws(() => server.CommandList(moduleName: "JSON", pattern: "a*")); + await Assert.ThrowsAsync(() => server.CommandListAsync(moduleName: "JSON", pattern: "a*")); + } [Fact] public async Task CountKeys() diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/Strings.cs index 54dfcce0d..034036703 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/Strings.cs @@ -702,7 +702,7 @@ public void LongestCommonSubsequence() // Default value stringMatchResult = db.StringLongestCommonSubsequenceWithMatches(key1, key2, flags: CommandFlags.FireAndForget); - Assert.Equal(stringMatchResult, LCSMatchResult.Null); + Assert.True(stringMatchResult.IsEmpty); } [Fact] @@ -742,7 +742,7 @@ public async Task LongestCommonSubsequenceAsync() // Default value stringMatchResult = await db.StringLongestCommonSubsequenceWithMatchesAsync(key1, key2, flags: CommandFlags.FireAndForget); - Assert.Equal(stringMatchResult, LCSMatchResult.Null); + Assert.True(stringMatchResult.IsEmpty); } private static byte[] Encode(string value) => Encoding.UTF8.GetBytes(value); From e0d8320c89b180c267a8bf9481f28cac976adf03 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 28 May 2022 13:48:19 -0400 Subject: [PATCH 158/435] Builds: GitHub Actions logger upgrade (#2106) Co-authored-by: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com> --- .github/workflows/CI.yml | 4 ++-- Directory.Packages.props | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 382d72520..bbf7997c7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -30,7 +30,7 @@ jobs: working-directory: ./tests/RedisConfigs run: docker-compose -f docker-compose.yml up -d - name: StackExchange.Redis.Tests - run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --results-directory ./test-results/ /p:CI=true + run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true - uses: dorny/test-reporter@v1 continue-on-error: true if: success() || failure() @@ -79,7 +79,7 @@ jobs: .\redis-server.exe --service-install --service-name "redis-26381" "..\Sentinel\sentinel-26381.conf" --sentinel Start-Service redis-* - name: StackExchange.Redis.Tests - run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --results-directory ./test-results/ /p:CI=true + run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true - uses: dorny/test-reporter@v1 continue-on-error: true if: success() || failure() diff --git a/Directory.Packages.props b/Directory.Packages.props index 689a6607e..5f38b84ee 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,7 +10,7 @@ - + From 67297e39a497c148aae83f78527483ad13b3577d Mon Sep 17 00:00:00 2001 From: Avital Fine <98389525+Avital-Fine@users.noreply.github.com> Date: Tue, 14 Jun 2022 17:14:53 +0300 Subject: [PATCH 159/435] Support GT, LT and CH in ZADD command (#2136) [SortedSetAdd](https://redis.io/commands/zadd/) currently not supporting GT and LT features. Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 3 +- .../Enums/SortedSetWhen.cs | 52 ++++++++ .../Interfaces/IDatabase.cs | 58 ++++++--- .../Interfaces/IDatabaseAsync.cs | 58 ++++++--- .../KeyspaceIsolation/DatabaseWrapper.cs | 12 ++ .../KeyspaceIsolation/WrapperBase.cs | 11 ++ src/StackExchange.Redis/PublicAPI.Shipped.txt | 22 +++- src/StackExchange.Redis/RedisDatabase.cs | 122 ++++++++++++------ src/StackExchange.Redis/RedisLiterals.cs | 1 + .../DatabaseWrapperTests.cs | 16 +++ .../OverloadCompat.cs | 61 ++++++++- .../SortedSetWhen.cs | 43 ++++++ tests/StackExchange.Redis.Tests/SortedSets.cs | 19 +++ .../WrapperBaseTests.cs | 16 +++ 14 files changed, 402 insertions(+), 92 deletions(-) create mode 100644 src/StackExchange.Redis/Enums/SortedSetWhen.cs create mode 100644 tests/StackExchange.Redis.Tests/SortedSetWhen.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index e92a4084d..6eee8c388 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -31,9 +31,10 @@ - Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105)) - Performance: Avoids allocations when computing cluster hash slots or testing key equality ([#2110 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2110)) - Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111)) -- Adds: Support for `BIT | BYTE` to `BITCOUNT` and `BITPOS` with `.StringBitCount()`/`.StringBitCountAsync()` and `.StringBitPosition()`/`.StringBitPositionAsync()` [#2116 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2116)) +- Adds: Support for `BIT | BYTE` to `BITCOUNT` and `BITPOS` with `.StringBitCount()`/`.StringBitCountAsync()` and `.StringBitPosition()`/`.StringBitPositionAsync()` ([#2116 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2116)) - Adds: Support for pub/sub payloads that are unary arrays ([#2118 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2118)) - Fix: Sentinel timer race during dispose ([#2133 by ewisuri](https://github.com/StackExchange/StackExchange.Redis/pull/2133)) +- Adds: Support for `GT`, `LT`, and `CH` on `ZADD` with `.SortedSetAdd()`/`.SortedSetAddAsync()` and `.SortedSetUpdate()`/`.SortedSetUpdateAsync()` ([#2136 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2136)) - Adds: Support for `COMMAND COUNT`, `COMMAND GETKEYS`, and `COMMAND LIST`, with `.CommandCount()`/`.CommandCountAsync()`, `.CommandGetKeys()`/`.CommandGetKeysAsync()`, and `.CommandList()`/`.CommandListAsync()` ([#2143 by shacharPash](https://github.com/StackExchange/StackExchange.Redis/pull/2143)) diff --git a/src/StackExchange.Redis/Enums/SortedSetWhen.cs b/src/StackExchange.Redis/Enums/SortedSetWhen.cs new file mode 100644 index 000000000..a394482b6 --- /dev/null +++ b/src/StackExchange.Redis/Enums/SortedSetWhen.cs @@ -0,0 +1,52 @@ +using System; + +namespace StackExchange.Redis +{ + /// + /// Indicates when this operation should be performed (only some variations are legal in a given context). + /// + [Flags] + public enum SortedSetWhen + { + /// + /// The operation won't be prevented. + /// + Always = 0, + /// + /// The operation should only occur when there is an existing value. + /// + Exists = 1 << 0, + /// + /// The operation should only occur when the new score is greater than the current score. + /// + GreaterThan = 1 << 1, + /// + /// The operation should only occur when the new score is less than the current score. + /// + LessThan = 1 << 2, + /// + /// The operation should only occur when there is not an existing value. + /// + NotExists = 1 << 3, + } + + internal static class SortedSetWhenExtensions + { + internal static uint CountBits(this SortedSetWhen when) + { + uint v = (uint)when; + v -= ((v >> 1) & 0x55555555); // reuse input as temporary + v = (v & 0x33333333) + ((v >> 2) & 0x33333333); // temp + uint c = ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; // count + return c; + } + + internal static SortedSetWhen Parse(When when)=> when switch + { + When.Always => SortedSetWhen.Always, + When.Exists => SortedSetWhen.Exists, + When.NotExists => SortedSetWhen.NotExists, + _ => throw new ArgumentOutOfRangeException(nameof(when)) + }; + } +} diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index ee4dc0c4b..8325ce8e7 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1590,18 +1590,14 @@ public interface IDatabase : IRedis, IDatabaseAsync /// long SortAndStore(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); - /// - /// Adds the specified member with the specified score to the sorted set stored at key. - /// If the specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. - /// - /// The key of the sorted set. - /// The member to add to the sorted set. - /// The score for the member to add to the sorted set. - /// The flags to use for this operation. - /// if the value was added. if it already existed (the score is still updated). - /// + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags); + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when, CommandFlags flags= CommandFlags.None); + /// /// Adds the specified member with the specified score to the sorted set stored at key. /// If the specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. @@ -1613,19 +1609,16 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// if the value was added. if it already existed (the score is still updated). /// - bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None); + bool SortedSetAdd(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None); - /// - /// Adds all the specified members with the specified scores to the sorted set stored at key. - /// If a specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. - /// - /// The key of the sorted set. - /// The members and values to add to the sorted set. - /// The flags to use for this operation. - /// The number of elements added to the sorted sets, not including elements already existing for which the score was updated. - /// + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags flags); + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when, CommandFlags flags = CommandFlags.None); + /// /// Adds all the specified members with the specified scores to the sorted set stored at key. /// If a specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. @@ -1636,7 +1629,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The number of elements added to the sorted sets, not including elements already existing for which the score was updated. /// - long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); + long SortedSetAdd(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None); /// /// Computes a set operation for multiple sorted sets (optionally using per-set ), @@ -2160,6 +2153,29 @@ IEnumerable SortedSetScan(RedisKey key, /// SortedSetPopResult SortedSetPop(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); + /// + /// Same as but return the number of the elements changed. + /// + /// The key of the sorted set. + /// The member to add/update to the sorted set. + /// The score for the member to add/update to the sorted set. + /// What conditions to add the element under (defaults to always). + /// The flags to use for this operation. + /// The number of elements changed. + /// + bool SortedSetUpdate(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None); + + /// + /// Same as but return the number of the elements changed. + /// + /// The key of the sorted set. + /// The members and values to add/update to the sorted set. + /// What conditions to add the element under (defaults to always). + /// The flags to use for this operation. + /// The number of elements changed. + /// + long SortedSetUpdate(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None); + /// /// Allow the consumer to mark a pending message as correctly processed. Returns the number of messages acknowledged. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index b3950743e..d6db59caa 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1554,18 +1554,14 @@ public interface IDatabaseAsync : IRedisAsync /// Task SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); - /// - /// Adds the specified member with the specified score to the sorted set stored at key. - /// If the specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. - /// - /// The key of the sorted set. - /// The member to add to the sorted set. - /// The score for the member to add to the sorted set. - /// The flags to use for this operation. - /// if the value was added. if it already existed (the score is still updated). - /// + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, CommandFlags flags); + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when, CommandFlags flags = CommandFlags.None); + /// /// Adds the specified member with the specified score to the sorted set stored at key. /// If the specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. @@ -1577,19 +1573,16 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// if the value was added. if it already existed (the score is still updated). /// - Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None); + Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None); - /// - /// Adds all the specified members with the specified scores to the sorted set stored at key. - /// If a specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. - /// - /// The key of the sorted set. - /// The members and values to add to the sorted set. - /// The flags to use for this operation. - /// The number of elements added to the sorted sets, not including elements already existing for which the score was updated. - /// + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, CommandFlags flags); + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When when, CommandFlags flags = CommandFlags.None); + /// /// Adds all the specified members with the specified scores to the sorted set stored at key. /// If a specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. @@ -1600,7 +1593,7 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. /// The number of elements added to the sorted sets, not including elements already existing for which the score was updated. /// - Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); + Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None); /// /// Computes a set operation for multiple sorted sets (optionally using per-set ), @@ -2073,6 +2066,29 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// Task SortedSetScoresAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); + /// + /// Same as but return the number of the elements changed. + /// + /// The key of the sorted set. + /// The member to add/update to the sorted set. + /// The score for the member to add/update to the sorted set. + /// What conditions to add the element under (defaults to always). + /// The flags to use for this operation. + /// The number of elements changed. + /// + Task SortedSetUpdateAsync(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None); + + /// + /// Same as but return the number of the elements changed. + /// + /// The key of the sorted set. + /// The members and values to add/update to the sorted set. + /// What conditions to add the element under (defaults to always). + /// The flags to use for this operation. + /// The number of elements changed. + /// + Task SortedSetUpdateAsync(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None); + /// /// Removes and returns the first element from the sorted set stored at key, by default with the scores ordered from low to high. /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs index 6fa9bfbc0..d04ca60f6 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs @@ -399,12 +399,18 @@ public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags fla public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetAdd(ToInner(key), values, when, flags); + public long SortedSetAdd(RedisKey key, SortedSetEntry[] values,SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetAdd(ToInner(key), values, when, flags); + public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags) => Inner.SortedSetAdd(ToInner(key), member, score, flags); public bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetAdd(ToInner(key), member, score, when, flags); + public bool SortedSetAdd(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetAdd(ToInner(key), member, score, when, flags); + public RedisValue[] SortedSetCombine(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => Inner.SortedSetCombine(operation, keys, weights, aggregate, flags); @@ -496,6 +502,12 @@ public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue public double?[] SortedSetScores(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => Inner.SortedSetScores(ToInner(key), members, flags); + public long SortedSetUpdate(RedisKey key, SortedSetEntry[] values,SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetUpdate(ToInner(key), values, when, flags); + + public bool SortedSetUpdate(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetUpdate(ToInner(key), member, score, when, flags); + public SortedSetEntry? SortedSetPop(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => Inner.SortedSetPop(ToInner(key), order, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs index c604a21c9..702939f51 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs @@ -413,12 +413,17 @@ public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, Comma public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetAddAsync(ToInner(key), values, when, flags); + public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, SortedSetWhen updateWhen = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetAddAsync(ToInner(key), values, updateWhen, flags); + public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, CommandFlags flags) => Inner.SortedSetAddAsync(ToInner(key), member, score, flags); public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetAddAsync(ToInner(key), member, score, when, flags); + public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, SortedSetWhen updateWhen = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetAddAsync(ToInner(key), member, score, updateWhen, flags); public Task SortedSetCombineAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => Inner.SortedSetCombineAsync(operation, keys, weights, aggregate, flags); @@ -513,6 +518,12 @@ public Task SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min, public IAsyncEnumerable SortedSetScanAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => Inner.SortedSetScanAsync(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); + public Task SortedSetUpdateAsync(RedisKey key, SortedSetEntry[] values, SortedSetWhen updateWhen = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetUpdateAsync(ToInner(key), values, updateWhen, flags); + + public Task SortedSetUpdateAsync(RedisKey key, RedisValue member, double score, SortedSetWhen updateWhen = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.SortedSetUpdateAsync(ToInner(key), member, score, updateWhen, flags); + public Task SortedSetPopAsync(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None) => Inner.SortedSetPopAsync(ToInner(key), order, flags); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 9f7168a51..8f20c4192 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -616,9 +616,11 @@ StackExchange.Redis.IDatabase.SetScan(StackExchange.Redis.RedisKey key, StackExc StackExchange.Redis.IDatabase.Sort(StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[]? get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.SortAndStore(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[]? get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.CommandFlags flags) -> bool -StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.SortedSetWhen when = StackExchange.Redis.SortedSetWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.CommandFlags flags) -> long -StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.SortedSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.SortedSetWhen when = StackExchange.Redis.SortedSetWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SortedSetCombine(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.SortedSetCombineWithScores(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.SortedSetEntry[]! StackExchange.Redis.IDatabase.SortedSetCombineAndStore(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long @@ -651,6 +653,8 @@ StackExchange.Redis.IDatabase.SortedSetScan(StackExchange.Redis.RedisKey key, St StackExchange.Redis.IDatabase.SortedSetScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable! StackExchange.Redis.IDatabase.SortedSetScore(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double? StackExchange.Redis.IDatabase.SortedSetScores(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> double?[]! +StackExchange.Redis.IDatabase.SortedSetUpdate(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.SortedSetWhen when = StackExchange.Redis.SortedSetWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.SortedSetUpdate(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.SortedSetWhen when = StackExchange.Redis.SortedSetWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StreamAcknowledge(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue messageId, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StreamAcknowledge(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue @@ -840,9 +844,11 @@ StackExchange.Redis.IDatabaseAsync.SetScanAsync(StackExchange.Redis.RedisKey key StackExchange.Redis.IDatabaseAsync.SortAndStoreAsync(StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[]? get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortAsync(StackExchange.Redis.RedisKey key, long skip = 0, long take = -1, StackExchange.Redis.Order order = StackExchange.Redis.Order.Ascending, StackExchange.Redis.SortType sortType = StackExchange.Redis.SortType.Numeric, StackExchange.Redis.RedisValue by = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue[]? get = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.SortedSetWhen when = StackExchange.Redis.SortedSetWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.SortedSetWhen when = StackExchange.Redis.SortedSetWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetCombineAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetCombineWithScoresAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey[]! keys, double[]? weights = null, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.Aggregate aggregate = StackExchange.Redis.Aggregate.Sum, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -874,6 +880,8 @@ StackExchange.Redis.IDatabaseAsync.SortedSetRemoveRangeByValueAsync(StackExchang StackExchange.Redis.IDatabaseAsync.SortedSetScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! StackExchange.Redis.IDatabaseAsync.SortedSetScoreAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SortedSetScoresAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! members, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetUpdateAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, double score, StackExchange.Redis.SortedSetWhen when = StackExchange.Redis.SortedSetWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.SortedSetUpdateAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.SortedSetWhen when = StackExchange.Redis.SortedSetWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue messageId, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -1502,6 +1510,12 @@ StackExchange.Redis.StreamPosition.StreamPosition(StackExchange.Redis.RedisKey k StackExchange.Redis.StringIndexType StackExchange.Redis.StringIndexType.Byte = 0 -> StackExchange.Redis.StringIndexType StackExchange.Redis.StringIndexType.Bit = 1 -> StackExchange.Redis.StringIndexType +StackExchange.Redis.SortedSetWhen +StackExchange.Redis.SortedSetWhen.Always = 0 -> StackExchange.Redis.SortedSetWhen +StackExchange.Redis.SortedSetWhen.Exists = 1 -> StackExchange.Redis.SortedSetWhen +StackExchange.Redis.SortedSetWhen.GreaterThan = 2 -> StackExchange.Redis.SortedSetWhen +StackExchange.Redis.SortedSetWhen.LessThan = 4 -> StackExchange.Redis.SortedSetWhen +StackExchange.Redis.SortedSetWhen.NotExists = 8 -> StackExchange.Redis.SortedSetWhen StackExchange.Redis.When StackExchange.Redis.When.Always = 0 -> StackExchange.Redis.When StackExchange.Redis.When.Exists = 1 -> StackExchange.Redis.When diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index e713eb80e..a785b502c 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1834,51 +1834,75 @@ public Task SortAsync(RedisKey key, long skip = 0, long take = -1, return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty(), server: server); } - public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags) + public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags) => + SortedSetAdd(key, member, score, SortedSetWhen.Always, flags); + + public bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) => + SortedSetAdd(key, member, score, SortedSetWhenExtensions.Parse(when), flags); + + public bool SortedSetAdd(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetSortedSetAddMessage(key, member, score, When.Always, flags); + var msg = GetSortedSetAddMessage(key, member, score, when, false, flags); return ExecuteSync(msg, ResultProcessor.Boolean); } - public bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) + public bool SortedSetUpdate(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetSortedSetAddMessage(key, member, score, when, flags); + var msg = GetSortedSetAddMessage(key, member, score, when, true, flags); return ExecuteSync(msg, ResultProcessor.Boolean); } - public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags flags) + public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags flags) => + SortedSetAdd(key, values, SortedSetWhen.Always, flags); + + public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => + SortedSetAdd(key, values, SortedSetWhenExtensions.Parse(when), flags); + + public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetSortedSetAddMessage(key, values, When.Always, flags); + var msg = GetSortedSetAddMessage(key, values, when, false, flags); return ExecuteSync(msg, ResultProcessor.Int64); } - public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) + public long SortedSetUpdate(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetSortedSetAddMessage(key, values, when, flags); + var msg = GetSortedSetAddMessage(key, values, when, true, flags); return ExecuteSync(msg, ResultProcessor.Int64); } - public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, CommandFlags flags) + public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, CommandFlags flags) => + SortedSetAddAsync(key, member, score, SortedSetWhen.Always, flags); + + public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) => + SortedSetAddAsync(key, member, score, SortedSetWhenExtensions.Parse(when), flags); + + public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetSortedSetAddMessage(key, member, score, When.Always, flags); + var msg = GetSortedSetAddMessage(key, member, score, when, false, flags); return ExecuteAsync(msg, ResultProcessor.Boolean); } - public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) + public Task SortedSetUpdateAsync(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetSortedSetAddMessage(key, member, score, when, flags); + var msg = GetSortedSetAddMessage(key, member, score, when, true, flags); return ExecuteAsync(msg, ResultProcessor.Boolean); } - public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, CommandFlags flags) + public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, CommandFlags flags) => + SortedSetAddAsync(key, values, SortedSetWhen.Always, flags); + + public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => + SortedSetAddAsync(key, values, SortedSetWhenExtensions.Parse(when), flags); + + public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetSortedSetAddMessage(key, values, When.Always, flags); + var msg = GetSortedSetAddMessage(key, values, when, false, flags); return ExecuteAsync(msg, ResultProcessor.Int64); } - public Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) + public Task SortedSetUpdateAsync(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetSortedSetAddMessage(key, values, when, flags); + var msg = GetSortedSetAddMessage(key, values, when, true, flags); return ExecuteAsync(msg, ResultProcessor.Int64); } @@ -3536,45 +3560,57 @@ private Message GetSetIntersectionLengthMessage(RedisKey[] keys, long limit = 0, return Message.Create(Database, flags, RedisCommand.SINTERCARD, values); } - private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double score, When when, CommandFlags flags) + private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double score, SortedSetWhen when, bool change, CommandFlags flags) { - WhenAlwaysOrExistsOrNotExists(when); - return when switch - { - When.Always => Message.Create(Database, flags, RedisCommand.ZADD, key, score, member), - When.NotExists => Message.Create(Database, flags, RedisCommand.ZADD, key, RedisLiterals.NX, score, member), - When.Exists => Message.Create(Database, flags, RedisCommand.ZADD, key, RedisLiterals.XX, score, member), - _ => throw new ArgumentOutOfRangeException(nameof(when)), - }; + RedisValue[] arr = new RedisValue[2 + when.CountBits() + (change? 1:0)]; + int index = 0; + if ((when & SortedSetWhen.NotExists) != 0) { + arr[index++] = RedisLiterals.NX; + } + if ((when & SortedSetWhen.Exists) != 0) { + arr[index++] = RedisLiterals.XX; + } + if ((when & SortedSetWhen.GreaterThan) != 0) { + arr[index++] = RedisLiterals.GT; + } + if ((when & SortedSetWhen.LessThan) != 0) { + arr[index++] = RedisLiterals.LT; + } + if (change) { + arr[index++] = RedisLiterals.CH; + } + arr[index++] = score; + arr[index++] = member; + return Message.Create(Database, flags, RedisCommand.ZADD, key, arr); } - private Message? GetSortedSetAddMessage(RedisKey key, SortedSetEntry[] values, When when, CommandFlags flags) + private Message? GetSortedSetAddMessage(RedisKey key, SortedSetEntry[] values, SortedSetWhen when, bool change, CommandFlags flags) { - WhenAlwaysOrExistsOrNotExists(when); if (values == null) throw new ArgumentNullException(nameof(values)); switch (values.Length) { case 0: return null; case 1: - return GetSortedSetAddMessage(key, values[0].element, values[0].score, when, flags); + return GetSortedSetAddMessage(key, values[0].element, values[0].score, when, change, flags); default: - RedisValue[] arr; + RedisValue[] arr = new RedisValue[(values.Length * 2) + when.CountBits() + (change? 1:0)]; int index = 0; - switch (when) - { - case When.Always: - arr = new RedisValue[values.Length * 2]; - break; - case When.NotExists: - arr = new RedisValue[(values.Length * 2) + 1]; - arr[index++] = RedisLiterals.NX; - break; - case When.Exists: - arr = new RedisValue[(values.Length * 2) + 1]; - arr[index++] = RedisLiterals.XX; - break; - default: throw new ArgumentOutOfRangeException(nameof(when)); + if ((when & SortedSetWhen.NotExists) != 0) { + arr[index++] = RedisLiterals.NX; + } + if ((when & SortedSetWhen.Exists) != 0) { + arr[index++] = RedisLiterals.XX; + } + if ((when & SortedSetWhen.GreaterThan) != 0) { + arr[index++] = RedisLiterals.GT; } + if ((when & SortedSetWhen.LessThan) != 0) { + arr[index++] = RedisLiterals.LT; + } + if (change) { + arr[index++] = RedisLiterals.CH; + } + for (int i = 0; i < values.Length; i++) { arr[index++] = values[i].score; diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 9747a141a..cd9395368 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -55,6 +55,7 @@ public static readonly RedisValue BYLEX = "BYLEX", BYSCORE = "BYSCORE", BYTE = "BYTE", + CH = "CH", CHANNELS = "CHANNELS", COPY = "COPY", COUNT = "COUNT", diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs index 95c11fad2..d3fba4d08 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs @@ -785,6 +785,14 @@ public void SortedSetAdd_2() mock.Verify(_ => _.SortedSetAdd("prefix:key", values, When.Exists, CommandFlags.None)); } + [Fact] + public void SortedSetAdd_3() + { + SortedSetEntry[] values = Array.Empty(); + wrapper.SortedSetAdd("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); + mock.Verify(_ => _.SortedSetAdd("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None)); + } + [Fact] public void SortedSetCombine() { @@ -987,6 +995,14 @@ public void SortedSetScore_Multiple() mock.Verify(_ => _.SortedSetScores("prefix:key", new RedisValue[] { "member1", "member2" }, CommandFlags.None)); } + [Fact] + public void SortedSetUpdate() + { + SortedSetEntry[] values = Array.Empty(); + wrapper.SortedSetUpdate("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); + mock.Verify(_ => _.SortedSetUpdate("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None)); + } + [Fact] public void StreamAcknowledge_1() { diff --git a/tests/StackExchange.Redis.Tests/OverloadCompat.cs b/tests/StackExchange.Redis.Tests/OverloadCompat.cs index 81d8bad82..f07b48933 100644 --- a/tests/StackExchange.Redis.Tests/OverloadCompat.cs +++ b/tests/StackExchange.Redis.Tests/OverloadCompat.cs @@ -66,6 +66,8 @@ public async Task StringBitCount() var db = conn.GetDatabase(); var key = Me(); + var flags = CommandFlags.None; + db.KeyDelete(key, flags: CommandFlags.FireAndForget); db.StringSet(key, "foobar", flags: CommandFlags.FireAndForget); @@ -76,7 +78,6 @@ public async Task StringBitCount() db.StringBitCount(key, end: 1); db.StringBitCount(key, start: 1, end: 1); - var flags = CommandFlags.None; db.StringBitCount(key, flags: flags); db.StringBitCount(key, 0, 0, flags); db.StringBitCount(key, 1, flags: flags); @@ -112,6 +113,8 @@ public async Task StringBitPosition() var db = conn.GetDatabase(); var key = Me(); + var flags = CommandFlags.None; + db.KeyDelete(key, flags: CommandFlags.FireAndForget); db.StringSet(key, "foo", flags: CommandFlags.FireAndForget); @@ -124,7 +127,6 @@ public async Task StringBitPosition() db.StringBitPosition(key, bit: true, start: 1, end: 1); db.StringBitPosition(key, true, start: 1, end: 1); - var flags = CommandFlags.None; db.StringBitPosition(key, true, flags: flags); db.StringBitPosition(key, true, 1, 3, flags); db.StringBitPosition(key, true, 1, flags: flags); @@ -155,6 +157,61 @@ public async Task StringBitPosition() await db.StringBitPositionAsync(key, true, start: 1, end: 1, flags: flags); } + [Fact] + public async Task SortedSetAdd() + { + using var conn = Create(); + var db = conn.GetDatabase(); + RedisKey key = Me(); + RedisValue val = "myval"; + var score = 1.0d; + var values = new SortedSetEntry[]{new SortedSetEntry(val, score)}; + var when = When.Exists; + var flags = CommandFlags.None; + + db.SortedSetAdd(key, val, score); + db.SortedSetAdd(key, val, score, when); + db.SortedSetAdd(key, val, score, when: when); + db.SortedSetAdd(key, val, score, flags); + db.SortedSetAdd(key, val, score, flags: flags); + db.SortedSetAdd(key, val, score, when, flags); + db.SortedSetAdd(key, val, score, when, flags: flags); + db.SortedSetAdd(key, val, score, when: when, flags); + db.SortedSetAdd(key, val, score, when: when, flags: flags); + + db.SortedSetAdd(key, values); + db.SortedSetAdd(key, values, when); + db.SortedSetAdd(key, values, when: when); + db.SortedSetAdd(key, values, flags); + db.SortedSetAdd(key, values, flags: flags); + db.SortedSetAdd(key, values, when, flags); + db.SortedSetAdd(key, values, when, flags: flags); + db.SortedSetAdd(key, values, when: when, flags); + db.SortedSetAdd(key, values, when: when, flags: flags); + + // Async + + await db.SortedSetAddAsync(key, val, score); + await db.SortedSetAddAsync(key, val, score, when); + await db.SortedSetAddAsync(key, val, score, when: when); + await db.SortedSetAddAsync(key, val, score, flags); + await db.SortedSetAddAsync(key, val, score, flags: flags); + await db.SortedSetAddAsync(key, val, score, when, flags); + await db.SortedSetAddAsync(key, val, score, when, flags: flags); + await db.SortedSetAddAsync(key, val, score, when: when, flags); + await db.SortedSetAddAsync(key, val, score, when: when, flags: flags); + + await db.SortedSetAddAsync(key, values); + await db.SortedSetAddAsync(key, values, when); + await db.SortedSetAddAsync(key, values, when: when); + await db.SortedSetAddAsync(key, values, flags); + await db.SortedSetAddAsync(key, values, flags: flags); + await db.SortedSetAddAsync(key, values, when, flags); + await db.SortedSetAddAsync(key, values, when, flags: flags); + await db.SortedSetAddAsync(key, values, when: when, flags); + await db.SortedSetAddAsync(key, values, when: when, flags: flags); + } + [Fact] public async Task StringSet() { diff --git a/tests/StackExchange.Redis.Tests/SortedSetWhen.cs b/tests/StackExchange.Redis.Tests/SortedSetWhen.cs new file mode 100644 index 000000000..a26f2d5ba --- /dev/null +++ b/tests/StackExchange.Redis.Tests/SortedSetWhen.cs @@ -0,0 +1,43 @@ +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public class SortedSetWhenTest : TestBase +{ + public SortedSetWhenTest(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public void GreaterThanLessThan() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + var member = "a"; + db.KeyDelete(key, CommandFlags.FireAndForget); + db.SortedSetAdd(key, member, 2); + + Assert.True(db.SortedSetUpdate(key, member, 5, when: SortedSetWhen.GreaterThan)); + Assert.False(db.SortedSetUpdate(key, member, 1, when: SortedSetWhen.GreaterThan)); + Assert.True(db.SortedSetUpdate(key, member, 1, when: SortedSetWhen.LessThan)); + Assert.False(db.SortedSetUpdate(key, member, 5, when: SortedSetWhen.LessThan)); + } + + [Fact] + public void IllegalCombinations() + { + using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + var member = "a"; + db.KeyDelete(key, CommandFlags.FireAndForget); + + Assert.Throws(() => db.SortedSetAdd(key, member, 5, when: SortedSetWhen.LessThan | SortedSetWhen.GreaterThan)); + Assert.Throws(() => db.SortedSetAdd(key, member, 5, when: SortedSetWhen.Exists | SortedSetWhen.NotExists)); + Assert.Throws(() => db.SortedSetAdd(key, member, 5, when: SortedSetWhen.GreaterThan | SortedSetWhen.NotExists)); + Assert.Throws(() => db.SortedSetAdd(key, member, 5, when: SortedSetWhen.LessThan | SortedSetWhen.NotExists)); + } +} diff --git a/tests/StackExchange.Redis.Tests/SortedSets.cs b/tests/StackExchange.Redis.Tests/SortedSets.cs index 94c21a996..6d10f7bf3 100644 --- a/tests/StackExchange.Redis.Tests/SortedSets.cs +++ b/tests/StackExchange.Redis.Tests/SortedSets.cs @@ -1366,4 +1366,23 @@ public async Task SortedSetScoresMultiple_ReturnsScoresAndNullItemsAsync() Assert.Equal((double)1.75, scores[2]); Assert.Equal(2, scores[3]); } + + [Fact] + public async Task SortedSetUpdate() + { + using var conn = Create(require: RedisFeatures.v3_0_0); + + var db = conn.GetDatabase(); + var key = Me(); + var member = "a"; + var values = new SortedSetEntry[] {new SortedSetEntry(member, 5)}; + db.KeyDelete(key, CommandFlags.FireAndForget); + db.SortedSetAdd(key, member, 2); + + Assert.True(db.SortedSetUpdate(key, member, 1)); + Assert.Equal(1, db.SortedSetUpdate(key, values)); + + Assert.True(await db.SortedSetUpdateAsync(key, member, 1)); + Assert.Equal(1,await db.SortedSetUpdateAsync(key, values)); + } } diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs index d63921d70..2dc09247d 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs @@ -732,6 +732,14 @@ public void SortedSetAddAsync_2() mock.Verify(_ => _.SortedSetAddAsync("prefix:key", values, When.Exists, CommandFlags.None)); } + [Fact] + public void SortedSetAddAsync_3() + { + SortedSetEntry[] values = Array.Empty(); + wrapper.SortedSetAddAsync("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); + mock.Verify(_ => _.SortedSetAddAsync("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None)); + } + [Fact] public void SortedSetCombineAsync() { @@ -920,6 +928,14 @@ public void SortedSetScoreAsync_Multiple() mock.Verify(_ => _.SortedSetScoresAsync("prefix:key", new RedisValue[] { "member1", "member2" }, CommandFlags.None)); } + [Fact] + public void SortedSetUpdateAsync() + { + SortedSetEntry[] values = Array.Empty(); + wrapper.SortedSetUpdateAsync("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); + mock.Verify(_ => _.SortedSetUpdateAsync("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None)); + } + [Fact] public void StreamAcknowledgeAsync_1() { From 8c9cb1b02049aa89f6af685d9dd52a33b3151224 Mon Sep 17 00:00:00 2001 From: nielsderdaele Date: Tue, 14 Jun 2022 16:30:48 +0200 Subject: [PATCH 160/435] Fix incorrect HashSlot calculation for XREAD and XREADGROUP commands (#2093) I have created a fix for the incorrect HashSlot calculation for the XREAD and XREADGROUP commands (#2086) Since these differ a lot from other messages I have created some new CommandMessage classes: - SingleStreamReadGroupCommandMessage - MultiStreamReadGroupCommandMessage - SingleStreamReadCommandMessage - MultiStreamReadCommandMessage The single CommandMessage classes could be removed by only using the multi command messages, but then we have to create a new StreamPostion array and a new StreamPostion object. I wasn't sure which option too choose, so feel free to give input on this (GC vs duplicate code). There is also some code duplication between the Read and ReadGroup Command messages as the ReadGroup command is very similar to the XREAD but has some additional values in the front of the command (group, consumer, noack). A base class could be created as well to reduce some of the duplication (calculating the hashslot in case of multiple streams and writing the streams part of the commands). The method CommandAndKey is also not overriden for these Messages as I wasn't which argument of the command to include in the string. Co-authored-by: Niels Derdaele Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/RedisDatabase.cs | 323 ++++++++++++++--------- 2 files changed, 206 insertions(+), 118 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 6eee8c388..05f9c54c3 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -27,6 +27,7 @@ - Adds: Support for `ZMPOP` with `.SortedSetPop()`/`.SortedSetPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094)) - Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2095)) - Fix [#2071](https://github.com/StackExchange/StackExchange.Redis/issues/2071): Add `.StringSet()`/`.StringSetAsync()` overloads for source compat broken for 1 case in 2.5.61 ([#2098 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2098)) +- Fix [#2086](https://github.com/StackExchange/StackExchange.Redis/issues/2086): Correct HashSlot calculations for `XREAD` and `XREADGROUP` commands ([#2093 by nielsderdaele](https://github.com/StackExchange/StackExchange.Redis/pull/2093)) - Adds: Support for `LCS` with `.StringLongestCommonSubsequence()`/`.StringLongestCommonSubsequence()`, `.StringLongestCommonSubsequenceLength()`/`.StringLongestCommonSubsequenceLengthAsync()`, and `.StringLongestCommonSubsequenceWithMatches()`/`.StringLongestCommonSubsequenceWithMatchesAsync()` ([#2104 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2104)) - Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105)) - Performance: Avoids allocations when computing cluster hash slots or testing key equality ([#2110 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2110)) diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index a785b502c..38338390c 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -3416,109 +3416,161 @@ private static RedisValue GetLexRange(RedisValue value, Exclude exclude, bool is return result; } - private Message GetMultiStreamReadGroupMessage(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, bool noAck, CommandFlags flags) + private Message GetMultiStreamReadGroupMessage(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, bool noAck, CommandFlags flags) => + new MultiStreamReadGroupCommandMessage(Database, + flags, + streamPositions, + groupName, + consumerName, + countPerStream, + noAck); + + private sealed class MultiStreamReadGroupCommandMessage : Message // XREADGROUP with multiple stream. Example: XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2 { - // Example: XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2 - if (streamPositions == null) throw new ArgumentNullException(nameof(streamPositions)); - if (streamPositions.Length == 0) throw new ArgumentOutOfRangeException(nameof(streamPositions), "streamOffsetPairs must contain at least one item."); + private readonly StreamPosition[] streamPositions; + private readonly RedisValue groupName; + private readonly RedisValue consumerName; + private readonly int? countPerStream; + private readonly bool noAck; + private readonly int argCount; - if (countPerStream.HasValue && countPerStream <= 0) + public MultiStreamReadGroupCommandMessage(int db, CommandFlags flags, StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, bool noAck) + : base(db, flags, RedisCommand.XREADGROUP) { - throw new ArgumentOutOfRangeException(nameof(countPerStream), "countPerStream must be greater than 0."); - } - - var values = new RedisValue[ - 4 // Room for GROUP groupName consumerName & STREAMS - + (streamPositions.Length * 2) // Enough room for the stream keys and associated IDs. - + (countPerStream.HasValue ? 2 : 0) // Room for "COUNT num" or 0 if countPerStream is null. - + (noAck ? 1 : 0)]; // Allow for the NOACK subcommand. + if (streamPositions == null) throw new ArgumentNullException(nameof(streamPositions)); + if (streamPositions.Length == 0) throw new ArgumentOutOfRangeException(nameof(streamPositions), "streamOffsetPairs must contain at least one item."); + for (int i = 0; i < streamPositions.Length; i++) + { + streamPositions[i].Key.AssertNotNull(); + } - var offset = 0; + if (countPerStream.HasValue && countPerStream <= 0) + { + throw new ArgumentOutOfRangeException(nameof(countPerStream), "countPerStream must be greater than 0."); + } - values[offset++] = StreamConstants.Group; - values[offset++] = groupName; - values[offset++] = consumerName; + groupName.AssertNotNull(); + consumerName.AssertNotNull(); + + this.streamPositions = streamPositions; + this.groupName = groupName; + this.consumerName = consumerName; + this.countPerStream = countPerStream; + this.noAck = noAck; - if (countPerStream.HasValue) - { - values[offset++] = StreamConstants.Count; - values[offset++] = countPerStream; + argCount = 4 // Room for GROUP groupName consumerName & STREAMS + + (streamPositions.Length * 2) // Enough room for the stream keys and associated IDs. + + (countPerStream.HasValue ? 2 : 0) // Room for "COUNT num" or 0 if countPerStream is null. + + (noAck ? 1 : 0); // Allow for the NOACK subcommand. + } - if (noAck) + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) { - values[offset++] = StreamConstants.NoAck; + int slot = ServerSelectionStrategy.NoSlot; + for (int i = 0; i < streamPositions.Length; i++) + { + slot = serverSelectionStrategy.CombineSlot(slot, streamPositions[i].Key); + } + return slot; } - values[offset++] = StreamConstants.Streams; + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, argCount); + physical.WriteBulkString(StreamConstants.Group); + physical.WriteBulkString(groupName); + physical.WriteBulkString(consumerName); - var pairCount = streamPositions.Length; + if (countPerStream.HasValue) + { + physical.WriteBulkString(StreamConstants.Count); + physical.WriteBulkString(countPerStream.Value); + } - for (var i = 0; i < pairCount; i++) - { - values[offset] = streamPositions[i].Key.AsRedisValue(); - values[offset + pairCount] = StreamPosition.Resolve(streamPositions[i].Position, RedisCommand.XREADGROUP); + if (noAck) + { + physical.WriteBulkString(StreamConstants.NoAck); + } - offset++; - } + physical.WriteBulkString(StreamConstants.Streams); + for (int i = 0; i < streamPositions.Length; i++) + { + physical.Write(streamPositions[i].Key); + } + for (int i = 0; i < streamPositions.Length; i++) + { + physical.WriteBulkString(StreamPosition.Resolve(streamPositions[i].Position, RedisCommand.XREADGROUP)); + } + } - return Message.Create(Database, flags, RedisCommand.XREADGROUP, values); + public override int ArgCount => argCount; } - private Message GetMultiStreamReadMessage(StreamPosition[] streamPositions, int? countPerStream, CommandFlags flags) - { - // Example: XREAD COUNT 2 STREAMS mystream writers 0-0 0-0 + private Message GetMultiStreamReadMessage(StreamPosition[] streamPositions, int? countPerStream, CommandFlags flags) => + new MultiStreamReadCommandMessage(Database, flags, streamPositions, countPerStream); - if (streamPositions == null) throw new ArgumentNullException(nameof(streamPositions)); - if (streamPositions.Length == 0) throw new ArgumentOutOfRangeException(nameof(streamPositions), "streamOffsetPairs must contain at least one item."); + private sealed class MultiStreamReadCommandMessage : Message // XREAD with multiple stream. Example: XREAD COUNT 2 STREAMS mystream writers 0-0 0-0 + { + private readonly StreamPosition[] streamPositions; + private readonly int? countPerStream; + private readonly int argCount; - if (countPerStream.HasValue && countPerStream <= 0) + public MultiStreamReadCommandMessage(int db, CommandFlags flags, StreamPosition[] streamPositions, int? countPerStream) + : base(db, flags, RedisCommand.XREAD) { - throw new ArgumentOutOfRangeException(nameof(countPerStream), "countPerStream must be greater than 0."); - } + if (streamPositions == null) throw new ArgumentNullException(nameof(streamPositions)); + if (streamPositions.Length == 0) throw new ArgumentOutOfRangeException(nameof(streamPositions), "streamOffsetPairs must contain at least one item."); + for (int i = 0; i < streamPositions.Length; i++) + { + streamPositions[i].Key.AssertNotNull(); + } - var values = new RedisValue[ - 1 // Streams keyword. - + (streamPositions.Length * 2) // Room for the stream names and the ID after which to begin reading. - + (countPerStream.HasValue ? 2 : 0)]; // Room for "COUNT num" or 0 if countPerStream is null. + if (countPerStream.HasValue && countPerStream <= 0) + { + throw new ArgumentOutOfRangeException(nameof(countPerStream), "countPerStream must be greater than 0."); + } - var offset = 0; + this.streamPositions = streamPositions; + this.countPerStream = countPerStream; - if (countPerStream.HasValue) - { - values[offset++] = StreamConstants.Count; - values[offset++] = countPerStream; + argCount = 1 // Streams keyword. + + (countPerStream.HasValue ? 2 : 0) // Room for "COUNT num" or 0 if countPerStream is null. + + (streamPositions.Length * 2); // Room for the stream names and the ID after which to begin reading. } - values[offset++] = StreamConstants.Streams; - - // Write the stream names and the message IDs from which to read for the associated stream. Each pair - // will be separated by an offset of the index of the stream name plus the pair count. - - /* - * [0] = COUNT - * [1] = 2 - * [3] = STREAMS - * [4] = stream1 - * [5] = stream2 - * [6] = stream3 - * [7] = id1 - * [8] = id2 - * [9] = id3 - * - * */ - - var pairCount = streamPositions.Length; + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) + { + int slot = ServerSelectionStrategy.NoSlot; + for (int i = 0; i < streamPositions.Length; i++) + { + slot = serverSelectionStrategy.CombineSlot(slot, streamPositions[i].Key); + } + return slot; + } - for (var i = 0; i < pairCount; i++) + protected override void WriteImpl(PhysicalConnection physical) { - values[offset] = streamPositions[i].Key.AsRedisValue(); - values[offset + pairCount] = StreamPosition.Resolve(streamPositions[i].Position, RedisCommand.XREAD); + physical.WriteHeader(Command, argCount); - offset++; + if (countPerStream.HasValue) + { + physical.WriteBulkString(StreamConstants.Count); + physical.WriteBulkString(countPerStream.Value); + } + + physical.WriteBulkString(StreamConstants.Streams); + for (int i = 0; i < streamPositions.Length; i++) + { + physical.Write(streamPositions[i].Key); + } + for (int i = 0; i < streamPositions.Length; i++) + { + physical.WriteBulkString(StreamPosition.Resolve(streamPositions[i].Position, RedisCommand.XREADGROUP)); + } } - return Message.Create(Database, flags, RedisCommand.XREAD, values); + public override int ArgCount => argCount; } private static RedisValue GetRange(double value, Exclude exclude, bool isStart) @@ -4107,71 +4159,106 @@ private Message GetStreamRangeMessage(RedisKey key, RedisValue? minId, RedisValu values); } - private Message GetStreamReadGroupMessage(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue afterId, int? count, bool noAck, CommandFlags flags) + private Message GetStreamReadGroupMessage(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue afterId, int? count, bool noAck, CommandFlags flags) => + new SingleStreamReadGroupCommandMessage(Database, flags, key, groupName, consumerName, afterId, count, noAck); + + private sealed class SingleStreamReadGroupCommandMessage : Message.CommandKeyBase // XREADGROUP with single stream. eg XREADGROUP GROUP mygroup Alice COUNT 1 STREAMS mystream > { - // Example: > XREADGROUP GROUP mygroup Alice COUNT 1 STREAMS mystream > - if (count.HasValue && count <= 0) + private readonly RedisValue groupName; + private readonly RedisValue consumerName; + private readonly RedisValue afterId; + private readonly int? count; + private readonly bool noAck; + private readonly int argCount; + + public SingleStreamReadGroupCommandMessage(int db, CommandFlags flags, RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue afterId, int? count, bool noAck) + : base(db, flags, RedisCommand.XREADGROUP, key) { - throw new ArgumentOutOfRangeException(nameof(count), "count must be greater than 0."); - } + if (count.HasValue && count <= 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "count must be greater than 0."); + } - var totalValueCount = 6 + (count.HasValue ? 2 : 0) + (noAck ? 1 : 0); - var values = new RedisValue[totalValueCount]; + groupName.AssertNotNull(); + consumerName.AssertNotNull(); + afterId.AssertNotNull(); - var offset = 0; + this.groupName = groupName; + this.consumerName = consumerName; + this.afterId = afterId; + this.count = count; + this.noAck = noAck; + argCount = 6 + (count.HasValue ? 2 : 0) + (noAck ? 1 : 0); + } - values[offset++] = StreamConstants.Group; - values[offset++] = groupName; - values[offset++] = consumerName; + protected override void WriteImpl(PhysicalConnection physical) { + physical.WriteHeader(Command, argCount); + physical.WriteBulkString(StreamConstants.Group); + physical.WriteBulkString(groupName); + physical.WriteBulkString(consumerName); - if (count.HasValue) - { - values[offset++] = StreamConstants.Count; - values[offset++] = count.Value; - } + if (count.HasValue) + { + physical.WriteBulkString(StreamConstants.Count); + physical.WriteBulkString(count.Value); + } - if (noAck) - { - values[offset++] = StreamConstants.NoAck; - } + if (noAck) + { + physical.WriteBulkString(StreamConstants.NoAck); + } - values[offset++] = StreamConstants.Streams; - values[offset++] = key.AsRedisValue(); - values[offset] = afterId; + physical.WriteBulkString(StreamConstants.Streams); + physical.Write(Key); + physical.WriteBulkString(afterId); + } - return Message.Create(Database, - flags, - RedisCommand.XREADGROUP, - values); + public override int ArgCount => argCount; } - private Message GetSingleStreamReadMessage(RedisKey key, RedisValue afterId, int? count, CommandFlags flags) + private Message GetSingleStreamReadMessage(RedisKey key, RedisValue afterId, int? count, CommandFlags flags) => + new SingleStreamReadCommandMessage(Database, flags, key, afterId, count); + + private sealed class SingleStreamReadCommandMessage : Message.CommandKeyBase // XREAD with a single stream. Example: XREAD COUNT 2 STREAMS mystream 0-0 { - if (count.HasValue && count <= 0) + private readonly RedisValue afterId; + private readonly int? count; + private readonly int argCount; + + public SingleStreamReadCommandMessage(int db, CommandFlags flags, RedisKey key, RedisValue afterId, int? count) + : base(db, flags, RedisCommand.XREAD, key) { - throw new ArgumentOutOfRangeException(nameof(count), "count must be greater than 0."); - } + if (count.HasValue && count <= 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "count must be greater than 0."); + } - var values = new RedisValue[3 + (count.HasValue ? 2 : 0)]; - var offset = 0; + afterId.AssertNotNull(); - if (count.HasValue) - { - values[offset++] = StreamConstants.Count; - values[offset++] = count.Value; + this.afterId = afterId; + this.count = count; + argCount = count.HasValue ? 5 : 3; } - values[offset++] = StreamConstants.Streams; - values[offset++] = key.AsRedisValue(); - values[offset] = afterId; + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, argCount); - // Example: > XREAD COUNT 2 STREAMS writers 1526999352406-0 - return Message.Create(Database, - flags, - RedisCommand.XREAD, - values); + if (count.HasValue) + { + physical.WriteBulkString(StreamConstants.Count); + physical.WriteBulkString(count.Value); + } + + physical.WriteBulkString(StreamConstants.Streams); + physical.Write(Key); + physical.WriteBulkString(afterId); + } + + public override int ArgCount => argCount; } + private Message GetStreamTrimMessage(RedisKey key, int maxLength, bool useApproximateMaxLength, CommandFlags flags) { if (maxLength < 0) From 83b3de87bcf18ad6824084a2cbccecc613c8f34c Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 14 Jun 2022 10:44:51 -0400 Subject: [PATCH 161/435] Bumping version by 1 so we don't collide with 2.5.43 so closely From 5f1630e77eb3fc000dbc9de742bb7f052aeb7d1b Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 14 Jun 2022 10:55:33 -0400 Subject: [PATCH 162/435] Version release notes --- docs/ReleaseNotes.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 05f9c54c3..2495c7491 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,6 +1,6 @@ # Release Notes -## Unreleased +## 2.6.45 - Adds: [Nullable reference type](https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references) annotations ([#2041 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2041)) - Adds annotations themselves for nullability to everything in the library @@ -38,7 +38,6 @@ - Adds: Support for `GT`, `LT`, and `CH` on `ZADD` with `.SortedSetAdd()`/`.SortedSetAddAsync()` and `.SortedSetUpdate()`/`.SortedSetUpdateAsync()` ([#2136 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2136)) - Adds: Support for `COMMAND COUNT`, `COMMAND GETKEYS`, and `COMMAND LIST`, with `.CommandCount()`/`.CommandCountAsync()`, `.CommandGetKeys()`/`.CommandGetKeysAsync()`, and `.CommandList()`/`.CommandListAsync()` ([#2143 by shacharPash](https://github.com/StackExchange/StackExchange.Redis/pull/2143)) - ## 2.5.61 - Adds: `GETEX` support with `.StringGetSetExpiry()`/`.StringGetSetExpiryAsync()` ([#1743 by benbryant0](https://github.com/StackExchange/StackExchange.Redis/pull/1743)) From bc0c5a28913b8a29b92751726820113f42efc20a Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 21 Jun 2022 15:04:07 +0100 Subject: [PATCH 163/435] PrepareScript should work for parameterless scripts; fix #2164 (#2166) * PrepareScript should work for parameterless scripts; fix #2164 * update link in release notes * tyop --- docs/ReleaseNotes.md | 4 ++ .../ScriptParameterMapper.cs | 17 ++---- .../Issues/Issue2164.cs | 55 +++++++++++++++++++ 3 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/Issues/Issue2164.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 2495c7491..0affae5cb 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,5 +1,9 @@ # Release Notes +## Pending + +- Fix: [#2164](https://github.com/StackExchange/StackExchange.Redis/issues/2164): fix `LuaScript.Prepare` for scripts that don't have parameters ([#2166 by MarcGravell](https://github.com/StackExchange/StackExchange.Redis/pull/2166)) + ## 2.6.45 - Adds: [Nullable reference type](https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references) annotations ([#2041 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2041)) diff --git a/src/StackExchange.Redis/ScriptParameterMapper.cs b/src/StackExchange.Redis/ScriptParameterMapper.cs index 17920bcb5..2c0e76314 100644 --- a/src/StackExchange.Redis/ScriptParameterMapper.cs +++ b/src/StackExchange.Redis/ScriptParameterMapper.cs @@ -26,13 +26,12 @@ public ScriptParameters(RedisKey[] keys, RedisValue[] args) private static readonly Regex ParameterExtractor = new Regex(@"@(? ([a-z]|_) ([a-z]|_|\d)*)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - private static bool TryExtractParameters(string script, [NotNullWhen(true)] out string[]? parameters) + private static string[] ExtractParameters(string script) { var ps = ParameterExtractor.Matches(script); if (ps.Count == 0) { - parameters = null; - return false; + return Array.Empty(); } var ret = new HashSet(); @@ -56,8 +55,7 @@ private static bool TryExtractParameters(string script, [NotNullWhen(true)] out if (!ret.Contains(n)) ret.Add(n); } - parameters = ret.ToArray(); - return true; + return ret.ToArray(); } private static string MakeOrdinalScriptWithoutKeys(string rawScript, string[] args) @@ -137,12 +135,9 @@ static ScriptParameterMapper() /// The script to prepare. public static LuaScript PrepareScript(string script) { - if (TryExtractParameters(script, out var ps)) - { - var ordinalScript = MakeOrdinalScriptWithoutKeys(script, ps); - return new LuaScript(script, ordinalScript, ps); - } - throw new ArgumentException("Count not parse script: " + script); + var ps = ExtractParameters(script); + var ordinalScript = MakeOrdinalScriptWithoutKeys(script, ps); + return new LuaScript(script, ordinalScript, ps); } private static readonly HashSet ConvertableTypes = new() diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2164.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2164.cs new file mode 100644 index 000000000..8ffd4f21a --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2164.cs @@ -0,0 +1,55 @@ +namespace StackExchange.Redis.Tests.Issues +{ + public class Issue2164 + { + [Fact] + public void LoadSimpleScript() + { + LuaScript.Prepare("return 42"); + } + [Fact] + public void LoadComplexScript() + { + LuaScript.Prepare(@" +------------------------------------------------------------------------------- +-- API definitions +------------------------------------------------------------------------------- +local MessageStoreAPI = {} + +MessageStoreAPI.confirmPendingDelivery = function(smscMessageId, smscDeliveredAt, smscMessageState) + local messageId = redis.call('hget', ""smId:"" .. smscMessageId, 'mId') + if not messageId then + return nil + end + -- delete pending delivery + redis.call('del', ""smId:"" .. smscMessageId) + + local mIdK = 'm:'..messageId + + local result = redis.call('hsetnx', mIdK, 'sState', smscMessageState) + if result == 1 then + redis.call('hset', mIdK, 'sDlvAt', smscDeliveredAt) + redis.call('zrem', ""msg.validUntil"", messageId) + return redis.call('hget', mIdK, 'payload') + else + return nil + end +end + + +------------------------------------------------------------------------------- +-- Function lookup +------------------------------------------------------------------------------- + +-- None of the function calls accept keys +if #KEYS > 0 then error('No Keys should be provided') end + +-- The first argument must be the function that we intend to call, and it must +-- exist +local command_name = assert(table.remove(ARGV, 1), 'Must provide a command as first argument') +local command = assert(MessageStoreAPI[command_name], 'Unknown command ' .. command_name) + +return command(unpack(ARGV))"); + } + } +} From 0ebe530610db182162a5616d753edb4b5ed28f33 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 28 Jun 2022 14:14:18 +0100 Subject: [PATCH 164/435] URGENT Fix error in batch/transaction handling (#2177) * PrepareScript should work for parameterless scripts; fix #2164 * update link in release notes * tyop * Batch/Transaction need to override new ExecuteAsync API; fix #2167 fix #2176 * Maintain correctness on NRTs and asyncState Previously the F+F case was getting a null default rather than the pass default value - this corrects that as well. It is not DRY across classes, but let's get this fix in ASAP then address that. * test both batch and transaction * bump Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/RedisBatch.cs | 23 +++++ src/StackExchange.Redis/RedisTransaction.cs | 32 ++++++- .../Issues/Issue2176.cs | 83 +++++++++++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 tests/StackExchange.Redis.Tests/Issues/Issue2176.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 0affae5cb..2a6a13092 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -2,6 +2,7 @@ ## Pending +- URGENT Fix: [#2167](https://github.com/StackExchange/StackExchange.Redis/issues/2167), [#2176](https://github.com/StackExchange/StackExchange.Redis/issues/2176): fix error in batch/transaction handling that can result in out-of-order instructions ([#2177 by MarcGravell](https://github.com/StackExchange/StackExchange.Redis/pull/2177)) - Fix: [#2164](https://github.com/StackExchange/StackExchange.Redis/issues/2164): fix `LuaScript.Prepare` for scripts that don't have parameters ([#2166 by MarcGravell](https://github.com/StackExchange/StackExchange.Redis/pull/2166)) ## 2.6.45 diff --git a/src/StackExchange.Redis/RedisBatch.cs b/src/StackExchange.Redis/RedisBatch.cs index 3a4815a40..6c9727d0d 100644 --- a/src/StackExchange.Redis/RedisBatch.cs +++ b/src/StackExchange.Redis/RedisBatch.cs @@ -63,6 +63,29 @@ public void Execute() } } + internal override Task ExecuteAsync(Message? message, ResultProcessor? processor, T defaultValue, ServerEndPoint? server = null) + { + if (message == null) return CompletedTask.FromDefault(defaultValue, asyncState); + multiplexer.CheckMessage(message); + + // prepare the inner command as a task + Task task; + if (message.IsFireAndForget) + { + task = CompletedTask.FromDefault(defaultValue, null); // F+F explicitly does not get async-state + } + else + { + var source = TaskResultBox.Create(out var tcs, asyncState); + task = tcs.Task; + message.SetSource(source, processor); + } + + // store it + (pending ??= new List()).Add(message); + return task!; + } + internal override Task ExecuteAsync(Message? message, ResultProcessor? processor, ServerEndPoint? server = null) where T : default { if (message == null) return CompletedTask.Default(asyncState); diff --git a/src/StackExchange.Redis/RedisTransaction.cs b/src/StackExchange.Redis/RedisTransaction.cs index b1c901552..32e7bfb1d 100644 --- a/src/StackExchange.Redis/RedisTransaction.cs +++ b/src/StackExchange.Redis/RedisTransaction.cs @@ -56,6 +56,30 @@ public Task ExecuteAsync(CommandFlags flags) return base.ExecuteAsync(msg, proc); // need base to avoid our local wrapping override } + internal override Task ExecuteAsync(Message? message, ResultProcessor? processor, T defaultValue, ServerEndPoint? server = null) + { + if (message == null) return CompletedTask.FromDefault(defaultValue, asyncState); + multiplexer.CheckMessage(message); + + multiplexer.Trace("Wrapping " + message.Command, "Transaction"); + // prepare the inner command as a task + Task task; + if (message.IsFireAndForget) + { + task = CompletedTask.FromDefault(defaultValue, null); // F+F explicitly does not get async-state + } + else + { + var source = TaskResultBox.Create(out var tcs, asyncState); + message.SetSource(source, processor); + task = tcs.Task; + } + + QueueMessage(message); + + return task; + } + internal override Task ExecuteAsync(Message? message, ResultProcessor? processor, ServerEndPoint? server = null) where T : default { if (message == null) return CompletedTask.Default(asyncState); @@ -75,6 +99,13 @@ public Task ExecuteAsync(CommandFlags flags) task = tcs.Task; } + QueueMessage(message); + + return task; + } + + private void QueueMessage(Message message) + { // prepare an outer-command that decorates that, but expects QUEUED var queued = new QueuedMessage(message); var wasQueued = SimpleResultBox.Create(); @@ -102,7 +133,6 @@ public Task ExecuteAsync(CommandFlags flags) break; } } - return task; } internal override T? ExecuteSync(Message? message, ResultProcessor? processor, ServerEndPoint? server = null, T? defaultValue = default) where T : default diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2176.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2176.cs new file mode 100644 index 000000000..7f546892d --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2176.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests.Issues +{ + public class Issue2176 : TestBase + { + public Issue2176(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Execute_Batch() + { + using var conn = Create(); + var db = conn.GetDatabase(); + + var me = Me(); + var key = me + ":1"; + var key2 = me + ":2"; + var keyIntersect = me + ":result"; + + db.KeyDelete(key); + db.KeyDelete(key2); + db.KeyDelete(keyIntersect); + db.SortedSetAdd(key, "a", 1345); + + var tasks = new List(); + var batch = db.CreateBatch(); + tasks.Add(batch.SortedSetAddAsync(key2, "a", 4567)); + tasks.Add(batch.SortedSetCombineAndStoreAsync(SetOperation.Intersect, + keyIntersect, new RedisKey[] { key, key2 })); + var rangeByRankTask = batch.SortedSetRangeByRankAsync(keyIntersect); + tasks.Add(rangeByRankTask); + batch.Execute(); + + Task.WhenAll(tasks.ToArray()); + + var rangeByRankSortedSetValues = rangeByRankTask.Result; + + int size = rangeByRankSortedSetValues.Length; + Assert.Equal(1, size); + string firstRedisValue = rangeByRankSortedSetValues.FirstOrDefault().ToString(); + Assert.Equal("a", firstRedisValue); + } + + [Fact] + public void Execute_Transaction() + { + using var conn = Create(); + var db = conn.GetDatabase(); + + var me = Me(); + var key = me + ":1"; + var key2 = me + ":2"; + var keyIntersect = me + ":result"; + + db.KeyDelete(key); + db.KeyDelete(key2); + db.KeyDelete(keyIntersect); + db.SortedSetAdd(key, "a", 1345); + + var tasks = new List(); + var batch = db.CreateTransaction(); + tasks.Add(batch.SortedSetAddAsync(key2, "a", 4567)); + tasks.Add(batch.SortedSetCombineAndStoreAsync(SetOperation.Intersect, + keyIntersect, new RedisKey[] { key, key2 })); + var rangeByRankTask = batch.SortedSetRangeByRankAsync(keyIntersect); + tasks.Add(rangeByRankTask); + batch.Execute(); + + Task.WhenAll(tasks.ToArray()); + + var rangeByRankSortedSetValues = rangeByRankTask.Result; + + int size = rangeByRankSortedSetValues.Length; + Assert.Equal(1, size); + string firstRedisValue = rangeByRankSortedSetValues.FirstOrDefault().ToString(); + Assert.Equal("a", firstRedisValue); + } + } +} From d6e05f656a7194a041fd369da779b922ab5c01f0 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 28 Jun 2022 14:47:49 +0100 Subject: [PATCH 165/435] release notes for 2.6.48 --- docs/ReleaseNotes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 2a6a13092..59f83aa39 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,6 +1,6 @@ # Release Notes -## Pending +## 2.6.48 - URGENT Fix: [#2167](https://github.com/StackExchange/StackExchange.Redis/issues/2167), [#2176](https://github.com/StackExchange/StackExchange.Redis/issues/2176): fix error in batch/transaction handling that can result in out-of-order instructions ([#2177 by MarcGravell](https://github.com/StackExchange/StackExchange.Redis/pull/2177)) - Fix: [#2164](https://github.com/StackExchange/StackExchange.Redis/issues/2164): fix `LuaScript.Prepare` for scripts that don't have parameters ([#2166 by MarcGravell](https://github.com/StackExchange/StackExchange.Redis/pull/2166)) From 10988f0de40a65cc0d43a1636d6f30d439d1accb Mon Sep 17 00:00:00 2001 From: Steve Lorello <42971704+slorello89@users.noreply.github.com> Date: Mon, 18 Jul 2022 09:36:28 -0400 Subject: [PATCH 166/435] moving pre 2.6.45 write commands that were not previously considered primary-only out of primary-only (#2183) Fixes #2182 @NickCraver for background in #2101, we changed some of the logic around primary-only to require all commands to be explicitly stated as primary-only or not. During this effort, I went through the command list and checked to see which were considered write commands (which would be rejected by a replica) so that there would be consistency in behavior when stumbling on commands that would ordinarily be rejected by a replica. This made a slightly more restrictive list of commands. Enter #2182, where this inconsistency had evidently become load-bearing. The user has evidently designated their replicas as writeable (I think I was subconsciously aware of this capability but have never actually seen anyone use it). As it turns out By resolving this inconsistency in #2101 and the follow-on issue when I introduced `SORT_RO` in #2111 I apparently introduced a break. This PR reverts all the pre-2.6.45 commands' primary vs replica disposition back to their previous state which will remove the break. Any command that was introduced in 2.6.45 is correctly dispositioned. Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 4 + src/StackExchange.Redis/Enums/RedisCommand.cs | 22 ++--- tests/StackExchange.Redis.Tests/Streams.cs | 88 ------------------- 3 files changed, 15 insertions(+), 99 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 59f83aa39..8e39bfdde 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,5 +1,9 @@ # Release Notes +## Unreleased + +- Fix [#2182](https://github.com/StackExchange/StackExchange.Redis/issues/2182): Be more flexible in which commands are "primary only" in order to support users with replicas that are explicitly configured to allow writes ([#2183 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2183)) + ## 2.6.48 - URGENT Fix: [#2167](https://github.com/StackExchange/StackExchange.Redis/issues/2167), [#2176](https://github.com/StackExchange/StackExchange.Redis/issues/2176): fix error in batch/transaction handling that can result in out-of-order instructions ([#2177 by MarcGravell](https://github.com/StackExchange/StackExchange.Redis/pull/2177)) diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 6eded0e78..40df9416e 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -264,7 +264,6 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.BLPOP: case RedisCommand.BRPOP: case RedisCommand.BRPOPLPUSH: - case RedisCommand.COPY: case RedisCommand.DECR: case RedisCommand.DECRBY: case RedisCommand.DEL: @@ -273,7 +272,6 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.EXPIRETIME: case RedisCommand.FLUSHALL: case RedisCommand.FLUSHDB: - case RedisCommand.GEOADD: case RedisCommand.GEOSEARCHSTORE: case RedisCommand.GETDEL: case RedisCommand.GETEX: @@ -323,21 +321,13 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.SETRANGE: case RedisCommand.SINTERSTORE: case RedisCommand.SMOVE: - case RedisCommand.SORT: case RedisCommand.SPOP: case RedisCommand.SREM: case RedisCommand.SUNIONSTORE: case RedisCommand.SWAPDB: case RedisCommand.TOUCH: case RedisCommand.UNLINK: - case RedisCommand.XACK: - case RedisCommand.XADD: case RedisCommand.XAUTOCLAIM: - case RedisCommand.XCLAIM: - case RedisCommand.XDEL: - case RedisCommand.XGROUP: - case RedisCommand.XREADGROUP: - case RedisCommand.XTRIM: case RedisCommand.ZADD: case RedisCommand.ZDIFFSTORE: case RedisCommand.ZINTERSTORE: @@ -447,7 +437,6 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.UNSUBSCRIBE: case RedisCommand.UNWATCH: case RedisCommand.WATCH: - // Stream commands verified working on replicas case RedisCommand.XINFO: case RedisCommand.XLEN: case RedisCommand.XPENDING: @@ -474,6 +463,17 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.ZSCORE: case RedisCommand.ZUNION: case RedisCommand.UNKNOWN: + // Writable commands, but allowed for the writable-replicas scenario + case RedisCommand.COPY: + case RedisCommand.GEOADD: + case RedisCommand.SORT: + case RedisCommand.XACK: + case RedisCommand.XADD: + case RedisCommand.XCLAIM: + case RedisCommand.XDEL: + case RedisCommand.XGROUP: + case RedisCommand.XREADGROUP: + case RedisCommand.XTRIM: return false; default: throw new ArgumentOutOfRangeException(nameof(command), $"Every RedisCommand must be defined in Message.IsPrimaryOnly, unknown command '{command}' encountered."); diff --git a/tests/StackExchange.Redis.Tests/Streams.cs b/tests/StackExchange.Redis.Tests/Streams.cs index b24d7cfd7..3ae1a19ae 100644 --- a/tests/StackExchange.Redis.Tests/Streams.cs +++ b/tests/StackExchange.Redis.Tests/Streams.cs @@ -26,94 +26,6 @@ public void IsStreamType() Assert.Equal(RedisType.Stream, keyType); } - [Fact] - public void StreamOpsFailOnReplica() - { - using var conn = Create(configuration: TestConfig.Current.PrimaryServerAndPort, require: RedisFeatures.v5_0_0); - using var replicaConn = Create(configuration: TestConfig.Current.ReplicaServerAndPort, require: RedisFeatures.v5_0_0); - - var db = conn.GetDatabase(); - var replicaDb = replicaConn.GetDatabase(); - - // XADD: Works on primary, not secondary - db.StreamAdd(GetUniqueKey("auto_id"), "field1", "value1"); - var ex = Assert.Throws(() => replicaDb.StreamAdd(GetUniqueKey("auto_id"), "field1", "value1")); - Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available", ex.Message); - - // Add stream content to primary - var key = GetUniqueKey("group_ack"); - const string groupName1 = "test_group1", - groupName2 = "test_group2", - consumer1 = "test_consumer1", - consumer2 = "test_consumer2"; - - // Add for primary - var id1 = db.StreamAdd(key, "field1", "value1"); - var id2 = db.StreamAdd(key, "field2", "value2"); - var id3 = db.StreamAdd(key, "field3", "value3"); - var id4 = db.StreamAdd(key, "field4", "value4"); - - // XGROUP: Works on primary, not replica - db.StreamCreateConsumerGroup(key, groupName1, StreamPosition.Beginning); - ex = Assert.Throws(() => replicaDb.StreamCreateConsumerGroup(key, groupName2, StreamPosition.Beginning)); - Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available", ex.Message); - - // Create the second group on the primary, for the rest of the tests. - db.StreamCreateConsumerGroup(key, groupName2, StreamPosition.Beginning); - - // XREADGROUP: Works on primary, not replica - // Read all 4 messages, they will be assigned to the consumer - var entries = db.StreamReadGroup(key, groupName1, consumer1, StreamPosition.NewMessages); - ex = Assert.Throws(() => replicaDb.StreamReadGroup(key, groupName2, consumer2, StreamPosition.NewMessages)); - Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available", ex.Message); - - // XACK: Works on primary, not secondary - var oneAck = db.StreamAcknowledge(key, groupName1, id1); - ex = Assert.Throws(() => replicaDb.StreamAcknowledge(key, groupName2, id1)); - Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available", ex.Message); - - // XPENDING: Works on primary and replica - // Get the pending messages for consumer2. - var pendingMessages = db.StreamPendingMessages(key, groupName1, 10, consumer1); - var pendingMessages2 = replicaDb.StreamPendingMessages(key, groupName2, 10, consumer2); - - // XCLAIM: Works on primary, not replica - // Claim the messages for consumer1. - var messages = db.StreamClaim(key, groupName1, consumer1, 0, messageIds: pendingMessages.Select(pm => pm.MessageId).ToArray()); - ex = Assert.Throws(() => replicaDb.StreamClaim(key, groupName2, consumer2, 0, messageIds: pendingMessages.Select(pm => pm.MessageId).ToArray())); - Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available", ex.Message); - - // XDEL: Works on primary, not replica - db.StreamDelete(key, new RedisValue[] { id4 }); - ex = Assert.Throws(() => replicaDb.StreamDelete(key, new RedisValue[] { id3 })); - Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available", ex.Message); - - // XINFO: Works on primary and replica - db.StreamInfo(key); - replicaDb.StreamInfo(key); - - // XLEN: Works on primary and replica - db.StreamLength(key); - replicaDb.StreamLength(key); - - // XRANGE: Works on primary and replica - db.StreamRange(key); - replicaDb.StreamRange(key); - - // XREVRANGE: Works on primary and replica - db.StreamRange(key, messageOrder: Order.Descending); - replicaDb.StreamRange(key, messageOrder: Order.Descending); - - // XREAD: Works on primary and replica - db.StreamRead(key, "0-1"); - replicaDb.StreamRead(key, "0-1"); - - // XTRIM: Works on primary, not replica - db.StreamTrim(key, 10); - ex = Assert.Throws(() => replicaDb.StreamTrim(key, 10)); - Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available", ex.Message); - } - [Fact] public void StreamAddSinglePairWithAutoId() { From de2971ac6acafa6fc60d6d6c34201780d44c764b Mon Sep 17 00:00:00 2001 From: Jacob Bundgaard Date: Mon, 25 Jul 2022 15:43:04 +0200 Subject: [PATCH 167/435] Add instructions for how start Redis servers (#2193) --- docs/Testing.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/Testing.md b/docs/Testing.md index 60e56a429..7ec718890 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -6,12 +6,15 @@ Welcome to documentation for the `StackExchange.Redis` test suite! Supported platforms: - Windows -...that's it. For now. I'll add Docker files for the instances soon, unless someone's willing to get to it first. The tests (for `netcoreapp`) can run multi-platform. +...that's it. For now. The tests (for `netcoreapp`) can run multi-platform. **Note: some tests are not yet green, about 20 are failing (~31 in CI)**. A large set of .NET Core, testing, and CI changes just slammed us, we're getting back in action. The unit and integration tests here are fairly straightforward. There are 2 primary steps: 1. Start the servers + +This can be done either by installing Docker and running `docker-compose up` in the `tests\RedisConfigs` folder or by running the `start-all` script in the same folder. + 2. Run the tests Tests default to `127.0.0.1` as their server, however you can override any of the test IPs/Hostnames and ports by placing a `TestConfig.json` in the `StackExchange.Redis.Tests\` folder. This file is intentionally in `.gitignore` already, as it's for *your* personal overrides. This is useful for testing local or remote servers, different versions, various ports, etc. From 74f4c9a0d39b822f67e50e851d764f68d540f304 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 25 Jul 2022 09:44:04 -0400 Subject: [PATCH 168/435] More love to testing docs --- docs/Testing.md | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/docs/Testing.md b/docs/Testing.md index 7ec718890..52776f3b6 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -4,20 +4,17 @@ Testing Welcome to documentation for the `StackExchange.Redis` test suite! Supported platforms: -- Windows - -...that's it. For now. The tests (for `netcoreapp`) can run multi-platform. - -**Note: some tests are not yet green, about 20 are failing (~31 in CI)**. A large set of .NET Core, testing, and CI changes just slammed us, we're getting back in action. +- Windows (all tests) +- Other .NET-supported platforms (.NET Core tests) The unit and integration tests here are fairly straightforward. There are 2 primary steps: 1. Start the servers -This can be done either by installing Docker and running `docker-compose up` in the `tests\RedisConfigs` folder or by running the `start-all` script in the same folder. +This can be done either by installing Docker and running `docker compose up` in the `tests\RedisConfigs` folder or by running the `start-all` script in the same folder. Docker is the preferred method. 2. Run the tests -Tests default to `127.0.0.1` as their server, however you can override any of the test IPs/Hostnames and ports by placing a `TestConfig.json` in the `StackExchange.Redis.Tests\` folder. This file is intentionally in `.gitignore` already, as it's for *your* personal overrides. This is useful for testing local or remote servers, different versions, various ports, etc. +Tests default to `127.0.0.1` as their server, however you can override any of the test IPs/hostnames and ports by placing a `TestConfig.json` in the `StackExchange.Redis.Tests\` folder. This file is intentionally in `.gitignore` already, as it's for *your* personal overrides. This is useful for testing local or remote servers, different versions, various ports, etc. You can find all the JSON properties at [TestConfig.cs](https://github.com/StackExchange/StackExchange.Redis/blob/main/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs). An example override (everything not specified being a default) would look like this: ```json @@ -33,12 +30,4 @@ You can find all the JSON properties at [TestConfig.cs](https://github.com/Stack The tests are run (by default) as part of the build. You can simply run this in the repository root: ```cmd .\build.cmd -BuildNumber local -``` - -To specifically run the tests with far more options, from the repository root: -```cmd -dotnet build -.\RedisConfigs\start-all.cmd -cd StackExchange.Redis.Tests -dotnet xunit -``` +``` \ No newline at end of file From 4f5ebac95a70d8849df66b5ee7d4ff1c2f53353d Mon Sep 17 00:00:00 2001 From: Jacob Bundgaard Date: Tue, 26 Jul 2022 16:38:29 +0200 Subject: [PATCH 169/435] Implement IAsyncDisposable from IConnectionMultiplexer (#2161) Fixes #2160. We may want to implement `IAsyncDisposable` in more public `IDisposable` classes which can dispose their resources asynchronously. Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ConnectionMultiplexer.cs | 15 +++++++++++++++ .../Interfaces/IConnectionMultiplexer.cs | 2 +- src/StackExchange.Redis/PublicAPI.Shipped.txt | 1 + .../SharedConnectionFixture.cs | 2 ++ 5 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 8e39bfdde..eaf100579 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -3,6 +3,7 @@ ## Unreleased - Fix [#2182](https://github.com/StackExchange/StackExchange.Redis/issues/2182): Be more flexible in which commands are "primary only" in order to support users with replicas that are explicitly configured to allow writes ([#2183 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2183)) +- Adds: `IConnectionMultiplexer` now implements `IAsyncDisposable` ([#2161 by kimsey0](https://github.com/StackExchange/StackExchange.Redis/pull/2161)) ## 2.6.48 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 099c46071..8e0a6c891 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -2016,6 +2016,21 @@ public void Dispose() oldTimer?.Dispose(); } + /// + /// Release all resources associated with this object. + /// + public async ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + await CloseAsync(!_isDisposed); + if (sentinelConnection is ConnectionMultiplexer sentinel) + { + await sentinel.DisposeAsync(); + } + var oldTimer = Interlocked.Exchange(ref sentinelPrimaryReconnectTimer, null); + oldTimer?.Dispose(); + } + /// /// Close all connections and release all resources associated with this object. /// diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index 0a898638a..e2e95cf2c 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -20,7 +20,7 @@ internal interface IInternalConnectionMultiplexer : IConnectionMultiplexer /// /// Represents the abstract multiplexer API. /// - public interface IConnectionMultiplexer : IDisposable + public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable { /// /// Gets the client-name that will be used on all new connections. diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 8f20c4192..2d6960d54 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -308,6 +308,7 @@ StackExchange.Redis.ConnectionMultiplexer.ConfigureAsync(System.IO.TextWriter? l StackExchange.Redis.ConnectionMultiplexer.ConnectionFailed -> System.EventHandler? StackExchange.Redis.ConnectionMultiplexer.ConnectionRestored -> System.EventHandler? StackExchange.Redis.ConnectionMultiplexer.Dispose() -> void +StackExchange.Redis.ConnectionMultiplexer.DisposeAsync() -> System.Threading.Tasks.ValueTask StackExchange.Redis.ConnectionMultiplexer.ErrorMessage -> System.EventHandler? StackExchange.Redis.ConnectionMultiplexer.ExportConfiguration(System.IO.Stream! destination, StackExchange.Redis.ExportOptions options = (StackExchange.Redis.ExportOptions)-1) -> void StackExchange.Redis.ConnectionMultiplexer.GetCounters() -> StackExchange.Redis.ServerCounters! diff --git a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs index eb199e625..cdf82948d 100644 --- a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs @@ -128,6 +128,8 @@ public event EventHandler HashSlotMoved public void Dispose() { } // DO NOT call _inner.Dispose(); + public ValueTask DisposeAsync() => default; // DO NOT call _inner.DisposeAsync(); + public ServerCounters GetCounters() => _inner.GetCounters(); public IDatabase GetDatabase(int db = -1, object? asyncState = null) => _inner.GetDatabase(db, asyncState); From 620c28105f029105e40c35b186043712b33dee04 Mon Sep 17 00:00:00 2001 From: Steve Lorello <42971704+slorello89@users.noreply.github.com> Date: Tue, 2 Aug 2022 19:08:25 -0400 Subject: [PATCH 170/435] adding some smarts to GetFeatures (#2191) @NickCraver - This will fix #2016 The issue is that when `GetFeatures` does the server selection, it uses `PING` as the command it's asking about, so when the multiplexer responds with a server, it responds with any server. Of course, then when it goes to execute the command, it's been explicitly handed a server, so it honors that choice but checks it to make sure it's a valid server to send the command to, so if you're executing a write command and `GetFeatuers` just so happened to output a read-only replica, when the multiplexer does the 'is this a valid server?' check, it determines it is not and propagates the error. that's what causes the blowup. By passing in the RedisCommand to `GetFeatures`, we allow it to make an informed choice of server for that Redis Command. The other option is to simply not pass the server on down the line when we we've done a feature check, and allow the muxer to make it's own decision, this would cause an extra run of `Select` which the current pattern e.g. see [sort](https://github.com/StackExchange/StackExchange.Redis/blob/main/src/StackExchange.Redis/RedisDatabase.cs#L1816) tries to avoid One weird case, with SORT/SORT_RO, if the RO command is what we prefer to run (because we don't have a destination key to output it to), and `SORT_RO` is not available (because they aren't on Redis 7+), we cannot trust the output of GetFeatures, so I just set the selected server back to null and let the muxer figure it out down the line. Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/Directory.Build.props | 1 - src/StackExchange.Redis/RedisBase.cs | 4 +-- src/StackExchange.Redis/RedisDatabase.cs | 29 ++++++++++++++-------- src/StackExchange.Redis/RedisServer.cs | 2 +- src/StackExchange.Redis/RedisSubscriber.cs | 2 +- 6 files changed, 23 insertions(+), 16 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index eaf100579..22f63e6b7 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -4,6 +4,7 @@ - Fix [#2182](https://github.com/StackExchange/StackExchange.Redis/issues/2182): Be more flexible in which commands are "primary only" in order to support users with replicas that are explicitly configured to allow writes ([#2183 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2183)) - Adds: `IConnectionMultiplexer` now implements `IAsyncDisposable` ([#2161 by kimsey0](https://github.com/StackExchange/StackExchange.Redis/pull/2161)) +- Fix [#2016](https://github.com/StackExchange/StackExchange.Redis/issues/2016): Align server selection with supported commands (e.g. with writable servers) to reduce `Command cannot be issued to a replica` errors ([#2191 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2191)) ## 2.6.48 diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c50acad80..09f6d5e9f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -7,7 +7,6 @@ - diff --git a/src/StackExchange.Redis/RedisBase.cs b/src/StackExchange.Redis/RedisBase.cs index 0f0952607..095835efd 100644 --- a/src/StackExchange.Redis/RedisBase.cs +++ b/src/StackExchange.Redis/RedisBase.cs @@ -62,9 +62,9 @@ internal virtual Task ExecuteAsync(Message? message, ResultProcessor? p return multiplexer.ExecuteSyncImpl(message, processor, server, defaultValue); } - internal virtual RedisFeatures GetFeatures(in RedisKey key, CommandFlags flags, out ServerEndPoint? server) + internal virtual RedisFeatures GetFeatures(in RedisKey key, CommandFlags flags, RedisCommand command, out ServerEndPoint? server) { - server = multiplexer.SelectServer(RedisCommand.PING, flags, key); + server = multiplexer.SelectServer(command, flags, key); var version = server == null ? multiplexer.RawConfig.DefaultVersion : server.Version; return new RedisFeatures(version); } diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 38338390c..bb4f15a2c 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -635,7 +635,7 @@ public Task HyperLogLogAddAsync(RedisKey key, RedisValue[] values, Command public long HyperLogLogLength(RedisKey key, CommandFlags flags = CommandFlags.None) { - var features = GetFeatures(key, flags, out ServerEndPoint? server); + var features = GetFeatures(key, flags, RedisCommand.PFCOUNT, out ServerEndPoint? server); var cmd = Message.Create(Database, flags, RedisCommand.PFCOUNT, key); // technically a write / primary-only command until 2.8.18 if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetPrimaryOnly(); @@ -649,7 +649,7 @@ public long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags var cmd = Message.Create(Database, flags, RedisCommand.PFCOUNT, keys); if (keys.Length != 0) { - var features = GetFeatures(keys[0], flags, out server); + var features = GetFeatures(keys[0], flags, RedisCommand.PFCOUNT, out server); // technically a write / primary-only command until 2.8.18 if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetPrimaryOnly(); } @@ -658,7 +658,7 @@ public long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags public Task HyperLogLogLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { - var features = GetFeatures(key, flags, out ServerEndPoint? server); + var features = GetFeatures(key, flags, RedisCommand.PFCOUNT, out ServerEndPoint? server); var cmd = Message.Create(Database, flags, RedisCommand.PFCOUNT, key); // technically a write / primary-only command until 2.8.18 if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetPrimaryOnly(); @@ -672,7 +672,7 @@ public Task HyperLogLogLengthAsync(RedisKey[] keys, CommandFlags flags = C var cmd = Message.Create(Database, flags, RedisCommand.PFCOUNT, keys); if (keys.Length != 0) { - var features = GetFeatures(keys[0], flags, out server); + var features = GetFeatures(keys[0], flags, RedisCommand.PFCOUNT, out server); // technically a write / primary-only command until 2.8.18 if (server != null && !features.HyperLogLogCountReplicaSafe) cmd.SetPrimaryOnly(); } @@ -773,7 +773,7 @@ public Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFl private RedisCommand GetDeleteCommand(RedisKey key, CommandFlags flags, out ServerEndPoint? server) { - var features = GetFeatures(key, flags, out server); + var features = GetFeatures(key, flags, RedisCommand.UNLINK, out server); if (server != null && features.Unlink && multiplexer.CommandMap.IsAvailable(RedisCommand.UNLINK)) { return RedisCommand.UNLINK; @@ -1036,7 +1036,7 @@ public Task KeyRestoreAsync(RedisKey key, byte[] value, TimeSpan? expiry = null, public TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None) { - var features = GetFeatures(key, flags, out ServerEndPoint? server); + var features = GetFeatures(key, flags, RedisCommand.TTL, out ServerEndPoint? server); Message msg; if (server != null && features.MillisecondExpiry && multiplexer.CommandMap.IsAvailable(RedisCommand.PTTL)) { @@ -1049,7 +1049,7 @@ public Task KeyRestoreAsync(RedisKey key, byte[] value, TimeSpan? expiry = null, public Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None) { - var features = GetFeatures(key, flags, out ServerEndPoint? server); + var features = GetFeatures(key, flags, RedisCommand.TTL, out ServerEndPoint? server); Message msg; if (server != null && features.MillisecondExpiry && multiplexer.CommandMap.IsAvailable(RedisCommand.PTTL)) { @@ -3278,7 +3278,7 @@ private Message GetExpiryMessage(in RedisKey key, server = null; if ((milliseconds % 1000) != 0) { - var features = GetFeatures(key, flags, out server); + var features = GetFeatures(key, flags, RedisCommand.PEXPIRE, out server); if (server is not null && features.MillisecondExpiry && multiplexer.CommandMap.IsAvailable(millisecondsCommand)) { return when switch @@ -3675,10 +3675,17 @@ private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double s private Message GetSortMessage(RedisKey destination, RedisKey key, long skip, long take, Order order, SortType sortType, RedisValue by, RedisValue[]? get, CommandFlags flags, out ServerEndPoint? server) { server = null; - var command = destination.IsNull && GetFeatures(key, flags, out server).ReadOnlySort + var command = destination.IsNull && GetFeatures(key, flags, RedisCommand.SORT_RO, out server).ReadOnlySort ? RedisCommand.SORT_RO : RedisCommand.SORT; + //if SORT_RO is not available, we cannot issue the command to a read-only replica + if (command == RedisCommand.SORT) + { + server = null; + } + + // most common cases; no "get", no "by", no "destination", no "skip", no "take" if (destination.IsNull && skip == 0 && take == -1 && by.IsNull && (get == null || get.Length == 0)) { @@ -4338,7 +4345,7 @@ private Message GetStringGetWithExpiryMessage(RedisKey key, CommandFlags flags, { throw new NotSupportedException("This operation is not possible inside a transaction or batch; please issue separate GetString and KeyTimeToLive requests"); } - var features = GetFeatures(key, flags, out server); + var features = GetFeatures(key, flags, RedisCommand.PTTL, out server); processor = StringGetWithExpiryProcessor.Default; if (server != null && features.MillisecondExpiry && multiplexer.CommandMap.IsAvailable(RedisCommand.PTTL)) { @@ -4495,7 +4502,7 @@ private Message GetStringSetAndGetMessage( throw new ArgumentOutOfRangeException(nameof(pageSize)); if (!multiplexer.CommandMap.IsAvailable(command)) return null; - var features = GetFeatures(key, flags, out server); + var features = GetFeatures(key, flags, RedisCommand.SCAN, out server); if (!features.Scan) return null; if (CursorUtils.IsNil(pattern)) pattern = (byte[]?)null; diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index dea6d586d..c3d4cbe92 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -742,7 +742,7 @@ internal override Task ExecuteAsync(Message? message, ResultProcessor? return base.ExecuteSync(message, processor, server, defaultValue); } - internal override RedisFeatures GetFeatures(in RedisKey key, CommandFlags flags, out ServerEndPoint server) + internal override RedisFeatures GetFeatures(in RedisKey key, CommandFlags flags, RedisCommand command, out ServerEndPoint server) { server = this.server; return server.GetFeatures(); diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index b87cb2ea3..d26368c20 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -307,7 +307,7 @@ private Message CreatePingMessage(CommandFlags flags) bool usePing = false; if (multiplexer.CommandMap.IsAvailable(RedisCommand.PING)) { - try { usePing = GetFeatures(default, flags, out _).PingOnSubscriber; } + try { usePing = GetFeatures(default, flags, RedisCommand.PING, out _).PingOnSubscriber; } catch { } } From 4895a0b361b323adb9c5f6a2c554e553ec1ba8a0 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 9 Aug 2022 11:08:03 -0400 Subject: [PATCH 171/435] Add ConnectionMultiplexer.GetServers() API (#2203) Making things a bit easier for when you need to iterate over servers for a multiplexer --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ConnectionMultiplexer.cs | 14 ++++++++++++++ .../Interfaces/IConnectionMultiplexer.cs | 6 ++++++ src/StackExchange.Redis/Interfaces/IDatabase.cs | 6 ++++-- .../Interfaces/IDatabaseAsync.cs | 5 ++++- src/StackExchange.Redis/PublicAPI.Shipped.txt | 2 ++ .../SharedConnectionFixture.cs | 2 ++ tests/StackExchange.Redis.Tests/TestBase.cs | 4 +--- 8 files changed, 34 insertions(+), 6 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 22f63e6b7..43543d621 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -4,6 +4,7 @@ - Fix [#2182](https://github.com/StackExchange/StackExchange.Redis/issues/2182): Be more flexible in which commands are "primary only" in order to support users with replicas that are explicitly configured to allow writes ([#2183 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2183)) - Adds: `IConnectionMultiplexer` now implements `IAsyncDisposable` ([#2161 by kimsey0](https://github.com/StackExchange/StackExchange.Redis/pull/2161)) +- Adds: `IConnectionMultiplexer.GetServers()` to get all `IServer` instances for a multiplexer ([#2203 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2203)) - Fix [#2016](https://github.com/StackExchange/StackExchange.Redis/issues/2016): Align server selection with supported commands (e.g. with writable servers) to reduce `Command cannot be issued to a replica` errors ([#2191 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2191)) ## 2.6.48 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 8e0a6c891..abcfea570 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1029,6 +1029,20 @@ public IServer GetServer(EndPoint? endpoint, object? asyncState = null) return new RedisServer(this, server, asyncState); } + /// + /// Obtain configuration APIs for all servers in this multiplexer. + /// + public IServer[] GetServers() + { + var snapshot = GetServerSnapshot(); + var result = new IServer[snapshot.Length]; + for (var i = 0; i < snapshot.Length; i++) + { + result[i] = new RedisServer(this, snapshot[i], null); + } + return result; + } + /// /// Get the hash-slot associated with a given key, if applicable. /// This can be useful for grouping operations. diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index e2e95cf2c..c9a8fe96a 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Threading.Tasks; @@ -192,6 +193,11 @@ public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable /// The async state to pass to the created . IServer GetServer(EndPoint endpoint, object? asyncState = null); + /// + /// Obtain configuration APIs for all servers in this multiplexer. + /// + IServer[] GetServers(); + /// /// Reconfigure the current connections based on the existing configuration. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 8325ce8e7..1c9b8b9ce 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1565,8 +1565,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key pattern to sort by, if any e.g. ExternalKey_* would return the value of ExternalKey_{listvalue} for each entry. /// The flags to use for this operation. /// The sorted elements, or the external values if get is specified. - /// - /// + /// + /// , + /// + /// RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index d6db59caa..b875e2887 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1530,7 +1530,10 @@ public interface IDatabaseAsync : IRedisAsync /// The key pattern to sort by, if any e.g. ExternalKey_* would return the value of ExternalKey_{listvalue} for each entry. /// The flags to use for this operation. /// The sorted elements, or the external values if get is specified. - /// + /// + /// , + /// + /// Task SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); /// diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI.Shipped.txt index 2d6960d54..8ffbccb7b 100644 --- a/src/StackExchange.Redis/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI.Shipped.txt @@ -320,6 +320,7 @@ StackExchange.Redis.ConnectionMultiplexer.GetServer(string! host, int port, obje StackExchange.Redis.ConnectionMultiplexer.GetServer(string! hostAndPort, object? asyncState = null) -> StackExchange.Redis.IServer! StackExchange.Redis.ConnectionMultiplexer.GetServer(System.Net.EndPoint? endpoint, object? asyncState = null) -> StackExchange.Redis.IServer! StackExchange.Redis.ConnectionMultiplexer.GetServer(System.Net.IPAddress! host, int port) -> StackExchange.Redis.IServer! +StackExchange.Redis.ConnectionMultiplexer.GetServers() -> StackExchange.Redis.IServer![]! StackExchange.Redis.ConnectionMultiplexer.GetStatus() -> string! StackExchange.Redis.ConnectionMultiplexer.GetStatus(System.IO.TextWriter! log) -> void StackExchange.Redis.ConnectionMultiplexer.GetStormLog() -> string? @@ -460,6 +461,7 @@ StackExchange.Redis.IConnectionMultiplexer.GetServer(string! host, int port, obj StackExchange.Redis.IConnectionMultiplexer.GetServer(string! hostAndPort, object? asyncState = null) -> StackExchange.Redis.IServer! StackExchange.Redis.IConnectionMultiplexer.GetServer(System.Net.EndPoint! endpoint, object? asyncState = null) -> StackExchange.Redis.IServer! StackExchange.Redis.IConnectionMultiplexer.GetServer(System.Net.IPAddress! host, int port) -> StackExchange.Redis.IServer! +StackExchange.Redis.IConnectionMultiplexer.GetServers() -> StackExchange.Redis.IServer![]! StackExchange.Redis.IConnectionMultiplexer.GetStatus() -> string! StackExchange.Redis.IConnectionMultiplexer.GetStatus(System.IO.TextWriter! log) -> void StackExchange.Redis.IConnectionMultiplexer.GetStormLog() -> string? diff --git a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs index cdf82948d..fa71909cd 100644 --- a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs @@ -146,6 +146,8 @@ public event EventHandler HashSlotMoved public IServer GetServer(EndPoint endpoint, object? asyncState = null) => _inner.GetServer(endpoint, asyncState); + public IServer[] GetServers() => _inner.GetServers(); + public string GetStatus() => _inner.GetStatus(); public void GetStatus(TextWriter log) => _inner.GetStatus(log); diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index a57cdaf14..c547e5e5e 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -221,11 +221,9 @@ public void Teardown() protected static IServer GetServer(IConnectionMultiplexer muxer) { - EndPoint[] endpoints = muxer.GetEndPoints(); IServer? result = null; - foreach (var endpoint in endpoints) + foreach (var server in muxer.GetServers()) { - var server = muxer.GetServer(endpoint); if (server.IsReplica || !server.IsConnected) continue; if (result != null) throw new InvalidOperationException("Requires exactly one primary endpoint (found " + server.EndPoint + " and " + result.EndPoint + ")"); result = server; From 7293213169a07fafe85716d1a91e4d0d34efb6d3 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 16 Aug 2022 20:38:09 -0400 Subject: [PATCH 172/435] Timeouts: hop out of queue processing as soon as we know no more are eligible for timeout (#2217) Since we're in a head-of-queue style situation here, the first non-timeout we hit means we're done - hop out and release the lock. Small docs fix tucked in here because reasons. --- Directory.Build.props | 2 +- docs/ReleaseNotes.md | 1 + docs/Timeouts.md | 2 +- src/StackExchange.Redis/PhysicalConnection.cs | 25 +++++++++++++------ 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 2c67e1a02..eeefc966e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,7 +10,7 @@ true $(MSBuildThisFileDirectory)Shared.ruleset NETSDK1069 - NU5105 + NU5105;NU1507 https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes https://github.com/StackExchange/StackExchange.Redis/ MIT diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 43543d621..36d22254a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,6 +6,7 @@ - Adds: `IConnectionMultiplexer` now implements `IAsyncDisposable` ([#2161 by kimsey0](https://github.com/StackExchange/StackExchange.Redis/pull/2161)) - Adds: `IConnectionMultiplexer.GetServers()` to get all `IServer` instances for a multiplexer ([#2203 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2203)) - Fix [#2016](https://github.com/StackExchange/StackExchange.Redis/issues/2016): Align server selection with supported commands (e.g. with writable servers) to reduce `Command cannot be issued to a replica` errors ([#2191 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2191)) +- Performance: Optimization around timeout processing to reduce lock contention in the case of many items that haven't yet timed out during a heartbeat ([#2217 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2217)) ## 2.6.48 diff --git a/docs/Timeouts.md b/docs/Timeouts.md index e84b8436a..cdbe6061e 100644 --- a/docs/Timeouts.md +++ b/docs/Timeouts.md @@ -87,7 +87,7 @@ By default Redis Timeout exception(s) includes useful information, which can hel |qu | Queue-Awaiting-Write : {int}|There are x operations currently waiting in queue to write to the redis server.| |qs | Queue-Awaiting-Response : {int}|There are x operations currently awaiting replies from redis server.| |aw | Active-Writer: {bool}|| -|bw | Backlog-Writer: {enum} | Possible values are Inactive, Started, CheckingForWork, CheckingForTimeout, RecordingTimeout, WritingMessage, Flushing, MarkingInactive, RecordingWriteFailure, RecordingFault,SettingIdle,Faulted| +|bw | Backlog-Writer: {enum} | Possible values are Inactive, Started, CheckingForWork, CheckingForTimeout, RecordingTimeout, WritingMessage, Flushing, MarkingInactive, RecordingWriteFailure, RecordingFault, SettingIdle, SpinningDown, Faulted| |rs | Read-State: {enum}|Possible values are NotStarted, Init, RanToCompletion, Faulted, ReadSync, ReadAsync, UpdateWriteTime, ProcessBuffer, MarkProcessed, TryParseResult, MatchResult, PubSubMessage, PubSubPMessage, Reconfigure, InvokePubSub, DequeueResult, ComputeResult, CompletePendingMessage, NA| |ws | Write-State: {enum}| Possible values are Initializing, Idle, Writing, Flushing, Flushed, NA| |in | Inbound-Bytes : {long}|there are x bytes waiting to be read from the input stream from redis| diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index a6647134c..b42c9a016 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -665,15 +665,24 @@ internal void OnBridgeHeartbeat() { // We only handle async timeouts here, synchronous timeouts are handled upstream. // Those sync timeouts happen in ConnectionMultiplexer.ExecuteSyncImpl() via Monitor.Wait. - if (msg.ResultBoxIsAsync && msg.HasTimedOut(now, timeout, out var elapsed)) + if (msg.HasTimedOut(now, timeout, out var elapsed)) { - bool haveDeltas = msg.TryGetPhysicalState(out _, out _, out long sentDelta, out var receivedDelta) && sentDelta >= 0 && receivedDelta >= 0; - var timeoutEx = ExceptionFactory.Timeout(multiplexer, haveDeltas - ? $"Timeout awaiting response (outbound={sentDelta >> 10}KiB, inbound={receivedDelta >> 10}KiB, {elapsed}ms elapsed, timeout is {timeout}ms)" - : $"Timeout awaiting response ({elapsed}ms elapsed, timeout is {timeout}ms)", msg, server); - multiplexer.OnMessageFaulted(msg, timeoutEx); - msg.SetExceptionAndComplete(timeoutEx, bridge); // tell the message that it is doomed - multiplexer.OnAsyncTimeout(); + if (msg.ResultBoxIsAsync) + { + bool haveDeltas = msg.TryGetPhysicalState(out _, out _, out long sentDelta, out var receivedDelta) && sentDelta >= 0 && receivedDelta >= 0; + var timeoutEx = ExceptionFactory.Timeout(multiplexer, haveDeltas + ? $"Timeout awaiting response (outbound={sentDelta >> 10}KiB, inbound={receivedDelta >> 10}KiB, {elapsed}ms elapsed, timeout is {timeout}ms)" + : $"Timeout awaiting response ({elapsed}ms elapsed, timeout is {timeout}ms)", msg, server); + multiplexer.OnMessageFaulted(msg, timeoutEx); + msg.SetExceptionAndComplete(timeoutEx, bridge); // tell the message that it is doomed + multiplexer.OnAsyncTimeout(); + } + } + else + { + // This is a head-of-line queue, which means the first thing we hit that *hasn't* timed out means no more will timeout + // and we can stop looping and release the lock early. + break; } // Note: it is important that we **do not** remove the message unless we're tearing down the socket; that // would disrupt the chain for MatchResult; we just preemptively abort the message from the caller's From f3f3013fa20383b7ac70392af27fe441f879f1e0 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 23 Aug 2022 10:59:23 -0400 Subject: [PATCH 173/435] Docs: improvement for #2103 & #1795 (#2225) This adds a bit of info on required command permissions to connect. --- docs/Configuration.md | 21 +++++++++++++++++++++ docs/Server.md | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 0590edd54..1009f9564 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -170,6 +170,27 @@ The above is equivalent to (in the connection string): $INFO=,$SELECT=use ``` +Redis Server Permissions +--- +If the user you're connecting to Redis with is limited, it still needs to have certain commands enabled for the StackExchange.Redis to succeed in connecting. The client uses: +- `AUTH` to authenticate +- `CLIENT` to set the client name +- `INFO` to understand server topology/settings +- `ECHO` for heartbeat. +- (Optional) `SUBSCRIBE` to observe change events +- (Optional) `CONFIG` to get/understand settings +- (Optional) `CLUSTER` to get cluster nodes +- (Optional) `SENTINEL` only for Sentinel servers +- (Optional) `GET` to determine tie breakers +- (Optional) `SET` (_only_ if `INFO` is disabled) to see if we're writable + +For example, a common _very_ minimal configuration ACL on the server (non-cluster) would be: +```bash +-@all +@pubsub +@read +echo +info +``` + +Note that if you choose to disable access to the above commands, it needs to be done via the `CommandMap` and not only the ACL on the server (otherwise we'll attempt the command and fail the handshake). Also, if any of the these commands are disabled, some functionality may be diminished or broken. + twemproxy --- diff --git a/docs/Server.md b/docs/Server.md index b8fcdff56..a0777c478 100644 --- a/docs/Server.md +++ b/docs/Server.md @@ -13,7 +13,7 @@ There are multiple ways of running redis on windows: - [Memurai](https://www.memurai.com/) : a fully supported, well-maintained port of redis for Windows (this is a commercial product, with a free developer version available, and free trials) - previous to Memurai, MSOpenTech had a Windows port of linux, but this is no longer maintained and is now very out of date; it is not recommended, but: [here](https://www.nuget.org/packages/redis-64/) -- WSL/WSL2 : on Windows 10, you can run redis for linux in the Windows Subsystem for Linux; note, however, that WSL may have some significant performance implications, and WSL2 appears as a *different* machine (not the local machine), due to running as a VM +- WSL/WSL2 : on Windows 10+, you can run redis for linux in the Windows Subsystem for Linux; note, however, that WSL may have some significant performance implications, and WSL2 appears as a *different* machine (not the local machine), due to running as a VM ## Docker From 2a0468ef9bdf76affa9b1eeea9379516e0efeb49 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 24 Aug 2022 14:16:34 +0100 Subject: [PATCH 174/435] Resolve sync-context issues (missing configureawait) in the multiplexer (#2229) * add test to investigate #2223 * add connect test * assert zeros in SyncConfigure * add missing ForAwait uses * investigate rantocompletion fault * fix brittle test * stabilize tests * release notes --- docs/ReleaseNotes.md | 1 + .../ConnectionMultiplexer.cs | 24 +-- src/StackExchange.Redis/LoggingPipe.cs | 2 +- src/StackExchange.Redis/RedisServer.cs | 8 +- src/StackExchange.Redis/ServerEndPoint.cs | 2 +- .../SyncContextTests.cs | 182 ++++++++++++++++++ 6 files changed, 201 insertions(+), 18 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/SyncContextTests.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 36d22254a..bc5629564 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,6 +7,7 @@ - Adds: `IConnectionMultiplexer.GetServers()` to get all `IServer` instances for a multiplexer ([#2203 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2203)) - Fix [#2016](https://github.com/StackExchange/StackExchange.Redis/issues/2016): Align server selection with supported commands (e.g. with writable servers) to reduce `Command cannot be issued to a replica` errors ([#2191 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2191)) - Performance: Optimization around timeout processing to reduce lock contention in the case of many items that haven't yet timed out during a heartbeat ([#2217 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2217)) +- Fix: Resolve sync-context issues (missing `ConfigureAwait(false)`) ([#2229 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2229)) ## 2.6.48 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index abcfea570..f8167bf10 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -234,7 +234,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt log?.WriteLine($"Checking {Format.ToString(srv.EndPoint)} is available..."); try { - await srv.PingAsync(flags); // if it isn't happy, we're not happy + await srv.PingAsync(flags).ForAwait(); // if it isn't happy, we're not happy } catch (Exception ex) { @@ -257,7 +257,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt msg = Message.Create(0, flags | CommandFlags.FireAndForget, RedisCommand.SET, tieBreakerKey, newPrimary); try { - await node.WriteDirectAsync(msg, ResultProcessor.DemandOK); + await node.WriteDirectAsync(msg, ResultProcessor.DemandOK).ForAwait(); } catch { } } @@ -267,7 +267,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt log?.WriteLine($"Making {Format.ToString(srv.EndPoint)} a primary..."); try { - await srv.ReplicaOfAsync(null, flags); + await srv.ReplicaOfAsync(null, flags).ForAwait(); } catch (Exception ex) { @@ -282,7 +282,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt msg = Message.Create(0, flags | CommandFlags.FireAndForget, RedisCommand.SET, tieBreakerKey, newPrimary); try { - await server.WriteDirectAsync(msg, ResultProcessor.DemandOK); + await server.WriteDirectAsync(msg, ResultProcessor.DemandOK).ForAwait(); } catch { } } @@ -311,13 +311,13 @@ async Task BroadcastAsync(ServerEndPoint[] serverNodes) if (!node.IsConnected) continue; log?.WriteLine($"Broadcasting via {Format.ToString(node.EndPoint)}..."); msg = Message.Create(-1, flags | CommandFlags.FireAndForget, RedisCommand.PUBLISH, channel, newPrimary); - await node.WriteDirectAsync(msg, ResultProcessor.Int64); + await node.WriteDirectAsync(msg, ResultProcessor.Int64).ForAwait(); } } } // Send a message before it happens - because afterwards a new replica may be unresponsive - await BroadcastAsync(nodes); + await BroadcastAsync(nodes).ForAwait(); if (options.HasFlag(ReplicationChangeOptions.ReplicateToOtherEndpoints)) { @@ -327,14 +327,14 @@ async Task BroadcastAsync(ServerEndPoint[] serverNodes) log?.WriteLine($"Replicating to {Format.ToString(node.EndPoint)}..."); msg = RedisServer.CreateReplicaOfMessage(node, server.EndPoint, flags); - await node.WriteDirectAsync(msg, ResultProcessor.DemandOK); + await node.WriteDirectAsync(msg, ResultProcessor.DemandOK).ForAwait(); } } // ...and send one after it happens - because the first broadcast may have landed on a secondary client // and it can reconfigure before any topology change actually happened. This is most likely to happen // in low-latency environments. - await BroadcastAsync(nodes); + await BroadcastAsync(nodes).ForAwait(); // and reconfigure the muxer log?.WriteLine("Reconfiguring all endpoints..."); @@ -344,7 +344,7 @@ async Task BroadcastAsync(ServerEndPoint[] serverNodes) { Interlocked.Exchange(ref activeConfigCause, null); } - if (!await ReconfigureAsync(first: false, reconfigureAll: true, log, srv.EndPoint, cause: nameof(MakePrimaryAsync))) + if (!await ReconfigureAsync(first: false, reconfigureAll: true, log, srv.EndPoint, cause: nameof(MakePrimaryAsync)).ForAwait()) { log?.WriteLine("Verifying the configuration was incomplete; please verify"); } @@ -1097,7 +1097,7 @@ public bool Configure(TextWriter? log = null) public async Task ConfigureAsync(TextWriter? log = null) { using var logProxy = LogProxy.TryCreate(log); - return await ReconfigureAsync(first: false, reconfigureAll: true, logProxy, null, "configure").ObserveErrors(); + return await ReconfigureAsync(first: false, reconfigureAll: true, logProxy, null, "configure").ObserveErrors().ForAwait(); } internal int SyncConnectTimeout(bool forConnect) @@ -2036,10 +2036,10 @@ public void Dispose() public async ValueTask DisposeAsync() { GC.SuppressFinalize(this); - await CloseAsync(!_isDisposed); + await CloseAsync(!_isDisposed).ForAwait(); if (sentinelConnection is ConnectionMultiplexer sentinel) { - await sentinel.DisposeAsync(); + await sentinel.DisposeAsync().ForAwait(); } var oldTimer = Interlocked.Exchange(ref sentinelPrimaryReconnectTimer, null); oldTimer?.Dispose(); diff --git a/src/StackExchange.Redis/LoggingPipe.cs b/src/StackExchange.Redis/LoggingPipe.cs index 9adef7768..ba2343d23 100644 --- a/src/StackExchange.Redis/LoggingPipe.cs +++ b/src/StackExchange.Redis/LoggingPipe.cs @@ -54,7 +54,7 @@ private async Task CloneAsync(string path, PipeReader from, PipeWriter to) while(true) { - var result = await from.ReadAsync(); + var result = await from.ReadAsync().ForAwait(); var buffer = result.Buffer; if (result.IsCompleted && buffer.IsEmpty) break; diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index c3d4cbe92..142ee1aa0 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -418,7 +418,7 @@ public async Task MakePrimaryAsync(ReplicationChangeOptions options, TextWriter? { using (var proxy = LogProxy.TryCreate(log)) { - await multiplexer.MakePrimaryAsync(server, options, proxy); + await multiplexer.MakePrimaryAsync(server, options, proxy).ForAwait(); } } @@ -793,18 +793,18 @@ public async Task ReplicaOfAsync(EndPoint? master, CommandFlags flags = CommandF { try { - await server.WriteDirectAsync(tieBreakerRemoval, ResultProcessor.Boolean); + await server.WriteDirectAsync(tieBreakerRemoval, ResultProcessor.Boolean).ForAwait(); } catch { } } var msg = CreateReplicaOfMessage(server, master, flags); - await ExecuteAsync(msg, ResultProcessor.DemandOK); + await ExecuteAsync(msg, ResultProcessor.DemandOK).ForAwait(); // attempt to broadcast a reconfigure message to anybody listening to this server if (GetConfigChangeMessage() is Message configChangeMessage) { - await server.WriteDirectAsync(configChangeMessage, ResultProcessor.Int64); + await server.WriteDirectAsync(configChangeMessage, ResultProcessor.Int64).ForAwait(); } } diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index de9fb50c8..876e21dc4 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -939,7 +939,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) var connType = bridge.ConnectionType; if (connType == ConnectionType.Interactive) { - await AutoConfigureAsync(connection, log); + await AutoConfigureAsync(connection, log).ForAwait(); } var tracer = GetTracerMessage(true); diff --git a/tests/StackExchange.Redis.Tests/SyncContextTests.cs b/tests/StackExchange.Redis.Tests/SyncContextTests.cs new file mode 100644 index 000000000..a645f66c0 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/SyncContextTests.cs @@ -0,0 +1,182 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests +{ + public class SyncContextTests : TestBase + { + public SyncContextTests(ITestOutputHelper testOutput) : base(testOutput) { } + + /* Note A (referenced below) + * + * When sync-context is *enabled*, we don't validate OpCount > 0 - this is because *with the additional checks*, + * it can genuinely happen that by the time we actually await it, it has completd - which results in a brittle test. + */ + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task DetectSyncContextUsafe(bool continueOnCapturedContext) + { + using var ctx = new MySyncContext(Writer); + Assert.Equal(0, ctx.OpCount); + await Task.Delay(100).ConfigureAwait(continueOnCapturedContext); + + AssertState(continueOnCapturedContext, ctx); + } + + private void AssertState(bool continueOnCapturedContext, MySyncContext ctx) + { + LogNoTime($"Context in AssertState: {ctx}"); + if (continueOnCapturedContext) + { + Assert.True(ctx.IsCurrent, nameof(ctx.IsCurrent)); + // see note A re OpCount + } + else + { + // no guarantees on sync-context still being current; depends on sync vs async + Assert.Equal(0, ctx.OpCount); + } + } + + [Fact] + public void SyncPing() + { + using var ctx = new MySyncContext(Writer); + using var conn = Create(); + Assert.Equal(0, ctx.OpCount); + var db = conn.GetDatabase(); + db.Ping(); + Assert.Equal(0, ctx.OpCount); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task AsyncPing(bool continueOnCapturedContext) + { + using var ctx = new MySyncContext(Writer); + using var conn = Create(); + Assert.Equal(0, ctx.OpCount); + var db = conn.GetDatabase(); + LogNoTime($"Context before await: {ctx}"); + await db.PingAsync().ConfigureAwait(continueOnCapturedContext); + + AssertState(continueOnCapturedContext, ctx); + } + + [Fact] + public void SyncConfigure() + { + using var ctx = new MySyncContext(Writer); + using var conn = Create(); + Assert.Equal(0, ctx.OpCount); + Assert.True(conn.Configure()); + Assert.Equal(0, ctx.OpCount); + } + + [Theory] + [InlineData(true)] // fail: Expected: Not RanToCompletion, Actual: RanToCompletion + [InlineData(false)] // pass + public async Task AsyncConfigure(bool continueOnCapturedContext) + { + using var ctx = new MySyncContext(Writer); + using var conn = Create(); + + LogNoTime($"Context initial: {ctx}"); + await Task.Delay(500); + await conn.GetDatabase().PingAsync(); // ensure we're all ready + ctx.Reset(); + LogNoTime($"Context before: {ctx}"); + + Assert.Equal(0, ctx.OpCount); + Assert.True(await conn.ConfigureAsync(Writer).ConfigureAwait(continueOnCapturedContext), "config ran"); + + AssertState(continueOnCapturedContext, ctx); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ConnectAsync(bool continueOnCapturedContext) + { + using var ctx = new MySyncContext(Writer); + var config = GetConfiguration(); // not ideal, but sufficient + await ConnectionMultiplexer.ConnectAsync(config, Writer).ConfigureAwait(continueOnCapturedContext); + + AssertState(continueOnCapturedContext, ctx); + } + + public sealed class MySyncContext : SynchronizationContext, IDisposable + { + private readonly SynchronizationContext? _previousContext; + private readonly TextWriter? _log; + public MySyncContext(TextWriter? log) + { + _previousContext = Current; + _log = log; + SetSynchronizationContext(this); + } + public int OpCount => Thread.VolatileRead(ref _opCount); + private int _opCount; + private void Incr() + { + Interlocked.Increment(ref _opCount); + } + + public void Reset() => Thread.VolatileWrite(ref _opCount, 0); + + public override string ToString() => $"Sync context ({(IsCurrent ? "active" : "inactive")}): {OpCount}"; + + void IDisposable.Dispose() => SetSynchronizationContext(_previousContext); + + public override void Post(SendOrPostCallback d, object? state) + { + _log?.WriteLine("sync-ctx: Post"); + Incr(); + ThreadPool.QueueUserWorkItem(static state => + { + var tuple = (Tuple)state!; + tuple.Item1.Invoke(tuple.Item2, tuple.Item3); + }, Tuple.Create(this, d, state)); + } + + private void Invoke(SendOrPostCallback d, object? state) + { + _log?.WriteLine("sync-ctx: Invoke"); + if (!IsCurrent) SetSynchronizationContext(this); + d(state); + } + + public override void Send(SendOrPostCallback d, object? state) + { + _log?.WriteLine("sync-ctx: Send"); + Incr(); + Invoke(d, state); + } + + public bool IsCurrent => ReferenceEquals(this, Current); + + public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) + { + Incr(); + return base.Wait(waitHandles, waitAll, millisecondsTimeout); + } + public override void OperationStarted() + { + Incr(); + base.OperationStarted(); + } + public override void OperationCompleted() + { + Incr(); + base.OperationCompleted(); + } + } + + } +} From fdc90e936a8892d684bdfce2f189f212985b8d33 Mon Sep 17 00:00:00 2001 From: martintmk <103487740+martintmk@users.noreply.github.com> Date: Wed, 24 Aug 2022 16:23:46 +0200 Subject: [PATCH 175/435] Allow LoadedLuaScript fail-over in case the Redis instance is restarted (#2170) **Motivation** We want to take advantage of sending just hashed `LoadedLuaScript` to a server, but still, be able automatically failover and reconstruct the hash in case the Redis instance restarts. The goal was not to change the public API surface. Addresses #1968 Unit tests are still missing, first I want to check with you guys whether this is the right approach. Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 3 +- .../Interfaces/IDatabase.cs | 6 ++- .../Interfaces/IDatabaseAsync.cs | 6 ++- src/StackExchange.Redis/LuaScript.cs | 13 ++++-- src/StackExchange.Redis/RedisDatabase.cs | 17 ++++++-- src/StackExchange.Redis/ResultProcessor.cs | 7 +++- tests/StackExchange.Redis.Tests/Profiling.cs | 23 +++++------ tests/StackExchange.Redis.Tests/Scripting.cs | 40 ++++++++++++------- 8 files changed, 77 insertions(+), 38 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index bc5629564..b515c43e6 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,7 +7,8 @@ - Adds: `IConnectionMultiplexer.GetServers()` to get all `IServer` instances for a multiplexer ([#2203 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2203)) - Fix [#2016](https://github.com/StackExchange/StackExchange.Redis/issues/2016): Align server selection with supported commands (e.g. with writable servers) to reduce `Command cannot be issued to a replica` errors ([#2191 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2191)) - Performance: Optimization around timeout processing to reduce lock contention in the case of many items that haven't yet timed out during a heartbeat ([#2217 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2217)) -- Fix: Resolve sync-context issues (missing `ConfigureAwait(false)`) ([#2229 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2229)) +- Fix [#2223](https://github.com/StackExchange/StackExchange.Redis/issues/2223): Resolve sync-context issues (missing `ConfigureAwait(false)`) ([#2229 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2229)) +- Fix [#1968](https://github.com/StackExchange/StackExchange.Redis/issues/1968): Improved handling of EVAL scripts during server restarts and failovers, detecting and re-sending the script for a retry when needed ([#2170 by martintmk](https://github.com/StackExchange/StackExchange.Redis/pull/2170)) ## 2.6.48 diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 1c9b8b9ce..983f4df8c 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1272,7 +1272,11 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The values to execute against. /// The flags to use for this operation. /// A dynamic representation of the script's result. - /// + /// + /// Be aware that this method is not resilient to Redis server restarts. Use instead. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] RedisResult ScriptEvaluate(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index b875e2887..49771cb91 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1248,7 +1248,11 @@ public interface IDatabaseAsync : IRedisAsync /// The values to execute against. /// The flags to use for this operation. /// A dynamic representation of the script's result. - /// + /// + /// Be aware that this method is not resilient to Redis server restarts. Use instead. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] Task ScriptEvaluateAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); /// diff --git a/src/StackExchange.Redis/LuaScript.cs b/src/StackExchange.Redis/LuaScript.cs index 9c7d6553c..6e4ac7cd3 100644 --- a/src/StackExchange.Redis/LuaScript.cs +++ b/src/StackExchange.Redis/LuaScript.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Concurrent; +using System.ComponentModel; using System.Threading.Tasks; namespace StackExchange.Redis @@ -166,7 +167,7 @@ public Task EvaluateAsync(IDatabaseAsync db, object? ps = null, Red /// /// /// Loads this LuaScript into the given IServer so it can be run with it's SHA1 hash, instead of - /// passing the full script on each Evaluate or EvaluateAsync call. + /// using the implicit SHA1 hash that's calculated after the script is sent to the server for the first time. /// /// Note: the FireAndForget command flag cannot be set. /// @@ -186,7 +187,7 @@ public LoadedLuaScript Load(IServer server, CommandFlags flags = CommandFlags.No /// /// /// Loads this LuaScript into the given IServer so it can be run with it's SHA1 hash, instead of - /// passing the full script on each Evaluate or EvaluateAsync call. + /// using the implicit SHA1 hash that's calculated after the script is sent to the server for the first time. /// /// Note: the FireAndForget command flag cannot be set /// @@ -240,6 +241,8 @@ public sealed class LoadedLuaScript /// The SHA1 hash of ExecutableScript. /// This is sent to Redis instead of ExecutableScript during Evaluate and EvaluateAsync calls. /// + /// Be aware that using hash directly is not resilient to Redis server restarts. + [EditorBrowsable(EditorBrowsableState.Never)] public byte[] Hash { get; } // internal for testing purposes only @@ -265,7 +268,8 @@ internal LoadedLuaScript(LuaScript original, byte[] hash) public RedisResult Evaluate(IDatabase db, object? ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None) { Original.ExtractParameters(ps, withKeyPrefix, out RedisKey[]? keys, out RedisValue[]? args); - return db.ScriptEvaluate(Hash, keys, args, flags); + + return db.ScriptEvaluate(ExecutableScript, keys, args, flags); } /// @@ -282,7 +286,8 @@ public RedisResult Evaluate(IDatabase db, object? ps = null, RedisKey? withKeyPr public Task EvaluateAsync(IDatabaseAsync db, object? ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None) { Original.ExtractParameters(ps, withKeyPrefix, out RedisKey[]? keys, out RedisValue[]? args); - return db.ScriptEvaluateAsync(Hash, keys, args, flags); + + return db.ScriptEvaluateAsync(ExecutableScript, keys, args, flags); } } } diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index bb4f15a2c..a9dda4aff 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1538,13 +1538,22 @@ public RedisResult ScriptEvaluate(LuaScript script, object? parameters = null, C public RedisResult ScriptEvaluate(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) { - return script.Evaluate(this, parameters, null, flags); + return script.Evaluate(this, parameters, withKeyPrefix: null, flags); } - public Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) + public async Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { var msg = new ScriptEvalMessage(Database, flags, script, keys, values); - return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + + try + { + return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ConfigureAwait(false); + } + catch (RedisServerException) when (msg.IsScriptUnavailable) + { + // could be a NOSCRIPT; for a sync call, we can re-issue that without problem + return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ConfigureAwait(false); + } } public Task ScriptEvaluateAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) @@ -1560,7 +1569,7 @@ public Task ScriptEvaluateAsync(LuaScript script, object? parameter public Task ScriptEvaluateAsync(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) { - return script.EvaluateAsync(this, parameters, null, flags); + return script.EvaluateAsync(this, parameters, withKeyPrefix: null, flags); } public bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 17b6d383e..f14575f2c 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -484,9 +484,14 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal sealed class ScriptLoadProcessor : ResultProcessor { + /// + /// Anything hashed with SHA1 has exactly 40 characters. We can use that as a shortcut in the code bellow. + /// + private const int SHA1Length = 40; + private static readonly Regex sha1 = new Regex("^[0-9a-f]{40}$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - internal static bool IsSHA1(string script) => script is not null && sha1.IsMatch(script); + internal static bool IsSHA1(string script) => script is not null && script.Length == SHA1Length && sha1.IsMatch(script); internal const int Sha1HashLength = 20; internal static byte[] ParseSHA1(byte[] value) diff --git a/tests/StackExchange.Redis.Tests/Profiling.cs b/tests/StackExchange.Redis.Tests/Profiling.cs index 5a7718017..2ff0cfb26 100644 --- a/tests/StackExchange.Redis.Tests/Profiling.cs +++ b/tests/StackExchange.Redis.Tests/Profiling.cs @@ -44,6 +44,8 @@ public void Simple() Assert.Equal("fii", s); var cmds = session.FinishProfiling(); + var evalCmds = cmds.Where(c => c.Command == "EVAL").ToList(); + Assert.Equal(2, evalCmds.Count); var i = 0; foreach (var cmd in cmds) { @@ -51,7 +53,7 @@ public void Simple() } var all = string.Join(",", cmds.Select(x => x.Command)); - Assert.Equal("SET,EVAL,EVALSHA,GET,ECHO", all); + Assert.Equal("SET,EVAL,EVAL,GET,ECHO", all); Log("Checking for SET"); var set = cmds.SingleOrDefault(cmd => cmd.Command == "SET"); Assert.NotNull(set); @@ -59,28 +61,25 @@ public void Simple() var get = cmds.SingleOrDefault(cmd => cmd.Command == "GET"); Assert.NotNull(get); Log("Checking for EVAL"); - var eval = cmds.SingleOrDefault(cmd => cmd.Command == "EVAL"); - Assert.NotNull(eval); - Log("Checking for EVALSHA"); - var evalSha = cmds.SingleOrDefault(cmd => cmd.Command == "EVALSHA"); - Assert.NotNull(evalSha); - Log("Checking for ECHO"); + var eval1 = evalCmds[0]; + Log("Checking for EVAL"); + var eval2 = evalCmds[1]; var echo = cmds.SingleOrDefault(cmd => cmd.Command == "ECHO"); Assert.NotNull(echo); Assert.Equal(5, cmds.Count()); - Assert.True(set.CommandCreated <= eval.CommandCreated); - Assert.True(eval.CommandCreated <= evalSha.CommandCreated); - Assert.True(evalSha.CommandCreated <= get.CommandCreated); + Assert.True(set.CommandCreated <= eval1.CommandCreated); + Assert.True(eval1.CommandCreated <= eval2.CommandCreated); + Assert.True(eval2.CommandCreated <= get.CommandCreated); AssertProfiledCommandValues(set, conn, dbId); AssertProfiledCommandValues(get, conn, dbId); - AssertProfiledCommandValues(eval, conn, dbId); + AssertProfiledCommandValues(eval1, conn, dbId); - AssertProfiledCommandValues(evalSha, conn, dbId); + AssertProfiledCommandValues(eval2, conn, dbId); AssertProfiledCommandValues(echo, conn, dbId); } diff --git a/tests/StackExchange.Redis.Tests/Scripting.cs b/tests/StackExchange.Redis.Tests/Scripting.cs index 0e80ff08a..0f8c91eec 100644 --- a/tests/StackExchange.Redis.Tests/Scripting.cs +++ b/tests/StackExchange.Redis.Tests/Scripting.cs @@ -407,11 +407,12 @@ public async Task CheckLoads(bool async) Assert.False(server.ScriptExists(script)); // run once, causes to be cached - Assert.True((bool)db.ScriptEvaluate(script)); + Assert.True(await EvaluateScript()); + Assert.True(server.ScriptExists(script)); // can run again - Assert.True((bool)db.ScriptEvaluate(script)); + Assert.True(await EvaluateScript()); // ditch the scripts; should no longer exist db.Ping(); @@ -419,23 +420,21 @@ public async Task CheckLoads(bool async) Assert.False(server.ScriptExists(script)); db.Ping(); - if (async) - { - // now: fails the first time - var ex = await Assert.ThrowsAsync(async () => await db.ScriptEvaluateAsync(script).ForAwait()).ForAwait(); - Assert.Equal("NOSCRIPT No matching script. Please use EVAL.", ex.Message); - } - else - { - // just works; magic - Assert.True((bool)db.ScriptEvaluate(script)); - } + // just works; magic + Assert.True(await EvaluateScript()); // but gets marked as unloaded, so we can use it again... - Assert.True((bool)db.ScriptEvaluate(script)); + Assert.True(await EvaluateScript()); // which will cause it to be cached Assert.True(server.ScriptExists(script)); + + async Task EvaluateScript() + { + return async ? + (bool)await db!.ScriptEvaluateAsync(script) : + (bool)db!.ScriptEvaluate(script); + } } [Fact] @@ -1016,9 +1015,22 @@ public void ScriptWithKeyPrefixCompare() [Fact] public void RedisResultUnderstandsNullArrayArray() => TestNullArray(RedisResult.NullArray); + [Fact] public void RedisResultUnderstandsNullArrayNull() => TestNullArray(null); + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("829c3804401b0727f70f73d4415e162400cbe57b", true)] + [InlineData("$29c3804401b0727f70f73d4415e162400cbe57b", false)] + [InlineData("829c3804401b0727f70f73d4415e162400cbe57", false)] + [InlineData("829c3804401b0727f70f73d4415e162400cbe57bb", false)] + public void Sha1Detection(string candidate, bool isSha) + { + Assert.Equal(isSha, ResultProcessor.ScriptLoadProcessor.IsSHA1(candidate)); + } + private static void TestNullArray(RedisResult? value) { Assert.True(value == null || value.IsNull); From 612d32761a7f849f2bb27ab337071b8e1973d0b3 Mon Sep 17 00:00:00 2001 From: Corey <24794972+yeroc-sebrof@users.noreply.github.com> Date: Thu, 25 Aug 2022 14:15:23 +0100 Subject: [PATCH 176/435] Update Configuration.md (#2230) Removed duplicate entry `checkCertificateRevocation` --- docs/Configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 1009f9564..a248874cd 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -96,7 +96,7 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a | asyncTimeout={int} | `AsyncTimeout` | `SyncTimeout` | Time (ms) to allow for asynchronous operations | | tiebreaker={string} | `TieBreaker` | `__Booksleeve_TieBreak` | Key to use for selecting a server in an ambiguous primary scenario | | version={string} | `DefaultVersion` | (`3.0` in Azure, else `2.0`) | Redis version level (useful when the server does not make this available) | -| | `CheckCertificateRevocation` | `true` | A Boolean value that specifies whether the certificate revocation list is checked during authentication. | + Additional code-only options: - ReconnectRetryPolicy (`IReconnectRetryPolicy`) - Default: `ReconnectRetryPolicy = ExponentialRetry(ConnectTimeout / 2);` From d2d13bc4cf972beea8fe528be38db2eccbc72d9a Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 30 Aug 2022 21:27:16 -0400 Subject: [PATCH 177/435] SSL/TLS: Add SslClientAuthenticationOptions configurability (#2224) Solution for #2219, allowing explicit configuration of the SSL connection for advanced use cases. This needs some thought, but pitching the general idea here as an option available for the frameworks that support it. Co-authored-by: slorello Co-authored-by: Steve Lorello <42971704+slorello89@users.noreply.github.com> --- Directory.Packages.props | 2 +- StackExchange.Redis.sln | 1 + docs/Configuration.md | 14 +- docs/ReleaseNotes.md | 2 + .../ConfigurationOptions.cs | 11 + .../ConnectionMultiplexer.cs | 4 +- src/StackExchange.Redis/ExceptionFactory.cs | 22 +- src/StackExchange.Redis/PhysicalConnection.cs | 14 +- .../{ => PublicAPI}/PublicAPI.Shipped.txt | 0 .../{ => PublicAPI}/PublicAPI.Unshipped.txt | 0 .../netcoreapp3.1/PublicAPI.Shipped.txt | 2 + src/StackExchange.Redis/ResultProcessor.cs | 4 +- .../StackExchange.Redis.csproj | 8 + .../RedisConfigs/Basic/tls-ciphers-6384.conf | 11 + tests/RedisConfigs/Certs/ca.crt | 29 +++ tests/RedisConfigs/Certs/redis.crt | 23 +++ tests/RedisConfigs/Certs/redis.key | 27 +++ tests/RedisConfigs/Docker/supervisord.conf | 7 + tests/RedisConfigs/Dockerfile | 4 +- tests/RedisConfigs/docker-compose.yml | 2 +- tests/RedisConfigs/start-all.sh | 2 + tests/RedisConfigs/start-basic.cmd | 3 + tests/RedisConfigs/start-basic.sh | 2 + tests/StackExchange.Redis.Tests/BasicOps.cs | 2 +- .../Helpers/TestConfig.cs | 24 ++- tests/StackExchange.Redis.Tests/SSL.cs | 193 ++++++++++++++---- tests/StackExchange.Redis.Tests/Secure.cs | 2 +- 27 files changed, 349 insertions(+), 66 deletions(-) rename src/StackExchange.Redis/{ => PublicAPI}/PublicAPI.Shipped.txt (100%) rename src/StackExchange.Redis/{ => PublicAPI}/PublicAPI.Unshipped.txt (100%) create mode 100644 src/StackExchange.Redis/PublicAPI/netcoreapp3.1/PublicAPI.Shipped.txt create mode 100644 tests/RedisConfigs/Basic/tls-ciphers-6384.conf create mode 100755 tests/RedisConfigs/Certs/ca.crt create mode 100755 tests/RedisConfigs/Certs/redis.crt create mode 100755 tests/RedisConfigs/Certs/redis.key diff --git a/Directory.Packages.props b/Directory.Packages.props index 5f38b84ee..8cbfea70b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,7 +11,7 @@ - + diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index 71e2d5f90..12f52c00d 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -72,6 +72,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Basic", "Basic", "{38BDEEED tests\RedisConfigs\Basic\primary-6379.conf = tests\RedisConfigs\Basic\primary-6379.conf tests\RedisConfigs\Basic\replica-6380.conf = tests\RedisConfigs\Basic\replica-6380.conf tests\RedisConfigs\Basic\secure-6381.conf = tests\RedisConfigs\Basic\secure-6381.conf + tests\RedisConfigs\Basic\tls-ciphers-6384.conf = tests\RedisConfigs\Basic\tls-ciphers-6384.conf EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicTestBaseline", "tests\BasicTestBaseline\BasicTestBaseline.csproj", "{8FDB623D-779B-4A84-BC6B-75106E41D8A4}" diff --git a/docs/Configuration.md b/docs/Configuration.md index a248874cd..68556dba6 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -76,11 +76,11 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a | abortConnect={bool} | `AbortOnConnectFail` | `true` (`false` on Azure) | If true, `Connect` will not create a connection while no servers are available | | allowAdmin={bool} | `AllowAdmin` | `false` | Enables a range of commands that are considered risky | | channelPrefix={string} | `ChannelPrefix` | `null` | Optional channel prefix for all pub/sub operations | -| checkCertificateRevocation={bool} | `CheckCertificateRevocation` | `true` | A Boolean value that specifies whether the certificate revocation list is checked during authentication. | +| checkCertificateRevocation={bool} | `CheckCertificateRevocation` | `true` | A Boolean value that specifies whether the certificate revocation list is checked during authentication. | | connectRetry={int} | `ConnectRetry` | `3` | The number of times to repeat connect attempts during initial `Connect` | | connectTimeout={int} | `ConnectTimeout` | `5000` | Timeout (ms) for connect operations | | configChannel={string} | `ConfigurationChannel` | `__Booksleeve_MasterChanged` | Broadcast channel name for communicating configuration changes | -| configCheckSeconds={int} | `ConfigCheckSeconds` | `60` | Time (seconds) to check configuration. This serves as a keep-alive for interactive sockets, if it is supported. | +| configCheckSeconds={int} | `ConfigCheckSeconds` | `60` | Time (seconds) to check configuration. This serves as a keep-alive for interactive sockets, if it is supported. | | defaultDatabase={int} | `DefaultDatabase` | `null` | Default database index, from `0` to `databases - 1` | | keepAlive={int} | `KeepAlive` | `-1` | Time (seconds) at which to send a message to help keep sockets alive (60 sec default) | | name={string} | `ClientName` | `null` | Identification for the connection within redis | @@ -88,14 +88,14 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a | user={string} | `User` | `null` | User for the redis server (for use with ACLs on redis 6 and above) | | proxy={proxy type} | `Proxy` | `Proxy.None` | Type of proxy in use (if any); for example "twemproxy/envoyproxy" | | resolveDns={bool} | `ResolveDns` | `false` | Specifies that DNS resolution should be explicit and eager, rather than implicit | -| serviceName={string} | `ServiceName` | `null` | Used for connecting to a sentinel primary service | +| serviceName={string} | `ServiceName` | `null` | Used for connecting to a sentinel primary service | | ssl={bool} | `Ssl` | `false` | Specifies that SSL encryption should be used | | sslHost={string} | `SslHost` | `null` | Enforces a particular SSL host identity on the server's certificate | | sslProtocols={enum} | `SslProtocols` | `null` | Ssl/Tls versions supported when using an encrypted connection. Use '\|' to provide multiple values. | | syncTimeout={int} | `SyncTimeout` | `5000` | Time (ms) to allow for synchronous operations | -| asyncTimeout={int} | `AsyncTimeout` | `SyncTimeout` | Time (ms) to allow for asynchronous operations | -| tiebreaker={string} | `TieBreaker` | `__Booksleeve_TieBreak` | Key to use for selecting a server in an ambiguous primary scenario | -| version={string} | `DefaultVersion` | (`3.0` in Azure, else `2.0`) | Redis version level (useful when the server does not make this available) | +| asyncTimeout={int} | `AsyncTimeout` | `SyncTimeout` | Time (ms) to allow for asynchronous operations | +| tiebreaker={string} | `TieBreaker` | `__Booksleeve_TieBreak` | Key to use for selecting a server in an ambiguous primary scenario | +| version={string} | `DefaultVersion` | (`4.0` in Azure, else `2.0`) | Redis version level (useful when the server does not make this available) | Additional code-only options: @@ -105,6 +105,8 @@ Additional code-only options: - Determines how commands will be queued (or not) during a disconnect, for sending when it's available again - BeforeSocketConnect - Default: `null` - Allows modifying a `Socket` before connecting (for advanced scenarios) +- SslClientAuthenticationOptions (`netcooreapp3.1`/`net5.0` and higher) - Default: `null` + - Allows specifying exact options for SSL/TLS authentication against a server (e.g. cipher suites, protocols, etc.) - overrides all other SSL configuration options. This is a `Func` which receiveces the host (or `SslHost` if set) to get the options for. If `null` is returned from the `Func`, it's the same as this property not being set at all when connecting. Tokens in the configuration string are comma-separated; any without an `=` sign are assumed to be redis server endpoints. Endpoints without an explicit port will use 6379 if ssl is not enabled, and 6380 if ssl is enabled. Tokens starting with `$` are taken to represent command maps, for example: `$config=cfg`. diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b515c43e6..47f7046f9 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,8 @@ - Performance: Optimization around timeout processing to reduce lock contention in the case of many items that haven't yet timed out during a heartbeat ([#2217 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2217)) - Fix [#2223](https://github.com/StackExchange/StackExchange.Redis/issues/2223): Resolve sync-context issues (missing `ConfigureAwait(false)`) ([#2229 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2229)) - Fix [#1968](https://github.com/StackExchange/StackExchange.Redis/issues/1968): Improved handling of EVAL scripts during server restarts and failovers, detecting and re-sending the script for a retry when needed ([#2170 by martintmk](https://github.com/StackExchange/StackExchange.Redis/pull/2170)) +- Adds: `ConfigurationOptions.SslClientAuthenticationOptions` (`netcoreapp3.1`/`net5.0`+ only) to give more control over SSL/TLS authentication ([#2224 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2224)) + ## 2.6.48 diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 3a746a7d3..12e561c21 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -490,6 +490,14 @@ public int ResponseTimeout /// public SocketManager? SocketManager { get; set; } +#if NETCOREAPP3_1_OR_GREATER + /// + /// A provider for a given host, for custom TLS connection options. + /// Note: this overrides *all* other TLS and certificate settings, only for advanced use cases. + /// + public Func? SslClientAuthenticationOptions { get; set; } +#endif + /// /// Indicates whether the connection should be encrypted. /// @@ -619,6 +627,9 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow checkCertificateRevocation = checkCertificateRevocation, BeforeSocketConnect = BeforeSocketConnect, EndPoints = EndPoints.Clone(), +#if NETCOREAPP3_1_OR_GREATER + SslClientAuthenticationOptions = SslClientAuthenticationOptions, +#endif }; /// diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index f8167bf10..6669cfd13 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -557,8 +557,8 @@ private static bool AllComplete(Task[] tasks) return true; } - internal bool AuthSuspect { get; private set; } - internal void SetAuthSuspect() => AuthSuspect = true; + internal Exception? AuthException { get; private set; } + internal void SetAuthSuspect(Exception authException) => AuthException = authException; /// /// Creates a new instance. diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 4c3a95b2a..0c3f9b2d9 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Reflection; +using System.Security.Authentication; using System.Text; using System.Threading; @@ -383,17 +385,29 @@ private static string GetLabel(bool includeDetail, RedisCommand command, Message internal static Exception UnableToConnect(ConnectionMultiplexer muxer, string? failureMessage = null) { var sb = new StringBuilder("It was not possible to connect to the redis server(s)."); - if (muxer != null) + Exception? inner = null; + if (muxer is not null) { - if (muxer.AuthSuspect) sb.Append(" There was an authentication failure; check that passwords (or client certificates) are configured correctly."); - else if (muxer.RawConfig.AbortOnConnectFail) sb.Append(" Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code."); + if (muxer.AuthException is Exception aex) + { + sb.Append(" There was an authentication failure; check that passwords (or client certificates) are configured correctly: (").Append(aex.GetType().Name).Append(") ").Append(aex.Message); + inner = aex; + if (aex is AuthenticationException && aex.InnerException is Exception iaex) + { + sb.Append(" (Inner - ").Append(iaex.GetType().Name).Append(") ").Append(iaex.Message); + } + } + else if (muxer.RawConfig.AbortOnConnectFail) + { + sb.Append(" Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code."); + } } if (!failureMessage.IsNullOrWhiteSpace()) { sb.Append(' ').Append(failureMessage.Trim()); } - return new RedisConnectionException(ConnectionFailureType.UnableToConnect, sb.ToString()); + return new RedisConnectionException(ConnectionFailureType.UnableToConnect, sb.ToString(), inner); } internal static Exception BeganProfilingWithDuplicateContext(object forContext) diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index b42c9a016..eac909359 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -1432,12 +1432,24 @@ internal async ValueTask ConnectedAsync(Socket socket, LogProxy? log, Sock { try { +#if NETCOREAPP3_1_OR_GREATER + var configOptions = config.SslClientAuthenticationOptions?.Invoke(host); + if (configOptions is not null) + { + await ssl.AuthenticateAsClientAsync(configOptions); + } + else + { + ssl.AuthenticateAsClient(host, config.SslProtocols, config.CheckCertificateRevocation); + } +#else ssl.AuthenticateAsClient(host, config.SslProtocols, config.CheckCertificateRevocation); +#endif } catch (Exception ex) { Debug.WriteLine(ex.Message); - bridge.Multiplexer?.SetAuthSuspect(); + bridge.Multiplexer?.SetAuthSuspect(ex); throw; } log?.WriteLine($"TLS connection established successfully using protocol: {ssl.SslProtocol}"); diff --git a/src/StackExchange.Redis/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt similarity index 100% rename from src/StackExchange.Redis/PublicAPI.Shipped.txt rename to src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt diff --git a/src/StackExchange.Redis/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt similarity index 100% rename from src/StackExchange.Redis/PublicAPI.Unshipped.txt rename to src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt diff --git a/src/StackExchange.Redis/PublicAPI/netcoreapp3.1/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/netcoreapp3.1/PublicAPI.Shipped.txt new file mode 100644 index 000000000..194e1b51b --- /dev/null +++ b/src/StackExchange.Redis/PublicAPI/netcoreapp3.1/PublicAPI.Shipped.txt @@ -0,0 +1,2 @@ +StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.get -> System.Func? +StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.set -> void \ No newline at end of file diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index f14575f2c..fa6639f82 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -227,7 +227,7 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in } if (result.IsError) { - if (result.StartsWith(CommonReplies.NOAUTH)) bridge?.Multiplexer?.SetAuthSuspect(); + if (result.StartsWith(CommonReplies.NOAUTH)) bridge?.Multiplexer?.SetAuthSuspect(new RedisServerException("NOAUTH Returned - connection has not authenticated")); var server = bridge?.ServerEndPoint; bool log = !message.IsInternalCall; @@ -1112,7 +1112,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes SetResult(message, true); return true; } - if(message.Command == RedisCommand.AUTH) connection?.BridgeCouldBeNull?.Multiplexer?.SetAuthSuspect(); + if(message.Command == RedisCommand.AUTH) connection?.BridgeCouldBeNull?.Multiplexer?.SetAuthSuspect(new RedisException("Unknown AUTH exception")); return false; } } diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 30c639a18..d12bbaf3a 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -31,6 +31,14 @@ + + + + + + + + diff --git a/tests/RedisConfigs/Basic/tls-ciphers-6384.conf b/tests/RedisConfigs/Basic/tls-ciphers-6384.conf new file mode 100644 index 000000000..52fc7d7b1 --- /dev/null +++ b/tests/RedisConfigs/Basic/tls-ciphers-6384.conf @@ -0,0 +1,11 @@ +port 0 +tls-port 6384 +timeout 0 +protected-mode no +tls-auth-clients no +tls-ciphers ECDHE-RSA-AES256-GCM-SHA384 +tls-ciphersuites TLS_AES_256_GCM_SHA384 +tls-protocols "TLSv1.2 TLSv1.3" +tls-cert-file /Certs/redis.crt +tls-key-file /Certs/redis.key +tls-ca-cert-file /Certs/ca.crt diff --git a/tests/RedisConfigs/Certs/ca.crt b/tests/RedisConfigs/Certs/ca.crt new file mode 100755 index 000000000..4eee6fbfe --- /dev/null +++ b/tests/RedisConfigs/Certs/ca.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE5jCCAs4CCQCCCQ8gWCbLVjANBgkqhkiG9w0BAQsFADA1MRMwEQYDVQQKDApS +ZWRpcyBUZXN0MR4wHAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjIw +ODI0MTMwOTEwWhcNMzIwODIxMTMwOTEwWjA1MRMwEQYDVQQKDApSZWRpcyBUZXN0 +MR4wHAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDDuKJ+rUI5/MmOdc7PmhKlJrceqwnDsgyVtvV5X6Xn +P8OG2jSXSqpmQn32WCDOL0EldTYS5UPIE0dS7XOYKFJaZnlSZtKBVZaam1+T2mMv +/rNjk3qmJpNiFpjJbktEchiwrsF6l91gsNfRdc1XXku9nvLhjEyhpNRZ7NKLT+Vx +F7h3wkEqLJFwzaAxIPPyvt6aQsip5dRfExFSwCLY4PTGzsvfNNauWASFvgh+zk80 +FFTeDm6AZRmMIgizUc+0JK46QposPZHZA4N9/wmNZ3gAGzIEXvIZ1A5Nn/xMmU/7 +3IRdFkE6pZmaCLA5CwE2M8Z8WyYtPTwLGU9c5yjTKrcX69Dy1hzjyk3H+DsqObuR +rpEcCx6x9SlrJQb0zLcumeqNsXSLdLlUwOgGX/d78J3jYEMSwatnU9wTP26nWhXH +b37sQZz+kh9ZM9rlfhzij4eq/4QtDRzLN0G+y6uveujW+s2LXlhY73K6DP7ujUUW +tCYy0X+iw8YfXHYgYyoby84gYETg0kpR1bjUKQL2PNNf0BOKUjQF9K9IPIiQX0v7 +0YFg/2Fs3fidTVPFCwiLGCQzmy6P9VZQ3EkblHcLtoNAaPieoXdX/s/wMXTqj/hU +b9jwmrqJ2sbEb6VBMrrIgCJqz52zQzE+64KgHCmrQR/ABTCUWhgnsDsUGmaHs8y6 +cwIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQCSg6CZEvBoRqrOCZ+m4z3pQzt/oRxZ +sUz9ZM94pumEOnbEV0AjObqq2LnNTfuyexguH+5HH4Dr2OTvDIL7xNMor7NSUXGA +yhiu+BxiS1tB1o6IyExwyjpbS61iqtUX09abSWP/+2UW1i8oEIuwUmkRlAxbxFWI +HN6+LFe1L32fbrbp90sRS96bljvxNBxGpYqcooLAHCbK2T6jHDAZF0cK5ByWZoJ4 +FcD3tRYWelj8k80ZeoG4PIsCZylsSMPWeglbFqDV4gSpWx7nb4Pgpzs9REp02Cp0 +4MWxAt2fmvPFn9xypeyo6gxZ+R2cmSKiu0sdVnp3u1RscH1aGnVJTpdygpuDYJQ7 +hxn1Wv91zRi+h4MfVywSO/3gMIvdiJIiV7avgNEWiLXYUn2bb4gHzEMOrp2Z7BUp +/SwNHmikaWQj0MY6sOW7pOaistbokyyZw8TjgrTnS4GocN91h16JbuSgAI+Nrwsa +VcFvDCd7qSmJgeMfGhhlOdNenDGXsA9UVyTTkGfw3mgcm62uvewmj1I70ogk8M3z +khwAMI2OeagmHtXtjtg2BULM53IwFHJKV41B4rwekcMkboCsOfbhZwz42aLpT0YG +d0btKJkNcL7n8QiGtFmvreizrdyC5r23GNVnNdn2dhuJBqN65xJQoXh0x1PTnK7/ +4IWfRo8kosNhmw== +-----END CERTIFICATE----- diff --git a/tests/RedisConfigs/Certs/redis.crt b/tests/RedisConfigs/Certs/redis.crt new file mode 100755 index 000000000..cb69138e7 --- /dev/null +++ b/tests/RedisConfigs/Certs/redis.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID3TCCAcUCCQDIqu2SpngxXjANBgkqhkiG9w0BAQsFADA1MRMwEQYDVQQKDApS +ZWRpcyBUZXN0MR4wHAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjIw +ODI0MTMwOTEwWhcNMjMwODI0MTMwOTEwWjAsMRMwEQYDVQQKDApSZWRpcyBUZXN0 +MRUwEwYDVQQDDAxHZW5lcmljLWNlcnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDCtcwbyBNSBgM4Ym72Sz7grrpYtx43JQtgiz4o7rYfCjoxkEcWic2J +3/UC2nqbtmb2vUOTyqxe5VUh6bXHB3OaZfLkyyGJM8dJN3p3rC8Ef4zr2CkCzpAK +jquXz0do9epXrUZCsYSdw1pOZDsRXx9ZgImtvB5Yj0UXfatAQvt9xk7UdxIrNDs4 +r0V34gZVvU4OhFnTEQVwLYqi0VOiknKRtW9BaD0niObjdc+aMmVYBo00G0UmFO4A +UuanO6PJz3FiX+ejY+aViBCD4lJUbuH719/EwWXYNxXbZasC5I0EE6zU0PEOcECm +cbWlSS23eid06HuaqRmcEwTNKPk0/CVjAgMBAAEwDQYJKoZIhvcNAQELBQADggIB +AJOtU8CutoZncckfG+6YL1raeqAcnCYvm0bL3RTs4OYMwHDPOIBCSG6kqyIKiTc2 +uU2G2XUZcWs2a3MxhoOgZhl0TDQBgSQMnMcr/d91IBNwUnRWA553KSpBhOn31SGr +fo8U4IOMz9I/wJ05AFt0bE4WDfm73tiwsIx/2SMn75/d5UgY+II7epx+MpIrWGpT +SwBbm7is9Go/Mwr1bdNy35lrUAL+Si80aHhVPWa+bIFqyqsWal+iZZND+NrqilJe +y27Syhikq0R+U8gPjSdIT2OYj7kwrUZI1exOzpUDa9gUjfy13+lLJWxPbgQEc7Uq +hyu6+CaY9q9YNT6eIIymdLtGTSs/rMYLACHylS/J4WNXr/YCmk2xhGqGDlPq3wjw +Q5WtmdHDaSQXo2+H9fQbw2N2loQ29Gcz4FEgF1CVhbuCZUstelDl6F38cvgRHPrY +gLro6ijlxtfvka6GOZZeVksJWaW9ikAz+aw3yqKQoFMnILjvwxpuCTphvgvlKIb4 +TFg5DU+a+RHW/S3qP3PCatw+f/FaFkRavD2P9oNz0XAcmLld0iWbDXHntDBF1q0N +c9bgdoP9pVS+NKb6Hq/zf2kUC7AseUiLAju5iMQVglunhNcbm/H6RnxfnYekUMkp +DdenAmOqjXa/n5IQkfwOxW97EJyI9SGo3Is+DKgUEmd4 +-----END CERTIFICATE----- diff --git a/tests/RedisConfigs/Certs/redis.key b/tests/RedisConfigs/Certs/redis.key new file mode 100755 index 000000000..56f301528 --- /dev/null +++ b/tests/RedisConfigs/Certs/redis.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwrXMG8gTUgYDOGJu9ks+4K66WLceNyULYIs+KO62Hwo6MZBH +FonNid/1Atp6m7Zm9r1Dk8qsXuVVIem1xwdzmmXy5MshiTPHSTd6d6wvBH+M69gp +As6QCo6rl89HaPXqV61GQrGEncNaTmQ7EV8fWYCJrbweWI9FF32rQEL7fcZO1HcS +KzQ7OK9Fd+IGVb1ODoRZ0xEFcC2KotFTopJykbVvQWg9J4jm43XPmjJlWAaNNBtF +JhTuAFLmpzujyc9xYl/no2PmlYgQg+JSVG7h+9ffxMFl2DcV22WrAuSNBBOs1NDx +DnBApnG1pUktt3ondOh7mqkZnBMEzSj5NPwlYwIDAQABAoIBAQC8kO2r1hcH77S8 +rW+C7Rpm5DCp7CXCCAk9pXw8jfook3IKQAzogephZVhWPBpTpNGQkXjZr4VBnd3V +qw4VQ10soSEbfLHsuw18FdNwBHvAYnqqiTwmcL/Eyajaq64fs1EROkj6HAsv8loJ +4z3lM/cbacVsUOwenhmuh1ELOhNvGKQuCzSpCoVykP2cWCMnqHEl43Ilqm5tga23 +PtMJS1jM6IazE6EzfelwuGGCEmKK5EKeDHB+3PU4sUSHfXv/l17adSJtDbiK/JiH +2Op3DzSGWZ2xhkYt35Oj7auxJ90f3BoG0/JZABdiaZu3DOgp9JNzqr1sH4rFPWfe +dWBk665JAoGBAPrRhcjkHmRVvQwViGrzE8RUqblW6Wd6xVcf7KbparKjcFbPkoo1 +3NJpKonkvyLj3WQdltXNXuvM+QcdT2TNFv+hywkCC7Wxb/MPZ2eF+nRBMg59209T +eAWjq9GflPn7uO/4jnfCLCR+DNiEvctJ1nHf0qBTVC+s+QyhO9ilZd1PAoGBAMa7 +imK7XH7kX0zpsafPLiORr7XvOzDo/NE/kpKHisdre8VL397KlVsQmQldx33zRa7g +ctCIGjQcsnitpa24vS2G4wru3fqGbKqf3tASoC9yNMRxIBDxlhsASe0TczRw4HKT +i2HMlb7rDZdXa9mY+eDszOUUnGtkmX/D372fcTmtAoGBAOOpFoQX+zYbVLMJQH/D +D2gfaMbgCo9wsnq4cXe3Wq+3Bhrl4h8tcLhT2Na9GHi015kt+mEqPkROEqPQiOX3 ++i4iT0Zn4vUSj4jRrIwc4g5vtt3Mgynnm4OS4jwtW23kfCLlO3ucdbDR8Rr+sb85 +0DogbPA1cq6rlItQNiAZUPKlAoGAKqEL/EXIf4epUaxHaYGtmf+kO1iHz+QKZzBF +1pywjjpmIFo4OWgnRZN34GR3aHMInYyT1Ft9k3QcbHqDMZKRMfTfOvcmMpknMip8 +9xEnv0W2P/UsNbY8xqn3MZ2cdsFHxAwWN/JUpNFy5uXfwptn7nGdOf6D1x2LN7bi +haBv/zkCgYAqSHcp5ETqJMMz/v3H3eLDu/Xc//SdyqldKEjmv+kd0BYQNqHB9rEB +B4rtRVeWUZ6TA5x1T8dK5OaDZ+W+vdnzmOGw27eFuD+203m76+3cJS37mroEcNPt +5npe1IydjS2qU8iA8lhDeIWr2dTnrQnBtgkKiJvYbP2XG5/LahxixA== +-----END RSA PRIVATE KEY----- diff --git a/tests/RedisConfigs/Docker/supervisord.conf b/tests/RedisConfigs/Docker/supervisord.conf index b828ead92..21fe45f1b 100644 --- a/tests/RedisConfigs/Docker/supervisord.conf +++ b/tests/RedisConfigs/Docker/supervisord.conf @@ -22,6 +22,13 @@ stdout_logfile=/var/log/supervisor/%(program_name)s.log stderr_logfile=/var/log/supervisor/%(program_name)s.log autorestart=true +[program:tls-6384] +command=/usr/local/bin/redis-server /data/Basic/tls-ciphers-6384.conf +directory=/data/Basic +stdout_logfile=/var/log/supervisor/%(program_name)s.log +stderr_logfile=/var/log/supervisor/%(program_name)s.log +autorestart=true + [program:primary-6382] command=/usr/local/bin/redis-server /data/Failover/primary-6382.conf directory=/data/Failover diff --git a/tests/RedisConfigs/Dockerfile b/tests/RedisConfigs/Dockerfile index cba8d6af5..3517b4de2 100644 --- a/tests/RedisConfigs/Dockerfile +++ b/tests/RedisConfigs/Dockerfile @@ -4,8 +4,10 @@ COPY Basic /data/Basic/ COPY Failover /data/Failover/ COPY Cluster /data/Cluster/ COPY Sentinel /data/Sentinel/ +COPY Certs /Certs/ RUN chown -R redis:redis /data +RUN chown -R redis:redis /Certs COPY Docker/docker-entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/docker-entrypoint.sh @@ -22,4 +24,4 @@ ADD Docker/supervisord.conf /etc/ ENTRYPOINT ["docker-entrypoint.sh"] -EXPOSE 6379 6380 6381 6382 6383 7000 7001 7002 7003 7004 7005 7010 7011 7015 26379 26380 26381 +EXPOSE 6379 6380 6381 6382 6383 6384 7000 7001 7002 7003 7004 7005 7010 7011 7015 26379 26380 26381 diff --git a/tests/RedisConfigs/docker-compose.yml b/tests/RedisConfigs/docker-compose.yml index afc4cbf3c..e27bec0b8 100644 --- a/tests/RedisConfigs/docker-compose.yml +++ b/tests/RedisConfigs/docker-compose.yml @@ -7,7 +7,7 @@ services: image: stackexchange/redis-tests:latest platform: linux ports: - - 6379-6383:6379-6383 + - 6379-6384:6379-6384 - 7000-7006:7000-7006 - 7010-7011:7010-7011 - 7015:7015 diff --git a/tests/RedisConfigs/start-all.sh b/tests/RedisConfigs/start-all.sh index 3f0a5c910..792d75a5f 100644 --- a/tests/RedisConfigs/start-all.sh +++ b/tests/RedisConfigs/start-all.sh @@ -10,6 +10,8 @@ echo "${INDENT}Replica: 6380" redis-server replica-6380.conf &>/dev/null & echo "${INDENT}Secure: 6381" redis-server secure-6381.conf &>/dev/null & +echo "${INDENT}Tls: 6384" +redis-server tls-ciphers-6384.conf &>/dev/null & popd > /dev/null #Failover Servers diff --git a/tests/RedisConfigs/start-basic.cmd b/tests/RedisConfigs/start-basic.cmd index 723e30f98..16bec8780 100644 --- a/tests/RedisConfigs/start-basic.cmd +++ b/tests/RedisConfigs/start-basic.cmd @@ -7,4 +7,7 @@ echo Replica: 6380 @start "Redis (Replica): 6380" /min ..\3.0.503\redis-server.exe replica-6380.conf echo Secure: 6381 @start "Redis (Secure): 6381" /min ..\3.0.503\redis-server.exe secure-6381.conf +@REM TLS config doesn't work in 3.x - don't even start it +@REM echo TLS: 6384 +@REM @start "Redis (TLS): 6384" /min ..\3.0.503\redis-server.exe tls-ciphers-6384.conf popd \ No newline at end of file diff --git a/tests/RedisConfigs/start-basic.sh b/tests/RedisConfigs/start-basic.sh index 35a98a335..4c35ea5c1 100644 --- a/tests/RedisConfigs/start-basic.sh +++ b/tests/RedisConfigs/start-basic.sh @@ -10,6 +10,8 @@ echo "${INDENT}Replica: 6380" redis-server replica-6380.conf &>/dev/null & echo "${INDENT}Secure: 6381" redis-server secure-6381.conf &>/dev/null & +echo "${INDENT}Tls: 6384" +redis-server tls-ciphers-6384.conf &>/dev/null & popd > /dev/null echo Servers started. \ No newline at end of file diff --git a/tests/StackExchange.Redis.Tests/BasicOps.cs b/tests/StackExchange.Redis.Tests/BasicOps.cs index 63ae438f1..60f260dc5 100644 --- a/tests/StackExchange.Redis.Tests/BasicOps.cs +++ b/tests/StackExchange.Redis.Tests/BasicOps.cs @@ -23,7 +23,7 @@ public async Task PingOnce() Assert.True(duration.TotalMilliseconds > 0); } - [Fact] + [Fact(Skip = "This needs some CI love, it's not a scenario we care about too much but noisy atm.")] public async Task RapidDispose() { using var primary = Create(); diff --git a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs index f7d5c3c63..6c86e10ab 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using System.Threading; using System.Linq; +using System.Net.Sockets; namespace StackExchange.Redis.Tests; @@ -42,6 +43,24 @@ static TestConfig() } } + public static bool IsServerRunning(string? host, int port) + { + if (host.IsNullOrEmpty()) + { + return false; + } + + try + { + using var client = new TcpClient(host, port); + return true; + } + catch (SocketException) + { + return false; + } + } + public class Config { public bool UseSharedConnection { get; set; } = true; @@ -90,8 +109,9 @@ public class Config public int ClusterServerCount { get; set; } = 6; public string ClusterServersAndPorts => string.Join(",", Enumerable.Range(ClusterStartPort, ClusterServerCount).Select(port => ClusterServer + ":" + port)); - public string? SslServer { get; set; } - public int SslPort { get; set; } + public string? SslServer { get; set; } = "127.0.0.1"; + public int SslPort { get; set; } = 6384; + public string SslServerAndPort => SslServer + ":" + SslPort.ToString(); public string? RedisLabsSslServer { get; set; } public int RedisLabsSslPort { get; set; } = 6379; diff --git a/tests/StackExchange.Redis.Tests/SSL.cs b/tests/StackExchange.Redis.Tests/SSL.cs index ad09b3fe0..13863a605 100644 --- a/tests/StackExchange.Redis.Tests/SSL.cs +++ b/tests/StackExchange.Redis.Tests/SSL.cs @@ -9,6 +9,7 @@ using System.Reflection; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; +using System.Text; using System.Threading.Tasks; using StackExchange.Redis.Tests.Helpers; using Xunit; @@ -16,9 +17,11 @@ namespace StackExchange.Redis.Tests; -public class SSL : TestBase +public class SSL : TestBase, IClassFixture { - public SSL(ITestOutputHelper output) : base(output) { } + private SSLServerFixture Fixture { get; } + + public SSL(ITestOutputHelper output, SSLServerFixture fixture) : base(output) => Fixture = fixture; [Theory] [InlineData(null, true)] // auto-infer port (but specify 6380) @@ -55,6 +58,8 @@ public void ConnectToAzure(int? port, bool ssl) [InlineData(true, true)] public async Task ConnectToSSLServer(bool useSsl, bool specifyHost) { + Fixture.SkipIfNoServer(); + var server = TestConfig.Current.SslServer; int? port = TestConfig.Current.SslPort; string? password = ""; @@ -104,58 +109,137 @@ public async Task ConnectToSSLServer(bool useSsl, bool specifyHost) var clone = ConfigurationOptions.Parse(configString); Assert.Equal(configString, clone.ToString()); - using var log = new StringWriter(); - using var conn = ConnectionMultiplexer.Connect(config, log); + var log = new StringBuilder(); + Writer.EchoTo(log); - Log("Connect log:"); - lock (log) + if (useSsl) { - Log(log.ToString()); + using var conn = ConnectionMultiplexer.Connect(config, Writer); + + Log("Connect log:"); + lock (log) + { + Log(log.ToString()); + } + Log("===="); + conn.ConnectionFailed += OnConnectionFailed; + conn.InternalError += OnInternalError; + var db = conn.GetDatabase(); + await db.PingAsync().ForAwait(); + using (var file = File.Create("ssl-" + useSsl + "-" + specifyHost + ".zip")) + { + conn.ExportConfiguration(file); + } + RedisKey key = "SE.Redis"; + + const int AsyncLoop = 2000; + // perf; async + await db.KeyDeleteAsync(key).ForAwait(); + var watch = Stopwatch.StartNew(); + for (int i = 0; i < AsyncLoop; i++) + { + try + { + await db.StringIncrementAsync(key, flags: CommandFlags.FireAndForget).ForAwait(); + } + catch (Exception ex) + { + Log($"Failure on i={i}: {ex.Message}"); + throw; + } + } + // need to do this inside the timer to measure the TTLB + long value = (long)await db.StringGetAsync(key).ForAwait(); + watch.Stop(); + Assert.Equal(AsyncLoop, value); + Log("F&F: {0} INCR, {1:###,##0}ms, {2} ops/s; final value: {3}", + AsyncLoop, + watch.ElapsedMilliseconds, + (long)(AsyncLoop / watch.Elapsed.TotalSeconds), + value); + + // perf: sync/multi-threaded + // TestConcurrent(db, key, 30, 10); + //TestConcurrent(db, key, 30, 20); + //TestConcurrent(db, key, 30, 30); + //TestConcurrent(db, key, 30, 40); + //TestConcurrent(db, key, 30, 50); } - Log("===="); - conn.ConnectionFailed += OnConnectionFailed; - conn.InternalError += OnInternalError; - var db = conn.GetDatabase(); - await db.PingAsync().ForAwait(); - using (var file = File.Create("ssl-" + useSsl + "-" + specifyHost + ".zip")) + else { - conn.ExportConfiguration(file); + Assert.Throws(() => ConnectionMultiplexer.Connect(config, Writer)); } - RedisKey key = "SE.Redis"; + } + +#if NETCOREAPP3_1_OR_GREATER +#pragma warning disable CS0618 // Type or member is obsolete + // Docker configured with only TLS_AES_256_GCM_SHA384 for testing + [Theory] + [InlineData(SslProtocols.None, true, TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TlsCipherSuite.TLS_AES_256_GCM_SHA384)] + [InlineData(SslProtocols.Tls12 , true, TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TlsCipherSuite.TLS_AES_256_GCM_SHA384)] + [InlineData(SslProtocols.Tls13 , true, TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TlsCipherSuite.TLS_AES_256_GCM_SHA384)] + [InlineData(SslProtocols.Tls12, false, TlsCipherSuite.TLS_AES_128_CCM_8_SHA256)] + [InlineData(SslProtocols.Tls12, true)] + [InlineData(SslProtocols.Tls13, true)] + [InlineData(SslProtocols.Ssl2, false)] + [InlineData(SslProtocols.Ssl3, false)] + [InlineData(SslProtocols.Tls12 | SslProtocols.Tls13, true)] + [InlineData(SslProtocols.Ssl3 | SslProtocols.Tls12 | SslProtocols.Tls13, true)] + [InlineData(SslProtocols.Ssl2, false, TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TlsCipherSuite.TLS_AES_256_GCM_SHA384)] +#pragma warning restore CS0618 // Type or member is obsolete + public async Task ConnectSslClientAuthenticationOptions(SslProtocols protocols, bool expectSuccess, params TlsCipherSuite[] tlsCipherSuites) + { + Fixture.SkipIfNoServer(); - const int AsyncLoop = 2000; - // perf; async - await db.KeyDeleteAsync(key).ForAwait(); - var watch = Stopwatch.StartNew(); - for (int i = 0; i < AsyncLoop; i++) + var config = new ConfigurationOptions() { - try + EndPoints = { TestConfig.Current.SslServerAndPort }, + AllowAdmin = true, + ConnectRetry = 1, + SyncTimeout = Debugger.IsAttached ? int.MaxValue : 5000, + Ssl = true, + SslClientAuthenticationOptions = host => new SslClientAuthenticationOptions() { - await db.StringIncrementAsync(key, flags: CommandFlags.FireAndForget).ForAwait(); + TargetHost = host, + CertificateRevocationCheckMode = X509RevocationMode.NoCheck, + EnabledSslProtocols = protocols, + CipherSuitesPolicy = tlsCipherSuites?.Length > 0 ? new CipherSuitesPolicy(tlsCipherSuites) : null, + RemoteCertificateValidationCallback = (sender, cert, chain, errors) => + { + Log(" Errors: " + errors); + Log(" Cert issued to: " + cert?.Subject); + return true; + } } - catch (Exception ex) + }; + + try + { + if (expectSuccess) + { + using var conn = await ConnectionMultiplexer.ConnectAsync(config, Writer); + + var db = conn.GetDatabase(); + Log("Pinging..."); + var time = await db.PingAsync().ForAwait(); + Log($"Ping time: {time}"); + } + else { - Log($"Failure on i={i}: {ex.Message}"); - throw; + var ex = await Assert.ThrowsAsync(() => ConnectionMultiplexer.ConnectAsync(config, Writer)); + Log("(Expected) Failure connecting: " + ex.Message); + if (ex.InnerException is PlatformNotSupportedException pnse) + { + Skip.Inconclusive("Expected failure, but also test not supported on this platform: " + pnse.Message); + } } } - // need to do this inside the timer to measure the TTLB - long value = (long)await db.StringGetAsync(key).ForAwait(); - watch.Stop(); - Assert.Equal(AsyncLoop, value); - Log("F&F: {0} INCR, {1:###,##0}ms, {2} ops/s; final value: {3}", - AsyncLoop, - watch.ElapsedMilliseconds, - (long)(AsyncLoop / watch.Elapsed.TotalSeconds), - value); - - // perf: sync/multi-threaded - // TestConcurrent(db, key, 30, 10); - //TestConcurrent(db, key, 30, 20); - //TestConcurrent(db, key, 30, 30); - //TestConcurrent(db, key, 30, 40); - //TestConcurrent(db, key, 30, 50); + catch (RedisException ex) when (ex.InnerException is PlatformNotSupportedException pnse) + { + Skip.Inconclusive("Test not supported on this platform: " + pnse.Message); + } } +#endif [Fact] public void RedisLabsSSL() @@ -280,15 +364,15 @@ public void RedisLabsEnvironmentVariableClientCertificate(bool setEnv) [Fact] public void SSLHostInferredFromEndpoints() { - var options = new ConfigurationOptions() + var options = new ConfigurationOptions { EndPoints = { { "mycache.rediscache.windows.net", 15000}, { "mycache.rediscache.windows.net", 15001 }, { "mycache.rediscache.windows.net", 15002 }, - } + }, + Ssl = true, }; - options.Ssl = true; Assert.True(options.SslHost == "mycache.rediscache.windows.net"); options = new ConfigurationOptions() { @@ -460,4 +544,25 @@ public void ConfigObject_Issue1407_ToStringIncludesSslProtocols() var targetOptions = ConfigurationOptions.Parse(sourceOptions.ToString()); Assert.Equal(sourceOptions.SslProtocols, targetOptions.SslProtocols); } + + public class SSLServerFixture : IDisposable + { + public bool ServerRunning { get; } + + public SSLServerFixture() + { + ServerRunning = TestConfig.IsServerRunning(TestConfig.Current.SslServer, TestConfig.Current.SslPort); + } + + public void SkipIfNoServer() + { + Skip.IfNoConfig(nameof(TestConfig.Config.SslServer), TestConfig.Current.SslServer); + if (!ServerRunning) + { + Skip.Inconclusive($"SSL/TLS Server was not running at {TestConfig.Current.SslServer}:{TestConfig.Current.SslPort}"); + } + } + + public void Dispose() { } + } } diff --git a/tests/StackExchange.Redis.Tests/Secure.cs b/tests/StackExchange.Redis.Tests/Secure.cs index fe4418c69..bdebef2e0 100644 --- a/tests/StackExchange.Redis.Tests/Secure.cs +++ b/tests/StackExchange.Redis.Tests/Secure.cs @@ -74,6 +74,6 @@ public async Task ConnectWithWrongPassword(string password) conn.GetDatabase().Ping(); }).ConfigureAwait(false); Log("Exception: " + ex.Message); - Assert.StartsWith("It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly.", ex.Message); + Assert.StartsWith("It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly: (RedisServerException) NOAUTH Returned - connection has not authenticated", ex.Message); } } From a0ddbf8a7845e11f5cfc05bf77e426d61912d3fe Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 3 Sep 2022 11:21:45 -0400 Subject: [PATCH 178/435] Fix for #2240 (#2241) Follow-up to #900, trying to fix this a bit cleaner for dual IPv4/v6 DNS scenarios (the overload docs point to this solution). --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/SocketManager.cs | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 47f7046f9..9b1a8e00c 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ - Fix [#2223](https://github.com/StackExchange/StackExchange.Redis/issues/2223): Resolve sync-context issues (missing `ConfigureAwait(false)`) ([#2229 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2229)) - Fix [#1968](https://github.com/StackExchange/StackExchange.Redis/issues/1968): Improved handling of EVAL scripts during server restarts and failovers, detecting and re-sending the script for a retry when needed ([#2170 by martintmk](https://github.com/StackExchange/StackExchange.Redis/pull/2170)) - Adds: `ConfigurationOptions.SslClientAuthenticationOptions` (`netcoreapp3.1`/`net5.0`+ only) to give more control over SSL/TLS authentication ([#2224 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2224)) +- Fix [#2240](https://github.com/StackExchange/StackExchange.Redis/pull/2241): Improve support for DNS-based IPv6 endpoints ([#2241 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2241)) ## 2.6.48 diff --git a/src/StackExchange.Redis/SocketManager.cs b/src/StackExchange.Redis/SocketManager.cs index bf1918660..a0c755834 100644 --- a/src/StackExchange.Redis/SocketManager.cs +++ b/src/StackExchange.Redis/SocketManager.cs @@ -214,13 +214,11 @@ private void DisposeRefs() internal static Socket CreateSocket(EndPoint endpoint) { var addressFamily = endpoint.AddressFamily; - if (addressFamily == AddressFamily.Unspecified && endpoint is DnsEndPoint) - { // default DNS to ipv4 if not specified explicitly - addressFamily = AddressFamily.InterNetwork; - } - var protocolType = addressFamily == AddressFamily.Unix ? ProtocolType.Unspecified : ProtocolType.Tcp; - var socket = new Socket(addressFamily, SocketType.Stream, protocolType); + + var socket = addressFamily == AddressFamily.Unspecified + ? new Socket(SocketType.Stream, protocolType) + : new Socket(addressFamily, SocketType.Stream, protocolType); SocketConnection.SetRecommendedClientOptions(socket); //socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Linger, false); return socket; From 384d342eccdcc83e9354699b310ca2ee5e29ac84 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 6 Sep 2022 10:39:48 -0400 Subject: [PATCH 179/435] Docs: add SocketManager (#2245) Fixes #1419 - adding brief descriptions for primary `SocketManager` options. --- docs/Configuration.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/Configuration.md b/docs/Configuration.md index 68556dba6..5d4590383 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -107,6 +107,10 @@ Additional code-only options: - Allows modifying a `Socket` before connecting (for advanced scenarios) - SslClientAuthenticationOptions (`netcooreapp3.1`/`net5.0` and higher) - Default: `null` - Allows specifying exact options for SSL/TLS authentication against a server (e.g. cipher suites, protocols, etc.) - overrides all other SSL configuration options. This is a `Func` which receiveces the host (or `SslHost` if set) to get the options for. If `null` is returned from the `Func`, it's the same as this property not being set at all when connecting. +- SocketManager - Default: `SocketManager.Shared`: + - The thread pool to use for scheduling work to and from the socket connected to Redis, one of... + - `SocketManager.Shared`: Use a shared dedicated thread pool for _all_ multiplexers (defaults to 10 threads) - best balance for most scenarios. + - `SocketManager.ThreadPool`: Use the build-in .NET thread pool for scheduling. This can perform better for very small numbers of cores or with large apps on large machines that need to use more than 10 threads (total, across all multiplexers) under load. **Important**: this option isn't the default because it's subject to thread pool growth/starvation and if for example synchronous calls are waiting on a redis command to come back to unblock other threads, stalls/hangs can result. Use with caution, especially if you have sync-over-async work in play. Tokens in the configuration string are comma-separated; any without an `=` sign are assumed to be redis server endpoints. Endpoints without an explicit port will use 6379 if ssl is not enabled, and 6380 if ssl is enabled. Tokens starting with `$` are taken to represent command maps, for example: `$config=cfg`. From 846e096ee35f432c2e003b8e99c322235c0cb5fc Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 6 Sep 2022 10:48:38 -0400 Subject: [PATCH 180/435] Sentinel: fix repeat connects (#2242) Fixes #2233. There were 2 issues here contributing: 1. Sentinel was modifying the config options, which we no longer clone. Now, we clone the endpoints and pass them down as explicit overrides into the child connection. 2. EndpointCollection...wasn't actually cloning. I can't believe this didn't show up elsewhere. Collection takes a ref directly without copy of members when forming, so even if ew did clone...we'd be modifying the same thing. --- docs/ReleaseNotes.md | 1 + .../ConnectionMultiplexer.Sentinel.cs | 17 +++++---- .../ConnectionMultiplexer.cs | 12 +++---- src/StackExchange.Redis/EndPointCollection.cs | 2 +- tests/StackExchange.Redis.Tests/Sentinel.cs | 36 ++++++++++++++++++- 5 files changed, 53 insertions(+), 15 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 9b1a8e00c..a7772982d 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -11,6 +11,7 @@ - Fix [#1968](https://github.com/StackExchange/StackExchange.Redis/issues/1968): Improved handling of EVAL scripts during server restarts and failovers, detecting and re-sending the script for a retry when needed ([#2170 by martintmk](https://github.com/StackExchange/StackExchange.Redis/pull/2170)) - Adds: `ConfigurationOptions.SslClientAuthenticationOptions` (`netcoreapp3.1`/`net5.0`+ only) to give more control over SSL/TLS authentication ([#2224 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2224)) - Fix [#2240](https://github.com/StackExchange/StackExchange.Redis/pull/2241): Improve support for DNS-based IPv6 endpoints ([#2241 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2241)) +- Fix [#2233](https://github.com/StackExchange/StackExchange.Redis/issues/2233): Repeated connection to Sentinel servers using the same ConfigurationOptions would fail ([#2242 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2242)) ## 2.6.48 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs index 05305cc47..eb0b70c77 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs @@ -181,6 +181,7 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co bool success = false; ConnectionMultiplexer? connection = null; + EndPointCollection? endpoints = null; var sw = ValueStopwatch.StartNew(); do @@ -206,27 +207,29 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co replicaEndPoints = GetReplicasForService(serviceName); } + endpoints = config.EndPoints.Clone(); + // Replace the primary endpoint, if we found another one // If not, assume the last state is the best we have and minimize the race - if (config.EndPoints.Count == 1) + if (endpoints.Count == 1) { - config.EndPoints[0] = newPrimaryEndPoint; + endpoints[0] = newPrimaryEndPoint; } else { - config.EndPoints.Clear(); - config.EndPoints.TryAdd(newPrimaryEndPoint); + endpoints.Clear(); + endpoints.TryAdd(newPrimaryEndPoint); } if (replicaEndPoints is not null) { foreach (var replicaEndPoint in replicaEndPoints) { - config.EndPoints.TryAdd(replicaEndPoint); + endpoints.TryAdd(replicaEndPoint); } } - connection = ConnectImpl(config, log); + connection = ConnectImpl(config, log, endpoints: endpoints); // verify role is primary according to: // https://redis.io/topics/sentinel-clients @@ -257,7 +260,7 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co } // Perform the initial switchover - SwitchPrimary(EndPoints[0], connection, log); + SwitchPrimary(endpoints[0], connection, log); return connection; } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 6669cfd13..405e87591 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -140,10 +140,10 @@ static ConnectionMultiplexer() SetAutodetectFeatureFlags(); } - private ConnectionMultiplexer(ConfigurationOptions configuration, ServerType? serverType = null) + private ConnectionMultiplexer(ConfigurationOptions configuration, ServerType? serverType = null, EndPointCollection? endpoints = null) { RawConfig = configuration ?? throw new ArgumentNullException(nameof(configuration)); - EndPoints = RawConfig.EndPoints.Clone(); + EndPoints = endpoints ?? RawConfig.EndPoints.Clone(); EndPoints.SetDefaultPorts(serverType, ssl: RawConfig.Ssl); var map = CommandMap = configuration.GetCommandMap(serverType); @@ -169,9 +169,9 @@ private ConnectionMultiplexer(ConfigurationOptions configuration, ServerType? se lastHeartbeatTicks = Environment.TickCount; } - private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions configuration, LogProxy? log, ServerType? serverType, out EventHandler? connectHandler) + private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions configuration, LogProxy? log, ServerType? serverType, out EventHandler? connectHandler, EndPointCollection? endpoints = null) { - var muxer = new ConnectionMultiplexer(configuration, serverType); + var muxer = new ConnectionMultiplexer(configuration, serverType, endpoints); connectHandler = null; if (log is not null) { @@ -679,7 +679,7 @@ public static ConnectionMultiplexer Connect(ConfigurationOptions configuration, : ConnectImpl(configuration, log); } - private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configuration, TextWriter? log, ServerType? serverType = null) + private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configuration, TextWriter? log, ServerType? serverType = null, EndPointCollection? endpoints = null) { IDisposable? killMe = null; EventHandler? connectHandler = null; @@ -690,7 +690,7 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat var sw = ValueStopwatch.StartNew(); logProxy?.WriteLine($"Connecting (sync) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); - muxer = CreateMultiplexer(configuration, logProxy, serverType, out connectHandler); + muxer = CreateMultiplexer(configuration, logProxy, serverType, out connectHandler, endpoints); killMe = muxer; Interlocked.Increment(ref muxer._connectAttemptCount); // note that task has timeouts internally, so it might take *just over* the regular timeout diff --git a/src/StackExchange.Redis/EndPointCollection.cs b/src/StackExchange.Redis/EndPointCollection.cs index 1b6095932..29b223f24 100644 --- a/src/StackExchange.Redis/EndPointCollection.cs +++ b/src/StackExchange.Redis/EndPointCollection.cs @@ -228,6 +228,6 @@ internal async Task ResolveEndPointsAsync(ConnectionMultiplexer multiplexer, Log } } - internal EndPointCollection Clone() => new EndPointCollection(this); + internal EndPointCollection Clone() => new EndPointCollection(new List(Items)); } } diff --git a/tests/StackExchange.Redis.Tests/Sentinel.cs b/tests/StackExchange.Redis.Tests/Sentinel.cs index e277c4883..df8768a89 100644 --- a/tests/StackExchange.Redis.Tests/Sentinel.cs +++ b/tests/StackExchange.Redis.Tests/Sentinel.cs @@ -90,7 +90,7 @@ public void SentinelConnectTest() { var options = ServiceOptions.Clone(); options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA); - var conn = ConnectionMultiplexer.SentinelConnect(options); + using var conn = ConnectionMultiplexer.SentinelConnect(options); var db = conn.GetDatabase(); var test = db.Ping(); @@ -98,6 +98,40 @@ public void SentinelConnectTest() TestConfig.Current.SentinelPortA, test.TotalMilliseconds); } + [Fact] + public void SentinelRepeatConnectTest() + { + var options = ConfigurationOptions.Parse($"{TestConfig.Current.SentinelServer}:{TestConfig.Current.SentinelPortA}"); + options.ServiceName = ServiceName; + options.AllowAdmin = true; + + Log("Service Name: " + options.ServiceName); + foreach (var ep in options.EndPoints) + { + Log(" Endpoint: " + ep); + } + + using var conn = ConnectionMultiplexer.Connect(options); + + var db = conn.GetDatabase(); + var test = db.Ping(); + Log("ping to 1st sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, + TestConfig.Current.SentinelPortA, test.TotalMilliseconds); + + Log("Service Name: " + options.ServiceName); + foreach (var ep in options.EndPoints) + { + Log(" Endpoint: " + ep); + } + + using var conn2 = ConnectionMultiplexer.Connect(options); + + var db2 = conn2.GetDatabase(); + var test2 = db2.Ping(); + Log("ping to 2nd sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, + TestConfig.Current.SentinelPortA, test2.TotalMilliseconds); + } + [Fact] public async Task SentinelConnectAsyncTest() { From 5ceb775ca05a99226a0d88ec00f72013c08f856c Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 6 Sep 2022 11:08:13 -0400 Subject: [PATCH 181/435] Auth: Better exception for bad passwords (#2246) Fixes #1879. In the case where a password is wrong, now indicates "WRONGPASS invalid username-password pair or user is disabled" rather than "NOAUTH Returned - connection has not authenticated" to help users diagnose their problem a bit easier. --- docs/ReleaseNotes.md | 1 + .../ConnectionMultiplexer.cs | 2 +- src/StackExchange.Redis/RedisLiterals.cs | 1 + src/StackExchange.Redis/ResultProcessor.cs | 9 ++++++- tests/StackExchange.Redis.Tests/Secure.cs | 25 ++++++++++++++----- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index a7772982d..2e100a2f7 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -11,6 +11,7 @@ - Fix [#1968](https://github.com/StackExchange/StackExchange.Redis/issues/1968): Improved handling of EVAL scripts during server restarts and failovers, detecting and re-sending the script for a retry when needed ([#2170 by martintmk](https://github.com/StackExchange/StackExchange.Redis/pull/2170)) - Adds: `ConfigurationOptions.SslClientAuthenticationOptions` (`netcoreapp3.1`/`net5.0`+ only) to give more control over SSL/TLS authentication ([#2224 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2224)) - Fix [#2240](https://github.com/StackExchange/StackExchange.Redis/pull/2241): Improve support for DNS-based IPv6 endpoints ([#2241 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2241)) +- Fix [#1879](https://github.com/StackExchange/StackExchange.Redis/issues/1879): Improve exception message when the wrong password is used ([#2246 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2246)) - Fix [#2233](https://github.com/StackExchange/StackExchange.Redis/issues/2233): Repeated connection to Sentinel servers using the same ConfigurationOptions would fail ([#2242 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2242)) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 405e87591..2be70f4bb 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -558,7 +558,7 @@ private static bool AllComplete(Task[] tasks) } internal Exception? AuthException { get; private set; } - internal void SetAuthSuspect(Exception authException) => AuthException = authException; + internal void SetAuthSuspect(Exception authException) => AuthException ??= authException; /// /// Creates a new instance. diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index cd9395368..73363968c 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -24,6 +24,7 @@ public static readonly CommandBytes slave_read_only = "slave-read-only", timeout = "timeout", wildcard = "*", + WRONGPASS = "WRONGPASS", yes = "yes", zero = "0", diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index fa6639f82..3044857ae 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -227,7 +227,14 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in } if (result.IsError) { - if (result.StartsWith(CommonReplies.NOAUTH)) bridge?.Multiplexer?.SetAuthSuspect(new RedisServerException("NOAUTH Returned - connection has not authenticated")); + if (result.StartsWith(CommonReplies.NOAUTH)) + { + bridge?.Multiplexer?.SetAuthSuspect(new RedisServerException("NOAUTH Returned - connection has not yet authenticated")); + } + else if (result.StartsWith(CommonReplies.WRONGPASS)) + { + bridge?.Multiplexer?.SetAuthSuspect(new RedisServerException(result.ToString())); + } var server = bridge?.ServerEndPoint; bool log = !message.IsInternalCall; diff --git a/tests/StackExchange.Redis.Tests/Secure.cs b/tests/StackExchange.Redis.Tests/Secure.cs index bdebef2e0..615bf143e 100644 --- a/tests/StackExchange.Redis.Tests/Secure.cs +++ b/tests/StackExchange.Redis.Tests/Secure.cs @@ -50,16 +50,19 @@ public void CheckConfig() [Fact] public void Connect() { - using var server = Create(); + using var conn = Create(); - server.GetDatabase().Ping(); + conn.GetDatabase().Ping(); } [Theory] - [InlineData("wrong")] - [InlineData("")] - public async Task ConnectWithWrongPassword(string password) + [InlineData("wrong", "WRONGPASS invalid username-password pair or user is disabled.")] + [InlineData("", "NOAUTH Returned - connection has not yet authenticated")] + public async Task ConnectWithWrongPassword(string password, string exepctedMessage) { + using var checkConn = Create(); + var checkServer = GetServer(checkConn); + var config = ConfigurationOptions.Parse(GetConfiguration()); config.Password = password; config.ConnectRetry = 0; // we don't want to retry on closed sockets in this case. @@ -74,6 +77,16 @@ public async Task ConnectWithWrongPassword(string password) conn.GetDatabase().Ping(); }).ConfigureAwait(false); Log("Exception: " + ex.Message); - Assert.StartsWith("It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly: (RedisServerException) NOAUTH Returned - connection has not authenticated", ex.Message); + Assert.StartsWith("It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly: (RedisServerException) ", ex.Message); + + // This changed in some version...not sure which. For our purposes, splitting on v3 vs v6+ + if (checkServer.Version >= RedisFeatures.v6_0_0) + { + Assert.EndsWith(exepctedMessage, ex.Message); + } + else + { + Assert.EndsWith("NOAUTH Returned - connection has not yet authenticated", ex.Message); + } } } From d1b802bc534bee3be0901545589ad90a4ef3f58e Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 6 Sep 2022 11:22:58 -0400 Subject: [PATCH 182/435] Heartbeat interval: Make it configurable (#2243) When I was looking at a separate timer for async timeout evaluation < 1000ms fidelity it became apparent most of the cost in a heartbeat is the timeout evaluation anyway, but also: if you are chasing a lower timeout fidelity you likely want to observe a server outage ASAP as well so I think it makes more sense to actually open up the heartbeat to make it configurable. I added some docs about how this is risky but that portion and comments could use scrutiny (if we even agree with doing this) on being strong enough warnings around this. I would prefer to leave this as code-only due to potential downsides people may not appreciate otherwise. --- docs/Configuration.md | 5 +- docs/ReleaseNotes.md | 1 + .../Configuration/DefaultOptionsProvider.cs | 9 +++ .../ConfigurationOptions.cs | 20 +++++++ .../ConnectionMultiplexer.cs | 7 +-- src/StackExchange.Redis/PhysicalBridge.cs | 2 +- .../PublicAPI/PublicAPI.Shipped.txt | 3 + .../CommandTimeouts.cs | 57 +++++++++++++++++++ 8 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/CommandTimeouts.cs diff --git a/docs/Configuration.md b/docs/Configuration.md index 5d4590383..cbe20001b 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -106,11 +106,14 @@ Additional code-only options: - BeforeSocketConnect - Default: `null` - Allows modifying a `Socket` before connecting (for advanced scenarios) - SslClientAuthenticationOptions (`netcooreapp3.1`/`net5.0` and higher) - Default: `null` - - Allows specifying exact options for SSL/TLS authentication against a server (e.g. cipher suites, protocols, etc.) - overrides all other SSL configuration options. This is a `Func` which receiveces the host (or `SslHost` if set) to get the options for. If `null` is returned from the `Func`, it's the same as this property not being set at all when connecting. + - Allows specifying exact options for SSL/TLS authentication against a server (e.g. cipher suites, protocols, etc.) - overrides all other SSL configuration options. This is a `Func` which receives the host (or `SslHost` if set) to get the options for. If `null` is returned from the `Func`, it's the same as this property not being set at all when connecting. - SocketManager - Default: `SocketManager.Shared`: - The thread pool to use for scheduling work to and from the socket connected to Redis, one of... - `SocketManager.Shared`: Use a shared dedicated thread pool for _all_ multiplexers (defaults to 10 threads) - best balance for most scenarios. - `SocketManager.ThreadPool`: Use the build-in .NET thread pool for scheduling. This can perform better for very small numbers of cores or with large apps on large machines that need to use more than 10 threads (total, across all multiplexers) under load. **Important**: this option isn't the default because it's subject to thread pool growth/starvation and if for example synchronous calls are waiting on a redis command to come back to unblock other threads, stalls/hangs can result. Use with caution, especially if you have sync-over-async work in play. +- HeartbeatInterval - Default: `1000ms` + - Allows running the heartbeat more often which importantly includes timeout evaluation. For example if you have a 50ms command timeout, we're only actually checking it during the heartbeat (once per second by default), so it's possible 50-1050ms pass _before we notice it timed out_. If you want more fidelity in that check and to observe that a server failed faster, you can lower this to run the heartbeat more often to achieve that. + - **Note: heartbeats are not free and that's why the default is 1 second. There is additional overhead to running this more often simply because it does some work each time it fires.** Tokens in the configuration string are comma-separated; any without an `=` sign are assumed to be redis server endpoints. Endpoints without an explicit port will use 6379 if ssl is not enabled, and 6380 if ssl is enabled. Tokens starting with `$` are taken to represent command maps, for example: `$config=cfg`. diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 2e100a2f7..43e589327 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -11,6 +11,7 @@ - Fix [#1968](https://github.com/StackExchange/StackExchange.Redis/issues/1968): Improved handling of EVAL scripts during server restarts and failovers, detecting and re-sending the script for a retry when needed ([#2170 by martintmk](https://github.com/StackExchange/StackExchange.Redis/pull/2170)) - Adds: `ConfigurationOptions.SslClientAuthenticationOptions` (`netcoreapp3.1`/`net5.0`+ only) to give more control over SSL/TLS authentication ([#2224 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2224)) - Fix [#2240](https://github.com/StackExchange/StackExchange.Redis/pull/2241): Improve support for DNS-based IPv6 endpoints ([#2241 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2241)) +- Adds: `ConfigurationOptions.HeartbeatInterval` (**Advanced Setting** - [see docs](https://stackexchange.github.io/StackExchange.Redis/Configuration#configuration-options)) To allow more finite control of the client heartbeat, which encompases how often command timeouts are actually evaluated - still defaults to 1,000 ms ([#2243 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2243)) - Fix [#1879](https://github.com/StackExchange/StackExchange.Redis/issues/1879): Improve exception message when the wrong password is used ([#2246 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2246)) - Fix [#2233](https://github.com/StackExchange/StackExchange.Redis/issues/2233): Repeated connection to Sentinel servers using the same ConfigurationOptions would fail ([#2242 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2242)) diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index 492dd7270..72b1d3997 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -114,6 +114,15 @@ public static void AddProvider(DefaultOptionsProvider provider) /// public virtual Version DefaultVersion => RedisFeatures.v3_0_0; + /// + /// Controls how often the connection heartbeats. A heartbeat includes: + /// - Evaluating if any messages have timed out + /// - Evaluating connection status (checking for failures) + /// - Sending a server message to keep the connection alive if needed + /// + /// Be aware setting this very low incurs additional overhead of evaluating the above more often. + public virtual TimeSpan HeartbeatInterval => TimeSpan.FromSeconds(1); + /// /// Should exceptions include identifiable details? (key names, additional .Data annotations) /// diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 12e561c21..061ac8f3e 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -145,6 +145,8 @@ public static string TryNormalize(string value) private string? tieBreaker, sslHost, configChannel; + private TimeSpan? heartbeatInterval; + private CommandMap? commandMap; private Version? defaultVersion; @@ -366,6 +368,24 @@ public Version DefaultVersion /// public EndPointCollection EndPoints { get; init; } = new EndPointCollection(); + /// + /// Controls how often the connection heartbeats. A heartbeat includes: + /// - Evaluating if any messages have timed out + /// - Evaluating connection status (checking for failures) + /// - Sending a server message to keep the connection alive if needed + /// + /// + /// This defaults to 1000 milliseconds and should not be changed for most use cases. + /// If for example you want to evaluate whether commands have violated the at a lower fidelity + /// than 1000 milliseconds, you could lower this value. + /// Be aware setting this very low incurs additional overhead of evaluating the above more often. + /// + public TimeSpan HeartbeatInterval + { + get => heartbeatInterval ?? Defaults.HeartbeatInterval; + set => heartbeatInterval = value; + } + /// /// Use ThreadPriority.AboveNormal for SocketManager reader and writer threads (true by default). /// If , will be used. diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 2be70f4bb..4d64fa7de 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -22,8 +22,6 @@ namespace StackExchange.Redis /// public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplexer // implies : IConnectionMultiplexer and : IDisposable { - internal const int MillisecondsPerHeartbeat = 1000; - // This gets accessed for every received event; let's make sure we can process it "raw" internal readonly byte[]? ConfigurationChangedChannel; // Unique identifier used when tracing @@ -812,7 +810,7 @@ internal EndPoint[] GetEndPoints() private sealed class TimerToken { - public TimerToken(ConnectionMultiplexer muxer) + private TimerToken(ConnectionMultiplexer muxer) { _ref = new WeakReference(muxer); } @@ -840,7 +838,8 @@ public TimerToken(ConnectionMultiplexer muxer) internal static IDisposable Create(ConnectionMultiplexer connection) { var token = new TimerToken(connection); - var timer = new Timer(Heartbeat, token, MillisecondsPerHeartbeat, MillisecondsPerHeartbeat); + var heartbeatMilliseconds = (int)connection.RawConfig.HeartbeatInterval.TotalMilliseconds; + var timer = new Timer(Heartbeat, token, heartbeatMilliseconds, heartbeatMilliseconds); token.SetTimer(timer); return timer; } diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 64080d07f..cd225a529 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -20,7 +20,7 @@ internal sealed class PhysicalBridge : IDisposable private const int ProfileLogSamples = 10; - private const double ProfileLogSeconds = (ConnectionMultiplexer.MillisecondsPerHeartbeat * ProfileLogSamples) / 1000.0; + private const double ProfileLogSeconds = (1000 /* ms */ * ProfileLogSamples) / 1000.0; private static readonly Message ReusableAskingCommand = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.ASKING); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 8ffbccb7b..4f9d4658e 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -220,6 +220,8 @@ StackExchange.Redis.ConfigurationOptions.DefaultVersion.get -> System.Version! StackExchange.Redis.ConfigurationOptions.DefaultVersion.set -> void StackExchange.Redis.ConfigurationOptions.EndPoints.get -> StackExchange.Redis.EndPointCollection! StackExchange.Redis.ConfigurationOptions.EndPoints.init -> void +StackExchange.Redis.ConfigurationOptions.HeartbeatInterval.get -> System.TimeSpan +StackExchange.Redis.ConfigurationOptions.HeartbeatInterval.set -> void StackExchange.Redis.ConfigurationOptions.HighPrioritySocketThreads.get -> bool StackExchange.Redis.ConfigurationOptions.HighPrioritySocketThreads.set -> void StackExchange.Redis.ConfigurationOptions.IncludeDetailInExceptions.get -> bool @@ -1759,6 +1761,7 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.DefaultVersion. virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetDefaultClientName() -> string! virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetDefaultSsl(StackExchange.Redis.EndPointCollection! endPoints) -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetSslHostFromEndpoints(StackExchange.Redis.EndPointCollection! endPoints) -> string? +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.HeartbeatInterval.get -> System.TimeSpan virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludeDetailInExceptions.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludePerformanceCountersInExceptions.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IsMatch(System.Net.EndPoint! endpoint) -> bool diff --git a/tests/StackExchange.Redis.Tests/CommandTimeouts.cs b/tests/StackExchange.Redis.Tests/CommandTimeouts.cs new file mode 100644 index 000000000..c7530018b --- /dev/null +++ b/tests/StackExchange.Redis.Tests/CommandTimeouts.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests; + +[Collection(NonParallelCollection.Name)] +public class CommandTimeouts : TestBase +{ + public CommandTimeouts(ITestOutputHelper output) : base (output) { } + + [Fact] + public async Task DefaultHeartbeatTimeout() + { + var options = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort); + options.AllowAdmin = true; + options.AsyncTimeout = 1000; + + using var pauseConn = ConnectionMultiplexer.Connect(options); + using var conn = ConnectionMultiplexer.Connect(options); + + var pauseServer = GetServer(pauseConn); + _ = pauseServer.ExecuteAsync("CLIENT", "PAUSE", 2000); + + var key = Me(); + var db = conn.GetDatabase(); + var sw = ValueStopwatch.StartNew(); + var ex = await Assert.ThrowsAsync(async () => await db.StringGetAsync(key)); + Log(ex.Message); + var duration = sw.GetElapsedTime(); + Assert.True(duration < TimeSpan.FromSeconds(2100), $"Duration ({duration.Milliseconds} ms) should be less than 2100ms"); + } + + [Fact] + public async Task DefaultHeartbeatLowTimeout() + { + var options = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort); + options.AllowAdmin = true; + options.AsyncTimeout = 50; + options.HeartbeatInterval = TimeSpan.FromMilliseconds(100); + + using var pauseConn = ConnectionMultiplexer.Connect(options); + using var conn = ConnectionMultiplexer.Connect(options); + + var pauseServer = GetServer(pauseConn); + _ = pauseServer.ExecuteAsync("CLIENT", "PAUSE", 2000); + + var key = Me(); + var db = conn.GetDatabase(); + var sw = ValueStopwatch.StartNew(); + var ex = await Assert.ThrowsAsync(async () => await db.StringGetAsync(key)); + Log(ex.Message); + var duration = sw.GetElapsedTime(); + Assert.True(duration < TimeSpan.FromSeconds(250), $"Duration ({duration.Milliseconds} ms) should be less than 250ms"); + } +} From ae87f1239939aefeaebb0c13f2c6a8fac6d7198f Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 6 Sep 2022 12:45:44 -0400 Subject: [PATCH 183/435] Update release notes for 2.6.66 --- docs/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 43e589327..2509106ff 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,6 +1,9 @@ # Release Notes ## Unreleased +- No unreleased changes at this time + +## 2.6.66 - Fix [#2182](https://github.com/StackExchange/StackExchange.Redis/issues/2182): Be more flexible in which commands are "primary only" in order to support users with replicas that are explicitly configured to allow writes ([#2183 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2183)) - Adds: `IConnectionMultiplexer` now implements `IAsyncDisposable` ([#2161 by kimsey0](https://github.com/StackExchange/StackExchange.Redis/pull/2161)) From f81553b03dffe58b3f859d31f86feab15a89e153 Mon Sep 17 00:00:00 2001 From: Sviataslau Hankovich Date: Thu, 8 Sep 2022 13:21:21 +0300 Subject: [PATCH 184/435] Fix typo (#2247) --- docs/Configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index cbe20001b..b7de92e66 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -105,7 +105,7 @@ Additional code-only options: - Determines how commands will be queued (or not) during a disconnect, for sending when it's available again - BeforeSocketConnect - Default: `null` - Allows modifying a `Socket` before connecting (for advanced scenarios) -- SslClientAuthenticationOptions (`netcooreapp3.1`/`net5.0` and higher) - Default: `null` +- SslClientAuthenticationOptions (`netcoreapp3.1`/`net5.0` and higher) - Default: `null` - Allows specifying exact options for SSL/TLS authentication against a server (e.g. cipher suites, protocols, etc.) - overrides all other SSL configuration options. This is a `Func` which receives the host (or `SslHost` if set) to get the options for. If `null` is returned from the `Func`, it's the same as this property not being set at all when connecting. - SocketManager - Default: `SocketManager.Shared`: - The thread pool to use for scheduling work to and from the socket connected to Redis, one of... From bff081140689096fe9a569b7000b4f283fea172b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 12 Oct 2022 13:04:50 +0100 Subject: [PATCH 185/435] Respect IncludeDetailInExceptions in MOVED etc scenarios from ResultProcessor (#2267) ResultProcessor should be more reticent if IncludeDetailInExceptions is disabled --- docs/ReleaseNotes.md | 4 +++- src/StackExchange.Redis/ResultProcessor.cs | 20 +++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 2509106ff..9a1cf32f6 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,7 +1,9 @@ # Release Notes ## Unreleased -- No unreleased changes at this time + +- Fix: `MOVED` with `NoRedirect` (and other non-reachable errors) should respect the `IncludeDetailInExceptions` setting + ## 2.6.66 diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 3044857ae..d643f4b62 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -267,13 +267,27 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in { if (isMoved && wasNoRedirect) { - err = $"Key has MOVED to Endpoint {endpoint} and hashslot {hashSlot} but CommandFlags.NoRedirect was specified - redirect not followed for {message.CommandAndKey}. "; + if (bridge.Multiplexer.IncludeDetailInExceptions) + { + err = $"Key has MOVED to Endpoint {endpoint} and hashslot {hashSlot} but CommandFlags.NoRedirect was specified - redirect not followed for {message.CommandAndKey}. "; + } + else + { + err = "Key has MOVED but CommandFlags.NoRedirect was specified - redirect not followed. "; + } } else { unableToConnectError = true; - err = $"Endpoint {endpoint} serving hashslot {hashSlot} is not reachable at this point of time. Please check connectTimeout value. If it is low, try increasing it to give the ConnectionMultiplexer a chance to recover from the network disconnect. " - + PerfCounterHelper.GetThreadPoolAndCPUSummary(bridge.Multiplexer.RawConfig.IncludePerformanceCountersInExceptions); + if (bridge.Multiplexer.IncludeDetailInExceptions) + { + err = $"Endpoint {endpoint} serving hashslot {hashSlot} is not reachable at this point of time. Please check connectTimeout value. If it is low, try increasing it to give the ConnectionMultiplexer a chance to recover from the network disconnect. " + + PerfCounterHelper.GetThreadPoolAndCPUSummary(bridge.Multiplexer.RawConfig.IncludePerformanceCountersInExceptions); + } + else + { + err = "Endpoint is not reachable at this point of time. Please check connectTimeout value. If it is low, try increasing it to give the ConnectionMultiplexer a chance to recover from the network disconnect. "; + } } } } From 85c1b2bf6d51d4d6d86ce30e39b5a6ffd0a4733f Mon Sep 17 00:00:00 2001 From: Ilya Teplov <47264728+iteplov@users.noreply.github.com> Date: Sat, 15 Oct 2022 16:54:04 -0700 Subject: [PATCH 186/435] 2251: Fixes missing activation of the sub leg during initial endpoint discovery (#2268) Fix for #2251 and #2265 ensuring subscription connections are proactively created in all cases. Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 3 ++- src/StackExchange.Redis/ConnectionMultiplexer.cs | 15 ++++++++++++--- src/StackExchange.Redis/ServerEndPoint.cs | 3 ++- tests/StackExchange.Redis.Tests/Cluster.cs | 16 ++++++++++++++++ .../StackExchange.Redis.Tests/CommandTimeouts.cs | 10 ++++++++-- .../ConnectingFailDetection.cs | 16 ++++++++++++++++ tests/StackExchange.Redis.Tests/Failover.cs | 5 +++-- tests/StackExchange.Redis.Tests/TestBase.cs | 2 +- 8 files changed, 60 insertions(+), 10 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 9a1cf32f6..5fad07b24 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -2,7 +2,8 @@ ## Unreleased -- Fix: `MOVED` with `NoRedirect` (and other non-reachable errors) should respect the `IncludeDetailInExceptions` setting +- Fix: `MOVED` with `NoRedirect` (and other non-reachable errors) should respect the `IncludeDetailInExceptions` setting ([#2267 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2267)) +- Fix [#2251](https://github.com/StackExchange/StackExchange.Redis/issues/2251) & [#2265](https://github.com/StackExchange/StackExchange.Redis/issues/2265): Cluster endpoint connections weren't proactively connecting subscriptions in all cases and taking the full connection timeout to complete as a result ([#2268 by iteplov](https://github.com/StackExchange/StackExchange.Redis/pull/2268)) ## 2.6.66 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 4d64fa7de..5630b28fe 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -803,7 +803,15 @@ internal EndPoint[] GetEndPoints() } } // spin up the connection if this is new - if (isNew && activate) server.Activate(ConnectionType.Interactive, log); + if (isNew && activate) + { + server.Activate(ConnectionType.Interactive, log); + if (server.SupportsSubscriptions) + { + // Intentionally not logging the sub connection + server.Activate(ConnectionType.Subscription, null); + } + } } return server; } @@ -1300,9 +1308,10 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP // Log current state after await foreach (var server in servers) { - log?.WriteLine($" {Format.ToString(server.EndPoint)}: Endpoint is {server.ConnectionState}"); + log?.WriteLine($" {Format.ToString(server.EndPoint)}: Endpoint is (Interactive: {server.InteractiveConnectionState}, Subscription: {server.SubscriptionConnectionState})"); } + log?.WriteLine("Task summary:"); EndPointCollection? updatedClusterEndpointCollection = null; for (int i = 0; i < available.Length; i++) { @@ -1388,7 +1397,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP else { server.SetUnselectable(UnselectableFlags.DidNotRespond); - log?.WriteLine($" {Format.ToString(server)}: Did not respond"); + log?.WriteLine($" {Format.ToString(server)}: Did not respond (Task.Status: {task.Status})"); } } diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 876e21dc4..89ce6a974 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -157,7 +157,8 @@ internal Exception? LastException } } - internal State ConnectionState => interactive?.ConnectionState ?? State.Disconnected; + internal State InteractiveConnectionState => interactive?.ConnectionState ?? State.Disconnected; + internal State SubscriptionConnectionState => subscription?.ConnectionState ?? State.Disconnected; public long OperationCount => interactive?.OperationCount ?? 0 + subscription?.OperationCount ?? 0; diff --git a/tests/StackExchange.Redis.Tests/Cluster.cs b/tests/StackExchange.Redis.Tests/Cluster.cs index f81375c97..d1fe5ef5f 100644 --- a/tests/StackExchange.Redis.Tests/Cluster.cs +++ b/tests/StackExchange.Redis.Tests/Cluster.cs @@ -728,4 +728,20 @@ public void MovedProfiling() } } } + + [Fact] + public void ConnectIncludesSubscriber() + { + using var conn = Create(keepAlive: 1, connectTimeout: 3000, shared: false); + + var db = conn.GetDatabase(); + db.Ping(); + Assert.True(conn.IsConnected); + + foreach (var server in conn.GetServerSnapshot()) + { + Assert.Equal(PhysicalBridge.State.ConnectedEstablished, server.InteractiveConnectionState); + Assert.Equal(PhysicalBridge.State.ConnectedEstablished, server.SubscriptionConnectionState); + } + } } diff --git a/tests/StackExchange.Redis.Tests/CommandTimeouts.cs b/tests/StackExchange.Redis.Tests/CommandTimeouts.cs index c7530018b..bcb4c8754 100644 --- a/tests/StackExchange.Redis.Tests/CommandTimeouts.cs +++ b/tests/StackExchange.Redis.Tests/CommandTimeouts.cs @@ -21,7 +21,7 @@ public async Task DefaultHeartbeatTimeout() using var conn = ConnectionMultiplexer.Connect(options); var pauseServer = GetServer(pauseConn); - _ = pauseServer.ExecuteAsync("CLIENT", "PAUSE", 2000); + var pauseTask = pauseServer.ExecuteAsync("CLIENT", "PAUSE", 2500); var key = Me(); var db = conn.GetDatabase(); @@ -30,6 +30,9 @@ public async Task DefaultHeartbeatTimeout() Log(ex.Message); var duration = sw.GetElapsedTime(); Assert.True(duration < TimeSpan.FromSeconds(2100), $"Duration ({duration.Milliseconds} ms) should be less than 2100ms"); + + // Await as to not bias the next test + await pauseTask; } [Fact] @@ -44,7 +47,7 @@ public async Task DefaultHeartbeatLowTimeout() using var conn = ConnectionMultiplexer.Connect(options); var pauseServer = GetServer(pauseConn); - _ = pauseServer.ExecuteAsync("CLIENT", "PAUSE", 2000); + var pauseTask = pauseServer.ExecuteAsync("CLIENT", "PAUSE", 500); var key = Me(); var db = conn.GetDatabase(); @@ -53,5 +56,8 @@ public async Task DefaultHeartbeatLowTimeout() Log(ex.Message); var duration = sw.GetElapsedTime(); Assert.True(duration < TimeSpan.FromSeconds(250), $"Duration ({duration.Milliseconds} ms) should be less than 250ms"); + + // Await as to not bias the next test + await pauseTask; } } diff --git a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs index d32066bf3..3fe53187c 100644 --- a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs +++ b/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs @@ -151,4 +151,20 @@ public void ConnectsWhenBeginConnectCompletesSynchronously() ClearAmbientFailures(); } } + + [Fact] + public void ConnectIncludesSubscriber() + { + using var conn = Create(keepAlive: 1, connectTimeout: 3000, shared: false); + + var db = conn.GetDatabase(); + db.Ping(); + Assert.True(conn.IsConnected); + + foreach (var server in conn.GetServerSnapshot()) + { + Assert.Equal(PhysicalBridge.State.ConnectedEstablished, server.InteractiveConnectionState); + Assert.Equal(PhysicalBridge.State.ConnectedEstablished, server.SubscriptionConnectionState); + } + } } diff --git a/tests/StackExchange.Redis.Tests/Failover.cs b/tests/StackExchange.Redis.Tests/Failover.cs index b9b23cd21..16faff432 100644 --- a/tests/StackExchange.Redis.Tests/Failover.cs +++ b/tests/StackExchange.Redis.Tests/Failover.cs @@ -295,8 +295,9 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() Log("FAILURE: B has not detected the topology change."); foreach (var server in bConn.GetServerSnapshot().ToArray()) { - Log(" Server" + server.EndPoint); - Log(" State: " + server.ConnectionState); + Log(" Server: " + server.EndPoint); + Log(" State (Interactive): " + server.InteractiveConnectionState); + Log(" State (Subscription): " + server.SubscriptionConnectionState); Log(" IsReplica: " + !server.IsReplica); Log(" Type: " + server.ServerType); } diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index c547e5e5e..39ec34229 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -268,7 +268,7 @@ internal virtual IInternalConnectionMultiplexer Create( { if (Output == null) { - Assert.True(false, "Failure: Be sure to call the TestBase constuctor like this: BasicOpsTests(ITestOutputHelper output) : base(output) { }"); + Assert.True(false, "Failure: Be sure to call the TestBase constructor like this: BasicOpsTests(ITestOutputHelper output) : base(output) { }"); } // Share a connection if instructed to and we can - many specifics mean no sharing From 624aeb9d22fc96e7fef72f4a8c71278ff2eeea3a Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 15 Oct 2022 20:51:15 -0400 Subject: [PATCH 187/435] Add release notes for 2.6.70 --- docs/ReleaseNotes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 5fad07b24..1e2ac210a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -2,6 +2,10 @@ ## Unreleased +No pending changes for the next release yet. + +## 2.6.70 + - Fix: `MOVED` with `NoRedirect` (and other non-reachable errors) should respect the `IncludeDetailInExceptions` setting ([#2267 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2267)) - Fix [#2251](https://github.com/StackExchange/StackExchange.Redis/issues/2251) & [#2265](https://github.com/StackExchange/StackExchange.Redis/issues/2265): Cluster endpoint connections weren't proactively connecting subscriptions in all cases and taking the full connection timeout to complete as a result ([#2268 by iteplov](https://github.com/StackExchange/StackExchange.Redis/pull/2268)) From 430de79349bcadea204fcd13d24e858ac21db5c0 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 15 Oct 2022 20:57:57 -0400 Subject: [PATCH 188/435] Update release note docs --- README.md | 2 ++ docs/ReleaseNotes.md | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index da72b84ae..3823e855a 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,5 @@ MyGet Pre-release feed: https://www.myget.org/gallery/stackoverflow | Package | NuGet Stable | NuGet Pre-release | Downloads | MyGet | | ------- | ------------ | ----------------- | --------- | ----- | | [StackExchange.Redis](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/dt/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | + +Release notes at: https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes \ No newline at end of file diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 1e2ac210a..34fc6b936 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,5 +1,11 @@ # Release Notes +Current package versions: + +| NuGet Stable | NuGet Pre-release | MyGet | +| ------------ | ----------------- | ----- | +| [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | + ## Unreleased No pending changes for the next release yet. From d90b21cd7392142f63dd38c6bde1882a4dabf065 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 18 Oct 2022 11:12:34 -0400 Subject: [PATCH 189/435] Timeouts: better SLOWLOG link --- docs/Timeouts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Timeouts.md b/docs/Timeouts.md index cdbe6061e..345f25dc5 100644 --- a/docs/Timeouts.md +++ b/docs/Timeouts.md @@ -10,7 +10,7 @@ it is possible that the reader loop has been hijacked; see [Thread Theft](Thread Are there commands taking a long time to process on the redis-server? --------------- -There can be commands that are taking a long time to process on the redis-server causing the request to timeout. Few examples of long running commands are mget with large number of keys, keys * or poorly written lua script. You can run the SlowLog command to see if there are requests taking longer than expected. More details regarding the command can be found [here](https://redis.io/commands/slowlog). +There can be commands that are taking a long time to process on the redis-server causing the request to timeout. Few examples of long running commands are mget with large number of keys, keys * or poorly written lua script. You can run [the `SLOWLOG` command](https://redis.io/commands/slowlog) to see if there are requests taking longer than expected. More details regarding the command can be found [here](https://redis.io/commands/slowlog). Was there a big request preceding several small requests to the Redis that timed out? --------------- From 35beeeb61efaa7d02d61b797632afbc83786bec2 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 21 Oct 2022 10:17:59 -0400 Subject: [PATCH 190/435] Timeouts: add "last-in" for the last payload's length to exception messages (#2276) In a multiplexed setup we often see a timeout behind a large payload in the process of parsing (or not). This is meant to help better diagnose that case, by recording what the last payload size was that came through this connection. Needs some @mgravell eyes on the record point here and if we want to adjust (or if it's wrong due to buffers in a way I'm not understanding). I realize this won't work on a payload so large enough it blows all buffers completely, but I don't see how to solve that either the way we properly stream parse. I think it'll still improve most cases we see this in. --- docs/ReleaseNotes.md | 2 +- src/StackExchange.Redis/ExceptionFactory.cs | 2 ++ src/StackExchange.Redis/PhysicalConnection.cs | 20 +++++++++++++++++++ tests/StackExchange.Redis.Tests/AsyncTests.cs | 12 ++++++++--- .../ExceptionFactoryTests.cs | 10 +++++----- 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 34fc6b936..714dbde9a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -No pending changes for the next release yet. +- Adds: `last-in` and `cur-in` (bytes) to timeout exceptions to help identify timeouts that were just-behind another large payload off the wire ([#2276 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2276)) ## 2.6.70 diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 0c3f9b2d9..0743ad302 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -320,6 +320,8 @@ private static void AddCommonDetail( if (bs.Connection.BytesAvailableOnSocket >= 0) Add(data, sb, "Inbound-Bytes", "in", bs.Connection.BytesAvailableOnSocket.ToString()); if (bs.Connection.BytesInReadPipe >= 0) Add(data, sb, "Inbound-Pipe-Bytes", "in-pipe", bs.Connection.BytesInReadPipe.ToString()); if (bs.Connection.BytesInWritePipe >= 0) Add(data, sb, "Outbound-Pipe-Bytes", "out-pipe", bs.Connection.BytesInWritePipe.ToString()); + Add(data, sb, "Last-Result-Bytes", "last-in", bs.Connection.BytesLastResult.ToString()); + Add(data, sb, "Inbound-Buffer-Bytes", "cur-in", bs.Connection.BytesInBuffer.ToString()); if (multiplexer.StormLogThreshold >= 0 && bs.Connection.MessagesSentAwaitingResponse >= multiplexer.StormLogThreshold && Interlocked.CompareExchange(ref multiplexer.haveStormLog, 1, 0) == 0) { diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index eac909359..b4baa0e5e 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -52,6 +52,9 @@ private static readonly Message private int lastWriteTickCount, lastReadTickCount, lastBeatTickCount; + private long bytesLastResult; + private long bytesInBuffer; + internal void GetBytes(out long sent, out long received) { if (_ioPipe is IMeasuredDuplexPipe sc) @@ -1283,6 +1286,14 @@ internal readonly struct ConnectionStatus /// Bytes in the writer pipe, waiting to be written to the socket. /// public long BytesInWritePipe { get; init; } + /// + /// Byte size of the last result we processed. + /// + public long BytesLastResult { get; init; } + /// + /// Byte size on the buffer that isn't processed yet. + /// + public long BytesInBuffer { get; init; } /// /// The inbound pipe reader status. @@ -1334,6 +1345,8 @@ public ConnectionStatus GetStatus() BytesInWritePipe = counters.BytesWaitingToBeSent, ReadStatus = _readStatus, WriteStatus = _writeStatus, + BytesLastResult = bytesLastResult, + BytesInBuffer = bytesInBuffer, }; } @@ -1356,6 +1369,8 @@ public ConnectionStatus GetStatus() BytesInWritePipe = -1, ReadStatus = _readStatus, WriteStatus = _writeStatus, + BytesLastResult = bytesLastResult, + BytesInBuffer = bytesInBuffer, }; } @@ -1702,6 +1717,7 @@ private async Task ReadFromPipe() private int ProcessBuffer(ref ReadOnlySequence buffer) { int messageCount = 0; + bytesInBuffer = buffer.Length; while (!buffer.IsEmpty) { @@ -1718,6 +1734,10 @@ private int ProcessBuffer(ref ReadOnlySequence buffer) Trace(result.ToString()); _readStatus = ReadStatus.MatchResult; MatchResult(result); + + // Track the last result size *after* processing for the *next* error message + bytesInBuffer = buffer.Length; + bytesLastResult = result.Payload.Length; } else { diff --git a/tests/StackExchange.Redis.Tests/AsyncTests.cs b/tests/StackExchange.Redis.Tests/AsyncTests.cs index e754cda7c..bebd81438 100644 --- a/tests/StackExchange.Redis.Tests/AsyncTests.cs +++ b/tests/StackExchange.Redis.Tests/AsyncTests.cs @@ -48,7 +48,7 @@ public async Task AsyncTimeoutIsNoticed() using var conn = Create(syncTimeout: 1000); var opt = ConfigurationOptions.Parse(conn.Configuration); if (!Debugger.IsAttached) - { // we max the timeouts if a degugger is detected + { // we max the timeouts if a debugger is detected Assert.Equal(1000, opt.AsyncTimeout); } @@ -65,14 +65,20 @@ public async Task AsyncTimeoutIsNoticed() var ex = await Assert.ThrowsAsync(async () => { await db.StringGetAsync(key).ForAwait(); // but *subsequent* operations are paused - ms.Stop(); + ms.Stop(); Writer.WriteLine($"Unexpectedly succeeded after {ms.ElapsedMilliseconds}ms"); }).ForAwait(); ms.Stop(); Writer.WriteLine($"Timed out after {ms.ElapsedMilliseconds}ms"); + Writer.WriteLine("Exception message: " + ex.Message); Assert.Contains("Timeout awaiting response", ex.Message); - Writer.WriteLine(ex.Message); + // Ensure we are including the last payload size + Assert.Contains("last-in:", ex.Message); + Assert.DoesNotContain("last-in: 0", ex.Message); + Assert.NotNull(ex.Data["Redis-Last-Result-Bytes"]); + + Assert.Contains("cur-in:", ex.Message); string status = conn.GetStatus(); Writer.WriteLine(status); diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index cb34aa19c..89dc06e5d 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -112,11 +112,11 @@ public void TimeoutException() var ex = Assert.IsType(rawEx); Writer.WriteLine("Exception: " + ex.Message); - // Example format: "Test Timeout, command=PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, serverEndpoint: 127.0.0.1:6379, mgr: 10 of 10 available, clientName: TimeoutException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), v: 2.1.0 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)"; + // Example format: "Test Timeout, command=PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, last-in: 0, cur-in: 0, serverEndpoint: 127.0.0.1:6379, mgr: 10 of 10 available, clientName: TimeoutException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), v: 2.1.0 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)"; Assert.StartsWith("Test Timeout, command=PING", ex.Message); Assert.Contains("clientName: " + nameof(TimeoutException), ex.Message); // Ensure our pipe numbers are in place - Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); + Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0, last-in: 0, cur-in: 0", ex.Message); Assert.Contains("mc: 1/1/0", ex.Message); Assert.Contains("serverEndpoint: " + server.EndPoint, ex.Message); Assert.Contains("IOCP: ", ex.Message); @@ -183,19 +183,19 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple var ex = Assert.IsType(rawEx); Writer.WriteLine("Exception: " + ex.Message); - // Example format: "Exception: No connection is active/available to service this operation: PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, serverEndpoint: 127.0.0.1:6379, mc: 1/1/0, mgr: 10 of 10 available, clientName: NoConnectionException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), Local-CPU: 100%, v: 2.1.0.5"; + // Example format: "Exception: No connection is active/available to service this operation: PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, last-in: 0, cur-in: 0, serverEndpoint: 127.0.0.1:6379, mc: 1/1/0, mgr: 10 of 10 available, clientName: NoConnectionException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), Local-CPU: 100%, v: 2.1.0.5"; Assert.StartsWith(messageStart, ex.Message); // Ensure our pipe numbers are in place if they should be if (hasDetail) { - Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); + Assert.Contains("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0, last-in: 0, cur-in: 0", ex.Message); Assert.Contains($"mc: {connCount}/{completeCount}/0", ex.Message); Assert.Contains("serverEndpoint: " + server.EndPoint.ToString()?.Replace("Unspecified/", ""), ex.Message); } else { - Assert.DoesNotContain("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0", ex.Message); + Assert.DoesNotContain("inst: 0, qu: 0, qs: 0, aw: False, bw: Inactive, in: 0, in-pipe: 0, out-pipe: 0, last-in: 0, cur-in: 0", ex.Message); Assert.DoesNotContain($"mc: {connCount}/{completeCount}/0", ex.Message); Assert.DoesNotContain("serverEndpoint: " + server.EndPoint.ToString()?.Replace("Unspecified/", ""), ex.Message); } From 487145810016887fd58944a8b74634dd4276258f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 21 Oct 2022 16:23:17 +0100 Subject: [PATCH 191/435] Configuration/Connections: Allow HTTP tunneling (#2274) 1. add a tunnel config option, which is used to influence connection creation 2. implement http-tunnel as a well-known tunnel, integrated via the `http:` prefix on the tunnel option tunnels are implemented as concrete subclasses of the `Tunnel` type; an HTTP proxy "connect" implementation is provided as a well-known version that is supported inside `Parse` - but custom 3rd-party tunnel implementations can also be provided via the object-model (not `Parse`) A `Tunnel` allows: - overriding the `EndPoint` used to create `Socket` connections, or to suppress `Socket` creation entirely (by default, the same logical endpoint requested is provided back out) - provide a "before socket connect" twin (mirrors delegate approach, but: async; by default, do nothing) - provide a "before authenticate" injection point, which can a: perform additional handshake operations, and b: subvert the entire `Stream` (by default, nothing is done and no custom stream is returned) Co-authored-by: maksimkim Co-authored-by: Nick Craver --- docs/Configuration.md | 2 +- docs/ReleaseNotes.md | 1 + .../Configuration/Tunnel.cs | 114 ++++++++++++++++++ .../ConfigurationOptions.cs | 32 ++++- src/StackExchange.Redis/PhysicalConnection.cs | 57 +++++++-- .../PublicAPI/PublicAPI.Shipped.txt | 9 ++ tests/StackExchange.Redis.Tests/Config.cs | 15 +++ .../HttpTunnelConnect.cs | 63 ++++++++++ 8 files changed, 279 insertions(+), 14 deletions(-) create mode 100644 src/StackExchange.Redis/Configuration/Tunnel.cs create mode 100644 tests/StackExchange.Redis.Tests/HttpTunnelConnect.cs diff --git a/docs/Configuration.md b/docs/Configuration.md index b7de92e66..833bb80c1 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -96,7 +96,7 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a | asyncTimeout={int} | `AsyncTimeout` | `SyncTimeout` | Time (ms) to allow for asynchronous operations | | tiebreaker={string} | `TieBreaker` | `__Booksleeve_TieBreak` | Key to use for selecting a server in an ambiguous primary scenario | | version={string} | `DefaultVersion` | (`4.0` in Azure, else `2.0`) | Redis version level (useful when the server does not make this available) | - +| tunnel={string} | `Tunnel` | `null` | Tunnel for connections (use `http:{proxy url}` for "connect"-based proxy server) Additional code-only options: - ReconnectRetryPolicy (`IReconnectRetryPolicy`) - Default: `ReconnectRetryPolicy = ExponentialRetry(ConnectTimeout / 2);` diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 714dbde9a..815e9bb26 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Adds: `last-in` and `cur-in` (bytes) to timeout exceptions to help identify timeouts that were just-behind another large payload off the wire ([#2276 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2276)) +- Adds: general-purpose tunnel support, with HTTP proxy "connect" support included ([#2274 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2274)) ## 2.6.70 diff --git a/src/StackExchange.Redis/Configuration/Tunnel.cs b/src/StackExchange.Redis/Configuration/Tunnel.cs new file mode 100644 index 000000000..d6815a3ec --- /dev/null +++ b/src/StackExchange.Redis/Configuration/Tunnel.cs @@ -0,0 +1,114 @@ +using System; +using System.Buffers; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Pipelines.Sockets.Unofficial; + +namespace StackExchange.Redis.Configuration +{ + /// + /// Allows interception of the transport used to communicate with Redis. + /// + public abstract class Tunnel + { + /// + /// Gets the underlying socket endpoint to use when connecting to a logical endpoint. + /// + /// null should be returned if a socket is not required for this endpoint. + public virtual ValueTask GetSocketConnectEndpointAsync(EndPoint endpoint, CancellationToken cancellationToken) => new(endpoint); + + /// + /// Allows modification of a between creation and connection. + /// Passed in is the endpoint we're connecting to, which type of connection it is, and the socket itself. + /// For example, a specific local IP endpoint could be bound, linger time altered, etc. + /// + public virtual ValueTask BeforeSocketConnectAsync(EndPoint endPoint, ConnectionType connectionType, Socket? socket, CancellationToken cancellationToken) => default; + + /// + /// Invoked on a connected endpoint before server authentication and other handshakes occur, allowing pre-redis handshakes. By returning a custom , + /// the entire data flow can be intercepted, providing entire custom transports. + /// + public virtual ValueTask BeforeAuthenticateAsync(EndPoint endpoint, ConnectionType connectionType, Socket? socket, CancellationToken cancellationToken) => default; + /// + public abstract override string ToString(); + + private sealed class HttpProxyTunnel : Tunnel + { + public EndPoint Proxy { get; } + public HttpProxyTunnel(EndPoint proxy) => Proxy = proxy ?? throw new ArgumentNullException(nameof(proxy)); + + public override ValueTask GetSocketConnectEndpointAsync(EndPoint endpoint, CancellationToken cancellationToken) => new(Proxy); + + public override async ValueTask BeforeAuthenticateAsync(EndPoint endpoint, ConnectionType connectionType, Socket? socket, CancellationToken cancellationToken) + { + if (socket is not null) + { + var encoding = Encoding.ASCII; + var ep = Format.ToString(endpoint); + const string Prefix = "CONNECT ", Suffix = " HTTP/1.1\r\n\r\n", ExpectedResponse = "HTTP/1.1 200 OK\r\n\r\n"; + byte[] chunk = ArrayPool.Shared.Rent(Math.Max( + encoding.GetByteCount(Prefix) + encoding.GetByteCount(ep) + encoding.GetByteCount(Suffix), + encoding.GetByteCount(ExpectedResponse) + )); + var offset = 0; + offset += encoding.GetBytes(Prefix, 0, Prefix.Length, chunk, offset); + offset += encoding.GetBytes(ep, 0, ep.Length, chunk, offset); + offset += encoding.GetBytes(Suffix, 0, Suffix.Length, chunk, offset); + + static void SafeAbort(object? obj) + { + try + { + (obj as SocketAwaitableEventArgs)?.Abort(SocketError.TimedOut); + } + catch { } // best effort only + } + + using (var args = new SocketAwaitableEventArgs()) + using (cancellationToken.Register(static s => SafeAbort(s), args)) + { + args.SetBuffer(chunk, 0, offset); + if (!socket.SendAsync(args)) args.Complete(); + await args; + + // we expect to see: "HTTP/1.1 200 OK\n"; note our buffer is definitely big enough already + int toRead = encoding.GetByteCount(ExpectedResponse), read; + offset = 0; + + while (toRead > 0) + { + args.SetBuffer(chunk, offset, toRead); + if (!socket.ReceiveAsync(args)) args.Complete(); + read = await args; + + if (read <= 0) break; // EOF (since we're never doing zero-length reads) + toRead -= read; + offset += read; + } + if (toRead != 0) throw new EndOfStreamException("EOF negotiating HTTP tunnel"); + // lazy + var actualResponse = encoding.GetString(chunk, 0, offset); + if (ExpectedResponse != actualResponse) + { + throw new InvalidOperationException("Unexpected response negotiating HTTP tunnel"); + } + ArrayPool.Shared.Return(chunk); + } + } + return default; // no need for custom stream wrapper here + } + + public override string ToString() => "http:" + Format.ToString(Proxy); + } + + /// + /// Create a tunnel via an HTTP proxy server. + /// + /// The endpoint to use as an HTTP proxy server. + public static Tunnel HttpProxy(EndPoint proxy) => new HttpProxyTunnel(proxy); + } +} diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 061ac8f3e..6f71b7437 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -96,7 +96,8 @@ internal const string TieBreaker = "tiebreaker", Version = "version", WriteBuffer = "writeBuffer", - CheckCertificateRevocation = "checkCertificateRevocation"; + CheckCertificateRevocation = "checkCertificateRevocation", + Tunnel = "tunnel"; private static readonly Dictionary normalizedOptions = new[] { @@ -650,6 +651,7 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow #if NETCOREAPP3_1_OR_GREATER SslClientAuthenticationOptions = SslClientAuthenticationOptions, #endif + Tunnel = Tunnel, }; /// @@ -729,6 +731,10 @@ public string ToString(bool includePassword) Append(sb, OptionKeys.ConfigCheckSeconds, configCheckSeconds); Append(sb, OptionKeys.ResponseTimeout, responseTimeout); Append(sb, OptionKeys.DefaultDatabase, DefaultDatabase); + if (Tunnel is Tunnel tunnel) + { + Append(sb, OptionKeys.Tunnel, tunnel.ToString()); + } commandMap?.AppendDeltas(sb); return sb.ToString(); } @@ -877,6 +883,25 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) case OptionKeys.SslProtocols: SslProtocols = OptionKeys.ParseSslProtocols(key, value); break; + case OptionKeys.Tunnel: + if (value.IsNullOrWhiteSpace()) + { + Tunnel = null; + } + else if (value.StartsWith("http:")) + { + value = value.Substring(5); + if (!Format.TryParseEndPoint(value, out var ep)) + { + throw new ArgumentException("HTTP tunnel cannot be parsed: " + value); + } + Tunnel = Tunnel.HttpProxy(ep); + } + else + { + throw new ArgumentException("Tunnel cannot be parsed: " + value); + } + break; // Deprecated options we ignore... case OptionKeys.HighPrioritySocketThreads: case OptionKeys.PreserveAsyncOrder: @@ -914,5 +939,10 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) } return this; } + + /// + /// Allows custom transport implementations, such as http-tunneling via a proxy. + /// + public Tunnel? Tunnel { get; set; } } } diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index b4baa0e5e..6bfb872af 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -103,31 +103,48 @@ internal async Task BeginConnectAsync(LogProxy? log) } Trace("Connecting..."); - _socket = SocketManager.CreateSocket(endpoint); - bridge.Multiplexer.RawConfig.BeforeSocketConnect?.Invoke(endpoint, bridge.ConnectionType, _socket); + var tunnel = bridge.Multiplexer.RawConfig.Tunnel; + var connectTo = endpoint; + if (tunnel is not null) + { + connectTo = await tunnel.GetSocketConnectEndpointAsync(endpoint, CancellationToken.None).ForAwait(); + } + if (connectTo is not null) + { + _socket = SocketManager.CreateSocket(connectTo); + } + + if (_socket is not null) + { + bridge.Multiplexer.RawConfig.BeforeSocketConnect?.Invoke(endpoint, bridge.ConnectionType, _socket); + if (tunnel is not null) + { // same functionality as part of a tunnel + await tunnel.BeforeSocketConnectAsync(endpoint, bridge.ConnectionType, _socket, CancellationToken.None).ForAwait(); + } + } bridge.Multiplexer.OnConnecting(endpoint, bridge.ConnectionType); log?.WriteLine($"{Format.ToString(endpoint)}: BeginConnectAsync"); CancellationTokenSource? timeoutSource = null; try { - using (var args = new SocketAwaitableEventArgs + using (var args = connectTo is null ? null : new SocketAwaitableEventArgs { - RemoteEndPoint = endpoint, + RemoteEndPoint = connectTo, }) { var x = VolatileSocket; if (x == null) { - args.Abort(); + args?.Abort(); } - else if (x.ConnectAsync(args)) + else if (args is not null && x.ConnectAsync(args)) { // asynchronous operation is pending timeoutSource = ConfigureTimeout(args, bridge.Multiplexer.RawConfig.ConnectTimeout); } else { // completed synchronously - args.Complete(); + args?.Complete(); } // Complete connection @@ -136,7 +153,10 @@ internal async Task BeginConnectAsync(LogProxy? log) // If we're told to ignore connect, abort here if (BridgeCouldBeNull?.Multiplexer?.IgnoreConnect ?? false) return; - await args; // wait for the connect to complete or fail (will throw) + if (args is not null) + { + await args; // wait for the connect to complete or fail (will throw) + } if (timeoutSource != null) { timeoutSource.Cancel(); @@ -144,7 +164,7 @@ internal async Task BeginConnectAsync(LogProxy? log) } x = VolatileSocket; - if (x == null) + if (x == null && args is not null) { ConnectionMultiplexer.TraceWithoutContext("Socket was already aborted"); } @@ -1413,7 +1433,7 @@ public ConnectionStatus GetStatus() return null; } - internal async ValueTask ConnectedAsync(Socket socket, LogProxy? log, SocketManager manager) + internal async ValueTask ConnectedAsync(Socket? socket, LogProxy? log, SocketManager manager) { var bridge = BridgeCouldBeNull; if (bridge == null) return false; @@ -1430,6 +1450,13 @@ internal async ValueTask ConnectedAsync(Socket socket, LogProxy? log, Sock var config = bridge.Multiplexer.RawConfig; + var tunnel = config.Tunnel; + Stream? stream = null; + if (tunnel is not null) + { + stream = await tunnel.BeforeAuthenticateAsync(bridge.ServerEndPoint.EndPoint, bridge.ConnectionType, socket, CancellationToken.None).ForAwait(); + } + if (config.Ssl) { log?.WriteLine("Configuring TLS"); @@ -1439,7 +1466,8 @@ internal async ValueTask ConnectedAsync(Socket socket, LogProxy? log, Sock host = Format.ToStringHostOnly(bridge.ServerEndPoint.EndPoint); } - var ssl = new SslStream(new NetworkStream(socket), false, + stream ??= new NetworkStream(socket ?? throw new InvalidOperationException("No socket or stream available - possibly a tunnel error")); + var ssl = new SslStream(stream, false, config.CertificateValidationCallback ?? GetAmbientIssuerCertificateCallback(), config.CertificateSelectionCallback ?? GetAmbientClientCertificateCallback(), EncryptionPolicy.RequireEncryption); @@ -1475,7 +1503,12 @@ internal async ValueTask ConnectedAsync(Socket socket, LogProxy? log, Sock bridge.Multiplexer.Trace("Encryption failure"); return false; } - pipe = StreamConnection.GetDuplex(ssl, manager.SendPipeOptions, manager.ReceivePipeOptions, name: bridge.Name); + stream = ssl; + } + + if (stream is not null) + { + pipe = StreamConnection.GetDuplex(stream, manager.SendPipeOptions, manager.ReceivePipeOptions, name: bridge.Name); } else { diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 4f9d4658e..ad400b7ec 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -180,6 +180,13 @@ StackExchange.Redis.Configuration.AzureOptionsProvider.AzureOptionsProvider() -> StackExchange.Redis.Configuration.DefaultOptionsProvider StackExchange.Redis.Configuration.DefaultOptionsProvider.ClientName.get -> string! StackExchange.Redis.Configuration.DefaultOptionsProvider.DefaultOptionsProvider() -> void +StackExchange.Redis.Configuration.Tunnel +StackExchange.Redis.Configuration.Tunnel.Tunnel() -> void +override abstract StackExchange.Redis.Configuration.Tunnel.ToString() -> string! +static StackExchange.Redis.Configuration.Tunnel.HttpProxy(System.Net.EndPoint! proxy) -> StackExchange.Redis.Configuration.Tunnel! +virtual StackExchange.Redis.Configuration.Tunnel.BeforeAuthenticateAsync(System.Net.EndPoint! endpoint, StackExchange.Redis.ConnectionType connectionType, System.Net.Sockets.Socket? socket, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +virtual StackExchange.Redis.Configuration.Tunnel.BeforeSocketConnectAsync(System.Net.EndPoint! endPoint, StackExchange.Redis.ConnectionType connectionType, System.Net.Sockets.Socket? socket, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +virtual StackExchange.Redis.Configuration.Tunnel.GetSocketConnectEndpointAsync(System.Net.EndPoint! endpoint, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask StackExchange.Redis.ConfigurationOptions StackExchange.Redis.ConfigurationOptions.AbortOnConnectFail.get -> bool StackExchange.Redis.ConfigurationOptions.AbortOnConnectFail.set -> void @@ -260,6 +267,8 @@ StackExchange.Redis.ConfigurationOptions.TieBreaker.set -> void StackExchange.Redis.ConfigurationOptions.ToString(bool includePassword) -> string! StackExchange.Redis.ConfigurationOptions.TrustIssuer(string! issuerCertificatePath) -> void StackExchange.Redis.ConfigurationOptions.TrustIssuer(System.Security.Cryptography.X509Certificates.X509Certificate2! issuer) -> void +StackExchange.Redis.ConfigurationOptions.Tunnel.get -> StackExchange.Redis.Configuration.Tunnel? +StackExchange.Redis.ConfigurationOptions.Tunnel.set -> void StackExchange.Redis.ConfigurationOptions.User.get -> string? StackExchange.Redis.ConfigurationOptions.User.set -> void StackExchange.Redis.ConfigurationOptions.UseSsl.get -> bool diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index 5198a0e6c..a7560278c 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -611,4 +611,19 @@ public async Task MutableOptions() var newPass = options.Password = "newPassword"; Assert.Equal(newPass, conn.RawConfig.Password); } + + [Fact] + public void HttpTunnel() + { + var config = ConfigurationOptions.Parse("127.0.0.1:6380,tunnel=http:somewhere:22"); + var ip = Assert.IsType(Assert.Single(config.EndPoints)); + Assert.Equal(6380, ip.Port); + Assert.Equal("127.0.0.1", ip.Address.ToString()); + + Assert.NotNull(config.Tunnel); + Assert.Equal("http:somewhere:22", config.Tunnel.ToString()); + + var cs = config.ToString(); + Assert.Equal("127.0.0.1:6380,tunnel=http:somewhere:22", cs); + } } diff --git a/tests/StackExchange.Redis.Tests/HttpTunnelConnect.cs b/tests/StackExchange.Redis.Tests/HttpTunnelConnect.cs new file mode 100644 index 000000000..e77f8e9d1 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/HttpTunnelConnect.cs @@ -0,0 +1,63 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests +{ + public class HttpTunnelConnect + { + private ITestOutputHelper Log { get; } + public HttpTunnelConnect(ITestOutputHelper log) => Log = log; + + [Theory] + [InlineData("")] + [InlineData(",tunnel=http:127.0.0.1:8080")] + public async Task Connect(string suffix) + { + var cs = Environment.GetEnvironmentVariable("HACK_TUNNEL_ENDPOINT"); + if (string.IsNullOrWhiteSpace(cs)) + { + Skip.Inconclusive("Need HACK_TUNNEL_ENDPOINT environment variable"); + } + var config = ConfigurationOptions.Parse(cs + suffix); + if (!string.IsNullOrWhiteSpace(suffix)) + { + Assert.NotNull(config.Tunnel); + } + await using var conn = await ConnectionMultiplexer.ConnectAsync(config); + var db = conn.GetDatabase(); + await db.PingAsync(); + RedisKey key = "HttpTunnel"; + await db.KeyDeleteAsync(key); + + // latency test + var watch = Stopwatch.StartNew(); + const int LATENCY_LOOP = 25, BANDWIDTH_LOOP = 10; + for (int i = 0; i < LATENCY_LOOP; i++) + { + await db.StringIncrementAsync(key); + } + watch.Stop(); + int count = (int)await db.StringGetAsync(key); + Log.WriteLine($"{LATENCY_LOOP}xINCR: {watch.ElapsedMilliseconds}ms"); + Assert.Equal(LATENCY_LOOP, count); + + // bandwidth test + var chunk = new byte[4096]; + var rand = new Random(1234); + for (int i = 0; i < BANDWIDTH_LOOP; i++) + { + rand.NextBytes(chunk); + watch = Stopwatch.StartNew(); + await db.StringSetAsync(key, chunk); + using var fetch = await db.StringGetLeaseAsync(key); + watch.Stop(); + Assert.NotNull(fetch); + Log.WriteLine($"SET+GET {chunk.Length} bytes: {watch.ElapsedMilliseconds}ms"); + Assert.True(fetch.Span.SequenceEqual(chunk)); + } + } + } +} From 6b20bba3a5599914220bff82f254dea6229e969d Mon Sep 17 00:00:00 2001 From: John Coulter Date: Mon, 24 Oct 2022 00:37:18 -0400 Subject: [PATCH 192/435] Correction on documented behavior of 'httpRuntime targetFramework="4.5"' (#2254) The old wording equated `` to ``, when the linked citation makes it clear that the effect is the opposite: `` is inferred as adding the setting ``. > Second, `` is a shortcut that allows the ASP.NET runtime to infer a wide array of configuration settings. If the runtime sees this setting, it will expand it out just as if you had written the following: > ``` > ... > > ``` https://devblogs.microsoft.com/dotnet/all-about-httpruntime-targetframework/ Co-authored-by: Nick Craver --- docs/ThreadTheft.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ThreadTheft.md b/docs/ThreadTheft.md index 6f6581bf2..d5b8e717e 100644 --- a/docs/ThreadTheft.md +++ b/docs/ThreadTheft.md @@ -44,7 +44,7 @@ configure ASP.NET with: ``` -or +or if you do _not_ have a `` of at least 4.5 (which causes the above to default `true`) like this: ```xml From e6bd1614b9b27eca6ecc357820892af9ccf61b8b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 24 Oct 2022 09:22:20 +0100 Subject: [PATCH 193/435] custom tunnel should not break ConfigurationOptions.ToString() --- .../Configuration/Tunnel.cs | 5 +++-- .../ConfigurationOptions.cs | 2 +- .../PublicAPI/PublicAPI.Shipped.txt | 1 - tests/StackExchange.Redis.Tests/Config.cs | 20 +++++++++++++++++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/StackExchange.Redis/Configuration/Tunnel.cs b/src/StackExchange.Redis/Configuration/Tunnel.cs index d6815a3ec..2deaf664a 100644 --- a/src/StackExchange.Redis/Configuration/Tunnel.cs +++ b/src/StackExchange.Redis/Configuration/Tunnel.cs @@ -21,6 +21,8 @@ public abstract class Tunnel /// null should be returned if a socket is not required for this endpoint. public virtual ValueTask GetSocketConnectEndpointAsync(EndPoint endpoint, CancellationToken cancellationToken) => new(endpoint); + internal virtual bool IsInbuilt => false; // only inbuilt tunnels get added to config strings + /// /// Allows modification of a between creation and connection. /// Passed in is the endpoint we're connecting to, which type of connection it is, and the socket itself. @@ -33,8 +35,6 @@ public abstract class Tunnel /// the entire data flow can be intercepted, providing entire custom transports. /// public virtual ValueTask BeforeAuthenticateAsync(EndPoint endpoint, ConnectionType connectionType, Socket? socket, CancellationToken cancellationToken) => default; - /// - public abstract override string ToString(); private sealed class HttpProxyTunnel : Tunnel { @@ -102,6 +102,7 @@ static void SafeAbort(object? obj) return default; // no need for custom stream wrapper here } + internal override bool IsInbuilt => true; public override string ToString() => "http:" + Format.ToString(Proxy); } diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 6f71b7437..e773944d1 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -731,7 +731,7 @@ public string ToString(bool includePassword) Append(sb, OptionKeys.ConfigCheckSeconds, configCheckSeconds); Append(sb, OptionKeys.ResponseTimeout, responseTimeout); Append(sb, OptionKeys.DefaultDatabase, DefaultDatabase); - if (Tunnel is Tunnel tunnel) + if (Tunnel is { IsInbuilt: true } tunnel) { Append(sb, OptionKeys.Tunnel, tunnel.ToString()); } diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index ad400b7ec..f1201d35f 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -182,7 +182,6 @@ StackExchange.Redis.Configuration.DefaultOptionsProvider.ClientName.get -> strin StackExchange.Redis.Configuration.DefaultOptionsProvider.DefaultOptionsProvider() -> void StackExchange.Redis.Configuration.Tunnel StackExchange.Redis.Configuration.Tunnel.Tunnel() -> void -override abstract StackExchange.Redis.Configuration.Tunnel.ToString() -> string! static StackExchange.Redis.Configuration.Tunnel.HttpProxy(System.Net.EndPoint! proxy) -> StackExchange.Redis.Configuration.Tunnel! virtual StackExchange.Redis.Configuration.Tunnel.BeforeAuthenticateAsync(System.Net.EndPoint! endpoint, StackExchange.Redis.ConnectionType connectionType, System.Net.Sockets.Socket? socket, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask virtual StackExchange.Redis.Configuration.Tunnel.BeforeSocketConnectAsync(System.Net.EndPoint! endPoint, StackExchange.Redis.ConnectionType connectionType, System.Net.Sockets.Socket? socket, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/Config.cs index a7560278c..c3a253173 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/Config.cs @@ -1,4 +1,5 @@ -using System; +using StackExchange.Redis.Configuration; +using System; using System.Globalization; using System.IO; using System.IO.Pipelines; @@ -613,7 +614,7 @@ public async Task MutableOptions() } [Fact] - public void HttpTunnel() + public void HttpTunnelCanRoundtrip() { var config = ConfigurationOptions.Parse("127.0.0.1:6380,tunnel=http:somewhere:22"); var ip = Assert.IsType(Assert.Single(config.EndPoints)); @@ -626,4 +627,19 @@ public void HttpTunnel() var cs = config.ToString(); Assert.Equal("127.0.0.1:6380,tunnel=http:somewhere:22", cs); } + + private class CustomTunnel : Tunnel { } + + [Fact] + public void CustomTunnelCanRoundtripMinusTunnel() + { + // we don't expect to be able to parse custom tunnels, but we should still be able to round-trip + // the rest of the config, which means ignoring them *in both directions* (unless first party) + var options = ConfigurationOptions.Parse("127.0.0.1,Ssl=true"); + options.Tunnel = new CustomTunnel(); + var cs = options.ToString(); + Assert.Equal("127.0.0.1,ssl=True", cs); + options = ConfigurationOptions.Parse(cs); + Assert.Null(options.Tunnel); + } } From c76e9aed68cbca8c3d6dc776dfe7a59fb7a77879 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 24 Oct 2022 19:22:56 -0400 Subject: [PATCH 194/435] Tests: align naming for easier finding, etc. (#2280) This aligns all tests to *Tests.cs (and class names), moves a few heleprs to Helpers\ and general cleanup - no net significant changes. Next: finally renaming the wrapper classes to be sensible :) --- .../{BasicOps.cs => BasicOpTests.cs} | 0 .../{Batches.cs => BatchTests.cs} | 4 ++-- .../{Bits.cs => BitTests.cs} | 4 ++-- .../{BoxUnbox.cs => BoxUnboxTests.cs} | 0 .../{Cluster.cs => ClusterTests.cs} | 6 +++--- .../{Commands.cs => CommandTests.cs} | 2 +- ...mandTimeouts.cs => CommandTimeoutTests.cs} | 4 ++-- .../{Config.cs => ConfigTests.cs} | 4 ++-- .../{ConnectByIP.cs => ConnectByIPTests.cs} | 4 ++-- ...mConfig.cs => ConnectCustomConfigTests.cs} | 4 ++-- ...lTimeout.cs => ConnectFailTimeoutTests.cs} | 4 ++-- ...ost.cs => ConnectToUnexistingHostTests.cs} | 4 ++-- ...ion.cs => ConnectingFailDetectionTests.cs} | 4 ++-- ...rors.cs => ConnectionFailedErrorsTests.cs} | 6 +++--- ...Shutdown.cs => ConnectionShutdownTests.cs} | 4 ++-- .../{Constraints.cs => ConstraintsTests.cs} | 4 ++-- .../{Copy.cs => CopyTests.cs} | 4 ++-- .../{Databases.cs => DatabaseTests.cs} | 4 ++-- ...faultOptions.cs => DefaultOptionsTests.cs} | 4 ++-- .../{DefaultPorts.cs => DefaultPortsTests.cs} | 2 +- .../{Deprecated.cs => DeprecatedTests.cs} | 4 ++-- .../{Execute.cs => ExecuteTests.cs} | 0 .../{Expiry.cs => ExpiryTests.cs} | 4 ++-- .../{FSharpCompat.cs => FSharpCompatTests.cs} | 4 ++-- .../{Failover.cs => FailoverTests.cs} | 4 ++-- .../{FeatureFlags.cs => FeatureFlagTests.cs} | 2 +- ...FloatingPoint.cs => FloatingPointTests.cs} | 4 ++-- .../GlobalSuppressions.cs | 20 +++++++++---------- .../{Hashes.cs => HashTests.cs} | 4 ++-- .../{ => Helpers}/SharedConnectionFixture.cs | 0 .../{ => Helpers}/TestExtensions.cs | 0 .../Helpers/redis-sharp.cs | 2 +- ...elConnect.cs => HttpTunnelConnectTests.cs} | 4 ++-- .../{HyperLogLog.cs => HyperLogLogTests.cs} | 4 ++-- ...Checks.cs => InfoReplicationCheckTests.cs} | 4 ++-- ...SaveResponse.cs => BgSaveResponseTests.cs} | 4 ++-- ...ultDatabase.cs => DefaultDatabaseTests.cs} | 4 ++-- .../Issues/{Issue10.cs => Issue10Tests.cs} | 4 ++-- .../{Issue1101.cs => Issue1101Tests.cs} | 4 ++-- .../{Issue1103.cs => Issue1103Tests.cs} | 4 ++-- .../Issues/{Issue182.cs => Issue182Tests.cs} | 4 ++-- .../{Issue2164.cs => Issue2164Tests.cs} | 2 +- .../{Issue2176.cs => Issue2176Tests.cs} | 4 ++-- .../Issues/{Issue25.cs => Issue25Tests.cs} | 4 ++-- .../Issues/{Issue6.cs => Issue6Tests.cs} | 4 ++-- ...assive Delete.cs => MassiveDeleteTests.cs} | 4 ++-- .../{SO10504853.cs => SO10504853Tests.cs} | 4 ++-- .../{SO10825542.cs => SO10825542Tests.cs} | 4 ++-- .../{SO11766033.cs => SO11766033Tests.cs} | 4 ++-- .../{SO22786599.cs => SO22786599Tests.cs} | 4 ++-- .../{SO23949477.cs => SO23949477Tests.cs} | 4 ++-- .../{SO24807536.cs => SO24807536Tests.cs} | 4 ++-- .../{SO25113323.cs => SO25113323Tests.cs} | 7 +++---- .../{SO25567566.cs => SO25567566Tests.cs} | 4 ++-- .../{KeysAndValues.cs => KeyAndValueTests.cs} | 2 +- .../{Keys.cs => KeyTests.cs} | 4 ++-- .../{Latency.cs => LatencyTests.cs} | 4 ++-- .../{Lex.cs => LexTests.cs} | 4 ++-- .../{Lists.cs => ListTests.cs} | 4 ++-- .../{Locking.cs => LockingTests.cs} | 4 ++-- .../{MassiveOps.cs => MassiveOpsTests.cs} | 4 ++-- .../{Memory.cs => MemoryTests.cs} | 4 ++-- .../{Migrate.cs => MigrateTests.cs} | 4 ++-- .../{MultiAdd.cs => MultiAddTests.cs} | 4 ++-- .../{MultiPrimary.cs => MultiPrimaryTests.cs} | 4 ++-- .../{Naming.cs => NamingTests.cs} | 4 ++-- ...erloadCompat.cs => OverloadCompatTests.cs} | 4 ++-- .../{Parse.cs => ParseTests.cs} | 0 .../{Performance.cs => PerformanceTests.cs} | 4 ++-- ...PreserveOrder.cs => PreserveOrderTests.cs} | 4 ++-- .../{Profiling.cs => ProfilingTests.cs} | 4 ++-- ...PubSubCommand.cs => PubSubCommandTests.cs} | 4 ++-- ...ltiserver.cs => PubSubMultiserverTests.cs} | 4 ++-- .../{PubSub.cs => PubSubTests.cs} | 4 ++-- .../{RealWorld.cs => RealWorldTests.cs} | 4 ++-- ...lency.cs => RedisValueEquivalencyTests.cs} | 6 +++--- .../{Roles.cs => RoleTests.cs} | 0 .../{SSDB.cs => SSDBTests.cs} | 4 ++-- .../{SSL.cs => SSLTests.cs} | 4 ++-- .../{SanityChecks.cs => SanityCheckTests.cs} | 0 .../{Scans.cs => ScanTests.cs} | 4 ++-- .../{Scripting.cs => ScriptingTests.cs} | 4 ++-- .../{Secure.cs => SecureTests.cs} | 4 ++-- ...elFailover.cs => SentinelFailoverTests.cs} | 4 ++-- .../{Sentinel.cs => SentinelTests.cs} | 4 ++-- .../{Sets.cs => SetTests.cs} | 4 ++-- .../{Sockets.cs => SocketTests.cs} | 4 ++-- .../{SortedSets.cs => SortedSetTests.cs} | 4 ++-- ...SortedSetWhen.cs => SortedSetWhenTests.cs} | 0 .../{Streams.cs => StreamTests.cs} | 4 ++-- .../{Strings.cs => StringTests.cs} | 4 ++-- tests/StackExchange.Redis.Tests/TestBase.cs | 1 - .../{Transactions.cs => TransactionTests.cs} | 4 ++-- .../{Values.cs => ValueTests.cs} | 4 ++-- 94 files changed, 174 insertions(+), 176 deletions(-) rename tests/StackExchange.Redis.Tests/{BasicOps.cs => BasicOpTests.cs} (100%) rename tests/StackExchange.Redis.Tests/{Batches.cs => BatchTests.cs} (91%) rename tests/StackExchange.Redis.Tests/{Bits.cs => BitTests.cs} (76%) rename tests/StackExchange.Redis.Tests/{BoxUnbox.cs => BoxUnboxTests.cs} (100%) rename tests/StackExchange.Redis.Tests/{Cluster.cs => ClusterTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{Commands.cs => CommandTests.cs} (98%) rename tests/StackExchange.Redis.Tests/{CommandTimeouts.cs => CommandTimeoutTests.cs} (94%) rename tests/StackExchange.Redis.Tests/{Config.cs => ConfigTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{ConnectByIP.cs => ConnectByIPTests.cs} (97%) rename tests/StackExchange.Redis.Tests/{ConnectCustomConfig.cs => ConnectCustomConfigTests.cs} (95%) rename tests/StackExchange.Redis.Tests/{ConnectFailTimeout.cs => ConnectFailTimeoutTests.cs} (92%) rename tests/StackExchange.Redis.Tests/{ConnectToUnexistingHost.cs => ConnectToUnexistingHostTests.cs} (95%) rename tests/StackExchange.Redis.Tests/{ConnectingFailDetection.cs => ConnectingFailDetectionTests.cs} (97%) rename tests/StackExchange.Redis.Tests/{ConnectionFailedErrors.cs => ConnectionFailedErrorsTests.cs} (97%) rename tests/StackExchange.Redis.Tests/{ConnectionShutdown.cs => ConnectionShutdownTests.cs} (94%) rename tests/StackExchange.Redis.Tests/{Constraints.cs => ConstraintsTests.cs} (89%) rename tests/StackExchange.Redis.Tests/{Copy.cs => CopyTests.cs} (93%) rename tests/StackExchange.Redis.Tests/{Databases.cs => DatabaseTests.cs} (97%) rename tests/StackExchange.Redis.Tests/{DefaultOptions.cs => DefaultOptionsTests.cs} (98%) rename tests/StackExchange.Redis.Tests/{DefaultPorts.cs => DefaultPortsTests.cs} (98%) rename tests/StackExchange.Redis.Tests/{Deprecated.cs => DeprecatedTests.cs} (95%) rename tests/StackExchange.Redis.Tests/{Execute.cs => ExecuteTests.cs} (100%) rename tests/StackExchange.Redis.Tests/{Expiry.cs => ExpiryTests.cs} (98%) rename tests/StackExchange.Redis.Tests/{FSharpCompat.cs => FSharpCompatTests.cs} (83%) rename tests/StackExchange.Redis.Tests/{Failover.cs => FailoverTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{FeatureFlags.cs => FeatureFlagTests.cs} (96%) rename tests/StackExchange.Redis.Tests/{FloatingPoint.cs => FloatingPointTests.cs} (96%) rename tests/StackExchange.Redis.Tests/{Hashes.cs => HashTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{ => Helpers}/SharedConnectionFixture.cs (100%) rename tests/StackExchange.Redis.Tests/{ => Helpers}/TestExtensions.cs (100%) rename tests/StackExchange.Redis.Tests/{HttpTunnelConnect.cs => HttpTunnelConnectTests.cs} (94%) rename tests/StackExchange.Redis.Tests/{HyperLogLog.cs => HyperLogLogTests.cs} (83%) rename tests/StackExchange.Redis.Tests/{TestInfoReplicationChecks.cs => InfoReplicationCheckTests.cs} (87%) rename tests/StackExchange.Redis.Tests/Issues/{BgSaveResponse.cs => BgSaveResponseTests.cs} (81%) rename tests/StackExchange.Redis.Tests/Issues/{DefaultDatabase.cs => DefaultDatabaseTests.cs} (91%) rename tests/StackExchange.Redis.Tests/Issues/{Issue10.cs => Issue10Tests.cs} (87%) rename tests/StackExchange.Redis.Tests/Issues/{Issue1101.cs => Issue1101Tests.cs} (98%) rename tests/StackExchange.Redis.Tests/Issues/{Issue1103.cs => Issue1103Tests.cs} (94%) rename tests/StackExchange.Redis.Tests/Issues/{Issue182.cs => Issue182Tests.cs} (95%) rename tests/StackExchange.Redis.Tests/Issues/{Issue2164.cs => Issue2164Tests.cs} (98%) rename tests/StackExchange.Redis.Tests/Issues/{Issue2176.cs => Issue2176Tests.cs} (95%) rename tests/StackExchange.Redis.Tests/Issues/{Issue25.cs => Issue25Tests.cs} (91%) rename tests/StackExchange.Redis.Tests/Issues/{Issue6.cs => Issue6Tests.cs} (77%) rename tests/StackExchange.Redis.Tests/Issues/{Massive Delete.cs => MassiveDeleteTests.cs} (95%) rename tests/StackExchange.Redis.Tests/Issues/{SO10504853.cs => SO10504853Tests.cs} (95%) rename tests/StackExchange.Redis.Tests/Issues/{SO10825542.cs => SO10825542Tests.cs} (87%) rename tests/StackExchange.Redis.Tests/Issues/{SO11766033.cs => SO11766033Tests.cs} (87%) rename tests/StackExchange.Redis.Tests/Issues/{SO22786599.cs => SO22786599Tests.cs} (89%) rename tests/StackExchange.Redis.Tests/Issues/{SO23949477.cs => SO23949477Tests.cs} (90%) rename tests/StackExchange.Redis.Tests/Issues/{SO24807536.cs => SO24807536Tests.cs} (92%) rename tests/StackExchange.Redis.Tests/Issues/{SO25113323.cs => SO25113323Tests.cs} (87%) rename tests/StackExchange.Redis.Tests/Issues/{SO25567566.cs => SO25567566Tests.cs} (94%) rename tests/StackExchange.Redis.Tests/{KeysAndValues.cs => KeyAndValueTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{Keys.cs => KeyTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{Latency.cs => LatencyTests.cs} (94%) rename tests/StackExchange.Redis.Tests/{Lex.cs => LexTests.cs} (96%) rename tests/StackExchange.Redis.Tests/{Lists.cs => ListTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{Locking.cs => LockingTests.cs} (98%) rename tests/StackExchange.Redis.Tests/{MassiveOps.cs => MassiveOpsTests.cs} (96%) rename tests/StackExchange.Redis.Tests/{Memory.cs => MemoryTests.cs} (93%) rename tests/StackExchange.Redis.Tests/{Migrate.cs => MigrateTests.cs} (95%) rename tests/StackExchange.Redis.Tests/{MultiAdd.cs => MultiAddTests.cs} (96%) rename tests/StackExchange.Redis.Tests/{MultiPrimary.cs => MultiPrimaryTests.cs} (97%) rename tests/StackExchange.Redis.Tests/{Naming.cs => NamingTests.cs} (98%) rename tests/StackExchange.Redis.Tests/{OverloadCompat.cs => OverloadCompatTests.cs} (98%) rename tests/StackExchange.Redis.Tests/{Parse.cs => ParseTests.cs} (100%) rename tests/StackExchange.Redis.Tests/{Performance.cs => PerformanceTests.cs} (97%) rename tests/StackExchange.Redis.Tests/{PreserveOrder.cs => PreserveOrderTests.cs} (92%) rename tests/StackExchange.Redis.Tests/{Profiling.cs => ProfilingTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{PubSubCommand.cs => PubSubCommandTests.cs} (94%) rename tests/StackExchange.Redis.Tests/{PubSubMultiserver.cs => PubSubMultiserverTests.cs} (97%) rename tests/StackExchange.Redis.Tests/{PubSub.cs => PubSubTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{RealWorld.cs => RealWorldTests.cs} (90%) rename tests/StackExchange.Redis.Tests/{RedisValueEquivalency.cs => RedisValueEquivalencyTests.cs} (98%) rename tests/StackExchange.Redis.Tests/{Roles.cs => RoleTests.cs} (100%) rename tests/StackExchange.Redis.Tests/{SSDB.cs => SSDBTests.cs} (88%) rename tests/StackExchange.Redis.Tests/{SSL.cs => SSLTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{SanityChecks.cs => SanityCheckTests.cs} (100%) rename tests/StackExchange.Redis.Tests/{Scans.cs => ScanTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{Scripting.cs => ScriptingTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{Secure.cs => SecureTests.cs} (96%) rename tests/StackExchange.Redis.Tests/{SentinelFailover.cs => SentinelFailoverTests.cs} (97%) rename tests/StackExchange.Redis.Tests/{Sentinel.cs => SentinelTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{Sets.cs => SetTests.cs} (98%) rename tests/StackExchange.Redis.Tests/{Sockets.cs => SocketTests.cs} (87%) rename tests/StackExchange.Redis.Tests/{SortedSets.cs => SortedSetTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{SortedSetWhen.cs => SortedSetWhenTests.cs} (100%) rename tests/StackExchange.Redis.Tests/{Streams.cs => StreamTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{Strings.cs => StringTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{Transactions.cs => TransactionTests.cs} (99%) rename tests/StackExchange.Redis.Tests/{Values.cs => ValueTests.cs} (92%) diff --git a/tests/StackExchange.Redis.Tests/BasicOps.cs b/tests/StackExchange.Redis.Tests/BasicOpTests.cs similarity index 100% rename from tests/StackExchange.Redis.Tests/BasicOps.cs rename to tests/StackExchange.Redis.Tests/BasicOpTests.cs diff --git a/tests/StackExchange.Redis.Tests/Batches.cs b/tests/StackExchange.Redis.Tests/BatchTests.cs similarity index 91% rename from tests/StackExchange.Redis.Tests/Batches.cs rename to tests/StackExchange.Redis.Tests/BatchTests.cs index 8590e15b8..6783360e5 100644 --- a/tests/StackExchange.Redis.Tests/Batches.cs +++ b/tests/StackExchange.Redis.Tests/BatchTests.cs @@ -7,9 +7,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Batches : TestBase +public class BatchTests : TestBase { - public Batches(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public BatchTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public void TestBatchNotSent() diff --git a/tests/StackExchange.Redis.Tests/Bits.cs b/tests/StackExchange.Redis.Tests/BitTests.cs similarity index 76% rename from tests/StackExchange.Redis.Tests/Bits.cs rename to tests/StackExchange.Redis.Tests/BitTests.cs index e78b09dfa..5dd3d05c2 100644 --- a/tests/StackExchange.Redis.Tests/Bits.cs +++ b/tests/StackExchange.Redis.Tests/BitTests.cs @@ -4,9 +4,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Bits : TestBase +public class BitTests : TestBase { - public Bits(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public BitTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } [Fact] public void BasicOps() diff --git a/tests/StackExchange.Redis.Tests/BoxUnbox.cs b/tests/StackExchange.Redis.Tests/BoxUnboxTests.cs similarity index 100% rename from tests/StackExchange.Redis.Tests/BoxUnbox.cs rename to tests/StackExchange.Redis.Tests/BoxUnboxTests.cs diff --git a/tests/StackExchange.Redis.Tests/Cluster.cs b/tests/StackExchange.Redis.Tests/ClusterTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/Cluster.cs rename to tests/StackExchange.Redis.Tests/ClusterTests.cs index d1fe5ef5f..c945812a8 100644 --- a/tests/StackExchange.Redis.Tests/Cluster.cs +++ b/tests/StackExchange.Redis.Tests/ClusterTests.cs @@ -11,9 +11,9 @@ namespace StackExchange.Redis.Tests; -public class Cluster : TestBase +public class ClusterTests : TestBase { - public Cluster(ITestOutputHelper output) : base (output) { } + public ClusterTests(ITestOutputHelper output) : base (output) { } protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; [Fact] @@ -652,7 +652,7 @@ public void MovedProfiling() var Key = Me(); const string Value = "redirected-value"; - var profiler = new Profiling.PerThreadProfiler(); + var profiler = new ProfilingTests.PerThreadProfiler(); using var conn = Create(); diff --git a/tests/StackExchange.Redis.Tests/Commands.cs b/tests/StackExchange.Redis.Tests/CommandTests.cs similarity index 98% rename from tests/StackExchange.Redis.Tests/Commands.cs rename to tests/StackExchange.Redis.Tests/CommandTests.cs index c63dbba04..42df92dd1 100644 --- a/tests/StackExchange.Redis.Tests/Commands.cs +++ b/tests/StackExchange.Redis.Tests/CommandTests.cs @@ -3,7 +3,7 @@ namespace StackExchange.Redis.Tests; -public class Commands +public class CommandTests { [Fact] public void CommandByteLength() diff --git a/tests/StackExchange.Redis.Tests/CommandTimeouts.cs b/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs similarity index 94% rename from tests/StackExchange.Redis.Tests/CommandTimeouts.cs rename to tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs index bcb4c8754..4ba77cfd8 100644 --- a/tests/StackExchange.Redis.Tests/CommandTimeouts.cs +++ b/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class CommandTimeouts : TestBase +public class CommandTimeoutTests : TestBase { - public CommandTimeouts(ITestOutputHelper output) : base (output) { } + public CommandTimeoutTests(ITestOutputHelper output) : base (output) { } [Fact] public async Task DefaultHeartbeatTimeout() diff --git a/tests/StackExchange.Redis.Tests/Config.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/Config.cs rename to tests/StackExchange.Redis.Tests/ConfigTests.cs index c3a253173..2f74496ef 100644 --- a/tests/StackExchange.Redis.Tests/Config.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -15,12 +15,12 @@ namespace StackExchange.Redis.Tests; -public class Config : TestBase +public class ConfigTests : TestBase { public Version DefaultVersion = new (3, 0, 0); public Version DefaultAzureVersion = new (4, 0, 0); - public Config(ITestOutputHelper output) : base(output) { } + public ConfigTests(ITestOutputHelper output) : base(output) { } [Fact] public void SslProtocols_SingleValue() diff --git a/tests/StackExchange.Redis.Tests/ConnectByIP.cs b/tests/StackExchange.Redis.Tests/ConnectByIPTests.cs similarity index 97% rename from tests/StackExchange.Redis.Tests/ConnectByIP.cs rename to tests/StackExchange.Redis.Tests/ConnectByIPTests.cs index 73a0b9825..b79f2e07d 100644 --- a/tests/StackExchange.Redis.Tests/ConnectByIP.cs +++ b/tests/StackExchange.Redis.Tests/ConnectByIPTests.cs @@ -7,9 +7,9 @@ namespace StackExchange.Redis.Tests; -public class ConnectByIP : TestBase +public class ConnectByIPTests : TestBase { - public ConnectByIP(ITestOutputHelper output) : base (output) { } + public ConnectByIPTests(ITestOutputHelper output) : base (output) { } [Fact] public void ParseEndpoints() diff --git a/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs b/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs similarity index 95% rename from tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs rename to tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs index c7c2b6045..8eeea36e9 100644 --- a/tests/StackExchange.Redis.Tests/ConnectCustomConfig.cs +++ b/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs @@ -3,9 +3,9 @@ namespace StackExchange.Redis.Tests; -public class ConnectCustomConfig : TestBase +public class ConnectCustomConfigTests : TestBase { - public ConnectCustomConfig(ITestOutputHelper output) : base (output) { } + public ConnectCustomConfigTests(ITestOutputHelper output) : base (output) { } // So we're triggering tiebreakers here protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; diff --git a/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs b/tests/StackExchange.Redis.Tests/ConnectFailTimeoutTests.cs similarity index 92% rename from tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs rename to tests/StackExchange.Redis.Tests/ConnectFailTimeoutTests.cs index 1362e92a8..2b4b5a29f 100644 --- a/tests/StackExchange.Redis.Tests/ConnectFailTimeout.cs +++ b/tests/StackExchange.Redis.Tests/ConnectFailTimeoutTests.cs @@ -5,9 +5,9 @@ namespace StackExchange.Redis.Tests; -public class ConnectFailTimeout : TestBase +public class ConnectFailTimeoutTests : TestBase { - public ConnectFailTimeout(ITestOutputHelper output) : base (output) { } + public ConnectFailTimeoutTests(ITestOutputHelper output) : base (output) { } [Fact] public async Task NoticesConnectFail() diff --git a/tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs b/tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs similarity index 95% rename from tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs rename to tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs index 17f260ede..35767d753 100644 --- a/tests/StackExchange.Redis.Tests/ConnectToUnexistingHost.cs +++ b/tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs @@ -7,9 +7,9 @@ namespace StackExchange.Redis.Tests; -public class ConnectToUnexistingHost : TestBase +public class ConnectToUnexistingHostTests : TestBase { - public ConnectToUnexistingHost(ITestOutputHelper output) : base (output) { } + public ConnectToUnexistingHostTests(ITestOutputHelper output) : base (output) { } [Fact] public async Task FailsWithinTimeout() diff --git a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs b/tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs similarity index 97% rename from tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs rename to tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs index 3fe53187c..7ed717eab 100644 --- a/tests/StackExchange.Redis.Tests/ConnectingFailDetection.cs +++ b/tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis.Tests; -public class ConnectingFailDetection : TestBase +public class ConnectingFailDetectionTests : TestBase { - public ConnectingFailDetection(ITestOutputHelper output) : base (output) { } + public ConnectingFailDetectionTests(ITestOutputHelper output) : base (output) { } protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; diff --git a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs b/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs similarity index 97% rename from tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs rename to tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs index 791bbd44e..3f7576c65 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionFailedErrors.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs @@ -8,9 +8,9 @@ namespace StackExchange.Redis.Tests; -public class ConnectionFailedErrors : TestBase +public class ConnectionFailedErrorsTests : TestBase { - public ConnectionFailedErrors(ITestOutputHelper output) : base (output) { } + public ConnectionFailedErrorsTests(ITestOutputHelper output) : base (output) { } [Theory] [InlineData(true)] @@ -68,7 +68,7 @@ public async Task AuthenticationFailureError() options.Ssl = true; options.Password = ""; options.AbortOnConnectFail = false; - options.CertificateValidation += SSL.ShowCertFailures(Writer); + options.CertificateValidation += SSLTests.ShowCertFailures(Writer); using var conn = ConnectionMultiplexer.Connect(options); diff --git a/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs b/tests/StackExchange.Redis.Tests/ConnectionShutdownTests.cs similarity index 94% rename from tests/StackExchange.Redis.Tests/ConnectionShutdown.cs rename to tests/StackExchange.Redis.Tests/ConnectionShutdownTests.cs index 13d06cbfd..c39bc4a76 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionShutdown.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionShutdownTests.cs @@ -6,10 +6,10 @@ namespace StackExchange.Redis.Tests; -public class ConnectionShutdown : TestBase +public class ConnectionShutdownTests : TestBase { protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public ConnectionShutdown(ITestOutputHelper output) : base(output) { } + public ConnectionShutdownTests(ITestOutputHelper output) : base(output) { } [Fact(Skip = "Unfriendly")] public async Task ShutdownRaisesConnectionFailedAndRestore() diff --git a/tests/StackExchange.Redis.Tests/Constraints.cs b/tests/StackExchange.Redis.Tests/ConstraintsTests.cs similarity index 89% rename from tests/StackExchange.Redis.Tests/Constraints.cs rename to tests/StackExchange.Redis.Tests/ConstraintsTests.cs index 614c6b4a1..d5d58cbef 100644 --- a/tests/StackExchange.Redis.Tests/Constraints.cs +++ b/tests/StackExchange.Redis.Tests/ConstraintsTests.cs @@ -5,9 +5,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Constraints : TestBase +public class ConstraintsTests : TestBase { - public Constraints(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public ConstraintsTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public void ValueEquals() diff --git a/tests/StackExchange.Redis.Tests/Copy.cs b/tests/StackExchange.Redis.Tests/CopyTests.cs similarity index 93% rename from tests/StackExchange.Redis.Tests/Copy.cs rename to tests/StackExchange.Redis.Tests/CopyTests.cs index 45b683b35..20a43d1df 100644 --- a/tests/StackExchange.Redis.Tests/Copy.cs +++ b/tests/StackExchange.Redis.Tests/CopyTests.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Copy : TestBase +public class CopyTests : TestBase { - public Copy(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public CopyTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } [Fact] public async Task Basic() diff --git a/tests/StackExchange.Redis.Tests/Databases.cs b/tests/StackExchange.Redis.Tests/DatabaseTests.cs similarity index 97% rename from tests/StackExchange.Redis.Tests/Databases.cs rename to tests/StackExchange.Redis.Tests/DatabaseTests.cs index bacce1cd7..aed1dbf00 100644 --- a/tests/StackExchange.Redis.Tests/Databases.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseTests.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Databases : TestBase +public class DatabaseTests : TestBase { - public Databases(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public DatabaseTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public async Task CommandCount() diff --git a/tests/StackExchange.Redis.Tests/DefaultOptions.cs b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs similarity index 98% rename from tests/StackExchange.Redis.Tests/DefaultOptions.cs rename to tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs index 7a9a97543..4dd4872e7 100644 --- a/tests/StackExchange.Redis.Tests/DefaultOptions.cs +++ b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs @@ -9,9 +9,9 @@ namespace StackExchange.Redis.Tests; -public class DefaultOptions : TestBase +public class DefaultOptionsTests : TestBase { - public DefaultOptions(ITestOutputHelper output) : base(output) { } + public DefaultOptionsTests(ITestOutputHelper output) : base(output) { } public class TestOptionsProvider : DefaultOptionsProvider { diff --git a/tests/StackExchange.Redis.Tests/DefaultPorts.cs b/tests/StackExchange.Redis.Tests/DefaultPortsTests.cs similarity index 98% rename from tests/StackExchange.Redis.Tests/DefaultPorts.cs rename to tests/StackExchange.Redis.Tests/DefaultPortsTests.cs index ef2c2d699..965bc6ef1 100644 --- a/tests/StackExchange.Redis.Tests/DefaultPorts.cs +++ b/tests/StackExchange.Redis.Tests/DefaultPortsTests.cs @@ -4,7 +4,7 @@ namespace StackExchange.Redis.Tests; -public class DefaultPorts +public class DefaultPortsTests { [Theory] [InlineData("foo", 6379)] diff --git a/tests/StackExchange.Redis.Tests/Deprecated.cs b/tests/StackExchange.Redis.Tests/DeprecatedTests.cs similarity index 95% rename from tests/StackExchange.Redis.Tests/Deprecated.cs rename to tests/StackExchange.Redis.Tests/DeprecatedTests.cs index 83efced7f..3e0971d6c 100644 --- a/tests/StackExchange.Redis.Tests/Deprecated.cs +++ b/tests/StackExchange.Redis.Tests/DeprecatedTests.cs @@ -7,9 +7,9 @@ namespace StackExchange.Redis.Tests; /// /// Testing that things we deprecate still parse, but are otherwise defaults. /// -public class Deprecated : TestBase +public class DeprecatedTests : TestBase { - public Deprecated(ITestOutputHelper output) : base(output) { } + public DeprecatedTests(ITestOutputHelper output) : base(output) { } #pragma warning disable CS0618 // Type or member is obsolete [Fact] diff --git a/tests/StackExchange.Redis.Tests/Execute.cs b/tests/StackExchange.Redis.Tests/ExecuteTests.cs similarity index 100% rename from tests/StackExchange.Redis.Tests/Execute.cs rename to tests/StackExchange.Redis.Tests/ExecuteTests.cs diff --git a/tests/StackExchange.Redis.Tests/Expiry.cs b/tests/StackExchange.Redis.Tests/ExpiryTests.cs similarity index 98% rename from tests/StackExchange.Redis.Tests/Expiry.cs rename to tests/StackExchange.Redis.Tests/ExpiryTests.cs index c87fb542b..305bab944 100644 --- a/tests/StackExchange.Redis.Tests/Expiry.cs +++ b/tests/StackExchange.Redis.Tests/ExpiryTests.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Expiry : TestBase +public class ExpiryTests : TestBase { - public Expiry(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public ExpiryTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } private static string[]? GetMap(bool disablePTimes) => disablePTimes ? (new[] { "pexpire", "pexpireat", "pttl" }) : null; diff --git a/tests/StackExchange.Redis.Tests/FSharpCompat.cs b/tests/StackExchange.Redis.Tests/FSharpCompatTests.cs similarity index 83% rename from tests/StackExchange.Redis.Tests/FSharpCompat.cs rename to tests/StackExchange.Redis.Tests/FSharpCompatTests.cs index 3a1366326..7539c1198 100644 --- a/tests/StackExchange.Redis.Tests/FSharpCompat.cs +++ b/tests/StackExchange.Redis.Tests/FSharpCompatTests.cs @@ -3,9 +3,9 @@ namespace StackExchange.Redis.Tests; -public class FSharpCompat : TestBase +public class FSharpCompatTests : TestBase { - public FSharpCompat(ITestOutputHelper output) : base (output) { } + public FSharpCompatTests(ITestOutputHelper output) : base (output) { } [Fact] public void RedisKeyConstructor() diff --git a/tests/StackExchange.Redis.Tests/Failover.cs b/tests/StackExchange.Redis.Tests/FailoverTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/Failover.cs rename to tests/StackExchange.Redis.Tests/FailoverTests.cs index 16faff432..f3955c5e7 100644 --- a/tests/StackExchange.Redis.Tests/Failover.cs +++ b/tests/StackExchange.Redis.Tests/FailoverTests.cs @@ -7,11 +7,11 @@ namespace StackExchange.Redis.Tests; -public class Failover : TestBase, IAsyncLifetime +public class FailoverTests : TestBase, IAsyncLifetime { protected override string GetConfiguration() => GetPrimaryReplicaConfig().ToString(); - public Failover(ITestOutputHelper output) : base(output) { } + public FailoverTests(ITestOutputHelper output) : base(output) { } public Task DisposeAsync() => Task.CompletedTask; diff --git a/tests/StackExchange.Redis.Tests/FeatureFlags.cs b/tests/StackExchange.Redis.Tests/FeatureFlagTests.cs similarity index 96% rename from tests/StackExchange.Redis.Tests/FeatureFlags.cs rename to tests/StackExchange.Redis.Tests/FeatureFlagTests.cs index 966c2df43..bf5aacc13 100644 --- a/tests/StackExchange.Redis.Tests/FeatureFlags.cs +++ b/tests/StackExchange.Redis.Tests/FeatureFlagTests.cs @@ -3,7 +3,7 @@ namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class FeatureFlags +public class FeatureFlagTests { [Fact] public void UnknownFlagToggle() diff --git a/tests/StackExchange.Redis.Tests/FloatingPoint.cs b/tests/StackExchange.Redis.Tests/FloatingPointTests.cs similarity index 96% rename from tests/StackExchange.Redis.Tests/FloatingPoint.cs rename to tests/StackExchange.Redis.Tests/FloatingPointTests.cs index 029a32696..6a7158fe3 100644 --- a/tests/StackExchange.Redis.Tests/FloatingPoint.cs +++ b/tests/StackExchange.Redis.Tests/FloatingPointTests.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class FloatingPoint : TestBase +public class FloatingPointTests : TestBase { - public FloatingPoint(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public FloatingPointTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } private static bool Within(double x, double y, double delta) => Math.Abs(x - y) <= delta; diff --git a/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs b/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs index e72154301..503124c7d 100644 --- a/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs +++ b/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs @@ -6,17 +6,17 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionFailedErrors.SSLCertificateValidationError(System.Boolean)")] -[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.PubSub.ExplicitPublishMode")] -[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSL.ConnectToSSLServer(System.Boolean,System.Boolean)")] -[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSL.ShowCertFailures(StackExchange.Redis.Tests.Helpers.TextWriterOutputHelper)~System.Net.Security.RemoteCertificateValidationCallback")] -[assembly: SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionShutdown.ShutdownRaisesConnectionFailedAndRestore")] -[assembly: SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Issues.BgSaveResponse.ShouldntThrowException(StackExchange.Redis.SaveType)")] -[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Sentinel.PrimaryConnectTest~System.Threading.Tasks.Task")] -[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Sentinel.PrimaryConnectAsyncTest~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionFailedErrorsTests.SSLCertificateValidationError(System.Boolean)")] +[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.PubSubTests.ExplicitPublishMode")] +[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSLTests.ConnectToSSLServer(System.Boolean,System.Boolean)")] +[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSLTests.ShowCertFailures(StackExchange.Redis.Tests.Helpers.TextWriterOutputHelper)~System.Net.Security.RemoteCertificateValidationCallback")] +[assembly: SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionShutdownTests.ShutdownRaisesConnectionFailedAndRestore")] +[assembly: SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Issues.BgSaveResponseTests.ShouldntThrowException(StackExchange.Redis.SaveType)")] +[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelTests.PrimaryConnectTest~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelTests.PrimaryConnectAsyncTest~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelBase.WaitForReplicationAsync(StackExchange.Redis.IServer,System.Nullable{System.TimeSpan})~System.Threading.Tasks.Task")] -[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelFailover.ManagedPrimaryConnectionEndToEndWithFailoverTest~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelFailoverTests.ManagedPrimaryConnectionEndToEndWithFailoverTest~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Performance", "CA1846:Prefer 'AsSpan' over 'Substring'", Justification = "", Scope = "member", Target = "~M:RedisSharp.Redis.ReadData~System.Byte[]")] -[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Naming.IgnoreMethodConventions(System.Reflection.MethodInfo)~System.Boolean")] +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.NamingTests.IgnoreMethodConventions(System.Reflection.MethodInfo)~System.Boolean")] [assembly: SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelBase.WaitForReadyAsync(System.Net.EndPoint,System.Boolean,System.Nullable{System.TimeSpan})~System.Threading.Tasks.Task")] [assembly: SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelBase.WaitForRoleAsync(StackExchange.Redis.IServer,System.String,System.Nullable{System.TimeSpan})~System.Threading.Tasks.Task")] diff --git a/tests/StackExchange.Redis.Tests/Hashes.cs b/tests/StackExchange.Redis.Tests/HashTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/Hashes.cs rename to tests/StackExchange.Redis.Tests/HashTests.cs index dac10d936..3022779c4 100644 --- a/tests/StackExchange.Redis.Tests/Hashes.cs +++ b/tests/StackExchange.Redis.Tests/HashTests.cs @@ -12,9 +12,9 @@ namespace StackExchange.Redis.Tests; /// Tests for . /// [Collection(SharedConnectionFixture.Key)] -public class Hashes : TestBase +public class HashTests : TestBase { - public Hashes(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public HashTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public async Task TestIncrBy() diff --git a/tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs similarity index 100% rename from tests/StackExchange.Redis.Tests/SharedConnectionFixture.cs rename to tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs diff --git a/tests/StackExchange.Redis.Tests/TestExtensions.cs b/tests/StackExchange.Redis.Tests/Helpers/TestExtensions.cs similarity index 100% rename from tests/StackExchange.Redis.Tests/TestExtensions.cs rename to tests/StackExchange.Redis.Tests/Helpers/TestExtensions.cs diff --git a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs index bd6e99b55..50fd6bbc8 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs @@ -9,7 +9,7 @@ // // Copyright 2010 Novell, Inc. // -// Licensed under the same terms of reddis: new BSD license. +// Licensed under the same terms of Redis: new BSD license. // #nullable disable diff --git a/tests/StackExchange.Redis.Tests/HttpTunnelConnect.cs b/tests/StackExchange.Redis.Tests/HttpTunnelConnectTests.cs similarity index 94% rename from tests/StackExchange.Redis.Tests/HttpTunnelConnect.cs rename to tests/StackExchange.Redis.Tests/HttpTunnelConnectTests.cs index e77f8e9d1..2c1dc1ec6 100644 --- a/tests/StackExchange.Redis.Tests/HttpTunnelConnect.cs +++ b/tests/StackExchange.Redis.Tests/HttpTunnelConnectTests.cs @@ -6,10 +6,10 @@ namespace StackExchange.Redis.Tests { - public class HttpTunnelConnect + public class HttpTunnelConnectTests { private ITestOutputHelper Log { get; } - public HttpTunnelConnect(ITestOutputHelper log) => Log = log; + public HttpTunnelConnectTests(ITestOutputHelper log) => Log = log; [Theory] [InlineData("")] diff --git a/tests/StackExchange.Redis.Tests/HyperLogLog.cs b/tests/StackExchange.Redis.Tests/HyperLogLogTests.cs similarity index 83% rename from tests/StackExchange.Redis.Tests/HyperLogLog.cs rename to tests/StackExchange.Redis.Tests/HyperLogLogTests.cs index 27d39feec..d110c86b6 100644 --- a/tests/StackExchange.Redis.Tests/HyperLogLog.cs +++ b/tests/StackExchange.Redis.Tests/HyperLogLogTests.cs @@ -4,9 +4,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class HyperLogLog : TestBase +public class HyperLogLogTests : TestBase { - public HyperLogLog(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public HyperLogLogTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } [Fact] public void SingleKeyLength() diff --git a/tests/StackExchange.Redis.Tests/TestInfoReplicationChecks.cs b/tests/StackExchange.Redis.Tests/InfoReplicationCheckTests.cs similarity index 87% rename from tests/StackExchange.Redis.Tests/TestInfoReplicationChecks.cs rename to tests/StackExchange.Redis.Tests/InfoReplicationCheckTests.cs index 10873e7ef..b0311a7f1 100644 --- a/tests/StackExchange.Redis.Tests/TestInfoReplicationChecks.cs +++ b/tests/StackExchange.Redis.Tests/InfoReplicationCheckTests.cs @@ -4,10 +4,10 @@ namespace StackExchange.Redis.Tests; -public class TestInfoReplicationChecks : TestBase +public class InfoReplicationCheckTests : TestBase { protected override string GetConfiguration() => base.GetConfiguration() + ",configCheckSeconds=2"; - public TestInfoReplicationChecks(ITestOutputHelper output) : base (output) { } + public InfoReplicationCheckTests(ITestOutputHelper output) : base (output) { } [Fact] public async Task Exec() diff --git a/tests/StackExchange.Redis.Tests/Issues/BgSaveResponse.cs b/tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs similarity index 81% rename from tests/StackExchange.Redis.Tests/Issues/BgSaveResponse.cs rename to tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs index b40bf63fe..0c54c40ff 100644 --- a/tests/StackExchange.Redis.Tests/Issues/BgSaveResponse.cs +++ b/tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs @@ -4,9 +4,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class BgSaveResponse : TestBase +public class BgSaveResponseTests : TestBase { - public BgSaveResponse(ITestOutputHelper output) : base (output) { } + public BgSaveResponseTests(ITestOutputHelper output) : base (output) { } [Theory (Skip = "We don't need to test this, and it really screws local testing hard.")] [InlineData(SaveType.BackgroundSave)] diff --git a/tests/StackExchange.Redis.Tests/Issues/DefaultDatabase.cs b/tests/StackExchange.Redis.Tests/Issues/DefaultDatabaseTests.cs similarity index 91% rename from tests/StackExchange.Redis.Tests/Issues/DefaultDatabase.cs rename to tests/StackExchange.Redis.Tests/Issues/DefaultDatabaseTests.cs index 5f9f37aeb..5514bc5c4 100644 --- a/tests/StackExchange.Redis.Tests/Issues/DefaultDatabase.cs +++ b/tests/StackExchange.Redis.Tests/Issues/DefaultDatabaseTests.cs @@ -4,9 +4,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class DefaultDatabase : TestBase +public class DefaultDatabaseTests : TestBase { - public DefaultDatabase(ITestOutputHelper output) : base(output) { } + public DefaultDatabaseTests(ITestOutputHelper output) : base(output) { } [Fact] public void UnspecifiedDbId_ReturnsNull() diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue10.cs b/tests/StackExchange.Redis.Tests/Issues/Issue10Tests.cs similarity index 87% rename from tests/StackExchange.Redis.Tests/Issues/Issue10.cs rename to tests/StackExchange.Redis.Tests/Issues/Issue10Tests.cs index 2a9d693eb..af95cd71e 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue10.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue10Tests.cs @@ -3,9 +3,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class Issue10 : TestBase +public class Issue10Tests : TestBase { - public Issue10(ITestOutputHelper output) : base(output) { } + public Issue10Tests(ITestOutputHelper output) : base(output) { } [Fact] public void Execute() diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs b/tests/StackExchange.Redis.Tests/Issues/Issue1101Tests.cs similarity index 98% rename from tests/StackExchange.Redis.Tests/Issues/Issue1101.cs rename to tests/StackExchange.Redis.Tests/Issues/Issue1101Tests.cs index ebefcd53b..440d18a8e 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue1101.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue1101Tests.cs @@ -8,9 +8,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class Issue1101 : TestBase +public class Issue1101Tests : TestBase { - public Issue1101(ITestOutputHelper output) : base(output) { } + public Issue1101Tests(ITestOutputHelper output) : base(output) { } private static void AssertCounts(ISubscriber pubsub, in RedisChannel channel, bool has, int handlers, int queues) { diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue1103.cs b/tests/StackExchange.Redis.Tests/Issues/Issue1103Tests.cs similarity index 94% rename from tests/StackExchange.Redis.Tests/Issues/Issue1103.cs rename to tests/StackExchange.Redis.Tests/Issues/Issue1103Tests.cs index d712b0dee..4d9ff3731 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue1103.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue1103Tests.cs @@ -5,9 +5,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class Issue1103 : TestBase +public class Issue1103Tests : TestBase { - public Issue1103(ITestOutputHelper output) : base(output) { } + public Issue1103Tests(ITestOutputHelper output) : base(output) { } [Theory] [InlineData(142205255210238005UL, (int)StorageType.Int64)] diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue182.cs b/tests/StackExchange.Redis.Tests/Issues/Issue182Tests.cs similarity index 95% rename from tests/StackExchange.Redis.Tests/Issues/Issue182.cs rename to tests/StackExchange.Redis.Tests/Issues/Issue182Tests.cs index c932cfa12..1f8837d0b 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue182.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue182Tests.cs @@ -6,11 +6,11 @@ namespace StackExchange.Redis.Tests.Issues; -public class Issue182 : TestBase +public class Issue182Tests : TestBase { protected override string GetConfiguration() => $"{TestConfig.Current.PrimaryServerAndPort},responseTimeout=10000"; - public Issue182(ITestOutputHelper output) : base (output) { } + public Issue182Tests(ITestOutputHelper output) : base (output) { } [FactLongRunning] public async Task SetMembers() diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2164.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2164Tests.cs similarity index 98% rename from tests/StackExchange.Redis.Tests/Issues/Issue2164.cs rename to tests/StackExchange.Redis.Tests/Issues/Issue2164Tests.cs index 8ffd4f21a..b52e9f627 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue2164.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2164Tests.cs @@ -1,6 +1,6 @@ namespace StackExchange.Redis.Tests.Issues { - public class Issue2164 + public class Issue2164Tests { [Fact] public void LoadSimpleScript() diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2176.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2176Tests.cs similarity index 95% rename from tests/StackExchange.Redis.Tests/Issues/Issue2176.cs rename to tests/StackExchange.Redis.Tests/Issues/Issue2176Tests.cs index 7f546892d..acfa67bb8 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue2176.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2176Tests.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis.Tests.Issues { - public class Issue2176 : TestBase + public class Issue2176Tests : TestBase { - public Issue2176(ITestOutputHelper output) : base(output) { } + public Issue2176Tests(ITestOutputHelper output) : base(output) { } [Fact] public void Execute_Batch() diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue25.cs b/tests/StackExchange.Redis.Tests/Issues/Issue25Tests.cs similarity index 91% rename from tests/StackExchange.Redis.Tests/Issues/Issue25.cs rename to tests/StackExchange.Redis.Tests/Issues/Issue25Tests.cs index 090a7ae8c..bcba4fa0c 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue25.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue25Tests.cs @@ -4,9 +4,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class Issue25 : TestBase +public class Issue25Tests : TestBase { - public Issue25(ITestOutputHelper output) : base (output) { } + public Issue25Tests(ITestOutputHelper output) : base (output) { } [Fact] public void CaseInsensitive() diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue6.cs b/tests/StackExchange.Redis.Tests/Issues/Issue6Tests.cs similarity index 77% rename from tests/StackExchange.Redis.Tests/Issues/Issue6.cs rename to tests/StackExchange.Redis.Tests/Issues/Issue6Tests.cs index 87299aeb1..8043a317e 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue6.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue6Tests.cs @@ -2,9 +2,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class Issue6 : TestBase +public class Issue6Tests : TestBase { - public Issue6(ITestOutputHelper output) : base (output) { } + public Issue6Tests(ITestOutputHelper output) : base (output) { } [Fact] public void ShouldWorkWithoutEchoOrPing() diff --git a/tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs b/tests/StackExchange.Redis.Tests/Issues/MassiveDeleteTests.cs similarity index 95% rename from tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs rename to tests/StackExchange.Redis.Tests/Issues/MassiveDeleteTests.cs index e742690ca..2655592da 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Massive Delete.cs +++ b/tests/StackExchange.Redis.Tests/Issues/MassiveDeleteTests.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class Massive_Delete : TestBase +public class MassiveDeleteTests : TestBase { - public Massive_Delete(ITestOutputHelper output) : base(output) { } + public MassiveDeleteTests(ITestOutputHelper output) : base(output) { } private void Prep(int dbId, string key) { diff --git a/tests/StackExchange.Redis.Tests/Issues/SO10504853.cs b/tests/StackExchange.Redis.Tests/Issues/SO10504853Tests.cs similarity index 95% rename from tests/StackExchange.Redis.Tests/Issues/SO10504853.cs rename to tests/StackExchange.Redis.Tests/Issues/SO10504853Tests.cs index 9f8e71e4d..abf5cc3cc 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO10504853.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO10504853Tests.cs @@ -5,9 +5,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class SO10504853 : TestBase +public class SO10504853Tests : TestBase { - public SO10504853(ITestOutputHelper output) : base(output) { } + public SO10504853Tests(ITestOutputHelper output) : base(output) { } [Fact] public void LoopLotsOfTrivialStuff() diff --git a/tests/StackExchange.Redis.Tests/Issues/SO10825542.cs b/tests/StackExchange.Redis.Tests/Issues/SO10825542Tests.cs similarity index 87% rename from tests/StackExchange.Redis.Tests/Issues/SO10825542.cs rename to tests/StackExchange.Redis.Tests/Issues/SO10825542Tests.cs index c4c20a210..b19386f6b 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO10825542.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO10825542Tests.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class SO10825542 : TestBase +public class SO10825542Tests : TestBase { - public SO10825542(ITestOutputHelper output) : base(output) { } + public SO10825542Tests(ITestOutputHelper output) : base(output) { } [Fact] public async Task Execute() diff --git a/tests/StackExchange.Redis.Tests/Issues/SO11766033.cs b/tests/StackExchange.Redis.Tests/Issues/SO11766033Tests.cs similarity index 87% rename from tests/StackExchange.Redis.Tests/Issues/SO11766033.cs rename to tests/StackExchange.Redis.Tests/Issues/SO11766033Tests.cs index 4cc7596e5..d350bcff3 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO11766033.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO11766033Tests.cs @@ -3,9 +3,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class SO11766033 : TestBase +public class SO11766033Tests : TestBase { - public SO11766033(ITestOutputHelper output) : base(output) { } + public SO11766033Tests(ITestOutputHelper output) : base(output) { } [Fact] public void TestNullString() diff --git a/tests/StackExchange.Redis.Tests/Issues/SO22786599.cs b/tests/StackExchange.Redis.Tests/Issues/SO22786599Tests.cs similarity index 89% rename from tests/StackExchange.Redis.Tests/Issues/SO22786599.cs rename to tests/StackExchange.Redis.Tests/Issues/SO22786599Tests.cs index 5726f2bcd..b958b52e1 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO22786599.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO22786599Tests.cs @@ -5,9 +5,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class SO22786599 : TestBase +public class SO22786599Tests : TestBase { - public SO22786599(ITestOutputHelper output) : base(output) { } + public SO22786599Tests(ITestOutputHelper output) : base(output) { } [Fact] public void Execute() diff --git a/tests/StackExchange.Redis.Tests/Issues/SO23949477.cs b/tests/StackExchange.Redis.Tests/Issues/SO23949477Tests.cs similarity index 90% rename from tests/StackExchange.Redis.Tests/Issues/SO23949477.cs rename to tests/StackExchange.Redis.Tests/Issues/SO23949477Tests.cs index f8b7baa2a..87d26ee05 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO23949477.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO23949477Tests.cs @@ -3,9 +3,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class SO23949477 : TestBase +public class SO23949477Tests : TestBase { - public SO23949477(ITestOutputHelper output) : base (output) { } + public SO23949477Tests(ITestOutputHelper output) : base (output) { } [Fact] public void Execute() diff --git a/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs b/tests/StackExchange.Redis.Tests/Issues/SO24807536Tests.cs similarity index 92% rename from tests/StackExchange.Redis.Tests/Issues/SO24807536.cs rename to tests/StackExchange.Redis.Tests/Issues/SO24807536Tests.cs index 954bf9fee..9881b8c38 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO24807536.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO24807536Tests.cs @@ -5,9 +5,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class SO24807536 : TestBase +public class SO24807536Tests : TestBase { - public SO24807536(ITestOutputHelper output) : base (output) { } + public SO24807536Tests(ITestOutputHelper output) : base (output) { } [Fact] public async Task Exec() diff --git a/tests/StackExchange.Redis.Tests/Issues/SO25113323.cs b/tests/StackExchange.Redis.Tests/Issues/SO25113323Tests.cs similarity index 87% rename from tests/StackExchange.Redis.Tests/Issues/SO25113323.cs rename to tests/StackExchange.Redis.Tests/Issues/SO25113323Tests.cs index b38743848..b10be1aea 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO25113323.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO25113323Tests.cs @@ -1,13 +1,12 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues; -public class SO25113323 : TestBase +public class SO25113323Tests : TestBase { - public SO25113323(ITestOutputHelper output) : base (output) { } + public SO25113323Tests(ITestOutputHelper output) : base (output) { } [Fact] public async Task SetExpirationToPassed() diff --git a/tests/StackExchange.Redis.Tests/Issues/SO25567566.cs b/tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs similarity index 94% rename from tests/StackExchange.Redis.Tests/Issues/SO25567566.cs rename to tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs index 86be32862..18163b23c 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO25567566.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs @@ -5,10 +5,10 @@ namespace StackExchange.Redis.Tests.Issues; -public class SO25567566 : TestBase +public class SO25567566Tests : TestBase { protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public SO25567566(ITestOutputHelper output) : base(output) { } + public SO25567566Tests(ITestOutputHelper output) : base(output) { } [FactLongRunning] public async Task Execute() diff --git a/tests/StackExchange.Redis.Tests/KeysAndValues.cs b/tests/StackExchange.Redis.Tests/KeyAndValueTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/KeysAndValues.cs rename to tests/StackExchange.Redis.Tests/KeyAndValueTests.cs index ad8cc7ae7..38f7f062e 100644 --- a/tests/StackExchange.Redis.Tests/KeysAndValues.cs +++ b/tests/StackExchange.Redis.Tests/KeyAndValueTests.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis.Tests; -public class KeysAndValues +public class KeyAndValueTests { [Fact] public void TestValues() diff --git a/tests/StackExchange.Redis.Tests/Keys.cs b/tests/StackExchange.Redis.Tests/KeyTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/Keys.cs rename to tests/StackExchange.Redis.Tests/KeyTests.cs index e6b4c6395..d0846872d 100644 --- a/tests/StackExchange.Redis.Tests/Keys.cs +++ b/tests/StackExchange.Redis.Tests/KeyTests.cs @@ -10,9 +10,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Keys : TestBase +public class KeyTests : TestBase { - public Keys(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public KeyTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } [Fact] public void TestScan() diff --git a/tests/StackExchange.Redis.Tests/Latency.cs b/tests/StackExchange.Redis.Tests/LatencyTests.cs similarity index 94% rename from tests/StackExchange.Redis.Tests/Latency.cs rename to tests/StackExchange.Redis.Tests/LatencyTests.cs index 1d49300c4..c82c947b7 100644 --- a/tests/StackExchange.Redis.Tests/Latency.cs +++ b/tests/StackExchange.Redis.Tests/LatencyTests.cs @@ -5,9 +5,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Latency : TestBase +public class LatencyTests : TestBase { - public Latency(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public LatencyTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public async Task CanCallDoctor() diff --git a/tests/StackExchange.Redis.Tests/Lex.cs b/tests/StackExchange.Redis.Tests/LexTests.cs similarity index 96% rename from tests/StackExchange.Redis.Tests/Lex.cs rename to tests/StackExchange.Redis.Tests/LexTests.cs index 3c5c32250..ace821ca6 100644 --- a/tests/StackExchange.Redis.Tests/Lex.cs +++ b/tests/StackExchange.Redis.Tests/LexTests.cs @@ -4,9 +4,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Lex : TestBase +public class LexTests : TestBase { - public Lex(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public LexTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public void QueryRangeAndLengthByLex() diff --git a/tests/StackExchange.Redis.Tests/Lists.cs b/tests/StackExchange.Redis.Tests/ListTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/Lists.cs rename to tests/StackExchange.Redis.Tests/ListTests.cs index 119df434e..bb212db14 100644 --- a/tests/StackExchange.Redis.Tests/Lists.cs +++ b/tests/StackExchange.Redis.Tests/ListTests.cs @@ -7,9 +7,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Lists : TestBase +public class ListTests : TestBase { - public Lists(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public ListTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public void Ranges() diff --git a/tests/StackExchange.Redis.Tests/Locking.cs b/tests/StackExchange.Redis.Tests/LockingTests.cs similarity index 98% rename from tests/StackExchange.Redis.Tests/Locking.cs rename to tests/StackExchange.Redis.Tests/LockingTests.cs index 08dc7db48..989c5826e 100644 --- a/tests/StackExchange.Redis.Tests/Locking.cs +++ b/tests/StackExchange.Redis.Tests/LockingTests.cs @@ -8,10 +8,10 @@ namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class Locking : TestBase +public class LockingTests : TestBase { protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public Locking(ITestOutputHelper output) : base (output) { } + public LockingTests(ITestOutputHelper output) : base (output) { } public enum TestMode { diff --git a/tests/StackExchange.Redis.Tests/MassiveOps.cs b/tests/StackExchange.Redis.Tests/MassiveOpsTests.cs similarity index 96% rename from tests/StackExchange.Redis.Tests/MassiveOps.cs rename to tests/StackExchange.Redis.Tests/MassiveOpsTests.cs index 000f22fe2..26816a8b1 100644 --- a/tests/StackExchange.Redis.Tests/MassiveOps.cs +++ b/tests/StackExchange.Redis.Tests/MassiveOpsTests.cs @@ -7,9 +7,9 @@ namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class MassiveOps : TestBase +public class MassiveOpsTests : TestBase { - public MassiveOps(ITestOutputHelper output) : base(output) { } + public MassiveOpsTests(ITestOutputHelper output) : base(output) { } [FactLongRunning] public async Task LongRunning() diff --git a/tests/StackExchange.Redis.Tests/Memory.cs b/tests/StackExchange.Redis.Tests/MemoryTests.cs similarity index 93% rename from tests/StackExchange.Redis.Tests/Memory.cs rename to tests/StackExchange.Redis.Tests/MemoryTests.cs index b8fb7ea02..21325e0f2 100644 --- a/tests/StackExchange.Redis.Tests/Memory.cs +++ b/tests/StackExchange.Redis.Tests/MemoryTests.cs @@ -5,9 +5,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Memory : TestBase +public class MemoryTests : TestBase { - public Memory(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public MemoryTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public async Task CanCallDoctor() diff --git a/tests/StackExchange.Redis.Tests/Migrate.cs b/tests/StackExchange.Redis.Tests/MigrateTests.cs similarity index 95% rename from tests/StackExchange.Redis.Tests/Migrate.cs rename to tests/StackExchange.Redis.Tests/MigrateTests.cs index 9a497b4f5..1fad9adf3 100644 --- a/tests/StackExchange.Redis.Tests/Migrate.cs +++ b/tests/StackExchange.Redis.Tests/MigrateTests.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis.Tests; -public class Migrate : TestBase +public class MigrateTests : TestBase { - public Migrate(ITestOutputHelper output) : base (output) { } + public MigrateTests(ITestOutputHelper output) : base (output) { } [FactLongRunning] public async Task Basic() diff --git a/tests/StackExchange.Redis.Tests/MultiAdd.cs b/tests/StackExchange.Redis.Tests/MultiAddTests.cs similarity index 96% rename from tests/StackExchange.Redis.Tests/MultiAdd.cs rename to tests/StackExchange.Redis.Tests/MultiAddTests.cs index 5f8dee77e..d542b80b6 100644 --- a/tests/StackExchange.Redis.Tests/MultiAdd.cs +++ b/tests/StackExchange.Redis.Tests/MultiAddTests.cs @@ -5,9 +5,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class MultiAdd : TestBase +public class MultiAddTests : TestBase { - public MultiAdd(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public MultiAddTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public void AddSortedSetEveryWay() diff --git a/tests/StackExchange.Redis.Tests/MultiPrimary.cs b/tests/StackExchange.Redis.Tests/MultiPrimaryTests.cs similarity index 97% rename from tests/StackExchange.Redis.Tests/MultiPrimary.cs rename to tests/StackExchange.Redis.Tests/MultiPrimaryTests.cs index 240213798..52b6c6297 100644 --- a/tests/StackExchange.Redis.Tests/MultiPrimary.cs +++ b/tests/StackExchange.Redis.Tests/MultiPrimaryTests.cs @@ -6,11 +6,11 @@ namespace StackExchange.Redis.Tests; -public class MultiPrimary : TestBase +public class MultiPrimaryTests : TestBase { protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword; - public MultiPrimary(ITestOutputHelper output) : base (output) { } + public MultiPrimaryTests(ITestOutputHelper output) : base (output) { } [Fact] public void CannotFlushReplica() diff --git a/tests/StackExchange.Redis.Tests/Naming.cs b/tests/StackExchange.Redis.Tests/NamingTests.cs similarity index 98% rename from tests/StackExchange.Redis.Tests/Naming.cs rename to tests/StackExchange.Redis.Tests/NamingTests.cs index 7540eb8a6..3f34ada64 100644 --- a/tests/StackExchange.Redis.Tests/Naming.cs +++ b/tests/StackExchange.Redis.Tests/NamingTests.cs @@ -9,9 +9,9 @@ namespace StackExchange.Redis.Tests; -public class Naming : TestBase +public class NamingTests : TestBase { - public Naming(ITestOutputHelper output) : base(output) { } + public NamingTests(ITestOutputHelper output) : base(output) { } [Theory] [InlineData(typeof(IDatabase), false)] diff --git a/tests/StackExchange.Redis.Tests/OverloadCompat.cs b/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs similarity index 98% rename from tests/StackExchange.Redis.Tests/OverloadCompat.cs rename to tests/StackExchange.Redis.Tests/OverloadCompatTests.cs index f07b48933..91abbf5e9 100644 --- a/tests/StackExchange.Redis.Tests/OverloadCompat.cs +++ b/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs @@ -10,9 +10,9 @@ namespace StackExchange.Redis.Tests; /// past versions work correctly and aren't source breaking. /// [Collection(SharedConnectionFixture.Key)] -public class OverloadCompat : TestBase +public class OverloadCompatTests : TestBase { - public OverloadCompat(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public OverloadCompatTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } [Fact] public async Task KeyExpire() diff --git a/tests/StackExchange.Redis.Tests/Parse.cs b/tests/StackExchange.Redis.Tests/ParseTests.cs similarity index 100% rename from tests/StackExchange.Redis.Tests/Parse.cs rename to tests/StackExchange.Redis.Tests/ParseTests.cs diff --git a/tests/StackExchange.Redis.Tests/Performance.cs b/tests/StackExchange.Redis.Tests/PerformanceTests.cs similarity index 97% rename from tests/StackExchange.Redis.Tests/Performance.cs rename to tests/StackExchange.Redis.Tests/PerformanceTests.cs index 34807aab3..068ff2870 100644 --- a/tests/StackExchange.Redis.Tests/Performance.cs +++ b/tests/StackExchange.Redis.Tests/PerformanceTests.cs @@ -7,9 +7,9 @@ namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class Performance : TestBase +public class PerformanceTests : TestBase { - public Performance(ITestOutputHelper output) : base(output) { } + public PerformanceTests(ITestOutputHelper output) : base(output) { } [FactLongRunning] public void VerifyPerformanceImprovement() diff --git a/tests/StackExchange.Redis.Tests/PreserveOrder.cs b/tests/StackExchange.Redis.Tests/PreserveOrderTests.cs similarity index 92% rename from tests/StackExchange.Redis.Tests/PreserveOrder.cs rename to tests/StackExchange.Redis.Tests/PreserveOrderTests.cs index f794a3650..b0f7df995 100644 --- a/tests/StackExchange.Redis.Tests/PreserveOrder.cs +++ b/tests/StackExchange.Redis.Tests/PreserveOrderTests.cs @@ -8,9 +8,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class PreserveOrder : TestBase +public class PreserveOrderTests : TestBase { - public PreserveOrder(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public PreserveOrderTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } [Fact] public void Execute() diff --git a/tests/StackExchange.Redis.Tests/Profiling.cs b/tests/StackExchange.Redis.Tests/ProfilingTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/Profiling.cs rename to tests/StackExchange.Redis.Tests/ProfilingTests.cs index 2ff0cfb26..41aef9337 100644 --- a/tests/StackExchange.Redis.Tests/Profiling.cs +++ b/tests/StackExchange.Redis.Tests/ProfilingTests.cs @@ -11,9 +11,9 @@ namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class Profiling : TestBase +public class ProfilingTests : TestBase { - public Profiling(ITestOutputHelper output) : base(output) { } + public ProfilingTests(ITestOutputHelper output) : base(output) { } [Fact] public void Simple() diff --git a/tests/StackExchange.Redis.Tests/PubSubCommand.cs b/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs similarity index 94% rename from tests/StackExchange.Redis.Tests/PubSubCommand.cs rename to tests/StackExchange.Redis.Tests/PubSubCommandTests.cs index 12ab1f17e..809a9968e 100644 --- a/tests/StackExchange.Redis.Tests/PubSubCommand.cs +++ b/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs @@ -8,9 +8,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class PubSubCommand : TestBase +public class PubSubCommandTests : TestBase { - public PubSubCommand(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public PubSubCommandTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } [Fact] public void SubscriberCount() diff --git a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs b/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs similarity index 97% rename from tests/StackExchange.Redis.Tests/PubSubMultiserver.cs rename to tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs index d58982ec1..d3e634a47 100644 --- a/tests/StackExchange.Redis.Tests/PubSubMultiserver.cs +++ b/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs @@ -7,9 +7,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class PubSubMultiserver : TestBase +public class PubSubMultiserverTests : TestBase { - public PubSubMultiserver(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public PubSubMultiserverTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; [Fact] diff --git a/tests/StackExchange.Redis.Tests/PubSub.cs b/tests/StackExchange.Redis.Tests/PubSubTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/PubSub.cs rename to tests/StackExchange.Redis.Tests/PubSubTests.cs index 0cb9efcad..91df2aa99 100644 --- a/tests/StackExchange.Redis.Tests/PubSub.cs +++ b/tests/StackExchange.Redis.Tests/PubSubTests.cs @@ -13,9 +13,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class PubSub : TestBase +public class PubSubTests : TestBase { - public PubSub(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public PubSubTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public async Task ExplicitPublishMode() diff --git a/tests/StackExchange.Redis.Tests/RealWorld.cs b/tests/StackExchange.Redis.Tests/RealWorldTests.cs similarity index 90% rename from tests/StackExchange.Redis.Tests/RealWorld.cs rename to tests/StackExchange.Redis.Tests/RealWorldTests.cs index 246a64462..340303f22 100644 --- a/tests/StackExchange.Redis.Tests/RealWorld.cs +++ b/tests/StackExchange.Redis.Tests/RealWorldTests.cs @@ -4,9 +4,9 @@ namespace StackExchange.Redis.Tests; -public class RealWorld : TestBase +public class RealWorldTests : TestBase { - public RealWorld(ITestOutputHelper output) : base(output) { } + public RealWorldTests(ITestOutputHelper output) : base(output) { } [Fact] public async Task WhyDoesThisNotWork() diff --git a/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs b/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs similarity index 98% rename from tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs rename to tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs index 195c971ad..6aec46cf8 100644 --- a/tests/StackExchange.Redis.Tests/RedisValueEquivalency.cs +++ b/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs @@ -14,7 +14,7 @@ public void Int32_Matrix() { static void Check(RedisValue known, RedisValue test) { - KeysAndValues.CheckSame(known, test); + KeyAndValueTests.CheckSame(known, test); if (known.IsNull) { Assert.True(test.IsNull); @@ -53,7 +53,7 @@ public void Int64_Matrix() { static void Check(RedisValue known, RedisValue test) { - KeysAndValues.CheckSame(known, test); + KeyAndValueTests.CheckSame(known, test); if (known.IsNull) { Assert.True(test.IsNull); @@ -92,7 +92,7 @@ public void Double_Matrix() { static void Check(RedisValue known, RedisValue test) { - KeysAndValues.CheckSame(known, test); + KeyAndValueTests.CheckSame(known, test); if (known.IsNull) { Assert.True(test.IsNull); diff --git a/tests/StackExchange.Redis.Tests/Roles.cs b/tests/StackExchange.Redis.Tests/RoleTests.cs similarity index 100% rename from tests/StackExchange.Redis.Tests/Roles.cs rename to tests/StackExchange.Redis.Tests/RoleTests.cs diff --git a/tests/StackExchange.Redis.Tests/SSDB.cs b/tests/StackExchange.Redis.Tests/SSDBTests.cs similarity index 88% rename from tests/StackExchange.Redis.Tests/SSDB.cs rename to tests/StackExchange.Redis.Tests/SSDBTests.cs index 982d61244..6f3348892 100644 --- a/tests/StackExchange.Redis.Tests/SSDB.cs +++ b/tests/StackExchange.Redis.Tests/SSDBTests.cs @@ -3,9 +3,9 @@ namespace StackExchange.Redis.Tests; -public class SSDB : TestBase +public class SSDBTests : TestBase { - public SSDB(ITestOutputHelper output) : base (output) { } + public SSDBTests(ITestOutputHelper output) : base (output) { } [Fact] public void ConnectToSSDB() diff --git a/tests/StackExchange.Redis.Tests/SSL.cs b/tests/StackExchange.Redis.Tests/SSLTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/SSL.cs rename to tests/StackExchange.Redis.Tests/SSLTests.cs index 13863a605..105fc031b 100644 --- a/tests/StackExchange.Redis.Tests/SSL.cs +++ b/tests/StackExchange.Redis.Tests/SSLTests.cs @@ -17,11 +17,11 @@ namespace StackExchange.Redis.Tests; -public class SSL : TestBase, IClassFixture +public class SSLTests : TestBase, IClassFixture { private SSLServerFixture Fixture { get; } - public SSL(ITestOutputHelper output, SSLServerFixture fixture) : base(output) => Fixture = fixture; + public SSLTests(ITestOutputHelper output, SSLServerFixture fixture) : base(output) => Fixture = fixture; [Theory] [InlineData(null, true)] // auto-infer port (but specify 6380) diff --git a/tests/StackExchange.Redis.Tests/SanityChecks.cs b/tests/StackExchange.Redis.Tests/SanityCheckTests.cs similarity index 100% rename from tests/StackExchange.Redis.Tests/SanityChecks.cs rename to tests/StackExchange.Redis.Tests/SanityCheckTests.cs diff --git a/tests/StackExchange.Redis.Tests/Scans.cs b/tests/StackExchange.Redis.Tests/ScanTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/Scans.cs rename to tests/StackExchange.Redis.Tests/ScanTests.cs index b8b98f22c..b90d37592 100644 --- a/tests/StackExchange.Redis.Tests/Scans.cs +++ b/tests/StackExchange.Redis.Tests/ScanTests.cs @@ -8,9 +8,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Scans : TestBase +public class ScanTests : TestBase { - public Scans(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public ScanTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } [Theory] [InlineData(true)] diff --git a/tests/StackExchange.Redis.Tests/Scripting.cs b/tests/StackExchange.Redis.Tests/ScriptingTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/Scripting.cs rename to tests/StackExchange.Redis.Tests/ScriptingTests.cs index 0f8c91eec..f3495767d 100644 --- a/tests/StackExchange.Redis.Tests/Scripting.cs +++ b/tests/StackExchange.Redis.Tests/ScriptingTests.cs @@ -10,9 +10,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Scripting : TestBase +public class ScriptingTests : TestBase { - public Scripting(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public ScriptingTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } private IConnectionMultiplexer GetScriptConn(bool allowAdmin = false) { diff --git a/tests/StackExchange.Redis.Tests/Secure.cs b/tests/StackExchange.Redis.Tests/SecureTests.cs similarity index 96% rename from tests/StackExchange.Redis.Tests/Secure.cs rename to tests/StackExchange.Redis.Tests/SecureTests.cs index 615bf143e..1c6989daf 100644 --- a/tests/StackExchange.Redis.Tests/Secure.cs +++ b/tests/StackExchange.Redis.Tests/SecureTests.cs @@ -6,12 +6,12 @@ namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class Secure : TestBase +public class SecureTests : TestBase { protected override string GetConfiguration() => TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword + ",name=MyClient"; - public Secure(ITestOutputHelper output) : base (output) { } + public SecureTests(ITestOutputHelper output) : base (output) { } [Fact] public void MassiveBulkOpsFireAndForgetSecure() diff --git a/tests/StackExchange.Redis.Tests/SentinelFailover.cs b/tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs similarity index 97% rename from tests/StackExchange.Redis.Tests/SentinelFailover.cs rename to tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs index 25b3bf6fd..0afdf03ec 100644 --- a/tests/StackExchange.Redis.Tests/SentinelFailover.cs +++ b/tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs @@ -8,9 +8,9 @@ namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class SentinelFailover : SentinelBase +public class SentinelFailoverTests : SentinelBase { - public SentinelFailover(ITestOutputHelper output) : base(output) { } + public SentinelFailoverTests(ITestOutputHelper output) : base(output) { } [Fact] public async Task ManagedPrimaryConnectionEndToEndWithFailoverTest() diff --git a/tests/StackExchange.Redis.Tests/Sentinel.cs b/tests/StackExchange.Redis.Tests/SentinelTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/Sentinel.cs rename to tests/StackExchange.Redis.Tests/SentinelTests.cs index df8768a89..9deea9259 100644 --- a/tests/StackExchange.Redis.Tests/Sentinel.cs +++ b/tests/StackExchange.Redis.Tests/SentinelTests.cs @@ -8,9 +8,9 @@ namespace StackExchange.Redis.Tests; -public class Sentinel : SentinelBase +public class SentinelTests : SentinelBase { - public Sentinel(ITestOutputHelper output) : base(output) { } + public SentinelTests(ITestOutputHelper output) : base(output) { } [Fact] public async Task PrimaryConnectTest() diff --git a/tests/StackExchange.Redis.Tests/Sets.cs b/tests/StackExchange.Redis.Tests/SetTests.cs similarity index 98% rename from tests/StackExchange.Redis.Tests/Sets.cs rename to tests/StackExchange.Redis.Tests/SetTests.cs index 436691eff..ea7043cf8 100644 --- a/tests/StackExchange.Redis.Tests/Sets.cs +++ b/tests/StackExchange.Redis.Tests/SetTests.cs @@ -7,9 +7,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Sets : TestBase +public class SetTests : TestBase { - public Sets(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public SetTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } [Fact] public void SetContains() diff --git a/tests/StackExchange.Redis.Tests/Sockets.cs b/tests/StackExchange.Redis.Tests/SocketTests.cs similarity index 87% rename from tests/StackExchange.Redis.Tests/Sockets.cs rename to tests/StackExchange.Redis.Tests/SocketTests.cs index a4d415237..10723d7d0 100644 --- a/tests/StackExchange.Redis.Tests/Sockets.cs +++ b/tests/StackExchange.Redis.Tests/SocketTests.cs @@ -3,10 +3,10 @@ namespace StackExchange.Redis.Tests; -public class Sockets : TestBase +public class SocketTests : TestBase { protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public Sockets(ITestOutputHelper output) : base (output) { } + public SocketTests(ITestOutputHelper output) : base (output) { } [FactLongRunning] public void CheckForSocketLeaks() diff --git a/tests/StackExchange.Redis.Tests/SortedSets.cs b/tests/StackExchange.Redis.Tests/SortedSetTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/SortedSets.cs rename to tests/StackExchange.Redis.Tests/SortedSetTests.cs index 6d10f7bf3..3b99478ce 100644 --- a/tests/StackExchange.Redis.Tests/SortedSets.cs +++ b/tests/StackExchange.Redis.Tests/SortedSetTests.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class SortedSets : TestBase +public class SortedSetTests : TestBase { - public SortedSets(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public SortedSetTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } private static readonly SortedSetEntry[] entries = new SortedSetEntry[] { diff --git a/tests/StackExchange.Redis.Tests/SortedSetWhen.cs b/tests/StackExchange.Redis.Tests/SortedSetWhenTests.cs similarity index 100% rename from tests/StackExchange.Redis.Tests/SortedSetWhen.cs rename to tests/StackExchange.Redis.Tests/SortedSetWhenTests.cs diff --git a/tests/StackExchange.Redis.Tests/Streams.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/Streams.cs rename to tests/StackExchange.Redis.Tests/StreamTests.cs index 3ae1a19ae..36856de38 100644 --- a/tests/StackExchange.Redis.Tests/Streams.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -8,9 +8,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Streams : TestBase +public class StreamTests : TestBase { - public Streams(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public StreamTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public void IsStreamType() diff --git a/tests/StackExchange.Redis.Tests/Strings.cs b/tests/StackExchange.Redis.Tests/StringTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/Strings.cs rename to tests/StackExchange.Redis.Tests/StringTests.cs index 034036703..b82ce1d9c 100644 --- a/tests/StackExchange.Redis.Tests/Strings.cs +++ b/tests/StackExchange.Redis.Tests/StringTests.cs @@ -12,9 +12,9 @@ namespace StackExchange.Redis.Tests; /// Tests for . /// [Collection(SharedConnectionFixture.Key)] -public class Strings : TestBase +public class StringTests : TestBase { - public Strings(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public StringTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public async Task Append() diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 39ec34229..98ea41550 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Net; using System.Runtime; using System.Runtime.CompilerServices; using System.Threading; diff --git a/tests/StackExchange.Redis.Tests/Transactions.cs b/tests/StackExchange.Redis.Tests/TransactionTests.cs similarity index 99% rename from tests/StackExchange.Redis.Tests/Transactions.cs rename to tests/StackExchange.Redis.Tests/TransactionTests.cs index 1b1279136..d151e5985 100644 --- a/tests/StackExchange.Redis.Tests/Transactions.cs +++ b/tests/StackExchange.Redis.Tests/TransactionTests.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] -public class Transactions : TestBase +public class TransactionTests : TestBase { - public Transactions(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public TransactionTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public void BasicEmptyTran() diff --git a/tests/StackExchange.Redis.Tests/Values.cs b/tests/StackExchange.Redis.Tests/ValueTests.cs similarity index 92% rename from tests/StackExchange.Redis.Tests/Values.cs rename to tests/StackExchange.Redis.Tests/ValueTests.cs index 0d0b18c35..f4d13617a 100644 --- a/tests/StackExchange.Redis.Tests/Values.cs +++ b/tests/StackExchange.Redis.Tests/ValueTests.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis.Tests; -public class Values : TestBase +public class ValueTests : TestBase { - public Values(ITestOutputHelper output) : base (output) { } + public ValueTests(ITestOutputHelper output) : base (output) { } [Fact] public void NullValueChecks() From ae28ae6cc9438752ecd27057c54f20a22e925c85 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 26 Oct 2022 09:05:57 -0400 Subject: [PATCH 195/435] Internals Rename: *Wrapper -> "KeyPrefixed" (#2282) This renames the various wrappers to IMO more intuitive "KeyPrefixedX" - just couldn't get used to the previous model when touching N files for an API addition, hoping this makes it a little clearer overall, also has the benefit of batching them in UI due to common prefix. --- .../KeyspaceIsolation/BatchWrapper.cs | 9 - .../KeyspaceIsolation/DatabaseExtension.cs | 8 +- .../{WrapperBase.cs => KeyPrefixed.cs} | 4 +- .../KeyspaceIsolation/KeyPrefixedBatch.cs | 9 + ...abaseWrapper.cs => KeyPrefixedDatabase.cs} | 8 +- ...onWrapper.cs => KeyPrefixedTransaction.cs} | 4 +- .../StackExchange.Redis.Tests/ConfigTests.cs | 16 +- ...apperTests.cs => KeyPrefixedBatchTests.cs} | 10 +- ...erTests.cs => KeyPrefixedDatabaseTests.cs} | 382 +++++++++--------- ...rapperBaseTests.cs => KeyPrefixedTests.cs} | 363 +++++++++-------- ...ests.cs => KeyPrefixedTransactionTests.cs} | 40 +- .../StackExchange.Redis.Tests/LockingTests.cs | 16 +- 12 files changed, 437 insertions(+), 432 deletions(-) delete mode 100644 src/StackExchange.Redis/KeyspaceIsolation/BatchWrapper.cs rename src/StackExchange.Redis/KeyspaceIsolation/{WrapperBase.cs => KeyPrefixed.cs} (99%) create mode 100644 src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedBatch.cs rename src/StackExchange.Redis/KeyspaceIsolation/{DatabaseWrapper.cs => KeyPrefixedDatabase.cs} (99%) rename src/StackExchange.Redis/KeyspaceIsolation/{TransactionWrapper.cs => KeyPrefixedTransaction.cs} (71%) rename tests/StackExchange.Redis.Tests/{BatchWrapperTests.cs => KeyPrefixedBatchTests.cs} (60%) rename tests/StackExchange.Redis.Tests/{DatabaseWrapperTests.cs => KeyPrefixedDatabaseTests.cs} (70%) rename tests/StackExchange.Redis.Tests/{WrapperBaseTests.cs => KeyPrefixedTests.cs} (73%) rename tests/StackExchange.Redis.Tests/{TransactionWrapperTests.cs => KeyPrefixedTransactionTests.cs} (70%) diff --git a/src/StackExchange.Redis/KeyspaceIsolation/BatchWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/BatchWrapper.cs deleted file mode 100644 index a8fb90162..000000000 --- a/src/StackExchange.Redis/KeyspaceIsolation/BatchWrapper.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace StackExchange.Redis.KeyspaceIsolation -{ - internal sealed class BatchWrapper : WrapperBase, IBatch - { - public BatchWrapper(IBatch inner, byte[] prefix) : base(inner, prefix) { } - - public void Execute() => Inner.Execute(); - } -} diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs index 7a327b2a8..742bc06eb 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/DatabaseExtension.cs @@ -54,14 +54,14 @@ public static IDatabase WithKeyPrefix(this IDatabase database, RedisKey keyPrefi return database; // fine - you can keep using the original, then } - if (database is DatabaseWrapper wrapper) + if (database is KeyPrefixedDatabase prefixed) { // combine the key in advance to minimize indirection - keyPrefix = wrapper.ToInner(keyPrefix); - database = wrapper.Inner; + keyPrefix = prefixed.ToInner(keyPrefix); + database = prefixed.Inner; } - return new DatabaseWrapper(database, keyPrefix.AsPrefix()!); + return new KeyPrefixedDatabase(database, keyPrefix.AsPrefix()!); } } } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs similarity index 99% rename from src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs rename to src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 702939f51..49285588e 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -7,9 +7,9 @@ namespace StackExchange.Redis.KeyspaceIsolation { - internal class WrapperBase : IDatabaseAsync where TInner : IDatabaseAsync + internal class KeyPrefixed : IDatabaseAsync where TInner : IDatabaseAsync { - internal WrapperBase(TInner inner, byte[] keyPrefix) + internal KeyPrefixed(TInner inner, byte[] keyPrefix) { Inner = inner; Prefix = keyPrefix; diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedBatch.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedBatch.cs new file mode 100644 index 000000000..6f5679a66 --- /dev/null +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedBatch.cs @@ -0,0 +1,9 @@ +namespace StackExchange.Redis.KeyspaceIsolation +{ + internal sealed class KeyPrefixedBatch : KeyPrefixed, IBatch + { + public KeyPrefixedBatch(IBatch inner, byte[] prefix) : base(inner, prefix) { } + + public void Execute() => Inner.Execute(); + } +} diff --git a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs similarity index 99% rename from src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs rename to src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index d04ca60f6..c8c3e54f3 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -4,17 +4,17 @@ namespace StackExchange.Redis.KeyspaceIsolation { - internal sealed class DatabaseWrapper : WrapperBase, IDatabase + internal sealed class KeyPrefixedDatabase : KeyPrefixed, IDatabase { - public DatabaseWrapper(IDatabase inner, byte[] prefix) : base(inner, prefix) + public KeyPrefixedDatabase(IDatabase inner, byte[] prefix) : base(inner, prefix) { } public IBatch CreateBatch(object? asyncState = null) => - new BatchWrapper(Inner.CreateBatch(asyncState), Prefix); + new KeyPrefixedBatch(Inner.CreateBatch(asyncState), Prefix); public ITransaction CreateTransaction(object? asyncState = null) => - new TransactionWrapper(Inner.CreateTransaction(asyncState), Prefix); + new KeyPrefixedTransaction(Inner.CreateTransaction(asyncState), Prefix); public int Database => Inner.Database; diff --git a/src/StackExchange.Redis/KeyspaceIsolation/TransactionWrapper.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedTransaction.cs similarity index 71% rename from src/StackExchange.Redis/KeyspaceIsolation/TransactionWrapper.cs rename to src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedTransaction.cs index 8e1ab1b7b..89703ba6a 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/TransactionWrapper.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedTransaction.cs @@ -2,9 +2,9 @@ namespace StackExchange.Redis.KeyspaceIsolation { - internal sealed class TransactionWrapper : WrapperBase, ITransaction + internal sealed class KeyPrefixedTransaction : KeyPrefixed, ITransaction { - public TransactionWrapper(ITransaction inner, byte[] prefix) : base(inner, prefix) { } + public KeyPrefixedTransaction(ITransaction inner, byte[] prefix) : base(inner, prefix) { } public ConditionResult AddCondition(Condition condition) => Inner.AddCondition(condition.MapKeys(GetMapFunction())); diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 2f74496ef..db13d822f 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -187,19 +187,25 @@ public void TalkToNonsenseServer() [Fact] public async Task TestManaulHeartbeat() { - using var conn = Create(keepAlive: 2); + var options = ConfigurationOptions.Parse(GetConfiguration()); + options.HeartbeatInterval = TimeSpan.FromMilliseconds(100); + using var conn = await ConnectionMultiplexer.ConnectAsync(options); + + foreach (var ep in conn.GetServerSnapshot().ToArray()) + { + ep.WriteEverySeconds = 1; + } var db = conn.GetDatabase(); db.Ping(); var before = conn.OperationCount; - Log("sleeping to test heartbeat..."); - await Task.Delay(5000).ForAwait(); - + Log("Sleeping to test heartbeat..."); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => conn.OperationCount > before + 1).ForAwait(); var after = conn.OperationCount; - Assert.True(after >= before + 2, $"after: {after}, before: {before}"); + Assert.True(after >= before + 1, $"after: {after}, before: {before}"); } [Theory] diff --git a/tests/StackExchange.Redis.Tests/BatchWrapperTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedBatchTests.cs similarity index 60% rename from tests/StackExchange.Redis.Tests/BatchWrapperTests.cs rename to tests/StackExchange.Redis.Tests/KeyPrefixedBatchTests.cs index e06d478a5..4d0700eb0 100644 --- a/tests/StackExchange.Redis.Tests/BatchWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedBatchTests.cs @@ -6,21 +6,21 @@ namespace StackExchange.Redis.Tests; [Collection(nameof(MoqDependentCollection))] -public sealed class BatchWrapperTests +public sealed class KeyPrefixedBatchTests { private readonly Mock mock; - private readonly BatchWrapper wrapper; + private readonly KeyPrefixedBatch prefixed; - public BatchWrapperTests() + public KeyPrefixedBatchTests() { mock = new Mock(); - wrapper = new BatchWrapper(mock.Object, Encoding.UTF8.GetBytes("prefix:")); + prefixed = new KeyPrefixedBatch(mock.Object, Encoding.UTF8.GetBytes("prefix:")); } [Fact] public void Execute() { - wrapper.Execute(); + prefixed.Execute(); mock.Verify(_ => _.Execute(), Times.Once()); } } diff --git a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs similarity index 70% rename from tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs rename to tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs index d3fba4d08..96f7d4c85 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs @@ -13,15 +13,15 @@ namespace StackExchange.Redis.Tests; public class MoqDependentCollection { } [Collection(nameof(MoqDependentCollection))] -public sealed class DatabaseWrapperTests +public sealed class KeyPrefixedDatabaseTests { private readonly Mock mock; - private readonly IDatabase wrapper; + private readonly IDatabase prefixed; - public DatabaseWrapperTests() + public KeyPrefixedDatabaseTests() { mock = new Mock(); - wrapper = new DatabaseWrapper(mock.Object, Encoding.UTF8.GetBytes("prefix:")); + prefixed = new KeyPrefixedDatabase(mock.Object, Encoding.UTF8.GetBytes("prefix:")); } [Fact] @@ -30,10 +30,10 @@ public void CreateBatch() object asyncState = new(); IBatch innerBatch = new Mock().Object; mock.Setup(_ => _.CreateBatch(asyncState)).Returns(innerBatch); - IBatch wrappedBatch = wrapper.CreateBatch(asyncState); + IBatch wrappedBatch = prefixed.CreateBatch(asyncState); mock.Verify(_ => _.CreateBatch(asyncState)); - Assert.IsType(wrappedBatch); - Assert.Same(innerBatch, ((BatchWrapper)wrappedBatch).Inner); + Assert.IsType(wrappedBatch); + Assert.Same(innerBatch, ((KeyPrefixedBatch)wrappedBatch).Inner); } [Fact] @@ -42,16 +42,16 @@ public void CreateTransaction() object asyncState = new(); ITransaction innerTransaction = new Mock().Object; mock.Setup(_ => _.CreateTransaction(asyncState)).Returns(innerTransaction); - ITransaction wrappedTransaction = wrapper.CreateTransaction(asyncState); + ITransaction wrappedTransaction = prefixed.CreateTransaction(asyncState); mock.Verify(_ => _.CreateTransaction(asyncState)); - Assert.IsType(wrappedTransaction); - Assert.Same(innerTransaction, ((TransactionWrapper)wrappedTransaction).Inner); + Assert.IsType(wrappedTransaction); + Assert.Same(innerTransaction, ((KeyPrefixedTransaction)wrappedTransaction).Inner); } [Fact] public void DebugObject() { - wrapper.DebugObject("key", CommandFlags.None); + prefixed.DebugObject("key", CommandFlags.None); mock.Verify(_ => _.DebugObject("prefix:key", CommandFlags.None)); } @@ -59,27 +59,27 @@ public void DebugObject() public void Get_Database() { mock.SetupGet(_ => _.Database).Returns(123); - Assert.Equal(123, wrapper.Database); + Assert.Equal(123, prefixed.Database); } [Fact] public void HashDecrement_1() { - wrapper.HashDecrement("key", "hashField", 123, CommandFlags.None); + prefixed.HashDecrement("key", "hashField", 123, CommandFlags.None); mock.Verify(_ => _.HashDecrement("prefix:key", "hashField", 123, CommandFlags.None)); } [Fact] public void HashDecrement_2() { - wrapper.HashDecrement("key", "hashField", 1.23, CommandFlags.None); + prefixed.HashDecrement("key", "hashField", 1.23, CommandFlags.None); mock.Verify(_ => _.HashDecrement("prefix:key", "hashField", 1.23, CommandFlags.None)); } [Fact] public void HashDelete_1() { - wrapper.HashDelete("key", "hashField", CommandFlags.None); + prefixed.HashDelete("key", "hashField", CommandFlags.None); mock.Verify(_ => _.HashDelete("prefix:key", "hashField", CommandFlags.None)); } @@ -87,21 +87,21 @@ public void HashDelete_1() public void HashDelete_2() { RedisValue[] hashFields = Array.Empty(); - wrapper.HashDelete("key", hashFields, CommandFlags.None); + prefixed.HashDelete("key", hashFields, CommandFlags.None); mock.Verify(_ => _.HashDelete("prefix:key", hashFields, CommandFlags.None)); } [Fact] public void HashExists() { - wrapper.HashExists("key", "hashField", CommandFlags.None); + prefixed.HashExists("key", "hashField", CommandFlags.None); mock.Verify(_ => _.HashExists("prefix:key", "hashField", CommandFlags.None)); } [Fact] public void HashGet_1() { - wrapper.HashGet("key", "hashField", CommandFlags.None); + prefixed.HashGet("key", "hashField", CommandFlags.None); mock.Verify(_ => _.HashGet("prefix:key", "hashField", CommandFlags.None)); } @@ -109,56 +109,56 @@ public void HashGet_1() public void HashGet_2() { RedisValue[] hashFields = Array.Empty(); - wrapper.HashGet("key", hashFields, CommandFlags.None); + prefixed.HashGet("key", hashFields, CommandFlags.None); mock.Verify(_ => _.HashGet("prefix:key", hashFields, CommandFlags.None)); } [Fact] public void HashGetAll() { - wrapper.HashGetAll("key", CommandFlags.None); + prefixed.HashGetAll("key", CommandFlags.None); mock.Verify(_ => _.HashGetAll("prefix:key", CommandFlags.None)); } [Fact] public void HashIncrement_1() { - wrapper.HashIncrement("key", "hashField", 123, CommandFlags.None); + prefixed.HashIncrement("key", "hashField", 123, CommandFlags.None); mock.Verify(_ => _.HashIncrement("prefix:key", "hashField", 123, CommandFlags.None)); } [Fact] public void HashIncrement_2() { - wrapper.HashIncrement("key", "hashField", 1.23, CommandFlags.None); + prefixed.HashIncrement("key", "hashField", 1.23, CommandFlags.None); mock.Verify(_ => _.HashIncrement("prefix:key", "hashField", 1.23, CommandFlags.None)); } [Fact] public void HashKeys() { - wrapper.HashKeys("key", CommandFlags.None); + prefixed.HashKeys("key", CommandFlags.None); mock.Verify(_ => _.HashKeys("prefix:key", CommandFlags.None)); } [Fact] public void HashLength() { - wrapper.HashLength("key", CommandFlags.None); + prefixed.HashLength("key", CommandFlags.None); mock.Verify(_ => _.HashLength("prefix:key", CommandFlags.None)); } [Fact] public void HashScan() { - wrapper.HashScan("key", "pattern", 123, flags: CommandFlags.None); + prefixed.HashScan("key", "pattern", 123, flags: CommandFlags.None); mock.Verify(_ => _.HashScan("prefix:key", "pattern", 123, CommandFlags.None)); } [Fact] public void HashScan_Full() { - wrapper.HashScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); + prefixed.HashScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); mock.Verify(_ => _.HashScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None)); } @@ -166,35 +166,35 @@ public void HashScan_Full() public void HashSet_1() { HashEntry[] hashFields = Array.Empty(); - wrapper.HashSet("key", hashFields, CommandFlags.None); + prefixed.HashSet("key", hashFields, CommandFlags.None); mock.Verify(_ => _.HashSet("prefix:key", hashFields, CommandFlags.None)); } [Fact] public void HashSet_2() { - wrapper.HashSet("key", "hashField", "value", When.Exists, CommandFlags.None); + prefixed.HashSet("key", "hashField", "value", When.Exists, CommandFlags.None); mock.Verify(_ => _.HashSet("prefix:key", "hashField", "value", When.Exists, CommandFlags.None)); } [Fact] public void HashStringLength() { - wrapper.HashStringLength("key", "field", CommandFlags.None); + prefixed.HashStringLength("key", "field", CommandFlags.None); mock.Verify(_ => _.HashStringLength("prefix:key", "field", CommandFlags.None)); } [Fact] public void HashValues() { - wrapper.HashValues("key", CommandFlags.None); + prefixed.HashValues("key", CommandFlags.None); mock.Verify(_ => _.HashValues("prefix:key", CommandFlags.None)); } [Fact] public void HyperLogLogAdd_1() { - wrapper.HyperLogLogAdd("key", "value", CommandFlags.None); + prefixed.HyperLogLogAdd("key", "value", CommandFlags.None); mock.Verify(_ => _.HyperLogLogAdd("prefix:key", "value", CommandFlags.None)); } @@ -202,21 +202,21 @@ public void HyperLogLogAdd_1() public void HyperLogLogAdd_2() { RedisValue[] values = Array.Empty(); - wrapper.HyperLogLogAdd("key", values, CommandFlags.None); + prefixed.HyperLogLogAdd("key", values, CommandFlags.None); mock.Verify(_ => _.HyperLogLogAdd("prefix:key", values, CommandFlags.None)); } [Fact] public void HyperLogLogLength() { - wrapper.HyperLogLogLength("key", CommandFlags.None); + prefixed.HyperLogLogLength("key", CommandFlags.None); mock.Verify(_ => _.HyperLogLogLength("prefix:key", CommandFlags.None)); } [Fact] public void HyperLogLogMerge_1() { - wrapper.HyperLogLogMerge("destination", "first", "second", CommandFlags.None); + prefixed.HyperLogLogMerge("destination", "first", "second", CommandFlags.None); mock.Verify(_ => _.HyperLogLogMerge("prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); } @@ -225,28 +225,28 @@ public void HyperLogLogMerge_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.HyperLogLogMerge("destination", keys, CommandFlags.None); + prefixed.HyperLogLogMerge("destination", keys, CommandFlags.None); mock.Verify(_ => _.HyperLogLogMerge("prefix:destination", It.Is(valid), CommandFlags.None)); } [Fact] public void IdentifyEndpoint() { - wrapper.IdentifyEndpoint("key", CommandFlags.None); + prefixed.IdentifyEndpoint("key", CommandFlags.None); mock.Verify(_ => _.IdentifyEndpoint("prefix:key", CommandFlags.None)); } [Fact] public void KeyCopy() { - wrapper.KeyCopy("key", "destination", flags: CommandFlags.None); + prefixed.KeyCopy("key", "destination", flags: CommandFlags.None); mock.Verify(_ => _.KeyCopy("prefix:key", "prefix:destination", -1, false, CommandFlags.None)); } [Fact] public void KeyDelete_1() { - wrapper.KeyDelete("key", CommandFlags.None); + prefixed.KeyDelete("key", CommandFlags.None); mock.Verify(_ => _.KeyDelete("prefix:key", CommandFlags.None)); } @@ -255,28 +255,28 @@ public void KeyDelete_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.KeyDelete(keys, CommandFlags.None); + prefixed.KeyDelete(keys, CommandFlags.None); mock.Verify(_ => _.KeyDelete(It.Is(valid), CommandFlags.None)); } [Fact] public void KeyDump() { - wrapper.KeyDump("key", CommandFlags.None); + prefixed.KeyDump("key", CommandFlags.None); mock.Verify(_ => _.KeyDump("prefix:key", CommandFlags.None)); } [Fact] public void KeyEncoding() { - wrapper.KeyEncoding("key", CommandFlags.None); + prefixed.KeyEncoding("key", CommandFlags.None); mock.Verify(_ => _.KeyEncoding("prefix:key", CommandFlags.None)); } [Fact] public void KeyExists() { - wrapper.KeyExists("key", CommandFlags.None); + prefixed.KeyExists("key", CommandFlags.None); mock.Verify(_ => _.KeyExists("prefix:key", CommandFlags.None)); } @@ -284,7 +284,7 @@ public void KeyExists() public void KeyExpire_1() { TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.KeyExpire("key", expiry, CommandFlags.None); + prefixed.KeyExpire("key", expiry, CommandFlags.None); mock.Verify(_ => _.KeyExpire("prefix:key", expiry, CommandFlags.None)); } @@ -292,7 +292,7 @@ public void KeyExpire_1() public void KeyExpire_2() { DateTime expiry = DateTime.Now; - wrapper.KeyExpire("key", expiry, CommandFlags.None); + prefixed.KeyExpire("key", expiry, CommandFlags.None); mock.Verify(_ => _.KeyExpire("prefix:key", expiry, CommandFlags.None)); } @@ -300,7 +300,7 @@ public void KeyExpire_2() public void KeyExpire_3() { TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.KeyExpire("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); + prefixed.KeyExpire("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); mock.Verify(_ => _.KeyExpire("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); } @@ -308,21 +308,21 @@ public void KeyExpire_3() public void KeyExpire_4() { DateTime expiry = DateTime.Now; - wrapper.KeyExpire("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); + prefixed.KeyExpire("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); mock.Verify(_ => _.KeyExpire("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); } [Fact] public void KeyExpireTime() { - wrapper.KeyExpireTime("key", CommandFlags.None); + prefixed.KeyExpireTime("key", CommandFlags.None); mock.Verify(_ => _.KeyExpireTime("prefix:key", CommandFlags.None)); } [Fact] public void KeyFrequency() { - wrapper.KeyFrequency("key", CommandFlags.None); + prefixed.KeyFrequency("key", CommandFlags.None); mock.Verify(_ => _.KeyFrequency("prefix:key", CommandFlags.None)); } @@ -330,41 +330,41 @@ public void KeyFrequency() public void KeyMigrate() { EndPoint toServer = new IPEndPoint(IPAddress.Loopback, 123); - wrapper.KeyMigrate("key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None); + prefixed.KeyMigrate("key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None); mock.Verify(_ => _.KeyMigrate("prefix:key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None)); } [Fact] public void KeyMove() { - wrapper.KeyMove("key", 123, CommandFlags.None); + prefixed.KeyMove("key", 123, CommandFlags.None); mock.Verify(_ => _.KeyMove("prefix:key", 123, CommandFlags.None)); } [Fact] public void KeyPersist() { - wrapper.KeyPersist("key", CommandFlags.None); + prefixed.KeyPersist("key", CommandFlags.None); mock.Verify(_ => _.KeyPersist("prefix:key", CommandFlags.None)); } [Fact] public void KeyRandom() { - Assert.Throws(() => wrapper.KeyRandom()); + Assert.Throws(() => prefixed.KeyRandom()); } [Fact] public void KeyRefCount() { - wrapper.KeyRefCount("key", CommandFlags.None); + prefixed.KeyRefCount("key", CommandFlags.None); mock.Verify(_ => _.KeyRefCount("prefix:key", CommandFlags.None)); } [Fact] public void KeyRename() { - wrapper.KeyRename("key", "newKey", When.Exists, CommandFlags.None); + prefixed.KeyRename("key", "newKey", When.Exists, CommandFlags.None); mock.Verify(_ => _.KeyRename("prefix:key", "prefix:newKey", When.Exists, CommandFlags.None)); } @@ -373,63 +373,63 @@ public void KeyRestore() { byte[] value = Array.Empty(); TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.KeyRestore("key", value, expiry, CommandFlags.None); + prefixed.KeyRestore("key", value, expiry, CommandFlags.None); mock.Verify(_ => _.KeyRestore("prefix:key", value, expiry, CommandFlags.None)); } [Fact] public void KeyTimeToLive() { - wrapper.KeyTimeToLive("key", CommandFlags.None); + prefixed.KeyTimeToLive("key", CommandFlags.None); mock.Verify(_ => _.KeyTimeToLive("prefix:key", CommandFlags.None)); } [Fact] public void KeyType() { - wrapper.KeyType("key", CommandFlags.None); + prefixed.KeyType("key", CommandFlags.None); mock.Verify(_ => _.KeyType("prefix:key", CommandFlags.None)); } [Fact] public void ListGetByIndex() { - wrapper.ListGetByIndex("key", 123, CommandFlags.None); + prefixed.ListGetByIndex("key", 123, CommandFlags.None); mock.Verify(_ => _.ListGetByIndex("prefix:key", 123, CommandFlags.None)); } [Fact] public void ListInsertAfter() { - wrapper.ListInsertAfter("key", "pivot", "value", CommandFlags.None); + prefixed.ListInsertAfter("key", "pivot", "value", CommandFlags.None); mock.Verify(_ => _.ListInsertAfter("prefix:key", "pivot", "value", CommandFlags.None)); } [Fact] public void ListInsertBefore() { - wrapper.ListInsertBefore("key", "pivot", "value", CommandFlags.None); + prefixed.ListInsertBefore("key", "pivot", "value", CommandFlags.None); mock.Verify(_ => _.ListInsertBefore("prefix:key", "pivot", "value", CommandFlags.None)); } [Fact] public void ListLeftPop() { - wrapper.ListLeftPop("key", CommandFlags.None); + prefixed.ListLeftPop("key", CommandFlags.None); mock.Verify(_ => _.ListLeftPop("prefix:key", CommandFlags.None)); } [Fact] public void ListLeftPop_1() { - wrapper.ListLeftPop("key", 123, CommandFlags.None); + prefixed.ListLeftPop("key", 123, CommandFlags.None); mock.Verify(_ => _.ListLeftPop("prefix:key", 123, CommandFlags.None)); } [Fact] public void ListLeftPush_1() { - wrapper.ListLeftPush("key", "value", When.Exists, CommandFlags.None); + prefixed.ListLeftPush("key", "value", When.Exists, CommandFlags.None); mock.Verify(_ => _.ListLeftPush("prefix:key", "value", When.Exists, CommandFlags.None)); } @@ -437,7 +437,7 @@ public void ListLeftPush_1() public void ListLeftPush_2() { RedisValue[] values = Array.Empty(); - wrapper.ListLeftPush("key", values, CommandFlags.None); + prefixed.ListLeftPush("key", values, CommandFlags.None); mock.Verify(_ => _.ListLeftPush("prefix:key", values, CommandFlags.None)); } @@ -445,63 +445,63 @@ public void ListLeftPush_2() public void ListLeftPush_3() { RedisValue[] values = new RedisValue[] { "value1", "value2" }; - wrapper.ListLeftPush("key", values, When.Exists, CommandFlags.None); + prefixed.ListLeftPush("key", values, When.Exists, CommandFlags.None); mock.Verify(_ => _.ListLeftPush("prefix:key", values, When.Exists, CommandFlags.None)); } [Fact] public void ListLength() { - wrapper.ListLength("key", CommandFlags.None); + prefixed.ListLength("key", CommandFlags.None); mock.Verify(_ => _.ListLength("prefix:key", CommandFlags.None)); } [Fact] public void ListMove() { - wrapper.ListMove("key", "destination", ListSide.Left, ListSide.Right, CommandFlags.None); + prefixed.ListMove("key", "destination", ListSide.Left, ListSide.Right, CommandFlags.None); mock.Verify(_ => _.ListMove("prefix:key", "prefix:destination", ListSide.Left, ListSide.Right, CommandFlags.None)); } [Fact] public void ListRange() { - wrapper.ListRange("key", 123, 456, CommandFlags.None); + prefixed.ListRange("key", 123, 456, CommandFlags.None); mock.Verify(_ => _.ListRange("prefix:key", 123, 456, CommandFlags.None)); } [Fact] public void ListRemove() { - wrapper.ListRemove("key", "value", 123, CommandFlags.None); + prefixed.ListRemove("key", "value", 123, CommandFlags.None); mock.Verify(_ => _.ListRemove("prefix:key", "value", 123, CommandFlags.None)); } [Fact] public void ListRightPop() { - wrapper.ListRightPop("key", CommandFlags.None); + prefixed.ListRightPop("key", CommandFlags.None); mock.Verify(_ => _.ListRightPop("prefix:key", CommandFlags.None)); } [Fact] public void ListRightPop_1() { - wrapper.ListRightPop("key", 123, CommandFlags.None); + prefixed.ListRightPop("key", 123, CommandFlags.None); mock.Verify(_ => _.ListRightPop("prefix:key", 123, CommandFlags.None)); } [Fact] public void ListRightPopLeftPush() { - wrapper.ListRightPopLeftPush("source", "destination", CommandFlags.None); + prefixed.ListRightPopLeftPush("source", "destination", CommandFlags.None); mock.Verify(_ => _.ListRightPopLeftPush("prefix:source", "prefix:destination", CommandFlags.None)); } [Fact] public void ListRightPush_1() { - wrapper.ListRightPush("key", "value", When.Exists, CommandFlags.None); + prefixed.ListRightPush("key", "value", When.Exists, CommandFlags.None); mock.Verify(_ => _.ListRightPush("prefix:key", "value", When.Exists, CommandFlags.None)); } @@ -509,7 +509,7 @@ public void ListRightPush_1() public void ListRightPush_2() { RedisValue[] values = Array.Empty(); - wrapper.ListRightPush("key", values, CommandFlags.None); + prefixed.ListRightPush("key", values, CommandFlags.None); mock.Verify(_ => _.ListRightPush("prefix:key", values, CommandFlags.None)); } @@ -517,21 +517,21 @@ public void ListRightPush_2() public void ListRightPush_3() { RedisValue[] values = new RedisValue[] { "value1", "value2" }; - wrapper.ListRightPush("key", values, When.Exists, CommandFlags.None); + prefixed.ListRightPush("key", values, When.Exists, CommandFlags.None); mock.Verify(_ => _.ListRightPush("prefix:key", values, When.Exists, CommandFlags.None)); } [Fact] public void ListSetByIndex() { - wrapper.ListSetByIndex("key", 123, "value", CommandFlags.None); + prefixed.ListSetByIndex("key", 123, "value", CommandFlags.None); mock.Verify(_ => _.ListSetByIndex("prefix:key", 123, "value", CommandFlags.None)); } [Fact] public void ListTrim() { - wrapper.ListTrim("key", 123, 456, CommandFlags.None); + prefixed.ListTrim("key", 123, 456, CommandFlags.None); mock.Verify(_ => _.ListTrim("prefix:key", 123, 456, CommandFlags.None)); } @@ -539,21 +539,21 @@ public void ListTrim() public void LockExtend() { TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.LockExtend("key", "value", expiry, CommandFlags.None); + prefixed.LockExtend("key", "value", expiry, CommandFlags.None); mock.Verify(_ => _.LockExtend("prefix:key", "value", expiry, CommandFlags.None)); } [Fact] public void LockQuery() { - wrapper.LockQuery("key", CommandFlags.None); + prefixed.LockQuery("key", CommandFlags.None); mock.Verify(_ => _.LockQuery("prefix:key", CommandFlags.None)); } [Fact] public void LockRelease() { - wrapper.LockRelease("key", "value", CommandFlags.None); + prefixed.LockRelease("key", "value", CommandFlags.None); mock.Verify(_ => _.LockRelease("prefix:key", "value", CommandFlags.None)); } @@ -561,14 +561,14 @@ public void LockRelease() public void LockTake() { TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.LockTake("key", "value", expiry, CommandFlags.None); + prefixed.LockTake("key", "value", expiry, CommandFlags.None); mock.Verify(_ => _.LockTake("prefix:key", "value", expiry, CommandFlags.None)); } [Fact] public void Publish() { - wrapper.Publish("channel", "message", CommandFlags.None); + prefixed.Publish("channel", "message", CommandFlags.None); mock.Verify(_ => _.Publish("prefix:channel", "message", CommandFlags.None)); } @@ -579,7 +579,7 @@ public void ScriptEvaluate_1() RedisValue[] values = Array.Empty(); RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.ScriptEvaluate(hash, keys, values, CommandFlags.None); + prefixed.ScriptEvaluate(hash, keys, values, CommandFlags.None); mock.Verify(_ => _.ScriptEvaluate(hash, It.Is(valid), values, CommandFlags.None)); } @@ -589,14 +589,14 @@ public void ScriptEvaluate_2() RedisValue[] values = Array.Empty(); RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.ScriptEvaluate("script", keys, values, CommandFlags.None); + prefixed.ScriptEvaluate("script", keys, values, CommandFlags.None); mock.Verify(_ => _.ScriptEvaluate("script", It.Is(valid), values, CommandFlags.None)); } [Fact] public void SetAdd_1() { - wrapper.SetAdd("key", "value", CommandFlags.None); + prefixed.SetAdd("key", "value", CommandFlags.None); mock.Verify(_ => _.SetAdd("prefix:key", "value", CommandFlags.None)); } @@ -604,14 +604,14 @@ public void SetAdd_1() public void SetAdd_2() { RedisValue[] values = Array.Empty(); - wrapper.SetAdd("key", values, CommandFlags.None); + prefixed.SetAdd("key", values, CommandFlags.None); mock.Verify(_ => _.SetAdd("prefix:key", values, CommandFlags.None)); } [Fact] public void SetCombine_1() { - wrapper.SetCombine(SetOperation.Intersect, "first", "second", CommandFlags.None); + prefixed.SetCombine(SetOperation.Intersect, "first", "second", CommandFlags.None); mock.Verify(_ => _.SetCombine(SetOperation.Intersect, "prefix:first", "prefix:second", CommandFlags.None)); } @@ -620,14 +620,14 @@ public void SetCombine_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.SetCombine(SetOperation.Intersect, keys, CommandFlags.None); + prefixed.SetCombine(SetOperation.Intersect, keys, CommandFlags.None); mock.Verify(_ => _.SetCombine(SetOperation.Intersect, It.Is(valid), CommandFlags.None)); } [Fact] public void SetCombineAndStore_1() { - wrapper.SetCombineAndStore(SetOperation.Intersect, "destination", "first", "second", CommandFlags.None); + prefixed.SetCombineAndStore(SetOperation.Intersect, "destination", "first", "second", CommandFlags.None); mock.Verify(_ => _.SetCombineAndStore(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); } @@ -636,14 +636,14 @@ public void SetCombineAndStore_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None); + prefixed.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None); mock.Verify(_ => _.SetCombineAndStore(SetOperation.Intersect, "prefix:destination", It.Is(valid), CommandFlags.None)); } [Fact] public void SetContains() { - wrapper.SetContains("key", "value", CommandFlags.None); + prefixed.SetContains("key", "value", CommandFlags.None); mock.Verify(_ => _.SetContains("prefix:key", "value", CommandFlags.None)); } @@ -651,7 +651,7 @@ public void SetContains() public void SetContains_2() { RedisValue[] values = new RedisValue[] { "value1", "value2" }; - wrapper.SetContains("key", values, CommandFlags.None); + prefixed.SetContains("key", values, CommandFlags.None); mock.Verify(_ => _.SetContains("prefix:key", values, CommandFlags.None)); } @@ -659,66 +659,66 @@ public void SetContains_2() public void SetIntersectionLength() { var keys = new RedisKey[] { "key1", "key2" }; - wrapper.SetIntersectionLength(keys); + prefixed.SetIntersectionLength(keys); mock.Verify(_ => _.SetIntersectionLength(keys, 0, CommandFlags.None)); } [Fact] public void SetLength() { - wrapper.SetLength("key", CommandFlags.None); + prefixed.SetLength("key", CommandFlags.None); mock.Verify(_ => _.SetLength("prefix:key", CommandFlags.None)); } [Fact] public void SetMembers() { - wrapper.SetMembers("key", CommandFlags.None); + prefixed.SetMembers("key", CommandFlags.None); mock.Verify(_ => _.SetMembers("prefix:key", CommandFlags.None)); } [Fact] public void SetMove() { - wrapper.SetMove("source", "destination", "value", CommandFlags.None); + prefixed.SetMove("source", "destination", "value", CommandFlags.None); mock.Verify(_ => _.SetMove("prefix:source", "prefix:destination", "value", CommandFlags.None)); } [Fact] public void SetPop_1() { - wrapper.SetPop("key", CommandFlags.None); + prefixed.SetPop("key", CommandFlags.None); mock.Verify(_ => _.SetPop("prefix:key", CommandFlags.None)); - wrapper.SetPop("key", 5, CommandFlags.None); + prefixed.SetPop("key", 5, CommandFlags.None); mock.Verify(_ => _.SetPop("prefix:key", 5, CommandFlags.None)); } [Fact] public void SetPop_2() { - wrapper.SetPop("key", 5, CommandFlags.None); + prefixed.SetPop("key", 5, CommandFlags.None); mock.Verify(_ => _.SetPop("prefix:key", 5, CommandFlags.None)); } [Fact] public void SetRandomMember() { - wrapper.SetRandomMember("key", CommandFlags.None); + prefixed.SetRandomMember("key", CommandFlags.None); mock.Verify(_ => _.SetRandomMember("prefix:key", CommandFlags.None)); } [Fact] public void SetRandomMembers() { - wrapper.SetRandomMembers("key", 123, CommandFlags.None); + prefixed.SetRandomMembers("key", 123, CommandFlags.None); mock.Verify(_ => _.SetRandomMembers("prefix:key", 123, CommandFlags.None)); } [Fact] public void SetRemove_1() { - wrapper.SetRemove("key", "value", CommandFlags.None); + prefixed.SetRemove("key", "value", CommandFlags.None); mock.Verify(_ => _.SetRemove("prefix:key", "value", CommandFlags.None)); } @@ -726,21 +726,21 @@ public void SetRemove_1() public void SetRemove_2() { RedisValue[] values = Array.Empty(); - wrapper.SetRemove("key", values, CommandFlags.None); + prefixed.SetRemove("key", values, CommandFlags.None); mock.Verify(_ => _.SetRemove("prefix:key", values, CommandFlags.None)); } [Fact] public void SetScan() { - wrapper.SetScan("key", "pattern", 123, flags: CommandFlags.None); + prefixed.SetScan("key", "pattern", 123, flags: CommandFlags.None); mock.Verify(_ => _.SetScan("prefix:key", "pattern", 123, CommandFlags.None)); } [Fact] public void SetScan_Full() { - wrapper.SetScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); + prefixed.SetScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); mock.Verify(_ => _.SetScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None)); } @@ -750,8 +750,8 @@ public void Sort() RedisValue[] get = new RedisValue[] { "a", "#" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; - wrapper.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); - wrapper.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); + prefixed.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); + prefixed.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); mock.Verify(_ => _.Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", It.Is(valid), CommandFlags.None)); mock.Verify(_ => _.Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", It.Is(valid), CommandFlags.None)); @@ -763,8 +763,8 @@ public void SortAndStore() RedisValue[] get = new RedisValue[] { "a", "#" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; - wrapper.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); - wrapper.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); + prefixed.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); + prefixed.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); mock.Verify(_ => _.SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", It.Is(valid), CommandFlags.None)); mock.Verify(_ => _.SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", It.Is(valid), CommandFlags.None)); @@ -773,7 +773,7 @@ public void SortAndStore() [Fact] public void SortedSetAdd_1() { - wrapper.SortedSetAdd("key", "member", 1.23, When.Exists, CommandFlags.None); + prefixed.SortedSetAdd("key", "member", 1.23, When.Exists, CommandFlags.None); mock.Verify(_ => _.SortedSetAdd("prefix:key", "member", 1.23, When.Exists, CommandFlags.None)); } @@ -781,7 +781,7 @@ public void SortedSetAdd_1() public void SortedSetAdd_2() { SortedSetEntry[] values = Array.Empty(); - wrapper.SortedSetAdd("key", values, When.Exists, CommandFlags.None); + prefixed.SortedSetAdd("key", values, When.Exists, CommandFlags.None); mock.Verify(_ => _.SortedSetAdd("prefix:key", values, When.Exists, CommandFlags.None)); } @@ -789,7 +789,7 @@ public void SortedSetAdd_2() public void SortedSetAdd_3() { SortedSetEntry[] values = Array.Empty(); - wrapper.SortedSetAdd("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); + prefixed.SortedSetAdd("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); mock.Verify(_ => _.SortedSetAdd("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None)); } @@ -797,7 +797,7 @@ public void SortedSetAdd_3() public void SortedSetCombine() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - wrapper.SortedSetCombine(SetOperation.Intersect, keys); + prefixed.SortedSetCombine(SetOperation.Intersect, keys); mock.Verify(_ => _.SortedSetCombine(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); } @@ -805,14 +805,14 @@ public void SortedSetCombine() public void SortedSetCombineWithScores() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - wrapper.SortedSetCombineWithScores(SetOperation.Intersect, keys); + prefixed.SortedSetCombineWithScores(SetOperation.Intersect, keys); mock.Verify(_ => _.SortedSetCombineWithScores(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); } [Fact] public void SortedSetCombineAndStore_1() { - wrapper.SortedSetCombineAndStore(SetOperation.Intersect, "destination", "first", "second", Aggregate.Max, CommandFlags.None); + prefixed.SortedSetCombineAndStore(SetOperation.Intersect, "destination", "first", "second", Aggregate.Max, CommandFlags.None); mock.Verify(_ => _.SortedSetCombineAndStore(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", Aggregate.Max, CommandFlags.None)); } @@ -821,21 +821,21 @@ public void SortedSetCombineAndStore_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None); + prefixed.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None); mock.Verify(_ => _.SetCombineAndStore(SetOperation.Intersect, "prefix:destination", It.Is(valid), CommandFlags.None)); } [Fact] public void SortedSetDecrement() { - wrapper.SortedSetDecrement("key", "member", 1.23, CommandFlags.None); + prefixed.SortedSetDecrement("key", "member", 1.23, CommandFlags.None); mock.Verify(_ => _.SortedSetDecrement("prefix:key", "member", 1.23, CommandFlags.None)); } [Fact] public void SortedSetIncrement() { - wrapper.SortedSetIncrement("key", "member", 1.23, CommandFlags.None); + prefixed.SortedSetIncrement("key", "member", 1.23, CommandFlags.None); mock.Verify(_ => _.SortedSetIncrement("prefix:key", "member", 1.23, CommandFlags.None)); } @@ -843,98 +843,98 @@ public void SortedSetIncrement() public void SortedSetIntersectionLength() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - wrapper.SortedSetIntersectionLength(keys, 1, CommandFlags.None); + prefixed.SortedSetIntersectionLength(keys, 1, CommandFlags.None); mock.Verify(_ => _.SortedSetIntersectionLength(keys, 1, CommandFlags.None)); } [Fact] public void SortedSetLength() { - wrapper.SortedSetLength("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); + prefixed.SortedSetLength("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); mock.Verify(_ => _.SortedSetLength("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None)); } [Fact] public void SortedSetRandomMember() { - wrapper.SortedSetRandomMember("key", CommandFlags.None); + prefixed.SortedSetRandomMember("key", CommandFlags.None); mock.Verify(_ => _.SortedSetRandomMember("prefix:key", CommandFlags.None)); } [Fact] public void SortedSetRandomMembers() { - wrapper.SortedSetRandomMembers("key", 2, CommandFlags.None); + prefixed.SortedSetRandomMembers("key", 2, CommandFlags.None); mock.Verify(_ => _.SortedSetRandomMembers("prefix:key", 2, CommandFlags.None)); } [Fact] public void SortedSetRandomMembersWithScores() { - wrapper.SortedSetRandomMembersWithScores("key", 2, CommandFlags.None); + prefixed.SortedSetRandomMembersWithScores("key", 2, CommandFlags.None); mock.Verify(_ => _.SortedSetRandomMembersWithScores("prefix:key", 2, CommandFlags.None)); } [Fact] public void SortedSetLengthByValue() { - wrapper.SortedSetLengthByValue("key", "min", "max", Exclude.Start, CommandFlags.None); + prefixed.SortedSetLengthByValue("key", "min", "max", Exclude.Start, CommandFlags.None); mock.Verify(_ => _.SortedSetLengthByValue("prefix:key", "min", "max", Exclude.Start, CommandFlags.None)); } [Fact] public void SortedSetRangeByRank() { - wrapper.SortedSetRangeByRank("key", 123, 456, Order.Descending, CommandFlags.None); + prefixed.SortedSetRangeByRank("key", 123, 456, Order.Descending, CommandFlags.None); mock.Verify(_ => _.SortedSetRangeByRank("prefix:key", 123, 456, Order.Descending, CommandFlags.None)); } [Fact] public void SortedSetRangeByRankWithScores() { - wrapper.SortedSetRangeByRankWithScores("key", 123, 456, Order.Descending, CommandFlags.None); + prefixed.SortedSetRangeByRankWithScores("key", 123, 456, Order.Descending, CommandFlags.None); mock.Verify(_ => _.SortedSetRangeByRankWithScores("prefix:key", 123, 456, Order.Descending, CommandFlags.None)); } [Fact] public void SortedSetRangeByScore() { - wrapper.SortedSetRangeByScore("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); + prefixed.SortedSetRangeByScore("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); mock.Verify(_ => _.SortedSetRangeByScore("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); } [Fact] public void SortedSetRangeByScoreWithScores() { - wrapper.SortedSetRangeByScoreWithScores("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); + prefixed.SortedSetRangeByScoreWithScores("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); mock.Verify(_ => _.SortedSetRangeByScoreWithScores("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); } [Fact] public void SortedSetRangeByValue() { - wrapper.SortedSetRangeByValue("key", "min", "max", Exclude.Start, 123, 456, CommandFlags.None); + prefixed.SortedSetRangeByValue("key", "min", "max", Exclude.Start, 123, 456, CommandFlags.None); mock.Verify(_ => _.SortedSetRangeByValue("prefix:key", "min", "max", Exclude.Start, Order.Ascending, 123, 456, CommandFlags.None)); } [Fact] public void SortedSetRangeByValueDesc() { - wrapper.SortedSetRangeByValue("key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); + prefixed.SortedSetRangeByValue("key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); mock.Verify(_ => _.SortedSetRangeByValue("prefix:key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); } [Fact] public void SortedSetRank() { - wrapper.SortedSetRank("key", "member", Order.Descending, CommandFlags.None); + prefixed.SortedSetRank("key", "member", Order.Descending, CommandFlags.None); mock.Verify(_ => _.SortedSetRank("prefix:key", "member", Order.Descending, CommandFlags.None)); } [Fact] public void SortedSetRemove_1() { - wrapper.SortedSetRemove("key", "member", CommandFlags.None); + prefixed.SortedSetRemove("key", "member", CommandFlags.None); mock.Verify(_ => _.SortedSetRemove("prefix:key", "member", CommandFlags.None)); } @@ -942,56 +942,56 @@ public void SortedSetRemove_1() public void SortedSetRemove_2() { RedisValue[] members = Array.Empty(); - wrapper.SortedSetRemove("key", members, CommandFlags.None); + prefixed.SortedSetRemove("key", members, CommandFlags.None); mock.Verify(_ => _.SortedSetRemove("prefix:key", members, CommandFlags.None)); } [Fact] public void SortedSetRemoveRangeByRank() { - wrapper.SortedSetRemoveRangeByRank("key", 123, 456, CommandFlags.None); + prefixed.SortedSetRemoveRangeByRank("key", 123, 456, CommandFlags.None); mock.Verify(_ => _.SortedSetRemoveRangeByRank("prefix:key", 123, 456, CommandFlags.None)); } [Fact] public void SortedSetRemoveRangeByScore() { - wrapper.SortedSetRemoveRangeByScore("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); + prefixed.SortedSetRemoveRangeByScore("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); mock.Verify(_ => _.SortedSetRemoveRangeByScore("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None)); } [Fact] public void SortedSetRemoveRangeByValue() { - wrapper.SortedSetRemoveRangeByValue("key", "min", "max", Exclude.Start, CommandFlags.None); + prefixed.SortedSetRemoveRangeByValue("key", "min", "max", Exclude.Start, CommandFlags.None); mock.Verify(_ => _.SortedSetRemoveRangeByValue("prefix:key", "min", "max", Exclude.Start, CommandFlags.None)); } [Fact] public void SortedSetScan() { - wrapper.SortedSetScan("key", "pattern", 123, flags: CommandFlags.None); + prefixed.SortedSetScan("key", "pattern", 123, flags: CommandFlags.None); mock.Verify(_ => _.SortedSetScan("prefix:key", "pattern", 123, CommandFlags.None)); } [Fact] public void SortedSetScan_Full() { - wrapper.SortedSetScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); + prefixed.SortedSetScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); mock.Verify(_ => _.SortedSetScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None)); } [Fact] public void SortedSetScore() { - wrapper.SortedSetScore("key", "member", CommandFlags.None); + prefixed.SortedSetScore("key", "member", CommandFlags.None); mock.Verify(_ => _.SortedSetScore("prefix:key", "member", CommandFlags.None)); } [Fact] public void SortedSetScore_Multiple() { - wrapper.SortedSetScores("key", new RedisValue[] { "member1", "member2" }, CommandFlags.None); + prefixed.SortedSetScores("key", new RedisValue[] { "member1", "member2" }, CommandFlags.None); mock.Verify(_ => _.SortedSetScores("prefix:key", new RedisValue[] { "member1", "member2" }, CommandFlags.None)); } @@ -999,14 +999,14 @@ public void SortedSetScore_Multiple() public void SortedSetUpdate() { SortedSetEntry[] values = Array.Empty(); - wrapper.SortedSetUpdate("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); + prefixed.SortedSetUpdate("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); mock.Verify(_ => _.SortedSetUpdate("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None)); } [Fact] public void StreamAcknowledge_1() { - wrapper.StreamAcknowledge("key", "group", "0-0", CommandFlags.None); + prefixed.StreamAcknowledge("key", "group", "0-0", CommandFlags.None); mock.Verify(_ => _.StreamAcknowledge("prefix:key", "group", "0-0", CommandFlags.None)); } @@ -1014,14 +1014,14 @@ public void StreamAcknowledge_1() public void StreamAcknowledge_2() { var messageIds = new RedisValue[] { "0-0", "0-1", "0-2" }; - wrapper.StreamAcknowledge("key", "group", messageIds, CommandFlags.None); + prefixed.StreamAcknowledge("key", "group", messageIds, CommandFlags.None); mock.Verify(_ => _.StreamAcknowledge("prefix:key", "group", messageIds, CommandFlags.None)); } [Fact] public void StreamAdd_1() { - wrapper.StreamAdd("key", "field1", "value1", "*", 1000, true, CommandFlags.None); + prefixed.StreamAdd("key", "field1", "value1", "*", 1000, true, CommandFlags.None); mock.Verify(_ => _.StreamAdd("prefix:key", "field1", "value1", "*", 1000, true, CommandFlags.None)); } @@ -1029,21 +1029,21 @@ public void StreamAdd_1() public void StreamAdd_2() { var fields = Array.Empty(); - wrapper.StreamAdd("key", fields, "*", 1000, true, CommandFlags.None); + prefixed.StreamAdd("key", fields, "*", 1000, true, CommandFlags.None); mock.Verify(_ => _.StreamAdd("prefix:key", fields, "*", 1000, true, CommandFlags.None)); } [Fact] public void StreamAutoClaim() { - wrapper.StreamAutoClaim("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); + prefixed.StreamAutoClaim("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); mock.Verify(_ => _.StreamAutoClaim("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None)); } [Fact] public void StreamAutoClaimIdsOnly() { - wrapper.StreamAutoClaimIdsOnly("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); + prefixed.StreamAutoClaimIdsOnly("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); mock.Verify(_ => _.StreamAutoClaimIdsOnly("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None)); } @@ -1051,7 +1051,7 @@ public void StreamAutoClaimIdsOnly() public void StreamClaimMessages() { var messageIds = Array.Empty(); - wrapper.StreamClaim("key", "group", "consumer", 1000, messageIds, CommandFlags.None); + prefixed.StreamClaim("key", "group", "consumer", 1000, messageIds, CommandFlags.None); mock.Verify(_ => _.StreamClaim("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); } @@ -1059,49 +1059,49 @@ public void StreamClaimMessages() public void StreamClaimMessagesReturningIds() { var messageIds = Array.Empty(); - wrapper.StreamClaimIdsOnly("key", "group", "consumer", 1000, messageIds, CommandFlags.None); + prefixed.StreamClaimIdsOnly("key", "group", "consumer", 1000, messageIds, CommandFlags.None); mock.Verify(_ => _.StreamClaimIdsOnly("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); } [Fact] public void StreamConsumerGroupSetPosition() { - wrapper.StreamConsumerGroupSetPosition("key", "group", StreamPosition.Beginning, CommandFlags.None); + prefixed.StreamConsumerGroupSetPosition("key", "group", StreamPosition.Beginning, CommandFlags.None); mock.Verify(_ => _.StreamConsumerGroupSetPosition("prefix:key", "group", StreamPosition.Beginning, CommandFlags.None)); } [Fact] public void StreamConsumerInfoGet() { - wrapper.StreamConsumerInfo("key", "group", CommandFlags.None); + prefixed.StreamConsumerInfo("key", "group", CommandFlags.None); mock.Verify(_ => _.StreamConsumerInfo("prefix:key", "group", CommandFlags.None)); } [Fact] public void StreamCreateConsumerGroup() { - wrapper.StreamCreateConsumerGroup("key", "group", StreamPosition.Beginning, false, CommandFlags.None); + prefixed.StreamCreateConsumerGroup("key", "group", StreamPosition.Beginning, false, CommandFlags.None); mock.Verify(_ => _.StreamCreateConsumerGroup("prefix:key", "group", StreamPosition.Beginning, false, CommandFlags.None)); } [Fact] public void StreamGroupInfoGet() { - wrapper.StreamGroupInfo("key", CommandFlags.None); + prefixed.StreamGroupInfo("key", CommandFlags.None); mock.Verify(_ => _.StreamGroupInfo("prefix:key", CommandFlags.None)); } [Fact] public void StreamInfoGet() { - wrapper.StreamInfo("key", CommandFlags.None); + prefixed.StreamInfo("key", CommandFlags.None); mock.Verify(_ => _.StreamInfo("prefix:key", CommandFlags.None)); } [Fact] public void StreamLength() { - wrapper.StreamLength("key", CommandFlags.None); + prefixed.StreamLength("key", CommandFlags.None); mock.Verify(_ => _.StreamLength("prefix:key", CommandFlags.None)); } @@ -1109,42 +1109,42 @@ public void StreamLength() public void StreamMessagesDelete() { var messageIds = Array.Empty(); - wrapper.StreamDelete("key", messageIds, CommandFlags.None); + prefixed.StreamDelete("key", messageIds, CommandFlags.None); mock.Verify(_ => _.StreamDelete("prefix:key", messageIds, CommandFlags.None)); } [Fact] public void StreamDeleteConsumer() { - wrapper.StreamDeleteConsumer("key", "group", "consumer", CommandFlags.None); + prefixed.StreamDeleteConsumer("key", "group", "consumer", CommandFlags.None); mock.Verify(_ => _.StreamDeleteConsumer("prefix:key", "group", "consumer", CommandFlags.None)); } [Fact] public void StreamDeleteConsumerGroup() { - wrapper.StreamDeleteConsumerGroup("key", "group", CommandFlags.None); + prefixed.StreamDeleteConsumerGroup("key", "group", CommandFlags.None); mock.Verify(_ => _.StreamDeleteConsumerGroup("prefix:key", "group", CommandFlags.None)); } [Fact] public void StreamPendingInfoGet() { - wrapper.StreamPending("key", "group", CommandFlags.None); + prefixed.StreamPending("key", "group", CommandFlags.None); mock.Verify(_ => _.StreamPending("prefix:key", "group", CommandFlags.None)); } [Fact] public void StreamPendingMessageInfoGet() { - wrapper.StreamPendingMessages("key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); + prefixed.StreamPendingMessages("key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); mock.Verify(_ => _.StreamPendingMessages("prefix:key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None)); } [Fact] public void StreamRange() { - wrapper.StreamRange("key", "-", "+", null, Order.Ascending, CommandFlags.None); + prefixed.StreamRange("key", "-", "+", null, Order.Ascending, CommandFlags.None); mock.Verify(_ => _.StreamRange("prefix:key", "-", "+", null, Order.Ascending, CommandFlags.None)); } @@ -1152,21 +1152,21 @@ public void StreamRange() public void StreamRead_1() { var streamPositions = Array.Empty(); - wrapper.StreamRead(streamPositions, null, CommandFlags.None); + prefixed.StreamRead(streamPositions, null, CommandFlags.None); mock.Verify(_ => _.StreamRead(streamPositions, null, CommandFlags.None)); } [Fact] public void StreamRead_2() { - wrapper.StreamRead("key", "0-0", null, CommandFlags.None); + prefixed.StreamRead("key", "0-0", null, CommandFlags.None); mock.Verify(_ => _.StreamRead("prefix:key", "0-0", null, CommandFlags.None)); } [Fact] public void StreamStreamReadGroup_1() { - wrapper.StreamReadGroup("key", "group", "consumer", "0-0", 10, false, CommandFlags.None); + prefixed.StreamReadGroup("key", "group", "consumer", "0-0", 10, false, CommandFlags.None); mock.Verify(_ => _.StreamReadGroup("prefix:key", "group", "consumer", "0-0", 10, false, CommandFlags.None)); } @@ -1174,42 +1174,42 @@ public void StreamStreamReadGroup_1() public void StreamStreamReadGroup_2() { var streamPositions = Array.Empty(); - wrapper.StreamReadGroup(streamPositions, "group", "consumer", 10, false, CommandFlags.None); + prefixed.StreamReadGroup(streamPositions, "group", "consumer", 10, false, CommandFlags.None); mock.Verify(_ => _.StreamReadGroup(streamPositions, "group", "consumer", 10, false, CommandFlags.None)); } [Fact] public void StreamTrim() { - wrapper.StreamTrim("key", 1000, true, CommandFlags.None); + prefixed.StreamTrim("key", 1000, true, CommandFlags.None); mock.Verify(_ => _.StreamTrim("prefix:key", 1000, true, CommandFlags.None)); } [Fact] public void StringAppend() { - wrapper.StringAppend("key", "value", CommandFlags.None); + prefixed.StringAppend("key", "value", CommandFlags.None); mock.Verify(_ => _.StringAppend("prefix:key", "value", CommandFlags.None)); } [Fact] public void StringBitCount() { - wrapper.StringBitCount("key", 123, 456, CommandFlags.None); + prefixed.StringBitCount("key", 123, 456, CommandFlags.None); mock.Verify(_ => _.StringBitCount("prefix:key", 123, 456, CommandFlags.None)); } [Fact] public void StringBitCount_2() { - wrapper.StringBitCount("key", 123, 456, StringIndexType.Byte, CommandFlags.None); + prefixed.StringBitCount("key", 123, 456, StringIndexType.Byte, CommandFlags.None); mock.Verify(_ => _.StringBitCount("prefix:key", 123, 456, StringIndexType.Byte, CommandFlags.None)); } [Fact] public void StringBitOperation_1() { - wrapper.StringBitOperation(Bitwise.Xor, "destination", "first", "second", CommandFlags.None); + prefixed.StringBitOperation(Bitwise.Xor, "destination", "first", "second", CommandFlags.None); mock.Verify(_ => _.StringBitOperation(Bitwise.Xor, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); } @@ -1218,42 +1218,42 @@ public void StringBitOperation_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.StringBitOperation(Bitwise.Xor, "destination", keys, CommandFlags.None); + prefixed.StringBitOperation(Bitwise.Xor, "destination", keys, CommandFlags.None); mock.Verify(_ => _.StringBitOperation(Bitwise.Xor, "prefix:destination", It.Is(valid), CommandFlags.None)); } [Fact] public void StringBitPosition() { - wrapper.StringBitPosition("key", true, 123, 456, CommandFlags.None); + prefixed.StringBitPosition("key", true, 123, 456, CommandFlags.None); mock.Verify(_ => _.StringBitPosition("prefix:key", true, 123, 456, CommandFlags.None)); } [Fact] public void StringBitPosition_2() { - wrapper.StringBitPosition("key", true, 123, 456, StringIndexType.Byte, CommandFlags.None); + prefixed.StringBitPosition("key", true, 123, 456, StringIndexType.Byte, CommandFlags.None); mock.Verify(_ => _.StringBitPosition("prefix:key", true, 123, 456, StringIndexType.Byte, CommandFlags.None)); } [Fact] public void StringDecrement_1() { - wrapper.StringDecrement("key", 123, CommandFlags.None); + prefixed.StringDecrement("key", 123, CommandFlags.None); mock.Verify(_ => _.StringDecrement("prefix:key", 123, CommandFlags.None)); } [Fact] public void StringDecrement_2() { - wrapper.StringDecrement("key", 1.23, CommandFlags.None); + prefixed.StringDecrement("key", 1.23, CommandFlags.None); mock.Verify(_ => _.StringDecrement("prefix:key", 1.23, CommandFlags.None)); } [Fact] public void StringGet_1() { - wrapper.StringGet("key", CommandFlags.None); + prefixed.StringGet("key", CommandFlags.None); mock.Verify(_ => _.StringGet("prefix:key", CommandFlags.None)); } @@ -1262,63 +1262,63 @@ public void StringGet_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.StringGet(keys, CommandFlags.None); + prefixed.StringGet(keys, CommandFlags.None); mock.Verify(_ => _.StringGet(It.Is(valid), CommandFlags.None)); } [Fact] public void StringGetBit() { - wrapper.StringGetBit("key", 123, CommandFlags.None); + prefixed.StringGetBit("key", 123, CommandFlags.None); mock.Verify(_ => _.StringGetBit("prefix:key", 123, CommandFlags.None)); } [Fact] public void StringGetRange() { - wrapper.StringGetRange("key", 123, 456, CommandFlags.None); + prefixed.StringGetRange("key", 123, 456, CommandFlags.None); mock.Verify(_ => _.StringGetRange("prefix:key", 123, 456, CommandFlags.None)); } [Fact] public void StringGetSet() { - wrapper.StringGetSet("key", "value", CommandFlags.None); + prefixed.StringGetSet("key", "value", CommandFlags.None); mock.Verify(_ => _.StringGetSet("prefix:key", "value", CommandFlags.None)); } [Fact] public void StringGetDelete() { - wrapper.StringGetDelete("key", CommandFlags.None); + prefixed.StringGetDelete("key", CommandFlags.None); mock.Verify(_ => _.StringGetDelete("prefix:key", CommandFlags.None)); } [Fact] public void StringGetWithExpiry() { - wrapper.StringGetWithExpiry("key", CommandFlags.None); + prefixed.StringGetWithExpiry("key", CommandFlags.None); mock.Verify(_ => _.StringGetWithExpiry("prefix:key", CommandFlags.None)); } [Fact] public void StringIncrement_1() { - wrapper.StringIncrement("key", 123, CommandFlags.None); + prefixed.StringIncrement("key", 123, CommandFlags.None); mock.Verify(_ => _.StringIncrement("prefix:key", 123, CommandFlags.None)); } [Fact] public void StringIncrement_2() { - wrapper.StringIncrement("key", 1.23, CommandFlags.None); + prefixed.StringIncrement("key", 1.23, CommandFlags.None); mock.Verify(_ => _.StringIncrement("prefix:key", 1.23, CommandFlags.None)); } [Fact] public void StringLength() { - wrapper.StringLength("key", CommandFlags.None); + prefixed.StringLength("key", CommandFlags.None); mock.Verify(_ => _.StringLength("prefix:key", CommandFlags.None)); } @@ -1326,7 +1326,7 @@ public void StringLength() public void StringSet_1() { TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.StringSet("key", "value", expiry, When.Exists, CommandFlags.None); + prefixed.StringSet("key", "value", expiry, When.Exists, CommandFlags.None); mock.Verify(_ => _.StringSet("prefix:key", "value", expiry, When.Exists, CommandFlags.None)); } @@ -1334,7 +1334,7 @@ public void StringSet_1() public void StringSet_2() { TimeSpan? expiry = null; - wrapper.StringSet("key", "value", expiry, true, When.Exists, CommandFlags.None); + prefixed.StringSet("key", "value", expiry, true, When.Exists, CommandFlags.None); mock.Verify(_ => _.StringSet("prefix:key", "value", expiry, true, When.Exists, CommandFlags.None)); } @@ -1343,7 +1343,7 @@ public void StringSet_3() { KeyValuePair[] values = new KeyValuePair[] { new KeyValuePair("a", "x"), new KeyValuePair("b", "y") }; Expression[], bool>> valid = _ => _.Length == 2 && _[0].Key == "prefix:a" && _[0].Value == "x" && _[1].Key == "prefix:b" && _[1].Value == "y"; - wrapper.StringSet(values, When.Exists, CommandFlags.None); + prefixed.StringSet(values, When.Exists, CommandFlags.None); mock.Verify(_ => _.StringSet(It.Is(valid), When.Exists, CommandFlags.None)); } @@ -1351,21 +1351,21 @@ public void StringSet_3() public void StringSet_Compat() { TimeSpan? expiry = null; - wrapper.StringSet("key", "value", expiry, When.Exists); + prefixed.StringSet("key", "value", expiry, When.Exists); mock.Verify(_ => _.StringSet("prefix:key", "value", expiry, When.Exists)); } [Fact] public void StringSetBit() { - wrapper.StringSetBit("key", 123, true, CommandFlags.None); + prefixed.StringSetBit("key", 123, true, CommandFlags.None); mock.Verify(_ => _.StringSetBit("prefix:key", 123, true, CommandFlags.None)); } [Fact] public void StringSetRange() { - wrapper.StringSetRange("key", 123, "value", CommandFlags.None); + prefixed.StringSetRange("key", 123, "value", CommandFlags.None); mock.Verify(_ => _.StringSetRange("prefix:key", 123, "value", CommandFlags.None)); } } diff --git a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs similarity index 73% rename from tests/StackExchange.Redis.Tests/WrapperBaseTests.cs rename to tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs index 2dc09247d..d5fea204c 100644 --- a/tests/StackExchange.Redis.Tests/WrapperBaseTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs @@ -11,42 +11,42 @@ namespace StackExchange.Redis.Tests { [Collection(nameof(MoqDependentCollection))] - public sealed class WrapperBaseTests + public sealed class KeyPrefixedTests { private readonly Mock mock; - private readonly WrapperBase wrapper; + private readonly KeyPrefixed prefixed; - public WrapperBaseTests() + public KeyPrefixedTests() { mock = new Mock(); - wrapper = new WrapperBase(mock.Object, Encoding.UTF8.GetBytes("prefix:")); + prefixed = new KeyPrefixed(mock.Object, Encoding.UTF8.GetBytes("prefix:")); } [Fact] public async Task DebugObjectAsync() { - await wrapper.DebugObjectAsync("key", CommandFlags.None); + await prefixed.DebugObjectAsync("key", CommandFlags.None); mock.Verify(_ => _.DebugObjectAsync("prefix:key", CommandFlags.None)); } [Fact] public void HashDecrementAsync_1() { - wrapper.HashDecrementAsync("key", "hashField", 123, CommandFlags.None); + prefixed.HashDecrementAsync("key", "hashField", 123, CommandFlags.None); mock.Verify(_ => _.HashDecrementAsync("prefix:key", "hashField", 123, CommandFlags.None)); } [Fact] public void HashDecrementAsync_2() { - wrapper.HashDecrementAsync("key", "hashField", 1.23, CommandFlags.None); + prefixed.HashDecrementAsync("key", "hashField", 1.23, CommandFlags.None); mock.Verify(_ => _.HashDecrementAsync("prefix:key", "hashField", 1.23, CommandFlags.None)); } [Fact] public void HashDeleteAsync_1() { - wrapper.HashDeleteAsync("key", "hashField", CommandFlags.None); + prefixed.HashDeleteAsync("key", "hashField", CommandFlags.None); mock.Verify(_ => _.HashDeleteAsync("prefix:key", "hashField", CommandFlags.None)); } @@ -54,28 +54,28 @@ public void HashDeleteAsync_1() public void HashDeleteAsync_2() { RedisValue[] hashFields = Array.Empty(); - wrapper.HashDeleteAsync("key", hashFields, CommandFlags.None); + prefixed.HashDeleteAsync("key", hashFields, CommandFlags.None); mock.Verify(_ => _.HashDeleteAsync("prefix:key", hashFields, CommandFlags.None)); } [Fact] public void HashExistsAsync() { - wrapper.HashExistsAsync("key", "hashField", CommandFlags.None); + prefixed.HashExistsAsync("key", "hashField", CommandFlags.None); mock.Verify(_ => _.HashExistsAsync("prefix:key", "hashField", CommandFlags.None)); } [Fact] public void HashGetAllAsync() { - wrapper.HashGetAllAsync("key", CommandFlags.None); + prefixed.HashGetAllAsync("key", CommandFlags.None); mock.Verify(_ => _.HashGetAllAsync("prefix:key", CommandFlags.None)); } [Fact] public void HashGetAsync_1() { - wrapper.HashGetAsync("key", "hashField", CommandFlags.None); + prefixed.HashGetAsync("key", "hashField", CommandFlags.None); mock.Verify(_ => _.HashGetAsync("prefix:key", "hashField", CommandFlags.None)); } @@ -83,35 +83,35 @@ public void HashGetAsync_1() public void HashGetAsync_2() { RedisValue[] hashFields = Array.Empty(); - wrapper.HashGetAsync("key", hashFields, CommandFlags.None); + prefixed.HashGetAsync("key", hashFields, CommandFlags.None); mock.Verify(_ => _.HashGetAsync("prefix:key", hashFields, CommandFlags.None)); } [Fact] public void HashIncrementAsync_1() { - wrapper.HashIncrementAsync("key", "hashField", 123, CommandFlags.None); + prefixed.HashIncrementAsync("key", "hashField", 123, CommandFlags.None); mock.Verify(_ => _.HashIncrementAsync("prefix:key", "hashField", 123, CommandFlags.None)); } [Fact] public void HashIncrementAsync_2() { - wrapper.HashIncrementAsync("key", "hashField", 1.23, CommandFlags.None); + prefixed.HashIncrementAsync("key", "hashField", 1.23, CommandFlags.None); mock.Verify(_ => _.HashIncrementAsync("prefix:key", "hashField", 1.23, CommandFlags.None)); } [Fact] public void HashKeysAsync() { - wrapper.HashKeysAsync("key", CommandFlags.None); + prefixed.HashKeysAsync("key", CommandFlags.None); mock.Verify(_ => _.HashKeysAsync("prefix:key", CommandFlags.None)); } [Fact] public void HashLengthAsync() { - wrapper.HashLengthAsync("key", CommandFlags.None); + prefixed.HashLengthAsync("key", CommandFlags.None); mock.Verify(_ => _.HashLengthAsync("prefix:key", CommandFlags.None)); } @@ -119,35 +119,35 @@ public void HashLengthAsync() public void HashSetAsync_1() { HashEntry[] hashFields = Array.Empty(); - wrapper.HashSetAsync("key", hashFields, CommandFlags.None); + prefixed.HashSetAsync("key", hashFields, CommandFlags.None); mock.Verify(_ => _.HashSetAsync("prefix:key", hashFields, CommandFlags.None)); } [Fact] public void HashSetAsync_2() { - wrapper.HashSetAsync("key", "hashField", "value", When.Exists, CommandFlags.None); + prefixed.HashSetAsync("key", "hashField", "value", When.Exists, CommandFlags.None); mock.Verify(_ => _.HashSetAsync("prefix:key", "hashField", "value", When.Exists, CommandFlags.None)); } [Fact] public void HashStringLengthAsync() { - wrapper.HashStringLengthAsync("key","field", CommandFlags.None); + prefixed.HashStringLengthAsync("key","field", CommandFlags.None); mock.Verify(_ => _.HashStringLengthAsync("prefix:key", "field", CommandFlags.None)); } [Fact] public void HashValuesAsync() { - wrapper.HashValuesAsync("key", CommandFlags.None); + prefixed.HashValuesAsync("key", CommandFlags.None); mock.Verify(_ => _.HashValuesAsync("prefix:key", CommandFlags.None)); } [Fact] public void HyperLogLogAddAsync_1() { - wrapper.HyperLogLogAddAsync("key", "value", CommandFlags.None); + prefixed.HyperLogLogAddAsync("key", "value", CommandFlags.None); mock.Verify(_ => _.HyperLogLogAddAsync("prefix:key", "value", CommandFlags.None)); } @@ -155,21 +155,21 @@ public void HyperLogLogAddAsync_1() public void HyperLogLogAddAsync_2() { var values = Array.Empty(); - wrapper.HyperLogLogAddAsync("key", values, CommandFlags.None); + prefixed.HyperLogLogAddAsync("key", values, CommandFlags.None); mock.Verify(_ => _.HyperLogLogAddAsync("prefix:key", values, CommandFlags.None)); } [Fact] public void HyperLogLogLengthAsync() { - wrapper.HyperLogLogLengthAsync("key", CommandFlags.None); + prefixed.HyperLogLogLengthAsync("key", CommandFlags.None); mock.Verify(_ => _.HyperLogLogLengthAsync("prefix:key", CommandFlags.None)); } [Fact] public void HyperLogLogMergeAsync_1() { - wrapper.HyperLogLogMergeAsync("destination", "first", "second", CommandFlags.None); + prefixed.HyperLogLogMergeAsync("destination", "first", "second", CommandFlags.None); mock.Verify(_ => _.HyperLogLogMergeAsync("prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); } @@ -178,35 +178,35 @@ public void HyperLogLogMergeAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.HyperLogLogMergeAsync("destination", keys, CommandFlags.None); + prefixed.HyperLogLogMergeAsync("destination", keys, CommandFlags.None); mock.Verify(_ => _.HyperLogLogMergeAsync("prefix:destination", It.Is(valid), CommandFlags.None)); } [Fact] public void IdentifyEndpointAsync() { - wrapper.IdentifyEndpointAsync("key", CommandFlags.None); + prefixed.IdentifyEndpointAsync("key", CommandFlags.None); mock.Verify(_ => _.IdentifyEndpointAsync("prefix:key", CommandFlags.None)); } [Fact] public void IsConnected() { - wrapper.IsConnected("key", CommandFlags.None); + prefixed.IsConnected("key", CommandFlags.None); mock.Verify(_ => _.IsConnected("prefix:key", CommandFlags.None)); } [Fact] public void KeyCopyAsync() { - wrapper.KeyCopyAsync("key", "destination", flags: CommandFlags.None); + prefixed.KeyCopyAsync("key", "destination", flags: CommandFlags.None); mock.Verify(_ => _.KeyCopyAsync("prefix:key", "prefix:destination", -1, false, CommandFlags.None)); } [Fact] public void KeyDeleteAsync_1() { - wrapper.KeyDeleteAsync("key", CommandFlags.None); + prefixed.KeyDeleteAsync("key", CommandFlags.None); mock.Verify(_ => _.KeyDeleteAsync("prefix:key", CommandFlags.None)); } @@ -215,29 +215,28 @@ public void KeyDeleteAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.KeyDeleteAsync(keys, CommandFlags.None); + prefixed.KeyDeleteAsync(keys, CommandFlags.None); mock.Verify(_ => _.KeyDeleteAsync(It.Is(valid), CommandFlags.None)); } [Fact] public void KeyDumpAsync() { - wrapper.KeyDumpAsync("key", CommandFlags.None); + prefixed.KeyDumpAsync("key", CommandFlags.None); mock.Verify(_ => _.KeyDumpAsync("prefix:key", CommandFlags.None)); } [Fact] public void KeyEncodingAsync() { - wrapper.KeyEncodingAsync("key", CommandFlags.None); + prefixed.KeyEncodingAsync("key", CommandFlags.None); mock.Verify(_ => _.KeyEncodingAsync("prefix:key", CommandFlags.None)); } - [Fact] public void KeyExistsAsync() { - wrapper.KeyExistsAsync("key", CommandFlags.None); + prefixed.KeyExistsAsync("key", CommandFlags.None); mock.Verify(_ => _.KeyExistsAsync("prefix:key", CommandFlags.None)); } @@ -245,7 +244,7 @@ public void KeyExistsAsync() public void KeyExpireAsync_1() { TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.KeyExpireAsync("key", expiry, CommandFlags.None); + prefixed.KeyExpireAsync("key", expiry, CommandFlags.None); mock.Verify(_ => _.KeyExpireAsync("prefix:key", expiry, CommandFlags.None)); } @@ -253,7 +252,7 @@ public void KeyExpireAsync_1() public void KeyExpireAsync_2() { DateTime expiry = DateTime.Now; - wrapper.KeyExpireAsync("key", expiry, CommandFlags.None); + prefixed.KeyExpireAsync("key", expiry, CommandFlags.None); mock.Verify(_ => _.KeyExpireAsync("prefix:key", expiry, CommandFlags.None)); } @@ -261,7 +260,7 @@ public void KeyExpireAsync_2() public void KeyExpireAsync_3() { TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.KeyExpireAsync("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); + prefixed.KeyExpireAsync("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); mock.Verify(_ => _.KeyExpireAsync("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); } @@ -269,21 +268,21 @@ public void KeyExpireAsync_3() public void KeyExpireAsync_4() { DateTime expiry = DateTime.Now; - wrapper.KeyExpireAsync("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); + prefixed.KeyExpireAsync("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); mock.Verify(_ => _.KeyExpireAsync("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); } [Fact] public void KeyExpireTimeAsync() { - wrapper.KeyExpireTimeAsync("key", CommandFlags.None); + prefixed.KeyExpireTimeAsync("key", CommandFlags.None); mock.Verify(_ => _.KeyExpireTimeAsync("prefix:key", CommandFlags.None)); } [Fact] public void KeyFrequencyAsync() { - wrapper.KeyFrequencyAsync("key", CommandFlags.None); + prefixed.KeyFrequencyAsync("key", CommandFlags.None); mock.Verify(_ => _.KeyFrequencyAsync("prefix:key", CommandFlags.None)); } @@ -291,41 +290,41 @@ public void KeyFrequencyAsync() public void KeyMigrateAsync() { EndPoint toServer = new IPEndPoint(IPAddress.Loopback, 123); - wrapper.KeyMigrateAsync("key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None); + prefixed.KeyMigrateAsync("key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None); mock.Verify(_ => _.KeyMigrateAsync("prefix:key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None)); } [Fact] public void KeyMoveAsync() { - wrapper.KeyMoveAsync("key", 123, CommandFlags.None); + prefixed.KeyMoveAsync("key", 123, CommandFlags.None); mock.Verify(_ => _.KeyMoveAsync("prefix:key", 123, CommandFlags.None)); } [Fact] public void KeyPersistAsync() { - wrapper.KeyPersistAsync("key", CommandFlags.None); + prefixed.KeyPersistAsync("key", CommandFlags.None); mock.Verify(_ => _.KeyPersistAsync("prefix:key", CommandFlags.None)); } [Fact] public Task KeyRandomAsync() { - return Assert.ThrowsAsync(() => wrapper.KeyRandomAsync()); + return Assert.ThrowsAsync(() => prefixed.KeyRandomAsync()); } [Fact] public void KeyRefCountAsync() { - wrapper.KeyRefCountAsync("key", CommandFlags.None); + prefixed.KeyRefCountAsync("key", CommandFlags.None); mock.Verify(_ => _.KeyRefCountAsync("prefix:key", CommandFlags.None)); } [Fact] public void KeyRenameAsync() { - wrapper.KeyRenameAsync("key", "newKey", When.Exists, CommandFlags.None); + prefixed.KeyRenameAsync("key", "newKey", When.Exists, CommandFlags.None); mock.Verify(_ => _.KeyRenameAsync("prefix:key", "prefix:newKey", When.Exists, CommandFlags.None)); } @@ -334,63 +333,63 @@ public void KeyRestoreAsync() { byte[] value = Array.Empty(); TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.KeyRestoreAsync("key", value, expiry, CommandFlags.None); + prefixed.KeyRestoreAsync("key", value, expiry, CommandFlags.None); mock.Verify(_ => _.KeyRestoreAsync("prefix:key", value, expiry, CommandFlags.None)); } [Fact] public void KeyTimeToLiveAsync() { - wrapper.KeyTimeToLiveAsync("key", CommandFlags.None); + prefixed.KeyTimeToLiveAsync("key", CommandFlags.None); mock.Verify(_ => _.KeyTimeToLiveAsync("prefix:key", CommandFlags.None)); } [Fact] public void KeyTypeAsync() { - wrapper.KeyTypeAsync("key", CommandFlags.None); + prefixed.KeyTypeAsync("key", CommandFlags.None); mock.Verify(_ => _.KeyTypeAsync("prefix:key", CommandFlags.None)); } [Fact] public void ListGetByIndexAsync() { - wrapper.ListGetByIndexAsync("key", 123, CommandFlags.None); + prefixed.ListGetByIndexAsync("key", 123, CommandFlags.None); mock.Verify(_ => _.ListGetByIndexAsync("prefix:key", 123, CommandFlags.None)); } [Fact] public void ListInsertAfterAsync() { - wrapper.ListInsertAfterAsync("key", "pivot", "value", CommandFlags.None); + prefixed.ListInsertAfterAsync("key", "pivot", "value", CommandFlags.None); mock.Verify(_ => _.ListInsertAfterAsync("prefix:key", "pivot", "value", CommandFlags.None)); } [Fact] public void ListInsertBeforeAsync() { - wrapper.ListInsertBeforeAsync("key", "pivot", "value", CommandFlags.None); + prefixed.ListInsertBeforeAsync("key", "pivot", "value", CommandFlags.None); mock.Verify(_ => _.ListInsertBeforeAsync("prefix:key", "pivot", "value", CommandFlags.None)); } [Fact] public void ListLeftPopAsync() { - wrapper.ListLeftPopAsync("key", CommandFlags.None); + prefixed.ListLeftPopAsync("key", CommandFlags.None); mock.Verify(_ => _.ListLeftPopAsync("prefix:key", CommandFlags.None)); } [Fact] public void ListLeftPopAsync_1() { - wrapper.ListLeftPopAsync("key", 123, CommandFlags.None); + prefixed.ListLeftPopAsync("key", 123, CommandFlags.None); mock.Verify(_ => _.ListLeftPopAsync("prefix:key", 123, CommandFlags.None)); } [Fact] public void ListLeftPushAsync_1() { - wrapper.ListLeftPushAsync("key", "value", When.Exists, CommandFlags.None); + prefixed.ListLeftPushAsync("key", "value", When.Exists, CommandFlags.None); mock.Verify(_ => _.ListLeftPushAsync("prefix:key", "value", When.Exists, CommandFlags.None)); } @@ -398,7 +397,7 @@ public void ListLeftPushAsync_1() public void ListLeftPushAsync_2() { RedisValue[] values = Array.Empty(); - wrapper.ListLeftPushAsync("key", values, CommandFlags.None); + prefixed.ListLeftPushAsync("key", values, CommandFlags.None); mock.Verify(_ => _.ListLeftPushAsync("prefix:key", values, CommandFlags.None)); } @@ -406,63 +405,63 @@ public void ListLeftPushAsync_2() public void ListLeftPushAsync_3() { RedisValue[] values = new RedisValue[] { "value1", "value2" }; - wrapper.ListLeftPushAsync("key", values, When.Exists, CommandFlags.None); + prefixed.ListLeftPushAsync("key", values, When.Exists, CommandFlags.None); mock.Verify(_ => _.ListLeftPushAsync("prefix:key", values, When.Exists, CommandFlags.None)); } [Fact] public void ListLengthAsync() { - wrapper.ListLengthAsync("key", CommandFlags.None); + prefixed.ListLengthAsync("key", CommandFlags.None); mock.Verify(_ => _.ListLengthAsync("prefix:key", CommandFlags.None)); } [Fact] public void ListMoveAsync() { - wrapper.ListMoveAsync("key", "destination", ListSide.Left, ListSide.Right, CommandFlags.None); + prefixed.ListMoveAsync("key", "destination", ListSide.Left, ListSide.Right, CommandFlags.None); mock.Verify(_ => _.ListMoveAsync("prefix:key", "prefix:destination", ListSide.Left, ListSide.Right, CommandFlags.None)); } [Fact] public void ListRangeAsync() { - wrapper.ListRangeAsync("key", 123, 456, CommandFlags.None); + prefixed.ListRangeAsync("key", 123, 456, CommandFlags.None); mock.Verify(_ => _.ListRangeAsync("prefix:key", 123, 456, CommandFlags.None)); } [Fact] public void ListRemoveAsync() { - wrapper.ListRemoveAsync("key", "value", 123, CommandFlags.None); + prefixed.ListRemoveAsync("key", "value", 123, CommandFlags.None); mock.Verify(_ => _.ListRemoveAsync("prefix:key", "value", 123, CommandFlags.None)); } [Fact] public void ListRightPopAsync() { - wrapper.ListRightPopAsync("key", CommandFlags.None); + prefixed.ListRightPopAsync("key", CommandFlags.None); mock.Verify(_ => _.ListRightPopAsync("prefix:key", CommandFlags.None)); } [Fact] public void ListRightPopAsync_1() { - wrapper.ListRightPopAsync("key", 123, CommandFlags.None); + prefixed.ListRightPopAsync("key", 123, CommandFlags.None); mock.Verify(_ => _.ListRightPopAsync("prefix:key", 123, CommandFlags.None)); } [Fact] public void ListRightPopLeftPushAsync() { - wrapper.ListRightPopLeftPushAsync("source", "destination", CommandFlags.None); + prefixed.ListRightPopLeftPushAsync("source", "destination", CommandFlags.None); mock.Verify(_ => _.ListRightPopLeftPushAsync("prefix:source", "prefix:destination", CommandFlags.None)); } [Fact] public void ListRightPushAsync_1() { - wrapper.ListRightPushAsync("key", "value", When.Exists, CommandFlags.None); + prefixed.ListRightPushAsync("key", "value", When.Exists, CommandFlags.None); mock.Verify(_ => _.ListRightPushAsync("prefix:key", "value", When.Exists, CommandFlags.None)); } @@ -470,7 +469,7 @@ public void ListRightPushAsync_1() public void ListRightPushAsync_2() { RedisValue[] values = Array.Empty(); - wrapper.ListRightPushAsync("key", values, CommandFlags.None); + prefixed.ListRightPushAsync("key", values, CommandFlags.None); mock.Verify(_ => _.ListRightPushAsync("prefix:key", values, CommandFlags.None)); } @@ -478,21 +477,21 @@ public void ListRightPushAsync_2() public void ListRightPushAsync_3() { RedisValue[] values = new RedisValue[] { "value1", "value2" }; - wrapper.ListRightPushAsync("key", values, When.Exists, CommandFlags.None); + prefixed.ListRightPushAsync("key", values, When.Exists, CommandFlags.None); mock.Verify(_ => _.ListRightPushAsync("prefix:key", values, When.Exists, CommandFlags.None)); } [Fact] public void ListSetByIndexAsync() { - wrapper.ListSetByIndexAsync("key", 123, "value", CommandFlags.None); + prefixed.ListSetByIndexAsync("key", 123, "value", CommandFlags.None); mock.Verify(_ => _.ListSetByIndexAsync("prefix:key", 123, "value", CommandFlags.None)); } [Fact] public void ListTrimAsync() { - wrapper.ListTrimAsync("key", 123, 456, CommandFlags.None); + prefixed.ListTrimAsync("key", 123, 456, CommandFlags.None); mock.Verify(_ => _.ListTrimAsync("prefix:key", 123, 456, CommandFlags.None)); } @@ -500,21 +499,21 @@ public void ListTrimAsync() public void LockExtendAsync() { TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.LockExtendAsync("key", "value", expiry, CommandFlags.None); + prefixed.LockExtendAsync("key", "value", expiry, CommandFlags.None); mock.Verify(_ => _.LockExtendAsync("prefix:key", "value", expiry, CommandFlags.None)); } [Fact] public void LockQueryAsync() { - wrapper.LockQueryAsync("key", CommandFlags.None); + prefixed.LockQueryAsync("key", CommandFlags.None); mock.Verify(_ => _.LockQueryAsync("prefix:key", CommandFlags.None)); } [Fact] public void LockReleaseAsync() { - wrapper.LockReleaseAsync("key", "value", CommandFlags.None); + prefixed.LockReleaseAsync("key", "value", CommandFlags.None); mock.Verify(_ => _.LockReleaseAsync("prefix:key", "value", CommandFlags.None)); } @@ -522,14 +521,14 @@ public void LockReleaseAsync() public void LockTakeAsync() { TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.LockTakeAsync("key", "value", expiry, CommandFlags.None); + prefixed.LockTakeAsync("key", "value", expiry, CommandFlags.None); mock.Verify(_ => _.LockTakeAsync("prefix:key", "value", expiry, CommandFlags.None)); } [Fact] public void PublishAsync() { - wrapper.PublishAsync("channel", "message", CommandFlags.None); + prefixed.PublishAsync("channel", "message", CommandFlags.None); mock.Verify(_ => _.PublishAsync("prefix:channel", "message", CommandFlags.None)); } @@ -540,7 +539,7 @@ public void ScriptEvaluateAsync_1() RedisValue[] values = Array.Empty(); RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.ScriptEvaluateAsync(hash, keys, values, CommandFlags.None); + prefixed.ScriptEvaluateAsync(hash, keys, values, CommandFlags.None); mock.Verify(_ => _.ScriptEvaluateAsync(hash, It.Is(valid), values, CommandFlags.None)); } @@ -550,14 +549,14 @@ public void ScriptEvaluateAsync_2() RedisValue[] values = Array.Empty(); RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.ScriptEvaluateAsync("script", keys, values, CommandFlags.None); + prefixed.ScriptEvaluateAsync("script", keys, values, CommandFlags.None); mock.Verify(_ => _.ScriptEvaluateAsync("script", It.Is(valid), values, CommandFlags.None)); } [Fact] public void SetAddAsync_1() { - wrapper.SetAddAsync("key", "value", CommandFlags.None); + prefixed.SetAddAsync("key", "value", CommandFlags.None); mock.Verify(_ => _.SetAddAsync("prefix:key", "value", CommandFlags.None)); } @@ -565,14 +564,14 @@ public void SetAddAsync_1() public void SetAddAsync_2() { RedisValue[] values = Array.Empty(); - wrapper.SetAddAsync("key", values, CommandFlags.None); + prefixed.SetAddAsync("key", values, CommandFlags.None); mock.Verify(_ => _.SetAddAsync("prefix:key", values, CommandFlags.None)); } [Fact] public void SetCombineAndStoreAsync_1() { - wrapper.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", "first", "second", CommandFlags.None); + prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", "first", "second", CommandFlags.None); mock.Verify(_ => _.SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); } @@ -581,14 +580,14 @@ public void SetCombineAndStoreAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", keys, CommandFlags.None); + prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", keys, CommandFlags.None); mock.Verify(_ => _.SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", It.Is(valid), CommandFlags.None)); } [Fact] public void SetCombineAsync_1() { - wrapper.SetCombineAsync(SetOperation.Intersect, "first", "second", CommandFlags.None); + prefixed.SetCombineAsync(SetOperation.Intersect, "first", "second", CommandFlags.None); mock.Verify(_ => _.SetCombineAsync(SetOperation.Intersect, "prefix:first", "prefix:second", CommandFlags.None)); } @@ -597,14 +596,14 @@ public void SetCombineAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.SetCombineAsync(SetOperation.Intersect, keys, CommandFlags.None); + prefixed.SetCombineAsync(SetOperation.Intersect, keys, CommandFlags.None); mock.Verify(_ => _.SetCombineAsync(SetOperation.Intersect, It.Is(valid), CommandFlags.None)); } [Fact] public void SetContainsAsync() { - wrapper.SetContainsAsync("key", "value", CommandFlags.None); + prefixed.SetContainsAsync("key", "value", CommandFlags.None); mock.Verify(_ => _.SetContainsAsync("prefix:key", "value", CommandFlags.None)); } @@ -612,7 +611,7 @@ public void SetContainsAsync() public void SetContainsAsync_2() { RedisValue[] values = new RedisValue[] { "value1", "value2" }; - wrapper.SetContainsAsync("key", values, CommandFlags.None); + prefixed.SetContainsAsync("key", values, CommandFlags.None); mock.Verify(_ => _.SetContainsAsync("prefix:key", values, CommandFlags.None)); } @@ -620,66 +619,66 @@ public void SetContainsAsync_2() public void SetIntersectionLengthAsync() { var keys = new RedisKey[] { "key1", "key2" }; - wrapper.SetIntersectionLengthAsync(keys); + prefixed.SetIntersectionLengthAsync(keys); mock.Verify(_ => _.SetIntersectionLengthAsync(keys, 0, CommandFlags.None)); } [Fact] public void SetLengthAsync() { - wrapper.SetLengthAsync("key", CommandFlags.None); + prefixed.SetLengthAsync("key", CommandFlags.None); mock.Verify(_ => _.SetLengthAsync("prefix:key", CommandFlags.None)); } [Fact] public void SetMembersAsync() { - wrapper.SetMembersAsync("key", CommandFlags.None); + prefixed.SetMembersAsync("key", CommandFlags.None); mock.Verify(_ => _.SetMembersAsync("prefix:key", CommandFlags.None)); } [Fact] public void SetMoveAsync() { - wrapper.SetMoveAsync("source", "destination", "value", CommandFlags.None); + prefixed.SetMoveAsync("source", "destination", "value", CommandFlags.None); mock.Verify(_ => _.SetMoveAsync("prefix:source", "prefix:destination", "value", CommandFlags.None)); } [Fact] public void SetPopAsync_1() { - wrapper.SetPopAsync("key", CommandFlags.None); + prefixed.SetPopAsync("key", CommandFlags.None); mock.Verify(_ => _.SetPopAsync("prefix:key", CommandFlags.None)); - wrapper.SetPopAsync("key", 5, CommandFlags.None); + prefixed.SetPopAsync("key", 5, CommandFlags.None); mock.Verify(_ => _.SetPopAsync("prefix:key", 5, CommandFlags.None)); } [Fact] public void SetPopAsync_2() { - wrapper.SetPopAsync("key", 5, CommandFlags.None); + prefixed.SetPopAsync("key", 5, CommandFlags.None); mock.Verify(_ => _.SetPopAsync("prefix:key", 5, CommandFlags.None)); } [Fact] public void SetRandomMemberAsync() { - wrapper.SetRandomMemberAsync("key", CommandFlags.None); + prefixed.SetRandomMemberAsync("key", CommandFlags.None); mock.Verify(_ => _.SetRandomMemberAsync("prefix:key", CommandFlags.None)); } [Fact] public void SetRandomMembersAsync() { - wrapper.SetRandomMembersAsync("key", 123, CommandFlags.None); + prefixed.SetRandomMembersAsync("key", 123, CommandFlags.None); mock.Verify(_ => _.SetRandomMembersAsync("prefix:key", 123, CommandFlags.None)); } [Fact] public void SetRemoveAsync_1() { - wrapper.SetRemoveAsync("key", "value", CommandFlags.None); + prefixed.SetRemoveAsync("key", "value", CommandFlags.None); mock.Verify(_ => _.SetRemoveAsync("prefix:key", "value", CommandFlags.None)); } @@ -687,7 +686,7 @@ public void SetRemoveAsync_1() public void SetRemoveAsync_2() { RedisValue[] values = Array.Empty(); - wrapper.SetRemoveAsync("key", values, CommandFlags.None); + prefixed.SetRemoveAsync("key", values, CommandFlags.None); mock.Verify(_ => _.SetRemoveAsync("prefix:key", values, CommandFlags.None)); } @@ -697,8 +696,8 @@ public void SortAndStoreAsync() RedisValue[] get = new RedisValue[] { "a", "#" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; - wrapper.SortAndStoreAsync("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); - wrapper.SortAndStoreAsync("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); + prefixed.SortAndStoreAsync("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); + prefixed.SortAndStoreAsync("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); mock.Verify(_ => _.SortAndStoreAsync("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", It.Is(valid), CommandFlags.None)); mock.Verify(_ => _.SortAndStoreAsync("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", It.Is(valid), CommandFlags.None)); @@ -710,8 +709,8 @@ public void SortAsync() RedisValue[] get = new RedisValue[] { "a", "#" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; - wrapper.SortAsync("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); - wrapper.SortAsync("key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); + prefixed.SortAsync("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); + prefixed.SortAsync("key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); mock.Verify(_ => _.SortAsync("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", It.Is(valid), CommandFlags.None)); mock.Verify(_ => _.SortAsync("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", It.Is(valid), CommandFlags.None)); @@ -720,7 +719,7 @@ public void SortAsync() [Fact] public void SortedSetAddAsync_1() { - wrapper.SortedSetAddAsync("key", "member", 1.23, When.Exists, CommandFlags.None); + prefixed.SortedSetAddAsync("key", "member", 1.23, When.Exists, CommandFlags.None); mock.Verify(_ => _.SortedSetAddAsync("prefix:key", "member", 1.23, When.Exists, CommandFlags.None)); } @@ -728,7 +727,7 @@ public void SortedSetAddAsync_1() public void SortedSetAddAsync_2() { SortedSetEntry[] values = Array.Empty(); - wrapper.SortedSetAddAsync("key", values, When.Exists, CommandFlags.None); + prefixed.SortedSetAddAsync("key", values, When.Exists, CommandFlags.None); mock.Verify(_ => _.SortedSetAddAsync("prefix:key", values, When.Exists, CommandFlags.None)); } @@ -736,7 +735,7 @@ public void SortedSetAddAsync_2() public void SortedSetAddAsync_3() { SortedSetEntry[] values = Array.Empty(); - wrapper.SortedSetAddAsync("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); + prefixed.SortedSetAddAsync("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); mock.Verify(_ => _.SortedSetAddAsync("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None)); } @@ -744,7 +743,7 @@ public void SortedSetAddAsync_3() public void SortedSetCombineAsync() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - wrapper.SortedSetCombineAsync(SetOperation.Intersect, keys); + prefixed.SortedSetCombineAsync(SetOperation.Intersect, keys); mock.Verify(_ => _.SortedSetCombineAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); } @@ -752,14 +751,14 @@ public void SortedSetCombineAsync() public void SortedSetCombineWithScoresAsync() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - wrapper.SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys); + prefixed.SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys); mock.Verify(_ => _.SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); } [Fact] public void SortedSetCombineAndStoreAsync_1() { - wrapper.SortedSetCombineAndStoreAsync(SetOperation.Intersect, "destination", "first", "second", Aggregate.Max, CommandFlags.None); + prefixed.SortedSetCombineAndStoreAsync(SetOperation.Intersect, "destination", "first", "second", Aggregate.Max, CommandFlags.None); mock.Verify(_ => _.SortedSetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", Aggregate.Max, CommandFlags.None)); } @@ -768,21 +767,21 @@ public void SortedSetCombineAndStoreAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", keys, CommandFlags.None); + prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", keys, CommandFlags.None); mock.Verify(_ => _.SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", It.Is(valid), CommandFlags.None)); } [Fact] public void SortedSetDecrementAsync() { - wrapper.SortedSetDecrementAsync("key", "member", 1.23, CommandFlags.None); + prefixed.SortedSetDecrementAsync("key", "member", 1.23, CommandFlags.None); mock.Verify(_ => _.SortedSetDecrementAsync("prefix:key", "member", 1.23, CommandFlags.None)); } [Fact] public void SortedSetIncrementAsync() { - wrapper.SortedSetIncrementAsync("key", "member", 1.23, CommandFlags.None); + prefixed.SortedSetIncrementAsync("key", "member", 1.23, CommandFlags.None); mock.Verify(_ => _.SortedSetIncrementAsync("prefix:key", "member", 1.23, CommandFlags.None)); } @@ -790,98 +789,98 @@ public void SortedSetIncrementAsync() public void SortedSetIntersectionLengthAsync() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - wrapper.SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None); + prefixed.SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None); mock.Verify(_ => _.SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None)); } [Fact] public void SortedSetLengthAsync() { - wrapper.SortedSetLengthAsync("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); + prefixed.SortedSetLengthAsync("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); mock.Verify(_ => _.SortedSetLengthAsync("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None)); } [Fact] public void SortedSetLengthByValueAsync() { - wrapper.SortedSetLengthByValueAsync("key", "min", "max", Exclude.Start, CommandFlags.None); + prefixed.SortedSetLengthByValueAsync("key", "min", "max", Exclude.Start, CommandFlags.None); mock.Verify(_ => _.SortedSetLengthByValueAsync("prefix:key", "min", "max", Exclude.Start, CommandFlags.None)); } [Fact] public void SortedSetRandomMemberAsync() { - wrapper.SortedSetRandomMemberAsync("key", CommandFlags.None); + prefixed.SortedSetRandomMemberAsync("key", CommandFlags.None); mock.Verify(_ => _.SortedSetRandomMemberAsync("prefix:key", CommandFlags.None)); } [Fact] public void SortedSetRandomMembersAsync() { - wrapper.SortedSetRandomMembersAsync("key", 2, CommandFlags.None); + prefixed.SortedSetRandomMembersAsync("key", 2, CommandFlags.None); mock.Verify(_ => _.SortedSetRandomMembersAsync("prefix:key", 2, CommandFlags.None)); } [Fact] public void SortedSetRandomMemberWithScoresAsync() { - wrapper.SortedSetRandomMembersWithScoresAsync("key", 2, CommandFlags.None); + prefixed.SortedSetRandomMembersWithScoresAsync("key", 2, CommandFlags.None); mock.Verify(_ => _.SortedSetRandomMembersWithScoresAsync("prefix:key", 2, CommandFlags.None)); } [Fact] public void SortedSetRangeByRankAsync() { - wrapper.SortedSetRangeByRankAsync("key", 123, 456, Order.Descending, CommandFlags.None); + prefixed.SortedSetRangeByRankAsync("key", 123, 456, Order.Descending, CommandFlags.None); mock.Verify(_ => _.SortedSetRangeByRankAsync("prefix:key", 123, 456, Order.Descending, CommandFlags.None)); } [Fact] public void SortedSetRangeByRankWithScoresAsync() { - wrapper.SortedSetRangeByRankWithScoresAsync("key", 123, 456, Order.Descending, CommandFlags.None); + prefixed.SortedSetRangeByRankWithScoresAsync("key", 123, 456, Order.Descending, CommandFlags.None); mock.Verify(_ => _.SortedSetRangeByRankWithScoresAsync("prefix:key", 123, 456, Order.Descending, CommandFlags.None)); } [Fact] public void SortedSetRangeByScoreAsync() { - wrapper.SortedSetRangeByScoreAsync("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); + prefixed.SortedSetRangeByScoreAsync("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); mock.Verify(_ => _.SortedSetRangeByScoreAsync("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); } [Fact] public void SortedSetRangeByScoreWithScoresAsync() { - wrapper.SortedSetRangeByScoreWithScoresAsync("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); + prefixed.SortedSetRangeByScoreWithScoresAsync("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); mock.Verify(_ => _.SortedSetRangeByScoreWithScoresAsync("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); } [Fact] public void SortedSetRangeByValueAsync() { - wrapper.SortedSetRangeByValueAsync("key", "min", "max", Exclude.Start, 123, 456, CommandFlags.None); + prefixed.SortedSetRangeByValueAsync("key", "min", "max", Exclude.Start, 123, 456, CommandFlags.None); mock.Verify(_ => _.SortedSetRangeByValueAsync("prefix:key", "min", "max", Exclude.Start, Order.Ascending, 123, 456, CommandFlags.None)); } [Fact] public void SortedSetRangeByValueDescAsync() { - wrapper.SortedSetRangeByValueAsync("key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); + prefixed.SortedSetRangeByValueAsync("key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); mock.Verify(_ => _.SortedSetRangeByValueAsync("prefix:key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); } [Fact] public void SortedSetRankAsync() { - wrapper.SortedSetRankAsync("key", "member", Order.Descending, CommandFlags.None); + prefixed.SortedSetRankAsync("key", "member", Order.Descending, CommandFlags.None); mock.Verify(_ => _.SortedSetRankAsync("prefix:key", "member", Order.Descending, CommandFlags.None)); } [Fact] public void SortedSetRemoveAsync_1() { - wrapper.SortedSetRemoveAsync("key", "member", CommandFlags.None); + prefixed.SortedSetRemoveAsync("key", "member", CommandFlags.None); mock.Verify(_ => _.SortedSetRemoveAsync("prefix:key", "member", CommandFlags.None)); } @@ -889,42 +888,42 @@ public void SortedSetRemoveAsync_1() public void SortedSetRemoveAsync_2() { RedisValue[] members = Array.Empty(); - wrapper.SortedSetRemoveAsync("key", members, CommandFlags.None); + prefixed.SortedSetRemoveAsync("key", members, CommandFlags.None); mock.Verify(_ => _.SortedSetRemoveAsync("prefix:key", members, CommandFlags.None)); } [Fact] public void SortedSetRemoveRangeByRankAsync() { - wrapper.SortedSetRemoveRangeByRankAsync("key", 123, 456, CommandFlags.None); + prefixed.SortedSetRemoveRangeByRankAsync("key", 123, 456, CommandFlags.None); mock.Verify(_ => _.SortedSetRemoveRangeByRankAsync("prefix:key", 123, 456, CommandFlags.None)); } [Fact] public void SortedSetRemoveRangeByScoreAsync() { - wrapper.SortedSetRemoveRangeByScoreAsync("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); + prefixed.SortedSetRemoveRangeByScoreAsync("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); mock.Verify(_ => _.SortedSetRemoveRangeByScoreAsync("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None)); } [Fact] public void SortedSetRemoveRangeByValueAsync() { - wrapper.SortedSetRemoveRangeByValueAsync("key", "min", "max", Exclude.Start, CommandFlags.None); + prefixed.SortedSetRemoveRangeByValueAsync("key", "min", "max", Exclude.Start, CommandFlags.None); mock.Verify(_ => _.SortedSetRemoveRangeByValueAsync("prefix:key", "min", "max", Exclude.Start, CommandFlags.None)); } [Fact] public void SortedSetScoreAsync() { - wrapper.SortedSetScoreAsync("key", "member", CommandFlags.None); + prefixed.SortedSetScoreAsync("key", "member", CommandFlags.None); mock.Verify(_ => _.SortedSetScoreAsync("prefix:key", "member", CommandFlags.None)); } [Fact] public void SortedSetScoreAsync_Multiple() { - wrapper.SortedSetScoresAsync("key", new RedisValue[] { "member1", "member2" }, CommandFlags.None); + prefixed.SortedSetScoresAsync("key", new RedisValue[] { "member1", "member2" }, CommandFlags.None); mock.Verify(_ => _.SortedSetScoresAsync("prefix:key", new RedisValue[] { "member1", "member2" }, CommandFlags.None)); } @@ -932,14 +931,14 @@ public void SortedSetScoreAsync_Multiple() public void SortedSetUpdateAsync() { SortedSetEntry[] values = Array.Empty(); - wrapper.SortedSetUpdateAsync("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); + prefixed.SortedSetUpdateAsync("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); mock.Verify(_ => _.SortedSetUpdateAsync("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None)); } [Fact] public void StreamAcknowledgeAsync_1() { - wrapper.StreamAcknowledgeAsync("key", "group", "0-0", CommandFlags.None); + prefixed.StreamAcknowledgeAsync("key", "group", "0-0", CommandFlags.None); mock.Verify(_ => _.StreamAcknowledgeAsync("prefix:key", "group", "0-0", CommandFlags.None)); } @@ -947,14 +946,14 @@ public void StreamAcknowledgeAsync_1() public void StreamAcknowledgeAsync_2() { var messageIds = new RedisValue[] { "0-0", "0-1", "0-2" }; - wrapper.StreamAcknowledgeAsync("key", "group", messageIds, CommandFlags.None); + prefixed.StreamAcknowledgeAsync("key", "group", messageIds, CommandFlags.None); mock.Verify(_ => _.StreamAcknowledgeAsync("prefix:key", "group", messageIds, CommandFlags.None)); } [Fact] public void StreamAddAsync_1() { - wrapper.StreamAddAsync("key", "field1", "value1", "*", 1000, true, CommandFlags.None); + prefixed.StreamAddAsync("key", "field1", "value1", "*", 1000, true, CommandFlags.None); mock.Verify(_ => _.StreamAddAsync("prefix:key", "field1", "value1", "*", 1000, true, CommandFlags.None)); } @@ -962,21 +961,21 @@ public void StreamAddAsync_1() public void StreamAddAsync_2() { var fields = Array.Empty(); - wrapper.StreamAddAsync("key", fields, "*", 1000, true, CommandFlags.None); + prefixed.StreamAddAsync("key", fields, "*", 1000, true, CommandFlags.None); mock.Verify(_ => _.StreamAddAsync("prefix:key", fields, "*", 1000, true, CommandFlags.None)); } [Fact] public void StreamAutoClaimAsync() { - wrapper.StreamAutoClaimAsync("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); + prefixed.StreamAutoClaimAsync("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); mock.Verify(_ => _.StreamAutoClaimAsync("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None)); } [Fact] public void StreamAutoClaimIdsOnlyAsync() { - wrapper.StreamAutoClaimIdsOnlyAsync("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); + prefixed.StreamAutoClaimIdsOnlyAsync("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); mock.Verify(_ => _.StreamAutoClaimIdsOnlyAsync("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None)); } @@ -984,7 +983,7 @@ public void StreamAutoClaimIdsOnlyAsync() public void StreamClaimMessagesAsync() { var messageIds = Array.Empty(); - wrapper.StreamClaimAsync("key", "group", "consumer", 1000, messageIds, CommandFlags.None); + prefixed.StreamClaimAsync("key", "group", "consumer", 1000, messageIds, CommandFlags.None); mock.Verify(_ => _.StreamClaimAsync("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); } @@ -992,49 +991,49 @@ public void StreamClaimMessagesAsync() public void StreamClaimMessagesReturningIdsAsync() { var messageIds = Array.Empty(); - wrapper.StreamClaimIdsOnlyAsync("key", "group", "consumer", 1000, messageIds, CommandFlags.None); + prefixed.StreamClaimIdsOnlyAsync("key", "group", "consumer", 1000, messageIds, CommandFlags.None); mock.Verify(_ => _.StreamClaimIdsOnlyAsync("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); } [Fact] public void StreamConsumerInfoGetAsync() { - wrapper.StreamConsumerInfoAsync("key", "group", CommandFlags.None); + prefixed.StreamConsumerInfoAsync("key", "group", CommandFlags.None); mock.Verify(_ => _.StreamConsumerInfoAsync("prefix:key", "group", CommandFlags.None)); } [Fact] public void StreamConsumerGroupSetPositionAsync() { - wrapper.StreamConsumerGroupSetPositionAsync("key", "group", StreamPosition.Beginning, CommandFlags.None); + prefixed.StreamConsumerGroupSetPositionAsync("key", "group", StreamPosition.Beginning, CommandFlags.None); mock.Verify(_ => _.StreamConsumerGroupSetPositionAsync("prefix:key", "group", StreamPosition.Beginning, CommandFlags.None)); } [Fact] public void StreamCreateConsumerGroupAsync() { - wrapper.StreamCreateConsumerGroupAsync("key", "group", "0-0", false, CommandFlags.None); + prefixed.StreamCreateConsumerGroupAsync("key", "group", "0-0", false, CommandFlags.None); mock.Verify(_ => _.StreamCreateConsumerGroupAsync("prefix:key", "group", "0-0", false, CommandFlags.None)); } [Fact] public void StreamGroupInfoGetAsync() { - wrapper.StreamGroupInfoAsync("key", CommandFlags.None); + prefixed.StreamGroupInfoAsync("key", CommandFlags.None); mock.Verify(_ => _.StreamGroupInfoAsync("prefix:key", CommandFlags.None)); } [Fact] public void StreamInfoGetAsync() { - wrapper.StreamInfoAsync("key", CommandFlags.None); + prefixed.StreamInfoAsync("key", CommandFlags.None); mock.Verify(_ => _.StreamInfoAsync("prefix:key", CommandFlags.None)); } [Fact] public void StreamLengthAsync() { - wrapper.StreamLengthAsync("key", CommandFlags.None); + prefixed.StreamLengthAsync("key", CommandFlags.None); mock.Verify(_ => _.StreamLengthAsync("prefix:key", CommandFlags.None)); } @@ -1042,42 +1041,42 @@ public void StreamLengthAsync() public void StreamMessagesDeleteAsync() { var messageIds = Array.Empty(); - wrapper.StreamDeleteAsync("key", messageIds, CommandFlags.None); + prefixed.StreamDeleteAsync("key", messageIds, CommandFlags.None); mock.Verify(_ => _.StreamDeleteAsync("prefix:key", messageIds, CommandFlags.None)); } [Fact] public void StreamDeleteConsumerAsync() { - wrapper.StreamDeleteConsumerAsync("key", "group", "consumer", CommandFlags.None); + prefixed.StreamDeleteConsumerAsync("key", "group", "consumer", CommandFlags.None); mock.Verify(_ => _.StreamDeleteConsumerAsync("prefix:key", "group", "consumer", CommandFlags.None)); } [Fact] public void StreamDeleteConsumerGroupAsync() { - wrapper.StreamDeleteConsumerGroupAsync("key", "group", CommandFlags.None); + prefixed.StreamDeleteConsumerGroupAsync("key", "group", CommandFlags.None); mock.Verify(_ => _.StreamDeleteConsumerGroupAsync("prefix:key", "group", CommandFlags.None)); } [Fact] public void StreamPendingInfoGetAsync() { - wrapper.StreamPendingAsync("key", "group", CommandFlags.None); + prefixed.StreamPendingAsync("key", "group", CommandFlags.None); mock.Verify(_ => _.StreamPendingAsync("prefix:key", "group", CommandFlags.None)); } [Fact] public void StreamPendingMessageInfoGetAsync() { - wrapper.StreamPendingMessagesAsync("key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); + prefixed.StreamPendingMessagesAsync("key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); mock.Verify(_ => _.StreamPendingMessagesAsync("prefix:key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None)); } [Fact] public void StreamRangeAsync() { - wrapper.StreamRangeAsync("key", "-", "+", null, Order.Ascending, CommandFlags.None); + prefixed.StreamRangeAsync("key", "-", "+", null, Order.Ascending, CommandFlags.None); mock.Verify(_ => _.StreamRangeAsync("prefix:key", "-", "+", null, Order.Ascending, CommandFlags.None)); } @@ -1085,21 +1084,21 @@ public void StreamRangeAsync() public void StreamReadAsync_1() { var streamPositions = Array.Empty(); - wrapper.StreamReadAsync(streamPositions, null, CommandFlags.None); + prefixed.StreamReadAsync(streamPositions, null, CommandFlags.None); mock.Verify(_ => _.StreamReadAsync(streamPositions, null, CommandFlags.None)); } [Fact] public void StreamReadAsync_2() { - wrapper.StreamReadAsync("key", "0-0", null, CommandFlags.None); + prefixed.StreamReadAsync("key", "0-0", null, CommandFlags.None); mock.Verify(_ => _.StreamReadAsync("prefix:key", "0-0", null, CommandFlags.None)); } [Fact] public void StreamReadGroupAsync_1() { - wrapper.StreamReadGroupAsync("key", "group", "consumer", StreamPosition.Beginning, 10, false, CommandFlags.None); + prefixed.StreamReadGroupAsync("key", "group", "consumer", StreamPosition.Beginning, 10, false, CommandFlags.None); mock.Verify(_ => _.StreamReadGroupAsync("prefix:key", "group", "consumer", StreamPosition.Beginning, 10, false, CommandFlags.None)); } @@ -1107,42 +1106,42 @@ public void StreamReadGroupAsync_1() public void StreamStreamReadGroupAsync_2() { var streamPositions = Array.Empty(); - wrapper.StreamReadGroupAsync(streamPositions, "group", "consumer", 10, false, CommandFlags.None); + prefixed.StreamReadGroupAsync(streamPositions, "group", "consumer", 10, false, CommandFlags.None); mock.Verify(_ => _.StreamReadGroupAsync(streamPositions, "group", "consumer", 10, false, CommandFlags.None)); } [Fact] public void StreamTrimAsync() { - wrapper.StreamTrimAsync("key", 1000, true, CommandFlags.None); + prefixed.StreamTrimAsync("key", 1000, true, CommandFlags.None); mock.Verify(_ => _.StreamTrimAsync("prefix:key", 1000, true, CommandFlags.None)); } [Fact] public void StringAppendAsync() { - wrapper.StringAppendAsync("key", "value", CommandFlags.None); + prefixed.StringAppendAsync("key", "value", CommandFlags.None); mock.Verify(_ => _.StringAppendAsync("prefix:key", "value", CommandFlags.None)); } [Fact] public void StringBitCountAsync() { - wrapper.StringBitCountAsync("key", 123, 456, CommandFlags.None); + prefixed.StringBitCountAsync("key", 123, 456, CommandFlags.None); mock.Verify(_ => _.StringBitCountAsync("prefix:key", 123, 456, CommandFlags.None)); } [Fact] public void StringBitCountAsync_2() { - wrapper.StringBitCountAsync("key", 123, 456, StringIndexType.Byte, CommandFlags.None); + prefixed.StringBitCountAsync("key", 123, 456, StringIndexType.Byte, CommandFlags.None); mock.Verify(_ => _.StringBitCountAsync("prefix:key", 123, 456, StringIndexType.Byte, CommandFlags.None)); } [Fact] public void StringBitOperationAsync_1() { - wrapper.StringBitOperationAsync(Bitwise.Xor, "destination", "first", "second", CommandFlags.None); + prefixed.StringBitOperationAsync(Bitwise.Xor, "destination", "first", "second", CommandFlags.None); mock.Verify(_ => _.StringBitOperationAsync(Bitwise.Xor, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); } @@ -1151,42 +1150,42 @@ public void StringBitOperationAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.StringBitOperationAsync(Bitwise.Xor, "destination", keys, CommandFlags.None); + prefixed.StringBitOperationAsync(Bitwise.Xor, "destination", keys, CommandFlags.None); mock.Verify(_ => _.StringBitOperationAsync(Bitwise.Xor, "prefix:destination", It.Is(valid), CommandFlags.None)); } [Fact] public void StringBitPositionAsync() { - wrapper.StringBitPositionAsync("key", true, 123, 456, CommandFlags.None); + prefixed.StringBitPositionAsync("key", true, 123, 456, CommandFlags.None); mock.Verify(_ => _.StringBitPositionAsync("prefix:key", true, 123, 456, CommandFlags.None)); } [Fact] public void StringBitPositionAsync_2() { - wrapper.StringBitPositionAsync("key", true, 123, 456, StringIndexType.Byte, CommandFlags.None); + prefixed.StringBitPositionAsync("key", true, 123, 456, StringIndexType.Byte, CommandFlags.None); mock.Verify(_ => _.StringBitPositionAsync("prefix:key", true, 123, 456, StringIndexType.Byte, CommandFlags.None)); } [Fact] public void StringDecrementAsync_1() { - wrapper.StringDecrementAsync("key", 123, CommandFlags.None); + prefixed.StringDecrementAsync("key", 123, CommandFlags.None); mock.Verify(_ => _.StringDecrementAsync("prefix:key", 123, CommandFlags.None)); } [Fact] public void StringDecrementAsync_2() { - wrapper.StringDecrementAsync("key", 1.23, CommandFlags.None); + prefixed.StringDecrementAsync("key", 1.23, CommandFlags.None); mock.Verify(_ => _.StringDecrementAsync("prefix:key", 1.23, CommandFlags.None)); } [Fact] public void StringGetAsync_1() { - wrapper.StringGetAsync("key", CommandFlags.None); + prefixed.StringGetAsync("key", CommandFlags.None); mock.Verify(_ => _.StringGetAsync("prefix:key", CommandFlags.None)); } @@ -1195,63 +1194,63 @@ public void StringGetAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.StringGetAsync(keys, CommandFlags.None); + prefixed.StringGetAsync(keys, CommandFlags.None); mock.Verify(_ => _.StringGetAsync(It.Is(valid), CommandFlags.None)); } [Fact] public void StringGetBitAsync() { - wrapper.StringGetBitAsync("key", 123, CommandFlags.None); + prefixed.StringGetBitAsync("key", 123, CommandFlags.None); mock.Verify(_ => _.StringGetBitAsync("prefix:key", 123, CommandFlags.None)); } [Fact] public void StringGetRangeAsync() { - wrapper.StringGetRangeAsync("key", 123, 456, CommandFlags.None); + prefixed.StringGetRangeAsync("key", 123, 456, CommandFlags.None); mock.Verify(_ => _.StringGetRangeAsync("prefix:key", 123, 456, CommandFlags.None)); } [Fact] public void StringGetSetAsync() { - wrapper.StringGetSetAsync("key", "value", CommandFlags.None); + prefixed.StringGetSetAsync("key", "value", CommandFlags.None); mock.Verify(_ => _.StringGetSetAsync("prefix:key", "value", CommandFlags.None)); } [Fact] public void StringGetDeleteAsync() { - wrapper.StringGetDeleteAsync("key", CommandFlags.None); + prefixed.StringGetDeleteAsync("key", CommandFlags.None); mock.Verify(_ => _.StringGetDeleteAsync("prefix:key", CommandFlags.None)); } [Fact] public void StringGetWithExpiryAsync() { - wrapper.StringGetWithExpiryAsync("key", CommandFlags.None); + prefixed.StringGetWithExpiryAsync("key", CommandFlags.None); mock.Verify(_ => _.StringGetWithExpiryAsync("prefix:key", CommandFlags.None)); } [Fact] public void StringIncrementAsync_1() { - wrapper.StringIncrementAsync("key", 123, CommandFlags.None); + prefixed.StringIncrementAsync("key", 123, CommandFlags.None); mock.Verify(_ => _.StringIncrementAsync("prefix:key", 123, CommandFlags.None)); } [Fact] public void StringIncrementAsync_2() { - wrapper.StringIncrementAsync("key", 1.23, CommandFlags.None); + prefixed.StringIncrementAsync("key", 1.23, CommandFlags.None); mock.Verify(_ => _.StringIncrementAsync("prefix:key", 1.23, CommandFlags.None)); } [Fact] public void StringLengthAsync() { - wrapper.StringLengthAsync("key", CommandFlags.None); + prefixed.StringLengthAsync("key", CommandFlags.None); mock.Verify(_ => _.StringLengthAsync("prefix:key", CommandFlags.None)); } @@ -1259,7 +1258,7 @@ public void StringLengthAsync() public void StringSetAsync_1() { TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.StringSetAsync("key", "value", expiry, When.Exists, CommandFlags.None); + prefixed.StringSetAsync("key", "value", expiry, When.Exists, CommandFlags.None); mock.Verify(_ => _.StringSetAsync("prefix:key", "value", expiry, When.Exists, CommandFlags.None)); } @@ -1267,7 +1266,7 @@ public void StringSetAsync_1() public void StringSetAsync_2() { TimeSpan? expiry = null; - wrapper.StringSetAsync("key", "value", expiry, true, When.Exists, CommandFlags.None); + prefixed.StringSetAsync("key", "value", expiry, true, When.Exists, CommandFlags.None); mock.Verify(_ => _.StringSetAsync("prefix:key", "value", expiry, true, When.Exists, CommandFlags.None)); } @@ -1276,7 +1275,7 @@ public void StringSetAsync_3() { KeyValuePair[] values = new KeyValuePair[] { new KeyValuePair("a", "x"), new KeyValuePair("b", "y") }; Expression[], bool>> valid = _ => _.Length == 2 && _[0].Key == "prefix:a" && _[0].Value == "x" && _[1].Key == "prefix:b" && _[1].Value == "y"; - wrapper.StringSetAsync(values, When.Exists, CommandFlags.None); + prefixed.StringSetAsync(values, When.Exists, CommandFlags.None); mock.Verify(_ => _.StringSetAsync(It.Is(valid), When.Exists, CommandFlags.None)); } @@ -1284,28 +1283,28 @@ public void StringSetAsync_3() public void StringSetAsync_Compat() { TimeSpan expiry = TimeSpan.FromSeconds(123); - wrapper.StringSetAsync("key", "value", expiry, When.Exists); + prefixed.StringSetAsync("key", "value", expiry, When.Exists); mock.Verify(_ => _.StringSetAsync("prefix:key", "value", expiry, When.Exists)); } [Fact] public void StringSetBitAsync() { - wrapper.StringSetBitAsync("key", 123, true, CommandFlags.None); + prefixed.StringSetBitAsync("key", 123, true, CommandFlags.None); mock.Verify(_ => _.StringSetBitAsync("prefix:key", 123, true, CommandFlags.None)); } [Fact] public void StringSetRangeAsync() { - wrapper.StringSetRangeAsync("key", 123, "value", CommandFlags.None); + prefixed.StringSetRangeAsync("key", 123, "value", CommandFlags.None); mock.Verify(_ => _.StringSetRangeAsync("prefix:key", 123, "value", CommandFlags.None)); } [Fact] public void KeyTouchAsync_1() { - wrapper.KeyTouchAsync("key", CommandFlags.None); + prefixed.KeyTouchAsync("key", CommandFlags.None); mock.Verify(_ => _.KeyTouchAsync("prefix:key", CommandFlags.None)); } @@ -1314,7 +1313,7 @@ public void KeyTouchAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - wrapper.KeyTouchAsync(keys, CommandFlags.None); + prefixed.KeyTouchAsync(keys, CommandFlags.None); mock.Verify(_ => _.KeyTouchAsync(It.Is(valid), CommandFlags.None)); } } diff --git a/tests/StackExchange.Redis.Tests/TransactionWrapperTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedTransactionTests.cs similarity index 70% rename from tests/StackExchange.Redis.Tests/TransactionWrapperTests.cs rename to tests/StackExchange.Redis.Tests/KeyPrefixedTransactionTests.cs index 9fccd26d8..e8af40c6e 100644 --- a/tests/StackExchange.Redis.Tests/TransactionWrapperTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedTransactionTests.cs @@ -7,126 +7,126 @@ namespace StackExchange.Redis.Tests; [Collection(nameof(MoqDependentCollection))] -public sealed class TransactionWrapperTests +public sealed class KeyPrefixedTransactionTests { private readonly Mock mock; - private readonly TransactionWrapper wrapper; + private readonly KeyPrefixedTransaction prefixed; - public TransactionWrapperTests() + public KeyPrefixedTransactionTests() { mock = new Mock(); - wrapper = new TransactionWrapper(mock.Object, Encoding.UTF8.GetBytes("prefix:")); + prefixed = new KeyPrefixedTransaction(mock.Object, Encoding.UTF8.GetBytes("prefix:")); } [Fact] public void AddCondition_HashEqual() { - wrapper.AddCondition(Condition.HashEqual("key", "field", "value")); + prefixed.AddCondition(Condition.HashEqual("key", "field", "value")); mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field == value" == value.ToString()))); } [Fact] public void AddCondition_HashNotEqual() { - wrapper.AddCondition(Condition.HashNotEqual("key", "field", "value")); + prefixed.AddCondition(Condition.HashNotEqual("key", "field", "value")); mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field != value" == value.ToString()))); } [Fact] public void AddCondition_HashExists() { - wrapper.AddCondition(Condition.HashExists("key", "field")); + prefixed.AddCondition(Condition.HashExists("key", "field")); mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field exists" == value.ToString()))); } [Fact] public void AddCondition_HashNotExists() { - wrapper.AddCondition(Condition.HashNotExists("key", "field")); + prefixed.AddCondition(Condition.HashNotExists("key", "field")); mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field does not exists" == value.ToString()))); } [Fact] public void AddCondition_KeyExists() { - wrapper.AddCondition(Condition.KeyExists("key")); + prefixed.AddCondition(Condition.KeyExists("key")); mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key exists" == value.ToString()))); } [Fact] public void AddCondition_KeyNotExists() { - wrapper.AddCondition(Condition.KeyNotExists("key")); + prefixed.AddCondition(Condition.KeyNotExists("key")); mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key does not exists" == value.ToString()))); } [Fact] public void AddCondition_StringEqual() { - wrapper.AddCondition(Condition.StringEqual("key", "value")); + prefixed.AddCondition(Condition.StringEqual("key", "value")); mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key == value" == value.ToString()))); } [Fact] public void AddCondition_StringNotEqual() { - wrapper.AddCondition(Condition.StringNotEqual("key", "value")); + prefixed.AddCondition(Condition.StringNotEqual("key", "value")); mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key != value" == value.ToString()))); } [Fact] public void AddCondition_SortedSetEqual() { - wrapper.AddCondition(Condition.SortedSetEqual("key", "member", "score")); + prefixed.AddCondition(Condition.SortedSetEqual("key", "member", "score")); mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key SortedSet > member == score" == value.ToString()))); } [Fact] public void AddCondition_SortedSetNotEqual() { - wrapper.AddCondition(Condition.SortedSetNotEqual("key", "member", "score")); + prefixed.AddCondition(Condition.SortedSetNotEqual("key", "member", "score")); mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key SortedSet > member != score" == value.ToString()))); } [Fact] public void AddCondition_SortedSetScoreExists() { - wrapper.AddCondition(Condition.SortedSetScoreExists("key", "score")); + prefixed.AddCondition(Condition.SortedSetScoreExists("key", "score")); mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key not contains 0 members with score: score" == value.ToString()))); } [Fact] public void AddCondition_SortedSetScoreNotExists() { - wrapper.AddCondition(Condition.SortedSetScoreNotExists("key", "score")); + prefixed.AddCondition(Condition.SortedSetScoreNotExists("key", "score")); mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key contains 0 members with score: score" == value.ToString()))); } [Fact] public void AddCondition_SortedSetScoreCountExists() { - wrapper.AddCondition(Condition.SortedSetScoreExists("key", "score", "count")); + prefixed.AddCondition(Condition.SortedSetScoreExists("key", "score", "count")); mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key contains count members with score: score" == value.ToString()))); } [Fact] public void AddCondition_SortedSetScoreCountNotExists() { - wrapper.AddCondition(Condition.SortedSetScoreNotExists("key", "score", "count")); + prefixed.AddCondition(Condition.SortedSetScoreNotExists("key", "score", "count")); mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key not contains count members with score: score" == value.ToString()))); } [Fact] public async Task ExecuteAsync() { - await wrapper.ExecuteAsync(CommandFlags.None); + await prefixed.ExecuteAsync(CommandFlags.None); mock.Verify(_ => _.ExecuteAsync(CommandFlags.None), Times.Once()); } [Fact] public void Execute() { - wrapper.Execute(CommandFlags.None); + prefixed.Execute(CommandFlags.None); mock.Verify(_ => _.Execute(CommandFlags.None), Times.Once()); } } diff --git a/tests/StackExchange.Redis.Tests/LockingTests.cs b/tests/StackExchange.Redis.Tests/LockingTests.cs index 989c5826e..1d9c8742e 100644 --- a/tests/StackExchange.Redis.Tests/LockingTests.cs +++ b/tests/StackExchange.Redis.Tests/LockingTests.cs @@ -34,7 +34,7 @@ public void AggressiveParallel(TestMode testMode) int errorCount = 0; int bgErrorCount = 0; var evt = new ManualResetEvent(false); - var key = Me(); + var key = Me() + testMode; using (var conn1 = Create(testMode)) using (var conn2 = Create(testMode)) { @@ -111,21 +111,21 @@ private static void TestLockOpCountByVersion(IConnectionMultiplexer conn, int ex }; [Theory, MemberData(nameof(TestModes))] - public async Task TakeLockAndExtend(TestMode mode) + public async Task TakeLockAndExtend(TestMode testMode) { - using var conn = Create(mode); + using var conn = Create(testMode); RedisValue right = Guid.NewGuid().ToString(), wrong = Guid.NewGuid().ToString(); - int DB = mode == TestMode.Twemproxy ? 0 : 7; - RedisKey Key = Me(); + int DB = testMode == TestMode.Twemproxy ? 0 : 7; + RedisKey Key = Me() + testMode; var db = conn.GetDatabase(DB); db.KeyDelete(Key, CommandFlags.FireAndForget); - bool withTran = mode == TestMode.MultiExec; + bool withTran = testMode == TestMode.MultiExec; var t1 = db.LockTakeAsync(Key, right, TimeSpan.FromSeconds(20)); var t1b = db.LockTakeAsync(Key, wrong, TimeSpan.FromSeconds(10)); var t2 = db.LockQueryAsync(Key); @@ -175,7 +175,7 @@ public async Task TestBasicLockNotTaken(TestMode testMode) const int LOOP = 50; var db = conn.GetDatabase(); - var key = Me(); + var key = Me() + testMode; for (int i = 0; i < LOOP; i++) { _ = db.KeyDeleteAsync(key); @@ -197,7 +197,7 @@ public async Task TestBasicLockTaken(TestMode testMode) using var conn = Create(testMode); var db = conn.GetDatabase(); - var key = Me(); + var key = Me() + testMode; db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, "old-value", TimeSpan.FromSeconds(20), flags: CommandFlags.FireAndForget); var taken = db.LockTakeAsync(key, "new-value", TimeSpan.FromSeconds(10)); From 52636088b5ceb8233999f52deacafbd3b71ff424 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 26 Oct 2022 09:06:11 -0400 Subject: [PATCH 196/435] Dependencies: remove System.Diagnostics.PerformanceCounter (#2285) The original purpose of this was to get system-level CPU to help advise people filing issues that their machine overall was under too much load and that's why we were seeing timeouts. However, due to ecosystem changes and shifts the actual reporting of this counter has dropped off so dramatically it's not actually doing what it's supposed to be doing and giving us signal data to help. Given it's a dependency chain that also depends ultimately on some problematic packages now (e.g. System.Drawing.Common) and isn't cross-platform correctly...let's just remove it. It's not a net win anymore. Fixes #2283. --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ExceptionFactory.cs | 7 +- src/StackExchange.Redis/PerfCounterHelper.cs | 64 +------------------ src/StackExchange.Redis/ResultProcessor.cs | 2 +- .../StackExchange.Redis.csproj | 1 - 5 files changed, 6 insertions(+), 69 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 815e9bb26..1bb9bf808 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: - Adds: `last-in` and `cur-in` (bytes) to timeout exceptions to help identify timeouts that were just-behind another large payload off the wire ([#2276 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2276)) - Adds: general-purpose tunnel support, with HTTP proxy "connect" support included ([#2274 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2274)) +- Removes: Package dependency (`System.Diagnostics.PerformanceCounter`) ([#2285 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2285)) ## 2.6.70 diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 0743ad302..19947fda4 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -156,7 +156,7 @@ internal static Exception NoConnectionAvailable( if (multiplexer.RawConfig.IncludeDetailInExceptions) { CopyDataToException(data, ex); - sb.Append("; ").Append(PerfCounterHelper.GetThreadPoolAndCPUSummary(multiplexer.RawConfig.IncludePerformanceCountersInExceptions)); + sb.Append("; ").Append(PerfCounterHelper.GetThreadPoolAndCPUSummary()); AddExceptionDetail(ex, message, server, commandLabel); } return ex; @@ -353,11 +353,6 @@ private static void AddCommonDetail( } data.Add(Tuple.Create("Busy-Workers", busyWorkerCount.ToString())); - if (multiplexer.RawConfig.IncludePerformanceCountersInExceptions) - { - Add(data, sb, "Local-CPU", "Local-CPU", PerfCounterHelper.GetSystemCpuPercent()); - } - Add(data, sb, "Version", "v", Utils.GetLibVersion()); } diff --git a/src/StackExchange.Redis/PerfCounterHelper.cs b/src/StackExchange.Redis/PerfCounterHelper.cs index f7b5b5421..bcc1b5b0a 100644 --- a/src/StackExchange.Redis/PerfCounterHelper.cs +++ b/src/StackExchange.Redis/PerfCounterHelper.cs @@ -1,73 +1,15 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Threading; -#if NET5_0_OR_GREATER -using System.Runtime.Versioning; -#endif +using System.Threading; namespace StackExchange.Redis { internal static class PerfCounterHelper { - private static readonly object staticLock = new(); - private static volatile PerformanceCounter? _cpu; - private static volatile bool _disabled = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - -#if NET5_0_OR_GREATER - [SupportedOSPlatform("Windows")] -#endif - public static bool TryGetSystemCPU(out float value) - { - value = -1; - - try - { - if (!_disabled && _cpu == null) - { - lock (staticLock) - { - if (_cpu == null) - { - _cpu = new PerformanceCounter("Processor", "% Processor Time", "_Total"); - - // First call always returns 0, so get that out of the way. - _cpu.NextValue(); - } - } - } - } - catch (UnauthorizedAccessException) - { - // Some environments don't allow access to Performance Counters, so stop trying. - _disabled = true; - } - catch (Exception e) - { - // this shouldn't happen, but just being safe... - Trace.WriteLine(e); - } - - if (!_disabled && _cpu != null) - { - value = _cpu.NextValue(); - return true; - } - return false; - } - - internal static string GetThreadPoolAndCPUSummary(bool includePerformanceCounters) + internal static string GetThreadPoolAndCPUSummary() { GetThreadPoolStats(out string iocp, out string worker, out string? workItems); - var cpu = includePerformanceCounters ? GetSystemCpuPercent() : "n/a"; - return $"IOCP: {iocp}, WORKER: {worker}, POOL: {workItems ?? "n/a"}, Local-CPU: {cpu}"; + return $"IOCP: {iocp}, WORKER: {worker}, POOL: {workItems ?? "n/a"}"; } - internal static string GetSystemCpuPercent() => - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && TryGetSystemCPU(out float systemCPU) - ? Math.Round(systemCPU, 2) + "%" - : "unavailable"; - internal static int GetThreadPoolStats(out string iocp, out string worker, out string? workItems) { ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxIoThreads); diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index d643f4b62..6805baead 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -282,7 +282,7 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in if (bridge.Multiplexer.IncludeDetailInExceptions) { err = $"Endpoint {endpoint} serving hashslot {hashSlot} is not reachable at this point of time. Please check connectTimeout value. If it is low, try increasing it to give the ConnectionMultiplexer a chance to recover from the network disconnect. " - + PerfCounterHelper.GetThreadPoolAndCPUSummary(bridge.Multiplexer.RawConfig.IncludePerformanceCountersInExceptions); + + PerfCounterHelper.GetThreadPoolAndCPUSummary(); } else { diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index d12bbaf3a..4a8b28f92 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -18,7 +18,6 @@ - From e2e4c195804b5eb906f784896c8d31dd8ccbca8e Mon Sep 17 00:00:00 2001 From: Karthick Ramachandran Date: Wed, 26 Oct 2022 06:07:34 -0700 Subject: [PATCH 197/435] Update README to reflect heartbeatInterval which applies only to async (#2284) From my reading of code, the sync execute path uses `syncTimeout` and applies it [instantaneously through](https://github.com/StackExchange/StackExchange.Redis/blob/main/src/StackExchange.Redis/ConnectionMultiplexer.cs#L1879) `Monitor.Wait`. Looks like the heartbeatInterval only applies to async commands? https://github.com/StackExchange/StackExchange.Redis/blob/main/src/StackExchange.Redis/PhysicalConnection.cs#L690 --- docs/Configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 833bb80c1..8982b1298 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -112,7 +112,7 @@ Additional code-only options: - `SocketManager.Shared`: Use a shared dedicated thread pool for _all_ multiplexers (defaults to 10 threads) - best balance for most scenarios. - `SocketManager.ThreadPool`: Use the build-in .NET thread pool for scheduling. This can perform better for very small numbers of cores or with large apps on large machines that need to use more than 10 threads (total, across all multiplexers) under load. **Important**: this option isn't the default because it's subject to thread pool growth/starvation and if for example synchronous calls are waiting on a redis command to come back to unblock other threads, stalls/hangs can result. Use with caution, especially if you have sync-over-async work in play. - HeartbeatInterval - Default: `1000ms` - - Allows running the heartbeat more often which importantly includes timeout evaluation. For example if you have a 50ms command timeout, we're only actually checking it during the heartbeat (once per second by default), so it's possible 50-1050ms pass _before we notice it timed out_. If you want more fidelity in that check and to observe that a server failed faster, you can lower this to run the heartbeat more often to achieve that. + - Allows running the heartbeat more often which importantly includes timeout evaluation for async commands. For example if you have a 50ms async command timeout, we're only actually checking it during the heartbeat (once per second by default), so it's possible 50-1050ms pass _before we notice it timed out_. If you want more fidelity in that check and to observe that a server failed faster, you can lower this to run the heartbeat more often to achieve that. - **Note: heartbeats are not free and that's why the default is 1 second. There is additional overhead to running this more often simply because it does some work each time it fires.** Tokens in the configuration string are comma-separated; any without an `=` sign are assumed to be redis server endpoints. Endpoints without an explicit port will use 6379 if ssl is not enabled, and 6380 if ssl is enabled. From f8303a67fadc3b904d674c521231c59976238ac9 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 2 Nov 2022 09:45:05 -0400 Subject: [PATCH 198/435] 2.6.80 Release notes --- docs/ReleaseNotes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 1bb9bf808..5195525d1 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,10 +8,16 @@ Current package versions: ## Unreleased +No pending changes for the next release yet. + + +## 2.6.80 + - Adds: `last-in` and `cur-in` (bytes) to timeout exceptions to help identify timeouts that were just-behind another large payload off the wire ([#2276 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2276)) - Adds: general-purpose tunnel support, with HTTP proxy "connect" support included ([#2274 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2274)) - Removes: Package dependency (`System.Diagnostics.PerformanceCounter`) ([#2285 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2285)) + ## 2.6.70 - Fix: `MOVED` with `NoRedirect` (and other non-reachable errors) should respect the `IncludeDetailInExceptions` setting ([#2267 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2267)) From 9af4b75c7609f36426e26e820dc63b1254484de9 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 15 Nov 2022 08:19:45 -0500 Subject: [PATCH 199/435] Cluster: Proactively reconfigure when we hit a MOVED response (#2286) Meant to help address #1520, #1660, #2074, and #2020. I'm not 100% sure about this because if there is a MOVED happening (e.g. bad proxy somewhere) this would just continually re-run...but only once every 5 seconds. Overall though, we linger in a bad state retrying moves until a discovery happens today and this could be resolved much faster. --- docs/ReleaseNotes.md | 2 +- .../ConnectionMultiplexer.cs | 25 ++++++++++++++----- .../CommandTimeoutTests.cs | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 5195525d1..b17d6da20 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -No pending changes for the next release yet. +- Fix [#1520](https://github.com/StackExchange/StackExchange.Redis/issues/1520) & [#1660](https://github.com/StackExchange/StackExchange.Redis/issues/1660): When `MOVED` is encountered from a cluster, a reconfigure will happen proactively to react to cluster changes ASAP ([#2286 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2286)) ## 2.6.80 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 5630b28fe..e4080de77 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -49,6 +49,10 @@ public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplex ConfigurationOptions IInternalConnectionMultiplexer.RawConfig => RawConfig; + private int lastReconfigiureTicks = Environment.TickCount; + internal long LastReconfigureSecondsAgo => + unchecked(Environment.TickCount - Thread.VolatileRead(ref lastReconfigiureTicks)) / 1000; + private int _activeHeartbeatErrors, lastHeartbeatTicks; internal long LastHeartbeatSecondsAgo => pulse is null @@ -366,8 +370,19 @@ internal void CheckMessage(Message message) } } - internal bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isMoved) => - ServerSelectionStrategy.TryResend(hashSlot, message, endpoint, isMoved); + internal bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isMoved) + { + // If we're being told to re-send something because the hash slot moved, that means our topology is out of date + // ...and we should re-evaluate what's what. + // Allow for a 5-second back-off so we don't hammer this in a loop though + if (isMoved && LastReconfigureSecondsAgo > 5) + { + // Async kickoff a reconfigure + ReconfigureIfNeeded(endpoint, false, "MOVED encountered"); + } + + return ServerSelectionStrategy.TryResend(hashSlot, message, endpoint, isMoved); + } /// /// Wait for a given asynchronous operation to complete (or timeout). @@ -1214,6 +1229,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP } Trace("Starting reconfiguration..."); Trace(blame != null, "Blaming: " + Format.ToString(blame)); + Interlocked.Exchange(ref lastReconfigiureTicks, Environment.TickCount); log?.WriteLine(RawConfig.ToString(includePassword: false)); log?.WriteLine(); @@ -1552,10 +1568,7 @@ public EndPoint[] GetEndPoints(bool configuredOnly = false) => foreach (EndPoint endpoint in clusterEndpoints) { serverEndpoint = GetServerEndPoint(endpoint); - if (serverEndpoint != null) - { - serverEndpoint.UpdateNodeRelations(clusterConfig); - } + serverEndpoint?.UpdateNodeRelations(clusterConfig); } return clusterEndpoints; } diff --git a/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs b/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs index 4ba77cfd8..f4b50a930 100644 --- a/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs +++ b/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs @@ -47,7 +47,7 @@ public async Task DefaultHeartbeatLowTimeout() using var conn = ConnectionMultiplexer.Connect(options); var pauseServer = GetServer(pauseConn); - var pauseTask = pauseServer.ExecuteAsync("CLIENT", "PAUSE", 500); + var pauseTask = pauseServer.ExecuteAsync("CLIENT", "PAUSE", 2000); var key = Me(); var db = conn.GetDatabase(); From f3ac74a0107b06ec3f950b341665196ec5657c82 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 17 Nov 2022 09:44:14 -0500 Subject: [PATCH 200/435] Fix #2249: Handle fail on cluster node responses appropriately (#2288) Right now we don't pay attention to fail state (PFAIL == FAIL) and continue trying to connect in the main loop. I don't believe this was intended looking at the code, we just weren't handling the flag appropriately. Added now. Docs at: https://redis.io/commands/cluster-nodes/ --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ClusterConfiguration.cs | 15 ++++++++++++++- .../PublicAPI/PublicAPI.Shipped.txt | 2 ++ .../CommandTimeoutTests.cs | 6 +++--- tests/StackExchange.Redis.Tests/ConfigTests.cs | 2 +- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b17d6da20..02dda249a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Fix [#1520](https://github.com/StackExchange/StackExchange.Redis/issues/1520) & [#1660](https://github.com/StackExchange/StackExchange.Redis/issues/1660): When `MOVED` is encountered from a cluster, a reconfigure will happen proactively to react to cluster changes ASAP ([#2286 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2286)) +- Fix [#2249](https://github.com/StackExchange/StackExchange.Redis/issues/2249): Properly handle a `fail` state (new `ClusterNode.IsFail` property) for `CLUSTER NODES` and expose `fail?` as a property (`IsPossiblyFail`) as well ([#2288 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2288)) ## 2.6.80 diff --git a/src/StackExchange.Redis/ClusterConfiguration.cs b/src/StackExchange.Redis/ClusterConfiguration.cs index 92cbec872..83ab19501 100644 --- a/src/StackExchange.Redis/ClusterConfiguration.cs +++ b/src/StackExchange.Redis/ClusterConfiguration.cs @@ -171,7 +171,7 @@ internal ClusterConfiguration(ServerSelectionStrategy serverSelectionStrategy, s var node = new ClusterNode(this, line, origin); // Be resilient to ":0 {primary,replica},fail,noaddr" nodes, and nodes where the endpoint doesn't parse - if (node.IsNoAddr || node.EndPoint == null) + if (node.IsNoAddr || node.IsFail || node.EndPoint == null) continue; // Override the origin value with the endpoint advertised with the target node to @@ -301,6 +301,8 @@ internal ClusterNode(ClusterConfiguration configuration, string raw, EndPoint or } NodeId = parts[0]; + IsFail = flags.Contains("fail"); + IsPossiblyFail = flags.Contains("fail?"); IsReplica = flags.Contains("slave") || flags.Contains("replica"); IsNoAddr = flags.Contains("noaddr"); ParentNodeId = string.IsNullOrWhiteSpace(parts[3]) ? null : parts[3]; @@ -345,6 +347,17 @@ public IList Children /// public EndPoint? EndPoint { get; } + /// + /// Gets whether this node is in a failed state. + /// + public bool IsFail { get; } + + /// + /// Gets whether this node is possibly in a failed state. + /// Possibly here means the node we're getting status from can't communicate with it, but doesn't mean it's down for sure. + /// + public bool IsPossiblyFail { get; } + /// /// Gets whether this is the node which responded to the CLUSTER NODES request. /// diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index f1201d35f..1baa595f9 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -140,8 +140,10 @@ StackExchange.Redis.ClusterNode.CompareTo(StackExchange.Redis.ClusterNode? other StackExchange.Redis.ClusterNode.EndPoint.get -> System.Net.EndPoint? StackExchange.Redis.ClusterNode.Equals(StackExchange.Redis.ClusterNode? other) -> bool StackExchange.Redis.ClusterNode.IsConnected.get -> bool +StackExchange.Redis.ClusterNode.IsFail.get -> bool StackExchange.Redis.ClusterNode.IsMyself.get -> bool StackExchange.Redis.ClusterNode.IsNoAddr.get -> bool +StackExchange.Redis.ClusterNode.IsPossiblyFail.get -> bool StackExchange.Redis.ClusterNode.IsReplica.get -> bool StackExchange.Redis.ClusterNode.IsSlave.get -> bool StackExchange.Redis.ClusterNode.NodeId.get -> string! diff --git a/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs b/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs index f4b50a930..54847e2be 100644 --- a/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs +++ b/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs @@ -10,7 +10,7 @@ public class CommandTimeoutTests : TestBase { public CommandTimeoutTests(ITestOutputHelper output) : base (output) { } - [Fact] + [FactLongRunning] public async Task DefaultHeartbeatTimeout() { var options = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort); @@ -21,7 +21,7 @@ public async Task DefaultHeartbeatTimeout() using var conn = ConnectionMultiplexer.Connect(options); var pauseServer = GetServer(pauseConn); - var pauseTask = pauseServer.ExecuteAsync("CLIENT", "PAUSE", 2500); + var pauseTask = pauseServer.ExecuteAsync("CLIENT", "PAUSE", 5000); var key = Me(); var db = conn.GetDatabase(); @@ -29,7 +29,7 @@ public async Task DefaultHeartbeatTimeout() var ex = await Assert.ThrowsAsync(async () => await db.StringGetAsync(key)); Log(ex.Message); var duration = sw.GetElapsedTime(); - Assert.True(duration < TimeSpan.FromSeconds(2100), $"Duration ({duration.Milliseconds} ms) should be less than 2100ms"); + Assert.True(duration < TimeSpan.FromSeconds(4000), $"Duration ({duration.Milliseconds} ms) should be less than 4000ms"); // Await as to not bias the next test await pauseTask; diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index db13d822f..668abe607 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -414,7 +414,7 @@ public async Task TestAutomaticHeartbeat() await Task.Delay(8000).ForAwait(); var after = innerConn.OperationCount; - Assert.True(after >= before + 2, $"after: {after}, before: {before}"); + Assert.True(after >= before + 1, $"after: {after}, before: {before}"); } finally { From 84766514b0af2d24ba4273506b389c1992ef382b Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 18 Nov 2022 12:02:26 -0500 Subject: [PATCH 201/435] Add IConnectionMultiplexer.ServerMaintenanceEvent (#2306) Fixing a few requests here. --- docs/ReleaseNotes.md | 1 + .../Interfaces/IConnectionMultiplexer.cs | 6 ++++++ src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt | 1 + .../Helpers/SharedConnectionFixture.cs | 7 +++++++ 4 files changed, 15 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 02dda249a..5ed16dbcf 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: - Fix [#1520](https://github.com/StackExchange/StackExchange.Redis/issues/1520) & [#1660](https://github.com/StackExchange/StackExchange.Redis/issues/1660): When `MOVED` is encountered from a cluster, a reconfigure will happen proactively to react to cluster changes ASAP ([#2286 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2286)) - Fix [#2249](https://github.com/StackExchange/StackExchange.Redis/issues/2249): Properly handle a `fail` state (new `ClusterNode.IsFail` property) for `CLUSTER NODES` and expose `fail?` as a property (`IsPossiblyFail`) as well ([#2288 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2288)) +- Adds: `IConnectionMultiplexer.ServerMaintenanceEvent` (was on `ConnectionMultiplexer` but not the interface) ([#2306 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2306)) ## 2.6.80 diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index c9a8fe96a..583d621eb 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -3,6 +3,7 @@ using System.IO; using System.Net; using System.Threading.Tasks; +using StackExchange.Redis.Maintenance; using StackExchange.Redis.Profiling; namespace StackExchange.Redis @@ -115,6 +116,11 @@ public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable /// event EventHandler ConfigurationChangedBroadcast; + /// + /// Raised when server indicates a maintenance event is going to happen. + /// + event EventHandler ServerMaintenanceEvent; + /// /// Gets all endpoints defined on the multiplexer. /// diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 1baa595f9..0a9c27315 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -492,6 +492,7 @@ StackExchange.Redis.IConnectionMultiplexer.PublishReconfigure(StackExchange.Redi StackExchange.Redis.IConnectionMultiplexer.PublishReconfigureAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IConnectionMultiplexer.RegisterProfiler(System.Func! profilingSessionProvider) -> void StackExchange.Redis.IConnectionMultiplexer.ResetStormLog() -> void +StackExchange.Redis.IConnectionMultiplexer.ServerMaintenanceEvent -> System.EventHandler! StackExchange.Redis.IConnectionMultiplexer.StormLogThreshold.get -> int StackExchange.Redis.IConnectionMultiplexer.StormLogThreshold.set -> void StackExchange.Redis.IConnectionMultiplexer.TimeoutMilliseconds.get -> int diff --git a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs index fa71909cd..aec2e0a83 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs @@ -5,6 +5,7 @@ using System.Net; using System.Threading; using System.Threading.Tasks; +using StackExchange.Redis.Maintenance; using StackExchange.Redis.Profiling; using Xunit; @@ -118,6 +119,12 @@ public event EventHandler HashSlotMoved remove => _inner.HashSlotMoved -= value; } + public event EventHandler ServerMaintenanceEvent + { + add => _inner.ServerMaintenanceEvent += value; + remove => _inner.ServerMaintenanceEvent -= value; + } + public void Close(bool allowCommandsToComplete = true) => _inner.Close(allowCommandsToComplete); public Task CloseAsync(bool allowCommandsToComplete = true) => _inner.CloseAsync(allowCommandsToComplete); From 02c29ef8e8ddf8de9556c113a8a6fb1563f46a83 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 22 Nov 2022 18:09:29 -0500 Subject: [PATCH 202/435] Counters: add sync-ops, async-ops, and how long a server has been connected (#2300) More info to help us advise and debug timeouts for users! Overall adds for timeout messages: - `Sync-Ops`: A count of synchronous operation calls - `Async-Ops`: A count of asynchronous operation calls - `Server-Connected-Seconds`: How long the bridge in question has been connected ("n/a" if not connected) --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ConnectionMultiplexer.cs | 7 +++++++ src/StackExchange.Redis/ExceptionFactory.cs | 5 ++++- src/StackExchange.Redis/PhysicalBridge.cs | 10 +++++++++- tests/BasicTest/BasicTest.csproj | 2 +- tests/BasicTestBaseline/BasicTestBaseline.csproj | 2 +- .../StackExchange.Redis.Tests/ExceptionFactoryTests.cs | 3 +++ tests/StackExchange.Redis.Tests/SentinelTests.cs | 6 +++++- toys/KestrelRedisServer/KestrelRedisServer.csproj | 2 +- toys/TestConsole/TestConsole.csproj | 2 +- toys/TestConsoleBaseline/TestConsoleBaseline.csproj | 2 +- 11 files changed, 34 insertions(+), 8 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 5ed16dbcf..45a034cd9 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -11,6 +11,7 @@ Current package versions: - Fix [#1520](https://github.com/StackExchange/StackExchange.Redis/issues/1520) & [#1660](https://github.com/StackExchange/StackExchange.Redis/issues/1660): When `MOVED` is encountered from a cluster, a reconfigure will happen proactively to react to cluster changes ASAP ([#2286 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2286)) - Fix [#2249](https://github.com/StackExchange/StackExchange.Redis/issues/2249): Properly handle a `fail` state (new `ClusterNode.IsFail` property) for `CLUSTER NODES` and expose `fail?` as a property (`IsPossiblyFail`) as well ([#2288 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2288)) - Adds: `IConnectionMultiplexer.ServerMaintenanceEvent` (was on `ConnectionMultiplexer` but not the interface) ([#2306 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2306)) +- Adds: To timeout messages, additional debug information: `Sync-Ops` (synchronous operations), `Async-Ops` (asynchronous operations), and `Server-Connected-Seconds` (how long the connection in question has been connected, or `"n/a"`) ([#2300 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2300)) ## 2.6.80 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index e4080de77..ff2e0c977 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -31,6 +31,7 @@ public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplex /// Tracks overall connection multiplexer counts. /// internal int _connectAttemptCount = 0, _connectCompletedCount = 0, _connectionCloseCount = 0; + internal long syncOps, asyncOps; private long syncTimeouts, fireAndForgets, asyncTimeouts; private string? failureMessage, activeConfigCause; private IDisposable? pulse; @@ -1867,6 +1868,8 @@ internal static void ThrowFailed(TaskCompletionSource? source, Exception u return defaultValue; } + Interlocked.Increment(ref syncOps); + if (message.IsFireAndForget) { #pragma warning disable CS0618 // Type or member is obsolete @@ -1929,6 +1932,8 @@ static async Task ExecuteAsyncImpl_Awaited(ConnectionMultiplexer @this, Value return CompletedTask.FromDefault(defaultValue, state); } + Interlocked.Increment(ref asyncOps); + TaskCompletionSource? tcs = null; IResultBox? source = null; if (!message.IsFireAndForget) @@ -1978,6 +1983,8 @@ static async Task ExecuteAsyncImpl_Awaited(ConnectionMultiplexer @this, Value return CompletedTask.Default(state); } + Interlocked.Increment(ref asyncOps); + TaskCompletionSource? tcs = null; IResultBox? source = null; if (!message.IsFireAndForget) diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 19947fda4..07f172129 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Reflection; using System.Security.Authentication; using System.Text; using System.Threading; @@ -323,6 +322,9 @@ private static void AddCommonDetail( Add(data, sb, "Last-Result-Bytes", "last-in", bs.Connection.BytesLastResult.ToString()); Add(data, sb, "Inbound-Buffer-Bytes", "cur-in", bs.Connection.BytesInBuffer.ToString()); + Add(data, sb, "Sync-Ops", "sync-ops", multiplexer.syncOps.ToString()); + Add(data, sb, "Async-Ops", "async-ops", multiplexer.asyncOps.ToString()); + if (multiplexer.StormLogThreshold >= 0 && bs.Connection.MessagesSentAwaitingResponse >= multiplexer.StormLogThreshold && Interlocked.CompareExchange(ref multiplexer.haveStormLog, 1, 0) == 0) { var log = server.GetStormLog(message); @@ -330,6 +332,7 @@ private static void AddCommonDetail( else Interlocked.Exchange(ref multiplexer.stormLogSnapshot, log); } Add(data, sb, "Server-Endpoint", "serverEndpoint", (server.EndPoint.ToString() ?? "Unknown").Replace("Unspecified/", "")); + Add(data, sb, "Server-Connected-Seconds", "conn-sec", bs.ConnectedAt is DateTime dt ? (DateTime.UtcNow - dt).TotalSeconds.ToString("0.##") : "n/a"); } Add(data, sb, "Multiplexer-Connects", "mc", $"{multiplexer._connectAttemptCount}/{multiplexer._connectCompletedCount}/{multiplexer._connectionCloseCount}"); Add(data, sb, "Manager", "mgr", multiplexer.SocketManager?.GetState()); diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index cd225a529..7730c39c5 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -66,6 +66,7 @@ internal sealed class PhysicalBridge : IDisposable #endif internal string? PhysicalName => physical?.ToString(); + public DateTime? ConnectedAt { get; private set; } public PhysicalBridge(ServerEndPoint serverEndPoint, ConnectionType type, int timeoutMilliseconds) { @@ -265,6 +266,10 @@ internal readonly struct BridgeStatus /// public int MessagesSinceLastHeartbeat { get; init; } /// + /// The time this connection was connected at, if it's connected currently. + /// + public DateTime? ConnectedAt { get; init; } + /// /// Whether the pipe writer is currently active. /// public bool IsWriterActive { get; init; } @@ -298,12 +303,13 @@ internal readonly struct BridgeStatus public static BridgeStatus Zero { get; } = new() { Connection = PhysicalConnection.ConnectionStatus.Zero }; public override string ToString() => - $"MessagesSinceLastHeartbeat: {MessagesSinceLastHeartbeat}, Writer: {(IsWriterActive ? "Active" : "Inactive")}, BacklogStatus: {BacklogStatus}, BacklogMessagesPending: (Queue: {BacklogMessagesPending}, Counter: {BacklogMessagesPendingCounter}), TotalBacklogMessagesQueued: {TotalBacklogMessagesQueued}, Connection: ({Connection})"; + $"MessagesSinceLastHeartbeat: {MessagesSinceLastHeartbeat}, ConnectedAt: {ConnectedAt?.ToString("u") ?? "n/a"}, Writer: {(IsWriterActive ? "Active" : "Inactive")}, BacklogStatus: {BacklogStatus}, BacklogMessagesPending: (Queue: {BacklogMessagesPending}, Counter: {BacklogMessagesPendingCounter}), TotalBacklogMessagesQueued: {TotalBacklogMessagesQueued}, Connection: ({Connection})"; } internal BridgeStatus GetStatus() => new() { MessagesSinceLastHeartbeat = (int)(Interlocked.Read(ref operationCount) - Interlocked.Read(ref profileLastLog)), + ConnectedAt = ConnectedAt, #if NETCOREAPP IsWriterActive = _singleWriterMutex.CurrentCount == 0, #else @@ -385,6 +391,7 @@ internal async Task OnConnectedAsync(PhysicalConnection connection, LogProxy? lo Trace("OnConnected"); if (physical == connection && !isDisposed && ChangeState(State.Connecting, State.ConnectedEstablishing)) { + ConnectedAt ??= DateTime.UtcNow; await ServerEndPoint.OnEstablishingAsync(connection, log).ForAwait(); log?.WriteLine($"{Format.ToString(ServerEndPoint)}: OnEstablishingAsync complete"); } @@ -431,6 +438,7 @@ internal void OnDisconnected(ConnectionFailureType failureType, PhysicalConnecti Trace($"OnDisconnected: {failureType}"); oldState = default(State); // only defined when isCurrent = true + ConnectedAt = default; if (isCurrent = (physical == connection)) { Trace("Bridge noting disconnect from active connection" + (isDisposed ? " (disposed)" : "")); diff --git a/tests/BasicTest/BasicTest.csproj b/tests/BasicTest/BasicTest.csproj index 70d2c8ecf..0ba04d459 100644 --- a/tests/BasicTest/BasicTest.csproj +++ b/tests/BasicTest/BasicTest.csproj @@ -2,7 +2,7 @@ StackExchange.Redis.BasicTest .NET Core - net472;net5.0 + net472;net6.0 BasicTest Exe BasicTest diff --git a/tests/BasicTestBaseline/BasicTestBaseline.csproj b/tests/BasicTestBaseline/BasicTestBaseline.csproj index 71fd51459..43cf8a8b3 100644 --- a/tests/BasicTestBaseline/BasicTestBaseline.csproj +++ b/tests/BasicTestBaseline/BasicTestBaseline.csproj @@ -2,7 +2,7 @@ StackExchange.Redis.BasicTest .NET Core - net472;net5.0 + net472;net6.0 BasicTestBaseline Exe BasicTestBaseline diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 89dc06e5d..abc19d0f4 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -121,6 +121,9 @@ public void TimeoutException() Assert.Contains("serverEndpoint: " + server.EndPoint, ex.Message); Assert.Contains("IOCP: ", ex.Message); Assert.Contains("WORKER: ", ex.Message); + Assert.Contains("sync-ops: ", ex.Message); + Assert.Contains("async-ops: ", ex.Message); + Assert.Contains("conn-sec: n/a", ex.Message); #if NETCOREAPP Assert.Contains("POOL: ", ex.Message); #endif diff --git a/tests/StackExchange.Redis.Tests/SentinelTests.cs b/tests/StackExchange.Redis.Tests/SentinelTests.cs index 9deea9259..518441ef0 100644 --- a/tests/StackExchange.Redis.Tests/SentinelTests.cs +++ b/tests/StackExchange.Redis.Tests/SentinelTests.cs @@ -451,8 +451,12 @@ public async Task SentinelGetSentinelAddressesTest() public async Task ReadOnlyConnectionReplicasTest() { var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); - var config = new ConfigurationOptions(); + if (replicas.Length == 0) + { + Skip.Inconclusive("Sentinel race: 0 replicas to test against."); + } + var config = new ConfigurationOptions(); foreach (var replica in replicas) { config.EndPoints.Add(replica); diff --git a/toys/KestrelRedisServer/KestrelRedisServer.csproj b/toys/KestrelRedisServer/KestrelRedisServer.csproj index 5f0d6287d..11bb95103 100644 --- a/toys/KestrelRedisServer/KestrelRedisServer.csproj +++ b/toys/KestrelRedisServer/KestrelRedisServer.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 $(NoWarn);CS1591 diff --git a/toys/TestConsole/TestConsole.csproj b/toys/TestConsole/TestConsole.csproj index 0361fe4d6..71bc9fe63 100644 --- a/toys/TestConsole/TestConsole.csproj +++ b/toys/TestConsole/TestConsole.csproj @@ -2,7 +2,7 @@ Exe - net50;net472 + net6.0;net472 SEV2 true diff --git a/toys/TestConsoleBaseline/TestConsoleBaseline.csproj b/toys/TestConsoleBaseline/TestConsoleBaseline.csproj index 4368a8274..c2d46d20a 100644 --- a/toys/TestConsoleBaseline/TestConsoleBaseline.csproj +++ b/toys/TestConsoleBaseline/TestConsoleBaseline.csproj @@ -2,7 +2,7 @@ Exe - net50;net461;net462;net47;net472 + net6.0;net461;net462;net47;net472 From e596322aefbe22a13accbdd0e4a76022c1ccbda9 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 12 Dec 2022 22:01:59 -0500 Subject: [PATCH 203/435] 2.6.86 Release Notes --- docs/ReleaseNotes.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 45a034cd9..1457a33f8 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,11 @@ Current package versions: ## Unreleased +No pending changes for the next release yet. + + +## 2.6.86 + - Fix [#1520](https://github.com/StackExchange/StackExchange.Redis/issues/1520) & [#1660](https://github.com/StackExchange/StackExchange.Redis/issues/1660): When `MOVED` is encountered from a cluster, a reconfigure will happen proactively to react to cluster changes ASAP ([#2286 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2286)) - Fix [#2249](https://github.com/StackExchange/StackExchange.Redis/issues/2249): Properly handle a `fail` state (new `ClusterNode.IsFail` property) for `CLUSTER NODES` and expose `fail?` as a property (`IsPossiblyFail`) as well ([#2288 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2288)) - Adds: `IConnectionMultiplexer.ServerMaintenanceEvent` (was on `ConnectionMultiplexer` but not the interface) ([#2306 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2306)) From c4e5453b9f9b91a7b11c8ad03bd2bc1766a6a4d5 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 4 Jan 2023 13:59:54 +0000 Subject: [PATCH 204/435] ServerSnapshot: Improve API and allow filtering with custom struct enumerator (#2337) So we aren't limited *just* to `ReadOnlySpan`, and don't need the allocatey `ToArray()` Implemented as custom iterator which allows `async` and LINQ to work directly' existing code still uses span for efficiency, with the new API used in limited scenarios only Intent here: 1. Provide an efficient basis for filtered "all server's matching X" functionality 2. Provide the `Where(CommandFlags)` basis for upcoming broadcast work 3. Avoid some unnecessary allocations --- docs/ReleaseNotes.md | 2 +- .../ConnectionMultiplexer.Sentinel.cs | 15 +- .../ConnectionMultiplexer.cs | 153 +++++++++++++----- src/StackExchange.Redis/ResultProcessor.cs | 4 +- src/StackExchange.Redis/ServerEndPoint.cs | 5 +- .../ServerSnapshotTests.cs | 113 +++++++++++++ 6 files changed, 240 insertions(+), 52 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 1457a33f8..3380172d2 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -No pending changes for the next release yet. +- Internal: revisit endpoint-snapshot implementation ## 2.6.86 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs index eb0b70c77..302bccea9 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs @@ -349,9 +349,8 @@ internal void OnManagedConnectionFailed(object? sender, ConnectionFailedEventArg } internal EndPoint? GetConfiguredPrimaryForService(string serviceName) => - GetServerSnapshot() - .ToArray() - .Where(s => s.ServerType == ServerType.Sentinel) + _serverSnapshot // same as GetServerSnapshot, but without forcing span + .Where(static s => s.ServerType == ServerType.Sentinel) .AsParallel() .Select(s => { @@ -361,9 +360,8 @@ internal void OnManagedConnectionFailed(object? sender, ConnectionFailedEventArg .FirstOrDefault(r => r != null); internal EndPoint[]? GetReplicasForService(string serviceName) => - GetServerSnapshot() - .ToArray() - .Where(s => s.ServerType == ServerType.Sentinel) + _serverSnapshot // same as GetServerSnapshot, but without forcing span + .Where(static s => s.ServerType == ServerType.Sentinel) .AsParallel() .Select(s => { @@ -425,9 +423,8 @@ internal void SwitchPrimary(EndPoint? switchBlame, ConnectionMultiplexer connect internal void UpdateSentinelAddressList(string serviceName) { - var firstCompleteRequest = GetServerSnapshot() - .ToArray() - .Where(s => s.ServerType == ServerType.Sentinel) + var firstCompleteRequest = _serverSnapshot // same as GetServerSnapshot, but without forcing span + .Where(static s => s.ServerType == ServerType.Sentinel) .AsParallel() .Select(s => { diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index ff2e0c977..ea6ed6885 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -113,30 +113,12 @@ public bool IncludePerformanceCountersInExceptions /// /// Indicates whether any servers are connected. /// - public bool IsConnected - { - get - { - var tmp = GetServerSnapshot(); - for (int i = 0; i < tmp.Length; i++) - if (tmp[i].IsConnected) return true; - return false; - } - } + public bool IsConnected => _serverSnapshot.Any(static s => s.IsConnected); /// /// Indicates whether any servers are currently trying to connect. /// - public bool IsConnecting - { - get - { - var tmp = GetServerSnapshot(); - for (int i = 0; i < tmp.Length; i++) - if (tmp[i].IsConnecting) return true; - return false; - } - } + public bool IsConnecting => _serverSnapshot.Any(static s => s.IsConnecting); static ConnectionMultiplexer() { @@ -245,7 +227,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt throw; } - var nodes = GetServerSnapshot().ToArray(); // Have to array because async/await + var nodes = _serverSnapshot; // same as GetServerSnapshot(), but doesn't force span RedisValue newPrimary = Format.ToString(server.EndPoint); // try and write this everywhere; don't worry if some folks reject our advances @@ -302,7 +284,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt // We want everyone possible to pick it up. // We broadcast before *and after* the change to remote members, so that they don't go without detecting a change happened. // This eliminates the race of pub/sub *then* re-slaving happening, since a method both precedes and follows. - async Task BroadcastAsync(ServerEndPoint[] serverNodes) + async Task BroadcastAsync(ServerSnapshot serverNodes) { if (options.HasFlag(ReplicationChangeOptions.Broadcast) && ConfigurationChangedChannel != null @@ -746,19 +728,20 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat } } - ReadOnlySpan IInternalConnectionMultiplexer.GetServerSnapshot() => GetServerSnapshot(); - internal ReadOnlySpan GetServerSnapshot() => _serverSnapshot.Span; - private sealed class ServerSnapshot + ReadOnlySpan IInternalConnectionMultiplexer.GetServerSnapshot() => _serverSnapshot.AsSpan(); + internal ReadOnlySpan GetServerSnapshot() => _serverSnapshot.AsSpan(); + internal sealed class ServerSnapshot : IEnumerable { public static ServerSnapshot Empty { get; } = new ServerSnapshot(Array.Empty(), 0); - private ServerSnapshot(ServerEndPoint[] arr, int count) + private ServerSnapshot(ServerEndPoint[] endpoints, int count) { - _arr = arr; + _endpoints = endpoints; _count = count; } - private readonly ServerEndPoint[] _arr; + private readonly ServerEndPoint[] _endpoints; private readonly int _count; - public ReadOnlySpan Span => new ReadOnlySpan(_arr, 0, _count); + public ReadOnlySpan AsSpan() => new ReadOnlySpan(_endpoints, 0, _count); + public ReadOnlyMemory AsMemory() => new ReadOnlyMemory(_endpoints, 0, _count); internal ServerSnapshot Add(ServerEndPoint value) { @@ -767,21 +750,21 @@ internal ServerSnapshot Add(ServerEndPoint value) return this; } - ServerEndPoint[] arr; - if (_arr.Length > _count) + ServerEndPoint[] nextEndpoints; + if (_endpoints.Length > _count) { - arr = _arr; + nextEndpoints = _endpoints; } else { // no more room; need a new array - int newLen = _arr.Length << 1; + int newLen = _endpoints.Length << 1; if (newLen == 0) newLen = 4; - arr = new ServerEndPoint[newLen]; - _arr.CopyTo(arr, 0); + nextEndpoints = new ServerEndPoint[newLen]; + _endpoints.CopyTo(nextEndpoints, 0); } - arr[_count] = value; - return new ServerSnapshot(arr, _count + 1); + nextEndpoints[_count] = value; + return new ServerSnapshot(nextEndpoints, _count + 1); } internal EndPoint[] GetEndPoints() @@ -791,10 +774,104 @@ internal EndPoint[] GetEndPoints() var arr = new EndPoint[_count]; for (int i = 0; i < _count; i++) { - arr[i] = _arr[i].EndPoint; + arr[i] = _endpoints[i].EndPoint; } return arr; } + + public Enumerator GetEnumerator() => new(_endpoints, _count); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public struct Enumerator : IEnumerator + { + private readonly ServerEndPoint[] _endpoints; + private readonly Func? _predicate; + private readonly int _count; + private int _index; + + public ServerEndPoint Current { get; private set; } + + object IEnumerator.Current => Current; + + public bool MoveNext() + { + while (_index < _count && ++_index < _count) + { + Current = _endpoints[_index]; + if (_predicate is null || _predicate(Current)) + { + return true; + } + } + Current = default!; + return false; + } + void IDisposable.Dispose() { } + void IEnumerator.Reset() + { + _index = -1; + Current = default!; + } + + public Enumerator(ServerEndPoint[] endpoints, int count, Func? predicate = null) + { + _index = -1; + _endpoints = endpoints; + _count = count; + _predicate = predicate; + Current = default!; + } + } + + public int Count => _count; + + public bool Any(Func? predicate = null) + { + if (_count > 0) + { + if (predicate is null) return true; + foreach (var item in AsSpan()) // span for bounds elision + { + if (predicate(item)) return true; + } + } + return false; + } + + + public ServerSnapshotFiltered Where(CommandFlags flags) + { + var effectiveFlags = flags & (CommandFlags.DemandMaster | CommandFlags.DemandReplica); + return (effectiveFlags) switch + { + CommandFlags.DemandMaster => Where(static s => !s.IsReplica), + CommandFlags.DemandReplica => Where(static s => s.IsReplica), + _ => Where(null!), + // note we don't need to consider "both", since the composition of the flags-enum precludes that + }; + } + + public ServerSnapshotFiltered Where(Func predicate) + => new ServerSnapshotFiltered(_endpoints, _count, predicate); + + public readonly struct ServerSnapshotFiltered : IEnumerable + { + private readonly ServerEndPoint[] _endpoints; + private readonly Func? _predicate; + private readonly int _count; + + public ServerSnapshotFiltered(ServerEndPoint[] endpoints, int count, Func? predicate) + { + _endpoints = endpoints; + _count = count; + _predicate = predicate; + } + + public Enumerator GetEnumerator() => new(_endpoints, _count, _predicate); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } [return: NotNullIfNotNull("endpoint")] diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 6805baead..80f3fb805 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -267,7 +267,7 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in { if (isMoved && wasNoRedirect) { - if (bridge.Multiplexer.IncludeDetailInExceptions) + if (bridge.Multiplexer.RawConfig.IncludeDetailInExceptions) { err = $"Key has MOVED to Endpoint {endpoint} and hashslot {hashSlot} but CommandFlags.NoRedirect was specified - redirect not followed for {message.CommandAndKey}. "; } @@ -279,7 +279,7 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in else { unableToConnectError = true; - if (bridge.Multiplexer.IncludeDetailInExceptions) + if (bridge.Multiplexer.RawConfig.IncludeDetailInExceptions) { err = $"Endpoint {endpoint} serving hashslot {hashSlot} is not reachable at this point of time. Please check connectTimeout value. If it is low, try increasing it to give the ConnectionMultiplexer a chance to recover from the network disconnect. " + PerfCounterHelper.GetThreadPoolAndCPUSummary(); diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 89ce6a974..1e192d85e 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -968,10 +968,11 @@ private void SetConfig(ref T field, T value, [CallerMemberName] string? calle { if (!EqualityComparer.Default.Equals(field, value)) { - Multiplexer.Trace(caller + " changed from " + field + " to " + value, "Configuration"); + // multiplexer might be null here in some test scenarios; just roll with it... + Multiplexer?.Trace(caller + " changed from " + field + " to " + value, "Configuration"); field = value; ClearMemoized(); - Multiplexer.ReconfigureIfNeeded(EndPoint, false, caller!); + Multiplexer?.ReconfigureIfNeeded(EndPoint, false, caller!); } } diff --git a/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs b/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs new file mode 100644 index 000000000..01688a337 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs @@ -0,0 +1,113 @@ +using System; +using System.Linq; +using System.Runtime.Serialization; +using Xunit; +using static StackExchange.Redis.ConnectionMultiplexer; + +namespace StackExchange.Redis.Tests; + +public class ServerSnapshotTests +{ + [Fact] + public void EmptyBehaviour() + { + var snapshot = ServerSnapshot.Empty; + Assert.Same(snapshot, snapshot.Add(null!)); + + Assert.Equal(0, snapshot.Count); + Assert.Equal(0, ManualCount(snapshot)); + Assert.Equal(0, ManualCount(snapshot, static _ => true)); + Assert.Equal(0, ManualCount(snapshot, static _ => false)); + + Assert.Equal(0, Enumerable.Count(snapshot)); + Assert.Equal(0, Enumerable.Count(snapshot, static _ => true)); + Assert.Equal(0, Enumerable.Count(snapshot, static _ => false)); + + Assert.False(Enumerable.Any(snapshot)); + Assert.False(snapshot.Any()); + + Assert.False(Enumerable.Any(snapshot, static _ => true)); + Assert.False(snapshot.Any(static _ => true)); + Assert.False(Enumerable.Any(snapshot, static _ => false)); + Assert.False(snapshot.Any(static _ => false)); + + Assert.Empty(snapshot); + Assert.Empty(Enumerable.Where(snapshot, static _ => true)); + Assert.Empty(snapshot.Where(static _ => true)); + Assert.Empty(Enumerable.Where(snapshot, static _ => false)); + Assert.Empty(snapshot.Where(static _ => false)); + + Assert.Empty(snapshot.Where(CommandFlags.DemandMaster)); + Assert.Empty(snapshot.Where(CommandFlags.DemandReplica)); + Assert.Empty(snapshot.Where(CommandFlags.None)); + Assert.Empty(snapshot.Where(CommandFlags.FireAndForget | CommandFlags.NoRedirect | CommandFlags.NoScriptCache)); + } + + [Theory] + [InlineData(1, 0)] + [InlineData(1, 1)] + [InlineData(5, 0)] + [InlineData(5, 3)] + [InlineData(5, 5)] + public void NonEmptyBehaviour(int count, int replicaCount) + { + var snapshot = ServerSnapshot.Empty; + for (int i = 0; i < count; i++) + { + var dummy = (ServerEndPoint)FormatterServices.GetSafeUninitializedObject(typeof(ServerEndPoint)); + dummy.IsReplica = i < replicaCount; + snapshot = snapshot.Add(dummy); + } + + Assert.Equal(count, snapshot.Count); + Assert.Equal(count, ManualCount(snapshot)); + Assert.Equal(count, ManualCount(snapshot, static _ => true)); + Assert.Equal(0, ManualCount(snapshot, static _ => false)); + Assert.Equal(replicaCount, ManualCount(snapshot, static s => s.IsReplica)); + + Assert.Equal(count, Enumerable.Count(snapshot)); + Assert.Equal(count, Enumerable.Count(snapshot, static _ => true)); + Assert.Equal(0, Enumerable.Count(snapshot, static _ => false)); + Assert.Equal(replicaCount, Enumerable.Count(snapshot, static s => s.IsReplica)); + + Assert.True(Enumerable.Any(snapshot)); + Assert.True(snapshot.Any()); + + Assert.True(Enumerable.Any(snapshot, static _ => true)); + Assert.True(snapshot.Any(static _ => true)); + Assert.False(Enumerable.Any(snapshot, static _ => false)); + Assert.False(snapshot.Any(static _ => false)); + + Assert.NotEmpty(snapshot); + Assert.NotEmpty(Enumerable.Where(snapshot, static _ => true)); + Assert.NotEmpty(snapshot.Where(static _ => true)); + Assert.Empty(Enumerable.Where(snapshot, static _ => false)); + Assert.Empty(snapshot.Where(static _ => false)); + + Assert.Equal(snapshot.Count - replicaCount, snapshot.Where(CommandFlags.DemandMaster).Count()); + Assert.Equal(replicaCount, snapshot.Where(CommandFlags.DemandReplica).Count()); + Assert.Equal(snapshot.Count, snapshot.Where(CommandFlags.None).Count()); + Assert.Equal(snapshot.Count, snapshot.Where(CommandFlags.FireAndForget | CommandFlags.NoRedirect | CommandFlags.NoScriptCache).Count()); + } + + private static int ManualCount(ServerSnapshot snapshot, Func? predicate = null) + { + // ^^^ tests the custom iterator implementation + int count = 0; + if (predicate is null) + { + foreach (var item in snapshot) + { + count++; + } + } + else + { + foreach (var item in snapshot.Where(predicate)) + { + count++; + } + } + return count; + } +} From b04761edcaf239c4055555c8041c6be330ecb074 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 4 Jan 2023 09:48:03 -0500 Subject: [PATCH 205/435] PhysicalBridge: Fix orphaning cases of PhysicalConnection where it never finishes (#2338) What happens here is that `PhysicalConnection` attempts to connect but never hears back, so it's stuck in `State.ConnectedEstablishing` state, which is handled in the heartbeat code. In the situation where the "last write seconds ago" passes in the heartbeat and we haven't heard back, we fire an `PhysicalBridge.OnDisconnected()` which clears out `physical` and orphans the socket listening forever. This now properly disposes of that `PhysicalConnection` mirroring like we do in `State.Connecting` which will properly fire `OnDisconnected()` and clean up the orphan socket. The situation manifests where we establish a TCP connection, but not a Redis connection. All of the cases in the memory dump we're analyzing are some bytes sent and 0 received. Likely a Redis server issue, but the client is then handling it incorrectly and leaking. I nuked the unused `RemovePhysical` method just to prevent further oops here. Addresses a new case of #1458. --- docs/ReleaseNotes.md | 3 ++- src/StackExchange.Redis/PhysicalBridge.cs | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 3380172d2..f62a99091 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,8 @@ Current package versions: ## Unreleased -- Internal: revisit endpoint-snapshot implementation +- Fix [#1458](https://github.com/StackExchange/StackExchange.Redis/issues/1458): Fixes a leak condition when a connection completes on the TCP phase but not the Redis handshake ([#2238 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2238)) +- Internal: ServerSnapshot: Improve API and allow filtering with custom struct enumerator ([#2337 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2337)) ## 2.6.86 diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 7730c39c5..efcd6518f 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -542,11 +542,16 @@ internal void OnHeartbeat(bool ifConnectedOnly) // abort and reconnect var snapshot = physical; OnDisconnected(ConnectionFailureType.UnableToConnect, snapshot, out bool isCurrent, out State oldState); - using (snapshot) { } // dispose etc + snapshot?.Dispose(); // Cleanup the existing connection/socket if any, otherwise it will wait reading indefinitely TryConnect(null); } break; case (int)State.ConnectedEstablishing: + // (Fall through) Happens when we successfully connected via TCP, but no Redis handshake completion yet. + // This can happen brief (usual) or when the server never answers (rare). When we're in this state, + // a socket is open and reader likely listening indefinitely for incoming data on an async background task. + // We need to time that out and cleanup the PhysicalConnection if needed, otherwise that reader and socket will remain open + // for the lifetime of the application due to being orphaned, yet still referenced by the active task doing the pipe read. case (int)State.ConnectedEstablished: var tmp = physical; if (tmp != null) @@ -576,6 +581,7 @@ internal void OnHeartbeat(bool ifConnectedOnly) else { OnDisconnected(ConnectionFailureType.SocketFailure, tmp, out bool ignore, out State oldState); + tmp.Dispose(); // Cleanup the existing connection/socket if any, otherwise it will wait reading indefinitely } } else if (writeEverySeconds <= 0 && tmp.IsIdle() @@ -614,9 +620,6 @@ internal void OnHeartbeat(bool ifConnectedOnly) } } - internal void RemovePhysical(PhysicalConnection connection) => - Interlocked.CompareExchange(ref physical, null, connection); - [Conditional("VERBOSE")] internal void Trace(string message) => Multiplexer.Trace(message, ToString()); From b1fddf3d1e449326e044e2ae91d934af33cad8a3 Mon Sep 17 00:00:00 2001 From: shacharPash <93581407+shacharPash@users.noreply.github.com> Date: Wed, 4 Jan 2023 17:14:39 +0200 Subject: [PATCH 206/435] EVAL_RO & EVALSHA_RO commands (#2168) Adds: Support for `EVAL_RO` and `EVALSHA_RO` via `IDatabase.ScriptEvaluateReadOnly`/`IDatabase.ScriptEvaluateReadOnlyAsync` Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 4 + .../Interfaces/IDatabase.cs | 25 ++++ .../Interfaces/IDatabaseAsync.cs | 25 ++++ .../KeyspaceIsolation/KeyPrefixed.cs | 8 ++ .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 8 ++ .../PublicAPI/PublicAPI.Shipped.txt | 4 + src/StackExchange.Redis/RedisDatabase.cs | 112 ++++++++++++------ .../ScriptingTests.cs | 61 ++++++++++ 9 files changed, 210 insertions(+), 38 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index f62a99091..211bb4cbc 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,7 @@ Current package versions: ## Unreleased +- Adds: Support for `EVAL_RO` and `EVALSHA_RO` via `IDatabase.ScriptEvaluateReadOnly`/`IDatabase.ScriptEvaluateReadOnlyAsync` ([#2168 by shacharPash](https://github.com/StackExchange/StackExchange.Redis/pull/2168)) - Fix [#1458](https://github.com/StackExchange/StackExchange.Redis/issues/1458): Fixes a leak condition when a connection completes on the TCP phase but not the Redis handshake ([#2238 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2238)) - Internal: ServerSnapshot: Improve API and allow filtering with custom struct enumerator ([#2337 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2337)) diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 40df9416e..40cb5c708 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -36,6 +36,8 @@ internal enum RedisCommand ECHO, EVAL, EVALSHA, + EVAL_RO, + EVALSHA_RO, EXEC, EXISTS, EXPIRE, @@ -361,6 +363,8 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.ECHO: case RedisCommand.EVAL: case RedisCommand.EVALSHA: + case RedisCommand.EVAL_RO: + case RedisCommand.EVALSHA_RO: case RedisCommand.EXEC: case RedisCommand.EXISTS: case RedisCommand.GEODIST: diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 983f4df8c..d0d01a201 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1302,6 +1302,31 @@ public interface IDatabase : IRedis, IDatabaseAsync /// RedisResult ScriptEvaluate(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None); + /// + /// Read-only variant of the EVAL command that cannot execute commands that modify data, Execute a Lua script against the server. + /// + /// The script to execute. + /// The keys to execute against. + /// The values to execute against. + /// The flags to use for this operation. + /// A dynamic representation of the script's result. + /// + /// , + /// + /// + RedisResult ScriptEvaluateReadOnly(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); + + /// + /// Read-only variant of the EVALSHA command that cannot execute commands that modify data, Execute a Lua script against the server using just the SHA1 hash. + /// + /// The hash of the script to execute. + /// The keys to execute against. + /// The values to execute against. + /// The flags to use for this operation. + /// A dynamic representation of the script's result. + /// + RedisResult ScriptEvaluateReadOnly(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); + /// /// Add the specified member to the set stored at key. /// Specified members that are already a member of this set are ignored. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 49771cb91..7f90cf1a5 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1278,6 +1278,31 @@ public interface IDatabaseAsync : IRedisAsync /// Task ScriptEvaluateAsync(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None); + /// + /// Read-only variant of the EVAL command that cannot execute commands that modify data, Execute a Lua script against the server. + /// + /// The script to execute. + /// The keys to execute against. + /// The values to execute against. + /// The flags to use for this operation. + /// A dynamic representation of the script's result. + /// + /// , + /// + /// + Task ScriptEvaluateReadOnlyAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); + + /// + /// Read-only variant of the EVALSHA command that cannot execute commands that modify data, Execute a Lua script against the server using just the SHA1 hash. + /// + /// The hash of the script to execute. + /// The keys to execute against. + /// The values to execute against. + /// The flags to use for this operation. + /// A dynamic representation of the script's result. + /// + Task ScriptEvaluateReadOnlyAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); + /// /// Add the specified member to the set stored at key. /// Specified members that are already a member of this set are ignored. diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 49285588e..290fbed59 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -344,6 +344,14 @@ public Task ScriptEvaluateAsync(LoadedLuaScript script, object? par // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? script.EvaluateAsync(Inner, parameters, Prefix, flags); + public Task ScriptEvaluateReadOnlyAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => + // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? + Inner.ScriptEvaluateAsync(hash, ToInner(keys), values, flags); + + public Task ScriptEvaluateReadOnlyAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => + // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? + Inner.ScriptEvaluateAsync(script, ToInner(keys), values, flags); + public Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => Inner.SetAddAsync(ToInner(key), values, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index c8c3e54f3..d1c47aeab 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -333,6 +333,14 @@ public RedisResult ScriptEvaluate(LoadedLuaScript script, object? parameters = n // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? script.Evaluate(Inner, parameters, Prefix, flags); + public RedisResult ScriptEvaluateReadOnly(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => + // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? + Inner.ScriptEvaluateReadOnly(hash, ToInner(keys), values, flags); + + public RedisResult ScriptEvaluateReadOnly(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => + // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? + Inner.ScriptEvaluateReadOnly(script, ToInner(keys), values, flags); + public long SetAdd(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => Inner.SetAdd(ToInner(key), values, flags); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 0a9c27315..72ae963c1 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -609,6 +609,8 @@ StackExchange.Redis.IDatabase.ScriptEvaluate(byte[]! hash, StackExchange.Redis.R StackExchange.Redis.IDatabase.ScriptEvaluate(StackExchange.Redis.LoadedLuaScript! script, object? parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! StackExchange.Redis.IDatabase.ScriptEvaluate(StackExchange.Redis.LuaScript! script, object? parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! StackExchange.Redis.IDatabase.ScriptEvaluate(string! script, StackExchange.Redis.RedisKey[]? keys = null, StackExchange.Redis.RedisValue[]? values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.IDatabase.ScriptEvaluateReadOnly(byte[]! hash, StackExchange.Redis.RedisKey[]? keys = null, StackExchange.Redis.RedisValue[]? values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.IDatabase.ScriptEvaluateReadOnly(string! script, StackExchange.Redis.RedisKey[]? keys = null, StackExchange.Redis.RedisValue[]? values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! StackExchange.Redis.IDatabase.SetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.SetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.SetCombine(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! @@ -838,6 +840,8 @@ StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(byte[]! hash, StackExchan StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(StackExchange.Redis.LoadedLuaScript! script, object? parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(StackExchange.Redis.LuaScript! script, object? parameters = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.ScriptEvaluateAsync(string! script, StackExchange.Redis.RedisKey[]? keys = null, StackExchange.Redis.RedisValue[]? values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ScriptEvaluateReadOnlyAsync(byte[]! hash, StackExchange.Redis.RedisKey[]? keys = null, StackExchange.Redis.RedisValue[]? values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.ScriptEvaluateReadOnlyAsync(string! script, StackExchange.Redis.RedisKey[]? keys = null, StackExchange.Redis.RedisValue[]? values = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.SetCombineAndStoreAsync(StackExchange.Redis.SetOperation operation, StackExchange.Redis.RedisKey destination, StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index a9dda4aff..9df7ac742 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1493,20 +1493,6 @@ public Task PublishAsync(RedisChannel channel, RedisValue message, Command return ExecuteAsync(msg, ResultProcessor.Int64); } - public RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) - { - var msg = new ScriptEvalMessage(Database, flags, script, keys, values); - try - { - return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); - } - catch (RedisServerException) when (msg.IsScriptUnavailable) - { - // could be a NOSCRIPT; for a sync call, we can re-issue that without problem - return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); - } - } - public RedisResult Execute(string command, params object[] args) => Execute(command, args, CommandFlags.None); @@ -1525,9 +1511,24 @@ public Task ExecuteAsync(string command, ICollection? args, return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } + public RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) + { + var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA : RedisCommand.EVAL; + var msg = new ScriptEvalMessage(Database, flags, command, script, keys, values); + try + { + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + catch (RedisServerException) when (msg.IsScriptUnavailable) + { + // could be a NOSCRIPT; for a sync call, we can re-issue that without problem + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + } + public RedisResult ScriptEvaluate(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { - var msg = new ScriptEvalMessage(Database, flags, hash, keys, values); + var msg = new ScriptEvalMessage(Database, flags, RedisCommand.EVALSHA, hash, keys, values); return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } @@ -1543,7 +1544,8 @@ public RedisResult ScriptEvaluate(LoadedLuaScript script, object? parameters = n public async Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { - var msg = new ScriptEvalMessage(Database, flags, script, keys, values); + var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA : RedisCommand.EVAL; + var msg = new ScriptEvalMessage(Database, flags, command, script, keys, values); try { @@ -1558,7 +1560,7 @@ public async Task ScriptEvaluateAsync(string script, RedisKey[]? ke public Task ScriptEvaluateAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) { - var msg = new ScriptEvalMessage(Database, flags, hash, keys, values); + var msg = new ScriptEvalMessage(Database, flags, RedisCommand.EVALSHA, hash, keys, values); return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } @@ -1572,6 +1574,40 @@ public Task ScriptEvaluateAsync(LoadedLuaScript script, object? par return script.EvaluateAsync(this, parameters, withKeyPrefix: null, flags); } + public RedisResult ScriptEvaluateReadOnly(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) + { + var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA_RO : RedisCommand.EVAL_RO; + var msg = new ScriptEvalMessage(Database, flags, command, script, keys, values); + try + { + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + catch (RedisServerException) when (msg.IsScriptUnavailable) + { + // could be a NOSCRIPT; for a sync call, we can re-issue that without problem + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + } + + public RedisResult ScriptEvaluateReadOnly(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) + { + var msg = new ScriptEvalMessage(Database, flags, RedisCommand.EVALSHA_RO, hash, keys, values); + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + + public Task ScriptEvaluateReadOnlyAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) + { + var command = ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA_RO : RedisCommand.EVAL_RO; + var msg = new ScriptEvalMessage(Database, flags, command, script, keys, values); + return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + + public Task ScriptEvaluateReadOnlyAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) + { + var msg = new ScriptEvalMessage(Database, flags, RedisCommand.EVALSHA_RO, hash, keys, values); + return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + public bool SetAdd(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.SADD, key, value); @@ -3438,7 +3474,7 @@ private sealed class MultiStreamReadGroupCommandMessage : Message // XREADGROUP { private readonly StreamPosition[] streamPositions; private readonly RedisValue groupName; - private readonly RedisValue consumerName; + private readonly RedisValue consumerName; private readonly int? countPerStream; private readonly bool noAck; private readonly int argCount; @@ -3456,11 +3492,11 @@ public MultiStreamReadGroupCommandMessage(int db, CommandFlags flags, StreamPosi if (countPerStream.HasValue && countPerStream <= 0) { throw new ArgumentOutOfRangeException(nameof(countPerStream), "countPerStream must be greater than 0."); - } + } groupName.AssertNotNull(); consumerName.AssertNotNull(); - + this.streamPositions = streamPositions; this.groupName = groupName; this.consumerName = consumerName; @@ -3471,7 +3507,7 @@ public MultiStreamReadGroupCommandMessage(int db, CommandFlags flags, StreamPosi + (streamPositions.Length * 2) // Enough room for the stream keys and associated IDs. + (countPerStream.HasValue ? 2 : 0) // Room for "COUNT num" or 0 if countPerStream is null. + (noAck ? 1 : 0); // Allow for the NOACK subcommand. - + } public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) @@ -3505,13 +3541,13 @@ protected override void WriteImpl(PhysicalConnection physical) physical.WriteBulkString(StreamConstants.Streams); for (int i = 0; i < streamPositions.Length; i++) { - physical.Write(streamPositions[i].Key); + physical.Write(streamPositions[i].Key); } for (int i = 0; i < streamPositions.Length; i++) { physical.WriteBulkString(StreamPosition.Resolve(streamPositions[i].Position, RedisCommand.XREADGROUP)); - } - } + } + } public override int ArgCount => argCount; } @@ -3521,8 +3557,8 @@ private Message GetMultiStreamReadMessage(StreamPosition[] streamPositions, int? private sealed class MultiStreamReadCommandMessage : Message // XREAD with multiple stream. Example: XREAD COUNT 2 STREAMS mystream writers 0-0 0-0 { - private readonly StreamPosition[] streamPositions; - private readonly int? countPerStream; + private readonly StreamPosition[] streamPositions; + private readonly int? countPerStream; private readonly int argCount; public MultiStreamReadCommandMessage(int db, CommandFlags flags, StreamPosition[] streamPositions, int? countPerStream) @@ -3538,10 +3574,10 @@ public MultiStreamReadCommandMessage(int db, CommandFlags flags, StreamPosition[ if (countPerStream.HasValue && countPerStream <= 0) { throw new ArgumentOutOfRangeException(nameof(countPerStream), "countPerStream must be greater than 0."); - } + } - this.streamPositions = streamPositions; - this.countPerStream = countPerStream; + this.streamPositions = streamPositions; + this.countPerStream = countPerStream; argCount = 1 // Streams keyword. + (countPerStream.HasValue ? 2 : 0) // Room for "COUNT num" or 0 if countPerStream is null. @@ -3566,7 +3602,7 @@ protected override void WriteImpl(PhysicalConnection physical) { physical.WriteBulkString(StreamConstants.Count); physical.WriteBulkString(countPerStream.Value); - } + } physical.WriteBulkString(StreamConstants.Streams); for (int i = 0; i < streamPositions.Length; i++) @@ -4181,7 +4217,7 @@ private Message GetStreamReadGroupMessage(RedisKey key, RedisValue groupName, Re private sealed class SingleStreamReadGroupCommandMessage : Message.CommandKeyBase // XREADGROUP with single stream. eg XREADGROUP GROUP mygroup Alice COUNT 1 STREAMS mystream > { private readonly RedisValue groupName; - private readonly RedisValue consumerName; + private readonly RedisValue consumerName; private readonly RedisValue afterId; private readonly int? count; private readonly bool noAck; @@ -4216,7 +4252,7 @@ protected override void WriteImpl(PhysicalConnection physical) { if (count.HasValue) { physical.WriteBulkString(StreamConstants.Count); - physical.WriteBulkString(count.Value); + physical.WriteBulkString(count.Value); } if (noAck) @@ -4238,7 +4274,7 @@ private Message GetSingleStreamReadMessage(RedisKey key, RedisValue afterId, int private sealed class SingleStreamReadCommandMessage : Message.CommandKeyBase // XREAD with a single stream. Example: XREAD COUNT 2 STREAMS mystream 0-0 { private readonly RedisValue afterId; - private readonly int? count; + private readonly int? count; private readonly int argCount; public SingleStreamReadCommandMessage(int db, CommandFlags flags, RedisKey key, RedisValue afterId, int? count) @@ -4264,7 +4300,7 @@ protected override void WriteImpl(PhysicalConnection physical) { physical.WriteBulkString(StreamConstants.Count); physical.WriteBulkString(count.Value); - } + } physical.WriteBulkString(StreamConstants.Streams); physical.Write(Key); @@ -4750,14 +4786,14 @@ private sealed class ScriptEvalMessage : Message, IMultiMessage private byte[]? asciiHash; private readonly byte[]? hexHash; - public ScriptEvalMessage(int db, CommandFlags flags, string script, RedisKey[]? keys, RedisValue[]? values) - : this(db, flags, ResultProcessor.ScriptLoadProcessor.IsSHA1(script) ? RedisCommand.EVALSHA : RedisCommand.EVAL, script, null, keys, values) + public ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, string script, RedisKey[]? keys, RedisValue[]? values) + : this(db, flags, command, script, null, keys, values) { if (script == null) throw new ArgumentNullException(nameof(script)); } - public ScriptEvalMessage(int db, CommandFlags flags, byte[] hash, RedisKey[]? keys, RedisValue[]? values) - : this(db, flags, RedisCommand.EVALSHA, null, hash, keys, values) + public ScriptEvalMessage(int db, CommandFlags flags, RedisCommand command, byte[] hash, RedisKey[]? keys, RedisValue[]? values) + : this(db, flags, command, null, hash, keys, values) { if (hash == null) throw new ArgumentNullException(nameof(hash)); if (hash.Length != ResultProcessor.ScriptLoadProcessor.Sha1HashLength) throw new ArgumentOutOfRangeException(nameof(hash), "Invalid hash length"); diff --git a/tests/StackExchange.Redis.Tests/ScriptingTests.cs b/tests/StackExchange.Redis.Tests/ScriptingTests.cs index f3495767d..8164983de 100644 --- a/tests/StackExchange.Redis.Tests/ScriptingTests.cs +++ b/tests/StackExchange.Redis.Tests/ScriptingTests.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Linq; +using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using StackExchange.Redis.KeyspaceIsolation; @@ -1052,6 +1053,66 @@ private static void TestNullArray(RedisResult? value) [Fact] public void RedisResultUnderstandsNullValue() => TestNullValue(RedisResult.Create(RedisValue.Null, ResultType.None)); + [Fact] + public void TestEvalReadonly() + { + using var conn = GetScriptConn(); + var db = conn.GetDatabase(); + + string script = "return KEYS[1]"; + RedisKey[] keys = new RedisKey[1] { "key1" }; + RedisValue[] values = new RedisValue[1] { "first" }; + + var result = db.ScriptEvaluateReadOnly(script, keys, values); + Assert.Equal("key1", result.ToString()); + } + + [Fact] + public async Task TestEvalReadonlyAsync() + { + using var conn = GetScriptConn(); + var db = conn.GetDatabase(); + + string script = "return KEYS[1]"; + RedisKey[] keys = new RedisKey[1] { "key1" }; + RedisValue[] values = new RedisValue[1] { "first" }; + + var result = await db.ScriptEvaluateReadOnlyAsync(script, keys, values); + Assert.Equal("key1", result.ToString()); + } + + [Fact] + public void TestEvalShaReadOnly() + { + using var conn = GetScriptConn(); + var db = conn.GetDatabase(); + db.StringSet("foo", "bar"); + db.ScriptEvaluate("return redis.call('get','foo')"); + // Create a SHA1 hash of the script: 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 + SHA1 sha1Hash = SHA1.Create(); + + byte[] hash = sha1Hash.ComputeHash(Encoding.UTF8.GetBytes("return redis.call('get','foo')")); + var result = db.ScriptEvaluateReadOnly(hash); + + Assert.Equal("bar", result.ToString()); + } + + [Fact] + public async Task TestEvalShaReadOnlyAsync() + { + using var conn = GetScriptConn(); + var db = conn.GetDatabase(); + db.StringSet("foo", "bar"); + db.ScriptEvaluate("return redis.call('get','foo')"); + // Create a SHA1 hash of the script: 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 + SHA1 sha1Hash = SHA1.Create(); + + byte[] hash = sha1Hash.ComputeHash(Encoding.UTF8.GetBytes("return redis.call('get','foo')")); + var result = await db.ScriptEvaluateReadOnlyAsync(hash); + + Assert.Equal("bar", result.ToString()); + } + private static void TestNullValue(RedisResult? value) { Assert.True(value == null || value.IsNull); From 806929dec3dfc22b71bf78d737e4b5aff022d694 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 10 Jan 2023 11:23:24 -0500 Subject: [PATCH 207/435] Add 2.6.90 release notes --- docs/ReleaseNotes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 211bb4cbc..8bfa2c147 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,10 @@ Current package versions: ## Unreleased +No pending changes for the next release yet. + +## 2.6.90 + - Adds: Support for `EVAL_RO` and `EVALSHA_RO` via `IDatabase.ScriptEvaluateReadOnly`/`IDatabase.ScriptEvaluateReadOnlyAsync` ([#2168 by shacharPash](https://github.com/StackExchange/StackExchange.Redis/pull/2168)) - Fix [#1458](https://github.com/StackExchange/StackExchange.Redis/issues/1458): Fixes a leak condition when a connection completes on the TCP phase but not the Redis handshake ([#2238 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2238)) - Internal: ServerSnapshot: Improve API and allow filtering with custom struct enumerator ([#2337 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2337)) From 757169b3068c8d4b9b1ebd0564ffc146caabc6e3 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 11 Jan 2023 21:28:40 -0500 Subject: [PATCH 208/435] Project: Release notes tidy & ProjectURL pointing to GH Pages (#2343) 1. Found a few inconsistencies when doing https://github.com/StackExchange/StackExchange.Redis/releases - mirroring on the markdown. 2. Adjust the project URL for NuGet to https://stackexchange.github.io/StackExchange.Redis/ so that people can go to "main page" and repo (rather than the repo linked twice) from NuGet.org as intended. --- Directory.Build.props | 2 +- docs/ReleaseNotes.md | 27 ++++++++++++--------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index eeefc966e..0d57b6c7f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,7 +12,7 @@ NETSDK1069 NU5105;NU1507 https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes - https://github.com/StackExchange/StackExchange.Redis/ + https://stackexchange.github.io/StackExchange.Redis/ MIT 10.0 diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 8bfa2c147..2fafa9f72 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -56,8 +56,8 @@ No pending changes for the next release yet. ## 2.6.48 -- URGENT Fix: [#2167](https://github.com/StackExchange/StackExchange.Redis/issues/2167), [#2176](https://github.com/StackExchange/StackExchange.Redis/issues/2176): fix error in batch/transaction handling that can result in out-of-order instructions ([#2177 by MarcGravell](https://github.com/StackExchange/StackExchange.Redis/pull/2177)) -- Fix: [#2164](https://github.com/StackExchange/StackExchange.Redis/issues/2164): fix `LuaScript.Prepare` for scripts that don't have parameters ([#2166 by MarcGravell](https://github.com/StackExchange/StackExchange.Redis/pull/2166)) +- URGENT Fix: [#2167](https://github.com/StackExchange/StackExchange.Redis/issues/2167), [#2176](https://github.com/StackExchange/StackExchange.Redis/issues/2176): fix error in batch/transaction handling that can result in out-of-order instructions ([#2177 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2177)) +- Fix: [#2164](https://github.com/StackExchange/StackExchange.Redis/issues/2164): fix `LuaScript.Prepare` for scripts that don't have parameters ([#2166 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2166)) ## 2.6.45 @@ -89,10 +89,10 @@ No pending changes for the next release yet. - Fix [#2086](https://github.com/StackExchange/StackExchange.Redis/issues/2086): Correct HashSlot calculations for `XREAD` and `XREADGROUP` commands ([#2093 by nielsderdaele](https://github.com/StackExchange/StackExchange.Redis/pull/2093)) - Adds: Support for `LCS` with `.StringLongestCommonSubsequence()`/`.StringLongestCommonSubsequence()`, `.StringLongestCommonSubsequenceLength()`/`.StringLongestCommonSubsequenceLengthAsync()`, and `.StringLongestCommonSubsequenceWithMatches()`/`.StringLongestCommonSubsequenceWithMatchesAsync()` ([#2104 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2104)) - Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105)) -- Performance: Avoids allocations when computing cluster hash slots or testing key equality ([#2110 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2110)) +- Performance: Avoids allocations when computing cluster hash slots or testing key equality ([#2110 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2110)) - Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111)) - Adds: Support for `BIT | BYTE` to `BITCOUNT` and `BITPOS` with `.StringBitCount()`/`.StringBitCountAsync()` and `.StringBitPosition()`/`.StringBitPositionAsync()` ([#2116 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2116)) -- Adds: Support for pub/sub payloads that are unary arrays ([#2118 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2118)) +- Adds: Support for pub/sub payloads that are unary arrays ([#2118 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2118)) - Fix: Sentinel timer race during dispose ([#2133 by ewisuri](https://github.com/StackExchange/StackExchange.Redis/pull/2133)) - Adds: Support for `GT`, `LT`, and `CH` on `ZADD` with `.SortedSetAdd()`/`.SortedSetAddAsync()` and `.SortedSetUpdate()`/`.SortedSetUpdateAsync()` ([#2136 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2136)) - Adds: Support for `COMMAND COUNT`, `COMMAND GETKEYS`, and `COMMAND LIST`, with `.CommandCount()`/`.CommandCountAsync()`, `.CommandGetKeys()`/`.CommandGetKeysAsync()`, and `.CommandList()`/`.CommandListAsync()` ([#2143 by shacharPash](https://github.com/StackExchange/StackExchange.Redis/pull/2143)) @@ -147,7 +147,7 @@ No pending changes for the next release yet. ## 2.2.88 - Change: Connection backoff default is now exponential instead of linear ([#1896 by lolodi](https://github.com/StackExchange/StackExchange.Redis/pull/1896)) -- Adds: Support for NodeMaintenanceScaleComplete event (handles Redis cluster scaling) ([#1902 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1902)) +- Adds: Support for `NodeMaintenanceScaleComplete` event (handles Redis cluster scaling) ([#1902 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1902)) ## 2.2.79 @@ -222,12 +222,12 @@ No pending changes for the next release yet. ## 2.1.30 -- Build: Fix deterministic builds +- Build: Fix deterministic builds ([#1420 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/1420)) ## 2.1.28 - Fix: Stability in new sentinel APIs -- Fix: Include `SslProtocolos` in `ConfigurationOptions.ToString()` ([#1408 by vksampath and Sampath Vuyyuru](https://github.com/StackExchange/StackExchange.Redis/pull/1408)) +- Fix [#1407](https://github.com/StackExchange/StackExchange.Redis/issues/1407): Include `SslProtocolos` in `ConfigurationOptions.ToString()` ([#1408 by vksampath and Sampath Vuyyuru](https://github.com/StackExchange/StackExchange.Redis/pull/1408)) - Fix: Clarify messaging around disconnected multiplexers ([#1396 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/1396)) - Change: Tweak methods of new sentinel API (this is technically a breaking change, but since this is a new API that was pulled quickly, we consider this to be acceptable) - Adds: New thread `SocketManager` mode (opt-in) to always use the regular thread-pool instead of the dedicated pool @@ -277,7 +277,7 @@ No pending changes for the next release yet. ## 2.0.593 - Performance: Unify spin-wait usage on sync/async paths to one competitor -- Fix [#1101](https://github.com/StackExchange/StackExchange.Redis/issues/1101) - when a `ChannelMessageQueue` is involved, unsubscribing *via any route* should still unsubscribe and mark the queue-writer as complete +- Fix [#1101](https://github.com/StackExchange/StackExchange.Redis/issues/1101): When a `ChannelMessageQueue` is involved, unsubscribing *via any route* should still unsubscribe and mark the queue-writer as complete ## 2.0.588 @@ -303,7 +303,7 @@ No pending changes for the next release yet. ## 2.0.513 -- Fix [#961](https://github.com/StackExchange/StackExchange.Redis/issues/962) - fix assembly binding redirect problems; IMPORTANT: this drops to an older `System.Buffers` version - if you have manually added redirects for `4.0.3.0`, you may need to manually update to `4.0.2.0` (or remove completely) +- Fix [#961](https://github.com/StackExchange/StackExchange.Redis/issues/962): fix assembly binding redirect problems; IMPORTANT: this drops to an older `System.Buffers` version - if you have manually added redirects for `4.0.3.0`, you may need to manually update to `4.0.2.0` (or remove completely) - Fix [#962](https://github.com/StackExchange/StackExchange.Redis/issues/962): Avoid NRE in edge-case when fetching bridge ## 2.0.505 @@ -314,9 +314,7 @@ No pending changes for the next release yet. ## 2.0.495 -- 2.0 is a large - and breaking - change - -The key focus of this release is stability and reliability. +2.0 is a large - and breaking - change. The key focus of this release is stability and reliability. - **Hard Break**: The package identity has changed; instead of `StackExchange.Redis` (not strong-named) and `StackExchange.Redis.StrongName` (strong-named), we are now only releasing `StackExchange.Redis` (strong-named). This is a binary breaking change that requires consumers to be re-compiled; it cannot be applied via binding-redirects @@ -338,10 +336,9 @@ The key focus of this release is stability and reliability. - Fix: A *lot* of general bugs and issues have been resolved - **Break**: `RedisValue.TryParse` was accidentally omitted in the overhaul; this has been rectified and will be available in the next build -a more complete list of issues addressed can be seen in [this tracking issue](https://github.com/StackExchange/StackExchange.Redis/issues/871) +A more complete list of issues addressed can be seen in [this tracking issue](https://github.com/StackExchange/StackExchange.Redis/issues/871) -Note: we currently have no plans to do an additional `1.*` release. In particular, even though there was a `1.2.7-alpha` build on nuget, we *do not* currently have -plans to release `1.2.7`. +Note: we currently have no plans to do an additional `1.*` release. In particular, even though there was a `1.2.7-alpha` build on nuget, we *do not* currently have plans to release `1.2.7`. --- From d8aa7f8c19cff1ed3349bb6138ed4607033e04cb Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 24 Jan 2023 10:57:51 -0500 Subject: [PATCH 209/435] LuaScript: use invariant regex on parameter parsing (fixes #2350) (#2351) This changes the regex to use `RegexOptions.CultureInvariant` and adds an easy way to do tests like this in the future with `[Fact, TestCulture("tr-TR")]` for example, which will set and restore the culture for a specific test. Before/after break/fix test included for the Turkish case. --- docs/ReleaseNotes.md | 2 +- .../ScriptParameterMapper.cs | 2 +- .../Helpers/Attributes.cs | 47 +++++++++++++++++++ .../ScriptingTests.cs | 23 +++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 2fafa9f72..864c04a0a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -No pending changes for the next release yet. +- Fix [#2350](https://github.com/StackExchange/StackExchange.Redis/issues/2350): Properly parse lua script paramters in all cultures ([#2351 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2351)) ## 2.6.90 diff --git a/src/StackExchange.Redis/ScriptParameterMapper.cs b/src/StackExchange.Redis/ScriptParameterMapper.cs index 2c0e76314..88be5d338 100644 --- a/src/StackExchange.Redis/ScriptParameterMapper.cs +++ b/src/StackExchange.Redis/ScriptParameterMapper.cs @@ -24,7 +24,7 @@ public ScriptParameters(RedisKey[] keys, RedisValue[] args) } } - private static readonly Regex ParameterExtractor = new Regex(@"@(? ([a-z]|_) ([a-z]|_|\d)*)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + private static readonly Regex ParameterExtractor = new Regex(@"@(? ([a-z]|_) ([a-z]|_|\d)*)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.CultureInvariant); private static string[] ExtractParameters(string script) { diff --git a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs index aa285c15a..2dce70904 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Xunit.Abstractions; @@ -184,3 +186,48 @@ public static RunSummary Update(this RunSummary summary, SkippableMessageBus bus return summary; } } + +/// +/// Supports changing culture for the duration of a single test. +/// and with another culture. +/// +/// +/// Based on: https://bartwullems.blogspot.com/2022/03/xunit-change-culture-during-your-test.html +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +public class TestCultureAttribute : BeforeAfterTestAttribute +{ + private readonly CultureInfo culture; + private CultureInfo? originalCulture; + + /// + /// Replaces the culture and UI culture of the current thread with . + /// + /// The name of the culture. + public TestCultureAttribute(string culture) => this.culture = new CultureInfo(culture, false); + + /// + /// Stores the current and + /// and replaces them with the new cultures defined in the constructor. + /// + /// The method under test + public override void Before(MethodInfo methodUnderTest) + { + originalCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = culture; + CultureInfo.CurrentCulture.ClearCachedData(); + } + + /// + /// Restores the original to . + /// + /// The method under test + public override void After(MethodInfo methodUnderTest) + { + if (originalCulture is not null) + { + Thread.CurrentThread.CurrentCulture = originalCulture; + CultureInfo.CurrentCulture.ClearCachedData(); + } + } +} diff --git a/tests/StackExchange.Redis.Tests/ScriptingTests.cs b/tests/StackExchange.Redis.Tests/ScriptingTests.cs index 8164983de..ad290259e 100644 --- a/tests/StackExchange.Redis.Tests/ScriptingTests.cs +++ b/tests/StackExchange.Redis.Tests/ScriptingTests.cs @@ -1113,6 +1113,29 @@ public async Task TestEvalShaReadOnlyAsync() Assert.Equal("bar", result.ToString()); } + [Fact, TestCulture("en-US")] + public void LuaScriptEnglishParameters() => LuaScriptParameterShared(); + + [Fact, TestCulture("tr-TR")] + public void LuaScriptTurkishParameters() => LuaScriptParameterShared(); + + private void LuaScriptParameterShared() + { + const string Script = "redis.call('set', @key, @testIId)"; + var prepared = LuaScript.Prepare(Script); + var key = Me(); + var p = new { key = (RedisKey)key, testIId = "hello" }; + + prepared.ExtractParameters(p, null, out RedisKey[]? keys, out RedisValue[]? args); + Assert.NotNull(keys); + Assert.Single(keys); + Assert.Equal(key, keys[0]); + Assert.NotNull(args); + Assert.Equal(2, args.Length); + Assert.Equal(key, args[0]); + Assert.Equal("hello", args[1]); + } + private static void TestNullValue(RedisResult? value) { Assert.True(value == null || value.IsNull); From b94a8cf57fee9088ae8cf19130c9134be5909dbf Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 8 Feb 2023 06:58:13 -0500 Subject: [PATCH 210/435] Fix #2362: Set FailureType to AuthenticationFailure for auth exceptions (#2367) --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ExceptionFactory.cs | 4 +++- tests/StackExchange.Redis.Tests/SecureTests.cs | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 864c04a0a..a8359c4a8 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Fix [#2350](https://github.com/StackExchange/StackExchange.Redis/issues/2350): Properly parse lua script paramters in all cultures ([#2351 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2351)) +- Fix [#2362](https://github.com/StackExchange/StackExchange.Redis/issues/2362): Set `RedisConnectionException.FailureType` to `AuthenticationFailure` on all authentication scenarios for better handling ([#2367 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2367)) ## 2.6.90 diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 07f172129..c18a7994c 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -386,10 +386,12 @@ internal static Exception UnableToConnect(ConnectionMultiplexer muxer, string? f { var sb = new StringBuilder("It was not possible to connect to the redis server(s)."); Exception? inner = null; + var failureType = ConnectionFailureType.UnableToConnect; if (muxer is not null) { if (muxer.AuthException is Exception aex) { + failureType = ConnectionFailureType.AuthenticationFailure; sb.Append(" There was an authentication failure; check that passwords (or client certificates) are configured correctly: (").Append(aex.GetType().Name).Append(") ").Append(aex.Message); inner = aex; if (aex is AuthenticationException && aex.InnerException is Exception iaex) @@ -407,7 +409,7 @@ internal static Exception UnableToConnect(ConnectionMultiplexer muxer, string? f sb.Append(' ').Append(failureMessage.Trim()); } - return new RedisConnectionException(ConnectionFailureType.UnableToConnect, sb.ToString(), inner); + return new RedisConnectionException(failureType, sb.ToString(), inner); } internal static Exception BeganProfilingWithDuplicateContext(object forContext) diff --git a/tests/StackExchange.Redis.Tests/SecureTests.cs b/tests/StackExchange.Redis.Tests/SecureTests.cs index 1c6989daf..454dedc68 100644 --- a/tests/StackExchange.Redis.Tests/SecureTests.cs +++ b/tests/StackExchange.Redis.Tests/SecureTests.cs @@ -76,7 +76,8 @@ public async Task ConnectWithWrongPassword(string password, string exepctedMessa conn.GetDatabase().Ping(); }).ConfigureAwait(false); - Log("Exception: " + ex.Message); + Log($"Exception ({ex.FailureType}): {ex.Message}"); + Assert.Equal(ConnectionFailureType.AuthenticationFailure, ex.FailureType); Assert.StartsWith("It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly: (RedisServerException) ", ex.Message); // This changed in some version...not sure which. For our purposes, splitting on v3 vs v6+ From 51a7d908ed5ef2756c518a4332bf859518a6173f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 9 Feb 2023 15:37:39 +0000 Subject: [PATCH 211/435] Implement RedisValue.Length for all underlying storage kinds (#2370) * fix #2368 - implement Length() for other encodings (using format layout) - unify format code - switch to C# 11 for u8 strings (needed a few "scoped" modifiers adding) - tests for format and Length * tweak langver * cleanup double format * use 7.0.101 SDK (102 not yet on ubuntu?) * tweak SDK in CI.yml; add CI.yml to sln * We need Redis 6 runtime for tests, so let's grab both * Add release notes --------- Co-authored-by: Nick Craver --- .github/workflows/CI.yml | 2 + Directory.Build.props | 2 +- StackExchange.Redis.sln | 1 + appveyor.yml | 2 +- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/BufferReader.cs | 2 +- src/StackExchange.Redis/Format.cs | 76 ++++++++++++++++- src/StackExchange.Redis/PhysicalConnection.cs | 54 +++++------- src/StackExchange.Redis/RawResult.cs | 4 +- src/StackExchange.Redis/RedisValue.cs | 14 +-- .../StackExchange.Redis.Tests/FormatTests.cs | 85 ++++++++++++++++++- .../RedisValueEquivalencyTests.cs | 51 ++++++++++- .../RedisRequest.cs | 2 +- 13 files changed, 245 insertions(+), 51 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index bbf7997c7..7624fb699 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -24,6 +24,7 @@ jobs: with: dotnet-version: | 6.0.x + 7.0.x - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true - name: Start Redis Services (docker-compose) @@ -56,6 +57,7 @@ jobs: # with: # dotnet-version: | # 6.0.x + # 7.0.x - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true - name: Start Redis Services (v3.0.503) diff --git a/Directory.Build.props b/Directory.Build.props index 0d57b6c7f..9f3216a7b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,7 +15,7 @@ https://stackexchange.github.io/StackExchange.Redis/ MIT - 10.0 + 11 git https://github.com/StackExchange/StackExchange.Redis/ diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index 12f52c00d..cdd254217 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution build.cmd = build.cmd Build.csproj = Build.csproj build.ps1 = build.ps1 + .github\workflows\CI.yml = .github\workflows\CI.yml Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets Directory.Packages.props = Directory.Packages.props diff --git a/appveyor.yml b/appveyor.yml index 8e774d89c..335309289 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,7 +6,7 @@ init: install: - cmd: >- - choco install dotnet-sdk --version 6.0.101 + choco install dotnet-sdk --version 7.0.102 cd tests\RedisConfigs\3.0.503 diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index a8359c4a8..265fda3a1 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: - Fix [#2350](https://github.com/StackExchange/StackExchange.Redis/issues/2350): Properly parse lua script paramters in all cultures ([#2351 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2351)) - Fix [#2362](https://github.com/StackExchange/StackExchange.Redis/issues/2362): Set `RedisConnectionException.FailureType` to `AuthenticationFailure` on all authentication scenarios for better handling ([#2367 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2367)) +- Fix [#2368](https://github.com/StackExchange/StackExchange.Redis/issues/2368): Support `RedisValue.Length()` for all storage types ([#2370 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2370)) ## 2.6.90 diff --git a/src/StackExchange.Redis/BufferReader.cs b/src/StackExchange.Redis/BufferReader.cs index 6403053d9..a7199fe6b 100644 --- a/src/StackExchange.Redis/BufferReader.cs +++ b/src/StackExchange.Redis/BufferReader.cs @@ -44,7 +44,7 @@ private bool FetchNextSegment() return true; } - public BufferReader(ReadOnlySequence buffer) + public BufferReader(scoped in ReadOnlySequence buffer) { _buffer = buffer; _lastSnapshotPosition = buffer.Start; diff --git a/src/StackExchange.Redis/Format.cs b/src/StackExchange.Redis/Format.cs index cdf1aef80..9c96ccebe 100644 --- a/src/StackExchange.Redis/Format.cs +++ b/src/StackExchange.Redis/Format.cs @@ -174,7 +174,7 @@ internal static bool TryParseInt64(ReadOnlySpan s, out long value) => internal static bool CouldBeInteger(string s) { - if (string.IsNullOrEmpty(s) || s.Length > PhysicalConnection.MaxInt64TextLen) return false; + if (string.IsNullOrEmpty(s) || s.Length > Format.MaxInt64TextLen) return false; bool isSigned = s[0] == '-'; for (int i = isSigned ? 1 : 0; i < s.Length; i++) { @@ -185,7 +185,7 @@ internal static bool CouldBeInteger(string s) } internal static bool CouldBeInteger(ReadOnlySpan s) { - if (s.IsEmpty | s.Length > PhysicalConnection.MaxInt64TextLen) return false; + if (s.IsEmpty | s.Length > Format.MaxInt64TextLen) return false; bool isSigned = s[0] == '-'; for (int i = isSigned ? 1 : 0; i < s.Length; i++) { @@ -355,5 +355,77 @@ internal static unsafe string GetString(ReadOnlySpan span) return Encoding.UTF8.GetString(ptr, span.Length); } } + + [DoesNotReturn] + private static void ThrowFormatFailed() => throw new InvalidOperationException("TryFormat failed"); + + internal const int + MaxInt32TextLen = 11, // -2,147,483,648 (not including the commas) + MaxInt64TextLen = 20; // -9,223,372,036,854,775,808 (not including the commas) + + internal static int MeasureDouble(double value) + { + if (double.IsInfinity(value)) return 4; // +inf / -inf + var s = value.ToString("G17", NumberFormatInfo.InvariantInfo); // this looks inefficient, but is how Utf8Formatter works too, just: more direct + return s.Length; + } + + internal static int FormatDouble(double value, Span destination) + { + if (double.IsInfinity(value)) + { + if (double.IsPositiveInfinity(value)) + { + if (!"+inf"u8.TryCopyTo(destination)) ThrowFormatFailed(); + } + else + { + if (!"-inf"u8.TryCopyTo(destination)) ThrowFormatFailed(); + } + return 4; + } + var s = value.ToString("G17", NumberFormatInfo.InvariantInfo); // this looks inefficient, but is how Utf8Formatter works too, just: more direct + if (s.Length > destination.Length) ThrowFormatFailed(); + + var chars = s.AsSpan(); + for (int i = 0; i < chars.Length; i++) + { + destination[i] = (byte)chars[i]; + } + return chars.Length; + } + + internal static int MeasureInt64(long value) + { + Span valueSpan = stackalloc byte[MaxInt64TextLen]; + return FormatInt64(value, valueSpan); + } + + internal static int FormatInt64(long value, Span destination) + { + if (!Utf8Formatter.TryFormat(value, destination, out var len)) + ThrowFormatFailed(); + return len; + } + + internal static int MeasureUInt64(ulong value) + { + Span valueSpan = stackalloc byte[MaxInt64TextLen]; + return FormatUInt64(value, valueSpan); + } + + internal static int FormatUInt64(ulong value, Span destination) + { + if (!Utf8Formatter.TryFormat(value, destination, out var len)) + ThrowFormatFailed(); + return len; + } + + internal static int FormatInt32(int value, Span destination) + { + if (!Utf8Formatter.TryFormat(value, destination, out var len)) + ThrowFormatFailed(); + return len; + } } } diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 6bfb872af..2ba402119 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -1,6 +1,7 @@ -using System; +using Pipelines.Sockets.Unofficial; +using Pipelines.Sockets.Unofficial.Arenas; +using System; using System.Buffers; -using System.Buffers.Text; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -15,8 +16,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Pipelines.Sockets.Unofficial; -using Pipelines.Sockets.Unofficial.Arenas; namespace StackExchange.Redis { @@ -449,7 +448,7 @@ void add(string lk, string sk, string? v) add("Outstanding-Responses", "outstanding", GetSentAwaitingResponseCount().ToString()); add("Last-Read", "last-read", (unchecked(now - lastRead) / 1000) + "s ago"); add("Last-Write", "last-write", (unchecked(now - lastWrite) / 1000) + "s ago"); - if(unansweredWriteTime != 0) add("Unanswered-Write", "unanswered-write", (unchecked(now - unansweredWriteTime) / 1000) + "s ago"); + if (unansweredWriteTime != 0) add("Unanswered-Write", "unanswered-write", (unchecked(now - unansweredWriteTime) / 1000) + "s ago"); add("Keep-Alive", "keep-alive", bridge.ServerEndPoint?.WriteEverySeconds + "s"); add("Previous-Physical-State", "state", oldState.ToString()); add("Manager", "mgr", bridge.Multiplexer.SocketManager?.GetState()); @@ -777,8 +776,7 @@ internal static void WriteBulkString(in RedisValue value, PipeWriter output) internal void WriteHeader(RedisCommand command, int arguments, CommandBytes commandBytes = default) { - var bridge = BridgeCouldBeNull; - if (bridge == null) throw new ObjectDisposedException(ToString()); + var bridge = BridgeCouldBeNull ?? throw new ObjectDisposedException(ToString()); if (command == RedisCommand.UNKNOWN) { @@ -801,7 +799,7 @@ internal void WriteHeader(RedisCommand command, int arguments, CommandBytes comm // *{argCount}\r\n = 3 + MaxInt32TextLen // ${cmd-len}\r\n = 3 + MaxInt32TextLen // {cmd}\r\n = 2 + commandBytes.Length - var span = _ioPipe!.Output.GetSpan(commandBytes.Length + 8 + MaxInt32TextLen + MaxInt32TextLen); + var span = _ioPipe!.Output.GetSpan(commandBytes.Length + 8 + Format.MaxInt32TextLen + Format.MaxInt32TextLen); span[0] = (byte)'*'; int offset = WriteRaw(span, arguments + 1, offset: 1); @@ -817,16 +815,12 @@ internal void WriteHeader(RedisCommand command, int arguments, CommandBytes comm internal static void WriteMultiBulkHeader(PipeWriter output, long count) { // *{count}\r\n = 3 + MaxInt32TextLen - var span = output.GetSpan(3 + MaxInt32TextLen); + var span = output.GetSpan(3 + Format.MaxInt32TextLen); span[0] = (byte)'*'; int offset = WriteRaw(span, count, offset: 1); output.Advance(offset); } - internal const int - MaxInt32TextLen = 11, // -2,147,483,648 (not including the commas) - MaxInt64TextLen = 20; // -9,223,372,036,854,775,808 (not including the commas) - [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int WriteCrlf(Span span, int offset) { @@ -906,25 +900,16 @@ internal static int WriteRaw(Span span, long value, bool withLengthPrefix { // we're going to write it, but *to the wrong place* var availableChunk = span.Slice(offset); - if (!Utf8Formatter.TryFormat(value, availableChunk, out int formattedLength)) - { - throw new InvalidOperationException("TryFormat failed"); - } + var formattedLength = Format.FormatInt64(value, availableChunk); if (withLengthPrefix) { // now we know how large the prefix is: write the prefix, then write the value - if (!Utf8Formatter.TryFormat(formattedLength, availableChunk, out int prefixLength)) - { - throw new InvalidOperationException("TryFormat failed"); - } + var prefixLength = Format.FormatInt32(formattedLength, availableChunk); offset += prefixLength; offset = WriteCrlf(span, offset); availableChunk = span.Slice(offset); - if (!Utf8Formatter.TryFormat(value, availableChunk, out int finalLength)) - { - throw new InvalidOperationException("TryFormat failed"); - } + var finalLength = Format.FormatInt64(value, availableChunk); offset += finalLength; Debug.Assert(finalLength == formattedLength); } @@ -1035,7 +1020,7 @@ private static void WriteUnifiedSpan(PipeWriter writer, ReadOnlySpan value } else if (value.Length <= MaxQuickSpanSize) { - var span = writer.GetSpan(5 + MaxInt32TextLen + value.Length); + var span = writer.GetSpan(5 + Format.MaxInt32TextLen + value.Length); span[0] = (byte)'$'; int bytes = AppendToSpan(span, value, 1); writer.Advance(bytes); @@ -1043,7 +1028,7 @@ private static void WriteUnifiedSpan(PipeWriter writer, ReadOnlySpan value else { // too big to guarantee can do in a single span - var span = writer.GetSpan(3 + MaxInt32TextLen); + var span = writer.GetSpan(3 + Format.MaxInt32TextLen); span[0] = (byte)'$'; int bytes = WriteRaw(span, value.Length, offset: 1); writer.Advance(bytes); @@ -1136,7 +1121,7 @@ internal static void WriteUnifiedPrefixedString(PipeWriter writer, byte[]? prefi } else { - var span = writer.GetSpan(3 + MaxInt32TextLen); + var span = writer.GetSpan(3 + Format.MaxInt32TextLen); span[0] = (byte)'$'; int bytes = WriteRaw(span, totalLength, offset: 1); writer.Advance(bytes); @@ -1228,7 +1213,7 @@ private static void WriteUnifiedPrefixedBlob(PipeWriter writer, byte[]? prefix, } else { - var span = writer.GetSpan(3 + MaxInt32TextLen); // note even with 2 max-len, we're still in same text range + var span = writer.GetSpan(3 + Format.MaxInt32TextLen); // note even with 2 max-len, we're still in same text range span[0] = (byte)'$'; int bytes = WriteRaw(span, prefix.LongLength + value.LongLength, offset: 1); writer.Advance(bytes); @@ -1249,7 +1234,7 @@ private static void WriteUnifiedInt64(PipeWriter writer, long value) // ${asc-len}\r\n = 3 + MaxInt32TextLen // {asc}\r\n = MaxInt64TextLen + 2 - var span = writer.GetSpan(5 + MaxInt32TextLen + MaxInt64TextLen); + var span = writer.GetSpan(5 + Format.MaxInt32TextLen + Format.MaxInt64TextLen); span[0] = (byte)'$'; var bytes = WriteRaw(span, value, withLengthPrefix: true, offset: 1); @@ -1263,11 +1248,10 @@ private static void WriteUnifiedUInt64(PipeWriter writer, ulong value) // ${asc-len}\r\n = 3 + MaxInt32TextLen // {asc}\r\n = MaxInt64TextLen + 2 - var span = writer.GetSpan(5 + MaxInt32TextLen + MaxInt64TextLen); + var span = writer.GetSpan(5 + Format.MaxInt32TextLen + Format.MaxInt64TextLen); - Span valueSpan = stackalloc byte[MaxInt64TextLen]; - if (!Utf8Formatter.TryFormat(value, valueSpan, out var len)) - throw new InvalidOperationException("TryFormat failed"); + Span valueSpan = stackalloc byte[Format.MaxInt64TextLen]; + var len = Format.FormatUInt64(value, valueSpan); span[0] = (byte)'$'; int offset = WriteRaw(span, len, withLengthPrefix: false, offset: 1); valueSpan.Slice(0, len).CopyTo(span.Slice(offset)); @@ -1280,7 +1264,7 @@ internal static void WriteInteger(PipeWriter writer, long value) //note: client should never write integer; only server does this // :{asc}\r\n = MaxInt64TextLen + 3 - var span = writer.GetSpan(3 + MaxInt64TextLen); + var span = writer.GetSpan(3 + Format.MaxInt64TextLen); span[0] = (byte)':'; var bytes = WriteRaw(span, value, withLengthPrefix: false, offset: 1); diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 8a4f4cf88..fc189f10c 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -77,7 +77,7 @@ internal ref struct Tokenizer public Tokenizer GetEnumerator() => this; private BufferReader _value; - public Tokenizer(in ReadOnlySequence value) + public Tokenizer(scoped in ReadOnlySequence value) { _value = new BufferReader(value); Current = default; @@ -384,7 +384,7 @@ internal bool TryGetDouble(out double val) internal bool TryGetInt64(out long value) { - if (IsNull || Payload.IsEmpty || Payload.Length > PhysicalConnection.MaxInt64TextLen) + if (IsNull || Payload.IsEmpty || Payload.Length > Format.MaxInt64TextLen) { value = 0; return false; diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index 45f23474b..d2f8a1ca9 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -333,6 +333,9 @@ internal StorageType Type StorageType.Null => 0, StorageType.Raw => _memory.Length, StorageType.String => Encoding.UTF8.GetByteCount((string)_objectOrSentinel!), + StorageType.Int64 => Format.MeasureInt64(OverlappedValueInt64), + StorageType.UInt64 => Format.MeasureUInt64(OverlappedValueUInt64), + StorageType.Double => Format.MeasureDouble(OverlappedValueDouble), _ => throw new InvalidOperationException("Unable to compute length of type: " + Type), }; @@ -824,16 +827,15 @@ private static string ToHex(ReadOnlySpan src) return value._memory.ToArray(); case StorageType.Int64: - Span span = stackalloc byte[PhysicalConnection.MaxInt64TextLen + 2]; + Span span = stackalloc byte[Format.MaxInt64TextLen + 2]; int len = PhysicalConnection.WriteRaw(span, value.OverlappedValueInt64, false, 0); arr = new byte[len - 2]; // don't need the CRLF span.Slice(0, arr.Length).CopyTo(arr); return arr; case StorageType.UInt64: // we know it is a huge value - just jump straight to Utf8Formatter - span = stackalloc byte[PhysicalConnection.MaxInt64TextLen]; - if (!Utf8Formatter.TryFormat(value.OverlappedValueUInt64, span, out len)) - throw new InvalidOperationException("TryFormat failed"); + span = stackalloc byte[Format.MaxInt64TextLen]; + len = Format.FormatUInt64(value.OverlappedValueUInt64, span); arr = new byte[len]; span.Slice(0, len).CopyTo(arr); return arr; @@ -1123,11 +1125,11 @@ private ReadOnlyMemory AsMemory(out byte[]? leased) s = Format.ToString(OverlappedValueDouble); goto HaveString; case StorageType.Int64: - leased = ArrayPool.Shared.Rent(PhysicalConnection.MaxInt64TextLen + 2); // reused code has CRLF terminator + leased = ArrayPool.Shared.Rent(Format.MaxInt64TextLen + 2); // reused code has CRLF terminator len = PhysicalConnection.WriteRaw(leased, OverlappedValueInt64) - 2; // drop the CRLF return new ReadOnlyMemory(leased, 0, len); case StorageType.UInt64: - leased = ArrayPool.Shared.Rent(PhysicalConnection.MaxInt64TextLen); // reused code has CRLF terminator + leased = ArrayPool.Shared.Rent(Format.MaxInt64TextLen); // reused code has CRLF terminator // value is huge, jump direct to Utf8Formatter if (!Utf8Formatter.TryFormat(OverlappedValueUInt64, leased, out len)) throw new InvalidOperationException("TryFormat failed"); diff --git a/tests/StackExchange.Redis.Tests/FormatTests.cs b/tests/StackExchange.Redis.Tests/FormatTests.cs index 8da9262fc..bb2f18740 100644 --- a/tests/StackExchange.Redis.Tests/FormatTests.cs +++ b/tests/StackExchange.Redis.Tests/FormatTests.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Net; +using System.Text; using Xunit; using Xunit.Abstractions; @@ -78,4 +80,85 @@ public void ClientFlagsFormatting(ClientFlags value, string expected) [InlineData(ReplicationChangeOptions.Broadcast | ReplicationChangeOptions.SetTiebreaker | ReplicationChangeOptions.ReplicateToOtherEndpoints, "All")] public void ReplicationChangeOptionsFormatting(ReplicationChangeOptions value, string expected) => Assert.Equal(expected, value.ToString()); + + + [Theory] + [InlineData(0, "0")] + [InlineData(1, "1")] + [InlineData(-1, "-1")] + [InlineData(100, "100")] + [InlineData(-100, "-100")] + [InlineData(int.MaxValue, "2147483647")] + [InlineData(int.MinValue, "-2147483648")] + public unsafe void FormatInt32(int value, string expectedValue) + { + Span dest = stackalloc byte[expectedValue.Length]; + Assert.Equal(expectedValue.Length, Format.FormatInt32(value, dest)); + fixed (byte* s = dest) + { + Assert.Equal(expectedValue, Encoding.ASCII.GetString(s, expectedValue.Length)); + } + } + + [Theory] + [InlineData(0, "0")] + [InlineData(1, "1")] + [InlineData(-1, "-1")] + [InlineData(100, "100")] + [InlineData(-100, "-100")] + [InlineData(long.MaxValue, "9223372036854775807")] + [InlineData(long.MinValue, "-9223372036854775808")] + public unsafe void FormatInt64(long value, string expectedValue) + { + Assert.Equal(expectedValue.Length, Format.MeasureInt64(value)); + Span dest = stackalloc byte[expectedValue.Length]; + Assert.Equal(expectedValue.Length, Format.FormatInt64(value, dest)); + fixed (byte* s = dest) + { + Assert.Equal(expectedValue, Encoding.ASCII.GetString(s, expectedValue.Length)); + } + } + + [Theory] + [InlineData(0, "0")] + [InlineData(1, "1")] + [InlineData(100, "100")] + [InlineData(ulong.MaxValue, "18446744073709551615")] + public unsafe void FormatUInt64(ulong value, string expectedValue) + { + Assert.Equal(expectedValue.Length, Format.MeasureUInt64(value)); + Span dest = stackalloc byte[expectedValue.Length]; + Assert.Equal(expectedValue.Length, Format.FormatUInt64(value, dest)); + fixed (byte* s = dest) + { + Assert.Equal(expectedValue, Encoding.ASCII.GetString(s, expectedValue.Length)); + } + } + + [Theory] + [InlineData(0, "0")] + [InlineData(1, "1")] + [InlineData(-1, "-1")] + [InlineData(0.5, "0.5")] + [InlineData(0.50001, "0.50000999999999995")] + [InlineData(Math.PI, "3.1415926535897931")] + [InlineData(100, "100")] + [InlineData(-100, "-100")] + [InlineData(double.MaxValue, "1.7976931348623157E+308")] + [InlineData(double.MinValue, "-1.7976931348623157E+308")] + [InlineData(double.Epsilon, "4.9406564584124654E-324")] + [InlineData(double.PositiveInfinity, "+inf")] + [InlineData(double.NegativeInfinity, "-inf")] + [InlineData(double.NaN, "NaN")] // never used in normal code + + public unsafe void FormatDouble(double value, string expectedValue) + { + Assert.Equal(expectedValue.Length, Format.MeasureDouble(value)); + Span dest = stackalloc byte[expectedValue.Length]; + Assert.Equal(expectedValue.Length, Format.FormatDouble(value, dest)); + fixed (byte* s = dest) + { + Assert.Equal(expectedValue, Encoding.ASCII.GetString(s, expectedValue.Length)); + } + } } diff --git a/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs b/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs index 6aec46cf8..2d9c69fad 100644 --- a/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs +++ b/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System; +using System.Runtime.CompilerServices; using System.Text; using Xunit; @@ -278,4 +279,52 @@ public void TryParseDouble() Assert.False(((RedisValue)"abc").TryParse(out double _)); } + + [Fact] + public void RedisValueLengthString() + { + RedisValue value = "abc"; + Assert.Equal(RedisValue.StorageType.String, value.Type); + Assert.Equal(3, value.Length()); + } + + [Fact] + public void RedisValueLengthDouble() + { + RedisValue value = Math.PI; + Assert.Equal(RedisValue.StorageType.Double, value.Type); + Assert.Equal(18, value.Length()); + } + + [Fact] + public void RedisValueLengthInt64() + { + RedisValue value = 123; + Assert.Equal(RedisValue.StorageType.Int64, value.Type); + Assert.Equal(3, value.Length()); + } + + [Fact] + public void RedisValueLengthUInt64() + { + RedisValue value = ulong.MaxValue - 5; + Assert.Equal(RedisValue.StorageType.UInt64, value.Type); + Assert.Equal(20, value.Length()); + } + + [Fact] + public void RedisValueLengthRaw() + { + RedisValue value = new byte[] { 0, 1, 2 }; + Assert.Equal(RedisValue.StorageType.Raw, value.Type); + Assert.Equal(3, value.Length()); + } + + [Fact] + public void RedisValueLengthNull() + { + RedisValue value = RedisValue.Null; + Assert.Equal(RedisValue.StorageType.Null, value.Type); + Assert.Equal(0, value.Length()); + } } diff --git a/toys/StackExchange.Redis.Server/RedisRequest.cs b/toys/StackExchange.Redis.Server/RedisRequest.cs index fda9474c9..36d73a4bc 100644 --- a/toys/StackExchange.Redis.Server/RedisRequest.cs +++ b/toys/StackExchange.Redis.Server/RedisRequest.cs @@ -28,7 +28,7 @@ public bool IsString(int index, string value) // TODO: optimize => string.Equals(value, _inner[index].GetString(), StringComparison.OrdinalIgnoreCase); public override int GetHashCode() => throw new NotSupportedException(); - internal RedisRequest(in RawResult result) + internal RedisRequest(scoped in RawResult result) { _inner = result; Count = result.ItemsCount; From ab757f7299bbff195d0acddf555434401e44e7d2 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 20 Feb 2023 14:45:17 +0000 Subject: [PATCH 212/435] fix #2376 - avoid deadlock scenario when completing dead connections (#2378) * fix #2376 1. to fix the immediate scenario: don't hold the queue lock when we abort things - only hold it when fetching next 2. to avoid similar not yet seen: in GetHeadMessages, don't blindly wait forever also standardise on TryPeek/TryDequeue * ExecuteSyncImpl: don't hold the lock-obj when throwing for timeout * use placeholder message when unable to query the connection queue * release notes --- docs/ReleaseNotes.md | 1 + .../ConnectionMultiplexer.cs | 12 ++- .../ExtensionMethods.Internal.cs | 26 ++++++- src/StackExchange.Redis/Message.cs | 10 +++ src/StackExchange.Redis/PhysicalConnection.cs | 73 +++++++++++-------- 5 files changed, 89 insertions(+), 33 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 265fda3a1..3557d2ab4 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -11,6 +11,7 @@ Current package versions: - Fix [#2350](https://github.com/StackExchange/StackExchange.Redis/issues/2350): Properly parse lua script paramters in all cultures ([#2351 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2351)) - Fix [#2362](https://github.com/StackExchange/StackExchange.Redis/issues/2362): Set `RedisConnectionException.FailureType` to `AuthenticationFailure` on all authentication scenarios for better handling ([#2367 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2367)) - Fix [#2368](https://github.com/StackExchange/StackExchange.Redis/issues/2368): Support `RedisValue.Length()` for all storage types ([#2370 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2370)) +- Fix [#2376](https://github.com/StackExchange/StackExchange.Redis/issues/2376): Avoid a (rare) deadlock scenario ([#2378 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2378)) ## 2.6.90 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index ea6ed6885..b94e4650b 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1959,6 +1959,7 @@ internal static void ThrowFailed(TaskCompletionSource? source, Exception u { var source = SimpleResultBox.Get(); + bool timeout = false; lock (source) { #pragma warning disable CS0618 // Type or member is obsolete @@ -1976,11 +1977,16 @@ internal static void ThrowFailed(TaskCompletionSource? source, Exception u else { Trace("Timeout performing " + message); - Interlocked.Increment(ref syncTimeouts); - throw ExceptionFactory.Timeout(this, null, message, server); - // Very important not to return "source" to the pool here + timeout = true; } } + + if (timeout) // note we throw *outside* of the main lock to avoid deadlock scenarios (#2376) + { + Interlocked.Increment(ref syncTimeouts); + // Very important not to return "source" to the pool here + throw ExceptionFactory.Timeout(this, null, message, server); + } // Snapshot these so that we can recycle the box var val = source.GetResult(out var ex, canRecycle: true); // now that we aren't locking it... if (ex != null) throw ex; diff --git a/src/StackExchange.Redis/ExtensionMethods.Internal.cs b/src/StackExchange.Redis/ExtensionMethods.Internal.cs index 6b29c4d45..de5a9f2a6 100644 --- a/src/StackExchange.Redis/ExtensionMethods.Internal.cs +++ b/src/StackExchange.Redis/ExtensionMethods.Internal.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace StackExchange.Redis { @@ -9,5 +10,28 @@ internal static bool IsNullOrEmpty([NotNullWhen(false)] this string? s) => internal static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? s) => string.IsNullOrWhiteSpace(s); + +#if !NETCOREAPP3_1_OR_GREATER + internal static bool TryDequeue(this Queue queue, [NotNullWhen(true)] out T? result) + { + if (queue.Count == 0) + { + result = default; + return false; + } + result = queue.Dequeue()!; + return true; + } + internal static bool TryPeek(this Queue queue, [NotNullWhen(true)] out T? result) + { + if (queue.Count == 0) + { + result = default; + return false; + } + result = queue.Peek()!; + return true; + } +#endif } } diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index e9c35925a..d489b1c7c 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -1566,5 +1566,15 @@ protected override void WriteImpl(PhysicalConnection physical) } public override int ArgCount => 1; } + + // this is a placeholder message for use when (for example) unable to queue the + // connection queue due to a lock timeout + internal sealed class UnknownMessage : Message + { + public static UnknownMessage Instance { get; } = new(); + private UnknownMessage() : base(0, CommandFlags.None, RedisCommand.UNKNOWN) { } + public override int ArgCount => 0; + protected override void WriteImpl(PhysicalConnection physical) => throw new InvalidOperationException("This message cannot be written"); + } } } diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 2ba402119..2c5fc3c0d 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Pipelines; using System.Linq; @@ -16,6 +17,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using static StackExchange.Redis.Message; namespace StackExchange.Redis { @@ -396,9 +398,8 @@ public void RecordConnectionFailed( lock (_writtenAwaitingResponse) { // find oldest message awaiting a response - if (_writtenAwaitingResponse.Count != 0) + if (_writtenAwaitingResponse.TryPeek(out var next)) { - var next = _writtenAwaitingResponse.Peek(); unansweredWriteTime = next.GetWriteTime(); } } @@ -478,34 +479,42 @@ void add(string lk, string sk, string? v) bridge?.OnConnectionFailed(this, failureType, outerException); } } - // cleanup + // clean up (note: avoid holding the lock when we complete things, even if this means taking + // the lock multiple times; this is fine here - we shouldn't be fighting anyone, and we're already toast) lock (_writtenAwaitingResponse) { bridge?.Trace(_writtenAwaitingResponse.Count != 0, "Failing outstanding messages: " + _writtenAwaitingResponse.Count); - while (_writtenAwaitingResponse.Count != 0) - { - var next = _writtenAwaitingResponse.Dequeue(); + } - if (next.Command == RedisCommand.QUIT && next.TrySetResult(true)) - { - // fine, death of a socket is close enough - next.Complete(); - } - else + while (TryDequeueLocked(_writtenAwaitingResponse, out var next)) + { + if (next.Command == RedisCommand.QUIT && next.TrySetResult(true)) + { + // fine, death of a socket is close enough + next.Complete(); + } + else + { + var ex = innerException is RedisException ? innerException : outerException; + if (bridge != null) { - var ex = innerException is RedisException ? innerException : outerException; - if (bridge != null) - { - bridge.Trace("Failing: " + next); - bridge.Multiplexer?.OnMessageFaulted(next, ex, origin); - } - next.SetExceptionAndComplete(ex!, bridge); + bridge.Trace("Failing: " + next); + bridge.Multiplexer?.OnMessageFaulted(next, ex, origin); } + next.SetExceptionAndComplete(ex!, bridge); } } // burn the socket Shutdown(); + + static bool TryDequeueLocked(Queue queue, [NotNullWhen(true)] out Message? message) + { + lock (queue) + { + return queue.TryDequeue(out message); + } + } } internal bool IsIdle() => _writeStatus == WriteStatus.Idle; @@ -1580,18 +1589,10 @@ private void MatchResult(in RawResult result) _readStatus = ReadStatus.DequeueResult; lock (_writtenAwaitingResponse) { -#if NET5_0_OR_GREATER if (!_writtenAwaitingResponse.TryDequeue(out msg)) { throw new InvalidOperationException("Received response with no message waiting: " + result.ToString()); } -#else - if (_writtenAwaitingResponse.Count == 0) - { - throw new InvalidOperationException("Received response with no message waiting: " + result.ToString()); - } - msg = _writtenAwaitingResponse.Dequeue(); -#endif } _activeMessage = msg; @@ -1632,9 +1633,23 @@ static bool TryGetPubSubPayload(in RawResult value, out RedisValue parsed, bool internal void GetHeadMessages(out Message? now, out Message? next) { now = _activeMessage; - lock(_writtenAwaitingResponse) + bool haveLock = false; + try + { + // careful locking here; a: don't try too hard (this is error info only), b: avoid deadlock (see #2376) + Monitor.TryEnter(_writtenAwaitingResponse, 10, ref haveLock); + if (haveLock) + { + _writtenAwaitingResponse.TryPeek(out next); + } + else + { + next = UnknownMessage.Instance; + } + } + finally { - next = _writtenAwaitingResponse.Count == 0 ? null : _writtenAwaitingResponse.Peek(); + if (haveLock) Monitor.Exit(_writtenAwaitingResponse); } } From 6fa6a794d3204e664d4caa285d4ae589954966ae Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 21 Feb 2023 07:21:31 -0500 Subject: [PATCH 213/435] 2.6.96 Release Notes --- docs/ReleaseNotes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 3557d2ab4..84781cbad 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,10 @@ Current package versions: ## Unreleased +No pending changes. + +## 2.6.96 + - Fix [#2350](https://github.com/StackExchange/StackExchange.Redis/issues/2350): Properly parse lua script paramters in all cultures ([#2351 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2351)) - Fix [#2362](https://github.com/StackExchange/StackExchange.Redis/issues/2362): Set `RedisConnectionException.FailureType` to `AuthenticationFailure` on all authentication scenarios for better handling ([#2367 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2367)) - Fix [#2368](https://github.com/StackExchange/StackExchange.Redis/issues/2368): Support `RedisValue.Length()` for all storage types ([#2370 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2370)) From 867b04dc188a38a1d88993aa7be2a63ef783de04 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 28 Feb 2023 15:18:29 -0500 Subject: [PATCH 214/435] AppVeyor: move to VS 2022 image (#2387) * AppVeyor: move to VS 2022 image The update blog post (https://www.appveyor.com/updates/2023/02/24/) doesn't match the inventory (https://www.appveyor.com/docs/windows-images-software/), primarily: .NET 6 SDK which is LTS is no longer on the 2019 image which seems like an oops. Bypassing the issue for now by getting on the 2022 image. * Okay...install both I guess Cloud images are fun! * bahhhhhhhhh * 4th time's a charm * Maybe 5th * Force it * 7.0.200 * trying explicitly named packages * Splitting lines --------- Co-authored-by: slorello89 --- appveyor.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 335309289..1d480410e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,7 +6,9 @@ init: install: - cmd: >- - choco install dotnet-sdk --version 7.0.102 + choco install dotnet-6.0-sdk + + choco install dotnet-7.0-sdk cd tests\RedisConfigs\3.0.503 From 9698aaa8c5e372a73e0dec0adeeb9c85c09dd7f9 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 15 Mar 2023 12:39:59 +0000 Subject: [PATCH 215/435] expose IAsyncEnumerable on ChannelMessageQueue (#2402) * expose IAsyncEnumerable on ChannelMessageQueue fix #2400 * PR number * move ChannelMessageQueue.GetAsyncEnumerator to shipped --- docs/ReleaseNotes.md | 4 +-- .../ChannelMessageQueue.cs | 26 +++++++++++--- .../PublicAPI/PublicAPI.Shipped.txt | 1 + .../StackExchange.Redis.Tests/PubSubTests.cs | 34 +++++++++++++++++++ 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 84781cbad..239b25aa8 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,11 +8,11 @@ Current package versions: ## Unreleased -No pending changes. +- Fix [#2400](https://github.com/StackExchange/StackExchange.Redis/issues/2400): Expose `ChannelMessageQueue` as `IAsyncEnumerable` ([#2402 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2402)) ## 2.6.96 -- Fix [#2350](https://github.com/StackExchange/StackExchange.Redis/issues/2350): Properly parse lua script paramters in all cultures ([#2351 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2351)) +- Fix [#2350](https://github.com/StackExchange/StackExchange.Redis/issues/2350): Properly parse lua script parameters in all cultures ([#2351 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2351)) - Fix [#2362](https://github.com/StackExchange/StackExchange.Redis/issues/2362): Set `RedisConnectionException.FailureType` to `AuthenticationFailure` on all authentication scenarios for better handling ([#2367 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2367)) - Fix [#2368](https://github.com/StackExchange/StackExchange.Redis/issues/2368): Support `RedisValue.Length()` for all storage types ([#2370 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2370)) - Fix [#2376](https://github.com/StackExchange/StackExchange.Redis/issues/2376): Avoid a (rare) deadlock scenario ([#2378 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2378)) diff --git a/src/StackExchange.Redis/ChannelMessageQueue.cs b/src/StackExchange.Redis/ChannelMessageQueue.cs index 14af669ef..435b067fc 100644 --- a/src/StackExchange.Redis/ChannelMessageQueue.cs +++ b/src/StackExchange.Redis/ChannelMessageQueue.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -66,7 +68,7 @@ internal ChannelMessage(ChannelMessageQueue queue, in RedisChannel channel, in R /// To create a ChannelMessageQueue, use /// or . /// - public sealed class ChannelMessageQueue + public sealed class ChannelMessageQueue : IAsyncEnumerable { private readonly Channel _queue; /// @@ -319,10 +321,7 @@ internal void UnsubscribeImpl(Exception? error = null, CommandFlags flags = Comm { var parent = _parent; _parent = null; - if (parent != null) - { - parent.UnsubscribeAsync(Channel, null, this, flags); - } + parent?.UnsubscribeAsync(Channel, null, this, flags); _queue.Writer.TryComplete(error); } @@ -348,5 +347,22 @@ internal async Task UnsubscribeAsyncImpl(Exception? error = null, CommandFlags f /// /// The flags to use when unsubscribing. public Task UnsubscribeAsync(CommandFlags flags = CommandFlags.None) => UnsubscribeAsyncImpl(null, flags); + + /// +#if NETCOREAPP3_0_OR_GREATER + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + => _queue.Reader.ReadAllAsync().GetAsyncEnumerator(cancellationToken); +#else + public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + while (await _queue.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) + { + while (_queue.Reader.TryRead(out var item)) + { + yield return item; + } + } + } +#endif } } diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 72ae963c1..986eab9c6 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -79,6 +79,7 @@ StackExchange.Redis.ChannelMessage.SubscriptionChannel.get -> StackExchange.Redi StackExchange.Redis.ChannelMessageQueue StackExchange.Redis.ChannelMessageQueue.Channel.get -> StackExchange.Redis.RedisChannel StackExchange.Redis.ChannelMessageQueue.Completion.get -> System.Threading.Tasks.Task! +StackExchange.Redis.ChannelMessageQueue.GetAsyncEnumerator(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerator! StackExchange.Redis.ChannelMessageQueue.OnMessage(System.Action! handler) -> void StackExchange.Redis.ChannelMessageQueue.OnMessage(System.Func! handler) -> void StackExchange.Redis.ChannelMessageQueue.ReadAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask diff --git a/tests/StackExchange.Redis.Tests/PubSubTests.cs b/tests/StackExchange.Redis.Tests/PubSubTests.cs index 91df2aa99..b5287d018 100644 --- a/tests/StackExchange.Redis.Tests/PubSubTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubTests.cs @@ -308,6 +308,40 @@ private void TestMassivePublish(ISubscriber sub, string channel, string caption) Assert.True(withFAF.ElapsedMilliseconds < withAsync.ElapsedMilliseconds + 3000, caption); } + [Fact] + public async Task SubscribeAsyncEnumerable() + { + using var conn = Create(syncTimeout: 20000, shared: false, log: Writer); + + var sub = conn.GetSubscriber(); + RedisChannel channel = Me(); + + const int TO_SEND = 5; + var gotall = new TaskCompletionSource(); + + var source = await sub.SubscribeAsync(channel); + var op = Task.Run(async () => { + int count = 0; + await foreach (var item in source) + { + count++; + if (count == TO_SEND) gotall.TrySetResult(count); + } + return count; + }); + + for (int i = 0; i < TO_SEND; i++) + { + await sub.PublishAsync(channel, i); + } + await gotall.Task.WithTimeout(5000); + + // check the enumerator exits cleanly + sub.Unsubscribe(channel); + var count = await op.WithTimeout(1000); + Assert.Equal(5, count); + } + [Fact] public async Task PubSubGetAllAnyOrder() { From 3f8fd08bae36439864efc7576696977ea68db31f Mon Sep 17 00:00:00 2001 From: Vasil Kotsev <9307969+SonnyRR@users.noreply.github.com> Date: Tue, 28 Mar 2023 23:03:30 +0300 Subject: [PATCH 216/435] Reference the correct default retry policy (#2410) Update the 'Configuration' documentation page with the correct default 'ReconnectRetryPolicy' value. Reference: - [src/StackExchange.Redis/ConfigurationOptions.cs](https://github.com/SonnyRR/StackExchange.Redis/blob/improvement/configuration-docs-retry-policy/src/StackExchange.Redis/ConfigurationOptions.cs#L466) - [src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs](https://github.com/SonnyRR/StackExchange.Redis/blob/improvement/configuration-docs-retry-policy/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs#L152) --- docs/Configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 8982b1298..d085d967c 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -238,7 +238,7 @@ These settings are also used by the `IServer.MakeMaster()` method, which can set ReconnectRetryPolicy --- StackExchange.Redis automatically tries to reconnect in the background when the connection is lost for any reason. It keeps retrying until the connection has been restored. It would use ReconnectRetryPolicy to decide how long it should wait between the retries. -ReconnectRetryPolicy can be linear (default), exponential or a custom retry policy. +ReconnectRetryPolicy can be exponential (default), linear or a custom retry policy. Examples: From 1364ef83fedb2e09035cc77ae2ac9b1b9c0ebc90 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 28 Mar 2023 21:28:35 +0100 Subject: [PATCH 217/435] Add support for CLIENT SETINFO (#2414) Includes: - ConfigurationOptions (to opt-opt) - handshake (to send) - release notes - configuration documentation note we can't validate this yet as not on any released servers most contentious point: what lib-name to use - I've gone with `SE.Redis`, but: happy to use `StackExchange.Redis` if people prefer; I'm *not* aiming to make the name configurable cross-reference: https://github.com/redis/redis/pull/11758 Co-authored-by: Nick Craver --- docs/Configuration.md | 3 ++- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ClientInfo.cs | 20 +++++++++++++++++ .../Configuration/DefaultOptionsProvider.cs | 5 +++++ .../ConfigurationOptions.cs | 22 ++++++++++++++++--- .../PublicAPI/PublicAPI.Shipped.txt | 6 +++++ src/StackExchange.Redis/RedisLiterals.cs | 4 ++++ src/StackExchange.Redis/ServerEndPoint.cs | 20 +++++++++++++++++ .../StackExchange.Redis.Tests/ConfigTests.cs | 14 ++++++++++++ 9 files changed, 91 insertions(+), 4 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index d085d967c..90df67b9f 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -96,7 +96,8 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a | asyncTimeout={int} | `AsyncTimeout` | `SyncTimeout` | Time (ms) to allow for asynchronous operations | | tiebreaker={string} | `TieBreaker` | `__Booksleeve_TieBreak` | Key to use for selecting a server in an ambiguous primary scenario | | version={string} | `DefaultVersion` | (`4.0` in Azure, else `2.0`) | Redis version level (useful when the server does not make this available) | -| tunnel={string} | `Tunnel` | `null` | Tunnel for connections (use `http:{proxy url}` for "connect"-based proxy server) +| tunnel={string} | `Tunnel` | `null` | Tunnel for connections (use `http:{proxy url}` for "connect"-based proxy server) | +| setlib={bool} | `SetClientLibrary` | `true` | Whether to attempt to use `CLIENT SETINFO` to set the lib name/version on the connection | Additional code-only options: - ReconnectRetryPolicy (`IReconnectRetryPolicy`) - Default: `ReconnectRetryPolicy = ExponentialRetry(ConnectTimeout / 2);` diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 239b25aa8..d1cd9e7cb 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Fix [#2400](https://github.com/StackExchange/StackExchange.Redis/issues/2400): Expose `ChannelMessageQueue` as `IAsyncEnumerable` ([#2402 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2402)) +- Add: support for `CLIENT SETINFO` (lib name/version) during handshake; opt-out is via `ConfigurationOptions`; also support read of `resp`, `lib-ver` and `lib-name` via `CLIENT LIST` ([#2414 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2414)) ## 2.6.96 diff --git a/src/StackExchange.Redis/ClientInfo.cs b/src/StackExchange.Redis/ClientInfo.cs index 272bd97da..4fa0aa378 100644 --- a/src/StackExchange.Redis/ClientInfo.cs +++ b/src/StackExchange.Redis/ClientInfo.cs @@ -180,6 +180,23 @@ public ClientType ClientType } } + /// + /// Client RESP protocol version. Added in Redis 7.0 + /// + public string? ProtocolVersion { get; private set; } + + /// + /// Client library name. Added in Redis 7.2 + /// + /// + public string? LibraryName { get; private set; } + + /// + /// Client library version. Added in Redis 7.2 + /// + /// + public string? LibraryVersion { get; private set; } + internal static bool TryParse(string? input, [NotNullWhen(true)] out ClientInfo[]? clientList) { if (input == null) @@ -241,6 +258,9 @@ internal static bool TryParse(string? input, [NotNullWhen(true)] out ClientInfo[ client.Flags = flags; break; case "id": client.Id = Format.ParseInt64(value); break; + case "resp": client.ProtocolVersion = value; break; + case "lib-name": client.LibraryName = value; break; + case "lib-ver": client.LibraryVersion = value; break; } } clients.Add(client); diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index 72b1d3997..ad9a31031 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -197,6 +197,11 @@ protected virtual string GetDefaultClientName() => /// protected static string ComputerName => Environment.MachineName ?? Environment.GetEnvironmentVariable("ComputerName") ?? "Unknown"; + /// + /// Whether to identify the client by library name/version when possible + /// + public virtual bool SetClientLibrary => true; + /// /// Tries to get the RoleInstance Id if Microsoft.WindowsAzure.ServiceRuntime is loaded. /// In case of any failure, swallows the exception and returns null. diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index e773944d1..2d52640d8 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -97,7 +97,8 @@ internal const string Version = "version", WriteBuffer = "writeBuffer", CheckCertificateRevocation = "checkCertificateRevocation", - Tunnel = "tunnel"; + Tunnel = "tunnel", + SetClientLibrary = "setlib"; private static readonly Dictionary normalizedOptions = new[] { @@ -142,7 +143,7 @@ public static string TryNormalize(string value) private DefaultOptionsProvider? defaultOptions; private bool? allowAdmin, abortOnConnectFail, resolveDns, ssl, checkCertificateRevocation, - includeDetailInExceptions, includePerformanceCountersInExceptions; + includeDetailInExceptions, includePerformanceCountersInExceptions, setClientLibrary; private string? tieBreaker, sslHost, configChannel; @@ -231,6 +232,15 @@ public bool UseSsl set => Ssl = value; } + /// + /// Gets or sets whether the library should identify itself by library-name/version when possible + /// + public bool SetClientLibrary + { + get => setClientLibrary ?? Defaults.SetClientLibrary; + set => setClientLibrary = value; + } + /// /// Automatically encodes and decodes channels. /// @@ -652,6 +662,7 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow SslClientAuthenticationOptions = SslClientAuthenticationOptions, #endif Tunnel = Tunnel, + setClientLibrary = setClientLibrary, }; /// @@ -731,6 +742,7 @@ public string ToString(bool includePassword) Append(sb, OptionKeys.ConfigCheckSeconds, configCheckSeconds); Append(sb, OptionKeys.ResponseTimeout, responseTimeout); Append(sb, OptionKeys.DefaultDatabase, DefaultDatabase); + Append(sb, OptionKeys.SetClientLibrary, setClientLibrary); if (Tunnel is { IsInbuilt: true } tunnel) { Append(sb, OptionKeys.Tunnel, tunnel.ToString()); @@ -768,7 +780,7 @@ private void Clear() { ClientName = ServiceName = User = Password = tieBreaker = sslHost = configChannel = null; keepAlive = syncTimeout = asyncTimeout = connectTimeout = connectRetry = configCheckSeconds = DefaultDatabase = null; - allowAdmin = abortOnConnectFail = resolveDns = ssl = null; + allowAdmin = abortOnConnectFail = resolveDns = ssl = setClientLibrary = null; SslProtocols = null; defaultVersion = null; EndPoints.Clear(); @@ -778,6 +790,7 @@ private void Clear() CertificateValidation = null; ChannelPrefix = default; SocketManager = null; + Tunnel = null; } object ICloneable.Clone() => Clone(); @@ -883,6 +896,9 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) case OptionKeys.SslProtocols: SslProtocols = OptionKeys.ParseSslProtocols(key, value); break; + case OptionKeys.SetClientLibrary: + SetClientLibrary = OptionKeys.ParseBoolean(key, value); + break; case OptionKeys.Tunnel: if (value.IsNullOrWhiteSpace()) { diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 986eab9c6..e0a17c546 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -118,9 +118,12 @@ StackExchange.Redis.ClientInfo.Host.get -> string? StackExchange.Redis.ClientInfo.Id.get -> long StackExchange.Redis.ClientInfo.IdleSeconds.get -> int StackExchange.Redis.ClientInfo.LastCommand.get -> string? +StackExchange.Redis.ClientInfo.LibraryName.get -> string? +StackExchange.Redis.ClientInfo.LibraryVersion.get -> string? StackExchange.Redis.ClientInfo.Name.get -> string? StackExchange.Redis.ClientInfo.PatternSubscriptionCount.get -> int StackExchange.Redis.ClientInfo.Port.get -> int +StackExchange.Redis.ClientInfo.ProtocolVersion.get -> string? StackExchange.Redis.ClientInfo.Raw.get -> string? StackExchange.Redis.ClientInfo.SubscriptionCount.get -> int StackExchange.Redis.ClientInfo.TransactionCommandLength.get -> int @@ -253,6 +256,8 @@ StackExchange.Redis.ConfigurationOptions.ResponseTimeout.get -> int StackExchange.Redis.ConfigurationOptions.ResponseTimeout.set -> void StackExchange.Redis.ConfigurationOptions.ServiceName.get -> string? StackExchange.Redis.ConfigurationOptions.ServiceName.set -> void +StackExchange.Redis.ConfigurationOptions.SetClientLibrary.get -> bool +StackExchange.Redis.ConfigurationOptions.SetClientLibrary.set -> void StackExchange.Redis.ConfigurationOptions.SetDefaultPorts() -> void StackExchange.Redis.ConfigurationOptions.SocketManager.get -> StackExchange.Redis.SocketManager? StackExchange.Redis.ConfigurationOptions.SocketManager.set -> void @@ -1785,5 +1790,6 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.KeepAliveInterv virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Proxy.get -> StackExchange.Redis.Proxy virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ReconnectRetryPolicy.get -> StackExchange.Redis.IReconnectRetryPolicy? virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ResolveDns.get -> bool +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.SetClientLibrary.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.SyncTimeout.get -> System.TimeSpan virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.TieBreaker.get -> string! diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 73363968c..e6d1e76c2 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -83,6 +83,8 @@ public static readonly RedisValue LATEST = "LATEST", LEFT = "LEFT", LEN = "LEN", + lib_name = "lib-name", + lib_ver = "lib-ver", LIMIT = "LIMIT", LIST = "LIST", LOAD = "LOAD", @@ -118,8 +120,10 @@ public static readonly RedisValue REWRITE = "REWRITE", RIGHT = "RIGHT", SAVE = "SAVE", + SE_Redis = "SE.Redis", SEGFAULT = "SEGFAULT", SET = "SET", + SETINFO = "SETINFO", SETNAME = "SETNAME", SKIPME = "SKIPME", STATS = "STATS", diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 1e192d85e..b7d367cce 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -929,6 +929,26 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } } + if (Multiplexer.RawConfig.SetClientLibrary) + { + // note that this is a relatively new feature, but usually we won't know the + // server version, so we will use this speculatively and hope for the best + log?.WriteLine($"{Format.ToString(this)}: Setting client lib/ver"); + + msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, + RedisLiterals.SETINFO, RedisLiterals.lib_name, RedisLiterals.SE_Redis); + msg.SetInternalCall(); + await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); + + var version = Utils.GetLibVersion(); + if (!string.IsNullOrWhiteSpace(version)) + { + msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, + RedisLiterals.SETINFO, RedisLiterals.lib_ver, version); + msg.SetInternalCall(); + await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); + } + } } var bridge = connection.BridgeCouldBeNull; diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 668abe607..a90bd96df 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -648,4 +648,18 @@ public void CustomTunnelCanRoundtripMinusTunnel() options = ConfigurationOptions.Parse(cs); Assert.Null(options.Tunnel); } + + [Theory] + [InlineData("server:6379", true)] + [InlineData("server:6379,setlib=True", true)] + [InlineData("server:6379,setlib=False", false)] + public void DefaultConfigOptionsForSetLib(string configurationString, bool setlib) + { + var options = ConfigurationOptions.Parse(configurationString); + Assert.Equal(setlib, options.SetClientLibrary); + Assert.Equal(configurationString, options.ToString()); + options = options.Clone(); + Assert.Equal(setlib, options.SetClientLibrary); + Assert.Equal(configurationString, options.ToString()); + } } From ef388bd9e9371bc9302ee7d2697906d626ee3571 Mon Sep 17 00:00:00 2001 From: Kornel Pal Date: Wed, 29 Mar 2023 14:16:17 +0200 Subject: [PATCH 218/435] Fix #2392: Dequeue all timed out messages from the backlog when not connected, even when no completion is needed, to be able to dequeue and complete other timed out messages. (#2397) When the client is not connected timed out fire and forget messages currently are not removed from the backlog that also results in subsequent timed out messages not being marked as timed out, as described in #2392. --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/PhysicalBridge.cs | 19 +++++--- .../Issues/Issue2392Tests.cs | 46 +++++++++++++++++++ 3 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/Issues/Issue2392Tests.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index d1cd9e7cb..dd5d92f83 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,7 @@ Current package versions: ## Unreleased +- Fix [#2392](https://github.com/StackExchange/StackExchange.Redis/issues/2392): Dequeue *all* timed out messages from the backlog when not connected (including Fire+Forget) ([#2397 by kornelpal](https://github.com/StackExchange/StackExchange.Redis/pull/2397)) - Fix [#2400](https://github.com/StackExchange/StackExchange.Redis/issues/2400): Expose `ChannelMessageQueue` as `IAsyncEnumerable` ([#2402 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2402)) - Add: support for `CLIENT SETINFO` (lib name/version) during handshake; opt-out is via `ConfigurationOptions`; also support read of `resp`, `lib-ver` and `lib-name` via `CLIENT LIST` ([#2414 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2414)) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index efcd6518f..7fa8af27f 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -869,14 +869,14 @@ private void CheckBacklogForTimeouts() while (_backlog.TryPeek(out Message? message)) { // See if the message has pass our async timeout threshold - // or has otherwise been completed (e.g. a sync wait timed out) which would have cleared the ResultBox - if (!message.HasTimedOut(now, timeout, out var _) || message.ResultBox == null) break; // not a timeout - we can stop looking + // Note: All timed out messages must be dequeued, even when no completion is needed, to be able to dequeue and complete other timed out messages. + if (!message.HasTimedOut(now, timeout, out var _)) break; // not a timeout - we can stop looking lock (_backlog) { // Peek again since we didn't have lock before... // and rerun the exact same checks as above, note that it may be a different message now if (!_backlog.TryPeek(out message)) break; - if (!message.HasTimedOut(now, timeout, out var _) && message.ResultBox != null) break; + if (!message.HasTimedOut(now, timeout, out var _)) break; if (!BacklogTryDequeue(out var message2) || (message != message2)) // consume it for real { @@ -884,10 +884,15 @@ private void CheckBacklogForTimeouts() } } - // Tell the message it has failed - // Note: Attempting to *avoid* reentrancy/deadlock issues by not holding the lock while completing messages. - var ex = Multiplexer.GetException(WriteResult.TimeoutBeforeWrite, message, ServerEndPoint); - message.SetExceptionAndComplete(ex, this); + // We only handle async timeouts here, synchronous timeouts are handled upstream. + // Those sync timeouts happen in ConnectionMultiplexer.ExecuteSyncImpl() via Monitor.Wait. + if (message.ResultBoxIsAsync) + { + // Tell the message it has failed + // Note: Attempting to *avoid* reentrancy/deadlock issues by not holding the lock while completing messages. + var ex = Multiplexer.GetException(WriteResult.TimeoutBeforeWrite, message, ServerEndPoint); + message.SetExceptionAndComplete(ex, this); + } } } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2392Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2392Tests.cs new file mode 100644 index 000000000..fe3e9673d --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2392Tests.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests.Issues +{ + public class Issue2392Tests : TestBase + { + public Issue2392Tests(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task Execute() + { + var options = new ConfigurationOptions() + { + BacklogPolicy = new() + { + QueueWhileDisconnected = true, + AbortPendingOnConnectionFailure = false, + }, + AbortOnConnectFail = false, + ConnectTimeout = 1, + ConnectRetry = 0, + AsyncTimeout = 1, + SyncTimeout = 1, + AllowAdmin = true, + }; + options.EndPoints.Add("127.0.0.1:1234"); + + using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + var key = Me(); + var db = conn.GetDatabase(); + var server = conn.GetServerSnapshot()[0]; + + // Fail the connection + conn.AllowConnect = false; + server.SimulateConnectionFailure(SimulatedFailureType.All); + Assert.False(conn.IsConnected); + + await db.StringGetAsync(key, flags: CommandFlags.FireAndForget); + var ex = await Assert.ThrowsAnyAsync(() => db.StringGetAsync(key).WithTimeout(5000)); + Assert.True(ex is RedisTimeoutException or RedisConnectionException); + } + } +} From f690d168b5b881b91a0bba9d683f25a1d01aba9b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 30 Mar 2023 16:07:36 +0100 Subject: [PATCH 219/435] Clarify meaning of RedisValue.IsInteger (#2420) * add passing (modified) test for #2418 * - clarify the meaning of RedisValue.IsInteger, and reduce the visibility - fix a typo * fix PR number --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/RedisServer.cs | 2 +- src/StackExchange.Redis/RedisValue.cs | 28 ++++++----- .../Issues/Issue2418.cs | 48 +++++++++++++++++++ 4 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/Issues/Issue2418.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index dd5d92f83..5fb4c566a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -11,6 +11,7 @@ Current package versions: - Fix [#2392](https://github.com/StackExchange/StackExchange.Redis/issues/2392): Dequeue *all* timed out messages from the backlog when not connected (including Fire+Forget) ([#2397 by kornelpal](https://github.com/StackExchange/StackExchange.Redis/pull/2397)) - Fix [#2400](https://github.com/StackExchange/StackExchange.Redis/issues/2400): Expose `ChannelMessageQueue` as `IAsyncEnumerable` ([#2402 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2402)) - Add: support for `CLIENT SETINFO` (lib name/version) during handshake; opt-out is via `ConfigurationOptions`; also support read of `resp`, `lib-ver` and `lib-name` via `CLIENT LIST` ([#2414 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2414)) +- Documentation: clarify the meaning of `RedisValue.IsInteger` re [#2418](https://github.com/StackExchange/StackExchange.Redis/issues/2418) ([#2420 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2420)) ## 2.6.96 diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 142ee1aa0..13b114da0 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -263,7 +263,7 @@ private Message GetCommandListMessage(RedisValue? moduleName = null, RedisValue? } else - throw new ArgumentException("More then one filter is not allwed"); + throw new ArgumentException("More then one filter is not allowed"); } private RedisValue[] AddValueToArray(RedisValue val, RedisValue[] arr) diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index d2f8a1ca9..a0b045cf4 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Buffers.Text; +using System.ComponentModel; using System.IO; using System.Linq; using System.Reflection; @@ -107,8 +108,11 @@ public static RedisValue Unbox(object? value) public static RedisValue Null { get; } = new RedisValue(0, default, null); /// - /// Indicates whether the value is a primitive integer (signed or unsigned). + /// Indicates whether the **underlying** value is a primitive integer (signed or unsigned); this is **not** + /// the same as whether the value can be *treated* as an integer - see + /// and , which is usually the more appropriate test. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced)] // hide it, because this *probably* isn't what callers need public bool IsInteger => _objectOrSentinel == Sentinel_SignedInteger || _objectOrSentinel == Sentinel_UnsignedInteger; /// @@ -698,28 +702,28 @@ private static bool TryParseDouble(ReadOnlySpan blob, out double value) /// Converts the to a . /// /// The to convert. - public static explicit operator double? (RedisValue value) + public static explicit operator double?(RedisValue value) => value.IsNull ? (double?)null : (double)value; /// /// Converts the to a . /// /// The to convert. - public static explicit operator float? (RedisValue value) + public static explicit operator float?(RedisValue value) => value.IsNull ? (float?)null : (float)value; /// /// Converts the to a . /// /// The to convert. - public static explicit operator decimal? (RedisValue value) + public static explicit operator decimal?(RedisValue value) => value.IsNull ? (decimal?)null : (decimal)value; /// /// Converts the to a . /// /// The to convert. - public static explicit operator long? (RedisValue value) + public static explicit operator long?(RedisValue value) => value.IsNull ? (long?)null : (long)value; /// @@ -727,14 +731,14 @@ private static bool TryParseDouble(ReadOnlySpan blob, out double value) /// /// The to convert. [CLSCompliant(false)] - public static explicit operator ulong? (RedisValue value) + public static explicit operator ulong?(RedisValue value) => value.IsNull ? (ulong?)null : (ulong)value; /// /// Converts the to a . /// /// The to convert. - public static explicit operator int? (RedisValue value) + public static explicit operator int?(RedisValue value) => value.IsNull ? (int?)null : (int)value; /// @@ -742,21 +746,21 @@ private static bool TryParseDouble(ReadOnlySpan blob, out double value) /// /// The to convert. [CLSCompliant(false)] - public static explicit operator uint? (RedisValue value) + public static explicit operator uint?(RedisValue value) => value.IsNull ? (uint?)null : (uint)value; /// /// Converts the to a . /// /// The to convert. - public static explicit operator bool? (RedisValue value) + public static explicit operator bool?(RedisValue value) => value.IsNull ? (bool?)null : (bool)value; /// /// Converts a to a . /// /// The to convert. - public static implicit operator string? (RedisValue value) + public static implicit operator string?(RedisValue value) { switch (value.Type) { @@ -810,7 +814,7 @@ private static string ToHex(ReadOnlySpan src) /// Converts a to a . /// /// The to convert. - public static implicit operator byte[]? (RedisValue value) + public static implicit operator byte[]?(RedisValue value) { switch (value.Type) { @@ -1112,7 +1116,7 @@ private ReadOnlyMemory AsMemory(out byte[]? leased) return _memory; case StorageType.String: string s = (string)_objectOrSentinel!; - HaveString: +HaveString: if (s.Length == 0) { leased = null; diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2418.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2418.cs new file mode 100644 index 000000000..4fc034843 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2418.cs @@ -0,0 +1,48 @@ +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests.Issues; + +public class Issue2418 : TestBase +{ + public Issue2418(ITestOutputHelper output, SharedConnectionFixture? fixture = null) + : base(output, fixture) { } + + [Fact] + public async Task Execute() + { + using var conn = Create(); + var db = conn.GetDatabase(); + + RedisKey key = Me(); + RedisValue someInt = 12; + Assert.False(someInt.IsNullOrEmpty, nameof(someInt.IsNullOrEmpty) + " before"); + Assert.True(someInt.IsInteger, nameof(someInt.IsInteger) + " before"); + await db.HashSetAsync(key, new[] + { + new HashEntry("some_int", someInt), + // ... + }); + + // check we can fetch it + var entry = await db.HashGetAllAsync(key); + Assert.NotEmpty(entry); + Assert.Single(entry); + foreach (var pair in entry) + { + Log($"'{pair.Name}'='{pair.Value}'"); + } + + + // filter with LINQ + Assert.True(entry.Any(x => x.Name == "some_int"), "Any"); + someInt = entry.FirstOrDefault(x => x.Name == "some_int").Value; + Log($"found via Any: '{someInt}'"); + Assert.False(someInt.IsNullOrEmpty, nameof(someInt.IsNullOrEmpty) + " after"); + Assert.True(someInt.TryParse(out int i)); + Assert.Equal(12, i); + Assert.Equal(12, (int)someInt); + } +} From 129d59f9ed013130e4cac12a6091683ec23b6aa9 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 30 Mar 2023 14:03:01 -0400 Subject: [PATCH 220/435] Timeouts: Improve Backlog handling and errors for users + GC rooting fixes for outstanding scenarios (#2408) This combination PR is both fixing a GC issue (see below, and #2413 for details) and improves timeout exception. Basically if a timeout happens for a message that was in the backlog but was never sent, the user now gets a much more informative message like this: > Exception: The message timed out in the backlog attempting to send because no connection became available - Last Connection Exception: InternalFailure on 127.0.0.1:6379/Interactive, Initializing/NotStarted, last: GET, origin: ConnectedAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.99.22667, command=PING, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=18,QueuedItems=0,CompletedItems=65), v: 2.6.99.22667 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) Today, this isn't intuitive especially for connections with `AbortOnConnectFail` set to `false`. What happens is a multiplexer _never_ connects successfully, but the user just gets generic timeouts. This makes the error more specific and includes the inner exception (also as `.InnerException`) for more details, informing the user of a config/auth/whatever error underneath as to why things are never successfully sending. Also adds `aoc: (0|1)` to the exception message for easier advice in issues (reflecting what `AbortOnConnectFail` is set to). Co-authored-by: Nick Craver Co-authored-by: Marc Gravell --- Directory.Packages.props | 2 +- docs/ReleaseNotes.md | 4 +- src/Directory.Build.props | 1 + .../ConnectionMultiplexer.cs | 141 +++++++++++++++--- .../Enums/CommandStatus.cs | 6 +- src/StackExchange.Redis/ExceptionFactory.cs | 44 ++++-- src/StackExchange.Redis/Message.cs | 5 + src/StackExchange.Redis/PhysicalBridge.cs | 84 +++++++++-- src/StackExchange.Redis/PhysicalConnection.cs | 51 +++++++ .../PublicAPI/PublicAPI.Shipped.txt | 1 + src/StackExchange.Redis/ServerEndPoint.cs | 9 +- .../AbortOnConnectFailTests.cs | 100 +++++++++++++ .../ExceptionFactoryTests.cs | 1 + .../Helpers/SharedConnectionFixture.cs | 4 +- .../StackExchange.Redis.Tests/PubSubTests.cs | 6 +- 15 files changed, 414 insertions(+), 45 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 8cbfea70b..2a844af7c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,7 +19,7 @@ - + diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 5fb4c566a..1aca1c9e8 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,9 +8,11 @@ Current package versions: ## Unreleased +- Fix [#2412](https://github.com/StackExchange/StackExchange.Redis/issues/2412): Critical (but rare) GC bug that can lead to async tasks never completing if the multiplexer is not held by the consumer ([#2408 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2408)) +- Add: Better error messages (over generic timeout) when commands are backlogged and unable to write to any connection ([#2408 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2408)) - Fix [#2392](https://github.com/StackExchange/StackExchange.Redis/issues/2392): Dequeue *all* timed out messages from the backlog when not connected (including Fire+Forget) ([#2397 by kornelpal](https://github.com/StackExchange/StackExchange.Redis/pull/2397)) - Fix [#2400](https://github.com/StackExchange/StackExchange.Redis/issues/2400): Expose `ChannelMessageQueue` as `IAsyncEnumerable` ([#2402 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2402)) -- Add: support for `CLIENT SETINFO` (lib name/version) during handshake; opt-out is via `ConfigurationOptions`; also support read of `resp`, `lib-ver` and `lib-name` via `CLIENT LIST` ([#2414 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2414)) +- Add: Support for `CLIENT SETINFO` (lib name/version) during handshake; opt-out is via `ConfigurationOptions`; also support read of `resp`, `lib-ver` and `lib-name` via `CLIENT LIST` ([#2414 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2414)) - Documentation: clarify the meaning of `RedisValue.IsInteger` re [#2418](https://github.com/StackExchange/StackExchange.Redis/issues/2418) ([#2420 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2420)) ## 2.6.96 diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 09f6d5e9f..29eadff61 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,6 +3,7 @@ true true + false diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index b94e4650b..37d45be09 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -34,7 +34,7 @@ public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplex internal long syncOps, asyncOps; private long syncTimeouts, fireAndForgets, asyncTimeouts; private string? failureMessage, activeConfigCause; - private IDisposable? pulse; + private TimerToken? pulse; private readonly Hashtable servers = new Hashtable(); private volatile ServerSnapshot _serverSnapshot = ServerSnapshot.Empty; @@ -874,7 +874,7 @@ public ServerSnapshotFiltered(ServerEndPoint[] endpoints, int count, Func pulse?.Root(this); + + // note that this also acts (conditionally) as the GC root for the multiplexer + // when there are in-flight messages; the timer can then acts as the heartbeat + // to make sure that everything *eventually* completes + private sealed class TimerToken : IDisposable { private TimerToken(ConnectionMultiplexer muxer) { - _ref = new WeakReference(muxer); + _weakRef = new(muxer); } private Timer? _timer; public void SetTimer(Timer timer) => _timer = timer; - private readonly WeakReference _ref; + + private readonly WeakReference _weakRef; + + private object StrongRefSyncLock => _weakRef; // private and readonly? it'll do + private ConnectionMultiplexer? _strongRef; + private int _strongRefToken; private static readonly TimerCallback Heartbeat = state => { var token = (TimerToken)state!; - var muxer = (ConnectionMultiplexer?)(token._ref?.Target); - if (muxer != null) + if (token._weakRef.TryGetTarget(out var muxer)) { muxer.OnHeartbeat(); } else { // the muxer got disposed from out of us; kill the timer - var tmp = token._timer; - token._timer = null; - if (tmp != null) try { tmp.Dispose(); } catch { } + token.Dispose(); } }; - internal static IDisposable Create(ConnectionMultiplexer connection) + internal static TimerToken Create(ConnectionMultiplexer connection) { var token = new TimerToken(connection); var heartbeatMilliseconds = (int)connection.RawConfig.HeartbeatInterval.TotalMilliseconds; var timer = new Timer(Heartbeat, token, heartbeatMilliseconds, heartbeatMilliseconds); token.SetTimer(timer); - return timer; + return token; + } + + public void Dispose() + { + var tmp = _timer; + _timer = null; + if (tmp is not null) try { tmp.Dispose(); } catch { } + + _strongRef = null; // note that this shouldn't be relevant since we've unrooted the TimerToken + } + + + // explanation of rooting model: + // + // the timer has a reference to the TimerToken; this *always* has a weak-ref, + // and *may* sometimes have a strong-ref; this is so that if a consumer + // drops a multiplexer, it can be garbage collected, i.e. the heartbeat timer + // doesn't keep the entire thing alive forever; instead, if the heartbeat detects + // the weak-ref has been collected, it can cancel the timer and *itself* go away; + // however: this leaves a problem where there is *in flight work* when the consumer + // drops the multiplexer; in particular, if that happens when disconnected, there + // could be consumer-visible pending TCS items *in the backlog queue*; we don't want + // to leave those incomplete, as that fails the contractual expectations of async/await; + // instead we need to root ourselves. The natural place to do this is by rooting the + // multiplexer, allowing the heartbeat to keep poking things, so that the usual + // message-processing and timeout rules apply. This is why we *sometimes* also keep + // a strong-ref to the same multiplexer. + // + // The TimerToken is rooted by the timer callback; this then roots the multiplexer, + // which keeps our bridges and connections in scope - until we're sure we're done + // with them. + // + // 1) any bridge or connection will trigger rooting by calling Root when + // they change from "empty" to "non-empty" i.e. whenever there + // in-flight items; this always changes the token; this includes both the + // backlog and awaiting-reply queues. + // + // 2) the heartbeat is responsible for unrooting, after processing timeouts + // etc; first it checks whether it is needed (IsRooted), which also gives + // it the current token. + // + // 3) if so, the heartbeat will (outside of the lock) query all sources to + // see if they still have outstanding work; if everyone reports negatively, + // then the heartbeat calls UnRoot passing in the old token; if this still + // matches (i.e. no new work came in while we were looking away), then the + // strong reference is removed; note that "has outstanding work" ignores + // internal-call messages; we are only interested in consumer-facing items + // (but we need to check this *here* rather than when adding, as otherwise + // the definition of "is empty, should root" becomes more complicated, which + // impacts the write path, rather than the heartbeat path. + // + // This means that the multiplexer (via the timer) lasts as long as there are + // outstanding messages; if the consumer has dropped the multiplexer, then + // there will be no new incoming messages, and after timeouts: everything + // should drop. + + public void Root(ConnectionMultiplexer multiplexer) + { + lock (StrongRefSyncLock) + { + _strongRef = multiplexer; + _strongRefToken++; + } + } + + public bool IsRooted(out int token) + { + lock (StrongRefSyncLock) + { + token = _strongRefToken; + return _strongRef is not null; + } + } + + public void UnRoot(int token) + { + lock (StrongRefSyncLock) + { + if (token == _strongRefToken) + { + _strongRef = null; + } + } } } @@ -956,8 +1046,21 @@ private void OnHeartbeat() Trace("heartbeat"); var tmp = GetServerSnapshot(); + int token = 0; + bool isRooted = pulse?.IsRooted(out token) ?? false, hasPendingCallerFacingItems = false; + for (int i = 0; i < tmp.Length; i++) + { tmp[i].OnHeartbeat(); + if (isRooted && !hasPendingCallerFacingItems) + { + hasPendingCallerFacingItems = tmp[i].HasPendingCallerFacingItems(); + } + } + if (isRooted && !hasPendingCallerFacingItems) + { // release the GC root on the heartbeat *if* the token still matches + pulse?.UnRoot(token); + } } catch (Exception ex) { @@ -1909,11 +2012,11 @@ private WriteResult TryPushMessageToBridgeSync(Message message, ResultProcess /// public override string ToString() => string.IsNullOrWhiteSpace(ClientName) ? GetType().Name : ClientName; - internal Exception GetException(WriteResult result, Message message, ServerEndPoint? server) => result switch + internal Exception GetException(WriteResult result, Message message, ServerEndPoint? server, PhysicalBridge? bridge = null) => result switch { WriteResult.Success => throw new ArgumentOutOfRangeException(nameof(result), "Be sure to check result isn't successful before calling GetException."), WriteResult.NoConnectionAvailable => ExceptionFactory.NoConnectionAvailable(this, message, server), - WriteResult.TimeoutBeforeWrite => ExceptionFactory.Timeout(this, "The timeout was reached before the message could be written to the output buffer, and it was not sent", message, server, result), + WriteResult.TimeoutBeforeWrite => ExceptionFactory.Timeout(this, null, message, server, result, bridge), _ => ExceptionFactory.ConnectionFailure(RawConfig.IncludeDetailInExceptions, ConnectionFailureType.ProtocolFailure, "An unknown error occurred when writing the message", server), }; @@ -1935,7 +2038,7 @@ internal static void ThrowFailed(TaskCompletionSource? source, Exception u } } - [return: NotNullIfNotNull("defaultValue")] + [return: NotNullIfNotNull(nameof(defaultValue))] internal T? ExecuteSyncImpl(Message message, ResultProcessor? processor, ServerEndPoint? server, T? defaultValue = default) { if (_isDisposed) throw new ObjectDisposedException(ToString()); @@ -1960,10 +2063,11 @@ internal static void ThrowFailed(TaskCompletionSource? source, Exception u var source = SimpleResultBox.Get(); bool timeout = false; + WriteResult result; lock (source) { #pragma warning disable CS0618 // Type or member is obsolete - var result = TryPushMessageToBridgeSync(message, processor, source, ref server); + result = TryPushMessageToBridgeSync(message, processor, source, ref server); #pragma warning restore CS0618 if (result != WriteResult.Success) { @@ -1985,7 +2089,8 @@ internal static void ThrowFailed(TaskCompletionSource? source, Exception u { Interlocked.Increment(ref syncTimeouts); // Very important not to return "source" to the pool here - throw ExceptionFactory.Timeout(this, null, message, server); + // Also note we return "success" when queueing a messages to the backlog, so we need to manually fake it back here when timing out in the backlog + throw ExceptionFactory.Timeout(this, null, message, server, message.IsBacklogged ? WriteResult.TimeoutBeforeWrite : result, server?.GetBridge(message.Command, create: false)); } // Snapshot these so that we can recycle the box var val = source.GetResult(out var ex, canRecycle: true); // now that we aren't locking it... @@ -2047,7 +2152,7 @@ static async Task ExecuteAsyncImpl_Awaited(ConnectionMultiplexer @this, Value internal Task ExecuteAsyncImpl(Message? message, ResultProcessor? processor, object? state, ServerEndPoint? server) { - [return: NotNullIfNotNull("tcs")] + [return: NotNullIfNotNull(nameof(tcs))] static async Task ExecuteAsyncImpl_Awaited(ConnectionMultiplexer @this, ValueTask write, TaskCompletionSource? tcs, Message message, ServerEndPoint? server) { var result = await write.ForAwait(); diff --git a/src/StackExchange.Redis/Enums/CommandStatus.cs b/src/StackExchange.Redis/Enums/CommandStatus.cs index f4cdd1810..472c96dfc 100644 --- a/src/StackExchange.Redis/Enums/CommandStatus.cs +++ b/src/StackExchange.Redis/Enums/CommandStatus.cs @@ -10,12 +10,16 @@ public enum CommandStatus /// Unknown, /// - /// ConnectionMultiplexer has not yet started writing this command to redis. + /// ConnectionMultiplexer has not yet started writing this command to Redis. /// WaitingToBeSent, /// /// Command has been sent to Redis. /// Sent, + /// + /// Command is in the backlog, waiting to be processed and written to Redis. + /// + WaitingInBacklog, } } diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index c18a7994c..05a47b5c9 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -212,13 +212,32 @@ private static void Add(List> data, StringBuilder sb, stri } } - internal static Exception Timeout(ConnectionMultiplexer multiplexer, string? baseErrorMessage, Message message, ServerEndPoint? server, WriteResult? result = null) + internal static Exception Timeout(ConnectionMultiplexer multiplexer, string? baseErrorMessage, Message message, ServerEndPoint? server, WriteResult? result = null, PhysicalBridge? bridge = null) { List> data = new List> { Tuple.Create("Message", message.CommandAndKey) }; var sb = new StringBuilder(); + + // We timeout writing messages in quite different ways sync/async - so centralize messaging here. + if (string.IsNullOrEmpty(baseErrorMessage) && result == WriteResult.TimeoutBeforeWrite) + { + baseErrorMessage = message.IsBacklogged + ? "The message timed out in the backlog attempting to send because no connection became available" + : "The timeout was reached before the message could be written to the output buffer, and it was not sent"; + } + + var lastConnectionException = bridge?.LastException as RedisConnectionException; + var logConnectionException = message.IsBacklogged && lastConnectionException is not null; + if (!string.IsNullOrEmpty(baseErrorMessage)) { sb.Append(baseErrorMessage); + + // If we're in the situation where we've never connected + if (logConnectionException && lastConnectionException is not null) + { + sb.Append(" - Last Connection Exception: ").Append(lastConnectionException.Message); + } + if (message != null) { sb.Append(", command=").Append(message.Command); // no key here, note @@ -252,17 +271,23 @@ internal static Exception Timeout(ConnectionMultiplexer multiplexer, string? bas } catch { } } - AddCommonDetail(data, sb, message, multiplexer, server); - sb.Append(" (Please take a look at this article for some common client-side issues that can cause timeouts: "); - sb.Append(TimeoutHelpLink); - sb.Append(')'); + sb.Append(" (Please take a look at this article for some common client-side issues that can cause timeouts: ") + .Append(TimeoutHelpLink) + .Append(')'); - var ex = new RedisTimeoutException(sb.ToString(), message?.Status ?? CommandStatus.Unknown) - { - HelpLink = TimeoutHelpLink - }; + // If we're from a backlog timeout scenario, we log a more intuitive connection exception for the timeout...because the timeout was a symptom + // and we have a more direct cause: we had no connection to send it on. + Exception ex = logConnectionException && lastConnectionException is not null + ? new RedisConnectionException(lastConnectionException.FailureType, sb.ToString(), lastConnectionException, message?.Status ?? CommandStatus.Unknown) + { + HelpLink = TimeoutHelpLink + } + : new RedisTimeoutException(sb.ToString(), message?.Status ?? CommandStatus.Unknown) + { + HelpLink = TimeoutHelpLink + }; CopyDataToException(data, ex); if (multiplexer.RawConfig.IncludeDetailInExceptions) AddExceptionDetail(ex, message, server, null); @@ -333,6 +358,7 @@ private static void AddCommonDetail( } Add(data, sb, "Server-Endpoint", "serverEndpoint", (server.EndPoint.ToString() ?? "Unknown").Replace("Unspecified/", "")); Add(data, sb, "Server-Connected-Seconds", "conn-sec", bs.ConnectedAt is DateTime dt ? (DateTime.UtcNow - dt).TotalSeconds.ToString("0.##") : "n/a"); + Add(data, sb, "Abort-On-Connect", "aoc", multiplexer.RawConfig.AbortOnConnectFail ? "1" : "0"); } Add(data, sb, "Multiplexer-Connects", "mc", $"{multiplexer._connectAttemptCount}/{multiplexer._connectCompletedCount}/{multiplexer._connectionCloseCount}"); Add(data, sb, "Manager", "mgr", multiplexer.SocketManager?.GetState()); diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index d489b1c7c..a76001756 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -609,6 +609,11 @@ internal bool TryGetPhysicalState( } } + internal bool IsBacklogged => Status == CommandStatus.WaitingInBacklog; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SetBacklogged() => Status = CommandStatus.WaitingInBacklog; + private PhysicalConnection? _enqueuedTo; private long _queuedStampReceived, _queuedStampSent; diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 7fa8af27f..e7af56a69 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -114,17 +114,28 @@ public enum State : byte public void Dispose() { isDisposed = true; - _backlogAutoReset?.Dispose(); + // If there's anything in the backlog and we're being torn down - exfil it immediately (e.g. so all awaitables complete) + AbandonPendingBacklog(new ObjectDisposedException("Connection is being disposed")); + try + { + _backlogAutoReset?.Set(); + _backlogAutoReset?.Dispose(); + } + catch { } using (var tmp = physical) { physical = null; } GC.SuppressFinalize(this); } + ~PhysicalBridge() { isDisposed = true; // make damn sure we don't true to resurrect + // If there's anything in the backlog and we're being torn down - exfil it immediately (e.g. so all awaitables complete) + AbandonPendingBacklog(new ObjectDisposedException("Connection is being finalized")); + // shouldn't *really* touch managed objects // in a finalizer, but we need to kill that socket, // and this is the first place that isn't going to @@ -162,7 +173,7 @@ private WriteResult QueueOrFailMessage(Message message) // Anything else goes in the bin - we're just not ready for you yet message.Cancel(); - Multiplexer?.OnMessageFaulted(message, null); + Multiplexer.OnMessageFaulted(message, null); message.Complete(); return WriteResult.NoConnectionAvailable; } @@ -170,7 +181,7 @@ private WriteResult QueueOrFailMessage(Message message) private WriteResult FailDueToNoConnection(Message message) { message.Cancel(); - Multiplexer?.OnMessageFaulted(message, null); + Multiplexer.OnMessageFaulted(message, null); message.Complete(); return WriteResult.NoConnectionAvailable; } @@ -474,7 +485,7 @@ private void AbandonPendingBacklog(Exception ex) { while (BacklogTryDequeue(out Message? next)) { - Multiplexer?.OnMessageFaulted(next, ex); + Multiplexer.OnMessageFaulted(next, ex); next.SetExceptionAndComplete(ex, this); } } @@ -663,7 +674,7 @@ private WriteResult WriteMessageInsideLock(PhysicalConnection physical, Message var existingMessage = Interlocked.CompareExchange(ref _activeMessage, message, null); if (existingMessage != null) { - Multiplexer?.OnInfoMessage($"Reentrant call to WriteMessageTakingWriteLock for {message.CommandAndKey}, {existingMessage.CommandAndKey} is still active"); + Multiplexer.OnInfoMessage($"Reentrant call to WriteMessageTakingWriteLock for {message.CommandAndKey}, {existingMessage.CommandAndKey} is still active"); return WriteResult.NoConnectionAvailable; } @@ -808,8 +819,22 @@ private bool TryPushToBacklog(Message message, bool onlyIfExists, bool bypassBac [MethodImpl(MethodImplOptions.AggressiveInlining)] private void BacklogEnqueue(Message message) { - _backlog.Enqueue(message); + bool wasEmpty = _backlog.IsEmpty; + // important that this *precedes* enqueue, to play well with HasPendingCallerFacingItems + Interlocked.Increment(ref _backlogCurrentEnqueued); Interlocked.Increment(ref _backlogTotalEnqueued); + _backlog.Enqueue(message); + message.SetBacklogged(); + + if (wasEmpty) + { + // it is important to do this *after* adding, so that we can't + // get into a thread-race where the heartbeat checks too fast; + // the fact that we're accessing Multiplexer down here means that + // we're rooting it ourselves via the stack, so we don't need + // to worry about it being collected until at least after this + Multiplexer.Root(); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -890,7 +915,7 @@ private void CheckBacklogForTimeouts() { // Tell the message it has failed // Note: Attempting to *avoid* reentrancy/deadlock issues by not holding the lock while completing messages. - var ex = Multiplexer.GetException(WriteResult.TimeoutBeforeWrite, message, ServerEndPoint); + var ex = Multiplexer.GetException(WriteResult.TimeoutBeforeWrite, message, ServerEndPoint, this); message.SetExceptionAndComplete(ex, this); } } @@ -914,6 +939,7 @@ internal enum BacklogStatus : byte RecordingFault, SettingIdle, Faulted, + NotifyingDisposed, } private volatile BacklogStatus _backlogStatus; @@ -926,7 +952,7 @@ private async Task ProcessBacklogAsync() _backlogStatus = BacklogStatus.Starting; try { - while (true) + while (!isDisposed) { if (!_backlog.IsEmpty) { @@ -946,8 +972,34 @@ private async Task ProcessBacklogAsync() break; } } + // If we're being disposed but have items in the backlog, we need to complete them or async messages can linger forever. + if (isDisposed && BacklogHasItems) + { + _backlogStatus = BacklogStatus.NotifyingDisposed; + // Because peeking at the backlog, checking message and then dequeuing, is not thread-safe, we do have to use + // a lock here, for mutual exclusion of backlog DEQUEUERS. Unfortunately. + // But we reduce contention by only locking if we see something that looks timed out. + while (BacklogHasItems) + { + Message? message = null; + lock (_backlog) + { + if (!BacklogTryDequeue(out message)) + { + break; + } + } + + var ex = ExceptionFactory.Timeout(Multiplexer, "The message was in the backlog when connection was disposed", message, ServerEndPoint, WriteResult.TimeoutBeforeWrite, this); + message.SetExceptionAndComplete(ex, this); + } + } + } + catch (ObjectDisposedException) when (!BacklogHasItems) + { + // We're being torn down and we have no backlog to process - all good. } - catch + catch (Exception) { _backlogStatus = BacklogStatus.Faulted; } @@ -1080,10 +1132,22 @@ private async Task ProcessBridgeBacklogAsync() } } + public bool HasPendingCallerFacingItems() + { + if (BacklogHasItems) + { + foreach (var item in _backlog) // non-consuming, thread-safe, etc + { + if (!item.IsInternalCall) return true; + } + } + return physical?.HasPendingCallerFacingItems() ?? false; + } + private WriteResult TimedOutBeforeWrite(Message message) { message.Cancel(); - Multiplexer?.OnMessageFaulted(message, null); + Multiplexer.OnMessageFaulted(message, null); message.Complete(); return WriteResult.TimeoutBeforeWrite; } diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 2c5fc3c0d..3c43002a3 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -561,10 +561,31 @@ internal static void IdentifyFailureType(Exception? exception, ref ConnectionFai internal void EnqueueInsideWriteLock(Message next) { + var multiplexer = BridgeCouldBeNull?.Multiplexer; + if (multiplexer is null) + { + // multiplexer already collected? then we're almost certainly doomed; + // we can still process it to avoid making things worse/more complex, + // but: we can't reliably assume this works, so: shout now! + next.Cancel(); + next.Complete(); + } + + bool wasEmpty; lock (_writtenAwaitingResponse) { + wasEmpty = _writtenAwaitingResponse.Count == 0; _writtenAwaitingResponse.Enqueue(next); } + if (wasEmpty) + { + // it is important to do this *after* adding, so that we can't + // get into a thread-race where the heartbeat checks too fast; + // the fact that we're accessing Multiplexer down here means that + // we're rooting it ourselves via the stack, so we don't need + // to worry about it being collected until at least after this + multiplexer?.Root(); + } } internal void GetCounters(ConnectionCounters counters) @@ -1975,5 +1996,35 @@ private static RawResult ParseInlineProtocol(Arena arena, in RawResul } return new RawResult(block, false); } + + internal bool HasPendingCallerFacingItems() + { + bool lockTaken = false; + try + { + Monitor.TryEnter(_writtenAwaitingResponse, 0, ref lockTaken); + if (lockTaken) + { + if (_writtenAwaitingResponse.Count != 0) + { + foreach (var item in _writtenAwaitingResponse) + { + if (!item.IsInternalCall) return true; + } + } + return false; + } + else + { + // don't contend the lock; *presume* that something + // qualifies; we can check again next heartbeat + return true; + } + } + finally + { + if (lockTaken) Monitor.Exit(_writtenAwaitingResponse); + } + } } } diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index e0a17c546..ffaf84dd6 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -172,6 +172,7 @@ StackExchange.Redis.CommandStatus StackExchange.Redis.CommandStatus.Sent = 2 -> StackExchange.Redis.CommandStatus StackExchange.Redis.CommandStatus.Unknown = 0 -> StackExchange.Redis.CommandStatus StackExchange.Redis.CommandStatus.WaitingToBeSent = 1 -> StackExchange.Redis.CommandStatus +StackExchange.Redis.CommandStatus.WaitingInBacklog = 3 -> StackExchange.Redis.CommandStatus StackExchange.Redis.CommandTrace StackExchange.Redis.CommandTrace.Arguments.get -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.CommandTrace.Duration.get -> System.TimeSpan diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index b7d367cce..45b62896c 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -769,7 +769,7 @@ internal void OnHeartbeat() var source = TaskResultBox.Create(out var tcs, null); message.SetSource(processor, source); - if (bridge == null) bridge = GetBridge(message); + bridge ??= GetBridge(message); WriteResult result; if (bridge == null) @@ -1010,5 +1010,12 @@ internal void SimulateConnectionFailure(SimulatedFailureType failureType) interactive?.SimulateConnectionFailure(failureType); subscription?.SimulateConnectionFailure(failureType); } + + internal bool HasPendingCallerFacingItems() + { + // check whichever bridges exist + if (interactive?.HasPendingCallerFacingItems() == true) return true; + return subscription?.HasPendingCallerFacingItems() ?? false; + } } } diff --git a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs new file mode 100644 index 000000000..49138c535 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs @@ -0,0 +1,100 @@ +using System; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests; + +public class AbortOnConnectFailTests : TestBase +{ + public AbortOnConnectFailTests(ITestOutputHelper output) : base (output) { } + + [Fact] + public void NeverEverConnectedNoBacklogThrowsConnectionNotAvailableSync() + { + using var conn = GetFailFastConn(); + var db = conn.GetDatabase(); + var key = Me(); + + // No connection is active/available to service this operation: GET 6.0.14AbortOnConnectFailTests-NeverEverConnectedThrowsConnectionNotAvailable; UnableToConnect on doesnot.exist.0d034c26350e4ee199d6c5f385a073f7:6379/Interactive, Initializing/NotStarted, last: NONE, origin: BeginConnectAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, global: 0s ago, v: 2.6.99.22667, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=17,QueuedItems=0,CompletedItems=20), v: 2.6.99.22667 + var ex = Assert.Throws(() => db.StringGet(key)); + Log("Exception: " + ex.Message); + Assert.Contains("No connection is active/available to service this operation", ex.Message); + } + + [Fact] + public async Task NeverEverConnectedNoBacklogThrowsConnectionNotAvailableAsync() + { + using var conn = GetFailFastConn(); + var db = conn.GetDatabase(); + var key = Me(); + + // No connection is active/available to service this operation: GET 6.0.14AbortOnConnectFailTests-NeverEverConnectedThrowsConnectionNotAvailable; UnableToConnect on doesnot.exist.0d034c26350e4ee199d6c5f385a073f7:6379/Interactive, Initializing/NotStarted, last: NONE, origin: BeginConnectAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, global: 0s ago, v: 2.6.99.22667, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=17,QueuedItems=0,CompletedItems=20), v: 2.6.99.22667 + var ex = await Assert.ThrowsAsync(() => db.StringGetAsync(key)); + Log("Exception: " + ex.Message); + Assert.Contains("No connection is active/available to service this operation", ex.Message); + } + + [Fact] + public void DisconnectAndReconnectThrowsConnectionExceptionSync() + { + using var conn = GetWorkingBacklogConn(); + + var db = conn.GetDatabase(); + var key = Me(); + _ = db.Ping(); // Doesn't throw - we're connected + + // Disconnect and don't allow re-connection + conn.AllowConnect = false; + var server = conn.GetServerSnapshot()[0]; + server.SimulateConnectionFailure(SimulatedFailureType.All); + + // Exception: The message timed out in the backlog attempting to send because no connection became available - Last Connection Exception: InternalFailure on 127.0.0.1:6379/Interactive, Initializing/NotStarted, last: GET, origin: ConnectedAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.99.22667, command=PING, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=18,QueuedItems=0,CompletedItems=65), v: 2.6.99.22667 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) + var ex = Assert.ThrowsAny(() => db.Ping()); + Log("Exception: " + ex.Message); + Assert.True(ex is RedisConnectionException or RedisTimeoutException); + Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available - Last Connection Exception: ", ex.Message); + Assert.NotNull(ex.InnerException); + var iex = Assert.IsType(ex.InnerException); + Assert.Contains(iex.Message, ex.Message); + } + + [Fact] + public async Task DisconnectAndNoReconnectThrowsConnectionExceptionAsync() + { + using var conn = GetWorkingBacklogConn(); + + var db = conn.GetDatabase(); + var key = Me(); + _ = db.Ping(); // Doesn't throw - we're connected + + // Disconnect and don't allow re-connection + conn.AllowConnect = false; + var server = conn.GetServerSnapshot()[0]; + server.SimulateConnectionFailure(SimulatedFailureType.All); + + // Exception: The message timed out in the backlog attempting to send because no connection became available - Last Connection Exception: InternalFailure on 127.0.0.1:6379/Interactive, Initializing/NotStarted, last: GET, origin: ConnectedAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.99.22667, command=PING, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=18,QueuedItems=0,CompletedItems=65), v: 2.6.99.22667 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) + var ex = await Assert.ThrowsAsync(() => db.PingAsync()); + Log("Exception: " + ex.Message); + Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available - Last Connection Exception: ", ex.Message); + Assert.NotNull(ex.InnerException); + var iex = Assert.IsType(ex.InnerException); + Assert.Contains(iex.Message, ex.Message); + } + + private ConnectionMultiplexer GetFailFastConn() => + ConnectionMultiplexer.Connect(GetOptions(BacklogPolicy.FailFast).Apply(o => o.EndPoints.Add($"doesnot.exist.{Guid.NewGuid():N}:6379")), Writer); + + private ConnectionMultiplexer GetWorkingBacklogConn() => + ConnectionMultiplexer.Connect(GetOptions(BacklogPolicy.Default).Apply(o => o.EndPoints.Add(GetConfiguration())), Writer); + + private ConfigurationOptions GetOptions(BacklogPolicy policy) => new ConfigurationOptions() + { + AbortOnConnectFail = false, + BacklogPolicy = policy, + ConnectTimeout = 50, + SyncTimeout = 100, + KeepAlive = 100, + AllowAdmin = true, + }; +} diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index abc19d0f4..42f444418 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -124,6 +124,7 @@ public void TimeoutException() Assert.Contains("sync-ops: ", ex.Message); Assert.Contains("async-ops: ", ex.Message); Assert.Contains("conn-sec: n/a", ex.Message); + Assert.Contains("aoc: 1", ex.Message); #if NETCOREAPP Assert.Contains("POOL: ", ex.Message); #endif diff --git a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs index aec2e0a83..af1879542 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs @@ -232,10 +232,10 @@ public void Teardown(TextWriter output) foreach (var ep in _actualConnection.GetServerSnapshot()) { var interactive = ep.GetBridge(ConnectionType.Interactive); - TestBase.Log(output, $" {Format.ToString(interactive)}: " + interactive?.GetStatus()); + TestBase.Log(output, $" {Format.ToString(interactive)}: {interactive?.GetStatus()}"); var subscription = ep.GetBridge(ConnectionType.Subscription); - TestBase.Log(output, $" {Format.ToString(subscription)}: " + subscription?.GetStatus()); + TestBase.Log(output, $" {Format.ToString(subscription)}: {subscription?.GetStatus()}"); } } } diff --git a/tests/StackExchange.Redis.Tests/PubSubTests.cs b/tests/StackExchange.Redis.Tests/PubSubTests.cs index b5287d018..3cc21fa0a 100644 --- a/tests/StackExchange.Redis.Tests/PubSubTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubTests.cs @@ -825,8 +825,10 @@ await sub.SubscribeAsync(channel, delegate Log("Failing connection"); // Fail all connections server.SimulateConnectionFailure(SimulatedFailureType.All); - // Trigger failure (RedisTimeoutException because of backlog behavior) - Assert.Throws(() => sub.Ping()); + // Trigger failure (RedisTimeoutException or RedisConnectionException because + // of backlog behavior) + var ex = Assert.ThrowsAny(() => sub.Ping()); + Assert.True(ex is RedisTimeoutException or RedisConnectionException); Assert.False(sub.IsConnected(channel)); // Now reconnect... From 88725539175424f8d5054211378c3a702bff9ed0 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 30 Mar 2023 14:45:49 -0400 Subject: [PATCH 221/435] 2.6.104 Release notes --- docs/ReleaseNotes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 1aca1c9e8..e7e56b7f5 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,10 @@ Current package versions: ## Unreleased +No pending unreleased changes. + +## 2.6.104 + - Fix [#2412](https://github.com/StackExchange/StackExchange.Redis/issues/2412): Critical (but rare) GC bug that can lead to async tasks never completing if the multiplexer is not held by the consumer ([#2408 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2408)) - Add: Better error messages (over generic timeout) when commands are backlogged and unable to write to any connection ([#2408 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2408)) - Fix [#2392](https://github.com/StackExchange/StackExchange.Redis/issues/2392): Dequeue *all* timed out messages from the backlog when not connected (including Fire+Forget) ([#2397 by kornelpal](https://github.com/StackExchange/StackExchange.Redis/pull/2397)) From 571d832dae6cf2c1f23ee4a93ca8ed0f40e54b25 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 3 Apr 2023 15:39:51 +0100 Subject: [PATCH 222/435] Fix #2426 - don't restrict multi-slot operations on envoy proxy (#2428) --- docs/ReleaseNotes.md | 2 +- src/StackExchange.Redis/ServerSelectionStrategy.cs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index e7e56b7f5..abe80ad12 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -No pending unreleased changes. +- Fix [#2426](https://github.com/StackExchange/StackExchange.Redis/issues/2426): Don't restrict multi-slot operations on Envoy proxy; let the proxy decide ([#2428 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2428)) ## 2.6.104 diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index a4dbb92a7..4a32da0d4 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -140,10 +140,15 @@ private static unsafe int GetClusterSlot(ReadOnlySpan blob) // strictly speaking some proxies use a different hashing algorithm, but the hash-tag behavior is // the same, so this does a pretty good job of spotting illegal commands before sending them case ServerType.Twemproxy: - case ServerType.Envoyproxy: slot = message.GetHashSlot(this); if (slot == MultipleSlots) throw ExceptionFactory.MultiSlot(multiplexer.RawConfig.IncludeDetailInExceptions, message); break; + /* just shown for completeness + case ServerType.Standalone: // don't use sharding + case ServerType.Envoyproxy: // defer to the proxy; see #2426 + default: // unknown scenario; defer to the server + break; + */ } return Select(slot, message.Command, message.Flags, allowDisconnected); } From 7ad0add610f913479016bd012ea742d5d74f77b7 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 27 Apr 2023 11:06:17 -0400 Subject: [PATCH 223/435] DefaultOptionsProvider: allow providing User/Password (#2445) For wrappers which intend to provide this (e.g. managed service accounts and such), the intent was for them to override these and return their "current" values (e.g. as a token rotates), but I missed them in the initial pass...even though this was the original extensibility reason, because I suck! Fixing. --- docs/ReleaseNotes.md | 1 + .../Configuration/DefaultOptionsProvider.cs | 10 ++ .../ConfigurationOptions.cs | 30 +++-- .../PublicAPI/PublicAPI.Shipped.txt | 2 + .../CommandTimeoutTests.cs | 2 + .../DefaultOptionsTests.cs | 4 + .../FailoverTests.cs | 113 ++++++++++++++++-- .../StackExchange.Redis.Tests/PubSubTests.cs | 98 --------------- 8 files changed, 141 insertions(+), 119 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index abe80ad12..466f2061b 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Fix [#2426](https://github.com/StackExchange/StackExchange.Redis/issues/2426): Don't restrict multi-slot operations on Envoy proxy; let the proxy decide ([#2428 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2428)) +- Add: Support for `User`/`Password` in `DefaultOptionsProvider` to support token rotation scenarios ([#2445 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2445)) ## 2.6.104 diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index ad9a31031..f2fd42757 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -172,6 +172,16 @@ public static void AddProvider(DefaultOptionsProvider provider) /// public virtual TimeSpan ConfigCheckInterval => TimeSpan.FromMinutes(1); + /// + /// The username to use to authenticate with the server. + /// + public virtual string? User => null; + + /// + /// The password to use to authenticate with the server. + /// + public virtual string? Password => null; + // We memoize this to reduce cost on re-access private string? defaultClientName; /// diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 2d52640d8..48a20ed66 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -145,7 +145,7 @@ public static string TryNormalize(string value) private bool? allowAdmin, abortOnConnectFail, resolveDns, ssl, checkCertificateRevocation, includeDetailInExceptions, includePerformanceCountersInExceptions, setClientLibrary; - private string? tieBreaker, sslHost, configChannel; + private string? tieBreaker, sslHost, configChannel, user, password; private TimeSpan? heartbeatInterval; @@ -440,14 +440,22 @@ public int KeepAlive } /// - /// The user to use to authenticate with the server. + /// The username to use to authenticate with the server. /// - public string? User { get; set; } + public string? User + { + get => user ?? Defaults.User; + set => user = value; + } /// /// The password to use to authenticate with the server. /// - public string? Password { get; set; } + public string? Password + { + get => password ?? Defaults.Password; + set => password = value; + } /// /// Specifies whether asynchronous operations should be invoked in a way that guarantees their original delivery order. @@ -634,8 +642,8 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow allowAdmin = allowAdmin, defaultVersion = defaultVersion, connectTimeout = connectTimeout, - User = User, - Password = Password, + user = user, + password = password, tieBreaker = tieBreaker, ssl = ssl, sslHost = sslHost, @@ -726,8 +734,8 @@ public string ToString(bool includePassword) Append(sb, OptionKeys.AllowAdmin, allowAdmin); Append(sb, OptionKeys.Version, defaultVersion); Append(sb, OptionKeys.ConnectTimeout, connectTimeout); - Append(sb, OptionKeys.User, User); - Append(sb, OptionKeys.Password, (includePassword || string.IsNullOrEmpty(Password)) ? Password : "*****"); + Append(sb, OptionKeys.User, user); + Append(sb, OptionKeys.Password, (includePassword || string.IsNullOrEmpty(password)) ? password : "*****"); Append(sb, OptionKeys.TieBreaker, tieBreaker); Append(sb, OptionKeys.Ssl, ssl); Append(sb, OptionKeys.SslProtocols, SslProtocols?.ToString().Replace(',', '|')); @@ -778,7 +786,7 @@ private static void Append(StringBuilder sb, string prefix, object? value) private void Clear() { - ClientName = ServiceName = User = Password = tieBreaker = sslHost = configChannel = null; + ClientName = ServiceName = user = password = tieBreaker = sslHost = configChannel = null; keepAlive = syncTimeout = asyncTimeout = connectTimeout = connectRetry = configCheckSeconds = DefaultDatabase = null; allowAdmin = abortOnConnectFail = resolveDns = ssl = setClientLibrary = null; SslProtocols = null; @@ -873,10 +881,10 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) DefaultVersion = OptionKeys.ParseVersion(key, value); break; case OptionKeys.User: - User = value; + user = value; break; case OptionKeys.Password: - Password = value; + password = value; break; case OptionKeys.TieBreaker: TieBreaker = value; diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index ffaf84dd6..0f5a2880d 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1788,9 +1788,11 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludeDetailIn virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludePerformanceCountersInExceptions.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IsMatch(System.Net.EndPoint! endpoint) -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.KeepAliveInterval.get -> System.TimeSpan +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Password.get -> string? virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Proxy.get -> StackExchange.Redis.Proxy virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ReconnectRetryPolicy.get -> StackExchange.Redis.IReconnectRetryPolicy? virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ResolveDns.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.SetClientLibrary.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.SyncTimeout.get -> System.TimeSpan virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.TieBreaker.get -> string! +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.User.get -> string? diff --git a/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs b/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs index 54847e2be..671345f9f 100644 --- a/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs +++ b/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs @@ -35,6 +35,7 @@ public async Task DefaultHeartbeatTimeout() await pauseTask; } +#if DEBUG [Fact] public async Task DefaultHeartbeatLowTimeout() { @@ -60,4 +61,5 @@ public async Task DefaultHeartbeatLowTimeout() // Await as to not bias the next test await pauseTask; } +#endif } diff --git a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs index 4dd4872e7..8223a0d21 100644 --- a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs +++ b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs @@ -36,6 +36,8 @@ public class TestOptionsProvider : DefaultOptionsProvider public override bool ResolveDns => true; public override TimeSpan SyncTimeout => TimeSpan.FromSeconds(126); public override string TieBreaker => "TestTiebreaker"; + public override string? User => "TestUser"; + public override string? Password => "TestPassword"; } public class TestRetryPolicy : IReconnectRetryPolicy @@ -99,6 +101,8 @@ private static void AssertAllOverrides(ConfigurationOptions options) Assert.True(options.ResolveDns); Assert.Equal(TimeSpan.FromSeconds(126), TimeSpan.FromMilliseconds(options.SyncTimeout)); Assert.Equal("TestTiebreaker", options.TieBreaker); + Assert.Equal("TestUser", options.User); + Assert.Equal("TestPassword", options.Password); } public class TestAfterConnectOptionsProvider : DefaultOptionsProvider diff --git a/tests/StackExchange.Redis.Tests/FailoverTests.cs b/tests/StackExchange.Redis.Tests/FailoverTests.cs index f3955c5e7..53c7e74f4 100644 --- a/tests/StackExchange.Redis.Tests/FailoverTests.cs +++ b/tests/StackExchange.Redis.Tests/FailoverTests.cs @@ -7,6 +7,7 @@ namespace StackExchange.Redis.Tests; +[Collection(NonParallelCollection.Name)] public class FailoverTests : TestBase, IAsyncLifetime { protected override string GetConfiguration() => GetPrimaryReplicaConfig().ToString(); @@ -196,6 +197,104 @@ public async Task DereplicateGoesToPrimary() } #if DEBUG + [Fact] + public async Task SubscriptionsSurviveConnectionFailureAsync() + { + using var conn = (Create(allowAdmin: true, shared: false, log: Writer, syncTimeout: 1000) as ConnectionMultiplexer)!; + + var profiler = conn.AddProfiler(); + RedisChannel channel = Me(); + var sub = conn.GetSubscriber(); + int counter = 0; + Assert.True(sub.IsConnected()); + await sub.SubscribeAsync(channel, delegate + { + Interlocked.Increment(ref counter); + }).ConfigureAwait(false); + + var profile1 = Log(profiler); + + Assert.Equal(1, conn.GetSubscriptionsCount()); + + await Task.Delay(200).ConfigureAwait(false); + + await sub.PublishAsync(channel, "abc").ConfigureAwait(false); + sub.Ping(); + await Task.Delay(200).ConfigureAwait(false); + + var counter1 = Thread.VolatileRead(ref counter); + Log($"Expecting 1 message, got {counter1}"); + Assert.Equal(1, counter1); + + var server = GetServer(conn); + var socketCount = server.GetCounters().Subscription.SocketCount; + Log($"Expecting 1 socket, got {socketCount}"); + Assert.Equal(1, socketCount); + + // We might fail both connections or just the primary in the time period + SetExpectedAmbientFailureCount(-1); + + // Make sure we fail all the way + conn.AllowConnect = false; + Log("Failing connection"); + // Fail all connections + server.SimulateConnectionFailure(SimulatedFailureType.All); + // Trigger failure (RedisTimeoutException or RedisConnectionException because + // of backlog behavior) + var ex = Assert.ThrowsAny(() => sub.Ping()); + Assert.True(ex is RedisTimeoutException or RedisConnectionException); + Assert.False(sub.IsConnected(channel)); + + // Now reconnect... + conn.AllowConnect = true; + Log("Waiting on reconnect"); + // Wait until we're reconnected + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => sub.IsConnected(channel)); + Log("Reconnected"); + // Ensure we're reconnected + Assert.True(sub.IsConnected(channel)); + + // Ensure we've sent the subscribe command after reconnecting + var profile2 = Log(profiler); + //Assert.Equal(1, profile2.Count(p => p.Command == nameof(RedisCommand.SUBSCRIBE))); + + Log("Issuing ping after reconnected"); + sub.Ping(); + + var muxerSubCount = conn.GetSubscriptionsCount(); + Log($"Muxer thinks we have {muxerSubCount} subscriber(s)."); + Assert.Equal(1, muxerSubCount); + + var muxerSubs = conn.GetSubscriptions(); + foreach (var pair in muxerSubs) + { + var muxerSub = pair.Value; + Log($" Muxer Sub: {pair.Key}: (EndPoint: {muxerSub.GetCurrentServer()}, Connected: {muxerSub.IsConnected})"); + } + + Log("Publishing"); + var published = await sub.PublishAsync(channel, "abc").ConfigureAwait(false); + + Log($"Published to {published} subscriber(s)."); + Assert.Equal(1, published); + + // Give it a few seconds to get our messages + Log("Waiting for 2 messages"); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => Thread.VolatileRead(ref counter) == 2); + + var counter2 = Thread.VolatileRead(ref counter); + Log($"Expecting 2 messages, got {counter2}"); + Assert.Equal(2, counter2); + + // Log all commands at the end + Log("All commands since connecting:"); + var profile3 = profiler.FinishProfiling(); + foreach (var command in profile3) + { + Log($"{command.EndPoint}: {command}"); + } + } + [Fact] public async Task SubscriptionsSurvivePrimarySwitchAsync() { @@ -215,14 +314,8 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() var subB = bConn.GetSubscriber(); long primaryChanged = 0, aCount = 0, bCount = 0; - aConn.ConfigurationChangedBroadcast += delegate - { - Log("A noticed config broadcast: " + Interlocked.Increment(ref primaryChanged)); - }; - bConn.ConfigurationChangedBroadcast += delegate - { - Log("B noticed config broadcast: " + Interlocked.Increment(ref primaryChanged)); - }; + aConn.ConfigurationChangedBroadcast += (s, args) => Log("A noticed config broadcast: " + Interlocked.Increment(ref primaryChanged) + " (Endpoint:" + args.EndPoint + ")"); + bConn.ConfigurationChangedBroadcast += (s, args) => Log("B noticed config broadcast: " + Interlocked.Increment(ref primaryChanged) + " (Endpoint:" + args.EndPoint + ")"); subA.Subscribe(channel, (_, message) => { Log("A got message: " + message); @@ -333,8 +426,8 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() Assert.Equal(2, Interlocked.Read(ref aCount)); Assert.Equal(2, Interlocked.Read(ref bCount)); - // Expect 12, because a sees a, but b sees a and b due to replication - Assert.Equal(12, Interlocked.CompareExchange(ref primaryChanged, 0, 0)); + // Expect 12, because a sees a, but b sees a and b due to replication, but contenders may add their own + Assert.True(Interlocked.CompareExchange(ref primaryChanged, 0, 0) >= 12); } catch { diff --git a/tests/StackExchange.Redis.Tests/PubSubTests.cs b/tests/StackExchange.Redis.Tests/PubSubTests.cs index 3cc21fa0a..8fd23ffdd 100644 --- a/tests/StackExchange.Redis.Tests/PubSubTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubTests.cs @@ -782,102 +782,4 @@ public async Task AzureRedisEventsAutomaticSubscribe() Assert.True(didUpdate); } } - - [Fact] - public async Task SubscriptionsSurviveConnectionFailureAsync() - { - using var conn = (Create(allowAdmin: true, shared: false, log: Writer, syncTimeout: 1000) as ConnectionMultiplexer)!; - - var profiler = conn.AddProfiler(); - RedisChannel channel = Me(); - var sub = conn.GetSubscriber(); - int counter = 0; - Assert.True(sub.IsConnected()); - await sub.SubscribeAsync(channel, delegate - { - Interlocked.Increment(ref counter); - }).ConfigureAwait(false); - - var profile1 = Log(profiler); - - Assert.Equal(1, conn.GetSubscriptionsCount()); - - await Task.Delay(200).ConfigureAwait(false); - - await sub.PublishAsync(channel, "abc").ConfigureAwait(false); - sub.Ping(); - await Task.Delay(200).ConfigureAwait(false); - - var counter1 = Thread.VolatileRead(ref counter); - Log($"Expecting 1 message, got {counter1}"); - Assert.Equal(1, counter1); - - var server = GetServer(conn); - var socketCount = server.GetCounters().Subscription.SocketCount; - Log($"Expecting 1 socket, got {socketCount}"); - Assert.Equal(1, socketCount); - - // We might fail both connections or just the primary in the time period - SetExpectedAmbientFailureCount(-1); - - // Make sure we fail all the way - conn.AllowConnect = false; - Log("Failing connection"); - // Fail all connections - server.SimulateConnectionFailure(SimulatedFailureType.All); - // Trigger failure (RedisTimeoutException or RedisConnectionException because - // of backlog behavior) - var ex = Assert.ThrowsAny(() => sub.Ping()); - Assert.True(ex is RedisTimeoutException or RedisConnectionException); - Assert.False(sub.IsConnected(channel)); - - // Now reconnect... - conn.AllowConnect = true; - Log("Waiting on reconnect"); - // Wait until we're reconnected - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => sub.IsConnected(channel)); - Log("Reconnected"); - // Ensure we're reconnected - Assert.True(sub.IsConnected(channel)); - - // Ensure we've sent the subscribe command after reconnecting - var profile2 = Log(profiler); - //Assert.Equal(1, profile2.Count(p => p.Command == nameof(RedisCommand.SUBSCRIBE))); - - Log("Issuing ping after reconnected"); - sub.Ping(); - - var muxerSubCount = conn.GetSubscriptionsCount(); - Log($"Muxer thinks we have {muxerSubCount} subscriber(s)."); - Assert.Equal(1, muxerSubCount); - - var muxerSubs = conn.GetSubscriptions(); - foreach (var pair in muxerSubs) - { - var muxerSub = pair.Value; - Log($" Muxer Sub: {pair.Key}: (EndPoint: {muxerSub.GetCurrentServer()}, Connected: {muxerSub.IsConnected})"); - } - - Log("Publishing"); - var published = await sub.PublishAsync(channel, "abc").ConfigureAwait(false); - - Log($"Published to {published} subscriber(s)."); - Assert.Equal(1, published); - - // Give it a few seconds to get our messages - Log("Waiting for 2 messages"); - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => Thread.VolatileRead(ref counter) == 2); - - var counter2 = Thread.VolatileRead(ref counter); - Log($"Expecting 2 messages, got {counter2}"); - Assert.Equal(2, counter2); - - // Log all commands at the end - Log("All commands since connecting:"); - var profile3 = profiler.FinishProfiling(); - foreach (var command in profile3) - { - Log($"{command.EndPoint}: {command}"); - } - } } From 3ba8d2ac943b13d46497ea441dbdea1ce90e2b46 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 1 May 2023 17:16:12 -0500 Subject: [PATCH 224/435] Fix first round of trim warnings (#2451) - DefaultOptionsProvider.TryGetAzureRoleInstanceIdNoThrow use Type.GetType to check for Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment, so the trimmer knows to preserve this type, if it is in the app - ChannelMessage use the added public API for ChannelReader CanCount and Count, but fallback to reflection on netcoreapp3.1, since those APIs are not available on that runtime. Contributes to #2449 This at least removes the warnings from using `Microsoft.Extensions.Caching.StackExchangeRedis`. We could also add a trimming test app as outlined in https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/prepare-libraries-for-trimming#show-all-warnings-with-sample-application, along with a unit test that publishes the app and ensures there are no new warnings. LMK if you think this is valuable. There are still these warnings left in this library: ``` /_/src/StackExchange.Redis/ScriptParameterMapper.cs(173): Trim analysis warning IL2070: StackExchange.Redis.ScriptParameterMapper.IsValidParameterHash(Type,LuaScript,String&,String&): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors', 'DynamicallyAccessedMemberTypes.PublicMethods', 'DynamicallyAccessedMemberTypes.PublicFields', 'DynamicallyAccessedMemberTypes.PublicNestedTypes', 'DynamicallyAccessedMemberTypes.PublicProperties', 'DynamicallyAccessedMemberTypes.PublicEvents' in call to 'System.Type.GetMember(String)'. The parameter 't' of method 'StackExchange.Redis.ScriptParameterMapper.IsValidParameterHash(Type,LuaScript,String&,String&)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. [C:\git\azure-activedirectory-identitymodel-extensions-for-dotnet\test\Microsoft.IdentityModel.AotCompatibility.TestApp\Microsoft.IdentityModel.AotCompatibility.TestApp.csproj] /_/src/StackExchange.Redis/ScriptParameterMapper.cs(227): Trim analysis warning IL2070: StackExchange.Redis.ScriptParameterMapper.GetParameterExtractor(Type,LuaScript): 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors', 'DynamicallyAccessedMemberTypes.PublicMethods', 'DynamicallyAccessedMemberTypes.PublicFields', 'DynamicallyAccessedMemberTypes.PublicNestedTypes', 'DynamicallyAccessedMemberTypes.PublicProperties', 'DynamicallyAccessedMemberTypes.PublicEvents' in call to 'System.Type.GetMember(String)'. The parameter 't' of method 'StackExchange.Redis.ScriptParameterMapper.GetParameterExtractor(Type,LuaScript)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. [C:\git\azure-activedirectory-identitymodel-extensions-for-dotnet\test\Microsoft.IdentityModel.AotCompatibility.TestApp\Microsoft.IdentityModel.AotCompatibility.TestApp.csproj] /_/src/StackExchange.Redis/ScriptParameterMapper.cs(260): Trim analysis warning IL2026: StackExchange.Redis.ScriptParameterMapper.GetParameterExtractor(Type,LuaScript): Using member 'System.Linq.Expressions.Expression.Property(Expression,String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Creating Expressions requires unreferenced code because the members being referenced by the Expression may be trimmed. [C:\git\azure-activedirectory-identitymodel-extensions-for-dotnet\test\Microsoft.IdentityModel.AotCompatibility.TestApp\Microsoft.IdentityModel.AotCompatibility.TestApp.csproj] /_/src/StackExchange.Redis/ScriptParameterMapper.cs(261): Trim analysis warning IL2026: StackExchange.Redis.ScriptParameterMapper.GetParameterExtractor(Type,LuaScript): Using member 'System.Linq.Expressions.Expression.Call(Expression,String,Type[],Expression[])' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Creating Expressions requires unreferenced code because the members being referenced by the Expression may be trimmed. [C:\git\azure-activedirectory-identitymodel-extensions-for-dotnet\test\Microsoft.IdentityModel.AotCompatibility.TestApp\Microsoft.IdentityModel.AotCompatibility.TestApp.csproj] ``` Fixing those will require a bit more work, as the whole LuaScript functionality looks like it needs to be marked `RequiresUnreferencedCode`. cc @mgravell @NickCraver --- docs/ReleaseNotes.md | 1 + .../ChannelMessageQueue.cs | 11 +++++++++- .../Configuration/DefaultOptionsProvider.cs | 22 +++++-------------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 466f2061b..f87f6fd2a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: - Fix [#2426](https://github.com/StackExchange/StackExchange.Redis/issues/2426): Don't restrict multi-slot operations on Envoy proxy; let the proxy decide ([#2428 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2428)) - Add: Support for `User`/`Password` in `DefaultOptionsProvider` to support token rotation scenarios ([#2445 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2445)) +- Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Resolve AOT trim warnings in `TryGetAzureRoleInstanceIdNoThrow` ([#2451 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2451)) ## 2.6.104 diff --git a/src/StackExchange.Redis/ChannelMessageQueue.cs b/src/StackExchange.Redis/ChannelMessageQueue.cs index 435b067fc..3cc6c3d5a 100644 --- a/src/StackExchange.Redis/ChannelMessageQueue.cs +++ b/src/StackExchange.Redis/ChannelMessageQueue.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Reflection; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -126,6 +125,7 @@ public ValueTask ReadAsync(CancellationToken cancellationToken = /// The (approximate) count of items in the Channel. public bool TryGetCount(out int count) { +#if NETCOREAPP3_1 // get this using the reflection try { @@ -137,6 +137,15 @@ public bool TryGetCount(out int count) } } catch { } +#else + var reader = _queue.Reader; + if (reader.CanCount) + { + count = reader.Count; + return true; + } +#endif + count = default; return false; } diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index f2fd42757..f990d8265 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -225,31 +225,21 @@ protected virtual string GetDefaultClientName() => string? roleInstanceId; try { - Assembly? asm = null; - foreach (var asmb in AppDomain.CurrentDomain.GetAssemblies()) - { - if (asmb.GetName()?.Name?.Equals("Microsoft.WindowsAzure.ServiceRuntime") == true) - { - asm = asmb; - break; - } - } - if (asm == null) - return null; - - var type = asm.GetType("Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment"); + var roleEnvironmentType = Type.GetType("Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment, Microsoft.WindowsAzure.ServiceRuntime", throwOnError: false); // https://msdn.microsoft.com/en-us/library/microsoft.windowsazure.serviceruntime.roleenvironment.isavailable.aspx - if (type?.GetProperty("IsAvailable") is not PropertyInfo isAvailableProp + if (roleEnvironmentType?.GetProperty("IsAvailable") is not PropertyInfo isAvailableProp || isAvailableProp.GetValue(null, null) is not bool isAvailableVal || !isAvailableVal) { return null; } - var currentRoleInstanceProp = type.GetProperty("CurrentRoleInstance"); + var currentRoleInstanceProp = roleEnvironmentType.GetProperty("CurrentRoleInstance"); var currentRoleInstanceId = currentRoleInstanceProp?.GetValue(null, null); - roleInstanceId = currentRoleInstanceId?.GetType().GetProperty("Id")?.GetValue(currentRoleInstanceId, null)?.ToString(); + + var roleInstanceType = Type.GetType("Microsoft.WindowsAzure.ServiceRuntime.RoleInstance, Microsoft.WindowsAzure.ServiceRuntime", throwOnError: false); + roleInstanceId = roleInstanceType?.GetProperty("Id")?.GetValue(currentRoleInstanceId, null)?.ToString(); if (roleInstanceId.IsNullOrEmpty()) { From 9f3f76d0f4ab7de855a343f9dbd7834f8ce53898 Mon Sep 17 00:00:00 2001 From: Florian Bernd Date: Tue, 2 May 2023 00:35:53 +0200 Subject: [PATCH 225/435] Fix HTTP tunnel (#2448) According to https://datatracker.ietf.org/doc/html/draft-luotonen-web-proxy-tunneling-01#section-3.2, the expected response to a `CONNECT` should be `HTTP/1.1 200 Connection established` instead of `HTTP/1.1 200 OK`. Allow both responses for now (as still a lot of proxy implementations fail to follow the standard). A HTTP(S), SOCKS, etc. proxy is traditionally specified by an URI of form `scheme://[user:pass@]host[:port]`. Accept both formats for backwards compatibility. --- docs/ReleaseNotes.md | 1 + .../Configuration/Tunnel.cs | 16 ++++++++------- .../ConfigurationOptions.cs | 20 ++++++++++++------- .../StackExchange.Redis.Tests/ConfigTests.cs | 12 ++++++----- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index f87f6fd2a..9b39d48f3 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -11,6 +11,7 @@ Current package versions: - Fix [#2426](https://github.com/StackExchange/StackExchange.Redis/issues/2426): Don't restrict multi-slot operations on Envoy proxy; let the proxy decide ([#2428 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2428)) - Add: Support for `User`/`Password` in `DefaultOptionsProvider` to support token rotation scenarios ([#2445 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2445)) - Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Resolve AOT trim warnings in `TryGetAzureRoleInstanceIdNoThrow` ([#2451 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2451)) +- Adds: Support for `HTTP/1.1 200 Connection established` in HTTP Tunnel ([#2448 by flobernd](https://github.com/StackExchange/StackExchange.Redis/pull/2448)) ## 2.6.104 diff --git a/src/StackExchange.Redis/Configuration/Tunnel.cs b/src/StackExchange.Redis/Configuration/Tunnel.cs index 2deaf664a..15c9abd15 100644 --- a/src/StackExchange.Redis/Configuration/Tunnel.cs +++ b/src/StackExchange.Redis/Configuration/Tunnel.cs @@ -49,10 +49,10 @@ private sealed class HttpProxyTunnel : Tunnel { var encoding = Encoding.ASCII; var ep = Format.ToString(endpoint); - const string Prefix = "CONNECT ", Suffix = " HTTP/1.1\r\n\r\n", ExpectedResponse = "HTTP/1.1 200 OK\r\n\r\n"; + const string Prefix = "CONNECT ", Suffix = " HTTP/1.1\r\n\r\n", ExpectedResponse1 = "HTTP/1.1 200 OK\r\n\r\n", ExpectedResponse2 = "HTTP/1.1 200 Connection established\r\n\r\n"; byte[] chunk = ArrayPool.Shared.Rent(Math.Max( encoding.GetByteCount(Prefix) + encoding.GetByteCount(ep) + encoding.GetByteCount(Suffix), - encoding.GetByteCount(ExpectedResponse) + Math.Max(encoding.GetByteCount(ExpectedResponse1), encoding.GetByteCount(ExpectedResponse2)) )); var offset = 0; offset += encoding.GetBytes(Prefix, 0, Prefix.Length, chunk, offset); @@ -76,10 +76,11 @@ static void SafeAbort(object? obj) await args; // we expect to see: "HTTP/1.1 200 OK\n"; note our buffer is definitely big enough already - int toRead = encoding.GetByteCount(ExpectedResponse), read; + int toRead = Math.Max(encoding.GetByteCount(ExpectedResponse1), encoding.GetByteCount(ExpectedResponse2)), read; offset = 0; - while (toRead > 0) + var actualResponse = ""; + while (toRead > 0 && !actualResponse.EndsWith("\r\n\r\n")) { args.SetBuffer(chunk, offset, toRead); if (!socket.ReceiveAsync(args)) args.Complete(); @@ -88,11 +89,12 @@ static void SafeAbort(object? obj) if (read <= 0) break; // EOF (since we're never doing zero-length reads) toRead -= read; offset += read; + + actualResponse = encoding.GetString(chunk, 0, offset); } - if (toRead != 0) throw new EndOfStreamException("EOF negotiating HTTP tunnel"); + if (toRead != 0 && !actualResponse.EndsWith("\r\n\r\n")) throw new EndOfStreamException("EOF negotiating HTTP tunnel"); // lazy - var actualResponse = encoding.GetString(chunk, 0, offset); - if (ExpectedResponse != actualResponse) + if (ExpectedResponse1 != actualResponse && ExpectedResponse2 != actualResponse) { throw new InvalidOperationException("Unexpected response negotiating HTTP tunnel"); } diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 48a20ed66..e9f01264f 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -912,19 +912,25 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) { Tunnel = null; } - else if (value.StartsWith("http:")) + else { - value = value.Substring(5); - if (!Format.TryParseEndPoint(value, out var ep)) + // For backwards compatibility with `http:address_with_port`. + if (value.StartsWith("http:") && !value.StartsWith("http://")) + { + value = value.Insert(5, "//"); + } + + var uri = new Uri(value, UriKind.Absolute); + if (uri.Scheme != "http") + { + throw new ArgumentException("Tunnel cannot be parsed: " + value); + } + if (!Format.TryParseEndPoint($"{uri.Host}:{uri.Port}", out var ep)) { throw new ArgumentException("HTTP tunnel cannot be parsed: " + value); } Tunnel = Tunnel.HttpProxy(ep); } - else - { - throw new ArgumentException("Tunnel cannot be parsed: " + value); - } break; // Deprecated options we ignore... case OptionKeys.HighPrioritySocketThreads: diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index a90bd96df..c07b32c8a 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -619,19 +619,21 @@ public async Task MutableOptions() Assert.Equal(newPass, conn.RawConfig.Password); } - [Fact] - public void HttpTunnelCanRoundtrip() + [Theory] + [InlineData("http://somewhere:22", "http:somewhere:22")] + [InlineData("http:somewhere:22", "http:somewhere:22")] + public void HttpTunnelCanRoundtrip(string input, string expected) { - var config = ConfigurationOptions.Parse("127.0.0.1:6380,tunnel=http:somewhere:22"); + var config = ConfigurationOptions.Parse($"127.0.0.1:6380,tunnel={input}"); var ip = Assert.IsType(Assert.Single(config.EndPoints)); Assert.Equal(6380, ip.Port); Assert.Equal("127.0.0.1", ip.Address.ToString()); Assert.NotNull(config.Tunnel); - Assert.Equal("http:somewhere:22", config.Tunnel.ToString()); + Assert.Equal(expected, config.Tunnel.ToString()); var cs = config.ToString(); - Assert.Equal("127.0.0.1:6380,tunnel=http:somewhere:22", cs); + Assert.Equal($"127.0.0.1:6380,tunnel={expected}", cs); } private class CustomTunnel : Tunnel { } From 9fda7c56a866df814923f3dde2adb68c1957efc9 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 2 May 2023 11:09:21 -0400 Subject: [PATCH 226/435] Backlog timeout: include the time in message (#2452) One regression I just noticed in the exception message tweaks we did was we no longer readily include the timeout in the main message...but we should. Fixing messages and tests here. Before: > Exception: The message timed out in the backlog attempting to send because no connection became available - Last Connection Exception: InternalFailure on 127.0.0.1:6379/Interactive, Initializing/NotStarted, last: GET, origin: ConnectedAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.99.22667, command=PING, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=18,QueuedItems=0,CompletedItems=65), v: 2.6.99.22667 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) After: > Exception: The message timed out in the backlog attempting to send because no connection became available **(100ms)** - Last Connection Exception: InternalFailure on 127.0.0.1:6379/Interactive, Initializing/NotStarted, last: GET, origin: ConnectedAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.99.22667, command=PING, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=18,QueuedItems=0,CompletedItems=65), v: 2.6.99.22667 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ExceptionFactory.cs | 1 + .../StackExchange.Redis.Tests/AbortOnConnectFailTests.cs | 8 ++++---- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 9b39d48f3..e3bea8649 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -12,6 +12,7 @@ Current package versions: - Add: Support for `User`/`Password` in `DefaultOptionsProvider` to support token rotation scenarios ([#2445 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2445)) - Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Resolve AOT trim warnings in `TryGetAzureRoleInstanceIdNoThrow` ([#2451 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2451)) - Adds: Support for `HTTP/1.1 200 Connection established` in HTTP Tunnel ([#2448 by flobernd](https://github.com/StackExchange/StackExchange.Redis/pull/2448)) +- Adds: Timeout duration to backlog timeout error messages ([#2452 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2452)) ## 2.6.104 diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 05a47b5c9..4308b4f00 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -235,6 +235,7 @@ internal static Exception Timeout(ConnectionMultiplexer multiplexer, string? bas // If we're in the situation where we've never connected if (logConnectionException && lastConnectionException is not null) { + sb.Append(" (").Append(Format.ToString(multiplexer.TimeoutMilliseconds)).Append("ms)"); sb.Append(" - Last Connection Exception: ").Append(lastConnectionException.Message); } diff --git a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs index 49138c535..0ba8fca9e 100644 --- a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs +++ b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs @@ -49,11 +49,11 @@ public void DisconnectAndReconnectThrowsConnectionExceptionSync() var server = conn.GetServerSnapshot()[0]; server.SimulateConnectionFailure(SimulatedFailureType.All); - // Exception: The message timed out in the backlog attempting to send because no connection became available - Last Connection Exception: InternalFailure on 127.0.0.1:6379/Interactive, Initializing/NotStarted, last: GET, origin: ConnectedAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.99.22667, command=PING, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=18,QueuedItems=0,CompletedItems=65), v: 2.6.99.22667 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) + // Exception: The message timed out in the backlog attempting to send because no connection became available (100ms) - Last Connection Exception: InternalFailure on 127.0.0.1:6379/Interactive, Initializing/NotStarted, last: GET, origin: ConnectedAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.99.22667, command=PING, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=18,QueuedItems=0,CompletedItems=65), v: 2.6.99.22667 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) var ex = Assert.ThrowsAny(() => db.Ping()); Log("Exception: " + ex.Message); Assert.True(ex is RedisConnectionException or RedisTimeoutException); - Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available - Last Connection Exception: ", ex.Message); + Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available (100ms) - Last Connection Exception: ", ex.Message); Assert.NotNull(ex.InnerException); var iex = Assert.IsType(ex.InnerException); Assert.Contains(iex.Message, ex.Message); @@ -73,10 +73,10 @@ public async Task DisconnectAndNoReconnectThrowsConnectionExceptionAsync() var server = conn.GetServerSnapshot()[0]; server.SimulateConnectionFailure(SimulatedFailureType.All); - // Exception: The message timed out in the backlog attempting to send because no connection became available - Last Connection Exception: InternalFailure on 127.0.0.1:6379/Interactive, Initializing/NotStarted, last: GET, origin: ConnectedAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.99.22667, command=PING, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=18,QueuedItems=0,CompletedItems=65), v: 2.6.99.22667 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) + // Exception: The message timed out in the backlog attempting to send because no connection became available (100ms) - Last Connection Exception: InternalFailure on 127.0.0.1:6379/Interactive, Initializing/NotStarted, last: GET, origin: ConnectedAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.99.22667, command=PING, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=18,QueuedItems=0,CompletedItems=65), v: 2.6.99.22667 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) var ex = await Assert.ThrowsAsync(() => db.PingAsync()); Log("Exception: " + ex.Message); - Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available - Last Connection Exception: ", ex.Message); + Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available (100ms) - Last Connection Exception: ", ex.Message); Assert.NotNull(ex.InnerException); var iex = Assert.IsType(ex.InnerException); Assert.Contains(iex.Message, ex.Message); From 0dfa58f144aca6f6e85c506da2aaaa2196b72001 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 2 May 2023 17:17:49 -0400 Subject: [PATCH 227/435] DefaultOptionsProvider: allow overriding LibName (#2453) If a wrapper package is generally in use in a deployment, it may want to override what we set as the library name in `CLIENT SETINFO lib-name `. This allows doing so via the `DefaultOptionsProvider` (intentionally not on `ConfigurationOptions` directly as version isn't either). Note that this does NOT upgrade the test suite to 7.2.0 RC1. I did test and this works, but there are other breaks we need to evaluate - I'll open another PR separately to demonstrate. --- docs/ReleaseNotes.md | 1 + .../Configuration/DefaultOptionsProvider.cs | 6 ++++ .../PublicAPI/PublicAPI.Shipped.txt | 1 + src/StackExchange.Redis/RedisFeatures.cs | 3 +- src/StackExchange.Redis/RedisLiterals.cs | 1 - src/StackExchange.Redis/ServerEndPoint.cs | 12 +++++--- .../DefaultOptionsTests.cs | 29 +++++++++++++++++++ tests/StackExchange.Redis.Tests/TestBase.cs | 2 +- 8 files changed, 48 insertions(+), 7 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index e3bea8649..1051dfee2 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -13,6 +13,7 @@ Current package versions: - Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Resolve AOT trim warnings in `TryGetAzureRoleInstanceIdNoThrow` ([#2451 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2451)) - Adds: Support for `HTTP/1.1 200 Connection established` in HTTP Tunnel ([#2448 by flobernd](https://github.com/StackExchange/StackExchange.Redis/pull/2448)) - Adds: Timeout duration to backlog timeout error messages ([#2452 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2452)) +- Adds: `DefaultOptionsProvider.LibraryName` for specifying lib-name passed to `CLIENT SETINFO` in Redis 7.2+ ([#2453 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2453)) ## 2.6.104 diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index f990d8265..ced64c8be 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -197,6 +197,12 @@ protected virtual string GetDefaultClientName() => ?? ComputerName ?? "StackExchange.Redis") + "(SE.Redis-v" + LibraryVersion + ")"; + /// + /// Gets the library name to use for CLIENT SETINFO lib-name calls to Redis during handshake. + /// Defaults to "SE.Redis". + /// + public virtual string LibraryName => "SE.Redis"; + /// /// String version of the StackExchange.Redis library, for use in any options. /// diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 0f5a2880d..ccfb4edb1 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1788,6 +1788,7 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludeDetailIn virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludePerformanceCountersInExceptions.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IsMatch(System.Net.EndPoint! endpoint) -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.KeepAliveInterval.get -> System.TimeSpan +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.LibraryName.get -> string! virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Password.get -> string? virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Proxy.get -> StackExchange.Redis.Proxy virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ReconnectRetryPolicy.get -> StackExchange.Redis.IReconnectRetryPolicy? diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 1a5bd9b98..70c7d4fbc 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -39,7 +39,8 @@ public readonly struct RedisFeatures v6_0_0 = new Version(6, 0, 0), v6_0_6 = new Version(6, 0, 6), v6_2_0 = new Version(6, 2, 0), - v7_0_0_rc1 = new Version(6, 9, 240); // 7.0 RC1 is version 6.9.240 + v7_0_0_rc1 = new Version(6, 9, 240), // 7.0 RC1 is version 6.9.240 + v7_2_0_rc1 = new Version(7, 1, 240); // 7.2 RC1 is version 7.1.240 private readonly Version version; diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index e6d1e76c2..e926b6da4 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -120,7 +120,6 @@ public static readonly RedisValue REWRITE = "REWRITE", RIGHT = "RIGHT", SAVE = "SAVE", - SE_Redis = "SE.Redis", SEGFAULT = "SEGFAULT", SET = "SET", SETINFO = "SETINFO", diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 45b62896c..36163578d 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -935,10 +935,14 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) // server version, so we will use this speculatively and hope for the best log?.WriteLine($"{Format.ToString(this)}: Setting client lib/ver"); - msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, - RedisLiterals.SETINFO, RedisLiterals.lib_name, RedisLiterals.SE_Redis); - msg.SetInternalCall(); - await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); + var libName = Multiplexer.RawConfig.Defaults.LibraryName; + if (!string.IsNullOrWhiteSpace(libName)) + { + msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, + RedisLiterals.SETINFO, RedisLiterals.lib_name, libName); + msg.SetInternalCall(); + await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); + } var version = Utils.GetLibVersion(); if (!string.IsNullOrWhiteSpace(version)) diff --git a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs index 8223a0d21..e59926379 100644 --- a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs +++ b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -158,4 +159,32 @@ public async Task ClientNameExplicitWins() Assert.True(conn.IsConnected); Assert.Equal("FooBar", conn.ClientName); } + + public class TestLibraryNameOptionsProvider : DefaultOptionsProvider + { + public string Id { get; } = Guid.NewGuid().ToString(); + public override string LibraryName => Id; + } + + [Fact] + public async Task LibraryNameOverride() + { + var options = ConfigurationOptions.Parse(GetConfiguration()); + var defaults = new TestLibraryNameOptionsProvider(); + options.AllowAdmin = true; + options.Defaults = defaults; + + using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + // CLIENT SETINFO is in 7.2.0+ + ThrowIfBelowMinVersion(conn, RedisFeatures.v7_2_0_rc1); + + var clients = await GetServer(conn).ClientListAsync(); + foreach (var client in clients) + { + Log("Library name: " + client.LibraryName); + } + + Assert.True(conn.IsConnected); + Assert.True(clients.Any(c => c.LibraryName == defaults.LibraryName), "Did not find client with name: " + defaults.Id); + } } diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 98ea41550..6738bf490 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -315,7 +315,7 @@ internal virtual IInternalConnectionMultiplexer Create( return conn; } - private void ThrowIfBelowMinVersion(IInternalConnectionMultiplexer conn, Version? requiredVersion) + protected void ThrowIfBelowMinVersion(IConnectionMultiplexer conn, Version? requiredVersion) { if (requiredVersion is null) { From 19ce4085eb1a7c78ca6ab2e3410cccc11bc91a83 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 2 May 2023 21:28:32 -0400 Subject: [PATCH 228/435] 2.6.111 Release notes --- docs/ReleaseNotes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 1051dfee2..b2c6243af 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,10 @@ Current package versions: ## Unreleased +No pending unreleased changes. + +## 2.6.111 + - Fix [#2426](https://github.com/StackExchange/StackExchange.Redis/issues/2426): Don't restrict multi-slot operations on Envoy proxy; let the proxy decide ([#2428 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2428)) - Add: Support for `User`/`Password` in `DefaultOptionsProvider` to support token rotation scenarios ([#2445 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2445)) - Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Resolve AOT trim warnings in `TryGetAzureRoleInstanceIdNoThrow` ([#2451 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2451)) From 89b20296806abfb3590d1847c7b0483fb157b994 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Thu, 4 May 2023 11:59:32 -0500 Subject: [PATCH 229/435] Update Pipelines.Sockets.Unofficial to v2.2.8 (#2456) This version contains fixes for supporting NativeAOT. Contributes to #2449 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2a844af7c..309672642 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ - + From ae6419a164600b4b54dd7ce8a699efe8e98d8f1c Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Thu, 4 May 2023 13:14:20 -0500 Subject: [PATCH 230/435] Update ReleaseNotes for #2456 (#2457) Update ReleaseNotes for #2456 --- docs/ReleaseNotes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b2c6243af..f60adc161 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -No pending unreleased changes. +- Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Update `Pipelines.Sockets.Unofficial` to `v2.2.8` to support native AOT ([#2456 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2456)) ## 2.6.111 From 8abe0025b80cda5960288fb9987da9828187eee6 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 13 Jun 2023 16:11:45 +0100 Subject: [PATCH 231/435] add RedisChannel UseImplicitAutoPattern and IsPatternBased (#2480) * add RedisChannel UseImplicitAutoPattern and IsPatternBased * mark the implicit RedisChannel operators as [Obsolete] (#2481) --- docs/ReleaseNotes.md | 2 + .../ConfigurationOptions.cs | 2 +- .../ConnectionMultiplexer.Sentinel.cs | 12 +- .../ConnectionMultiplexer.cs | 4 +- .../KeyspaceIsolation/KeyPrefixed.cs | 7 +- .../Maintenance/AzureMaintenanceEvent.cs | 2 +- src/StackExchange.Redis/PhysicalBridge.cs | 2 +- .../PublicAPI/PublicAPI.Shipped.txt | 7 + src/StackExchange.Redis/RedisChannel.cs | 79 +++++++-- src/StackExchange.Redis/RedisSubscriber.cs | 2 +- src/StackExchange.Redis/ServerEndPoint.cs | 2 +- tests/ConsoleTest/Program.cs | 2 +- .../StackExchange.Redis.Tests/ChannelTests.cs | 153 ++++++++++++++++++ .../StackExchange.Redis.Tests/ConfigTests.cs | 2 +- .../Issues/Issue1101Tests.cs | 6 +- .../KeyPrefixedDatabaseTests.cs | 2 + .../KeyPrefixedTests.cs | 4 +- .../PreserveOrderTests.cs | 4 +- .../PubSubCommandTests.cs | 10 ++ .../PubSubMultiserverTests.cs | 8 +- .../StackExchange.Redis.Tests/PubSubTests.cs | 56 +++++-- .../SentinelFailoverTests.cs | 2 + tests/StackExchange.Redis.Tests/TestBase.cs | 2 +- 23 files changed, 322 insertions(+), 50 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/ChannelTests.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index f60adc161..70b49d1b6 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,8 @@ Current package versions: ## Unreleased +- Fix [#2479](https://github.com/StackExchange/StackExchange.Redis/issues/2479): Add `RedisChannel.UseImplicitAutoPattern` (global) and `RedisChannel.IsPatternBased` ([#2480 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2480)) +- Fix [#2479](https://github.com/StackExchange/StackExchange.Redis/issues/2479): Mark `RedisChannel` conversion operators as obsolete; add `RedisChannel.Literal` and `RedisChannel.Pattern` helpers ([#2481 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2481)) - Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Update `Pipelines.Sockets.Unofficial` to `v2.2.8` to support native AOT ([#2456 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2456)) ## 2.6.111 diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index e9f01264f..da30beb56 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -860,7 +860,7 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) ClientName = value; break; case OptionKeys.ChannelPrefix: - ChannelPrefix = value; + ChannelPrefix = RedisChannel.Literal(value); break; case OptionKeys.ConfigChannel: ConfigurationChannel = value; diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs index 302bccea9..145826e3c 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs @@ -1,11 +1,11 @@ -using System; +using Pipelines.Sockets.Unofficial; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; -using Pipelines.Sockets.Unofficial; namespace StackExchange.Redis; @@ -30,9 +30,9 @@ internal void InitializeSentinel(LogProxy? logProxy) // Subscribe to sentinel change events ISubscriber sub = GetSubscriber(); - if (sub.SubscribedEndpoint("+switch-master") == null) + if (sub.SubscribedEndpoint(RedisChannel.Literal("+switch-master")) == null) { - sub.Subscribe("+switch-master", (__, message) => + sub.Subscribe(RedisChannel.Literal("+switch-master"), (__, message) => { string[] messageParts = ((string)message!).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); // We don't care about the result of this - we're just trying @@ -68,9 +68,9 @@ internal void InitializeSentinel(LogProxy? logProxy) ReconfigureAsync(first: false, reconfigureAll: true, logProxy, e.EndPoint, "Lost sentinel connection", false).Wait(); // Subscribe to new sentinels being added - if (sub.SubscribedEndpoint("+sentinel") == null) + if (sub.SubscribedEndpoint(RedisChannel.Literal("+sentinel")) == null) { - sub.Subscribe("+sentinel", (_, message) => + sub.Subscribe(RedisChannel.Literal("+sentinel"), (_, message) => { string[] messageParts = ((string)message!).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); UpdateSentinelAddressList(messageParts[0]); diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 37d45be09..88c6c37c2 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -2221,7 +2221,7 @@ public long PublishReconfigure(CommandFlags flags = CommandFlags.None) private long PublishReconfigureImpl(CommandFlags flags) => ConfigurationChangedChannel is byte[] channel - ? GetSubscriber().Publish(channel, RedisLiterals.Wildcard, flags) + ? GetSubscriber().Publish(RedisChannel.Literal(channel), RedisLiterals.Wildcard, flags) : 0; /// @@ -2231,7 +2231,7 @@ ConfigurationChangedChannel is byte[] channel /// The number of instances known to have received the message (however, the actual number can be higher). public Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None) => ConfigurationChangedChannel is byte[] channel - ? GetSubscriber().PublishAsync(channel, RedisLiterals.Wildcard, flags) + ? GetSubscriber().PublishAsync(RedisChannel.Literal(channel), RedisLiterals.Wildcard, flags) : CompletedTask.Default(null); /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 290fbed59..3bad77d43 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -841,8 +841,11 @@ protected RedisValue SortGetToInner(RedisValue outer) => } } - protected RedisChannel ToInner(RedisChannel outer) => - RedisKey.ConcatenateBytes(Prefix, null, (byte[]?)outer); + protected RedisChannel ToInner(RedisChannel outer) + { + var combined = RedisKey.ConcatenateBytes(Prefix, null, (byte[]?)outer); + return new RedisChannel(combined, outer.IsPatternBased ? RedisChannel.PatternMode.Pattern : RedisChannel.PatternMode.Literal); + } private Func? mapFunction; protected Func GetMapFunction() => diff --git a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs index c965e1231..330b27683 100644 --- a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs @@ -130,7 +130,7 @@ internal async static Task AddListenerAsync(ConnectionMultiplexer multiplexer, A return; } - await sub.SubscribeAsync(PubSubChannelName, async (_, message) => + await sub.SubscribeAsync(RedisChannel.Literal(PubSubChannelName), async (_, message) => { var newMessage = new AzureMaintenanceEvent(message!); newMessage.NotifyMultiplexer(multiplexer); diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index e7af56a69..b42c40a19 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -373,7 +373,7 @@ internal void KeepAlive() else if (commandMap.IsAvailable(RedisCommand.UNSUBSCRIBE)) { msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.UNSUBSCRIBE, - (RedisChannel)Multiplexer.UniqueId); + RedisChannel.Literal(Multiplexer.UniqueId)); msg.SetSource(ResultProcessor.TrackSubscriptions, null); } break; diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index ccfb4edb1..464af8023 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1259,6 +1259,7 @@ StackExchange.Redis.Proxy.Twemproxy = 1 -> StackExchange.Redis.Proxy StackExchange.Redis.RedisChannel StackExchange.Redis.RedisChannel.Equals(StackExchange.Redis.RedisChannel other) -> bool StackExchange.Redis.RedisChannel.IsNullOrEmpty.get -> bool +StackExchange.Redis.RedisChannel.IsPatternBased.get -> bool StackExchange.Redis.RedisChannel.PatternMode StackExchange.Redis.RedisChannel.PatternMode.Auto = 0 -> StackExchange.Redis.RedisChannel.PatternMode StackExchange.Redis.RedisChannel.PatternMode.Literal = 1 -> StackExchange.Redis.RedisChannel.PatternMode @@ -1657,6 +1658,8 @@ static StackExchange.Redis.RedisChannel.implicit operator byte[]?(StackExchange. static StackExchange.Redis.RedisChannel.implicit operator StackExchange.Redis.RedisChannel(byte[]? key) -> StackExchange.Redis.RedisChannel static StackExchange.Redis.RedisChannel.implicit operator StackExchange.Redis.RedisChannel(string! key) -> StackExchange.Redis.RedisChannel static StackExchange.Redis.RedisChannel.implicit operator string?(StackExchange.Redis.RedisChannel key) -> string? +static StackExchange.Redis.RedisChannel.Literal(byte[]! value) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.Literal(string! value) -> StackExchange.Redis.RedisChannel static StackExchange.Redis.RedisChannel.operator !=(byte[]! x, StackExchange.Redis.RedisChannel y) -> bool static StackExchange.Redis.RedisChannel.operator !=(StackExchange.Redis.RedisChannel x, byte[]! y) -> bool static StackExchange.Redis.RedisChannel.operator !=(StackExchange.Redis.RedisChannel x, StackExchange.Redis.RedisChannel y) -> bool @@ -1667,6 +1670,10 @@ static StackExchange.Redis.RedisChannel.operator ==(StackExchange.Redis.RedisCha static StackExchange.Redis.RedisChannel.operator ==(StackExchange.Redis.RedisChannel x, StackExchange.Redis.RedisChannel y) -> bool static StackExchange.Redis.RedisChannel.operator ==(StackExchange.Redis.RedisChannel x, string! y) -> bool static StackExchange.Redis.RedisChannel.operator ==(string! x, StackExchange.Redis.RedisChannel y) -> bool +static StackExchange.Redis.RedisChannel.Pattern(byte[]! value) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.Pattern(string! value) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.UseImplicitAutoPattern.get -> bool +static StackExchange.Redis.RedisChannel.UseImplicitAutoPattern.set -> void static StackExchange.Redis.RedisFeatures.operator !=(StackExchange.Redis.RedisFeatures left, StackExchange.Redis.RedisFeatures right) -> bool static StackExchange.Redis.RedisFeatures.operator ==(StackExchange.Redis.RedisFeatures left, StackExchange.Redis.RedisFeatures right) -> bool static StackExchange.Redis.RedisKey.implicit operator byte[]?(StackExchange.Redis.RedisKey key) -> byte[]? diff --git a/src/StackExchange.Redis/RedisChannel.cs b/src/StackExchange.Redis/RedisChannel.cs index ffd56aed4..16d4e7107 100644 --- a/src/StackExchange.Redis/RedisChannel.cs +++ b/src/StackExchange.Redis/RedisChannel.cs @@ -9,33 +9,67 @@ namespace StackExchange.Redis public readonly struct RedisChannel : IEquatable { internal readonly byte[]? Value; - internal readonly bool IsPatternBased; + internal readonly bool _isPatternBased; /// /// Indicates whether the channel-name is either null or a zero-length value. /// public bool IsNullOrEmpty => Value == null || Value.Length == 0; + /// + /// Indicates whether this channel represents a wildcard pattern (see PSUBSCRIBE) + /// + public bool IsPatternBased => _isPatternBased; + internal bool IsNull => Value == null; + + /// + /// Indicates whether channels should use when no + /// is specified; this is enabled by default, but can be disabled to avoid unexpected wildcard scenarios. + /// + public static bool UseImplicitAutoPattern + { + get => s_DefaultPatternMode == PatternMode.Auto; + set => s_DefaultPatternMode = value ? PatternMode.Auto : PatternMode.Literal; + } + private static PatternMode s_DefaultPatternMode = PatternMode.Auto; + + /// + /// Creates a new that does not act as a wildcard subscription + /// + public static RedisChannel Literal(string value) => new RedisChannel(value, PatternMode.Literal); + /// + /// Creates a new that does not act as a wildcard subscription + /// + public static RedisChannel Literal(byte[] value) => new RedisChannel(value, PatternMode.Literal); + /// + /// Creates a new that acts as a wildcard subscription + /// + public static RedisChannel Pattern(string value) => new RedisChannel(value, PatternMode.Pattern); + /// + /// Creates a new that acts as a wildcard subscription + /// + public static RedisChannel Pattern(byte[] value) => new RedisChannel(value, PatternMode.Pattern); + /// /// Create a new redis channel from a buffer, explicitly controlling the pattern mode. /// /// The name of the channel to create. /// The mode for name matching. - public RedisChannel(byte[]? value, PatternMode mode) : this(value, DeterminePatternBased(value, mode)) {} + public RedisChannel(byte[]? value, PatternMode mode) : this(value, DeterminePatternBased(value, mode)) { } /// /// Create a new redis channel from a string, explicitly controlling the pattern mode. /// /// The string name of the channel to create. /// The mode for name matching. - public RedisChannel(string value, PatternMode mode) : this(value == null ? null : Encoding.UTF8.GetBytes(value), mode) {} + public RedisChannel(string value, PatternMode mode) : this(value == null ? null : Encoding.UTF8.GetBytes(value), mode) { } private RedisChannel(byte[]? value, bool isPatternBased) { Value = value; - IsPatternBased = isPatternBased; + _isPatternBased = isPatternBased; } private static bool DeterminePatternBased(byte[]? value, PatternMode mode) => mode switch @@ -87,7 +121,7 @@ private RedisChannel(byte[]? value, bool isPatternBased) /// The first to compare. /// The second to compare. public static bool operator ==(RedisChannel x, RedisChannel y) => - x.IsPatternBased == y.IsPatternBased && RedisValue.Equals(x.Value, y.Value); + x._isPatternBased == y._isPatternBased && RedisValue.Equals(x.Value, y.Value); /// /// Indicate whether two channel names are equal. @@ -135,10 +169,10 @@ private RedisChannel(byte[]? value, bool isPatternBased) /// Indicate whether two channel names are equal. /// /// The to compare to. - public bool Equals(RedisChannel other) => IsPatternBased == other.IsPatternBased && RedisValue.Equals(Value, other.Value); + public bool Equals(RedisChannel other) => _isPatternBased == other._isPatternBased && RedisValue.Equals(Value, other.Value); /// - public override int GetHashCode() => RedisValue.GetHashCode(Value) + (IsPatternBased ? 1 : 0); + public override int GetHashCode() => RedisValue.GetHashCode(Value) + (_isPatternBased ? 1 : 0); /// /// Obtains a string representation of the channel name. @@ -159,7 +193,16 @@ internal void AssertNotNull() if (IsNull) throw new ArgumentException("A null key is not valid in this context"); } - internal RedisChannel Clone() => (byte[]?)Value?.Clone() ?? default; + internal RedisChannel Clone() + { + if (Value is null || Value.Length == 0) + { + // no need to duplicate anything + return this; + } + var copy = (byte[])Value.Clone(); // defensive array copy + return new RedisChannel(copy, _isPatternBased); + } /// /// The matching pattern for this channel. @@ -184,33 +227,35 @@ public enum PatternMode /// Create a channel name from a . /// /// The string to get a channel from. + [Obsolete("It is preferable to explicitly specify a " + nameof(PatternMode) + ", or use the " + nameof(Literal) + "/" + nameof(Pattern) + " methods", error: false)] public static implicit operator RedisChannel(string key) { if (key == null) return default; - return new RedisChannel(Encoding.UTF8.GetBytes(key), PatternMode.Auto); + return new RedisChannel(Encoding.UTF8.GetBytes(key), s_DefaultPatternMode); } /// /// Create a channel name from a . /// /// The byte array to get a channel from. + [Obsolete("It is preferable to explicitly specify a " + nameof(PatternMode) + ", or use the " + nameof(Literal) + "/" + nameof(Pattern) + " methods", error: false)] public static implicit operator RedisChannel(byte[]? key) { if (key == null) return default; - return new RedisChannel(key, PatternMode.Auto); + return new RedisChannel(key, s_DefaultPatternMode); } /// /// Obtain the channel name as a . /// /// The channel to get a byte[] from. - public static implicit operator byte[]? (RedisChannel key) => key.Value; + public static implicit operator byte[]?(RedisChannel key) => key.Value; /// /// Obtain the channel name as a . /// /// The channel to get a string from. - public static implicit operator string? (RedisChannel key) + public static implicit operator string?(RedisChannel key) { var arr = key.Value; if (arr == null) @@ -226,5 +271,15 @@ public static implicit operator RedisChannel(byte[]? key) return BitConverter.ToString(arr); } } + +#if DEBUG + // these exist *purely* to ensure that we never add them later *without* + // giving due consideration to the default pattern mode (UseImplicitAutoPattern) + // (since we don't ship them, we don't need them in release) + [Obsolete("Watch for " + nameof(UseImplicitAutoPattern), error: true)] + private RedisChannel(string value) => throw new NotSupportedException(); + [Obsolete("Watch for " + nameof(UseImplicitAutoPattern), error: true)] + private RedisChannel(byte[]? value) => throw new NotSupportedException(); +#endif } } diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index d26368c20..39b99bfe2 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -159,7 +159,7 @@ public Subscription(CommandFlags flags) /// internal Message GetMessage(RedisChannel channel, SubscriptionAction action, CommandFlags flags, bool internalCall) { - var isPattern = channel.IsPatternBased; + var isPattern = channel._isPatternBased; var command = action switch { SubscriptionAction.Subscribe when isPattern => RedisCommand.PSUBSCRIBE, diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 36163578d..d3082e35c 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -979,7 +979,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) var configChannel = Multiplexer.ConfigurationChangedChannel; if (configChannel != null) { - msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.SUBSCRIBE, (RedisChannel)configChannel); + msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.SUBSCRIBE, RedisChannel.Literal(configChannel)); // Note: this is NOT internal, we want it to queue in a backlog for sending when ready if necessary await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.TrackSubscriptions).ForAwait(); } diff --git a/tests/ConsoleTest/Program.cs b/tests/ConsoleTest/Program.cs index 1d33a968e..0d6fa7e13 100644 --- a/tests/ConsoleTest/Program.cs +++ b/tests/ConsoleTest/Program.cs @@ -110,7 +110,7 @@ static void ParallelRun(int taskId, ConnectionMultiplexer connection) static void MassPublish(ConnectionMultiplexer connection) { var subscriber = connection.GetSubscriber(); - Parallel.For(0, 1000, _ => subscriber.Publish("cache-events:cache-testing", "hey")); + Parallel.For(0, 1000, _ => subscriber.Publish(new RedisChannel("cache-events:cache-testing", RedisChannel.PatternMode.Literal), "hey")); } static string GetLibVersion() diff --git a/tests/StackExchange.Redis.Tests/ChannelTests.cs b/tests/StackExchange.Redis.Tests/ChannelTests.cs new file mode 100644 index 000000000..3f11d2ef1 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ChannelTests.cs @@ -0,0 +1,153 @@ +using System.Text; +using Xunit; + +namespace StackExchange.Redis.Tests +{ + public class ChannelTests + { + [Fact] + public void UseImplicitAutoPattern_OnByDefault() + { + Assert.True(RedisChannel.UseImplicitAutoPattern); + } + + [Theory] + [InlineData("abc", true, false)] + [InlineData("abc*def", true, true)] + [InlineData("abc", false, false)] + [InlineData("abc*def", false, false)] + public void ValidateAutoPatternModeString(string name, bool useImplicitAutoPattern, bool isPatternBased) + { + bool oldValue = RedisChannel.UseImplicitAutoPattern; + try + { + RedisChannel.UseImplicitAutoPattern = useImplicitAutoPattern; +#pragma warning disable CS0618 // we need to test the operator + RedisChannel channel = name; +#pragma warning restore CS0618 + Assert.Equal(isPatternBased, channel.IsPatternBased); + } + finally + { + RedisChannel.UseImplicitAutoPattern = oldValue; + } + } + + [Theory] + [InlineData("abc", RedisChannel.PatternMode.Auto, true, false)] + [InlineData("abc*def", RedisChannel.PatternMode.Auto, true, true)] + [InlineData("abc", RedisChannel.PatternMode.Literal, true, false)] + [InlineData("abc*def", RedisChannel.PatternMode.Literal, true, false)] + [InlineData("abc", RedisChannel.PatternMode.Pattern, true, true)] + [InlineData("abc*def", RedisChannel.PatternMode.Pattern, true, true)] + [InlineData("abc", RedisChannel.PatternMode.Auto, false, false)] + [InlineData("abc*def", RedisChannel.PatternMode.Auto, false, true)] + [InlineData("abc", RedisChannel.PatternMode.Literal, false, false)] + [InlineData("abc*def", RedisChannel.PatternMode.Literal, false, false)] + [InlineData("abc", RedisChannel.PatternMode.Pattern, false, true)] + [InlineData("abc*def", RedisChannel.PatternMode.Pattern, false, true)] + public void ValidateModeSpecifiedIgnoresGlobalSetting(string name, RedisChannel.PatternMode mode, bool useImplicitAutoPattern, bool isPatternBased) + { + bool oldValue = RedisChannel.UseImplicitAutoPattern; + try + { + RedisChannel.UseImplicitAutoPattern = useImplicitAutoPattern; + RedisChannel channel = new(name, mode); + Assert.Equal(isPatternBased, channel.IsPatternBased); + } + finally + { + RedisChannel.UseImplicitAutoPattern = oldValue; + } + } + + [Theory] + [InlineData("abc", true, false)] + [InlineData("abc*def", true, true)] + [InlineData("abc", false, false)] + [InlineData("abc*def", false, false)] + public void ValidateAutoPatternModeBytes(string name, bool useImplicitAutoPattern, bool isPatternBased) + { + var bytes = Encoding.UTF8.GetBytes(name); + bool oldValue = RedisChannel.UseImplicitAutoPattern; + try + { + RedisChannel.UseImplicitAutoPattern = useImplicitAutoPattern; +#pragma warning disable CS0618 // we need to test the operator + RedisChannel channel = bytes; +#pragma warning restore CS0618 + Assert.Equal(isPatternBased, channel.IsPatternBased); + } + finally + { + RedisChannel.UseImplicitAutoPattern = oldValue; + } + } + + [Theory] + [InlineData("abc", RedisChannel.PatternMode.Auto, true, false)] + [InlineData("abc*def", RedisChannel.PatternMode.Auto, true, true)] + [InlineData("abc", RedisChannel.PatternMode.Literal, true, false)] + [InlineData("abc*def", RedisChannel.PatternMode.Literal, true, false)] + [InlineData("abc", RedisChannel.PatternMode.Pattern, true, true)] + [InlineData("abc*def", RedisChannel.PatternMode.Pattern, true, true)] + [InlineData("abc", RedisChannel.PatternMode.Auto, false, false)] + [InlineData("abc*def", RedisChannel.PatternMode.Auto, false, true)] + [InlineData("abc", RedisChannel.PatternMode.Literal, false, false)] + [InlineData("abc*def", RedisChannel.PatternMode.Literal, false, false)] + [InlineData("abc", RedisChannel.PatternMode.Pattern, false, true)] + [InlineData("abc*def", RedisChannel.PatternMode.Pattern, false, true)] + public void ValidateModeSpecifiedIgnoresGlobalSettingBytes(string name, RedisChannel.PatternMode mode, bool useImplicitAutoPattern, bool isPatternBased) + { + var bytes = Encoding.UTF8.GetBytes(name); + bool oldValue = RedisChannel.UseImplicitAutoPattern; + try + { + RedisChannel.UseImplicitAutoPattern = useImplicitAutoPattern; + RedisChannel channel = new(bytes, mode); + Assert.Equal(isPatternBased, channel.IsPatternBased); + } + finally + { + RedisChannel.UseImplicitAutoPattern = oldValue; + } + } + + [Theory] + [InlineData("abc*def", false)] + [InlineData("abcdef", false)] + [InlineData("abc*def", true)] + [InlineData("abcdef", true)] + public void ValidateLiteralPatternMode(string name, bool useImplicitAutoPattern) + { + bool oldValue = RedisChannel.UseImplicitAutoPattern; + try + { + RedisChannel.UseImplicitAutoPattern = useImplicitAutoPattern; + RedisChannel channel; + + // literal, string + channel = RedisChannel.Literal(name); + Assert.False(channel.IsPatternBased); + + // pattern, string + channel = RedisChannel.Pattern(name); + Assert.True(channel.IsPatternBased); + + var bytes = Encoding.UTF8.GetBytes(name); + + // literal, byte[] + channel = RedisChannel.Literal(bytes); + Assert.False(channel.IsPatternBased); + + // pattern, byte[] + channel = RedisChannel.Pattern(bytes); + Assert.True(channel.IsPatternBased); + } + finally + { + RedisChannel.UseImplicitAutoPattern = oldValue; + } + } + } +} diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index c07b32c8a..b11c968cf 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -276,7 +276,7 @@ public void ConnectWithSubscribeDisabled() Assert.True(servers[0].IsConnected); Assert.False(servers[0].IsSubscriberConnected); - var ex = Assert.Throws(() => conn.GetSubscriber().Subscribe(Me(), (_, _) => GC.KeepAlive(this))); + var ex = Assert.Throws(() => conn.GetSubscriber().Subscribe(RedisChannel.Literal(Me()), (_, _) => GC.KeepAlive(this))); Assert.Equal("This operation has been disabled in the command-map and cannot be used: SUBSCRIBE", ex.Message); } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue1101Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue1101Tests.cs index 440d18a8e..3f248b480 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue1101Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue1101Tests.cs @@ -28,7 +28,7 @@ public async Task ExecuteWithUnsubscribeViaChannel() { using var conn = Create(log: Writer); - RedisChannel name = Me(); + RedisChannel name = RedisChannel.Literal(Me()); var pubsub = conn.GetSubscriber(); AssertCounts(pubsub, name, false, 0, 0); @@ -93,7 +93,7 @@ public async Task ExecuteWithUnsubscribeViaSubscriber() { using var conn = Create(shared: false, log: Writer); - RedisChannel name = Me(); + RedisChannel name = RedisChannel.Literal(Me()); var pubsub = conn.GetSubscriber(); AssertCounts(pubsub, name, false, 0, 0); @@ -144,7 +144,7 @@ public async Task ExecuteWithUnsubscribeViaClearAll() { using var conn = Create(log: Writer); - RedisChannel name = Me(); + RedisChannel name = RedisChannel.Literal(Me()); var pubsub = conn.GetSubscriber(); AssertCounts(pubsub, name, false, 0, 0); diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs index 96f7d4c85..b4eff605a 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs @@ -568,8 +568,10 @@ public void LockTake() [Fact] public void Publish() { +#pragma warning disable CS0618 prefixed.Publish("channel", "message", CommandFlags.None); mock.Verify(_ => _.Publish("prefix:channel", "message", CommandFlags.None)); +#pragma warning restore CS0618 } [Fact] diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs index d5fea204c..2d6b53390 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs @@ -528,8 +528,8 @@ public void LockTakeAsync() [Fact] public void PublishAsync() { - prefixed.PublishAsync("channel", "message", CommandFlags.None); - mock.Verify(_ => _.PublishAsync("prefix:channel", "message", CommandFlags.None)); + prefixed.PublishAsync(RedisChannel.Literal("channel"), "message", CommandFlags.None); + mock.Verify(_ => _.PublishAsync(RedisChannel.Literal("prefix:channel"), "message", CommandFlags.None)); } [Fact] diff --git a/tests/StackExchange.Redis.Tests/PreserveOrderTests.cs b/tests/StackExchange.Redis.Tests/PreserveOrderTests.cs index b0f7df995..f11cda5c5 100644 --- a/tests/StackExchange.Redis.Tests/PreserveOrderTests.cs +++ b/tests/StackExchange.Redis.Tests/PreserveOrderTests.cs @@ -22,7 +22,7 @@ public void Execute() var received = new List(); Log("Subscribing..."); const int COUNT = 500; - sub.Subscribe(channel, (_, message) => + sub.Subscribe(RedisChannel.Literal(channel), (_, message) => { lock (received) { @@ -42,7 +42,7 @@ public void Execute() // it all goes to the server and back for (int i = 0; i < COUNT; i++) { - sub.Publish(channel, i, CommandFlags.FireAndForget); + sub.Publish(RedisChannel.Literal(channel), i, CommandFlags.FireAndForget); } Log("Allowing time for delivery etc..."); diff --git a/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs b/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs index 809a9968e..dd77dc106 100644 --- a/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs @@ -17,10 +17,12 @@ public void SubscriberCount() { using var conn = Create(); +#pragma warning disable CS0618 RedisChannel channel = Me() + Guid.NewGuid(); var server = conn.GetServer(conn.GetEndPoints()[0]); var channels = server.SubscriptionChannels(Me() + "*"); +#pragma warning restore CS0618 Assert.DoesNotContain(channel, channels); _ = server.SubscriptionPatternCount(); @@ -30,7 +32,9 @@ public void SubscriberCount() count = server.SubscriptionSubscriberCount(channel); Assert.Equal(1, count); +#pragma warning disable CS0618 channels = server.SubscriptionChannels(Me() + "*"); +#pragma warning restore CS0618 Assert.Contains(channel, channels); } @@ -39,10 +43,14 @@ public async Task SubscriberCountAsync() { using var conn = Create(); +#pragma warning disable CS0618 RedisChannel channel = Me() + Guid.NewGuid(); +#pragma warning restore CS0618 var server = conn.GetServer(conn.GetEndPoints()[0]); +#pragma warning disable CS0618 var channels = await server.SubscriptionChannelsAsync(Me() + "*").WithTimeout(2000); +#pragma warning restore CS0618 Assert.DoesNotContain(channel, channels); _ = await server.SubscriptionPatternCountAsync().WithTimeout(2000); @@ -52,7 +60,9 @@ public async Task SubscriberCountAsync() count = await server.SubscriptionSubscriberCountAsync(channel).WithTimeout(2000); Assert.Equal(1, count); +#pragma warning disable CS0618 channels = await server.SubscriptionChannelsAsync(Me() + "*").WithTimeout(2000); +#pragma warning restore CS0618 Assert.Contains(channel, channels); } } diff --git a/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs b/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs index d3e634a47..dcf706e76 100644 --- a/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs @@ -18,8 +18,8 @@ public void ChannelSharding() using var conn = (Create(channelPrefix: Me()) as ConnectionMultiplexer)!; var defaultSlot = conn.ServerSelectionStrategy.HashSlot(default(RedisChannel)); - var slot1 = conn.ServerSelectionStrategy.HashSlot((RedisChannel)"hey"); - var slot2 = conn.ServerSelectionStrategy.HashSlot((RedisChannel)"hey2"); + var slot1 = conn.ServerSelectionStrategy.HashSlot(RedisChannel.Literal("hey")); + var slot2 = conn.ServerSelectionStrategy.HashSlot(RedisChannel.Literal("hey2")); Assert.NotEqual(defaultSlot, slot1); Assert.NotEqual(ServerSelectionStrategy.NoSlot, slot1); @@ -34,7 +34,7 @@ public async Task ClusterNodeSubscriptionFailover() using var conn = (Create(allowAdmin: true) as ConnectionMultiplexer)!; var sub = conn.GetSubscriber(); - var channel = (RedisChannel)Me(); + var channel = RedisChannel.Literal(Me()); var count = 0; Log("Subscribing..."); @@ -108,7 +108,7 @@ public async Task PrimaryReplicaSubscriptionFailover(CommandFlags flags, bool ex using var conn = (Create(configuration: config, shared: false, allowAdmin: true) as ConnectionMultiplexer)!; var sub = conn.GetSubscriber(); - var channel = (RedisChannel)(Me() + flags.ToString()); // Individual channel per case to not overlap publishers + var channel = RedisChannel.Literal(Me() + flags.ToString()); // Individual channel per case to not overlap publishers var count = 0; Log("Subscribing..."); diff --git a/tests/StackExchange.Redis.Tests/PubSubTests.cs b/tests/StackExchange.Redis.Tests/PubSubTests.cs index 8fd23ffdd..833d888e9 100644 --- a/tests/StackExchange.Redis.Tests/PubSubTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubTests.cs @@ -27,9 +27,11 @@ public async Task ExplicitPublishMode() pub.Subscribe(new RedisChannel("*bcd", RedisChannel.PatternMode.Literal), (x, y) => Interlocked.Increment(ref a)); pub.Subscribe(new RedisChannel("a*cd", RedisChannel.PatternMode.Pattern), (x, y) => Interlocked.Increment(ref b)); pub.Subscribe(new RedisChannel("ab*d", RedisChannel.PatternMode.Auto), (x, y) => Interlocked.Increment(ref c)); +#pragma warning disable CS0618 pub.Subscribe("abc*", (x, y) => Interlocked.Increment(ref d)); pub.Publish("abcd", "efg"); +#pragma warning restore CS0618 await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Thread.VolatileRead(ref b) == 1 && Thread.VolatileRead(ref c) == 1 @@ -39,7 +41,9 @@ await UntilConditionAsync(TimeSpan.FromSeconds(10), Assert.Equal(1, Thread.VolatileRead(ref c)); Assert.Equal(1, Thread.VolatileRead(ref d)); +#pragma warning disable CS0618 pub.Publish("*bcd", "efg"); +#pragma warning restore CS0618 await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Thread.VolatileRead(ref a) == 1); Assert.Equal(1, Thread.VolatileRead(ref a)); } @@ -77,15 +81,19 @@ public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string br } } , handler2 = (_, __) => Interlocked.Increment(ref secondHandler); +#pragma warning disable CS0618 sub.Subscribe(subChannel, handler1); sub.Subscribe(subChannel, handler2); +#pragma warning restore CS0618 lock (received) { Assert.Empty(received); } Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); +#pragma warning disable CS0618 var count = sub.Publish(pubChannel, "def"); +#pragma warning restore CS0618 await PingAsync(pub, sub, 3).ForAwait(); @@ -99,8 +107,10 @@ public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string br Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); // unsubscribe from first; should still see second +#pragma warning disable CS0618 sub.Unsubscribe(subChannel, handler1); count = sub.Publish(pubChannel, "ghi"); +#pragma warning restore CS0618 await PingAsync(pub, sub).ForAwait(); lock (received) { @@ -115,8 +125,10 @@ public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string br Assert.Equal(1, count); // unsubscribe from second; should see nothing this time +#pragma warning disable CS0618 sub.Unsubscribe(subChannel, handler2); count = sub.Publish(pubChannel, "ghi"); +#pragma warning restore CS0618 await PingAsync(pub, sub).ForAwait(); lock (received) { @@ -137,7 +149,7 @@ public async Task TestBasicPubSubFireAndForget() var pub = GetAnyPrimary(conn); var sub = conn.GetSubscriber(); - RedisChannel key = Me() + Guid.NewGuid(); + RedisChannel key = RedisChannel.Literal(Me() + Guid.NewGuid()); HashSet received = new(); int secondHandler = 0; await PingAsync(pub, sub).ForAwait(); @@ -210,7 +222,9 @@ public async Task TestPatternPubSub() HashSet received = new(); int secondHandler = 0; +#pragma warning disable CS0618 sub.Subscribe("a*c", (channel, payload) => +#pragma warning restore CS0618 { lock (received) { @@ -221,7 +235,9 @@ public async Task TestPatternPubSub() } }); +#pragma warning disable CS0618 sub.Subscribe("a*c", (_, __) => Interlocked.Increment(ref secondHandler)); +#pragma warning restore CS0618 lock (received) { Assert.Empty(received); @@ -229,7 +245,7 @@ public async Task TestPatternPubSub() Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); await PingAsync(pub, sub).ForAwait(); - var count = sub.Publish("abc", "def"); + var count = sub.Publish(RedisChannel.Literal("abc"), "def"); await PingAsync(pub, sub).ForAwait(); await UntilConditionAsync(TimeSpan.FromSeconds(5), () => received.Count == 1); @@ -242,8 +258,10 @@ public async Task TestPatternPubSub() await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); +#pragma warning disable CS0618 sub.Unsubscribe("a*c"); count = sub.Publish("abc", "ghi"); +#pragma warning restore CS0618 await PingAsync(pub, sub).ForAwait(); @@ -259,7 +277,9 @@ public void TestPublishWithNoSubscribers() using var conn = Create(); var sub = conn.GetSubscriber(); +#pragma warning disable CS0618 Assert.Equal(0, sub.Publish(Me() + "channel", "message")); +#pragma warning restore CS0618 } [FactLongRunning] @@ -289,14 +309,18 @@ private void TestMassivePublish(ISubscriber sub, string channel, string caption) var withFAF = Stopwatch.StartNew(); for (int i = 0; i < loop; i++) { +#pragma warning disable CS0618 sub.Publish(channel, "bar", CommandFlags.FireAndForget); +#pragma warning restore CS0618 } withFAF.Stop(); var withAsync = Stopwatch.StartNew(); for (int i = 0; i < loop; i++) { +#pragma warning disable CS0618 tasks[i] = sub.PublishAsync(channel, "bar"); +#pragma warning restore CS0618 } sub.WaitAll(tasks); withAsync.Stop(); @@ -314,7 +338,7 @@ public async Task SubscribeAsyncEnumerable() using var conn = Create(syncTimeout: 20000, shared: false, log: Writer); var sub = conn.GetSubscriber(); - RedisChannel channel = Me(); + RedisChannel channel = RedisChannel.Literal(Me()); const int TO_SEND = 5; var gotall = new TaskCompletionSource(); @@ -348,7 +372,7 @@ public async Task PubSubGetAllAnyOrder() using var sonn = Create(syncTimeout: 20000, shared: false, log: Writer); var sub = sonn.GetSubscriber(); - RedisChannel channel = Me(); + RedisChannel channel = RedisChannel.Literal(Me()); const int count = 1000; var syncLock = new object(); @@ -396,7 +420,7 @@ public async Task PubSubGetAllCorrectOrder() using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { var sub = conn.GetSubscriber(); - RedisChannel channel = Me(); + RedisChannel channel = RedisChannel.Literal(Me()); const int count = 250; var syncLock = new object(); @@ -469,7 +493,7 @@ public async Task PubSubGetAllCorrectOrder_OnMessage_Sync() using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { var sub = conn.GetSubscriber(); - RedisChannel channel = Me(); + RedisChannel channel = RedisChannel.Literal(Me()); const int count = 1000; var syncLock = new object(); @@ -538,7 +562,7 @@ public async Task PubSubGetAllCorrectOrder_OnMessage_Async() using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { var sub = conn.GetSubscriber(); - RedisChannel channel = Me(); + RedisChannel channel = RedisChannel.Literal(Me()); const int count = 1000; var syncLock = new object(); @@ -616,8 +640,10 @@ public async Task TestPublishWithSubscribers() var channel = Me(); var listenA = connA.GetSubscriber(); var listenB = connB.GetSubscriber(); +#pragma warning disable CS0618 var t1 = listenA.SubscribeAsync(channel, delegate { }); var t2 = listenB.SubscribeAsync(channel, delegate { }); +#pragma warning restore CS0618 await Task.WhenAll(t1, t2).ForAwait(); @@ -625,7 +651,9 @@ public async Task TestPublishWithSubscribers() await listenA.PingAsync(); await listenB.PingAsync(); +#pragma warning disable CS0618 var pub = connPub.GetSubscriber().PublishAsync(channel, "message"); +#pragma warning restore CS0618 Assert.Equal(2, await pub); // delivery count } @@ -636,7 +664,7 @@ public async Task TestMultipleSubscribersGetMessage() using var connB = Create(shared: false, log: Writer); using var connPub = Create(); - var channel = Me(); + var channel = RedisChannel.Literal(Me()); var listenA = connA.GetSubscriber(); var listenB = connB.GetSubscriber(); connPub.GetDatabase().Ping(); @@ -668,16 +696,20 @@ public async Task Issue38() int count = 0; var prefix = Me(); void handler(RedisChannel _, RedisValue __) => Interlocked.Increment(ref count); +#pragma warning disable CS0618 var a0 = sub.SubscribeAsync(prefix + "foo", handler); var a1 = sub.SubscribeAsync(prefix + "bar", handler); var b0 = sub.SubscribeAsync(prefix + "f*o", handler); var b1 = sub.SubscribeAsync(prefix + "b*r", handler); +#pragma warning restore CS0618 await Task.WhenAll(a0, a1, b0, b1).ForAwait(); +#pragma warning disable CS0618 var c = sub.PublishAsync(prefix + "foo", "foo"); var d = sub.PublishAsync(prefix + "f@o", "f@o"); var e = sub.PublishAsync(prefix + "bar", "bar"); var f = sub.PublishAsync(prefix + "b@r", "b@r"); +#pragma warning restore CS0618 await Task.WhenAll(c, d, e, f).ForAwait(); long total = c.Result + d.Result + e.Result + f.Result; @@ -702,18 +734,22 @@ public async Task TestPartialSubscriberGetMessage() var listenB = connB.GetSubscriber(); var pub = connPub.GetSubscriber(); var prefix = Me(); +#pragma warning disable CS0618 var tA = listenA.SubscribeAsync(prefix + "channel", (s, msg) => { if (s == prefix + "channel" && msg == "message") Interlocked.Increment(ref gotA); }); var tB = listenB.SubscribeAsync(prefix + "chann*", (s, msg) => { if (s == prefix + "channel" && msg == "message") Interlocked.Increment(ref gotB); }); await Task.WhenAll(tA, tB).ForAwait(); Assert.Equal(2, pub.Publish(prefix + "channel", "message")); +#pragma warning restore CS0618 await AllowReasonableTimeToPublishAndProcess().ForAwait(); Assert.Equal(1, Interlocked.CompareExchange(ref gotA, 0, 0)); Assert.Equal(1, Interlocked.CompareExchange(ref gotB, 0, 0)); // and unsubscibe... +#pragma warning disable CS0618 tB = listenB.UnsubscribeAsync(prefix + "chann*", null); await tB; Assert.Equal(1, pub.Publish(prefix + "channel", "message")); +#pragma warning restore CS0618 await AllowReasonableTimeToPublishAndProcess().ForAwait(); Assert.Equal(2, Interlocked.CompareExchange(ref gotA, 0, 0)); Assert.Equal(1, Interlocked.CompareExchange(ref gotB, 0, 0)); @@ -729,6 +765,7 @@ public async Task TestSubscribeUnsubscribeAndSubscribeAgain() var pub = connPub.GetSubscriber(); var sub = connSub.GetSubscriber(); int x = 0, y = 0; +#pragma warning disable CS0618 var t1 = sub.SubscribeAsync(prefix + "abc", delegate { Interlocked.Increment(ref x); }); var t2 = sub.SubscribeAsync(prefix + "ab*", delegate { Interlocked.Increment(ref y); }); await Task.WhenAll(t1, t2).ForAwait(); @@ -746,6 +783,7 @@ public async Task TestSubscribeUnsubscribeAndSubscribeAgain() t2 = sub.SubscribeAsync(prefix + "ab*", delegate { Interlocked.Increment(ref y); }); await Task.WhenAll(t1, t2).ForAwait(); pub.Publish(prefix + "abc", ""); +#pragma warning restore CS0618 await AllowReasonableTimeToPublishAndProcess().ForAwait(); Assert.Equal(2, Volatile.Read(ref x)); Assert.Equal(2, Volatile.Read(ref y)); @@ -776,7 +814,7 @@ public async Task AzureRedisEventsAutomaticSubscribe() }; var pubSub = connection.GetSubscriber(); - await pubSub.PublishAsync("AzureRedisEvents", "HI"); + await pubSub.PublishAsync(RedisChannel.Literal("AzureRedisEvents"), "HI"); await Task.Delay(100); Assert.True(didUpdate); diff --git a/tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs b/tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs index 0afdf03ec..1e4d8c28e 100644 --- a/tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs +++ b/tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs @@ -21,7 +21,9 @@ public async Task ManagedPrimaryConnectionEndToEndWithFailoverTest() conn.ConfigurationChanged += (s, e) => Log($"Configuration changed: {e.EndPoint}"); var sub = conn.GetSubscriber(); +#pragma warning disable CS0618 sub.Subscribe("*", (channel, message) => Log($"Sub: {channel}, message:{message}")); +#pragma warning restore CS0618 var db = conn.GetDatabase(); await db.PingAsync(); diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 6738bf490..4ba21d4f5 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -377,7 +377,7 @@ public static ConnectionMultiplexer CreateDefault( syncTimeout = int.MaxValue; } - if (channelPrefix != null) config.ChannelPrefix = channelPrefix; + if (channelPrefix != null) config.ChannelPrefix = RedisChannel.Literal(channelPrefix); if (tieBreaker != null) config.TieBreaker = tieBreaker; if (password != null) config.Password = string.IsNullOrEmpty(password) ? null : password; if (clientName != null) config.ClientName = clientName; From 309d2760cd111ec0c5ac6ae1031a3ba461ee4328 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 13 Jun 2023 16:27:51 +0100 Subject: [PATCH 232/435] naming only: IsPatternBased -> IsPattern (#2482) (also fix some remaining debug tests) --- docs/ReleaseNotes.md | 2 +- .../KeyspaceIsolation/KeyPrefixed.cs | 2 +- .../PublicAPI/PublicAPI.Shipped.txt | 2 +- src/StackExchange.Redis/RedisChannel.cs | 2 +- tests/StackExchange.Redis.Tests/ChannelTests.cs | 16 ++++++++-------- tests/StackExchange.Redis.Tests/FailoverTests.cs | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 70b49d1b6..9b79e7b1e 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -- Fix [#2479](https://github.com/StackExchange/StackExchange.Redis/issues/2479): Add `RedisChannel.UseImplicitAutoPattern` (global) and `RedisChannel.IsPatternBased` ([#2480 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2480)) +- Fix [#2479](https://github.com/StackExchange/StackExchange.Redis/issues/2479): Add `RedisChannel.UseImplicitAutoPattern` (global) and `RedisChannel.IsPattern` ([#2480 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2480)) - Fix [#2479](https://github.com/StackExchange/StackExchange.Redis/issues/2479): Mark `RedisChannel` conversion operators as obsolete; add `RedisChannel.Literal` and `RedisChannel.Pattern` helpers ([#2481 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2481)) - Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Update `Pipelines.Sockets.Unofficial` to `v2.2.8` to support native AOT ([#2456 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2456)) diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 3bad77d43..e34cad895 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -844,7 +844,7 @@ protected RedisValue SortGetToInner(RedisValue outer) => protected RedisChannel ToInner(RedisChannel outer) { var combined = RedisKey.ConcatenateBytes(Prefix, null, (byte[]?)outer); - return new RedisChannel(combined, outer.IsPatternBased ? RedisChannel.PatternMode.Pattern : RedisChannel.PatternMode.Literal); + return new RedisChannel(combined, outer.IsPattern ? RedisChannel.PatternMode.Pattern : RedisChannel.PatternMode.Literal); } private Func? mapFunction; diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 464af8023..50ac23e75 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1259,7 +1259,7 @@ StackExchange.Redis.Proxy.Twemproxy = 1 -> StackExchange.Redis.Proxy StackExchange.Redis.RedisChannel StackExchange.Redis.RedisChannel.Equals(StackExchange.Redis.RedisChannel other) -> bool StackExchange.Redis.RedisChannel.IsNullOrEmpty.get -> bool -StackExchange.Redis.RedisChannel.IsPatternBased.get -> bool +StackExchange.Redis.RedisChannel.IsPattern.get -> bool StackExchange.Redis.RedisChannel.PatternMode StackExchange.Redis.RedisChannel.PatternMode.Auto = 0 -> StackExchange.Redis.RedisChannel.PatternMode StackExchange.Redis.RedisChannel.PatternMode.Literal = 1 -> StackExchange.Redis.RedisChannel.PatternMode diff --git a/src/StackExchange.Redis/RedisChannel.cs b/src/StackExchange.Redis/RedisChannel.cs index 16d4e7107..5e9446506 100644 --- a/src/StackExchange.Redis/RedisChannel.cs +++ b/src/StackExchange.Redis/RedisChannel.cs @@ -19,7 +19,7 @@ namespace StackExchange.Redis /// /// Indicates whether this channel represents a wildcard pattern (see PSUBSCRIBE) /// - public bool IsPatternBased => _isPatternBased; + public bool IsPattern => _isPatternBased; internal bool IsNull => Value == null; diff --git a/tests/StackExchange.Redis.Tests/ChannelTests.cs b/tests/StackExchange.Redis.Tests/ChannelTests.cs index 3f11d2ef1..4843090f4 100644 --- a/tests/StackExchange.Redis.Tests/ChannelTests.cs +++ b/tests/StackExchange.Redis.Tests/ChannelTests.cs @@ -25,7 +25,7 @@ public void ValidateAutoPatternModeString(string name, bool useImplicitAutoPatte #pragma warning disable CS0618 // we need to test the operator RedisChannel channel = name; #pragma warning restore CS0618 - Assert.Equal(isPatternBased, channel.IsPatternBased); + Assert.Equal(isPatternBased, channel.IsPattern); } finally { @@ -53,7 +53,7 @@ public void ValidateModeSpecifiedIgnoresGlobalSetting(string name, RedisChannel. { RedisChannel.UseImplicitAutoPattern = useImplicitAutoPattern; RedisChannel channel = new(name, mode); - Assert.Equal(isPatternBased, channel.IsPatternBased); + Assert.Equal(isPatternBased, channel.IsPattern); } finally { @@ -76,7 +76,7 @@ public void ValidateAutoPatternModeBytes(string name, bool useImplicitAutoPatter #pragma warning disable CS0618 // we need to test the operator RedisChannel channel = bytes; #pragma warning restore CS0618 - Assert.Equal(isPatternBased, channel.IsPatternBased); + Assert.Equal(isPatternBased, channel.IsPattern); } finally { @@ -105,7 +105,7 @@ public void ValidateModeSpecifiedIgnoresGlobalSettingBytes(string name, RedisCha { RedisChannel.UseImplicitAutoPattern = useImplicitAutoPattern; RedisChannel channel = new(bytes, mode); - Assert.Equal(isPatternBased, channel.IsPatternBased); + Assert.Equal(isPatternBased, channel.IsPattern); } finally { @@ -128,21 +128,21 @@ public void ValidateLiteralPatternMode(string name, bool useImplicitAutoPattern) // literal, string channel = RedisChannel.Literal(name); - Assert.False(channel.IsPatternBased); + Assert.False(channel.IsPattern); // pattern, string channel = RedisChannel.Pattern(name); - Assert.True(channel.IsPatternBased); + Assert.True(channel.IsPattern); var bytes = Encoding.UTF8.GetBytes(name); // literal, byte[] channel = RedisChannel.Literal(bytes); - Assert.False(channel.IsPatternBased); + Assert.False(channel.IsPattern); // pattern, byte[] channel = RedisChannel.Pattern(bytes); - Assert.True(channel.IsPatternBased); + Assert.True(channel.IsPattern); } finally { diff --git a/tests/StackExchange.Redis.Tests/FailoverTests.cs b/tests/StackExchange.Redis.Tests/FailoverTests.cs index 53c7e74f4..e9ff75d9a 100644 --- a/tests/StackExchange.Redis.Tests/FailoverTests.cs +++ b/tests/StackExchange.Redis.Tests/FailoverTests.cs @@ -203,7 +203,7 @@ public async Task SubscriptionsSurviveConnectionFailureAsync() using var conn = (Create(allowAdmin: true, shared: false, log: Writer, syncTimeout: 1000) as ConnectionMultiplexer)!; var profiler = conn.AddProfiler(); - RedisChannel channel = Me(); + RedisChannel channel = RedisChannel.Literal(Me()); var sub = conn.GetSubscriber(); int counter = 0; Assert.True(sub.IsConnected()); @@ -308,7 +308,7 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() using var aConn = Create(allowAdmin: true, shared: false); using var bConn = Create(allowAdmin: true, shared: false); - RedisChannel channel = Me(); + RedisChannel channel = RedisChannel.Literal(Me()); Log("Using Channel: " + channel); var subA = aConn.GetSubscriber(); var subB = bConn.GetSubscriber(); From a1ccdc2a0f1d3435bbfcc60a125f0507cc31ccd0 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 13 Jun 2023 16:46:02 +0100 Subject: [PATCH 233/435] fix version in release notes --- docs/ReleaseNotes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 9b79e7b1e..51c76f7a1 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,8 @@ Current package versions: ## Unreleased +## 2.6.116 + - Fix [#2479](https://github.com/StackExchange/StackExchange.Redis/issues/2479): Add `RedisChannel.UseImplicitAutoPattern` (global) and `RedisChannel.IsPattern` ([#2480 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2480)) - Fix [#2479](https://github.com/StackExchange/StackExchange.Redis/issues/2479): Mark `RedisChannel` conversion operators as obsolete; add `RedisChannel.Literal` and `RedisChannel.Pattern` helpers ([#2481 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2481)) - Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Update `Pipelines.Sockets.Unofficial` to `v2.2.8` to support native AOT ([#2456 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2456)) From f6171a19a0be078c6528b4631d42dfa4adcc8564 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 14 Jun 2023 10:08:24 -0400 Subject: [PATCH 234/435] Redis 7.2.0 RC1: Prep for upgrades (#2454) I'm seeing a few issues here that we need to resolve for the upcoming Redis 7.2 release: - [x] The internal encoding has changed 1 spot to listpack, which is correct from https://github.com/redis/redis/pull/11303, but is missing in the docs at https://redis.io/commands/object-encoding/ (fixed in tests themselves) - [x] The `HackyGetPerf` reliably returns 0 now, regardless of how long has passed (e.g. upping iterations tremendously)...this may legit be bugged. - [x] `StreamAutoClaim_IncludesDeletedMessageId` expectations are broken, not sure what to make of this yet but it's an odd change to hit between 7.0 and 7.2 versions. Note: no release notes because these are all test tweaks. Co-authored-by: slorello89 --- tests/RedisConfigs/Dockerfile | 2 +- tests/StackExchange.Redis.Tests/KeyTests.cs | 2 +- .../ScriptingTests.cs | 21 ------------------- .../StackExchange.Redis.Tests/StreamTests.cs | 4 ++-- 4 files changed, 4 insertions(+), 25 deletions(-) diff --git a/tests/RedisConfigs/Dockerfile b/tests/RedisConfigs/Dockerfile index 3517b4de2..32cbe0663 100644 --- a/tests/RedisConfigs/Dockerfile +++ b/tests/RedisConfigs/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:7.0-rc3 +FROM redis:7.2-rc1 COPY Basic /data/Basic/ COPY Failover /data/Failover/ diff --git a/tests/StackExchange.Redis.Tests/KeyTests.cs b/tests/StackExchange.Redis.Tests/KeyTests.cs index d0846872d..062f2caaa 100644 --- a/tests/StackExchange.Redis.Tests/KeyTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyTests.cs @@ -272,7 +272,7 @@ public async Task KeyEncoding() db.ListLeftPush(key, "new value", flags: CommandFlags.FireAndForget); // Depending on server version, this is going to vary - we're sanity checking here. - var listTypes = new [] { "ziplist", "quicklist" }; + var listTypes = new [] { "ziplist", "quicklist", "listpack" }; Assert.Contains(db.KeyEncoding(key), listTypes); Assert.Contains(await db.KeyEncodingAsync(key), listTypes); diff --git a/tests/StackExchange.Redis.Tests/ScriptingTests.cs b/tests/StackExchange.Redis.Tests/ScriptingTests.cs index ad290259e..00919d0c0 100644 --- a/tests/StackExchange.Redis.Tests/ScriptingTests.cs +++ b/tests/StackExchange.Redis.Tests/ScriptingTests.cs @@ -97,27 +97,6 @@ public async Task TestRandomThingFromForum() Assert.Equal("4", vals[2]); } - [Fact] - public void HackyGetPerf() - { - using var conn = GetScriptConn(); - - var key = Me(); - var db = conn.GetDatabase(); - db.StringSet(key + "foo", "bar", flags: CommandFlags.FireAndForget); - var result = (long)db.ScriptEvaluate(@" -redis.call('psetex', KEYS[1], 60000, 'timing') -for i = 1,5000 do - redis.call('set', 'ignore','abc') -end -local timeTaken = 60000 - redis.call('pttl', KEYS[1]) -redis.call('del', KEYS[1]) -return timeTaken -", new RedisKey[] { key }, null); - Log(result.ToString()); - Assert.True(result > 0); - } - [Fact] public async Task MultiIncrWithoutReplies() { diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index 36856de38..b6568d525 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -290,7 +290,7 @@ public void StreamAutoClaim_IncludesDeletedMessageId() db.StreamDelete(key, new RedisValue[] { messageIds[0] }); // Claim a single pending message and reassign it to consumer2. - var result = db.StreamAutoClaim(key, group, consumer2, 0, "0-0", count: 1); + var result = db.StreamAutoClaim(key, group, consumer2, 0, "0-0", count: 2); Assert.Equal("0-0", result.NextStartId); Assert.NotEmpty(result.ClaimedEntries); @@ -318,7 +318,7 @@ public async Task StreamAutoClaim_IncludesDeletedMessageIdAsync() db.StreamDelete(key, new RedisValue[] { messageIds[0] }); // Claim a single pending message and reassign it to consumer2. - var result = await db.StreamAutoClaimAsync(key, group, consumer2, 0, "0-0", count: 1); + var result = await db.StreamAutoClaimAsync(key, group, consumer2, 0, "0-0", count: 2); Assert.Equal("0-0", result.NextStartId); Assert.NotEmpty(result.ClaimedEntries); From 5e618dc07a47f9a154709222b22eb2beac8cb642 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Wed, 28 Jun 2023 17:36:06 -0500 Subject: [PATCH 235/435] Fix nullability annotation of IConnectionMultiplexer.RegisterProfiler (#2494) RegisterProfiler allows for a null ProfilingSession to be returned - it is even documented with "or returning null to not profile". Fixing the nullable annotation to indicate that null is an acceptable result of the profilingSessionProvider function. --- docs/ReleaseNotes.md | 2 ++ src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs | 4 ++-- src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs | 2 +- src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt | 4 ++-- .../Helpers/SharedConnectionFixture.cs | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 51c76f7a1..2bfa07809 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,8 @@ Current package versions: ## Unreleased +- Fix: Fix nullability annotation of IConnectionMultiplexer.RegisterProfiler ([#2494 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2494)) + ## 2.6.116 - Fix [#2479](https://github.com/StackExchange/StackExchange.Redis/issues/2479): Add `RedisChannel.UseImplicitAutoPattern` (global) and `RedisChannel.IsPattern` ([#2480 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2480)) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs index e50498f13..b6ecbdf3f 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis; public partial class ConnectionMultiplexer { - private Func? _profilingSessionProvider; + private Func? _profilingSessionProvider; /// /// Register a callback to provide an on-demand ambient session provider based on the @@ -13,5 +13,5 @@ public partial class ConnectionMultiplexer /// based on ambient context, or returning null to not profile /// /// The session provider to register. - public void RegisterProfiler(Func profilingSessionProvider) => _profilingSessionProvider = profilingSessionProvider; + public void RegisterProfiler(Func profilingSessionProvider) => _profilingSessionProvider = profilingSessionProvider; } diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index 583d621eb..017dc84d2 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -78,7 +78,7 @@ public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable /// based on ambient context, or returning null to not profile. /// /// The profiling session provider. - void RegisterProfiler(Func profilingSessionProvider); + void RegisterProfiler(Func profilingSessionProvider); /// /// Get summary statistics associates with this server. diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 50ac23e75..a55d11cc5 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -359,7 +359,7 @@ StackExchange.Redis.ConnectionMultiplexer.PreserveAsyncOrder.set -> void StackExchange.Redis.ConnectionMultiplexer.PublishReconfigure(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.ConnectionMultiplexer.PublishReconfigureAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.ConnectionMultiplexer.ReconfigureAsync(string! reason) -> System.Threading.Tasks.Task! -StackExchange.Redis.ConnectionMultiplexer.RegisterProfiler(System.Func! profilingSessionProvider) -> void +StackExchange.Redis.ConnectionMultiplexer.RegisterProfiler(System.Func! profilingSessionProvider) -> void StackExchange.Redis.ConnectionMultiplexer.ResetStormLog() -> void StackExchange.Redis.ConnectionMultiplexer.ServerMaintenanceEvent -> System.EventHandler? StackExchange.Redis.ConnectionMultiplexer.StormLogThreshold.get -> int @@ -497,7 +497,7 @@ StackExchange.Redis.IConnectionMultiplexer.PreserveAsyncOrder.get -> bool StackExchange.Redis.IConnectionMultiplexer.PreserveAsyncOrder.set -> void StackExchange.Redis.IConnectionMultiplexer.PublishReconfigure(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IConnectionMultiplexer.PublishReconfigureAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IConnectionMultiplexer.RegisterProfiler(System.Func! profilingSessionProvider) -> void +StackExchange.Redis.IConnectionMultiplexer.RegisterProfiler(System.Func! profilingSessionProvider) -> void StackExchange.Redis.IConnectionMultiplexer.ResetStormLog() -> void StackExchange.Redis.IConnectionMultiplexer.ServerMaintenanceEvent -> System.EventHandler! StackExchange.Redis.IConnectionMultiplexer.StormLogThreshold.get -> int diff --git a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs index af1879542..7b2665014 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs @@ -169,7 +169,7 @@ public event EventHandler ServerMaintenanceEvent public Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None) => _inner.PublishReconfigureAsync(flags); - public void RegisterProfiler(Func profilingSessionProvider) => _inner.RegisterProfiler(profilingSessionProvider); + public void RegisterProfiler(Func profilingSessionProvider) => _inner.RegisterProfiler(profilingSessionProvider); public void ResetStormLog() => _inner.ResetStormLog(); From c0c71c326fa385429b20ce18e00f27c141ab50b6 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Sat, 1 Jul 2023 09:21:31 -0500 Subject: [PATCH 236/435] Enable net6.0 build (#2497) This allows for using new APIs introduced in net6.0. In order to enable building for net6.0, we also need to revert the thread pool changes in #1939 and #1950. This was already effectively reverted in #1992 by not building for net6.0. Now that we are building for net6.0 again, these if-defs need to be removed. --- docs/ReleaseNotes.md | 1 + .../ConnectionMultiplexer.ReaderWriter.cs | 20 ++----------------- src/StackExchange.Redis/PhysicalBridge.cs | 5 ----- .../StackExchange.Redis.csproj | 5 ++--- .../StackExchange.Redis.Tests/ConfigTests.cs | 2 +- 5 files changed, 6 insertions(+), 27 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 2bfa07809..cf44a985d 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,7 @@ Current package versions: ## Unreleased +- Change: Target net6.0 instead of net5.0, since net5.0 is end of life. ([#2497 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2497)) - Fix: Fix nullability annotation of IConnectionMultiplexer.RegisterProfiler ([#2494 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2494)) ## 2.6.116 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs b/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs index b9ae3a15a..a30da7865 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.ReaderWriter.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; namespace StackExchange.Redis; @@ -9,27 +9,11 @@ public partial class ConnectionMultiplexer [MemberNotNull(nameof(SocketManager))] private void OnCreateReaderWriter(ConfigurationOptions configuration) { - SocketManager = configuration.SocketManager ?? GetDefaultSocketManager(); + SocketManager = configuration.SocketManager ?? SocketManager.Shared; } private void OnCloseReaderWriter() { SocketManager = null; } - - /// - /// .NET 6.0+ has changes to sync-over-async stalls in the .NET primary thread pool - /// If we're in that environment, by default remove the overhead of our own threadpool - /// This will eliminate some context-switching overhead and better-size threads on both large - /// and small environments, from 16 core machines to single core VMs where the default 10 threads - /// isn't an ideal situation. - /// - internal static SocketManager GetDefaultSocketManager() - { -#if NET6_0_OR_GREATER - return SocketManager.ThreadPool; -#else - return SocketManager.Shared; -#endif - } } diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index b42c40a19..f211cae61 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -855,10 +855,6 @@ private void StartBacklogProcessor() { _backlogStatus = BacklogStatus.Activating; -#if NET6_0_OR_GREATER - // In .NET 6, use the thread pool stall semantics to our advantage and use a lighter-weight Task - Task.Run(ProcessBacklogAsync); -#else // Start the backlog processor; this is a bit unorthodox, as you would *expect* this to just // be Task.Run; that would work fine when healthy, but when we're falling on our face, it is // easy to get into a thread-pool-starvation "spiral of death" if we rely on the thread-pool @@ -871,7 +867,6 @@ private void StartBacklogProcessor() Name = "StackExchange.Redis Backlog", // help anyone looking at thread-dumps }; thread.Start(this); -#endif } else { diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 4a8b28f92..b0e2a93ff 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -2,8 +2,7 @@ enable - - net461;netstandard2.0;net472;netcoreapp3.1;net5.0 + net461;netstandard2.0;net472;netcoreapp3.1;net6.0 High performance Redis client, incorporating both synchronous and asynchronous usage. StackExchange.Redis StackExchange.Redis @@ -35,7 +34,7 @@ - + diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index b11c968cf..f5a2764c7 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -468,7 +468,7 @@ public void DefaultThreadPoolManagerIsDetected() using var conn = ConnectionMultiplexer.Connect(config); - Assert.Same(ConnectionMultiplexer.GetDefaultSocketManager().Scheduler, conn.SocketManager?.Scheduler); + Assert.Same(SocketManager.Shared.Scheduler, conn.SocketManager?.Scheduler); } [Theory] From cd2dbb4e2bb4d7fa7332234c1e735f0a4fe53175 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 5 Jul 2023 08:36:09 -0400 Subject: [PATCH 237/435] Exceptions: include Timer.ActiveCount in .NET Core stats (#2500) We're seeing some instances of quite delayed timeouts and at least in two deeper investigations it was due to a huge number of timers firing and the backlog timeout assessment timer not triggering for an extended period of time. To help users diagnose this, adding the cheap counter to the .NET Core pool stats where `Timer.ActiveCount` is readily available. This is available in all the .NET (non-Framework) versions we support. Before: > ...POOL: (Threads=25,QueuedItems=0,CompletedItems=1066) After: > ...POOL: (Threads=25,QueuedItems=0,CompletedItems=1066,Timers=46) See #2477 for another possible instance of this. --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/PerfCounterHelper.cs | 2 +- .../StackExchange.Redis.Tests/AbortOnConnectFailTests.cs | 8 ++++---- tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs | 7 ++++++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index cf44a985d..99315cf2b 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: - Change: Target net6.0 instead of net5.0, since net5.0 is end of life. ([#2497 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2497)) - Fix: Fix nullability annotation of IConnectionMultiplexer.RegisterProfiler ([#2494 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2494)) +- Add: `Timer.ActiveCount` under `POOL` in timeout messages on .NET 6+ to help diagnose timer overload affecting timeout evaluations ([#2500 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2500)) ## 2.6.116 diff --git a/src/StackExchange.Redis/PerfCounterHelper.cs b/src/StackExchange.Redis/PerfCounterHelper.cs index bcc1b5b0a..8d8b6fbb0 100644 --- a/src/StackExchange.Redis/PerfCounterHelper.cs +++ b/src/StackExchange.Redis/PerfCounterHelper.cs @@ -23,7 +23,7 @@ internal static int GetThreadPoolStats(out string iocp, out string worker, out s worker = $"(Busy={busyWorkerThreads},Free={freeWorkerThreads},Min={minWorkerThreads},Max={maxWorkerThreads})"; #if NETCOREAPP - workItems = $"(Threads={ThreadPool.ThreadCount},QueuedItems={ThreadPool.PendingWorkItemCount},CompletedItems={ThreadPool.CompletedWorkItemCount})"; + workItems = $"(Threads={ThreadPool.ThreadCount},QueuedItems={ThreadPool.PendingWorkItemCount},CompletedItems={ThreadPool.CompletedWorkItemCount},Timers={Timer.ActiveCount})"; #else workItems = null; #endif diff --git a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs index 0ba8fca9e..3cccda4de 100644 --- a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs +++ b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs @@ -16,7 +16,7 @@ public void NeverEverConnectedNoBacklogThrowsConnectionNotAvailableSync() var db = conn.GetDatabase(); var key = Me(); - // No connection is active/available to service this operation: GET 6.0.14AbortOnConnectFailTests-NeverEverConnectedThrowsConnectionNotAvailable; UnableToConnect on doesnot.exist.0d034c26350e4ee199d6c5f385a073f7:6379/Interactive, Initializing/NotStarted, last: NONE, origin: BeginConnectAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, global: 0s ago, v: 2.6.99.22667, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=17,QueuedItems=0,CompletedItems=20), v: 2.6.99.22667 + // No connection is active/available to service this operation: GET 6.0.18AbortOnConnectFailTests-NeverEverConnectedNoBacklogThrowsConnectionNotAvailableSync; UnableToConnect on doesnot.exist.d4d1424806204b68b047954b1db3411d:6379/Interactive, Initializing/NotStarted, last: NONE, origin: BeginConnectAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 100s, state: Connecting, mgr: 4 of 10 available, last-heartbeat: never, global: 0s ago, v: 2.6.120.51136, mc: 1/1/0, mgr: 5 of 10 available, clientName: CRAVERTOP7(SE.Redis-v2.6.120.51136), IOCP: (Busy=0,Free=1000,Min=16,Max=1000), WORKER: (Busy=3,Free=32764,Min=16,Max=32767), POOL: (Threads=25,QueuedItems=0,CompletedItems=1066,Timers=46), v: 2.6.120.51136 var ex = Assert.Throws(() => db.StringGet(key)); Log("Exception: " + ex.Message); Assert.Contains("No connection is active/available to service this operation", ex.Message); @@ -29,7 +29,7 @@ public async Task NeverEverConnectedNoBacklogThrowsConnectionNotAvailableAsync() var db = conn.GetDatabase(); var key = Me(); - // No connection is active/available to service this operation: GET 6.0.14AbortOnConnectFailTests-NeverEverConnectedThrowsConnectionNotAvailable; UnableToConnect on doesnot.exist.0d034c26350e4ee199d6c5f385a073f7:6379/Interactive, Initializing/NotStarted, last: NONE, origin: BeginConnectAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, global: 0s ago, v: 2.6.99.22667, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=17,QueuedItems=0,CompletedItems=20), v: 2.6.99.22667 + // No connection is active/available to service this operation: GET 6.0.18AbortOnConnectFailTests-NeverEverConnectedNoBacklogThrowsConnectionNotAvailableSync; UnableToConnect on doesnot.exist.d4d1424806204b68b047954b1db3411d:6379/Interactive, Initializing/NotStarted, last: NONE, origin: BeginConnectAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 100s, state: Connecting, mgr: 4 of 10 available, last-heartbeat: never, global: 0s ago, v: 2.6.120.51136, mc: 1/1/0, mgr: 5 of 10 available, clientName: CRAVERTOP7(SE.Redis-v2.6.120.51136), IOCP: (Busy=0,Free=1000,Min=16,Max=1000), WORKER: (Busy=3,Free=32764,Min=16,Max=32767), POOL: (Threads=25,QueuedItems=0,CompletedItems=1066,Timers=46), v: 2.6.120.51136 var ex = await Assert.ThrowsAsync(() => db.StringGetAsync(key)); Log("Exception: " + ex.Message); Assert.Contains("No connection is active/available to service this operation", ex.Message); @@ -49,7 +49,7 @@ public void DisconnectAndReconnectThrowsConnectionExceptionSync() var server = conn.GetServerSnapshot()[0]; server.SimulateConnectionFailure(SimulatedFailureType.All); - // Exception: The message timed out in the backlog attempting to send because no connection became available (100ms) - Last Connection Exception: InternalFailure on 127.0.0.1:6379/Interactive, Initializing/NotStarted, last: GET, origin: ConnectedAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.99.22667, command=PING, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=18,QueuedItems=0,CompletedItems=65), v: 2.6.99.22667 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) + // Exception: The message timed out in the backlog attempting to send because no connection became available (100ms) - Last Connection Exception: SocketFailure (InputReaderCompleted, last-recv: 7) on 127.0.0.1:6379/Interactive, Idle/ReadAsync, last: PING, origin: SimulateConnectionFailure, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 100s, state: ConnectedEstablished, mgr: 10 of 10 available, in: 0, in-pipe: 0, out-pipe: 0, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.120.51136, command=PING, timeout: 100, inst: 13, qu: 1, qs: 0, aw: False, bw: Inactive, last-in: 0, cur-in: 0, sync-ops: 2, async-ops: 0, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: CRAVERTOP7(SE.Redis-v2.6.120.51136), IOCP: (Busy=0,Free=1000,Min=16,Max=1000), WORKER: (Busy=2,Free=32765,Min=16,Max=32767), POOL: (Threads=33,QueuedItems=0,CompletedItems=6237,Timers=39), v: 2.6.120.51136 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) var ex = Assert.ThrowsAny(() => db.Ping()); Log("Exception: " + ex.Message); Assert.True(ex is RedisConnectionException or RedisTimeoutException); @@ -73,7 +73,7 @@ public async Task DisconnectAndNoReconnectThrowsConnectionExceptionAsync() var server = conn.GetServerSnapshot()[0]; server.SimulateConnectionFailure(SimulatedFailureType.All); - // Exception: The message timed out in the backlog attempting to send because no connection became available (100ms) - Last Connection Exception: InternalFailure on 127.0.0.1:6379/Interactive, Initializing/NotStarted, last: GET, origin: ConnectedAsync, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 500s, state: Connecting, mgr: 10 of 10 available, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.99.22667, command=PING, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: NAMISTOU-3(SE.Redis-v2.6.99.22667), IOCP: (Busy=0,Free=1000,Min=32,Max=1000), WORKER: (Busy=2,Free=32765,Min=32,Max=32767), POOL: (Threads=18,QueuedItems=0,CompletedItems=65), v: 2.6.99.22667 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) + // Exception: The message timed out in the backlog attempting to send because no connection became available (100ms) - Last Connection Exception: SocketFailure (InputReaderCompleted, last-recv: 7) on 127.0.0.1:6379/Interactive, Idle/ReadAsync, last: PING, origin: SimulateConnectionFailure, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 100s, state: ConnectedEstablished, mgr: 8 of 10 available, in: 0, in-pipe: 0, out-pipe: 0, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.120.51136, command=PING, timeout: 100, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 8 of 10 available, clientName: CRAVERTOP7(SE.Redis-v2.6.120.51136), IOCP: (Busy=0,Free=1000,Min=16,Max=1000), WORKER: (Busy=6,Free=32761,Min=16,Max=32767), POOL: (Threads=33,QueuedItems=0,CompletedItems=5547,Timers=60), v: 2.6.120.51136 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) var ex = await Assert.ThrowsAsync(() => db.PingAsync()); Log("Exception: " + ex.Message); Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available (100ms) - Last Connection Exception: ", ex.Message); diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 42f444418..02309ec79 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -126,7 +126,12 @@ public void TimeoutException() Assert.Contains("conn-sec: n/a", ex.Message); Assert.Contains("aoc: 1", ex.Message); #if NETCOREAPP - Assert.Contains("POOL: ", ex.Message); + // ...POOL: (Threads=33,QueuedItems=0,CompletedItems=5547,Timers=60)... + Assert.Contains("POOL: ", ex.Message); + Assert.Contains("Threads=", ex.Message); + Assert.Contains("QueuedItems=", ex.Message); + Assert.Contains("CompletedItems=", ex.Message); + Assert.Contains("Timers=", ex.Message); #endif Assert.DoesNotContain("Unspecified/", ex.Message); Assert.EndsWith(" (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)", ex.Message); From ce9506b74774cc083a10373372ad2a1362bd4be5 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 7 Jul 2023 14:36:02 +0100 Subject: [PATCH 238/435] add LibraryName at ConfigurationOptions level (#2502) * add LibraryName at ConfigurationOptions level * add PR number * add GetProvider * remove `libname` config-string; move docs to code-only section * remove s_DefaultProvider * Update docs/ReleaseNotes.md --------- Co-authored-by: Nick Craver --- docs/Configuration.md | 4 +++- docs/ReleaseNotes.md | 2 ++ .../Configuration/AzureOptionsProvider.cs | 5 ++--- .../Configuration/DefaultOptionsProvider.cs | 21 ++++++++++++++++--- .../ConfigurationOptions.cs | 14 +++++++++++-- .../PublicAPI/PublicAPI.Shipped.txt | 4 ++++ src/StackExchange.Redis/ServerEndPoint.cs | 15 +++++++++---- .../DefaultOptionsTests.cs | 4 ++-- 8 files changed, 54 insertions(+), 15 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 90df67b9f..753abf83b 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -97,7 +97,7 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a | tiebreaker={string} | `TieBreaker` | `__Booksleeve_TieBreak` | Key to use for selecting a server in an ambiguous primary scenario | | version={string} | `DefaultVersion` | (`4.0` in Azure, else `2.0`) | Redis version level (useful when the server does not make this available) | | tunnel={string} | `Tunnel` | `null` | Tunnel for connections (use `http:{proxy url}` for "connect"-based proxy server) | -| setlib={bool} | `SetClientLibrary` | `true` | Whether to attempt to use `CLIENT SETINFO` to set the lib name/version on the connection | +| setlib={bool} | `SetClientLibrary` | `true` | Whether to attempt to use `CLIENT SETINFO` to set the library name/version on the connection | Additional code-only options: - ReconnectRetryPolicy (`IReconnectRetryPolicy`) - Default: `ReconnectRetryPolicy = ExponentialRetry(ConnectTimeout / 2);` @@ -115,6 +115,8 @@ Additional code-only options: - HeartbeatInterval - Default: `1000ms` - Allows running the heartbeat more often which importantly includes timeout evaluation for async commands. For example if you have a 50ms async command timeout, we're only actually checking it during the heartbeat (once per second by default), so it's possible 50-1050ms pass _before we notice it timed out_. If you want more fidelity in that check and to observe that a server failed faster, you can lower this to run the heartbeat more often to achieve that. - **Note: heartbeats are not free and that's why the default is 1 second. There is additional overhead to running this more often simply because it does some work each time it fires.** +- LibraryName - Default: `SE.Redis` (unless a `DefaultOptionsProvider` specifies otherwise) + - The library name to use with `CLIENT SETINFO` when setting the library name/version on the connection Tokens in the configuration string are comma-separated; any without an `=` sign are assumed to be redis server endpoints. Endpoints without an explicit port will use 6379 if ssl is not enabled, and 6380 if ssl is enabled. Tokens starting with `$` are taken to represent command maps, for example: `$config=cfg`. diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 99315cf2b..91285e821 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -11,6 +11,8 @@ Current package versions: - Change: Target net6.0 instead of net5.0, since net5.0 is end of life. ([#2497 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2497)) - Fix: Fix nullability annotation of IConnectionMultiplexer.RegisterProfiler ([#2494 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2494)) - Add: `Timer.ActiveCount` under `POOL` in timeout messages on .NET 6+ to help diagnose timer overload affecting timeout evaluations ([#2500 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2500)) +- Add: `LibraryName` configuration option; allows the library name to be controlled at the individual options level (in addition to the existing controls in `DefaultOptionsProvider`) ([#2502 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2502)) +- Add: `DefaultOptionsProvider.GetProvider` allows lookup of provider by endpoint ([#2502 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2502)) ## 2.6.116 diff --git a/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs b/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs index e4ccc92a1..e66b0b210 100644 --- a/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs @@ -1,8 +1,7 @@ -using System; -using System.Collections.Generic; +using StackExchange.Redis.Maintenance; +using System; using System.Net; using System.Threading.Tasks; -using StackExchange.Redis.Maintenance; namespace StackExchange.Redis.Configuration { diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index ced64c8be..8e6cf85b1 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -52,7 +52,7 @@ public static void AddProvider(DefaultOptionsProvider provider) /// /// Gets a provider for the given endpoints, falling back to if nothing more specific is found. /// - internal static Func GetForEndpoints { get; } = (endpoints) => + public static DefaultOptionsProvider GetProvider(EndPointCollection endpoints) { foreach (var provider in KnownProviders) { @@ -65,8 +65,23 @@ public static void AddProvider(DefaultOptionsProvider provider) } } - return new DefaultOptionsProvider(); - }; + return new DefaultOptionsProvider(); // no memoize; allow mutability concerns (also impacts subclasses, but: pragmatism) + } + + /// + /// Gets a provider for a given endpoints, falling back to if nothing more specific is found. + /// + public static DefaultOptionsProvider GetProvider(EndPoint endpoint) + { + foreach (var provider in KnownProviders) + { + if (provider.IsMatch(endpoint)) + { + return provider; + } + } + return new DefaultOptionsProvider(); // no memoize; allow mutability concerns (also impacts subclasses, but: pragmatism) + } /// /// Gets or sets whether connect/configuration timeouts should be explicitly notified via a TimeoutException. diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index da30beb56..19314c344 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -180,7 +180,7 @@ public static string TryNormalize(string value) /// public DefaultOptionsProvider Defaults { - get => defaultOptions ??= DefaultOptionsProvider.GetForEndpoints(EndPoints); + get => defaultOptions ??= DefaultOptionsProvider.GetProvider(EndPoints); set => defaultOptions = value; } @@ -233,7 +233,7 @@ public bool UseSsl } /// - /// Gets or sets whether the library should identify itself by library-name/version when possible + /// Gets or sets whether the library should identify itself by library-name/version when possible. /// public bool SetClientLibrary { @@ -241,6 +241,15 @@ public bool SetClientLibrary set => setClientLibrary = value; } + + /// + /// Gets or sets the library name to use for CLIENT SETINFO lib-name calls to Redis during handshake. + /// Defaults to "SE.Redis". + /// + /// If the value is null, empty or whitespace, then the value from the options-provideer is used; + /// to disable the library name feature, use instead. + public string? LibraryName { get; set; } + /// /// Automatically encodes and decodes channels. /// @@ -671,6 +680,7 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow #endif Tunnel = Tunnel, setClientLibrary = setClientLibrary, + LibraryName = LibraryName, }; /// diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index a55d11cc5..84d4ce032 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -243,6 +243,8 @@ StackExchange.Redis.ConfigurationOptions.IncludePerformanceCountersInExceptions. StackExchange.Redis.ConfigurationOptions.IncludePerformanceCountersInExceptions.set -> void StackExchange.Redis.ConfigurationOptions.KeepAlive.get -> int StackExchange.Redis.ConfigurationOptions.KeepAlive.set -> void +StackExchange.Redis.ConfigurationOptions.LibraryName.get -> string? +StackExchange.Redis.ConfigurationOptions.LibraryName.set -> void StackExchange.Redis.ConfigurationOptions.Password.get -> string? StackExchange.Redis.ConfigurationOptions.Password.set -> void StackExchange.Redis.ConfigurationOptions.PreserveAsyncOrder.get -> bool @@ -1604,6 +1606,8 @@ static StackExchange.Redis.Condition.StringLengthLessThan(StackExchange.Redis.Re static StackExchange.Redis.Condition.StringNotEqual(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value) -> StackExchange.Redis.Condition! static StackExchange.Redis.Configuration.DefaultOptionsProvider.AddProvider(StackExchange.Redis.Configuration.DefaultOptionsProvider! provider) -> void static StackExchange.Redis.Configuration.DefaultOptionsProvider.ComputerName.get -> string! +static StackExchange.Redis.Configuration.DefaultOptionsProvider.GetProvider(StackExchange.Redis.EndPointCollection! endpoints) -> StackExchange.Redis.Configuration.DefaultOptionsProvider! +static StackExchange.Redis.Configuration.DefaultOptionsProvider.GetProvider(System.Net.EndPoint! endpoint) -> StackExchange.Redis.Configuration.DefaultOptionsProvider! static StackExchange.Redis.Configuration.DefaultOptionsProvider.LibraryVersion.get -> string! static StackExchange.Redis.ConfigurationOptions.Parse(string! configuration) -> StackExchange.Redis.ConfigurationOptions! static StackExchange.Redis.ConfigurationOptions.Parse(string! configuration, bool ignoreUnknown) -> StackExchange.Redis.ConfigurationOptions! diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index d3082e35c..aae13234b 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -898,8 +898,9 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) } Message msg; // Note that we need "" (not null) for password in the case of 'nopass' logins - string? user = Multiplexer.RawConfig.User; - string password = Multiplexer.RawConfig.Password ?? ""; + var config = Multiplexer.RawConfig; + string? user = config.User; + string password = config.Password ?? ""; if (!string.IsNullOrWhiteSpace(user)) { log?.WriteLine($"{Format.ToString(this)}: Authenticating (user/password)"); @@ -929,13 +930,19 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } } - if (Multiplexer.RawConfig.SetClientLibrary) + if (config.SetClientLibrary) { // note that this is a relatively new feature, but usually we won't know the // server version, so we will use this speculatively and hope for the best log?.WriteLine($"{Format.ToString(this)}: Setting client lib/ver"); - var libName = Multiplexer.RawConfig.Defaults.LibraryName; + var libName = config.LibraryName; + if (string.IsNullOrWhiteSpace(libName)) + { + // defer to provider if missing (note re null vs blank; if caller wants to disable + // it, they should set SetClientLibrary to false, not set the name to empty string) + libName = config.Defaults.LibraryName; + } if (!string.IsNullOrWhiteSpace(libName)) { msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, diff --git a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs index e59926379..412bb8da5 100644 --- a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs +++ b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs @@ -52,11 +52,11 @@ public void IsMatchOnDomain() DefaultOptionsProvider.AddProvider(new TestOptionsProvider(".testdomain")); var epc = new EndPointCollection(new List() { new DnsEndPoint("local.testdomain", 0) }); - var provider = DefaultOptionsProvider.GetForEndpoints(epc); + var provider = DefaultOptionsProvider.GetProvider(epc); Assert.IsType(provider); epc = new EndPointCollection(new List() { new DnsEndPoint("local.nottestdomain", 0) }); - provider = DefaultOptionsProvider.GetForEndpoints(epc); + provider = DefaultOptionsProvider.GetProvider(epc); Assert.IsType(provider); } From 40c2918a1b52078b49c75eb2c93cf4b5c2518d06 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 7 Jul 2023 17:07:48 +0100 Subject: [PATCH 239/435] release notes for 2.6.122 --- docs/ReleaseNotes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 91285e821..87f3d222a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,10 @@ Current package versions: ## Unreleased +(none) + +## 2.6.122 + - Change: Target net6.0 instead of net5.0, since net5.0 is end of life. ([#2497 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2497)) - Fix: Fix nullability annotation of IConnectionMultiplexer.RegisterProfiler ([#2494 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2494)) - Add: `Timer.ActiveCount` under `POOL` in timeout messages on .NET 6+ to help diagnose timer overload affecting timeout evaluations ([#2500 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2500)) From df805be6ccb977c4b5fc5dc9a60174219c9564b5 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 12 Jul 2023 14:49:41 +0100 Subject: [PATCH 240/435] fix #2507 - support multi-item MESSAGE broadcasts - implement CLIENT ID tracking (only on internal API for now, pending design) --- .../ConnectionMultiplexer.cs | 3 ++ .../Interfaces/IConnectionMultiplexer.cs | 9 ++-- src/StackExchange.Redis/PhysicalBridge.cs | 2 + src/StackExchange.Redis/PhysicalConnection.cs | 44 ++++++++++++++++--- src/StackExchange.Redis/RedisSubscriber.cs | 19 ++++++++ src/StackExchange.Redis/ResultProcessor.cs | 28 ++++++++++-- src/StackExchange.Redis/ServerEndPoint.cs | 3 ++ .../Helpers/SharedConnectionFixture.cs | 2 + .../Issues/Issue2507.cs | 39 ++++++++++++++++ 9 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/Issues/Issue2507.cs diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 88c6c37c2..68d58253a 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -2338,5 +2338,8 @@ private Task[] QuitAllServers() } return quits; } + + long? IInternalConnectionMultiplexer.GetConnectionId(EndPoint endpoint, ConnectionType type) + => GetServerEndPoint(endpoint)?.GetBridge(type)?.ConnectionId; } } diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index 017dc84d2..58973df68 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -1,10 +1,9 @@ -using System; -using System.Collections.Generic; +using StackExchange.Redis.Maintenance; +using StackExchange.Redis.Profiling; +using System; using System.IO; using System.Net; using System.Threading.Tasks; -using StackExchange.Redis.Maintenance; -using StackExchange.Redis.Profiling; namespace StackExchange.Redis { @@ -17,6 +16,8 @@ internal interface IInternalConnectionMultiplexer : IConnectionMultiplexer ReadOnlySpan GetServerSnapshot(); ConfigurationOptions RawConfig { get; } + + long? GetConnectionId(EndPoint endPoint, ConnectionType type); } /// diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index f211cae61..68ea70105 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -59,6 +59,8 @@ internal sealed class PhysicalBridge : IDisposable private volatile int state = (int)State.Disconnected; + internal long? ConnectionId => physical?.ConnectionId; + #if NETCOREAPP private readonly SemaphoreSlim _singleWriterMutex = new(1,1); #else diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 3c43002a3..595371529 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -55,6 +55,7 @@ private static readonly Message private long bytesLastResult; private long bytesInBuffer; + internal long? ConnectionId { get; set; } internal void GetBytes(out long sent, out long received) { @@ -1581,10 +1582,19 @@ private void MatchResult(in RawResult result) // invoke the handlers var channel = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.PatternMode.Literal); Trace("MESSAGE: " + channel); - if (!channel.IsNull && TryGetPubSubPayload(items[2], out var payload)) + if (!channel.IsNull) { - _readStatus = ReadStatus.InvokePubSub; - muxer.OnMessage(channel, channel, payload); + if (TryGetPubSubPayload(items[2], out var payload)) + { + _readStatus = ReadStatus.InvokePubSub; + muxer.OnMessage(channel, channel, payload); + } + // could be multi-message: https://github.com/StackExchange/StackExchange.Redis/issues/2507 + else if (TryGetMultiPubSubPayload(items[2], out var payloads)) + { + _readStatus = ReadStatus.InvokePubSub; + muxer.OnMessage(channel, channel, payloads); + } } return; // AND STOP PROCESSING! } @@ -1594,11 +1604,20 @@ private void MatchResult(in RawResult result) var channel = items[2].AsRedisChannel(ChannelPrefix, RedisChannel.PatternMode.Literal); Trace("PMESSAGE: " + channel); - if (!channel.IsNull && TryGetPubSubPayload(items[3], out var payload)) + if (!channel.IsNull) { - var sub = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.PatternMode.Pattern); - _readStatus = ReadStatus.InvokePubSub; - muxer.OnMessage(sub, channel, payload); + if (TryGetPubSubPayload(items[3], out var payload)) + { + var sub = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.PatternMode.Pattern); + _readStatus = ReadStatus.InvokePubSub; + muxer.OnMessage(sub, channel, payload); + } + else if (TryGetMultiPubSubPayload(items[3], out var payloads)) + { + var sub = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.PatternMode.Pattern); + _readStatus = ReadStatus.InvokePubSub; + muxer.OnMessage(sub, channel, payloads); + } } return; // AND STOP PROCESSING! } @@ -1647,6 +1666,17 @@ static bool TryGetPubSubPayload(in RawResult value, out RedisValue parsed, bool parsed = default; return false; } + + static bool TryGetMultiPubSubPayload(in RawResult value, out Sequence parsed) + { + if (value.Type == ResultType.MultiBulk && value.ItemsCount != 0) + { + parsed = value.GetItems(); + return true; + } + parsed = default; + return false; + } } private volatile Message? _activeMessage; diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index 39b99bfe2..5a24a716e 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Pipelines.Sockets.Unofficial; +using Pipelines.Sockets.Unofficial.Arenas; using static StackExchange.Redis.ConnectionMultiplexer; namespace StackExchange.Redis @@ -92,6 +93,24 @@ internal void OnMessage(in RedisChannel subscription, in RedisChannel channel, i } } + internal void OnMessage(in RedisChannel subscription, in RedisChannel channel, Sequence payload) + { + if (payload.IsSingleSegment) + { + foreach (var message in payload.FirstSpan) + { + OnMessage(subscription, channel, message.AsRedisValue()); + } + } + else + { + foreach (var message in payload) + { + OnMessage(subscription, channel, message.AsRedisValue()); + } + } + } + /// /// Updates all subscriptions re-evaluating their state. /// This clears the current server if it's not connected, prepping them to reconnect. diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 80f3fb805..5b43401e4 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -56,7 +56,8 @@ public static readonly MultiStreamProcessor public static readonly ResultProcessor Int64 = new Int64Processor(), PubSubNumSub = new PubSubNumSubProcessor(), - Int64DefaultNegativeOne = new Int64DefaultValueProcessor(-1); + Int64DefaultNegativeOne = new Int64DefaultValueProcessor(-1), + ClientId = new ClientIdProcessor(); public static readonly ResultProcessor NullableDouble = new NullableDoubleProcessor(); @@ -960,8 +961,7 @@ private sealed class ClusterNodesProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Type) + { + case ResultType.Integer: + case ResultType.SimpleString: + case ResultType.BulkString: + long i64; + if (result.TryGetInt64(out i64)) + { + SetResult(message, i64); + connection.ConnectionId = i64; + return true; + } + break; + } + return false; + } + } + private class PubSubNumSubProcessor : Int64Processor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index aae13234b..e661d8953 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -960,6 +960,9 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } } + msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, RedisLiterals.ID); + msg.SetInternalCall(); + await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.ClientId).ForAwait(); } var bridge = connection.BridgeCouldBeNull; diff --git a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs index 7b2665014..9c557911d 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs @@ -183,6 +183,8 @@ public void ExportConfiguration(Stream destination, ExportOptions options = Expo => _inner.ExportConfiguration(destination, options); public override string ToString() => _inner.ToString(); + long? IInternalConnectionMultiplexer.GetConnectionId(EndPoint endPoint, ConnectionType type) + => _inner.GetConnectionId(endPoint, type); } public void Dispose() diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs new file mode 100644 index 000000000..723623bdb --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests.Issues +{ + public class Issue2507 : TestBase + { + public Issue2507(ITestOutputHelper output, SharedConnectionFixture? fixture = null) + : base(output, fixture) { } + + [Fact] + public async Task Execute() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var pubsub = conn.GetSubscriber(); + var queue = await pubsub.SubscribeAsync(RedisChannel.Literal("__redis__:invalidate")); + await Task.Delay(100); + var connectionId = conn.GetConnectionId(conn.GetEndPoints().Single(), ConnectionType.Subscription); + if (connectionId is null) Skip.Inconclusive("Connection id not available"); + await db.StringSetAsync(new KeyValuePair[] { new("abc", "def"), new("ghi", "jkl"), new("mno", "pqr") }); + // this is not supported, but: we want it to at least not fail + await db.ExecuteAsync("CLIENT", "TRACKING", "on", "REDIRECT", connectionId!.Value, "BCAST"); + await db.KeyDeleteAsync(new RedisKey[] { "abc", "ghi", "mno" }); + await Task.Delay(100); + queue.Unsubscribe(); + Assert.True(queue.TryRead(out var message)); + Assert.Equal("abc", message.Message); + Assert.True(queue.TryRead(out message)); + Assert.Equal("ghi", message.Message); + Assert.True(queue.TryRead(out message)); + Assert.Equal("mno", message.Message); + Assert.False(queue.TryRead(out message)); + } + } +} From 8ce9e4d424e588adaae425328069081298ed97d5 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 12 Jul 2023 15:00:33 +0100 Subject: [PATCH 241/435] release notes (#2508) * release notes * mention connection-id tracking --- docs/ReleaseNotes.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 87f3d222a..8d3cc613b 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,8 @@ Current package versions: ## Unreleased -(none) +- Fix [#2507](https://github.com/StackExchange/StackExchange.Redis/issues/2507): Pub/sub with multi-item payloads should be usable ([#2508 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2508)) +- Add: connection-id tracking (internal only, no public API) ([#2508 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2508)) ## 2.6.122 From 7e9b90001e9ca648d771ec11c24a7392e7121ec9 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 28 Jul 2023 13:45:43 -0400 Subject: [PATCH 242/435] Test: Fix Issue2507 and add String.SetEmpty (#2514) Fixing the current tests stalling PRs. Issue2507 fundamentally can't play nice either others, especially shared connections and simultaneous, so fixing that case, and adding `SetEmpty`) which has come up a few times as an example of this working. --- .../Issues/Issue2507.cs | 19 +++++++++++++------ .../StackExchange.Redis.Tests/StringTests.cs | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs index 723623bdb..7d2a9b19a 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs @@ -6,6 +6,7 @@ namespace StackExchange.Redis.Tests.Issues { + [Collection(NonParallelCollection.Name)] public class Issue2507 : TestBase { public Issue2507(ITestOutputHelper output, SharedConnectionFixture? fixture = null) @@ -14,25 +15,31 @@ public Issue2507(ITestOutputHelper output, SharedConnectionFixture? fixture = nu [Fact] public async Task Execute() { - using var conn = Create(); + using var conn = Create(shared: false); var db = conn.GetDatabase(); var pubsub = conn.GetSubscriber(); var queue = await pubsub.SubscribeAsync(RedisChannel.Literal("__redis__:invalidate")); await Task.Delay(100); var connectionId = conn.GetConnectionId(conn.GetEndPoints().Single(), ConnectionType.Subscription); if (connectionId is null) Skip.Inconclusive("Connection id not available"); - await db.StringSetAsync(new KeyValuePair[] { new("abc", "def"), new("ghi", "jkl"), new("mno", "pqr") }); + + string baseKey = Me(); + RedisKey key1 = baseKey + "abc", + key2 = baseKey + "ghi", + key3 = baseKey + "mno"; + + await db.StringSetAsync(new KeyValuePair[] { new(key1, "def"), new(key2, "jkl"), new(key3, "pqr") }); // this is not supported, but: we want it to at least not fail await db.ExecuteAsync("CLIENT", "TRACKING", "on", "REDIRECT", connectionId!.Value, "BCAST"); - await db.KeyDeleteAsync(new RedisKey[] { "abc", "ghi", "mno" }); + await db.KeyDeleteAsync(new RedisKey[] { key1, key2, key3 }); await Task.Delay(100); queue.Unsubscribe(); Assert.True(queue.TryRead(out var message)); - Assert.Equal("abc", message.Message); + Assert.Equal(key1, message.Message); Assert.True(queue.TryRead(out message)); - Assert.Equal("ghi", message.Message); + Assert.Equal(key2, message.Message); Assert.True(queue.TryRead(out message)); - Assert.Equal("mno", message.Message); + Assert.Equal(key3, message.Message); Assert.False(queue.TryRead(out message)); } } diff --git a/tests/StackExchange.Redis.Tests/StringTests.cs b/tests/StackExchange.Redis.Tests/StringTests.cs index b82ce1d9c..9ba3c73bd 100644 --- a/tests/StackExchange.Redis.Tests/StringTests.cs +++ b/tests/StackExchange.Redis.Tests/StringTests.cs @@ -69,6 +69,24 @@ public async Task Set() Assert.Equal("def", Decode(await v2)); } + [Fact] + public async Task SetEmpty() + { + using var conn = Create(); + + var db = conn.GetDatabase(); + var key = Me(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.StringSet(key, new byte[] { }); + var exists = await db.KeyExistsAsync(key); + var val = await db.StringGetAsync(key); + + Assert.True(exists); + Log("Value: " + val); + Assert.Equal(0, val.Length()); + } + [Fact] public async Task StringGetSetExpiryNoValue() { From 2e6b2a81528df758e6c349e70147ddee0ff53ca8 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 9 Aug 2023 08:10:53 -0400 Subject: [PATCH 243/435] [Tests] Remove Moq as a dependency (#2522) This is a test-only change. I don't want to risk an upgrade and harvesting PII from anyone who works on our project, so I'm removing Moq immediately. See https://github.com/moq/moq/issues/1372 for details/discussion. --- Directory.Packages.props | 3 +- .../KeyPrefixedBatchTests.cs | 14 +- .../KeyPrefixedDatabaseTests.cs | 413 +++---- .../KeyPrefixedTests.cs | 1079 +++++++++-------- .../KeyPrefixedTransactionTests.cs | 42 +- .../StackExchange.Redis.Tests.csproj | 1 - 6 files changed, 776 insertions(+), 776 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 309672642..91a4646c0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,10 +15,9 @@ - - + diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedBatchTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedBatchTests.cs index 4d0700eb0..0a38766af 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedBatchTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedBatchTests.cs @@ -1,26 +1,26 @@ -using Moq; -using StackExchange.Redis.KeyspaceIsolation; +using StackExchange.Redis.KeyspaceIsolation; using System.Text; +using NSubstitute; using Xunit; namespace StackExchange.Redis.Tests; -[Collection(nameof(MoqDependentCollection))] +[Collection(nameof(SubstituteDependentCollection))] public sealed class KeyPrefixedBatchTests { - private readonly Mock mock; + private readonly IBatch mock; private readonly KeyPrefixedBatch prefixed; public KeyPrefixedBatchTests() { - mock = new Mock(); - prefixed = new KeyPrefixedBatch(mock.Object, Encoding.UTF8.GetBytes("prefix:")); + mock = Substitute.For(); + prefixed = new KeyPrefixedBatch(mock, Encoding.UTF8.GetBytes("prefix:")); } [Fact] public void Execute() { prefixed.Execute(); - mock.Verify(_ => _.Execute(), Times.Once()); + mock.Received(1).Execute(); } } diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs index b4eff605a..587d5a0da 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs @@ -3,35 +3,35 @@ using System.Linq.Expressions; using System.Net; using System.Text; -using Moq; +using NSubstitute; using StackExchange.Redis.KeyspaceIsolation; using Xunit; namespace StackExchange.Redis.Tests; -[CollectionDefinition(nameof(MoqDependentCollection), DisableParallelization = true)] -public class MoqDependentCollection { } +[CollectionDefinition(nameof(SubstituteDependentCollection), DisableParallelization = true)] +public class SubstituteDependentCollection { } -[Collection(nameof(MoqDependentCollection))] +[Collection(nameof(SubstituteDependentCollection))] public sealed class KeyPrefixedDatabaseTests { - private readonly Mock mock; + private readonly IDatabase mock; private readonly IDatabase prefixed; public KeyPrefixedDatabaseTests() { - mock = new Mock(); - prefixed = new KeyPrefixedDatabase(mock.Object, Encoding.UTF8.GetBytes("prefix:")); + mock = Substitute.For(); + prefixed = new KeyPrefixedDatabase(mock, Encoding.UTF8.GetBytes("prefix:")); } [Fact] public void CreateBatch() { object asyncState = new(); - IBatch innerBatch = new Mock().Object; - mock.Setup(_ => _.CreateBatch(asyncState)).Returns(innerBatch); + IBatch innerBatch = Substitute.For(); + mock.CreateBatch(asyncState).Returns(innerBatch); IBatch wrappedBatch = prefixed.CreateBatch(asyncState); - mock.Verify(_ => _.CreateBatch(asyncState)); + mock.Received().CreateBatch(asyncState); Assert.IsType(wrappedBatch); Assert.Same(innerBatch, ((KeyPrefixedBatch)wrappedBatch).Inner); } @@ -40,10 +40,10 @@ public void CreateBatch() public void CreateTransaction() { object asyncState = new(); - ITransaction innerTransaction = new Mock().Object; - mock.Setup(_ => _.CreateTransaction(asyncState)).Returns(innerTransaction); + ITransaction innerTransaction = Substitute.For(); + mock.CreateTransaction(asyncState).Returns(innerTransaction); ITransaction wrappedTransaction = prefixed.CreateTransaction(asyncState); - mock.Verify(_ => _.CreateTransaction(asyncState)); + mock.Received().CreateTransaction(asyncState); Assert.IsType(wrappedTransaction); Assert.Same(innerTransaction, ((KeyPrefixedTransaction)wrappedTransaction).Inner); } @@ -52,13 +52,13 @@ public void CreateTransaction() public void DebugObject() { prefixed.DebugObject("key", CommandFlags.None); - mock.Verify(_ => _.DebugObject("prefix:key", CommandFlags.None)); + mock.Received().DebugObject("prefix:key", CommandFlags.None); } [Fact] public void Get_Database() { - mock.SetupGet(_ => _.Database).Returns(123); + mock.Database.Returns(123); Assert.Equal(123, prefixed.Database); } @@ -66,21 +66,21 @@ public void Get_Database() public void HashDecrement_1() { prefixed.HashDecrement("key", "hashField", 123, CommandFlags.None); - mock.Verify(_ => _.HashDecrement("prefix:key", "hashField", 123, CommandFlags.None)); + mock.Received().HashDecrement("prefix:key", "hashField", 123, CommandFlags.None); } [Fact] public void HashDecrement_2() { prefixed.HashDecrement("key", "hashField", 1.23, CommandFlags.None); - mock.Verify(_ => _.HashDecrement("prefix:key", "hashField", 1.23, CommandFlags.None)); + mock.Received().HashDecrement("prefix:key", "hashField", 1.23, CommandFlags.None); } [Fact] public void HashDelete_1() { prefixed.HashDelete("key", "hashField", CommandFlags.None); - mock.Verify(_ => _.HashDelete("prefix:key", "hashField", CommandFlags.None)); + mock.Received().HashDelete("prefix:key", "hashField", CommandFlags.None); } [Fact] @@ -88,21 +88,21 @@ public void HashDelete_2() { RedisValue[] hashFields = Array.Empty(); prefixed.HashDelete("key", hashFields, CommandFlags.None); - mock.Verify(_ => _.HashDelete("prefix:key", hashFields, CommandFlags.None)); + mock.Received().HashDelete("prefix:key", hashFields, CommandFlags.None); } [Fact] public void HashExists() { prefixed.HashExists("key", "hashField", CommandFlags.None); - mock.Verify(_ => _.HashExists("prefix:key", "hashField", CommandFlags.None)); + mock.Received().HashExists("prefix:key", "hashField", CommandFlags.None); } [Fact] public void HashGet_1() { prefixed.HashGet("key", "hashField", CommandFlags.None); - mock.Verify(_ => _.HashGet("prefix:key", "hashField", CommandFlags.None)); + mock.Received().HashGet("prefix:key", "hashField", CommandFlags.None); } [Fact] @@ -110,56 +110,56 @@ public void HashGet_2() { RedisValue[] hashFields = Array.Empty(); prefixed.HashGet("key", hashFields, CommandFlags.None); - mock.Verify(_ => _.HashGet("prefix:key", hashFields, CommandFlags.None)); + mock.Received().HashGet("prefix:key", hashFields, CommandFlags.None); } [Fact] public void HashGetAll() { prefixed.HashGetAll("key", CommandFlags.None); - mock.Verify(_ => _.HashGetAll("prefix:key", CommandFlags.None)); + mock.Received().HashGetAll("prefix:key", CommandFlags.None); } [Fact] public void HashIncrement_1() { prefixed.HashIncrement("key", "hashField", 123, CommandFlags.None); - mock.Verify(_ => _.HashIncrement("prefix:key", "hashField", 123, CommandFlags.None)); + mock.Received().HashIncrement("prefix:key", "hashField", 123, CommandFlags.None); } [Fact] public void HashIncrement_2() { prefixed.HashIncrement("key", "hashField", 1.23, CommandFlags.None); - mock.Verify(_ => _.HashIncrement("prefix:key", "hashField", 1.23, CommandFlags.None)); + mock.Received().HashIncrement("prefix:key", "hashField", 1.23, CommandFlags.None); } [Fact] public void HashKeys() { prefixed.HashKeys("key", CommandFlags.None); - mock.Verify(_ => _.HashKeys("prefix:key", CommandFlags.None)); + mock.Received().HashKeys("prefix:key", CommandFlags.None); } [Fact] public void HashLength() { prefixed.HashLength("key", CommandFlags.None); - mock.Verify(_ => _.HashLength("prefix:key", CommandFlags.None)); + mock.Received().HashLength("prefix:key", CommandFlags.None); } [Fact] public void HashScan() { prefixed.HashScan("key", "pattern", 123, flags: CommandFlags.None); - mock.Verify(_ => _.HashScan("prefix:key", "pattern", 123, CommandFlags.None)); + mock.Received().HashScan("prefix:key", "pattern", 123, CommandFlags.None); } [Fact] public void HashScan_Full() { prefixed.HashScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); - mock.Verify(_ => _.HashScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None)); + mock.Received().HashScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None); } [Fact] @@ -167,35 +167,35 @@ public void HashSet_1() { HashEntry[] hashFields = Array.Empty(); prefixed.HashSet("key", hashFields, CommandFlags.None); - mock.Verify(_ => _.HashSet("prefix:key", hashFields, CommandFlags.None)); + mock.Received().HashSet("prefix:key", hashFields, CommandFlags.None); } [Fact] public void HashSet_2() { prefixed.HashSet("key", "hashField", "value", When.Exists, CommandFlags.None); - mock.Verify(_ => _.HashSet("prefix:key", "hashField", "value", When.Exists, CommandFlags.None)); + mock.Received().HashSet("prefix:key", "hashField", "value", When.Exists, CommandFlags.None); } [Fact] public void HashStringLength() { prefixed.HashStringLength("key", "field", CommandFlags.None); - mock.Verify(_ => _.HashStringLength("prefix:key", "field", CommandFlags.None)); + mock.Received().HashStringLength("prefix:key", "field", CommandFlags.None); } [Fact] public void HashValues() { prefixed.HashValues("key", CommandFlags.None); - mock.Verify(_ => _.HashValues("prefix:key", CommandFlags.None)); + mock.Received().HashValues("prefix:key", CommandFlags.None); } [Fact] public void HyperLogLogAdd_1() { prefixed.HyperLogLogAdd("key", "value", CommandFlags.None); - mock.Verify(_ => _.HyperLogLogAdd("prefix:key", "value", CommandFlags.None)); + mock.Received().HyperLogLogAdd("prefix:key", "value", CommandFlags.None); } [Fact] @@ -203,81 +203,81 @@ public void HyperLogLogAdd_2() { RedisValue[] values = Array.Empty(); prefixed.HyperLogLogAdd("key", values, CommandFlags.None); - mock.Verify(_ => _.HyperLogLogAdd("prefix:key", values, CommandFlags.None)); + mock.Received().HyperLogLogAdd("prefix:key", values, CommandFlags.None); } [Fact] public void HyperLogLogLength() { prefixed.HyperLogLogLength("key", CommandFlags.None); - mock.Verify(_ => _.HyperLogLogLength("prefix:key", CommandFlags.None)); + mock.Received().HyperLogLogLength("prefix:key", CommandFlags.None); } [Fact] public void HyperLogLogMerge_1() { prefixed.HyperLogLogMerge("destination", "first", "second", CommandFlags.None); - mock.Verify(_ => _.HyperLogLogMerge("prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); + mock.Received().HyperLogLogMerge("prefix:destination", "prefix:first", "prefix:second", CommandFlags.None); } [Fact] public void HyperLogLogMerge_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.HyperLogLogMerge("destination", keys, CommandFlags.None); - mock.Verify(_ => _.HyperLogLogMerge("prefix:destination", It.Is(valid), CommandFlags.None)); + mock.Received().HyperLogLogMerge("prefix:destination", Arg.Is(valid), CommandFlags.None); } [Fact] public void IdentifyEndpoint() { prefixed.IdentifyEndpoint("key", CommandFlags.None); - mock.Verify(_ => _.IdentifyEndpoint("prefix:key", CommandFlags.None)); + mock.Received().IdentifyEndpoint("prefix:key", CommandFlags.None); } [Fact] public void KeyCopy() { prefixed.KeyCopy("key", "destination", flags: CommandFlags.None); - mock.Verify(_ => _.KeyCopy("prefix:key", "prefix:destination", -1, false, CommandFlags.None)); + mock.Received().KeyCopy("prefix:key", "prefix:destination", -1, false, CommandFlags.None); } [Fact] public void KeyDelete_1() { prefixed.KeyDelete("key", CommandFlags.None); - mock.Verify(_ => _.KeyDelete("prefix:key", CommandFlags.None)); + mock.Received().KeyDelete("prefix:key", CommandFlags.None); } [Fact] public void KeyDelete_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.KeyDelete(keys, CommandFlags.None); - mock.Verify(_ => _.KeyDelete(It.Is(valid), CommandFlags.None)); + mock.Received().KeyDelete(Arg.Is(valid), CommandFlags.None); } [Fact] public void KeyDump() { prefixed.KeyDump("key", CommandFlags.None); - mock.Verify(_ => _.KeyDump("prefix:key", CommandFlags.None)); + mock.Received().KeyDump("prefix:key", CommandFlags.None); } [Fact] public void KeyEncoding() { prefixed.KeyEncoding("key", CommandFlags.None); - mock.Verify(_ => _.KeyEncoding("prefix:key", CommandFlags.None)); + mock.Received().KeyEncoding("prefix:key", CommandFlags.None); } [Fact] public void KeyExists() { prefixed.KeyExists("key", CommandFlags.None); - mock.Verify(_ => _.KeyExists("prefix:key", CommandFlags.None)); + mock.Received().KeyExists("prefix:key", CommandFlags.None); } [Fact] @@ -285,7 +285,7 @@ public void KeyExpire_1() { TimeSpan expiry = TimeSpan.FromSeconds(123); prefixed.KeyExpire("key", expiry, CommandFlags.None); - mock.Verify(_ => _.KeyExpire("prefix:key", expiry, CommandFlags.None)); + mock.Received().KeyExpire("prefix:key", expiry, CommandFlags.None); } [Fact] @@ -293,7 +293,7 @@ public void KeyExpire_2() { DateTime expiry = DateTime.Now; prefixed.KeyExpire("key", expiry, CommandFlags.None); - mock.Verify(_ => _.KeyExpire("prefix:key", expiry, CommandFlags.None)); + mock.Received().KeyExpire("prefix:key", expiry, CommandFlags.None); } [Fact] @@ -301,7 +301,7 @@ public void KeyExpire_3() { TimeSpan expiry = TimeSpan.FromSeconds(123); prefixed.KeyExpire("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); - mock.Verify(_ => _.KeyExpire("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); + mock.Received().KeyExpire("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); } [Fact] @@ -309,21 +309,21 @@ public void KeyExpire_4() { DateTime expiry = DateTime.Now; prefixed.KeyExpire("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); - mock.Verify(_ => _.KeyExpire("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); + mock.Received().KeyExpire("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); } [Fact] public void KeyExpireTime() { prefixed.KeyExpireTime("key", CommandFlags.None); - mock.Verify(_ => _.KeyExpireTime("prefix:key", CommandFlags.None)); + mock.Received().KeyExpireTime("prefix:key", CommandFlags.None); } [Fact] public void KeyFrequency() { prefixed.KeyFrequency("key", CommandFlags.None); - mock.Verify(_ => _.KeyFrequency("prefix:key", CommandFlags.None)); + mock.Received().KeyFrequency("prefix:key", CommandFlags.None); } [Fact] @@ -331,21 +331,21 @@ public void KeyMigrate() { EndPoint toServer = new IPEndPoint(IPAddress.Loopback, 123); prefixed.KeyMigrate("key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None); - mock.Verify(_ => _.KeyMigrate("prefix:key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None)); + mock.Received().KeyMigrate("prefix:key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None); } [Fact] public void KeyMove() { prefixed.KeyMove("key", 123, CommandFlags.None); - mock.Verify(_ => _.KeyMove("prefix:key", 123, CommandFlags.None)); + mock.Received().KeyMove("prefix:key", 123, CommandFlags.None); } [Fact] public void KeyPersist() { prefixed.KeyPersist("key", CommandFlags.None); - mock.Verify(_ => _.KeyPersist("prefix:key", CommandFlags.None)); + mock.Received().KeyPersist("prefix:key", CommandFlags.None); } [Fact] @@ -358,14 +358,14 @@ public void KeyRandom() public void KeyRefCount() { prefixed.KeyRefCount("key", CommandFlags.None); - mock.Verify(_ => _.KeyRefCount("prefix:key", CommandFlags.None)); + mock.Received().KeyRefCount("prefix:key", CommandFlags.None); } [Fact] public void KeyRename() { prefixed.KeyRename("key", "newKey", When.Exists, CommandFlags.None); - mock.Verify(_ => _.KeyRename("prefix:key", "prefix:newKey", When.Exists, CommandFlags.None)); + mock.Received().KeyRename("prefix:key", "prefix:newKey", When.Exists, CommandFlags.None); } [Fact] @@ -374,63 +374,63 @@ public void KeyRestore() byte[] value = Array.Empty(); TimeSpan expiry = TimeSpan.FromSeconds(123); prefixed.KeyRestore("key", value, expiry, CommandFlags.None); - mock.Verify(_ => _.KeyRestore("prefix:key", value, expiry, CommandFlags.None)); + mock.Received().KeyRestore("prefix:key", value, expiry, CommandFlags.None); } [Fact] public void KeyTimeToLive() { prefixed.KeyTimeToLive("key", CommandFlags.None); - mock.Verify(_ => _.KeyTimeToLive("prefix:key", CommandFlags.None)); + mock.Received().KeyTimeToLive("prefix:key", CommandFlags.None); } [Fact] public void KeyType() { prefixed.KeyType("key", CommandFlags.None); - mock.Verify(_ => _.KeyType("prefix:key", CommandFlags.None)); + mock.Received().KeyType("prefix:key", CommandFlags.None); } [Fact] public void ListGetByIndex() { prefixed.ListGetByIndex("key", 123, CommandFlags.None); - mock.Verify(_ => _.ListGetByIndex("prefix:key", 123, CommandFlags.None)); + mock.Received().ListGetByIndex("prefix:key", 123, CommandFlags.None); } [Fact] public void ListInsertAfter() { prefixed.ListInsertAfter("key", "pivot", "value", CommandFlags.None); - mock.Verify(_ => _.ListInsertAfter("prefix:key", "pivot", "value", CommandFlags.None)); + mock.Received().ListInsertAfter("prefix:key", "pivot", "value", CommandFlags.None); } [Fact] public void ListInsertBefore() { prefixed.ListInsertBefore("key", "pivot", "value", CommandFlags.None); - mock.Verify(_ => _.ListInsertBefore("prefix:key", "pivot", "value", CommandFlags.None)); + mock.Received().ListInsertBefore("prefix:key", "pivot", "value", CommandFlags.None); } [Fact] public void ListLeftPop() { prefixed.ListLeftPop("key", CommandFlags.None); - mock.Verify(_ => _.ListLeftPop("prefix:key", CommandFlags.None)); + mock.Received().ListLeftPop("prefix:key", CommandFlags.None); } [Fact] public void ListLeftPop_1() { prefixed.ListLeftPop("key", 123, CommandFlags.None); - mock.Verify(_ => _.ListLeftPop("prefix:key", 123, CommandFlags.None)); + mock.Received().ListLeftPop("prefix:key", 123, CommandFlags.None); } [Fact] public void ListLeftPush_1() { prefixed.ListLeftPush("key", "value", When.Exists, CommandFlags.None); - mock.Verify(_ => _.ListLeftPush("prefix:key", "value", When.Exists, CommandFlags.None)); + mock.Received().ListLeftPush("prefix:key", "value", When.Exists, CommandFlags.None); } [Fact] @@ -438,7 +438,7 @@ public void ListLeftPush_2() { RedisValue[] values = Array.Empty(); prefixed.ListLeftPush("key", values, CommandFlags.None); - mock.Verify(_ => _.ListLeftPush("prefix:key", values, CommandFlags.None)); + mock.Received().ListLeftPush("prefix:key", values, CommandFlags.None); } [Fact] @@ -446,63 +446,63 @@ public void ListLeftPush_3() { RedisValue[] values = new RedisValue[] { "value1", "value2" }; prefixed.ListLeftPush("key", values, When.Exists, CommandFlags.None); - mock.Verify(_ => _.ListLeftPush("prefix:key", values, When.Exists, CommandFlags.None)); + mock.Received().ListLeftPush("prefix:key", values, When.Exists, CommandFlags.None); } [Fact] public void ListLength() { prefixed.ListLength("key", CommandFlags.None); - mock.Verify(_ => _.ListLength("prefix:key", CommandFlags.None)); + mock.Received().ListLength("prefix:key", CommandFlags.None); } [Fact] public void ListMove() { prefixed.ListMove("key", "destination", ListSide.Left, ListSide.Right, CommandFlags.None); - mock.Verify(_ => _.ListMove("prefix:key", "prefix:destination", ListSide.Left, ListSide.Right, CommandFlags.None)); + mock.Received().ListMove("prefix:key", "prefix:destination", ListSide.Left, ListSide.Right, CommandFlags.None); } [Fact] public void ListRange() { prefixed.ListRange("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.ListRange("prefix:key", 123, 456, CommandFlags.None)); + mock.Received().ListRange("prefix:key", 123, 456, CommandFlags.None); } [Fact] public void ListRemove() { prefixed.ListRemove("key", "value", 123, CommandFlags.None); - mock.Verify(_ => _.ListRemove("prefix:key", "value", 123, CommandFlags.None)); + mock.Received().ListRemove("prefix:key", "value", 123, CommandFlags.None); } [Fact] public void ListRightPop() { prefixed.ListRightPop("key", CommandFlags.None); - mock.Verify(_ => _.ListRightPop("prefix:key", CommandFlags.None)); + mock.Received().ListRightPop("prefix:key", CommandFlags.None); } [Fact] public void ListRightPop_1() { prefixed.ListRightPop("key", 123, CommandFlags.None); - mock.Verify(_ => _.ListRightPop("prefix:key", 123, CommandFlags.None)); + mock.Received().ListRightPop("prefix:key", 123, CommandFlags.None); } [Fact] public void ListRightPopLeftPush() { prefixed.ListRightPopLeftPush("source", "destination", CommandFlags.None); - mock.Verify(_ => _.ListRightPopLeftPush("prefix:source", "prefix:destination", CommandFlags.None)); + mock.Received().ListRightPopLeftPush("prefix:source", "prefix:destination", CommandFlags.None); } [Fact] public void ListRightPush_1() { prefixed.ListRightPush("key", "value", When.Exists, CommandFlags.None); - mock.Verify(_ => _.ListRightPush("prefix:key", "value", When.Exists, CommandFlags.None)); + mock.Received().ListRightPush("prefix:key", "value", When.Exists, CommandFlags.None); } [Fact] @@ -510,7 +510,7 @@ public void ListRightPush_2() { RedisValue[] values = Array.Empty(); prefixed.ListRightPush("key", values, CommandFlags.None); - mock.Verify(_ => _.ListRightPush("prefix:key", values, CommandFlags.None)); + mock.Received().ListRightPush("prefix:key", values, CommandFlags.None); } [Fact] @@ -518,21 +518,21 @@ public void ListRightPush_3() { RedisValue[] values = new RedisValue[] { "value1", "value2" }; prefixed.ListRightPush("key", values, When.Exists, CommandFlags.None); - mock.Verify(_ => _.ListRightPush("prefix:key", values, When.Exists, CommandFlags.None)); + mock.Received().ListRightPush("prefix:key", values, When.Exists, CommandFlags.None); } [Fact] public void ListSetByIndex() { prefixed.ListSetByIndex("key", 123, "value", CommandFlags.None); - mock.Verify(_ => _.ListSetByIndex("prefix:key", 123, "value", CommandFlags.None)); + mock.Received().ListSetByIndex("prefix:key", 123, "value", CommandFlags.None); } [Fact] public void ListTrim() { prefixed.ListTrim("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.ListTrim("prefix:key", 123, 456, CommandFlags.None)); + mock.Received().ListTrim("prefix:key", 123, 456, CommandFlags.None); } [Fact] @@ -540,21 +540,21 @@ public void LockExtend() { TimeSpan expiry = TimeSpan.FromSeconds(123); prefixed.LockExtend("key", "value", expiry, CommandFlags.None); - mock.Verify(_ => _.LockExtend("prefix:key", "value", expiry, CommandFlags.None)); + mock.Received().LockExtend("prefix:key", "value", expiry, CommandFlags.None); } [Fact] public void LockQuery() { prefixed.LockQuery("key", CommandFlags.None); - mock.Verify(_ => _.LockQuery("prefix:key", CommandFlags.None)); + mock.Received().LockQuery("prefix:key", CommandFlags.None); } [Fact] public void LockRelease() { prefixed.LockRelease("key", "value", CommandFlags.None); - mock.Verify(_ => _.LockRelease("prefix:key", "value", CommandFlags.None)); + mock.Received().LockRelease("prefix:key", "value", CommandFlags.None); } [Fact] @@ -562,7 +562,7 @@ public void LockTake() { TimeSpan expiry = TimeSpan.FromSeconds(123); prefixed.LockTake("key", "value", expiry, CommandFlags.None); - mock.Verify(_ => _.LockTake("prefix:key", "value", expiry, CommandFlags.None)); + mock.Received().LockTake("prefix:key", "value", expiry, CommandFlags.None); } [Fact] @@ -570,7 +570,7 @@ public void Publish() { #pragma warning disable CS0618 prefixed.Publish("channel", "message", CommandFlags.None); - mock.Verify(_ => _.Publish("prefix:channel", "message", CommandFlags.None)); + mock.Received().Publish("prefix:channel", "message", CommandFlags.None); #pragma warning restore CS0618 } @@ -580,9 +580,9 @@ public void ScriptEvaluate_1() byte[] hash = Array.Empty(); RedisValue[] values = Array.Empty(); RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.ScriptEvaluate(hash, keys, values, CommandFlags.None); - mock.Verify(_ => _.ScriptEvaluate(hash, It.Is(valid), values, CommandFlags.None)); + mock.Received().ScriptEvaluate(hash, Arg.Is(valid), values, CommandFlags.None); } [Fact] @@ -590,16 +590,16 @@ public void ScriptEvaluate_2() { RedisValue[] values = Array.Empty(); RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.ScriptEvaluate("script", keys, values, CommandFlags.None); - mock.Verify(_ => _.ScriptEvaluate("script", It.Is(valid), values, CommandFlags.None)); + mock.Received().ScriptEvaluate("script", Arg.Is(valid), values, CommandFlags.None); } [Fact] public void SetAdd_1() { prefixed.SetAdd("key", "value", CommandFlags.None); - mock.Verify(_ => _.SetAdd("prefix:key", "value", CommandFlags.None)); + mock.Received().SetAdd("prefix:key", "value", CommandFlags.None); } [Fact] @@ -607,46 +607,46 @@ public void SetAdd_2() { RedisValue[] values = Array.Empty(); prefixed.SetAdd("key", values, CommandFlags.None); - mock.Verify(_ => _.SetAdd("prefix:key", values, CommandFlags.None)); + mock.Received().SetAdd("prefix:key", values, CommandFlags.None); } [Fact] public void SetCombine_1() { prefixed.SetCombine(SetOperation.Intersect, "first", "second", CommandFlags.None); - mock.Verify(_ => _.SetCombine(SetOperation.Intersect, "prefix:first", "prefix:second", CommandFlags.None)); + mock.Received().SetCombine(SetOperation.Intersect, "prefix:first", "prefix:second", CommandFlags.None); } [Fact] public void SetCombine_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.SetCombine(SetOperation.Intersect, keys, CommandFlags.None); - mock.Verify(_ => _.SetCombine(SetOperation.Intersect, It.Is(valid), CommandFlags.None)); + mock.Received().SetCombine(SetOperation.Intersect, Arg.Is(valid), CommandFlags.None); } [Fact] public void SetCombineAndStore_1() { prefixed.SetCombineAndStore(SetOperation.Intersect, "destination", "first", "second", CommandFlags.None); - mock.Verify(_ => _.SetCombineAndStore(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); + mock.Received().SetCombineAndStore(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None); } [Fact] public void SetCombineAndStore_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None); - mock.Verify(_ => _.SetCombineAndStore(SetOperation.Intersect, "prefix:destination", It.Is(valid), CommandFlags.None)); + mock.Received().SetCombineAndStore(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None); } [Fact] public void SetContains() { prefixed.SetContains("key", "value", CommandFlags.None); - mock.Verify(_ => _.SetContains("prefix:key", "value", CommandFlags.None)); + mock.Received().SetContains("prefix:key", "value", CommandFlags.None); } [Fact] @@ -654,7 +654,7 @@ public void SetContains_2() { RedisValue[] values = new RedisValue[] { "value1", "value2" }; prefixed.SetContains("key", values, CommandFlags.None); - mock.Verify(_ => _.SetContains("prefix:key", values, CommandFlags.None)); + mock.Received().SetContains("prefix:key", values, CommandFlags.None); } [Fact] @@ -662,66 +662,66 @@ public void SetIntersectionLength() { var keys = new RedisKey[] { "key1", "key2" }; prefixed.SetIntersectionLength(keys); - mock.Verify(_ => _.SetIntersectionLength(keys, 0, CommandFlags.None)); + mock.Received().SetIntersectionLength(keys, 0, CommandFlags.None); } [Fact] public void SetLength() { prefixed.SetLength("key", CommandFlags.None); - mock.Verify(_ => _.SetLength("prefix:key", CommandFlags.None)); + mock.Received().SetLength("prefix:key", CommandFlags.None); } [Fact] public void SetMembers() { prefixed.SetMembers("key", CommandFlags.None); - mock.Verify(_ => _.SetMembers("prefix:key", CommandFlags.None)); + mock.Received().SetMembers("prefix:key", CommandFlags.None); } [Fact] public void SetMove() { prefixed.SetMove("source", "destination", "value", CommandFlags.None); - mock.Verify(_ => _.SetMove("prefix:source", "prefix:destination", "value", CommandFlags.None)); + mock.Received().SetMove("prefix:source", "prefix:destination", "value", CommandFlags.None); } [Fact] public void SetPop_1() { prefixed.SetPop("key", CommandFlags.None); - mock.Verify(_ => _.SetPop("prefix:key", CommandFlags.None)); + mock.Received().SetPop("prefix:key", CommandFlags.None); prefixed.SetPop("key", 5, CommandFlags.None); - mock.Verify(_ => _.SetPop("prefix:key", 5, CommandFlags.None)); + mock.Received().SetPop("prefix:key", 5, CommandFlags.None); } [Fact] public void SetPop_2() { prefixed.SetPop("key", 5, CommandFlags.None); - mock.Verify(_ => _.SetPop("prefix:key", 5, CommandFlags.None)); + mock.Received().SetPop("prefix:key", 5, CommandFlags.None); } [Fact] public void SetRandomMember() { prefixed.SetRandomMember("key", CommandFlags.None); - mock.Verify(_ => _.SetRandomMember("prefix:key", CommandFlags.None)); + mock.Received().SetRandomMember("prefix:key", CommandFlags.None); } [Fact] public void SetRandomMembers() { prefixed.SetRandomMembers("key", 123, CommandFlags.None); - mock.Verify(_ => _.SetRandomMembers("prefix:key", 123, CommandFlags.None)); + mock.Received().SetRandomMembers("prefix:key", 123, CommandFlags.None); } [Fact] public void SetRemove_1() { prefixed.SetRemove("key", "value", CommandFlags.None); - mock.Verify(_ => _.SetRemove("prefix:key", "value", CommandFlags.None)); + mock.Received().SetRemove("prefix:key", "value", CommandFlags.None); } [Fact] @@ -729,54 +729,54 @@ public void SetRemove_2() { RedisValue[] values = Array.Empty(); prefixed.SetRemove("key", values, CommandFlags.None); - mock.Verify(_ => _.SetRemove("prefix:key", values, CommandFlags.None)); + mock.Received().SetRemove("prefix:key", values, CommandFlags.None); } [Fact] public void SetScan() { prefixed.SetScan("key", "pattern", 123, flags: CommandFlags.None); - mock.Verify(_ => _.SetScan("prefix:key", "pattern", 123, CommandFlags.None)); + mock.Received().SetScan("prefix:key", "pattern", 123, CommandFlags.None); } [Fact] public void SetScan_Full() { prefixed.SetScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); - mock.Verify(_ => _.SetScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None)); + mock.Received().SetScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None); } [Fact] public void Sort() { RedisValue[] get = new RedisValue[] { "a", "#" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; prefixed.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); prefixed.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); - mock.Verify(_ => _.Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", It.Is(valid), CommandFlags.None)); - mock.Verify(_ => _.Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", It.Is(valid), CommandFlags.None)); + mock.Received().Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", Arg.Is(valid), CommandFlags.None); + mock.Received().Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", Arg.Is(valid), CommandFlags.None); } [Fact] public void SortAndStore() { RedisValue[] get = new RedisValue[] { "a", "#" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; prefixed.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); prefixed.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); - mock.Verify(_ => _.SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", It.Is(valid), CommandFlags.None)); - mock.Verify(_ => _.SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", It.Is(valid), CommandFlags.None)); + mock.Received().SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", Arg.Is(valid), CommandFlags.None); + mock.Received().SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", Arg.Is(valid), CommandFlags.None); } [Fact] public void SortedSetAdd_1() { prefixed.SortedSetAdd("key", "member", 1.23, When.Exists, CommandFlags.None); - mock.Verify(_ => _.SortedSetAdd("prefix:key", "member", 1.23, When.Exists, CommandFlags.None)); + mock.Received().SortedSetAdd("prefix:key", "member", 1.23, When.Exists, CommandFlags.None); } [Fact] @@ -784,7 +784,7 @@ public void SortedSetAdd_2() { SortedSetEntry[] values = Array.Empty(); prefixed.SortedSetAdd("key", values, When.Exists, CommandFlags.None); - mock.Verify(_ => _.SortedSetAdd("prefix:key", values, When.Exists, CommandFlags.None)); + mock.Received().SortedSetAdd("prefix:key", values, When.Exists, CommandFlags.None); } [Fact] @@ -792,7 +792,7 @@ public void SortedSetAdd_3() { SortedSetEntry[] values = Array.Empty(); prefixed.SortedSetAdd("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); - mock.Verify(_ => _.SortedSetAdd("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None)); + mock.Received().SortedSetAdd("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None); } [Fact] @@ -800,7 +800,7 @@ public void SortedSetCombine() { RedisKey[] keys = new RedisKey[] { "a", "b" }; prefixed.SortedSetCombine(SetOperation.Intersect, keys); - mock.Verify(_ => _.SortedSetCombine(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); + mock.Received().SortedSetCombine(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None); } [Fact] @@ -808,37 +808,37 @@ public void SortedSetCombineWithScores() { RedisKey[] keys = new RedisKey[] { "a", "b" }; prefixed.SortedSetCombineWithScores(SetOperation.Intersect, keys); - mock.Verify(_ => _.SortedSetCombineWithScores(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); + mock.Received().SortedSetCombineWithScores(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None); } [Fact] public void SortedSetCombineAndStore_1() { prefixed.SortedSetCombineAndStore(SetOperation.Intersect, "destination", "first", "second", Aggregate.Max, CommandFlags.None); - mock.Verify(_ => _.SortedSetCombineAndStore(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", Aggregate.Max, CommandFlags.None)); + mock.Received().SortedSetCombineAndStore(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", Aggregate.Max, CommandFlags.None); } [Fact] public void SortedSetCombineAndStore_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None); - mock.Verify(_ => _.SetCombineAndStore(SetOperation.Intersect, "prefix:destination", It.Is(valid), CommandFlags.None)); + mock.Received().SetCombineAndStore(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None); } [Fact] public void SortedSetDecrement() { prefixed.SortedSetDecrement("key", "member", 1.23, CommandFlags.None); - mock.Verify(_ => _.SortedSetDecrement("prefix:key", "member", 1.23, CommandFlags.None)); + mock.Received().SortedSetDecrement("prefix:key", "member", 1.23, CommandFlags.None); } [Fact] public void SortedSetIncrement() { prefixed.SortedSetIncrement("key", "member", 1.23, CommandFlags.None); - mock.Verify(_ => _.SortedSetIncrement("prefix:key", "member", 1.23, CommandFlags.None)); + mock.Received().SortedSetIncrement("prefix:key", "member", 1.23, CommandFlags.None); } [Fact] @@ -846,98 +846,98 @@ public void SortedSetIntersectionLength() { RedisKey[] keys = new RedisKey[] { "a", "b" }; prefixed.SortedSetIntersectionLength(keys, 1, CommandFlags.None); - mock.Verify(_ => _.SortedSetIntersectionLength(keys, 1, CommandFlags.None)); + mock.Received().SortedSetIntersectionLength(keys, 1, CommandFlags.None); } [Fact] public void SortedSetLength() { prefixed.SortedSetLength("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); - mock.Verify(_ => _.SortedSetLength("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None)); + mock.Received().SortedSetLength("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None); } [Fact] public void SortedSetRandomMember() { prefixed.SortedSetRandomMember("key", CommandFlags.None); - mock.Verify(_ => _.SortedSetRandomMember("prefix:key", CommandFlags.None)); + mock.Received().SortedSetRandomMember("prefix:key", CommandFlags.None); } [Fact] public void SortedSetRandomMembers() { prefixed.SortedSetRandomMembers("key", 2, CommandFlags.None); - mock.Verify(_ => _.SortedSetRandomMembers("prefix:key", 2, CommandFlags.None)); + mock.Received().SortedSetRandomMembers("prefix:key", 2, CommandFlags.None); } [Fact] public void SortedSetRandomMembersWithScores() { prefixed.SortedSetRandomMembersWithScores("key", 2, CommandFlags.None); - mock.Verify(_ => _.SortedSetRandomMembersWithScores("prefix:key", 2, CommandFlags.None)); + mock.Received().SortedSetRandomMembersWithScores("prefix:key", 2, CommandFlags.None); } [Fact] public void SortedSetLengthByValue() { prefixed.SortedSetLengthByValue("key", "min", "max", Exclude.Start, CommandFlags.None); - mock.Verify(_ => _.SortedSetLengthByValue("prefix:key", "min", "max", Exclude.Start, CommandFlags.None)); + mock.Received().SortedSetLengthByValue("prefix:key", "min", "max", Exclude.Start, CommandFlags.None); } [Fact] public void SortedSetRangeByRank() { prefixed.SortedSetRangeByRank("key", 123, 456, Order.Descending, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByRank("prefix:key", 123, 456, Order.Descending, CommandFlags.None)); + mock.Received().SortedSetRangeByRank("prefix:key", 123, 456, Order.Descending, CommandFlags.None); } [Fact] public void SortedSetRangeByRankWithScores() { prefixed.SortedSetRangeByRankWithScores("key", 123, 456, Order.Descending, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByRankWithScores("prefix:key", 123, 456, Order.Descending, CommandFlags.None)); + mock.Received().SortedSetRangeByRankWithScores("prefix:key", 123, 456, Order.Descending, CommandFlags.None); } [Fact] public void SortedSetRangeByScore() { prefixed.SortedSetRangeByScore("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByScore("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); + mock.Received().SortedSetRangeByScore("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); } [Fact] public void SortedSetRangeByScoreWithScores() { prefixed.SortedSetRangeByScoreWithScores("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByScoreWithScores("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); + mock.Received().SortedSetRangeByScoreWithScores("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); } [Fact] public void SortedSetRangeByValue() { prefixed.SortedSetRangeByValue("key", "min", "max", Exclude.Start, 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByValue("prefix:key", "min", "max", Exclude.Start, Order.Ascending, 123, 456, CommandFlags.None)); + mock.Received().SortedSetRangeByValue("prefix:key", "min", "max", Exclude.Start, Order.Ascending, 123, 456, CommandFlags.None); } [Fact] public void SortedSetRangeByValueDesc() { prefixed.SortedSetRangeByValue("key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByValue("prefix:key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); + mock.Received().SortedSetRangeByValue("prefix:key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); } [Fact] public void SortedSetRank() { prefixed.SortedSetRank("key", "member", Order.Descending, CommandFlags.None); - mock.Verify(_ => _.SortedSetRank("prefix:key", "member", Order.Descending, CommandFlags.None)); + mock.Received().SortedSetRank("prefix:key", "member", Order.Descending, CommandFlags.None); } [Fact] public void SortedSetRemove_1() { prefixed.SortedSetRemove("key", "member", CommandFlags.None); - mock.Verify(_ => _.SortedSetRemove("prefix:key", "member", CommandFlags.None)); + mock.Received().SortedSetRemove("prefix:key", "member", CommandFlags.None); } [Fact] @@ -945,56 +945,57 @@ public void SortedSetRemove_2() { RedisValue[] members = Array.Empty(); prefixed.SortedSetRemove("key", members, CommandFlags.None); - mock.Verify(_ => _.SortedSetRemove("prefix:key", members, CommandFlags.None)); + mock.Received().SortedSetRemove("prefix:key", members, CommandFlags.None); } [Fact] public void SortedSetRemoveRangeByRank() { prefixed.SortedSetRemoveRangeByRank("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRemoveRangeByRank("prefix:key", 123, 456, CommandFlags.None)); + mock.Received().SortedSetRemoveRangeByRank("prefix:key", 123, 456, CommandFlags.None); } [Fact] public void SortedSetRemoveRangeByScore() { prefixed.SortedSetRemoveRangeByScore("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); - mock.Verify(_ => _.SortedSetRemoveRangeByScore("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None)); + mock.Received().SortedSetRemoveRangeByScore("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None); } [Fact] public void SortedSetRemoveRangeByValue() { prefixed.SortedSetRemoveRangeByValue("key", "min", "max", Exclude.Start, CommandFlags.None); - mock.Verify(_ => _.SortedSetRemoveRangeByValue("prefix:key", "min", "max", Exclude.Start, CommandFlags.None)); + mock.Received().SortedSetRemoveRangeByValue("prefix:key", "min", "max", Exclude.Start, CommandFlags.None); } [Fact] public void SortedSetScan() { prefixed.SortedSetScan("key", "pattern", 123, flags: CommandFlags.None); - mock.Verify(_ => _.SortedSetScan("prefix:key", "pattern", 123, CommandFlags.None)); + mock.Received().SortedSetScan("prefix:key", "pattern", 123, CommandFlags.None); } [Fact] public void SortedSetScan_Full() { prefixed.SortedSetScan("key", "pattern", 123, 42, 64, flags: CommandFlags.None); - mock.Verify(_ => _.SortedSetScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None)); + mock.Received().SortedSetScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None); } [Fact] public void SortedSetScore() { prefixed.SortedSetScore("key", "member", CommandFlags.None); - mock.Verify(_ => _.SortedSetScore("prefix:key", "member", CommandFlags.None)); + mock.Received().SortedSetScore("prefix:key", "member", CommandFlags.None); } [Fact] public void SortedSetScore_Multiple() { - prefixed.SortedSetScores("key", new RedisValue[] { "member1", "member2" }, CommandFlags.None); - mock.Verify(_ => _.SortedSetScores("prefix:key", new RedisValue[] { "member1", "member2" }, CommandFlags.None)); + var values = new RedisValue[] { "member1", "member2" }; + prefixed.SortedSetScores("key", values, CommandFlags.None); + mock.Received().SortedSetScores("prefix:key", values, CommandFlags.None); } [Fact] @@ -1002,14 +1003,14 @@ public void SortedSetUpdate() { SortedSetEntry[] values = Array.Empty(); prefixed.SortedSetUpdate("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); - mock.Verify(_ => _.SortedSetUpdate("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None)); + mock.Received().SortedSetUpdate("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None); } [Fact] public void StreamAcknowledge_1() { prefixed.StreamAcknowledge("key", "group", "0-0", CommandFlags.None); - mock.Verify(_ => _.StreamAcknowledge("prefix:key", "group", "0-0", CommandFlags.None)); + mock.Received().StreamAcknowledge("prefix:key", "group", "0-0", CommandFlags.None); } [Fact] @@ -1017,14 +1018,14 @@ public void StreamAcknowledge_2() { var messageIds = new RedisValue[] { "0-0", "0-1", "0-2" }; prefixed.StreamAcknowledge("key", "group", messageIds, CommandFlags.None); - mock.Verify(_ => _.StreamAcknowledge("prefix:key", "group", messageIds, CommandFlags.None)); + mock.Received().StreamAcknowledge("prefix:key", "group", messageIds, CommandFlags.None); } [Fact] public void StreamAdd_1() { prefixed.StreamAdd("key", "field1", "value1", "*", 1000, true, CommandFlags.None); - mock.Verify(_ => _.StreamAdd("prefix:key", "field1", "value1", "*", 1000, true, CommandFlags.None)); + mock.Received().StreamAdd("prefix:key", "field1", "value1", "*", 1000, true, CommandFlags.None); } [Fact] @@ -1032,21 +1033,21 @@ public void StreamAdd_2() { var fields = Array.Empty(); prefixed.StreamAdd("key", fields, "*", 1000, true, CommandFlags.None); - mock.Verify(_ => _.StreamAdd("prefix:key", fields, "*", 1000, true, CommandFlags.None)); + mock.Received().StreamAdd("prefix:key", fields, "*", 1000, true, CommandFlags.None); } [Fact] public void StreamAutoClaim() { prefixed.StreamAutoClaim("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); - mock.Verify(_ => _.StreamAutoClaim("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None)); + mock.Received().StreamAutoClaim("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); } [Fact] public void StreamAutoClaimIdsOnly() { prefixed.StreamAutoClaimIdsOnly("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); - mock.Verify(_ => _.StreamAutoClaimIdsOnly("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None)); + mock.Received().StreamAutoClaimIdsOnly("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); } [Fact] @@ -1054,7 +1055,7 @@ public void StreamClaimMessages() { var messageIds = Array.Empty(); prefixed.StreamClaim("key", "group", "consumer", 1000, messageIds, CommandFlags.None); - mock.Verify(_ => _.StreamClaim("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); + mock.Received().StreamClaim("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None); } [Fact] @@ -1062,49 +1063,49 @@ public void StreamClaimMessagesReturningIds() { var messageIds = Array.Empty(); prefixed.StreamClaimIdsOnly("key", "group", "consumer", 1000, messageIds, CommandFlags.None); - mock.Verify(_ => _.StreamClaimIdsOnly("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); + mock.Received().StreamClaimIdsOnly("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None); } [Fact] public void StreamConsumerGroupSetPosition() { prefixed.StreamConsumerGroupSetPosition("key", "group", StreamPosition.Beginning, CommandFlags.None); - mock.Verify(_ => _.StreamConsumerGroupSetPosition("prefix:key", "group", StreamPosition.Beginning, CommandFlags.None)); + mock.Received().StreamConsumerGroupSetPosition("prefix:key", "group", StreamPosition.Beginning, CommandFlags.None); } [Fact] public void StreamConsumerInfoGet() { prefixed.StreamConsumerInfo("key", "group", CommandFlags.None); - mock.Verify(_ => _.StreamConsumerInfo("prefix:key", "group", CommandFlags.None)); + mock.Received().StreamConsumerInfo("prefix:key", "group", CommandFlags.None); } [Fact] public void StreamCreateConsumerGroup() { prefixed.StreamCreateConsumerGroup("key", "group", StreamPosition.Beginning, false, CommandFlags.None); - mock.Verify(_ => _.StreamCreateConsumerGroup("prefix:key", "group", StreamPosition.Beginning, false, CommandFlags.None)); + mock.Received().StreamCreateConsumerGroup("prefix:key", "group", StreamPosition.Beginning, false, CommandFlags.None); } [Fact] public void StreamGroupInfoGet() { prefixed.StreamGroupInfo("key", CommandFlags.None); - mock.Verify(_ => _.StreamGroupInfo("prefix:key", CommandFlags.None)); + mock.Received().StreamGroupInfo("prefix:key", CommandFlags.None); } [Fact] public void StreamInfoGet() { prefixed.StreamInfo("key", CommandFlags.None); - mock.Verify(_ => _.StreamInfo("prefix:key", CommandFlags.None)); + mock.Received().StreamInfo("prefix:key", CommandFlags.None); } [Fact] public void StreamLength() { prefixed.StreamLength("key", CommandFlags.None); - mock.Verify(_ => _.StreamLength("prefix:key", CommandFlags.None)); + mock.Received().StreamLength("prefix:key", CommandFlags.None); } [Fact] @@ -1112,42 +1113,42 @@ public void StreamMessagesDelete() { var messageIds = Array.Empty(); prefixed.StreamDelete("key", messageIds, CommandFlags.None); - mock.Verify(_ => _.StreamDelete("prefix:key", messageIds, CommandFlags.None)); + mock.Received().StreamDelete("prefix:key", messageIds, CommandFlags.None); } [Fact] public void StreamDeleteConsumer() { prefixed.StreamDeleteConsumer("key", "group", "consumer", CommandFlags.None); - mock.Verify(_ => _.StreamDeleteConsumer("prefix:key", "group", "consumer", CommandFlags.None)); + mock.Received().StreamDeleteConsumer("prefix:key", "group", "consumer", CommandFlags.None); } [Fact] public void StreamDeleteConsumerGroup() { prefixed.StreamDeleteConsumerGroup("key", "group", CommandFlags.None); - mock.Verify(_ => _.StreamDeleteConsumerGroup("prefix:key", "group", CommandFlags.None)); + mock.Received().StreamDeleteConsumerGroup("prefix:key", "group", CommandFlags.None); } [Fact] public void StreamPendingInfoGet() { prefixed.StreamPending("key", "group", CommandFlags.None); - mock.Verify(_ => _.StreamPending("prefix:key", "group", CommandFlags.None)); + mock.Received().StreamPending("prefix:key", "group", CommandFlags.None); } [Fact] public void StreamPendingMessageInfoGet() { prefixed.StreamPendingMessages("key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); - mock.Verify(_ => _.StreamPendingMessages("prefix:key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None)); + mock.Received().StreamPendingMessages("prefix:key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); } [Fact] public void StreamRange() { prefixed.StreamRange("key", "-", "+", null, Order.Ascending, CommandFlags.None); - mock.Verify(_ => _.StreamRange("prefix:key", "-", "+", null, Order.Ascending, CommandFlags.None)); + mock.Received().StreamRange("prefix:key", "-", "+", null, Order.Ascending, CommandFlags.None); } [Fact] @@ -1155,21 +1156,21 @@ public void StreamRead_1() { var streamPositions = Array.Empty(); prefixed.StreamRead(streamPositions, null, CommandFlags.None); - mock.Verify(_ => _.StreamRead(streamPositions, null, CommandFlags.None)); + mock.Received().StreamRead(streamPositions, null, CommandFlags.None); } [Fact] public void StreamRead_2() { prefixed.StreamRead("key", "0-0", null, CommandFlags.None); - mock.Verify(_ => _.StreamRead("prefix:key", "0-0", null, CommandFlags.None)); + mock.Received().StreamRead("prefix:key", "0-0", null, CommandFlags.None); } [Fact] public void StreamStreamReadGroup_1() { prefixed.StreamReadGroup("key", "group", "consumer", "0-0", 10, false, CommandFlags.None); - mock.Verify(_ => _.StreamReadGroup("prefix:key", "group", "consumer", "0-0", 10, false, CommandFlags.None)); + mock.Received().StreamReadGroup("prefix:key", "group", "consumer", "0-0", 10, false, CommandFlags.None); } [Fact] @@ -1177,151 +1178,151 @@ public void StreamStreamReadGroup_2() { var streamPositions = Array.Empty(); prefixed.StreamReadGroup(streamPositions, "group", "consumer", 10, false, CommandFlags.None); - mock.Verify(_ => _.StreamReadGroup(streamPositions, "group", "consumer", 10, false, CommandFlags.None)); + mock.Received().StreamReadGroup(streamPositions, "group", "consumer", 10, false, CommandFlags.None); } [Fact] public void StreamTrim() { prefixed.StreamTrim("key", 1000, true, CommandFlags.None); - mock.Verify(_ => _.StreamTrim("prefix:key", 1000, true, CommandFlags.None)); + mock.Received().StreamTrim("prefix:key", 1000, true, CommandFlags.None); } [Fact] public void StringAppend() { prefixed.StringAppend("key", "value", CommandFlags.None); - mock.Verify(_ => _.StringAppend("prefix:key", "value", CommandFlags.None)); + mock.Received().StringAppend("prefix:key", "value", CommandFlags.None); } [Fact] public void StringBitCount() { prefixed.StringBitCount("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.StringBitCount("prefix:key", 123, 456, CommandFlags.None)); + mock.Received().StringBitCount("prefix:key", 123, 456, CommandFlags.None); } [Fact] public void StringBitCount_2() { prefixed.StringBitCount("key", 123, 456, StringIndexType.Byte, CommandFlags.None); - mock.Verify(_ => _.StringBitCount("prefix:key", 123, 456, StringIndexType.Byte, CommandFlags.None)); + mock.Received().StringBitCount("prefix:key", 123, 456, StringIndexType.Byte, CommandFlags.None); } [Fact] public void StringBitOperation_1() { prefixed.StringBitOperation(Bitwise.Xor, "destination", "first", "second", CommandFlags.None); - mock.Verify(_ => _.StringBitOperation(Bitwise.Xor, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); + mock.Received().StringBitOperation(Bitwise.Xor, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None); } [Fact] public void StringBitOperation_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.StringBitOperation(Bitwise.Xor, "destination", keys, CommandFlags.None); - mock.Verify(_ => _.StringBitOperation(Bitwise.Xor, "prefix:destination", It.Is(valid), CommandFlags.None)); + mock.Received().StringBitOperation(Bitwise.Xor, "prefix:destination", Arg.Is(valid), CommandFlags.None); } [Fact] public void StringBitPosition() { prefixed.StringBitPosition("key", true, 123, 456, CommandFlags.None); - mock.Verify(_ => _.StringBitPosition("prefix:key", true, 123, 456, CommandFlags.None)); + mock.Received().StringBitPosition("prefix:key", true, 123, 456, CommandFlags.None); } [Fact] public void StringBitPosition_2() { prefixed.StringBitPosition("key", true, 123, 456, StringIndexType.Byte, CommandFlags.None); - mock.Verify(_ => _.StringBitPosition("prefix:key", true, 123, 456, StringIndexType.Byte, CommandFlags.None)); + mock.Received().StringBitPosition("prefix:key", true, 123, 456, StringIndexType.Byte, CommandFlags.None); } [Fact] public void StringDecrement_1() { prefixed.StringDecrement("key", 123, CommandFlags.None); - mock.Verify(_ => _.StringDecrement("prefix:key", 123, CommandFlags.None)); + mock.Received().StringDecrement("prefix:key", 123, CommandFlags.None); } [Fact] public void StringDecrement_2() { prefixed.StringDecrement("key", 1.23, CommandFlags.None); - mock.Verify(_ => _.StringDecrement("prefix:key", 1.23, CommandFlags.None)); + mock.Received().StringDecrement("prefix:key", 1.23, CommandFlags.None); } [Fact] public void StringGet_1() { prefixed.StringGet("key", CommandFlags.None); - mock.Verify(_ => _.StringGet("prefix:key", CommandFlags.None)); + mock.Received().StringGet("prefix:key", CommandFlags.None); } [Fact] public void StringGet_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.StringGet(keys, CommandFlags.None); - mock.Verify(_ => _.StringGet(It.Is(valid), CommandFlags.None)); + mock.Received().StringGet(Arg.Is(valid), CommandFlags.None); } [Fact] public void StringGetBit() { prefixed.StringGetBit("key", 123, CommandFlags.None); - mock.Verify(_ => _.StringGetBit("prefix:key", 123, CommandFlags.None)); + mock.Received().StringGetBit("prefix:key", 123, CommandFlags.None); } [Fact] public void StringGetRange() { prefixed.StringGetRange("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.StringGetRange("prefix:key", 123, 456, CommandFlags.None)); + mock.Received().StringGetRange("prefix:key", 123, 456, CommandFlags.None); } [Fact] public void StringGetSet() { prefixed.StringGetSet("key", "value", CommandFlags.None); - mock.Verify(_ => _.StringGetSet("prefix:key", "value", CommandFlags.None)); + mock.Received().StringGetSet("prefix:key", "value", CommandFlags.None); } [Fact] public void StringGetDelete() { prefixed.StringGetDelete("key", CommandFlags.None); - mock.Verify(_ => _.StringGetDelete("prefix:key", CommandFlags.None)); + mock.Received().StringGetDelete("prefix:key", CommandFlags.None); } [Fact] public void StringGetWithExpiry() { prefixed.StringGetWithExpiry("key", CommandFlags.None); - mock.Verify(_ => _.StringGetWithExpiry("prefix:key", CommandFlags.None)); + mock.Received().StringGetWithExpiry("prefix:key", CommandFlags.None); } [Fact] public void StringIncrement_1() { prefixed.StringIncrement("key", 123, CommandFlags.None); - mock.Verify(_ => _.StringIncrement("prefix:key", 123, CommandFlags.None)); + mock.Received().StringIncrement("prefix:key", 123, CommandFlags.None); } [Fact] public void StringIncrement_2() { prefixed.StringIncrement("key", 1.23, CommandFlags.None); - mock.Verify(_ => _.StringIncrement("prefix:key", 1.23, CommandFlags.None)); + mock.Received().StringIncrement("prefix:key", 1.23, CommandFlags.None); } [Fact] public void StringLength() { prefixed.StringLength("key", CommandFlags.None); - mock.Verify(_ => _.StringLength("prefix:key", CommandFlags.None)); + mock.Received().StringLength("prefix:key", CommandFlags.None); } [Fact] @@ -1329,7 +1330,7 @@ public void StringSet_1() { TimeSpan expiry = TimeSpan.FromSeconds(123); prefixed.StringSet("key", "value", expiry, When.Exists, CommandFlags.None); - mock.Verify(_ => _.StringSet("prefix:key", "value", expiry, When.Exists, CommandFlags.None)); + mock.Received().StringSet("prefix:key", "value", expiry, When.Exists, CommandFlags.None); } [Fact] @@ -1337,16 +1338,16 @@ public void StringSet_2() { TimeSpan? expiry = null; prefixed.StringSet("key", "value", expiry, true, When.Exists, CommandFlags.None); - mock.Verify(_ => _.StringSet("prefix:key", "value", expiry, true, When.Exists, CommandFlags.None)); + mock.Received().StringSet("prefix:key", "value", expiry, true, When.Exists, CommandFlags.None); } [Fact] public void StringSet_3() { KeyValuePair[] values = new KeyValuePair[] { new KeyValuePair("a", "x"), new KeyValuePair("b", "y") }; - Expression[], bool>> valid = _ => _.Length == 2 && _[0].Key == "prefix:a" && _[0].Value == "x" && _[1].Key == "prefix:b" && _[1].Value == "y"; + Expression[]>> valid = _ => _.Length == 2 && _[0].Key == "prefix:a" && _[0].Value == "x" && _[1].Key == "prefix:b" && _[1].Value == "y"; prefixed.StringSet(values, When.Exists, CommandFlags.None); - mock.Verify(_ => _.StringSet(It.Is(valid), When.Exists, CommandFlags.None)); + mock.Received().StringSet(Arg.Is(valid), When.Exists, CommandFlags.None); } [Fact] @@ -1354,20 +1355,20 @@ public void StringSet_Compat() { TimeSpan? expiry = null; prefixed.StringSet("key", "value", expiry, When.Exists); - mock.Verify(_ => _.StringSet("prefix:key", "value", expiry, When.Exists)); + mock.Received().StringSet("prefix:key", "value", expiry, When.Exists); } [Fact] public void StringSetBit() { prefixed.StringSetBit("key", 123, true, CommandFlags.None); - mock.Verify(_ => _.StringSetBit("prefix:key", 123, true, CommandFlags.None)); + mock.Received().StringSetBit("prefix:key", 123, true, CommandFlags.None); } [Fact] public void StringSetRange() { prefixed.StringSetRange("key", 123, "value", CommandFlags.None); - mock.Verify(_ => _.StringSetRange("prefix:key", 123, "value", CommandFlags.None)); + mock.Received().StringSetRange("prefix:key", 123, "value", CommandFlags.None); } } diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs index 2d6b53390..8cbe7ad7f 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs @@ -3,309 +3,309 @@ using System.Linq.Expressions; using System.Net; using System.Text; -using Moq; +using NSubstitute; using StackExchange.Redis.KeyspaceIsolation; using Xunit; using System.Threading.Tasks; namespace StackExchange.Redis.Tests { - [Collection(nameof(MoqDependentCollection))] + [Collection(nameof(SubstituteDependentCollection))] public sealed class KeyPrefixedTests { - private readonly Mock mock; + private readonly IDatabaseAsync mock; private readonly KeyPrefixed prefixed; public KeyPrefixedTests() { - mock = new Mock(); - prefixed = new KeyPrefixed(mock.Object, Encoding.UTF8.GetBytes("prefix:")); + mock = Substitute.For(); + prefixed = new KeyPrefixed(mock, Encoding.UTF8.GetBytes("prefix:")); } [Fact] public async Task DebugObjectAsync() { await prefixed.DebugObjectAsync("key", CommandFlags.None); - mock.Verify(_ => _.DebugObjectAsync("prefix:key", CommandFlags.None)); + await mock.Received().DebugObjectAsync("prefix:key", CommandFlags.None); } [Fact] - public void HashDecrementAsync_1() + public async Task HashDecrementAsync_1() { - prefixed.HashDecrementAsync("key", "hashField", 123, CommandFlags.None); - mock.Verify(_ => _.HashDecrementAsync("prefix:key", "hashField", 123, CommandFlags.None)); + await prefixed.HashDecrementAsync("key", "hashField", 123, CommandFlags.None); + await mock.Received().HashDecrementAsync("prefix:key", "hashField", 123, CommandFlags.None); } [Fact] - public void HashDecrementAsync_2() + public async Task HashDecrementAsync_2() { - prefixed.HashDecrementAsync("key", "hashField", 1.23, CommandFlags.None); - mock.Verify(_ => _.HashDecrementAsync("prefix:key", "hashField", 1.23, CommandFlags.None)); + await prefixed.HashDecrementAsync("key", "hashField", 1.23, CommandFlags.None); + await mock.Received().HashDecrementAsync("prefix:key", "hashField", 1.23, CommandFlags.None); } [Fact] - public void HashDeleteAsync_1() + public async Task HashDeleteAsync_1() { - prefixed.HashDeleteAsync("key", "hashField", CommandFlags.None); - mock.Verify(_ => _.HashDeleteAsync("prefix:key", "hashField", CommandFlags.None)); + await prefixed.HashDeleteAsync("key", "hashField", CommandFlags.None); + await mock.Received().HashDeleteAsync("prefix:key", "hashField", CommandFlags.None); } [Fact] - public void HashDeleteAsync_2() + public async Task HashDeleteAsync_2() { RedisValue[] hashFields = Array.Empty(); - prefixed.HashDeleteAsync("key", hashFields, CommandFlags.None); - mock.Verify(_ => _.HashDeleteAsync("prefix:key", hashFields, CommandFlags.None)); + await prefixed.HashDeleteAsync("key", hashFields, CommandFlags.None); + await mock.Received().HashDeleteAsync("prefix:key", hashFields, CommandFlags.None); } [Fact] - public void HashExistsAsync() + public async Task HashExistsAsync() { - prefixed.HashExistsAsync("key", "hashField", CommandFlags.None); - mock.Verify(_ => _.HashExistsAsync("prefix:key", "hashField", CommandFlags.None)); + await prefixed.HashExistsAsync("key", "hashField", CommandFlags.None); + await mock.Received().HashExistsAsync("prefix:key", "hashField", CommandFlags.None); } [Fact] - public void HashGetAllAsync() + public async Task HashGetAllAsync() { - prefixed.HashGetAllAsync("key", CommandFlags.None); - mock.Verify(_ => _.HashGetAllAsync("prefix:key", CommandFlags.None)); + await prefixed.HashGetAllAsync("key", CommandFlags.None); + await mock.Received().HashGetAllAsync("prefix:key", CommandFlags.None); } [Fact] - public void HashGetAsync_1() + public async Task HashGetAsync_1() { - prefixed.HashGetAsync("key", "hashField", CommandFlags.None); - mock.Verify(_ => _.HashGetAsync("prefix:key", "hashField", CommandFlags.None)); + await prefixed.HashGetAsync("key", "hashField", CommandFlags.None); + await mock.Received().HashGetAsync("prefix:key", "hashField", CommandFlags.None); } [Fact] - public void HashGetAsync_2() + public async Task HashGetAsync_2() { RedisValue[] hashFields = Array.Empty(); - prefixed.HashGetAsync("key", hashFields, CommandFlags.None); - mock.Verify(_ => _.HashGetAsync("prefix:key", hashFields, CommandFlags.None)); + await prefixed.HashGetAsync("key", hashFields, CommandFlags.None); + await mock.Received().HashGetAsync("prefix:key", hashFields, CommandFlags.None); } [Fact] - public void HashIncrementAsync_1() + public async Task HashIncrementAsync_1() { - prefixed.HashIncrementAsync("key", "hashField", 123, CommandFlags.None); - mock.Verify(_ => _.HashIncrementAsync("prefix:key", "hashField", 123, CommandFlags.None)); + await prefixed.HashIncrementAsync("key", "hashField", 123, CommandFlags.None); + await mock.Received().HashIncrementAsync("prefix:key", "hashField", 123, CommandFlags.None); } [Fact] - public void HashIncrementAsync_2() + public async Task HashIncrementAsync_2() { - prefixed.HashIncrementAsync("key", "hashField", 1.23, CommandFlags.None); - mock.Verify(_ => _.HashIncrementAsync("prefix:key", "hashField", 1.23, CommandFlags.None)); + await prefixed.HashIncrementAsync("key", "hashField", 1.23, CommandFlags.None); + await mock.Received().HashIncrementAsync("prefix:key", "hashField", 1.23, CommandFlags.None); } [Fact] - public void HashKeysAsync() + public async Task HashKeysAsync() { - prefixed.HashKeysAsync("key", CommandFlags.None); - mock.Verify(_ => _.HashKeysAsync("prefix:key", CommandFlags.None)); + await prefixed.HashKeysAsync("key", CommandFlags.None); + await mock.Received().HashKeysAsync("prefix:key", CommandFlags.None); } [Fact] - public void HashLengthAsync() + public async Task HashLengthAsync() { - prefixed.HashLengthAsync("key", CommandFlags.None); - mock.Verify(_ => _.HashLengthAsync("prefix:key", CommandFlags.None)); + await prefixed.HashLengthAsync("key", CommandFlags.None); + await mock.Received().HashLengthAsync("prefix:key", CommandFlags.None); } [Fact] - public void HashSetAsync_1() + public async Task HashSetAsync_1() { HashEntry[] hashFields = Array.Empty(); - prefixed.HashSetAsync("key", hashFields, CommandFlags.None); - mock.Verify(_ => _.HashSetAsync("prefix:key", hashFields, CommandFlags.None)); + await prefixed.HashSetAsync("key", hashFields, CommandFlags.None); + await mock.Received().HashSetAsync("prefix:key", hashFields, CommandFlags.None); } [Fact] - public void HashSetAsync_2() + public async Task HashSetAsync_2() { - prefixed.HashSetAsync("key", "hashField", "value", When.Exists, CommandFlags.None); - mock.Verify(_ => _.HashSetAsync("prefix:key", "hashField", "value", When.Exists, CommandFlags.None)); + await prefixed.HashSetAsync("key", "hashField", "value", When.Exists, CommandFlags.None); + await mock.Received().HashSetAsync("prefix:key", "hashField", "value", When.Exists, CommandFlags.None); } [Fact] - public void HashStringLengthAsync() + public async Task HashStringLengthAsync() { - prefixed.HashStringLengthAsync("key","field", CommandFlags.None); - mock.Verify(_ => _.HashStringLengthAsync("prefix:key", "field", CommandFlags.None)); + await prefixed.HashStringLengthAsync("key","field", CommandFlags.None); + await mock.Received().HashStringLengthAsync("prefix:key", "field", CommandFlags.None); } [Fact] - public void HashValuesAsync() + public async Task HashValuesAsync() { - prefixed.HashValuesAsync("key", CommandFlags.None); - mock.Verify(_ => _.HashValuesAsync("prefix:key", CommandFlags.None)); + await prefixed.HashValuesAsync("key", CommandFlags.None); + await mock.Received().HashValuesAsync("prefix:key", CommandFlags.None); } [Fact] - public void HyperLogLogAddAsync_1() + public async Task HyperLogLogAddAsync_1() { - prefixed.HyperLogLogAddAsync("key", "value", CommandFlags.None); - mock.Verify(_ => _.HyperLogLogAddAsync("prefix:key", "value", CommandFlags.None)); + await prefixed.HyperLogLogAddAsync("key", "value", CommandFlags.None); + await mock.Received().HyperLogLogAddAsync("prefix:key", "value", CommandFlags.None); } [Fact] - public void HyperLogLogAddAsync_2() + public async Task HyperLogLogAddAsync_2() { var values = Array.Empty(); - prefixed.HyperLogLogAddAsync("key", values, CommandFlags.None); - mock.Verify(_ => _.HyperLogLogAddAsync("prefix:key", values, CommandFlags.None)); + await prefixed.HyperLogLogAddAsync("key", values, CommandFlags.None); + await mock.Received().HyperLogLogAddAsync("prefix:key", values, CommandFlags.None); } [Fact] - public void HyperLogLogLengthAsync() + public async Task HyperLogLogLengthAsync() { - prefixed.HyperLogLogLengthAsync("key", CommandFlags.None); - mock.Verify(_ => _.HyperLogLogLengthAsync("prefix:key", CommandFlags.None)); + await prefixed.HyperLogLogLengthAsync("key", CommandFlags.None); + await mock.Received().HyperLogLogLengthAsync("prefix:key", CommandFlags.None); } [Fact] - public void HyperLogLogMergeAsync_1() + public async Task HyperLogLogMergeAsync_1() { - prefixed.HyperLogLogMergeAsync("destination", "first", "second", CommandFlags.None); - mock.Verify(_ => _.HyperLogLogMergeAsync("prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); + await prefixed.HyperLogLogMergeAsync("destination", "first", "second", CommandFlags.None); + await mock.Received().HyperLogLogMergeAsync("prefix:destination", "prefix:first", "prefix:second", CommandFlags.None); } [Fact] - public void HyperLogLogMergeAsync_2() + public async Task HyperLogLogMergeAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - prefixed.HyperLogLogMergeAsync("destination", keys, CommandFlags.None); - mock.Verify(_ => _.HyperLogLogMergeAsync("prefix:destination", It.Is(valid), CommandFlags.None)); + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + await prefixed.HyperLogLogMergeAsync("destination", keys, CommandFlags.None); + await mock.Received().HyperLogLogMergeAsync("prefix:destination", Arg.Is(valid), CommandFlags.None); } [Fact] - public void IdentifyEndpointAsync() + public async Task IdentifyEndpointAsync() { - prefixed.IdentifyEndpointAsync("key", CommandFlags.None); - mock.Verify(_ => _.IdentifyEndpointAsync("prefix:key", CommandFlags.None)); + await prefixed.IdentifyEndpointAsync("key", CommandFlags.None); + await mock.Received().IdentifyEndpointAsync("prefix:key", CommandFlags.None); } [Fact] public void IsConnected() { prefixed.IsConnected("key", CommandFlags.None); - mock.Verify(_ => _.IsConnected("prefix:key", CommandFlags.None)); + mock.Received().IsConnected("prefix:key", CommandFlags.None); } [Fact] - public void KeyCopyAsync() + public async Task KeyCopyAsync() { - prefixed.KeyCopyAsync("key", "destination", flags: CommandFlags.None); - mock.Verify(_ => _.KeyCopyAsync("prefix:key", "prefix:destination", -1, false, CommandFlags.None)); + await prefixed.KeyCopyAsync("key", "destination", flags: CommandFlags.None); + await mock.Received().KeyCopyAsync("prefix:key", "prefix:destination", -1, false, CommandFlags.None); } [Fact] - public void KeyDeleteAsync_1() + public async Task KeyDeleteAsync_1() { - prefixed.KeyDeleteAsync("key", CommandFlags.None); - mock.Verify(_ => _.KeyDeleteAsync("prefix:key", CommandFlags.None)); + await prefixed.KeyDeleteAsync("key", CommandFlags.None); + await mock.Received().KeyDeleteAsync("prefix:key", CommandFlags.None); } [Fact] - public void KeyDeleteAsync_2() + public async Task KeyDeleteAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - prefixed.KeyDeleteAsync(keys, CommandFlags.None); - mock.Verify(_ => _.KeyDeleteAsync(It.Is(valid), CommandFlags.None)); + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + await prefixed.KeyDeleteAsync(keys, CommandFlags.None); + await mock.Received().KeyDeleteAsync(Arg.Is(valid), CommandFlags.None); } [Fact] - public void KeyDumpAsync() + public async Task KeyDumpAsync() { - prefixed.KeyDumpAsync("key", CommandFlags.None); - mock.Verify(_ => _.KeyDumpAsync("prefix:key", CommandFlags.None)); + await prefixed.KeyDumpAsync("key", CommandFlags.None); + await mock.Received().KeyDumpAsync("prefix:key", CommandFlags.None); } [Fact] - public void KeyEncodingAsync() + public async Task KeyEncodingAsync() { - prefixed.KeyEncodingAsync("key", CommandFlags.None); - mock.Verify(_ => _.KeyEncodingAsync("prefix:key", CommandFlags.None)); + await prefixed.KeyEncodingAsync("key", CommandFlags.None); + await mock.Received().KeyEncodingAsync("prefix:key", CommandFlags.None); } [Fact] - public void KeyExistsAsync() + public async Task KeyExistsAsync() { - prefixed.KeyExistsAsync("key", CommandFlags.None); - mock.Verify(_ => _.KeyExistsAsync("prefix:key", CommandFlags.None)); + await prefixed.KeyExistsAsync("key", CommandFlags.None); + await mock.Received().KeyExistsAsync("prefix:key", CommandFlags.None); } [Fact] - public void KeyExpireAsync_1() + public async Task KeyExpireAsync_1() { TimeSpan expiry = TimeSpan.FromSeconds(123); - prefixed.KeyExpireAsync("key", expiry, CommandFlags.None); - mock.Verify(_ => _.KeyExpireAsync("prefix:key", expiry, CommandFlags.None)); + await prefixed.KeyExpireAsync("key", expiry, CommandFlags.None); + await mock.Received().KeyExpireAsync("prefix:key", expiry, CommandFlags.None); } [Fact] - public void KeyExpireAsync_2() + public async Task KeyExpireAsync_2() { DateTime expiry = DateTime.Now; - prefixed.KeyExpireAsync("key", expiry, CommandFlags.None); - mock.Verify(_ => _.KeyExpireAsync("prefix:key", expiry, CommandFlags.None)); + await prefixed.KeyExpireAsync("key", expiry, CommandFlags.None); + await mock.Received().KeyExpireAsync("prefix:key", expiry, CommandFlags.None); } [Fact] - public void KeyExpireAsync_3() + public async Task KeyExpireAsync_3() { TimeSpan expiry = TimeSpan.FromSeconds(123); - prefixed.KeyExpireAsync("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); - mock.Verify(_ => _.KeyExpireAsync("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); + await prefixed.KeyExpireAsync("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); + await mock.Received().KeyExpireAsync("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); } [Fact] - public void KeyExpireAsync_4() + public async Task KeyExpireAsync_4() { DateTime expiry = DateTime.Now; - prefixed.KeyExpireAsync("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); - mock.Verify(_ => _.KeyExpireAsync("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None)); + await prefixed.KeyExpireAsync("key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); + await mock.Received().KeyExpireAsync("prefix:key", expiry, ExpireWhen.HasNoExpiry, CommandFlags.None); } [Fact] - public void KeyExpireTimeAsync() + public async Task KeyExpireTimeAsync() { - prefixed.KeyExpireTimeAsync("key", CommandFlags.None); - mock.Verify(_ => _.KeyExpireTimeAsync("prefix:key", CommandFlags.None)); + await prefixed.KeyExpireTimeAsync("key", CommandFlags.None); + await mock.Received().KeyExpireTimeAsync("prefix:key", CommandFlags.None); } [Fact] - public void KeyFrequencyAsync() + public async Task KeyFrequencyAsync() { - prefixed.KeyFrequencyAsync("key", CommandFlags.None); - mock.Verify(_ => _.KeyFrequencyAsync("prefix:key", CommandFlags.None)); + await prefixed.KeyFrequencyAsync("key", CommandFlags.None); + await mock.Received().KeyFrequencyAsync("prefix:key", CommandFlags.None); } [Fact] - public void KeyMigrateAsync() + public async Task KeyMigrateAsync() { EndPoint toServer = new IPEndPoint(IPAddress.Loopback, 123); - prefixed.KeyMigrateAsync("key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None); - mock.Verify(_ => _.KeyMigrateAsync("prefix:key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None)); + await prefixed.KeyMigrateAsync("key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None); + await mock.Received().KeyMigrateAsync("prefix:key", toServer, 123, 456, MigrateOptions.Copy, CommandFlags.None); } [Fact] - public void KeyMoveAsync() + public async Task KeyMoveAsync() { - prefixed.KeyMoveAsync("key", 123, CommandFlags.None); - mock.Verify(_ => _.KeyMoveAsync("prefix:key", 123, CommandFlags.None)); + await prefixed.KeyMoveAsync("key", 123, CommandFlags.None); + await mock.Received().KeyMoveAsync("prefix:key", 123, CommandFlags.None); } [Fact] - public void KeyPersistAsync() + public async Task KeyPersistAsync() { - prefixed.KeyPersistAsync("key", CommandFlags.None); - mock.Verify(_ => _.KeyPersistAsync("prefix:key", CommandFlags.None)); + await prefixed.KeyPersistAsync("key", CommandFlags.None); + await mock.Received().KeyPersistAsync("prefix:key", CommandFlags.None); } [Fact] @@ -315,1006 +315,1007 @@ public Task KeyRandomAsync() } [Fact] - public void KeyRefCountAsync() + public async Task KeyRefCountAsync() { - prefixed.KeyRefCountAsync("key", CommandFlags.None); - mock.Verify(_ => _.KeyRefCountAsync("prefix:key", CommandFlags.None)); + await prefixed.KeyRefCountAsync("key", CommandFlags.None); + await mock.Received().KeyRefCountAsync("prefix:key", CommandFlags.None); } [Fact] - public void KeyRenameAsync() + public async Task KeyRenameAsync() { - prefixed.KeyRenameAsync("key", "newKey", When.Exists, CommandFlags.None); - mock.Verify(_ => _.KeyRenameAsync("prefix:key", "prefix:newKey", When.Exists, CommandFlags.None)); + await prefixed.KeyRenameAsync("key", "newKey", When.Exists, CommandFlags.None); + await mock.Received().KeyRenameAsync("prefix:key", "prefix:newKey", When.Exists, CommandFlags.None); } [Fact] - public void KeyRestoreAsync() + public async Task KeyRestoreAsync() { byte[] value = Array.Empty(); TimeSpan expiry = TimeSpan.FromSeconds(123); - prefixed.KeyRestoreAsync("key", value, expiry, CommandFlags.None); - mock.Verify(_ => _.KeyRestoreAsync("prefix:key", value, expiry, CommandFlags.None)); + await prefixed.KeyRestoreAsync("key", value, expiry, CommandFlags.None); + await mock.Received().KeyRestoreAsync("prefix:key", value, expiry, CommandFlags.None); } [Fact] - public void KeyTimeToLiveAsync() + public async Task KeyTimeToLiveAsync() { - prefixed.KeyTimeToLiveAsync("key", CommandFlags.None); - mock.Verify(_ => _.KeyTimeToLiveAsync("prefix:key", CommandFlags.None)); + await prefixed.KeyTimeToLiveAsync("key", CommandFlags.None); + await mock.Received().KeyTimeToLiveAsync("prefix:key", CommandFlags.None); } [Fact] - public void KeyTypeAsync() + public async Task KeyTypeAsync() { - prefixed.KeyTypeAsync("key", CommandFlags.None); - mock.Verify(_ => _.KeyTypeAsync("prefix:key", CommandFlags.None)); + await prefixed.KeyTypeAsync("key", CommandFlags.None); + await mock.Received().KeyTypeAsync("prefix:key", CommandFlags.None); } [Fact] - public void ListGetByIndexAsync() + public async Task ListGetByIndexAsync() { - prefixed.ListGetByIndexAsync("key", 123, CommandFlags.None); - mock.Verify(_ => _.ListGetByIndexAsync("prefix:key", 123, CommandFlags.None)); + await prefixed.ListGetByIndexAsync("key", 123, CommandFlags.None); + await mock.Received().ListGetByIndexAsync("prefix:key", 123, CommandFlags.None); } [Fact] - public void ListInsertAfterAsync() + public async Task ListInsertAfterAsync() { - prefixed.ListInsertAfterAsync("key", "pivot", "value", CommandFlags.None); - mock.Verify(_ => _.ListInsertAfterAsync("prefix:key", "pivot", "value", CommandFlags.None)); + await prefixed.ListInsertAfterAsync("key", "pivot", "value", CommandFlags.None); + await mock.Received().ListInsertAfterAsync("prefix:key", "pivot", "value", CommandFlags.None); } [Fact] - public void ListInsertBeforeAsync() + public async Task ListInsertBeforeAsync() { - prefixed.ListInsertBeforeAsync("key", "pivot", "value", CommandFlags.None); - mock.Verify(_ => _.ListInsertBeforeAsync("prefix:key", "pivot", "value", CommandFlags.None)); + await prefixed.ListInsertBeforeAsync("key", "pivot", "value", CommandFlags.None); + await mock.Received().ListInsertBeforeAsync("prefix:key", "pivot", "value", CommandFlags.None); } [Fact] - public void ListLeftPopAsync() + public async Task ListLeftPopAsync() { - prefixed.ListLeftPopAsync("key", CommandFlags.None); - mock.Verify(_ => _.ListLeftPopAsync("prefix:key", CommandFlags.None)); + await prefixed.ListLeftPopAsync("key", CommandFlags.None); + await mock.Received().ListLeftPopAsync("prefix:key", CommandFlags.None); } [Fact] - public void ListLeftPopAsync_1() + public async Task ListLeftPopAsync_1() { - prefixed.ListLeftPopAsync("key", 123, CommandFlags.None); - mock.Verify(_ => _.ListLeftPopAsync("prefix:key", 123, CommandFlags.None)); + await prefixed.ListLeftPopAsync("key", 123, CommandFlags.None); + await mock.Received().ListLeftPopAsync("prefix:key", 123, CommandFlags.None); } [Fact] - public void ListLeftPushAsync_1() + public async Task ListLeftPushAsync_1() { - prefixed.ListLeftPushAsync("key", "value", When.Exists, CommandFlags.None); - mock.Verify(_ => _.ListLeftPushAsync("prefix:key", "value", When.Exists, CommandFlags.None)); + await prefixed.ListLeftPushAsync("key", "value", When.Exists, CommandFlags.None); + await mock.Received().ListLeftPushAsync("prefix:key", "value", When.Exists, CommandFlags.None); } [Fact] - public void ListLeftPushAsync_2() + public async Task ListLeftPushAsync_2() { RedisValue[] values = Array.Empty(); - prefixed.ListLeftPushAsync("key", values, CommandFlags.None); - mock.Verify(_ => _.ListLeftPushAsync("prefix:key", values, CommandFlags.None)); + await prefixed.ListLeftPushAsync("key", values, CommandFlags.None); + await mock.Received().ListLeftPushAsync("prefix:key", values, CommandFlags.None); } [Fact] - public void ListLeftPushAsync_3() + public async Task ListLeftPushAsync_3() { RedisValue[] values = new RedisValue[] { "value1", "value2" }; - prefixed.ListLeftPushAsync("key", values, When.Exists, CommandFlags.None); - mock.Verify(_ => _.ListLeftPushAsync("prefix:key", values, When.Exists, CommandFlags.None)); + await prefixed.ListLeftPushAsync("key", values, When.Exists, CommandFlags.None); + await mock.Received().ListLeftPushAsync("prefix:key", values, When.Exists, CommandFlags.None); } [Fact] - public void ListLengthAsync() + public async Task ListLengthAsync() { - prefixed.ListLengthAsync("key", CommandFlags.None); - mock.Verify(_ => _.ListLengthAsync("prefix:key", CommandFlags.None)); + await prefixed.ListLengthAsync("key", CommandFlags.None); + await mock.Received().ListLengthAsync("prefix:key", CommandFlags.None); } [Fact] - public void ListMoveAsync() + public async Task ListMoveAsync() { - prefixed.ListMoveAsync("key", "destination", ListSide.Left, ListSide.Right, CommandFlags.None); - mock.Verify(_ => _.ListMoveAsync("prefix:key", "prefix:destination", ListSide.Left, ListSide.Right, CommandFlags.None)); + await prefixed.ListMoveAsync("key", "destination", ListSide.Left, ListSide.Right, CommandFlags.None); + await mock.Received().ListMoveAsync("prefix:key", "prefix:destination", ListSide.Left, ListSide.Right, CommandFlags.None); } [Fact] - public void ListRangeAsync() + public async Task ListRangeAsync() { - prefixed.ListRangeAsync("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.ListRangeAsync("prefix:key", 123, 456, CommandFlags.None)); + await prefixed.ListRangeAsync("key", 123, 456, CommandFlags.None); + await mock.Received().ListRangeAsync("prefix:key", 123, 456, CommandFlags.None); } [Fact] - public void ListRemoveAsync() + public async Task ListRemoveAsync() { - prefixed.ListRemoveAsync("key", "value", 123, CommandFlags.None); - mock.Verify(_ => _.ListRemoveAsync("prefix:key", "value", 123, CommandFlags.None)); + await prefixed.ListRemoveAsync("key", "value", 123, CommandFlags.None); + await mock.Received().ListRemoveAsync("prefix:key", "value", 123, CommandFlags.None); } [Fact] - public void ListRightPopAsync() + public async Task ListRightPopAsync() { - prefixed.ListRightPopAsync("key", CommandFlags.None); - mock.Verify(_ => _.ListRightPopAsync("prefix:key", CommandFlags.None)); + await prefixed.ListRightPopAsync("key", CommandFlags.None); + await mock.Received().ListRightPopAsync("prefix:key", CommandFlags.None); } [Fact] - public void ListRightPopAsync_1() + public async Task ListRightPopAsync_1() { - prefixed.ListRightPopAsync("key", 123, CommandFlags.None); - mock.Verify(_ => _.ListRightPopAsync("prefix:key", 123, CommandFlags.None)); + await prefixed.ListRightPopAsync("key", 123, CommandFlags.None); + await mock.Received().ListRightPopAsync("prefix:key", 123, CommandFlags.None); } [Fact] - public void ListRightPopLeftPushAsync() + public async Task ListRightPopLeftPushAsync() { - prefixed.ListRightPopLeftPushAsync("source", "destination", CommandFlags.None); - mock.Verify(_ => _.ListRightPopLeftPushAsync("prefix:source", "prefix:destination", CommandFlags.None)); + await prefixed.ListRightPopLeftPushAsync("source", "destination", CommandFlags.None); + await mock.Received().ListRightPopLeftPushAsync("prefix:source", "prefix:destination", CommandFlags.None); } [Fact] - public void ListRightPushAsync_1() + public async Task ListRightPushAsync_1() { - prefixed.ListRightPushAsync("key", "value", When.Exists, CommandFlags.None); - mock.Verify(_ => _.ListRightPushAsync("prefix:key", "value", When.Exists, CommandFlags.None)); + await prefixed.ListRightPushAsync("key", "value", When.Exists, CommandFlags.None); + await mock.Received().ListRightPushAsync("prefix:key", "value", When.Exists, CommandFlags.None); } [Fact] - public void ListRightPushAsync_2() + public async Task ListRightPushAsync_2() { RedisValue[] values = Array.Empty(); - prefixed.ListRightPushAsync("key", values, CommandFlags.None); - mock.Verify(_ => _.ListRightPushAsync("prefix:key", values, CommandFlags.None)); + await prefixed.ListRightPushAsync("key", values, CommandFlags.None); + await mock.Received().ListRightPushAsync("prefix:key", values, CommandFlags.None); } [Fact] - public void ListRightPushAsync_3() + public async Task ListRightPushAsync_3() { RedisValue[] values = new RedisValue[] { "value1", "value2" }; - prefixed.ListRightPushAsync("key", values, When.Exists, CommandFlags.None); - mock.Verify(_ => _.ListRightPushAsync("prefix:key", values, When.Exists, CommandFlags.None)); + await prefixed.ListRightPushAsync("key", values, When.Exists, CommandFlags.None); + await mock.Received().ListRightPushAsync("prefix:key", values, When.Exists, CommandFlags.None); } [Fact] - public void ListSetByIndexAsync() + public async Task ListSetByIndexAsync() { - prefixed.ListSetByIndexAsync("key", 123, "value", CommandFlags.None); - mock.Verify(_ => _.ListSetByIndexAsync("prefix:key", 123, "value", CommandFlags.None)); + await prefixed.ListSetByIndexAsync("key", 123, "value", CommandFlags.None); + await mock.Received().ListSetByIndexAsync("prefix:key", 123, "value", CommandFlags.None); } [Fact] - public void ListTrimAsync() + public async Task ListTrimAsync() { - prefixed.ListTrimAsync("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.ListTrimAsync("prefix:key", 123, 456, CommandFlags.None)); + await prefixed.ListTrimAsync("key", 123, 456, CommandFlags.None); + await mock.Received().ListTrimAsync("prefix:key", 123, 456, CommandFlags.None); } [Fact] - public void LockExtendAsync() + public async Task LockExtendAsync() { TimeSpan expiry = TimeSpan.FromSeconds(123); - prefixed.LockExtendAsync("key", "value", expiry, CommandFlags.None); - mock.Verify(_ => _.LockExtendAsync("prefix:key", "value", expiry, CommandFlags.None)); + await prefixed.LockExtendAsync("key", "value", expiry, CommandFlags.None); + await mock.Received().LockExtendAsync("prefix:key", "value", expiry, CommandFlags.None); } [Fact] - public void LockQueryAsync() + public async Task LockQueryAsync() { - prefixed.LockQueryAsync("key", CommandFlags.None); - mock.Verify(_ => _.LockQueryAsync("prefix:key", CommandFlags.None)); + await prefixed.LockQueryAsync("key", CommandFlags.None); + await mock.Received().LockQueryAsync("prefix:key", CommandFlags.None); } [Fact] - public void LockReleaseAsync() + public async Task LockReleaseAsync() { - prefixed.LockReleaseAsync("key", "value", CommandFlags.None); - mock.Verify(_ => _.LockReleaseAsync("prefix:key", "value", CommandFlags.None)); + await prefixed.LockReleaseAsync("key", "value", CommandFlags.None); + await mock.Received().LockReleaseAsync("prefix:key", "value", CommandFlags.None); } [Fact] - public void LockTakeAsync() + public async Task LockTakeAsync() { TimeSpan expiry = TimeSpan.FromSeconds(123); - prefixed.LockTakeAsync("key", "value", expiry, CommandFlags.None); - mock.Verify(_ => _.LockTakeAsync("prefix:key", "value", expiry, CommandFlags.None)); + await prefixed.LockTakeAsync("key", "value", expiry, CommandFlags.None); + await mock.Received().LockTakeAsync("prefix:key", "value", expiry, CommandFlags.None); } [Fact] - public void PublishAsync() + public async Task PublishAsync() { - prefixed.PublishAsync(RedisChannel.Literal("channel"), "message", CommandFlags.None); - mock.Verify(_ => _.PublishAsync(RedisChannel.Literal("prefix:channel"), "message", CommandFlags.None)); + await prefixed.PublishAsync(RedisChannel.Literal("channel"), "message", CommandFlags.None); + await mock.Received().PublishAsync(RedisChannel.Literal("prefix:channel"), "message", CommandFlags.None); } [Fact] - public void ScriptEvaluateAsync_1() + public async Task ScriptEvaluateAsync_1() { byte[] hash = Array.Empty(); RedisValue[] values = Array.Empty(); RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - prefixed.ScriptEvaluateAsync(hash, keys, values, CommandFlags.None); - mock.Verify(_ => _.ScriptEvaluateAsync(hash, It.Is(valid), values, CommandFlags.None)); + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + await prefixed.ScriptEvaluateAsync(hash, keys, values, CommandFlags.None); + await mock.Received().ScriptEvaluateAsync(hash, Arg.Is(valid), values, CommandFlags.None); } [Fact] - public void ScriptEvaluateAsync_2() + public async Task ScriptEvaluateAsync_2() { RedisValue[] values = Array.Empty(); RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - prefixed.ScriptEvaluateAsync("script", keys, values, CommandFlags.None); - mock.Verify(_ => _.ScriptEvaluateAsync("script", It.Is(valid), values, CommandFlags.None)); + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + await prefixed.ScriptEvaluateAsync("script", keys, values, CommandFlags.None); + await mock.Received().ScriptEvaluateAsync("script", Arg.Is(valid), values, CommandFlags.None); } [Fact] - public void SetAddAsync_1() + public async Task SetAddAsync_1() { - prefixed.SetAddAsync("key", "value", CommandFlags.None); - mock.Verify(_ => _.SetAddAsync("prefix:key", "value", CommandFlags.None)); + await prefixed.SetAddAsync("key", "value", CommandFlags.None); + await mock.Received().SetAddAsync("prefix:key", "value", CommandFlags.None); } [Fact] - public void SetAddAsync_2() + public async Task SetAddAsync_2() { RedisValue[] values = Array.Empty(); - prefixed.SetAddAsync("key", values, CommandFlags.None); - mock.Verify(_ => _.SetAddAsync("prefix:key", values, CommandFlags.None)); + await prefixed.SetAddAsync("key", values, CommandFlags.None); + await mock.Received().SetAddAsync("prefix:key", values, CommandFlags.None); } [Fact] - public void SetCombineAndStoreAsync_1() + public async Task SetCombineAndStoreAsync_1() { - prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", "first", "second", CommandFlags.None); - mock.Verify(_ => _.SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); + await prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", "first", "second", CommandFlags.None); + await mock.Received().SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None); } [Fact] - public void SetCombineAndStoreAsync_2() + public async Task SetCombineAndStoreAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", keys, CommandFlags.None); - mock.Verify(_ => _.SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", It.Is(valid), CommandFlags.None)); + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + await prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", keys, CommandFlags.None); + await mock.Received().SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None); } [Fact] - public void SetCombineAsync_1() + public async Task SetCombineAsync_1() { - prefixed.SetCombineAsync(SetOperation.Intersect, "first", "second", CommandFlags.None); - mock.Verify(_ => _.SetCombineAsync(SetOperation.Intersect, "prefix:first", "prefix:second", CommandFlags.None)); + await prefixed.SetCombineAsync(SetOperation.Intersect, "first", "second", CommandFlags.None); + await mock.Received().SetCombineAsync(SetOperation.Intersect, "prefix:first", "prefix:second", CommandFlags.None); } [Fact] - public void SetCombineAsync_2() + public async Task SetCombineAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - prefixed.SetCombineAsync(SetOperation.Intersect, keys, CommandFlags.None); - mock.Verify(_ => _.SetCombineAsync(SetOperation.Intersect, It.Is(valid), CommandFlags.None)); + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + await prefixed.SetCombineAsync(SetOperation.Intersect, keys, CommandFlags.None); + await mock.Received().SetCombineAsync(SetOperation.Intersect, Arg.Is(valid), CommandFlags.None); } [Fact] - public void SetContainsAsync() + public async Task SetContainsAsync() { - prefixed.SetContainsAsync("key", "value", CommandFlags.None); - mock.Verify(_ => _.SetContainsAsync("prefix:key", "value", CommandFlags.None)); + await prefixed.SetContainsAsync("key", "value", CommandFlags.None); + await mock.Received().SetContainsAsync("prefix:key", "value", CommandFlags.None); } [Fact] - public void SetContainsAsync_2() + public async Task SetContainsAsync_2() { RedisValue[] values = new RedisValue[] { "value1", "value2" }; - prefixed.SetContainsAsync("key", values, CommandFlags.None); - mock.Verify(_ => _.SetContainsAsync("prefix:key", values, CommandFlags.None)); + await prefixed.SetContainsAsync("key", values, CommandFlags.None); + await mock.Received().SetContainsAsync("prefix:key", values, CommandFlags.None); } [Fact] - public void SetIntersectionLengthAsync() + public async Task SetIntersectionLengthAsync() { var keys = new RedisKey[] { "key1", "key2" }; - prefixed.SetIntersectionLengthAsync(keys); - mock.Verify(_ => _.SetIntersectionLengthAsync(keys, 0, CommandFlags.None)); + await prefixed.SetIntersectionLengthAsync(keys); + await mock.Received().SetIntersectionLengthAsync(keys, 0, CommandFlags.None); } [Fact] - public void SetLengthAsync() + public async Task SetLengthAsync() { - prefixed.SetLengthAsync("key", CommandFlags.None); - mock.Verify(_ => _.SetLengthAsync("prefix:key", CommandFlags.None)); + await prefixed.SetLengthAsync("key", CommandFlags.None); + await mock.Received().SetLengthAsync("prefix:key", CommandFlags.None); } [Fact] - public void SetMembersAsync() + public async Task SetMembersAsync() { - prefixed.SetMembersAsync("key", CommandFlags.None); - mock.Verify(_ => _.SetMembersAsync("prefix:key", CommandFlags.None)); + await prefixed.SetMembersAsync("key", CommandFlags.None); + await mock.Received().SetMembersAsync("prefix:key", CommandFlags.None); } [Fact] - public void SetMoveAsync() + public async Task SetMoveAsync() { - prefixed.SetMoveAsync("source", "destination", "value", CommandFlags.None); - mock.Verify(_ => _.SetMoveAsync("prefix:source", "prefix:destination", "value", CommandFlags.None)); + await prefixed.SetMoveAsync("source", "destination", "value", CommandFlags.None); + await mock.Received().SetMoveAsync("prefix:source", "prefix:destination", "value", CommandFlags.None); } [Fact] - public void SetPopAsync_1() + public async Task SetPopAsync_1() { - prefixed.SetPopAsync("key", CommandFlags.None); - mock.Verify(_ => _.SetPopAsync("prefix:key", CommandFlags.None)); + await prefixed.SetPopAsync("key", CommandFlags.None); + await mock.Received().SetPopAsync("prefix:key", CommandFlags.None); - prefixed.SetPopAsync("key", 5, CommandFlags.None); - mock.Verify(_ => _.SetPopAsync("prefix:key", 5, CommandFlags.None)); + await prefixed.SetPopAsync("key", 5, CommandFlags.None); + await mock.Received().SetPopAsync("prefix:key", 5, CommandFlags.None); } [Fact] - public void SetPopAsync_2() + public async Task SetPopAsync_2() { - prefixed.SetPopAsync("key", 5, CommandFlags.None); - mock.Verify(_ => _.SetPopAsync("prefix:key", 5, CommandFlags.None)); + await prefixed.SetPopAsync("key", 5, CommandFlags.None); + await mock.Received().SetPopAsync("prefix:key", 5, CommandFlags.None); } [Fact] - public void SetRandomMemberAsync() + public async Task SetRandomMemberAsync() { - prefixed.SetRandomMemberAsync("key", CommandFlags.None); - mock.Verify(_ => _.SetRandomMemberAsync("prefix:key", CommandFlags.None)); + await prefixed.SetRandomMemberAsync("key", CommandFlags.None); + await mock.Received().SetRandomMemberAsync("prefix:key", CommandFlags.None); } [Fact] - public void SetRandomMembersAsync() + public async Task SetRandomMembersAsync() { - prefixed.SetRandomMembersAsync("key", 123, CommandFlags.None); - mock.Verify(_ => _.SetRandomMembersAsync("prefix:key", 123, CommandFlags.None)); + await prefixed.SetRandomMembersAsync("key", 123, CommandFlags.None); + await mock.Received().SetRandomMembersAsync("prefix:key", 123, CommandFlags.None); } [Fact] - public void SetRemoveAsync_1() + public async Task SetRemoveAsync_1() { - prefixed.SetRemoveAsync("key", "value", CommandFlags.None); - mock.Verify(_ => _.SetRemoveAsync("prefix:key", "value", CommandFlags.None)); + await prefixed.SetRemoveAsync("key", "value", CommandFlags.None); + await mock.Received().SetRemoveAsync("prefix:key", "value", CommandFlags.None); } [Fact] - public void SetRemoveAsync_2() + public async Task SetRemoveAsync_2() { RedisValue[] values = Array.Empty(); - prefixed.SetRemoveAsync("key", values, CommandFlags.None); - mock.Verify(_ => _.SetRemoveAsync("prefix:key", values, CommandFlags.None)); + await prefixed.SetRemoveAsync("key", values, CommandFlags.None); + await mock.Received().SetRemoveAsync("prefix:key", values, CommandFlags.None); } [Fact] - public void SortAndStoreAsync() + public async Task SortAndStoreAsync() { RedisValue[] get = new RedisValue[] { "a", "#" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; - prefixed.SortAndStoreAsync("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); - prefixed.SortAndStoreAsync("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); + await prefixed.SortAndStoreAsync("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); + await prefixed.SortAndStoreAsync("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); - mock.Verify(_ => _.SortAndStoreAsync("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", It.Is(valid), CommandFlags.None)); - mock.Verify(_ => _.SortAndStoreAsync("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", It.Is(valid), CommandFlags.None)); + await mock.Received().SortAndStoreAsync("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", Arg.Is(valid), CommandFlags.None); + await mock.Received().SortAndStoreAsync("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", Arg.Is(valid), CommandFlags.None); } [Fact] - public void SortAsync() + public async Task SortAsync() { RedisValue[] get = new RedisValue[] { "a", "#" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; - prefixed.SortAsync("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); - prefixed.SortAsync("key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); + await prefixed.SortAsync("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); + await prefixed.SortAsync("key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); - mock.Verify(_ => _.SortAsync("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", It.Is(valid), CommandFlags.None)); - mock.Verify(_ => _.SortAsync("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", It.Is(valid), CommandFlags.None)); + await mock.Received().SortAsync("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", Arg.Is(valid), CommandFlags.None); + await mock.Received().SortAsync("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", Arg.Is(valid), CommandFlags.None); } [Fact] - public void SortedSetAddAsync_1() + public async Task SortedSetAddAsync_1() { - prefixed.SortedSetAddAsync("key", "member", 1.23, When.Exists, CommandFlags.None); - mock.Verify(_ => _.SortedSetAddAsync("prefix:key", "member", 1.23, When.Exists, CommandFlags.None)); + await prefixed.SortedSetAddAsync("key", "member", 1.23, When.Exists, CommandFlags.None); + await mock.Received().SortedSetAddAsync("prefix:key", "member", 1.23, When.Exists, CommandFlags.None); } [Fact] - public void SortedSetAddAsync_2() + public async Task SortedSetAddAsync_2() { SortedSetEntry[] values = Array.Empty(); - prefixed.SortedSetAddAsync("key", values, When.Exists, CommandFlags.None); - mock.Verify(_ => _.SortedSetAddAsync("prefix:key", values, When.Exists, CommandFlags.None)); + await prefixed.SortedSetAddAsync("key", values, When.Exists, CommandFlags.None); + await mock.Received().SortedSetAddAsync("prefix:key", values, When.Exists, CommandFlags.None); } [Fact] - public void SortedSetAddAsync_3() + public async Task SortedSetAddAsync_3() { SortedSetEntry[] values = Array.Empty(); - prefixed.SortedSetAddAsync("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); - mock.Verify(_ => _.SortedSetAddAsync("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None)); + await prefixed.SortedSetAddAsync("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); + await mock.Received().SortedSetAddAsync("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None); } [Fact] - public void SortedSetCombineAsync() + public async Task SortedSetCombineAsync() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - prefixed.SortedSetCombineAsync(SetOperation.Intersect, keys); - mock.Verify(_ => _.SortedSetCombineAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); + await prefixed.SortedSetCombineAsync(SetOperation.Intersect, keys); + await mock.Received().SortedSetCombineAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None); } [Fact] - public void SortedSetCombineWithScoresAsync() + public async Task SortedSetCombineWithScoresAsync() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - prefixed.SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys); - mock.Verify(_ => _.SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None)); + await prefixed.SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys); + await mock.Received().SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None); } [Fact] - public void SortedSetCombineAndStoreAsync_1() + public async Task SortedSetCombineAndStoreAsync_1() { - prefixed.SortedSetCombineAndStoreAsync(SetOperation.Intersect, "destination", "first", "second", Aggregate.Max, CommandFlags.None); - mock.Verify(_ => _.SortedSetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", Aggregate.Max, CommandFlags.None)); + await prefixed.SortedSetCombineAndStoreAsync(SetOperation.Intersect, "destination", "first", "second", Aggregate.Max, CommandFlags.None); + await mock.Received().SortedSetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", "prefix:first", "prefix:second", Aggregate.Max, CommandFlags.None); } [Fact] - public void SortedSetCombineAndStoreAsync_2() + public async Task SortedSetCombineAndStoreAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", keys, CommandFlags.None); - mock.Verify(_ => _.SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", It.Is(valid), CommandFlags.None)); + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + await prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", keys, CommandFlags.None); + await mock.Received().SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None); } [Fact] - public void SortedSetDecrementAsync() + public async Task SortedSetDecrementAsync() { - prefixed.SortedSetDecrementAsync("key", "member", 1.23, CommandFlags.None); - mock.Verify(_ => _.SortedSetDecrementAsync("prefix:key", "member", 1.23, CommandFlags.None)); + await prefixed.SortedSetDecrementAsync("key", "member", 1.23, CommandFlags.None); + await mock.Received().SortedSetDecrementAsync("prefix:key", "member", 1.23, CommandFlags.None); } [Fact] - public void SortedSetIncrementAsync() + public async Task SortedSetIncrementAsync() { - prefixed.SortedSetIncrementAsync("key", "member", 1.23, CommandFlags.None); - mock.Verify(_ => _.SortedSetIncrementAsync("prefix:key", "member", 1.23, CommandFlags.None)); + await prefixed.SortedSetIncrementAsync("key", "member", 1.23, CommandFlags.None); + await mock.Received().SortedSetIncrementAsync("prefix:key", "member", 1.23, CommandFlags.None); } [Fact] - public void SortedSetIntersectionLengthAsync() + public async Task SortedSetIntersectionLengthAsync() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - prefixed.SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None); - mock.Verify(_ => _.SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None)); + await prefixed.SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None); + await mock.Received().SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None); } [Fact] - public void SortedSetLengthAsync() + public async Task SortedSetLengthAsync() { - prefixed.SortedSetLengthAsync("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); - mock.Verify(_ => _.SortedSetLengthAsync("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None)); + await prefixed.SortedSetLengthAsync("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); + await mock.Received().SortedSetLengthAsync("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None); } [Fact] - public void SortedSetLengthByValueAsync() + public async Task SortedSetLengthByValueAsync() { - prefixed.SortedSetLengthByValueAsync("key", "min", "max", Exclude.Start, CommandFlags.None); - mock.Verify(_ => _.SortedSetLengthByValueAsync("prefix:key", "min", "max", Exclude.Start, CommandFlags.None)); + await prefixed.SortedSetLengthByValueAsync("key", "min", "max", Exclude.Start, CommandFlags.None); + await mock.Received().SortedSetLengthByValueAsync("prefix:key", "min", "max", Exclude.Start, CommandFlags.None); } [Fact] - public void SortedSetRandomMemberAsync() + public async Task SortedSetRandomMemberAsync() { - prefixed.SortedSetRandomMemberAsync("key", CommandFlags.None); - mock.Verify(_ => _.SortedSetRandomMemberAsync("prefix:key", CommandFlags.None)); + await prefixed.SortedSetRandomMemberAsync("key", CommandFlags.None); + await mock.Received().SortedSetRandomMemberAsync("prefix:key", CommandFlags.None); } [Fact] - public void SortedSetRandomMembersAsync() + public async Task SortedSetRandomMembersAsync() { - prefixed.SortedSetRandomMembersAsync("key", 2, CommandFlags.None); - mock.Verify(_ => _.SortedSetRandomMembersAsync("prefix:key", 2, CommandFlags.None)); + await prefixed.SortedSetRandomMembersAsync("key", 2, CommandFlags.None); + await mock.Received().SortedSetRandomMembersAsync("prefix:key", 2, CommandFlags.None); } [Fact] - public void SortedSetRandomMemberWithScoresAsync() + public async Task SortedSetRandomMemberWithScoresAsync() { - prefixed.SortedSetRandomMembersWithScoresAsync("key", 2, CommandFlags.None); - mock.Verify(_ => _.SortedSetRandomMembersWithScoresAsync("prefix:key", 2, CommandFlags.None)); + await prefixed.SortedSetRandomMembersWithScoresAsync("key", 2, CommandFlags.None); + await mock.Received().SortedSetRandomMembersWithScoresAsync("prefix:key", 2, CommandFlags.None); } [Fact] - public void SortedSetRangeByRankAsync() + public async Task SortedSetRangeByRankAsync() { - prefixed.SortedSetRangeByRankAsync("key", 123, 456, Order.Descending, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByRankAsync("prefix:key", 123, 456, Order.Descending, CommandFlags.None)); + await prefixed.SortedSetRangeByRankAsync("key", 123, 456, Order.Descending, CommandFlags.None); + await mock.Received().SortedSetRangeByRankAsync("prefix:key", 123, 456, Order.Descending, CommandFlags.None); } [Fact] - public void SortedSetRangeByRankWithScoresAsync() + public async Task SortedSetRangeByRankWithScoresAsync() { - prefixed.SortedSetRangeByRankWithScoresAsync("key", 123, 456, Order.Descending, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByRankWithScoresAsync("prefix:key", 123, 456, Order.Descending, CommandFlags.None)); + await prefixed.SortedSetRangeByRankWithScoresAsync("key", 123, 456, Order.Descending, CommandFlags.None); + await mock.Received().SortedSetRangeByRankWithScoresAsync("prefix:key", 123, 456, Order.Descending, CommandFlags.None); } [Fact] - public void SortedSetRangeByScoreAsync() + public async Task SortedSetRangeByScoreAsync() { - prefixed.SortedSetRangeByScoreAsync("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByScoreAsync("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); + await prefixed.SortedSetRangeByScoreAsync("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); + await mock.Received().SortedSetRangeByScoreAsync("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); } [Fact] - public void SortedSetRangeByScoreWithScoresAsync() + public async Task SortedSetRangeByScoreWithScoresAsync() { - prefixed.SortedSetRangeByScoreWithScoresAsync("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByScoreWithScoresAsync("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); + await prefixed.SortedSetRangeByScoreWithScoresAsync("key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); + await mock.Received().SortedSetRangeByScoreWithScoresAsync("prefix:key", 1.23, 1.23, Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); } [Fact] - public void SortedSetRangeByValueAsync() + public async Task SortedSetRangeByValueAsync() { - prefixed.SortedSetRangeByValueAsync("key", "min", "max", Exclude.Start, 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByValueAsync("prefix:key", "min", "max", Exclude.Start, Order.Ascending, 123, 456, CommandFlags.None)); + await prefixed.SortedSetRangeByValueAsync("key", "min", "max", Exclude.Start, 123, 456, CommandFlags.None); + await mock.Received().SortedSetRangeByValueAsync("prefix:key", "min", "max", Exclude.Start, Order.Ascending, 123, 456, CommandFlags.None); } [Fact] - public void SortedSetRangeByValueDescAsync() + public async Task SortedSetRangeByValueDescAsync() { - prefixed.SortedSetRangeByValueAsync("key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRangeByValueAsync("prefix:key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None)); + await prefixed.SortedSetRangeByValueAsync("key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); + await mock.Received().SortedSetRangeByValueAsync("prefix:key", "min", "max", Exclude.Start, Order.Descending, 123, 456, CommandFlags.None); } [Fact] - public void SortedSetRankAsync() + public async Task SortedSetRankAsync() { - prefixed.SortedSetRankAsync("key", "member", Order.Descending, CommandFlags.None); - mock.Verify(_ => _.SortedSetRankAsync("prefix:key", "member", Order.Descending, CommandFlags.None)); + await prefixed.SortedSetRankAsync("key", "member", Order.Descending, CommandFlags.None); + await mock.Received().SortedSetRankAsync("prefix:key", "member", Order.Descending, CommandFlags.None); } [Fact] - public void SortedSetRemoveAsync_1() + public async Task SortedSetRemoveAsync_1() { - prefixed.SortedSetRemoveAsync("key", "member", CommandFlags.None); - mock.Verify(_ => _.SortedSetRemoveAsync("prefix:key", "member", CommandFlags.None)); + await prefixed.SortedSetRemoveAsync("key", "member", CommandFlags.None); + await mock.Received().SortedSetRemoveAsync("prefix:key", "member", CommandFlags.None); } [Fact] - public void SortedSetRemoveAsync_2() + public async Task SortedSetRemoveAsync_2() { RedisValue[] members = Array.Empty(); - prefixed.SortedSetRemoveAsync("key", members, CommandFlags.None); - mock.Verify(_ => _.SortedSetRemoveAsync("prefix:key", members, CommandFlags.None)); + await prefixed.SortedSetRemoveAsync("key", members, CommandFlags.None); + await mock.Received().SortedSetRemoveAsync("prefix:key", members, CommandFlags.None); } [Fact] - public void SortedSetRemoveRangeByRankAsync() + public async Task SortedSetRemoveRangeByRankAsync() { - prefixed.SortedSetRemoveRangeByRankAsync("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.SortedSetRemoveRangeByRankAsync("prefix:key", 123, 456, CommandFlags.None)); + await prefixed.SortedSetRemoveRangeByRankAsync("key", 123, 456, CommandFlags.None); + await mock.Received().SortedSetRemoveRangeByRankAsync("prefix:key", 123, 456, CommandFlags.None); } [Fact] - public void SortedSetRemoveRangeByScoreAsync() + public async Task SortedSetRemoveRangeByScoreAsync() { - prefixed.SortedSetRemoveRangeByScoreAsync("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); - mock.Verify(_ => _.SortedSetRemoveRangeByScoreAsync("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None)); + await prefixed.SortedSetRemoveRangeByScoreAsync("key", 1.23, 1.23, Exclude.Start, CommandFlags.None); + await mock.Received().SortedSetRemoveRangeByScoreAsync("prefix:key", 1.23, 1.23, Exclude.Start, CommandFlags.None); } [Fact] - public void SortedSetRemoveRangeByValueAsync() + public async Task SortedSetRemoveRangeByValueAsync() { - prefixed.SortedSetRemoveRangeByValueAsync("key", "min", "max", Exclude.Start, CommandFlags.None); - mock.Verify(_ => _.SortedSetRemoveRangeByValueAsync("prefix:key", "min", "max", Exclude.Start, CommandFlags.None)); + await prefixed.SortedSetRemoveRangeByValueAsync("key", "min", "max", Exclude.Start, CommandFlags.None); + await mock.Received().SortedSetRemoveRangeByValueAsync("prefix:key", "min", "max", Exclude.Start, CommandFlags.None); } [Fact] - public void SortedSetScoreAsync() + public async Task SortedSetScoreAsync() { - prefixed.SortedSetScoreAsync("key", "member", CommandFlags.None); - mock.Verify(_ => _.SortedSetScoreAsync("prefix:key", "member", CommandFlags.None)); + await prefixed.SortedSetScoreAsync("key", "member", CommandFlags.None); + await mock.Received().SortedSetScoreAsync("prefix:key", "member", CommandFlags.None); } [Fact] - public void SortedSetScoreAsync_Multiple() + public async Task SortedSetScoreAsync_Multiple() { - prefixed.SortedSetScoresAsync("key", new RedisValue[] { "member1", "member2" }, CommandFlags.None); - mock.Verify(_ => _.SortedSetScoresAsync("prefix:key", new RedisValue[] { "member1", "member2" }, CommandFlags.None)); + var values = new RedisValue[] { "member1", "member2" }; + await prefixed.SortedSetScoresAsync("key", values, CommandFlags.None); + await mock.Received().SortedSetScoresAsync("prefix:key", values, CommandFlags.None); } [Fact] - public void SortedSetUpdateAsync() + public async Task SortedSetUpdateAsync() { SortedSetEntry[] values = Array.Empty(); - prefixed.SortedSetUpdateAsync("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); - mock.Verify(_ => _.SortedSetUpdateAsync("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None)); + await prefixed.SortedSetUpdateAsync("key", values, SortedSetWhen.GreaterThan, CommandFlags.None); + await mock.Received().SortedSetUpdateAsync("prefix:key", values, SortedSetWhen.GreaterThan, CommandFlags.None); } [Fact] - public void StreamAcknowledgeAsync_1() + public async Task StreamAcknowledgeAsync_1() { - prefixed.StreamAcknowledgeAsync("key", "group", "0-0", CommandFlags.None); - mock.Verify(_ => _.StreamAcknowledgeAsync("prefix:key", "group", "0-0", CommandFlags.None)); + await prefixed.StreamAcknowledgeAsync("key", "group", "0-0", CommandFlags.None); + await mock.Received().StreamAcknowledgeAsync("prefix:key", "group", "0-0", CommandFlags.None); } [Fact] - public void StreamAcknowledgeAsync_2() + public async Task StreamAcknowledgeAsync_2() { var messageIds = new RedisValue[] { "0-0", "0-1", "0-2" }; - prefixed.StreamAcknowledgeAsync("key", "group", messageIds, CommandFlags.None); - mock.Verify(_ => _.StreamAcknowledgeAsync("prefix:key", "group", messageIds, CommandFlags.None)); + await prefixed.StreamAcknowledgeAsync("key", "group", messageIds, CommandFlags.None); + await mock.Received().StreamAcknowledgeAsync("prefix:key", "group", messageIds, CommandFlags.None); } [Fact] - public void StreamAddAsync_1() + public async Task StreamAddAsync_1() { - prefixed.StreamAddAsync("key", "field1", "value1", "*", 1000, true, CommandFlags.None); - mock.Verify(_ => _.StreamAddAsync("prefix:key", "field1", "value1", "*", 1000, true, CommandFlags.None)); + await prefixed.StreamAddAsync("key", "field1", "value1", "*", 1000, true, CommandFlags.None); + await mock.Received().StreamAddAsync("prefix:key", "field1", "value1", "*", 1000, true, CommandFlags.None); } [Fact] - public void StreamAddAsync_2() + public async Task StreamAddAsync_2() { var fields = Array.Empty(); - prefixed.StreamAddAsync("key", fields, "*", 1000, true, CommandFlags.None); - mock.Verify(_ => _.StreamAddAsync("prefix:key", fields, "*", 1000, true, CommandFlags.None)); + await prefixed.StreamAddAsync("key", fields, "*", 1000, true, CommandFlags.None); + await mock.Received().StreamAddAsync("prefix:key", fields, "*", 1000, true, CommandFlags.None); } [Fact] - public void StreamAutoClaimAsync() + public async Task StreamAutoClaimAsync() { - prefixed.StreamAutoClaimAsync("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); - mock.Verify(_ => _.StreamAutoClaimAsync("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None)); + await prefixed.StreamAutoClaimAsync("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); + await mock.Received().StreamAutoClaimAsync("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); } [Fact] - public void StreamAutoClaimIdsOnlyAsync() + public async Task StreamAutoClaimIdsOnlyAsync() { - prefixed.StreamAutoClaimIdsOnlyAsync("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); - mock.Verify(_ => _.StreamAutoClaimIdsOnlyAsync("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None)); + await prefixed.StreamAutoClaimIdsOnlyAsync("key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); + await mock.Received().StreamAutoClaimIdsOnlyAsync("prefix:key", "group", "consumer", 0, "0-0", 100, CommandFlags.None); } [Fact] - public void StreamClaimMessagesAsync() + public async Task StreamClaimMessagesAsync() { var messageIds = Array.Empty(); - prefixed.StreamClaimAsync("key", "group", "consumer", 1000, messageIds, CommandFlags.None); - mock.Verify(_ => _.StreamClaimAsync("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); + await prefixed.StreamClaimAsync("key", "group", "consumer", 1000, messageIds, CommandFlags.None); + await mock.Received().StreamClaimAsync("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None); } [Fact] - public void StreamClaimMessagesReturningIdsAsync() + public async Task StreamClaimMessagesReturningIdsAsync() { var messageIds = Array.Empty(); - prefixed.StreamClaimIdsOnlyAsync("key", "group", "consumer", 1000, messageIds, CommandFlags.None); - mock.Verify(_ => _.StreamClaimIdsOnlyAsync("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None)); + await prefixed.StreamClaimIdsOnlyAsync("key", "group", "consumer", 1000, messageIds, CommandFlags.None); + await mock.Received().StreamClaimIdsOnlyAsync("prefix:key", "group", "consumer", 1000, messageIds, CommandFlags.None); } [Fact] - public void StreamConsumerInfoGetAsync() + public async Task StreamConsumerInfoGetAsync() { - prefixed.StreamConsumerInfoAsync("key", "group", CommandFlags.None); - mock.Verify(_ => _.StreamConsumerInfoAsync("prefix:key", "group", CommandFlags.None)); + await prefixed.StreamConsumerInfoAsync("key", "group", CommandFlags.None); + await mock.Received().StreamConsumerInfoAsync("prefix:key", "group", CommandFlags.None); } [Fact] - public void StreamConsumerGroupSetPositionAsync() + public async Task StreamConsumerGroupSetPositionAsync() { - prefixed.StreamConsumerGroupSetPositionAsync("key", "group", StreamPosition.Beginning, CommandFlags.None); - mock.Verify(_ => _.StreamConsumerGroupSetPositionAsync("prefix:key", "group", StreamPosition.Beginning, CommandFlags.None)); + await prefixed.StreamConsumerGroupSetPositionAsync("key", "group", StreamPosition.Beginning, CommandFlags.None); + await mock.Received().StreamConsumerGroupSetPositionAsync("prefix:key", "group", StreamPosition.Beginning, CommandFlags.None); } [Fact] - public void StreamCreateConsumerGroupAsync() + public async Task StreamCreateConsumerGroupAsync() { - prefixed.StreamCreateConsumerGroupAsync("key", "group", "0-0", false, CommandFlags.None); - mock.Verify(_ => _.StreamCreateConsumerGroupAsync("prefix:key", "group", "0-0", false, CommandFlags.None)); + await prefixed.StreamCreateConsumerGroupAsync("key", "group", "0-0", false, CommandFlags.None); + await mock.Received().StreamCreateConsumerGroupAsync("prefix:key", "group", "0-0", false, CommandFlags.None); } [Fact] - public void StreamGroupInfoGetAsync() + public async Task StreamGroupInfoGetAsync() { - prefixed.StreamGroupInfoAsync("key", CommandFlags.None); - mock.Verify(_ => _.StreamGroupInfoAsync("prefix:key", CommandFlags.None)); + await prefixed.StreamGroupInfoAsync("key", CommandFlags.None); + await mock.Received().StreamGroupInfoAsync("prefix:key", CommandFlags.None); } [Fact] - public void StreamInfoGetAsync() + public async Task StreamInfoGetAsync() { - prefixed.StreamInfoAsync("key", CommandFlags.None); - mock.Verify(_ => _.StreamInfoAsync("prefix:key", CommandFlags.None)); + await prefixed.StreamInfoAsync("key", CommandFlags.None); + await mock.Received().StreamInfoAsync("prefix:key", CommandFlags.None); } [Fact] - public void StreamLengthAsync() + public async Task StreamLengthAsync() { - prefixed.StreamLengthAsync("key", CommandFlags.None); - mock.Verify(_ => _.StreamLengthAsync("prefix:key", CommandFlags.None)); + await prefixed.StreamLengthAsync("key", CommandFlags.None); + await mock.Received().StreamLengthAsync("prefix:key", CommandFlags.None); } [Fact] - public void StreamMessagesDeleteAsync() + public async Task StreamMessagesDeleteAsync() { var messageIds = Array.Empty(); - prefixed.StreamDeleteAsync("key", messageIds, CommandFlags.None); - mock.Verify(_ => _.StreamDeleteAsync("prefix:key", messageIds, CommandFlags.None)); + await prefixed.StreamDeleteAsync("key", messageIds, CommandFlags.None); + await mock.Received().StreamDeleteAsync("prefix:key", messageIds, CommandFlags.None); } [Fact] - public void StreamDeleteConsumerAsync() + public async Task StreamDeleteConsumerAsync() { - prefixed.StreamDeleteConsumerAsync("key", "group", "consumer", CommandFlags.None); - mock.Verify(_ => _.StreamDeleteConsumerAsync("prefix:key", "group", "consumer", CommandFlags.None)); + await prefixed.StreamDeleteConsumerAsync("key", "group", "consumer", CommandFlags.None); + await mock.Received().StreamDeleteConsumerAsync("prefix:key", "group", "consumer", CommandFlags.None); } [Fact] - public void StreamDeleteConsumerGroupAsync() + public async Task StreamDeleteConsumerGroupAsync() { - prefixed.StreamDeleteConsumerGroupAsync("key", "group", CommandFlags.None); - mock.Verify(_ => _.StreamDeleteConsumerGroupAsync("prefix:key", "group", CommandFlags.None)); + await prefixed.StreamDeleteConsumerGroupAsync("key", "group", CommandFlags.None); + await mock.Received().StreamDeleteConsumerGroupAsync("prefix:key", "group", CommandFlags.None); } [Fact] - public void StreamPendingInfoGetAsync() + public async Task StreamPendingInfoGetAsync() { - prefixed.StreamPendingAsync("key", "group", CommandFlags.None); - mock.Verify(_ => _.StreamPendingAsync("prefix:key", "group", CommandFlags.None)); + await prefixed.StreamPendingAsync("key", "group", CommandFlags.None); + await mock.Received().StreamPendingAsync("prefix:key", "group", CommandFlags.None); } [Fact] - public void StreamPendingMessageInfoGetAsync() + public async Task StreamPendingMessageInfoGetAsync() { - prefixed.StreamPendingMessagesAsync("key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); - mock.Verify(_ => _.StreamPendingMessagesAsync("prefix:key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None)); + await prefixed.StreamPendingMessagesAsync("key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); + await mock.Received().StreamPendingMessagesAsync("prefix:key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); } [Fact] - public void StreamRangeAsync() + public async Task StreamRangeAsync() { - prefixed.StreamRangeAsync("key", "-", "+", null, Order.Ascending, CommandFlags.None); - mock.Verify(_ => _.StreamRangeAsync("prefix:key", "-", "+", null, Order.Ascending, CommandFlags.None)); + await prefixed.StreamRangeAsync("key", "-", "+", null, Order.Ascending, CommandFlags.None); + await mock.Received().StreamRangeAsync("prefix:key", "-", "+", null, Order.Ascending, CommandFlags.None); } [Fact] - public void StreamReadAsync_1() + public async Task StreamReadAsync_1() { var streamPositions = Array.Empty(); - prefixed.StreamReadAsync(streamPositions, null, CommandFlags.None); - mock.Verify(_ => _.StreamReadAsync(streamPositions, null, CommandFlags.None)); + await prefixed.StreamReadAsync(streamPositions, null, CommandFlags.None); + await mock.Received().StreamReadAsync(streamPositions, null, CommandFlags.None); } [Fact] - public void StreamReadAsync_2() + public async Task StreamReadAsync_2() { - prefixed.StreamReadAsync("key", "0-0", null, CommandFlags.None); - mock.Verify(_ => _.StreamReadAsync("prefix:key", "0-0", null, CommandFlags.None)); + await prefixed.StreamReadAsync("key", "0-0", null, CommandFlags.None); + await mock.Received().StreamReadAsync("prefix:key", "0-0", null, CommandFlags.None); } [Fact] - public void StreamReadGroupAsync_1() + public async Task StreamReadGroupAsync_1() { - prefixed.StreamReadGroupAsync("key", "group", "consumer", StreamPosition.Beginning, 10, false, CommandFlags.None); - mock.Verify(_ => _.StreamReadGroupAsync("prefix:key", "group", "consumer", StreamPosition.Beginning, 10, false, CommandFlags.None)); + await prefixed.StreamReadGroupAsync("key", "group", "consumer", StreamPosition.Beginning, 10, false, CommandFlags.None); + await mock.Received().StreamReadGroupAsync("prefix:key", "group", "consumer", StreamPosition.Beginning, 10, false, CommandFlags.None); } [Fact] - public void StreamStreamReadGroupAsync_2() + public async Task StreamStreamReadGroupAsync_2() { var streamPositions = Array.Empty(); - prefixed.StreamReadGroupAsync(streamPositions, "group", "consumer", 10, false, CommandFlags.None); - mock.Verify(_ => _.StreamReadGroupAsync(streamPositions, "group", "consumer", 10, false, CommandFlags.None)); + await prefixed.StreamReadGroupAsync(streamPositions, "group", "consumer", 10, false, CommandFlags.None); + await mock.Received().StreamReadGroupAsync(streamPositions, "group", "consumer", 10, false, CommandFlags.None); } [Fact] - public void StreamTrimAsync() + public async Task StreamTrimAsync() { - prefixed.StreamTrimAsync("key", 1000, true, CommandFlags.None); - mock.Verify(_ => _.StreamTrimAsync("prefix:key", 1000, true, CommandFlags.None)); + await prefixed.StreamTrimAsync("key", 1000, true, CommandFlags.None); + await mock.Received().StreamTrimAsync("prefix:key", 1000, true, CommandFlags.None); } [Fact] - public void StringAppendAsync() + public async Task StringAppendAsync() { - prefixed.StringAppendAsync("key", "value", CommandFlags.None); - mock.Verify(_ => _.StringAppendAsync("prefix:key", "value", CommandFlags.None)); + await prefixed.StringAppendAsync("key", "value", CommandFlags.None); + await mock.Received().StringAppendAsync("prefix:key", "value", CommandFlags.None); } [Fact] - public void StringBitCountAsync() + public async Task StringBitCountAsync() { - prefixed.StringBitCountAsync("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.StringBitCountAsync("prefix:key", 123, 456, CommandFlags.None)); + await prefixed.StringBitCountAsync("key", 123, 456, CommandFlags.None); + await mock.Received().StringBitCountAsync("prefix:key", 123, 456, CommandFlags.None); } [Fact] - public void StringBitCountAsync_2() + public async Task StringBitCountAsync_2() { - prefixed.StringBitCountAsync("key", 123, 456, StringIndexType.Byte, CommandFlags.None); - mock.Verify(_ => _.StringBitCountAsync("prefix:key", 123, 456, StringIndexType.Byte, CommandFlags.None)); + await prefixed.StringBitCountAsync("key", 123, 456, StringIndexType.Byte, CommandFlags.None); + await mock.Received().StringBitCountAsync("prefix:key", 123, 456, StringIndexType.Byte, CommandFlags.None); } [Fact] - public void StringBitOperationAsync_1() + public async Task StringBitOperationAsync_1() { - prefixed.StringBitOperationAsync(Bitwise.Xor, "destination", "first", "second", CommandFlags.None); - mock.Verify(_ => _.StringBitOperationAsync(Bitwise.Xor, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None)); + await prefixed.StringBitOperationAsync(Bitwise.Xor, "destination", "first", "second", CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.Xor, "prefix:destination", "prefix:first", "prefix:second", CommandFlags.None); } [Fact] - public void StringBitOperationAsync_2() + public async Task StringBitOperationAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - prefixed.StringBitOperationAsync(Bitwise.Xor, "destination", keys, CommandFlags.None); - mock.Verify(_ => _.StringBitOperationAsync(Bitwise.Xor, "prefix:destination", It.Is(valid), CommandFlags.None)); + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + await prefixed.StringBitOperationAsync(Bitwise.Xor, "destination", keys, CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.Xor, "prefix:destination", Arg.Is(valid), CommandFlags.None); } [Fact] - public void StringBitPositionAsync() + public async Task StringBitPositionAsync() { - prefixed.StringBitPositionAsync("key", true, 123, 456, CommandFlags.None); - mock.Verify(_ => _.StringBitPositionAsync("prefix:key", true, 123, 456, CommandFlags.None)); + await prefixed.StringBitPositionAsync("key", true, 123, 456, CommandFlags.None); + await mock.Received().StringBitPositionAsync("prefix:key", true, 123, 456, CommandFlags.None); } [Fact] - public void StringBitPositionAsync_2() + public async Task StringBitPositionAsync_2() { - prefixed.StringBitPositionAsync("key", true, 123, 456, StringIndexType.Byte, CommandFlags.None); - mock.Verify(_ => _.StringBitPositionAsync("prefix:key", true, 123, 456, StringIndexType.Byte, CommandFlags.None)); + await prefixed.StringBitPositionAsync("key", true, 123, 456, StringIndexType.Byte, CommandFlags.None); + await mock.Received().StringBitPositionAsync("prefix:key", true, 123, 456, StringIndexType.Byte, CommandFlags.None); } [Fact] - public void StringDecrementAsync_1() + public async Task StringDecrementAsync_1() { - prefixed.StringDecrementAsync("key", 123, CommandFlags.None); - mock.Verify(_ => _.StringDecrementAsync("prefix:key", 123, CommandFlags.None)); + await prefixed.StringDecrementAsync("key", 123, CommandFlags.None); + await mock.Received().StringDecrementAsync("prefix:key", 123, CommandFlags.None); } [Fact] - public void StringDecrementAsync_2() + public async Task StringDecrementAsync_2() { - prefixed.StringDecrementAsync("key", 1.23, CommandFlags.None); - mock.Verify(_ => _.StringDecrementAsync("prefix:key", 1.23, CommandFlags.None)); + await prefixed.StringDecrementAsync("key", 1.23, CommandFlags.None); + await mock.Received().StringDecrementAsync("prefix:key", 1.23, CommandFlags.None); } [Fact] - public void StringGetAsync_1() + public async Task StringGetAsync_1() { - prefixed.StringGetAsync("key", CommandFlags.None); - mock.Verify(_ => _.StringGetAsync("prefix:key", CommandFlags.None)); + await prefixed.StringGetAsync("key", CommandFlags.None); + await mock.Received().StringGetAsync("prefix:key", CommandFlags.None); } [Fact] - public void StringGetAsync_2() + public async Task StringGetAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - prefixed.StringGetAsync(keys, CommandFlags.None); - mock.Verify(_ => _.StringGetAsync(It.Is(valid), CommandFlags.None)); + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + await prefixed.StringGetAsync(keys, CommandFlags.None); + await mock.Received().StringGetAsync(Arg.Is(valid), CommandFlags.None); } [Fact] - public void StringGetBitAsync() + public async Task StringGetBitAsync() { - prefixed.StringGetBitAsync("key", 123, CommandFlags.None); - mock.Verify(_ => _.StringGetBitAsync("prefix:key", 123, CommandFlags.None)); + await prefixed.StringGetBitAsync("key", 123, CommandFlags.None); + await mock.Received().StringGetBitAsync("prefix:key", 123, CommandFlags.None); } [Fact] - public void StringGetRangeAsync() + public async Task StringGetRangeAsync() { - prefixed.StringGetRangeAsync("key", 123, 456, CommandFlags.None); - mock.Verify(_ => _.StringGetRangeAsync("prefix:key", 123, 456, CommandFlags.None)); + await prefixed.StringGetRangeAsync("key", 123, 456, CommandFlags.None); + await mock.Received().StringGetRangeAsync("prefix:key", 123, 456, CommandFlags.None); } [Fact] - public void StringGetSetAsync() + public async Task StringGetSetAsync() { - prefixed.StringGetSetAsync("key", "value", CommandFlags.None); - mock.Verify(_ => _.StringGetSetAsync("prefix:key", "value", CommandFlags.None)); + await prefixed.StringGetSetAsync("key", "value", CommandFlags.None); + await mock.Received().StringGetSetAsync("prefix:key", "value", CommandFlags.None); } [Fact] - public void StringGetDeleteAsync() + public async Task StringGetDeleteAsync() { - prefixed.StringGetDeleteAsync("key", CommandFlags.None); - mock.Verify(_ => _.StringGetDeleteAsync("prefix:key", CommandFlags.None)); + await prefixed.StringGetDeleteAsync("key", CommandFlags.None); + await mock.Received().StringGetDeleteAsync("prefix:key", CommandFlags.None); } [Fact] - public void StringGetWithExpiryAsync() + public async Task StringGetWithExpiryAsync() { - prefixed.StringGetWithExpiryAsync("key", CommandFlags.None); - mock.Verify(_ => _.StringGetWithExpiryAsync("prefix:key", CommandFlags.None)); + await prefixed.StringGetWithExpiryAsync("key", CommandFlags.None); + await mock.Received().StringGetWithExpiryAsync("prefix:key", CommandFlags.None); } [Fact] - public void StringIncrementAsync_1() + public async Task StringIncrementAsync_1() { - prefixed.StringIncrementAsync("key", 123, CommandFlags.None); - mock.Verify(_ => _.StringIncrementAsync("prefix:key", 123, CommandFlags.None)); + await prefixed.StringIncrementAsync("key", 123, CommandFlags.None); + await mock.Received().StringIncrementAsync("prefix:key", 123, CommandFlags.None); } [Fact] - public void StringIncrementAsync_2() + public async Task StringIncrementAsync_2() { - prefixed.StringIncrementAsync("key", 1.23, CommandFlags.None); - mock.Verify(_ => _.StringIncrementAsync("prefix:key", 1.23, CommandFlags.None)); + await prefixed.StringIncrementAsync("key", 1.23, CommandFlags.None); + await mock.Received().StringIncrementAsync("prefix:key", 1.23, CommandFlags.None); } [Fact] - public void StringLengthAsync() + public async Task StringLengthAsync() { - prefixed.StringLengthAsync("key", CommandFlags.None); - mock.Verify(_ => _.StringLengthAsync("prefix:key", CommandFlags.None)); + await prefixed.StringLengthAsync("key", CommandFlags.None); + await mock.Received().StringLengthAsync("prefix:key", CommandFlags.None); } [Fact] - public void StringSetAsync_1() + public async Task StringSetAsync_1() { TimeSpan expiry = TimeSpan.FromSeconds(123); - prefixed.StringSetAsync("key", "value", expiry, When.Exists, CommandFlags.None); - mock.Verify(_ => _.StringSetAsync("prefix:key", "value", expiry, When.Exists, CommandFlags.None)); + await prefixed.StringSetAsync("key", "value", expiry, When.Exists, CommandFlags.None); + await mock.Received().StringSetAsync("prefix:key", "value", expiry, When.Exists, CommandFlags.None); } [Fact] - public void StringSetAsync_2() + public async Task StringSetAsync_2() { TimeSpan? expiry = null; - prefixed.StringSetAsync("key", "value", expiry, true, When.Exists, CommandFlags.None); - mock.Verify(_ => _.StringSetAsync("prefix:key", "value", expiry, true, When.Exists, CommandFlags.None)); + await prefixed.StringSetAsync("key", "value", expiry, true, When.Exists, CommandFlags.None); + await mock.Received().StringSetAsync("prefix:key", "value", expiry, true, When.Exists, CommandFlags.None); } [Fact] - public void StringSetAsync_3() + public async Task StringSetAsync_3() { KeyValuePair[] values = new KeyValuePair[] { new KeyValuePair("a", "x"), new KeyValuePair("b", "y") }; - Expression[], bool>> valid = _ => _.Length == 2 && _[0].Key == "prefix:a" && _[0].Value == "x" && _[1].Key == "prefix:b" && _[1].Value == "y"; - prefixed.StringSetAsync(values, When.Exists, CommandFlags.None); - mock.Verify(_ => _.StringSetAsync(It.Is(valid), When.Exists, CommandFlags.None)); + Expression[]>> valid = _ => _.Length == 2 && _[0].Key == "prefix:a" && _[0].Value == "x" && _[1].Key == "prefix:b" && _[1].Value == "y"; + await prefixed.StringSetAsync(values, When.Exists, CommandFlags.None); + await mock.Received().StringSetAsync(Arg.Is(valid), When.Exists, CommandFlags.None); } [Fact] - public void StringSetAsync_Compat() + public async Task StringSetAsync_Compat() { TimeSpan expiry = TimeSpan.FromSeconds(123); - prefixed.StringSetAsync("key", "value", expiry, When.Exists); - mock.Verify(_ => _.StringSetAsync("prefix:key", "value", expiry, When.Exists)); + await prefixed.StringSetAsync("key", "value", expiry, When.Exists); + await mock.Received().StringSetAsync("prefix:key", "value", expiry, When.Exists); } [Fact] - public void StringSetBitAsync() + public async Task StringSetBitAsync() { - prefixed.StringSetBitAsync("key", 123, true, CommandFlags.None); - mock.Verify(_ => _.StringSetBitAsync("prefix:key", 123, true, CommandFlags.None)); + await prefixed.StringSetBitAsync("key", 123, true, CommandFlags.None); + await mock.Received().StringSetBitAsync("prefix:key", 123, true, CommandFlags.None); } [Fact] - public void StringSetRangeAsync() + public async Task StringSetRangeAsync() { - prefixed.StringSetRangeAsync("key", 123, "value", CommandFlags.None); - mock.Verify(_ => _.StringSetRangeAsync("prefix:key", 123, "value", CommandFlags.None)); + await prefixed.StringSetRangeAsync("key", 123, "value", CommandFlags.None); + await mock.Received().StringSetRangeAsync("prefix:key", 123, "value", CommandFlags.None); } [Fact] - public void KeyTouchAsync_1() + public async Task KeyTouchAsync_1() { - prefixed.KeyTouchAsync("key", CommandFlags.None); - mock.Verify(_ => _.KeyTouchAsync("prefix:key", CommandFlags.None)); + await prefixed.KeyTouchAsync("key", CommandFlags.None); + await mock.Received().KeyTouchAsync("prefix:key", CommandFlags.None); } [Fact] - public void KeyTouchAsync_2() + public async Task KeyTouchAsync_2() { RedisKey[] keys = new RedisKey[] { "a", "b" }; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - prefixed.KeyTouchAsync(keys, CommandFlags.None); - mock.Verify(_ => _.KeyTouchAsync(It.Is(valid), CommandFlags.None)); + Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; + await prefixed.KeyTouchAsync(keys, CommandFlags.None); + await mock.Received().KeyTouchAsync(Arg.Is(valid), CommandFlags.None); } } } diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedTransactionTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedTransactionTests.cs index e8af40c6e..04a974808 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedTransactionTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedTransactionTests.cs @@ -1,132 +1,132 @@ using System.Text; using System.Threading.Tasks; -using Moq; +using NSubstitute; using StackExchange.Redis.KeyspaceIsolation; using Xunit; namespace StackExchange.Redis.Tests; -[Collection(nameof(MoqDependentCollection))] +[Collection(nameof(SubstituteDependentCollection))] public sealed class KeyPrefixedTransactionTests { - private readonly Mock mock; + private readonly ITransaction mock; private readonly KeyPrefixedTransaction prefixed; public KeyPrefixedTransactionTests() { - mock = new Mock(); - prefixed = new KeyPrefixedTransaction(mock.Object, Encoding.UTF8.GetBytes("prefix:")); + mock = Substitute.For(); + prefixed = new KeyPrefixedTransaction(mock, Encoding.UTF8.GetBytes("prefix:")); } [Fact] public void AddCondition_HashEqual() { prefixed.AddCondition(Condition.HashEqual("key", "field", "value")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field == value" == value.ToString()))); + mock.Received().AddCondition(Arg.Is(value => "prefix:key Hash > field == value" == value.ToString())); } [Fact] public void AddCondition_HashNotEqual() { prefixed.AddCondition(Condition.HashNotEqual("key", "field", "value")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field != value" == value.ToString()))); + mock.Received().AddCondition(Arg.Is(value => "prefix:key Hash > field != value" == value.ToString())); } [Fact] public void AddCondition_HashExists() { prefixed.AddCondition(Condition.HashExists("key", "field")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field exists" == value.ToString()))); + mock.Received().AddCondition(Arg.Is(value => "prefix:key Hash > field exists" == value.ToString())); } [Fact] public void AddCondition_HashNotExists() { prefixed.AddCondition(Condition.HashNotExists("key", "field")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key Hash > field does not exists" == value.ToString()))); + mock.Received().AddCondition(Arg.Is(value => "prefix:key Hash > field does not exists" == value.ToString())); } [Fact] public void AddCondition_KeyExists() { prefixed.AddCondition(Condition.KeyExists("key")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key exists" == value.ToString()))); + mock.Received().AddCondition(Arg.Is(value => "prefix:key exists" == value.ToString())); } [Fact] public void AddCondition_KeyNotExists() { prefixed.AddCondition(Condition.KeyNotExists("key")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key does not exists" == value.ToString()))); + mock.Received().AddCondition(Arg.Is(value => "prefix:key does not exists" == value.ToString())); } [Fact] public void AddCondition_StringEqual() { prefixed.AddCondition(Condition.StringEqual("key", "value")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key == value" == value.ToString()))); + mock.Received().AddCondition(Arg.Is(value => "prefix:key == value" == value.ToString())); } [Fact] public void AddCondition_StringNotEqual() { prefixed.AddCondition(Condition.StringNotEqual("key", "value")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key != value" == value.ToString()))); + mock.Received().AddCondition(Arg.Is(value => "prefix:key != value" == value.ToString())); } [Fact] public void AddCondition_SortedSetEqual() { prefixed.AddCondition(Condition.SortedSetEqual("key", "member", "score")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key SortedSet > member == score" == value.ToString()))); + mock.Received().AddCondition(Arg.Is(value => "prefix:key SortedSet > member == score" == value.ToString())); } [Fact] public void AddCondition_SortedSetNotEqual() { prefixed.AddCondition(Condition.SortedSetNotEqual("key", "member", "score")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key SortedSet > member != score" == value.ToString()))); + mock.Received().AddCondition(Arg.Is(value => "prefix:key SortedSet > member != score" == value.ToString())); } [Fact] public void AddCondition_SortedSetScoreExists() { prefixed.AddCondition(Condition.SortedSetScoreExists("key", "score")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key not contains 0 members with score: score" == value.ToString()))); + mock.Received().AddCondition(Arg.Is(value => "prefix:key not contains 0 members with score: score" == value.ToString())); } [Fact] public void AddCondition_SortedSetScoreNotExists() { prefixed.AddCondition(Condition.SortedSetScoreNotExists("key", "score")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key contains 0 members with score: score" == value.ToString()))); + mock.Received().AddCondition(Arg.Is(value => "prefix:key contains 0 members with score: score" == value.ToString())); } [Fact] public void AddCondition_SortedSetScoreCountExists() { prefixed.AddCondition(Condition.SortedSetScoreExists("key", "score", "count")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key contains count members with score: score" == value.ToString()))); + mock.Received().AddCondition(Arg.Is(value => "prefix:key contains count members with score: score" == value.ToString())); } [Fact] public void AddCondition_SortedSetScoreCountNotExists() { prefixed.AddCondition(Condition.SortedSetScoreNotExists("key", "score", "count")); - mock.Verify(_ => _.AddCondition(It.Is(value => "prefix:key not contains count members with score: score" == value.ToString()))); + mock.Received().AddCondition(Arg.Is(value => "prefix:key not contains count members with score: score" == value.ToString())); } [Fact] public async Task ExecuteAsync() { await prefixed.ExecuteAsync(CommandFlags.None); - mock.Verify(_ => _.ExecuteAsync(CommandFlags.None), Times.Once()); + await mock.Received(1).ExecuteAsync(CommandFlags.None); } [Fact] public void Execute() { prefixed.Execute(CommandFlags.None); - mock.Verify(_ => _.Execute(CommandFlags.None), Times.Once()); + mock.Received(1).Execute(CommandFlags.None); } } diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index 112513a85..9e0d5f36a 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -19,7 +19,6 @@ - From 4cf2013b5cb7523c811e199a93a8917380017e1d Mon Sep 17 00:00:00 2001 From: Thiago Vaz Dias <1681936+tvdias@users.noreply.github.com> Date: Tue, 15 Aug 2023 16:05:00 +0100 Subject: [PATCH 244/435] add StreamGroupInfo EntriesRead and Lag (#2510) Fixes #2467 --- docs/ReleaseNotes.md | 1 + .../APITypes/StreamGroupInfo.cs | 14 +++++++++++++- .../PublicAPI/PublicAPI.Shipped.txt | 2 ++ src/StackExchange.Redis/ResultProcessor.cs | 15 ++++++++++++++- tests/StackExchange.Redis.Tests/StreamTests.cs | 12 +++++++++++- 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 8d3cc613b..4a12643b8 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: - Fix [#2507](https://github.com/StackExchange/StackExchange.Redis/issues/2507): Pub/sub with multi-item payloads should be usable ([#2508 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2508)) - Add: connection-id tracking (internal only, no public API) ([#2508 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2508)) +- Fix [#2467](https://github.com/StackExchange/StackExchange.Redis/issues/2467): Add StreamGroupInfo EntriesRead and Lag ([#2510 by tvdias](https://github.com/StackExchange/StackExchange.Redis/pull/2510)) ## 2.6.122 diff --git a/src/StackExchange.Redis/APITypes/StreamGroupInfo.cs b/src/StackExchange.Redis/APITypes/StreamGroupInfo.cs index 33281737f..a357c400e 100644 --- a/src/StackExchange.Redis/APITypes/StreamGroupInfo.cs +++ b/src/StackExchange.Redis/APITypes/StreamGroupInfo.cs @@ -5,12 +5,14 @@ /// public readonly struct StreamGroupInfo { - internal StreamGroupInfo(string name, int consumerCount, int pendingMessageCount, string? lastDeliveredId) + internal StreamGroupInfo(string name, int consumerCount, int pendingMessageCount, string? lastDeliveredId, long? entriesRead, long? lag) { Name = name; ConsumerCount = consumerCount; PendingMessageCount = pendingMessageCount; LastDeliveredId = lastDeliveredId; + EntriesRead = entriesRead; + Lag = lag; } /// @@ -33,4 +35,14 @@ internal StreamGroupInfo(string name, int consumerCount, int pendingMessageCount /// The Id of the last message delivered to the group. /// public string? LastDeliveredId { get; } + + /// + /// Total number of entries the group had read. + /// + public long? EntriesRead { get; } + + /// + /// The number of entries in the range between the group's read entries and the stream's entries. + /// + public long? Lag { get; } } diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 84d4ce032..754037887 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1507,6 +1507,8 @@ StackExchange.Redis.StreamEntry.this[StackExchange.Redis.RedisValue fieldName].g StackExchange.Redis.StreamEntry.Values.get -> StackExchange.Redis.NameValueEntry[]! StackExchange.Redis.StreamGroupInfo StackExchange.Redis.StreamGroupInfo.ConsumerCount.get -> int +StackExchange.Redis.StreamGroupInfo.EntriesRead.get -> long? +StackExchange.Redis.StreamGroupInfo.Lag.get -> long? StackExchange.Redis.StreamGroupInfo.LastDeliveredId.get -> string? StackExchange.Redis.StreamGroupInfo.Name.get -> string! StackExchange.Redis.StreamGroupInfo.PendingMessageCount.get -> int diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 5b43401e4..f541a2c24 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -2049,6 +2049,8 @@ internal static readonly CommandBytes Pending = "pending", Idle = "idle", LastDeliveredId = "last-delivered-id", + EntriesRead = "entries-read", + Lag = "lag", IP = "ip", Port = "port"; @@ -2107,6 +2109,10 @@ protected override StreamGroupInfo ParseItem(in RawResult result) // 6) (integer)2 // 7) last-delivered-id // 8) "1588152489012-0" + // 9) "entries-read" + // 10) (integer)2 + // 11) "lag" + // 12) (integer)0 // 2) 1) name // 2) "some-other-group" // 3) consumers @@ -2115,17 +2121,24 @@ protected override StreamGroupInfo ParseItem(in RawResult result) // 6) (integer)0 // 7) last-delivered-id // 8) "1588152498034-0" + // 9) "entries-read" + // 10) (integer)1 + // 11) "lag" + // 12) (integer)1 var arr = result.GetItems(); string? name = default, lastDeliveredId = default; int consumerCount = default, pendingMessageCount = default; + long entriesRead = default, lag = default; KeyValuePairParser.TryRead(arr, KeyValuePairParser.Name, ref name); KeyValuePairParser.TryRead(arr, KeyValuePairParser.Consumers, ref consumerCount); KeyValuePairParser.TryRead(arr, KeyValuePairParser.Pending, ref pendingMessageCount); KeyValuePairParser.TryRead(arr, KeyValuePairParser.LastDeliveredId, ref lastDeliveredId); + KeyValuePairParser.TryRead(arr, KeyValuePairParser.EntriesRead, ref entriesRead); + KeyValuePairParser.TryRead(arr, KeyValuePairParser.Lag, ref lag); - return new StreamGroupInfo(name!, consumerCount, pendingMessageCount, lastDeliveredId); + return new StreamGroupInfo(name!, consumerCount, pendingMessageCount, lastDeliveredId, entriesRead, lag); } } diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index b6568d525..824ff48a3 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -1242,13 +1242,19 @@ public void StreamGroupInfoGet() db.StreamCreateConsumerGroup(key, group1, StreamPosition.Beginning); db.StreamCreateConsumerGroup(key, group2, StreamPosition.Beginning); + var groupInfoList = db.StreamGroupInfo(key); + Assert.Equal(0, groupInfoList[0].EntriesRead); + Assert.Equal(4, groupInfoList[0].Lag); + Assert.Equal(0, groupInfoList[0].EntriesRead); + Assert.Equal(4, groupInfoList[1].Lag); + // Read a single message into the first consumer. db.StreamReadGroup(key, group1, consumer1, count: 1); // Read the remaining messages into the second consumer. db.StreamReadGroup(key, group2, consumer2); - var groupInfoList = db.StreamGroupInfo(key); + groupInfoList = db.StreamGroupInfo(key); Assert.NotNull(groupInfoList); Assert.Equal(2, groupInfoList.Length); @@ -1256,10 +1262,14 @@ public void StreamGroupInfoGet() Assert.Equal(group1, groupInfoList[0].Name); Assert.Equal(1, groupInfoList[0].PendingMessageCount); Assert.True(IsMessageId(groupInfoList[0].LastDeliveredId)); // can't test actual - will vary + Assert.Equal(1, groupInfoList[0].EntriesRead); + Assert.Equal(3, groupInfoList[0].Lag); Assert.Equal(group2, groupInfoList[1].Name); Assert.Equal(4, groupInfoList[1].PendingMessageCount); Assert.True(IsMessageId(groupInfoList[1].LastDeliveredId)); // can't test actual - will vary + Assert.Equal(4, groupInfoList[1].EntriesRead); + Assert.Equal(0, groupInfoList[1].Lag); } static bool IsMessageId(string? value) From 830d2c641ba6535ecebf7ad0c379d8398d7c9c47 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 19 Aug 2023 10:18:50 -0400 Subject: [PATCH 245/435] ILogger for ConnectionMultiplexer (#2051) ILogger support - since we support `Action` this can be added today e.g. with `o => o.LoggerFactory = myLoggerFactory`: ```cs var muxer = ConnectionMultiplexer.Connect("localhost", o => o.LoggerFactory = myLoggerFactory); ``` ...or we could add sibling overloads to the existing `TextWriter`. I'd love to not create a ^2 matrix every time we do this, though. Starting simpler for that reason. Note: this is on top of #2050 for simplicity, will roll against `main` when merged. --- .github/workflows/CI.yml | 4 +- Directory.Packages.props | 1 + appveyor.yml | 1 + docs/Configuration.md | 2 + docs/ReleaseNotes.md | 1 + .../Configuration/DefaultOptionsProvider.cs | 7 + .../ConfigurationOptions.cs | 14 ++ .../ConnectionMultiplexer.Sentinel.cs | 81 ++++--- .../ConnectionMultiplexer.cs | 204 +++++++++--------- src/StackExchange.Redis/DebuggingAids.cs | 9 - src/StackExchange.Redis/EndPointCollection.cs | 11 +- src/StackExchange.Redis/Enums/ServerType.cs | 2 +- src/StackExchange.Redis/ExceptionFactory.cs | 14 -- src/StackExchange.Redis/LogProxy.cs | 65 ------ src/StackExchange.Redis/Message.cs | 11 +- src/StackExchange.Redis/PhysicalBridge.cs | 27 ++- src/StackExchange.Redis/PhysicalConnection.cs | 37 +++- .../PublicAPI/PublicAPI.Shipped.txt | 3 + src/StackExchange.Redis/RedisServer.cs | 14 +- src/StackExchange.Redis/ResultProcessor.cs | 33 +-- src/StackExchange.Redis/ServerEndPoint.cs | 47 ++-- .../StackExchange.Redis.csproj | 1 + src/StackExchange.Redis/TextWriterLogger.cs | 52 +++++ .../AbortOnConnectFailTests.cs | 12 +- ...AggresssiveTests.cs => AggressiveTests.cs} | 22 +- tests/StackExchange.Redis.Tests/AsyncTests.cs | 8 +- .../StackExchange.Redis.Tests/BacklogTests.cs | 132 ++++++------ .../StackExchange.Redis.Tests/BasicOpTests.cs | 2 +- .../StackExchange.Redis.Tests/ConfigTests.cs | 5 +- .../ConnectionFailedErrorsTests.cs | 4 +- .../DefaultOptionsTests.cs | 4 + .../ExceptionFactoryTests.cs | 6 +- .../FailoverTests.cs | 24 +-- .../Helpers/SharedConnectionFixture.cs | 2 +- .../Helpers/TextWriterOutputHelper.cs | 6 - .../StackExchange.Redis.Tests/LoggerTests.cs | 127 +++++++++++ tests/StackExchange.Redis.Tests/ParseTests.cs | 4 +- tests/StackExchange.Redis.Tests/RoleTests.cs | 6 + tests/StackExchange.Redis.Tests/SSLTests.cs | 22 +- .../StackExchange.Redis.Tests/SentinelBase.cs | 2 +- .../SyncContextTests.cs | 30 ++- tests/StackExchange.Redis.Tests/TestBase.cs | 21 +- .../TransactionTests.cs | 2 +- 43 files changed, 607 insertions(+), 475 deletions(-) delete mode 100644 src/StackExchange.Redis/LogProxy.cs create mode 100644 src/StackExchange.Redis/TextWriterLogger.cs rename tests/StackExchange.Redis.Tests/{AggresssiveTests.cs => AggressiveTests.cs} (93%) create mode 100644 tests/StackExchange.Redis.Tests/LoggerTests.cs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7624fb699..6f070b83d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -20,7 +20,7 @@ jobs: - name: Checkout code uses: actions/checkout@v1 - name: Install .NET SDK - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: | 6.0.x @@ -53,7 +53,7 @@ jobs: - name: Checkout code uses: actions/checkout@v1 # - name: Install .NET SDK - # uses: actions/setup-dotnet@v1 + # uses: actions/setup-dotnet@v3 # with: # dotnet-version: | # 6.0.x diff --git a/Directory.Packages.props b/Directory.Packages.props index 91a4646c0..2304eb77e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,6 +12,7 @@ + diff --git a/appveyor.yml b/appveyor.yml index 1d480410e..d9dd350af 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -74,6 +74,7 @@ build_script: test: off artifacts: - path: .\.nupkgs\*.nupkg +- path: '**\*.trx' deploy: - provider: NuGet diff --git a/docs/Configuration.md b/docs/Configuration.md index 753abf83b..5893bf855 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -100,6 +100,8 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a | setlib={bool} | `SetClientLibrary` | `true` | Whether to attempt to use `CLIENT SETINFO` to set the library name/version on the connection | Additional code-only options: +- LoggerFactory (`ILoggerFactory`) - Default: `null` + - The logger to use for connection events (not per command), e.g. connection log, disconnects, reconnects, server errors. - ReconnectRetryPolicy (`IReconnectRetryPolicy`) - Default: `ReconnectRetryPolicy = ExponentialRetry(ConnectTimeout / 2);` - Determines how often a multiplexer will try to reconnect after a failure - BacklogPolicy - Default: `BacklogPolicy = BacklogPolicy.Default;` diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 4a12643b8..ee25d40a0 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: - Fix [#2507](https://github.com/StackExchange/StackExchange.Redis/issues/2507): Pub/sub with multi-item payloads should be usable ([#2508 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2508)) - Add: connection-id tracking (internal only, no public API) ([#2508 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2508)) +- Add: `ConfigurationOptions.LoggerFactory` for logging to an `ILoggerFactory` (e.g. `ILogger`) all connection and error events ([#2051 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2051)) - Fix [#2467](https://github.com/StackExchange/StackExchange.Redis/issues/2467): Add StreamGroupInfo EntriesRead and Lag ([#2510 by tvdias](https://github.com/StackExchange/StackExchange.Redis/pull/2510)) ## 2.6.122 diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index 8e6cf85b1..67c74089b 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -3,6 +3,7 @@ using System.Net; using System.Reflection; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; namespace StackExchange.Redis.Configuration { @@ -156,6 +157,12 @@ public static DefaultOptionsProvider GetProvider(EndPoint endpoint) /// public virtual TimeSpan KeepAliveInterval => TimeSpan.FromSeconds(60); + /// + /// The to get loggers for connection events. + /// Note: changes here only affect s created after. + /// + public virtual ILoggerFactory? LoggerFactory => null; + /// /// Type of proxy to use (if any); for example . /// diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 19314c344..12c372715 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -10,6 +10,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using StackExchange.Redis.Configuration; namespace StackExchange.Redis @@ -161,6 +162,8 @@ public static string TryNormalize(string value) private BacklogPolicy? backlogPolicy; + private ILoggerFactory? loggerFactory; + /// /// A LocalCertificateSelectionCallback delegate responsible for selecting the certificate used for authentication; note /// that this cannot be specified in the configuration-string. @@ -448,6 +451,16 @@ public int KeepAlive set => keepAlive = value; } + /// + /// The to get loggers for connection events. + /// Note: changes here only affect s created after. + /// + public ILoggerFactory? LoggerFactory + { + get => loggerFactory ?? Defaults.LoggerFactory; + set => loggerFactory = value; + } + /// /// The username to use to authenticate with the server. /// @@ -675,6 +688,7 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow checkCertificateRevocation = checkCertificateRevocation, BeforeSocketConnect = BeforeSocketConnect, EndPoints = EndPoints.Clone(), + LoggerFactory = LoggerFactory, #if NETCOREAPP3_1_OR_GREATER SslClientAuthenticationOptions = SslClientAuthenticationOptions, #endif diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs index 145826e3c..238c32bda 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs @@ -1,4 +1,5 @@ -using Pipelines.Sockets.Unofficial; +using Microsoft.Extensions.Logging; +using Pipelines.Sockets.Unofficial; using System; using System.Collections.Generic; using System.IO; @@ -19,8 +20,8 @@ public partial class ConnectionMultiplexer /// /// Initializes the connection as a Sentinel connection and adds the necessary event handlers to track changes to the managed primaries. /// - /// The writer to log to, if any. - internal void InitializeSentinel(LogProxy? logProxy) + /// The to log to, if any. + internal void InitializeSentinel(ILogger? log) { if (ServerSelectionStrategy.ServerType != ServerType.Sentinel) { @@ -65,7 +66,7 @@ internal void InitializeSentinel(LogProxy? logProxy) // we need to reconfigure to make sure we still have a subscription to the +switch-master channel ConnectionFailed += (sender, e) => // Reconfigure to get subscriptions back online - ReconfigureAsync(first: false, reconfigureAll: true, logProxy, e.EndPoint, "Lost sentinel connection", false).Wait(); + ReconfigureAsync(first: false, reconfigureAll: true, log, e.EndPoint, "Lost sentinel connection", false).Wait(); // Subscribe to new sentinels being added if (sub.SubscribedEndpoint(RedisChannel.Literal("+sentinel")) == null) @@ -142,12 +143,12 @@ private static ConnectionMultiplexer SentinelPrimaryConnect(ConfigurationOptions /// for the specified in the config and returns a managed connection to the current primary server. /// /// The configuration options to use for this multiplexer. - /// The to log to. - private static async Task SentinelPrimaryConnectAsync(ConfigurationOptions configuration, TextWriter? log = null) + /// The to log to. + private static async Task SentinelPrimaryConnectAsync(ConfigurationOptions configuration, TextWriter? writer = null) { - var sentinelConnection = await SentinelConnectAsync(configuration, log).ForAwait(); + var sentinelConnection = await SentinelConnectAsync(configuration, writer).ForAwait(); - var muxer = sentinelConnection.GetSentinelMasterConnection(configuration, log); + var muxer = sentinelConnection.GetSentinelMasterConnection(configuration, writer); // Set reference to sentinel connection so that we can dispose it muxer.sentinelConnection = sentinelConnection; @@ -375,49 +376,45 @@ internal void OnManagedConnectionFailed(object? sender, ConnectionFailedEventArg /// /// The endpoint responsible for the switch. /// The connection that should be switched over to a new primary endpoint. - /// The writer to log to, if any. - internal void SwitchPrimary(EndPoint? switchBlame, ConnectionMultiplexer connection, TextWriter? log = null) + /// The writer to log to, if any. + internal void SwitchPrimary(EndPoint? switchBlame, ConnectionMultiplexer connection, TextWriter? writer = null) { - if (log == null) log = TextWriter.Null; - - using (var logProxy = LogProxy.TryCreate(log)) + var logger = Logger.With(writer); + if (connection.RawConfig.ServiceName is not string serviceName) { - if (connection.RawConfig.ServiceName is not string serviceName) - { - logProxy?.WriteLine("Service name not defined."); - return; - } + logger?.LogInformation("Service name not defined."); + return; + } - // Get new primary - try twice - EndPoint newPrimaryEndPoint = GetConfiguredPrimaryForService(serviceName) - ?? GetConfiguredPrimaryForService(serviceName) - ?? throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, - $"Sentinel: Failed connecting to switch primary for service: {serviceName}"); + // Get new primary - try twice + EndPoint newPrimaryEndPoint = GetConfiguredPrimaryForService(serviceName) + ?? GetConfiguredPrimaryForService(serviceName) + ?? throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, + $"Sentinel: Failed connecting to switch primary for service: {serviceName}"); - connection.currentSentinelPrimaryEndPoint = newPrimaryEndPoint; + connection.currentSentinelPrimaryEndPoint = newPrimaryEndPoint; - if (!connection.servers.Contains(newPrimaryEndPoint)) - { - EndPoint[]? replicaEndPoints = GetReplicasForService(serviceName) - ?? GetReplicasForService(serviceName); + if (!connection.servers.Contains(newPrimaryEndPoint)) + { + EndPoint[]? replicaEndPoints = GetReplicasForService(serviceName) + ?? GetReplicasForService(serviceName); - connection.servers.Clear(); - connection.EndPoints.Clear(); - connection.EndPoints.TryAdd(newPrimaryEndPoint); - if (replicaEndPoints is not null) + connection.servers.Clear(); + connection.EndPoints.Clear(); + connection.EndPoints.TryAdd(newPrimaryEndPoint); + if (replicaEndPoints is not null) + { + foreach (var replicaEndPoint in replicaEndPoints) { - foreach (var replicaEndPoint in replicaEndPoints) - { - connection.EndPoints.TryAdd(replicaEndPoint); - } + connection.EndPoints.TryAdd(replicaEndPoint); } - Trace($"Switching primary to {newPrimaryEndPoint}"); - // Trigger a reconfigure - connection.ReconfigureAsync(first: false, reconfigureAll: false, logProxy, switchBlame, - $"Primary switch {serviceName}", false, CommandFlags.PreferMaster).Wait(); - - UpdateSentinelAddressList(serviceName); } + Trace($"Switching primary to {newPrimaryEndPoint}"); + // Trigger a reconfigure + connection.ReconfigureAsync(first: false, reconfigureAll: false, logger, switchBlame, + $"Primary switch {serviceName}", false, CommandFlags.PreferMaster).Wait(); + + UpdateSentinelAddressList(serviceName); } } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 68d58253a..cc239ad3f 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -10,6 +10,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Pipelines.Sockets.Unofficial; using StackExchange.Redis.Profiling; @@ -41,6 +42,7 @@ public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplex private volatile bool _isDisposed; internal bool IsDisposed => _isDisposed; + internal ILogger? Logger { get; } internal CommandMap CommandMap { get; } internal EndPointCollection EndPoints { get; } @@ -130,6 +132,7 @@ private ConnectionMultiplexer(ConfigurationOptions configuration, ServerType? se RawConfig = configuration ?? throw new ArgumentNullException(nameof(configuration)); EndPoints = endpoints ?? RawConfig.EndPoints.Clone(); EndPoints.SetDefaultPorts(serverType, ssl: RawConfig.Ssl); + Logger = configuration.LoggerFactory?.CreateLogger(); var map = CommandMap = configuration.GetCommandMap(serverType); if (!string.IsNullOrWhiteSpace(configuration.Password)) @@ -154,7 +157,7 @@ private ConnectionMultiplexer(ConfigurationOptions configuration, ServerType? se lastHeartbeatTicks = Environment.TickCount; } - private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions configuration, LogProxy? log, ServerType? serverType, out EventHandler? connectHandler, EndPointCollection? endpoints = null) + private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions configuration, ILogger? log, ServerType? serverType, out EventHandler? connectHandler, EndPointCollection? endpoints = null) { var muxer = new ConnectionMultiplexer(configuration, serverType, endpoints); connectHandler = null; @@ -165,13 +168,13 @@ private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions conf { try { - lock (log.SyncLock) // Keep the outer and any inner errors contiguous + lock (log) // Keep the outer and any inner errors contiguous { var ex = a.Exception; - log?.WriteLine($"Connection failed: {Format.ToString(a.EndPoint)} ({a.ConnectionType}, {a.FailureType}): {ex?.Message ?? "(unknown)"}"); + log?.LogError(ex, $"Connection failed: {Format.ToString(a.EndPoint)} ({a.ConnectionType}, {a.FailureType}): {ex?.Message ?? "(unknown)"}"); while ((ex = ex?.InnerException) != null) { - log?.WriteLine($"> {ex.Message}"); + log?.LogError(ex, $"> {ex.Message}"); } } } @@ -196,9 +199,10 @@ public ServerCounters GetCounters() return counters; } - internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOptions options, LogProxy? log) + internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOptions options, TextWriter? writer) { _ = server ?? throw new ArgumentNullException(nameof(server)); + var log = Logger.With(writer); var cmd = server.GetFeatures().ReplicaCommands ? RedisCommand.REPLICAOF : RedisCommand.SLAVEOF; CommandMap.AssertAvailable(cmd); @@ -216,14 +220,14 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt const CommandFlags flags = CommandFlags.NoRedirect; Message msg; - log?.WriteLine($"Checking {Format.ToString(srv.EndPoint)} is available..."); + log?.LogInformation($"Checking {Format.ToString(srv.EndPoint)} is available..."); try { await srv.PingAsync(flags).ForAwait(); // if it isn't happy, we're not happy } catch (Exception ex) { - log?.WriteLine($"Operation failed on {Format.ToString(srv.EndPoint)}, aborting: {ex.Message}"); + log?.LogError(ex, $"Operation failed on {Format.ToString(srv.EndPoint)}, aborting: {ex.Message}"); throw; } @@ -238,7 +242,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt foreach (var node in nodes) { if (!node.IsConnected || node.IsReplica) continue; - log?.WriteLine($"Attempting to set tie-breaker on {Format.ToString(node.EndPoint)}..."); + log?.LogInformation($"Attempting to set tie-breaker on {Format.ToString(node.EndPoint)}..."); msg = Message.Create(0, flags | CommandFlags.FireAndForget, RedisCommand.SET, tieBreakerKey, newPrimary); try { @@ -249,21 +253,21 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt } // stop replicating, promote to a standalone primary - log?.WriteLine($"Making {Format.ToString(srv.EndPoint)} a primary..."); + log?.LogInformation($"Making {Format.ToString(srv.EndPoint)} a primary..."); try { await srv.ReplicaOfAsync(null, flags).ForAwait(); } catch (Exception ex) { - log?.WriteLine($"Operation failed on {Format.ToString(srv.EndPoint)}, aborting: {ex.Message}"); + log?.LogError(ex, $"Operation failed on {Format.ToString(srv.EndPoint)}, aborting: {ex.Message}"); throw; } // also, in case it was a replica a moment ago, and hasn't got the tie-breaker yet, we re-send the tie-breaker to this one if (!tieBreakerKey.IsNull && !server.IsReplica) { - log?.WriteLine($"Resending tie-breaker to {Format.ToString(server.EndPoint)}..."); + log?.LogInformation($"Resending tie-breaker to {Format.ToString(server.EndPoint)}..."); msg = Message.Create(0, flags | CommandFlags.FireAndForget, RedisCommand.SET, tieBreakerKey, newPrimary); try { @@ -294,7 +298,7 @@ async Task BroadcastAsync(ServerSnapshot serverNodes) foreach (var node in serverNodes) { if (!node.IsConnected) continue; - log?.WriteLine($"Broadcasting via {Format.ToString(node.EndPoint)}..."); + log?.LogInformation($"Broadcasting via {Format.ToString(node.EndPoint)}..."); msg = Message.Create(-1, flags | CommandFlags.FireAndForget, RedisCommand.PUBLISH, channel, newPrimary); await node.WriteDirectAsync(msg, ResultProcessor.Int64).ForAwait(); } @@ -310,7 +314,7 @@ async Task BroadcastAsync(ServerSnapshot serverNodes) { if (node == server || node.ServerType != ServerType.Standalone) continue; - log?.WriteLine($"Replicating to {Format.ToString(node.EndPoint)}..."); + log?.LogInformation($"Replicating to {Format.ToString(node.EndPoint)}..."); msg = RedisServer.CreateReplicaOfMessage(node, server.EndPoint, flags); await node.WriteDirectAsync(msg, ResultProcessor.DemandOK).ForAwait(); } @@ -322,7 +326,7 @@ async Task BroadcastAsync(ServerSnapshot serverNodes) await BroadcastAsync(nodes).ForAwait(); // and reconfigure the muxer - log?.WriteLine("Reconfiguring all endpoints..."); + log?.LogInformation("Reconfiguring all endpoints..."); // Yes, there is a tiny latency race possible between this code and the next call, but it's far more minute than before. // The effective gap between 0 and > 0 (likely off-box) latency is something that may never get hit here by anyone. if (blockingReconfig) @@ -331,7 +335,7 @@ async Task BroadcastAsync(ServerSnapshot serverNodes) } if (!await ReconfigureAsync(first: false, reconfigureAll: true, log, srv.EndPoint, cause: nameof(MakePrimaryAsync)).ForAwait()) { - log?.WriteLine("Verifying the configuration was incomplete; please verify"); + log?.LogInformation("Verifying the configuration was incomplete; please verify"); } } @@ -466,21 +470,21 @@ private bool WaitAllIgnoreErrors(Task[] tasks) return false; } - private static async Task WaitAllIgnoreErrorsAsync(string name, Task[] tasks, int timeoutMilliseconds, LogProxy? log, [CallerMemberName] string? caller = null, [CallerLineNumber] int callerLineNumber = 0) + private static async Task WaitAllIgnoreErrorsAsync(string name, Task[] tasks, int timeoutMilliseconds, ILogger? log, [CallerMemberName] string? caller = null, [CallerLineNumber] int callerLineNumber = 0) { _ = tasks ?? throw new ArgumentNullException(nameof(tasks)); if (tasks.Length == 0) { - log?.WriteLine("No tasks to await"); + log?.LogInformation("No tasks to await"); return true; } if (AllComplete(tasks)) { - log?.WriteLine("All tasks are already complete"); + log?.LogInformation("All tasks are already complete"); return true; } - static void LogWithThreadPoolStats(LogProxy? log, string message, out int busyWorkerCount) + static void LogWithThreadPoolStats(ILogger? log, string message, out int busyWorkerCount) { busyWorkerCount = 0; if (log is not null) @@ -493,7 +497,7 @@ static void LogWithThreadPoolStats(LogProxy? log, string message, out int busyWo { sb.Append(", POOL: ").Append(workItems); } - log?.WriteLine(sb.ToString()); + log?.LogInformation(sb.ToString()); } } @@ -589,21 +593,22 @@ public static Task ConnectAsync(ConfigurationOptions conf : ConnectImplAsync(configuration, log); } - private static async Task ConnectImplAsync(ConfigurationOptions configuration, TextWriter? log = null, ServerType? serverType = null) + private static async Task ConnectImplAsync(ConfigurationOptions configuration, TextWriter? writer = null, ServerType? serverType = null) { IDisposable? killMe = null; EventHandler? connectHandler = null; ConnectionMultiplexer? muxer = null; - using var logProxy = LogProxy.TryCreate(log); + var configLogger = configuration.LoggerFactory?.CreateLogger(); + var log = configLogger.With(writer); try { var sw = ValueStopwatch.StartNew(); - logProxy?.WriteLine($"Connecting (async) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); + log?.LogInformation($"Connecting (async) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); - muxer = CreateMultiplexer(configuration, logProxy, serverType, out connectHandler); + muxer = CreateMultiplexer(configuration, log, serverType, out connectHandler); killMe = muxer; Interlocked.Increment(ref muxer._connectAttemptCount); - bool configured = await muxer.ReconfigureAsync(first: true, reconfigureAll: false, logProxy, null, "connect").ObserveErrors().ForAwait(); + bool configured = await muxer.ReconfigureAsync(first: true, reconfigureAll: false, log, null, "connect").ObserveErrors().ForAwait(); if (!configured) { throw ExceptionFactory.UnableToConnect(muxer, muxer.failureMessage); @@ -614,12 +619,12 @@ private static async Task ConnectImplAsync(ConfigurationO if (muxer.ServerSelectionStrategy.ServerType == ServerType.Sentinel) { // Initialize the Sentinel handlers - muxer.InitializeSentinel(logProxy); + muxer.InitializeSentinel(log); } - await configuration.AfterConnectAsync(muxer, logProxy != null ? logProxy.WriteLine : LogProxy.NullWriter).ForAwait(); + await configuration.AfterConnectAsync(muxer, s => log?.LogInformation(s)).ForAwait(); - logProxy?.WriteLine($"Total connect time: {sw.ElapsedMilliseconds:n0} ms"); + log?.LogInformation($"Total connect time: {sw.ElapsedMilliseconds:n0} ms"); return muxer; } @@ -627,6 +632,7 @@ private static async Task ConnectImplAsync(ConfigurationO { if (connectHandler != null && muxer != null) muxer.ConnectionFailed -= connectHandler; if (killMe != null) try { killMe.Dispose(); } catch { } + if (log is TextWriterLogger twLogger) twLogger.Dispose(); } } @@ -675,22 +681,23 @@ public static ConnectionMultiplexer Connect(ConfigurationOptions configuration, : ConnectImpl(configuration, log); } - private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configuration, TextWriter? log, ServerType? serverType = null, EndPointCollection? endpoints = null) + private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configuration, TextWriter? writer, ServerType? serverType = null, EndPointCollection? endpoints = null) { IDisposable? killMe = null; EventHandler? connectHandler = null; ConnectionMultiplexer? muxer = null; - using var logProxy = LogProxy.TryCreate(log); + var configLogger = configuration.LoggerFactory?.CreateLogger(); + var log = configLogger.With(writer); try { var sw = ValueStopwatch.StartNew(); - logProxy?.WriteLine($"Connecting (sync) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); + log?.LogInformation($"Connecting (sync) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); - muxer = CreateMultiplexer(configuration, logProxy, serverType, out connectHandler, endpoints); + muxer = CreateMultiplexer(configuration, log, serverType, out connectHandler, endpoints); killMe = muxer; Interlocked.Increment(ref muxer._connectAttemptCount); // note that task has timeouts internally, so it might take *just over* the regular timeout - var task = muxer.ReconfigureAsync(first: true, reconfigureAll: false, logProxy, null, "connect"); + var task = muxer.ReconfigureAsync(first: true, reconfigureAll: false, log, null, "connect"); if (!task.Wait(muxer.SyncConnectTimeout(true))) { @@ -701,7 +708,9 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat } else { - muxer.LastException = ExceptionFactory.UnableToConnect(muxer, "ConnectTimeout"); + var ex = ExceptionFactory.UnableToConnect(muxer, "ConnectTimeout"); + muxer.LastException = ex; + muxer.Logger?.LogError(ex, ex.Message); } } @@ -712,12 +721,12 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat if (muxer.ServerSelectionStrategy.ServerType == ServerType.Sentinel) { // Initialize the Sentinel handlers - muxer.InitializeSentinel(logProxy); + muxer.InitializeSentinel(log); } - configuration.AfterConnectAsync(muxer, logProxy != null ? logProxy.WriteLine : LogProxy.NullWriter).Wait(muxer.SyncConnectTimeout(true)); + configuration.AfterConnectAsync(muxer, s => log?.LogInformation(s)).Wait(muxer.SyncConnectTimeout(true)); - logProxy?.WriteLine($"Total connect time: {sw.ElapsedMilliseconds:n0} ms"); + log?.LogInformation($"Total connect time: {sw.ElapsedMilliseconds:n0} ms"); return muxer; } @@ -725,6 +734,7 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat { if (connectHandler != null && muxer != null) muxer.ConnectionFailed -= connectHandler; if (killMe != null) try { killMe.Dispose(); } catch { } + if (log is TextWriterLogger twLogger) twLogger.Dispose(); } } @@ -839,7 +849,6 @@ public bool Any(Func? predicate = null) return false; } - public ServerSnapshotFiltered Where(CommandFlags flags) { var effectiveFlags = flags & (CommandFlags.DemandMaster | CommandFlags.DemandReplica); @@ -875,7 +884,7 @@ public ServerSnapshotFiltered(ServerEndPoint[] endpoints, int count, FuncThe to log to. public async Task ConfigureAsync(TextWriter? log = null) { - using var logProxy = LogProxy.TryCreate(log); - return await ReconfigureAsync(first: false, reconfigureAll: true, logProxy, null, "configure").ObserveErrors().ForAwait(); + return await ReconfigureAsync(first: false, reconfigureAll: true, Logger.With(log), null, "configure").ObserveErrors().ForAwait(); } internal int SyncConnectTimeout(bool forConnect) @@ -1330,28 +1336,24 @@ public string GetStatus() /// Provides a text overview of the status of all connections. /// /// The to log to. - public void GetStatus(TextWriter log) - { - using var proxy = LogProxy.TryCreate(log); - GetStatus(proxy); - } + public void GetStatus(TextWriter log) => GetStatus(new TextWriterLogger(log, null)); - internal void GetStatus(LogProxy? log) + internal void GetStatus(ILogger? log) { if (log == null) return; var tmp = GetServerSnapshot(); - log?.WriteLine("Endpoint Summary:"); + log?.LogInformation("Endpoint Summary:"); foreach (var server in tmp) { - log?.WriteLine(prefix: " ", message: server.Summary()); - log?.WriteLine(prefix: " ", message: server.GetCounters().ToString()); - log?.WriteLine(prefix: " ", message: server.GetProfile()); + log?.LogInformation(" " + server.Summary()); + log?.LogInformation(" " + server.GetCounters().ToString()); + log?.LogInformation(" " + server.GetProfile()); } - log?.WriteLine($"Sync timeouts: {Interlocked.Read(ref syncTimeouts)}; async timeouts: {Interlocked.Read(ref asyncTimeouts)}; fire and forget: {Interlocked.Read(ref fireAndForgets)}; last heartbeat: {LastHeartbeatSecondsAgo}s ago"); + log?.LogInformation($"Sync timeouts: {Interlocked.Read(ref syncTimeouts)}; async timeouts: {Interlocked.Read(ref asyncTimeouts)}; fire and forget: {Interlocked.Read(ref fireAndForgets)}; last heartbeat: {LastHeartbeatSecondsAgo}s ago"); } - private void ActivateAllServers(LogProxy? log) + private void ActivateAllServers(ILogger? log) { foreach (var server in GetServerSnapshot()) { @@ -1392,7 +1394,7 @@ internal bool ReconfigureIfNeeded(EndPoint? blame, bool fromBroadcast, string ca public Task ReconfigureAsync(string reason) => ReconfigureAsync(first: false, reconfigureAll: false, log: null, blame: null, cause: reason); - internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogProxy? log, EndPoint? blame, string cause, bool publishReconfigure = false, CommandFlags publishReconfigureFlags = CommandFlags.None) + internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILogger? log, EndPoint? blame, string cause, bool publishReconfigure = false, CommandFlags publishReconfigureFlags = CommandFlags.None) { if (_isDisposed) throw new ObjectDisposedException(ToString()); bool showStats = log is not null; @@ -1405,15 +1407,14 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP if (!ranThisCall) { - log?.WriteLine($"Reconfiguration was already in progress due to: {activeConfigCause}, attempted to run for: {cause}"); + log?.LogInformation($"Reconfiguration was already in progress due to: {activeConfigCause}, attempted to run for: {cause}"); return false; } Trace("Starting reconfiguration..."); Trace(blame != null, "Blaming: " + Format.ToString(blame)); Interlocked.Exchange(ref lastReconfigiureTicks, Environment.TickCount); - log?.WriteLine(RawConfig.ToString(includePassword: false)); - log?.WriteLine(); + log?.LogInformation(RawConfig.ToString(includePassword: false)); if (first) { @@ -1443,7 +1444,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP int standaloneCount = 0, clusterCount = 0, sentinelCount = 0; var endpoints = EndPoints; bool useTieBreakers = RawConfig.TryGetTieBreaker(out var tieBreakerKey); - log?.WriteLine($"{endpoints.Count} unique nodes specified ({(useTieBreakers ? "with" : "without")} tiebreaker)"); + log?.LogInformation($"{endpoints.Count} unique nodes specified ({(useTieBreakers ? "with" : "without")} tiebreaker)"); if (endpoints.Count == 0) { @@ -1484,7 +1485,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP watch ??= ValueStopwatch.StartNew(); var remaining = RawConfig.ConnectTimeout - watch.Value.ElapsedMilliseconds; - log?.WriteLine($"Allowing {available.Length} endpoint(s) {TimeSpan.FromMilliseconds(remaining)} to respond..."); + log?.LogInformation($"Allowing {available.Length} endpoint(s) {TimeSpan.FromMilliseconds(remaining)} to respond..."); Trace("Allowing endpoints " + TimeSpan.FromMilliseconds(remaining) + " to respond..."); var allConnected = await WaitAllIgnoreErrorsAsync("available", available, remaining, log).ForAwait(); @@ -1497,18 +1498,18 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP var task = available[i]; var bs = server.GetBridgeStatus(ConnectionType.Interactive); - log?.WriteLine($" Server[{i}] ({Format.ToString(server)}) Status: {task.Status} (inst: {bs.MessagesSinceLastHeartbeat}, qs: {bs.Connection.MessagesSentAwaitingResponse}, in: {bs.Connection.BytesAvailableOnSocket}, qu: {bs.MessagesSinceLastHeartbeat}, aw: {bs.IsWriterActive}, in-pipe: {bs.Connection.BytesInReadPipe}, out-pipe: {bs.Connection.BytesInWritePipe}, bw: {bs.BacklogStatus}, rs: {bs.Connection.ReadStatus}. ws: {bs.Connection.WriteStatus})"); + log?.LogInformation($" Server[{i}] ({Format.ToString(server)}) Status: {task.Status} (inst: {bs.MessagesSinceLastHeartbeat}, qs: {bs.Connection.MessagesSentAwaitingResponse}, in: {bs.Connection.BytesAvailableOnSocket}, qu: {bs.MessagesSinceLastHeartbeat}, aw: {bs.IsWriterActive}, in-pipe: {bs.Connection.BytesInReadPipe}, out-pipe: {bs.Connection.BytesInWritePipe}, bw: {bs.BacklogStatus}, rs: {bs.Connection.ReadStatus}. ws: {bs.Connection.WriteStatus})"); } } - log?.WriteLine("Endpoint summary:"); + log?.LogInformation("Endpoint summary:"); // Log current state after await foreach (var server in servers) { - log?.WriteLine($" {Format.ToString(server.EndPoint)}: Endpoint is (Interactive: {server.InteractiveConnectionState}, Subscription: {server.SubscriptionConnectionState})"); + log?.LogInformation($" {Format.ToString(server.EndPoint)}: Endpoint is (Interactive: {server.InteractiveConnectionState}, Subscription: {server.SubscriptionConnectionState})"); } - log?.WriteLine("Task summary:"); + log?.LogInformation("Task summary:"); EndPointCollection? updatedClusterEndpointCollection = null; for (int i = 0; i < available.Length; i++) { @@ -1521,21 +1522,21 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP var aex = task.Exception!; foreach (var ex in aex.InnerExceptions) { - log?.WriteLine($" {Format.ToString(server)}: Faulted: {ex.Message}"); + log?.LogError(ex, $" {Format.ToString(server)}: Faulted: {ex.Message}"); failureMessage = ex.Message; } } else if (task.IsCanceled) { server.SetUnselectable(UnselectableFlags.DidNotRespond); - log?.WriteLine($" {Format.ToString(server)}: Connect task canceled"); + log?.LogInformation($" {Format.ToString(server)}: Connect task canceled"); } else if (task.IsCompleted) { if (task.Result != "Disconnected") { server.ClearUnselectable(UnselectableFlags.DidNotRespond); - log?.WriteLine($" {Format.ToString(server)}: Returned with success as {server.ServerType} {(server.IsReplica ? "replica" : "primary")} (Source: {task.Result})"); + log?.LogInformation($" {Format.ToString(server)}: Returned with success as {server.ServerType} {(server.IsReplica ? "replica" : "primary")} (Source: {task.Result})"); // Count the server types switch (server.ServerType) @@ -1588,13 +1589,13 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP else { server.SetUnselectable(UnselectableFlags.DidNotRespond); - log?.WriteLine($" {Format.ToString(server)}: Returned, but incorrectly"); + log?.LogInformation($" {Format.ToString(server)}: Returned, but incorrectly"); } } else { server.SetUnselectable(UnselectableFlags.DidNotRespond); - log?.WriteLine($" {Format.ToString(server)}: Did not respond (Task.Status: {task.Status})"); + log?.LogInformation($" {Format.ToString(server)}: Did not respond (Task.Status: {task.Status})"); } } @@ -1638,12 +1639,12 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP { if (primary == preferred || primary.IsReplica) { - log?.WriteLine($"{Format.ToString(primary)}: Clearing as RedundantPrimary"); + log?.LogInformation($"{Format.ToString(primary)}: Clearing as RedundantPrimary"); primary.ClearUnselectable(UnselectableFlags.RedundantPrimary); } else { - log?.WriteLine($"{Format.ToString(primary)}: Setting as RedundantPrimary"); + log?.LogInformation($"{Format.ToString(primary)}: Setting as RedundantPrimary"); primary.SetUnselectable(UnselectableFlags.RedundantPrimary); } } @@ -1653,7 +1654,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP { ServerSelectionStrategy.ServerType = ServerType.Cluster; long coveredSlots = ServerSelectionStrategy.CountCoveredSlots(); - log?.WriteLine($"Cluster: {coveredSlots} of {ServerSelectionStrategy.TotalSlots} slots covered"); + log?.LogInformation($"Cluster: {coveredSlots} of {ServerSelectionStrategy.TotalSlots} slots covered"); } if (!first) { @@ -1661,11 +1662,11 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP long subscriptionChanges = EnsureSubscriptions(CommandFlags.FireAndForget); if (subscriptionChanges == 0) { - log?.WriteLine("No subscription changes necessary"); + log?.LogInformation("No subscription changes necessary"); } else { - log?.WriteLine($"Subscriptions attempting reconnect: {subscriptionChanges}"); + log?.LogInformation($"Subscriptions attempting reconnect: {subscriptionChanges}"); } } if (showStats) @@ -1676,15 +1677,14 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP string? stormLog = GetStormLog(); if (!string.IsNullOrWhiteSpace(stormLog)) { - log?.WriteLine(); - log?.WriteLine(stormLog); + log?.LogInformation(stormLog); } healthy = standaloneCount != 0 || clusterCount != 0 || sentinelCount != 0; if (first && !healthy && attemptsLeft > 0) { - log?.WriteLine("Resetting failing connections to retry..."); + log?.LogInformation("Resetting failing connections to retry..."); ResetAllNonConnected(); - log?.WriteLine($" Retrying - attempts left: {attemptsLeft}..."); + log?.LogInformation($" Retrying - attempts left: {attemptsLeft}..."); } //WTF("?: " + attempts); } while (first && !healthy && attemptsLeft > 0); @@ -1695,14 +1695,14 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, LogP } if (first) { - log?.WriteLine("Starting heartbeat..."); + log?.LogInformation("Starting heartbeat..."); pulse = TimerToken.Create(this); } if (publishReconfigure) { try { - log?.WriteLine("Broadcasting reconfigure..."); + log?.LogInformation("Broadcasting reconfigure..."); PublishReconfigureImpl(publishReconfigureFlags); } catch @@ -1733,7 +1733,7 @@ public EndPoint[] GetEndPoints(bool configuredOnly = false) => ? EndPoints.ToArray() : _serverSnapshot.GetEndPoints(); - private async Task GetEndpointsFromClusterNodes(ServerEndPoint server, LogProxy? log) + private async Task GetEndpointsFromClusterNodes(ServerEndPoint server, ILogger? log) { var message = Message.Create(-1, CommandFlags.None, RedisCommand.CLUSTER, RedisLiterals.NODES); try @@ -1755,7 +1755,7 @@ public EndPoint[] GetEndPoints(bool configuredOnly = false) => } catch (Exception ex) { - log?.WriteLine($"Encountered error while updating cluster config: {ex.Message}"); + log?.LogError(ex, $"Encountered error while updating cluster config: {ex.Message}"); return null; } } @@ -1769,9 +1769,9 @@ private void ResetAllNonConnected() } } - private static ServerEndPoint? NominatePreferredPrimary(LogProxy? log, ServerEndPoint[] servers, bool useTieBreakers, List primaries) + private static ServerEndPoint? NominatePreferredPrimary(ILogger? log, ServerEndPoint[] servers, bool useTieBreakers, List primaries) { - log?.WriteLine("Election summary:"); + log?.LogInformation("Election summary:"); Dictionary? uniques = null; if (useTieBreakers) @@ -1785,11 +1785,11 @@ private void ResetAllNonConnected() if (serverResult.IsNullOrWhiteSpace()) { - log?.WriteLine($" Election: {Format.ToString(server)} had no tiebreaker set"); + log?.LogInformation($" Election: {Format.ToString(server)} had no tiebreaker set"); } else { - log?.WriteLine($" Election: {Format.ToString(server)} nominates: {serverResult}"); + log?.LogInformation($" Election: {Format.ToString(server)} nominates: {serverResult}"); if (!uniques.TryGetValue(serverResult, out int count)) count = 0; uniques[serverResult] = count + 1; } @@ -1799,37 +1799,37 @@ private void ResetAllNonConnected() switch (primaries.Count) { case 0: - log?.WriteLine(" Election: No primaries detected"); + log?.LogInformation(" Election: No primaries detected"); return null; case 1: - log?.WriteLine($" Election: Single primary detected: {Format.ToString(primaries[0].EndPoint)}"); + log?.LogInformation($" Election: Single primary detected: {Format.ToString(primaries[0].EndPoint)}"); return primaries[0]; default: - log?.WriteLine(" Election: Multiple primaries detected..."); + log?.LogInformation(" Election: Multiple primaries detected..."); if (useTieBreakers && uniques != null) { switch (uniques.Count) { case 0: - log?.WriteLine(" Election: No nominations by tie-breaker"); + log?.LogInformation(" Election: No nominations by tie-breaker"); break; case 1: string unanimous = uniques.Keys.Single(); - log?.WriteLine($" Election: Tie-breaker unanimous: {unanimous}"); + log?.LogInformation($" Election: Tie-breaker unanimous: {unanimous}"); var found = SelectServerByElection(servers, unanimous, log); if (found != null) { - log?.WriteLine($" Election: Elected: {Format.ToString(found.EndPoint)}"); + log?.LogInformation($" Election: Elected: {Format.ToString(found.EndPoint)}"); return found; } break; default: - log?.WriteLine(" Election is contested:"); + log?.LogInformation(" Election is contested:"); ServerEndPoint? highest = null; bool arbitrary = false; foreach (var pair in uniques.OrderByDescending(x => x.Value)) { - log?.WriteLine($" Election: {pair.Key} has {pair.Value} votes"); + log?.LogInformation($" Election: {pair.Key} has {pair.Value} votes"); if (highest == null) { highest = SelectServerByElection(servers, pair.Key, log); @@ -1844,11 +1844,11 @@ private void ResetAllNonConnected() { if (arbitrary) { - log?.WriteLine($" Election: Choosing primary arbitrarily: {Format.ToString(highest.EndPoint)}"); + log?.LogInformation($" Election: Choosing primary arbitrarily: {Format.ToString(highest.EndPoint)}"); } else { - log?.WriteLine($" Election: Elected: {Format.ToString(highest.EndPoint)}"); + log?.LogInformation($" Election: Elected: {Format.ToString(highest.EndPoint)}"); } return highest; } @@ -1858,11 +1858,11 @@ private void ResetAllNonConnected() break; } - log?.WriteLine($" Election: Choosing primary arbitrarily: {Format.ToString(primaries[0].EndPoint)}"); + log?.LogInformation($" Election: Choosing primary arbitrarily: {Format.ToString(primaries[0].EndPoint)}"); return primaries[0]; } - private static ServerEndPoint? SelectServerByElection(ServerEndPoint[] servers, string endpoint, LogProxy? log) + private static ServerEndPoint? SelectServerByElection(ServerEndPoint[] servers, string endpoint, ILogger? log) { if (servers == null || string.IsNullOrWhiteSpace(endpoint)) return null; for (int i = 0; i < servers.Length; i++) @@ -1870,13 +1870,13 @@ private void ResetAllNonConnected() if (string.Equals(Format.ToString(servers[i].EndPoint), endpoint, StringComparison.OrdinalIgnoreCase)) return servers[i]; } - log?.WriteLine("...but we couldn't find that"); + log?.LogInformation("...but we couldn't find that"); var deDottedEndpoint = DeDotifyHost(endpoint); for (int i = 0; i < servers.Length; i++) { if (string.Equals(DeDotifyHost(Format.ToString(servers[i].EndPoint)), deDottedEndpoint, StringComparison.OrdinalIgnoreCase)) { - log?.WriteLine($"...but we did find instead: {deDottedEndpoint}"); + log?.LogInformation($"...but we did find instead: {deDottedEndpoint}"); return servers[i]; } } diff --git a/src/StackExchange.Redis/DebuggingAids.cs b/src/StackExchange.Redis/DebuggingAids.cs index 00f8dde90..6fb3b380e 100644 --- a/src/StackExchange.Redis/DebuggingAids.cs +++ b/src/StackExchange.Redis/DebuggingAids.cs @@ -4,7 +4,6 @@ namespace StackExchange.Redis { #if VERBOSE - partial class ConnectionMultiplexer { private readonly int epoch = Environment.TickCount; @@ -19,14 +18,6 @@ static partial void OnTraceWithoutContext(string message, string category) { Debug.WriteLine(message, Environment.CurrentManagedThreadId + " ~ " + category); } - - partial void OnTraceLog(LogProxy log, string caller) - { - lock (UniqueId) - { - Trace(log.ToString(), caller); // note that this won't always be useful, but we only do it in debug builds anyway - } - } } #endif diff --git a/src/StackExchange.Redis/EndPointCollection.cs b/src/StackExchange.Redis/EndPointCollection.cs index 29b223f24..44fd67e79 100644 --- a/src/StackExchange.Redis/EndPointCollection.cs +++ b/src/StackExchange.Redis/EndPointCollection.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Extensions.Logging; +using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -189,7 +190,7 @@ internal bool HasDnsEndPoints() return false; } - internal async Task ResolveEndPointsAsync(ConnectionMultiplexer multiplexer, LogProxy? log) + internal async Task ResolveEndPointsAsync(ConnectionMultiplexer multiplexer, ILogger? log) { var cache = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < Count; i++) @@ -208,12 +209,12 @@ internal async Task ResolveEndPointsAsync(ConnectionMultiplexer multiplexer, Log } else { - log?.WriteLine($"Using DNS to resolve '{dns.Host}'..."); + log?.LogInformation($"Using DNS to resolve '{dns.Host}'..."); var ips = await Dns.GetHostAddressesAsync(dns.Host).ObserveErrors().ForAwait(); if (ips.Length == 1) { ip = ips[0]; - log?.WriteLine($"'{dns.Host}' => {ip}"); + log?.LogInformation($"'{dns.Host}' => {ip}"); cache[dns.Host] = ip; this[i] = new IPEndPoint(ip, dns.Port); } @@ -222,7 +223,7 @@ internal async Task ResolveEndPointsAsync(ConnectionMultiplexer multiplexer, Log catch (Exception ex) { multiplexer.OnInternalError(ex); - log?.WriteLine(ex.Message); + log?.LogError(ex, ex.Message); } } } diff --git a/src/StackExchange.Redis/Enums/ServerType.cs b/src/StackExchange.Redis/Enums/ServerType.cs index 580bd8cee..19c6a3f19 100644 --- a/src/StackExchange.Redis/Enums/ServerType.cs +++ b/src/StackExchange.Redis/Enums/ServerType.cs @@ -39,7 +39,7 @@ internal static class ServerTypeExtensions }; /// - /// Whether a server type supports . + /// Whether a server type supports . /// internal static bool SupportsAutoConfigure(this ServerType type) => type switch { diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 4308b4f00..c1e53a329 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -438,19 +438,5 @@ internal static Exception UnableToConnect(ConnectionMultiplexer muxer, string? f return new RedisConnectionException(failureType, sb.ToString(), inner); } - - internal static Exception BeganProfilingWithDuplicateContext(object forContext) - { - var exc = new InvalidOperationException("Attempted to begin profiling for the same context twice"); - exc.Data["forContext"] = forContext; - return exc; - } - - internal static Exception FinishedProfilingWithInvalidContext(object forContext) - { - var exc = new InvalidOperationException("Attempted to finish profiling for a context which is no longer valid, or was never begun"); - exc.Data["forContext"] = forContext; - return exc; - } } } diff --git a/src/StackExchange.Redis/LogProxy.cs b/src/StackExchange.Redis/LogProxy.cs deleted file mode 100644 index 1e6fa7320..000000000 --- a/src/StackExchange.Redis/LogProxy.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.IO; - -namespace StackExchange.Redis; - -internal sealed class LogProxy : IDisposable -{ - public static LogProxy? TryCreate(TextWriter? writer) - => writer == null ? null : new LogProxy(writer); - - public override string ToString() - { - string? s = null; - if (_log != null) - { - lock (SyncLock) - { - s = _log?.ToString(); - } - } - return s ?? base.ToString() ?? string.Empty; - } - private TextWriter? _log; - internal static Action NullWriter = _ => { }; - - public object SyncLock => this; - private LogProxy(TextWriter log) => _log = log; - public void WriteLine() - { - if (_log != null) // note: double-checked - { - lock (SyncLock) - { - _log?.WriteLine(); - } - } - } - public void WriteLine(string? message = null) - { - if (_log != null) // note: double-checked - { - lock (SyncLock) - { - _log?.WriteLine($"{DateTime.UtcNow:HH:mm:ss.ffff}: {message}"); - } - } - } - public void WriteLine(string prefix, string message) - { - if (_log != null) // note: double-checked - { - lock (SyncLock) - { - _log?.WriteLine($"{DateTime.UtcNow:HH:mm:ss.ffff}: {prefix}{message}"); - } - } - } - public void Dispose() - { - if (_log != null) // note: double-checked - { - lock (SyncLock) { _log = null; } - } - } -} diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index a76001756..65df7d0e7 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -5,21 +5,22 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading; +using Microsoft.Extensions.Logging; using StackExchange.Redis.Profiling; namespace StackExchange.Redis { internal sealed class LoggingMessage : Message { - public readonly LogProxy log; + public readonly ILogger log; private readonly Message tail; - public static Message Create(LogProxy? log, Message tail) + public static Message Create(ILogger? log, Message tail) { return log == null ? tail : new LoggingMessage(log, tail); } - private LoggingMessage(LogProxy log, Message tail) : base(tail.Db, tail.Flags, tail.Command) + private LoggingMessage(ILogger log, Message tail) : base(tail.Db, tail.Flags, tail.Command) { this.log = log; this.tail = tail; @@ -37,14 +38,14 @@ protected override void WriteImpl(PhysicalConnection physical) try { var bridge = physical.BridgeCouldBeNull; - log?.WriteLine($"{bridge?.Name}: Writing: {tail.CommandAndKey}"); + log?.LogTrace($"{bridge?.Name}: Writing: {tail.CommandAndKey}"); } catch { } tail.WriteTo(physical); } public override int ArgCount => tail.ArgCount; - public LogProxy Log => log; + public ILogger Log => log; } internal abstract class Message : ICompletable diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 68ea70105..7041cf0af 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; #if !NETCOREAPP using Pipelines.Sockets.Unofficial.Threading; using static Pipelines.Sockets.Unofficial.Threading.MutexSlim; @@ -399,14 +400,14 @@ internal void KeepAlive() } } - internal async Task OnConnectedAsync(PhysicalConnection connection, LogProxy? log) + internal async Task OnConnectedAsync(PhysicalConnection connection, ILogger? log) { Trace("OnConnected"); if (physical == connection && !isDisposed && ChangeState(State.Connecting, State.ConnectedEstablishing)) { ConnectedAt ??= DateTime.UtcNow; await ServerEndPoint.OnEstablishingAsync(connection, log).ForAwait(); - log?.WriteLine($"{Format.ToString(ServerEndPoint)}: OnEstablishingAsync complete"); + log?.LogInformation($"{Format.ToString(ServerEndPoint)}: OnEstablishingAsync complete"); } else { @@ -428,8 +429,16 @@ internal void ResetNonConnected() TryConnect(null); } - internal void OnConnectionFailed(PhysicalConnection connection, ConnectionFailureType failureType, Exception innerException) + internal void OnConnectionFailed(PhysicalConnection connection, ConnectionFailureType failureType, Exception innerException, bool wasRequested) { + if (wasRequested) + { + Multiplexer.Logger?.LogInformation(innerException, innerException.Message); + } + else + { + Multiplexer.Logger?.LogError(innerException, innerException.Message); + } Trace($"OnConnectionFailed: {connection}"); // If we're configured to, fail all pending backlogged messages if (Multiplexer.RawConfig.BacklogPolicy?.AbortPendingOnConnectionFailure == true) @@ -550,7 +559,9 @@ internal void OnHeartbeat(bool ifConnectedOnly) if (shouldRetry) { Interlocked.Increment(ref connectTimeoutRetryCount); - LastException = ExceptionFactory.UnableToConnect(Multiplexer, "ConnectTimeout"); + var ex = ExceptionFactory.UnableToConnect(Multiplexer, "ConnectTimeout"); + LastException = ex; + Multiplexer.Logger?.LogError(ex, ex.Message); Trace("Aborting connect"); // abort and reconnect var snapshot = physical; @@ -986,7 +997,7 @@ private async Task ProcessBacklogAsync() break; } } - + var ex = ExceptionFactory.Timeout(Multiplexer, "The message was in the backlog when connection was disposed", message, ServerEndPoint, WriteResult.TimeoutBeforeWrite, this); message.SetExceptionAndComplete(ex, this); } @@ -1362,7 +1373,7 @@ private bool ChangeState(State oldState, State newState) return result; } - public PhysicalConnection? TryConnect(LogProxy? log) + public PhysicalConnection? TryConnect(ILogger? log) { if (state == (int)State.Disconnected) { @@ -1370,7 +1381,7 @@ private bool ChangeState(State oldState, State newState) { if (!Multiplexer.IsDisposed) { - log?.WriteLine($"{Name}: Connecting..."); + log?.LogInformation($"{Name}: Connecting..."); Multiplexer.Trace("Connecting...", Name); if (ChangeState(State.Disconnected, State.Connecting)) { @@ -1387,7 +1398,7 @@ private bool ChangeState(State oldState, State newState) } catch (Exception ex) { - log?.WriteLine($"{Name}: Connect failed: {ex.Message}"); + log?.LogError(ex, $"{Name}: Connect failed: {ex.Message}"); Multiplexer.Trace("Connect failed: " + ex.Message, Name); ChangeState(State.Disconnected); OnInternalError(ex); diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 595371529..eb0787606 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -17,6 +17,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using static StackExchange.Redis.Message; namespace StackExchange.Redis @@ -94,13 +95,13 @@ public PhysicalConnection(PhysicalBridge bridge) OnCreateEcho(); } - internal async Task BeginConnectAsync(LogProxy? log) + internal async Task BeginConnectAsync(ILogger? log) { var bridge = BridgeCouldBeNull; var endpoint = bridge?.ServerEndPoint?.EndPoint; if (bridge == null || endpoint == null) { - log?.WriteLine("No endpoint"); + log?.LogError(new ArgumentNullException(nameof(endpoint)), "No endpoint"); return; } @@ -125,7 +126,7 @@ internal async Task BeginConnectAsync(LogProxy? log) } } bridge.Multiplexer.OnConnecting(endpoint, bridge.ConnectionType); - log?.WriteLine($"{Format.ToString(endpoint)}: BeginConnectAsync"); + log?.LogInformation($"{Format.ToString(endpoint)}: BeginConnectAsync"); CancellationTokenSource? timeoutSource = null; try @@ -172,7 +173,7 @@ internal async Task BeginConnectAsync(LogProxy? log) } else if (await ConnectedAsync(x, log, bridge.Multiplexer.SocketManager!).ForAwait()) { - log?.WriteLine($"{Format.ToString(endpoint)}: Starting read"); + log?.LogInformation($"{Format.ToString(endpoint)}: Starting read"); try { StartReading(); @@ -190,9 +191,9 @@ internal async Task BeginConnectAsync(LogProxy? log) Shutdown(); } } - catch (ObjectDisposedException) + catch (ObjectDisposedException ex) { - log?.WriteLine($"{Format.ToString(endpoint)}: (socket shutdown)"); + log?.LogError(ex, $"{Format.ToString(endpoint)}: (socket shutdown)"); try { RecordConnectionFailed(ConnectionFailureType.UnableToConnect, isInitialConnect: true); } catch (Exception inner) { @@ -357,6 +358,15 @@ internal void SimulateConnectionFailure(SimulatedFailureType failureType) } } + /// + /// Did we ask for the shutdown? If so this leads to informational messages for tracking but not errors. + /// + private bool IsRequestedShutdown(PipeShutdownKind kind) => kind switch + { + PipeShutdownKind.ProtocolExitClient => true, + _ => false, + }; + public void RecordConnectionFailed( ConnectionFailureType failureType, Exception? innerException = null, @@ -365,6 +375,7 @@ public void RecordConnectionFailed( IDuplexPipe? connectingPipe = null ) { + bool weAskedForThis = false; Exception? outerException = innerException; IdentifyFailureType(innerException, ref failureType); var bridge = BridgeCouldBeNull; @@ -410,6 +421,9 @@ public void RecordConnectionFailed( var pipe = connectingPipe ?? _ioPipe; if (pipe is SocketConnection sc) { + // If the reason for the shutdown was we asked for the socket to die, don't log it as an error (only informational) + weAskedForThis = IsRequestedShutdown(sc.ShutdownKind); + exMessage.Append(" (").Append(sc.ShutdownKind); if (sc.SocketError != SocketError.Success) { @@ -477,7 +491,7 @@ void add(string lk, string sk, string? v) outerException.Data["Redis-" + kv.Item1] = kv.Item2; } - bridge?.OnConnectionFailed(this, failureType, outerException); + bridge?.OnConnectionFailed(this, failureType, outerException, wasRequested: weAskedForThis); } } // clean up (note: avoid holding the lock when we complete things, even if this means taking @@ -1448,7 +1462,7 @@ public ConnectionStatus GetStatus() return null; } - internal async ValueTask ConnectedAsync(Socket? socket, LogProxy? log, SocketManager manager) + internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, SocketManager manager) { var bridge = BridgeCouldBeNull; if (bridge == null) return false; @@ -1474,7 +1488,7 @@ internal async ValueTask ConnectedAsync(Socket? socket, LogProxy? log, Soc if (config.Ssl) { - log?.WriteLine("Configuring TLS"); + log?.LogInformation("Configuring TLS"); var host = config.SslHost; if (host.IsNullOrWhiteSpace()) { @@ -1508,9 +1522,10 @@ internal async ValueTask ConnectedAsync(Socket? socket, LogProxy? log, Soc { Debug.WriteLine(ex.Message); bridge.Multiplexer?.SetAuthSuspect(ex); + bridge.Multiplexer?.Logger?.LogError(ex, ex.Message); throw; } - log?.WriteLine($"TLS connection established successfully using protocol: {ssl.SslProtocol}"); + log?.LogInformation($"TLS connection established successfully using protocol: {ssl.SslProtocol}"); } catch (AuthenticationException authexception) { @@ -1533,7 +1548,7 @@ internal async ValueTask ConnectedAsync(Socket? socket, LogProxy? log, Soc _ioPipe = pipe; - log?.WriteLine($"{bridge.Name}: Connected "); + log?.LogInformation($"{bridge.Name}: Connected "); await bridge.OnConnectedAsync(this, log).ForAwait(); return true; diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 754037887..c49cf328a 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -245,6 +245,8 @@ StackExchange.Redis.ConfigurationOptions.KeepAlive.get -> int StackExchange.Redis.ConfigurationOptions.KeepAlive.set -> void StackExchange.Redis.ConfigurationOptions.LibraryName.get -> string? StackExchange.Redis.ConfigurationOptions.LibraryName.set -> void +StackExchange.Redis.ConfigurationOptions.LoggerFactory.get -> Microsoft.Extensions.Logging.ILoggerFactory? +StackExchange.Redis.ConfigurationOptions.LoggerFactory.set -> void StackExchange.Redis.ConfigurationOptions.Password.get -> string? StackExchange.Redis.ConfigurationOptions.Password.set -> void StackExchange.Redis.ConfigurationOptions.PreserveAsyncOrder.get -> bool @@ -1802,6 +1804,7 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludePerforma virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IsMatch(System.Net.EndPoint! endpoint) -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.KeepAliveInterval.get -> System.TimeSpan virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.LibraryName.get -> string! +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.LoggerFactory.get -> Microsoft.Extensions.Logging.ILoggerFactory? virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Password.get -> string? virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Proxy.get -> StackExchange.Redis.Proxy virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ReconnectRetryPolicy.get -> StackExchange.Redis.IReconnectRetryPolicy? diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 13b114da0..b8e0de696 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -406,20 +406,14 @@ public Task LastSaveAsync(CommandFlags flags = CommandFlags.None) } public void MakeMaster(ReplicationChangeOptions options, TextWriter? log = null) - { - using (var proxy = LogProxy.TryCreate(log)) - { - // Do you believe in magic? - multiplexer.MakePrimaryAsync(server, options, proxy).Wait(60000); - } + { + // Do you believe in magic? + multiplexer.MakePrimaryAsync(server, options, log).Wait(60000); } public async Task MakePrimaryAsync(ReplicationChangeOptions options, TextWriter? log = null) { - using (var proxy = LogProxy.TryCreate(log)) - { - await multiplexer.MakePrimaryAsync(server, options, proxy).ForAwait(); - } + await multiplexer.MakePrimaryAsync(server, options, log).ForAwait(); } public Role Role(CommandFlags flags = CommandFlags.None) diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index f541a2c24..236946d57 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -8,6 +8,7 @@ using System.Net; using System.Text; using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; using Pipelines.Sockets.Unofficial.Arenas; namespace StackExchange.Redis @@ -222,7 +223,7 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in { try { - logging.Log?.WriteLine($"Response from {bridge?.Name} / {message.CommandAndKey}: {result}"); + logging.Log?.LogInformation($"Response from {bridge?.Name} / {message.CommandAndKey}: {result}"); } catch { } } @@ -724,8 +725,8 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal sealed class AutoConfigureProcessor : ResultProcessor { - private LogProxy? Log { get; } - public AutoConfigureProcessor(LogProxy? log = null) => Log = log; + private ILogger? Log { get; } + public AutoConfigureProcessor(ILogger? log = null) => Log = log; public override bool SetResult(PhysicalConnection connection, Message message, in RawResult result) { @@ -735,7 +736,7 @@ public override bool SetResult(PhysicalConnection connection, Message message, i if (bridge != null) { var server = bridge.ServerEndPoint; - Log?.WriteLine($"{Format.ToString(server)}: Auto-configured role: replica"); + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured role: replica"); server.IsReplica = true; } } @@ -776,12 +777,12 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { case "master": server.IsReplica = false; - Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (INFO) role: primary"); + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) role: primary"); break; case "replica": case "slave": server.IsReplica = true; - Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (INFO) role: replica"); + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) role: replica"); break; } } @@ -798,7 +799,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (Version.TryParse(val, out Version? version)) { server.Version = version; - Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (INFO) version: " + version); + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) version: " + version); } } else if ((val = Extract(line, "redis_mode:")) != null) @@ -807,15 +808,15 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { case "standalone": server.ServerType = ServerType.Standalone; - Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (INFO) server-type: standalone"); + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) server-type: standalone"); break; case "cluster": server.ServerType = ServerType.Cluster; - Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (INFO) server-type: cluster"); + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) server-type: cluster"); break; case "sentinel": server.ServerType = ServerType.Sentinel; - Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (INFO) server-type: sentinel"); + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) server-type: sentinel"); break; } } @@ -834,7 +835,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes else if (message?.Command == RedisCommand.SENTINEL) { server.ServerType = ServerType.Sentinel; - Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (SENTINEL) server-type: sentinel"); + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (SENTINEL) server-type: sentinel"); } SetResult(message, true); return true; @@ -862,14 +863,14 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { targetSeconds = (timeoutSeconds * 3) / 4; } - Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (CONFIG) timeout: " + targetSeconds + "s"); + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (CONFIG) timeout: " + targetSeconds + "s"); server.WriteEverySeconds = targetSeconds; } } else if (key.IsEqual(CommonReplies.databases) && val.TryGetInt64(out i64)) { int dbCount = checked((int)i64); - Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (CONFIG) databases: " + dbCount); + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (CONFIG) databases: " + dbCount); server.Databases = dbCount; } else if (key.IsEqual(CommonReplies.slave_read_only) || key.IsEqual(CommonReplies.replica_read_only)) @@ -877,12 +878,12 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (val.IsEqual(CommonReplies.yes)) { server.ReplicaReadOnly = true; - Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (CONFIG) read-only replica: true"); + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (CONFIG) read-only replica: true"); } else if (val.IsEqual(CommonReplies.no)) { server.ReplicaReadOnly = false; - Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (CONFIG) read-only replica: false"); + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (CONFIG) read-only replica: false"); } } } @@ -890,7 +891,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes else if (message?.Command == RedisCommand.SENTINEL) { server.ServerType = ServerType.Sentinel; - Log?.WriteLine($"{Format.ToString(server)}: Auto-configured (SENTINEL) server-type: sentinel"); + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (SENTINEL) server-type: sentinel"); } SetResult(message, true); return true; diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index e661d8953..a90023580 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Extensions.Logging; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -103,11 +104,11 @@ public int Databases /// /// Awaitable state seeing if this endpoint is connected. /// - public Task OnConnectedAsync(LogProxy? log = null, bool sendTracerIfConnected = false, bool autoConfigureIfConnected = false) + public Task OnConnectedAsync(ILogger? log = null, bool sendTracerIfConnected = false, bool autoConfigureIfConnected = false) { - async Task IfConnectedAsync(LogProxy? log, bool sendTracerIfConnected, bool autoConfigureIfConnected) + async Task IfConnectedAsync(ILogger? log, bool sendTracerIfConnected, bool autoConfigureIfConnected) { - log?.WriteLine($"{Format.ToString(this)}: OnConnectedAsync already connected start"); + log?.LogInformation($"{Format.ToString(this)}: OnConnectedAsync already connected start"); if (autoConfigureIfConnected) { await AutoConfigureAsync(null, log).ForAwait(); @@ -116,15 +117,15 @@ async Task IfConnectedAsync(LogProxy? log, bool sendTracerIfConnected, b { await SendTracerAsync(log).ForAwait(); } - log?.WriteLine($"{Format.ToString(this)}: OnConnectedAsync already connected end"); + log?.LogInformation($"{Format.ToString(this)}: OnConnectedAsync already connected end"); return "Already connected"; } if (!IsConnected) { - log?.WriteLine($"{Format.ToString(this)}: OnConnectedAsync init (State={interactive?.ConnectionState})"); + log?.LogInformation($"{Format.ToString(this)}: OnConnectedAsync init (State={interactive?.ConnectionState})"); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _ = tcs.Task.ContinueWith(t => log?.WriteLine($"{Format.ToString(this)}: OnConnectedAsync completed ({t.Result})")); + _ = tcs.Task.ContinueWith(t => log?.LogInformation($"{Format.ToString(this)}: OnConnectedAsync completed ({t.Result})")); lock (_pendingConnectionMonitors) { _pendingConnectionMonitors.Add(tcs); @@ -218,7 +219,7 @@ public void Dispose() tmp?.Dispose(); } - public PhysicalBridge? GetBridge(ConnectionType type, bool create = true, LogProxy? log = null) + public PhysicalBridge? GetBridge(ConnectionType type, bool create = true, ILogger? log = null) { if (isDisposed) return null; return type switch @@ -339,7 +340,7 @@ public void ClearUnselectable(UnselectableFlags flags) public ValueTask TryWriteAsync(Message message) => GetBridge(message)?.TryWriteAsync(message, isReplica) ?? new ValueTask(WriteResult.NoConnectionAvailable); - internal void Activate(ConnectionType type, LogProxy? log) => GetBridge(type, true, log); + internal void Activate(ConnectionType type, ILogger? log) => GetBridge(type, true, log); internal void AddScript(string script, byte[] hash) { @@ -349,7 +350,7 @@ internal void AddScript(string script, byte[] hash) } } - internal async Task AutoConfigureAsync(PhysicalConnection? connection, LogProxy? log = null) + internal async Task AutoConfigureAsync(PhysicalConnection? connection, ILogger? log = null) { if (!serverType.SupportsAutoConfigure()) { @@ -358,7 +359,7 @@ internal async Task AutoConfigureAsync(PhysicalConnection? connection, LogProxy? return; } - log?.WriteLine($"{Format.ToString(this)}: Auto-configuring..."); + log?.LogInformation($"{Format.ToString(this)}: Auto-configuring..."); var commandMap = Multiplexer.CommandMap; const CommandFlags flags = CommandFlags.FireAndForget | CommandFlags.NoRedirect; @@ -429,7 +430,7 @@ internal async Task AutoConfigureAsync(PhysicalConnection? connection, LogProxy? // But if GETs are disabled on this, do not fail the connection - we just don't get tiebreaker benefits if (Multiplexer.RawConfig.TryGetTieBreaker(out var tieBreakerKey) && Multiplexer.CommandMap.IsAvailable(RedisCommand.GET)) { - log?.WriteLine($"{Format.ToString(EndPoint)}: Requesting tie-break (Key=\"{tieBreakerKey}\")..."); + log?.LogInformation($"{Format.ToString(EndPoint)}: Requesting tie-break (Key=\"{tieBreakerKey}\")..."); msg = Message.Create(0, flags, RedisCommand.GET, tieBreakerKey); msg.SetInternalCall(); msg = LoggingMessage.Create(log, msg); @@ -612,7 +613,7 @@ internal void OnDisconnected(PhysicalBridge bridge) } } - internal Task OnEstablishingAsync(PhysicalConnection connection, LogProxy? log) + internal Task OnEstablishingAsync(PhysicalConnection connection, ILogger? log) { static async Task OnEstablishingAsyncAwaited(PhysicalConnection connection, Task handshake) { @@ -800,7 +801,7 @@ internal void ReportNextFailure() subscription?.ReportNextFailure(); } - internal Task SendTracerAsync(LogProxy? log = null) + internal Task SendTracerAsync(ILogger? log = null) { var msg = GetTracerMessage(false); msg = LoggingMessage.Create(log, msg); @@ -879,7 +880,7 @@ internal ValueTask WriteDirectOrQueueFireAndForgetAsync(PhysicalConnection? c return default; } - private PhysicalBridge? CreateBridge(ConnectionType type, LogProxy? log) + private PhysicalBridge? CreateBridge(ConnectionType type, ILogger? log) { if (Multiplexer.IsDisposed) return null; Multiplexer.Trace(type.ToString()); @@ -888,9 +889,9 @@ internal ValueTask WriteDirectOrQueueFireAndForgetAsync(PhysicalConnection? c return bridge; } - private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) + private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) { - log?.WriteLine($"{Format.ToString(this)}: Server handshake"); + log?.LogInformation($"{Format.ToString(this)}: Server handshake"); if (connection == null) { Multiplexer.Trace("No connection!?"); @@ -903,14 +904,14 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) string password = config.Password ?? ""; if (!string.IsNullOrWhiteSpace(user)) { - log?.WriteLine($"{Format.ToString(this)}: Authenticating (user/password)"); + log?.LogInformation($"{Format.ToString(this)}: Authenticating (user/password)"); msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.AUTH, (RedisValue)user, (RedisValue)password); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } else if (!string.IsNullOrWhiteSpace(password)) { - log?.WriteLine($"{Format.ToString(this)}: Authenticating (password)"); + log?.LogInformation($"{Format.ToString(this)}: Authenticating (password)"); msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.AUTH, (RedisValue)password); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); @@ -924,7 +925,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) name = nameSanitizer.Replace(name, ""); if (!string.IsNullOrWhiteSpace(name)) { - log?.WriteLine($"{Format.ToString(this)}: Setting client name: {name}"); + log?.LogInformation($"{Format.ToString(this)}: Setting client name: {name}"); msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, RedisLiterals.SETNAME, (RedisValue)name); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); @@ -934,7 +935,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) { // note that this is a relatively new feature, but usually we won't know the // server version, so we will use this speculatively and hope for the best - log?.WriteLine($"{Format.ToString(this)}: Setting client lib/ver"); + log?.LogInformation($"{Format.ToString(this)}: Setting client lib/ver"); var libName = config.LibraryName; if (string.IsNullOrWhiteSpace(libName)) @@ -979,7 +980,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) var tracer = GetTracerMessage(true); tracer = LoggingMessage.Create(log, tracer); - log?.WriteLine($"{Format.ToString(this)}: Sending critical tracer (handshake): {tracer.CommandAndKey}"); + log?.LogInformation($"{Format.ToString(this)}: Sending critical tracer (handshake): {tracer.CommandAndKey}"); await WriteDirectOrQueueFireAndForgetAsync(connection, tracer, ResultProcessor.EstablishConnection).ForAwait(); // Note: this **must** be the last thing on the subscription handshake, because after this @@ -994,7 +995,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, LogProxy? log) await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.TrackSubscriptions).ForAwait(); } } - log?.WriteLine($"{Format.ToString(this)}: Flushing outbound buffer"); + log?.LogInformation($"{Format.ToString(this)}: Flushing outbound buffer"); await connection.FlushAsync().ForAwait(); } diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index b0e2a93ff..8cdc20730 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -16,6 +16,7 @@ + diff --git a/src/StackExchange.Redis/TextWriterLogger.cs b/src/StackExchange.Redis/TextWriterLogger.cs new file mode 100644 index 000000000..4a4a99fd4 --- /dev/null +++ b/src/StackExchange.Redis/TextWriterLogger.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using Microsoft.Extensions.Logging; + +namespace StackExchange.Redis; + +internal sealed class TextWriterLogger : ILogger, IDisposable +{ + private TextWriter? _writer; + private ILogger? _wrapped; + + internal static Action NullWriter = _ => { }; + + public TextWriterLogger(TextWriter writer, ILogger? wrapped) + { + _writer = writer; + _wrapped = wrapped; + } + + public IDisposable BeginScope(TState state) => NothingDisposable.Instance; + public bool IsEnabled(LogLevel logLevel) => _writer is not null || _wrapped is not null; + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + _wrapped?.Log(logLevel, eventId, state, exception, formatter); + if (_writer is TextWriter writer) + { + lock (writer) + { + writer.Write($"{DateTime.UtcNow:HH:mm:ss.ffff}: "); + writer.WriteLine(formatter(state, exception)); + } + } + } + + public void Dispose() + { + _writer = null; + _wrapped = null; + } +} + +internal static class TextWriterLoggerExtensions +{ + internal static ILogger? With(this ILogger? logger, TextWriter? writer) => + writer is not null ? new TextWriterLogger(writer, logger) : logger; +} + +internal sealed class NothingDisposable : IDisposable +{ + public static readonly NothingDisposable Instance = new NothingDisposable(); + public void Dispose() { } +} diff --git a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs index 3cccda4de..6bf0a8c58 100644 --- a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs +++ b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs @@ -49,11 +49,11 @@ public void DisconnectAndReconnectThrowsConnectionExceptionSync() var server = conn.GetServerSnapshot()[0]; server.SimulateConnectionFailure(SimulatedFailureType.All); - // Exception: The message timed out in the backlog attempting to send because no connection became available (100ms) - Last Connection Exception: SocketFailure (InputReaderCompleted, last-recv: 7) on 127.0.0.1:6379/Interactive, Idle/ReadAsync, last: PING, origin: SimulateConnectionFailure, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 100s, state: ConnectedEstablished, mgr: 10 of 10 available, in: 0, in-pipe: 0, out-pipe: 0, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.120.51136, command=PING, timeout: 100, inst: 13, qu: 1, qs: 0, aw: False, bw: Inactive, last-in: 0, cur-in: 0, sync-ops: 2, async-ops: 0, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: CRAVERTOP7(SE.Redis-v2.6.120.51136), IOCP: (Busy=0,Free=1000,Min=16,Max=1000), WORKER: (Busy=2,Free=32765,Min=16,Max=32767), POOL: (Threads=33,QueuedItems=0,CompletedItems=6237,Timers=39), v: 2.6.120.51136 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) + // Exception: The message timed out in the backlog attempting to send because no connection became available (400ms) - Last Connection Exception: SocketFailure (InputReaderCompleted, last-recv: 7) on 127.0.0.1:6379/Interactive, Idle/ReadAsync, last: PING, origin: SimulateConnectionFailure, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 100s, state: ConnectedEstablished, mgr: 10 of 10 available, in: 0, in-pipe: 0, out-pipe: 0, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.120.51136, command=PING, timeout: 100, inst: 13, qu: 1, qs: 0, aw: False, bw: Inactive, last-in: 0, cur-in: 0, sync-ops: 2, async-ops: 0, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 10 of 10 available, clientName: CRAVERTOP7(SE.Redis-v2.6.120.51136), IOCP: (Busy=0,Free=1000,Min=16,Max=1000), WORKER: (Busy=2,Free=32765,Min=16,Max=32767), POOL: (Threads=33,QueuedItems=0,CompletedItems=6237,Timers=39), v: 2.6.120.51136 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) var ex = Assert.ThrowsAny(() => db.Ping()); Log("Exception: " + ex.Message); Assert.True(ex is RedisConnectionException or RedisTimeoutException); - Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available (100ms) - Last Connection Exception: ", ex.Message); + Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available (400ms) - Last Connection Exception: ", ex.Message); Assert.NotNull(ex.InnerException); var iex = Assert.IsType(ex.InnerException); Assert.Contains(iex.Message, ex.Message); @@ -73,10 +73,10 @@ public async Task DisconnectAndNoReconnectThrowsConnectionExceptionAsync() var server = conn.GetServerSnapshot()[0]; server.SimulateConnectionFailure(SimulatedFailureType.All); - // Exception: The message timed out in the backlog attempting to send because no connection became available (100ms) - Last Connection Exception: SocketFailure (InputReaderCompleted, last-recv: 7) on 127.0.0.1:6379/Interactive, Idle/ReadAsync, last: PING, origin: SimulateConnectionFailure, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 100s, state: ConnectedEstablished, mgr: 8 of 10 available, in: 0, in-pipe: 0, out-pipe: 0, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.120.51136, command=PING, timeout: 100, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 8 of 10 available, clientName: CRAVERTOP7(SE.Redis-v2.6.120.51136), IOCP: (Busy=0,Free=1000,Min=16,Max=1000), WORKER: (Busy=6,Free=32761,Min=16,Max=32767), POOL: (Threads=33,QueuedItems=0,CompletedItems=5547,Timers=60), v: 2.6.120.51136 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) + // Exception: The message timed out in the backlog attempting to send because no connection became available (400ms) - Last Connection Exception: SocketFailure (InputReaderCompleted, last-recv: 7) on 127.0.0.1:6379/Interactive, Idle/ReadAsync, last: PING, origin: SimulateConnectionFailure, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 100s, state: ConnectedEstablished, mgr: 8 of 10 available, in: 0, in-pipe: 0, out-pipe: 0, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.120.51136, command=PING, timeout: 100, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 8 of 10 available, clientName: CRAVERTOP7(SE.Redis-v2.6.120.51136), IOCP: (Busy=0,Free=1000,Min=16,Max=1000), WORKER: (Busy=6,Free=32761,Min=16,Max=32767), POOL: (Threads=33,QueuedItems=0,CompletedItems=5547,Timers=60), v: 2.6.120.51136 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) var ex = await Assert.ThrowsAsync(() => db.PingAsync()); Log("Exception: " + ex.Message); - Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available (100ms) - Last Connection Exception: ", ex.Message); + Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available (400ms) - Last Connection Exception: ", ex.Message); Assert.NotNull(ex.InnerException); var iex = Assert.IsType(ex.InnerException); Assert.Contains(iex.Message, ex.Message); @@ -93,8 +93,8 @@ private ConnectionMultiplexer GetWorkingBacklogConn() => AbortOnConnectFail = false, BacklogPolicy = policy, ConnectTimeout = 50, - SyncTimeout = 100, - KeepAlive = 100, + SyncTimeout = 400, + KeepAlive = 400, AllowAdmin = true, }; } diff --git a/tests/StackExchange.Redis.Tests/AggresssiveTests.cs b/tests/StackExchange.Redis.Tests/AggressiveTests.cs similarity index 93% rename from tests/StackExchange.Redis.Tests/AggresssiveTests.cs rename to tests/StackExchange.Redis.Tests/AggressiveTests.cs index 04d472965..2e8b23f5c 100644 --- a/tests/StackExchange.Redis.Tests/AggresssiveTests.cs +++ b/tests/StackExchange.Redis.Tests/AggressiveTests.cs @@ -6,9 +6,9 @@ namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class AggresssiveTests : TestBase +public class AggressiveTests : TestBase { - public AggresssiveTests(ITestOutputHelper output) : base(output) { } + public AggressiveTests(ITestOutputHelper output) : base(output) { } [FactLongRunning] public async Task ParallelTransactionsWithConditions() @@ -59,7 +59,7 @@ public async Task ParallelTransactionsWithConditions() } var actual = (int)await muxers[0].GetDatabase().StringGetAsync(hits).ForAwait(); Assert.Equal(expectedSuccess, actual); - Writer.WriteLine($"success: {actual} out of {Workers * PerThread} attempts"); + Log($"success: {actual} out of {Workers * PerThread} attempts"); } finally { @@ -93,7 +93,7 @@ public void RunCompetingBatchesOnSameMuxer() x.Join(); y.Join(); - Writer.WriteLine(conn.GetCounters().Interactive); + Log(conn.GetCounters().Interactive.ToString()); } private void BatchRunIntegers(IDatabase db) @@ -114,7 +114,7 @@ private void BatchRunIntegers(IDatabase db) } var count = (long)db.StringGet(key); - Writer.WriteLine($"tally: {count}"); + Log($"tally: {count}"); } private static void BatchRunPings(IDatabase db) @@ -144,7 +144,7 @@ public async Task RunCompetingBatchesOnSameMuxerAsync() await x; await y; - Writer.WriteLine(conn.GetCounters().Interactive); + Log(conn.GetCounters().Interactive.ToString()); } private async Task BatchRunIntegersAsync(IDatabase db) @@ -168,7 +168,7 @@ private async Task BatchRunIntegersAsync(IDatabase db) } var count = (long)await db.StringGetAsync(key).ForAwait(); - Writer.WriteLine($"tally: {count}"); + Log($"tally: {count}"); } private static async Task BatchRunPingsAsync(IDatabase db) @@ -209,7 +209,7 @@ public void RunCompetingTransactionsOnSameMuxer() x.Join(); y.Join(); - Writer.WriteLine(conn.GetCounters().Interactive); + Log(conn.GetCounters().Interactive.ToString()); } private void TranRunIntegers(IDatabase db) @@ -231,7 +231,7 @@ private void TranRunIntegers(IDatabase db) } var count = (long)db.StringGet(key); - Writer.WriteLine($"tally: {count}"); + Log($"tally: {count}"); } private static void TranRunPings(IDatabase db) @@ -264,7 +264,7 @@ public async Task RunCompetingTransactionsOnSameMuxerAsync() await x; await y; - Writer.WriteLine(conn.GetCounters().Interactive); + Log(conn.GetCounters().Interactive.ToString()); } private async Task TranRunIntegersAsync(IDatabase db) @@ -289,7 +289,7 @@ private async Task TranRunIntegersAsync(IDatabase db) } var count = (long)await db.StringGetAsync(key).ForAwait(); - Writer.WriteLine($"tally: {count}"); + Log($"tally: {count}"); } private static async Task TranRunPingsAsync(IDatabase db) diff --git a/tests/StackExchange.Redis.Tests/AsyncTests.cs b/tests/StackExchange.Redis.Tests/AsyncTests.cs index bebd81438..760cc4c94 100644 --- a/tests/StackExchange.Redis.Tests/AsyncTests.cs +++ b/tests/StackExchange.Redis.Tests/AsyncTests.cs @@ -66,12 +66,12 @@ public async Task AsyncTimeoutIsNoticed() { await db.StringGetAsync(key).ForAwait(); // but *subsequent* operations are paused ms.Stop(); - Writer.WriteLine($"Unexpectedly succeeded after {ms.ElapsedMilliseconds}ms"); + Log($"Unexpectedly succeeded after {ms.ElapsedMilliseconds}ms"); }).ForAwait(); ms.Stop(); - Writer.WriteLine($"Timed out after {ms.ElapsedMilliseconds}ms"); + Log($"Timed out after {ms.ElapsedMilliseconds}ms"); - Writer.WriteLine("Exception message: " + ex.Message); + Log("Exception message: " + ex.Message); Assert.Contains("Timeout awaiting response", ex.Message); // Ensure we are including the last payload size Assert.Contains("last-in:", ex.Message); @@ -81,7 +81,7 @@ public async Task AsyncTimeoutIsNoticed() Assert.Contains("cur-in:", ex.Message); string status = conn.GetStatus(); - Writer.WriteLine(status); + Log(status); Assert.Contains("; async timeouts: 1;", status); } } diff --git a/tests/StackExchange.Redis.Tests/BacklogTests.cs b/tests/StackExchange.Redis.Tests/BacklogTests.cs index 30ad1cad9..d1c853577 100644 --- a/tests/StackExchange.Redis.Tests/BacklogTests.cs +++ b/tests/StackExchange.Redis.Tests/BacklogTests.cs @@ -16,20 +16,20 @@ public async Task FailFast() { void PrintSnapshot(ConnectionMultiplexer muxer) { - Writer.WriteLine("Snapshot summary:"); + Log("Snapshot summary:"); foreach (var server in muxer.GetServerSnapshot()) { - Writer.WriteLine($" {server.EndPoint}: "); - Writer.WriteLine($" Type: {server.ServerType}"); - Writer.WriteLine($" IsConnected: {server.IsConnected}"); - Writer.WriteLine($" IsConnecting: {server.IsConnecting}"); - Writer.WriteLine($" IsSelectable(allowDisconnected: true): {server.IsSelectable(RedisCommand.PING, true)}"); - Writer.WriteLine($" IsSelectable(allowDisconnected: false): {server.IsSelectable(RedisCommand.PING, false)}"); - Writer.WriteLine($" UnselectableFlags: {server.GetUnselectableFlags()}"); + Log($" {server.EndPoint}: "); + Log($" Type: {server.ServerType}"); + Log($" IsConnected: {server.IsConnected}"); + Log($" IsConnecting: {server.IsConnecting}"); + Log($" IsSelectable(allowDisconnected: true): {server.IsSelectable(RedisCommand.PING, true)}"); + Log($" IsSelectable(allowDisconnected: false): {server.IsSelectable(RedisCommand.PING, false)}"); + Log($" UnselectableFlags: {server.GetUnselectableFlags()}"); var bridge = server.GetBridge(RedisCommand.PING, create: false); - Writer.WriteLine($" GetBridge: {bridge}"); - Writer.WriteLine($" IsConnected: {bridge?.IsConnected}"); - Writer.WriteLine($" ConnectionState: {bridge?.ConnectionState}"); + Log($" GetBridge: {bridge}"); + Log($" IsConnected: {bridge?.IsConnected}"); + Log($" ConnectionState: {bridge?.ConnectionState}"); } } @@ -52,7 +52,7 @@ void PrintSnapshot(ConnectionMultiplexer muxer) using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); var db = conn.GetDatabase(); - Writer.WriteLine("Test: Initial (connected) ping"); + Log("Test: Initial (connected) ping"); await db.PingAsync(); var server = conn.GetServerSnapshot()[0]; @@ -60,25 +60,25 @@ void PrintSnapshot(ConnectionMultiplexer muxer) Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal // Fail the connection - Writer.WriteLine("Test: Simulating failure"); + Log("Test: Simulating failure"); conn.AllowConnect = false; server.SimulateConnectionFailure(SimulatedFailureType.All); Assert.False(conn.IsConnected); // Queue up some commands - Writer.WriteLine("Test: Disconnected pings"); + Log("Test: Disconnected pings"); await Assert.ThrowsAsync(() => db.PingAsync()); var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); Assert.False(conn.IsConnected); Assert.Equal(0, disconnectedStats.BacklogMessagesPending); - Writer.WriteLine("Test: Allowing reconnect"); + Log("Test: Allowing reconnect"); conn.AllowConnect = true; - Writer.WriteLine("Test: Awaiting reconnect"); + Log("Test: Awaiting reconnect"); await UntilConditionAsync(TimeSpan.FromSeconds(3), () => conn.IsConnected).ForAwait(); - Writer.WriteLine("Test: Reconnecting"); + Log("Test: Reconnecting"); Assert.True(conn.IsConnected); Assert.True(server.IsConnected); var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); @@ -129,7 +129,7 @@ public async Task QueuesAndFlushesAfterReconnectingAsync() conn.ConnectionRestored += (s, a) => Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); var db = conn.GetDatabase(); - Writer.WriteLine("Test: Initial (connected) ping"); + Log("Test: Initial (connected) ping"); await db.PingAsync(); var server = conn.GetServerSnapshot()[0]; @@ -137,13 +137,13 @@ public async Task QueuesAndFlushesAfterReconnectingAsync() Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal // Fail the connection - Writer.WriteLine("Test: Simulating failure"); + Log("Test: Simulating failure"); conn.AllowConnect = false; server.SimulateConnectionFailure(SimulatedFailureType.All); Assert.False(conn.IsConnected); // Queue up some commands - Writer.WriteLine("Test: Disconnected pings"); + Log("Test: Disconnected pings"); var ignoredA = db.PingAsync(); var ignoredB = db.PingAsync(); var lastPing = db.PingAsync(); @@ -154,40 +154,40 @@ public async Task QueuesAndFlushesAfterReconnectingAsync() Assert.False(conn.IsConnected); Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); - Writer.WriteLine("Test: Allowing reconnect"); + Log("Test: Allowing reconnect"); conn.AllowConnect = true; - Writer.WriteLine("Test: Awaiting reconnect"); + Log("Test: Awaiting reconnect"); await UntilConditionAsync(TimeSpan.FromSeconds(3), () => conn.IsConnected).ForAwait(); - Writer.WriteLine("Test: Checking reconnected 1"); + Log("Test: Checking reconnected 1"); Assert.True(conn.IsConnected); - Writer.WriteLine("Test: ignoredA Status: " + ignoredA.Status); - Writer.WriteLine("Test: ignoredB Status: " + ignoredB.Status); - Writer.WriteLine("Test: lastPing Status: " + lastPing.Status); + Log("Test: ignoredA Status: " + ignoredA.Status); + Log("Test: ignoredB Status: " + ignoredB.Status); + Log("Test: lastPing Status: " + lastPing.Status); var afterConnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); - Writer.WriteLine($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); + Log($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); - Writer.WriteLine("Test: Awaiting lastPing 1"); + Log("Test: Awaiting lastPing 1"); await lastPing; - Writer.WriteLine("Test: Checking reconnected 2"); + Log("Test: Checking reconnected 2"); Assert.True(conn.IsConnected); var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); Assert.Equal(0, reconnectedStats.BacklogMessagesPending); - Writer.WriteLine("Test: Pinging again..."); + Log("Test: Pinging again..."); _ = db.PingAsync(); _ = db.PingAsync(); - Writer.WriteLine("Test: Last Ping issued"); + Log("Test: Last Ping issued"); lastPing = db.PingAsync(); // We should see none queued - Writer.WriteLine("Test: BacklogMessagesPending check"); + Log("Test: BacklogMessagesPending check"); Assert.Equal(0, stats.BacklogMessagesPending); - Writer.WriteLine("Test: Awaiting lastPing 2"); + Log("Test: Awaiting lastPing 2"); await lastPing; - Writer.WriteLine("Test: Done"); + Log("Test: Done"); } finally { @@ -221,7 +221,7 @@ public async Task QueuesAndFlushesAfterReconnecting() conn.ConnectionRestored += (s, a) => Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); var db = conn.GetDatabase(); - Writer.WriteLine("Test: Initial (connected) ping"); + Log("Test: Initial (connected) ping"); await db.PingAsync(); var server = conn.GetServerSnapshot()[0]; @@ -229,13 +229,13 @@ public async Task QueuesAndFlushesAfterReconnecting() Assert.Equal(0, stats.BacklogMessagesPending); // Everything's normal // Fail the connection - Writer.WriteLine("Test: Simulating failure"); + Log("Test: Simulating failure"); conn.AllowConnect = false; server.SimulateConnectionFailure(SimulatedFailureType.All); Assert.False(conn.IsConnected); // Queue up some commands - Writer.WriteLine("Test: Disconnected pings"); + Log("Test: Disconnected pings"); Task[] pings = new Task[3]; pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(1)); @@ -248,7 +248,7 @@ void disconnectedPings(int id) var result = db.Ping(); Log($"Pinging (disconnected - {id}) - result: " + result); } - Writer.WriteLine("Test: Disconnected pings issued"); + Log("Test: Disconnected pings issued"); Assert.False(conn.IsConnected); // Give the tasks time to queue @@ -258,37 +258,37 @@ void disconnectedPings(int id) Log($"Test Stats: (BacklogMessagesPending: {disconnectedStats.BacklogMessagesPending}, TotalBacklogMessagesQueued: {disconnectedStats.TotalBacklogMessagesQueued})"); Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); - Writer.WriteLine("Test: Allowing reconnect"); + Log("Test: Allowing reconnect"); conn.AllowConnect = true; - Writer.WriteLine("Test: Awaiting reconnect"); + Log("Test: Awaiting reconnect"); await UntilConditionAsync(TimeSpan.FromSeconds(3), () => conn.IsConnected).ForAwait(); - Writer.WriteLine("Test: Checking reconnected 1"); + Log("Test: Checking reconnected 1"); Assert.True(conn.IsConnected); var afterConnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); - Writer.WriteLine($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); + Log($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); - Writer.WriteLine("Test: Awaiting 3 pings"); + Log("Test: Awaiting 3 pings"); await Task.WhenAll(pings); - Writer.WriteLine("Test: Checking reconnected 2"); + Log("Test: Checking reconnected 2"); Assert.True(conn.IsConnected); var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); Assert.Equal(0, reconnectedStats.BacklogMessagesPending); - Writer.WriteLine("Test: Pinging again..."); + Log("Test: Pinging again..."); pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(4)); pings[1] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(5)); pings[2] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(6)); - Writer.WriteLine("Test: Last Ping queued"); + Log("Test: Last Ping queued"); // We should see none queued - Writer.WriteLine("Test: BacklogMessagesPending check"); + Log("Test: BacklogMessagesPending check"); Assert.Equal(0, stats.BacklogMessagesPending); - Writer.WriteLine("Test: Awaiting 3 more pings"); + Log("Test: Awaiting 3 more pings"); await Task.WhenAll(pings); - Writer.WriteLine("Test: Done"); + Log("Test: Done"); } finally { @@ -319,7 +319,7 @@ public async Task QueuesAndFlushesAfterReconnectingClusterAsync() conn.ConnectionRestored += (s, a) => Log("Reconnected: " + EndPointCollection.ToString(a.EndPoint)); var db = conn.GetDatabase(); - Writer.WriteLine("Test: Initial (connected) ping"); + Log("Test: Initial (connected) ping"); await db.PingAsync(); RedisKey meKey = Me(); @@ -341,14 +341,14 @@ static Task PingAsync(ServerEndPoint server, CommandFlags flags = Comm } // Fail the connection - Writer.WriteLine("Test: Simulating failure"); + Log("Test: Simulating failure"); conn.AllowConnect = false; server.SimulateConnectionFailure(SimulatedFailureType.All); Assert.False(server.IsConnected); // Server isn't connected Assert.True(conn.IsConnected); // ...but the multiplexer is // Queue up some commands - Writer.WriteLine("Test: Disconnected pings"); + Log("Test: Disconnected pings"); var ignoredA = PingAsync(server); var ignoredB = PingAsync(server); var lastPing = PingAsync(server); @@ -358,42 +358,42 @@ static Task PingAsync(ServerEndPoint server, CommandFlags flags = Comm Assert.True(conn.IsConnected); Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); - Writer.WriteLine("Test: Allowing reconnect"); + Log("Test: Allowing reconnect"); conn.AllowConnect = true; - Writer.WriteLine("Test: Awaiting reconnect"); + Log("Test: Awaiting reconnect"); await UntilConditionAsync(TimeSpan.FromSeconds(3), () => server.IsConnected).ForAwait(); - Writer.WriteLine("Test: Checking reconnected 1"); + Log("Test: Checking reconnected 1"); Assert.True(server.IsConnected); Assert.True(conn.IsConnected); - Writer.WriteLine("Test: ignoredA Status: " + ignoredA.Status); - Writer.WriteLine("Test: ignoredB Status: " + ignoredB.Status); - Writer.WriteLine("Test: lastPing Status: " + lastPing.Status); + Log("Test: ignoredA Status: " + ignoredA.Status); + Log("Test: ignoredB Status: " + ignoredB.Status); + Log("Test: lastPing Status: " + lastPing.Status); var afterConnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); - Writer.WriteLine($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); + Log($"Test: BacklogStatus: {afterConnectedStats.BacklogStatus}, BacklogMessagesPending: {afterConnectedStats.BacklogMessagesPending}, IsWriterActive: {afterConnectedStats.IsWriterActive}, MessagesSinceLastHeartbeat: {afterConnectedStats.MessagesSinceLastHeartbeat}, TotalBacklogMessagesQueued: {afterConnectedStats.TotalBacklogMessagesQueued}"); - Writer.WriteLine("Test: Awaiting lastPing 1"); + Log("Test: Awaiting lastPing 1"); await lastPing; - Writer.WriteLine("Test: Checking reconnected 2"); + Log("Test: Checking reconnected 2"); Assert.True(server.IsConnected); Assert.True(conn.IsConnected); var reconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); Assert.Equal(0, reconnectedStats.BacklogMessagesPending); - Writer.WriteLine("Test: Pinging again..."); + Log("Test: Pinging again..."); _ = PingAsync(server); _ = PingAsync(server); - Writer.WriteLine("Test: Last Ping issued"); + Log("Test: Last Ping issued"); lastPing = PingAsync(server); // We should see none queued - Writer.WriteLine("Test: BacklogMessagesPending check"); + Log("Test: BacklogMessagesPending check"); Assert.Equal(0, stats.BacklogMessagesPending); - Writer.WriteLine("Test: Awaiting lastPing 2"); + Log("Test: Awaiting lastPing 2"); await lastPing; - Writer.WriteLine("Test: Done"); + Log("Test: Done"); } finally { diff --git a/tests/StackExchange.Redis.Tests/BasicOpTests.cs b/tests/StackExchange.Redis.Tests/BasicOpTests.cs index 60f260dc5..83499fed3 100644 --- a/tests/StackExchange.Redis.Tests/BasicOpTests.cs +++ b/tests/StackExchange.Redis.Tests/BasicOpTests.cs @@ -370,7 +370,7 @@ private static void Incr(IDatabase database, RedisKey key, int delta, ref int to [Fact] public void ShouldUseSharedMuxer() { - Writer.WriteLine($"Shared: {SharedFixtureAvailable}"); + Log($"Shared: {SharedFixtureAvailable}"); if (SharedFixtureAvailable) { using var a = Create(); diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index f5a2764c7..9229228ab 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -10,6 +10,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; using Xunit; using Xunit.Abstractions; @@ -185,7 +186,7 @@ public void TalkToNonsenseServer() } [Fact] - public async Task TestManaulHeartbeat() + public async Task TestManualHeartbeat() { var options = ConfigurationOptions.Parse(GetConfiguration()); options.HeartbeatInterval = TimeSpan.FromMilliseconds(100); @@ -569,6 +570,7 @@ public void BeforeSocketConnect() public async Task MutableOptions() { var options = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort + ",name=Details"); + options.LoggerFactory = NullLoggerFactory.Instance; var originalConfigChannel = options.ConfigurationChannel = "originalConfig"; var originalUser = options.User = "originalUser"; var originalPassword = options.Password = "originalPassword"; @@ -617,6 +619,7 @@ public async Task MutableOptions() Assert.Equal(originalPassword, conn.RawConfig.Password); var newPass = options.Password = "newPassword"; Assert.Equal(newPass, conn.RawConfig.Password); + Assert.Equal(options.LoggerFactory, conn.RawConfig.LoggerFactory); } [Theory] diff --git a/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs b/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs index 3f7576c65..68bc8dc6e 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs @@ -128,12 +128,12 @@ void innerScenario() } else { - Writer.WriteLine(outer.InnerException.ToString()); + Log(outer.InnerException.ToString()); if (outer.InnerException is AggregateException inner) { foreach (var ex in inner.InnerExceptions) { - Writer.WriteLine(ex.ToString()); + Log(ex.ToString()); } } Assert.False(true); // force fail diff --git a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs index 412bb8da5..e3941f749 100644 --- a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs +++ b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs @@ -4,6 +4,8 @@ using System.Net; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using StackExchange.Redis.Configuration; using Xunit; using Xunit.Abstractions; @@ -32,6 +34,7 @@ public class TestOptionsProvider : DefaultOptionsProvider protected override string GetDefaultClientName() => "TestPrefix-" + base.GetDefaultClientName(); public override bool IsMatch(EndPoint endpoint) => endpoint is DnsEndPoint dnsep && dnsep.Host.EndsWith(_domainSuffix); public override TimeSpan KeepAliveInterval => TimeSpan.FromSeconds(125); + public override ILoggerFactory? LoggerFactory => NullLoggerFactory.Instance; public override Proxy Proxy => Proxy.Twemproxy; public override IReconnectRetryPolicy ReconnectRetryPolicy => new TestRetryPolicy(); public override bool ResolveDns => true; @@ -97,6 +100,7 @@ private static void AssertAllOverrides(ConfigurationOptions options) Assert.Equal(new Version(1, 2, 3, 4), options.DefaultVersion); Assert.Equal(TimeSpan.FromSeconds(125), TimeSpan.FromSeconds(options.KeepAlive)); + Assert.Equal(NullLoggerFactory.Instance, options.LoggerFactory); Assert.Equal(Proxy.Twemproxy, options.Proxy); Assert.IsType(options.ReconnectRetryPolicy); Assert.True(options.ResolveDns); diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 02309ec79..864448610 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -110,7 +110,7 @@ public void TimeoutException() var msg = Message.Create(-1, CommandFlags.None, RedisCommand.PING); var rawEx = ExceptionFactory.Timeout(conn, "Test Timeout", msg, new ServerEndPoint(conn, server.EndPoint)); var ex = Assert.IsType(rawEx); - Writer.WriteLine("Exception: " + ex.Message); + Log("Exception: " + ex.Message); // Example format: "Test Timeout, command=PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, last-in: 0, cur-in: 0, serverEndpoint: 127.0.0.1:6379, mgr: 10 of 10 available, clientName: TimeoutException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), v: 2.1.0 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)"; Assert.StartsWith("Test Timeout, command=PING", ex.Message); @@ -190,7 +190,7 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple var msg = Message.Create(-1, CommandFlags.None, RedisCommand.PING); var rawEx = ExceptionFactory.NoConnectionAvailable(conn, msg, new ServerEndPoint(conn, server.EndPoint)); var ex = Assert.IsType(rawEx); - Writer.WriteLine("Exception: " + ex.Message); + Log("Exception: " + ex.Message); // Example format: "Exception: No connection is active/available to service this operation: PING, inst: 0, qu: 0, qs: 0, aw: False, in: 0, in-pipe: 0, out-pipe: 0, last-in: 0, cur-in: 0, serverEndpoint: 127.0.0.1:6379, mc: 1/1/0, mgr: 10 of 10 available, clientName: NoConnectionException, IOCP: (Busy=0,Free=1000,Min=8,Max=1000), WORKER: (Busy=2,Free=2045,Min=8,Max=2047), Local-CPU: 100%, v: 2.1.0.5"; Assert.StartsWith(messageStart, ex.Message); @@ -226,7 +226,7 @@ public void NoConnectionPrimaryOnlyException() Assert.True(msg.IsPrimaryOnly()); var rawEx = ExceptionFactory.NoConnectionAvailable(conn, msg, null); var ex = Assert.IsType(rawEx); - Writer.WriteLine("Exception: " + ex.Message); + Log("Exception: " + ex.Message); // Ensure a primary-only operation like SET gives the additional context Assert.StartsWith("No connection (requires writable - not eligible for replica) is active/available to service this operation: SET", ex.Message); diff --git a/tests/StackExchange.Redis.Tests/FailoverTests.cs b/tests/StackExchange.Redis.Tests/FailoverTests.cs index e9ff75d9a..aec2983e4 100644 --- a/tests/StackExchange.Redis.Tests/FailoverTests.cs +++ b/tests/StackExchange.Redis.Tests/FailoverTests.cs @@ -131,7 +131,7 @@ public async Task DereplicateGoesToPrimary() { conn.Configure(writer); string log = writer.ToString(); - Writer.WriteLine(log); + Log(log); bool isUnanimous = log.Contains("tie-break is unanimous at " + TestConfig.Current.FailoverPrimaryServerAndPort); if (!isUnanimous) Skip.Inconclusive("this is timing sensitive; unable to verify this time"); } @@ -146,33 +146,33 @@ public async Task DereplicateGoesToPrimary() var ex = Assert.Throws(() => db.IdentifyEndpoint(key, CommandFlags.DemandReplica)); Assert.StartsWith("No connection is active/available to service this operation: EXISTS " + Me(), ex.Message); - Writer.WriteLine("Invoking MakePrimaryAsync()..."); + Log("Invoking MakePrimaryAsync()..."); await primary.MakePrimaryAsync(ReplicationChangeOptions.Broadcast | ReplicationChangeOptions.ReplicateToOtherEndpoints | ReplicationChangeOptions.SetTiebreaker, Writer); - Writer.WriteLine("Finished MakePrimaryAsync() call."); + Log("Finished MakePrimaryAsync() call."); await Task.Delay(100).ConfigureAwait(false); - Writer.WriteLine("Invoking Ping() (post-primary)"); + Log("Invoking Ping() (post-primary)"); primary.Ping(); secondary.Ping(); - Writer.WriteLine("Finished Ping() (post-primary)"); + Log("Finished Ping() (post-primary)"); Assert.True(primary.IsConnected, $"{primary.EndPoint} is not connected."); Assert.True(secondary.IsConnected, $"{secondary.EndPoint} is not connected."); - Writer.WriteLine($"{primary.EndPoint}: {primary.ServerType}, Mode: {(primary.IsReplica ? "Replica" : "Primary")}"); - Writer.WriteLine($"{secondary.EndPoint}: {secondary.ServerType}, Mode: {(secondary.IsReplica ? "Replica" : "Primary")}"); + Log($"{primary.EndPoint}: {primary.ServerType}, Mode: {(primary.IsReplica ? "Replica" : "Primary")}"); + Log($"{secondary.EndPoint}: {secondary.ServerType}, Mode: {(secondary.IsReplica ? "Replica" : "Primary")}"); // Create a separate multiplexer with a valid view of the world to distinguish between failures of // server topology changes from failures to recognize those changes - Writer.WriteLine("Connecting to secondary validation connection."); + Log("Connecting to secondary validation connection."); using (var conn2 = ConnectionMultiplexer.Connect(config)) { var primary2 = conn2.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort); var secondary2 = conn2.GetServer(TestConfig.Current.FailoverReplicaServerAndPort); - Writer.WriteLine($"Check: {primary2.EndPoint}: {primary2.ServerType}, Mode: {(primary2.IsReplica ? "Replica" : "Primary")}"); - Writer.WriteLine($"Check: {secondary2.EndPoint}: {secondary2.ServerType}, Mode: {(secondary2.IsReplica ? "Replica" : "Primary")}"); + Log($"Check: {primary2.EndPoint}: {primary2.ServerType}, Mode: {(primary2.IsReplica ? "Replica" : "Primary")}"); + Log($"Check: {secondary2.EndPoint}: {secondary2.ServerType}, Mode: {(secondary2.IsReplica ? "Replica" : "Primary")}"); Assert.False(primary2.IsReplica, $"{primary2.EndPoint} should be a primary (verification connection)."); Assert.True(secondary2.IsReplica, $"{secondary2.EndPoint} should be a replica (verification connection)."); @@ -431,9 +431,9 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() } catch { - LogNoTime(""); + Log(""); Log("ERROR: Something went bad - see above! Roooooolling back. Back it up. Baaaaaack it on up."); - LogNoTime(""); + Log(""); throw; } finally diff --git a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs index 9c557911d..f61e73e32 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs @@ -221,7 +221,7 @@ public void Teardown(TextWriter output) { foreach (var item in privateExceptions.Take(5)) { - TestBase.LogNoTime(output, item); + TestBase.Log(output, item); } privateExceptions.Clear(); } diff --git a/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs b/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs index 5a9d267d3..fe54c472c 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs @@ -43,12 +43,6 @@ public override void WriteLine(string? value) try { - // Prevent double timestamps - if (value.Length < "HH:mm:ss.ffff:".Length || value["HH:mm:ss.ffff:".Length - 1] != ':') - { - base.Write(TestBase.Time()); - base.Write(": "); - } base.WriteLine(value); } catch (Exception ex) diff --git a/tests/StackExchange.Redis.Tests/LoggerTests.cs b/tests/StackExchange.Redis.Tests/LoggerTests.cs new file mode 100644 index 000000000..1d15a69b9 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/LoggerTests.cs @@ -0,0 +1,127 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests; + +[Collection(NonParallelCollection.Name)] +public class LoggerTests : TestBase +{ + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; + public LoggerTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public async Task BasicLoggerConfig() + { + var traceLogger = new TestLogger(LogLevel.Trace, Writer); + var debugLogger = new TestLogger(LogLevel.Debug, Writer); + var infoLogger = new TestLogger(LogLevel.Information, Writer); + var warningLogger = new TestLogger(LogLevel.Warning, Writer); + var errorLogger = new TestLogger(LogLevel.Error, Writer); + var criticalLogger = new TestLogger(LogLevel.Critical, Writer); + + var options = ConfigurationOptions.Parse(GetConfiguration()); + options.LoggerFactory = new TestWrapperLoggerFactory(new TestMultiLogger(traceLogger, debugLogger, infoLogger, warningLogger, errorLogger, criticalLogger)); + + using var conn = await ConnectionMultiplexer.ConnectAsync(options); + // We expect more at the trace level: GET, ECHO, PING on commands + Assert.True(traceLogger.CallCount > debugLogger.CallCount); + // Many calls for all log lines - don't set exact here since every addition would break the test + Assert.True(debugLogger.CallCount > 30); + Assert.True(infoLogger.CallCount > 30); + // No debug calls at this time + // We expect no error/critical level calls to have happened here + Assert.Equal(0, errorLogger.CallCount); + Assert.Equal(0, criticalLogger.CallCount); + } + + [Fact] + public async Task WrappedLogger() + { + var options = ConfigurationOptions.Parse(GetConfiguration()); + var wrapped = new TestWrapperLoggerFactory(NullLogger.Instance); + options.LoggerFactory = wrapped; + + using var conn = await ConnectionMultiplexer.ConnectAsync(options); + Assert.True(wrapped.Logger.LogCount > 0); + } + + public class TestWrapperLoggerFactory : ILoggerFactory + { + public TestWrapperLogger Logger { get; } + public TestWrapperLoggerFactory(ILogger logger) => Logger = new TestWrapperLogger(logger); + + public void AddProvider(ILoggerProvider provider) => throw new NotImplementedException(); + public ILogger CreateLogger(string categoryName) => Logger; + public void Dispose() { } + } + + public class TestWrapperLogger : ILogger + { + public int LogCount = 0; + public ILogger _inner { get; } + + public TestWrapperLogger(ILogger toWrap) => _inner = toWrap; + + public IDisposable BeginScope(TState state) => _inner.BeginScope(state); + public bool IsEnabled(LogLevel logLevel) => _inner.IsEnabled(logLevel); + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + Interlocked.Increment(ref LogCount); + _inner.Log(logLevel, eventId, state, exception, formatter); + } + } + + /// + /// To save on test time, no reason to spin up n connections just to test n logging implementations... + /// + private class TestMultiLogger : ILogger + { + private readonly ILogger[] _loggers; + public TestMultiLogger(params ILogger[] loggers) => _loggers = loggers; + + public IDisposable BeginScope(TState state) => throw new NotImplementedException(); + public bool IsEnabled(LogLevel logLevel) => true; + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + foreach (var logger in _loggers) + { + logger.Log(logLevel, eventId, state, exception, formatter); + } + } + } + + private class TestLogger : ILogger + { + private readonly StringBuilder sb = new StringBuilder(); + private long _callCount; + private readonly LogLevel _logLevel; + private readonly TextWriter _output; + public TestLogger(LogLevel logLevel, TextWriter output) => + (_logLevel, _output) = (logLevel, output); + + public IDisposable BeginScope(TState state) => throw new NotImplementedException(); + public bool IsEnabled(LogLevel logLevel) => logLevel >= _logLevel; + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + Interlocked.Increment(ref _callCount); + var logLine = $"{_logLevel}> [LogLevel: {logLevel}, EventId: {eventId}]: {formatter?.Invoke(state, exception)}"; + sb.AppendLine(logLine); + _output.WriteLine(logLine); + } + + public long CallCount => Interlocked.Read(ref _callCount); + public override string ToString() => sb.ToString(); + } +} diff --git a/tests/StackExchange.Redis.Tests/ParseTests.cs b/tests/StackExchange.Redis.Tests/ParseTests.cs index e0a46a2ce..a7c4248aa 100644 --- a/tests/StackExchange.Redis.Tests/ParseTests.cs +++ b/tests/StackExchange.Redis.Tests/ParseTests.cs @@ -70,13 +70,13 @@ public void ParseAsLotsOfChunks(string ascii, int expected) private void ProcessMessages(Arena arena, ReadOnlySequence buffer, int expected) { - Writer.WriteLine($"chain: {buffer.Length}"); + Log($"chain: {buffer.Length}"); var reader = new BufferReader(buffer); RawResult result; int found = 0; while (!(result = PhysicalConnection.TryParseResult(arena, buffer, ref reader, false, null, false)).IsNull) { - Writer.WriteLine($"{result} - {result.GetString()}"); + Log($"{result} - {result.GetString()}"); found++; } Assert.Equal(expected, found); diff --git a/tests/StackExchange.Redis.Tests/RoleTests.cs b/tests/StackExchange.Redis.Tests/RoleTests.cs index 09e88ce77..021ce8ebb 100644 --- a/tests/StackExchange.Redis.Tests/RoleTests.cs +++ b/tests/StackExchange.Redis.Tests/RoleTests.cs @@ -22,6 +22,12 @@ public void PrimaryRole(bool allowAdmin) // should work with or without admin no var primary = role as Role.Master; Assert.NotNull(primary); Assert.NotNull(primary.Replicas); + Log($"Searching for: {TestConfig.Current.ReplicaServer}:{TestConfig.Current.ReplicaPort}"); + Log($"Replica count: {primary.Replicas.Count}"); + foreach (var r in primary.Replicas) + { + Log($" Replica: {r.Ip}:{r.Port} (offset: {r.ReplicationOffset})"); + } Assert.Contains(primary.Replicas, r => r.Ip == TestConfig.Current.ReplicaServer && r.Port == TestConfig.Current.ReplicaPort); diff --git a/tests/StackExchange.Redis.Tests/SSLTests.cs b/tests/StackExchange.Redis.Tests/SSLTests.cs index 105fc031b..74c16e20c 100644 --- a/tests/StackExchange.Redis.Tests/SSLTests.cs +++ b/tests/StackExchange.Redis.Tests/SSLTests.cs @@ -249,7 +249,7 @@ public void RedisLabsSSL() var cert = new X509Certificate2(TestConfig.Current.RedisLabsPfxPath, ""); Assert.NotNull(cert); - Writer.WriteLine("Thumbprint: " + cert.Thumbprint); + Log("Thumbprint: " + cert.Thumbprint); int timeout = 5000; if (Debugger.IsAttached) timeout *= 100; @@ -385,7 +385,7 @@ public void SSLHostInferredFromEndpoints() private void Check(string name, object? x, object? y) { - Writer.WriteLine($"{name}: {(x == null ? "(null)" : x.ToString())} vs {(y == null ? "(null)" : y.ToString())}"); + Log($"{name}: {(x == null ? "(null)" : x.ToString())} vs {(y == null ? "(null)" : y.ToString())}"); Assert.Equal(x, y); } @@ -396,10 +396,10 @@ public void Issue883_Exhaustive() try { var all = CultureInfo.GetCultures(CultureTypes.AllCultures); - Writer.WriteLine($"Checking {all.Length} cultures..."); + Log($"Checking {all.Length} cultures..."); foreach (var ci in all) { - Writer.WriteLine("Testing: " + ci.Name); + Log("Testing: " + ci.Name); CultureInfo.CurrentCulture = ci; var a = ConfigurationOptions.Parse("myDNS:883,password=mypassword,connectRetry=3,connectTimeout=5000,syncTimeout=5000,defaultDatabase=0,ssl=true,abortConnect=false"); @@ -414,9 +414,9 @@ public void Issue883_Exhaustive() Ssl = true, AbortOnConnectFail = false, }; - Writer.WriteLine($"computed: {b.ToString(true)}"); + Log($"computed: {b.ToString(true)}"); - Writer.WriteLine("Checking endpoints..."); + Log("Checking endpoints..."); var c = a.EndPoints.Cast().Single(); var d = b.EndPoints.Cast().Single(); Check(nameof(c.Host), c.Host, d.Host); @@ -424,7 +424,7 @@ public void Issue883_Exhaustive() Check(nameof(c.AddressFamily), c.AddressFamily, d.AddressFamily); var fields = typeof(ConfigurationOptions).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - Writer.WriteLine($"Comparing {fields.Length} fields..."); + Log($"Comparing {fields.Length} fields..."); Array.Sort(fields, (x, y) => string.CompareOrdinal(x.Name, y.Name)); foreach (var field in fields) { @@ -478,7 +478,7 @@ void WriteStatus(X509ChainStatus[] status) for (int i = 0; i < status.Length; i++) { var item = status[i]; - output.WriteLine($"\tstatus {i}: {item.Status}, {item.StatusInformation}"); + Log(output, $"\tstatus {i}: {item.Status}, {item.StatusInformation}"); } } } @@ -486,9 +486,9 @@ void WriteStatus(X509ChainStatus[] status) { if (certificate != null) { - output.WriteLine($"Subject: {certificate.Subject}"); + Log(output, $"Subject: {certificate.Subject}"); } - output.WriteLine($"Policy errors: {sslPolicyErrors}"); + Log(output, $"Policy errors: {sslPolicyErrors}"); if (chain != null) { WriteStatus(chain.ChainStatus); @@ -499,7 +499,7 @@ void WriteStatus(X509ChainStatus[] status) int index = 0; foreach (var item in elements) { - output.WriteLine($"{index++}: {item.Certificate.Subject}; {item.Information}"); + Log(output, $"{index++}: {item.Certificate.Subject}; {item.Information}"); WriteStatus(item.ChainElementStatus); } } diff --git a/tests/StackExchange.Redis.Tests/SentinelBase.cs b/tests/StackExchange.Redis.Tests/SentinelBase.cs index 6b967c914..fc1a74967 100644 --- a/tests/StackExchange.Redis.Tests/SentinelBase.cs +++ b/tests/StackExchange.Redis.Tests/SentinelBase.cs @@ -176,7 +176,7 @@ static void LogEndpoints(IServer primary, Action log) if (replicaOffset == primaryOffset) { Log($"Done waiting for primary ({primaryOffset}) / replica ({replicaOffset}) replication to be in sync"); - LogEndpoints(primary, Log); + LogEndpoints(primary, m => Log(m)); return; } diff --git a/tests/StackExchange.Redis.Tests/SyncContextTests.cs b/tests/StackExchange.Redis.Tests/SyncContextTests.cs index a645f66c0..9acfa83be 100644 --- a/tests/StackExchange.Redis.Tests/SyncContextTests.cs +++ b/tests/StackExchange.Redis.Tests/SyncContextTests.cs @@ -14,12 +14,12 @@ public SyncContextTests(ITestOutputHelper testOutput) : base(testOutput) { } /* Note A (referenced below) * * When sync-context is *enabled*, we don't validate OpCount > 0 - this is because *with the additional checks*, - * it can genuinely happen that by the time we actually await it, it has completd - which results in a brittle test. + * it can genuinely happen that by the time we actually await it, it has completed - which results in a brittle test. */ [Theory] [InlineData(true)] [InlineData(false)] - public async Task DetectSyncContextUsafe(bool continueOnCapturedContext) + public async Task DetectSyncContextUnsafe(bool continueOnCapturedContext) { using var ctx = new MySyncContext(Writer); Assert.Equal(0, ctx.OpCount); @@ -30,7 +30,7 @@ public async Task DetectSyncContextUsafe(bool continueOnCapturedContext) private void AssertState(bool continueOnCapturedContext, MySyncContext ctx) { - LogNoTime($"Context in AssertState: {ctx}"); + Log($"Context in AssertState: {ctx}"); if (continueOnCapturedContext) { Assert.True(ctx.IsCurrent, nameof(ctx.IsCurrent)); @@ -63,9 +63,9 @@ public async Task AsyncPing(bool continueOnCapturedContext) using var conn = Create(); Assert.Equal(0, ctx.OpCount); var db = conn.GetDatabase(); - LogNoTime($"Context before await: {ctx}"); + Log($"Context before await: {ctx}"); await db.PingAsync().ConfigureAwait(continueOnCapturedContext); - + AssertState(continueOnCapturedContext, ctx); } @@ -87,11 +87,11 @@ public async Task AsyncConfigure(bool continueOnCapturedContext) using var ctx = new MySyncContext(Writer); using var conn = Create(); - LogNoTime($"Context initial: {ctx}"); + Log($"Context initial: {ctx}"); await Task.Delay(500); await conn.GetDatabase().PingAsync(); // ensure we're all ready ctx.Reset(); - LogNoTime($"Context before: {ctx}"); + Log($"Context before: {ctx}"); Assert.Equal(0, ctx.OpCount); Assert.True(await conn.ConfigureAsync(Writer).ConfigureAwait(continueOnCapturedContext), "config ran"); @@ -114,8 +114,8 @@ public async Task ConnectAsync(bool continueOnCapturedContext) public sealed class MySyncContext : SynchronizationContext, IDisposable { private readonly SynchronizationContext? _previousContext; - private readonly TextWriter? _log; - public MySyncContext(TextWriter? log) + private readonly TextWriter _log; + public MySyncContext(TextWriter log) { _previousContext = Current; _log = log; @@ -123,10 +123,7 @@ public MySyncContext(TextWriter? log) } public int OpCount => Thread.VolatileRead(ref _opCount); private int _opCount; - private void Incr() - { - Interlocked.Increment(ref _opCount); - } + private void Incr() => Interlocked.Increment(ref _opCount); public void Reset() => Thread.VolatileWrite(ref _opCount, 0); @@ -136,7 +133,7 @@ private void Incr() public override void Post(SendOrPostCallback d, object? state) { - _log?.WriteLine("sync-ctx: Post"); + Log(_log, "sync-ctx: Post"); Incr(); ThreadPool.QueueUserWorkItem(static state => { @@ -147,14 +144,14 @@ public override void Post(SendOrPostCallback d, object? state) private void Invoke(SendOrPostCallback d, object? state) { - _log?.WriteLine("sync-ctx: Invoke"); + Log(_log, "sync-ctx: Invoke"); if (!IsCurrent) SetSynchronizationContext(this); d(state); } public override void Send(SendOrPostCallback d, object? state) { - _log?.WriteLine("sync-ctx: Send"); + Log(_log, "sync-ctx: Send"); Incr(); Invoke(d, state); } @@ -177,6 +174,5 @@ public override void OperationCompleted() base.OperationCompleted(); } } - } } diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 4ba21d4f5..d1435d26f 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -46,19 +46,6 @@ protected TestBase(ITestOutputHelper output, SharedConnectionFixture? fixture = /// See 'ConnectFailTimeout' class for example usage. protected static Task RunBlockingSynchronousWithExtraThreadAsync(Action testScenario) => Task.Factory.StartNew(testScenario, CancellationToken.None, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); - protected void LogNoTime(string message) => LogNoTime(Writer, message); - internal static void LogNoTime(TextWriter output, string? message) - { - lock (output) - { - output.WriteLine(message); - } - if (TestConfig.Current.LogToConsole) - { - Console.WriteLine(message); - } - } - protected void Log(string? message) => LogNoTime(Writer, message); public static void Log(TextWriter output, string message) { lock (output) @@ -70,7 +57,7 @@ public static void Log(TextWriter output, string message) Console.WriteLine(message); } } - protected void Log(string message, params object?[] args) + protected void Log(string? message, params object?[] args) { lock (Output) { @@ -78,7 +65,7 @@ protected void Log(string message, params object?[] args) } if (TestConfig.Current.LogToConsole) { - Console.WriteLine(message, args); + Console.WriteLine(message ?? "", args); } } @@ -202,14 +189,14 @@ public void Teardown() { foreach (var item in privateExceptions.Take(5)) { - LogNoTime(item); + Log(item); } } lock (backgroundExceptions) { foreach (var item in backgroundExceptions.Take(5)) { - LogNoTime(item); + Log(item); } } Skip.Inconclusive($"There were {privateFailCount} private and {sharedFailCount.Value} ambient exceptions; expected {expectedFailCount}."); diff --git a/tests/StackExchange.Redis.Tests/TransactionTests.cs b/tests/StackExchange.Redis.Tests/TransactionTests.cs index d151e5985..075c0eb2c 100644 --- a/tests/StackExchange.Redis.Tests/TransactionTests.cs +++ b/tests/StackExchange.Redis.Tests/TransactionTests.cs @@ -1298,7 +1298,7 @@ public async Task ExecCompletes_Issue943() } } - Writer.WriteLine($"hash hit: {hashHit}, miss: {hashMiss}; expire hit: {expireHit}, miss: {expireMiss}"); + Log($"hash hit: {hashHit}, miss: {hashMiss}; expire hit: {expireHit}, miss: {expireMiss}"); Assert.Equal(0, hashMiss); Assert.Equal(0, expireMiss); } From d206ee369a7873bd7c8974d966b66b321b51ff48 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 22 Aug 2023 16:31:36 +0100 Subject: [PATCH 246/435] Satisfy pointer arithmetic check (#2526) - satisfy infosec scanner that we're not risking pointer weirdness (although in reality, this is always UTF8; can't be hijacked) - prefer EncodingExtensions.GetString when it is available - switch payload to an explicit field so we can use ^^^ via pass-by-ref There is no actual infosec here; if the inbuilt UTF8 implementation is broken, we're already hosed; there is no extension point to replace UTF8 --- src/StackExchange.Redis/RawResult.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index fc189f10c..ee4aeaa86 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -11,7 +11,9 @@ internal readonly struct RawResult internal ref RawResult this[int index] => ref GetItems()[index]; internal int ItemsCount => (int)_items.Length; - internal ReadOnlySequence Payload { get; } + + private readonly ReadOnlySequence _payload; + internal ReadOnlySequence Payload => _payload; internal static readonly RawResult NullMultiBulk = new RawResult(default(Sequence), isNull: true); internal static readonly RawResult EmptyMultiBulk = new RawResult(default(Sequence), isNull: false); @@ -36,14 +38,14 @@ public RawResult(ResultType resultType, in ReadOnlySequence payload, bool } if (!isNull) resultType |= NonNullFlag; _type = resultType; - Payload = payload; + _payload = payload; _items = default; } public RawResult(Sequence items, bool isNull) { _type = isNull ? ResultType.MultiBulk : (ResultType.MultiBulk | NonNullFlag); - Payload = default; + _payload = default; _items = items.Untyped(); } @@ -332,6 +334,10 @@ private static GeoPosition AsGeoPosition(in Sequence coords) { return Format.GetString(Payload.First.Span); } +#if NET6_0_OR_GREATER + // use system-provided sequence decoder + return Encoding.UTF8.GetString(in _payload); +#else var decoder = Encoding.UTF8.GetDecoder(); int charCount = 0; foreach(var segment in Payload) @@ -359,12 +365,16 @@ private static GeoPosition AsGeoPosition(in Sequence coords) fixed (byte* bPtr = span) { var written = decoder.GetChars(bPtr, span.Length, cPtr, charCount, false); + if (written < 0 || written > charCount) Throw(); // protect against hypothetical cPtr weirdness cPtr += written; charCount -= written; } } } return s; + + static void Throw() => throw new InvalidOperationException("Invalid result from GetChars"); +#endif } internal bool TryGetDouble(out double val) From 2e9e8e969552b0a0ba7c1baff615d363847e2769 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 22 Aug 2023 12:51:57 -0400 Subject: [PATCH 247/435] CodeQL: Initial pass (#2503) Setting up CodeQL scanning on the repo to see where it's at, and hopefully leave enabled as a safeguard. --- .github/workflows/codeql.yml | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..1d560447c --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,62 @@ +name: "CodeQL" + +on: + push: + branches: [ 'main' ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ 'main' ] + schedule: + - cron: '8 9 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - if: matrix.language != 'csharp' + name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - if: matrix.language == 'csharp' + name: .NET Build + run: dotnet build Build.csproj -c Release /p:CI=true + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" \ No newline at end of file From 20f41f6eb27a7a0f19ce38495b49e8a7c4e2ab1e Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Tue, 5 Sep 2023 21:40:06 +0800 Subject: [PATCH 248/435] update pre-release package badge link (#2539) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3823e855a..44c06c537 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,6 @@ MyGet Pre-release feed: https://www.myget.org/gallery/stackoverflow | Package | NuGet Stable | NuGet Pre-release | Downloads | MyGet | | ------- | ------------ | ----------------- | --------- | ----- | -| [StackExchange.Redis](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/dt/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | +| [StackExchange.Redis](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/absoluteLatest) | [![StackExchange.Redis](https://img.shields.io/nuget/dt/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | -Release notes at: https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes \ No newline at end of file +Release notes at: https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes From cfb110cf4a6014eea305ababefda6f0559504178 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 7 Sep 2023 17:02:10 +0100 Subject: [PATCH 249/435] Protocol support: RESP3 (#2396) Overall changes: - [x] introduce `Resp2Type` and `Resp3Type` shims (`Resp2Type` has reduced types); existing code using `[Obsolete] Type` uses `Resp2Type` for minimal code impact - [x] mark existing `Type` as `[Obsolete]`, and proxy to `Resp2Type` for compat - [x] deal with null handling differences - [x] deal with `Boolean`, which works very differently (`t`/`f` instead of `1`/`0`) - [x] deal with `[+|-]{inf|nan}` when parsing doubles (explicitly called out in the RESP3 spec) - [x] parse new tokens - [x] `HELLO` handshake - [x] core message and result handling - [x] validation and fallback - [x] prove all return types (see: https://github.com/redis/redis-specifications/issues/15) - [x] streamed RESP3 omitting; not implemented by server; can revisit - [x] deal with pub/sub differences - [x] check routing of publish etc - [x] check re-wire of subscription if failed - [x] check receive notifications - [x] connection management (i.e. not to spin up in resp3) - [x] connection fallback spinup (i.e. if we were trying resp3 but failed) - [x] other - [x] [undocumented RESP3 delta](https://github.com/redis/redis-doc/pull/2513) - [x] run core tests in both RESP2 and RESP3 - [x] compensate for tests that expect separate subscription connections Co-authored-by: Nick Craver Co-authored-by: Nick Craver --- Directory.Packages.props | 4 +- StackExchange.Redis.sln | 1 + docs/Configuration.md | 41 +- docs/ReleaseNotes.md | 1 + docs/Resp3.md | 45 ++ docs/index.md | 1 + .../APITypes/LatencyHistoryEntry.cs | 2 +- .../APITypes/LatencyLatestEntry.cs | 2 +- .../ChannelMessageQueue.cs | 5 +- src/StackExchange.Redis/ClientInfo.cs | 13 +- src/StackExchange.Redis/CommandTrace.cs | 4 +- src/StackExchange.Redis/Condition.cs | 10 +- .../ConfigurationOptions.cs | 88 ++- .../ConnectionMultiplexer.Compat.cs | 3 + .../ConnectionMultiplexer.cs | 14 +- src/StackExchange.Redis/DebuggingAids.cs | 5 +- src/StackExchange.Redis/Enums/CommandFlags.cs | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 2 + src/StackExchange.Redis/Enums/ResultType.cs | 74 ++- src/StackExchange.Redis/ExceptionFactory.cs | 10 +- src/StackExchange.Redis/Format.cs | 138 +++- .../Interfaces/IConnectionMultiplexer.cs | 13 + src/StackExchange.Redis/Interfaces/IServer.cs | 7 + src/StackExchange.Redis/LoggingPipe.cs | 8 +- src/StackExchange.Redis/Message.cs | 57 +- src/StackExchange.Redis/PhysicalBridge.cs | 8 +- src/StackExchange.Redis/PhysicalConnection.cs | 162 +++-- .../Profiling/ProfiledCommand.cs | 2 +- .../PublicAPI/PublicAPI.Shipped.txt | 1 - .../PublicAPI/PublicAPI.Unshipped.txt | 32 +- src/StackExchange.Redis/RawResult.cs | 170 +++-- src/StackExchange.Redis/RedisDatabase.cs | 14 +- src/StackExchange.Redis/RedisFeatures.cs | 144 +++-- src/StackExchange.Redis/RedisLiterals.cs | 11 +- src/StackExchange.Redis/RedisProtocol.cs | 21 + src/StackExchange.Redis/RedisResult.cs | 150 ++++- src/StackExchange.Redis/RedisServer.cs | 8 +- src/StackExchange.Redis/RedisSubscriber.cs | 3 + src/StackExchange.Redis/RedisTransaction.cs | 9 +- src/StackExchange.Redis/ResultBox.cs | 1 - src/StackExchange.Redis/ResultProcessor.cs | 597 +++++++++++------- .../ResultTypeExtensions.cs | 11 + src/StackExchange.Redis/Role.cs | 3 + .../ScriptParameterMapper.cs | 8 +- src/StackExchange.Redis/ServerEndPoint.cs | 133 +++- .../AggressiveTests.cs | 4 +- tests/StackExchange.Redis.Tests/AsyncTests.cs | 8 +- tests/StackExchange.Redis.Tests/BitTests.cs | 1 + .../StackExchange.Redis.Tests/ClusterTests.cs | 19 +- .../StackExchange.Redis.Tests/ConfigTests.cs | 37 +- .../ConnectCustomConfigTests.cs | 4 +- .../ConnectToUnexistingHostTests.cs | 2 +- tests/StackExchange.Redis.Tests/EnvoyTests.cs | 4 +- .../EventArgsTests.cs | 14 +- .../ExceptionFactoryTests.cs | 14 +- .../StackExchange.Redis.Tests/ExpiryTests.cs | 2 +- .../FailoverTests.cs | 6 +- tests/StackExchange.Redis.Tests/GeoTests.cs | 15 +- tests/StackExchange.Redis.Tests/HashTests.cs | 1 + .../Helpers/Attributes.cs | 100 ++- .../Helpers/Extensions.cs | 4 +- .../Helpers/IRedisTest.cs | 8 + .../Helpers/SharedConnectionFixture.cs | 52 +- .../Helpers/TestContext.cs | 20 + .../HyperLogLogTests.cs | 3 +- .../Issues/BgSaveResponseTests.cs | 2 +- .../Issues/SO10504853Tests.cs | 2 +- .../Issues/SO25567566Tests.cs | 2 +- tests/StackExchange.Redis.Tests/KeyTests.cs | 5 +- tests/StackExchange.Redis.Tests/ListTests.cs | 1 + .../StackExchange.Redis.Tests/LockingTests.cs | 2 +- .../StackExchange.Redis.Tests/MemoryTests.cs | 8 +- .../OverloadCompatTests.cs | 1 + tests/StackExchange.Redis.Tests/ParseTests.cs | 2 +- .../ProfilingTests.cs | 4 +- .../PubSubCommandTests.cs | 3 +- .../PubSubMultiserverTests.cs | 47 +- .../StackExchange.Redis.Tests/PubSubTests.cs | 3 +- .../RawResultTests.cs | 18 +- .../RespProtocolTests.cs | 431 +++++++++++++ tests/StackExchange.Redis.Tests/RoleTests.cs | 6 +- tests/StackExchange.Redis.Tests/SSLTests.cs | 2 +- tests/StackExchange.Redis.Tests/ScanTests.cs | 4 +- .../ScriptingTests.cs | 10 +- .../StackExchange.Redis.Tests/SecureTests.cs | 2 +- .../ServerSnapshotTests.cs | 3 + tests/StackExchange.Redis.Tests/SetTests.cs | 3 +- .../SortedSetTests.cs | 78 ++- .../StackExchange.Redis.Tests/StreamTests.cs | 146 ++--- .../StackExchange.Redis.Tests/StringTests.cs | 1 + tests/StackExchange.Redis.Tests/TestBase.cs | 139 ++-- .../TransactionTests.cs | 1 + .../xunit.runner.json | 2 +- toys/StackExchange.Redis.Server/RespServer.cs | 6 +- .../TypedRedisValue.cs | 12 +- version.json | 2 +- 96 files changed, 2530 insertions(+), 773 deletions(-) create mode 100644 docs/Resp3.md create mode 100644 src/StackExchange.Redis/RedisProtocol.cs create mode 100644 src/StackExchange.Redis/ResultTypeExtensions.cs create mode 100644 tests/StackExchange.Redis.Tests/Helpers/IRedisTest.cs create mode 100644 tests/StackExchange.Redis.Tests/Helpers/TestContext.cs create mode 100644 tests/StackExchange.Redis.Tests/RespProtocolTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 2304eb77e..2280f9df2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,7 +23,7 @@ - - + + \ No newline at end of file diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index cdd254217..1fa39f4c2 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -119,6 +119,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{153A10E4-E docs\Profiling_v2.md = docs\Profiling_v2.md docs\PubSubOrder.md = docs\PubSubOrder.md docs\ReleaseNotes.md = docs\ReleaseNotes.md + docs\Resp3.md = docs\Resp3.md docs\Scripting.md = docs\Scripting.md docs\Server.md = docs\Server.md docs\Testing.md = docs\Testing.md diff --git a/docs/Configuration.md b/docs/Configuration.md index 5893bf855..2f63c5358 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -1,4 +1,4 @@ -Configuration +# Configuration === When connecting to Redis version 6 or above with an ACL configured, your ACL user needs to at least have permissions to run the ECHO command. We run this command to verify that we have a valid connection to the Redis service. @@ -15,7 +15,7 @@ The `configuration` here can be either: The latter is *basically* a tokenized form of the former. -Basic Configuration Strings +## Basic Configuration Strings - The *simplest* configuration example is just the host name: @@ -66,7 +66,7 @@ Microsoft Azure Redis example with password var conn = ConnectionMultiplexer.Connect("contoso5.redis.cache.windows.net,ssl=true,password=..."); ``` -Configuration Options +## Configuration Options --- The `ConfigurationOptions` object has a wide range of properties, all of which are fully documented in intellisense. Some of the more common options to use include: @@ -98,6 +98,7 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a | version={string} | `DefaultVersion` | (`4.0` in Azure, else `2.0`) | Redis version level (useful when the server does not make this available) | | tunnel={string} | `Tunnel` | `null` | Tunnel for connections (use `http:{proxy url}` for "connect"-based proxy server) | | setlib={bool} | `SetClientLibrary` | `true` | Whether to attempt to use `CLIENT SETINFO` to set the library name/version on the connection | +| protocol={string} | `Protocol` | `null` | Redis protocol to use; see section below | Additional code-only options: - LoggerFactory (`ILoggerFactory`) - Default: `null` @@ -123,8 +124,9 @@ Additional code-only options: Tokens in the configuration string are comma-separated; any without an `=` sign are assumed to be redis server endpoints. Endpoints without an explicit port will use 6379 if ssl is not enabled, and 6380 if ssl is enabled. Tokens starting with `$` are taken to represent command maps, for example: `$config=cfg`. -Obsolete Configuration Options +## Obsolete Configuration Options --- + These options are parsed in connection strings for backwards compatibility (meaning they do not error as invalid), but no longer have any effect. | Configuration string | `ConfigurationOptions` | Previous Default | Previous Meaning | @@ -132,7 +134,7 @@ These options are parsed in connection strings for backwards compatibility (mean | responseTimeout={int} | `ResponseTimeout` | `SyncTimeout` | Time (ms) to decide whether the socket is unhealthy | | writeBuffer={int} | `WriteBuffer` | `4096` | Size of the output buffer | -Automatic and Manual Configuration +## Automatic and Manual Configuration --- In many common scenarios, StackExchange.Redis will automatically configure a lot of settings, including the server type and version, connection timeouts, and primary/replica relationships. Sometimes, though, the commands for this have been disabled on the redis server. In this case, it is useful to provide more information: @@ -161,7 +163,8 @@ Which is equivalent to the command string: ```config redis0:6379,redis1:6380,keepAlive=180,version=2.8.8,$CLIENT=,$CLUSTER=,$CONFIG=,$ECHO=,$INFO=,$PING= ``` -Renaming Commands + +## Renaming Commands --- A slightly unusual feature of redis is that you can disable and/or rename individual commands. As per the previous example, this is done via the `CommandMap`, but instead of passing a `HashSet` to `Create()` (to indicate the available or unavailable commands), you pass a `Dictionary`. All commands not mentioned in the dictionary are assumed to be enabled and not renamed. A `null` or blank value records that the command is disabled. For example: @@ -184,8 +187,9 @@ The above is equivalent to (in the connection string): $INFO=,$SELECT=use ``` -Redis Server Permissions +## Redis Server Permissions --- + If the user you're connecting to Redis with is limited, it still needs to have certain commands enabled for the StackExchange.Redis to succeed in connecting. The client uses: - `AUTH` to authenticate - `CLIENT` to set the client name @@ -205,7 +209,7 @@ For example, a common _very_ minimal configuration ACL on the server (non-cluste Note that if you choose to disable access to the above commands, it needs to be done via the `CommandMap` and not only the ACL on the server (otherwise we'll attempt the command and fail the handshake). Also, if any of the these commands are disabled, some functionality may be diminished or broken. -twemproxy +## twemproxy --- [twemproxy](https://github.com/twitter/twemproxy) is a tool that allows multiple redis instances to be used as though it were a single server, with inbuilt sharding and fault tolerance (much like redis cluster, but implemented separately). The feature-set available to Twemproxy is reduced. To avoid having to configure this manually, the `Proxy` option can be used: @@ -218,8 +222,9 @@ var options = new ConfigurationOptions }; ``` -envoyproxy +##envoyproxy --- + [Envoyproxy](https://github.com/envoyproxy/envoy) is a tool that allows to front a redis cluster with a set of proxies, with inbuilt discovery and fault tolerance. The feature-set available to Envoyproxy is reduced. To avoid having to configure this manually, the `Proxy` option can be used: ```csharp var options = new ConfigurationOptions+{ @@ -229,7 +234,7 @@ var options = new ConfigurationOptions+{ ``` -Tiebreakers and Configuration Change Announcements +## Tiebreakers and Configuration Change Announcements --- Normally StackExchange.Redis will resolve primary/replica nodes automatically. However, if you are not using a management tool such as redis-sentinel or redis cluster, there is a chance that occasionally you will get multiple primary nodes (for example, while resetting a node for maintenance it may reappear on the network as a primary). To help with this, StackExchange.Redis can use the notion of a *tie-breaker* - which is only used when multiple primaries are detected (not including redis cluster, where multiple primaries are *expected*). For compatibility with BookSleeve, this defaults to the key named `"__Booksleeve_TieBreak"` (always in database 0). This is used as a crude voting mechanism to help determine the *preferred* primary, so that work is routed correctly. @@ -240,8 +245,9 @@ Both options can be customized or disabled (set to `""`), via the `.Configuratio These settings are also used by the `IServer.MakeMaster()` method, which can set the tie-breaker in the database and broadcast the configuration change message. The configuration message can also be used separately to primary/replica changes simply to request all nodes to refresh their configurations, via the `ConnectionMultiplexer.PublishReconfigure` method. -ReconnectRetryPolicy +## ReconnectRetryPolicy --- + StackExchange.Redis automatically tries to reconnect in the background when the connection is lost for any reason. It keeps retrying until the connection has been restored. It would use ReconnectRetryPolicy to decide how long it should wait between the retries. ReconnectRetryPolicy can be exponential (default), linear or a custom retry policy. @@ -266,3 +272,16 @@ config.ReconnectRetryPolicy = new LinearRetry(5000); //5 5000 //6 5000 ``` + +## Redis protocol + +Without specific configuration, StackExchange.Redis will use the RESP2 protocol; this means that pub/sub requires a separate connection to the server. RESP3 is a newer protocol +(usually, but not always, available on v6 servers and above) which allows (among other changes) pub/sub messages to be communicated on the *same* connection - which can be very +desirable in servers with a large number of clients. The protocol handshake needs to happen very early in the connection, so *by default* the library does not attempt a RESP3 connection +unless it has reason to expect it to work. + +The library determines whether to use RESP3 by: +- The `HELLO` command has been disabled: RESP2 is used +- A protocol *other than* `resp3` or `3` is specified: RESP2 is used +- A protocol of `resp3` or `3` is specified: RESP3 is attempted (with fallback if it fails) +- In all other scenarios: RESP2 is used diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index ee25d40a0..b238fe10c 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,7 @@ Current package versions: ## Unreleased +- Adds: RESP3 support ([#2396 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2396)) - see https://stackexchange.github.io/StackExchange.Redis/Resp3 - Fix [#2507](https://github.com/StackExchange/StackExchange.Redis/issues/2507): Pub/sub with multi-item payloads should be usable ([#2508 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2508)) - Add: connection-id tracking (internal only, no public API) ([#2508 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2508)) - Add: `ConfigurationOptions.LoggerFactory` for logging to an `ILoggerFactory` (e.g. `ILogger`) all connection and error events ([#2051 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2051)) diff --git a/docs/Resp3.md b/docs/Resp3.md new file mode 100644 index 000000000..126b460f4 --- /dev/null +++ b/docs/Resp3.md @@ -0,0 +1,45 @@ +# RESP3 and StackExchange.Redis + +RESP2 and RESP3 are evolutions of the Redis protocol, with RESP3 existing from Redis server version 6 onwards (v7.2+ for Redis Enterprise). The main differences are: + +1. RESP3 can carry out-of-band / "push" messages on a single connection, where-as RESP2 requires a separate connection for these messages +2. RESP3 can (when appropriate) convey additional semantic meaning about returned payloads inside the same result structure +3. Some commands (see [this topic](https://github.com/redis/redis-doc/issues/2511)) return different result structures in RESP3 mode; for example a flat interleaved array might become a jagged array + +For most people, #1 is the main reason to consider RESP3, as in high-usage servers - this can halve the number of connections required. +This is particularly useful in hosted environments where the number of inbound connections to the server is capped as part of a service plan. +Alternatively, where users are currently choosing to disable the out-of-band connection to achieve this, they may now be able to re-enable this +(for example, to receive server maintenance notifications) *without* incurring any additional connection overhead. + +Because of the significance of #3 (and to avoid breaking your code), the library does not currently default to RESP3 mode. This must be enabled explicitly +via `ConfigurationOptions.Protocol` or by adding `,protocol=resp3` (or `,protocol=3`) to the configuration string. + +--- + +#3 is a critical one - the library *should* already handle all documented commands that have revised results in RESP3, but if you're using +`Execute[Async]` to issue ad-hoc commands, you may need to update your processing code to compensate for this, ideally using detection to handle +*either* format so that the same code works in both REP2 and RESP3. Since the impacted commands are handled internally by the library, in reality +this should not usually present a difficulty. + +The minor (#2) and major (#3) differences to results are only visible to your code when using: + +- Lua scripts invoked via the `ScriptEvaluate[Async](...)` or related APIs, that either: + - Uses the `redis.setresp(3)` API and returns a value from `redis.[p]call(...)` + - Returns a value that satisfies the [LUA to RESP3 type conversion rules](https://redis.io/docs/manual/programmability/lua-api/#lua-to-resp3-type-conversion) +- Ad-hoc commands (in particular: *modules*) that are invoked via the `Execute[Async](string command, ...)` API + +...both which return `RedisResult`. **If you are not using these APIs, you should not need to do anything additional.** + +Historically, you could use the `RedisResult.Type` property to query the type of data returned (integer, string, etc). In particular: + +- Two new properties are added: `RedisResult.Resp2Type` and `RedisResult.Resp3Type` + - The `Resp3Type` property exposes the new semantic data (when using RESP3) - for example, it can indicate that a value is a double-precision number, a boolean, a map, etc (types that did not historically exist) + - The `Resp2Type` property exposes the same value that *would* have been returned if this data had been returned over RESP2 + - The `Type` property is now marked obsolete, but functions identically to `Resp2Type`, so that pre-existing code (for example, that has a `switch` on the type) is not impacted by RESP3 +- The `ResultType.MultiBulk` is superseded by `ResultType.Array` (this is a nomenclature change only; they are the same value and function identically) + +Possible changes required due to RESP3: + +1. To prevent build warnings, replace usage of `ResultType.MultiBulk` with `ResultType.Array`, and usage of `RedisResult.Type` with `RedisResult.Resp2Type` +2. If you wish to exploit the additional semantic data when enabling RESP3, use `RedisResult.Resp3Type` where appropriate +3. If you are enabling RESP3, you must verify whether the commands you are using can give different result shapes on RESP3 connections \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index d1711e346..cd1c84d7e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,6 +39,7 @@ Documentation - [Transactions](Transactions) - how atomic transactions work in redis - [Events](Events) - the events available for logging / information purposes - [Pub/Sub Message Order](PubSubOrder) - advice on sequential and concurrent processing +- [Using RESP3](Resp3) - information on using RESP3 - [ServerMaintenanceEvent](ServerMaintenanceEvent) - how to listen and prepare for hosted server maintenance (e.g. Azure Cache for Redis) - [Streams](Streams) - how to use the Stream data type - [Where are `KEYS` / `SCAN` / `FLUSH*`?](KeysScan) - how to use server-based commands diff --git a/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs b/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs index e07d89342..2303c6e49 100644 --- a/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs +++ b/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs @@ -13,7 +13,7 @@ private sealed class Processor : ArrayResultProcessor { protected override bool TryParse(in RawResult raw, out LatencyHistoryEntry parsed) { - if (raw.Type == ResultType.MultiBulk) + if (raw.Resp2TypeArray == ResultType.Array) { var items = raw.GetItems(); if (items.Length >= 2 diff --git a/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs b/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs index 739d1c71d..67e416dc8 100644 --- a/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs +++ b/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs @@ -13,7 +13,7 @@ private sealed class Processor : ArrayResultProcessor { protected override bool TryParse(in RawResult raw, out LatencyLatestEntry parsed) { - if (raw.Type == ResultType.MultiBulk) + if (raw.Resp2TypeArray == ResultType.Array) { var items = raw.GetItems(); if (items.Length >= 4 diff --git a/src/StackExchange.Redis/ChannelMessageQueue.cs b/src/StackExchange.Redis/ChannelMessageQueue.cs index 3cc6c3d5a..3bf7635f3 100644 --- a/src/StackExchange.Redis/ChannelMessageQueue.cs +++ b/src/StackExchange.Redis/ChannelMessageQueue.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; -using System.Reflection; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; +#if NETCOREAPP3_1 +using System.Reflection; +#endif namespace StackExchange.Redis { @@ -125,6 +127,7 @@ public ValueTask ReadAsync(CancellationToken cancellationToken = /// The (approximate) count of items in the Channel. public bool TryGetCount(out int count) { + // This is specific to netcoreapp3.1, because full framework was out of band and the new prop is present #if NETCOREAPP3_1 // get this using the reflection try diff --git a/src/StackExchange.Redis/ClientInfo.cs b/src/StackExchange.Redis/ClientInfo.cs index 4fa0aa378..215403fe8 100644 --- a/src/StackExchange.Redis/ClientInfo.cs +++ b/src/StackExchange.Redis/ClientInfo.cs @@ -181,18 +181,23 @@ public ClientType ClientType } /// - /// Client RESP protocol version. Added in Redis 7.0 + /// Client RESP protocol version. Added in Redis 7.0. /// public string? ProtocolVersion { get; private set; } /// - /// Client library name. Added in Redis 7.2 + /// Client RESP protocol version. Added in Redis 7.0. + /// + public RedisProtocol? Protocol => ConfigurationOptions.TryParseRedisProtocol(ProtocolVersion, out var value) ? value : null; + + /// + /// Client library name. Added in Redis 7.2. /// /// public string? LibraryName { get; private set; } /// - /// Client library version. Added in Redis 7.2 + /// Client library version. Added in Redis 7.2. /// /// public string? LibraryVersion { get; private set; } @@ -280,7 +285,7 @@ private class ClientInfoProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch(result.Type) + switch(result.Resp2TypeBulkString) { case ResultType.BulkString: var raw = result.GetString(); diff --git a/src/StackExchange.Redis/CommandTrace.cs b/src/StackExchange.Redis/CommandTrace.cs index fcb9aefdf..061f252b9 100644 --- a/src/StackExchange.Redis/CommandTrace.cs +++ b/src/StackExchange.Redis/CommandTrace.cs @@ -73,9 +73,9 @@ private class CommandTraceProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch(result.Type) + switch(result.Resp2TypeArray) { - case ResultType.MultiBulk: + case ResultType.Array: var parts = result.GetItems(); CommandTrace[] arr = new CommandTrace[parts.Length]; int i = 0; diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs index 85d78de8c..0dcccf59c 100644 --- a/src/StackExchange.Redis/Condition.cs +++ b/src/StackExchange.Redis/Condition.cs @@ -563,7 +563,7 @@ internal override bool TryValidate(in RawResult result, out bool value) return true; default: - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.BulkString: case ResultType.SimpleString: @@ -619,7 +619,7 @@ internal sealed override IEnumerable CreateMessages(int db, IResultBox? internal override bool TryValidate(in RawResult result, out bool value) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.BulkString: case ResultType.SimpleString: @@ -692,7 +692,7 @@ internal sealed override IEnumerable CreateMessages(int db, IResultBox? internal override bool TryValidate(in RawResult result, out bool value) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.BulkString: case ResultType.SimpleString: @@ -749,7 +749,7 @@ internal sealed override IEnumerable CreateMessages(int db, IResultBox? internal override bool TryValidate(in RawResult result, out bool value) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.BulkString: case ResultType.SimpleString: @@ -806,7 +806,7 @@ internal sealed override IEnumerable CreateMessages(int db, IResultBox? internal override bool TryValidate(in RawResult result, out bool value) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.Integer: var parsedValue = result.AsRedisValue(); diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 12c372715..a85232172 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -47,8 +47,11 @@ internal static bool ParseBoolean(string key, string value) internal static Version ParseVersion(string key, string value) { - if (!System.Version.TryParse(value, out Version? tmp)) throw new ArgumentOutOfRangeException(key, $"Keyword '{key}' requires a version value; the value '{value}' is not recognised."); - return tmp; + if (Format.TryParseVersion(value, out Version? tmp)) + { + return tmp; + } + throw new ArgumentOutOfRangeException(key, $"Keyword '{key}' requires a version value; the value '{value}' is not recognised."); } internal static Proxy ParseProxy(string key, string value) @@ -67,6 +70,12 @@ internal static SslProtocols ParseSslProtocols(string key, string? value) return tmp; } + internal static RedisProtocol ParseRedisProtocol(string key, string value) + { + if (TryParseRedisProtocol(value, out var protocol)) return protocol; + throw new ArgumentOutOfRangeException(key, $"Keyword '{key}' requires a RedisProtocol value or a known protocol version number; the value '{value}' is not recognised."); + } + internal static void Unknown(string key) => throw new ArgumentException($"Keyword '{key}' is not supported.", key); @@ -99,7 +108,8 @@ internal const string WriteBuffer = "writeBuffer", CheckCertificateRevocation = "checkCertificateRevocation", Tunnel = "tunnel", - SetClientLibrary = "setlib"; + SetClientLibrary = "setlib", + Protocol = "protocol"; private static readonly Dictionary normalizedOptions = new[] { @@ -128,7 +138,8 @@ internal const string TieBreaker, Version, WriteBuffer, - CheckCertificateRevocation + CheckCertificateRevocation, + Protocol, }.ToDictionary(x => x, StringComparer.OrdinalIgnoreCase); public static string TryNormalize(string value) @@ -414,6 +425,7 @@ public TimeSpan HeartbeatInterval /// If , will be used. /// [Obsolete($"This setting no longer has any effect, please use {nameof(SocketManager.SocketManagerOptions)}.{nameof(SocketManager.SocketManagerOptions.UseHighPrioritySocketThreads)} instead - this setting will be removed in 3.0.")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public bool HighPrioritySocketThreads { get => false; @@ -483,6 +495,7 @@ public string? Password /// Specifies whether asynchronous operations should be invoked in a way that guarantees their original delivery order. /// [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue) + " - this will be removed in 3.0.", false)] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public bool PreserveAsyncOrder { get => false; @@ -530,6 +543,7 @@ public bool ResolveDns /// Specifies the time in milliseconds that the system should allow for responses before concluding that the socket is unhealthy. /// [Obsolete("This setting no longer has any effect, and should not be used - will be removed in 3.0.")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public int ResponseTimeout { get => 0; @@ -604,6 +618,7 @@ public string TieBreaker /// The size of the output buffer to use. /// [Obsolete("This setting no longer has any effect, and should not be used - will be removed in 3.0.")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public int WriteBuffer { get => 0; @@ -695,6 +710,7 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow Tunnel = Tunnel, setClientLibrary = setClientLibrary, LibraryName = LibraryName, + Protocol = Protocol, }; /// @@ -775,12 +791,20 @@ public string ToString(bool includePassword) Append(sb, OptionKeys.ResponseTimeout, responseTimeout); Append(sb, OptionKeys.DefaultDatabase, DefaultDatabase); Append(sb, OptionKeys.SetClientLibrary, setClientLibrary); + Append(sb, OptionKeys.Protocol, FormatProtocol(Protocol)); if (Tunnel is { IsInbuilt: true } tunnel) { Append(sb, OptionKeys.Tunnel, tunnel.ToString()); } commandMap?.AppendDeltas(sb); return sb.ToString(); + + static string? FormatProtocol(RedisProtocol? protocol) => protocol switch { + null => null, + RedisProtocol.Resp2 => "resp2", + RedisProtocol.Resp3 => "resp3", + _ => protocol.GetValueOrDefault().ToString(), + }; } private static void Append(StringBuilder sb, object value) @@ -956,6 +980,9 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) Tunnel = Tunnel.HttpProxy(ep); } break; + case OptionKeys.Protocol: + Protocol = OptionKeys.ParseRedisProtocol(key, value); + break; // Deprecated options we ignore... case OptionKeys.HighPrioritySocketThreads: case OptionKeys.PreserveAsyncOrder: @@ -998,5 +1025,58 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) /// Allows custom transport implementations, such as http-tunneling via a proxy. /// public Tunnel? Tunnel { get; set; } + + /// + /// Specify the redis protocol type + /// + public RedisProtocol? Protocol { get; set; } + + internal bool TryResp3() + { + // note: deliberately leaving the IsAvailable duplicated to use short-circuit + + //if (Protocol is null) + //{ + // // if not specified, lean on the server version and whether HELLO is available + // return new RedisFeatures(DefaultVersion).Resp3 && CommandMap.IsAvailable(RedisCommand.HELLO); + //} + //else + // ^^^ left for context; originally our intention was to auto-enable RESP3 by default *if* the server version + // is >= 6; however, it turns out (see extensive conversation here https://github.com/StackExchange/StackExchange.Redis/pull/2396) + // that tangential undocumented API breaks were made at the same time; this means that even if we fix every + // edge case in the library itself, the break is still visible to external callers via Execute[Async]; with an + // abundance of caution, we are therefore making RESP3 explicit opt-in only for now; we may revisit this in a major + { + return Protocol.GetValueOrDefault() >= RedisProtocol.Resp3 && CommandMap.IsAvailable(RedisCommand.HELLO); + } + } + + internal static bool TryParseRedisProtocol(string? value, out RedisProtocol protocol) + { + // accept raw integers too, but only trust them if we recognize them + // (note we need to do this before enums, because Enum.TryParse will + // accept integers as the raw value, which is not what we want here) + if (value is not null) + { + if (Format.TryParseInt32(value, out int i32)) + { + switch (i32) + { + case 2: + protocol = RedisProtocol.Resp2; + return true; + case 3: + protocol = RedisProtocol.Resp3; + return true; + } + } + else + { + if (Enum.TryParse(value, true, out protocol)) return true; + } + } + protocol = default; + return false; + } } } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Compat.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Compat.cs index f105fe2ca..6786e87d2 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Compat.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Compat.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Threading.Tasks; namespace StackExchange.Redis; @@ -9,11 +10,13 @@ public partial class ConnectionMultiplexer /// No longer used. /// [Obsolete("No longer used, will be removed in 3.0.")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public static TaskFactory Factory { get => Task.Factory; set { } } /// /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order. /// [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue) + ", will be removed in 3.0", false)] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public bool PreserveAsyncOrder { get => false; set { } } } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index cc239ad3f..64267aa2f 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -48,6 +49,9 @@ public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplex internal EndPointCollection EndPoints { get; } internal ConfigurationOptions RawConfig { get; } internal ServerSelectionStrategy ServerSelectionStrategy { get; } + ServerSelectionStrategy IInternalConnectionMultiplexer.ServerSelectionStrategy => ServerSelectionStrategy; + ConnectionMultiplexer IInternalConnectionMultiplexer.UnderlyingMultiplexer => this; + internal Exception? LastException { get; set; } ConfigurationOptions IInternalConnectionMultiplexer.RawConfig => RawConfig; @@ -70,6 +74,7 @@ pulse is null /// Should exceptions include identifiable details? (key names, additional .Data annotations) /// [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public bool IncludeDetailInExceptions { get => RawConfig.IncludeDetailInExceptions; @@ -83,6 +88,7 @@ public bool IncludeDetailInExceptions /// CPU usage, etc - note that this can be problematic on some platforms. /// [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludePerformanceCountersInExceptions)} instead - this will be removed in 3.0.")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public bool IncludePerformanceCountersInExceptions { get => RawConfig.IncludePerformanceCountersInExceptions; @@ -135,7 +141,7 @@ private ConnectionMultiplexer(ConfigurationOptions configuration, ServerType? se Logger = configuration.LoggerFactory?.CreateLogger(); var map = CommandMap = configuration.GetCommandMap(serverType); - if (!string.IsNullOrWhiteSpace(configuration.Password)) + if (!string.IsNullOrWhiteSpace(configuration.Password) && !configuration.TryResp3()) // RESP3 doesn't need AUTH (can issue as part of HELLO) { map.AssertAvailable(RedisCommand.AUTH); } @@ -883,6 +889,8 @@ public ServerSnapshotFiltered(ServerEndPoint[] endpoints, int count, Func GetServerEndPoint(endpoint); + [return: NotNullIfNotNull(nameof(endpoint))] internal ServerEndPoint? GetServerEndPoint(EndPoint? endpoint, ILogger? log = null, bool activate = true) { @@ -908,7 +916,7 @@ public ServerSnapshotFiltered(ServerEndPoint[] endpoints, int count, Func [Obsolete("From 2.0, this flag is not used, this will be removed in 3.0.", false)] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] HighPriority = 1, /// /// The caller is not interested in the result; the caller will immediately receive a default-value diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 40cb5c708..884114139 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -64,6 +64,7 @@ internal enum RedisCommand GETSET, HDEL, + HELLO, HEXISTS, HGET, HGETALL, @@ -376,6 +377,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.GET: case RedisCommand.GETBIT: case RedisCommand.GETRANGE: + case RedisCommand.HELLO: case RedisCommand.HEXISTS: case RedisCommand.HGET: case RedisCommand.HGETALL: diff --git a/src/StackExchange.Redis/Enums/ResultType.cs b/src/StackExchange.Redis/Enums/ResultType.cs index 3ea559d0a..ca09f64b0 100644 --- a/src/StackExchange.Redis/Enums/ResultType.cs +++ b/src/StackExchange.Redis/Enums/ResultType.cs @@ -1,4 +1,7 @@ -namespace StackExchange.Redis +using System; +using System.ComponentModel; + +namespace StackExchange.Redis { /// /// The underlying result type as defined by Redis. @@ -9,6 +12,9 @@ public enum ResultType : byte /// No value was received. /// None = 0, + + // RESP 2 + /// /// Basic strings typically represent status results such as "OK". /// @@ -25,9 +31,75 @@ public enum ResultType : byte /// Bulk strings represent typical user content values. /// BulkString = 4, + + /// + /// Array of results (former Multi-bulk). + /// + Array = 5, + /// /// Multi-bulk replies represent complex results such as arrays. /// + [Obsolete("Please use " + nameof(Array))] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] MultiBulk = 5, + + // RESP3: https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md + + // note: we will arrange the values such as the last 3 bits are the RESP2 equivalent, + // and then we count up from there + + /// + /// A single null value replacing RESP v2 blob and multi-bulk nulls. + /// + Null = (1 << 3) | None, + + /// + /// True or false. + /// + Boolean = (1 << 3) | Integer, + + /// + /// A floating point number. + /// + Double = (1 << 3) | SimpleString, + + /// + /// A large number non representable by the type + /// + BigInteger = (2 << 3) | SimpleString, + + /// + /// Binary safe error code and message. + /// + BlobError = (1 << 3) | Error, + + /// + /// A binary safe string that should be displayed to humans without any escaping or filtering. For instance the output of LATENCY DOCTOR in Redis. + /// + VerbatimString = (1 << 3) | BulkString, + + /// + /// An unordered collection of key-value pairs. Keys and values can be any other RESP3 type. + /// + Map = (1 << 3) | Array, + + /// + /// An unordered collection of N other types. + /// + Set = (2 << 3) | Array, + + /// + /// Like the type, but the client should keep reading the reply ignoring the attribute type, and return it to the client as additional information. + /// + Attribute = (3 << 3) | Array, + + /// + /// Out of band data. The format is like the type, but the client should just check the first string element, + /// stating the type of the out of band data, a call a callback if there is one registered for this specific type of push information. + /// Push types are not related to replies, since they are information that the server may push at any time in the connection, + /// so the client should keep reading if it is reading the reply of a command. + /// + Push = (4 << 3) | Array, } } diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index c1e53a329..fd1953de6 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -241,12 +241,12 @@ internal static Exception Timeout(ConnectionMultiplexer multiplexer, string? bas if (message != null) { - sb.Append(", command=").Append(message.Command); // no key here, note + sb.Append(", command=").Append(message.CommandString); // no key here, note } } else { - sb.Append("Timeout performing ").Append(message.Command).Append(" (").Append(Format.ToString(multiplexer.TimeoutMilliseconds)).Append("ms)"); + sb.Append("Timeout performing ").Append(message.CommandString).Append(" (").Append(Format.ToString(multiplexer.TimeoutMilliseconds)).Append("ms)"); } // Add timeout data, if we have it @@ -318,8 +318,8 @@ private static void AddCommonDetail( if (message != null) { message.TryGetHeadMessages(out var now, out var next); - if (now != null) Add(data, sb, "Message-Current", "active", multiplexer.RawConfig.IncludeDetailInExceptions ? now.CommandAndKey : now.Command.ToString()); - if (next != null) Add(data, sb, "Message-Next", "next", multiplexer.RawConfig.IncludeDetailInExceptions ? next.CommandAndKey : next.Command.ToString()); + if (now != null) Add(data, sb, "Message-Current", "active", multiplexer.RawConfig.IncludeDetailInExceptions ? now.CommandAndKey : now.CommandString); + if (next != null) Add(data, sb, "Message-Next", "next", multiplexer.RawConfig.IncludeDetailInExceptions ? next.CommandAndKey : next.CommandString); } // Add server data, if we have it @@ -406,7 +406,7 @@ private static void AddExceptionDetail(Exception? exception, Message? message, S private static string GetLabel(bool includeDetail, RedisCommand command, Message? message) { - return message == null ? command.ToString() : (includeDetail ? message.CommandAndKey : message.Command.ToString()); + return message == null ? command.ToString() : (includeDetail ? message.CommandAndKey : message.CommandString); } internal static Exception UnableToConnect(ConnectionMultiplexer muxer, string? failureMessage = null) diff --git a/src/StackExchange.Redis/Format.cs b/src/StackExchange.Redis/Format.cs index 9c96ccebe..73c29a82e 100644 --- a/src/StackExchange.Redis/Format.cs +++ b/src/StackExchange.Redis/Format.cs @@ -5,6 +5,7 @@ using System.Net; using System.Text; using System.Diagnostics.CodeAnalysis; + #if UNIX_SOCKET using System.Net.Sockets; #endif @@ -139,26 +140,37 @@ internal static bool TryGetHostPort(EndPoint? endpoint, [NotNullWhen(true)] out internal static bool TryParseDouble(string? s, out double value) { - if (s.IsNullOrEmpty()) + if (s is null) { value = 0; return false; } - if (s.Length == 1 && s[0] >= '0' && s[0] <= '9') - { - value = (int)(s[0] - '0'); - return true; - } - // need to handle these - if (string.Equals("+inf", s, StringComparison.OrdinalIgnoreCase) || string.Equals("inf", s, StringComparison.OrdinalIgnoreCase)) + switch (s.Length) { - value = double.PositiveInfinity; - return true; - } - if (string.Equals("-inf", s, StringComparison.OrdinalIgnoreCase)) - { - value = double.NegativeInfinity; - return true; + case 0: + value = 0; + return false; + // single-digits + case 1 when s[0] >= '0' && s[0] <= '9': + value = s[0] - '0'; + return true; + // RESP3 spec demands inf/nan handling + case 3 when CaseInsensitiveASCIIEqual("inf", s): + value = double.PositiveInfinity; + return true; + case 3 when CaseInsensitiveASCIIEqual("nan", s): + value = double.NaN; + return true; + case 4 when CaseInsensitiveASCIIEqual("+inf", s): + value = double.PositiveInfinity; + return true; + case 4 when CaseInsensitiveASCIIEqual("-inf", s): + value = double.NegativeInfinity; + return true; + case 4 when CaseInsensitiveASCIIEqual("+nan", s): + case 4 when CaseInsensitiveASCIIEqual("-nan", s): + value = double.NaN; + return true; } return double.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out value); } @@ -200,30 +212,39 @@ internal static bool TryParseInt64(string s, out long value) => internal static bool TryParseDouble(ReadOnlySpan s, out double value) { - if (s.IsEmpty) + switch (s.Length) { - value = 0; - return false; - } - if (s.Length == 1 && s[0] >= '0' && s[0] <= '9') - { - value = (int)(s[0] - '0'); - return true; - } - // need to handle these - if (CaseInsensitiveASCIIEqual("+inf", s) || CaseInsensitiveASCIIEqual("inf", s)) - { - value = double.PositiveInfinity; - return true; - } - if (CaseInsensitiveASCIIEqual("-inf", s)) - { - value = double.NegativeInfinity; - return true; + case 0: + value = 0; + return false; + // single-digits + case 1 when s[0] >= '0' && s[0] <= '9': + value = s[0] - '0'; + return true; + // RESP3 spec demands inf/nan handling + case 3 when CaseInsensitiveASCIIEqual("inf", s): + value = double.PositiveInfinity; + return true; + case 3 when CaseInsensitiveASCIIEqual("nan", s): + value = double.NaN; + return true; + case 4 when CaseInsensitiveASCIIEqual("+inf", s): + value = double.PositiveInfinity; + return true; + case 4 when CaseInsensitiveASCIIEqual("-inf", s): + value = double.NegativeInfinity; + return true; + case 4 when CaseInsensitiveASCIIEqual("+nan", s): + case 4 when CaseInsensitiveASCIIEqual("-nan", s): + value = double.NaN; + return true; } return Utf8Parser.TryParse(s, out value, out int bytes) & bytes == s.Length; } + private static bool CaseInsensitiveASCIIEqual(string xLowerCase, string y) + => string.Equals(xLowerCase, y, StringComparison.OrdinalIgnoreCase); + private static bool CaseInsensitiveASCIIEqual(string xLowerCase, ReadOnlySpan y) { if (y.Length != xLowerCase.Length) return false; @@ -350,10 +371,14 @@ internal static string GetString(ReadOnlySequence buffer) internal static unsafe string GetString(ReadOnlySpan span) { if (span.IsEmpty) return ""; +#if NETCOREAPP3_1_OR_GREATER + return Encoding.UTF8.GetString(span); +#else fixed (byte* ptr = span) { return Encoding.UTF8.GetString(ptr, span.Length); } +#endif } [DoesNotReturn] @@ -427,5 +452,50 @@ internal static int FormatInt32(int value, Span destination) ThrowFormatFailed(); return len; } + + internal static bool TryParseVersion(ReadOnlySpan input, [NotNullWhen(true)] out Version? version) + { +#if NETCOREAPP3_1_OR_GREATER + if (Version.TryParse(input, out version)) return true; + // allow major-only (Version doesn't do this, because... reasons?) + if (TryParseInt32(input, out int i32)) + { + version = new(i32, 0); + return true; + } + version = null; + return false; +#else + if (input.IsEmpty) + { + version = null; + return false; + } + unsafe + { + fixed (char* ptr = input) + { + string s = new(ptr, 0, input.Length); + return TryParseVersion(s, out version); + } + } +#endif + } + + internal static bool TryParseVersion(string? input, [NotNullWhen(true)] out Version? version) + { + if (input is not null) + { + if (Version.TryParse(input, out version)) return true; + // allow major-only (Version doesn't do this, because... reasons?) + if (TryParseInt32(input, out int i32)) + { + version = new(i32, 0); + return true; + } + } + version = null; + return false; + } } } diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index 58973df68..0c1494641 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -1,9 +1,12 @@ using StackExchange.Redis.Maintenance; using StackExchange.Redis.Profiling; using System; +using System.Collections.Concurrent; +using System.ComponentModel; using System.IO; using System.Net; using System.Threading.Tasks; +using static StackExchange.Redis.ConnectionMultiplexer; namespace StackExchange.Redis { @@ -14,10 +17,18 @@ internal interface IInternalConnectionMultiplexer : IConnectionMultiplexer bool IgnoreConnect { get; set; } ReadOnlySpan GetServerSnapshot(); + ServerEndPoint GetServerEndPoint(EndPoint endpoint); ConfigurationOptions RawConfig { get; } long? GetConnectionId(EndPoint endPoint, ConnectionType type); + + ServerSelectionStrategy ServerSelectionStrategy { get; } + + int GetSubscriptionsCount(); + ConcurrentDictionary GetSubscriptions(); + + ConnectionMultiplexer UnderlyingMultiplexer { get; } } /// @@ -49,6 +60,7 @@ public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order. /// [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue), false)] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] bool PreserveAsyncOrder { get; set; } /// @@ -65,6 +77,7 @@ public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable /// Should exceptions include identifiable details? (key names, additional annotations). /// [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] bool IncludeDetailInExceptions { get; set; } /// diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 7319f7feb..9f8f0b075 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -33,6 +33,11 @@ public partial interface IServer : IRedis /// bool IsConnected { get; } + /// + /// The protocol being used to communicate with this server (if not connected/known, then the anticipated protocol from the configuration is returned, assuming success) + /// + RedisProtocol Protocol { get; } + /// /// Gets whether the connected server is a replica. /// @@ -357,6 +362,7 @@ public partial interface IServer : IRedis /// [Obsolete("Please use " + nameof(MakePrimaryAsync) + ", this will be removed in 3.0.")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] void MakeMaster(ReplicationChangeOptions options, TextWriter? log = null); /// @@ -468,6 +474,7 @@ public partial interface IServer : IRedis /// [Obsolete("Please use " + nameof(ReplicaOfAsync) + ", this will be removed in 3.0.")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] void ReplicaOf(EndPoint master, CommandFlags flags = CommandFlags.None); /// diff --git a/src/StackExchange.Redis/LoggingPipe.cs b/src/StackExchange.Redis/LoggingPipe.cs index ba2343d23..3c89110ae 100644 --- a/src/StackExchange.Redis/LoggingPipe.cs +++ b/src/StackExchange.Redis/LoggingPipe.cs @@ -1,10 +1,4 @@ -using System; -using System.Buffers; -using System.IO; -using System.IO.Pipelines; -using System.Runtime.InteropServices; - -namespace StackExchange.Redis +namespace StackExchange.Redis { #if LOGOUTPUT sealed class LoggingPipe : IDuplexPipe diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 65df7d0e7..9acf41fb1 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -1,12 +1,12 @@ -using System; +using Microsoft.Extensions.Logging; +using StackExchange.Redis.Profiling; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading; -using Microsoft.Extensions.Logging; -using StackExchange.Redis.Profiling; namespace StackExchange.Redis { @@ -479,6 +479,7 @@ internal static bool RequiresDatabase(RedisCommand command) case RedisCommand.DISCARD: case RedisCommand.ECHO: case RedisCommand.FLUSHALL: + case RedisCommand.HELLO: case RedisCommand.INFO: case RedisCommand.LASTSAVE: case RedisCommand.LATENCY: @@ -637,6 +638,9 @@ internal void SetWriteTime() /// Gets if this command should be sent over the subscription bridge. /// internal bool IsForSubscriptionBridge => (Flags & DemandSubscriptionConnection) != 0; + + public virtual string CommandString => Command.ToString(); + /// /// Sends this command to the subscription connection rather than the interactive. /// @@ -706,6 +710,53 @@ internal void WriteTo(PhysicalConnection physical) } } + internal static Message CreateHello(int protocolVersion, string? username, string? password, string? clientName, CommandFlags flags) + => new HelloMessage(protocolVersion, username, password, clientName, flags); + + internal sealed class HelloMessage : Message + { + private readonly string? _username, _password, _clientName; + private readonly int _protocolVersion; + + internal HelloMessage(int protocolVersion, string? username, string? password, string? clientName, CommandFlags flags) + : base(-1, flags, RedisCommand.HELLO) + { + _protocolVersion = protocolVersion; + _username = username; + _password = password; + _clientName = clientName; + } + + public override string CommandAndKey => Command + " " + _protocolVersion; + + public override int ArgCount + { + get + { + int count = 1; // HELLO protover + if (!string.IsNullOrWhiteSpace(_password)) count += 3; // [AUTH username password] + if (!string.IsNullOrWhiteSpace(_clientName)) count += 2; // [SETNAME client] + return count; + } + } + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, ArgCount); + physical.WriteBulkString(_protocolVersion); + if (!string.IsNullOrWhiteSpace(_password)) + { + physical.WriteBulkString(RedisLiterals.AUTH); + physical.WriteBulkString(string.IsNullOrWhiteSpace(_username) ? RedisLiterals.@default : _username); + physical.WriteBulkString(_password); + } + if (!string.IsNullOrWhiteSpace(_clientName)) + { + physical.WriteBulkString(RedisLiterals.SETNAME); + physical.WriteBulkString(_clientName); + } + } + } + internal abstract class CommandChannelBase : Message { protected readonly RedisChannel Channel; diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 7041cf0af..3a4494821 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -69,6 +69,7 @@ internal sealed class PhysicalBridge : IDisposable #endif internal string? PhysicalName => physical?.ToString(); + public DateTime? ConnectedAt { get; private set; } public PhysicalBridge(ServerEndPoint serverEndPoint, ConnectionType type, int timeoutMilliseconds) @@ -114,6 +115,11 @@ public enum State : byte public RedisCommand LastCommand { get; private set; } + /// + /// If we have a connection, report the protocol being used + /// + public RedisProtocol? Protocol => physical?.Protocol; + public void Dispose() { isDisposed = true; @@ -1481,7 +1487,7 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne // If we are executing AUTH, it means we are still unauthenticated // Setting READONLY before AUTH always fails but we think it succeeded since // we run it as Fire and Forget. - if (cmd != RedisCommand.AUTH) + if (cmd != RedisCommand.AUTH && cmd != RedisCommand.HELLO) { var readmode = connection.GetReadModeCommand(isPrimaryOnly); if (readmode != null) diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index eb0787606..22c9d2894 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -259,6 +259,10 @@ private enum ReadMode : byte public bool TransactionActive { get; internal set; } + private RedisProtocol _protocol; // note starts at **zero**, not RESP2 + public RedisProtocol? Protocol => _protocol == 0 ? null : _protocol; + internal void SetProtocol(RedisProtocol value) => _protocol = value; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] internal void Shutdown() { @@ -1508,7 +1512,7 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock var configOptions = config.SslClientAuthenticationOptions?.Invoke(host); if (configOptions is not null) { - await ssl.AuthenticateAsClientAsync(configOptions); + await ssl.AuthenticateAsClientAsync(configOptions).ForAwait(); } else { @@ -1564,7 +1568,7 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock private void MatchResult(in RawResult result) { // check to see if it could be an out-of-band pubsub message - if (connectionType == ConnectionType.Subscription && result.Type == ResultType.MultiBulk) + if ((connectionType == ConnectionType.Subscription && result.Resp2TypeArray == ResultType.Array) || result.Resp3Type == ResultType.Push) { var muxer = BridgeCouldBeNull?.Multiplexer; if (muxer == null) return; @@ -1668,14 +1672,14 @@ static bool TryGetPubSubPayload(in RawResult value, out RedisValue parsed, bool parsed = RedisValue.Null; return true; } - switch (value.Type) + switch (value.Resp2TypeBulkString) { case ResultType.Integer: case ResultType.SimpleString: case ResultType.BulkString: parsed = value.AsRedisValue(); return true; - case ResultType.MultiBulk when allowArraySingleton && value.ItemsCount == 1: + case ResultType.Array when allowArraySingleton && value.ItemsCount == 1: return TryGetPubSubPayload(in value[0], out parsed, allowArraySingleton: false); } parsed = default; @@ -1684,7 +1688,7 @@ static bool TryGetPubSubPayload(in RawResult value, out RedisValue parsed, bool static bool TryGetMultiPubSubPayload(in RawResult value, out Sequence parsed) { - if (value.Type == ResultType.MultiBulk && value.ItemsCount != 0) + if (value.Resp2TypeArray == ResultType.Array && value.ItemsCount != 0) { parsed = value.GetItems(); return true; @@ -1821,7 +1825,7 @@ private int ProcessBuffer(ref ReadOnlySequence buffer) { _readStatus = ReadStatus.TryParseResult; var reader = new BufferReader(buffer); - var result = TryParseResult(_arena, in buffer, ref reader, IncludeDetailInExceptions, BridgeCouldBeNull?.ServerEndPoint); + var result = TryParseResult(_protocol >= RedisProtocol.Resp3, _arena, in buffer, ref reader, IncludeDetailInExceptions, this); try { if (result.HasValue) @@ -1876,34 +1880,39 @@ private int ProcessBuffer(ref ReadOnlySequence buffer) // } //} - private static RawResult ReadArray(Arena arena, in ReadOnlySequence buffer, ref BufferReader reader, bool includeDetailInExceptions, ServerEndPoint? server) + private static RawResult.ResultFlags AsNull(RawResult.ResultFlags flags) => flags & ~RawResult.ResultFlags.NonNull; + + private static RawResult ReadArray(ResultType resultType, RawResult.ResultFlags flags, Arena arena, in ReadOnlySequence buffer, ref BufferReader reader, bool includeDetailInExceptions, ServerEndPoint? server) { - var itemCount = ReadLineTerminatedString(ResultType.Integer, ref reader); + var itemCount = ReadLineTerminatedString(ResultType.Integer, flags, ref reader); if (itemCount.HasValue) { - if (!itemCount.TryGetInt64(out long i64)) throw ExceptionFactory.ConnectionFailure(includeDetailInExceptions, ConnectionFailureType.ProtocolFailure, "Invalid array length", server); + if (!itemCount.TryGetInt64(out long i64)) throw ExceptionFactory.ConnectionFailure(includeDetailInExceptions, ConnectionFailureType.ProtocolFailure, + itemCount.Is('?') ? "Streamed aggregate types not yet implemented" : "Invalid array length", server); int itemCountActual = checked((int)i64); if (itemCountActual < 0) { //for null response by command like EXEC, RESP array: *-1\r\n - return RawResult.NullMultiBulk; + return new RawResult(resultType, items: default, AsNull(flags)); } else if (itemCountActual == 0) { //for zero array response by command like SCAN, Resp array: *0\r\n - return RawResult.EmptyMultiBulk; + return new RawResult(resultType, items: default, flags); } + if (resultType == ResultType.Map) itemCountActual <<= 1; // if it says "3", it means 3 pairs, i.e. 6 values + var oversized = arena.Allocate(itemCountActual); - var result = new RawResult(oversized, false); + var result = new RawResult(resultType, oversized, flags); if (oversized.IsSingleSegment) { var span = oversized.FirstSpan; - for(int i = 0; i < span.Length; i++) + for (int i = 0; i < span.Length; i++) { - if (!(span[i] = TryParseResult(arena, in buffer, ref reader, includeDetailInExceptions, server)).HasValue) + if (!(span[i] = TryParseResult(flags, arena, in buffer, ref reader, includeDetailInExceptions, server)).HasValue) { return RawResult.Nil; } @@ -1911,11 +1920,11 @@ private static RawResult ReadArray(Arena arena, in ReadOnlySequence arena, in ReadOnlySequence.Empty, true); + return new RawResult(type, ReadOnlySequence.Empty, AsNull(flags)); } if (reader.TryConsumeAsBuffer(bodySize, out var payload)) @@ -1946,7 +1959,7 @@ private static RawResult ReadBulkString(ref BufferReader reader, bool includeDet case ConsumeResult.NeedMoreData: break; // see NilResult below case ConsumeResult.Success: - return new RawResult(ResultType.BulkString, payload, false); + return new RawResult(type, payload, flags); default: throw ExceptionFactory.ConnectionFailure(includeDetailInExceptions, ConnectionFailureType.ProtocolFailure, "Invalid bulk string terminator", server); } @@ -1955,7 +1968,7 @@ private static RawResult ReadBulkString(ref BufferReader reader, bool includeDet return RawResult.Nil; } - private static RawResult ReadLineTerminatedString(ResultType type, ref BufferReader reader) + private static RawResult ReadLineTerminatedString(ResultType type, RawResult.ResultFlags flags, ref BufferReader reader) { int crlfOffsetFromCurrent = BufferReader.FindNextCrLf(reader); if (crlfOffsetFromCurrent < 0) return RawResult.Nil; @@ -1963,7 +1976,7 @@ private static RawResult ReadLineTerminatedString(ResultType type, ref BufferRea var payload = reader.ConsumeAsBuffer(crlfOffsetFromCurrent); reader.Consume(2); - return new RawResult(type, payload, false); + return new RawResult(type, payload, flags); } internal enum ReadStatus @@ -1997,36 +2010,83 @@ internal enum ReadStatus internal void StartReading() => ReadFromPipe().RedisFireAndForget(); - internal static RawResult TryParseResult(Arena arena, in ReadOnlySequence buffer, ref BufferReader reader, + internal static RawResult TryParseResult(bool isResp3, Arena arena, in ReadOnlySequence buffer, ref BufferReader reader, + bool includeDetilInExceptions, PhysicalConnection? connection, bool allowInlineProtocol = false) + { + return TryParseResult(isResp3 ? (RawResult.ResultFlags.Resp3 | RawResult.ResultFlags.NonNull) : RawResult.ResultFlags.NonNull, + arena, buffer, ref reader, includeDetilInExceptions, connection?.BridgeCouldBeNull?.ServerEndPoint, allowInlineProtocol); + } + + private static RawResult TryParseResult(RawResult.ResultFlags flags, Arena arena, in ReadOnlySequence buffer, ref BufferReader reader, bool includeDetilInExceptions, ServerEndPoint? server, bool allowInlineProtocol = false) { - var prefix = reader.PeekByte(); - if (prefix < 0) return RawResult.Nil; // EOF - switch (prefix) - { - case '+': // simple string - reader.Consume(1); - return ReadLineTerminatedString(ResultType.SimpleString, ref reader); - case '-': // error - reader.Consume(1); - return ReadLineTerminatedString(ResultType.Error, ref reader); - case ':': // integer - reader.Consume(1); - return ReadLineTerminatedString(ResultType.Integer, ref reader); - case '$': // bulk string - reader.Consume(1); - return ReadBulkString(ref reader, includeDetilInExceptions, server); - case '*': // array - reader.Consume(1); - return ReadArray(arena, in buffer, ref reader, includeDetilInExceptions, server); - default: - // string s = Format.GetString(buffer); - if (allowInlineProtocol) return ParseInlineProtocol(arena, ReadLineTerminatedString(ResultType.SimpleString, ref reader)); - throw new InvalidOperationException("Unexpected response prefix: " + (char)prefix); - } + int prefix; + do // this loop is just to allow us to parse (skip) attributes without doing a stack-dive + { + prefix = reader.PeekByte(); + if (prefix < 0) return RawResult.Nil; // EOF + switch (prefix) + { + // RESP2 + case '+': // simple string + reader.Consume(1); + return ReadLineTerminatedString(ResultType.SimpleString, flags, ref reader); + case '-': // error + reader.Consume(1); + return ReadLineTerminatedString(ResultType.Error, flags, ref reader); + case ':': // integer + reader.Consume(1); + return ReadLineTerminatedString(ResultType.Integer, flags, ref reader); + case '$': // bulk string + reader.Consume(1); + return ReadBulkString(ResultType.BulkString, flags, ref reader, includeDetilInExceptions, server); + case '*': // array + reader.Consume(1); + return ReadArray(ResultType.Array, flags, arena, in buffer, ref reader, includeDetilInExceptions, server); + // RESP3 + case '_': // null + reader.Consume(1); + return ReadLineTerminatedString(ResultType.Null, flags, ref reader); + case ',': // double + reader.Consume(1); + return ReadLineTerminatedString(ResultType.Double, flags, ref reader); + case '#': // boolean + reader.Consume(1); + return ReadLineTerminatedString(ResultType.Boolean, flags, ref reader); + case '!': // blob error + reader.Consume(1); + return ReadBulkString(ResultType.BlobError, flags, ref reader, includeDetilInExceptions, server); + case '=': // verbatim string + reader.Consume(1); + return ReadBulkString(ResultType.VerbatimString, flags, ref reader, includeDetilInExceptions, server); + case '(': // big number + reader.Consume(1); + return ReadLineTerminatedString(ResultType.BigInteger, flags, ref reader); + case '%': // map + reader.Consume(1); + return ReadArray(ResultType.Map, flags, arena, in buffer, ref reader, includeDetilInExceptions, server); + case '~': // set + reader.Consume(1); + return ReadArray(ResultType.Set, flags, arena, in buffer, ref reader, includeDetilInExceptions, server); + case '|': // attribute + reader.Consume(1); + var arr = ReadArray(ResultType.Attribute, flags, arena, in buffer, ref reader, includeDetilInExceptions, server); + if (!arr.HasValue) return RawResult.Nil; // failed to parse attribute data + + // for now, we want to just skip attribute data; so + // drop whatever we parsed on the floor and keep looking + break; // exits the SWITCH, not the DO/WHILE + case '>': // push + reader.Consume(1); + return ReadArray(ResultType.Push, flags, arena, in buffer, ref reader, includeDetilInExceptions, server); + } + } while (prefix == '|'); + + if (allowInlineProtocol) return ParseInlineProtocol(flags, arena, ReadLineTerminatedString(ResultType.SimpleString, flags, ref reader)); + throw new InvalidOperationException("Unexpected response prefix: " + (char)prefix); } - private static RawResult ParseInlineProtocol(Arena arena, in RawResult line) + private static RawResult ParseInlineProtocol(RawResult.ResultFlags flags, Arena arena, in RawResult line) { if (!line.HasValue) return RawResult.Nil; // incomplete line @@ -2037,9 +2097,9 @@ private static RawResult ParseInlineProtocol(Arena arena, in RawResul var iter = block.GetEnumerator(); foreach (var token in line.GetInlineTokenizer()) { // this assigns *via a reference*, returned via the iterator; just... sweet - iter.GetNext() = new RawResult(line.Type, token, false); + iter.GetNext() = new RawResult(line.Resp3Type, token, flags); // spoof RESP2 from RESP1 } - return new RawResult(block, false); + return new RawResult(ResultType.Array, block, flags); // spoof RESP2 from RESP1 } internal bool HasPendingCallerFacingItems() diff --git a/src/StackExchange.Redis/Profiling/ProfiledCommand.cs b/src/StackExchange.Redis/Profiling/ProfiledCommand.cs index e4037902e..a549b2699 100644 --- a/src/StackExchange.Redis/Profiling/ProfiledCommand.cs +++ b/src/StackExchange.Redis/Profiling/ProfiledCommand.cs @@ -15,7 +15,7 @@ internal sealed class ProfiledCommand : IProfiledCommand public int Db => Message!.Db; - public string Command => Message is RedisDatabase.ExecuteMessage em ? em.Command.ToString() : Message!.Command.ToString(); + public string Command => Message!.CommandString; public CommandFlags Flags => Message!.Flags; diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index c49cf328a..459f66cf8 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1,6 +1,5 @@ #nullable enable abstract StackExchange.Redis.RedisResult.IsNull.get -> bool -abstract StackExchange.Redis.RedisResult.Type.get -> StackExchange.Redis.ResultType override StackExchange.Redis.ChannelMessage.Equals(object? obj) -> bool override StackExchange.Redis.ChannelMessage.GetHashCode() -> int override StackExchange.Redis.ChannelMessage.ToString() -> string! diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 5f282702b..eff457070 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,31 @@ - \ No newline at end of file +abstract StackExchange.Redis.RedisResult.ToString(out string? type) -> string? +override sealed StackExchange.Redis.RedisResult.ToString() -> string! +override StackExchange.Redis.Role.Master.Replica.ToString() -> string! +StackExchange.Redis.ClientInfo.Protocol.get -> StackExchange.Redis.RedisProtocol? +StackExchange.Redis.ConfigurationOptions.Protocol.get -> StackExchange.Redis.RedisProtocol? +StackExchange.Redis.ConfigurationOptions.Protocol.set -> void +StackExchange.Redis.IServer.Protocol.get -> StackExchange.Redis.RedisProtocol +StackExchange.Redis.RedisFeatures.ClientId.get -> bool +StackExchange.Redis.RedisFeatures.Equals(StackExchange.Redis.RedisFeatures other) -> bool +StackExchange.Redis.RedisFeatures.Resp3.get -> bool +StackExchange.Redis.RedisProtocol +StackExchange.Redis.RedisProtocol.Resp2 = 20000 -> StackExchange.Redis.RedisProtocol +StackExchange.Redis.RedisProtocol.Resp3 = 30000 -> StackExchange.Redis.RedisProtocol +StackExchange.Redis.RedisResult.Resp2Type.get -> StackExchange.Redis.ResultType +StackExchange.Redis.RedisResult.Resp3Type.get -> StackExchange.Redis.ResultType +StackExchange.Redis.RedisResult.Type.get -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Array = 5 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Attribute = 29 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.BigInteger = 17 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.BlobError = 10 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Boolean = 11 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Double = 9 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Map = 13 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Null = 8 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Push = 37 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Set = 21 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.VerbatimString = 12 -> StackExchange.Redis.ResultType +static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisResult![]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult! +static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult! +virtual StackExchange.Redis.RedisResult.Length.get -> int +virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult! \ No newline at end of file diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index ee4aeaa86..1581c29c9 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -1,8 +1,8 @@ -using System; +using Pipelines.Sockets.Unofficial.Arenas; +using System; using System.Buffers; using System.Runtime.CompilerServices; using System.Text; -using Pipelines.Sockets.Unofficial.Arenas; namespace StackExchange.Redis { @@ -15,16 +15,22 @@ internal readonly struct RawResult private readonly ReadOnlySequence _payload; internal ReadOnlySequence Payload => _payload; - internal static readonly RawResult NullMultiBulk = new RawResult(default(Sequence), isNull: true); - internal static readonly RawResult EmptyMultiBulk = new RawResult(default(Sequence), isNull: false); internal static readonly RawResult Nil = default; // Note: can't use Memory here - struct recursion breaks runtime private readonly Sequence _items; - private readonly ResultType _type; + private readonly ResultType _resultType; + private readonly ResultFlags _flags; - private const ResultType NonNullFlag = (ResultType)128; + [Flags] + internal enum ResultFlags + { + None = 0, + HasValue = 1 << 0, // simply indicates "not the default" (always set in .ctor) + NonNull = 1 << 1, // defines explicit null; isn't "IsNull" because we want default to be null + Resp3 = 1 << 2, // was the connection in RESP3 mode? + } - public RawResult(ResultType resultType, in ReadOnlySequence payload, bool isNull) + public RawResult(ResultType resultType, in ReadOnlySequence payload, ResultFlags flags) { switch (resultType) { @@ -32,40 +38,76 @@ public RawResult(ResultType resultType, in ReadOnlySequence payload, bool case ResultType.Error: case ResultType.Integer: case ResultType.BulkString: + case ResultType.Double: + case ResultType.Boolean: + case ResultType.BlobError: + case ResultType.VerbatimString: + case ResultType.BigInteger: + break; + case ResultType.Null: + flags &= ~ResultFlags.NonNull; break; default: - throw new ArgumentOutOfRangeException(nameof(resultType)); + ThrowInvalidType(resultType); + break; } - if (!isNull) resultType |= NonNullFlag; - _type = resultType; + _resultType = resultType; + _flags = flags | ResultFlags.HasValue; _payload = payload; _items = default; } - public RawResult(Sequence items, bool isNull) + public RawResult(ResultType resultType, Sequence items, ResultFlags flags) { - _type = isNull ? ResultType.MultiBulk : (ResultType.MultiBulk | NonNullFlag); + switch (resultType) + { + case ResultType.Array: + case ResultType.Map: + case ResultType.Set: + case ResultType.Attribute: + case ResultType.Push: + break; + case ResultType.Null: + flags &= ~ResultFlags.NonNull; + break; + default: + ThrowInvalidType(resultType); + break; + } + _resultType = resultType; + _flags = flags | ResultFlags.HasValue; _payload = default; _items = items.Untyped(); } - public bool IsError => Type == ResultType.Error; + private static void ThrowInvalidType(ResultType resultType) + => throw new ArgumentOutOfRangeException(nameof(resultType), $"Invalid result-type: {resultType}"); + + public bool IsError => _resultType.IsError(); + + public ResultType Resp3Type => _resultType; + + // if null, assume string + public ResultType Resp2TypeBulkString => _resultType == ResultType.Null ? ResultType.BulkString : _resultType.ToResp2(); + // if null, assume array + public ResultType Resp2TypeArray => _resultType == ResultType.Null ? ResultType.Array : _resultType.ToResp2(); + + internal bool IsNull => (_flags & ResultFlags.NonNull) == 0; - public ResultType Type => _type & ~NonNullFlag; + public bool HasValue => (_flags & ResultFlags.HasValue) != 0; - internal bool IsNull => (_type & NonNullFlag) == 0; - public bool HasValue => Type != ResultType.None; + public bool IsResp3 => (_flags & ResultFlags.Resp3) != 0; public override string ToString() { if (IsNull) return "(null)"; - return Type switch + return _resultType.ToResp2() switch { - ResultType.SimpleString or ResultType.Integer or ResultType.Error => $"{Type}: {GetString()}", - ResultType.BulkString => $"{Type}: {Payload.Length} bytes", - ResultType.MultiBulk => $"{Type}: {ItemsCount} items", - _ => $"(unknown: {Type})", + ResultType.SimpleString or ResultType.Integer or ResultType.Error => $"{Resp3Type}: {GetString()}", + ResultType.BulkString => $"{Resp3Type}: {Payload.Length} bytes", + ResultType.Array => $"{Resp3Type}: {ItemsCount} items", + _ => $"(unknown: {Resp3Type})", }; } @@ -121,7 +163,7 @@ public bool MoveNext() } internal RedisChannel AsRedisChannel(byte[]? channelPrefix, RedisChannel.PatternMode mode) { - switch (Type) + switch (Resp2TypeBulkString) { case ResultType.SimpleString: case ResultType.BulkString: @@ -136,20 +178,31 @@ internal RedisChannel AsRedisChannel(byte[]? channelPrefix, RedisChannel.Pattern } return default; default: - throw new InvalidCastException("Cannot convert to RedisChannel: " + Type); + throw new InvalidCastException("Cannot convert to RedisChannel: " + Resp3Type); } } - internal RedisKey AsRedisKey() => Type switch + internal RedisKey AsRedisKey() { - ResultType.SimpleString or ResultType.BulkString => (RedisKey)GetBlob(), - _ => throw new InvalidCastException("Cannot convert to RedisKey: " + Type), - }; + return Resp2TypeBulkString switch + { + ResultType.SimpleString or ResultType.BulkString => (RedisKey)GetBlob(), + _ => throw new InvalidCastException("Cannot convert to RedisKey: " + Resp3Type), + }; + } internal RedisValue AsRedisValue() { if (IsNull) return RedisValue.Null; - switch (Type) + if (Resp3Type == ResultType.Boolean && Payload.Length == 1) + { + switch (Payload.First.Span[0]) + { + case (byte)'t': return (RedisValue)true; + case (byte)'f': return (RedisValue)false; + } + } + switch (Resp2TypeBulkString) { case ResultType.Integer: long i64; @@ -159,13 +212,13 @@ internal RedisValue AsRedisValue() case ResultType.BulkString: return (RedisValue)GetBlob(); } - throw new InvalidCastException("Cannot convert to RedisValue: " + Type); + throw new InvalidCastException("Cannot convert to RedisValue: " + Resp3Type); } internal Lease? AsLease() { if (IsNull) return null; - switch (Type) + switch (Resp2TypeBulkString) { case ResultType.SimpleString: case ResultType.BulkString: @@ -174,7 +227,7 @@ internal RedisValue AsRedisValue() payload.CopyTo(lease.Span); return lease; } - throw new InvalidCastException("Cannot convert to Lease: " + Type); + throw new InvalidCastException("Cannot convert to Lease: " + Resp3Type); } internal bool IsEqual(in CommandBytes expected) @@ -244,6 +297,15 @@ internal bool StartsWith(byte[] expected) internal bool GetBoolean() { if (Payload.Length != 1) throw new InvalidCastException(); + if (Resp3Type == ResultType.Boolean) + { + return Payload.First.Span[0] switch + { + (byte)'t' => true, + (byte)'f' => false, + _ => throw new InvalidCastException(), + }; + } return Payload.First.Span[0] switch { (byte)'1' => true, @@ -325,14 +387,18 @@ private static GeoPosition AsGeoPosition(in Sequence coords) internal GeoPosition?[]? GetItemsAsGeoPositionArray() => this.ToArray((in RawResult item) => item.IsNull ? default : AsGeoPosition(item.GetItems())); - internal unsafe string? GetString() + internal unsafe string? GetString() => GetString(out _); + internal unsafe string? GetString(out ReadOnlySpan verbatimPrefix) { + verbatimPrefix = default; if (IsNull) return null; if (Payload.IsEmpty) return ""; + string s; if (Payload.IsSingleSegment) { - return Format.GetString(Payload.First.Span); + s = Format.GetString(Payload.First.Span); + return Resp3Type == ResultType.VerbatimString ? GetVerbatimString(s, out verbatimPrefix) : s; } #if NET6_0_OR_GREATER // use system-provided sequence decoder @@ -353,7 +419,7 @@ private static GeoPosition AsGeoPosition(in Sequence coords) decoder.Reset(); - string s = new string((char)0, charCount); + s = new string((char)0, charCount); fixed (char* sPtr = s) { char* cPtr = sPtr; @@ -371,15 +437,33 @@ private static GeoPosition AsGeoPosition(in Sequence coords) } } } - return s; + + return Resp3Type == ResultType.VerbatimString ? GetVerbatimString(s, out verbatimPrefix) : s; static void Throw() => throw new InvalidOperationException("Invalid result from GetChars"); #endif + static string? GetVerbatimString(string? value, out ReadOnlySpan type) + { + // the first three bytes provide information about the format of the following string, which + // can be txt for plain text, or mkd for markdown. The fourth byte is always `:` + // Then the real string follows. + if (value is not null + && value.Length >= 4 && value[3] == ':') + { + type = value.AsSpan().Slice(0, 3); + value = value.Substring(4); + } + else + { + type = default; + } + return value; + } } internal bool TryGetDouble(out double val) { - if (IsNull) + if (IsNull || Payload.IsEmpty) { val = 0; return false; @@ -389,6 +473,14 @@ internal bool TryGetDouble(out double val) val = i64; return true; } + + if (Payload.IsSingleSegment) return Format.TryParseDouble(Payload.First.Span, out val); + if (Payload.Length < 64) + { + Span span = stackalloc byte[(int)Payload.Length]; + Payload.CopyTo(span); + return Format.TryParseDouble(span, out val); + } return Format.TryParseDouble(GetString(), out val); } @@ -406,5 +498,11 @@ internal bool TryGetInt64(out long value) Payload.CopyTo(span); return Format.TryParseInt64(span, out value); } + + internal bool Is(char value) + { + var span = Payload.First.Span; + return span.Length == 1 && (char)span[0] == value && Payload.IsSingleSegment; + } } } diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 9df7ac742..85cf25576 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -2,7 +2,6 @@ using System.Buffers; using System.Collections.Generic; using System.Net; -using System.Text; using System.Threading.Tasks; using Pipelines.Sockets.Unofficial.Arenas; @@ -1549,12 +1548,12 @@ public async Task ScriptEvaluateAsync(string script, RedisKey[]? ke try { - return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ConfigureAwait(false); + return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ForAwait(); } catch (RedisServerException) when (msg.IsScriptUnavailable) { // could be a NOSCRIPT; for a sync call, we can re-issue that without problem - return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ConfigureAwait(false); + return await ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle).ForAwait(); } } @@ -4702,14 +4701,14 @@ private abstract class ScanResultProcessor : ResultProcessor.ScanResult(i64, oversized, count, true); @@ -4761,6 +4760,7 @@ protected override void WriteImpl(PhysicalConnection physical) } } + public override string CommandString => Command.ToString(); public override string CommandAndKey => Command.ToString(); public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) @@ -5048,7 +5048,7 @@ private class StringGetWithExpiryProcessor : ResultProcessor /// Provides basic information about the features available on a particular version of Redis. /// - public readonly struct RedisFeatures + public readonly struct RedisFeatures : IEquatable { internal static readonly Version v2_0_0 = new Version(2, 0, 0), v2_1_0 = new Version(2, 1, 0), @@ -56,167 +56,172 @@ public RedisFeatures(Version version) /// /// Are BITOP and BITCOUNT available? /// - public bool BitwiseOperations => Version >= v2_6_0; + public bool BitwiseOperations => Version.IsAtLeast(v2_6_0); /// /// Is CLIENT SETNAME available? /// - public bool ClientName => Version >= v2_6_9; + public bool ClientName => Version.IsAtLeast(v2_6_9); + + /// + /// Is CLIENT ID available? + /// + public bool ClientId => Version.IsAtLeast(v5_0_0); /// /// Does EXEC support EXECABORT if there are errors? /// - public bool ExecAbort => Version >= v2_6_5 && Version != v2_9_5; + public bool ExecAbort => Version.IsAtLeast(v2_6_5) && !Version.IsEqual(v2_9_5); /// /// Can EXPIRE be used to set expiration on a key that is already volatile (i.e. has an expiration)? /// - public bool ExpireOverwrite => Version >= v2_1_3; + public bool ExpireOverwrite => Version.IsAtLeast(v2_1_3); /// /// Is GETDEL available? /// - public bool GetDelete => Version >= v6_2_0; + public bool GetDelete => Version.IsAtLeast(v6_2_0); /// /// Is HSTRLEN available? /// - public bool HashStringLength => Version >= v3_2_0; + public bool HashStringLength => Version.IsAtLeast(v3_2_0); /// /// Does HDEL support variadic usage? /// - public bool HashVaradicDelete => Version >= v2_4_0; + public bool HashVaradicDelete => Version.IsAtLeast(v2_4_0); /// /// Are INCRBYFLOAT and HINCRBYFLOAT available? /// - public bool IncrementFloat => Version >= v2_6_0; + public bool IncrementFloat => Version.IsAtLeast(v2_6_0); /// /// Does INFO support sections? /// - public bool InfoSections => Version >= v2_8_0; + public bool InfoSections => Version.IsAtLeast(v2_8_0); /// /// Is LINSERT available? /// - public bool ListInsert => Version >= v2_1_1; + public bool ListInsert => Version.IsAtLeast(v2_1_1); /// /// Is MEMORY available? /// - public bool Memory => Version >= v4_0_0; + public bool Memory => Version.IsAtLeast(v4_0_0); /// /// Are PEXPIRE and PTTL available? /// - public bool MillisecondExpiry => Version >= v2_6_0; + public bool MillisecondExpiry => Version.IsAtLeast(v2_6_0); /// /// Is MODULE available? /// - public bool Module => Version >= v4_0_0; + public bool Module => Version.IsAtLeast(v4_0_0); /// /// Does SRANDMEMBER support the "count" option? /// - public bool MultipleRandom => Version >= v2_5_14; + public bool MultipleRandom => Version.IsAtLeast(v2_5_14); /// /// Is PERSIST available? /// - public bool Persist => Version >= v2_1_2; + public bool Persist => Version.IsAtLeast(v2_1_2); /// /// Are LPUSHX and RPUSHX available? /// - public bool PushIfNotExists => Version >= v2_1_1; + public bool PushIfNotExists => Version.IsAtLeast(v2_1_1); /// /// Does this support SORT_RO? /// - internal bool ReadOnlySort => Version >= v7_0_0_rc1; + internal bool ReadOnlySort => Version.IsAtLeast(v7_0_0_rc1); /// /// Is SCAN (cursor-based scanning) available? /// - public bool Scan => Version >= v2_8_0; + public bool Scan => Version.IsAtLeast(v2_8_0); /// /// Are EVAL, EVALSHA, and other script commands available? /// - public bool Scripting => Version >= v2_6_0; + public bool Scripting => Version.IsAtLeast(v2_6_0); /// /// Does SET support the GET option? /// - public bool SetAndGet => Version >= v6_2_0; + public bool SetAndGet => Version.IsAtLeast(v6_2_0); /// /// Does SET support the EX, PX, NX, and XX options? /// - public bool SetConditional => Version >= v2_6_12; + public bool SetConditional => Version.IsAtLeast(v2_6_12); /// /// Does SET have the KEEPTTL option? /// - public bool SetKeepTtl => Version >= v6_0_0; + public bool SetKeepTtl => Version.IsAtLeast(v6_0_0); /// /// Does SET allow the NX and GET options to be used together? /// - public bool SetNotExistsAndGet => Version >= v7_0_0_rc1; + public bool SetNotExistsAndGet => Version.IsAtLeast(v7_0_0_rc1); /// /// Does SADD support variadic usage? /// - public bool SetVaradicAddRemove => Version >= v2_4_0; + public bool SetVaradicAddRemove => Version.IsAtLeast(v2_4_0); /// /// Are ZPOPMIN and ZPOPMAX available? /// - public bool SortedSetPop => Version >= v5_0_0; + public bool SortedSetPop => Version.IsAtLeast(v5_0_0); /// /// Is ZRANGESTORE available? /// - public bool SortedSetRangeStore => Version >= v6_2_0; + public bool SortedSetRangeStore => Version.IsAtLeast(v6_2_0); /// /// Are Redis Streams available? /// - public bool Streams => Version >= v4_9_1; + public bool Streams => Version.IsAtLeast(v4_9_1); /// /// Is STRLEN available? /// - public bool StringLength => Version >= v2_1_2; + public bool StringLength => Version.IsAtLeast(v2_1_2); /// /// Is SETRANGE available? /// - public bool StringSetRange => Version >= v2_1_8; + public bool StringSetRange => Version.IsAtLeast(v2_1_8); /// /// Is SWAPDB available? /// - public bool SwapDB => Version >= v4_0_0; + public bool SwapDB => Version.IsAtLeast(v4_0_0); /// /// Is TIME available? /// - public bool Time => Version >= v2_6_0; + public bool Time => Version.IsAtLeast(v2_6_0); /// /// Is UNLINK available? /// - public bool Unlink => Version >= v4_0_0; + public bool Unlink => Version.IsAtLeast(v4_0_0); /// /// Are Lua changes to the calling database transparent to the calling client? /// - public bool ScriptingDatabaseSafe => Version >= v2_8_12; + public bool ScriptingDatabaseSafe => Version.IsAtLeast(v2_8_12); /// [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(HyperLogLogCountReplicaSafe) + " instead, this will be removed in 3.0.")] @@ -226,37 +231,43 @@ public RedisFeatures(Version version) /// /// Is PFCOUNT available on replicas? /// - public bool HyperLogLogCountReplicaSafe => Version >= v2_8_18; + public bool HyperLogLogCountReplicaSafe => Version.IsAtLeast(v2_8_18); /// /// Are geospatial commands available? /// - public bool Geo => Version >= v3_2_0; + public bool Geo => Version.IsAtLeast(v3_2_0); /// /// Can PING be used on a subscription connection? /// - internal bool PingOnSubscriber => Version >= v3_0_0; + internal bool PingOnSubscriber => Version.IsAtLeast(v3_0_0); /// /// Does SPOP support popping multiple items? /// - public bool SetPopMultiple => Version >= v3_2_0; + public bool SetPopMultiple => Version.IsAtLeast(v3_2_0); /// /// Is TOUCH available? /// - public bool KeyTouch => Version >= v3_2_1; + public bool KeyTouch => Version.IsAtLeast(v3_2_1); /// /// Does the server prefer 'replica' terminology - 'REPLICAOF', etc? /// - public bool ReplicaCommands => Version >= v5_0_0; + public bool ReplicaCommands => Version.IsAtLeast(v5_0_0); /// /// Do list-push commands support multiple arguments? /// - public bool PushMultiple => Version >= v4_0_0; + public bool PushMultiple => Version.IsAtLeast(v4_0_0); + + + /// + /// Is the RESP3 protocol available? + /// + public bool Resp3 => Version.IsAtLeast(v6_0_0); /// /// The Redis version of the server @@ -274,7 +285,7 @@ public override string ToString() if (v.Build >= 0) sb.Append('.').Append(v.Build); sb.AppendLine(); object boxed = this; - foreach(var prop in s_props) + foreach (var prop in s_props) { sb.Append(prop.Name).Append(": ").Append(prop.GetValue(boxed)).AppendLine(); } @@ -291,7 +302,7 @@ orderby prop.Name /// Returns the hash code for this instance. /// A 32-bit signed integer that is the hash code for this instance. - public override int GetHashCode() => Version.GetHashCode(); + public override int GetHashCode() => Version.GetNormalizedHashCode(); /// /// Indicates whether this instance and a specified object are equal. @@ -300,16 +311,57 @@ orderby prop.Name /// if and this instance are the same type and represent the same value, otherwise. /// /// The object to compare with the current instance. - public override bool Equals(object? obj) => obj is RedisFeatures f && f.Version == Version; + public override bool Equals(object? obj) => obj is RedisFeatures f && f.Version.IsEqual(Version); + + /// + /// Indicates whether this instance and a specified object are equal. + /// + /// + /// if and this instance are the same type and represent the same value, otherwise. + /// + /// The object to compare with the current instance. + public bool Equals(RedisFeatures other) => other.Version.IsEqual(Version); /// /// Checks if 2 are .Equal(). /// - public static bool operator ==(RedisFeatures left, RedisFeatures right) => left.Equals(right); + public static bool operator ==(RedisFeatures left, RedisFeatures right) => left.Version.IsEqual(right.Version); /// /// Checks if 2 are not .Equal(). /// - public static bool operator !=(RedisFeatures left, RedisFeatures right) => !left.Equals(right); + public static bool operator !=(RedisFeatures left, RedisFeatures right) => !left.Version.IsEqual(right.Version); + } +} + +internal static class VersionExtensions +{ + // normalize two version parts and smash them together into a long; if either part is -ve, + // zero is used instead; this gives us consistent ordering following "long" rules + + private static long ComposeMajorMinor(Version version) // always specified + => (((long)version.Major) << 32) | (long)version.Minor; + + private static long ComposeBuildRevision(Version version) // can be -ve for "not specified" + { + int build = version.Build, revision = version.Revision; + return (((long)(build < 0 ? 0 : build)) << 32) | (long)(revision < 0 ? 0 : revision); + } + + internal static int GetNormalizedHashCode(this Version value) + => (ComposeMajorMinor(value) * ComposeBuildRevision(value)).GetHashCode(); + + internal static bool IsEqual(this Version x, Version y) + => ComposeMajorMinor(x) == ComposeMajorMinor(y) + && ComposeBuildRevision(x) == ComposeBuildRevision(y); + + internal static bool IsAtLeast(this Version x, Version y) + { + // think >=, but: without the... "unusual behaviour" in how Version's >= operator + // compares values with different part lengths, i.e. "6.0" **is not** >= "6.0.0" + // under the inbuilt operator + var delta = ComposeMajorMinor(x) - ComposeMajorMinor(y); + if (delta > 0) return true; + return delta < 0 ? false : ComposeBuildRevision(x) >= ComposeBuildRevision(y); } } diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index e926b6da4..7f786c4d7 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -35,7 +35,14 @@ public static readonly CommandBytes groups = "groups", lastGeneratedId = "last-generated-id", firstEntry = "first-entry", - lastEntry = "last-entry"; + lastEntry = "last-entry", + + // HELLO + version = "version", + proto = "proto", + role = "role", + mode = "mode", + id = "id"; } internal static class RedisLiterals { @@ -50,6 +57,7 @@ public static readonly RedisValue AND = "AND", ANY = "ANY", ASC = "ASC", + AUTH = "AUTH", BEFORE = "BEFORE", BIT = "BIT", BY = "BY", @@ -61,6 +69,7 @@ public static readonly RedisValue COPY = "COPY", COUNT = "COUNT", DB = "DB", + @default = "default", DESC = "DESC", DOCTOR = "DOCTOR", ENCODING = "ENCODING", diff --git a/src/StackExchange.Redis/RedisProtocol.cs b/src/StackExchange.Redis/RedisProtocol.cs new file mode 100644 index 000000000..8c1c9b869 --- /dev/null +++ b/src/StackExchange.Redis/RedisProtocol.cs @@ -0,0 +1,21 @@ +namespace StackExchange.Redis; + +/// +/// Indicates the protocol for communicating with the server. +/// +public enum RedisProtocol +{ + // note: the non-binary safe protocol is not supported by the client, although the parser does support it (it is used in the toy server) + + // important: please use "major_minor_revision" numbers (two digit minor/revision), to allow for possible scenarios like + // "hey, we've added RESP 3.1; oops, we've added RESP 3.1.1" + + /// + /// The protocol used by all redis server versions since 1.2, as defined by https://github.com/redis/redis-specifications/blob/master/protocol/RESP2.md + /// + Resp2 = 2_00_00, // major__minor__revision + /// + /// Opt-in variant introduced in server version 6, as defined by https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md + /// + Resp3 = 3_00_00, // major__minor__revision +} diff --git a/src/StackExchange.Redis/RedisResult.cs b/src/StackExchange.Redis/RedisResult.cs index 9935deee7..b39a646e0 100644 --- a/src/StackExchange.Redis/RedisResult.cs +++ b/src/StackExchange.Redis/RedisResult.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -10,12 +11,21 @@ namespace StackExchange.Redis /// public abstract class RedisResult { + /// + /// Do not use. + /// + [Obsolete("Please specify a result type", true)] // retained purely for binary compat + public RedisResult() : this(default) { } + + internal RedisResult(ResultType resultType) => Resp3Type = resultType; + /// /// Create a new RedisResult representing a single value. /// /// The to create a result from. /// The type of result being represented /// new . + [SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads", Justification = "")] public static RedisResult Create(RedisValue value, ResultType? resultType = null) => new SingleRedisResult(value, resultType); /// @@ -23,9 +33,18 @@ public abstract class RedisResult /// /// The s to create a result from. /// new . - public static RedisResult Create(RedisValue[] values) => - values == null ? NullArray : values.Length == 0 ? EmptyArray : - new ArrayRedisResult(Array.ConvertAll(values, value => new SingleRedisResult(value, null))); + public static RedisResult Create(RedisValue[] values) + => Create(values, ResultType.Array); + + /// + /// Create a new RedisResult representing an array of values. + /// + /// The s to create a result from. + /// The explicit data type. + /// new . + public static RedisResult Create(RedisValue[] values, ResultType resultType) => + values == null ? NullArray : values.Length == 0 ? EmptyArray(resultType) : + new ArrayRedisResult(Array.ConvertAll(values, value => new SingleRedisResult(value, null)), resultType); /// /// Create a new RedisResult representing an array of values. @@ -33,22 +52,54 @@ public static RedisResult Create(RedisValue[] values) => /// The s to create a result from. /// new . public static RedisResult Create(RedisResult[] values) - => values == null ? NullArray : values.Length == 0 ? EmptyArray : new ArrayRedisResult(values); + => Create(values, ResultType.Array); + + /// + /// Create a new RedisResult representing an array of values. + /// + /// The s to create a result from. + /// The explicit data type. + /// new . + public static RedisResult Create(RedisResult[] values, ResultType resultType) + => values == null ? NullArray : values.Length == 0 ? EmptyArray(resultType) : new ArrayRedisResult(values, resultType); /// /// An empty array result. /// - internal static RedisResult EmptyArray { get; } = new ArrayRedisResult(Array.Empty()); + internal static RedisResult EmptyArray(ResultType type) => type switch + { + ResultType.Array => s_EmptyArray ??= new ArrayRedisResult(Array.Empty(), type), + ResultType.Set => s_EmptySet ??= new ArrayRedisResult(Array.Empty(), type), + ResultType.Map => s_EmptyMap ??= new ArrayRedisResult(Array.Empty(), type), + _ => new ArrayRedisResult(Array.Empty(), type), + }; + + private static RedisResult? s_EmptyArray, s_EmptySet, s_EmptyMap; /// /// A null array result. /// - internal static RedisResult NullArray { get; } = new ArrayRedisResult(null); + internal static RedisResult NullArray { get; } = new ArrayRedisResult(null, ResultType.Null); /// /// A null single result, to use as a default for invalid returns. /// - internal static RedisResult NullSingle { get; } = new SingleRedisResult(RedisValue.Null, ResultType.None); + internal static RedisResult NullSingle { get; } = new SingleRedisResult(RedisValue.Null, ResultType.Null); + + /// + /// Gets the number of elements in this item if it is a valid array, or -1 otherwise. + /// + public virtual int Length => -1; + + /// + public sealed override string ToString() => ToString(out _) ?? ""; + + /// + /// Gets the string content as per , but also obtains the declared type from verbatim strings (for example LATENCY DOCTOR) + /// + /// The type of the returned string. + /// The content + public abstract string? ToString(out string? type); /// /// Internally, this is very similar to RawResult, except it is designed to be usable, @@ -58,14 +109,14 @@ internal static bool TryCreate(PhysicalConnection connection, in RawResult resul { try { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.Integer: case ResultType.SimpleString: case ResultType.BulkString: - redisResult = new SingleRedisResult(result.AsRedisValue(), result.Type); + redisResult = new SingleRedisResult(result.AsRedisValue(), result.Resp3Type); return true; - case ResultType.MultiBulk: + case ResultType.Array: if (result.IsNull) { redisResult = NullArray; @@ -74,7 +125,7 @@ internal static bool TryCreate(PhysicalConnection connection, in RawResult resul var items = result.GetItems(); if (items.Length == 0) { - redisResult = EmptyArray; + redisResult = EmptyArray(result.Resp3Type); return true; } var arr = new RedisResult[items.Length]; @@ -91,10 +142,10 @@ internal static bool TryCreate(PhysicalConnection connection, in RawResult resul return false; } } - redisResult = new ArrayRedisResult(arr); + redisResult = new ArrayRedisResult(arr, result.Resp3Type); return true; case ResultType.Error: - redisResult = new ErrorRedisResult(result.GetString()); + redisResult = new ErrorRedisResult(result.GetString(), result.Resp3Type); return true; default: redisResult = null; @@ -110,9 +161,23 @@ internal static bool TryCreate(PhysicalConnection connection, in RawResult resul } /// - /// Indicate the type of result that was received from redis. + /// Indicate the type of result that was received from redis, in RESP2 terms. + /// + [Obsolete($"Please use either {nameof(Resp2Type)} (simplified) or {nameof(Resp3Type)} (full)")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public ResultType Type => Resp2Type; + + /// + /// Indicate the type of result that was received from redis, in RESP3 terms. + /// + public ResultType Resp3Type { get; } + + /// + /// Indicate the type of result that was received from redis, in RESP2 terms. /// - public abstract ResultType Type { get; } + public ResultType Resp2Type => Resp3Type == ResultType.Null ? Resp2NullType : Resp3Type.ToResp2(); + + internal virtual ResultType Resp2NullType => ResultType.BulkString; /// /// Indicates whether this result was a null result. @@ -263,6 +328,11 @@ public Dictionary ToDictionary(IEqualityComparer? c return result; } + /// + /// Get a sub-item by index. + /// + public virtual RedisResult this[int index] => throw new InvalidOperationException("Indexers can only be used on array results"); + internal abstract bool AsBoolean(); internal abstract bool[]? AsBooleanArray(); internal abstract byte[]? AsByteArray(); @@ -287,18 +357,26 @@ public Dictionary ToDictionary(IEqualityComparer? c internal abstract RedisValue[]? AsRedisValueArray(); internal abstract string? AsString(); internal abstract string?[]? AsStringArray(); + private sealed class ArrayRedisResult : RedisResult { - public override bool IsNull => _value == null; + public override bool IsNull => _value is null; private readonly RedisResult[]? _value; - public override ResultType Type => ResultType.MultiBulk; - public ArrayRedisResult(RedisResult[]? value) + internal override ResultType Resp2NullType => ResultType.Array; + + public ArrayRedisResult(RedisResult[]? value, ResultType resultType) : base(value is null ? ResultType.Null : resultType) { _value = value; } - public override string ToString() => _value == null ? "(nil)" : (_value.Length + " element(s)"); + public override int Length => _value is null ? -1 : _value.Length; + + public override string? ToString(out string? type) + { + type = null; + return _value == null ? "(nil)" : (_value.Length + " element(s)"); + } internal override bool AsBoolean() { @@ -306,6 +384,8 @@ internal override bool AsBoolean() throw new InvalidCastException(); } + public override RedisResult this[int index] => _value![index]; + internal override bool[]? AsBooleanArray() => IsNull ? null : Array.ConvertAll(_value!, x => x.AsBoolean()); internal override byte[]? AsByteArray() @@ -446,14 +526,17 @@ private sealed class ErrorRedisResult : RedisResult { private readonly string value; - public override ResultType Type => ResultType.Error; - public ErrorRedisResult(string? value) + public ErrorRedisResult(string? value, ResultType type) : base(type) { this.value = value ?? throw new ArgumentNullException(nameof(value)); } public override bool IsNull => value == null; - public override string ToString() => value; + public override string? ToString(out string? type) + { + type = null; + return value; + } internal override bool AsBoolean() => throw new RedisServerException(value); internal override bool[] AsBooleanArray() => throw new RedisServerException(value); internal override byte[] AsByteArray() => throw new RedisServerException(value); @@ -483,17 +566,26 @@ public ErrorRedisResult(string? value) private sealed class SingleRedisResult : RedisResult, IConvertible { private readonly RedisValue _value; - public override ResultType Type { get; } - public SingleRedisResult(RedisValue value, ResultType? resultType) + public SingleRedisResult(RedisValue value, ResultType? resultType) : base(value.IsNull ? ResultType.Null : resultType ?? (value.IsInteger ? ResultType.Integer : ResultType.BulkString)) { _value = value; - Type = resultType ?? (value.IsInteger ? ResultType.Integer : ResultType.BulkString); } - public override bool IsNull => _value.IsNull; + public override bool IsNull => Resp3Type == ResultType.Null || _value.IsNull; + + public override string? ToString(out string? type) + { + type = null; + string? s = _value; + if (Resp3Type == ResultType.VerbatimString && s is not null && s.Length >= 4 && s[3] == ':') + { // remove the prefix + type = s.Substring(0, 3); + s = s.Substring(4); + } + return s; + } - public override string ToString() => _value.ToString(); internal override bool AsBoolean() => (bool)_value; internal override bool[] AsBooleanArray() => new[] { AsBoolean() }; internal override byte[]? AsByteArray() => (byte[]?)_value; @@ -555,7 +647,7 @@ ulong IConvertible.ToUInt64(IFormatProvider? provider) decimal IConvertible.ToDecimal(IFormatProvider? provider) { // we can do this safely *sometimes* - if (Type == ResultType.Integer) return AsInt64(); + if (Resp2Type == ResultType.Integer) return AsInt64(); // but not always ThrowNotSupported(); return default; @@ -578,7 +670,7 @@ object IConvertible.ToType(Type conversionType, IFormatProvider? provider) case TypeCode.UInt64: checked { return (ulong)AsInt64(); } case TypeCode.Single: return (float)AsDouble(); case TypeCode.Double: return AsDouble(); - case TypeCode.Decimal when Type == ResultType.Integer: return AsInt64(); + case TypeCode.Decimal when Resp2Type == ResultType.Integer: return AsInt64(); case TypeCode.String: return AsString()!; default: ThrowNotSupported(); diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index b8e0de696..bce499bec 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -34,6 +34,8 @@ internal RedisServer(ConnectionMultiplexer multiplexer, ServerEndPoint server, o bool IServer.IsSlave => IsReplica; public bool IsReplica => server.IsReplica; + public RedisProtocol Protocol => server.Protocol ?? (multiplexer.RawConfig.TryResp3() ? RedisProtocol.Resp3 : RedisProtocol.Resp2); + bool IServer.AllowSlaveWrites { get => AllowReplicaWrites; @@ -917,12 +919,12 @@ private class ScanResultProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeArray) { - case ResultType.MultiBulk: + case ResultType.Array: var arr = result.GetItems(); RawResult inner; - if (arr.Length == 2 && (inner = arr[1]).Type == ResultType.MultiBulk) + if (arr.Length == 2 && (inner = arr[1]).Resp2TypeArray == ResultType.Array) { var items = inner.GetItems(); RedisKey[] keys; diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index 5a24a716e..cb4940e41 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -18,7 +18,10 @@ public partial class ConnectionMultiplexer private readonly ConcurrentDictionary subscriptions = new(); internal ConcurrentDictionary GetSubscriptions() => subscriptions; + ConcurrentDictionary IInternalConnectionMultiplexer.GetSubscriptions() => GetSubscriptions(); + internal int GetSubscriptionsCount() => subscriptions.Count; + int IInternalConnectionMultiplexer.GetSubscriptionsCount() => GetSubscriptionsCount(); internal Subscription GetOrAddSubscription(in RedisChannel channel, CommandFlags flags) { diff --git a/src/StackExchange.Redis/RedisTransaction.cs b/src/StackExchange.Redis/RedisTransaction.cs index 32e7bfb1d..ea71e7dd1 100644 --- a/src/StackExchange.Redis/RedisTransaction.cs +++ b/src/StackExchange.Redis/RedisTransaction.cs @@ -199,7 +199,7 @@ private class QueuedProcessor : ResultProcessor protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - if (result.Type == ResultType.SimpleString && result.IsEqual(CommonReplies.QUEUED)) + if (result.Resp2TypeBulkString == ResultType.SimpleString && result.IsEqual(CommonReplies.QUEUED)) { if (message is QueuedMessage q) { @@ -270,8 +270,7 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) public IEnumerable GetMessages(PhysicalConnection connection) { IResultBox? lastBox = null; - var bridge = connection.BridgeCouldBeNull; - if (bridge == null) throw new ObjectDisposedException(connection.ToString()); + var bridge = connection.BridgeCouldBeNull ?? throw new ObjectDisposedException(connection.ToString()); bool explicitCheckForQueued = !bridge.ServerEndPoint.GetFeatures().ExecAbort; var multiplexer = bridge.Multiplexer; @@ -486,7 +485,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (message is TransactionMessage tran) { var wrapped = tran.InnerOperations; - switch (result.Type) + switch (result.Resp2TypeArray) { case ResultType.SimpleString: if (tran.IsAborted && result.IsEqual(CommonReplies.OK)) @@ -510,7 +509,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return true; } break; - case ResultType.MultiBulk: + case ResultType.Array: if (!tran.IsAborted) { var arr = result.GetItems(); diff --git a/src/StackExchange.Redis/ResultBox.cs b/src/StackExchange.Redis/ResultBox.cs index 111394321..c61221018 100644 --- a/src/StackExchange.Redis/ResultBox.cs +++ b/src/StackExchange.Redis/ResultBox.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 236946d57..6fd229af1 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -1,4 +1,6 @@ -using System; +using Microsoft.Extensions.Logging; +using Pipelines.Sockets.Unofficial.Arenas; +using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; @@ -8,8 +10,6 @@ using System.Net; using System.Text; using System.Text.RegularExpressions; -using Microsoft.Extensions.Logging; -using Pipelines.Sockets.Unofficial.Arenas; namespace StackExchange.Redis { @@ -57,8 +57,7 @@ public static readonly MultiStreamProcessor public static readonly ResultProcessor Int64 = new Int64Processor(), PubSubNumSub = new PubSubNumSubProcessor(), - Int64DefaultNegativeOne = new Int64DefaultValueProcessor(-1), - ClientId = new ClientIdProcessor(); + Int64DefaultNegativeOne = new Int64DefaultValueProcessor(-1); public static readonly ResultProcessor NullableDouble = new NullableDoubleProcessor(); @@ -335,7 +334,7 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in private void UnexpectedResponse(Message message, in RawResult result) { ConnectionMultiplexer.TraceWithoutContext("From " + GetType().Name, "Unexpected Response"); - ConnectionFail(message, ConnectionFailureType.ProtocolFailure, "Unexpected response to " + (message?.Command.ToString() ?? "n/a") + ": " + result.ToString()); + ConnectionFail(message, ConnectionFailureType.ProtocolFailure, "Unexpected response to " + (message?.CommandString ?? "n/a") + ": " + result.ToString()); } public sealed class TimeSpanProcessor : ResultProcessor @@ -348,7 +347,7 @@ public TimeSpanProcessor(bool isMilliseconds) public bool TryParse(in RawResult result, out TimeSpan? expiry) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.Integer: long time; @@ -398,7 +397,7 @@ public static TimerMessage CreateMessage(int db, CommandFlags flags, RedisComman protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - if (result.Type == ResultType.Error) + if (result.IsError) { return false; } @@ -455,7 +454,7 @@ public sealed class TrackSubscriptionsProcessor : ResultProcessor protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - if (result.Type == ResultType.MultiBulk) + if (result.Resp2TypeArray == ResultType.Array) { var items = result.GetItems(); if (items.Length >= 3 && items[2].TryGetInt64(out long count)) @@ -481,7 +480,7 @@ internal sealed class DemandZeroOrOneProcessor : ResultProcessor { public static bool TryGet(in RawResult result, out bool value) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.Integer: case ResultType.SimpleString: @@ -549,7 +548,7 @@ static int FromHex(char c) // (is that a thing?) will be wrapped in the RedisResult protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.BulkString: var asciiHash = result.GetBlob(); @@ -574,16 +573,16 @@ internal sealed class SortedSetEntryProcessor : ResultProcessor { public static bool TryParse(in RawResult result, out SortedSetEntry? entry) { - switch (result.Type) + switch (result.Resp2TypeArray) { - case ResultType.MultiBulk: - var arr = result.GetItems(); - if (result.IsNull || arr.Length < 2) + case ResultType.Array: + if (result.IsNull || result.ItemsCount < 2) { entry = null; } else { + var arr = result.GetItems(); entry = new SortedSetEntry(arr[0].AsRedisValue(), arr[1].TryGetDouble(out double val) ? val : double.NaN); } return true; @@ -606,7 +605,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal sealed class SortedSetEntryArrayProcessor : ValuePairInterleavedProcessorBase { - protected override SortedSetEntry Parse(in RawResult first, in RawResult second) => + protected override SortedSetEntry Parse(in RawResult first, in RawResult second, object? state) => new SortedSetEntry(first.AsRedisValue(), second.TryGetDouble(out double val) ? val : double.NaN); } @@ -614,7 +613,7 @@ internal sealed class SortedSetPopResultProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - if (result.Type == ResultType.MultiBulk) + if (result.Resp2TypeArray == ResultType.Array) { if (result.IsNull) { @@ -655,63 +654,126 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal sealed class HashEntryArrayProcessor : ValuePairInterleavedProcessorBase { - protected override HashEntry Parse(in RawResult first, in RawResult second) => + protected override HashEntry Parse(in RawResult first, in RawResult second, object? state) => new HashEntry(first.AsRedisValue(), second.AsRedisValue()); } internal abstract class ValuePairInterleavedProcessorBase : ResultProcessor { + // when RESP3 was added, some interleaved value/pair responses: became jagged instead; + // this isn't strictly a RESP3 thing (RESP2 supports jagged), but: it is a thing that + // happened, and we need to handle that; thus, by default, we'll detect jagged data + // and handle it automatically; this virtual is included so we can turn it off + // on a per-processor basis if needed + protected virtual bool AllowJaggedPairs => true; + public bool TryParse(in RawResult result, out T[]? pairs) => TryParse(result, out pairs, false, out _); - public bool TryParse(in RawResult result, out T[]? pairs, bool allowOversized, out int count) + public T[]? ParseArray(in RawResult result, bool allowOversized, out int count, object? state) { - count = 0; - switch (result.Type) + if (result.IsNull) { - case ResultType.MultiBulk: - var arr = result.GetItems(); - if (result.IsNull) + count = 0; + return null; + } + + var arr = result.GetItems(); + count = (int)arr.Length; + if (count == 0) + { + return Array.Empty(); + } + + bool interleaved = !(result.IsResp3 && AllowJaggedPairs && IsAllJaggedPairs(arr)); + if (interleaved) count >>= 1; // so: half of that + var pairs = allowOversized ? ArrayPool.Shared.Rent(count) : new T[count]; + + if (interleaved) + { + // linear elements i.e. {key,value,key,value,key,value} + if (arr.IsSingleSegment) + { + var span = arr.FirstSpan; + int offset = 0; + for (int i = 0; i < count; i++) { - pairs = null; + pairs[i] = Parse(span[offset++], span[offset++], state); } - else + } + else + { + var iter = arr.GetEnumerator(); // simplest way of getting successive values + for (int i = 0; i < count; i++) { - count = (int)arr.Length / 2; - if (count == 0) - { - pairs = Array.Empty(); - } - else - { - pairs = allowOversized ? ArrayPool.Shared.Rent(count) : new T[count]; - if (arr.IsSingleSegment) - { - var span = arr.FirstSpan; - int offset = 0; - for (int i = 0; i < count; i++) - { - pairs[i] = Parse(span[offset++], span[offset++]); - } - } - else - { - var iter = arr.GetEnumerator(); // simplest way of getting successive values - for (int i = 0; i < count; i++) - { - pairs[i] = Parse(iter.GetNext(), iter.GetNext()); - } - } - } + pairs[i] = Parse(iter.GetNext(), iter.GetNext(), state); + } + } + } + else + { + // jagged elements i.e. {{key,value},{key,value},{key,value}} + // to get here, we've already asserted that all elements are arrays with length 2 + if (arr.IsSingleSegment) + { + int i = 0; + foreach (var el in arr.FirstSpan) + { + var inner = el.GetItems(); + pairs[i++] = Parse(inner[0], inner[1], state); + } + } + else + { + var iter = arr.GetEnumerator(); // simplest way of getting successive values + for (int i = 0; i < count; i++) + { + var inner = iter.GetNext().GetItems(); + pairs[i] = Parse(inner[0], inner[1], state); + } + } + } + return pairs; + + static bool IsAllJaggedPairs(in Sequence arr) + { + return arr.IsSingleSegment ? CheckSpan(arr.FirstSpan) : CheckSpans(arr); + + static bool CheckSpans(in Sequence arr) + { + foreach (var chunk in arr.Spans) + { + if (!CheckSpan(chunk)) return false; } return true; + } + static bool CheckSpan(ReadOnlySpan chunk) + { + // check whether each value is actually an array of length 2 + foreach (ref readonly RawResult el in chunk) + { + if (el is not { Resp2TypeArray: ResultType.Array, ItemsCount: 2 }) return false; + } + return true; + } + } + } + + public bool TryParse(in RawResult result, out T[]? pairs, bool allowOversized, out int count) + { + switch (result.Resp2TypeArray) + { + case ResultType.Array: + pairs = ParseArray(in result, allowOversized, out count, null); + return true; default: + count = 0; pairs = null; return false; } } - protected abstract T Parse(in RawResult first, in RawResult second); + protected abstract T Parse(in RawResult first, in RawResult second, object? state); protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { if (TryParse(result, out T[]? arr)) @@ -740,6 +802,7 @@ public override bool SetResult(PhysicalConnection connection, Message message, i server.IsReplica = true; } } + return base.SetResult(connection, message, result); } @@ -747,8 +810,19 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { var server = connection.BridgeCouldBeNull?.ServerEndPoint; if (server == null) return false; - switch (result.Type) + + switch (result.Resp2TypeBulkString) { + case ResultType.Integer: + if (message?.Command == RedisCommand.CLIENT) + { + if (result.TryGetInt64(out long clientId)) + { + connection.ConnectionId = clientId; + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (CLIENT) connection-id: {clientId}"); + } + } + break; case ResultType.BulkString: if (message?.Command == RedisCommand.INFO) { @@ -773,17 +847,10 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if ((val = Extract(line, "role:")) != null) { roleSeen = true; - switch (val) + if (TryParseRole(val, out bool isReplica)) { - case "master": - server.IsReplica = false; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) role: primary"); - break; - case "replica": - case "slave": - server.IsReplica = true; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) role: replica"); - break; + server.IsReplica = isReplica; + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) role: {(isReplica ? "replica" : "primary")}"); } } else if ((val = Extract(line, "master_host:")) != null) @@ -796,7 +863,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } else if ((val = Extract(line, "redis_version:")) != null) { - if (Version.TryParse(val, out Version? version)) + if (Format.TryParseVersion(val, out Version? version)) { server.Version = version; Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) version: " + version); @@ -804,20 +871,10 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } else if ((val = Extract(line, "redis_mode:")) != null) { - switch (val) + if (TryParseServerType(val, out var serverType)) { - case "standalone": - server.ServerType = ServerType.Standalone; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) server-type: standalone"); - break; - case "cluster": - server.ServerType = ServerType.Cluster; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) server-type: cluster"); - break; - case "sentinel": - server.ServerType = ServerType.Sentinel; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) server-type: sentinel"); - break; + server.ServerType = serverType; + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) server-type: {serverType}"); } } else if ((val = Extract(line, "run_id:")) != null) @@ -839,7 +896,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } SetResult(message, true); return true; - case ResultType.MultiBulk: + case ResultType.Array: if (message?.Command == RedisCommand.CONFIG) { var iter = result.GetItems().GetEnumerator(); @@ -888,6 +945,42 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } } + else if (message?.Command == RedisCommand.HELLO) + { + var iter = result.GetItems().GetEnumerator(); + while (iter.MoveNext()) + { + ref RawResult key = ref iter.Current; + if (!iter.MoveNext()) break; + ref RawResult val = ref iter.Current; + + if (key.IsEqual(CommonReplies.version) && Format.TryParseVersion(val.GetString(), out var version)) + { + server.Version = version; + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (HELLO) server-version: {version}"); + } + else if (key.IsEqual(CommonReplies.proto) && val.TryGetInt64(out var i64)) + { + connection.SetProtocol(i64 >= 3 ? RedisProtocol.Resp3 : RedisProtocol.Resp2); + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (HELLO) protocol: {connection.Protocol}"); + } + else if (key.IsEqual(CommonReplies.id) && val.TryGetInt64(out i64)) + { + connection.ConnectionId = i64; + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (HELLO) connection-id: {i64}"); + } + else if (key.IsEqual(CommonReplies.mode) && TryParseServerType(val.GetString(), out var serverType)) + { + server.ServerType = serverType; + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (HELLO) server-type: {serverType}"); + } + else if (key.IsEqual(CommonReplies.role) && TryParseRole(val.GetString(), out bool isReplica)) + { + server.IsReplica = isReplica; + Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (HELLO) role: {(isReplica ? "replica" : "primary")}"); + } + } + } else if (message?.Command == RedisCommand.SENTINEL) { server.ServerType = ServerType.Sentinel; @@ -904,6 +997,45 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (line.StartsWith(prefix)) return line.Substring(prefix.Length).Trim(); return null; } + + private static bool TryParseServerType(string? val, out ServerType serverType) + { + switch (val) + { + case "standalone": + serverType = ServerType.Standalone; + return true; + case "cluster": + serverType = ServerType.Cluster; + return true; + case "sentinel": + serverType = ServerType.Sentinel; + return true; + default: + serverType = default; + return false; + } + } + + private static bool TryParseRole(string? val, out bool isReplica) + { + switch (val) + { + case "primary": + case "master": + isReplica = false; + return true; + case "replica": + case "slave": + isReplica = true; + return true; + default: + isReplica = default; + return false; + } + } + + internal static ResultProcessor Create(ILogger? log) => log is null ? AutoConfigure : new AutoConfigureProcessor(log); } private sealed class BooleanProcessor : ResultProcessor @@ -915,7 +1047,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes SetResult(message, false); // lots of ops return (nil) when they mean "no" return true; } - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.SimpleString: if (result.IsEqual(CommonReplies.OK)) @@ -931,7 +1063,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes case ResultType.BulkString: SetResult(message, result.GetBoolean()); return true; - case ResultType.MultiBulk: + case ResultType.Array: var items = result.GetItems(); if (items.Length == 1) { // treat an array of 1 like a single reply (for example, SCRIPT EXISTS) @@ -948,7 +1080,7 @@ private sealed class ByteArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.BulkString: SetResult(message, result.GetBlob()); @@ -971,7 +1103,7 @@ internal static ClusterConfiguration Parse(PhysicalConnection connection, string protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.BulkString: string nodes = result.GetString()!; @@ -989,7 +1121,7 @@ private sealed class ClusterNodesRawProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.Integer: case ResultType.SimpleString: @@ -1024,7 +1156,7 @@ private sealed class DateTimeProcessor : ResultProcessor protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { long unixTime; - switch (result.Type) + switch (result.Resp2TypeArray) { case ResultType.Integer: if (result.TryGetInt64(out unixTime)) @@ -1034,7 +1166,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return true; } break; - case ResultType.MultiBulk: + case ResultType.Array: var arr = result.GetItems(); switch (arr.Length) { @@ -1068,7 +1200,7 @@ public sealed class NullableDateTimeProcessor : ResultProcessor protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.Integer when result.TryGetInt64(out var duration): DateTime? expiry = duration switch @@ -1093,7 +1225,7 @@ private sealed class DoubleProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.Integer: long i64; @@ -1143,12 +1275,14 @@ private sealed class InfoProcessor : ResultProcessor>>(); - using (var reader = new StringReader(result.GetString()!)) + var raw = result.GetString(); + if (raw is not null) { + using var reader = new StringReader(raw); while (reader.ReadLine() is string line) { if (string.IsNullOrWhiteSpace(line)) continue; @@ -1189,7 +1323,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes SetResult(message, _defaultValue); return true; } - if (result.Type == ResultType.Integer && result.TryGetInt64(out var i64)) + if (result.Resp2TypeBulkString == ResultType.Integer && result.TryGetInt64(out var i64)) { SetResult(message, i64); return true; @@ -1202,28 +1336,7 @@ private class Int64Processor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) - { - case ResultType.Integer: - case ResultType.SimpleString: - case ResultType.BulkString: - long i64; - if (result.TryGetInt64(out i64)) - { - SetResult(message, i64); - return true; - } - break; - } - return false; - } - } - - private class ClientIdProcessor : ResultProcessor - { - protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) - { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.Integer: case ResultType.SimpleString: @@ -1232,7 +1345,6 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (result.TryGetInt64(out i64)) { SetResult(message, i64); - connection.ConnectionId = i64; return true; } break; @@ -1245,7 +1357,7 @@ private class PubSubNumSubProcessor : Int64Processor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - if (result.Type == ResultType.MultiBulk) + if (result.Resp2TypeArray == ResultType.Array) { var arr = result.GetItems(); if (arr.Length == 2 && arr[1].TryGetInt64(out long val)) @@ -1262,7 +1374,7 @@ private sealed class NullableDoubleArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - if (result.Type == ResultType.MultiBulk && !result.IsNull) + if (result.Resp2TypeArray == ResultType.Array && !result.IsNull) { var arr = result.GetItemsAsDoubles()!; SetResult(message, arr); @@ -1276,7 +1388,7 @@ private sealed class NullableDoubleProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.Integer: case ResultType.SimpleString: @@ -1302,7 +1414,7 @@ private sealed class NullableInt64Processor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.Integer: case ResultType.SimpleString: @@ -1344,9 +1456,9 @@ public ChannelState(byte[]? prefix, RedisChannel.PatternMode mode) } protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeArray) { - case ResultType.MultiBulk: + case ResultType.Array: var final = result.ToArray( (in RawResult item, in ChannelState state) => item.AsRedisChannel(state.Prefix, state.Mode), new ChannelState(connection.ChannelPrefix, mode))!; @@ -1362,9 +1474,9 @@ private sealed class RedisKeyArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeArray) { - case ResultType.MultiBulk: + case ResultType.Array: var arr = result.GetItemsAsKeys()!; SetResult(message, arr); return true; @@ -1377,7 +1489,7 @@ private sealed class RedisKeyProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.Integer: case ResultType.SimpleString: @@ -1393,7 +1505,7 @@ private sealed class RedisTypeProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.SimpleString: case ResultType.BulkString: @@ -1412,7 +1524,7 @@ private sealed class RedisValueArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { // allow a single item to pass explicitly pretending to be an array; example: SPOP {key} 1 case ResultType.BulkString: @@ -1422,7 +1534,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes : new[] { result.AsRedisValue() }; SetResult(message, arr); return true; - case ResultType.MultiBulk: + case ResultType.Array: arr = result.GetItemsAsValues()!; SetResult(message, arr); return true; @@ -1435,7 +1547,7 @@ private sealed class Int64ArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - if (result.Type == ResultType.MultiBulk && !result.IsNull) + if (result.Resp2TypeArray == ResultType.Array && !result.IsNull) { var arr = result.ToArray((in RawResult x) => (long)x.AsRedisValue())!; SetResult(message, arr); @@ -1450,9 +1562,9 @@ private sealed class NullableStringArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeArray) { - case ResultType.MultiBulk: + case ResultType.Array: var arr = result.GetItemsAsStrings()!; SetResult(message, arr); @@ -1466,9 +1578,9 @@ private sealed class StringArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeArray) { - case ResultType.MultiBulk: + case ResultType.Array: var arr = result.GetItemsAsStringsNotNullable()!; SetResult(message, arr); return true; @@ -1481,7 +1593,7 @@ private sealed class BooleanArrayProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - if (result.Type == ResultType.MultiBulk && !result.IsNull) + if (result.Resp2TypeArray == ResultType.Array && !result.IsNull) { var arr = result.GetItemsAsBooleans()!; SetResult(message, arr); @@ -1495,9 +1607,9 @@ private sealed class RedisValueGeoPositionProcessor : ResultProcessor Parse(item, radiusOptions), options)!; SetResult(message, typed); @@ -1610,10 +1722,9 @@ private sealed class LongestCommonSubsequenceProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.Integer: case ResultType.SimpleString: @@ -1790,7 +1901,7 @@ private sealed class LeaseProcessor : ResultProcessor> { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.Integer: case ResultType.SimpleString: @@ -1806,7 +1917,7 @@ private class ScriptResultProcessor : ResultProcessor { public override bool SetResult(PhysicalConnection connection, Message message, in RawResult result) { - if (result.Type == ResultType.Error && result.StartsWith(CommonReplies.NOSCRIPT)) + if (result.IsError && result.StartsWith(CommonReplies.NOSCRIPT)) { // scripts are not flushed individually, so assume the entire script cache is toast ("SCRIPT FLUSH") connection.BridgeCouldBeNull?.ServerEndPoint?.FlushScriptCache(); message.SetScriptUnavailable(); @@ -1849,7 +1960,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return true; } - if (result.Type != ResultType.MultiBulk) + if (result.Resp2TypeArray != ResultType.Array) { return false; } @@ -1858,23 +1969,47 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (skipStreamName) { - // > XREAD COUNT 2 STREAMS mystream 0 - // 1) 1) "mystream" <== Skip the stream name - // 2) 1) 1) 1519073278252 - 0 <== Index 1 contains the array of stream entries - // 2) 1) "foo" - // 2) "value_1" - // 2) 1) 1519073279157 - 0 - // 2) 1) "foo" - // 2) "value_2" - - // Retrieve the initial array. For XREAD of a single stream it will - // be an array of only 1 element in the response. - var readResult = result.GetItems(); - - // Within that single element, GetItems will return an array of - // 2 elements: the stream name and the stream entries. - // Skip the stream name (index 0) and only process the stream entries (index 1). - entries = ParseRedisStreamEntries(readResult[0].GetItems()[1]); + /* + RESP 2: array element per stream; each element is an array of a name plus payload; payload is array of name/value pairs + + 127.0.0.1:6379> XREAD COUNT 2 STREAMS temperatures:us-ny:10007 0-0 + 1) 1) "temperatures:us-ny:10007" + 2) 1) 1) "1691504774593-0" + 2) 1) "temp_f" + 2) "87.2" + 3) "pressure" + 4) "29.69" + 5) "humidity" + 6) "46" + 2) 1) "1691504856705-0" + 2) 1) "temp_f" + 2) "87.2" + 3) "pressure" + 4) "29.69" + 5) "humidity" + 6) "46" + + RESP 3: map of element names with array of name plus payload; payload is array of name/value pairs + + 127.0.0.1:6379> XREAD COUNT 2 STREAMS temperatures:us-ny:10007 0-0 + 1# "temperatures:us-ny:10007" => 1) 1) "1691504774593-0" + 2) 1) "temp_f" + 2) "87.2" + 3) "pressure" + 4) "29.69" + 5) "humidity" + 6) "46" + 2) 1) "1691504856705-0" + 2) 1) "temp_f" + 2) "87.2" + 3) "pressure" + 4) "29.69" + 5) "humidity" + 6) "46" + */ + + ref readonly RawResult readResult = ref (result.Resp3Type == ResultType.Map ? ref result[1] : ref result[0][1]); + entries = ParseRedisStreamEntries(readResult); } else { @@ -1930,26 +2065,45 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return true; } - if (result.Type != ResultType.MultiBulk) + if (result.Resp2TypeArray != ResultType.Array) { return false; } - var streams = result.GetItems().ToArray((in RawResult item, in MultiStreamProcessor obj) => + RedisStream[] streams; + if (result.Resp3Type == ResultType.Map) // see SetResultCore for the shape delta between RESP2 and RESP3 + { + // root is a map of named inner-arrays + streams = RedisStreamInterleavedProcessor.Instance.ParseArray(result, false, out _, this)!; // null-checked + } + else { - var details = item.GetItems(); + streams = result.GetItems().ToArray((in RawResult item, in MultiStreamProcessor obj) => + { + var details = item.GetItems(); - // details[0] = Name of the Stream - // details[1] = Multibulk Array of Stream Entries - return new RedisStream(key: details[0].AsRedisKey(), - entries: obj.ParseRedisStreamEntries(details[1])!); - }, this); + // details[0] = Name of the Stream + // details[1] = Multibulk Array of Stream Entries + return new RedisStream(key: details[0].AsRedisKey(), + entries: obj.ParseRedisStreamEntries(details[1])!); + }, this); + } SetResult(message, streams); return true; } } + private sealed class RedisStreamInterleavedProcessor : ValuePairInterleavedProcessorBase + { + protected override bool AllowJaggedPairs => false; // we only use this on a flattened map + + public static readonly RedisStreamInterleavedProcessor Instance = new(); + private RedisStreamInterleavedProcessor() { } + protected override RedisStream Parse(in RawResult first, in RawResult second, object? state) + => new(key: first.AsRedisKey(), entries: ((MultiStreamProcessor)state!).ParseRedisStreamEntries(second)); + } + /// /// This processor is for *without* the option. /// @@ -1959,7 +2113,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { // See https://redis.io/commands/xautoclaim for command documentation. // Note that the result should never be null, so intentionally treating it as a failure to parse here - if (result.Type == ResultType.MultiBulk && !result.IsNull) + if (result.Resp2TypeArray == ResultType.Array && !result.IsNull) { var items = result.GetItems(); @@ -1988,7 +2142,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { // See https://redis.io/commands/xautoclaim for command documentation. // Note that the result should never be null, so intentionally treating it as a failure to parse here - if (result.Type == ResultType.MultiBulk && !result.IsNull) + if (result.Resp2TypeArray == ResultType.Array && !result.IsNull) { var items = result.GetItems(); @@ -2149,7 +2303,7 @@ internal abstract class InterleavedStreamInfoProcessorBase : ResultProcessor< protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - if (result.Type != ResultType.MultiBulk) + if (result.Resp2TypeArray != ResultType.Array) { return false; } @@ -2186,7 +2340,7 @@ internal sealed class StreamInfoProcessor : StreamProcessorBase // 2) "banana" protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - if (result.Type != ResultType.MultiBulk) + if (result.Resp2TypeArray != ResultType.Array) { return false; } @@ -2262,7 +2416,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes // 5) 1) 1) "Joe" // 2) "8" - if (result.Type != ResultType.MultiBulk) + if (result.Resp2TypeArray != ResultType.Array) { return false; } @@ -2306,7 +2460,7 @@ internal sealed class StreamPendingMessagesProcessor : ResultProcessor + { + public static readonly StreamNameValueEntryProcessor Instance = new(); + private StreamNameValueEntryProcessor() { } + protected override NameValueEntry Parse(in RawResult first, in RawResult second, object? state) + => new NameValueEntry(first.AsRedisValue(), second.AsRedisValue()); + } + /// /// Handles stream responses. For formats, see . /// @@ -2333,7 +2495,7 @@ internal abstract class StreamProcessorBase : ResultProcessor { protected static StreamEntry ParseRedisStreamEntry(in RawResult item) { - if (item.IsNull || item.Type != ResultType.MultiBulk) + if (item.IsNull || item.Resp2TypeArray != ResultType.Array) { return StreamEntry.Null; } @@ -2345,7 +2507,7 @@ protected static StreamEntry ParseRedisStreamEntry(in RawResult item) return new StreamEntry(id: entryDetails[0].AsRedisValue(), values: ParseStreamEntryValues(entryDetails[1])); } - protected StreamEntry[] ParseRedisStreamEntries(in RawResult result) => + protected internal StreamEntry[] ParseRedisStreamEntries(in RawResult result) => result.GetItems().ToArray((in RawResult item, in StreamProcessorBase _) => ParseRedisStreamEntry(item), this); protected static NameValueEntry[] ParseStreamEntryValues(in RawResult result) @@ -2365,34 +2527,17 @@ protected static NameValueEntry[] ParseStreamEntryValues(in RawResult result) // 3) "temperature" // 4) "18.2" - if (result.Type != ResultType.MultiBulk || result.IsNull) + if (result.Resp2TypeArray != ResultType.Array || result.IsNull) { return Array.Empty(); } - - var arr = result.GetItems(); - - // Calculate how many name/value pairs are in the stream entry. - int count = (int)arr.Length / 2; - - if (count == 0) return Array.Empty(); - - var pairs = new NameValueEntry[count]; - - var iter = arr.GetEnumerator(); - for (int i = 0; i < pairs.Length; i++) - { - pairs[i] = new NameValueEntry(iter.GetNext().AsRedisValue(), - iter.GetNext().AsRedisValue()); - } - - return pairs; + return StreamNameValueEntryProcessor.Instance.ParseArray(result, false, out _, null)!; // ! because we checked null above } } private sealed class StringPairInterleavedProcessor : ValuePairInterleavedProcessorBase> { - protected override KeyValuePair Parse(in RawResult first, in RawResult second) => + protected override KeyValuePair Parse(in RawResult first, in RawResult second, object? state) => new KeyValuePair(first.GetString()!, second.GetString()!); } @@ -2400,14 +2545,14 @@ private sealed class StringProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.Integer: case ResultType.SimpleString: case ResultType.BulkString: SetResult(message, result.GetString()); return true; - case ResultType.MultiBulk: + case ResultType.Array: var arr = result.GetItems(); if (arr.Length == 1) { @@ -2424,7 +2569,7 @@ private sealed class TieBreakerProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeBulkString) { case ResultType.SimpleString: case ResultType.BulkString: @@ -2474,6 +2619,13 @@ public override bool SetResult(PhysicalConnection connection, Message message, i connection.RecordConnectionFailed(ConnectionFailureType.ProtocolFailure, new RedisServerException(result.ToString())); } } + + if (connection.Protocol is null) + { + // if we didn't get a valid response from HELLO, then we have to assume RESP2 at some point + connection.SetProtocol(RedisProtocol.Resp2); + } + return final; } @@ -2484,26 +2636,19 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes switch (message.Command) { case RedisCommand.ECHO: - happy = result.Type == ResultType.BulkString && (!establishConnection || result.IsEqual(connection.BridgeCouldBeNull?.Multiplexer?.UniqueId)); + happy = result.Resp2TypeBulkString == ResultType.BulkString && (!establishConnection || result.IsEqual(connection.BridgeCouldBeNull?.Multiplexer?.UniqueId)); break; case RedisCommand.PING: // there are two different PINGs; "interactive" is a +PONG or +{your message}, // but subscriber returns a bulk-array of [ "pong", {your message} ] - switch (result.Type) + switch (result.Resp2TypeArray) { case ResultType.SimpleString: happy = result.IsEqual(CommonReplies.PONG); break; - case ResultType.MultiBulk: - if (result.ItemsCount == 2) - { - var items = result.GetItems(); - happy = items[0].IsEqual(CommonReplies.PONG) && items[1].Payload.IsEmpty; - } - else - { - happy = false; - } + case ResultType.Array when result.ItemsCount == 2: + var items = result.GetItems(); + happy = items[0].IsEqual(CommonReplies.PONG) && items[1].Payload.IsEmpty; break; default: happy = false; @@ -2511,10 +2656,10 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } break; case RedisCommand.TIME: - happy = result.Type == ResultType.MultiBulk && result.GetItems().Length == 2; + happy = result.Resp2TypeArray == ResultType.Array && result.ItemsCount == 2; break; case RedisCommand.EXISTS: - happy = result.Type == ResultType.Integer; + happy = result.Resp2TypeBulkString == ResultType.Integer; break; default: happy = false; @@ -2543,9 +2688,9 @@ private sealed class SentinelGetPrimaryAddressByNameProcessor : ResultProcessor< { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch (result.Type) + switch (result.Resp2TypeArray) { - case ResultType.MultiBulk: + case ResultType.Array: var items = result.GetItems(); if (result.IsNull) { @@ -2573,9 +2718,9 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { List endPoints = new List(); - switch (result.Type) + switch (result.Resp2TypeArray) { - case ResultType.MultiBulk: + case ResultType.Array: foreach (RawResult item in result.GetItems()) { var pairs = item.GetItems(); @@ -2607,9 +2752,9 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { List endPoints = new List(); - switch (result.Type) + switch (result.Resp2TypeArray) { - case ResultType.MultiBulk: + case ResultType.Array: foreach (RawResult item in result.GetItems()) { var pairs = item.GetItems(); @@ -2649,9 +2794,9 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return false; } - switch (result.Type) + switch (result.Resp2TypeArray) { - case ResultType.MultiBulk: + case ResultType.Array: var arrayOfArrays = result.GetItems(); var returnArray = result.ToArray[], StringPairInterleavedProcessor>( @@ -2691,9 +2836,9 @@ internal abstract class ArrayResultProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch(result.Type) + switch(result.Resp2TypeArray) { - case ResultType.MultiBulk: + case ResultType.Array: var items = result.GetItems(); T[] arr; if (items.IsEmpty) diff --git a/src/StackExchange.Redis/ResultTypeExtensions.cs b/src/StackExchange.Redis/ResultTypeExtensions.cs new file mode 100644 index 000000000..e2f941f00 --- /dev/null +++ b/src/StackExchange.Redis/ResultTypeExtensions.cs @@ -0,0 +1,11 @@ +namespace StackExchange.Redis +{ + internal static class ResultTypeExtensions + { + public static bool IsError(this ResultType value) + => (value & (ResultType)0b111) == ResultType.Error; + + public static ResultType ToResp2(this ResultType value) + => value & (ResultType)0b111; // just keep the last 3 bits + } +} diff --git a/src/StackExchange.Redis/Role.cs b/src/StackExchange.Redis/Role.cs index 7f26220fb..587194026 100644 --- a/src/StackExchange.Redis/Role.cs +++ b/src/StackExchange.Redis/Role.cs @@ -62,6 +62,9 @@ internal Replica(string ip, int port, long offset) Port = port; ReplicationOffset = offset; } + + /// + public override string ToString() => $"{Ip}:{Port} - {ReplicationOffset}"; } internal Master(long offset, ICollection replicas) : base(RedisLiterals.master!) diff --git a/src/StackExchange.Redis/ScriptParameterMapper.cs b/src/StackExchange.Redis/ScriptParameterMapper.cs index 88be5d338..9960d87b7 100644 --- a/src/StackExchange.Redis/ScriptParameterMapper.cs +++ b/src/StackExchange.Redis/ScriptParameterMapper.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -224,12 +223,7 @@ public static bool IsValidParameterHash(Type t, LuaScript script, out string? mi for (var i = 0; i < script.Arguments.Length; i++) { var argName = script.Arguments[i]; - var member = t.GetMember(argName).SingleOrDefault(m => m is PropertyInfo || m is FieldInfo); - if (member is null) - { - throw new ArgumentException($"There was no member found for {argName}"); - } - + var member = t.GetMember(argName).SingleOrDefault(m => m is PropertyInfo || m is FieldInfo) ?? throw new ArgumentException($"There was no member found for {argName}"); var memberType = member is FieldInfo memberFieldInfo ? memberFieldInfo.FieldType : ((PropertyInfo)member).PropertyType; if (memberType == typeof(RedisKey)) diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index a90023580..ebb66ec2a 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -94,7 +94,15 @@ public int Databases public bool IsConnecting => interactive?.IsConnecting == true; public bool IsConnected => interactive?.IsConnected == true; - public bool IsSubscriberConnected => subscription?.IsConnected == true; + public bool IsSubscriberConnected => KnowOrAssumeResp3() ? IsConnected : subscription?.IsConnected == true; + + public bool KnowOrAssumeResp3() + { + var protocol = interactive?.Protocol; + return protocol is not null + ? protocol.GetValueOrDefault() >= RedisProtocol.Resp3 // <= if we've completed handshake, use what we *know for sure* + : Multiplexer.RawConfig.TryResp3(); // otherwise, use what we *expect* + } public bool SupportsSubscriptions => Multiplexer.CommandMap.IsAvailable(RedisCommand.SUBSCRIBE); public bool SupportsPrimaryWrites => supportsPrimaryWrites ??= (!IsReplica || !ReplicaReadOnly || AllowReplicaWrites); @@ -159,7 +167,7 @@ internal Exception? LastException } internal State InteractiveConnectionState => interactive?.ConnectionState ?? State.Disconnected; - internal State SubscriptionConnectionState => subscription?.ConnectionState ?? State.Disconnected; + internal State SubscriptionConnectionState => KnowOrAssumeResp3() ? InteractiveConnectionState : subscription?.ConnectionState ?? State.Disconnected; public long OperationCount => interactive?.OperationCount ?? 0 + subscription?.OperationCount ?? 0; @@ -199,6 +207,11 @@ public Version Version set => SetConfig(ref version, value); } + /// + /// If we have a connection (interactive), report the protocol being used + /// + public RedisProtocol? Protocol => interactive?.Protocol; + public int WriteEverySeconds { get => writeEverySeconds; @@ -222,12 +235,16 @@ public void Dispose() public PhysicalBridge? GetBridge(ConnectionType type, bool create = true, ILogger? log = null) { if (isDisposed) return null; - return type switch + switch (type) { - ConnectionType.Interactive => interactive ?? (create ? interactive = CreateBridge(ConnectionType.Interactive, log) : null), - ConnectionType.Subscription => subscription ?? (create ? subscription = CreateBridge(ConnectionType.Subscription, log) : null), - _ => null, - }; + case ConnectionType.Interactive: + case ConnectionType.Subscription when KnowOrAssumeResp3(): + return interactive ?? (create ? interactive = CreateBridge(ConnectionType.Interactive, log) : null); + case ConnectionType.Subscription: + return subscription ?? (create ? subscription = CreateBridge(ConnectionType.Subscription, log) : null); + default: + return null; + } } public PhysicalBridge? GetBridge(Message message) @@ -237,6 +254,7 @@ public void Dispose() // Subscription commands go to a specific bridge - so we need to set that up. // There are other commands we need to send to the right connection (e.g. subscriber PING with an explicit SetForSubscriptionBridge call), // but these always go subscriber. + switch (message.Command) { case RedisCommand.SUBSCRIBE: @@ -247,7 +265,7 @@ public void Dispose() break; } - return message.IsForSubscriptionBridge + return (message.IsForSubscriptionBridge && !KnowOrAssumeResp3()) ? subscription ??= CreateBridge(ConnectionType.Subscription, null) : interactive ??= CreateBridge(ConnectionType.Interactive, null); } @@ -261,10 +279,13 @@ public void Dispose() case RedisCommand.UNSUBSCRIBE: case RedisCommand.PSUBSCRIBE: case RedisCommand.PUNSUBSCRIBE: - return subscription ?? (create ? subscription = CreateBridge(ConnectionType.Subscription, null) : null); - default: - return interactive ?? (create ? interactive = CreateBridge(ConnectionType.Interactive, null) : null); + if (!KnowOrAssumeResp3()) + { + return subscription ?? (create ? subscription = CreateBridge(ConnectionType.Subscription, null) : null); + } + break; } + return interactive ?? (create ? interactive = CreateBridge(ConnectionType.Interactive, null) : null); } public RedisFeatures GetFeatures() => new RedisFeatures(version); @@ -366,7 +387,7 @@ internal async Task AutoConfigureAsync(PhysicalConnection? connection, ILogger? var features = GetFeatures(); Message msg; - var autoConfigProcessor = new ResultProcessor.AutoConfigureProcessor(log); + var autoConfigProcessor = ResultProcessor.AutoConfigureProcessor.Create(log); if (commandMap.IsAvailable(RedisCommand.CONFIG)) { @@ -630,10 +651,13 @@ static async Task OnEstablishingAsyncAwaited(PhysicalConnection connection, Task try { if (connection == null) return Task.CompletedTask; + var handshake = HandshakeAsync(connection, log); if (handshake.Status != TaskStatus.RanToCompletion) + { return OnEstablishingAsyncAwaited(connection, handshake); + } } catch (Exception ex) { @@ -659,7 +683,7 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) // Since we're issuing commands inside a SetResult path in a message, we'd create a deadlock by waiting. Multiplexer.EnsureSubscriptions(CommandFlags.FireAndForget); } - if (IsConnected && (IsSubscriberConnected || !SupportsSubscriptions)) + if (IsConnected && (IsSubscriberConnected || !SupportsSubscriptions || KnowOrAssumeResp3())) { // Only connect on the second leg - we can accomplish this by checking both // Or the first leg, if we're only making 1 connection because subscriptions aren't supported @@ -695,7 +719,7 @@ internal bool CheckInfoReplication() lastInfoReplicationCheckTicks = Environment.TickCount; ResetExponentiallyReplicationCheck(); - if (version >= RedisFeatures.v2_8_0 && Multiplexer.CommandMap.IsAvailable(RedisCommand.INFO) + if (version.IsAtLeast(RedisFeatures.v2_8_0) && Multiplexer.CommandMap.IsAvailable(RedisCommand.INFO) && GetBridge(ConnectionType.Interactive, false) is PhysicalBridge bridge) { var msg = Message.Create(-1, CommandFlags.FireAndForget | CommandFlags.NoRedirect, RedisCommand.INFO, RedisLiterals.replication); @@ -902,14 +926,70 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) var config = Multiplexer.RawConfig; string? user = config.User; string password = config.Password ?? ""; - if (!string.IsNullOrWhiteSpace(user)) + + string clientName = Multiplexer.ClientName; + if (!string.IsNullOrWhiteSpace(clientName)) + { + clientName = nameSanitizer.Replace(clientName, ""); + } + + // NOTE: + // we might send the auth and client-name *twice* in RESP3 mode; this is intentional: + // - we don't know for sure which commands are available; HELLO is not always available, + // even on v6 servers, and we don't usually even know the server version yet; likewise, + // CLIENT could be disabled/renamed + // - on an authenticated server, you MUST issue HELLO with AUTH, so we can't avoid it there + // - but if the HELLO with AUTH isn't recognized, we might still need to auth; the following is + // legal in all scenarios, and results in a consistent state: + // + // (auth enabled) + // + // HELLO 3 AUTH {user} {password} SETNAME {client} + // AUTH {user} {password} + // CLIENT SETNAME {client} + // + // (auth disabled) + // + // HELLO 3 SETNAME {client} + // CLIENT SETNAME {client} + // + // this might look a little redundant, but: we only do it once per connection, and it isn't + // many bytes different; this allows us to pipeline the entire handshake without having to + // add latency + + // note on the use of FireAndForget here; in F+F, the result processor is still invoked, which + // is what we need for things to work; what *doesn't* happen is the result-box activation etc; + // that's fine and doesn't cause a problem; if we wanted we could probably just discard (`_ =`) + // the various tasks and just `return connection.FlushAsync();` - however, since handshake is low + // volume, we can afford to optimize for a good stack-trace rather than avoiding state machines. + + ResultProcessor? autoConfig = null; + if (Multiplexer.RawConfig.TryResp3()) // note this includes an availability check on HELLO + { + log?.LogInformation($"{Format.ToString(this)}: Authenticating via HELLO"); + var hello = Message.CreateHello(3, user, password, clientName, CommandFlags.FireAndForget); + hello.SetInternalCall(); + await WriteDirectOrQueueFireAndForgetAsync(connection, hello, autoConfig ??= ResultProcessor.AutoConfigureProcessor.Create(log)).ForAwait(); + + // note that the server can reject RESP3 via either an -ERR response (HELLO not understood), or by simply saying "nope", + // so we don't set the actual .Protocol until we process the result of the HELLO request + } + else + { + // if we're not even issuing HELLO, we're RESP2 + connection.SetProtocol(RedisProtocol.Resp2); + } + + // note: we auth EVEN IF we have used HELLO to AUTH; because otherwise the fallback/detection path is pure hell, + // and: we're pipelined here, so... meh + if (!string.IsNullOrWhiteSpace(user) && Multiplexer.CommandMap.IsAvailable(RedisCommand.AUTH)) { log?.LogInformation($"{Format.ToString(this)}: Authenticating (user/password)"); msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.AUTH, (RedisValue)user, (RedisValue)password); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } - else if (!string.IsNullOrWhiteSpace(password)) + else if (!string.IsNullOrWhiteSpace(password) && Multiplexer.CommandMap.IsAvailable(RedisCommand.AUTH)) { log?.LogInformation($"{Format.ToString(this)}: Authenticating (password)"); msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.AUTH, (RedisValue)password); @@ -919,18 +999,14 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) if (Multiplexer.CommandMap.IsAvailable(RedisCommand.CLIENT)) { - string name = Multiplexer.ClientName; - if (!string.IsNullOrWhiteSpace(name)) + if (!string.IsNullOrWhiteSpace(clientName)) { - name = nameSanitizer.Replace(name, ""); - if (!string.IsNullOrWhiteSpace(name)) - { - log?.LogInformation($"{Format.ToString(this)}: Setting client name: {name}"); - msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, RedisLiterals.SETNAME, (RedisValue)name); - msg.SetInternalCall(); - await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); - } + log?.LogInformation($"{Format.ToString(this)}: Setting client name: {clientName}"); + msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, RedisLiterals.SETNAME, (RedisValue)clientName); + msg.SetInternalCall(); + await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } + if (config.SetClientLibrary) { // note that this is a relatively new feature, but usually we won't know the @@ -961,13 +1037,14 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } } + msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, RedisLiterals.ID); msg.SetInternalCall(); - await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.ClientId).ForAwait(); + await WriteDirectOrQueueFireAndForgetAsync(connection, msg, autoConfig ??= ResultProcessor.AutoConfigureProcessor.Create(log)).ForAwait(); } var bridge = connection.BridgeCouldBeNull; - if (bridge == null) + if (bridge is null) { return; } diff --git a/tests/StackExchange.Redis.Tests/AggressiveTests.cs b/tests/StackExchange.Redis.Tests/AggressiveTests.cs index 2e8b23f5c..375e2da3d 100644 --- a/tests/StackExchange.Redis.Tests/AggressiveTests.cs +++ b/tests/StackExchange.Redis.Tests/AggressiveTests.cs @@ -234,7 +234,7 @@ private void TranRunIntegers(IDatabase db) Log($"tally: {count}"); } - private static void TranRunPings(IDatabase db) + private void TranRunPings(IDatabase db) { var key = Me(); db.KeyDelete(key); @@ -292,7 +292,7 @@ private async Task TranRunIntegersAsync(IDatabase db) Log($"tally: {count}"); } - private static async Task TranRunPingsAsync(IDatabase db) + private async Task TranRunPingsAsync(IDatabase db) { var key = Me(); db.KeyDelete(key); diff --git a/tests/StackExchange.Redis.Tests/AsyncTests.cs b/tests/StackExchange.Redis.Tests/AsyncTests.cs index 760cc4c94..e42e2f07d 100644 --- a/tests/StackExchange.Redis.Tests/AsyncTests.cs +++ b/tests/StackExchange.Redis.Tests/AsyncTests.cs @@ -45,7 +45,8 @@ public void AsyncTasksReportFailureIfServerUnavailable() [Fact] public async Task AsyncTimeoutIsNoticed() { - using var conn = Create(syncTimeout: 1000); + using var conn = Create(syncTimeout: 1000, asyncTimeout: 1000); + using var pauseConn = Create(); var opt = ConfigurationOptions.Parse(conn.Configuration); if (!Debugger.IsAttached) { // we max the timeouts if a debugger is detected @@ -59,11 +60,14 @@ public async Task AsyncTimeoutIsNoticed() Assert.Contains("; async timeouts: 0;", conn.GetStatus()); - await db.ExecuteAsync("client", "pause", 4000).ForAwait(); // client pause returns immediately + // This is done on another connection, because it queues a SELECT due to being an unknown command that will not timeout + // at the head of the queue + await pauseConn.GetDatabase().ExecuteAsync("client", "pause", 4000).ForAwait(); // client pause returns immediately var ms = Stopwatch.StartNew(); var ex = await Assert.ThrowsAsync(async () => { + Log("Issuing StringGetAsync"); await db.StringGetAsync(key).ForAwait(); // but *subsequent* operations are paused ms.Stop(); Log($"Unexpectedly succeeded after {ms.ElapsedMilliseconds}ms"); diff --git a/tests/StackExchange.Redis.Tests/BitTests.cs b/tests/StackExchange.Redis.Tests/BitTests.cs index 5dd3d05c2..1a870f37e 100644 --- a/tests/StackExchange.Redis.Tests/BitTests.cs +++ b/tests/StackExchange.Redis.Tests/BitTests.cs @@ -3,6 +3,7 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class BitTests : TestBase { diff --git a/tests/StackExchange.Redis.Tests/ClusterTests.cs b/tests/StackExchange.Redis.Tests/ClusterTests.cs index c945812a8..535b4a91a 100644 --- a/tests/StackExchange.Redis.Tests/ClusterTests.cs +++ b/tests/StackExchange.Redis.Tests/ClusterTests.cs @@ -11,9 +11,12 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] +[Collection(SharedConnectionFixture.Key)] public class ClusterTests : TestBase { - public ClusterTests(ITestOutputHelper output) : base (output) { } + public ClusterTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; [Fact] @@ -48,7 +51,7 @@ public void ConnectUsesSingleSocket() var srv = conn.GetServer(ep); var counters = srv.GetCounters(); Assert.Equal(1, counters.Interactive.SocketCount); - Assert.Equal(1, counters.Subscription.SocketCount); + Assert.Equal(Context.IsResp3 ? 0 : 1, counters.Subscription.SocketCount); } } } @@ -116,7 +119,7 @@ public void Connect() { Log(fail.ToString()); } - Assert.True(false, "not all servers connected"); + Assert.Fail("not all servers connected"); } Assert.Equal(TestConfig.Current.ClusterServerCount / 2, replicas); @@ -197,10 +200,9 @@ public void IntentionalWrongServer() [Fact] public void TransactionWithMultiServerKeys() { + using var conn = Create(); var ex = Assert.Throws(() => { - using var conn = Create(); - // connect var cluster = conn.GetDatabase(); var anyServer = conn.GetServer(conn.GetEndPoints()[0]); @@ -237,7 +239,7 @@ public void TransactionWithMultiServerKeys() _ = tran.StringSetAsync(y, "y-val"); tran.Execute(); - Assert.True(false, "Expected single-slot rules to apply"); + Assert.Fail("Expected single-slot rules to apply"); // the rest no longer applies while we are following single-slot rules //// check that everything was aborted @@ -255,10 +257,9 @@ public void TransactionWithMultiServerKeys() [Fact] public void TransactionWithSameServerKeys() { + using var conn = Create(); var ex = Assert.Throws(() => { - using var conn = Create(); - // connect var cluster = conn.GetDatabase(); var anyServer = conn.GetServer(conn.GetEndPoints()[0]); @@ -294,7 +295,7 @@ public void TransactionWithSameServerKeys() _ = tran.StringSetAsync(y, "y-val"); tran.Execute(); - Assert.True(false, "Expected single-slot rules to apply"); + Assert.Fail("Expected single-slot rules to apply"); // the rest no longer applies while we are following single-slot rules //// check that everything was aborted diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 9229228ab..84a8f916b 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -16,13 +16,15 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] +[Collection(SharedConnectionFixture.Key)] public class ConfigTests : TestBase { + public ConfigTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public Version DefaultVersion = new (3, 0, 0); public Version DefaultAzureVersion = new (4, 0, 0); - public ConfigTests(ITestOutputHelper output) : base(output) { } - [Fact] public void SslProtocols_SingleValue() { @@ -233,7 +235,7 @@ public void ClearSlowlog() [Fact] public void ClientName() { - using var conn = Create(clientName: "Test Rig", allowAdmin: true); + using var conn = Create(clientName: "Test Rig", allowAdmin: true, shared: false); Assert.Equal("Test Rig", conn.ClientName); @@ -247,7 +249,7 @@ public void ClientName() [Fact] public void DefaultClientName() { - using var conn = Create(allowAdmin: true, caller: null); // force default naming to kick in + using var conn = Create(allowAdmin: true, caller: "", shared: false); // force default naming to kick in Assert.Equal($"{Environment.MachineName}(SE.Redis-v{Utils.GetLibVersion()})", conn.ClientName); var db = conn.GetDatabase(); @@ -275,7 +277,10 @@ public void ConnectWithSubscribeDisabled() Assert.True(conn.IsConnected); var servers = conn.GetServerSnapshot(); Assert.True(servers[0].IsConnected); - Assert.False(servers[0].IsSubscriberConnected); + if (!Context.IsResp3) + { + Assert.False(servers[0].IsSubscriberConnected); + } var ex = Assert.Throws(() => conn.GetSubscriber().Subscribe(RedisChannel.Literal(Me()), (_, _) => GC.KeepAlive(this))); Assert.Equal("This operation has been disabled in the command-map and cannot be used: SUBSCRIBE", ex.Message); @@ -374,12 +379,32 @@ public void GetInfoRaw() public void GetClients() { var name = Guid.NewGuid().ToString(); - using var conn = Create(clientName: name, allowAdmin: true); + using var conn = Create(clientName: name, allowAdmin: true, shared: false); var server = GetAnyPrimary(conn); var clients = server.ClientList(); Assert.True(clients.Length > 0, "no clients"); // ourselves! Assert.True(clients.Any(x => x.Name == name), "expected: " + name); + + if (server.Features.ClientId) + { + var id = conn.GetConnectionId(server.EndPoint, ConnectionType.Interactive); + Assert.NotNull(id); + Assert.True(clients.Any(x => x.Id == id), "expected: " + id); + id = conn.GetConnectionId(server.EndPoint, ConnectionType.Subscription); + Assert.NotNull(id); + Assert.True(clients.Any(x => x.Id == id), "expected: " + id); + + var self = clients.First(x => x.Id == id); + if (server.Version.Major >= 7) + { + Assert.Equal(Context.Test.Protocol, self.Protocol); + } + else + { + Assert.Null(self.Protocol); + } + } } [Fact] diff --git a/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs b/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs index 8eeea36e9..6041bf12c 100644 --- a/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs @@ -47,7 +47,7 @@ public void DisabledCommandsStillConnectCluster(string disabledCommands) [Fact] public void TieBreakerIntact() { - using var conn = (Create(allowAdmin: true, log: Writer) as ConnectionMultiplexer)!; + using var conn = Create(allowAdmin: true, log: Writer); var tiebreaker = conn.GetDatabase().StringGet(conn.RawConfig.TieBreaker); Log($"Tiebreaker: {tiebreaker}"); @@ -61,7 +61,7 @@ public void TieBreakerIntact() [Fact] public void TieBreakerSkips() { - using var conn = (Create(allowAdmin: true, disabledCommands: new[] { "get" }, log: Writer) as ConnectionMultiplexer)!; + using var conn = Create(allowAdmin: true, disabledCommands: new[] { "get" }, log: Writer); Assert.Throws(() => conn.GetDatabase().StringGet(conn.RawConfig.TieBreaker)); foreach (var server in conn.GetServerSnapshot()) diff --git a/tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs b/tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs index 35767d753..a8bfe69b0 100644 --- a/tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs @@ -29,7 +29,7 @@ public async Task FailsWithinTimeout() await Task.Delay(10000).ForAwait(); } - Assert.True(false, "Connect should fail with RedisConnectionException exception"); + Assert.Fail("Connect should fail with RedisConnectionException exception"); } catch (RedisConnectionException) { diff --git a/tests/StackExchange.Redis.Tests/EnvoyTests.cs b/tests/StackExchange.Redis.Tests/EnvoyTests.cs index fac91496c..5015a660d 100644 --- a/tests/StackExchange.Redis.Tests/EnvoyTests.cs +++ b/tests/StackExchange.Redis.Tests/EnvoyTests.cs @@ -25,13 +25,13 @@ public void TestBasicEnvoyConnection() var db = conn.GetDatabase(); - const string key = "foobar"; + var key = Me() + "foobar"; const string value = "barfoo"; db.StringSet(key, value); var expectedVal = db.StringGet(key); - Assert.Equal(expectedVal, value); + Assert.Equal(value, expectedVal); } catch (TimeoutException ex) when (ex.Message == "Connect timeout" || sb.ToString().Contains("Returned, but incorrectly")) { diff --git a/tests/StackExchange.Redis.Tests/EventArgsTests.cs b/tests/StackExchange.Redis.Tests/EventArgsTests.cs index 27245f1ec..74b5e369a 100644 --- a/tests/StackExchange.Redis.Tests/EventArgsTests.cs +++ b/tests/StackExchange.Redis.Tests/EventArgsTests.cs @@ -27,25 +27,25 @@ HashSlotMovedEventArgs hashSlotMovedArgsMock DiagnosticStub stub = new DiagnosticStub(); stub.ConfigurationChangedBroadcastHandler(default, endpointArgsMock); - Assert.Equal(stub.Message,DiagnosticStub.ConfigurationChangedBroadcastHandlerMessage); + Assert.Equal(DiagnosticStub.ConfigurationChangedBroadcastHandlerMessage, stub.Message); stub.ErrorMessageHandler(default, redisErrorArgsMock); - Assert.Equal(stub.Message, DiagnosticStub.ErrorMessageHandlerMessage); + Assert.Equal(DiagnosticStub.ErrorMessageHandlerMessage, stub.Message); stub.ConnectionFailedHandler(default, connectionFailedArgsMock); - Assert.Equal(stub.Message, DiagnosticStub.ConnectionFailedHandlerMessage); + Assert.Equal(DiagnosticStub.ConnectionFailedHandlerMessage, stub.Message); stub.InternalErrorHandler(default, internalErrorArgsMock); - Assert.Equal(stub.Message, DiagnosticStub.InternalErrorHandlerMessage); + Assert.Equal(DiagnosticStub.InternalErrorHandlerMessage, stub.Message); stub.ConnectionRestoredHandler(default, connectionFailedArgsMock); - Assert.Equal(stub.Message, DiagnosticStub.ConnectionRestoredHandlerMessage); + Assert.Equal(DiagnosticStub.ConnectionRestoredHandlerMessage, stub.Message); stub.ConfigurationChangedHandler(default, endpointArgsMock); - Assert.Equal(stub.Message, DiagnosticStub.ConfigurationChangedHandlerMessage); + Assert.Equal(DiagnosticStub.ConfigurationChangedHandlerMessage, stub.Message); stub.HashSlotMovedHandler(default, hashSlotMovedArgsMock); - Assert.Equal(stub.Message, DiagnosticStub.HashSlotMovedHandlerMessage); + Assert.Equal(DiagnosticStub.HashSlotMovedHandlerMessage, stub.Message); } public class DiagnosticStub diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 864448610..1922c3edf 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -15,7 +15,7 @@ public void NullLastException() conn.GetDatabase(); Assert.Null(conn.GetServerSnapshot()[0].LastException); - var ex = ExceptionFactory.NoConnectionAvailable((conn as ConnectionMultiplexer)!, null, null); + var ex = ExceptionFactory.NoConnectionAvailable(conn.UnderlyingMultiplexer, null, null); Assert.Null(ex.InnerException); } @@ -42,7 +42,7 @@ public void MultipleEndpointsThrowConnectionException() conn.GetServer(endpoint).SimulateConnectionFailure(SimulatedFailureType.All); } - var ex = ExceptionFactory.NoConnectionAvailable((conn as ConnectionMultiplexer)!, null, null); + var ex = ExceptionFactory.NoConnectionAvailable(conn.UnderlyingMultiplexer, null, null); var outer = Assert.IsType(ex); Assert.Equal(ConnectionFailureType.UnableToResolvePhysicalConnection, outer.FailureType); var inner = Assert.IsType(outer.InnerException); @@ -68,7 +68,7 @@ public void ServerTakesPrecendenceOverSnapshot() conn.GetServer(conn.GetEndPoints()[0]).SimulateConnectionFailure(SimulatedFailureType.All); - var ex = ExceptionFactory.NoConnectionAvailable((conn as ConnectionMultiplexer)!, null, conn.GetServerSnapshot()[0]); + var ex = ExceptionFactory.NoConnectionAvailable(conn.UnderlyingMultiplexer, null, conn.GetServerSnapshot()[0]); Assert.IsType(ex); Assert.IsType(ex.InnerException); Assert.Equal(ex.InnerException, conn.GetServerSnapshot()[0].LastException); @@ -88,7 +88,7 @@ public void NullInnerExceptionForMultipleEndpointsWithNoLastException() conn.GetDatabase(); conn.AllowConnect = false; - var ex = ExceptionFactory.NoConnectionAvailable((conn as ConnectionMultiplexer)!, null, null); + var ex = ExceptionFactory.NoConnectionAvailable(conn.UnderlyingMultiplexer, null, null); Assert.IsType(ex); Assert.Null(ex.InnerException); } @@ -103,12 +103,12 @@ public void TimeoutException() { try { - using var conn = (Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false) as ConnectionMultiplexer)!; + using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false); var server = GetServer(conn); conn.AllowConnect = false; var msg = Message.Create(-1, CommandFlags.None, RedisCommand.PING); - var rawEx = ExceptionFactory.Timeout(conn, "Test Timeout", msg, new ServerEndPoint(conn, server.EndPoint)); + var rawEx = ExceptionFactory.Timeout(conn.UnderlyingMultiplexer, "Test Timeout", msg, new ServerEndPoint(conn.UnderlyingMultiplexer, server.EndPoint)); var ex = Assert.IsType(rawEx); Log("Exception: " + ex.Message); @@ -247,7 +247,7 @@ public void MessageFail(bool includeDetail, ConnectionFailureType failType, stri var resultBox = SimpleResultBox.Create(); message.SetSource(ResultProcessor.String, resultBox); - message.Fail(failType, null, "my annotation", conn as ConnectionMultiplexer); + message.Fail(failType, null, "my annotation", conn.UnderlyingMultiplexer); resultBox.GetResult(out var ex); Assert.NotNull(ex); diff --git a/tests/StackExchange.Redis.Tests/ExpiryTests.cs b/tests/StackExchange.Redis.Tests/ExpiryTests.cs index 305bab944..d69ab53d5 100644 --- a/tests/StackExchange.Redis.Tests/ExpiryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExpiryTests.cs @@ -149,7 +149,7 @@ public void KeyExpiryTime(bool disablePTimes) var time = db.KeyExpireTime(key); Assert.NotNull(time); - Assert.Equal(expireTime, time.Value, TimeSpan.FromSeconds(30)); + Assert.Equal(expireTime, time!.Value, TimeSpan.FromSeconds(30)); // Without associated expiration time db.KeyDelete(key, CommandFlags.FireAndForget); diff --git a/tests/StackExchange.Redis.Tests/FailoverTests.cs b/tests/StackExchange.Redis.Tests/FailoverTests.cs index aec2983e4..2449e6a4b 100644 --- a/tests/StackExchange.Redis.Tests/FailoverTests.cs +++ b/tests/StackExchange.Redis.Tests/FailoverTests.cs @@ -1,4 +1,5 @@ -using System; +#if NET6_0_OR_GREATER +using System; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -200,7 +201,7 @@ public async Task DereplicateGoesToPrimary() [Fact] public async Task SubscriptionsSurviveConnectionFailureAsync() { - using var conn = (Create(allowAdmin: true, shared: false, log: Writer, syncTimeout: 1000) as ConnectionMultiplexer)!; + using var conn = Create(allowAdmin: true, shared: false, log: Writer, syncTimeout: 1000); var profiler = conn.AddProfiler(); RedisChannel channel = RedisChannel.Literal(Me()); @@ -449,3 +450,4 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() } #endif } +#endif diff --git a/tests/StackExchange.Redis.Tests/GeoTests.cs b/tests/StackExchange.Redis.Tests/GeoTests.cs index 562bd1f5b..f1be0bad1 100644 --- a/tests/StackExchange.Redis.Tests/GeoTests.cs +++ b/tests/StackExchange.Redis.Tests/GeoTests.cs @@ -5,6 +5,7 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class GeoTests : TestBase { @@ -39,8 +40,8 @@ public void GeoAdd() // Validate var pos = db.GeoPosition(key, palermo.Member); Assert.NotNull(pos); - Assert.Equal(palermo.Longitude, pos.Value.Longitude, 5); - Assert.Equal(palermo.Latitude, pos.Value.Latitude, 5); + Assert.Equal(palermo.Longitude, pos!.Value.Longitude, 5); + Assert.Equal(palermo.Latitude, pos!.Value.Latitude, 5); } [Fact] @@ -141,18 +142,18 @@ public void GeoRadius() Assert.Equal(0, results[0].Distance); var position0 = results[0].Position; Assert.NotNull(position0); - Assert.Equal(Math.Round(position0.Value.Longitude, 5), Math.Round(cefalù.Position.Longitude, 5)); - Assert.Equal(Math.Round(position0.Value.Latitude, 5), Math.Round(cefalù.Position.Latitude, 5)); + Assert.Equal(Math.Round(position0!.Value.Longitude, 5), Math.Round(cefalù.Position.Longitude, 5)); + Assert.Equal(Math.Round(position0!.Value.Latitude, 5), Math.Round(cefalù.Position.Latitude, 5)); Assert.False(results[0].Hash.HasValue); Assert.Equal(results[1].Member, palermo.Member); var distance1 = results[1].Distance; Assert.NotNull(distance1); - Assert.Equal(Math.Round(36.5319, 6), Math.Round(distance1.Value, 6)); + Assert.Equal(Math.Round(36.5319, 6), Math.Round(distance1!.Value, 6)); var position1 = results[1].Position; Assert.NotNull(position1); - Assert.Equal(Math.Round(position1.Value.Longitude, 5), Math.Round(palermo.Position.Longitude, 5)); - Assert.Equal(Math.Round(position1.Value.Latitude, 5), Math.Round(palermo.Position.Latitude, 5)); + Assert.Equal(Math.Round(position1!.Value.Longitude, 5), Math.Round(palermo.Position.Longitude, 5)); + Assert.Equal(Math.Round(position1!.Value.Latitude, 5), Math.Round(palermo.Position.Latitude, 5)); Assert.False(results[1].Hash.HasValue); results = db.GeoRadius(key, cefalù.Member, 60, GeoUnit.Miles, 2, Order.Ascending, GeoRadiusOptions.None); diff --git a/tests/StackExchange.Redis.Tests/HashTests.cs b/tests/StackExchange.Redis.Tests/HashTests.cs index 3022779c4..34a2d12c1 100644 --- a/tests/StackExchange.Redis.Tests/HashTests.cs +++ b/tests/StackExchange.Redis.Tests/HashTests.cs @@ -11,6 +11,7 @@ namespace StackExchange.Redis.Tests; /// /// Tests for . /// +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class HashTests : TestBase { diff --git a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs index 2dce70904..6b72b659e 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs @@ -60,8 +60,21 @@ public class FactDiscoverer : Xunit.Sdk.FactDiscoverer { public FactDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) { } - protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) - => new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod); + public override IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + { + if (testMethod.Method.GetParameters().Any()) + { + return new[] { new ExecutionErrorTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, "[Fact] methods are not allowed to have parameters. Did you mean to use [Theory]?") }; + } + else if (testMethod.Method.IsGenericMethodDefinition) + { + return new[] { new ExecutionErrorTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, "[Fact] methods are not allowed to be generic.") }; + } + else + { + return testMethod.Expand(protocol => new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, protocol: protocol)); + } + } } public class TheoryDiscoverer : Xunit.Sdk.TheoryDiscoverer @@ -69,29 +82,53 @@ public class TheoryDiscoverer : Xunit.Sdk.TheoryDiscoverer public TheoryDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) { } protected override IEnumerable CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow) - => new[] { new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, dataRow) }; + => testMethod.Expand(protocol => new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, dataRow, protocol: protocol)); protected override IEnumerable CreateTestCasesForSkip(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, string skipReason) - => new[] { new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod) }; + => testMethod.Expand(protocol => new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, protocol: protocol)); protected override IEnumerable CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) - => new[] { new SkippableTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod) }; + => testMethod.Expand(protocol => new SkippableTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, protocol: protocol)); protected override IEnumerable CreateTestCasesForSkippedDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow, string skipReason) => new[] { new NamedSkippedDataRowTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, skipReason, dataRow) }; } -public class SkippableTestCase : XunitTestCase +public class SkippableTestCase : XunitTestCase, IRedisTest { + public RedisProtocol Protocol { get; set; } + public string ProtocolString => Protocol switch + { + RedisProtocol.Resp2 => "RESP2", + RedisProtocol.Resp3 => "RESP3", + _ => "UnknownProtocolFixMeeeeee" + }; + + protected override string GetUniqueID() => base.GetUniqueID() + ProtocolString; + protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => - base.GetDisplayName(factAttribute, displayName).StripName(); + base.GetDisplayName(factAttribute, displayName).StripName() + "(" + ProtocolString + ")"; [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] public SkippableTestCase() { } - public SkippableTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[]? testMethodArguments = null) + public SkippableTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[]? testMethodArguments = null, RedisProtocol? protocol = null) : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) { + // TODO: Default RESP2 somewhere cleaner + Protocol = protocol ?? RedisProtocol.Resp2; + } + + public override void Serialize(IXunitSerializationInfo data) + { + data.AddValue(nameof(Protocol), (int)Protocol); + base.Serialize(data); + } + + public override void Deserialize(IXunitSerializationInfo data) + { + Protocol = (RedisProtocol)data.GetValue(nameof(Protocol)); + base.Deserialize(data); } public override async Task RunAsync( @@ -102,21 +139,28 @@ public override async Task RunAsync( CancellationTokenSource cancellationTokenSource) { var skipMessageBus = new SkippableMessageBus(messageBus); + TestBase.SetContext(new TestContext(this)); var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource).ForAwait(); return result.Update(skipMessageBus); } } -public class SkippableTheoryTestCase : XunitTheoryTestCase +public class SkippableTheoryTestCase : XunitTheoryTestCase, IRedisTest { + public RedisProtocol Protocol { get; set; } + protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => base.GetDisplayName(factAttribute, displayName).StripName(); [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] public SkippableTheoryTestCase() { } - public SkippableTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod) - : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) { } + public SkippableTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, RedisProtocol? protocol = null) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) + { + // TODO: Default RESP2 somewhere cleaner + Protocol = protocol ?? RedisProtocol.Resp2; + } public override async Task RunAsync( IMessageSink diagnosticMessageSink, @@ -126,11 +170,21 @@ public override async Task RunAsync( CancellationTokenSource cancellationTokenSource) { var skipMessageBus = new SkippableMessageBus(messageBus); + TestBase.SetContext(new TestContext(this)); var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource).ForAwait(); return result.Update(skipMessageBus); } } +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] +public class RunPerProtocol : Attribute +{ + public static RedisProtocol[] AllProtocols { get; } = new[] { RedisProtocol.Resp2, RedisProtocol.Resp3 }; + + public RedisProtocol[] Protocols { get; } + public RunPerProtocol(params RedisProtocol[] procotols) => Protocols = procotols ?? AllProtocols; +} + public class NamedSkippedDataRowTestCase : XunitSkippedDataRowTestCase { protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => @@ -185,6 +239,30 @@ public static RunSummary Update(this RunSummary summary, SkippableMessageBus bus } return summary; } + + public static IEnumerable Expand(this ITestMethod testMethod, Func generator) + { + if ((testMethod.Method.GetCustomAttributes(typeof(RunPerProtocol)).FirstOrDefault() + ?? testMethod.TestClass.Class.GetCustomAttributes(typeof(RunPerProtocol)).FirstOrDefault()) is IAttributeInfo attr) + { + // params means not null but default empty + var protocols = attr.GetNamedArgument(nameof(RunPerProtocol.Protocols)); + if (protocols.Length == 0) + { + protocols = RunPerProtocol.AllProtocols; + } + var results = new List(); + foreach (var protocol in protocols) + { + results.Add(generator(protocol)); + } + return results; + } + else + { + return new[] { generator(RedisProtocol.Resp2) }; + } + } } /// diff --git a/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs b/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs index 25fd219ad..1d5f8f91c 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs @@ -17,11 +17,11 @@ static Extensions() #endif try { - VersionInfo += "\n Running on: " + RuntimeInformation.OSDescription; + VersionInfo += "\n Running on: " + RuntimeInformation.OSDescription; } catch (Exception) { - VersionInfo += "\n Failed to get OS version"; + VersionInfo += "\n Failed to get OS version"; } } diff --git a/tests/StackExchange.Redis.Tests/Helpers/IRedisTest.cs b/tests/StackExchange.Redis.Tests/Helpers/IRedisTest.cs new file mode 100644 index 000000000..76ea5bc1b --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Helpers/IRedisTest.cs @@ -0,0 +1,8 @@ +using Xunit.Sdk; + +namespace StackExchange.Redis.Tests; + +public interface IRedisTest : IXunitTestCase +{ + public RedisProtocol Protocol { get; set; } +} diff --git a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs index f61e73e32..c88c0ec4d 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using StackExchange.Redis.Maintenance; @@ -17,7 +19,6 @@ public class SharedConnectionFixture : IDisposable public const string Key = "Shared Muxer"; private readonly ConnectionMultiplexer _actualConnection; - internal IInternalConnectionMultiplexer Connection { get; } public string Configuration { get; } public SharedConnectionFixture() @@ -32,12 +33,45 @@ public SharedConnectionFixture() ); _actualConnection.InternalError += OnInternalError; _actualConnection.ConnectionFailed += OnConnectionFailed; + } + + private NonDisposingConnection? resp2, resp3; + internal IInternalConnectionMultiplexer GetConnection(TestBase obj, RedisProtocol protocol, [CallerMemberName] string caller = "") + { + Version? require = protocol == RedisProtocol.Resp3 ? RedisFeatures.v6_0_0 : null; + lock (this) + { + ref NonDisposingConnection? field = ref protocol == RedisProtocol.Resp3 ? ref resp3 : ref resp2; + if (field is { IsConnected: false }) + { // abandon memoized connection if disconnected + var muxer = field.UnderlyingMultiplexer; + field = null; + muxer.Dispose(); + } + return field ??= VerifyAndWrap(obj.Create(protocol: protocol, require: require, caller: caller, shared: false, allowAdmin: true), protocol); + } - Connection = new NonDisposingConnection(_actualConnection); + static NonDisposingConnection VerifyAndWrap(IInternalConnectionMultiplexer muxer, RedisProtocol protocol) + { + var ep = muxer.GetEndPoints().FirstOrDefault(); + Assert.NotNull(ep); + var server = muxer.GetServer(ep); + server.Ping(); + var sep = muxer.GetServerEndPoint(ep); + if (sep.Protocol is null) + { + throw new InvalidOperationException("No RESP protocol; this means no connection?"); + } + Assert.Equal(protocol, sep.Protocol); + Assert.Equal(protocol, server.Protocol); + return new NonDisposingConnection(muxer); + } } - private class NonDisposingConnection : IInternalConnectionMultiplexer + internal sealed class NonDisposingConnection : IInternalConnectionMultiplexer { + public IInternalConnectionMultiplexer UnderlyingConnection => _inner; + public bool AllowConnect { get => _inner.AllowConnect; @@ -50,11 +84,20 @@ public bool IgnoreConnect set => _inner.IgnoreConnect = value; } + public ServerSelectionStrategy ServerSelectionStrategy => _inner.ServerSelectionStrategy; + + public ServerEndPoint GetServerEndPoint(EndPoint endpoint) => _inner.GetServerEndPoint(endpoint); + public ReadOnlySpan GetServerSnapshot() => _inner.GetServerSnapshot(); + public ConnectionMultiplexer UnderlyingMultiplexer => _inner.UnderlyingMultiplexer; + private readonly IInternalConnectionMultiplexer _inner; public NonDisposingConnection(IInternalConnectionMultiplexer inner) => _inner = inner; + public int GetSubscriptionsCount() => _inner.GetSubscriptionsCount(); + public ConcurrentDictionary GetSubscriptions() => _inner.GetSubscriptions(); + public string ClientName => _inner.ClientName; public string Configuration => _inner.Configuration; @@ -189,7 +232,8 @@ public void ExportConfiguration(Stream destination, ExportOptions options = Expo public void Dispose() { - _actualConnection.Dispose(); + resp2?.UnderlyingConnection?.Dispose(); + resp3?.UnderlyingConnection?.Dispose(); GC.SuppressFinalize(this); } diff --git a/tests/StackExchange.Redis.Tests/Helpers/TestContext.cs b/tests/StackExchange.Redis.Tests/Helpers/TestContext.cs new file mode 100644 index 000000000..799f753b4 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Helpers/TestContext.cs @@ -0,0 +1,20 @@ +namespace StackExchange.Redis.Tests; + +public class TestContext +{ + public IRedisTest Test { get; set; } + + public bool IsResp2 => Test.Protocol == RedisProtocol.Resp2; + public bool IsResp3 => Test.Protocol == RedisProtocol.Resp3; + + public string KeySuffix => Test.Protocol switch + { + RedisProtocol.Resp2 => "R2", + RedisProtocol.Resp3 => "R3", + _ => "", + }; + + public TestContext(IRedisTest test) => Test = test; + + public override string ToString() => $"Protocol: {Test.Protocol}"; +} diff --git a/tests/StackExchange.Redis.Tests/HyperLogLogTests.cs b/tests/StackExchange.Redis.Tests/HyperLogLogTests.cs index d110c86b6..e0451e9c5 100644 --- a/tests/StackExchange.Redis.Tests/HyperLogLogTests.cs +++ b/tests/StackExchange.Redis.Tests/HyperLogLogTests.cs @@ -3,10 +3,11 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class HyperLogLogTests : TestBase { - public HyperLogLogTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public HyperLogLogTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public void SingleKeyLength() diff --git a/tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs b/tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs index 0c54c40ff..a7bcfc737 100644 --- a/tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs @@ -13,7 +13,7 @@ public BgSaveResponseTests(ITestOutputHelper output) : base (output) { } [InlineData(SaveType.BackgroundRewriteAppendOnlyFile)] public async Task ShouldntThrowException(SaveType saveType) { - using var conn = Create(null, null, true); + using var conn = Create(allowAdmin: true); var Server = GetServer(conn); Server.Save(saveType); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO10504853Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO10504853Tests.cs index abf5cc3cc..ee2fd9bbc 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO10504853Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO10504853Tests.cs @@ -75,7 +75,7 @@ public void ExecuteWithNonHashStartingPoint() try { db.Wait(taskResult); - Assert.True(false, "Should throw a WRONGTYPE"); + Assert.Fail("Should throw a WRONGTYPE"); } catch (AggregateException ex) { diff --git a/tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs index 18163b23c..f7bde6c4a 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs @@ -21,7 +21,7 @@ public async Task Execute() } } - private static async Task DoStuff(ConnectionMultiplexer conn) + private async Task DoStuff(ConnectionMultiplexer conn) { var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/KeyTests.cs b/tests/StackExchange.Redis.Tests/KeyTests.cs index 062f2caaa..b0c028d56 100644 --- a/tests/StackExchange.Redis.Tests/KeyTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyTests.cs @@ -9,17 +9,18 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class KeyTests : TestBase { - public KeyTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public KeyTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public void TestScan() { using var conn = Create(allowAdmin: true); - var dbId = TestConfig.GetDedicatedDB(); + var dbId = TestConfig.GetDedicatedDB(conn); var db = conn.GetDatabase(dbId); var server = GetAnyPrimary(conn); var prefix = Me(); diff --git a/tests/StackExchange.Redis.Tests/ListTests.cs b/tests/StackExchange.Redis.Tests/ListTests.cs index bb212db14..5fdb5d60a 100644 --- a/tests/StackExchange.Redis.Tests/ListTests.cs +++ b/tests/StackExchange.Redis.Tests/ListTests.cs @@ -6,6 +6,7 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class ListTests : TestBase { diff --git a/tests/StackExchange.Redis.Tests/LockingTests.cs b/tests/StackExchange.Redis.Tests/LockingTests.cs index 1d9c8742e..a2ce6986d 100644 --- a/tests/StackExchange.Redis.Tests/LockingTests.cs +++ b/tests/StackExchange.Redis.Tests/LockingTests.cs @@ -75,7 +75,7 @@ public void TestOpCountByVersionLocal_UpLevel() TestLockOpCountByVersion(conn, 1, true); } - private static void TestLockOpCountByVersion(IConnectionMultiplexer conn, int expectedOps, bool existFirst) + private void TestLockOpCountByVersion(IConnectionMultiplexer conn, int expectedOps, bool existFirst) { const int LockDuration = 30; RedisKey Key = Me(); diff --git a/tests/StackExchange.Redis.Tests/MemoryTests.cs b/tests/StackExchange.Redis.Tests/MemoryTests.cs index 21325e0f2..50812e597 100644 --- a/tests/StackExchange.Redis.Tests/MemoryTests.cs +++ b/tests/StackExchange.Redis.Tests/MemoryTests.cs @@ -58,20 +58,20 @@ public async Task GetStats() var server = conn.GetServer(conn.GetEndPoints()[0]); var stats = server.MemoryStats(); Assert.NotNull(stats); - Assert.Equal(ResultType.MultiBulk, stats.Type); + Assert.Equal(ResultType.Array, stats.Resp2Type); var parsed = stats.ToDictionary(); var alloc = parsed["total.allocated"]; - Assert.Equal(ResultType.Integer, alloc.Type); + Assert.Equal(ResultType.Integer, alloc.Resp2Type); Assert.True(alloc.AsInt64() > 0); stats = await server.MemoryStatsAsync(); Assert.NotNull(stats); - Assert.Equal(ResultType.MultiBulk, stats.Type); + Assert.Equal(ResultType.Array, stats.Resp2Type); alloc = parsed["total.allocated"]; - Assert.Equal(ResultType.Integer, alloc.Type); + Assert.Equal(ResultType.Integer, alloc.Resp2Type); Assert.True(alloc.AsInt64() > 0); } } diff --git a/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs b/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs index 91abbf5e9..e0b09d545 100644 --- a/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs +++ b/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs @@ -60,6 +60,7 @@ public async Task KeyExpire() await db.KeyExpireAsync(key, expireTime, when: when, flags: flags); } + [Fact] public async Task StringBitCount() { using var conn = Create(require: RedisFeatures.v2_6_0); diff --git a/tests/StackExchange.Redis.Tests/ParseTests.cs b/tests/StackExchange.Redis.Tests/ParseTests.cs index a7c4248aa..80e1fbbef 100644 --- a/tests/StackExchange.Redis.Tests/ParseTests.cs +++ b/tests/StackExchange.Redis.Tests/ParseTests.cs @@ -74,7 +74,7 @@ private void ProcessMessages(Arena arena, ReadOnlySequence buff var reader = new BufferReader(buffer); RawResult result; int found = 0; - while (!(result = PhysicalConnection.TryParseResult(arena, buffer, ref reader, false, null, false)).IsNull) + while (!(result = PhysicalConnection.TryParseResult(false, arena, buffer, ref reader, false, null, false)).IsNull) { Log($"{result} - {result.GetString()}"); found++; diff --git a/tests/StackExchange.Redis.Tests/ProfilingTests.cs b/tests/StackExchange.Redis.Tests/ProfilingTests.cs index 41aef9337..7c4dcbe59 100644 --- a/tests/StackExchange.Redis.Tests/ProfilingTests.cs +++ b/tests/StackExchange.Redis.Tests/ProfilingTests.cs @@ -29,7 +29,7 @@ public void Simple() conn.RegisterProfiler(() => session); - var dbId = TestConfig.GetDedicatedDB(); + var dbId = TestConfig.GetDedicatedDB(conn); var db = conn.GetDatabase(dbId); db.StringSet(key, "world"); var result = db.ScriptEvaluate(script, new { key = (RedisKey)key }); @@ -143,7 +143,7 @@ public void ManyThreads() Assert.Contains("SET", kinds); if (kinds.Count == 2 && !kinds.Contains("SELECT") && !kinds.Contains("GET")) { - Assert.True(false, "Non-SET, Non-SELECT, Non-GET command seen"); + Assert.Fail("Non-SET, Non-SELECT, Non-GET command seen"); } Assert.Equal(16 * CountPer, relevant.Count); diff --git a/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs b/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs index dd77dc106..e689c980b 100644 --- a/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs @@ -7,10 +7,11 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class PubSubCommandTests : TestBase { - public PubSubCommandTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public PubSubCommandTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public void SubscriberCount() diff --git a/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs b/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs index dcf706e76..aa363984f 100644 --- a/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs @@ -6,16 +6,18 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class PubSubMultiserverTests : TestBase { public PubSubMultiserverTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; [Fact] public void ChannelSharding() { - using var conn = (Create(channelPrefix: Me()) as ConnectionMultiplexer)!; + using var conn = Create(channelPrefix: Me()); var defaultSlot = conn.ServerSelectionStrategy.HashSlot(default(RedisChannel)); var slot1 = conn.ServerSelectionStrategy.HashSlot(RedisChannel.Literal("hey")); @@ -31,7 +33,7 @@ public async Task ClusterNodeSubscriptionFailover() { Log("Connecting..."); - using var conn = (Create(allowAdmin: true) as ConnectionMultiplexer)!; + using var conn = Create(allowAdmin: true); var sub = conn.GetSubscriber(); var channel = RedisChannel.Literal(Me()); @@ -54,7 +56,7 @@ await sub.SubscribeAsync(channel, (_, val) => Log($" Published (1) to {publishedTo} subscriber(s)."); Assert.Equal(1, publishedTo); - var endpoint = sub.SubscribedEndpoint(channel); + var endpoint = sub.SubscribedEndpoint(channel)!; var subscribedServer = conn.GetServer(endpoint); var subscribedServerEndpoint = conn.GetServerEndPoint(endpoint); @@ -63,18 +65,27 @@ await sub.SubscribeAsync(channel, (_, val) => Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); Assert.True(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); - Assert.True(conn.TryGetSubscription(channel, out var subscription)); + Assert.True(conn.GetSubscriptions().TryGetValue(channel, out var subscription)); var initialServer = subscription.GetCurrentServer(); Assert.NotNull(initialServer); Assert.True(initialServer.IsConnected); Log("Connected to: " + initialServer); conn.AllowConnect = false; - subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.AllSubscription); + if (Context.IsResp3) + { + subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.All); - Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); - Assert.False(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + Assert.False(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); + Assert.False(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + } + else + { + subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.AllSubscription); + Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); + Assert.False(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + } await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); Assert.True(subscription.IsConnected); @@ -105,7 +116,7 @@ public async Task PrimaryReplicaSubscriptionFailover(CommandFlags flags, bool ex var config = TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; Log("Connecting..."); - using var conn = (Create(configuration: config, shared: false, allowAdmin: true) as ConnectionMultiplexer)!; + using var conn = Create(configuration: config, shared: false, allowAdmin: true); var sub = conn.GetSubscriber(); var channel = RedisChannel.Literal(Me() + flags.ToString()); // Individual channel per case to not overlap publishers @@ -127,7 +138,7 @@ await sub.SubscribeAsync(channel, (_, val) => Assert.Equal(1, count); Log($" Published (1) to {publishedTo} subscriber(s)."); - var endpoint = sub.SubscribedEndpoint(channel); + var endpoint = sub.SubscribedEndpoint(channel)!; var subscribedServer = conn.GetServer(endpoint); var subscribedServerEndpoint = conn.GetServerEndPoint(endpoint); @@ -136,17 +147,25 @@ await sub.SubscribeAsync(channel, (_, val) => Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); Assert.True(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); - Assert.True(conn.TryGetSubscription(channel, out var subscription)); + Assert.True(conn.GetSubscriptions().TryGetValue(channel, out var subscription)); var initialServer = subscription.GetCurrentServer(); Assert.NotNull(initialServer); Assert.True(initialServer.IsConnected); Log("Connected to: " + initialServer); conn.AllowConnect = false; - subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.AllSubscription); - - Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); - Assert.False(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + if (Context.IsResp3) + { + subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.All); // need to kill the main connection + Assert.False(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); + Assert.False(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + } + else + { + subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.AllSubscription); + Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); + Assert.False(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); + } if (expectSuccess) { diff --git a/tests/StackExchange.Redis.Tests/PubSubTests.cs b/tests/StackExchange.Redis.Tests/PubSubTests.cs index 833d888e9..697bf2771 100644 --- a/tests/StackExchange.Redis.Tests/PubSubTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubTests.cs @@ -12,6 +12,7 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class PubSubTests : TestBase { @@ -376,7 +377,7 @@ public async Task PubSubGetAllAnyOrder() const int count = 1000; var syncLock = new object(); - Assert.True(sub.IsConnected()); + Assert.True(sub.IsConnected(), nameof(sub.IsConnected)); var data = new HashSet(); await sub.SubscribeAsync(channel, (_, val) => { diff --git a/tests/StackExchange.Redis.Tests/RawResultTests.cs b/tests/StackExchange.Redis.Tests/RawResultTests.cs index 895ec4ec1..9cf578ee1 100644 --- a/tests/StackExchange.Redis.Tests/RawResultTests.cs +++ b/tests/StackExchange.Redis.Tests/RawResultTests.cs @@ -12,11 +12,14 @@ public void TypeLoads() Assert.Equal(nameof(RawResult), type.Name); } - [Fact] - public void NullWorks() + [Theory] + [InlineData(ResultType.BulkString)] + [InlineData(ResultType.Null)] + public void NullWorks(ResultType type) { - var result = new RawResult(ResultType.BulkString, ReadOnlySequence.Empty, true); - Assert.Equal(ResultType.BulkString, result.Type); + var result = new RawResult(type, ReadOnlySequence.Empty, RawResult.ResultFlags.None); + Assert.Equal(type, result.Resp3Type); + Assert.True(result.HasValue); Assert.True(result.IsNull); var value = result.AsRedisValue(); @@ -32,8 +35,9 @@ public void NullWorks() [Fact] public void DefaultWorks() { - var result = default(RawResult); - Assert.Equal(ResultType.None, result.Type); + var result = RawResult.Nil; + Assert.Equal(ResultType.None, result.Resp3Type); + Assert.False(result.HasValue); Assert.True(result.IsNull); var value = result.AsRedisValue(); @@ -50,7 +54,7 @@ public void DefaultWorks() public void NilWorks() { var result = RawResult.Nil; - Assert.Equal(ResultType.None, result.Type); + Assert.Equal(ResultType.None, result.Resp3Type); Assert.True(result.IsNull); var value = result.AsRedisValue(); diff --git a/tests/StackExchange.Redis.Tests/RespProtocolTests.cs b/tests/StackExchange.Redis.Tests/RespProtocolTests.cs new file mode 100644 index 000000000..6c444e43c --- /dev/null +++ b/tests/StackExchange.Redis.Tests/RespProtocolTests.cs @@ -0,0 +1,431 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests; + +[Collection(SharedConnectionFixture.Key)] +public sealed class RespProtocolTests : TestBase +{ + public RespProtocolTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + [RunPerProtocol] + public async Task ConnectWithTiming() + { + using var conn = Create(shared: false, log: Writer); + await conn.GetDatabase().PingAsync(); + } + + [Theory] + // specify nothing + [InlineData("someserver", false)] + // specify *just* the protocol; sure, we'll believe you + [InlineData("someserver,protocol=resp3", true)] + [InlineData("someserver,protocol=resp3,$HELLO=", false)] + [InlineData("someserver,protocol=resp3,$HELLO=BONJOUR", true)] + [InlineData("someserver,protocol=3", true, "resp3")] + [InlineData("someserver,protocol=3,$HELLO=", false, "resp3")] + [InlineData("someserver,protocol=3,$HELLO=BONJOUR", true, "resp3")] + [InlineData("someserver,protocol=2", false, "resp2")] + [InlineData("someserver,protocol=2,$HELLO=", false, "resp2")] + [InlineData("someserver,protocol=2,$HELLO=BONJOUR", false, "resp2")] + // specify a pre-6 version - only used if protocol specified + [InlineData("someserver,version=5.9", false)] + [InlineData("someserver,version=5.9,$HELLO=", false)] + [InlineData("someserver,version=5.9,$HELLO=BONJOUR", false)] + [InlineData("someserver,version=5.9,protocol=resp3", true)] + [InlineData("someserver,version=5.9,protocol=resp3,$HELLO=", false)] + [InlineData("someserver,version=5.9,protocol=resp3,$HELLO=BONJOUR", true)] + [InlineData("someserver,version=5.9,protocol=3", true, "resp3")] + [InlineData("someserver,version=5.9,protocol=3,$HELLO=", false, "resp3")] + [InlineData("someserver,version=5.9,protocol=3,$HELLO=BONJOUR", true, "resp3")] + [InlineData("someserver,version=5.9,protocol=2", false, "resp2")] + [InlineData("someserver,version=5.9,protocol=2,$HELLO=", false, "resp2")] + [InlineData("someserver,version=5.9,protocol=2,$HELLO=BONJOUR", false, "resp2")] + // specify a post-6 version; attempt by default + [InlineData("someserver,version=6.0", false)] + [InlineData("someserver,version=6.0,$HELLO=", false)] + [InlineData("someserver,version=6.0,$HELLO=BONJOUR", false)] + [InlineData("someserver,version=6.0,protocol=resp3", true)] + [InlineData("someserver,version=6.0,protocol=resp3,$HELLO=", false)] + [InlineData("someserver,version=6.0,protocol=resp3,$HELLO=BONJOUR", true)] + [InlineData("someserver,version=6.0,protocol=3", true, "resp3")] + [InlineData("someserver,version=6.0,protocol=3,$HELLO=", false, "resp3")] + [InlineData("someserver,version=6.0,protocol=3,$HELLO=BONJOUR", true, "resp3")] + [InlineData("someserver,version=6.0,protocol=2", false, "resp2")] + [InlineData("someserver,version=6.0,protocol=2,$HELLO=", false, "resp2")] + [InlineData("someserver,version=6.0,protocol=2,$HELLO=BONJOUR", false, "resp2")] + [InlineData("someserver,version=7.2", false)] + [InlineData("someserver,version=7.2,$HELLO=", false)] + [InlineData("someserver,version=7.2,$HELLO=BONJOUR", false)] + public void ParseFormatConfigOptions(string configurationString, bool tryResp3, string? formatProtocol = null) + { + var config = ConfigurationOptions.Parse(configurationString); + + string expectedConfigurationString = formatProtocol is null ? configurationString : Regex.Replace(configurationString, "(?<=protocol=)[^,]+", formatProtocol); + + Assert.Equal(expectedConfigurationString, config.ToString(true)); // check round-trip + Assert.Equal(expectedConfigurationString, config.Clone().ToString(true)); // check clone + Assert.Equal(tryResp3, config.TryResp3()); + } + + [Fact] + [RunPerProtocol] + public async Task TryConnect() + { + var muxer = Create(shared: false); + await muxer.GetDatabase().PingAsync(); + + var server = muxer.GetServerEndPoint(muxer.GetEndPoints().Single()); + if (Context.IsResp3 && !server.GetFeatures().Resp3) + { + Skip.Inconclusive("server does not support RESP3"); + } + if (Context.IsResp3) + { + Assert.Equal(RedisProtocol.Resp3, server.Protocol); + } + else + { + Assert.Equal(RedisProtocol.Resp2, server.Protocol); + } + var cid = server.GetBridge(RedisCommand.GET)?.ConnectionId; + if (server.GetFeatures().ClientId) + { + Assert.NotNull(cid); + } + else + { + Assert.Null(cid); + } + } + + [Theory] + [InlineData("HELLO", true)] + [InlineData("BONJOUR", false)] + public async Task ConnectWithBrokenHello(string command, bool isResp3) + { + var config = ConfigurationOptions.Parse(TestConfig.Current.SecureServerAndPort); + config.Password = TestConfig.Current.SecurePassword; + config.Protocol = RedisProtocol.Resp3; + config.CommandMap = CommandMap.Create(new() { ["hello"] = command }); + + using var muxer = await ConnectionMultiplexer.ConnectAsync(config, Writer); + await muxer.GetDatabase().PingAsync(); // is connected + var ep = muxer.GetServerEndPoint(muxer.GetEndPoints()[0]); + if (!ep.GetFeatures().Resp3) // this is just a v6 check + { + isResp3 = false; // then, no: it won't be + } + Assert.Equal(isResp3 ? RedisProtocol.Resp3 : RedisProtocol.Resp2, ep.Protocol); + var result = await muxer.GetDatabase().ExecuteAsync("latency", "doctor"); + Assert.Equal(isResp3 ? ResultType.VerbatimString : ResultType.BulkString, result.Resp3Type); + } + + [Theory] + [InlineData("return 42", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, 42)] + [InlineData("return 'abc'", RedisProtocol.Resp2, ResultType.BulkString, ResultType.BulkString, "abc")] + [InlineData(@"return {1,2,3}", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, ARR_123)] + [InlineData("return nil", RedisProtocol.Resp2, ResultType.BulkString, ResultType.Null, null)] + [InlineData(@"return redis.pcall('hgetall', 'key')", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, MAP_ABC)] + [InlineData(@"redis.setresp(3) +return redis.pcall('hgetall', 'key')", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, MAP_ABC)] + [InlineData("return true", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, 1)] + [InlineData("return false", RedisProtocol.Resp2, ResultType.BulkString, ResultType.Null, null)] + [InlineData("redis.setresp(3) return true", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, 1)] + [InlineData("redis.setresp(3) return false", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, 0)] + + [InlineData("return { map = { a = 1, b = 2, c = 3 } }", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, MAP_ABC, 6)] + [InlineData("return { set = { a = 1, b = 2, c = 3 } }", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, SET_ABC, 6)] + [InlineData("return { double = 42 }", RedisProtocol.Resp2, ResultType.BulkString, ResultType.BulkString, 42.0, 6)] + + [InlineData("return 42", RedisProtocol.Resp3, ResultType.Integer, ResultType.Integer, 42)] + [InlineData("return 'abc'", RedisProtocol.Resp3, ResultType.BulkString, ResultType.BulkString, "abc")] + [InlineData("return {1,2,3}", RedisProtocol.Resp3, ResultType.Array, ResultType.Array, ARR_123)] + [InlineData("return nil", RedisProtocol.Resp3, ResultType.BulkString, ResultType.Null, null)] + [InlineData(@"return redis.pcall('hgetall', 'key')", RedisProtocol.Resp3, ResultType.Array, ResultType.Array, MAP_ABC)] + [InlineData(@"redis.setresp(3) +return redis.pcall('hgetall', 'key')", RedisProtocol.Resp3, ResultType.Array, ResultType.Map, MAP_ABC)] + [InlineData("return true", RedisProtocol.Resp3, ResultType.Integer, ResultType.Integer, 1)] + [InlineData("return false", RedisProtocol.Resp3, ResultType.BulkString, ResultType.Null, null)] + [InlineData("redis.setresp(3) return true", RedisProtocol.Resp3, ResultType.Integer, ResultType.Boolean, true)] + [InlineData("redis.setresp(3) return false", RedisProtocol.Resp3, ResultType.Integer, ResultType.Boolean, false)] + + [InlineData("return { map = { a = 1, b = 2, c = 3 } }", RedisProtocol.Resp3, ResultType.Array, ResultType.Map, MAP_ABC, 6)] + [InlineData("return { set = { a = 1, b = 2, c = 3 } }", RedisProtocol.Resp3, ResultType.Array, ResultType.Set, SET_ABC, 6)] + [InlineData("return { double = 42 }", RedisProtocol.Resp3, ResultType.SimpleString, ResultType.Double, 42.0, 6)] + public async Task CheckLuaResult(string script, RedisProtocol protocol, ResultType resp2, ResultType resp3, object expected, int serverMin = 1) + { + // note Lua does not appear to return RESP3 types in any scenarios + var muxer = Create(protocol: protocol); + var ep = muxer.GetServerEndPoint(muxer.GetEndPoints().Single()); + if (serverMin > ep.Version.Major) + { + Skip.Inconclusive($"applies to v{serverMin} onwards - detected v{ep.Version.Major}"); + } + if (script.Contains("redis.setresp(3)") && !ep.GetFeatures().Resp3) /* v6 check */ + { + Skip.Inconclusive("debug protocol not available"); + } + if (ep.Protocol is null) throw new InvalidOperationException($"No protocol! {ep.InteractiveConnectionState}"); + Assert.Equal(protocol, ep.Protocol); + + var db = muxer.GetDatabase(); + if (expected is MAP_ABC) + { + db.KeyDelete("key"); + db.HashSet("key", "a", 1); + db.HashSet("key", "b", 2); + db.HashSet("key", "c", 3); + } + var result = await db.ScriptEvaluateAsync(script, flags: CommandFlags.NoScriptCache); + Assert.Equal(resp2, result.Resp2Type); + Assert.Equal(resp3, result.Resp3Type); + + switch (expected) + { + case null: + Assert.True(result.IsNull); + break; + case ARR_123: + Assert.Equal(3, result.Length); + for (int i = 0; i < result.Length; i++) + { + Assert.Equal(i + 1, result[i].AsInt32()); + } + break; + case MAP_ABC: + var map = result.ToDictionary(); + Assert.Equal(3, map.Count); + Assert.True(map.TryGetValue("a", out var value)); + Assert.Equal(1, value.AsInt32()); + Assert.True(map.TryGetValue("b", out value)); + Assert.Equal(2, value.AsInt32()); + Assert.True(map.TryGetValue("c", out value)); + Assert.Equal(3, value.AsInt32()); + break; + case SET_ABC: + Assert.Equal(3, result.Length); + var arr = result.AsStringArray()!; + Assert.Contains("a", arr); + Assert.Contains("b", arr); + Assert.Contains("c", arr); + break; + case string s: + Assert.Equal(s, result.AsString()); + break; + case double d: + Assert.Equal(d, result.AsDouble()); + break; + case int i: + Assert.Equal(i, result.AsInt32()); + break; + case bool b: + Assert.Equal(b, result.AsBoolean()); + break; + } + } + + + [Theory] + //[InlineData("return 42", false, ResultType.Integer, ResultType.Integer, 42)] + //[InlineData("return 'abc'", false, ResultType.BulkString, ResultType.BulkString, "abc")] + //[InlineData(@"return {1,2,3}", false, ResultType.Array, ResultType.Array, ARR_123)] + //[InlineData("return nil", false, ResultType.BulkString, ResultType.Null, null)] + //[InlineData(@"return redis.pcall('hgetall', 'key')", false, ResultType.Array, ResultType.Array, MAP_ABC)] + //[InlineData("return true", false, ResultType.Integer, ResultType.Integer, 1)] + + //[InlineData("return 42", true, ResultType.Integer, ResultType.Integer, 42)] + //[InlineData("return 'abc'", true, ResultType.BulkString, ResultType.BulkString, "abc")] + //[InlineData("return {1,2,3}", true, ResultType.Array, ResultType.Array, ARR_123)] + //[InlineData("return nil", true, ResultType.BulkString, ResultType.Null, null)] + //[InlineData(@"return redis.pcall('hgetall', 'key')", true, ResultType.Array, ResultType.Array, MAP_ABC)] + //[InlineData("return true", true, ResultType.Integer, ResultType.Integer, 1)] + + + [InlineData("incrby", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, 42, "ikey", 2)] + [InlineData("incrby", RedisProtocol.Resp3, ResultType.Integer, ResultType.Integer, 42, "ikey", 2)] + [InlineData("incrby", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, 2, "nkey", 2)] + [InlineData("incrby", RedisProtocol.Resp3, ResultType.Integer, ResultType.Integer, 2, "nkey", 2)] + + [InlineData("get", RedisProtocol.Resp2, ResultType.BulkString, ResultType.BulkString, "40", "ikey")] + [InlineData("get", RedisProtocol.Resp3, ResultType.BulkString, ResultType.BulkString, "40", "ikey")] + [InlineData("get", RedisProtocol.Resp2, ResultType.BulkString, ResultType.Null, null, "nkey")] + [InlineData("get", RedisProtocol.Resp3, ResultType.BulkString, ResultType.Null, null, "nkey")] + + [InlineData("smembers", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, SET_ABC, "skey")] + [InlineData("smembers", RedisProtocol.Resp3, ResultType.Array, ResultType.Set, SET_ABC, "skey")] + [InlineData("smembers", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, EMPTY_ARR, "nkey")] + [InlineData("smembers", RedisProtocol.Resp3, ResultType.Array, ResultType.Set, EMPTY_ARR, "nkey")] + + [InlineData("hgetall", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, MAP_ABC, "hkey")] + [InlineData("hgetall", RedisProtocol.Resp3, ResultType.Array, ResultType.Map, MAP_ABC, "hkey")] + [InlineData("hgetall", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, EMPTY_ARR, "nkey")] + [InlineData("hgetall", RedisProtocol.Resp3, ResultType.Array, ResultType.Map, EMPTY_ARR, "nkey")] + + [InlineData("sismember", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, true, "skey", "b")] + [InlineData("sismember", RedisProtocol.Resp3, ResultType.Integer, ResultType.Integer, true, "skey", "b")] + [InlineData("sismember", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, false, "nkey", "b")] + [InlineData("sismember", RedisProtocol.Resp3, ResultType.Integer, ResultType.Integer, false, "nkey", "b")] + [InlineData("sismember", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, false, "skey", "d")] + [InlineData("sismember", RedisProtocol.Resp3, ResultType.Integer, ResultType.Integer, false, "skey", "d")] + + [InlineData("latency", RedisProtocol.Resp2, ResultType.BulkString, ResultType.BulkString, STR_DAVE, "doctor")] + [InlineData("latency", RedisProtocol.Resp3, ResultType.BulkString, ResultType.VerbatimString, STR_DAVE, "doctor")] + + [InlineData("incrbyfloat", RedisProtocol.Resp2, ResultType.BulkString, ResultType.BulkString, 41.5, "ikey", 1.5)] + [InlineData("incrbyfloat", RedisProtocol.Resp3, ResultType.BulkString, ResultType.BulkString, 41.5, "ikey", 1.5)] + + /* DEBUG PROTOCOL + * Reply with a test value of the specified type. can be: string, + * integer, double, bignum, null, array, set, map, attrib, push, verbatim, + * true, false., + * + * NOTE: "debug protocol" may be disabled in later default server configs; if this starts + * failing when we upgrade the test server: update the config to re-enable the command + */ + [InlineData("debug", RedisProtocol.Resp2, ResultType.BulkString, ResultType.BulkString, ANY, "protocol", "string")] + [InlineData("debug", RedisProtocol.Resp3, ResultType.BulkString, ResultType.BulkString, ANY, "protocol", "string")] + + [InlineData("debug", RedisProtocol.Resp2, ResultType.BulkString, ResultType.BulkString, ANY, "protocol", "double")] + [InlineData("debug", RedisProtocol.Resp3, ResultType.SimpleString, ResultType.Double, ANY, "protocol", "double")] + + [InlineData("debug", RedisProtocol.Resp2, ResultType.BulkString, ResultType.BulkString, ANY, "protocol", "bignum")] + [InlineData("debug", RedisProtocol.Resp3, ResultType.SimpleString, ResultType.BigInteger, ANY, "protocol", "bignum")] + + [InlineData("debug", RedisProtocol.Resp2, ResultType.BulkString, ResultType.Null, null, "protocol", "null")] + [InlineData("debug", RedisProtocol.Resp3, ResultType.BulkString, ResultType.Null, null, "protocol", "null")] + + [InlineData("debug", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, ANY, "protocol", "array")] + [InlineData("debug", RedisProtocol.Resp3, ResultType.Array, ResultType.Array, ANY, "protocol", "array")] + + [InlineData("debug", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, ANY, "protocol", "set")] + [InlineData("debug", RedisProtocol.Resp3, ResultType.Array, ResultType.Set, ANY, "protocol", "set")] + + [InlineData("debug", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, ANY, "protocol", "map")] + [InlineData("debug", RedisProtocol.Resp3, ResultType.Array, ResultType.Map, ANY, "protocol", "map")] + + [InlineData("debug", RedisProtocol.Resp2, ResultType.BulkString, ResultType.BulkString, ANY, "protocol", "verbatim")] + [InlineData("debug", RedisProtocol.Resp3, ResultType.BulkString, ResultType.VerbatimString, ANY, "protocol", "verbatim")] + + [InlineData("debug", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, true, "protocol", "true")] + [InlineData("debug", RedisProtocol.Resp3, ResultType.Integer, ResultType.Boolean, true, "protocol", "true")] + + [InlineData("debug", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, false, "protocol", "false")] + [InlineData("debug", RedisProtocol.Resp3, ResultType.Integer, ResultType.Boolean, false, "protocol", "false")] + + public async Task CheckCommandResult(string command, RedisProtocol protocol, ResultType resp2, ResultType resp3, object expected, params object[] args) + { + var muxer = Create(protocol: protocol); + var ep = muxer.GetServerEndPoint(muxer.GetEndPoints().Single()); + if (command == "debug" && args.Length > 0 && args[0] is "protocol" && !ep.GetFeatures().Resp3 /* v6 check */ ) + { + Skip.Inconclusive("debug protocol not available"); + } + Assert.Equal(protocol, ep.Protocol); + + var db = muxer.GetDatabase(); + if (args.Length > 0) + { + await db.KeyDeleteAsync((string)args[0]); + switch (args[0]) + { + case "ikey": + await db.StringSetAsync("ikey", "40"); + break; + case "skey": + await db.SetAddAsync("skey", new RedisValue[] { "a", "b", "c" }); + break; + case "hkey": + await db.HashSetAsync("hkey", new HashEntry[] { new("a", 1), new("b", 2), new("c",3) }); + break; + } + } + var result = await db.ExecuteAsync(command, args); + Assert.Equal(resp2, result.Resp2Type); + Assert.Equal(resp3, result.Resp3Type); + + switch (expected) + { + case null: + Assert.True(result.IsNull); + break; + case ANY: + // not checked beyond type + break; + case EMPTY_ARR: + Assert.Equal(0, result.Length); + break; + case ARR_123: + Assert.Equal(3, result.Length); + for (int i = 0; i < result.Length; i++) + { + Assert.Equal(i + 1, result[i].AsInt32()); + } + break; + case STR_DAVE: + var scontent = result.ToString(); + Log(scontent); + Assert.NotNull(scontent); + var isExpectedContent = scontent.StartsWith("Dave, ") || scontent.StartsWith("I'm sorry, Dave"); + Assert.True(isExpectedContent); + Log(scontent); + + scontent = result.ToString(out var type); + Assert.NotNull(scontent); + isExpectedContent = scontent.StartsWith("Dave, ") || scontent.StartsWith("I'm sorry, Dave"); + Assert.True(isExpectedContent); + Log(scontent); + if (protocol == RedisProtocol.Resp3) + { + Assert.Equal("txt", type); + } + else + { + Assert.Null(type); + } + break; + case SET_ABC: + Assert.Equal(3, result.Length); + var arr = result.AsStringArray()!; + Assert.Contains("a", arr); + Assert.Contains("b", arr); + Assert.Contains("c", arr); + break; + case MAP_ABC: + var map = result.ToDictionary(); + Assert.Equal(3, map.Count); + Assert.True(map.TryGetValue("a", out var value)); + Assert.Equal(1, value.AsInt32()); + Assert.True(map.TryGetValue("b", out value)); + Assert.Equal(2, value.AsInt32()); + Assert.True(map.TryGetValue("c", out value)); + Assert.Equal(3, value.AsInt32()); + break; + case string s: + Assert.Equal(s, result.AsString()); + break; + case int i: + Assert.Equal(i, result.AsInt32()); + break; + case bool b: + Assert.Equal(b, result.AsBoolean()); + Assert.Equal(b ? 1 : 0, result.AsInt32()); + Assert.Equal(b ? 1 : 0, result.AsInt64()); + break; + } + + + } + + private const string SET_ABC = nameof(SET_ABC); + private const string ARR_123 = nameof(ARR_123); + private const string MAP_ABC = nameof(MAP_ABC); + private const string EMPTY_ARR = nameof(EMPTY_ARR); + private const string STR_DAVE = nameof(STR_DAVE); + private const string ANY = nameof(ANY); +} diff --git a/tests/StackExchange.Redis.Tests/RoleTests.cs b/tests/StackExchange.Redis.Tests/RoleTests.cs index 021ce8ebb..405ac39b9 100644 --- a/tests/StackExchange.Redis.Tests/RoleTests.cs +++ b/tests/StackExchange.Redis.Tests/RoleTests.cs @@ -24,9 +24,11 @@ public void PrimaryRole(bool allowAdmin) // should work with or without admin no Assert.NotNull(primary.Replicas); Log($"Searching for: {TestConfig.Current.ReplicaServer}:{TestConfig.Current.ReplicaPort}"); Log($"Replica count: {primary.Replicas.Count}"); - foreach (var r in primary.Replicas) + Assert.NotEmpty(primary.Replicas); + foreach (var replica in primary.Replicas) { - Log($" Replica: {r.Ip}:{r.Port} (offset: {r.ReplicationOffset})"); + Log($" Replica: {replica.Ip}:{replica.Port} (offset: {replica.ReplicationOffset})"); + Log(replica.ToString()); } Assert.Contains(primary.Replicas, r => r.Ip == TestConfig.Current.ReplicaServer && diff --git a/tests/StackExchange.Redis.Tests/SSLTests.cs b/tests/StackExchange.Redis.Tests/SSLTests.cs index 74c16e20c..87e589b97 100644 --- a/tests/StackExchange.Redis.Tests/SSLTests.cs +++ b/tests/StackExchange.Redis.Tests/SSLTests.cs @@ -334,7 +334,7 @@ public void RedisLabsEnvironmentVariableClientCertificate(bool setEnv) using var conn = ConnectionMultiplexer.Connect(options); RedisKey key = Me(); - if (!setEnv) Assert.True(false, "Could not set environment"); + if (!setEnv) Assert.Fail("Could not set environment"); var db = conn.GetDatabase(); db.KeyDelete(key, CommandFlags.FireAndForget); diff --git a/tests/StackExchange.Redis.Tests/ScanTests.cs b/tests/StackExchange.Redis.Tests/ScanTests.cs index b90d37592..bcab2da4c 100644 --- a/tests/StackExchange.Redis.Tests/ScanTests.cs +++ b/tests/StackExchange.Redis.Tests/ScanTests.cs @@ -7,10 +7,11 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class ScanTests : TestBase { - public ScanTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public ScanTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Theory] [InlineData(true)] @@ -24,6 +25,7 @@ public void KeysScan(bool supported) var db = conn.GetDatabase(dbId); var prefix = Me() + ":"; var server = GetServer(conn); + Assert.Equal(Context.Test.Protocol, server.Protocol); server.FlushDatabase(dbId); for (int i = 0; i < 100; i++) { diff --git a/tests/StackExchange.Redis.Tests/ScriptingTests.cs b/tests/StackExchange.Redis.Tests/ScriptingTests.cs index 00919d0c0..35bfbf36d 100644 --- a/tests/StackExchange.Redis.Tests/ScriptingTests.cs +++ b/tests/StackExchange.Redis.Tests/ScriptingTests.cs @@ -10,10 +10,11 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class ScriptingTests : TestBase { - public ScriptingTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public ScriptingTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } private IConnectionMultiplexer GetScriptConn(bool allowAdmin = false) { @@ -247,10 +248,9 @@ public void NonAsciiScripts() [Fact] public async Task ScriptThrowsError() { + using var conn = GetScriptConn(); await Assert.ThrowsAsync(async () => { - using var conn = GetScriptConn(); - var db = conn.GetDatabase(); try { @@ -791,13 +791,13 @@ public void IDatabaseLuaScriptConvenienceMethods() var db = conn.GetDatabase(); var key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - db.ScriptEvaluate(script, new { key = (RedisKey)key, value = "value" }, flags: CommandFlags.FireAndForget); + db.ScriptEvaluate(script, new { key = (RedisKey)key, value = "value" }); var val = db.StringGet(key); Assert.Equal("value", val); var prepared = script.Load(conn.GetServer(conn.GetEndPoints()[0])); - db.ScriptEvaluate(prepared, new { key = (RedisKey)(key + "2"), value = "value2" }, flags: CommandFlags.FireAndForget); + db.ScriptEvaluate(prepared, new { key = (RedisKey)(key + "2"), value = "value2" }); var val2 = db.StringGet(key + "2"); Assert.Equal("value2", val2); } diff --git a/tests/StackExchange.Redis.Tests/SecureTests.cs b/tests/StackExchange.Redis.Tests/SecureTests.cs index 454dedc68..9763cc15f 100644 --- a/tests/StackExchange.Redis.Tests/SecureTests.cs +++ b/tests/StackExchange.Redis.Tests/SecureTests.cs @@ -81,7 +81,7 @@ public async Task ConnectWithWrongPassword(string password, string exepctedMessa Assert.StartsWith("It was not possible to connect to the redis server(s). There was an authentication failure; check that passwords (or client certificates) are configured correctly: (RedisServerException) ", ex.Message); // This changed in some version...not sure which. For our purposes, splitting on v3 vs v6+ - if (checkServer.Version >= RedisFeatures.v6_0_0) + if (checkServer.Version.IsAtLeast(RedisFeatures.v6_0_0)) { Assert.EndsWith(exepctedMessage, ex.Message); } diff --git a/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs b/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs index 01688a337..ed1d995a5 100644 --- a/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs +++ b/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs @@ -9,6 +9,8 @@ namespace StackExchange.Redis.Tests; public class ServerSnapshotTests { [Fact] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2012:Do not use boolean check to check if a value exists in a collection", Justification = "Explicit testing")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2013:Do not use equality check to check for collection size.", Justification = "Explicit testing")] public void EmptyBehaviour() { var snapshot = ServerSnapshot.Empty; @@ -49,6 +51,7 @@ public void EmptyBehaviour() [InlineData(5, 0)] [InlineData(5, 3)] [InlineData(5, 5)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2012:Do not use boolean check to check if a value exists in a collection", Justification = "Explicit testing")] public void NonEmptyBehaviour(int count, int replicaCount) { var snapshot = ServerSnapshot.Empty; diff --git a/tests/StackExchange.Redis.Tests/SetTests.cs b/tests/StackExchange.Redis.Tests/SetTests.cs index ea7043cf8..d90e4a8c3 100644 --- a/tests/StackExchange.Redis.Tests/SetTests.cs +++ b/tests/StackExchange.Redis.Tests/SetTests.cs @@ -6,10 +6,11 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class SetTests : TestBase { - public SetTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public SetTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public void SetContains() diff --git a/tests/StackExchange.Redis.Tests/SortedSetTests.cs b/tests/StackExchange.Redis.Tests/SortedSetTests.cs index 3b99478ce..49c464142 100644 --- a/tests/StackExchange.Redis.Tests/SortedSetTests.cs +++ b/tests/StackExchange.Redis.Tests/SortedSetTests.cs @@ -5,6 +5,7 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class SortedSetTests : TestBase { @@ -327,6 +328,81 @@ public async Task SortedSetIntersectionLengthAsync() Assert.Equal(3, inter); } + [Fact] + public void SortedSetRangeViaScript() + { + using var conn = Create(require: RedisFeatures.v5_0_0); + var db = conn.GetDatabase(); + var key = Me(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); + + var result = db.ScriptEvaluate("return redis.call('ZRANGE', KEYS[1], 0, -1, 'WITHSCORES')", new RedisKey[] { key }); + AssertFlatArrayEntries(result); + } + + [Fact] + public void SortedSetRangeViaExecute() + { + using var conn = Create(require: RedisFeatures.v5_0_0); + var db = conn.GetDatabase(); + var key = Me(); + + db.KeyDelete(key, CommandFlags.FireAndForget); + db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); + + var result = db.Execute("ZRANGE", new object[] { key, 0, -1, "WITHSCORES" }); + + if (Context.IsResp3) + { + AssertJaggedArrayEntries(result); + } + else + { + AssertFlatArrayEntries(result); + } + } + + private void AssertFlatArrayEntries(RedisResult result) + { + Assert.Equal(ResultType.Array, result.Resp2Type); + Assert.Equal(entries.Length * 2, (int)result.Length); + int index = 0; + foreach (var entry in entries) + { + var e = result[index++]; + Assert.Equal(ResultType.BulkString, e.Resp2Type); + Assert.Equal(entry.Element, e.AsRedisValue()); + + e = result[index++]; + Assert.Equal(ResultType.BulkString, e.Resp2Type); + Assert.Equal(entry.Score, e.AsDouble()); + } + } + + private void AssertJaggedArrayEntries(RedisResult result) + { + Assert.Equal(ResultType.Array, result.Resp2Type); + Assert.Equal(entries.Length, (int)result.Length); + int index = 0; + foreach (var entry in entries) + { + var arr = result[index++]; + Assert.Equal(ResultType.Array, arr.Resp2Type); + Assert.Equal(2, arr.Length); + + var e = arr[0]; + Assert.Equal(ResultType.BulkString, e.Resp2Type); + Assert.Equal(entry.Element, e.AsRedisValue()); + + e = arr[1]; + Assert.Equal(ResultType.SimpleString, e.Resp2Type); + Assert.Equal(ResultType.Double, e.Resp3Type); + Assert.Equal(entry.Score, e.AsDouble()); + } + } + [Fact] public void SortedSetPopMulti_Multi() { @@ -1134,7 +1210,7 @@ public void SortedSetScoresSingle() var score = db.SortedSetScore(key, memberName); Assert.NotNull(score); - Assert.Equal((double)1.5, score.Value); + Assert.Equal((double)1.5, score); } [Fact] diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index 824ff48a3..305e38298 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Newtonsoft.Json; using Xunit; @@ -7,18 +8,22 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class StreamTests : TestBase { public StreamTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public override string Me([CallerFilePath] string? filePath = null, [CallerMemberName] string? caller = null) => + base.Me(filePath, caller) + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + [Fact] public void IsStreamType() { using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("type_check"); + var key = Me(); db.StreamAdd(key, "field1", "value1"); var keyType = db.KeyType(key); @@ -32,7 +37,8 @@ public void StreamAddSinglePairWithAutoId() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var messageId = db.StreamAdd(GetUniqueKey("auto_id"), "field1", "value1"); + var key = Me(); + var messageId = db.StreamAdd(key, "field1", "value1"); Assert.True(messageId != RedisValue.Null && ((string?)messageId)?.Length > 0); } @@ -43,7 +49,7 @@ public void StreamAddMultipleValuePairsWithAutoId() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("multiple_value_pairs"); + var key = Me(); var fields = new[] { new NameValueEntry("field1", "value1"), @@ -72,7 +78,7 @@ public void StreamAddWithManualId() var db = conn.GetDatabase(); const string id = "42-0"; - var key = GetUniqueKey("manual_id"); + var key = Me(); var messageId = db.StreamAdd(key, "field1", "value1", id); @@ -86,7 +92,7 @@ public void StreamAddMultipleValuePairsWithManualId() var db = conn.GetDatabase(); const string id = "42-0"; - var key = GetUniqueKey("manual_id_multiple_values"); + var key = Me(); var fields = new[] { @@ -492,7 +498,7 @@ public void StreamConsumerGroupSetId() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_set_id"); + var key = Me(); const string groupName = "test_group", consumer = "consumer"; @@ -523,7 +529,7 @@ public void StreamConsumerGroupWithNoConsumers() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_with_no_consumers"); + var key = Me(); const string groupName = "test_group"; // Create a stream @@ -544,7 +550,7 @@ public void StreamCreateConsumerGroup() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_create"); + var key = Me(); const string groupName = "test_group"; // Create a stream @@ -562,7 +568,7 @@ public void StreamCreateConsumerGroupBeforeCreatingStream() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_create_before_stream"); + var key = Me(); // Ensure the key doesn't exist. var keyExistsBeforeCreate = db.KeyExists(key); @@ -583,7 +589,7 @@ public void StreamCreateConsumerGroupFailsIfKeyDoesntExist() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_create_before_stream_should_fail"); + var key = Me(); // Pass 'false' for 'createStream' to ensure that an // exception is thrown when the stream doesn't exist. @@ -600,7 +606,7 @@ public void StreamCreateConsumerGroupSucceedsWhenKeyExists() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_create_after_stream"); + var key = Me(); db.StreamAdd(key, "f1", "v1"); @@ -621,7 +627,7 @@ public void StreamConsumerGroupReadOnlyNewMessagesWithEmptyResponse() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_read"); + var key = Me(); const string groupName = "test_group"; // Create a stream @@ -643,7 +649,7 @@ public void StreamConsumerGroupReadFromStreamBeginning() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_read_beginning"); + var key = Me(); const string groupName = "test_group"; var id1 = db.StreamAdd(key, "field1", "value1"); @@ -664,7 +670,7 @@ public void StreamConsumerGroupReadFromStreamBeginningWithCount() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_read_with_count"); + var key = Me(); const string groupName = "test_group"; var id1 = db.StreamAdd(key, "field1", "value1"); @@ -689,7 +695,7 @@ public void StreamConsumerGroupAcknowledgeMessage() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_ack"); + var key = Me(); const string groupName = "test_group", consumer = "test_consumer"; @@ -727,7 +733,7 @@ public void StreamConsumerGroupClaimMessages() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_claim"); + var key = Me(); const string groupName = "test_group", consumer1 = "test_consumer_1", consumer2 = "test_consumer_2"; @@ -774,7 +780,7 @@ public void StreamConsumerGroupClaimMessagesReturningIds() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_claim_view_ids"); + var key = Me(); const string groupName = "test_group", consumer1 = "test_consumer_1", consumer2 = "test_consumer_2"; @@ -826,8 +832,8 @@ public void StreamConsumerGroupReadMultipleOneReadBeginningOneReadNew() var db = conn.GetDatabase(); const string groupName = "test_group"; - var stream1 = GetUniqueKey("stream1a"); - var stream2 = GetUniqueKey("stream2a"); + var stream1 = Me() + "a"; + var stream2 = Me() + "b"; db.StreamAdd(stream1, "field1-1", "value1-1"); db.StreamAdd(stream1, "field1-2", "value1-2"); @@ -865,8 +871,8 @@ public void StreamConsumerGroupReadMultipleOnlyNewMessagesExpectNoResult() var db = conn.GetDatabase(); const string groupName = "test_group"; - var stream1 = GetUniqueKey("stream1b"); - var stream2 = GetUniqueKey("stream2b"); + var stream1 = Me() + "a"; + var stream2 = Me() + "b"; db.StreamAdd(stream1, "field1-1", "value1-1"); db.StreamAdd(stream2, "field2-1", "value2-1"); @@ -897,8 +903,8 @@ public void StreamConsumerGroupReadMultipleOnlyNewMessagesExpect1Result() var db = conn.GetDatabase(); const string groupName = "test_group"; - var stream1 = GetUniqueKey("stream1c"); - var stream2 = GetUniqueKey("stream2c"); + var stream1 = Me() + "a"; + var stream2 = Me() + "b"; // These messages won't be read. db.StreamAdd(stream1, "field1-1", "value1-1"); @@ -936,8 +942,8 @@ public void StreamConsumerGroupReadMultipleRestrictCount() var db = conn.GetDatabase(); const string groupName = "test_group"; - var stream1 = GetUniqueKey("stream1d"); - var stream2 = GetUniqueKey("stream2d"); + var stream1 = Me() + "a"; + var stream2 = Me() + "b"; var id1_1 = db.StreamAdd(stream1, "field1-1", "value1-1"); var id1_2 = db.StreamAdd(stream1, "field1-2", "value1-2"); @@ -973,7 +979,7 @@ public void StreamConsumerGroupViewPendingInfoNoConsumers() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_pending_info_no_consumers"); + var key = Me(); const string groupName = "test_group"; db.StreamAdd(key, "field1", "value1"); @@ -995,7 +1001,7 @@ public void StreamConsumerGroupViewPendingInfoWhenNothingPending() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_pending_info_nothing_pending"); + var key = Me(); const string groupName = "test_group"; db.StreamAdd(key, "field1", "value1"); @@ -1017,7 +1023,7 @@ public void StreamConsumerGroupViewPendingInfoSummary() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_pending_info"); + var key = Me(); const string groupName = "test_group", consumer1 = "test_consumer_1", consumer2 = "test_consumer_2"; @@ -1055,7 +1061,7 @@ public async Task StreamConsumerGroupViewPendingMessageInfo() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_pending_messages"); + var key = Me(); const string groupName = "test_group", consumer1 = "test_consumer_1", consumer2 = "test_consumer_2"; @@ -1092,7 +1098,7 @@ public void StreamConsumerGroupViewPendingMessageInfoForConsumer() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_pending_for_consumer"); + var key = Me(); const string groupName = "test_group", consumer1 = "test_consumer_1", consumer2 = "test_consumer_2"; @@ -1126,7 +1132,7 @@ public void StreamDeleteConsumer() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("delete_consumer"); + var key = Me(); const string groupName = "test_group", consumer = "test_consumer"; @@ -1157,7 +1163,7 @@ public void StreamDeleteConsumerGroup() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("delete_consumer_group"); + var key = Me(); const string groupName = "test_group", consumer = "test_consumer"; @@ -1186,7 +1192,7 @@ public void StreamDeleteMessage() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("delete_msg"); + var key = Me(); db.StreamAdd(key, "field1", "value1"); db.StreamAdd(key, "field2", "value2"); @@ -1206,7 +1212,7 @@ public void StreamDeleteMessages() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("delete_msgs"); + var key = Me(); db.StreamAdd(key, "field1", "value1"); var id2 = db.StreamAdd(key, "field2", "value2"); @@ -1223,7 +1229,7 @@ public void StreamDeleteMessages() [Fact] public void StreamGroupInfoGet() { - var key = GetUniqueKey("group_info"); + var key = Me(); const string group1 = "test_group_1", group2 = "test_group_2", consumer1 = "test_consumer_1", @@ -1285,7 +1291,7 @@ public void StreamGroupConsumerInfoGet() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("group_consumer_info"); + var key = Me(); const string group = "test_group", consumer1 = "test_consumer_1", consumer2 = "test_consumer_2"; @@ -1317,7 +1323,7 @@ public void StreamInfoGet() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("stream_info"); + var key = Me(); var id1 = db.StreamAdd(key, "field1", "value1"); db.StreamAdd(key, "field2", "value2"); @@ -1339,7 +1345,7 @@ public void StreamInfoGetWithEmptyStream() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("stream_info_empty"); + var key = Me(); // Add an entry and then delete it so the stream is empty, then run streaminfo // to ensure it functions properly on an empty stream. Namely, the first-entry @@ -1362,7 +1368,7 @@ public void StreamNoConsumerGroups() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("stream_with_no_consumers"); + var key = Me(); db.StreamAdd(key, "field1", "value1"); @@ -1378,7 +1384,7 @@ public void StreamPendingNoMessagesOrConsumers() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("stream_pending_empty"); + var key = Me(); const string groupName = "test_group"; var id = db.StreamAdd(key, "field1", "value1"); @@ -1437,7 +1443,7 @@ public void StreamRead() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("read"); + var key = Me(); var id1 = db.StreamAdd(key, "field1", "value1"); var id2 = db.StreamAdd(key, "field2", "value2"); @@ -1458,7 +1464,7 @@ public void StreamReadEmptyStream() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("read_empty_stream"); + var key = Me(); // Write to a stream to create the key. var id1 = db.StreamAdd(key, "field1", "value1"); @@ -1480,8 +1486,8 @@ public void StreamReadEmptyStreams() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key1 = GetUniqueKey("read_empty_stream_1"); - var key2 = GetUniqueKey("read_empty_stream_2"); + var key1 = Me() + "a"; + var key2 = Me() + "b"; // Write to a stream to create the key. var id1 = db.StreamAdd(key1, "field1", "value1"); @@ -1525,7 +1531,7 @@ public void StreamReadExpectedExceptionInvalidCountSingleStream() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("read_exception_invalid_count_single"); + var key = Me(); Assert.Throws(() => db.StreamRead(key, "0-0", 0)); } @@ -1554,8 +1560,8 @@ public void StreamReadMultipleStreams() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key1 = GetUniqueKey("read_multi_1a"); - var key2 = GetUniqueKey("read_multi_2a"); + var key1 = Me() + "a"; + var key2 = Me() + "b"; var id1 = db.StreamAdd(key1, "field1", "value1"); var id2 = db.StreamAdd(key1, "field2", "value2"); @@ -1590,8 +1596,8 @@ public void StreamReadMultipleStreamsWithCount() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key1 = GetUniqueKey("read_multi_count_1"); - var key2 = GetUniqueKey("read_multi_count_2"); + var key1 = Me() + "a"; + var key2 = Me() + "b"; var id1 = db.StreamAdd(key1, "field1", "value1"); db.StreamAdd(key1, "field2", "value2"); @@ -1624,8 +1630,8 @@ public void StreamReadMultipleStreamsWithReadPastSecondStream() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key1 = GetUniqueKey("read_multi_1b"); - var key2 = GetUniqueKey("read_multi_2b"); + var key1 = Me() + "a"; + var key2 = Me() + "b"; db.StreamAdd(key1, "field1", "value1"); db.StreamAdd(key1, "field2", "value2"); @@ -1655,8 +1661,8 @@ public void StreamReadMultipleStreamsWithEmptyResponse() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key1 = GetUniqueKey("read_multi_1c"); - var key2 = GetUniqueKey("read_multi_2c"); + var key1 = Me() + "a"; + var key2 = Me() + "b"; db.StreamAdd(key1, "field1", "value1"); var id2 = db.StreamAdd(key1, "field2", "value2"); @@ -1682,7 +1688,7 @@ public void StreamReadPastEndOfStream() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("read_empty"); + var key = Me(); db.StreamAdd(key, "field1", "value1"); var id2 = db.StreamAdd(key, "field2", "value2"); @@ -1700,7 +1706,7 @@ public void StreamReadRange() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("range"); + var key = Me(); var id1 = db.StreamAdd(key, "field1", "value1"); var id2 = db.StreamAdd(key, "field2", "value2"); @@ -1718,7 +1724,7 @@ public void StreamReadRangeOfEmptyStream() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("range_empty"); + var key = Me(); var id1 = db.StreamAdd(key, "field1", "value1"); var id2 = db.StreamAdd(key, "field2", "value2"); @@ -1738,7 +1744,7 @@ public void StreamReadRangeWithCount() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("range_count"); + var key = Me(); var id1 = db.StreamAdd(key, "field1", "value1"); db.StreamAdd(key, "field2", "value2"); @@ -1755,7 +1761,7 @@ public void StreamReadRangeReverse() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("rangerev"); + var key = Me(); var id1 = db.StreamAdd(key, "field1", "value1"); var id2 = db.StreamAdd(key, "field2", "value2"); @@ -1773,7 +1779,7 @@ public void StreamReadRangeReverseWithCount() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("rangerev_count"); + var key = Me(); var id1 = db.StreamAdd(key, "field1", "value1"); var id2 = db.StreamAdd(key, "field2", "value2"); @@ -1790,7 +1796,7 @@ public void StreamReadWithAfterIdAndCount_1() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("read1"); + var key = Me(); var id1 = db.StreamAdd(key, "field1", "value1"); var id2 = db.StreamAdd(key, "field2", "value2"); @@ -1809,7 +1815,7 @@ public void StreamReadWithAfterIdAndCount_2() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("read2"); + var key = Me(); var id1 = db.StreamAdd(key, "field1", "value1"); var id2 = db.StreamAdd(key, "field2", "value2"); @@ -1830,7 +1836,7 @@ public void StreamTrimLength() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("trimlen"); + var key = Me(); // Add a couple items and check length. db.StreamAdd(key, "field1", "value1"); @@ -1851,7 +1857,7 @@ public void StreamVerifyLength() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("len"); + var key = Me(); // Add a couple items and check length. db.StreamAdd(key, "field1", "value1"); @@ -1868,7 +1874,7 @@ public async Task AddWithApproxCountAsync() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("approx-async"); + var key = Me(); await db.StreamAddAsync(key, "field", "value", maxLength: 10, useApproximateMaxLength: true, flags: CommandFlags.None).ConfigureAwait(false); } @@ -1878,7 +1884,7 @@ public void AddWithApproxCount() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("approx"); + var key = Me(); db.StreamAdd(key, "field", "value", maxLength: 10, useApproximateMaxLength: true, flags: CommandFlags.None); } @@ -1888,7 +1894,7 @@ public void StreamReadGroupWithNoAckShowsNoPendingMessages() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key = GetUniqueKey("read_group_noack"); + var key = Me(); const string groupName = "test_group", consumer = "consumer"; @@ -1914,8 +1920,8 @@ public void StreamReadGroupMultiStreamWithNoAckShowsNoPendingMessages() using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var key1 = GetUniqueKey("read_group_noack1"); - var key2 = GetUniqueKey("read_group_noack2"); + var key1 = Me() + "a"; + var key2 = Me() + "b"; const string groupName = "test_group", consumer = "consumer"; @@ -1944,15 +1950,13 @@ public void StreamReadGroupMultiStreamWithNoAckShowsNoPendingMessages() Assert.Equal(0, pending2.PendingMessageCount); } - private static RedisKey GetUniqueKey(string type) => $"{type}_stream_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; - [Fact] public async Task StreamReadIndexerUsage() { using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); - var streamName = GetUniqueKey("read-group-indexer"); + var streamName = Me(); await db.StreamAddAsync(streamName, new[] { new NameValueEntry("x", "blah"), diff --git a/tests/StackExchange.Redis.Tests/StringTests.cs b/tests/StackExchange.Redis.Tests/StringTests.cs index 9ba3c73bd..23acf737f 100644 --- a/tests/StackExchange.Redis.Tests/StringTests.cs +++ b/tests/StackExchange.Redis.Tests/StringTests.cs @@ -11,6 +11,7 @@ namespace StackExchange.Redis.Tests; /// /// Tests for . /// +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class StringTests : TestBase { diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index d1435d26f..c0dfb028c 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -1,4 +1,6 @@ -using System; +using StackExchange.Redis.Profiling; +using StackExchange.Redis.Tests.Helpers; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -7,8 +9,6 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using StackExchange.Redis.Profiling; -using StackExchange.Redis.Tests.Helpers; using Xunit; using Xunit.Abstractions; @@ -22,6 +22,13 @@ public abstract class TestBase : IDisposable protected virtual string GetConfiguration() => GetDefaultConfiguration(); internal static string GetDefaultConfiguration() => TestConfig.Current.PrimaryServerAndPort; + /// + /// Gives the current TestContext, propulated by the runner (this type of thing will be built-in in xUnit 3.x) + /// + protected TestContext Context => _context.Value!; + private static readonly AsyncLocal _context = new(); + public static void SetContext(TestContext context) => _context.Value = context; + private readonly SharedConnectionFixture? _fixture; protected bool SharedFixtureAvailable => _fixture != null && _fixture.IsEnabled; @@ -30,6 +37,7 @@ protected TestBase(ITestOutputHelper output, SharedConnectionFixture? fixture = { Output = output; Output.WriteFrameworkVersion(); + Output.WriteLine(" Context: " + Context.ToString()); Writer = new TextWriterOutputHelper(output, TestConfig.Current.LogToConsole); _fixture = fixture; ClearAmbientFailures(); @@ -231,6 +239,7 @@ protected static IServer GetAnyPrimary(IConnectionMultiplexer muxer) internal virtual IInternalConnectionMultiplexer Create( string? clientName = null, int? syncTimeout = null, + int? asyncTimeout = null, bool? allowAdmin = null, int? keepAlive = null, int? connectTimeout = null, @@ -250,50 +259,47 @@ internal virtual IInternalConnectionMultiplexer Create( int? defaultDatabase = null, BacklogPolicy? backlogPolicy = null, Version? require = null, - [CallerMemberName] string? caller = null) + RedisProtocol? protocol = null, + [CallerMemberName] string caller = "") { if (Output == null) { - Assert.True(false, "Failure: Be sure to call the TestBase constructor like this: BasicOpsTests(ITestOutputHelper output) : base(output) { }"); + Assert.Fail("Failure: Be sure to call the TestBase constructor like this: BasicOpsTests(ITestOutputHelper output) : base(output) { }"); } + // Default to protocol context if not explicitly passed in + protocol ??= Context.Test.Protocol; + // Share a connection if instructed to and we can - many specifics mean no sharing - if (shared + if (shared && expectedFailCount == 0 && _fixture != null && _fixture.IsEnabled - && enabledCommands == null - && disabledCommands == null - && fail - && channelPrefix == null - && proxy == null - && configuration == null - && password == null - && tieBreaker == null - && defaultDatabase == null - && (allowAdmin == null || allowAdmin == true) - && expectedFailCount == 0 - && backlogPolicy == null) + && CanShare(allowAdmin, password, tieBreaker, fail, disabledCommands, enabledCommands, channelPrefix, proxy, configuration, defaultDatabase, backlogPolicy)) { configuration = GetConfiguration(); + var fixtureConn = _fixture.GetConnection(this, protocol.Value, caller: caller); // Only return if we match + ThrowIfIncorrectProtocol(fixtureConn, protocol); + if (configuration == _fixture.Configuration) { - ThrowIfBelowMinVersion(_fixture.Connection, require); - return _fixture.Connection; + ThrowIfBelowMinVersion(fixtureConn, require); + return fixtureConn; } } var conn = CreateDefault( Writer, configuration ?? GetConfiguration(), - clientName, syncTimeout, allowAdmin, keepAlive, + clientName, syncTimeout, asyncTimeout, allowAdmin, keepAlive, connectTimeout, password, tieBreaker, log, fail, disabledCommands, enabledCommands, checkConnect, failMessage, channelPrefix, proxy, logTransactionData, defaultDatabase, - backlogPolicy, + backlogPolicy, protocol, caller); + ThrowIfIncorrectProtocol(conn, protocol); ThrowIfBelowMinVersion(conn, require); conn.InternalError += OnInternalError; @@ -302,15 +308,57 @@ internal virtual IInternalConnectionMultiplexer Create( return conn; } - protected void ThrowIfBelowMinVersion(IConnectionMultiplexer conn, Version? requiredVersion) + internal static bool CanShare( + bool? allowAdmin, + string? password, + string? tieBreaker, + bool fail, + string[]? disabledCommands, + string[]? enabledCommands, + string? channelPrefix, + Proxy? proxy, + string? configuration, + int? defaultDatabase, + BacklogPolicy? backlogPolicy + ) + => enabledCommands == null + && disabledCommands == null + && fail + && channelPrefix == null + && proxy == null + && configuration == null + && password == null + && tieBreaker == null + && defaultDatabase == null + && (allowAdmin == null || allowAdmin == true) + && backlogPolicy == null; + + internal void ThrowIfIncorrectProtocol(IInternalConnectionMultiplexer conn, RedisProtocol? requiredProtocol) + { + if (requiredProtocol is null) + { + return; + } + + var serverProtocol = conn.GetServerEndPoint(conn.GetEndPoints()[0]).Protocol ?? RedisProtocol.Resp2; + if (serverProtocol != requiredProtocol) + { + throw new SkipTestException($"Requires protocol {requiredProtocol}, but connection is {serverProtocol}.") + { + MissingFeatures = $"Protocol {requiredProtocol}." + }; + } + } + + internal void ThrowIfBelowMinVersion(IInternalConnectionMultiplexer conn, Version? requiredVersion) { if (requiredVersion is null) { return; } - var serverVersion = conn.GetServer(conn.GetEndPoints()[0]).Version; - if (requiredVersion > serverVersion) + var serverVersion = conn.GetServerEndPoint(conn.GetEndPoints()[0]).Version; + if (!serverVersion.IsAtLeast(requiredVersion)) { throw new SkipTestException($"Requires server version {requiredVersion}, but server is only {serverVersion}.") { @@ -324,6 +372,7 @@ public static ConnectionMultiplexer CreateDefault( string configuration, string? clientName = null, int? syncTimeout = null, + int? asyncTimeout = null, bool? allowAdmin = null, int? keepAlive = null, int? connectTimeout = null, @@ -340,13 +389,11 @@ public static ConnectionMultiplexer CreateDefault( bool logTransactionData = true, int? defaultDatabase = null, BacklogPolicy? backlogPolicy = null, - [CallerMemberName] string? caller = null) + RedisProtocol? protocol = null, + [CallerMemberName] string caller = "") { StringWriter? localLog = null; - if (log == null) - { - log = localLog = new StringWriter(); - } + log ??= localLog = new StringWriter(); try { var config = ConfigurationOptions.Parse(configuration); @@ -364,18 +411,20 @@ public static ConnectionMultiplexer CreateDefault( syncTimeout = int.MaxValue; } - if (channelPrefix != null) config.ChannelPrefix = RedisChannel.Literal(channelPrefix); - if (tieBreaker != null) config.TieBreaker = tieBreaker; - if (password != null) config.Password = string.IsNullOrEmpty(password) ? null : password; - if (clientName != null) config.ClientName = clientName; - else if (caller != null) config.ClientName = caller; - if (syncTimeout != null) config.SyncTimeout = syncTimeout.Value; - if (allowAdmin != null) config.AllowAdmin = allowAdmin.Value; - if (keepAlive != null) config.KeepAlive = keepAlive.Value; - if (connectTimeout != null) config.ConnectTimeout = connectTimeout.Value; - if (proxy != null) config.Proxy = proxy.Value; - if (defaultDatabase != null) config.DefaultDatabase = defaultDatabase.Value; - if (backlogPolicy != null) config.BacklogPolicy = backlogPolicy; + if (channelPrefix is not null) config.ChannelPrefix = RedisChannel.Literal(channelPrefix); + if (tieBreaker is not null) config.TieBreaker = tieBreaker; + if (password is not null) config.Password = string.IsNullOrEmpty(password) ? null : password; + if (clientName is not null) config.ClientName = clientName; + else if (!string.IsNullOrEmpty(caller)) config.ClientName = caller; + if (syncTimeout is not null) config.SyncTimeout = syncTimeout.Value; + if (asyncTimeout is not null) config.AsyncTimeout = asyncTimeout.Value; + if (allowAdmin is not null) config.AllowAdmin = allowAdmin.Value; + if (keepAlive is not null) config.KeepAlive = keepAlive.Value; + if (connectTimeout is not null) config.ConnectTimeout = connectTimeout.Value; + if (proxy is not null) config.Proxy = proxy.Value; + if (defaultDatabase is not null) config.DefaultDatabase = defaultDatabase.Value; + if (backlogPolicy is not null) config.BacklogPolicy = backlogPolicy; + if (protocol is not null) config.Protocol = protocol; var watch = Stopwatch.StartNew(); var task = ConnectionMultiplexer.ConnectAsync(config, log); if (!task.Wait(config.ConnectTimeout >= (int.MaxValue / 2) ? int.MaxValue : config.ConnectTimeout * 2)) @@ -430,10 +479,10 @@ public static ConnectionMultiplexer CreateDefault( } } - public static string Me([CallerFilePath] string? filePath = null, [CallerMemberName] string? caller = null) => - Environment.Version.ToString() + Path.GetFileNameWithoutExtension(filePath) + "-" + caller; + public virtual string Me([CallerFilePath] string? filePath = null, [CallerMemberName] string? caller = null) => + Environment.Version.ToString() + Path.GetFileNameWithoutExtension(filePath) + "-" + caller + Context.KeySuffix; - protected static TimeSpan RunConcurrent(Action work, int threads, int timeout = 10000, [CallerMemberName] string? caller = null) + protected TimeSpan RunConcurrent(Action work, int threads, int timeout = 10000, [CallerMemberName] string? caller = null) { if (work == null) throw new ArgumentNullException(nameof(work)); if (threads < 1) throw new ArgumentOutOfRangeException(nameof(threads)); diff --git a/tests/StackExchange.Redis.Tests/TransactionTests.cs b/tests/StackExchange.Redis.Tests/TransactionTests.cs index 075c0eb2c..c9ccec71b 100644 --- a/tests/StackExchange.Redis.Tests/TransactionTests.cs +++ b/tests/StackExchange.Redis.Tests/TransactionTests.cs @@ -5,6 +5,7 @@ namespace StackExchange.Redis.Tests; +[RunPerProtocol] [Collection(SharedConnectionFixture.Key)] public class TransactionTests : TestBase { diff --git a/tests/StackExchange.Redis.Tests/xunit.runner.json b/tests/StackExchange.Redis.Tests/xunit.runner.json index 65a35fb2f..8bca1f742 100644 --- a/tests/StackExchange.Redis.Tests/xunit.runner.json +++ b/tests/StackExchange.Redis.Tests/xunit.runner.json @@ -1,6 +1,6 @@ { "methodDisplay": "classAndMethod", - "maxParallelThreads": 8, + "maxParallelThreads": 16, "diagnosticMessages": false, "longRunningTestSeconds": 60 } \ No newline at end of file diff --git a/toys/StackExchange.Redis.Server/RespServer.cs b/toys/StackExchange.Redis.Server/RespServer.cs index 174eb10fc..1edd2a3a7 100644 --- a/toys/StackExchange.Redis.Server/RespServer.cs +++ b/toys/StackExchange.Redis.Server/RespServer.cs @@ -315,7 +315,7 @@ static void WritePrefix(PipeWriter ooutput, char pprefix) if (value.IsNil) return; // not actually a request (i.e. empty/whitespace request) if (client != null && client.ShouldSkipResponse()) return; // intentionally skipping the result char prefix; - switch (value.Type) + switch (value.Type.ToResp2()) { case ResultType.Integer: PhysicalConnection.WriteInteger(output, (long)value.AsRedisValue()); @@ -335,7 +335,7 @@ static void WritePrefix(PipeWriter ooutput, char pprefix) case ResultType.BulkString: PhysicalConnection.WriteBulkString(value.AsRedisValue(), output); break; - case ResultType.MultiBulk: + case ResultType.Array: if (value.IsNullArray) { PhysicalConnection.WriteMultiBulkHeader(output, -1); @@ -367,7 +367,7 @@ static void WritePrefix(PipeWriter ooutput, char pprefix) private static bool TryParseRequest(Arena arena, ref ReadOnlySequence buffer, out RedisRequest request) { var reader = new BufferReader(buffer); - var raw = PhysicalConnection.TryParseResult(arena, in buffer, ref reader, false, null, true); + var raw = PhysicalConnection.TryParseResult(false, arena, in buffer, ref reader, false, null, true); if (raw.HasValue) { buffer = reader.SliceFromCurrent(); diff --git a/toys/StackExchange.Redis.Server/TypedRedisValue.cs b/toys/StackExchange.Redis.Server/TypedRedisValue.cs index e6d27110c..b7240370b 100644 --- a/toys/StackExchange.Redis.Server/TypedRedisValue.cs +++ b/toys/StackExchange.Redis.Server/TypedRedisValue.cs @@ -35,7 +35,7 @@ internal static TypedRedisValue Rent(int count, out Span span) /// /// Returns whether this value represents a null array. /// - public bool IsNullArray => Type == ResultType.MultiBulk && _value.DirectObject == null; + public bool IsNullArray => Type == ResultType.Array && _value.DirectObject == null; private readonly RedisValue _value; @@ -85,7 +85,7 @@ public ReadOnlySpan Span { get { - if (Type != ResultType.MultiBulk) return default; + if (Type != ResultType.Array) return default; var arr = (TypedRedisValue[])_value.DirectObject; if (arr == null) return default; var length = (int)_value.DirectOverlappedBits64; @@ -96,7 +96,7 @@ public ArraySegment Segment { get { - if (Type != ResultType.MultiBulk) return default; + if (Type != ResultType.Array) return default; var arr = (TypedRedisValue[])_value.DirectObject; if (arr == null) return default; var length = (int)_value.DirectOverlappedBits64; @@ -156,7 +156,7 @@ private TypedRedisValue(TypedRedisValue[] oversizedItems, int count) if (count == 0) oversizedItems = Array.Empty(); } _value = new RedisValue(oversizedItems, count); - Type = ResultType.MultiBulk; + Type = ResultType.Array; } internal void Recycle(int limit = -1) @@ -175,7 +175,7 @@ internal void Recycle(int limit = -1) /// /// Get the underlying assuming that it is a valid type with a meaningful value. /// - internal RedisValue AsRedisValue() => Type == ResultType.MultiBulk ? default :_value; + internal RedisValue AsRedisValue() => Type == ResultType.Array ? default :_value; /// /// Obtain the value as a string. @@ -189,7 +189,7 @@ public override string ToString() case ResultType.Integer: case ResultType.Error: return $"{Type}:{_value}"; - case ResultType.MultiBulk: + case ResultType.Array: return $"{Type}:[{Span.Length}]"; default: return Type.ToString(); diff --git a/version.json b/version.json index f5ce755a1..c37674c0e 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "2.6", + "version": "2.7", "versionHeightOffset": -1, "assemblyVersion": "2.0", "publicReleaseRefSpec": [ From 5504ed9a93f729f204e6cb53280cb4efa1ccd390 Mon Sep 17 00:00:00 2001 From: Matiszak Date: Sun, 10 Sep 2023 14:35:14 +0000 Subject: [PATCH 250/435] =?UTF-8?q?Do=20not=20send=20tracers=20when=20runn?= =?UTF-8?q?ing=20second=20iteration=20of=20cluster=20nodes=20di=E2=80=A6?= =?UTF-8?q?=20(#2525)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sending tracers is not necessary because we just connected to the nodes a few seconds ago. It causes problems because sent tracers do not have enough time to respond. --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ConnectionMultiplexer.cs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b238fe10c..09884bf77 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -18,6 +18,7 @@ Current package versions: - Change: Target net6.0 instead of net5.0, since net5.0 is end of life. ([#2497 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2497)) - Fix: Fix nullability annotation of IConnectionMultiplexer.RegisterProfiler ([#2494 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2494)) +- Fix [#2520](https://github.com/StackExchange/StackExchange.Redis/issues/2520): Improve cluster connections in down scenarios by not re-pinging successful nodes ([#2525 by Matiszak](https://github.com/StackExchange/StackExchange.Redis/pull/2525)) - Add: `Timer.ActiveCount` under `POOL` in timeout messages on .NET 6+ to help diagnose timer overload affecting timeout evaluations ([#2500 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2500)) - Add: `LibraryName` configuration option; allows the library name to be controlled at the individual options level (in addition to the existing controls in `DefaultOptionsProvider`) ([#2502 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2502)) - Add: `DefaultOptionsProvider.GetProvider` allows lookup of provider by endpoint ([#2502 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2502)) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 64267aa2f..8f50b7fbc 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1487,8 +1487,9 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog servers[i] = server; // This awaits either the endpoint's initial connection, or a tracer if we're already connected - // (which is the reconfigure case) - available[i] = server.OnConnectedAsync(log, sendTracerIfConnected: true, autoConfigureIfConnected: reconfigureAll); + // (which is the reconfigure case, except second iteration which is only for newly discovered cluster members). + var isFirstIteration = iter == 0; + available[i] = server.OnConnectedAsync(log, sendTracerIfConnected: isFirstIteration, autoConfigureIfConnected: reconfigureAll); } watch ??= ValueStopwatch.StartNew(); From 4a13caf4b126d149e549cf6a0f6893a93de6ba4b Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 30 Oct 2023 10:47:02 -0400 Subject: [PATCH 251/435] Logging: fix race in disposal of a passed-in TextWriter (#2581) Occasionally we'd see `chunkLength` errors from `StringWriter` `.ToString()` calls after connecting. I think we've isolated this (via test stress runs) down to a write happening post-lock on the `TextWriterLogger` disposal. This lock in dispose ensures we're not trying to write to a writer we should have fully released at the end of a `.Connect()`/`.ConnectAsync()` call. --- .../ConnectionMultiplexer.cs | 4 +-- src/StackExchange.Redis/TextWriterLogger.cs | 25 +++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 8f50b7fbc..44cc9f7c7 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -638,7 +638,7 @@ private static async Task ConnectImplAsync(ConfigurationO { if (connectHandler != null && muxer != null) muxer.ConnectionFailed -= connectHandler; if (killMe != null) try { killMe.Dispose(); } catch { } - if (log is TextWriterLogger twLogger) twLogger.Dispose(); + if (log is TextWriterLogger twLogger) twLogger.Release(); } } @@ -740,7 +740,7 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat { if (connectHandler != null && muxer != null) muxer.ConnectionFailed -= connectHandler; if (killMe != null) try { killMe.Dispose(); } catch { } - if (log is TextWriterLogger twLogger) twLogger.Dispose(); + if (log is TextWriterLogger twLogger) twLogger.Release(); } } diff --git a/src/StackExchange.Redis/TextWriterLogger.cs b/src/StackExchange.Redis/TextWriterLogger.cs index 4a4a99fd4..0582b70d3 100644 --- a/src/StackExchange.Redis/TextWriterLogger.cs +++ b/src/StackExchange.Redis/TextWriterLogger.cs @@ -4,10 +4,10 @@ namespace StackExchange.Redis; -internal sealed class TextWriterLogger : ILogger, IDisposable +internal sealed class TextWriterLogger : ILogger { private TextWriter? _writer; - private ILogger? _wrapped; + private readonly ILogger? _wrapped; internal static Action NullWriter = _ => { }; @@ -26,16 +26,27 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except { lock (writer) { - writer.Write($"{DateTime.UtcNow:HH:mm:ss.ffff}: "); - writer.WriteLine(formatter(state, exception)); + // We check here again because it's possible we've released below, and never want to write past releasing. + if (_writer is TextWriter innerWriter) + { + innerWriter.Write($"{DateTime.UtcNow:HH:mm:ss.ffff}: "); + innerWriter.WriteLine(formatter(state, exception)); + } } } } - public void Dispose() + public void Release() { - _writer = null; - _wrapped = null; + // We lock here because we may have piled up on a lock above and still be writing. + // We never want a write to go past the Release(), as many TextWriter implementations are not thread safe. + if (_writer is TextWriter writer) + { + lock (writer) + { + _writer = null; + } + } } } From 8f7e04077b4e84b5afcf341e3f14b2add26681c1 Mon Sep 17 00:00:00 2001 From: Rifhan Akram Date: Mon, 30 Oct 2023 20:19:01 +0530 Subject: [PATCH 252/435] Documentation: Update Timeouts.md to include new Timers info in the log (#2568) With this change https://github.com/StackExchange/StackExchange.Redis/pull/2500 active Timers count is now part of the log --- docs/Timeouts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Timeouts.md b/docs/Timeouts.md index 345f25dc5..1c4ac3756 100644 --- a/docs/Timeouts.md +++ b/docs/Timeouts.md @@ -96,7 +96,7 @@ By default Redis Timeout exception(s) includes useful information, which can hel |mgr | 8 of 10 available|Redis Internal Dedicated Thread Pool State| |IOCP | IOCP: (Busy=0,Free=500,Min=248,Max=500)| Runtime Global Thread Pool IO Threads. | |WORKER | WORKER: (Busy=170,Free=330,Min=248,Max=500)| Runtime Global Thread Pool Worker Threads.| -|POOL | POOL: (Threads=8,QueuedItems=0,CompletedItems=42)| Thread Pool Work Item Stats.| +|POOL | POOL: (Threads=8,QueuedItems=0,CompletedItems=42,Timers=10)| Thread Pool Work Item Stats.| |v | Redis Version: version |The `StackExchange.Redis` version you are currently using in your application.| |active | Message-Current: {string} |Included in exception message when `IncludeDetailInExceptions=True` on multiplexer| |next | Message-Next: {string} |When `IncludeDetailInExceptions=True` on multiplexer, it might include command and key, otherwise only command.| From c05179fd542d120650239bda8bd151f4da753115 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 31 Oct 2023 10:46:35 -0400 Subject: [PATCH 253/435] Tests: Increase overall stability (#2548) Fixes: - `DisconnectAndNoReconnectThrowsConnectionExceptionAsync`: Under load, we're not connecting in 50ms for the initial thing, and stability > speed, so give a little on this test to help out. Average runtime will be higher, but that's way better than sporadically failing. - `Roles`: Don't assume we're in the initial primary/replica setup (we may have failed over) and make it work either way. --- .../AbortOnConnectFailTests.cs | 7 ++- .../Helpers/Extensions.cs | 7 +++ tests/StackExchange.Redis.Tests/RoleTests.cs | 59 +++++++++++++++---- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs index 6bf0a8c58..cfb96a547 100644 --- a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs +++ b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs @@ -1,4 +1,5 @@ -using System; +using StackExchange.Redis.Tests.Helpers; +using System; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -92,9 +93,9 @@ private ConnectionMultiplexer GetWorkingBacklogConn() => { AbortOnConnectFail = false, BacklogPolicy = policy, - ConnectTimeout = 50, + ConnectTimeout = 500, SyncTimeout = 400, KeepAlive = 400, AllowAdmin = true, - }; + }.WithoutSubscriptions(); } diff --git a/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs b/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs index 1d5f8f91c..052129a9a 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using Xunit.Abstractions; @@ -26,4 +27,10 @@ static Extensions() } public static void WriteFrameworkVersion(this ITestOutputHelper output) => output.WriteLine(VersionInfo); + + public static ConfigurationOptions WithoutSubscriptions(this ConfigurationOptions options) + { + options.CommandMap = CommandMap.Create(new HashSet() { nameof(RedisCommand.SUBSCRIBE) }, available: false); + return options; + } } diff --git a/tests/StackExchange.Redis.Tests/RoleTests.cs b/tests/StackExchange.Redis.Tests/RoleTests.cs index 405ac39b9..396877283 100644 --- a/tests/StackExchange.Redis.Tests/RoleTests.cs +++ b/tests/StackExchange.Redis.Tests/RoleTests.cs @@ -1,4 +1,5 @@ -using Xunit; +using System.Linq; +using Xunit; using Xunit.Abstractions; namespace StackExchange.Redis.Tests; @@ -8,38 +9,70 @@ public class Roles : TestBase { public Roles(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + [Theory] [InlineData(true)] [InlineData(false)] public void PrimaryRole(bool allowAdmin) // should work with or without admin now { using var conn = Create(allowAdmin: allowAdmin); - var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); - + var servers = conn.GetServers(); + Log("Server list:"); + foreach (var s in servers) + { + Log($" Server: {s.EndPoint} (isConnected: {s.IsConnected}, isReplica: {s.IsReplica})"); + } + var server = servers.First(conn => !conn.IsReplica); var role = server.Role(); + Log($"Chosen primary: {server.EndPoint} (role: {role})"); + if (allowAdmin) + { + Log($"Info (Replication) dump for {server.EndPoint}:"); + Log(server.InfoRaw("Replication")); + Log(""); + + foreach (var s in servers) + { + if (s.IsReplica) + { + Log($"Info (Replication) dump for {s.EndPoint}:"); + Log(s.InfoRaw("Replication")); + Log(""); + } + } + } Assert.NotNull(role); Assert.Equal(role.Value, RedisLiterals.master); var primary = role as Role.Master; Assert.NotNull(primary); Assert.NotNull(primary.Replicas); - Log($"Searching for: {TestConfig.Current.ReplicaServer}:{TestConfig.Current.ReplicaPort}"); - Log($"Replica count: {primary.Replicas.Count}"); - Assert.NotEmpty(primary.Replicas); - foreach (var replica in primary.Replicas) + + // Only do this check for Redis > 4 (to exclude Redis 3.x on Windows). + // Unrelated to this test, the replica isn't connecting and we'll revisit swapping the server out. + // TODO: MemuraiDeveloper check + if (server.Version > RedisFeatures.v4_0_0) { - Log($" Replica: {replica.Ip}:{replica.Port} (offset: {replica.ReplicationOffset})"); - Log(replica.ToString()); + Log($"Searching for: {TestConfig.Current.ReplicaServer}:{TestConfig.Current.ReplicaPort}"); + Log($"Replica count: {primary.Replicas.Count}"); + + Assert.NotEmpty(primary.Replicas); + foreach (var replica in primary.Replicas) + { + Log($" Replica: {replica.Ip}:{replica.Port} (offset: {replica.ReplicationOffset})"); + Log(replica.ToString()); + } + Assert.Contains(primary.Replicas, r => + r.Ip == TestConfig.Current.ReplicaServer && + r.Port == TestConfig.Current.ReplicaPort); } - Assert.Contains(primary.Replicas, r => - r.Ip == TestConfig.Current.ReplicaServer && - r.Port == TestConfig.Current.ReplicaPort); } [Fact] public void ReplicaRole() { using var conn = ConnectionMultiplexer.Connect($"{TestConfig.Current.ReplicaServerAndPort},allowAdmin=true"); - var server = conn.GetServer(TestConfig.Current.ReplicaServerAndPort); + var server = conn.GetServers().First(conn => conn.IsReplica); var role = server.Role(); Assert.NotNull(role); From 34ba53c1991ae01503a50835e4f682552e4c82d5 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 31 Oct 2023 11:00:17 -0400 Subject: [PATCH 254/435] Add release notes for 2.7.4 --- docs/ReleaseNotes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 09884bf77..3ed8f7de6 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,10 @@ Current package versions: ## Unreleased +No pending unreleased changes. + +## 2.7.4 + - Adds: RESP3 support ([#2396 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2396)) - see https://stackexchange.github.io/StackExchange.Redis/Resp3 - Fix [#2507](https://github.com/StackExchange/StackExchange.Redis/issues/2507): Pub/sub with multi-item payloads should be usable ([#2508 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2508)) - Add: connection-id tracking (internal only, no public API) ([#2508 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2508)) From 3ff58ccfb15b297ed39ca7858268ea0987b6ed2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Je=C5=BE?= Date: Sun, 19 Nov 2023 21:09:03 +0100 Subject: [PATCH 255/435] Fix HashDelete return value documentation (#2599) fix #2596 --- src/StackExchange.Redis/Interfaces/IDatabase.cs | 2 +- src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index d0d01a201..e5c120eb9 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -300,7 +300,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the hash. /// The field in the hash to delete. /// The flags to use for this operation. - /// The number of fields that were removed. + /// if the field was removed. /// bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 7f90cf1a5..4a9cd400d 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -287,7 +287,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the hash. /// The field in the hash to delete. /// The flags to use for this operation. - /// The number of fields that were removed. + /// if the field was removed. /// Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); From 79e8346cde2df5fab95d749bc69a8a5333552ecd Mon Sep 17 00:00:00 2001 From: Steve Lorello <42971704+slorello89@users.noreply.github.com> Date: Tue, 21 Nov 2023 10:56:08 -0500 Subject: [PATCH 256/435] properly categorizing EXPIRETIME and PEXPIRETIME (#2593) * properly categorizing EXPIRETIME and PEXPIRETIME * release notes --- docs/ReleaseNotes.md | 2 +- src/StackExchange.Redis/Enums/RedisCommand.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 3ed8f7de6..36d5e7991 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -No pending unreleased changes. +- Fix [#2593](https://github.com/StackExchange/StackExchange.Redis/pull/2593): `EXPIRETIME` and `PEXPIRETIME` miscategorized as `PrimaryOnly` commands causing them to fail when issued against a read-only replica. ## 2.7.4 diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 884114139..728b88a76 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -272,7 +272,6 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.DEL: case RedisCommand.EXPIRE: case RedisCommand.EXPIREAT: - case RedisCommand.EXPIRETIME: case RedisCommand.FLUSHALL: case RedisCommand.FLUSHDB: case RedisCommand.GEOSEARCHSTORE: @@ -304,7 +303,6 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.PERSIST: case RedisCommand.PEXPIRE: case RedisCommand.PEXPIREAT: - case RedisCommand.PEXPIRETIME: case RedisCommand.PFADD: case RedisCommand.PFMERGE: case RedisCommand.PSETEX: @@ -368,6 +366,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.EVALSHA_RO: case RedisCommand.EXEC: case RedisCommand.EXISTS: + case RedisCommand.EXPIRETIME: case RedisCommand.GEODIST: case RedisCommand.GEOHASH: case RedisCommand.GEOPOS: @@ -402,6 +401,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.MONITOR: case RedisCommand.MULTI: case RedisCommand.OBJECT: + case RedisCommand.PEXPIRETIME: case RedisCommand.PFCOUNT: case RedisCommand.PING: case RedisCommand.PSUBSCRIBE: From 510b5e21b256b5d4226c79062b8ecfc4d2265817 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 22 Nov 2023 20:36:14 -0500 Subject: [PATCH 257/435] Sentinel: Add HELLO support (#2601) This allows connections through Sentinel via RESP3 by adding `HELLO` to the command map. Fixes #2591. --- docs/ReleaseNotes.md | 3 ++- src/StackExchange.Redis/CommandMap.cs | 2 +- tests/StackExchange.Redis.Tests/SentinelTests.cs | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 36d5e7991..a657ee394 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,8 @@ Current package versions: ## Unreleased -- Fix [#2593](https://github.com/StackExchange/StackExchange.Redis/pull/2593): `EXPIRETIME` and `PEXPIRETIME` miscategorized as `PrimaryOnly` commands causing them to fail when issued against a read-only replica. +- Fix [#2593](https://github.com/StackExchange/StackExchange.Redis/pull/2593): `EXPIRETIME` and `PEXPIRETIME` miscategorized as `PrimaryOnly` commands causing them to fail when issued against a read-only replica ([#2593 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2593)) +- Fix [#2591](https://github.com/StackExchange/StackExchange.Redis/pull/2591): Add `HELLO` to Sentinel connections so they can support RESP3 ([#2601 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2601)) ## 2.7.4 diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index a12828033..0a42d3e34 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -93,7 +93,7 @@ public sealed class CommandMap /// /// public static CommandMap Sentinel { get; } = Create(new HashSet { - "auth", "ping", "info", "role", "sentinel", "subscribe", "shutdown", "psubscribe", "unsubscribe", "punsubscribe" }, true); + "auth", "hello", "ping", "info", "role", "sentinel", "subscribe", "shutdown", "psubscribe", "unsubscribe", "punsubscribe" }, true); /// /// Create a new , customizing some commands. diff --git a/tests/StackExchange.Redis.Tests/SentinelTests.cs b/tests/StackExchange.Redis.Tests/SentinelTests.cs index 518441ef0..49e96a82d 100644 --- a/tests/StackExchange.Redis.Tests/SentinelTests.cs +++ b/tests/StackExchange.Redis.Tests/SentinelTests.cs @@ -86,6 +86,7 @@ public async Task PrimaryConnectAsyncTest() } [Fact] + [RunPerProtocol] public void SentinelConnectTest() { var options = ServiceOptions.Clone(); From 76f520515ab67a846a1ea685200a25cf90827b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Ate=C5=9F=20UZUN?= Date: Fri, 8 Dec 2023 17:03:18 +0300 Subject: [PATCH 258/435] fix: RedisLiterals.PlusSymbol typo (#2612) --- src/StackExchange.Redis/RedisDatabase.cs | 2 +- src/StackExchange.Redis/RedisLiterals.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 85cf25576..6a0210e6b 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -3449,7 +3449,7 @@ private static RedisValue GetLexRange(RedisValue value, Exclude exclude, bool is { if (value.IsNull) { - return isStart ? RedisLiterals.MinusSymbol : RedisLiterals.PlusSumbol; + return isStart ? RedisLiterals.MinusSymbol : RedisLiterals.PlusSymbol; } byte[] orig = value!; diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 7f786c4d7..bfd6b1a44 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -168,7 +168,7 @@ public static readonly RedisValue sync = "sync", MinusSymbol = "-", - PlusSumbol = "+", + PlusSymbol = "+", Wildcard = "*", // Geo Radius/Search Literals From 9a3003fa7d3220d406f8bed68ec4f4d9b9d26d8e Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 8 Dec 2023 17:22:59 -0500 Subject: [PATCH 259/435] Connections: detect and handle the Linux dead socket case (#2610) In Linux, we see 15 minute socket stalls due to OS-level TCP retries. What this means is the PhysicalConnection detects no issues on the pipe that's retrying, but is also not receiving data at all leading to long stalls in client applications. The goal here is to detect that we're timing out commands people have issued to the connection but we're getting _NOTHING_ back on the socket at all. In this case, we should assume the socket is dead and issue a reconnect so that we get out of the hung situation much faster. For an initial go at this, we've chosen 4x the timeout interval as a threshold, but could make this configurable if needed. --- docs/ReleaseNotes.md | 5 +++-- src/StackExchange.Redis/PhysicalBridge.cs | 13 ++++++++++++- src/StackExchange.Redis/PhysicalConnection.cs | 10 +++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index a657ee394..bf27b5eca 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,8 +8,9 @@ Current package versions: ## Unreleased -- Fix [#2593](https://github.com/StackExchange/StackExchange.Redis/pull/2593): `EXPIRETIME` and `PEXPIRETIME` miscategorized as `PrimaryOnly` commands causing them to fail when issued against a read-only replica ([#2593 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2593)) -- Fix [#2591](https://github.com/StackExchange/StackExchange.Redis/pull/2591): Add `HELLO` to Sentinel connections so they can support RESP3 ([#2601 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2601)) +- Fix [#2593](https://github.com/StackExchange/StackExchange.Redis/issues/2593): `EXPIRETIME` and `PEXPIRETIME` miscategorized as `PrimaryOnly` commands causing them to fail when issued against a read-only replica ([#2593 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2593)) +- Fix [#2591](https://github.com/StackExchange/StackExchange.Redis/issues/2591): Add `HELLO` to Sentinel connections so they can support RESP3 ([#2601 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2601)) +- Fix [#2595](https://github.com/StackExchange/StackExchange.Redis/issues/2595): Add detection handling for dead sockets that the OS says are okay, seen especially in Linux environments (https://github.com/StackExchange/StackExchange.Redis/pull/2610) ## 2.7.4 diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 3a4494821..bf49e6e30 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -591,7 +591,7 @@ internal void OnHeartbeat(bool ifConnectedOnly) Interlocked.Exchange(ref connectTimeoutRetryCount, 0); tmp.BridgeCouldBeNull?.ServerEndPoint?.ClearUnselectable(UnselectableFlags.DidNotRespond); } - tmp.OnBridgeHeartbeat(); + int timedOutThisHeartbeat = tmp.OnBridgeHeartbeat(); int writeEverySeconds = ServerEndPoint.WriteEverySeconds, checkConfigSeconds = ServerEndPoint.ConfigCheckSeconds; @@ -623,6 +623,17 @@ internal void OnHeartbeat(bool ifConnectedOnly) // queue, test the socket KeepAlive(); } + else if (timedOutThisHeartbeat > 0 + && tmp.LastReadSecondsAgo * 1_000 > (tmp.BridgeCouldBeNull?.Multiplexer.AsyncTimeoutMilliseconds * 4)) + { + // If we've received *NOTHING* on the pipe in 4 timeouts worth of time and we're timing out commands, issue a connection failure so that we reconnect + // This is meant to address the scenario we see often in Linux configs where TCP retries will happen for 15 minutes. + // To us as a client, we'll see the socket as green/open/fine when writing but we'll bet getting nothing back. + // Since we can't depend on the pipe to fail in that case, we want to error here based on the criteria above so we reconnect broken clients much faster. + tmp.BridgeCouldBeNull?.Multiplexer.Logger?.LogWarning($"Dead socket detected, no reads in {tmp.LastReadSecondsAgo} seconds with {timedOutThisHeartbeat} timeouts, issuing disconnect"); + OnDisconnected(ConnectionFailureType.SocketFailure, tmp, out _, out State oldState); + tmp.Dispose(); // Cleanup the existing connection/socket if any, otherwise it will wait reading indefinitely + } } break; case (int)State.Disconnected: diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 22c9d2894..2ed2a1819 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -248,6 +248,7 @@ private enum ReadMode : byte private readonly WeakReference _bridge; public PhysicalBridge? BridgeCouldBeNull => (PhysicalBridge?)_bridge.Target; + public long LastReadSecondsAgo => unchecked(Environment.TickCount - Thread.VolatileRead(ref lastReadTickCount)) / 1000; public long LastWriteSecondsAgo => unchecked(Environment.TickCount - Thread.VolatileRead(ref lastWriteTickCount)) / 1000; private bool IncludeDetailInExceptions => BridgeCouldBeNull?.Multiplexer.RawConfig.IncludeDetailInExceptions ?? false; @@ -720,8 +721,13 @@ internal void GetStormLog(StringBuilder sb) } } - internal void OnBridgeHeartbeat() + /// + /// Runs on every heartbeat for a bridge, timing out any commands that are overdue and returning an integer of how many we timed out. + /// + /// How many commands were overdue and threw timeout exceptions. + internal int OnBridgeHeartbeat() { + var result = 0; var now = Environment.TickCount; Interlocked.Exchange(ref lastBeatTickCount, now); @@ -747,6 +753,7 @@ internal void OnBridgeHeartbeat() multiplexer.OnMessageFaulted(msg, timeoutEx); msg.SetExceptionAndComplete(timeoutEx, bridge); // tell the message that it is doomed multiplexer.OnAsyncTimeout(); + result++; } } else @@ -761,6 +768,7 @@ internal void OnBridgeHeartbeat() } } } + return result; } internal void OnInternalError(Exception exception, [CallerMemberName] string? origin = null) From f99e95f7563323f8e44250d3f0cae3432a94d760 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 12 Dec 2023 11:01:27 -0500 Subject: [PATCH 260/435] Add 2.7.10 release notes --- docs/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index bf27b5eca..a5fbab67b 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,6 +7,9 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased +No unreleased changes yet! + +## 2.7.10 - Fix [#2593](https://github.com/StackExchange/StackExchange.Redis/issues/2593): `EXPIRETIME` and `PEXPIRETIME` miscategorized as `PrimaryOnly` commands causing them to fail when issued against a read-only replica ([#2593 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2593)) - Fix [#2591](https://github.com/StackExchange/StackExchange.Redis/issues/2591): Add `HELLO` to Sentinel connections so they can support RESP3 ([#2601 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2601)) From 1943d953c3b8d8259623e7860cdc5a48bf641178 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 20 Dec 2023 10:36:29 +0000 Subject: [PATCH 261/435] note shipped APIs (#2620) --- .../PublicAPI/PublicAPI.Shipped.txt | 31 ++++++++++++++++++ .../PublicAPI/PublicAPI.Unshipped.txt | 32 +------------------ 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 459f66cf8..cded72738 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1812,3 +1812,34 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.SetClientLibrar virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.SyncTimeout.get -> System.TimeSpan virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.TieBreaker.get -> string! virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.User.get -> string? +abstract StackExchange.Redis.RedisResult.ToString(out string? type) -> string? +override sealed StackExchange.Redis.RedisResult.ToString() -> string! +override StackExchange.Redis.Role.Master.Replica.ToString() -> string! +StackExchange.Redis.ClientInfo.Protocol.get -> StackExchange.Redis.RedisProtocol? +StackExchange.Redis.ConfigurationOptions.Protocol.get -> StackExchange.Redis.RedisProtocol? +StackExchange.Redis.ConfigurationOptions.Protocol.set -> void +StackExchange.Redis.IServer.Protocol.get -> StackExchange.Redis.RedisProtocol +StackExchange.Redis.RedisFeatures.ClientId.get -> bool +StackExchange.Redis.RedisFeatures.Equals(StackExchange.Redis.RedisFeatures other) -> bool +StackExchange.Redis.RedisFeatures.Resp3.get -> bool +StackExchange.Redis.RedisProtocol +StackExchange.Redis.RedisProtocol.Resp2 = 20000 -> StackExchange.Redis.RedisProtocol +StackExchange.Redis.RedisProtocol.Resp3 = 30000 -> StackExchange.Redis.RedisProtocol +StackExchange.Redis.RedisResult.Resp2Type.get -> StackExchange.Redis.ResultType +StackExchange.Redis.RedisResult.Resp3Type.get -> StackExchange.Redis.ResultType +StackExchange.Redis.RedisResult.Type.get -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Array = 5 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Attribute = 29 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.BigInteger = 17 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.BlobError = 10 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Boolean = 11 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Double = 9 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Map = 13 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Null = 8 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Push = 37 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.Set = 21 -> StackExchange.Redis.ResultType +StackExchange.Redis.ResultType.VerbatimString = 12 -> StackExchange.Redis.ResultType +static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisResult![]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult! +static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult! +virtual StackExchange.Redis.RedisResult.Length.get -> int +virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult! \ No newline at end of file diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index eff457070..5f282702b 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,31 +1 @@ -abstract StackExchange.Redis.RedisResult.ToString(out string? type) -> string? -override sealed StackExchange.Redis.RedisResult.ToString() -> string! -override StackExchange.Redis.Role.Master.Replica.ToString() -> string! -StackExchange.Redis.ClientInfo.Protocol.get -> StackExchange.Redis.RedisProtocol? -StackExchange.Redis.ConfigurationOptions.Protocol.get -> StackExchange.Redis.RedisProtocol? -StackExchange.Redis.ConfigurationOptions.Protocol.set -> void -StackExchange.Redis.IServer.Protocol.get -> StackExchange.Redis.RedisProtocol -StackExchange.Redis.RedisFeatures.ClientId.get -> bool -StackExchange.Redis.RedisFeatures.Equals(StackExchange.Redis.RedisFeatures other) -> bool -StackExchange.Redis.RedisFeatures.Resp3.get -> bool -StackExchange.Redis.RedisProtocol -StackExchange.Redis.RedisProtocol.Resp2 = 20000 -> StackExchange.Redis.RedisProtocol -StackExchange.Redis.RedisProtocol.Resp3 = 30000 -> StackExchange.Redis.RedisProtocol -StackExchange.Redis.RedisResult.Resp2Type.get -> StackExchange.Redis.ResultType -StackExchange.Redis.RedisResult.Resp3Type.get -> StackExchange.Redis.ResultType -StackExchange.Redis.RedisResult.Type.get -> StackExchange.Redis.ResultType -StackExchange.Redis.ResultType.Array = 5 -> StackExchange.Redis.ResultType -StackExchange.Redis.ResultType.Attribute = 29 -> StackExchange.Redis.ResultType -StackExchange.Redis.ResultType.BigInteger = 17 -> StackExchange.Redis.ResultType -StackExchange.Redis.ResultType.BlobError = 10 -> StackExchange.Redis.ResultType -StackExchange.Redis.ResultType.Boolean = 11 -> StackExchange.Redis.ResultType -StackExchange.Redis.ResultType.Double = 9 -> StackExchange.Redis.ResultType -StackExchange.Redis.ResultType.Map = 13 -> StackExchange.Redis.ResultType -StackExchange.Redis.ResultType.Null = 8 -> StackExchange.Redis.ResultType -StackExchange.Redis.ResultType.Push = 37 -> StackExchange.Redis.ResultType -StackExchange.Redis.ResultType.Set = 21 -> StackExchange.Redis.ResultType -StackExchange.Redis.ResultType.VerbatimString = 12 -> StackExchange.Redis.ResultType -static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisResult![]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult! -static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult! -virtual StackExchange.Redis.RedisResult.Length.get -> int -virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult! \ No newline at end of file + \ No newline at end of file From d60d987e2a738307c7497240740b80cfe571dd7b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 21 Dec 2023 13:44:47 +0000 Subject: [PATCH 262/435] add type-forward on IsExternalInit (#2621) * add type-forward on IsExternalInit to resolve #2619 --- Directory.Packages.props | 2 +- docs/ReleaseNotes.md | 3 ++- src/StackExchange.Redis/Hacks.cs | 7 ++++--- .../PublicAPI/net6.0/PublicAPI.Shipped.txt | 3 +++ src/StackExchange.Redis/StackExchange.Redis.csproj | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt diff --git a/Directory.Packages.props b/Directory.Packages.props index 2280f9df2..9ef9afb25 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,7 +11,7 @@ - + diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index a5fbab67b..e98ed8ba9 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,7 +7,8 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased -No unreleased changes yet! + +- Fix [#2619](https://github.com/StackExchange/StackExchange.Redis/issues/2619): Type-forward `IsExternalInit` to support down-level TFMs ([#2621 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2621)) ## 2.7.10 diff --git a/src/StackExchange.Redis/Hacks.cs b/src/StackExchange.Redis/Hacks.cs index 411a796d5..8dda522a3 100644 --- a/src/StackExchange.Redis/Hacks.cs +++ b/src/StackExchange.Redis/Hacks.cs @@ -1,5 +1,7 @@ -#if !NET5_0_OR_GREATER - +#if NET5_0_OR_GREATER +// context: https://github.com/StackExchange/StackExchange.Redis/issues/2619 +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))] +#else // To support { get; init; } properties using System.ComponentModel; @@ -8,5 +10,4 @@ namespace System.Runtime.CompilerServices [EditorBrowsable(EditorBrowsableState.Never)] internal static class IsExternalInit { } } - #endif diff --git a/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt new file mode 100644 index 000000000..599891ac2 --- /dev/null +++ b/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt @@ -0,0 +1,3 @@ +StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.get -> System.Func? +StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.set -> void +System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime) \ No newline at end of file diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 8cdc20730..06c2bc8a7 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -35,7 +35,7 @@ - + From 023358528746a617567dffa1857186691aaab198 Mon Sep 17 00:00:00 2001 From: Weihan Li Date: Thu, 21 Dec 2023 23:01:44 +0800 Subject: [PATCH 263/435] Simplify InternalsVisibleTo (#2623) * Simplify InternalsVisibleTo * move PublicKey to Directory.Build.props * docs: add release notes entry --------- Co-authored-by: Marc Gravell --- Directory.Build.props | 1 + docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/StackExchange.Redis.csproj | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 9f3216a7b..43dd33169 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,6 +26,7 @@ true false true + 00240000048000009400000006020000002400005253413100040000010001007791a689e9d8950b44a9a8886baad2ea180e7a8a854f158c9b98345ca5009cdd2362c84f368f1c3658c132b3c0f74e44ff16aeb2e5b353b6e0fe02f923a050470caeac2bde47a2238a9c7125ed7dab14f486a5a64558df96640933b9f2b6db188fc4a820f96dce963b662fa8864adbff38e5b4542343f162ecdc6dad16912fff true diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index e98ed8ba9..e9e6e10ca 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Fix [#2619](https://github.com/StackExchange/StackExchange.Redis/issues/2619): Type-forward `IsExternalInit` to support down-level TFMs ([#2621 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2621)) +- `InternalsVisibleTo` `PublicKey` enhancements([#2623 by WeihanLi](https://github.com/StackExchange/StackExchange.Redis/pull/2623)) ## 2.7.10 diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 06c2bc8a7..ba7c31c35 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -39,7 +39,7 @@ - - + + \ No newline at end of file From e8b00069bb18fb5ac78d9d64e892dbd1473a4f31 Mon Sep 17 00:00:00 2001 From: Steve Lorello <42971704+slorello89@users.noreply.github.com> Date: Thu, 21 Dec 2023 10:04:29 -0500 Subject: [PATCH 264/435] Honor select disposition in transactions (#2322) * honor select disposition in transactions * rearranging if/switch * using server instead of multiplxer * release notes --- docs/ReleaseNotes.md | 2 +- src/StackExchange.Redis/RedisTransaction.cs | 22 +++++++++++-------- .../TransactionTests.cs | 16 ++++++++++++++ 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index e9e6e10ca..3941c9c85 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -78,7 +78,7 @@ Current package versions: - Fix [#2249](https://github.com/StackExchange/StackExchange.Redis/issues/2249): Properly handle a `fail` state (new `ClusterNode.IsFail` property) for `CLUSTER NODES` and expose `fail?` as a property (`IsPossiblyFail`) as well ([#2288 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2288)) - Adds: `IConnectionMultiplexer.ServerMaintenanceEvent` (was on `ConnectionMultiplexer` but not the interface) ([#2306 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2306)) - Adds: To timeout messages, additional debug information: `Sync-Ops` (synchronous operations), `Async-Ops` (asynchronous operations), and `Server-Connected-Seconds` (how long the connection in question has been connected, or `"n/a"`) ([#2300 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2300)) - +- Fix: [#2321](https://github.com/StackExchange/StackExchange.Redis/issues/2321): Honor disposition of select command in Command Map for transactions [(#2322 by slorello89)](https://github.com/StackExchange/StackExchange.Redis/pull/2322) ## 2.6.80 diff --git a/src/StackExchange.Redis/RedisTransaction.cs b/src/StackExchange.Redis/RedisTransaction.cs index ea71e7dd1..7f0d1a7ec 100644 --- a/src/StackExchange.Redis/RedisTransaction.cs +++ b/src/StackExchange.Redis/RedisTransaction.cs @@ -116,20 +116,24 @@ private void QueueMessage(Message message) lock (SyncLock) { (_pending ??= new List()).Add(queued); - switch (message.Command) { case RedisCommand.UNKNOWN: case RedisCommand.EVAL: case RedisCommand.EVALSHA: - // people can do very naughty things in an EVAL - // including change the DB; change it back to what we - // think it should be! - var sel = PhysicalConnection.GetSelectDatabaseCommand(message.Db); - queued = new QueuedMessage(sel); - wasQueued = SimpleResultBox.Create(); - queued.SetSource(wasQueued, QueuedProcessor.Default); - _pending.Add(queued); + var server = multiplexer.SelectServer(message); + if (server != null && server.SupportsDatabases) + { + // people can do very naughty things in an EVAL + // including change the DB; change it back to what we + // think it should be! + var sel = PhysicalConnection.GetSelectDatabaseCommand(message.Db); + queued = new QueuedMessage(sel); + wasQueued = SimpleResultBox.Create(); + queued.SetSource(wasQueued, QueuedProcessor.Default); + _pending.Add(queued); + } + break; } } diff --git a/tests/StackExchange.Redis.Tests/TransactionTests.cs b/tests/StackExchange.Redis.Tests/TransactionTests.cs index c9ccec71b..ac67961be 100644 --- a/tests/StackExchange.Redis.Tests/TransactionTests.cs +++ b/tests/StackExchange.Redis.Tests/TransactionTests.cs @@ -1219,6 +1219,22 @@ public async Task CombineFireAndForgetAndRegularAsyncInTransaction() Assert.Equal(30, count); } + [Fact] + public async Task TransactionWithAdHocCommandsAndSelectDisabled() + { + using var conn = Create(disabledCommands: new string[] { "SELECT" }); + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + Assert.False(db.KeyExists(key)); + + var tran = db.CreateTransaction("state"); + var a = tran.ExecuteAsync("SET", "foo", "bar"); + Assert.True(await tran.ExecuteAsync()); + var setting = db.StringGet("foo"); + Assert.Equal("bar",setting); + } + #if VERBOSE [Fact] public async Task WatchAbort_StringEqual() From ad2e69fe0bb5db2a8b54e820dce4d3d5cced52af Mon Sep 17 00:00:00 2001 From: Kornel Pal Date: Fri, 22 Dec 2023 10:11:34 +0100 Subject: [PATCH 265/435] Add test for multiplexer GC rooting. (#2427) Co-authored-by: Nick Craver --- .../GarbageCollectionTests.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs b/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs index 0bff4abc5..42a132835 100644 --- a/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs +++ b/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -52,4 +53,51 @@ public async Task MuxerIsCollected() // Assert.Equal(before + 1, after); //#endif } + + [Fact] + public async Task UnrootedBackloggedAsyncTaskIsCompletedOnTimeout() + { + // Run the test on a separate thread without keeping a reference to the task to ensure + // that there are no references to the variables in test task from the main thread. + // WithTimeout must not be used within Task.Run because timers are rooted and would keep everything alive. + var startGC = new TaskCompletionSource(); + Task? completedTestTask = null; + _ = Task.Run(async () => + { + using var conn = await ConnectionMultiplexer.ConnectAsync(new ConfigurationOptions() + { + BacklogPolicy = BacklogPolicy.Default, + AbortOnConnectFail = false, + ConnectTimeout = 50, + SyncTimeout = 1000, + AllowAdmin = true, + EndPoints = { GetConfiguration() }, + }, Writer); + var db = conn.GetDatabase(); + + // Disconnect and don't allow re-connection + conn.AllowConnect = false; + var server = conn.GetServerSnapshot()[0]; + server.SimulateConnectionFailure(SimulatedFailureType.All); + Assert.False(conn.IsConnected); + + var pingTask = Assert.ThrowsAsync(() => db.PingAsync()); + startGC.SetResult(true); + await pingTask; + }).ContinueWith(testTask => Volatile.Write(ref completedTestTask, testTask)); + + // Use sync wait and sleep to ensure a more timely GC. + var timeoutTask = Task.Delay(5000); + Task.WaitAny(startGC.Task, timeoutTask); + while (Volatile.Read(ref completedTestTask) == null && !timeoutTask.IsCompleted) + { + ForceGC(); + Thread.Sleep(200); + } + + var testTask = Volatile.Read(ref completedTestTask); + if (testTask == null) Assert.Fail("Timeout."); + + await testTask; + } } From b2694b385c876793a827bd4d2556439983061877 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 11 Jan 2024 12:33:28 -0500 Subject: [PATCH 266/435] Fix #2576: PhysicalConnection: Better shutdown handling (#2629) This adds a bit of null ref handling (few ifs). Fixes #2576. Overall, this is biting people in the shutdown race, more likely under load, so let's eat the if checks here to prevent it. I decided to go with the specific approach here as to not affect inlining. --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/PhysicalConnection.cs | 54 +++++++++++++------ 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 3941c9c85..ad31188d2 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: - Fix [#2619](https://github.com/StackExchange/StackExchange.Redis/issues/2619): Type-forward `IsExternalInit` to support down-level TFMs ([#2621 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2621)) - `InternalsVisibleTo` `PublicKey` enhancements([#2623 by WeihanLi](https://github.com/StackExchange/StackExchange.Redis/pull/2623)) +- Fix [#2576](https://github.com/StackExchange/StackExchange.Redis/issues/2576): Prevent `NullReferenceException` during shutdown of connections ([#2629 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2629)) ## 2.7.10 diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 2ed2a1819..70bc33c0d 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -790,39 +790,44 @@ internal void Write(in RedisKey key) var val = key.KeyValue; if (val is string s) { - WriteUnifiedPrefixedString(_ioPipe!.Output, key.KeyPrefix, s); + WriteUnifiedPrefixedString(_ioPipe?.Output, key.KeyPrefix, s); } else { - WriteUnifiedPrefixedBlob(_ioPipe!.Output, key.KeyPrefix, (byte[]?)val); + WriteUnifiedPrefixedBlob(_ioPipe?.Output, key.KeyPrefix, (byte[]?)val); } } internal void Write(in RedisChannel channel) - => WriteUnifiedPrefixedBlob(_ioPipe!.Output, ChannelPrefix, channel.Value); + => WriteUnifiedPrefixedBlob(_ioPipe?.Output, ChannelPrefix, channel.Value); [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void WriteBulkString(in RedisValue value) - => WriteBulkString(value, _ioPipe!.Output); - internal static void WriteBulkString(in RedisValue value, PipeWriter output) + => WriteBulkString(value, _ioPipe?.Output); + internal static void WriteBulkString(in RedisValue value, PipeWriter? maybeNullWriter) { + if (maybeNullWriter is not PipeWriter writer) + { + return; // Prevent null refs during disposal + } + switch (value.Type) { case RedisValue.StorageType.Null: - WriteUnifiedBlob(output, (byte[]?)null); + WriteUnifiedBlob(writer, (byte[]?)null); break; case RedisValue.StorageType.Int64: - WriteUnifiedInt64(output, value.OverlappedValueInt64); + WriteUnifiedInt64(writer, value.OverlappedValueInt64); break; case RedisValue.StorageType.UInt64: - WriteUnifiedUInt64(output, value.OverlappedValueUInt64); + WriteUnifiedUInt64(writer, value.OverlappedValueUInt64); break; case RedisValue.StorageType.Double: // use string case RedisValue.StorageType.String: - WriteUnifiedPrefixedString(output, null, (string?)value); + WriteUnifiedPrefixedString(writer, null, (string?)value); break; case RedisValue.StorageType.Raw: - WriteUnifiedSpan(output, ((ReadOnlyMemory)value).Span); + WriteUnifiedSpan(writer, ((ReadOnlyMemory)value).Span); break; default: throw new InvalidOperationException($"Unexpected {value.Type} value: '{value}'"); @@ -833,6 +838,11 @@ internal static void WriteBulkString(in RedisValue value, PipeWriter output) internal void WriteHeader(RedisCommand command, int arguments, CommandBytes commandBytes = default) { + if (_ioPipe?.Output is not PipeWriter writer) + { + return; // Prevent null refs during disposal + } + var bridge = BridgeCouldBeNull ?? throw new ObjectDisposedException(ToString()); if (command == RedisCommand.UNKNOWN) @@ -856,14 +866,14 @@ internal void WriteHeader(RedisCommand command, int arguments, CommandBytes comm // *{argCount}\r\n = 3 + MaxInt32TextLen // ${cmd-len}\r\n = 3 + MaxInt32TextLen // {cmd}\r\n = 2 + commandBytes.Length - var span = _ioPipe!.Output.GetSpan(commandBytes.Length + 8 + Format.MaxInt32TextLen + Format.MaxInt32TextLen); + var span = writer.GetSpan(commandBytes.Length + 8 + Format.MaxInt32TextLen + Format.MaxInt32TextLen); span[0] = (byte)'*'; int offset = WriteRaw(span, arguments + 1, offset: 1); offset = AppendToSpanCommand(span, commandBytes, offset: offset); - _ioPipe.Output.Advance(offset); + writer.Advance(offset); } internal void RecordQuit() // don't blame redis if we fired the first shot @@ -1116,7 +1126,11 @@ private static int AppendToSpan(Span span, ReadOnlySpan value, int o internal void WriteSha1AsHex(byte[] value) { - var writer = _ioPipe!.Output; + if (_ioPipe?.Output is not PipeWriter writer) + { + return; // Prevent null refs during disposal + } + if (value == null) { writer.Write(NullBulkString.Span); @@ -1156,8 +1170,13 @@ internal static byte ToHexNibble(int value) return value < 10 ? (byte)('0' + value) : (byte)('a' - 10 + value); } - internal static void WriteUnifiedPrefixedString(PipeWriter writer, byte[]? prefix, string? value) + internal static void WriteUnifiedPrefixedString(PipeWriter? maybeNullWriter, byte[]? prefix, string? value) { + if (maybeNullWriter is not PipeWriter writer) + { + return; // Prevent null refs during disposal + } + if (value == null) { // special case @@ -1259,8 +1278,13 @@ internal static unsafe void WriteRaw(PipeWriter writer, string value, int expect } } - private static void WriteUnifiedPrefixedBlob(PipeWriter writer, byte[]? prefix, byte[]? value) + private static void WriteUnifiedPrefixedBlob(PipeWriter? maybeNullWriter, byte[]? prefix, byte[]? value) { + if (maybeNullWriter is not PipeWriter writer) + { + return; // Prevent null refs during disposal + } + // ${total-len}\r\n // {prefix}{value}\r\n if (prefix == null || prefix.Length == 0 || value == null) From 7dff15d7d4fcf232d976d07275ac29b313ef40cd Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 16 Jan 2024 11:26:13 -0500 Subject: [PATCH 267/435] Add release notes --- docs/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index ad31188d2..36b1706e0 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,6 +7,9 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased +No unreleased changes + +## 2.7.17 - Fix [#2619](https://github.com/StackExchange/StackExchange.Redis/issues/2619): Type-forward `IsExternalInit` to support down-level TFMs ([#2621 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2621)) - `InternalsVisibleTo` `PublicKey` enhancements([#2623 by WeihanLi](https://github.com/StackExchange/StackExchange.Redis/pull/2623)) From d9c9f7b62194999729f63c8125c7b96964431837 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 16 Jan 2024 11:31:50 -0500 Subject: [PATCH 268/435] Fix release notes position for #2322 --- docs/ReleaseNotes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 36b1706e0..46704f235 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -11,6 +11,7 @@ No unreleased changes ## 2.7.17 +- Fix [#2321](https://github.com/StackExchange/StackExchange.Redis/issues/2321): Honor disposition of select command in Command Map for transactions [(#2322 by slorello89)](https://github.com/StackExchange/StackExchange.Redis/pull/2322) - Fix [#2619](https://github.com/StackExchange/StackExchange.Redis/issues/2619): Type-forward `IsExternalInit` to support down-level TFMs ([#2621 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2621)) - `InternalsVisibleTo` `PublicKey` enhancements([#2623 by WeihanLi](https://github.com/StackExchange/StackExchange.Redis/pull/2623)) - Fix [#2576](https://github.com/StackExchange/StackExchange.Redis/issues/2576): Prevent `NullReferenceException` during shutdown of connections ([#2629 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2629)) @@ -82,7 +83,6 @@ No unreleased changes - Fix [#2249](https://github.com/StackExchange/StackExchange.Redis/issues/2249): Properly handle a `fail` state (new `ClusterNode.IsFail` property) for `CLUSTER NODES` and expose `fail?` as a property (`IsPossiblyFail`) as well ([#2288 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2288)) - Adds: `IConnectionMultiplexer.ServerMaintenanceEvent` (was on `ConnectionMultiplexer` but not the interface) ([#2306 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2306)) - Adds: To timeout messages, additional debug information: `Sync-Ops` (synchronous operations), `Async-Ops` (asynchronous operations), and `Server-Connected-Seconds` (how long the connection in question has been connected, or `"n/a"`) ([#2300 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2300)) -- Fix: [#2321](https://github.com/StackExchange/StackExchange.Redis/issues/2321): Honor disposition of select command in Command Map for transactions [(#2322 by slorello89)](https://github.com/StackExchange/StackExchange.Redis/pull/2322) ## 2.6.80 From 39eac0123d9aca1ae2e3f905b15b8b0e6ec0aaa7 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 13 Feb 2024 16:07:31 +0000 Subject: [PATCH 269/435] Support Alibaba pseudo-cluster configurations (#2646) * fix #2642 1: don't treat trivial clusters as clusters - Alibaba uses this config 2: report synchronous failures immidiately and accurately * instead of using node count, use explicit tracking of the DB count * release notes --- docs/ReleaseNotes.md | 3 +- .../ConnectionMultiplexer.cs | 33 ++++++++++--------- src/StackExchange.Redis/PhysicalConnection.cs | 4 +++ src/StackExchange.Redis/ResultProcessor.cs | 11 ++++++- src/StackExchange.Redis/ServerEndPoint.cs | 4 +++ 5 files changed, 38 insertions(+), 17 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 46704f235..5c87ec5bc 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,7 +7,8 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased -No unreleased changes + +- Fix [#2642](https://github.com/StackExchange/StackExchange.Redis/issues/2642): Detect and support multi-DB pseudo-cluster/proxy scenarios ([#2646](https://github.com/StackExchange/StackExchange.Redis/pull/2646) by mgravell) ## 2.7.17 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 44cc9f7c7..022ae8cd9 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1,4 +1,7 @@ -using System; +using Microsoft.Extensions.Logging; +using Pipelines.Sockets.Unofficial; +using StackExchange.Redis.Profiling; +using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; @@ -11,9 +14,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Pipelines.Sockets.Unofficial; -using StackExchange.Redis.Profiling; namespace StackExchange.Redis { @@ -2078,19 +2078,22 @@ internal static void ThrowFailed(TaskCompletionSource? source, Exception u #pragma warning disable CS0618 // Type or member is obsolete result = TryPushMessageToBridgeSync(message, processor, source, ref server); #pragma warning restore CS0618 - if (result != WriteResult.Success) + if (!source.IsFaulted) // if we faulted while writing, we don't need to wait { - throw GetException(result, message, server); - } + if (result != WriteResult.Success) + { + throw GetException(result, message, server); + } - if (Monitor.Wait(source, TimeoutMilliseconds)) - { - Trace("Timely response to " + message); - } - else - { - Trace("Timeout performing " + message); - timeout = true; + if (Monitor.Wait(source, TimeoutMilliseconds)) + { + Trace("Timely response to " + message); + } + else + { + Trace("Timeout performing " + message); + timeout = true; + } } } diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 70bc33c0d..9c467dcd3 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -95,6 +95,9 @@ public PhysicalConnection(PhysicalBridge bridge) OnCreateEcho(); } + // *definitely* multi-database; this can help identify some unusual config scenarios + internal bool MultiDatabasesOverride { get; set; } // switch to flags-enum if more needed later + internal async Task BeginConnectAsync(ILogger? log) { var bridge = BridgeCouldBeNull; @@ -262,6 +265,7 @@ private enum ReadMode : byte private RedisProtocol _protocol; // note starts at **zero**, not RESP2 public RedisProtocol? Protocol => _protocol == 0 ? null : _protocol; + internal void SetProtocol(RedisProtocol value) => _protocol = value; [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 6fd229af1..95dc4bd87 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -929,6 +929,10 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes int dbCount = checked((int)i64); Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (CONFIG) databases: " + dbCount); server.Databases = dbCount; + if (dbCount > 1) + { + connection.MultiDatabasesOverride = true; + } } else if (key.IsEqual(CommonReplies.slave_read_only) || key.IsEqual(CommonReplies.replica_read_only)) { @@ -1108,8 +1112,13 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes case ResultType.BulkString: string nodes = result.GetString()!; var bridge = connection.BridgeCouldBeNull; - if (bridge != null) bridge.ServerEndPoint.ServerType = ServerType.Cluster; var config = Parse(connection, nodes); + + // re multi-db: https://github.com/StackExchange/StackExchange.Redis/issues/2642 + if (bridge != null && !connection.MultiDatabasesOverride) + { + bridge.ServerEndPoint.ServerType = ServerType.Cluster; + } SetResult(message, config); return true; } diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index ebb66ec2a..97230f85f 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -415,6 +415,10 @@ internal async Task AutoConfigureAsync(PhysicalConnection? connection, ILogger? lastInfoReplicationCheckTicks = Environment.TickCount; if (features.InfoSections) { + // note: Redis 7.0 has a multi-section usage, but we don't know + // the server version at this point; we *could* use the optional + // value on the config, but let's keep things simple: these + // commands are suitably cheap msg = Message.Create(-1, flags, RedisCommand.INFO, RedisLiterals.replication); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, autoConfigProcessor).ForAwait(); From e4cdd905480d21cd3d7ee1a65017d4aac5b6a7f5 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 20 Feb 2024 10:37:18 -0500 Subject: [PATCH 270/435] Release notes for 2.7.20 --- docs/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 5c87ec5bc..4a4b1c3fc 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,6 +7,9 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased +No pending unreleased changes. + +## 2.7.20 - Fix [#2642](https://github.com/StackExchange/StackExchange.Redis/issues/2642): Detect and support multi-DB pseudo-cluster/proxy scenarios ([#2646](https://github.com/StackExchange/StackExchange.Redis/pull/2646) by mgravell) From 639287188b419ec3650486192e6a0db86795b958 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 24 Feb 2024 22:16:09 +0000 Subject: [PATCH 271/435] Sanitize client setinfo metadata (#2654) * fix #2653 * Update src/StackExchange.Redis/ServerEndPoint.cs Co-authored-by: Philo * release notes * merge fail --------- Co-authored-by: Philo --- docs/ReleaseNotes.md | 3 ++- src/StackExchange.Redis/ServerEndPoint.cs | 8 ++++++-- .../Issues/Issue2653.cs | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/Issues/Issue2653.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 4a4b1c3fc..2098349e2 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,7 +7,8 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased -No pending unreleased changes. + +- Fix [#2653](https://github.com/StackExchange/StackExchange.Redis/issues/2653): Client library metadata should validate contents ([#2654](https://github.com/StackExchange/StackExchange.Redis/pull/2654) by mgravell) ## 2.7.20 diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 97230f85f..a5471b5d5 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -26,7 +26,7 @@ internal sealed partial class ServerEndPoint : IDisposable { internal volatile ServerEndPoint? Primary; internal volatile ServerEndPoint[] Replicas = Array.Empty(); - private static readonly Regex nameSanitizer = new Regex("[^!-~]", RegexOptions.Compiled); + private static readonly Regex nameSanitizer = new Regex("[^!-~]+", RegexOptions.Compiled); private readonly Hashtable knownScripts = new Hashtable(StringComparer.Ordinal); @@ -1024,6 +1024,8 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) // it, they should set SetClientLibrary to false, not set the name to empty string) libName = config.Defaults.LibraryName; } + + libName = ClientInfoSanitize(libName); if (!string.IsNullOrWhiteSpace(libName)) { msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, @@ -1032,7 +1034,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } - var version = Utils.GetLibVersion(); + var version = ClientInfoSanitize(Utils.GetLibVersion()); if (!string.IsNullOrWhiteSpace(version)) { msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, @@ -1091,6 +1093,8 @@ private void SetConfig(ref T field, T value, [CallerMemberName] string? calle Multiplexer?.ReconfigureIfNeeded(EndPoint, false, caller!); } } + internal static string ClientInfoSanitize(string? value) + => string.IsNullOrWhiteSpace(value) ? "" : nameSanitizer.Replace(value!.Trim(), "-"); private void ClearMemoized() { diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2653.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2653.cs new file mode 100644 index 000000000..973a01da5 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2653.cs @@ -0,0 +1,16 @@ +using Xunit; + +namespace StackExchange.Redis.Tests.Issues; + +public class Issue2653 +{ + [Theory] + [InlineData(null, "")] + [InlineData("", "")] + [InlineData("abcdef", "abcdef")] + [InlineData("abc.def", "abc.def")] + [InlineData("abc d \t ef", "abc-d-ef")] + [InlineData(" abc\r\ndef\n", "abc-def")] + public void CheckLibraySanitization(string input, string expected) + => Assert.Equal(expected, ServerEndPoint.ClientInfoSanitize(input)); +} From 54a633fdb7f8e1dde456420eb04522c7e4fd3327 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 26 Feb 2024 07:39:09 -0500 Subject: [PATCH 272/435] Feature: Add HeartbeatConsistencyChecks (#2656) This is a new feature for allowing keepalive commands to be sent every heartbeat regardless of if they're needed for actual keepalives. I've gotten a ping about a network drop that went undetected for ~8 minutes because of usage only in strings. We see this use case not uncommonly, and because of that string type being the only thing used, we don't detect a protocol fault from subsequent string commands. The symptoms here are partial/unexpected string payloads but ultimately the wrong answers to the wrong query. Given we don't know at what layer this drop happens (this appears to be extremely rare, 1 in quadrillions as far as we know), the best we can currently do is react to it ASAP. There may be a better way to accomplish what this is after - discussing with Marc soon as we're both online but prepping this PR in case it's best path. --- docs/Configuration.md | 2 + docs/ReleaseNotes.md | 3 +- .../Configuration/DefaultOptionsProvider.cs | 6 +++ .../ConfigurationOptions.cs | 12 +++++- src/StackExchange.Redis/PhysicalBridge.cs | 25 +++++++++--- .../PublicAPI/PublicAPI.Shipped.txt | 3 ++ src/StackExchange.Redis/ServerEndPoint.cs | 6 +-- .../ConnectCustomConfigTests.cs | 38 ++++++++++++++++++- .../DefaultOptionsTests.cs | 5 +++ 9 files changed, 88 insertions(+), 12 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 2f63c5358..323d89984 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -115,6 +115,8 @@ Additional code-only options: - The thread pool to use for scheduling work to and from the socket connected to Redis, one of... - `SocketManager.Shared`: Use a shared dedicated thread pool for _all_ multiplexers (defaults to 10 threads) - best balance for most scenarios. - `SocketManager.ThreadPool`: Use the build-in .NET thread pool for scheduling. This can perform better for very small numbers of cores or with large apps on large machines that need to use more than 10 threads (total, across all multiplexers) under load. **Important**: this option isn't the default because it's subject to thread pool growth/starvation and if for example synchronous calls are waiting on a redis command to come back to unblock other threads, stalls/hangs can result. Use with caution, especially if you have sync-over-async work in play. +- HeartbeatConsistencyChecks - Default: `false` + - Allows _always_ sending keepalive checks even if a connection isn't idle. This trades extra commands (per `HeartbeatInterval` - default 1 second) to check the network stream for consistency. If any data was lost, the result won't be as expected and the connection will be terminated ASAP. This is a check to react to any data loss at the network layer as soon as possible. - HeartbeatInterval - Default: `1000ms` - Allows running the heartbeat more often which importantly includes timeout evaluation for async commands. For example if you have a 50ms async command timeout, we're only actually checking it during the heartbeat (once per second by default), so it's possible 50-1050ms pass _before we notice it timed out_. If you want more fidelity in that check and to observe that a server failed faster, you can lower this to run the heartbeat more often to achieve that. - **Note: heartbeats are not free and that's why the default is 1 second. There is additional overhead to running this more often simply because it does some work each time it fires.** diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 2098349e2..168d20f36 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Fix [#2653](https://github.com/StackExchange/StackExchange.Redis/issues/2653): Client library metadata should validate contents ([#2654](https://github.com/StackExchange/StackExchange.Redis/pull/2654) by mgravell) +- Add `HeartbeatConsistencyChecks` option (opt-in) to enabled per-heartbeat (defaults to once per second) checks to be sent to ensure no network stream corruption has occured ([#2656 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2656)) ## 2.7.20 @@ -25,7 +26,7 @@ Current package versions: - Fix [#2593](https://github.com/StackExchange/StackExchange.Redis/issues/2593): `EXPIRETIME` and `PEXPIRETIME` miscategorized as `PrimaryOnly` commands causing them to fail when issued against a read-only replica ([#2593 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2593)) - Fix [#2591](https://github.com/StackExchange/StackExchange.Redis/issues/2591): Add `HELLO` to Sentinel connections so they can support RESP3 ([#2601 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2601)) -- Fix [#2595](https://github.com/StackExchange/StackExchange.Redis/issues/2595): Add detection handling for dead sockets that the OS says are okay, seen especially in Linux environments (https://github.com/StackExchange/StackExchange.Redis/pull/2610) +- Fix [#2595](https://github.com/StackExchange/StackExchange.Redis/issues/2595): Add detection handling for dead sockets that the OS says are okay, seen especially in Linux environments ([#2610 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2610)) ## 2.7.4 diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index 67c74089b..67bbc72ac 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -139,6 +139,12 @@ public static DefaultOptionsProvider GetProvider(EndPoint endpoint) /// Be aware setting this very low incurs additional overhead of evaluating the above more often. public virtual TimeSpan HeartbeatInterval => TimeSpan.FromSeconds(1); + /// + /// Whether to enable ECHO checks on every heartbeat to ensure network stream consistency. + /// This is a rare measure to react to any potential network traffic drops ASAP, terminating the connection. + /// + public virtual bool HeartbeatConsistencyChecks => false; + /// /// Should exceptions include identifiable details? (key names, additional .Data annotations) /// diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index a85232172..0cb8f1a78 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -154,7 +154,7 @@ public static string TryNormalize(string value) private DefaultOptionsProvider? defaultOptions; - private bool? allowAdmin, abortOnConnectFail, resolveDns, ssl, checkCertificateRevocation, + private bool? allowAdmin, abortOnConnectFail, resolveDns, ssl, checkCertificateRevocation, heartbeatConsistencyChecks, includeDetailInExceptions, includePerformanceCountersInExceptions, setClientLibrary; private string? tieBreaker, sslHost, configChannel, user, password; @@ -402,6 +402,16 @@ public Version DefaultVersion /// public EndPointCollection EndPoints { get; init; } = new EndPointCollection(); + /// + /// Whether to enable ECHO checks on every heartbeat to ensure network stream consistency. + /// This is a rare measure to react to any potential network traffic drops ASAP, terminating the connection. + /// + public bool HeartbeatConsistencyChecks + { + get => heartbeatConsistencyChecks ?? Defaults.HeartbeatConsistencyChecks; + set => heartbeatConsistencyChecks = value; + } + /// /// Controls how often the connection heartbeats. A heartbeat includes: /// - Evaluating if any messages have timed out diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index bf49e6e30..f870cf340 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -359,9 +359,13 @@ internal void IncrementOpCount() Interlocked.Increment(ref operationCount); } - internal void KeepAlive() + /// + /// Sends a keepalive message (ECHO or PING) to keep connections alive and check validity of response. + /// + /// Whether to run even then the connection isn't idle. + internal void KeepAlive(bool forceRun = false) { - if (!(physical?.IsIdle() ?? false)) return; // don't pile on if already doing something + if (!forceRun && !(physical?.IsIdle() ?? false)) return; // don't pile on if already doing something var commandMap = Multiplexer.CommandMap; Message? msg = null; @@ -596,6 +600,15 @@ internal void OnHeartbeat(bool ifConnectedOnly) checkConfigSeconds = ServerEndPoint.ConfigCheckSeconds; if (state == (int)State.ConnectedEstablished && ConnectionType == ConnectionType.Interactive + && tmp.BridgeCouldBeNull?.Multiplexer.RawConfig.HeartbeatConsistencyChecks == true) + { + // If HeartbeatConsistencyChecks are enabled, we're sending a PING (expecting PONG) or ECHO (expecting UniqueID back) every single + // heartbeat as an opt-in measure to react to any network stream drop ASAP to terminate the connection as faulted. + // If we don't get the expected response to that command, then the connection is terminated. + // This is to prevent the case of things like 100% string command usage where a protocol error isn't otherwise encountered. + KeepAlive(forceRun: true); + } + else if (state == (int)State.ConnectedEstablished && ConnectionType == ConnectionType.Interactive && checkConfigSeconds > 0 && ServerEndPoint.LastInfoReplicationCheckSecondsAgo >= checkConfigSeconds && ServerEndPoint.CheckInfoReplication()) { @@ -614,13 +627,13 @@ internal void OnHeartbeat(bool ifConnectedOnly) tmp.Dispose(); // Cleanup the existing connection/socket if any, otherwise it will wait reading indefinitely } } - else if (writeEverySeconds <= 0 && tmp.IsIdle() + else if (writeEverySeconds <= 0 + && tmp.IsIdle() && tmp.LastWriteSecondsAgo > 2 && tmp.GetSentAwaitingResponseCount() != 0) { - // there's a chance this is a dead socket; sending data will shake that - // up a bit, so if we have an empty unsent queue and a non-empty sent - // queue, test the socket + // There's a chance this is a dead socket; sending data will shake that up a bit, + // so if we have an empty unsent queue and a non-empty sent queue, test the socket. KeepAlive(); } else if (timedOutThisHeartbeat > 0 diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index cded72738..906130e7a 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -232,6 +232,8 @@ StackExchange.Redis.ConfigurationOptions.DefaultVersion.get -> System.Version! StackExchange.Redis.ConfigurationOptions.DefaultVersion.set -> void StackExchange.Redis.ConfigurationOptions.EndPoints.get -> StackExchange.Redis.EndPointCollection! StackExchange.Redis.ConfigurationOptions.EndPoints.init -> void +StackExchange.Redis.ConfigurationOptions.HeartbeatConsistencyChecks.get -> bool +StackExchange.Redis.ConfigurationOptions.HeartbeatConsistencyChecks.set -> void StackExchange.Redis.ConfigurationOptions.HeartbeatInterval.get -> System.TimeSpan StackExchange.Redis.ConfigurationOptions.HeartbeatInterval.set -> void StackExchange.Redis.ConfigurationOptions.HighPrioritySocketThreads.get -> bool @@ -1797,6 +1799,7 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.DefaultVersion. virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetDefaultClientName() -> string! virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetDefaultSsl(StackExchange.Redis.EndPointCollection! endPoints) -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetSslHostFromEndpoints(StackExchange.Redis.EndPointCollection! endPoints) -> string? +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.HeartbeatConsistencyChecks.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.HeartbeatInterval.get -> System.TimeSpan virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludeDetailInExceptions.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludePerformanceCountersInExceptions.get -> bool diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index a5471b5d5..6191da28c 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -567,7 +567,7 @@ internal string GetProfile() internal string? GetStormLog(Message message) => GetBridge(message)?.GetStormLog(); - internal Message GetTracerMessage(bool assertIdentity) + internal Message GetTracerMessage(bool checkResponse) { // Different configurations block certain commands, as can ad-hoc local configurations, so // we'll do the best with what we have available. @@ -576,7 +576,7 @@ internal Message GetTracerMessage(bool assertIdentity) var map = Multiplexer.CommandMap; Message msg; const CommandFlags flags = CommandFlags.NoRedirect | CommandFlags.FireAndForget; - if (assertIdentity && map.IsAvailable(RedisCommand.ECHO)) + if (checkResponse && map.IsAvailable(RedisCommand.ECHO)) { msg = Message.Create(-1, flags, RedisCommand.ECHO, (RedisValue)Multiplexer.UniqueId); } @@ -588,7 +588,7 @@ internal Message GetTracerMessage(bool assertIdentity) { msg = Message.Create(-1, flags, RedisCommand.TIME); } - else if (!assertIdentity && map.IsAvailable(RedisCommand.ECHO)) + else if (!checkResponse && map.IsAvailable(RedisCommand.ECHO)) { // We'll use echo as a PING substitute if it is all we have (in preference to EXISTS) msg = Message.Create(-1, flags, RedisCommand.ECHO, (RedisValue)Multiplexer.UniqueId); diff --git a/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs b/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs index 6041bf12c..98351d04b 100644 --- a/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs @@ -1,4 +1,6 @@ -using Xunit; +using System; +using System.Threading.Tasks; +using Xunit; using Xunit.Abstractions; namespace StackExchange.Redis.Tests; @@ -89,4 +91,38 @@ public void TiebreakerIncorrectType() var ex = Assert.Throws(() => db.StringGet(tiebreakerKey)); Assert.Contains("WRONGTYPE", ex.Message); } + + [Theory] + [InlineData(true, 5, 15)] + [InlineData(false, 0, 0)] + public async Task HeartbeatConsistencyCheckPingsAsync(bool enableConsistencyChecks, int minExpected, int maxExpected) + { + var options = new ConfigurationOptions() + { + HeartbeatConsistencyChecks = enableConsistencyChecks, + HeartbeatInterval = TimeSpan.FromMilliseconds(100), + }; + options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); + + using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + + var db = conn.GetDatabase(); + db.Ping(); + Assert.True(db.IsConnected(default)); + + var preCount = conn.OperationCount; + Log("OperationCount (pre-delay): " + preCount); + + // Allow several heartbeats to happen, but don't need to be strict here + // e.g. allow thread pool starvation flex with the test suite's load (just check for a few) + await Task.Delay(TimeSpan.FromSeconds(1)); + + var postCount = conn.OperationCount; + Log("OperationCount (post-delay): " + postCount); + + var opCount = postCount - preCount; + Log("OperationCount (diff): " + opCount); + + Assert.True(minExpected <= opCount && opCount >= minExpected, $"Expected opcount ({opCount}) between {minExpected}-{maxExpected}"); + } } diff --git a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs index e3941f749..74e52b96d 100644 --- a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs +++ b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs @@ -32,6 +32,8 @@ public class TestOptionsProvider : DefaultOptionsProvider public override int ConnectRetry => 123; public override Version DefaultVersion => new Version(1, 2, 3, 4); protected override string GetDefaultClientName() => "TestPrefix-" + base.GetDefaultClientName(); + public override bool HeartbeatConsistencyChecks => true; + public override TimeSpan HeartbeatInterval => TimeSpan.FromMilliseconds(500); public override bool IsMatch(EndPoint endpoint) => endpoint is DnsEndPoint dnsep && dnsep.Host.EndsWith(_domainSuffix); public override TimeSpan KeepAliveInterval => TimeSpan.FromSeconds(125); public override ILoggerFactory? LoggerFactory => NullLoggerFactory.Instance; @@ -99,6 +101,9 @@ private static void AssertAllOverrides(ConfigurationOptions options) Assert.Equal(123, options.ConnectRetry); Assert.Equal(new Version(1, 2, 3, 4), options.DefaultVersion); + Assert.True(options.HeartbeatConsistencyChecks); + Assert.Equal(TimeSpan.FromMilliseconds(500), options.HeartbeatInterval); + Assert.Equal(TimeSpan.FromSeconds(125), TimeSpan.FromSeconds(options.KeepAlive)); Assert.Equal(NullLoggerFactory.Instance, options.LoggerFactory); Assert.Equal(Proxy.Twemproxy, options.Proxy); From 2753c37a6f7dafdb9e4947e9156217186653832b Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 26 Feb 2024 07:55:29 -0500 Subject: [PATCH 273/435] Add 2.7.23 release notes --- docs/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 168d20f36..cb51defab 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,6 +7,9 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased +No pending unreleased changes. + +## 2.7.23 - Fix [#2653](https://github.com/StackExchange/StackExchange.Redis/issues/2653): Client library metadata should validate contents ([#2654](https://github.com/StackExchange/StackExchange.Redis/pull/2654) by mgravell) - Add `HeartbeatConsistencyChecks` option (opt-in) to enabled per-heartbeat (defaults to once per second) checks to be sent to ensure no network stream corruption has occured ([#2656 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2656)) From 7bec9dfa58183d81a6fc99b0c083ea9bfd4eb7fe Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 26 Feb 2024 07:56:58 -0500 Subject: [PATCH 274/435] Update 2.7.23 release notes -Fix spelling --- docs/ReleaseNotes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index cb51defab..6b2643998 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -12,7 +12,7 @@ No pending unreleased changes. ## 2.7.23 - Fix [#2653](https://github.com/StackExchange/StackExchange.Redis/issues/2653): Client library metadata should validate contents ([#2654](https://github.com/StackExchange/StackExchange.Redis/pull/2654) by mgravell) -- Add `HeartbeatConsistencyChecks` option (opt-in) to enabled per-heartbeat (defaults to once per second) checks to be sent to ensure no network stream corruption has occured ([#2656 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2656)) +- Add `HeartbeatConsistencyChecks` option (opt-in) to enabled per-heartbeat (defaults to once per second) checks to be sent to ensure no network stream corruption has occurred ([#2656 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2656)) ## 2.7.20 From a517561a1d9e9ce490661cf6d7d7c50b6108a56d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 27 Feb 2024 15:41:51 +0000 Subject: [PATCH 275/435] Support `HeartbeatConsistencyChecks` in `Clone()` (#2658) * Support `HeartbeatConsistencyChecks` in `Clone()` * include heartbeatInterval --- docs/ReleaseNotes.md | 3 +- .../ConfigurationOptions.cs | 2 + .../StackExchange.Redis.Tests/ConfigTests.cs | 37 ++++++++++++++++++- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 6b2643998..63bd07b0a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,7 +7,8 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased -No pending unreleased changes. + +- Support `HeartbeatConsistencyChecks` and `HeartbeatInterval` in `Clone()` ([#2658 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2658)) ## 2.7.23 diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 0cb8f1a78..4f025d218 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -721,6 +721,8 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow setClientLibrary = setClientLibrary, LibraryName = LibraryName, Protocol = Protocol, + heartbeatInterval = heartbeatInterval, + heartbeatConsistencyChecks = heartbeatConsistencyChecks, }; /// diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 84a8f916b..97c48e356 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -1,4 +1,5 @@ -using StackExchange.Redis.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using StackExchange.Redis.Configuration; using System; using System.Globalization; using System.IO; @@ -6,11 +7,12 @@ using System.Linq; using System.Net; using System.Net.Sockets; +using System.Reflection; using System.Security.Authentication; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Logging.Abstractions; using Xunit; using Xunit.Abstractions; @@ -25,6 +27,37 @@ public ConfigTests(ITestOutputHelper output, SharedConnectionFixture fixture) : public Version DefaultVersion = new (3, 0, 0); public Version DefaultAzureVersion = new (4, 0, 0); + [Fact] + public void ExpectedFields() + { + // if this test fails, check that you've updated ConfigurationOptions.Clone(), then: fix the test! + // this is a simple but pragmatic "have you considered?" check + + var fields = Array.ConvertAll(typeof(ConfigurationOptions).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), + x => Regex.Replace(x.Name, """^<(\w+)>k__BackingField$""", "$1")); + Array.Sort(fields); + Assert.Equal(new[] { + "abortOnConnectFail", "allowAdmin", "asyncTimeout", "backlogPolicy", "BeforeSocketConnect", + "CertificateSelection", "CertificateValidation", "ChannelPrefix", + "checkCertificateRevocation", "ClientName", "commandMap", + "configChannel", "configCheckSeconds", "connectRetry", + "connectTimeout", "DefaultDatabase", "defaultOptions", + "defaultVersion", "EndPoints", "heartbeatConsistencyChecks", + "heartbeatInterval", "includeDetailInExceptions", "includePerformanceCountersInExceptions", + "keepAlive", "LibraryName", "loggerFactory", + "password", "Protocol", "proxy", + "reconnectRetryPolicy", "resolveDns", "responseTimeout", + "ServiceName", "setClientLibrary", "SocketManager", + "ssl", +#if !NETFRAMEWORK + "SslClientAuthenticationOptions", +#endif + "sslHost", "SslProtocols", + "syncTimeout", "tieBreaker", "Tunnel", + "user" + }, fields); + } + [Fact] public void SslProtocols_SingleValue() { From 18c057ba7284b16d73b6bf15d364b39e1ebab4a9 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 28 Feb 2024 15:19:58 +0000 Subject: [PATCH 276/435] add new AddLibraryNameSuffix API for annotating connections with usage (#2659) * add new AddLibraryNameSuffix API for annotating connections with usage * fixup test * new partial for lib-name bits * use hashing rather than array shenanigans * move to shipped; fix comment typo * comment example * reverse comment owner --- docs/ReleaseNotes.md | 1 + .../ConfigurationOptions.cs | 2 +- .../ConnectionMultiplexer.LibraryName.cs | 76 +++++++++++++++++++ .../Interfaces/IConnectionMultiplexer.cs | 8 ++ .../PublicAPI/PublicAPI.Shipped.txt | 4 +- src/StackExchange.Redis/ServerEndPoint.cs | 12 +-- .../StackExchange.Redis.Tests/ConfigTests.cs | 35 +++++++++ .../Helpers/SharedConnectionFixture.cs | 2 + 8 files changed, 128 insertions(+), 12 deletions(-) create mode 100644 src/StackExchange.Redis/ConnectionMultiplexer.LibraryName.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 63bd07b0a..c144d9235 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Support `HeartbeatConsistencyChecks` and `HeartbeatInterval` in `Clone()` ([#2658 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2658)) +- Add `AddLibraryNameSuffix` to multiplexer; allows usage-specific tokens to be appended *after connect* ## 2.7.23 diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 4f025d218..2c47d034d 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -260,7 +260,7 @@ public bool SetClientLibrary /// Gets or sets the library name to use for CLIENT SETINFO lib-name calls to Redis during handshake. /// Defaults to "SE.Redis". /// - /// If the value is null, empty or whitespace, then the value from the options-provideer is used; + /// If the value is null, empty or whitespace, then the value from the options-provider is used; /// to disable the library name feature, use instead. public string? LibraryName { get; set; } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.LibraryName.cs b/src/StackExchange.Redis/ConnectionMultiplexer.LibraryName.cs new file mode 100644 index 000000000..2c79f80c5 --- /dev/null +++ b/src/StackExchange.Redis/ConnectionMultiplexer.LibraryName.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace StackExchange.Redis; + +public partial class ConnectionMultiplexer +{ + private readonly HashSet _libraryNameSuffixHash = new(); + private string _libraryNameSuffixCombined = ""; + + /// + public void AddLibraryNameSuffix(string suffix) + { + if (string.IsNullOrWhiteSpace(suffix)) return; // trivial + + // sanitize and re-check + suffix = ServerEndPoint.ClientInfoSanitize(suffix ?? "").Trim(); + if (string.IsNullOrWhiteSpace(suffix)) return; // trivial + + lock (_libraryNameSuffixHash) + { + if (!_libraryNameSuffixHash.Add(suffix)) return; // already cited; nothing to do + + _libraryNameSuffixCombined = "-" + string.Join("-", _libraryNameSuffixHash.OrderBy(_ => _)); + } + + // if we get here, we *actually changed something*; we can retroactively fixup the connections + var libName = GetFullLibraryName(); // note this also checks SetClientLibrary + if (string.IsNullOrWhiteSpace(libName) || !CommandMap.IsAvailable(RedisCommand.CLIENT)) return; // disabled on no lib name + + // note that during initial handshake we use raw Message; this is low frequency - no + // concern over overhead of Execute here + var args = new object[] { RedisLiterals.SETINFO, RedisLiterals.lib_name, libName }; + foreach (var server in GetServers()) + { + try + { + // note we can only fixup the *interactive* channel; that's tolerable here + if (server.IsConnected) + { + // best effort only + server.Execute("CLIENT", args, CommandFlags.FireAndForget); + } + } + catch (Exception ex) + { + // if an individual server trips, that's fine - best effort; note we're using + // F+F here anyway, so we don't *expect* any failures + Debug.WriteLine(ex.Message); + } + } + } + + internal string GetFullLibraryName() + { + var config = RawConfig; + if (!config.SetClientLibrary) return ""; // disabled + + var libName = config.LibraryName; + if (string.IsNullOrWhiteSpace(libName)) + { + // defer to provider if missing (note re null vs blank; if caller wants to disable + // it, they should set SetClientLibrary to false, not set the name to empty string) + libName = config.Defaults.LibraryName; + } + + libName = ServerEndPoint.ClientInfoSanitize(libName); + // if no primary name, return nothing, even if suffixes exist + if (string.IsNullOrWhiteSpace(libName)) return ""; + + return libName + Volatile.Read(ref _libraryNameSuffixCombined); + } +} diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index 0c1494641..25d2f7099 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -294,5 +294,13 @@ public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable /// The destination stream to write the export to. /// The options to use for this export. void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All); + + /// + /// Append a usage-specific modifier to the advertised library name; suffixes are de-duplicated + /// and sorted alphabetically (so adding 'a', 'b' and 'a' will result in suffix '-a-b'). + /// Connections will be updated as necessary (RESP2 subscription + /// connections will not show updates until those connections next connect). + /// + void AddLibraryNameSuffix(string suffix); } } diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 906130e7a..1bcc6c66d 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1845,4 +1845,6 @@ StackExchange.Redis.ResultType.VerbatimString = 12 -> StackExchange.Redis.Result static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisResult![]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult! static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult! virtual StackExchange.Redis.RedisResult.Length.get -> int -virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult! \ No newline at end of file +virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult! +StackExchange.Redis.ConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void +StackExchange.Redis.IConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void \ No newline at end of file diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 6191da28c..2cbe36920 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -930,7 +930,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) var config = Multiplexer.RawConfig; string? user = config.User; string password = config.Password ?? ""; - + string clientName = Multiplexer.ClientName; if (!string.IsNullOrWhiteSpace(clientName)) { @@ -1017,15 +1017,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) // server version, so we will use this speculatively and hope for the best log?.LogInformation($"{Format.ToString(this)}: Setting client lib/ver"); - var libName = config.LibraryName; - if (string.IsNullOrWhiteSpace(libName)) - { - // defer to provider if missing (note re null vs blank; if caller wants to disable - // it, they should set SetClientLibrary to false, not set the name to empty string) - libName = config.Defaults.LibraryName; - } - - libName = ClientInfoSanitize(libName); + var libName = Multiplexer.GetFullLibraryName(); if (!string.IsNullOrWhiteSpace(libName)) { msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 97c48e356..a2329dc04 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -279,6 +279,41 @@ public void ClientName() Assert.Equal("TestRig", name); } + [Fact] + public async Task ClientLibraryName() + { + using var conn = Create(allowAdmin: true, shared: false); + var server = GetAnyPrimary(conn); + + await server.PingAsync(); + var possibleId = conn.GetConnectionId(server.EndPoint, ConnectionType.Interactive); + + if (possibleId is null) + { + Log("(client id not available)"); + return; + } + var id = possibleId.Value; + var libName = server.ClientList().Single(x => x.Id == id).LibraryName; + if (libName is not null) // server-version dependent + { + Log("library name: {0}", libName); + Assert.Equal("SE.Redis", libName); + + conn.AddLibraryNameSuffix("foo"); + conn.AddLibraryNameSuffix("bar"); + conn.AddLibraryNameSuffix("foo"); + + libName = (await server.ClientListAsync()).Single(x => x.Id == id).LibraryName; + Log("library name: {0}", libName); + Assert.Equal("SE.Redis-bar-foo", libName); + } + else + { + Log("(library name not available)"); + } + } + [Fact] public void DefaultClientName() { diff --git a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs index c88c0ec4d..1d7396033 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs @@ -98,6 +98,8 @@ public bool IgnoreConnect public int GetSubscriptionsCount() => _inner.GetSubscriptionsCount(); public ConcurrentDictionary GetSubscriptions() => _inner.GetSubscriptions(); + public void AddLibraryNameSuffix(string suffix) => _inner.AddLibraryNameSuffix(suffix); + public string ClientName => _inner.ClientName; public string Configuration => _inner.Configuration; From ae72fb754cde160ffb160be4416f6c79ae6ee116 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 1 Mar 2024 08:11:19 +0000 Subject: [PATCH 277/435] release notes --- docs/ReleaseNotes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index c144d9235..2589d8bd0 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,8 @@ Current package versions: ## Unreleased +## 2.7.27 + - Support `HeartbeatConsistencyChecks` and `HeartbeatInterval` in `Clone()` ([#2658 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2658)) - Add `AddLibraryNameSuffix` to multiplexer; allows usage-specific tokens to be appended *after connect* From 9ab79e30027b9a4b06c2cb34c19ac7d1b45d581d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 1 Mar 2024 17:40:58 +0000 Subject: [PATCH 278/435] new tunnel-based RESP logging and validation (#2660) * Provide new LoggingTunnel API; this * words * fix PR number * fix file location * save the sln * identify smessage as out-of-band * add .ForAwait() throughout LoggingTunnel * clarify meaning of path parameter --- StackExchange.Redis.sln | 1 + docs/ReleaseNotes.md | 4 +- docs/RespLogging.md | 150 +++++ docs/index.md | 1 + src/StackExchange.Redis/BufferReader.cs | 2 + .../Configuration/LoggingTunnel.cs | 627 ++++++++++++++++++ src/StackExchange.Redis/PhysicalConnection.cs | 4 +- src/StackExchange.Redis/RedisResult.cs | 2 +- 8 files changed, 787 insertions(+), 4 deletions(-) create mode 100644 docs/RespLogging.md create mode 100644 src/StackExchange.Redis/Configuration/LoggingTunnel.cs diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index 1fa39f4c2..f59d5f6fc 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -120,6 +120,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{153A10E4-E docs\PubSubOrder.md = docs\PubSubOrder.md docs\ReleaseNotes.md = docs\ReleaseNotes.md docs\Resp3.md = docs\Resp3.md + docs\RespLogging.md = docs\RespLogging.md docs\Scripting.md = docs\Scripting.md docs\Server.md = docs\Server.md docs\Testing.md = docs\Testing.md diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 2589d8bd0..18adb415e 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,10 +8,12 @@ Current package versions: ## Unreleased +- Add new `LoggingTunnel` API; see https://stackexchange.github.io/StackExchange.Redis/Logging [#2660 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2660) + ## 2.7.27 - Support `HeartbeatConsistencyChecks` and `HeartbeatInterval` in `Clone()` ([#2658 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2658)) -- Add `AddLibraryNameSuffix` to multiplexer; allows usage-specific tokens to be appended *after connect* +- Add `AddLibraryNameSuffix` to multiplexer; allows usage-specific tokens to be appended *after connect* [#2659 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2659) ## 2.7.23 diff --git a/docs/RespLogging.md b/docs/RespLogging.md new file mode 100644 index 000000000..3ff3b0164 --- /dev/null +++ b/docs/RespLogging.md @@ -0,0 +1,150 @@ +Logging and validating the underlying RESP stream +=== + +Sometimes (rarely) there is a question over the validity of the RESP stream from a server (especially when using proxies +or a "redis-like-but-not-actually-redis" server), and it is hard to know whether the *data sent* was bad, vs +the client library tripped over the data. + +To help with this, an experimental API exists to help log and validate RESP streams. This API is not intended +for routine use (and may change at any time), but can be useful for diagnosing problems. + +For example, consider we have the following load test which (on some setup) causes a failure with some +degree of reliability (even if you need to run it 6 times to see a failure): + +``` c# +// connect +Console.WriteLine("Connecting..."); +var options = ConfigurationOptions.Parse(ConnectionString); +await using var muxer = await ConnectionMultiplexer.ConnectAsync(options); +var db = muxer.GetDatabase(); + +// load +RedisKey testKey = "marc_abc"; +await db.KeyDeleteAsync(testKey); +Console.WriteLine("Writing..."); +for (int i = 0; i < 100; i++) +{ + // sync every 50 iterations (pipeline the rest) + var flags = (i % 50) == 0 ? CommandFlags.None : CommandFlags.FireAndForget; + await db.SetAddAsync(testKey, Guid.NewGuid().ToString(), flags); +} + +// fetch +Console.WriteLine("Reading..."); +int count = 0; +for (int i = 0; i < 10; i++) +{ + // this is deliberately not using SCARD + // (to put load on the inbound) + count += (await db.SetMembersAsync(testKey)).Length; +} +Console.WriteLine("all done"); +``` + +## Logging RESP streams + +When this fails, it will not be obvious exactly who is to blame. However, we can ask for the data streams +to be logged to the local file-system. + +**Obviously, this may leave data on disk, so this may present security concerns if used with production data; use +this feature sparingly, and clean up after yourself!** + +``` c# +// connect +Console.WriteLine("Connecting..."); +var options = ConfigurationOptions.Parse(ConnectionString); +LoggingTunnel.LogToDirectory(options, @"C:\Code\RedisLog"); // <=== added! +await using var muxer = await ConnectionMultiplexer.ConnectAsync(options); +... +``` + +This API is marked `[Obsolete]` simply to discourage usage, but you can ignore this warning once you +understand what it is saying (using `#pragma warning disable CS0618` if necessary). + +This will update the `ConfigurationOptions` with a custom `Tunnel` that performs file-based mirroring +of the RESP streams. If `Ssl` is enabled on the `ConfigurationOptions`, the `Tunnel` will *take over that responsibility* +(so that the unencrypted data can be logged), and will *disable* `Ssl` on the `ConfigurationOptions` - but TLS +will still be used correctly. + +If we run our code, we will see that 2 files are written per connection ("in" and "out"); if you are using RESP2 (the default), +then 2 connections are usually established (one for regular "interactive" commands, and one for pub/sub messages), so this will +typically create 4 files. + +## Validating RESP streams + +RESP is *mostly* text, so a quick eyeball can be achieved using any text tool; an "out" file will typically start: + +``` txt +$6 +CLIENT +$7 +SETNAME +... +``` + +and an "in" file will typically start: + +``` txt ++OK ++OK ++OK +... +``` + +This is the start of the handshakes for identifying the client to the redis server, and the server acknowledging this (if +you have authentication enabled, there will be a `AUTH` command first, or `HELLO` on RESP3). + +If there is a failure, you obviously don't want to manually check these files. Instead, an API exists to validate RESP streams: + +``` c# +var messages = await LoggingTunnel.ValidateAsync(@"C:\Code\RedisLog"); +Console.WriteLine($"{messages} RESP fragments validated"); +``` + +If the RESP streams are *not* valid, an exception will provide further details. + +**An exception here is strong evidence that there is a fault either in the redis server, or an intermediate proxy**. + +Conversely, if the library reported a protocol failure but the validation step here *does not* report an error, then +that is strong evidence of a library error; [**please report this**](https://github.com/StackExchange/StackExchange.Redis/issues/new) (with details). + +You can also *replay* the conversation locally, seeing the individual requests and responses: + +``` c# +var messages = await LoggingTunnel.ReplayAsync(@"C:\Code\RedisLog", (cmd, resp) => +{ + if (cmd.IsNull) + { + // out-of-band/"push" response + Console.WriteLine("<< " + LoggingTunnel.DefaultFormatResponse(resp)); + } + else + { + Console.WriteLine(" > " + LoggingTunnel.DefaultFormatCommand(cmd)); + Console.WriteLine(" < " + LoggingTunnel.DefaultFormatResponse(resp)); + } +}); +Console.WriteLine($"{messages} RESP commands validated"); +``` + +The `DefaultFormatCommand` and `DefaultFormatResponse` methods are provided for convenience, but you +can perform your own formatting logic if required. If a RESP erorr is encountered in the response to +a particular message, the callback will still be invoked to indicate that error. For example, after deliberately +introducing an error into the captured file, we might see: + +``` txt + > CLUSTER NODES + < -ERR This instance has cluster support disabled + > GET __Booksleeve_TieBreak + < (null) + > ECHO ... + < -Invalid bulk string terminator +Unhandled exception. StackExchange.Redis.RedisConnectionException: Invalid bulk string terminator +``` + +The `-ERR` message is not a problem - that's normal and simply indicates that this is not a redis cluster; however, the +final pair is an `ECHO` request, for which the corresponding response was invalid. This information is useful for finding +out what happened. + +Emphasis: this API is not intended for common/frequent usage; it is intended only to assist validating the underlying +RESP stream. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index cd1c84d7e..b66f8f9a7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -48,6 +48,7 @@ Documentation - [Testing](Testing) - running the `StackExchange.Redis.Tests` suite to validate changes - [Timeouts](Timeouts) - guidance on dealing with timeout problems - [Thread Theft](ThreadTheft) - guidance on avoiding TPL threading problems +- [RESP Logging](RespLogging) - capturing and validating RESP streams Questions and Contributions --- diff --git a/src/StackExchange.Redis/BufferReader.cs b/src/StackExchange.Redis/BufferReader.cs index a7199fe6b..5691217f9 100644 --- a/src/StackExchange.Redis/BufferReader.cs +++ b/src/StackExchange.Redis/BufferReader.cs @@ -17,6 +17,8 @@ internal ref struct BufferReader public int OffsetThisSpan { get; private set; } public int RemainingThisSpan { get; private set; } + public long TotalConsumed => _totalConsumed; + private ReadOnlySequence.Enumerator _iterator; private ReadOnlySpan _current; diff --git a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs new file mode 100644 index 000000000..a058a522c --- /dev/null +++ b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs @@ -0,0 +1,627 @@ +using Pipelines.Sockets.Unofficial; +using Pipelines.Sockets.Unofficial.Arenas; +using System; +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Pipelines; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using static StackExchange.Redis.PhysicalConnection; + +namespace StackExchange.Redis.Configuration; + +/// +/// Captures redis traffic; intended for debug use +/// +[Obsolete("This API is experimental, has security and performance implications, and may change without notice", false)] +[SuppressMessage("ApiDesign", "RS0016:Add public types and members to the declared API", Justification = "Experimental API")] +public abstract class LoggingTunnel : Tunnel +{ + private readonly ConfigurationOptions _options; + private readonly bool _ssl; + private readonly Tunnel? _tail; + + /// + /// Replay the RESP messages for a pair of streams, invoking a callback per operation + /// + public static async Task ReplayAsync(Stream @out, Stream @in, Action pair) + { + using Arena arena = new(); + var outPipe = StreamConnection.GetReader(@out); + var inPipe = StreamConnection.GetReader(@in); + + long count = 0; + while (true) + { + var sent = await ReadOneAsync(outPipe, arena, isInbound: false).ForAwait(); + ContextualRedisResult received; + try + { + do + { + received = await ReadOneAsync(inPipe, arena, isInbound: true).ForAwait(); + if (received.IsOutOfBand && received.Result is not null) + { + // spoof an empty request for OOB messages + pair(RedisResult.NullSingle, received.Result); + } + } while (received.IsOutOfBand); + } + catch (Exception ex) + { + // if we got an exception following a command, spoof that as a pair, + // so we see the message that had a corrupted reply + if (sent.Result is not null) + { + pair(sent.Result, RedisResult.Create(ex.Message, ResultType.Error)); + } + throw; // still surface the original exception + } + + if (sent.Result is null || received.Result is null) break; // no more paired messages + + pair(sent.Result, received.Result); + count++; + } + return count; + } + + + /// + /// Replay the RESP messages all the streams in a folder, invoking a callback per operation + /// + /// The directory of captured files to replay. + /// Operation to perform per replayed message pair. + public static async Task ReplayAsync(string path, Action pair) + { + long total = 0; + foreach (var outPath in Directory.EnumerateFiles(path, "*.out")) + { + var inPath = Path.ChangeExtension(outPath, "in"); + if (!File.Exists(outPath)) continue; + + using var outFile = File.OpenRead(outPath); + using var inFile = File.OpenRead(inPath); + total += await ReplayAsync(outFile, inFile, pair).ForAwait(); + } + return total; + } + + private static async ValueTask ReadOneAsync(PipeReader input, Arena arena, bool isInbound) + { + while (true) + { + var readResult = await input.ReadAsync().ForAwait(); + var buffer = readResult.Buffer; + int handled = 0; + var result = buffer.IsEmpty ? default : ProcessBuffer(arena, ref buffer, isInbound); + input.AdvanceTo(buffer.Start, buffer.End); + + if (result.Result is not null) return result; + + if (handled == 0 && readResult.IsCompleted) + { + break; // no more data, or trailing incomplete messages + } + } + return default; + } + + /// + /// Validate a RESP stream and return the number of top-level RESP fragments. + /// + /// The path of a single file to validate, or a directory of captured files to validate. + public static async Task ValidateAsync(string path) + { + if (File.Exists(path)) + { + using var singleFile = File.OpenRead(path); + return await ValidateAsync(singleFile).ForAwait(); + } + else if (Directory.Exists(path)) + { + long total = 0; + foreach (var file in Directory.EnumerateFiles(path)) + { + try + { + using var folderFile = File.OpenRead(file); + total += await ValidateAsync(folderFile).ForAwait(); + } + catch (Exception ex) + { + throw new InvalidOperationException(ex.Message + " in " + file, ex); + } + } + return total; + } + else + { + throw new FileNotFoundException(path); + } + } + + /// + /// Validate a RESP stream and return the number of top-level RESP fragments. + /// + public static async Task ValidateAsync(Stream stream) + { + using var arena = new Arena(); + var input = StreamConnection.GetReader(stream); + long total = 0, position = 0; + while (true) + { + var readResult = await input.ReadAsync().ForAwait(); + var buffer = readResult.Buffer; + int handled = 0; + if (!buffer.IsEmpty) + { + try + { + ProcessBuffer(arena, ref buffer, ref position, ref handled); // updates buffer.Start + } + catch (Exception ex) + { + throw new InvalidOperationException($"Invalid fragment starting at {position} (fragment {total + handled})", ex); + } + total += handled; + } + + input.AdvanceTo(buffer.Start, buffer.End); + + if (handled == 0 && readResult.IsCompleted) + { + break; // no more data, or trailing incomplete messages + } + } + return total; + } + private static void ProcessBuffer(Arena arena, ref ReadOnlySequence buffer, ref long position, ref int messageCount) + { + while (!buffer.IsEmpty) + { + var reader = new BufferReader(buffer); + try + { + var result = TryParseResult(true, arena, in buffer, ref reader, true, null); + if (result.HasValue) + { + buffer = reader.SliceFromCurrent(); + position += reader.TotalConsumed; + messageCount++; + } + else + { + break; // remaining buffer isn't enough; give up + } + } + finally + { + arena.Reset(); + } + } + } + + private readonly struct ContextualRedisResult + { + public readonly RedisResult? Result; + public readonly bool IsOutOfBand; + public ContextualRedisResult(RedisResult? result, bool isOutOfBand) + { + Result = result; + IsOutOfBand = isOutOfBand; + } + } + + private static ContextualRedisResult ProcessBuffer(Arena arena, ref ReadOnlySequence buffer, bool isInbound) + { + if (!buffer.IsEmpty) + { + var reader = new BufferReader(buffer); + try + { + var result = TryParseResult(true, arena, in buffer, ref reader, true, null); + bool isOutOfBand = result.Resp3Type == ResultType.Push + || (isInbound && result.Resp2TypeArray == ResultType.Array && IsArrayOutOfBand(result)); + if (result.HasValue) + { + buffer = reader.SliceFromCurrent(); + if (!RedisResult.TryCreate(null, result, out var parsed)) + { + throw new InvalidOperationException("Unable to parse raw result to RedisResult"); + } + return new(parsed, isOutOfBand); + } + } + finally + { + arena.Reset(); + } + } + return default; + + static bool IsArrayOutOfBand(in RawResult result) + { + var items = result.GetItems(); + return (items.Length >= 3 && items[0].IsEqual(message) || items[0].IsEqual(smessage)) + || (items.Length >= 4 && items[0].IsEqual(pmessage)); + + } + } + private static readonly CommandBytes message = "message", pmessage = "pmessage", smessage = "smessage"; + + /// + /// Create a new instance of a + /// + protected LoggingTunnel(ConfigurationOptions? options = null, Tunnel? tail = null) + { + options ??= new(); + _options = options; + _ssl = options.Ssl; + _tail = tail; + options.Ssl = false; // disable here, since we want to log *decrypted* + } + + /// + /// Configures the provided options to perform file-based logging to a directory; + /// files will be sequential per stream starting from zero, and will blindly overwrite existing files. + /// + public static void LogToDirectory(ConfigurationOptions options, string path) + { + var tunnel = new DirectoryLoggingTunnel(path, options, options.Tunnel); + options.Tunnel = tunnel; + } + + private class DirectoryLoggingTunnel : LoggingTunnel + { + private readonly string path; + private int _nextIndex = -1; + + internal DirectoryLoggingTunnel(string path, ConfigurationOptions? options = null, Tunnel? tail = null) + : base(options, tail) + { + this.path = path; + if (!Directory.Exists(path)) throw new InvalidOperationException("Directly does not exist: " + path); + } + + protected override Stream Log(Stream stream, EndPoint endpoint, ConnectionType connectionType) + { + int index = Interlocked.Increment(ref _nextIndex); + var name = $"{Format.ToString(endpoint)} {connectionType} {index}.tmp"; + foreach (var c in InvalidChars) + { + name = name.Replace(c, ' '); + } + name = Path.Combine(path, name); + var reads = File.Create(Path.ChangeExtension(name, ".in")); + var writes = File.Create(Path.ChangeExtension(name, ".out")); + return new LoggingDuplexStream(stream, reads, writes); + } + + private static readonly char[] InvalidChars = Path.GetInvalidFileNameChars(); + } + + /// + public override async ValueTask BeforeAuthenticateAsync(EndPoint endpoint, ConnectionType connectionType, Socket? socket, CancellationToken cancellationToken) + { + Stream? stream = null; + if (_tail is not null) + { + stream = await _tail.BeforeAuthenticateAsync(endpoint, connectionType, socket, cancellationToken).ForAwait(); + } + stream ??= new NetworkStream(socket ?? throw new InvalidOperationException("No stream or socket available")); + if (_ssl) + { + stream = await TlsHandshakeAsync(stream, endpoint).ForAwait(); + } + return Log(stream, endpoint, connectionType); + } + + /// + /// Perform logging on the provided stream + /// + protected abstract Stream Log(Stream stream, EndPoint endpoint, ConnectionType connectionType); + + /// + public override ValueTask BeforeSocketConnectAsync(EndPoint endPoint, ConnectionType connectionType, Socket? socket, CancellationToken cancellationToken) + { + return _tail is null ? base.BeforeSocketConnectAsync(endPoint, connectionType, socket, cancellationToken) + : _tail.BeforeSocketConnectAsync(endPoint, connectionType, socket, cancellationToken); + } + + /// + public override ValueTask GetSocketConnectEndpointAsync(EndPoint endpoint, CancellationToken cancellationToken) + { + return _tail is null ? base.GetSocketConnectEndpointAsync(endpoint, cancellationToken) + : _tail.GetSocketConnectEndpointAsync(endpoint, cancellationToken); + } + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - netfx back-compat mode + private async Task TlsHandshakeAsync(Stream stream, EndPoint endpoint) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + // mirrors TLS handshake from PhysicalConnection, but wouldn't help to share code here + var host = _options.SslHost; + if (host.IsNullOrWhiteSpace()) + { + host = Format.ToStringHostOnly(endpoint); + } + + var ssl = new SslStream(stream, false, + _options.CertificateValidationCallback ?? PhysicalConnection.GetAmbientIssuerCertificateCallback(), + _options.CertificateSelectionCallback ?? PhysicalConnection.GetAmbientClientCertificateCallback(), + EncryptionPolicy.RequireEncryption); + +#if NETCOREAPP3_1_OR_GREATER + var configOptions = _options.SslClientAuthenticationOptions?.Invoke(host); + if (configOptions is not null) + { + await ssl.AuthenticateAsClientAsync(configOptions).ForAwait(); + } + else + { + ssl.AuthenticateAsClient(host, _options.SslProtocols, _options.CheckCertificateRevocation); + } +#else + ssl.AuthenticateAsClient(host, _options.SslProtocols, _options.CheckCertificateRevocation); +#endif + return ssl; + } + + /// + /// Get a typical text representation of a redis command + /// + public static string DefaultFormatCommand(RedisResult value) + { + try + { + if (value.IsNull) return "(null)"; + if (value.Type == ResultType.Array) + { + var sb = new StringBuilder(); + for (int i = 0; i < value.Length; i++) + { + var item = value[i]; + if (i != 0) sb.Append(' '); + if (IsSimple(item)) + { + sb.Append(item.AsString()); + } + else + { + sb.Append("..."); + break; + } + } + return sb.ToString(); + } + } + catch {} + return value.Type.ToString(); + + static bool IsSimple(RedisResult value) + { + try + { + switch (value.Resp2Type) + { + case ResultType.Array: return false; + case ResultType.Error: return true; + default: + var blob = value.AsByteArray(); // note non-alloc in the remaining cases + if (blob is null) return true; + if (blob.Length >= 50) return false; + for (int i = 0; i < blob.Length; i++) + { + char c = (char)blob[i]; + if (c < ' ' || c > '~') return false; + } + return true; + } + } + catch + { + return false; + } + } + } + + /// + /// Get a typical text representation of a redis response + /// + public static string DefaultFormatResponse(RedisResult value) + { + try + { + if (value.IsNull) return "(null)"; + switch (value.Type.ToResp2()) + { + case ResultType.Integer: + case ResultType.BulkString: + case ResultType.SimpleString: + return value.AsString()!; + case ResultType.Error: + return "-" + value.ToString(); + case ResultType.Array: + return $"[{value.Length}]"; + } + } + catch (Exception ex) + { + Debug.Write(ex.Message); + } + return value.Type.ToString(); + } + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + protected sealed class LoggingDuplexStream : Stream + { + private readonly Stream _inner, _reads, _writes; + + internal LoggingDuplexStream(Stream inner, Stream reads, Stream writes) + { + _inner = inner; + _reads = reads; + _writes = writes; + } + + public override bool CanRead => _inner.CanRead; + public override bool CanWrite => _inner.CanWrite; + + public override bool CanSeek => false; // duplex + public override bool CanTimeout => _inner.CanTimeout; + public override int ReadTimeout { get => _inner.ReadTimeout; set => _inner.ReadTimeout = value; } + public override int WriteTimeout { get => _inner.WriteTimeout; set => _inner.WriteTimeout = value; } + public override long Length => throw new NotSupportedException(); // duplex + public override long Position + { + get => throw new NotSupportedException(); // duplex + set => throw new NotSupportedException(); // duplex + } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); // duplex + public override void SetLength(long value) => throw new NotSupportedException(); // duplex + + // we don't use these APIs + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => throw new NotSupportedException(); + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => throw new NotSupportedException(); + public override int EndRead(IAsyncResult asyncResult) => throw new NotSupportedException(); + public override void EndWrite(IAsyncResult asyncResult) => throw new NotSupportedException(); + + public override void Flush() + { + // note we don't flush _reads, as that could be cross-threaded + // (flush is a write operation, not a read one) + _writes.Flush(); + _inner.Flush(); + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + await _writes.FlushAsync().ForAwait(); + await _inner.FlushAsync().ForAwait(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _inner.Dispose(); + try { _reads.Flush(); } catch { } + _reads.Dispose(); + try { _writes.Flush(); } catch { } + _writes.Dispose(); + } + base.Dispose(disposing); + } + + public override void Close() + { + _inner.Close(); + try { _reads.Flush(); } catch { } + _reads.Close(); + try { _writes.Flush(); } catch { } + _writes.Close(); + base.Close(); + } + +#if NETCOREAPP3_0_OR_GREATER + public override async ValueTask DisposeAsync() + { + await _inner.DisposeAsync().ForAwait(); + try { await _reads.FlushAsync().ForAwait(); } catch { } + await _reads.DisposeAsync().ForAwait(); + try { await _writes.FlushAsync().ForAwait(); } catch { } + await _writes.DisposeAsync().ForAwait(); + await base.DisposeAsync().ForAwait(); + } +#endif + + public override int ReadByte() + { + var val = _inner.ReadByte(); + if (val >= 0) + { + _reads.WriteByte((byte)val); + _reads.Flush(); + } + return val; + } + public override int Read(byte[] buffer, int offset, int count) + { + var len = _inner.Read(buffer, offset, count); + if (len > 0) + { + _reads.Write(buffer, offset, len); + _reads.Flush(); + } + return len; + } + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + var len = await _inner.ReadAsync(buffer, offset, count, cancellationToken).ForAwait(); + if (len > 0) + { + await _reads.WriteAsync(buffer, offset, len, cancellationToken).ForAwait(); + await _reads.FlushAsync(cancellationToken).ForAwait(); + } + return len; + } +#if NETCOREAPP3_0_OR_GREATER + public override int Read(Span buffer) + { + var len = _inner.Read(buffer); + if (len > 0) + { + _reads.Write(buffer.Slice(0, len)); + _reads.Flush(); + } + return len; + } + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) + { + var len = await _inner.ReadAsync(buffer, cancellationToken).ForAwait(); + if (len > 0) + { + await _reads.WriteAsync(buffer.Slice(0, len), cancellationToken).ForAwait(); + await _reads.FlushAsync(cancellationToken).ForAwait(); + } + return len; + } +#endif + + public override void WriteByte(byte value) + { + _writes.WriteByte(value); + _inner.WriteByte(value); + } + public override void Write(byte[] buffer, int offset, int count) + { + _writes.Write(buffer, offset, count); + _inner.Write(buffer, offset, count); + } + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await _writes.WriteAsync(buffer, offset, count, cancellationToken).ForAwait(); + await _inner.WriteAsync(buffer, offset, count, cancellationToken).ForAwait(); + } +#if NETCOREAPP3_0_OR_GREATER + public override void Write(ReadOnlySpan buffer) + { + _writes.Write(buffer); + _inner.Write(buffer); + } + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + await _writes.WriteAsync(buffer, cancellationToken).ForAwait(); + await _inner.WriteAsync(buffer, cancellationToken).ForAwait(); + } +#endif + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 9c467dcd3..d13b6af5b 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -1463,7 +1463,7 @@ public ConnectionStatus GetStatus() }; } - private static RemoteCertificateValidationCallback? GetAmbientIssuerCertificateCallback() + internal static RemoteCertificateValidationCallback? GetAmbientIssuerCertificateCallback() { try { @@ -1476,7 +1476,7 @@ public ConnectionStatus GetStatus() } return null; } - private static LocalCertificateSelectionCallback? GetAmbientClientCertificateCallback() + internal static LocalCertificateSelectionCallback? GetAmbientClientCertificateCallback() { try { diff --git a/src/StackExchange.Redis/RedisResult.cs b/src/StackExchange.Redis/RedisResult.cs index b39a646e0..bf094f8af 100644 --- a/src/StackExchange.Redis/RedisResult.cs +++ b/src/StackExchange.Redis/RedisResult.cs @@ -105,7 +105,7 @@ public static RedisResult Create(RedisResult[] values, ResultType resultType) /// Internally, this is very similar to RawResult, except it is designed to be usable, /// outside of the IO-processing pipeline: the buffers are standalone, etc. /// - internal static bool TryCreate(PhysicalConnection connection, in RawResult result, [NotNullWhen(true)] out RedisResult? redisResult) + internal static bool TryCreate(PhysicalConnection? connection, in RawResult result, [NotNullWhen(true)] out RedisResult? redisResult) { try { From 441f89afeff6d9a55ecf6e413ed5bd29b5aec19d Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 2 Mar 2024 20:53:55 -0500 Subject: [PATCH 279/435] Normalize release notes --- docs/ReleaseNotes.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 18adb415e..fa9a2d8af 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,16 +8,16 @@ Current package versions: ## Unreleased -- Add new `LoggingTunnel` API; see https://stackexchange.github.io/StackExchange.Redis/Logging [#2660 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2660) +- Add new `LoggingTunnel` API; see https://stackexchange.github.io/StackExchange.Redis/Logging ([#2660 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2660)) ## 2.7.27 - Support `HeartbeatConsistencyChecks` and `HeartbeatInterval` in `Clone()` ([#2658 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2658)) -- Add `AddLibraryNameSuffix` to multiplexer; allows usage-specific tokens to be appended *after connect* [#2659 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2659) +- Add `AddLibraryNameSuffix` to multiplexer; allows usage-specific tokens to be appended *after connect* ([#2659 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2659)) ## 2.7.23 -- Fix [#2653](https://github.com/StackExchange/StackExchange.Redis/issues/2653): Client library metadata should validate contents ([#2654](https://github.com/StackExchange/StackExchange.Redis/pull/2654) by mgravell) +- Fix [#2653](https://github.com/StackExchange/StackExchange.Redis/issues/2653): Client library metadata should validate contents ([#2654 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2654)) - Add `HeartbeatConsistencyChecks` option (opt-in) to enabled per-heartbeat (defaults to once per second) checks to be sent to ensure no network stream corruption has occurred ([#2656 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2656)) ## 2.7.20 From 91520e020f352f9fce025bf84d256357bc908a3b Mon Sep 17 00:00:00 2001 From: testfirstcoder Date: Fri, 8 Mar 2024 00:43:51 +0100 Subject: [PATCH 280/435] simplify property initialization (#2666) --- src/StackExchange.Redis/RedisServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index bce499bec..0fec00428 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -330,7 +330,7 @@ public Task FlushDatabaseAsync(int database = -1, CommandFlags flags = CommandFl public ServerCounters GetCounters() => server.GetCounters(); private static IGrouping>[] InfoDefault => - Enumerable.Empty>().GroupBy(k => k.Key).ToArray(); + Array.Empty>>(); public IGrouping>[] Info(RedisValue section = default, CommandFlags flags = CommandFlags.None) { From baf2b1e818f0195253c000b211e5f17203ff6335 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 9 Mar 2024 09:53:11 -0500 Subject: [PATCH 281/435] CheckTrustedIssuer: Fixes for invalid chains (#2665) This issue was brought to my attention last night (thanks to Badrish Chandramouli): https://github.com/dotnet/dotnet-api-docs/pull/6660 This changeset ensures that we do not honor self-signed certs or partial/broken chains as a result of `X509VerificationFlags.AllowUnknownCertificateAuthority` downstream and adds a few tests and utilities to generate test certificates (currently valid for ~9000 days). Instead we are checking that the certificate we're being told to trust is explicitly in the chain, given that the result of `.Build()` cannot be trusted for this case. This also resolves an issue where `TrustIssuer` could be called but we'd error when _no errors_ were detected (due to requiring chain errors in our validator), this means users couldn't temporarily trust a cert while getting it installed on the machine for instance and migrating between the 2 setups was difficult. This needs careful eyes, please scrutinize heavily. It's possible this breaks an existing user, but...it should be broken if so unless there's a case I'm not seeing. --- docs/ReleaseNotes.md | 2 + .../ConfigurationOptions.cs | 66 +++++++++++++++---- .../Certificates/CertValidationTests.cs | 58 ++++++++++++++++ .../Certificates/README.md | 4 ++ .../Certificates/ca.foo.com.pem | 20 ++++++ .../Certificates/ca2.foo.com.pem | 20 ++++++ .../Certificates/create_certificates.sh | 39 +++++++++++ .../Certificates/device01.foo.com.pem | 18 +++++ .../StackExchange.Redis.Tests.csproj | 1 + 9 files changed, 216 insertions(+), 12 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs create mode 100644 tests/StackExchange.Redis.Tests/Certificates/README.md create mode 100644 tests/StackExchange.Redis.Tests/Certificates/ca.foo.com.pem create mode 100644 tests/StackExchange.Redis.Tests/Certificates/ca2.foo.com.pem create mode 100644 tests/StackExchange.Redis.Tests/Certificates/create_certificates.sh create mode 100644 tests/StackExchange.Redis.Tests/Certificates/device01.foo.com.pem diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index fa9a2d8af..ceaf04fc0 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,8 @@ Current package versions: ## Unreleased - Add new `LoggingTunnel` API; see https://stackexchange.github.io/StackExchange.Redis/Logging ([#2660 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2660)) +- **Potentiallty Breaking**: Fix `CheckTrustedIssuer` certificate validation for broken chain scenarios ([#2665 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2665)) + - Users inadvertently trusting a remote cert with a broken chain could not be failing custom validation before this change. This is only in play if you are using `ConfigurationOptions.TrustIssuer` at all. ## 2.7.27 diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 2c47d034d..d4edcb8b1 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -6,6 +6,7 @@ using System.Net.Security; using System.Net.Sockets; using System.Security.Authentication; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; @@ -279,13 +280,13 @@ public bool CheckCertificateRevocation } /// - /// Create a certificate validation check that checks against the supplied issuer even if not known by the machine. + /// Create a certificate validation check that checks against the supplied issuer even when not known by the machine. /// /// The file system path to find the certificate at. public void TrustIssuer(string issuerCertificatePath) => CertificateValidationCallback = TrustIssuerCallback(issuerCertificatePath); /// - /// Create a certificate validation check that checks against the supplied issuer even if not known by the machine. + /// Create a certificate validation check that checks against the supplied issuer even when not known by the machine. /// /// The issuer to trust. public void TrustIssuer(X509Certificate2 issuer) => CertificateValidationCallback = TrustIssuerCallback(issuer); @@ -296,24 +297,65 @@ private static RemoteCertificateValidationCallback TrustIssuerCallback(X509Certi { if (issuer == null) throw new ArgumentNullException(nameof(issuer)); - return (object _, X509Certificate? certificate, X509Chain? __, SslPolicyErrors sslPolicyError) - => sslPolicyError == SslPolicyErrors.RemoteCertificateChainErrors - && certificate is X509Certificate2 v2 - && CheckTrustedIssuer(v2, issuer); + return (object _, X509Certificate? certificate, X509Chain? certificateChain, SslPolicyErrors sslPolicyError) => + { + // If we're already valid, there's nothing further to check + if (sslPolicyError == SslPolicyErrors.None) + { + return true; + } + // If we're not valid due to chain errors - check against the trusted issuer + // Note that we're only proceeding at all here if the *only* issue is chain errors (not more flags in SslPolicyErrors) + return sslPolicyError == SslPolicyErrors.RemoteCertificateChainErrors + && certificate is X509Certificate2 v2 + && CheckTrustedIssuer(v2, certificateChain, issuer); + }; } - private static bool CheckTrustedIssuer(X509Certificate2 certificateToValidate, X509Certificate2 authority) + private static bool CheckTrustedIssuer(X509Certificate2 certificateToValidate, X509Chain? chainToValidate, X509Certificate2 authority) { - // reference: https://stackoverflow.com/questions/6497040/how-do-i-validate-that-a-certificate-was-created-by-a-particular-certification-a - X509Chain chain = new X509Chain(); + // Reference: + // https://stackoverflow.com/questions/6497040/how-do-i-validate-that-a-certificate-was-created-by-a-particular-certification-a + // https://github.com/stewartadam/dotnet-x509-certificate-verification + using X509Chain chain = new X509Chain(); chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; - chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; - chain.ChainPolicy.VerificationTime = DateTime.Now; + chain.ChainPolicy.VerificationTime = chainToValidate?.ChainPolicy?.VerificationTime ?? DateTime.Now; chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0); chain.ChainPolicy.ExtraStore.Add(authority); - return chain.Build(certificateToValidate); + try + { + // This only verifies that the chain is valid, but with AllowUnknownCertificateAuthority could trust + // self-signed or partial chained vertificates + var chainIsVerified = chain.Build(certificateToValidate); + if (chainIsVerified) + { + // Our method is "TrustIssuer", which means any intermediate cert we're being told to trust + // is a valid thing to trust, up until it's a root CA + bool found = false; + byte[] authorityData = authority.RawData; + foreach (var chainElement in chain.ChainElements) + { +#if NET8_0_OR_GREATER +#error TODO: use RawDataMemory (needs testing) +#endif + using var chainCert = chainElement.Certificate; + if (!found && chainCert.RawData.SequenceEqual(authorityData)) + { + found = true; + } + } + return found; + } + } + catch (CryptographicException) + { + // We specifically don't want to throw during validation here and would rather exit out gracefully + } + + // If we didn't find the trusted issuer in the chain at all - we do not trust the result. + return false; } /// diff --git a/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs b/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs new file mode 100644 index 000000000..528944429 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests; + +public class CertValidationTests : TestBase +{ + public CertValidationTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void CheckIssuerValidity() + { + // The endpoint cert is the same here + var endpointCert = LoadCert(Path.Combine("Certificates", "device01.foo.com.pem")); + + // Trusting CA explicitly + var callback = ConfigurationOptions.TrustIssuerCallback(Path.Combine("Certificates", "ca.foo.com.pem")); + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None)); + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors)); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch)); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable)); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch)); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable)); + + // Trusting the remote endpoint cert directly + callback = ConfigurationOptions.TrustIssuerCallback(Path.Combine("Certificates", "device01.foo.com.pem")); + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None)); + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors)); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch)); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable)); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch)); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable)); + + // Attempting to trust another CA (mismatch) + callback = ConfigurationOptions.TrustIssuerCallback(Path.Combine("Certificates", "ca2.foo.com.pem")); + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None)); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors)); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch)); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable)); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch)); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable)); + } + + private static X509Certificate2 LoadCert(string certificatePath) => new X509Certificate2(File.ReadAllBytes(certificatePath)); + + [Fact] + public void CheckIssuerArgs() + { + Assert.ThrowsAny(() => ConfigurationOptions.TrustIssuerCallback("")); + + var opt = new ConfigurationOptions(); + Assert.Throws(() => opt.TrustIssuer((X509Certificate2)null!)); + } +} diff --git a/tests/StackExchange.Redis.Tests/Certificates/README.md b/tests/StackExchange.Redis.Tests/Certificates/README.md new file mode 100644 index 000000000..83fb5a14c --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Certificates/README.md @@ -0,0 +1,4 @@ +The certificates here are randomly generated for testing only. +They are not valid and only used for test validation. + +Please do not file security issue noise - these have no impact being public at all. \ No newline at end of file diff --git a/tests/StackExchange.Redis.Tests/Certificates/ca.foo.com.pem b/tests/StackExchange.Redis.Tests/Certificates/ca.foo.com.pem new file mode 100644 index 000000000..33496adea --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Certificates/ca.foo.com.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDTTCCAjWgAwIBAgIUU9SR3QMGVO8yN/mr8SQJ1p/OgIAwDQYJKoZIhvcNAQEL +BQAwNjETMBEGA1UEAwwKY2EuZm9vLmNvbTESMBAGA1UECgwJTXlEZXZpY2VzMQsw +CQYDVQQGEwJVUzAeFw0yNDAzMDcxNDAxMzJaFw00ODEwMjcxNDAxMzJaMDYxEzAR +BgNVBAMMCmNhLmZvby5jb20xEjAQBgNVBAoMCU15RGV2aWNlczELMAkGA1UEBhMC +VVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqk8FT5dHU335oSEuY +RGeHOHmtxtr5Eoxe4pRHWcBKARzRYi+fPjP/aSWh75yYcmyQ5o5e2JQTZQRwSaLh +q8lrsT7AIeZboATVxECyT3kZdIJkLgWbfyzwJQtrW+ccDx3gDRt0FKRt8Hd3foIf +ULICgkiz3C5sihT589QWmcP4XhcRf3A1bt3rrFWJBO1jmKz0P7pijT14lkdW4sVL +AdFhoNg/a042a7wq2i8PxrkhWpwmHkW9ErnbWG9pRjMme+GDeNfGdHslL5grzbzC +4B4w3QP4opLUp29O9oO1DjaAuZ86JVdy3+glugMvj4f8NVCVlHxRM5Kn/3WgWIIM +XBK7AgMBAAGjUzBRMB0GA1UdDgQWBBRmgj4urVgvTcPgJtyqyUHaFX0svjAfBgNV +HSMEGDAWgBRmgj4urVgvTcPgJtyqyUHaFX0svjAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4IBAQB2DIGlKpCdluVHURfgA5zfwoOnhtOZm7lwC/zbNd5a +wNmb6Vy29feN/+6/dv7MFTXXB5f0TkDGrGbAsKVLD5mNSfQhHC8sxwotheMYq6LS +T1Pjv3Vxku1O7q6FQrslDWfSBxzwep2q8fDXwD4C7VgVRM2EGg/vVee2juuTCmMU +Z1LwJrOkBczW6b3ZvUThFGOvZkuI138EuR2gqPHMQIiQcPyX1syT7yhJAyDQRYOG +cHSRojNciYVotSTgyYguUJdU7vfRJ+MLfoZClzJgvNav8yUC+sSrb5RD5vQlvxzG +KrJ8Hh+hpIFsmQKj5dcochKvLLd1Z748b2+FB0jtxraU +-----END CERTIFICATE----- diff --git a/tests/StackExchange.Redis.Tests/Certificates/ca2.foo.com.pem b/tests/StackExchange.Redis.Tests/Certificates/ca2.foo.com.pem new file mode 100644 index 000000000..b2b18d02b --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Certificates/ca2.foo.com.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDTzCCAjegAwIBAgIUYXv168vvB1NPg3PfoRzcNFEMaC8wDQYJKoZIhvcNAQEL +BQAwNzEUMBIGA1UEAwwLY2EyLmZvby5jb20xEjAQBgNVBAoMCU15RGV2aWNlczEL +MAkGA1UEBhMCVVMwHhcNMjQwMzA3MTQwMTMyWhcNNDgxMDI3MTQwMTMyWjA3MRQw +EgYDVQQDDAtjYTIuZm9vLmNvbTESMBAGA1UECgwJTXlEZXZpY2VzMQswCQYDVQQG +EwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOFDZ2sf8Ik/I3jD +Mp4NGoo+kMY1BiRWjSKnaphfcosR2MAT/ROIVhnkzSeiQQByf34cqN/7xNkHaufr +oVcMuuiyWERPoZjBqnfzLZ+93uxnyIU6DVDdNIcKcBQxyhHMfOigFhKTia6eWhrf +zSaBhbkndaUsXdINBAJgSq3HDuk8bIw8MTZH0giorhIyyyqT/gjWEbzKx6Ww99qV +MMcjFIvXEmD9AEaNilHD4TtwqZrZKZpnVBaQvWrCK3tCGBDyiFlUhAibchbt/JzV +sK002TFfUbn41ygHmcrBVL7lSEsuT2W/PNuDOdWa6NQ2RVzYivs5jYbWV1cAvAJP +HMWJkZ8CAwEAAaNTMFEwHQYDVR0OBBYEFA6ZeCMPgDEu+eIUoCxU/Q06ViyoMB8G +A1UdIwQYMBaAFA6ZeCMPgDEu+eIUoCxU/Q06ViyoMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggEBAGOa/AD0JNPwvyDi9wbVU+Yktx3vfuVyOMbnUQSn +nOyWd6d95rZwbeYyN908PjERQT3EMo8/O0eOpoG9I79vjbcD6LAIbxS9XdI8kK4+ +D4e/DX/R85KoWSprB+VRXGqsChY0Y+4x5x2q/IoCi6+tywhzjqIlaGDYrlc688HO +/+4iR9L945gpg4NT1hLnCwDYcdZ5vjv4NfgXDbGPUcEheYnfz3cHE2mYxEG9KXta +f8hSj/MNNv31BzNcj4XKcDqp4Ke3ow4lAZsPPlixOxxRaLnpsKZmEYYQcLI8KVNk +gdAUOSPZgzRqAag0rvVfrpyvfvlu0D9xeiBLdhaJeZCq1/s= +-----END CERTIFICATE----- diff --git a/tests/StackExchange.Redis.Tests/Certificates/create_certificates.sh b/tests/StackExchange.Redis.Tests/Certificates/create_certificates.sh new file mode 100644 index 000000000..71ef0caa3 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Certificates/create_certificates.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -eu +# Adapted from https://github.com/stewartadam/dotnet-x509-certificate-verification/blob/main/create_certificates.sh + +base_dir="certificates" + +create_ca() { + local CA_CN="$1" + local certificate_output="${base_dir}/${CA_CN}.pem" + + openssl genrsa -out "${base_dir}/${CA_CN}.key.pem" 2048 # Generate private key + openssl req -x509 -new -nodes -key "${base_dir}/${CA_CN}.key.pem" -sha256 -days 9000 -out "${certificate_output}" -subj "/CN=${CA_CN}/O=MyDevices/C=US" # Generate root certificate + + echo -e "\nCertificate for CA ${CA_CN} saved to ${certificate_output}\n\n" +} + +create_leaf_cert_req() { + local DEVICE_CN="$1" + + openssl genrsa -out "${base_dir}/${DEVICE_CN}.key.pem" 2048 # new private key + openssl req -new -key "${base_dir}/${DEVICE_CN}.key.pem" -out "${base_dir}/${DEVICE_CN}.csr.pem" -subj "/CN=${DEVICE_CN}/O=MyDevices/C=US" # generate signing request for the CA +} + +sign_leaf_cert() { + local DEVICE_CN="$1" + local CA_CN="$2" + local certificate_output="${base_dir}/${DEVICE_CN}.pem" + + openssl x509 -req -in "${base_dir}/${DEVICE_CN}.csr.pem" -CA ""${base_dir}/${CA_CN}.pem"" -CAkey "${base_dir}/${CA_CN}.key.pem" -set_serial 01 -out "${certificate_output}" -days 8999 -sha256 # sign the CSR + + echo -e "\nCertificate for ${DEVICE_CN} saved to ${certificate_output}\n\n" +} + +mkdir -p "${base_dir}" + +# Create one self-issued CA + signed cert +create_ca "ca.foo.com" +create_leaf_cert_req "device01.foo.com" +sign_leaf_cert "device01.foo.com" "ca.foo.com" \ No newline at end of file diff --git a/tests/StackExchange.Redis.Tests/Certificates/device01.foo.com.pem b/tests/StackExchange.Redis.Tests/Certificates/device01.foo.com.pem new file mode 100644 index 000000000..58f47641b --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Certificates/device01.foo.com.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC5jCCAc4CAQEwDQYJKoZIhvcNAQELBQAwNjETMBEGA1UEAwwKY2EuZm9vLmNv +bTESMBAGA1UECgwJTXlEZXZpY2VzMQswCQYDVQQGEwJVUzAeFw0yNDAzMDcxNDAx +MzJaFw00ODEwMjYxNDAxMzJaMDwxGTAXBgNVBAMMEGRldmljZTAxLmZvby5jb20x +EjAQBgNVBAoMCU15RGV2aWNlczELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDBb4Mv87+MFVGLIWArc0wV1GH4h7Ha+49K+JAi8rtk +fpQACcu3OGq5TjUuxecOz5eXDwJj/vR1rvjP0DaCuIlx4SNXXqVKooWpCLb2g4Mr +IIiFcBsiaJNmhFvd92bqHOyuXsUTjkJKaLmH6nUqVIXEA/Py+jpuSFRp9N475IGZ +yUUdaQUx9Ur953FagLbPVeE5Vh+NEA8vnw+ZBCQRBHlRgvSJtCAR/oznXXPdHGGZ +rMWeNjl+v1iP8fZMq4vvooW0/zTVgH8lE/HJXtpaWEVeGpnOqBsgvl12WTGL5dMU +n81JiI3AdUyW0ieh/5yr+OFNa/HNqGLK1NvnCDPbBFpnAgMBAAEwDQYJKoZIhvcN +AQELBQADggEBAEpIIJJ7q4/EbCJog29Os9l5unn7QJon4R5TGJQIxdqDGrhXG8QA +HiBGl/lFhAp9tfKvQIj4aODzMgHmDpzZmH/yhjDlquJgB4JYDDjf9UhtwUUbRDp0 +rEc5VikLuTJ21hcusKALH5fgBjzplRNPH8P660FxWnv9gSWCMNaiFCCxTU91g4L3 +/qZPTl5nr1j6M/+zXbndS5qlF7GkU5Kv9xEmasZ0Z65Wao6Ufhw+nczLbiLErrxD +zLtTfr6WYFqzXeiPGnjTueG+1cYDjngmj2fbtjPn4W67q8Z0M/ZMj9ikr881d3zP +3dzUEaGexGqvA2MCapIQ2vCCMDF33ismQts= +-----END CERTIFICATE----- diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index 9e0d5f36a..4e354b846 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -13,6 +13,7 @@ + From 4da3d629a657a33b4d898af59ab2b56ac423d3f1 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 11 Mar 2024 09:41:35 -0400 Subject: [PATCH 282/435] Fix #2664 Backlog: move to full synchronous behavior since we're on a dedicated thread (#2667) Resolving #2664. This went through a few iterations early on, but in the current form the issue is that we can await hop to a thread pool thread and have our wait blocking there - not awesome. We want to avoid that entirely and do a full sync path here. --- docs/ReleaseNotes.md | 3 ++- src/StackExchange.Redis/PhysicalBridge.cs | 16 +++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index ceaf04fc0..71f46db78 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,9 +8,10 @@ Current package versions: ## Unreleased -- Add new `LoggingTunnel` API; see https://stackexchange.github.io/StackExchange.Redis/Logging ([#2660 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2660)) - **Potentiallty Breaking**: Fix `CheckTrustedIssuer` certificate validation for broken chain scenarios ([#2665 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2665)) - Users inadvertently trusting a remote cert with a broken chain could not be failing custom validation before this change. This is only in play if you are using `ConfigurationOptions.TrustIssuer` at all. +- Add new `LoggingTunnel` API; see https://stackexchange.github.io/StackExchange.Redis/Logging ([#2660 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2660)) +- Fix [#2664](https://github.com/StackExchange/StackExchange.Redis/issues/2664): Move ProcessBacklog to fully sync to prevent thread pool hopping and blocking on awaits ([#2667 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2667)) ## 2.7.27 diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index f870cf340..522e2fcc9 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -904,7 +904,7 @@ private void StartBacklogProcessor() // to unblock the thread-pool when there could be sync-over-async callers. Note that in reality, // the initial "enough" of the back-log processor is typically sync, which means that the thread // we start is actually useful, despite thinking "but that will just go async and back to the pool" - var thread = new Thread(s => ((PhysicalBridge)s!).ProcessBacklogAsync().RedisFireAndForget()) + var thread = new Thread(s => ((PhysicalBridge)s!).ProcessBacklog()) { IsBackground = true, // don't keep process alive (also: act like the thread-pool used to) Name = "StackExchange.Redis Backlog", // help anyone looking at thread-dumps @@ -985,7 +985,7 @@ internal enum BacklogStatus : byte /// Process the backlog(s) in play if any. /// This means flushing commands to an available/active connection (if any) or spinning until timeout if not. /// - private async Task ProcessBacklogAsync() + private void ProcessBacklog() { _backlogStatus = BacklogStatus.Starting; try @@ -997,7 +997,7 @@ private async Task ProcessBacklogAsync() // TODO: vNext handoff this backlog to another primary ("can handle everything") connection // and remove any per-server commands. This means we need to track a bit of whether something // was server-endpoint-specific in PrepareToPushMessageToBridge (was the server ref null or not) - await ProcessBridgeBacklogAsync().ForAwait(); + ProcessBridgeBacklog(); } // The cost of starting a new thread is high, and we can bounce in and out of the backlog a lot. @@ -1070,7 +1070,7 @@ private async Task ProcessBacklogAsync() /// private readonly AutoResetEvent _backlogAutoReset = new AutoResetEvent(false); - private async Task ProcessBridgeBacklogAsync() + private void ProcessBridgeBacklog() { // Importantly: don't assume we have a physical connection here // We are very likely to hit a state where it's not re-established or even referenced here @@ -1097,10 +1097,10 @@ private async Task ProcessBridgeBacklogAsync() // try and get the lock; if unsuccessful, retry #if NETCOREAPP - gotLock = await _singleWriterMutex.WaitAsync(TimeoutMilliseconds).ForAwait(); + gotLock = _singleWriterMutex.Wait(TimeoutMilliseconds); if (gotLock) break; // got the lock; now go do something with it #else - token = await _singleWriterMutex.TryWaitAsync().ForAwait(); + token = _singleWriterMutex.TryWait(); if (token.Success) break; // got the lock; now go do something with it #endif } @@ -1132,7 +1132,9 @@ private async Task ProcessBridgeBacklogAsync() if (result == WriteResult.Success) { _backlogStatus = BacklogStatus.Flushing; - result = await physical.FlushAsync(false).ForAwait(); +#pragma warning disable CS0618 // Type or member is obsolete + result = physical.FlushSync(false, TimeoutMilliseconds); +#pragma warning restore CS0618 // Type or member is obsolete } _backlogStatus = BacklogStatus.MarkingInactive; From 60e5d174c87178aff5906b1a0f8d9755f7e6a669 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 11 Mar 2024 11:04:36 -0400 Subject: [PATCH 283/435] 2.7.33 Release notes --- docs/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 71f46db78..6da46fb6a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,6 +7,9 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased +No pending/unreleased changes. + +## 2.7.33 - **Potentiallty Breaking**: Fix `CheckTrustedIssuer` certificate validation for broken chain scenarios ([#2665 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2665)) - Users inadvertently trusting a remote cert with a broken chain could not be failing custom validation before this change. This is only in play if you are using `ConfigurationOptions.TrustIssuer` at all. From 2f69707d87488febb7376739ed4dc51ad9a422c6 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 12 Mar 2024 17:54:11 -0400 Subject: [PATCH 284/435] TLS: Check EKU in X509 chain checks (#2670) Further hardening following #2665. This is an additional check to match the .NET implementation for TLS cert checks so that we don't treat a cert flagged as non-TLS-server effectively. This ensures that a certificate either doesn't have OIDs here (valid, backwards compatible) or has the server-certificate OID indicating it's valid for consumption over TLS for us. Cheers @bartonjs for the report and info here. --- docs/ReleaseNotes.md | 2 +- src/StackExchange.Redis/ConfigurationOptions.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 6da46fb6a..0a12e103d 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,7 +7,7 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased -No pending/unreleased changes. +- TLS certificate/`TrustIssuer`: Check EKU in X509 chain checks when validating cerificates ([#2670 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2670)) ## 2.7.33 diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index d4edcb8b1..3d0a3aae8 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -312,6 +312,8 @@ private static RemoteCertificateValidationCallback TrustIssuerCallback(X509Certi }; } + private static readonly Oid _serverAuthOid = new Oid("1.3.6.1.5.5.7.3.1", "1.3.6.1.5.5.7.3.1"); + private static bool CheckTrustedIssuer(X509Certificate2 certificateToValidate, X509Chain? chainToValidate, X509Certificate2 authority) { // Reference: @@ -322,6 +324,8 @@ private static bool CheckTrustedIssuer(X509Certificate2 certificateToValidate, X chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; chain.ChainPolicy.VerificationTime = chainToValidate?.ChainPolicy?.VerificationTime ?? DateTime.Now; chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0); + // Ensure entended key usage checks are run and that we're observing a server TLS certificate + chain.ChainPolicy.ApplicationPolicy.Add(_serverAuthOid); chain.ChainPolicy.ExtraStore.Add(authority); try From cb8b20df0e2975717bde97ce95ac20e8e8353572 Mon Sep 17 00:00:00 2001 From: Philo Date: Fri, 12 Apr 2024 12:26:48 -0700 Subject: [PATCH 285/435] Correct link to RespLogging doc (#2696) Also fix a couple typos --- docs/ReleaseNotes.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 0a12e103d..b4dd0fd4f 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,13 +7,13 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased -- TLS certificate/`TrustIssuer`: Check EKU in X509 chain checks when validating cerificates ([#2670 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2670)) +- TLS certificate/`TrustIssuer`: Check EKU in X509 chain checks when validating certificates ([#2670 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2670)) ## 2.7.33 -- **Potentiallty Breaking**: Fix `CheckTrustedIssuer` certificate validation for broken chain scenarios ([#2665 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2665)) +- **Potentially Breaking**: Fix `CheckTrustedIssuer` certificate validation for broken chain scenarios ([#2665 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2665)) - Users inadvertently trusting a remote cert with a broken chain could not be failing custom validation before this change. This is only in play if you are using `ConfigurationOptions.TrustIssuer` at all. -- Add new `LoggingTunnel` API; see https://stackexchange.github.io/StackExchange.Redis/Logging ([#2660 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2660)) +- Add new `LoggingTunnel` API; see [https://stackexchange.github.io/StackExchange.Redis/RespLogging](https://stackexchange.github.io/StackExchange.Redis/RespLogging) ([#2660 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2660)) - Fix [#2664](https://github.com/StackExchange/StackExchange.Redis/issues/2664): Move ProcessBacklog to fully sync to prevent thread pool hopping and blocking on awaits ([#2667 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2667)) ## 2.7.27 From 61c13c21844ff3e92eb077523dc876688878ba25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 7 May 2024 17:18:12 +0200 Subject: [PATCH 286/435] Replace all occurrences of "nil" in `IDatabase(Async)` xmldoc with less ambiguous alternatives (#2702) Closes https://github.com/StackExchange/StackExchange.Redis/issues/2697. All of the replacements were empirically tested to be correct via simple programs in combination with a local redis instance. Notably, there is one worrying nit; in testing it turns out that the `IDatabase.List{Left,Right}Pop(RedisKey, long, CommandFlags)` overload which I talked about in the issue _can_ actually return null, contrary to its nullability annotations. This occurs on missing key; in that case redis replies Nil reply: if the key does not exist. as per https://redis.io/docs/latest/commands/lpop/, which then at https://github.com/StackExchange/StackExchange.Redis/blob/cb8b20df0e2975717bde97ce95ac20e8e8353572/src/StackExchange.Redis/ResultProcessor.cs#L1546-L1547 and later at https://github.com/StackExchange/StackExchange.Redis/blob/cb8b20df0e2975717bde97ce95ac20e8e8353572/src/StackExchange.Redis/ExtensionMethods.cs#L339-L341 turns into a `null`. I briefly attempted to rectify this, but the `RedisValueArrayProcessor` poses a problem here, as changing it to derive `ResultProcessor` causes the solution to light up in red, and I'd rather not mess with that as a first contribution without at least prior discussion concerning direction there. --- .../Interfaces/IDatabase.cs | 57 ++++++++++--------- .../Interfaces/IDatabaseAsync.cs | 57 ++++++++++--------- 2 files changed, 58 insertions(+), 56 deletions(-) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index e5c120eb9..7cf2248fd 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -331,7 +331,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the hash. /// The field in the hash to get. /// The flags to use for this operation. - /// The value associated with field, or nil when field is not present in the hash or key does not exist. + /// The value associated with field, or when field is not present in the hash or key does not exist. /// RedisValue HashGet(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); @@ -341,13 +341,14 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the hash. /// The field in the hash to get. /// The flags to use for this operation. - /// The value associated with field, or nil when field is not present in the hash or key does not exist. + /// The value associated with field, or when field is not present in the hash or key does not exist. /// Lease? HashGetLease(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// /// Returns the values associated with the specified fields in the hash stored at key. - /// For every field that does not exist in the hash, a nil value is returned.Because a non-existing keys are treated as empty hashes, running HMGET against a non-existing key will return a list of nil values. + /// For every field that does not exist in the hash, a value is returned. + /// Because non-existing keys are treated as empty hashes, running HMGET against a non-existing key will return a list of values. /// /// The key of the hash. /// The fields in the hash to get. @@ -802,7 +803,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// Return a random key from the currently selected database. /// /// The flags to use for this operation. - /// The random key, or nil when the database is empty. + /// The random key, or when the database is empty. /// RedisKey KeyRandom(CommandFlags flags = CommandFlags.None); @@ -847,7 +848,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The key to check. /// The flags to use for this operation. - /// TTL, or nil when key does not exist or does not have a timeout. + /// TTL, or when key does not exist or does not have a timeout. /// TimeSpan? KeyTimeToLive(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -888,7 +889,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the list. /// The index position to get the value at. /// The flags to use for this operation. - /// The requested element, or nil when index is out of range. + /// The requested element, or when index is out of range. /// RedisValue ListGetByIndex(RedisKey key, long index, CommandFlags flags = CommandFlags.None); @@ -921,7 +922,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The key of the list. /// The flags to use for this operation. - /// The value of the first element, or nil when key does not exist. + /// The value of the first element, or when key does not exist. /// RedisValue ListLeftPop(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -932,7 +933,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the list. /// The number of elements to remove /// The flags to use for this operation. - /// Array of values that were popped, or nil if the key doesn't exist. + /// Array of values that were popped, or if the key doesn't exist. /// RedisValue[] ListLeftPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None); @@ -1075,7 +1076,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The key of the list. /// The flags to use for this operation. - /// The element being popped. + /// The element being popped, or when key does not exist.. /// RedisValue ListRightPop(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -1086,7 +1087,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The key of the list. /// The number of elements to pop /// The flags to use for this operation. - /// Array of values that were popped, or nil if the key doesn't exist. + /// Array of values that were popped, or if the key doesn't exist. /// RedisValue[] ListRightPop(RedisKey key, long count, CommandFlags flags = CommandFlags.None); @@ -1494,7 +1495,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The key of the set. /// The flags to use for this operation. - /// The removed element, or nil when key does not exist. + /// The removed element, or when key does not exist. /// RedisValue SetPop(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -2122,7 +2123,7 @@ IEnumerable SortedSetScan(RedisKey key, /// /// Returns the score of member in the sorted set at key. - /// If member does not exist in the sorted set, or key does not exist, nil is returned. + /// If member does not exist in the sorted set, or key does not exist, is returned. /// /// The key of the sorted set. /// The member to get a score for. @@ -2151,7 +2152,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the sorted set. /// The order to sort by (defaults to ascending). /// The flags to use for this operation. - /// The removed element, or nil when key does not exist. + /// The removed element, or when key does not exist. /// /// , /// @@ -2674,32 +2675,32 @@ IEnumerable SortedSetScan(RedisKey key, double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None); /// - /// Get the value of key. If the key does not exist the special value nil is returned. + /// Get the value of key. If the key does not exist the special value is returned. /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. - /// The value of key, or nil when key does not exist. + /// The value of key, or when key does not exist. /// RedisValue StringGet(RedisKey key, CommandFlags flags = CommandFlags.None); /// /// Returns the values of all specified keys. - /// For every key that does not hold a string value or does not exist, the special value nil is returned. + /// For every key that does not hold a string value or does not exist, the special value is returned. /// /// The keys of the strings. /// The flags to use for this operation. - /// The values of the strings with nil for keys do not exist. + /// The values of the strings with for keys do not exist. /// RedisValue[] StringGet(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Get the value of key. If the key does not exist the special value nil is returned. + /// Get the value of key. If the key does not exist the special value is returned. /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. - /// The value of key, or nil when key does not exist. + /// The value of key, or when key does not exist. /// Lease? StringGetLease(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -2733,7 +2734,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the string. /// The value to replace the existing value with. /// The flags to use for this operation. - /// The old value stored at key, or nil when key did not exist. + /// The old value stored at key, or when key did not exist. /// RedisValue StringGetSet(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); @@ -2744,7 +2745,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the string. /// The expiry to set. will remove expiry. /// The flags to use for this operation. - /// The value of key, or nil when key does not exist. + /// The value of key, or when key does not exist. /// RedisValue StringGetSetExpiry(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); @@ -2755,29 +2756,29 @@ IEnumerable SortedSetScan(RedisKey key, /// The key of the string. /// The exact date and time to expire at. will remove expiry. /// The flags to use for this operation. - /// The value of key, or nil when key does not exist. + /// The value of key, or when key does not exist. /// RedisValue StringGetSetExpiry(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None); /// /// Get the value of key and delete the key. - /// If the key does not exist the special value nil is returned. + /// If the key does not exist the special value is returned. /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. - /// The value of key, or nil when key does not exist. + /// The value of key, or when key does not exist. /// RedisValue StringGetDelete(RedisKey key, CommandFlags flags = CommandFlags.None); /// /// Get the value of key. - /// If the key does not exist the special value nil is returned. + /// If the key does not exist the special value is returned. /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. - /// The value of key and its expiry, or nil when key does not exist. + /// The value of key and its expiry, or when key does not exist. /// RedisValueWithExpiry StringGetWithExpiry(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -2901,7 +2902,7 @@ IEnumerable SortedSetScan(RedisKey key, /// The expiry to set. /// Which condition to set the value under (defaults to ). /// The flags to use for this operation. - /// The previous value stored at , or nil when key did not exist. + /// The previous value stored at , or when key did not exist. /// /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. /// @@ -2917,7 +2918,7 @@ IEnumerable SortedSetScan(RedisKey key, /// Whether to maintain the existing key's TTL (KEEPTTL flag). /// Which condition to set the value under (defaults to ). /// The flags to use for this operation. - /// The previous value stored at , or nil when key did not exist. + /// The previous value stored at , or when key did not exist. /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. /// RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 4a9cd400d..a19525925 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -318,7 +318,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the hash. /// The field in the hash to get. /// The flags to use for this operation. - /// The value associated with field, or nil when field is not present in the hash or key does not exist. + /// The value associated with field, or when field is not present in the hash or key does not exist. /// Task HashGetAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); @@ -328,13 +328,14 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the hash. /// The field in the hash to get. /// The flags to use for this operation. - /// The value associated with field, or nil when field is not present in the hash or key does not exist. + /// The value associated with field, or when field is not present in the hash or key does not exist. /// Task?> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); /// /// Returns the values associated with the specified fields in the hash stored at key. - /// For every field that does not exist in the hash, a nil value is returned.Because a non-existing keys are treated as empty hashes, running HMGET against a non-existing key will return a list of nil values. + /// For every field that does not exist in the hash, a value is returned. + /// Because a non-existing keys are treated as empty hashes, running HMGET against a non-existing key will return a list of values. /// /// The key of the hash. /// The fields in the hash to get. @@ -778,7 +779,7 @@ public interface IDatabaseAsync : IRedisAsync /// Return a random key from the currently selected database. /// /// The flags to use for this operation. - /// The random key, or nil when the database is empty. + /// The random key, or when the database is empty. /// Task KeyRandomAsync(CommandFlags flags = CommandFlags.None); @@ -823,7 +824,7 @@ public interface IDatabaseAsync : IRedisAsync /// /// The key to check. /// The flags to use for this operation. - /// TTL, or nil when key does not exist or does not have a timeout. + /// TTL, or when key does not exist or does not have a timeout. /// Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -864,7 +865,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the list. /// The index position to get the value at. /// The flags to use for this operation. - /// The requested element, or nil when index is out of range. + /// The requested element, or when index is out of range. /// Task ListGetByIndexAsync(RedisKey key, long index, CommandFlags flags = CommandFlags.None); @@ -897,7 +898,7 @@ public interface IDatabaseAsync : IRedisAsync /// /// The key of the list. /// The flags to use for this operation. - /// The value of the first element, or nil when key does not exist. + /// The value of the first element, or when key does not exist. /// Task ListLeftPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -908,7 +909,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the list. /// The number of elements to remove /// The flags to use for this operation. - /// Array of values that were popped, or nil if the key doesn't exist. + /// Array of values that were popped, or if the key doesn't exist. /// Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); @@ -1051,7 +1052,7 @@ public interface IDatabaseAsync : IRedisAsync /// /// The key of the list. /// The flags to use for this operation. - /// The element being popped. + /// The element being popped, or when key does not exist.. /// Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -1062,7 +1063,7 @@ public interface IDatabaseAsync : IRedisAsync /// The key of the list. /// The number of elements to pop /// The flags to use for this operation. - /// Array of values that were popped, or nil if the key doesn't exist. + /// Array of values that were popped, or if the key doesn't exist. /// Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); @@ -1470,7 +1471,7 @@ public interface IDatabaseAsync : IRedisAsync /// /// The key of the set. /// The flags to use for this operation. - /// The removed element, or nil when key does not exist. + /// The removed element, or when key does not exist. /// Task SetPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -2075,7 +2076,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// /// Returns the score of member in the sorted set at key. - /// If member does not exist in the sorted set, or key does not exist, nil is returned. + /// If member does not exist in the sorted set, or key does not exist, is returned. /// /// The key of the sorted set. /// The member to get a score for. @@ -2127,7 +2128,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The key of the sorted set. /// The order to sort by (defaults to ascending). /// The flags to use for this operation. - /// The removed element, or nil when key does not exist. + /// The removed element, or when key does not exist. /// /// , /// @@ -2627,32 +2628,32 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, Task StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None); /// - /// Get the value of key. If the key does not exist the special value nil is returned. + /// Get the value of key. If the key does not exist the special value is returned. /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. - /// The value of key, or nil when key does not exist. + /// The value of key, or when key does not exist. /// Task StringGetAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// /// Returns the values of all specified keys. - /// For every key that does not hold a string value or does not exist, the special value nil is returned. + /// For every key that does not hold a string value or does not exist, the special value is returned. /// /// The keys of the strings. /// The flags to use for this operation. - /// The values of the strings with nil for keys do not exist. + /// The values of the strings with for keys do not exist. /// Task StringGetAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Get the value of key. If the key does not exist the special value nil is returned. + /// Get the value of key. If the key does not exist the special value is returned. /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. - /// The value of key, or nil when key does not exist. + /// The value of key, or when key does not exist. /// Task?> StringGetLeaseAsync(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -2686,7 +2687,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The key of the string. /// The value to replace the existing value with. /// The flags to use for this operation. - /// The old value stored at key, or nil when key did not exist. + /// The old value stored at key, or when key did not exist. /// Task StringGetSetAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); @@ -2697,7 +2698,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The key of the string. /// The expiry to set. will remove expiry. /// The flags to use for this operation. - /// The value of key, or nil when key does not exist. + /// The value of key, or when key does not exist. /// Task StringGetSetExpiryAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); @@ -2708,29 +2709,29 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The key of the string. /// The exact date and time to expire at. will remove expiry. /// The flags to use for this operation. - /// The value of key, or nil when key does not exist. + /// The value of key, or when key does not exist. /// Task StringGetSetExpiryAsync(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None); /// /// Get the value of key and delete the key. - /// If the key does not exist the special value nil is returned. + /// If the key does not exist the special value is returned. /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. - /// The value of key, or nil when key does not exist. + /// The value of key, or when key does not exist. /// Task StringGetDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None); /// /// Get the value of key. - /// If the key does not exist the special value nil is returned. + /// If the key does not exist the special value is returned. /// An error is returned if the value stored at key is not a string, because GET only handles string values. /// /// The key of the string. /// The flags to use for this operation. - /// The value of key and its expiry, or nil when key does not exist. + /// The value of key and its expiry, or when key does not exist. /// Task StringGetWithExpiryAsync(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -2854,7 +2855,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// The expiry to set. /// Which condition to set the value under (defaults to ). /// The flags to use for this operation. - /// The previous value stored at , or nil when key did not exist. + /// The previous value stored at , or when key did not exist. /// /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. /// @@ -2870,7 +2871,7 @@ IAsyncEnumerable SortedSetScanAsync(RedisKey key, /// Whether to maintain the existing key's TTL (KEEPTTL flag). /// Which condition to set the value under (defaults to ). /// The flags to use for this operation. - /// The previous value stored at , or nil when key did not exist. + /// The previous value stored at , or when key did not exist. /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. /// Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); From 39b992fc4b7a32a6d96ff02f516f863807501a0c Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Sat, 8 Jun 2024 15:10:14 +0300 Subject: [PATCH 287/435] support reading from last message from stream with xread (#2725) * support reading from last message from stream with xread * recover previous formatting * add redisversion and use it in integration tests --- src/StackExchange.Redis/RedisFeatures.cs | 3 +- .../StackExchange.Redis.Tests/StreamTests.cs | 61 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 7e3bb77dc..a1bb14a8b 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -40,7 +40,8 @@ namespace StackExchange.Redis v6_0_6 = new Version(6, 0, 6), v6_2_0 = new Version(6, 2, 0), v7_0_0_rc1 = new Version(6, 9, 240), // 7.0 RC1 is version 6.9.240 - v7_2_0_rc1 = new Version(7, 1, 240); // 7.2 RC1 is version 7.1.240 + v7_2_0_rc1 = new Version(7, 1, 240), // 7.2 RC1 is version 7.1.240 + v7_4_0_rc1 = new Version(7, 4, 240); // 7.4 RC1 is version 7.4.240 private readonly Version version; diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index 305e38298..fefff3b02 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -1511,6 +1511,24 @@ public void StreamReadEmptyStreams() Assert.Equal(0, len2); } + [Fact] + public void StreamReadLastMessage() + { + using var conn = Create(require: RedisFeatures.v7_4_0_rc1); + var db = conn.GetDatabase(); + var key1 = Me(); + + // Read the entire stream from the beginning. + db.StreamRead(key1, "0-0"); + db.StreamAdd(key1, "field2", "value2"); + db.StreamAdd(key1, "fieldLast", "valueLast"); + var entries = db.StreamRead(key1, "+"); + + Assert.NotNull(entries); + Assert.True(entries.Length > 0); + Assert.Equal(new[] { new NameValueEntry("fieldLast", "valueLast") }, entries[0].Values); + } + [Fact] public void StreamReadExpectedExceptionInvalidCountMultipleStream() { @@ -1590,6 +1608,49 @@ public void StreamReadMultipleStreams() Assert.Equal(id4, streams[1].Entries[1].Id); } + [Fact] + public void StreamReadMultipleStreamsLastMessage() + { + using var conn = Create(require: RedisFeatures.v7_4_0_rc1); + + var db = conn.GetDatabase(); + var key1 = Me() + "a"; + var key2 = Me() + "b"; + + var id1 = db.StreamAdd(key1, "field1", "value1"); + var id2 = db.StreamAdd(key1, "field2", "value2"); + var id3 = db.StreamAdd(key2, "field3", "value3"); + var id4 = db.StreamAdd(key2, "field4", "value4"); + + var streamList = new[] { new StreamPosition(key1, "0-0"), new StreamPosition(key2, "0-0") }; + db.StreamRead(streamList); + + var streams = db.StreamRead(streamList); + + db.StreamAdd(key1, "field5", "value5"); + db.StreamAdd(key1, "field6", "value6"); + db.StreamAdd(key2, "field7", "value7"); + db.StreamAdd(key2, "field8", "value8"); + + streamList = new[] { new StreamPosition(key1, "+"), new StreamPosition(key2, "+") }; + + streams = db.StreamRead(streamList); + + Assert.NotNull(streams); + Assert.True(streams.Length == 2); + + var stream1 = streams.Where(e => e.Key == key1).First(); + Assert.NotNull(stream1.Entries); + Assert.True(stream1.Entries.Length > 0); + Assert.Equal(new[] { new NameValueEntry("field6", "value6") }, stream1.Entries[0].Values); + + var stream2 = streams.Where(e => e.Key == key2).First(); + Assert.NotNull(stream2.Entries); + Assert.True(stream2.Entries.Length > 0); + Assert.Equal(new[] { new NameValueEntry("field8", "value8") }, stream2.Entries[0].Values); + } + + [Fact] public void StreamReadMultipleStreamsWithCount() { From 1de8dac8ee593108ea5562683cc797e3ca0b2973 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 24 Jun 2024 16:17:38 +0100 Subject: [PATCH 288/435] Implement high integrity mode for commands (#2741) * initial implementation of #2706 * build: net8 compat * Docs: add HighIntegrity initial docs * PR fixups * Options fixes * Update tests/StackExchange.Redis.Tests/TestBase.cs * add tests that result boxes / continuations work correctly for high-integrity-mode * - switch to counter rather than entropy - add transaction work to basic tests, to ensure handled * naming is hard * - add explicit connection failure type - burn the connection on failure - add initial metrics * benchmark impact of high performance mode * be more flexible in HeartbeatConsistencyCheckPingsAsync * add config tests --------- Co-authored-by: Nick Craver Co-authored-by: Nick Craver --- docs/Configuration.md | 5 + .../Configuration/DefaultOptionsProvider.cs | 11 ++ .../ConfigurationOptions.cs | 28 +++- .../Enums/ConnectionFailureType.cs | 6 +- src/StackExchange.Redis/Message.cs | 32 +++++ src/StackExchange.Redis/PhysicalBridge.cs | 42 ++++++ src/StackExchange.Redis/PhysicalConnection.cs | 122 +++++++++++++++--- .../PublicAPI/PublicAPI.Shipped.txt | 4 + .../PublicAPI/PublicAPI.Unshipped.txt | 2 +- tests/BasicTest/BasicTest.csproj | 2 +- .../BasicTestBaseline.csproj | 2 +- tests/ConsoleTest/Program.cs | 89 +++++++++++-- .../ConsoleTestBaseline.csproj | 1 + .../StackExchange.Redis.Tests/BasicOpTests.cs | 48 ++++++- .../StackExchange.Redis.Tests/ConfigTests.cs | 23 +++- .../ConnectCustomConfigTests.cs | 2 +- .../ResultBoxTests.cs | 95 ++++++++++++++ tests/StackExchange.Redis.Tests/TestBase.cs | 17 ++- 18 files changed, 485 insertions(+), 46 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/ResultBoxTests.cs diff --git a/docs/Configuration.md b/docs/Configuration.md index 323d89984..96e4b5bae 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -99,6 +99,7 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a | tunnel={string} | `Tunnel` | `null` | Tunnel for connections (use `http:{proxy url}` for "connect"-based proxy server) | | setlib={bool} | `SetClientLibrary` | `true` | Whether to attempt to use `CLIENT SETINFO` to set the library name/version on the connection | | protocol={string} | `Protocol` | `null` | Redis protocol to use; see section below | +| highIntegrity={bool} | `HighIntegrity` | `false` | High integrity (incurs overhead) sequence checking on every command; see section below | Additional code-only options: - LoggerFactory (`ILoggerFactory`) - Default: `null` @@ -115,6 +116,10 @@ Additional code-only options: - The thread pool to use for scheduling work to and from the socket connected to Redis, one of... - `SocketManager.Shared`: Use a shared dedicated thread pool for _all_ multiplexers (defaults to 10 threads) - best balance for most scenarios. - `SocketManager.ThreadPool`: Use the build-in .NET thread pool for scheduling. This can perform better for very small numbers of cores or with large apps on large machines that need to use more than 10 threads (total, across all multiplexers) under load. **Important**: this option isn't the default because it's subject to thread pool growth/starvation and if for example synchronous calls are waiting on a redis command to come back to unblock other threads, stalls/hangs can result. Use with caution, especially if you have sync-over-async work in play. +- HighIntegrity - Default: `false` + - This enables sending a sequence check command after _every single command_ sent to Redis. This is an opt-in option that incurs overhead to add this integrity check which isn't in the Redis protocol (RESP2/3) itself. The impact on this for a given workload depends on the number of commands, size of payloads, etc. as to how proportionately impactful it will be - you should test with your workloads to assess this. + - This is especially relevant if your primary use case is all strings (e.g. key/value caching) where the protocol would otherwise not error. + - Intended for cases where network drops (e.g. bytes from the Redis stream, not packet loss) are suspected and integrity of responses is critical. - HeartbeatConsistencyChecks - Default: `false` - Allows _always_ sending keepalive checks even if a connection isn't idle. This trades extra commands (per `HeartbeatInterval` - default 1 second) to check the network stream for consistency. If any data was lost, the result won't be as expected and the connection will be terminated ASAP. This is a check to react to any data loss at the network layer as soon as possible. - HeartbeatInterval - Default: `1000ms` diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index 67bbc72ac..359b5f5f6 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -104,6 +104,17 @@ public static DefaultOptionsProvider GetProvider(EndPoint endpoint) /// public virtual bool CheckCertificateRevocation => true; + /// + /// A Boolean value that specifies whether to use per-command validation of strict protocol validity. + /// This sends an additional command after EVERY command which incurs measurable overhead. + /// + /// + /// The regular RESP protocol does not include correlation identifiers between requests and responses; in exceptional + /// scenarios, protocol desynchronization can occur, which may not be noticed immediately; this option adds additional data + /// to ensure that this cannot occur, at the cost of some (small) additional bandwidth usage. + /// + public virtual bool HighIntegrity => false; + /// /// The number of times to repeat the initial connect cycle if no servers respond promptly. /// diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 3d0a3aae8..4f3ff1287 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -110,7 +110,8 @@ internal const string CheckCertificateRevocation = "checkCertificateRevocation", Tunnel = "tunnel", SetClientLibrary = "setlib", - Protocol = "protocol"; + Protocol = "protocol", + HighIntegrity = "highIntegrity"; private static readonly Dictionary normalizedOptions = new[] { @@ -141,6 +142,7 @@ internal const string WriteBuffer, CheckCertificateRevocation, Protocol, + HighIntegrity, }.ToDictionary(x => x, StringComparer.OrdinalIgnoreCase); public static string TryNormalize(string value) @@ -156,7 +158,7 @@ public static string TryNormalize(string value) private DefaultOptionsProvider? defaultOptions; private bool? allowAdmin, abortOnConnectFail, resolveDns, ssl, checkCertificateRevocation, heartbeatConsistencyChecks, - includeDetailInExceptions, includePerformanceCountersInExceptions, setClientLibrary; + includeDetailInExceptions, includePerformanceCountersInExceptions, setClientLibrary, highIntegrity; private string? tieBreaker, sslHost, configChannel, user, password; @@ -279,6 +281,21 @@ public bool CheckCertificateRevocation set => checkCertificateRevocation = value; } + /// + /// A Boolean value that specifies whether to use per-command validation of strict protocol validity. + /// This sends an additional command after EVERY command which incurs measurable overhead. + /// + /// + /// The regular RESP protocol does not include correlation identifiers between requests and responses; in exceptional + /// scenarios, protocol desynchronization can occur, which may not be noticed immediately; this option adds additional data + /// to ensure that this cannot occur, at the cost of some (small) additional bandwidth usage. + /// + public bool HighIntegrity + { + get => highIntegrity ?? Defaults.HighIntegrity; + set => highIntegrity = value; + } + /// /// Create a certificate validation check that checks against the supplied issuer even when not known by the machine. /// @@ -769,6 +786,7 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow Protocol = Protocol, heartbeatInterval = heartbeatInterval, heartbeatConsistencyChecks = heartbeatConsistencyChecks, + highIntegrity = highIntegrity, }; /// @@ -849,6 +867,7 @@ public string ToString(bool includePassword) Append(sb, OptionKeys.ResponseTimeout, responseTimeout); Append(sb, OptionKeys.DefaultDatabase, DefaultDatabase); Append(sb, OptionKeys.SetClientLibrary, setClientLibrary); + Append(sb, OptionKeys.HighIntegrity, highIntegrity); Append(sb, OptionKeys.Protocol, FormatProtocol(Protocol)); if (Tunnel is { IsInbuilt: true } tunnel) { @@ -894,7 +913,7 @@ private void Clear() { ClientName = ServiceName = user = password = tieBreaker = sslHost = configChannel = null; keepAlive = syncTimeout = asyncTimeout = connectTimeout = connectRetry = configCheckSeconds = DefaultDatabase = null; - allowAdmin = abortOnConnectFail = resolveDns = ssl = setClientLibrary = null; + allowAdmin = abortOnConnectFail = resolveDns = ssl = setClientLibrary = highIntegrity = null; SslProtocols = null; defaultVersion = null; EndPoints.Clear(); @@ -1013,6 +1032,9 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) case OptionKeys.SetClientLibrary: SetClientLibrary = OptionKeys.ParseBoolean(key, value); break; + case OptionKeys.HighIntegrity: + HighIntegrity = OptionKeys.ParseBoolean(key, value); + break; case OptionKeys.Tunnel: if (value.IsNullOrWhiteSpace()) { diff --git a/src/StackExchange.Redis/Enums/ConnectionFailureType.cs b/src/StackExchange.Redis/Enums/ConnectionFailureType.cs index 57958ecbc..d9407b69e 100644 --- a/src/StackExchange.Redis/Enums/ConnectionFailureType.cs +++ b/src/StackExchange.Redis/Enums/ConnectionFailureType.cs @@ -44,6 +44,10 @@ public enum ConnectionFailureType /// /// It has not been possible to create an initial connection to the redis server(s). /// - UnableToConnect + UnableToConnect, + /// + /// High-integrity mode was enabled, and a failure was detected + /// + ResponseIntegrityFailure, } } diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 9acf41fb1..3cdb4c997 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using StackExchange.Redis.Profiling; using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -52,6 +53,8 @@ internal abstract class Message : ICompletable { public readonly int Db; + private uint _highIntegrityToken; + internal const CommandFlags InternalCallFlag = (CommandFlags)128; protected RedisCommand command; @@ -198,6 +201,13 @@ public bool IsAdmin public bool IsAsking => (Flags & AskingFlag) != 0; + public bool IsHighIntegrity => _highIntegrityToken != 0; + + public uint HighIntegrityToken => _highIntegrityToken; + + internal void WithHighIntegrity(uint value) + => _highIntegrityToken = value; + internal bool IsScriptUnavailable => (Flags & ScriptUnavailableFlag) != 0; internal void SetScriptUnavailable() => Flags |= ScriptUnavailableFlag; @@ -710,6 +720,28 @@ internal void WriteTo(PhysicalConnection physical) } } + private static ReadOnlySpan ChecksumTemplate => "$4\r\nXXXX\r\n"u8; + + internal void WriteHighIntegrityChecksumRequest(PhysicalConnection physical) + { + Debug.Assert(IsHighIntegrity, "should only be used for high-integrity"); + try + { + physical.WriteHeader(RedisCommand.ECHO, 1); // use WriteHeader to allow command-rewrite + + Span chk = stackalloc byte[10]; + Debug.Assert(ChecksumTemplate.Length == chk.Length, "checksum template length error"); + ChecksumTemplate.CopyTo(chk); + BinaryPrimitives.WriteUInt32LittleEndian(chk.Slice(4, 4), _highIntegrityToken); + physical.WriteRaw(chk); + } + catch (Exception ex) + { + physical?.OnInternalError(ex); + Fail(ConnectionFailureType.InternalFailure, ex, null, physical?.BridgeCouldBeNull?.Multiplexer); + } + } + internal static Message CreateHello(int protocolVersion, string? username, string? password, string? clientName, CommandFlags flags) => new HelloMessage(protocolVersion, username, password, clientName, flags); diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 522e2fcc9..7f9ffa32e 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -70,6 +70,8 @@ internal sealed class PhysicalBridge : IDisposable internal string? PhysicalName => physical?.ToString(); + private uint _nextHighIntegrityToken; // zero means not enabled + public DateTime? ConnectedAt { get; private set; } public PhysicalBridge(ServerEndPoint serverEndPoint, ConnectionType type, int timeoutMilliseconds) @@ -82,6 +84,11 @@ public PhysicalBridge(ServerEndPoint serverEndPoint, ConnectionType type, int ti #if !NETCOREAPP _singleWriterMutex = new MutexSlim(timeoutMilliseconds: timeoutMilliseconds); #endif + if (type == ConnectionType.Interactive && Multiplexer.RawConfig.HighIntegrity) + { + // we just need this to be non-zero to enable tracking + _nextHighIntegrityToken = 1; + } } private readonly int TimeoutMilliseconds; @@ -1546,10 +1553,30 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne break; } + if (_nextHighIntegrityToken is not 0 + && !connection.TransactionActive // validated in the UNWATCH/EXEC/DISCARD + && message.Command is not RedisCommand.AUTH or RedisCommand.HELLO // if auth fails, ECHO may also fail; avoid confusion + ) + { + // make sure this value exists early to avoid a race condition + // if the response comes back super quickly + message.WithHighIntegrity(NextHighIntegrityTokenInsideLock()); + Debug.Assert(message.IsHighIntegrity, "message should be high integrity"); + } + else + { + Debug.Assert(!message.IsHighIntegrity, "prior high integrity message found during transaction?"); + } connection.EnqueueInsideWriteLock(message); isQueued = true; message.WriteTo(connection); + if (message.IsHighIntegrity) + { + message.WriteHighIntegrityChecksumRequest(connection); + IncrementOpCount(); + } + message.SetRequestSent(); IncrementOpCount(); @@ -1602,6 +1629,21 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne } } + private uint NextHighIntegrityTokenInsideLock() + { + // inside lock: no concurrency concerns here + switch (_nextHighIntegrityToken) + { + case 0: return 0; // disabled + case uint.MaxValue: + // avoid leaving the value at zero due to wrap-around + _nextHighIntegrityToken = 1; + return ushort.MaxValue; + default: + return _nextHighIntegrityToken++; + } + } + /// /// For testing only /// diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index d13b6af5b..c282121a5 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -19,6 +19,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using static StackExchange.Redis.Message; +using System.Buffers.Binary; namespace StackExchange.Redis { @@ -44,6 +45,8 @@ private static readonly Message // things sent to this physical, but not yet received private readonly Queue _writtenAwaitingResponse = new Queue(); + private Message? _awaitingToken; + private readonly string _physicalName; private volatile int currentDatabase = 0; @@ -388,6 +391,8 @@ public void RecordConnectionFailed( Exception? outerException = innerException; IdentifyFailureType(innerException, ref failureType); var bridge = BridgeCouldBeNull; + Message? nextMessage; + if (_ioPipe != null || isInitialConnect) // if *we* didn't burn the pipe: flag it { if (failureType == ConnectionFailureType.InternalFailure && innerException is not null) @@ -419,9 +424,9 @@ public void RecordConnectionFailed( lock (_writtenAwaitingResponse) { // find oldest message awaiting a response - if (_writtenAwaitingResponse.TryPeek(out var next)) + if (_writtenAwaitingResponse.TryPeek(out nextMessage)) { - unansweredWriteTime = next.GetWriteTime(); + unansweredWriteTime = nextMessage.GetWriteTime(); } } @@ -510,23 +515,17 @@ void add(string lk, string sk, string? v) bridge?.Trace(_writtenAwaitingResponse.Count != 0, "Failing outstanding messages: " + _writtenAwaitingResponse.Count); } - while (TryDequeueLocked(_writtenAwaitingResponse, out var next)) + var ex = innerException is RedisException ? innerException : outerException; + + nextMessage = Interlocked.Exchange(ref _awaitingToken, null); + if (nextMessage is not null) { - if (next.Command == RedisCommand.QUIT && next.TrySetResult(true)) - { - // fine, death of a socket is close enough - next.Complete(); - } - else - { - var ex = innerException is RedisException ? innerException : outerException; - if (bridge != null) - { - bridge.Trace("Failing: " + next); - bridge.Multiplexer?.OnMessageFaulted(next, ex, origin); - } - next.SetExceptionAndComplete(ex!, bridge); - } + RecordMessageFailed(nextMessage, ex, origin, bridge); + } + + while (TryDequeueLocked(_writtenAwaitingResponse, out nextMessage)) + { + RecordMessageFailed(nextMessage, ex, origin, bridge); } // burn the socket @@ -541,6 +540,24 @@ static bool TryDequeueLocked(Queue queue, [NotNullWhen(true)] out Messa } } + private void RecordMessageFailed(Message next, Exception? ex, string? origin, PhysicalBridge? bridge) + { + if (next.Command == RedisCommand.QUIT && next.TrySetResult(true)) + { + // fine, death of a socket is close enough + next.Complete(); + } + else + { + if (bridge != null) + { + bridge.Trace("Failing: " + next); + bridge.Multiplexer?.OnMessageFaulted(next, ex, origin); + } + next.SetExceptionAndComplete(ex!, bridge); + } + } + internal bool IsIdle() => _writeStatus == WriteStatus.Idle; internal void SetIdle() => _writeStatus = WriteStatus.Idle; internal void SetWriting() => _writeStatus = WriteStatus.Writing; @@ -880,6 +897,8 @@ internal void WriteHeader(RedisCommand command, int arguments, CommandBytes comm writer.Advance(offset); } + internal void WriteRaw(ReadOnlySpan bytes) => _ioPipe?.Output?.Write(bytes); + internal void RecordQuit() // don't blame redis if we fired the first shot => (_ioPipe as SocketConnection)?.TrySetProtocolShutdown(PipeShutdownKind.ProtocolExitClient); @@ -1680,10 +1699,27 @@ private void MatchResult(in RawResult result) // if it didn't look like "[p]message", then we still need to process the pending queue } Trace("Matching result..."); - Message? msg; + + Message? msg = null; + // check whether we're waiting for a high-integrity mode post-response checksum (using cheap null-check first) + if (_awaitingToken is not null && (msg = Interlocked.Exchange(ref _awaitingToken, null)) is not null) + { + _readStatus = ReadStatus.ResponseSequenceCheck; + if (!ProcessHighIntegrityResponseToken(msg, in result, BridgeCouldBeNull)) + { + RecordConnectionFailed(ConnectionFailureType.ResponseIntegrityFailure, origin: nameof(ReadStatus.ResponseSequenceCheck)); + } + return; + } + _readStatus = ReadStatus.DequeueResult; lock (_writtenAwaitingResponse) { + if (msg is not null) + { + _awaitingToken = null; + } + if (!_writtenAwaitingResponse.TryDequeue(out msg)) { throw new InvalidOperationException("Received response with no message waiting: " + result.ToString()); @@ -1696,11 +1732,56 @@ private void MatchResult(in RawResult result) if (msg.ComputeResult(this, result)) { _readStatus = msg.ResultBoxIsAsync ? ReadStatus.CompletePendingMessageAsync : ReadStatus.CompletePendingMessageSync; - msg.Complete(); + if (!msg.IsHighIntegrity) + { + // can't complete yet if needs checksum + msg.Complete(); + } } + if (msg.IsHighIntegrity) + { + // stash this for the next non-OOB response + Volatile.Write(ref _awaitingToken, msg); + } + + _readStatus = ReadStatus.MatchResultComplete; _activeMessage = null; + static bool ProcessHighIntegrityResponseToken(Message message, in RawResult result, PhysicalBridge? bridge) + { + bool isValid = false; + if (result.Resp2TypeBulkString == ResultType.BulkString) + { + var payload = result.Payload; + if (payload.Length == 4) + { + uint interpreted; + if (payload.IsSingleSegment) + { + interpreted = BinaryPrimitives.ReadUInt32LittleEndian(payload.First.Span); + } + else + { + Span span = stackalloc byte[4]; + payload.CopyTo(span); + interpreted = BinaryPrimitives.ReadUInt32LittleEndian(span); + } + isValid = interpreted == message.HighIntegrityToken; + } + } + if (isValid) + { + message.Complete(); + return true; + } + else + { + message.SetExceptionAndComplete(new InvalidOperationException("High-integrity mode detected possible protocol de-sync"), bridge); + return false; + } + } + static bool TryGetPubSubPayload(in RawResult value, out RedisValue parsed, bool allowArraySingleton = true) { if (value.IsNull) @@ -2032,6 +2113,7 @@ internal enum ReadStatus PubSubPMessage, Reconfigure, InvokePubSub, + ResponseSequenceCheck, // high-integrity mode only DequeueResult, ComputeResult, CompletePendingMessageSync, diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 1bcc6c66d..4504423fe 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -236,6 +236,8 @@ StackExchange.Redis.ConfigurationOptions.HeartbeatConsistencyChecks.get -> bool StackExchange.Redis.ConfigurationOptions.HeartbeatConsistencyChecks.set -> void StackExchange.Redis.ConfigurationOptions.HeartbeatInterval.get -> System.TimeSpan StackExchange.Redis.ConfigurationOptions.HeartbeatInterval.set -> void +StackExchange.Redis.ConfigurationOptions.HighIntegrity.get -> bool +StackExchange.Redis.ConfigurationOptions.HighIntegrity.set -> void StackExchange.Redis.ConfigurationOptions.HighPrioritySocketThreads.get -> bool StackExchange.Redis.ConfigurationOptions.HighPrioritySocketThreads.set -> void StackExchange.Redis.ConfigurationOptions.IncludeDetailInExceptions.get -> bool @@ -319,6 +321,7 @@ StackExchange.Redis.ConnectionFailureType.ProtocolFailure = 4 -> StackExchange.R StackExchange.Redis.ConnectionFailureType.SocketClosed = 6 -> StackExchange.Redis.ConnectionFailureType StackExchange.Redis.ConnectionFailureType.SocketFailure = 2 -> StackExchange.Redis.ConnectionFailureType StackExchange.Redis.ConnectionFailureType.UnableToConnect = 9 -> StackExchange.Redis.ConnectionFailureType +StackExchange.Redis.ConnectionFailureType.ResponseIntegrityFailure = 10 -> StackExchange.Redis.ConnectionFailureType StackExchange.Redis.ConnectionFailureType.UnableToResolvePhysicalConnection = 1 -> StackExchange.Redis.ConnectionFailureType StackExchange.Redis.ConnectionMultiplexer StackExchange.Redis.ConnectionMultiplexer.ClientName.get -> string! @@ -1801,6 +1804,7 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetDefaultSsl(S virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.GetSslHostFromEndpoints(StackExchange.Redis.EndPointCollection! endPoints) -> string? virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.HeartbeatConsistencyChecks.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.HeartbeatInterval.get -> System.TimeSpan +virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.HighIntegrity.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludeDetailInExceptions.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludePerformanceCountersInExceptions.get -> bool virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IsMatch(System.Net.EndPoint! endpoint) -> bool diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 5f282702b..91b0e1a43 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1 @@ - \ No newline at end of file +#nullable enable \ No newline at end of file diff --git a/tests/BasicTest/BasicTest.csproj b/tests/BasicTest/BasicTest.csproj index 0ba04d459..7fcf776ee 100644 --- a/tests/BasicTest/BasicTest.csproj +++ b/tests/BasicTest/BasicTest.csproj @@ -6,7 +6,7 @@ BasicTest Exe BasicTest - win7-x64 + diff --git a/tests/BasicTestBaseline/BasicTestBaseline.csproj b/tests/BasicTestBaseline/BasicTestBaseline.csproj index 43cf8a8b3..f396ae7c1 100644 --- a/tests/BasicTestBaseline/BasicTestBaseline.csproj +++ b/tests/BasicTestBaseline/BasicTestBaseline.csproj @@ -6,7 +6,7 @@ BasicTestBaseline Exe BasicTestBaseline - win7-x64 + $(DefineConstants);TEST_BASELINE diff --git a/tests/ConsoleTest/Program.cs b/tests/ConsoleTest/Program.cs index 0d6fa7e13..39e00701e 100644 --- a/tests/ConsoleTest/Program.cs +++ b/tests/ConsoleTest/Program.cs @@ -5,14 +5,27 @@ Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); -var options = ConfigurationOptions.Parse("localhost"); +var options = ConfigurationOptions.Parse("127.0.0.1"); +#if !SEREDIS_BASELINE +options.HighIntegrity = false; // as needed +Console.WriteLine($"{nameof(options.HighIntegrity)}: {options.HighIntegrity}"); +#endif + //options.SocketManager = SocketManager.ThreadPool; +Console.WriteLine("Connecting..."); var connection = ConnectionMultiplexer.Connect(options); +Console.WriteLine("Connected"); +connection.ConnectionFailed += Connection_ConnectionFailed; + +void Connection_ConnectionFailed(object? sender, ConnectionFailedEventArgs e) +{ + Console.Error.WriteLine($"CONNECTION FAILED: {e.ConnectionType}, {e.FailureType}, {e.Exception}"); +} var startTime = DateTime.UtcNow; var startCpuUsage = Process.GetCurrentProcess().TotalProcessorTime; -var scenario = args?.Length > 0 ? args[0] : "parallel"; +var scenario = args?.Length > 0 ? args[0] : "mass-insert-async"; switch (scenario) { @@ -24,6 +37,10 @@ Console.WriteLine("Mass insert test..."); MassInsert(connection); break; + case "mass-insert-async": + Console.WriteLine("Mass insert (async/pipelined) test..."); + await MassInsertAsync(connection); + break; case "mass-publish": Console.WriteLine("Mass publish test..."); MassPublish(connection); @@ -48,7 +65,8 @@ static void MassInsert(ConnectionMultiplexer connection) { - const int NUM_INSERTIONS = 100000; + const int NUM_INSERTIONS = 100_000; + const int BATCH = 5000; int matchErrors = 0; var database = connection.GetDatabase(0); @@ -61,20 +79,69 @@ static void MassInsert(ConnectionMultiplexer connection) database.StringSet(key, value); var retrievedValue = database.StringGet(key); - if (retrievedValue != value) - { - matchErrors++; - } + if (retrievedValue != value) + { + matchErrors++; + } + + if (i > 0 && i % BATCH == 0) + { + Console.WriteLine(i); + } + } + + Console.WriteLine($"Match errors: {matchErrors}"); +} + +static async Task MassInsertAsync(ConnectionMultiplexer connection) +{ + const int NUM_INSERTIONS = 100_000; + const int BATCH = 5000; + int matchErrors = 0; + + var database = connection.GetDatabase(0); + + var outstanding = new List<(Task, Task, string)>(BATCH); + + for (int i = 0; i < NUM_INSERTIONS; i++) + { + var key = $"StackExchange.Redis.Test.{i}"; + var value = i.ToString(); + + var set = database.StringSetAsync(key, value); + var get = database.StringGetAsync(key); + + outstanding.Add((set, get, value)); + + if (i > 0 && i % BATCH == 0) + { + matchErrors += await ValidateAsync(outstanding); + Console.WriteLine(i); + } - if (i > 0 && i % 5000 == 0) - { - Console.WriteLine(i); - } } + matchErrors += await ValidateAsync(outstanding); Console.WriteLine($"Match errors: {matchErrors}"); + + static async Task ValidateAsync(List<(Task, Task, string)> outstanding) + { + int matchErrors = 0; + foreach (var row in outstanding) + { + var s = await row.Item2; + await row.Item1; + if (s != row.Item3) + { + matchErrors++; + } + } + outstanding.Clear(); + return matchErrors; + } } + static void ParallelTasks(ConnectionMultiplexer connection) { static void ParallelRun(int taskId, ConnectionMultiplexer connection) diff --git a/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj b/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj index dc644561d..2c4bac2f5 100644 --- a/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj +++ b/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj @@ -5,6 +5,7 @@ Exe enable enable + $(DefineConstants);SEREDIS_BASELINE diff --git a/tests/StackExchange.Redis.Tests/BasicOpTests.cs b/tests/StackExchange.Redis.Tests/BasicOpTests.cs index 83499fed3..58c82dda6 100644 --- a/tests/StackExchange.Redis.Tests/BasicOpTests.cs +++ b/tests/StackExchange.Redis.Tests/BasicOpTests.cs @@ -7,10 +7,18 @@ namespace StackExchange.Redis.Tests; +[Collection(SharedConnectionFixture.Key)] +public class HighIntegrityBasicOpsTests : BasicOpsTests +{ + public HighIntegrityBasicOpsTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + internal override bool HighIntegrity => true; +} + [Collection(SharedConnectionFixture.Key)] public class BasicOpsTests : TestBase { - public BasicOpsTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public BasicOpsTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public async Task PingOnce() @@ -469,4 +477,42 @@ public void WrappedDatabasePrefixIntegration() int count = (int)conn.GetDatabase().StringGet("abc" + key); Assert.Equal(3, count); } + + [Fact] + public void TransactionSync() + { + using var conn = Create(); + var db = conn.GetDatabase(); + + RedisKey key = Me(); + + var tran = db.CreateTransaction(); + _ = db.KeyDeleteAsync(key); + var x = tran.StringIncrementAsync(Me()); + var y = tran.StringIncrementAsync(Me()); + var z = tran.StringIncrementAsync(Me()); + Assert.True(tran.Execute()); + Assert.Equal(1, x.Result); + Assert.Equal(2, y.Result); + Assert.Equal(3, z.Result); + } + + [Fact] + public async Task TransactionAsync() + { + await using var conn = Create(); + var db = conn.GetDatabase(); + + RedisKey key = Me(); + + var tran = db.CreateTransaction(); + _ = db.KeyDeleteAsync(key); + var x = tran.StringIncrementAsync(Me()); + var y = tran.StringIncrementAsync(Me()); + var z = tran.StringIncrementAsync(Me()); + Assert.True(await tran.ExecuteAsync()); + Assert.Equal(1, await x); + Assert.Equal(2, await y); + Assert.Equal(3, await z); + } } diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index a2329dc04..f93eb176f 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -43,7 +43,7 @@ public void ExpectedFields() "configChannel", "configCheckSeconds", "connectRetry", "connectTimeout", "DefaultDatabase", "defaultOptions", "defaultVersion", "EndPoints", "heartbeatConsistencyChecks", - "heartbeatInterval", "includeDetailInExceptions", "includePerformanceCountersInExceptions", + "heartbeatInterval", "highIntegrity", "includeDetailInExceptions", "includePerformanceCountersInExceptions", "keepAlive", "LibraryName", "loggerFactory", "password", "Protocol", "proxy", "reconnectRetryPolicy", "resolveDns", "responseTimeout", @@ -760,4 +760,25 @@ public void DefaultConfigOptionsForSetLib(string configurationString, bool setli Assert.Equal(setlib, options.SetClientLibrary); Assert.Equal(configurationString, options.ToString()); } + + [Theory] + [InlineData(null, false, "dummy")] + [InlineData(false, false, "dummy,highIntegrity=False")] + [InlineData(true, true, "dummy,highIntegrity=True")] + public void CheckHighIntegrity(bool? assigned, bool expected, string cs) + { + var options = ConfigurationOptions.Parse("dummy"); + if (assigned.HasValue) options.HighIntegrity = assigned.Value; + + Assert.Equal(expected, options.HighIntegrity); + Assert.Equal(cs, options.ToString()); + + var clone = options.Clone(); + Assert.Equal(expected, clone.HighIntegrity); + Assert.Equal(cs, clone.ToString()); + + var parsed = ConfigurationOptions.Parse(cs); + Assert.Equal(expected, options.HighIntegrity); + + } } diff --git a/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs b/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs index 98351d04b..f0f189372 100644 --- a/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs @@ -93,7 +93,7 @@ public void TiebreakerIncorrectType() } [Theory] - [InlineData(true, 5, 15)] + [InlineData(true, 4, 15)] [InlineData(false, 0, 0)] public async Task HeartbeatConsistencyCheckPingsAsync(bool enableConsistencyChecks, int minExpected, int maxExpected) { diff --git a/tests/StackExchange.Redis.Tests/ResultBoxTests.cs b/tests/StackExchange.Redis.Tests/ResultBoxTests.cs new file mode 100644 index 000000000..adb1b309f --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ResultBoxTests.cs @@ -0,0 +1,95 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace StackExchange.Redis.Tests; + +public class ResultBoxTests +{ + [Fact] + public void SyncResultBox() + { + var msg = Message.Create(-1, CommandFlags.None, RedisCommand.PING); + var box = SimpleResultBox.Get(); + Assert.False(box.IsAsync); + + int activated = 0; + lock (box) + { + Task.Run(() => + { + lock (box) + { + // release the worker to start work + Monitor.PulseAll(box); + + // wait for the completion signal + if (Monitor.Wait(box, TimeSpan.FromSeconds(10))) + { + Interlocked.Increment(ref activated); + } + } + }); + Assert.True(Monitor.Wait(box, TimeSpan.FromSeconds(10)), "failed to handover lock to worker"); + } + + // check that continuation was not already signalled + Thread.Sleep(100); + Assert.Equal(0, Volatile.Read(ref activated)); + + msg.SetSource(ResultProcessor.DemandOK, box); + Assert.True(msg.TrySetResult("abc")); + + // check that TrySetResult did not signal continuation + Thread.Sleep(100); + Assert.Equal(0, Volatile.Read(ref activated)); + + // check that complete signals continuation + msg.Complete(); + Thread.Sleep(100); + Assert.Equal(1, Volatile.Read(ref activated)); + + var s = box.GetResult(out var ex); + Assert.Null(ex); + Assert.NotNull(s); + Assert.Equal("abc", s); + } + + [Fact] + public void TaskResultBox() + { + // TaskResultBox currently uses a stating field for values before activations are + // signalled; High Integrity Mode *demands* this behaviour, so: validate that it + // works correctly + var msg = Message.Create(-1, CommandFlags.None, RedisCommand.PING); + var box = TaskResultBox.Create(out var tcs, null); + Assert.True(box.IsAsync); + + msg.SetSource(ResultProcessor.DemandOK, box); + Assert.True(msg.TrySetResult("abc")); + + // check that continuation was not already signalled + Thread.Sleep(100); + Assert.False(tcs.Task.IsCompleted); + + msg.SetSource(ResultProcessor.DemandOK, box); + Assert.True(msg.TrySetResult("abc")); + + // check that TrySetResult did not signal continuation + Thread.Sleep(100); + Assert.False(tcs.Task.IsCompleted); + + // check that complete signals continuation + msg.Complete(); + Thread.Sleep(100); + Assert.True(tcs.Task.IsCompleted); + + var s = box.GetResult(out var ex); + Assert.Null(ex); + Assert.NotNull(s); + Assert.Equal("abc", s); + + Assert.Equal("abc", tcs.Task.Result); // we already checked IsCompleted + } +} diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index c0dfb028c..7038aaa2f 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -31,7 +31,7 @@ public abstract class TestBase : IDisposable private readonly SharedConnectionFixture? _fixture; - protected bool SharedFixtureAvailable => _fixture != null && _fixture.IsEnabled; + protected bool SharedFixtureAvailable => _fixture != null && _fixture.IsEnabled && !HighIntegrity; protected TestBase(ITestOutputHelper output, SharedConnectionFixture? fixture = null) { @@ -236,6 +236,8 @@ protected static IServer GetAnyPrimary(IConnectionMultiplexer muxer) throw new InvalidOperationException("Requires a primary endpoint (found none)"); } + internal virtual bool HighIntegrity => false; + internal virtual IInternalConnectionMultiplexer Create( string? clientName = null, int? syncTimeout = null, @@ -271,9 +273,10 @@ internal virtual IInternalConnectionMultiplexer Create( protocol ??= Context.Test.Protocol; // Share a connection if instructed to and we can - many specifics mean no sharing + bool highIntegrity = HighIntegrity; if (shared && expectedFailCount == 0 && _fixture != null && _fixture.IsEnabled - && CanShare(allowAdmin, password, tieBreaker, fail, disabledCommands, enabledCommands, channelPrefix, proxy, configuration, defaultDatabase, backlogPolicy)) + && CanShare(allowAdmin, password, tieBreaker, fail, disabledCommands, enabledCommands, channelPrefix, proxy, configuration, defaultDatabase, backlogPolicy, highIntegrity)) { configuration = GetConfiguration(); var fixtureConn = _fixture.GetConnection(this, protocol.Value, caller: caller); @@ -296,7 +299,7 @@ internal virtual IInternalConnectionMultiplexer Create( checkConnect, failMessage, channelPrefix, proxy, logTransactionData, defaultDatabase, - backlogPolicy, protocol, + backlogPolicy, protocol, highIntegrity, caller); ThrowIfIncorrectProtocol(conn, protocol); @@ -319,7 +322,8 @@ internal static bool CanShare( Proxy? proxy, string? configuration, int? defaultDatabase, - BacklogPolicy? backlogPolicy + BacklogPolicy? backlogPolicy, + bool highIntegrity ) => enabledCommands == null && disabledCommands == null @@ -331,7 +335,8 @@ internal static bool CanShare( && tieBreaker == null && defaultDatabase == null && (allowAdmin == null || allowAdmin == true) - && backlogPolicy == null; + && backlogPolicy == null + && !highIntegrity; internal void ThrowIfIncorrectProtocol(IInternalConnectionMultiplexer conn, RedisProtocol? requiredProtocol) { @@ -390,6 +395,7 @@ public static ConnectionMultiplexer CreateDefault( int? defaultDatabase = null, BacklogPolicy? backlogPolicy = null, RedisProtocol? protocol = null, + bool highIntegrity = false, [CallerMemberName] string caller = "") { StringWriter? localLog = null; @@ -425,6 +431,7 @@ public static ConnectionMultiplexer CreateDefault( if (defaultDatabase is not null) config.DefaultDatabase = defaultDatabase.Value; if (backlogPolicy is not null) config.BacklogPolicy = backlogPolicy; if (protocol is not null) config.Protocol = protocol; + if (highIntegrity) config.HighIntegrity = highIntegrity; var watch = Stopwatch.StartNew(); var task = ConnectionMultiplexer.ConnectAsync(config, log); if (!task.Wait(config.ConnectTimeout >= (int.MaxValue / 2) ? int.MaxValue : config.ConnectTimeout * 2)) From 1c6b59e6c7751f03aec74679bb2f0d9123b74e3d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 24 Jun 2024 16:21:48 +0100 Subject: [PATCH 289/435] release notes; bump minor to 2.8 --- docs/ReleaseNotes.md | 5 +++++ version.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b4dd0fd4f..5a1b9aa64 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,6 +7,11 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased + + +## 2.8.0 + +- Add high-integrity mode ([docs](https://stackexchange.github.io/StackExchange.Redis/Configuration), [#2471 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2741])) - TLS certificate/`TrustIssuer`: Check EKU in X509 chain checks when validating certificates ([#2670 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2670)) ## 2.7.33 diff --git a/version.json b/version.json index c37674c0e..be7077002 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "2.7", + "version": "2.8", "versionHeightOffset": -1, "assemblyVersion": "2.0", "publicReleaseRefSpec": [ From 0d38f6fab57577b5e438c581b11a54d33dc16980 Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Tue, 2 Jul 2024 18:01:18 +0300 Subject: [PATCH 290/435] test against version 7.4-rc1 issue #2738 (#2739) Closes/Fixes #2738 this is just to move the integration test against to newer version of Redis --- src/StackExchange.Redis/RedisFeatures.cs | 2 +- tests/RedisConfigs/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index a1bb14a8b..19c5233e1 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -41,7 +41,7 @@ namespace StackExchange.Redis v6_2_0 = new Version(6, 2, 0), v7_0_0_rc1 = new Version(6, 9, 240), // 7.0 RC1 is version 6.9.240 v7_2_0_rc1 = new Version(7, 1, 240), // 7.2 RC1 is version 7.1.240 - v7_4_0_rc1 = new Version(7, 4, 240); // 7.4 RC1 is version 7.4.240 + v7_4_0_rc1 = new Version(7, 3, 240); // 7.4 RC1 is version 7.3.240 private readonly Version version; diff --git a/tests/RedisConfigs/Dockerfile b/tests/RedisConfigs/Dockerfile index 32cbe0663..ab0f76e6c 100644 --- a/tests/RedisConfigs/Dockerfile +++ b/tests/RedisConfigs/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:7.2-rc1 +FROM redis:7.4-rc1 COPY Basic /data/Basic/ COPY Failover /data/Failover/ From a64ca140471cfd93916f0fb22f841ca95c93fa38 Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Tue, 9 Jul 2024 17:48:57 +0300 Subject: [PATCH 291/435] support for CLIENT KILL MAXAGE (#2727) Closes/Fixes #2726 added two overloads and use a [filter object](https://github.com/StackExchange/StackExchange.Redis/compare/main...atakavci:StackExchange.Redis:ali/maxage?expand=1#diff-6c39ce347d72497c65974f0d78ab9a986be3198e7b2555975ff06410a9831bd8) to avoid a breaking change. Co-authored-by: Nick Craver --- .../APITypes/ClientKillFilter.cs | 179 ++++++++++++++++++ src/StackExchange.Redis/Interfaces/IServer.cs | 12 ++ .../PublicAPI/PublicAPI.Shipped.txt | 21 +- src/StackExchange.Redis/RedisLiterals.cs | 3 + src/StackExchange.Redis/RedisServer.cs | 56 ++---- .../ClientKillTests.cs | 66 +++++++ 6 files changed, 296 insertions(+), 41 deletions(-) create mode 100644 src/StackExchange.Redis/APITypes/ClientKillFilter.cs create mode 100644 tests/StackExchange.Redis.Tests/ClientKillTests.cs diff --git a/src/StackExchange.Redis/APITypes/ClientKillFilter.cs b/src/StackExchange.Redis/APITypes/ClientKillFilter.cs new file mode 100644 index 000000000..b5c5e845c --- /dev/null +++ b/src/StackExchange.Redis/APITypes/ClientKillFilter.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Net; + +namespace StackExchange.Redis; + +/// +/// Filter determining which Redis clients to kill. +/// +/// +public class ClientKillFilter +{ + /// + /// Filter arguments builder for `CLIENT KILL`. + /// + public ClientKillFilter() { } + + /// + /// The ID of the client to kill. + /// + public long? Id { get; private set; } + + /// + /// The type of client. + /// + public ClientType? ClientType { get; private set; } + + /// + /// The authenticated ACL username. + /// + public string? Username { get; private set; } + + /// + /// The endpoint to kill. + /// + public EndPoint? Endpoint { get; private set; } + + /// + /// The server endpoint to kill. + /// + public EndPoint? ServerEndpoint { get; private set; } + + /// + /// Whether to skip the current connection. + /// + public bool? SkipMe { get; private set; } + + /// + /// Age of connection in seconds. + /// + public long? MaxAgeInSeconds { get; private set; } + + /// + /// Sets client id filter. + /// + /// Id of the client to kill. + public ClientKillFilter WithId(long? id) + { + Id = id; + return this; + } + + /// + /// Sets client type filter. + /// + /// The type of the client. + public ClientKillFilter WithClientType(ClientType? clientType) + { + ClientType = clientType; + return this; + } + + /// + /// Sets the username filter. + /// + /// Authenticated ACL username. + public ClientKillFilter WithUsername(string? username) + { + Username = username; + return this; + } + + /// + /// Set the endpoint filter. + /// + /// The endpoint to kill. + public ClientKillFilter WithEndpoint(EndPoint? endpoint) + { + Endpoint = endpoint; + return this; + } + + /// + /// Set the server endpoint filter. + /// + /// The server endpoint to kill. + public ClientKillFilter WithServerEndpoint(EndPoint? serverEndpoint) + { + ServerEndpoint = serverEndpoint; + return this; + } + + /// + /// Set the skipMe filter (whether to skip the current connection). + /// + /// Whether to skip the current connection. + public ClientKillFilter WithSkipMe(bool? skipMe) + { + SkipMe = skipMe; + return this; + } + + /// + /// Set the MaxAgeInSeconds filter. + /// + /// Age of connection in seconds + public ClientKillFilter WithMaxAgeInSeconds(long? maxAgeInSeconds) + { + MaxAgeInSeconds = maxAgeInSeconds; + return this; + } + + internal List ToList(bool withReplicaCommands) + { + var parts = new List(15) + { + RedisLiterals.KILL + }; + if (Id != null) + { + parts.Add(RedisLiterals.ID); + parts.Add(Id.Value); + } + if (ClientType != null) + { + parts.Add(RedisLiterals.TYPE); + switch (ClientType.Value) + { + case Redis.ClientType.Normal: + parts.Add(RedisLiterals.normal); + break; + case Redis.ClientType.Replica: + parts.Add(withReplicaCommands ? RedisLiterals.replica : RedisLiterals.slave); + break; + case Redis.ClientType.PubSub: + parts.Add(RedisLiterals.pubsub); + break; + default: + throw new ArgumentOutOfRangeException(nameof(ClientType)); + } + } + if (Username != null) + { + parts.Add(RedisLiterals.USERNAME); + parts.Add(Username); + } + if (Endpoint != null) + { + parts.Add(RedisLiterals.ADDR); + parts.Add((RedisValue)Format.ToString(Endpoint)); + } + if (ServerEndpoint != null) + { + parts.Add(RedisLiterals.LADDR); + parts.Add((RedisValue)Format.ToString(ServerEndpoint)); + } + if (SkipMe != null) + { + parts.Add(RedisLiterals.SKIPME); + parts.Add(SkipMe.Value ? RedisLiterals.yes : RedisLiterals.no); + } + if (MaxAgeInSeconds != null) + { + parts.Add(RedisLiterals.MAXAGE); + parts.Add(MaxAgeInSeconds); + } + return parts; + } +} diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 9f8f0b075..94baf5cd6 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -107,6 +107,18 @@ public partial interface IServer : IRedis /// Task ClientKillAsync(long? id = null, ClientType? clientType = null, EndPoint? endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None); + /// + /// The CLIENT KILL command closes multiple connections that match the specified filters. + /// + /// + /// + /// + long ClientKill(ClientKillFilter filter, CommandFlags flags = CommandFlags.None); + + /// + Task ClientKillAsync(ClientKillFilter filter, CommandFlags flags = CommandFlags.None); + + /// /// The CLIENT LIST command returns information and statistics about the client connections server in a mostly human readable format. /// diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 4504423fe..8707cc1b4 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -126,6 +126,22 @@ StackExchange.Redis.ClientInfo.ProtocolVersion.get -> string? StackExchange.Redis.ClientInfo.Raw.get -> string? StackExchange.Redis.ClientInfo.SubscriptionCount.get -> int StackExchange.Redis.ClientInfo.TransactionCommandLength.get -> int +StackExchange.Redis.ClientKillFilter +StackExchange.Redis.ClientKillFilter.ClientKillFilter() -> void +StackExchange.Redis.ClientKillFilter.ClientType.get -> StackExchange.Redis.ClientType? +StackExchange.Redis.ClientKillFilter.Endpoint.get -> System.Net.EndPoint? +StackExchange.Redis.ClientKillFilter.Id.get -> long? +StackExchange.Redis.ClientKillFilter.MaxAgeInSeconds.get -> long? +StackExchange.Redis.ClientKillFilter.ServerEndpoint.get -> System.Net.EndPoint? +StackExchange.Redis.ClientKillFilter.SkipMe.get -> bool? +StackExchange.Redis.ClientKillFilter.Username.get -> string? +StackExchange.Redis.ClientKillFilter.WithClientType(StackExchange.Redis.ClientType? clientType) -> StackExchange.Redis.ClientKillFilter! +StackExchange.Redis.ClientKillFilter.WithEndpoint(System.Net.EndPoint? endpoint) -> StackExchange.Redis.ClientKillFilter! +StackExchange.Redis.ClientKillFilter.WithId(long? id) -> StackExchange.Redis.ClientKillFilter! +StackExchange.Redis.ClientKillFilter.WithMaxAgeInSeconds(long? maxAgeInSeconds) -> StackExchange.Redis.ClientKillFilter! +StackExchange.Redis.ClientKillFilter.WithServerEndpoint(System.Net.EndPoint? serverEndpoint) -> StackExchange.Redis.ClientKillFilter! +StackExchange.Redis.ClientKillFilter.WithSkipMe(bool? skipMe) -> StackExchange.Redis.ClientKillFilter! +StackExchange.Redis.ClientKillFilter.WithUsername(string? username) -> StackExchange.Redis.ClientKillFilter! StackExchange.Redis.ClientType StackExchange.Redis.ClientType.Normal = 0 -> StackExchange.Redis.ClientType StackExchange.Redis.ClientType.PubSub = 2 -> StackExchange.Redis.ClientType @@ -1006,8 +1022,10 @@ StackExchange.Redis.IServer.AllowSlaveWrites.get -> bool StackExchange.Redis.IServer.AllowSlaveWrites.set -> void StackExchange.Redis.IServer.ClientKill(long? id = null, StackExchange.Redis.ClientType? clientType = null, System.Net.EndPoint? endpoint = null, bool skipMe = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IServer.ClientKill(System.Net.EndPoint! endpoint, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +StackExchange.Redis.IServer.ClientKill(StackExchange.Redis.ClientKillFilter! filter, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IServer.ClientKillAsync(long? id = null, StackExchange.Redis.ClientType? clientType = null, System.Net.EndPoint? endpoint = null, bool skipMe = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.ClientKillAsync(System.Net.EndPoint! endpoint, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IServer.ClientKillAsync(StackExchange.Redis.ClientKillFilter! filter, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.ClientList(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ClientInfo![]! StackExchange.Redis.IServer.ClientListAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IServer.ClusterConfiguration.get -> StackExchange.Redis.ClusterConfiguration? @@ -1851,4 +1869,5 @@ static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[]! virtual StackExchange.Redis.RedisResult.Length.get -> int virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult! StackExchange.Redis.ConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void -StackExchange.Redis.IConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void \ No newline at end of file +StackExchange.Redis.IConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void + diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index bfd6b1a44..11d2ba016 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -89,6 +89,7 @@ public static readonly RedisValue IDLETIME = "IDLETIME", KEEPTTL = "KEEPTTL", KILL = "KILL", + LADDR = "LADDR", LATEST = "LATEST", LEFT = "LEFT", LEN = "LEN", @@ -101,6 +102,7 @@ public static readonly RedisValue MATCH = "MATCH", MALLOC_STATS = "MALLOC-STATS", MAX = "MAX", + MAXAGE = "MAXAGE", MAXLEN = "MAXLEN", MIN = "MIN", MINMATCHLEN = "MINMATCHLEN", @@ -137,6 +139,7 @@ public static readonly RedisValue STATS = "STATS", STORE = "STORE", TYPE = "TYPE", + USERNAME = "USERNAME", WEIGHTS = "WEIGHTS", WITHMATCHLEN = "WITHMATCHLEN", WITHSCORES = "WITHSCORES", diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 0fec00428..1f7791dd2 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -75,46 +75,22 @@ public Task ClientKillAsync(long? id = null, ClientType? clientType = null return ExecuteAsync(msg, ResultProcessor.Int64); } - private Message GetClientKillMessage(EndPoint? endpoint, long? id, ClientType? clientType, bool skipMe, CommandFlags flags) + public long ClientKill(ClientKillFilter filter, CommandFlags flags = CommandFlags.None) { - var parts = new List(9) - { - RedisLiterals.KILL - }; - if (id != null) - { - parts.Add(RedisLiterals.ID); - parts.Add(id.Value); - } - if (clientType != null) - { - parts.Add(RedisLiterals.TYPE); - switch (clientType.Value) - { - case ClientType.Normal: - parts.Add(RedisLiterals.normal); - break; - case ClientType.Replica: - parts.Add(Features.ReplicaCommands ? RedisLiterals.replica : RedisLiterals.slave); - break; - case ClientType.PubSub: - parts.Add(RedisLiterals.pubsub); - break; - default: - throw new ArgumentOutOfRangeException(nameof(clientType)); - } - } - if (endpoint != null) - { - parts.Add(RedisLiterals.ADDR); - parts.Add((RedisValue)Format.ToString(endpoint)); - } - if (!skipMe) - { - parts.Add(RedisLiterals.SKIPME); - parts.Add(RedisLiterals.no); - } - return Message.Create(-1, flags, RedisCommand.CLIENT, parts); + var msg = Message.Create(-1, flags, RedisCommand.CLIENT, filter.ToList(Features.ReplicaCommands)); + return ExecuteSync(msg, ResultProcessor.Int64); + } + + public Task ClientKillAsync(ClientKillFilter filter, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(-1, flags, RedisCommand.CLIENT, filter.ToList(Features.ReplicaCommands)); + return ExecuteAsync(msg, ResultProcessor.Int64); + } + + private Message GetClientKillMessage(EndPoint? endpoint, long? id, ClientType? clientType, bool? skipMe, CommandFlags flags) + { + var args = new ClientKillFilter().WithId(id).WithClientType(clientType).WithEndpoint(endpoint).WithSkipMe(skipMe).ToList(Features.ReplicaCommands); + return Message.Create(-1, flags, RedisCommand.CLIENT, args); } public ClientInfo[] ClientList(CommandFlags flags = CommandFlags.None) @@ -408,7 +384,7 @@ public Task LastSaveAsync(CommandFlags flags = CommandFlags.None) } public void MakeMaster(ReplicationChangeOptions options, TextWriter? log = null) - { + { // Do you believe in magic? multiplexer.MakePrimaryAsync(server, options, log).Wait(60000); } diff --git a/tests/StackExchange.Redis.Tests/ClientKillTests.cs b/tests/StackExchange.Redis.Tests/ClientKillTests.cs new file mode 100644 index 000000000..34f00c6bc --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ClientKillTests.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Net; +using System.Threading; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests; + +[RunPerProtocol] + +public class ClientKillTests : TestBase +{ + protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; + public ClientKillTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void ClientKill() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + + SetExpectedAmbientFailureCount(-1); + using var otherConnection = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); + var id = otherConnection.GetDatabase().Execute(RedisCommand.CLIENT.ToString(), RedisLiterals.ID); + + using var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); + var server = conn.GetServer(conn.GetEndPoints()[0]); + long result = server.ClientKill(id.AsInt64(), ClientType.Normal, null, true); + Assert.Equal(1, result); + } + + [Fact] + public void ClientKillWithMaxAge() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + + SetExpectedAmbientFailureCount(-1); + using var otherConnection = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); + var id = otherConnection.GetDatabase().Execute(RedisCommand.CLIENT.ToString(), RedisLiterals.ID); + Thread.Sleep(1000); + + using var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); + var server = conn.GetServer(conn.GetEndPoints()[0]); + var filter = new ClientKillFilter().WithId(id.AsInt64()).WithMaxAgeInSeconds(1).WithSkipMe(true); + long result = server.ClientKill(filter, CommandFlags.DemandMaster); + Assert.Equal(1, result); + } + + [Fact] + public void TestClientKillMessageWithAllArguments() + { + long id = 101; + ClientType type = ClientType.Normal; + string userName = "user1"; + EndPoint endpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1234); + EndPoint serverEndpoint = new IPEndPoint(IPAddress.Parse("198.0.0.1"), 6379); ; + bool skipMe = true; + long maxAge = 102; + + var filter = new ClientKillFilter().WithId(id).WithClientType(type).WithUsername(userName).WithEndpoint(endpoint).WithServerEndpoint(serverEndpoint).WithSkipMe(skipMe).WithMaxAgeInSeconds(maxAge); + List expected = new List() + { + "KILL", "ID", "101", "TYPE", "normal", "USERNAME", "user1", "ADDR", "127.0.0.1:1234", "LADDR", "198.0.0.1:6379", "SKIPME", "yes", "MAXAGE", "102" + }; + Assert.Equal(expected, filter.ToList(true)); + } +} From dee9dba3d0ffc899d623eda8eaf131d64b0db20f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 25 Jul 2024 19:34:45 +0100 Subject: [PATCH 292/435] use minimal API for kestrel toy project --- .../KestrelRedisServer.csproj | 4 +- toys/KestrelRedisServer/Program.cs | 49 ++++++++++++------- toys/KestrelRedisServer/Startup.cs | 45 ----------------- 3 files changed, 34 insertions(+), 64 deletions(-) delete mode 100644 toys/KestrelRedisServer/Startup.cs diff --git a/toys/KestrelRedisServer/KestrelRedisServer.csproj b/toys/KestrelRedisServer/KestrelRedisServer.csproj index 11bb95103..8854d6ac8 100644 --- a/toys/KestrelRedisServer/KestrelRedisServer.csproj +++ b/toys/KestrelRedisServer/KestrelRedisServer.csproj @@ -1,8 +1,10 @@  - net6.0 + net8.0 $(NoWarn);CS1591 + enable + enable diff --git a/toys/KestrelRedisServer/Program.cs b/toys/KestrelRedisServer/Program.cs index 5139db291..349ad5d71 100644 --- a/toys/KestrelRedisServer/Program.cs +++ b/toys/KestrelRedisServer/Program.cs @@ -1,25 +1,38 @@ -using Microsoft.AspNetCore; +using KestrelRedisServer; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Hosting; +using StackExchange.Redis.Server; -namespace KestrelRedisServer +var server = new MemoryCacheRedisServer(); + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddSingleton(server); +builder.WebHost.ConfigureKestrel(options => { - public static class Program - { - public static void Main(string[] args) => CreateWebHostBuilder(args).Build().Run(); + // HTTP 5000 (test/debug API only) + options.ListenLocalhost(5000); - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseKestrel(options => - { - // Moved to SocketTransportOptions.UnsafePreferInlineScheduling = true; - //options.ApplicationSchedulingMode = SchedulingMode.Inline; + // this is the core of using Kestrel to create a TCP server + // TCP 6379 + options.ListenLocalhost(6379, builder => builder.UseConnectionHandler()); +}); - // HTTP 5000 - options.ListenLocalhost(5000); +var app = builder.Build(); - // TCP 6379 - options.ListenLocalhost(6379, builder => builder.UseConnectionHandler()); - }).UseStartup(); +// redis-specific hack - there is a redis command to shutdown the server +_ = server.Shutdown.ContinueWith(static (t, s) => +{ + try + { // if the resp server is shutdown by a client: stop the kestrel server too + if (t.Result == RespServer.ShutdownReason.ClientInitiated) + { + ((IServiceProvider)s!).GetService()?.StopApplication(); + } } -} + catch { /* Don't go boom on shutdown */ } +}, app.Services); + +// add debug route +app.Run(context => context.Response.WriteAsync(server.GetStats())); + +// run the server +await app.RunAsync(); diff --git a/toys/KestrelRedisServer/Startup.cs b/toys/KestrelRedisServer/Startup.cs deleted file mode 100644 index ab991c5fa..000000000 --- a/toys/KestrelRedisServer/Startup.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using StackExchange.Redis.Server; - -namespace KestrelRedisServer -{ - public class Startup : IDisposable - { - private readonly RespServer _server = new MemoryCacheRedisServer(); - - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - => services.Add(new ServiceDescriptor(typeof(RespServer), _server)); - - public void Dispose() - { - _server.Dispose(); - GC.SuppressFinalize(this); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime lifetime) - { - _server.Shutdown.ContinueWith((t, s) => - { - try - { // if the resp server is shutdown by a client: stop the kestrel server too - if (t.Result == RespServer.ShutdownReason.ClientInitiated) - { - ((IHostApplicationLifetime)s).StopApplication(); - } - } - catch { /* Don't go boom on shutdown */ } - }, lifetime); - - if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); - app.Run(context => context.Response.WriteAsync(_server.GetStats())); - } - } -} From 1e405090ca0b1869088ee48e82ad294c9d4b8339 Mon Sep 17 00:00:00 2001 From: Willem van Ketwich Date: Sun, 28 Jul 2024 16:48:44 +1000 Subject: [PATCH 293/435] fix typo (#2766) Co-authored-by: Willem van Ketwich --- src/StackExchange.Redis/Configuration/LoggingTunnel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs index a058a522c..a30dedf85 100644 --- a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs +++ b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs @@ -287,7 +287,7 @@ internal DirectoryLoggingTunnel(string path, ConfigurationOptions? options = nul : base(options, tail) { this.path = path; - if (!Directory.Exists(path)) throw new InvalidOperationException("Directly does not exist: " + path); + if (!Directory.Exists(path)) throw new InvalidOperationException("Directory does not exist: " + path); } protected override Stream Log(Stream stream, EndPoint endpoint, ConnectionType connectionType) From 4852cca54a4b426bb7244c37153c443ec8bd42f8 Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Sat, 3 Aug 2024 16:47:25 +0300 Subject: [PATCH 294/435] switch to docker compose (#2771) docker-compose is not available by default anymore on `ubuntu-latest` [details here]( https://github.com/actions/runner-images/issues/9692) --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6f070b83d..5e0b81ea7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -29,7 +29,7 @@ jobs: run: dotnet build Build.csproj -c Release /p:CI=true - name: Start Redis Services (docker-compose) working-directory: ./tests/RedisConfigs - run: docker-compose -f docker-compose.yml up -d + run: docker compose -f docker-compose.yml up -d - name: StackExchange.Redis.Tests run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true - uses: dorny/test-reporter@v1 From 8346a5c25e56b4823c23d8c13d386025c86603a5 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 3 Aug 2024 10:29:39 -0400 Subject: [PATCH 295/435] Project: Enable StyleCop and fix existing rules to make PRs easier going forward (#2757) * Project: Enable StyleCop and fix existing rules to make PRs easier going forward This adopts StyleCop and fixes most issues (and disables things we wouldn't want) to make PRs more consistent in an automated way and prevent formatting problems affecting git history, etc. It also does all the documentation enforcement. Note: since I had to fix up many anyway, I finally did the `` minimization on `IDatabase`/`IDatabaseAsync` to remove 10% of duplicate documentation. That should also make PRs and maintenance less monotonous. * Fix stylecop issues --- .editorconfig | 78 +- Directory.Build.props | 3 + Directory.Packages.props | 1 + .../APITypes/ClientKillFilter.cs | 8 +- .../APITypes/GeoRadiusOptions.cs | 6 +- .../APITypes/GeoRadiusResult.cs | 2 +- .../APITypes/GeoSearchShape.cs | 15 +- .../APITypes/LatencyHistoryEntry.cs | 6 +- .../APITypes/LatencyLatestEntry.cs | 10 +- .../APITypes/StreamPendingMessageInfo.cs | 3 +- src/StackExchange.Redis/BufferReader.cs | 22 +- .../ChannelMessageQueue.cs | 17 +- src/StackExchange.Redis/ClientInfo.cs | 6 +- .../ClusterConfiguration.cs | 13 +- src/StackExchange.Redis/CommandBytes.cs | 11 +- src/StackExchange.Redis/CommandMap.cs | 32 +- src/StackExchange.Redis/CommandTrace.cs | 10 +- src/StackExchange.Redis/Condition.cs | 4 +- .../Configuration/AzureOptionsProvider.cs | 4 +- .../Configuration/DefaultOptionsProvider.cs | 17 +- .../Configuration/LoggingTunnel.cs | 39 +- .../Configuration/Tunnel.cs | 3 +- .../ConfigurationOptions.cs | 35 +- src/StackExchange.Redis/ConnectionCounters.cs | 2 +- .../ConnectionFailedEventArgs.cs | 2 +- ...nnectionMultiplexer.ExportConfiguration.cs | 2 +- .../ConnectionMultiplexer.FeatureFlags.cs | 3 +- .../ConnectionMultiplexer.Profiling.cs | 2 +- .../ConnectionMultiplexer.Sentinel.cs | 129 +- .../ConnectionMultiplexer.StormLog.cs | 1 + .../ConnectionMultiplexer.cs | 32 +- src/StackExchange.Redis/CursorEnumerable.cs | 14 +- src/StackExchange.Redis/EndPointCollection.cs | 10 +- src/StackExchange.Redis/EndPointEventArgs.cs | 3 +- src/StackExchange.Redis/Enums/Aggregate.cs | 4 +- src/StackExchange.Redis/Enums/Bitwise.cs | 3 + src/StackExchange.Redis/Enums/ClientFlags.cs | 17 + src/StackExchange.Redis/Enums/ClientType.cs | 13 +- src/StackExchange.Redis/Enums/CommandFlags.cs | 9 +- .../Enums/CommandStatus.cs | 3 + .../Enums/ConnectionFailureType.cs | 12 +- .../Enums/ConnectionType.cs | 4 +- src/StackExchange.Redis/Enums/Exclude.cs | 5 +- src/StackExchange.Redis/Enums/ExpireWhen.cs | 8 +- .../Enums/ExportOptions.cs | 7 +- src/StackExchange.Redis/Enums/GeoUnit.cs | 13 +- src/StackExchange.Redis/Enums/ListSide.cs | 3 +- .../Enums/MigrateOptions.cs | 2 + src/StackExchange.Redis/Enums/Order.cs | 3 +- src/StackExchange.Redis/Enums/Proxy.cs | 8 +- src/StackExchange.Redis/Enums/RedisType.cs | 7 + .../Enums/ReplicationChangeOptions.cs | 5 + src/StackExchange.Redis/Enums/ResultType.cs | 5 +- .../Enums/RetransmissionReasonType.cs | 2 + src/StackExchange.Redis/Enums/SaveType.cs | 2 + src/StackExchange.Redis/Enums/ServerType.cs | 8 +- src/StackExchange.Redis/Enums/SetOperation.cs | 12 +- src/StackExchange.Redis/Enums/ShutdownMode.cs | 2 + .../Enums/SimulatedFailureType.cs | 8 +- src/StackExchange.Redis/Enums/SortType.cs | 1 + .../Enums/SortedSetOrder.cs | 4 +- .../Enums/SortedSetWhen.cs | 10 +- .../Enums/StringIndexType.cs | 3 +- src/StackExchange.Redis/Enums/When.cs | 2 + src/StackExchange.Redis/ExceptionFactory.cs | 13 +- src/StackExchange.Redis/Exceptions.cs | 6 +- src/StackExchange.Redis/ExponentialRetry.cs | 26 +- src/StackExchange.Redis/ExtensionMethods.cs | 15 +- src/StackExchange.Redis/Format.cs | 10 +- src/StackExchange.Redis/GlobalSuppressions.cs | 28 +- .../HashSlotMovedEventArgs.cs | 5 +- .../Interfaces/IConnectionMultiplexer.cs | 6 +- .../Interfaces/IDatabase.cs | 146 +- .../Interfaces/IDatabaseAsync.cs | 2623 ++--------------- src/StackExchange.Redis/Interfaces/IServer.cs | 29 +- .../Interfaces/ISubscriber.cs | 14 +- .../InternalErrorEventArgs.cs | 4 +- .../KeyspaceIsolation/KeyPrefixed.cs | 1 - .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 10 +- src/StackExchange.Redis/LuaScript.cs | 2 +- .../Maintenance/AzureMaintenanceEvent.cs | 2 +- src/StackExchange.Redis/Message.cs | 178 +- src/StackExchange.Redis/PhysicalBridge.cs | 44 +- src/StackExchange.Redis/PhysicalConnection.cs | 201 +- .../Profiling/ProfiledCommand.cs | 1 - .../Profiling/ProfiledCommandEnumerable.cs | 15 +- .../Profiling/ProfilingSession.cs | 1 + src/StackExchange.Redis/RawResult.cs | 18 +- src/StackExchange.Redis/RedisBatch.cs | 8 +- src/StackExchange.Redis/RedisChannel.cs | 20 +- src/StackExchange.Redis/RedisDatabase.cs | 350 ++- .../RedisErrorEventArgs.cs | 8 +- src/StackExchange.Redis/RedisFeatures.cs | 12 +- src/StackExchange.Redis/RedisKey.cs | 13 +- src/StackExchange.Redis/RedisLiterals.cs | 8 +- src/StackExchange.Redis/RedisProtocol.cs | 5 +- src/StackExchange.Redis/RedisResult.cs | 42 +- src/StackExchange.Redis/RedisServer.cs | 10 +- src/StackExchange.Redis/RedisSubscriber.cs | 4 +- src/StackExchange.Redis/RedisTransaction.cs | 12 +- src/StackExchange.Redis/RedisValue.cs | 56 +- src/StackExchange.Redis/ResultBox.cs | 9 +- src/StackExchange.Redis/ResultProcessor.cs | 91 +- .../ScriptParameterMapper.cs | 21 +- src/StackExchange.Redis/ServerCounters.cs | 2 +- src/StackExchange.Redis/ServerEndPoint.cs | 31 +- .../ServerSelectionStrategy.cs | 76 +- src/StackExchange.Redis/SkipLocalsInit.cs | 2 +- src/StackExchange.Redis/SocketManager.cs | 27 +- src/StackExchange.Redis/StreamConstants.cs | 3 +- src/StackExchange.Redis/TaskExtensions.cs | 2 +- tests/BasicTest/Program.cs | 33 +- tests/ConsoleTest/Program.cs | 33 +- .../AbortOnConnectFailTests.cs | 6 +- tests/StackExchange.Redis.Tests/AdhocTests.cs | 4 +- .../AggressiveTests.cs | 12 +- .../StackExchange.Redis.Tests/BacklogTests.cs | 17 +- tests/StackExchange.Redis.Tests/BitTests.cs | 2 +- .../BoxUnboxTests.cs | 12 +- .../ClientKillTests.cs | 4 +- .../StackExchange.Redis.Tests/ClusterTests.cs | 51 +- .../CommandTimeoutTests.cs | 2 +- .../StackExchange.Redis.Tests/ConfigTests.cs | 92 +- .../ConnectByIPTests.cs | 10 +- .../ConnectCustomConfigTests.cs | 2 +- .../ConnectFailTimeoutTests.cs | 7 +- .../ConnectToUnexistingHostTests.cs | 16 +- .../ConnectingFailDetectionTests.cs | 2 +- .../ConnectionFailedErrorsTests.cs | 25 +- .../ConnectionReconnectRetryPolicyTests.cs | 2 +- tests/StackExchange.Redis.Tests/CopyTests.cs | 2 +- .../ExceptionFactoryTests.cs | 4 +- .../StackExchange.Redis.Tests/ExpiryTests.cs | 4 +- .../FSharpCompatTests.cs | 4 +- .../FailoverTests.cs | 4 +- .../FloatingPointTests.cs | 86 +- .../StackExchange.Redis.Tests/FormatTests.cs | 1 - .../GarbageCollectionTests.cs | 28 +- tests/StackExchange.Redis.Tests/GeoTests.cs | 78 +- .../GlobalSuppressions.cs | 33 +- tests/StackExchange.Redis.Tests/HashTests.cs | 13 +- .../Helpers/Attributes.cs | 2 +- .../Helpers/SharedConnectionFixture.cs | 8 +- .../Helpers/TestConfig.cs | 8 +- .../Helpers/redis-sharp.cs | 15 +- .../InfoReplicationCheckTests.cs | 2 +- .../Issues/BgSaveResponseTests.cs | 8 +- .../Issues/Issue182Tests.cs | 2 +- .../Issues/Issue2176Tests.cs | 6 +- .../Issues/Issue2418.cs | 1 - .../Issues/Issue25Tests.cs | 2 +- .../Issues/Issue6Tests.cs | 4 +- .../Issues/MassiveDeleteTests.cs | 3 +- .../Issues/SO22786599Tests.cs | 8 +- .../Issues/SO23949477Tests.cs | 16 +- .../Issues/SO24807536Tests.cs | 2 +- .../Issues/SO25113323Tests.cs | 2 +- .../Issues/SO25567566Tests.cs | 2 +- .../KeyAndValueTests.cs | 20 +- .../KeyPrefixedBatchTests.cs | 4 +- .../KeyPrefixedTests.cs | 4 +- tests/StackExchange.Redis.Tests/KeyTests.cs | 4 +- tests/StackExchange.Redis.Tests/LexTests.cs | 58 +- .../StackExchange.Redis.Tests/LockingTests.cs | 60 +- .../StackExchange.Redis.Tests/LoggerTests.cs | 16 +- .../MassiveOpsTests.cs | 44 +- .../StackExchange.Redis.Tests/MigrateTests.cs | 2 +- .../MultiAddTests.cs | 72 +- .../MultiPrimaryTests.cs | 2 +- .../OverloadCompatTests.cs | 9 +- .../PreserveOrderTests.cs | 2 +- .../ProfilingTests.cs | 16 +- .../PubSubCommandTests.cs | 10 +- .../PubSubMultiserverTests.cs | 13 +- .../StackExchange.Redis.Tests/PubSubTests.cs | 69 +- .../RedisResultTests.cs | 28 +- .../RedisValueEquivalencyTests.cs | 1 - .../RespProtocolTests.cs | 45 +- tests/StackExchange.Redis.Tests/SSDBTests.cs | 4 +- tests/StackExchange.Redis.Tests/SSLTests.cs | 73 +- .../SanityCheckTests.cs | 6 +- tests/StackExchange.Redis.Tests/ScanTests.cs | 6 +- .../ScriptingTests.cs | 61 +- .../StackExchange.Redis.Tests/SecureTests.cs | 5 +- .../SentinelTests.cs | 58 +- tests/StackExchange.Redis.Tests/SetTests.cs | 14 +- .../StackExchange.Redis.Tests/SocketTests.cs | 2 +- .../SortedSetTests.cs | 25 +- .../StackExchange.Redis.Tests/StreamTests.cs | 101 +- .../StackExchange.Redis.Tests/StringTests.cs | 4 - .../SyncContextTests.cs | 12 +- tests/StackExchange.Redis.Tests/TestBase.cs | 67 +- .../TransactionTests.cs | 8 +- tests/StackExchange.Redis.Tests/ValueTests.cs | 2 +- toys/KestrelRedisServer/Program.cs | 21 +- .../GlobalSuppressions.cs | 2 +- .../MemoryCacheRedisServer.cs | 4 +- .../RedisRequest.cs | 3 +- .../StackExchange.Redis.Server/RedisServer.cs | 4 +- toys/StackExchange.Redis.Server/RespServer.cs | 19 +- .../TypedRedisValue.cs | 7 +- 201 files changed, 2553 insertions(+), 4054 deletions(-) diff --git a/.editorconfig b/.editorconfig index 6934b4a38..a00642936 100644 --- a/.editorconfig +++ b/.editorconfig @@ -138,23 +138,73 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true -# IDE0090: Use 'new(...)' -dotnet_diagnostic.IDE0090.severity = silent -# RCS1037: Remove trailing white-space. -dotnet_diagnostic.RCS1037.severity = error +# IDE preferences +dotnet_diagnostic.IDE0090.severity = silent # IDE0090: Use 'new(...)' + +#Roslynator preferences +dotnet_diagnostic.RCS1037.severity = error # RCS1037: Remove trailing white-space. +dotnet_diagnostic.RCS1098.severity = none # RCS1098: Constant values should be placed on right side of comparisons. + +dotnet_diagnostic.RCS1194.severity = none # RCS1194: Implement exception constructors. +dotnet_diagnostic.RCS1229.severity = none # RCS1229: Use async/await when necessary. +dotnet_diagnostic.RCS1233.severity = none # RCS1233: Use short-circuiting operator. +dotnet_diagnostic.RCS1234.severity = none # RCS1234: Duplicate enum value. + +# StyleCop preferences +dotnet_diagnostic.SA0001.severity = none # SA0001: XML comment analysis is disabled + +dotnet_diagnostic.SA1101.severity = none # SA1101: Prefix local calls with this +dotnet_diagnostic.SA1108.severity = none # SA1108: Block statements should not contain embedded comments +dotnet_diagnostic.SA1122.severity = none # SA1122: Use string.Empty for empty strings +dotnet_diagnostic.SA1127.severity = none # SA1127: Generic type constraints should be on their own line +dotnet_diagnostic.SA1128.severity = none # SA1128: Put constructor initializers on their own line +dotnet_diagnostic.SA1132.severity = none # SA1132: Do not combine fields +dotnet_diagnostic.SA1133.severity = none # SA1133: Do not combine attributes + +dotnet_diagnostic.SA1200.severity = none # SA1200: Using directives should be placed correctly +dotnet_diagnostic.SA1201.severity = none # SA1201: Elements should appear in the correct order +dotnet_diagnostic.SA1202.severity = none # SA1202: Elements should be ordered by access +dotnet_diagnostic.SA1203.severity = none # SA1203: Constants should appear before fields + +dotnet_diagnostic.SA1306.severity = none # SA1306: Field names should begin with lower-case letter +dotnet_diagnostic.SA1309.severity = none # SA1309: Field names should not begin with underscore +dotnet_diagnostic.SA1310.severity = silent # SA1310: Field names should not contain underscore +dotnet_diagnostic.SA1311.severity = none # SA1311: Static readonly fields should begin with upper-case letter +dotnet_diagnostic.SA1312.severity = none # SA1312: Variable names should begin with lower-case letter + +dotnet_diagnostic.SA1401.severity = silent # SA1401: Fields should be private +dotnet_diagnostic.SA1402.severity = suggestion # SA1402: File may only contain a single type + +dotnet_diagnostic.SA1503.severity = silent # SA1503: Braces should not be omitted +dotnet_diagnostic.SA1516.severity = silent # SA1516: Elements should be separated by blank line + +dotnet_diagnostic.SA1600.severity = none # SA1600: Elements should be documented +dotnet_diagnostic.SA1601.severity = none # SA1601: Partial elements should be documented +dotnet_diagnostic.SA1602.severity = none # SA1602: Enumeration items should be documented +dotnet_diagnostic.SA1615.severity = none # SA1615: Element return value should be documented +dotnet_diagnostic.SA1623.severity = none # SA1623: Property summary documentation should match accessors +dotnet_diagnostic.SA1633.severity = none # SA1633: File should have header +dotnet_diagnostic.SA1642.severity = none # SA1642: Constructor summary documentation should begin with standard text +dotnet_diagnostic.SA1643.severity = none # SA1643: Destructor summary documentation should begin with standard text + + +# To Fix: +dotnet_diagnostic.SA1204.severity = none # SA1204: Static elements should appear before instance elements +dotnet_diagnostic.SA1214.severity = none # SA1214: Readonly fields should appear before non-readonly fields +dotnet_diagnostic.SA1304.severity = none # SA1304: Non-private readonly fields should begin with upper-case letter +dotnet_diagnostic.SA1307.severity = none # SA1307: Accessible fields should begin with upper-case letter +dotnet_diagnostic.SA1308.severity = suggestion # SA1308: Variable names should not be prefixed +dotnet_diagnostic.SA1131.severity = none # SA1131: Use readable conditions +dotnet_diagnostic.SA1405.severity = none # SA1405: Debug.Assert should provide message text +dotnet_diagnostic.SA1501.severity = none # SA1501: Statement should not be on a single line +dotnet_diagnostic.SA1502.severity = suggestion # SA1502: Element should not be on a single line +dotnet_diagnostic.SA1513.severity = none # SA1513: Closing brace should be followed by blank line +dotnet_diagnostic.SA1515.severity = none # SA1515: Single-line comment should be preceded by blank line +dotnet_diagnostic.SA1611.severity = suggestion # SA1611: Element parameters should be documented +dotnet_diagnostic.SA1649.severity = suggestion # SA1649: File name should match first type name -# RCS1098: Constant values should be placed on right side of comparisons. -dotnet_diagnostic.RCS1098.severity = none -# RCS1194: Implement exception constructors. -dotnet_diagnostic.RCS1194.severity = none -# RCS1229: Use async/await when necessary. -dotnet_diagnostic.RCS1229.severity = none -# RCS1233: Use short-circuiting operator. -dotnet_diagnostic.RCS1233.severity = none -# RCS1234: Duplicate enum value. -dotnet_diagnostic.RCS1234.severity = none \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 43dd33169..ff720e26a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,10 +33,13 @@ true true + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 9ef9afb25..49d37a3ae 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,6 +20,7 @@ + diff --git a/src/StackExchange.Redis/APITypes/ClientKillFilter.cs b/src/StackExchange.Redis/APITypes/ClientKillFilter.cs index b5c5e845c..3d1883549 100644 --- a/src/StackExchange.Redis/APITypes/ClientKillFilter.cs +++ b/src/StackExchange.Redis/APITypes/ClientKillFilter.cs @@ -113,7 +113,7 @@ public ClientKillFilter WithSkipMe(bool? skipMe) /// /// Set the MaxAgeInSeconds filter. /// - /// Age of connection in seconds + /// Age of connection in seconds. public ClientKillFilter WithMaxAgeInSeconds(long? maxAgeInSeconds) { MaxAgeInSeconds = maxAgeInSeconds; @@ -123,9 +123,9 @@ public ClientKillFilter WithMaxAgeInSeconds(long? maxAgeInSeconds) internal List ToList(bool withReplicaCommands) { var parts = new List(15) - { - RedisLiterals.KILL - }; + { + RedisLiterals.KILL, + }; if (Id != null) { parts.Add(RedisLiterals.ID); diff --git a/src/StackExchange.Redis/APITypes/GeoRadiusOptions.cs b/src/StackExchange.Redis/APITypes/GeoRadiusOptions.cs index f9f182f5b..d21254fcd 100644 --- a/src/StackExchange.Redis/APITypes/GeoRadiusOptions.cs +++ b/src/StackExchange.Redis/APITypes/GeoRadiusOptions.cs @@ -13,22 +13,26 @@ public enum GeoRadiusOptions /// No Options. /// None = 0, + /// /// Redis will return the coordinates of any results. /// WithCoordinates = 1, + /// /// Redis will return the distance from center for all results. /// WithDistance = 2, + /// /// Redis will return the geo hash value as an integer. (This is the score in the sorted set). /// WithGeoHash = 4, + /// /// Populates the commonly used values from the entry (the integer hash is not returned as it is not commonly useful). /// - Default = WithCoordinates | WithDistance + Default = WithCoordinates | WithDistance, } internal static class GeoRadiusOptionsExtensions diff --git a/src/StackExchange.Redis/APITypes/GeoRadiusResult.cs b/src/StackExchange.Redis/APITypes/GeoRadiusResult.cs index d4cdbe8f8..952ca1625 100644 --- a/src/StackExchange.Redis/APITypes/GeoRadiusResult.cs +++ b/src/StackExchange.Redis/APITypes/GeoRadiusResult.cs @@ -23,7 +23,7 @@ public readonly struct GeoRadiusResult /// /// The hash value of the matched member as an integer. (The key in the sorted set). /// - /// Note that this is not the same as the hash returned from GeoHash + /// Note that this is not the same as the hash returned from GeoHash. public long? Hash { get; } /// diff --git a/src/StackExchange.Redis/APITypes/GeoSearchShape.cs b/src/StackExchange.Redis/APITypes/GeoSearchShape.cs index 68f1ee754..7d85c3bfa 100644 --- a/src/StackExchange.Redis/APITypes/GeoSearchShape.cs +++ b/src/StackExchange.Redis/APITypes/GeoSearchShape.cs @@ -3,7 +3,7 @@ namespace StackExchange.Redis; /// -/// A Shape that you can use for a GeoSearch +/// A Shape that you can use for a GeoSearch. /// public abstract class GeoSearchShape { @@ -18,9 +18,9 @@ public abstract class GeoSearchShape internal abstract int ArgCount { get; } /// - /// constructs a + /// constructs a . /// - /// + /// The geography unit to use. public GeoSearchShape(GeoUnit unit) { Unit = unit; @@ -30,7 +30,7 @@ public GeoSearchShape(GeoUnit unit) } /// -/// A circle drawn on a map bounding +/// A circle drawn on a map bounding. /// public class GeoSearchCircle : GeoSearchShape { @@ -41,7 +41,7 @@ public class GeoSearchCircle : GeoSearchShape /// /// The radius of the circle. /// The distance unit the circle will use, defaults to Meters. - public GeoSearchCircle(double radius, GeoUnit unit = GeoUnit.Meters) : base (unit) + public GeoSearchCircle(double radius, GeoUnit unit = GeoUnit.Meters) : base(unit) { _radius = radius; } @@ -49,9 +49,8 @@ public GeoSearchCircle(double radius, GeoUnit unit = GeoUnit.Meters) : base (uni internal override int ArgCount => 3; /// - /// Gets the s for this shape + /// Gets the s for this shape. /// - /// internal override void AddArgs(List args) { args.Add(RedisLiterals.BYRADIUS); @@ -61,7 +60,7 @@ internal override void AddArgs(List args) } /// -/// A box drawn on a map +/// A box drawn on a map. /// public class GeoSearchBox : GeoSearchShape { diff --git a/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs b/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs index 2303c6e49..003708e6a 100644 --- a/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs +++ b/src/StackExchange.Redis/APITypes/LatencyHistoryEntry.cs @@ -3,7 +3,7 @@ namespace StackExchange.Redis; /// -/// A latency entry as reported by the built-in LATENCY HISTORY command +/// A latency entry as reported by the built-in LATENCY HISTORY command. /// public readonly struct LatencyHistoryEntry { @@ -30,12 +30,12 @@ protected override bool TryParse(in RawResult raw, out LatencyHistoryEntry parse } /// - /// The time at which this entry was recorded + /// The time at which this entry was recorded. /// public DateTime Timestamp { get; } /// - /// The latency recorded for this event + /// The latency recorded for this event. /// public int DurationMilliseconds { get; } diff --git a/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs b/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs index 67e416dc8..d1bc70e42 100644 --- a/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs +++ b/src/StackExchange.Redis/APITypes/LatencyLatestEntry.cs @@ -3,7 +3,7 @@ namespace StackExchange.Redis; /// -/// A latency entry as reported by the built-in LATENCY LATEST command +/// A latency entry as reported by the built-in LATENCY LATEST command. /// public readonly struct LatencyLatestEntry { @@ -31,22 +31,22 @@ protected override bool TryParse(in RawResult raw, out LatencyLatestEntry parsed } /// - /// The name of this event + /// The name of this event. /// public string EventName { get; } /// - /// The time at which this entry was recorded + /// The time at which this entry was recorded. /// public DateTime Timestamp { get; } /// - /// The latency recorded for this event + /// The latency recorded for this event. /// public int DurationMilliseconds { get; } /// - /// The max latency recorded for all events + /// The max latency recorded for all events. /// public int MaxDurationMilliseconds { get; } diff --git a/src/StackExchange.Redis/APITypes/StreamPendingMessageInfo.cs b/src/StackExchange.Redis/APITypes/StreamPendingMessageInfo.cs index 95f545ca5..32cbbcc8d 100644 --- a/src/StackExchange.Redis/APITypes/StreamPendingMessageInfo.cs +++ b/src/StackExchange.Redis/APITypes/StreamPendingMessageInfo.cs @@ -1,5 +1,4 @@ - -namespace StackExchange.Redis; +namespace StackExchange.Redis; /// /// Describes properties of a pending message. diff --git a/src/StackExchange.Redis/BufferReader.cs b/src/StackExchange.Redis/BufferReader.cs index 5691217f9..22b36ccb6 100644 --- a/src/StackExchange.Redis/BufferReader.cs +++ b/src/StackExchange.Redis/BufferReader.cs @@ -41,7 +41,8 @@ private bool FetchNextSegment() _current = _iterator.Current.Span; OffsetThisSpan = 0; RemainingThisSpan = _current.Length; - } while (IsEmpty); // skip empty segments, they don't help us! + } + while (IsEmpty); // skip empty segments, they don't help us! return true; } @@ -59,7 +60,7 @@ public BufferReader(scoped in ReadOnlySequence buffer) } /// - /// Note that in results other than success, no guarantees are made about final state; if you care: snapshot + /// Note that in results other than success, no guarantees are made about final state; if you care: snapshot. /// public ConsumeResult TryConsumeCRLF() { @@ -82,6 +83,7 @@ public ConsumeResult TryConsumeCRLF() return result; } } + public bool TryConsume(int count) { if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); @@ -102,7 +104,8 @@ public bool TryConsume(int count) // consume all of this span _totalConsumed += available; count -= available; - } while (FetchNextSegment()); + } + while (FetchNextSegment()); return false; } @@ -127,13 +130,18 @@ public ReadOnlySequence ConsumeAsBuffer(int count) if (!TryConsumeAsBuffer(count, out var buffer)) throw new EndOfStreamException(); return buffer; } + public ReadOnlySequence ConsumeToEnd() { var from = SnapshotPosition(); var result = _buffer.Slice(from); - while (FetchNextSegment()) { } // consume all + while (FetchNextSegment()) + { + // consume all + } return result; } + public bool TryConsumeAsBuffer(int count, out ReadOnlySequence buffer) { var from = SnapshotPosition(); @@ -146,6 +154,7 @@ public bool TryConsumeAsBuffer(int count, out ReadOnlySequence buffer) buffer = _buffer.Slice(from, to); return true; } + public void Consume(int count) { if (!TryConsume(count)) throw new EndOfStreamException(); @@ -163,13 +172,14 @@ public void Consume(int count) if (found >= 0) return totalSkipped + found; totalSkipped += span.Length; - } while (reader.FetchNextSegment()); + } + while (reader.FetchNextSegment()); return -1; } + internal static int FindNextCrLf(BufferReader reader) // very deliberately not ref; want snapshot { // is it in the current span? (we need to handle the offsets differently if so) - int totalSkipped = 0; bool haveTrailingCR = false; do diff --git a/src/StackExchange.Redis/ChannelMessageQueue.cs b/src/StackExchange.Redis/ChannelMessageQueue.cs index 3bf7635f3..e58fb393b 100644 --- a/src/StackExchange.Redis/ChannelMessageQueue.cs +++ b/src/StackExchange.Redis/ChannelMessageQueue.cs @@ -52,12 +52,12 @@ internal ChannelMessage(ChannelMessageQueue queue, in RedisChannel channel, in R public RedisValue Message { get; } /// - /// Checks if 2 messages are .Equal() + /// Checks if 2 messages are .Equal(). /// public static bool operator ==(ChannelMessage left, ChannelMessage right) => left.Equals(right); /// - /// Checks if 2 messages are not .Equal() + /// Checks if 2 messages are not .Equal(). /// public static bool operator !=(ChannelMessage left, ChannelMessage right) => !left.Equals(right); } @@ -72,6 +72,7 @@ internal ChannelMessage(ChannelMessageQueue queue, in RedisChannel channel, in R public sealed class ChannelMessageQueue : IAsyncEnumerable { private readonly Channel _queue; + /// /// The Channel that was subscribed for this queue. /// @@ -202,7 +203,8 @@ internal static void Combine(ref ChannelMessageQueue? head, ChannelMessageQueue { old = Volatile.Read(ref head); queue._next = old; - } while (Interlocked.CompareExchange(ref head, queue, old) != old); + } + while (Interlocked.CompareExchange(ref head, queue, old) != old); } } @@ -226,7 +228,8 @@ internal static void Remove(ref ChannelMessageQueue? head, ChannelMessageQueue q } bool found; - do // if we fail due to a conflict, re-do from start + // if we fail due to a conflict, re-do from start + do { var current = Volatile.Read(ref head); if (current == null) return; // no queue? nothing to do @@ -261,9 +264,11 @@ internal static void Remove(ref ChannelMessageQueue? head, ChannelMessageQueue q } previous = current; current = Volatile.Read(ref previous!._next); - } while (current != null); + } + while (current != null); } - } while (found); + } + while (found); } internal static int Count(ref ChannelMessageQueue? head) diff --git a/src/StackExchange.Redis/ClientInfo.cs b/src/StackExchange.Redis/ClientInfo.cs index 215403fe8..f04058495 100644 --- a/src/StackExchange.Redis/ClientInfo.cs +++ b/src/StackExchange.Redis/ClientInfo.cs @@ -156,7 +156,7 @@ public sealed class ClientInfo /// /// A unique 64-bit client ID (introduced in Redis 2.8.12). /// - public long Id { get;private set; } + public long Id { get; private set; } /// /// Format the object as a string. @@ -217,7 +217,7 @@ internal static bool TryParse(string? input, [NotNullWhen(true)] out ClientInfo[ { var client = new ClientInfo { - Raw = line + Raw = line, }; string[] tokens = line.Split(StringSplits.Space); for (int i = 0; i < tokens.Length; i++) @@ -285,7 +285,7 @@ private class ClientInfoProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch(result.Resp2TypeBulkString) + switch (result.Resp2TypeBulkString) { case ResultType.BulkString: var raw = result.GetString(); diff --git a/src/StackExchange.Redis/ClusterConfiguration.cs b/src/StackExchange.Redis/ClusterConfiguration.cs index 83ab19501..0ce256c95 100644 --- a/src/StackExchange.Redis/ClusterConfiguration.cs +++ b/src/StackExchange.Redis/ClusterConfiguration.cs @@ -247,7 +247,7 @@ internal ClusterNode? this[string nodeId] /// The slot ID to get a node by. public ClusterNode? GetBySlot(int slot) { - foreach(var node in Nodes) + foreach (var node in Nodes) { if (!node.IsReplica && node.ServesSlot(slot)) return node; } @@ -265,7 +265,7 @@ internal ClusterNode? this[string nodeId] /// Represents the configuration of a single node in a cluster configuration. /// /// - public sealed class ClusterNode : IEquatable, IComparable, IComparable + public sealed class ClusterNode : IEquatable, IComparable, IComparable { private readonly ClusterConfiguration configuration; private IList? children; @@ -421,7 +421,8 @@ public int CompareTo(ClusterNode? other) if (IsReplica != other.IsReplica) return IsReplica ? 1 : -1; // primaries first - if (IsReplica) // both replicas? compare by parent, so we get primaries A, B, C and then replicas of A, B, C + // both replicas? compare by parent, so we get primaries A, B, C and then replicas of A, B, C + if (IsReplica) { int i = string.CompareOrdinal(ParentNodeId, other.ParentNodeId); if (i != 0) return i; @@ -457,16 +458,16 @@ public override string ToString() if (Parent is ClusterNode parent) sb.Append(" at ").Append(parent.EndPoint); } var childCount = Children.Count; - switch(childCount) + switch (childCount) { case 0: break; case 1: sb.Append(", 1 replica"); break; default: sb.Append(", ").Append(childCount).Append(" replicas"); break; } - if(Slots.Count != 0) + if (Slots.Count != 0) { sb.Append(", slots: "); - foreach(var slot in Slots) + foreach (var slot in Slots) { sb.Append(slot).Append(' '); } diff --git a/src/StackExchange.Redis/CommandBytes.cs b/src/StackExchange.Redis/CommandBytes.cs index d9c96a3ab..19a69549b 100644 --- a/src/StackExchange.Redis/CommandBytes.cs +++ b/src/StackExchange.Redis/CommandBytes.cs @@ -54,7 +54,6 @@ public override int GetHashCode() public bool Equals(in CommandBytes other) => _0 == other._0 && _1 == other._1 && _2 == other._2 && _3 == other._3; // note: don't add == operators; with the implicit op above, that invalidates "==null" compiler checks (which should report a failure!) - public static implicit operator CommandBytes(string value) => new CommandBytes(value); public override unsafe string ToString() @@ -181,10 +180,16 @@ private static unsafe int UpperCasifyUnicode(int oldLen, byte* bPtr) char* workspace = stackalloc char[MaxChars]; int charCount = Encoding.GetChars(bPtr, oldLen, workspace, MaxChars); char* c = workspace; - for (int i = 0; i < charCount; i++) *c = char.ToUpperInvariant(*c++); + for (int i = 0; i < charCount; i++) + { + *c = char.ToUpperInvariant(*c++); + } int newLen = Encoding.GetBytes(workspace, charCount, bPtr, MaxLength); // don't forget to zero any shrink - for (int i = newLen; i < oldLen; i++) bPtr[i] = 0; + for (int i = newLen; i < oldLen; i++) + { + bPtr[i] = 0; + } return newLen; } diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index 0a42d3e34..67b6f1a9e 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -41,7 +41,7 @@ public sealed class CommandMap RedisCommand.BGREWRITEAOF, RedisCommand.BGSAVE, RedisCommand.CLIENT, RedisCommand.CLUSTER, RedisCommand.CONFIG, RedisCommand.DBSIZE, RedisCommand.DEBUG, RedisCommand.FLUSHALL, RedisCommand.FLUSHDB, RedisCommand.INFO, RedisCommand.LASTSAVE, RedisCommand.MONITOR, RedisCommand.REPLICAOF, - RedisCommand.SAVE, RedisCommand.SHUTDOWN, RedisCommand.SLAVEOF, RedisCommand.SLOWLOG, RedisCommand.SYNC, RedisCommand.TIME + RedisCommand.SAVE, RedisCommand.SHUTDOWN, RedisCommand.SLAVEOF, RedisCommand.SLOWLOG, RedisCommand.SYNC, RedisCommand.TIME, }); /// @@ -80,20 +80,27 @@ public sealed class CommandMap /// The commands available to SSDB. /// /// - public static CommandMap SSDB { get; } = Create(new HashSet { - "ping", - "get", "set", "del", "incr", "incrby", "mget", "mset", "keys", "getset", "setnx", - "hget", "hset", "hdel", "hincrby", "hkeys", "hvals", "hmget", "hmset", "hlen", - "zscore", "zadd", "zrem", "zrange", "zrangebyscore", "zincrby", "zdecrby", "zcard", - "llen", "lpush", "rpush", "lpop", "rpop", "lrange", "lindex" - }, true); + public static CommandMap SSDB { get; } = Create( + new HashSet + { + "ping", + "get", "set", "del", "incr", "incrby", "mget", "mset", "keys", "getset", "setnx", + "hget", "hset", "hdel", "hincrby", "hkeys", "hvals", "hmget", "hmset", "hlen", + "zscore", "zadd", "zrem", "zrange", "zrangebyscore", "zincrby", "zdecrby", "zcard", + "llen", "lpush", "rpush", "lpop", "rpop", "lrange", "lindex", + }, + true); /// /// The commands available to Sentinel. /// /// - public static CommandMap Sentinel { get; } = Create(new HashSet { - "auth", "hello", "ping", "info", "role", "sentinel", "subscribe", "shutdown", "psubscribe", "unsubscribe", "punsubscribe" }, true); + public static CommandMap Sentinel { get; } = Create( + new HashSet + { + "auth", "hello", "ping", "info", "role", "sentinel", "subscribe", "shutdown", "psubscribe", "unsubscribe", "punsubscribe", + }, + true); /// /// Create a new , customizing some commands. @@ -195,8 +202,9 @@ internal void AssertAvailable(RedisCommand command) internal CommandBytes GetBytes(string command) { if (command == null) return default; - if(Enum.TryParse(command, true, out RedisCommand cmd)) - { // we know that one! + if (Enum.TryParse(command, true, out RedisCommand cmd)) + { + // we know that one! return map[(int)cmd]; } return new CommandBytes(command); diff --git a/src/StackExchange.Redis/CommandTrace.cs b/src/StackExchange.Redis/CommandTrace.cs index 061f252b9..aedd05fea 100644 --- a/src/StackExchange.Redis/CommandTrace.cs +++ b/src/StackExchange.Redis/CommandTrace.cs @@ -14,7 +14,7 @@ internal CommandTrace(long uniqueId, long time, long duration, RedisValue[] argu UniqueId = uniqueId; Time = RedisBase.UnixEpoch.AddSeconds(time); // duration = The amount of time needed for its execution, in microseconds. - // A tick is equal to 100 nanoseconds, or one ten-millionth of a second. + // A tick is equal to 100 nanoseconds, or one ten-millionth of a second. // So 1 microsecond = 10 ticks Duration = TimeSpan.FromTicks(duration * 10); Arguments = arguments; @@ -42,7 +42,7 @@ internal CommandTrace(long uniqueId, long time, long duration, RedisValue[] argu public long UniqueId { get; } /// - /// Deduces a link to the redis documentation about the specified command + /// Deduces a link to the redis documentation about the specified command. /// public string? GetHelpUrl() { @@ -73,18 +73,18 @@ private class CommandTraceProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch(result.Resp2TypeArray) + switch (result.Resp2TypeArray) { case ResultType.Array: var parts = result.GetItems(); CommandTrace[] arr = new CommandTrace[parts.Length]; int i = 0; - foreach(var item in parts) + foreach (var item in parts) { var subParts = item.GetItems(); if (!subParts[0].TryGetInt64(out long uniqueid) || !subParts[1].TryGetInt64(out long time) || !subParts[2].TryGetInt64(out long duration)) return false; - arr[i++] = new CommandTrace(uniqueid, time, duration, subParts[3].GetItemsAsValues()!); + arr[i++] = new CommandTrace(uniqueid, time, duration, subParts[3].GetItemsAsValues()!); } SetResult(message, arr); return true; diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs index 0dcccf59c..308c87c11 100644 --- a/src/StackExchange.Redis/Condition.cs +++ b/src/StackExchange.Redis/Condition.cs @@ -483,7 +483,7 @@ internal override bool TryValidate(in RawResult result, out bool value) { case RedisType.SortedSet: var parsedValue = result.AsRedisValue(); - value = (parsedValue.IsNull != expectedResult); + value = parsedValue.IsNull != expectedResult; ConnectionMultiplexer.TraceWithoutContext("exists: " + parsedValue + "; expected: " + expectedResult + "; voting: " + value); return true; @@ -633,7 +633,7 @@ internal override bool TryValidate(in RawResult result, out bool value) } else { - value = (parsed.IsNull != expectedResult); + value = parsed.IsNull != expectedResult; ConnectionMultiplexer.TraceWithoutContext("exists: " + parsed + "; expected: " + expectedResult + "; voting: " + value); } return true; diff --git a/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs b/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs index e66b0b210..6e38e15a9 100644 --- a/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs @@ -1,7 +1,7 @@ -using StackExchange.Redis.Maintenance; -using System; +using System; using System.Net; using System.Threading.Tasks; +using StackExchange.Redis.Maintenance; namespace StackExchange.Redis.Configuration { diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index 359b5f5f6..703adbcac 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -31,7 +31,7 @@ public class DefaultOptionsProvider /// /// The current list of providers to match (potentially modified from defaults via . /// - private static LinkedList KnownProviders { get; set; } = new (BuiltInProviders); + private static LinkedList KnownProviders { get; set; } = new(BuiltInProviders); /// /// Adds a provider to match endpoints against. The last provider added has the highest priority. @@ -143,9 +143,9 @@ public static DefaultOptionsProvider GetProvider(EndPoint endpoint) /// /// Controls how often the connection heartbeats. A heartbeat includes: - /// - Evaluating if any messages have timed out - /// - Evaluating connection status (checking for failures) - /// - Sending a server message to keep the connection alive if needed + /// - Evaluating if any messages have timed out. + /// - Evaluating connection status (checking for failures). + /// - Sending a server message to keep the connection alive if needed. /// /// Be aware setting this very low incurs additional overhead of evaluating the above more often. public virtual TimeSpan HeartbeatInterval => TimeSpan.FromSeconds(1); @@ -157,12 +157,12 @@ public static DefaultOptionsProvider GetProvider(EndPoint endpoint) public virtual bool HeartbeatConsistencyChecks => false; /// - /// Should exceptions include identifiable details? (key names, additional .Data annotations) + /// Whether exceptions include identifiable details (key names, additional .Data annotations). /// public virtual bool IncludeDetailInExceptions => true; /// - /// Should exceptions include performance counter details? + /// Whether exceptions include performance counter details. /// /// /// CPU usage, etc - note that this can be problematic on some platforms. @@ -223,6 +223,7 @@ public static DefaultOptionsProvider GetProvider(EndPoint endpoint) // We memoize this to reduce cost on re-access private string? defaultClientName; + /// /// The default client name for a connection, with the library version appended. /// @@ -253,7 +254,7 @@ protected virtual string GetDefaultClientName() => protected static string ComputerName => Environment.MachineName ?? Environment.GetEnvironmentVariable("ComputerName") ?? "Unknown"; /// - /// Whether to identify the client by library name/version when possible + /// Whether to identify the client by library name/version when possible. /// public virtual bool SetClientLibrary => true; @@ -293,7 +294,7 @@ protected virtual string GetDefaultClientName() => } catch (Exception) { - //silently ignores the exception + // Silently ignores the exception roleInstanceId = null; } return roleInstanceId; diff --git a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs index a30dedf85..987d2075c 100644 --- a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs +++ b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs @@ -1,6 +1,4 @@ -using Pipelines.Sockets.Unofficial; -using Pipelines.Sockets.Unofficial.Arenas; -using System; +using System; using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -12,12 +10,14 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Pipelines.Sockets.Unofficial; +using Pipelines.Sockets.Unofficial.Arenas; using static StackExchange.Redis.PhysicalConnection; namespace StackExchange.Redis.Configuration; /// -/// Captures redis traffic; intended for debug use +/// Captures redis traffic; intended for debug use. /// [Obsolete("This API is experimental, has security and performance implications, and may change without notice", false)] [SuppressMessage("ApiDesign", "RS0016:Add public types and members to the declared API", Justification = "Experimental API")] @@ -28,7 +28,7 @@ public abstract class LoggingTunnel : Tunnel private readonly Tunnel? _tail; /// - /// Replay the RESP messages for a pair of streams, invoking a callback per operation + /// Replay the RESP messages for a pair of streams, invoking a callback per operation. /// public static async Task ReplayAsync(Stream @out, Stream @in, Action pair) { @@ -51,7 +51,8 @@ public static async Task ReplayAsync(Stream @out, Stream @in, Action ReplayAsync(Stream @out, Stream @in, Action - /// Replay the RESP messages all the streams in a folder, invoking a callback per operation + /// Replay the RESP messages all the streams in a folder, invoking a callback per operation. /// /// The directory of captured files to replay. /// Operation to perform per replayed message pair. @@ -249,15 +249,14 @@ private static ContextualRedisResult ProcessBuffer(Arena arena, ref R static bool IsArrayOutOfBand(in RawResult result) { var items = result.GetItems(); - return (items.Length >= 3 && items[0].IsEqual(message) || items[0].IsEqual(smessage)) + return (items.Length >= 3 && (items[0].IsEqual(message) || items[0].IsEqual(smessage))) || (items.Length >= 4 && items[0].IsEqual(pmessage)); - } } private static readonly CommandBytes message = "message", pmessage = "pmessage", smessage = "smessage"; /// - /// Create a new instance of a + /// Create a new instance of a . /// protected LoggingTunnel(ConfigurationOptions? options = null, Tunnel? tail = null) { @@ -324,7 +323,7 @@ protected override Stream Log(Stream stream, EndPoint endpoint, ConnectionType c } /// - /// Perform logging on the provided stream + /// Perform logging on the provided stream. /// protected abstract Stream Log(Stream stream, EndPoint endpoint, ConnectionType connectionType); @@ -353,10 +352,12 @@ private async Task TlsHandshakeAsync(Stream stream, EndPoint endpoint) host = Format.ToStringHostOnly(endpoint); } - var ssl = new SslStream(stream, false, - _options.CertificateValidationCallback ?? PhysicalConnection.GetAmbientIssuerCertificateCallback(), - _options.CertificateSelectionCallback ?? PhysicalConnection.GetAmbientClientCertificateCallback(), - EncryptionPolicy.RequireEncryption); + var ssl = new SslStream( + innerStream: stream, + leaveInnerStreamOpen: false, + userCertificateValidationCallback: _options.CertificateValidationCallback ?? PhysicalConnection.GetAmbientIssuerCertificateCallback(), + userCertificateSelectionCallback: _options.CertificateSelectionCallback ?? PhysicalConnection.GetAmbientClientCertificateCallback(), + encryptionPolicy: EncryptionPolicy.RequireEncryption); #if NETCOREAPP3_1_OR_GREATER var configOptions = _options.SslClientAuthenticationOptions?.Invoke(host); @@ -375,7 +376,7 @@ private async Task TlsHandshakeAsync(Stream stream, EndPoint endpoint) } /// - /// Get a typical text representation of a redis command + /// Get a typical text representation of a redis command. /// public static string DefaultFormatCommand(RedisResult value) { @@ -402,7 +403,7 @@ public static string DefaultFormatCommand(RedisResult value) return sb.ToString(); } } - catch {} + catch { } return value.Type.ToString(); static bool IsSimple(RedisResult value) @@ -433,7 +434,7 @@ static bool IsSimple(RedisResult value) } /// - /// Get a typical text representation of a redis response + /// Get a typical text representation of a redis response. /// public static string DefaultFormatResponse(RedisResult value) { diff --git a/src/StackExchange.Redis/Configuration/Tunnel.cs b/src/StackExchange.Redis/Configuration/Tunnel.cs index 15c9abd15..beebff2dc 100644 --- a/src/StackExchange.Redis/Configuration/Tunnel.cs +++ b/src/StackExchange.Redis/Configuration/Tunnel.cs @@ -52,8 +52,7 @@ private sealed class HttpProxyTunnel : Tunnel const string Prefix = "CONNECT ", Suffix = " HTTP/1.1\r\n\r\n", ExpectedResponse1 = "HTTP/1.1 200 OK\r\n\r\n", ExpectedResponse2 = "HTTP/1.1 200 Connection established\r\n\r\n"; byte[] chunk = ArrayPool.Shared.Rent(Math.Max( encoding.GetByteCount(Prefix) + encoding.GetByteCount(ep) + encoding.GetByteCount(Suffix), - Math.Max(encoding.GetByteCount(ExpectedResponse1), encoding.GetByteCount(ExpectedResponse2)) - )); + Math.Max(encoding.GetByteCount(ExpectedResponse1), encoding.GetByteCount(ExpectedResponse2)))); var offset = 0; offset += encoding.GetBytes(Prefix, 0, Prefix.Length, chunk, offset); offset += encoding.GetBytes(ep, 0, ep.Length, chunk, offset); diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 4f3ff1287..e972962b2 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -63,7 +63,7 @@ internal static Proxy ParseProxy(string key, string value) internal static SslProtocols ParseSslProtocols(string key, string? value) { - //Flags expect commas as separators, but we need to use '|' since commas are already used in the connection string to mean something else + // Flags expect commas as separators, but we need to use '|' since commas are already used in the connection string to mean something else value = value?.Replace("|", ","); if (!Enum.TryParse(value, true, out SslProtocols tmp)) throw new ArgumentOutOfRangeException(key, $"Keyword '{key}' requires an SslProtocol value (multiple values separated by '|'); the value '{value}' is not recognised."); @@ -182,14 +182,14 @@ public static string TryNormalize(string value) /// A LocalCertificateSelectionCallback delegate responsible for selecting the certificate used for authentication; note /// that this cannot be specified in the configuration-string. /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly", Justification = "Existing compatibility")] public event LocalCertificateSelectionCallback? CertificateSelection; /// /// A RemoteCertificateValidationCallback delegate responsible for validating the certificate supplied by the remote party; note /// that this cannot be specified in the configuration-string. /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly", Justification = "Existing compatibility")] public event RemoteCertificateValidationCallback? CertificateValidation; /// @@ -238,7 +238,7 @@ public int AsyncTimeout } /// - /// Indicates whether the connection should be encrypted + /// Indicates whether the connection should be encrypted. /// [Obsolete("Please use .Ssl instead of .UseSsl, will be removed in 3.0."), Browsable(false), @@ -258,7 +258,6 @@ public bool SetClientLibrary set => setClientLibrary = value; } - /// /// Gets or sets the library name to use for CLIENT SETINFO lib-name calls to Redis during handshake. /// Defaults to "SE.Redis". @@ -477,9 +476,9 @@ public bool HeartbeatConsistencyChecks /// /// Controls how often the connection heartbeats. A heartbeat includes: - /// - Evaluating if any messages have timed out - /// - Evaluating connection status (checking for failures) - /// - Sending a server message to keep the connection alive if needed + /// - Evaluating if any messages have timed out. + /// - Evaluating connection status (checking for failures). + /// - Sending a server message to keep the connection alive if needed. /// /// /// This defaults to 1000 milliseconds and should not be changed for most use cases. @@ -506,7 +505,7 @@ public bool HighPrioritySocketThreads } /// - /// Should exceptions include identifiable details? (key names, additional .Data annotations) + /// Whether exceptions include identifiable details (key names, additional .Data annotations). /// public bool IncludeDetailInExceptions { @@ -515,7 +514,7 @@ public bool IncludeDetailInExceptions } /// - /// Should exceptions include performance counter details? + /// Whether exceptions include performance counter details. /// /// /// CPU usage, etc - note that this can be problematic on some platforms. @@ -528,7 +527,7 @@ public bool IncludePerformanceCountersInExceptions /// /// Specifies the time in seconds at which connections should be pinged to ensure validity. - /// -1 Defaults to 60 Seconds + /// -1 Defaults to 60 Seconds. /// public int KeepAlive { @@ -1107,7 +1106,7 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown) public Tunnel? Tunnel { get; set; } /// - /// Specify the redis protocol type + /// Specify the redis protocol type. /// public RedisProtocol? Protocol { get; set; } @@ -1115,12 +1114,12 @@ internal bool TryResp3() { // note: deliberately leaving the IsAvailable duplicated to use short-circuit - //if (Protocol is null) - //{ - // // if not specified, lean on the server version and whether HELLO is available - // return new RedisFeatures(DefaultVersion).Resp3 && CommandMap.IsAvailable(RedisCommand.HELLO); - //} - //else + // if (Protocol is null) + // { + // // if not specified, lean on the server version and whether HELLO is available + // return new RedisFeatures(DefaultVersion).Resp3 && CommandMap.IsAvailable(RedisCommand.HELLO); + // } + // else // ^^^ left for context; originally our intention was to auto-enable RESP3 by default *if* the server version // is >= 6; however, it turns out (see extensive conversation here https://github.com/StackExchange/StackExchange.Redis/pull/2396) // that tangential undocumented API breaks were made at the same time; this means that even if we fix every diff --git a/src/StackExchange.Redis/ConnectionCounters.cs b/src/StackExchange.Redis/ConnectionCounters.cs index 5be2ae488..546e2eff5 100644 --- a/src/StackExchange.Redis/ConnectionCounters.cs +++ b/src/StackExchange.Redis/ConnectionCounters.cs @@ -71,7 +71,7 @@ internal ConnectionCounters(ConnectionType connectionType) /// /// The number of subscriptions (with and without patterns) currently held against this connection. /// - public long Subscriptions { get;internal set; } + public long Subscriptions { get; internal set; } /// /// Indicates the total number of outstanding items against this connection. diff --git a/src/StackExchange.Redis/ConnectionFailedEventArgs.cs b/src/StackExchange.Redis/ConnectionFailedEventArgs.cs index 01f9ff408..5d165add1 100644 --- a/src/StackExchange.Redis/ConnectionFailedEventArgs.cs +++ b/src/StackExchange.Redis/ConnectionFailedEventArgs.cs @@ -32,7 +32,7 @@ internal ConnectionFailedEventArgs(EventHandler? hand /// The exception that occurred. /// Connection physical name. public ConnectionFailedEventArgs(object sender, EndPoint endPoint, ConnectionType connectionType, ConnectionFailureType failureType, Exception exception, string physicalName) - : this (null, sender, endPoint, connectionType, failureType, exception, physicalName) + : this(null, sender, endPoint, connectionType, failureType, exception, physicalName) { } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.ExportConfiguration.cs b/src/StackExchange.Redis/ConnectionMultiplexer.ExportConfiguration.cs index 55c8deefb..c095ffd53 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.ExportConfiguration.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.ExportConfiguration.cs @@ -98,7 +98,7 @@ public void ExportConfiguration(Stream destination, ExportOptions options = Expo } } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "We're not double disposing.")] private static void Write(ZipArchive zip, string name, Task task, Action callback) { var entry = zip.CreateEntry(name, CompressionLevel.Optimal); diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.FeatureFlags.cs b/src/StackExchange.Redis/ConnectionMultiplexer.FeatureFlags.cs index a6c2168f6..975da5de1 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.FeatureFlags.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.FeatureFlags.cs @@ -19,7 +19,8 @@ private static void SetAutodetectFeatureFlags() { bool value = false; try - { // attempt to detect a known problem scenario + { + // attempt to detect a known problem scenario value = SynchronizationContext.Current?.GetType()?.Name == "LegacyAspNetSynchronizationContext"; } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs index b6ecbdf3f..c60966234 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs @@ -10,7 +10,7 @@ public partial class ConnectionMultiplexer /// /// Register a callback to provide an on-demand ambient session provider based on the /// calling context; the implementing code is responsible for reliably resolving the same provider - /// based on ambient context, or returning null to not profile + /// based on ambient context, or returning null to not profile. /// /// The session provider to register. public void RegisterProfiler(Func profilingSessionProvider) => _profilingSessionProvider = profilingSessionProvider; diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs index 238c32bda..7753954d0 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs @@ -1,12 +1,12 @@ -using Microsoft.Extensions.Logging; -using Pipelines.Sockets.Unofficial; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Pipelines.Sockets.Unofficial; namespace StackExchange.Redis; @@ -33,33 +33,36 @@ internal void InitializeSentinel(ILogger? log) if (sub.SubscribedEndpoint(RedisChannel.Literal("+switch-master")) == null) { - sub.Subscribe(RedisChannel.Literal("+switch-master"), (__, message) => - { - string[] messageParts = ((string)message!).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - // We don't care about the result of this - we're just trying - _ = Format.TryParseEndPoint(string.Format("{0}:{1}", messageParts[1], messageParts[2]), out var switchBlame); - - lock (sentinelConnectionChildren) + sub.Subscribe( + RedisChannel.Literal("+switch-master"), + (__, message) => { - // Switch the primary if we have connections for that service - if (sentinelConnectionChildren.ContainsKey(messageParts[0])) - { - ConnectionMultiplexer child = sentinelConnectionChildren[messageParts[0]]; + string[] messageParts = ((string)message!).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + // We don't care about the result of this - we're just trying + _ = Format.TryParseEndPoint(string.Format("{0}:{1}", messageParts[1], messageParts[2]), out var switchBlame); - // Is the connection still valid? - if (child.IsDisposed) - { - child.ConnectionFailed -= OnManagedConnectionFailed; - child.ConnectionRestored -= OnManagedConnectionRestored; - sentinelConnectionChildren.Remove(messageParts[0]); - } - else + lock (sentinelConnectionChildren) + { + // Switch the primary if we have connections for that service + if (sentinelConnectionChildren.ContainsKey(messageParts[0])) { - SwitchPrimary(switchBlame, sentinelConnectionChildren[messageParts[0]]); + ConnectionMultiplexer child = sentinelConnectionChildren[messageParts[0]]; + + // Is the connection still valid? + if (child.IsDisposed) + { + child.ConnectionFailed -= OnManagedConnectionFailed; + child.ConnectionRestored -= OnManagedConnectionRestored; + sentinelConnectionChildren.Remove(messageParts[0]); + } + else + { + SwitchPrimary(switchBlame, sentinelConnectionChildren[messageParts[0]]); + } } } - } - }, CommandFlags.FireAndForget); + }, + CommandFlags.FireAndForget); } // If we lose connection to a sentinel server, @@ -71,11 +74,14 @@ internal void InitializeSentinel(ILogger? log) // Subscribe to new sentinels being added if (sub.SubscribedEndpoint(RedisChannel.Literal("+sentinel")) == null) { - sub.Subscribe(RedisChannel.Literal("+sentinel"), (_, message) => - { - string[] messageParts = ((string)message!).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - UpdateSentinelAddressList(messageParts[0]); - }, CommandFlags.FireAndForget); + sub.Subscribe( + RedisChannel.Literal("+sentinel"), + (_, message) => + { + string[] messageParts = ((string)message!).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + UpdateSentinelAddressList(messageParts[0]); + }, + CommandFlags.FireAndForget); } } @@ -164,7 +170,8 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co { if (ServerSelectionStrategy.ServerType != ServerType.Sentinel) { - throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, + throw new RedisConnectionException( + ConnectionFailureType.UnableToConnect, "Sentinel: The ConnectionMultiplexer is not a Sentinel connection. Detected as: " + ServerSelectionStrategy.ServerType); } @@ -198,7 +205,8 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co if (newPrimaryEndPoint is null) { - throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, + throw new RedisConnectionException( + ConnectionFailureType.UnableToConnect, $"Sentinel: Failed connecting to configured primary for service: {config.ServiceName}"); } @@ -241,11 +249,13 @@ public ConnectionMultiplexer GetSentinelMasterConnection(ConfigurationOptions co } Thread.Sleep(100); - } while (sw.ElapsedMilliseconds < config.ConnectTimeout); + } + while (sw.ElapsedMilliseconds < config.ConnectTimeout); if (!success) { - throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, + throw new RedisConnectionException( + ConnectionFailureType.UnableToConnect, $"Sentinel: Failed connecting to configured primary for service: {config.ServiceName}"); } @@ -323,29 +333,33 @@ internal void OnManagedConnectionFailed(object? sender, ConnectionFailedEventArg // or if we miss the published primary change. if (connection.sentinelPrimaryReconnectTimer == null) { - connection.sentinelPrimaryReconnectTimer = new Timer(_ => - { - try - { - // Attempt, but do not fail here - SwitchPrimary(e.EndPoint, connection); - } - catch (Exception) - { - } - finally + connection.sentinelPrimaryReconnectTimer = new Timer( + _ => { try { - connection.sentinelPrimaryReconnectTimer?.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan); + // Attempt, but do not fail here + SwitchPrimary(e.EndPoint, connection); } - catch (ObjectDisposedException) + catch (Exception) { - // If we get here the managed connection was restored and the timer was - // disposed by another thread, so there's no need to run the timer again. } - } - }, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan); + finally + { + try + { + connection.sentinelPrimaryReconnectTimer?.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan); + } + catch (ObjectDisposedException) + { + // If we get here the managed connection was restored and the timer was + // disposed by another thread, so there's no need to run the timer again. + } + } + }, + null, + TimeSpan.Zero, + Timeout.InfiniteTimeSpan); } } @@ -389,8 +403,7 @@ internal void SwitchPrimary(EndPoint? switchBlame, ConnectionMultiplexer connect // Get new primary - try twice EndPoint newPrimaryEndPoint = GetConfiguredPrimaryForService(serviceName) ?? GetConfiguredPrimaryForService(serviceName) - ?? throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, - $"Sentinel: Failed connecting to switch primary for service: {serviceName}"); + ?? throw new RedisConnectionException(ConnectionFailureType.UnableToConnect, $"Sentinel: Failed connecting to switch primary for service: {serviceName}"); connection.currentSentinelPrimaryEndPoint = newPrimaryEndPoint; @@ -411,8 +424,14 @@ internal void SwitchPrimary(EndPoint? switchBlame, ConnectionMultiplexer connect } Trace($"Switching primary to {newPrimaryEndPoint}"); // Trigger a reconfigure - connection.ReconfigureAsync(first: false, reconfigureAll: false, logger, switchBlame, - $"Primary switch {serviceName}", false, CommandFlags.PreferMaster).Wait(); + connection.ReconfigureAsync( + first: false, + reconfigureAll: false, + log: logger, + blame: switchBlame, + cause: $"Primary switch {serviceName}", + publishReconfigure: false, + publishReconfigureFlags: CommandFlags.PreferMaster).Wait(); UpdateSentinelAddressList(serviceName); } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.StormLog.cs b/src/StackExchange.Redis/ConnectionMultiplexer.StormLog.cs index a32687b5d..51c62d00e 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.StormLog.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.StormLog.cs @@ -6,6 +6,7 @@ public partial class ConnectionMultiplexer { internal int haveStormLog = 0; internal string? stormLogSnapshot; + /// /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time). /// Set to a negative value to disable this feature. diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 022ae8cd9..8a4b8733d 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1,7 +1,4 @@ -using Microsoft.Extensions.Logging; -using Pipelines.Sockets.Unofficial; -using StackExchange.Redis.Profiling; -using System; +using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; @@ -14,6 +11,9 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Pipelines.Sockets.Unofficial; +using StackExchange.Redis.Profiling; namespace StackExchange.Redis { @@ -70,9 +70,7 @@ pulse is null internal static long LastGlobalHeartbeatSecondsAgo => unchecked(Environment.TickCount - Thread.VolatileRead(ref lastGlobalHeartbeatTicks)) / 1000; - /// - /// Should exceptions include identifiable details? (key names, additional .Data annotations) - /// + /// [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public bool IncludeDetailInExceptions @@ -81,12 +79,7 @@ public bool IncludeDetailInExceptions set => RawConfig.IncludeDetailInExceptions = value; } - /// - /// Should exceptions include performance counter details? - /// - /// - /// CPU usage, etc - note that this can be problematic on some platforms. - /// + /// [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludePerformanceCountersInExceptions)} instead - this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] public bool IncludePerformanceCountersInExceptions @@ -858,7 +851,7 @@ public bool Any(Func? predicate = null) public ServerSnapshotFiltered Where(CommandFlags flags) { var effectiveFlags = flags & (CommandFlags.DemandMaster | CommandFlags.DemandReplica); - return (effectiveFlags) switch + return effectiveFlags switch { CommandFlags.DemandMaster => Where(static s => !s.IsReplica), CommandFlags.DemandReplica => Where(static s => s.IsReplica), @@ -1021,7 +1014,6 @@ public void Dispose() // outstanding messages; if the consumer has dropped the multiplexer, then // there will be no new incoming messages, and after timeouts: everything // should drop. - public void Root(ConnectionMultiplexer multiplexer) { lock (StrongRefSyncLock) @@ -1074,7 +1066,8 @@ private void OnHeartbeat() } } if (isRooted && !hasPendingCallerFacingItems) - { // release the GC root on the heartbeat *if* the token still matches + { + // release the GC root on the heartbeat *if* the token still matches pulse?.UnRoot(token); } } @@ -1483,7 +1476,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog Trace("Testing: " + Format.ToString(endpoints[i])); var server = GetServerEndPoint(endpoints[i]); - //server.ReportNextFailure(); + // server.ReportNextFailure(); servers[i] = server; // This awaits either the endpoint's initial connection, or a tracer if we're already connected @@ -1695,8 +1688,9 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog ResetAllNonConnected(); log?.LogInformation($" Retrying - attempts left: {attemptsLeft}..."); } - //WTF("?: " + attempts); - } while (first && !healthy && attemptsLeft > 0); + // WTF("?: " + attempts); + } + while (first && !healthy && attemptsLeft > 0); if (first && RawConfig.AbortOnConnectFail && !healthy) { diff --git a/src/StackExchange.Redis/CursorEnumerable.cs b/src/StackExchange.Redis/CursorEnumerable.cs index efe2db61a..55d93d6a6 100644 --- a/src/StackExchange.Redis/CursorEnumerable.cs +++ b/src/StackExchange.Redis/CursorEnumerable.cs @@ -12,6 +12,7 @@ namespace StackExchange.Redis /// /// Provides the ability to iterate over a cursor-based sequence of redis data, synchronously or asynchronously. /// + /// The type of the data in the cursor. internal abstract class CursorEnumerable : IEnumerable, IScanningCursor, IAsyncEnumerable { private readonly RedisBase redis; @@ -91,7 +92,8 @@ internal Enumerator(CursorEnumerable parent, CancellationToken cancellationTo /// /// Gets the current value of the enumerator. /// - public T Current { + public T Current + { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { @@ -165,7 +167,7 @@ private void ProcessReply(in ScanResult result, bool isInitial) { _currentCursor = _nextCursor; _nextCursor = result.Cursor; - _pageOffset = isInitial ? parent.initialOffset - 1 : -1; + _pageOffset = isInitial ? parent.initialOffset - 1 : -1; Recycle(ref _pageOversized, ref _isPooled); // recycle any existing data _pageOversized = result.ValuesOversized ?? Array.Empty(); _isPooled = result.IsPooled; @@ -206,7 +208,7 @@ private protected TResult Wait(Task pending, Message message) /// public ValueTask MoveNextAsync() { - if(SimpleNext()) return new ValueTask(true); + if (SimpleNext()) return new ValueTask(true); return SlowNextAsync(); } @@ -274,7 +276,7 @@ private async ValueTask AwaitedNextAsync(bool isInitial) { scanResult = await pending.ForAwait(); } - catch(Exception ex) + catch (Exception ex) { TryAppendExceptionState(ex); throw; @@ -344,8 +346,8 @@ internal static CursorEnumerable From(RedisBase redis, ServerEndPoint? server private class SingleBlockEnumerable : CursorEnumerable { private readonly Task _pending; - public SingleBlockEnumerable(RedisBase redis, ServerEndPoint? server, - Task pending, int pageOffset) : base(redis, server, 0, int.MaxValue, 0, pageOffset, default) + public SingleBlockEnumerable(RedisBase redis, ServerEndPoint? server, Task pending, int pageOffset) + : base(redis, server, 0, int.MaxValue, 0, pageOffset, default) { _pending = pending; } diff --git a/src/StackExchange.Redis/EndPointCollection.cs b/src/StackExchange.Redis/EndPointCollection.cs index 44fd67e79..cf4c844c1 100644 --- a/src/StackExchange.Redis/EndPointCollection.cs +++ b/src/StackExchange.Redis/EndPointCollection.cs @@ -1,10 +1,10 @@ -using Microsoft.Extensions.Logging; -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Net; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; namespace StackExchange.Redis { @@ -23,13 +23,13 @@ private static class DefaultPorts /// /// Create a new . /// - public EndPointCollection() {} + public EndPointCollection() { } /// /// Create a new . /// /// The endpoints to add to the collection. - public EndPointCollection(IList endpoints) : base(endpoints) {} + public EndPointCollection(IList endpoints) : base(endpoints) { } /// /// Format an . @@ -165,7 +165,7 @@ internal void SetDefaultPorts(ServerType? serverType, bool ssl = false) IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - /// + /// public new IEnumerator GetEnumerator() { // this does *not* need to handle all threading scenarios; but we do diff --git a/src/StackExchange.Redis/EndPointEventArgs.cs b/src/StackExchange.Redis/EndPointEventArgs.cs index 5f8cc6b18..bef0db9b6 100644 --- a/src/StackExchange.Redis/EndPointEventArgs.cs +++ b/src/StackExchange.Redis/EndPointEventArgs.cs @@ -11,6 +11,7 @@ public class EndPointEventArgs : EventArgs, ICompletable { private readonly EventHandler? handler; private readonly object sender; + internal EndPointEventArgs(EventHandler? handler, object sender, EndPoint endpoint) { this.handler = handler; @@ -24,7 +25,7 @@ internal EndPointEventArgs(EventHandler? handler, object send /// The source of the event. /// Redis endpoint. public EndPointEventArgs(object sender, EndPoint endpoint) - : this (null, sender, endpoint) + : this(null, sender, endpoint) { } diff --git a/src/StackExchange.Redis/Enums/Aggregate.cs b/src/StackExchange.Redis/Enums/Aggregate.cs index 0c4d890fa..41e1d435d 100644 --- a/src/StackExchange.Redis/Enums/Aggregate.cs +++ b/src/StackExchange.Redis/Enums/Aggregate.cs @@ -9,13 +9,15 @@ public enum Aggregate /// The values of the combined elements are added. /// Sum, + /// /// The least value of the combined elements is used. /// Min, + /// /// The greatest value of the combined elements is used. /// - Max + Max, } } diff --git a/src/StackExchange.Redis/Enums/Bitwise.cs b/src/StackExchange.Redis/Enums/Bitwise.cs index ada2e99c5..b38423eac 100644 --- a/src/StackExchange.Redis/Enums/Bitwise.cs +++ b/src/StackExchange.Redis/Enums/Bitwise.cs @@ -9,14 +9,17 @@ public enum Bitwise /// And /// And, + /// /// Or /// Or, + /// /// Xor /// Xor, + /// /// Not /// diff --git a/src/StackExchange.Redis/Enums/ClientFlags.cs b/src/StackExchange.Redis/Enums/ClientFlags.cs index 50d32261b..eb687bba6 100644 --- a/src/StackExchange.Redis/Enums/ClientFlags.cs +++ b/src/StackExchange.Redis/Enums/ClientFlags.cs @@ -85,74 +85,91 @@ public enum ClientFlags : long /// No specific flag set. /// None = 0, + /// /// The client is a replica in MONITOR mode. /// [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicaMonitor) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] SlaveMonitor = 1, + /// /// The client is a replica in MONITOR mode. /// ReplicaMonitor = 1, // as an implementation detail, note that enum.ToString on [Flags] prefers *later* options when naming Flags + /// /// The client is a normal replica server. /// [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(Replica) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Slave = 2, + /// /// The client is a normal replica server. /// Replica = 2, // as an implementation detail, note that enum.ToString on [Flags] prefers *later* options when naming Flags + /// /// The client is a primary. /// Master = 4, + /// /// The client is in a MULTI/EXEC context. /// Transaction = 8, + /// /// The client is waiting in a blocking operation. /// Blocked = 16, + /// /// A watched keys has been modified - EXEC will fail. /// TransactionDoomed = 32, + /// /// Connection to be closed after writing entire reply. /// Closing = 64, + /// /// The client is unblocked. /// Unblocked = 128, + /// /// Connection to be closed ASAP. /// CloseASAP = 256, + /// /// The client is a Pub/Sub subscriber. /// PubSubSubscriber = 512, + /// /// The client is in readonly mode against a cluster node. /// ReadOnlyCluster = 1024, + /// /// The client is connected via a Unix domain socket. /// UnixDomainSocket = 2048, + /// /// The client enabled keys tracking in order to perform client side caching. /// KeysTracking = 4096, + /// /// The client tracking target client is invalid. /// TrackingTargetInvalid = 8192, + /// /// The client enabled broadcast tracking mode. /// diff --git a/src/StackExchange.Redis/Enums/ClientType.cs b/src/StackExchange.Redis/Enums/ClientType.cs index 498c7dd70..c2b003d9a 100644 --- a/src/StackExchange.Redis/Enums/ClientType.cs +++ b/src/StackExchange.Redis/Enums/ClientType.cs @@ -4,27 +4,30 @@ namespace StackExchange.Redis { /// - /// The class of the connection + /// The class of the connection. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "Compatibility")] public enum ClientType { /// - /// Regular connections, including MONITOR connections + /// Regular connections, including MONITOR connections. /// Normal = 0, + /// - /// Replication connections + /// Replication connections. /// Replica = 1, // as an implementation detail, note that enum.ToString without [Flags] prefers *earlier* values + /// - /// Replication connections + /// Replication connections. /// [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(Replica) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Slave = 1, + /// - /// Subscription connections + /// Subscription connections. /// PubSub = 2, } diff --git a/src/StackExchange.Redis/Enums/CommandFlags.cs b/src/StackExchange.Redis/Enums/CommandFlags.cs index 95e815a3a..bafaee70f 100644 --- a/src/StackExchange.Redis/Enums/CommandFlags.cs +++ b/src/StackExchange.Redis/Enums/CommandFlags.cs @@ -4,7 +4,7 @@ namespace StackExchange.Redis { /// - /// Behaviour markers associated with a given command + /// Behaviour markers associated with a given command. /// [Flags] [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "Compatibility")] @@ -16,11 +16,12 @@ public enum CommandFlags None = 0, /// - /// From 2.0, this flag is not used + /// From 2.0, this flag is not used. /// [Obsolete("From 2.0, this flag is not used, this will be removed in 3.0.", false)] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] HighPriority = 1, + /// /// The caller is not interested in the result; the caller will immediately receive a default-value /// of the expected return type (this value is not indicative of anything at the server). @@ -69,7 +70,7 @@ public enum CommandFlags // 32: used for "asking" flag; never user-specified, so not visible on the public API /// - /// Indicates that this operation should not be forwarded to other servers as a result of an ASK or MOVED response + /// Indicates that this operation should not be forwarded to other servers as a result of an ASK or MOVED response. /// NoRedirect = 64, @@ -78,7 +79,7 @@ public enum CommandFlags // 256: used for "script unavailable"; never user-specified, so not visible on the public API /// - /// Indicates that script-related operations should use EVAL, not SCRIPT LOAD + EVALSHA + /// Indicates that script-related operations should use EVAL, not SCRIPT LOAD + EVALSHA. /// NoScriptCache = 512, diff --git a/src/StackExchange.Redis/Enums/CommandStatus.cs b/src/StackExchange.Redis/Enums/CommandStatus.cs index 472c96dfc..c4de5753d 100644 --- a/src/StackExchange.Redis/Enums/CommandStatus.cs +++ b/src/StackExchange.Redis/Enums/CommandStatus.cs @@ -9,14 +9,17 @@ public enum CommandStatus /// Command status unknown. /// Unknown, + /// /// ConnectionMultiplexer has not yet started writing this command to Redis. /// WaitingToBeSent, + /// /// Command has been sent to Redis. /// Sent, + /// /// Command is in the backlog, waiting to be processed and written to Redis. /// diff --git a/src/StackExchange.Redis/Enums/ConnectionFailureType.cs b/src/StackExchange.Redis/Enums/ConnectionFailureType.cs index d9407b69e..55eeacef6 100644 --- a/src/StackExchange.Redis/Enums/ConnectionFailureType.cs +++ b/src/StackExchange.Redis/Enums/ConnectionFailureType.cs @@ -9,44 +9,54 @@ public enum ConnectionFailureType /// This event is not a failure. /// None, + /// /// No viable connections were available for this operation. /// UnableToResolvePhysicalConnection, + /// /// The socket for this connection failed. /// SocketFailure, + /// /// Either SSL Stream or Redis authentication failed. /// AuthenticationFailure, + /// /// An unexpected response was received from the server. /// ProtocolFailure, + /// /// An unknown internal error occurred. /// InternalFailure, + /// /// The socket was closed. /// SocketClosed, + /// /// The socket was closed. /// ConnectionDisposed, + /// /// The database is loading and is not available for use. /// Loading, + /// /// It has not been possible to create an initial connection to the redis server(s). /// UnableToConnect, + /// - /// High-integrity mode was enabled, and a failure was detected + /// High-integrity mode was enabled, and a failure was detected. /// ResponseIntegrityFailure, } diff --git a/src/StackExchange.Redis/Enums/ConnectionType.cs b/src/StackExchange.Redis/Enums/ConnectionType.cs index ead82f222..8db655c08 100644 --- a/src/StackExchange.Redis/Enums/ConnectionType.cs +++ b/src/StackExchange.Redis/Enums/ConnectionType.cs @@ -9,13 +9,15 @@ public enum ConnectionType /// Not connection-type related. /// None = 0, + /// /// An interactive connection handles request/response commands for accessing data on demand. /// Interactive, + /// /// A subscriber connection receives unsolicited messages from the server as pub/sub events occur. /// - Subscription + Subscription, } } diff --git a/src/StackExchange.Redis/Enums/Exclude.cs b/src/StackExchange.Redis/Enums/Exclude.cs index 4da2e9ef4..912a2af95 100644 --- a/src/StackExchange.Redis/Enums/Exclude.cs +++ b/src/StackExchange.Redis/Enums/Exclude.cs @@ -13,17 +13,20 @@ public enum Exclude /// Both start and stop are inclusive. /// None = 0, + /// /// Start is exclusive, stop is inclusive. /// Start = 1, + /// /// Start is inclusive, stop is exclusive. /// Stop = 2, + /// /// Both start and stop are exclusive. /// - Both = Start | Stop + Both = Start | Stop, } } diff --git a/src/StackExchange.Redis/Enums/ExpireWhen.cs b/src/StackExchange.Redis/Enums/ExpireWhen.cs index 0ed3782bc..2637e7625 100644 --- a/src/StackExchange.Redis/Enums/ExpireWhen.cs +++ b/src/StackExchange.Redis/Enums/ExpireWhen.cs @@ -11,20 +11,24 @@ public enum ExpireWhen /// Set expiry whether or not there is an existing expiry. /// Always, + /// /// Set expiry only when the new expiry is greater than current one. /// GreaterThanCurrentExpiry, + /// /// Set expiry only when the key has an existing expiry. /// HasExpiry, + /// /// Set expiry only when the key has no expiry. /// HasNoExpiry, + /// - /// Set expiry only when the new expiry is less than current one + /// Set expiry only when the new expiry is less than current one. /// LessThanCurrentExpiry, } @@ -37,6 +41,6 @@ internal static class ExpiryOptionExtensions ExpireWhen.HasExpiry => RedisLiterals.XX, ExpireWhen.GreaterThanCurrentExpiry => RedisLiterals.GT, ExpireWhen.LessThanCurrentExpiry => RedisLiterals.LT, - _ => throw new ArgumentOutOfRangeException(nameof(op)) + _ => throw new ArgumentOutOfRangeException(nameof(op)), }; } diff --git a/src/StackExchange.Redis/Enums/ExportOptions.cs b/src/StackExchange.Redis/Enums/ExportOptions.cs index fd29dd388..594651955 100644 --- a/src/StackExchange.Redis/Enums/ExportOptions.cs +++ b/src/StackExchange.Redis/Enums/ExportOptions.cs @@ -12,25 +12,30 @@ public enum ExportOptions /// No options. /// None = 0, + /// /// The output of INFO. /// Info = 1, + /// /// The output of CONFIG GET *. /// Config = 2, + /// /// The output of CLIENT LIST. /// Client = 4, + /// /// The output of CLUSTER NODES. /// Cluster = 8, + /// /// Everything available. /// - All = -1 + All = -1, } } diff --git a/src/StackExchange.Redis/Enums/GeoUnit.cs b/src/StackExchange.Redis/Enums/GeoUnit.cs index 3f5104742..99ab0a143 100644 --- a/src/StackExchange.Redis/Enums/GeoUnit.cs +++ b/src/StackExchange.Redis/Enums/GeoUnit.cs @@ -8,19 +8,22 @@ namespace StackExchange.Redis public enum GeoUnit { /// - /// Meters + /// Meters. /// Meters, + /// - /// Kilometers + /// Kilometers. /// Kilometers, + /// - /// Miles + /// Miles. /// Miles, + /// - /// Feet + /// Feet. /// Feet, } @@ -33,7 +36,7 @@ internal static class GeoUnitExtensions GeoUnit.Kilometers => RedisLiterals.km, GeoUnit.Meters => RedisLiterals.m, GeoUnit.Miles => RedisLiterals.mi, - _ => throw new ArgumentOutOfRangeException(nameof(unit)) + _ => throw new ArgumentOutOfRangeException(nameof(unit)), }; } } diff --git a/src/StackExchange.Redis/Enums/ListSide.cs b/src/StackExchange.Redis/Enums/ListSide.cs index dfb74383d..8d326a8af 100644 --- a/src/StackExchange.Redis/Enums/ListSide.cs +++ b/src/StackExchange.Redis/Enums/ListSide.cs @@ -11,6 +11,7 @@ public enum ListSide /// The head of the list. /// Left, + /// /// The tail of the list. /// @@ -23,7 +24,7 @@ internal static class ListSideExtensions { ListSide.Left => RedisLiterals.LEFT, ListSide.Right => RedisLiterals.RIGHT, - _ => throw new ArgumentOutOfRangeException(nameof(side)) + _ => throw new ArgumentOutOfRangeException(nameof(side)), }; } } diff --git a/src/StackExchange.Redis/Enums/MigrateOptions.cs b/src/StackExchange.Redis/Enums/MigrateOptions.cs index 561b5494d..fbfdaa731 100644 --- a/src/StackExchange.Redis/Enums/MigrateOptions.cs +++ b/src/StackExchange.Redis/Enums/MigrateOptions.cs @@ -12,10 +12,12 @@ public enum MigrateOptions /// No options specified. /// None = 0, + /// /// Do not remove the key from the local instance. /// Copy = 1, + /// /// Replace existing key on the remote instance. /// diff --git a/src/StackExchange.Redis/Enums/Order.cs b/src/StackExchange.Redis/Enums/Order.cs index be3dd0a8b..99d989006 100644 --- a/src/StackExchange.Redis/Enums/Order.cs +++ b/src/StackExchange.Redis/Enums/Order.cs @@ -11,6 +11,7 @@ public enum Order /// Ordered from low values to high values. /// Ascending, + /// /// Ordered from high values to low values. /// @@ -23,7 +24,7 @@ internal static class OrderExtensions { Order.Ascending => RedisLiterals.ASC, Order.Descending => RedisLiterals.DESC, - _ => throw new ArgumentOutOfRangeException(nameof(order)) + _ => throw new ArgumentOutOfRangeException(nameof(order)), }; } } diff --git a/src/StackExchange.Redis/Enums/Proxy.cs b/src/StackExchange.Redis/Enums/Proxy.cs index f529ac123..9dc1d3770 100644 --- a/src/StackExchange.Redis/Enums/Proxy.cs +++ b/src/StackExchange.Redis/Enums/Proxy.cs @@ -9,10 +9,12 @@ public enum Proxy /// Direct communication to the redis server(s). /// None, + /// /// Communication via twemproxy. /// Twemproxy, + /// /// Communication via envoyproxy. /// @@ -28,7 +30,7 @@ internal static class ProxyExtensions { Proxy.Twemproxy => false, Proxy.Envoyproxy => false, - _ => true + _ => true, }; /// @@ -38,7 +40,7 @@ internal static class ProxyExtensions { Proxy.Twemproxy => false, Proxy.Envoyproxy => false, - _ => true + _ => true, }; /// @@ -48,7 +50,7 @@ internal static class ProxyExtensions { Proxy.Twemproxy => false, Proxy.Envoyproxy => false, - _ => true + _ => true, }; } } diff --git a/src/StackExchange.Redis/Enums/RedisType.cs b/src/StackExchange.Redis/Enums/RedisType.cs index b061dc906..f1da87505 100644 --- a/src/StackExchange.Redis/Enums/RedisType.cs +++ b/src/StackExchange.Redis/Enums/RedisType.cs @@ -10,6 +10,7 @@ public enum RedisType /// The specified key does not exist. /// None, + /// /// Strings are the most basic kind of Redis value. Redis Strings are binary safe, this means that /// a Redis string can contain any kind of data, for instance a JPEG image or a serialized Ruby object. @@ -17,6 +18,7 @@ public enum RedisType /// /// String, + /// /// Redis Lists are simply lists of strings, sorted by insertion order. /// It is possible to add elements to a Redis List pushing new elements on the head (on the left) or @@ -24,6 +26,7 @@ public enum RedisType /// /// List, + /// /// Redis Sets are an unordered collection of Strings. It is possible to add, remove, and test for /// existence of members in O(1) (constant time regardless of the number of elements contained inside the Set). @@ -33,6 +36,7 @@ public enum RedisType /// /// Set, + /// /// Redis Sorted Sets are, similarly to Redis Sets, non repeating collections of Strings. /// The difference is that every member of a Sorted Set is associated with score, that is used @@ -41,12 +45,14 @@ public enum RedisType /// /// SortedSet, + /// /// Redis Hashes are maps between string fields and string values, so they are the perfect data type /// to represent objects (e.g. A User with a number of fields like name, surname, age, and so forth). /// /// Hash, + /// /// A Redis Stream is a data structure which models the behavior of an append only log but it has more /// advanced features for manipulating the data contained within the stream. Each entry in a @@ -54,6 +60,7 @@ public enum RedisType /// /// Stream, + /// /// The data-type was not recognised by the client library. /// diff --git a/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs b/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs index 12f84ffba..897ebbb6c 100644 --- a/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs +++ b/src/StackExchange.Redis/Enums/ReplicationChangeOptions.cs @@ -14,24 +14,29 @@ public enum ReplicationChangeOptions /// No additional operations. /// None = 0, + /// /// Set the tie-breaker key on all available primaries, to specify this server. /// SetTiebreaker = 1, + /// /// Broadcast to the pub-sub channel to listening clients to reconfigure themselves. /// Broadcast = 2, + /// /// Issue a REPLICAOF to all other known nodes, making this primary of all. /// [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicateToOtherEndpoints) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] EnslaveSubordinates = 4, + /// /// Issue a REPLICAOF to all other known nodes, making this primary of all. /// ReplicateToOtherEndpoints = 4, // note ToString prefers *later* options + /// /// All additional operations. /// diff --git a/src/StackExchange.Redis/Enums/ResultType.cs b/src/StackExchange.Redis/Enums/ResultType.cs index ca09f64b0..63e267a91 100644 --- a/src/StackExchange.Redis/Enums/ResultType.cs +++ b/src/StackExchange.Redis/Enums/ResultType.cs @@ -19,14 +19,17 @@ public enum ResultType : byte /// Basic strings typically represent status results such as "OK". /// SimpleString = 1, + /// /// Error strings represent invalid operation results from the server. /// Error = 2, + /// /// Integers are returned for count operations and some integer-based increment operations. /// Integer = 3, + /// /// Bulk strings represent typical user content values. /// @@ -65,7 +68,7 @@ public enum ResultType : byte Double = (1 << 3) | SimpleString, /// - /// A large number non representable by the type + /// A large number non representable by the type. /// BigInteger = (2 << 3) | SimpleString, diff --git a/src/StackExchange.Redis/Enums/RetransmissionReasonType.cs b/src/StackExchange.Redis/Enums/RetransmissionReasonType.cs index 18529b029..6bd9d43e6 100644 --- a/src/StackExchange.Redis/Enums/RetransmissionReasonType.cs +++ b/src/StackExchange.Redis/Enums/RetransmissionReasonType.cs @@ -16,10 +16,12 @@ public enum RetransmissionReasonType /// No stated reason. /// None = 0, + /// /// Issued to investigate which node owns a key. /// Ask, + /// /// A node has indicated that it does *not* own the given key. /// diff --git a/src/StackExchange.Redis/Enums/SaveType.cs b/src/StackExchange.Redis/Enums/SaveType.cs index 740e262a9..5296d110e 100644 --- a/src/StackExchange.Redis/Enums/SaveType.cs +++ b/src/StackExchange.Redis/Enums/SaveType.cs @@ -13,6 +13,7 @@ public enum SaveType /// /// BackgroundRewriteAppendOnlyFile, + /// /// Save the DB in background. The OK code is immediately returned. /// Redis forks, the parent continues to serve the clients, the child saves the DB on disk then exits. @@ -20,6 +21,7 @@ public enum SaveType /// /// BackgroundSave, + /// /// Save the DB in foreground. /// This is almost never a good thing to do, and could cause significant blocking. diff --git a/src/StackExchange.Redis/Enums/ServerType.cs b/src/StackExchange.Redis/Enums/ServerType.cs index 19c6a3f19..ef49a8449 100644 --- a/src/StackExchange.Redis/Enums/ServerType.cs +++ b/src/StackExchange.Redis/Enums/ServerType.cs @@ -9,18 +9,22 @@ public enum ServerType /// Classic redis-server server. /// Standalone, + /// /// Monitoring/configuration redis-sentinel server. /// Sentinel, + /// /// Distributed redis-cluster server. /// Cluster, + /// /// Distributed redis installation via twemproxy. /// Twemproxy, + /// /// Redis cluster via envoyproxy. /// @@ -35,7 +39,7 @@ internal static class ServerTypeExtensions internal static bool HasSinglePrimary(this ServerType type) => type switch { ServerType.Envoyproxy => false, - _ => true + _ => true, }; /// @@ -45,7 +49,7 @@ internal static class ServerTypeExtensions { ServerType.Twemproxy => false, ServerType.Envoyproxy => false, - _ => true + _ => true, }; } } diff --git a/src/StackExchange.Redis/Enums/SetOperation.cs b/src/StackExchange.Redis/Enums/SetOperation.cs index 7e649847f..a529d348e 100644 --- a/src/StackExchange.Redis/Enums/SetOperation.cs +++ b/src/StackExchange.Redis/Enums/SetOperation.cs @@ -11,10 +11,12 @@ public enum SetOperation /// Returns the members of the set resulting from the union of all the given sets. /// Union, + /// /// Returns the members of the set resulting from the intersection of all the given sets. /// Intersect, + /// /// Returns the members of the set resulting from the difference between the first set and all the successive sets. /// @@ -25,12 +27,12 @@ internal static class SetOperationExtensions { internal static RedisCommand ToCommand(this SetOperation operation, bool store) => operation switch { - SetOperation.Intersect when store => RedisCommand.ZINTERSTORE, - SetOperation.Intersect => RedisCommand.ZINTER, - SetOperation.Union when store => RedisCommand.ZUNIONSTORE, - SetOperation.Union => RedisCommand.ZUNION, + SetOperation.Intersect when store => RedisCommand.ZINTERSTORE, + SetOperation.Intersect => RedisCommand.ZINTER, + SetOperation.Union when store => RedisCommand.ZUNIONSTORE, + SetOperation.Union => RedisCommand.ZUNION, SetOperation.Difference when store => RedisCommand.ZDIFFSTORE, - SetOperation.Difference => RedisCommand.ZDIFF, + SetOperation.Difference => RedisCommand.ZDIFF, _ => throw new ArgumentOutOfRangeException(nameof(operation)), }; } diff --git a/src/StackExchange.Redis/Enums/ShutdownMode.cs b/src/StackExchange.Redis/Enums/ShutdownMode.cs index dfd46b70f..a8b701ea8 100644 --- a/src/StackExchange.Redis/Enums/ShutdownMode.cs +++ b/src/StackExchange.Redis/Enums/ShutdownMode.cs @@ -9,10 +9,12 @@ public enum ShutdownMode /// The data is persisted if save points are configured. /// Default, + /// /// The data is NOT persisted even if save points are configured. /// Never, + /// /// The data is persisted even if save points are NOT configured. /// diff --git a/src/StackExchange.Redis/Enums/SimulatedFailureType.cs b/src/StackExchange.Redis/Enums/SimulatedFailureType.cs index 80fca095c..7f2968eca 100644 --- a/src/StackExchange.Redis/Enums/SimulatedFailureType.cs +++ b/src/StackExchange.Redis/Enums/SimulatedFailureType.cs @@ -5,10 +5,10 @@ namespace StackExchange.Redis [Flags] internal enum SimulatedFailureType { - None = 0, - InteractiveInbound = 1 << 0, - InteractiveOutbound = 1 << 1, - SubscriptionInbound = 1 << 2, + None = 0, + InteractiveInbound = 1 << 0, + InteractiveOutbound = 1 << 1, + SubscriptionInbound = 1 << 2, SubscriptionOutbound = 1 << 3, AllInbound = InteractiveInbound | SubscriptionInbound, diff --git a/src/StackExchange.Redis/Enums/SortType.cs b/src/StackExchange.Redis/Enums/SortType.cs index 9fc3a20ae..48a3596b6 100644 --- a/src/StackExchange.Redis/Enums/SortType.cs +++ b/src/StackExchange.Redis/Enums/SortType.cs @@ -9,6 +9,7 @@ public enum SortType /// Elements are interpreted as a double-precision floating point number and sorted numerically. /// Numeric, + /// /// Elements are sorted using their alphabetic form /// (Redis is UTF-8 aware as long as the !LC_COLLATE environment variable is set at the server). diff --git a/src/StackExchange.Redis/Enums/SortedSetOrder.cs b/src/StackExchange.Redis/Enums/SortedSetOrder.cs index 6c205bae0..474cd3612 100644 --- a/src/StackExchange.Redis/Enums/SortedSetOrder.cs +++ b/src/StackExchange.Redis/Enums/SortedSetOrder.cs @@ -16,7 +16,7 @@ public enum SortedSetOrder ByScore, /// - /// Bases ordering off of lexicographical order, this is only appropriate in an instance where all the members of your sorted set are given the same score + /// Bases ordering off of lexicographical order, this is only appropriate in an instance where all the members of your sorted set are given the same score. /// ByLex, } @@ -27,6 +27,6 @@ internal static class SortedSetOrderByExtensions { SortedSetOrder.ByLex => RedisLiterals.BYLEX, SortedSetOrder.ByScore => RedisLiterals.BYSCORE, - _ => RedisValue.Null + _ => RedisValue.Null, }; } diff --git a/src/StackExchange.Redis/Enums/SortedSetWhen.cs b/src/StackExchange.Redis/Enums/SortedSetWhen.cs index a394482b6..517aaeaa5 100644 --- a/src/StackExchange.Redis/Enums/SortedSetWhen.cs +++ b/src/StackExchange.Redis/Enums/SortedSetWhen.cs @@ -12,18 +12,22 @@ public enum SortedSetWhen /// The operation won't be prevented. /// Always = 0, + /// /// The operation should only occur when there is an existing value. /// Exists = 1 << 0, + /// /// The operation should only occur when the new score is greater than the current score. /// GreaterThan = 1 << 1, + /// /// The operation should only occur when the new score is less than the current score. /// LessThan = 1 << 2, + /// /// The operation should only occur when there is not an existing value. /// @@ -35,18 +39,18 @@ internal static class SortedSetWhenExtensions internal static uint CountBits(this SortedSetWhen when) { uint v = (uint)when; - v -= ((v >> 1) & 0x55555555); // reuse input as temporary + v -= (v >> 1) & 0x55555555; // reuse input as temporary v = (v & 0x33333333) + ((v >> 2) & 0x33333333); // temp uint c = ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; // count return c; } - internal static SortedSetWhen Parse(When when)=> when switch + internal static SortedSetWhen Parse(When when) => when switch { When.Always => SortedSetWhen.Always, When.Exists => SortedSetWhen.Exists, When.NotExists => SortedSetWhen.NotExists, - _ => throw new ArgumentOutOfRangeException(nameof(when)) + _ => throw new ArgumentOutOfRangeException(nameof(when)), }; } } diff --git a/src/StackExchange.Redis/Enums/StringIndexType.cs b/src/StackExchange.Redis/Enums/StringIndexType.cs index deb180404..fcb41e391 100644 --- a/src/StackExchange.Redis/Enums/StringIndexType.cs +++ b/src/StackExchange.Redis/Enums/StringIndexType.cs @@ -11,6 +11,7 @@ public enum StringIndexType /// Indicates the index is the number of bytes into a string. /// Byte, + /// /// Indicates the index is the number of bits into a string. /// @@ -23,6 +24,6 @@ internal static class StringIndexTypeExtensions { StringIndexType.Bit => RedisLiterals.BIT, StringIndexType.Byte => RedisLiterals.BYTE, - _ => throw new ArgumentOutOfRangeException(nameof(indexType)) + _ => throw new ArgumentOutOfRangeException(nameof(indexType)), }; } diff --git a/src/StackExchange.Redis/Enums/When.cs b/src/StackExchange.Redis/Enums/When.cs index d0bc5d303..412e4064a 100644 --- a/src/StackExchange.Redis/Enums/When.cs +++ b/src/StackExchange.Redis/Enums/When.cs @@ -9,10 +9,12 @@ public enum When /// The operation should occur whether or not there is an existing value. /// Always, + /// /// The operation should only occur when there is an existing value. /// Exists, + /// /// The operation should only occur when there is not an existing value. /// diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index fd1953de6..24e519b54 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -102,8 +102,8 @@ internal static Exception NoConnectionAvailable( if (server != null) { - //if we already have the serverEndpoint for connection failure use that - //otherwise it would output state of all the endpoints + // If we already have the serverEndpoint for connection failure use that, + // otherwise it would output state of all the endpoints. serverSnapshot = new ServerEndPoint[] { server }; } @@ -283,11 +283,11 @@ internal static Exception Timeout(ConnectionMultiplexer multiplexer, string? bas Exception ex = logConnectionException && lastConnectionException is not null ? new RedisConnectionException(lastConnectionException.FailureType, sb.ToString(), lastConnectionException, message?.Status ?? CommandStatus.Unknown) { - HelpLink = TimeoutHelpLink + HelpLink = TimeoutHelpLink, } : new RedisTimeoutException(sb.ToString(), message?.Status ?? CommandStatus.Unknown) { - HelpLink = TimeoutHelpLink + HelpLink = TimeoutHelpLink, }; CopyDataToException(data, ex); @@ -312,8 +312,7 @@ private static void AddCommonDetail( StringBuilder sb, Message? message, ConnectionMultiplexer multiplexer, - ServerEndPoint? server - ) + ServerEndPoint? server) { if (message != null) { @@ -325,7 +324,7 @@ private static void AddCommonDetail( // Add server data, if we have it if (server != null && message != null) { - var bs = server.GetBridgeStatus(message.IsForSubscriptionBridge ? ConnectionType.Subscription: ConnectionType.Interactive); + var bs = server.GetBridgeStatus(message.IsForSubscriptionBridge ? ConnectionType.Subscription : ConnectionType.Interactive); switch (bs.Connection.ReadStatus) { diff --git a/src/StackExchange.Redis/Exceptions.cs b/src/StackExchange.Redis/Exceptions.cs index 17abcc21c..9315eb806 100644 --- a/src/StackExchange.Redis/Exceptions.cs +++ b/src/StackExchange.Redis/Exceptions.cs @@ -50,6 +50,7 @@ private RedisTimeoutException(SerializationInfo info, StreamingContext ctx) : ba { Commandstatus = info.GetValue("commandStatus", typeof(CommandStatus)) as CommandStatus? ?? CommandStatus.Unknown; } + /// /// Serialization implementation; not intended for general usage. /// @@ -73,7 +74,7 @@ public sealed partial class RedisConnectionException : RedisException /// /// The type of connection failure. /// The message for the exception. - public RedisConnectionException(ConnectionFailureType failureType, string message) : this(failureType, message, null, CommandStatus.Unknown) {} + public RedisConnectionException(ConnectionFailureType failureType, string message) : this(failureType, message, null, CommandStatus.Unknown) { } /// /// Creates a new . @@ -81,7 +82,7 @@ public RedisConnectionException(ConnectionFailureType failureType, string messag /// The type of connection failure. /// The message for the exception. /// The inner exception. - public RedisConnectionException(ConnectionFailureType failureType, string message, Exception? innerException) : this(failureType, message, innerException, CommandStatus.Unknown) {} + public RedisConnectionException(ConnectionFailureType failureType, string message, Exception? innerException) : this(failureType, message, innerException, CommandStatus.Unknown) { } /// /// Creates a new . @@ -111,6 +112,7 @@ private RedisConnectionException(SerializationInfo info, StreamingContext ctx) : FailureType = (ConnectionFailureType)info.GetInt32("failureType"); CommandStatus = info.GetValue("commandStatus", typeof(CommandStatus)) as CommandStatus? ?? CommandStatus.Unknown; } + /// /// Serialization implementation; not intended for general usage. /// diff --git a/src/StackExchange.Redis/ExponentialRetry.cs b/src/StackExchange.Redis/ExponentialRetry.cs index 4cf965abc..f28708679 100644 --- a/src/StackExchange.Redis/ExponentialRetry.cs +++ b/src/StackExchange.Redis/ExponentialRetry.cs @@ -15,14 +15,14 @@ public class ExponentialRetry : IReconnectRetryPolicy /// /// Initializes a new instance using the specified back off interval with default maxDeltaBackOffMilliseconds of 10 seconds. /// - /// time in milliseconds for the back-off interval between retries + /// Time in milliseconds for the back-off interval between retries. public ExponentialRetry(int deltaBackOffMilliseconds) : this(deltaBackOffMilliseconds, Math.Max(deltaBackOffMilliseconds, (int)TimeSpan.FromSeconds(10).TotalMilliseconds)) { } /// /// Initializes a new instance using the specified back off interval. /// - /// time in milliseconds for the back-off interval between retries. - /// time in milliseconds for the maximum value that the back-off interval can exponentially grow up to. + /// Time in milliseconds for the back-off interval between retries. + /// Time in milliseconds for the maximum value that the back-off interval can exponentially grow up to. public ExponentialRetry(int deltaBackOffMilliseconds, int maxDeltaBackOffMilliseconds) { if (deltaBackOffMilliseconds < 0) @@ -54,16 +54,16 @@ public bool ShouldRetry(long currentRetryCount, int timeElapsedMillisecondsSince r ??= new Random(); random = r.Next((int)deltaBackOffMilliseconds, exponential); return timeElapsedMillisecondsSinceLastRetry >= random; - //exponential backoff with deltaBackOff of 5000ms - //deltabackoff exponential - //5000 5500 - //5000 6050 - //5000 6655 - //5000 8053 - //5000 10718 - //5000 17261 - //5000 37001 - //5000 127738 + // exponential backoff with deltaBackOff of 5000ms + // deltabackoff exponential + // 5000 5500 + // 5000 6050 + // 5000 6655 + // 5000 8053 + // 5000 10718 + // 5000 17261 + // 5000 37001 + // 5000 127738 } } } diff --git a/src/StackExchange.Redis/ExtensionMethods.cs b/src/StackExchange.Redis/ExtensionMethods.cs index 89b9a0e21..87904aa9c 100644 --- a/src/StackExchange.Redis/ExtensionMethods.cs +++ b/src/StackExchange.Redis/ExtensionMethods.cs @@ -21,7 +21,7 @@ public static class ExtensionMethods /// /// The entry to convert to a dictionary. [return: NotNullIfNotNull("hash")] - public static Dictionary? ToStringDictionary(this HashEntry[]? hash) + public static Dictionary? ToStringDictionary(this HashEntry[]? hash) { if (hash is null) { @@ -29,7 +29,7 @@ public static class ExtensionMethods } var result = new Dictionary(hash.Length, StringComparer.Ordinal); - for(int i = 0; i < hash.Length; i++) + for (int i = 0; i < hash.Length; i++) { result.Add(hash[i].name!, hash[i].value!); } @@ -192,7 +192,7 @@ internal static void AuthenticateAsClient(this SslStream ssl, string host, SslPr { if (!allowedProtocols.HasValue) { - //Default to the sslProtocols defined by the .NET Framework + // Default to the sslProtocols defined by the .NET Framework AuthenticateAsClientUsingDefaultProtocols(ssl, host); return; } @@ -278,9 +278,8 @@ private static void AuthenticateAsClientUsingDefaultProtocols(SslStream ssl, str private sealed class LeaseMemoryStream : MemoryStream { private readonly IDisposable _parent; - public LeaseMemoryStream(ArraySegment segment, IDisposable parent) - : base(segment.Array!, segment.Offset, segment.Count, false, true) - => _parent = parent; + + public LeaseMemoryStream(ArraySegment segment, IDisposable parent) : base(segment.Array!, segment.Offset, segment.Count, false, true) => _parent = parent; protected override void Dispose(bool disposing) { @@ -302,7 +301,6 @@ protected override void Dispose(bool disposing) // assembly-binding-redirect entries to fix this up, so; it would present an unreasonable support burden // otherwise. And yes, I've tried explicitly referencing System.Numerics.Vectors in the manifest to // force it... nothing. Nada. - #if VECTOR_SAFE [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int VectorSafeIndexOf(this ReadOnlySpan span, byte value) @@ -325,12 +323,13 @@ internal static int VectorSafeIndexOf(this ReadOnlySpan span, byte value) } return -1; } + internal static int VectorSafeIndexOfCRLF(this ReadOnlySpan span) { // yes, this has zero optimization; I'm OK with this as the fallback strategy for (int i = 1; i < span.Length; i++) { - if (span[i] == '\n' && span[i-1] == '\r') return i - 1; + if (span[i] == '\n' && span[i - 1] == '\r') return i - 1; } return -1; } diff --git a/src/StackExchange.Redis/Format.cs b/src/StackExchange.Redis/Format.cs index 73c29a82e..6836a70da 100644 --- a/src/StackExchange.Redis/Format.cs +++ b/src/StackExchange.Redis/Format.cs @@ -1,10 +1,10 @@ using System; using System.Buffers; using System.Buffers.Text; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Net; using System.Text; -using System.Diagnostics.CodeAnalysis; #if UNIX_SOCKET using System.Net.Sockets; @@ -86,7 +86,7 @@ internal static string ToString(double value) float f => ToString(f), double d => ToString(d), EndPoint e => ToString(e), - _ => Convert.ToString(value, CultureInfo.InvariantCulture) + _ => Convert.ToString(value, CultureInfo.InvariantCulture), }; internal static string ToString(EndPoint? endpoint) @@ -113,7 +113,7 @@ internal static string ToStringHostOnly(EndPoint endpoint) => { DnsEndPoint dns => dns.Host, IPEndPoint ip => ip.Address.ToString(), - _ => "" + _ => "", }; internal static bool TryGetHostPort(EndPoint? endpoint, [NotNullWhen(true)] out string? host, [NotNullWhen(true)] out int? port) @@ -258,7 +258,7 @@ private static bool CaseInsensitiveASCIIEqual(string xLowerCase, ReadOnlySpan /// /// Adapted from IPEndPointParser in Microsoft.AspNetCore - /// Link: + /// Link: . /// /// /// Copyright (c) .NET Foundation. All rights reserved. @@ -276,7 +276,7 @@ internal static bool TryParseEndPoint(string? addressWithPort, [NotNullWhen(true return false; } - if (addressWithPort[0]=='!') + if (addressWithPort[0] == '!') { if (addressWithPort.Length == 1) { diff --git a/src/StackExchange.Redis/GlobalSuppressions.cs b/src/StackExchange.Redis/GlobalSuppressions.cs index 3882f4776..84d04d110 100644 --- a/src/StackExchange.Redis/GlobalSuppressions.cs +++ b/src/StackExchange.Redis/GlobalSuppressions.cs @@ -5,17 +5,17 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~P:StackExchange.Redis.Message.IsAdmin")] -[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.ServerEndPoint.GetBridge(StackExchange.Redis.RedisCommand,System.Boolean)~StackExchange.Redis.PhysicalBridge")] -[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisValue.op_Equality(StackExchange.Redis.RedisValue,StackExchange.Redis.RedisValue)~System.Boolean")] -[assembly: SuppressMessage("Style", "IDE0075:Simplify conditional expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisSubscriber.Unsubscribe(StackExchange.Redis.RedisChannel@,System.Action{StackExchange.Redis.RedisChannel,StackExchange.Redis.RedisValue},StackExchange.Redis.ChannelMessageQueue,StackExchange.Redis.CommandFlags)~System.Boolean")] -[assembly: SuppressMessage("Roslynator", "RCS1104:Simplify conditional expression.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisSubscriber.Unsubscribe(StackExchange.Redis.RedisChannel@,System.Action{StackExchange.Redis.RedisChannel,StackExchange.Redis.RedisValue},StackExchange.Redis.ChannelMessageQueue,StackExchange.Redis.CommandFlags)~System.Boolean")] -[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Message.IsPrimaryOnly(StackExchange.Redis.RedisCommand)~System.Boolean")] -[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Message.RequiresDatabase(StackExchange.Redis.RedisCommand)~System.Boolean")] -[assembly: SuppressMessage("Style", "IDE0180:Use tuple to swap values", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisDatabase.ReverseLimits(StackExchange.Redis.Order,StackExchange.Redis.Exclude@,StackExchange.Redis.RedisValue@,StackExchange.Redis.RedisValue@)")] -[assembly: SuppressMessage("Style", "IDE0180:Use tuple to swap values", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.RedisDatabase.GetSortedSetRangeByScoreMessage(StackExchange.Redis.RedisKey,System.Double,System.Double,StackExchange.Redis.Exclude,StackExchange.Redis.Order,System.Int64,System.Int64,StackExchange.Redis.CommandFlags,System.Boolean)~StackExchange.Redis.Message")] -[assembly: SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.PhysicalConnection.FlushSync(System.Boolean,System.Int32)~StackExchange.Redis.WriteResult")] -[assembly: SuppressMessage("Usage", "CA2219:Do not raise exceptions in finally clauses", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.PhysicalBridge.ProcessBacklogAsync~System.Threading.Tasks.Task")] -[assembly: SuppressMessage("Usage", "CA2249:Consider using 'string.Contains' instead of 'string.IndexOf'", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.ClientInfo.AddFlag(StackExchange.Redis.ClientFlags@,System.String,StackExchange.Redis.ClientFlags,System.Char)")] -[assembly: SuppressMessage("Style", "IDE0070:Use 'System.HashCode'", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.CommandBytes.GetHashCode~System.Int32")] -[assembly: SuppressMessage("Roslynator", "RCS1085:Use auto-implemented property.", Justification = "", Scope = "member", Target = "~P:StackExchange.Redis.RedisValue.OverlappedValueInt64")] +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "Pending", Scope = "member", Target = "~P:StackExchange.Redis.Message.IsAdmin")] +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.ServerEndPoint.GetBridge(StackExchange.Redis.RedisCommand,System.Boolean)~StackExchange.Redis.PhysicalBridge")] +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.RedisValue.op_Equality(StackExchange.Redis.RedisValue,StackExchange.Redis.RedisValue)~System.Boolean")] +[assembly: SuppressMessage("Style", "IDE0075:Simplify conditional expression", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.RedisSubscriber.Unsubscribe(StackExchange.Redis.RedisChannel@,System.Action{StackExchange.Redis.RedisChannel,StackExchange.Redis.RedisValue},StackExchange.Redis.ChannelMessageQueue,StackExchange.Redis.CommandFlags)~System.Boolean")] +[assembly: SuppressMessage("Roslynator", "RCS1104:Simplify conditional expression.", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.RedisSubscriber.Unsubscribe(StackExchange.Redis.RedisChannel@,System.Action{StackExchange.Redis.RedisChannel,StackExchange.Redis.RedisValue},StackExchange.Redis.ChannelMessageQueue,StackExchange.Redis.CommandFlags)~System.Boolean")] +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Message.IsPrimaryOnly(StackExchange.Redis.RedisCommand)~System.Boolean")] +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Message.RequiresDatabase(StackExchange.Redis.RedisCommand)~System.Boolean")] +[assembly: SuppressMessage("Style", "IDE0180:Use tuple to swap values", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.RedisDatabase.ReverseLimits(StackExchange.Redis.Order,StackExchange.Redis.Exclude@,StackExchange.Redis.RedisValue@,StackExchange.Redis.RedisValue@)")] +[assembly: SuppressMessage("Style", "IDE0180:Use tuple to swap values", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.RedisDatabase.GetSortedSetRangeByScoreMessage(StackExchange.Redis.RedisKey,System.Double,System.Double,StackExchange.Redis.Exclude,StackExchange.Redis.Order,System.Int64,System.Int64,StackExchange.Redis.CommandFlags,System.Boolean)~StackExchange.Redis.Message")] +[assembly: SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.PhysicalConnection.FlushSync(System.Boolean,System.Int32)~StackExchange.Redis.WriteResult")] +[assembly: SuppressMessage("Usage", "CA2219:Do not raise exceptions in finally clauses", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.PhysicalBridge.ProcessBacklogAsync~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Usage", "CA2249:Consider using 'string.Contains' instead of 'string.IndexOf'", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.ClientInfo.AddFlag(StackExchange.Redis.ClientFlags@,System.String,StackExchange.Redis.ClientFlags,System.Char)")] +[assembly: SuppressMessage("Style", "IDE0070:Use 'System.HashCode'", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.CommandBytes.GetHashCode~System.Int32")] +[assembly: SuppressMessage("Roslynator", "RCS1085:Use auto-implemented property.", Justification = "Pending", Scope = "member", Target = "~P:StackExchange.Redis.RedisValue.OverlappedValueInt64")] diff --git a/src/StackExchange.Redis/HashSlotMovedEventArgs.cs b/src/StackExchange.Redis/HashSlotMovedEventArgs.cs index d92c2af8c..876088e5c 100644 --- a/src/StackExchange.Redis/HashSlotMovedEventArgs.cs +++ b/src/StackExchange.Redis/HashSlotMovedEventArgs.cs @@ -27,8 +27,7 @@ public class HashSlotMovedEventArgs : EventArgs, ICompletable /// public EndPoint NewEndPoint { get; } - internal HashSlotMovedEventArgs(EventHandler? handler, object sender, - int hashSlot, EndPoint? old, EndPoint @new) + internal HashSlotMovedEventArgs(EventHandler? handler, object sender, int hashSlot, EndPoint? old, EndPoint @new) { this.handler = handler; this.sender = sender; @@ -45,7 +44,7 @@ internal HashSlotMovedEventArgs(EventHandler? handler, o /// Old endpoint. /// New endpoint. public HashSlotMovedEventArgs(object sender, int hashSlot, EndPoint old, EndPoint @new) - : this (null, sender, hashSlot, old, @new) + : this(null, sender, hashSlot, old, @new) { } diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index 25d2f7099..b4bdb0950 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -1,11 +1,11 @@ -using StackExchange.Redis.Maintenance; -using StackExchange.Redis.Profiling; -using System; +using System; using System.Collections.Concurrent; using System.ComponentModel; using System.IO; using System.Net; using System.Threading.Tasks; +using StackExchange.Redis.Maintenance; +using StackExchange.Redis.Profiling; using static StackExchange.Redis.ConnectionMultiplexer; namespace StackExchange.Redis diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 7cf2248fd..6178051d0 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -6,12 +6,12 @@ namespace StackExchange.Redis { /// - /// Describes functionality that is common to both standalone redis servers and redis clusters + /// Describes functionality that is common to both standalone redis servers and redis clusters. /// public interface IDatabase : IRedis, IDatabaseAsync { /// - /// The numeric identifier of this database + /// The numeric identifier of this database. /// int Database { get; } @@ -492,8 +492,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// if field is a new field in the hash and value was set, if field already exists in the hash and the value was updated. /// + /// See /// , - /// + /// . /// bool HashSet(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); @@ -501,7 +502,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// Returns the string length of the value associated with field in the hash stored at key. /// /// The key of the hash. - /// The field containing the string + /// The field containing the string. /// The flags to use for this operation. /// The length of the string at field, or 0 when key does not exist. /// @@ -559,7 +560,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The key of the merged hyperloglog. /// The key of the first hyperloglog to merge. - /// The key of the first hyperloglog to merge. + /// The key of the second hyperloglog to merge. /// The flags to use for this operation. /// void HyperLogLogMerge(RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); @@ -601,8 +602,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// if the key was removed. /// + /// See /// , - /// + /// . /// bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -614,8 +616,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The number of keys that were removed. /// + /// See /// , - /// + /// . /// long KeyDelete(RedisKey[] keys, CommandFlags flags = CommandFlags.None); @@ -677,9 +680,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// See the page on key expiry for more information. /// /// + /// See /// , /// , - /// + /// . /// /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] @@ -696,8 +700,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// if the timeout was set. if key does not exist or the timeout could not be set. /// + /// See /// , - /// + /// . /// bool KeyExpire(RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); @@ -722,9 +727,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// See the page on key expiry for more information. /// /// + /// See /// , /// , - /// + /// . /// /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] @@ -741,8 +747,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// if the timeout was set. if key does not exist or the timeout could not be set. /// + /// See /// , - /// + /// . /// bool KeyExpire(RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); @@ -753,8 +760,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The time at which the given key will expire, or if the key does not exist or has no associated expiration time. /// + /// See /// , - /// + /// . /// DateTime? KeyExpireTime(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -826,8 +834,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// if the key was renamed, otherwise. /// + /// See /// , - /// + /// . /// bool KeyRename(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None); @@ -931,7 +940,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// If the list contains less than count elements, removes and returns the number of elements in the list. /// /// The key of the list. - /// The number of elements to remove + /// The number of elements to remove. /// The flags to use for this operation. /// Array of values that were popped, or if the key doesn't exist. /// @@ -985,8 +994,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The length of the list after the push operations. /// + /// See /// , - /// + /// . /// long ListLeftPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); @@ -1000,8 +1010,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The length of the list after the push operations. /// + /// See /// , - /// + /// . /// long ListLeftPush(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); @@ -1085,7 +1096,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// If the list contains less than count elements, removes and returns the number of elements in the list. /// /// The key of the list. - /// The number of elements to pop + /// The number of elements to pop. /// The flags to use for this operation. /// Array of values that were popped, or if the key doesn't exist. /// @@ -1122,8 +1133,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The length of the list after the push operation. /// + /// See /// , - /// + /// . /// long ListRightPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); @@ -1137,8 +1149,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The length of the list after the push operation. /// + /// See /// , - /// + /// . /// long ListRightPush(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); @@ -1260,8 +1273,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// A dynamic representation of the script's result. /// + /// See /// , - /// + /// . /// RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); @@ -1312,8 +1326,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// A dynamic representation of the script's result. /// + /// See /// , - /// + /// . /// RedisResult ScriptEvaluateReadOnly(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); @@ -1361,9 +1376,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// List with members of the resulting set. /// + /// See /// , /// , - /// + /// . /// RedisValue[] SetCombine(SetOperation operation, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); @@ -1375,9 +1391,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// List with members of the resulting set. /// + /// See /// , /// , - /// + /// . /// RedisValue[] SetCombine(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None); @@ -1392,9 +1409,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The number of elements in the resulting set. /// + /// See /// , /// , - /// + /// . /// long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); @@ -1408,9 +1426,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The number of elements in the resulting set. /// + /// See /// , /// , - /// + /// . /// long SetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None); @@ -1565,7 +1584,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// /// The SSCAN command is used to incrementally iterate over set. - /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to . + /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to . /// /// The key of the set. /// The pattern to match. @@ -1596,8 +1615,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The sorted elements, or the external values if get is specified. /// + /// See /// , - /// + /// . /// RedisValue[] Sort(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); @@ -1628,7 +1648,7 @@ public interface IDatabase : IRedis, IDatabaseAsync /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when, CommandFlags flags= CommandFlags.None); + bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when, CommandFlags flags = CommandFlags.None); /// /// Adds the specified member with the specified score to the sorted set stored at key. @@ -1675,9 +1695,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The resulting sorted set. /// + /// See /// , /// , - /// + /// . /// RedisValue[] SortedSetCombine(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); @@ -1693,9 +1714,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The resulting sorted set with scores. /// + /// See /// , /// , - /// + /// . /// SortedSetEntry[] SortedSetCombineWithScores(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); @@ -1712,9 +1734,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The number of elements in the resulting sorted set at destination. /// + /// See /// , /// , - /// + /// . /// long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); @@ -1731,9 +1754,10 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The number of elements in the resulting sorted set at destination. /// + /// See /// , /// , - /// + /// . /// long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); @@ -1856,8 +1880,9 @@ public interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// List of elements in the specified range. /// + /// See /// , - /// + /// . /// RedisValue[] SortedSetRangeByRank(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); @@ -1907,8 +1932,9 @@ long SortedSetRangeAndStore( /// The flags to use for this operation. /// List of elements in the specified range. /// + /// See /// , - /// + /// . /// SortedSetEntry[] SortedSetRangeByRankWithScores(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); @@ -1929,10 +1955,12 @@ long SortedSetRangeAndStore( /// The flags to use for this operation. /// List of elements in the specified score range. /// + /// See /// , - /// + /// . /// - RedisValue[] SortedSetRangeByScore(RedisKey key, + RedisValue[] SortedSetRangeByScore( + RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, @@ -1958,10 +1986,12 @@ RedisValue[] SortedSetRangeByScore(RedisKey key, /// The flags to use for this operation. /// List of elements in the specified score range. /// + /// See /// , - /// + /// . /// - SortedSetEntry[] SortedSetRangeByScoreWithScores(RedisKey key, + SortedSetEntry[] SortedSetRangeByScoreWithScores( + RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, @@ -1983,7 +2013,8 @@ SortedSetEntry[] SortedSetRangeByScoreWithScores(RedisKey key, /// The flags to use for this operation. /// List of elements in the specified score range. /// - RedisValue[] SortedSetRangeByValue(RedisKey key, + RedisValue[] SortedSetRangeByValue( + RedisKey key, RedisValue min, RedisValue max, Exclude exclude, @@ -1999,16 +2030,18 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// The min value to filter by. /// The max value to filter by. /// Which of and to exclude (defaults to both inclusive). - /// Whether to order the data ascending or descending + /// Whether to order the data ascending or descending. /// How many items to skip. /// How many items to take. /// The flags to use for this operation. /// List of elements in the specified score range. /// + /// See /// , - /// + /// . /// - RedisValue[] SortedSetRangeByValue(RedisKey key, + RedisValue[] SortedSetRangeByValue( + RedisKey key, RedisValue min = default, RedisValue max = default, Exclude exclude = Exclude.None, @@ -2027,8 +2060,9 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// The flags to use for this operation. /// If member exists in the sorted set, the rank of member. If member does not exist in the sorted set or key does not exist, . /// + /// See /// , - /// + /// . /// long? SortedSetRank(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); @@ -2104,7 +2138,7 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// /// The ZSCAN command is used to incrementally iterate over a sorted set - /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to IScanningCursor. + /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to IScanningCursor. /// /// The key of the sorted set. /// The pattern to match. @@ -2114,7 +2148,8 @@ RedisValue[] SortedSetRangeByValue(RedisKey key, /// The flags to use for this operation. /// Yields all matching elements of the sorted set. /// - IEnumerable SortedSetScan(RedisKey key, + IEnumerable SortedSetScan( + RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, @@ -2154,8 +2189,9 @@ IEnumerable SortedSetScan(RedisKey key, /// The flags to use for this operation. /// The removed element, or when key does not exist. /// + /// See /// , - /// + /// . /// SortedSetEntry? SortedSetPop(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); @@ -2168,8 +2204,9 @@ IEnumerable SortedSetScan(RedisKey key, /// The flags to use for this operation. /// An array of elements, or an empty array when key does not exist. /// + /// See /// , - /// + /// . /// SortedSetEntry[] SortedSetPop(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); @@ -2428,7 +2465,7 @@ IEnumerable SortedSetScan(RedisKey key, /// A pending message is a message read using StreamReadGroup (XREADGROUP) but not yet acknowledged. /// /// The key of the stream. - /// The name of the consumer group + /// The name of the consumer group. /// The flags to use for this operation. /// /// An instance of . @@ -2527,7 +2564,7 @@ IEnumerable SortedSetScan(RedisKey key, /// /// Array of streams and the positions from which to begin reading for each stream. /// The name of the consumer group. - /// + /// The name of the consumer. /// The maximum number of messages to return from each stream. /// The flags to use for this operation. /// A value of for each stream. @@ -2543,7 +2580,7 @@ IEnumerable SortedSetScan(RedisKey key, /// /// Array of streams and the positions from which to begin reading for each stream. /// The name of the consumer group. - /// + /// The name of the consumer. /// The maximum number of messages to return from each stream. /// When true, the message will not be added to the pending message list. /// The flags to use for this operation. @@ -2657,8 +2694,9 @@ IEnumerable SortedSetScan(RedisKey key, /// The flags to use for this operation. /// The value of key after the decrement. /// + /// See /// , - /// + /// . /// long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); @@ -2793,8 +2831,9 @@ IEnumerable SortedSetScan(RedisKey key, /// The flags to use for this operation. /// The value of key after the increment. /// + /// See /// , - /// + /// . /// long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); @@ -2889,8 +2928,9 @@ IEnumerable SortedSetScan(RedisKey key, /// The flags to use for this operation. /// if the keys were set, otherwise. /// + /// See /// , - /// + /// . /// bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index a19525925..103b7151b 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -18,1834 +18,452 @@ public interface IDatabaseAsync : IRedisAsync /// The flags to use for this operation. bool IsConnected(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Atomically transfer a key from a source Redis instance to a destination Redis instance. - /// On success the key is deleted from the original instance by default, and is guaranteed to exist in the target instance. - /// - /// The key to migrate. - /// The server to migrate the key to. - /// The database to migrate the key to. - /// The timeout to use for the transfer. - /// The options to use for this migration. - /// The flags to use for this operation. - /// + /// Task KeyMigrateAsync(RedisKey key, EndPoint toServer, int toDatabase = 0, int timeoutMilliseconds = 0, MigrateOptions migrateOptions = MigrateOptions.None, CommandFlags flags = CommandFlags.None); - /// - /// Returns the raw DEBUG OBJECT output for a key. - /// This command is not fully documented and should be avoided unless you have good reason, and then avoided anyway. - /// - /// The key to debug. - /// The flags to use for this migration. - /// The raw output from DEBUG OBJECT. - /// + /// " Task DebugObjectAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Add the specified member to the set stored at key. - /// Specified members that are already a member of this set are ignored. - /// If key does not exist, a new set is created before adding the specified members. - /// - /// The key of the set. - /// The longitude of geo entry. - /// The latitude of the geo entry. - /// The value to set at this entry. - /// The flags to use for this operation. - /// if the specified member was not already present in the set, else . - /// + /// Task GeoAddAsync(RedisKey key, double longitude, double latitude, RedisValue member, CommandFlags flags = CommandFlags.None); - /// - /// Add the specified member to the set stored at key. - /// Specified members that are already a member of this set are ignored. - /// If key does not exist, a new set is created before adding the specified members. - /// - /// The key of the set. - /// The geo value to store. - /// The flags to use for this operation. - /// if the specified member was not already present in the set, else . - /// + /// Task GeoAddAsync(RedisKey key, GeoEntry value, CommandFlags flags = CommandFlags.None); - /// - /// Add the specified members to the set stored at key. - /// Specified members that are already a member of this set are ignored. - /// If key does not exist, a new set is created before adding the specified members. - /// - /// The key of the set. - /// The geo values add to the set. - /// The flags to use for this operation. - /// The number of elements that were added to the set, not including all the elements already present into the set. - /// + /// Task GeoAddAsync(RedisKey key, GeoEntry[] values, CommandFlags flags = CommandFlags.None); - /// - /// Removes the specified member from the geo sorted set stored at key. - /// Non existing members are ignored. - /// - /// The key of the set. - /// The geo value to remove. - /// The flags to use for this operation. - /// if the member existed in the sorted set and was removed, else . - /// + /// Task GeoRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - /// - /// Return the distance between two members in the geospatial index represented by the sorted set. - /// - /// The key of the set. - /// The first member to check. - /// The second member to check. - /// The unit of distance to return (defaults to meters). - /// The flags to use for this operation. - /// The command returns the distance as a double (represented as a string) in the specified unit, or if one or both the elements are missing. - /// + /// Task GeoDistanceAsync(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None); - /// - /// Return valid Geohash strings representing the position of one or more elements in a sorted set value representing a geospatial index (where elements were added using GEOADD). - /// - /// The key of the set. - /// The members to get. - /// The flags to use for this operation. - /// The command returns an array where each element is the Geohash corresponding to each member name passed as argument to the command. - /// + /// Task GeoHashAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); - /// - /// Return valid Geohash strings representing the position of one or more elements in a sorted set value representing a geospatial index (where elements were added using GEOADD). - /// - /// The key of the set. - /// The member to get. - /// The flags to use for this operation. - /// The command returns an array where each element is the Geohash corresponding to each member name passed as argument to the command. - /// + /// Task GeoHashAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - /// - /// Return the positions (longitude,latitude) of all the specified members of the geospatial index represented by the sorted set at key. - /// - /// The key of the set. - /// The members to get. - /// The flags to use for this operation. - /// - /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command. - /// Non existing elements are reported as NULL elements of the array. - /// - /// + /// Task GeoPositionAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); - /// - /// Return the positions (longitude,latitude) of all the specified members of the geospatial index represented by the sorted set at key. - /// - /// The key of the set. - /// The member to get. - /// The flags to use for this operation. - /// - /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command. - /// Non existing elements are reported as NULL elements of the array. - /// - /// + /// Task GeoPositionAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - /// - /// Return the members of a sorted set populated with geospatial information using GEOADD, which are - /// within the borders of the area specified with the center location and the maximum distance from the center (the radius). - /// - /// The key of the set. - /// The member to get a radius of results from. - /// The radius to check. - /// The unit of (defaults to meters). - /// The count of results to get, -1 for unlimited. - /// The order of the results. - /// The search options to use. - /// The flags to use for this operation. - /// The results found within the radius, if any. - /// + /// Task GeoRadiusAsync(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); - /// - /// Return the members of a sorted set populated with geospatial information using GEOADD, which are - /// within the borders of the area specified with the center location and the maximum distance from the center (the radius). - /// - /// The key of the set. - /// The longitude of the point to get a radius of results from. - /// The latitude of the point to get a radius of results from. - /// The radius to check. - /// The unit of (defaults to meters). - /// The count of results to get, -1 for unlimited. - /// The order of the results. - /// The search options to use. - /// The flags to use for this operation. - /// The results found within the radius, if any. - /// + /// Task GeoRadiusAsync(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); - /// - /// Return the members of the geo-encoded sorted set stored at bounded by the provided - /// , centered at the provided set . - /// - /// The key of the set. - /// The set member to use as the center of the shape. - /// The shape to use to bound the geo search. - /// The maximum number of results to pull back. - /// Whether or not to terminate the search after finding results. Must be true of count is -1. - /// The order to sort by (defaults to unordered). - /// The search options to use. - /// The flags for this operation. - /// The results found within the shape, if any. - /// + /// Task GeoSearchAsync(RedisKey key, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); - /// - /// Return the members of the geo-encoded sorted set stored at bounded by the provided - /// , centered at the point provided by the and . - /// - /// The key of the set. - /// The longitude of the center point. - /// The latitude of the center point. - /// The shape to use to bound the geo search. - /// The maximum number of results to pull back. - /// Whether or not to terminate the search after finding results. Must be true of count is -1. - /// The order to sort by (defaults to unordered). - /// The search options to use. - /// The flags for this operation. - /// The results found within the shape, if any. - /// + /// Task GeoSearchAsync(RedisKey key, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None); - /// - /// Stores the members of the geo-encoded sorted set stored at bounded by the provided - /// , centered at the provided set . - /// - /// The key of the set. - /// The key to store the result at. - /// The set member to use as the center of the shape. - /// The shape to use to bound the geo search. - /// The maximum number of results to pull back. - /// Whether or not to terminate the search after finding results. Must be true of count is -1. - /// The order to sort by (defaults to unordered). - /// If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set. - /// The flags for this operation. - /// The size of the set stored at . - /// + /// Task GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, RedisValue member, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); - /// - /// Stores the members of the geo-encoded sorted set stored at bounded by the provided - /// , centered at the point provided by the and . - /// - /// The key of the set. - /// The key to store the result at. - /// The longitude of the center point. - /// The latitude of the center point. - /// The shape to use to bound the geo search. - /// The maximum number of results to pull back. - /// Whether or not to terminate the search after finding results. Must be true of count is -1. - /// The order to sort by (defaults to unordered). - /// If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set. - /// The flags for this operation. - /// The size of the set stored at . - /// + /// Task GeoSearchAndStoreAsync(RedisKey sourceKey, RedisKey destinationKey, double longitude, double latitude, GeoSearchShape shape, int count = -1, bool demandClosest = true, Order? order = null, bool storeDistances = false, CommandFlags flags = CommandFlags.None); - /// - /// Decrements the number stored at field in the hash stored at key by decrement. - /// If key does not exist, a new key holding a hash is created. - /// If field does not exist the value is set to 0 before the operation is performed. - /// - /// The key of the hash. - /// The field in the hash to decrement. - /// The amount to decrement by. - /// The flags to use for this operation. - /// The value at field after the decrement operation. - /// - /// The range of values supported by HINCRBY is limited to 64 bit signed integers. - /// - /// + /// Task HashDecrementAsync(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None); - /// - /// Decrement the specified field of an hash stored at key, and representing a floating point number, by the specified decrement. - /// If the field does not exist, it is set to 0 before performing the operation. - /// - /// The key of the hash. - /// The field in the hash to decrement. - /// The amount to decrement by. - /// The flags to use for this operation. - /// The value at field after the decrement operation. - /// - /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. - /// - /// + /// Task HashDecrementAsync(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None); - /// - /// Removes the specified fields from the hash stored at key. - /// Non-existing fields are ignored. Non-existing keys are treated as empty hashes and this command returns 0. - /// - /// The key of the hash. - /// The field in the hash to delete. - /// The flags to use for this operation. - /// if the field was removed. - /// + /// Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); - /// - /// Removes the specified fields from the hash stored at key. - /// Non-existing fields are ignored. Non-existing keys are treated as empty hashes and this command returns 0. - /// - /// The key of the hash. - /// The fields in the hash to delete. - /// The flags to use for this operation. - /// The number of fields that were removed. - /// + /// Task HashDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); - /// - /// Returns if field is an existing field in the hash stored at key. - /// - /// The key of the hash. - /// The field in the hash to check. - /// The flags to use for this operation. - /// if the hash contains field, if the hash does not contain field, or key does not exist. - /// + /// Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); - /// - /// Returns the value associated with field in the hash stored at key. - /// - /// The key of the hash. - /// The field in the hash to get. - /// The flags to use for this operation. - /// The value associated with field, or when field is not present in the hash or key does not exist. - /// + /// Task HashGetAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); - /// - /// Returns the value associated with field in the hash stored at key. - /// - /// The key of the hash. - /// The field in the hash to get. - /// The flags to use for this operation. - /// The value associated with field, or when field is not present in the hash or key does not exist. - /// + /// Task?> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); - /// - /// Returns the values associated with the specified fields in the hash stored at key. - /// For every field that does not exist in the hash, a value is returned. - /// Because a non-existing keys are treated as empty hashes, running HMGET against a non-existing key will return a list of values. - /// - /// The key of the hash. - /// The fields in the hash to get. - /// The flags to use for this operation. - /// List of values associated with the given fields, in the same order as they are requested. - /// + /// Task HashGetAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); - /// - /// Returns all fields and values of the hash stored at key. - /// - /// The key of the hash to get all entries from. - /// The flags to use for this operation. - /// List of fields and their values stored in the hash, or an empty list when key does not exist. - /// + /// Task HashGetAllAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Increments the number stored at field in the hash stored at key by increment. - /// If key does not exist, a new key holding a hash is created. - /// If field does not exist the value is set to 0 before the operation is performed. - /// - /// The key of the hash. - /// The field in the hash to increment. - /// The amount to increment by. - /// The flags to use for this operation. - /// The value at field after the increment operation. - /// - /// The range of values supported by HINCRBY is limited to 64 bit signed integers. - /// - /// + /// Task HashIncrementAsync(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None); - /// - /// Increment the specified field of an hash stored at key, and representing a floating point number, by the specified increment. - /// If the field does not exist, it is set to 0 before performing the operation. - /// - /// The key of the hash. - /// The field in the hash to increment. - /// The amount to increment by. - /// The flags to use for this operation. - /// The value at field after the increment operation. - /// - /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. - /// - /// + /// Task HashIncrementAsync(RedisKey key, RedisValue hashField, double value, CommandFlags flags = CommandFlags.None); - /// - /// Returns all field names in the hash stored at key. - /// - /// The key of the hash. - /// The flags to use for this operation. - /// List of fields in the hash, or an empty list when key does not exist. - /// + /// Task HashKeysAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Returns the number of fields contained in the hash stored at key. - /// - /// The key of the hash. - /// The flags to use for this operation. - /// The number of fields in the hash, or 0 when key does not exist. - /// + /// Task HashLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Gets a random field from the hash at . - /// - /// The key of the hash. - /// The flags to use for this operation. - /// A random hash field name or if the hash does not exist. - /// + /// Task HashRandomFieldAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Gets field names from the hash at . - /// - /// The key of the hash. - /// The number of fields to return. - /// The flags to use for this operation. - /// An array of hash field names of size of at most , or if the hash does not exist. - /// + /// Task HashRandomFieldsAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); - /// - /// Gets field names and values from the hash at . - /// - /// The key of the hash. - /// The number of fields to return. - /// The flags to use for this operation. - /// An array of hash entries of size of at most , or if the hash does not exist. - /// + /// Task HashRandomFieldsWithValuesAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); - /// - /// The HSCAN command is used to incrementally iterate over a hash. - /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to . - /// - /// The key of the hash. - /// The pattern of keys to get entries for. - /// The page size to iterate by. - /// The cursor position to start at. - /// The page offset to start at. - /// The flags to use for this operation. - /// Yields all elements of the hash matching the pattern. - /// + /// IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); - /// - /// Sets the specified fields to their respective values in the hash stored at key. - /// This command overwrites any specified fields that already exist in the hash, leaving other unspecified fields untouched. - /// If key does not exist, a new key holding a hash is created. - /// - /// The key of the hash. - /// The entries to set in the hash. - /// The flags to use for this operation. - /// + /// Task HashSetAsync(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None); - /// - /// Sets field in the hash stored at key to value. - /// If key does not exist, a new key holding a hash is created. - /// If field already exists in the hash, it is overwritten. - /// - /// The key of the hash. - /// The field to set in the hash. - /// The value to set. - /// Which conditions under which to set the field value (defaults to always). - /// The flags to use for this operation. - /// if field is a new field in the hash and value was set, if field already exists in the hash and the value was updated. - /// - /// , - /// - /// + /// Task HashSetAsync(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); - /// - /// Returns the string length of the value associated with field in the hash stored at key. - /// - /// The key of the hash. - /// The field containing the string - /// The flags to use for this operation. - /// The length of the string at field, or 0 when key does not exist. - /// + /// Task HashStringLengthAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); - /// - /// Returns all values in the hash stored at key. - /// - /// The key of the hash. - /// The flags to use for this operation. - /// List of values in the hash, or an empty list when key does not exist. - /// + /// Task HashValuesAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Adds the element to the HyperLogLog data structure stored at the variable name specified as first argument. - /// - /// The key of the hyperloglog. - /// The value to add. - /// The flags to use for this operation. - /// if at least 1 HyperLogLog internal register was altered, otherwise. - /// + /// Task HyperLogLogAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); - /// - /// Adds all the element arguments to the HyperLogLog data structure stored at the variable name specified as first argument. - /// - /// The key of the hyperloglog. - /// The values to add. - /// The flags to use for this operation. - /// if at least 1 HyperLogLog internal register was altered, otherwise. - /// + /// Task HyperLogLogAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); - /// - /// Returns the approximated cardinality computed by the HyperLogLog data structure stored at the specified variable, or 0 if the variable does not exist. - /// - /// The key of the hyperloglog. - /// The flags to use for this operation. - /// The approximated number of unique elements observed via HyperLogLogAdd. - /// + /// Task HyperLogLogLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Returns the approximated cardinality of the union of the HyperLogLogs passed, by internally merging the HyperLogLogs stored at the provided keys into a temporary hyperLogLog, or 0 if the variable does not exist. - /// - /// The keys of the hyperloglogs. - /// The flags to use for this operation. - /// The approximated number of unique elements observed via HyperLogLogAdd. - /// + /// Task HyperLogLogLengthAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); - /// - /// Merge multiple HyperLogLog values into an unique value that will approximate the cardinality of the union of the observed Sets of the source HyperLogLog structures. - /// - /// The key of the merged hyperloglog. - /// The key of the first hyperloglog to merge. - /// The key of the first hyperloglog to merge. - /// The flags to use for this operation. - /// + /// Task HyperLogLogMergeAsync(RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); - /// - /// Merge multiple HyperLogLog values into an unique value that will approximate the cardinality of the union of the observed Sets of the source HyperLogLog structures. - /// - /// The key of the merged hyperloglog. - /// The keys of the hyperloglogs to merge. - /// The flags to use for this operation. - /// + /// Task HyperLogLogMergeAsync(RedisKey destination, RedisKey[] sourceKeys, CommandFlags flags = CommandFlags.None); - /// - /// Indicate exactly which redis server we are talking to. - /// - /// The key to check. - /// The flags to use for this operation. - /// The endpoint serving the key. + /// Task IdentifyEndpointAsync(RedisKey key = default, CommandFlags flags = CommandFlags.None); - /// - /// Copies the value from the to the specified . - /// - /// The key of the source value to copy. - /// The destination key to copy the source to. - /// The database ID to store in. If default (-1), current database is used. - /// Whether to overwrite an existing values at . If and the key exists, the copy will not succeed. - /// The flags to use for this operation. - /// if key was copied. if key was not copied. - /// + /// Task KeyCopyAsync(RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase = -1, bool replace = false, CommandFlags flags = CommandFlags.None); - /// - /// Removes the specified key. A key is ignored if it does not exist. - /// If UNLINK is available (Redis 4.0+), it will be used. - /// - /// The key to delete. - /// The flags to use for this operation. - /// if the key was removed. - /// - /// , - /// - /// + /// Task KeyDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Removes the specified keys. A key is ignored if it does not exist. - /// If UNLINK is available (Redis 4.0+), it will be used. - /// - /// The keys to delete. - /// The flags to use for this operation. - /// The number of keys that were removed. - /// - /// , - /// - /// + /// Task KeyDeleteAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); - /// - /// Serialize the value stored at key in a Redis-specific format and return it to the user. - /// The returned value can be synthesized back into a Redis key using the RESTORE command. - /// - /// The key to dump. - /// The flags to use for this operation. - /// The serialized value. - /// + /// Task KeyDumpAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Returns the internal encoding for the Redis object stored at . - /// - /// The key to dump. - /// The flags to use for this operation. - /// The Redis encoding for the value or is the key does not exist. - /// + /// Task KeyEncodingAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Returns if key exists. - /// - /// The key to check. - /// The flags to use for this operation. - /// if the key exists. if the key does not exist. - /// + /// Task KeyExistsAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Indicates how many of the supplied keys exists. - /// - /// The keys to check. - /// The flags to use for this operation. - /// The number of keys that existed. - /// + /// Task KeyExistsAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); - /// - /// Set a timeout on . - /// After the timeout has expired, the key will automatically be deleted. - /// A key with an associated timeout is said to be volatile in Redis terminology. - /// - /// The key to set the expiration for. - /// The timeout to set. - /// The flags to use for this operation. - /// if the timeout was set. if key does not exist or the timeout could not be set. - /// - /// If key is updated before the timeout has expired, then the timeout is removed as if the PERSIST command was invoked on key. - /// - /// For Redis versions < 2.1.3, existing timeouts cannot be overwritten. - /// So, if key already has an associated timeout, it will do nothing and return 0. - /// - /// - /// Since Redis 2.1.3, you can update the timeout of a key. - /// It is also possible to remove the timeout using the PERSIST command. - /// See the page on key expiry for more information. - /// - /// - /// , - /// , - /// - /// - /// + /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags); - /// - /// Set a timeout on . - /// After the timeout has expired, the key will automatically be deleted. - /// A key with an associated timeout is said to be volatile in Redis terminology. - /// - /// The key to set the expiration for. - /// The timeout to set. - /// In Redis 7+, we choose under which condition the expiration will be set using . - /// The flags to use for this operation. - /// if the timeout was set. if key does not exist or the timeout could not be set. - /// - /// , - /// - /// + /// Task KeyExpireAsync(RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); - /// - /// Set a timeout on . - /// After the timeout has expired, the key will automatically be deleted. - /// A key with an associated timeout is said to be volatile in Redis terminology. - /// - /// The key to set the expiration for. - /// The exact date to expiry to set. - /// The flags to use for this operation. - /// if the timeout was set. if key does not exist or the timeout could not be set. - /// - /// If key is updated before the timeout has expired, then the timeout is removed as if the PERSIST command was invoked on key. - /// - /// For Redis versions < 2.1.3, existing timeouts cannot be overwritten. - /// So, if key already has an associated timeout, it will do nothing and return 0. - /// - /// - /// Since Redis 2.1.3, you can update the timeout of a key. - /// It is also possible to remove the timeout using the PERSIST command. - /// See the page on key expiry for more information. - /// - /// - /// , - /// , - /// - /// - /// + /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task KeyExpireAsync(RedisKey key, DateTime? expiry, CommandFlags flags); - /// - /// Set a timeout on . - /// After the timeout has expired, the key will automatically be deleted. - /// A key with an associated timeout is said to be volatile in Redis terminology. - /// - /// The key to set the expiration for. - /// The timeout to set. - /// In Redis 7+, we choose under which condition the expiration will be set using . - /// The flags to use for this operation. - /// if the timeout was set. if key does not exist or the timeout could not be set. - /// - /// , - /// - /// + /// Task KeyExpireAsync(RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); - /// - /// Returns the absolute time at which the given will expire, if it exists and has an expiration. - /// - /// The key to get the expiration for. - /// The flags to use for this operation. - /// The time at which the given key will expire, or if the key does not exist or has no associated expiration time. - /// - /// , - /// - /// + /// Task KeyExpireTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Returns the logarithmic access frequency counter of the object stored at . - /// The command is only available when the maxmemory-policy configuration directive is set to - /// one of the LFU policies. - /// - /// The key to get a frequency count for. - /// The flags to use for this operation. - /// The number of logarithmic access frequency counter, ( if the key does not exist). - /// + /// Task KeyFrequencyAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Returns the time since the object stored at the specified key is idle (not requested by read or write operations). - /// - /// The key to get the time of. - /// The flags to use for this operation. - /// The time since the object stored at the specified key is idle. - /// + /// Task KeyIdleTimeAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Move key from the currently selected database (see SELECT) to the specified destination database. - /// When key already exists in the destination database, or it does not exist in the source database, it does nothing. - /// It is possible to use MOVE as a locking primitive because of this. - /// - /// The key to move. - /// The database to move the key to. - /// The flags to use for this operation. - /// if key was moved. if key was not moved. - /// + /// Task KeyMoveAsync(RedisKey key, int database, CommandFlags flags = CommandFlags.None); - /// - /// Remove the existing timeout on key, turning the key from volatile (a key with an expire set) to persistent (a key that will never expire as no timeout is associated). - /// - /// The key to persist. - /// The flags to use for this operation. - /// if the timeout was removed. if key does not exist or does not have an associated timeout. - /// + /// Task KeyPersistAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Return a random key from the currently selected database. - /// - /// The flags to use for this operation. - /// The random key, or when the database is empty. - /// + /// Task KeyRandomAsync(CommandFlags flags = CommandFlags.None); - /// - /// Returns the reference count of the object stored at . - /// - /// The key to get a reference count for. - /// The flags to use for this operation. - /// The number of references ( if the key does not exist). - /// + /// Task KeyRefCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Renames to . - /// It returns an error when the source and destination names are the same, or when key does not exist. - /// - /// The key to rename. - /// The key to rename to. - /// What conditions to rename under (defaults to always). - /// The flags to use for this operation. - /// if the key was renamed, otherwise. - /// - /// , - /// - /// + /// Task KeyRenameAsync(RedisKey key, RedisKey newKey, When when = When.Always, CommandFlags flags = CommandFlags.None); - /// - /// Create a key associated with a value that is obtained by deserializing the provided serialized value (obtained via DUMP). - /// If is 0 the key is created without any expire, otherwise the specified expire time (in milliseconds) is set. - /// - /// The key to restore. - /// The value of the key. - /// The expiry to set. - /// The flags to use for this operation. - /// + /// Task KeyRestoreAsync(RedisKey key, byte[] value, TimeSpan? expiry = null, CommandFlags flags = CommandFlags.None); - /// - /// Returns the remaining time to live of a key that has a timeout. - /// This introspection capability allows a Redis client to check how many seconds a given key will continue to be part of the dataset. - /// - /// The key to check. - /// The flags to use for this operation. - /// TTL, or when key does not exist or does not have a timeout. - /// + /// Task KeyTimeToLiveAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Alters the last access time of a key. - /// - /// The key to touch. - /// The flags to use for this operation. - /// if the key was touched, otherwise. - /// + /// Task KeyTouchAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Alters the last access time of the specified . A key is ignored if it does not exist. - /// - /// The keys to touch. - /// The flags to use for this operation. - /// The number of keys that were touched. - /// + /// Task KeyTouchAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); - /// - /// Returns the string representation of the type of the value stored at key. - /// The different types that can be returned are: string, list, set, zset and hash. - /// - /// The key to get the type of. - /// The flags to use for this operation. - /// Type of key, or none when key does not exist. - /// + /// Task KeyTypeAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Returns the element at index in the list stored at key. - /// The index is zero-based, so 0 means the first element, 1 the second element and so on. - /// Negative indices can be used to designate elements starting at the tail of the list. - /// Here, -1 means the last element, -2 means the penultimate and so forth. - /// - /// The key of the list. - /// The index position to get the value at. - /// The flags to use for this operation. - /// The requested element, or when index is out of range. - /// + /// Task ListGetByIndexAsync(RedisKey key, long index, CommandFlags flags = CommandFlags.None); - /// - /// Inserts value in the list stored at key either before or after the reference value pivot. - /// When key does not exist, it is considered an empty list and no operation is performed. - /// - /// The key of the list. - /// The value to insert after. - /// The value to insert. - /// The flags to use for this operation. - /// The length of the list after the insert operation, or -1 when the value pivot was not found. - /// + /// Task ListInsertAfterAsync(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None); - /// - /// Inserts value in the list stored at key either before or after the reference value pivot. - /// When key does not exist, it is considered an empty list and no operation is performed. - /// - /// The key of the list. - /// The value to insert before. - /// The value to insert. - /// The flags to use for this operation. - /// The length of the list after the insert operation, or -1 when the value pivot was not found. - /// + /// Task ListInsertBeforeAsync(RedisKey key, RedisValue pivot, RedisValue value, CommandFlags flags = CommandFlags.None); - /// - /// Removes and returns the first element of the list stored at key. - /// - /// The key of the list. - /// The flags to use for this operation. - /// The value of the first element, or when key does not exist. - /// + /// Task ListLeftPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Removes and returns count elements from the head of the list stored at key. - /// If the list contains less than count elements, removes and returns the number of elements in the list. - /// - /// The key of the list. - /// The number of elements to remove - /// The flags to use for this operation. - /// Array of values that were popped, or if the key doesn't exist. - /// + /// Task ListLeftPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); - /// - /// Removes and returns at most elements from the first non-empty list in . - /// Starts on the left side of the list. - /// - /// The keys to look through for elements to pop. - /// The maximum number of elements to pop from the list. - /// The flags to use for this operation. - /// A span of contiguous elements from the list, or if no non-empty lists are found. - /// + /// Task ListLeftPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None); - /// - /// Scans through the list stored at looking for , returning the 0-based - /// index of the first matching element. - /// - /// The key of the list. - /// The element to search for. - /// The rank of the first element to return, within the sub-list of matching indexes in the case of multiple matches. - /// The maximum number of elements to scan through before stopping, defaults to 0 (a full scan of the list.) - /// The flags to use for this operation. - /// The 0-based index of the first matching element, or -1 if not found. - /// + /// Task ListPositionAsync(RedisKey key, RedisValue element, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None); - /// - /// Scans through the list stored at looking for instances of , returning the 0-based - /// indexes of any matching elements. - /// - /// The key of the list. - /// The element to search for. - /// The number of matches to find. A count of 0 will return the indexes of all occurrences of the element. - /// The rank of the first element to return, within the sub-list of matching indexes in the case of multiple matches. - /// The maximum number of elements to scan through before stopping, defaults to 0 (a full scan of the list.) - /// The flags to use for this operation. - /// An array of at most of indexes of matching elements. If none are found, and empty array is returned. - /// + /// Task ListPositionsAsync(RedisKey key, RedisValue element, long count, long rank = 1, long maxLength = 0, CommandFlags flags = CommandFlags.None); - /// - /// Insert the specified value at the head of the list stored at key. - /// If key does not exist, it is created as empty list before performing the push operations. - /// - /// The key of the list. - /// The value to add to the head of the list. - /// Which conditions to add to the list under (defaults to always). - /// The flags to use for this operation. - /// The length of the list after the push operations. - /// - /// , - /// - /// + /// Task ListLeftPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); - /// - /// Insert the specified value at the head of the list stored at key. - /// If key does not exist, it is created as empty list before performing the push operations. - /// - /// The key of the list. - /// The value to add to the head of the list. - /// Which conditions to add to the list under (defaults to always). - /// The flags to use for this operation. - /// The length of the list after the push operations. - /// - /// , - /// - /// + /// Task ListLeftPushAsync(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); - /// - /// Insert all the specified values at the head of the list stored at key. - /// If key does not exist, it is created as empty list before performing the push operations. - /// Elements are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. - /// So for instance the command LPUSH mylist a b c will result into a list containing c as first element, b as second element and a as third element. - /// - /// The key of the list. - /// The values to add to the head of the list. - /// The flags to use for this operation. - /// The length of the list after the push operations. - /// + /// Task ListLeftPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags); - /// - /// Returns the length of the list stored at key. If key does not exist, it is interpreted as an empty list and 0 is returned. - /// - /// The key of the list. - /// The flags to use for this operation. - /// The length of the list at key. - /// + /// Task ListLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Returns and removes the first or last element of the list stored at , and pushes the element - /// as the first or last element of the list stored at . - /// - /// The key of the list to remove from. - /// The key of the list to move to. - /// What side of the list to remove from. - /// What side of the list to move to. - /// The flags to use for this operation. - /// The element being popped and pushed or if there is no element to move. - /// + /// Task ListMoveAsync(RedisKey sourceKey, RedisKey destinationKey, ListSide sourceSide, ListSide destinationSide, CommandFlags flags = CommandFlags.None); - /// - /// Returns the specified elements of the list stored at key. - /// The offsets start and stop are zero-based indexes, with 0 being the first element of the list (the head of the list), 1 being the next element and so on. - /// These offsets can also be negative numbers indicating offsets starting at the end of the list.For example, -1 is the last element of the list, -2 the penultimate, and so on. - /// Note that if you have a list of numbers from 0 to 100, LRANGE list 0 10 will return 11 elements, that is, the rightmost item is included. - /// - /// The key of the list. - /// The start index of the list. - /// The stop index of the list. - /// The flags to use for this operation. - /// List of elements in the specified range. - /// + /// Task ListRangeAsync(RedisKey key, long start = 0, long stop = -1, CommandFlags flags = CommandFlags.None); - /// - /// Removes the first count occurrences of elements equal to value from the list stored at key. - /// The count argument influences the operation in the following ways: - /// - /// count > 0: Remove elements equal to value moving from head to tail. - /// count < 0: Remove elements equal to value moving from tail to head. - /// count = 0: Remove all elements equal to value. - /// - /// - /// The key of the list. - /// The value to remove from the list. - /// The count behavior (see method summary). - /// The flags to use for this operation. - /// The number of removed elements. - /// + /// Task ListRemoveAsync(RedisKey key, RedisValue value, long count = 0, CommandFlags flags = CommandFlags.None); - /// - /// Removes and returns the last element of the list stored at key. - /// - /// The key of the list. - /// The flags to use for this operation. - /// The element being popped, or when key does not exist.. - /// + /// Task ListRightPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Removes and returns count elements from the end the list stored at key. - /// If the list contains less than count elements, removes and returns the number of elements in the list. - /// - /// The key of the list. - /// The number of elements to pop - /// The flags to use for this operation. - /// Array of values that were popped, or if the key doesn't exist. - /// + /// Task ListRightPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); - /// - /// Removes and returns at most elements from the first non-empty list in . - /// Starts on the right side of the list. - /// - /// The keys to look through for elements to pop. - /// The maximum number of elements to pop from the list. - /// The flags to use for this operation. - /// A span of contiguous elements from the list, or if no non-empty lists are found. - /// + /// Task ListRightPopAsync(RedisKey[] keys, long count, CommandFlags flags = CommandFlags.None); - /// - /// Atomically returns and removes the last element (tail) of the list stored at source, and pushes the element at the first element (head) of the list stored at destination. - /// - /// The key of the source list. - /// The key of the destination list. - /// The flags to use for this operation. - /// The element being popped and pushed. - /// + /// Task ListRightPopLeftPushAsync(RedisKey source, RedisKey destination, CommandFlags flags = CommandFlags.None); - /// - /// Insert the specified value at the tail of the list stored at key. - /// If key does not exist, it is created as empty list before performing the push operation. - /// - /// The key of the list. - /// The value to add to the tail of the list. - /// Which conditions to add to the list under. - /// The flags to use for this operation. - /// The length of the list after the push operation. - /// - /// , - /// - /// + /// Task ListRightPushAsync(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); - /// - /// Insert the specified value at the tail of the list stored at key. - /// If key does not exist, it is created as empty list before performing the push operation. - /// - /// The key of the list. - /// The values to add to the tail of the list. - /// Which conditions to add to the list under. - /// The flags to use for this operation. - /// The length of the list after the push operation. - /// - /// , - /// - /// + /// Task ListRightPushAsync(RedisKey key, RedisValue[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); - /// - /// Insert all the specified values at the tail of the list stored at key. - /// If key does not exist, it is created as empty list before performing the push operation. - /// Elements are inserted one after the other to the tail of the list, from the leftmost element to the rightmost element. - /// So for instance the command RPUSH mylist a b c will result into a list containing a as first element, b as second element and c as third element. - /// - /// The key of the list. - /// The values to add to the tail of the list. - /// The flags to use for this operation. - /// The length of the list after the push operation. - /// + /// Task ListRightPushAsync(RedisKey key, RedisValue[] values, CommandFlags flags); - /// - /// Sets the list element at index to value. - /// For more information on the index argument, see . - /// An error is returned for out of range indexes. - /// - /// The key of the list. - /// The index to set the value at. - /// The values to add to the list. - /// The flags to use for this operation. - /// + /// Task ListSetByIndexAsync(RedisKey key, long index, RedisValue value, CommandFlags flags = CommandFlags.None); - /// - /// Trim an existing list so that it will contain only the specified range of elements specified. - /// Both start and stop are zero-based indexes, where 0 is the first element of the list (the head), 1 the next element and so on. - /// For example: LTRIM foobar 0 2 will modify the list stored at foobar so that only the first three elements of the list will remain. - /// start and end can also be negative numbers indicating offsets from the end of the list, where -1 is the last element of the list, -2 the penultimate element and so on. - /// - /// The key of the list. - /// The start index of the list to trim to. - /// The end index of the list to trim to. - /// The flags to use for this operation. - /// + /// Task ListTrimAsync(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None); - /// - /// Extends a lock, if the token value is correct. - /// - /// The key of the lock. - /// The value to set at the key. - /// The expiration of the lock key. - /// The flags to use for this operation. - /// if the lock was successfully extended. + /// Task LockExtendAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None); - /// - /// Queries the token held against a lock. - /// - /// The key of the lock. - /// The flags to use for this operation. - /// The current value of the lock, if any. + /// Task LockQueryAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Releases a lock, if the token value is correct. - /// - /// The key of the lock. - /// The value at the key that must match. - /// The flags to use for this operation. - /// if the lock was successfully released, otherwise. + /// Task LockReleaseAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); - /// - /// Takes a lock (specifying a token value) if it is not already taken. - /// - /// The key of the lock. - /// The value to set at the key. - /// The expiration of the lock key. - /// The flags to use for this operation. - /// if the lock was successfully taken, otherwise. + /// Task LockTakeAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None); - /// - /// Posts a message to the given channel. - /// - /// The channel to publish to. - /// The message to send. - /// The flags to use for this operation. - /// - /// The number of clients that received the message *on the destination server*, - /// note that this doesn't mean much in a cluster as clients can get the message through other nodes. - /// - /// + /// Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None); - /// - /// Execute an arbitrary command against the server; this is primarily intended for executing modules, - /// but may also be used to provide access to new features that lack a direct API. - /// - /// The command to run. - /// The arguments to pass for the command. - /// A dynamic representation of the command's result. - /// This API should be considered an advanced feature; inappropriate use can be harmful. + /// Task ExecuteAsync(string command, params object[] args); - /// - /// Execute an arbitrary command against the server; this is primarily intended for executing modules, - /// but may also be used to provide access to new features that lack a direct API. - /// - /// The command to run. - /// The arguments to pass for the command. - /// The flags to use for this operation. - /// A dynamic representation of the command's result. - /// This API should be considered an advanced feature; inappropriate use can be harmful. + /// Task ExecuteAsync(string command, ICollection? args, CommandFlags flags = CommandFlags.None); - /// - /// Execute a Lua script against the server. - /// - /// The script to execute. - /// The keys to execute against. - /// The values to execute against. - /// The flags to use for this operation. - /// A dynamic representation of the script's result. - /// - /// , - /// - /// + /// Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); - /// - /// Execute a Lua script against the server using just the SHA1 hash. - /// - /// The hash of the script to execute. - /// The keys to execute against. - /// The values to execute against. - /// The flags to use for this operation. - /// A dynamic representation of the script's result. - /// - /// Be aware that this method is not resilient to Redis server restarts. Use instead. - /// - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] Task ScriptEvaluateAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); - /// - /// Execute a lua script against the server, using previously prepared script. - /// Named parameters, if any, are provided by the `parameters` object. - /// - /// The script to execute. - /// The parameters to pass to the script. - /// The flags to use for this operation. - /// A dynamic representation of the script's result. - /// + /// Task ScriptEvaluateAsync(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None); - /// - /// Execute a lua script against the server, using previously prepared and loaded script. - /// This method sends only the SHA1 hash of the lua script to Redis. - /// Named parameters, if any, are provided by the `parameters` object. - /// - /// The already-loaded script to execute. - /// The parameters to pass to the script. - /// The flags to use for this operation. - /// A dynamic representation of the script's result. - /// + /// Task ScriptEvaluateAsync(LoadedLuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None); - /// - /// Read-only variant of the EVAL command that cannot execute commands that modify data, Execute a Lua script against the server. - /// - /// The script to execute. - /// The keys to execute against. - /// The values to execute against. - /// The flags to use for this operation. - /// A dynamic representation of the script's result. - /// - /// , - /// - /// + /// Task ScriptEvaluateReadOnlyAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); - /// - /// Read-only variant of the EVALSHA command that cannot execute commands that modify data, Execute a Lua script against the server using just the SHA1 hash. - /// - /// The hash of the script to execute. - /// The keys to execute against. - /// The values to execute against. - /// The flags to use for this operation. - /// A dynamic representation of the script's result. - /// + /// Task ScriptEvaluateReadOnlyAsync(byte[] hash, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None); - /// - /// Add the specified member to the set stored at key. - /// Specified members that are already a member of this set are ignored. - /// If key does not exist, a new set is created before adding the specified members. - /// - /// The key of the set. - /// The value to add to the set. - /// The flags to use for this operation. - /// if the specified member was not already present in the set, else . - /// + /// Task SetAddAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); - /// - /// Add the specified members to the set stored at key. - /// Specified members that are already a member of this set are ignored. - /// If key does not exist, a new set is created before adding the specified members. - /// - /// The key of the set. - /// The values to add to the set. - /// The flags to use for this operation. - /// The number of elements that were added to the set, not including all the elements already present into the set. - /// + /// Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); - /// - /// Returns the members of the set resulting from the specified operation against the given sets. - /// - /// The operation to perform. - /// The key of the first set. - /// The key of the second set. - /// The flags to use for this operation. - /// List with members of the resulting set. - /// - /// , - /// , - /// - /// + /// Task SetCombineAsync(SetOperation operation, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); - /// - /// Returns the members of the set resulting from the specified operation against the given sets. - /// - /// The operation to perform. - /// The keys of the sets to operate on. - /// The flags to use for this operation. - /// List with members of the resulting set. - /// - /// , - /// , - /// - /// + /// Task SetCombineAsync(SetOperation operation, RedisKey[] keys, CommandFlags flags = CommandFlags.None); - /// - /// This command is equal to SetCombine, but instead of returning the resulting set, it is stored in destination. - /// If destination already exists, it is overwritten. - /// - /// The operation to perform. - /// The key of the destination set. - /// The key of the first set. - /// The key of the second set. - /// The flags to use for this operation. - /// The number of elements in the resulting set. - /// - /// , - /// , - /// - /// + /// Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); - /// - /// This command is equal to SetCombine, but instead of returning the resulting set, it is stored in destination. - /// If destination already exists, it is overwritten. - /// - /// The operation to perform. - /// The key of the destination set. - /// The keys of the sets to operate on. - /// The flags to use for this operation. - /// The number of elements in the resulting set. - /// - /// , - /// , - /// - /// + /// Task SetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None); - /// - /// Returns whether is a member of the set stored at . - /// - /// The key of the set. - /// The value to check for. - /// The flags to use for this operation. - /// - /// if the element is a member of the set. - /// if the element is not a member of the set, or if key does not exist. - /// - /// + /// Task SetContainsAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); - /// - /// Returns whether each of is a member of the set stored at . - /// - /// The key of the set. - /// The members to check for. - /// The flags to use for this operation. - /// - /// if the element is a member of the set. - /// if the element is not a member of the set, or if key does not exist. - /// - /// + /// Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); - /// - /// - /// Returns the set cardinality (number of elements) of the intersection between the sets stored at the given . - /// - /// - /// If the intersection cardinality reaches partway through the computation, - /// the algorithm will exit and yield as the cardinality. - /// - /// - /// The keys of the sets. - /// The number of elements to check (defaults to 0 and means unlimited). - /// The flags to use for this operation. - /// The cardinality (number of elements) of the set, or 0 if key does not exist. - /// + /// Task SetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None); - /// - /// Returns the set cardinality (number of elements) of the set stored at key. - /// - /// The key of the set. - /// The flags to use for this operation. - /// The cardinality (number of elements) of the set, or 0 if key does not exist. - /// + /// Task SetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Returns all the members of the set value stored at key. - /// - /// The key of the set. - /// The flags to use for this operation. - /// All elements of the set. - /// + /// Task SetMembersAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Move member from the set at source to the set at destination. - /// This operation is atomic. In every given moment the element will appear to be a member of source or destination for other clients. - /// When the specified element already exists in the destination set, it is only removed from the source set. - /// - /// The key of the source set. - /// The key of the destination set. - /// The value to move. - /// The flags to use for this operation. - /// - /// if the element is moved. - /// if the element is not a member of source and no operation was performed. - /// - /// + /// Task SetMoveAsync(RedisKey source, RedisKey destination, RedisValue value, CommandFlags flags = CommandFlags.None); - /// - /// Removes and returns a random element from the set value stored at key. - /// - /// The key of the set. - /// The flags to use for this operation. - /// The removed element, or when key does not exist. - /// + /// Task SetPopAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Removes and returns the specified number of random elements from the set value stored at key. - /// - /// The key of the set. - /// The number of elements to return. - /// The flags to use for this operation. - /// An array of elements, or an empty array when key does not exist. - /// + /// Task SetPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); - /// - /// Return a random element from the set value stored at . - /// - /// The key of the set. - /// The flags to use for this operation. - /// The randomly selected element, or when does not exist. - /// + /// Task SetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Return an array of count distinct elements if count is positive. - /// If called with a negative count the behavior changes and the command is allowed to return the same element multiple times. - /// In this case the number of returned elements is the absolute value of the specified count. - /// - /// The key of the set. - /// The count of members to get. - /// The flags to use for this operation. - /// An array of elements, or an empty array when does not exist. - /// + /// Task SetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); - /// - /// Remove the specified member from the set stored at key. - /// Specified members that are not a member of this set are ignored. - /// - /// The key of the set. - /// The value to remove. - /// The flags to use for this operation. - /// if the specified member was already present in the set, otherwise. - /// + /// Task SetRemoveAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); - /// - /// Remove the specified members from the set stored at key. - /// Specified members that are not a member of this set are ignored. - /// - /// The key of the set. - /// The values to remove. - /// The flags to use for this operation. - /// The number of members that were removed from the set, not including non existing members. - /// + /// Task SetRemoveAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None); - /// - /// The SSCAN command is used to incrementally iterate over set. - /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to . - /// - /// The key of the set. - /// The pattern to match. - /// The page size to iterate by. - /// The cursor position to start at. - /// The page offset to start at. - /// The flags to use for this operation. - /// Yields all matching elements of the set. - /// + /// IAsyncEnumerable SetScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); - /// - /// Sorts a list, set or sorted set (numerically or alphabetically, ascending by default). - /// By default, the elements themselves are compared, but the values can also be used to perform external key-lookups using the by parameter. - /// By default, the elements themselves are returned, but external key-lookups (one or many) can be performed instead by specifying - /// the get parameter (note that # specifies the element itself, when used in get). - /// Referring to the redis SORT documentation for examples is recommended. - /// When used in hashes, by and get can be used to specify fields using -> notation (again, refer to redis documentation). - /// Uses SORT_RO when possible. - /// - /// The key of the list, set, or sorted set. - /// How many entries to skip on the return. - /// How many entries to take on the return. - /// The ascending or descending order (defaults to ascending). - /// The sorting method (defaults to numeric). - /// The key pattern to sort by, if any. e.g. ExternalKey_* would sort by ExternalKey_{listvalue} as a lookup. - /// The key pattern to sort by, if any e.g. ExternalKey_* would return the value of ExternalKey_{listvalue} for each entry. - /// The flags to use for this operation. - /// The sorted elements, or the external values if get is specified. - /// - /// , - /// - /// + /// Task SortAsync(RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); - /// - /// Sorts a list, set or sorted set (numerically or alphabetically, ascending by default). - /// By default, the elements themselves are compared, but the values can also be used to perform external key-lookups using the by parameter. - /// By default, the elements themselves are returned, but external key-lookups (one or many) can be performed instead by specifying - /// the get parameter (note that # specifies the element itself, when used in get). - /// Referring to the redis SORT documentation for examples is recommended. - /// When used in hashes, by and get can be used to specify fields using -> notation (again, refer to redis documentation). - /// - /// The destination key to store results in. - /// The key of the list, set, or sorted set. - /// How many entries to skip on the return. - /// How many entries to take on the return. - /// The ascending or descending order (defaults to ascending). - /// The sorting method (defaults to numeric). - /// The key pattern to sort by, if any. e.g. ExternalKey_* would sort by ExternalKey_{listvalue} as a lookup. - /// The key pattern to sort by, if any e.g. ExternalKey_* would return the value of ExternalKey_{listvalue} for each entry. - /// The flags to use for this operation. - /// The number of elements stored in the new list. - /// + /// Task SortAndStoreAsync(RedisKey destination, RedisKey key, long skip = 0, long take = -1, Order order = Order.Ascending, SortType sortType = SortType.Numeric, RedisValue by = default, RedisValue[]? get = null, CommandFlags flags = CommandFlags.None); - /// + /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, CommandFlags flags); - /// + /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, When when, CommandFlags flags = CommandFlags.None); - /// - /// Adds the specified member with the specified score to the sorted set stored at key. - /// If the specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. - /// - /// The key of the sorted set. - /// The member to add to the sorted set. - /// The score for the member to add to the sorted set. - /// What conditions to add the element under (defaults to always). - /// The flags to use for this operation. - /// if the value was added. if it already existed (the score is still updated). - /// + /// Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None); - /// + /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, CommandFlags flags); - /// + /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, When when, CommandFlags flags = CommandFlags.None); - /// - /// Adds all the specified members with the specified scores to the sorted set stored at key. - /// If a specified member is already a member of the sorted set, the score is updated and the element reinserted at the right position to ensure the correct ordering. - /// - /// The key of the sorted set. - /// The members and values to add to the sorted set. - /// What conditions to add the element under (defaults to always). - /// The flags to use for this operation. - /// The number of elements added to the sorted sets, not including elements already existing for which the score was updated. - /// + /// Task SortedSetAddAsync(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None); - /// - /// Computes a set operation for multiple sorted sets (optionally using per-set ), - /// optionally performing a specific aggregation (defaults to ). - /// cannot be used with weights or aggregation. - /// - /// The operation to perform. - /// The keys of the sorted sets. - /// The optional weights per set that correspond to . - /// The aggregation method (defaults to ). - /// The flags to use for this operation. - /// The resulting sorted set. - /// - /// , - /// , - /// - /// + /// Task SortedSetCombineAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); - /// - /// Computes a set operation for multiple sorted sets (optionally using per-set ), - /// optionally performing a specific aggregation (defaults to ). - /// cannot be used with weights or aggregation. - /// - /// The operation to perform. - /// The keys of the sorted sets. - /// The optional weights per set that correspond to . - /// The aggregation method (defaults to ). - /// The flags to use for this operation. - /// The resulting sorted set with scores. - /// - /// , - /// , - /// - /// + /// Task SortedSetCombineWithScoresAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); - /// - /// Computes a set operation over two sorted sets, and stores the result in destination, optionally performing - /// a specific aggregation (defaults to sum). - /// cannot be used with aggregation. - /// - /// The operation to perform. - /// The key to store the results in. - /// The key of the first sorted set. - /// The key of the second sorted set. - /// The aggregation method (defaults to sum). - /// The flags to use for this operation. - /// The number of elements in the resulting sorted set at destination. - /// - /// , - /// , - /// - /// + /// Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey first, RedisKey second, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); - /// - /// Computes a set operation over multiple sorted sets (optionally using per-set weights), and stores the result in destination, optionally performing - /// a specific aggregation (defaults to sum). - /// cannot be used with aggregation. - /// - /// The operation to perform. - /// The key to store the results in. - /// The keys of the sorted sets. - /// The optional weights per set that correspond to . - /// The aggregation method (defaults to sum). - /// The flags to use for this operation. - /// The number of elements in the resulting sorted set at destination. - /// - /// , - /// , - /// - /// - Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); - - /// - /// Decrements the score of member in the sorted set stored at key by decrement. - /// If member does not exist in the sorted set, it is added with -decrement as its score (as if its previous score was 0.0). - /// - /// The key of the sorted set. - /// The member to decrement. - /// The amount to decrement by. - /// The flags to use for this operation. - /// The new score of member. - /// + /// + Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None); + + /// Task SortedSetDecrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None); - /// - /// Increments the score of member in the sorted set stored at key by increment. If member does not exist in the sorted set, it is added with increment as its score (as if its previous score was 0.0). - /// - /// The key of the sorted set. - /// The member to increment. - /// The amount to increment by. - /// The flags to use for this operation. - /// The new score of member. - /// + /// Task SortedSetIncrementAsync(RedisKey key, RedisValue member, double value, CommandFlags flags = CommandFlags.None); - /// - /// Returns the cardinality of the intersection of the sorted sets at . - /// - /// The keys of the sorted sets. - /// If the intersection cardinality reaches partway through the computation, the algorithm will exit and yield as the cardinality (defaults to 0 meaning unlimited). - /// The flags to use for this operation. - /// The number of elements in the resulting intersection. - /// + /// Task SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None); - /// - /// Returns the sorted set cardinality (number of elements) of the sorted set stored at key. - /// - /// The key of the sorted set. - /// The min score to filter by (defaults to negative infinity). - /// The max score to filter by (defaults to positive infinity). - /// Whether to exclude and from the range check (defaults to both inclusive). - /// The flags to use for this operation. - /// The cardinality (number of elements) of the sorted set, or 0 if key does not exist. - /// + /// Task SortedSetLengthAsync(RedisKey key, double min = double.NegativeInfinity, double max = double.PositiveInfinity, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); - /// - /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering. - /// This command returns the number of elements in the sorted set at key with a value between min and max. - /// - /// The key of the sorted set. - /// The min value to filter by. - /// The max value to filter by. - /// Whether to exclude and from the range check (defaults to both inclusive). - /// The flags to use for this operation. - /// The number of elements in the specified score range. - /// + /// Task SortedSetLengthByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); - /// - /// Returns a random element from the sorted set value stored at . - /// - /// The key of the sorted set. - /// The flags to use for this operation. - /// The randomly selected element, or when does not exist. - /// + /// Task SortedSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Returns an array of random elements from the sorted set value stored at . - /// - /// The key of the sorted set. - /// - /// - /// If the provided count argument is positive, returns an array of distinct elements. - /// The array's length is either or the sorted set's cardinality (ZCARD), whichever is lower. - /// - /// - /// If called with a negative count, the behavior changes and the command is allowed to return the same element multiple times. - /// In this case, the number of returned elements is the absolute value of the specified count. - /// - /// - /// The flags to use for this operation. - /// The randomly selected elements, or an empty array when does not exist. - /// + /// Task SortedSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); - /// - /// Returns an array of random elements from the sorted set value stored at . - /// - /// The key of the sorted set. - /// - /// - /// If the provided count argument is positive, returns an array of distinct elements. - /// The array's length is either or the sorted set's cardinality (ZCARD), whichever is lower. - /// - /// - /// If called with a negative count, the behavior changes and the command is allowed to return the same element multiple times. - /// In this case, the number of returned elements is the absolute value of the specified count. - /// - /// - /// The flags to use for this operation. - /// The randomly selected elements with scores, or an empty array when does not exist. - /// + /// Task SortedSetRandomMembersWithScoresAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); - /// - /// Returns the specified range of elements in the sorted set stored at key. - /// By default the elements are considered to be ordered from the lowest to the highest score. - /// Lexicographical order is used for elements with equal score. - /// Both start and stop are zero-based indexes, where 0 is the first element, 1 is the next element and so on. - /// They can also be negative numbers indicating offsets from the end of the sorted set, with -1 being the last element of the sorted set, -2 the penultimate element and so on. - /// - /// The key of the sorted set. - /// The start index to get. - /// The stop index to get. - /// The order to sort by (defaults to ascending). - /// The flags to use for this operation. - /// List of elements in the specified range. - /// - /// , - /// - /// + /// Task SortedSetRangeByRankAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); - /// - /// Takes the specified range of elements in the sorted set of the - /// and stores them in a new sorted set at the . - /// - /// The sorted set to take the range from. - /// Where the resulting set will be stored. - /// The starting point in the sorted set. If is , this should be a string. - /// The stopping point in the range of the sorted set. If is , this should be a string. - /// The ordering criteria to use for the range. Choices are , , and (defaults to ). - /// Whether to exclude and from the range check (defaults to both inclusive). - /// - /// The direction to consider the and in. - /// If , the must be smaller than the . - /// If , must be smaller than . - /// - /// The number of elements into the sorted set to skip. Note: this iterates after sorting so incurs O(n) cost for large values. - /// The maximum number of elements to pull into the new () set. - /// The flags to use for this operation. - /// The cardinality of (number of elements in) the newly created sorted set. - /// + /// Task SortedSetRangeAndStoreAsync( RedisKey sourceKey, RedisKey destinationKey, @@ -1858,46 +476,12 @@ Task SortedSetRangeAndStoreAsync( long? take = null, CommandFlags flags = CommandFlags.None); - /// - /// Returns the specified range of elements in the sorted set stored at key. - /// By default the elements are considered to be ordered from the lowest to the highest score. - /// Lexicographical order is used for elements with equal score. - /// Both start and stop are zero-based indexes, where 0 is the first element, 1 is the next element and so on. - /// They can also be negative numbers indicating offsets from the end of the sorted set, with -1 being the last element of the sorted set, -2 the penultimate element and so on. - /// - /// The key of the sorted set. - /// The start index to get. - /// The stop index to get. - /// The order to sort by (defaults to ascending). - /// The flags to use for this operation. - /// List of elements in the specified range. - /// - /// , - /// - /// + /// Task SortedSetRangeByRankWithScoresAsync(RedisKey key, long start = 0, long stop = -1, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); - /// - /// Returns the specified range of elements in the sorted set stored at key. - /// By default the elements are considered to be ordered from the lowest to the highest score. - /// Lexicographical order is used for elements with equal score. - /// Start and stop are used to specify the min and max range for score values. - /// Similar to other range methods the values are inclusive. - /// - /// The key of the sorted set. - /// The minimum score to filter by. - /// The maximum score to filter by. - /// Which of and to exclude (defaults to both inclusive). - /// The order to sort by (defaults to ascending). - /// How many items to skip. - /// How many items to take. - /// The flags to use for this operation. - /// List of elements in the specified score range. - /// - /// , - /// - /// - Task SortedSetRangeByScoreAsync(RedisKey key, + /// + Task SortedSetRangeByScoreAsync( + RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, @@ -1906,27 +490,9 @@ Task SortedSetRangeByScoreAsync(RedisKey key, long take = -1, CommandFlags flags = CommandFlags.None); - /// - /// Returns the specified range of elements in the sorted set stored at key. - /// By default the elements are considered to be ordered from the lowest to the highest score. - /// Lexicographical order is used for elements with equal score. - /// Start and stop are used to specify the min and max range for score values. - /// Similar to other range methods the values are inclusive. - /// - /// The key of the sorted set. - /// The minimum score to filter by. - /// The maximum score to filter by. - /// Which of and to exclude (defaults to both inclusive). - /// The order to sort by (defaults to ascending). - /// How many items to skip. - /// How many items to take. - /// The flags to use for this operation. - /// List of elements in the specified score range. - /// - /// , - /// - /// - Task SortedSetRangeByScoreWithScoresAsync(RedisKey key, + /// + Task SortedSetRangeByScoreWithScoresAsync( + RedisKey key, double start = double.NegativeInfinity, double stop = double.PositiveInfinity, Exclude exclude = Exclude.None, @@ -1935,20 +501,9 @@ Task SortedSetRangeByScoreWithScoresAsync(RedisKey key, long take = -1, CommandFlags flags = CommandFlags.None); - /// - /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering. - /// This command returns all the elements in the sorted set at key with a value between min and max. - /// - /// The key of the sorted set. - /// The min value to filter by. - /// The max value to filter by. - /// Which of and to exclude (defaults to both inclusive). - /// How many items to skip. - /// How many items to take. - /// The flags to use for this operation. - /// List of elements in the specified score range. - /// - Task SortedSetRangeByValueAsync(RedisKey key, + /// + Task SortedSetRangeByValueAsync( + RedisKey key, RedisValue min, RedisValue max, Exclude exclude, @@ -1956,24 +511,9 @@ Task SortedSetRangeByValueAsync(RedisKey key, long take = -1, CommandFlags flags = CommandFlags.None); // defaults removed to avoid ambiguity with overload with order - /// - /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering. - /// This command returns all the elements in the sorted set at key with a value between min and max. - /// - /// The key of the sorted set. - /// The min value to filter by. - /// The max value to filter by. - /// Which of and to exclude (defaults to both inclusive). - /// Whether to order the data ascending or descending - /// How many items to skip. - /// How many items to take. - /// The flags to use for this operation. - /// List of elements in the specified score range. - /// - /// , - /// - /// - Task SortedSetRangeByValueAsync(RedisKey key, + /// + Task SortedSetRangeByValueAsync( + RedisKey key, RedisValue min = default, RedisValue max = default, Exclude exclude = Exclude.None, @@ -1982,924 +522,239 @@ Task SortedSetRangeByValueAsync(RedisKey key, long take = -1, CommandFlags flags = CommandFlags.None); - /// - /// Returns the rank of member in the sorted set stored at key, by default with the scores ordered from low to high. - /// The rank (or index) is 0-based, which means that the member with the lowest score has rank 0. - /// - /// The key of the sorted set. - /// The member to get the rank of. - /// The order to sort by (defaults to ascending). - /// The flags to use for this operation. - /// If member exists in the sorted set, the rank of member. If member does not exist in the sorted set or key does not exist, . - /// - /// , - /// - /// + /// Task SortedSetRankAsync(RedisKey key, RedisValue member, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); - /// - /// Removes the specified member from the sorted set stored at key. Non existing members are ignored. - /// - /// The key of the sorted set. - /// The member to remove. - /// The flags to use for this operation. - /// if the member existed in the sorted set and was removed. otherwise. - /// + /// Task SortedSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - /// - /// Removes the specified members from the sorted set stored at key. Non existing members are ignored. - /// - /// The key of the sorted set. - /// The members to remove. - /// The flags to use for this operation. - /// The number of members removed from the sorted set, not including non existing members. - /// + /// Task SortedSetRemoveAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); - /// - /// Removes all elements in the sorted set stored at key with rank between start and stop. - /// Both start and stop are 0 -based indexes with 0 being the element with the lowest score. - /// These indexes can be negative numbers, where they indicate offsets starting at the element with the highest score. - /// For example: -1 is the element with the highest score, -2 the element with the second highest score and so forth. - /// - /// The key of the sorted set. - /// The minimum rank to remove. - /// The maximum rank to remove. - /// The flags to use for this operation. - /// The number of elements removed. - /// + /// Task SortedSetRemoveRangeByRankAsync(RedisKey key, long start, long stop, CommandFlags flags = CommandFlags.None); - /// - /// Removes all elements in the sorted set stored at key with a score between min and max (inclusive by default). - /// - /// The key of the sorted set. - /// The minimum score to remove. - /// The maximum score to remove. - /// Which of and to exclude (defaults to both inclusive). - /// The flags to use for this operation. - /// The number of elements removed. - /// + /// Task SortedSetRemoveRangeByScoreAsync(RedisKey key, double start, double stop, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); - /// - /// When all the elements in a sorted set are inserted with the same score, in order to force lexicographical ordering. - /// This command removes all elements in the sorted set stored at key between the lexicographical range specified by min and max. - /// - /// The key of the sorted set. - /// The minimum value to remove. - /// The maximum value to remove. - /// Which of and to exclude (defaults to both inclusive). - /// The flags to use for this operation. - /// The number of elements removed. - /// + /// Task SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None); - /// - /// The ZSCAN command is used to incrementally iterate over a sorted set. - /// - /// The key of the sorted set. - /// The pattern to match. - /// The page size to iterate by. - /// The cursor position to start at. - /// The page offset to start at. - /// The flags to use for this operation. - /// Yields all matching elements of the sorted set. - /// - IAsyncEnumerable SortedSetScanAsync(RedisKey key, + /// + IAsyncEnumerable SortedSetScanAsync( + RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); - /// - /// Returns the score of member in the sorted set at key. - /// If member does not exist in the sorted set, or key does not exist, is returned. - /// - /// The key of the sorted set. - /// The member to get a score for. - /// The flags to use for this operation. - /// The score of the member. - /// + /// Task SortedSetScoreAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); - /// - /// Returns the scores of members in the sorted set at . - /// If a member does not exist in the sorted set, or key does not exist, is returned. - /// - /// The key of the sorted set. - /// The members to get a score for. - /// The flags to use for this operation. - /// - /// The scores of the members in the same order as the array. - /// If a member does not exist in the set, is returned. - /// - /// + /// Task SortedSetScoresAsync(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); - /// - /// Same as but return the number of the elements changed. - /// - /// The key of the sorted set. - /// The member to add/update to the sorted set. - /// The score for the member to add/update to the sorted set. - /// What conditions to add the element under (defaults to always). - /// The flags to use for this operation. - /// The number of elements changed. - /// + /// Task SortedSetUpdateAsync(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None); - /// - /// Same as but return the number of the elements changed. - /// - /// The key of the sorted set. - /// The members and values to add/update to the sorted set. - /// What conditions to add the element under (defaults to always). - /// The flags to use for this operation. - /// The number of elements changed. - /// + /// Task SortedSetUpdateAsync(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None); - /// - /// Removes and returns the first element from the sorted set stored at key, by default with the scores ordered from low to high. - /// - /// The key of the sorted set. - /// The order to sort by (defaults to ascending). - /// The flags to use for this operation. - /// The removed element, or when key does not exist. - /// - /// , - /// - /// + /// Task SortedSetPopAsync(RedisKey key, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); - /// - /// Removes and returns the specified number of first elements from the sorted set stored at key, by default with the scores ordered from low to high. - /// - /// The key of the sorted set. - /// The number of elements to return. - /// The order to sort by (defaults to ascending). - /// The flags to use for this operation. - /// An array of elements, or an empty array when key does not exist. - /// - /// , - /// - /// + /// Task SortedSetPopAsync(RedisKey key, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); - /// - /// Removes and returns up to entries from the first non-empty sorted set in . - /// Returns if none of the sets exist or contain any elements. - /// - /// The keys to check. - /// The maximum number of records to pop out of the sorted set. - /// The order to sort by when popping items out of the set. - /// The flags to use for the operation. - /// A contiguous collection of sorted set entries with the key they were popped from, or if no non-empty sorted sets are found. - /// + /// Task SortedSetPopAsync(RedisKey[] keys, long count, Order order = Order.Ascending, CommandFlags flags = CommandFlags.None); - /// - /// Allow the consumer to mark a pending message as correctly processed. Returns the number of messages acknowledged. - /// - /// The key of the stream. - /// The name of the consumer group that received the message. - /// The ID of the message to acknowledge. - /// The flags to use for this operation. - /// The number of messages acknowledged. - /// + /// Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags = CommandFlags.None); - /// - /// Allow the consumer to mark a pending message as correctly processed. Returns the number of messages acknowledged. - /// - /// The key of the stream. - /// The name of the consumer group that received the message. - /// The IDs of the messages to acknowledge. - /// The flags to use for this operation. - /// The number of messages acknowledged. - /// + /// Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); - /// - /// Adds an entry using the specified values to the given stream key. - /// If key does not exist, a new key holding a stream is created. - /// The command returns the ID of the newly created stream entry. - /// - /// The key of the stream. - /// The field name for the stream entry. - /// The value to set in the stream entry. - /// The ID to assign to the stream entry, defaults to an auto-generated ID ("*"). - /// The maximum length of the stream. - /// If true, the "~" argument is used to allow the stream to exceed max length by a small number. This improves performance when removing messages. - /// The flags to use for this operation. - /// The ID of the newly created message. - /// + /// Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); - /// - /// Adds an entry using the specified values to the given stream key. - /// If key does not exist, a new key holding a stream is created. - /// The command returns the ID of the newly created stream entry. - /// - /// The key of the stream. - /// The fields and their associated values to set in the stream entry. - /// The ID to assign to the stream entry, defaults to an auto-generated ID ("*"). - /// The maximum length of the stream. - /// If true, the "~" argument is used to allow the stream to exceed max length by a small number. This improves performance when removing messages. - /// The flags to use for this operation. - /// The ID of the newly created message. - /// + /// Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); - /// - /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. - /// Messages that have been idle for more than will be claimed. - /// - /// The key of the stream. - /// The consumer group. - /// The consumer claiming the messages(s). - /// The minimum idle time threshold for pending messages to be claimed. - /// The starting ID to scan for pending messages that have an idle time greater than . - /// The upper limit of the number of entries that the command attempts to claim. If , Redis will default the value to 100. - /// The flags to use for this operation. - /// An instance of . - /// + /// Task StreamAutoClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None); - /// - /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. - /// Messages that have been idle for more than will be claimed. - /// The result will contain the claimed message IDs instead of a instance. - /// - /// The key of the stream. - /// The consumer group. - /// The consumer claiming the messages(s). - /// The minimum idle time threshold for pending messages to be claimed. - /// The starting ID to scan for pending messages that have an idle time greater than . - /// The upper limit of the number of entries that the command attempts to claim. If , Redis will default the value to 100. - /// The flags to use for this operation. - /// An instance of . - /// + /// Task StreamAutoClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None); - /// - /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. - /// This method returns the complete message for the claimed message(s). - /// - /// The key of the stream. - /// The consumer group. - /// The consumer claiming the given message(s). - /// The minimum message idle time to allow the reassignment of the message(s). - /// The IDs of the messages to claim for the given consumer. - /// The flags to use for this operation. - /// The messages successfully claimed by the given consumer. - /// + /// Task StreamClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); - /// - /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. - /// This method returns the IDs for the claimed message(s). - /// - /// The key of the stream. - /// The consumer group. - /// The consumer claiming the given message(s). - /// The minimum message idle time to allow the reassignment of the message(s). - /// The IDs of the messages to claim for the given consumer. - /// The flags to use for this operation. - /// The message IDs for the messages successfully claimed by the given consumer. - /// + /// Task StreamClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); - /// - /// Set the position from which to read a stream for a consumer group. - /// - /// The key of the stream. - /// The name of the consumer group. - /// The position from which to read for the consumer group. - /// The flags to use for this operation. - /// if successful, otherwise. - /// + /// Task StreamConsumerGroupSetPositionAsync(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None); - /// - /// Retrieve information about the consumers for the given consumer group. - /// This is the equivalent of calling "XINFO GROUPS key group". - /// - /// The key of the stream. - /// The consumer group name. - /// The flags to use for this operation. - /// An instance of for each of the consumer group's consumers. - /// + /// Task StreamConsumerInfoAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); - /// - /// Create a consumer group for the given stream. - /// - /// The key of the stream. - /// The name of the group to create. - /// The position to begin reading the stream. Defaults to . - /// The flags to use for this operation. - /// if the group was created, otherwise. - /// + /// Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position, CommandFlags flags); - /// - /// Create a consumer group for the given stream. - /// - /// The key of the stream. - /// The name of the group to create. - /// The position to begin reading the stream. Defaults to . - /// Create the stream if it does not already exist. - /// The flags to use for this operation. - /// if the group was created, otherwise. - /// + /// Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None); - /// - /// Delete messages in the stream. This method does not delete the stream. - /// - /// The key of the stream. - /// The IDs of the messages to delete. - /// The flags to use for this operation. - /// Returns the number of messages successfully deleted from the stream. - /// + /// Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); - /// - /// Delete a consumer from a consumer group. - /// - /// The key of the stream. - /// The name of the consumer group. - /// The name of the consumer. - /// The flags to use for this operation. - /// The number of messages that were pending for the deleted consumer. - /// + /// Task StreamDeleteConsumerAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None); - /// - /// Delete a consumer group. - /// - /// The key of the stream. - /// The name of the consumer group. - /// The flags to use for this operation. - /// if deleted, otherwise. - /// + /// Task StreamDeleteConsumerGroupAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); - /// - /// Retrieve information about the groups created for the given stream. This is the equivalent of calling "XINFO GROUPS key". - /// - /// The key of the stream. - /// The flags to use for this operation. - /// An instance of for each of the stream's groups. - /// + /// Task StreamGroupInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Retrieve information about the given stream. This is the equivalent of calling "XINFO STREAM key". - /// - /// The key of the stream. - /// The flags to use for this operation. - /// A instance with information about the stream. - /// + /// Task StreamInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Return the number of entries in a stream. - /// - /// The key of the stream. - /// The flags to use for this operation. - /// The number of entries inside the given stream. - /// + /// Task StreamLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// View information about pending messages for a stream. - /// A pending message is a message read using StreamReadGroup (XREADGROUP) but not yet acknowledged. - /// - /// The key of the stream. - /// The name of the consumer group - /// The flags to use for this operation. - /// - /// An instance of . - /// contains the number of pending messages. - /// The highest and lowest ID of the pending messages, and the consumers with their pending message count. - /// - /// The equivalent of calling XPENDING key group. - /// + /// Task StreamPendingAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); - /// - /// View information about each pending message. - /// - /// The key of the stream. - /// The name of the consumer group. - /// The maximum number of pending messages to return. - /// The consumer name for the pending messages. Pass RedisValue.Null to include pending messages for all consumers. - /// The minimum ID from which to read the stream of pending messages. The method will default to reading from the beginning of the stream. - /// The maximum ID to read to within the stream of pending messages. The method will default to reading to the end of the stream. - /// The flags to use for this operation. - /// An instance of for each pending message. - /// Equivalent of calling XPENDING key group start-id end-id count consumer-name. - /// + /// Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None); - /// - /// Read a stream using the given range of IDs. - /// - /// The key of the stream. - /// The minimum ID from which to read the stream. The method will default to reading from the beginning of the stream. - /// The maximum ID to read to within the stream. The method will default to reading to the end of the stream. - /// The maximum number of messages to return. - /// The order of the messages. will execute XRANGE and will execute XREVRANGE. - /// The flags to use for this operation. - /// Returns an instance of for each message returned. - /// + /// Task StreamRangeAsync(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None); - /// - /// Read from a single stream. - /// - /// The key of the stream. - /// The position from which to read the stream. - /// The maximum number of messages to return. - /// The flags to use for this operation. - /// Returns an instance of for each message returned. - /// - /// Equivalent of calling XREAD COUNT num STREAMS key id. - /// - /// + /// Task StreamReadAsync(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None); - /// - /// Read from multiple streams. - /// - /// Array of streams and the positions from which to begin reading for each stream. - /// The maximum number of messages to return from each stream. - /// The flags to use for this operation. - /// A value of for each stream. - /// - /// Equivalent of calling XREAD COUNT num STREAMS key1 key2 id1 id2. - /// - /// + /// Task StreamReadAsync(StreamPosition[] streamPositions, int? countPerStream = null, CommandFlags flags = CommandFlags.None); - /// - /// Read messages from a stream into an associated consumer group. - /// - /// The key of the stream. - /// The name of the consumer group. - /// The consumer name. - /// The position from which to read the stream. Defaults to when . - /// The maximum number of messages to return. - /// The flags to use for this operation. - /// Returns a value of for each message returned. - /// + /// Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, CommandFlags flags); - /// - /// Read messages from a stream into an associated consumer group. - /// - /// The key of the stream. - /// The name of the consumer group. - /// The consumer name. - /// The position from which to read the stream. Defaults to when . - /// The maximum number of messages to return. - /// When true, the message will not be added to the pending message list. - /// The flags to use for this operation. - /// Returns a value of for each message returned. - /// + /// Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None); - /// - /// Read from multiple streams into the given consumer group. - /// The consumer group with the given will need to have been created for each stream prior to calling this method. - /// - /// Array of streams and the positions from which to begin reading for each stream. - /// The name of the consumer group. - /// - /// The maximum number of messages to return from each stream. - /// The flags to use for this operation. - /// A value of for each stream. - /// - /// Equivalent of calling XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2. - /// - /// + /// Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags); - /// - /// Read from multiple streams into the given consumer group. - /// The consumer group with the given will need to have been created for each stream prior to calling this method. - /// - /// Array of streams and the positions from which to begin reading for each stream. - /// The name of the consumer group. - /// - /// The maximum number of messages to return from each stream. - /// When true, the message will not be added to the pending message list. - /// The flags to use for this operation. - /// A value of for each stream. - /// - /// Equivalent of calling XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2. - /// - /// + /// Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None); - /// - /// Trim the stream to a specified maximum length. - /// - /// The key of the stream. - /// The maximum length of the stream. - /// If true, the "~" argument is used to allow the stream to exceed max length by a small number. This improves performance when removing messages. - /// The flags to use for this operation. - /// The number of messages removed from the stream. - /// + /// Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); - /// - /// If key already exists and is a string, this command appends the value at the end of the string. - /// If key does not exist it is created and set as an empty string, so APPEND will be similar to SET in this special case. - /// - /// The key of the string. - /// The value to append to the string. - /// The flags to use for this operation. - /// The length of the string after the append operation. - /// + /// Task StringAppendAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); - /// + /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task StringBitCountAsync(RedisKey key, long start, long end, CommandFlags flags); - /// - /// Count the number of set bits (population counting) in a string. - /// By default all the bytes contained in the string are examined. - /// It is possible to specify the counting operation only in an interval passing the additional arguments start and end. - /// Like for the GETRANGE command start and end can contain negative values in order to index bytes starting from the end of the string, where -1 is the last byte, -2 is the penultimate, and so forth. - /// - /// The key of the string. - /// The start byte to count at. - /// The end byte to count at. - /// In Redis 7+, we can choose if and specify a bit index or byte index (defaults to ). - /// The flags to use for this operation. - /// The number of bits set to 1. - /// + /// Task StringBitCountAsync(RedisKey key, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None); - /// - /// Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key. - /// The BITOP command supports four bitwise operations; note that NOT is a unary operator: the second key should be omitted in this case - /// and only the first key will be considered. - /// The result of the operation is always stored at . - /// - /// The operation to perform. - /// The destination key to store the result in. - /// The first key to get the bit value from. - /// The second key to get the bit value from. - /// The flags to use for this operation. - /// The size of the string stored in the destination key, that is equal to the size of the longest input string. - /// + /// Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey first, RedisKey second = default, CommandFlags flags = CommandFlags.None); - /// - /// Perform a bitwise operation between multiple keys (containing string values) and store the result in the destination key. - /// The BITOP command supports four bitwise operations; note that NOT is a unary operator. - /// The result of the operation is always stored at . - /// - /// The operation to perform. - /// The destination key to store the result in. - /// The keys to get the bit values from. - /// The flags to use for this operation. - /// The size of the string stored in the destination key, that is equal to the size of the longest input string. - /// + /// Task StringBitOperationAsync(Bitwise operation, RedisKey destination, RedisKey[] keys, CommandFlags flags = CommandFlags.None); - /// + /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task StringBitPositionAsync(RedisKey key, bool bit, long start, long end, CommandFlags flags); - /// - /// Return the position of the first bit set to 1 or 0 in a string. - /// The position is returned thinking at the string as an array of bits from left to right where the first byte most significant bit is at position 0, the second byte most significant bit is at position 8 and so forth. - /// A and may be specified - these are in bytes, not bits. - /// and can contain negative values in order to index bytes starting from the end of the string, where -1 is the last byte, -2 is the penultimate, and so forth. - /// - /// The key of the string. - /// True to check for the first 1 bit, false to check for the first 0 bit. - /// The position to start looking (defaults to 0). - /// The position to stop looking (defaults to -1, unlimited). - /// In Redis 7+, we can choose if and specify a bit index or byte index (defaults to ). - /// The flags to use for this operation. - /// - /// The command returns the position of the first bit set to 1 or 0 according to the request. - /// If we look for set bits(the bit argument is 1) and the string is empty or composed of just zero bytes, -1 is returned. - /// - /// + /// Task StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None); - /// - /// Decrements the number stored at key by decrement. - /// If the key does not exist, it is set to 0 before performing the operation. - /// An error is returned if the key contains a value of the wrong type or contains a string that is not representable as integer. - /// This operation is limited to 64 bit signed integers. - /// - /// The key of the string. - /// The amount to decrement by (defaults to 1). - /// The flags to use for this operation. - /// The value of key after the decrement. - /// - /// , - /// - /// + /// Task StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); - /// - /// Decrements the string representing a floating point number stored at key by the specified decrement. - /// If the key does not exist, it is set to 0 before performing the operation. - /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. - /// - /// The key of the string. - /// The amount to decrement by (defaults to 1). - /// The flags to use for this operation. - /// The value of key after the decrement. - /// + /// Task StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None); - /// - /// Get the value of key. If the key does not exist the special value is returned. - /// An error is returned if the value stored at key is not a string, because GET only handles string values. - /// - /// The key of the string. - /// The flags to use for this operation. - /// The value of key, or when key does not exist. - /// + /// Task StringGetAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Returns the values of all specified keys. - /// For every key that does not hold a string value or does not exist, the special value is returned. - /// - /// The keys of the strings. - /// The flags to use for this operation. - /// The values of the strings with for keys do not exist. - /// + /// Task StringGetAsync(RedisKey[] keys, CommandFlags flags = CommandFlags.None); - /// - /// Get the value of key. If the key does not exist the special value is returned. - /// An error is returned if the value stored at key is not a string, because GET only handles string values. - /// - /// The key of the string. - /// The flags to use for this operation. - /// The value of key, or when key does not exist. - /// + /// Task?> StringGetLeaseAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Returns the bit value at offset in the string value stored at key. - /// When offset is beyond the string length, the string is assumed to be a contiguous space with 0 bits. - /// - /// The key of the string. - /// The offset in the string to get a bit at. - /// The flags to use for this operation. - /// The bit value stored at offset. - /// + /// Task StringGetBitAsync(RedisKey key, long offset, CommandFlags flags = CommandFlags.None); - /// - /// Returns the substring of the string value stored at key, determined by the offsets start and end (both are inclusive). - /// Negative offsets can be used in order to provide an offset starting from the end of the string. - /// So -1 means the last character, -2 the penultimate and so forth. - /// - /// The key of the string. - /// The start index of the substring to get. - /// The end index of the substring to get. - /// The flags to use for this operation. - /// The substring of the string value stored at key. - /// + /// Task StringGetRangeAsync(RedisKey key, long start, long end, CommandFlags flags = CommandFlags.None); - /// - /// Atomically sets key to value and returns the old value stored at key. - /// - /// The key of the string. - /// The value to replace the existing value with. - /// The flags to use for this operation. - /// The old value stored at key, or when key did not exist. - /// + /// Task StringGetSetAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); - /// - /// Gets the value of and update its (relative) expiry. - /// If the key does not exist, the result will be . - /// - /// The key of the string. - /// The expiry to set. will remove expiry. - /// The flags to use for this operation. - /// The value of key, or when key does not exist. - /// + /// Task StringGetSetExpiryAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None); - /// - /// Gets the value of and update its (absolute) expiry. - /// If the key does not exist, the result will be . - /// - /// The key of the string. - /// The exact date and time to expire at. will remove expiry. - /// The flags to use for this operation. - /// The value of key, or when key does not exist. - /// + /// Task StringGetSetExpiryAsync(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None); - /// - /// Get the value of key and delete the key. - /// If the key does not exist the special value is returned. - /// An error is returned if the value stored at key is not a string, because GET only handles string values. - /// - /// The key of the string. - /// The flags to use for this operation. - /// The value of key, or when key does not exist. - /// + /// Task StringGetDeleteAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Get the value of key. - /// If the key does not exist the special value is returned. - /// An error is returned if the value stored at key is not a string, because GET only handles string values. - /// - /// The key of the string. - /// The flags to use for this operation. - /// The value of key and its expiry, or when key does not exist. - /// + /// Task StringGetWithExpiryAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Increments the number stored at key by increment. - /// If the key does not exist, it is set to 0 before performing the operation. - /// An error is returned if the key contains a value of the wrong type or contains a string that is not representable as integer. - /// This operation is limited to 64 bit signed integers. - /// - /// The key of the string. - /// The amount to increment by (defaults to 1). - /// The flags to use for this operation. - /// The value of key after the increment. - /// - /// , - /// - /// + /// Task StringIncrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); - /// - /// Increments the string representing a floating point number stored at key by the specified increment. - /// If the key does not exist, it is set to 0 before performing the operation. - /// The precision of the output is fixed at 17 digits after the decimal point regardless of the actual internal precision of the computation. - /// - /// The key of the string. - /// The amount to increment by (defaults to 1). - /// The flags to use for this operation. - /// The value of key after the increment. - /// + /// Task StringIncrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None); - /// - /// Returns the length of the string value stored at key. - /// - /// The key of the string. - /// The flags to use for this operation. - /// The length of the string at key, or 0 when key does not exist. - /// + /// Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); - /// - /// Implements the longest common subsequence algorithm between the values at and , - /// returning a string containing the common sequence. - /// Note that this is different than the longest common string algorithm, - /// since matching characters in the string does not need to be contiguous. - /// - /// The key of the first string. - /// The key of the second string. - /// The flags to use for this operation. - /// A string (sequence of characters) of the LCS match. - /// + /// Task StringLongestCommonSubsequenceAsync(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); - /// - /// Implements the longest common subsequence algorithm between the values at and , - /// returning the legnth of the common sequence. - /// Note that this is different than the longest common string algorithm, - /// since matching characters in the string does not need to be contiguous. - /// - /// The key of the first string. - /// The key of the second string. - /// The flags to use for this operation. - /// The length of the LCS match. - /// + /// t Task StringLongestCommonSubsequenceLengthAsync(RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); - /// - /// Implements the longest common subsequence algorithm between the values at and , - /// returning a list of all common sequences. - /// Note that this is different than the longest common string algorithm, - /// since matching characters in the string does not need to be contiguous. - /// - /// The key of the first string. - /// The key of the second string. - /// Can be used to restrict the list of matches to the ones of a given minimum length. - /// The flags to use for this operation. - /// The result of LCS algorithm, based on the given parameters. - /// + /// Task StringLongestCommonSubsequenceWithMatchesAsync(RedisKey first, RedisKey second, long minLength = 0, CommandFlags flags = CommandFlags.None); - /// + /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when); - /// + /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags); - /// - /// Set key to hold the string value. If key already holds a value, it is overwritten, regardless of its type. - /// - /// The key of the string. - /// The value to set. - /// The expiry to set. - /// Whether to maintain the existing key's TTL (KEEPTTL flag). - /// Which condition to set the value under (defaults to always). - /// The flags to use for this operation. - /// if the string was set, otherwise. - /// + /// Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); - /// - /// Sets the given keys to their respective values. - /// If is specified, this will not perform any operation at all even if just a single key already exists. - /// - /// The keys and values to set. - /// Which condition to set the value under (defaults to always). - /// The flags to use for this operation. - /// if the keys were set, otherwise. - /// - /// , - /// - /// + /// Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); - /// - /// Atomically sets key to value and returns the previous value (if any) stored at . - /// - /// The key of the string. - /// The value to set. - /// The expiry to set. - /// Which condition to set the value under (defaults to ). - /// The flags to use for this operation. - /// The previous value stored at , or when key did not exist. - /// - /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. - /// - /// + /// Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags); - /// - /// Atomically sets key to value and returns the previous value (if any) stored at . - /// - /// The key of the string. - /// The value to set. - /// The expiry to set. - /// Whether to maintain the existing key's TTL (KEEPTTL flag). - /// Which condition to set the value under (defaults to ). - /// The flags to use for this operation. - /// The previous value stored at , or when key did not exist. - /// This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command. - /// + /// Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); - /// - /// Sets or clears the bit at offset in the string value stored at key. - /// The bit is either set or cleared depending on value, which can be either 0 or 1. - /// When key does not exist, a new string value is created.The string is grown to make sure it can hold a bit at offset. - /// - /// The key of the string. - /// The offset in the string to set . - /// The bit value to set, true for 1, false for 0. - /// The flags to use for this operation. - /// The original bit value stored at offset. - /// + /// Task StringSetBitAsync(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None); - /// - /// Overwrites part of the string stored at key, starting at the specified offset, for the entire length of value. - /// If the offset is larger than the current length of the string at key, the string is padded with zero-bytes to make offset fit. - /// Non-existing keys are considered as empty strings, so this command will make sure it holds a string large enough to be able to set value at offset. - /// - /// The key of the string. - /// The offset in the string to overwrite. - /// The value to overwrite with. - /// The flags to use for this operation. - /// The length of the string after it was modified by the command. - /// + /// Task StringSetRangeAsync(RedisKey key, long offset, RedisValue value, CommandFlags flags = CommandFlags.None); } } diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 94baf5cd6..fad2d4232 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -34,7 +34,7 @@ public partial interface IServer : IRedis bool IsConnected { get; } /// - /// The protocol being used to communicate with this server (if not connected/known, then the anticipated protocol from the configuration is returned, assuming success) + /// The protocol being used to communicate with this server (if not connected/known, then the anticipated protocol from the configuration is returned, assuming success). /// RedisProtocol Protocol { get; } @@ -110,15 +110,14 @@ public partial interface IServer : IRedis /// /// The CLIENT KILL command closes multiple connections that match the specified filters. /// - /// - /// - /// + /// The filter to use in choosing which clients to kill. + /// The command flags to use. + /// the number of clients killed. long ClientKill(ClientKillFilter filter, CommandFlags flags = CommandFlags.None); /// Task ClientKillAsync(ClientKillFilter filter, CommandFlags flags = CommandFlags.None); - /// /// The CLIENT LIST command returns information and statistics about the client connections server in a mostly human readable format. /// @@ -350,8 +349,9 @@ public partial interface IServer : IRedis /// /// Warning: consider KEYS as a command that should only be used in production environments with extreme care. /// + /// See /// , - /// + /// . /// /// IEnumerable Keys(int database = -1, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); @@ -400,10 +400,11 @@ public partial interface IServer : IRedis /// The method of the save (e.g. background or foreground). /// The command flags to use. /// + /// See /// , /// , /// , - /// + /// . /// void Save(SaveType type, CommandFlags flags = CommandFlags.None); @@ -589,8 +590,9 @@ public partial interface IServer : IRedis /// /// The full text result of latency doctor. /// + /// See /// , - /// + /// . /// string LatencyDoctor(CommandFlags flags = CommandFlags.None); @@ -602,8 +604,9 @@ public partial interface IServer : IRedis /// /// The number of events that were reset. /// + /// See /// , - /// + /// . /// long LatencyReset(string[]? eventNames = null, CommandFlags flags = CommandFlags.None); @@ -615,8 +618,9 @@ public partial interface IServer : IRedis /// /// An array of latency history entries. /// + /// See /// , - /// + /// . /// LatencyHistoryEntry[] LatencyHistory(string eventName, CommandFlags flags = CommandFlags.None); @@ -628,8 +632,9 @@ public partial interface IServer : IRedis /// /// An array of the latest latency history entries. /// + /// See /// , - /// + /// . /// LatencyLatestEntry[] LatencyLatest(CommandFlags flags = CommandFlags.None); @@ -784,7 +789,7 @@ public partial interface IServer : IRedis internal static class IServerExtensions { /// - /// For testing only: Break the connection without mercy or thought + /// For testing only: Break the connection without mercy or thought. /// /// The server to simulate failure on. /// The type of failure(s) to simulate. diff --git a/src/StackExchange.Redis/Interfaces/ISubscriber.cs b/src/StackExchange.Redis/Interfaces/ISubscriber.cs index b81ed3af4..e0c509f49 100644 --- a/src/StackExchange.Redis/Interfaces/ISubscriber.cs +++ b/src/StackExchange.Redis/Interfaces/ISubscriber.cs @@ -52,8 +52,9 @@ public interface ISubscriber : IRedis /// The handler to invoke when a message is received on . /// The command flags to use. /// + /// See /// , - /// + /// . /// void Subscribe(RedisChannel channel, Action handler, CommandFlags flags = CommandFlags.None); @@ -65,10 +66,11 @@ public interface ISubscriber : IRedis /// /// The redis channel to subscribe to. /// The command flags to use. - /// A channel that represents this source + /// A channel that represents this source. /// + /// See /// , - /// + /// . /// ChannelMessageQueue Subscribe(RedisChannel channel, CommandFlags flags = CommandFlags.None); @@ -91,8 +93,9 @@ public interface ISubscriber : IRedis /// The handler to no longer invoke when a message is received on . /// The command flags to use. /// + /// See /// , - /// + /// . /// void Unsubscribe(RedisChannel channel, Action? handler = null, CommandFlags flags = CommandFlags.None); @@ -104,8 +107,9 @@ public interface ISubscriber : IRedis /// /// The command flags to use. /// + /// See /// , - /// + /// . /// void UnsubscribeAll(CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/InternalErrorEventArgs.cs b/src/StackExchange.Redis/InternalErrorEventArgs.cs index 8693d66f7..f664f4d62 100644 --- a/src/StackExchange.Redis/InternalErrorEventArgs.cs +++ b/src/StackExchange.Redis/InternalErrorEventArgs.cs @@ -25,12 +25,12 @@ internal InternalErrorEventArgs(EventHandler? handler, o /// This constructor is only for testing purposes. /// /// The source of the event. - /// + /// The endpoint (if any) involved in the event. /// Redis connection type. /// The exception that occurred. /// Origin. public InternalErrorEventArgs(object sender, EndPoint endpoint, ConnectionType connectionType, Exception exception, string origin) - : this (null, sender, endpoint, connectionType, exception, origin) + : this(null, sender, endpoint, connectionType, exception, origin) { } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index e34cad895..f6821af1d 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -117,7 +117,6 @@ public Task HashRandomFieldsAsync(RedisKey key, long count, Comman public Task HashRandomFieldsWithValuesAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => Inner.HashRandomFieldsWithValuesAsync(ToInner(key), count, flags); - public IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => Inner.HashScanAsync(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index d1c47aeab..e2b484935 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -33,7 +33,7 @@ public bool GeoAdd(RedisKey key, GeoEntry value, CommandFlags flags = CommandFla public bool GeoRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => Inner.GeoRemove(ToInner(key), member, flags); - public double? GeoDistance(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters,CommandFlags flags = CommandFlags.None) => + public double? GeoDistance(RedisKey key, RedisValue member1, RedisValue member2, GeoUnit unit = GeoUnit.Meters, CommandFlags flags = CommandFlags.None) => Inner.GeoDistance(ToInner(key), member1, member2, unit, flags); public string?[] GeoHash(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => @@ -48,7 +48,7 @@ public bool GeoRemove(RedisKey key, RedisValue member, CommandFlags flags = Comm public GeoPosition? GeoPosition(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => Inner.GeoPosition(ToInner(key), member, flags); - public GeoRadiusResult[] GeoRadius(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null,GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => + public GeoRadiusResult[] GeoRadius(RedisKey key, RedisValue member, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => Inner.GeoRadius(ToInner(key), member, radius, unit, count, order, options, flags); public GeoRadiusResult[] GeoRadius(RedisKey key, double longitude, double latitude, double radius, GeoUnit unit = GeoUnit.Meters, int count = -1, Order? order = null, GeoRadiusOptions options = GeoRadiusOptions.Default, CommandFlags flags = CommandFlags.None) => @@ -407,7 +407,7 @@ public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, CommandFlags fla public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetAdd(ToInner(key), values, when, flags); - public long SortedSetAdd(RedisKey key, SortedSetEntry[] values,SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + public long SortedSetAdd(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetAdd(ToInner(key), values, when, flags); public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandFlags flags) => @@ -510,7 +510,7 @@ public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue public double?[] SortedSetScores(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None) => Inner.SortedSetScores(ToInner(key), members, flags); - public long SortedSetUpdate(RedisKey key, SortedSetEntry[] values,SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => + public long SortedSetUpdate(RedisKey key, SortedSetEntry[] values, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetUpdate(ToInner(key), values, when, flags); public bool SortedSetUpdate(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => @@ -707,7 +707,7 @@ IEnumerable IDatabase.HashScan(RedisKey key, RedisValue pattern, int => Inner.HashScan(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); IEnumerable IDatabase.SetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) - => Inner.SetScan(ToInner(key), pattern, pageSize, flags); + => Inner.SetScan(ToInner(key), pattern, pageSize, flags); IEnumerable IDatabase.SetScan(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => Inner.SetScan(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); diff --git a/src/StackExchange.Redis/LuaScript.cs b/src/StackExchange.Redis/LuaScript.cs index 6e4ac7cd3..8a9bdbcc1 100644 --- a/src/StackExchange.Redis/LuaScript.cs +++ b/src/StackExchange.Redis/LuaScript.cs @@ -189,7 +189,7 @@ public LoadedLuaScript Load(IServer server, CommandFlags flags = CommandFlags.No /// Loads this LuaScript into the given IServer so it can be run with it's SHA1 hash, instead of /// using the implicit SHA1 hash that's calculated after the script is sent to the server for the first time. /// - /// Note: the FireAndForget command flag cannot be set + /// Note: the FireAndForget command flag cannot be set. /// /// The server to load the script on. /// The command flags to use. diff --git a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs index 330b27683..75bcb6de9 100644 --- a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs @@ -114,7 +114,7 @@ internal AzureMaintenanceEvent(string azureEvent) } } - internal async static Task AddListenerAsync(ConnectionMultiplexer multiplexer, Action? log = null) + internal static async Task AddListenerAsync(ConnectionMultiplexer multiplexer, Action? log = null) { if (!multiplexer.CommandMap.IsAvailable(RedisCommand.SUBSCRIBE)) { diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 3cdb4c997..876f718c2 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.Logging; -using StackExchange.Redis.Profiling; -using System; +using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; @@ -8,6 +6,8 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading; +using Microsoft.Extensions.Logging; +using StackExchange.Redis.Profiling; namespace StackExchange.Redis { @@ -166,7 +166,7 @@ internal void PrepareToResend(ServerEndPoint resendTo, bool isMoved) public virtual string CommandAndKey => Command.ToString(); /// - /// Things with the potential to cause harm, or to reveal configuration information + /// Things with the potential to cause harm, or to reveal configuration information. /// public bool IsAdmin { @@ -307,29 +307,79 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) => new CommandValueValueValueValueValueMessage(db, flags, command, value0, value1, value2, value3, value4); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, - in RedisKey key1, in RedisValue value0, in RedisValue value1) => + public static Message Create( + int db, + CommandFlags flags, + RedisCommand command, + in RedisKey key0, + in RedisKey key1, + in RedisValue value0, + in RedisValue value1) => new CommandKeyKeyValueValueMessage(db, flags, command, key0, key1, value0, value1); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, - in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2) => + public static Message Create( + int db, + CommandFlags flags, + RedisCommand command, + in RedisKey key0, + in RedisKey key1, + in RedisValue value0, + in RedisValue value1, + in RedisValue value2) => new CommandKeyKeyValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, - in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3) => + public static Message Create( + int db, + CommandFlags flags, + RedisCommand command, + in RedisKey key0, + in RedisKey key1, + in RedisValue value0, + in RedisValue value1, + in RedisValue value2, + in RedisValue value3) => new CommandKeyKeyValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2, value3); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, - in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) => + public static Message Create( + int db, + CommandFlags flags, + RedisCommand command, + in RedisKey key0, + in RedisKey key1, + in RedisValue value0, + in RedisValue value1, + in RedisValue value2, + in RedisValue value3, + in RedisValue value4) => new CommandKeyKeyValueValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2, value3, value4); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, - in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4, in RedisValue value5) => + public static Message Create( + int db, + CommandFlags flags, + RedisCommand command, + in RedisKey key0, + in RedisKey key1, + in RedisValue value0, + in RedisValue value1, + in RedisValue value2, + in RedisValue value3, + in RedisValue value4, + in RedisValue value5) => new CommandKeyKeyValueValueValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2, value3, value4, value5); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, - in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, - in RedisValue value4, in RedisValue value5, in RedisValue value6) => + public static Message Create( + int db, + CommandFlags flags, + RedisCommand command, + in RedisKey key0, + in RedisKey key1, + in RedisValue value0, + in RedisValue value1, + in RedisValue value2, + in RedisValue value3, + in RedisValue value4, + in RedisValue value5, + in RedisValue value6) => new CommandKeyKeyValueValueValueValueValueValueValueMessage(db, flags, command, key0, key1, value0, value1, value2, value3, value4, value5, value6); public static Message CreateInSlot(int db, int slot, CommandFlags flags, RedisCommand command, RedisValue[] values) => @@ -357,30 +407,34 @@ public virtual void AppendStormLog(StringBuilder sb) /// (i.e. "why does my standalone server keep saying ERR unknown command 'cluster' ?") /// 2: it allows the initial PING and GET (during connect) to get queued rather /// than be rejected as no-server-available (note that this doesn't apply to - /// handshake messages, as they bypass the queue completely) - /// 3: it disables non-pref logging, as it is usually server-targeted + /// handshake messages, as they bypass the queue completely). + /// 3: it disables non-pref logging, as it is usually server-targeted. /// public void SetInternalCall() => Flags |= InternalCallFlag; /// - /// Gets a string representation of this message: "[{DB}]:{CommandAndKey} ({resultProcessor})" + /// Gets a string representation of this message: "[{DB}]:{CommandAndKey} ({resultProcessor})". /// public override string ToString() => $"[{Db}]:{CommandAndKey} ({resultProcessor?.GetType().Name ?? "(n/a)"})"; /// - /// Gets a string representation of this message without the key: "[{DB}]:{Command} ({resultProcessor})" + /// Gets a string representation of this message without the key: "[{DB}]:{Command} ({resultProcessor})". /// public string ToStringCommandOnly() => $"[{Db}]:{Command} ({resultProcessor?.GetType().Name ?? "(n/a)"})"; public void SetResponseReceived() => performance?.SetResponseReceived(); - bool ICompletable.TryComplete(bool isAsync) { Complete(); return true; } + bool ICompletable.TryComplete(bool isAsync) + { + Complete(); + return true; + } public void Complete() { - //Ensure we can never call Complete on the same resultBox from two threads by grabbing it now + // Ensure we can never call Complete on the same resultBox from two threads by grabbing it now var currBox = Interlocked.Exchange(ref resultBox, null); // set the completion/performance data @@ -452,7 +506,7 @@ internal static Message Create(int db, CommandFlags flags, RedisCommand command, 3 => new CommandKeyKeyValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2]), 4 => new CommandKeyKeyValueValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2], values[3]), 5 => new CommandKeyKeyValueValueValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2], values[3], values[4]), - 6 => new CommandKeyKeyValueValueValueValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2], values[3],values[4],values[5]), + 6 => new CommandKeyKeyValueValueValueValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2], values[3], values[4], values[5]), 7 => new CommandKeyKeyValueValueValueValueValueValueValueMessage(db, flags, command, key0, key1, values[0], values[1], values[2], values[3], values[4], values[5], values[6]), _ => new CommandKeyKeyValuesMessage(db, flags, command, key0, key1, values), }; @@ -699,6 +753,7 @@ internal void SetSource(ResultProcessor? resultProcessor, IResultBox? resultBox) /// /// Note order here is reversed to prevent overload resolution errors. /// + /// The type of the result box result. internal void SetSource(IResultBox resultBox, ResultProcessor? resultProcessor) { this.resultBox = resultBox; @@ -1263,8 +1318,14 @@ private sealed class CommandKeyKeyValueValueMessage : CommandKeyBase private readonly RedisValue value0, value1; private readonly RedisKey key1; - public CommandKeyKeyValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, - in RedisKey key1, in RedisValue value0, in RedisValue value1) : base(db, flags, command, key0) + public CommandKeyKeyValueValueMessage( + int db, + CommandFlags flags, + RedisCommand command, + in RedisKey key0, + in RedisKey key1, + in RedisValue value0, + in RedisValue value1) : base(db, flags, command, key0) { key1.AssertNotNull(); value0.AssertNotNull(); @@ -1291,8 +1352,15 @@ private sealed class CommandKeyKeyValueValueValueMessage : CommandKeyBase private readonly RedisValue value0, value1, value2; private readonly RedisKey key1; - public CommandKeyKeyValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, - in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2) : base(db, flags, command, key0) + public CommandKeyKeyValueValueValueMessage( + int db, + CommandFlags flags, + RedisCommand command, + in RedisKey key0, + in RedisKey key1, + in RedisValue value0, + in RedisValue value1, + in RedisValue value2) : base(db, flags, command, key0) { key1.AssertNotNull(); value0.AssertNotNull(); @@ -1322,8 +1390,16 @@ private sealed class CommandKeyKeyValueValueValueValueMessage : CommandKeyBase private readonly RedisValue value0, value1, value2, value3; private readonly RedisKey key1; - public CommandKeyKeyValueValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, - in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3) : base(db, flags, command, key0) + public CommandKeyKeyValueValueValueValueMessage( + int db, + CommandFlags flags, + RedisCommand command, + in RedisKey key0, + in RedisKey key1, + in RedisValue value0, + in RedisValue value1, + in RedisValue value2, + in RedisValue value3) : base(db, flags, command, key0) { key1.AssertNotNull(); value0.AssertNotNull(); @@ -1356,8 +1432,17 @@ private sealed class CommandKeyKeyValueValueValueValueValueMessage : CommandKeyB private readonly RedisValue value0, value1, value2, value3, value4; private readonly RedisKey key1; - public CommandKeyKeyValueValueValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, - in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) : base(db, flags, command, key0) + public CommandKeyKeyValueValueValueValueValueMessage( + int db, + CommandFlags flags, + RedisCommand command, + in RedisKey key0, + in RedisKey key1, + in RedisValue value0, + in RedisValue value1, + in RedisValue value2, + in RedisValue value3, + in RedisValue value4) : base(db, flags, command, key0) { key1.AssertNotNull(); value0.AssertNotNull(); @@ -1393,8 +1478,18 @@ private sealed class CommandKeyKeyValueValueValueValueValueValueMessage : Comman private readonly RedisValue value0, value1, value2, value3, value4, value5; private readonly RedisKey key1; - public CommandKeyKeyValueValueValueValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, - in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4, in RedisValue value5) : base(db, flags, command, key0) + public CommandKeyKeyValueValueValueValueValueValueMessage( + int db, + CommandFlags flags, + RedisCommand command, + in RedisKey key0, + in RedisKey key1, + in RedisValue value0, + in RedisValue value1, + in RedisValue value2, + in RedisValue value3, + in RedisValue value4, + in RedisValue value5) : base(db, flags, command, key0) { key1.AssertNotNull(); value0.AssertNotNull(); @@ -1433,8 +1528,19 @@ private sealed class CommandKeyKeyValueValueValueValueValueValueValueMessage : C private readonly RedisValue value0, value1, value2, value3, value4, value5, value6; private readonly RedisKey key1; - public CommandKeyKeyValueValueValueValueValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, - in RedisKey key1, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4, in RedisValue value5, in RedisValue value6) : base(db, flags, command, key0) + public CommandKeyKeyValueValueValueValueValueValueValueMessage( + int db, + CommandFlags flags, + RedisCommand command, + in RedisKey key0, + in RedisKey key1, + in RedisValue value0, + in RedisValue value1, + in RedisValue value2, + in RedisValue value3, + in RedisValue value4, + in RedisValue value5, + in RedisValue value6) : base(db, flags, command, key0) { key1.AssertNotNull(); value0.AssertNotNull(); diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 7f9ffa32e..0755c939e 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -49,7 +49,7 @@ internal sealed class PhysicalBridge : IDisposable private volatile bool isDisposed; private long nonPreferredEndpointCount; - //private volatile int missedHeartbeats; + // private volatile int missedHeartbeats; private long operationCount, socketCount; private volatile PhysicalConnection? physical; @@ -63,7 +63,7 @@ internal sealed class PhysicalBridge : IDisposable internal long? ConnectionId => physical?.ConnectionId; #if NETCOREAPP - private readonly SemaphoreSlim _singleWriterMutex = new(1,1); + private readonly SemaphoreSlim _singleWriterMutex = new(1, 1); #else private readonly MutexSlim _singleWriterMutex; #endif @@ -98,7 +98,7 @@ public enum State : byte Connecting, ConnectedEstablishing, ConnectedEstablished, - Disconnected + Disconnected, } public Exception? LastException { get; private set; } @@ -123,7 +123,7 @@ public enum State : byte public RedisCommand LastCommand { get; private set; } /// - /// If we have a connection, report the protocol being used + /// If we have a connection, report the protocol being used. /// public RedisProtocol? Protocol => physical?.Protocol; @@ -292,10 +292,12 @@ internal readonly struct BridgeStatus /// Number of messages sent since the last heartbeat was processed. /// public int MessagesSinceLastHeartbeat { get; init; } + /// /// The time this connection was connected at, if it's connected currently. /// public DateTime? ConnectedAt { get; init; } + /// /// Whether the pipe writer is currently active. /// @@ -310,10 +312,12 @@ internal readonly struct BridgeStatus /// The number of messages that are in the backlog queue (waiting to be sent when the connection is healthy again). /// public int BacklogMessagesPending { get; init; } + /// /// The number of messages that are in the backlog queue (waiting to be sent when the connection is healthy again). /// public int BacklogMessagesPendingCounter { get; init; } + /// /// The number of messages ever added to the backlog queue in the life of this connection. /// @@ -392,8 +396,7 @@ internal void KeepAlive(bool forceRun = false) } else if (commandMap.IsAvailable(RedisCommand.UNSUBSCRIBE)) { - msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.UNSUBSCRIBE, - RedisChannel.Literal(Multiplexer.UniqueId)); + msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.UNSUBSCRIBE, RedisChannel.Literal(Multiplexer.UniqueId)); msg.SetSource(ResultProcessor.TrackSubscriptions, null); } break; @@ -478,7 +481,7 @@ internal void OnDisconnected(ConnectionFailureType failureType, PhysicalConnecti oldState = default(State); // only defined when isCurrent = true ConnectedAt = default; - if (isCurrent = (physical == connection)) + if (isCurrent = physical == connection) { Trace("Bridge noting disconnect from active connection" + (isDisposed ? " (disposed)" : "")); oldState = ChangeState(State.Disconnected); @@ -745,8 +748,8 @@ private WriteResult WriteMessageInsideLock(PhysicalConnection physical, Message message.Complete(); return result; } - //The parent message (next) may be returned from GetMessages - //and should not be marked as sent again below + // The parent message (next) may be returned from GetMessages + // and should not be marked as sent again below. messageIsSent = messageIsSent || subCommand == message; } if (!messageIsSent) @@ -840,7 +843,6 @@ private bool TryPushToBacklog(Message message, bool onlyIfExists, bool bypassBac // In the handshake case: send the command directly through. // If we're disconnected *in the middle of a handshake*, we've bombed a brand new socket and failing, // backing off, and retrying next heartbeat is best anyway. - // // Internal calls also shouldn't queue - try immediately. If these aren't errors (most aren't), we // won't alert the user. if (bypassBacklog || message.IsInternalCall) @@ -848,9 +850,9 @@ private bool TryPushToBacklog(Message message, bool onlyIfExists, bool bypassBac return false; } - // Note, for deciding emptiness for whether to push onlyIfExists, and start worker, - // we only need care if WE are able to - // see the queue when its empty. Not whether anyone else sees it as empty. + // Note, for deciding emptiness for whether to push onlyIfExists, and start worker, + // we only need care if WE are able to see the queue when its empty. + // Not whether anyone else sees it as empty. // So strong synchronization is not required. if (onlyIfExists && Volatile.Read(ref _backlogCurrentEnqueued) == 0) { @@ -988,6 +990,7 @@ internal enum BacklogStatus : byte } private volatile BacklogStatus _backlogStatus; + /// /// Process the backlog(s) in play if any. /// This means flushing commands to an available/active connection (if any) or spinning until timeout if not. @@ -1312,7 +1315,8 @@ private async ValueTask WriteMessageTakingWriteLockAsync_Awaited( #else ValueTask pending, #endif - PhysicalConnection physical, Message message) + PhysicalConnection physical, + Message message) { #if NETCOREAPP bool gotLock = false; @@ -1353,6 +1357,7 @@ private async ValueTask WriteMessageTakingWriteLockAsync_Awaited( } } + [SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1519:Braces should not be omitted from multi-line child statement", Justification = "Detector is confused with the #ifdefs here")] private async ValueTask CompleteWriteAndReleaseLockAsync( #if !NETCOREAPP LockToken lockToken, @@ -1503,7 +1508,7 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne { throw ExceptionFactory.PrimaryOnly(Multiplexer.RawConfig.IncludeDetailInExceptions, message.Command, message, ServerEndPoint); } - switch(cmd) + switch (cmd) { case RedisCommand.QUIT: connection.RecordQuit(); @@ -1519,7 +1524,7 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne { // If we are executing AUTH, it means we are still unauthenticated // Setting READONLY before AUTH always fails but we think it succeeded since - // we run it as Fire and Forget. + // we run it as Fire and Forget. if (cmd != RedisCommand.AUTH && cmd != RedisCommand.HELLO) { var readmode = connection.GetReadModeCommand(isPrimaryOnly); @@ -1555,8 +1560,7 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne if (_nextHighIntegrityToken is not 0 && !connection.TransactionActive // validated in the UNWATCH/EXEC/DISCARD - && message.Command is not RedisCommand.AUTH or RedisCommand.HELLO // if auth fails, ECHO may also fail; avoid confusion - ) + && message.Command is not RedisCommand.AUTH or RedisCommand.HELLO) // if auth fails, ECHO may also fail; avoid confusion { // make sure this value exists early to avoid a race condition // if the response comes back super quickly @@ -1607,8 +1611,8 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne Trace("Write failed: " + ex.Message); message.Fail(ConnectionFailureType.InternalFailure, ex, null, Multiplexer); message.Complete(); - // This failed without actually writing; we're OK with that... unless there's a transaction + // This failed without actually writing; we're OK with that... unless there's a transaction if (connection?.TransactionActive == true) { // We left it in a broken state - need to kill the connection @@ -1645,7 +1649,7 @@ private uint NextHighIntegrityTokenInsideLock() } /// - /// For testing only + /// For testing only. /// internal void SimulateConnectionFailure(SimulatedFailureType failureType) { diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index c282121a5..0a07f8ac4 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -1,7 +1,6 @@ -using Pipelines.Sockets.Unofficial; -using Pipelines.Sockets.Unofficial.Arenas; -using System; +using System; using System.Buffers; +using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -18,8 +17,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Pipelines.Sockets.Unofficial; +using Pipelines.Sockets.Unofficial.Arenas; using static StackExchange.Redis.Message; -using System.Buffers.Binary; namespace StackExchange.Redis { @@ -127,7 +127,8 @@ internal async Task BeginConnectAsync(ILogger? log) { bridge.Multiplexer.RawConfig.BeforeSocketConnect?.Invoke(endpoint, bridge.ConnectionType, _socket); if (tunnel is not null) - { // same functionality as part of a tunnel + { + // same functionality as part of a tunnel await tunnel.BeforeSocketConnectAsync(endpoint, bridge.ConnectionType, _socket, CancellationToken.None).ForAwait(); } } @@ -148,11 +149,13 @@ internal async Task BeginConnectAsync(ILogger? log) args?.Abort(); } else if (args is not null && x.ConnectAsync(args)) - { // asynchronous operation is pending + { + // asynchronous operation is pending timeoutSource = ConfigureTimeout(args, bridge.Multiplexer.RawConfig.ConnectTimeout); } else - { // completed synchronously + { + // completed synchronously args?.Complete(); } @@ -231,16 +234,18 @@ private static CancellationTokenSource ConfigureTimeout(SocketAwaitableEventArgs { var cts = new CancellationTokenSource(); var timeout = Task.Delay(timeoutMilliseconds, cts.Token); - timeout.ContinueWith((_, state) => - { - try + timeout.ContinueWith( + (_, state) => { - var a = (SocketAwaitableEventArgs)state!; - a.Abort(SocketError.TimedOut); - Socket.CancelConnectAsync(a); - } - catch { } - }, args); + try + { + var a = (SocketAwaitableEventArgs)state!; + a.Abort(SocketError.TimedOut); + Socket.CancelConnectAsync(a); + } + catch { } + }, + args); return cts; } @@ -271,7 +276,7 @@ private enum ReadMode : byte internal void SetProtocol(RedisProtocol value) => _protocol = value; - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Trust me yo")] internal void Shutdown() { var ioPipe = Interlocked.Exchange(ref _ioPipe, null); // compare to the critical read @@ -384,8 +389,7 @@ public void RecordConnectionFailed( Exception? innerException = null, [CallerMemberName] string? origin = null, bool isInitialConnect = false, - IDuplexPipe? connectingPipe = null - ) + IDuplexPipe? connectingPipe = null) { bool weAskedForThis = false; Exception? outerException = innerException; @@ -456,7 +460,7 @@ public void RecordConnectionFailed( } var data = new List>(); - void add(string lk, string sk, string? v) + void AddData(string lk, string sk, string? v) { if (lk != null) data.Add(Tuple.Create(lk, v)); if (sk != null) exMessage.Append(", ").Append(sk).Append(": ").Append(v); @@ -473,30 +477,30 @@ void add(string lk, string sk, string? v) data.Add(Tuple.Create("FailureType", failureType.ToString())); data.Add(Tuple.Create("EndPoint", Format.ToString(bridge.ServerEndPoint?.EndPoint))); - add("Origin", "origin", origin); + AddData("Origin", "origin", origin); // add("Input-Buffer", "input-buffer", _ioPipe.Input); - add("Outstanding-Responses", "outstanding", GetSentAwaitingResponseCount().ToString()); - add("Last-Read", "last-read", (unchecked(now - lastRead) / 1000) + "s ago"); - add("Last-Write", "last-write", (unchecked(now - lastWrite) / 1000) + "s ago"); - if (unansweredWriteTime != 0) add("Unanswered-Write", "unanswered-write", (unchecked(now - unansweredWriteTime) / 1000) + "s ago"); - add("Keep-Alive", "keep-alive", bridge.ServerEndPoint?.WriteEverySeconds + "s"); - add("Previous-Physical-State", "state", oldState.ToString()); - add("Manager", "mgr", bridge.Multiplexer.SocketManager?.GetState()); - if (connStatus.BytesAvailableOnSocket >= 0) add("Inbound-Bytes", "in", connStatus.BytesAvailableOnSocket.ToString()); - if (connStatus.BytesInReadPipe >= 0) add("Inbound-Pipe-Bytes", "in-pipe", connStatus.BytesInReadPipe.ToString()); - if (connStatus.BytesInWritePipe >= 0) add("Outbound-Pipe-Bytes", "out-pipe", connStatus.BytesInWritePipe.ToString()); - - add("Last-Heartbeat", "last-heartbeat", (lastBeat == 0 ? "never" : ((unchecked(now - lastBeat) / 1000) + "s ago")) + (bridge.IsBeating ? " (mid-beat)" : "")); + AddData("Outstanding-Responses", "outstanding", GetSentAwaitingResponseCount().ToString()); + AddData("Last-Read", "last-read", (unchecked(now - lastRead) / 1000) + "s ago"); + AddData("Last-Write", "last-write", (unchecked(now - lastWrite) / 1000) + "s ago"); + if (unansweredWriteTime != 0) AddData("Unanswered-Write", "unanswered-write", (unchecked(now - unansweredWriteTime) / 1000) + "s ago"); + AddData("Keep-Alive", "keep-alive", bridge.ServerEndPoint?.WriteEverySeconds + "s"); + AddData("Previous-Physical-State", "state", oldState.ToString()); + AddData("Manager", "mgr", bridge.Multiplexer.SocketManager?.GetState()); + if (connStatus.BytesAvailableOnSocket >= 0) AddData("Inbound-Bytes", "in", connStatus.BytesAvailableOnSocket.ToString()); + if (connStatus.BytesInReadPipe >= 0) AddData("Inbound-Pipe-Bytes", "in-pipe", connStatus.BytesInReadPipe.ToString()); + if (connStatus.BytesInWritePipe >= 0) AddData("Outbound-Pipe-Bytes", "out-pipe", connStatus.BytesInWritePipe.ToString()); + + AddData("Last-Heartbeat", "last-heartbeat", (lastBeat == 0 ? "never" : ((unchecked(now - lastBeat) / 1000) + "s ago")) + (bridge.IsBeating ? " (mid-beat)" : "")); var mbeat = bridge.Multiplexer.LastHeartbeatSecondsAgo; if (mbeat >= 0) { - add("Last-Multiplexer-Heartbeat", "last-mbeat", mbeat + "s ago"); + AddData("Last-Multiplexer-Heartbeat", "last-mbeat", mbeat + "s ago"); } - add("Last-Global-Heartbeat", "global", ConnectionMultiplexer.LastGlobalHeartbeatSecondsAgo + "s ago"); + AddData("Last-Global-Heartbeat", "global", ConnectionMultiplexer.LastGlobalHeartbeatSecondsAgo + "s ago"); } } - add("Version", "v", Utils.GetLibVersion()); + AddData("Version", "v", Utils.GetLibVersion()); outerException = new RedisConnectionException(failureType, exMessage.ToString(), innerException); @@ -595,7 +599,7 @@ internal static void IdentifyFailureType(Exception? exception, ref ConnectionFai AuthenticationException => ConnectionFailureType.AuthenticationFailure, EndOfStreamException or ObjectDisposedException => ConnectionFailureType.SocketClosed, SocketException or IOException => ConnectionFailureType.SocketFailure, - _ => failureType + _ => failureType, }; } } @@ -768,9 +772,10 @@ internal int OnBridgeHeartbeat() if (msg.ResultBoxIsAsync) { bool haveDeltas = msg.TryGetPhysicalState(out _, out _, out long sentDelta, out var receivedDelta) && sentDelta >= 0 && receivedDelta >= 0; - var timeoutEx = ExceptionFactory.Timeout(multiplexer, haveDeltas + var baseErrorMessage = haveDeltas ? $"Timeout awaiting response (outbound={sentDelta >> 10}KiB, inbound={receivedDelta >> 10}KiB, {elapsed}ms elapsed, timeout is {timeout}ms)" - : $"Timeout awaiting response ({elapsed}ms elapsed, timeout is {timeout}ms)", msg, server); + : $"Timeout awaiting response ({elapsed}ms elapsed, timeout is {timeout}ms)"; + var timeoutEx = ExceptionFactory.Timeout(multiplexer, baseErrorMessage, msg, server); multiplexer.OnMessageFaulted(msg, timeoutEx); msg.SetExceptionAndComplete(timeoutEx, bridge); // tell the message that it is doomed multiplexer.OnAsyncTimeout(); @@ -1101,7 +1106,6 @@ private static void WriteUnifiedSpan(PipeWriter writer, ReadOnlySpan value { // ${len}\r\n = 3 + MaxInt32TextLen // {value}\r\n = 2 + value.Length - const int MaxQuickSpanSize = 512; if (value.Length == 0) { @@ -1162,7 +1166,6 @@ internal void WriteSha1AsHex(byte[] value) { // $40\r\n = 5 // {40 bytes}\r\n = 42 - var span = writer.GetSpan(47); span[0] = (byte)'$'; span[1] = (byte)'4'; @@ -1308,10 +1311,11 @@ private static void WriteUnifiedPrefixedBlob(PipeWriter? maybeNullWriter, byte[] return; // Prevent null refs during disposal } - // ${total-len}\r\n + // ${total-len}\r\n // {prefix}{value}\r\n if (prefix == null || prefix.Length == 0 || value == null) - { // if no prefix, just use the non-prefixed version; + { + // if no prefix, just use the non-prefixed version; // even if prefixed, a null value writes as null, so can use the non-prefixed version WriteUnifiedBlob(writer, value); } @@ -1365,8 +1369,7 @@ private static void WriteUnifiedUInt64(PipeWriter writer, ulong value) } internal static void WriteInteger(PipeWriter writer, long value) { - //note: client should never write integer; only server does this - + // note: client should never write integer; only server does this // :{asc}\r\n = MaxInt64TextLen + 3 var span = writer.GetSpan(3 + Format.MaxInt64TextLen); @@ -1386,18 +1389,22 @@ internal readonly struct ConnectionStatus /// Bytes available on the socket, not yet read into the pipe. /// public long BytesAvailableOnSocket { get; init; } + /// /// Bytes read from the socket, pending in the reader pipe. /// public long BytesInReadPipe { get; init; } + /// /// Bytes in the writer pipe, waiting to be written to the socket. /// public long BytesInWritePipe { get; init; } + /// /// Byte size of the last result we processed. /// public long BytesLastResult { get; init; } + /// /// Byte size on the buffer that isn't processed yet. /// @@ -1407,6 +1414,7 @@ internal readonly struct ConnectionStatus /// The inbound pipe reader status. /// public ReadStatus ReadStatus { get; init; } + /// /// The outbound pipe writer status. /// @@ -1511,7 +1519,8 @@ public ConnectionStatus GetStatus() if (!string.IsNullOrEmpty(pfxPath) && File.Exists(pfxPath)) { - return delegate { return new X509Certificate2(pfxPath, pfxPassword ?? "", flags ?? X509KeyStorageFlags.DefaultKeySet); }; + return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => + new X509Certificate2(pfxPath, pfxPassword ?? "", flags ?? X509KeyStorageFlags.DefaultKeySet); } } catch (Exception ex) @@ -1535,7 +1544,6 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock // the order is important here: // non-TLS: [Socket]<==[SocketConnection:IDuplexPipe] // TLS: [Socket]<==[NetworkStream]<==[SslStream]<==[StreamConnection:IDuplexPipe] - var config = bridge.Multiplexer.RawConfig; var tunnel = config.Tunnel; @@ -1555,10 +1563,12 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock } stream ??= new NetworkStream(socket ?? throw new InvalidOperationException("No socket or stream available - possibly a tunnel error")); - var ssl = new SslStream(stream, false, - config.CertificateValidationCallback ?? GetAmbientIssuerCertificateCallback(), - config.CertificateSelectionCallback ?? GetAmbientClientCertificateCallback(), - EncryptionPolicy.RequireEncryption); + var ssl = new SslStream( + innerStream: stream, + leaveInnerStreamOpen: false, + userCertificateValidationCallback: config.CertificateValidationCallback ?? GetAmbientIssuerCertificateCallback(), + userCertificateSelectionCallback: config.CertificateSelectionCallback ?? GetAmbientClientCertificateCallback(), + encryptionPolicy: EncryptionPolicy.RequireEncryption); try { try @@ -1744,7 +1754,6 @@ private void MatchResult(in RawResult result) Volatile.Write(ref _awaitingToken, msg); } - _readStatus = ReadStatus.MatchResultComplete; _activeMessage = null; @@ -1972,30 +1981,6 @@ private int ProcessBuffer(ref ReadOnlySequence buffer) _readStatus = ReadStatus.ProcessBufferComplete; return messageCount; } - //void ISocketCallback.Read() - //{ - // Interlocked.Increment(ref haveReader); - // try - // { - // do - // { - // int space = EnsureSpaceAndComputeBytesToRead(); - // int bytesRead = netStream?.Read(ioBuffer, ioBufferBytes, space) ?? 0; - - // if (!ProcessReadBytes(bytesRead)) return; // EOF - // } while (socketToken.Available != 0); - // Multiplexer.Trace("Buffer exhausted", physicalName); - // // ^^^ note that the socket manager will call us again when there is something to do - // } - // catch (Exception ex) - // { - // RecordConnectionFailed(ConnectionFailureType.InternalFailure, ex); - // } - // finally - // { - // Interlocked.Decrement(ref haveReader); - // } - //} private static RawResult.ResultFlags AsNull(RawResult.ResultFlags flags) => flags & ~RawResult.ResultFlags.NonNull; @@ -2004,18 +1989,25 @@ private static RawResult ReadArray(ResultType resultType, RawResult.ResultFlags var itemCount = ReadLineTerminatedString(ResultType.Integer, flags, ref reader); if (itemCount.HasValue) { - if (!itemCount.TryGetInt64(out long i64)) throw ExceptionFactory.ConnectionFailure(includeDetailInExceptions, ConnectionFailureType.ProtocolFailure, - itemCount.Is('?') ? "Streamed aggregate types not yet implemented" : "Invalid array length", server); + if (!itemCount.TryGetInt64(out long i64)) + { + throw ExceptionFactory.ConnectionFailure( + includeDetailInExceptions, + ConnectionFailureType.ProtocolFailure, + itemCount.Is('?') ? "Streamed aggregate types not yet implemented" : "Invalid array length", + server); + } + int itemCountActual = checked((int)i64); if (itemCountActual < 0) { - //for null response by command like EXEC, RESP array: *-1\r\n + // for null response by command like EXEC, RESP array: *-1\r\n return new RawResult(resultType, items: default, AsNull(flags)); } else if (itemCountActual == 0) { - //for zero array response by command like SCAN, Resp array: *0\r\n + // for zero array response by command like SCAN, Resp array: *0\r\n return new RawResult(resultType, items: default, flags); } @@ -2060,8 +2052,11 @@ private static RawResult ReadBulkString(ResultType type, RawResult.ResultFlags f { if (!prefix.TryGetInt64(out long i64)) { - throw ExceptionFactory.ConnectionFailure(includeDetailInExceptions, ConnectionFailureType.ProtocolFailure, - prefix.Is('?') ? "Streamed strings not yet implemented" : "Invalid bulk string length", server); + throw ExceptionFactory.ConnectionFailure( + includeDetailInExceptions, + ConnectionFailureType.ProtocolFailure, + prefix.Is('?') ? "Streamed strings not yet implemented" : "Invalid bulk string length", + server); } int bodySize = checked((int)i64); if (bodySize < 0) @@ -2128,15 +2123,33 @@ internal enum ReadStatus internal void StartReading() => ReadFromPipe().RedisFireAndForget(); - internal static RawResult TryParseResult(bool isResp3, Arena arena, in ReadOnlySequence buffer, ref BufferReader reader, - bool includeDetilInExceptions, PhysicalConnection? connection, bool allowInlineProtocol = false) - { - return TryParseResult(isResp3 ? (RawResult.ResultFlags.Resp3 | RawResult.ResultFlags.NonNull) : RawResult.ResultFlags.NonNull, - arena, buffer, ref reader, includeDetilInExceptions, connection?.BridgeCouldBeNull?.ServerEndPoint, allowInlineProtocol); - } - - private static RawResult TryParseResult(RawResult.ResultFlags flags, Arena arena, in ReadOnlySequence buffer, ref BufferReader reader, - bool includeDetilInExceptions, ServerEndPoint? server, bool allowInlineProtocol = false) + internal static RawResult TryParseResult( + bool isResp3, + Arena arena, + in ReadOnlySequence buffer, + ref BufferReader reader, + bool includeDetilInExceptions, + PhysicalConnection? connection, + bool allowInlineProtocol = false) + { + return TryParseResult( + isResp3 ? (RawResult.ResultFlags.Resp3 | RawResult.ResultFlags.NonNull) : RawResult.ResultFlags.NonNull, + arena, + buffer, + ref reader, + includeDetilInExceptions, + connection?.BridgeCouldBeNull?.ServerEndPoint, + allowInlineProtocol); + } + + private static RawResult TryParseResult( + RawResult.ResultFlags flags, + Arena arena, + in ReadOnlySequence buffer, + ref BufferReader reader, + bool includeDetilInExceptions, + ServerEndPoint? server, + bool allowInlineProtocol = false) { int prefix; do // this loop is just to allow us to parse (skip) attributes without doing a stack-dive @@ -2198,7 +2211,8 @@ private static RawResult TryParseResult(RawResult.ResultFlags flags, Arena - /// Returns the number of commands captured in this snapshot + /// Returns the number of commands captured in this snapshot. /// public int Count() => _count; /// - /// Returns the number of commands captured in this snapshot that match a condition + /// Returns the number of commands captured in this snapshot that match a condition. /// /// The predicate to match. public int Count(Func predicate) @@ -103,10 +104,11 @@ public int Count(Func predicate) } /// - /// Returns the captured commands as an array + /// Returns the captured commands as an array. /// public IProfiledCommand[] ToArray() - { // exploit the fact that we know the length + { + // exploit the fact that we know the length if (_count == 0) return Array.Empty(); var arr = new IProfiledCommand[_count]; @@ -120,10 +122,11 @@ public IProfiledCommand[] ToArray() } /// - /// Returns the captured commands as a list + /// Returns the captured commands as a list. /// public List ToList() - { // exploit the fact that we know the length + { + // exploit the fact that we know the length var list = new List(_count); ProfiledCommand? cur = _head; while (cur != null) diff --git a/src/StackExchange.Redis/Profiling/ProfilingSession.cs b/src/StackExchange.Redis/Profiling/ProfilingSession.cs index 83f1969bd..f83a49c91 100644 --- a/src/StackExchange.Redis/Profiling/ProfilingSession.cs +++ b/src/StackExchange.Redis/Profiling/ProfilingSession.cs @@ -11,6 +11,7 @@ public sealed class ProfilingSession /// Caller-defined state object. /// public object? UserToken { get; } + /// /// Create a new profiling session, optionally including a caller-defined state object. /// diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 1581c29c9..300503f57 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -1,8 +1,8 @@ -using Pipelines.Sockets.Unofficial.Arenas; -using System; +using System; using System.Buffers; using System.Runtime.CompilerServices; using System.Text; +using Pipelines.Sockets.Unofficial.Arenas; namespace StackExchange.Redis { @@ -92,7 +92,7 @@ private static void ThrowInvalidType(ResultType resultType) // if null, assume array public ResultType Resp2TypeArray => _resultType == ResultType.Null ? ResultType.Array : _resultType.ToResp2(); - internal bool IsNull => (_flags & ResultFlags.NonNull) == 0; + internal bool IsNull => (_flags & ResultFlags.NonNull) == 0; public bool HasValue => (_flags & ResultFlags.HasValue) != 0; @@ -274,7 +274,7 @@ internal bool StartsWith(byte[] expected) if (rangeToCheck.IsSingleSegment) return rangeToCheck.First.Span.SequenceEqual(expected); int offset = 0; - foreach(var segment in rangeToCheck) + foreach (var segment in rangeToCheck) { var from = segment.Span; var to = new Span(expected, offset, from.Length); @@ -406,12 +406,12 @@ private static GeoPosition AsGeoPosition(in Sequence coords) #else var decoder = Encoding.UTF8.GetDecoder(); int charCount = 0; - foreach(var segment in Payload) + foreach (var segment in Payload) { var span = segment.Span; if (span.IsEmpty) continue; - fixed(byte* bPtr = span) + fixed (byte* bPtr = span) { charCount += decoder.GetCharCount(bPtr, span.Length, false); } @@ -444,9 +444,9 @@ private static GeoPosition AsGeoPosition(in Sequence coords) #endif static string? GetVerbatimString(string? value, out ReadOnlySpan type) { - // the first three bytes provide information about the format of the following string, which - // can be txt for plain text, or mkd for markdown. The fourth byte is always `:` - // Then the real string follows. + // The first three bytes provide information about the format of the following string, which + // can be txt for plain text, or mkd for markdown. The fourth byte is always `:`. + // Then the real string follows. if (value is not null && value.Length >= 4 && value[3] == ':') { diff --git a/src/StackExchange.Redis/RedisBatch.cs b/src/StackExchange.Redis/RedisBatch.cs index 6c9727d0d..0a4c888f2 100644 --- a/src/StackExchange.Redis/RedisBatch.cs +++ b/src/StackExchange.Redis/RedisBatch.cs @@ -8,7 +8,7 @@ internal class RedisBatch : RedisDatabase, IBatch { private List? pending; - public RedisBatch(RedisDatabase wrapped, object? asyncState) : base(wrapped.multiplexer, wrapped.Database, asyncState ?? wrapped.AsyncState) {} + public RedisBatch(RedisDatabase wrapped, object? asyncState) : base(wrapped.multiplexer, wrapped.Database, asyncState ?? wrapped.AsyncState) { } public void Execute() { @@ -109,13 +109,13 @@ internal override Task ExecuteAsync(Message? message, ResultProcessor? return task; } - internal override T ExecuteSync(Message? message, ResultProcessor? processor, ServerEndPoint? server = null, T? defaultValue = default) where T : default => - throw new NotSupportedException("ExecuteSync cannot be used inside a batch"); + internal override T ExecuteSync(Message? message, ResultProcessor? processor, ServerEndPoint? server = null, T? defaultValue = default) where T : default + => throw new NotSupportedException("ExecuteSync cannot be used inside a batch"); private static void FailNoServer(ConnectionMultiplexer muxer, List messages) { if (messages == null) return; - foreach(var msg in messages) + foreach (var msg in messages) { msg.Fail(ConnectionFailureType.UnableToResolvePhysicalConnection, null, "unable to write batch", muxer); msg.Complete(); diff --git a/src/StackExchange.Redis/RedisChannel.cs b/src/StackExchange.Redis/RedisChannel.cs index 5e9446506..561cce21f 100644 --- a/src/StackExchange.Redis/RedisChannel.cs +++ b/src/StackExchange.Redis/RedisChannel.cs @@ -17,13 +17,12 @@ namespace StackExchange.Redis public bool IsNullOrEmpty => Value == null || Value.Length == 0; /// - /// Indicates whether this channel represents a wildcard pattern (see PSUBSCRIBE) + /// Indicates whether this channel represents a wildcard pattern (see PSUBSCRIBE). /// public bool IsPattern => _isPatternBased; internal bool IsNull => Value == null; - /// /// Indicates whether channels should use when no /// is specified; this is enabled by default, but can be disabled to avoid unexpected wildcard scenarios. @@ -36,19 +35,22 @@ public static bool UseImplicitAutoPattern private static PatternMode s_DefaultPatternMode = PatternMode.Auto; /// - /// Creates a new that does not act as a wildcard subscription + /// Creates a new that does not act as a wildcard subscription. /// public static RedisChannel Literal(string value) => new RedisChannel(value, PatternMode.Literal); + /// - /// Creates a new that does not act as a wildcard subscription + /// Creates a new that does not act as a wildcard subscription. /// public static RedisChannel Literal(byte[] value) => new RedisChannel(value, PatternMode.Literal); + /// - /// Creates a new that acts as a wildcard subscription + /// Creates a new that acts as a wildcard subscription. /// public static RedisChannel Pattern(string value) => new RedisChannel(value, PatternMode.Pattern); + /// - /// Creates a new that acts as a wildcard subscription + /// Creates a new that acts as a wildcard subscription. /// public static RedisChannel Pattern(byte[] value) => new RedisChannel(value, PatternMode.Pattern); @@ -162,7 +164,7 @@ private RedisChannel(byte[]? value, bool isPatternBased) RedisChannel rcObj => RedisValue.Equals(Value, rcObj.Value), string sObj => RedisValue.Equals(Value, Encoding.UTF8.GetBytes(sObj)), byte[] bObj => RedisValue.Equals(Value, bObj), - _ => false + _ => false, }; /// @@ -213,14 +215,16 @@ public enum PatternMode /// Will be treated as a pattern if it includes *. /// Auto = 0, + /// /// Never a pattern. /// Literal = 1, + /// /// Always a pattern. /// - Pattern = 2 + Pattern = 2, } /// diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 6a0210e6b..288e263d2 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1776,7 +1776,7 @@ public RedisValue[] SetPop(RedisKey key, long count, CommandFlags flags = Comman public Task SetPopAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) { - if(count == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState); + if (count == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState); var msg = count == 1 ? Message.Create(Database, flags, RedisCommand.SPOP, key) : Message.Create(Database, flags, RedisCommand.SPOP, key, count); @@ -1882,7 +1882,7 @@ public bool SortedSetAdd(RedisKey key, RedisValue member, double score, CommandF SortedSetAdd(key, member, score, SortedSetWhen.Always, flags); public bool SortedSetAdd(RedisKey key, RedisValue member, double score, When when = When.Always, CommandFlags flags = CommandFlags.None) => - SortedSetAdd(key, member, score, SortedSetWhenExtensions.Parse(when), flags); + SortedSetAdd(key, member, score, SortedSetWhenExtensions.Parse(when), flags); public bool SortedSetAdd(RedisKey key, RedisValue member, double score, SortedSetWhen when = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) { @@ -2331,7 +2331,8 @@ public Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, Red public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) { - var msg = GetStreamAddMessage(key, + var msg = GetStreamAddMessage( + key, messageId ?? StreamConstants.AutoGeneratedId, maxLength, useApproximateMaxLength, @@ -2343,7 +2344,8 @@ public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue str public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) { - var msg = GetStreamAddMessage(key, + var msg = GetStreamAddMessage( + key, messageId ?? StreamConstants.AutoGeneratedId, maxLength, useApproximateMaxLength, @@ -2355,7 +2357,8 @@ public Task StreamAddAsync(RedisKey key, RedisValue streamField, Red public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) { - var msg = GetStreamAddMessage(key, + var msg = GetStreamAddMessage( + key, messageId ?? StreamConstants.AutoGeneratedId, maxLength, useApproximateMaxLength, @@ -2367,7 +2370,8 @@ public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisVal public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) { - var msg = GetStreamAddMessage(key, + var msg = GetStreamAddMessage( + key, messageId ?? StreamConstants.AutoGeneratedId, maxLength, useApproximateMaxLength, @@ -2403,7 +2407,8 @@ public Task StreamAutoClaimIdsOnlyAsync(RedisKey k public StreamEntry[] StreamClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) { - var msg = GetStreamClaimMessage(key, + var msg = GetStreamClaimMessage( + key, consumerGroup, claimingConsumer, minIdleTimeInMs, @@ -2416,7 +2421,8 @@ public StreamEntry[] StreamClaim(RedisKey key, RedisValue consumerGroup, RedisVa public Task StreamClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) { - var msg = GetStreamClaimMessage(key, + var msg = GetStreamClaimMessage( + key, consumerGroup, claimingConsumer, minIdleTimeInMs, @@ -2429,7 +2435,8 @@ public Task StreamClaimAsync(RedisKey key, RedisValue consumerGro public RedisValue[] StreamClaimIdsOnly(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) { - var msg = GetStreamClaimMessage(key, + var msg = GetStreamClaimMessage( + key, consumerGroup, claimingConsumer, minIdleTimeInMs, @@ -2442,7 +2449,8 @@ public RedisValue[] StreamClaimIdsOnly(RedisKey key, RedisValue consumerGroup, R public Task StreamClaimIdsOnlyAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) { - var msg = GetStreamClaimMessage(key, + var msg = GetStreamClaimMessage( + key, consumerGroup, claimingConsumer, minIdleTimeInMs, @@ -2455,7 +2463,8 @@ public Task StreamClaimIdsOnlyAsync(RedisKey key, RedisValue consu public bool StreamConsumerGroupSetPosition(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, + var msg = Message.Create( + Database, flags, RedisCommand.XGROUP, new RedisValue[] @@ -2463,7 +2472,7 @@ public bool StreamConsumerGroupSetPosition(RedisKey key, RedisValue groupName, R StreamConstants.SetId, key.AsRedisValue(), groupName, - StreamPosition.Resolve(position, RedisCommand.XGROUP) + StreamPosition.Resolve(position, RedisCommand.XGROUP), }); return ExecuteSync(msg, ResultProcessor.Boolean); @@ -2471,7 +2480,8 @@ public bool StreamConsumerGroupSetPosition(RedisKey key, RedisValue groupName, R public Task StreamConsumerGroupSetPositionAsync(RedisKey key, RedisValue groupName, RedisValue position, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, + var msg = Message.Create( + Database, flags, RedisCommand.XGROUP, new RedisValue[] @@ -2479,7 +2489,7 @@ public Task StreamConsumerGroupSetPositionAsync(RedisKey key, RedisValue g StreamConstants.SetId, key.AsRedisValue(), groupName, - StreamPosition.Resolve(position, RedisCommand.XGROUP) + StreamPosition.Resolve(position, RedisCommand.XGROUP), }); return ExecuteAsync(msg, ResultProcessor.Boolean); @@ -2531,14 +2541,15 @@ public Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupN public StreamConsumerInfo[] StreamConsumerInfo(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, + var msg = Message.Create( + Database, flags, RedisCommand.XINFO, new RedisValue[] { StreamConstants.Consumers, key.AsRedisValue(), - groupName + groupName, }); return ExecuteSync(msg, ResultProcessor.StreamConsumerInfo, defaultValue: Array.Empty()); @@ -2546,14 +2557,15 @@ public StreamConsumerInfo[] StreamConsumerInfo(RedisKey key, RedisValue groupNam public Task StreamConsumerInfoAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, + var msg = Message.Create( + Database, flags, RedisCommand.XINFO, new RedisValue[] { StreamConstants.Consumers, key.AsRedisValue(), - groupName + groupName, }); return ExecuteAsync(msg, ResultProcessor.StreamConsumerInfo, defaultValue: Array.Empty()); @@ -2597,7 +2609,8 @@ public Task StreamLengthAsync(RedisKey key, CommandFlags flags = CommandFl public long StreamDelete(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, + var msg = Message.Create( + Database, flags, RedisCommand.XDEL, key, @@ -2608,7 +2621,8 @@ public long StreamDelete(RedisKey key, RedisValue[] messageIds, CommandFlags fla public Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, + var msg = Message.Create( + Database, flags, RedisCommand.XDEL, key, @@ -2619,7 +2633,8 @@ public Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, Comma public long StreamDeleteConsumer(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, + var msg = Message.Create( + Database, flags, RedisCommand.XGROUP, new RedisValue[] @@ -2627,7 +2642,7 @@ public long StreamDeleteConsumer(RedisKey key, RedisValue groupName, RedisValue StreamConstants.DeleteConsumer, key.AsRedisValue(), groupName, - consumerName + consumerName, }); return ExecuteSync(msg, ResultProcessor.Int64); @@ -2635,7 +2650,8 @@ public long StreamDeleteConsumer(RedisKey key, RedisValue groupName, RedisValue public Task StreamDeleteConsumerAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, + var msg = Message.Create( + Database, flags, RedisCommand.XGROUP, new RedisValue[] @@ -2643,7 +2659,7 @@ public Task StreamDeleteConsumerAsync(RedisKey key, RedisValue groupName, StreamConstants.DeleteConsumer, key.AsRedisValue(), groupName, - consumerName + consumerName, }); return ExecuteAsync(msg, ResultProcessor.Int64); @@ -2651,14 +2667,15 @@ public Task StreamDeleteConsumerAsync(RedisKey key, RedisValue groupName, public bool StreamDeleteConsumerGroup(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, + var msg = Message.Create( + Database, flags, RedisCommand.XGROUP, new RedisValue[] { StreamConstants.Destroy, key.AsRedisValue(), - groupName + groupName, }); return ExecuteSync(msg, ResultProcessor.Boolean); @@ -2666,14 +2683,15 @@ public bool StreamDeleteConsumerGroup(RedisKey key, RedisValue groupName, Comman public Task StreamDeleteConsumerGroupAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, + var msg = Message.Create( + Database, flags, RedisCommand.XGROUP, new RedisValue[] { StreamConstants.Destroy, key.AsRedisValue(), - groupName + groupName, }); return ExecuteAsync(msg, ResultProcessor.Boolean); @@ -2693,7 +2711,8 @@ public Task StreamPendingAsync(RedisKey key, RedisValue group public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) { - var msg = GetStreamPendingMessagesMessage(key, + var msg = GetStreamPendingMessagesMessage( + key, groupName, minId, maxId, @@ -2706,7 +2725,8 @@ public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue public Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) { - var msg = GetStreamPendingMessagesMessage(key, + var msg = GetStreamPendingMessagesMessage( + key, groupName, minId, maxId, @@ -2719,7 +2739,8 @@ public Task StreamPendingMessagesAsync(RedisKey key, public StreamEntry[] StreamRange(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None) { - var msg = GetStreamRangeMessage(key, + var msg = GetStreamRangeMessage( + key, minId, maxId, count, @@ -2731,7 +2752,8 @@ public StreamEntry[] StreamRange(RedisKey key, RedisValue? minId = null, RedisVa public Task StreamRangeAsync(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None) { - var msg = GetStreamRangeMessage(key, + var msg = GetStreamRangeMessage( + key, minId, maxId, count, @@ -2743,7 +2765,8 @@ public Task StreamRangeAsync(RedisKey key, RedisValue? minId = nu public StreamEntry[] StreamRead(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None) { - var msg = GetSingleStreamReadMessage(key, + var msg = GetSingleStreamReadMessage( + key, StreamPosition.Resolve(position, RedisCommand.XREAD), count, flags); @@ -2753,7 +2776,8 @@ public StreamEntry[] StreamRead(RedisKey key, RedisValue position, int? count = public Task StreamReadAsync(RedisKey key, RedisValue position, int? count = null, CommandFlags flags = CommandFlags.None) { - var msg = GetSingleStreamReadMessage(key, + var msg = GetSingleStreamReadMessage( + key, StreamPosition.Resolve(position, RedisCommand.XREAD), count, flags); @@ -2775,7 +2799,8 @@ public Task StreamReadAsync(StreamPosition[] streamPositions, int public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, CommandFlags flags) { - return StreamReadGroup(key, + return StreamReadGroup( + key, groupName, consumerName, position, @@ -2788,7 +2813,8 @@ public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisVa { var actualPosition = position ?? StreamPosition.NewMessages; - var msg = GetStreamReadGroupMessage(key, + var msg = GetStreamReadGroupMessage( + key, groupName, consumerName, StreamPosition.Resolve(actualPosition, RedisCommand.XREADGROUP), @@ -2801,7 +2827,8 @@ public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisVa public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, CommandFlags flags) { - return StreamReadGroupAsync(key, + return StreamReadGroupAsync( + key, groupName, consumerName, position, @@ -2814,7 +2841,8 @@ public Task StreamReadGroupAsync(RedisKey key, RedisValue groupNa { var actualPosition = position ?? StreamPosition.NewMessages; - var msg = GetStreamReadGroupMessage(key, + var msg = GetStreamReadGroupMessage( + key, groupName, consumerName, StreamPosition.Resolve(actualPosition, RedisCommand.XREADGROUP), @@ -2827,7 +2855,8 @@ public Task StreamReadGroupAsync(RedisKey key, RedisValue groupNa public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags) { - return StreamReadGroup(streamPositions, + return StreamReadGroup( + streamPositions, groupName, consumerName, countPerStream, @@ -2837,7 +2866,8 @@ public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValu public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) { - var msg = GetMultiStreamReadGroupMessage(streamPositions, + var msg = GetMultiStreamReadGroupMessage( + streamPositions, groupName, consumerName, countPerStream, @@ -2849,7 +2879,8 @@ public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValu public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags) { - return StreamReadGroupAsync(streamPositions, + return StreamReadGroupAsync( + streamPositions, groupName, consumerName, countPerStream, @@ -2859,7 +2890,8 @@ public Task StreamReadGroupAsync(StreamPosition[] streamPositions public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) { - var msg = GetMultiStreamReadGroupMessage(streamPositions, + var msg = GetMultiStreamReadGroupMessage( + streamPositions, groupName, consumerName, countPerStream, @@ -3274,9 +3306,9 @@ private Message GetCopyMessage(in RedisKey sourceKey, RedisKey destinationKey, i { < -1 => throw new ArgumentOutOfRangeException(nameof(destinationDatabase)), -1 when replace => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.REPLACE), - -1 => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey), - _ when replace => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.DB, destinationDatabase, RedisLiterals.REPLACE), - _ => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.DB, destinationDatabase), + -1 => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey), + _ when replace => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.DB, destinationDatabase, RedisLiterals.REPLACE), + _ => Message.Create(Database, flags, RedisCommand.COPY, sourceKey, destinationKey, RedisLiterals.DB, destinationDatabase), }; private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, TimeSpan? expiry, ExpireWhen when, out ServerEndPoint? server) @@ -3287,7 +3319,7 @@ private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, TimeSpan? return when switch { ExpireWhen.Always => Message.Create(Database, flags, RedisCommand.PERSIST, key), - _ => throw new ArgumentException("PERSIST cannot be used with when.") + _ => throw new ArgumentException("PERSIST cannot be used with when."), }; } @@ -3303,7 +3335,7 @@ private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, DateTime? return when switch { ExpireWhen.Always => Message.Create(Database, flags, RedisCommand.PERSIST, key), - _ => throw new ArgumentException("PERSIST cannot be used with when.") + _ => throw new ArgumentException("PERSIST cannot be used with when."), }; } @@ -3311,7 +3343,8 @@ private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, DateTime? return GetExpiryMessage(key, RedisCommand.PEXPIREAT, RedisCommand.EXPIREAT, milliseconds, when, flags, out server); } - private Message GetExpiryMessage(in RedisKey key, + private Message GetExpiryMessage( + in RedisKey key, RedisCommand millisecondsCommand, RedisCommand secondsCommand, long milliseconds, @@ -3328,7 +3361,7 @@ private Message GetExpiryMessage(in RedisKey key, return when switch { ExpireWhen.Always => Message.Create(Database, flags, millisecondsCommand, key, milliseconds), - _ => Message.Create(Database, flags, millisecondsCommand, key, milliseconds, when.ToLiteral()) + _ => Message.Create(Database, flags, millisecondsCommand, key, milliseconds, when.ToLiteral()), }; } server = null; @@ -3338,7 +3371,7 @@ private Message GetExpiryMessage(in RedisKey key, return when switch { ExpireWhen.Always => Message.Create(Database, flags, secondsCommand, key, seconds), - _ => Message.Create(Database, flags, secondsCommand, key, seconds, when.ToLiteral()) + _ => Message.Create(Database, flags, secondsCommand, key, seconds, when.ToLiteral()), }; } @@ -3405,12 +3438,23 @@ private Message GetSortedSetMultiPopMessage(RedisKey[] keys, Order order, long c { case 0: return null; case 1: - return Message.Create(Database, flags, RedisCommand.HMSET, key, - hashFields[0].name, hashFields[0].value); + return Message.Create( + Database, + flags, + RedisCommand.HMSET, + key, + hashFields[0].name, + hashFields[0].value); case 2: - return Message.Create(Database, flags, RedisCommand.HMSET, key, - hashFields[0].name, hashFields[0].value, - hashFields[1].name, hashFields[1].value); + return Message.Create( + Database, + flags, + RedisCommand.HMSET, + key, + hashFields[0].name, + hashFields[0].value, + hashFields[1].name, + hashFields[1].value); default: var arr = new RedisValue[hashFields.Length * 2]; int offset = 0; @@ -3461,7 +3505,8 @@ private static RedisValue GetLexRange(RedisValue value, Exclude exclude, bool is } private Message GetMultiStreamReadGroupMessage(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, bool noAck, CommandFlags flags) => - new MultiStreamReadGroupCommandMessage(Database, + new MultiStreamReadGroupCommandMessage( + Database, flags, streamPositions, groupName, @@ -3502,11 +3547,10 @@ public MultiStreamReadGroupCommandMessage(int db, CommandFlags flags, StreamPosi this.countPerStream = countPerStream; this.noAck = noAck; - argCount = 4 // Room for GROUP groupName consumerName & STREAMS - + (streamPositions.Length * 2) // Enough room for the stream keys and associated IDs. - + (countPerStream.HasValue ? 2 : 0) // Room for "COUNT num" or 0 if countPerStream is null. - + (noAck ? 1 : 0); // Allow for the NOACK subcommand. - + argCount = 4 // Room for GROUP groupName consumerName & STREAMS + + (streamPositions.Length * 2) // Enough room for the stream keys and associated IDs. + + (countPerStream.HasValue ? 2 : 0) // Room for "COUNT num" or 0 if countPerStream is null. + + (noAck ? 1 : 0); // Allow for the NOACK subcommand. } public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) @@ -3578,9 +3622,9 @@ public MultiStreamReadCommandMessage(int db, CommandFlags flags, StreamPosition[ this.streamPositions = streamPositions; this.countPerStream = countPerStream; - argCount = 1 // Streams keyword. - + (countPerStream.HasValue ? 2 : 0) // Room for "COUNT num" or 0 if countPerStream is null. - + (streamPositions.Length * 2); // Room for the stream names and the ID after which to begin reading. + argCount = 1 // Streams keyword. + + (countPerStream.HasValue ? 2 : 0) // Room for "COUNT num" or 0 if countPerStream is null. + + (streamPositions.Length * 2); // Room for the stream names and the ID after which to begin reading. } public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) @@ -3658,21 +3702,26 @@ private Message GetSetIntersectionLengthMessage(RedisKey[] keys, long limit = 0, private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double score, SortedSetWhen when, bool change, CommandFlags flags) { - RedisValue[] arr = new RedisValue[2 + when.CountBits() + (change? 1:0)]; + RedisValue[] arr = new RedisValue[2 + when.CountBits() + (change ? 1 : 0)]; int index = 0; - if ((when & SortedSetWhen.NotExists) != 0) { + if ((when & SortedSetWhen.NotExists) != 0) + { arr[index++] = RedisLiterals.NX; } - if ((when & SortedSetWhen.Exists) != 0) { + if ((when & SortedSetWhen.Exists) != 0) + { arr[index++] = RedisLiterals.XX; } - if ((when & SortedSetWhen.GreaterThan) != 0) { + if ((when & SortedSetWhen.GreaterThan) != 0) + { arr[index++] = RedisLiterals.GT; } - if ((when & SortedSetWhen.LessThan) != 0) { + if ((when & SortedSetWhen.LessThan) != 0) + { arr[index++] = RedisLiterals.LT; } - if (change) { + if (change) + { arr[index++] = RedisLiterals.CH; } arr[index++] = score; @@ -3689,21 +3738,26 @@ private Message GetSortedSetAddMessage(RedisKey key, RedisValue member, double s case 1: return GetSortedSetAddMessage(key, values[0].element, values[0].score, when, change, flags); default: - RedisValue[] arr = new RedisValue[(values.Length * 2) + when.CountBits() + (change? 1:0)]; + RedisValue[] arr = new RedisValue[(values.Length * 2) + when.CountBits() + (change ? 1 : 0)]; int index = 0; - if ((when & SortedSetWhen.NotExists) != 0) { + if ((when & SortedSetWhen.NotExists) != 0) + { arr[index++] = RedisLiterals.NX; } - if ((when & SortedSetWhen.Exists) != 0) { + if ((when & SortedSetWhen.Exists) != 0) + { arr[index++] = RedisLiterals.XX; } - if ((when & SortedSetWhen.GreaterThan) != 0) { + if ((when & SortedSetWhen.GreaterThan) != 0) + { arr[index++] = RedisLiterals.GT; } - if ((when & SortedSetWhen.LessThan) != 0) { + if ((when & SortedSetWhen.LessThan) != 0) + { arr[index++] = RedisLiterals.LT; } - if (change) { + if (change) + { arr[index++] = RedisLiterals.CH; } @@ -3723,21 +3777,20 @@ private Message GetSortMessage(RedisKey destination, RedisKey key, long skip, lo ? RedisCommand.SORT_RO : RedisCommand.SORT; - //if SORT_RO is not available, we cannot issue the command to a read-only replica + // If SORT_RO is not available, we cannot issue the command to a read-only replica if (command == RedisCommand.SORT) { server = null; } - // most common cases; no "get", no "by", no "destination", no "skip", no "take" if (destination.IsNull && skip == 0 && take == -1 && by.IsNull && (get == null || get.Length == 0)) { return order switch { - Order.Ascending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key), - Order.Ascending when sortType == SortType.Alphabetic => Message.Create(Database, flags, command, key, RedisLiterals.ALPHA), - Order.Descending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key, RedisLiterals.DESC), + Order.Ascending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key), + Order.Ascending when sortType == SortType.Alphabetic => Message.Create(Database, flags, command, key, RedisLiterals.ALPHA), + Order.Descending when sortType == SortType.Numeric => Message.Create(Database, flags, command, key, RedisLiterals.DESC), Order.Descending when sortType == SortType.Alphabetic => Message.Create(Database, flags, command, key, RedisLiterals.DESC, RedisLiterals.ALPHA), Order.Ascending or Order.Descending => throw new ArgumentOutOfRangeException(nameof(sortType)), _ => throw new ArgumentOutOfRangeException(nameof(order)), @@ -3751,7 +3804,8 @@ private Message GetSortMessage(RedisKey destination, RedisKey key, long skip, lo values.Add(RedisLiterals.BY); values.Add(by); } - if (skip != 0 || take != -1)// these are our defaults that mean "everything"; anything else needs to be sent explicitly + // these are our defaults that mean "everything"; anything else needs to be sent explicitly + if (skip != 0 || take != -1) { values.Add(RedisLiterals.LIMIT); values.Add(skip); @@ -3950,8 +4004,13 @@ private Message GetSortedSetRangeByScoreMessage(RedisKey key, double start, doub private Message GetSortedSetRemoveRangeByScoreMessage(RedisKey key, double start, double stop, Exclude exclude, CommandFlags flags) { - return Message.Create(Database, flags, RedisCommand.ZREMRANGEBYSCORE, key, - GetRange(start, exclude, true), GetRange(stop, exclude, false)); + return Message.Create( + Database, + flags, + RedisCommand.ZREMRANGEBYSCORE, + key, + GetRange(start, exclude, true), + GetRange(stop, exclude, false)); } private Message GetStreamAcknowledgeMessage(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags) @@ -3959,7 +4018,7 @@ private Message GetStreamAcknowledgeMessage(RedisKey key, RedisValue groupName, var values = new RedisValue[] { groupName, - messageId + messageId, }; return Message.Create(Database, flags, RedisCommand.XACK, key, values); @@ -4035,9 +4094,9 @@ private Message GetStreamAddMessage(RedisKey key, RedisValue entryId, int? maxLe var includeMaxLen = maxLength.HasValue ? 2 : 0; var includeApproxLen = maxLength.HasValue && useApproximateMaxLength ? 1 : 0; - var totalLength = (streamPairs.Length * 2) // Room for the name/value pairs - + 1 // The stream entry ID - + includeMaxLen // 2 or 0 (MAXLEN keyword & the count) + var totalLength = (streamPairs.Length * 2) // Room for the name/value pairs + + 1 // The stream entry ID + + includeMaxLen // 2 or 0 (MAXLEN keyword & the count) + includeApproxLen; // 1 or 0 var values = new RedisValue[totalLength]; @@ -4136,14 +4195,15 @@ private Message GetStreamCreateConsumerGroupMessage(RedisKey key, RedisValue gro values[4] = StreamConstants.MkStream; } - return Message.Create(Database, + return Message.Create( + Database, flags, RedisCommand.XGROUP, values); } /// - /// Gets a message for + /// Gets a message for . /// /// private Message GetStreamPendingMessagesMessage(RedisKey key, RedisValue groupName, RedisValue? minId, RedisValue? maxId, int count, RedisValue consumerName, CommandFlags flags) @@ -4157,7 +4217,6 @@ private Message GetStreamPendingMessagesMessage(RedisKey key, RedisValue groupNa // 2) "Bob" // 3) (integer)74170458 // 4) (integer)1 - if (count <= 0) { throw new ArgumentOutOfRangeException(nameof(count), "count must be greater than 0."); @@ -4175,7 +4234,8 @@ private Message GetStreamPendingMessagesMessage(RedisKey key, RedisValue groupNa values[4] = consumerName; } - return Message.Create(Database, + return Message.Create( + Database, flags, RedisCommand.XPENDING, key, @@ -4194,8 +4254,8 @@ private Message GetStreamRangeMessage(RedisKey key, RedisValue? minId, RedisValu var values = new RedisValue[2 + (count.HasValue ? 2 : 0)]; - values[0] = (messageOrder == Order.Ascending ? actualMin : actualMax); - values[1] = (messageOrder == Order.Ascending ? actualMax : actualMin); + values[0] = messageOrder == Order.Ascending ? actualMin : actualMax; + values[1] = messageOrder == Order.Ascending ? actualMax : actualMin; if (count.HasValue) { @@ -4203,7 +4263,8 @@ private Message GetStreamRangeMessage(RedisKey key, RedisValue? minId, RedisValu values[3] = count.Value; } - return Message.Create(Database, + return Message.Create( + Database, flags, messageOrder == Order.Ascending ? RedisCommand.XRANGE : RedisCommand.XREVRANGE, key, @@ -4242,7 +4303,8 @@ public SingleStreamReadGroupCommandMessage(int db, CommandFlags flags, RedisKey argCount = 6 + (count.HasValue ? 2 : 0) + (noAck ? 1 : 0); } - protected override void WriteImpl(PhysicalConnection physical) { + protected override void WriteImpl(PhysicalConnection physical) + { physical.WriteHeader(Command, argCount); physical.WriteBulkString(StreamConstants.Group); physical.WriteBulkString(groupName); @@ -4309,7 +4371,6 @@ protected override void WriteImpl(PhysicalConnection physical) public override int ArgCount => argCount; } - private Message GetStreamTrimMessage(RedisKey key, int maxLength, bool useApproximateMaxLength, CommandFlags flags) { if (maxLength < 0) @@ -4331,7 +4392,8 @@ private Message GetStreamTrimMessage(RedisKey key, int maxLength, bool useApprox values[1] = maxLength; } - return Message.Create(Database, + return Message.Create( + Database, flags, RedisCommand.XTRIM, key, @@ -4365,7 +4427,8 @@ private Message GetStringBitOperationMessage(Bitwise operation, RedisKey destina int slot = serverSelectionStrategy.HashSlot(destination); slot = serverSelectionStrategy.CombineSlot(slot, first); if (second.IsNull || operation == Bitwise.Not) - { // unary + { + // unary return Message.CreateInSlot(Database, slot, flags, RedisCommand.BITOP, new[] { op, destination.AsRedisValue(), first.AsRedisValue() }); } // binary @@ -4376,7 +4439,7 @@ private Message GetStringBitOperationMessage(Bitwise operation, RedisKey destina private Message GetStringGetExMessage(in RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => expiry switch { null => Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PERSIST), - _ => Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PX, (long)expiry.Value.TotalMilliseconds) + _ => Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PX, (long)expiry.Value.TotalMilliseconds), }; private Message GetStringGetExMessage(in RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) => expiry == DateTime.MaxValue @@ -4438,12 +4501,12 @@ private Message GetStringSetMessage( // no expiry return when switch { - When.Always when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value), - When.Always when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.KEEPTTL), + When.Always when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value), + When.Always when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.KEEPTTL), When.NotExists when !keepTtl => Message.Create(Database, flags, RedisCommand.SETNX, key, value), - When.NotExists when keepTtl => Message.Create(Database, flags, RedisCommand.SETNX, key, value, RedisLiterals.KEEPTTL), - When.Exists when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX), - When.Exists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.KEEPTTL), + When.NotExists when keepTtl => Message.Create(Database, flags, RedisCommand.SETNX, key, value, RedisLiterals.KEEPTTL), + When.Exists when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX), + When.Exists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.KEEPTTL), _ => throw new ArgumentOutOfRangeException(nameof(when)), }; } @@ -4455,8 +4518,8 @@ private Message GetStringSetMessage( long seconds = milliseconds / 1000; return when switch { - When.Always => Message.Create(Database, flags, RedisCommand.SETEX, key, seconds, value), - When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX), + When.Always => Message.Create(Database, flags, RedisCommand.SETEX, key, seconds, value), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX), When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.NX), _ => throw new ArgumentOutOfRangeException(nameof(when)), }; @@ -4464,8 +4527,8 @@ private Message GetStringSetMessage( return when switch { - When.Always => Message.Create(Database, flags, RedisCommand.PSETEX, key, milliseconds, value), - When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX), + When.Always => Message.Create(Database, flags, RedisCommand.PSETEX, key, milliseconds, value), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX), When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.NX), _ => throw new ArgumentOutOfRangeException(nameof(when)), }; @@ -4487,12 +4550,12 @@ private Message GetStringSetAndGetMessage( // no expiry return when switch { - When.Always when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.GET), - When.Always when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.GET, RedisLiterals.KEEPTTL), - When.Exists when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.GET), - When.Exists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.GET, RedisLiterals.KEEPTTL), + When.Always when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.GET), + When.Always when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.GET, RedisLiterals.KEEPTTL), + When.Exists when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.GET), + When.Exists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.GET, RedisLiterals.KEEPTTL), When.NotExists when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.NX, RedisLiterals.GET), - When.NotExists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.NX, RedisLiterals.GET, RedisLiterals.KEEPTTL), + When.NotExists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.NX, RedisLiterals.GET, RedisLiterals.KEEPTTL), _ => throw new ArgumentOutOfRangeException(nameof(when)), }; } @@ -4504,8 +4567,8 @@ private Message GetStringSetAndGetMessage( long seconds = milliseconds / 1000; return when switch { - When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.GET), - When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX, RedisLiterals.GET), + When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.GET), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX, RedisLiterals.GET), When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.NX, RedisLiterals.GET), _ => throw new ArgumentOutOfRangeException(nameof(when)), }; @@ -4513,8 +4576,8 @@ private Message GetStringSetAndGetMessage( return when switch { - When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.GET), - When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX, RedisLiterals.GET), + When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.GET), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX, RedisLiterals.GET), When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.NX, RedisLiterals.GET), _ => throw new ArgumentOutOfRangeException(nameof(when)), }; @@ -4525,10 +4588,10 @@ private Message GetStringSetAndGetMessage( 0 => ((flags & CommandFlags.FireAndForget) != 0) ? null : Message.Create(Database, flags, RedisCommand.INCRBY, key, value), - 1 => Message.Create(Database, flags, RedisCommand.INCR, key), - -1 => Message.Create(Database, flags, RedisCommand.DECR, key), + 1 => Message.Create(Database, flags, RedisCommand.INCR, key), + -1 => Message.Create(Database, flags, RedisCommand.DECR, key), > 0 => Message.Create(Database, flags, RedisCommand.INCRBY, key, value), - _ => Message.Create(Database, flags, RedisCommand.DECRBY, key, -value), + _ => Message.Create(Database, flags, RedisCommand.DECRBY, key, -value), }; private static RedisCommand SetOperationCommand(SetOperation operation, bool store) => operation switch @@ -4587,8 +4650,15 @@ private static void ReverseLimits(Order order, ref Exclude exclude, ref RedisVal } } } - public RedisValue[] SortedSetRangeByValue(RedisKey key, RedisValue min = default, RedisValue max = default, - Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) + public RedisValue[] SortedSetRangeByValue( + RedisKey key, + RedisValue min = default, + RedisValue max = default, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) { ReverseLimits(order, ref exclude, ref min, ref max); var msg = GetLexMessage(order == Order.Ascending ? RedisCommand.ZRANGEBYLEX : RedisCommand.ZREVRANGEBYLEX, key, min, max, exclude, skip, take, flags); @@ -4610,8 +4680,15 @@ public Task SortedSetLengthByValueAsync(RedisKey key, RedisValue min, Redi public Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude, long skip, long take, CommandFlags flags) => SortedSetRangeByValueAsync(key, min, max, exclude, Order.Ascending, skip, take, flags); - public Task SortedSetRangeByValueAsync(RedisKey key, RedisValue min = default, RedisValue max = default, - Exclude exclude = Exclude.None, Order order = Order.Ascending, long skip = 0, long take = -1, CommandFlags flags = CommandFlags.None) + public Task SortedSetRangeByValueAsync( + RedisKey key, + RedisValue min = default, + RedisValue max = default, + Exclude exclude = Exclude.None, + Order order = Order.Ascending, + long skip = 0, + long take = -1, + CommandFlags flags = CommandFlags.None) { ReverseLimits(order, ref exclude, ref min, ref max); var msg = GetLexMessage(order == Order.Ascending ? RedisCommand.ZRANGEBYLEX : RedisCommand.ZREVRANGEBYLEX, key, min, max, exclude, skip, take, flags); @@ -4630,8 +4707,17 @@ internal class ScanEnumerable : CursorEnumerable private readonly RedisValue pattern; private readonly RedisCommand command; - public ScanEnumerable(RedisDatabase database, ServerEndPoint? server, RedisKey key, in RedisValue pattern, int pageSize, in RedisValue cursor, int pageOffset, CommandFlags flags, - RedisCommand command, ResultProcessor processor) + public ScanEnumerable( + RedisDatabase database, + ServerEndPoint? server, + RedisKey key, + in RedisValue pattern, + int pageSize, + in RedisValue cursor, + int pageOffset, + CommandFlags flags, + RedisCommand command, + ResultProcessor processor) : base(database, server, database.Database, pageSize, cursor, pageOffset, flags) { this.key = key; @@ -4752,7 +4838,7 @@ protected override void WriteImpl(PhysicalConnection physical) physical.Write(channel); } else - { // recognises well-known types + { // recognises well-known types var val = RedisValue.TryParse(arg, out var valid); if (!valid) throw new InvalidCastException($"Unable to parse value: '{arg}'"); physical.WriteBulkString(val); @@ -4922,7 +5008,7 @@ private static Message CreateSortedSetRangeStoreMessage( { Order.Ascending => Message.Create(db, flags, RedisCommand.ZRANGESTORE, destinationKey, sourceKey, start, stop), Order.Descending => Message.Create(db, flags, RedisCommand.ZRANGESTORE, destinationKey, sourceKey, start, stop, RedisLiterals.REV), - _ => throw new ArgumentOutOfRangeException(nameof(order)) + _ => throw new ArgumentOutOfRangeException(nameof(order)), }; } @@ -4930,14 +5016,14 @@ private static Message CreateSortedSetRangeStoreMessage( { Exclude.Both or Exclude.Start => $"({start}", _ when sortedSetOrder == SortedSetOrder.ByLex => $"[{start}", - _ => start + _ => start, }; RedisValue formattedStop = exclude switch { Exclude.Both or Exclude.Stop => $"({stop}", _ when sortedSetOrder == SortedSetOrder.ByLex => $"[{stop}", - _ => stop + _ => stop, }; return order switch @@ -4950,7 +5036,7 @@ private static Message CreateSortedSetRangeStoreMessage( Message.Create(db, flags, RedisCommand.ZRANGESTORE, destinationKey, sourceKey, formattedStart, formattedStop, sortedSetOrder.GetLiteral(), RedisLiterals.REV, RedisLiterals.LIMIT, skip, take), Order.Descending => Message.Create(db, flags, RedisCommand.ZRANGESTORE, destinationKey, sourceKey, formattedStart, formattedStop, sortedSetOrder.GetLiteral(), RedisLiterals.REV), - _ => throw new ArgumentOutOfRangeException(nameof(order)) + _ => throw new ArgumentOutOfRangeException(nameof(order)), }; } diff --git a/src/StackExchange.Redis/RedisErrorEventArgs.cs b/src/StackExchange.Redis/RedisErrorEventArgs.cs index bfe114a7c..2213baf1c 100644 --- a/src/StackExchange.Redis/RedisErrorEventArgs.cs +++ b/src/StackExchange.Redis/RedisErrorEventArgs.cs @@ -12,8 +12,10 @@ public class RedisErrorEventArgs : EventArgs, ICompletable private readonly EventHandler? handler; private readonly object sender; internal RedisErrorEventArgs( - EventHandler? handler, object sender, - EndPoint endpoint, string message) + EventHandler? handler, + object sender, + EndPoint endpoint, + string message) { this.handler = handler; this.sender = sender; @@ -28,7 +30,7 @@ internal RedisErrorEventArgs( /// Redis endpoint. /// Error message. public RedisErrorEventArgs(object sender, EndPoint endpoint, string message) - : this (null, sender, endpoint, message) + : this(null, sender, endpoint, message) { } diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 19c5233e1..e81043010 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -11,6 +11,8 @@ namespace StackExchange.Redis /// public readonly struct RedisFeatures : IEquatable { +#pragma warning disable SA1310 // Field names should not contain underscore +#pragma warning disable SA1311 // Static readonly fields should begin with upper-case letter internal static readonly Version v2_0_0 = new Version(2, 0, 0), v2_1_0 = new Version(2, 1, 0), v2_1_1 = new Version(2, 1, 1), @@ -42,6 +44,8 @@ namespace StackExchange.Redis v7_0_0_rc1 = new Version(6, 9, 240), // 7.0 RC1 is version 6.9.240 v7_2_0_rc1 = new Version(7, 1, 240), // 7.2 RC1 is version 7.1.240 v7_4_0_rc1 = new Version(7, 3, 240); // 7.4 RC1 is version 7.3.240 +#pragma warning restore SA1310 // Field names should not contain underscore +#pragma warning restore SA1311 // Static readonly fields should begin with upper-case letter private readonly Version version; @@ -54,6 +58,8 @@ public RedisFeatures(Version version) this.version = version ?? throw new ArgumentNullException(nameof(version)); } +#pragma warning disable SA1629 // Documentation should end with a period + /// /// Are BITOP and BITCOUNT available? /// @@ -264,14 +270,15 @@ public RedisFeatures(Version version) /// public bool PushMultiple => Version.IsAtLeast(v4_0_0); - /// /// Is the RESP3 protocol available? /// public bool Resp3 => Version.IsAtLeast(v6_0_0); +#pragma warning restore 1629 // Documentation text should end with a period. + /// - /// The Redis version of the server + /// The Redis version of the server. /// public Version Version => version ?? v2_0_0; @@ -339,7 +346,6 @@ internal static class VersionExtensions { // normalize two version parts and smash them together into a long; if either part is -ve, // zero is used instead; this gives us consistent ordering following "long" rules - private static long ComposeMajorMinor(Version version) // always specified => (((long)version.Major) << 32) | (long)version.Minor; diff --git a/src/StackExchange.Redis/RedisKey.cs b/src/StackExchange.Redis/RedisKey.cs index 28378d116..9bfc041b1 100644 --- a/src/StackExchange.Redis/RedisKey.cs +++ b/src/StackExchange.Redis/RedisKey.cs @@ -7,7 +7,7 @@ namespace StackExchange.Redis { /// - /// Represents a key that can be stored in redis + /// Represents a key that can be stored in redis. /// public readonly struct RedisKey : IEquatable { @@ -239,6 +239,7 @@ public static implicit operator RedisKey(string? key) if (key == null) return default; return new RedisKey(null, key); } + /// /// Create a from a . /// @@ -253,7 +254,7 @@ public static implicit operator RedisKey(byte[]? key) /// Obtain the as a . /// /// The key to get a byte array for. - public static implicit operator byte[]? (RedisKey key) + public static implicit operator byte[]?(RedisKey key) { if (key.IsNull) return null; if (key.TryGetSimpleBuffer(out var arr)) return arr; @@ -270,7 +271,7 @@ public static implicit operator RedisKey(byte[]? key) /// Obtain the key as a . /// /// The key to get a string for. - public static implicit operator string? (RedisKey key) + public static implicit operator string?(RedisKey key) { if (key.KeyPrefix is null) { @@ -413,9 +414,11 @@ internal int CopyTo(Span destination) unsafe { fixed (byte* bPtr = destination) - fixed (char* cPtr = s) { - written += Encoding.UTF8.GetBytes(cPtr, s.Length, bPtr, destination.Length); + fixed (char* cPtr = s) + { + written += Encoding.UTF8.GetBytes(cPtr, s.Length, bPtr, destination.Length); + } } } #endif diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 11d2ba016..b77649402 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -2,6 +2,8 @@ namespace StackExchange.Redis { +#pragma warning disable SA1310 // Field names should not contain underscore +#pragma warning disable SA1311 // Static readonly fields should begin with upper-case letter internal static class CommonReplies { public static readonly CommandBytes @@ -153,14 +155,14 @@ public static readonly RedisValue REPLICAS = "REPLICAS", SLAVES = "SLAVES", GETMASTERADDRBYNAME = "GET-MASTER-ADDR-BY-NAME", - // RESET = "RESET", + // RESET = "RESET", FAILOVER = "FAILOVER", SENTINELS = "SENTINELS", // Sentinel Literals as of 2.8.4 MONITOR = "MONITOR", REMOVE = "REMOVE", - // SET = "SET", + // SET = "SET", // replication states connect = "connect", @@ -215,4 +217,6 @@ public static readonly RedisValue _ => throw new ArgumentOutOfRangeException(nameof(operation)), }; } +#pragma warning restore SA1310 // Field names should not contain underscore +#pragma warning restore SA1311 // Static readonly fields should begin with upper-case letter } diff --git a/src/StackExchange.Redis/RedisProtocol.cs b/src/StackExchange.Redis/RedisProtocol.cs index 8c1c9b869..077671bd6 100644 --- a/src/StackExchange.Redis/RedisProtocol.cs +++ b/src/StackExchange.Redis/RedisProtocol.cs @@ -11,11 +11,12 @@ public enum RedisProtocol // "hey, we've added RESP 3.1; oops, we've added RESP 3.1.1" /// - /// The protocol used by all redis server versions since 1.2, as defined by https://github.com/redis/redis-specifications/blob/master/protocol/RESP2.md + /// The protocol used by all redis server versions since 1.2, as defined by https://github.com/redis/redis-specifications/blob/master/protocol/RESP2.md. /// Resp2 = 2_00_00, // major__minor__revision + /// - /// Opt-in variant introduced in server version 6, as defined by https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md + /// Opt-in variant introduced in server version 6, as defined by https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md. /// Resp3 = 3_00_00, // major__minor__revision } diff --git a/src/StackExchange.Redis/RedisResult.cs b/src/StackExchange.Redis/RedisResult.cs index bf094f8af..4a1644c36 100644 --- a/src/StackExchange.Redis/RedisResult.cs +++ b/src/StackExchange.Redis/RedisResult.cs @@ -23,9 +23,9 @@ public RedisResult() : this(default) { } /// Create a new RedisResult representing a single value. /// /// The to create a result from. - /// The type of result being represented + /// The type of result being represented. /// new . - [SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads", Justification = "")] + [SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads", Justification = "Legacy compat.")] public static RedisResult Create(RedisValue value, ResultType? resultType = null) => new SingleRedisResult(value, resultType); /// @@ -95,10 +95,10 @@ public static RedisResult Create(RedisResult[] values, ResultType resultType) public sealed override string ToString() => ToString(out _) ?? ""; /// - /// Gets the string content as per , but also obtains the declared type from verbatim strings (for example LATENCY DOCTOR) + /// Gets the string content as per , but also obtains the declared type from verbatim strings (for example LATENCY DOCTOR). /// /// The type of the returned string. - /// The content + /// The content. public abstract string? ToString(out string? type); /// @@ -189,119 +189,142 @@ internal static bool TryCreate(PhysicalConnection? connection, in RawResult resu /// /// The result to convert to a . public static explicit operator string?(RedisResult? result) => result?.AsString(); + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator byte[]?(RedisResult? result) => result?.AsByteArray(); + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator double(RedisResult result) => result.AsDouble(); + /// /// Interprets the result as an . /// /// The result to convert to a . public static explicit operator long(RedisResult result) => result.AsInt64(); + /// /// Interprets the result as an . /// /// The result to convert to a . [CLSCompliant(false)] public static explicit operator ulong(RedisResult result) => result.AsUInt64(); + /// /// Interprets the result as an . /// /// The result to convert to a . public static explicit operator int(RedisResult result) => result.AsInt32(); + /// - /// Interprets the result as a + /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator bool(RedisResult result) => result.AsBoolean(); + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator RedisValue(RedisResult? result) => result?.AsRedisValue() ?? RedisValue.Null; + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator RedisKey(RedisResult? result) => result?.AsRedisKey() ?? default; + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator double?(RedisResult? result) => result?.AsNullableDouble(); + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator long?(RedisResult? result) => result?.AsNullableInt64(); + /// /// Interprets the result as a . /// /// The result to convert to a . [CLSCompliant(false)] public static explicit operator ulong?(RedisResult? result) => result?.AsNullableUInt64(); + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator int?(RedisResult? result) => result?.AsNullableInt32(); + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator bool?(RedisResult? result) => result?.AsNullableBoolean(); + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator string?[]?(RedisResult? result) => result?.AsStringArray(); + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator byte[]?[]?(RedisResult? result) => result?.AsByteArrayArray(); + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator double[]?(RedisResult? result) => result?.AsDoubleArray(); + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator long[]?(RedisResult? result) => result?.AsInt64Array(); + /// /// Interprets the result as a . /// /// The result to convert to a . [CLSCompliant(false)] public static explicit operator ulong[]?(RedisResult? result) => result?.AsUInt64Array(); + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator int[]?(RedisResult? result) => result?.AsInt32Array(); + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator bool[]?(RedisResult? result) => result?.AsBooleanArray(); + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator RedisValue[]?(RedisResult? result) => result?.AsRedisValueArray(); + /// /// Interprets the result as a . /// /// The result to convert to a . public static explicit operator RedisKey[]?(RedisResult? result) => result?.AsRedisKeyArray(); + /// /// Interprets the result as a . /// @@ -579,7 +602,8 @@ public SingleRedisResult(RedisValue value, ResultType? resultType) : base(value. type = null; string? s = _value; if (Resp3Type == ResultType.VerbatimString && s is not null && s.Length >= 4 && s[3] == ':') - { // remove the prefix + { + // remove the prefix type = s.Substring(0, 3); s = s.Substring(4); } @@ -652,7 +676,11 @@ decimal IConvertible.ToDecimal(IFormatProvider? provider) ThrowNotSupported(); return default; } - DateTime IConvertible.ToDateTime(IFormatProvider? provider) { ThrowNotSupported(); return default; } + DateTime IConvertible.ToDateTime(IFormatProvider? provider) + { + ThrowNotSupported(); + return default; + } string IConvertible.ToString(IFormatProvider? provider) => AsString()!; object IConvertible.ToType(Type conversionType, IFormatProvider? provider) { diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 1f7791dd2..8810e1e2b 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -224,24 +224,22 @@ private Message GetCommandListMessage(RedisValue? moduleName = null, RedisValue? { return Message.Create(-1, flags, RedisCommand.COMMAND, RedisLiterals.LIST); } - else if (moduleName != null && category == null && pattern == null) { return Message.Create(-1, flags, RedisCommand.COMMAND, MakeArray(RedisLiterals.LIST, RedisLiterals.FILTERBY, RedisLiterals.MODULE, (RedisValue)moduleName)); } - else if (moduleName == null && category != null && pattern == null) { return Message.Create(-1, flags, RedisCommand.COMMAND, MakeArray(RedisLiterals.LIST, RedisLiterals.FILTERBY, RedisLiterals.ACLCAT, (RedisValue)category)); } - else if (moduleName == null && category == null && pattern != null) { return Message.Create(-1, flags, RedisCommand.COMMAND, MakeArray(RedisLiterals.LIST, RedisLiterals.FILTERBY, RedisLiterals.PATTERN, (RedisValue)pattern)); } - else + { throw new ArgumentException("More then one filter is not allowed"); + } } private RedisValue[] AddValueToArray(RedisValue val, RedisValue[] arr) @@ -253,7 +251,7 @@ private RedisValue[] AddValueToArray(RedisValue val, RedisValue[] arr) return result; } - private RedisValue[] MakeArray(params RedisValue[] redisValues) { return redisValues; } + private RedisValue[] MakeArray(params RedisValue[] redisValues) => redisValues; public long DatabaseSize(int database = -1, CommandFlags flags = CommandFlags.None) { @@ -1048,7 +1046,7 @@ public Task ExecuteAsync(string command, ICollection args, } /// - /// For testing only + /// For testing only. /// internal void SimulateConnectionFailure(SimulatedFailureType failureType) => server.SimulateConnectionFailure(failureType); diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index cb4940e41..92c96ad6c 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -148,7 +148,7 @@ internal long EnsureSubscriptions(CommandFlags flags = CommandFlags.None) internal enum SubscriptionAction { Subscribe, - Unsubscribe + Unsubscribe, } /// @@ -396,7 +396,6 @@ internal bool EnsureSubscribedToServer(Subscription sub, RedisChannel channel, C if (sub.IsConnected) { return true; } // TODO: Cleanup old hangers here? - sub.SetCurrentServer(null); // we're not appropriately connected, so blank it out for eligible reconnection var message = sub.GetMessage(channel, SubscriptionAction.Subscribe, flags, internalCall); var selected = multiplexer.SelectServer(message); @@ -428,7 +427,6 @@ public Task EnsureSubscribedToServerAsync(Subscription sub, RedisChannel c if (sub.IsConnected) { return CompletedTask.Default(null); } // TODO: Cleanup old hangers here? - sub.SetCurrentServer(null); // we're not appropriately connected, so blank it out for eligible reconnection var message = sub.GetMessage(channel, SubscriptionAction.Subscribe, flags, internalCall); var selected = multiplexer.SelectServer(message); diff --git a/src/StackExchange.Redis/RedisTransaction.cs b/src/StackExchange.Redis/RedisTransaction.cs index 7f0d1a7ec..04d7293ac 100644 --- a/src/StackExchange.Redis/RedisTransaction.cs +++ b/src/StackExchange.Redis/RedisTransaction.cs @@ -219,6 +219,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes private class TransactionMessage : Message, IMultiMessage { private readonly ConditionResult[] conditions; + public QueuedMessage[] InnerOperations { get; } public TransactionMessage(int db, CommandFlags flags, List? conditions, List? operations) @@ -233,7 +234,7 @@ internal override void SetExceptionAndComplete(Exception exception, PhysicalBrid var inner = InnerOperations; if (inner != null) { - for(int i = 0; i < inner.Length;i++) + for (int i = 0; i < inner.Length; i++) { inner[i]?.Wrapped?.SetExceptionAndComplete(exception, bridge); } @@ -358,7 +359,8 @@ public IEnumerable GetMessages(PhysicalConnection connection) foreach (var op in InnerOperations) { if (explicitCheckForQueued) - { // need to have locked them before sending them + { + // need to have locked them before sending them // to guarantee that we see the pulse IResultBox? thisBox = op.ResultBox; if (thisBox != null) @@ -498,11 +500,11 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes SetResult(message, false); return true; } - //EXEC returned with a NULL + // EXEC returned with a NULL if (!tran.IsAborted && result.IsNull) { connection.Trace("Server aborted due to failed EXEC"); - //cancel the commands in the transaction and mark them as complete with the completion manager + // cancel the commands in the transaction and mark them as complete with the completion manager foreach (var op in wrapped) { var inner = op.Wrapped; @@ -536,7 +538,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes muxer?.OnTransactionLog($"Processing {arr.Length} wrapped messages"); int i = 0; - foreach(ref RawResult item in arr) + foreach (ref RawResult item in arr) { var inner = wrapped[i++].Wrapped; muxer?.OnTransactionLog($"> got {item} for {inner.CommandAndKey}"); diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index a0b045cf4..0eb1a5812 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -30,7 +30,8 @@ private RedisValue(long overlappedValue64, ReadOnlyMemory memory, object? } internal RedisValue(object obj, long overlappedBits) - { // this creates a bodged RedisValue which should **never** + { + // this creates a bodged RedisValue which should **never** // be seen directly; the contents are ... unexpected _overlappedBits64 = overlappedBits; _objectOrSentinel = obj; @@ -53,7 +54,7 @@ public RedisValue(string value) : this(0, default, value) { } private static readonly object Sentinel_Double = new(); /// - /// Obtain this value as an object - to be used alongside Unbox + /// Obtain this value as an object - to be used alongside Unbox. /// public object? Box() { @@ -244,7 +245,7 @@ private static int GetHashCode(RedisValue x) StorageType.Null => -1, StorageType.Double => x.OverlappedValueDouble.GetHashCode(), StorageType.Int64 or StorageType.UInt64 => x._overlappedBits64.GetHashCode(), - StorageType.Raw => ((string)x!).GetHashCode(),// to match equality + StorageType.Raw => ((string)x!).GetHashCode(), // to match equality _ => x._objectOrSentinel!.GetHashCode(), }; } @@ -291,13 +292,13 @@ internal static unsafe int GetHashCode(ReadOnlySpan span) for (int i = 0; i < span64.Length; i++) { var val = span64[i]; - int valHash = (((int)val) ^ ((int)(val >> 32))); - acc = (((acc << 5) + acc) ^ valHash); + int valHash = ((int)val) ^ ((int)(val >> 32)); + acc = ((acc << 5) + acc) ^ valHash; } int spare = len % 8, offset = len - spare; while (spare-- != 0) { - acc = (((acc << 5) + acc) ^ span[offset++]); + acc = ((acc << 5) + acc) ^ span[offset++]; } return acc; } @@ -310,7 +311,12 @@ internal void AssertNotNull() internal enum StorageType { - Null, Int64, UInt64, Double, Raw, String, + Null, + Int64, + UInt64, + Double, + Raw, + String, } internal StorageType Type @@ -330,7 +336,7 @@ internal StorageType Type } /// - /// Get the size of this value in bytes + /// Get the size of this value in bytes. /// public long Length() => Type switch { @@ -527,6 +533,7 @@ public static implicit operator RedisValue(ReadOnlyMemory value) if (value.Length == 0) return EmptyString; return new RedisValue(0, value, Sentinel_Raw); } + /// /// Creates a new from a . /// @@ -594,9 +601,9 @@ public static explicit operator long(RedisValue value) value = value.Simplify(); return value.Type switch { - StorageType.Null => 0,// in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") + StorageType.Null => 0, // in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") StorageType.Int64 => value.OverlappedValueInt64, - StorageType.UInt64 => checked((long)value.OverlappedValueUInt64),// this will throw since unsigned is always 64-bit + StorageType.UInt64 => checked((long)value.OverlappedValueUInt64), // this will throw since unsigned is always 64-bit _ => throw new InvalidCastException($"Unable to cast from {value.Type} to long: '{value}'"), }; } @@ -611,7 +618,7 @@ public static explicit operator uint(RedisValue value) value = value.Simplify(); return value.Type switch { - StorageType.Null => 0,// in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") + StorageType.Null => 0, // in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") StorageType.Int64 => checked((uint)value.OverlappedValueInt64), StorageType.UInt64 => checked((uint)value.OverlappedValueUInt64), _ => throw new InvalidCastException($"Unable to cast from {value.Type} to uint: '{value}'"), @@ -628,8 +635,8 @@ public static explicit operator ulong(RedisValue value) value = value.Simplify(); return value.Type switch { - StorageType.Null => 0,// in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") - StorageType.Int64 => checked((ulong)value.OverlappedValueInt64),// throw if negative + StorageType.Null => 0, // in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") + StorageType.Int64 => checked((ulong)value.OverlappedValueInt64), // throw if negative StorageType.UInt64 => value.OverlappedValueUInt64, _ => throw new InvalidCastException($"Unable to cast from {value.Type} to ulong: '{value}'"), }; @@ -644,7 +651,7 @@ public static explicit operator double(RedisValue value) value = value.Simplify(); return value.Type switch { - StorageType.Null => 0,// in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") + StorageType.Null => 0, // in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") StorageType.Int64 => value.OverlappedValueInt64, StorageType.UInt64 => value.OverlappedValueUInt64, StorageType.Double => value.OverlappedValueDouble, @@ -661,7 +668,7 @@ public static explicit operator decimal(RedisValue value) value = value.Simplify(); return value.Type switch { - StorageType.Null => 0,// in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") + StorageType.Null => 0, // in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") StorageType.Int64 => value.OverlappedValueInt64, StorageType.UInt64 => value.OverlappedValueUInt64, StorageType.Double => (decimal)value.OverlappedValueDouble, @@ -678,7 +685,7 @@ public static explicit operator float(RedisValue value) value = value.Simplify(); return value.Type switch { - StorageType.Null => 0,// in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") + StorageType.Null => 0, // in redis, an arithmetic zero is kinda the same thing as not-exists (think "incr") StorageType.Int64 => value.OverlappedValueInt64, StorageType.UInt64 => value.OverlappedValueUInt64, StorageType.Double => (float)value.OverlappedValueDouble, @@ -908,7 +915,7 @@ object IConvertible.ToType(Type conversionType, IFormatProvider? provider) /// but more importantly b: because it can change values - for example, if they start /// with "123.000", it should **stay** as "123.000", not become 123L; this could be /// a hash key or similar - we don't want to break it; RedisConnection uses - /// the storage type, not the "does it look like a long?" - for this reason + /// the storage type, not the "does it look like a long?" - for this reason. /// internal RedisValue Simplify() { @@ -965,8 +972,15 @@ public bool TryParse(out long val) return Format.TryParseInt64(_memory.Span, out val); case StorageType.Double: var d = OverlappedValueDouble; - try { val = (long)d; } - catch { val = default; return false; } + try + { + val = (long)d; + } + catch + { + val = default; + return false; + } return val == d; case StorageType.Null: // in redis-land 0 approx. equal null; so roll with it @@ -1083,8 +1097,8 @@ public bool StartsWith(RedisValue value) switch (thisType) { case StorageType.String: - var sThis = ((string)_objectOrSentinel!); - var sOther = ((string)value._objectOrSentinel!); + var sThis = (string)_objectOrSentinel!; + var sOther = (string)value._objectOrSentinel!; return sThis.StartsWith(sOther, StringComparison.Ordinal); case StorageType.Raw: rawThis = _memory; diff --git a/src/StackExchange.Redis/ResultBox.cs b/src/StackExchange.Redis/ResultBox.cs index c61221018..20b76ba15 100644 --- a/src/StackExchange.Redis/ResultBox.cs +++ b/src/StackExchange.Redis/ResultBox.cs @@ -102,7 +102,7 @@ private TaskResultBox(object? asyncState, TaskCreationOptions creationOptions) : void IResultBox.SetResult(T value) => _value = value; - T? IResultBox.GetResult(out Exception? ex, bool _) + T? IResultBox.GetResult(out Exception? ex, bool unused) { ex = _exception; return _value; @@ -145,9 +145,10 @@ public static IResultBox Create(out TaskCompletionSource source, object? a // how it is being used in those 2 different ways; also, the *fact* that they // are the same underlying object is an implementation detail that the rest of // the code doesn't need to know about - var obj = new TaskResultBox(asyncState, ConnectionMultiplexer.PreventThreadTheft - ? TaskCreationOptions.None // if we don't trust the TPL/sync-context, avoid a double QUWI dispatch - : TaskCreationOptions.RunContinuationsAsynchronously); + var obj = new TaskResultBox( + asyncState, + // if we don't trust the TPL/sync-context, avoid a double QUWI dispatch + ConnectionMultiplexer.PreventThreadTheft ? TaskCreationOptions.None : TaskCreationOptions.RunContinuationsAsynchronously); source = obj; return obj; } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 95dc4bd87..894e6f5f9 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.Logging; -using Pipelines.Sockets.Unofficial.Arenas; -using System; +using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; @@ -10,6 +8,8 @@ using System.Net; using System.Text; using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Pipelines.Sockets.Unofficial.Arenas; namespace StackExchange.Redis { @@ -402,7 +402,8 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return false; } else - { // don't check the actual reply; there are multiple ways of constructing + { + // don't check the actual reply; there are multiple ways of constructing // a timing message, and we don't actually care about what approach was used TimeSpan duration; if (message is TimerMessage timingMessage) @@ -465,7 +466,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var newServer = message.Command switch { RedisCommand.SUBSCRIBE or RedisCommand.PSUBSCRIBE => connection.BridgeCouldBeNull?.ServerEndPoint, - _ => null + _ => null, }; Subscription?.SetCurrentServer(newServer); return true; @@ -485,8 +486,16 @@ public static bool TryGet(in RawResult result, out bool value) case ResultType.Integer: case ResultType.SimpleString: case ResultType.BulkString: - if (result.IsEqual(CommonReplies.one)) { value = true; return true; } - else if (result.IsEqual(CommonReplies.zero)) { value = false; return true; } + if (result.IsEqual(CommonReplies.one)) + { + value = true; + return true; + } + else if (result.IsEqual(CommonReplies.zero)) + { + value = false; + return true; + } break; } value = false; @@ -651,7 +660,6 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - internal sealed class HashEntryArrayProcessor : ValuePairInterleavedProcessorBase { protected override HashEntry Parse(in RawResult first, in RawResult second, object? state) => @@ -900,7 +908,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (message?.Command == RedisCommand.CONFIG) { var iter = result.GetItems().GetEnumerator(); - while(iter.MoveNext()) + while (iter.MoveNext()) { ref RawResult key = ref iter.Current; if (!iter.MoveNext()) break; @@ -1137,9 +1145,13 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes case ResultType.BulkString: string nodes = result.GetString()!; try - { ClusterNodesProcessor.Parse(connection, nodes); } + { + ClusterNodesProcessor.Parse(connection, nodes); + } catch - { /* tralalalala */} + { + /* tralalalala */ + } SetResult(message, nodes); return true; } @@ -1217,7 +1229,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes // -1 means no expiry and -2 means key does not exist < 0 => null, _ when isMilliseconds => RedisBase.UnixEpoch.AddMilliseconds(duration), - _ => RedisBase.UnixEpoch.AddSeconds(duration) + _ => RedisBase.UnixEpoch.AddSeconds(duration), }; SetResult(message, expiry); return true; @@ -1275,7 +1287,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes SetResult(message, true); return true; } - if(message.Command == RedisCommand.AUTH) connection?.BridgeCouldBeNull?.Multiplexer?.SetAuthSuspect(new RedisException("Unknown AUTH exception")); + if (message.Command == RedisCommand.AUTH) connection?.BridgeCouldBeNull?.Multiplexer?.SetAuthSuspect(new RedisException("Unknown AUTH exception")); return false; } } @@ -1716,7 +1728,7 @@ The geohash integer. /// /// Parser for the https://redis.io/commands/lcs/ format with the and arguments. /// - /// + /// /// Example response: /// 1) "matches" /// 2) 1) 1) 1) (integer) 4 @@ -1726,7 +1738,8 @@ The geohash integer. /// 3) (integer) 4 /// 3) "len" /// 4) (integer) 6 - /// + /// ... + /// private sealed class LongestCommonSubsequenceProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) @@ -2087,15 +2100,16 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } else { - streams = result.GetItems().ToArray((in RawResult item, in MultiStreamProcessor obj) => - { - var details = item.GetItems(); + streams = result.GetItems().ToArray( + (in RawResult item, in MultiStreamProcessor obj) => + { + var details = item.GetItems(); - // details[0] = Name of the Stream - // details[1] = Multibulk Array of Stream Entries - return new RedisStream(key: details[0].AsRedisKey(), - entries: obj.ParseRedisStreamEntries(details[1])!); - }, this); + // details[0] = Name of the Stream + // details[1] = Multibulk Array of Stream Entries + return new RedisStream(key: details[0].AsRedisKey(), entries: obj.ParseRedisStreamEntries(details[1])!); + }, + this); } SetResult(message, streams); @@ -2191,7 +2205,6 @@ protected override StreamConsumerInfo ParseItem(in RawResult result) // 4) (integer)1 // 5) idle // 6) (integer)83841983 - var arr = result.GetItems(); string? name = default; int pendingMessageCount = default; @@ -2235,7 +2248,8 @@ internal static bool TryRead(Sequence pairs, in CommandBytes key, ref internal static bool TryRead(Sequence pairs, in CommandBytes key, ref int value) { long tmp = default; - if(TryRead(pairs, key, ref tmp)) { + if (TryRead(pairs, key, ref tmp)) + { value = checked((int)tmp); return true; } @@ -2289,7 +2303,6 @@ protected override StreamGroupInfo ParseItem(in RawResult result) // 10) (integer)1 // 11) "lag" // 12) (integer)1 - var arr = result.GetItems(); string? name = default, lastDeliveredId = default; int consumerCount = default, pendingMessageCount = default; @@ -2361,13 +2374,13 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var lastGeneratedId = Redis.RedisValue.Null; StreamEntry firstEntry = StreamEntry.Null, lastEntry = StreamEntry.Null; var iter = arr.GetEnumerator(); - for(int i = 0; i < max; i++) + for (int i = 0; i < max; i++) { ref RawResult key = ref iter.GetNext(), value = ref iter.GetNext(); if (key.Payload.Length > CommandBytes.MaxLength) continue; var keyBytes = new CommandBytes(key.Payload); - if(keyBytes.Equals(CommonReplies.length)) + if (keyBytes.Equals(CommonReplies.length)) { if (!value.TryGetInt64(out length)) return false; } @@ -2424,7 +2437,6 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes // 2) "2" // 5) 1) 1) "Joe" // 2) "8" - if (result.Resp2TypeArray != ResultType.Array) { return false; @@ -2453,12 +2465,11 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes }); } - var pendingInfo = new StreamPendingInfo(pendingMessageCount: (int)arr[0].AsRedisValue(), + var pendingInfo = new StreamPendingInfo( + pendingMessageCount: (int)arr[0].AsRedisValue(), lowestId: arr[1].AsRedisValue(), highestId: arr[2].AsRedisValue(), consumers: consumers ?? Array.Empty()); - // ^^^^^ - // Should we bother allocating an empty array only to prevent the need for a null check? SetResult(message, pendingInfo); return true; @@ -2478,7 +2489,8 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { var details = item.GetItems().GetEnumerator(); - return new StreamPendingMessageInfo(messageId: details.GetNext().AsRedisValue(), + return new StreamPendingMessageInfo( + messageId: details.GetNext().AsRedisValue(), consumerName: details.GetNext().AsRedisValue(), idleTimeInMs: (long)details.GetNext().AsRedisValue(), deliveryCount: (int)details.GetNext().AsRedisValue()); @@ -2500,6 +2512,7 @@ protected override NameValueEntry Parse(in RawResult first, in RawResult second, /// /// Handles stream responses. For formats, see . /// + /// The type of the stream result. internal abstract class StreamProcessorBase : ResultProcessor { protected static StreamEntry ParseRedisStreamEntry(in RawResult item) @@ -2513,7 +2526,8 @@ protected static StreamEntry ParseRedisStreamEntry(in RawResult item) // [1] = Multibulk array of the name/value pairs of the stream entry's data var entryDetails = item.GetItems(); - return new StreamEntry(id: entryDetails[0].AsRedisValue(), + return new StreamEntry( + id: entryDetails[0].AsRedisValue(), values: ParseStreamEntryValues(entryDetails[1])); } protected internal StreamEntry[] ParseRedisStreamEntries(in RawResult result) => @@ -2535,7 +2549,6 @@ protected static NameValueEntry[] ParseStreamEntryValues(in RawResult result) // 2) "9999" // 3) "temperature" // 4) "18.2" - if (result.Resp2TypeArray != ResultType.Array || result.IsNull) { return Array.Empty(); @@ -2686,7 +2699,8 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } else { - connection.RecordConnectionFailed(ConnectionFailureType.ProtocolFailure, + connection.RecordConnectionFailed( + ConnectionFailureType.ProtocolFailure, new InvalidOperationException($"unexpected tracer reply to {message.Command}: {result.ToString()}")); return false; } @@ -2819,7 +2833,8 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { throw new ArgumentOutOfRangeException(nameof(rawInnerArray), $"Error processing {message.CommandAndKey}, could not decode array '{rawInnerArray}'"); } - }, innerProcessor)!; + }, + innerProcessor)!; SetResult(message, returnArray); return true; @@ -2845,7 +2860,7 @@ internal abstract class ArrayResultProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { - switch(result.Resp2TypeArray) + switch (result.Resp2TypeArray) { case ResultType.Array: var items = result.GetItems(); diff --git a/src/StackExchange.Redis/ScriptParameterMapper.cs b/src/StackExchange.Redis/ScriptParameterMapper.cs index 9960d87b7..10dccd5ff 100644 --- a/src/StackExchange.Redis/ScriptParameterMapper.cs +++ b/src/StackExchange.Redis/ScriptParameterMapper.cs @@ -154,7 +154,7 @@ public static LuaScript PrepareScript(string script) typeof(bool?), typeof(RedisKey), - typeof(RedisValue) + typeof(RedisValue), }; /// @@ -252,20 +252,18 @@ public static bool IsValidParameterHash(Type t, LuaScript script, out string? mi else { var needsKeyPrefix = Expression.Property(keyPrefix, nameof(Nullable.HasValue)); - var keyPrefixValueArr = new[] { Expression.Call(keyPrefix, - nameof(Nullable.GetValueOrDefault), null, null) }; - var prepend = typeof(RedisKey).GetMethod(nameof(RedisKey.Prepend), - BindingFlags.Public | BindingFlags.Instance)!; - asRedisValue = typeof(RedisKey).GetMethod(nameof(RedisKey.AsRedisValue), - BindingFlags.NonPublic | BindingFlags.Instance)!; + var keyPrefixValueArr = new[] + { + Expression.Call(keyPrefix, nameof(Nullable.GetValueOrDefault), null, null), + }; + var prepend = typeof(RedisKey).GetMethod(nameof(RedisKey.Prepend), BindingFlags.Public | BindingFlags.Instance)!; + asRedisValue = typeof(RedisKey).GetMethod(nameof(RedisKey.AsRedisValue), BindingFlags.NonPublic | BindingFlags.Instance)!; keysResultArr = new Expression[keys.Count]; for (int i = 0; i < keysResultArr.Length; i++) { var member = GetMember(objTyped, keys[i]); - keysResultArr[i] = Expression.Condition(needsKeyPrefix, - Expression.Call(member, prepend, keyPrefixValueArr), - member); + keysResultArr[i] = Expression.Condition(needsKeyPrefix, Expression.Call(member, prepend, keyPrefixValueArr), member); } keysResult = Expression.NewArrayInit(typeof(RedisKey), keysResultArr); } @@ -294,8 +292,7 @@ public static bool IsValidParameterHash(Type t, LuaScript script, out string? mi } var body = Expression.Lambda>( - Expression.New(ScriptParameters.Cons, keysResult, valuesResult), - objUntyped, keyPrefix); + Expression.New(ScriptParameters.Cons, keysResult, valuesResult), objUntyped, keyPrefix); return body.Compile(); } } diff --git a/src/StackExchange.Redis/ServerCounters.cs b/src/StackExchange.Redis/ServerCounters.cs index f6d96c6a6..b661f27d7 100644 --- a/src/StackExchange.Redis/ServerCounters.cs +++ b/src/StackExchange.Redis/ServerCounters.cs @@ -46,7 +46,7 @@ public ServerCounters(EndPoint? endpoint) public long TotalOutstanding => Interactive.TotalOutstanding + Subscription.TotalOutstanding + Other.TotalOutstanding; /// - /// See Object.ToString(); + /// See . /// public override string ToString() { diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 2cbe36920..8b099afd2 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Logging; -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -9,6 +8,7 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using static StackExchange.Redis.PhysicalBridge; namespace StackExchange.Redis @@ -84,7 +84,7 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) /// This is memoized because it's accessed on hot paths inside the write lock. /// public bool SupportsDatabases => - supportsDatabases ??= (serverType == ServerType.Standalone && Multiplexer.CommandMap.IsAvailable(RedisCommand.SELECT)); + supportsDatabases ??= serverType == ServerType.Standalone && Multiplexer.CommandMap.IsAvailable(RedisCommand.SELECT); public int Databases { @@ -105,7 +105,7 @@ public bool KnowOrAssumeResp3() } public bool SupportsSubscriptions => Multiplexer.CommandMap.IsAvailable(RedisCommand.SUBSCRIBE); - public bool SupportsPrimaryWrites => supportsPrimaryWrites ??= (!IsReplica || !ReplicaReadOnly || AllowReplicaWrites); + public bool SupportsPrimaryWrites => supportsPrimaryWrites ??= !IsReplica || !ReplicaReadOnly || AllowReplicaWrites; private readonly List> _pendingConnectionMonitors = new List>(); @@ -157,7 +157,7 @@ internal Exception? LastException var subEx = subscription?.LastException; var subExData = subEx?.Data; - //check if subscription endpoint has a better last exception + // check if subscription endpoint has a better last exception if (subExData != null && subExData.Contains("Redis-FailureType") && subExData["Redis-FailureType"]?.ToString() != nameof(ConnectionFailureType.UnableToConnect)) { return subEx; @@ -208,7 +208,7 @@ public Version Version } /// - /// If we have a connection (interactive), report the protocol being used + /// If we have a connection (interactive), report the protocol being used. /// public RedisProtocol? Protocol => interactive?.Protocol; @@ -254,7 +254,6 @@ public void Dispose() // Subscription commands go to a specific bridge - so we need to set that up. // There are other commands we need to send to the right connection (e.g. subscriber PING with an explicit SetForSubscriptionBridge call), // but these always go subscriber. - switch (message.Command) { case RedisCommand.SUBSCRIBE: @@ -464,8 +463,9 @@ internal async Task AutoConfigureAsync(PhysicalConnection? connection, ILogger? } private int _nextReplicaOffset; + /// - /// Used to round-robin between multiple replicas + /// Used to round-robin between multiple replicas. /// internal uint NextReplicaOffset() => (uint)Interlocked.Increment(ref _nextReplicaOffset); @@ -533,7 +533,8 @@ internal BridgeStatus GetBridgeStatus(ConnectionType connectionType) return GetBridge(connectionType, false)?.GetStatus() ?? BridgeStatus.Zero; } catch (Exception ex) - { // only needs to be best efforts + { + // only needs to be best efforts System.Diagnostics.Debug.WriteLine(ex.Message); } @@ -872,6 +873,7 @@ internal string Summary() /// /// Write the message directly to the pipe or fail...will not queue. /// + /// The type of the result processor. internal ValueTask WriteDirectOrQueueFireAndForgetAsync(PhysicalConnection? connection, Message message, ResultProcessor processor) { static async ValueTask Awaited(ValueTask l_result) => await l_result.ForAwait(); @@ -966,7 +968,6 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) // that's fine and doesn't cause a problem; if we wanted we could probably just discard (`_ =`) // the various tasks and just `return connection.FlushAsync();` - however, since handshake is low // volume, we can afford to optimize for a good stack-trace rather than avoiding state machines. - ResultProcessor? autoConfig = null; if (Multiplexer.RawConfig.TryResp3()) // note this includes an availability check on HELLO { @@ -1020,8 +1021,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) var libName = Multiplexer.GetFullLibraryName(); if (!string.IsNullOrWhiteSpace(libName)) { - msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, - RedisLiterals.SETINFO, RedisLiterals.lib_name, libName); + msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, RedisLiterals.SETINFO, RedisLiterals.lib_name, libName); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } @@ -1029,8 +1029,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) var version = ClientInfoSanitize(Utils.GetLibVersion()); if (!string.IsNullOrWhiteSpace(version)) { - msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, - RedisLiterals.SETINFO, RedisLiterals.lib_ver, version); + msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, RedisLiterals.SETINFO, RedisLiterals.lib_ver, version); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } @@ -1086,7 +1085,7 @@ private void SetConfig(ref T field, T value, [CallerMemberName] string? calle } } internal static string ClientInfoSanitize(string? value) - => string.IsNullOrWhiteSpace(value) ? "" : nameSanitizer.Replace(value!.Trim(), "-"); + => string.IsNullOrWhiteSpace(value) ? "" : nameSanitizer.Replace(value!.Trim(), "-"); private void ClearMemoized() { @@ -1095,7 +1094,7 @@ private void ClearMemoized() } /// - /// For testing only + /// For testing only. /// internal void SimulateConnectionFailure(SimulatedFailureType failureType) { diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index 4a32da0d4..44241d373 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -10,40 +10,40 @@ internal sealed class ServerSelectionStrategy { public const int NoSlot = -1, MultipleSlots = -2; private const int RedisClusterSlotCount = 16384; - private static readonly ushort[] s_crc16tab = new ushort[] + private static readonly ushort[] Crc16tab = new ushort[] { - 0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7, - 0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef, - 0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6, - 0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de, - 0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485, - 0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d, - 0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4, - 0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc, - 0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823, - 0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b, - 0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12, - 0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a, - 0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41, - 0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49, - 0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70, - 0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78, - 0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f, - 0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067, - 0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e, - 0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256, - 0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d, - 0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405, - 0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c, - 0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634, - 0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab, - 0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3, - 0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a, - 0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92, - 0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9, - 0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1, - 0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8, - 0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0 + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, }; private readonly ConnectionMultiplexer multiplexer; @@ -103,7 +103,7 @@ public int HashSlot(in RedisChannel channel) /// Gets the hashslot for a given byte sequence. /// /// - /// HASH_SLOT = CRC16(key) mod 16384 + /// HASH_SLOT = CRC16(key) mod 16384. /// private static unsafe int GetClusterSlot(ReadOnlySpan blob) { @@ -111,7 +111,7 @@ private static unsafe int GetClusterSlot(ReadOnlySpan blob) { fixed (byte* ptr = blob) { - fixed (ushort* crc16tab = s_crc16tab) + fixed (ushort* crc16tab = ServerSelectionStrategy.Crc16tab) { int offset = 0, count = blob.Length, start, end; if ((start = IndexOf(ptr, (byte)'{', 0, count - 1)) >= 0 @@ -288,7 +288,8 @@ private static unsafe int IndexOf(byte* ptr, byte value, int start, int end) if (!cursor.IsReplica && cursor.IsSelectable(command)) return cursor; cursor = cursor.Primary; - } while (cursor != null && --max != 0); + } + while (cursor != null && --max != 0); return null; } @@ -330,8 +331,7 @@ private ServerEndPoint[] MapForMutation() if (slot == NoSlot || (arr = map) == null) return Any(command, flags, allowDisconnected); ServerEndPoint endpoint = arr[slot]; - ServerEndPoint? testing; - // but: ^^^ is the PRIMARY slots; if we want a replica, we need to do some thinking + ServerEndPoint? testing; // but: ^^^ is the PRIMARY slots; if we want a replica, we need to do some thinking if (endpoint != null) { diff --git a/src/StackExchange.Redis/SkipLocalsInit.cs b/src/StackExchange.Redis/SkipLocalsInit.cs index 66f84567e..353b00142 100644 --- a/src/StackExchange.Redis/SkipLocalsInit.cs +++ b/src/StackExchange.Redis/SkipLocalsInit.cs @@ -9,6 +9,6 @@ namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Interface, Inherited = false)] - internal sealed class SkipLocalsInitAttribute : Attribute {} + internal sealed class SkipLocalsInitAttribute : Attribute { } } #endif diff --git a/src/StackExchange.Redis/SocketManager.cs b/src/StackExchange.Redis/SocketManager.cs index a0c755834..146e576ff 100644 --- a/src/StackExchange.Redis/SocketManager.cs +++ b/src/StackExchange.Redis/SocketManager.cs @@ -41,7 +41,7 @@ public SocketManager(string name, bool useHighPrioritySocketThreads) /// the number of dedicated workers for this . /// Whether this should use high priority sockets. public SocketManager(string name, int workerCount, bool useHighPrioritySocketThreads) - : this(name, workerCount, UseHighPrioritySocketThreads(useHighPrioritySocketThreads)) {} + : this(name, workerCount, UseHighPrioritySocketThreads(useHighPrioritySocketThreads)) { } private static SocketManagerOptions UseHighPrioritySocketThreads(bool value) => value ? SocketManagerOptions.UseHighPrioritySocketThreads : SocketManagerOptions.None; @@ -56,12 +56,14 @@ public enum SocketManagerOptions /// No additional options. /// None = 0, + /// /// Whether the should use high priority sockets. /// UseHighPrioritySocketThreads = 1 << 0, + /// - /// Use the regular thread-pool for all scheduling + /// Use the regular thread-pool for all scheduling. /// UseThreadPool = 1 << 1, } @@ -70,8 +72,8 @@ public enum SocketManagerOptions /// Creates a new (optionally named) instance. /// /// The name for this . - /// the number of dedicated workers for this . - /// + /// The number of dedicated workers for this . + /// Options to use when creating the socket manager. public SocketManager(string? name = null, int workerCount = 0, SocketManagerOptions options = SocketManagerOptions.None) { if (name.IsNullOrWhiteSpace()) name = GetType().Name; @@ -85,17 +87,18 @@ public SocketManager(string? name = null, int workerCount = 0, SocketManagerOpti var defaultPipeOptions = PipeOptions.Default; - long Send_PauseWriterThreshold = Math.Max( - 512 * 1024,// send: let's give it up to 0.5MiB + long send_PauseWriterThreshold = Math.Max( + 512 * 1024, // send: let's give it up to 0.5MiB defaultPipeOptions.PauseWriterThreshold); // or the default, whichever is bigger - long Send_ResumeWriterThreshold = Math.Max( - Send_PauseWriterThreshold / 2, + long send_ResumeWriterThreshold = Math.Max( + send_PauseWriterThreshold / 2, defaultPipeOptions.ResumeWriterThreshold); Scheduler = PipeScheduler.ThreadPool; if (!useThreadPool) { - Scheduler = new DedicatedThreadPoolPipeScheduler(name + ":IO", + Scheduler = new DedicatedThreadPoolPipeScheduler( + name: name + ":IO", workerCount: workerCount, priority: useHighPrioritySocketThreads ? ThreadPriority.AboveNormal : ThreadPriority.Normal); } @@ -103,8 +106,8 @@ public SocketManager(string? name = null, int workerCount = 0, SocketManagerOpti pool: defaultPipeOptions.Pool, readerScheduler: Scheduler, writerScheduler: Scheduler, - pauseWriterThreshold: Send_PauseWriterThreshold, - resumeWriterThreshold: Send_ResumeWriterThreshold, + pauseWriterThreshold: send_PauseWriterThreshold, + resumeWriterThreshold: send_ResumeWriterThreshold, minimumSegmentSize: Math.Max(defaultPipeOptions.MinimumSegmentSize, MINIMUM_SEGMENT_SIZE), useSynchronizationContext: false); ReceivePipeOptions = new PipeOptions( @@ -220,7 +223,7 @@ internal static Socket CreateSocket(EndPoint endpoint) ? new Socket(SocketType.Stream, protocolType) : new Socket(addressFamily, SocketType.Stream, protocolType); SocketConnection.SetRecommendedClientOptions(socket); - //socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Linger, false); + // socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Linger, false); return socket; } diff --git a/src/StackExchange.Redis/StreamConstants.cs b/src/StackExchange.Redis/StreamConstants.cs index 409653c72..74650e010 100644 --- a/src/StackExchange.Redis/StreamConstants.cs +++ b/src/StackExchange.Redis/StreamConstants.cs @@ -1,5 +1,4 @@ - -namespace StackExchange.Redis +namespace StackExchange.Redis { /// /// Constants representing values used in Redis Stream commands. diff --git a/src/StackExchange.Redis/TaskExtensions.cs b/src/StackExchange.Redis/TaskExtensions.cs index 0a70cfc09..081a691ec 100644 --- a/src/StackExchange.Redis/TaskExtensions.cs +++ b/src/StackExchange.Redis/TaskExtensions.cs @@ -40,7 +40,7 @@ internal static Task ObserveErrors(this Task task) /// Licensed to the .NET Foundation under one or more agreements. /// The .NET Foundation licenses this file to you under the MIT license. /// - /// Inspired from + /// Inspired from . internal static async Task TimeoutAfter(this Task task, int timeoutMs) { var cts = new CancellationTokenSource(); diff --git a/tests/BasicTest/Program.cs b/tests/BasicTest/Program.cs index 5c8ef687c..faff1b7d7 100644 --- a/tests/BasicTest/Program.cs +++ b/tests/BasicTest/Program.cs @@ -21,7 +21,7 @@ internal class CustomConfig : ManualConfig { protected virtual Job Configure(Job j) => j.WithGcMode(new GcMode { Force = true }) - //.With(InProcessToolchain.Instance) + // .With(InProcessToolchain.Instance) ; public CustomConfig() @@ -41,9 +41,7 @@ protected override Job Configure(Job j) .WithWarmupCount(1) .WithIterationCount(5); } - /// - /// The tests - /// + [Config(typeof(CustomConfig))] public class RedisBenchmarks : IDisposable { @@ -51,14 +49,10 @@ public class RedisBenchmarks : IDisposable private ConnectionMultiplexer connection; private IDatabase db; - /// - /// Create - /// [GlobalSetup] public void Setup() { // Pipelines.Sockets.Unofficial.SocketConnection.AssertDependencies(); - var options = ConfigurationOptions.Parse("127.0.0.1:6379"); connection = ConnectionMultiplexer.Connect(options); db = connection.GetDatabase(3); @@ -88,10 +82,9 @@ void IDisposable.Dispose() private const int COUNT = 50; /// - /// Run INCRBY lots of times + /// Run INCRBY lots of times. /// // [Benchmark(Description = "INCRBY/s", OperationsPerInvoke = COUNT)] - public int ExecuteIncrBy() { var rand = new Random(12345); @@ -110,7 +103,7 @@ public int ExecuteIncrBy() } /// - /// Run INCRBY lots of times + /// Run INCRBY lots of times. /// // [Benchmark(Description = "INCRBY/a", OperationsPerInvoke = COUNT)] public async Task ExecuteIncrByAsync() @@ -131,7 +124,7 @@ public async Task ExecuteIncrByAsync() } /// - /// Run GEORADIUS lots of times + /// Run GEORADIUS lots of times. /// // [Benchmark(Description = "GEORADIUS/s", OperationsPerInvoke = COUNT)] public int ExecuteGeoRadius() @@ -139,15 +132,14 @@ public int ExecuteGeoRadius() int total = 0; for (int i = 0; i < COUNT; i++) { - var results = db.GeoRadius(GeoKey, 15, 37, 200, GeoUnit.Kilometers, - options: GeoRadiusOptions.WithCoordinates | GeoRadiusOptions.WithDistance | GeoRadiusOptions.WithGeoHash); + var results = db.GeoRadius(GeoKey, 15, 37, 200, GeoUnit.Kilometers, options: GeoRadiusOptions.WithCoordinates | GeoRadiusOptions.WithDistance | GeoRadiusOptions.WithGeoHash); total += results.Length; } return total; } /// - /// Run GEORADIUS lots of times + /// Run GEORADIUS lots of times. /// // [Benchmark(Description = "GEORADIUS/a", OperationsPerInvoke = COUNT)] public async Task ExecuteGeoRadiusAsync() @@ -155,15 +147,14 @@ public async Task ExecuteGeoRadiusAsync() int total = 0; for (int i = 0; i < COUNT; i++) { - var results = await db.GeoRadiusAsync(GeoKey, 15, 37, 200, GeoUnit.Kilometers, - options: GeoRadiusOptions.WithCoordinates | GeoRadiusOptions.WithDistance | GeoRadiusOptions.WithGeoHash).ConfigureAwait(false); + var results = await db.GeoRadiusAsync(GeoKey, 15, 37, 200, GeoUnit.Kilometers, options: GeoRadiusOptions.WithCoordinates | GeoRadiusOptions.WithDistance | GeoRadiusOptions.WithGeoHash).ConfigureAwait(false); total += results.Length; } return total; } /// - /// Run StringSet lots of times + /// Run StringSet lots of times. /// [Benchmark(Description = "StringSet/s", OperationsPerInvoke = COUNT)] public void StringSet() @@ -175,7 +166,7 @@ public void StringSet() } /// - /// Run StringGet lots of times + /// Run StringGet lots of times. /// [Benchmark(Description = "StringGet/s", OperationsPerInvoke = COUNT)] public void StringGet() @@ -187,7 +178,7 @@ public void StringGet() } /// - /// Run HashGetAll lots of times + /// Run HashGetAll lots of times. /// [Benchmark(Description = "HashGetAll F+F/s", OperationsPerInvoke = COUNT)] public void HashGetAll_FAF() @@ -200,7 +191,7 @@ public void HashGetAll_FAF() } /// - /// Run HashGetAll lots of times + /// Run HashGetAll lots of times. /// [Benchmark(Description = "HashGetAll F+F/a", OperationsPerInvoke = COUNT)] diff --git a/tests/ConsoleTest/Program.cs b/tests/ConsoleTest/Program.cs index 39e00701e..98d96f259 100644 --- a/tests/ConsoleTest/Program.cs +++ b/tests/ConsoleTest/Program.cs @@ -1,6 +1,6 @@ -using StackExchange.Redis; -using System.Diagnostics; +using System.Diagnostics; using System.Reflection; +using StackExchange.Redis; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); @@ -11,7 +11,7 @@ Console.WriteLine($"{nameof(options.HighIntegrity)}: {options.HighIntegrity}"); #endif -//options.SocketManager = SocketManager.ThreadPool; +// options.SocketManager = SocketManager.ThreadPool; Console.WriteLine("Connecting..."); var connection = ConnectionMultiplexer.Connect(options); Console.WriteLine("Connected"); @@ -118,8 +118,8 @@ static async Task MassInsertAsync(ConnectionMultiplexer connection) matchErrors += await ValidateAsync(outstanding); Console.WriteLine(i); } - } + matchErrors += await ValidateAsync(outstanding); Console.WriteLine($"Match errors: {matchErrors}"); @@ -141,7 +141,6 @@ static async Task ValidateAsync(List<(Task, Task, string)> outs } } - static void ParallelTasks(ConnectionMultiplexer connection) { static void ParallelRun(int taskId, ConnectionMultiplexer connection) @@ -164,14 +163,14 @@ static void ParallelRun(int taskId, ConnectionMultiplexer connection) } var taskList = new List(); - for (int i = 0; i < 10; i++) - { - var i1 = i; - var task = new Task(() => ParallelRun(i1, connection)); - task.Start(); - taskList.Add(task); - } - Task.WaitAll(taskList.ToArray()); + for (int i = 0; i < 10; i++) + { + var i1 = i; + var task = new Task(() => ParallelRun(i1, connection)); + task.Start(); + taskList.Add(task); + } + Task.WaitAll(taskList.ToArray()); } static void MassPublish(ConnectionMultiplexer connection) @@ -182,8 +181,8 @@ static void MassPublish(ConnectionMultiplexer connection) static string GetLibVersion() { - var assembly = typeof(ConnectionMultiplexer).Assembly; - return (Attribute.GetCustomAttribute(assembly, typeof(AssemblyFileVersionAttribute)) as AssemblyFileVersionAttribute)?.Version - ?? assembly.GetName().Version?.ToString() - ?? "Unknown"; + var assembly = typeof(ConnectionMultiplexer).Assembly; + return (Attribute.GetCustomAttribute(assembly, typeof(AssemblyFileVersionAttribute)) as AssemblyFileVersionAttribute)?.Version + ?? assembly.GetName().Version?.ToString() + ?? "Unknown"; } diff --git a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs index cfb96a547..2920a51d3 100644 --- a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs +++ b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs @@ -1,6 +1,6 @@ -using StackExchange.Redis.Tests.Helpers; -using System; +using System; using System.Threading.Tasks; +using StackExchange.Redis.Tests.Helpers; using Xunit; using Xunit.Abstractions; @@ -8,7 +8,7 @@ namespace StackExchange.Redis.Tests; public class AbortOnConnectFailTests : TestBase { - public AbortOnConnectFailTests(ITestOutputHelper output) : base (output) { } + public AbortOnConnectFailTests(ITestOutputHelper output) : base(output) { } [Fact] public void NeverEverConnectedNoBacklogThrowsConnectionNotAvailableSync() diff --git a/tests/StackExchange.Redis.Tests/AdhocTests.cs b/tests/StackExchange.Redis.Tests/AdhocTests.cs index 50ceb88c4..38ae5639a 100644 --- a/tests/StackExchange.Redis.Tests/AdhocTests.cs +++ b/tests/StackExchange.Redis.Tests/AdhocTests.cs @@ -6,7 +6,7 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] public class AdhocTests : TestBase { - public AdhocTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public AdhocTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public void TestAdhocCommandsAPI() @@ -20,7 +20,7 @@ public void TestAdhocCommandsAPI() RedisKey key = Me(); // note: if command renames are configured in - // the API, they will still work automatically + // the API, they will still work automatically db.Execute("del", key); db.Execute("set", key, "12"); db.Execute("incrby", key, 4); diff --git a/tests/StackExchange.Redis.Tests/AggressiveTests.cs b/tests/StackExchange.Redis.Tests/AggressiveTests.cs index 375e2da3d..73d06c4c7 100644 --- a/tests/StackExchange.Redis.Tests/AggressiveTests.cs +++ b/tests/StackExchange.Redis.Tests/AggressiveTests.cs @@ -81,11 +81,11 @@ public void RunCompetingBatchesOnSameMuxer() Thread x = new Thread(state => BatchRunPings((IDatabase)state!)) { - Name = nameof(BatchRunPings) + Name = nameof(BatchRunPings), }; Thread y = new Thread(state => BatchRunIntegers((IDatabase)state!)) { - Name = nameof(BatchRunIntegers) + Name = nameof(BatchRunIntegers), }; x.Start(db); @@ -102,7 +102,7 @@ private void BatchRunIntegers(IDatabase db) db.KeyDelete(key); db.StringSet(key, 1); Task[] tasks = new Task[InnerCount]; - for(int i = 0; i < IterationCount; i++) + for (int i = 0; i < IterationCount; i++) { var batch = db.CreateBatch(); for (int j = 0; j < tasks.Length; j++) @@ -161,7 +161,7 @@ private async Task BatchRunIntegersAsync(IDatabase db) tasks[j] = batch.StringIncrementAsync(key); } batch.Execute(); - for(int j = tasks.Length - 1; j >= 0;j--) + for (int j = tasks.Length - 1; j >= 0; j--) { await tasks[j]; } @@ -197,11 +197,11 @@ public void RunCompetingTransactionsOnSameMuxer() Thread x = new Thread(state => TranRunPings((IDatabase)state!)) { - Name = nameof(BatchRunPings) + Name = nameof(BatchRunPings), }; Thread y = new Thread(state => TranRunIntegers((IDatabase)state!)) { - Name = nameof(BatchRunIntegers) + Name = nameof(BatchRunIntegers), }; x.Start(db); diff --git a/tests/StackExchange.Redis.Tests/BacklogTests.cs b/tests/StackExchange.Redis.Tests/BacklogTests.cs index d1c853577..b81c86b8a 100644 --- a/tests/StackExchange.Redis.Tests/BacklogTests.cs +++ b/tests/StackExchange.Redis.Tests/BacklogTests.cs @@ -7,7 +7,7 @@ namespace StackExchange.Redis.Tests; public class BacklogTests : TestBase { - public BacklogTests(ITestOutputHelper output) : base (output) { } + public BacklogTests(ITestOutputHelper output) : base(output) { } protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; @@ -149,7 +149,6 @@ public async Task QueuesAndFlushesAfterReconnectingAsync() var lastPing = db.PingAsync(); // TODO: Add specific server call - var disconnectedStats = server.GetBridgeStatus(ConnectionType.Interactive); Assert.False(conn.IsConnected); Assert.True(disconnectedStats.BacklogMessagesPending >= 3, $"Expected {nameof(disconnectedStats.BacklogMessagesPending)} > 3, got {disconnectedStats.BacklogMessagesPending}"); @@ -238,10 +237,10 @@ public async Task QueuesAndFlushesAfterReconnecting() Log("Test: Disconnected pings"); Task[] pings = new Task[3]; - pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(1)); - pings[1] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(2)); - pings[2] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(3)); - void disconnectedPings(int id) + pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => DisconnectedPings(1)); + pings[1] = RunBlockingSynchronousWithExtraThreadAsync(() => DisconnectedPings(2)); + pings[2] = RunBlockingSynchronousWithExtraThreadAsync(() => DisconnectedPings(3)); + void DisconnectedPings(int id) { // No need to delay, we're going to try a disconnected connection immediately so it'll fail... Log($"Pinging (disconnected - {id})"); @@ -278,9 +277,9 @@ void disconnectedPings(int id) Assert.Equal(0, reconnectedStats.BacklogMessagesPending); Log("Test: Pinging again..."); - pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(4)); - pings[1] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(5)); - pings[2] = RunBlockingSynchronousWithExtraThreadAsync(() => disconnectedPings(6)); + pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => DisconnectedPings(4)); + pings[1] = RunBlockingSynchronousWithExtraThreadAsync(() => DisconnectedPings(5)); + pings[2] = RunBlockingSynchronousWithExtraThreadAsync(() => DisconnectedPings(6)); Log("Test: Last Ping queued"); // We should see none queued diff --git a/tests/StackExchange.Redis.Tests/BitTests.cs b/tests/StackExchange.Redis.Tests/BitTests.cs index 1a870f37e..c6b12c02b 100644 --- a/tests/StackExchange.Redis.Tests/BitTests.cs +++ b/tests/StackExchange.Redis.Tests/BitTests.cs @@ -7,7 +7,7 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] public class BitTests : TestBase { - public BitTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public BitTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public void BasicOps() diff --git a/tests/StackExchange.Redis.Tests/BoxUnboxTests.cs b/tests/StackExchange.Redis.Tests/BoxUnboxTests.cs index 123a33a88..1ffb972a2 100644 --- a/tests/StackExchange.Redis.Tests/BoxUnboxTests.cs +++ b/tests/StackExchange.Redis.Tests/BoxUnboxTests.cs @@ -52,7 +52,7 @@ private static void AssertEqualGiveOrTakeNaN(RedisValue expected, RedisValue act private static readonly byte[] s_abc = Encoding.UTF8.GetBytes("abc"); public static IEnumerable RoundTripValues - => new [] + => new[] { new object[] { RedisValue.Null }, new object[] { RedisValue.EmptyString }, @@ -94,7 +94,7 @@ public static IEnumerable RoundTripValues }; public static IEnumerable UnboxValues - => new [] + => new[] { new object?[] { null, RedisValue.Null }, new object[] { "", RedisValue.EmptyString }, @@ -125,7 +125,7 @@ public static IEnumerable UnboxValues new object[] { double.NegativeInfinity, (RedisValue)double.NegativeInfinity }, new object[] { double.NaN, (RedisValue)double.NaN }, new object[] { true, (RedisValue)true }, - new object[] { false, (RedisValue)false}, + new object[] { false, (RedisValue)false }, new object[] { "abc", (RedisValue)"abc" }, new object[] { s_abc, (RedisValue)s_abc }, new object[] { new Memory(s_abc), (RedisValue)s_abc }, @@ -135,7 +135,7 @@ public static IEnumerable UnboxValues public static IEnumerable InternedValues() { - for(int i = -20; i <= 40; i++) + for (int i = -20; i <= 40; i++) { bool expectInterned = i >= -1 & i <= 20; yield return new object[] { (RedisValue)i, expectInterned }; @@ -146,13 +146,13 @@ public static IEnumerable InternedValues() yield return new object[] { (RedisValue)float.NegativeInfinity, true }; yield return new object[] { (RedisValue)(-0.5F), false }; - yield return new object[] { (RedisValue)(0.5F), false }; + yield return new object[] { (RedisValue)0.5F, false }; yield return new object[] { (RedisValue)float.PositiveInfinity, true }; yield return new object[] { (RedisValue)float.NaN, true }; yield return new object[] { (RedisValue)double.NegativeInfinity, true }; yield return new object[] { (RedisValue)(-0.5D), false }; - yield return new object[] { (RedisValue)(0.5D), false }; + yield return new object[] { (RedisValue)0.5D, false }; yield return new object[] { (RedisValue)double.PositiveInfinity, true }; yield return new object[] { (RedisValue)double.NaN, true }; diff --git a/tests/StackExchange.Redis.Tests/ClientKillTests.cs b/tests/StackExchange.Redis.Tests/ClientKillTests.cs index 34f00c6bc..eaf91e073 100644 --- a/tests/StackExchange.Redis.Tests/ClientKillTests.cs +++ b/tests/StackExchange.Redis.Tests/ClientKillTests.cs @@ -52,14 +52,14 @@ public void TestClientKillMessageWithAllArguments() ClientType type = ClientType.Normal; string userName = "user1"; EndPoint endpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1234); - EndPoint serverEndpoint = new IPEndPoint(IPAddress.Parse("198.0.0.1"), 6379); ; + EndPoint serverEndpoint = new IPEndPoint(IPAddress.Parse("198.0.0.1"), 6379); bool skipMe = true; long maxAge = 102; var filter = new ClientKillFilter().WithId(id).WithClientType(type).WithUsername(userName).WithEndpoint(endpoint).WithServerEndpoint(serverEndpoint).WithSkipMe(skipMe).WithMaxAgeInSeconds(maxAge); List expected = new List() { - "KILL", "ID", "101", "TYPE", "normal", "USERNAME", "user1", "ADDR", "127.0.0.1:1234", "LADDR", "198.0.0.1:6379", "SKIPME", "yes", "MAXAGE", "102" + "KILL", "ID", "101", "TYPE", "normal", "USERNAME", "user1", "ADDR", "127.0.0.1:1234", "LADDR", "198.0.0.1:6379", "SKIPME", "yes", "MAXAGE", "102", }; Assert.Equal(expected, filter.ToList(true)); } diff --git a/tests/StackExchange.Redis.Tests/ClusterTests.cs b/tests/StackExchange.Redis.Tests/ClusterTests.cs index 535b4a91a..fcab7bb14 100644 --- a/tests/StackExchange.Redis.Tests/ClusterTests.cs +++ b/tests/StackExchange.Redis.Tests/ClusterTests.cs @@ -219,7 +219,8 @@ public void TransactionWithMultiServerKeys() do { y = Guid.NewGuid().ToString(); - } while (--abort > 0 && config.GetBySlot(y) == xNode); + } + while (--abort > 0 && config.GetBySlot(y) == xNode); if (abort == 0) Skip.Inconclusive("failed to find a different node to use"); var yNode = config.GetBySlot(y); Assert.NotNull(yNode); @@ -243,13 +244,13 @@ public void TransactionWithMultiServerKeys() // the rest no longer applies while we are following single-slot rules //// check that everything was aborted - //Assert.False(success, "tran aborted"); - //Assert.True(setX.IsCanceled, "set x cancelled"); - //Assert.True(setY.IsCanceled, "set y cancelled"); - //var existsX = cluster.KeyExistsAsync(x); - //var existsY = cluster.KeyExistsAsync(y); - //Assert.False(cluster.Wait(existsX), "x exists"); - //Assert.False(cluster.Wait(existsY), "y exists"); + // Assert.False(success, "tran aborted"); + // Assert.True(setX.IsCanceled, "set x cancelled"); + // Assert.True(setY.IsCanceled, "set y cancelled"); + // var existsX = cluster.KeyExistsAsync(x); + // var existsY = cluster.KeyExistsAsync(y); + // Assert.False(cluster.Wait(existsX), "x exists"); + // Assert.False(cluster.Wait(existsY), "y exists"); }); Assert.Equal("Multi-key operations must involve a single slot; keys can use 'hash tags' to help this, i.e. '{/users/12345}/account' and '{/users/12345}/contacts' will always be in the same slot", ex.Message); } @@ -274,7 +275,8 @@ public void TransactionWithSameServerKeys() do { y = Guid.NewGuid().ToString(); - } while (--abort > 0 && config.GetBySlot(y) != xNode); + } + while (--abort > 0 && config.GetBySlot(y) != xNode); if (abort == 0) Skip.Inconclusive("failed to find a key with the same node to use"); var yNode = config.GetBySlot(y); Assert.NotNull(xNode); @@ -299,13 +301,13 @@ public void TransactionWithSameServerKeys() // the rest no longer applies while we are following single-slot rules //// check that everything was aborted - //Assert.True(success, "tran aborted"); - //Assert.False(setX.IsCanceled, "set x cancelled"); - //Assert.False(setY.IsCanceled, "set y cancelled"); - //var existsX = cluster.KeyExistsAsync(x); - //var existsY = cluster.KeyExistsAsync(y); - //Assert.True(cluster.Wait(existsX), "x exists"); - //Assert.True(cluster.Wait(existsY), "y exists"); + // Assert.True(success, "tran aborted"); + // Assert.False(setX.IsCanceled, "set x cancelled"); + // Assert.False(setY.IsCanceled, "set y cancelled"); + // var existsX = cluster.KeyExistsAsync(x); + // var existsY = cluster.KeyExistsAsync(y); + // Assert.True(cluster.Wait(existsX), "x exists"); + // Assert.True(cluster.Wait(existsY), "y exists"); }); Assert.Equal("Multi-key operations must involve a single slot; keys can use 'hash tags' to help this, i.e. '{/users/12345}/account' and '{/users/12345}/contacts' will always be in the same slot", ex.Message); } @@ -358,7 +360,7 @@ public void TransactionWithSameSlotKeys() } [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "Because.")] - [Theory (Skip = "FlushAllDatabases")] + [Theory(Skip = "FlushAllDatabases")] [InlineData(null, 10)] [InlineData(null, 100)] [InlineData("abc", 10)] @@ -624,7 +626,6 @@ public void GroupedQueriesWork() // note it doesn't matter that the data doesn't exist for this; // the point here is that the entire thing *won't work* otherwise, // as per above test - var keys = InventKeys(); using var conn = Create(); @@ -650,7 +651,7 @@ public void GroupedQueriesWork() [Fact] public void MovedProfiling() { - var Key = Me(); + var key = Me(); const string Value = "redirected-value"; var profiler = new ProfilingTests.PerThreadProfiler(); @@ -663,24 +664,24 @@ public void MovedProfiling() var servers = endpoints.Select(e => conn.GetServer(e)); var db = conn.GetDatabase(); - db.KeyDelete(Key); - db.StringSet(Key, Value); + db.KeyDelete(key); + db.StringSet(key, Value); var config = servers.First().ClusterConfiguration; Assert.NotNull(config); - //int slot = conn.HashSlot(Key); - var rightPrimaryNode = config.GetBySlot(Key); + // int slot = conn.HashSlot(Key); + var rightPrimaryNode = config.GetBySlot(key); Assert.NotNull(rightPrimaryNode); Assert.NotNull(rightPrimaryNode.EndPoint); - string? a = (string?)conn.GetServer(rightPrimaryNode.EndPoint).Execute("GET", Key); + string? a = (string?)conn.GetServer(rightPrimaryNode.EndPoint).Execute("GET", key); Assert.Equal(Value, a); // right primary var wrongPrimaryNode = config.Nodes.FirstOrDefault(x => !x.IsReplica && x.NodeId != rightPrimaryNode.NodeId); Assert.NotNull(wrongPrimaryNode); Assert.NotNull(wrongPrimaryNode.EndPoint); - string? b = (string?)conn.GetServer(wrongPrimaryNode.EndPoint).Execute("GET", Key); + string? b = (string?)conn.GetServer(wrongPrimaryNode.EndPoint).Execute("GET", key); Assert.Equal(Value, b); // wrong primary, allow redirect var msgs = profiler.GetSession().FinishProfiling().ToList(); diff --git a/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs b/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs index 671345f9f..13125d722 100644 --- a/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs +++ b/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] public class CommandTimeoutTests : TestBase { - public CommandTimeoutTests(ITestOutputHelper output) : base (output) { } + public CommandTimeoutTests(ITestOutputHelper output) : base(output) { } [FactLongRunning] public async Task DefaultHeartbeatTimeout() diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index f93eb176f..2a4b2bc75 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.Logging.Abstractions; -using StackExchange.Redis.Configuration; -using System; +using System; using System.Globalization; using System.IO; using System.IO.Pipelines; @@ -13,6 +11,8 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using StackExchange.Redis.Configuration; using Xunit; using Xunit.Abstractions; @@ -24,38 +24,69 @@ public class ConfigTests : TestBase { public ConfigTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - public Version DefaultVersion = new (3, 0, 0); - public Version DefaultAzureVersion = new (4, 0, 0); + public Version DefaultVersion = new(3, 0, 0); + public Version DefaultAzureVersion = new(4, 0, 0); [Fact] public void ExpectedFields() { // if this test fails, check that you've updated ConfigurationOptions.Clone(), then: fix the test! // this is a simple but pragmatic "have you considered?" check - - var fields = Array.ConvertAll(typeof(ConfigurationOptions).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), + var fields = Array.ConvertAll( + typeof(ConfigurationOptions).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), x => Regex.Replace(x.Name, """^<(\w+)>k__BackingField$""", "$1")); Array.Sort(fields); - Assert.Equal(new[] { - "abortOnConnectFail", "allowAdmin", "asyncTimeout", "backlogPolicy", "BeforeSocketConnect", - "CertificateSelection", "CertificateValidation", "ChannelPrefix", - "checkCertificateRevocation", "ClientName", "commandMap", - "configChannel", "configCheckSeconds", "connectRetry", - "connectTimeout", "DefaultDatabase", "defaultOptions", - "defaultVersion", "EndPoints", "heartbeatConsistencyChecks", - "heartbeatInterval", "highIntegrity", "includeDetailInExceptions", "includePerformanceCountersInExceptions", - "keepAlive", "LibraryName", "loggerFactory", - "password", "Protocol", "proxy", - "reconnectRetryPolicy", "resolveDns", "responseTimeout", - "ServiceName", "setClientLibrary", "SocketManager", - "ssl", -#if !NETFRAMEWORK - "SslClientAuthenticationOptions", -#endif - "sslHost", "SslProtocols", - "syncTimeout", "tieBreaker", "Tunnel", - "user" - }, fields); + Assert.Equal( + new[] + { + "abortOnConnectFail", + "allowAdmin", + "asyncTimeout", + "backlogPolicy", + "BeforeSocketConnect", + "CertificateSelection", + "CertificateValidation", + "ChannelPrefix", + "checkCertificateRevocation", + "ClientName", + "commandMap", + "configChannel", + "configCheckSeconds", + "connectRetry", + "connectTimeout", + "DefaultDatabase", + "defaultOptions", + "defaultVersion", + "EndPoints", + "heartbeatConsistencyChecks", + "heartbeatInterval", + "highIntegrity", + "includeDetailInExceptions", + "includePerformanceCountersInExceptions", + "keepAlive", + "LibraryName", + "loggerFactory", + "password", + "Protocol", + "proxy", + "reconnectRetryPolicy", + "resolveDns", + "responseTimeout", + "ServiceName", + "setClientLibrary", + "SocketManager", + "ssl", + #if !NETFRAMEWORK + "SslClientAuthenticationOptions", + #endif + "sslHost", + "SslProtocols", + "syncTimeout", + "tieBreaker", + "Tunnel", + "user", + }, + fields); } [Fact] @@ -208,9 +239,9 @@ public void TalkToNonsenseServer() AbortOnConnectFail = false, EndPoints = { - { "127.0.0.1:1234" } + { "127.0.0.1:1234" }, }, - ConnectTimeout = 200 + ConnectTimeout = 200, }; var log = new StringWriter(); using (var conn = ConnectionMultiplexer.Connect(config, log)) @@ -544,7 +575,7 @@ public void ThreadPoolManagerIsDetected() var config = new ConfigurationOptions { EndPoints = { { IPAddress.Loopback, 6379 } }, - SocketManager = SocketManager.ThreadPool + SocketManager = SocketManager.ThreadPool, }; using var conn = ConnectionMultiplexer.Connect(config); @@ -779,6 +810,5 @@ public void CheckHighIntegrity(bool? assigned, bool expected, string cs) var parsed = ConfigurationOptions.Parse(cs); Assert.Equal(expected, options.HighIntegrity); - } } diff --git a/tests/StackExchange.Redis.Tests/ConnectByIPTests.cs b/tests/StackExchange.Redis.Tests/ConnectByIPTests.cs index b79f2e07d..24fc8d44d 100644 --- a/tests/StackExchange.Redis.Tests/ConnectByIPTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectByIPTests.cs @@ -9,7 +9,7 @@ namespace StackExchange.Redis.Tests; public class ConnectByIPTests : TestBase { - public ConnectByIPTests(ITestOutputHelper output) : base (output) { } + public ConnectByIPTests(ITestOutputHelper output) : base(output) { } [Fact] public void ParseEndpoints() @@ -18,7 +18,7 @@ public void ParseEndpoints() { { "127.0.0.1", 1000 }, { "::1", 1001 }, - { "localhost", 1002 } + { "localhost", 1002 }, }; Assert.Equal(AddressFamily.InterNetwork, eps[0].AddressFamily); @@ -35,7 +35,7 @@ public void IPv4Connection() { var config = new ConfigurationOptions { - EndPoints = { { TestConfig.Current.IPv4Server, TestConfig.Current.IPv4Port } } + EndPoints = { { TestConfig.Current.IPv4Server, TestConfig.Current.IPv4Port } }, }; using var conn = ConnectionMultiplexer.Connect(config); @@ -49,7 +49,7 @@ public void IPv6Connection() { var config = new ConfigurationOptions { - EndPoints = { { TestConfig.Current.IPv6Server, TestConfig.Current.IPv6Port } } + EndPoints = { { TestConfig.Current.IPv6Server, TestConfig.Current.IPv6Port } }, }; using var conn = ConnectionMultiplexer.Connect(config); @@ -65,7 +65,7 @@ public void ConnectByVariousEndpoints(EndPoint ep, AddressFamily expectedFamily) Assert.Equal(expectedFamily, ep.AddressFamily); var config = new ConfigurationOptions { - EndPoints = { ep } + EndPoints = { ep }, }; if (ep.AddressFamily != AddressFamily.InterNetworkV6) // I don't have IPv6 servers { diff --git a/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs b/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs index f0f189372..460f6f6b6 100644 --- a/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs @@ -7,7 +7,7 @@ namespace StackExchange.Redis.Tests; public class ConnectCustomConfigTests : TestBase { - public ConnectCustomConfigTests(ITestOutputHelper output) : base (output) { } + public ConnectCustomConfigTests(ITestOutputHelper output) : base(output) { } // So we're triggering tiebreakers here protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; diff --git a/tests/StackExchange.Redis.Tests/ConnectFailTimeoutTests.cs b/tests/StackExchange.Redis.Tests/ConnectFailTimeoutTests.cs index 2b4b5a29f..58d9795bb 100644 --- a/tests/StackExchange.Redis.Tests/ConnectFailTimeoutTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectFailTimeoutTests.cs @@ -7,7 +7,7 @@ namespace StackExchange.Redis.Tests; public class ConnectFailTimeoutTests : TestBase { - public ConnectFailTimeoutTests(ITestOutputHelper output) : base (output) { } + public ConnectFailTimeoutTests(ITestOutputHelper output) : base(output) { } [Fact] public async Task NoticesConnectFail() @@ -17,8 +17,9 @@ public async Task NoticesConnectFail() var server = conn.GetServer(conn.GetEndPoints()[0]); - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() + await RunBlockingSynchronousWithExtraThreadAsync(InnerScenario).ForAwait(); + + void InnerScenario() { conn.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); diff --git a/tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs b/tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs index a8bfe69b0..d8a942a28 100644 --- a/tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs @@ -9,7 +9,7 @@ namespace StackExchange.Redis.Tests; public class ConnectToUnexistingHostTests : TestBase { - public ConnectToUnexistingHostTests(ITestOutputHelper output) : base (output) { } + public ConnectToUnexistingHostTests(ITestOutputHelper output) : base(output) { } [Fact] public async Task FailsWithinTimeout() @@ -21,7 +21,7 @@ public async Task FailsWithinTimeout() var config = new ConfigurationOptions { EndPoints = { { "invalid", 1234 } }, - ConnectTimeout = timeout + ConnectTimeout = timeout, }; using (ConnectionMultiplexer.Connect(config, Writer)) @@ -43,8 +43,8 @@ public async Task FailsWithinTimeout() [Fact] public async Task CanNotOpenNonsenseConnection_IP() { - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() + await RunBlockingSynchronousWithExtraThreadAsync(InnerScenario).ForAwait(); + void InnerScenario() { var ex = Assert.Throws(() => { @@ -67,8 +67,8 @@ public async Task CanNotOpenNonsenseConnection_DNS() [Fact] public async Task CreateDisconnectedNonsenseConnection_IP() { - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() + await RunBlockingSynchronousWithExtraThreadAsync(InnerScenario).ForAwait(); + void InnerScenario() { using (var conn = ConnectionMultiplexer.Connect(TestConfig.Current.PrimaryServer + ":6500,abortConnect=false,connectTimeout=1000,connectRetry=0", Writer)) { @@ -81,8 +81,8 @@ void innerScenario() [Fact] public async Task CreateDisconnectedNonsenseConnection_DNS() { - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() + await RunBlockingSynchronousWithExtraThreadAsync(InnerScenario).ForAwait(); + void InnerScenario() { using (var conn = ConnectionMultiplexer.Connect($"doesnot.exist.ds.{Guid.NewGuid():N}.com:6500,abortConnect=false,connectTimeout=1000,connectRetry=0", Writer)) { diff --git a/tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs b/tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs index 7ed717eab..db8d1dd1a 100644 --- a/tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis.Tests; public class ConnectingFailDetectionTests : TestBase { - public ConnectingFailDetectionTests(ITestOutputHelper output) : base (output) { } + public ConnectingFailDetectionTests(ITestOutputHelper output) : base(output) { } protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; diff --git a/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs b/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs index 68bc8dc6e..df3802118 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs @@ -10,7 +10,7 @@ namespace StackExchange.Redis.Tests; public class ConnectionFailedErrorsTests : TestBase { - public ConnectionFailedErrorsTests(ITestOutputHelper output) : base (output) { } + public ConnectionFailedErrorsTests(ITestOutputHelper output) : base(output) { } [Theory] [InlineData(true)] @@ -29,14 +29,15 @@ public async Task SSLCertificateValidationError(bool isCertValidationSucceeded) using var conn = ConnectionMultiplexer.Connect(options); - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() + await RunBlockingSynchronousWithExtraThreadAsync(InnerScenario).ForAwait(); + + void InnerScenario() { conn.ConnectionFailed += (sender, e) => Assert.Equal(ConnectionFailureType.AuthenticationFailure, e.FailureType); if (!isCertValidationSucceeded) { - //validate that in this case it throws an certificatevalidation exception + // Validate that in this case it throws an certificatevalidation exception var outer = Assert.Throws(() => conn.GetDatabase().Ping()); Assert.Equal(ConnectionFailureType.UnableToResolvePhysicalConnection, outer.FailureType); @@ -72,13 +73,13 @@ public async Task AuthenticationFailureError() using var conn = ConnectionMultiplexer.Connect(options); - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() + await RunBlockingSynchronousWithExtraThreadAsync(InnerScenario).ForAwait(); + void InnerScenario() { conn.ConnectionFailed += (sender, e) => { if (e.FailureType == ConnectionFailureType.SocketFailure) Skip.Inconclusive("socket fail"); // this is OK too - Assert.Equal(ConnectionFailureType.AuthenticationFailure, e.FailureType); + Assert.Equal(ConnectionFailureType.AuthenticationFailure, e.FailureType); }; var ex = Assert.Throws(() => conn.GetDatabase().Ping()); @@ -90,15 +91,15 @@ void innerScenario() Assert.Equal("Error: NOAUTH Authentication required. Verify if the Redis password provided is correct.", rde.InnerException.Message); } - //wait for a second for connectionfailed event to fire + // Wait for a second for connectionfailed event to fire await Task.Delay(1000).ForAwait(); } [Fact] public async Task SocketFailureError() { - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() + await RunBlockingSynchronousWithExtraThreadAsync(InnerScenario).ForAwait(); + void InnerScenario() { var options = new ConfigurationOptions(); options.EndPoints.Add($"{Guid.NewGuid():N}.redis.cache.windows.net"); @@ -144,8 +145,8 @@ void innerScenario() [Fact] public async Task AbortOnConnectFailFalseConnectTimeoutError() { - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() + await RunBlockingSynchronousWithExtraThreadAsync(InnerScenario).ForAwait(); + void InnerScenario() { Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); diff --git a/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs b/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs index 2ce71f2f7..07e90dabc 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs @@ -6,7 +6,7 @@ namespace StackExchange.Redis.Tests; public class TransientErrorTests : TestBase { - public TransientErrorTests(ITestOutputHelper output) : base (output) { } + public TransientErrorTests(ITestOutputHelper output) : base(output) { } [Fact] public void TestExponentialRetry() diff --git a/tests/StackExchange.Redis.Tests/CopyTests.cs b/tests/StackExchange.Redis.Tests/CopyTests.cs index 20a43d1df..b40f00c02 100644 --- a/tests/StackExchange.Redis.Tests/CopyTests.cs +++ b/tests/StackExchange.Redis.Tests/CopyTests.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] public class CopyTests : TestBase { - public CopyTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public CopyTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public async Task Basic() diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 1922c3edf..2edee05c6 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -6,7 +6,7 @@ namespace StackExchange.Redis.Tests; public class ExceptionFactoryTests : TestBase { - public ExceptionFactoryTests(ITestOutputHelper output) : base (output) { } + public ExceptionFactoryTests(ITestOutputHelper output) : base(output) { } [Fact] public void NullLastException() @@ -163,7 +163,7 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple BacklogPolicy = BacklogPolicy.FailFast, ConnectTimeout = 1000, SyncTimeout = 500, - KeepAlive = 5000 + KeepAlive = 5000, }; ConnectionMultiplexer conn; diff --git a/tests/StackExchange.Redis.Tests/ExpiryTests.cs b/tests/StackExchange.Redis.Tests/ExpiryTests.cs index d69ab53d5..3d11442e6 100644 --- a/tests/StackExchange.Redis.Tests/ExpiryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExpiryTests.cs @@ -8,9 +8,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] public class ExpiryTests : TestBase { - public ExpiryTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public ExpiryTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - private static string[]? GetMap(bool disablePTimes) => disablePTimes ? (new[] { "pexpire", "pexpireat", "pttl" }) : null; + private static string[]? GetMap(bool disablePTimes) => disablePTimes ? new[] { "pexpire", "pexpireat", "pttl" } : null; [Theory] [InlineData(true)] diff --git a/tests/StackExchange.Redis.Tests/FSharpCompatTests.cs b/tests/StackExchange.Redis.Tests/FSharpCompatTests.cs index 7539c1198..7b39d36b9 100644 --- a/tests/StackExchange.Redis.Tests/FSharpCompatTests.cs +++ b/tests/StackExchange.Redis.Tests/FSharpCompatTests.cs @@ -5,8 +5,9 @@ namespace StackExchange.Redis.Tests; public class FSharpCompatTests : TestBase { - public FSharpCompatTests(ITestOutputHelper output) : base (output) { } + public FSharpCompatTests(ITestOutputHelper output) : base(output) { } +#pragma warning disable SA1129 // Do not use default value type constructor [Fact] public void RedisKeyConstructor() { @@ -22,4 +23,5 @@ public void RedisValueConstructor() Assert.Equal((RedisValue)"MyKey", new RedisValue("MyKey")); Assert.Equal((RedisValue)"MyKey2", new RedisValue("MyKey2", 0)); } +#pragma warning restore SA1129 // Do not use default value type constructor } diff --git a/tests/StackExchange.Redis.Tests/FailoverTests.cs b/tests/StackExchange.Redis.Tests/FailoverTests.cs index 2449e6a4b..e40d12a14 100644 --- a/tests/StackExchange.Redis.Tests/FailoverTests.cs +++ b/tests/StackExchange.Redis.Tests/FailoverTests.cs @@ -47,7 +47,7 @@ private static ConfigurationOptions GetPrimaryReplicaConfig() { { TestConfig.Current.FailoverPrimaryServer, TestConfig.Current.FailoverPrimaryPort }, { TestConfig.Current.FailoverReplicaServer, TestConfig.Current.FailoverReplicaPort }, - } + }, }; } @@ -136,8 +136,8 @@ public async Task DereplicateGoesToPrimary() bool isUnanimous = log.Contains("tie-break is unanimous at " + TestConfig.Current.FailoverPrimaryServerAndPort); if (!isUnanimous) Skip.Inconclusive("this is timing sensitive; unable to verify this time"); } - // k, so we know everyone loves 6379; is that what we get? + // k, so we know everyone loves 6379; is that what we get? var db = conn.GetDatabase(); RedisKey key = Me(); diff --git a/tests/StackExchange.Redis.Tests/FloatingPointTests.cs b/tests/StackExchange.Redis.Tests/FloatingPointTests.cs index 6a7158fe3..71bab0386 100644 --- a/tests/StackExchange.Redis.Tests/FloatingPointTests.cs +++ b/tests/StackExchange.Redis.Tests/FloatingPointTests.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] public class FloatingPointTests : TestBase { - public FloatingPointTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public FloatingPointTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } private static bool Within(double x, double y, double delta) => Math.Abs(x - y) <= delta; @@ -22,16 +22,17 @@ public void IncrDecrFloatingPoint() db.KeyDelete(key, CommandFlags.FireAndForget); double[] incr = { - 12.134, - -14561.0000002, - 125.3421, - -2.49892498 - }, decr = - { - 99.312, - 12, - -35 - }; + 12.134, + -14561.0000002, + 125.3421, + -2.49892498, + }, + decr = + { + 99.312, + 12, + -35, + }; double sum = 0; foreach (var value in incr) { @@ -58,16 +59,17 @@ public async Task IncrDecrFloatingPointAsync() db.KeyDelete(key, CommandFlags.FireAndForget); double[] incr = { - 12.134, - -14561.0000002, - 125.3421, - -2.49892498 - }, decr = - { - 99.312, - 12, - -35 - }; + 12.134, + -14561.0000002, + 125.3421, + -2.49892498, + }, + decr = + { + 99.312, + 12, + -35, + }; double sum = 0; foreach (var value in incr) { @@ -95,16 +97,17 @@ public void HashIncrDecrFloatingPoint() db.KeyDelete(key, CommandFlags.FireAndForget); double[] incr = { - 12.134, - -14561.0000002, - 125.3421, - -2.49892498 - }, decr = - { - 99.312, - 12, - -35 - }; + 12.134, + -14561.0000002, + 125.3421, + -2.49892498, + }, + decr = + { + 99.312, + 12, + -35, + }; double sum = 0; foreach (var value in incr) { @@ -132,16 +135,17 @@ public async Task HashIncrDecrFloatingPointAsync() db.KeyDelete(key, CommandFlags.FireAndForget); double[] incr = { - 12.134, - -14561.0000002, - 125.3421, - -2.49892498 - }, decr = - { - 99.312, - 12, - -35 - }; + 12.134, + -14561.0000002, + 125.3421, + -2.49892498, + }, + decr = + { + 99.312, + 12, + -35, + }; double sum = 0; foreach (var value in incr) { diff --git a/tests/StackExchange.Redis.Tests/FormatTests.cs b/tests/StackExchange.Redis.Tests/FormatTests.cs index bb2f18740..8b3c152ed 100644 --- a/tests/StackExchange.Redis.Tests/FormatTests.cs +++ b/tests/StackExchange.Redis.Tests/FormatTests.cs @@ -81,7 +81,6 @@ public void ClientFlagsFormatting(ClientFlags value, string expected) public void ReplicationChangeOptionsFormatting(ReplicationChangeOptions value, string expected) => Assert.Equal(expected, value.ToString()); - [Theory] [InlineData(0, "0")] [InlineData(1, "1")] diff --git a/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs b/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs index 42a132835..8a263c553 100644 --- a/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs +++ b/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs @@ -35,9 +35,9 @@ public async Task MuxerIsCollected() ForceGC(); -//#if DEBUG // this counter only exists in debug +// #if DEBUG // this counter only exists in debug // int before = ConnectionMultiplexer.CollectedWithoutDispose; -//#endif +// #endif var wr = new WeakReference(conn); conn = null; @@ -48,10 +48,10 @@ public async Task MuxerIsCollected() // should be collectable Assert.Null(wr.Target); -//#if DEBUG // this counter only exists in debug +// #if DEBUG // this counter only exists in debug // int after = ConnectionMultiplexer.CollectedWithoutDispose; // Assert.Equal(before + 1, after); -//#endif +// #endif } [Fact] @@ -64,15 +64,17 @@ public async Task UnrootedBackloggedAsyncTaskIsCompletedOnTimeout() Task? completedTestTask = null; _ = Task.Run(async () => { - using var conn = await ConnectionMultiplexer.ConnectAsync(new ConfigurationOptions() - { - BacklogPolicy = BacklogPolicy.Default, - AbortOnConnectFail = false, - ConnectTimeout = 50, - SyncTimeout = 1000, - AllowAdmin = true, - EndPoints = { GetConfiguration() }, - }, Writer); + using var conn = await ConnectionMultiplexer.ConnectAsync( + new ConfigurationOptions() + { + BacklogPolicy = BacklogPolicy.Default, + AbortOnConnectFail = false, + ConnectTimeout = 50, + SyncTimeout = 1000, + AllowAdmin = true, + EndPoints = { GetConfiguration() }, + }, + Writer); var db = conn.GetDatabase(); // Disconnect and don't allow re-connection diff --git a/tests/StackExchange.Redis.Tests/GeoTests.cs b/tests/StackExchange.Redis.Tests/GeoTests.cs index f1be0bad1..84fa30386 100644 --- a/tests/StackExchange.Redis.Tests/GeoTests.cs +++ b/tests/StackExchange.Redis.Tests/GeoTests.cs @@ -1,7 +1,7 @@ -using Xunit; -using System; -using Xunit.Abstractions; +using System; using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; namespace StackExchange.Redis.Tests; @@ -9,14 +9,15 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] public class GeoTests : TestBase { - public GeoTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public GeoTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } private static readonly GeoEntry - palermo = new GeoEntry(13.361389, 38.115556, "Palermo"), - catania = new GeoEntry(15.087269, 37.502669, "Catania"), - agrigento = new GeoEntry(13.5765, 37.311, "Agrigento"), - cefalù = new GeoEntry(14.0188, 38.0084, "Cefalù"); - private static readonly GeoEntry[] all = { palermo, catania, agrigento, cefalù }; + Palermo = new GeoEntry(13.361389, 38.115556, "Palermo"), + Catania = new GeoEntry(15.087269, 37.502669, "Catania"), + Agrigento = new GeoEntry(13.5765, 37.311, "Agrigento"), + Cefalù = new GeoEntry(14.0188, 38.0084, "Cefalù"); + + private static readonly GeoEntry[] All = { Palermo, Catania, Agrigento, Cefalù }; [Fact] public void GeoAdd() @@ -28,20 +29,20 @@ public void GeoAdd() db.KeyDelete(key, CommandFlags.FireAndForget); // add while not there - Assert.True(db.GeoAdd(key, cefalù.Longitude, cefalù.Latitude, cefalù.Member)); - Assert.Equal(2, db.GeoAdd(key, new[] { palermo, catania })); - Assert.True(db.GeoAdd(key, agrigento)); + Assert.True(db.GeoAdd(key, Cefalù.Longitude, Cefalù.Latitude, Cefalù.Member)); + Assert.Equal(2, db.GeoAdd(key, new[] { Palermo, Catania })); + Assert.True(db.GeoAdd(key, Agrigento)); // now add again - Assert.False(db.GeoAdd(key, cefalù.Longitude, cefalù.Latitude, cefalù.Member)); - Assert.Equal(0, db.GeoAdd(key, new[] { palermo, catania })); - Assert.False(db.GeoAdd(key, agrigento)); + Assert.False(db.GeoAdd(key, Cefalù.Longitude, Cefalù.Latitude, Cefalù.Member)); + Assert.Equal(0, db.GeoAdd(key, new[] { Palermo, Catania })); + Assert.False(db.GeoAdd(key, Agrigento)); // Validate - var pos = db.GeoPosition(key, palermo.Member); + var pos = db.GeoPosition(key, Palermo.Member); Assert.NotNull(pos); - Assert.Equal(palermo.Longitude, pos!.Value.Longitude, 5); - Assert.Equal(palermo.Latitude, pos!.Value.Latitude, 5); + Assert.Equal(Palermo.Longitude, pos!.Value.Longitude, 5); + Assert.Equal(Palermo.Latitude, pos!.Value.Latitude, 5); } [Fact] @@ -52,7 +53,7 @@ public void GetDistance() var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - db.GeoAdd(key, all, CommandFlags.FireAndForget); + db.GeoAdd(key, All, CommandFlags.FireAndForget); var val = db.GeoDistance(key, "Palermo", "Catania", GeoUnit.Meters); Assert.True(val.HasValue); Assert.Equal(166274.1516, val); @@ -69,9 +70,9 @@ public void GeoHash() var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - db.GeoAdd(key, all, CommandFlags.FireAndForget); + db.GeoAdd(key, All, CommandFlags.FireAndForget); - var hashes = db.GeoHash(key, new RedisValue[] { palermo.Member, "Nowhere", agrigento.Member }); + var hashes = db.GeoHash(key, new RedisValue[] { Palermo.Member, "Nowhere", Agrigento.Member }); Assert.NotNull(hashes); Assert.Equal(3, hashes.Length); Assert.Equal("sqc8b49rny0", hashes[0]); @@ -93,12 +94,12 @@ public void GeoGetPosition() var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - db.GeoAdd(key, all, CommandFlags.FireAndForget); + db.GeoAdd(key, All, CommandFlags.FireAndForget); - var pos = db.GeoPosition(key, palermo.Member); + var pos = db.GeoPosition(key, Palermo.Member); Assert.True(pos.HasValue); - Assert.Equal(Math.Round(palermo.Longitude, 6), Math.Round(pos.Value.Longitude, 6)); - Assert.Equal(Math.Round(palermo.Latitude, 6), Math.Round(pos.Value.Latitude, 6)); + Assert.Equal(Math.Round(Palermo.Longitude, 6), Math.Round(pos.Value.Longitude, 6)); + Assert.Equal(Math.Round(Palermo.Latitude, 6), Math.Round(pos.Value.Latitude, 6)); pos = db.GeoPosition(key, "Nowhere"); Assert.False(pos.HasValue); @@ -112,7 +113,7 @@ public void GeoRemove() var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - db.GeoAdd(key, all, CommandFlags.FireAndForget); + db.GeoAdd(key, All, CommandFlags.FireAndForget); var pos = db.GeoPosition(key, "Palermo"); Assert.True(pos.HasValue); @@ -133,37 +134,37 @@ public void GeoRadius() var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - db.GeoAdd(key, all, CommandFlags.FireAndForget); + db.GeoAdd(key, All, CommandFlags.FireAndForget); - var results = db.GeoRadius(key, cefalù.Member, 60, GeoUnit.Miles, 2, Order.Ascending); + var results = db.GeoRadius(key, Cefalù.Member, 60, GeoUnit.Miles, 2, Order.Ascending); Assert.Equal(2, results.Length); - Assert.Equal(results[0].Member, cefalù.Member); + Assert.Equal(results[0].Member, Cefalù.Member); Assert.Equal(0, results[0].Distance); var position0 = results[0].Position; Assert.NotNull(position0); - Assert.Equal(Math.Round(position0!.Value.Longitude, 5), Math.Round(cefalù.Position.Longitude, 5)); - Assert.Equal(Math.Round(position0!.Value.Latitude, 5), Math.Round(cefalù.Position.Latitude, 5)); + Assert.Equal(Math.Round(position0!.Value.Longitude, 5), Math.Round(Cefalù.Position.Longitude, 5)); + Assert.Equal(Math.Round(position0!.Value.Latitude, 5), Math.Round(Cefalù.Position.Latitude, 5)); Assert.False(results[0].Hash.HasValue); - Assert.Equal(results[1].Member, palermo.Member); + Assert.Equal(results[1].Member, Palermo.Member); var distance1 = results[1].Distance; Assert.NotNull(distance1); Assert.Equal(Math.Round(36.5319, 6), Math.Round(distance1!.Value, 6)); var position1 = results[1].Position; Assert.NotNull(position1); - Assert.Equal(Math.Round(position1!.Value.Longitude, 5), Math.Round(palermo.Position.Longitude, 5)); - Assert.Equal(Math.Round(position1!.Value.Latitude, 5), Math.Round(palermo.Position.Latitude, 5)); + Assert.Equal(Math.Round(position1!.Value.Longitude, 5), Math.Round(Palermo.Position.Longitude, 5)); + Assert.Equal(Math.Round(position1!.Value.Latitude, 5), Math.Round(Palermo.Position.Latitude, 5)); Assert.False(results[1].Hash.HasValue); - results = db.GeoRadius(key, cefalù.Member, 60, GeoUnit.Miles, 2, Order.Ascending, GeoRadiusOptions.None); + results = db.GeoRadius(key, Cefalù.Member, 60, GeoUnit.Miles, 2, Order.Ascending, GeoRadiusOptions.None); Assert.Equal(2, results.Length); - Assert.Equal(results[0].Member, cefalù.Member); + Assert.Equal(results[0].Member, Cefalù.Member); Assert.False(results[0].Position.HasValue); Assert.False(results[0].Distance.HasValue); Assert.False(results[0].Hash.HasValue); - Assert.Equal(results[1].Member, palermo.Member); + Assert.Equal(results[1].Member, Palermo.Member); Assert.False(results[1].Position.HasValue); Assert.False(results[1].Distance.HasValue); Assert.False(results[1].Hash.HasValue); @@ -622,7 +623,6 @@ public void GeoSearchBadArgs() var exception = Assert.Throws(() => db.GeoSearch(key, "irrelevant", circle, demandClosest: false)); - Assert.Contains("demandClosest must be true if you are not limiting the count for a GEOSEARCH", - exception.Message); + Assert.Contains("demandClosest must be true if you are not limiting the count for a GEOSEARCH", exception.Message); } } diff --git a/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs b/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs index 503124c7d..05be64b21 100644 --- a/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs +++ b/tests/StackExchange.Redis.Tests/GlobalSuppressions.cs @@ -1,22 +1,21 @@ - -// This file is used by Code Analysis to maintain SuppressMessage +// This file is used by Code Analysis to maintain SuppressMessage // attributes that are applied to this project. -// Project-level suppressions either have no target or are given +// Project-level suppressions either have no target or are given // a specific target and scoped to a namespace, type, member, etc. using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionFailedErrorsTests.SSLCertificateValidationError(System.Boolean)")] -[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.PubSubTests.ExplicitPublishMode")] -[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSLTests.ConnectToSSLServer(System.Boolean,System.Boolean)")] -[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSLTests.ShowCertFailures(StackExchange.Redis.Tests.Helpers.TextWriterOutputHelper)~System.Net.Security.RemoteCertificateValidationCallback")] -[assembly: SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionShutdownTests.ShutdownRaisesConnectionFailedAndRestore")] -[assembly: SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Issues.BgSaveResponseTests.ShouldntThrowException(StackExchange.Redis.SaveType)")] -[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelTests.PrimaryConnectTest~System.Threading.Tasks.Task")] -[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelTests.PrimaryConnectAsyncTest~System.Threading.Tasks.Task")] -[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelBase.WaitForReplicationAsync(StackExchange.Redis.IServer,System.Nullable{System.TimeSpan})~System.Threading.Tasks.Task")] -[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelFailoverTests.ManagedPrimaryConnectionEndToEndWithFailoverTest~System.Threading.Tasks.Task")] -[assembly: SuppressMessage("Performance", "CA1846:Prefer 'AsSpan' over 'Substring'", Justification = "", Scope = "member", Target = "~M:RedisSharp.Redis.ReadData~System.Byte[]")] -[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.NamingTests.IgnoreMethodConventions(System.Reflection.MethodInfo)~System.Boolean")] -[assembly: SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelBase.WaitForReadyAsync(System.Net.EndPoint,System.Boolean,System.Nullable{System.TimeSpan})~System.Threading.Tasks.Task")] -[assembly: SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelBase.WaitForRoleAsync(StackExchange.Redis.IServer,System.String,System.Nullable{System.TimeSpan})~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionFailedErrorsTests.SSLCertificateValidationError(System.Boolean)")] +[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Tests.PubSubTests.ExplicitPublishMode")] +[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSLTests.ConnectToSSLServer(System.Boolean,System.Boolean)")] +[assembly: SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SSLTests.ShowCertFailures(StackExchange.Redis.Tests.Helpers.TextWriterOutputHelper)~System.Net.Security.RemoteCertificateValidationCallback")] +[assembly: SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Tests.ConnectionShutdownTests.ShutdownRaisesConnectionFailedAndRestore")] +[assembly: SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Tests.Issues.BgSaveResponseTests.ShouldntThrowException(StackExchange.Redis.SaveType)")] +[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelTests.PrimaryConnectTest~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelTests.PrimaryConnectAsyncTest~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelBase.WaitForReplicationAsync(StackExchange.Redis.IServer,System.Nullable{System.TimeSpan})~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Roslynator", "RCS1077:Optimize LINQ method call.", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelFailoverTests.ManagedPrimaryConnectionEndToEndWithFailoverTest~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Performance", "CA1846:Prefer 'AsSpan' over 'Substring'", Justification = "Pending", Scope = "member", Target = "~M:RedisSharp.Redis.ReadData~System.Byte[]")] +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Tests.NamingTests.IgnoreMethodConventions(System.Reflection.MethodInfo)~System.Boolean")] +[assembly: SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelBase.WaitForReadyAsync(System.Net.EndPoint,System.Boolean,System.Nullable{System.TimeSpan})~System.Threading.Tasks.Task")] +[assembly: SuppressMessage("Roslynator", "RCS1075:Avoid empty catch clause that catches System.Exception.", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.Tests.SentinelBase.WaitForRoleAsync(StackExchange.Redis.IServer,System.String,System.Nullable{System.TimeSpan})~System.Threading.Tasks.Task")] diff --git a/tests/StackExchange.Redis.Tests/HashTests.cs b/tests/StackExchange.Redis.Tests/HashTests.cs index 34a2d12c1..31f3fa9f6 100644 --- a/tests/StackExchange.Redis.Tests/HashTests.cs +++ b/tests/StackExchange.Redis.Tests/HashTests.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using System.Text; using System.Linq; +using System.Text; +using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; -using System.Threading.Tasks; namespace StackExchange.Redis.Tests; @@ -576,10 +576,11 @@ public void TestSetPairs() var result0 = db.HashGetAllAsync(hashkey); - var data = new[] { - new HashEntry("foo", Encoding.UTF8.GetBytes("abc")), - new HashEntry("bar", Encoding.UTF8.GetBytes("def")) - }; + var data = new[] + { + new HashEntry("foo", Encoding.UTF8.GetBytes("abc")), + new HashEntry("bar", Encoding.UTF8.GetBytes("def")), + }; db.HashSetAsync(hashkey, data).ForAwait(); var result1 = db.Wait(db.HashGetAllAsync(hashkey)); diff --git a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs index 6b72b659e..1fab56cbb 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs @@ -101,7 +101,7 @@ public class SkippableTestCase : XunitTestCase, IRedisTest { RedisProtocol.Resp2 => "RESP2", RedisProtocol.Resp3 => "RESP3", - _ => "UnknownProtocolFixMeeeeee" + _ => "UnknownProtocolFixMeeeeee", }; protected override string GetUniqueID() => base.GetUniqueID() + ProtocolString; diff --git a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs index 1d7396033..ae48ff676 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs @@ -29,8 +29,7 @@ public SharedConnectionFixture() output: null, clientName: nameof(SharedConnectionFixture), configuration: Configuration, - allowAdmin: true - ); + allowAdmin: true); _actualConnection.InternalError += OnInternalError; _actualConnection.ConnectionFailed += OnConnectionFailed; } @@ -43,7 +42,8 @@ internal IInternalConnectionMultiplexer GetConnection(TestBase obj, RedisProtoco { ref NonDisposingConnection? field = ref protocol == RedisProtocol.Resp3 ? ref resp3 : ref resp2; if (field is { IsConnected: false }) - { // abandon memoized connection if disconnected + { + // abandon memoized connection if disconnected var muxer = field.UnderlyingMultiplexer; field = null; muxer.Dispose(); @@ -271,7 +271,7 @@ public void Teardown(TextWriter output) } privateExceptions.Clear(); } - //Assert.True(false, $"There were {privateFailCount} private ambient exceptions."); + // Assert.True(false, $"There were {privateFailCount} private ambient exceptions."); } if (_actualConnection != null) diff --git a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs index 6c86e10ab..fafb30543 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs @@ -1,9 +1,9 @@ -using System.IO; -using System; -using Newtonsoft.Json; -using System.Threading; +using System; +using System.IO; using System.Linq; using System.Net.Sockets; +using System.Threading; +using Newtonsoft.Json; namespace StackExchange.Redis.Tests; diff --git a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs index 50fd6bbc8..fd614fa93 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs @@ -30,7 +30,10 @@ public class Redis : IDisposable public enum KeyType { - None, String, List, Set + None, + String, + List, + Set, } public class ResponseException : Exception @@ -217,7 +220,7 @@ private void Connect() socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true, - SendTimeout = SendTimeout + SendTimeout = SendTimeout, }; socket.Connect(Host, Port); if (!socket.Connected) @@ -232,7 +235,7 @@ private void Connect() SendExpectSuccess("AUTH {0}\r\n", Password); } - private readonly byte[] end_data = new byte[] { (byte)'\r', (byte)'\n' }; + private readonly byte[] endData = new byte[] { (byte)'\r', (byte)'\n' }; private bool SendDataCommand(byte[] data, string cmd, params object[] args) { @@ -250,7 +253,7 @@ private bool SendDataCommand(byte[] data, string cmd, params object[] args) if (data != null) { socket.Send(data); - socket.Send(end_data); + socket.Send(endData); } } catch (SocketException) @@ -380,9 +383,7 @@ private string SendExpectString(string cmd, params object[] args) throw new ResponseException("Unknown reply on integer request: " + c + s); } - // // This one does not throw errors - // private string SendGetString(string cmd, params object[] args) { if (!SendCommand(cmd, args)) @@ -435,7 +436,7 @@ private byte[] ReadData() throw new ResponseException("Invalid length"); } - //returns the number of matches + // returns the number of matches if (c == '*') { if (int.TryParse(r.Substring(1), out int n)) diff --git a/tests/StackExchange.Redis.Tests/InfoReplicationCheckTests.cs b/tests/StackExchange.Redis.Tests/InfoReplicationCheckTests.cs index b0311a7f1..547d3cc88 100644 --- a/tests/StackExchange.Redis.Tests/InfoReplicationCheckTests.cs +++ b/tests/StackExchange.Redis.Tests/InfoReplicationCheckTests.cs @@ -7,7 +7,7 @@ namespace StackExchange.Redis.Tests; public class InfoReplicationCheckTests : TestBase { protected override string GetConfiguration() => base.GetConfiguration() + ",configCheckSeconds=2"; - public InfoReplicationCheckTests(ITestOutputHelper output) : base (output) { } + public InfoReplicationCheckTests(ITestOutputHelper output) : base(output) { } [Fact] public async Task Exec() diff --git a/tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs b/tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs index a7bcfc737..2b4a8797e 100644 --- a/tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs @@ -6,17 +6,17 @@ namespace StackExchange.Redis.Tests.Issues; public class BgSaveResponseTests : TestBase { - public BgSaveResponseTests(ITestOutputHelper output) : base (output) { } + public BgSaveResponseTests(ITestOutputHelper output) : base(output) { } - [Theory (Skip = "We don't need to test this, and it really screws local testing hard.")] + [Theory(Skip = "We don't need to test this, and it really screws local testing hard.")] [InlineData(SaveType.BackgroundSave)] [InlineData(SaveType.BackgroundRewriteAppendOnlyFile)] public async Task ShouldntThrowException(SaveType saveType) { using var conn = Create(allowAdmin: true); - var Server = GetServer(conn); - Server.Save(saveType); + var server = GetServer(conn); + server.Save(saveType); await Task.Delay(1000); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue182Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue182Tests.cs index 1f8837d0b..396b40b5f 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue182Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue182Tests.cs @@ -10,7 +10,7 @@ public class Issue182Tests : TestBase { protected override string GetConfiguration() => $"{TestConfig.Current.PrimaryServerAndPort},responseTimeout=10000"; - public Issue182Tests(ITestOutputHelper output) : base (output) { } + public Issue182Tests(ITestOutputHelper output) : base(output) { } [FactLongRunning] public async Task SetMembers() diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2176Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2176Tests.cs index acfa67bb8..6ec0b86f5 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue2176Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2176Tests.cs @@ -29,8 +29,7 @@ public void Execute_Batch() var tasks = new List(); var batch = db.CreateBatch(); tasks.Add(batch.SortedSetAddAsync(key2, "a", 4567)); - tasks.Add(batch.SortedSetCombineAndStoreAsync(SetOperation.Intersect, - keyIntersect, new RedisKey[] { key, key2 })); + tasks.Add(batch.SortedSetCombineAndStoreAsync(SetOperation.Intersect, keyIntersect, new RedisKey[] { key, key2 })); var rangeByRankTask = batch.SortedSetRangeByRankAsync(keyIntersect); tasks.Add(rangeByRankTask); batch.Execute(); @@ -64,8 +63,7 @@ public void Execute_Transaction() var tasks = new List(); var batch = db.CreateTransaction(); tasks.Add(batch.SortedSetAddAsync(key2, "a", 4567)); - tasks.Add(batch.SortedSetCombineAndStoreAsync(SetOperation.Intersect, - keyIntersect, new RedisKey[] { key, key2 })); + tasks.Add(batch.SortedSetCombineAndStoreAsync(SetOperation.Intersect, keyIntersect, new RedisKey[] { key, key2 })); var rangeByRankTask = batch.SortedSetRangeByRankAsync(keyIntersect); tasks.Add(rangeByRankTask); batch.Execute(); diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2418.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2418.cs index 4fc034843..a22bbbcbd 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue2418.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2418.cs @@ -35,7 +35,6 @@ await db.HashSetAsync(key, new[] Log($"'{pair.Name}'='{pair.Value}'"); } - // filter with LINQ Assert.True(entry.Any(x => x.Name == "some_int"), "Any"); someInt = entry.FirstOrDefault(x => x.Name == "some_int").Value; diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue25Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue25Tests.cs index bcba4fa0c..8841d81d3 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue25Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue25Tests.cs @@ -6,7 +6,7 @@ namespace StackExchange.Redis.Tests.Issues; public class Issue25Tests : TestBase { - public Issue25Tests(ITestOutputHelper output) : base (output) { } + public Issue25Tests(ITestOutputHelper output) : base(output) { } [Fact] public void CaseInsensitive() diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue6Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue6Tests.cs index 8043a317e..3b6d4f8fc 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue6Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue6Tests.cs @@ -2,9 +2,9 @@ namespace StackExchange.Redis.Tests.Issues; -public class Issue6Tests : TestBase +public class Issue6Tests : TestBase { - public Issue6Tests(ITestOutputHelper output) : base (output) { } + public Issue6Tests(ITestOutputHelper output) : base(output) { } [Fact] public void ShouldWorkWithoutEchoOrPing() diff --git a/tests/StackExchange.Redis.Tests/Issues/MassiveDeleteTests.cs b/tests/StackExchange.Redis.Tests/Issues/MassiveDeleteTests.cs index 2655592da..bbd9171ea 100644 --- a/tests/StackExchange.Redis.Tests/Issues/MassiveDeleteTests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/MassiveDeleteTests.cs @@ -67,8 +67,7 @@ public async Task ExecuteMassiveDelete() } watch.Stop(); long remaining = await db.SetLengthAsync(key).ForAwait(); - Log("From {0} to {1}; {2}ms", originally, remaining, - watch.ElapsedMilliseconds); + Log($"From {originally} to {remaining}; {watch.ElapsedMilliseconds}ms"); var counters = GetServer(conn).GetCounters(); Log("Completions: {0} sync, {1} async", counters.Interactive.CompletedSynchronously, counters.Interactive.CompletedAsynchronously); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO22786599Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO22786599Tests.cs index b958b52e1..562eef8c5 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO22786599Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO22786599Tests.cs @@ -12,8 +12,8 @@ public SO22786599Tests(ITestOutputHelper output) : base(output) { } [Fact] public void Execute() { - string CurrentIdsSetDbKey = Me() + ".x"; - string CurrentDetailsSetDbKey = Me() + ".y"; + string currentIdsSetDbKey = Me() + ".x"; + string currentDetailsSetDbKey = Me() + ".y"; RedisValue[] stringIds = Enumerable.Range(1, 750).Select(i => (RedisValue)(i + " id")).ToArray(); RedisValue[] stringDetails = Enumerable.Range(1, 750).Select(i => (RedisValue)(i + " detail")).ToArray(); @@ -23,8 +23,8 @@ public void Execute() var db = conn.GetDatabase(); var tran = db.CreateTransaction(); - tran.SetAddAsync(CurrentIdsSetDbKey, stringIds); - tran.SetAddAsync(CurrentDetailsSetDbKey, stringDetails); + tran.SetAddAsync(currentIdsSetDbKey, stringIds); + tran.SetAddAsync(currentDetailsSetDbKey, stringDetails); var watch = Stopwatch.StartNew(); var isOperationSuccessful = tran.Execute(); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO23949477Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO23949477Tests.cs index 87d26ee05..aa545a3cb 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO23949477Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO23949477Tests.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis.Tests.Issues; public class SO23949477Tests : TestBase { - public SO23949477Tests(ITestOutputHelper output) : base (output) { } + public SO23949477Tests(ITestOutputHelper output) : base(output) { } [Fact] public void Execute() @@ -16,12 +16,14 @@ public void Execute() RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); db.SortedSetAdd(key, "c", 3, When.Always, CommandFlags.FireAndForget); - db.SortedSetAdd(key, - new[] { - new SortedSetEntry("a", 1), - new SortedSetEntry("b", 2), - new SortedSetEntry("d", 4), - new SortedSetEntry("e", 5) + db.SortedSetAdd( + key, + new[] + { + new SortedSetEntry("a", 1), + new SortedSetEntry("b", 2), + new SortedSetEntry("d", 4), + new SortedSetEntry("e", 5), }, When.Always, CommandFlags.FireAndForget); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO24807536Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO24807536Tests.cs index 9881b8c38..d4b449dd3 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO24807536Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO24807536Tests.cs @@ -7,7 +7,7 @@ namespace StackExchange.Redis.Tests.Issues; public class SO24807536Tests : TestBase { - public SO24807536Tests(ITestOutputHelper output) : base (output) { } + public SO24807536Tests(ITestOutputHelper output) : base(output) { } [Fact] public async Task Exec() diff --git a/tests/StackExchange.Redis.Tests/Issues/SO25113323Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO25113323Tests.cs index b10be1aea..8de318e2a 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO25113323Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO25113323Tests.cs @@ -6,7 +6,7 @@ namespace StackExchange.Redis.Tests.Issues; public class SO25113323Tests : TestBase { - public SO25113323Tests(ITestOutputHelper output) : base (output) { } + public SO25113323Tests(ITestOutputHelper output) : base(output) { } [Fact] public async Task SetExpirationToPassed() diff --git a/tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs index f7bde6c4a..b71b2a1f9 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs @@ -47,8 +47,8 @@ private async Task DoStuff(ConnectionMultiplexer conn) var exec = tran.ExecuteAsync(); // SWAP THESE TWO + // bool ok = true; bool ok = await Task.WhenAny(exec, timeout).ForAwait() == exec; - //bool ok = true; if (ok) { diff --git a/tests/StackExchange.Redis.Tests/KeyAndValueTests.cs b/tests/StackExchange.Redis.Tests/KeyAndValueTests.cs index 38f7f062e..1f8864083 100644 --- a/tests/StackExchange.Redis.Tests/KeyAndValueTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyAndValueTests.cs @@ -72,7 +72,7 @@ internal static void CheckSame(RedisValue x, RedisValue y) Assert.True(y == x, "y==x"); Assert.False(x != y, "x!=y"); Assert.False(y != x, "y!=x"); - Assert.True(x.Equals(y),"x.EQ(y)"); + Assert.True(x.Equals(y), "x.EQ(y)"); Assert.True(y.Equals(x), "y.EQ(x)"); Assert.True(x.GetHashCode() == y.GetHashCode(), "GetHashCode"); } @@ -122,9 +122,9 @@ internal static void CheckNull(RedisValue value) Assert.Equal(0L, (long)value); CheckSame(value, value); - //CheckSame(value, default(RedisValue)); - //CheckSame(value, (string)null); - //CheckSame(value, (byte[])null); + // CheckSame(value, default(RedisValue)); + // CheckSame(value, (string)null); + // CheckSame(value, (byte[])null); } [Fact] @@ -145,14 +145,14 @@ public void ValuesAreConvertible() // ReSharper disable RedundantCast Assert.Equal((short)123, c.ToInt16(CultureInfo.InvariantCulture)); Assert.Equal((int)123, c.ToInt32(CultureInfo.InvariantCulture)); - Assert.Equal((long)123, c.ToInt64(CultureInfo.InvariantCulture)); - Assert.Equal((float)123, c.ToSingle(CultureInfo.InvariantCulture)); + Assert.Equal(123L, c.ToInt64(CultureInfo.InvariantCulture)); + Assert.Equal(123F, c.ToSingle(CultureInfo.InvariantCulture)); Assert.Equal("123", c.ToString(CultureInfo.InvariantCulture)); - Assert.Equal((double)123, c.ToDouble(CultureInfo.InvariantCulture)); - Assert.Equal((decimal)123, c.ToDecimal(CultureInfo.InvariantCulture)); + Assert.Equal(123D, c.ToDouble(CultureInfo.InvariantCulture)); + Assert.Equal(123M, c.ToDecimal(CultureInfo.InvariantCulture)); Assert.Equal((ushort)123, c.ToUInt16(CultureInfo.InvariantCulture)); - Assert.Equal((uint)123, c.ToUInt32(CultureInfo.InvariantCulture)); - Assert.Equal((ulong)123, c.ToUInt64(CultureInfo.InvariantCulture)); + Assert.Equal(123U, c.ToUInt32(CultureInfo.InvariantCulture)); + Assert.Equal(123UL, c.ToUInt64(CultureInfo.InvariantCulture)); blob = (byte[])c.ToType(typeof(byte[]), CultureInfo.InvariantCulture); Assert.Equal(3, blob.Length); diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedBatchTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedBatchTests.cs index 0a38766af..e92a7a227 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedBatchTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedBatchTests.cs @@ -1,6 +1,6 @@ -using StackExchange.Redis.KeyspaceIsolation; -using System.Text; +using System.Text; using NSubstitute; +using StackExchange.Redis.KeyspaceIsolation; using Xunit; namespace StackExchange.Redis.Tests; diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs index 8cbe7ad7f..aebc8fc6d 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs @@ -3,10 +3,10 @@ using System.Linq.Expressions; using System.Net; using System.Text; +using System.Threading.Tasks; using NSubstitute; using StackExchange.Redis.KeyspaceIsolation; using Xunit; -using System.Threading.Tasks; namespace StackExchange.Redis.Tests { @@ -133,7 +133,7 @@ public async Task HashSetAsync_2() [Fact] public async Task HashStringLengthAsync() { - await prefixed.HashStringLengthAsync("key","field", CommandFlags.None); + await prefixed.HashStringLengthAsync("key", "field", CommandFlags.None); await mock.Received().HashStringLengthAsync("prefix:key", "field", CommandFlags.None); } diff --git a/tests/StackExchange.Redis.Tests/KeyTests.cs b/tests/StackExchange.Redis.Tests/KeyTests.cs index b0c028d56..bfb57a425 100644 --- a/tests/StackExchange.Redis.Tests/KeyTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyTests.cs @@ -273,7 +273,7 @@ public async Task KeyEncoding() db.ListLeftPush(key, "new value", flags: CommandFlags.FireAndForget); // Depending on server version, this is going to vary - we're sanity checking here. - var listTypes = new [] { "ziplist", "quicklist", "listpack" }; + var listTypes = new[] { "ziplist", "quicklist", "listpack" }; Assert.Contains(db.KeyEncoding(key), listTypes); Assert.Contains(await db.KeyEncodingAsync(key), listTypes); @@ -381,7 +381,7 @@ private static int GetHashSlot(in RedisKey key) { var strategy = new ServerSelectionStrategy(null!) { - ServerType = ServerType.Cluster + ServerType = ServerType.Cluster, }; return strategy.HashSlot(key); } diff --git a/tests/StackExchange.Redis.Tests/LexTests.cs b/tests/StackExchange.Redis.Tests/LexTests.cs index ace821ca6..a72aeb142 100644 --- a/tests/StackExchange.Redis.Tests/LexTests.cs +++ b/tests/StackExchange.Redis.Tests/LexTests.cs @@ -17,17 +17,19 @@ public void QueryRangeAndLengthByLex() RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - db.SortedSetAdd(key, + db.SortedSetAdd( + key, new[] - { - new SortedSetEntry("a", 0), - new SortedSetEntry("b", 0), - new SortedSetEntry("c", 0), - new SortedSetEntry("d", 0), - new SortedSetEntry("e", 0), - new SortedSetEntry("f", 0), - new SortedSetEntry("g", 0), - }, CommandFlags.FireAndForget); + { + new SortedSetEntry("a", 0), + new SortedSetEntry("b", 0), + new SortedSetEntry("c", 0), + new SortedSetEntry("d", 0), + new SortedSetEntry("e", 0), + new SortedSetEntry("f", 0), + new SortedSetEntry("g", 0), + }, + CommandFlags.FireAndForget); var set = db.SortedSetRangeByValue(key, default(RedisValue), "c"); var count = db.SortedSetLengthByValue(key, default(RedisValue), "c"); @@ -64,24 +66,28 @@ public void RemoveRangeByLex() RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - db.SortedSetAdd(key, + db.SortedSetAdd( + key, new[] - { - new SortedSetEntry("aaaa", 0), - new SortedSetEntry("b", 0), - new SortedSetEntry("c", 0), - new SortedSetEntry("d", 0), - new SortedSetEntry("e", 0), - }, CommandFlags.FireAndForget); - db.SortedSetAdd(key, + { + new SortedSetEntry("aaaa", 0), + new SortedSetEntry("b", 0), + new SortedSetEntry("c", 0), + new SortedSetEntry("d", 0), + new SortedSetEntry("e", 0), + }, + CommandFlags.FireAndForget); + db.SortedSetAdd( + key, new[] - { - new SortedSetEntry("foo", 0), - new SortedSetEntry("zap", 0), - new SortedSetEntry("zip", 0), - new SortedSetEntry("ALPHA", 0), - new SortedSetEntry("alpha", 0), - }, CommandFlags.FireAndForget); + { + new SortedSetEntry("foo", 0), + new SortedSetEntry("zap", 0), + new SortedSetEntry("zip", 0), + new SortedSetEntry("ALPHA", 0), + new SortedSetEntry("alpha", 0), + }, + CommandFlags.FireAndForget); var set = db.SortedSetRangeByRank(key); Equate(set, set.Length, "ALPHA", "aaaa", "alpha", "b", "c", "d", "e", "foo", "zap", "zip"); diff --git a/tests/StackExchange.Redis.Tests/LockingTests.cs b/tests/StackExchange.Redis.Tests/LockingTests.cs index a2ce6986d..c2aa3611f 100644 --- a/tests/StackExchange.Redis.Tests/LockingTests.cs +++ b/tests/StackExchange.Redis.Tests/LockingTests.cs @@ -11,13 +11,13 @@ namespace StackExchange.Redis.Tests; public class LockingTests : TestBase { protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public LockingTests(ITestOutputHelper output) : base (output) { } + public LockingTests(ITestOutputHelper output) : base(output) { } public enum TestMode { MultiExec, NoMultiExec, - Twemproxy + Twemproxy, } public static IEnumerable TestModes() @@ -38,12 +38,12 @@ public void AggressiveParallel(TestMode testMode) using (var conn1 = Create(testMode)) using (var conn2 = Create(testMode)) { - void cb(object? obj) + void Inner(object? obj) { try { var conn = (IDatabase?)obj!; - conn.Multiplexer.ErrorMessage += delegate { Interlocked.Increment(ref errorCount); }; + conn.Multiplexer.ErrorMessage += (sender, e) => Interlocked.Increment(ref errorCount); for (int i = 0; i < 1000; i++) { @@ -58,8 +58,8 @@ void cb(object? obj) } } int db = testMode == TestMode.Twemproxy ? 0 : 2; - ThreadPool.QueueUserWorkItem(cb, conn1.GetDatabase(db)); - ThreadPool.QueueUserWorkItem(cb, conn2.GetDatabase(db)); + ThreadPool.QueueUserWorkItem(Inner, conn1.GetDatabase(db)); + ThreadPool.QueueUserWorkItem(Inner, conn2.GetDatabase(db)); evt.WaitOne(8000); } Assert.Equal(0, Interlocked.CompareExchange(ref errorCount, 0, 0)); @@ -78,23 +78,23 @@ public void TestOpCountByVersionLocal_UpLevel() private void TestLockOpCountByVersion(IConnectionMultiplexer conn, int expectedOps, bool existFirst) { const int LockDuration = 30; - RedisKey Key = Me(); + RedisKey key = Me(); var db = conn.GetDatabase(); - db.KeyDelete(Key, CommandFlags.FireAndForget); + db.KeyDelete(key, CommandFlags.FireAndForget); RedisValue newVal = "us:" + Guid.NewGuid().ToString(); RedisValue expectedVal = newVal; if (existFirst) { expectedVal = "other:" + Guid.NewGuid().ToString(); - db.StringSet(Key, expectedVal, TimeSpan.FromSeconds(LockDuration), flags: CommandFlags.FireAndForget); + db.StringSet(key, expectedVal, TimeSpan.FromSeconds(LockDuration), flags: CommandFlags.FireAndForget); } long countBefore = GetServer(conn).GetCounters().Interactive.OperationCount; - var taken = db.LockTake(Key, newVal, TimeSpan.FromSeconds(LockDuration)); + var taken = db.LockTake(key, newVal, TimeSpan.FromSeconds(LockDuration)); long countAfter = GetServer(conn).GetCounters().Interactive.OperationCount; - var valAfter = db.StringGet(Key); + var valAfter = db.StringGet(key); Assert.Equal(!existFirst, taken); Assert.Equal(expectedVal, valAfter); @@ -118,28 +118,28 @@ public async Task TakeLockAndExtend(TestMode testMode) RedisValue right = Guid.NewGuid().ToString(), wrong = Guid.NewGuid().ToString(); - int DB = testMode == TestMode.Twemproxy ? 0 : 7; - RedisKey Key = Me() + testMode; + int dbId = testMode == TestMode.Twemproxy ? 0 : 7; + RedisKey key = Me() + testMode; - var db = conn.GetDatabase(DB); + var db = conn.GetDatabase(dbId); - db.KeyDelete(Key, CommandFlags.FireAndForget); + db.KeyDelete(key, CommandFlags.FireAndForget); bool withTran = testMode == TestMode.MultiExec; - var t1 = db.LockTakeAsync(Key, right, TimeSpan.FromSeconds(20)); - var t1b = db.LockTakeAsync(Key, wrong, TimeSpan.FromSeconds(10)); - var t2 = db.LockQueryAsync(Key); - var t3 = withTran ? db.LockReleaseAsync(Key, wrong) : null; - var t4 = db.LockQueryAsync(Key); - var t5 = withTran ? db.LockExtendAsync(Key, wrong, TimeSpan.FromSeconds(60)) : null; - var t6 = db.LockQueryAsync(Key); - var t7 = db.KeyTimeToLiveAsync(Key); - var t8 = db.LockExtendAsync(Key, right, TimeSpan.FromSeconds(60)); - var t9 = db.LockQueryAsync(Key); - var t10 = db.KeyTimeToLiveAsync(Key); - var t11 = db.LockReleaseAsync(Key, right); - var t12 = db.LockQueryAsync(Key); - var t13 = db.LockTakeAsync(Key, wrong, TimeSpan.FromSeconds(10)); + var t1 = db.LockTakeAsync(key, right, TimeSpan.FromSeconds(20)); + var t1b = db.LockTakeAsync(key, wrong, TimeSpan.FromSeconds(10)); + var t2 = db.LockQueryAsync(key); + var t3 = withTran ? db.LockReleaseAsync(key, wrong) : null; + var t4 = db.LockQueryAsync(key); + var t5 = withTran ? db.LockExtendAsync(key, wrong, TimeSpan.FromSeconds(60)) : null; + var t6 = db.LockQueryAsync(key); + var t7 = db.KeyTimeToLiveAsync(key); + var t8 = db.LockExtendAsync(key, right, TimeSpan.FromSeconds(60)); + var t9 = db.LockQueryAsync(key); + var t10 = db.KeyTimeToLiveAsync(key); + var t11 = db.LockReleaseAsync(key, right); + var t12 = db.LockQueryAsync(key); + var t13 = db.LockTakeAsync(key, wrong, TimeSpan.FromSeconds(10)); Assert.NotEqual(default(RedisValue), right); Assert.NotEqual(default(RedisValue), wrong); @@ -168,7 +168,7 @@ public async Task TestBasicLockNotTaken(TestMode testMode) using var conn = Create(testMode); int errorCount = 0; - conn.ErrorMessage += delegate { Interlocked.Increment(ref errorCount); }; + conn.ErrorMessage += (sender, e) => Interlocked.Increment(ref errorCount); Task? taken = null; Task? newValue = null; Task? ttl = null; diff --git a/tests/StackExchange.Redis.Tests/LoggerTests.cs b/tests/StackExchange.Redis.Tests/LoggerTests.cs index 1d15a69b9..217864ba7 100644 --- a/tests/StackExchange.Redis.Tests/LoggerTests.cs +++ b/tests/StackExchange.Redis.Tests/LoggerTests.cs @@ -1,10 +1,10 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using System; +using System; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Xunit; using Xunit.Abstractions; @@ -65,16 +65,16 @@ public void Dispose() { } public class TestWrapperLogger : ILogger { public int LogCount = 0; - public ILogger _inner { get; } + private ILogger Inner { get; } - public TestWrapperLogger(ILogger toWrap) => _inner = toWrap; + public TestWrapperLogger(ILogger toWrap) => Inner = toWrap; - public IDisposable BeginScope(TState state) => _inner.BeginScope(state); - public bool IsEnabled(LogLevel logLevel) => _inner.IsEnabled(logLevel); + public IDisposable BeginScope(TState state) => Inner.BeginScope(state); + public bool IsEnabled(LogLevel logLevel) => Inner.IsEnabled(logLevel); public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { Interlocked.Increment(ref LogCount); - _inner.Log(logLevel, eventId, state, exception, formatter); + Inner.Log(logLevel, eventId, state, exception, formatter); } } diff --git a/tests/StackExchange.Redis.Tests/MassiveOpsTests.cs b/tests/StackExchange.Redis.Tests/MassiveOpsTests.cs index 26816a8b1..3b3f8157e 100644 --- a/tests/StackExchange.Redis.Tests/MassiveOpsTests.cs +++ b/tests/StackExchange.Redis.Tests/MassiveOpsTests.cs @@ -38,7 +38,7 @@ public async Task MassiveBulkOpsAsync(bool withContinuation) RedisKey key = Me(); var db = conn.GetDatabase(); await db.PingAsync().ForAwait(); - static void nonTrivial(Task _) + static void NonTrivial(Task unused) { Thread.SpinWait(5); } @@ -49,13 +49,12 @@ static void nonTrivial(Task _) if (withContinuation) { // Intentionally unawaited - _ = t.ContinueWith(nonTrivial); + _ = t.ContinueWith(NonTrivial); } } Assert.Equal(AsyncOpsQty, await db.StringGetAsync(key).ForAwait()); watch.Stop(); - Log("{2}: Time for {0} ops: {1}ms ({3}, any order); ops/s: {4}", AsyncOpsQty, watch.ElapsedMilliseconds, Me(), - withContinuation ? "with continuation" : "no continuation", AsyncOpsQty / watch.Elapsed.TotalSeconds); + Log($"{Me()}: Time for {AsyncOpsQty} ops: {watch.ElapsedMilliseconds}ms ({(withContinuation ? "with continuation" : "no continuation")}, any order); ops/s: {AsyncOpsQty / watch.Elapsed.TotalSeconds}"); } [TheoryLongRunning] @@ -71,18 +70,19 @@ public void MassiveBulkOpsSync(int threads) var db = conn.GetDatabase(); db.KeyDelete(key, CommandFlags.FireAndForget); int workPerThread = SyncOpsQty / threads; - var timeTaken = RunConcurrent(delegate - { - for (int i = 0; i < workPerThread; i++) + var timeTaken = RunConcurrent( + () => { - db.StringIncrement(key, flags: CommandFlags.FireAndForget); - } - }, threads); + for (int i = 0; i < workPerThread; i++) + { + db.StringIncrement(key, flags: CommandFlags.FireAndForget); + } + }, + threads); int val = (int)db.StringGet(key); Assert.Equal(workPerThread * threads, val); - Log("{2}: Time for {0} ops on {3} threads: {1}ms (any order); ops/s: {4}", - threads * workPerThread, timeTaken.TotalMilliseconds, Me(), threads, (workPerThread * threads) / timeTaken.TotalSeconds); + Log($"{Me()}: Time for {threads * workPerThread} ops on {threads} threads: {timeTaken.TotalMilliseconds}ms (any order); ops/s: {(workPerThread * threads) / timeTaken.TotalSeconds}"); } [Theory] @@ -98,19 +98,19 @@ public void MassiveBulkOpsFireAndForget(int threads) db.KeyDelete(key, CommandFlags.FireAndForget); int perThread = AsyncOpsQty / threads; - var elapsed = RunConcurrent(delegate - { - for (int i = 0; i < perThread; i++) + var elapsed = RunConcurrent( + () => { - db.StringIncrement(key, flags: CommandFlags.FireAndForget); - } - db.Ping(); - }, threads); + for (int i = 0; i < perThread; i++) + { + db.StringIncrement(key, flags: CommandFlags.FireAndForget); + } + db.Ping(); + }, + threads); var val = (long)db.StringGet(key); Assert.Equal(perThread * threads, val); - Log("{2}: Time for {0} ops over {4} threads: {1:###,###}ms (any order); ops/s: {3:###,###,##0}", - val, elapsed.TotalMilliseconds, Me(), - val / elapsed.TotalSeconds, threads); + Log($"{Me()}: Time for {val} ops over {threads} threads: {elapsed.TotalMilliseconds:###,###}ms (any order); ops/s: {val / elapsed.TotalSeconds:###,###,##0}"); } } diff --git a/tests/StackExchange.Redis.Tests/MigrateTests.cs b/tests/StackExchange.Redis.Tests/MigrateTests.cs index 1fad9adf3..54fe77649 100644 --- a/tests/StackExchange.Redis.Tests/MigrateTests.cs +++ b/tests/StackExchange.Redis.Tests/MigrateTests.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis.Tests; public class MigrateTests : TestBase { - public MigrateTests(ITestOutputHelper output) : base (output) { } + public MigrateTests(ITestOutputHelper output) : base(output) { } [FactLongRunning] public async Task Basic() diff --git a/tests/StackExchange.Redis.Tests/MultiAddTests.cs b/tests/StackExchange.Redis.Tests/MultiAddTests.cs index d542b80b6..cf5f70c23 100644 --- a/tests/StackExchange.Redis.Tests/MultiAddTests.cs +++ b/tests/StackExchange.Redis.Tests/MultiAddTests.cs @@ -19,20 +19,40 @@ public void AddSortedSetEveryWay() db.KeyDelete(key, CommandFlags.FireAndForget); db.SortedSetAdd(key, "a", 1, CommandFlags.FireAndForget); - db.SortedSetAdd(key, new[] { - new SortedSetEntry("b", 2) }, CommandFlags.FireAndForget); - db.SortedSetAdd(key, new[] { + db.SortedSetAdd( + key, + new[] + { + new SortedSetEntry("b", 2), + }, + CommandFlags.FireAndForget); + db.SortedSetAdd( + key, + new[] + { new SortedSetEntry("c", 3), - new SortedSetEntry("d", 4)}, CommandFlags.FireAndForget); - db.SortedSetAdd(key, new[] { + new SortedSetEntry("d", 4), + }, + CommandFlags.FireAndForget); + db.SortedSetAdd( + key, + new[] + { new SortedSetEntry("e", 5), new SortedSetEntry("f", 6), - new SortedSetEntry("g", 7)}, CommandFlags.FireAndForget); - db.SortedSetAdd(key, new[] { + new SortedSetEntry("g", 7), + }, + CommandFlags.FireAndForget); + db.SortedSetAdd( + key, + new[] + { new SortedSetEntry("h", 8), new SortedSetEntry("i", 9), new SortedSetEntry("j", 10), - new SortedSetEntry("k", 11)}, CommandFlags.FireAndForget); + new SortedSetEntry("k", 11), + }, + CommandFlags.FireAndForget); var vals = db.SortedSetRangeByScoreWithScores(key); string s = string.Join(",", vals.OrderByDescending(x => x.Score).Select(x => x.Element)); Assert.Equal("k,j,i,h,g,f,e,d,c,b,a", s); @@ -50,20 +70,40 @@ public void AddHashEveryWay() db.KeyDelete(key, CommandFlags.FireAndForget); db.HashSet(key, "a", 1, flags: CommandFlags.FireAndForget); - db.HashSet(key, new[] { - new HashEntry("b", 2) }, CommandFlags.FireAndForget); - db.HashSet(key, new[] { + db.HashSet( + key, + new[] + { + new HashEntry("b", 2), + }, + CommandFlags.FireAndForget); + db.HashSet( + key, + new[] + { new HashEntry("c", 3), - new HashEntry("d", 4)}, CommandFlags.FireAndForget); - db.HashSet(key, new[] { + new HashEntry("d", 4), + }, + CommandFlags.FireAndForget); + db.HashSet( + key, + new[] + { new HashEntry("e", 5), new HashEntry("f", 6), - new HashEntry("g", 7)}, CommandFlags.FireAndForget); - db.HashSet(key, new[] { + new HashEntry("g", 7), + }, + CommandFlags.FireAndForget); + db.HashSet( + key, + new[] + { new HashEntry("h", 8), new HashEntry("i", 9), new HashEntry("j", 10), - new HashEntry("k", 11)}, CommandFlags.FireAndForget); + new HashEntry("k", 11), + }, + CommandFlags.FireAndForget); var vals = db.HashGetAll(key); string s = string.Join(",", vals.OrderByDescending(x => (double)x.Value).Select(x => x.Name)); Assert.Equal("k,j,i,h,g,f,e,d,c,b,a", s); diff --git a/tests/StackExchange.Redis.Tests/MultiPrimaryTests.cs b/tests/StackExchange.Redis.Tests/MultiPrimaryTests.cs index 52b6c6297..bf3e5690e 100644 --- a/tests/StackExchange.Redis.Tests/MultiPrimaryTests.cs +++ b/tests/StackExchange.Redis.Tests/MultiPrimaryTests.cs @@ -10,7 +10,7 @@ public class MultiPrimaryTests : TestBase { protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword; - public MultiPrimaryTests(ITestOutputHelper output) : base (output) { } + public MultiPrimaryTests(ITestOutputHelper output) : base(output) { } [Fact] public void CannotFlushReplica() diff --git a/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs b/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs index e0b09d545..f562a850b 100644 --- a/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs +++ b/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs @@ -12,7 +12,7 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] public class OverloadCompatTests : TestBase { - public OverloadCompatTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public OverloadCompatTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public async Task KeyExpire() @@ -42,7 +42,6 @@ public async Task KeyExpire() db.KeyExpire(key, expireTime, when: when, flags: flags); // Async - await db.KeyExpireAsync(key, expiresIn); await db.KeyExpireAsync(key, expiresIn, when); await db.KeyExpireAsync(key, expiresIn, when: when); @@ -89,7 +88,6 @@ public async Task StringBitCount() db.StringBitCount(key, start: 1, end: 1, flags: flags); // Async - await db.StringBitCountAsync(key); await db.StringBitCountAsync(key, 1); await db.StringBitCountAsync(key, 0, 0); @@ -138,7 +136,6 @@ public async Task StringBitPosition() db.StringBitPosition(key, true, start: 1, end: 1, flags: flags); // Async - await db.StringBitPositionAsync(key, true); await db.StringBitPositionAsync(key, true, 1); await db.StringBitPositionAsync(key, true, 1, 3); @@ -166,7 +163,7 @@ public async Task SortedSetAdd() RedisKey key = Me(); RedisValue val = "myval"; var score = 1.0d; - var values = new SortedSetEntry[]{new SortedSetEntry(val, score)}; + var values = new SortedSetEntry[] { new SortedSetEntry(val, score) }; var when = When.Exists; var flags = CommandFlags.None; @@ -191,7 +188,6 @@ public async Task SortedSetAdd() db.SortedSetAdd(key, values, when: when, flags: flags); // Async - await db.SortedSetAddAsync(key, val, score); await db.SortedSetAddAsync(key, val, score, when); await db.SortedSetAddAsync(key, val, score, when: when); @@ -239,7 +235,6 @@ public async Task StringSet() db.StringSet(key, val, null, When.NotExists, flags); // Async - await db.StringSetAsync(key, val); await db.StringSetAsync(key, val, expiry: expiresIn); await db.StringSetAsync(key, val, when: when); diff --git a/tests/StackExchange.Redis.Tests/PreserveOrderTests.cs b/tests/StackExchange.Redis.Tests/PreserveOrderTests.cs index f11cda5c5..0cb5eb271 100644 --- a/tests/StackExchange.Redis.Tests/PreserveOrderTests.cs +++ b/tests/StackExchange.Redis.Tests/PreserveOrderTests.cs @@ -10,7 +10,7 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] public class PreserveOrderTests : TestBase { - public PreserveOrderTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base (output, fixture) { } + public PreserveOrderTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } [Fact] public void Execute() diff --git a/tests/StackExchange.Redis.Tests/ProfilingTests.cs b/tests/StackExchange.Redis.Tests/ProfilingTests.cs index 7c4dcbe59..2d0264984 100644 --- a/tests/StackExchange.Redis.Tests/ProfilingTests.cs +++ b/tests/StackExchange.Redis.Tests/ProfilingTests.cs @@ -1,12 +1,12 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using System.Threading; -using System.Collections.Concurrent; +using System.Threading.Tasks; +using StackExchange.Redis.Profiling; using Xunit; using Xunit.Abstractions; -using StackExchange.Redis.Profiling; namespace StackExchange.Redis.Tests; @@ -182,8 +182,8 @@ public void ManyContexts() { var g = db.StringGetAsync(prefix + ix); var s = db.StringSetAsync(prefix + ix, "world" + ix); - // overlap the g+s, just for fun - await g; + // overlap the g+s, just for fun + await g; await s; } @@ -243,11 +243,7 @@ public void LowAllocationEnumerable() foreach (var i in Enumerable.Range(0, OuterLoop)) { - var t = - db.StringSetAsync(prefix + i, "bar" + i) - .ContinueWith( - async _ => (string?)(await db.StringGetAsync(prefix + i).ForAwait()) - ); + var t = db.StringSetAsync(prefix + i, "bar" + i).ContinueWith(async _ => (string?)(await db.StringGetAsync(prefix + i).ForAwait())); var finalResult = t.Unwrap(); allTasks.Add(finalResult); diff --git a/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs b/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs index e689c980b..f3ea9eca7 100644 --- a/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs @@ -29,7 +29,7 @@ public void SubscriberCount() _ = server.SubscriptionPatternCount(); var count = server.SubscriptionSubscriberCount(channel); Assert.Equal(0, count); - conn.GetSubscriber().Subscribe(channel, delegate { }); + conn.GetSubscriber().Subscribe(channel, (channel, value) => { }); count = server.SubscriptionSubscriberCount(channel); Assert.Equal(1, count); @@ -57,7 +57,7 @@ public async Task SubscriberCountAsync() _ = await server.SubscriptionPatternCountAsync().WithTimeout(2000); var count = await server.SubscriptionSubscriberCountAsync(channel).WithTimeout(2000); Assert.Equal(0, count); - await conn.GetSubscriber().SubscribeAsync(channel, delegate { }).WithTimeout(2000); + await conn.GetSubscriber().SubscribeAsync(channel, (channel, value) => { }).WithTimeout(2000); count = await server.SubscriptionSubscriberCountAsync(channel).WithTimeout(2000); Assert.Equal(1, count); @@ -69,8 +69,7 @@ public async Task SubscriberCountAsync() } internal static class Util { - public static async Task WithTimeout(this Task task, int timeoutMs, - [CallerMemberName] string? caller = null, [CallerLineNumber] int line = 0) + public static async Task WithTimeout(this Task task, int timeoutMs, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = 0) { var cts = new CancellationTokenSource(); if (task == await Task.WhenAny(task, Task.Delay(timeoutMs, cts.Token)).ForAwait()) @@ -83,8 +82,7 @@ public static async Task WithTimeout(this Task task, int timeoutMs, throw new TimeoutException($"timout from {caller} line {line}"); } } - public static async Task WithTimeout(this Task task, int timeoutMs, - [CallerMemberName] string? caller = null, [CallerLineNumber] int line = 0) + public static async Task WithTimeout(this Task task, int timeoutMs, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = 0) { var cts = new CancellationTokenSource(); if (task == await Task.WhenAny(task, Task.Delay(timeoutMs, cts.Token)).ForAwait()) diff --git a/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs b/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs index aa363984f..d7a0ee513 100644 --- a/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs @@ -123,11 +123,14 @@ public async Task PrimaryReplicaSubscriptionFailover(CommandFlags flags, bool ex var count = 0; Log("Subscribing..."); - await sub.SubscribeAsync(channel, (_, val) => - { - Interlocked.Increment(ref count); - Log("Message: " + val); - }, flags); + await sub.SubscribeAsync( + channel, + (_, val) => + { + Interlocked.Increment(ref count); + Log("Message: " + val); + }, + flags); Assert.True(sub.IsConnected(channel)); Log("Publishing (1)..."); diff --git a/tests/StackExchange.Redis.Tests/PubSubTests.cs b/tests/StackExchange.Redis.Tests/PubSubTests.cs index 697bf2771..d064f298d 100644 --- a/tests/StackExchange.Redis.Tests/PubSubTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubTests.cs @@ -8,7 +8,6 @@ using StackExchange.Redis.Maintenance; using Xunit; using Xunit.Abstractions; -// ReSharper disable AccessToModifiedClosure namespace StackExchange.Redis.Tests; @@ -33,7 +32,8 @@ public async Task ExplicitPublishMode() pub.Publish("abcd", "efg"); #pragma warning restore CS0618 - await UntilConditionAsync(TimeSpan.FromSeconds(10), + await UntilConditionAsync( + TimeSpan.FromSeconds(10), () => Thread.VolatileRead(ref b) == 1 && Thread.VolatileRead(ref c) == 1 && Thread.VolatileRead(ref d) == 1); @@ -80,8 +80,7 @@ public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string br Log(channel); } } - } - , handler2 = (_, __) => Interlocked.Increment(ref secondHandler); + }, handler2 = (_, __) => Interlocked.Increment(ref secondHandler); #pragma warning disable CS0618 sub.Subscribe(subChannel, handler1); sub.Subscribe(subChannel, handler2); @@ -154,16 +153,19 @@ public async Task TestBasicPubSubFireAndForget() HashSet received = new(); int secondHandler = 0; await PingAsync(pub, sub).ForAwait(); - sub.Subscribe(key, (channel, payload) => - { - lock (received) + sub.Subscribe( + key, + (channel, payload) => { - if (channel == key) + lock (received) { - received.Add(payload); + if (channel == key) + { + received.Add(payload); + } } - } - }, CommandFlags.FireAndForget); + }, + CommandFlags.FireAndForget); sub.Subscribe(key, (_, __) => Interlocked.Increment(ref secondHandler), CommandFlags.FireAndForget); Log(profiler); @@ -326,8 +328,7 @@ private void TestMassivePublish(ISubscriber sub, string channel, string caption) sub.WaitAll(tasks); withAsync.Stop(); - Log("{2}: {0}ms (F+F) vs {1}ms (async)", - withFAF.ElapsedMilliseconds, withAsync.ElapsedMilliseconds, caption); + Log($"{caption}: {withFAF.ElapsedMilliseconds}ms (F+F) vs {withAsync.ElapsedMilliseconds}ms (async)"); // We've made async so far, this test isn't really valid anymore // So let's check they're at least within a few seconds. Assert.True(withFAF.ElapsedMilliseconds < withAsync.ElapsedMilliseconds + 3000, caption); @@ -345,7 +346,8 @@ public async Task SubscribeAsyncEnumerable() var gotall = new TaskCompletionSource(); var source = await sub.SubscribeAsync(channel); - var op = Task.Run(async () => { + var op = Task.Run(async () => + { int count = 0; await foreach (var item in source) { @@ -479,10 +481,7 @@ async Task RunLoop() Log("Awaiting completion."); await subChannel.Completion; Log("Completion awaited."); - await Assert.ThrowsAsync(async delegate - { - await subChannel.ReadAsync().ForAwait(); - }).ForAwait(); + await Assert.ThrowsAsync(async () => await subChannel.ReadAsync().ForAwait()).ForAwait(); Log("End of muxer."); } Log("End of test."); @@ -548,10 +547,7 @@ public async Task PubSubGetAllCorrectOrder_OnMessage_Sync() await subChannel.Completion; Log("Completion awaited."); Assert.True(subChannel.Completion.IsCompleted); - await Assert.ThrowsAsync(async delegate - { - await subChannel.ReadAsync().ForAwait(); - }).ForAwait(); + await Assert.ThrowsAsync(async () => await subChannel.ReadAsync().ForAwait()).ForAwait(); Log("End of muxer."); } Log("End of test."); @@ -622,10 +618,7 @@ public async Task PubSubGetAllCorrectOrder_OnMessage_Async() await subChannel.Completion; Log("Completion awaited."); Assert.True(subChannel.Completion.IsCompleted); - await Assert.ThrowsAsync(async delegate - { - await subChannel.ReadAsync().ForAwait(); - }).ForAwait(); + await Assert.ThrowsAsync(async () => await subChannel.ReadAsync().ForAwait()).ForAwait(); Log("End of muxer."); } Log("End of test."); @@ -642,8 +635,8 @@ public async Task TestPublishWithSubscribers() var listenA = connA.GetSubscriber(); var listenB = connB.GetSubscriber(); #pragma warning disable CS0618 - var t1 = listenA.SubscribeAsync(channel, delegate { }); - var t2 = listenB.SubscribeAsync(channel, delegate { }); + var t1 = listenA.SubscribeAsync(channel, (arg1, arg2) => { }); + var t2 = listenB.SubscribeAsync(channel, (arg1, arg2) => { }); #pragma warning restore CS0618 await Task.WhenAll(t1, t2).ForAwait(); @@ -696,12 +689,12 @@ public async Task Issue38() var sub = conn.GetSubscriber(); int count = 0; var prefix = Me(); - void handler(RedisChannel _, RedisValue __) => Interlocked.Increment(ref count); + void Handler(RedisChannel unused, RedisValue unused2) => Interlocked.Increment(ref count); #pragma warning disable CS0618 - var a0 = sub.SubscribeAsync(prefix + "foo", handler); - var a1 = sub.SubscribeAsync(prefix + "bar", handler); - var b0 = sub.SubscribeAsync(prefix + "f*o", handler); - var b1 = sub.SubscribeAsync(prefix + "b*r", handler); + var a0 = sub.SubscribeAsync(prefix + "foo", Handler); + var a1 = sub.SubscribeAsync(prefix + "bar", Handler); + var b0 = sub.SubscribeAsync(prefix + "f*o", Handler); + var b1 = sub.SubscribeAsync(prefix + "b*r", Handler); #pragma warning restore CS0618 await Task.WhenAll(a0, a1, b0, b1).ForAwait(); @@ -767,8 +760,8 @@ public async Task TestSubscribeUnsubscribeAndSubscribeAgain() var sub = connSub.GetSubscriber(); int x = 0, y = 0; #pragma warning disable CS0618 - var t1 = sub.SubscribeAsync(prefix + "abc", delegate { Interlocked.Increment(ref x); }); - var t2 = sub.SubscribeAsync(prefix + "ab*", delegate { Interlocked.Increment(ref y); }); + var t1 = sub.SubscribeAsync(prefix + "abc", (arg1, arg2) => Interlocked.Increment(ref x)); + var t2 = sub.SubscribeAsync(prefix + "ab*", (arg1, arg2) => Interlocked.Increment(ref y)); await Task.WhenAll(t1, t2).ForAwait(); pub.Publish(prefix + "abc", ""); await AllowReasonableTimeToPublishAndProcess().ForAwait(); @@ -780,8 +773,8 @@ public async Task TestSubscribeUnsubscribeAndSubscribeAgain() pub.Publish(prefix + "abc", ""); Assert.Equal(1, Volatile.Read(ref x)); Assert.Equal(1, Volatile.Read(ref y)); - t1 = sub.SubscribeAsync(prefix + "abc", delegate { Interlocked.Increment(ref x); }); - t2 = sub.SubscribeAsync(prefix + "ab*", delegate { Interlocked.Increment(ref y); }); + t1 = sub.SubscribeAsync(prefix + "abc", (arg1, arg2) => Interlocked.Increment(ref x)); + t2 = sub.SubscribeAsync(prefix + "ab*", (arg1, arg2) => Interlocked.Increment(ref y)); await Task.WhenAll(t1, t2).ForAwait(); pub.Publish(prefix + "abc", ""); #pragma warning restore CS0618 @@ -801,7 +794,7 @@ public async Task AzureRedisEventsAutomaticSubscribe() { EndPoints = { TestConfig.Current.AzureCacheServer }, Password = TestConfig.Current.AzureCachePassword, - Ssl = true + Ssl = true, }; using (var connection = await ConnectionMultiplexer.ConnectAsync(options)) diff --git a/tests/StackExchange.Redis.Tests/RedisResultTests.cs b/tests/StackExchange.Redis.Tests/RedisResultTests.cs index 27ceba755..47ac20a9e 100644 --- a/tests/StackExchange.Redis.Tests/RedisResultTests.cs +++ b/tests/StackExchange.Redis.Tests/RedisResultTests.cs @@ -5,12 +5,12 @@ namespace StackExchange.Redis.Tests; /// -/// Tests for +/// Tests for . /// public sealed class RedisResultTests { /// - /// Tests the basic functionality of + /// Tests the basic functionality of . /// [Fact] public void ToDictionaryWorks() @@ -29,16 +29,16 @@ public void ToDictionaryWorks() /// /// Tests the basic functionality of - /// when the results contain a nested results array, which is common for lua script results + /// when the results contain a nested results array, which is common for lua script results. /// [Fact] public void ToDictionaryWorksWhenNested() { var redisArrayResult = RedisResult.Create( - new [] + new[] { RedisResult.Create((RedisValue)"one"), - RedisResult.Create(new RedisValue[]{"two", 2, "three", 3}), + RedisResult.Create(new RedisValue[] { "two", 2, "three", 3 }), RedisResult.Create((RedisValue)"four"), RedisResult.Create(new RedisValue[] { "five", 5, "six", 6 }), @@ -67,7 +67,7 @@ public void ToDictionaryFailsWithDuplicateKeys() } /// - /// Tests that correctly uses the provided comparator + /// Tests that correctly uses the provided comparator. /// [Fact] public void ToDictionaryWorksWithCustomComparator() @@ -84,7 +84,7 @@ public void ToDictionaryWorksWithCustomComparator() /// /// Tests that fails when the redis results array contains an odd number - /// of elements. In other words, it's not actually a Key,Value,Key,Value... etc. array + /// of elements. In other words, it's not actually a Key,Value,Key,Value... etc. array. /// [Fact] public void ToDictionaryFailsOnMishapenResults() @@ -92,7 +92,7 @@ public void ToDictionaryFailsOnMishapenResults() var redisArrayResult = RedisResult.Create( new RedisValue[] { "one", 1, "two", 2, "three", 3, "four" /* missing 4 */ }); - Assert.Throws(()=>redisArrayResult.ToDictionary(StringComparer.Ordinal)); + Assert.Throws(() => redisArrayResult.ToDictionary(StringComparer.Ordinal)); } [Fact] @@ -101,8 +101,8 @@ public void SingleResultConvertibleViaTo() var value = RedisResult.Create(123); Assert.StrictEqual((int)123, Convert.ToInt32(value)); Assert.StrictEqual((uint)123U, Convert.ToUInt32(value)); - Assert.StrictEqual((long)123, Convert.ToInt64(value)); - Assert.StrictEqual((ulong)123U, Convert.ToUInt64(value)); + Assert.StrictEqual(123L, Convert.ToInt64(value)); + Assert.StrictEqual(123UL, Convert.ToUInt64(value)); Assert.StrictEqual((byte)123, Convert.ToByte(value)); Assert.StrictEqual((sbyte)123, Convert.ToSByte(value)); Assert.StrictEqual((short)123, Convert.ToInt16(value)); @@ -120,8 +120,8 @@ public void SingleResultConvertibleDirectViaChangeType_Type() var value = RedisResult.Create(123); Assert.StrictEqual((int)123, Convert.ChangeType(value, typeof(int))); Assert.StrictEqual((uint)123U, Convert.ChangeType(value, typeof(uint))); - Assert.StrictEqual((long)123, Convert.ChangeType(value, typeof(long))); - Assert.StrictEqual((ulong)123U, Convert.ChangeType(value, typeof(ulong))); + Assert.StrictEqual(123L, Convert.ChangeType(value, typeof(long))); + Assert.StrictEqual(123UL, Convert.ChangeType(value, typeof(ulong))); Assert.StrictEqual((byte)123, Convert.ChangeType(value, typeof(byte))); Assert.StrictEqual((sbyte)123, Convert.ChangeType(value, typeof(sbyte))); Assert.StrictEqual((short)123, Convert.ChangeType(value, typeof(short))); @@ -139,8 +139,8 @@ public void SingleResultConvertibleDirectViaChangeType_TypeCode() var value = RedisResult.Create(123); Assert.StrictEqual((int)123, Convert.ChangeType(value, TypeCode.Int32)); Assert.StrictEqual((uint)123U, Convert.ChangeType(value, TypeCode.UInt32)); - Assert.StrictEqual((long)123, Convert.ChangeType(value, TypeCode.Int64)); - Assert.StrictEqual((ulong)123U, Convert.ChangeType(value, TypeCode.UInt64)); + Assert.StrictEqual(123L, Convert.ChangeType(value, TypeCode.Int64)); + Assert.StrictEqual(123UL, Convert.ChangeType(value, TypeCode.UInt64)); Assert.StrictEqual((byte)123, Convert.ChangeType(value, TypeCode.Byte)); Assert.StrictEqual((sbyte)123, Convert.ChangeType(value, TypeCode.SByte)); Assert.StrictEqual((short)123, Convert.ChangeType(value, TypeCode.Int16)); diff --git a/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs b/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs index 2d9c69fad..7a56d16b0 100644 --- a/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs +++ b/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs @@ -9,7 +9,6 @@ public class RedisValueEquivalency { // internal storage types: null, integer, double, string, raw // public perceived types: int, long, double, bool, memory / byte[] - [Fact] public void Int32_Matrix() { diff --git a/tests/StackExchange.Redis.Tests/RespProtocolTests.cs b/tests/StackExchange.Redis.Tests/RespProtocolTests.cs index 6c444e43c..c2cd8a026 100644 --- a/tests/StackExchange.Redis.Tests/RespProtocolTests.cs +++ b/tests/StackExchange.Redis.Tests/RespProtocolTests.cs @@ -132,8 +132,7 @@ public async Task ConnectWithBrokenHello(string command, bool isResp3) [InlineData(@"return {1,2,3}", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, ARR_123)] [InlineData("return nil", RedisProtocol.Resp2, ResultType.BulkString, ResultType.Null, null)] [InlineData(@"return redis.pcall('hgetall', 'key')", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, MAP_ABC)] - [InlineData(@"redis.setresp(3) -return redis.pcall('hgetall', 'key')", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, MAP_ABC)] + [InlineData(@"redis.setresp(3) return redis.pcall('hgetall', 'key')", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, MAP_ABC)] [InlineData("return true", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, 1)] [InlineData("return false", RedisProtocol.Resp2, ResultType.BulkString, ResultType.Null, null)] [InlineData("redis.setresp(3) return true", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, 1)] @@ -148,8 +147,7 @@ public async Task ConnectWithBrokenHello(string command, bool isResp3) [InlineData("return {1,2,3}", RedisProtocol.Resp3, ResultType.Array, ResultType.Array, ARR_123)] [InlineData("return nil", RedisProtocol.Resp3, ResultType.BulkString, ResultType.Null, null)] [InlineData(@"return redis.pcall('hgetall', 'key')", RedisProtocol.Resp3, ResultType.Array, ResultType.Array, MAP_ABC)] - [InlineData(@"redis.setresp(3) -return redis.pcall('hgetall', 'key')", RedisProtocol.Resp3, ResultType.Array, ResultType.Map, MAP_ABC)] + [InlineData(@"redis.setresp(3) return redis.pcall('hgetall', 'key')", RedisProtocol.Resp3, ResultType.Array, ResultType.Map, MAP_ABC)] [InlineData("return true", RedisProtocol.Resp3, ResultType.Integer, ResultType.Integer, 1)] [InlineData("return false", RedisProtocol.Resp3, ResultType.BulkString, ResultType.Null, null)] [InlineData("redis.setresp(3) return true", RedisProtocol.Resp3, ResultType.Integer, ResultType.Boolean, true)] @@ -230,23 +228,20 @@ public async Task CheckLuaResult(string script, RedisProtocol protocol, ResultTy } } - [Theory] - //[InlineData("return 42", false, ResultType.Integer, ResultType.Integer, 42)] - //[InlineData("return 'abc'", false, ResultType.BulkString, ResultType.BulkString, "abc")] - //[InlineData(@"return {1,2,3}", false, ResultType.Array, ResultType.Array, ARR_123)] - //[InlineData("return nil", false, ResultType.BulkString, ResultType.Null, null)] - //[InlineData(@"return redis.pcall('hgetall', 'key')", false, ResultType.Array, ResultType.Array, MAP_ABC)] - //[InlineData("return true", false, ResultType.Integer, ResultType.Integer, 1)] - - //[InlineData("return 42", true, ResultType.Integer, ResultType.Integer, 42)] - //[InlineData("return 'abc'", true, ResultType.BulkString, ResultType.BulkString, "abc")] - //[InlineData("return {1,2,3}", true, ResultType.Array, ResultType.Array, ARR_123)] - //[InlineData("return nil", true, ResultType.BulkString, ResultType.Null, null)] - //[InlineData(@"return redis.pcall('hgetall', 'key')", true, ResultType.Array, ResultType.Array, MAP_ABC)] - //[InlineData("return true", true, ResultType.Integer, ResultType.Integer, 1)] - - + // [InlineData("return 42", false, ResultType.Integer, ResultType.Integer, 42)] + // [InlineData("return 'abc'", false, ResultType.BulkString, ResultType.BulkString, "abc")] + // [InlineData(@"return {1,2,3}", false, ResultType.Array, ResultType.Array, ARR_123)] + // [InlineData("return nil", false, ResultType.BulkString, ResultType.Null, null)] + // [InlineData(@"return redis.pcall('hgetall', 'key')", false, ResultType.Array, ResultType.Array, MAP_ABC)] + // [InlineData("return true", false, ResultType.Integer, ResultType.Integer, 1)] + + // [InlineData("return 42", true, ResultType.Integer, ResultType.Integer, 42)] + // [InlineData("return 'abc'", true, ResultType.BulkString, ResultType.BulkString, "abc")] + // [InlineData("return {1,2,3}", true, ResultType.Array, ResultType.Array, ARR_123)] + // [InlineData("return nil", true, ResultType.BulkString, ResultType.Null, null)] + // [InlineData(@"return redis.pcall('hgetall', 'key')", true, ResultType.Array, ResultType.Array, MAP_ABC)] + // [InlineData("return true", true, ResultType.Integer, ResultType.Integer, 1)] [InlineData("incrby", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, 42, "ikey", 2)] [InlineData("incrby", RedisProtocol.Resp3, ResultType.Integer, ResultType.Integer, 42, "ikey", 2)] [InlineData("incrby", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, 2, "nkey", 2)] @@ -322,7 +317,7 @@ public async Task CheckCommandResult(string command, RedisProtocol protocol, Res { var muxer = Create(protocol: protocol); var ep = muxer.GetServerEndPoint(muxer.GetEndPoints().Single()); - if (command == "debug" && args.Length > 0 && args[0] is "protocol" && !ep.GetFeatures().Resp3 /* v6 check */ ) + if (command == "debug" && args.Length > 0 && args[0] is "protocol" && !ep.GetFeatures().Resp3 /* v6 check */) { Skip.Inconclusive("debug protocol not available"); } @@ -341,7 +336,7 @@ public async Task CheckCommandResult(string command, RedisProtocol protocol, Res await db.SetAddAsync("skey", new RedisValue[] { "a", "b", "c" }); break; case "hkey": - await db.HashSetAsync("hkey", new HashEntry[] { new("a", 1), new("b", 2), new("c",3) }); + await db.HashSetAsync("hkey", new HashEntry[] { new("a", 1), new("b", 2), new("c", 3) }); break; } } @@ -382,7 +377,7 @@ public async Task CheckCommandResult(string command, RedisProtocol protocol, Res Log(scontent); if (protocol == RedisProtocol.Resp3) { - Assert.Equal("txt", type); + Assert.Equal("txt", type); } else { @@ -418,14 +413,14 @@ public async Task CheckCommandResult(string command, RedisProtocol protocol, Res Assert.Equal(b ? 1 : 0, result.AsInt64()); break; } - - } +#pragma warning disable SA1310 // Field names should not contain underscore private const string SET_ABC = nameof(SET_ABC); private const string ARR_123 = nameof(ARR_123); private const string MAP_ABC = nameof(MAP_ABC); private const string EMPTY_ARR = nameof(EMPTY_ARR); private const string STR_DAVE = nameof(STR_DAVE); private const string ANY = nameof(ANY); +#pragma warning restore SA1310 // Field names should not contain underscore } diff --git a/tests/StackExchange.Redis.Tests/SSDBTests.cs b/tests/StackExchange.Redis.Tests/SSDBTests.cs index 6f3348892..b05a781b2 100644 --- a/tests/StackExchange.Redis.Tests/SSDBTests.cs +++ b/tests/StackExchange.Redis.Tests/SSDBTests.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis.Tests; public class SSDBTests : TestBase { - public SSDBTests(ITestOutputHelper output) : base (output) { } + public SSDBTests(ITestOutputHelper output) : base(output) { } [Fact] public void ConnectToSSDB() @@ -15,7 +15,7 @@ public void ConnectToSSDB() using var conn = ConnectionMultiplexer.Connect(new ConfigurationOptions { EndPoints = { { TestConfig.Current.SSDBServer, TestConfig.Current.SSDBPort } }, - CommandMap = CommandMap.SSDB + CommandMap = CommandMap.SSDB, }); RedisKey key = Me(); diff --git a/tests/StackExchange.Redis.Tests/SSLTests.cs b/tests/StackExchange.Redis.Tests/SSLTests.cs index 87e589b97..2a261fe1a 100644 --- a/tests/StackExchange.Redis.Tests/SSLTests.cs +++ b/tests/StackExchange.Redis.Tests/SSLTests.cs @@ -23,10 +23,9 @@ public class SSLTests : TestBase, IClassFixture public SSLTests(ITestOutputHelper output, SSLServerFixture fixture) : base(output) => Fixture = fixture; - [Theory] + [Theory] // (note the 6379 port is closed) [InlineData(null, true)] // auto-infer port (but specify 6380) [InlineData(6380, true)] // all explicit - // (note the 6379 port is closed) public void ConnectToAzure(int? port, bool ssl) { Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); @@ -82,7 +81,7 @@ public async Task ConnectToSSLServer(bool useSsl, bool specifyHost) }; var map = new Dictionary { - ["config"] = null // don't rely on config working + ["config"] = null, // don't rely on config working }; if (!isAzure) map["cluster"] = null; config.CommandMap = CommandMap.Create(map); @@ -152,18 +151,14 @@ public async Task ConnectToSSLServer(bool useSsl, bool specifyHost) long value = (long)await db.StringGetAsync(key).ForAwait(); watch.Stop(); Assert.Equal(AsyncLoop, value); - Log("F&F: {0} INCR, {1:###,##0}ms, {2} ops/s; final value: {3}", - AsyncLoop, - watch.ElapsedMilliseconds, - (long)(AsyncLoop / watch.Elapsed.TotalSeconds), - value); + Log($"F&F: {AsyncLoop} INCR, {watch.ElapsedMilliseconds:###,##0}ms, {(long)(AsyncLoop / watch.Elapsed.TotalSeconds)} ops/s; final value: {value}"); // perf: sync/multi-threaded // TestConcurrent(db, key, 30, 10); - //TestConcurrent(db, key, 30, 20); - //TestConcurrent(db, key, 30, 30); - //TestConcurrent(db, key, 30, 40); - //TestConcurrent(db, key, 30, 50); + // TestConcurrent(db, key, 30, 20); + // TestConcurrent(db, key, 30, 30); + // TestConcurrent(db, key, 30, 40); + // TestConcurrent(db, key, 30, 50); } else { @@ -176,8 +171,8 @@ public async Task ConnectToSSLServer(bool useSsl, bool specifyHost) // Docker configured with only TLS_AES_256_GCM_SHA384 for testing [Theory] [InlineData(SslProtocols.None, true, TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TlsCipherSuite.TLS_AES_256_GCM_SHA384)] - [InlineData(SslProtocols.Tls12 , true, TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TlsCipherSuite.TLS_AES_256_GCM_SHA384)] - [InlineData(SslProtocols.Tls13 , true, TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TlsCipherSuite.TLS_AES_256_GCM_SHA384)] + [InlineData(SslProtocols.Tls12, true, TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TlsCipherSuite.TLS_AES_256_GCM_SHA384)] + [InlineData(SslProtocols.Tls13, true, TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TlsCipherSuite.TLS_AES_256_GCM_SHA384)] [InlineData(SslProtocols.Tls12, false, TlsCipherSuite.TLS_AES_128_CCM_8_SHA256)] [InlineData(SslProtocols.Tls12, true)] [InlineData(SslProtocols.Tls13, true)] @@ -209,8 +204,8 @@ public async Task ConnectSslClientAuthenticationOptions(SslProtocols protocols, Log(" Errors: " + errors); Log(" Cert issued to: " + cert?.Subject); return true; - } - } + }, + }, }; try @@ -258,9 +253,14 @@ public void RedisLabsSSL() EndPoints = { { TestConfig.Current.RedisLabsSslServer, TestConfig.Current.RedisLabsSslPort } }, ConnectTimeout = timeout, AllowAdmin = true, - CommandMap = CommandMap.Create(new HashSet { - "subscribe", "unsubscribe", "cluster" - }, false) + CommandMap = CommandMap.Create( + new HashSet + { + "subscribe", + "unsubscribe", + "cluster", + }, + false), }; options.TrustIssuer("redislabs_ca.pem"); @@ -270,10 +270,7 @@ public void RedisLabsSSL() ConnectionMultiplexer.EchoPath = Me(); #endif options.Ssl = true; - options.CertificateSelection += delegate - { - return cert; - }; + options.CertificateSelection += (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => cert; using var conn = ConnectionMultiplexer.Connect(options); @@ -320,9 +317,14 @@ public void RedisLabsEnvironmentVariableClientCertificate(bool setEnv) EndPoints = { { TestConfig.Current.RedisLabsSslServer, TestConfig.Current.RedisLabsSslPort } }, ConnectTimeout = timeout, AllowAdmin = true, - CommandMap = CommandMap.Create(new HashSet { - "subscribe", "unsubscribe", "cluster" - }, false) + CommandMap = CommandMap.Create( + new HashSet + { + "subscribe", + "unsubscribe", + "cluster", + }, + false), }; if (!Directory.Exists(Me())) Directory.CreateDirectory(Me()); @@ -366,19 +368,18 @@ public void SSLHostInferredFromEndpoints() { var options = new ConfigurationOptions { - EndPoints = { - { "mycache.rediscache.windows.net", 15000}, - { "mycache.rediscache.windows.net", 15001 }, - { "mycache.rediscache.windows.net", 15002 }, - }, + EndPoints = + { + { "mycache.rediscache.windows.net", 15000 }, + { "mycache.rediscache.windows.net", 15001 }, + { "mycache.rediscache.windows.net", 15002 }, + }, Ssl = true, }; Assert.True(options.SslHost == "mycache.rediscache.windows.net"); options = new ConfigurationOptions() { - EndPoints = { - { "121.23.23.45", 15000}, - } + EndPoints = { { "121.23.23.45", 15000 } }, }; Assert.True(options.SslHost == null); } @@ -453,7 +454,7 @@ public void SSLParseViaConfig_Issue883_ConfigObject() SyncTimeout = 5000, DefaultDatabase = 0, EndPoints = { { TestConfig.Current.AzureCacheServer, 6380 } }, - Password = TestConfig.Current.AzureCachePassword + Password = TestConfig.Current.AzureCachePassword, }; options.CertificateValidation += ShowCertFailures(Writer); @@ -538,7 +539,7 @@ public void ConfigObject_Issue1407_ToStringIncludesSslProtocols() SyncTimeout = 5000, DefaultDatabase = 0, EndPoints = { { "endpoint.test", 6380 } }, - Password = "123456" + Password = "123456", }; var targetOptions = ConfigurationOptions.Parse(sourceOptions.ToString()); diff --git a/tests/StackExchange.Redis.Tests/SanityCheckTests.cs b/tests/StackExchange.Redis.Tests/SanityCheckTests.cs index e1a2de977..353098fd5 100644 --- a/tests/StackExchange.Redis.Tests/SanityCheckTests.cs +++ b/tests/StackExchange.Redis.Tests/SanityCheckTests.cs @@ -1,7 +1,7 @@ using System; using System.IO; -using System.Reflection.PortableExecutable; using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; using Xunit; namespace StackExchange.Redis.Tests; @@ -9,11 +9,11 @@ namespace StackExchange.Redis.Tests; public sealed class SanityChecks { /// - /// Ensure we don't reference System.ValueTuple as it causes issues with .NET Full Framework + /// Ensure we don't reference System.ValueTuple as it causes issues with .NET Full Framework. /// /// /// Modified from . - /// Thanks Lucas Trzesniewski! + /// Thanks Lucas Trzesniewski!. /// [Fact] public void ValueTupleNotReferenced() diff --git a/tests/StackExchange.Redis.Tests/ScanTests.cs b/tests/StackExchange.Redis.Tests/ScanTests.cs index bcab2da4c..4524aed9f 100644 --- a/tests/StackExchange.Redis.Tests/ScanTests.cs +++ b/tests/StackExchange.Redis.Tests/ScanTests.cs @@ -3,7 +3,6 @@ using System.Linq; using Xunit; using Xunit.Abstractions; -// ReSharper disable PossibleMultipleEnumeration namespace StackExchange.Redis.Tests; @@ -146,7 +145,6 @@ public void ScanResume() // page size, with zero guarantees; in this particular test, the first page actually has 19 elements, for example. So: we cannot // make the following assertion: // Assert.Equal(12, snapOffset); - seq = server.Keys(dbId, prefix + ":*", pageSize: 15, cursor: snapCursor, pageOffset: snapOffset); var seqCur = (IScanningCursor)seq; Assert.Equal(snapCursor, seqCur.Cursor); @@ -257,7 +255,7 @@ public void SortedSetScan(bool supported) Assert.Equal(2, basicArr[1].Score); Assert.Equal(3, basicArr[2].Score); basic = basicArr.ToDictionary(); - Assert.Equal(3, basic.Count); //asc + Assert.Equal(3, basic.Count); // asc Assert.Equal(1, basic["a"]); Assert.Equal(2, basic["b"]); Assert.Equal(3, basic["c"]); @@ -364,7 +362,7 @@ private static bool GotCursors(IConnectionMultiplexer conn, RedisKey key, int co var found = false; var response = db.HashScan(key); - var cursor = ((IScanningCursor)response); + var cursor = (IScanningCursor)response; foreach (var _ in response) { if (cursor.Cursor > 0) diff --git a/tests/StackExchange.Redis.Tests/ScriptingTests.cs b/tests/StackExchange.Redis.Tests/ScriptingTests.cs index 35bfbf36d..bbacb96bd 100644 --- a/tests/StackExchange.Redis.Tests/ScriptingTests.cs +++ b/tests/StackExchange.Redis.Tests/ScriptingTests.cs @@ -36,10 +36,14 @@ public async Task BasicScripting() using var conn = GetScriptConn(); var db = conn.GetDatabase(); - var noCache = db.ScriptEvaluateAsync("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", - new RedisKey[] { "key1", "key2" }, new RedisValue[] { "first", "second" }); - var cache = db.ScriptEvaluateAsync("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", - new RedisKey[] { "key1", "key2" }, new RedisValue[] { "first", "second" }); + var noCache = db.ScriptEvaluateAsync( + "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", + new RedisKey[] { "key1", "key2" }, + new RedisValue[] { "first", "second" }); + var cache = db.ScriptEvaluateAsync( + "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", + new RedisKey[] { "key1", "key2" }, + new RedisValue[] { "first", "second" }); var results = (string[]?)await noCache; Assert.NotNull(results); Assert.Equal(4, results.Length); @@ -144,8 +148,8 @@ public async Task MultiIncrByWithoutReplies() db.StringIncrement(prefix + "c", flags: CommandFlags.FireAndForget); db.StringIncrement(prefix + "c", flags: CommandFlags.FireAndForget); - //run the script, passing "a", "b", "c" and 1,2,3 - // increment a &b by 1, c twice + // run the script, passing "a", "b", "c" and 1,2,3 + // increment a & b by 1, c twice var result = db.ScriptEvaluateAsync( "for i,key in ipairs(KEYS) do redis.call('incrby', key, ARGV[i]) end", new RedisKey[] { prefix + "a", prefix + "b", prefix + "c" }, // <== aka "KEYS" in the script @@ -190,7 +194,7 @@ public void FlushDetection() // now cause all kinds of problems GetServer(conn).ScriptFlush(); - //expect this one to fail just work fine (self-fix) + // expect this one to fail just work fine (self-fix) db.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); result = (string?)db.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); @@ -210,7 +214,7 @@ public void PrepareScript() server.ScriptLoad(scripts[0]); server.ScriptLoad(scripts[1]); - //when known to exist + // when known to exist server.ScriptLoad(scripts[0]); server.ScriptLoad(scripts[1]); } @@ -218,15 +222,15 @@ public void PrepareScript() { var server = GetServer(conn); - //when vanilla + // when vanilla server.ScriptLoad(scripts[0]); server.ScriptLoad(scripts[1]); - //when known to exist + // when known to exist server.ScriptLoad(scripts[0]); server.ScriptLoad(scripts[1]); - //when known to exist + // when known to exist server.ScriptLoad(scripts[0]); server.ScriptLoad(scripts[1]); } @@ -317,8 +321,11 @@ public async Task ChangeDbInScript() Log("Key: " + key); var db = conn.GetDatabase(2); - var evalResult = db.ScriptEvaluateAsync(@"redis.call('select', 1) - return redis.call('get','" + key + "')", null, null); + var evalResult = db.ScriptEvaluateAsync( + @"redis.call('select', 1) + return redis.call('get','" + key + "')", + null, + null); var getResult = db.StringGetAsync(key); Assert.Equal("db 1", (string?)await evalResult); @@ -337,8 +344,11 @@ public async Task ChangeDbInTranScript() var db = conn.GetDatabase(2); var tran = db.CreateTransaction(); - var evalResult = tran.ScriptEvaluateAsync(@"redis.call('select', 1) - return redis.call('get','" + key + "')", null, null); + var evalResult = tran.ScriptEvaluateAsync( + @"redis.call('select', 1) + return redis.call('get','" + key + "')", + null, + null); var getResult = tran.StringGetAsync(key); Assert.True(tran.Execute()); @@ -358,13 +368,17 @@ public void TestBasicScripting() db.KeyDelete(key, CommandFlags.FireAndForget); db.HashSet(key, "id", 123, flags: CommandFlags.FireAndForget); - var wasSet = (bool)db.ScriptEvaluate("if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", - new[] { key }, new[] { newId }); + var wasSet = (bool)db.ScriptEvaluate( + "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", + new[] { key }, + new[] { newId }); Assert.True(wasSet); - wasSet = (bool)db.ScriptEvaluate("if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", - new[] { key }, new[] { newId }); + wasSet = (bool)db.ScriptEvaluate( + "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", + new[] { key }, + new[] { newId }); Assert.False(wasSet); } @@ -460,9 +474,7 @@ public void CompareScriptToDirect() Assert.Equal(LOOP, (long)scriptResult); Assert.Equal(LOOP, directResult); - Log("script: {0}ms; direct: {1}ms", - scriptTime.TotalMilliseconds, - directTime.TotalMilliseconds); + Log("script: {0}ms; direct: {1}ms", scriptTime.TotalMilliseconds, directTime.TotalMilliseconds); } [Fact] @@ -505,6 +517,7 @@ public void SimpleLuaScript() var db = conn.GetDatabase(); + // Scopes for repeated use { var val = prepared.Evaluate(db, new { ident = "hello" }); Assert.Equal("hello", (string?)val); @@ -556,6 +569,7 @@ public void SimpleRawScriptEvaluate() var db = conn.GetDatabase(); + // Scopes for repeated use { var val = db.ScriptEvaluate(Script, values: new RedisValue[] { "hello" }); Assert.Equal("hello", (string?)val); @@ -671,6 +685,7 @@ public void SimpleLoadedLuaScript() var db = conn.GetDatabase(); + // Scopes for repeated use { var val = loaded.Evaluate(db, new { ident = "hello" }); Assert.Equal("hello", (string?)val); @@ -836,7 +851,7 @@ public void LuaScriptPrefixedKeys() Assert.Equal("prefix-" + key, keys[0]); Assert.NotNull(args); Assert.Equal(2, args.Length); - Assert.Equal("prefix-" + key, args[0]); + Assert.Equal("prefix-" + key, args[0]); Assert.Equal("hello", args[1]); } diff --git a/tests/StackExchange.Redis.Tests/SecureTests.cs b/tests/StackExchange.Redis.Tests/SecureTests.cs index 9763cc15f..35e9dd580 100644 --- a/tests/StackExchange.Redis.Tests/SecureTests.cs +++ b/tests/StackExchange.Redis.Tests/SecureTests.cs @@ -11,7 +11,7 @@ public class SecureTests : TestBase protected override string GetConfiguration() => TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword + ",name=MyClient"; - public SecureTests(ITestOutputHelper output) : base (output) { } + public SecureTests(ITestOutputHelper output) : base(output) { } [Fact] public void MassiveBulkOpsFireAndForgetSecure() @@ -31,8 +31,7 @@ public void MassiveBulkOpsFireAndForgetSecure() int val = (int)db.StringGet(key); Assert.Equal(AsyncOpsQty, val); watch.Stop(); - Log("{2}: Time for {0} ops: {1}ms (any order); ops/s: {3}", AsyncOpsQty, watch.ElapsedMilliseconds, Me(), - AsyncOpsQty / watch.Elapsed.TotalSeconds); + Log("{2}: Time for {0} ops: {1}ms (any order); ops/s: {3}", AsyncOpsQty, watch.ElapsedMilliseconds, Me(), AsyncOpsQty / watch.Elapsed.TotalSeconds); } [Fact] diff --git a/tests/StackExchange.Redis.Tests/SentinelTests.cs b/tests/StackExchange.Redis.Tests/SentinelTests.cs index 49e96a82d..3fc2afd4e 100644 --- a/tests/StackExchange.Redis.Tests/SentinelTests.cs +++ b/tests/StackExchange.Redis.Tests/SentinelTests.cs @@ -95,8 +95,7 @@ public void SentinelConnectTest() var db = conn.GetDatabase(); var test = db.Ping(); - Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, - TestConfig.Current.SentinelPortA, test.TotalMilliseconds); + Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA, test.TotalMilliseconds); } [Fact] @@ -116,8 +115,7 @@ public void SentinelRepeatConnectTest() var db = conn.GetDatabase(); var test = db.Ping(); - Log("ping to 1st sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, - TestConfig.Current.SentinelPortA, test.TotalMilliseconds); + Log("ping to 1st sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA, test.TotalMilliseconds); Log("Service Name: " + options.ServiceName); foreach (var ep in options.EndPoints) @@ -129,8 +127,7 @@ public void SentinelRepeatConnectTest() var db2 = conn2.GetDatabase(); var test2 = db2.Ping(); - Log("ping to 2nd sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, - TestConfig.Current.SentinelPortA, test2.TotalMilliseconds); + Log("ping to 2nd sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA, test2.TotalMilliseconds); } [Fact] @@ -142,8 +139,7 @@ public async Task SentinelConnectAsyncTest() var db = conn.GetDatabase(); var test = await db.PingAsync(); - Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, - TestConfig.Current.SentinelPortA, test.TotalMilliseconds); + Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA, test.TotalMilliseconds); } [Fact] @@ -163,14 +159,11 @@ public void SentinelRole() public void PingTest() { var test = SentinelServerA.Ping(); - Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, - TestConfig.Current.SentinelPortA, test.TotalMilliseconds); + Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA, test.TotalMilliseconds); test = SentinelServerB.Ping(); - Log("ping to sentinel {0}:{1} took {1} ms", TestConfig.Current.SentinelServer, - TestConfig.Current.SentinelPortB, test.TotalMilliseconds); + Log("ping to sentinel {0}:{1} took {1} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortB, test.TotalMilliseconds); test = SentinelServerC.Ping(); - Log("ping to sentinel {0}:{1} took {1} ms", TestConfig.Current.SentinelServer, - TestConfig.Current.SentinelPortC, test.TotalMilliseconds); + Log("ping to sentinel {0}:{1} took {1} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortC, test.TotalMilliseconds); } [Fact] @@ -260,9 +253,10 @@ public void SentinelSentinelsTest() { var sentinels = SentinelServerA.SentinelSentinels(ServiceName); - var expected = new List { + var expected = new List + { SentinelServerB.EndPoint.ToString(), - SentinelServerC.EndPoint.ToString() + SentinelServerC.EndPoint.ToString(), }; var actual = new List(); @@ -282,9 +276,11 @@ public void SentinelSentinelsTest() var data = kv.ToDictionary(); actual.Add(data["ip"] + ":" + data["port"]); } - expected = new List { + + expected = new List + { SentinelServerA.EndPoint.ToString(), - SentinelServerC.EndPoint.ToString() + SentinelServerC.EndPoint.ToString(), }; Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerB.EndPoint.ToString())); @@ -297,9 +293,11 @@ public void SentinelSentinelsTest() var data = kv.ToDictionary(); actual.Add(data["ip"] + ":" + data["port"]); } - expected = new List { + + expected = new List + { SentinelServerA.EndPoint.ToString(), - SentinelServerB.EndPoint.ToString() + SentinelServerB.EndPoint.ToString(), }; Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerC.EndPoint.ToString())); @@ -311,9 +309,10 @@ public void SentinelSentinelsTest() public async Task SentinelSentinelsAsyncTest() { var sentinels = await SentinelServerA.SentinelSentinelsAsync(ServiceName).ForAwait(); - var expected = new List { + var expected = new List + { SentinelServerB.EndPoint.ToString(), - SentinelServerC.EndPoint.ToString() + SentinelServerC.EndPoint.ToString(), }; var actual = new List(); @@ -322,15 +321,17 @@ public async Task SentinelSentinelsAsyncTest() var data = kv.ToDictionary(); actual.Add(data["ip"] + ":" + data["port"]); } + Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerA.EndPoint.ToString())); Assert.True(sentinels.Length == 2); Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); sentinels = await SentinelServerB.SentinelSentinelsAsync(ServiceName).ForAwait(); - expected = new List { + expected = new List + { SentinelServerA.EndPoint.ToString(), - SentinelServerC.EndPoint.ToString() + SentinelServerC.EndPoint.ToString(), }; actual = new List(); @@ -339,14 +340,16 @@ public async Task SentinelSentinelsAsyncTest() var data = kv.ToDictionary(); actual.Add(data["ip"] + ":" + data["port"]); } + Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerB.EndPoint.ToString())); Assert.True(sentinels.Length == 2); Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); sentinels = await SentinelServerC.SentinelSentinelsAsync(ServiceName).ForAwait(); - expected = new List { + expected = new List + { SentinelServerA.EndPoint.ToString(), - SentinelServerB.EndPoint.ToString() + SentinelServerB.EndPoint.ToString(), }; actual = new List(); foreach (var kv in sentinels) @@ -354,6 +357,7 @@ public async Task SentinelSentinelsAsyncTest() var data = kv.ToDictionary(); actual.Add(data["ip"] + ":" + data["port"]); } + Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerC.EndPoint.ToString())); Assert.True(sentinels.Length == 2); Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); @@ -470,7 +474,5 @@ public async Task ReadOnlyConnectionReplicasTest() var db = readonlyConn.GetDatabase(); var s = db.StringGet("test"); Assert.True(s.IsNullOrEmpty); - //var ex = Assert.Throws(() => db.StringSet("test", "try write to read only instance")); - //Assert.StartsWith("No connection is available to service this operation", ex.Message); } } diff --git a/tests/StackExchange.Redis.Tests/SetTests.cs b/tests/StackExchange.Redis.Tests/SetTests.cs index d90e4a8c3..22df40be7 100644 --- a/tests/StackExchange.Redis.Tests/SetTests.cs +++ b/tests/StackExchange.Redis.Tests/SetTests.cs @@ -90,9 +90,9 @@ public void SetIntersectionLength() db.KeyDelete(key2, CommandFlags.FireAndForget); db.SetAdd(key2, new RedisValue[] { 1, 2, 3, 4, 5 }, CommandFlags.FireAndForget); - Assert.Equal(4, db.SetIntersectionLength(new RedisKey[]{ key1, key2})); + Assert.Equal(4, db.SetIntersectionLength(new RedisKey[] { key1, key2 })); // with limit - Assert.Equal(3, db.SetIntersectionLength(new RedisKey[]{ key1, key2}, 3)); + Assert.Equal(3, db.SetIntersectionLength(new RedisKey[] { key1, key2 }, 3)); // Missing keys should be 0 var key3 = Me() + "3"; @@ -116,9 +116,9 @@ public async Task SetIntersectionLengthAsync() db.KeyDelete(key2, CommandFlags.FireAndForget); db.SetAdd(key2, new RedisValue[] { 1, 2, 3, 4, 5 }, CommandFlags.FireAndForget); - Assert.Equal(4, await db.SetIntersectionLengthAsync(new RedisKey[]{ key1, key2})); + Assert.Equal(4, await db.SetIntersectionLengthAsync(new RedisKey[] { key1, key2 })); // with limit - Assert.Equal(3, await db.SetIntersectionLengthAsync(new RedisKey[]{ key1, key2}, 3)); + Assert.Equal(3, await db.SetIntersectionLengthAsync(new RedisKey[] { key1, key2 }, 3)); // Missing keys should be 0 var key3 = Me() + "3"; @@ -356,10 +356,10 @@ public async Task TestSortReadonlyPrimary() var random = new Random(); var items = Enumerable.Repeat(0, 200).Select(_ => random.Next()).ToList(); - await db.SetAddAsync(key, items.Select(x=>(RedisValue)x).ToArray()); + await db.SetAddAsync(key, items.Select(x => (RedisValue)x).ToArray()); items.Sort(); - var result = db.Sort(key).Select(x=>(int)x); + var result = db.Sort(key).Select(x => (int)x); Assert.Equal(items, result); result = (await db.SortAsync(key)).Select(x => (int)x); @@ -377,7 +377,7 @@ public async Task TestSortReadonlyReplica() var random = new Random(); var items = Enumerable.Repeat(0, 200).Select(_ => random.Next()).ToList(); - await db.SetAddAsync(key, items.Select(x=>(RedisValue)x).ToArray()); + await db.SetAddAsync(key, items.Select(x => (RedisValue)x).ToArray()); using var readonlyConn = Create(configuration: TestConfig.Current.ReplicaServerAndPort, require: RedisFeatures.v7_0_0_rc1); var readonlyDb = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/SocketTests.cs b/tests/StackExchange.Redis.Tests/SocketTests.cs index 10723d7d0..71a1ffe47 100644 --- a/tests/StackExchange.Redis.Tests/SocketTests.cs +++ b/tests/StackExchange.Redis.Tests/SocketTests.cs @@ -6,7 +6,7 @@ namespace StackExchange.Redis.Tests; public class SocketTests : TestBase { protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public SocketTests(ITestOutputHelper output) : base (output) { } + public SocketTests(ITestOutputHelper output) : base(output) { } [FactLongRunning] public void CheckForSocketLeaks() diff --git a/tests/StackExchange.Redis.Tests/SortedSetTests.cs b/tests/StackExchange.Redis.Tests/SortedSetTests.cs index 49c464142..9bcd02e2e 100644 --- a/tests/StackExchange.Redis.Tests/SortedSetTests.cs +++ b/tests/StackExchange.Redis.Tests/SortedSetTests.cs @@ -22,7 +22,7 @@ public SortedSetTests(ITestOutputHelper output, SharedConnectionFixture fixture) new SortedSetEntry("g", 7), new SortedSetEntry("h", 8), new SortedSetEntry("i", 9), - new SortedSetEntry("j", 10) + new SortedSetEntry("j", 10), }; private static readonly SortedSetEntry[] entriesPow2 = new SortedSetEntry[] @@ -36,7 +36,7 @@ public SortedSetTests(ITestOutputHelper output, SharedConnectionFixture fixture) new SortedSetEntry("g", 64), new SortedSetEntry("h", 128), new SortedSetEntry("i", 256), - new SortedSetEntry("j", 512) + new SortedSetEntry("j", 512), }; private static readonly SortedSetEntry[] entriesPow3 = new SortedSetEntry[] @@ -59,7 +59,7 @@ public SortedSetTests(ITestOutputHelper output, SharedConnectionFixture fixture) new SortedSetEntry("g", 0), new SortedSetEntry("h", 0), new SortedSetEntry("i", 0), - new SortedSetEntry("j", 0) + new SortedSetEntry("j", 0), }; [Fact] @@ -1051,7 +1051,10 @@ public void SortedSetMultiPopSingleKey() var key = Me(); db.KeyDelete(key); - db.SortedSetAdd(key, new SortedSetEntry[] { + db.SortedSetAdd( + key, + new SortedSetEntry[] + { new SortedSetEntry("rays", 100), new SortedSetEntry("yankees", 92), new SortedSetEntry("red sox", 92), @@ -1085,7 +1088,10 @@ public void SortedSetMultiPopMultiKey() var key = Me(); db.KeyDelete(key); - db.SortedSetAdd(key, new SortedSetEntry[] { + db.SortedSetAdd( + key, + new SortedSetEntry[] + { new SortedSetEntry("rays", 100), new SortedSetEntry("yankees", 92), new SortedSetEntry("red sox", 92), @@ -1143,7 +1149,10 @@ public async Task SortedSetMultiPopAsync() var key = Me(); db.KeyDelete(key); - db.SortedSetAdd(key, new SortedSetEntry[] { + db.SortedSetAdd( + key, + new SortedSetEntry[] + { new SortedSetEntry("rays", 100), new SortedSetEntry("yankees", 92), new SortedSetEntry("red sox", 92), @@ -1451,7 +1460,7 @@ public async Task SortedSetUpdate() var db = conn.GetDatabase(); var key = Me(); var member = "a"; - var values = new SortedSetEntry[] {new SortedSetEntry(member, 5)}; + var values = new SortedSetEntry[] { new SortedSetEntry(member, 5) }; db.KeyDelete(key, CommandFlags.FireAndForget); db.SortedSetAdd(key, member, 2); @@ -1459,6 +1468,6 @@ public async Task SortedSetUpdate() Assert.Equal(1, db.SortedSetUpdate(key, values)); Assert.True(await db.SortedSetUpdateAsync(key, member, 1)); - Assert.Equal(1,await db.SortedSetUpdateAsync(key, values)); + Assert.Equal(1, await db.SortedSetUpdateAsync(key, values)); } } diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index fefff3b02..0ea744848 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -96,9 +96,9 @@ public void StreamAddMultipleValuePairsWithManualId() var fields = new[] { - new NameValueEntry("field1", "value1"), - new NameValueEntry("field2", "value2") - }; + new NameValueEntry("field1", "value1"), + new NameValueEntry("field2", "value2"), + }; var messageId = db.StreamAdd(key, fields, id); var entries = db.StreamRange(key); @@ -754,12 +754,15 @@ public void StreamConsumerGroupClaimMessages() // Claim the 3 messages consumed by consumer2 for consumer1. // Get the pending messages for consumer2. - var pendingMessages = db.StreamPendingMessages(key, groupName, + var pendingMessages = db.StreamPendingMessages( + key, + groupName, 10, consumer2); // Claim the messages for consumer1. - var messages = db.StreamClaim(key, + var messages = db.StreamClaim( + key, groupName, consumer1, 0, // Min message idle time @@ -801,12 +804,15 @@ public void StreamConsumerGroupClaimMessagesReturningIds() // Claim the 3 messages consumed by consumer2 for consumer1. // Get the pending messages for consumer2. - var pendingMessages = db.StreamPendingMessages(key, groupName, + var pendingMessages = db.StreamPendingMessages( + key, + groupName, 10, consumer2); // Claim the messages for consumer1. - var messageIds = db.StreamClaimIdsOnly(key, + var messageIds = db.StreamClaimIdsOnly( + key, groupName, consumer1, 0, // Min message idle time @@ -827,7 +833,6 @@ public void StreamConsumerGroupReadMultipleOneReadBeginningOneReadNew() // Ask redis to read from the beginning of both stream, expect messages // for only the stream set to read from the beginning. - using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); @@ -851,10 +856,10 @@ public void StreamConsumerGroupReadMultipleOneReadBeginningOneReadNew() // Read for both streams from the beginning. We shouldn't get anything back for stream1. var pairs = new[] { - // StreamPosition.NewMessages will send ">" which indicates "Undelivered" messages. - new StreamPosition(stream1, StreamPosition.NewMessages), - new StreamPosition(stream2, StreamPosition.NewMessages) - }; + // StreamPosition.NewMessages will send ">" which indicates "Undelivered" messages. + new StreamPosition(stream1, StreamPosition.NewMessages), + new StreamPosition(stream2, StreamPosition.NewMessages), + }; var streams = db.StreamReadGroup(pairs, groupName, "test_consumer"); @@ -884,9 +889,9 @@ public void StreamConsumerGroupReadMultipleOnlyNewMessagesExpectNoResult() // We shouldn't get anything for either stream. var pairs = new[] { - new StreamPosition(stream1, StreamPosition.Beginning), - new StreamPosition(stream2, StreamPosition.Beginning) - }; + new StreamPosition(stream1, StreamPosition.Beginning), + new StreamPosition(stream2, StreamPosition.Beginning), + }; var streams = db.StreamReadGroup(pairs, groupName, "test_consumer"); @@ -921,9 +926,9 @@ public void StreamConsumerGroupReadMultipleOnlyNewMessagesExpect1Result() // Read the new messages (messages created after the group was created). var pairs = new[] { - new StreamPosition(stream1, StreamPosition.NewMessages), - new StreamPosition(stream2, StreamPosition.NewMessages) - }; + new StreamPosition(stream1, StreamPosition.NewMessages), + new StreamPosition(stream2, StreamPosition.NewMessages), + }; var streams = db.StreamReadGroup(pairs, groupName, "test_consumer"); @@ -958,10 +963,10 @@ public void StreamConsumerGroupReadMultipleRestrictCount() var pairs = new[] { - // Read after the first id in both streams - new StreamPosition(stream1, StreamPosition.NewMessages), - new StreamPosition(stream2, StreamPosition.NewMessages) - }; + // Read after the first id in both streams + new StreamPosition(stream1, StreamPosition.NewMessages), + new StreamPosition(stream2, StreamPosition.NewMessages), + }; // Restrict the count to 2 (expect only 1 message from first stream, 2 from the second). var streams = db.StreamReadGroup(pairs, groupName, "test_consumer", 2); @@ -1008,7 +1013,8 @@ public void StreamConsumerGroupViewPendingInfoWhenNothingPending() db.StreamCreateConsumerGroup(key, groupName, "0-0"); - var pendingMessages = db.StreamPendingMessages(key, + var pendingMessages = db.StreamPendingMessages( + key, groupName, 10, consumerName: RedisValue.Null); @@ -1117,7 +1123,8 @@ public void StreamConsumerGroupViewPendingMessageInfoForConsumer() db.StreamReadGroup(key, groupName, consumer2); // Get the pending info about the messages themselves. - var pendingMessageInfoList = db.StreamPendingMessages(key, + var pendingMessageInfoList = db.StreamPendingMessages( + key, groupName, 10, consumer2); @@ -1350,7 +1357,6 @@ public void StreamInfoGetWithEmptyStream() // Add an entry and then delete it so the stream is empty, then run streaminfo // to ensure it functions properly on an empty stream. Namely, the first-entry // and last-entry messages should be null. - var id = db.StreamAdd(key, "field1", "value1"); db.StreamDelete(key, new[] { id }); @@ -1538,7 +1544,7 @@ public void StreamReadExpectedExceptionInvalidCountMultipleStream() var streamPositions = new[] { new StreamPosition("key1", "0-0"), - new StreamPosition("key2", "0-0") + new StreamPosition("key2", "0-0"), }; Assert.Throws(() => db.StreamRead(streamPositions, 0)); } @@ -1589,9 +1595,9 @@ public void StreamReadMultipleStreams() // Read from both streams at the same time. var streamList = new[] { - new StreamPosition(key1, "0-0"), - new StreamPosition(key2, "0-0") - }; + new StreamPosition(key1, "0-0"), + new StreamPosition(key2, "0-0"), + }; var streams = db.StreamRead(streamList); @@ -1650,7 +1656,6 @@ public void StreamReadMultipleStreamsLastMessage() Assert.Equal(new[] { new NameValueEntry("field8", "value8") }, stream2.Entries[0].Values); } - [Fact] public void StreamReadMultipleStreamsWithCount() { @@ -1667,9 +1672,9 @@ public void StreamReadMultipleStreamsWithCount() var streamList = new[] { - new StreamPosition(key1, "0-0"), - new StreamPosition(key2, "0-0") - }; + new StreamPosition(key1, "0-0"), + new StreamPosition(key2, "0-0"), + }; var streams = db.StreamRead(streamList, countPerStream: 1); @@ -1701,11 +1706,11 @@ public void StreamReadMultipleStreamsWithReadPastSecondStream() var streamList = new[] { - new StreamPosition(key1, "0-0"), + new StreamPosition(key1, "0-0"), - // read past the end of stream # 2 - new StreamPosition(key2, id4) - }; + // read past the end of stream # 2 + new StreamPosition(key2, id4), + }; var streams = db.StreamRead(streamList); @@ -1732,10 +1737,10 @@ public void StreamReadMultipleStreamsWithEmptyResponse() var streamList = new[] { - // Read past the end of both streams. - new StreamPosition(key1, id2), - new StreamPosition(key2, id4) - }; + // Read past the end of both streams. + new StreamPosition(key1, id2), + new StreamPosition(key2, id4), + }; var streams = db.StreamRead(streamList); @@ -1755,7 +1760,6 @@ public void StreamReadPastEndOfStream() var id2 = db.StreamAdd(key, "field2", "value2"); // Read after the final ID in the stream, we expect an empty array as a response. - var entries = db.StreamRead(key, id2); Assert.Empty(entries); @@ -1964,7 +1968,8 @@ public void StreamReadGroupWithNoAckShowsNoPendingMessages() db.StreamCreateConsumerGroup(key, groupName, StreamPosition.NewMessages); - db.StreamReadGroup(key, + db.StreamReadGroup( + key, groupName, consumer, StreamPosition.NewMessages, @@ -1995,10 +2000,11 @@ public void StreamReadGroupMultiStreamWithNoAckShowsNoPendingMessages() db.StreamCreateConsumerGroup(key1, groupName, StreamPosition.NewMessages); db.StreamCreateConsumerGroup(key2, groupName, StreamPosition.NewMessages); - db.StreamReadGroup(new[] + db.StreamReadGroup( + new[] { new StreamPosition(key1, StreamPosition.NewMessages), - new StreamPosition(key2, StreamPosition.NewMessages) + new StreamPosition(key2, StreamPosition.NewMessages), }, groupName, consumer, @@ -2019,7 +2025,10 @@ public async Task StreamReadIndexerUsage() var db = conn.GetDatabase(); var streamName = Me(); - await db.StreamAddAsync(streamName, new[] { + await db.StreamAddAsync( + streamName, + new[] + { new NameValueEntry("x", "blah"), new NameValueEntry("msg", /*lang=json,strict*/ @"{""name"":""test"",""id"":123}"), new NameValueEntry("y", "more blah"), diff --git a/tests/StackExchange.Redis.Tests/StringTests.cs b/tests/StackExchange.Redis.Tests/StringTests.cs index 23acf737f..275f15fe2 100644 --- a/tests/StackExchange.Redis.Tests/StringTests.cs +++ b/tests/StackExchange.Redis.Tests/StringTests.cs @@ -527,7 +527,6 @@ public async Task BitCount() Assert.Equal(6, r3); // Async - r1 = await db.StringBitCountAsync(key); r2 = await db.StringBitCountAsync(key, 0, 0); r3 = await db.StringBitCountAsync(key, 1, 1); @@ -554,7 +553,6 @@ public async Task BitCountWithBitUnit() Assert.Equal(1, r2); // Async - r1 = await db.StringBitCountAsync(key, 1, 1); // Using default byte r2 = await db.StringBitCountAsync(key, 1, 1, StringIndexType.Bit); @@ -616,7 +614,6 @@ public async Task BitPosition() Assert.Equal(9, r3); // Async - r1 = await db.StringBitPositionAsync(key, true); r2 = await db.StringBitPositionAsync(key, true, 10, 10); r3 = await db.StringBitPositionAsync(key, true, 1, 3); @@ -670,7 +667,6 @@ public async Task HashStringLengthAsync() Assert.Equal(0, await resNonExistingAsync); } - [Fact] public void HashStringLength() { diff --git a/tests/StackExchange.Redis.Tests/SyncContextTests.cs b/tests/StackExchange.Redis.Tests/SyncContextTests.cs index 9acfa83be..fcc183ca4 100644 --- a/tests/StackExchange.Redis.Tests/SyncContextTests.cs +++ b/tests/StackExchange.Redis.Tests/SyncContextTests.cs @@ -135,11 +135,13 @@ public override void Post(SendOrPostCallback d, object? state) { Log(_log, "sync-ctx: Post"); Incr(); - ThreadPool.QueueUserWorkItem(static state => - { - var tuple = (Tuple)state!; - tuple.Item1.Invoke(tuple.Item2, tuple.Item3); - }, Tuple.Create(this, d, state)); + ThreadPool.QueueUserWorkItem( + static state => + { + var tuple = (Tuple)state!; + tuple.Item1.Invoke(tuple.Item2, tuple.Item3); + }, + Tuple.Create(this, d, state)); } private void Invoke(SendOrPostCallback d, object? state) diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 7038aaa2f..230438d07 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -1,6 +1,4 @@ -using StackExchange.Redis.Profiling; -using StackExchange.Redis.Tests.Helpers; -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -9,6 +7,8 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using StackExchange.Redis.Profiling; +using StackExchange.Redis.Tests.Helpers; using Xunit; using Xunit.Abstractions; @@ -23,7 +23,7 @@ public abstract class TestBase : IDisposable internal static string GetDefaultConfiguration() => TestConfig.Current.PrimaryServerAndPort; /// - /// Gives the current TestContext, propulated by the runner (this type of thing will be built-in in xUnit 3.x) + /// Gives the current TestContext, propulated by the runner (this type of thing will be built-in in xUnit 3.x). /// protected TestContext Context => _context.Value!; private static readonly AsyncLocal _context = new(); @@ -94,7 +94,7 @@ protected static void CollectGarbage() GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Trust me yo")] public void Dispose() { _fixture?.Teardown(Writer); @@ -293,13 +293,27 @@ internal virtual IInternalConnectionMultiplexer Create( var conn = CreateDefault( Writer, configuration ?? GetConfiguration(), - clientName, syncTimeout, asyncTimeout, allowAdmin, keepAlive, - connectTimeout, password, tieBreaker, log, - fail, disabledCommands, enabledCommands, - checkConnect, failMessage, - channelPrefix, proxy, - logTransactionData, defaultDatabase, - backlogPolicy, protocol, highIntegrity, + clientName, + syncTimeout, + asyncTimeout, + allowAdmin, + keepAlive, + connectTimeout, + password, + tieBreaker, + log, + fail, + disabledCommands, + enabledCommands, + checkConnect, + failMessage, + channelPrefix, + proxy, + logTransactionData, + defaultDatabase, + backlogPolicy, + protocol, + highIntegrity, caller); ThrowIfIncorrectProtocol(conn, protocol); @@ -323,8 +337,7 @@ internal static bool CanShare( string? configuration, int? defaultDatabase, BacklogPolicy? backlogPolicy, - bool highIntegrity - ) + bool highIntegrity) => enabledCommands == null && disabledCommands == null && fail @@ -350,7 +363,7 @@ internal void ThrowIfIncorrectProtocol(IInternalConnectionMultiplexer conn, Redi { throw new SkipTestException($"Requires protocol {requiredProtocol}, but connection is {serverProtocol}.") { - MissingFeatures = $"Protocol {requiredProtocol}." + MissingFeatures = $"Protocol {requiredProtocol}.", }; } } @@ -367,7 +380,7 @@ internal void ThrowIfBelowMinVersion(IInternalConnectionMultiplexer conn, Versio { throw new SkipTestException($"Requires server version {requiredVersion}, but server is only {serverVersion}.") { - MissingFeatures = $"Server version >= {requiredVersion}." + MissingFeatures = $"Server version >= {requiredVersion}.", }; } } @@ -436,14 +449,16 @@ public static ConnectionMultiplexer CreateDefault( var task = ConnectionMultiplexer.ConnectAsync(config, log); if (!task.Wait(config.ConnectTimeout >= (int.MaxValue / 2) ? int.MaxValue : config.ConnectTimeout * 2)) { - task.ContinueWith(x => - { - try + task.ContinueWith( + x => { - GC.KeepAlive(x.Exception); - } - catch { /* No boom */ } - }, TaskContinuationOptions.OnlyOnFaulted); + try + { + GC.KeepAlive(x.Exception); + } + catch { /* No boom */ } + }, + TaskContinuationOptions.OnlyOnFaulted); throw new TimeoutException("Connect timeout"); } watch.Stop(); @@ -498,7 +513,7 @@ protected TimeSpan RunConcurrent(Action work, int threads, int timeout = 10000, ManualResetEvent allDone = new ManualResetEvent(false); object token = new object(); int active = 0; - void callback() + void Callback() { lock (token) { @@ -524,9 +539,9 @@ void callback() var threadArr = new Thread[threads]; for (int i = 0; i < threads; i++) { - var thd = new Thread(callback) + var thd = new Thread(Callback) { - Name = caller + Name = caller, }; threadArr[i] = thd; thd.Start(); diff --git a/tests/StackExchange.Redis.Tests/TransactionTests.cs b/tests/StackExchange.Redis.Tests/TransactionTests.cs index ac67961be..b1f676d67 100644 --- a/tests/StackExchange.Redis.Tests/TransactionTests.cs +++ b/tests/StackExchange.Redis.Tests/TransactionTests.cs @@ -330,7 +330,7 @@ public enum ComparisonType { Equal, LessThan, - GreaterThan + GreaterThan, } [Theory] @@ -1232,7 +1232,7 @@ public async Task TransactionWithAdHocCommandsAndSelectDisabled() var a = tran.ExecuteAsync("SET", "foo", "bar"); Assert.True(await tran.ExecuteAsync()); var setting = db.StringGet("foo"); - Assert.Equal("bar",setting); + Assert.Equal("bar", setting); } #if VERBOSE @@ -1296,9 +1296,9 @@ public async Task ExecCompletes_Issue943() { RedisKey key = Me(); await db.KeyDeleteAsync(key); - HashEntry[] hashEntries = new [] + HashEntry[] hashEntries = new[] { - new HashEntry("blah", DateTime.UtcNow.ToString("R")) + new HashEntry("blah", DateTime.UtcNow.ToString("R")), }; ITransaction transaction = db.CreateTransaction(); transaction.AddCondition(Condition.KeyNotExists(key)); diff --git a/tests/StackExchange.Redis.Tests/ValueTests.cs b/tests/StackExchange.Redis.Tests/ValueTests.cs index f4d13617a..1877eb7c7 100644 --- a/tests/StackExchange.Redis.Tests/ValueTests.cs +++ b/tests/StackExchange.Redis.Tests/ValueTests.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis.Tests; public class ValueTests : TestBase { - public ValueTests(ITestOutputHelper output) : base (output) { } + public ValueTests(ITestOutputHelper output) : base(output) { } [Fact] public void NullValueChecks() diff --git a/toys/KestrelRedisServer/Program.cs b/toys/KestrelRedisServer/Program.cs index 349ad5d71..6cabf95d1 100644 --- a/toys/KestrelRedisServer/Program.cs +++ b/toys/KestrelRedisServer/Program.cs @@ -19,17 +19,20 @@ var app = builder.Build(); // redis-specific hack - there is a redis command to shutdown the server -_ = server.Shutdown.ContinueWith(static (t, s) => -{ - try - { // if the resp server is shutdown by a client: stop the kestrel server too - if (t.Result == RespServer.ShutdownReason.ClientInitiated) +_ = server.Shutdown.ContinueWith( + static (t, s) => + { + try { - ((IServiceProvider)s!).GetService()?.StopApplication(); + // if the resp server is shutdown by a client: stop the kestrel server too + if (t.Result == RespServer.ShutdownReason.ClientInitiated) + { + ((IServiceProvider)s!).GetService()?.StopApplication(); + } } - } - catch { /* Don't go boom on shutdown */ } -}, app.Services); + catch { /* Don't go boom on shutdown */ } + }, + app.Services); // add debug route app.Run(context => context.Response.WriteAsync(server.GetStats())); diff --git a/toys/StackExchange.Redis.Server/GlobalSuppressions.cs b/toys/StackExchange.Redis.Server/GlobalSuppressions.cs index 150a689ac..8784fa37f 100644 --- a/toys/StackExchange.Redis.Server/GlobalSuppressions.cs +++ b/toys/StackExchange.Redis.Server/GlobalSuppressions.cs @@ -5,4 +5,4 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "", Scope = "member", Target = "~M:StackExchange.Redis.TypedRedisValue.ToString~System.String")] +[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "Pending", Scope = "member", Target = "~M:StackExchange.Redis.TypedRedisValue.ToString~System.String")] diff --git a/toys/StackExchange.Redis.Server/MemoryCacheRedisServer.cs b/toys/StackExchange.Redis.Server/MemoryCacheRedisServer.cs index f7c100afe..b57ec4aea 100644 --- a/toys/StackExchange.Redis.Server/MemoryCacheRedisServer.cs +++ b/toys/StackExchange.Redis.Server/MemoryCacheRedisServer.cs @@ -18,7 +18,7 @@ private void CreateNewCache() { var old = _cache; _cache = new MemoryCache(GetType().Name); - if (old != null) old.Dispose(); + old?.Dispose(); } protected override void Dispose(bool disposing) @@ -100,7 +100,7 @@ protected override RedisValue Lpop(int database, RedisKey key) if (stack == null) return RedisValue.Null; var val = stack.Pop(); - if(stack.Count == 0) _cache.Remove(key); + if (stack.Count == 0) _cache.Remove(key); return val; } diff --git a/toys/StackExchange.Redis.Server/RedisRequest.cs b/toys/StackExchange.Redis.Server/RedisRequest.cs index 36d73a4bc..54102815c 100644 --- a/toys/StackExchange.Redis.Server/RedisRequest.cs +++ b/toys/StackExchange.Redis.Server/RedisRequest.cs @@ -3,7 +3,8 @@ namespace StackExchange.Redis.Server { public readonly ref struct RedisRequest - { // why ref? don't *really* need it, but: these things are "in flight" + { + // why ref? don't *really* need it, but: these things are "in flight" // based on an open RawResult (which is just the detokenized ReadOnlySequence) // so: using "ref" makes it clear that you can't expect to store these and have // them keep working diff --git a/toys/StackExchange.Redis.Server/RedisServer.cs b/toys/StackExchange.Redis.Server/RedisServer.cs index 8d3607e28..63efbfd1b 100644 --- a/toys/StackExchange.Redis.Server/RedisServer.cs +++ b/toys/StackExchange.Redis.Server/RedisServer.cs @@ -383,8 +383,8 @@ StringBuilder AddHeader() { sb.Append("process:").Append(process.Id).AppendLine(); } - //var port = TcpPort(); - //if (port >= 0) sb.Append("tcp_port:").Append(port).AppendLine(); + // var port = TcpPort(); + // if (port >= 0) sb.Append("tcp_port:").Append(port).AppendLine(); break; case "Clients": AddHeader().Append("connected_clients:").Append(ClientCount).AppendLine(); diff --git a/toys/StackExchange.Redis.Server/RespServer.cs b/toys/StackExchange.Redis.Server/RespServer.cs index 1edd2a3a7..75a0273ea 100644 --- a/toys/StackExchange.Redis.Server/RespServer.cs +++ b/toys/StackExchange.Redis.Server/RespServer.cs @@ -1,5 +1,4 @@ - -using System; +using System; using System.Buffers; using System.Collections.Generic; using System.IO; @@ -21,6 +20,7 @@ public enum ShutdownReason ServerDisposed, ClientInitiated, } + private readonly List _clients = new List(); private readonly TextWriter _output; @@ -70,6 +70,7 @@ public string GetStats() AppendStats(sb); return sb.ToString(); } + protected virtual void AppendStats(StringBuilder sb) => sb.Append("Current clients:\t").Append(ClientCount).AppendLine() .Append("Total clients:\t").Append(TotalClientCount).AppendLine() @@ -79,8 +80,10 @@ protected virtual void AppendStats(StringBuilder sb) => [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] protected sealed class RedisCommandAttribute : Attribute { - public RedisCommandAttribute(int arity, - string command = null, string subcommand = null) + public RedisCommandAttribute( + int arity, + string command = null, + string subcommand = null) { Command = command; SubCommand = subcommand; @@ -276,7 +279,8 @@ public async Task RunClientAsync(IDuplexPipe pipe) if (ex.GetType().Name != nameof(ConnectionResetException)) { // aspnet core has one too; swallow it by pattern - fault = ex; throw; + fault = ex; + throw; } } finally @@ -411,7 +415,7 @@ static async ValueTask Awaited(ValueTask wwrite, TypedRedisValue rresponse public TypedRedisValue Execute(RedisClient client, RedisRequest request) { - if (request.Count == 0) return default;// not a request + if (request.Count == 0) return default; // not a request if (!request.TryGetCommandBytes(0, out var cmdBytes)) return request.CommandNotFound(); if (cmdBytes.Length == 0) return default; // not a request @@ -496,11 +500,12 @@ protected virtual TypedRedisValue CommandInfo(RedisClient client, RedisRequest r for (int i = 2; i < request.Count; i++) { span[i - 2] = request.TryGetCommandBytes(i, out var cmdBytes) - &&_commands.TryGetValue(cmdBytes, out var cmdInfo) + && _commands.TryGetValue(cmdBytes, out var cmdInfo) ? CommandInfo(cmdInfo) : TypedRedisValue.NullArray; } return results; } + private TypedRedisValue CommandInfo(RespCommand command) { var arr = TypedRedisValue.Rent(6, out var span); diff --git a/toys/StackExchange.Redis.Server/TypedRedisValue.cs b/toys/StackExchange.Redis.Server/TypedRedisValue.cs index b7240370b..a67ab8d5a 100644 --- a/toys/StackExchange.Redis.Server/TypedRedisValue.cs +++ b/toys/StackExchange.Redis.Server/TypedRedisValue.cs @@ -27,6 +27,7 @@ internal static TypedRedisValue Rent(int count, out Span span) /// An invalid empty value that has no type. /// public static TypedRedisValue Nil => default; + /// /// Returns whether this value is an invalid empty value. /// @@ -70,7 +71,7 @@ public static TypedRedisValue SimpleString(string value) => new TypedRedisValue(value, ResultType.SimpleString); /// - /// The simple string OK + /// The simple string OK. /// public static TypedRedisValue OK { get; } = SimpleString("OK"); internal static TypedRedisValue Zero { get; } = Integer(0); @@ -79,7 +80,7 @@ public static TypedRedisValue SimpleString(string value) internal static TypedRedisValue EmptyArray { get; } = new TypedRedisValue(Array.Empty(), 0); /// - /// Gets the array elements as a span + /// Gets the array elements as a span. /// public ReadOnlySpan Span { @@ -175,7 +176,7 @@ internal void Recycle(int limit = -1) /// /// Get the underlying assuming that it is a valid type with a meaningful value. /// - internal RedisValue AsRedisValue() => Type == ResultType.Array ? default :_value; + internal RedisValue AsRedisValue() => Type == ResultType.Array ? default : _value; /// /// Obtain the value as a string. From e208905e90cbccda1c3f7d8bbcb3ca52828f3c1d Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:04:45 +0300 Subject: [PATCH 296/435] Feature: Support for hash field expiration (#2716) Closes/Fixes #2715 This PR add support for a set of new commands related to expiration of individual members of hash: > **_HashFieldExpire_** exposes the functionality of commands HPEXPIRE/HPEXPIREAT, for each specified field, it gets the value and sets the field's remaining time to live or expireation timestamp > **_HashFieldExpireTime_** exposes the functionality of command HPEXPIRETIME, for specified field, it gets the remaining time to live in milliseconds or expiration timestamp > **_HashFieldPersist_** exposes the functionality of command HPERSIST, for each specified field, it removes the expiration time > **_HashFieldTimeToLive_** expoes the functionality of command HPTTL, for specified field, it gets the remaining time to live in milliseconds or expiration timestamp --------- Co-authored-by: Nick Craver Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Enums/ExpireResult.cs | 27 ++ .../Enums/PersistResult.cs | 22 ++ src/StackExchange.Redis/Enums/RedisCommand.cs | 16 + .../Interfaces/IDatabase.cs | 159 +++++++++ .../Interfaces/IDatabaseAsync.cs | 15 + .../KeyspaceIsolation/KeyPrefixed.cs | 15 + .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 15 + .../PublicAPI/PublicAPI.Shipped.txt | 21 ++ src/StackExchange.Redis/RedisDatabase.cs | 77 +++++ src/StackExchange.Redis/RedisFeatures.cs | 3 +- src/StackExchange.Redis/RedisLiterals.cs | 1 + src/StackExchange.Redis/ResultProcessor.cs | 45 +++ .../HashFieldTests.cs | 305 ++++++++++++++++++ 14 files changed, 721 insertions(+), 1 deletion(-) create mode 100644 src/StackExchange.Redis/Enums/ExpireResult.cs create mode 100644 src/StackExchange.Redis/Enums/PersistResult.cs create mode 100644 tests/StackExchange.Redis.Tests/HashFieldTests.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 5a1b9aa64..bf057512f 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,7 @@ Current package versions: ## Unreleased +- Add support for hash field expiration (see [#2715](https://github.com/StackExchange/StackExchange.Redis/issues/2715)) ([#2716 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2716])) ## 2.8.0 diff --git a/src/StackExchange.Redis/Enums/ExpireResult.cs b/src/StackExchange.Redis/Enums/ExpireResult.cs new file mode 100644 index 000000000..6211492e6 --- /dev/null +++ b/src/StackExchange.Redis/Enums/ExpireResult.cs @@ -0,0 +1,27 @@ +namespace StackExchange.Redis; + +/// +/// Specifies the result of operation to set expire time. +/// +public enum ExpireResult +{ + /// + /// Field deleted because the specified expiration time is due. + /// + Due = 2, + + /// + /// Expiration time/duration updated successfully. + /// + Success = 1, + + /// + /// Expiration not set because of a specified NX | XX | GT | LT condition not met. + /// + ConditionNotMet = 0, + + /// + /// No such field. + /// + NoSuchField = -2, +} diff --git a/src/StackExchange.Redis/Enums/PersistResult.cs b/src/StackExchange.Redis/Enums/PersistResult.cs new file mode 100644 index 000000000..91fdf9fa7 --- /dev/null +++ b/src/StackExchange.Redis/Enums/PersistResult.cs @@ -0,0 +1,22 @@ +namespace StackExchange.Redis; + +/// +/// Specifies the result of operation to remove the expire time. +/// +public enum PersistResult +{ + /// + /// Expiration removed successfully. + /// + Success = 1, + + /// + /// Expiration not removed because of a specified NX | XX | GT | LT condition not met. + /// + ConditionNotMet = -1, + + /// + /// No such field. + /// + NoSuchField = -2, +} diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 728b88a76..a4647d7eb 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -66,6 +66,9 @@ internal enum RedisCommand HDEL, HELLO, HEXISTS, + HEXPIRE, + HEXPIREAT, + HEXPIRETIME, HGET, HGETALL, HINCRBY, @@ -74,6 +77,11 @@ internal enum RedisCommand HLEN, HMGET, HMSET, + HPERSIST, + HPEXPIRE, + HPEXPIREAT, + HPEXPIRETIME, + HPTTL, HRANDFIELD, HSCAN, HSET, @@ -279,9 +287,14 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.GETEX: case RedisCommand.GETSET: case RedisCommand.HDEL: + case RedisCommand.HEXPIRE: + case RedisCommand.HEXPIREAT: case RedisCommand.HINCRBY: case RedisCommand.HINCRBYFLOAT: case RedisCommand.HMSET: + case RedisCommand.HPERSIST: + case RedisCommand.HPEXPIRE: + case RedisCommand.HPEXPIREAT: case RedisCommand.HSET: case RedisCommand.HSETNX: case RedisCommand.INCR: @@ -378,11 +391,14 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.GETRANGE: case RedisCommand.HELLO: case RedisCommand.HEXISTS: + case RedisCommand.HEXPIRETIME: case RedisCommand.HGET: case RedisCommand.HGETALL: case RedisCommand.HKEYS: case RedisCommand.HLEN: case RedisCommand.HMGET: + case RedisCommand.HPEXPIRETIME: + case RedisCommand.HPTTL: case RedisCommand.HRANDFIELD: case RedisCommand.HSCAN: case RedisCommand.HSTRLEN: diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 6178051d0..c192f07f9 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -325,6 +325,165 @@ public interface IDatabase : IRedis, IDatabaseAsync /// bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); + /// + /// Set the remaining time to live in milliseconds for the given set of fields of hash + /// After the timeout has expired, the field of the hash will automatically be deleted. + /// + /// The key of the hash. + /// The fields in the hash to set expire time. + /// The timeout to set. + /// under which condition the expiration will be set using . + /// The flags to use for this operation. + /// + /// Empty array if the key does not exist. Otherwise returns an array where each item is the result of operation for given fields: + /// + /// + /// Result + /// Description + /// + /// + /// 2 + /// Field deleted because the specified expiration time is due. + /// + /// + /// 1 + /// Expiration time set/updated. + /// + /// + /// 0 + /// Expiration time is not set/update (a specified ExpireWhen condition is not met). + /// + /// + /// -1 + /// No such field exists. + /// + /// + /// + ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); + + /// + /// Set the time out on a field of the given set of fields of hash. + /// After the timeout has expired, the field of the hash will automatically be deleted. + /// + /// The key of the hash. + /// The fields in the hash to set expire time. + /// The exact date to expiry to set. + /// under which condition the expiration will be set using . + /// The flags to use for this operation. + /// + /// Empty array if the key does not exist. Otherwise returns an array where each item is the result of operation for given fields: + /// + /// + /// Result + /// Description + /// + /// + /// 2 + /// Field deleted because the specified expiration time is due. + /// + /// + /// 1 + /// Expiration time set/updated. + /// + /// + /// 0 + /// Expiration time is not set/update (a specified ExpireWhen condition is not met). + /// + /// + /// -1 + /// No such field exists. + /// + /// + /// + ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); + + /// + /// For each specified field, it gets the expiration time as a Unix timestamp in milliseconds (milliseconds since the Unix epoch). + /// + /// The key of the hash. + /// The fields in the hash to get expire time. + /// The flags to use for this operation. + /// + /// Empty array if the key does not exist. Otherwise returns the result of operation for given fields: + /// + /// + /// Result + /// Description + /// + /// + /// > 0 + /// Expiration time, as a Unix timestamp in milliseconds. + /// + /// + /// -1 + /// Field has no associated expiration time. + /// + /// + /// -2 + /// No such field exists. + /// + /// + /// + long[] HashFieldGetExpireDateTime(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + + /// + /// For each specified field, it removes the expiration time. + /// + /// The key of the hash. + /// The fields in the hash to remove expire time. + /// The flags to use for this operation. + /// + /// Empty array if the key does not exist. Otherwise returns the result of operation for given fields: + /// + /// + /// Result + /// Description + /// + /// + /// 1 + /// Expiration time was removed. + /// + /// + /// -1 + /// Field has no associated expiration time. + /// + /// + /// -2 + /// No such field exists. + /// + /// + /// + PersistResult[] HashFieldPersist(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + + /// + /// For each specified field, it gets the remaining time to live in milliseconds. + /// + /// The key of the hash. + /// The fields in the hash to get expire time. + /// The flags to use for this operation. + /// + /// Empty array if the key does not exist. Otherwise returns the result of operation for given fields: + /// + /// + /// Result + /// Description + /// + /// + /// > 0 + /// Time to live, in milliseconds. + /// + /// + /// -1 + /// Field has no associated expiration time. + /// + /// + /// -2 + /// No such field exists. + /// + /// + /// + long[] HashFieldGetTimeToLive(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + /// /// Returns the value associated with field in the hash stored at key. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 103b7151b..8600a5a1a 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -84,6 +84,21 @@ public interface IDatabaseAsync : IRedisAsync /// Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); + /// + Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldGetExpireDateTimeAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldPersistAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldGetTimeToLiveAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + /// Task HashGetAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index f6821af1d..f18e74512 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -84,6 +84,21 @@ public Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFla public Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => Inner.HashExistsAsync(ToInner(key), hashField, flags); + public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldExpireAsync(ToInner(key), hashFields, expiry, when, flags); + + public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldExpireAsync(ToInner(key), hashFields, expiry, when, flags); + + public Task HashFieldGetExpireDateTimeAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags) => + Inner.HashFieldGetExpireDateTimeAsync(ToInner(key), hashFields, flags); + + public Task HashFieldPersistAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags) => + Inner.HashFieldPersistAsync(ToInner(key), hashFields, flags); + + public Task HashFieldGetTimeToLiveAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags) => + Inner.HashFieldGetTimeToLiveAsync(ToInner(key), hashFields, flags); + public Task HashGetAllAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.HashGetAllAsync(ToInner(key), flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index e2b484935..8f570edd6 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -81,6 +81,21 @@ public bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = public bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => Inner.HashExists(ToInner(key), hashField, flags); + public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldExpire(ToInner(key), hashFields, expiry, when, flags); + + public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldExpire(ToInner(key), hashFields, expiry, when, flags); + + public long[] HashFieldGetExpireDateTime(RedisKey key, RedisValue[] hashFields, CommandFlags flags) => + Inner.HashFieldGetExpireDateTime(ToInner(key), hashFields, flags); + + public PersistResult[] HashFieldPersist(RedisKey key, RedisValue[] hashFields, CommandFlags flags) => + Inner.HashFieldPersist(ToInner(key), hashFields, flags); + + public long[] HashFieldGetTimeToLive(RedisKey key, RedisValue[] hashFields, CommandFlags flags) => + Inner.HashFieldGetTimeToLive(ToInner(key), hashFields, flags); + public HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.HashGetAll(ToInner(key), flags); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 8707cc1b4..7b0a515df 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -412,6 +412,11 @@ StackExchange.Redis.Exclude.Both = StackExchange.Redis.Exclude.Start | StackExch StackExchange.Redis.Exclude.None = 0 -> StackExchange.Redis.Exclude StackExchange.Redis.Exclude.Start = 1 -> StackExchange.Redis.Exclude StackExchange.Redis.Exclude.Stop = 2 -> StackExchange.Redis.Exclude +StackExchange.Redis.ExpireResult +StackExchange.Redis.ExpireResult.ConditionNotMet = 0 -> StackExchange.Redis.ExpireResult +StackExchange.Redis.ExpireResult.Due = 2 -> StackExchange.Redis.ExpireResult +StackExchange.Redis.ExpireResult.NoSuchField = -2 -> StackExchange.Redis.ExpireResult +StackExchange.Redis.ExpireResult.Success = 1 -> StackExchange.Redis.ExpireResult StackExchange.Redis.ExpireWhen StackExchange.Redis.ExpireWhen.Always = 0 -> StackExchange.Redis.ExpireWhen StackExchange.Redis.ExpireWhen.GreaterThanCurrentExpiry = 1 -> StackExchange.Redis.ExpireWhen @@ -558,6 +563,11 @@ StackExchange.Redis.IDatabase.HashDecrement(StackExchange.Redis.RedisKey key, St StackExchange.Redis.IDatabase.HashDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.HashDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.HashExists(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.HashFieldExpire(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.DateTime expiry, StackExchange.Redis.ExpireWhen when = StackExchange.Redis.ExpireWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ExpireResult[]! +StackExchange.Redis.IDatabase.HashFieldExpire(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.TimeSpan expiry, StackExchange.Redis.ExpireWhen when = StackExchange.Redis.ExpireWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ExpireResult[]! +StackExchange.Redis.IDatabase.HashFieldGetExpireDateTime(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long[]! +StackExchange.Redis.IDatabase.HashFieldGetTimeToLive(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long[]! +StackExchange.Redis.IDatabase.HashFieldPersist(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.PersistResult[]! StackExchange.Redis.IDatabase.HashGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.HashGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! StackExchange.Redis.IDatabase.HashGetAll(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.HashEntry[]! @@ -789,6 +799,13 @@ StackExchange.Redis.IDatabaseAsync.HashDecrementAsync(StackExchange.Redis.RedisK StackExchange.Redis.IDatabaseAsync.HashDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashExistsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! + +StackExchange.Redis.IDatabaseAsync.HashFieldExpireAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.DateTime expiry, StackExchange.Redis.ExpireWhen when = StackExchange.Redis.ExpireWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldExpireAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.TimeSpan expiry, StackExchange.Redis.ExpireWhen when = StackExchange.Redis.ExpireWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldGetExpireDateTimeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldGetTimeToLiveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldPersistAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! + StackExchange.Redis.IDatabaseAsync.HashGetAllAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -1248,6 +1265,10 @@ StackExchange.Redis.NameValueEntry.Value.get -> StackExchange.Redis.RedisValue StackExchange.Redis.Order StackExchange.Redis.Order.Ascending = 0 -> StackExchange.Redis.Order StackExchange.Redis.Order.Descending = 1 -> StackExchange.Redis.Order +StackExchange.Redis.PersistResult +StackExchange.Redis.PersistResult.ConditionNotMet = -1 -> StackExchange.Redis.PersistResult +StackExchange.Redis.PersistResult.NoSuchField = -2 -> StackExchange.Redis.PersistResult +StackExchange.Redis.PersistResult.Success = 1 -> StackExchange.Redis.PersistResult StackExchange.Redis.Profiling.IProfiledCommand StackExchange.Redis.Profiling.IProfiledCommand.Command.get -> string! StackExchange.Redis.Profiling.IProfiledCommand.CommandCreated.get -> System.DateTime diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 288e263d2..0e5eada53 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -387,6 +387,83 @@ public Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFla return ExecuteAsync(msg, ResultProcessor.Boolean); } + public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + { + long milliseconds = expiry.Ticks / TimeSpan.TicksPerMillisecond; + return HashFieldExpireExecute(key, milliseconds, when, PickExpireCommandByPrecision, SyncCustomArrExecutor>, ResultProcessor.ExpireResultArray, flags, hashFields); + } + + public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + { + long milliseconds = GetMillisecondsUntil(expiry); + return HashFieldExpireExecute(key, milliseconds, when, PickExpireAtCommandByPrecision, SyncCustomArrExecutor>, ResultProcessor.ExpireResultArray, flags, hashFields); + } + + public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + { + long milliseconds = expiry.Ticks / TimeSpan.TicksPerMillisecond; + return HashFieldExpireExecute(key, milliseconds, when, PickExpireCommandByPrecision, AsyncCustomArrExecutor>, ResultProcessor.ExpireResultArray, flags, hashFields); + } + + public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) + { + long milliseconds = GetMillisecondsUntil(expiry); + return HashFieldExpireExecute(key, milliseconds, when, PickExpireAtCommandByPrecision, AsyncCustomArrExecutor>, ResultProcessor.ExpireResultArray, flags, hashFields); + } + + private T HashFieldExpireExecute(RedisKey key, long milliseconds, ExpireWhen when, Func getCmd, CustomExecutor executor, TProcessor processor, CommandFlags flags, params RedisValue[] hashFields) + { + if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); + var useSeconds = milliseconds % 1000 == 0; + var cmd = getCmd(useSeconds); + long expiry = useSeconds ? (milliseconds / 1000) : milliseconds; + + var values = when switch + { + ExpireWhen.Always => new List { expiry, RedisLiterals.FIELDS, hashFields.Length }, + _ => new List { expiry, when.ToLiteral(), RedisLiterals.FIELDS, hashFields.Length }, + }; + values.AddRange(hashFields); + var msg = Message.Create(Database, flags, cmd, key, values.ToArray()); + return executor(msg, processor); + } + + private static RedisCommand PickExpireCommandByPrecision(bool useSeconds) => useSeconds ? RedisCommand.HEXPIRE : RedisCommand.HPEXPIRE; + + private static RedisCommand PickExpireAtCommandByPrecision(bool useSeconds) => useSeconds ? RedisCommand.HEXPIREAT : RedisCommand.HPEXPIREAT; + + private T HashFieldExecute(RedisCommand cmd, RedisKey key, CustomExecutor executor, TProcessor processor, CommandFlags flags = CommandFlags.None, params RedisValue[] hashFields) + { + var values = new List { RedisLiterals.FIELDS, hashFields.Length }; + values.AddRange(hashFields); + var msg = Message.Create(Database, flags, cmd, key, values.ToArray()); + return executor(msg, processor); + } + + private delegate T CustomExecutor(Message msg, TProcessor processor); + + private T[] SyncCustomArrExecutor(Message msg, TProcessor processor) where TProcessor : ResultProcessor => ExecuteSync(msg, processor)!; + + private Task AsyncCustomArrExecutor(Message msg, TProcessor processor) where TProcessor : ResultProcessor => ExecuteAsync(msg, processor)!; + + public long[] HashFieldGetExpireDateTime(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + HashFieldExecute(RedisCommand.HPEXPIRETIME, key, SyncCustomArrExecutor>, ResultProcessor.Int64Array, flags, hashFields); + + public Task HashFieldGetExpireDateTimeAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + HashFieldExecute(RedisCommand.HPEXPIRETIME, key, AsyncCustomArrExecutor>, ResultProcessor.Int64Array, flags, hashFields); + + public PersistResult[] HashFieldPersist(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + HashFieldExecute(RedisCommand.HPERSIST, key, SyncCustomArrExecutor>, ResultProcessor.PersistResultArray, flags, hashFields); + + public Task HashFieldPersistAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + HashFieldExecute(RedisCommand.HPERSIST, key, AsyncCustomArrExecutor>, ResultProcessor.PersistResultArray, flags, hashFields); + + public long[] HashFieldGetTimeToLive(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + HashFieldExecute(RedisCommand.HPTTL, key, SyncCustomArrExecutor>, ResultProcessor.Int64Array, flags, hashFields); + + public Task HashFieldGetTimeToLiveAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + HashFieldExecute(RedisCommand.HPTTL, key, AsyncCustomArrExecutor>, ResultProcessor.Int64Array, flags, hashFields); + public RedisValue HashGet(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) { var msg = Message.Create(Database, flags, RedisCommand.HGET, key, hashField); diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index e81043010..225516433 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -43,7 +43,8 @@ namespace StackExchange.Redis v6_2_0 = new Version(6, 2, 0), v7_0_0_rc1 = new Version(6, 9, 240), // 7.0 RC1 is version 6.9.240 v7_2_0_rc1 = new Version(7, 1, 240), // 7.2 RC1 is version 7.1.240 - v7_4_0_rc1 = new Version(7, 3, 240); // 7.4 RC1 is version 7.3.240 + v7_4_0_rc1 = new Version(7, 3, 240), // 7.4 RC1 is version 7.3.240 + v7_4_0_rc2 = new Version(7, 3, 241); // 7.4 RC2 is version 7.3.241 #pragma warning restore SA1310 // Field names should not contain underscore #pragma warning restore SA1311 // Static readonly fields should begin with upper-case letter diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index b77649402..a076ff3fe 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -78,6 +78,7 @@ public static readonly RedisValue EX = "EX", EXAT = "EXAT", EXISTS = "EXISTS", + FIELDS = "FIELDS", FILTERBY = "FILTERBY", FLUSH = "FLUSH", FREQ = "FREQ", diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 894e6f5f9..7a69f7429 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -68,6 +68,10 @@ public static readonly ResultProcessor public static readonly ResultProcessor NullableInt64 = new NullableInt64Processor(); + public static readonly ResultProcessor ExpireResultArray = new ExpireResultArrayProcessor(); + + public static readonly ResultProcessor PersistResultArray = new PersistResultArrayProcessor(); + public static readonly ResultProcessor RedisChannelArrayLiteral = new RedisChannelArrayProcessor(RedisChannel.PatternMode.Literal); @@ -1452,6 +1456,47 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return true; } break; + case ResultType.Array: + var items = result.GetItems(); + if (items.Length == 1) + { // treat an array of 1 like a single reply + if (items[0].TryGetInt64(out long value)) + { + SetResult(message, value); + return true; + } + } + break; + } + return false; + } + } + + private sealed class ExpireResultArrayProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray == ResultType.Array || result.IsNull) + { + var arr = result.ToArray((in RawResult x) => (ExpireResult)(long)x.AsRedisValue())!; + + SetResult(message, arr); + return true; + } + return false; + } + } + + private sealed class PersistResultArrayProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray == ResultType.Array || result.IsNull) + { + var arr = result.ToArray((in RawResult x) => (PersistResult)(long)x.AsRedisValue())!; + + SetResult(message, arr); + return true; } return false; } diff --git a/tests/StackExchange.Redis.Tests/HashFieldTests.cs b/tests/StackExchange.Redis.Tests/HashFieldTests.cs new file mode 100644 index 000000000..e50cd0546 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/HashFieldTests.cs @@ -0,0 +1,305 @@ +using System; +using System.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests; + +/// +/// Tests for . +/// +[RunPerProtocol] +[Collection(SharedConnectionFixture.Key)] +public class HashFieldTests : TestBase +{ + private readonly DateTime nextCentury = new DateTime(2101, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private readonly TimeSpan oneYearInMs = TimeSpan.FromMilliseconds(31536000000); + + private readonly HashEntry[] entries = new HashEntry[] { new("f1", 1), new("f2", 2) }; + + private readonly RedisValue[] fields = new RedisValue[] { "f1", "f2" }; + + public HashFieldTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) + { + } + + [Fact] + public void HashFieldExpire() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + + var fieldsResult = db.HashFieldExpire(hashKey, fields, oneYearInMs); + Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success }, fieldsResult); + + fieldsResult = db.HashFieldExpire(hashKey, fields, nextCentury); + Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success, }, fieldsResult); + } + + [Fact] + public void HashFieldExpireNoKey() + { + var db = Create(require: RedisFeatures.v7_4_0_rc2).GetDatabase(); + var hashKey = Me(); + + var fieldsResult = db.HashFieldExpire(hashKey, fields, oneYearInMs); + Assert.Equal(new[] { ExpireResult.NoSuchField, ExpireResult.NoSuchField }, fieldsResult); + + fieldsResult = db.HashFieldExpire(hashKey, fields, nextCentury); + Assert.Equal(new[] { ExpireResult.NoSuchField, ExpireResult.NoSuchField }, fieldsResult); + } + + [Fact] + public async void HashFieldExpireAsync() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + + var fieldsResult = await db.HashFieldExpireAsync(hashKey, fields, oneYearInMs); + Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success }, fieldsResult); + + fieldsResult = await db.HashFieldExpireAsync(hashKey, fields, nextCentury); + Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success }, fieldsResult); + } + + [Fact] + public async void HashFieldExpireAsyncNoKey() + { + var db = Create(require: RedisFeatures.v7_4_0_rc2).GetDatabase(); + var hashKey = Me(); + + var fieldsResult = await db.HashFieldExpireAsync(hashKey, fields, oneYearInMs); + Assert.Equal(new[] { ExpireResult.NoSuchField, ExpireResult.NoSuchField }, fieldsResult); + + fieldsResult = await db.HashFieldExpireAsync(hashKey, fields, nextCentury); + Assert.Equal(new[] { ExpireResult.NoSuchField, ExpireResult.NoSuchField }, fieldsResult); + } + + [Fact] + public void HashFieldGetExpireDateTimeIsDue() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + + var result = db.HashFieldExpire(hashKey, new RedisValue[] { "f1" }, new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + Assert.Equal(new[] { ExpireResult.Due }, result); + } + + [Fact] + public void HashFieldExpireNoField() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + + var result = db.HashFieldExpire(hashKey, new RedisValue[] { "nonExistingField" }, oneYearInMs); + Assert.Equal(new[] { ExpireResult.NoSuchField }, result); + } + + [Fact] + public void HashFieldExpireConditionsSatisfied() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.KeyDelete(hashKey); + db.HashSet(hashKey, entries); + db.HashSet(hashKey, new HashEntry[] { new("f3", 3), new("f4", 4) }); + var initialExpire = db.HashFieldExpire(hashKey, new RedisValue[] { "f2", "f3", "f4" }, new DateTime(2050, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success, ExpireResult.Success }, initialExpire); + + var result = db.HashFieldExpire(hashKey, new RedisValue[] { "f1" }, oneYearInMs, ExpireWhen.HasNoExpiry); + Assert.Equal(new[] { ExpireResult.Success }, result); + + result = db.HashFieldExpire(hashKey, new RedisValue[] { "f2" }, oneYearInMs, ExpireWhen.HasExpiry); + Assert.Equal(new[] { ExpireResult.Success }, result); + + result = db.HashFieldExpire(hashKey, new RedisValue[] { "f3" }, nextCentury, ExpireWhen.GreaterThanCurrentExpiry); + Assert.Equal(new[] { ExpireResult.Success }, result); + + result = db.HashFieldExpire(hashKey, new RedisValue[] { "f4" }, oneYearInMs, ExpireWhen.LessThanCurrentExpiry); + Assert.Equal(new[] { ExpireResult.Success }, result); + } + + [Fact] + public void HashFieldExpireConditionsNotSatisfied() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.KeyDelete(hashKey); + db.HashSet(hashKey, entries); + db.HashSet(hashKey, new HashEntry[] { new("f3", 3), new("f4", 4) }); + var initialExpire = db.HashFieldExpire(hashKey, new RedisValue[] { "f2", "f3", "f4" }, new DateTime(2050, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success, ExpireResult.Success }, initialExpire); + + var result = db.HashFieldExpire(hashKey, new RedisValue[] { "f1" }, oneYearInMs, ExpireWhen.HasExpiry); + Assert.Equal(new[] { ExpireResult.ConditionNotMet }, result); + + result = db.HashFieldExpire(hashKey, new RedisValue[] { "f2" }, oneYearInMs, ExpireWhen.HasNoExpiry); + Assert.Equal(new[] { ExpireResult.ConditionNotMet }, result); + + result = db.HashFieldExpire(hashKey, new RedisValue[] { "f3" }, nextCentury, ExpireWhen.LessThanCurrentExpiry); + Assert.Equal(new[] { ExpireResult.ConditionNotMet }, result); + + result = db.HashFieldExpire(hashKey, new RedisValue[] { "f4" }, oneYearInMs, ExpireWhen.GreaterThanCurrentExpiry); + Assert.Equal(new[] { ExpireResult.ConditionNotMet }, result); + } + + [Fact] + public void HashFieldGetExpireDateTime() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + db.HashFieldExpire(hashKey, fields, nextCentury); + long ms = new DateTimeOffset(nextCentury).ToUnixTimeMilliseconds(); + + var result = db.HashFieldGetExpireDateTime(hashKey, new RedisValue[] { "f1" }); + Assert.Equal(new[] { ms }, result); + + var fieldsResult = db.HashFieldGetExpireDateTime(hashKey, fields); + Assert.Equal(new[] { ms, ms }, fieldsResult); + } + + [Fact] + public void HashFieldExpireFieldNoExpireTime() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + + var result = db.HashFieldGetExpireDateTime(hashKey, new RedisValue[] { "f1" }); + Assert.Equal(new[] { -1L }, result); + + var fieldsResult = db.HashFieldGetExpireDateTime(hashKey, fields); + Assert.Equal(new long[] { -1, -1, }, fieldsResult); + } + + [Fact] + public void HashFieldGetExpireDateTimeNoKey() + { + var db = Create(require: RedisFeatures.v7_4_0_rc2).GetDatabase(); + var hashKey = Me(); + + var fieldsResult = db.HashFieldGetExpireDateTime(hashKey, fields); + Assert.Equal(new long[] { -2, -2, }, fieldsResult); + } + + [Fact] + public void HashFieldGetExpireDateTimeNoField() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + db.HashFieldExpire(hashKey, fields, oneYearInMs); + + var fieldsResult = db.HashFieldGetExpireDateTime(hashKey, new RedisValue[] { "notExistingField1", "notExistingField2" }); + Assert.Equal(new long[] { -2, -2, }, fieldsResult); + } + + [Fact] + public void HashFieldGetTimeToLive() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + db.HashFieldExpire(hashKey, fields, oneYearInMs); + long ms = new DateTimeOffset(nextCentury).ToUnixTimeMilliseconds(); + + var result = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" }); + Assert.NotNull(result); + Assert.True(result.Length == 1); + Assert.True(result[0] > 0); + + var fieldsResult = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.NotNull(fieldsResult); + Assert.True(fieldsResult.Length > 0); + Assert.True(fieldsResult.All(x => x > 0)); + } + + [Fact] + public void HashFieldGetTimeToLiveNoExpireTime() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + + var fieldsResult = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.Equal(new long[] { -1, -1, }, fieldsResult); + } + + [Fact] + public void HashFieldGetTimeToLiveNoKey() + { + var db = Create(require: RedisFeatures.v7_4_0_rc2).GetDatabase(); + var hashKey = Me(); + + var fieldsResult = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.Equal(new long[] { -2, -2, }, fieldsResult); + } + + [Fact] + public void HashFieldGetTimeToLiveNoField() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + db.HashFieldExpire(hashKey, fields, oneYearInMs); + + var fieldsResult = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "notExistingField1", "notExistingField2" }); + Assert.Equal(new long[] { -2, -2, }, fieldsResult); + } + + [Fact] + public void HashFieldPersist() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + db.HashFieldExpire(hashKey, fields, oneYearInMs); + long ms = new DateTimeOffset(nextCentury).ToUnixTimeMilliseconds(); + + var result = db.HashFieldPersist(hashKey, new RedisValue[] { "f1" }); + Assert.Equal(new[] { PersistResult.Success }, result); + + db.HashFieldExpire(hashKey, fields, oneYearInMs); + + var fieldsResult = db.HashFieldPersist(hashKey, fields); + Assert.Equal(new[] { PersistResult.Success, PersistResult.Success }, fieldsResult); + } + + [Fact] + public void HashFieldPersistNoExpireTime() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + + var fieldsResult = db.HashFieldPersist(hashKey, fields); + Assert.Equal(new[] { PersistResult.ConditionNotMet, PersistResult.ConditionNotMet }, fieldsResult); + } + + [Fact] + public void HashFieldPersistNoKey() + { + var db = Create(require: RedisFeatures.v7_4_0_rc2).GetDatabase(); + var hashKey = Me(); + + var fieldsResult = db.HashFieldPersist(hashKey, fields); + Assert.Equal(new[] { PersistResult.NoSuchField, PersistResult.NoSuchField }, fieldsResult); + } + + [Fact] + public void HashFieldPersistNoField() + { + var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); + var hashKey = Me(); + db.HashSet(hashKey, entries); + db.HashFieldExpire(hashKey, fields, oneYearInMs); + + var fieldsResult = db.HashFieldPersist(hashKey, new RedisValue[] { "notExistingField1", "notExistingField2" }); + Assert.Equal(new[] { PersistResult.NoSuchField, PersistResult.NoSuchField }, fieldsResult); + } +} From 3701b50430fea92f1bc0a1348549811c8c250903 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sun, 18 Aug 2024 09:08:30 -0400 Subject: [PATCH 297/435] Build: Simplify Docker layout, fix Envoy, and upgrade tests (#2774) It seems the Envoy apts have gone missing breaking out build, so instead of relying on the install let's docker compose their image as a proxy against the Redis supervisor instance as a simpler and faster-to-start setup that also works. This rearranged some things to simplify the Docker story overall. A move to AzDO or just GitHub builds would simplify everything further, but we need to figure out Windows testing against a Docker setup in CI. Note: we still can't use Linux containers on a Windows GitHub Actions host (https://github.com/actions/runner/issues/904), so this remains much more complicated and not-really-testing-the-real-thing in the Windows front. --- .github/workflows/CI.yml | 11 +++-- Directory.Packages.props | 16 +++--- .../Maintenance/AzureMaintenanceEvent.cs | 2 +- src/StackExchange.Redis/ResultProcessor.cs | 2 +- tests/RedisConfigs/.docker/Envoy/Dockerfile | 6 +++ tests/RedisConfigs/.docker/Envoy/envoy.yaml | 35 +++++++++++++ tests/RedisConfigs/.docker/Redis/Dockerfile | 23 +++++++++ .../Redis}/docker-entrypoint.sh | 0 .../Redis}/supervisord.conf | 7 --- tests/RedisConfigs/Docker/install-envoy.sh | 9 ---- tests/RedisConfigs/Dockerfile | 27 ---------- tests/RedisConfigs/Envoy/envoy.yaml | 49 ------------------- tests/RedisConfigs/docker-compose.yml | 28 ++++++++--- .../AzureMaintenanceEventTests.cs | 2 +- .../StackExchange.Redis.Tests/ClusterTests.cs | 2 +- tests/StackExchange.Redis.Tests/EnvoyTests.cs | 10 ++-- .../Helpers/Attributes.cs | 12 ++--- .../Issues/Issue2653.cs | 2 +- .../StackExchange.Redis.Tests/PubSubTests.cs | 2 +- .../RespProtocolTests.cs | 4 +- .../ScriptingTests.cs | 2 +- .../SentinelFailoverTests.cs | 2 +- .../ServerSnapshotTests.cs | 2 + .../TransactionTests.cs | 8 +-- .../WithKeyPrefixTests.cs | 2 +- .../xunit.runner.json | 3 ++ 26 files changed, 130 insertions(+), 138 deletions(-) create mode 100644 tests/RedisConfigs/.docker/Envoy/Dockerfile create mode 100644 tests/RedisConfigs/.docker/Envoy/envoy.yaml create mode 100644 tests/RedisConfigs/.docker/Redis/Dockerfile rename tests/RedisConfigs/{Docker => .docker/Redis}/docker-entrypoint.sh (100%) rename tests/RedisConfigs/{Docker => .docker/Redis}/supervisord.conf (95%) delete mode 100644 tests/RedisConfigs/Docker/install-envoy.sh delete mode 100644 tests/RedisConfigs/Dockerfile delete mode 100644 tests/RedisConfigs/Envoy/envoy.yaml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5e0b81ea7..58856abbc 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -24,12 +24,12 @@ jobs: with: dotnet-version: | 6.0.x - 7.0.x + 8.0.x - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true - name: Start Redis Services (docker-compose) working-directory: ./tests/RedisConfigs - run: docker compose -f docker-compose.yml up -d + run: docker compose -f docker-compose.yml up -d --wait - name: StackExchange.Redis.Tests run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true - uses: dorny/test-reporter@v1 @@ -49,6 +49,7 @@ jobs: NUGET_CERT_REVOCATION_MODE: offline # Disabling signing because of massive perf hit, see https://github.com/NuGet/Home/issues/11548 DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: "1" # Note this doesn't work yet for Windows - see https://github.com/dotnet/runtime/issues/68340 TERM: xterm + DOCKER_BUILDKIT: 1 steps: - name: Checkout code uses: actions/checkout@v1 @@ -57,9 +58,13 @@ jobs: # with: # dotnet-version: | # 6.0.x - # 7.0.x + # 8.0.x - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true + # We can't do this combination - see https://github.com/actions/runner/issues/904 + # - name: Start Redis Services (docker-compose) + # working-directory: .\tests\RedisConfigs + # run: docker compose -f docker-compose.yml up -d --wait - name: Start Redis Services (v3.0.503) working-directory: .\tests\RedisConfigs\3.0.503 run: | diff --git a/Directory.Packages.props b/Directory.Packages.props index 49d37a3ae..c8a4189e6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,21 +10,21 @@ - + - - - - - + + + + + - - + + \ No newline at end of file diff --git a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs index 75bcb6de9..4e32afa5a 100644 --- a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs @@ -15,7 +15,7 @@ public sealed class AzureMaintenanceEvent : ServerMaintenanceEvent { private const string PubSubChannelName = "AzureRedisEvents"; - internal AzureMaintenanceEvent(string azureEvent) + internal AzureMaintenanceEvent(string? azureEvent) { if (azureEvent == null) { diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 7a69f7429..648387b87 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -526,7 +526,7 @@ internal sealed class ScriptLoadProcessor : ResultProcessor private static readonly Regex sha1 = new Regex("^[0-9a-f]{40}$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - internal static bool IsSHA1(string script) => script is not null && script.Length == SHA1Length && sha1.IsMatch(script); + internal static bool IsSHA1(string? script) => script is not null && script.Length == SHA1Length && sha1.IsMatch(script); internal const int Sha1HashLength = 20; internal static byte[] ParseSHA1(byte[] value) diff --git a/tests/RedisConfigs/.docker/Envoy/Dockerfile b/tests/RedisConfigs/.docker/Envoy/Dockerfile new file mode 100644 index 000000000..5c20d350c --- /dev/null +++ b/tests/RedisConfigs/.docker/Envoy/Dockerfile @@ -0,0 +1,6 @@ +FROM envoyproxy/envoy:v1.31-latest + +COPY envoy.yaml /etc/envoy/envoy.yaml +RUN chmod go+r /etc/envoy/envoy.yaml + +EXPOSE 7015 diff --git a/tests/RedisConfigs/.docker/Envoy/envoy.yaml b/tests/RedisConfigs/.docker/Envoy/envoy.yaml new file mode 100644 index 000000000..fe57c8c1f --- /dev/null +++ b/tests/RedisConfigs/.docker/Envoy/envoy.yaml @@ -0,0 +1,35 @@ +admin: + address: { socket_address: { protocol: TCP, address: 0.0.0.0, port_value: 8001 } } +static_resources: + listeners: + - name: redis_listener + address: { socket_address: { protocol: TCP, address: 0.0.0.0, port_value: 7015 } } + filter_chains: + - filters: + - name: envoy.filters.network.redis_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.redis_proxy.v3.RedisProxy + stat_prefix: envoy_redis_stats + settings: + op_timeout: 3s + dns_cache_config: + name: dynamic_forward_proxy_cache_config + dns_lookup_family: V4_ONLY + prefix_routes: + catch_all_route: + cluster: redis_cluster + clusters: + - name: redis_cluster + connect_timeout: 3s + type: STRICT_DNS + dns_lookup_family: V4_ONLY + load_assignment: + cluster_name: redis_cluster + endpoints: + - lb_endpoints: + - endpoint: { address: { socket_address: { address: redis, port_value: 7000 } } } + - endpoint: { address: { socket_address: { address: redis, port_value: 7001 } } } + - endpoint: { address: { socket_address: { address: redis, port_value: 7002 } } } + - endpoint: { address: { socket_address: { address: redis, port_value: 7003 } } } + - endpoint: { address: { socket_address: { address: redis, port_value: 7004 } } } + - endpoint: { address: { socket_address: { address: redis, port_value: 7005 } } } \ No newline at end of file diff --git a/tests/RedisConfigs/.docker/Redis/Dockerfile b/tests/RedisConfigs/.docker/Redis/Dockerfile new file mode 100644 index 000000000..d4c7dec7c --- /dev/null +++ b/tests/RedisConfigs/.docker/Redis/Dockerfile @@ -0,0 +1,23 @@ +FROM redis:7.4-rc1 + +COPY --from=configs ./Basic /data/Basic/ +COPY --from=configs ./Failover /data/Failover/ +COPY --from=configs ./Cluster /data/Cluster/ +COPY --from=configs ./Sentinel /data/Sentinel/ +COPY --from=configs ./Certs /Certs/ + +RUN chown -R redis:redis /data +RUN chown -R redis:redis /Certs + +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +RUN apt-get -y update && apt-get install supervisor -y + +RUN apt-get clean + +ADD supervisord.conf /etc/ + +ENTRYPOINT ["docker-entrypoint.sh"] + +EXPOSE 6379 6380 6381 6382 6383 6384 7000 7001 7002 7003 7004 7005 7010 7011 26379 26380 26381 diff --git a/tests/RedisConfigs/Docker/docker-entrypoint.sh b/tests/RedisConfigs/.docker/Redis/docker-entrypoint.sh similarity index 100% rename from tests/RedisConfigs/Docker/docker-entrypoint.sh rename to tests/RedisConfigs/.docker/Redis/docker-entrypoint.sh diff --git a/tests/RedisConfigs/Docker/supervisord.conf b/tests/RedisConfigs/.docker/Redis/supervisord.conf similarity index 95% rename from tests/RedisConfigs/Docker/supervisord.conf rename to tests/RedisConfigs/.docker/Redis/supervisord.conf index 21fe45f1b..e0bd20571 100644 --- a/tests/RedisConfigs/Docker/supervisord.conf +++ b/tests/RedisConfigs/.docker/Redis/supervisord.conf @@ -99,13 +99,6 @@ stdout_logfile=/var/log/supervisor/%(program_name)s.log stderr_logfile=/var/log/supervisor/%(program_name)s.log autorestart=true -[program:redis-7015] -command=/usr/bin/envoy -c /envoy/envoy.yaml -directory=/envoy -stdout_logfile=/var/log/supervisor/%(program_name)s.log -stderr_logfile=/var/log/supervisor/%(program_name)s.log -autorestart=true - [program:sentinel-26379] command=/usr/local/bin/redis-server /data/Sentinel/sentinel-26379.conf --sentinel directory=/data/Sentinel diff --git a/tests/RedisConfigs/Docker/install-envoy.sh b/tests/RedisConfigs/Docker/install-envoy.sh deleted file mode 100644 index 9bf7c9863..000000000 --- a/tests/RedisConfigs/Docker/install-envoy.sh +++ /dev/null @@ -1,9 +0,0 @@ -# instructions from https://www.envoyproxy.io/docs/envoy/latest/start/install -apt update -apt -y install debian-keyring debian-archive-keyring apt-transport-https curl lsb-release -curl -sL 'https://deb.dl.getenvoy.io/public/gpg.8115BA8E629CC074.key' | gpg --dearmor -o /usr/share/keyrings/getenvoy-keyring.gpg -echo "deb [arch=amd64 signed-by=/usr/share/keyrings/getenvoy-keyring.gpg] https://deb.dl.getenvoy.io/public/deb/debian $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/getenvoy.list -apt update -apt install getenvoy-envoy - - diff --git a/tests/RedisConfigs/Dockerfile b/tests/RedisConfigs/Dockerfile deleted file mode 100644 index ab0f76e6c..000000000 --- a/tests/RedisConfigs/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -FROM redis:7.4-rc1 - -COPY Basic /data/Basic/ -COPY Failover /data/Failover/ -COPY Cluster /data/Cluster/ -COPY Sentinel /data/Sentinel/ -COPY Certs /Certs/ - -RUN chown -R redis:redis /data -RUN chown -R redis:redis /Certs - -COPY Docker/docker-entrypoint.sh /usr/local/bin/ -RUN chmod +x /usr/local/bin/docker-entrypoint.sh - -RUN apt-get -y update && apt-get install -y git gcc make supervisor - -COPY Docker/install-envoy.sh /usr/local/bin -RUN sh /usr/local/bin/install-envoy.sh - -RUN apt-get clean - -COPY Envoy/envoy.yaml /envoy/envoy.yaml -ADD Docker/supervisord.conf /etc/ - -ENTRYPOINT ["docker-entrypoint.sh"] - -EXPOSE 6379 6380 6381 6382 6383 6384 7000 7001 7002 7003 7004 7005 7010 7011 7015 26379 26380 26381 diff --git a/tests/RedisConfigs/Envoy/envoy.yaml b/tests/RedisConfigs/Envoy/envoy.yaml deleted file mode 100644 index 3f1c5c1a9..000000000 --- a/tests/RedisConfigs/Envoy/envoy.yaml +++ /dev/null @@ -1,49 +0,0 @@ -admin: - access_log_path: "/dev/null" - address: - socket_address: - protocol: TCP - address: 0.0.0.0 - port_value: 8001 -static_resources: - listeners: - - name: redis_listener - address: - socket_address: - address: 0.0.0.0 - port_value: 7015 - filter_chains: - - filters: - - name: envoy.filters.network.redis_proxy - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.redis_proxy.v3.RedisProxy - stat_prefix: envoy_redis_stats - settings: - op_timeout: 5s - enable_redirection: true - prefix_routes: - catch_all_route: - cluster: redis_cluster - clusters: - - name: redis_cluster - connect_timeout: 1s - dns_lookup_family: V4_ONLY - lb_policy: CLUSTER_PROVIDED - load_assignment: - cluster_name: redis_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: 127.0.0.1 - port_value: 7000 - cluster_type: - name: envoy.clusters.redis - typed_config: - "@type": type.googleapis.com/google.protobuf.Struct - value: - cluster_refresh_rate: 30s - cluster_refresh_timeout: 0.5s - redirect_refresh_interval: 10s - redirect_refresh_threshold: 10 diff --git a/tests/RedisConfigs/docker-compose.yml b/tests/RedisConfigs/docker-compose.yml index e27bec0b8..84cb3cc75 100644 --- a/tests/RedisConfigs/docker-compose.yml +++ b/tests/RedisConfigs/docker-compose.yml @@ -1,16 +1,28 @@ -version: '2.6' +version: '2.7' services: redis: build: - context: . - image: stackexchange/redis-tests:latest + context: .docker/Redis + additional_contexts: + configs: . platform: linux ports: - - 6379-6384:6379-6384 - - 7000-7006:7000-7006 - - 7010-7011:7010-7011 - - 7015:7015 - - 26379-26381:26379-26381 + - 6379-6384:6379-6384 # Misc + - 7000-7006:7000-7006 # Cluster + - 7010-7011:7010-7011 # Sentinel Controllers + - 26379-26381:26379-26381 # Sentinel Data sysctls : net.core.somaxconn: '511' + envoy: + build: + context: .docker/Envoy + platform: linux + environment: + loglevel: warning + depends_on: + redis: + condition: service_started + ports: + - 7015:7015 # Cluster + - 8001:8001 # Admin diff --git a/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs b/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs index 4e3bdcbd6..852ab9af7 100644 --- a/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs +++ b/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs @@ -29,7 +29,7 @@ public AzureMaintenanceEventTests(ITestOutputHelper output) : base(output) { } [InlineData("NonSSLPort |", AzureNotificationType.Unknown, null, false, null, 0, 0)] [InlineData("StartTimeInUTC|thisisthestart", AzureNotificationType.Unknown, null, false, null, 0, 0)] [InlineData(null, AzureNotificationType.Unknown, null, false, null, 0, 0)] - public void TestAzureMaintenanceEventStrings(string message, AzureNotificationType expectedEventType, string expectedStart, bool expectedIsReplica, string expectedIP, int expectedSSLPort, int expectedNonSSLPort) + public void TestAzureMaintenanceEventStrings(string? message, AzureNotificationType expectedEventType, string? expectedStart, bool expectedIsReplica, string? expectedIP, int expectedSSLPort, int expectedNonSSLPort) { DateTime? expectedStartTimeUtc = null; if (expectedStart != null && DateTime.TryParseExact(expectedStart, "s", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTime startTimeUtc)) diff --git a/tests/StackExchange.Redis.Tests/ClusterTests.cs b/tests/StackExchange.Redis.Tests/ClusterTests.cs index fcab7bb14..742ce51bb 100644 --- a/tests/StackExchange.Redis.Tests/ClusterTests.cs +++ b/tests/StackExchange.Redis.Tests/ClusterTests.cs @@ -365,7 +365,7 @@ public void TransactionWithSameSlotKeys() [InlineData(null, 100)] [InlineData("abc", 10)] [InlineData("abc", 100)] - public void Keys(string pattern, int pageSize) + public void Keys(string? pattern, int pageSize) { using var conn = Create(allowAdmin: true); diff --git a/tests/StackExchange.Redis.Tests/EnvoyTests.cs b/tests/StackExchange.Redis.Tests/EnvoyTests.cs index 5015a660d..6d669a6db 100644 --- a/tests/StackExchange.Redis.Tests/EnvoyTests.cs +++ b/tests/StackExchange.Redis.Tests/EnvoyTests.cs @@ -35,15 +35,15 @@ public void TestBasicEnvoyConnection() } catch (TimeoutException ex) when (ex.Message == "Connect timeout" || sb.ToString().Contains("Returned, but incorrectly")) { - Skip.Inconclusive("Envoy server not found."); + Skip.Inconclusive($"Envoy server not found: {ex}."); } - catch (AggregateException) + catch (AggregateException ex) { - Skip.Inconclusive("Envoy server not found."); + Skip.Inconclusive($"Envoy server not found: {ex}."); } - catch (RedisConnectionException) when (sb.ToString().Contains("It was not possible to connect to the redis server(s)")) + catch (RedisConnectionException ex) when (sb.ToString().Contains("It was not possible to connect to the redis server(s)")) { - Skip.Inconclusive("Envoy server not found."); + Skip.Inconclusive($"Envoy server not found: {ex}."); } } } diff --git a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs index 1fab56cbb..ab0583764 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs @@ -251,16 +251,14 @@ public static IEnumerable Expand(this ITestMethod testMethod, Fu { protocols = RunPerProtocol.AllProtocols; } - var results = new List(); foreach (var protocol in protocols) { - results.Add(generator(protocol)); + yield return generator(protocol); } - return results; } else { - return new[] { generator(RedisProtocol.Resp2) }; + yield return generator(RedisProtocol.Resp2); } } } @@ -270,7 +268,7 @@ public static IEnumerable Expand(this ITestMethod testMethod, Fu /// and with another culture. /// /// -/// Based on: https://bartwullems.blogspot.com/2022/03/xunit-change-culture-during-your-test.html +/// Based on: https://bartwullems.blogspot.com/2022/03/xunit-change-culture-during-your-test.html. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class TestCultureAttribute : BeforeAfterTestAttribute @@ -288,7 +286,7 @@ public class TestCultureAttribute : BeforeAfterTestAttribute /// Stores the current and /// and replaces them with the new cultures defined in the constructor. /// - /// The method under test + /// The method under test. public override void Before(MethodInfo methodUnderTest) { originalCulture = Thread.CurrentThread.CurrentCulture; @@ -299,7 +297,7 @@ public override void Before(MethodInfo methodUnderTest) /// /// Restores the original to . /// - /// The method under test + /// The method under test. public override void After(MethodInfo methodUnderTest) { if (originalCulture is not null) diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2653.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2653.cs index 973a01da5..d304ff44a 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue2653.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2653.cs @@ -11,6 +11,6 @@ public class Issue2653 [InlineData("abc.def", "abc.def")] [InlineData("abc d \t ef", "abc-d-ef")] [InlineData(" abc\r\ndef\n", "abc-def")] - public void CheckLibraySanitization(string input, string expected) + public void CheckLibraySanitization(string? input, string expected) => Assert.Equal(expected, ServerEndPoint.ClientInfoSanitize(input)); } diff --git a/tests/StackExchange.Redis.Tests/PubSubTests.cs b/tests/StackExchange.Redis.Tests/PubSubTests.cs index d064f298d..249b8e63d 100644 --- a/tests/StackExchange.Redis.Tests/PubSubTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubTests.cs @@ -56,7 +56,7 @@ await UntilConditionAsync( [InlineData(null, true, "d")] [InlineData("", true, "e")] [InlineData("Foo:", true, "f")] - public async Task TestBasicPubSub(string channelPrefix, bool wildCard, string breaker) + public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string breaker) { using var conn = Create(channelPrefix: channelPrefix, shared: false, log: Writer); diff --git a/tests/StackExchange.Redis.Tests/RespProtocolTests.cs b/tests/StackExchange.Redis.Tests/RespProtocolTests.cs index c2cd8a026..c508bd1c9 100644 --- a/tests/StackExchange.Redis.Tests/RespProtocolTests.cs +++ b/tests/StackExchange.Redis.Tests/RespProtocolTests.cs @@ -156,7 +156,7 @@ public async Task ConnectWithBrokenHello(string command, bool isResp3) [InlineData("return { map = { a = 1, b = 2, c = 3 } }", RedisProtocol.Resp3, ResultType.Array, ResultType.Map, MAP_ABC, 6)] [InlineData("return { set = { a = 1, b = 2, c = 3 } }", RedisProtocol.Resp3, ResultType.Array, ResultType.Set, SET_ABC, 6)] [InlineData("return { double = 42 }", RedisProtocol.Resp3, ResultType.SimpleString, ResultType.Double, 42.0, 6)] - public async Task CheckLuaResult(string script, RedisProtocol protocol, ResultType resp2, ResultType resp3, object expected, int serverMin = 1) + public async Task CheckLuaResult(string script, RedisProtocol protocol, ResultType resp2, ResultType resp3, object? expected, int? serverMin = 1) { // note Lua does not appear to return RESP3 types in any scenarios var muxer = Create(protocol: protocol); @@ -313,7 +313,7 @@ public async Task CheckLuaResult(string script, RedisProtocol protocol, ResultTy [InlineData("debug", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, false, "protocol", "false")] [InlineData("debug", RedisProtocol.Resp3, ResultType.Integer, ResultType.Boolean, false, "protocol", "false")] - public async Task CheckCommandResult(string command, RedisProtocol protocol, ResultType resp2, ResultType resp3, object expected, params object[] args) + public async Task CheckCommandResult(string command, RedisProtocol protocol, ResultType resp2, ResultType resp3, object? expected, params object[] args) { var muxer = Create(protocol: protocol); var ep = muxer.GetServerEndPoint(muxer.GetEndPoints().Single()); diff --git a/tests/StackExchange.Redis.Tests/ScriptingTests.cs b/tests/StackExchange.Redis.Tests/ScriptingTests.cs index bbacb96bd..59bb0e608 100644 --- a/tests/StackExchange.Redis.Tests/ScriptingTests.cs +++ b/tests/StackExchange.Redis.Tests/ScriptingTests.cs @@ -1021,7 +1021,7 @@ public void ScriptWithKeyPrefixCompare() [InlineData("$29c3804401b0727f70f73d4415e162400cbe57b", false)] [InlineData("829c3804401b0727f70f73d4415e162400cbe57", false)] [InlineData("829c3804401b0727f70f73d4415e162400cbe57bb", false)] - public void Sha1Detection(string candidate, bool isSha) + public void Sha1Detection(string? candidate, bool isSha) { Assert.Equal(isSha, ResultProcessor.ScriptLoadProcessor.IsSHA1(candidate)); } diff --git a/tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs b/tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs index 1e4d8c28e..873e93d3e 100644 --- a/tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs +++ b/tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs @@ -12,7 +12,7 @@ public class SentinelFailoverTests : SentinelBase { public SentinelFailoverTests(ITestOutputHelper output) : base(output) { } - [Fact] + [FactLongRunning] public async Task ManagedPrimaryConnectionEndToEndWithFailoverTest() { var connectionString = $"{TestConfig.Current.SentinelServer}:{TestConfig.Current.SentinelPortA},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; diff --git a/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs b/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs index ed1d995a5..241999533 100644 --- a/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs +++ b/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs @@ -11,6 +11,7 @@ public class ServerSnapshotTests [Fact] [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2012:Do not use boolean check to check if a value exists in a collection", Justification = "Explicit testing")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2013:Do not use equality check to check for collection size.", Justification = "Explicit testing")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2029:Do not use Empty() to check if a value does not exist in a collection", Justification = "Explicit testing")] public void EmptyBehaviour() { var snapshot = ServerSnapshot.Empty; @@ -52,6 +53,7 @@ public void EmptyBehaviour() [InlineData(5, 3)] [InlineData(5, 5)] [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2012:Do not use boolean check to check if a value exists in a collection", Justification = "Explicit testing")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2029:Do not use Empty() to check if a value does not exist in a collection", Justification = "Explicit testing")] public void NonEmptyBehaviour(int count, int replicaCount) { var snapshot = ServerSnapshot.Empty; diff --git a/tests/StackExchange.Redis.Tests/TransactionTests.cs b/tests/StackExchange.Redis.Tests/TransactionTests.cs index b1f676d67..db4554bef 100644 --- a/tests/StackExchange.Redis.Tests/TransactionTests.cs +++ b/tests/StackExchange.Redis.Tests/TransactionTests.cs @@ -90,7 +90,7 @@ public async Task BasicTranWithExistsCondition(bool demandKeyExists, bool keyExi [InlineData("x", null, false, true)] [InlineData(null, "y", false, true)] [InlineData(null, null, false, false)] - public async Task BasicTranWithEqualsCondition(string expected, string value, bool expectEqual, bool expectTranResult) + public async Task BasicTranWithEqualsCondition(string? expected, string? value, bool expectEqual, bool expectTranResult) { using var conn = Create(); @@ -179,7 +179,7 @@ public async Task BasicTranWithHashExistsCondition(bool demandKeyExists, bool ke [InlineData("x", null, false, true)] [InlineData(null, "y", false, true)] [InlineData(null, null, false, false)] - public async Task BasicTranWithHashEqualsCondition(string expected, string value, bool expectEqual, bool expectedTranResult) + public async Task BasicTranWithHashEqualsCondition(string? expected, string? value, bool expectEqual, bool expectedTranResult) { using var conn = Create(); @@ -290,7 +290,7 @@ public async Task BasicTranWithListExistsCondition(bool demandKeyExists, bool ke [InlineData("x", null, false, true)] [InlineData(null, "y", false, true)] [InlineData(null, null, false, false)] - public async Task BasicTranWithListEqualsCondition(string expected, string value, bool expectEqual, bool expectTranResult) + public async Task BasicTranWithListEqualsCondition(string? expected, string? value, bool expectEqual, bool expectTranResult) { using var conn = Create(); @@ -357,7 +357,7 @@ public enum ComparisonType [InlineData("", ComparisonType.GreaterThan, 0L, false)] [InlineData(null, ComparisonType.GreaterThan, 1L, false)] [InlineData(null, ComparisonType.GreaterThan, 0L, false)] - public async Task BasicTranWithStringLengthCondition(string value, ComparisonType type, long length, bool expectTranResult) + public async Task BasicTranWithStringLengthCondition(string? value, ComparisonType type, long length, bool expectTranResult) { using var conn = Create(); diff --git a/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs b/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs index 60cccdc7d..72a99f2cc 100644 --- a/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs +++ b/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs @@ -58,7 +58,7 @@ public void NullPrefixIsError_String() [InlineData("abc")] [InlineData("")] [InlineData(null)] - public void NullDatabaseIsError(string prefix) + public void NullDatabaseIsError(string? prefix) { Assert.Throws(() => { diff --git a/tests/StackExchange.Redis.Tests/xunit.runner.json b/tests/StackExchange.Redis.Tests/xunit.runner.json index 8bca1f742..99e81e741 100644 --- a/tests/StackExchange.Redis.Tests/xunit.runner.json +++ b/tests/StackExchange.Redis.Tests/xunit.runner.json @@ -1,6 +1,9 @@ { "methodDisplay": "classAndMethod", "maxParallelThreads": 16, + "parallelizeAssembly": true, + "parallelizeTestCollections": true, + "parallelAlgorithm": "aggressive", "diagnosticMessages": false, "longRunningTestSeconds": 60 } \ No newline at end of file From c0bb4eba181b30c4109d5f561fbf625bb8891fa6 Mon Sep 17 00:00:00 2001 From: atakavci <58048133+atakavci@users.noreply.github.com> Date: Sun, 18 Aug 2024 16:28:58 +0300 Subject: [PATCH 298/435] Add support for HSCAN NOVALUES (#2722) Closes/Fixes #2721 Brings new functions to the API ; - IDatabase.HashScanNoValues - IDatabase.HashScanNoValues - IDatabaseAsync.HashScanNoValuesAsync ...to enable the return type consisting of keys in the hash. Added some unit and integration tests in paralleled to what is there for `HashScan` and `HashScanAsnyc`. Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + .../Interfaces/IDatabase.cs | 14 +++ .../Interfaces/IDatabaseAsync.cs | 3 + .../KeyspaceIsolation/KeyPrefixed.cs | 3 + .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 3 + src/StackExchange.Redis/Message.cs | 37 ++++++++ .../PublicAPI/PublicAPI.Shipped.txt | 2 + src/StackExchange.Redis/RedisDatabase.cs | 35 +++++++- src/StackExchange.Redis/RedisLiterals.cs | 1 + tests/StackExchange.Redis.Tests/HashTests.cs | 85 +++++++++++++++++++ .../KeyPrefixedDatabaseTests.cs | 14 +++ .../StackExchange.Redis.Tests/NamingTests.cs | 1 + tests/StackExchange.Redis.Tests/ScanTests.cs | 52 ++++++++++++ 13 files changed, 247 insertions(+), 4 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index bf057512f..02570d3e1 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Add support for hash field expiration (see [#2715](https://github.com/StackExchange/StackExchange.Redis/issues/2715)) ([#2716 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2716])) +- Add support for `HSCAN NOVALUES` (see [#2721](https://github.com/StackExchange/StackExchange.Redis/issues/2721)) ([#2722 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2722)) ## 2.8.0 diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index c192f07f9..207c03326 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -628,6 +628,20 @@ public interface IDatabase : IRedis, IDatabaseAsync /// IEnumerable HashScan(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + /// + /// The HSCAN command is used to incrementally iterate over a hash and return only field names. + /// Note: to resume an iteration via cursor, cast the original enumerable or enumerator to . + /// + /// The key of the hash. + /// The pattern of keys to get entries for. + /// The page size to iterate by. + /// The cursor position to start at. + /// The page offset to start at. + /// The flags to use for this operation. + /// Yields all elements of the hash matching the pattern. + /// + IEnumerable HashScanNoValues(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + /// /// Sets the specified fields to their respective values in the hash stored at key. /// This command overwrites any specified fields that already exist in the hash, leaving other unspecified fields untouched. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 8600a5a1a..9852c131c 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -135,6 +135,9 @@ public interface IDatabaseAsync : IRedisAsync /// IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + /// + IAsyncEnumerable HashScanNoValuesAsync(RedisKey key, RedisValue pattern = default, int pageSize = RedisBase.CursorUtils.DefaultLibraryPageSize, long cursor = RedisBase.CursorUtils.Origin, int pageOffset = 0, CommandFlags flags = CommandFlags.None); + /// Task HashSetAsync(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index f18e74512..b97bba73b 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -135,6 +135,9 @@ public Task HashRandomFieldsWithValuesAsync(RedisKey key, long coun public IAsyncEnumerable HashScanAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => Inner.HashScanAsync(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); + public IAsyncEnumerable HashScanNoValuesAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => + Inner.HashScanNoValuesAsync(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); + public Task HashSetAsync(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.HashSetAsync(ToInner(key), hashField, value, when, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index 8f570edd6..75d93d0f9 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -721,6 +721,9 @@ IEnumerable IDatabase.HashScan(RedisKey key, RedisValue pattern, int IEnumerable IDatabase.HashScan(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) => Inner.HashScan(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); + IEnumerable IDatabase.HashScanNoValues(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) + => Inner.HashScanNoValues(ToInner(key), pattern, pageSize, cursor, pageOffset, flags); + IEnumerable IDatabase.SetScan(RedisKey key, RedisValue pattern, int pageSize, CommandFlags flags) => Inner.SetScan(ToInner(key), pattern, pageSize, flags); diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 876f718c2..b89a6b946 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -292,6 +292,9 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) => new CommandKeyValueValueValueValueValueMessage(db, flags, command, key, value0, value1, value2, value3, value4); + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4, in RedisValue value5) => + new CommandKeyValueValueValueValueValueValueMessage(db, flags, command, key, value0, value1, value2, value3, value4, value5); + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4, in RedisValue value5, in RedisValue value6) => new CommandKeyValueValueValueValueValueValueValueMessage(db, flags, command, key, value0, value1, value2, value3, value4, value5, value6); @@ -1276,6 +1279,40 @@ protected override void WriteImpl(PhysicalConnection physical) public override int ArgCount => 6; } + private sealed class CommandKeyValueValueValueValueValueValueMessage : CommandKeyBase + { + private readonly RedisValue value0, value1, value2, value3, value4, value5; + + public CommandKeyValueValueValueValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4, in RedisValue value5) : base(db, flags, command, key) + { + value0.AssertNotNull(); + value1.AssertNotNull(); + value2.AssertNotNull(); + value3.AssertNotNull(); + value4.AssertNotNull(); + value5.AssertNotNull(); + this.value0 = value0; + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.value4 = value4; + this.value5 = value5; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, ArgCount); + physical.Write(Key); + physical.WriteBulkString(value0); + physical.WriteBulkString(value1); + physical.WriteBulkString(value2); + physical.WriteBulkString(value3); + physical.WriteBulkString(value4); + physical.WriteBulkString(value5); + } + public override int ArgCount => 7; + } + private sealed class CommandKeyValueValueValueValueValueValueValueMessage : CommandKeyBase { private readonly RedisValue value0, value1, value2, value3, value4, value5, value6; diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 7b0a515df..a24333c8e 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -581,6 +581,7 @@ StackExchange.Redis.IDatabase.HashRandomFields(StackExchange.Redis.RedisKey key, StackExchange.Redis.IDatabase.HashRandomFieldsWithValues(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.HashEntry[]! StackExchange.Redis.IDatabase.HashScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! StackExchange.Redis.IDatabase.HashScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern, int pageSize, StackExchange.Redis.CommandFlags flags) -> System.Collections.Generic.IEnumerable! +StackExchange.Redis.IDatabase.HashScanNoValues(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! StackExchange.Redis.IDatabase.HashSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void StackExchange.Redis.IDatabase.HashSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.HashStringLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long @@ -818,6 +819,7 @@ StackExchange.Redis.IDatabaseAsync.HashRandomFieldAsync(StackExchange.Redis.Redi StackExchange.Redis.IDatabaseAsync.HashRandomFieldsAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashRandomFieldsWithValuesAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! +StackExchange.Redis.IDatabaseAsync.HashScanNoValuesAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue pattern = default(StackExchange.Redis.RedisValue), int pageSize = 250, long cursor = 0, int pageOffset = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! StackExchange.Redis.IDatabaseAsync.HashSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.RedisValue value, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashStringLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 0e5eada53..7468bdb64 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -624,6 +624,23 @@ private CursorEnumerable HashScanAsync(RedisKey key, RedisValue patte throw ExceptionFactory.NotSupported(true, RedisCommand.HSCAN); } + IEnumerable IDatabase.HashScanNoValues(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) + => HashScanNoValuesAsync(key, pattern, pageSize, cursor, pageOffset, flags); + + IAsyncEnumerable IDatabaseAsync.HashScanNoValuesAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) + => HashScanNoValuesAsync(key, pattern, pageSize, cursor, pageOffset, flags); + + private CursorEnumerable HashScanNoValuesAsync(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags) + { + var scan = TryScan(key, pattern, pageSize, cursor, pageOffset, flags, RedisCommand.HSCAN, SetScanResultProcessor.Default, out var server, true); + if (scan != null) return scan; + + if (cursor != 0) throw ExceptionFactory.NoCursor(RedisCommand.HKEYS); + + if (pattern.IsNull) return CursorEnumerable.From(this, server, HashKeysAsync(key, flags), pageOffset); + throw ExceptionFactory.NotSupported(true, RedisCommand.HSCAN); + } + public bool HashSet(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None) { WhenAlwaysOrNotExists(when); @@ -4679,7 +4696,7 @@ private Message GetStringSetAndGetMessage( _ => throw new ArgumentOutOfRangeException(nameof(operation)), }; - private CursorEnumerable? TryScan(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags, RedisCommand command, ResultProcessor.ScanResult> processor, out ServerEndPoint? server) + private CursorEnumerable? TryScan(RedisKey key, RedisValue pattern, int pageSize, long cursor, int pageOffset, CommandFlags flags, RedisCommand command, ResultProcessor.ScanResult> processor, out ServerEndPoint? server, bool noValues = false) { server = null; if (pageSize <= 0) @@ -4690,7 +4707,7 @@ private Message GetStringSetAndGetMessage( if (!features.Scan) return null; if (CursorUtils.IsNil(pattern)) pattern = (byte[]?)null; - return new ScanEnumerable(this, server, key, pattern, pageSize, cursor, pageOffset, flags, command, processor); + return new ScanEnumerable(this, server, key, pattern, pageSize, cursor, pageOffset, flags, command, processor, noValues); } private Message GetLexMessage(RedisCommand command, RedisKey key, RedisValue min, RedisValue max, Exclude exclude, long skip, long take, CommandFlags flags) @@ -4783,6 +4800,7 @@ internal class ScanEnumerable : CursorEnumerable private readonly RedisKey key; private readonly RedisValue pattern; private readonly RedisCommand command; + private readonly bool noValues; public ScanEnumerable( RedisDatabase database, @@ -4794,19 +4812,28 @@ public ScanEnumerable( int pageOffset, CommandFlags flags, RedisCommand command, - ResultProcessor processor) + ResultProcessor processor, + bool noValues) : base(database, server, database.Database, pageSize, cursor, pageOffset, flags) { this.key = key; this.pattern = pattern; this.command = command; Processor = processor; + this.noValues = noValues; } private protected override ResultProcessor.ScanResult> Processor { get; } private protected override Message CreateMessage(in RedisValue cursor) { + if (noValues) + { + if (CursorUtils.IsNil(pattern) && pageSize == CursorUtils.DefaultRedisPageSize) return Message.Create(db, flags, command, key, cursor, RedisLiterals.NOVALUES); + if (CursorUtils.IsNil(pattern)) return Message.Create(db, flags, command, key, cursor, RedisLiterals.COUNT, pageSize, RedisLiterals.NOVALUES); + return Message.Create(db, flags, command, key, cursor, RedisLiterals.MATCH, pattern, RedisLiterals.COUNT, pageSize, RedisLiterals.NOVALUES); + } + if (CursorUtils.IsNil(pattern)) { if (pageSize == CursorUtils.DefaultRedisPageSize) @@ -4826,7 +4853,7 @@ private protected override Message CreateMessage(in RedisValue cursor) } else { - return Message.Create(db, flags, command, key, new RedisValue[] { cursor, RedisLiterals.MATCH, pattern, RedisLiterals.COUNT, pageSize }); + return Message.Create(db, flags, command, key, cursor, RedisLiterals.MATCH, pattern, RedisLiterals.COUNT, pageSize); } } } diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index a076ff3fe..549691fd2 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -113,6 +113,7 @@ public static readonly RedisValue NODES = "NODES", NOSAVE = "NOSAVE", NOT = "NOT", + NOVALUES = "NOVALUES", NUMPAT = "NUMPAT", NUMSUB = "NUMSUB", NX = "NX", diff --git a/tests/StackExchange.Redis.Tests/HashTests.cs b/tests/StackExchange.Redis.Tests/HashTests.cs index 31f3fa9f6..b949f5911 100644 --- a/tests/StackExchange.Redis.Tests/HashTests.cs +++ b/tests/StackExchange.Redis.Tests/HashTests.cs @@ -126,6 +126,91 @@ public void Scan() Assert.Equal("ghi=jkl", string.Join(",", v4.Select(pair => pair.Name + "=" + pair.Value))); } + [Fact] + public async Task ScanNoValuesAsync() + { + using var conn = Create(require: RedisFeatures.v7_4_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key); + for (int i = 0; i < 200; i++) + { + await db.HashSetAsync(key, "key" + i, "value " + i); + } + + int count = 0; + // works for async + await foreach (var _ in db.HashScanNoValuesAsync(key, pageSize: 20)) + { + count++; + } + Assert.Equal(200, count); + + // and sync=>async (via cast) + count = 0; + await foreach (var _ in (IAsyncEnumerable)db.HashScanNoValues(key, pageSize: 20)) + { + count++; + } + Assert.Equal(200, count); + + // and sync (native) + count = 0; + foreach (var _ in db.HashScanNoValues(key, pageSize: 20)) + { + count++; + } + Assert.Equal(200, count); + + // and async=>sync (via cast) + count = 0; + foreach (var _ in (IEnumerable)db.HashScanNoValuesAsync(key, pageSize: 20)) + { + count++; + } + Assert.Equal(200, count); + } + + [Fact] + public void ScanNoValues() + { + using var conn = Create(require: RedisFeatures.v7_4_0_rc1); + + var db = conn.GetDatabase(); + + var key = Me(); + db.KeyDeleteAsync(key); + db.HashSetAsync(key, "abc", "def"); + db.HashSetAsync(key, "ghi", "jkl"); + db.HashSetAsync(key, "mno", "pqr"); + + var t1 = db.HashScanNoValues(key); + var t2 = db.HashScanNoValues(key, "*h*"); + var t3 = db.HashScanNoValues(key); + var t4 = db.HashScanNoValues(key, "*h*"); + + var v1 = t1.ToArray(); + var v2 = t2.ToArray(); + var v3 = t3.ToArray(); + var v4 = t4.ToArray(); + + Assert.Equal(3, v1.Length); + Assert.Single(v2); + Assert.Equal(3, v3.Length); + Assert.Single(v4); + + Array.Sort(v1); + Array.Sort(v2); + Array.Sort(v3); + Array.Sort(v4); + + Assert.Equal(new RedisValue[] { "abc", "ghi", "mno" }, v1); + Assert.Equal(new RedisValue[] { "ghi" }, v2); + Assert.Equal(new RedisValue[] { "abc", "ghi", "mno" }, v3); + Assert.Equal(new RedisValue[] { "ghi" }, v4); + } + [Fact] public void TestIncrementOnHashThatDoesntExist() { diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs index 587d5a0da..f467aca24 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs @@ -162,6 +162,20 @@ public void HashScan_Full() mock.Received().HashScan("prefix:key", "pattern", 123, 42, 64, CommandFlags.None); } + [Fact] + public void HashScanNoValues() + { + prefixed.HashScanNoValues("key", "pattern", 123, flags: CommandFlags.None); + mock.Received().HashScanNoValues("prefix:key", "pattern", 123, flags: CommandFlags.None); + } + + [Fact] + public void HashScanNoValues_Full() + { + prefixed.HashScanNoValues("key", "pattern", 123, 42, 64, flags: CommandFlags.None); + mock.Received().HashScanNoValues("prefix:key", "pattern", 123, 42, 64, CommandFlags.None); + } + [Fact] public void HashSet_1() { diff --git a/tests/StackExchange.Redis.Tests/NamingTests.cs b/tests/StackExchange.Redis.Tests/NamingTests.cs index 3f34ada64..2990e04c4 100644 --- a/tests/StackExchange.Redis.Tests/NamingTests.cs +++ b/tests/StackExchange.Redis.Tests/NamingTests.cs @@ -115,6 +115,7 @@ private static bool IgnoreMethodConventions(MethodInfo method) case nameof(IDatabase.SetScan): case nameof(IDatabase.SortedSetScan): case nameof(IDatabase.HashScan): + case nameof(IDatabase.HashScanNoValues): case nameof(ISubscriber.SubscribedEndpoint): return true; } diff --git a/tests/StackExchange.Redis.Tests/ScanTests.cs b/tests/StackExchange.Redis.Tests/ScanTests.cs index 4524aed9f..a95435e4d 100644 --- a/tests/StackExchange.Redis.Tests/ScanTests.cs +++ b/tests/StackExchange.Redis.Tests/ScanTests.cs @@ -332,6 +332,58 @@ public void HashScanLarge(int pageSize) Assert.Equal(2000, count); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void HashScanNoValues(bool supported) + { + string[]? disabledCommands = supported ? null : new[] { "hscan" }; + + using var conn = Create(require: RedisFeatures.v7_4_0_rc1, disabledCommands: disabledCommands); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + db.HashSet(key, "a", "1", flags: CommandFlags.FireAndForget); + db.HashSet(key, "b", "2", flags: CommandFlags.FireAndForget); + db.HashSet(key, "c", "3", flags: CommandFlags.FireAndForget); + + var arr = db.HashScanNoValues(key).ToArray(); + Assert.Equal(3, arr.Length); + Assert.True(arr.Any(x => x == "a"), "a"); + Assert.True(arr.Any(x => x == "b"), "b"); + Assert.True(arr.Any(x => x == "c"), "c"); + + var basic = db.HashGetAll(key).ToDictionary(); + Assert.Equal(3, basic.Count); + Assert.Equal(1, (long)basic["a"]); + Assert.Equal(2, (long)basic["b"]); + Assert.Equal(3, (long)basic["c"]); + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + [InlineData(1000)] + [InlineData(10000)] + public void HashScanNoValuesLarge(int pageSize) + { + using var conn = Create(require: RedisFeatures.v7_4_0_rc1); + + RedisKey key = Me() + pageSize; + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + + for (int i = 0; i < 2000; i++) + { + db.HashSet(key, "k" + i, "v" + i, flags: CommandFlags.FireAndForget); + } + + int count = db.HashScanNoValues(key, pageSize: pageSize).Count(); + Assert.Equal(2000, count); + } + /// /// See . /// From fe40d17167ec25b16b1d6d6f91b9bea53276d854 Mon Sep 17 00:00:00 2001 From: Chuck Date: Sun, 18 Aug 2024 19:38:24 -0400 Subject: [PATCH 299/435] Fix #2763: ConnectionMultiplexer.Subscription is not Thread-safe (#2769) ## Issue #2763 ## Solution Simply added a lock around `_handlers` in `ConnectionMultiplexer.Subscription`, like I was suggesting in the issue. ## Unit Test I added one that does exactly what the example code in #2763 was doing & testing for. I used the other tests as template/guide, let me know if something isn't up to spec. --------- Co-authored-by: Nick Craver --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/RedisSubscriber.cs | 16 +++++-- .../Issues/Issue2763Tests.cs | 46 +++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/Issues/Issue2763Tests.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 02570d3e1..26d277442 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: - Add support for hash field expiration (see [#2715](https://github.com/StackExchange/StackExchange.Redis/issues/2715)) ([#2716 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2716])) - Add support for `HSCAN NOVALUES` (see [#2721](https://github.com/StackExchange/StackExchange.Redis/issues/2721)) ([#2722 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2722)) +- Fix [#2763](https://github.com/StackExchange/StackExchange.Redis/issues/2763): Make ConnectionMultiplexer.Subscription thread-safe ([#2769 by Chuck-EP](https://github.com/StackExchange/StackExchange.Redis/pull/2769)) ## 2.8.0 diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index 92c96ad6c..ee28f4c56 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -159,6 +159,7 @@ internal enum SubscriptionAction internal sealed class Subscription { private Action? _handlers; + private readonly object _handlersLock = new object(); private ChannelMessageQueue? _queues; private ServerEndPoint? CurrentServer; public CommandFlags Flags { get; } @@ -206,7 +207,10 @@ public void Add(Action? handler, ChannelMessageQueue? { if (handler != null) { - _handlers += handler; + lock (_handlersLock) + { + _handlers += handler; + } } if (queue != null) { @@ -218,7 +222,10 @@ public bool Remove(Action? handler, ChannelMessageQueu { if (handler != null) { - _handlers -= handler; + lock (_handlersLock) + { + _handlers -= handler; + } } if (queue != null) { @@ -236,7 +243,10 @@ public bool Remove(Action? handler, ChannelMessageQueu internal void MarkCompleted() { - _handlers = null; + lock (_handlersLock) + { + _handlers = null; + } ChannelMessageQueue.MarkAllCompleted(ref _queues); } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2763Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2763Tests.cs new file mode 100644 index 000000000..4da997e7d --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2763Tests.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests.Issues +{ + public class Issue2763Tests : TestBase + { + public Issue2763Tests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Execute() + { + using var conn = Create(); + var subscriber = conn.GetSubscriber(); + + static void Handler(RedisChannel c, RedisValue v) { } + + const int COUNT = 1000; + RedisChannel channel = RedisChannel.Literal("CHANNEL:TEST"); + + List subscribes = new List(COUNT); + for (int i = 0; i < COUNT; i++) + subscribes.Add(() => subscriber.Subscribe(channel, Handler)); + Parallel.ForEach(subscribes, action => action()); + + Assert.Equal(COUNT, CountSubscriptionsForChannel(subscriber, channel)); + + List unsubscribes = new List(COUNT); + for (int i = 0; i < COUNT; i++) + unsubscribes.Add(() => subscriber.Unsubscribe(channel, Handler)); + Parallel.ForEach(unsubscribes, action => action()); + + Assert.Equal(0, CountSubscriptionsForChannel(subscriber, channel)); + } + + private static int CountSubscriptionsForChannel(ISubscriber subscriber, RedisChannel channel) + { + ConnectionMultiplexer connMultiplexer = (ConnectionMultiplexer)subscriber.Multiplexer; + connMultiplexer.GetSubscriberCounts(channel, out int handlers, out int _); + return handlers; + } + } +} From e151cd5c8046bb511f144db1999d32b205fef20f Mon Sep 17 00:00:00 2001 From: Weihan Li <7604648+WeihanLi@users.noreply.github.com> Date: Sat, 24 Aug 2024 23:50:39 +0800 Subject: [PATCH 300/435] docs: fix a PR link (#2776) --- docs/ReleaseNotes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 26d277442..1e72da3db 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -14,7 +14,7 @@ Current package versions: ## 2.8.0 -- Add high-integrity mode ([docs](https://stackexchange.github.io/StackExchange.Redis/Configuration), [#2471 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2741])) +- Add high-integrity mode ([docs](https://stackexchange.github.io/StackExchange.Redis/Configuration), [#2471 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2741)) - TLS certificate/`TrustIssuer`: Check EKU in X509 chain checks when validating certificates ([#2670 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2670)) ## 2.7.33 From b2b4a88680d7bc43869d6779e0c00ac14bf919da Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 3 Sep 2024 11:13:31 -0400 Subject: [PATCH 301/435] Fix #2778: Run CheckInfoReplication even with HeartbeatConsistencyChecks (#2784) * Fix #2778: Run CheckInfoReplication even with HeartbeatConsistencyChecks This is an issue identified in ##2778 and #2779 where we're not updating replication topology if heartbeat consistency checks are enabled because we exit the `if` structure early. This still runs that check if it's due in both cases, without changing the behavior of the result processor. * Add release notes --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/PhysicalBridge.cs | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 1e72da3db..d19287ee9 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -11,6 +11,7 @@ Current package versions: - Add support for hash field expiration (see [#2715](https://github.com/StackExchange/StackExchange.Redis/issues/2715)) ([#2716 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2716])) - Add support for `HSCAN NOVALUES` (see [#2721](https://github.com/StackExchange/StackExchange.Redis/issues/2721)) ([#2722 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2722)) - Fix [#2763](https://github.com/StackExchange/StackExchange.Redis/issues/2763): Make ConnectionMultiplexer.Subscription thread-safe ([#2769 by Chuck-EP](https://github.com/StackExchange/StackExchange.Redis/pull/2769)) +- Fix [#2778](https://github.com/StackExchange/StackExchange.Redis/issues/2778): Run `CheckInfoReplication` even with `HeartbeatConsistencyChecks` ([#2784 by NickCraver and leachdaniel-clark](https://github.com/StackExchange/StackExchange.Redis/pull/2784)) ## 2.8.0 diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 0755c939e..6cc8aa6f6 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -606,8 +606,8 @@ internal void OnHeartbeat(bool ifConnectedOnly) tmp.BridgeCouldBeNull?.ServerEndPoint?.ClearUnselectable(UnselectableFlags.DidNotRespond); } int timedOutThisHeartbeat = tmp.OnBridgeHeartbeat(); - int writeEverySeconds = ServerEndPoint.WriteEverySeconds, - checkConfigSeconds = ServerEndPoint.ConfigCheckSeconds; + int writeEverySeconds = ServerEndPoint.WriteEverySeconds; + bool configCheckDue = ServerEndPoint.ConfigCheckSeconds > 0 && ServerEndPoint.LastInfoReplicationCheckSecondsAgo >= ServerEndPoint.ConfigCheckSeconds; if (state == (int)State.ConnectedEstablished && ConnectionType == ConnectionType.Interactive && tmp.BridgeCouldBeNull?.Multiplexer.RawConfig.HeartbeatConsistencyChecks == true) @@ -617,9 +617,15 @@ internal void OnHeartbeat(bool ifConnectedOnly) // If we don't get the expected response to that command, then the connection is terminated. // This is to prevent the case of things like 100% string command usage where a protocol error isn't otherwise encountered. KeepAlive(forceRun: true); + + // If we're configured to check info replication, perform that too + if (configCheckDue) + { + ServerEndPoint.CheckInfoReplication(); + } } else if (state == (int)State.ConnectedEstablished && ConnectionType == ConnectionType.Interactive - && checkConfigSeconds > 0 && ServerEndPoint.LastInfoReplicationCheckSecondsAgo >= checkConfigSeconds + && configCheckDue && ServerEndPoint.CheckInfoReplication()) { // that serves as a keep-alive, if it is accepted From c8d2c28992e6c13053858efc2b04148dd7429899 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 3 Sep 2024 11:51:31 -0400 Subject: [PATCH 302/435] Adding 2.8.12 release notes --- docs/ReleaseNotes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index d19287ee9..af78f85cc 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,10 @@ Current package versions: ## Unreleased +No pending unreleased changes. + +## 2.8.12 + - Add support for hash field expiration (see [#2715](https://github.com/StackExchange/StackExchange.Redis/issues/2715)) ([#2716 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2716])) - Add support for `HSCAN NOVALUES` (see [#2721](https://github.com/StackExchange/StackExchange.Redis/issues/2721)) ([#2722 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2722)) - Fix [#2763](https://github.com/StackExchange/StackExchange.Redis/issues/2763): Make ConnectionMultiplexer.Subscription thread-safe ([#2769 by Chuck-EP](https://github.com/StackExchange/StackExchange.Redis/pull/2769)) From 4d94342ef4ef01fa0aca9acb61c17616aeca13a2 Mon Sep 17 00:00:00 2001 From: Diogo Barbosa Date: Mon, 9 Sep 2024 17:03:05 +0200 Subject: [PATCH 303/435] Update command map for Envoy (#2794) * Update command map for Envoy * Release notes * Update format to match other release notes --- docs/ReleaseNotes.md | 2 +- src/StackExchange.Redis/CommandMap.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index af78f85cc..5363e6965 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -No pending unreleased changes. +- Fix [#2793](https://github.com/StackExchange/StackExchange.Redis/issues/2793): Update Envoyproxy's command map according to latest Envoy documentation ([#2794 by dbarbosapn](https://github.com/StackExchange/StackExchange.Redis/pull/2794)) ## 2.8.12 diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index 67b6f1a9e..a5f1aa68f 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -57,13 +57,13 @@ public sealed class CommandMap RedisCommand.BLPOP, RedisCommand.BRPOP, RedisCommand.BRPOPLPUSH, // yeah, me neither! - RedisCommand.PSUBSCRIBE, RedisCommand.PUBLISH, RedisCommand.PUNSUBSCRIBE, RedisCommand.SUBSCRIBE, RedisCommand.UNSUBSCRIBE, + RedisCommand.PSUBSCRIBE, RedisCommand.PUNSUBSCRIBE, RedisCommand.SUBSCRIBE, RedisCommand.UNSUBSCRIBE, - RedisCommand.DISCARD, RedisCommand.EXEC, RedisCommand.MULTI, RedisCommand.UNWATCH, RedisCommand.WATCH, + RedisCommand.UNWATCH, RedisCommand.SCRIPT, - RedisCommand.ECHO, RedisCommand.QUIT, RedisCommand.SELECT, + RedisCommand.SELECT, RedisCommand.BGREWRITEAOF, RedisCommand.BGSAVE, RedisCommand.CLIENT, RedisCommand.CLUSTER, RedisCommand.CONFIG, RedisCommand.DBSIZE, RedisCommand.DEBUG, RedisCommand.FLUSHALL, RedisCommand.FLUSHDB, RedisCommand.INFO, RedisCommand.LASTSAVE, RedisCommand.MONITOR, RedisCommand.REPLICAOF, From 654859f7f5e0e35694f4f65a49c9e65ee122a0dd Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 9 Sep 2024 11:17:40 -0400 Subject: [PATCH 304/435] Release notes for 2.8.14 --- docs/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 5363e6965..9dff1c3e0 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,6 +7,9 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased +No pending unreleased changes. + +## 2.8.14 - Fix [#2793](https://github.com/StackExchange/StackExchange.Redis/issues/2793): Update Envoyproxy's command map according to latest Envoy documentation ([#2794 by dbarbosapn](https://github.com/StackExchange/StackExchange.Redis/pull/2794)) From 322c7044d3a056c150099be09b5aa638987a48c8 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 10 Sep 2024 11:54:47 -0400 Subject: [PATCH 305/435] Always perform "last read" check in heartbeat when HeartbeatConsistencyChecks is enabled (#2795) When we have slowly adding things to the heartbeat (originally intended just to send data to keep connections alive) like detecting connection health, the if/else has gotten more complicated. With the addition of HeartbeatConsistencyChecks, we prevented some fall throughs to later checks which means that if that option is enabled, we were no longer detecting dead sockets as intended. This is a tactical fix for the combination, but I think overall we should look at refactoring how this entire method works because shoehorning these things into the original structure/purpose has been problematic several times. --- docs/ReleaseNotes.md | 3 ++- src/StackExchange.Redis/PhysicalBridge.cs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 9dff1c3e0..74fb39034 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,7 +7,8 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased -No pending unreleased changes. + +- Fix: PhysicalBridge: Always perform "last read" check in heartbeat when `HeartbeatConsistencyChecks` is enabled ([#2795 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2795)) ## 2.8.14 diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 6cc8aa6f6..e063233b5 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -652,7 +652,9 @@ internal void OnHeartbeat(bool ifConnectedOnly) // so if we have an empty unsent queue and a non-empty sent queue, test the socket. KeepAlive(); } - else if (timedOutThisHeartbeat > 0 + + // This is an "always" check - we always want to evaluate a dead connection from a non-responsive sever regardless of the need to heartbeat above + if (timedOutThisHeartbeat > 0 && tmp.LastReadSecondsAgo * 1_000 > (tmp.BridgeCouldBeNull?.Multiplexer.AsyncTimeoutMilliseconds * 4)) { // If we've received *NOTHING* on the pipe in 4 timeouts worth of time and we're timing out commands, issue a connection failure so that we reconnect From d7ca83259a868928353088a0435630a850a71c0f Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 10 Sep 2024 12:12:27 -0400 Subject: [PATCH 306/435] Add release notes for 2.8.16 --- docs/ReleaseNotes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 74fb39034..5ed2d6cfa 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,10 @@ Current package versions: ## Unreleased +No pending unreleased changes. + +## 2.8.16 + - Fix: PhysicalBridge: Always perform "last read" check in heartbeat when `HeartbeatConsistencyChecks` is enabled ([#2795 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2795)) ## 2.8.14 From c8a7265d475c24834ed3497140cdf98bbfd50975 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 31 Oct 2024 15:59:00 +0000 Subject: [PATCH 307/435] format ipv6 endpoints correctly (#2813) --- src/StackExchange.Redis/Format.cs | 15 +++++- .../ConnectionFailedErrorsTests.cs | 4 +- .../FailoverTests.cs | 10 ++-- .../StackExchange.Redis.Tests/FormatTests.cs | 52 +++++++++++-------- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/src/StackExchange.Redis/Format.cs b/src/StackExchange.Redis/Format.cs index 6836a70da..329da4363 100644 --- a/src/StackExchange.Redis/Format.cs +++ b/src/StackExchange.Redis/Format.cs @@ -97,7 +97,20 @@ internal static string ToString(EndPoint? endpoint) if (dns.Port == 0) return dns.Host; return dns.Host + ":" + Format.ToString(dns.Port); case IPEndPoint ip: - if (ip.Port == 0) return ip.Address.ToString(); + var addr = ip.Address.ToString(); + + if (ip.Port == 0) + { + // no port specified; use naked IP + return addr; + } + + if (addr.IndexOf(':') >= 0) + { + // ipv6 with port; use "[IP]:port" notation + return "[" + addr + "]:" + Format.ToString(ip.Port); + } + // ipv4 with port; use "IP:port" notation return ip.Address + ":" + Format.ToString(ip.Port); #if UNIX_SOCKET case UnixDomainSocketEndPoint uds: diff --git a/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs b/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs index df3802118..d359bd4b4 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs @@ -178,8 +178,8 @@ public async Task CheckFailureRecovered() { using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, log: Writer, shared: false); - await RunBlockingSynchronousWithExtraThreadAsync(innerScenario).ForAwait(); - void innerScenario() + await RunBlockingSynchronousWithExtraThreadAsync(InnerScenario).ForAwait(); + void InnerScenario() { conn.GetDatabase(); var server = conn.GetServer(conn.GetEndPoints()[0]); diff --git a/tests/StackExchange.Redis.Tests/FailoverTests.cs b/tests/StackExchange.Redis.Tests/FailoverTests.cs index e40d12a14..fdbd23a94 100644 --- a/tests/StackExchange.Redis.Tests/FailoverTests.cs +++ b/tests/StackExchange.Redis.Tests/FailoverTests.cs @@ -208,10 +208,7 @@ public async Task SubscriptionsSurviveConnectionFailureAsync() var sub = conn.GetSubscriber(); int counter = 0; Assert.True(sub.IsConnected()); - await sub.SubscribeAsync(channel, delegate - { - Interlocked.Increment(ref counter); - }).ConfigureAwait(false); + await sub.SubscribeAsync(channel, (arg1, arg2) => Interlocked.Increment(ref counter)).ConfigureAwait(false); var profile1 = Log(profiler); @@ -257,8 +254,7 @@ await sub.SubscribeAsync(channel, delegate // Ensure we've sent the subscribe command after reconnecting var profile2 = Log(profiler); - //Assert.Equal(1, profile2.Count(p => p.Command == nameof(RedisCommand.SUBSCRIBE))); - + // Assert.Equal(1, profile2.Count(p => p.Command == nameof(RedisCommand.SUBSCRIBE))); Log("Issuing ping after reconnected"); sub.Ping(); @@ -395,7 +391,7 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() Log(" IsReplica: " + !server.IsReplica); Log(" Type: " + server.ServerType); } - //Skip.Inconclusive("Not enough latency."); + // Skip.Inconclusive("Not enough latency."); } Assert.True(sanityCheck, $"B Connection: {TestConfig.Current.FailoverPrimaryServerAndPort} should be a replica"); Assert.False(bConn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"B Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a primary"); diff --git a/tests/StackExchange.Redis.Tests/FormatTests.cs b/tests/StackExchange.Redis.Tests/FormatTests.cs index 8b3c152ed..cf7593a08 100644 --- a/tests/StackExchange.Redis.Tests/FormatTests.cs +++ b/tests/StackExchange.Redis.Tests/FormatTests.cs @@ -11,38 +11,46 @@ public class FormatTests : TestBase { public FormatTests(ITestOutputHelper output) : base(output) { } - public static IEnumerable EndpointData() + public static IEnumerable EndpointData() { + // note: the 3rd arg is for formatting; null means "expect the original string" + // DNS - yield return new object[] { "localhost", new DnsEndPoint("localhost", 0) }; - yield return new object[] { "localhost:6390", new DnsEndPoint("localhost", 6390) }; - yield return new object[] { "bob.the.builder.com", new DnsEndPoint("bob.the.builder.com", 0) }; - yield return new object[] { "bob.the.builder.com:6390", new DnsEndPoint("bob.the.builder.com", 6390) }; + yield return new object?[] { "localhost", new DnsEndPoint("localhost", 0), null }; + yield return new object?[] { "localhost:6390", new DnsEndPoint("localhost", 6390), null }; + yield return new object?[] { "bob.the.builder.com", new DnsEndPoint("bob.the.builder.com", 0), null }; + yield return new object?[] { "bob.the.builder.com:6390", new DnsEndPoint("bob.the.builder.com", 6390), null }; // IPv4 - yield return new object[] { "0.0.0.0", new IPEndPoint(IPAddress.Parse("0.0.0.0"), 0) }; - yield return new object[] { "127.0.0.1", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 0) }; - yield return new object[] { "127.1", new IPEndPoint(IPAddress.Parse("127.1"), 0) }; - yield return new object[] { "127.1:6389", new IPEndPoint(IPAddress.Parse("127.1"), 6389) }; - yield return new object[] { "127.0.0.1:6389", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 6389) }; - yield return new object[] { "127.0.0.1:1", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1) }; - yield return new object[] { "127.0.0.1:2", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 2) }; - yield return new object[] { "10.10.9.18:2", new IPEndPoint(IPAddress.Parse("10.10.9.18"), 2) }; + yield return new object?[] { "0.0.0.0", new IPEndPoint(IPAddress.Parse("0.0.0.0"), 0), null }; + yield return new object?[] { "127.0.0.1", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 0), null }; + yield return new object?[] { "127.1", new IPEndPoint(IPAddress.Parse("127.1"), 0), "127.0.0.1" }; + yield return new object?[] { "127.1:6389", new IPEndPoint(IPAddress.Parse("127.1"), 6389), "127.0.0.1:6389" }; + yield return new object?[] { "127.0.0.1:6389", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 6389), null }; + yield return new object?[] { "127.0.0.1:1", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1), null }; + yield return new object?[] { "127.0.0.1:2", new IPEndPoint(IPAddress.Parse("127.0.0.1"), 2), null }; + yield return new object?[] { "10.10.9.18:2", new IPEndPoint(IPAddress.Parse("10.10.9.18"), 2), null }; // IPv6 - yield return new object[] { "::1", new IPEndPoint(IPAddress.Parse("::1"), 0) }; - yield return new object[] { "::1:6379", new IPEndPoint(IPAddress.Parse("::0.1.99.121"), 0) }; // remember your brackets! - yield return new object[] { "[::1]:6379", new IPEndPoint(IPAddress.Parse("::1"), 6379) }; - yield return new object[] { "[::1]", new IPEndPoint(IPAddress.Parse("::1"), 0) }; - yield return new object[] { "[::1]:1000", new IPEndPoint(IPAddress.Parse("::1"), 1000) }; - yield return new object[] { "[2001:db7:85a3:8d2:1319:8a2e:370:7348]", new IPEndPoint(IPAddress.Parse("2001:db7:85a3:8d2:1319:8a2e:370:7348"), 0) }; - yield return new object[] { "[2001:db7:85a3:8d2:1319:8a2e:370:7348]:1000", new IPEndPoint(IPAddress.Parse("2001:db7:85a3:8d2:1319:8a2e:370:7348"), 1000) }; + yield return new object?[] { "::1", new IPEndPoint(IPAddress.Parse("::1"), 0), null }; + yield return new object?[] { "::1:6379", new IPEndPoint(IPAddress.Parse("::0.1.99.121"), 0), "::0.1.99.121" }; // remember your brackets! + yield return new object?[] { "[::1]:6379", new IPEndPoint(IPAddress.Parse("::1"), 6379), null }; + yield return new object?[] { "[::1]", new IPEndPoint(IPAddress.Parse("::1"), 0), "::1" }; + yield return new object?[] { "[::1]:1000", new IPEndPoint(IPAddress.Parse("::1"), 1000), null }; + yield return new object?[] { "2001:db7:85a3:8d2:1319:8a2e:370:7348", new IPEndPoint(IPAddress.Parse("2001:db7:85a3:8d2:1319:8a2e:370:7348"), 0), null }; + yield return new object?[] { "[2001:db7:85a3:8d2:1319:8a2e:370:7348]", new IPEndPoint(IPAddress.Parse("2001:db7:85a3:8d2:1319:8a2e:370:7348"), 0), "2001:db7:85a3:8d2:1319:8a2e:370:7348" }; + yield return new object?[] { "[2001:db7:85a3:8d2:1319:8a2e:370:7348]:1000", new IPEndPoint(IPAddress.Parse("2001:db7:85a3:8d2:1319:8a2e:370:7348"), 1000), null }; } [Theory] [MemberData(nameof(EndpointData))] - public void ParseEndPoint(string data, EndPoint expected) + public void ParseEndPoint(string data, EndPoint expected, string? expectedFormat) { - _ = Format.TryParseEndPoint(data, out var result); + Assert.True(Format.TryParseEndPoint(data, out var result)); Assert.Equal(expected, result); + + // and write again + var s = Format.ToString(result); + expectedFormat ??= data; + Assert.Equal(expectedFormat, s); } [Theory] From ebacf39712e919e10a64e824aac063956bff3b93 Mon Sep 17 00:00:00 2001 From: Philo Date: Tue, 5 Nov 2024 08:06:48 -0800 Subject: [PATCH 308/435] Update default Redis version from 4.0.0 to 6.0.0 for Azure Redis (#2810) * Update default Redis version from 4.0.0 to 6.0.0 for Azure Redis resources --- docs/ReleaseNotes.md | 2 +- src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs | 4 ++-- tests/StackExchange.Redis.Tests/ConfigTests.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 5ed2d6cfa..f6e274254 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -No pending unreleased changes. +- Update default Redis version from 4.0.0 to 6.0.0 for Azure Redis resources ([#2810 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2810)) ## 2.8.16 diff --git a/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs b/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs index 6e38e15a9..dcdb9f26a 100644 --- a/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs @@ -16,9 +16,9 @@ public class AzureOptionsProvider : DefaultOptionsProvider public override bool AbortOnConnectFail => false; /// - /// The minimum version of Redis in Azure is 4, so use the widest set of available commands when connecting. + /// The minimum version of Redis in Azure is 6, so use the widest set of available commands when connecting. /// - public override Version DefaultVersion => RedisFeatures.v4_0_0; + public override Version DefaultVersion => RedisFeatures.v6_0_0; /// /// List of domains known to be Azure Redis, so we can light up some helpful functionality diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 2a4b2bc75..75f6d25f4 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -25,7 +25,7 @@ public class ConfigTests : TestBase public ConfigTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } public Version DefaultVersion = new(3, 0, 0); - public Version DefaultAzureVersion = new(4, 0, 0); + public Version DefaultAzureVersion = new(6, 0, 0); [Fact] public void ExpectedFields() From 00711481f92c06ccd4f83886e3d2b6e70718206b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 5 Nov 2024 16:11:01 +0000 Subject: [PATCH 309/435] release notes for ipv6 --- docs/ReleaseNotes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index f6e274254..ace3e31d8 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,7 @@ Current package versions: ## Unreleased +- Format IPv6 endpoints correctly when rewriting configration strings ([#2813 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2813)) - Update default Redis version from 4.0.0 to 6.0.0 for Azure Redis resources ([#2810 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2810)) ## 2.8.16 From 11ef77d768866dae8cdbc599711ccda0c28abc33 Mon Sep 17 00:00:00 2001 From: Philo Date: Mon, 25 Nov 2024 04:47:52 -0800 Subject: [PATCH 310/435] Automatically detect and tune for Azure Managed Redis caches (#2818) Detect when connecting to a new Azure Managed Redis cache, and adjust default connection settings to align with their capabilities. More about Azure Managed Redis: https://learn.microsoft.com/azure/azure-cache-for-redis/managed-redis/managed-redis-overview --- docs/ReleaseNotes.md | 1 + .../Configuration/AzureOptionsProvider.cs | 35 +++++++++++--- .../StackExchange.Redis.Tests/ConfigTests.cs | 46 ++++++------------- .../DefaultOptionsTests.cs | 16 +++++++ 4 files changed, 60 insertions(+), 38 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index ace3e31d8..fc5cf3134 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: - Format IPv6 endpoints correctly when rewriting configration strings ([#2813 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2813)) - Update default Redis version from 4.0.0 to 6.0.0 for Azure Redis resources ([#2810 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2810)) +- Detect Azure Managed Redis caches and tune default connection settings for them ([#2818 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2818)) ## 2.8.16 diff --git a/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs b/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs index dcdb9f26a..fb01f0704 100644 --- a/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs @@ -21,7 +21,7 @@ public class AzureOptionsProvider : DefaultOptionsProvider public override Version DefaultVersion => RedisFeatures.v6_0_0; /// - /// List of domains known to be Azure Redis, so we can light up some helpful functionality + /// Lists of domains known to be Azure Redis, so we can light up some helpful functionality /// for minimizing downtime during maintenance events and such. /// private static readonly string[] azureRedisDomains = new[] @@ -29,23 +29,40 @@ public class AzureOptionsProvider : DefaultOptionsProvider ".redis.cache.windows.net", ".redis.cache.chinacloudapi.cn", ".redis.cache.usgovcloudapi.net", - ".redis.cache.cloudapi.de", ".redisenterprise.cache.azure.net", }; + private static readonly string[] azureManagedRedisDomains = new[] + { + ".redis.azure.net", + ".redis.chinacloudapi.cn", + ".redis.usgovcloudapi.net", + }; + /// public override bool IsMatch(EndPoint endpoint) { if (endpoint is DnsEndPoint dnsEp) { - foreach (var host in azureRedisDomains) + if (IsHostInDomains(dnsEp.Host, azureRedisDomains) || IsHostInDomains(dnsEp.Host, azureManagedRedisDomains)) { - if (dnsEp.Host.EndsWith(host, StringComparison.InvariantCultureIgnoreCase)) - { - return true; - } + return true; } } + + return false; + } + + private bool IsHostInDomains(string hostName, string[] domains) + { + foreach (var domain in domains) + { + if (hostName.EndsWith(domain, StringComparison.InvariantCultureIgnoreCase)) + { + return true; + } + } + return false; } @@ -65,6 +82,10 @@ public override bool GetDefaultSsl(EndPointCollection endPoints) { return true; } + if (dns.Port == 10000 && IsHostInDomains(dns.Host, azureManagedRedisDomains)) + { + return true; // SSL is enabled by default on AMR caches + } break; case IPEndPoint ip: if (ip.Port == 6380) diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 75f6d25f4..4db6b1163 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.IO.Pipelines; @@ -25,7 +26,6 @@ public class ConfigTests : TestBase public ConfigTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } public Version DefaultVersion = new(3, 0, 0); - public Version DefaultAzureVersion = new(6, 0, 0); [Fact] public void ExpectedFields() @@ -132,12 +132,21 @@ public void SslProtocols_InvalidValue() Assert.Throws(() => ConfigurationOptions.Parse("myhost,sslProtocols=InvalidSslProtocol")); } - [Fact] - public void ConfigurationOptionsDefaultForAzure() - { - var options = ConfigurationOptions.Parse("contoso.redis.cache.windows.net"); - Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); + [Theory] + [InlineData("contoso.redis.cache.windows.net:6380", true)] + [InlineData("contoso.REDIS.CACHE.chinacloudapi.cn:6380", true)] // added a few upper case chars to validate comparison + [InlineData("contoso.redis.cache.usgovcloudapi.net:6380", true)] + [InlineData("contoso.redisenterprise.cache.azure.net:10000", false)] + [InlineData("contoso.redis.azure.net:10000", true)] + [InlineData("contoso.redis.chinacloudapi.cn:10000", true)] + [InlineData("contoso.redis.usgovcloudapi.net:10000", true)] + public void ConfigurationOptionsDefaultForAzure(string hostAndPort, bool sslShouldBeEnabled) + { + Version defaultAzureVersion = new(6, 0, 0); + var options = ConfigurationOptions.Parse(hostAndPort); + Assert.True(options.DefaultVersion.Equals(defaultAzureVersion)); Assert.False(options.AbortOnConnectFail); + Assert.Equal(sslShouldBeEnabled, options.Ssl); } [Fact] @@ -148,31 +157,6 @@ public void ConfigurationOptionsForAzureWhenSpecified() Assert.True(options.AbortOnConnectFail); } - [Fact] - public void ConfigurationOptionsDefaultForAzureChina() - { - // added a few upper case chars to validate comparison - var options = ConfigurationOptions.Parse("contoso.REDIS.CACHE.chinacloudapi.cn"); - Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); - Assert.False(options.AbortOnConnectFail); - } - - [Fact] - public void ConfigurationOptionsDefaultForAzureGermany() - { - var options = ConfigurationOptions.Parse("contoso.redis.cache.cloudapi.de"); - Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); - Assert.False(options.AbortOnConnectFail); - } - - [Fact] - public void ConfigurationOptionsDefaultForAzureUSGov() - { - var options = ConfigurationOptions.Parse("contoso.redis.cache.usgovcloudapi.net"); - Assert.True(options.DefaultVersion.Equals(DefaultAzureVersion)); - Assert.False(options.AbortOnConnectFail); - } - [Fact] public void ConfigurationOptionsDefaultForNonAzure() { diff --git a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs index 74e52b96d..8c35ab3e1 100644 --- a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs +++ b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Configuration; using System.Linq; using System.Net; using System.Threading; @@ -65,6 +66,21 @@ public void IsMatchOnDomain() Assert.IsType(provider); } + [Theory] + [InlineData("contoso.redis.cache.windows.net")] + [InlineData("contoso.REDIS.CACHE.chinacloudapi.cn")] // added a few upper case chars to validate comparison + [InlineData("contoso.redis.cache.usgovcloudapi.net")] + [InlineData("contoso.redisenterprise.cache.azure.net")] + [InlineData("contoso.redis.azure.net")] + [InlineData("contoso.redis.chinacloudapi.cn")] + [InlineData("contoso.redis.usgovcloudapi.net")] + public void IsMatchOnAzureDomain(string hostName) + { + var epc = new EndPointCollection(new List() { new DnsEndPoint(hostName, 0) }); + var provider = DefaultOptionsProvider.GetProvider(epc); + Assert.IsType(provider); + } + [Fact] public void AllOverridesFromDefaultsProp() { From 33358b7a41bbe12e1ddb1b10933b3399ad5472c7 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 27 Nov 2024 07:34:23 -0500 Subject: [PATCH 311/435] Packages: update to remove CVE dependencies (#2820) * Packages: update to remove CVE dependencies This bumps *testing* (not the core package) to net8.0 for an easier time maintaining and updates packages outside StackExchange.Redis except for `Microsoft.Bcl.AsyncInterfaces`. `Microsoft.Bcl.AsyncInterfaces` was bumped from 5.0.0 to 6.0.0 due to deprecation warnings, still maintaining widest compatibility we can. * Fix .NET Framework test diff * fix enum flags rendering; involves adding a net8.0 TFM, but that's LTS *anyway*, so: fine also added appropriate [Obsolete] to respect transient net8.0 changes --------- Co-authored-by: Marc Gravell --- Directory.Packages.props | 22 +++++---- .../ConfigurationOptions.cs | 14 ++++-- src/StackExchange.Redis/Enums/CommandFlags.cs | 23 +++++++++ src/StackExchange.Redis/Exceptions.cs | 29 ++++++++++++ src/StackExchange.Redis/Obsoletions.cs | 7 +++ .../PublicAPI/net8.0/PublicAPI.Shipped.txt | 3 ++ .../StackExchange.Redis.csproj | 2 +- tests/BasicTest/BasicTest.csproj | 5 +- .../BasicTestBaseline.csproj | 4 +- tests/ConsoleTest/ConsoleTest.csproj | 2 +- .../ConsoleTestBaseline.csproj | 2 +- .../StackExchange.Redis.Tests/ConfigTests.cs | 16 +++---- .../StackExchange.Redis.Tests/FormatTests.cs | 8 +++- tests/StackExchange.Redis.Tests/SSLTests.cs | 47 ++++++++++--------- .../ServerSnapshotTests.cs | 18 +++++-- .../StackExchange.Redis.Tests.csproj | 2 +- toys/TestConsole/TestConsole.csproj | 2 +- .../TestConsoleBaseline.csproj | 2 +- 18 files changed, 149 insertions(+), 59 deletions(-) create mode 100644 src/StackExchange.Redis/Obsoletions.cs create mode 100644 src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt diff --git a/Directory.Packages.props b/Directory.Packages.props index c8a4189e6..12097728a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,8 @@ - + + @@ -9,22 +10,23 @@ - + - - - + + - + - + + + - - - + + + \ No newline at end of file diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index e972962b2..199f1a378 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -357,13 +357,17 @@ private static bool CheckTrustedIssuer(X509Certificate2 certificateToValidate, X byte[] authorityData = authority.RawData; foreach (var chainElement in chain.ChainElements) { -#if NET8_0_OR_GREATER -#error TODO: use RawDataMemory (needs testing) -#endif using var chainCert = chainElement.Certificate; - if (!found && chainCert.RawData.SequenceEqual(authorityData)) + if (!found) { - found = true; +#if NET8_0_OR_GREATER + if (chainCert.RawDataMemory.Span.SequenceEqual(authorityData)) +#else + if (chainCert.RawData.SequenceEqual(authorityData)) +#endif + { + found = true; + } } } return found; diff --git a/src/StackExchange.Redis/Enums/CommandFlags.cs b/src/StackExchange.Redis/Enums/CommandFlags.cs index bafaee70f..83331a3c5 100644 --- a/src/StackExchange.Redis/Enums/CommandFlags.cs +++ b/src/StackExchange.Redis/Enums/CommandFlags.cs @@ -34,11 +34,22 @@ public enum CommandFlags /// PreferMaster = 0, +#if NET8_0_OR_GREATER + /// + /// This operation should be performed on the replica if it is available, but will be performed on + /// a primary if no replicas are available. Suitable for read operations only. + /// + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(PreferReplica) + " instead, this will be removed in 3.0.")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + PreferSlave = 8, +#endif + /// /// This operation should only be performed on the primary. /// DemandMaster = 4, +#if !NET8_0_OR_GREATER /// /// This operation should be performed on the replica if it is available, but will be performed on /// a primary if no replicas are available. Suitable for read operations only. @@ -46,6 +57,7 @@ public enum CommandFlags [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(PreferReplica) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] PreferSlave = 8, +#endif /// /// This operation should be performed on the replica if it is available, but will be performed on @@ -53,17 +65,28 @@ public enum CommandFlags /// PreferReplica = 8, // note: we're using a 2-bit set here, which [Flags] formatting hates; position is doing the best we can for reasonable outcomes here +#if NET8_0_OR_GREATER + /// + /// This operation should only be performed on a replica. Suitable for read operations only. + /// + [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(DemandReplica) + " instead, this will be removed in 3.0.")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + DemandSlave = 12, +#endif + /// /// This operation should only be performed on a replica. Suitable for read operations only. /// DemandReplica = 12, // note: we're using a 2-bit set here, which [Flags] formatting hates; position is doing the best we can for reasonable outcomes here +#if !NET8_0_OR_GREATER /// /// This operation should only be performed on a replica. Suitable for read operations only. /// [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(DemandReplica) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] DemandSlave = 12, +#endif // 16: reserved for additional "demand/prefer" options diff --git a/src/StackExchange.Redis/Exceptions.cs b/src/StackExchange.Redis/Exceptions.cs index 9315eb806..1f1c973ce 100644 --- a/src/StackExchange.Redis/Exceptions.cs +++ b/src/StackExchange.Redis/Exceptions.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Runtime.Serialization; namespace StackExchange.Redis @@ -22,6 +23,10 @@ public RedisCommandException(string message) : base(message) { } /// The inner exception. public RedisCommandException(string message, Exception innerException) : base(message, innerException) { } +#if NET8_0_OR_GREATER + [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId)] + [EditorBrowsable(EditorBrowsableState.Never)] +#endif private RedisCommandException(SerializationInfo info, StreamingContext ctx) : base(info, ctx) { } } @@ -46,6 +51,10 @@ public RedisTimeoutException(string message, CommandStatus commandStatus) : base /// public CommandStatus Commandstatus { get; } +#if NET8_0_OR_GREATER + [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId)] + [EditorBrowsable(EditorBrowsableState.Never)] +#endif private RedisTimeoutException(SerializationInfo info, StreamingContext ctx) : base(info, ctx) { Commandstatus = info.GetValue("commandStatus", typeof(CommandStatus)) as CommandStatus? ?? CommandStatus.Unknown; @@ -56,6 +65,10 @@ private RedisTimeoutException(SerializationInfo info, StreamingContext ctx) : ba /// /// Serialization info. /// Serialization context. +#if NET8_0_OR_GREATER + [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId)] + [EditorBrowsable(EditorBrowsableState.Never)] +#endif public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); @@ -107,6 +120,10 @@ public RedisConnectionException(ConnectionFailureType failureType, string messag /// public CommandStatus CommandStatus { get; } +#if NET8_0_OR_GREATER + [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId)] + [EditorBrowsable(EditorBrowsableState.Never)] +#endif private RedisConnectionException(SerializationInfo info, StreamingContext ctx) : base(info, ctx) { FailureType = (ConnectionFailureType)info.GetInt32("failureType"); @@ -118,6 +135,10 @@ private RedisConnectionException(SerializationInfo info, StreamingContext ctx) : /// /// Serialization info. /// Serialization context. +#if NET8_0_OR_GREATER + [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId)] + [EditorBrowsable(EditorBrowsableState.Never)] +#endif public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); @@ -150,6 +171,10 @@ public RedisException(string message, Exception? innerException) : base(message, /// /// Serialization info. /// Serialization context. +#if NET8_0_OR_GREATER + [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId)] + [EditorBrowsable(EditorBrowsableState.Never)] +#endif protected RedisException(SerializationInfo info, StreamingContext ctx) : base(info, ctx) { } } @@ -165,6 +190,10 @@ public sealed partial class RedisServerException : RedisException /// The message for the exception. public RedisServerException(string message) : base(message) { } +#if NET8_0_OR_GREATER + [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId)] + [EditorBrowsable(EditorBrowsableState.Never)] +#endif private RedisServerException(SerializationInfo info, StreamingContext ctx) : base(info, ctx) { } } } diff --git a/src/StackExchange.Redis/Obsoletions.cs b/src/StackExchange.Redis/Obsoletions.cs new file mode 100644 index 000000000..44ede249b --- /dev/null +++ b/src/StackExchange.Redis/Obsoletions.cs @@ -0,0 +1,7 @@ +namespace StackExchange.Redis; + +internal static class Obsoletions +{ + public const string LegacyFormatterImplMessage = "This API supports obsolete formatter-based serialization. It should not be called or extended by application code."; + public const string LegacyFormatterImplDiagId = "SYSLIB0051"; +} diff --git a/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt new file mode 100644 index 000000000..599891ac2 --- /dev/null +++ b/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt @@ -0,0 +1,3 @@ +StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.get -> System.Func? +StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.set -> void +System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime) \ No newline at end of file diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index ba7c31c35..53b1542c3 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -2,7 +2,7 @@ enable - net461;netstandard2.0;net472;netcoreapp3.1;net6.0 + net461;netstandard2.0;net472;netcoreapp3.1;net6.0;net8.0 High performance Redis client, incorporating both synchronous and asynchronous usage. StackExchange.Redis StackExchange.Redis diff --git a/tests/BasicTest/BasicTest.csproj b/tests/BasicTest/BasicTest.csproj index 7fcf776ee..593d26619 100644 --- a/tests/BasicTest/BasicTest.csproj +++ b/tests/BasicTest/BasicTest.csproj @@ -2,7 +2,7 @@ StackExchange.Redis.BasicTest .NET Core - net472;net6.0 + net472;net8.0 BasicTest Exe BasicTest @@ -11,6 +11,9 @@ + + + diff --git a/tests/BasicTestBaseline/BasicTestBaseline.csproj b/tests/BasicTestBaseline/BasicTestBaseline.csproj index f396ae7c1..a9f75e441 100644 --- a/tests/BasicTestBaseline/BasicTestBaseline.csproj +++ b/tests/BasicTestBaseline/BasicTestBaseline.csproj @@ -2,7 +2,7 @@ StackExchange.Redis.BasicTest .NET Core - net472;net6.0 + net472;net8.0 BasicTestBaseline Exe BasicTestBaseline @@ -17,6 +17,8 @@ + + diff --git a/tests/ConsoleTest/ConsoleTest.csproj b/tests/ConsoleTest/ConsoleTest.csproj index 883ab204a..b3c1fd998 100644 --- a/tests/ConsoleTest/ConsoleTest.csproj +++ b/tests/ConsoleTest/ConsoleTest.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 Exe enable enable diff --git a/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj b/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj index 2c4bac2f5..1a6a7149d 100644 --- a/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj +++ b/tests/ConsoleTestBaseline/ConsoleTestBaseline.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 Exe enable enable diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 4db6b1163..c8a5eacd6 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -92,15 +92,15 @@ public void ExpectedFields() [Fact] public void SslProtocols_SingleValue() { - var options = ConfigurationOptions.Parse("myhost,sslProtocols=Tls11"); - Assert.Equal(SslProtocols.Tls11, options.SslProtocols.GetValueOrDefault()); + var options = ConfigurationOptions.Parse("myhost,sslProtocols=Tls12"); + Assert.Equal(SslProtocols.Tls12, options.SslProtocols.GetValueOrDefault()); } [Fact] public void SslProtocols_MultipleValues() { - var options = ConfigurationOptions.Parse("myhost,sslProtocols=Tls11|Tls12"); - Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, options.SslProtocols.GetValueOrDefault()); + var options = ConfigurationOptions.Parse("myhost,sslProtocols=Tls12|Tls13"); + Assert.Equal(SslProtocols.Tls12 | SslProtocols.Tls13, options.SslProtocols.GetValueOrDefault()); } [Theory] @@ -121,9 +121,9 @@ public void SslProtocols_UsingIntegerValue() // The below scenario is for cases where the *targeted* // .NET framework version (e.g. .NET 4.0) doesn't define an enum value (e.g. Tls11) // but the OS has been patched with support - const int integerValue = (int)(SslProtocols.Tls11 | SslProtocols.Tls12); + const int integerValue = (int)(SslProtocols.Tls12 | SslProtocols.Tls13); var options = ConfigurationOptions.Parse("myhost,sslProtocols=" + integerValue); - Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, options.SslProtocols.GetValueOrDefault()); + Assert.Equal(SslProtocols.Tls12 | SslProtocols.Tls13, options.SslProtocols.GetValueOrDefault()); } [Fact] @@ -203,7 +203,7 @@ public void ConfigurationOptionsIPv6Parsing(string configString, AddressFamily f public void CanParseAndFormatUnixDomainSocket() { const string ConfigString = "!/some/path,allowAdmin=True"; -#if NET472 +#if NETFRAMEWORK var ex = Assert.Throws(() => ConfigurationOptions.Parse(ConfigString)); Assert.Equal("Unix domain sockets require .NET Core 3 or above", ex.Message); #else @@ -793,6 +793,6 @@ public void CheckHighIntegrity(bool? assigned, bool expected, string cs) Assert.Equal(cs, clone.ToString()); var parsed = ConfigurationOptions.Parse(cs); - Assert.Equal(expected, options.HighIntegrity); + Assert.Equal(expected, parsed.HighIntegrity); } } diff --git a/tests/StackExchange.Redis.Tests/FormatTests.cs b/tests/StackExchange.Redis.Tests/FormatTests.cs index cf7593a08..9356c2e31 100644 --- a/tests/StackExchange.Redis.Tests/FormatTests.cs +++ b/tests/StackExchange.Redis.Tests/FormatTests.cs @@ -55,15 +55,21 @@ public void ParseEndPoint(string data, EndPoint expected, string? expectedFormat [Theory] [InlineData(CommandFlags.None, "None")] -#if NET472 +#if NETFRAMEWORK [InlineData(CommandFlags.PreferReplica, "PreferMaster, PreferReplica")] // 2-bit flag is hit-and-miss [InlineData(CommandFlags.DemandReplica, "PreferMaster, DemandReplica")] // 2-bit flag is hit-and-miss #else [InlineData(CommandFlags.PreferReplica, "PreferReplica")] // 2-bit flag is hit-and-miss [InlineData(CommandFlags.DemandReplica, "DemandReplica")] // 2-bit flag is hit-and-miss #endif + +#if NET8_0_OR_GREATER + [InlineData(CommandFlags.PreferReplica | CommandFlags.FireAndForget, "FireAndForget, PreferReplica")] // 2-bit flag is hit-and-miss + [InlineData(CommandFlags.DemandReplica | CommandFlags.FireAndForget, "FireAndForget, DemandReplica")] // 2-bit flag is hit-and-miss +#else [InlineData(CommandFlags.PreferReplica | CommandFlags.FireAndForget, "PreferMaster, FireAndForget, PreferReplica")] // 2-bit flag is hit-and-miss [InlineData(CommandFlags.DemandReplica | CommandFlags.FireAndForget, "PreferMaster, FireAndForget, DemandReplica")] // 2-bit flag is hit-and-miss +#endif public void CommandFlagsFormatting(CommandFlags value, string expected) => Assert.Equal(expected, value.ToString()); diff --git a/tests/StackExchange.Redis.Tests/SSLTests.cs b/tests/StackExchange.Redis.Tests/SSLTests.cs index 2a261fe1a..5b11f68c8 100644 --- a/tests/StackExchange.Redis.Tests/SSLTests.cs +++ b/tests/StackExchange.Redis.Tests/SSLTests.cs @@ -7,10 +7,12 @@ using System.Net; using System.Net.Security; using System.Reflection; +using System.Runtime.InteropServices; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; +using NSubstitute.Exceptions; using StackExchange.Redis.Tests.Helpers; using Xunit; using Xunit.Abstractions; @@ -182,34 +184,35 @@ public async Task ConnectToSSLServer(bool useSsl, bool specifyHost) [InlineData(SslProtocols.Ssl3 | SslProtocols.Tls12 | SslProtocols.Tls13, true)] [InlineData(SslProtocols.Ssl2, false, TlsCipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TlsCipherSuite.TLS_AES_256_GCM_SHA384)] #pragma warning restore CS0618 // Type or member is obsolete + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "Yes, we know.")] public async Task ConnectSslClientAuthenticationOptions(SslProtocols protocols, bool expectSuccess, params TlsCipherSuite[] tlsCipherSuites) { Fixture.SkipIfNoServer(); - var config = new ConfigurationOptions() + try { - EndPoints = { TestConfig.Current.SslServerAndPort }, - AllowAdmin = true, - ConnectRetry = 1, - SyncTimeout = Debugger.IsAttached ? int.MaxValue : 5000, - Ssl = true, - SslClientAuthenticationOptions = host => new SslClientAuthenticationOptions() + var config = new ConfigurationOptions() { - TargetHost = host, - CertificateRevocationCheckMode = X509RevocationMode.NoCheck, - EnabledSslProtocols = protocols, - CipherSuitesPolicy = tlsCipherSuites?.Length > 0 ? new CipherSuitesPolicy(tlsCipherSuites) : null, - RemoteCertificateValidationCallback = (sender, cert, chain, errors) => + EndPoints = { TestConfig.Current.SslServerAndPort }, + AllowAdmin = true, + ConnectRetry = 1, + SyncTimeout = Debugger.IsAttached ? int.MaxValue : 5000, + Ssl = true, + SslClientAuthenticationOptions = host => new SslClientAuthenticationOptions() { - Log(" Errors: " + errors); - Log(" Cert issued to: " + cert?.Subject); - return true; + TargetHost = host, + CertificateRevocationCheckMode = X509RevocationMode.NoCheck, + EnabledSslProtocols = protocols, + CipherSuitesPolicy = tlsCipherSuites?.Length > 0 ? new CipherSuitesPolicy(tlsCipherSuites) : null, + RemoteCertificateValidationCallback = (sender, cert, chain, errors) => + { + Log(" Errors: " + errors); + Log(" Cert issued to: " + cert?.Subject); + return true; + }, }, - }, - }; + }; - try - { if (expectSuccess) { using var conn = await ConnectionMultiplexer.ConnectAsync(config, Writer); @@ -376,12 +379,12 @@ public void SSLHostInferredFromEndpoints() }, Ssl = true, }; - Assert.True(options.SslHost == "mycache.rediscache.windows.net"); + Assert.Equal("mycache.rediscache.windows.net", options.SslHost); options = new ConfigurationOptions() { EndPoints = { { "121.23.23.45", 15000 } }, }; - Assert.True(options.SslHost == null); + Assert.Null(options.SslHost); } private void Check(string name, object? x, object? y) @@ -528,7 +531,7 @@ public void SSLParseViaConfig_Issue883_ConfigString() [Fact] public void ConfigObject_Issue1407_ToStringIncludesSslProtocols() { - const SslProtocols sslProtocols = SslProtocols.Tls12 | SslProtocols.Tls; + const SslProtocols sslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13; var sourceOptions = new ConfigurationOptions { AbortOnConnectFail = false, diff --git a/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs b/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs index 241999533..2c81c3826 100644 --- a/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs +++ b/tests/StackExchange.Redis.Tests/ServerSnapshotTests.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.Serialization; using Xunit; @@ -9,9 +10,11 @@ namespace StackExchange.Redis.Tests; public class ServerSnapshotTests { [Fact] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2012:Do not use boolean check to check if a value exists in a collection", Justification = "Explicit testing")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2013:Do not use equality check to check for collection size.", Justification = "Explicit testing")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2029:Do not use Empty() to check if a value does not exist in a collection", Justification = "Explicit testing")] + [SuppressMessage("Assertions", "xUnit2012:Do not use boolean check to check if a value exists in a collection", Justification = "Explicit testing")] + [SuppressMessage("Assertions", "xUnit2013:Do not use equality check to check for collection size.", Justification = "Explicit testing")] + [SuppressMessage("Assertions", "xUnit2029:Do not use Empty() to check if a value does not exist in a collection", Justification = "Explicit testing")] + [SuppressMessage("Performance", "CA1829:Use Length/Count property instead of Count() when available", Justification = "Explicit testing")] + [SuppressMessage("Performance", "CA1860:Avoid using 'Enumerable.Any()' extension method", Justification = "Explicit testing")] public void EmptyBehaviour() { var snapshot = ServerSnapshot.Empty; @@ -52,14 +55,19 @@ public void EmptyBehaviour() [InlineData(5, 0)] [InlineData(5, 3)] [InlineData(5, 5)] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2012:Do not use boolean check to check if a value exists in a collection", Justification = "Explicit testing")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2029:Do not use Empty() to check if a value does not exist in a collection", Justification = "Explicit testing")] + [SuppressMessage("Assertions", "xUnit2012:Do not use boolean check to check if a value exists in a collection", Justification = "Explicit testing")] + [SuppressMessage("Assertions", "xUnit2029:Do not use Empty() to check if a value does not exist in a collection", Justification = "Explicit testing")] + [SuppressMessage("Assertions", "xUnit2030:Do not use Assert.NotEmpty to check if a value exists in a collection", Justification = "Explicit testing")] + [SuppressMessage("Performance", "CA1829:Use Length/Count property instead of Count() when available", Justification = "Explicit testing")] + [SuppressMessage("Performance", "CA1860:Avoid using 'Enumerable.Any()' extension method", Justification = "Explicit testing")] public void NonEmptyBehaviour(int count, int replicaCount) { var snapshot = ServerSnapshot.Empty; for (int i = 0; i < count; i++) { +#pragma warning disable SYSLIB0050 // Type or member is obsolete var dummy = (ServerEndPoint)FormatterServices.GetSafeUninitializedObject(typeof(ServerEndPoint)); +#pragma warning restore SYSLIB0050 // Type or member is obsolete dummy.IsReplica = i < replicaCount; snapshot = snapshot.Add(dummy); } diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index 4e354b846..80a5762cf 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -1,6 +1,6 @@  - net472;net6.0 + net481;net8.0 StackExchange.Redis.Tests true true diff --git a/toys/TestConsole/TestConsole.csproj b/toys/TestConsole/TestConsole.csproj index 71bc9fe63..674ec5cfe 100644 --- a/toys/TestConsole/TestConsole.csproj +++ b/toys/TestConsole/TestConsole.csproj @@ -2,7 +2,7 @@ Exe - net6.0;net472 + net8.0;net472 SEV2 true diff --git a/toys/TestConsoleBaseline/TestConsoleBaseline.csproj b/toys/TestConsoleBaseline/TestConsoleBaseline.csproj index c2d46d20a..10d5ab321 100644 --- a/toys/TestConsoleBaseline/TestConsoleBaseline.csproj +++ b/toys/TestConsoleBaseline/TestConsoleBaseline.csproj @@ -2,7 +2,7 @@ Exe - net6.0;net461;net462;net47;net472 + net8.0;net461;net462;net47;net472 From 7170cb46b3b8aeb7b29b822919b94e9621762b2a Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 27 Nov 2024 07:49:44 -0500 Subject: [PATCH 312/435] Update release notes for 2.8.22 --- docs/ReleaseNotes.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index fc5cf3134..b1d002d53 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,10 +7,14 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased +No pending unreleased changes. + +## 2.8.22 - Format IPv6 endpoints correctly when rewriting configration strings ([#2813 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2813)) -- Update default Redis version from 4.0.0 to 6.0.0 for Azure Redis resources ([#2810 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2810)) +- Update default Redis version from `4.0.0` to `6.0.0` for Azure Redis resources ([#2810 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2810)) - Detect Azure Managed Redis caches and tune default connection settings for them ([#2818 by philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2818)) +- Bump `Microsoft.Bcl.AsyncInterfaces` dependency from `5.0.0` to `6.0.0` ([#2820 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2820)) ## 2.8.16 From 0cb7d58977201035e945ac54752c726ef8da9a88 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 18 Dec 2024 16:52:37 +0000 Subject: [PATCH 313/435] update envoy defs to allow `UNWATCH` (#2824) --- docs/ReleaseNotes.md | 3 ++- src/StackExchange.Redis/CommandMap.cs | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b1d002d53..3dbbcaf84 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,7 +7,8 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased -No pending unreleased changes. + +- Update *envoy* command definitions to [allow `UNWATCH`](https://github.com/envoyproxy/envoy/pull/37620) ([#2824 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2824)) ## 2.8.22 diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index a5f1aa68f..d1e125bb3 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -59,8 +59,6 @@ public sealed class CommandMap RedisCommand.PSUBSCRIBE, RedisCommand.PUNSUBSCRIBE, RedisCommand.SUBSCRIBE, RedisCommand.UNSUBSCRIBE, - RedisCommand.UNWATCH, - RedisCommand.SCRIPT, RedisCommand.SELECT, From 1a4c66db3040394a2584fd4a60d14a63ebfa4828 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Wed, 18 Dec 2024 13:53:52 -0500 Subject: [PATCH 314/435] Update release notes for 2.8.22 --- docs/ReleaseNotes.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 3dbbcaf84..08c8c4cea 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,8 +7,11 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased +No pending unreleased changes. -- Update *envoy* command definitions to [allow `UNWATCH`](https://github.com/envoyproxy/envoy/pull/37620) ([#2824 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2824)) +## 2.8.24 + +- Update Envoy command definitions to [allow `UNWATCH`](https://github.com/envoyproxy/envoy/pull/37620) ([#2824 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2824)) ## 2.8.22 From fe04b9a8c72ed6039b5129fac1b5994a2a35410b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 18 Feb 2025 16:23:02 +0000 Subject: [PATCH 315/435] Improve readme - what even is this? (#2837) * Improve readme - what even is this? Propose words for #2836 * Update README.md --------- Co-authored-by: Nick Craver --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 44c06c537..e65e97260 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ StackExchange.Redis =================== +StackExchange.Redis is a .NET client for communicating with RESP servers such as [Redis](https://redis.io/), [Garnet](https://microsoft.github.io/garnet/), [Valkey](https://valkey.io/), [Azure Cache for Redis](https://azure.microsoft.com/products/cache), [AWS ElastiCache](https://aws.amazon.com/elasticache/), and a wide range of other Redis-like servers. We do not maintain a list of compatible servers, but if the server has a Redis-like API: it will *probably* work fine. If not: log an issue with details! + For all documentation, [see here](https://stackexchange.github.io/StackExchange.Redis/). #### Build Status From 3bd3e665289e1b3566f8a74148643ccc4ba33447 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Tue, 4 Mar 2025 08:17:19 -0800 Subject: [PATCH 316/435] Track client-initiated shutdown for any pipe type. Fixes #2652 (#2814) --- src/StackExchange.Redis/PhysicalConnection.cs | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 0a07f8ac4..bac6ebac0 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -55,6 +55,8 @@ private static readonly Message private int failureReported; + private int clientSentQuit; + private int lastWriteTickCount, lastReadTickCount, lastBeatTickCount; private long bytesLastResult; @@ -375,15 +377,6 @@ internal void SimulateConnectionFailure(SimulatedFailureType failureType) } } - /// - /// Did we ask for the shutdown? If so this leads to informational messages for tracking but not errors. - /// - private bool IsRequestedShutdown(PipeShutdownKind kind) => kind switch - { - PipeShutdownKind.ProtocolExitClient => true, - _ => false, - }; - public void RecordConnectionFailed( ConnectionFailureType failureType, Exception? innerException = null, @@ -436,12 +429,12 @@ public void RecordConnectionFailed( var exMessage = new StringBuilder(failureType.ToString()); + // If the reason for the shutdown was we asked for the socket to die, don't log it as an error (only informational) + weAskedForThis = Thread.VolatileRead(ref clientSentQuit) != 0; + var pipe = connectingPipe ?? _ioPipe; if (pipe is SocketConnection sc) { - // If the reason for the shutdown was we asked for the socket to die, don't log it as an error (only informational) - weAskedForThis = IsRequestedShutdown(sc.ShutdownKind); - exMessage.Append(" (").Append(sc.ShutdownKind); if (sc.SocketError != SocketError.Success) { @@ -904,8 +897,12 @@ internal void WriteHeader(RedisCommand command, int arguments, CommandBytes comm internal void WriteRaw(ReadOnlySpan bytes) => _ioPipe?.Output?.Write(bytes); - internal void RecordQuit() // don't blame redis if we fired the first shot - => (_ioPipe as SocketConnection)?.TrySetProtocolShutdown(PipeShutdownKind.ProtocolExitClient); + internal void RecordQuit() + { + // don't blame redis if we fired the first shot + Thread.VolatileWrite(ref clientSentQuit, 1); + (_ioPipe as SocketConnection)?.TrySetProtocolShutdown(PipeShutdownKind.ProtocolExitClient); + } internal static void WriteMultiBulkHeader(PipeWriter output, long count) { From 437e1ffd475ab6f0f7da262810bb518ad2488847 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 4 Mar 2025 11:22:57 -0500 Subject: [PATCH 317/435] Fix: Respect IReconnectRetryPolicy in disconnect loops (#2853) Fixes the case where we loop from `Connecting` -> `BeginConnectAsync` -> `OnDisconnected` and the next heartbeat triggers a reconnect again due to the backoff thresholds resetting. This ultimately causes the backoff to not be respected. Here, we're doing 2 things: - Upping the max exponential backoff window to 60 seconds which better handlers clients at scale and mass DDoS cases. - Only resets the reconnect counter on the first disconnect, not doing so again until we've successfully reconnected and heartbeated again, so backoffs are properly respected. --- docs/ReleaseNotes.md | 3 ++- src/StackExchange.Redis/ExponentialRetry.cs | 2 +- src/StackExchange.Redis/PhysicalBridge.cs | 12 +++++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 08c8c4cea..c7a48ffd6 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,7 +7,8 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased -No pending unreleased changes. +- Fix: Respect `IReconnectRetryPolicy` timing in the case that a node that was present disconnects indefinitely ([#2853 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2853)) +- Changes max default retry policy backoff to 60 seconds ([#2853 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2853)) ## 2.8.24 diff --git a/src/StackExchange.Redis/ExponentialRetry.cs b/src/StackExchange.Redis/ExponentialRetry.cs index f28708679..5ee10a951 100644 --- a/src/StackExchange.Redis/ExponentialRetry.cs +++ b/src/StackExchange.Redis/ExponentialRetry.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis public class ExponentialRetry : IReconnectRetryPolicy { private readonly int deltaBackOffMilliseconds; - private readonly int maxDeltaBackOffMilliseconds = (int)TimeSpan.FromSeconds(10).TotalMilliseconds; + private readonly int maxDeltaBackOffMilliseconds = (int)TimeSpan.FromSeconds(60).TotalMilliseconds; [ThreadStatic] private static Random? r; diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index e063233b5..56dd0f5c5 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -47,6 +47,7 @@ internal sealed class PhysicalBridge : IDisposable private int beating; private int failConnectCount = 0; private volatile bool isDisposed; + private volatile bool shouldResetConnectionRetryCount; private long nonPreferredEndpointCount; // private volatile int missedHeartbeats; @@ -597,6 +598,8 @@ internal void OnHeartbeat(bool ifConnectedOnly) // We need to time that out and cleanup the PhysicalConnection if needed, otherwise that reader and socket will remain open // for the lifetime of the application due to being orphaned, yet still referenced by the active task doing the pipe read. case (int)State.ConnectedEstablished: + // Track that we should reset the count on the next disconnect, but not do so in a loop + shouldResetConnectionRetryCount = true; var tmp = physical; if (tmp != null) { @@ -668,7 +671,14 @@ internal void OnHeartbeat(bool ifConnectedOnly) } break; case (int)State.Disconnected: - Interlocked.Exchange(ref connectTimeoutRetryCount, 0); + // Only if we should reset the connection count + // This should only happen after a successful reconnection, and not every time we bounce from BeginConnectAsync -> Disconnected + // in a failure loop case that happens when a node goes missing forever. + if (shouldResetConnectionRetryCount) + { + shouldResetConnectionRetryCount = false; + Interlocked.Exchange(ref connectTimeoutRetryCount, 0); + } if (!ifConnectedOnly) { Multiplexer.Trace("Resurrecting " + ToString()); From 2a47eb5282c54de6d772ec520e03c72b6a780c5e Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 4 Mar 2025 11:27:06 -0500 Subject: [PATCH 318/435] Update release notes --- docs/ReleaseNotes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index c7a48ffd6..9520dcdb2 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Fix: Respect `IReconnectRetryPolicy` timing in the case that a node that was present disconnects indefinitely ([#2853 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2853)) - Changes max default retry policy backoff to 60 seconds ([#2853 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2853)) +- Fix [#2652](https://github.com/StackExchange/StackExchange.Redis/issues/2652): Track client-initiated shutdown for any pipe type ([#2814 by bgrainger](https://github.com/StackExchange/StackExchange.Redis/pull/2814)) ## 2.8.24 From 50f2218a334ef387db869ed79fb30c4197cc0ee2 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 6 Mar 2025 16:30:20 -0500 Subject: [PATCH 319/435] Fix: Rexpect backoff in the disconnect -> resurrection case as well (#2856) Additional case handling for #2853, respecting backoff in the disconnected -> resurrecting state case. We need to check backoff in that flow as well to prevent a premature `TryConnect`. --- docs/ReleaseNotes.md | 2 +- src/StackExchange.Redis/PhysicalBridge.cs | 12 ++++++++---- .../ConnectingFailDetectionTests.cs | 2 ++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 9520dcdb2..d800f21a6 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,7 +7,7 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased -- Fix: Respect `IReconnectRetryPolicy` timing in the case that a node that was present disconnects indefinitely ([#2853 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2853)) +- Fix: Respect `IReconnectRetryPolicy` timing in the case that a node that was present disconnects indefinitely ([#2853](https://github.com/StackExchange/StackExchange.Redis/pull/2853) & [#2856](https://github.com/StackExchange/StackExchange.Redis/pull/2856) by NickCraver) - Changes max default retry policy backoff to 60 seconds ([#2853 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2853)) - Fix [#2652](https://github.com/StackExchange/StackExchange.Redis/issues/2652): Track client-initiated shutdown for any pipe type ([#2814 by bgrainger](https://github.com/StackExchange/StackExchange.Redis/pull/2814)) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 56dd0f5c5..b1736ccb2 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -550,6 +550,12 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) private int connectStartTicks; private long connectTimeoutRetryCount = 0; + private bool DueForConnectRetry() + { + int connectTimeMilliseconds = unchecked(Environment.TickCount - Thread.VolatileRead(ref connectStartTicks)); + return Multiplexer.RawConfig.ReconnectRetryPolicy.ShouldRetry(Interlocked.Read(ref connectTimeoutRetryCount), connectTimeMilliseconds); + } + internal void OnHeartbeat(bool ifConnectedOnly) { bool runThisTime = false; @@ -575,9 +581,7 @@ internal void OnHeartbeat(bool ifConnectedOnly) switch (state) { case (int)State.Connecting: - int connectTimeMilliseconds = unchecked(Environment.TickCount - Thread.VolatileRead(ref connectStartTicks)); - bool shouldRetry = Multiplexer.RawConfig.ReconnectRetryPolicy.ShouldRetry(Interlocked.Read(ref connectTimeoutRetryCount), connectTimeMilliseconds); - if (shouldRetry) + if (DueForConnectRetry()) { Interlocked.Increment(ref connectTimeoutRetryCount); var ex = ExceptionFactory.UnableToConnect(Multiplexer, "ConnectTimeout"); @@ -679,7 +683,7 @@ internal void OnHeartbeat(bool ifConnectedOnly) shouldResetConnectionRetryCount = false; Interlocked.Exchange(ref connectTimeoutRetryCount, 0); } - if (!ifConnectedOnly) + if (!ifConnectedOnly && DueForConnectRetry()) { Multiplexer.Trace("Resurrecting " + ToString()); Multiplexer.OnResurrecting(ServerEndPoint.EndPoint, ConnectionType); diff --git a/tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs b/tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs index db8d1dd1a..bc0fa9d0c 100644 --- a/tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs @@ -18,6 +18,7 @@ public async Task FastNoticesFailOnConnectingSyncCompletion() try { using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false); + conn.RawConfig.ReconnectRetryPolicy = new LinearRetry(200); var db = conn.GetDatabase(); db.Ping(); @@ -57,6 +58,7 @@ public async Task FastNoticesFailOnConnectingAsyncCompletion() try { using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false); + conn.RawConfig.ReconnectRetryPolicy = new LinearRetry(200); var db = conn.GetDatabase(); db.Ping(); From cd7a3b210ad5689a5fb3babab4ef7d27e6e95ed2 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Thu, 6 Mar 2025 17:28:53 -0500 Subject: [PATCH 320/435] Physical bridge: handle the reconnect count case Without this, we don't increase the current count and don't escalate in the exponential retry case, so let's both do that and log that something is happening so we can diagnose from the logs. --- src/StackExchange.Redis/PhysicalBridge.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index b1736ccb2..c697ed7f6 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -685,7 +685,10 @@ internal void OnHeartbeat(bool ifConnectedOnly) } if (!ifConnectedOnly && DueForConnectRetry()) { - Multiplexer.Trace("Resurrecting " + ToString()); + // Increment count here, so that we don't re-enter in Connecting case up top - we don't want to re-enter and log there. + Interlocked.Increment(ref connectTimeoutRetryCount); + + Multiplexer.Logger?.LogInformation($"Resurrecting {ToString()} (retry: {connectTimeoutRetryCount})"); Multiplexer.OnResurrecting(ServerEndPoint.EndPoint, ConnectionType); TryConnect(null); } From be5c8a6c89b89d68d89012a679208169c6a0c3d5 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 7 Mar 2025 08:28:17 -0500 Subject: [PATCH 321/435] Update release notes for 2.8.31 --- docs/ReleaseNotes.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index d800f21a6..ecb6dda66 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,7 +7,12 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased +No pending unreleased changes + +## 2.8.31 + - Fix: Respect `IReconnectRetryPolicy` timing in the case that a node that was present disconnects indefinitely ([#2853](https://github.com/StackExchange/StackExchange.Redis/pull/2853) & [#2856](https://github.com/StackExchange/StackExchange.Redis/pull/2856) by NickCraver) + - Special thanks to [sampdei](https://github.com/sampdei) tracking this down and working a fix - Changes max default retry policy backoff to 60 seconds ([#2853 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2853)) - Fix [#2652](https://github.com/StackExchange/StackExchange.Redis/issues/2652): Track client-initiated shutdown for any pipe type ([#2814 by bgrainger](https://github.com/StackExchange/StackExchange.Redis/pull/2814)) From e93de400ef310c029966fdbdb48b0f97d2ed1a27 Mon Sep 17 00:00:00 2001 From: st-dev-gh <_St@centrum.cz> Date: Wed, 2 Apr 2025 14:06:00 +0200 Subject: [PATCH 322/435] Log multiplexer reconfiguration (#2864) * Log Multiplexer reconfiguration * Include bridge name in connection exception --- .../ConnectionMultiplexer.Sentinel.cs | 2 +- src/StackExchange.Redis/ConnectionMultiplexer.cs | 4 ++-- src/StackExchange.Redis/ExceptionFactory.cs | 9 +++++++-- src/StackExchange.Redis/PhysicalBridge.cs | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs index 7753954d0..88e42ed5e 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs @@ -463,7 +463,7 @@ internal void UpdateSentinelAddressList(string serviceName) if (hasNew) { // Reconfigure the sentinel multiplexer if we added new endpoints - ReconfigureAsync(first: false, reconfigureAll: true, null, EndPoints[0], "Updating Sentinel List", false).Wait(); + ReconfigureAsync(first: false, reconfigureAll: true, Logger, EndPoints[0], "Updating Sentinel List", false).Wait(); } } } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 8a4b8733d..e17a9503b 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1378,7 +1378,7 @@ internal bool ReconfigureIfNeeded(EndPoint? blame, bool fromBroadcast, string ca { bool reconfigureAll = fromBroadcast || publishReconfigure; Trace("Configuration change detected; checking nodes", "Configuration"); - ReconfigureAsync(first: false, reconfigureAll, null, blame, cause, publishReconfigure, flags).ObserveErrors(); + ReconfigureAsync(first: false, reconfigureAll, Logger, blame, cause, publishReconfigure, flags).ObserveErrors(); return true; } else @@ -1393,7 +1393,7 @@ internal bool ReconfigureIfNeeded(EndPoint? blame, bool fromBroadcast, string ca /// This re-assessment of all server endpoints to get the current topology and adjust, the same as if we had first connected. /// public Task ReconfigureAsync(string reason) => - ReconfigureAsync(first: false, reconfigureAll: false, log: null, blame: null, cause: reason); + ReconfigureAsync(first: false, reconfigureAll: false, log: Logger, blame: null, cause: reason); internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILogger? log, EndPoint? blame, string cause, bool publishReconfigure = false, CommandFlags publishReconfigureFlags = CommandFlags.None) { diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 24e519b54..7e4eca49a 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -408,9 +408,14 @@ private static string GetLabel(bool includeDetail, RedisCommand command, Message return message == null ? command.ToString() : (includeDetail ? message.CommandAndKey : message.CommandString); } - internal static Exception UnableToConnect(ConnectionMultiplexer muxer, string? failureMessage = null) + internal static Exception UnableToConnect(ConnectionMultiplexer muxer, string? failureMessage = null, string? connectionName = null) { - var sb = new StringBuilder("It was not possible to connect to the redis server(s)."); + var sb = new StringBuilder("It was not possible to connect to the redis server(s)"); + if (connectionName is not null) + { + sb.Append(' ').Append(connectionName); + } + sb.Append('.'); Exception? inner = null; var failureType = ConnectionFailureType.UnableToConnect; if (muxer is not null) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index c697ed7f6..a10b241cb 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -584,7 +584,7 @@ internal void OnHeartbeat(bool ifConnectedOnly) if (DueForConnectRetry()) { Interlocked.Increment(ref connectTimeoutRetryCount); - var ex = ExceptionFactory.UnableToConnect(Multiplexer, "ConnectTimeout"); + var ex = ExceptionFactory.UnableToConnect(Multiplexer, "ConnectTimeout", Name); LastException = ex; Multiplexer.Logger?.LogError(ex, ex.Message); Trace("Aborting connect"); From 328b4b5a3f7c2084ce34db644b45673bdb15d30c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 15 Apr 2025 18:12:07 +0100 Subject: [PATCH 323/435] Make it easier to use user certificate files (#2873) * Make it easier to use user certificate files * make key file optional; PEM can include the private key * - release notes - use Enum.TryParse for the X509 flags --- docs/ReleaseNotes.md | 2 + .../ConfigurationOptions.cs | 43 +++++++++++++++++++ src/StackExchange.Redis/PhysicalConnection.cs | 27 +++++++----- .../PublicAPI/PublicAPI.Shipped.txt | 2 +- .../PublicAPI/net6.0/PublicAPI.Shipped.txt | 3 +- .../PublicAPI/net8.0/PublicAPI.Shipped.txt | 3 +- 6 files changed, 67 insertions(+), 13 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index ecb6dda66..4b26d9348 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,8 @@ Current package versions: ## Unreleased No pending unreleased changes +- Add `ConfigurationOptions.SetUserPemCertificate(...)` and `ConfigurationOptions.SetUserPfxCertificate(...)` methods to simplify using client certificates ([#2873 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2873)) + ## 2.8.31 - Fix: Respect `IReconnectRetryPolicy` timing in the case that a node that was present disconnects indefinitely ([#2853](https://github.com/StackExchange/StackExchange.Redis/pull/2853) & [#2856](https://github.com/StackExchange/StackExchange.Redis/pull/2856) by NickCraver) diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index 199f1a378..dfdab5f4e 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -301,6 +301,49 @@ public bool HighIntegrity /// The file system path to find the certificate at. public void TrustIssuer(string issuerCertificatePath) => CertificateValidationCallback = TrustIssuerCallback(issuerCertificatePath); +#if NET5_0_OR_GREATER + /// + /// Supply a user certificate from a PEM file pair and enable TLS. + /// + /// The path for the the user certificate (commonly a .crt file). + /// The path for the the user key (commonly a .key file). + public void SetUserPemCertificate(string userCertificatePath, string? userKeyPath = null) + { + CertificateSelectionCallback = CreatePemUserCertificateCallback(userCertificatePath, userKeyPath); + Ssl = true; + } +#endif + + /// + /// Supply a user certificate from a PFX file and optional password and enable TLS. + /// + /// The path for the the user certificate (commonly a .pfx file). + /// The password for the certificate file. + public void SetUserPfxCertificate(string userCertificatePath, string? password = null) + { + CertificateSelectionCallback = CreatePfxUserCertificateCallback(userCertificatePath, password); + Ssl = true; + } + +#if NET5_0_OR_GREATER + internal static LocalCertificateSelectionCallback CreatePemUserCertificateCallback(string userCertificatePath, string? userKeyPath) + { + // PEM handshakes not universally supported and causes a runtime error about ephemeral certificates; to avoid, export as PFX + using var pem = X509Certificate2.CreateFromPemFile(userCertificatePath, userKeyPath); +#pragma warning disable SYSLIB0057 // Type or member is obsolete + var pfx = new X509Certificate2(pem.Export(X509ContentType.Pfx)); +#pragma warning restore SYSLIB0057 // Type or member is obsolete + + return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => pfx; + } +#endif + + internal static LocalCertificateSelectionCallback CreatePfxUserCertificateCallback(string userCertificatePath, string? password, X509KeyStorageFlags storageFlags = X509KeyStorageFlags.DefaultKeySet) + { + var pfx = new X509Certificate2(userCertificatePath, password ?? "", storageFlags); + return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => pfx; + } + /// /// Create a certificate validation check that checks against the supplied issuer even when not known by the machine. /// diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index bac6ebac0..df30ab207 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -1504,21 +1504,28 @@ public ConnectionStatus GetStatus() { try { - var pfxPath = Environment.GetEnvironmentVariable("SERedis_ClientCertPfxPath"); - var pfxPassword = Environment.GetEnvironmentVariable("SERedis_ClientCertPassword"); - var pfxStorageFlags = Environment.GetEnvironmentVariable("SERedis_ClientCertStorageFlags"); - - X509KeyStorageFlags? flags = null; - if (!string.IsNullOrEmpty(pfxStorageFlags)) + var certificatePath = Environment.GetEnvironmentVariable("SERedis_ClientCertPfxPath"); + if (!string.IsNullOrEmpty(certificatePath) && File.Exists(certificatePath)) { - flags = Enum.Parse(typeof(X509KeyStorageFlags), pfxStorageFlags) as X509KeyStorageFlags?; + var password = Environment.GetEnvironmentVariable("SERedis_ClientCertPassword"); + var pfxStorageFlags = Environment.GetEnvironmentVariable("SERedis_ClientCertStorageFlags"); + X509KeyStorageFlags storageFlags = X509KeyStorageFlags.DefaultKeySet; + if (!string.IsNullOrEmpty(pfxStorageFlags) && Enum.TryParse(pfxStorageFlags, true, out var typedFlags)) + { + storageFlags = typedFlags; + } + + return ConfigurationOptions.CreatePfxUserCertificateCallback(certificatePath, password, storageFlags); } - if (!string.IsNullOrEmpty(pfxPath) && File.Exists(pfxPath)) +#if NET5_0_OR_GREATER + certificatePath = Environment.GetEnvironmentVariable("SERedis_ClientCertPemPath"); + if (!string.IsNullOrEmpty(certificatePath) && File.Exists(certificatePath)) { - return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => - new X509Certificate2(pfxPath, pfxPassword ?? "", flags ?? X509KeyStorageFlags.DefaultKeySet); + var passwordPath = Environment.GetEnvironmentVariable("SERedis_ClientCertPasswordPath"); + return ConfigurationOptions.CreatePemUserCertificateCallback(certificatePath, passwordPath); } +#endif } catch (Exception ex) { diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index a24333c8e..8263defd3 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1893,4 +1893,4 @@ virtual StackExchange.Redis.RedisResult.Length.get -> int virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult! StackExchange.Redis.ConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void StackExchange.Redis.IConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void - +StackExchange.Redis.ConfigurationOptions.SetUserPfxCertificate(string! userCertificatePath, string? password = null) -> void diff --git a/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt index 599891ac2..fae4f65ce 100644 --- a/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/net6.0/PublicAPI.Shipped.txt @@ -1,3 +1,4 @@ StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.get -> System.Func? StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.set -> void -System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime) \ No newline at end of file +System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime) +StackExchange.Redis.ConfigurationOptions.SetUserPemCertificate(string! userCertificatePath, string? userKeyPath = null) -> void \ No newline at end of file diff --git a/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt index 599891ac2..fae4f65ce 100644 --- a/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt @@ -1,3 +1,4 @@ StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.get -> System.Func? StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.set -> void -System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime) \ No newline at end of file +System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime) +StackExchange.Redis.ConfigurationOptions.SetUserPemCertificate(string! userCertificatePath, string? userKeyPath = null) -> void \ No newline at end of file From 28fbe780c21bf8ea3e5914be88192052a74443f0 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 3 May 2025 14:19:34 -0400 Subject: [PATCH 324/435] Connections: Move all AuthenticateAsClient to async (#2878) Previously when supporting earlier versions of .NET Framework, we did not have an async version of `AuthenticateAsClient` due to socket itself not having async handlers in earlier versions of `netstandard`. These days though with the supported target framework matrix, this can be fully async to prevent thread starvation specifically in the case where we're able to connect a socket but the server is overwhelmed and unable to finish TLS negotiations. Example: ![image](https://github.com/user-attachments/assets/4b82c2b0-7f54-4720-ae4e-d540b5584d49) This is a case we've never seen stall before, where the server is able to accept connections but unable to complete TLS negotiation, so there's never been a reported stall raising this potential issue. Last night we saw it live on a major instance though. It's still TBD how this was manifesting server-side (could be a proxy stall in front of nodes, etc.) but we saw this against Redis Enterprise which we know a lot of folks use, so let's definitely improve anything we can as always. Note that none of these methods take a cancellation token, so while we can avoid thread growth on the client side as a primary symptom of the case here, the server-side impact of backlogged but unconnected things may continue to grow. It's net better overall, though, and should help a bit on mass connection cases for some applications as well, in the cases TLS negotiation isn't instant. --- docs/ReleaseNotes.md | 1 + .../Configuration/LoggingTunnel.cs | 4 ++-- src/StackExchange.Redis/ExtensionMethods.cs | 13 ++++--------- src/StackExchange.Redis/PhysicalConnection.cs | 4 ++-- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 4b26d9348..c1e77aa17 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: No pending unreleased changes - Add `ConfigurationOptions.SetUserPemCertificate(...)` and `ConfigurationOptions.SetUserPfxCertificate(...)` methods to simplify using client certificates ([#2873 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2873)) +- Fix: Move `AuthenticateAsClient` to fully async after dropping older framework support, to help client thread starvation in cases TLS negotiation stalls server-side ([#2878 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2878)) ## 2.8.31 diff --git a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs index 987d2075c..d61442071 100644 --- a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs +++ b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs @@ -367,10 +367,10 @@ private async Task TlsHandshakeAsync(Stream stream, EndPoint endpoint) } else { - ssl.AuthenticateAsClient(host, _options.SslProtocols, _options.CheckCertificateRevocation); + await ssl.AuthenticateAsClientAsync(host, _options.SslProtocols, _options.CheckCertificateRevocation).ForAwait(); } #else - ssl.AuthenticateAsClient(host, _options.SslProtocols, _options.CheckCertificateRevocation); + await ssl.AuthenticateAsClientAsync(host, _options.SslProtocols, _options.CheckCertificateRevocation).ForAwait(); #endif return ssl; } diff --git a/src/StackExchange.Redis/ExtensionMethods.cs b/src/StackExchange.Redis/ExtensionMethods.cs index 87904aa9c..e5a5c4d4d 100644 --- a/src/StackExchange.Redis/ExtensionMethods.cs +++ b/src/StackExchange.Redis/ExtensionMethods.cs @@ -7,6 +7,7 @@ using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Threading.Tasks; using Pipelines.Sockets.Unofficial.Arenas; namespace StackExchange.Redis @@ -188,22 +189,16 @@ public static class ExtensionMethods return Array.ConvertAll(values, x => (string?)x); } - internal static void AuthenticateAsClient(this SslStream ssl, string host, SslProtocols? allowedProtocols, bool checkCertificateRevocation) + internal static Task AuthenticateAsClientAsync(this SslStream ssl, string host, SslProtocols? allowedProtocols, bool checkCertificateRevocation) { if (!allowedProtocols.HasValue) { // Default to the sslProtocols defined by the .NET Framework - AuthenticateAsClientUsingDefaultProtocols(ssl, host); - return; + return ssl.AuthenticateAsClientAsync(host); } var certificateCollection = new X509CertificateCollection(); - ssl.AuthenticateAsClient(host, certificateCollection, allowedProtocols.Value, checkCertificateRevocation); - } - - private static void AuthenticateAsClientUsingDefaultProtocols(SslStream ssl, string host) - { - ssl.AuthenticateAsClient(host); + return ssl.AuthenticateAsClientAsync(host, certificateCollection, allowedProtocols.Value, checkCertificateRevocation); } /// diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index df30ab207..51fac7c3d 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -1585,10 +1585,10 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock } else { - ssl.AuthenticateAsClient(host, config.SslProtocols, config.CheckCertificateRevocation); + await ssl.AuthenticateAsClientAsync(host, config.SslProtocols, config.CheckCertificateRevocation).ForAwait(); } #else - ssl.AuthenticateAsClient(host, config.SslProtocols, config.CheckCertificateRevocation); + await ssl.AuthenticateAsClientAsync(host, config.SslProtocols, config.CheckCertificateRevocation).ForAwait(); #endif } catch (Exception ex) From bb23b57c453596e7a904c14a60dd24a8abd99f47 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 3 May 2025 19:23:00 +0100 Subject: [PATCH 325/435] add package README.md to make NuGet happier (#2875) links are intentionally duplicated; IIRC the release notes MD processor is fairly restricted --- src/StackExchange.Redis/README.md | 6 ++++++ src/StackExchange.Redis/StackExchange.Redis.csproj | 3 +++ 2 files changed, 9 insertions(+) create mode 100644 src/StackExchange.Redis/README.md diff --git a/src/StackExchange.Redis/README.md b/src/StackExchange.Redis/README.md new file mode 100644 index 000000000..9cc0fe157 --- /dev/null +++ b/src/StackExchange.Redis/README.md @@ -0,0 +1,6 @@ +StackExchange.Redis is a high-performance RESP (Redis, etc) client for .NET, available under the MIT license. + +- Release notes: [https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes](https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes) +- NuGet package: [https://www.nuget.org/packages/StackExchange.Redis/](https://www.nuget.org/packages/StackExchange.Redis/) +- General docs: [https://stackexchange.github.io/StackExchange.Redis/](https://stackexchange.github.io/StackExchange.Redis/) +- Code: [https://github.com/StackExchange/StackExchange.Redis/](https://github.com/StackExchange/StackExchange.Redis/) \ No newline at end of file diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 53b1542c3..17e34eab9 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -12,6 +12,7 @@ true $(DefineConstants);VECTOR_SAFE $(DefineConstants);UNIX_SOCKET + README.md @@ -28,6 +29,8 @@ + + From e91c3ebc477eda82e82b4d4b6f63b2d14883e992 Mon Sep 17 00:00:00 2001 From: atakavci Date: Sun, 4 May 2025 03:09:18 +0300 Subject: [PATCH 326/435] Use up-to-date releases of Redis for CI workflow (#2855) This PR is to update the _Redis_ versions running in the CI, there are two different test setups according to environments, one on _Ubuntu_ and the other on _Windows_; - _Ubuntu_ job is now using docker image with latest _Redis_ release, it was using a relatively up-to-date version (**7.4-rc1 -> 7.4.2**) - _Windows_ job was using the binaries of an old version of _Redis_ which was build to run on _Windows_ env, but it is quite old(**Redis 3.0.503**) and lack of a set of features/changes that _StackExchange.Redis_ already capable of when running with up-to-date _Redis_ versions. So this is replaced with the **Redis 7.4.2** which is the latest GA Release(as of now) in package repository. Setup for _Windows_ essentially leverages [wsl setup](https://github.com/Vampire/setup-wsl) on windows and install+run the version of _Redis_ from its own package repository. The rest is similar to existing approach, using the same config files etc.. --- .github/workflows/CI.yml | 118 ++++++++++++++------ tests/RedisConfigs/.docker/Redis/Dockerfile | 2 +- 2 files changed, 84 insertions(+), 36 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 58856abbc..679e796fd 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -18,7 +18,12 @@ jobs: TERM: xterm # Enable color output in GitHub Actions steps: - name: Checkout code - uses: actions/checkout@v1 + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch the full history + - name: Start Redis Services (docker-compose) + working-directory: ./tests/RedisConfigs + run: docker compose -f docker-compose.yml up -d --wait - name: Install .NET SDK uses: actions/setup-dotnet@v3 with: @@ -27,9 +32,6 @@ jobs: 8.0.x - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true - - name: Start Redis Services (docker-compose) - working-directory: ./tests/RedisConfigs - run: docker compose -f docker-compose.yml up -d --wait - name: StackExchange.Redis.Tests run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true - uses: dorny/test-reporter@v1 @@ -52,39 +54,85 @@ jobs: DOCKER_BUILDKIT: 1 steps: - name: Checkout code - uses: actions/checkout@v1 - # - name: Install .NET SDK - # uses: actions/setup-dotnet@v3 - # with: - # dotnet-version: | - # 6.0.x - # 8.0.x + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch the full history + - uses: Vampire/setup-wsl@v2 + with: + distribution: Ubuntu-22.04 + - name: Install Redis + shell: wsl-bash {0} + working-directory: ./tests/RedisConfigs + run: | + apt-get update + apt-get install curl gpg lsb-release libgomp1 jq -y + curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg + chmod 644 /usr/share/keyrings/redis-archive-keyring.gpg + echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/redis.list + apt-get update + apt-get install -y redis + mkdir redis + - name: Run redis-server + shell: wsl-bash {0} + working-directory: ./tests/RedisConfigs/redis + run: | + pwd + ls . + # Run each server instance in order + redis-server ../Basic/primary-6379.conf & + redis-server ../Basic/replica-6380.conf & + redis-server ../Basic/secure-6381.conf & + redis-server ../Failover/primary-6382.conf & + redis-server ../Failover/replica-6383.conf & + redis-server ../Cluster/cluster-7000.conf --dir ../Cluster & + redis-server ../Cluster/cluster-7001.conf --dir ../Cluster & + redis-server ../Cluster/cluster-7002.conf --dir ../Cluster & + redis-server ../Cluster/cluster-7003.conf --dir ../Cluster & + redis-server ../Cluster/cluster-7004.conf --dir ../Cluster & + redis-server ../Cluster/cluster-7005.conf --dir ../Cluster & + redis-server ../Sentinel/redis-7010.conf & + redis-server ../Sentinel/redis-7011.conf & + redis-server ../Sentinel/sentinel-26379.conf --sentinel & + redis-server ../Sentinel/sentinel-26380.conf --sentinel & + redis-server ../Sentinel/sentinel-26381.conf --sentinel & + # Wait for server instances to get ready + sleep 5 + echo "Checking redis-server version with port 6379" + redis-cli -p 6379 INFO SERVER | grep redis_version || echo "Failed to get version for port 6379" + echo "Checking redis-server version with port 6380" + redis-cli -p 6380 INFO SERVER | grep redis_version || echo "Failed to get version for port 6380" + echo "Checking redis-server version with port 6381" + redis-cli -p 6381 INFO SERVER | grep redis_version || echo "Failed to get version for port 6381" + echo "Checking redis-server version with port 6382" + redis-cli -p 6382 INFO SERVER | grep redis_version || echo "Failed to get version for port 6382" + echo "Checking redis-server version with port 6383" + redis-cli -p 6383 INFO SERVER | grep redis_version || echo "Failed to get version for port 6383" + echo "Checking redis-server version with port 7000" + redis-cli -p 7000 INFO SERVER | grep redis_version || echo "Failed to get version for port 7000" + echo "Checking redis-server version with port 7001" + redis-cli -p 7001 INFO SERVER | grep redis_version || echo "Failed to get version for port 7001" + echo "Checking redis-server version with port 7002" + redis-cli -p 7002 INFO SERVER | grep redis_version || echo "Failed to get version for port 7002" + echo "Checking redis-server version with port 7003" + redis-cli -p 7003 INFO SERVER | grep redis_version || echo "Failed to get version for port 7003" + echo "Checking redis-server version with port 7004" + redis-cli -p 7004 INFO SERVER | grep redis_version || echo "Failed to get version for port 7004" + echo "Checking redis-server version with port 7005" + redis-cli -p 7005 INFO SERVER | grep redis_version || echo "Failed to get version for port 7005" + echo "Checking redis-server version with port 7010" + redis-cli -p 7010 INFO SERVER | grep redis_version || echo "Failed to get version for port 7010" + echo "Checking redis-server version with port 7011" + redis-cli -p 7011 INFO SERVER | grep redis_version || echo "Failed to get version for port 7011" + echo "Checking redis-server version with port 26379" + redis-cli -p 26379 INFO SERVER | grep redis_version || echo "Failed to get version for port 26379" + echo "Checking redis-server version with port 26380" + redis-cli -p 26380 INFO SERVER | grep redis_version || echo "Failed to get version for port 26380" + echo "Checking redis-server version with port 26381" + redis-cli -p 26381 INFO SERVER | grep redis_version || echo "Failed to get version for port 26381" + continue-on-error: true + - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true - # We can't do this combination - see https://github.com/actions/runner/issues/904 - # - name: Start Redis Services (docker-compose) - # working-directory: .\tests\RedisConfigs - # run: docker compose -f docker-compose.yml up -d --wait - - name: Start Redis Services (v3.0.503) - working-directory: .\tests\RedisConfigs\3.0.503 - run: | - .\redis-server.exe --service-install --service-name "redis-6379" "..\Basic\primary-6379-3.0.conf" - .\redis-server.exe --service-install --service-name "redis-6380" "..\Basic\replica-6380.conf" - .\redis-server.exe --service-install --service-name "redis-6381" "..\Basic\secure-6381.conf" - .\redis-server.exe --service-install --service-name "redis-6382" "..\Failover\primary-6382.conf" - .\redis-server.exe --service-install --service-name "redis-6383" "..\Failover\replica-6383.conf" - .\redis-server.exe --service-install --service-name "redis-7000" "..\Cluster\cluster-7000.conf" --dir "..\Cluster" - .\redis-server.exe --service-install --service-name "redis-7001" "..\Cluster\cluster-7001.conf" --dir "..\Cluster" - .\redis-server.exe --service-install --service-name "redis-7002" "..\Cluster\cluster-7002.conf" --dir "..\Cluster" - .\redis-server.exe --service-install --service-name "redis-7003" "..\Cluster\cluster-7003.conf" --dir "..\Cluster" - .\redis-server.exe --service-install --service-name "redis-7004" "..\Cluster\cluster-7004.conf" --dir "..\Cluster" - .\redis-server.exe --service-install --service-name "redis-7005" "..\Cluster\cluster-7005.conf" --dir "..\Cluster" - .\redis-server.exe --service-install --service-name "redis-7010" "..\Sentinel\redis-7010.conf" - .\redis-server.exe --service-install --service-name "redis-7011" "..\Sentinel\redis-7011.conf" - .\redis-server.exe --service-install --service-name "redis-26379" "..\Sentinel\sentinel-26379.conf" --sentinel - .\redis-server.exe --service-install --service-name "redis-26380" "..\Sentinel\sentinel-26380.conf" --sentinel - .\redis-server.exe --service-install --service-name "redis-26381" "..\Sentinel\sentinel-26381.conf" --sentinel - Start-Service redis-* - name: StackExchange.Redis.Tests run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true - uses: dorny/test-reporter@v1 diff --git a/tests/RedisConfigs/.docker/Redis/Dockerfile b/tests/RedisConfigs/.docker/Redis/Dockerfile index d4c7dec7c..4de03f221 100644 --- a/tests/RedisConfigs/.docker/Redis/Dockerfile +++ b/tests/RedisConfigs/.docker/Redis/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:7.4-rc1 +FROM redis:7.4.2 COPY --from=configs ./Basic /data/Basic/ COPY --from=configs ./Failover /data/Failover/ From cfb91054ba2ccce357f6d8b57646ec62238bd0c5 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 6 May 2025 10:53:04 -0400 Subject: [PATCH 327/435] Add release notes for 2.8.37 --- docs/ReleaseNotes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index c1e77aa17..7ed1cbbfe 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,8 @@ Current package versions: ## Unreleased No pending unreleased changes +## 2.8.37 + - Add `ConfigurationOptions.SetUserPemCertificate(...)` and `ConfigurationOptions.SetUserPfxCertificate(...)` methods to simplify using client certificates ([#2873 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2873)) - Fix: Move `AuthenticateAsClient` to fully async after dropping older framework support, to help client thread starvation in cases TLS negotiation stalls server-side ([#2878 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2878)) From 9420b59d1ba662b1e5896bcc18acf757e07b90bd Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 6 May 2025 10:59:09 -0400 Subject: [PATCH 328/435] Add missing release notes for #2864 --- docs/ReleaseNotes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 7ed1cbbfe..264c0d52b 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -12,6 +12,7 @@ No pending unreleased changes ## 2.8.37 - Add `ConfigurationOptions.SetUserPemCertificate(...)` and `ConfigurationOptions.SetUserPfxCertificate(...)` methods to simplify using client certificates ([#2873 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2873)) +- Add logging for when a Multiplexer reconfigures ([#2864 by st-dev-gh](https://github.com/StackExchange/StackExchange.Redis/pull/2864)) - Fix: Move `AuthenticateAsClient` to fully async after dropping older framework support, to help client thread starvation in cases TLS negotiation stalls server-side ([#2878 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2878)) ## 2.8.31 From cfbd474857b25de83e3501a02f4d685435611427 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 15 May 2025 15:13:12 +0100 Subject: [PATCH 329/435] Remove fuget link now phish --- docs/index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index b66f8f9a7..40acca077 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,7 +2,6 @@ StackExchange.Redis =================== - [Release Notes](ReleaseNotes) -- [API Browser (via fuget.org)](https://www.fuget.org/packages/StackExchange.Redis/) ## Overview From ad5f656eb08fbcaa1635c91807bb8f2fa9244219 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 10 Jun 2025 17:06:14 +0100 Subject: [PATCH 330/435] Support sharded pubsub commands (#2887) * Support sharded pubsub commands (#2498) Co-authored-by: vandyvilla Co-authored-by: xli Co-authored-by: atakavci --- docs/ReleaseNotes.md | 2 + docs/Timeouts.md | 2 +- src/StackExchange.Redis/ClientInfo.cs | 8 +- src/StackExchange.Redis/CommandMap.cs | 6 +- src/StackExchange.Redis/Enums/RedisCommand.cs | 6 + .../Interfaces/ISubscriber.cs | 1 + src/StackExchange.Redis/Message.cs | 3 + src/StackExchange.Redis/PhysicalBridge.cs | 7 +- src/StackExchange.Redis/PhysicalConnection.cs | 39 ++- .../PublicAPI/PublicAPI.Shipped.txt | 5 + src/StackExchange.Redis/RawResult.cs | 7 +- src/StackExchange.Redis/RedisChannel.cs | 52 +++- src/StackExchange.Redis/RedisDatabase.cs | 4 +- src/StackExchange.Redis/RedisFeatures.cs | 5 + src/StackExchange.Redis/RedisSubscriber.cs | 32 ++- src/StackExchange.Redis/ResultProcessor.cs | 48 ++-- src/StackExchange.Redis/ServerEndPoint.cs | 8 + .../StackExchange.Redis.Tests/ClusterTests.cs | 224 +++++++++++++++++- .../RedisRequest.cs | 4 +- .../StackExchange.Redis.Server/RedisServer.cs | 2 +- 20 files changed, 396 insertions(+), 69 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 264c0d52b..5eb4aae41 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,8 @@ Current package versions: ## Unreleased No pending unreleased changes +- Add support for sharded pub/sub via `RedisChannel.Sharded` - ([#2887 by vandyvilla, atakavci and mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2887)) + ## 2.8.37 - Add `ConfigurationOptions.SetUserPemCertificate(...)` and `ConfigurationOptions.SetUserPfxCertificate(...)` methods to simplify using client certificates ([#2873 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2873)) diff --git a/docs/Timeouts.md b/docs/Timeouts.md index 1c4ac3756..ea9830041 100644 --- a/docs/Timeouts.md +++ b/docs/Timeouts.md @@ -88,7 +88,7 @@ By default Redis Timeout exception(s) includes useful information, which can hel |qs | Queue-Awaiting-Response : {int}|There are x operations currently awaiting replies from redis server.| |aw | Active-Writer: {bool}|| |bw | Backlog-Writer: {enum} | Possible values are Inactive, Started, CheckingForWork, CheckingForTimeout, RecordingTimeout, WritingMessage, Flushing, MarkingInactive, RecordingWriteFailure, RecordingFault, SettingIdle, SpinningDown, Faulted| -|rs | Read-State: {enum}|Possible values are NotStarted, Init, RanToCompletion, Faulted, ReadSync, ReadAsync, UpdateWriteTime, ProcessBuffer, MarkProcessed, TryParseResult, MatchResult, PubSubMessage, PubSubPMessage, Reconfigure, InvokePubSub, DequeueResult, ComputeResult, CompletePendingMessage, NA| +|rs | Read-State: {enum}|Possible values are NotStarted, Init, RanToCompletion, Faulted, ReadSync, ReadAsync, UpdateWriteTime, ProcessBuffer, MarkProcessed, TryParseResult, MatchResult, PubSubMessage, PubSubSMessage, PubSubPMessage, Reconfigure, InvokePubSub, DequeueResult, ComputeResult, CompletePendingMessage, NA| |ws | Write-State: {enum}| Possible values are Initializing, Idle, Writing, Flushing, Flushed, NA| |in | Inbound-Bytes : {long}|there are x bytes waiting to be read from the input stream from redis| |in-pipe | Inbound-Pipe-Bytes: {long}|Bytes waiting to be read| diff --git a/src/StackExchange.Redis/ClientInfo.cs b/src/StackExchange.Redis/ClientInfo.cs index f04058495..c5ce0d0bf 100644 --- a/src/StackExchange.Redis/ClientInfo.cs +++ b/src/StackExchange.Redis/ClientInfo.cs @@ -129,10 +129,15 @@ public sealed class ClientInfo public string? Name { get; private set; } /// - /// Number of pattern matching subscriptions. + /// Number of pattern-matching subscriptions. /// public int PatternSubscriptionCount { get; private set; } + /// + /// Number of sharded subscriptions. + /// + public int ShardedSubscriptionCount { get; private set; } + /// /// The port of the client. /// @@ -236,6 +241,7 @@ internal static bool TryParse(string? input, [NotNullWhen(true)] out ClientInfo[ case "name": client.Name = value; break; case "sub": client.SubscriptionCount = Format.ParseInt32(value); break; case "psub": client.PatternSubscriptionCount = Format.ParseInt32(value); break; + case "ssub": client.ShardedSubscriptionCount = Format.ParseInt32(value); break; case "multi": client.TransactionCommandLength = Format.ParseInt32(value); break; case "cmd": client.LastCommand = value; break; case "flags": diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index d1e125bb3..31974cab9 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -31,7 +31,7 @@ public sealed class CommandMap RedisCommand.BLPOP, RedisCommand.BRPOP, RedisCommand.BRPOPLPUSH, // yeah, me neither! - RedisCommand.PSUBSCRIBE, RedisCommand.PUBLISH, RedisCommand.PUNSUBSCRIBE, RedisCommand.SUBSCRIBE, RedisCommand.UNSUBSCRIBE, + RedisCommand.PSUBSCRIBE, RedisCommand.PUBLISH, RedisCommand.PUNSUBSCRIBE, RedisCommand.SUBSCRIBE, RedisCommand.UNSUBSCRIBE, RedisCommand.SPUBLISH, RedisCommand.SSUBSCRIBE, RedisCommand.SUNSUBSCRIBE, RedisCommand.DISCARD, RedisCommand.EXEC, RedisCommand.MULTI, RedisCommand.UNWATCH, RedisCommand.WATCH, @@ -57,7 +57,9 @@ public sealed class CommandMap RedisCommand.BLPOP, RedisCommand.BRPOP, RedisCommand.BRPOPLPUSH, // yeah, me neither! - RedisCommand.PSUBSCRIBE, RedisCommand.PUNSUBSCRIBE, RedisCommand.SUBSCRIBE, RedisCommand.UNSUBSCRIBE, + RedisCommand.PSUBSCRIBE, RedisCommand.PUBLISH, RedisCommand.PUNSUBSCRIBE, RedisCommand.SUBSCRIBE, RedisCommand.UNSUBSCRIBE, RedisCommand.SPUBLISH, RedisCommand.SSUBSCRIBE, RedisCommand.SUNSUBSCRIBE, + + RedisCommand.DISCARD, RedisCommand.EXEC, RedisCommand.MULTI, RedisCommand.UNWATCH, RedisCommand.WATCH, RedisCommand.SCRIPT, diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index a4647d7eb..3909be4c2 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -181,6 +181,7 @@ internal enum RedisCommand SORT, SORT_RO, SPOP, + SPUBLISH, SRANDMEMBER, SREM, STRLEN, @@ -188,6 +189,8 @@ internal enum RedisCommand SUNION, SUNIONSTORE, SSCAN, + SSUBSCRIBE, + SUNSUBSCRIBE, SWAPDB, SYNC, @@ -447,10 +450,13 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.SMEMBERS: case RedisCommand.SMISMEMBER: case RedisCommand.SORT_RO: + case RedisCommand.SPUBLISH: case RedisCommand.SRANDMEMBER: + case RedisCommand.SSUBSCRIBE: case RedisCommand.STRLEN: case RedisCommand.SUBSCRIBE: case RedisCommand.SUNION: + case RedisCommand.SUNSUBSCRIBE: case RedisCommand.SSCAN: case RedisCommand.SYNC: case RedisCommand.TIME: diff --git a/src/StackExchange.Redis/Interfaces/ISubscriber.cs b/src/StackExchange.Redis/Interfaces/ISubscriber.cs index e0c509f49..a9c0bf298 100644 --- a/src/StackExchange.Redis/Interfaces/ISubscriber.cs +++ b/src/StackExchange.Redis/Interfaces/ISubscriber.cs @@ -110,6 +110,7 @@ public interface ISubscriber : IRedis /// See /// , /// . + /// . /// void UnsubscribeAll(CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index b89a6b946..fd75585a5 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -569,6 +569,9 @@ internal static bool RequiresDatabase(RedisCommand command) case RedisCommand.SLAVEOF: case RedisCommand.SLOWLOG: case RedisCommand.SUBSCRIBE: + case RedisCommand.SPUBLISH: + case RedisCommand.SSUBSCRIBE: + case RedisCommand.SUNSUBSCRIBE: case RedisCommand.SWAPDB: case RedisCommand.SYNC: case RedisCommand.TIME: diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index a10b241cb..f5d75c188 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -124,9 +124,12 @@ public enum State : byte public RedisCommand LastCommand { get; private set; } /// - /// If we have a connection, report the protocol being used. + /// If we have (or had) a connection, report the protocol being used. /// - public RedisProtocol? Protocol => physical?.Protocol; + /// The value remains after disconnect, so that appropriate follow-up actions (pub/sub etc) can work reliably. + public RedisProtocol? Protocol => _protocol == 0 ? default(RedisProtocol?) : _protocol; + private RedisProtocol _protocol; // note starts at zero, not RESP2 + internal void SetProtocol(RedisProtocol protocol) => _protocol = protocol; public void Dispose() { diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 51fac7c3d..c92290696 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -29,7 +29,7 @@ internal sealed partial class PhysicalConnection : IDisposable private const int DefaultRedisDatabaseCount = 16; - private static readonly CommandBytes message = "message", pmessage = "pmessage"; + private static readonly CommandBytes message = "message", pmessage = "pmessage", smessage = "smessage"; private static readonly Message[] ReusableChangeDatabaseCommands = Enumerable.Range(0, DefaultRedisDatabaseCount).Select( i => Message.Create(i, CommandFlags.FireAndForget, RedisCommand.SELECT)).ToArray(); @@ -276,7 +276,11 @@ private enum ReadMode : byte private RedisProtocol _protocol; // note starts at **zero**, not RESP2 public RedisProtocol? Protocol => _protocol == 0 ? null : _protocol; - internal void SetProtocol(RedisProtocol value) => _protocol = value; + internal void SetProtocol(RedisProtocol value) + { + _protocol = value; + BridgeCouldBeNull?.SetProtocol(value); + } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Trust me yo")] internal void Shutdown() @@ -384,7 +388,7 @@ public void RecordConnectionFailed( bool isInitialConnect = false, IDuplexPipe? connectingPipe = null) { - bool weAskedForThis = false; + bool weAskedForThis; Exception? outerException = innerException; IdentifyFailureType(innerException, ref failureType); var bridge = BridgeCouldBeNull; @@ -1644,9 +1648,9 @@ private void MatchResult(in RawResult result) // out of band message does not match to a queued message var items = result.GetItems(); - if (items.Length >= 3 && items[0].IsEqual(message)) + if (items.Length >= 3 && (items[0].IsEqual(message) || items[0].IsEqual(smessage))) { - _readStatus = ReadStatus.PubSubMessage; + _readStatus = items[0].IsEqual(message) ? ReadStatus.PubSubMessage : ReadStatus.PubSubSMessage; // special-case the configuration change broadcasts (we don't keep that in the usual pub/sub registry) var configChanged = muxer.ConfigurationChangedChannel; @@ -1668,8 +1672,17 @@ private void MatchResult(in RawResult result) } // invoke the handlers - var channel = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.PatternMode.Literal); - Trace("MESSAGE: " + channel); + RedisChannel channel; + if (items[0].IsEqual(message)) + { + channel = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.RedisChannelOptions.None); + Trace("MESSAGE: " + channel); + } + else // see check on outer-if that restricts to message / smessage + { + channel = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.RedisChannelOptions.Sharded); + Trace("SMESSAGE: " + channel); + } if (!channel.IsNull) { if (TryGetPubSubPayload(items[2], out var payload)) @@ -1690,19 +1703,22 @@ private void MatchResult(in RawResult result) { _readStatus = ReadStatus.PubSubPMessage; - var channel = items[2].AsRedisChannel(ChannelPrefix, RedisChannel.PatternMode.Literal); + var channel = items[2].AsRedisChannel(ChannelPrefix, RedisChannel.RedisChannelOptions.Pattern); + Trace("PMESSAGE: " + channel); if (!channel.IsNull) { if (TryGetPubSubPayload(items[3], out var payload)) { - var sub = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.PatternMode.Pattern); + var sub = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.RedisChannelOptions.Pattern); + _readStatus = ReadStatus.InvokePubSub; muxer.OnMessage(sub, channel, payload); } else if (TryGetMultiPubSubPayload(items[3], out var payloads)) { - var sub = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.PatternMode.Pattern); + var sub = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.RedisChannelOptions.Pattern); + _readStatus = ReadStatus.InvokePubSub; muxer.OnMessage(sub, channel, payloads); } @@ -1710,7 +1726,7 @@ private void MatchResult(in RawResult result) return; // AND STOP PROCESSING! } - // if it didn't look like "[p]message", then we still need to process the pending queue + // if it didn't look like "[p|s]message", then we still need to process the pending queue } Trace("Matching result..."); @@ -2110,6 +2126,7 @@ internal enum ReadStatus MatchResult, PubSubMessage, PubSubPMessage, + PubSubSMessage, Reconfigure, InvokePubSub, ResponseSequenceCheck, // high-integrity mode only diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 8263defd3..683332070 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1309,6 +1309,7 @@ StackExchange.Redis.RedisChannel StackExchange.Redis.RedisChannel.Equals(StackExchange.Redis.RedisChannel other) -> bool StackExchange.Redis.RedisChannel.IsNullOrEmpty.get -> bool StackExchange.Redis.RedisChannel.IsPattern.get -> bool +StackExchange.Redis.RedisChannel.IsSharded.get -> bool StackExchange.Redis.RedisChannel.PatternMode StackExchange.Redis.RedisChannel.PatternMode.Auto = 0 -> StackExchange.Redis.RedisChannel.PatternMode StackExchange.Redis.RedisChannel.PatternMode.Literal = 1 -> StackExchange.Redis.RedisChannel.PatternMode @@ -1893,4 +1894,8 @@ virtual StackExchange.Redis.RedisResult.Length.get -> int virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult! StackExchange.Redis.ConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void StackExchange.Redis.IConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void +StackExchange.Redis.RedisFeatures.ShardedPubSub.get -> bool +static StackExchange.Redis.RedisChannel.Sharded(byte[]? value) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.Sharded(string! value) -> StackExchange.Redis.RedisChannel +StackExchange.Redis.ClientInfo.ShardedSubscriptionCount.get -> int StackExchange.Redis.ConfigurationOptions.SetUserPfxCertificate(string! userCertificatePath, string? password = null) -> void diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 300503f57..55c44652b 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -161,7 +161,7 @@ public bool MoveNext() } public ReadOnlySequence Current { get; private set; } } - internal RedisChannel AsRedisChannel(byte[]? channelPrefix, RedisChannel.PatternMode mode) + internal RedisChannel AsRedisChannel(byte[]? channelPrefix, RedisChannel.RedisChannelOptions options) { switch (Resp2TypeBulkString) { @@ -169,12 +169,13 @@ internal RedisChannel AsRedisChannel(byte[]? channelPrefix, RedisChannel.Pattern case ResultType.BulkString: if (channelPrefix == null) { - return new RedisChannel(GetBlob(), mode); + return new RedisChannel(GetBlob(), options); } if (StartsWith(channelPrefix)) { byte[] copy = Payload.Slice(channelPrefix.Length).ToArray(); - return new RedisChannel(copy, mode); + + return new RedisChannel(copy, options); } return default; default: diff --git a/src/StackExchange.Redis/RedisChannel.cs b/src/StackExchange.Redis/RedisChannel.cs index 561cce21f..f6debd1eb 100644 --- a/src/StackExchange.Redis/RedisChannel.cs +++ b/src/StackExchange.Redis/RedisChannel.cs @@ -9,7 +9,18 @@ namespace StackExchange.Redis public readonly struct RedisChannel : IEquatable { internal readonly byte[]? Value; - internal readonly bool _isPatternBased; + + internal readonly RedisChannelOptions Options; + + [Flags] + internal enum RedisChannelOptions + { + None = 0, + Pattern = 1 << 0, + Sharded = 1 << 1, + } + + internal RedisCommand PublishCommand => IsSharded ? RedisCommand.SPUBLISH : RedisCommand.PUBLISH; /// /// Indicates whether the channel-name is either null or a zero-length value. @@ -19,7 +30,12 @@ namespace StackExchange.Redis /// /// Indicates whether this channel represents a wildcard pattern (see PSUBSCRIBE). /// - public bool IsPattern => _isPatternBased; + public bool IsPattern => (Options & RedisChannelOptions.Pattern) != 0; + + /// + /// Indicates whether this channel represents a shard channel (see SSUBSCRIBE). + /// + public bool IsSharded => (Options & RedisChannelOptions.Sharded) != 0; internal bool IsNull => Value == null; @@ -59,19 +75,35 @@ public static bool UseImplicitAutoPattern /// /// The name of the channel to create. /// The mode for name matching. - public RedisChannel(byte[]? value, PatternMode mode) : this(value, DeterminePatternBased(value, mode)) { } + public RedisChannel(byte[]? value, PatternMode mode) : this(value, DeterminePatternBased(value, mode) ? RedisChannelOptions.Pattern : RedisChannelOptions.None) + { + } /// /// Create a new redis channel from a string, explicitly controlling the pattern mode. /// /// The string name of the channel to create. /// The mode for name matching. - public RedisChannel(string value, PatternMode mode) : this(value == null ? null : Encoding.UTF8.GetBytes(value), mode) { } + public RedisChannel(string value, PatternMode mode) : this(value is null ? null : Encoding.UTF8.GetBytes(value), mode) + { + } + + /// + /// Create a new redis channel from a buffer, representing a sharded channel. + /// + /// The name of the channel to create. + public static RedisChannel Sharded(byte[]? value) => new(value, RedisChannelOptions.Sharded); + + /// + /// Create a new redis channel from a string, representing a sharded channel. + /// + /// The string name of the channel to create. + public static RedisChannel Sharded(string value) => new(value is null ? null : Encoding.UTF8.GetBytes(value), RedisChannelOptions.Sharded); - private RedisChannel(byte[]? value, bool isPatternBased) + internal RedisChannel(byte[]? value, RedisChannelOptions options) { Value = value; - _isPatternBased = isPatternBased; + Options = options; } private static bool DeterminePatternBased(byte[]? value, PatternMode mode) => mode switch @@ -123,7 +155,7 @@ private RedisChannel(byte[]? value, bool isPatternBased) /// The first to compare. /// The second to compare. public static bool operator ==(RedisChannel x, RedisChannel y) => - x._isPatternBased == y._isPatternBased && RedisValue.Equals(x.Value, y.Value); + x.Options == y.Options && RedisValue.Equals(x.Value, y.Value); /// /// Indicate whether two channel names are equal. @@ -171,10 +203,10 @@ private RedisChannel(byte[]? value, bool isPatternBased) /// Indicate whether two channel names are equal. /// /// The to compare to. - public bool Equals(RedisChannel other) => _isPatternBased == other._isPatternBased && RedisValue.Equals(Value, other.Value); + public bool Equals(RedisChannel other) => Options == other.Options && RedisValue.Equals(Value, other.Value); /// - public override int GetHashCode() => RedisValue.GetHashCode(Value) + (_isPatternBased ? 1 : 0); + public override int GetHashCode() => RedisValue.GetHashCode(Value) ^ (int)Options; /// /// Obtains a string representation of the channel name. @@ -203,7 +235,7 @@ internal RedisChannel Clone() return this; } var copy = (byte[])Value.Clone(); // defensive array copy - return new RedisChannel(copy, _isPatternBased); + return new RedisChannel(copy, Options); } /// diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 7468bdb64..716176662 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1575,14 +1575,14 @@ public Task StringLongestCommonSubsequenceWithMatchesAsync(Redis public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { if (channel.IsNullOrEmpty) throw new ArgumentNullException(nameof(channel)); - var msg = Message.Create(-1, flags, RedisCommand.PUBLISH, channel, message); + var msg = Message.Create(-1, flags, channel.PublishCommand, channel, message); return ExecuteSync(msg, ResultProcessor.Int64); } public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { if (channel.IsNullOrEmpty) throw new ArgumentNullException(nameof(channel)); - var msg = Message.Create(-1, flags, RedisCommand.PUBLISH, channel, message); + var msg = Message.Create(-1, flags, channel.PublishCommand, channel, message); return ExecuteAsync(msg, ResultProcessor.Int64); } diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 225516433..faba07e68 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -186,6 +186,11 @@ public RedisFeatures(Version version) /// public bool SetVaradicAddRemove => Version.IsAtLeast(v2_4_0); + /// + /// Are SSUBSCRIBE and SPUBLISH available? + /// + public bool ShardedPubSub => Version.IsAtLeast(v7_0_0_rc1); + /// /// Are ZPOPMIN and ZPOPMAX available? /// diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index ee28f4c56..b641baf05 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -182,15 +182,25 @@ public Subscription(CommandFlags flags) /// internal Message GetMessage(RedisChannel channel, SubscriptionAction action, CommandFlags flags, bool internalCall) { - var isPattern = channel._isPatternBased; + var isPattern = channel.IsPattern; + var isSharded = channel.IsSharded; var command = action switch { - SubscriptionAction.Subscribe when isPattern => RedisCommand.PSUBSCRIBE, - SubscriptionAction.Unsubscribe when isPattern => RedisCommand.PUNSUBSCRIBE, - - SubscriptionAction.Subscribe when !isPattern => RedisCommand.SUBSCRIBE, - SubscriptionAction.Unsubscribe when !isPattern => RedisCommand.UNSUBSCRIBE, - _ => throw new ArgumentOutOfRangeException(nameof(action), "This would be an impressive boolean feat"), + SubscriptionAction.Subscribe => channel.Options switch + { + RedisChannel.RedisChannelOptions.None => RedisCommand.SUBSCRIBE, + RedisChannel.RedisChannelOptions.Pattern => RedisCommand.PSUBSCRIBE, + RedisChannel.RedisChannelOptions.Sharded => RedisCommand.SSUBSCRIBE, + _ => Unknown(action, channel.Options), + }, + SubscriptionAction.Unsubscribe => channel.Options switch + { + RedisChannel.RedisChannelOptions.None => RedisCommand.UNSUBSCRIBE, + RedisChannel.RedisChannelOptions.Pattern => RedisCommand.PUNSUBSCRIBE, + RedisChannel.RedisChannelOptions.Sharded => RedisCommand.SUNSUBSCRIBE, + _ => Unknown(action, channel.Options), + }, + _ => Unknown(action, channel.Options), }; // TODO: Consider flags here - we need to pass Fire and Forget, but don't want to intermingle Primary/Replica @@ -203,6 +213,9 @@ internal Message GetMessage(RedisChannel channel, SubscriptionAction action, Com return msg; } + private RedisCommand Unknown(SubscriptionAction action, RedisChannel.RedisChannelOptions options) + => throw new ArgumentException($"Unable to determine pub/sub operation for '{action}' against '{options}'"); + public void Add(Action? handler, ChannelMessageQueue? queue) { if (handler != null) @@ -370,14 +383,14 @@ private static void ThrowIfNull(in RedisChannel channel) public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { ThrowIfNull(channel); - var msg = Message.Create(-1, flags, RedisCommand.PUBLISH, channel, message); + var msg = Message.Create(-1, flags, channel.PublishCommand, channel, message); return ExecuteSync(msg, ResultProcessor.Int64); } public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { ThrowIfNull(channel); - var msg = Message.Create(-1, flags, RedisCommand.PUBLISH, channel, message); + var msg = Message.Create(-1, flags, channel.PublishCommand, channel, message); return ExecuteAsync(msg, ResultProcessor.Int64); } @@ -515,6 +528,7 @@ private bool UnregisterSubscription(in RedisChannel channel, Action public static readonly ResultProcessor PersistResultArray = new PersistResultArrayProcessor(); public static readonly ResultProcessor - RedisChannelArrayLiteral = new RedisChannelArrayProcessor(RedisChannel.PatternMode.Literal); + RedisChannelArrayLiteral = new RedisChannelArrayProcessor(RedisChannel.RedisChannelOptions.None); public static readonly ResultProcessor RedisKey = new RedisKeyProcessor(); @@ -354,8 +354,7 @@ public bool TryParse(in RawResult result, out TimeSpan? expiry) switch (result.Resp2TypeBulkString) { case ResultType.Integer: - long time; - if (result.TryGetInt64(out time)) + if (result.TryGetInt64(out long time)) { if (time < 0) { @@ -469,7 +468,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var newServer = message.Command switch { - RedisCommand.SUBSCRIBE or RedisCommand.PSUBSCRIBE => connection.BridgeCouldBeNull?.ServerEndPoint, + RedisCommand.SUBSCRIBE or RedisCommand.SSUBSCRIBE or RedisCommand.PSUBSCRIBE => connection.BridgeCouldBeNull?.ServerEndPoint, _ => null, }; Subscription?.SetCurrentServer(newServer); @@ -1253,8 +1252,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes switch (result.Resp2TypeBulkString) { case ResultType.Integer: - long i64; - if (result.TryGetInt64(out i64)) + if (result.TryGetInt64(out long i64)) { SetResult(message, i64); return true; @@ -1262,8 +1260,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes break; case ResultType.SimpleString: case ResultType.BulkString: - double val; - if (result.TryGetDouble(out val)) + if (result.TryGetDouble(out double val)) { SetResult(message, val); return true; @@ -1366,8 +1363,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes case ResultType.Integer: case ResultType.SimpleString: case ResultType.BulkString: - long i64; - if (result.TryGetInt64(out i64)) + if (result.TryGetInt64(out long i64)) { SetResult(message, i64); return true; @@ -1423,8 +1419,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes SetResult(message, null); return true; } - double val; - if (result.TryGetDouble(out val)) + if (result.TryGetDouble(out double val)) { SetResult(message, val); return true; @@ -1449,8 +1444,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes SetResult(message, null); return true; } - long i64; - if (result.TryGetInt64(out i64)) + if (result.TryGetInt64(out long i64)) { SetResult(message, i64); return true; @@ -1504,20 +1498,20 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes private sealed class RedisChannelArrayProcessor : ResultProcessor { - private readonly RedisChannel.PatternMode mode; - public RedisChannelArrayProcessor(RedisChannel.PatternMode mode) + private readonly RedisChannel.RedisChannelOptions options; + public RedisChannelArrayProcessor(RedisChannel.RedisChannelOptions options) { - this.mode = mode; + this.options = options; } private readonly struct ChannelState // I would use a value-tuple here, but that is binding hell { public readonly byte[]? Prefix; - public readonly RedisChannel.PatternMode Mode; - public ChannelState(byte[]? prefix, RedisChannel.PatternMode mode) + public readonly RedisChannel.RedisChannelOptions Options; + public ChannelState(byte[]? prefix, RedisChannel.RedisChannelOptions options) { Prefix = prefix; - Mode = mode; + Options = options; } } protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) @@ -1526,8 +1520,8 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { case ResultType.Array: var final = result.ToArray( - (in RawResult item, in ChannelState state) => item.AsRedisChannel(state.Prefix, state.Mode), - new ChannelState(connection.ChannelPrefix, mode))!; + (in RawResult item, in ChannelState state) => item.AsRedisChannel(state.Prefix, state.Options), + new ChannelState(connection.ChannelPrefix, options))!; SetResult(message, final); return true; @@ -2167,7 +2161,10 @@ private sealed class RedisStreamInterleavedProcessor : ValuePairInterleavedProce protected override bool AllowJaggedPairs => false; // we only use this on a flattened map public static readonly RedisStreamInterleavedProcessor Instance = new(); - private RedisStreamInterleavedProcessor() { } + private RedisStreamInterleavedProcessor() + { + } + protected override RedisStream Parse(in RawResult first, in RawResult second, object? state) => new(key: first.AsRedisKey(), entries: ((MultiStreamProcessor)state!).ParseRedisStreamEntries(second)); } @@ -2549,7 +2546,10 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal class StreamNameValueEntryProcessor : ValuePairInterleavedProcessorBase { public static readonly StreamNameValueEntryProcessor Instance = new(); - private StreamNameValueEntryProcessor() { } + private StreamNameValueEntryProcessor() + { + } + protected override NameValueEntry Parse(in RawResult first, in RawResult second, object? state) => new NameValueEntry(first.AsRedisValue(), second.AsRedisValue()); } diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 8b099afd2..55fe6cff3 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -260,6 +260,8 @@ public void Dispose() case RedisCommand.UNSUBSCRIBE: case RedisCommand.PSUBSCRIBE: case RedisCommand.PUNSUBSCRIBE: + case RedisCommand.SSUBSCRIBE: + case RedisCommand.SUNSUBSCRIBE: message.SetForSubscriptionBridge(); break; } @@ -278,6 +280,8 @@ public void Dispose() case RedisCommand.UNSUBSCRIBE: case RedisCommand.PSUBSCRIBE: case RedisCommand.PUNSUBSCRIBE: + case RedisCommand.SSUBSCRIBE: + case RedisCommand.SUNSUBSCRIBE: if (!KnowOrAssumeResp3()) { return subscription ?? (create ? subscription = CreateBridge(ConnectionType.Subscription, null) : null); @@ -632,6 +636,10 @@ internal void OnDisconnected(PhysicalBridge bridge) if (bridge == interactive) { CompletePendingConnectionMonitors("Disconnected"); + if (Protocol is RedisProtocol.Resp3) + { + Multiplexer.UpdateSubscriptions(); + } } else if (bridge == subscription) { diff --git a/tests/StackExchange.Redis.Tests/ClusterTests.cs b/tests/StackExchange.Redis.Tests/ClusterTests.cs index 742ce51bb..b1f9d01cd 100644 --- a/tests/StackExchange.Redis.Tests/ClusterTests.cs +++ b/tests/StackExchange.Redis.Tests/ClusterTests.cs @@ -15,7 +15,9 @@ namespace StackExchange.Redis.Tests; [Collection(SharedConnectionFixture.Key)] public class ClusterTests : TestBase { - public ClusterTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + public ClusterTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) + { + } protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; @@ -746,4 +748,224 @@ public void ConnectIncludesSubscriber() Assert.Equal(PhysicalBridge.State.ConnectedEstablished, server.SubscriptionConnectionState); } } + + [Fact] + public async Task TestShardedPubsubSubscriberAgainstReconnects() + { + var channel = RedisChannel.Sharded(Me()); + using var conn = Create(allowAdmin: true, keepAlive: 1, connectTimeout: 3000, shared: false, require: RedisFeatures.v7_0_0_rc1); + Assert.True(conn.IsConnected); + var db = conn.GetDatabase(); + Assert.Equal(0, await db.PublishAsync(channel, "noClientReceivesThis")); + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + + var pubsub = conn.GetSubscriber(); + List<(RedisChannel, RedisValue)> received = new(); + var queue = await pubsub.SubscribeAsync(channel); + _ = Task.Run(async () => + { + // use queue API to have control over order + await foreach (var item in queue) + { + lock (received) + { + if (item.Channel.IsSharded && item.Channel == channel) received.Add((item.Channel, item.Message)); + } + } + }); + Assert.Equal(1, conn.GetSubscriptionsCount()); + + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + await db.PingAsync(); + + for (int i = 0; i < 5; i++) + { + // check we get a hit + Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); + } + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + + // this is endpoint at index 1 which has the hashslot for "testShardChannel" + var server = conn.GetServer(conn.GetEndPoints()[1]); + server.SimulateConnectionFailure(SimulatedFailureType.All); + SetExpectedAmbientFailureCount(2); + + await Task.Delay(4000); + for (int i = 0; i < 5; i++) + { + // check we get a hit + Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); + } + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + + Assert.Equal(1, conn.GetSubscriptionsCount()); + Assert.Equal(10, received.Count); + ClearAmbientFailures(); + } + + [Fact] + public async Task TestShardedPubsubSubscriberAgainsHashSlotMigration() + { + var channel = RedisChannel.Sharded(Me()); + using var conn = Create(allowAdmin: true, keepAlive: 1, connectTimeout: 3000, shared: false, require: RedisFeatures.v7_0_0_rc1); + Assert.True(conn.IsConnected); + var db = conn.GetDatabase(); + Assert.Equal(0, await db.PublishAsync(channel, "noClientReceivesThis")); + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + + var pubsub = conn.GetSubscriber(); + List<(RedisChannel, RedisValue)> received = new(); + var queue = await pubsub.SubscribeAsync(channel); + _ = Task.Run(async () => + { + // use queue API to have control over order + await foreach (var item in queue) + { + lock (received) + { + if (item.Channel.IsSharded && item.Channel == channel) received.Add((item.Channel, item.Message)); + } + } + }); + Assert.Equal(1, conn.GetSubscriptionsCount()); + + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + await db.PingAsync(); + + for (int i = 0; i < 5; i++) + { + // check we get a hit + Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); + } + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + + // lets migrate the slot for "testShardChannel" to another node + DoHashSlotMigration(); + + await Task.Delay(4000); + for (int i = 0; i < 5; i++) + { + // check we get a hit + Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); + } + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + + Assert.Equal(1, conn.GetSubscriptionsCount()); + Assert.Equal(10, received.Count); + RollbackHashSlotMigration(); + ClearAmbientFailures(); + } + + private void DoHashSlotMigration() + { + MigrateSlotForTestShardChannel(false); + } + private void RollbackHashSlotMigration() + { + MigrateSlotForTestShardChannel(true); + } + + private void MigrateSlotForTestShardChannel(bool rollback) + { + int hashSlotForTestShardChannel = 7177; + using var conn = Create(allowAdmin: true, keepAlive: 1, connectTimeout: 5000, shared: false); + var servers = conn.GetServers(); + IServer? serverWithPort7000 = null; + IServer? serverWithPort7001 = null; + + string nodeIdForPort7000 = "780813af558af81518e58e495d63b6e248e80adf"; + string nodeIdForPort7001 = "ea828c6074663c8bd4e705d3e3024d9d1721ef3b"; + foreach (var server in servers) + { + string id = server.Execute("CLUSTER", "MYID").ToString(); + if (id == nodeIdForPort7000) + { + serverWithPort7000 = server; + } + if (id == nodeIdForPort7001) + { + serverWithPort7001 = server; + } + } + + IServer fromServer, toServer; + string fromNode, toNode; + if (rollback) + { + fromServer = serverWithPort7000!; + fromNode = nodeIdForPort7000; + toServer = serverWithPort7001!; + toNode = nodeIdForPort7001; + } + else + { + fromServer = serverWithPort7001!; + fromNode = nodeIdForPort7001; + toServer = serverWithPort7000!; + toNode = nodeIdForPort7000; + } + + Assert.Equal("OK", toServer.Execute("CLUSTER", "SETSLOT", hashSlotForTestShardChannel, "IMPORTING", fromNode).ToString()); + Assert.Equal("OK", fromServer.Execute("CLUSTER", "SETSLOT", hashSlotForTestShardChannel, "MIGRATING", toNode).ToString()); + Assert.Equal("OK", toServer.Execute("CLUSTER", "SETSLOT", hashSlotForTestShardChannel, "NODE", toNode).ToString()); + Assert.Equal("OK", fromServer!.Execute("CLUSTER", "SETSLOT", hashSlotForTestShardChannel, "NODE", toNode).ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ClusterPubSub(bool sharded) + { + var guid = Guid.NewGuid().ToString(); + var channel = sharded ? RedisChannel.Sharded(guid) : RedisChannel.Literal(guid); + using var conn = Create(keepAlive: 1, connectTimeout: 3000, shared: false, require: sharded ? RedisFeatures.v7_0_0_rc1 : RedisFeatures.v2_0_0); + Assert.True(conn.IsConnected); + + var pubsub = conn.GetSubscriber(); + List<(RedisChannel, RedisValue)> received = new(); + var queue = await pubsub.SubscribeAsync(channel); + _ = Task.Run(async () => + { + // use queue API to have control over order + await foreach (var item in queue) + { + lock (received) + { + received.Add((item.Channel, item.Message)); + } + } + }); + + var db = conn.GetDatabase(); + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + await db.PingAsync(); + for (int i = 0; i < 10; i++) + { + // check we get a hit + Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); + } + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + await db.PingAsync(); + await pubsub.UnsubscribeAsync(channel); + + (RedisChannel Channel, RedisValue Value)[] snap; + lock (received) + { + snap = received.ToArray(); // in case of concurrency + } + Log("items received: {0}", snap.Length); + Assert.Equal(10, snap.Length); + // separate log and validate loop here simplifies debugging (ask me how I know!) + for (int i = 0; i < 10; i++) + { + var pair = snap[i]; + Log("element {0}: {1}/{2}", i, pair.Channel, pair.Value); + } + for (int i = 0; i < 10; i++) + { + var pair = snap[i]; + Assert.Equal(channel, pair.Channel); + Assert.Equal(i, pair.Value); + } + } } diff --git a/toys/StackExchange.Redis.Server/RedisRequest.cs b/toys/StackExchange.Redis.Server/RedisRequest.cs index 54102815c..36d133bab 100644 --- a/toys/StackExchange.Redis.Server/RedisRequest.cs +++ b/toys/StackExchange.Redis.Server/RedisRequest.cs @@ -45,8 +45,8 @@ public int GetInt32(int index) public RedisKey GetKey(int index) => _inner[index].AsRedisKey(); - public RedisChannel GetChannel(int index, RedisChannel.PatternMode mode) - => _inner[index].AsRedisChannel(null, mode); + internal RedisChannel GetChannel(int index, RedisChannel.RedisChannelOptions options) + => _inner[index].AsRedisChannel(null, options); internal bool TryGetCommandBytes(int i, out CommandBytes command) { diff --git a/toys/StackExchange.Redis.Server/RedisServer.cs b/toys/StackExchange.Redis.Server/RedisServer.cs index 63efbfd1b..52728fd44 100644 --- a/toys/StackExchange.Redis.Server/RedisServer.cs +++ b/toys/StackExchange.Redis.Server/RedisServer.cs @@ -479,7 +479,7 @@ private TypedRedisValue SubscribeImpl(RedisClient client, RedisRequest request) int index = 0; request.TryGetCommandBytes(0, out var cmd); var cmdString = TypedRedisValue.BulkString(cmd.ToArray()); - var mode = cmd[0] == (byte)'p' ? RedisChannel.PatternMode.Pattern : RedisChannel.PatternMode.Literal; + var mode = cmd[0] == (byte)'p' ? RedisChannel.RedisChannelOptions.Pattern : RedisChannel.RedisChannelOptions.None; for (int i = 1; i < request.Count; i++) { var channel = request.GetChannel(i, mode); From 2cb97afe6df2f038430bdccceb55762c2193eedc Mon Sep 17 00:00:00 2001 From: mgravell Date: Tue, 10 Jun 2025 20:55:40 +0100 Subject: [PATCH 331/435] release notes 2.8.41 --- docs/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 5eb4aae41..62edec160 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -7,8 +7,11 @@ Current package versions: | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | ## Unreleased + No pending unreleased changes +## 2.8.41 + - Add support for sharded pub/sub via `RedisChannel.Sharded` - ([#2887 by vandyvilla, atakavci and mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2887)) ## 2.8.37 From 7351ad966248f6d32408525c82551bfb1880214e Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 9 Jul 2025 16:41:46 +0100 Subject: [PATCH 332/435] (code-rot) Update package dependencies (#2906) * initial cleanup from migration to Rider * update lib-refs; only non-test one is logging-abstractions, where we'll eat the 8.0 upgrade from 8.0 onwards * net9 on CI * release notes --- .github/workflows/CI.yml | 1 + Directory.Packages.props | 15 +- StackExchange.Redis.sln | 1 - appveyor.yml | 2 + docs/ReleaseNotes.md | 2 +- src/StackExchange.Redis/Interfaces/IServer.cs | 10 +- .../KeyspaceIsolation/KeyPrefixed.cs | 4 +- .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 2 +- src/StackExchange.Redis/LuaScript.cs | 8 +- src/StackExchange.Redis/PhysicalConnection.cs | 4 +- src/StackExchange.Redis/ResultProcessor.cs | 10 +- src/StackExchange.Redis/ServerEndPoint.cs | 2 +- .../StackExchange.Redis.csproj | 5 +- src/StackExchange.Redis/TextWriterLogger.cs | 5 + .../KeyPrefixedDatabaseTests.cs | 4 +- .../KeyPrefixedTests.cs | 2 +- .../StackExchange.Redis.Tests/LockingTests.cs | 8 +- .../StackExchange.Redis.Tests/LoggerTests.cs | 12 ++ .../RespProtocolTests.cs | 2 +- .../ScriptingTests.cs | 173 +++++++++--------- .../SortedSetTests.cs | 2 +- 21 files changed, 149 insertions(+), 125 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 679e796fd..1796710a7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -30,6 +30,7 @@ jobs: dotnet-version: | 6.0.x 8.0.x + 9.0.x - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true - name: StackExchange.Redis.Tests diff --git a/Directory.Packages.props b/Directory.Packages.props index 12097728a..6d15ad199 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,25 +8,24 @@ - - + - + - + - + - + - - + + \ No newline at end of file diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index f59d5f6fc..18a30a9af 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -93,7 +93,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{73A5C363-CA1F-44C4-9A9B-EF791A76BA6A}" ProjectSection(SolutionItems) = preProject tests\.editorconfig = tests\.editorconfig - tests\Directory.Build.props = tests\Directory.Build.props tests\Directory.Build.targets = tests\Directory.Build.targets EndProjectSection EndProject diff --git a/appveyor.yml b/appveyor.yml index d9dd350af..b180c6544 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,6 +9,8 @@ install: choco install dotnet-6.0-sdk choco install dotnet-7.0-sdk + + choco install dotnet-9.0-sdk cd tests\RedisConfigs\3.0.503 diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 62edec160..c775f6cb3 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -No pending unreleased changes +- Package updates ([#2906 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2906)) ## 2.8.41 diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index fad2d4232..4971c7f18 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -89,7 +89,7 @@ public partial interface IServer : IRedis /// void ClientKill(EndPoint endpoint, CommandFlags flags = CommandFlags.None); - /// + /// Task ClientKillAsync(EndPoint endpoint, CommandFlags flags = CommandFlags.None); /// @@ -104,7 +104,7 @@ public partial interface IServer : IRedis /// long ClientKill(long? id = null, ClientType? clientType = null, EndPoint? endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None); - /// + /// Task ClientKillAsync(long? id = null, ClientType? clientType = null, EndPoint? endpoint = null, bool skipMe = true, CommandFlags flags = CommandFlags.None); /// @@ -475,17 +475,17 @@ public partial interface IServer : IRedis /// void Shutdown(ShutdownMode shutdownMode = ShutdownMode.Default, CommandFlags flags = CommandFlags.None); - /// + /// [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicaOfAsync) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] void SlaveOf(EndPoint master, CommandFlags flags = CommandFlags.None); - /// + /// [Obsolete("Starting with Redis version 5, Redis has moved to 'replica' terminology. Please use " + nameof(ReplicaOfAsync) + " instead, this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] Task SlaveOfAsync(EndPoint master, CommandFlags flags = CommandFlags.None); - /// + /// [Obsolete("Please use " + nameof(ReplicaOfAsync) + ", this will be removed in 3.0.")] [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] void ReplicaOf(EndPoint master, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index b97bba73b..f45c29886 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -351,7 +351,7 @@ public Task ScriptEvaluateAsync(byte[] hash, RedisKey[]? keys = nul public Task ScriptEvaluateAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - Inner.ScriptEvaluateAsync(script, ToInner(keys), values, flags); + Inner.ScriptEvaluateAsync(script: script, keys: ToInner(keys), values: values, flags: flags); public Task ScriptEvaluateAsync(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? @@ -367,7 +367,7 @@ public Task ScriptEvaluateReadOnlyAsync(byte[] hash, RedisKey[]? ke public Task ScriptEvaluateReadOnlyAsync(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - Inner.ScriptEvaluateAsync(script, ToInner(keys), values, flags); + Inner.ScriptEvaluateAsync(script: script, keys: ToInner(keys), values: values, flags: flags); public Task SetAddAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) => Inner.SetAddAsync(ToInner(key), values, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index 75d93d0f9..a19dd0b7a 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -338,7 +338,7 @@ public RedisResult ScriptEvaluate(byte[] hash, RedisKey[]? keys = null, RedisVal public RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? - Inner.ScriptEvaluate(script, ToInner(keys), values, flags); + Inner.ScriptEvaluate(script: script, keys: ToInner(keys), values: values, flags: flags); public RedisResult ScriptEvaluate(LuaScript script, object? parameters = null, CommandFlags flags = CommandFlags.None) => // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those? diff --git a/src/StackExchange.Redis/LuaScript.cs b/src/StackExchange.Redis/LuaScript.cs index 8a9bdbcc1..7a99f635a 100644 --- a/src/StackExchange.Redis/LuaScript.cs +++ b/src/StackExchange.Redis/LuaScript.cs @@ -148,7 +148,7 @@ internal void ExtractParameters(object? ps, RedisKey? keyPrefix, out RedisKey[]? public RedisResult Evaluate(IDatabase db, object? ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None) { ExtractParameters(ps, withKeyPrefix, out RedisKey[]? keys, out RedisValue[]? args); - return db.ScriptEvaluate(ExecutableScript, keys, args, flags); + return db.ScriptEvaluate(script: ExecutableScript, keys: keys, values: args, flags: flags); } /// @@ -161,7 +161,7 @@ public RedisResult Evaluate(IDatabase db, object? ps = null, RedisKey? withKeyPr public Task EvaluateAsync(IDatabaseAsync db, object? ps = null, RedisKey? withKeyPrefix = null, CommandFlags flags = CommandFlags.None) { ExtractParameters(ps, withKeyPrefix, out RedisKey[]? keys, out RedisValue[]? args); - return db.ScriptEvaluateAsync(ExecutableScript, keys, args, flags); + return db.ScriptEvaluateAsync(script: ExecutableScript, keys: keys, values: args, flags: flags); } /// @@ -269,7 +269,7 @@ public RedisResult Evaluate(IDatabase db, object? ps = null, RedisKey? withKeyPr { Original.ExtractParameters(ps, withKeyPrefix, out RedisKey[]? keys, out RedisValue[]? args); - return db.ScriptEvaluate(ExecutableScript, keys, args, flags); + return db.ScriptEvaluate(script: ExecutableScript, keys: keys, values: args, flags: flags); } /// @@ -287,7 +287,7 @@ public Task EvaluateAsync(IDatabaseAsync db, object? ps = null, Red { Original.ExtractParameters(ps, withKeyPrefix, out RedisKey[]? keys, out RedisValue[]? args); - return db.ScriptEvaluateAsync(ExecutableScript, keys, args, flags); + return db.ScriptEvaluateAsync(script: ExecutableScript, keys: keys, values: args, flags: flags); } } } diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index c92290696..8a0bad393 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -1598,8 +1598,8 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock catch (Exception ex) { Debug.WriteLine(ex.Message); - bridge.Multiplexer?.SetAuthSuspect(ex); - bridge.Multiplexer?.Logger?.LogError(ex, ex.Message); + bridge.Multiplexer.SetAuthSuspect(ex); + bridge.Multiplexer.Logger?.LogError(ex, ex.Message); throw; } log?.LogInformation($"TLS connection established successfully using protocol: {ssl.SslProtocol}"); diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 47974b278..03e0a8577 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -234,11 +234,11 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in { if (result.StartsWith(CommonReplies.NOAUTH)) { - bridge?.Multiplexer?.SetAuthSuspect(new RedisServerException("NOAUTH Returned - connection has not yet authenticated")); + bridge?.Multiplexer.SetAuthSuspect(new RedisServerException("NOAUTH Returned - connection has not yet authenticated")); } else if (result.StartsWith(CommonReplies.WRONGPASS)) { - bridge?.Multiplexer?.SetAuthSuspect(new RedisServerException(result.ToString())); + bridge?.Multiplexer.SetAuthSuspect(new RedisServerException(result.ToString())); } var server = bridge?.ServerEndPoint; @@ -259,7 +259,7 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in // no point sending back to same server, and no point sending to a dead server if (!Equals(server?.EndPoint, endpoint)) { - if (bridge == null) + if (bridge is null) { // already toast } @@ -308,7 +308,7 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in { bridge?.Multiplexer.OnErrorMessage(server.EndPoint, err); } - bridge?.Multiplexer?.Trace("Completed with error: " + err + " (" + GetType().Name + ")", ToString()); + bridge?.Multiplexer.Trace("Completed with error: " + err + " (" + GetType().Name + ")", ToString()); if (unableToConnectError) { ConnectionFail(message, ConnectionFailureType.UnableToConnect, err); @@ -323,7 +323,7 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in bool coreResult = SetResultCore(connection, message, result); if (coreResult) { - bridge?.Multiplexer?.Trace("Completed with success: " + result.ToString() + " (" + GetType().Name + ")", ToString()); + bridge?.Multiplexer.Trace("Completed with success: " + result.ToString() + " (" + GetType().Name + ")", ToString()); } else { diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 55fe6cff3..1e32275d6 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -708,7 +708,7 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) } catch (Exception ex) { - connection.RecordConnectionFailed(ConnectionFailureType.InternalFailure, ex); + connection?.RecordConnectionFailed(ConnectionFailureType.InternalFailure, ex); } } diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 17e34eab9..e1b428a36 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -16,8 +16,9 @@ - - + + + diff --git a/src/StackExchange.Redis/TextWriterLogger.cs b/src/StackExchange.Redis/TextWriterLogger.cs index 0582b70d3..4d8507b95 100644 --- a/src/StackExchange.Redis/TextWriterLogger.cs +++ b/src/StackExchange.Redis/TextWriterLogger.cs @@ -17,7 +17,12 @@ public TextWriterLogger(TextWriter writer, ILogger? wrapped) _wrapped = wrapped; } +#if NET8_0_OR_GREATER + public IDisposable? BeginScope(TState state) where TState : notnull => NothingDisposable.Instance; +#else public IDisposable BeginScope(TState state) => NothingDisposable.Instance; +#endif + public bool IsEnabled(LogLevel logLevel) => _writer is not null || _wrapped is not null; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs index f467aca24..e73225031 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs @@ -605,8 +605,8 @@ public void ScriptEvaluate_2() RedisValue[] values = Array.Empty(); RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - prefixed.ScriptEvaluate("script", keys, values, CommandFlags.None); - mock.Received().ScriptEvaluate("script", Arg.Is(valid), values, CommandFlags.None); + prefixed.ScriptEvaluate(script: "script", keys: keys, values: values, flags: CommandFlags.None); + mock.Received().ScriptEvaluate(script: "script", keys: Arg.Is(valid), values: values, flags: CommandFlags.None); } [Fact] diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs index aebc8fc6d..e48227974 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs @@ -550,7 +550,7 @@ public async Task ScriptEvaluateAsync_2() RedisKey[] keys = new RedisKey[] { "a", "b" }; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.ScriptEvaluateAsync("script", keys, values, CommandFlags.None); - await mock.Received().ScriptEvaluateAsync("script", Arg.Is(valid), values, CommandFlags.None); + await mock.Received().ScriptEvaluateAsync(script: "script", keys: Arg.Is(valid), values: values, flags: CommandFlags.None); } [Fact] diff --git a/tests/StackExchange.Redis.Tests/LockingTests.cs b/tests/StackExchange.Redis.Tests/LockingTests.cs index c2aa3611f..4f9dbd402 100644 --- a/tests/StackExchange.Redis.Tests/LockingTests.cs +++ b/tests/StackExchange.Redis.Tests/LockingTests.cs @@ -151,11 +151,11 @@ public async Task TakeLockAndExtend(TestMode testMode) Assert.Equal(right, await t4); if (withTran) Assert.False(await t5!, "5"); Assert.Equal(right, await t6); - var ttl = (await t7).Value.TotalSeconds; + var ttl = (await t7)!.Value.TotalSeconds; Assert.True(ttl > 0 && ttl <= 20, "7"); Assert.True(await t8, "8"); Assert.Equal(right, await t9); - ttl = (await t10).Value.TotalSeconds; + ttl = (await t10)!.Value.TotalSeconds; Assert.True(ttl > 50 && ttl <= 60, "10"); Assert.True(await t11, "11"); Assert.Null((string?)await t12); @@ -185,7 +185,7 @@ public async Task TestBasicLockNotTaken(TestMode testMode) } Assert.True(await taken!, "taken"); Assert.Equal("new-value", await newValue!); - var ttlValue = (await ttl!).Value.TotalSeconds; + var ttlValue = (await ttl!)!.Value.TotalSeconds; Assert.True(ttlValue >= 8 && ttlValue <= 10, "ttl"); Assert.Equal(0, errorCount); @@ -206,7 +206,7 @@ public async Task TestBasicLockTaken(TestMode testMode) Assert.False(await taken, "taken"); Assert.Equal("old-value", await newValue); - var ttlValue = (await ttl).Value.TotalSeconds; + var ttlValue = (await ttl)!.Value.TotalSeconds; Assert.True(ttlValue >= 18 && ttlValue <= 20, "ttl"); } } diff --git a/tests/StackExchange.Redis.Tests/LoggerTests.cs b/tests/StackExchange.Redis.Tests/LoggerTests.cs index 217864ba7..75077f9a9 100644 --- a/tests/StackExchange.Redis.Tests/LoggerTests.cs +++ b/tests/StackExchange.Redis.Tests/LoggerTests.cs @@ -69,7 +69,11 @@ public class TestWrapperLogger : ILogger public TestWrapperLogger(ILogger toWrap) => Inner = toWrap; +#if NET8_0_OR_GREATER + public IDisposable? BeginScope(TState state) where TState : notnull => Inner.BeginScope(state); +#else public IDisposable BeginScope(TState state) => Inner.BeginScope(state); +#endif public bool IsEnabled(LogLevel logLevel) => Inner.IsEnabled(logLevel); public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { @@ -86,7 +90,11 @@ private class TestMultiLogger : ILogger private readonly ILogger[] _loggers; public TestMultiLogger(params ILogger[] loggers) => _loggers = loggers; +#if NET8_0_OR_GREATER + public IDisposable? BeginScope(TState state) where TState : notnull => throw new NotImplementedException(); +#else public IDisposable BeginScope(TState state) => throw new NotImplementedException(); +#endif public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { @@ -106,7 +114,11 @@ private class TestLogger : ILogger public TestLogger(LogLevel logLevel, TextWriter output) => (_logLevel, _output) = (logLevel, output); +#if NET8_0_OR_GREATER + public IDisposable? BeginScope(TState state) where TState : notnull => throw new NotImplementedException(); +#else public IDisposable BeginScope(TState state) => throw new NotImplementedException(); +#endif public bool IsEnabled(LogLevel logLevel) => logLevel >= _logLevel; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { diff --git a/tests/StackExchange.Redis.Tests/RespProtocolTests.cs b/tests/StackExchange.Redis.Tests/RespProtocolTests.cs index c508bd1c9..44932cf96 100644 --- a/tests/StackExchange.Redis.Tests/RespProtocolTests.cs +++ b/tests/StackExchange.Redis.Tests/RespProtocolTests.cs @@ -180,7 +180,7 @@ public async Task CheckLuaResult(string script, RedisProtocol protocol, ResultTy db.HashSet("key", "b", 2); db.HashSet("key", "c", 3); } - var result = await db.ScriptEvaluateAsync(script, flags: CommandFlags.NoScriptCache); + var result = await db.ScriptEvaluateAsync(script: script, flags: CommandFlags.NoScriptCache); Assert.Equal(resp2, result.Resp2Type); Assert.Equal(resp3, result.Resp3Type); diff --git a/tests/StackExchange.Redis.Tests/ScriptingTests.cs b/tests/StackExchange.Redis.Tests/ScriptingTests.cs index 59bb0e608..f6de8044a 100644 --- a/tests/StackExchange.Redis.Tests/ScriptingTests.cs +++ b/tests/StackExchange.Redis.Tests/ScriptingTests.cs @@ -8,6 +8,9 @@ using Xunit; using Xunit.Abstractions; +// ReSharper disable UseAwaitUsing # for consistency with existing tests +// ReSharper disable MethodHasAsyncOverload # grandfathered existing usage +// ReSharper disable StringLiteralTypo # because of Lua scripts namespace StackExchange.Redis.Tests; [RunPerProtocol] @@ -27,7 +30,7 @@ private IConnectionMultiplexer GetScriptConn(bool allowAdmin = false) public void ClientScripting() { using var conn = GetScriptConn(); - _ = conn.GetDatabase().ScriptEvaluate("return redis.call('info','server')", null, null); + _ = conn.GetDatabase().ScriptEvaluate(script: "return redis.call('info','server')", keys: null, values: null); } [Fact] @@ -37,14 +40,14 @@ public async Task BasicScripting() var db = conn.GetDatabase(); var noCache = db.ScriptEvaluateAsync( - "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", - new RedisKey[] { "key1", "key2" }, - new RedisValue[] { "first", "second" }); + script: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", + keys: new RedisKey[] { "key1", "key2" }, + values: new RedisValue[] { "first", "second" }); var cache = db.ScriptEvaluateAsync( - "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", - new RedisKey[] { "key1", "key2" }, - new RedisValue[] { "first", "second" }); - var results = (string[]?)await noCache; + script: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", + keys: new RedisKey[] { "key1", "key2" }, + values: new RedisValue[] { "first", "second" }); + var results = (string[]?)(await noCache)!; Assert.NotNull(results); Assert.Equal(4, results.Length); Assert.Equal("key1", results[0]); @@ -52,7 +55,7 @@ public async Task BasicScripting() Assert.Equal("first", results[2]); Assert.Equal("second", results[3]); - results = (string[]?)await cache; + results = (string[]?)(await cache)!; Assert.NotNull(results); Assert.Equal(4, results.Length); Assert.Equal("key1", results[0]); @@ -69,16 +72,18 @@ public void KeysScripting() var db = conn.GetDatabase(); var key = Me(); db.StringSet(key, "bar", flags: CommandFlags.FireAndForget); - var result = (string?)db.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); + var result = (string?)db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: new RedisKey[] { key }, values: null); Assert.Equal("bar", result); } [Fact] public async Task TestRandomThingFromForum() { - const string script = @"local currentVal = tonumber(redis.call('GET', KEYS[1])); - if (currentVal <= 0 ) then return 1 elseif (currentVal - (tonumber(ARGV[1])) < 0 ) then return 0 end; - return redis.call('INCRBY', KEYS[1], -tonumber(ARGV[1]));"; + const string Script = """ + local currentVal = tonumber(redis.call('GET', KEYS[1])); + if (currentVal <= 0 ) then return 1 elseif (currentVal - (tonumber(ARGV[1])) < 0 ) then return 0 end; + return redis.call('INCRBY', KEYS[1], -tonumber(ARGV[1])); + """; using var conn = GetScriptConn(); @@ -88,18 +93,18 @@ public async Task TestRandomThingFromForum() db.StringSet(prefix + "B", "5", flags: CommandFlags.FireAndForget); db.StringSet(prefix + "C", "10", flags: CommandFlags.FireAndForget); - var a = db.ScriptEvaluateAsync(script, new RedisKey[] { prefix + "A" }, new RedisValue[] { 6 }).ForAwait(); - var b = db.ScriptEvaluateAsync(script, new RedisKey[] { prefix + "B" }, new RedisValue[] { 6 }).ForAwait(); - var c = db.ScriptEvaluateAsync(script, new RedisKey[] { prefix + "C" }, new RedisValue[] { 6 }).ForAwait(); + var a = db.ScriptEvaluateAsync(script: Script, keys: new RedisKey[] { prefix + "A" }, values: new RedisValue[] { 6 }).ForAwait(); + var b = db.ScriptEvaluateAsync(script: Script, keys: new RedisKey[] { prefix + "B" }, values: new RedisValue[] { 6 }).ForAwait(); + var c = db.ScriptEvaluateAsync(script: Script, keys: new RedisKey[] { prefix + "C" }, values: new RedisValue[] { 6 }).ForAwait(); - var vals = await db.StringGetAsync(new RedisKey[] { prefix + "A", prefix + "B", prefix + "C" }).ForAwait(); + var values = await db.StringGetAsync(new RedisKey[] { prefix + "A", prefix + "B", prefix + "C" }).ForAwait(); Assert.Equal(1, (long)await a); // exit code when current val is non-positive Assert.Equal(0, (long)await b); // exit code when result would be negative Assert.Equal(4, (long)await c); // 10 - 6 = 4 - Assert.Equal("0", vals[0]); - Assert.Equal("5", vals[1]); - Assert.Equal("4", vals[2]); + Assert.Equal("0", values[0]); + Assert.Equal("5", values[1]); + Assert.Equal("4", values[2]); } [Fact] @@ -118,9 +123,9 @@ public async Task MultiIncrWithoutReplies() // run the script, passing "a", "b", "c", "c" to // increment a & b by 1, c twice var result = db.ScriptEvaluateAsync( - "for i,key in ipairs(KEYS) do redis.call('incr', key) end", - new RedisKey[] { prefix + "a", prefix + "b", prefix + "c", prefix + "c" }, // <== aka "KEYS" in the script - null).ForAwait(); // <== aka "ARGV" in the script + script: "for i,key in ipairs(KEYS) do redis.call('incr', key) end", + keys: new RedisKey[] { prefix + "a", prefix + "b", prefix + "c", prefix + "c" }, // <== aka "KEYS" in the script + values: null).ForAwait(); // <== aka "ARGV" in the script // check the incremented values var a = db.StringGetAsync(prefix + "a").ForAwait(); @@ -151,9 +156,9 @@ public async Task MultiIncrByWithoutReplies() // run the script, passing "a", "b", "c" and 1,2,3 // increment a & b by 1, c twice var result = db.ScriptEvaluateAsync( - "for i,key in ipairs(KEYS) do redis.call('incrby', key, ARGV[i]) end", - new RedisKey[] { prefix + "a", prefix + "b", prefix + "c" }, // <== aka "KEYS" in the script - new RedisValue[] { 1, 1, 2 }).ForAwait(); // <== aka "ARGV" in the script + script: "for i,key in ipairs(KEYS) do redis.call('incrby', key, ARGV[i]) end", + keys: new RedisKey[] { prefix + "a", prefix + "b", prefix + "c" }, // <== aka "KEYS" in the script + values: new RedisValue[] { 1, 1, 2 }).ForAwait(); // <== aka "ARGV" in the script // check the incremented values var a = db.StringGetAsync(prefix + "a").ForAwait(); @@ -174,7 +179,7 @@ public void DisableStringInference() var db = conn.GetDatabase(); var key = Me(); db.StringSet(key, "bar", flags: CommandFlags.FireAndForget); - var result = (byte[]?)db.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }); + var result = (byte[]?)db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: new RedisKey[] { key }); Assert.NotNull(result); Assert.Equal("bar", Encoding.UTF8.GetString(result)); } @@ -188,16 +193,16 @@ public void FlushDetection() var db = conn.GetDatabase(); var key = Me(); db.StringSet(key, "bar", flags: CommandFlags.FireAndForget); - var result = (string?)db.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); + var result = (string?)db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: new RedisKey[] { key }, values: null); Assert.Equal("bar", result); // now cause all kinds of problems GetServer(conn).ScriptFlush(); // expect this one to fail just work fine (self-fix) - db.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); + db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: new RedisKey[] { key }, values: null); - result = (string?)db.ScriptEvaluate("return redis.call('get', KEYS[1])", new RedisKey[] { key }, null); + result = (string?)db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: new RedisKey[] { key }, values: null); Assert.Equal("bar", result); } @@ -241,11 +246,11 @@ public void NonAsciiScripts() { using var conn = GetScriptConn(); - const string evil = "return '僕'"; + const string Evil = "return '僕'"; var db = conn.GetDatabase(); - GetServer(conn).ScriptLoad(evil); + GetServer(conn).ScriptLoad(Evil); - var result = (string?)db.ScriptEvaluate(evil, null, null); + var result = (string?)db.ScriptEvaluate(script: Evil, keys: null, values: null); Assert.Equal("僕", result); } @@ -258,7 +263,7 @@ await Assert.ThrowsAsync(async () => var db = conn.GetDatabase(); try { - await db.ScriptEvaluateAsync("return redis.error_reply('oops')", null, null).ForAwait(); + await db.ScriptEvaluateAsync(script: "return redis.error_reply('oops')", keys: null, values: null).ForAwait(); } catch (AggregateException ex) { @@ -280,7 +285,7 @@ public void ScriptThrowsErrorInsideTransaction() var tran = db.CreateTransaction(); { var a = tran.StringIncrementAsync(key); - var b = tran.ScriptEvaluateAsync("return redis.error_reply('oops')", null, null); + var b = tran.ScriptEvaluateAsync(script: "return redis.error_reply('oops')", keys: null, values: null); var c = tran.StringIncrementAsync(key); var complete = tran.ExecuteAsync(); @@ -322,10 +327,10 @@ public async Task ChangeDbInScript() Log("Key: " + key); var db = conn.GetDatabase(2); var evalResult = db.ScriptEvaluateAsync( - @"redis.call('select', 1) + script: @"redis.call('select', 1) return redis.call('get','" + key + "')", - null, - null); + keys: null, + values: null); var getResult = db.StringGetAsync(key); Assert.Equal("db 1", (string?)await evalResult); @@ -345,10 +350,10 @@ public async Task ChangeDbInTranScript() var db = conn.GetDatabase(2); var tran = db.CreateTransaction(); var evalResult = tran.ScriptEvaluateAsync( - @"redis.call('select', 1) + script: @"redis.call('select', 1) return redis.call('get','" + key + "')", - null, - null); + keys: null, + values: null); var getResult = tran.StringGetAsync(key); Assert.True(tran.Execute()); @@ -369,16 +374,16 @@ public void TestBasicScripting() db.HashSet(key, "id", 123, flags: CommandFlags.FireAndForget); var wasSet = (bool)db.ScriptEvaluate( - "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", - new[] { key }, - new[] { newId }); + script: "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", + keys: new[] { key }, + values: new[] { newId }); Assert.True(wasSet); wasSet = (bool)db.ScriptEvaluate( - "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", - new[] { key }, - new[] { newId }); + script: "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", + keys: new[] { key }, + values: new[] { newId }); Assert.False(wasSet); } @@ -394,16 +399,16 @@ public async Task CheckLoads(bool async) // the flush to drop the local cache - assume it is a surprise!) var server = conn0.GetServer(TestConfig.Current.PrimaryServerAndPort); var db = conn1.GetDatabase(); - const string script = "return 1;"; + const string Script = "return 1;"; // start empty server.ScriptFlush(); - Assert.False(server.ScriptExists(script)); + Assert.False(server.ScriptExists(Script)); // run once, causes to be cached Assert.True(await EvaluateScript()); - Assert.True(server.ScriptExists(script)); + Assert.True(server.ScriptExists(Script)); // can run again Assert.True(await EvaluateScript()); @@ -411,7 +416,7 @@ public async Task CheckLoads(bool async) // ditch the scripts; should no longer exist db.Ping(); server.ScriptFlush(); - Assert.False(server.ScriptExists(script)); + Assert.False(server.ScriptExists(Script)); db.Ping(); // just works; magic @@ -421,13 +426,13 @@ public async Task CheckLoads(bool async) Assert.True(await EvaluateScript()); // which will cause it to be cached - Assert.True(server.ScriptExists(script)); + Assert.True(server.ScriptExists(Script)); async Task EvaluateScript() { return async ? - (bool)await db!.ScriptEvaluateAsync(script) : - (bool)db!.ScriptEvaluate(script); + (bool)await db.ScriptEvaluateAsync(script: Script) : + (bool)db.ScriptEvaluate(script: Script); } } @@ -445,25 +450,25 @@ public void CompareScriptToDirect() db.Ping(); // k, we're all up to date now; clean db, minimal script cache // we're using a pipeline here, so send 1000 messages, but for timing: only care about the last - const int LOOP = 5000; + const int Loop = 5000; RedisKey key = Me(); RedisKey[] keys = new[] { key }; // script takes an array // run via script db.KeyDelete(key, CommandFlags.FireAndForget); var watch = Stopwatch.StartNew(); - for (int i = 1; i < LOOP; i++) // the i=1 is to do all-but-one + for (int i = 1; i < Loop; i++) // the i=1 is to do all-but-one { - db.ScriptEvaluate(Script, keys, flags: CommandFlags.FireAndForget); + db.ScriptEvaluate(script: Script, keys: keys, flags: CommandFlags.FireAndForget); } - var scriptResult = db.ScriptEvaluate(Script, keys); // last one we wait for (no F+F) + var scriptResult = db.ScriptEvaluate(script: Script, keys: keys); // last one we wait for (no F+F) watch.Stop(); TimeSpan scriptTime = watch.Elapsed; // run via raw op db.KeyDelete(key, CommandFlags.FireAndForget); watch = Stopwatch.StartNew(); - for (int i = 1; i < LOOP; i++) // the i=1 is to do all-but-one + for (int i = 1; i < Loop; i++) // the i=1 is to do all-but-one { db.StringIncrement(key, flags: CommandFlags.FireAndForget); } @@ -471,8 +476,8 @@ public void CompareScriptToDirect() watch.Stop(); TimeSpan directTime = watch.Elapsed; - Assert.Equal(LOOP, (long)scriptResult); - Assert.Equal(LOOP, directResult); + Assert.Equal(Loop, (long)scriptResult); + Assert.Equal(Loop, directResult); Log("script: {0}ms; direct: {1}ms", scriptTime.TotalMilliseconds, directTime.TotalMilliseconds); } @@ -486,7 +491,7 @@ public void TestCallByHash() var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); server.ScriptFlush(); - byte[]? hash = server.ScriptLoad(Script); + byte[] hash = server.ScriptLoad(Script); Assert.NotNull(hash); var db = conn.GetDatabase(); @@ -497,7 +502,7 @@ public void TestCallByHash() string hexHash = string.Concat(hash.Select(x => x.ToString("X2"))); Assert.Equal("2BAB3B661081DB58BD2341920E0BA7CF5DC77B25", hexHash); - db.ScriptEvaluate(hexHash, keys, flags: CommandFlags.FireAndForget); + db.ScriptEvaluate(script: hexHash, keys: keys, flags: CommandFlags.FireAndForget); db.ScriptEvaluate(hash, keys, flags: CommandFlags.FireAndForget); var count = (int)db.StringGet(keys)[0]; @@ -571,39 +576,39 @@ public void SimpleRawScriptEvaluate() // Scopes for repeated use { - var val = db.ScriptEvaluate(Script, values: new RedisValue[] { "hello" }); + var val = db.ScriptEvaluate(script: Script, values: new RedisValue[] { "hello" }); Assert.Equal("hello", (string?)val); } { - var val = db.ScriptEvaluate(Script, values: new RedisValue[] { 123 }); + var val = db.ScriptEvaluate(script: Script, values: new RedisValue[] { 123 }); Assert.Equal(123, (int)val); } { - var val = db.ScriptEvaluate(Script, values: new RedisValue[] { 123L }); + var val = db.ScriptEvaluate(script: Script, values: new RedisValue[] { 123L }); Assert.Equal(123L, (long)val); } { - var val = db.ScriptEvaluate(Script, values: new RedisValue[] { 1.1 }); + var val = db.ScriptEvaluate(script: Script, values: new RedisValue[] { 1.1 }); Assert.Equal(1.1, (double)val); } { - var val = db.ScriptEvaluate(Script, values: new RedisValue[] { true }); + var val = db.ScriptEvaluate(script: Script, values: new RedisValue[] { true }); Assert.True((bool)val); } { - var val = db.ScriptEvaluate(Script, values: new RedisValue[] { new byte[] { 4, 5, 6 } }); + var val = db.ScriptEvaluate(script: Script, values: new RedisValue[] { new byte[] { 4, 5, 6 } }); var valArray = (byte[]?)val; Assert.NotNull(valArray); Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); } { - var val = db.ScriptEvaluate(Script, values: new RedisValue[] { new ReadOnlyMemory(new byte[] { 4, 5, 6 }) }); + var val = db.ScriptEvaluate(script: Script, values: new RedisValue[] { new ReadOnlyMemory(new byte[] { 4, 5, 6 }) }); var valArray = (byte[]?)val; Assert.NotNull(valArray); Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); @@ -786,7 +791,7 @@ public void PurgeLuaScriptOnFinalize() Assert.Equal(0, LuaScript.GetCachedScriptCount()); // This has to be a separate method to guarantee that the created LuaScript objects go out of scope, - // and are thus available to be GC'd + // and are thus available to be garbage collected. PurgeLuaScriptOnFinalizeImpl(Script); CollectGarbage(); @@ -797,7 +802,7 @@ public void PurgeLuaScriptOnFinalize() } [Fact] - public void IDatabaseLuaScriptConvenienceMethods() + public void DatabaseLuaScriptConvenienceMethods() { using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); @@ -818,7 +823,7 @@ public void IDatabaseLuaScriptConvenienceMethods() } [Fact] - public void IServerLuaScriptConvenienceMethods() + public void ServerLuaScriptConvenienceMethods() { using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); @@ -978,14 +983,14 @@ public void ScriptWithKeyPrefixViaArrays() var p = conn.GetDatabase().WithKeyPrefix("prefix/"); - const string script = @" + const string Script = @" local arr = {}; arr[1] = ARGV[1]; arr[2] = KEYS[1]; arr[3] = ARGV[2]; return arr; "; - var result = (RedisValue[]?)p.ScriptEvaluate(script, new RedisKey[] { "def" }, new RedisValue[] { "abc", 123 }); + var result = (RedisValue[]?)p.ScriptEvaluate(script: Script, keys: new RedisKey[] { "def" }, values: new RedisValue[] { "abc", 123 }); Assert.NotNull(result); Assert.Equal("abc", result[0]); Assert.Equal("prefix/def", result[1]); @@ -1002,7 +1007,7 @@ public void ScriptWithKeyPrefixCompare() LuaScript lua = LuaScript.Prepare("return {@k, @s, @v}"); var viaArgs = (RedisValue[]?)p.ScriptEvaluate(lua, args); - var viaArr = (RedisValue[]?)p.ScriptEvaluate("return {KEYS[1], ARGV[1], ARGV[2]}", new[] { args.k }, new RedisValue[] { args.s, args.v }); + var viaArr = (RedisValue[]?)p.ScriptEvaluate(script: "return {KEYS[1], ARGV[1], ARGV[2]}", keys: new[] { args.k }, values: new RedisValue[] { args.s, args.v }); Assert.NotNull(viaArr); Assert.NotNull(viaArgs); Assert.Equal(string.Join(",", viaArr), string.Join(",", viaArgs)); @@ -1035,10 +1040,10 @@ private static void TestNullArray(RedisResult? value) Assert.Null((bool[]?)value); Assert.Null((long[]?)value); Assert.Null((ulong[]?)value); - Assert.Null((string[]?)value); + Assert.Null((string[]?)value!); Assert.Null((int[]?)value); Assert.Null((double[]?)value); - Assert.Null((byte[][]?)value); + Assert.Null((byte[][]?)value!); Assert.Null((RedisResult[]?)value); } @@ -1054,8 +1059,8 @@ public void TestEvalReadonly() var db = conn.GetDatabase(); string script = "return KEYS[1]"; - RedisKey[] keys = new RedisKey[1] { "key1" }; - RedisValue[] values = new RedisValue[1] { "first" }; + RedisKey[] keys = { "key1" }; + RedisValue[] values = { "first" }; var result = db.ScriptEvaluateReadOnly(script, keys, values); Assert.Equal("key1", result.ToString()); @@ -1068,8 +1073,8 @@ public async Task TestEvalReadonlyAsync() var db = conn.GetDatabase(); string script = "return KEYS[1]"; - RedisKey[] keys = new RedisKey[1] { "key1" }; - RedisValue[] values = new RedisValue[1] { "first" }; + RedisKey[] keys = { "key1" }; + RedisValue[] values = { "first" }; var result = await db.ScriptEvaluateReadOnlyAsync(script, keys, values); Assert.Equal("key1", result.ToString()); @@ -1081,7 +1086,7 @@ public void TestEvalShaReadOnly() using var conn = GetScriptConn(); var db = conn.GetDatabase(); db.StringSet("foo", "bar"); - db.ScriptEvaluate("return redis.call('get','foo')"); + db.ScriptEvaluate(script: "return redis.call('get','foo')"); // Create a SHA1 hash of the script: 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 SHA1 sha1Hash = SHA1.Create(); @@ -1097,7 +1102,7 @@ public async Task TestEvalShaReadOnlyAsync() using var conn = GetScriptConn(); var db = conn.GetDatabase(); db.StringSet("foo", "bar"); - db.ScriptEvaluate("return redis.call('get','foo')"); + db.ScriptEvaluate(script: "return redis.call('get','foo')"); // Create a SHA1 hash of the script: 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 SHA1 sha1Hash = SHA1.Create(); diff --git a/tests/StackExchange.Redis.Tests/SortedSetTests.cs b/tests/StackExchange.Redis.Tests/SortedSetTests.cs index 9bcd02e2e..7ba86fcf6 100644 --- a/tests/StackExchange.Redis.Tests/SortedSetTests.cs +++ b/tests/StackExchange.Redis.Tests/SortedSetTests.cs @@ -338,7 +338,7 @@ public void SortedSetRangeViaScript() db.KeyDelete(key, CommandFlags.FireAndForget); db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); - var result = db.ScriptEvaluate("return redis.call('ZRANGE', KEYS[1], 0, -1, 'WITHSCORES')", new RedisKey[] { key }); + var result = db.ScriptEvaluate(script: "return redis.call('ZRANGE', KEYS[1], 0, -1, 'WITHSCORES')", keys: new RedisKey[] { key }); AssertFlatArrayEntries(result); } From be39ec985658df170b098aa783c47968bc4217c1 Mon Sep 17 00:00:00 2001 From: atakavci Date: Thu, 10 Jul 2025 19:00:20 +0300 Subject: [PATCH 333/435] new operations with BITOP command (#2900) --- src/StackExchange.Redis/Enums/Bitwise.cs | 25 ++ .../PublicAPI/PublicAPI.Shipped.txt | 4 + src/StackExchange.Redis/RedisFeatures.cs | 3 +- src/StackExchange.Redis/RedisLiterals.cs | 8 + .../KeyPrefixedDatabaseTests.cs | 36 +++ .../KeyPrefixedTests.cs | 36 +++ .../StackExchange.Redis.Tests/StringTests.cs | 273 ++++++++++++++++++ 7 files changed, 384 insertions(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/Enums/Bitwise.cs b/src/StackExchange.Redis/Enums/Bitwise.cs index b38423eac..82e70b38a 100644 --- a/src/StackExchange.Redis/Enums/Bitwise.cs +++ b/src/StackExchange.Redis/Enums/Bitwise.cs @@ -24,5 +24,30 @@ public enum Bitwise /// Not /// Not, + + /// + /// DIFF operation: members of X that are not members of any of Y1, Y2, ... + /// Equivalent to X ∧ ¬(Y1 ∨ Y2 ∨ ...) + /// + Diff, + + /// + /// DIFF1 operation: members of one or more of Y1, Y2, ... that are not members of X + /// Equivalent to ¬X ∧ (Y1 ∨ Y2 ∨ ...) + /// + Diff1, + + /// + /// ANDOR operation: members of X that are also members of one or more of Y1, Y2, ... + /// Equivalent to X ∧ (Y1 ∨ Y2 ∨ ...) + /// + AndOr, + + /// + /// ONE operation: members of exactly one of X1, X2, ... + /// For two bitmaps this is equivalent to XOR. For more than two bitmaps, + /// this returns bits that are set in exactly one of the input bitmaps. + /// + One, } } diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 683332070..00ae49025 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1899,3 +1899,7 @@ static StackExchange.Redis.RedisChannel.Sharded(byte[]? value) -> StackExchange. static StackExchange.Redis.RedisChannel.Sharded(string! value) -> StackExchange.Redis.RedisChannel StackExchange.Redis.ClientInfo.ShardedSubscriptionCount.get -> int StackExchange.Redis.ConfigurationOptions.SetUserPfxCertificate(string! userCertificatePath, string? password = null) -> void +StackExchange.Redis.Bitwise.AndOr = 6 -> StackExchange.Redis.Bitwise +StackExchange.Redis.Bitwise.Diff = 4 -> StackExchange.Redis.Bitwise +StackExchange.Redis.Bitwise.Diff1 = 5 -> StackExchange.Redis.Bitwise +StackExchange.Redis.Bitwise.One = 7 -> StackExchange.Redis.Bitwise \ No newline at end of file diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index faba07e68..06a44e643 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -44,7 +44,8 @@ namespace StackExchange.Redis v7_0_0_rc1 = new Version(6, 9, 240), // 7.0 RC1 is version 6.9.240 v7_2_0_rc1 = new Version(7, 1, 240), // 7.2 RC1 is version 7.1.240 v7_4_0_rc1 = new Version(7, 3, 240), // 7.4 RC1 is version 7.3.240 - v7_4_0_rc2 = new Version(7, 3, 241); // 7.4 RC2 is version 7.3.241 + v7_4_0_rc2 = new Version(7, 3, 241), // 7.4 RC2 is version 7.3.241 + v8_2_0_rc1 = new Version(8, 1, 240); // 8.2 RC1 is version 8.1.240 #pragma warning restore SA1310 // Field names should not contain underscore #pragma warning restore SA1311 // Static readonly fields should begin with upper-case letter diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 549691fd2..29937c0dd 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -57,6 +57,7 @@ public static readonly RedisValue AGGREGATE = "AGGREGATE", ALPHA = "ALPHA", AND = "AND", + ANDOR = "ANDOR", ANY = "ANY", ASC = "ASC", AUTH = "AUTH", @@ -73,6 +74,8 @@ public static readonly RedisValue DB = "DB", @default = "default", DESC = "DESC", + DIFF = "DIFF", + DIFF1 = "DIFF1", DOCTOR = "DOCTOR", ENCODING = "ENCODING", EX = "EX", @@ -118,6 +121,7 @@ public static readonly RedisValue NUMSUB = "NUMSUB", NX = "NX", OBJECT = "OBJECT", + ONE = "ONE", OR = "OR", PATTERN = "PATTERN", PAUSE = "PAUSE", @@ -216,6 +220,10 @@ public static readonly RedisValue Bitwise.Or => OR, Bitwise.Xor => XOR, Bitwise.Not => NOT, + Bitwise.Diff => DIFF, + Bitwise.Diff1 => DIFF1, + Bitwise.AndOr => ANDOR, + Bitwise.One => ONE, _ => throw new ArgumentOutOfRangeException(nameof(operation)), }; } diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs index e73225031..2552ac7aa 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs @@ -1239,6 +1239,42 @@ public void StringBitOperation_2() mock.Received().StringBitOperation(Bitwise.Xor, "prefix:destination", Arg.Is(valid), CommandFlags.None); } + [Fact] + public void StringBitOperation_Diff() + { + RedisKey[] keys = new RedisKey[] { "x", "y1", "y2" }; + Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; + prefixed.StringBitOperation(Bitwise.Diff, "destination", keys, CommandFlags.None); + mock.Received().StringBitOperation(Bitwise.Diff, "prefix:destination", Arg.Is(valid), CommandFlags.None); + } + + [Fact] + public void StringBitOperation_Diff1() + { + RedisKey[] keys = new RedisKey[] { "x", "y1", "y2" }; + Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; + prefixed.StringBitOperation(Bitwise.Diff1, "destination", keys, CommandFlags.None); + mock.Received().StringBitOperation(Bitwise.Diff1, "prefix:destination", Arg.Is(valid), CommandFlags.None); + } + + [Fact] + public void StringBitOperation_AndOr() + { + RedisKey[] keys = new RedisKey[] { "x", "y1", "y2" }; + Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; + prefixed.StringBitOperation(Bitwise.AndOr, "destination", keys, CommandFlags.None); + mock.Received().StringBitOperation(Bitwise.AndOr, "prefix:destination", Arg.Is(valid), CommandFlags.None); + } + + [Fact] + public void StringBitOperation_One() + { + RedisKey[] keys = new RedisKey[] { "a", "b", "c" }; + Expression> valid = _ => _.Length == 3 && _[0] == "prefix:a" && _[1] == "prefix:b" && _[2] == "prefix:c"; + prefixed.StringBitOperation(Bitwise.One, "destination", keys, CommandFlags.None); + mock.Received().StringBitOperation(Bitwise.One, "prefix:destination", Arg.Is(valid), CommandFlags.None); + } + [Fact] public void StringBitPosition() { diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs index e48227974..e768b9ec5 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs @@ -1155,6 +1155,42 @@ public async Task StringBitOperationAsync_2() await mock.Received().StringBitOperationAsync(Bitwise.Xor, "prefix:destination", Arg.Is(valid), CommandFlags.None); } + [Fact] + public async Task StringBitOperationAsync_Diff() + { + RedisKey[] keys = new RedisKey[] { "x", "y1", "y2" }; + Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; + await prefixed.StringBitOperationAsync(Bitwise.Diff, "destination", keys, CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.Diff, "prefix:destination", Arg.Is(valid), CommandFlags.None); + } + + [Fact] + public async Task StringBitOperationAsync_Diff1() + { + RedisKey[] keys = new RedisKey[] { "x", "y1", "y2" }; + Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; + await prefixed.StringBitOperationAsync(Bitwise.Diff1, "destination", keys, CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.Diff1, "prefix:destination", Arg.Is(valid), CommandFlags.None); + } + + [Fact] + public async Task StringBitOperationAsync_AndOr() + { + RedisKey[] keys = new RedisKey[] { "x", "y1", "y2" }; + Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; + await prefixed.StringBitOperationAsync(Bitwise.AndOr, "destination", keys, CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.AndOr, "prefix:destination", Arg.Is(valid), CommandFlags.None); + } + + [Fact] + public async Task StringBitOperationAsync_One() + { + RedisKey[] keys = new RedisKey[] { "a", "b", "c" }; + Expression> valid = _ => _.Length == 3 && _[0] == "prefix:a" && _[1] == "prefix:b" && _[2] == "prefix:c"; + await prefixed.StringBitOperationAsync(Bitwise.One, "destination", keys, CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.One, "prefix:destination", Arg.Is(valid), CommandFlags.None); + } + [Fact] public async Task StringBitPositionAsync() { diff --git a/tests/StackExchange.Redis.Tests/StringTests.cs b/tests/StackExchange.Redis.Tests/StringTests.cs index 275f15fe2..d430b90a6 100644 --- a/tests/StackExchange.Redis.Tests/StringTests.cs +++ b/tests/StackExchange.Redis.Tests/StringTests.cs @@ -595,6 +595,279 @@ public async Task BitOp() Assert.Equal(unchecked((byte)(~3)), r_not); } + [Fact] + public async Task BitOpExtended() + { + using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + var db = conn.GetDatabase(); + var prefix = Me(); + var keyX = prefix + "X"; + var keyY1 = prefix + "Y1"; + var keyY2 = prefix + "Y2"; + var keyY3 = prefix + "Y3"; + + // Clean up keys + db.KeyDelete(new RedisKey[] { keyX, keyY1, keyY2, keyY3 }, CommandFlags.FireAndForget); + + // Set up test data with more complex patterns + // X = 11110000 (240) + // Y1 = 10101010 (170) + // Y2 = 01010101 (85) + // Y3 = 11001100 (204) + db.StringSet(keyX, new byte[] { 240 }, flags: CommandFlags.FireAndForget); + db.StringSet(keyY1, new byte[] { 170 }, flags: CommandFlags.FireAndForget); + db.StringSet(keyY2, new byte[] { 85 }, flags: CommandFlags.FireAndForget); + db.StringSet(keyY3, new byte[] { 204 }, flags: CommandFlags.FireAndForget); + + // Test DIFF: X ∧ ¬(Y1 ∨ Y2 ∨ Y3) + // Y1 ∨ Y2 ∨ Y3 = 170 | 85 | 204 = 255 + // X ∧ ¬(Y1 ∨ Y2 ∨ Y3) = 240 & ~255 = 240 & 0 = 0 + var len_diff = await db.StringBitOperationAsync(Bitwise.Diff, "diff", new RedisKey[] { keyX, keyY1, keyY2, keyY3 }); + Assert.Equal(1, len_diff); + var r_diff = ((byte[]?)(await db.StringGetAsync("diff")))?.Single(); + Assert.Equal((byte)0, r_diff); + + // Test DIFF1: ¬X ∧ (Y1 ∨ Y2 ∨ Y3) + // ¬X = ~240 = 15 + // Y1 ∨ Y2 ∨ Y3 = 255 + // ¬X ∧ (Y1 ∨ Y2 ∨ Y3) = 15 & 255 = 15 + var len_diff1 = await db.StringBitOperationAsync(Bitwise.Diff1, "diff1", new RedisKey[] { keyX, keyY1, keyY2, keyY3 }); + Assert.Equal(1, len_diff1); + var r_diff1 = ((byte[]?)(await db.StringGetAsync("diff1")))?.Single(); + Assert.Equal((byte)15, r_diff1); + + // Test ANDOR: X ∧ (Y1 ∨ Y2 ∨ Y3) + // Y1 ∨ Y2 ∨ Y3 = 255 + // X ∧ (Y1 ∨ Y2 ∨ Y3) = 240 & 255 = 240 + var len_andor = await db.StringBitOperationAsync(Bitwise.AndOr, "andor", new RedisKey[] { keyX, keyY1, keyY2, keyY3 }); + Assert.Equal(1, len_andor); + var r_andor = ((byte[]?)(await db.StringGetAsync("andor")))?.Single(); + Assert.Equal((byte)240, r_andor); + + // Test ONE: bits set in exactly one bitmap + // For X=240, Y1=170, Y2=85, Y3=204 + // We need to count bits that appear in exactly one of these values + var len_one = await db.StringBitOperationAsync(Bitwise.One, "one", new RedisKey[] { keyX, keyY1, keyY2, keyY3 }); + Assert.Equal(1, len_one); + var r_one = ((byte[]?)(await db.StringGetAsync("one")))?.Single(); + + // Calculate expected ONE result manually + // Bit 7: X=1, Y1=1, Y2=0, Y3=1 -> count=3, not exactly 1 + // Bit 6: X=1, Y1=0, Y2=1, Y3=1 -> count=3, not exactly 1 + // Bit 5: X=1, Y1=1, Y2=0, Y3=0 -> count=2, not exactly 1 + // Bit 4: X=1, Y1=0, Y2=1, Y3=0 -> count=2, not exactly 1 + // Bit 3: X=0, Y1=1, Y2=0, Y3=1 -> count=2, not exactly 1 + // Bit 2: X=0, Y1=0, Y2=1, Y3=1 -> count=2, not exactly 1 + // Bit 1: X=0, Y1=1, Y2=0, Y3=0 -> count=1, exactly 1! -> bit should be set + // Bit 0: X=0, Y1=0, Y2=1, Y3=0 -> count=1, exactly 1! -> bit should be set + // Expected result: 00000011 = 3 + Assert.Equal((byte)3, r_one); + } + + [Fact] + public async Task BitOpTwoOperands() + { + using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + var db = conn.GetDatabase(); + var prefix = Me(); + var key1 = prefix + "1"; + var key2 = prefix + "2"; + + // Clean up keys + db.KeyDelete(new RedisKey[] { key1, key2 }, CommandFlags.FireAndForget); + + // Test with two operands: key1=10101010 (170), key2=11001100 (204) + db.StringSet(key1, new byte[] { 170 }, flags: CommandFlags.FireAndForget); + db.StringSet(key2, new byte[] { 204 }, flags: CommandFlags.FireAndForget); + + // Test DIFF: key1 ∧ ¬key2 = 170 & ~204 = 170 & 51 = 34 + var len_diff = await db.StringBitOperationAsync(Bitwise.Diff, "diff2", new RedisKey[] { key1, key2 }); + Assert.Equal(1, len_diff); + var r_diff = ((byte[]?)(await db.StringGetAsync("diff2")))?.Single(); + Assert.Equal((byte)(170 & ~204), r_diff); + + // Test ONE with two operands (should be equivalent to XOR) + var len_one = await db.StringBitOperationAsync(Bitwise.One, "one2", new RedisKey[] { key1, key2 }); + Assert.Equal(1, len_one); + var r_one = ((byte[]?)(await db.StringGetAsync("one2")))?.Single(); + Assert.Equal((byte)(170 ^ 204), r_one); + + // Verify ONE equals XOR for two operands + var len_xor = await db.StringBitOperationAsync(Bitwise.Xor, "xor2", new RedisKey[] { key1, key2 }); + Assert.Equal(1, len_xor); + var r_xor = ((byte[]?)(await db.StringGetAsync("xor2")))?.Single(); + Assert.Equal(r_one, r_xor); + } + + [Fact] + public void BitOpDiff() + { + using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + var db = conn.GetDatabase(); + var prefix = Me(); + var keyX = prefix + "X"; + var keyY1 = prefix + "Y1"; + var keyY2 = prefix + "Y2"; + var keyResult = prefix + "result"; + + // Clean up keys + db.KeyDelete(new RedisKey[] { keyX, keyY1, keyY2, keyResult }, CommandFlags.FireAndForget); + + // Set up test data: X=11110000, Y1=10100000, Y2=01010000 + // Expected DIFF result: X ∧ ¬(Y1 ∨ Y2) = 11110000 ∧ ¬(11110000) = 00000000 + db.StringSet(keyX, new byte[] { 0b11110000 }, flags: CommandFlags.FireAndForget); + db.StringSet(keyY1, new byte[] { 0b10100000 }, flags: CommandFlags.FireAndForget); + db.StringSet(keyY2, new byte[] { 0b01010000 }, flags: CommandFlags.FireAndForget); + + var length = db.StringBitOperation(Bitwise.Diff, keyResult, new RedisKey[] { keyX, keyY1, keyY2 }); + Assert.Equal(1, length); + + var result = ((byte[]?)db.StringGet(keyResult))?.Single(); + // X ∧ ¬(Y1 ∨ Y2) = 11110000 ∧ ¬(11110000) = 11110000 ∧ 00001111 = 00000000 + Assert.Equal((byte)0b00000000, result); + } + + [Fact] + public void BitOpDiff1() + { + using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + var db = conn.GetDatabase(); + var prefix = Me(); + var keyX = prefix + "X"; + var keyY1 = prefix + "Y1"; + var keyY2 = prefix + "Y2"; + var keyResult = prefix + "result"; + + // Clean up keys + db.KeyDelete(new RedisKey[] { keyX, keyY1, keyY2, keyResult }, CommandFlags.FireAndForget); + + // Set up test data: X=11000000, Y1=10100000, Y2=01010000 + // Expected DIFF1 result: ¬X ∧ (Y1 ∨ Y2) = ¬11000000 ∧ (10100000 ∨ 01010000) = 00111111 ∧ 11110000 = 00110000 + db.StringSet(keyX, new byte[] { 0b11000000 }, flags: CommandFlags.FireAndForget); + db.StringSet(keyY1, new byte[] { 0b10100000 }, flags: CommandFlags.FireAndForget); + db.StringSet(keyY2, new byte[] { 0b01010000 }, flags: CommandFlags.FireAndForget); + + var length = db.StringBitOperation(Bitwise.Diff1, keyResult, new RedisKey[] { keyX, keyY1, keyY2 }); + Assert.Equal(1, length); + + var result = ((byte[]?)db.StringGet(keyResult))?.Single(); + // ¬X ∧ (Y1 ∨ Y2) = 00111111 ∧ 11110000 = 00110000 + Assert.Equal((byte)0b00110000, result); + } + + [Fact] + public void BitOpAndOr() + { + using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + var db = conn.GetDatabase(); + var prefix = Me(); + var keyX = prefix + "X"; + var keyY1 = prefix + "Y1"; + var keyY2 = prefix + "Y2"; + var keyResult = prefix + "result"; + + // Clean up keys + db.KeyDelete(new RedisKey[] { keyX, keyY1, keyY2, keyResult }, CommandFlags.FireAndForget); + + // Set up test data: X=11110000, Y1=10100000, Y2=01010000 + // Expected ANDOR result: X ∧ (Y1 ∨ Y2) = 11110000 ∧ (10100000 ∨ 01010000) = 11110000 ∧ 11110000 = 11110000 + db.StringSet(keyX, new byte[] { 0b11110000 }, flags: CommandFlags.FireAndForget); + db.StringSet(keyY1, new byte[] { 0b10100000 }, flags: CommandFlags.FireAndForget); + db.StringSet(keyY2, new byte[] { 0b01010000 }, flags: CommandFlags.FireAndForget); + + var length = db.StringBitOperation(Bitwise.AndOr, keyResult, new RedisKey[] { keyX, keyY1, keyY2 }); + Assert.Equal(1, length); + + var result = ((byte[]?)db.StringGet(keyResult))?.Single(); + // X ∧ (Y1 ∨ Y2) = 11110000 ∧ 11110000 = 11110000 + Assert.Equal((byte)0b11110000, result); + } + + [Fact] + public void BitOpOne() + { + using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + var db = conn.GetDatabase(); + var prefix = Me(); + var key1 = prefix + "1"; + var key2 = prefix + "2"; + var key3 = prefix + "3"; + var keyResult = prefix + "result"; + + // Clean up keys + db.KeyDelete(new RedisKey[] { key1, key2, key3, keyResult }, CommandFlags.FireAndForget); + + // Set up test data: key1=10100000, key2=01010000, key3=00110000 + // Expected ONE result: bits set in exactly one bitmap = 11000000 + db.StringSet(key1, new byte[] { 0b10100000 }, flags: CommandFlags.FireAndForget); + db.StringSet(key2, new byte[] { 0b01010000 }, flags: CommandFlags.FireAndForget); + db.StringSet(key3, new byte[] { 0b00110000 }, flags: CommandFlags.FireAndForget); + + var length = db.StringBitOperation(Bitwise.One, keyResult, new RedisKey[] { key1, key2, key3 }); + Assert.Equal(1, length); + + var result = ((byte[]?)db.StringGet(keyResult))?.Single(); + // Bits set in exactly one: position 7 (key1 only), position 6 (key2 only) = 11000000 + Assert.Equal((byte)0b11000000, result); + } + + [Fact] + public async Task BitOpDiffAsync() + { + using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + var db = conn.GetDatabase(); + var prefix = Me(); + var keyX = prefix + "X"; + var keyY1 = prefix + "Y1"; + var keyResult = prefix + "result"; + + // Clean up keys + db.KeyDelete(new RedisKey[] { keyX, keyY1, keyResult }, CommandFlags.FireAndForget); + + // Set up test data: X=11110000, Y1=10100000 + // Expected DIFF result: X ∧ ¬Y1 = 11110000 ∧ 01011111 = 01010000 + db.StringSet(keyX, new byte[] { 0b11110000 }, flags: CommandFlags.FireAndForget); + db.StringSet(keyY1, new byte[] { 0b10100000 }, flags: CommandFlags.FireAndForget); + + var length = await db.StringBitOperationAsync(Bitwise.Diff, keyResult, new RedisKey[] { keyX, keyY1 }); + Assert.Equal(1, length); + + var result = ((byte[]?)await db.StringGetAsync(keyResult))?.Single(); + // X ∧ ¬Y1 = 11110000 ∧ 01011111 = 01010000 + Assert.Equal((byte)0b01010000, result); + } + + [Fact] + public void BitOpEdgeCases() + { + using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + var db = conn.GetDatabase(); + var prefix = Me(); + var keyEmpty = prefix + "empty"; + var keyNonEmpty = prefix + "nonempty"; + var keyResult = prefix + "result"; + + // Clean up keys + db.KeyDelete(new RedisKey[] { keyEmpty, keyNonEmpty, keyResult }, CommandFlags.FireAndForget); + + // Test with empty bitmap + db.StringSet(keyNonEmpty, new byte[] { 0b11110000 }, flags: CommandFlags.FireAndForget); + + // DIFF with empty key should return the first key + var length = db.StringBitOperation(Bitwise.Diff, keyResult, new RedisKey[] { keyNonEmpty, keyEmpty }); + Assert.Equal(1, length); + + var result = ((byte[]?)db.StringGet(keyResult))?.Single(); + Assert.Equal((byte)0b11110000, result); + + // ONE with single key should return that key + length = db.StringBitOperation(Bitwise.One, keyResult, new RedisKey[] { keyNonEmpty }); + Assert.Equal(1, length); + + result = ((byte[]?)db.StringGet(keyResult))?.Single(); + Assert.Equal((byte)0b11110000, result); + } + [Fact] public async Task BitPosition() { From 0488c42a8c0d30874a122c8b7b11e6a2b3418e34 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 10 Jul 2025 17:01:21 +0100 Subject: [PATCH 334/435] release notes --- docs/ReleaseNotes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index c775f6cb3..b182284f0 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,7 @@ Current package versions: ## Unreleased +- Add support for new `BITOP` operations in CE 8.2 ([#2900 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2900)) - Package updates ([#2906 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2906)) ## 2.8.41 From e7b3767f2810e0aeba9217e9a5c115076a373e69 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 17 Jul 2025 10:26:33 +0100 Subject: [PATCH 335/435] Fix CLIENT ID error during handshake (#2909) * Fix CLIENT ID error during handshake * update release notes * tilde --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ResultProcessor.cs | 3 +++ tests/StackExchange.Redis.Tests/ConfigTests.cs | 1 + 3 files changed, 5 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b182284f0..b3a788286 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: - Add support for new `BITOP` operations in CE 8.2 ([#2900 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2900)) - Package updates ([#2906 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2906)) +- Fix handshake error with `CLIENT ID` ([#2909 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2909)) ## 2.8.41 diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 03e0a8577..06647212b 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -831,6 +831,9 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { connection.ConnectionId = clientId; Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (CLIENT) connection-id: {clientId}"); + + SetResult(message, true); + return true; } } break; diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index c8a5eacd6..7c8e917b7 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -472,6 +472,7 @@ public void GetClients() if (server.Features.ClientId) { var id = conn.GetConnectionId(server.EndPoint, ConnectionType.Interactive); + Log("client id: " + id); Assert.NotNull(id); Assert.True(clients.Any(x => x.Id == id), "expected: " + id); id = conn.GetConnectionId(server.EndPoint, ConnectionType.Subscription); From 8a527830fd45b5eaef54e9b3b96f3f6ca7af3813 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 17 Jul 2025 12:35:35 +0100 Subject: [PATCH 336/435] docs: offer guidance on using framework methods for async timeouts / cancellation (#2910) --- StackExchange.Redis.sln | 32 +-- docs/AsyncTimeouts.md | 65 ++++++ docs/docs.csproj | 6 + docs/index.md | 1 + .../CancellationTests.cs | 198 ++++++++++++++++++ 5 files changed, 276 insertions(+), 26 deletions(-) create mode 100644 docs/AsyncTimeouts.md create mode 100644 docs/docs.csproj create mode 100644 tests/StackExchange.Redis.Tests/CancellationTests.cs diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index 18a30a9af..6e4416d7d 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -103,31 +103,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{00CA0876-DA9 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "toys", "toys", "{E25031D3-5C64-430D-B86F-697B66816FD8}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{153A10E4-E668-41AD-9E0F-6785CE7EED66}" - ProjectSection(SolutionItems) = preProject - docs\Basics.md = docs\Basics.md - docs\Configuration.md = docs\Configuration.md - docs\Events.md = docs\Events.md - docs\ExecSync.md = docs\ExecSync.md - docs\index.md = docs\index.md - docs\KeysScan.md = docs\KeysScan.md - docs\KeysValues.md = docs\KeysValues.md - docs\PipelinesMultiplexers.md = docs\PipelinesMultiplexers.md - docs\Profiling.md = docs\Profiling.md - docs\Profiling_v1.md = docs\Profiling_v1.md - docs\Profiling_v2.md = docs\Profiling_v2.md - docs\PubSubOrder.md = docs\PubSubOrder.md - docs\ReleaseNotes.md = docs\ReleaseNotes.md - docs\Resp3.md = docs\Resp3.md - docs\RespLogging.md = docs\RespLogging.md - docs\Scripting.md = docs\Scripting.md - docs\Server.md = docs\Server.md - docs\Testing.md = docs\Testing.md - docs\ThreadTheft.md = docs\ThreadTheft.md - docs\Timeouts.md = docs\Timeouts.md - docs\Transactions.md = docs\Transactions.md - EndProjectSection -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestConsoleBaseline", "toys\TestConsoleBaseline\TestConsoleBaseline.csproj", "{D58114AE-4998-4647-AFCA-9353D20495AE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = ".github", ".github\.github.csproj", "{8FB98E7D-DAE2-4465-BD9A-104000E0A2D4}" @@ -142,6 +117,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleTest", "tests\Consol EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleTestBaseline", "tests\ConsoleTestBaseline\ConsoleTestBaseline.csproj", "{69A0ACF2-DF1F-4F49-B554-F732DCA938A3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs", "docs\docs.csproj", "{1DC43E76-5372-4C7F-A433-0602273E87FC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -192,6 +169,10 @@ Global {69A0ACF2-DF1F-4F49-B554-F732DCA938A3}.Debug|Any CPU.Build.0 = Debug|Any CPU {69A0ACF2-DF1F-4F49-B554-F732DCA938A3}.Release|Any CPU.ActiveCfg = Release|Any CPU {69A0ACF2-DF1F-4F49-B554-F732DCA938A3}.Release|Any CPU.Build.0 = Release|Any CPU + {1DC43E76-5372-4C7F-A433-0602273E87FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1DC43E76-5372-4C7F-A433-0602273E87FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DC43E76-5372-4C7F-A433-0602273E87FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1DC43E76-5372-4C7F-A433-0602273E87FC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -209,7 +190,6 @@ Global {D082703F-1652-4C35-840D-7D377F6B9979} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05} {8375813E-FBAF-4DA3-A2C7-E4645B39B931} = {E25031D3-5C64-430D-B86F-697B66816FD8} {3DA1EEED-E9FE-43D9-B293-E000CFCCD91A} = {E25031D3-5C64-430D-B86F-697B66816FD8} - {153A10E4-E668-41AD-9E0F-6785CE7EED66} = {3AD17044-6BFF-4750-9AC2-2CA466375F2A} {D58114AE-4998-4647-AFCA-9353D20495AE} = {E25031D3-5C64-430D-B86F-697B66816FD8} {A9F81DA3-DA82-423E-A5DD-B11C37548E06} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05} {A0F89B8B-32A3-4C28-8F1B-ADE343F16137} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} diff --git a/docs/AsyncTimeouts.md b/docs/AsyncTimeouts.md new file mode 100644 index 000000000..5ba4fd3f1 --- /dev/null +++ b/docs/AsyncTimeouts.md @@ -0,0 +1,65 @@ +# Async timeouts and cancellation + +StackExchange.Redis directly supports timeout of *synchronous* operations, but for *asynchronous* operations, it is recommended +to use the inbuilt framework support for cancellation and timeouts, i.e. the [WaitAsync](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.waitasync) +family of methods. This allows the caller to control timeout (via `TimeSpan`), cancellation (via `CancellationToken`), or both. + +Note that it is possible that operations will still be buffered and may still be issued to the server *after* timeout/cancellation means +that the caller isn't observing the result. + +## Usage + +### Timeout + +Timeouts are probably the most common cancellation scenario: + +```csharp +var timeout = TimeSpan.FromSeconds(5); +await database.StringSetAsync("key", "value").WaitAsync(timeout); +var value = await database.StringGetAsync("key").WaitAsync(timeout); +``` + +### Cancellation + +You can also use `CancellationToken` to drive cancellation, identically: + +```csharp +CancellationToken token = ...; // for example, from HttpContext.RequestAborted +await database.StringSetAsync("key", "value").WaitAsync(token); +var value = await database.StringGetAsync("key").WaitAsync(token); +``` +### Combined Cancellation and Timeout + +These two concepts can be combined so that if either cancellation or timeout occur, the caller's +operation is cancelled: + +```csharp +var timeout = TimeSpan.FromSeconds(5); +CancellationToken token = ...; // for example, from HttpContext.RequestAborted +await database.StringSetAsync("key", "value").WaitAsync(timeout, token); +var value = await database.StringGetAsync("key").WaitAsync(timeout, token); +``` + +### Creating a timeout for multiple operations + +If you want a timeout to apply to a *group* of operations rather than individually, then you +can using `CancellationTokenSource` to create a `CancellationToken` that is cancelled after a +specified timeout. For example: + +```csharp +var timeout = TimeSpan.FromSeconds(5); +using var cts = new CancellationTokenSource(timeout); +await database.StringSetAsync("key", "value").WaitAsync(cts.Token); +var value = await database.StringGetAsync("key").WaitAsync(cts.Token); +``` + +This can additionally be combined with one-or-more cancellation tokens: + +```csharp +var timeout = TimeSpan.FromSeconds(5); +CancellationToken token = ...; // for example, from HttpContext.RequestAborted +using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); // or multiple tokens +cts.CancelAfter(timeout); +await database.StringSetAsync("key", "value").WaitAsync(cts.Token); +var value = await database.StringGetAsync("key").WaitAsync(cts.Token); +`````` \ No newline at end of file diff --git a/docs/docs.csproj b/docs/docs.csproj new file mode 100644 index 000000000..977e065bc --- /dev/null +++ b/docs/docs.csproj @@ -0,0 +1,6 @@ + + + + netstandard2.0 + + diff --git a/docs/index.md b/docs/index.md index 40acca077..0b4d9bb2e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -32,6 +32,7 @@ Documentation - [Server](Server) - running a redis server - [Basic Usage](Basics) - getting started and basic usage +- [Async Timeouts](AsyncTimeouts) - async timeouts and cancellation - [Configuration](Configuration) - options available when connecting to redis - [Pipelines and Multiplexers](PipelinesMultiplexers) - what is a multiplexer? - [Keys, Values and Channels](KeysValues) - discusses the data-types used on the API diff --git a/tests/StackExchange.Redis.Tests/CancellationTests.cs b/tests/StackExchange.Redis.Tests/CancellationTests.cs new file mode 100644 index 000000000..58e33e3de --- /dev/null +++ b/tests/StackExchange.Redis.Tests/CancellationTests.cs @@ -0,0 +1,198 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace StackExchange.Redis.Tests; + +#if !NET6_0_OR_GREATER +internal static class TaskExtensions +{ + // suboptimal polyfill version of the .NET 6+ API; I'm not recommending this for production use, + // but it's good enough for tests + public static Task WaitAsync(this Task task, CancellationToken cancellationToken) + { + if (task.IsCompleted || !cancellationToken.CanBeCanceled) return task; + return Wrap(task, cancellationToken); + + static async Task Wrap(Task task, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + using var reg = cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)); + _ = task.ContinueWith(t => + { + if (t.IsCanceled) tcs.TrySetCanceled(); + else if (t.IsFaulted) tcs.TrySetException(t.Exception!); + else tcs.TrySetResult(t.Result); + }); + return await tcs.Task; + } + } + + public static Task WaitAsync(this Task task, TimeSpan timeout) + { + if (task.IsCompleted) return task; + return Wrap(task, timeout); + + static async Task Wrap(Task task, TimeSpan timeout) + { + Task other = Task.Delay(timeout); + var first = await Task.WhenAny(task, other); + if (ReferenceEquals(first, other)) + { + throw new TimeoutException(); + } + return await task; + } + } +} +#endif + +[Collection(SharedConnectionFixture.Key)] +public class CancellationTests : TestBase +{ + public CancellationTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } + + [Fact] + public async Task WithCancellation_CancelledToken_ThrowsOperationCanceledException() + { + using var conn = Create(); + var db = conn.GetDatabase(); + + using var cts = new CancellationTokenSource(); + cts.Cancel(); // Cancel immediately + + await Assert.ThrowsAnyAsync(async () => + { + await db.StringSetAsync(Me(), "value").WaitAsync(cts.Token); + }); + } + + private IInternalConnectionMultiplexer Create() => Create(syncTimeout: 10_000); + + [Fact] + public async Task WithCancellation_ValidToken_OperationSucceeds() + { + using var conn = Create(); + var db = conn.GetDatabase(); + + using var cts = new CancellationTokenSource(); + + RedisKey key = Me(); + // This should succeed + await db.StringSetAsync(key, "value"); + var result = await db.StringGetAsync(key).WaitAsync(cts.Token); + Assert.Equal("value", result); + } + + private void Pause(IDatabase db) + { + db.Execute("client", new object[] { "pause", ConnectionPauseMilliseconds }, CommandFlags.FireAndForget); + } + + [Fact] + public async Task WithTimeout_ShortTimeout_Async_ThrowsOperationCanceledException() + { + using var conn = Create(); + var db = conn.GetDatabase(); + + var watch = Stopwatch.StartNew(); + Pause(db); + + var timeout = TimeSpan.FromMilliseconds(ShortDelayMilliseconds); + // This might throw due to timeout, but let's test the mechanism + var pending = db.StringSetAsync(Me(), "value").WaitAsync(timeout); // check we get past this + try + { + await pending; + // If it succeeds, that's fine too - Redis is fast + Assert.Fail(ExpectedCancel + ": " + watch.ElapsedMilliseconds + "ms"); + } + catch (TimeoutException) + { + // Expected for very short timeouts + Log($"Timeout after {watch.ElapsedMilliseconds}ms"); + } + } + + private const string ExpectedCancel = "This operation should have been cancelled"; + + [Fact] + public async Task WithoutCancellation_OperationsWorkNormally() + { + using var conn = Create(); + var db = conn.GetDatabase(); + + // No cancellation - should work normally + RedisKey key = Me(); + await db.StringSetAsync(key, "value"); + var result = await db.StringGetAsync(key); + Assert.Equal("value", result); + } + + public enum CancelStrategy + { + Constructor, + Method, + Manual, + } + + private const int ConnectionPauseMilliseconds = 50, ShortDelayMilliseconds = 5; + + private static CancellationTokenSource CreateCts(CancelStrategy strategy) + { + switch (strategy) + { + case CancelStrategy.Constructor: + return new CancellationTokenSource(TimeSpan.FromMilliseconds(ShortDelayMilliseconds)); + case CancelStrategy.Method: + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMilliseconds(ShortDelayMilliseconds)); + return cts; + case CancelStrategy.Manual: + cts = new(); + _ = Task.Run(async () => + { + await Task.Delay(ShortDelayMilliseconds); + // ReSharper disable once MethodHasAsyncOverload - TFM-dependent + cts.Cancel(); + }); + return cts; + default: + throw new ArgumentOutOfRangeException(nameof(strategy)); + } + } + + [Theory] + [InlineData(CancelStrategy.Constructor)] + [InlineData(CancelStrategy.Method)] + [InlineData(CancelStrategy.Manual)] + public async Task CancellationDuringOperation_Async_CancelsGracefully(CancelStrategy strategy) + { + using var conn = Create(); + var db = conn.GetDatabase(); + + var watch = Stopwatch.StartNew(); + Pause(db); + + // Cancel after a short delay + using var cts = CreateCts(strategy); + + // Start an operation and cancel it mid-flight + var pending = db.StringSetAsync($"{Me()}:{strategy}", "value").WaitAsync(cts.Token); + + try + { + await pending; + Assert.Fail(ExpectedCancel + ": " + watch.ElapsedMilliseconds + "ms"); + } + catch (OperationCanceledException oce) + { + // Expected if cancellation happens during operation + Log($"Cancelled after {watch.ElapsedMilliseconds}ms"); + Assert.Equal(cts.Token, oce.CancellationToken); + } + } +} From 894b95e347bd2c9b1a6304fed5c17443d9c48415 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 19 Jul 2025 09:54:59 +0100 Subject: [PATCH 337/435] release notes 2.8.47 --- docs/ReleaseNotes.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b3a788286..6754ea017 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,8 +8,13 @@ Current package versions: ## Unreleased +- (none) + +## 2.8.47 + - Add support for new `BITOP` operations in CE 8.2 ([#2900 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2900)) - Package updates ([#2906 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2906)) +- Docs: added [guidance on async timeouts](https://stackexchange.github.io/StackExchange.Redis/AsyncTimeouts) ([#2910 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2910)) - Fix handshake error with `CLIENT ID` ([#2909 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2909)) ## 2.8.41 From 5f586170d4791bf85ceb1eb48ac8672b42cade50 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Mon, 21 Jul 2025 04:15:03 -0400 Subject: [PATCH 338/435] [Tests] Upgrade to xUnit v3 (#2907) * [Tests] Upgrade to xunit v3 Upgrading xunit to v3 to see if this helps our stability issues any. * More async and stabilization * Don't install .NET 6 + 7 anymore on AppVeyor * Fix CodeQL for latest C# * Explicit .NET 9 install * Revert TestAutomaticHeartbeat approach for now * More IAsyncDisposable * IAsyncDisposable for NoConnectionException * Fix shared on aborted connection * Merge latest in * Tweak DisconnectAndReconnectThrowsConnectionExceptionSync * Buffer on abort tests * Saveeeeeee * Fix sharing, method overlaps, source info + parallelism * Add console runner * Prevent cluster tests running first from hosing the default connection * More stability * Run only net8.0 tests on AppVeyor * Cleanup + stability * Try speeding up Sentinel * More key conflict cleanup * More optimization + stability * Idle test stability with split for time * Cleanup + more stability/perf * ClusterNodeSubscriptionFailover * More stability fixes * Cleanup --- .github/workflows/codeql.yml | 18 +- Directory.Build.props | 2 +- Directory.Packages.props | 6 +- appveyor.yml | 6 +- build.ps1 | 9 +- .../ConnectionMultiplexer.cs | 2 +- src/StackExchange.Redis/TaskExtensions.cs | 2 +- .../AbortOnConnectFailTests.cs | 35 +- tests/StackExchange.Redis.Tests/AdhocTests.cs | 13 +- .../AggressiveTests.cs | 34 +- tests/StackExchange.Redis.Tests/AsyncTests.cs | 15 +- .../AzureMaintenanceEventTests.cs | 5 +- .../StackExchange.Redis.Tests/BacklogTests.cs | 23 +- .../StackExchange.Redis.Tests/BasicOpTests.cs | 101 ++--- tests/StackExchange.Redis.Tests/BatchTests.cs | 32 +- tests/StackExchange.Redis.Tests/BitTests.cs | 13 +- .../BoxUnboxTests.cs | 140 +++---- .../CancellationTests.cs | 70 +--- .../Certificates/CertValidationTests.cs | 5 +- .../ClientKillTests.cs | 31 +- .../ClusterShardedTests.cs | 177 ++++++++ .../StackExchange.Redis.Tests/ClusterTests.cs | 299 +++----------- .../CommandTimeoutTests.cs | 16 +- .../StackExchange.Redis.Tests/ConfigTests.cs | 137 +++--- .../ConnectByIPTests.cs | 24 +- .../ConnectCustomConfigTests.cs | 39 +- .../ConnectFailTimeoutTests.cs | 9 +- .../ConnectToUnexistingHostTests.cs | 7 +- .../ConnectingFailDetectionTests.cs | 27 +- .../ConnectionFailedErrorsTests.cs | 13 +- .../ConnectionReconnectRetryPolicyTests.cs | 5 +- .../ConnectionShutdownTests.cs | 10 +- .../ConstraintsTests.cs | 8 +- tests/StackExchange.Redis.Tests/CopyTests.cs | 12 +- .../DatabaseTests.cs | 30 +- .../DefaultOptionsTests.cs | 21 +- .../DeprecatedTests.cs | 5 +- tests/StackExchange.Redis.Tests/EnvoyTests.cs | 16 +- .../ExceptionFactoryTests.cs | 38 +- .../StackExchange.Redis.Tests/ExecuteTests.cs | 10 +- .../StackExchange.Redis.Tests/ExpiryTests.cs | 20 +- .../FSharpCompatTests.cs | 5 +- .../FailoverTests.cs | 76 ++-- .../FloatingPointTests.cs | 50 ++- .../StackExchange.Redis.Tests/FormatTests.cs | 5 +- .../GarbageCollectionTests.cs | 12 +- tests/StackExchange.Redis.Tests/GeoTests.cs | 102 +++-- .../HashFieldTests.cs | 127 +++--- tests/StackExchange.Redis.Tests/HashTests.cs | 106 +++-- .../HeartbeatTests.cs | 46 +++ .../Helpers/Attributes.cs | 300 +++++--------- .../Helpers/Extensions.cs | 14 +- .../Helpers/IRedisTest.cs | 8 - .../Helpers/SharedConnectionFixture.cs | 21 +- .../StackExchange.Redis.Tests/Helpers/Skip.cs | 17 +- .../Helpers/TestConfig.cs | 7 +- .../Helpers/TestContext.cs | 20 - .../Helpers/TestExtensions.cs | 22 + .../Helpers/TextWriterOutputHelper.cs | 16 +- .../Helpers/redis-sharp.cs | 22 +- .../HighIntegrityBasicOpsTests.cs | 8 + .../HttpTunnelConnectTests.cs | 8 +- .../HyperLogLogTests.cs | 19 +- .../InfoReplicationCheckTests.cs | 8 +- .../Issues/BgSaveResponseTests.cs | 7 +- .../Issues/DefaultDatabaseTests.cs | 14 +- .../Issues/Issue10Tests.cs | 24 +- .../Issues/Issue1101Tests.cs | 11 +- .../Issues/Issue1103Tests.cs | 10 +- .../Issues/Issue182Tests.cs | 15 +- .../Issues/Issue2176Tests.cs | 21 +- .../Issues/Issue2392Tests.cs | 7 +- .../Issues/Issue2418.cs | 14 +- .../Issues/Issue2507.cs | 68 ++- .../Issues/Issue25Tests.cs | 5 +- .../Issues/Issue2763Tests.cs | 9 +- .../Issues/Issue6Tests.cs | 13 +- .../Issues/MassiveDeleteTests.cs | 21 +- .../Issues/SO10504853Tests.cs | 38 +- .../Issues/SO10825542Tests.cs | 7 +- .../Issues/SO11766033Tests.cs | 22 +- .../Issues/SO22786599Tests.cs | 14 +- .../Issues/SO23949477Tests.cs | 17 +- .../Issues/SO24807536Tests.cs | 11 +- .../Issues/SO25113323Tests.cs | 7 +- .../Issues/SO25567566Tests.cs | 11 +- .../KeyIdleAsyncTests.cs | 49 +++ .../StackExchange.Redis.Tests/KeyIdleTests.cs | 49 +++ .../KeyPrefixedDatabaseTests.cs | 44 +- .../KeyPrefixedTests.cs | 46 +-- tests/StackExchange.Redis.Tests/KeyTests.cs | 199 +++------ .../StackExchange.Redis.Tests/LatencyTests.cs | 25 +- tests/StackExchange.Redis.Tests/LexTests.cs | 32 +- tests/StackExchange.Redis.Tests/ListTests.cs | 198 +++++---- .../StackExchange.Redis.Tests/LockingTests.cs | 26 +- .../StackExchange.Redis.Tests/LoggerTests.cs | 28 +- .../MassiveOpsTests.cs | 25 +- .../StackExchange.Redis.Tests/MemoryTests.cs | 14 +- .../StackExchange.Redis.Tests/MigrateTests.cs | 14 +- .../MultiAddTests.cs | 79 ++-- .../MultiPrimaryTests.cs | 13 +- .../StackExchange.Redis.Tests/NamingTests.cs | 5 +- .../OverloadCompatTests.cs | 16 +- tests/StackExchange.Redis.Tests/ParseTests.cs | 5 +- .../PerformanceTests.cs | 20 +- .../PreserveOrderTests.cs | 11 +- .../ProfilingTests.cs | 48 +-- .../PubSubCommandTests.cs | 14 +- .../PubSubMultiserverTests.cs | 21 +- .../StackExchange.Redis.Tests/PubSubTests.cs | 78 ++-- .../RealWorldTests.cs | 7 +- .../RedisResultTests.cs | 17 +- .../{TestConfig.json => RedisTestConfig.json} | 0 .../RespProtocolTests.cs | 44 +- tests/StackExchange.Redis.Tests/RoleTests.cs | 15 +- tests/StackExchange.Redis.Tests/SSDBTests.cs | 12 +- tests/StackExchange.Redis.Tests/SSLTests.cs | 51 ++- tests/StackExchange.Redis.Tests/ScanTests.cs | 67 ++- .../ScriptingTests.cs | 269 ++++++------ .../StackExchange.Redis.Tests/SecureTests.cs | 23 +- .../StackExchange.Redis.Tests/SentinelBase.cs | 15 +- .../SentinelFailoverTests.cs | 10 +- .../SentinelTests.cs | 73 ++-- tests/StackExchange.Redis.Tests/SetTests.cs | 90 ++-- .../StackExchange.Redis.Tests/SocketTests.cs | 15 +- .../SortedSetTests.cs | 391 +++++++++--------- .../SortedSetWhenTests.cs | 17 +- .../StackExchange.Redis.Tests.csproj | 10 +- .../StackExchange.Redis.Tests/StreamTests.cs | 368 ++++++++--------- .../StackExchange.Redis.Tests/StringTests.cs | 146 ++++--- .../SyncContextTests.cs | 17 +- .../TaskExtensions.cs | 49 +++ tests/StackExchange.Redis.Tests/TestBase.cs | 83 ++-- .../TransactionTests.cs | 75 ++-- tests/StackExchange.Redis.Tests/ValueTests.cs | 5 +- .../WithKeyPrefixTests.cs | 39 +- .../xunit.runner.json | 3 +- 137 files changed, 2697 insertions(+), 3143 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/ClusterShardedTests.cs create mode 100644 tests/StackExchange.Redis.Tests/HeartbeatTests.cs delete mode 100644 tests/StackExchange.Redis.Tests/Helpers/IRedisTest.cs delete mode 100644 tests/StackExchange.Redis.Tests/Helpers/TestContext.cs create mode 100644 tests/StackExchange.Redis.Tests/HighIntegrityBasicOpsTests.cs create mode 100644 tests/StackExchange.Redis.Tests/KeyIdleAsyncTests.cs create mode 100644 tests/StackExchange.Redis.Tests/KeyIdleTests.cs rename tests/StackExchange.Redis.Tests/{TestConfig.json => RedisTestConfig.json} (100%) create mode 100644 tests/StackExchange.Redis.Tests/TaskExtensions.cs diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1d560447c..53f5701f2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,13 +29,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -45,18 +50,11 @@ jobs: # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). - # If this step fails, then you should remove it and run the build manually (see below) - - if: matrix.language != 'csharp' - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - if: matrix.language == 'csharp' name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index ff720e26a..9f512d5e9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,7 +15,7 @@ https://stackexchange.github.io/StackExchange.Redis/ MIT - 11 + 13 git https://github.com/StackExchange/StackExchange.Redis/ diff --git a/Directory.Packages.props b/Directory.Packages.props index 6d15ad199..79c404dc2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,6 +15,7 @@ + @@ -25,7 +26,8 @@ - - + + + \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index b180c6544..678032414 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,10 +6,6 @@ init: install: - cmd: >- - choco install dotnet-6.0-sdk - - choco install dotnet-7.0-sdk - choco install dotnet-9.0-sdk cd tests\RedisConfigs\3.0.503 @@ -71,7 +67,7 @@ nuget: disable_publish_on_pr: true build_script: -- ps: .\build.ps1 -PullRequestNumber "$env:APPVEYOR_PULL_REQUEST_NUMBER" -CreatePackages ($env:OS -eq "Windows_NT") +- ps: .\build.ps1 -PullRequestNumber "$env:APPVEYOR_PULL_REQUEST_NUMBER" -CreatePackages ($env:OS -eq "Windows_NT") -NetCoreOnlyTests test: off artifacts: diff --git a/build.ps1 b/build.ps1 index 24152baab..3ace75a06 100644 --- a/build.ps1 +++ b/build.ps1 @@ -3,7 +3,8 @@ param( [bool] $CreatePackages, [switch] $StartServers, [bool] $RunTests = $true, - [string] $PullRequestNumber + [string] $PullRequestNumber, + [switch] $NetCoreOnlyTests ) Write-Host "Run Parameters:" -ForegroundColor Cyan @@ -29,7 +30,11 @@ if ($RunTests) { Write-Host "Servers Started." -ForegroundColor "Green" } Write-Host "Running tests: Build.csproj traversal (all frameworks)" -ForegroundColor "Magenta" - dotnet test ".\Build.csproj" -c Release --no-build --logger trx + if ($NetCoreOnlyTests) { + dotnet test ".\Build.csproj" -c Release -f net8.0 --no-build --logger trx + } else { + dotnet test ".\Build.csproj" -c Release --no-build --logger trx + } if ($LastExitCode -ne 0) { Write-Host "Error with tests, aborting build." -Foreground "Red" Exit 1 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index e17a9503b..7ac25e42b 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -535,7 +535,7 @@ static void LogWithThreadPoolStats(ILogger? log, string message, out int busyWor } try { - await Task.WhenAny(task, Task.Delay(remaining)).ObserveErrors().ForAwait(); + await task.TimeoutAfter(remaining).ObserveErrors().ForAwait(); } catch { } diff --git a/src/StackExchange.Redis/TaskExtensions.cs b/src/StackExchange.Redis/TaskExtensions.cs index 081a691ec..ad4b41113 100644 --- a/src/StackExchange.Redis/TaskExtensions.cs +++ b/src/StackExchange.Redis/TaskExtensions.cs @@ -34,7 +34,7 @@ internal static Task ObserveErrors(this Task task) [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static ConfiguredValueTaskAwaitable ForAwait(this in ValueTask task) => task.ConfigureAwait(false); - internal static void RedisFireAndForget(this Task task) => task?.ContinueWith(t => GC.KeepAlive(t.Exception), TaskContinuationOptions.OnlyOnFaulted); + internal static void RedisFireAndForget(this Task task) => task?.ContinueWith(static t => GC.KeepAlive(t.Exception), TaskContinuationOptions.OnlyOnFaulted); /// /// Licensed to the .NET Foundation under one or more agreements. diff --git a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs index 2920a51d3..b68e57c9e 100644 --- a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs +++ b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs @@ -2,18 +2,15 @@ using System.Threading.Tasks; using StackExchange.Redis.Tests.Helpers; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class AbortOnConnectFailTests : TestBase +public class AbortOnConnectFailTests(ITestOutputHelper output) : TestBase(output) { - public AbortOnConnectFailTests(ITestOutputHelper output) : base(output) { } - [Fact] - public void NeverEverConnectedNoBacklogThrowsConnectionNotAvailableSync() + public async Task NeverEverConnectedNoBacklogThrowsConnectionNotAvailableSync() { - using var conn = GetFailFastConn(); + await using var conn = GetFailFastConn(); var db = conn.GetDatabase(); var key = Me(); @@ -26,7 +23,7 @@ public void NeverEverConnectedNoBacklogThrowsConnectionNotAvailableSync() [Fact] public async Task NeverEverConnectedNoBacklogThrowsConnectionNotAvailableAsync() { - using var conn = GetFailFastConn(); + await using var conn = GetFailFastConn(); var db = conn.GetDatabase(); var key = Me(); @@ -37,13 +34,13 @@ public async Task NeverEverConnectedNoBacklogThrowsConnectionNotAvailableAsync() } [Fact] - public void DisconnectAndReconnectThrowsConnectionExceptionSync() + public async Task DisconnectAndReconnectThrowsConnectionExceptionSync() { - using var conn = GetWorkingBacklogConn(); + await using var conn = GetWorkingBacklogConn(); var db = conn.GetDatabase(); var key = Me(); - _ = db.Ping(); // Doesn't throw - we're connected + await db.PingAsync(); // Doesn't throw - we're connected // Disconnect and don't allow re-connection conn.AllowConnect = false; @@ -54,7 +51,7 @@ public void DisconnectAndReconnectThrowsConnectionExceptionSync() var ex = Assert.ThrowsAny(() => db.Ping()); Log("Exception: " + ex.Message); Assert.True(ex is RedisConnectionException or RedisTimeoutException); - Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available (400ms) - Last Connection Exception: ", ex.Message); + Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available (1000ms) - Last Connection Exception: ", ex.Message); Assert.NotNull(ex.InnerException); var iex = Assert.IsType(ex.InnerException); Assert.Contains(iex.Message, ex.Message); @@ -63,11 +60,11 @@ public void DisconnectAndReconnectThrowsConnectionExceptionSync() [Fact] public async Task DisconnectAndNoReconnectThrowsConnectionExceptionAsync() { - using var conn = GetWorkingBacklogConn(); + await using var conn = GetWorkingBacklogConn(); var db = conn.GetDatabase(); var key = Me(); - _ = db.Ping(); // Doesn't throw - we're connected + await db.PingAsync(); // Doesn't throw - we're connected // Disconnect and don't allow re-connection conn.AllowConnect = false; @@ -77,25 +74,25 @@ public async Task DisconnectAndNoReconnectThrowsConnectionExceptionAsync() // Exception: The message timed out in the backlog attempting to send because no connection became available (400ms) - Last Connection Exception: SocketFailure (InputReaderCompleted, last-recv: 7) on 127.0.0.1:6379/Interactive, Idle/ReadAsync, last: PING, origin: SimulateConnectionFailure, outstanding: 0, last-read: 0s ago, last-write: 0s ago, keep-alive: 100s, state: ConnectedEstablished, mgr: 8 of 10 available, in: 0, in-pipe: 0, out-pipe: 0, last-heartbeat: never, last-mbeat: 0s ago, global: 0s ago, v: 2.6.120.51136, command=PING, timeout: 100, inst: 0, qu: 0, qs: 0, aw: False, bw: CheckingForTimeout, last-in: 0, cur-in: 0, sync-ops: 1, async-ops: 1, serverEndpoint: 127.0.0.1:6379, conn-sec: n/a, aoc: 0, mc: 1/1/0, mgr: 8 of 10 available, clientName: CRAVERTOP7(SE.Redis-v2.6.120.51136), IOCP: (Busy=0,Free=1000,Min=16,Max=1000), WORKER: (Busy=6,Free=32761,Min=16,Max=32767), POOL: (Threads=33,QueuedItems=0,CompletedItems=5547,Timers=60), v: 2.6.120.51136 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts) var ex = await Assert.ThrowsAsync(() => db.PingAsync()); Log("Exception: " + ex.Message); - Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available (400ms) - Last Connection Exception: ", ex.Message); + Assert.StartsWith("The message timed out in the backlog attempting to send because no connection became available (1000ms) - Last Connection Exception: ", ex.Message); Assert.NotNull(ex.InnerException); var iex = Assert.IsType(ex.InnerException); Assert.Contains(iex.Message, ex.Message); } private ConnectionMultiplexer GetFailFastConn() => - ConnectionMultiplexer.Connect(GetOptions(BacklogPolicy.FailFast).Apply(o => o.EndPoints.Add($"doesnot.exist.{Guid.NewGuid():N}:6379")), Writer); + ConnectionMultiplexer.Connect(GetOptions(BacklogPolicy.FailFast, 400).Apply(o => o.EndPoints.Add($"doesnot.exist.{Guid.NewGuid():N}:6379")), Writer); private ConnectionMultiplexer GetWorkingBacklogConn() => - ConnectionMultiplexer.Connect(GetOptions(BacklogPolicy.Default).Apply(o => o.EndPoints.Add(GetConfiguration())), Writer); + ConnectionMultiplexer.Connect(GetOptions(BacklogPolicy.Default, 1000).Apply(o => o.EndPoints.Add(GetConfiguration())), Writer); - private ConfigurationOptions GetOptions(BacklogPolicy policy) => new ConfigurationOptions() + private static ConfigurationOptions GetOptions(BacklogPolicy policy, int duration) => new ConfigurationOptions() { AbortOnConnectFail = false, BacklogPolicy = policy, ConnectTimeout = 500, - SyncTimeout = 400, - KeepAlive = 400, + SyncTimeout = duration, + KeepAlive = duration, AllowAdmin = true, }.WithoutSubscriptions(); } diff --git a/tests/StackExchange.Redis.Tests/AdhocTests.cs b/tests/StackExchange.Redis.Tests/AdhocTests.cs index 38ae5639a..42a5ebb23 100644 --- a/tests/StackExchange.Redis.Tests/AdhocTests.cs +++ b/tests/StackExchange.Redis.Tests/AdhocTests.cs @@ -1,17 +1,14 @@ -using Xunit; -using Xunit.Abstractions; +using System.Threading.Tasks; +using Xunit; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class AdhocTests : TestBase +public class AdhocTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public AdhocTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void TestAdhocCommandsAPI() + public async Task TestAdhocCommandsAPI() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); // needs explicit RedisKey type for key-based diff --git a/tests/StackExchange.Redis.Tests/AggressiveTests.cs b/tests/StackExchange.Redis.Tests/AggressiveTests.cs index 73d06c4c7..f0ba91f16 100644 --- a/tests/StackExchange.Redis.Tests/AggressiveTests.cs +++ b/tests/StackExchange.Redis.Tests/AggressiveTests.cs @@ -1,18 +1,16 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class AggressiveTests : TestBase +public class AggressiveTests(ITestOutputHelper output) : TestBase(output) { - public AggressiveTests(ITestOutputHelper output) : base(output) { } - - [FactLongRunning] + [Fact] public async Task ParallelTransactionsWithConditions() { + Skip.UnlessLongRunning(); const int Muxers = 4, Workers = 20, PerThread = 250; var muxers = new IConnectionMultiplexer[Muxers]; @@ -24,7 +22,7 @@ public async Task ParallelTransactionsWithConditions() RedisKey hits = Me(), trigger = Me() + "3"; int expectedSuccess = 0; - await muxers[0].GetDatabase().KeyDeleteAsync(new[] { hits, trigger }).ForAwait(); + await muxers[0].GetDatabase().KeyDeleteAsync([hits, trigger]).ForAwait(); Task[] tasks = new Task[Workers]; for (int i = 0; i < tasks.Length; i++) @@ -73,10 +71,11 @@ public async Task ParallelTransactionsWithConditions() private const int IterationCount = 5000, InnerCount = 20; - [FactLongRunning] - public void RunCompetingBatchesOnSameMuxer() + [Fact] + public async Task RunCompetingBatchesOnSameMuxer() { - using var conn = Create(); + Skip.UnlessLongRunning(); + await using var conn = Create(); var db = conn.GetDatabase(); Thread x = new Thread(state => BatchRunPings((IDatabase)state!)) @@ -132,10 +131,11 @@ private static void BatchRunPings(IDatabase db) } } - [FactLongRunning] + [Fact] public async Task RunCompetingBatchesOnSameMuxerAsync() { - using var conn = Create(); + Skip.UnlessLongRunning(); + await using var conn = Create(); var db = conn.GetDatabase(); var x = Task.Run(() => BatchRunPingsAsync(db)); @@ -189,10 +189,11 @@ private static async Task BatchRunPingsAsync(IDatabase db) } } - [FactLongRunning] - public void RunCompetingTransactionsOnSameMuxer() + [Fact] + public async Task RunCompetingTransactionsOnSameMuxer() { - using var conn = Create(logTransactionData: false); + Skip.UnlessLongRunning(); + await using var conn = Create(logTransactionData: false); var db = conn.GetDatabase(); Thread x = new Thread(state => TranRunPings((IDatabase)state!)) @@ -252,10 +253,11 @@ private void TranRunPings(IDatabase db) } } - [FactLongRunning] + [Fact] public async Task RunCompetingTransactionsOnSameMuxerAsync() { - using var conn = Create(logTransactionData: false); + Skip.UnlessLongRunning(); + await using var conn = Create(logTransactionData: false); var db = conn.GetDatabase(); var x = Task.Run(() => TranRunPingsAsync(db)); diff --git a/tests/StackExchange.Redis.Tests/AsyncTests.cs b/tests/StackExchange.Redis.Tests/AsyncTests.cs index e42e2f07d..cba1b1145 100644 --- a/tests/StackExchange.Redis.Tests/AsyncTests.cs +++ b/tests/StackExchange.Redis.Tests/AsyncTests.cs @@ -3,23 +3,18 @@ using System.Linq; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class AsyncTests : TestBase +public class AsyncTests(ITestOutputHelper output) : TestBase(output) { - public AsyncTests(ITestOutputHelper output) : base(output) { } - - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - [Fact] - public void AsyncTasksReportFailureIfServerUnavailable() + public async Task AsyncTasksReportFailureIfServerUnavailable() { SetExpectedAmbientFailureCount(-1); // this will get messy - using var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); + await using var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); var server = conn.GetServer(TestConfig.Current.PrimaryServer, TestConfig.Current.PrimaryPort); RedisKey key = Me(); @@ -45,8 +40,8 @@ public void AsyncTasksReportFailureIfServerUnavailable() [Fact] public async Task AsyncTimeoutIsNoticed() { - using var conn = Create(syncTimeout: 1000, asyncTimeout: 1000); - using var pauseConn = Create(); + await using var conn = Create(syncTimeout: 1000, asyncTimeout: 1000); + await using var pauseConn = Create(); var opt = ConfigurationOptions.Parse(conn.Configuration); if (!Debugger.IsAttached) { // we max the timeouts if a debugger is detected diff --git a/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs b/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs index 852ab9af7..b43731efc 100644 --- a/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs +++ b/tests/StackExchange.Redis.Tests/AzureMaintenanceEventTests.cs @@ -3,14 +3,11 @@ using System.Net; using StackExchange.Redis.Maintenance; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class AzureMaintenanceEventTests : TestBase +public class AzureMaintenanceEventTests(ITestOutputHelper output) : TestBase(output) { - public AzureMaintenanceEventTests(ITestOutputHelper output) : base(output) { } - [Theory] [InlineData("NotificationType|NodeMaintenanceStarting|StartTimeInUTC|2021-03-02T23:26:57|IsReplica|False|IPAddress||SSLPort|15001|NonSSLPort|13001", AzureNotificationType.NodeMaintenanceStarting, "2021-03-02T23:26:57", false, null, 15001, 13001)] [InlineData("NotificationType|NodeMaintenanceFailover|StartTimeInUTC||IsReplica|False|IPAddress||SSLPort|15001|NonSSLPort|13001", AzureNotificationType.NodeMaintenanceFailoverComplete, null, false, null, 15001, 13001)] diff --git a/tests/StackExchange.Redis.Tests/BacklogTests.cs b/tests/StackExchange.Redis.Tests/BacklogTests.cs index b81c86b8a..f0c0d3d0c 100644 --- a/tests/StackExchange.Redis.Tests/BacklogTests.cs +++ b/tests/StackExchange.Redis.Tests/BacklogTests.cs @@ -1,14 +1,11 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class BacklogTests : TestBase +public class BacklogTests(ITestOutputHelper output) : TestBase(output) { - public BacklogTests(ITestOutputHelper output) : base(output) { } - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; [Fact] @@ -49,7 +46,7 @@ void PrintSnapshot(ConnectionMultiplexer muxer) }; options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); - using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); var db = conn.GetDatabase(); Log("Test: Initial (connected) ping"); @@ -122,7 +119,7 @@ public async Task QueuesAndFlushesAfterReconnectingAsync() }; options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); - using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); conn.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); conn.InternalError += (s, e) => Log($"Internal Error {e.EndPoint}: {e.Exception.Message}"); conn.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); @@ -213,7 +210,7 @@ public async Task QueuesAndFlushesAfterReconnecting() }; options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); - using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); conn.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); conn.InternalError += (s, e) => Log($"Internal Error {e.EndPoint}: {e.Exception.Message}"); conn.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); @@ -236,10 +233,12 @@ public async Task QueuesAndFlushesAfterReconnecting() // Queue up some commands Log("Test: Disconnected pings"); - Task[] pings = new Task[3]; - pings[0] = RunBlockingSynchronousWithExtraThreadAsync(() => DisconnectedPings(1)); - pings[1] = RunBlockingSynchronousWithExtraThreadAsync(() => DisconnectedPings(2)); - pings[2] = RunBlockingSynchronousWithExtraThreadAsync(() => DisconnectedPings(3)); + Task[] pings = + [ + RunBlockingSynchronousWithExtraThreadAsync(() => DisconnectedPings(1)), + RunBlockingSynchronousWithExtraThreadAsync(() => DisconnectedPings(2)), + RunBlockingSynchronousWithExtraThreadAsync(() => DisconnectedPings(3)), + ]; void DisconnectedPings(int id) { // No need to delay, we're going to try a disconnected connection immediately so it'll fail... @@ -311,7 +310,7 @@ public async Task QueuesAndFlushesAfterReconnectingClusterAsync() options.AllowAdmin = true; options.SocketManager = SocketManager.ThreadPool; - using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); conn.ErrorMessage += (s, e) => Log($"Error Message {e.EndPoint}: {e.Message}"); conn.InternalError += (s, e) => Log($"Internal Error {e.EndPoint}: {e.Exception.Message}"); conn.ConnectionFailed += (s, a) => Log("Disconnected: " + EndPointCollection.ToString(a.EndPoint)); diff --git a/tests/StackExchange.Redis.Tests/BasicOpTests.cs b/tests/StackExchange.Redis.Tests/BasicOpTests.cs index 58c82dda6..ad85f5a88 100644 --- a/tests/StackExchange.Redis.Tests/BasicOpTests.cs +++ b/tests/StackExchange.Redis.Tests/BasicOpTests.cs @@ -3,27 +3,15 @@ using System.Threading.Tasks; using StackExchange.Redis.KeyspaceIsolation; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class HighIntegrityBasicOpsTests : BasicOpsTests +public class BasicOpsTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public HighIntegrityBasicOpsTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - - internal override bool HighIntegrity => true; -} - -[Collection(SharedConnectionFixture.Key)] -public class BasicOpsTests : TestBase -{ - public BasicOpsTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] public async Task PingOnce() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var duration = await db.PingAsync().ForAwait(); @@ -34,14 +22,14 @@ public async Task PingOnce() [Fact(Skip = "This needs some CI love, it's not a scenario we care about too much but noisy atm.")] public async Task RapidDispose() { - using var primary = Create(); + await using var primary = Create(); var db = primary.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); for (int i = 0; i < 10; i++) { - using var secondary = Create(fail: true, shared: false); + await using var secondary = Create(fail: true, shared: false); secondary.GetDatabase().StringIncrement(key, flags: CommandFlags.FireAndForget); } // Give it a moment to get through the pipe...they were fire and forget @@ -52,7 +40,7 @@ public async Task RapidDispose() [Fact] public async Task PingMany() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var tasks = new Task[100]; for (int i = 0; i < tasks.Length; i++) @@ -65,9 +53,9 @@ public async Task PingMany() } [Fact] - public void GetWithNullKey() + public async Task GetWithNullKey() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); const string? key = null; var ex = Assert.Throws(() => db.StringGet(key)); @@ -75,9 +63,9 @@ public void GetWithNullKey() } [Fact] - public void SetWithNullKey() + public async Task SetWithNullKey() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); const string? key = null, value = "abc"; var ex = Assert.Throws(() => db.StringSet(key!, value)); @@ -85,9 +73,9 @@ public void SetWithNullKey() } [Fact] - public void SetWithNullValue() + public async Task SetWithNullValue() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); string key = Me(); const string? value = null; @@ -103,9 +91,9 @@ public void SetWithNullValue() } [Fact] - public void SetWithDefaultValue() + public async Task SetWithDefaultValue() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); string key = Me(); var value = default(RedisValue); // this is kinda 0... ish @@ -121,9 +109,9 @@ public void SetWithDefaultValue() } [Fact] - public void SetWithZeroValue() + public async Task SetWithZeroValue() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); string key = Me(); const long value = 0; @@ -141,7 +129,7 @@ public void SetWithZeroValue() [Fact] public async Task GetSetAsync() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -164,9 +152,9 @@ public async Task GetSetAsync() } [Fact] - public void GetSetSync() + public async Task GetSetSync() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -193,7 +181,7 @@ public void GetSetSync() [InlineData(true, false)] public async Task GetWithExpiry(bool exists, bool hasExpiry) { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -229,7 +217,7 @@ public async Task GetWithExpiry(bool exists, bool hasExpiry) [Fact] public async Task GetWithExpiryWrongTypeAsync() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); _ = db.KeyDeleteAsync(key); @@ -250,12 +238,12 @@ public async Task GetWithExpiryWrongTypeAsync() } [Fact] - public void GetWithExpiryWrongTypeSync() + public async Task GetWithExpiryWrongTypeSync() { RedisKey key = Me(); - var ex = Assert.Throws(() => + var ex = await Assert.ThrowsAsync(async () => { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); db.KeyDelete(key, CommandFlags.FireAndForget); db.SetAdd(key, "abc", CommandFlags.FireAndForget); @@ -269,7 +257,7 @@ public void GetWithExpiryWrongTypeSync() public async Task TestSevered() { SetExpectedAmbientFailureCount(2); - using var conn = Create(allowAdmin: true, shared: false); + await using var conn = Create(allowAdmin: true, shared: false); var db = conn.GetDatabase(); string key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -289,7 +277,7 @@ public async Task TestSevered() [Fact] public async Task IncrAsync() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -315,11 +303,12 @@ public async Task IncrAsync() } [Fact] - public void IncrSync() + public async Task IncrSync() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); + Log(key); db.KeyDelete(key, CommandFlags.FireAndForget); var nix = db.KeyExists(key); var a = db.StringGet(key); @@ -343,9 +332,9 @@ public void IncrSync() } [Fact] - public void IncrDifferentSizes() + public async Task IncrDifferentSizes() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -376,21 +365,21 @@ private static void Incr(IDatabase database, RedisKey key, int delta, ref int to } [Fact] - public void ShouldUseSharedMuxer() + public async Task ShouldUseSharedMuxer() { Log($"Shared: {SharedFixtureAvailable}"); if (SharedFixtureAvailable) { - using var a = Create(); + await using var a = Create(); Assert.IsNotType(a); - using var b = Create(); + await using var b = Create(); Assert.Same(a, b); } else { - using var a = Create(); + await using var a = Create(); Assert.IsType(a); - using var b = Create(); + await using var b = Create(); Assert.NotSame(a, b); } } @@ -398,7 +387,7 @@ public void ShouldUseSharedMuxer() [Fact] public async Task Delete() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); _ = db.StringSetAsync(key, "Heyyyyy"); @@ -413,7 +402,7 @@ public async Task Delete() [Fact] public async Task DeleteAsync() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); _ = db.StringSetAsync(key, "Heyyyyy"); @@ -428,7 +417,7 @@ public async Task DeleteAsync() [Fact] public async Task DeleteMany() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key1 = Me(); var key2 = Me() + "2"; @@ -436,7 +425,7 @@ public async Task DeleteMany() _ = db.StringSetAsync(key1, "Heyyyyy"); _ = db.StringSetAsync(key2, "Heyyyyy"); // key 3 not set - var ku1 = db.KeyDelete(new RedisKey[] { key1, key2, key3 }); + var ku1 = db.KeyDelete([key1, key2, key3]); var ke1 = db.KeyExistsAsync(key1).ForAwait(); var ke2 = db.KeyExistsAsync(key2).ForAwait(); Assert.Equal(2, ku1); @@ -447,7 +436,7 @@ public async Task DeleteMany() [Fact] public async Task DeleteManyAsync() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key1 = Me(); var key2 = Me() + "2"; @@ -455,7 +444,7 @@ public async Task DeleteManyAsync() _ = db.StringSetAsync(key1, "Heyyyyy"); _ = db.StringSetAsync(key2, "Heyyyyy"); // key 3 not set - var ku1 = db.KeyDeleteAsync(new RedisKey[] { key1, key2, key3 }).ForAwait(); + var ku1 = db.KeyDeleteAsync([key1, key2, key3]).ForAwait(); var ke1 = db.KeyExistsAsync(key1).ForAwait(); var ke2 = db.KeyExistsAsync(key2).ForAwait(); Assert.Equal(2, await ku1); @@ -464,10 +453,10 @@ public async Task DeleteManyAsync() } [Fact] - public void WrappedDatabasePrefixIntegration() + public async Task WrappedDatabasePrefixIntegration() { var key = Me(); - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase().WithKeyPrefix("abc"); db.KeyDelete(key, CommandFlags.FireAndForget); db.StringIncrement(key, flags: CommandFlags.FireAndForget); @@ -479,9 +468,9 @@ public void WrappedDatabasePrefixIntegration() } [Fact] - public void TransactionSync() + public async Task TransactionSync() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); diff --git a/tests/StackExchange.Redis.Tests/BatchTests.cs b/tests/StackExchange.Redis.Tests/BatchTests.cs index 6783360e5..2605b172d 100644 --- a/tests/StackExchange.Redis.Tests/BatchTests.cs +++ b/tests/StackExchange.Redis.Tests/BatchTests.cs @@ -2,41 +2,37 @@ using System.Collections.Generic; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class BatchTests : TestBase +public class BatchTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public BatchTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void TestBatchNotSent() + public async Task TestBatchNotSent() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); - db.KeyDeleteAsync(key); - db.StringSetAsync(key, "batch-not-sent"); + _ = db.KeyDeleteAsync(key); + _ = db.StringSetAsync(key, "batch-not-sent"); var batch = db.CreateBatch(); - batch.KeyDeleteAsync(key); - batch.SetAddAsync(key, "a"); - batch.SetAddAsync(key, "b"); - batch.SetAddAsync(key, "c"); + _ = batch.KeyDeleteAsync(key); + _ = batch.SetAddAsync(key, "a"); + _ = batch.SetAddAsync(key, "b"); + _ = batch.SetAddAsync(key, "c"); Assert.Equal("batch-not-sent", db.StringGet(key)); } [Fact] - public void TestBatchSent() + public async Task TestBatchSent() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); - db.KeyDeleteAsync(key); - db.StringSetAsync(key, "batch-sent"); + _ = db.KeyDeleteAsync(key); + _ = db.StringSetAsync(key, "batch-sent"); var tasks = new List(); var batch = db.CreateBatch(); tasks.Add(batch.KeyDeleteAsync(key)); @@ -47,7 +43,7 @@ public void TestBatchSent() var result = db.SetMembersAsync(key); tasks.Add(result); - Task.WhenAll(tasks.ToArray()); + await Task.WhenAll(tasks.ToArray()); var arr = result.Result; Array.Sort(arr, (x, y) => string.Compare(x, y)); diff --git a/tests/StackExchange.Redis.Tests/BitTests.cs b/tests/StackExchange.Redis.Tests/BitTests.cs index c6b12c02b..b4c032366 100644 --- a/tests/StackExchange.Redis.Tests/BitTests.cs +++ b/tests/StackExchange.Redis.Tests/BitTests.cs @@ -1,18 +1,15 @@ -using Xunit; -using Xunit.Abstractions; +using System.Threading.Tasks; +using Xunit; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class BitTests : TestBase +public class BitTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public BitTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void BasicOps() + public async Task BasicOps() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); diff --git a/tests/StackExchange.Redis.Tests/BoxUnboxTests.cs b/tests/StackExchange.Redis.Tests/BoxUnboxTests.cs index 1ffb972a2..033a24839 100644 --- a/tests/StackExchange.Redis.Tests/BoxUnboxTests.cs +++ b/tests/StackExchange.Redis.Tests/BoxUnboxTests.cs @@ -55,82 +55,82 @@ public static IEnumerable RoundTripValues => new[] { new object[] { RedisValue.Null }, - new object[] { RedisValue.EmptyString }, - new object[] { (RedisValue)0L }, - new object[] { (RedisValue)1L }, - new object[] { (RedisValue)18L }, - new object[] { (RedisValue)19L }, - new object[] { (RedisValue)20L }, - new object[] { (RedisValue)21L }, - new object[] { (RedisValue)22L }, - new object[] { (RedisValue)(-1L) }, - new object[] { (RedisValue)0 }, - new object[] { (RedisValue)1 }, - new object[] { (RedisValue)18 }, - new object[] { (RedisValue)19 }, - new object[] { (RedisValue)20 }, - new object[] { (RedisValue)21 }, - new object[] { (RedisValue)22 }, - new object[] { (RedisValue)(-1) }, - new object[] { (RedisValue)0F }, - new object[] { (RedisValue)1F }, - new object[] { (RedisValue)(-1F) }, - new object[] { (RedisValue)0D }, - new object[] { (RedisValue)1D }, - new object[] { (RedisValue)(-1D) }, - new object[] { (RedisValue)float.PositiveInfinity }, - new object[] { (RedisValue)float.NegativeInfinity }, - new object[] { (RedisValue)float.NaN }, - new object[] { (RedisValue)double.PositiveInfinity }, - new object[] { (RedisValue)double.NegativeInfinity }, - new object[] { (RedisValue)double.NaN }, - new object[] { (RedisValue)true }, - new object[] { (RedisValue)false }, - new object[] { (RedisValue)(string?)null }, - new object[] { (RedisValue)"abc" }, - new object[] { (RedisValue)s_abc }, - new object[] { (RedisValue)new Memory(s_abc) }, - new object[] { (RedisValue)new ReadOnlyMemory(s_abc) }, + [RedisValue.EmptyString], + [(RedisValue)0L], + [(RedisValue)1L], + [(RedisValue)18L], + [(RedisValue)19L], + [(RedisValue)20L], + [(RedisValue)21L], + [(RedisValue)22L], + [(RedisValue)(-1L)], + [(RedisValue)0], + [(RedisValue)1], + [(RedisValue)18], + [(RedisValue)19], + [(RedisValue)20], + [(RedisValue)21], + [(RedisValue)22], + [(RedisValue)(-1)], + [(RedisValue)0F], + [(RedisValue)1F], + [(RedisValue)(-1F)], + [(RedisValue)0D], + [(RedisValue)1D], + [(RedisValue)(-1D)], + [(RedisValue)float.PositiveInfinity], + [(RedisValue)float.NegativeInfinity], + [(RedisValue)float.NaN], + [(RedisValue)double.PositiveInfinity], + [(RedisValue)double.NegativeInfinity], + [(RedisValue)double.NaN], + [(RedisValue)true], + [(RedisValue)false], + [(RedisValue)(string?)null], + [(RedisValue)"abc"], + [(RedisValue)s_abc], + [(RedisValue)new Memory(s_abc)], + [(RedisValue)new ReadOnlyMemory(s_abc)], }; public static IEnumerable UnboxValues => new[] { new object?[] { null, RedisValue.Null }, - new object[] { "", RedisValue.EmptyString }, - new object[] { 0, (RedisValue)0 }, - new object[] { 1, (RedisValue)1 }, - new object[] { 18, (RedisValue)18 }, - new object[] { 19, (RedisValue)19 }, - new object[] { 20, (RedisValue)20 }, - new object[] { 21, (RedisValue)21 }, - new object[] { 22, (RedisValue)22 }, - new object[] { -1, (RedisValue)(-1) }, - new object[] { 18L, (RedisValue)18 }, - new object[] { 19L, (RedisValue)19 }, - new object[] { 20L, (RedisValue)20 }, - new object[] { 21L, (RedisValue)21 }, - new object[] { 22L, (RedisValue)22 }, - new object[] { -1L, (RedisValue)(-1) }, - new object[] { 0F, (RedisValue)0 }, - new object[] { 1F, (RedisValue)1 }, - new object[] { -1F, (RedisValue)(-1) }, - new object[] { 0D, (RedisValue)0 }, - new object[] { 1D, (RedisValue)1 }, - new object[] { -1D, (RedisValue)(-1) }, - new object[] { float.PositiveInfinity, (RedisValue)double.PositiveInfinity }, - new object[] { float.NegativeInfinity, (RedisValue)double.NegativeInfinity }, - new object[] { float.NaN, (RedisValue)double.NaN }, - new object[] { double.PositiveInfinity, (RedisValue)double.PositiveInfinity }, - new object[] { double.NegativeInfinity, (RedisValue)double.NegativeInfinity }, - new object[] { double.NaN, (RedisValue)double.NaN }, - new object[] { true, (RedisValue)true }, - new object[] { false, (RedisValue)false }, - new object[] { "abc", (RedisValue)"abc" }, - new object[] { s_abc, (RedisValue)s_abc }, - new object[] { new Memory(s_abc), (RedisValue)s_abc }, - new object[] { new ReadOnlyMemory(s_abc), (RedisValue)s_abc }, - new object[] { (RedisValue)1234, (RedisValue)1234 }, + ["", RedisValue.EmptyString], + [0, (RedisValue)0], + [1, (RedisValue)1], + [18, (RedisValue)18], + [19, (RedisValue)19], + [20, (RedisValue)20], + [21, (RedisValue)21], + [22, (RedisValue)22], + [-1, (RedisValue)(-1)], + [18L, (RedisValue)18], + [19L, (RedisValue)19], + [20L, (RedisValue)20], + [21L, (RedisValue)21], + [22L, (RedisValue)22], + [-1L, (RedisValue)(-1)], + [0F, (RedisValue)0], + [1F, (RedisValue)1], + [-1F, (RedisValue)(-1)], + [0D, (RedisValue)0], + [1D, (RedisValue)1], + [-1D, (RedisValue)(-1)], + [float.PositiveInfinity, (RedisValue)double.PositiveInfinity], + [float.NegativeInfinity, (RedisValue)double.NegativeInfinity], + [float.NaN, (RedisValue)double.NaN], + [double.PositiveInfinity, (RedisValue)double.PositiveInfinity], + [double.NegativeInfinity, (RedisValue)double.NegativeInfinity], + [double.NaN, (RedisValue)double.NaN], + [true, (RedisValue)true], + [false, (RedisValue)false], + ["abc", (RedisValue)"abc"], + [s_abc, (RedisValue)s_abc], + [new Memory(s_abc), (RedisValue)s_abc], + [new ReadOnlyMemory(s_abc), (RedisValue)s_abc], + [(RedisValue)1234, (RedisValue)1234], }; public static IEnumerable InternedValues() diff --git a/tests/StackExchange.Redis.Tests/CancellationTests.cs b/tests/StackExchange.Redis.Tests/CancellationTests.cs index 58e33e3de..ba23fb6ed 100644 --- a/tests/StackExchange.Redis.Tests/CancellationTests.cs +++ b/tests/StackExchange.Redis.Tests/CancellationTests.cs @@ -3,71 +3,22 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -#if !NET6_0_OR_GREATER -internal static class TaskExtensions +[Collection(NonParallelCollection.Name)] +public class CancellationTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - // suboptimal polyfill version of the .NET 6+ API; I'm not recommending this for production use, - // but it's good enough for tests - public static Task WaitAsync(this Task task, CancellationToken cancellationToken) - { - if (task.IsCompleted || !cancellationToken.CanBeCanceled) return task; - return Wrap(task, cancellationToken); - - static async Task Wrap(Task task, CancellationToken cancellationToken) - { - var tcs = new TaskCompletionSource(); - using var reg = cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)); - _ = task.ContinueWith(t => - { - if (t.IsCanceled) tcs.TrySetCanceled(); - else if (t.IsFaulted) tcs.TrySetException(t.Exception!); - else tcs.TrySetResult(t.Result); - }); - return await tcs.Task; - } - } - - public static Task WaitAsync(this Task task, TimeSpan timeout) - { - if (task.IsCompleted) return task; - return Wrap(task, timeout); - - static async Task Wrap(Task task, TimeSpan timeout) - { - Task other = Task.Delay(timeout); - var first = await Task.WhenAny(task, other); - if (ReferenceEquals(first, other)) - { - throw new TimeoutException(); - } - return await task; - } - } -} -#endif - -[Collection(SharedConnectionFixture.Key)] -public class CancellationTests : TestBase -{ - public CancellationTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] public async Task WithCancellation_CancelledToken_ThrowsOperationCanceledException() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); using var cts = new CancellationTokenSource(); cts.Cancel(); // Cancel immediately - await Assert.ThrowsAnyAsync(async () => - { - await db.StringSetAsync(Me(), "value").WaitAsync(cts.Token); - }); + await Assert.ThrowsAnyAsync(async () => await db.StringSetAsync(Me(), "value").WaitAsync(cts.Token)); } private IInternalConnectionMultiplexer Create() => Create(syncTimeout: 10_000); @@ -75,7 +26,7 @@ await Assert.ThrowsAnyAsync(async () => [Fact] public async Task WithCancellation_ValidToken_OperationSucceeds() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); using var cts = new CancellationTokenSource(); @@ -87,15 +38,12 @@ public async Task WithCancellation_ValidToken_OperationSucceeds() Assert.Equal("value", result); } - private void Pause(IDatabase db) - { - db.Execute("client", new object[] { "pause", ConnectionPauseMilliseconds }, CommandFlags.FireAndForget); - } + private static void Pause(IDatabase db) => db.Execute("client", ["pause", ConnectionPauseMilliseconds], CommandFlags.FireAndForget); [Fact] public async Task WithTimeout_ShortTimeout_Async_ThrowsOperationCanceledException() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var watch = Stopwatch.StartNew(); @@ -122,7 +70,7 @@ public async Task WithTimeout_ShortTimeout_Async_ThrowsOperationCanceledExceptio [Fact] public async Task WithoutCancellation_OperationsWorkNormally() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); // No cancellation - should work normally @@ -171,7 +119,7 @@ private static CancellationTokenSource CreateCts(CancelStrategy strategy) [InlineData(CancelStrategy.Manual)] public async Task CancellationDuringOperation_Async_CancelsGracefully(CancelStrategy strategy) { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var watch = Stopwatch.StartNew(); diff --git a/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs b/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs index 528944429..529b29a02 100644 --- a/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs +++ b/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs @@ -3,14 +3,11 @@ using System.Net.Security; using System.Security.Cryptography.X509Certificates; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class CertValidationTests : TestBase +public class CertValidationTests(ITestOutputHelper output) : TestBase(output) { - public CertValidationTests(ITestOutputHelper output) : base(output) { } - [Fact] public void CheckIssuerValidity() { diff --git a/tests/StackExchange.Redis.Tests/ClientKillTests.cs b/tests/StackExchange.Redis.Tests/ClientKillTests.cs index eaf91e073..f10f69ef6 100644 --- a/tests/StackExchange.Redis.Tests/ClientKillTests.cs +++ b/tests/StackExchange.Redis.Tests/ClientKillTests.cs @@ -1,44 +1,37 @@ using System.Collections.Generic; using System.Net; using System.Threading; +using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [RunPerProtocol] -public class ClientKillTests : TestBase +public class ClientKillTests(ITestOutputHelper output) : TestBase(output) { - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public ClientKillTests(ITestOutputHelper output) : base(output) { } - [Fact] - public void ClientKill() + public async Task ClientKill() { - var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); - SetExpectedAmbientFailureCount(-1); - using var otherConnection = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); + await using var otherConnection = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast, require: RedisFeatures.v7_4_0_rc1); var id = otherConnection.GetDatabase().Execute(RedisCommand.CLIENT.ToString(), RedisLiterals.ID); - using var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); + await using var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); var server = conn.GetServer(conn.GetEndPoints()[0]); long result = server.ClientKill(id.AsInt64(), ClientType.Normal, null, true); Assert.Equal(1, result); } [Fact] - public void ClientKillWithMaxAge() + public async Task ClientKillWithMaxAge() { - var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); - SetExpectedAmbientFailureCount(-1); - using var otherConnection = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); + await using var otherConnection = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast, require: RedisFeatures.v7_4_0_rc1); var id = otherConnection.GetDatabase().Execute(RedisCommand.CLIENT.ToString(), RedisLiterals.ID); - Thread.Sleep(1000); + await Task.Delay(1000); - using var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); + await using var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); var server = conn.GetServer(conn.GetEndPoints()[0]); var filter = new ClientKillFilter().WithId(id.AsInt64()).WithMaxAgeInSeconds(1).WithSkipMe(true); long result = server.ClientKill(filter, CommandFlags.DemandMaster); @@ -57,10 +50,10 @@ public void TestClientKillMessageWithAllArguments() long maxAge = 102; var filter = new ClientKillFilter().WithId(id).WithClientType(type).WithUsername(userName).WithEndpoint(endpoint).WithServerEndpoint(serverEndpoint).WithSkipMe(skipMe).WithMaxAgeInSeconds(maxAge); - List expected = new List() - { + List expected = + [ "KILL", "ID", "101", "TYPE", "normal", "USERNAME", "user1", "ADDR", "127.0.0.1:1234", "LADDR", "198.0.0.1:6379", "SKIPME", "yes", "MAXAGE", "102", - }; + ]; Assert.Equal(expected, filter.ToList(true)); } } diff --git a/tests/StackExchange.Redis.Tests/ClusterShardedTests.cs b/tests/StackExchange.Redis.Tests/ClusterShardedTests.cs new file mode 100644 index 000000000..dd57483b9 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ClusterShardedTests.cs @@ -0,0 +1,177 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace StackExchange.Redis.Tests; + +[RunPerProtocol] +[Collection(NonParallelCollection.Name)] +public class ClusterShardedTests(ITestOutputHelper output) : TestBase(output) +{ + protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; + + [Fact] + public async Task TestShardedPubsubSubscriberAgainstReconnects() + { + Skip.UnlessLongRunning(); + var channel = RedisChannel.Sharded(Me()); + await using var conn = Create(allowAdmin: true, keepAlive: 1, connectTimeout: 3000, shared: false, require: RedisFeatures.v7_0_0_rc1); + Assert.True(conn.IsConnected); + var db = conn.GetDatabase(); + Assert.Equal(0, await db.PublishAsync(channel, "noClientReceivesThis")); + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + + var pubsub = conn.GetSubscriber(); + List<(RedisChannel, RedisValue)> received = []; + var queue = await pubsub.SubscribeAsync(channel); + _ = Task.Run(async () => + { + // use queue API to have control over order + await foreach (var item in queue) + { + lock (received) + { + if (item.Channel.IsSharded && item.Channel == channel) received.Add((item.Channel, item.Message)); + } + } + }); + Assert.Equal(1, conn.GetSubscriptionsCount()); + + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + await db.PingAsync(); + + for (int i = 0; i < 5; i++) + { + // check we get a hit + Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); + } + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + + // this is endpoint at index 1 which has the hashslot for "testShardChannel" + var server = conn.GetServer(conn.GetEndPoints()[1]); + server.SimulateConnectionFailure(SimulatedFailureType.All); + SetExpectedAmbientFailureCount(2); + + await Task.Delay(4000); + for (int i = 0; i < 5; i++) + { + // check we get a hit + Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); + } + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + + Assert.Equal(1, conn.GetSubscriptionsCount()); + Assert.Equal(10, received.Count); + ClearAmbientFailures(); + } + + [Fact] + public async Task TestShardedPubsubSubscriberAgainsHashSlotMigration() + { + Skip.UnlessLongRunning(); + var channel = RedisChannel.Sharded(Me()); + await using var conn = Create(allowAdmin: true, keepAlive: 1, connectTimeout: 3000, shared: false, require: RedisFeatures.v7_0_0_rc1); + Assert.True(conn.IsConnected); + var db = conn.GetDatabase(); + Assert.Equal(0, await db.PublishAsync(channel, "noClientReceivesThis")); + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + + var pubsub = conn.GetSubscriber(); + List<(RedisChannel, RedisValue)> received = []; + var queue = await pubsub.SubscribeAsync(channel); + _ = Task.Run(async () => + { + // use queue API to have control over order + await foreach (var item in queue) + { + lock (received) + { + if (item.Channel.IsSharded && item.Channel == channel) received.Add((item.Channel, item.Message)); + } + } + }); + Assert.Equal(1, conn.GetSubscriptionsCount()); + + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + await db.PingAsync(); + + for (int i = 0; i < 5; i++) + { + // check we get a hit + Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); + } + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + + // lets migrate the slot for "testShardChannel" to another node + await DoHashSlotMigrationAsync(); + + await Task.Delay(4000); + for (int i = 0; i < 5; i++) + { + // check we get a hit + Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); + } + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + + Assert.Equal(1, conn.GetSubscriptionsCount()); + Assert.Equal(10, received.Count); + await RollbackHashSlotMigrationAsync(); + ClearAmbientFailures(); + } + + private Task DoHashSlotMigrationAsync() => MigrateSlotForTestShardChannelAsync(false); + private Task RollbackHashSlotMigrationAsync() => MigrateSlotForTestShardChannelAsync(true); + + private async Task MigrateSlotForTestShardChannelAsync(bool rollback) + { + int hashSlotForTestShardChannel = 7177; + await using var conn = Create(allowAdmin: true, keepAlive: 1, connectTimeout: 5000, shared: false); + var servers = conn.GetServers(); + IServer? serverWithPort7000 = null; + IServer? serverWithPort7001 = null; + + string nodeIdForPort7000 = "780813af558af81518e58e495d63b6e248e80adf"; + string nodeIdForPort7001 = "ea828c6074663c8bd4e705d3e3024d9d1721ef3b"; + foreach (var server in servers) + { + string id = server.Execute("CLUSTER", "MYID").ToString(); + if (id == nodeIdForPort7000) + { + serverWithPort7000 = server; + } + if (id == nodeIdForPort7001) + { + serverWithPort7001 = server; + } + } + + IServer fromServer, toServer; + string fromNode, toNode; + if (rollback) + { + fromServer = serverWithPort7000!; + fromNode = nodeIdForPort7000; + toServer = serverWithPort7001!; + toNode = nodeIdForPort7001; + } + else + { + fromServer = serverWithPort7001!; + fromNode = nodeIdForPort7001; + toServer = serverWithPort7000!; + toNode = nodeIdForPort7000; + } + + try + { + Assert.Equal("OK", toServer.Execute("CLUSTER", "SETSLOT", hashSlotForTestShardChannel, "IMPORTING", fromNode).ToString()); + Assert.Equal("OK", fromServer.Execute("CLUSTER", "SETSLOT", hashSlotForTestShardChannel, "MIGRATING", toNode).ToString()); + Assert.Equal("OK", toServer.Execute("CLUSTER", "SETSLOT", hashSlotForTestShardChannel, "NODE", toNode).ToString()); + Assert.Equal("OK", fromServer!.Execute("CLUSTER", "SETSLOT", hashSlotForTestShardChannel, "NODE", toNode).ToString()); + } + catch (RedisServerException ex) when (ex.Message == "ERR I'm already the owner of hash slot 7177") + { + Log("Slot already migrated."); + } + } +} diff --git a/tests/StackExchange.Redis.Tests/ClusterTests.cs b/tests/StackExchange.Redis.Tests/ClusterTests.cs index b1f9d01cd..6fa963b8a 100644 --- a/tests/StackExchange.Redis.Tests/ClusterTests.cs +++ b/tests/StackExchange.Redis.Tests/ClusterTests.cs @@ -7,26 +7,20 @@ using System.Threading.Tasks; using StackExchange.Redis.Profiling; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class ClusterTests : TestBase +public class ClusterTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public ClusterTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) - { - } - protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; [Fact] - public void ExportConfiguration() + public async Task ExportConfiguration() { if (File.Exists("cluster.zip")) File.Delete("cluster.zip"); Assert.False(File.Exists("cluster.zip")); - using (var conn = Create(allowAdmin: true)) + await using (var conn = Create(allowAdmin: true)) using (var file = File.Create("cluster.zip")) { conn.ExportConfiguration(file); @@ -35,11 +29,11 @@ public void ExportConfiguration() } [Fact] - public void ConnectUsesSingleSocket() + public async Task ConnectUsesSingleSocket() { for (int i = 0; i < 5; i++) { - using var conn = Create(failMessage: i + ": ", log: Writer); + await using var conn = Create(failMessage: i + ": ", log: Writer); foreach (var ep in conn.GetEndPoints()) { @@ -53,15 +47,15 @@ public void ConnectUsesSingleSocket() var srv = conn.GetServer(ep); var counters = srv.GetCounters(); Assert.Equal(1, counters.Interactive.SocketCount); - Assert.Equal(Context.IsResp3 ? 0 : 1, counters.Subscription.SocketCount); + Assert.Equal(TestContext.Current.IsResp3() ? 0 : 1, counters.Subscription.SocketCount); } } } [Fact] - public void CanGetTotalStats() + public async Task CanGetTotalStats() { - using var conn = Create(); + await using var conn = Create(); var counters = conn.GetCounters(); Log(counters.ToString()); @@ -78,9 +72,9 @@ private void PrintEndpoints(EndPoint[] endpoints) } [Fact] - public void Connect() + public async Task Connect() { - using var conn = Create(log: Writer); + await using var conn = Create(log: Writer); var expectedPorts = new HashSet(Enumerable.Range(TestConfig.Current.ClusterStartPort, TestConfig.Current.ClusterServerCount)); var endpoints = conn.GetEndPoints(); @@ -129,9 +123,9 @@ public void Connect() } [Fact] - public void TestIdentity() + public async Task TestIdentity() { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Guid.NewGuid().ToByteArray(); var ep = conn.GetDatabase().IdentifyEndpoint(key); @@ -140,12 +134,12 @@ public void TestIdentity() } [Fact] - public void IntentionalWrongServer() + public async Task IntentionalWrongServer() { static string? StringGet(IServer server, RedisKey key, CommandFlags flags = CommandFlags.None) - => (string?)server.Execute("GET", new object[] { key }, flags); + => (string?)server.Execute("GET", [key], flags); - using var conn = Create(); + await using var conn = Create(); var endpoints = conn.GetEndPoints(); var servers = endpoints.Select(e => conn.GetServer(e)).ToList(); @@ -155,13 +149,13 @@ public void IntentionalWrongServer() var db = conn.GetDatabase(); db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, value, flags: CommandFlags.FireAndForget); - servers[0].Ping(); + await servers[0].PingAsync(); var config = servers[0].ClusterConfiguration; Assert.NotNull(config); int slot = conn.HashSlot(key); var rightPrimaryNode = config.GetBySlot(key); Assert.NotNull(rightPrimaryNode); - Log("Right Primary: {0} {1}", rightPrimaryNode.EndPoint, rightPrimaryNode.NodeId); + Log($"Right Primary: {rightPrimaryNode.EndPoint} {rightPrimaryNode.NodeId}"); Assert.NotNull(rightPrimaryNode.EndPoint); string? a = StringGet(conn.GetServer(rightPrimaryNode.EndPoint), key); @@ -169,7 +163,7 @@ public void IntentionalWrongServer() var node = config.Nodes.FirstOrDefault(x => !x.IsReplica && x.NodeId != rightPrimaryNode.NodeId); Assert.NotNull(node); - Log("Using Primary: {0}", node.EndPoint, node.NodeId); + Log($"Using Primary: {node.EndPoint} {node.NodeId}"); { Assert.NotNull(node.EndPoint); string? b = StringGet(conn.GetServer(node.EndPoint), key); @@ -200,15 +194,15 @@ public void IntentionalWrongServer() } [Fact] - public void TransactionWithMultiServerKeys() + public async Task TransactionWithMultiServerKeys() { - using var conn = Create(); - var ex = Assert.Throws(() => + await using var conn = Create(); + var ex = await Assert.ThrowsAsync(async () => { // connect var cluster = conn.GetDatabase(); var anyServer = conn.GetServer(conn.GetEndPoints()[0]); - anyServer.Ping(); + await anyServer.PingAsync(); Assert.Equal(ServerType.Cluster, anyServer.ServerType); var config = anyServer.ClusterConfiguration; Assert.NotNull(config); @@ -223,7 +217,7 @@ public void TransactionWithMultiServerKeys() y = Guid.NewGuid().ToString(); } while (--abort > 0 && config.GetBySlot(y) == xNode); - if (abort == 0) Skip.Inconclusive("failed to find a different node to use"); + if (abort == 0) Assert.Skip("failed to find a different node to use"); var yNode = config.GetBySlot(y); Assert.NotNull(yNode); Log("x={0}, served by {1}", x, xNode.NodeId); @@ -258,15 +252,15 @@ public void TransactionWithMultiServerKeys() } [Fact] - public void TransactionWithSameServerKeys() + public async Task TransactionWithSameServerKeys() { - using var conn = Create(); - var ex = Assert.Throws(() => + await using var conn = Create(); + var ex = await Assert.ThrowsAsync(async () => { // connect var cluster = conn.GetDatabase(); var anyServer = conn.GetServer(conn.GetEndPoints()[0]); - anyServer.Ping(); + await anyServer.PingAsync(); var config = anyServer.ClusterConfiguration; Assert.NotNull(config); @@ -279,7 +273,7 @@ public void TransactionWithSameServerKeys() y = Guid.NewGuid().ToString(); } while (--abort > 0 && config.GetBySlot(y) != xNode); - if (abort == 0) Skip.Inconclusive("failed to find a key with the same node to use"); + Assert.SkipWhen(abort == 0, "failed to find a key with the same node to use"); var yNode = config.GetBySlot(y); Assert.NotNull(xNode); Log("x={0}, served by {1}", x, xNode.NodeId); @@ -315,14 +309,14 @@ public void TransactionWithSameServerKeys() } [Fact] - public void TransactionWithSameSlotKeys() + public async Task TransactionWithSameSlotKeys() { - using var conn = Create(); + await using var conn = Create(); // connect var cluster = conn.GetDatabase(); var anyServer = conn.GetServer(conn.GetEndPoints()[0]); - anyServer.Ping(); + await anyServer.PingAsync(); var config = anyServer.ClusterConfiguration; Assert.NotNull(config); @@ -361,27 +355,26 @@ public void TransactionWithSameSlotKeys() Assert.True(cluster.Wait(existsY), "y exists"); } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "Because.")] - [Theory(Skip = "FlushAllDatabases")] + [Theory] [InlineData(null, 10)] [InlineData(null, 100)] [InlineData("abc", 10)] [InlineData("abc", 100)] - public void Keys(string? pattern, int pageSize) + public async Task Keys(string? pattern, int pageSize) { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); - _ = conn.GetDatabase(); + var dbId = TestConfig.GetDedicatedDB(conn); var server = conn.GetEndPoints().Select(x => conn.GetServer(x)).First(x => !x.IsReplica); - server.FlushAllDatabases(); + await server.FlushDatabaseAsync(dbId); try { - Assert.False(server.Keys(pattern: pattern, pageSize: pageSize).Any()); - Log("Complete: '{0}' / {1}", pattern, pageSize); + Assert.False(server.Keys(dbId, pattern: pattern, pageSize: pageSize).Any()); + Log($"Complete: '{pattern}' / {pageSize}"); } catch { - Log("Failed: '{0}' / {1}", pattern, pageSize); + Log($"Failed: '{pattern}' / {pageSize}"); throw; } } @@ -411,17 +404,17 @@ public void Keys(string? pattern, int pageSize) [InlineData("foo{bar}{zap}", 5061)] [InlineData("bar", 5061)] - public void HashSlots(string key, int slot) + public async Task HashSlots(string key, int slot) { - using var conn = Create(connectTimeout: 5000); + await using var conn = Create(connectTimeout: 5000); Assert.Equal(slot, conn.HashSlot(key)); } [Fact] - public void SScan() + public async Task SScan() { - using var conn = Create(); + await using var conn = Create(); RedisKey key = "a"; var db = conn.GetDatabase(); @@ -441,9 +434,9 @@ public void SScan() } [Fact] - public void GetConfig() + public async Task GetConfig() { - using var conn = Create(allowAdmin: true, log: Writer); + await using var conn = Create(allowAdmin: true, log: Writer); var endpoints = conn.GetEndPoints(); var server = conn.GetServer(endpoints[0]); @@ -467,9 +460,9 @@ public void GetConfig() [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "Because.")] [Fact(Skip = "FlushAllDatabases")] - public void AccessRandomKeys() + public async Task AccessRandomKeys() { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); var cluster = conn.GetDatabase(); int slotMovedCount = 0; @@ -488,8 +481,8 @@ public void AccessRandomKeys() { if (!server.IsReplica) { - server.Ping(); - server.FlushAllDatabases(); + await server.PingAsync(); + await server.FlushAllDatabasesAsync(); } } @@ -542,12 +535,12 @@ public void AccessRandomKeys() [InlineData(CommandFlags.DemandReplica, true)] [InlineData(CommandFlags.PreferMaster, false)] [InlineData(CommandFlags.PreferReplica, true)] - public void GetFromRightNodeBasedOnFlags(CommandFlags flags, bool isReplica) + public async Task GetFromRightNodeBasedOnFlags(CommandFlags flags, bool isReplica) { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); var db = conn.GetDatabase(); - for (int i = 0; i < 1000; i++) + for (int i = 0; i < 500; i++) { var key = Guid.NewGuid().ToString(); var endpoint = db.IdentifyEndpoint(key, flags); @@ -560,9 +553,9 @@ public void GetFromRightNodeBasedOnFlags(CommandFlags flags, bool isReplica) private static string Describe(EndPoint endpoint) => endpoint?.ToString() ?? "(unknown)"; [Fact] - public void SimpleProfiling() + public async Task SimpleProfiling() { - using var conn = Create(log: Writer); + await using var conn = Create(log: Writer); var profiler = new ProfilingSession(); var key = Me(); @@ -591,11 +584,11 @@ public void SimpleProfiling() } [Fact] - public void MultiKeyQueryFails() + public async Task MultiKeyQueryFails() { var keys = InventKeys(); // note the rules expected of this data are enforced in GroupedQueriesWork - using var conn = Create(); + await using var conn = Create(); var ex = Assert.Throws(() => conn.GetDatabase(0).StringGet(keys)); Assert.Contains("Multi-key operations must involve a single slot", ex.Message); @@ -623,13 +616,13 @@ string InventString() } [Fact] - public void GroupedQueriesWork() + public async Task GroupedQueriesWork() { // note it doesn't matter that the data doesn't exist for this; // the point here is that the entire thing *won't work* otherwise, // as per above test var keys = InventKeys(); - using var conn = Create(); + await using var conn = Create(); var grouped = keys.GroupBy(key => conn.GetHashSlot(key)).ToList(); Assert.True(grouped.Count > 1); // check not all a super-group @@ -651,14 +644,14 @@ public void GroupedQueriesWork() } [Fact] - public void MovedProfiling() + public async Task MovedProfiling() { var key = Me(); const string Value = "redirected-value"; var profiler = new ProfilingTests.PerThreadProfiler(); - using var conn = Create(); + await using var conn = Create(); conn.RegisterProfiler(profiler.GetSession); @@ -734,12 +727,12 @@ public void MovedProfiling() } [Fact] - public void ConnectIncludesSubscriber() + public async Task ConnectIncludesSubscriber() { - using var conn = Create(keepAlive: 1, connectTimeout: 3000, shared: false); + await using var conn = Create(keepAlive: 1, connectTimeout: 3000, shared: false); var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); Assert.True(conn.IsConnected); foreach (var server in conn.GetServerSnapshot()) @@ -749,168 +742,6 @@ public void ConnectIncludesSubscriber() } } - [Fact] - public async Task TestShardedPubsubSubscriberAgainstReconnects() - { - var channel = RedisChannel.Sharded(Me()); - using var conn = Create(allowAdmin: true, keepAlive: 1, connectTimeout: 3000, shared: false, require: RedisFeatures.v7_0_0_rc1); - Assert.True(conn.IsConnected); - var db = conn.GetDatabase(); - Assert.Equal(0, await db.PublishAsync(channel, "noClientReceivesThis")); - await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) - - var pubsub = conn.GetSubscriber(); - List<(RedisChannel, RedisValue)> received = new(); - var queue = await pubsub.SubscribeAsync(channel); - _ = Task.Run(async () => - { - // use queue API to have control over order - await foreach (var item in queue) - { - lock (received) - { - if (item.Channel.IsSharded && item.Channel == channel) received.Add((item.Channel, item.Message)); - } - } - }); - Assert.Equal(1, conn.GetSubscriptionsCount()); - - await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) - await db.PingAsync(); - - for (int i = 0; i < 5; i++) - { - // check we get a hit - Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); - } - await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) - - // this is endpoint at index 1 which has the hashslot for "testShardChannel" - var server = conn.GetServer(conn.GetEndPoints()[1]); - server.SimulateConnectionFailure(SimulatedFailureType.All); - SetExpectedAmbientFailureCount(2); - - await Task.Delay(4000); - for (int i = 0; i < 5; i++) - { - // check we get a hit - Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); - } - await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) - - Assert.Equal(1, conn.GetSubscriptionsCount()); - Assert.Equal(10, received.Count); - ClearAmbientFailures(); - } - - [Fact] - public async Task TestShardedPubsubSubscriberAgainsHashSlotMigration() - { - var channel = RedisChannel.Sharded(Me()); - using var conn = Create(allowAdmin: true, keepAlive: 1, connectTimeout: 3000, shared: false, require: RedisFeatures.v7_0_0_rc1); - Assert.True(conn.IsConnected); - var db = conn.GetDatabase(); - Assert.Equal(0, await db.PublishAsync(channel, "noClientReceivesThis")); - await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) - - var pubsub = conn.GetSubscriber(); - List<(RedisChannel, RedisValue)> received = new(); - var queue = await pubsub.SubscribeAsync(channel); - _ = Task.Run(async () => - { - // use queue API to have control over order - await foreach (var item in queue) - { - lock (received) - { - if (item.Channel.IsSharded && item.Channel == channel) received.Add((item.Channel, item.Message)); - } - } - }); - Assert.Equal(1, conn.GetSubscriptionsCount()); - - await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) - await db.PingAsync(); - - for (int i = 0; i < 5; i++) - { - // check we get a hit - Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); - } - await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) - - // lets migrate the slot for "testShardChannel" to another node - DoHashSlotMigration(); - - await Task.Delay(4000); - for (int i = 0; i < 5; i++) - { - // check we get a hit - Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); - } - await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) - - Assert.Equal(1, conn.GetSubscriptionsCount()); - Assert.Equal(10, received.Count); - RollbackHashSlotMigration(); - ClearAmbientFailures(); - } - - private void DoHashSlotMigration() - { - MigrateSlotForTestShardChannel(false); - } - private void RollbackHashSlotMigration() - { - MigrateSlotForTestShardChannel(true); - } - - private void MigrateSlotForTestShardChannel(bool rollback) - { - int hashSlotForTestShardChannel = 7177; - using var conn = Create(allowAdmin: true, keepAlive: 1, connectTimeout: 5000, shared: false); - var servers = conn.GetServers(); - IServer? serverWithPort7000 = null; - IServer? serverWithPort7001 = null; - - string nodeIdForPort7000 = "780813af558af81518e58e495d63b6e248e80adf"; - string nodeIdForPort7001 = "ea828c6074663c8bd4e705d3e3024d9d1721ef3b"; - foreach (var server in servers) - { - string id = server.Execute("CLUSTER", "MYID").ToString(); - if (id == nodeIdForPort7000) - { - serverWithPort7000 = server; - } - if (id == nodeIdForPort7001) - { - serverWithPort7001 = server; - } - } - - IServer fromServer, toServer; - string fromNode, toNode; - if (rollback) - { - fromServer = serverWithPort7000!; - fromNode = nodeIdForPort7000; - toServer = serverWithPort7001!; - toNode = nodeIdForPort7001; - } - else - { - fromServer = serverWithPort7001!; - fromNode = nodeIdForPort7001; - toServer = serverWithPort7000!; - toNode = nodeIdForPort7000; - } - - Assert.Equal("OK", toServer.Execute("CLUSTER", "SETSLOT", hashSlotForTestShardChannel, "IMPORTING", fromNode).ToString()); - Assert.Equal("OK", fromServer.Execute("CLUSTER", "SETSLOT", hashSlotForTestShardChannel, "MIGRATING", toNode).ToString()); - Assert.Equal("OK", toServer.Execute("CLUSTER", "SETSLOT", hashSlotForTestShardChannel, "NODE", toNode).ToString()); - Assert.Equal("OK", fromServer!.Execute("CLUSTER", "SETSLOT", hashSlotForTestShardChannel, "NODE", toNode).ToString()); - } - [Theory] [InlineData(true)] [InlineData(false)] @@ -918,11 +749,11 @@ public async Task ClusterPubSub(bool sharded) { var guid = Guid.NewGuid().ToString(); var channel = sharded ? RedisChannel.Sharded(guid) : RedisChannel.Literal(guid); - using var conn = Create(keepAlive: 1, connectTimeout: 3000, shared: false, require: sharded ? RedisFeatures.v7_0_0_rc1 : RedisFeatures.v2_0_0); + await using var conn = Create(keepAlive: 1, connectTimeout: 3000, shared: false, require: sharded ? RedisFeatures.v7_0_0_rc1 : RedisFeatures.v2_0_0); Assert.True(conn.IsConnected); var pubsub = conn.GetSubscriber(); - List<(RedisChannel, RedisValue)> received = new(); + List<(RedisChannel, RedisValue)> received = []; var queue = await pubsub.SubscribeAsync(channel); _ = Task.Run(async () => { diff --git a/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs b/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs index 13125d722..04e1ca624 100644 --- a/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs +++ b/tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs @@ -1,24 +1,22 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class CommandTimeoutTests : TestBase +public class CommandTimeoutTests(ITestOutputHelper output) : TestBase(output) { - public CommandTimeoutTests(ITestOutputHelper output) : base(output) { } - - [FactLongRunning] + [Fact] public async Task DefaultHeartbeatTimeout() { + Skip.UnlessLongRunning(); var options = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort); options.AllowAdmin = true; options.AsyncTimeout = 1000; - using var pauseConn = ConnectionMultiplexer.Connect(options); - using var conn = ConnectionMultiplexer.Connect(options); + await using var pauseConn = ConnectionMultiplexer.Connect(options); + await using var conn = ConnectionMultiplexer.Connect(options); var pauseServer = GetServer(pauseConn); var pauseTask = pauseServer.ExecuteAsync("CLIENT", "PAUSE", 5000); @@ -44,8 +42,8 @@ public async Task DefaultHeartbeatLowTimeout() options.AsyncTimeout = 50; options.HeartbeatInterval = TimeSpan.FromMilliseconds(100); - using var pauseConn = ConnectionMultiplexer.Connect(options); - using var conn = ConnectionMultiplexer.Connect(options); + await using var pauseConn = await ConnectionMultiplexer.ConnectAsync(options); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options); var pauseServer = GetServer(pauseConn); var pauseTask = pauseServer.ExecuteAsync("CLIENT", "PAUSE", 2000); diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 7c8e917b7..995b66a5a 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.IO; using System.IO.Pipelines; @@ -15,16 +14,12 @@ using Microsoft.Extensions.Logging.Abstractions; using StackExchange.Redis.Configuration; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class ConfigTests : TestBase +public class ConfigTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public ConfigTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - public Version DefaultVersion = new(3, 0, 0); [Fact] @@ -216,7 +211,7 @@ public void CanParseAndFormatUnixDomainSocket() } [Fact] - public void TalkToNonsenseServer() + public async Task TalkToNonsenseServer() { var config = new ConfigurationOptions { @@ -228,7 +223,7 @@ public void TalkToNonsenseServer() ConnectTimeout = 200, }; var log = new StringWriter(); - using (var conn = ConnectionMultiplexer.Connect(config, log)) + await using (var conn = ConnectionMultiplexer.Connect(config, log)) { Log(log.ToString()); Assert.False(conn.IsConnected); @@ -240,7 +235,7 @@ public async Task TestManualHeartbeat() { var options = ConfigurationOptions.Parse(GetConfiguration()); options.HeartbeatInterval = TimeSpan.FromMilliseconds(100); - using var conn = await ConnectionMultiplexer.ConnectAsync(options); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options); foreach (var ep in conn.GetServerSnapshot().ToArray()) { @@ -248,7 +243,7 @@ public async Task TestManualHeartbeat() } var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); var before = conn.OperationCount; @@ -264,40 +259,40 @@ public async Task TestManualHeartbeat() [InlineData(10)] [InlineData(100)] [InlineData(200)] - public void GetSlowlog(int count) + public async Task GetSlowlog(int count) { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); var rows = GetAnyPrimary(conn).SlowlogGet(count); Assert.NotNull(rows); } [Fact] - public void ClearSlowlog() + public async Task ClearSlowlog() { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); GetAnyPrimary(conn).SlowlogReset(); } [Fact] - public void ClientName() + public async Task ClientName() { - using var conn = Create(clientName: "Test Rig", allowAdmin: true, shared: false); + await using var conn = Create(clientName: "Test Rig", allowAdmin: true, shared: false); Assert.Equal("Test Rig", conn.ClientName); var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); - var name = (string?)GetAnyPrimary(conn).Execute("CLIENT", "GETNAME"); + var name = (string?)(await GetAnyPrimary(conn).ExecuteAsync("CLIENT", "GETNAME")); Assert.Equal("TestRig", name); } [Fact] public async Task ClientLibraryName() { - using var conn = Create(allowAdmin: true, shared: false); + await using var conn = Create(allowAdmin: true, shared: false); var server = GetAnyPrimary(conn); await server.PingAsync(); @@ -320,7 +315,7 @@ public async Task ClientLibraryName() conn.AddLibraryNameSuffix("foo"); libName = (await server.ClientListAsync()).Single(x => x.Id == id).LibraryName; - Log("library name: {0}", libName); + Log($"library name: {libName}"); Assert.Equal("SE.Redis-bar-foo", libName); } else @@ -330,22 +325,22 @@ public async Task ClientLibraryName() } [Fact] - public void DefaultClientName() + public async Task DefaultClientName() { - using var conn = Create(allowAdmin: true, caller: "", shared: false); // force default naming to kick in + await using var conn = Create(allowAdmin: true, caller: "", shared: false); // force default naming to kick in Assert.Equal($"{Environment.MachineName}(SE.Redis-v{Utils.GetLibVersion()})", conn.ClientName); var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); var name = (string?)GetAnyPrimary(conn).Execute("CLIENT", "GETNAME"); Assert.Equal($"{Environment.MachineName}(SE.Redis-v{Utils.GetLibVersion()})", name); } [Fact] - public void ReadConfigWithConfigDisabled() + public async Task ReadConfigWithConfigDisabled() { - using var conn = Create(allowAdmin: true, disabledCommands: new[] { "config", "info" }); + await using var conn = Create(allowAdmin: true, disabledCommands: ["config", "info"]); var server = GetAnyPrimary(conn); var ex = Assert.Throws(() => server.ConfigGet()); @@ -353,14 +348,14 @@ public void ReadConfigWithConfigDisabled() } [Fact] - public void ConnectWithSubscribeDisabled() + public async Task ConnectWithSubscribeDisabled() { - using var conn = Create(allowAdmin: true, disabledCommands: new[] { "subscribe" }); + await using var conn = Create(allowAdmin: true, disabledCommands: ["subscribe"]); Assert.True(conn.IsConnected); var servers = conn.GetServerSnapshot(); Assert.True(servers[0].IsConnected); - if (!Context.IsResp3) + if (!TestContext.Current.IsResp3()) { Assert.False(servers[0].IsSubscriberConnected); } @@ -370,9 +365,9 @@ public void ConnectWithSubscribeDisabled() } [Fact] - public void ReadConfig() + public async Task ReadConfig() { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); Log("about to get config"); var server = GetAnyPrimary(conn); @@ -391,9 +386,9 @@ public void ReadConfig() } [Fact] - public void GetTime() + public async Task GetTime() { - using var conn = Create(); + await using var conn = Create(); var server = GetAnyPrimary(conn); var serverTime = server.Time(); @@ -404,9 +399,9 @@ public void GetTime() } [Fact] - public void DebugObject() + public async Task DebugObject() { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -418,9 +413,9 @@ public void DebugObject() } [Fact] - public void GetInfo() + public async Task GetInfo() { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); var server = GetAnyPrimary(conn); var info1 = server.Info(); @@ -434,12 +429,17 @@ public void GetInfo() Log("Full info for: " + first.Key); foreach (var setting in first) { - Log("{0} ==> {1}", setting.Key, setting.Value); + Log(" {0} ==> {1}", setting.Key, setting.Value); } var info2 = server.Info("cpu"); Assert.Single(info2); var cpu = info2.Single(); + Log("Full info for: " + cpu.Key); + foreach (var setting in cpu) + { + Log(" {0} ==> {1}", setting.Key, setting.Value); + } var cpuCount = cpu.Count(); Assert.True(cpuCount > 2); Assert.Equal("CPU", cpu.Key); @@ -448,9 +448,9 @@ public void GetInfo() } [Fact] - public void GetInfoRaw() + public async Task GetInfoRaw() { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); var server = GetAnyPrimary(conn); var info = server.InfoRaw(); @@ -459,10 +459,10 @@ public void GetInfoRaw() } [Fact] - public void GetClients() + public async Task GetClients() { var name = Guid.NewGuid().ToString(); - using var conn = Create(clientName: name, allowAdmin: true, shared: false); + await using var conn = Create(clientName: name, allowAdmin: true, shared: false); var server = GetAnyPrimary(conn); var clients = server.ClientList(); @@ -482,7 +482,7 @@ public void GetClients() var self = clients.First(x => x.Id == id); if (server.Version.Major >= 7) { - Assert.Equal(Context.Test.Protocol, self.Protocol); + Assert.Equal(TestContext.Current.GetProtocol(), self.Protocol); } else { @@ -492,50 +492,15 @@ public void GetClients() } [Fact] - public void SlowLog() + public async Task SlowLog() { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); var server = GetAnyPrimary(conn); server.SlowlogGet(); server.SlowlogReset(); } - [Fact] - public async Task TestAutomaticHeartbeat() - { - RedisValue oldTimeout = RedisValue.Null; - using var configConn = Create(allowAdmin: true); - - try - { - configConn.GetDatabase(); - var srv = GetAnyPrimary(configConn); - oldTimeout = srv.ConfigGet("timeout")[0].Value; - srv.ConfigSet("timeout", 5); - - using var innerConn = Create(); - var innerDb = innerConn.GetDatabase(); - innerDb.Ping(); // need to wait to pick up configuration etc - - var before = innerConn.OperationCount; - - Log("sleeping to test heartbeat..."); - await Task.Delay(8000).ForAwait(); - - var after = innerConn.OperationCount; - Assert.True(after >= before + 1, $"after: {after}, before: {before}"); - } - finally - { - if (!oldTimeout.IsNull) - { - var srv = GetAnyPrimary(configConn); - srv.ConfigSet("timeout", oldTimeout); - } - } - } - [Fact] public void EndpointIteratorIsReliableOverChanges() { @@ -555,7 +520,7 @@ public void EndpointIteratorIsReliableOverChanges() } [Fact] - public void ThreadPoolManagerIsDetected() + public async Task ThreadPoolManagerIsDetected() { var config = new ConfigurationOptions { @@ -563,20 +528,20 @@ public void ThreadPoolManagerIsDetected() SocketManager = SocketManager.ThreadPool, }; - using var conn = ConnectionMultiplexer.Connect(config); + await using var conn = ConnectionMultiplexer.Connect(config); Assert.Same(PipeScheduler.ThreadPool, conn.SocketManager?.Scheduler); } [Fact] - public void DefaultThreadPoolManagerIsDetected() + public async Task DefaultThreadPoolManagerIsDetected() { var config = new ConfigurationOptions { EndPoints = { { IPAddress.Loopback, 6379 } }, }; - using var conn = ConnectionMultiplexer.Connect(config); + await using var conn = ConnectionMultiplexer.Connect(config); Assert.Same(SocketManager.Shared.Scheduler, conn.SocketManager?.Scheduler); } @@ -643,7 +608,7 @@ public void Apply() } [Fact] - public void BeforeSocketConnect() + public async Task BeforeSocketConnect() { var options = ConfigurationOptions.Parse(TestConfig.Current.PrimaryServerAndPort); int count = 0; @@ -654,7 +619,7 @@ public void BeforeSocketConnect() socket.DontFragment = true; socket.Ttl = (short)(connType == ConnectionType.Interactive ? 12 : 123); }; - using var conn = ConnectionMultiplexer.Connect(options); + await using var conn = ConnectionMultiplexer.Connect(options); Assert.True(conn.IsConnected); Assert.Equal(2, count); @@ -684,7 +649,7 @@ public async Task MutableOptions() var originalUser = options.User = "originalUser"; var originalPassword = options.Password = "originalPassword"; Assert.Equal("Details", options.ClientName); - using var conn = await ConnectionMultiplexer.ConnectAsync(options); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options); // Same instance Assert.Same(options, conn.RawConfig); diff --git a/tests/StackExchange.Redis.Tests/ConnectByIPTests.cs b/tests/StackExchange.Redis.Tests/ConnectByIPTests.cs index 24fc8d44d..3b6fc4d49 100644 --- a/tests/StackExchange.Redis.Tests/ConnectByIPTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectByIPTests.cs @@ -2,15 +2,13 @@ using System.Linq; using System.Net; using System.Net.Sockets; +using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class ConnectByIPTests : TestBase +public class ConnectByIPTests(ITestOutputHelper output) : TestBase(output) { - public ConnectByIPTests(ITestOutputHelper output) : base(output) { } - [Fact] public void ParseEndpoints() { @@ -31,36 +29,36 @@ public void ParseEndpoints() } [Fact] - public void IPv4Connection() + public async Task IPv4Connection() { var config = new ConfigurationOptions { EndPoints = { { TestConfig.Current.IPv4Server, TestConfig.Current.IPv4Port } }, }; - using var conn = ConnectionMultiplexer.Connect(config); + await using var conn = ConnectionMultiplexer.Connect(config); var server = conn.GetServer(config.EndPoints[0]); Assert.Equal(AddressFamily.InterNetwork, server.EndPoint.AddressFamily); - server.Ping(); + await server.PingAsync(); } [Fact] - public void IPv6Connection() + public async Task IPv6Connection() { var config = new ConfigurationOptions { EndPoints = { { TestConfig.Current.IPv6Server, TestConfig.Current.IPv6Port } }, }; - using var conn = ConnectionMultiplexer.Connect(config); + await using var conn = ConnectionMultiplexer.Connect(config); var server = conn.GetServer(config.EndPoints[0]); Assert.Equal(AddressFamily.InterNetworkV6, server.EndPoint.AddressFamily); - server.Ping(); + await server.PingAsync(); } [Theory] [MemberData(nameof(ConnectByVariousEndpointsData))] - public void ConnectByVariousEndpoints(EndPoint ep, AddressFamily expectedFamily) + public async Task ConnectByVariousEndpoints(EndPoint ep, AddressFamily expectedFamily) { Assert.Equal(expectedFamily, ep.AddressFamily); var config = new ConfigurationOptions @@ -69,11 +67,11 @@ public void ConnectByVariousEndpoints(EndPoint ep, AddressFamily expectedFamily) }; if (ep.AddressFamily != AddressFamily.InterNetworkV6) // I don't have IPv6 servers { - using (var conn = ConnectionMultiplexer.Connect(config)) + await using (var conn = ConnectionMultiplexer.Connect(config)) { var actual = conn.GetEndPoints().Single(); var server = conn.GetServer(actual); - server.Ping(); + await server.PingAsync(); } } } diff --git a/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs b/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs index 460f6f6b6..d0e67f35f 100644 --- a/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectCustomConfigTests.cs @@ -1,14 +1,11 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class ConnectCustomConfigTests : TestBase +public class ConnectCustomConfigTests(ITestOutputHelper output) : TestBase(output) { - public ConnectCustomConfigTests(ITestOutputHelper output) : base(output) { } - // So we're triggering tiebreakers here protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; @@ -19,12 +16,12 @@ public ConnectCustomConfigTests(ITestOutputHelper output) : base(output) { } [InlineData("config,get")] [InlineData("info,get")] [InlineData("config,info,get")] - public void DisabledCommandsStillConnect(string disabledCommands) + public async Task DisabledCommandsStillConnect(string disabledCommands) { - using var conn = Create(allowAdmin: true, disabledCommands: disabledCommands.Split(','), log: Writer); + await using var conn = Create(allowAdmin: true, disabledCommands: disabledCommands.Split(','), log: Writer); var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); Assert.True(db.IsConnected(default(RedisKey))); } @@ -37,19 +34,19 @@ public void DisabledCommandsStillConnect(string disabledCommands) [InlineData("info,get")] [InlineData("config,info,get")] [InlineData("config,info,get,cluster")] - public void DisabledCommandsStillConnectCluster(string disabledCommands) + public async Task DisabledCommandsStillConnectCluster(string disabledCommands) { - using var conn = Create(allowAdmin: true, configuration: TestConfig.Current.ClusterServersAndPorts, disabledCommands: disabledCommands.Split(','), log: Writer); + await using var conn = Create(allowAdmin: true, configuration: TestConfig.Current.ClusterServersAndPorts, disabledCommands: disabledCommands.Split(','), log: Writer); var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); Assert.True(db.IsConnected(default(RedisKey))); } [Fact] - public void TieBreakerIntact() + public async Task TieBreakerIntact() { - using var conn = Create(allowAdmin: true, log: Writer); + await using var conn = Create(allowAdmin: true, log: Writer); var tiebreaker = conn.GetDatabase().StringGet(conn.RawConfig.TieBreaker); Log($"Tiebreaker: {tiebreaker}"); @@ -61,9 +58,9 @@ public void TieBreakerIntact() } [Fact] - public void TieBreakerSkips() + public async Task TieBreakerSkips() { - using var conn = Create(allowAdmin: true, disabledCommands: new[] { "get" }, log: Writer); + await using var conn = Create(allowAdmin: true, disabledCommands: ["get"], log: Writer); Assert.Throws(() => conn.GetDatabase().StringGet(conn.RawConfig.TieBreaker)); foreach (var server in conn.GetServerSnapshot()) @@ -74,18 +71,18 @@ public void TieBreakerSkips() } [Fact] - public void TiebreakerIncorrectType() + public async Task TiebreakerIncorrectType() { var tiebreakerKey = Me(); - using var fubarConn = Create(allowAdmin: true, log: Writer); + await using var fubarConn = Create(allowAdmin: true, log: Writer); // Store something nonsensical in the tiebreaker key: fubarConn.GetDatabase().HashSet(tiebreakerKey, "foo", "bar"); // Ensure the next connection getting an invalid type still connects - using var conn = Create(allowAdmin: true, tieBreaker: tiebreakerKey, log: Writer); + await using var conn = Create(allowAdmin: true, tieBreaker: tiebreakerKey, log: Writer); var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); Assert.True(db.IsConnected(default(RedisKey))); var ex = Assert.Throws(() => db.StringGet(tiebreakerKey)); @@ -93,7 +90,7 @@ public void TiebreakerIncorrectType() } [Theory] - [InlineData(true, 4, 15)] + [InlineData(true, 2, 15)] [InlineData(false, 0, 0)] public async Task HeartbeatConsistencyCheckPingsAsync(bool enableConsistencyChecks, int minExpected, int maxExpected) { @@ -104,10 +101,10 @@ public async Task HeartbeatConsistencyCheckPingsAsync(bool enableConsistencyChec }; options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); - using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); Assert.True(db.IsConnected(default)); var preCount = conn.OperationCount; diff --git a/tests/StackExchange.Redis.Tests/ConnectFailTimeoutTests.cs b/tests/StackExchange.Redis.Tests/ConnectFailTimeoutTests.cs index 58d9795bb..6a7e253d7 100644 --- a/tests/StackExchange.Redis.Tests/ConnectFailTimeoutTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectFailTimeoutTests.cs @@ -1,19 +1,16 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class ConnectFailTimeoutTests : TestBase +public class ConnectFailTimeoutTests(ITestOutputHelper output) : TestBase(output) { - public ConnectFailTimeoutTests(ITestOutputHelper output) : base(output) { } - [Fact] public async Task NoticesConnectFail() { SetExpectedAmbientFailureCount(-1); - using var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); + await using var conn = Create(allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); var server = conn.GetServer(conn.GetEndPoints()[0]); @@ -41,7 +38,7 @@ void InnerScenario() await UntilConditionAsync(TimeSpan.FromSeconds(10), () => server.IsConnected); Log("pinging - expect success"); - var time = server.Ping(); + var time = await server.PingAsync(); Log("pinged"); Log(time.ToString()); } diff --git a/tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs b/tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs index d8a942a28..cc015c711 100644 --- a/tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectToUnexistingHostTests.cs @@ -3,14 +3,11 @@ using System.Linq; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class ConnectToUnexistingHostTests : TestBase +public class ConnectToUnexistingHostTests(ITestOutputHelper output) : TestBase(output) { - public ConnectToUnexistingHostTests(ITestOutputHelper output) : base(output) { } - [Fact] public async Task FailsWithinTimeout() { @@ -24,7 +21,7 @@ public async Task FailsWithinTimeout() ConnectTimeout = timeout, }; - using (ConnectionMultiplexer.Connect(config, Writer)) + await using (ConnectionMultiplexer.Connect(config, Writer)) { await Task.Delay(10000).ForAwait(); } diff --git a/tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs b/tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs index bc0fa9d0c..a905c613a 100644 --- a/tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectingFailDetectionTests.cs @@ -2,14 +2,11 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class ConnectingFailDetectionTests : TestBase +public class ConnectingFailDetectionTests(ITestOutputHelper output) : TestBase(output) { - public ConnectingFailDetectionTests(ITestOutputHelper output) : base(output) { } - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; [Fact] @@ -17,11 +14,11 @@ public async Task FastNoticesFailOnConnectingSyncCompletion() { try { - using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false); + await using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false); conn.RawConfig.ReconnectRetryPolicy = new LinearRetry(200); var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); var server = conn.GetServer(conn.GetEndPoints()[0]); var server2 = conn.GetServer(conn.GetEndPoints()[1]); @@ -57,11 +54,11 @@ public async Task FastNoticesFailOnConnectingAsyncCompletion() { try { - using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false); + await using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false); conn.RawConfig.ReconnectRetryPolicy = new LinearRetry(200); var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); var server = conn.GetServer(conn.GetEndPoints()[0]); var server2 = conn.GetServer(conn.GetEndPoints()[1]); @@ -106,7 +103,7 @@ public async Task Issue922_ReconnectRaised() int failCount = 0, restoreCount = 0; - using var conn = ConnectionMultiplexer.Connect(config); + await using var conn = await ConnectionMultiplexer.ConnectAsync(config); conn.ConnectionFailed += (s, e) => { @@ -137,14 +134,14 @@ public async Task Issue922_ReconnectRaised() } [Fact] - public void ConnectsWhenBeginConnectCompletesSynchronously() + public async Task ConnectsWhenBeginConnectCompletesSynchronously() { try { - using var conn = Create(keepAlive: 1, connectTimeout: 3000); + await using var conn = Create(keepAlive: 1, connectTimeout: 3000); var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); Assert.True(conn.IsConnected); } @@ -155,12 +152,12 @@ public void ConnectsWhenBeginConnectCompletesSynchronously() } [Fact] - public void ConnectIncludesSubscriber() + public async Task ConnectIncludesSubscriber() { - using var conn = Create(keepAlive: 1, connectTimeout: 3000, shared: false); + await using var conn = Create(keepAlive: 1, connectTimeout: 3000, shared: false); var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); Assert.True(conn.IsConnected); foreach (var server in conn.GetServerSnapshot()) diff --git a/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs b/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs index d359bd4b4..ce1a31980 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionFailedErrorsTests.cs @@ -4,14 +4,11 @@ using System.Threading.Tasks; using StackExchange.Redis.Configuration; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class ConnectionFailedErrorsTests : TestBase +public class ConnectionFailedErrorsTests(ITestOutputHelper output) : TestBase(output) { - public ConnectionFailedErrorsTests(ITestOutputHelper output) : base(output) { } - [Theory] [InlineData(true)] [InlineData(false)] @@ -27,7 +24,7 @@ public async Task SSLCertificateValidationError(bool isCertValidationSucceeded) options.CertificateValidation += (sender, cert, chain, errors) => isCertValidationSucceeded; options.AbortOnConnectFail = false; - using var conn = ConnectionMultiplexer.Connect(options); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options); await RunBlockingSynchronousWithExtraThreadAsync(InnerScenario).ForAwait(); @@ -71,14 +68,14 @@ public async Task AuthenticationFailureError() options.AbortOnConnectFail = false; options.CertificateValidation += SSLTests.ShowCertFailures(Writer); - using var conn = ConnectionMultiplexer.Connect(options); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options); await RunBlockingSynchronousWithExtraThreadAsync(InnerScenario).ForAwait(); void InnerScenario() { conn.ConnectionFailed += (sender, e) => { - if (e.FailureType == ConnectionFailureType.SocketFailure) Skip.Inconclusive("socket fail"); // this is OK too + if (e.FailureType == ConnectionFailureType.SocketFailure) Assert.Skip("socket fail"); // this is OK too Assert.Equal(ConnectionFailureType.AuthenticationFailure, e.FailureType); }; var ex = Assert.Throws(() => conn.GetDatabase().Ping()); @@ -176,7 +173,7 @@ public async Task CheckFailureRecovered() { try { - using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, log: Writer, shared: false); + await using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, log: Writer, shared: false); await RunBlockingSynchronousWithExtraThreadAsync(InnerScenario).ForAwait(); void InnerScenario() diff --git a/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs b/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs index 07e90dabc..db5f541b3 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionReconnectRetryPolicyTests.cs @@ -1,13 +1,10 @@ using System; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class TransientErrorTests : TestBase +public class TransientErrorTests(ITestOutputHelper output) : TestBase(output) { - public TransientErrorTests(ITestOutputHelper output) : base(output) { } - [Fact] public void TestExponentialRetry() { diff --git a/tests/StackExchange.Redis.Tests/ConnectionShutdownTests.cs b/tests/StackExchange.Redis.Tests/ConnectionShutdownTests.cs index c39bc4a76..279d8bd23 100644 --- a/tests/StackExchange.Redis.Tests/ConnectionShutdownTests.cs +++ b/tests/StackExchange.Redis.Tests/ConnectionShutdownTests.cs @@ -2,19 +2,15 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class ConnectionShutdownTests : TestBase +public class ConnectionShutdownTests(ITestOutputHelper output) : TestBase(output) { - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public ConnectionShutdownTests(ITestOutputHelper output) : base(output) { } - [Fact(Skip = "Unfriendly")] public async Task ShutdownRaisesConnectionFailedAndRestore() { - using var conn = Create(allowAdmin: true, shared: false); + await using var conn = Create(allowAdmin: true, shared: false); int failed = 0, restored = 0; Stopwatch watch = Stopwatch.StartNew(); @@ -29,7 +25,7 @@ public async Task ShutdownRaisesConnectionFailedAndRestore() Interlocked.Increment(ref restored); }; var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); Assert.Equal(0, Interlocked.CompareExchange(ref failed, 0, 0)); Assert.Equal(0, Interlocked.CompareExchange(ref restored, 0, 0)); await Task.Delay(1).ForAwait(); // To make compiler happy in Release diff --git a/tests/StackExchange.Redis.Tests/ConstraintsTests.cs b/tests/StackExchange.Redis.Tests/ConstraintsTests.cs index d5d58cbef..6740fe2b3 100644 --- a/tests/StackExchange.Redis.Tests/ConstraintsTests.cs +++ b/tests/StackExchange.Redis.Tests/ConstraintsTests.cs @@ -1,14 +1,10 @@ using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class ConstraintsTests : TestBase +public class ConstraintsTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public ConstraintsTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] public void ValueEquals() { @@ -20,7 +16,7 @@ public void ValueEquals() [Fact] public async Task TestManualIncr() { - using var conn = Create(syncTimeout: 120000); // big timeout while debugging + await using var conn = Create(syncTimeout: 120000); // big timeout while debugging var key = Me(); var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/CopyTests.cs b/tests/StackExchange.Redis.Tests/CopyTests.cs index b40f00c02..e0003136c 100644 --- a/tests/StackExchange.Redis.Tests/CopyTests.cs +++ b/tests/StackExchange.Redis.Tests/CopyTests.cs @@ -1,19 +1,15 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class CopyTests : TestBase +public class CopyTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public CopyTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] public async Task Basic() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var src = Me(); @@ -30,7 +26,7 @@ public async Task Basic() [Fact] public async Task CrossDB() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var dbDestId = TestConfig.GetDedicatedDB(conn); @@ -52,7 +48,7 @@ public async Task CrossDB() [Fact] public async Task WithReplace() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var src = Me(); diff --git a/tests/StackExchange.Redis.Tests/DatabaseTests.cs b/tests/StackExchange.Redis.Tests/DatabaseTests.cs index aed1dbf00..bb134c4fd 100644 --- a/tests/StackExchange.Redis.Tests/DatabaseTests.cs +++ b/tests/StackExchange.Redis.Tests/DatabaseTests.cs @@ -1,19 +1,15 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class DatabaseTests : TestBase +public class DatabaseTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public DatabaseTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] public async Task CommandCount() { - using var conn = Create(); + await using var conn = Create(); var server = GetAnyPrimary(conn); var count = server.CommandCount(); Assert.True(count > 100); @@ -25,13 +21,13 @@ public async Task CommandCount() [Fact] public async Task CommandGetKeys() { - using var conn = Create(); + await using var conn = Create(); var server = GetAnyPrimary(conn); - RedisValue[] command = { "MSET", "a", "b", "c", "d", "e", "f" }; + RedisValue[] command = ["MSET", "a", "b", "c", "d", "e", "f"]; RedisKey[] keys = server.CommandGetKeys(command); - RedisKey[] expected = { "a", "c", "e" }; + RedisKey[] expected = ["a", "c", "e"]; Assert.Equal(keys, expected); keys = await server.CommandGetKeysAsync(command); @@ -41,7 +37,7 @@ public async Task CommandGetKeys() [Fact] public async Task CommandList() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var server = GetAnyPrimary(conn); var commands = server.CommandList(); @@ -73,7 +69,7 @@ public async Task CountKeys() { var db1Id = TestConfig.GetDedicatedDB(); var db2Id = TestConfig.GetDedicatedDB(); - using (var conn = Create(allowAdmin: true)) + await using (var conn = Create(allowAdmin: true)) { Skip.IfMissingDatabase(conn, db1Id); Skip.IfMissingDatabase(conn, db2Id); @@ -81,7 +77,7 @@ public async Task CountKeys() server.FlushDatabase(db1Id, CommandFlags.FireAndForget); server.FlushDatabase(db2Id, CommandFlags.FireAndForget); } - using (var conn = Create(defaultDatabase: db2Id)) + await using (var conn = Create(defaultDatabase: db2Id)) { Skip.IfMissingDatabase(conn, db1Id); Skip.IfMissingDatabase(conn, db2Id); @@ -104,9 +100,9 @@ public async Task CountKeys() } [Fact] - public void DatabaseCount() + public async Task DatabaseCount() { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); var server = GetAnyPrimary(conn); var count = server.DatabaseCount; @@ -119,7 +115,7 @@ public void DatabaseCount() [Fact] public async Task MultiDatabases() { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(); var db0 = conn.GetDatabase(TestConfig.GetDedicatedDB(conn)); @@ -146,7 +142,7 @@ public async Task MultiDatabases() [Fact] public async Task SwapDatabases() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v4_0_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v4_0_0); RedisKey key = Me(); var db0id = TestConfig.GetDedicatedDB(conn); @@ -179,7 +175,7 @@ public async Task SwapDatabases() [Fact] public async Task SwapDatabasesAsync() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v4_0_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v4_0_0); RedisKey key = Me(); var db0id = TestConfig.GetDedicatedDB(conn); diff --git a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs index 8c35ab3e1..be80fd9c5 100644 --- a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs +++ b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Configuration; using System.Linq; using System.Net; using System.Threading; @@ -9,18 +8,14 @@ using Microsoft.Extensions.Logging.Abstractions; using StackExchange.Redis.Configuration; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class DefaultOptionsTests : TestBase +public class DefaultOptionsTests(ITestOutputHelper output) : TestBase(output) { - public DefaultOptionsTests(ITestOutputHelper output) : base(output) { } - - public class TestOptionsProvider : DefaultOptionsProvider + public class TestOptionsProvider(string domainSuffix) : DefaultOptionsProvider { - private readonly string _domainSuffix; - public TestOptionsProvider(string domainSuffix) => _domainSuffix = domainSuffix; + private readonly string _domainSuffix = domainSuffix; public override bool AbortOnConnectFail => true; public override TimeSpan? ConnectTimeout => TimeSpan.FromSeconds(123); @@ -150,7 +145,7 @@ public async Task AfterConnectAsyncHandler() var provider = new TestAfterConnectOptionsProvider(); options.Defaults = provider; - using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); Assert.True(conn.IsConnected); Assert.Equal(1, provider.Calls); @@ -167,7 +162,7 @@ public async Task ClientNameOverride() var options = ConfigurationOptions.Parse(GetConfiguration()); options.Defaults = new TestClientNameOptionsProvider(); - using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); Assert.True(conn.IsConnected); Assert.Equal("Hey there", conn.ClientName); @@ -179,7 +174,7 @@ public async Task ClientNameExplicitWins() var options = ConfigurationOptions.Parse(GetConfiguration() + ",name=FooBar"); options.Defaults = new TestClientNameOptionsProvider(); - using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); Assert.True(conn.IsConnected); Assert.Equal("FooBar", conn.ClientName); @@ -199,9 +194,9 @@ public async Task LibraryNameOverride() options.AllowAdmin = true; options.Defaults = defaults; - using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); // CLIENT SETINFO is in 7.2.0+ - ThrowIfBelowMinVersion(conn, RedisFeatures.v7_2_0_rc1); + TestBase.ThrowIfBelowMinVersion(conn, RedisFeatures.v7_2_0_rc1); var clients = await GetServer(conn).ClientListAsync(); foreach (var client in clients) diff --git a/tests/StackExchange.Redis.Tests/DeprecatedTests.cs b/tests/StackExchange.Redis.Tests/DeprecatedTests.cs index 3e0971d6c..ab909ea16 100644 --- a/tests/StackExchange.Redis.Tests/DeprecatedTests.cs +++ b/tests/StackExchange.Redis.Tests/DeprecatedTests.cs @@ -1,16 +1,13 @@ using System; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; /// /// Testing that things we deprecate still parse, but are otherwise defaults. /// -public class DeprecatedTests : TestBase +public class DeprecatedTests(ITestOutputHelper output) : TestBase(output) { - public DeprecatedTests(ITestOutputHelper output) : base(output) { } - #pragma warning disable CS0618 // Type or member is obsolete [Fact] public void HighPrioritySocketThreads() diff --git a/tests/StackExchange.Redis.Tests/EnvoyTests.cs b/tests/StackExchange.Redis.Tests/EnvoyTests.cs index 6d669a6db..d9d22c801 100644 --- a/tests/StackExchange.Redis.Tests/EnvoyTests.cs +++ b/tests/StackExchange.Redis.Tests/EnvoyTests.cs @@ -1,27 +1,25 @@ using System; using System.Text; +using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class EnvoyTests : TestBase +public class EnvoyTests(ITestOutputHelper output) : TestBase(output) { - public EnvoyTests(ITestOutputHelper output) : base(output) { } - protected override string GetConfiguration() => TestConfig.Current.ProxyServerAndPort; /// /// Tests basic envoy connection with the ability to set and get a key. /// [Fact] - public void TestBasicEnvoyConnection() + public async Task TestBasicEnvoyConnection() { var sb = new StringBuilder(); Writer.EchoTo(sb); try { - using var conn = Create(configuration: GetConfiguration(), keepAlive: 1, connectTimeout: 2000, allowAdmin: true, shared: false, proxy: Proxy.Envoyproxy, log: Writer); + await using var conn = Create(configuration: GetConfiguration(), keepAlive: 1, connectTimeout: 2000, allowAdmin: true, shared: false, proxy: Proxy.Envoyproxy, log: Writer); var db = conn.GetDatabase(); @@ -35,15 +33,15 @@ public void TestBasicEnvoyConnection() } catch (TimeoutException ex) when (ex.Message == "Connect timeout" || sb.ToString().Contains("Returned, but incorrectly")) { - Skip.Inconclusive($"Envoy server not found: {ex}."); + Assert.Skip($"Envoy server not found: {ex}."); } catch (AggregateException ex) { - Skip.Inconclusive($"Envoy server not found: {ex}."); + Assert.Skip($"Envoy server not found: {ex}."); } catch (RedisConnectionException ex) when (sb.ToString().Contains("It was not possible to connect to the redis server(s)")) { - Skip.Inconclusive($"Envoy server not found: {ex}."); + Assert.Skip($"Envoy server not found: {ex}."); } } } diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 2edee05c6..53f28f163 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -1,17 +1,15 @@ using System; +using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class ExceptionFactoryTests : TestBase +public class ExceptionFactoryTests(ITestOutputHelper output) : TestBase(output) { - public ExceptionFactoryTests(ITestOutputHelper output) : base(output) { } - [Fact] - public void NullLastException() + public async Task NullLastException() { - using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true); + await using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true); conn.GetDatabase(); Assert.Null(conn.GetServerSnapshot()[0].LastException); @@ -28,11 +26,11 @@ public void CanGetVersion() #if DEBUG [Fact] - public void MultipleEndpointsThrowConnectionException() + public async Task MultipleEndpointsThrowConnectionException() { try { - using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false); + await using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false); conn.GetDatabase(); conn.AllowConnect = false; @@ -57,11 +55,11 @@ public void MultipleEndpointsThrowConnectionException() #endif [Fact] - public void ServerTakesPrecendenceOverSnapshot() + public async Task ServerTakesPrecendenceOverSnapshot() { try { - using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); + await using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false, backlogPolicy: BacklogPolicy.FailFast); conn.GetDatabase(); conn.AllowConnect = false; @@ -80,11 +78,11 @@ public void ServerTakesPrecendenceOverSnapshot() } [Fact] - public void NullInnerExceptionForMultipleEndpointsWithNoLastException() + public async Task NullInnerExceptionForMultipleEndpointsWithNoLastException() { try { - using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true); + await using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true); conn.GetDatabase(); conn.AllowConnect = false; @@ -99,11 +97,11 @@ public void NullInnerExceptionForMultipleEndpointsWithNoLastException() } [Fact] - public void TimeoutException() + public async Task TimeoutException() { try { - using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false); + await using var conn = Create(keepAlive: 1, connectTimeout: 10000, allowAdmin: true, shared: false); var server = GetServer(conn); conn.AllowConnect = false; @@ -153,7 +151,7 @@ public void TimeoutException() [InlineData(true, 0, 0, true, "No connection is active/available to service this operation: PING")] [InlineData(true, 1, 0, true, "No connection is active/available to service this operation: PING")] [InlineData(true, 12, 0, true, "No connection is active/available to service this operation: PING")] - public void NoConnectionException(bool abortOnConnect, int connCount, int completeCount, bool hasDetail, string messageStart) + public async Task NoConnectionException(bool abortOnConnect, int connCount, int completeCount, bool hasDetail, string messageStart) { try { @@ -178,7 +176,7 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple conn = ConnectionMultiplexer.Connect(options, Writer); } - using (conn) + await using (conn) { var server = conn.GetServer(conn.GetEndPoints()[0]); conn.AllowConnect = false; @@ -218,9 +216,9 @@ public void NoConnectionException(bool abortOnConnect, int connCount, int comple } [Fact] - public void NoConnectionPrimaryOnlyException() + public async Task NoConnectionPrimaryOnlyException() { - using var conn = ConnectionMultiplexer.Connect(TestConfig.Current.ReplicaServerAndPort, Writer); + await using var conn = await ConnectionMultiplexer.ConnectAsync(TestConfig.Current.ReplicaServerAndPort, Writer); var msg = Message.Create(0, CommandFlags.None, RedisCommand.SET, (RedisKey)Me(), (RedisValue)"test"); Assert.True(msg.IsPrimaryOnly()); @@ -237,9 +235,9 @@ public void NoConnectionPrimaryOnlyException() [InlineData(true, ConnectionFailureType.ConnectionDisposed, "ConnectionDisposed on [0]:GET myKey (StringProcessor), my annotation")] [InlineData(false, ConnectionFailureType.ProtocolFailure, "ProtocolFailure on [0]:GET (StringProcessor), my annotation")] [InlineData(false, ConnectionFailureType.ConnectionDisposed, "ConnectionDisposed on [0]:GET (StringProcessor), my annotation")] - public void MessageFail(bool includeDetail, ConnectionFailureType failType, string messageStart) + public async Task MessageFail(bool includeDetail, ConnectionFailureType failType, string messageStart) { - using var conn = Create(shared: false); + await using var conn = Create(shared: false); conn.RawConfig.IncludeDetailInExceptions = includeDetail; diff --git a/tests/StackExchange.Redis.Tests/ExecuteTests.cs b/tests/StackExchange.Redis.Tests/ExecuteTests.cs index 30012001a..1e1f10bd4 100644 --- a/tests/StackExchange.Redis.Tests/ExecuteTests.cs +++ b/tests/StackExchange.Redis.Tests/ExecuteTests.cs @@ -1,19 +1,15 @@ using System.Linq; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class ExecuteTests : TestBase +public class ExecuteTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public ExecuteTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] public async Task DBExecute() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(4); RedisKey key = Me(); @@ -29,7 +25,7 @@ public async Task DBExecute() [Fact] public async Task ServerExecute() { - using var conn = Create(); + await using var conn = Create(); var server = conn.GetServer(conn.GetEndPoints().First()); var actual = (string?)server.Execute("echo", "some value"); diff --git a/tests/StackExchange.Redis.Tests/ExpiryTests.cs b/tests/StackExchange.Redis.Tests/ExpiryTests.cs index 3d11442e6..fab26586f 100644 --- a/tests/StackExchange.Redis.Tests/ExpiryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExpiryTests.cs @@ -1,23 +1,19 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class ExpiryTests : TestBase +public class ExpiryTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public ExpiryTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - - private static string[]? GetMap(bool disablePTimes) => disablePTimes ? new[] { "pexpire", "pexpireat", "pttl" } : null; + private static string[]? GetMap(bool disablePTimes) => disablePTimes ? ["pexpire", "pexpireat", "pttl"] : null; [Theory] [InlineData(true)] [InlineData(false)] public async Task TestBasicExpiryTimeSpan(bool disablePTimes) { - using var conn = Create(disabledCommands: GetMap(disablePTimes)); + await using var conn = Create(disabledCommands: GetMap(disablePTimes)); RedisKey key = Me(); var db = conn.GetDatabase(); @@ -53,7 +49,7 @@ public async Task TestBasicExpiryTimeSpan(bool disablePTimes) [InlineData(false)] public async Task TestExpiryOptions(bool disablePTimes) { - using var conn = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); var key = Me(); var db = conn.GetDatabase(); @@ -84,7 +80,7 @@ public async Task TestExpiryOptions(bool disablePTimes) [InlineData(false, false)] public async Task TestBasicExpiryDateTime(bool disablePTimes, bool utc) { - using var conn = Create(disabledCommands: GetMap(disablePTimes)); + await using var conn = Create(disabledCommands: GetMap(disablePTimes)); RedisKey key = Me(); var db = conn.GetDatabase(); @@ -135,9 +131,9 @@ public async Task TestBasicExpiryDateTime(bool disablePTimes, bool utc) [Theory] [InlineData(true)] [InlineData(false)] - public void KeyExpiryTime(bool disablePTimes) + public async Task KeyExpiryTime(bool disablePTimes) { - using var conn = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); var key = Me(); var db = conn.GetDatabase(); @@ -168,7 +164,7 @@ public void KeyExpiryTime(bool disablePTimes) [InlineData(false)] public async Task KeyExpiryTimeAsync(bool disablePTimes) { - using var conn = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(disabledCommands: GetMap(disablePTimes), require: RedisFeatures.v7_0_0_rc1); var key = Me(); var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/FSharpCompatTests.cs b/tests/StackExchange.Redis.Tests/FSharpCompatTests.cs index 7b39d36b9..192234f2d 100644 --- a/tests/StackExchange.Redis.Tests/FSharpCompatTests.cs +++ b/tests/StackExchange.Redis.Tests/FSharpCompatTests.cs @@ -1,12 +1,9 @@ using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class FSharpCompatTests : TestBase +public class FSharpCompatTests(ITestOutputHelper output) : TestBase(output) { - public FSharpCompatTests(ITestOutputHelper output) : base(output) { } - #pragma warning disable SA1129 // Do not use default value type constructor [Fact] public void RedisKeyConstructor() diff --git a/tests/StackExchange.Redis.Tests/FailoverTests.cs b/tests/StackExchange.Redis.Tests/FailoverTests.cs index fdbd23a94..68f8f2266 100644 --- a/tests/StackExchange.Redis.Tests/FailoverTests.cs +++ b/tests/StackExchange.Redis.Tests/FailoverTests.cs @@ -4,22 +4,19 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class FailoverTests : TestBase, IAsyncLifetime +public class FailoverTests(ITestOutputHelper output) : TestBase(output), IAsyncLifetime { protected override string GetConfiguration() => GetPrimaryReplicaConfig().ToString(); - public FailoverTests(ITestOutputHelper output) : base(output) { } + public ValueTask DisposeAsync() => ValueTask.CompletedTask; - public Task DisposeAsync() => Task.CompletedTask; - - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { - using var conn = Create(); + await using var conn = Create(); var shouldBePrimary = conn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort); if (shouldBePrimary.IsReplica) @@ -54,7 +51,7 @@ private static ConfigurationOptions GetPrimaryReplicaConfig() [Fact] public async Task ConfigureAsync() { - using var conn = Create(); + await using var conn = Create(); await Task.Delay(1000).ForAwait(); Log("About to reconfigure....."); @@ -65,7 +62,7 @@ public async Task ConfigureAsync() [Fact] public async Task ConfigureSync() { - using var conn = Create(); + await using var conn = Create(); await Task.Delay(1000).ForAwait(); Log("About to reconfigure....."); @@ -77,8 +74,8 @@ public async Task ConfigureSync() public async Task ConfigVerifyReceiveConfigChangeBroadcast() { _ = GetConfiguration(); - using var senderConn = Create(allowAdmin: true); - using var receiverConn = Create(syncTimeout: 2000); + await using var senderConn = Create(allowAdmin: true); + await using var receiverConn = Create(syncTimeout: 2000); int total = 0; receiverConn.ConfigurationChangedBroadcast += (s, a) => @@ -88,8 +85,8 @@ public async Task ConfigVerifyReceiveConfigChangeBroadcast() }; // send a reconfigure/reconnect message long count = senderConn.PublishReconfigure(); - GetServer(receiverConn).Ping(); - GetServer(receiverConn).Ping(); + await GetServer(receiverConn).PingAsync(); + await GetServer(receiverConn).PingAsync(); await Task.Delay(1000).ConfigureAwait(false); Assert.True(count == -1 || count >= 2, "subscribers"); Assert.True(Interlocked.CompareExchange(ref total, 0, 0) >= 1, "total (1st)"); @@ -98,11 +95,11 @@ public async Task ConfigVerifyReceiveConfigChangeBroadcast() // and send a second time via a re-primary operation var server = GetServer(senderConn); - if (server.IsReplica) Skip.Inconclusive("didn't expect a replica"); + if (server.IsReplica) Assert.Skip("didn't expect a replica"); await server.MakePrimaryAsync(ReplicationChangeOptions.Broadcast); await Task.Delay(1000).ConfigureAwait(false); - GetServer(receiverConn).Ping(); - GetServer(receiverConn).Ping(); + await GetServer(receiverConn).PingAsync(); + await GetServer(receiverConn).PingAsync(); Assert.True(Interlocked.CompareExchange(ref total, 0, 0) >= 1, "total (2nd)"); } @@ -112,21 +109,21 @@ public async Task DereplicateGoesToPrimary() ConfigurationOptions config = GetPrimaryReplicaConfig(); config.ConfigCheckSeconds = 5; - using var conn = ConnectionMultiplexer.Connect(config); + await using var conn = await ConnectionMultiplexer.ConnectAsync(config); var primary = conn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort); var secondary = conn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort); - primary.Ping(); - secondary.Ping(); + await primary.PingAsync(); + await secondary.PingAsync(); await primary.MakePrimaryAsync(ReplicationChangeOptions.SetTiebreaker); await secondary.MakePrimaryAsync(ReplicationChangeOptions.None); await Task.Delay(100).ConfigureAwait(false); - primary.Ping(); - secondary.Ping(); + await primary.PingAsync(); + await secondary.PingAsync(); using (var writer = new StringWriter()) { @@ -134,7 +131,7 @@ public async Task DereplicateGoesToPrimary() string log = writer.ToString(); Log(log); bool isUnanimous = log.Contains("tie-break is unanimous at " + TestConfig.Current.FailoverPrimaryServerAndPort); - if (!isUnanimous) Skip.Inconclusive("this is timing sensitive; unable to verify this time"); + if (!isUnanimous) Assert.Skip("this is timing sensitive; unable to verify this time"); } // k, so we know everyone loves 6379; is that what we get? @@ -154,8 +151,8 @@ public async Task DereplicateGoesToPrimary() await Task.Delay(100).ConfigureAwait(false); Log("Invoking Ping() (post-primary)"); - primary.Ping(); - secondary.Ping(); + await primary.PingAsync(); + await secondary.PingAsync(); Log("Finished Ping() (post-primary)"); Assert.True(primary.IsConnected, $"{primary.EndPoint} is not connected."); @@ -201,7 +198,7 @@ public async Task DereplicateGoesToPrimary() [Fact] public async Task SubscriptionsSurviveConnectionFailureAsync() { - using var conn = Create(allowAdmin: true, shared: false, log: Writer, syncTimeout: 1000); + await using var conn = Create(allowAdmin: true, shared: false, log: Writer, syncTimeout: 1000); var profiler = conn.AddProfiler(); RedisChannel channel = RedisChannel.Literal(Me()); @@ -217,7 +214,7 @@ public async Task SubscriptionsSurviveConnectionFailureAsync() await Task.Delay(200).ConfigureAwait(false); await sub.PublishAsync(channel, "abc").ConfigureAwait(false); - sub.Ping(); + await sub.PingAsync(); await Task.Delay(200).ConfigureAwait(false); var counter1 = Thread.VolatileRead(ref counter); @@ -256,7 +253,7 @@ public async Task SubscriptionsSurviveConnectionFailureAsync() var profile2 = Log(profiler); // Assert.Equal(1, profile2.Count(p => p.Command == nameof(RedisCommand.SUBSCRIBE))); Log("Issuing ping after reconnected"); - sub.Ping(); + await sub.PingAsync(); var muxerSubCount = conn.GetSubscriptionsCount(); Log($"Muxer thinks we have {muxerSubCount} subscriber(s)."); @@ -295,15 +292,10 @@ public async Task SubscriptionsSurviveConnectionFailureAsync() [Fact] public async Task SubscriptionsSurvivePrimarySwitchAsync() { - static void TopologyFail() => Skip.Inconclusive("Replication topology change failed...and that's both inconsistent and not what we're testing."); - - if (RunningInCI) - { - Skip.Inconclusive("TODO: Fix race in broadcast reconfig a zero latency."); - } + static void TopologyFail() => Assert.Skip("Replication topology change failed...and that's both inconsistent and not what we're testing."); - using var aConn = Create(allowAdmin: true, shared: false); - using var bConn = Create(allowAdmin: true, shared: false); + await using var aConn = Create(allowAdmin: true, shared: false); + await using var bConn = Create(allowAdmin: true, shared: false); RedisChannel channel = RedisChannel.Literal(Me()); Log("Using Channel: " + channel); @@ -362,8 +354,8 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() } Log("Waiting for connection B to detect..."); await UntilConditionAsync(TimeSpan.FromSeconds(10), () => bConn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica).ForAwait(); - subA.Ping(); - subB.Ping(); + await subA.PingAsync(); + await subB.PingAsync(); Log("Failover 2 Attempted. Pausing..."); Log(" A " + TestConfig.Current.FailoverPrimaryServerAndPort + " status: " + (aConn.GetServer(TestConfig.Current.FailoverPrimaryServerAndPort).IsReplica ? "Replica" : "Primary")); Log(" A " + TestConfig.Current.FailoverReplicaServerAndPort + " status: " + (aConn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica ? "Replica" : "Primary")); @@ -391,7 +383,7 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() Log(" IsReplica: " + !server.IsReplica); Log(" Type: " + server.ServerType); } - // Skip.Inconclusive("Not enough latency."); + // Assert.Skip("Not enough latency."); } Assert.True(sanityCheck, $"B Connection: {TestConfig.Current.FailoverPrimaryServerAndPort} should be a replica"); Assert.False(bConn.GetServer(TestConfig.Current.FailoverReplicaServerAndPort).IsReplica, $"B Connection: {TestConfig.Current.FailoverReplicaServerAndPort} should be a primary"); @@ -399,8 +391,8 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() Log("Pause complete"); Log(" A outstanding: " + aConn.GetCounters().TotalOutstanding); Log(" B outstanding: " + bConn.GetCounters().TotalOutstanding); - subA.Ping(); - subB.Ping(); + await subA.PingAsync(); + await subB.PingAsync(); await Task.Delay(5000).ForAwait(); epA = subA.SubscribedEndpoint(channel); epB = subB.SubscribedEndpoint(channel); @@ -411,8 +403,8 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync() var bSentTo = subB.Publish(channel, "B2"); Log(" A2 sent to: " + aSentTo); Log(" B2 sent to: " + bSentTo); - subA.Ping(); - subB.Ping(); + await subA.PingAsync(); + await subB.PingAsync(); Log("Ping Complete. Checking..."); await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Interlocked.Read(ref aCount) == 2 && Interlocked.Read(ref bCount) == 2).ForAwait(); diff --git a/tests/StackExchange.Redis.Tests/FloatingPointTests.cs b/tests/StackExchange.Redis.Tests/FloatingPointTests.cs index 71bab0386..9b4b2bd7e 100644 --- a/tests/StackExchange.Redis.Tests/FloatingPointTests.cs +++ b/tests/StackExchange.Redis.Tests/FloatingPointTests.cs @@ -1,38 +1,34 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class FloatingPointTests : TestBase +public class FloatingPointTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public FloatingPointTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - private static bool Within(double x, double y, double delta) => Math.Abs(x - y) <= delta; [Fact] - public void IncrDecrFloatingPoint() + public async Task IncrDecrFloatingPoint() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); double[] incr = - { + [ 12.134, -14561.0000002, 125.3421, -2.49892498, - }, + ], decr = - { + [ 99.312, 12, -35, - }; + ]; double sum = 0; foreach (var value in incr) { @@ -52,24 +48,24 @@ public void IncrDecrFloatingPoint() [Fact] public async Task IncrDecrFloatingPointAsync() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); double[] incr = - { + [ 12.134, -14561.0000002, 125.3421, -2.49892498, - }, + ], decr = - { + [ 99.312, 12, -35, - }; + ]; double sum = 0; foreach (var value in incr) { @@ -87,27 +83,27 @@ public async Task IncrDecrFloatingPointAsync() } [Fact] - public void HashIncrDecrFloatingPoint() + public async Task HashIncrDecrFloatingPoint() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); RedisValue field = "foo"; db.KeyDelete(key, CommandFlags.FireAndForget); double[] incr = - { + [ 12.134, -14561.0000002, 125.3421, -2.49892498, - }, + ], decr = - { + [ 99.312, 12, -35, - }; + ]; double sum = 0; foreach (var value in incr) { @@ -127,25 +123,25 @@ public void HashIncrDecrFloatingPoint() [Fact] public async Task HashIncrDecrFloatingPointAsync() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); RedisValue field = "bar"; db.KeyDelete(key, CommandFlags.FireAndForget); double[] incr = - { + [ 12.134, -14561.0000002, 125.3421, -2.49892498, - }, + ], decr = - { + [ 99.312, 12, -35, - }; + ]; double sum = 0; foreach (var value in incr) { diff --git a/tests/StackExchange.Redis.Tests/FormatTests.cs b/tests/StackExchange.Redis.Tests/FormatTests.cs index 9356c2e31..451db8c20 100644 --- a/tests/StackExchange.Redis.Tests/FormatTests.cs +++ b/tests/StackExchange.Redis.Tests/FormatTests.cs @@ -3,14 +3,11 @@ using System.Net; using System.Text; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class FormatTests : TestBase +public class FormatTests(ITestOutputHelper output) : TestBase(output) { - public FormatTests(ITestOutputHelper output) : base(output) { } - public static IEnumerable EndpointData() { // note: the 3rd arg is for formatting; null means "expect the original string" diff --git a/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs b/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs index 8a263c553..ef28ed6e9 100644 --- a/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs +++ b/tests/StackExchange.Redis.Tests/GarbageCollectionTests.cs @@ -2,15 +2,12 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] // because I need to measure some things that could get confused -public class GarbageCollectionTests : TestBase +public class GarbageCollectionTests(ITestOutputHelper helper) : TestBase(helper) { - public GarbageCollectionTests(ITestOutputHelper helper) : base(helper) { } - private static void ForceGC() { for (int i = 0; i < 3; i++) @@ -24,14 +21,14 @@ private static void ForceGC() public async Task MuxerIsCollected() { #if DEBUG - Skip.Inconclusive("Only predictable in release builds"); + Assert.Skip("Only predictable in release builds"); #endif // this is more nuanced than it looks; multiple sockets with // async callbacks, plus a heartbeat on a timer // deliberately not "using" - we *want* to leak this var conn = Create(); - conn.GetDatabase().Ping(); // smoke-test + await conn.GetDatabase().PingAsync(); // smoke-test ForceGC(); @@ -57,6 +54,7 @@ public async Task MuxerIsCollected() [Fact] public async Task UnrootedBackloggedAsyncTaskIsCompletedOnTimeout() { + Skip.UnlessLongRunning(); // Run the test on a separate thread without keeping a reference to the task to ensure // that there are no references to the variables in test task from the main thread. // WithTimeout must not be used within Task.Run because timers are rooted and would keep everything alive. @@ -64,7 +62,7 @@ public async Task UnrootedBackloggedAsyncTaskIsCompletedOnTimeout() Task? completedTestTask = null; _ = Task.Run(async () => { - using var conn = await ConnectionMultiplexer.ConnectAsync( + await using var conn = await ConnectionMultiplexer.ConnectAsync( new ConfigurationOptions() { BacklogPolicy = BacklogPolicy.Default, diff --git a/tests/StackExchange.Redis.Tests/GeoTests.cs b/tests/StackExchange.Redis.Tests/GeoTests.cs index 84fa30386..c46f65be7 100644 --- a/tests/StackExchange.Redis.Tests/GeoTests.cs +++ b/tests/StackExchange.Redis.Tests/GeoTests.cs @@ -1,28 +1,24 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class GeoTests : TestBase +public class GeoTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public GeoTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - private static readonly GeoEntry Palermo = new GeoEntry(13.361389, 38.115556, "Palermo"), Catania = new GeoEntry(15.087269, 37.502669, "Catania"), Agrigento = new GeoEntry(13.5765, 37.311, "Agrigento"), Cefalù = new GeoEntry(14.0188, 38.0084, "Cefalù"); - private static readonly GeoEntry[] All = { Palermo, Catania, Agrigento, Cefalù }; + private static readonly GeoEntry[] All = [Palermo, Catania, Agrigento, Cefalù]; [Fact] - public void GeoAdd() + public async Task GeoAdd() { - using var conn = Create(require: RedisFeatures.v3_2_0); + await using var conn = Create(require: RedisFeatures.v3_2_0); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -30,12 +26,12 @@ public void GeoAdd() // add while not there Assert.True(db.GeoAdd(key, Cefalù.Longitude, Cefalù.Latitude, Cefalù.Member)); - Assert.Equal(2, db.GeoAdd(key, new[] { Palermo, Catania })); + Assert.Equal(2, db.GeoAdd(key, [Palermo, Catania])); Assert.True(db.GeoAdd(key, Agrigento)); // now add again Assert.False(db.GeoAdd(key, Cefalù.Longitude, Cefalù.Latitude, Cefalù.Member)); - Assert.Equal(0, db.GeoAdd(key, new[] { Palermo, Catania })); + Assert.Equal(0, db.GeoAdd(key, [Palermo, Catania])); Assert.False(db.GeoAdd(key, Agrigento)); // Validate @@ -46,9 +42,9 @@ public void GeoAdd() } [Fact] - public void GetDistance() + public async Task GetDistance() { - using var conn = Create(require: RedisFeatures.v3_2_0); + await using var conn = Create(require: RedisFeatures.v3_2_0); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -63,16 +59,16 @@ public void GetDistance() } [Fact] - public void GeoHash() + public async Task GeoHash() { - using var conn = Create(require: RedisFeatures.v3_2_0); + await using var conn = Create(require: RedisFeatures.v3_2_0); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); db.GeoAdd(key, All, CommandFlags.FireAndForget); - var hashes = db.GeoHash(key, new RedisValue[] { Palermo.Member, "Nowhere", Agrigento.Member }); + var hashes = db.GeoHash(key, [Palermo.Member, "Nowhere", Agrigento.Member]); Assert.NotNull(hashes); Assert.Equal(3, hashes.Length); Assert.Equal("sqc8b49rny0", hashes[0]); @@ -87,9 +83,9 @@ public void GeoHash() } [Fact] - public void GeoGetPosition() + public async Task GeoGetPosition() { - using var conn = Create(require: RedisFeatures.v3_2_0); + await using var conn = Create(require: RedisFeatures.v3_2_0); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -106,9 +102,9 @@ public void GeoGetPosition() } [Fact] - public void GeoRemove() + public async Task GeoRemove() { - using var conn = Create(require: RedisFeatures.v3_2_0); + await using var conn = Create(require: RedisFeatures.v3_2_0); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -127,9 +123,9 @@ public void GeoRemove() } [Fact] - public void GeoRadius() + public async Task GeoRadius() { - using var conn = Create(require: RedisFeatures.v3_2_0); + await using var conn = Create(require: RedisFeatures.v3_2_0); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -173,7 +169,7 @@ public void GeoRadius() [Fact] public async Task GeoRadiusOverloads() { - using var conn = Create(require: RedisFeatures.v3_2_0); + await using var conn = Create(require: RedisFeatures.v3_2_0); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -222,7 +218,7 @@ private void GeoSearchSetup(RedisKey key, IDatabase db) [Fact] public async Task GeoSearchCircleMemberAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -243,7 +239,7 @@ public async Task GeoSearchCircleMemberAsync() [Fact] public async Task GeoSearchCircleMemberAsyncOnlyHash() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -264,7 +260,7 @@ public async Task GeoSearchCircleMemberAsyncOnlyHash() [Fact] public async Task GeoSearchCircleMemberAsyncHashAndDistance() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -285,7 +281,7 @@ public async Task GeoSearchCircleMemberAsyncHashAndDistance() [Fact] public async Task GeoSearchCircleLonLatAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -301,9 +297,9 @@ public async Task GeoSearchCircleLonLatAsync() } [Fact] - public void GeoSearchCircleMember() + public async Task GeoSearchCircleMember() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -319,9 +315,9 @@ public void GeoSearchCircleMember() } [Fact] - public void GeoSearchCircleLonLat() + public async Task GeoSearchCircleLonLat() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -339,7 +335,7 @@ public void GeoSearchCircleLonLat() [Fact] public async Task GeoSearchBoxMemberAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -356,7 +352,7 @@ public async Task GeoSearchBoxMemberAsync() [Fact] public async Task GeoSearchBoxLonLatAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -371,9 +367,9 @@ public async Task GeoSearchBoxLonLatAsync() } [Fact] - public void GeoSearchBoxMember() + public async Task GeoSearchBoxMember() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -388,9 +384,9 @@ public void GeoSearchBoxMember() } [Fact] - public void GeoSearchBoxLonLat() + public async Task GeoSearchBoxLonLat() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -405,9 +401,9 @@ public void GeoSearchBoxLonLat() } [Fact] - public void GeoSearchLimitCount() + public async Task GeoSearchLimitCount() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -421,9 +417,9 @@ public void GeoSearchLimitCount() } [Fact] - public void GeoSearchLimitCountMakeNoDemands() + public async Task GeoSearchLimitCountMakeNoDemands() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -439,7 +435,7 @@ public void GeoSearchLimitCountMakeNoDemands() [Fact] public async Task GeoSearchBoxLonLatDescending() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -457,7 +453,7 @@ public async Task GeoSearchBoxLonLatDescending() [Fact] public async Task GeoSearchBoxMemberAndStoreAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var me = Me(); var db = conn.GetDatabase(); @@ -479,7 +475,7 @@ public async Task GeoSearchBoxMemberAndStoreAsync() [Fact] public async Task GeoSearchBoxLonLatAndStoreAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var me = Me(); var db = conn.GetDatabase(); @@ -501,7 +497,7 @@ public async Task GeoSearchBoxLonLatAndStoreAsync() [Fact] public async Task GeoSearchCircleMemberAndStoreAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var me = Me(); var db = conn.GetDatabase(); @@ -523,7 +519,7 @@ public async Task GeoSearchCircleMemberAndStoreAsync() [Fact] public async Task GeoSearchCircleLonLatAndStoreAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var me = Me(); var db = conn.GetDatabase(); @@ -543,9 +539,9 @@ public async Task GeoSearchCircleLonLatAndStoreAsync() } [Fact] - public void GeoSearchCircleMemberAndStore() + public async Task GeoSearchCircleMemberAndStore() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var me = Me(); var db = conn.GetDatabase(); @@ -565,9 +561,9 @@ public void GeoSearchCircleMemberAndStore() } [Fact] - public void GeoSearchCircleLonLatAndStore() + public async Task GeoSearchCircleLonLatAndStore() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var me = Me(); var db = conn.GetDatabase(); @@ -587,9 +583,9 @@ public void GeoSearchCircleLonLatAndStore() } [Fact] - public void GeoSearchCircleAndStoreDistOnly() + public async Task GeoSearchCircleAndStoreDistOnly() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var me = Me(); var db = conn.GetDatabase(); @@ -612,9 +608,9 @@ public void GeoSearchCircleAndStoreDistOnly() } [Fact] - public void GeoSearchBadArgs() + public async Task GeoSearchBadArgs() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/HashFieldTests.cs b/tests/StackExchange.Redis.Tests/HashFieldTests.cs index e50cd0546..3d1cb0c6e 100644 --- a/tests/StackExchange.Redis.Tests/HashFieldTests.cs +++ b/tests/StackExchange.Redis.Tests/HashFieldTests.cs @@ -1,7 +1,7 @@ using System; using System.Linq; +using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; @@ -9,19 +9,14 @@ namespace StackExchange.Redis.Tests; /// Tests for . /// [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class HashFieldTests : TestBase +public class HashFieldTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { private readonly DateTime nextCentury = new DateTime(2101, 1, 1, 0, 0, 0, DateTimeKind.Utc); private readonly TimeSpan oneYearInMs = TimeSpan.FromMilliseconds(31536000000); - private readonly HashEntry[] entries = new HashEntry[] { new("f1", 1), new("f2", 2) }; + private readonly HashEntry[] entries = [new("f1", 1), new("f2", 2)]; - private readonly RedisValue[] fields = new RedisValue[] { "f1", "f2" }; - - public HashFieldTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) - { - } + private readonly RedisValue[] fields = ["f1", "f2"]; [Fact] public void HashFieldExpire() @@ -31,10 +26,10 @@ public void HashFieldExpire() db.HashSet(hashKey, entries); var fieldsResult = db.HashFieldExpire(hashKey, fields, oneYearInMs); - Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success }, fieldsResult); + Assert.Equal([ExpireResult.Success, ExpireResult.Success], fieldsResult); fieldsResult = db.HashFieldExpire(hashKey, fields, nextCentury); - Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success, }, fieldsResult); + Assert.Equal([ExpireResult.Success, ExpireResult.Success,], fieldsResult); } [Fact] @@ -44,37 +39,37 @@ public void HashFieldExpireNoKey() var hashKey = Me(); var fieldsResult = db.HashFieldExpire(hashKey, fields, oneYearInMs); - Assert.Equal(new[] { ExpireResult.NoSuchField, ExpireResult.NoSuchField }, fieldsResult); + Assert.Equal([ExpireResult.NoSuchField, ExpireResult.NoSuchField], fieldsResult); fieldsResult = db.HashFieldExpire(hashKey, fields, nextCentury); - Assert.Equal(new[] { ExpireResult.NoSuchField, ExpireResult.NoSuchField }, fieldsResult); + Assert.Equal([ExpireResult.NoSuchField, ExpireResult.NoSuchField], fieldsResult); } [Fact] - public async void HashFieldExpireAsync() + public async Task HashFieldExpireAsync() { var db = Create(require: RedisFeatures.v7_4_0_rc1).GetDatabase(); var hashKey = Me(); db.HashSet(hashKey, entries); var fieldsResult = await db.HashFieldExpireAsync(hashKey, fields, oneYearInMs); - Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success }, fieldsResult); + Assert.Equal([ExpireResult.Success, ExpireResult.Success], fieldsResult); fieldsResult = await db.HashFieldExpireAsync(hashKey, fields, nextCentury); - Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success }, fieldsResult); + Assert.Equal([ExpireResult.Success, ExpireResult.Success], fieldsResult); } [Fact] - public async void HashFieldExpireAsyncNoKey() + public async Task HashFieldExpireAsyncNoKey() { var db = Create(require: RedisFeatures.v7_4_0_rc2).GetDatabase(); var hashKey = Me(); var fieldsResult = await db.HashFieldExpireAsync(hashKey, fields, oneYearInMs); - Assert.Equal(new[] { ExpireResult.NoSuchField, ExpireResult.NoSuchField }, fieldsResult); + Assert.Equal([ExpireResult.NoSuchField, ExpireResult.NoSuchField], fieldsResult); fieldsResult = await db.HashFieldExpireAsync(hashKey, fields, nextCentury); - Assert.Equal(new[] { ExpireResult.NoSuchField, ExpireResult.NoSuchField }, fieldsResult); + Assert.Equal([ExpireResult.NoSuchField, ExpireResult.NoSuchField], fieldsResult); } [Fact] @@ -84,8 +79,8 @@ public void HashFieldGetExpireDateTimeIsDue() var hashKey = Me(); db.HashSet(hashKey, entries); - var result = db.HashFieldExpire(hashKey, new RedisValue[] { "f1" }, new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)); - Assert.Equal(new[] { ExpireResult.Due }, result); + var result = db.HashFieldExpire(hashKey, ["f1"], new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + Assert.Equal([ExpireResult.Due], result); } [Fact] @@ -95,8 +90,8 @@ public void HashFieldExpireNoField() var hashKey = Me(); db.HashSet(hashKey, entries); - var result = db.HashFieldExpire(hashKey, new RedisValue[] { "nonExistingField" }, oneYearInMs); - Assert.Equal(new[] { ExpireResult.NoSuchField }, result); + var result = db.HashFieldExpire(hashKey, ["nonExistingField"], oneYearInMs); + Assert.Equal([ExpireResult.NoSuchField], result); } [Fact] @@ -106,21 +101,21 @@ public void HashFieldExpireConditionsSatisfied() var hashKey = Me(); db.KeyDelete(hashKey); db.HashSet(hashKey, entries); - db.HashSet(hashKey, new HashEntry[] { new("f3", 3), new("f4", 4) }); - var initialExpire = db.HashFieldExpire(hashKey, new RedisValue[] { "f2", "f3", "f4" }, new DateTime(2050, 1, 1, 0, 0, 0, DateTimeKind.Utc)); - Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success, ExpireResult.Success }, initialExpire); + db.HashSet(hashKey, [new("f3", 3), new("f4", 4)]); + var initialExpire = db.HashFieldExpire(hashKey, ["f2", "f3", "f4"], new DateTime(2050, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + Assert.Equal([ExpireResult.Success, ExpireResult.Success, ExpireResult.Success], initialExpire); - var result = db.HashFieldExpire(hashKey, new RedisValue[] { "f1" }, oneYearInMs, ExpireWhen.HasNoExpiry); - Assert.Equal(new[] { ExpireResult.Success }, result); + var result = db.HashFieldExpire(hashKey, ["f1"], oneYearInMs, ExpireWhen.HasNoExpiry); + Assert.Equal([ExpireResult.Success], result); - result = db.HashFieldExpire(hashKey, new RedisValue[] { "f2" }, oneYearInMs, ExpireWhen.HasExpiry); - Assert.Equal(new[] { ExpireResult.Success }, result); + result = db.HashFieldExpire(hashKey, ["f2"], oneYearInMs, ExpireWhen.HasExpiry); + Assert.Equal([ExpireResult.Success], result); - result = db.HashFieldExpire(hashKey, new RedisValue[] { "f3" }, nextCentury, ExpireWhen.GreaterThanCurrentExpiry); - Assert.Equal(new[] { ExpireResult.Success }, result); + result = db.HashFieldExpire(hashKey, ["f3"], nextCentury, ExpireWhen.GreaterThanCurrentExpiry); + Assert.Equal([ExpireResult.Success], result); - result = db.HashFieldExpire(hashKey, new RedisValue[] { "f4" }, oneYearInMs, ExpireWhen.LessThanCurrentExpiry); - Assert.Equal(new[] { ExpireResult.Success }, result); + result = db.HashFieldExpire(hashKey, ["f4"], oneYearInMs, ExpireWhen.LessThanCurrentExpiry); + Assert.Equal([ExpireResult.Success], result); } [Fact] @@ -130,21 +125,21 @@ public void HashFieldExpireConditionsNotSatisfied() var hashKey = Me(); db.KeyDelete(hashKey); db.HashSet(hashKey, entries); - db.HashSet(hashKey, new HashEntry[] { new("f3", 3), new("f4", 4) }); - var initialExpire = db.HashFieldExpire(hashKey, new RedisValue[] { "f2", "f3", "f4" }, new DateTime(2050, 1, 1, 0, 0, 0, DateTimeKind.Utc)); - Assert.Equal(new[] { ExpireResult.Success, ExpireResult.Success, ExpireResult.Success }, initialExpire); + db.HashSet(hashKey, [new("f3", 3), new("f4", 4)]); + var initialExpire = db.HashFieldExpire(hashKey, ["f2", "f3", "f4"], new DateTime(2050, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + Assert.Equal([ExpireResult.Success, ExpireResult.Success, ExpireResult.Success], initialExpire); - var result = db.HashFieldExpire(hashKey, new RedisValue[] { "f1" }, oneYearInMs, ExpireWhen.HasExpiry); - Assert.Equal(new[] { ExpireResult.ConditionNotMet }, result); + var result = db.HashFieldExpire(hashKey, ["f1"], oneYearInMs, ExpireWhen.HasExpiry); + Assert.Equal([ExpireResult.ConditionNotMet], result); - result = db.HashFieldExpire(hashKey, new RedisValue[] { "f2" }, oneYearInMs, ExpireWhen.HasNoExpiry); - Assert.Equal(new[] { ExpireResult.ConditionNotMet }, result); + result = db.HashFieldExpire(hashKey, ["f2"], oneYearInMs, ExpireWhen.HasNoExpiry); + Assert.Equal([ExpireResult.ConditionNotMet], result); - result = db.HashFieldExpire(hashKey, new RedisValue[] { "f3" }, nextCentury, ExpireWhen.LessThanCurrentExpiry); - Assert.Equal(new[] { ExpireResult.ConditionNotMet }, result); + result = db.HashFieldExpire(hashKey, ["f3"], nextCentury, ExpireWhen.LessThanCurrentExpiry); + Assert.Equal([ExpireResult.ConditionNotMet], result); - result = db.HashFieldExpire(hashKey, new RedisValue[] { "f4" }, oneYearInMs, ExpireWhen.GreaterThanCurrentExpiry); - Assert.Equal(new[] { ExpireResult.ConditionNotMet }, result); + result = db.HashFieldExpire(hashKey, ["f4"], oneYearInMs, ExpireWhen.GreaterThanCurrentExpiry); + Assert.Equal([ExpireResult.ConditionNotMet], result); } [Fact] @@ -156,11 +151,11 @@ public void HashFieldGetExpireDateTime() db.HashFieldExpire(hashKey, fields, nextCentury); long ms = new DateTimeOffset(nextCentury).ToUnixTimeMilliseconds(); - var result = db.HashFieldGetExpireDateTime(hashKey, new RedisValue[] { "f1" }); - Assert.Equal(new[] { ms }, result); + var result = db.HashFieldGetExpireDateTime(hashKey, ["f1"]); + Assert.Equal([ms], result); var fieldsResult = db.HashFieldGetExpireDateTime(hashKey, fields); - Assert.Equal(new[] { ms, ms }, fieldsResult); + Assert.Equal([ms, ms], fieldsResult); } [Fact] @@ -170,11 +165,11 @@ public void HashFieldExpireFieldNoExpireTime() var hashKey = Me(); db.HashSet(hashKey, entries); - var result = db.HashFieldGetExpireDateTime(hashKey, new RedisValue[] { "f1" }); - Assert.Equal(new[] { -1L }, result); + var result = db.HashFieldGetExpireDateTime(hashKey, ["f1"]); + Assert.Equal([-1L], result); var fieldsResult = db.HashFieldGetExpireDateTime(hashKey, fields); - Assert.Equal(new long[] { -1, -1, }, fieldsResult); + Assert.Equal([-1, -1,], fieldsResult); } [Fact] @@ -184,7 +179,7 @@ public void HashFieldGetExpireDateTimeNoKey() var hashKey = Me(); var fieldsResult = db.HashFieldGetExpireDateTime(hashKey, fields); - Assert.Equal(new long[] { -2, -2, }, fieldsResult); + Assert.Equal([-2, -2,], fieldsResult); } [Fact] @@ -195,8 +190,8 @@ public void HashFieldGetExpireDateTimeNoField() db.HashSet(hashKey, entries); db.HashFieldExpire(hashKey, fields, oneYearInMs); - var fieldsResult = db.HashFieldGetExpireDateTime(hashKey, new RedisValue[] { "notExistingField1", "notExistingField2" }); - Assert.Equal(new long[] { -2, -2, }, fieldsResult); + var fieldsResult = db.HashFieldGetExpireDateTime(hashKey, ["notExistingField1", "notExistingField2"]); + Assert.Equal([-2, -2,], fieldsResult); } [Fact] @@ -208,7 +203,7 @@ public void HashFieldGetTimeToLive() db.HashFieldExpire(hashKey, fields, oneYearInMs); long ms = new DateTimeOffset(nextCentury).ToUnixTimeMilliseconds(); - var result = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" }); + var result = db.HashFieldGetTimeToLive(hashKey, ["f1"]); Assert.NotNull(result); Assert.True(result.Length == 1); Assert.True(result[0] > 0); @@ -227,7 +222,7 @@ public void HashFieldGetTimeToLiveNoExpireTime() db.HashSet(hashKey, entries); var fieldsResult = db.HashFieldGetTimeToLive(hashKey, fields); - Assert.Equal(new long[] { -1, -1, }, fieldsResult); + Assert.Equal([-1, -1,], fieldsResult); } [Fact] @@ -237,7 +232,7 @@ public void HashFieldGetTimeToLiveNoKey() var hashKey = Me(); var fieldsResult = db.HashFieldGetTimeToLive(hashKey, fields); - Assert.Equal(new long[] { -2, -2, }, fieldsResult); + Assert.Equal([-2, -2,], fieldsResult); } [Fact] @@ -248,8 +243,8 @@ public void HashFieldGetTimeToLiveNoField() db.HashSet(hashKey, entries); db.HashFieldExpire(hashKey, fields, oneYearInMs); - var fieldsResult = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "notExistingField1", "notExistingField2" }); - Assert.Equal(new long[] { -2, -2, }, fieldsResult); + var fieldsResult = db.HashFieldGetTimeToLive(hashKey, ["notExistingField1", "notExistingField2"]); + Assert.Equal([-2, -2,], fieldsResult); } [Fact] @@ -261,13 +256,13 @@ public void HashFieldPersist() db.HashFieldExpire(hashKey, fields, oneYearInMs); long ms = new DateTimeOffset(nextCentury).ToUnixTimeMilliseconds(); - var result = db.HashFieldPersist(hashKey, new RedisValue[] { "f1" }); - Assert.Equal(new[] { PersistResult.Success }, result); + var result = db.HashFieldPersist(hashKey, ["f1"]); + Assert.Equal([PersistResult.Success], result); db.HashFieldExpire(hashKey, fields, oneYearInMs); var fieldsResult = db.HashFieldPersist(hashKey, fields); - Assert.Equal(new[] { PersistResult.Success, PersistResult.Success }, fieldsResult); + Assert.Equal([PersistResult.Success, PersistResult.Success], fieldsResult); } [Fact] @@ -278,7 +273,7 @@ public void HashFieldPersistNoExpireTime() db.HashSet(hashKey, entries); var fieldsResult = db.HashFieldPersist(hashKey, fields); - Assert.Equal(new[] { PersistResult.ConditionNotMet, PersistResult.ConditionNotMet }, fieldsResult); + Assert.Equal([PersistResult.ConditionNotMet, PersistResult.ConditionNotMet], fieldsResult); } [Fact] @@ -288,7 +283,7 @@ public void HashFieldPersistNoKey() var hashKey = Me(); var fieldsResult = db.HashFieldPersist(hashKey, fields); - Assert.Equal(new[] { PersistResult.NoSuchField, PersistResult.NoSuchField }, fieldsResult); + Assert.Equal([PersistResult.NoSuchField, PersistResult.NoSuchField], fieldsResult); } [Fact] @@ -299,7 +294,7 @@ public void HashFieldPersistNoField() db.HashSet(hashKey, entries); db.HashFieldExpire(hashKey, fields, oneYearInMs); - var fieldsResult = db.HashFieldPersist(hashKey, new RedisValue[] { "notExistingField1", "notExistingField2" }); - Assert.Equal(new[] { PersistResult.NoSuchField, PersistResult.NoSuchField }, fieldsResult); + var fieldsResult = db.HashFieldPersist(hashKey, ["notExistingField1", "notExistingField2"]); + Assert.Equal([PersistResult.NoSuchField, PersistResult.NoSuchField], fieldsResult); } } diff --git a/tests/StackExchange.Redis.Tests/HashTests.cs b/tests/StackExchange.Redis.Tests/HashTests.cs index b949f5911..af2fa11c8 100644 --- a/tests/StackExchange.Redis.Tests/HashTests.cs +++ b/tests/StackExchange.Redis.Tests/HashTests.cs @@ -4,7 +4,6 @@ using System.Text; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; @@ -12,15 +11,12 @@ namespace StackExchange.Redis.Tests; /// Tests for . /// [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class HashTests : TestBase +public class HashTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public HashTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] public async Task TestIncrBy() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -45,7 +41,7 @@ public async Task TestIncrBy() [Fact] public async Task ScanAsync() { - using var conn = Create(require: RedisFeatures.v2_8_0); + await using var conn = Create(require: RedisFeatures.v2_8_0); var db = conn.GetDatabase(); var key = Me(); @@ -89,17 +85,17 @@ public async Task ScanAsync() } [Fact] - public void Scan() + public async Task Scan() { - using var conn = Create(require: RedisFeatures.v2_8_0); + await using var conn = Create(require: RedisFeatures.v2_8_0); var db = conn.GetDatabase(); var key = Me(); - db.KeyDeleteAsync(key); - db.HashSetAsync(key, "abc", "def"); - db.HashSetAsync(key, "ghi", "jkl"); - db.HashSetAsync(key, "mno", "pqr"); + _ = db.KeyDeleteAsync(key); + _ = db.HashSetAsync(key, "abc", "def"); + _ = db.HashSetAsync(key, "ghi", "jkl"); + _ = db.HashSetAsync(key, "mno", "pqr"); var t1 = db.HashScan(key); var t2 = db.HashScan(key, "*h*"); @@ -129,7 +125,7 @@ public void Scan() [Fact] public async Task ScanNoValuesAsync() { - using var conn = Create(require: RedisFeatures.v7_4_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_4_0_rc1); var db = conn.GetDatabase(); var key = Me(); @@ -173,17 +169,17 @@ public async Task ScanNoValuesAsync() } [Fact] - public void ScanNoValues() + public async Task ScanNoValues() { - using var conn = Create(require: RedisFeatures.v7_4_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_4_0_rc1); var db = conn.GetDatabase(); var key = Me(); - db.KeyDeleteAsync(key); - db.HashSetAsync(key, "abc", "def"); - db.HashSetAsync(key, "ghi", "jkl"); - db.HashSetAsync(key, "mno", "pqr"); + _ = db.KeyDeleteAsync(key); + _ = db.HashSetAsync(key, "abc", "def"); + _ = db.HashSetAsync(key, "ghi", "jkl"); + _ = db.HashSetAsync(key, "mno", "pqr"); var t1 = db.HashScanNoValues(key); var t2 = db.HashScanNoValues(key, "*h*"); @@ -212,12 +208,12 @@ public void ScanNoValues() } [Fact] - public void TestIncrementOnHashThatDoesntExist() + public async Task TestIncrementOnHashThatDoesntExist() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); - db.KeyDeleteAsync("keynotexist"); + _ = db.KeyDeleteAsync("keynotexist"); var result1 = db.Wait(db.HashIncrementAsync("keynotexist", "fieldnotexist", 1)); var result2 = db.Wait(db.HashIncrementAsync("keynotexist", "anotherfieldnotexist", 1)); Assert.Equal(1, result1); @@ -227,7 +223,7 @@ public void TestIncrementOnHashThatDoesntExist() [Fact] public async Task TestIncrByFloat() { - using var conn = Create(require: RedisFeatures.v2_6_0); + await using var conn = Create(require: RedisFeatures.v2_6_0); var db = conn.GetDatabase(); var key = Me(); @@ -250,7 +246,7 @@ public async Task TestIncrByFloat() [Fact] public async Task TestGetAll() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -282,7 +278,7 @@ public async Task TestGetAll() [Fact] public async Task TestGet() { - using var conn = Create(); + await using var conn = Create(); var key = Me(); var db = conn.GetDatabase(); @@ -314,7 +310,7 @@ public async Task TestGet() [Fact] public async Task TestSet() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var hashkey = Me(); @@ -356,7 +352,7 @@ public async Task TestSet() [Fact] public async Task TestSetNotExists() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var hashkey = Me(); @@ -390,7 +386,7 @@ public async Task TestSetNotExists() [Fact] public async Task TestDelSingle() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var hashkey = Me(); @@ -413,7 +409,7 @@ public async Task TestDelSingle() [Fact] public async Task TestDelMulti() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var hashkey = Me(); @@ -425,7 +421,7 @@ public async Task TestDelMulti() var s2 = db.HashExistsAsync(hashkey, "key2"); var s3 = db.HashExistsAsync(hashkey, "key3"); - var removed = db.HashDeleteAsync(hashkey, new RedisValue[] { "key1", "key3" }); + var removed = db.HashDeleteAsync(hashkey, ["key1", "key3"]); var d1 = db.HashExistsAsync(hashkey, "key1"); var d2 = db.HashExistsAsync(hashkey, "key2"); @@ -441,7 +437,7 @@ public async Task TestDelMulti() Assert.True(await d2); Assert.False(await d3); - var removeFinal = db.HashDeleteAsync(hashkey, new RedisValue[] { "key2" }); + var removeFinal = db.HashDeleteAsync(hashkey, ["key2"]); Assert.Equal(0, await db.HashLengthAsync(hashkey).ForAwait()); Assert.Equal(1, await removeFinal); @@ -453,7 +449,7 @@ public async Task TestDelMulti() [Fact] public async Task TestDelMultiInsideTransaction() { - using var conn = Create(); + await using var conn = Create(); var tran = conn.GetDatabase().CreateTransaction(); { @@ -466,7 +462,7 @@ public async Task TestDelMultiInsideTransaction() var s2 = tran.HashExistsAsync(hashkey, "key2"); var s3 = tran.HashExistsAsync(hashkey, "key3"); - var removed = tran.HashDeleteAsync(hashkey, new RedisValue[] { "key1", "key3" }); + var removed = tran.HashDeleteAsync(hashkey, ["key1", "key3"]); var d1 = tran.HashExistsAsync(hashkey, "key1"); var d2 = tran.HashExistsAsync(hashkey, "key2"); @@ -492,7 +488,7 @@ public async Task TestDelMultiInsideTransaction() [Fact] public async Task TestExists() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var hashkey = Me(); @@ -514,7 +510,7 @@ public async Task TestExists() [Fact] public async Task TestHashKeys() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var hashKey = Me(); @@ -540,7 +536,7 @@ public async Task TestHashKeys() [Fact] public async Task TestHashValues() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var hashkey = Me(); @@ -567,7 +563,7 @@ public async Task TestHashValues() [Fact] public async Task TestHashLength() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var hashkey = Me(); @@ -590,13 +586,13 @@ public async Task TestHashLength() [Fact] public async Task TestGetMulti() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var hashkey = Me(); db.KeyDelete(hashkey, CommandFlags.FireAndForget); - RedisValue[] fields = { "foo", "bar", "blop" }; + RedisValue[] fields = ["foo", "bar", "blop"]; var arr0 = await db.HashGetAsync(hashkey, fields).ForAwait(); db.HashSet(hashkey, "foo", "abc", flags: CommandFlags.FireAndForget); @@ -625,18 +621,18 @@ public async Task TestGetMulti() /// Tests for . /// [Fact] - public void TestGetPairs() + public async Task TestGetPairs() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var hashkey = Me(); - db.KeyDeleteAsync(hashkey); + _ = db.KeyDeleteAsync(hashkey); var result0 = db.HashGetAllAsync(hashkey); - db.HashSetAsync(hashkey, "foo", "abc"); - db.HashSetAsync(hashkey, "bar", "def"); + _ = db.HashSetAsync(hashkey, "foo", "abc"); + _ = db.HashSetAsync(hashkey, "bar", "def"); var result1 = db.HashGetAllAsync(hashkey); @@ -651,13 +647,13 @@ public void TestGetPairs() /// Tests for . /// [Fact] - public void TestSetPairs() + public async Task TestSetPairs() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var hashkey = Me(); - db.KeyDeleteAsync(hashkey).ForAwait(); + _ = db.KeyDeleteAsync(hashkey).ForAwait(); var result0 = db.HashGetAllAsync(hashkey); @@ -666,7 +662,7 @@ public void TestSetPairs() new HashEntry("foo", Encoding.UTF8.GetBytes("abc")), new HashEntry("bar", Encoding.UTF8.GetBytes("def")), }; - db.HashSetAsync(hashkey, data).ForAwait(); + _ = db.HashSetAsync(hashkey, data).ForAwait(); var result1 = db.Wait(db.HashGetAllAsync(hashkey)); @@ -680,7 +676,7 @@ public void TestSetPairs() [Fact] public async Task TestWhenAlwaysAsync() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var hashkey = Me(); @@ -701,7 +697,7 @@ public async Task TestWhenAlwaysAsync() [Fact] public async Task HashRandomFieldAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var hashKey = Me(); @@ -727,9 +723,9 @@ public async Task HashRandomFieldAsync() } [Fact] - public void HashRandomField() + public async Task HashRandomField() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var hashKey = Me(); @@ -755,9 +751,9 @@ public void HashRandomField() } [Fact] - public void HashRandomFieldEmptyHash() + public async Task HashRandomFieldEmptyHash() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var hashKey = Me(); diff --git a/tests/StackExchange.Redis.Tests/HeartbeatTests.cs b/tests/StackExchange.Redis.Tests/HeartbeatTests.cs new file mode 100644 index 000000000..4de271f9a --- /dev/null +++ b/tests/StackExchange.Redis.Tests/HeartbeatTests.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace StackExchange.Redis.Tests; + +[RunPerProtocol] +public class HeartbeatTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) +{ + [Fact] + public async Task TestAutomaticHeartbeat() + { + RedisValue oldTimeout = RedisValue.Null; + await using var configConn = Create(allowAdmin: true); + + try + { + configConn.GetDatabase(); + var srv = GetAnyPrimary(configConn); + oldTimeout = srv.ConfigGet("timeout")[0].Value; + Log("Old Timeout: " + oldTimeout); + srv.ConfigSet("timeout", 3); + + await using var innerConn = Create(); + var innerDb = innerConn.GetDatabase(); + await innerDb.PingAsync(); // need to wait to pick up configuration etc + + var before = innerConn.OperationCount; + + Log("sleeping to test heartbeat..."); + await Task.Delay(TimeSpan.FromSeconds(5)).ForAwait(); + + var after = innerConn.OperationCount; + Assert.True(after >= before + 1, $"after: {after}, before: {before}"); + } + finally + { + if (!oldTimeout.IsNull) + { + Log("Resetting old timeout: " + oldTimeout); + var srv = GetAnyPrimary(configConn); + srv.ConfigSet("timeout", oldTimeout); + } + } + } +} diff --git a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs index ab0583764..a3386e80c 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Attributes.cs @@ -3,11 +3,18 @@ using System.Globalization; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Xunit.Abstractions; +using Xunit; +using Xunit.Internal; using Xunit.Sdk; +using Xunit.v3; +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable SA1502 // Element should not be on a single line +#pragma warning disable SA1649 // File name should match first type name +#pragma warning disable IDE0130 // Namespace does not match folder structure namespace StackExchange.Redis.Tests; /// @@ -19,18 +26,8 @@ namespace StackExchange.Redis.Tests; /// /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] -[XunitTestCaseDiscoverer("StackExchange.Redis.Tests.FactDiscoverer", "StackExchange.Redis.Tests")] -public class FactAttribute : Xunit.FactAttribute { } - -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] -public class FactLongRunningAttribute : FactAttribute -{ - public override string Skip - { - get => TestConfig.Current.RunLongRunning ? base.Skip : "Config.RunLongRunning is false - skipping long test."; - set => base.Skip = value; - } -} +[XunitTestCaseDiscoverer(typeof(FactDiscoverer))] +public class FactAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = -1) : Xunit.FactAttribute(sourceFilePath, sourceLineNumber) { } /// /// Override for that truncates our DisplayName down. @@ -43,223 +40,134 @@ public override string Skip /// /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] -[XunitTestCaseDiscoverer("StackExchange.Redis.Tests.TheoryDiscoverer", "StackExchange.Redis.Tests")] -public class TheoryAttribute : Xunit.TheoryAttribute { } +[XunitTestCaseDiscoverer(typeof(TheoryDiscoverer))] +public class TheoryAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = -1) : Xunit.TheoryAttribute(sourceFilePath, sourceLineNumber) { } -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] -public class TheoryLongRunningAttribute : Xunit.TheoryAttribute +public class FactDiscoverer : Xunit.v3.FactDiscoverer { - public override string Skip - { - get => TestConfig.Current.RunLongRunning ? base.Skip : "Config.RunLongRunning is false - skipping long test."; - set => base.Skip = value; - } + public override ValueTask> Discover(ITestFrameworkDiscoveryOptions discoveryOptions, IXunitTestMethod testMethod, IFactAttribute factAttribute) + => base.Discover(discoveryOptions, testMethod, factAttribute).ExpandAsync(); } -public class FactDiscoverer : Xunit.Sdk.FactDiscoverer +public class TheoryDiscoverer : Xunit.v3.TheoryDiscoverer { - public FactDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) { } + protected override ValueTask> CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, IXunitTestMethod testMethod, ITheoryAttribute theoryAttribute, ITheoryDataRow dataRow, object?[] testMethodArguments) + => base.CreateTestCasesForDataRow(discoveryOptions, testMethod, theoryAttribute, dataRow, testMethodArguments).ExpandAsync(); - public override IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) - { - if (testMethod.Method.GetParameters().Any()) - { - return new[] { new ExecutionErrorTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, "[Fact] methods are not allowed to have parameters. Did you mean to use [Theory]?") }; - } - else if (testMethod.Method.IsGenericMethodDefinition) - { - return new[] { new ExecutionErrorTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, "[Fact] methods are not allowed to be generic.") }; - } - else - { - return testMethod.Expand(protocol => new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, protocol: protocol)); - } - } + protected override ValueTask> CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, IXunitTestMethod testMethod, ITheoryAttribute theoryAttribute) + => base.CreateTestCasesForTheory(discoveryOptions, testMethod, theoryAttribute).ExpandAsync(); } -public class TheoryDiscoverer : Xunit.Sdk.TheoryDiscoverer -{ - public TheoryDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) { } - - protected override IEnumerable CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow) - => testMethod.Expand(protocol => new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, dataRow, protocol: protocol)); - - protected override IEnumerable CreateTestCasesForSkip(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, string skipReason) - => testMethod.Expand(protocol => new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, protocol: protocol)); - - protected override IEnumerable CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) - => testMethod.Expand(protocol => new SkippableTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, protocol: protocol)); +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] +public class RunPerProtocol() : Attribute { } - protected override IEnumerable CreateTestCasesForSkippedDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow, string skipReason) - => new[] { new NamedSkippedDataRowTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, skipReason, dataRow) }; +public interface IProtocolTestCase +{ + RedisProtocol Protocol { get; } } -public class SkippableTestCase : XunitTestCase, IRedisTest +public class ProtocolTestCase : XunitTestCase, IProtocolTestCase { - public RedisProtocol Protocol { get; set; } - public string ProtocolString => Protocol switch - { - RedisProtocol.Resp2 => "RESP2", - RedisProtocol.Resp3 => "RESP3", - _ => "UnknownProtocolFixMeeeeee", - }; - - protected override string GetUniqueID() => base.GetUniqueID() + ProtocolString; - - protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => - base.GetDisplayName(factAttribute, displayName).StripName() + "(" + ProtocolString + ")"; + public RedisProtocol Protocol { get; private set; } [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] - public SkippableTestCase() { } - - public SkippableTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[]? testMethodArguments = null, RedisProtocol? protocol = null) - : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) - { - // TODO: Default RESP2 somewhere cleaner - Protocol = protocol ?? RedisProtocol.Resp2; - } - - public override void Serialize(IXunitSerializationInfo data) + public ProtocolTestCase() { } + + public ProtocolTestCase(XunitTestCase testCase, RedisProtocol protocol) : base( + testMethod: testCase.TestMethod, + testCaseDisplayName: $"{testCase.TestCaseDisplayName.Replace("StackExchange.Redis.Tests.", "")} ({protocol.GetString()})", + uniqueID: testCase.UniqueID + protocol.GetString(), + @explicit: testCase.Explicit, + skipExceptions: testCase.SkipExceptions, + skipReason: testCase.SkipReason, + skipType: testCase.SkipType, + skipUnless: testCase.SkipUnless, + skipWhen: testCase.SkipWhen, + traits: testCase.TestMethod.Traits.ToReadWrite(StringComparer.OrdinalIgnoreCase), + testMethodArguments: testCase.TestMethodArguments, + sourceFilePath: testCase.SourceFilePath, + sourceLineNumber: testCase.SourceLineNumber, + timeout: testCase.Timeout) + => Protocol = protocol; + + protected override void Serialize(IXunitSerializationInfo data) { - data.AddValue(nameof(Protocol), (int)Protocol); base.Serialize(data); + data.AddValue("resp", (int)Protocol); } - public override void Deserialize(IXunitSerializationInfo data) + protected override void Deserialize(IXunitSerializationInfo data) { - Protocol = (RedisProtocol)data.GetValue(nameof(Protocol)); base.Deserialize(data); - } - - public override async Task RunAsync( - IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - object[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) - { - var skipMessageBus = new SkippableMessageBus(messageBus); - TestBase.SetContext(new TestContext(this)); - var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource).ForAwait(); - return result.Update(skipMessageBus); + Protocol = (RedisProtocol)data.GetValue("resp"); } } -public class SkippableTheoryTestCase : XunitTheoryTestCase, IRedisTest +public class ProtocolDelayEnumeratedTestCase : XunitDelayEnumeratedTheoryTestCase, IProtocolTestCase { - public RedisProtocol Protocol { get; set; } - - protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => - base.GetDisplayName(factAttribute, displayName).StripName(); + public RedisProtocol Protocol { get; private set; } [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] - public SkippableTheoryTestCase() { } - - public SkippableTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, RedisProtocol? protocol = null) - : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) + public ProtocolDelayEnumeratedTestCase() { } + + public ProtocolDelayEnumeratedTestCase(XunitDelayEnumeratedTheoryTestCase testCase, RedisProtocol protocol) : base( + testMethod: testCase.TestMethod, + testCaseDisplayName: $"{testCase.TestCaseDisplayName.Replace("StackExchange.Redis.Tests.", "")} ({protocol.GetString()})", + uniqueID: testCase.UniqueID + protocol.GetString(), + @explicit: testCase.Explicit, + skipTestWithoutData: testCase.SkipTestWithoutData, + skipExceptions: testCase.SkipExceptions, + skipReason: testCase.SkipReason, + skipType: testCase.SkipType, + skipUnless: testCase.SkipUnless, + skipWhen: testCase.SkipWhen, + traits: testCase.TestMethod.Traits.ToReadWrite(StringComparer.OrdinalIgnoreCase), + sourceFilePath: testCase.SourceFilePath, + sourceLineNumber: testCase.SourceLineNumber, + timeout: testCase.Timeout) + => Protocol = protocol; + + protected override void Serialize(IXunitSerializationInfo data) { - // TODO: Default RESP2 somewhere cleaner - Protocol = protocol ?? RedisProtocol.Resp2; - } - - public override async Task RunAsync( - IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - object[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) - { - var skipMessageBus = new SkippableMessageBus(messageBus); - TestBase.SetContext(new TestContext(this)); - var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource).ForAwait(); - return result.Update(skipMessageBus); - } -} - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] -public class RunPerProtocol : Attribute -{ - public static RedisProtocol[] AllProtocols { get; } = new[] { RedisProtocol.Resp2, RedisProtocol.Resp3 }; - - public RedisProtocol[] Protocols { get; } - public RunPerProtocol(params RedisProtocol[] procotols) => Protocols = procotols ?? AllProtocols; -} - -public class NamedSkippedDataRowTestCase : XunitSkippedDataRowTestCase -{ - protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => - base.GetDisplayName(factAttribute, displayName).StripName(); - - [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] - public NamedSkippedDataRowTestCase() { } - - public NamedSkippedDataRowTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, string skipReason, object[]? testMethodArguments = null) - : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, skipReason, testMethodArguments) { } -} - -public class SkippableMessageBus : IMessageBus -{ - private readonly IMessageBus InnerBus; - public SkippableMessageBus(IMessageBus innerBus) => InnerBus = innerBus; - - public int DynamicallySkippedTestCount { get; private set; } - - public void Dispose() - { - InnerBus.Dispose(); - GC.SuppressFinalize(this); + base.Serialize(data); + data.AddValue("resp", (int)Protocol); } - public bool QueueMessage(IMessageSinkMessage message) + protected override void Deserialize(IXunitSerializationInfo data) { - if (message is ITestFailed testFailed) - { - var exceptionType = testFailed.ExceptionTypes.FirstOrDefault(); - if (exceptionType == typeof(SkipTestException).FullName) - { - DynamicallySkippedTestCount++; - return InnerBus.QueueMessage(new TestSkipped(testFailed.Test, testFailed.Messages.FirstOrDefault())); - } - } - return InnerBus.QueueMessage(message); + base.Deserialize(data); + Protocol = (RedisProtocol)data.GetValue("resp"); } } internal static class XUnitExtensions { - internal static string StripName(this string name) => - name.Replace("StackExchange.Redis.Tests.", ""); - - public static RunSummary Update(this RunSummary summary, SkippableMessageBus bus) + public static async ValueTask> ExpandAsync(this ValueTask> discovery) { - if (bus.DynamicallySkippedTestCount > 0) + static IXunitTestCase CreateTestCase(XunitTestCase tc, RedisProtocol protocol) => tc switch { - summary.Failed -= bus.DynamicallySkippedTestCount; - summary.Skipped += bus.DynamicallySkippedTestCount; - } - return summary; - } - - public static IEnumerable Expand(this ITestMethod testMethod, Func generator) - { - if ((testMethod.Method.GetCustomAttributes(typeof(RunPerProtocol)).FirstOrDefault() - ?? testMethod.TestClass.Class.GetCustomAttributes(typeof(RunPerProtocol)).FirstOrDefault()) is IAttributeInfo attr) + XunitDelayEnumeratedTheoryTestCase delayed => new ProtocolDelayEnumeratedTestCase(delayed, protocol), + _ => new ProtocolTestCase(tc, protocol), + }; + var testCases = await discovery; + List result = []; + foreach (var testCase in testCases.OfType()) { - // params means not null but default empty - var protocols = attr.GetNamedArgument(nameof(RunPerProtocol.Protocols)); - if (protocols.Length == 0) + var testMethod = testCase.TestMethod; + + if ((testMethod.Method.GetCustomAttributes(typeof(RunPerProtocol)).FirstOrDefault() + ?? testMethod.TestClass.Class.GetCustomAttributes(typeof(RunPerProtocol)).FirstOrDefault()) is RunPerProtocol) { - protocols = RunPerProtocol.AllProtocols; + result.Add(CreateTestCase(testCase, RedisProtocol.Resp2)); + result.Add(CreateTestCase(testCase, RedisProtocol.Resp3)); } - foreach (var protocol in protocols) + else { - yield return generator(protocol); + // Default to RESP2 everywhere else + result.Add(CreateTestCase(testCase, RedisProtocol.Resp2)); } } - else - { - yield return generator(RedisProtocol.Resp2); - } + return result; } } @@ -269,25 +177,22 @@ public static IEnumerable Expand(this ITestMethod testMethod, Fu /// /// /// Based on: https://bartwullems.blogspot.com/2022/03/xunit-change-culture-during-your-test.html. +/// Replaces the culture and UI culture of the current thread with . /// +/// The name of the culture. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] -public class TestCultureAttribute : BeforeAfterTestAttribute +public class TestCultureAttribute(string culture) : BeforeAfterTestAttribute { - private readonly CultureInfo culture; + private readonly CultureInfo culture = new CultureInfo(culture, false); private CultureInfo? originalCulture; - /// - /// Replaces the culture and UI culture of the current thread with . - /// - /// The name of the culture. - public TestCultureAttribute(string culture) => this.culture = new CultureInfo(culture, false); - /// /// Stores the current and /// and replaces them with the new cultures defined in the constructor. /// /// The method under test. - public override void Before(MethodInfo methodUnderTest) + /// The current . + public override void Before(MethodInfo methodUnderTest, IXunitTest test) { originalCulture = Thread.CurrentThread.CurrentCulture; Thread.CurrentThread.CurrentCulture = culture; @@ -298,7 +203,8 @@ public override void Before(MethodInfo methodUnderTest) /// Restores the original to . /// /// The method under test. - public override void After(MethodInfo methodUnderTest) + /// The current . + public override void After(MethodInfo methodUnderTest, IXunitTest test) { if (originalCulture is not null) { diff --git a/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs b/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs index 052129a9a..6f776d268 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Extensions.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; -using Xunit.Abstractions; +using Xunit; namespace StackExchange.Redis.Tests.Helpers; @@ -11,19 +11,7 @@ public static class Extensions static Extensions() { -#if NET462 - VersionInfo = "Compiled under .NET 4.6.2"; -#else VersionInfo = $"Running under {RuntimeInformation.FrameworkDescription} ({Environment.Version})"; -#endif - try - { - VersionInfo += "\n Running on: " + RuntimeInformation.OSDescription; - } - catch (Exception) - { - VersionInfo += "\n Failed to get OS version"; - } } public static void WriteFrameworkVersion(this ITestOutputHelper output) => output.WriteLine(VersionInfo); diff --git a/tests/StackExchange.Redis.Tests/Helpers/IRedisTest.cs b/tests/StackExchange.Redis.Tests/Helpers/IRedisTest.cs deleted file mode 100644 index 76ea5bc1b..000000000 --- a/tests/StackExchange.Redis.Tests/Helpers/IRedisTest.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Xunit.Sdk; - -namespace StackExchange.Redis.Tests; - -public interface IRedisTest : IXunitTestCase -{ - public RedisProtocol Protocol { get; set; } -} diff --git a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs index ae48ff676..cf6c7d326 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs @@ -11,13 +11,14 @@ using StackExchange.Redis.Profiling; using Xunit; +[assembly: AssemblyFixture(typeof(StackExchange.Redis.Tests.SharedConnectionFixture))] + namespace StackExchange.Redis.Tests; public class SharedConnectionFixture : IDisposable { public bool IsEnabled { get; } - public const string Key = "Shared Muxer"; private readonly ConnectionMultiplexer _actualConnection; public string Configuration { get; } @@ -68,7 +69,7 @@ static NonDisposingConnection VerifyAndWrap(IInternalConnectionMultiplexer muxer } } - internal sealed class NonDisposingConnection : IInternalConnectionMultiplexer + internal sealed class NonDisposingConnection(IInternalConnectionMultiplexer inner) : IInternalConnectionMultiplexer { public IInternalConnectionMultiplexer UnderlyingConnection => _inner; @@ -92,8 +93,7 @@ public bool IgnoreConnect public ConnectionMultiplexer UnderlyingMultiplexer => _inner.UnderlyingMultiplexer; - private readonly IInternalConnectionMultiplexer _inner; - public NonDisposingConnection(IInternalConnectionMultiplexer inner) => _inner = inner; + private readonly IInternalConnectionMultiplexer _inner = inner; public int GetSubscriptionsCount() => _inner.GetSubscriptionsCount(); public ConcurrentDictionary GetSubscriptions() => _inner.GetSubscriptions(); @@ -255,7 +255,7 @@ protected void OnConnectionFailed(object? sender, ConnectionFailedEventArgs e) privateExceptions.Add($"{TestBase.Time()}: Connection failed ({e.FailureType}): {EndPointCollection.ToString(e.EndPoint)}/{e.ConnectionType}: {e.Exception}"); } } - private readonly List privateExceptions = new List(); + private readonly List privateExceptions = []; private int privateFailCount; public void Teardown(TextWriter output) @@ -288,14 +288,3 @@ public void Teardown(TextWriter output) } } } - -/// -/// See . -/// -[CollectionDefinition(SharedConnectionFixture.Key)] -public class ConnectionCollection : ICollectionFixture -{ - // This class has no code, and is never created. Its purpose is simply - // to be the place to apply [CollectionDefinition] and all the - // ICollectionFixture<> interfaces. -} diff --git a/tests/StackExchange.Redis.Tests/Helpers/Skip.cs b/tests/StackExchange.Redis.Tests/Helpers/Skip.cs index 8627dbda2..72d62a3dc 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/Skip.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/Skip.cs @@ -1,30 +1,29 @@ using System; using System.Diagnostics.CodeAnalysis; +using Xunit; namespace StackExchange.Redis.Tests; public static class Skip { - public static void Inconclusive(string message) => throw new SkipTestException(message); + public static void UnlessLongRunning() + { + Assert.SkipUnless(TestConfig.Current.RunLongRunning, "Skipping long-running test"); + } public static void IfNoConfig(string prop, [NotNull] string? value) { - if (value.IsNullOrEmpty()) - { - throw new SkipTestException($"Config.{prop} is not set, skipping test."); - } + Assert.SkipWhen(value.IsNullOrEmpty(), $"Config.{prop} is not set, skipping test."); } internal static void IfMissingDatabase(IConnectionMultiplexer conn, int dbId) { var dbCount = conn.GetServer(conn.GetEndPoints()[0]).DatabaseCount; - if (dbId >= dbCount) throw new SkipTestException($"Database '{dbId}' is not supported on this server."); + Assert.SkipWhen(dbId >= dbCount, $"Database '{dbId}' is not supported on this server."); } } -public class SkipTestException : Exception +public class SkipTestException(string reason) : Exception(reason) { public string? MissingFeatures { get; set; } - - public SkipTestException(string reason) : base(reason) { } } diff --git a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs index fafb30543..c0194d5a6 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TestConfig.cs @@ -9,11 +9,15 @@ namespace StackExchange.Redis.Tests; public static class TestConfig { - private const string FileName = "TestConfig.json"; + private const string FileName = "RedisTestConfig.json"; public static Config Current { get; } +#if NET private static int _db = 17; +#else + private static int _db = 77; +#endif public static int GetDedicatedDB(IConnectionMultiplexer? conn = null) { int db = Interlocked.Increment(ref _db); @@ -65,7 +69,6 @@ public class Config { public bool UseSharedConnection { get; set; } = true; public bool RunLongRunning { get; set; } - public bool LogToConsole { get; set; } public string PrimaryServer { get; set; } = "127.0.0.1"; public int PrimaryPort { get; set; } = 6379; diff --git a/tests/StackExchange.Redis.Tests/Helpers/TestContext.cs b/tests/StackExchange.Redis.Tests/Helpers/TestContext.cs deleted file mode 100644 index 799f753b4..000000000 --- a/tests/StackExchange.Redis.Tests/Helpers/TestContext.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace StackExchange.Redis.Tests; - -public class TestContext -{ - public IRedisTest Test { get; set; } - - public bool IsResp2 => Test.Protocol == RedisProtocol.Resp2; - public bool IsResp3 => Test.Protocol == RedisProtocol.Resp3; - - public string KeySuffix => Test.Protocol switch - { - RedisProtocol.Resp2 => "R2", - RedisProtocol.Resp3 => "R3", - _ => "", - }; - - public TestContext(IRedisTest test) => Test = test; - - public override string ToString() => $"Protocol: {Test.Protocol}"; -} diff --git a/tests/StackExchange.Redis.Tests/Helpers/TestExtensions.cs b/tests/StackExchange.Redis.Tests/Helpers/TestExtensions.cs index 504f260a4..aab965f98 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TestExtensions.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TestExtensions.cs @@ -1,4 +1,5 @@ using StackExchange.Redis.Profiling; +using Xunit; namespace StackExchange.Redis.Tests; @@ -10,4 +11,25 @@ public static ProfilingSession AddProfiler(this IConnectionMultiplexer mutex) mutex.RegisterProfiler(() => session); return session; } + + public static RedisProtocol GetProtocol(this ITestContext context) => + context.Test?.TestCase is IProtocolTestCase protocolTestCase + ? protocolTestCase.Protocol : RedisProtocol.Resp2; + + public static bool IsResp2(this ITestContext context) => GetProtocol(context) == RedisProtocol.Resp2; + public static bool IsResp3(this ITestContext context) => GetProtocol(context) == RedisProtocol.Resp3; + + public static string KeySuffix(this ITestContext context) => GetProtocol(context) switch + { + RedisProtocol.Resp2 => "R2", + RedisProtocol.Resp3 => "R3", + _ => "", + }; + + public static string GetString(this RedisProtocol protocol) => protocol switch + { + RedisProtocol.Resp2 => "RESP2", + RedisProtocol.Resp3 => "RESP3", + _ => "UnknownProtocolFixMeeeeee", + }; } diff --git a/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs b/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs index fe54c472c..e41a46670 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs @@ -1,22 +1,16 @@ using System; using System.IO; using System.Text; -using Xunit.Abstractions; +using Xunit; namespace StackExchange.Redis.Tests.Helpers; -public class TextWriterOutputHelper : TextWriter +public class TextWriterOutputHelper(ITestOutputHelper outputHelper) : TextWriter { private StringBuilder Buffer { get; } = new StringBuilder(2048); private StringBuilder? Echo { get; set; } public override Encoding Encoding => Encoding.UTF8; - private readonly ITestOutputHelper Output; - private readonly bool ToConsole; - public TextWriterOutputHelper(ITestOutputHelper outputHelper, bool echoToConsole) - { - Output = outputHelper; - ToConsole = echoToConsole; - } + private readonly ITestOutputHelper Output = outputHelper; public void EchoTo(StringBuilder sb) => Echo = sb; @@ -90,10 +84,6 @@ private void FlushBuffer() // Thrown when writing from a handler after a test has ended - just bail in this case } Echo?.AppendLine(text); - if (ToConsole) - { - Console.WriteLine(text); - } Buffer.Clear(); } } diff --git a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs index fd614fa93..557562b71 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/redis-sharp.cs @@ -23,7 +23,7 @@ namespace RedisSharp { - public class Redis : IDisposable + public class Redis(string host, int port) : IDisposable { private Socket socket; private BufferedStream bstream; @@ -36,27 +36,19 @@ public enum KeyType Set, } - public class ResponseException : Exception + public class ResponseException(string code) : Exception("Response error") { - public string Code { get; } - public ResponseException(string code) : base("Response error") => Code = code; - } - - public Redis(string host, int port) - { - Host = host ?? throw new ArgumentNullException(nameof(host)); - Port = port; - SendTimeout = -1; + public string Code { get; } = code; } public Redis(string host) : this(host, 6379) { } public Redis() : this("localhost", 6379) { } - public string Host { get; } - public int Port { get; } + public string Host { get; } = host ?? throw new ArgumentNullException(nameof(host)); + public int Port { get; } = port; public int RetryTimeout { get; set; } public int RetryCount { get; set; } - public int SendTimeout { get; set; } + public int SendTimeout { get; set; } = -1; public string Password { get; set; } private int db; @@ -235,7 +227,7 @@ private void Connect() SendExpectSuccess("AUTH {0}\r\n", Password); } - private readonly byte[] endData = new byte[] { (byte)'\r', (byte)'\n' }; + private readonly byte[] endData = [(byte)'\r', (byte)'\n']; private bool SendDataCommand(byte[] data, string cmd, params object[] args) { diff --git a/tests/StackExchange.Redis.Tests/HighIntegrityBasicOpsTests.cs b/tests/StackExchange.Redis.Tests/HighIntegrityBasicOpsTests.cs new file mode 100644 index 000000000..d7b85cd62 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/HighIntegrityBasicOpsTests.cs @@ -0,0 +1,8 @@ +using Xunit; + +namespace StackExchange.Redis.Tests; + +public class HighIntegrityBasicOpsTests(ITestOutputHelper output, SharedConnectionFixture fixture) : BasicOpsTests(output, fixture) +{ + internal override bool HighIntegrity => true; +} diff --git a/tests/StackExchange.Redis.Tests/HttpTunnelConnectTests.cs b/tests/StackExchange.Redis.Tests/HttpTunnelConnectTests.cs index 2c1dc1ec6..4099c7b94 100644 --- a/tests/StackExchange.Redis.Tests/HttpTunnelConnectTests.cs +++ b/tests/StackExchange.Redis.Tests/HttpTunnelConnectTests.cs @@ -2,14 +2,12 @@ using System.Diagnostics; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests { - public class HttpTunnelConnectTests + public class HttpTunnelConnectTests(ITestOutputHelper log) { - private ITestOutputHelper Log { get; } - public HttpTunnelConnectTests(ITestOutputHelper log) => Log = log; + private ITestOutputHelper Log { get; } = log; [Theory] [InlineData("")] @@ -19,7 +17,7 @@ public async Task Connect(string suffix) var cs = Environment.GetEnvironmentVariable("HACK_TUNNEL_ENDPOINT"); if (string.IsNullOrWhiteSpace(cs)) { - Skip.Inconclusive("Need HACK_TUNNEL_ENDPOINT environment variable"); + Assert.Skip("Need HACK_TUNNEL_ENDPOINT environment variable"); } var config = ConfigurationOptions.Parse(cs + suffix); if (!string.IsNullOrWhiteSpace(suffix)) diff --git a/tests/StackExchange.Redis.Tests/HyperLogLogTests.cs b/tests/StackExchange.Redis.Tests/HyperLogLogTests.cs index e0451e9c5..f4c259854 100644 --- a/tests/StackExchange.Redis.Tests/HyperLogLogTests.cs +++ b/tests/StackExchange.Redis.Tests/HyperLogLogTests.cs @@ -1,18 +1,15 @@ -using Xunit; -using Xunit.Abstractions; +using System.Threading.Tasks; +using Xunit; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class HyperLogLogTests : TestBase +public class HyperLogLogTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public HyperLogLogTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void SingleKeyLength() + public async Task SingleKeyLength() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = "hll1"; @@ -25,12 +22,12 @@ public void SingleKeyLength() } [Fact] - public void MultiKeyLength() + public async Task MultiKeyLength() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); - RedisKey[] keys = { "hll1", "hll2", "hll3" }; + RedisKey[] keys = ["hll1", "hll2", "hll3"]; db.HyperLogLogAdd(keys[0], "a"); db.HyperLogLogAdd(keys[1], "b"); diff --git a/tests/StackExchange.Redis.Tests/InfoReplicationCheckTests.cs b/tests/StackExchange.Redis.Tests/InfoReplicationCheckTests.cs index 547d3cc88..03b9f5b7f 100644 --- a/tests/StackExchange.Redis.Tests/InfoReplicationCheckTests.cs +++ b/tests/StackExchange.Redis.Tests/InfoReplicationCheckTests.cs @@ -1,20 +1,18 @@ using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class InfoReplicationCheckTests : TestBase +public class InfoReplicationCheckTests(ITestOutputHelper output) : TestBase(output) { protected override string GetConfiguration() => base.GetConfiguration() + ",configCheckSeconds=2"; - public InfoReplicationCheckTests(ITestOutputHelper output) : base(output) { } [Fact] public async Task Exec() { - Skip.Inconclusive("need to think about CompletedSynchronously"); + Assert.Skip("need to think about CompletedSynchronously"); - using var conn = Create(); + await using var conn = Create(); var parsed = ConfigurationOptions.Parse(conn.Configuration); Assert.Equal(2, parsed.ConfigCheckSeconds); diff --git a/tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs b/tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs index 2b4a8797e..15e4c6ef3 100644 --- a/tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/BgSaveResponseTests.cs @@ -1,19 +1,16 @@ using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues; -public class BgSaveResponseTests : TestBase +public class BgSaveResponseTests(ITestOutputHelper output) : TestBase(output) { - public BgSaveResponseTests(ITestOutputHelper output) : base(output) { } - [Theory(Skip = "We don't need to test this, and it really screws local testing hard.")] [InlineData(SaveType.BackgroundSave)] [InlineData(SaveType.BackgroundRewriteAppendOnlyFile)] public async Task ShouldntThrowException(SaveType saveType) { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); var server = GetServer(conn); server.Save(saveType); diff --git a/tests/StackExchange.Redis.Tests/Issues/DefaultDatabaseTests.cs b/tests/StackExchange.Redis.Tests/Issues/DefaultDatabaseTests.cs index 5514bc5c4..9666c91a2 100644 --- a/tests/StackExchange.Redis.Tests/Issues/DefaultDatabaseTests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/DefaultDatabaseTests.cs @@ -1,13 +1,11 @@ using System.IO; +using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues; -public class DefaultDatabaseTests : TestBase +public class DefaultDatabaseTests(ITestOutputHelper output) : TestBase(output) { - public DefaultDatabaseTests(ITestOutputHelper output) : base(output) { } - [Fact] public void UnspecifiedDbId_ReturnsNull() { @@ -23,12 +21,12 @@ public void SpecifiedDbId_ReturnsExpected() } [Fact] - public void ConfigurationOptions_UnspecifiedDefaultDb() + public async Task ConfigurationOptions_UnspecifiedDefaultDb() { var log = new StringWriter(); try { - using var conn = ConnectionMultiplexer.Connect(TestConfig.Current.PrimaryServerAndPort, log); + await using var conn = await ConnectionMultiplexer.ConnectAsync(TestConfig.Current.PrimaryServerAndPort, log); var db = conn.GetDatabase(); Assert.Equal(0, db.Database); } @@ -39,12 +37,12 @@ public void ConfigurationOptions_UnspecifiedDefaultDb() } [Fact] - public void ConfigurationOptions_SpecifiedDefaultDb() + public async Task ConfigurationOptions_SpecifiedDefaultDb() { var log = new StringWriter(); try { - using var conn = ConnectionMultiplexer.Connect($"{TestConfig.Current.PrimaryServerAndPort},defaultDatabase=3", log); + await using var conn = await ConnectionMultiplexer.ConnectAsync($"{TestConfig.Current.PrimaryServerAndPort},defaultDatabase=3", log); var db = conn.GetDatabase(); Assert.Equal(3, db.Database); } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue10Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue10Tests.cs index af95cd71e..0a2f3fa8f 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue10Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue10Tests.cs @@ -1,26 +1,24 @@ -using Xunit; -using Xunit.Abstractions; +using System.Threading.Tasks; +using Xunit; namespace StackExchange.Redis.Tests.Issues; -public class Issue10Tests : TestBase +public class Issue10Tests(ITestOutputHelper output) : TestBase(output) { - public Issue10Tests(ITestOutputHelper output) : base(output) { } - [Fact] - public void Execute() + public async Task Execute() { - using var conn = Create(); + await using var conn = Create(); var key = Me(); var db = conn.GetDatabase(); - db.KeyDeleteAsync(key); // contents: nil - db.ListLeftPushAsync(key, "abc"); // "abc" - db.ListLeftPushAsync(key, "def"); // "def", "abc" - db.ListLeftPushAsync(key, "ghi"); // "ghi", "def", "abc", - db.ListSetByIndexAsync(key, 1, "jkl"); // "ghi", "jkl", "abc" + _ = db.KeyDeleteAsync(key); // contents: nil + _ = db.ListLeftPushAsync(key, "abc"); // "abc" + _ = db.ListLeftPushAsync(key, "def"); // "def", "abc" + _ = db.ListLeftPushAsync(key, "ghi"); // "ghi", "def", "abc", + _ = db.ListSetByIndexAsync(key, 1, "jkl"); // "ghi", "jkl", "abc" - var contents = db.Wait(db.ListRangeAsync(key, 0, -1)); + var contents = await db.ListRangeAsync(key, 0, -1); Assert.Equal(3, contents.Length); Assert.Equal("ghi", contents[0]); Assert.Equal("jkl", contents[1]); diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue1101Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue1101Tests.cs index 3f248b480..b0d9b9027 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue1101Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue1101Tests.cs @@ -4,14 +4,11 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues; -public class Issue1101Tests : TestBase +public class Issue1101Tests(ITestOutputHelper output) : TestBase(output) { - public Issue1101Tests(ITestOutputHelper output) : base(output) { } - private static void AssertCounts(ISubscriber pubsub, in RedisChannel channel, bool has, int handlers, int queues) { if (pubsub.Multiplexer is ConnectionMultiplexer muxer) @@ -26,7 +23,7 @@ private static void AssertCounts(ISubscriber pubsub, in RedisChannel channel, bo [Fact] public async Task ExecuteWithUnsubscribeViaChannel() { - using var conn = Create(log: Writer); + await using var conn = Create(log: Writer); RedisChannel name = RedisChannel.Literal(Me()); var pubsub = conn.GetSubscriber(); @@ -91,7 +88,7 @@ public async Task ExecuteWithUnsubscribeViaChannel() [Fact] public async Task ExecuteWithUnsubscribeViaSubscriber() { - using var conn = Create(shared: false, log: Writer); + await using var conn = Create(shared: false, log: Writer); RedisChannel name = RedisChannel.Literal(Me()); var pubsub = conn.GetSubscriber(); @@ -142,7 +139,7 @@ public async Task ExecuteWithUnsubscribeViaSubscriber() [Fact] public async Task ExecuteWithUnsubscribeViaClearAll() { - using var conn = Create(log: Writer); + await using var conn = Create(log: Writer); RedisChannel name = RedisChannel.Literal(Me()); var pubsub = conn.GetSubscriber(); diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue1103Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue1103Tests.cs index 4d9ff3731..ab4042042 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue1103Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue1103Tests.cs @@ -1,14 +1,12 @@ using System.Globalization; +using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; using static StackExchange.Redis.RedisValue; namespace StackExchange.Redis.Tests.Issues; -public class Issue1103Tests : TestBase +public class Issue1103Tests(ITestOutputHelper output) : TestBase(output) { - public Issue1103Tests(ITestOutputHelper output) : base(output) { } - [Theory] [InlineData(142205255210238005UL, (int)StorageType.Int64)] [InlineData(ulong.MaxValue, (int)StorageType.UInt64)] @@ -16,9 +14,9 @@ public Issue1103Tests(ITestOutputHelper output) : base(output) { } [InlineData(0x8000000000000000UL, (int)StorageType.UInt64)] [InlineData(0x8000000000000001UL, (int)StorageType.UInt64)] [InlineData(0x7FFFFFFFFFFFFFFFUL, (int)StorageType.Int64)] - public void LargeUInt64StoredCorrectly(ulong value, int storageType) + public async Task LargeUInt64StoredCorrectly(ulong value, int storageType) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(); var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue182Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue182Tests.cs index 396b40b5f..e60332603 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue182Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue182Tests.cs @@ -2,20 +2,18 @@ using System.Linq; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues; -public class Issue182Tests : TestBase +public class Issue182Tests(ITestOutputHelper output) : TestBase(output) { protected override string GetConfiguration() => $"{TestConfig.Current.PrimaryServerAndPort},responseTimeout=10000"; - public Issue182Tests(ITestOutputHelper output) : base(output) { } - - [FactLongRunning] + [Fact] public async Task SetMembers() { - using var conn = Create(syncTimeout: 20000); + Skip.UnlessLongRunning(); + await using var conn = Create(syncTimeout: 20000); conn.ConnectionFailed += (s, a) => { @@ -41,10 +39,11 @@ public async Task SetMembers() Assert.Equal(count, result.Length); // SMEMBERS result length } - [FactLongRunning] + [Fact] public async Task SetUnion() { - using var conn = Create(syncTimeout: 10000); + Skip.UnlessLongRunning(); + await using var conn = Create(syncTimeout: 10000); var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2176Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2176Tests.cs index 6ec0b86f5..39edd91d1 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue2176Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2176Tests.cs @@ -2,18 +2,15 @@ using System.Linq; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues { - public class Issue2176Tests : TestBase + public class Issue2176Tests(ITestOutputHelper output) : TestBase(output) { - public Issue2176Tests(ITestOutputHelper output) : base(output) { } - [Fact] - public void Execute_Batch() + public async Task Execute_Batch() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var me = Me(); @@ -29,12 +26,12 @@ public void Execute_Batch() var tasks = new List(); var batch = db.CreateBatch(); tasks.Add(batch.SortedSetAddAsync(key2, "a", 4567)); - tasks.Add(batch.SortedSetCombineAndStoreAsync(SetOperation.Intersect, keyIntersect, new RedisKey[] { key, key2 })); + tasks.Add(batch.SortedSetCombineAndStoreAsync(SetOperation.Intersect, keyIntersect, [key, key2])); var rangeByRankTask = batch.SortedSetRangeByRankAsync(keyIntersect); tasks.Add(rangeByRankTask); batch.Execute(); - Task.WhenAll(tasks.ToArray()); + await Task.WhenAll(tasks.ToArray()); var rangeByRankSortedSetValues = rangeByRankTask.Result; @@ -45,9 +42,9 @@ public void Execute_Batch() } [Fact] - public void Execute_Transaction() + public async Task Execute_Transaction() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var me = Me(); @@ -63,12 +60,12 @@ public void Execute_Transaction() var tasks = new List(); var batch = db.CreateTransaction(); tasks.Add(batch.SortedSetAddAsync(key2, "a", 4567)); - tasks.Add(batch.SortedSetCombineAndStoreAsync(SetOperation.Intersect, keyIntersect, new RedisKey[] { key, key2 })); + tasks.Add(batch.SortedSetCombineAndStoreAsync(SetOperation.Intersect, keyIntersect, [key, key2])); var rangeByRankTask = batch.SortedSetRangeByRankAsync(keyIntersect); tasks.Add(rangeByRankTask); batch.Execute(); - Task.WhenAll(tasks.ToArray()); + await Task.WhenAll(tasks.ToArray()); var rangeByRankSortedSetValues = rangeByRankTask.Result; diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2392Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2392Tests.cs index fe3e9673d..39df94021 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue2392Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2392Tests.cs @@ -1,14 +1,11 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues { - public class Issue2392Tests : TestBase + public class Issue2392Tests(ITestOutputHelper output) : TestBase(output) { - public Issue2392Tests(ITestOutputHelper output) : base(output) { } - [Fact] public async Task Execute() { @@ -28,7 +25,7 @@ public async Task Execute() }; options.EndPoints.Add("127.0.0.1:1234"); - using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); var key = Me(); var db = conn.GetDatabase(); var server = conn.GetServerSnapshot()[0]; diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2418.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2418.cs index a22bbbcbd..db38b1325 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue2418.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2418.cs @@ -1,30 +1,22 @@ using System.Linq; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues; -public class Issue2418 : TestBase +public class Issue2418(ITestOutputHelper output, SharedConnectionFixture? fixture = null) : TestBase(output, fixture) { - public Issue2418(ITestOutputHelper output, SharedConnectionFixture? fixture = null) - : base(output, fixture) { } - [Fact] public async Task Execute() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); RedisValue someInt = 12; Assert.False(someInt.IsNullOrEmpty, nameof(someInt.IsNullOrEmpty) + " before"); Assert.True(someInt.IsInteger, nameof(someInt.IsInteger) + " before"); - await db.HashSetAsync(key, new[] - { - new HashEntry("some_int", someInt), - // ... - }); + await db.HashSetAsync(key, [new HashEntry("some_int", someInt)]); // check we can fetch it var entry = await db.HashGetAllAsync(key); diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs index 7d2a9b19a..b548d7031 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs @@ -1,46 +1,40 @@ -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; -namespace StackExchange.Redis.Tests.Issues +namespace StackExchange.Redis.Tests.Issues; + +[Collection(NonParallelCollection.Name)] +public class Issue2507(ITestOutputHelper output, SharedConnectionFixture? fixture = null) : TestBase(output, fixture) { - [Collection(NonParallelCollection.Name)] - public class Issue2507 : TestBase + [Fact(Explicit = true)] + public async Task Execute() { - public Issue2507(ITestOutputHelper output, SharedConnectionFixture? fixture = null) - : base(output, fixture) { } - - [Fact] - public async Task Execute() - { - using var conn = Create(shared: false); - var db = conn.GetDatabase(); - var pubsub = conn.GetSubscriber(); - var queue = await pubsub.SubscribeAsync(RedisChannel.Literal("__redis__:invalidate")); - await Task.Delay(100); - var connectionId = conn.GetConnectionId(conn.GetEndPoints().Single(), ConnectionType.Subscription); - if (connectionId is null) Skip.Inconclusive("Connection id not available"); + await using var conn = Create(shared: false); + var db = conn.GetDatabase(); + var pubsub = conn.GetSubscriber(); + var queue = await pubsub.SubscribeAsync(RedisChannel.Literal("__redis__:invalidate")); + await Task.Delay(100); + var connectionId = conn.GetConnectionId(conn.GetEndPoints().Single(), ConnectionType.Subscription); + if (connectionId is null) Assert.Skip("Connection id not available"); - string baseKey = Me(); - RedisKey key1 = baseKey + "abc", - key2 = baseKey + "ghi", - key3 = baseKey + "mno"; + string baseKey = Me(); + RedisKey key1 = baseKey + "abc", + key2 = baseKey + "ghi", + key3 = baseKey + "mno"; - await db.StringSetAsync(new KeyValuePair[] { new(key1, "def"), new(key2, "jkl"), new(key3, "pqr") }); - // this is not supported, but: we want it to at least not fail - await db.ExecuteAsync("CLIENT", "TRACKING", "on", "REDIRECT", connectionId!.Value, "BCAST"); - await db.KeyDeleteAsync(new RedisKey[] { key1, key2, key3 }); - await Task.Delay(100); - queue.Unsubscribe(); - Assert.True(queue.TryRead(out var message)); - Assert.Equal(key1, message.Message); - Assert.True(queue.TryRead(out message)); - Assert.Equal(key2, message.Message); - Assert.True(queue.TryRead(out message)); - Assert.Equal(key3, message.Message); - Assert.False(queue.TryRead(out message)); - } + await db.StringSetAsync([new(key1, "def"), new(key2, "jkl"), new(key3, "pqr")]); + // this is not supported, but: we want it to at least not fail + await db.ExecuteAsync("CLIENT", "TRACKING", "on", "REDIRECT", connectionId!.Value, "BCAST"); + await db.KeyDeleteAsync([key1, key2, key3]); + await Task.Delay(100); + queue.Unsubscribe(); + Assert.True(queue.TryRead(out var message), "Queue 1 Read failed"); + Assert.Equal(key1, message.Message); + Assert.True(queue.TryRead(out message), "Queue 2 Read failed"); + Assert.Equal(key2, message.Message); + Assert.True(queue.TryRead(out message), "Queue 3 Read failed"); + Assert.Equal(key3, message.Message); + Assert.False(queue.TryRead(out message), "Queue 4 Read succeeded"); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue25Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue25Tests.cs index 8841d81d3..05dc4d57c 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue25Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue25Tests.cs @@ -1,13 +1,10 @@ using System; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues; -public class Issue25Tests : TestBase +public class Issue25Tests(ITestOutputHelper output) : TestBase(output) { - public Issue25Tests(ITestOutputHelper output) : base(output) { } - [Fact] public void CaseInsensitive() { diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2763Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2763Tests.cs index 4da997e7d..699076118 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue2763Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2763Tests.cs @@ -2,18 +2,15 @@ using System.Collections.Generic; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues { - public class Issue2763Tests : TestBase + public class Issue2763Tests(ITestOutputHelper output) : TestBase(output) { - public Issue2763Tests(ITestOutputHelper output) : base(output) { } - [Fact] - public void Execute() + public async Task Execute() { - using var conn = Create(); + await using var conn = Create(); var subscriber = conn.GetSubscriber(); static void Handler(RedisChannel c, RedisValue v) { } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue6Tests.cs b/tests/StackExchange.Redis.Tests/Issues/Issue6Tests.cs index 3b6d4f8fc..c7c6385c0 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue6Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue6Tests.cs @@ -1,19 +1,18 @@ -using Xunit.Abstractions; +using System.Threading.Tasks; +using Xunit; namespace StackExchange.Redis.Tests.Issues; -public class Issue6Tests : TestBase +public class Issue6Tests(ITestOutputHelper output) : TestBase(output) { - public Issue6Tests(ITestOutputHelper output) : base(output) { } - [Fact] - public void ShouldWorkWithoutEchoOrPing() + public async Task ShouldWorkWithoutEchoOrPing() { - using var conn = Create(proxy: Proxy.Twemproxy); + await using var conn = Create(proxy: Proxy.Twemproxy); Log("config: " + conn.Configuration); var db = conn.GetDatabase(); - var time = db.Ping(); + var time = await db.PingAsync(); Log("ping time: " + time); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/MassiveDeleteTests.cs b/tests/StackExchange.Redis.Tests/Issues/MassiveDeleteTests.cs index bbd9171ea..94590a186 100644 --- a/tests/StackExchange.Redis.Tests/Issues/MassiveDeleteTests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/MassiveDeleteTests.cs @@ -2,17 +2,15 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; -using Xunit.Abstractions; +using Xunit; namespace StackExchange.Redis.Tests.Issues; -public class MassiveDeleteTests : TestBase +public class MassiveDeleteTests(ITestOutputHelper output) : TestBase(output) { - public MassiveDeleteTests(ITestOutputHelper output) : base(output) { } - - private void Prep(int dbId, string key) + private async Task Prep(int dbId, string key) { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); var prefix = Me(); Skip.IfMissingDatabase(conn, dbId); @@ -22,20 +20,21 @@ private void Prep(int dbId, string key) for (int i = 0; i < 10000; i++) { string iKey = prefix + i; - db.StringSetAsync(iKey, iKey); + _ = db.StringSetAsync(iKey, iKey); last = db.SetAddAsync(key, iKey); } - db.Wait(last!); + await last!; } - [FactLongRunning] + [Fact] public async Task ExecuteMassiveDelete() { + Skip.UnlessLongRunning(); var dbId = TestConfig.GetDedicatedDB(); var key = Me(); - Prep(dbId, key); + await Prep(dbId, key); var watch = Stopwatch.StartNew(); - using var conn = Create(); + await using var conn = Create(); using var throttle = new SemaphoreSlim(1); var db = conn.GetDatabase(dbId); var originally = await db.SetLengthAsync(key).ForAwait(); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO10504853Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO10504853Tests.cs index ee2fd9bbc..7d4276e9d 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO10504853Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO10504853Tests.cs @@ -1,20 +1,18 @@ using System; using System.Diagnostics; +using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues; -public class SO10504853Tests : TestBase +public class SO10504853Tests(ITestOutputHelper output) : TestBase(output) { - public SO10504853Tests(ITestOutputHelper output) : base(output) { } - [Fact] - public void LoopLotsOfTrivialStuff() + public async Task LoopLotsOfTrivialStuff() { var key = Me(); Trace.WriteLine("### init"); - using (var conn = Create()) + await using (var conn = Create()) { var db = conn.GetDatabase(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -23,12 +21,12 @@ public void LoopLotsOfTrivialStuff() for (int i = 0; i < COUNT; i++) { Trace.WriteLine("### incr:" + i); - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); Assert.Equal(i + 1, db.StringIncrement(key)); } Trace.WriteLine("### close"); - using (var conn = Create()) + await using (var conn = Create()) { var db = conn.GetDatabase(); Assert.Equal(COUNT, (long)db.StringGet(key)); @@ -36,20 +34,20 @@ public void LoopLotsOfTrivialStuff() } [Fact] - public void ExecuteWithEmptyStartingPoint() + public async Task ExecuteWithEmptyStartingPoint() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); var task = new { priority = 3 }; - db.KeyDeleteAsync(key); - db.HashSetAsync(key, "something else", "abc"); - db.HashSetAsync(key, "priority", task.priority.ToString()); + _ = db.KeyDeleteAsync(key); + _ = db.HashSetAsync(key, "something else", "abc"); + _ = db.HashSetAsync(key, "priority", task.priority.ToString()); var taskResult = db.HashGetAsync(key, "priority"); - db.Wait(taskResult); + await taskResult; var priority = int.Parse(taskResult.Result!); @@ -57,18 +55,18 @@ public void ExecuteWithEmptyStartingPoint() } [Fact] - public void ExecuteWithNonHashStartingPoint() + public async Task ExecuteWithNonHashStartingPoint() { var key = Me(); - Assert.Throws(() => + await Assert.ThrowsAsync(async () => { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var task = new { priority = 3 }; - db.KeyDeleteAsync(key); - db.StringSetAsync(key, "not a hash"); - db.HashSetAsync(key, "priority", task.priority.ToString()); + _ = db.KeyDeleteAsync(key); + _ = db.StringSetAsync(key, "not a hash"); + _ = db.HashSetAsync(key, "priority", task.priority.ToString()); var taskResult = db.HashGetAsync(key, "priority"); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO10825542Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO10825542Tests.cs index b19386f6b..493f4ec1b 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO10825542Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO10825542Tests.cs @@ -2,18 +2,15 @@ using System.Text; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues; -public class SO10825542Tests : TestBase +public class SO10825542Tests(ITestOutputHelper output) : TestBase(output) { - public SO10825542Tests(ITestOutputHelper output) : base(output) { } - [Fact] public async Task Execute() { - using var conn = Create(); + await using var conn = Create(); var key = Me(); var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO11766033Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO11766033Tests.cs index d350bcff3..65cef55a7 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO11766033Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO11766033Tests.cs @@ -1,36 +1,34 @@ -using Xunit; -using Xunit.Abstractions; +using System.Threading.Tasks; +using Xunit; namespace StackExchange.Redis.Tests.Issues; -public class SO11766033Tests : TestBase +public class SO11766033Tests(ITestOutputHelper output) : TestBase(output) { - public SO11766033Tests(ITestOutputHelper output) : base(output) { } - [Fact] - public void TestNullString() + public async Task TestNullString() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); const string? expectedTestValue = null; var uid = Me(); - db.StringSetAsync(uid, "abc"); - db.StringSetAsync(uid, expectedTestValue); + _ = db.StringSetAsync(uid, "abc"); + _ = db.StringSetAsync(uid, expectedTestValue); string? testValue = db.StringGet(uid); Assert.Null(testValue); } [Fact] - public void TestEmptyString() + public async Task TestEmptyString() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); const string expectedTestValue = ""; var uid = Me(); - db.StringSetAsync(uid, expectedTestValue); + _ = db.StringSetAsync(uid, expectedTestValue); string? testValue = db.StringGet(uid); Assert.Equal(expectedTestValue, testValue); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO22786599Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO22786599Tests.cs index 562eef8c5..0fc653991 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO22786599Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO22786599Tests.cs @@ -1,16 +1,14 @@ using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues; -public class SO22786599Tests : TestBase +public class SO22786599Tests(ITestOutputHelper output) : TestBase(output) { - public SO22786599Tests(ITestOutputHelper output) : base(output) { } - [Fact] - public void Execute() + public async Task Execute() { string currentIdsSetDbKey = Me() + ".x"; string currentDetailsSetDbKey = Me() + ".y"; @@ -18,13 +16,13 @@ public void Execute() RedisValue[] stringIds = Enumerable.Range(1, 750).Select(i => (RedisValue)(i + " id")).ToArray(); RedisValue[] stringDetails = Enumerable.Range(1, 750).Select(i => (RedisValue)(i + " detail")).ToArray(); - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var tran = db.CreateTransaction(); - tran.SetAddAsync(currentIdsSetDbKey, stringIds); - tran.SetAddAsync(currentDetailsSetDbKey, stringDetails); + _ = tran.SetAddAsync(currentIdsSetDbKey, stringIds); + _ = tran.SetAddAsync(currentDetailsSetDbKey, stringDetails); var watch = Stopwatch.StartNew(); var isOperationSuccessful = tran.Execute(); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO23949477Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO23949477Tests.cs index aa545a3cb..92277289a 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO23949477Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO23949477Tests.cs @@ -1,16 +1,14 @@ -using Xunit; -using Xunit.Abstractions; +using System.Threading.Tasks; +using Xunit; namespace StackExchange.Redis.Tests.Issues; -public class SO23949477Tests : TestBase +public class SO23949477Tests(ITestOutputHelper output) : TestBase(output) { - public SO23949477Tests(ITestOutputHelper output) : base(output) { } - [Fact] - public void Execute() + public async Task Execute() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -18,13 +16,12 @@ public void Execute() db.SortedSetAdd(key, "c", 3, When.Always, CommandFlags.FireAndForget); db.SortedSetAdd( key, - new[] - { + [ new SortedSetEntry("a", 1), new SortedSetEntry("b", 2), new SortedSetEntry("d", 4), new SortedSetEntry("e", 5), - }, + ], When.Always, CommandFlags.FireAndForget); var pairs = db.SortedSetRangeByScoreWithScores( diff --git a/tests/StackExchange.Redis.Tests/Issues/SO24807536Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO24807536Tests.cs index d4b449dd3..ddff810c0 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO24807536Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO24807536Tests.cs @@ -1,18 +1,15 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues; -public class SO24807536Tests : TestBase +public class SO24807536Tests(ITestOutputHelper output) : TestBase(output) { - public SO24807536Tests(ITestOutputHelper output) : base(output) { } - [Fact] public async Task Exec() { - using var conn = Create(); + await using var conn = Create(); var key = Me(); var db = conn.GetDatabase(); @@ -20,7 +17,7 @@ public async Task Exec() // setup some data db.KeyDelete(key, CommandFlags.FireAndForget); db.HashSet(key, "full", "some value", flags: CommandFlags.FireAndForget); - db.KeyExpire(key, TimeSpan.FromSeconds(4), CommandFlags.FireAndForget); + db.KeyExpire(key, TimeSpan.FromSeconds(2), CommandFlags.FireAndForget); // test while exists var keyExists = db.KeyExists(key); @@ -28,7 +25,7 @@ public async Task Exec() var fullWait = db.HashGetAsync(key, "full", flags: CommandFlags.None); Assert.True(keyExists, "key exists"); Assert.NotNull(ttl); - Assert.Equal("some value", fullWait.Result); + Assert.Equal("some value", await fullWait); // wait for expiry await UntilConditionAsync(TimeSpan.FromSeconds(10), () => !db.KeyExists(key)).ForAwait(); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO25113323Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO25113323Tests.cs index 8de318e2a..00bc9836b 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO25113323Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO25113323Tests.cs @@ -1,17 +1,14 @@ using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues; -public class SO25113323Tests : TestBase +public class SO25113323Tests(ITestOutputHelper output) : TestBase(output) { - public SO25113323Tests(ITestOutputHelper output) : base(output) { } - [Fact] public async Task SetExpirationToPassed() { - using var conn = Create(); + await using var conn = Create(); // Given var key = Me(); diff --git a/tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs b/tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs index b71b2a1f9..6d00a705e 100644 --- a/tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs +++ b/tests/StackExchange.Redis.Tests/Issues/SO25567566Tests.cs @@ -1,19 +1,16 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests.Issues; -public class SO25567566Tests : TestBase +public class SO25567566Tests(ITestOutputHelper output) : TestBase(output) { - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public SO25567566Tests(ITestOutputHelper output) : base(output) { } - - [FactLongRunning] + [Fact] public async Task Execute() { - using var conn = ConnectionMultiplexer.Connect(GetConfiguration()); + Skip.UnlessLongRunning(); + await using var conn = await ConnectionMultiplexer.ConnectAsync(GetConfiguration()); for (int i = 0; i < 100; i++) { diff --git a/tests/StackExchange.Redis.Tests/KeyIdleAsyncTests.cs b/tests/StackExchange.Redis.Tests/KeyIdleAsyncTests.cs new file mode 100644 index 000000000..598e84d93 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/KeyIdleAsyncTests.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace StackExchange.Redis.Tests; + +[RunPerProtocol] +public class KeyIdleAsyncTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) +{ + [Fact] + public async Task IdleTimeAsync() + { + await using var conn = Create(); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + await Task.Delay(2000).ForAwait(); + var idleTime = await db.KeyIdleTimeAsync(key).ForAwait(); + Assert.True(idleTime > TimeSpan.Zero, "First check"); + + db.StringSet(key, "new value2", flags: CommandFlags.FireAndForget); + var idleTime2 = await db.KeyIdleTimeAsync(key).ForAwait(); + Assert.True(idleTime2 < idleTime, "Second check"); + + db.KeyDelete(key); + var idleTime3 = await db.KeyIdleTimeAsync(key).ForAwait(); + Assert.Null(idleTime3); + } + + [Fact] + public async Task TouchIdleTimeAsync() + { + await using var conn = Create(require: RedisFeatures.v3_2_1); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + await Task.Delay(2000).ForAwait(); + var idleTime = await db.KeyIdleTimeAsync(key).ForAwait(); + Assert.True(idleTime > TimeSpan.Zero, "First check"); + + Assert.True(await db.KeyTouchAsync(key).ForAwait(), "Second check"); + var idleTime1 = await db.KeyIdleTimeAsync(key).ForAwait(); + Assert.True(idleTime1 < idleTime, "Third check"); + } +} diff --git a/tests/StackExchange.Redis.Tests/KeyIdleTests.cs b/tests/StackExchange.Redis.Tests/KeyIdleTests.cs new file mode 100644 index 000000000..deec1efb4 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/KeyIdleTests.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace StackExchange.Redis.Tests; + +[RunPerProtocol] +public class KeyIdleTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) +{ + [Fact] + public async Task IdleTime() + { + await using var conn = Create(); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + await Task.Delay(2000).ForAwait(); + var idleTime = db.KeyIdleTime(key); + Assert.True(idleTime > TimeSpan.Zero); + + db.StringSet(key, "new value2", flags: CommandFlags.FireAndForget); + var idleTime2 = db.KeyIdleTime(key); + Assert.True(idleTime2 < idleTime); + + db.KeyDelete(key); + var idleTime3 = db.KeyIdleTime(key); + Assert.Null(idleTime3); + } + + [Fact] + public async Task TouchIdleTime() + { + await using var conn = Create(require: RedisFeatures.v3_2_1); + + RedisKey key = Me(); + var db = conn.GetDatabase(); + db.KeyDelete(key, CommandFlags.FireAndForget); + db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); + await Task.Delay(2000).ForAwait(); + var idleTime = db.KeyIdleTime(key); + Assert.True(idleTime > TimeSpan.Zero, "First check"); + + Assert.True(db.KeyTouch(key), "Second check"); + var idleTime1 = db.KeyIdleTime(key); + Assert.True(idleTime1 < idleTime, "Third check"); + } +} diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs index 2552ac7aa..3c56f7605 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs @@ -237,7 +237,7 @@ public void HyperLogLogMerge_1() [Fact] public void HyperLogLogMerge_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.HyperLogLogMerge("destination", keys, CommandFlags.None); mock.Received().HyperLogLogMerge("prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -267,7 +267,7 @@ public void KeyDelete_1() [Fact] public void KeyDelete_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.KeyDelete(keys, CommandFlags.None); mock.Received().KeyDelete(Arg.Is(valid), CommandFlags.None); @@ -458,7 +458,7 @@ public void ListLeftPush_2() [Fact] public void ListLeftPush_3() { - RedisValue[] values = new RedisValue[] { "value1", "value2" }; + RedisValue[] values = ["value1", "value2"]; prefixed.ListLeftPush("key", values, When.Exists, CommandFlags.None); mock.Received().ListLeftPush("prefix:key", values, When.Exists, CommandFlags.None); } @@ -530,7 +530,7 @@ public void ListRightPush_2() [Fact] public void ListRightPush_3() { - RedisValue[] values = new RedisValue[] { "value1", "value2" }; + RedisValue[] values = ["value1", "value2"]; prefixed.ListRightPush("key", values, When.Exists, CommandFlags.None); mock.Received().ListRightPush("prefix:key", values, When.Exists, CommandFlags.None); } @@ -593,7 +593,7 @@ public void ScriptEvaluate_1() { byte[] hash = Array.Empty(); RedisValue[] values = Array.Empty(); - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.ScriptEvaluate(hash, keys, values, CommandFlags.None); mock.Received().ScriptEvaluate(hash, Arg.Is(valid), values, CommandFlags.None); @@ -603,7 +603,7 @@ public void ScriptEvaluate_1() public void ScriptEvaluate_2() { RedisValue[] values = Array.Empty(); - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.ScriptEvaluate(script: "script", keys: keys, values: values, flags: CommandFlags.None); mock.Received().ScriptEvaluate(script: "script", keys: Arg.Is(valid), values: values, flags: CommandFlags.None); @@ -634,7 +634,7 @@ public void SetCombine_1() [Fact] public void SetCombine_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.SetCombine(SetOperation.Intersect, keys, CommandFlags.None); mock.Received().SetCombine(SetOperation.Intersect, Arg.Is(valid), CommandFlags.None); @@ -650,7 +650,7 @@ public void SetCombineAndStore_1() [Fact] public void SetCombineAndStore_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None); mock.Received().SetCombineAndStore(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -666,7 +666,7 @@ public void SetContains() [Fact] public void SetContains_2() { - RedisValue[] values = new RedisValue[] { "value1", "value2" }; + RedisValue[] values = ["value1", "value2"]; prefixed.SetContains("key", values, CommandFlags.None); mock.Received().SetContains("prefix:key", values, CommandFlags.None); } @@ -763,7 +763,7 @@ public void SetScan_Full() [Fact] public void Sort() { - RedisValue[] get = new RedisValue[] { "a", "#" }; + RedisValue[] get = ["a", "#"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; prefixed.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); @@ -776,7 +776,7 @@ public void Sort() [Fact] public void SortAndStore() { - RedisValue[] get = new RedisValue[] { "a", "#" }; + RedisValue[] get = ["a", "#"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; prefixed.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); @@ -812,7 +812,7 @@ public void SortedSetAdd_3() [Fact] public void SortedSetCombine() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; prefixed.SortedSetCombine(SetOperation.Intersect, keys); mock.Received().SortedSetCombine(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None); } @@ -820,7 +820,7 @@ public void SortedSetCombine() [Fact] public void SortedSetCombineWithScores() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; prefixed.SortedSetCombineWithScores(SetOperation.Intersect, keys); mock.Received().SortedSetCombineWithScores(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None); } @@ -835,7 +835,7 @@ public void SortedSetCombineAndStore_1() [Fact] public void SortedSetCombineAndStore_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None); mock.Received().SetCombineAndStore(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -858,7 +858,7 @@ public void SortedSetIncrement() [Fact] public void SortedSetIntersectionLength() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; prefixed.SortedSetIntersectionLength(keys, 1, CommandFlags.None); mock.Received().SortedSetIntersectionLength(keys, 1, CommandFlags.None); } @@ -1233,7 +1233,7 @@ public void StringBitOperation_1() [Fact] public void StringBitOperation_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.StringBitOperation(Bitwise.Xor, "destination", keys, CommandFlags.None); mock.Received().StringBitOperation(Bitwise.Xor, "prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -1242,7 +1242,7 @@ public void StringBitOperation_2() [Fact] public void StringBitOperation_Diff() { - RedisKey[] keys = new RedisKey[] { "x", "y1", "y2" }; + RedisKey[] keys = ["x", "y1", "y2"]; Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; prefixed.StringBitOperation(Bitwise.Diff, "destination", keys, CommandFlags.None); mock.Received().StringBitOperation(Bitwise.Diff, "prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -1251,7 +1251,7 @@ public void StringBitOperation_Diff() [Fact] public void StringBitOperation_Diff1() { - RedisKey[] keys = new RedisKey[] { "x", "y1", "y2" }; + RedisKey[] keys = ["x", "y1", "y2"]; Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; prefixed.StringBitOperation(Bitwise.Diff1, "destination", keys, CommandFlags.None); mock.Received().StringBitOperation(Bitwise.Diff1, "prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -1260,7 +1260,7 @@ public void StringBitOperation_Diff1() [Fact] public void StringBitOperation_AndOr() { - RedisKey[] keys = new RedisKey[] { "x", "y1", "y2" }; + RedisKey[] keys = ["x", "y1", "y2"]; Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; prefixed.StringBitOperation(Bitwise.AndOr, "destination", keys, CommandFlags.None); mock.Received().StringBitOperation(Bitwise.AndOr, "prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -1269,7 +1269,7 @@ public void StringBitOperation_AndOr() [Fact] public void StringBitOperation_One() { - RedisKey[] keys = new RedisKey[] { "a", "b", "c" }; + RedisKey[] keys = ["a", "b", "c"]; Expression> valid = _ => _.Length == 3 && _[0] == "prefix:a" && _[1] == "prefix:b" && _[2] == "prefix:c"; prefixed.StringBitOperation(Bitwise.One, "destination", keys, CommandFlags.None); mock.Received().StringBitOperation(Bitwise.One, "prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -1313,7 +1313,7 @@ public void StringGet_1() [Fact] public void StringGet_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.StringGet(keys, CommandFlags.None); mock.Received().StringGet(Arg.Is(valid), CommandFlags.None); @@ -1394,7 +1394,7 @@ public void StringSet_2() [Fact] public void StringSet_3() { - KeyValuePair[] values = new KeyValuePair[] { new KeyValuePair("a", "x"), new KeyValuePair("b", "y") }; + KeyValuePair[] values = [new KeyValuePair("a", "x"), new KeyValuePair("b", "y")]; Expression[]>> valid = _ => _.Length == 2 && _[0].Key == "prefix:a" && _[0].Value == "x" && _[1].Key == "prefix:b" && _[1].Value == "y"; prefixed.StringSet(values, When.Exists, CommandFlags.None); mock.Received().StringSet(Arg.Is(valid), When.Exists, CommandFlags.None); diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs index e768b9ec5..70893e510 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs @@ -176,7 +176,7 @@ public async Task HyperLogLogMergeAsync_1() [Fact] public async Task HyperLogLogMergeAsync_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.HyperLogLogMergeAsync("destination", keys, CommandFlags.None); await mock.Received().HyperLogLogMergeAsync("prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -213,7 +213,7 @@ public async Task KeyDeleteAsync_1() [Fact] public async Task KeyDeleteAsync_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.KeyDeleteAsync(keys, CommandFlags.None); await mock.Received().KeyDeleteAsync(Arg.Is(valid), CommandFlags.None); @@ -404,7 +404,7 @@ public async Task ListLeftPushAsync_2() [Fact] public async Task ListLeftPushAsync_3() { - RedisValue[] values = new RedisValue[] { "value1", "value2" }; + RedisValue[] values = ["value1", "value2"]; await prefixed.ListLeftPushAsync("key", values, When.Exists, CommandFlags.None); await mock.Received().ListLeftPushAsync("prefix:key", values, When.Exists, CommandFlags.None); } @@ -476,7 +476,7 @@ public async Task ListRightPushAsync_2() [Fact] public async Task ListRightPushAsync_3() { - RedisValue[] values = new RedisValue[] { "value1", "value2" }; + RedisValue[] values = ["value1", "value2"]; await prefixed.ListRightPushAsync("key", values, When.Exists, CommandFlags.None); await mock.Received().ListRightPushAsync("prefix:key", values, When.Exists, CommandFlags.None); } @@ -537,7 +537,7 @@ public async Task ScriptEvaluateAsync_1() { byte[] hash = Array.Empty(); RedisValue[] values = Array.Empty(); - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.ScriptEvaluateAsync(hash, keys, values, CommandFlags.None); await mock.Received().ScriptEvaluateAsync(hash, Arg.Is(valid), values, CommandFlags.None); @@ -547,7 +547,7 @@ public async Task ScriptEvaluateAsync_1() public async Task ScriptEvaluateAsync_2() { RedisValue[] values = Array.Empty(); - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.ScriptEvaluateAsync("script", keys, values, CommandFlags.None); await mock.Received().ScriptEvaluateAsync(script: "script", keys: Arg.Is(valid), values: values, flags: CommandFlags.None); @@ -578,7 +578,7 @@ public async Task SetCombineAndStoreAsync_1() [Fact] public async Task SetCombineAndStoreAsync_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", keys, CommandFlags.None); await mock.Received().SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -594,7 +594,7 @@ public async Task SetCombineAsync_1() [Fact] public async Task SetCombineAsync_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.SetCombineAsync(SetOperation.Intersect, keys, CommandFlags.None); await mock.Received().SetCombineAsync(SetOperation.Intersect, Arg.Is(valid), CommandFlags.None); @@ -610,7 +610,7 @@ public async Task SetContainsAsync() [Fact] public async Task SetContainsAsync_2() { - RedisValue[] values = new RedisValue[] { "value1", "value2" }; + RedisValue[] values = ["value1", "value2"]; await prefixed.SetContainsAsync("key", values, CommandFlags.None); await mock.Received().SetContainsAsync("prefix:key", values, CommandFlags.None); } @@ -693,7 +693,7 @@ public async Task SetRemoveAsync_2() [Fact] public async Task SortAndStoreAsync() { - RedisValue[] get = new RedisValue[] { "a", "#" }; + RedisValue[] get = ["a", "#"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; await prefixed.SortAndStoreAsync("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); @@ -706,7 +706,7 @@ public async Task SortAndStoreAsync() [Fact] public async Task SortAsync() { - RedisValue[] get = new RedisValue[] { "a", "#" }; + RedisValue[] get = ["a", "#"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; await prefixed.SortAsync("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); @@ -742,7 +742,7 @@ public async Task SortedSetAddAsync_3() [Fact] public async Task SortedSetCombineAsync() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; await prefixed.SortedSetCombineAsync(SetOperation.Intersect, keys); await mock.Received().SortedSetCombineAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None); } @@ -750,7 +750,7 @@ public async Task SortedSetCombineAsync() [Fact] public async Task SortedSetCombineWithScoresAsync() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; await prefixed.SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys); await mock.Received().SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None); } @@ -765,7 +765,7 @@ public async Task SortedSetCombineAndStoreAsync_1() [Fact] public async Task SortedSetCombineAndStoreAsync_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", keys, CommandFlags.None); await mock.Received().SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -788,7 +788,7 @@ public async Task SortedSetIncrementAsync() [Fact] public async Task SortedSetIntersectionLengthAsync() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; await prefixed.SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None); await mock.Received().SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None); } @@ -1149,7 +1149,7 @@ public async Task StringBitOperationAsync_1() [Fact] public async Task StringBitOperationAsync_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.StringBitOperationAsync(Bitwise.Xor, "destination", keys, CommandFlags.None); await mock.Received().StringBitOperationAsync(Bitwise.Xor, "prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -1158,7 +1158,7 @@ public async Task StringBitOperationAsync_2() [Fact] public async Task StringBitOperationAsync_Diff() { - RedisKey[] keys = new RedisKey[] { "x", "y1", "y2" }; + RedisKey[] keys = ["x", "y1", "y2"]; Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; await prefixed.StringBitOperationAsync(Bitwise.Diff, "destination", keys, CommandFlags.None); await mock.Received().StringBitOperationAsync(Bitwise.Diff, "prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -1167,7 +1167,7 @@ public async Task StringBitOperationAsync_Diff() [Fact] public async Task StringBitOperationAsync_Diff1() { - RedisKey[] keys = new RedisKey[] { "x", "y1", "y2" }; + RedisKey[] keys = ["x", "y1", "y2"]; Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; await prefixed.StringBitOperationAsync(Bitwise.Diff1, "destination", keys, CommandFlags.None); await mock.Received().StringBitOperationAsync(Bitwise.Diff1, "prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -1176,7 +1176,7 @@ public async Task StringBitOperationAsync_Diff1() [Fact] public async Task StringBitOperationAsync_AndOr() { - RedisKey[] keys = new RedisKey[] { "x", "y1", "y2" }; + RedisKey[] keys = ["x", "y1", "y2"]; Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; await prefixed.StringBitOperationAsync(Bitwise.AndOr, "destination", keys, CommandFlags.None); await mock.Received().StringBitOperationAsync(Bitwise.AndOr, "prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -1185,7 +1185,7 @@ public async Task StringBitOperationAsync_AndOr() [Fact] public async Task StringBitOperationAsync_One() { - RedisKey[] keys = new RedisKey[] { "a", "b", "c" }; + RedisKey[] keys = ["a", "b", "c"]; Expression> valid = _ => _.Length == 3 && _[0] == "prefix:a" && _[1] == "prefix:b" && _[2] == "prefix:c"; await prefixed.StringBitOperationAsync(Bitwise.One, "destination", keys, CommandFlags.None); await mock.Received().StringBitOperationAsync(Bitwise.One, "prefix:destination", Arg.Is(valid), CommandFlags.None); @@ -1229,7 +1229,7 @@ public async Task StringGetAsync_1() [Fact] public async Task StringGetAsync_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.StringGetAsync(keys, CommandFlags.None); await mock.Received().StringGetAsync(Arg.Is(valid), CommandFlags.None); @@ -1310,7 +1310,7 @@ public async Task StringSetAsync_2() [Fact] public async Task StringSetAsync_3() { - KeyValuePair[] values = new KeyValuePair[] { new KeyValuePair("a", "x"), new KeyValuePair("b", "y") }; + KeyValuePair[] values = [new KeyValuePair("a", "x"), new KeyValuePair("b", "y")]; Expression[]>> valid = _ => _.Length == 2 && _[0].Key == "prefix:a" && _[0].Value == "x" && _[1].Key == "prefix:b" && _[1].Value == "y"; await prefixed.StringSetAsync(values, When.Exists, CommandFlags.None); await mock.Received().StringSetAsync(Arg.Is(valid), When.Exists, CommandFlags.None); @@ -1348,7 +1348,7 @@ public async Task KeyTouchAsync_1() [Fact] public async Task KeyTouchAsync_2() { - RedisKey[] keys = new RedisKey[] { "a", "b" }; + RedisKey[] keys = ["a", "b"]; Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.KeyTouchAsync(keys, CommandFlags.None); await mock.Received().KeyTouchAsync(Arg.Is(valid), CommandFlags.None); diff --git a/tests/StackExchange.Redis.Tests/KeyTests.cs b/tests/StackExchange.Redis.Tests/KeyTests.cs index bfb57a425..31cd87d79 100644 --- a/tests/StackExchange.Redis.Tests/KeyTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyTests.cs @@ -5,20 +5,16 @@ using System.Text; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class KeyTests : TestBase +public class KeyTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public KeyTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void TestScan() + public async Task TestScan() { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); var dbId = TestConfig.GetDedicatedDB(conn); var db = conn.GetDatabase(dbId); @@ -35,9 +31,9 @@ public void TestScan() } [Fact] - public void FlushFetchRandomKey() + public async Task FlushFetchRandomKey() { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); var dbId = TestConfig.GetDedicatedDB(conn); Skip.IfMissingDatabase(conn, dbId); @@ -55,9 +51,9 @@ public void FlushFetchRandomKey() } [Fact] - public void Zeros() + public async Task Zeros() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -111,9 +107,9 @@ public void PrependAppend() } [Fact] - public void Exists() + public async Task Exists() { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(); RedisKey key2 = Me() + "2"; @@ -123,23 +119,23 @@ public void Exists() Assert.False(db.KeyExists(key)); Assert.False(db.KeyExists(key2)); - Assert.Equal(0, db.KeyExists(new[] { key, key2 })); + Assert.Equal(0, db.KeyExists([key, key2])); db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); Assert.True(db.KeyExists(key)); Assert.False(db.KeyExists(key2)); - Assert.Equal(1, db.KeyExists(new[] { key, key2 })); + Assert.Equal(1, db.KeyExists([key, key2])); db.StringSet(key2, "new value", flags: CommandFlags.FireAndForget); Assert.True(db.KeyExists(key)); Assert.True(db.KeyExists(key2)); - Assert.Equal(2, db.KeyExists(new[] { key, key2 })); + Assert.Equal(2, db.KeyExists([key, key2])); } [Fact] public async Task ExistsAsync() { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(); RedisKey key2 = Me() + "2"; @@ -148,19 +144,19 @@ public async Task ExistsAsync() db.KeyDelete(key2, CommandFlags.FireAndForget); var a1 = db.KeyExistsAsync(key).ForAwait(); var a2 = db.KeyExistsAsync(key2).ForAwait(); - var a3 = db.KeyExistsAsync(new[] { key, key2 }).ForAwait(); + var a3 = db.KeyExistsAsync([key, key2]).ForAwait(); db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); var b1 = db.KeyExistsAsync(key).ForAwait(); var b2 = db.KeyExistsAsync(key2).ForAwait(); - var b3 = db.KeyExistsAsync(new[] { key, key2 }).ForAwait(); + var b3 = db.KeyExistsAsync([key, key2]).ForAwait(); db.StringSet(key2, "new value", flags: CommandFlags.FireAndForget); var c1 = db.KeyExistsAsync(key).ForAwait(); var c2 = db.KeyExistsAsync(key2).ForAwait(); - var c3 = db.KeyExistsAsync(new[] { key, key2 }).ForAwait(); + var c3 = db.KeyExistsAsync([key, key2]).ForAwait(); Assert.False(await a1); Assert.False(await a2); @@ -175,90 +171,10 @@ public async Task ExistsAsync() Assert.Equal(2, await c3); } - [Fact] - public async Task IdleTime() - { - using var conn = Create(); - - RedisKey key = Me(); - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - await Task.Delay(2000).ForAwait(); - var idleTime = db.KeyIdleTime(key); - Assert.True(idleTime > TimeSpan.Zero); - - db.StringSet(key, "new value2", flags: CommandFlags.FireAndForget); - var idleTime2 = db.KeyIdleTime(key); - Assert.True(idleTime2 < idleTime); - - db.KeyDelete(key); - var idleTime3 = db.KeyIdleTime(key); - Assert.Null(idleTime3); - } - - [Fact] - public async Task TouchIdleTime() - { - using var conn = Create(require: RedisFeatures.v3_2_1); - - RedisKey key = Me(); - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - await Task.Delay(2000).ForAwait(); - var idleTime = db.KeyIdleTime(key); - Assert.True(idleTime > TimeSpan.Zero); - - Assert.True(db.KeyTouch(key)); - var idleTime1 = db.KeyIdleTime(key); - Assert.True(idleTime1 < idleTime); - } - - [Fact] - public async Task IdleTimeAsync() - { - using var conn = Create(); - - RedisKey key = Me(); - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - await Task.Delay(2000).ForAwait(); - var idleTime = await db.KeyIdleTimeAsync(key).ForAwait(); - Assert.True(idleTime > TimeSpan.Zero); - - db.StringSet(key, "new value2", flags: CommandFlags.FireAndForget); - var idleTime2 = await db.KeyIdleTimeAsync(key).ForAwait(); - Assert.True(idleTime2 < idleTime); - - db.KeyDelete(key); - var idleTime3 = await db.KeyIdleTimeAsync(key).ForAwait(); - Assert.Null(idleTime3); - } - - [Fact] - public async Task TouchIdleTimeAsync() - { - using var conn = Create(require: RedisFeatures.v3_2_1); - - RedisKey key = Me(); - var db = conn.GetDatabase(); - db.KeyDelete(key, CommandFlags.FireAndForget); - db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - await Task.Delay(2000).ForAwait(); - var idleTime = await db.KeyIdleTimeAsync(key).ForAwait(); - Assert.True(idleTime > TimeSpan.Zero); - - Assert.True(await db.KeyTouchAsync(key).ForAwait()); - var idleTime1 = await db.KeyIdleTimeAsync(key).ForAwait(); - Assert.True(idleTime1 < idleTime); - } - [Fact] public async Task KeyEncoding() { - using var conn = Create(); + await using var conn = Create(); var key = Me(); var db = conn.GetDatabase(); @@ -285,7 +201,7 @@ public async Task KeyEncoding() [Fact] public async Task KeyRefCount() { - using var conn = Create(); + await using var conn = Create(); var key = Me(); var db = conn.GetDatabase(); @@ -303,7 +219,7 @@ public async Task KeyRefCount() [Fact] public async Task KeyFrequency() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v4_0_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v4_0_0); var key = Me(); var db = conn.GetDatabase(); @@ -492,49 +408,50 @@ public void KeyEquality(RedisKey x, RedisKey y, bool equal) } } - public static IEnumerable KeyEqualityData() + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1046:Avoid using TheoryDataRow arguments that are not serializable", Justification = "No options at the moment.")] + public static IEnumerable> KeyEqualityData() { RedisKey abcString = "abc", abcBytes = Encoding.UTF8.GetBytes("abc"); RedisKey abcdefString = "abcdef", abcdefBytes = Encoding.UTF8.GetBytes("abcdef"); - yield return new object[] { RedisKey.Null, abcString, false }; - yield return new object[] { RedisKey.Null, abcBytes, false }; - yield return new object[] { abcString, RedisKey.Null, false }; - yield return new object[] { abcBytes, RedisKey.Null, false }; - yield return new object[] { RedisKey.Null, RedisKey.Null, true }; - yield return new object[] { new RedisKey((string?)null), RedisKey.Null, true }; - yield return new object[] { new RedisKey(null, (byte[]?)null), RedisKey.Null, true }; - yield return new object[] { new RedisKey(""), RedisKey.Null, false }; - yield return new object[] { new RedisKey(null, Array.Empty()), RedisKey.Null, false }; - - yield return new object[] { abcString, abcString, true }; - yield return new object[] { abcBytes, abcBytes, true }; - yield return new object[] { abcString, abcBytes, true }; - yield return new object[] { abcBytes, abcString, true }; - - yield return new object[] { abcdefString, abcdefString, true }; - yield return new object[] { abcdefBytes, abcdefBytes, true }; - yield return new object[] { abcdefString, abcdefBytes, true }; - yield return new object[] { abcdefBytes, abcdefString, true }; - - yield return new object[] { abcString, abcdefString, false }; - yield return new object[] { abcBytes, abcdefBytes, false }; - yield return new object[] { abcString, abcdefBytes, false }; - yield return new object[] { abcBytes, abcdefString, false }; - - yield return new object[] { abcdefString, abcString, false }; - yield return new object[] { abcdefBytes, abcBytes, false }; - yield return new object[] { abcdefString, abcBytes, false }; - yield return new object[] { abcdefBytes, abcString, false }; + yield return new(RedisKey.Null, abcString, false); + yield return new(RedisKey.Null, abcBytes, false); + yield return new(abcString, RedisKey.Null, false); + yield return new(abcBytes, RedisKey.Null, false); + yield return new(RedisKey.Null, RedisKey.Null, true); + yield return new(new RedisKey((string?)null), RedisKey.Null, true); + yield return new(new RedisKey(null, (byte[]?)null), RedisKey.Null, true); + yield return new(new RedisKey(""), RedisKey.Null, false); + yield return new(new RedisKey(null, Array.Empty()), RedisKey.Null, false); + + yield return new(abcString, abcString, true); + yield return new(abcBytes, abcBytes, true); + yield return new(abcString, abcBytes, true); + yield return new(abcBytes, abcString, true); + + yield return new(abcdefString, abcdefString, true); + yield return new(abcdefBytes, abcdefBytes, true); + yield return new(abcdefString, abcdefBytes, true); + yield return new(abcdefBytes, abcdefString, true); + + yield return new(abcString, abcdefString, false); + yield return new(abcBytes, abcdefBytes, false); + yield return new(abcString, abcdefBytes, false); + yield return new(abcBytes, abcdefString, false); + + yield return new(abcdefString, abcString, false); + yield return new(abcdefBytes, abcBytes, false); + yield return new(abcdefString, abcBytes, false); + yield return new(abcdefBytes, abcString, false); var x = abcString.Append("def"); - yield return new object[] { abcdefString, x, true }; - yield return new object[] { abcdefBytes, x, true }; - yield return new object[] { x, abcdefBytes, true }; - yield return new object[] { x, abcdefString, true }; - yield return new object[] { abcString, x, false }; - yield return new object[] { abcString, x, false }; - yield return new object[] { x, abcString, false }; - yield return new object[] { x, abcString, false }; + yield return new(abcdefString, x, true); + yield return new(abcdefBytes, x, true); + yield return new(x, abcdefBytes, true); + yield return new(x, abcdefString, true); + yield return new(abcString, x, false); + yield return new(abcString, x, false); + yield return new(x, abcString, false); + yield return new(x, abcString, false); } } diff --git a/tests/StackExchange.Redis.Tests/LatencyTests.cs b/tests/StackExchange.Redis.Tests/LatencyTests.cs index c82c947b7..42b4d7b05 100644 --- a/tests/StackExchange.Redis.Tests/LatencyTests.cs +++ b/tests/StackExchange.Redis.Tests/LatencyTests.cs @@ -1,18 +1,15 @@ using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class LatencyTests : TestBase +[Collection(NonParallelCollection.Name)] +public class LatencyTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public LatencyTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] public async Task CanCallDoctor() { - using var conn = Create(); + await using var conn = Create(); var server = conn.GetServer(conn.GetEndPoints()[0]); string? doctor = server.LatencyDoctor(); @@ -27,24 +24,25 @@ public async Task CanCallDoctor() [Fact] public async Task CanReset() { - using var conn = Create(); + await using var conn = Create(); var server = conn.GetServer(conn.GetEndPoints()[0]); _ = server.LatencyReset(); - var count = await server.LatencyResetAsync(new[] { "command" }); + var count = await server.LatencyResetAsync(["command"]); Assert.Equal(0, count); - count = await server.LatencyResetAsync(new[] { "command", "fast-command" }); + count = await server.LatencyResetAsync(["command", "fast-command"]); Assert.Equal(0, count); } [Fact] public async Task GetLatest() { - using var conn = Create(allowAdmin: true); + Skip.UnlessLongRunning(); + await using var conn = Create(allowAdmin: true); var server = conn.GetServer(conn.GetEndPoints()[0]); - server.ConfigSet("latency-monitor-threshold", 100); + server.ConfigSet("latency-monitor-threshold", 50); server.LatencyReset(); var arr = server.LatencyLatest(); Assert.Empty(arr); @@ -63,10 +61,11 @@ public async Task GetLatest() [Fact] public async Task GetHistory() { - using var conn = Create(allowAdmin: true); + Skip.UnlessLongRunning(); + await using var conn = Create(allowAdmin: true); var server = conn.GetServer(conn.GetEndPoints()[0]); - server.ConfigSet("latency-monitor-threshold", 100); + server.ConfigSet("latency-monitor-threshold", 50); server.LatencyReset(); var arr = server.LatencyHistory("command"); Assert.Empty(arr); diff --git a/tests/StackExchange.Redis.Tests/LexTests.cs b/tests/StackExchange.Redis.Tests/LexTests.cs index a72aeb142..e29255b24 100644 --- a/tests/StackExchange.Redis.Tests/LexTests.cs +++ b/tests/StackExchange.Redis.Tests/LexTests.cs @@ -1,17 +1,14 @@ -using Xunit; -using Xunit.Abstractions; +using System.Threading.Tasks; +using Xunit; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class LexTests : TestBase +public class LexTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public LexTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void QueryRangeAndLengthByLex() + public async Task QueryRangeAndLengthByLex() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -19,8 +16,7 @@ public void QueryRangeAndLengthByLex() db.SortedSetAdd( key, - new[] - { + [ new SortedSetEntry("a", 0), new SortedSetEntry("b", 0), new SortedSetEntry("c", 0), @@ -28,7 +24,7 @@ public void QueryRangeAndLengthByLex() new SortedSetEntry("e", 0), new SortedSetEntry("f", 0), new SortedSetEntry("g", 0), - }, + ], CommandFlags.FireAndForget); var set = db.SortedSetRangeByValue(key, default(RedisValue), "c"); @@ -58,9 +54,9 @@ public void QueryRangeAndLengthByLex() } [Fact] - public void RemoveRangeByLex() + public async Task RemoveRangeByLex() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -68,25 +64,23 @@ public void RemoveRangeByLex() db.SortedSetAdd( key, - new[] - { + [ new SortedSetEntry("aaaa", 0), new SortedSetEntry("b", 0), new SortedSetEntry("c", 0), new SortedSetEntry("d", 0), new SortedSetEntry("e", 0), - }, + ], CommandFlags.FireAndForget); db.SortedSetAdd( key, - new[] - { + [ new SortedSetEntry("foo", 0), new SortedSetEntry("zap", 0), new SortedSetEntry("zip", 0), new SortedSetEntry("ALPHA", 0), new SortedSetEntry("alpha", 0), - }, + ], CommandFlags.FireAndForget); var set = db.SortedSetRangeByRank(key); diff --git a/tests/StackExchange.Redis.Tests/ListTests.cs b/tests/StackExchange.Redis.Tests/ListTests.cs index 5fdb5d60a..cd0f2e0a3 100644 --- a/tests/StackExchange.Redis.Tests/ListTests.cs +++ b/tests/StackExchange.Redis.Tests/ListTests.cs @@ -2,20 +2,16 @@ using System.Linq; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class ListTests : TestBase +public class ListTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public ListTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void Ranges() + public async Task Ranges() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -35,9 +31,9 @@ public void Ranges() } [Fact] - public void ListLeftPushEmptyValues() + public async Task ListLeftPushEmptyValues() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -47,29 +43,29 @@ public void ListLeftPushEmptyValues() } [Fact] - public void ListLeftPushKeyDoesNotExists() + public async Task ListLeftPushKeyDoesNotExists() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - var result = db.ListLeftPush(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); + var result = db.ListLeftPush(key, ["testvalue"], When.Exists, CommandFlags.None); Assert.Equal(0, result); } [Fact] - public void ListLeftPushToExisitingKey() + public async Task ListLeftPushToExisitingKey() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - var pushResult = db.ListLeftPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + var pushResult = db.ListLeftPush(key, ["testvalue1"], CommandFlags.None); Assert.Equal(1, pushResult); - var pushXResult = db.ListLeftPush(key, new RedisValue[] { "testvalue2" }, When.Exists, CommandFlags.None); + var pushXResult = db.ListLeftPush(key, ["testvalue2"], When.Exists, CommandFlags.None); Assert.Equal(2, pushXResult); var rangeResult = db.ListRange(key, 0, -1); @@ -79,17 +75,17 @@ public void ListLeftPushToExisitingKey() } [Fact] - public void ListLeftPushMultipleToExisitingKey() + public async Task ListLeftPushMultipleToExisitingKey() { - using var conn = Create(require: RedisFeatures.v4_0_0); + await using var conn = Create(require: RedisFeatures.v4_0_0); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - var pushResult = db.ListLeftPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + var pushResult = db.ListLeftPush(key, ["testvalue1"], CommandFlags.None); Assert.Equal(1, pushResult); - var pushXResult = db.ListLeftPush(key, new RedisValue[] { "testvalue2", "testvalue3" }, When.Exists, CommandFlags.None); + var pushXResult = db.ListLeftPush(key, ["testvalue2", "testvalue3"], When.Exists, CommandFlags.None); Assert.Equal(3, pushXResult); var rangeResult = db.ListRange(key, 0, -1); @@ -102,7 +98,7 @@ public void ListLeftPushMultipleToExisitingKey() [Fact] public async Task ListLeftPushAsyncEmptyValues() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -114,27 +110,27 @@ public async Task ListLeftPushAsyncEmptyValues() [Fact] public async Task ListLeftPushAsyncKeyDoesNotExists() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - var result = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); + var result = await db.ListLeftPushAsync(key, ["testvalue"], When.Exists, CommandFlags.None); Assert.Equal(0, result); } [Fact] public async Task ListLeftPushAsyncToExisitingKey() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - var pushResult = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + var pushResult = await db.ListLeftPushAsync(key, ["testvalue1"], CommandFlags.None); Assert.Equal(1, pushResult); - var pushXResult = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue2" }, When.Exists, CommandFlags.None); + var pushXResult = await db.ListLeftPushAsync(key, ["testvalue2"], When.Exists, CommandFlags.None); Assert.Equal(2, pushXResult); var rangeResult = db.ListRange(key, 0, -1); @@ -146,15 +142,15 @@ public async Task ListLeftPushAsyncToExisitingKey() [Fact] public async Task ListLeftPushAsyncMultipleToExisitingKey() { - using var conn = Create(require: RedisFeatures.v4_0_0); + await using var conn = Create(require: RedisFeatures.v4_0_0); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - var pushResult = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + var pushResult = await db.ListLeftPushAsync(key, ["testvalue1"], CommandFlags.None); Assert.Equal(1, pushResult); - var pushXResult = await db.ListLeftPushAsync(key, new RedisValue[] { "testvalue2", "testvalue3" }, When.Exists, CommandFlags.None); + var pushXResult = await db.ListLeftPushAsync(key, ["testvalue2", "testvalue3"], When.Exists, CommandFlags.None); Assert.Equal(3, pushXResult); var rangeResult = db.ListRange(key, 0, -1); @@ -165,9 +161,9 @@ public async Task ListLeftPushAsyncMultipleToExisitingKey() } [Fact] - public void ListRightPushEmptyValues() + public async Task ListRightPushEmptyValues() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -177,29 +173,29 @@ public void ListRightPushEmptyValues() } [Fact] - public void ListRightPushKeyDoesNotExists() + public async Task ListRightPushKeyDoesNotExists() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - var result = db.ListRightPush(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); + var result = db.ListRightPush(key, ["testvalue"], When.Exists, CommandFlags.None); Assert.Equal(0, result); } [Fact] - public void ListRightPushToExisitingKey() + public async Task ListRightPushToExisitingKey() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - var pushResult = db.ListRightPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + var pushResult = db.ListRightPush(key, ["testvalue1"], CommandFlags.None); Assert.Equal(1, pushResult); - var pushXResult = db.ListRightPush(key, new RedisValue[] { "testvalue2" }, When.Exists, CommandFlags.None); + var pushXResult = db.ListRightPush(key, ["testvalue2"], When.Exists, CommandFlags.None); Assert.Equal(2, pushXResult); var rangeResult = db.ListRange(key, 0, -1); @@ -209,17 +205,17 @@ public void ListRightPushToExisitingKey() } [Fact] - public void ListRightPushMultipleToExisitingKey() + public async Task ListRightPushMultipleToExisitingKey() { - using var conn = Create(require: RedisFeatures.v4_0_0); + await using var conn = Create(require: RedisFeatures.v4_0_0); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - var pushResult = db.ListRightPush(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + var pushResult = db.ListRightPush(key, ["testvalue1"], CommandFlags.None); Assert.Equal(1, pushResult); - var pushXResult = db.ListRightPush(key, new RedisValue[] { "testvalue2", "testvalue3" }, When.Exists, CommandFlags.None); + var pushXResult = db.ListRightPush(key, ["testvalue2", "testvalue3"], When.Exists, CommandFlags.None); Assert.Equal(3, pushXResult); var rangeResult = db.ListRange(key, 0, -1); @@ -232,7 +228,7 @@ public void ListRightPushMultipleToExisitingKey() [Fact] public async Task ListRightPushAsyncEmptyValues() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -244,27 +240,27 @@ public async Task ListRightPushAsyncEmptyValues() [Fact] public async Task ListRightPushAsyncKeyDoesNotExists() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - var result = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue" }, When.Exists, CommandFlags.None); + var result = await db.ListRightPushAsync(key, ["testvalue"], When.Exists, CommandFlags.None); Assert.Equal(0, result); } [Fact] public async Task ListRightPushAsyncToExisitingKey() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - var pushResult = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + var pushResult = await db.ListRightPushAsync(key, ["testvalue1"], CommandFlags.None); Assert.Equal(1, pushResult); - var pushXResult = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue2" }, When.Exists, CommandFlags.None); + var pushXResult = await db.ListRightPushAsync(key, ["testvalue2"], When.Exists, CommandFlags.None); Assert.Equal(2, pushXResult); var rangeResult = db.ListRange(key, 0, -1); @@ -276,15 +272,15 @@ public async Task ListRightPushAsyncToExisitingKey() [Fact] public async Task ListRightPushAsyncMultipleToExisitingKey() { - using var conn = Create(require: RedisFeatures.v4_0_0); + await using var conn = Create(require: RedisFeatures.v4_0_0); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - var pushResult = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue1" }, CommandFlags.None); + var pushResult = await db.ListRightPushAsync(key, ["testvalue1"], CommandFlags.None); Assert.Equal(1, pushResult); - var pushXResult = await db.ListRightPushAsync(key, new RedisValue[] { "testvalue2", "testvalue3" }, When.Exists, CommandFlags.None); + var pushXResult = await db.ListRightPushAsync(key, ["testvalue2", "testvalue3"], When.Exists, CommandFlags.None); Assert.Equal(3, pushXResult); var rangeResult = db.ListRange(key, 0, -1); @@ -297,14 +293,14 @@ public async Task ListRightPushAsyncMultipleToExisitingKey() [Fact] public async Task ListMove() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); RedisKey src = Me(); RedisKey dest = Me() + "dest"; db.KeyDelete(src, CommandFlags.FireAndForget); - var pushResult = await db.ListRightPushAsync(src, new RedisValue[] { "testvalue1", "testvalue2" }); + var pushResult = await db.ListRightPushAsync(src, ["testvalue1", "testvalue2"]); Assert.Equal(2, pushResult); var rangeResult1 = db.ListMove(src, dest, ListSide.Left, ListSide.Right); @@ -318,9 +314,9 @@ public async Task ListMove() } [Fact] - public void ListMoveKeyDoesNotExist() + public async Task ListMoveKeyDoesNotExist() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); RedisKey src = Me(); @@ -332,9 +328,9 @@ public void ListMoveKeyDoesNotExist() } [Fact] - public void ListPositionHappyPath() + public async Task ListPositionHappyPath() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -348,9 +344,9 @@ public void ListPositionHappyPath() } [Fact] - public void ListPositionEmpty() + public async Task ListPositionEmpty() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -363,9 +359,9 @@ public void ListPositionEmpty() } [Fact] - public void ListPositionsHappyPath() + public async Task ListPositionsHappyPath() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -393,9 +389,9 @@ public void ListPositionsHappyPath() } [Fact] - public void ListPositionsTooFew() + public async Task ListPositionsTooFew() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -419,9 +415,9 @@ public void ListPositionsTooFew() } [Fact] - public void ListPositionsAll() + public async Task ListPositionsAll() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -449,9 +445,9 @@ public void ListPositionsAll() } [Fact] - public void ListPositionsAllLimitLength() + public async Task ListPositionsAllLimitLength() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -479,9 +475,9 @@ public void ListPositionsAllLimitLength() } [Fact] - public void ListPositionsEmpty() + public async Task ListPositionsEmpty() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -503,9 +499,9 @@ public void ListPositionsEmpty() } [Fact] - public void ListPositionByRank() + public async Task ListPositionByRank() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -530,9 +526,9 @@ public void ListPositionByRank() } [Fact] - public void ListPositionLimitSoNull() + public async Task ListPositionLimitSoNull() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -558,7 +554,7 @@ public void ListPositionLimitSoNull() [Fact] public async Task ListPositionHappyPathAsync() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -574,7 +570,7 @@ public async Task ListPositionHappyPathAsync() [Fact] public async Task ListPositionEmptyAsync() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -589,7 +585,7 @@ public async Task ListPositionEmptyAsync() [Fact] public async Task ListPositionsHappyPathAsync() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -619,7 +615,7 @@ public async Task ListPositionsHappyPathAsync() [Fact] public async Task ListPositionsTooFewAsync() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -645,7 +641,7 @@ public async Task ListPositionsTooFewAsync() [Fact] public async Task ListPositionsAllAsync() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -675,7 +671,7 @@ public async Task ListPositionsAllAsync() [Fact] public async Task ListPositionsAllLimitLengthAsync() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -705,7 +701,7 @@ public async Task ListPositionsAllLimitLengthAsync() [Fact] public async Task ListPositionsEmptyAsync() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -729,7 +725,7 @@ public async Task ListPositionsEmptyAsync() [Fact] public async Task ListPositionByRankAsync() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -756,7 +752,7 @@ public async Task ListPositionByRankAsync() [Fact] public async Task ListPositionLimitSoNullAsync() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -782,7 +778,7 @@ public async Task ListPositionLimitSoNullAsync() [Fact] public async Task ListPositionFireAndForgetAsync() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -807,9 +803,9 @@ public async Task ListPositionFireAndForgetAsync() } [Fact] - public void ListPositionFireAndForget() + public async Task ListPositionFireAndForget() { - using var conn = Create(require: RedisFeatures.v6_0_6); + await using var conn = Create(require: RedisFeatures.v6_0_6); var db = conn.GetDatabase(); var key = Me(); @@ -836,7 +832,7 @@ public void ListPositionFireAndForget() [Fact] public async Task ListMultiPopSingleKeyAsync() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key = Me(); @@ -848,13 +844,13 @@ public async Task ListMultiPopSingleKeyAsync() db.ListLeftPush(key, "red sox"); db.ListLeftPush(key, "rays"); - var res = await db.ListLeftPopAsync(new RedisKey[] { key }, 1); + var res = await db.ListLeftPopAsync([key], 1); Assert.False(res.IsNull); Assert.Single(res.Values); Assert.Equal("rays", res.Values[0]); - res = await db.ListRightPopAsync(new RedisKey[] { key }, 2); + res = await db.ListRightPopAsync([key], 2); Assert.False(res.IsNull); Assert.Equal(2, res.Values.Length); @@ -865,7 +861,7 @@ public async Task ListMultiPopSingleKeyAsync() [Fact] public async Task ListMultiPopMultipleKeysAsync() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key = Me(); @@ -877,14 +873,14 @@ public async Task ListMultiPopMultipleKeysAsync() db.ListLeftPush(key, "red sox"); db.ListLeftPush(key, "rays"); - var res = await db.ListLeftPopAsync(new RedisKey[] { "empty-key", key, "also-empty" }, 2); + var res = await db.ListLeftPopAsync(["empty-key", key, "also-empty"], 2); Assert.False(res.IsNull); Assert.Equal(2, res.Values.Length); Assert.Equal("rays", res.Values[0]); Assert.Equal("red sox", res.Values[1]); - res = await db.ListRightPopAsync(new RedisKey[] { "empty-key", key, "also-empty" }, 1); + res = await db.ListRightPopAsync(["empty-key", key, "also-empty"], 1); Assert.False(res.IsNull); Assert.Single(res.Values); @@ -892,9 +888,9 @@ public async Task ListMultiPopMultipleKeysAsync() } [Fact] - public void ListMultiPopSingleKey() + public async Task ListMultiPopSingleKey() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key = Me(); @@ -906,13 +902,13 @@ public void ListMultiPopSingleKey() db.ListLeftPush(key, "red sox"); db.ListLeftPush(key, "rays"); - var res = db.ListLeftPop(new RedisKey[] { key }, 1); + var res = db.ListLeftPop([key], 1); Assert.False(res.IsNull); Assert.Single(res.Values); Assert.Equal("rays", res.Values[0]); - res = db.ListRightPop(new RedisKey[] { key }, 2); + res = db.ListRightPop([key], 2); Assert.False(res.IsNull); Assert.Equal(2, res.Values.Length); @@ -923,33 +919,33 @@ public void ListMultiPopSingleKey() [Fact] public async Task ListMultiPopZeroCount() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key = Me(); db.KeyDelete(key); - var exception = await Assert.ThrowsAsync(() => db.ListLeftPopAsync(new RedisKey[] { key }, 0)); + var exception = await Assert.ThrowsAsync(() => db.ListLeftPopAsync([key], 0)); Assert.Contains("ERR count should be greater than 0", exception.Message); } [Fact] public async Task ListMultiPopEmpty() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key = Me(); db.KeyDelete(key); - var res = await db.ListLeftPopAsync(new RedisKey[] { key }, 1); + var res = await db.ListLeftPopAsync([key], 1); Assert.True(res.IsNull); } [Fact] - public void ListMultiPopEmptyKeys() + public async Task ListMultiPopEmptyKeys() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var exception = Assert.Throws(() => db.ListRightPop(Array.Empty(), 5)); diff --git a/tests/StackExchange.Redis.Tests/LockingTests.cs b/tests/StackExchange.Redis.Tests/LockingTests.cs index 4f9dbd402..52d03bb83 100644 --- a/tests/StackExchange.Redis.Tests/LockingTests.cs +++ b/tests/StackExchange.Redis.Tests/LockingTests.cs @@ -3,16 +3,12 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class LockingTests : TestBase +public class LockingTests(ITestOutputHelper output) : TestBase(output) { - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public LockingTests(ITestOutputHelper output) : base(output) { } - public enum TestMode { MultiExec, @@ -20,11 +16,11 @@ public enum TestMode Twemproxy, } - public static IEnumerable TestModes() + public static IEnumerable> TestModes() { - yield return new object[] { TestMode.MultiExec }; - yield return new object[] { TestMode.NoMultiExec }; - yield return new object[] { TestMode.Twemproxy }; + yield return new(TestMode.MultiExec); + yield return new(TestMode.NoMultiExec); + yield return new(TestMode.Twemproxy); } [Theory, MemberData(nameof(TestModes))] @@ -67,9 +63,9 @@ void Inner(object? obj) } [Fact] - public void TestOpCountByVersionLocal_UpLevel() + public async Task TestOpCountByVersionLocal_UpLevel() { - using var conn = Create(shared: false); + await using var conn = Create(shared: false); TestLockOpCountByVersion(conn, 1, false); TestLockOpCountByVersion(conn, 1, true); @@ -105,7 +101,7 @@ private void TestLockOpCountByVersion(IConnectionMultiplexer conn, int expectedO private IConnectionMultiplexer Create(TestMode mode) => mode switch { TestMode.MultiExec => Create(), - TestMode.NoMultiExec => Create(disabledCommands: new[] { "multi", "exec" }), + TestMode.NoMultiExec => Create(disabledCommands: ["multi", "exec"]), TestMode.Twemproxy => Create(proxy: Proxy.Twemproxy), _ => throw new NotSupportedException(mode.ToString()), }; @@ -113,7 +109,7 @@ private void TestLockOpCountByVersion(IConnectionMultiplexer conn, int expectedO [Theory, MemberData(nameof(TestModes))] public async Task TakeLockAndExtend(TestMode testMode) { - using var conn = Create(testMode); + await using var conn = Create(testMode); RedisValue right = Guid.NewGuid().ToString(), wrong = Guid.NewGuid().ToString(); @@ -165,7 +161,7 @@ public async Task TakeLockAndExtend(TestMode testMode) [Theory, MemberData(nameof(TestModes))] public async Task TestBasicLockNotTaken(TestMode testMode) { - using var conn = Create(testMode); + await using var conn = Create(testMode); int errorCount = 0; conn.ErrorMessage += (sender, e) => Interlocked.Increment(ref errorCount); @@ -194,7 +190,7 @@ public async Task TestBasicLockNotTaken(TestMode testMode) [Theory, MemberData(nameof(TestModes))] public async Task TestBasicLockTaken(TestMode testMode) { - using var conn = Create(testMode); + await using var conn = Create(testMode); var db = conn.GetDatabase(); var key = Me() + testMode; diff --git a/tests/StackExchange.Redis.Tests/LoggerTests.cs b/tests/StackExchange.Redis.Tests/LoggerTests.cs index 75077f9a9..bc097d24c 100644 --- a/tests/StackExchange.Redis.Tests/LoggerTests.cs +++ b/tests/StackExchange.Redis.Tests/LoggerTests.cs @@ -6,16 +6,12 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class LoggerTests : TestBase +public class LoggerTests(ITestOutputHelper output) : TestBase(output) { - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public LoggerTests(ITestOutputHelper output) : base(output) { } - [Fact] public async Task BasicLoggerConfig() { @@ -29,7 +25,7 @@ public async Task BasicLoggerConfig() var options = ConfigurationOptions.Parse(GetConfiguration()); options.LoggerFactory = new TestWrapperLoggerFactory(new TestMultiLogger(traceLogger, debugLogger, infoLogger, warningLogger, errorLogger, criticalLogger)); - using var conn = await ConnectionMultiplexer.ConnectAsync(options); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options); // We expect more at the trace level: GET, ECHO, PING on commands Assert.True(traceLogger.CallCount > debugLogger.CallCount); // Many calls for all log lines - don't set exact here since every addition would break the test @@ -48,26 +44,23 @@ public async Task WrappedLogger() var wrapped = new TestWrapperLoggerFactory(NullLogger.Instance); options.LoggerFactory = wrapped; - using var conn = await ConnectionMultiplexer.ConnectAsync(options); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options); Assert.True(wrapped.Logger.LogCount > 0); } - public class TestWrapperLoggerFactory : ILoggerFactory + public class TestWrapperLoggerFactory(ILogger logger) : ILoggerFactory { - public TestWrapperLogger Logger { get; } - public TestWrapperLoggerFactory(ILogger logger) => Logger = new TestWrapperLogger(logger); + public TestWrapperLogger Logger { get; } = new TestWrapperLogger(logger); public void AddProvider(ILoggerProvider provider) => throw new NotImplementedException(); public ILogger CreateLogger(string categoryName) => Logger; public void Dispose() { } } - public class TestWrapperLogger : ILogger + public class TestWrapperLogger(ILogger toWrap) : ILogger { public int LogCount = 0; - private ILogger Inner { get; } - - public TestWrapperLogger(ILogger toWrap) => Inner = toWrap; + private ILogger Inner { get; } = toWrap; #if NET8_0_OR_GREATER public IDisposable? BeginScope(TState state) where TState : notnull => Inner.BeginScope(state); @@ -85,11 +78,8 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except /// /// To save on test time, no reason to spin up n connections just to test n logging implementations... /// - private class TestMultiLogger : ILogger + private class TestMultiLogger(params ILogger[] loggers) : ILogger { - private readonly ILogger[] _loggers; - public TestMultiLogger(params ILogger[] loggers) => _loggers = loggers; - #if NET8_0_OR_GREATER public IDisposable? BeginScope(TState state) where TState : notnull => throw new NotImplementedException(); #else @@ -98,7 +88,7 @@ private class TestMultiLogger : ILogger public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - foreach (var logger in _loggers) + foreach (var logger in loggers) { logger.Log(logLevel, eventId, state, exception, formatter); } diff --git a/tests/StackExchange.Redis.Tests/MassiveOpsTests.cs b/tests/StackExchange.Redis.Tests/MassiveOpsTests.cs index 3b3f8157e..0140c7c97 100644 --- a/tests/StackExchange.Redis.Tests/MassiveOpsTests.cs +++ b/tests/StackExchange.Redis.Tests/MassiveOpsTests.cs @@ -2,19 +2,17 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class MassiveOpsTests : TestBase +public class MassiveOpsTests(ITestOutputHelper output) : TestBase(output) { - public MassiveOpsTests(ITestOutputHelper output) : base(output) { } - - [FactLongRunning] + [Fact] public async Task LongRunning() { - using var conn = Create(); + Skip.UnlessLongRunning(); + await using var conn = Create(); var key = Me(); var db = conn.GetDatabase(); @@ -33,7 +31,7 @@ public async Task LongRunning() [InlineData(false)] public async Task MassiveBulkOpsAsync(bool withContinuation) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(); var db = conn.GetDatabase(); @@ -57,14 +55,15 @@ static void NonTrivial(Task unused) Log($"{Me()}: Time for {AsyncOpsQty} ops: {watch.ElapsedMilliseconds}ms ({(withContinuation ? "with continuation" : "no continuation")}, any order); ops/s: {AsyncOpsQty / watch.Elapsed.TotalSeconds}"); } - [TheoryLongRunning] + [Theory] [InlineData(1)] [InlineData(5)] [InlineData(10)] [InlineData(50)] - public void MassiveBulkOpsSync(int threads) + public async Task MassiveBulkOpsSync(int threads) { - using var conn = Create(syncTimeout: 30000); + Skip.UnlessLongRunning(); + await using var conn = Create(syncTimeout: 30000); RedisKey key = Me(); var db = conn.GetDatabase(); @@ -88,13 +87,13 @@ public void MassiveBulkOpsSync(int threads) [Theory] [InlineData(1)] [InlineData(5)] - public void MassiveBulkOpsFireAndForget(int threads) + public async Task MassiveBulkOpsFireAndForget(int threads) { - using var conn = Create(syncTimeout: 30000); + await using var conn = Create(syncTimeout: 30000); RedisKey key = Me(); var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); db.KeyDelete(key, CommandFlags.FireAndForget); int perThread = AsyncOpsQty / threads; diff --git a/tests/StackExchange.Redis.Tests/MemoryTests.cs b/tests/StackExchange.Redis.Tests/MemoryTests.cs index 50812e597..48d3ea705 100644 --- a/tests/StackExchange.Redis.Tests/MemoryTests.cs +++ b/tests/StackExchange.Redis.Tests/MemoryTests.cs @@ -1,18 +1,14 @@ using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class MemoryTests : TestBase +public class MemoryTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public MemoryTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] public async Task CanCallDoctor() { - using var conn = Create(require: RedisFeatures.v4_0_0); + await using var conn = Create(require: RedisFeatures.v4_0_0); var server = conn.GetServer(conn.GetEndPoints()[0]); string? doctor = server.MemoryDoctor(); @@ -27,7 +23,7 @@ public async Task CanCallDoctor() [Fact] public async Task CanPurge() { - using var conn = Create(require: RedisFeatures.v4_0_0); + await using var conn = Create(require: RedisFeatures.v4_0_0); var server = conn.GetServer(conn.GetEndPoints()[0]); server.MemoryPurge(); @@ -39,7 +35,7 @@ public async Task CanPurge() [Fact] public async Task GetAllocatorStats() { - using var conn = Create(require: RedisFeatures.v4_0_0); + await using var conn = Create(require: RedisFeatures.v4_0_0); var server = conn.GetServer(conn.GetEndPoints()[0]); @@ -53,7 +49,7 @@ public async Task GetAllocatorStats() [Fact] public async Task GetStats() { - using var conn = Create(require: RedisFeatures.v4_0_0); + await using var conn = Create(require: RedisFeatures.v4_0_0); var server = conn.GetServer(conn.GetEndPoints()[0]); var stats = server.MemoryStats(); diff --git a/tests/StackExchange.Redis.Tests/MigrateTests.cs b/tests/StackExchange.Redis.Tests/MigrateTests.cs index 54fe77649..9939e0632 100644 --- a/tests/StackExchange.Redis.Tests/MigrateTests.cs +++ b/tests/StackExchange.Redis.Tests/MigrateTests.cs @@ -2,25 +2,23 @@ using System.Linq; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class MigrateTests : TestBase +public class MigrateTests(ITestOutputHelper output) : TestBase(output) { - public MigrateTests(ITestOutputHelper output) : base(output) { } - - [FactLongRunning] + [Fact] public async Task Basic() { + Skip.UnlessLongRunning(); var fromConfig = new ConfigurationOptions { EndPoints = { { TestConfig.Current.SecureServer, TestConfig.Current.SecurePort } }, Password = TestConfig.Current.SecurePassword, AllowAdmin = true }; var toConfig = new ConfigurationOptions { EndPoints = { { TestConfig.Current.PrimaryServer, TestConfig.Current.PrimaryPort } }, AllowAdmin = true }; - using var fromConn = ConnectionMultiplexer.Connect(fromConfig, Writer); - using var toConn = ConnectionMultiplexer.Connect(toConfig, Writer); + await using var fromConn = ConnectionMultiplexer.Connect(fromConfig, Writer); + await using var toConn = ConnectionMultiplexer.Connect(toConfig, Writer); if (await IsWindows(fromConn) || await IsWindows(toConn)) - Skip.Inconclusive("'migrate' is unreliable on redis-64"); + Assert.Skip("'migrate' is unreliable on redis-64"); RedisKey key = Me(); var fromDb = fromConn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/MultiAddTests.cs b/tests/StackExchange.Redis.Tests/MultiAddTests.cs index cf5f70c23..f5fb66335 100644 --- a/tests/StackExchange.Redis.Tests/MultiAddTests.cs +++ b/tests/StackExchange.Redis.Tests/MultiAddTests.cs @@ -1,18 +1,15 @@ using System.Linq; +using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class MultiAddTests : TestBase +public class MultiAddTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public MultiAddTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void AddSortedSetEveryWay() + public async Task AddSortedSetEveryWay() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -21,37 +18,33 @@ public void AddSortedSetEveryWay() db.SortedSetAdd(key, "a", 1, CommandFlags.FireAndForget); db.SortedSetAdd( key, - new[] - { + [ new SortedSetEntry("b", 2), - }, + ], CommandFlags.FireAndForget); db.SortedSetAdd( key, - new[] - { + [ new SortedSetEntry("c", 3), new SortedSetEntry("d", 4), - }, + ], CommandFlags.FireAndForget); db.SortedSetAdd( key, - new[] - { + [ new SortedSetEntry("e", 5), new SortedSetEntry("f", 6), new SortedSetEntry("g", 7), - }, + ], CommandFlags.FireAndForget); db.SortedSetAdd( key, - new[] - { + [ new SortedSetEntry("h", 8), new SortedSetEntry("i", 9), new SortedSetEntry("j", 10), new SortedSetEntry("k", 11), - }, + ], CommandFlags.FireAndForget); var vals = db.SortedSetRangeByScoreWithScores(key); string s = string.Join(",", vals.OrderByDescending(x => x.Score).Select(x => x.Element)); @@ -61,9 +54,9 @@ public void AddSortedSetEveryWay() } [Fact] - public void AddHashEveryWay() + public async Task AddHashEveryWay() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -72,37 +65,33 @@ public void AddHashEveryWay() db.HashSet(key, "a", 1, flags: CommandFlags.FireAndForget); db.HashSet( key, - new[] - { + [ new HashEntry("b", 2), - }, + ], CommandFlags.FireAndForget); db.HashSet( key, - new[] - { + [ new HashEntry("c", 3), new HashEntry("d", 4), - }, + ], CommandFlags.FireAndForget); db.HashSet( key, - new[] - { + [ new HashEntry("e", 5), new HashEntry("f", 6), new HashEntry("g", 7), - }, + ], CommandFlags.FireAndForget); db.HashSet( key, - new[] - { + [ new HashEntry("h", 8), new HashEntry("i", 9), new HashEntry("j", 10), new HashEntry("k", 11), - }, + ], CommandFlags.FireAndForget); var vals = db.HashGetAll(key); string s = string.Join(",", vals.OrderByDescending(x => (double)x.Value).Select(x => x.Name)); @@ -112,19 +101,19 @@ public void AddHashEveryWay() } [Fact] - public void AddSetEveryWay() + public async Task AddSetEveryWay() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); db.SetAdd(key, "a", CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "b" }, CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "c", "d" }, CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "e", "f", "g" }, CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "h", "i", "j", "k" }, CommandFlags.FireAndForget); + db.SetAdd(key, ["b"], CommandFlags.FireAndForget); + db.SetAdd(key, ["c", "d"], CommandFlags.FireAndForget); + db.SetAdd(key, ["e", "f", "g"], CommandFlags.FireAndForget); + db.SetAdd(key, ["h", "i", "j", "k"], CommandFlags.FireAndForget); var vals = db.SetMembers(key); string s = string.Join(",", vals.OrderByDescending(x => x)); @@ -132,18 +121,18 @@ public void AddSetEveryWay() } [Fact] - public void AddSetEveryWayNumbers() + public async Task AddSetEveryWayNumbers() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); db.SetAdd(key, "a", CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "1" }, CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "11", "2" }, CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "10", "3", "1.5" }, CommandFlags.FireAndForget); - db.SetAdd(key, new RedisValue[] { "2.2", "-1", "s", "t" }, CommandFlags.FireAndForget); + db.SetAdd(key, ["1"], CommandFlags.FireAndForget); + db.SetAdd(key, ["11", "2"], CommandFlags.FireAndForget); + db.SetAdd(key, ["10", "3", "1.5"], CommandFlags.FireAndForget); + db.SetAdd(key, ["2.2", "-1", "s", "t"], CommandFlags.FireAndForget); var vals = db.SetMembers(key); string s = string.Join(",", vals.OrderByDescending(x => x)); diff --git a/tests/StackExchange.Redis.Tests/MultiPrimaryTests.cs b/tests/StackExchange.Redis.Tests/MultiPrimaryTests.cs index bf3e5690e..3d88e097c 100644 --- a/tests/StackExchange.Redis.Tests/MultiPrimaryTests.cs +++ b/tests/StackExchange.Redis.Tests/MultiPrimaryTests.cs @@ -1,23 +1,22 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class MultiPrimaryTests : TestBase +public class MultiPrimaryTests(ITestOutputHelper output) : TestBase(output) { protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword; - public MultiPrimaryTests(ITestOutputHelper output) : base(output) { } [Fact] - public void CannotFlushReplica() + public async Task CannotFlushReplica() { - var ex = Assert.Throws(() => + var ex = await Assert.ThrowsAsync(async () => { - using var conn = ConnectionMultiplexer.Connect(TestConfig.Current.ReplicaServerAndPort + ",allowAdmin=true"); + await using var conn = await ConnectionMultiplexer.ConnectAsync(TestConfig.Current.ReplicaServerAndPort + ",allowAdmin=true"); var servers = conn.GetEndPoints().Select(e => conn.GetServer(e)); var replica = servers.FirstOrDefault(x => x.IsReplica); @@ -45,7 +44,7 @@ public void TestMultiNoTieBreak() yield return new object?[] { TestConfig.Current.SecureServerAndPort, TestConfig.Current.PrimaryServerAndPort, null }; yield return new object?[] { TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.SecureServerAndPort, null }; - yield return new object?[] { null, TestConfig.Current.PrimaryServerAndPort, TestConfig.Current.PrimaryServerAndPort }; + yield return new object?[] { null, TestConfig.Current.PrimaryServerAndPort, null }; yield return new object?[] { TestConfig.Current.PrimaryServerAndPort, null, TestConfig.Current.PrimaryServerAndPort }; yield return new object?[] { null, TestConfig.Current.SecureServerAndPort, TestConfig.Current.SecureServerAndPort }; yield return new object?[] { TestConfig.Current.SecureServerAndPort, null, TestConfig.Current.SecureServerAndPort }; diff --git a/tests/StackExchange.Redis.Tests/NamingTests.cs b/tests/StackExchange.Redis.Tests/NamingTests.cs index 2990e04c4..d0474f782 100644 --- a/tests/StackExchange.Redis.Tests/NamingTests.cs +++ b/tests/StackExchange.Redis.Tests/NamingTests.cs @@ -5,14 +5,11 @@ using System.Reflection; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class NamingTests : TestBase +public class NamingTests(ITestOutputHelper output) : TestBase(output) { - public NamingTests(ITestOutputHelper output) : base(output) { } - [Theory] [InlineData(typeof(IDatabase), false)] [InlineData(typeof(IDatabaseAsync), true)] diff --git a/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs b/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs index f562a850b..c695488f2 100644 --- a/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs +++ b/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; @@ -9,15 +8,12 @@ namespace StackExchange.Redis.Tests; /// This test set is for when we add an overload, to making sure all /// past versions work correctly and aren't source breaking. /// -[Collection(SharedConnectionFixture.Key)] -public class OverloadCompatTests : TestBase +public class OverloadCompatTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public OverloadCompatTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] public async Task KeyExpire() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); var expiresIn = TimeSpan.FromSeconds(10); @@ -62,7 +58,7 @@ public async Task KeyExpire() [Fact] public async Task StringBitCount() { - using var conn = Create(require: RedisFeatures.v2_6_0); + await using var conn = Create(require: RedisFeatures.v2_6_0); var db = conn.GetDatabase(); var key = Me(); @@ -108,7 +104,7 @@ public async Task StringBitCount() [Fact] public async Task StringBitPosition() { - using var conn = Create(require: RedisFeatures.v2_6_0); + await using var conn = Create(require: RedisFeatures.v2_6_0); var db = conn.GetDatabase(); var key = Me(); @@ -158,7 +154,7 @@ public async Task StringBitPosition() [Fact] public async Task SortedSetAdd() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); RedisKey key = Me(); RedisValue val = "myval"; @@ -212,7 +208,7 @@ public async Task SortedSetAdd() [Fact] public async Task StringSet() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); var val = "myval"; diff --git a/tests/StackExchange.Redis.Tests/ParseTests.cs b/tests/StackExchange.Redis.Tests/ParseTests.cs index 80e1fbbef..51c17fbf3 100644 --- a/tests/StackExchange.Redis.Tests/ParseTests.cs +++ b/tests/StackExchange.Redis.Tests/ParseTests.cs @@ -4,14 +4,11 @@ using System.Text; using Pipelines.Sockets.Unofficial.Arenas; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class ParseTests : TestBase +public class ParseTests(ITestOutputHelper output) : TestBase(output) { - public ParseTests(ITestOutputHelper output) : base(output) { } - public static IEnumerable GetTestData() { yield return new object[] { "$4\r\nPING\r\n$4\r\nPON", 1 }; diff --git a/tests/StackExchange.Redis.Tests/PerformanceTests.cs b/tests/StackExchange.Redis.Tests/PerformanceTests.cs index 068ff2870..b308bf0ac 100644 --- a/tests/StackExchange.Redis.Tests/PerformanceTests.cs +++ b/tests/StackExchange.Redis.Tests/PerformanceTests.cs @@ -2,26 +2,24 @@ using System.Text; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class PerformanceTests : TestBase +public class PerformanceTests(ITestOutputHelper output) : TestBase(output) { - public PerformanceTests(ITestOutputHelper output) : base(output) { } - - [FactLongRunning] - public void VerifyPerformanceImprovement() + [Fact] + public async Task VerifyPerformanceImprovement() { + Skip.UnlessLongRunning(); int asyncTimer, sync, op = 0, asyncFaF, syncFaF; var key = Me(); - using (var conn = Create()) + await using (var conn = Create()) { // do these outside the timings, just to ensure the core methods are JITted etc for (int dbId = 0; dbId < 5; dbId++) { - conn.GetDatabase(dbId).KeyDeleteAsync(key); + _ = conn.GetDatabase(dbId).KeyDeleteAsync(key); } var timer = Stopwatch.StartNew(); @@ -33,7 +31,9 @@ public void VerifyPerformanceImprovement() { var db = conn.GetDatabase(dbId); for (int j = 0; j < 10; j++) - db.StringIncrementAsync(key); + { + _ = db.StringIncrementAsync(key); + } } } asyncFaF = (int)timer.ElapsedMilliseconds; @@ -102,7 +102,7 @@ public void VerifyPerformanceImprovement() [Fact] public async Task BasicStringGetPerf() { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(); var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/PreserveOrderTests.cs b/tests/StackExchange.Redis.Tests/PreserveOrderTests.cs index 0cb5eb271..ce20ccf7b 100644 --- a/tests/StackExchange.Redis.Tests/PreserveOrderTests.cs +++ b/tests/StackExchange.Redis.Tests/PreserveOrderTests.cs @@ -2,20 +2,17 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading; +using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class PreserveOrderTests : TestBase +public class PreserveOrderTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public PreserveOrderTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void Execute() + public async Task Execute() { - using var conn = Create(); + await using var conn = Create(); var sub = conn.GetSubscriber(); var channel = Me(); diff --git a/tests/StackExchange.Redis.Tests/ProfilingTests.cs b/tests/StackExchange.Redis.Tests/ProfilingTests.cs index 2d0264984..f374788db 100644 --- a/tests/StackExchange.Redis.Tests/ProfilingTests.cs +++ b/tests/StackExchange.Redis.Tests/ProfilingTests.cs @@ -6,19 +6,16 @@ using System.Threading.Tasks; using StackExchange.Redis.Profiling; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class ProfilingTests : TestBase +public class ProfilingTests(ITestOutputHelper output) : TestBase(output) { - public ProfilingTests(ITestOutputHelper output) : base(output) { } - [Fact] - public void Simple() + public async Task Simple() { - using var conn = Create(); + await using var conn = Create(); var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); var script = LuaScript.Prepare("return redis.call('get', @key)"); @@ -49,7 +46,7 @@ public void Simple() var i = 0; foreach (var cmd in cmds) { - Log("Command {0} (DB: {1}): {2}", i++, cmd.Db, cmd?.ToString()?.Replace("\n", ", ")); + Log($"Command {i++} (DB: {cmd.Db}): {cmd?.ToString()?.Replace("\n", ", ")}"); } var all = string.Join(",", cmds.Select(x => x.Command)); @@ -98,10 +95,11 @@ private static void AssertProfiledCommandValues(IProfiledCommand command, IConne Assert.True(command.RetransmissionReason == null, nameof(command.RetransmissionReason)); } - [FactLongRunning] - public void ManyThreads() + [Fact] + public async Task ManyThreads() { - using var conn = Create(); + Skip.UnlessLongRunning(); + await using var conn = Create(); var session = new ProfilingSession(); var prefix = Me(); @@ -156,10 +154,11 @@ public void ManyThreads() } } - [FactLongRunning] - public void ManyContexts() + [Fact] + public async Task ManyContexts() { - using var conn = Create(); + Skip.UnlessLongRunning(); + await using var conn = Create(); var profiler = new AsyncLocalProfiler(); var prefix = Me(); @@ -228,9 +227,9 @@ public ProfilingSession GetSession() } [Fact] - public void LowAllocationEnumerable() + public async Task LowAllocationEnumerable() { - using var conn = Create(); + await using var conn = Create(); const int OuterLoop = 1000; var session = new ProfilingSession(); @@ -273,10 +272,11 @@ public void LowAllocationEnumerable() Assert.Equal(OuterLoop * 2, res.Count(r => r.Db > 0)); } - [FactLongRunning] - public void ProfilingMD_Ex1() + [Fact] + public async Task ProfilingMD_Ex1() { - using var conn = Create(); + Skip.UnlessLongRunning(); + await using var conn = Create(); var session = new ProfilingSession(); var prefix = Me(); @@ -313,10 +313,11 @@ public void ProfilingMD_Ex1() Assert.Equal(16000, timings.Count()); } - [FactLongRunning] - public void ProfilingMD_Ex2() + [Fact] + public async Task ProfilingMD_Ex2() { - using var conn = Create(); + Skip.UnlessLongRunning(); + await using var conn = Create(); var profiler = new PerThreadProfiler(); var prefix = Me(); @@ -356,10 +357,11 @@ public void ProfilingMD_Ex2() Assert.True(perThreadTimings.All(kv => kv.Value.Count == 1000)); } - [FactLongRunning] + [Fact] public async Task ProfilingMD_Ex2_Async() { - using var conn = Create(); + Skip.UnlessLongRunning(); + await using var conn = Create(); var profiler = new AsyncLocalProfiler(); var prefix = Me(); diff --git a/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs b/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs index f3ea9eca7..71b82e262 100644 --- a/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubCommandTests.cs @@ -3,20 +3,16 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class PubSubCommandTests : TestBase +public class PubSubCommandTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public PubSubCommandTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void SubscriberCount() + public async Task SubscriberCount() { - using var conn = Create(); + await using var conn = Create(); #pragma warning disable CS0618 RedisChannel channel = Me() + Guid.NewGuid(); @@ -42,7 +38,7 @@ public void SubscriberCount() [Fact] public async Task SubscriberCountAsync() { - using var conn = Create(); + await using var conn = Create(); #pragma warning disable CS0618 RedisChannel channel = Me() + Guid.NewGuid(); @@ -79,7 +75,7 @@ public static async Task WithTimeout(this Task task, int timeoutMs, [CallerMembe } else { - throw new TimeoutException($"timout from {caller} line {line}"); + throw new TimeoutException($"timeout from {caller} line {line}"); } } public static async Task WithTimeout(this Task task, int timeoutMs, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = 0) diff --git a/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs b/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs index d7a0ee513..43bb4b2b8 100644 --- a/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs @@ -2,22 +2,18 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class PubSubMultiserverTests : TestBase +public class PubSubMultiserverTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public PubSubMultiserverTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; [Fact] - public void ChannelSharding() + public async Task ChannelSharding() { - using var conn = Create(channelPrefix: Me()); + await using var conn = Create(channelPrefix: Me()); var defaultSlot = conn.ServerSelectionStrategy.HashSlot(default(RedisChannel)); var slot1 = conn.ServerSelectionStrategy.HashSlot(RedisChannel.Literal("hey")); @@ -31,9 +27,10 @@ public void ChannelSharding() [Fact] public async Task ClusterNodeSubscriptionFailover() { + Skip.UnlessLongRunning(); Log("Connecting..."); - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true, shared: false); var sub = conn.GetSubscriber(); var channel = RedisChannel.Literal(Me()); @@ -72,7 +69,7 @@ await sub.SubscribeAsync(channel, (_, val) => Log("Connected to: " + initialServer); conn.AllowConnect = false; - if (Context.IsResp3) + if (TestContext.Current.IsResp3()) { subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.All); @@ -106,7 +103,7 @@ await sub.SubscribeAsync(channel, (_, val) => ClearAmbientFailures(); } - [Theory] + [Theory(Skip="TODO: Hostile")] [InlineData(CommandFlags.PreferMaster, true)] [InlineData(CommandFlags.PreferReplica, true)] [InlineData(CommandFlags.DemandMaster, false)] @@ -116,7 +113,7 @@ public async Task PrimaryReplicaSubscriptionFailover(CommandFlags flags, bool ex var config = TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; Log("Connecting..."); - using var conn = Create(configuration: config, shared: false, allowAdmin: true); + await using var conn = Create(configuration: config, shared: false, allowAdmin: true); var sub = conn.GetSubscriber(); var channel = RedisChannel.Literal(Me() + flags.ToString()); // Individual channel per case to not overlap publishers @@ -157,7 +154,7 @@ await sub.SubscribeAsync( Log("Connected to: " + initialServer); conn.AllowConnect = false; - if (Context.IsResp3) + if (TestContext.Current.IsResp3()) { subscribedServerEndpoint.SimulateConnectionFailure(SimulatedFailureType.All); // need to kill the main connection Assert.False(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); diff --git a/tests/StackExchange.Redis.Tests/PubSubTests.cs b/tests/StackExchange.Redis.Tests/PubSubTests.cs index 249b8e63d..9418fe80f 100644 --- a/tests/StackExchange.Redis.Tests/PubSubTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubTests.cs @@ -7,20 +7,16 @@ using System.Threading.Tasks; using StackExchange.Redis.Maintenance; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class PubSubTests : TestBase +public class PubSubTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public PubSubTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] public async Task ExplicitPublishMode() { - using var conn = Create(channelPrefix: "foo:", log: Writer); + await using var conn = Create(channelPrefix: "foo:", log: Writer); var pub = conn.GetSubscriber(); int a = 0, b = 0, c = 0, d = 0; @@ -58,12 +54,12 @@ await UntilConditionAsync( [InlineData("Foo:", true, "f")] public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string breaker) { - using var conn = Create(channelPrefix: channelPrefix, shared: false, log: Writer); + await using var conn = Create(channelPrefix: channelPrefix, shared: false, log: Writer); var pub = GetAnyPrimary(conn); var sub = conn.GetSubscriber(); await PingAsync(pub, sub).ForAwait(); - HashSet received = new(); + HashSet received = []; int secondHandler = 0; string subChannel = (wildCard ? "a*c" : "abc") + breaker; string pubChannel = "abc" + breaker; @@ -143,14 +139,14 @@ public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string b [Fact] public async Task TestBasicPubSubFireAndForget() { - using var conn = Create(shared: false, log: Writer); + await using var conn = Create(shared: false, log: Writer); var profiler = conn.AddProfiler(); var pub = GetAnyPrimary(conn); var sub = conn.GetSubscriber(); RedisChannel key = RedisChannel.Literal(Me() + Guid.NewGuid()); - HashSet received = new(); + HashSet received = []; int secondHandler = 0; await PingAsync(pub, sub).ForAwait(); sub.Subscribe( @@ -218,12 +214,12 @@ private async Task PingAsync(IServer pub, ISubscriber sub, int times = 1) [Fact] public async Task TestPatternPubSub() { - using var conn = Create(shared: false, log: Writer); + await using var conn = Create(shared: false, log: Writer); var pub = GetAnyPrimary(conn); var sub = conn.GetSubscriber(); - HashSet received = new(); + HashSet received = []; int secondHandler = 0; #pragma warning disable CS0618 sub.Subscribe("a*c", (channel, payload) => @@ -275,9 +271,9 @@ public async Task TestPatternPubSub() } [Fact] - public void TestPublishWithNoSubscribers() + public async Task TestPublishWithNoSubscribers() { - using var conn = Create(); + await using var conn = Create(); var sub = conn.GetSubscriber(); #pragma warning disable CS0618 @@ -285,19 +281,21 @@ public void TestPublishWithNoSubscribers() #pragma warning restore CS0618 } - [FactLongRunning] - public void TestMassivePublishWithWithoutFlush_Local() + [Fact] + public async Task TestMassivePublishWithWithoutFlush_Local() { - using var conn = Create(); + Skip.UnlessLongRunning(); + await using var conn = Create(); var sub = conn.GetSubscriber(); TestMassivePublish(sub, Me(), "local"); } - [FactLongRunning] - public void TestMassivePublishWithWithoutFlush_Remote() + [Fact] + public async Task TestMassivePublishWithWithoutFlush_Remote() { - using var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort); + Skip.UnlessLongRunning(); + await using var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort); var sub = conn.GetSubscriber(); TestMassivePublish(sub, Me(), "remote"); @@ -337,7 +335,7 @@ private void TestMassivePublish(ISubscriber sub, string channel, string caption) [Fact] public async Task SubscribeAsyncEnumerable() { - using var conn = Create(syncTimeout: 20000, shared: false, log: Writer); + await using var conn = Create(syncTimeout: 20000, shared: false, log: Writer); var sub = conn.GetSubscriber(); RedisChannel channel = RedisChannel.Literal(Me()); @@ -372,9 +370,9 @@ public async Task SubscribeAsyncEnumerable() [Fact] public async Task PubSubGetAllAnyOrder() { - using var sonn = Create(syncTimeout: 20000, shared: false, log: Writer); + await using var conn = Create(syncTimeout: 20000, shared: false, log: Writer); - var sub = sonn.GetSubscriber(); + var sub = conn.GetSubscriber(); RedisChannel channel = RedisChannel.Literal(Me()); const int count = 1000; var syncLock = new object(); @@ -420,7 +418,7 @@ await sub.SubscribeAsync(channel, (_, val) => [Fact] public async Task PubSubGetAllCorrectOrder() { - using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) + await using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { var sub = conn.GetSubscriber(); RedisChannel channel = RedisChannel.Literal(Me()); @@ -490,7 +488,7 @@ async Task RunLoop() [Fact] public async Task PubSubGetAllCorrectOrder_OnMessage_Sync() { - using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) + await using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { var sub = conn.GetSubscriber(); RedisChannel channel = RedisChannel.Literal(Me()); @@ -556,7 +554,7 @@ public async Task PubSubGetAllCorrectOrder_OnMessage_Sync() [Fact] public async Task PubSubGetAllCorrectOrder_OnMessage_Async() { - using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) + await using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { var sub = conn.GetSubscriber(); RedisChannel channel = RedisChannel.Literal(Me()); @@ -627,9 +625,9 @@ public async Task PubSubGetAllCorrectOrder_OnMessage_Async() [Fact] public async Task TestPublishWithSubscribers() { - using var connA = Create(shared: false, log: Writer); - using var connB = Create(shared: false, log: Writer); - using var connPub = Create(); + await using var connA = Create(shared: false, log: Writer); + await using var connB = Create(shared: false, log: Writer); + await using var connPub = Create(); var channel = Me(); var listenA = connA.GetSubscriber(); @@ -654,14 +652,14 @@ public async Task TestPublishWithSubscribers() [Fact] public async Task TestMultipleSubscribersGetMessage() { - using var connA = Create(shared: false, log: Writer); - using var connB = Create(shared: false, log: Writer); - using var connPub = Create(); + await using var connA = Create(shared: false, log: Writer); + await using var connB = Create(shared: false, log: Writer); + await using var connPub = Create(); var channel = RedisChannel.Literal(Me()); var listenA = connA.GetSubscriber(); var listenB = connB.GetSubscriber(); - connPub.GetDatabase().Ping(); + await connPub.GetDatabase().PingAsync(); var pub = connPub.GetSubscriber(); int gotA = 0, gotB = 0; var tA = listenA.SubscribeAsync(channel, (_, msg) => { if (msg == "message") Interlocked.Increment(ref gotA); }); @@ -684,7 +682,7 @@ public async Task TestMultipleSubscribersGetMessage() [Fact] public async Task Issue38() { - using var conn = Create(log: Writer); + await using var conn = Create(log: Writer); var sub = conn.GetSubscriber(); int count = 0; @@ -719,9 +717,9 @@ public async Task Issue38() [Fact] public async Task TestPartialSubscriberGetMessage() { - using var connA = Create(); - using var connB = Create(); - using var connPub = Create(); + await using var connA = Create(); + await using var connB = Create(); + await using var connPub = Create(); int gotA = 0, gotB = 0; var listenA = connA.GetSubscriber(); @@ -752,8 +750,8 @@ public async Task TestPartialSubscriberGetMessage() [Fact] public async Task TestSubscribeUnsubscribeAndSubscribeAgain() { - using var connPub = Create(); - using var connSub = Create(); + await using var connPub = Create(); + await using var connSub = Create(); var prefix = Me(); var pub = connPub.GetSubscriber(); @@ -799,7 +797,7 @@ public async Task AzureRedisEventsAutomaticSubscribe() using (var connection = await ConnectionMultiplexer.ConnectAsync(options)) { - connection.ServerMaintenanceEvent += (object? _, ServerMaintenanceEvent e) => + connection.ServerMaintenanceEvent += (_, e) => { if (e is AzureMaintenanceEvent) { diff --git a/tests/StackExchange.Redis.Tests/RealWorldTests.cs b/tests/StackExchange.Redis.Tests/RealWorldTests.cs index 340303f22..ba9605b4f 100644 --- a/tests/StackExchange.Redis.Tests/RealWorldTests.cs +++ b/tests/StackExchange.Redis.Tests/RealWorldTests.cs @@ -1,13 +1,10 @@ using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class RealWorldTests : TestBase +public class RealWorldTests(ITestOutputHelper output) : TestBase(output) { - public RealWorldTests(ITestOutputHelper output) : base(output) { } - [Fact] public async Task WhyDoesThisNotWork() { @@ -17,7 +14,7 @@ public async Task WhyDoesThisNotWork() Log("Endpoint 0: {0} (AddressFamily: {1})", config.EndPoints[0], config.EndPoints[0].AddressFamily); Log("Endpoint 1: {0} (AddressFamily: {1})", config.EndPoints[1], config.EndPoints[1].AddressFamily); - using (var conn = ConnectionMultiplexer.Connect("localhost:6379,localhost:6380,name=Core (Q&A),tiebreaker=:RedisPrimary,abortConnect=False", Writer)) + await using (var conn = ConnectionMultiplexer.Connect("localhost:6379,localhost:6380,name=Core (Q&A),tiebreaker=:RedisPrimary,abortConnect=False", Writer)) { Log(""); Log("pausing..."); diff --git a/tests/StackExchange.Redis.Tests/RedisResultTests.cs b/tests/StackExchange.Redis.Tests/RedisResultTests.cs index 47ac20a9e..1f58ceecf 100644 --- a/tests/StackExchange.Redis.Tests/RedisResultTests.cs +++ b/tests/StackExchange.Redis.Tests/RedisResultTests.cs @@ -16,7 +16,7 @@ public sealed class RedisResultTests public void ToDictionaryWorks() { var redisArrayResult = RedisResult.Create( - new RedisValue[] { "one", 1, "two", 2, "three", 3, "four", 4 }); + ["one", 1, "two", 2, "three", 3, "four", 4]); var dict = redisArrayResult.ToDictionary(); @@ -35,14 +35,13 @@ public void ToDictionaryWorks() public void ToDictionaryWorksWhenNested() { var redisArrayResult = RedisResult.Create( - new[] - { + [ RedisResult.Create((RedisValue)"one"), - RedisResult.Create(new RedisValue[] { "two", 2, "three", 3 }), + RedisResult.Create(["two", 2, "three", 3]), RedisResult.Create((RedisValue)"four"), - RedisResult.Create(new RedisValue[] { "five", 5, "six", 6 }), - }); + RedisResult.Create(["five", 5, "six", 6]), + ]); var dict = redisArrayResult.ToDictionary(); var nestedDict = dict["one"].ToDictionary(); @@ -61,7 +60,7 @@ public void ToDictionaryWorksWhenNested() public void ToDictionaryFailsWithDuplicateKeys() { var redisArrayResult = RedisResult.Create( - new RedisValue[] { "banana", 1, "BANANA", 2, "orange", 3, "apple", 4 }); + ["banana", 1, "BANANA", 2, "orange", 3, "apple", 4]); Assert.Throws(() => redisArrayResult.ToDictionary(/* Use default comparer, causes collision of banana */)); } @@ -73,7 +72,7 @@ public void ToDictionaryFailsWithDuplicateKeys() public void ToDictionaryWorksWithCustomComparator() { var redisArrayResult = RedisResult.Create( - new RedisValue[] { "banana", 1, "BANANA", 2, "orange", 3, "apple", 4 }); + ["banana", 1, "BANANA", 2, "orange", 3, "apple", 4]); var dict = redisArrayResult.ToDictionary(StringComparer.Ordinal); @@ -90,7 +89,7 @@ public void ToDictionaryWorksWithCustomComparator() public void ToDictionaryFailsOnMishapenResults() { var redisArrayResult = RedisResult.Create( - new RedisValue[] { "one", 1, "two", 2, "three", 3, "four" /* missing 4 */ }); + ["one", 1, "two", 2, "three", 3, "four" /* missing 4 */]); Assert.Throws(() => redisArrayResult.ToDictionary(StringComparer.Ordinal)); } diff --git a/tests/StackExchange.Redis.Tests/TestConfig.json b/tests/StackExchange.Redis.Tests/RedisTestConfig.json similarity index 100% rename from tests/StackExchange.Redis.Tests/TestConfig.json rename to tests/StackExchange.Redis.Tests/RedisTestConfig.json diff --git a/tests/StackExchange.Redis.Tests/RespProtocolTests.cs b/tests/StackExchange.Redis.Tests/RespProtocolTests.cs index 44932cf96..08e31b699 100644 --- a/tests/StackExchange.Redis.Tests/RespProtocolTests.cs +++ b/tests/StackExchange.Redis.Tests/RespProtocolTests.cs @@ -3,20 +3,16 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public sealed class RespProtocolTests : TestBase +public sealed class RespProtocolTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public RespProtocolTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] [RunPerProtocol] public async Task ConnectWithTiming() { - using var conn = Create(shared: false, log: Writer); + await using var conn = Create(shared: false, log: Writer); await conn.GetDatabase().PingAsync(); } @@ -81,11 +77,11 @@ public async Task TryConnect() await muxer.GetDatabase().PingAsync(); var server = muxer.GetServerEndPoint(muxer.GetEndPoints().Single()); - if (Context.IsResp3 && !server.GetFeatures().Resp3) + if (TestContext.Current.IsResp3() && !server.GetFeatures().Resp3) { - Skip.Inconclusive("server does not support RESP3"); + Assert.Skip("server does not support RESP3"); } - if (Context.IsResp3) + if (TestContext.Current.IsResp3()) { Assert.Equal(RedisProtocol.Resp3, server.Protocol); } @@ -114,7 +110,7 @@ public async Task ConnectWithBrokenHello(string command, bool isResp3) config.Protocol = RedisProtocol.Resp3; config.CommandMap = CommandMap.Create(new() { ["hello"] = command }); - using var muxer = await ConnectionMultiplexer.ConnectAsync(config, Writer); + await using var muxer = await ConnectionMultiplexer.ConnectAsync(config, Writer); await muxer.GetDatabase().PingAsync(); // is connected var ep = muxer.GetServerEndPoint(muxer.GetEndPoints()[0]); if (!ep.GetFeatures().Resp3) // this is just a v6 check @@ -131,8 +127,8 @@ public async Task ConnectWithBrokenHello(string command, bool isResp3) [InlineData("return 'abc'", RedisProtocol.Resp2, ResultType.BulkString, ResultType.BulkString, "abc")] [InlineData(@"return {1,2,3}", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, ARR_123)] [InlineData("return nil", RedisProtocol.Resp2, ResultType.BulkString, ResultType.Null, null)] - [InlineData(@"return redis.pcall('hgetall', 'key')", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, MAP_ABC)] - [InlineData(@"redis.setresp(3) return redis.pcall('hgetall', 'key')", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, MAP_ABC)] + [InlineData(@"return redis.pcall('hgetall', '{key}')", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, MAP_ABC)] + [InlineData(@"redis.setresp(3) return redis.pcall('hgetall', '{key}')", RedisProtocol.Resp2, ResultType.Array, ResultType.Array, MAP_ABC)] [InlineData("return true", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, 1)] [InlineData("return false", RedisProtocol.Resp2, ResultType.BulkString, ResultType.Null, null)] [InlineData("redis.setresp(3) return true", RedisProtocol.Resp2, ResultType.Integer, ResultType.Integer, 1)] @@ -146,8 +142,8 @@ public async Task ConnectWithBrokenHello(string command, bool isResp3) [InlineData("return 'abc'", RedisProtocol.Resp3, ResultType.BulkString, ResultType.BulkString, "abc")] [InlineData("return {1,2,3}", RedisProtocol.Resp3, ResultType.Array, ResultType.Array, ARR_123)] [InlineData("return nil", RedisProtocol.Resp3, ResultType.BulkString, ResultType.Null, null)] - [InlineData(@"return redis.pcall('hgetall', 'key')", RedisProtocol.Resp3, ResultType.Array, ResultType.Array, MAP_ABC)] - [InlineData(@"redis.setresp(3) return redis.pcall('hgetall', 'key')", RedisProtocol.Resp3, ResultType.Array, ResultType.Map, MAP_ABC)] + [InlineData(@"return redis.pcall('hgetall', '{key}')", RedisProtocol.Resp3, ResultType.Array, ResultType.Array, MAP_ABC)] + [InlineData(@"redis.setresp(3) return redis.pcall('hgetall', '{key}')", RedisProtocol.Resp3, ResultType.Array, ResultType.Map, MAP_ABC)] [InlineData("return true", RedisProtocol.Resp3, ResultType.Integer, ResultType.Integer, 1)] [InlineData("return false", RedisProtocol.Resp3, ResultType.BulkString, ResultType.Null, null)] [InlineData("redis.setresp(3) return true", RedisProtocol.Resp3, ResultType.Integer, ResultType.Boolean, true)] @@ -163,22 +159,24 @@ public async Task CheckLuaResult(string script, RedisProtocol protocol, ResultTy var ep = muxer.GetServerEndPoint(muxer.GetEndPoints().Single()); if (serverMin > ep.Version.Major) { - Skip.Inconclusive($"applies to v{serverMin} onwards - detected v{ep.Version.Major}"); + Assert.Skip($"applies to v{serverMin} onwards - detected v{ep.Version.Major}"); } if (script.Contains("redis.setresp(3)") && !ep.GetFeatures().Resp3) /* v6 check */ { - Skip.Inconclusive("debug protocol not available"); + Assert.Skip("debug protocol not available"); } if (ep.Protocol is null) throw new InvalidOperationException($"No protocol! {ep.InteractiveConnectionState}"); Assert.Equal(protocol, ep.Protocol); + var key = Me(); + script = script.Replace("{key}", key); var db = muxer.GetDatabase(); if (expected is MAP_ABC) { - db.KeyDelete("key"); - db.HashSet("key", "a", 1); - db.HashSet("key", "b", 2); - db.HashSet("key", "c", 3); + db.KeyDelete(key); + db.HashSet(key, "a", 1); + db.HashSet(key, "b", 2); + db.HashSet(key, "c", 3); } var result = await db.ScriptEvaluateAsync(script: script, flags: CommandFlags.NoScriptCache); Assert.Equal(resp2, result.Resp2Type); @@ -319,7 +317,7 @@ public async Task CheckCommandResult(string command, RedisProtocol protocol, Res var ep = muxer.GetServerEndPoint(muxer.GetEndPoints().Single()); if (command == "debug" && args.Length > 0 && args[0] is "protocol" && !ep.GetFeatures().Resp3 /* v6 check */) { - Skip.Inconclusive("debug protocol not available"); + Assert.Skip("debug protocol not available"); } Assert.Equal(protocol, ep.Protocol); @@ -333,10 +331,10 @@ public async Task CheckCommandResult(string command, RedisProtocol protocol, Res await db.StringSetAsync("ikey", "40"); break; case "skey": - await db.SetAddAsync("skey", new RedisValue[] { "a", "b", "c" }); + await db.SetAddAsync("skey", ["a", "b", "c"]); break; case "hkey": - await db.HashSetAsync("hkey", new HashEntry[] { new("a", 1), new("b", 2), new("c", 3) }); + await db.HashSetAsync("hkey", [new("a", 1), new("b", 2), new("c", 3)]); break; } } diff --git a/tests/StackExchange.Redis.Tests/RoleTests.cs b/tests/StackExchange.Redis.Tests/RoleTests.cs index 396877283..198ae6da7 100644 --- a/tests/StackExchange.Redis.Tests/RoleTests.cs +++ b/tests/StackExchange.Redis.Tests/RoleTests.cs @@ -1,22 +1,19 @@ using System.Linq; +using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class Roles : TestBase +public class Roles(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public Roles(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; [Theory] [InlineData(true)] [InlineData(false)] - public void PrimaryRole(bool allowAdmin) // should work with or without admin now + public async Task PrimaryRole(bool allowAdmin) // should work with or without admin now { - using var conn = Create(allowAdmin: allowAdmin); + await using var conn = Create(allowAdmin: allowAdmin); var servers = conn.GetServers(); Log("Server list:"); foreach (var s in servers) @@ -69,9 +66,9 @@ public void PrimaryRole(bool allowAdmin) // should work with or without admin no } [Fact] - public void ReplicaRole() + public async Task ReplicaRole() { - using var conn = ConnectionMultiplexer.Connect($"{TestConfig.Current.ReplicaServerAndPort},allowAdmin=true"); + await using var conn = await ConnectionMultiplexer.ConnectAsync($"{TestConfig.Current.ReplicaServerAndPort},allowAdmin=true"); var server = conn.GetServers().First(conn => conn.IsReplica); var role = server.Role(); diff --git a/tests/StackExchange.Redis.Tests/SSDBTests.cs b/tests/StackExchange.Redis.Tests/SSDBTests.cs index b05a781b2..a1f2f3d5e 100644 --- a/tests/StackExchange.Redis.Tests/SSDBTests.cs +++ b/tests/StackExchange.Redis.Tests/SSDBTests.cs @@ -1,18 +1,16 @@ -using Xunit; -using Xunit.Abstractions; +using System.Threading.Tasks; +using Xunit; namespace StackExchange.Redis.Tests; -public class SSDBTests : TestBase +public class SSDBTests(ITestOutputHelper output) : TestBase(output) { - public SSDBTests(ITestOutputHelper output) : base(output) { } - [Fact] - public void ConnectToSSDB() + public async Task ConnectToSSDB() { Skip.IfNoConfig(nameof(TestConfig.Config.SSDBServer), TestConfig.Current.SSDBServer); - using var conn = ConnectionMultiplexer.Connect(new ConfigurationOptions + await using var conn = await ConnectionMultiplexer.ConnectAsync(new ConfigurationOptions { EndPoints = { { TestConfig.Current.SSDBServer, TestConfig.Current.SSDBPort } }, CommandMap = CommandMap.SSDB, diff --git a/tests/StackExchange.Redis.Tests/SSLTests.cs b/tests/StackExchange.Redis.Tests/SSLTests.cs index 5b11f68c8..0dafe3f9b 100644 --- a/tests/StackExchange.Redis.Tests/SSLTests.cs +++ b/tests/StackExchange.Redis.Tests/SSLTests.cs @@ -7,28 +7,23 @@ using System.Net; using System.Net.Security; using System.Reflection; -using System.Runtime.InteropServices; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; -using NSubstitute.Exceptions; using StackExchange.Redis.Tests.Helpers; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class SSLTests : TestBase, IClassFixture +public class SSLTests(ITestOutputHelper output, SSLTests.SSLServerFixture fixture) : TestBase(output), IClassFixture { - private SSLServerFixture Fixture { get; } - - public SSLTests(ITestOutputHelper output, SSLServerFixture fixture) : base(output) => Fixture = fixture; + private SSLServerFixture Fixture { get; } = fixture; [Theory] // (note the 6379 port is closed) [InlineData(null, true)] // auto-infer port (but specify 6380) [InlineData(6380, true)] // all explicit - public void ConnectToAzure(int? port, bool ssl) + public async Task ConnectToAzure(int? port, bool ssl) { Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); @@ -48,7 +43,7 @@ public void ConnectToAzure(int? port, bool ssl) Log(options.ToString()); using (var connection = ConnectionMultiplexer.Connect(options)) { - var ttl = connection.GetDatabase().Ping(); + var ttl = await connection.GetDatabase().PingAsync(); Log(ttl.ToString()); } } @@ -78,7 +73,7 @@ public async Task ConnectToSSLServer(bool useSsl, bool specifyHost) var config = new ConfigurationOptions { AllowAdmin = true, - SyncTimeout = Debugger.IsAttached ? int.MaxValue : 5000, + SyncTimeout = Debugger.IsAttached ? int.MaxValue : 2000, Password = password, }; var map = new Dictionary @@ -115,7 +110,7 @@ public async Task ConnectToSSLServer(bool useSsl, bool specifyHost) if (useSsl) { - using var conn = ConnectionMultiplexer.Connect(config, Writer); + await using var conn = await ConnectionMultiplexer.ConnectAsync(config, Writer); Log("Connect log:"); lock (log) @@ -215,7 +210,7 @@ public async Task ConnectSslClientAuthenticationOptions(SslProtocols protocols, if (expectSuccess) { - using var conn = await ConnectionMultiplexer.ConnectAsync(config, Writer); + await using var conn = await ConnectionMultiplexer.ConnectAsync(config, Writer); var db = conn.GetDatabase(); Log("Pinging..."); @@ -228,19 +223,19 @@ public async Task ConnectSslClientAuthenticationOptions(SslProtocols protocols, Log("(Expected) Failure connecting: " + ex.Message); if (ex.InnerException is PlatformNotSupportedException pnse) { - Skip.Inconclusive("Expected failure, but also test not supported on this platform: " + pnse.Message); + Assert.Skip("Expected failure, but also test not supported on this platform: " + pnse.Message); } } } catch (RedisException ex) when (ex.InnerException is PlatformNotSupportedException pnse) { - Skip.Inconclusive("Test not supported on this platform: " + pnse.Message); + Assert.Skip("Test not supported on this platform: " + pnse.Message); } } #endif [Fact] - public void RedisLabsSSL() + public async Task RedisLabsSSL() { Skip.IfNoConfig(nameof(TestConfig.Config.RedisLabsSslServer), TestConfig.Current.RedisLabsSslServer); Skip.IfNoConfig(nameof(TestConfig.Config.RedisLabsPfxPath), TestConfig.Current.RedisLabsPfxPath); @@ -275,7 +270,7 @@ public void RedisLabsSSL() options.Ssl = true; options.CertificateSelection += (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => cert; - using var conn = ConnectionMultiplexer.Connect(options); + await using var conn = ConnectionMultiplexer.Connect(options); RedisKey key = Me(); var db = conn.GetDatabase(); @@ -286,7 +281,7 @@ public void RedisLabsSSL() s = db.StringGet(key); Assert.Equal("abc", s); - var latency = db.Ping(); + var latency = await db.PingAsync(); Log("RedisLabs latency: {0:###,##0.##}ms", latency.TotalMilliseconds); using (var file = File.Create("RedisLabs.zip")) @@ -298,7 +293,7 @@ public void RedisLabsSSL() [Theory] [InlineData(false)] [InlineData(true)] - public void RedisLabsEnvironmentVariableClientCertificate(bool setEnv) + public async Task RedisLabsEnvironmentVariableClientCertificate(bool setEnv) { try { @@ -336,7 +331,7 @@ public void RedisLabsEnvironmentVariableClientCertificate(bool setEnv) #endif options.Ssl = true; - using var conn = ConnectionMultiplexer.Connect(options); + await using var conn = ConnectionMultiplexer.Connect(options); RedisKey key = Me(); if (!setEnv) Assert.Fail("Could not set environment"); @@ -349,7 +344,7 @@ public void RedisLabsEnvironmentVariableClientCertificate(bool setEnv) s = db.StringGet(key); Assert.Equal("abc", s); - var latency = db.Ping(); + var latency = await db.PingAsync(); Log("RedisLabs latency: {0:###,##0.##}ms", latency.TotalMilliseconds); using (var file = File.Create("RedisLabs.zip")) @@ -399,6 +394,7 @@ public void Issue883_Exhaustive() var old = CultureInfo.CurrentCulture; try { + var fields = typeof(ConfigurationOptions).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); var all = CultureInfo.GetCultures(CultureTypes.AllCultures); Log($"Checking {all.Length} cultures..."); foreach (var ci in all) @@ -427,7 +423,6 @@ public void Issue883_Exhaustive() Check(nameof(c.Port), c.Port, d.Port); Check(nameof(c.AddressFamily), c.AddressFamily, d.AddressFamily); - var fields = typeof(ConfigurationOptions).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); Log($"Comparing {fields.Length} fields..."); Array.Sort(fields, (x, y) => string.CompareOrdinal(x.Name, y.Name)); foreach (var field in fields) @@ -443,7 +438,7 @@ public void Issue883_Exhaustive() } [Fact] - public void SSLParseViaConfig_Issue883_ConfigObject() + public async Task SSLParseViaConfig_Issue883_ConfigObject() { Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); @@ -461,9 +456,9 @@ public void SSLParseViaConfig_Issue883_ConfigObject() }; options.CertificateValidation += ShowCertFailures(Writer); - using var conn = ConnectionMultiplexer.Connect(options); + await using var conn = ConnectionMultiplexer.Connect(options); - conn.GetDatabase().Ping(); + await conn.GetDatabase().PingAsync(); } public static RemoteCertificateValidationCallback? ShowCertFailures(TextWriterOutputHelper output) @@ -514,7 +509,7 @@ void WriteStatus(X509ChainStatus[] status) } [Fact] - public void SSLParseViaConfig_Issue883_ConfigString() + public async Task SSLParseViaConfig_Issue883_ConfigString() { Skip.IfNoConfig(nameof(TestConfig.Config.AzureCacheServer), TestConfig.Current.AzureCacheServer); Skip.IfNoConfig(nameof(TestConfig.Config.AzureCachePassword), TestConfig.Current.AzureCachePassword); @@ -523,9 +518,9 @@ public void SSLParseViaConfig_Issue883_ConfigString() var options = ConfigurationOptions.Parse(configString); options.CertificateValidation += ShowCertFailures(Writer); - using var conn = ConnectionMultiplexer.Connect(options); + await using var conn = ConnectionMultiplexer.Connect(options); - conn.GetDatabase().Ping(); + await conn.GetDatabase().PingAsync(); } [Fact] @@ -563,7 +558,7 @@ public void SkipIfNoServer() Skip.IfNoConfig(nameof(TestConfig.Config.SslServer), TestConfig.Current.SslServer); if (!ServerRunning) { - Skip.Inconclusive($"SSL/TLS Server was not running at {TestConfig.Current.SslServer}:{TestConfig.Current.SslPort}"); + Assert.Skip($"SSL/TLS Server was not running at {TestConfig.Current.SslServer}:{TestConfig.Current.SslPort}"); } } diff --git a/tests/StackExchange.Redis.Tests/ScanTests.cs b/tests/StackExchange.Redis.Tests/ScanTests.cs index a95435e4d..fe03cbf86 100644 --- a/tests/StackExchange.Redis.Tests/ScanTests.cs +++ b/tests/StackExchange.Redis.Tests/ScanTests.cs @@ -1,30 +1,27 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class ScanTests : TestBase +public class ScanTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public ScanTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Theory] [InlineData(true)] [InlineData(false)] - public void KeysScan(bool supported) + public async Task KeysScan(bool supported) { - string[]? disabledCommands = supported ? null : new[] { "scan" }; - using var conn = Create(disabledCommands: disabledCommands, allowAdmin: true); + string[]? disabledCommands = supported ? null : ["scan"]; + await using var conn = Create(disabledCommands: disabledCommands, allowAdmin: true); var dbId = TestConfig.GetDedicatedDB(conn); var db = conn.GetDatabase(dbId); var prefix = Me() + ":"; var server = GetServer(conn); - Assert.Equal(Context.Test.Protocol, server.Protocol); + Assert.Equal(TestContext.Current.GetProtocol(), server.Protocol); server.FlushDatabase(dbId); for (int i = 0; i < 100; i++) { @@ -52,9 +49,9 @@ public void KeysScan(bool supported) } [Fact] - public void ScansIScanning() + public async Task ScansIScanning() { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); var prefix = Me() + Guid.NewGuid(); var dbId = TestConfig.GetDedicatedDB(conn); @@ -99,9 +96,9 @@ public void ScansIScanning() } [Fact] - public void ScanResume() + public async Task ScanResume() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_8_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_8_0); var dbId = TestConfig.GetDedicatedDB(conn); var db = conn.GetDatabase(dbId); @@ -184,11 +181,11 @@ public void ScanResume() [Theory] [InlineData(true)] [InlineData(false)] - public void SetScan(bool supported) + public async Task SetScan(bool supported) { - string[]? disabledCommands = supported ? null : new[] { "sscan" }; + string[]? disabledCommands = supported ? null : ["sscan"]; - using var conn = Create(disabledCommands: disabledCommands); + await using var conn = Create(disabledCommands: disabledCommands); RedisKey key = Me(); var db = conn.GetDatabase(); @@ -207,11 +204,11 @@ public void SetScan(bool supported) [Theory] [InlineData(true)] [InlineData(false)] - public void SortedSetScan(bool supported) + public async Task SortedSetScan(bool supported) { - string[]? disabledCommands = supported ? null : new[] { "zscan" }; + string[]? disabledCommands = supported ? null : ["zscan"]; - using var conn = Create(disabledCommands: disabledCommands); + await using var conn = Create(disabledCommands: disabledCommands); RedisKey key = Me() + supported; var db = conn.GetDatabase(); @@ -275,11 +272,11 @@ public void SortedSetScan(bool supported) [Theory] [InlineData(true)] [InlineData(false)] - public void HashScan(bool supported) + public async Task HashScan(bool supported) { - string[]? disabledCommands = supported ? null : new[] { "hscan" }; + string[]? disabledCommands = supported ? null : ["hscan"]; - using var conn = Create(disabledCommands: disabledCommands); + await using var conn = Create(disabledCommands: disabledCommands); RedisKey key = Me(); var db = conn.GetDatabase(); @@ -317,9 +314,9 @@ public void HashScan(bool supported) [InlineData(100)] [InlineData(1000)] [InlineData(10000)] - public void HashScanLarge(int pageSize) + public async Task HashScanLarge(int pageSize) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me() + pageSize; var db = conn.GetDatabase(); @@ -335,11 +332,11 @@ public void HashScanLarge(int pageSize) [Theory] [InlineData(true)] [InlineData(false)] - public void HashScanNoValues(bool supported) + public async Task HashScanNoValues(bool supported) { - string[]? disabledCommands = supported ? null : new[] { "hscan" }; + string[]? disabledCommands = supported ? null : ["hscan"]; - using var conn = Create(require: RedisFeatures.v7_4_0_rc1, disabledCommands: disabledCommands); + await using var conn = Create(require: RedisFeatures.v7_4_0_rc1, disabledCommands: disabledCommands); RedisKey key = Me(); var db = conn.GetDatabase(); @@ -367,9 +364,9 @@ public void HashScanNoValues(bool supported) [InlineData(100)] [InlineData(1000)] [InlineData(10000)] - public void HashScanNoValuesLarge(int pageSize) + public async Task HashScanNoValuesLarge(int pageSize) { - using var conn = Create(require: RedisFeatures.v7_4_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_4_0_rc1); RedisKey key = Me() + pageSize; var db = conn.GetDatabase(); @@ -388,9 +385,9 @@ public void HashScanNoValuesLarge(int pageSize) /// See . /// [Fact] - public void HashScanThresholds() + public async Task HashScanThresholds() { - using var conn = Create(allowAdmin: true); + await using var conn = Create(allowAdmin: true); var config = conn.GetServer(conn.GetEndPoints(true)[0]).ConfigGet("hash-max-ziplist-entries").First(); var threshold = int.Parse(config.Value); @@ -430,9 +427,9 @@ private static bool GotCursors(IConnectionMultiplexer conn, RedisKey key, int co [InlineData(100)] [InlineData(1000)] [InlineData(10000)] - public void SetScanLarge(int pageSize) + public async Task SetScanLarge(int pageSize) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me() + pageSize; var db = conn.GetDatabase(); @@ -450,9 +447,9 @@ public void SetScanLarge(int pageSize) [InlineData(100)] [InlineData(1000)] [InlineData(10000)] - public void SortedSetScanLarge(int pageSize) + public async Task SortedSetScanLarge(int pageSize) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me() + pageSize; var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/ScriptingTests.cs b/tests/StackExchange.Redis.Tests/ScriptingTests.cs index f6de8044a..15ea6adb1 100644 --- a/tests/StackExchange.Redis.Tests/ScriptingTests.cs +++ b/tests/StackExchange.Redis.Tests/ScriptingTests.cs @@ -1,4 +1,5 @@ -using System; +#if NET // Since we're flushing and reloading scripts, only run this in once suite +using System; using System.Diagnostics; using System.Linq; using System.Security.Cryptography; @@ -6,7 +7,6 @@ using System.Threading.Tasks; using StackExchange.Redis.KeyspaceIsolation; using Xunit; -using Xunit.Abstractions; // ReSharper disable UseAwaitUsing # for consistency with existing tests // ReSharper disable MethodHasAsyncOverload # grandfathered existing usage @@ -14,11 +14,8 @@ namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class ScriptingTests : TestBase +public class ScriptingTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public ScriptingTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - private IConnectionMultiplexer GetScriptConn(bool allowAdmin = false) { int syncTimeout = 5000; @@ -27,26 +24,26 @@ private IConnectionMultiplexer GetScriptConn(bool allowAdmin = false) } [Fact] - public void ClientScripting() + public async Task ClientScripting() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); _ = conn.GetDatabase().ScriptEvaluate(script: "return redis.call('info','server')", keys: null, values: null); } [Fact] public async Task BasicScripting() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); var db = conn.GetDatabase(); var noCache = db.ScriptEvaluateAsync( script: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", - keys: new RedisKey[] { "key1", "key2" }, - values: new RedisValue[] { "first", "second" }); + keys: ["key1", "key2"], + values: ["first", "second"]); var cache = db.ScriptEvaluateAsync( script: "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", - keys: new RedisKey[] { "key1", "key2" }, - values: new RedisValue[] { "first", "second" }); + keys: ["key1", "key2"], + values: ["first", "second"]); var results = (string[]?)(await noCache)!; Assert.NotNull(results); Assert.Equal(4, results.Length); @@ -65,14 +62,14 @@ public async Task BasicScripting() } [Fact] - public void KeysScripting() + public async Task KeysScripting() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); var db = conn.GetDatabase(); var key = Me(); db.StringSet(key, "bar", flags: CommandFlags.FireAndForget); - var result = (string?)db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: new RedisKey[] { key }, values: null); + var result = (string?)db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: [key], values: null); Assert.Equal("bar", result); } @@ -85,7 +82,7 @@ public async Task TestRandomThingFromForum() return redis.call('INCRBY', KEYS[1], -tonumber(ARGV[1])); """; - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); var prefix = Me(); var db = conn.GetDatabase(); @@ -93,11 +90,11 @@ public async Task TestRandomThingFromForum() db.StringSet(prefix + "B", "5", flags: CommandFlags.FireAndForget); db.StringSet(prefix + "C", "10", flags: CommandFlags.FireAndForget); - var a = db.ScriptEvaluateAsync(script: Script, keys: new RedisKey[] { prefix + "A" }, values: new RedisValue[] { 6 }).ForAwait(); - var b = db.ScriptEvaluateAsync(script: Script, keys: new RedisKey[] { prefix + "B" }, values: new RedisValue[] { 6 }).ForAwait(); - var c = db.ScriptEvaluateAsync(script: Script, keys: new RedisKey[] { prefix + "C" }, values: new RedisValue[] { 6 }).ForAwait(); + var a = db.ScriptEvaluateAsync(script: Script, keys: [prefix + "A"], values: [6]).ForAwait(); + var b = db.ScriptEvaluateAsync(script: Script, keys: [prefix + "B"], values: [6]).ForAwait(); + var c = db.ScriptEvaluateAsync(script: Script, keys: [prefix + "C"], values: [6]).ForAwait(); - var values = await db.StringGetAsync(new RedisKey[] { prefix + "A", prefix + "B", prefix + "C" }).ForAwait(); + var values = await db.StringGetAsync([prefix + "A", prefix + "B", prefix + "C"]).ForAwait(); Assert.Equal(1, (long)await a); // exit code when current val is non-positive Assert.Equal(0, (long)await b); // exit code when result would be negative @@ -110,12 +107,12 @@ public async Task TestRandomThingFromForum() [Fact] public async Task MultiIncrWithoutReplies() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); var db = conn.GetDatabase(); var prefix = Me(); // prime some initial values - db.KeyDelete(new RedisKey[] { prefix + "a", prefix + "b", prefix + "c" }, CommandFlags.FireAndForget); + db.KeyDelete([prefix + "a", prefix + "b", prefix + "c"], CommandFlags.FireAndForget); db.StringIncrement(prefix + "b", flags: CommandFlags.FireAndForget); db.StringIncrement(prefix + "c", flags: CommandFlags.FireAndForget); db.StringIncrement(prefix + "c", flags: CommandFlags.FireAndForget); @@ -124,7 +121,7 @@ public async Task MultiIncrWithoutReplies() // increment a & b by 1, c twice var result = db.ScriptEvaluateAsync( script: "for i,key in ipairs(KEYS) do redis.call('incr', key) end", - keys: new RedisKey[] { prefix + "a", prefix + "b", prefix + "c", prefix + "c" }, // <== aka "KEYS" in the script + keys: [prefix + "a", prefix + "b", prefix + "c", prefix + "c"], // <== aka "KEYS" in the script values: null).ForAwait(); // <== aka "ARGV" in the script // check the incremented values @@ -143,12 +140,12 @@ public async Task MultiIncrWithoutReplies() [Fact] public async Task MultiIncrByWithoutReplies() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); var db = conn.GetDatabase(); var prefix = Me(); // prime some initial values - db.KeyDelete(new RedisKey[] { prefix + "a", prefix + "b", prefix + "c" }, CommandFlags.FireAndForget); + db.KeyDelete([prefix + "a", prefix + "b", prefix + "c"], CommandFlags.FireAndForget); db.StringIncrement(prefix + "b", flags: CommandFlags.FireAndForget); db.StringIncrement(prefix + "c", flags: CommandFlags.FireAndForget); db.StringIncrement(prefix + "c", flags: CommandFlags.FireAndForget); @@ -157,8 +154,8 @@ public async Task MultiIncrByWithoutReplies() // increment a & b by 1, c twice var result = db.ScriptEvaluateAsync( script: "for i,key in ipairs(KEYS) do redis.call('incrby', key, ARGV[i]) end", - keys: new RedisKey[] { prefix + "a", prefix + "b", prefix + "c" }, // <== aka "KEYS" in the script - values: new RedisValue[] { 1, 1, 2 }).ForAwait(); // <== aka "ARGV" in the script + keys: [prefix + "a", prefix + "b", prefix + "c"], // <== aka "KEYS" in the script + values: [1, 1, 2]).ForAwait(); // <== aka "ARGV" in the script // check the incremented values var a = db.StringGetAsync(prefix + "a").ForAwait(); @@ -172,45 +169,45 @@ public async Task MultiIncrByWithoutReplies() } [Fact] - public void DisableStringInference() + public async Task DisableStringInference() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); var db = conn.GetDatabase(); var key = Me(); db.StringSet(key, "bar", flags: CommandFlags.FireAndForget); - var result = (byte[]?)db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: new RedisKey[] { key }); + var result = (byte[]?)db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: [key]); Assert.NotNull(result); Assert.Equal("bar", Encoding.UTF8.GetString(result)); } [Fact] - public void FlushDetection() + public async Task FlushDetection() { // we don't expect this to handle everything; we just expect it to be predictable - using var conn = GetScriptConn(allowAdmin: true); + await using var conn = GetScriptConn(allowAdmin: true); var db = conn.GetDatabase(); var key = Me(); db.StringSet(key, "bar", flags: CommandFlags.FireAndForget); - var result = (string?)db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: new RedisKey[] { key }, values: null); + var result = (string?)db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: [key], values: null); Assert.Equal("bar", result); // now cause all kinds of problems GetServer(conn).ScriptFlush(); // expect this one to fail just work fine (self-fix) - db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: new RedisKey[] { key }, values: null); + db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: [key], values: null); - result = (string?)db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: new RedisKey[] { key }, values: null); + result = (string?)db.ScriptEvaluate(script: "return redis.call('get', KEYS[1])", keys: [key], values: null); Assert.Equal("bar", result); } [Fact] - public void PrepareScript() + public async Task PrepareScript() { - string[] scripts = { "return redis.call('get', KEYS[1])", "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" }; - using (var conn = GetScriptConn(allowAdmin: true)) + string[] scripts = ["return redis.call('get', KEYS[1])", "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"]; + await using (var conn = GetScriptConn(allowAdmin: true)) { var server = GetServer(conn); server.ScriptFlush(); @@ -223,7 +220,7 @@ public void PrepareScript() server.ScriptLoad(scripts[0]); server.ScriptLoad(scripts[1]); } - using (var conn = GetScriptConn()) + await using (var conn = GetScriptConn()) { var server = GetServer(conn); @@ -242,9 +239,9 @@ public void PrepareScript() } [Fact] - public void NonAsciiScripts() + public async Task NonAsciiScripts() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); const string Evil = "return '僕'"; var db = conn.GetDatabase(); @@ -257,7 +254,7 @@ public void NonAsciiScripts() [Fact] public async Task ScriptThrowsError() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); await Assert.ThrowsAsync(async () => { var db = conn.GetDatabase(); @@ -273,9 +270,9 @@ await Assert.ThrowsAsync(async () => } [Fact] - public void ScriptThrowsErrorInsideTransaction() + public async Task ScriptThrowsErrorInsideTransaction() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); var key = Me(); var db = conn.GetDatabase(); @@ -318,7 +315,7 @@ private static Task QuickWait(Task task) [Fact] public async Task ChangeDbInScript() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); var key = Me(); conn.GetDatabase(1).StringSet(key, "db 1", flags: CommandFlags.FireAndForget); @@ -341,7 +338,7 @@ public async Task ChangeDbInScript() [Fact] public async Task ChangeDbInTranScript() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); var key = Me(); conn.GetDatabase(1).StringSet(key, "db 1", flags: CommandFlags.FireAndForget); @@ -363,9 +360,9 @@ public async Task ChangeDbInTranScript() } [Fact] - public void TestBasicScripting() + public async Task TestBasicScripting() { - using var conn = Create(require: RedisFeatures.v2_6_0); + await using var conn = Create(require: RedisFeatures.v2_6_0); RedisValue newId = Guid.NewGuid().ToString(); RedisKey key = Me(); @@ -375,15 +372,15 @@ public void TestBasicScripting() var wasSet = (bool)db.ScriptEvaluate( script: "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", - keys: new[] { key }, - values: new[] { newId }); + keys: [key], + values: [newId]); Assert.True(wasSet); wasSet = (bool)db.ScriptEvaluate( script: "if redis.call('hexists', KEYS[1], 'UniqueId') then return redis.call('hset', KEYS[1], 'UniqueId', ARGV[1]) else return 0 end", - keys: new[] { key }, - values: new[] { newId }); + keys: [key], + values: [newId]); Assert.False(wasSet); } @@ -392,54 +389,56 @@ public void TestBasicScripting() [InlineData(false)] public async Task CheckLoads(bool async) { - using var conn0 = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); - using var conn1 = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn0 = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn1 = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); // note that these are on different connections (so we wouldn't expect // the flush to drop the local cache - assume it is a surprise!) var server = conn0.GetServer(TestConfig.Current.PrimaryServerAndPort); var db = conn1.GetDatabase(); - const string Script = "return 1;"; + var key = Me(); + var Script = $"return '{key}';"; // start empty server.ScriptFlush(); Assert.False(server.ScriptExists(Script)); // run once, causes to be cached - Assert.True(await EvaluateScript()); + Assert.Equal(key, await EvaluateScript()); Assert.True(server.ScriptExists(Script)); // can run again - Assert.True(await EvaluateScript()); + Assert.Equal(key, await EvaluateScript()); // ditch the scripts; should no longer exist - db.Ping(); + await db.PingAsync(); server.ScriptFlush(); Assert.False(server.ScriptExists(Script)); - db.Ping(); + await db.PingAsync(); // just works; magic - Assert.True(await EvaluateScript()); + Assert.Equal(key, await EvaluateScript()); // but gets marked as unloaded, so we can use it again... - Assert.True(await EvaluateScript()); + Assert.Equal(key, await EvaluateScript()); // which will cause it to be cached Assert.True(server.ScriptExists(Script)); - async Task EvaluateScript() + async Task EvaluateScript() { return async ? - (bool)await db.ScriptEvaluateAsync(script: Script) : - (bool)db.ScriptEvaluate(script: Script); + (string?)await db.ScriptEvaluateAsync(script: Script) : + (string?)db.ScriptEvaluate(script: Script); } } [Fact] - public void CompareScriptToDirect() + public async Task CompareScriptToDirect() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + Skip.UnlessLongRunning(); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); const string Script = "return redis.call('incr', KEYS[1])"; var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); @@ -447,12 +446,12 @@ public void CompareScriptToDirect() server.ScriptLoad(Script); var db = conn.GetDatabase(); - db.Ping(); // k, we're all up to date now; clean db, minimal script cache + await db.PingAsync(); // k, we're all up to date now; clean db, minimal script cache // we're using a pipeline here, so send 1000 messages, but for timing: only care about the last const int Loop = 5000; RedisKey key = Me(); - RedisKey[] keys = new[] { key }; // script takes an array + RedisKey[] keys = [key]; // script takes an array // run via script db.KeyDelete(key, CommandFlags.FireAndForget); @@ -483,9 +482,9 @@ public void CompareScriptToDirect() } [Fact] - public void TestCallByHash() + public async Task TestCallByHash() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); const string Script = "return redis.call('incr', KEYS[1])"; var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); @@ -497,7 +496,7 @@ public void TestCallByHash() var db = conn.GetDatabase(); var key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); - RedisKey[] keys = { key }; + RedisKey[] keys = [key]; string hexHash = string.Concat(hash.Select(x => x.ToString("X2"))); Assert.Equal("2BAB3B661081DB58BD2341920E0BA7CF5DC77B25", hexHash); @@ -510,9 +509,9 @@ public void TestCallByHash() } [Fact] - public void SimpleLuaScript() + public async Task SimpleLuaScript() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); const string Script = "return @ident"; var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); @@ -556,7 +555,7 @@ public void SimpleLuaScript() } { - var val = prepared.Evaluate(db, new { ident = new ReadOnlyMemory(new byte[] { 4, 5, 6 }) }); + var val = prepared.Evaluate(db, new { ident = new ReadOnlyMemory([4, 5, 6]) }); var valArray = (byte[]?)val; Assert.NotNull(valArray); Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); @@ -564,9 +563,9 @@ public void SimpleLuaScript() } [Fact] - public void SimpleRawScriptEvaluate() + public async Task SimpleRawScriptEvaluate() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); const string Script = "return ARGV[1]"; var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); @@ -576,39 +575,39 @@ public void SimpleRawScriptEvaluate() // Scopes for repeated use { - var val = db.ScriptEvaluate(script: Script, values: new RedisValue[] { "hello" }); + var val = db.ScriptEvaluate(script: Script, values: ["hello"]); Assert.Equal("hello", (string?)val); } { - var val = db.ScriptEvaluate(script: Script, values: new RedisValue[] { 123 }); + var val = db.ScriptEvaluate(script: Script, values: [123]); Assert.Equal(123, (int)val); } { - var val = db.ScriptEvaluate(script: Script, values: new RedisValue[] { 123L }); + var val = db.ScriptEvaluate(script: Script, values: [123L]); Assert.Equal(123L, (long)val); } { - var val = db.ScriptEvaluate(script: Script, values: new RedisValue[] { 1.1 }); + var val = db.ScriptEvaluate(script: Script, values: [1.1]); Assert.Equal(1.1, (double)val); } { - var val = db.ScriptEvaluate(script: Script, values: new RedisValue[] { true }); + var val = db.ScriptEvaluate(script: Script, values: [true]); Assert.True((bool)val); } { - var val = db.ScriptEvaluate(script: Script, values: new RedisValue[] { new byte[] { 4, 5, 6 } }); + var val = db.ScriptEvaluate(script: Script, values: [new byte[] { 4, 5, 6 }]); var valArray = (byte[]?)val; Assert.NotNull(valArray); Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); } { - var val = db.ScriptEvaluate(script: Script, values: new RedisValue[] { new ReadOnlyMemory(new byte[] { 4, 5, 6 }) }); + var val = db.ScriptEvaluate(script: Script, values: [new ReadOnlyMemory([4, 5, 6])]); var valArray = (byte[]?)val; Assert.NotNull(valArray); Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); @@ -616,9 +615,9 @@ public void SimpleRawScriptEvaluate() } [Fact] - public void LuaScriptWithKeys() + public async Task LuaScriptWithKeys() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); const string Script = "redis.call('set', @key, @value)"; var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); @@ -644,9 +643,9 @@ public void LuaScriptWithKeys() } [Fact] - public void NoInlineReplacement() + public async Task NoInlineReplacement() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); const string Script = "redis.call('set', @key, 'hello@example')"; var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); @@ -677,9 +676,9 @@ public void EscapeReplacement() } [Fact] - public void SimpleLoadedLuaScript() + public async Task SimpleLoadedLuaScript() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); const string Script = "return @ident"; var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); @@ -724,7 +723,7 @@ public void SimpleLoadedLuaScript() } { - var val = loaded.Evaluate(db, new { ident = new ReadOnlyMemory(new byte[] { 4, 5, 6 }) }); + var val = loaded.Evaluate(db, new { ident = new ReadOnlyMemory([4, 5, 6]) }); var valArray = (byte[]?)val; Assert.NotNull(valArray); Assert.True(new byte[] { 4, 5, 6 }.SequenceEqual(valArray)); @@ -732,9 +731,9 @@ public void SimpleLoadedLuaScript() } [Fact] - public void LoadedLuaScriptWithKeys() + public async Task LoadedLuaScriptWithKeys() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); const string Script = "redis.call('set', @key, @value)"; var server = conn.GetServer(TestConfig.Current.PrimaryServerAndPort); @@ -783,9 +782,10 @@ private static void PurgeLuaScriptOnFinalizeImpl(string script) Assert.Equal(1, LuaScript.GetCachedScriptCount()); } - [FactLongRunning] + [Fact] public void PurgeLuaScriptOnFinalize() { + Skip.UnlessLongRunning(); const string Script = "redis.call('set', @PurgeLuaScriptOnFinalizeKey, @PurgeLuaScriptOnFinalizeValue)"; LuaScript.PurgeCache(); Assert.Equal(0, LuaScript.GetCachedScriptCount()); @@ -802,9 +802,9 @@ public void PurgeLuaScriptOnFinalize() } [Fact] - public void DatabaseLuaScriptConvenienceMethods() + public async Task DatabaseLuaScriptConvenienceMethods() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); const string Script = "redis.call('set', @key, @value)"; var script = LuaScript.Prepare(Script); @@ -823,9 +823,9 @@ public void DatabaseLuaScriptConvenienceMethods() } [Fact] - public void ServerLuaScriptConvenienceMethods() + public async Task ServerLuaScriptConvenienceMethods() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); const string Script = "redis.call('set', @key, @value)"; var script = LuaScript.Prepare(Script); @@ -861,9 +861,9 @@ public void LuaScriptPrefixedKeys() } [Fact] - public void LuaScriptWithWrappedDatabase() + public async Task LuaScriptWithWrappedDatabase() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); const string Script = "redis.call('set', @key, @value)"; var db = conn.GetDatabase(); @@ -886,7 +886,7 @@ public void LuaScriptWithWrappedDatabase() [Fact] public async Task AsyncLuaScriptWithWrappedDatabase() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); const string Script = "redis.call('set', @key, @value)"; var db = conn.GetDatabase(); @@ -907,9 +907,9 @@ public async Task AsyncLuaScriptWithWrappedDatabase() } [Fact] - public void LoadedLuaScriptWithWrappedDatabase() + public async Task LoadedLuaScriptWithWrappedDatabase() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); const string Script = "redis.call('set', @key, @value)"; var db = conn.GetDatabase(); @@ -933,7 +933,7 @@ public void LoadedLuaScriptWithWrappedDatabase() [Fact] public async Task AsyncLoadedLuaScriptWithWrappedDatabase() { - using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); + await using var conn = Create(allowAdmin: true, require: RedisFeatures.v2_6_0); const string Script = "redis.call('set', @key, @value)"; var db = conn.GetDatabase(); @@ -955,9 +955,9 @@ public async Task AsyncLoadedLuaScriptWithWrappedDatabase() } [Fact] - public void ScriptWithKeyPrefixViaTokens() + public async Task ScriptWithKeyPrefixViaTokens() { - using var conn = Create(); + await using var conn = Create(); var p = conn.GetDatabase().WithKeyPrefix("prefix/"); @@ -977,9 +977,9 @@ public void ScriptWithKeyPrefixViaTokens() } [Fact] - public void ScriptWithKeyPrefixViaArrays() + public async Task ScriptWithKeyPrefixViaArrays() { - using var conn = Create(); + await using var conn = Create(); var p = conn.GetDatabase().WithKeyPrefix("prefix/"); @@ -990,7 +990,7 @@ public void ScriptWithKeyPrefixViaArrays() arr[3] = ARGV[2]; return arr; "; - var result = (RedisValue[]?)p.ScriptEvaluate(script: Script, keys: new RedisKey[] { "def" }, values: new RedisValue[] { "abc", 123 }); + var result = (RedisValue[]?)p.ScriptEvaluate(script: Script, keys: ["def"], values: ["abc", 123]); Assert.NotNull(result); Assert.Equal("abc", result[0]); Assert.Equal("prefix/def", result[1]); @@ -998,16 +998,16 @@ public void ScriptWithKeyPrefixViaArrays() } [Fact] - public void ScriptWithKeyPrefixCompare() + public async Task ScriptWithKeyPrefixCompare() { - using var conn = Create(); + await using var conn = Create(); var p = conn.GetDatabase().WithKeyPrefix("prefix/"); var args = new { k = (RedisKey)"key", s = "str", v = 123 }; LuaScript lua = LuaScript.Prepare("return {@k, @s, @v}"); var viaArgs = (RedisValue[]?)p.ScriptEvaluate(lua, args); - var viaArr = (RedisValue[]?)p.ScriptEvaluate(script: "return {KEYS[1], ARGV[1], ARGV[2]}", keys: new[] { args.k }, values: new RedisValue[] { args.s, args.v }); + var viaArr = (RedisValue[]?)p.ScriptEvaluate(script: "return {KEYS[1], ARGV[1], ARGV[2]}", keys: [args.k], values: [args.s, args.v]); Assert.NotNull(viaArr); Assert.NotNull(viaArgs); Assert.Equal(string.Join(",", viaArr), string.Join(",", viaArgs)); @@ -1053,14 +1053,14 @@ private static void TestNullArray(RedisResult? value) public void RedisResultUnderstandsNullValue() => TestNullValue(RedisResult.Create(RedisValue.Null, ResultType.None)); [Fact] - public void TestEvalReadonly() + public async Task TestEvalReadonly() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); var db = conn.GetDatabase(); string script = "return KEYS[1]"; - RedisKey[] keys = { "key1" }; - RedisValue[] values = { "first" }; + RedisKey[] keys = ["key1"]; + RedisValue[] values = ["first"]; var result = db.ScriptEvaluateReadOnly(script, keys, values); Assert.Equal("key1", result.ToString()); @@ -1069,28 +1069,30 @@ public void TestEvalReadonly() [Fact] public async Task TestEvalReadonlyAsync() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); var db = conn.GetDatabase(); string script = "return KEYS[1]"; - RedisKey[] keys = { "key1" }; - RedisValue[] values = { "first" }; + RedisKey[] keys = ["key1"]; + RedisValue[] values = ["first"]; var result = await db.ScriptEvaluateReadOnlyAsync(script, keys, values); Assert.Equal("key1", result.ToString()); } [Fact] - public void TestEvalShaReadOnly() + public async Task TestEvalShaReadOnly() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); var db = conn.GetDatabase(); - db.StringSet("foo", "bar"); - db.ScriptEvaluate(script: "return redis.call('get','foo')"); - // Create a SHA1 hash of the script: 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 - SHA1 sha1Hash = SHA1.Create(); + var key = Me(); + var script = $"return redis.call('get','{key}')"; + db.StringSet(key, "bar"); + db.ScriptEvaluate(script: script); - byte[] hash = sha1Hash.ComputeHash(Encoding.UTF8.GetBytes("return redis.call('get','foo')")); + SHA1 sha1Hash = SHA1.Create(); + byte[] hash = sha1Hash.ComputeHash(Encoding.UTF8.GetBytes(script)); + Log("Hash: " + Convert.ToBase64String(hash)); var result = db.ScriptEvaluateReadOnly(hash); Assert.Equal("bar", result.ToString()); @@ -1099,14 +1101,16 @@ public void TestEvalShaReadOnly() [Fact] public async Task TestEvalShaReadOnlyAsync() { - using var conn = GetScriptConn(); + await using var conn = GetScriptConn(); var db = conn.GetDatabase(); - db.StringSet("foo", "bar"); - db.ScriptEvaluate(script: "return redis.call('get','foo')"); - // Create a SHA1 hash of the script: 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 - SHA1 sha1Hash = SHA1.Create(); + var key = Me(); + var script = $"return redis.call('get','{key}')"; + db.StringSet(key, "bar"); + db.ScriptEvaluate(script: script); - byte[] hash = sha1Hash.ComputeHash(Encoding.UTF8.GetBytes("return redis.call('get','foo')")); + SHA1 sha1Hash = SHA1.Create(); + byte[] hash = sha1Hash.ComputeHash(Encoding.UTF8.GetBytes(script)); + Log("Hash: " + Convert.ToBase64String(hash)); var result = await db.ScriptEvaluateReadOnlyAsync(hash); Assert.Equal("bar", result.ToString()); @@ -1149,3 +1153,4 @@ private static void TestNullValue(RedisResult? value) Assert.Null((byte[]?)value); } } +#endif diff --git a/tests/StackExchange.Redis.Tests/SecureTests.cs b/tests/StackExchange.Redis.Tests/SecureTests.cs index 35e9dd580..8f90e04ba 100644 --- a/tests/StackExchange.Redis.Tests/SecureTests.cs +++ b/tests/StackExchange.Redis.Tests/SecureTests.cs @@ -1,26 +1,23 @@ using System.Diagnostics; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class SecureTests : TestBase +public class SecureTests(ITestOutputHelper output) : TestBase(output) { protected override string GetConfiguration() => TestConfig.Current.SecureServerAndPort + ",password=" + TestConfig.Current.SecurePassword + ",name=MyClient"; - public SecureTests(ITestOutputHelper output) : base(output) { } - [Fact] - public void MassiveBulkOpsFireAndForgetSecure() + public async Task MassiveBulkOpsFireAndForgetSecure() { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(); var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); var watch = Stopwatch.StartNew(); @@ -47,11 +44,11 @@ public void CheckConfig() } [Fact] - public void Connect() + public async Task Connect() { - using var conn = Create(); + await using var conn = Create(); - conn.GetDatabase().Ping(); + await conn.GetDatabase().PingAsync(); } [Theory] @@ -59,7 +56,7 @@ public void Connect() [InlineData("", "NOAUTH Returned - connection has not yet authenticated")] public async Task ConnectWithWrongPassword(string password, string exepctedMessage) { - using var checkConn = Create(); + await using var checkConn = Create(); var checkServer = GetServer(checkConn); var config = ConfigurationOptions.Parse(GetConfiguration()); @@ -71,9 +68,9 @@ public async Task ConnectWithWrongPassword(string password, string exepctedMessa { SetExpectedAmbientFailureCount(-1); - using var conn = await ConnectionMultiplexer.ConnectAsync(config, Writer).ConfigureAwait(false); + await using var conn = await ConnectionMultiplexer.ConnectAsync(config, Writer).ConfigureAwait(false); - conn.GetDatabase().Ping(); + await conn.GetDatabase().PingAsync(); }).ConfigureAwait(false); Log($"Exception ({ex.FailureType}): {ex.Message}"); Assert.Equal(ConnectionFailureType.AuthenticationFailure, ex.FailureType); diff --git a/tests/StackExchange.Redis.Tests/SentinelBase.cs b/tests/StackExchange.Redis.Tests/SentinelBase.cs index fc1a74967..826b9c613 100644 --- a/tests/StackExchange.Redis.Tests/SentinelBase.cs +++ b/tests/StackExchange.Redis.Tests/SentinelBase.cs @@ -5,7 +5,6 @@ using System.Net; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; @@ -28,9 +27,9 @@ public SentinelBase(ITestOutputHelper output) : base(output) } #nullable enable - public Task DisposeAsync() => Task.CompletedTask; + public ValueTask DisposeAsync() => default; - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { var options = ServiceOptions.Clone(); options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA); @@ -43,7 +42,7 @@ public async Task InitializeAsync() await Task.Delay(100).ForAwait(); if (Conn.IsConnected) { - using var checkConn = Conn.GetSentinelMasterConnection(options, Writer); + await using var checkConn = Conn.GetSentinelMasterConnection(options, Writer); if (checkConn.IsConnected) { break; @@ -54,7 +53,7 @@ public async Task InitializeAsync() SentinelServerA = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA)!; SentinelServerB = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortB)!; SentinelServerC = Conn.GetServer(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortC)!; - SentinelsServers = new[] { SentinelServerA, SentinelServerB, SentinelServerC }; + SentinelsServers = [SentinelServerA, SentinelServerB, SentinelServerC]; SentinelServerA.AllowReplicaWrites = true; // Wait until we are in a state of a single primary and replica @@ -99,14 +98,14 @@ protected async Task WaitForReadyAsync(EndPoint? expectedPrimary = null, bool wa throw new RedisException($"Primary was expected to be {expectedPrimary}"); Log($"Primary is {primary}"); - using var checkConn = Conn.GetSentinelMasterConnection(ServiceOptions); + await using var checkConn = Conn.GetSentinelMasterConnection(ServiceOptions); await WaitForRoleAsync(checkConn.GetServer(primary), "master", duration.Value.Subtract(sw.Elapsed)).ForAwait(); var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); if (replicas?.Length > 0) { - await Task.Delay(1000).ForAwait(); + await Task.Delay(100).ForAwait(); replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); await WaitForRoleAsync(checkConn.GetServer(replicas[0]), "slave", duration.Value.Subtract(sw.Elapsed)).ForAwait(); } @@ -138,7 +137,7 @@ protected async Task WaitForRoleAsync(IServer server, string role, TimeSpan? dur // ignore } - await Task.Delay(500).ForAwait(); + await Task.Delay(100).ForAwait(); } throw new RedisException($"Timeout waiting for server ({server.EndPoint}) to have expected role (\"{role}\") assigned"); diff --git a/tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs b/tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs index 873e93d3e..358722839 100644 --- a/tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs +++ b/tests/StackExchange.Redis.Tests/SentinelFailoverTests.cs @@ -3,20 +3,18 @@ using System.Linq; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [Collection(NonParallelCollection.Name)] -public class SentinelFailoverTests : SentinelBase +public class SentinelFailoverTests(ITestOutputHelper output) : SentinelBase(output) { - public SentinelFailoverTests(ITestOutputHelper output) : base(output) { } - - [FactLongRunning] + [Fact] public async Task ManagedPrimaryConnectionEndToEndWithFailoverTest() { + Skip.UnlessLongRunning(); var connectionString = $"{TestConfig.Current.SentinelServer}:{TestConfig.Current.SentinelPortA},serviceName={ServiceOptions.ServiceName},allowAdmin=true"; - using var conn = await ConnectionMultiplexer.ConnectAsync(connectionString); + await using var conn = await ConnectionMultiplexer.ConnectAsync(connectionString); conn.ConfigurationChanged += (s, e) => Log($"Configuration changed: {e.EndPoint}"); diff --git a/tests/StackExchange.Redis.Tests/SentinelTests.cs b/tests/StackExchange.Redis.Tests/SentinelTests.cs index 3fc2afd4e..e58f530fd 100644 --- a/tests/StackExchange.Redis.Tests/SentinelTests.cs +++ b/tests/StackExchange.Redis.Tests/SentinelTests.cs @@ -4,14 +4,11 @@ using System.Net; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class SentinelTests : SentinelBase +public class SentinelTests(ITestOutputHelper output) : SentinelBase(output) { - public SentinelTests(ITestOutputHelper output) : base(output) { } - [Fact] public async Task PrimaryConnectTest() { @@ -20,7 +17,7 @@ public async Task PrimaryConnectTest() var conn = ConnectionMultiplexer.Connect(connectionString); var db = conn.GetDatabase(); - db.Ping(); + await db.PingAsync(); var endpoints = conn.GetEndPoints(); Assert.Equal(2, endpoints.Length); @@ -87,19 +84,19 @@ public async Task PrimaryConnectAsyncTest() [Fact] [RunPerProtocol] - public void SentinelConnectTest() + public async Task SentinelConnectTest() { var options = ServiceOptions.Clone(); options.EndPoints.Add(TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA); - using var conn = ConnectionMultiplexer.SentinelConnect(options); + await using var conn = ConnectionMultiplexer.SentinelConnect(options); var db = conn.GetDatabase(); - var test = db.Ping(); + var test = await db.PingAsync(); Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA, test.TotalMilliseconds); } [Fact] - public void SentinelRepeatConnectTest() + public async Task SentinelRepeatConnectTest() { var options = ConfigurationOptions.Parse($"{TestConfig.Current.SentinelServer}:{TestConfig.Current.SentinelPortA}"); options.ServiceName = ServiceName; @@ -111,10 +108,10 @@ public void SentinelRepeatConnectTest() Log(" Endpoint: " + ep); } - using var conn = ConnectionMultiplexer.Connect(options); + await using var conn = await ConnectionMultiplexer.ConnectAsync(options); var db = conn.GetDatabase(); - var test = db.Ping(); + var test = await db.PingAsync(); Log("ping to 1st sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA, test.TotalMilliseconds); Log("Service Name: " + options.ServiceName); @@ -123,10 +120,10 @@ public void SentinelRepeatConnectTest() Log(" Endpoint: " + ep); } - using var conn2 = ConnectionMultiplexer.Connect(options); + await using var conn2 = ConnectionMultiplexer.Connect(options); var db2 = conn2.GetDatabase(); - var test2 = db2.Ping(); + var test2 = await db2.PingAsync(); Log("ping to 2nd sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA, test2.TotalMilliseconds); } @@ -156,13 +153,13 @@ public void SentinelRole() } [Fact] - public void PingTest() + public async Task PingTest() { - var test = SentinelServerA.Ping(); + var test = await SentinelServerA.PingAsync(); Log("ping to sentinel {0}:{1} took {2} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortA, test.TotalMilliseconds); - test = SentinelServerB.Ping(); + test = await SentinelServerB.PingAsync(); Log("ping to sentinel {0}:{1} took {1} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortB, test.TotalMilliseconds); - test = SentinelServerC.Ping(); + test = await SentinelServerC.PingAsync(); Log("ping to sentinel {0}:{1} took {1} ms", TestConfig.Current.SentinelServer, TestConfig.Current.SentinelPortC, test.TotalMilliseconds); } @@ -267,7 +264,7 @@ public void SentinelSentinelsTest() } Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerA.EndPoint.ToString())); - Assert.True(sentinels.Length == 2); + Assert.Equal(2, sentinels.Length); Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); sentinels = SentinelServerB.SentinelSentinels(ServiceName); @@ -277,14 +274,14 @@ public void SentinelSentinelsTest() actual.Add(data["ip"] + ":" + data["port"]); } - expected = new List - { + expected = + [ SentinelServerA.EndPoint.ToString(), SentinelServerC.EndPoint.ToString(), - }; + ]; Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerB.EndPoint.ToString())); - Assert.True(sentinels.Length == 2); + Assert.Equal(2, sentinels.Length); Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); sentinels = SentinelServerC.SentinelSentinels(ServiceName); @@ -294,14 +291,14 @@ public void SentinelSentinelsTest() actual.Add(data["ip"] + ":" + data["port"]); } - expected = new List - { + expected = + [ SentinelServerA.EndPoint.ToString(), SentinelServerB.EndPoint.ToString(), - }; + ]; Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerC.EndPoint.ToString())); - Assert.True(sentinels.Length == 2); + Assert.Equal(2, sentinels.Length); Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); } @@ -323,18 +320,18 @@ public async Task SentinelSentinelsAsyncTest() } Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerA.EndPoint.ToString())); - Assert.True(sentinels.Length == 2); + Assert.Equal(2, sentinels.Length); Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); sentinels = await SentinelServerB.SentinelSentinelsAsync(ServiceName).ForAwait(); - expected = new List - { + expected = + [ SentinelServerA.EndPoint.ToString(), SentinelServerC.EndPoint.ToString(), - }; + ]; - actual = new List(); + actual = []; foreach (var kv in sentinels) { var data = kv.ToDictionary(); @@ -342,16 +339,16 @@ public async Task SentinelSentinelsAsyncTest() } Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerB.EndPoint.ToString())); - Assert.True(sentinels.Length == 2); + Assert.Equal(2, sentinels.Length); Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); sentinels = await SentinelServerC.SentinelSentinelsAsync(ServiceName).ForAwait(); - expected = new List - { + expected = + [ SentinelServerA.EndPoint.ToString(), SentinelServerB.EndPoint.ToString(), - }; - actual = new List(); + ]; + actual = []; foreach (var kv in sentinels) { var data = kv.ToDictionary(); @@ -359,7 +356,7 @@ public async Task SentinelSentinelsAsyncTest() } Assert.All(expected, ep => Assert.NotEqual(ep, SentinelServerC.EndPoint.ToString())); - Assert.True(sentinels.Length == 2); + Assert.Equal(2, sentinels.Length); Assert.All(expected, ep => Assert.Contains(ep, actual, _ipComparer)); } @@ -458,7 +455,7 @@ public async Task ReadOnlyConnectionReplicasTest() var replicas = SentinelServerA.SentinelGetReplicaAddresses(ServiceName); if (replicas.Length == 0) { - Skip.Inconclusive("Sentinel race: 0 replicas to test against."); + Assert.Skip("Sentinel race: 0 replicas to test against."); } var config = new ConfigurationOptions(); diff --git a/tests/StackExchange.Redis.Tests/SetTests.cs b/tests/StackExchange.Redis.Tests/SetTests.cs index 22df40be7..9326ca7a7 100644 --- a/tests/StackExchange.Redis.Tests/SetTests.cs +++ b/tests/StackExchange.Redis.Tests/SetTests.cs @@ -2,20 +2,16 @@ using System.Linq; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class SetTests : TestBase +public class SetTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public SetTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void SetContains() + public async Task SetContains() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -30,7 +26,7 @@ public void SetContains() Assert.True(isMemeber); // Multi members - var areMemebers = db.SetContains(key, new RedisValue[] { 0, 1, 2 }); + var areMemebers = db.SetContains(key, [0, 1, 2]); Assert.Equal(3, areMemebers.Length); Assert.False(areMemebers[0]); Assert.True(areMemebers[1]); @@ -39,7 +35,7 @@ public void SetContains() db.KeyDelete(key); isMemeber = db.SetContains(key, 1); Assert.False(isMemeber); - areMemebers = db.SetContains(key, new RedisValue[] { 0, 1, 2 }); + areMemebers = db.SetContains(key, [0, 1, 2]); Assert.Equal(3, areMemebers.Length); Assert.True(areMemebers.All(i => !i)); // Check that all the elements are False } @@ -47,7 +43,7 @@ public void SetContains() [Fact] public async Task SetContainsAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -62,7 +58,7 @@ public async Task SetContainsAsync() Assert.True(isMemeber); // Multi members - var areMemebers = await db.SetContainsAsync(key, new RedisValue[] { 0, 1, 2 }); + var areMemebers = await db.SetContainsAsync(key, [0, 1, 2]); Assert.Equal(3, areMemebers.Length); Assert.False(areMemebers[0]); Assert.True(areMemebers[1]); @@ -71,67 +67,67 @@ public async Task SetContainsAsync() await db.KeyDeleteAsync(key); isMemeber = await db.SetContainsAsync(key, 1); Assert.False(isMemeber); - areMemebers = await db.SetContainsAsync(key, new RedisValue[] { 0, 1, 2 }); + areMemebers = await db.SetContainsAsync(key, [0, 1, 2]); Assert.Equal(3, areMemebers.Length); Assert.True(areMemebers.All(i => !i)); // Check that all the elements are False } [Fact] - public void SetIntersectionLength() + public async Task SetIntersectionLength() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key1 = Me() + "1"; db.KeyDelete(key1, CommandFlags.FireAndForget); - db.SetAdd(key1, new RedisValue[] { 0, 1, 2, 3, 4 }, CommandFlags.FireAndForget); + db.SetAdd(key1, [0, 1, 2, 3, 4], CommandFlags.FireAndForget); var key2 = Me() + "2"; db.KeyDelete(key2, CommandFlags.FireAndForget); - db.SetAdd(key2, new RedisValue[] { 1, 2, 3, 4, 5 }, CommandFlags.FireAndForget); + db.SetAdd(key2, [1, 2, 3, 4, 5], CommandFlags.FireAndForget); - Assert.Equal(4, db.SetIntersectionLength(new RedisKey[] { key1, key2 })); + Assert.Equal(4, db.SetIntersectionLength([key1, key2])); // with limit - Assert.Equal(3, db.SetIntersectionLength(new RedisKey[] { key1, key2 }, 3)); + Assert.Equal(3, db.SetIntersectionLength([key1, key2], 3)); // Missing keys should be 0 var key3 = Me() + "3"; var key4 = Me() + "4"; db.KeyDelete(key3, CommandFlags.FireAndForget); - Assert.Equal(0, db.SetIntersectionLength(new RedisKey[] { key1, key3 })); - Assert.Equal(0, db.SetIntersectionLength(new RedisKey[] { key3, key4 })); + Assert.Equal(0, db.SetIntersectionLength([key1, key3])); + Assert.Equal(0, db.SetIntersectionLength([key3, key4])); } [Fact] public async Task SetIntersectionLengthAsync() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key1 = Me() + "1"; db.KeyDelete(key1, CommandFlags.FireAndForget); - db.SetAdd(key1, new RedisValue[] { 0, 1, 2, 3, 4 }, CommandFlags.FireAndForget); + db.SetAdd(key1, [0, 1, 2, 3, 4], CommandFlags.FireAndForget); var key2 = Me() + "2"; db.KeyDelete(key2, CommandFlags.FireAndForget); - db.SetAdd(key2, new RedisValue[] { 1, 2, 3, 4, 5 }, CommandFlags.FireAndForget); + db.SetAdd(key2, [1, 2, 3, 4, 5], CommandFlags.FireAndForget); - Assert.Equal(4, await db.SetIntersectionLengthAsync(new RedisKey[] { key1, key2 })); + Assert.Equal(4, await db.SetIntersectionLengthAsync([key1, key2])); // with limit - Assert.Equal(3, await db.SetIntersectionLengthAsync(new RedisKey[] { key1, key2 }, 3)); + Assert.Equal(3, await db.SetIntersectionLengthAsync([key1, key2], 3)); // Missing keys should be 0 var key3 = Me() + "3"; var key4 = Me() + "4"; db.KeyDelete(key3, CommandFlags.FireAndForget); - Assert.Equal(0, await db.SetIntersectionLengthAsync(new RedisKey[] { key1, key3 })); - Assert.Equal(0, await db.SetIntersectionLengthAsync(new RedisKey[] { key3, key4 })); + Assert.Equal(0, await db.SetIntersectionLengthAsync([key1, key3])); + Assert.Equal(0, await db.SetIntersectionLengthAsync([key3, key4])); } [Fact] - public void SScan() + public async Task SScan() { - using var conn = Create(); + await using var conn = Create(); var server = GetAnyPrimary(conn); @@ -157,7 +153,7 @@ public void SScan() [Fact] public async Task SetRemoveArgTests() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -166,15 +162,15 @@ public async Task SetRemoveArgTests() Assert.Throws(() => db.SetRemove(key, values!)); await Assert.ThrowsAsync(async () => await db.SetRemoveAsync(key, values!).ForAwait()).ForAwait(); - values = Array.Empty(); + values = []; Assert.Equal(0, db.SetRemove(key, values)); Assert.Equal(0, await db.SetRemoveAsync(key, values).ForAwait()); } [Fact] - public void SetPopMulti_Multi() + public async Task SetPopMulti_Multi() { - using var conn = Create(require: RedisFeatures.v3_2_0); + await using var conn = Create(require: RedisFeatures.v3_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -182,7 +178,7 @@ public void SetPopMulti_Multi() db.KeyDelete(key, CommandFlags.FireAndForget); for (int i = 1; i < 11; i++) { - db.SetAddAsync(key, i, CommandFlags.FireAndForget); + _ = db.SetAddAsync(key, i, CommandFlags.FireAndForget); } var random = db.SetPop(key); @@ -198,9 +194,9 @@ public void SetPopMulti_Multi() } [Fact] - public void SetPopMulti_Single() + public async Task SetPopMulti_Single() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -226,7 +222,7 @@ public void SetPopMulti_Single() [Fact] public async Task SetPopMulti_Multi_Async() { - using var conn = Create(require: RedisFeatures.v3_2_0); + await using var conn = Create(require: RedisFeatures.v3_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -252,7 +248,7 @@ public async Task SetPopMulti_Multi_Async() [Fact] public async Task SetPopMulti_Single_Async() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -278,7 +274,7 @@ public async Task SetPopMulti_Single_Async() [Fact] public async Task SetPopMulti_Zero_Async() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -298,9 +294,9 @@ public async Task SetPopMulti_Zero_Async() } [Fact] - public void SetAdd_Zero() + public async Task SetAdd_Zero() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -316,7 +312,7 @@ public void SetAdd_Zero() [Fact] public async Task SetAdd_Zero_Async() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -332,9 +328,9 @@ public async Task SetAdd_Zero_Async() } [Fact] - public void SetPopMulti_Nil() + public async Task SetPopMulti_Nil() { - using var conn = Create(require: RedisFeatures.v3_2_0); + await using var conn = Create(require: RedisFeatures.v3_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -348,7 +344,7 @@ public void SetPopMulti_Nil() [Fact] public async Task TestSortReadonlyPrimary() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -369,7 +365,7 @@ public async Task TestSortReadonlyPrimary() [Fact] public async Task TestSortReadonlyReplica() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key = Me(); @@ -379,7 +375,7 @@ public async Task TestSortReadonlyReplica() var items = Enumerable.Repeat(0, 200).Select(_ => random.Next()).ToList(); await db.SetAddAsync(key, items.Select(x => (RedisValue)x).ToArray()); - using var readonlyConn = Create(configuration: TestConfig.Current.ReplicaServerAndPort, require: RedisFeatures.v7_0_0_rc1); + await using var readonlyConn = Create(configuration: TestConfig.Current.ReplicaServerAndPort, require: RedisFeatures.v7_0_0_rc1); var readonlyDb = conn.GetDatabase(); items.Sort(); diff --git a/tests/StackExchange.Redis.Tests/SocketTests.cs b/tests/StackExchange.Redis.Tests/SocketTests.cs index 71a1ffe47..2d11c0014 100644 --- a/tests/StackExchange.Redis.Tests/SocketTests.cs +++ b/tests/StackExchange.Redis.Tests/SocketTests.cs @@ -1,20 +1,19 @@ using System.Diagnostics; -using Xunit.Abstractions; +using System.Threading.Tasks; +using Xunit; namespace StackExchange.Redis.Tests; -public class SocketTests : TestBase +public class SocketTests(ITestOutputHelper output) : TestBase(output) { - protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort; - public SocketTests(ITestOutputHelper output) : base(output) { } - - [FactLongRunning] - public void CheckForSocketLeaks() + [Fact] + public async Task CheckForSocketLeaks() { + Skip.UnlessLongRunning(); const int count = 2000; for (var i = 0; i < count; i++) { - using var _ = Create(clientName: "Test: " + i); + await using var _ = Create(clientName: "Test: " + i); // Intentionally just creating and disposing to leak sockets here // ...so we can figure out what's happening. } diff --git a/tests/StackExchange.Redis.Tests/SortedSetTests.cs b/tests/StackExchange.Redis.Tests/SortedSetTests.cs index 7ba86fcf6..a6e6271ea 100644 --- a/tests/StackExchange.Redis.Tests/SortedSetTests.cs +++ b/tests/StackExchange.Redis.Tests/SortedSetTests.cs @@ -1,18 +1,14 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class SortedSetTests : TestBase +public class SortedSetTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public SortedSetTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - - private static readonly SortedSetEntry[] entries = new SortedSetEntry[] - { + private static readonly SortedSetEntry[] entries = + [ new SortedSetEntry("a", 1), new SortedSetEntry("b", 2), new SortedSetEntry("c", 3), @@ -23,10 +19,10 @@ public SortedSetTests(ITestOutputHelper output, SharedConnectionFixture fixture) new SortedSetEntry("h", 8), new SortedSetEntry("i", 9), new SortedSetEntry("j", 10), - }; + ]; - private static readonly SortedSetEntry[] entriesPow2 = new SortedSetEntry[] - { + private static readonly SortedSetEntry[] entriesPow2 = + [ new SortedSetEntry("a", 1), new SortedSetEntry("b", 2), new SortedSetEntry("c", 4), @@ -37,19 +33,19 @@ public SortedSetTests(ITestOutputHelper output, SharedConnectionFixture fixture) new SortedSetEntry("h", 128), new SortedSetEntry("i", 256), new SortedSetEntry("j", 512), - }; + ]; - private static readonly SortedSetEntry[] entriesPow3 = new SortedSetEntry[] - { + private static readonly SortedSetEntry[] entriesPow3 = + [ new SortedSetEntry("a", 1), new SortedSetEntry("c", 4), new SortedSetEntry("e", 16), new SortedSetEntry("g", 64), new SortedSetEntry("i", 256), - }; + ]; - private static readonly SortedSetEntry[] lexEntries = new SortedSetEntry[] - { + private static readonly SortedSetEntry[] lexEntries = + [ new SortedSetEntry("a", 0), new SortedSetEntry("b", 0), new SortedSetEntry("c", 0), @@ -60,12 +56,12 @@ public SortedSetTests(ITestOutputHelper output, SharedConnectionFixture fixture) new SortedSetEntry("h", 0), new SortedSetEntry("i", 0), new SortedSetEntry("j", 0), - }; + ]; [Fact] - public void SortedSetCombine() + public async Task SortedSetCombine() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key1 = Me(); @@ -76,15 +72,15 @@ public void SortedSetCombine() db.SortedSetAdd(key1, entries); db.SortedSetAdd(key2, entriesPow3); - var diff = db.SortedSetCombine(SetOperation.Difference, new RedisKey[] { key1, key2 }); + var diff = db.SortedSetCombine(SetOperation.Difference, [key1, key2]); Assert.Equal(5, diff.Length); Assert.Equal("b", diff[0]); - var inter = db.SortedSetCombine(SetOperation.Intersect, new RedisKey[] { key1, key2 }); + var inter = db.SortedSetCombine(SetOperation.Intersect, [key1, key2]); Assert.Equal(5, inter.Length); Assert.Equal("a", inter[0]); - var union = db.SortedSetCombine(SetOperation.Union, new RedisKey[] { key1, key2 }); + var union = db.SortedSetCombine(SetOperation.Union, [key1, key2]); Assert.Equal(10, union.Length); Assert.Equal("a", union[0]); } @@ -92,7 +88,7 @@ public void SortedSetCombine() [Fact] public async Task SortedSetCombineAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key1 = Me(); @@ -103,23 +99,23 @@ public async Task SortedSetCombineAsync() db.SortedSetAdd(key1, entries); db.SortedSetAdd(key2, entriesPow3); - var diff = await db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }); + var diff = await db.SortedSetCombineAsync(SetOperation.Difference, [key1, key2]); Assert.Equal(5, diff.Length); Assert.Equal("b", diff[0]); - var inter = await db.SortedSetCombineAsync(SetOperation.Intersect, new RedisKey[] { key1, key2 }); + var inter = await db.SortedSetCombineAsync(SetOperation.Intersect, [key1, key2]); Assert.Equal(5, inter.Length); Assert.Equal("a", inter[0]); - var union = await db.SortedSetCombineAsync(SetOperation.Union, new RedisKey[] { key1, key2 }); + var union = await db.SortedSetCombineAsync(SetOperation.Union, [key1, key2]); Assert.Equal(10, union.Length); Assert.Equal("a", union[0]); } [Fact] - public void SortedSetCombineWithScores() + public async Task SortedSetCombineWithScores() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key1 = Me(); @@ -130,15 +126,15 @@ public void SortedSetCombineWithScores() db.SortedSetAdd(key1, entries); db.SortedSetAdd(key2, entriesPow3); - var diff = db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[] { key1, key2 }); + var diff = db.SortedSetCombineWithScores(SetOperation.Difference, [key1, key2]); Assert.Equal(5, diff.Length); Assert.Equal(new SortedSetEntry("b", 2), diff[0]); - var inter = db.SortedSetCombineWithScores(SetOperation.Intersect, new RedisKey[] { key1, key2 }); + var inter = db.SortedSetCombineWithScores(SetOperation.Intersect, [key1, key2]); Assert.Equal(5, inter.Length); Assert.Equal(new SortedSetEntry("a", 2), inter[0]); - var union = db.SortedSetCombineWithScores(SetOperation.Union, new RedisKey[] { key1, key2 }); + var union = db.SortedSetCombineWithScores(SetOperation.Union, [key1, key2]); Assert.Equal(10, union.Length); Assert.Equal(new SortedSetEntry("a", 2), union[0]); } @@ -146,7 +142,7 @@ public void SortedSetCombineWithScores() [Fact] public async Task SortedSetCombineWithScoresAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key1 = Me(); @@ -157,23 +153,23 @@ public async Task SortedSetCombineWithScoresAsync() db.SortedSetAdd(key1, entries); db.SortedSetAdd(key2, entriesPow3); - var diff = await db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }); + var diff = await db.SortedSetCombineWithScoresAsync(SetOperation.Difference, [key1, key2]); Assert.Equal(5, diff.Length); Assert.Equal(new SortedSetEntry("b", 2), diff[0]); - var inter = await db.SortedSetCombineWithScoresAsync(SetOperation.Intersect, new RedisKey[] { key1, key2 }); + var inter = await db.SortedSetCombineWithScoresAsync(SetOperation.Intersect, [key1, key2]); Assert.Equal(5, inter.Length); Assert.Equal(new SortedSetEntry("a", 2), inter[0]); - var union = await db.SortedSetCombineWithScoresAsync(SetOperation.Union, new RedisKey[] { key1, key2 }); + var union = await db.SortedSetCombineWithScoresAsync(SetOperation.Union, [key1, key2]); Assert.Equal(10, union.Length); Assert.Equal(new SortedSetEntry("a", 2), union[0]); } [Fact] - public void SortedSetCombineAndStore() + public async Task SortedSetCombineAndStore() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key1 = Me(); @@ -186,20 +182,20 @@ public void SortedSetCombineAndStore() db.SortedSetAdd(key1, entries); db.SortedSetAdd(key2, entriesPow3); - var diff = db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }); + var diff = db.SortedSetCombineAndStore(SetOperation.Difference, destination, [key1, key2]); Assert.Equal(5, diff); - var inter = db.SortedSetCombineAndStore(SetOperation.Intersect, destination, new RedisKey[] { key1, key2 }); + var inter = db.SortedSetCombineAndStore(SetOperation.Intersect, destination, [key1, key2]); Assert.Equal(5, inter); - var union = db.SortedSetCombineAndStore(SetOperation.Union, destination, new RedisKey[] { key1, key2 }); + var union = db.SortedSetCombineAndStore(SetOperation.Union, destination, [key1, key2]); Assert.Equal(10, union); } [Fact] public async Task SortedSetCombineAndStoreAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key1 = Me(); @@ -212,20 +208,20 @@ public async Task SortedSetCombineAndStoreAsync() db.SortedSetAdd(key1, entries); db.SortedSetAdd(key2, entriesPow3); - var diff = await db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }); + var diff = await db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, [key1, key2]); Assert.Equal(5, diff); - var inter = await db.SortedSetCombineAndStoreAsync(SetOperation.Intersect, destination, new RedisKey[] { key1, key2 }); + var inter = await db.SortedSetCombineAndStoreAsync(SetOperation.Intersect, destination, [key1, key2]); Assert.Equal(5, inter); - var union = await db.SortedSetCombineAndStoreAsync(SetOperation.Union, destination, new RedisKey[] { key1, key2 }); + var union = await db.SortedSetCombineAndStoreAsync(SetOperation.Union, destination, [key1, key2]); Assert.Equal(10, union); } [Fact] public async Task SortedSetCombineErrors() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key1 = Me(); @@ -239,55 +235,55 @@ public async Task SortedSetCombineErrors() db.SortedSetAdd(key2, entriesPow3); // ZDIFF can't be used with weights - var ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + var ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Difference, [key1, key2], [1, 2])); Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Difference, [key1, key2], [1, 2])); Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Difference, destination, [key1, key2], [1, 2])); Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); // and Async... - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Difference, [key1, key2], [1, 2])); Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Difference, [key1, key2], [1, 2])); Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2 })); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, [key1, key2], [1, 2])); Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); // ZDIFF can't be used with aggregation - ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Difference, [key1, key2], aggregate: Aggregate.Max)); Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Difference, [key1, key2], aggregate: Aggregate.Max)); Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Difference, destination, [key1, key2], aggregate: Aggregate.Max)); Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); // and Async... - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Difference, [key1, key2], aggregate: Aggregate.Max)); Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Difference, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Difference, [key1, key2], aggregate: Aggregate.Max)); Assert.Equal("ZDIFF cannot be used with weights or aggregation.", ex.Message); - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, new RedisKey[] { key1, key2 }, aggregate: Aggregate.Max)); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Difference, destination, [key1, key2], aggregate: Aggregate.Max)); Assert.Equal("ZDIFFSTORE cannot be used with weights or aggregation.", ex.Message); // Too many weights - ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + ex = Assert.Throws(() => db.SortedSetCombine(SetOperation.Union, [key1, key2], [1, 2, 3])); Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); - ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + ex = Assert.Throws(() => db.SortedSetCombineWithScores(SetOperation.Union, [key1, key2], [1, 2, 3])); Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); - ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Union, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + ex = Assert.Throws(() => db.SortedSetCombineAndStore(SetOperation.Union, destination, [key1, key2], [1, 2, 3])); Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); // and Async... - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAsync(SetOperation.Union, [key1, key2], [1, 2, 3])); Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Union, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineWithScoresAsync(SetOperation.Union, [key1, key2], [1, 2, 3])); Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); - ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Union, destination, new RedisKey[] { key1, key2 }, new double[] { 1, 2, 3 })); + ex = await Assert.ThrowsAsync(() => db.SortedSetCombineAndStoreAsync(SetOperation.Union, destination, [key1, key2], [1, 2, 3])); Assert.StartsWith("Keys and weights should have the same number of elements.", ex.Message); } [Fact] - public void SortedSetIntersectionLength() + public async Task SortedSetIntersectionLength() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key1 = Me(); @@ -298,18 +294,18 @@ public void SortedSetIntersectionLength() db.SortedSetAdd(key1, entries); db.SortedSetAdd(key2, entriesPow3); - var inter = db.SortedSetIntersectionLength(new RedisKey[] { key1, key2 }); + var inter = db.SortedSetIntersectionLength([key1, key2]); Assert.Equal(5, inter); // with limit - inter = db.SortedSetIntersectionLength(new RedisKey[] { key1, key2 }, 3); + inter = db.SortedSetIntersectionLength([key1, key2], 3); Assert.Equal(3, inter); } [Fact] public async Task SortedSetIntersectionLengthAsync() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key1 = Me(); @@ -320,41 +316,41 @@ public async Task SortedSetIntersectionLengthAsync() db.SortedSetAdd(key1, entries); db.SortedSetAdd(key2, entriesPow3); - var inter = await db.SortedSetIntersectionLengthAsync(new RedisKey[] { key1, key2 }); + var inter = await db.SortedSetIntersectionLengthAsync([key1, key2]); Assert.Equal(5, inter); // with limit - inter = await db.SortedSetIntersectionLengthAsync(new RedisKey[] { key1, key2 }, 3); + inter = await db.SortedSetIntersectionLengthAsync([key1, key2], 3); Assert.Equal(3, inter); } [Fact] - public void SortedSetRangeViaScript() + public async Task SortedSetRangeViaScript() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); - var result = db.ScriptEvaluate(script: "return redis.call('ZRANGE', KEYS[1], 0, -1, 'WITHSCORES')", keys: new RedisKey[] { key }); + var result = db.ScriptEvaluate(script: "return redis.call('ZRANGE', KEYS[1], 0, -1, 'WITHSCORES')", keys: [key]); AssertFlatArrayEntries(result); } [Fact] - public void SortedSetRangeViaExecute() + public async Task SortedSetRangeViaExecute() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); db.SortedSetAdd(key, entries, CommandFlags.FireAndForget); - var result = db.Execute("ZRANGE", new object[] { key, 0, -1, "WITHSCORES" }); + var result = db.Execute("ZRANGE", [key, 0, -1, "WITHSCORES"]); - if (Context.IsResp3) + if (TestContext.Current.IsResp3()) { AssertJaggedArrayEntries(result); } @@ -404,9 +400,9 @@ private void AssertJaggedArrayEntries(RedisResult result) } [Fact] - public void SortedSetPopMulti_Multi() + public async Task SortedSetPopMulti_Multi() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -427,9 +423,9 @@ public void SortedSetPopMulti_Multi() } [Fact] - public void SortedSetPopMulti_Single() + public async Task SortedSetPopMulti_Single() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -451,7 +447,7 @@ public void SortedSetPopMulti_Single() [Fact] public async Task SortedSetPopMulti_Multi_Async() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -474,7 +470,7 @@ public async Task SortedSetPopMulti_Multi_Async() [Fact] public async Task SortedSetPopMulti_Single_Async() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -496,7 +492,7 @@ public async Task SortedSetPopMulti_Single_Async() [Fact] public async Task SortedSetPopMulti_Zero_Async() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -514,9 +510,9 @@ public async Task SortedSetPopMulti_Zero_Async() } [Fact] - public void SortedSetRandomMembers() + public async Task SortedSetRandomMembers() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -560,7 +556,7 @@ public void SortedSetRandomMembers() [Fact] public async Task SortedSetRandomMembersAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -603,14 +599,14 @@ public async Task SortedSetRandomMembersAsync() [Fact] public async Task SortedSetRangeStoreByRankAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); await db.SortedSetAddAsync(sourceKey, entries, CommandFlags.FireAndForget); var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 0, -1); Assert.Equal(entries.Length, res); @@ -619,14 +615,14 @@ public async Task SortedSetRangeStoreByRankAsync() [Fact] public async Task SortedSetRangeStoreByRankLimitedAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); await db.SortedSetAddAsync(sourceKey, entries, CommandFlags.FireAndForget); var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 1, 4); var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); @@ -640,14 +636,14 @@ public async Task SortedSetRangeStoreByRankLimitedAsync() [Fact] public async Task SortedSetRangeStoreByScoreAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 64, 128, SortedSetOrder.ByScore); var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); @@ -661,14 +657,14 @@ public async Task SortedSetRangeStoreByScoreAsync() [Fact] public async Task SortedSetRangeStoreByScoreAsyncDefault() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, double.NegativeInfinity, double.PositiveInfinity, SortedSetOrder.ByScore); var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); @@ -682,14 +678,14 @@ public async Task SortedSetRangeStoreByScoreAsyncDefault() [Fact] public async Task SortedSetRangeStoreByScoreAsyncLimited() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, double.NegativeInfinity, double.PositiveInfinity, SortedSetOrder.ByScore, skip: 1, take: 6); var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); @@ -703,14 +699,14 @@ public async Task SortedSetRangeStoreByScoreAsyncLimited() [Fact] public async Task SortedSetRangeStoreByScoreAsyncExclusiveRange() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, 32, 256, SortedSetOrder.ByScore, exclude: Exclude.Both); var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); @@ -724,14 +720,14 @@ public async Task SortedSetRangeStoreByScoreAsyncExclusiveRange() [Fact] public async Task SortedSetRangeStoreByScoreAsyncReverse() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); await db.SortedSetAddAsync(sourceKey, entriesPow2, CommandFlags.FireAndForget); var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, start: double.PositiveInfinity, double.NegativeInfinity, SortedSetOrder.ByScore, order: Order.Descending); var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); @@ -745,14 +741,14 @@ public async Task SortedSetRangeStoreByScoreAsyncReverse() [Fact] public async Task SortedSetRangeStoreByLexAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); await db.SortedSetAddAsync(sourceKey, lexEntries, CommandFlags.FireAndForget); var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, "a", "j", SortedSetOrder.ByLex); var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); @@ -766,14 +762,14 @@ public async Task SortedSetRangeStoreByLexAsync() [Fact] public async Task SortedSetRangeStoreByLexExclusiveRangeAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); await db.SortedSetAddAsync(sourceKey, lexEntries, CommandFlags.FireAndForget); var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, "a", "j", SortedSetOrder.ByLex, Exclude.Both); var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); @@ -787,14 +783,14 @@ public async Task SortedSetRangeStoreByLexExclusiveRangeAsync() [Fact] public async Task SortedSetRangeStoreByLexRevRangeAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); await db.SortedSetAddAsync(sourceKey, lexEntries, CommandFlags.FireAndForget); var res = await db.SortedSetRangeAndStoreAsync(sourceKey, destinationKey, "j", "a", SortedSetOrder.ByLex, exclude: Exclude.None, order: Order.Descending); var range = await db.SortedSetRangeByRankWithScoresAsync(destinationKey); @@ -806,32 +802,32 @@ public async Task SortedSetRangeStoreByLexRevRangeAsync() } [Fact] - public void SortedSetRangeStoreByRank() + public async Task SortedSetRangeStoreByRank() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); db.SortedSetAdd(sourceKey, entries, CommandFlags.FireAndForget); var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, 0, -1); Assert.Equal(entries.Length, res); } [Fact] - public void SortedSetRangeStoreByRankLimited() + public async Task SortedSetRangeStoreByRankLimited() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); db.SortedSetAdd(sourceKey, entries, CommandFlags.FireAndForget); var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, 1, 4); var range = db.SortedSetRangeByRankWithScores(destinationKey); @@ -843,16 +839,16 @@ public void SortedSetRangeStoreByRankLimited() } [Fact] - public void SortedSetRangeStoreByScore() + public async Task SortedSetRangeStoreByScore() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); db.SortedSetAdd(sourceKey, entriesPow2, CommandFlags.FireAndForget); var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, 64, 128, SortedSetOrder.ByScore); var range = db.SortedSetRangeByRankWithScores(destinationKey); @@ -864,16 +860,16 @@ public void SortedSetRangeStoreByScore() } [Fact] - public void SortedSetRangeStoreByScoreDefault() + public async Task SortedSetRangeStoreByScoreDefault() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); db.SortedSetAdd(sourceKey, entriesPow2, CommandFlags.FireAndForget); var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, double.NegativeInfinity, double.PositiveInfinity, SortedSetOrder.ByScore); var range = db.SortedSetRangeByRankWithScores(destinationKey); @@ -885,16 +881,16 @@ public void SortedSetRangeStoreByScoreDefault() } [Fact] - public void SortedSetRangeStoreByScoreLimited() + public async Task SortedSetRangeStoreByScoreLimited() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); db.SortedSetAdd(sourceKey, entriesPow2, CommandFlags.FireAndForget); var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, double.NegativeInfinity, double.PositiveInfinity, SortedSetOrder.ByScore, skip: 1, take: 6); var range = db.SortedSetRangeByRankWithScores(destinationKey); @@ -906,16 +902,16 @@ public void SortedSetRangeStoreByScoreLimited() } [Fact] - public void SortedSetRangeStoreByScoreExclusiveRange() + public async Task SortedSetRangeStoreByScoreExclusiveRange() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); db.SortedSetAdd(sourceKey, entriesPow2, CommandFlags.FireAndForget); var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, 32, 256, SortedSetOrder.ByScore, exclude: Exclude.Both); var range = db.SortedSetRangeByRankWithScores(destinationKey); @@ -927,16 +923,16 @@ public void SortedSetRangeStoreByScoreExclusiveRange() } [Fact] - public void SortedSetRangeStoreByScoreReverse() + public async Task SortedSetRangeStoreByScoreReverse() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); db.SortedSetAdd(sourceKey, entriesPow2, CommandFlags.FireAndForget); var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, start: double.PositiveInfinity, double.NegativeInfinity, SortedSetOrder.ByScore, order: Order.Descending); var range = db.SortedSetRangeByRankWithScores(destinationKey); @@ -948,16 +944,16 @@ public void SortedSetRangeStoreByScoreReverse() } [Fact] - public void SortedSetRangeStoreByLex() + public async Task SortedSetRangeStoreByLex() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, "a", "j", SortedSetOrder.ByLex); var range = db.SortedSetRangeByRankWithScores(destinationKey); @@ -969,16 +965,16 @@ public void SortedSetRangeStoreByLex() } [Fact] - public void SortedSetRangeStoreByLexExclusiveRange() + public async Task SortedSetRangeStoreByLexExclusiveRange() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, "a", "j", SortedSetOrder.ByLex, Exclude.Both); var range = db.SortedSetRangeByRankWithScores(destinationKey); @@ -990,16 +986,16 @@ public void SortedSetRangeStoreByLexExclusiveRange() } [Fact] - public void SortedSetRangeStoreByLexRevRange() + public async Task SortedSetRangeStoreByLexRevRange() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); var res = db.SortedSetRangeAndStore(sourceKey, destinationKey, "j", "a", SortedSetOrder.ByLex, Exclude.None, Order.Descending); var range = db.SortedSetRangeByRankWithScores(destinationKey); @@ -1011,41 +1007,41 @@ public void SortedSetRangeStoreByLexRevRange() } [Fact] - public void SortedSetRangeStoreFailErroneousTake() + public async Task SortedSetRangeStoreFailErroneousTake() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); var exception = Assert.Throws(() => db.SortedSetRangeAndStore(sourceKey, destinationKey, 0, -1, take: 5)); Assert.Equal("take", exception.ParamName); } [Fact] - public void SortedSetRangeStoreFailExclude() + public async Task SortedSetRangeStoreFailExclude() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); var exception = Assert.Throws(() => db.SortedSetRangeAndStore(sourceKey, destinationKey, 0, -1, exclude: Exclude.Both)); Assert.Equal("exclude", exception.ParamName); } [Fact] - public void SortedSetMultiPopSingleKey() + public async Task SortedSetMultiPopSingleKey() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key = Me(); @@ -1053,23 +1049,22 @@ public void SortedSetMultiPopSingleKey() db.SortedSetAdd( key, - new SortedSetEntry[] - { + [ new SortedSetEntry("rays", 100), new SortedSetEntry("yankees", 92), new SortedSetEntry("red sox", 92), new SortedSetEntry("blue jays", 91), new SortedSetEntry("orioles", 52), - }); + ]); - var highest = db.SortedSetPop(new RedisKey[] { key }, 1, order: Order.Descending); + var highest = db.SortedSetPop([key], 1, order: Order.Descending); Assert.False(highest.IsNull); Assert.Equal(key, highest.Key); var entry = Assert.Single(highest.Entries); Assert.Equal("rays", entry.Element); Assert.Equal(100, entry.Score); - var bottom2 = db.SortedSetPop(new RedisKey[] { key }, 2); + var bottom2 = db.SortedSetPop([key], 2); Assert.False(bottom2.IsNull); Assert.Equal(key, bottom2.Key); Assert.Equal(2, bottom2.Entries.Length); @@ -1080,9 +1075,9 @@ public void SortedSetMultiPopSingleKey() } [Fact] - public void SortedSetMultiPopMultiKey() + public async Task SortedSetMultiPopMultiKey() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key = Me(); @@ -1090,23 +1085,22 @@ public void SortedSetMultiPopMultiKey() db.SortedSetAdd( key, - new SortedSetEntry[] - { + [ new SortedSetEntry("rays", 100), new SortedSetEntry("yankees", 92), new SortedSetEntry("red sox", 92), new SortedSetEntry("blue jays", 91), new SortedSetEntry("orioles", 52), - }); + ]); - var highest = db.SortedSetPop(new RedisKey[] { "not a real key", key, "yet another not a real key" }, 1, order: Order.Descending); + var highest = db.SortedSetPop(["not a real key", key, "yet another not a real key"], 1, order: Order.Descending); Assert.False(highest.IsNull); Assert.Equal(key, highest.Key); var entry = Assert.Single(highest.Entries); Assert.Equal("rays", entry.Element); Assert.Equal(100, entry.Score); - var bottom2 = db.SortedSetPop(new RedisKey[] { "not a real key", key, "yet another not a real key" }, 2); + var bottom2 = db.SortedSetPop(["not a real key", key, "yet another not a real key"], 2); Assert.False(bottom2.IsNull); Assert.Equal(key, bottom2.Key); Assert.Equal(2, bottom2.Entries.Length); @@ -1117,33 +1111,33 @@ public void SortedSetMultiPopMultiKey() } [Fact] - public void SortedSetMultiPopNoSet() + public async Task SortedSetMultiPopNoSet() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key = Me(); db.KeyDelete(key); - var res = db.SortedSetPop(new RedisKey[] { key }, 1); + var res = db.SortedSetPop([key], 1); Assert.True(res.IsNull); } [Fact] - public void SortedSetMultiPopCount0() + public async Task SortedSetMultiPopCount0() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key = Me(); db.KeyDelete(key); - var exception = Assert.Throws(() => db.SortedSetPop(new RedisKey[] { key }, 0)); + var exception = Assert.Throws(() => db.SortedSetPop([key], 0)); Assert.Contains("ERR count should be greater than 0", exception.Message); } [Fact] public async Task SortedSetMultiPopAsync() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key = Me(); @@ -1151,24 +1145,23 @@ public async Task SortedSetMultiPopAsync() db.SortedSetAdd( key, - new SortedSetEntry[] - { + [ new SortedSetEntry("rays", 100), new SortedSetEntry("yankees", 92), new SortedSetEntry("red sox", 92), new SortedSetEntry("blue jays", 91), new SortedSetEntry("orioles", 52), - }); + ]); var highest = await db.SortedSetPopAsync( - new RedisKey[] { "not a real key", key, "yet another not a real key" }, 1, order: Order.Descending); + ["not a real key", key, "yet another not a real key"], 1, order: Order.Descending); Assert.False(highest.IsNull); Assert.Equal(key, highest.Key); var entry = Assert.Single(highest.Entries); Assert.Equal("rays", entry.Element); Assert.Equal(100, entry.Score); - var bottom2 = await db.SortedSetPopAsync(new RedisKey[] { "not a real key", key, "yet another not a real key" }, 2); + var bottom2 = await db.SortedSetPopAsync(["not a real key", key, "yet another not a real key"], 2); Assert.False(bottom2.IsNull); Assert.Equal(key, bottom2.Key); Assert.Equal(2, bottom2.Entries.Length); @@ -1179,9 +1172,9 @@ public async Task SortedSetMultiPopAsync() } [Fact] - public void SortedSetMultiPopEmptyKeys() + public async Task SortedSetMultiPopEmptyKeys() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var exception = Assert.Throws(() => db.SortedSetPop(Array.Empty(), 5)); @@ -1189,25 +1182,25 @@ public void SortedSetMultiPopEmptyKeys() } [Fact] - public void SortedSetRangeStoreFailForReplica() + public async Task SortedSetRangeStoreFailForReplica() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var me = Me(); var sourceKey = $"{me}:ZSetSource"; var destinationKey = $"{me}:ZSetDestination"; - db.KeyDelete(new RedisKey[] { sourceKey, destinationKey }, CommandFlags.FireAndForget); + db.KeyDelete([sourceKey, destinationKey], CommandFlags.FireAndForget); db.SortedSetAdd(sourceKey, lexEntries, CommandFlags.FireAndForget); var exception = Assert.Throws(() => db.SortedSetRangeAndStore(sourceKey, destinationKey, 0, -1, flags: CommandFlags.DemandReplica)); Assert.Contains("Command cannot be issued to a replica", exception.Message); } [Fact] - public void SortedSetScoresSingle() + public async Task SortedSetScoresSingle() { - using var conn = Create(require: RedisFeatures.v2_1_0); + await using var conn = Create(require: RedisFeatures.v2_1_0); var db = conn.GetDatabase(); var key = Me(); @@ -1225,7 +1218,7 @@ public void SortedSetScoresSingle() [Fact] public async Task SortedSetScoresSingleAsync() { - using var conn = Create(require: RedisFeatures.v2_1_0); + await using var conn = Create(require: RedisFeatures.v2_1_0); var db = conn.GetDatabase(); var key = Me(); @@ -1241,9 +1234,9 @@ public async Task SortedSetScoresSingleAsync() } [Fact] - public void SortedSetScoresSingle_MissingSetStillReturnsNull() + public async Task SortedSetScoresSingle_MissingSetStillReturnsNull() { - using var conn = Create(require: RedisFeatures.v2_1_0); + await using var conn = Create(require: RedisFeatures.v2_1_0); var db = conn.GetDatabase(); var key = Me(); @@ -1259,7 +1252,7 @@ public void SortedSetScoresSingle_MissingSetStillReturnsNull() [Fact] public async Task SortedSetScoresSingle_MissingSetStillReturnsNullAsync() { - using var conn = Create(require: RedisFeatures.v2_1_0); + await using var conn = Create(require: RedisFeatures.v2_1_0); var db = conn.GetDatabase(); var key = Me(); @@ -1273,9 +1266,9 @@ public async Task SortedSetScoresSingle_MissingSetStillReturnsNullAsync() } [Fact] - public void SortedSetScoresSingle_ReturnsNullForMissingMember() + public async Task SortedSetScoresSingle_ReturnsNullForMissingMember() { - using var conn = Create(require: RedisFeatures.v2_1_0); + await using var conn = Create(require: RedisFeatures.v2_1_0); var db = conn.GetDatabase(); var key = Me(); @@ -1292,7 +1285,7 @@ public void SortedSetScoresSingle_ReturnsNullForMissingMember() [Fact] public async Task SortedSetScoresSingle_ReturnsNullForMissingMemberAsync() { - using var conn = Create(require: RedisFeatures.v2_1_0); + await using var conn = Create(require: RedisFeatures.v2_1_0); var db = conn.GetDatabase(); var key = Me(); @@ -1307,9 +1300,9 @@ public async Task SortedSetScoresSingle_ReturnsNullForMissingMemberAsync() } [Fact] - public void SortedSetScoresMultiple() + public async Task SortedSetScoresMultiple() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -1322,7 +1315,7 @@ public void SortedSetScoresMultiple() db.SortedSetAdd(key, member2, 1.75); db.SortedSetAdd(key, member3, 2); - var scores = db.SortedSetScores(key, new RedisValue[] { member1, member2, member3 }); + var scores = db.SortedSetScores(key, [member1, member2, member3]); Assert.NotNull(scores); Assert.Equal(3, scores.Length); @@ -1334,7 +1327,7 @@ public void SortedSetScoresMultiple() [Fact] public async Task SortedSetScoresMultipleAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -1347,7 +1340,7 @@ public async Task SortedSetScoresMultipleAsync() await db.SortedSetAddAsync(key, member2, 1.75); await db.SortedSetAddAsync(key, member3, 2); - var scores = await db.SortedSetScoresAsync(key, new RedisValue[] { member1, member2, member3 }); + var scores = await db.SortedSetScoresAsync(key, [member1, member2, member3]); Assert.NotNull(scores); Assert.Equal(3, scores.Length); @@ -1357,9 +1350,9 @@ public async Task SortedSetScoresMultipleAsync() } [Fact] - public void SortedSetScoresMultiple_ReturnsNullItemsForMissingSet() + public async Task SortedSetScoresMultiple_ReturnsNullItemsForMissingSet() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -1367,7 +1360,7 @@ public void SortedSetScoresMultiple_ReturnsNullItemsForMissingSet() db.KeyDelete(key); // Missing set but should still return an array of nulls. - var scores = db.SortedSetScores(key, new RedisValue[] { "bogus1", "bogus2", "bogus3" }); + var scores = db.SortedSetScores(key, ["bogus1", "bogus2", "bogus3"]); Assert.NotNull(scores); Assert.Equal(3, scores.Length); @@ -1379,7 +1372,7 @@ public void SortedSetScoresMultiple_ReturnsNullItemsForMissingSet() [Fact] public async Task SortedSetScoresMultiple_ReturnsNullItemsForMissingSetAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -1387,7 +1380,7 @@ public async Task SortedSetScoresMultiple_ReturnsNullItemsForMissingSetAsync() await db.KeyDeleteAsync(key); // Missing set but should still return an array of nulls. - var scores = await db.SortedSetScoresAsync(key, new RedisValue[] { "bogus1", "bogus2", "bogus3" }); + var scores = await db.SortedSetScoresAsync(key, ["bogus1", "bogus2", "bogus3"]); Assert.NotNull(scores); Assert.Equal(3, scores.Length); @@ -1397,9 +1390,9 @@ public async Task SortedSetScoresMultiple_ReturnsNullItemsForMissingSetAsync() } [Fact] - public void SortedSetScoresMultiple_ReturnsScoresAndNullItems() + public async Task SortedSetScoresMultiple_ReturnsScoresAndNullItems() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -1414,7 +1407,7 @@ public void SortedSetScoresMultiple_ReturnsScoresAndNullItems() db.SortedSetAdd(key, member2, 1.75); db.SortedSetAdd(key, member3, 2); - var scores = db.SortedSetScores(key, new RedisValue[] { member1, bogusMember, member2, member3 }); + var scores = db.SortedSetScores(key, [member1, bogusMember, member2, member3]); Assert.NotNull(scores); Assert.Equal(4, scores.Length); @@ -1427,7 +1420,7 @@ public void SortedSetScoresMultiple_ReturnsScoresAndNullItems() [Fact] public async Task SortedSetScoresMultiple_ReturnsScoresAndNullItemsAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -1442,7 +1435,7 @@ public async Task SortedSetScoresMultiple_ReturnsScoresAndNullItemsAsync() await db.SortedSetAddAsync(key, member2, 1.75); await db.SortedSetAddAsync(key, member3, 2); - var scores = await db.SortedSetScoresAsync(key, new RedisValue[] { member1, bogusMember, member2, member3 }); + var scores = await db.SortedSetScoresAsync(key, [member1, bogusMember, member2, member3]); Assert.NotNull(scores); Assert.Equal(4, scores.Length); @@ -1455,7 +1448,7 @@ public async Task SortedSetScoresMultiple_ReturnsScoresAndNullItemsAsync() [Fact] public async Task SortedSetUpdate() { - using var conn = Create(require: RedisFeatures.v3_0_0); + await using var conn = Create(require: RedisFeatures.v3_0_0); var db = conn.GetDatabase(); var key = Me(); diff --git a/tests/StackExchange.Redis.Tests/SortedSetWhenTests.cs b/tests/StackExchange.Redis.Tests/SortedSetWhenTests.cs index a26f2d5ba..17c587079 100644 --- a/tests/StackExchange.Redis.Tests/SortedSetWhenTests.cs +++ b/tests/StackExchange.Redis.Tests/SortedSetWhenTests.cs @@ -1,17 +1,14 @@ -using Xunit; -using Xunit.Abstractions; +using System.Threading.Tasks; +using Xunit; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class SortedSetWhenTest : TestBase +public class SortedSetWhenTest(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public SortedSetWhenTest(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void GreaterThanLessThan() + public async Task GreaterThanLessThan() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -26,9 +23,9 @@ public void GreaterThanLessThan() } [Fact] - public void IllegalCombinations() + public async Task IllegalCombinations() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key = Me(); diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index 80a5762cf..50d4ae3d1 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -1,6 +1,7 @@  net481;net8.0 + Exe StackExchange.Redis.Tests true true @@ -20,17 +21,14 @@ + - + + - - - - - diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index 0ea744848..aef914293 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -4,23 +4,19 @@ using System.Threading.Tasks; using Newtonsoft.Json; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class StreamTests : TestBase +public class StreamTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public StreamTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - public override string Me([CallerFilePath] string? filePath = null, [CallerMemberName] string? caller = null) => base.Me(filePath, caller) + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); [Fact] - public void IsStreamType() + public async Task IsStreamType() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -32,9 +28,9 @@ public void IsStreamType() } [Fact] - public void StreamAddSinglePairWithAutoId() + public async Task StreamAddSinglePairWithAutoId() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -44,9 +40,9 @@ public void StreamAddSinglePairWithAutoId() } [Fact] - public void StreamAddMultipleValuePairsWithAutoId() + public async Task StreamAddMultipleValuePairsWithAutoId() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -72,9 +68,9 @@ public void StreamAddMultipleValuePairsWithAutoId() } [Fact] - public void StreamAddWithManualId() + public async Task StreamAddWithManualId() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); const string id = "42-0"; @@ -86,9 +82,9 @@ public void StreamAddWithManualId() } [Fact] - public void StreamAddMultipleValuePairsWithManualId() + public async Task StreamAddMultipleValuePairsWithManualId() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); const string id = "42-0"; @@ -112,7 +108,7 @@ public void StreamAddMultipleValuePairsWithManualId() [Fact] public async Task StreamAutoClaim_MissingKey() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -129,9 +125,9 @@ public async Task StreamAutoClaim_MissingKey() } [Fact] - public void StreamAutoClaim_ClaimsPendingMessages() + public async Task StreamAutoClaim_ClaimsPendingMessages() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -148,7 +144,7 @@ public void StreamAutoClaim_ClaimsPendingMessages() Assert.Equal("0-0", result.NextStartId); Assert.NotEmpty(result.ClaimedEntries); Assert.Empty(result.DeletedIds); - Assert.True(result.ClaimedEntries.Length == 2); + Assert.Equal(2, result.ClaimedEntries.Length); Assert.Equal("value1", result.ClaimedEntries[0].Values[0].Value); Assert.Equal("value2", result.ClaimedEntries[1].Values[0].Value); } @@ -156,7 +152,7 @@ public void StreamAutoClaim_ClaimsPendingMessages() [Fact] public async Task StreamAutoClaim_ClaimsPendingMessagesAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -173,15 +169,15 @@ public async Task StreamAutoClaim_ClaimsPendingMessagesAsync() Assert.Equal("0-0", result.NextStartId); Assert.NotEmpty(result.ClaimedEntries); Assert.Empty(result.DeletedIds); - Assert.True(result.ClaimedEntries.Length == 2); + Assert.Equal(2, result.ClaimedEntries.Length); Assert.Equal("value1", result.ClaimedEntries[0].Values[0].Value); Assert.Equal("value2", result.ClaimedEntries[1].Values[0].Value); } [Fact] - public void StreamAutoClaim_ClaimsSingleMessageWithCountOption() + public async Task StreamAutoClaim_ClaimsSingleMessageWithCountOption() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -199,14 +195,14 @@ public void StreamAutoClaim_ClaimsSingleMessageWithCountOption() Assert.Equal(messageIds[1], result.NextStartId); Assert.NotEmpty(result.ClaimedEntries); Assert.Empty(result.DeletedIds); - Assert.True(result.ClaimedEntries.Length == 1); + Assert.Single(result.ClaimedEntries); Assert.Equal("value1", result.ClaimedEntries[0].Values[0].Value); } [Fact] - public void StreamAutoClaim_ClaimsSingleMessageWithCountOptionIdsOnly() + public async Task StreamAutoClaim_ClaimsSingleMessageWithCountOptionIdsOnly() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -223,7 +219,7 @@ public void StreamAutoClaim_ClaimsSingleMessageWithCountOptionIdsOnly() // Should be the second message ID from the call to prepare. Assert.Equal(messageIds[1], result.NextStartId); Assert.NotEmpty(result.ClaimedIds); - Assert.True(result.ClaimedIds.Length == 1); + Assert.Single(result.ClaimedIds); Assert.Equal(messageIds[0], result.ClaimedIds[0]); Assert.Empty(result.DeletedIds); } @@ -231,7 +227,7 @@ public void StreamAutoClaim_ClaimsSingleMessageWithCountOptionIdsOnly() [Fact] public async Task StreamAutoClaim_ClaimsSingleMessageWithCountOptionAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -249,14 +245,14 @@ public async Task StreamAutoClaim_ClaimsSingleMessageWithCountOptionAsync() Assert.Equal(messageIds[1], result.NextStartId); Assert.NotEmpty(result.ClaimedEntries); Assert.Empty(result.DeletedIds); - Assert.True(result.ClaimedEntries.Length == 1); + Assert.Single(result.ClaimedEntries); Assert.Equal("value1", result.ClaimedEntries[0].Values[0].Value); } [Fact] public async Task StreamAutoClaim_ClaimsSingleMessageWithCountOptionIdsOnlyAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -273,15 +269,15 @@ public async Task StreamAutoClaim_ClaimsSingleMessageWithCountOptionIdsOnlyAsync // Should be the second message ID from the call to prepare. Assert.Equal(messageIds[1], result.NextStartId); Assert.NotEmpty(result.ClaimedIds); - Assert.True(result.ClaimedIds.Length == 1); + Assert.Single(result.ClaimedIds); Assert.Equal(messageIds[0], result.ClaimedIds[0]); Assert.Empty(result.DeletedIds); } [Fact] - public void StreamAutoClaim_IncludesDeletedMessageId() + public async Task StreamAutoClaim_IncludesDeletedMessageId() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var key = Me(); var db = conn.GetDatabase(); @@ -293,7 +289,7 @@ public void StreamAutoClaim_IncludesDeletedMessageId() var messageIds = StreamAutoClaim_PrepareTestData(db, key, group, consumer1); // Delete one of the messages, it should be included in the deleted message ID array. - db.StreamDelete(key, new RedisValue[] { messageIds[0] }); + db.StreamDelete(key, [messageIds[0]]); // Claim a single pending message and reassign it to consumer2. var result = db.StreamAutoClaim(key, group, consumer2, 0, "0-0", count: 2); @@ -301,15 +297,15 @@ public void StreamAutoClaim_IncludesDeletedMessageId() Assert.Equal("0-0", result.NextStartId); Assert.NotEmpty(result.ClaimedEntries); Assert.NotEmpty(result.DeletedIds); - Assert.True(result.ClaimedEntries.Length == 1); - Assert.True(result.DeletedIds.Length == 1); + Assert.Single(result.ClaimedEntries); + Assert.Single(result.DeletedIds); Assert.Equal(messageIds[0], result.DeletedIds[0]); } [Fact] public async Task StreamAutoClaim_IncludesDeletedMessageIdAsync() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var key = Me(); var db = conn.GetDatabase(); @@ -321,7 +317,7 @@ public async Task StreamAutoClaim_IncludesDeletedMessageIdAsync() var messageIds = StreamAutoClaim_PrepareTestData(db, key, group, consumer1); // Delete one of the messages, it should be included in the deleted message ID array. - db.StreamDelete(key, new RedisValue[] { messageIds[0] }); + db.StreamDelete(key, [messageIds[0]]); // Claim a single pending message and reassign it to consumer2. var result = await db.StreamAutoClaimAsync(key, group, consumer2, 0, "0-0", count: 2); @@ -329,15 +325,15 @@ public async Task StreamAutoClaim_IncludesDeletedMessageIdAsync() Assert.Equal("0-0", result.NextStartId); Assert.NotEmpty(result.ClaimedEntries); Assert.NotEmpty(result.DeletedIds); - Assert.True(result.ClaimedEntries.Length == 1); - Assert.True(result.DeletedIds.Length == 1); + Assert.Single(result.ClaimedEntries); + Assert.Single(result.DeletedIds); Assert.Equal(messageIds[0], result.DeletedIds[0]); } [Fact] - public void StreamAutoClaim_NoMessagesToClaim() + public async Task StreamAutoClaim_NoMessagesToClaim() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -361,7 +357,7 @@ public void StreamAutoClaim_NoMessagesToClaim() [Fact] public async Task StreamAutoClaim_NoMessagesToClaimAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -383,9 +379,9 @@ public async Task StreamAutoClaim_NoMessagesToClaimAsync() } [Fact] - public void StreamAutoClaim_NoMessageMeetsMinIdleTime() + public async Task StreamAutoClaim_NoMessageMeetsMinIdleTime() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -407,7 +403,7 @@ public void StreamAutoClaim_NoMessageMeetsMinIdleTime() [Fact] public async Task StreamAutoClaim_NoMessageMeetsMinIdleTimeAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -427,9 +423,9 @@ public async Task StreamAutoClaim_NoMessageMeetsMinIdleTimeAsync() } [Fact] - public void StreamAutoClaim_ReturnsMessageIdOnly() + public async Task StreamAutoClaim_ReturnsMessageIdOnly() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -446,7 +442,7 @@ public void StreamAutoClaim_ReturnsMessageIdOnly() Assert.Equal("0-0", result.NextStartId); Assert.NotEmpty(result.ClaimedIds); Assert.Empty(result.DeletedIds); - Assert.True(result.ClaimedIds.Length == 2); + Assert.Equal(2, result.ClaimedIds.Length); Assert.Equal(messageIds[0], result.ClaimedIds[0]); Assert.Equal(messageIds[1], result.ClaimedIds[1]); } @@ -454,7 +450,7 @@ public void StreamAutoClaim_ReturnsMessageIdOnly() [Fact] public async Task StreamAutoClaim_ReturnsMessageIdOnlyAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var key = Me(); var db = conn.GetDatabase(); @@ -471,12 +467,12 @@ public async Task StreamAutoClaim_ReturnsMessageIdOnlyAsync() Assert.Equal("0-0", result.NextStartId); Assert.NotEmpty(result.ClaimedIds); Assert.Empty(result.DeletedIds); - Assert.True(result.ClaimedIds.Length == 2); + Assert.Equal(2, result.ClaimedIds.Length); Assert.Equal(messageIds[0], result.ClaimedIds[0]); Assert.Equal(messageIds[1], result.ClaimedIds[1]); } - private RedisValue[] StreamAutoClaim_PrepareTestData(IDatabase db, RedisKey key, RedisValue group, RedisValue consumer) + private static RedisValue[] StreamAutoClaim_PrepareTestData(IDatabase db, RedisKey key, RedisValue group, RedisValue consumer) { // Create the group. db.KeyDelete(key); @@ -489,13 +485,13 @@ private RedisValue[] StreamAutoClaim_PrepareTestData(IDatabase db, RedisKey key, // Read the messages into the "c1" db.StreamReadGroup(key, group, consumer); - return new RedisValue[2] { id1, id2 }; + return [id1, id2]; } [Fact] - public void StreamConsumerGroupSetId() + public async Task StreamConsumerGroupSetId() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -524,9 +520,9 @@ public void StreamConsumerGroupSetId() } [Fact] - public void StreamConsumerGroupWithNoConsumers() + public async Task StreamConsumerGroupWithNoConsumers() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -545,9 +541,9 @@ public void StreamConsumerGroupWithNoConsumers() } [Fact] - public void StreamCreateConsumerGroup() + public async Task StreamCreateConsumerGroup() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -563,9 +559,9 @@ public void StreamCreateConsumerGroup() } [Fact] - public void StreamCreateConsumerGroupBeforeCreatingStream() + public async Task StreamCreateConsumerGroupBeforeCreatingStream() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -584,9 +580,9 @@ public void StreamCreateConsumerGroupBeforeCreatingStream() } [Fact] - public void StreamCreateConsumerGroupFailsIfKeyDoesntExist() + public async Task StreamCreateConsumerGroupFailsIfKeyDoesntExist() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -601,9 +597,9 @@ public void StreamCreateConsumerGroupFailsIfKeyDoesntExist() } [Fact] - public void StreamCreateConsumerGroupSucceedsWhenKeyExists() + public async Task StreamCreateConsumerGroupSucceedsWhenKeyExists() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -622,9 +618,9 @@ public void StreamCreateConsumerGroupSucceedsWhenKeyExists() } [Fact] - public void StreamConsumerGroupReadOnlyNewMessagesWithEmptyResponse() + public async Task StreamConsumerGroupReadOnlyNewMessagesWithEmptyResponse() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -644,9 +640,9 @@ public void StreamConsumerGroupReadOnlyNewMessagesWithEmptyResponse() } [Fact] - public void StreamConsumerGroupReadFromStreamBeginning() + public async Task StreamConsumerGroupReadFromStreamBeginning() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -665,9 +661,9 @@ public void StreamConsumerGroupReadFromStreamBeginning() } [Fact] - public void StreamConsumerGroupReadFromStreamBeginningWithCount() + public async Task StreamConsumerGroupReadFromStreamBeginningWithCount() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -690,9 +686,9 @@ public void StreamConsumerGroupReadFromStreamBeginningWithCount() } [Fact] - public void StreamConsumerGroupAcknowledgeMessage() + public async Task StreamConsumerGroupAcknowledgeMessage() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -715,7 +711,7 @@ public void StreamConsumerGroupAcknowledgeMessage() var oneAck = db.StreamAcknowledge(key, groupName, id1); // Multiple message Id overload. - var twoAck = db.StreamAcknowledge(key, groupName, new[] { id3, id4 }); + var twoAck = db.StreamAcknowledge(key, groupName, [id3, id4]); // Read the group again, it should only return the unacknowledged message. var notAcknowledged = db.StreamReadGroup(key, groupName, consumer, "0-0"); @@ -728,9 +724,9 @@ public void StreamConsumerGroupAcknowledgeMessage() } [Fact] - public void StreamConsumerGroupClaimMessages() + public async Task StreamConsumerGroupClaimMessages() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -778,9 +774,9 @@ public void StreamConsumerGroupClaimMessages() } [Fact] - public void StreamConsumerGroupClaimMessagesReturningIds() + public async Task StreamConsumerGroupClaimMessagesReturningIds() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -826,14 +822,14 @@ public void StreamConsumerGroupClaimMessagesReturningIds() } [Fact] - public void StreamConsumerGroupReadMultipleOneReadBeginningOneReadNew() + public async Task StreamConsumerGroupReadMultipleOneReadBeginningOneReadNew() { // Create a group for each stream. One set to read from the beginning of the // stream and the other to begin reading only new messages. // Ask redis to read from the beginning of both stream, expect messages // for only the stream set to read from the beginning. - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); const string groupName = "test_group"; @@ -870,9 +866,9 @@ public void StreamConsumerGroupReadMultipleOneReadBeginningOneReadNew() } [Fact] - public void StreamConsumerGroupReadMultipleOnlyNewMessagesExpectNoResult() + public async Task StreamConsumerGroupReadMultipleOnlyNewMessagesExpectNoResult() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); const string groupName = "test_group"; @@ -902,9 +898,9 @@ public void StreamConsumerGroupReadMultipleOnlyNewMessagesExpectNoResult() } [Fact] - public void StreamConsumerGroupReadMultipleOnlyNewMessagesExpect1Result() + public async Task StreamConsumerGroupReadMultipleOnlyNewMessagesExpect1Result() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); const string groupName = "test_group"; @@ -941,9 +937,9 @@ public void StreamConsumerGroupReadMultipleOnlyNewMessagesExpect1Result() } [Fact] - public void StreamConsumerGroupReadMultipleRestrictCount() + public async Task StreamConsumerGroupReadMultipleRestrictCount() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); const string groupName = "test_group"; @@ -979,9 +975,9 @@ public void StreamConsumerGroupReadMultipleRestrictCount() } [Fact] - public void StreamConsumerGroupViewPendingInfoNoConsumers() + public async Task StreamConsumerGroupViewPendingInfoNoConsumers() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1001,9 +997,9 @@ public void StreamConsumerGroupViewPendingInfoNoConsumers() } [Fact] - public void StreamConsumerGroupViewPendingInfoWhenNothingPending() + public async Task StreamConsumerGroupViewPendingInfoWhenNothingPending() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1024,9 +1020,9 @@ public void StreamConsumerGroupViewPendingInfoWhenNothingPending() } [Fact] - public void StreamConsumerGroupViewPendingInfoSummary() + public async Task StreamConsumerGroupViewPendingInfoSummary() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1052,7 +1048,7 @@ public void StreamConsumerGroupViewPendingInfoSummary() Assert.Equal(4, pendingInfo.PendingMessageCount); Assert.Equal(id1, pendingInfo.LowestPendingMessageId); Assert.Equal(id4, pendingInfo.HighestPendingMessageId); - Assert.True(pendingInfo.Consumers.Length == 2); + Assert.Equal(2, pendingInfo.Consumers.Length); var consumer1Count = pendingInfo.Consumers.First(c => c.Name == consumer1).PendingMessageCount; var consumer2Count = pendingInfo.Consumers.First(c => c.Name == consumer2).PendingMessageCount; @@ -1064,7 +1060,7 @@ public void StreamConsumerGroupViewPendingInfoSummary() [Fact] public async Task StreamConsumerGroupViewPendingMessageInfo() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1099,9 +1095,9 @@ public async Task StreamConsumerGroupViewPendingMessageInfo() } [Fact] - public void StreamConsumerGroupViewPendingMessageInfoForConsumer() + public async Task StreamConsumerGroupViewPendingMessageInfoForConsumer() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1134,9 +1130,9 @@ public void StreamConsumerGroupViewPendingMessageInfoForConsumer() } [Fact] - public void StreamDeleteConsumer() + public async Task StreamDeleteConsumer() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1165,9 +1161,9 @@ public void StreamDeleteConsumer() } [Fact] - public void StreamDeleteConsumerGroup() + public async Task StreamDeleteConsumerGroup() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1194,9 +1190,9 @@ public void StreamDeleteConsumerGroup() } [Fact] - public void StreamDeleteMessage() + public async Task StreamDeleteMessage() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1206,7 +1202,7 @@ public void StreamDeleteMessage() var id3 = db.StreamAdd(key, "field3", "value3"); db.StreamAdd(key, "field4", "value4"); - var deletedCount = db.StreamDelete(key, new[] { id3 }); + var deletedCount = db.StreamDelete(key, [id3]); var messages = db.StreamRange(key); Assert.Equal(1, deletedCount); @@ -1214,9 +1210,9 @@ public void StreamDeleteMessage() } [Fact] - public void StreamDeleteMessages() + public async Task StreamDeleteMessages() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1226,7 +1222,7 @@ public void StreamDeleteMessages() var id3 = db.StreamAdd(key, "field3", "value3"); db.StreamAdd(key, "field4", "value4"); - var deletedCount = db.StreamDelete(key, new[] { id2, id3 }, CommandFlags.None); + var deletedCount = db.StreamDelete(key, [id2, id3], CommandFlags.None); var messages = db.StreamRange(key); Assert.Equal(2, deletedCount); @@ -1234,7 +1230,7 @@ public void StreamDeleteMessages() } [Fact] - public void StreamGroupInfoGet() + public async Task StreamGroupInfoGet() { var key = Me(); const string group1 = "test_group_1", @@ -1242,7 +1238,7 @@ public void StreamGroupInfoGet() consumer1 = "test_consumer_1", consumer2 = "test_consumer_2"; - using (var conn = Create(require: RedisFeatures.v5_0_0)) + await using (var conn = Create(require: RedisFeatures.v5_0_0)) { var db = conn.GetDatabase(); db.KeyDelete(key); @@ -1293,9 +1289,9 @@ static bool IsMessageId(string? value) } [Fact] - public void StreamGroupConsumerInfoGet() + public async Task StreamGroupConsumerInfoGet() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1325,9 +1321,9 @@ public void StreamGroupConsumerInfoGet() } [Fact] - public void StreamInfoGet() + public async Task StreamInfoGet() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1347,9 +1343,9 @@ public void StreamInfoGet() } [Fact] - public void StreamInfoGetWithEmptyStream() + public async Task StreamInfoGetWithEmptyStream() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1358,7 +1354,7 @@ public void StreamInfoGetWithEmptyStream() // to ensure it functions properly on an empty stream. Namely, the first-entry // and last-entry messages should be null. var id = db.StreamAdd(key, "field1", "value1"); - db.StreamDelete(key, new[] { id }); + db.StreamDelete(key, [id]); Assert.Equal(0, db.StreamLength(key)); @@ -1369,9 +1365,9 @@ public void StreamInfoGetWithEmptyStream() } [Fact] - public void StreamNoConsumerGroups() + public async Task StreamNoConsumerGroups() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1385,16 +1381,16 @@ public void StreamNoConsumerGroups() } [Fact] - public void StreamPendingNoMessagesOrConsumers() + public async Task StreamPendingNoMessagesOrConsumers() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); const string groupName = "test_group"; var id = db.StreamAdd(key, "field1", "value1"); - db.StreamDelete(key, new[] { id }); + db.StreamDelete(key, [id]); db.StreamCreateConsumerGroup(key, groupName, "0-0"); @@ -1444,9 +1440,9 @@ public void StreamPositionValidateNew() } [Fact] - public void StreamRead() + public async Task StreamRead() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1465,9 +1461,9 @@ public void StreamRead() } [Fact] - public void StreamReadEmptyStream() + public async Task StreamReadEmptyStream() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1476,7 +1472,7 @@ public void StreamReadEmptyStream() var id1 = db.StreamAdd(key, "field1", "value1"); // Delete the key to empty the stream. - db.StreamDelete(key, new[] { id1 }); + db.StreamDelete(key, [id1]); var len = db.StreamLength(key); // Read the entire stream from the beginning. @@ -1487,9 +1483,9 @@ public void StreamReadEmptyStream() } [Fact] - public void StreamReadEmptyStreams() + public async Task StreamReadEmptyStreams() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key1 = Me() + "a"; @@ -1500,8 +1496,8 @@ public void StreamReadEmptyStreams() var id2 = db.StreamAdd(key2, "field2", "value2"); // Delete the key to empty the stream. - db.StreamDelete(key1, new[] { id1 }); - db.StreamDelete(key2, new[] { id2 }); + db.StreamDelete(key1, [id1]); + db.StreamDelete(key2, [id2]); var len1 = db.StreamLength(key1); var len2 = db.StreamLength(key2); @@ -1518,9 +1514,9 @@ public void StreamReadEmptyStreams() } [Fact] - public void StreamReadLastMessage() + public async Task StreamReadLastMessage() { - using var conn = Create(require: RedisFeatures.v7_4_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_4_0_rc1); var db = conn.GetDatabase(); var key1 = Me(); @@ -1536,9 +1532,9 @@ public void StreamReadLastMessage() } [Fact] - public void StreamReadExpectedExceptionInvalidCountMultipleStream() + public async Task StreamReadExpectedExceptionInvalidCountMultipleStream() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var streamPositions = new[] @@ -1550,9 +1546,9 @@ public void StreamReadExpectedExceptionInvalidCountMultipleStream() } [Fact] - public void StreamReadExpectedExceptionInvalidCountSingleStream() + public async Task StreamReadExpectedExceptionInvalidCountSingleStream() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1560,18 +1556,18 @@ public void StreamReadExpectedExceptionInvalidCountSingleStream() } [Fact] - public void StreamReadExpectedExceptionNullStreamList() + public async Task StreamReadExpectedExceptionNullStreamList() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); Assert.Throws(() => db.StreamRead(null!)); } [Fact] - public void StreamReadExpectedExceptionEmptyStreamList() + public async Task StreamReadExpectedExceptionEmptyStreamList() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var emptyList = Array.Empty(); @@ -1579,9 +1575,9 @@ public void StreamReadExpectedExceptionEmptyStreamList() } [Fact] - public void StreamReadMultipleStreams() + public async Task StreamReadMultipleStreams() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key1 = Me() + "a"; @@ -1601,7 +1597,7 @@ public void StreamReadMultipleStreams() var streams = db.StreamRead(streamList); - Assert.True(streams.Length == 2); + Assert.Equal(2, streams.Length); Assert.Equal(key1, streams[0].Key); Assert.Equal(2, streams[0].Entries.Length); @@ -1615,9 +1611,9 @@ public void StreamReadMultipleStreams() } [Fact] - public void StreamReadMultipleStreamsLastMessage() + public async Task StreamReadMultipleStreamsLastMessage() { - using var conn = Create(require: RedisFeatures.v7_4_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_4_0_rc1); var db = conn.GetDatabase(); var key1 = Me() + "a"; @@ -1638,12 +1634,12 @@ public void StreamReadMultipleStreamsLastMessage() db.StreamAdd(key2, "field7", "value7"); db.StreamAdd(key2, "field8", "value8"); - streamList = new[] { new StreamPosition(key1, "+"), new StreamPosition(key2, "+") }; + streamList = [new StreamPosition(key1, "+"), new StreamPosition(key2, "+")]; streams = db.StreamRead(streamList); Assert.NotNull(streams); - Assert.True(streams.Length == 2); + Assert.Equal(2, streams.Length); var stream1 = streams.Where(e => e.Key == key1).First(); Assert.NotNull(stream1.Entries); @@ -1657,9 +1653,9 @@ public void StreamReadMultipleStreamsLastMessage() } [Fact] - public void StreamReadMultipleStreamsWithCount() + public async Task StreamReadMultipleStreamsWithCount() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key1 = Me() + "a"; @@ -1691,9 +1687,9 @@ public void StreamReadMultipleStreamsWithCount() } [Fact] - public void StreamReadMultipleStreamsWithReadPastSecondStream() + public async Task StreamReadMultipleStreamsWithReadPastSecondStream() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key1 = Me() + "a"; @@ -1722,9 +1718,9 @@ public void StreamReadMultipleStreamsWithReadPastSecondStream() } [Fact] - public void StreamReadMultipleStreamsWithEmptyResponse() + public async Task StreamReadMultipleStreamsWithEmptyResponse() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key1 = Me() + "a"; @@ -1749,9 +1745,9 @@ public void StreamReadMultipleStreamsWithEmptyResponse() } [Fact] - public void StreamReadPastEndOfStream() + public async Task StreamReadPastEndOfStream() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1766,9 +1762,9 @@ public void StreamReadPastEndOfStream() } [Fact] - public void StreamReadRange() + public async Task StreamReadRange() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1784,9 +1780,9 @@ public void StreamReadRange() } [Fact] - public void StreamReadRangeOfEmptyStream() + public async Task StreamReadRangeOfEmptyStream() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1794,7 +1790,7 @@ public void StreamReadRangeOfEmptyStream() var id1 = db.StreamAdd(key, "field1", "value1"); var id2 = db.StreamAdd(key, "field2", "value2"); - var deleted = db.StreamDelete(key, new[] { id1, id2 }); + var deleted = db.StreamDelete(key, [id1, id2]); var entries = db.StreamRange(key); @@ -1804,9 +1800,9 @@ public void StreamReadRangeOfEmptyStream() } [Fact] - public void StreamReadRangeWithCount() + public async Task StreamReadRangeWithCount() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1821,9 +1817,9 @@ public void StreamReadRangeWithCount() } [Fact] - public void StreamReadRangeReverse() + public async Task StreamReadRangeReverse() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1839,9 +1835,9 @@ public void StreamReadRangeReverse() } [Fact] - public void StreamReadRangeReverseWithCount() + public async Task StreamReadRangeReverseWithCount() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1856,9 +1852,9 @@ public void StreamReadRangeReverseWithCount() } [Fact] - public void StreamReadWithAfterIdAndCount_1() + public async Task StreamReadWithAfterIdAndCount_1() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1875,9 +1871,9 @@ public void StreamReadWithAfterIdAndCount_1() } [Fact] - public void StreamReadWithAfterIdAndCount_2() + public async Task StreamReadWithAfterIdAndCount_2() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1896,9 +1892,9 @@ public void StreamReadWithAfterIdAndCount_2() } [Fact] - public void StreamTrimLength() + public async Task StreamTrimLength() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1917,9 +1913,9 @@ public void StreamTrimLength() } [Fact] - public void StreamVerifyLength() + public async Task StreamVerifyLength() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1936,7 +1932,7 @@ public void StreamVerifyLength() [Fact] public async Task AddWithApproxCountAsync() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1944,9 +1940,9 @@ public async Task AddWithApproxCountAsync() } [Fact] - public void AddWithApproxCount() + public async Task AddWithApproxCount() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1954,9 +1950,9 @@ public void AddWithApproxCount() } [Fact] - public void StreamReadGroupWithNoAckShowsNoPendingMessages() + public async Task StreamReadGroupWithNoAckShowsNoPendingMessages() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key = Me(); @@ -1981,9 +1977,9 @@ public void StreamReadGroupWithNoAckShowsNoPendingMessages() } [Fact] - public void StreamReadGroupMultiStreamWithNoAckShowsNoPendingMessages() + public async Task StreamReadGroupMultiStreamWithNoAckShowsNoPendingMessages() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var key1 = Me() + "a"; @@ -2001,11 +1997,10 @@ public void StreamReadGroupMultiStreamWithNoAckShowsNoPendingMessages() db.StreamCreateConsumerGroup(key2, groupName, StreamPosition.NewMessages); db.StreamReadGroup( - new[] - { + [ new StreamPosition(key1, StreamPosition.NewMessages), new StreamPosition(key2, StreamPosition.NewMessages), - }, + ], groupName, consumer, noAck: true); @@ -2020,19 +2015,18 @@ public void StreamReadGroupMultiStreamWithNoAckShowsNoPendingMessages() [Fact] public async Task StreamReadIndexerUsage() { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); var db = conn.GetDatabase(); var streamName = Me(); await db.StreamAddAsync( streamName, - new[] - { + [ new NameValueEntry("x", "blah"), new NameValueEntry("msg", /*lang=json,strict*/ @"{""name"":""test"",""id"":123}"), new NameValueEntry("y", "more blah"), - }); + ]); var streamResult = await db.StreamRangeAsync(streamName, count: 1000); var evntJson = streamResult diff --git a/tests/StackExchange.Redis.Tests/StringTests.cs b/tests/StackExchange.Redis.Tests/StringTests.cs index d430b90a6..85bcc7dd5 100644 --- a/tests/StackExchange.Redis.Tests/StringTests.cs +++ b/tests/StackExchange.Redis.Tests/StringTests.cs @@ -4,7 +4,6 @@ using System.Text; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; @@ -12,15 +11,12 @@ namespace StackExchange.Redis.Tests; /// Tests for . /// [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class StringTests : TestBase +public class StringTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public StringTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] public async Task Append() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var server = GetServer(conn); @@ -54,7 +50,7 @@ public async Task Append() [Fact] public async Task Set() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -73,7 +69,7 @@ public async Task Set() [Fact] public async Task SetEmpty() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -91,7 +87,7 @@ public async Task SetEmpty() [Fact] public async Task StringGetSetExpiryNoValue() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -105,7 +101,7 @@ public async Task StringGetSetExpiryNoValue() [Fact] public async Task StringGetSetExpiryRelative() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -124,7 +120,7 @@ public async Task StringGetSetExpiryRelative() [Fact] public async Task StringGetSetExpiryAbsolute() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -148,7 +144,7 @@ public async Task StringGetSetExpiryAbsolute() [Fact] public async Task StringGetSetExpiryPersist() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -165,7 +161,7 @@ public async Task StringGetSetExpiryPersist() [Fact] public async Task GetLease() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -182,7 +178,7 @@ public async Task GetLease() [Fact] public async Task GetLeaseAsStream() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -202,9 +198,9 @@ public async Task GetLeaseAsStream() } [Fact] - public void GetDelete() + public async Task GetDelete() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var prefix = Me(); @@ -226,7 +222,7 @@ public void GetDelete() [Fact] public async Task GetDeleteAsync() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var prefix = Me(); @@ -248,7 +244,7 @@ public async Task GetDeleteAsync() [Fact] public async Task SetNotExists() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var prefix = Me(); @@ -284,7 +280,7 @@ public async Task SetNotExists() [Fact] public async Task SetKeepTtl() { - using var conn = Create(require: RedisFeatures.v6_0_0); + await using var conn = Create(require: RedisFeatures.v6_0_0); var db = conn.GetDatabase(); var prefix = Me(); @@ -322,7 +318,7 @@ public async Task SetKeepTtl() [Fact] public async Task SetAndGet() { - using var conn = Create(require: RedisFeatures.v6_2_0); + await using var conn = Create(require: RedisFeatures.v6_2_0); var db = conn.GetDatabase(); var prefix = Me(); @@ -390,7 +386,7 @@ public async Task SetAndGet() [Fact] public async Task SetNotExistsAndGet() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var prefix = Me(); @@ -420,7 +416,7 @@ public async Task SetNotExistsAndGet() [Fact] public async Task Ranges() { - using var conn = Create(require: RedisFeatures.v2_1_8); + await using var conn = Create(require: RedisFeatures.v2_1_8); var db = conn.GetDatabase(); var key = Me(); @@ -439,7 +435,7 @@ public async Task Ranges() [Fact] public async Task IncrDecr() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -466,7 +462,7 @@ public async Task IncrDecr() [Fact] public async Task IncrDecrFloat() { - using var conn = Create(require: RedisFeatures.v2_6_0); + await using var conn = Create(require: RedisFeatures.v2_6_0); var db = conn.GetDatabase(); var key = Me(); @@ -494,7 +490,7 @@ public async Task IncrDecrFloat() [Fact] public async Task GetRange() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -511,7 +507,7 @@ public async Task GetRange() [Fact] public async Task BitCount() { - using var conn = Create(require: RedisFeatures.v2_6_0); + await using var conn = Create(require: RedisFeatures.v2_6_0); var db = conn.GetDatabase(); var key = Me(); @@ -539,7 +535,7 @@ public async Task BitCount() [Fact] public async Task BitCountWithBitUnit() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key = Me(); @@ -563,7 +559,7 @@ public async Task BitCountWithBitUnit() [Fact] public async Task BitOp() { - using var conn = Create(require: RedisFeatures.v2_6_0); + await using var conn = Create(require: RedisFeatures.v2_6_0); var db = conn.GetDatabase(); var prefix = Me(); @@ -574,9 +570,9 @@ public async Task BitOp() db.StringSet(key2, new byte[] { 6 }, flags: CommandFlags.FireAndForget); db.StringSet(key3, new byte[] { 12 }, flags: CommandFlags.FireAndForget); - var len_and = db.StringBitOperationAsync(Bitwise.And, "and", new RedisKey[] { key1, key2, key3 }); - var len_or = db.StringBitOperationAsync(Bitwise.Or, "or", new RedisKey[] { key1, key2, key3 }); - var len_xor = db.StringBitOperationAsync(Bitwise.Xor, "xor", new RedisKey[] { key1, key2, key3 }); + var len_and = db.StringBitOperationAsync(Bitwise.And, "and", [key1, key2, key3]); + var len_or = db.StringBitOperationAsync(Bitwise.Or, "or", [key1, key2, key3]); + var len_xor = db.StringBitOperationAsync(Bitwise.Xor, "xor", [key1, key2, key3]); var len_not = db.StringBitOperationAsync(Bitwise.Not, "not", key1); Assert.Equal(1, await len_and); @@ -598,7 +594,7 @@ public async Task BitOp() [Fact] public async Task BitOpExtended() { - using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + await using var conn = Create(require: RedisFeatures.v8_2_0_rc1); var db = conn.GetDatabase(); var prefix = Me(); var keyX = prefix + "X"; @@ -607,7 +603,7 @@ public async Task BitOpExtended() var keyY3 = prefix + "Y3"; // Clean up keys - db.KeyDelete(new RedisKey[] { keyX, keyY1, keyY2, keyY3 }, CommandFlags.FireAndForget); + db.KeyDelete([keyX, keyY1, keyY2, keyY3], CommandFlags.FireAndForget); // Set up test data with more complex patterns // X = 11110000 (240) @@ -622,7 +618,7 @@ public async Task BitOpExtended() // Test DIFF: X ∧ ¬(Y1 ∨ Y2 ∨ Y3) // Y1 ∨ Y2 ∨ Y3 = 170 | 85 | 204 = 255 // X ∧ ¬(Y1 ∨ Y2 ∨ Y3) = 240 & ~255 = 240 & 0 = 0 - var len_diff = await db.StringBitOperationAsync(Bitwise.Diff, "diff", new RedisKey[] { keyX, keyY1, keyY2, keyY3 }); + var len_diff = await db.StringBitOperationAsync(Bitwise.Diff, "diff", [keyX, keyY1, keyY2, keyY3]); Assert.Equal(1, len_diff); var r_diff = ((byte[]?)(await db.StringGetAsync("diff")))?.Single(); Assert.Equal((byte)0, r_diff); @@ -631,7 +627,7 @@ public async Task BitOpExtended() // ¬X = ~240 = 15 // Y1 ∨ Y2 ∨ Y3 = 255 // ¬X ∧ (Y1 ∨ Y2 ∨ Y3) = 15 & 255 = 15 - var len_diff1 = await db.StringBitOperationAsync(Bitwise.Diff1, "diff1", new RedisKey[] { keyX, keyY1, keyY2, keyY3 }); + var len_diff1 = await db.StringBitOperationAsync(Bitwise.Diff1, "diff1", [keyX, keyY1, keyY2, keyY3]); Assert.Equal(1, len_diff1); var r_diff1 = ((byte[]?)(await db.StringGetAsync("diff1")))?.Single(); Assert.Equal((byte)15, r_diff1); @@ -639,7 +635,7 @@ public async Task BitOpExtended() // Test ANDOR: X ∧ (Y1 ∨ Y2 ∨ Y3) // Y1 ∨ Y2 ∨ Y3 = 255 // X ∧ (Y1 ∨ Y2 ∨ Y3) = 240 & 255 = 240 - var len_andor = await db.StringBitOperationAsync(Bitwise.AndOr, "andor", new RedisKey[] { keyX, keyY1, keyY2, keyY3 }); + var len_andor = await db.StringBitOperationAsync(Bitwise.AndOr, "andor", [keyX, keyY1, keyY2, keyY3]); Assert.Equal(1, len_andor); var r_andor = ((byte[]?)(await db.StringGetAsync("andor")))?.Single(); Assert.Equal((byte)240, r_andor); @@ -647,7 +643,7 @@ public async Task BitOpExtended() // Test ONE: bits set in exactly one bitmap // For X=240, Y1=170, Y2=85, Y3=204 // We need to count bits that appear in exactly one of these values - var len_one = await db.StringBitOperationAsync(Bitwise.One, "one", new RedisKey[] { keyX, keyY1, keyY2, keyY3 }); + var len_one = await db.StringBitOperationAsync(Bitwise.One, "one", [keyX, keyY1, keyY2, keyY3]); Assert.Equal(1, len_one); var r_one = ((byte[]?)(await db.StringGetAsync("one")))?.Single(); @@ -667,42 +663,42 @@ public async Task BitOpExtended() [Fact] public async Task BitOpTwoOperands() { - using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + await using var conn = Create(require: RedisFeatures.v8_2_0_rc1); var db = conn.GetDatabase(); var prefix = Me(); var key1 = prefix + "1"; var key2 = prefix + "2"; // Clean up keys - db.KeyDelete(new RedisKey[] { key1, key2 }, CommandFlags.FireAndForget); + db.KeyDelete([key1, key2], CommandFlags.FireAndForget); // Test with two operands: key1=10101010 (170), key2=11001100 (204) db.StringSet(key1, new byte[] { 170 }, flags: CommandFlags.FireAndForget); db.StringSet(key2, new byte[] { 204 }, flags: CommandFlags.FireAndForget); // Test DIFF: key1 ∧ ¬key2 = 170 & ~204 = 170 & 51 = 34 - var len_diff = await db.StringBitOperationAsync(Bitwise.Diff, "diff2", new RedisKey[] { key1, key2 }); + var len_diff = await db.StringBitOperationAsync(Bitwise.Diff, "diff2", [key1, key2]); Assert.Equal(1, len_diff); var r_diff = ((byte[]?)(await db.StringGetAsync("diff2")))?.Single(); Assert.Equal((byte)(170 & ~204), r_diff); // Test ONE with two operands (should be equivalent to XOR) - var len_one = await db.StringBitOperationAsync(Bitwise.One, "one2", new RedisKey[] { key1, key2 }); + var len_one = await db.StringBitOperationAsync(Bitwise.One, "one2", [key1, key2]); Assert.Equal(1, len_one); var r_one = ((byte[]?)(await db.StringGetAsync("one2")))?.Single(); Assert.Equal((byte)(170 ^ 204), r_one); // Verify ONE equals XOR for two operands - var len_xor = await db.StringBitOperationAsync(Bitwise.Xor, "xor2", new RedisKey[] { key1, key2 }); + var len_xor = await db.StringBitOperationAsync(Bitwise.Xor, "xor2", [key1, key2]); Assert.Equal(1, len_xor); var r_xor = ((byte[]?)(await db.StringGetAsync("xor2")))?.Single(); Assert.Equal(r_one, r_xor); } [Fact] - public void BitOpDiff() + public async Task BitOpDiff() { - using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + await using var conn = Create(require: RedisFeatures.v8_2_0_rc1); var db = conn.GetDatabase(); var prefix = Me(); var keyX = prefix + "X"; @@ -711,7 +707,7 @@ public void BitOpDiff() var keyResult = prefix + "result"; // Clean up keys - db.KeyDelete(new RedisKey[] { keyX, keyY1, keyY2, keyResult }, CommandFlags.FireAndForget); + db.KeyDelete([keyX, keyY1, keyY2, keyResult], CommandFlags.FireAndForget); // Set up test data: X=11110000, Y1=10100000, Y2=01010000 // Expected DIFF result: X ∧ ¬(Y1 ∨ Y2) = 11110000 ∧ ¬(11110000) = 00000000 @@ -719,7 +715,7 @@ public void BitOpDiff() db.StringSet(keyY1, new byte[] { 0b10100000 }, flags: CommandFlags.FireAndForget); db.StringSet(keyY2, new byte[] { 0b01010000 }, flags: CommandFlags.FireAndForget); - var length = db.StringBitOperation(Bitwise.Diff, keyResult, new RedisKey[] { keyX, keyY1, keyY2 }); + var length = db.StringBitOperation(Bitwise.Diff, keyResult, [keyX, keyY1, keyY2]); Assert.Equal(1, length); var result = ((byte[]?)db.StringGet(keyResult))?.Single(); @@ -728,9 +724,9 @@ public void BitOpDiff() } [Fact] - public void BitOpDiff1() + public async Task BitOpDiff1() { - using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + await using var conn = Create(require: RedisFeatures.v8_2_0_rc1); var db = conn.GetDatabase(); var prefix = Me(); var keyX = prefix + "X"; @@ -739,7 +735,7 @@ public void BitOpDiff1() var keyResult = prefix + "result"; // Clean up keys - db.KeyDelete(new RedisKey[] { keyX, keyY1, keyY2, keyResult }, CommandFlags.FireAndForget); + db.KeyDelete([keyX, keyY1, keyY2, keyResult], CommandFlags.FireAndForget); // Set up test data: X=11000000, Y1=10100000, Y2=01010000 // Expected DIFF1 result: ¬X ∧ (Y1 ∨ Y2) = ¬11000000 ∧ (10100000 ∨ 01010000) = 00111111 ∧ 11110000 = 00110000 @@ -747,7 +743,7 @@ public void BitOpDiff1() db.StringSet(keyY1, new byte[] { 0b10100000 }, flags: CommandFlags.FireAndForget); db.StringSet(keyY2, new byte[] { 0b01010000 }, flags: CommandFlags.FireAndForget); - var length = db.StringBitOperation(Bitwise.Diff1, keyResult, new RedisKey[] { keyX, keyY1, keyY2 }); + var length = db.StringBitOperation(Bitwise.Diff1, keyResult, [keyX, keyY1, keyY2]); Assert.Equal(1, length); var result = ((byte[]?)db.StringGet(keyResult))?.Single(); @@ -756,9 +752,9 @@ public void BitOpDiff1() } [Fact] - public void BitOpAndOr() + public async Task BitOpAndOr() { - using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + await using var conn = Create(require: RedisFeatures.v8_2_0_rc1); var db = conn.GetDatabase(); var prefix = Me(); var keyX = prefix + "X"; @@ -767,7 +763,7 @@ public void BitOpAndOr() var keyResult = prefix + "result"; // Clean up keys - db.KeyDelete(new RedisKey[] { keyX, keyY1, keyY2, keyResult }, CommandFlags.FireAndForget); + db.KeyDelete([keyX, keyY1, keyY2, keyResult], CommandFlags.FireAndForget); // Set up test data: X=11110000, Y1=10100000, Y2=01010000 // Expected ANDOR result: X ∧ (Y1 ∨ Y2) = 11110000 ∧ (10100000 ∨ 01010000) = 11110000 ∧ 11110000 = 11110000 @@ -775,7 +771,7 @@ public void BitOpAndOr() db.StringSet(keyY1, new byte[] { 0b10100000 }, flags: CommandFlags.FireAndForget); db.StringSet(keyY2, new byte[] { 0b01010000 }, flags: CommandFlags.FireAndForget); - var length = db.StringBitOperation(Bitwise.AndOr, keyResult, new RedisKey[] { keyX, keyY1, keyY2 }); + var length = db.StringBitOperation(Bitwise.AndOr, keyResult, [keyX, keyY1, keyY2]); Assert.Equal(1, length); var result = ((byte[]?)db.StringGet(keyResult))?.Single(); @@ -784,9 +780,9 @@ public void BitOpAndOr() } [Fact] - public void BitOpOne() + public async Task BitOpOne() { - using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + await using var conn = Create(require: RedisFeatures.v8_2_0_rc1); var db = conn.GetDatabase(); var prefix = Me(); var key1 = prefix + "1"; @@ -795,7 +791,7 @@ public void BitOpOne() var keyResult = prefix + "result"; // Clean up keys - db.KeyDelete(new RedisKey[] { key1, key2, key3, keyResult }, CommandFlags.FireAndForget); + db.KeyDelete([key1, key2, key3, keyResult], CommandFlags.FireAndForget); // Set up test data: key1=10100000, key2=01010000, key3=00110000 // Expected ONE result: bits set in exactly one bitmap = 11000000 @@ -803,7 +799,7 @@ public void BitOpOne() db.StringSet(key2, new byte[] { 0b01010000 }, flags: CommandFlags.FireAndForget); db.StringSet(key3, new byte[] { 0b00110000 }, flags: CommandFlags.FireAndForget); - var length = db.StringBitOperation(Bitwise.One, keyResult, new RedisKey[] { key1, key2, key3 }); + var length = db.StringBitOperation(Bitwise.One, keyResult, [key1, key2, key3]); Assert.Equal(1, length); var result = ((byte[]?)db.StringGet(keyResult))?.Single(); @@ -814,7 +810,7 @@ public void BitOpOne() [Fact] public async Task BitOpDiffAsync() { - using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + await using var conn = Create(require: RedisFeatures.v8_2_0_rc1); var db = conn.GetDatabase(); var prefix = Me(); var keyX = prefix + "X"; @@ -822,14 +818,14 @@ public async Task BitOpDiffAsync() var keyResult = prefix + "result"; // Clean up keys - db.KeyDelete(new RedisKey[] { keyX, keyY1, keyResult }, CommandFlags.FireAndForget); + db.KeyDelete([keyX, keyY1, keyResult], CommandFlags.FireAndForget); // Set up test data: X=11110000, Y1=10100000 // Expected DIFF result: X ∧ ¬Y1 = 11110000 ∧ 01011111 = 01010000 db.StringSet(keyX, new byte[] { 0b11110000 }, flags: CommandFlags.FireAndForget); db.StringSet(keyY1, new byte[] { 0b10100000 }, flags: CommandFlags.FireAndForget); - var length = await db.StringBitOperationAsync(Bitwise.Diff, keyResult, new RedisKey[] { keyX, keyY1 }); + var length = await db.StringBitOperationAsync(Bitwise.Diff, keyResult, [keyX, keyY1]); Assert.Equal(1, length); var result = ((byte[]?)await db.StringGetAsync(keyResult))?.Single(); @@ -838,9 +834,9 @@ public async Task BitOpDiffAsync() } [Fact] - public void BitOpEdgeCases() + public async Task BitOpEdgeCases() { - using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + await using var conn = Create(require: RedisFeatures.v8_2_0_rc1); var db = conn.GetDatabase(); var prefix = Me(); var keyEmpty = prefix + "empty"; @@ -848,20 +844,20 @@ public void BitOpEdgeCases() var keyResult = prefix + "result"; // Clean up keys - db.KeyDelete(new RedisKey[] { keyEmpty, keyNonEmpty, keyResult }, CommandFlags.FireAndForget); + db.KeyDelete([keyEmpty, keyNonEmpty, keyResult], CommandFlags.FireAndForget); // Test with empty bitmap db.StringSet(keyNonEmpty, new byte[] { 0b11110000 }, flags: CommandFlags.FireAndForget); // DIFF with empty key should return the first key - var length = db.StringBitOperation(Bitwise.Diff, keyResult, new RedisKey[] { keyNonEmpty, keyEmpty }); + var length = db.StringBitOperation(Bitwise.Diff, keyResult, [keyNonEmpty, keyEmpty]); Assert.Equal(1, length); var result = ((byte[]?)db.StringGet(keyResult))?.Single(); Assert.Equal((byte)0b11110000, result); // ONE with single key should return that key - length = db.StringBitOperation(Bitwise.One, keyResult, new RedisKey[] { keyNonEmpty }); + length = db.StringBitOperation(Bitwise.One, keyResult, [keyNonEmpty]); Assert.Equal(1, length); result = ((byte[]?)db.StringGet(keyResult))?.Single(); @@ -871,7 +867,7 @@ public void BitOpEdgeCases() [Fact] public async Task BitPosition() { - using var conn = Create(require: RedisFeatures.v2_6_0); + await using var conn = Create(require: RedisFeatures.v2_6_0); var db = conn.GetDatabase(); var key = Me(); @@ -899,7 +895,7 @@ public async Task BitPosition() [Fact] public async Task BitPositionWithBitUnit() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key = Me(); @@ -916,7 +912,7 @@ public async Task BitPositionWithBitUnit() [Fact] public async Task RangeString() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var key = Me(); @@ -928,7 +924,7 @@ public async Task RangeString() [Fact] public async Task HashStringLengthAsync() { - using var conn = Create(require: RedisFeatures.v3_2_0); + await using var conn = Create(require: RedisFeatures.v3_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -941,9 +937,9 @@ public async Task HashStringLengthAsync() } [Fact] - public void HashStringLength() + public async Task HashStringLength() { - using var conn = Create(require: RedisFeatures.v3_2_0); + await using var conn = Create(require: RedisFeatures.v3_2_0); var db = conn.GetDatabase(); var key = Me(); @@ -954,9 +950,9 @@ public void HashStringLength() } [Fact] - public void LongestCommonSubsequence() + public async Task LongestCommonSubsequence() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key1 = Me() + "1"; @@ -996,7 +992,7 @@ public void LongestCommonSubsequence() [Fact] public async Task LongestCommonSubsequenceAsync() { - using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); var db = conn.GetDatabase(); var key1 = Me() + "1"; diff --git a/tests/StackExchange.Redis.Tests/SyncContextTests.cs b/tests/StackExchange.Redis.Tests/SyncContextTests.cs index fcc183ca4..8eddc9f1d 100644 --- a/tests/StackExchange.Redis.Tests/SyncContextTests.cs +++ b/tests/StackExchange.Redis.Tests/SyncContextTests.cs @@ -3,14 +3,11 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests { - public class SyncContextTests : TestBase + public class SyncContextTests(ITestOutputHelper testOutput) : TestBase(testOutput) { - public SyncContextTests(ITestOutputHelper testOutput) : base(testOutput) { } - /* Note A (referenced below) * * When sync-context is *enabled*, we don't validate OpCount > 0 - this is because *with the additional checks*, @@ -44,10 +41,10 @@ private void AssertState(bool continueOnCapturedContext, MySyncContext ctx) } [Fact] - public void SyncPing() + public async Task SyncPing() { using var ctx = new MySyncContext(Writer); - using var conn = Create(); + await using var conn = Create(); Assert.Equal(0, ctx.OpCount); var db = conn.GetDatabase(); db.Ping(); @@ -60,7 +57,7 @@ public void SyncPing() public async Task AsyncPing(bool continueOnCapturedContext) { using var ctx = new MySyncContext(Writer); - using var conn = Create(); + await using var conn = Create(); Assert.Equal(0, ctx.OpCount); var db = conn.GetDatabase(); Log($"Context before await: {ctx}"); @@ -70,10 +67,10 @@ public async Task AsyncPing(bool continueOnCapturedContext) } [Fact] - public void SyncConfigure() + public async Task SyncConfigure() { using var ctx = new MySyncContext(Writer); - using var conn = Create(); + await using var conn = Create(); Assert.Equal(0, ctx.OpCount); Assert.True(conn.Configure()); Assert.Equal(0, ctx.OpCount); @@ -85,7 +82,7 @@ public void SyncConfigure() public async Task AsyncConfigure(bool continueOnCapturedContext) { using var ctx = new MySyncContext(Writer); - using var conn = Create(); + await using var conn = Create(); Log($"Context initial: {ctx}"); await Task.Delay(500); diff --git a/tests/StackExchange.Redis.Tests/TaskExtensions.cs b/tests/StackExchange.Redis.Tests/TaskExtensions.cs new file mode 100644 index 000000000..19db48f7c --- /dev/null +++ b/tests/StackExchange.Redis.Tests/TaskExtensions.cs @@ -0,0 +1,49 @@ +#if !NET +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace StackExchange.Redis.Tests; + +internal static class TaskExtensions +{ + // suboptimal polyfill version of the .NET 6+ API; I'm not recommending this for production use, + // but it's good enough for tests + public static Task WaitAsync(this Task task, CancellationToken cancellationToken) + { + if (task.IsCompleted || !cancellationToken.CanBeCanceled) return task; + return Wrap(task, cancellationToken); + + static async Task Wrap(Task task, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + using var reg = cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)); + _ = task.ContinueWith(t => + { + if (t.IsCanceled) tcs.TrySetCanceled(); + else if (t.IsFaulted) tcs.TrySetException(t.Exception!); + else tcs.TrySetResult(t.Result); + }); + return await tcs.Task; + } + } + + public static Task WaitAsync(this Task task, TimeSpan timeout) + { + if (task.IsCompleted) return task; + return Wrap(task, timeout); + + static async Task Wrap(Task task, TimeSpan timeout) + { + Task other = Task.Delay(timeout); + var first = await Task.WhenAny(task, other); + if (ReferenceEquals(first, other)) + { + throw new TimeoutException(); + } + return await task; + } + } +} + +#endif diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 230438d07..94a14ee32 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -10,7 +10,6 @@ using StackExchange.Redis.Profiling; using StackExchange.Redis.Tests.Helpers; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; @@ -18,17 +17,9 @@ public abstract class TestBase : IDisposable { private ITestOutputHelper Output { get; } protected TextWriterOutputHelper Writer { get; } - protected static bool RunningInCI { get; } = Environment.GetEnvironmentVariable("APPVEYOR") != null; protected virtual string GetConfiguration() => GetDefaultConfiguration(); internal static string GetDefaultConfiguration() => TestConfig.Current.PrimaryServerAndPort; - /// - /// Gives the current TestContext, propulated by the runner (this type of thing will be built-in in xUnit 3.x). - /// - protected TestContext Context => _context.Value!; - private static readonly AsyncLocal _context = new(); - public static void SetContext(TestContext context) => _context.Value = context; - private readonly SharedConnectionFixture? _fixture; protected bool SharedFixtureAvailable => _fixture != null && _fixture.IsEnabled && !HighIntegrity; @@ -37,8 +28,7 @@ protected TestBase(ITestOutputHelper output, SharedConnectionFixture? fixture = { Output = output; Output.WriteFrameworkVersion(); - Output.WriteLine(" Context: " + Context.ToString()); - Writer = new TextWriterOutputHelper(output, TestConfig.Current.LogToConsole); + Writer = new TextWriterOutputHelper(output); _fixture = fixture; ClearAmbientFailures(); } @@ -60,22 +50,8 @@ public static void Log(TextWriter output, string message) { output?.WriteLine(Time() + ": " + message); } - if (TestConfig.Current.LogToConsole) - { - Console.WriteLine(message); - } - } - protected void Log(string? message, params object?[] args) - { - lock (Output) - { - Output.WriteLine(Time() + ": " + message, args); - } - if (TestConfig.Current.LogToConsole) - { - Console.WriteLine(message ?? "", args); - } } + protected void Log(string? message, params object[] args) => Output.WriteLine(Time() + ": " + message, args); protected ProfiledCommandEnumerable Log(ProfilingSession session) { @@ -157,8 +133,8 @@ protected void OnInternalError(object? sender, InternalErrorEventArgs e) private static readonly AsyncLocal sharedFailCount = new AsyncLocal(); private volatile int expectedFailCount; - private readonly List privateExceptions = new List(); - private static readonly List backgroundExceptions = new List(); + private readonly List privateExceptions = []; + private static readonly List backgroundExceptions = []; public void ClearAmbientFailures() { @@ -207,7 +183,7 @@ public void Teardown() Log(item); } } - Skip.Inconclusive($"There were {privateFailCount} private and {sharedFailCount.Value} ambient exceptions; expected {expectedFailCount}."); + Assert.Skip($"There were {privateFailCount} private and {sharedFailCount.Value} ambient exceptions; expected {expectedFailCount}."); } var pool = SocketManager.Shared?.SchedulerPool; Log($"Service Counts: (Scheduler) Queue: {pool?.TotalServicedByQueue.ToString()}, Pool: {pool?.TotalServicedByPool.ToString()}, Workers: {pool?.WorkerCount.ToString()}, Available: {pool?.AvailableCount.ToString()}"); @@ -270,22 +246,23 @@ internal virtual IInternalConnectionMultiplexer Create( } // Default to protocol context if not explicitly passed in - protocol ??= Context.Test.Protocol; + protocol ??= TestContext.Current.GetProtocol(); // Share a connection if instructed to and we can - many specifics mean no sharing bool highIntegrity = HighIntegrity; if (shared && expectedFailCount == 0 && _fixture != null && _fixture.IsEnabled + && GetConfiguration() == GetDefaultConfiguration() && CanShare(allowAdmin, password, tieBreaker, fail, disabledCommands, enabledCommands, channelPrefix, proxy, configuration, defaultDatabase, backlogPolicy, highIntegrity)) { configuration = GetConfiguration(); var fixtureConn = _fixture.GetConnection(this, protocol.Value, caller: caller); // Only return if we match - ThrowIfIncorrectProtocol(fixtureConn, protocol); + TestBase.ThrowIfIncorrectProtocol(fixtureConn, protocol); if (configuration == _fixture.Configuration) { - ThrowIfBelowMinVersion(fixtureConn, require); + TestBase.ThrowIfBelowMinVersion(fixtureConn, require); return fixtureConn; } } @@ -316,8 +293,8 @@ internal virtual IInternalConnectionMultiplexer Create( highIntegrity, caller); - ThrowIfIncorrectProtocol(conn, protocol); - ThrowIfBelowMinVersion(conn, require); + TestBase.ThrowIfIncorrectProtocol(conn, protocol); + TestBase.ThrowIfBelowMinVersion(conn, require); conn.InternalError += OnInternalError; conn.ConnectionFailed += OnConnectionFailed; @@ -351,7 +328,7 @@ internal static bool CanShare( && backlogPolicy == null && !highIntegrity; - internal void ThrowIfIncorrectProtocol(IInternalConnectionMultiplexer conn, RedisProtocol? requiredProtocol) + internal static void ThrowIfIncorrectProtocol(IInternalConnectionMultiplexer conn, RedisProtocol? requiredProtocol) { if (requiredProtocol is null) { @@ -361,14 +338,11 @@ internal void ThrowIfIncorrectProtocol(IInternalConnectionMultiplexer conn, Redi var serverProtocol = conn.GetServerEndPoint(conn.GetEndPoints()[0]).Protocol ?? RedisProtocol.Resp2; if (serverProtocol != requiredProtocol) { - throw new SkipTestException($"Requires protocol {requiredProtocol}, but connection is {serverProtocol}.") - { - MissingFeatures = $"Protocol {requiredProtocol}.", - }; + Assert.Skip($"Requires protocol {requiredProtocol}, but connection is {serverProtocol}."); } } - internal void ThrowIfBelowMinVersion(IInternalConnectionMultiplexer conn, Version? requiredVersion) + internal static void ThrowIfBelowMinVersion(IInternalConnectionMultiplexer conn, Version? requiredVersion) { if (requiredVersion is null) { @@ -378,10 +352,7 @@ internal void ThrowIfBelowMinVersion(IInternalConnectionMultiplexer conn, Versio var serverVersion = conn.GetServerEndPoint(conn.GetEndPoints()[0]).Version; if (!serverVersion.IsAtLeast(requiredVersion)) { - throw new SkipTestException($"Requires server version {requiredVersion}, but server is only {serverVersion}.") - { - MissingFeatures = $"Server version >= {requiredVersion}.", - }; + Assert.Skip($"Requires server version {requiredVersion}, but server is only {serverVersion}."); } } @@ -418,11 +389,11 @@ public static ConnectionMultiplexer CreateDefault( var config = ConfigurationOptions.Parse(configuration); if (disabledCommands != null && disabledCommands.Length != 0) { - config.CommandMap = CommandMap.Create(new HashSet(disabledCommands), false); + config.CommandMap = CommandMap.Create([.. disabledCommands], false); } else if (enabledCommands != null && enabledCommands.Length != 0) { - config.CommandMap = CommandMap.Create(new HashSet(enabledCommands), true); + config.CommandMap = CommandMap.Create([.. enabledCommands], true); } if (Debugger.IsAttached) @@ -471,7 +442,7 @@ public static ConnectionMultiplexer CreateDefault( { // If fail is true, we throw. Assert.False(fail, failMessage + "Server is not available"); - Skip.Inconclusive(failMessage + "Server is not available"); + Assert.Skip(failMessage + "Server is not available"); } if (output != null) { @@ -502,13 +473,23 @@ public static ConnectionMultiplexer CreateDefault( } public virtual string Me([CallerFilePath] string? filePath = null, [CallerMemberName] string? caller = null) => - Environment.Version.ToString() + Path.GetFileNameWithoutExtension(filePath) + "-" + caller + Context.KeySuffix; + Environment.Version.ToString() + "-" + GetType().Name + "-" + Path.GetFileNameWithoutExtension(filePath) + "-" + caller + TestContext.Current.KeySuffix(); protected TimeSpan RunConcurrent(Action work, int threads, int timeout = 10000, [CallerMemberName] string? caller = null) { - if (work == null) throw new ArgumentNullException(nameof(work)); - if (threads < 1) throw new ArgumentOutOfRangeException(nameof(threads)); - if (string.IsNullOrWhiteSpace(caller)) caller = Me(); + if (work == null) + { + throw new ArgumentNullException(nameof(work)); + } + if (threads < 1) + { + throw new ArgumentOutOfRangeException(nameof(threads)); + } + if (string.IsNullOrWhiteSpace(caller)) + { + caller = Me(); + } + Stopwatch? watch = null; ManualResetEvent allDone = new ManualResetEvent(false); object token = new object(); diff --git a/tests/StackExchange.Redis.Tests/TransactionTests.cs b/tests/StackExchange.Redis.Tests/TransactionTests.cs index db4554bef..daee7ee00 100644 --- a/tests/StackExchange.Redis.Tests/TransactionTests.cs +++ b/tests/StackExchange.Redis.Tests/TransactionTests.cs @@ -1,20 +1,16 @@ using System; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; [RunPerProtocol] -[Collection(SharedConnectionFixture.Key)] -public class TransactionTests : TestBase +public class TransactionTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public TransactionTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void BasicEmptyTran() + public async Task BasicEmptyTran() { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(); var db = conn.GetDatabase(); @@ -28,9 +24,9 @@ public void BasicEmptyTran() } [Fact] - public void NestedTransactionThrows() + public async Task NestedTransactionThrows() { - using var conn = Create(); + await using var conn = Create(); var db = conn.GetDatabase(); var tran = db.CreateTransaction(); @@ -45,7 +41,7 @@ public void NestedTransactionThrows() [InlineData(true, true, true)] public async Task BasicTranWithExistsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) { - using var conn = Create(disabledCommands: new[] { "info", "config" }); + await using var conn = Create(disabledCommands: ["info", "config"]); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -92,7 +88,7 @@ public async Task BasicTranWithExistsCondition(bool demandKeyExists, bool keyExi [InlineData(null, null, false, false)] public async Task BasicTranWithEqualsCondition(string? expected, string? value, bool expectEqual, bool expectTranResult) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -133,7 +129,7 @@ public async Task BasicTranWithEqualsCondition(string? expected, string? value, [InlineData(true, true, true)] public async Task BasicTranWithHashExistsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) { - using var conn = Create(disabledCommands: new[] { "info", "config" }); + await using var conn = Create(disabledCommands: ["info", "config"]); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -181,7 +177,7 @@ public async Task BasicTranWithHashExistsCondition(bool demandKeyExists, bool ke [InlineData(null, null, false, false)] public async Task BasicTranWithHashEqualsCondition(string? expected, string? value, bool expectEqual, bool expectedTranResult) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -245,7 +241,7 @@ private static TaskStatus SafeStatus(Task task) [InlineData(true, true, true)] public async Task BasicTranWithListExistsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) { - using var conn = Create(disabledCommands: new[] { "info", "config" }); + await using var conn = Create(disabledCommands: ["info", "config"]); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -292,7 +288,7 @@ public async Task BasicTranWithListExistsCondition(bool demandKeyExists, bool ke [InlineData(null, null, false, false)] public async Task BasicTranWithListEqualsCondition(string? expected, string? value, bool expectEqual, bool expectTranResult) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -359,7 +355,7 @@ public enum ComparisonType [InlineData(null, ComparisonType.GreaterThan, 0L, false)] public async Task BasicTranWithStringLengthCondition(string? value, ComparisonType type, long length, bool expectTranResult) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -438,7 +434,7 @@ public async Task BasicTranWithStringLengthCondition(string? value, ComparisonTy [InlineData("", ComparisonType.GreaterThan, 0L, false)] public async Task BasicTranWithHashLengthCondition(string value, ComparisonType type, long length, bool expectTranResult) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -517,7 +513,7 @@ public async Task BasicTranWithHashLengthCondition(string value, ComparisonType [InlineData("", ComparisonType.GreaterThan, 0L, false)] public async Task BasicTranWithSetCardinalityCondition(string value, ComparisonType type, long length, bool expectTranResult) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -583,7 +579,7 @@ public async Task BasicTranWithSetCardinalityCondition(string value, ComparisonT [InlineData(true, true, true)] public async Task BasicTranWithSetContainsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) { - using var conn = Create(disabledCommands: new[] { "info", "config" }); + await using var conn = Create(disabledCommands: ["info", "config"]); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -637,7 +633,7 @@ public async Task BasicTranWithSetContainsCondition(bool demandKeyExists, bool k [InlineData("", ComparisonType.GreaterThan, 0L, false)] public async Task BasicTranWithSortedSetCardinalityCondition(string value, ComparisonType type, long length, bool expectTranResult) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -716,7 +712,7 @@ public async Task BasicTranWithSortedSetCardinalityCondition(string value, Compa [InlineData(0, 0, ComparisonType.GreaterThan, 0L, true)] public async Task BasicTranWithSortedSetRangeCountCondition(double min, double max, ComparisonType type, long length, bool expectTranResult) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -782,7 +778,7 @@ public async Task BasicTranWithSortedSetRangeCountCondition(double min, double m [InlineData(true, true, true)] public async Task BasicTranWithSortedSetContainsCondition(bool demandKeyExists, bool keyExists, bool expectTranResult) { - using var conn = Create(disabledCommands: new[] { "info", "config" }); + await using var conn = Create(disabledCommands: ["info", "config"]); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -830,7 +826,7 @@ public async Task BasicTranWithSortedSetContainsCondition(bool demandKeyExists, [InlineData(null, null, false, false)] public async Task BasicTranWithSortedSetEqualCondition(double? expected, double? value, bool expectEqual, bool expectedTranResult) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -876,7 +872,7 @@ public async Task BasicTranWithSortedSetEqualCondition(double? expected, double? [InlineData(false, false, false, true)] public async Task BasicTranWithSortedSetScoreExistsCondition(bool member1HasScore, bool member2HasScore, bool demandScoreExists, bool expectedTranResult) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -942,7 +938,7 @@ public async Task BasicTranWithSortedSetScoreExistsCondition(bool member1HasScor [InlineData(false, false, 1L, false, true)] public async Task BasicTranWithSortedSetScoreCountExistsCondition(bool member1HasScore, bool member2HasScore, long expectedLength, bool expectEqual, bool expectedTranResult) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -1011,7 +1007,7 @@ public async Task BasicTranWithSortedSetScoreCountExistsCondition(bool member1Ha [InlineData("", ComparisonType.GreaterThan, 0L, false)] public async Task BasicTranWithListLengthCondition(string value, ComparisonType type, long length, bool expectTranResult) { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -1090,7 +1086,7 @@ public async Task BasicTranWithListLengthCondition(string value, ComparisonType [InlineData("", ComparisonType.GreaterThan, 0L, false)] public async Task BasicTranWithStreamLengthCondition(string value, ComparisonType type, long length, bool expectTranResult) { - using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: RedisFeatures.v5_0_0); RedisKey key = Me(), key2 = Me() + "2"; var db = conn.GetDatabase(); @@ -1152,7 +1148,7 @@ public async Task BasicTranWithStreamLengthCondition(string value, ComparisonTyp [Fact] public async Task BasicTran() { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(); var db = conn.GetDatabase(); @@ -1196,7 +1192,7 @@ public async Task BasicTran() [Fact] public async Task CombineFireAndForgetAndRegularAsyncInTransaction() { - using var conn = Create(); + await using var conn = Create(); RedisKey key = Me(); var db = conn.GetDatabase(); @@ -1222,7 +1218,7 @@ public async Task CombineFireAndForgetAndRegularAsyncInTransaction() [Fact] public async Task TransactionWithAdHocCommandsAndSelectDisabled() { - using var conn = Create(disabledCommands: new string[] { "SELECT" }); + await using var conn = Create(disabledCommands: ["SELECT"]); RedisKey key = Me(); var db = conn.GetDatabase(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -1239,8 +1235,8 @@ public async Task TransactionWithAdHocCommandsAndSelectDisabled() [Fact] public async Task WatchAbort_StringEqual() { - using var vicConn = Create(); - using var perpConn = Create(); + await using var vicConn = Create(); + await using var perpConn = Create(); var key = Me(); var db = vicConn.GetDatabase(); @@ -1263,8 +1259,8 @@ public async Task WatchAbort_StringEqual() [Fact] public async Task WatchAbort_HashLengthEqual() { - using var vicConn = Create(); - using var perpConn = Create(); + await using var vicConn = Create(); + await using var perpConn = Create(); var key = Me(); var db = vicConn.GetDatabase(); @@ -1285,21 +1281,22 @@ public async Task WatchAbort_HashLengthEqual() } #endif - [FactLongRunning] + [Fact] public async Task ExecCompletes_Issue943() { + Skip.UnlessLongRunning(); int hashHit = 0, hashMiss = 0, expireHit = 0, expireMiss = 0; - using (var conn = Create()) + await using (var conn = Create()) { var db = conn.GetDatabase(); for (int i = 0; i < 40000; i++) { RedisKey key = Me(); await db.KeyDeleteAsync(key); - HashEntry[] hashEntries = new[] - { + HashEntry[] hashEntries = + [ new HashEntry("blah", DateTime.UtcNow.ToString("R")), - }; + ]; ITransaction transaction = db.CreateTransaction(); transaction.AddCondition(Condition.KeyNotExists(key)); Task hashSetTask = transaction.HashSetAsync(key, hashEntries); diff --git a/tests/StackExchange.Redis.Tests/ValueTests.cs b/tests/StackExchange.Redis.Tests/ValueTests.cs index 1877eb7c7..69a8a2cbc 100644 --- a/tests/StackExchange.Redis.Tests/ValueTests.cs +++ b/tests/StackExchange.Redis.Tests/ValueTests.cs @@ -2,14 +2,11 @@ using System.IO; using System.Text; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -public class ValueTests : TestBase +public class ValueTests(ITestOutputHelper output) : TestBase(output) { - public ValueTests(ITestOutputHelper output) : base(output) { } - [Fact] public void NullValueChecks() { diff --git a/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs b/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs index 72a99f2cc..acbef74cf 100644 --- a/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs +++ b/tests/StackExchange.Redis.Tests/WithKeyPrefixTests.cs @@ -1,19 +1,16 @@ using System; +using System.Threading.Tasks; using StackExchange.Redis.KeyspaceIsolation; using Xunit; -using Xunit.Abstractions; namespace StackExchange.Redis.Tests; -[Collection(SharedConnectionFixture.Key)] -public class WithKeyPrefixTests : TestBase +public class WithKeyPrefixTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { - public WithKeyPrefixTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture) { } - [Fact] - public void BlankPrefixYieldsSame_Bytes() + public async Task BlankPrefixYieldsSame_Bytes() { - using var conn = Create(); + await using var conn = Create(); var raw = conn.GetDatabase(); var prefixed = raw.WithKeyPrefix(Array.Empty()); @@ -21,9 +18,9 @@ public void BlankPrefixYieldsSame_Bytes() } [Fact] - public void BlankPrefixYieldsSame_String() + public async Task BlankPrefixYieldsSame_String() { - using var conn = Create(); + await using var conn = Create(); var raw = conn.GetDatabase(); var prefixed = raw.WithKeyPrefix(""); @@ -31,11 +28,11 @@ public void BlankPrefixYieldsSame_String() } [Fact] - public void NullPrefixIsError_Bytes() + public async Task NullPrefixIsError_Bytes() { - Assert.Throws(() => + await Assert.ThrowsAsync(async () => { - using var conn = Create(); + await using var conn = Create(); var raw = conn.GetDatabase(); raw.WithKeyPrefix((byte[]?)null); @@ -43,11 +40,11 @@ public void NullPrefixIsError_Bytes() } [Fact] - public void NullPrefixIsError_String() + public async Task NullPrefixIsError_String() { - Assert.Throws(() => + await Assert.ThrowsAsync(async () => { - using var conn = Create(); + await using var conn = Create(); var raw = conn.GetDatabase(); raw.WithKeyPrefix((string?)null); @@ -68,9 +65,9 @@ public void NullDatabaseIsError(string? prefix) } [Fact] - public void BasicSmokeTest() + public async Task BasicSmokeTest() { - using var conn = Create(); + await using var conn = Create(); var raw = conn.GetDatabase(); @@ -101,9 +98,9 @@ public void BasicSmokeTest() } [Fact] - public void ConditionTest() + public async Task ConditionTest() { - using var conn = Create(); + await using var conn = Create(); var raw = conn.GetDatabase(); @@ -117,7 +114,7 @@ public void ConditionTest() raw.StringSet(prefix + "abc", "def", flags: CommandFlags.FireAndForget); var tran = foo.CreateTransaction(); tran.AddCondition(Condition.KeyExists("abc")); - tran.StringIncrementAsync("i"); + _ = tran.StringIncrementAsync("i"); tran.Execute(); int i = (int)raw.StringGet(prefix + "i"); @@ -127,7 +124,7 @@ public void ConditionTest() raw.KeyDelete(prefix + "abc", CommandFlags.FireAndForget); tran = foo.CreateTransaction(); tran.AddCondition(Condition.KeyExists("abc")); - tran.StringIncrementAsync("i"); + _ = tran.StringIncrementAsync("i"); tran.Execute(); i = (int)raw.StringGet(prefix + "i"); diff --git a/tests/StackExchange.Redis.Tests/xunit.runner.json b/tests/StackExchange.Redis.Tests/xunit.runner.json index 99e81e741..dc36b1875 100644 --- a/tests/StackExchange.Redis.Tests/xunit.runner.json +++ b/tests/StackExchange.Redis.Tests/xunit.runner.json @@ -1,9 +1,8 @@ { "methodDisplay": "classAndMethod", - "maxParallelThreads": 16, "parallelizeAssembly": true, + "maxParallelThreads": "2x", "parallelizeTestCollections": true, - "parallelAlgorithm": "aggressive", "diagnosticMessages": false, "longRunningTestSeconds": 60 } \ No newline at end of file From afd66ef734c01eebad4c814072115166649bf096 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 21 Jul 2025 09:53:41 +0100 Subject: [PATCH 339/435] test fix; skip CLIENT PAUSE tests unless long-running enabled (#2916) --- tests/StackExchange.Redis.Tests/CancellationTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/StackExchange.Redis.Tests/CancellationTests.cs b/tests/StackExchange.Redis.Tests/CancellationTests.cs index ba23fb6ed..71306e9dd 100644 --- a/tests/StackExchange.Redis.Tests/CancellationTests.cs +++ b/tests/StackExchange.Redis.Tests/CancellationTests.cs @@ -43,6 +43,8 @@ public async Task WithCancellation_ValidToken_OperationSucceeds() [Fact] public async Task WithTimeout_ShortTimeout_Async_ThrowsOperationCanceledException() { + Skip.UnlessLongRunning(); // because of CLIENT PAUSE impact to unrelated tests + await using var conn = Create(); var db = conn.GetDatabase(); @@ -119,6 +121,8 @@ private static CancellationTokenSource CreateCts(CancelStrategy strategy) [InlineData(CancelStrategy.Manual)] public async Task CancellationDuringOperation_Async_CancelsGracefully(CancelStrategy strategy) { + Skip.UnlessLongRunning(); // because of CLIENT PAUSE impact to unrelated tests + await using var conn = Create(); var db = conn.GetDatabase(); From c54c159d71025945b2b72fc7ceaf53a455315ec2 Mon Sep 17 00:00:00 2001 From: Robert Hopland Date: Mon, 21 Jul 2025 11:01:27 +0200 Subject: [PATCH 340/435] StreamGroupInfo.Lag can be null (#2902) * TryRead for nullable long, tests on null value for StreamConsumerGroupInfo.Lag * Update StreamTests.cs make tests async * Update StreamTests.cs tyop --------- Co-authored-by: Marc Gravell --- src/StackExchange.Redis/ResultProcessor.cs | 16 +++++++- .../StackExchange.Redis.Tests/StreamTests.cs | 39 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 06647212b..08f9a20ee 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -2289,6 +2289,19 @@ internal static bool TryRead(Sequence pairs, in CommandBytes key, ref } return false; } + internal static bool TryRead(Sequence pairs, in CommandBytes key, ref long? value) + { + var len = pairs.Length / 2; + for (int i = 0; i < len; i++) + { + if (pairs[i * 2].IsEqual(key) && pairs[(i * 2) + 1].TryGetInt64(out var tmp)) + { + value = tmp; + return true; + } + } + return false; + } internal static bool TryRead(Sequence pairs, in CommandBytes key, ref int value) { @@ -2351,7 +2364,8 @@ protected override StreamGroupInfo ParseItem(in RawResult result) var arr = result.GetItems(); string? name = default, lastDeliveredId = default; int consumerCount = default, pendingMessageCount = default; - long entriesRead = default, lag = default; + long entriesRead = default; + long? lag = default; KeyValuePairParser.TryRead(arr, KeyValuePairParser.Name, ref name); KeyValuePairParser.TryRead(arr, KeyValuePairParser.Consumers, ref consumerCount); diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index aef914293..05591db27 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -2036,4 +2036,43 @@ await db.StreamAddAsync( Assert.Equal(123, (int)obj!.id); Assert.Equal("test", (string)obj.name); } + + [Fact] + public async Task StreamConsumerGroupInfoLagIsNull() + { + await using var conn = Create(require: RedisFeatures.v5_0_0); + + var db = conn.GetDatabase(); + var key = Me(); + const string groupName = "test_group", + consumer = "consumer"; + + await db.StreamCreateConsumerGroupAsync(key, groupName); + await db.StreamReadGroupAsync(key, groupName, consumer, "0-0", 1); + await db.StreamAddAsync(key, "field1", "value1"); + await db.StreamAddAsync(key, "field1", "value1"); + + var streamInfo = await db.StreamInfoAsync(key); + await db.StreamDeleteAsync(key, new[] { streamInfo.LastEntry.Id }); + + Assert.Null((await db.StreamGroupInfoAsync(key))[0].Lag); + } + + [Fact] + public async Task StreamConsumerGroupInfoLagIsTwo() + { + await using var conn = Create(require: RedisFeatures.v5_0_0); + + var db = conn.GetDatabase(); + var key = Me(); + const string groupName = "test_group", + consumer = "consumer"; + + await db.StreamCreateConsumerGroupAsync(key, groupName); + await db.StreamReadGroupAsync(key, groupName, consumer, "0-0", 1); + await db.StreamAddAsync(key, "field1", "value1"); + await db.StreamAddAsync(key, "field1", "value1"); + + Assert.Equal(2, (await db.StreamGroupInfoAsync(key))[0].Lag); + } } From 6b02ba86373fb569a162ffd13a032efe254c3187 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 21 Jul 2025 10:01:47 +0100 Subject: [PATCH 341/435] Add xtrim with minid and new 8.2 stream features (#2912) * add minId overloads * fix test. clarify doc comment i now think the XTRIM documentation is saying that an entry at exactly MINID is kept. https://redis.io/docs/latest/commands/xtrim/ * fix test. forgot to update the length check. * change method name to StreamTrimByMinIdAsync implement useApproximateMaxLength and limit as per docs * add stream delete mode to minid api add xackdel * xref new pr * add limit to StreamTrim (maxlen) * XADD KEEPREF|DELREF|ACKED * more release notes * naming is hard * XDELEX * merge shipped * Update StreamTests.cs --------- Co-authored-by: Kijana Woodard --- StackExchange.Redis.sln | 1 + docs/ReleaseNotes.md | 2 + src/StackExchange.Redis/Enums/RedisCommand.cs | 4 + .../Enums/StreamTrimMode.cs | 24 ++ .../Enums/StreamTrimResult.cs | 23 ++ .../Interfaces/IDatabase.cs | 114 +++++++- .../Interfaces/IDatabaseAsync.cs | 33 ++- .../KeyspaceIsolation/KeyPrefixed.cs | 27 +- .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 27 +- .../PublicAPI/PublicAPI.Shipped.txt | 36 ++- src/StackExchange.Redis/RedisDatabase.cs | 244 ++++++++++++++---- src/StackExchange.Redis/ResultProcessor.cs | 72 ++++++ src/StackExchange.Redis/StreamConstants.cs | 17 +- .../KeyPrefixedDatabaseTests.cs | 21 ++ .../KeyPrefixedTests.cs | 21 ++ .../StackExchange.Redis.Tests/StreamTests.cs | 187 +++++++++++++- 16 files changed, 771 insertions(+), 82 deletions(-) create mode 100644 src/StackExchange.Redis/Enums/StreamTrimMode.cs create mode 100644 src/StackExchange.Redis/Enums/StreamTrimResult.cs diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index 6e4416d7d..20b5e2f01 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution docs\ReleaseNotes.md = docs\ReleaseNotes.md Shared.ruleset = Shared.ruleset version.json = version.json + tests\RedisConfigs\docker-compose.yml = tests\RedisConfigs\docker-compose.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RedisConfigs", "RedisConfigs", "{96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}" diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 6754ea017..b301cca8b 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -16,6 +16,8 @@ Current package versions: - Package updates ([#2906 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2906)) - Docs: added [guidance on async timeouts](https://stackexchange.github.io/StackExchange.Redis/AsyncTimeouts) ([#2910 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2910)) - Fix handshake error with `CLIENT ID` ([#2909 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2909)) +- Add `XTRIM MINID` support ([#2842 by kijanawoodard](https://github.com/StackExchange/StackExchange.Redis/pull/2842)) +- Add new CE 8.2 stream support - `XDELEX`, `XACKDEL`, `{XADD|XTRIM} [KEEPREF|DELREF|ACKED]` ([#2912 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2912)) ## 2.8.41 diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 3909be4c2..34e1eb296 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -206,10 +206,12 @@ internal enum RedisCommand WATCH, XACK, + XACKDEL, XADD, XAUTOCLAIM, XCLAIM, XDEL, + XDELEX, XGROUP, XINFO, XLEN, @@ -496,9 +498,11 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.GEOADD: case RedisCommand.SORT: case RedisCommand.XACK: + case RedisCommand.XACKDEL: case RedisCommand.XADD: case RedisCommand.XCLAIM: case RedisCommand.XDEL: + case RedisCommand.XDELEX: case RedisCommand.XGROUP: case RedisCommand.XREADGROUP: case RedisCommand.XTRIM: diff --git a/src/StackExchange.Redis/Enums/StreamTrimMode.cs b/src/StackExchange.Redis/Enums/StreamTrimMode.cs new file mode 100644 index 000000000..2033e8414 --- /dev/null +++ b/src/StackExchange.Redis/Enums/StreamTrimMode.cs @@ -0,0 +1,24 @@ +namespace StackExchange.Redis; + +/// +/// Determines how stream trimming works. +/// +public enum StreamTrimMode +{ + /// + /// Trims the stream according to the specified policy (MAXLEN or MINID) regardless of whether entries are referenced by any consumer groups, but preserves existing references to these entries in all consumer groups' PEL. + /// + KeepReferences = 0, + + /// + /// Trims the stream according to the specified policy and also removes all references to the trimmed entries from all consumer groups' PEL. + /// + /// Requires server 8.2 or above. + DeleteReferences = 1, + + /// + /// With ACKED: Only trims entries that were read and acknowledged by all consumer groups. + /// + /// Requires server 8.2 or above. + Acknowledged = 2, +} diff --git a/src/StackExchange.Redis/Enums/StreamTrimResult.cs b/src/StackExchange.Redis/Enums/StreamTrimResult.cs new file mode 100644 index 000000000..aa157a8a0 --- /dev/null +++ b/src/StackExchange.Redis/Enums/StreamTrimResult.cs @@ -0,0 +1,23 @@ +namespace StackExchange.Redis; + +/// +/// Determines how stream trimming works. +/// +public enum StreamTrimResult +{ + /// + /// No such id exists in the provided stream key. + /// + NotFound = -1, + + /// + /// Entry was deleted from the stream. + /// + Deleted = 1, + + /// + /// Entry was not deleted, but there are still dangling references. + /// + /// This response relates to the mode. + NotDeleted = 2, +} diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 207c03326..c37d3ddb0 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -2440,6 +2440,34 @@ IEnumerable SortedSetScan( /// long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); + /// + /// Allow the consumer to mark a pending message as correctly processed. Returns the number of messages acknowledged. + /// + /// The key of the stream. + /// The name of the consumer group that received the message. + /// The delete mode to use when acknowledging the message. + /// The ID of the message to acknowledge. + /// The flags to use for this operation. + /// The outcome of the delete operation. + /// +#pragma warning disable RS0026 // similar overloads + StreamTrimResult StreamAcknowledgeAndDelete(RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue messageId, CommandFlags flags = CommandFlags.None); +#pragma warning restore RS0026 + + /// + /// Allow the consumer to mark a pending message as correctly processed. Returns the number of messages acknowledged. + /// + /// The key of the stream. + /// The name of the consumer group that received the message. + /// /// The delete mode to use when acknowledging the message. + /// The IDs of the messages to acknowledge. + /// The flags to use for this operation. + /// The outcome of each delete operation. + /// +#pragma warning disable RS0026 // similar overloads + StreamTrimResult[] StreamAcknowledgeAndDelete(RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); +#pragma warning restore RS0026 + /// /// Adds an entry using the specified values to the given stream key. /// If key does not exist, a new key holding a stream is created. @@ -2454,7 +2482,7 @@ IEnumerable SortedSetScan( /// The flags to use for this operation. /// The ID of the newly created message. /// - RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); + RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, CommandFlags flags); /// /// Adds an entry using the specified values to the given stream key. @@ -2469,7 +2497,46 @@ IEnumerable SortedSetScan( /// The flags to use for this operation. /// The ID of the newly created message. /// - RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); + RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, CommandFlags flags); + + /// + /// Adds an entry using the specified values to the given stream key. + /// If key does not exist, a new key holding a stream is created. + /// The command returns the ID of the newly created stream entry. + /// + /// The key of the stream. + /// The field name for the stream entry. + /// The value to set in the stream entry. + /// The ID to assign to the stream entry, defaults to an auto-generated ID ("*"). + /// The maximum length of the stream. + /// If true, the "~" argument is used to allow the stream to exceed max length by a small number. This improves performance when removing messages. + /// Specifies the maximal count of entries that will be evicted. + /// Determines how stream trimming should be performed. + /// The flags to use for this operation. + /// The ID of the newly created message. + /// +#pragma warning disable RS0026 // different shape + RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); +#pragma warning restore RS0026 + + /// + /// Adds an entry using the specified values to the given stream key. + /// If key does not exist, a new key holding a stream is created. + /// The command returns the ID of the newly created stream entry. + /// + /// The key of the stream. + /// The fields and their associated values to set in the stream entry. + /// The ID to assign to the stream entry, defaults to an auto-generated ID ("*"). + /// The maximum length of the stream. + /// If true, the "~" argument is used to allow the stream to exceed max length by a small number. This improves performance when removing messages. + /// Specifies the maximal count of entries that will be evicted. + /// Determines how stream trimming should be performed. + /// The flags to use for this operation. + /// The ID of the newly created message. + /// +#pragma warning disable RS0026 // different shape + RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); +#pragma warning restore RS0026 /// /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. @@ -2583,7 +2650,22 @@ IEnumerable SortedSetScan( /// The flags to use for this operation. /// Returns the number of messages successfully deleted from the stream. /// +#pragma warning disable RS0026 // similar overloads long StreamDelete(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); +#pragma warning restore RS0026 + + /// + /// Delete messages in the stream. This method does not delete the stream. + /// + /// The key of the stream. + /// The IDs of the messages to delete. + /// Determines how stream trimming should be performed. + /// The flags to use for this operation. + /// Returns the number of messages successfully deleted from the stream. + /// +#pragma warning disable RS0026 // similar overloads + StreamTrimResult[] StreamDelete(RedisKey key, RedisValue[] messageIds, StreamTrimMode mode, CommandFlags flags = CommandFlags.None); +#pragma warning restore RS0026 /// /// Delete a consumer from a consumer group. @@ -2773,7 +2855,33 @@ IEnumerable SortedSetScan( /// The flags to use for this operation. /// The number of messages removed from the stream. /// - long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); + long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength, CommandFlags flags); + + /// + /// Trim the stream to a specified maximum length. + /// + /// The key of the stream. + /// The maximum length of the stream. + /// If true, the "~" argument is used to allow the stream to exceed max length by a small number. This improves performance when removing messages. + /// Specifies the maximal count of entries that will be evicted. + /// Determines how stream trimming should be performed. + /// The flags to use for this operation. + /// The number of messages removed from the stream. + /// + long StreamTrim(RedisKey key, long maxLength, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); + + /// + /// Trim the stream to a specified minimum timestamp. + /// + /// The key of the stream. + /// All entries with an id (timestamp) earlier minId will be removed. + /// If true, the "~" argument is used to allow the stream to exceed minId by a small number. This improves performance when removing messages. + /// The maximum number of entries to remove per call when useApproximateMaxLength = true. If 0, the limiting mechanism is disabled entirely. + /// Determines how stream trimming should be performed. + /// The flags to use for this operation. + /// The number of messages removed from the stream. + /// + long StreamTrimByMinId(RedisKey key, RedisValue minId, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); /// /// If key already exists and is a string, this command appends the value at the end of the string. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 9852c131c..4873c1069 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -594,11 +594,27 @@ IAsyncEnumerable SortedSetScanAsync( /// Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); +#pragma warning disable RS0026 // similar overloads + /// + Task StreamAcknowledgeAndDeleteAsync(RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue messageId, CommandFlags flags = CommandFlags.None); + + /// + Task StreamAcknowledgeAndDeleteAsync(RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); +#pragma warning restore RS0026 + /// - Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); + Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, CommandFlags flags); /// - Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); + Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, CommandFlags flags); + +#pragma warning disable RS0026 // similar overloads + /// + Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); + + /// + Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); +#pragma warning restore RS0026 /// Task StreamAutoClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None); @@ -624,9 +640,14 @@ IAsyncEnumerable SortedSetScanAsync( /// Task StreamCreateConsumerGroupAsync(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None); +#pragma warning disable RS0026 /// Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None); + /// + Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, StreamTrimMode mode, CommandFlags flags = CommandFlags.None); +#pragma warning restore RS0026 + /// Task StreamDeleteConsumerAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None); @@ -670,7 +691,13 @@ IAsyncEnumerable SortedSetScanAsync( Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None); /// - Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None); + Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength, CommandFlags flags); + + /// + Task StreamTrimAsync(RedisKey key, long maxLength, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); + + /// + Task StreamTrimByMinIdAsync(RedisKey key, RedisValue minId, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); /// Task StringAppendAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index f45c29886..331d23ea7 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -564,12 +564,24 @@ public Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, Red public Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => Inner.StreamAcknowledgeAsync(ToInner(key), groupName, messageIds, flags); - public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + public Task StreamAcknowledgeAndDeleteAsync(RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue messageId, CommandFlags flags = CommandFlags.None) => + Inner.StreamAcknowledgeAndDeleteAsync(ToInner(key), groupName, mode, messageId, flags); + + public Task StreamAcknowledgeAndDeleteAsync(RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + Inner.StreamAcknowledgeAndDeleteAsync(ToInner(key), groupName, mode, messageIds, flags); + + public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, CommandFlags flags) => Inner.StreamAddAsync(ToInner(key), streamField, streamValue, messageId, maxLength, useApproximateMaxLength, flags); - public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, CommandFlags flags) => Inner.StreamAddAsync(ToInner(key), streamPairs, messageId, maxLength, useApproximateMaxLength, flags); + public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + Inner.StreamAddAsync(ToInner(key), streamField, streamValue, messageId, maxLength, useApproximateMaxLength, limit, mode, flags); + + public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + Inner.StreamAddAsync(ToInner(key), streamPairs, messageId, maxLength, useApproximateMaxLength, limit, mode, flags); + public Task StreamAutoClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => Inner.StreamAutoClaimAsync(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, startAtId, count, flags); @@ -606,6 +618,9 @@ public Task StreamLengthAsync(RedisKey key, CommandFlags flags = CommandFl public Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => Inner.StreamDeleteAsync(ToInner(key), messageIds, flags); + public Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, StreamTrimMode mode, CommandFlags flags = CommandFlags.None) => + Inner.StreamDeleteAsync(ToInner(key), messageIds, mode, flags); + public Task StreamDeleteConsumerAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None) => Inner.StreamDeleteConsumerAsync(ToInner(key), groupName, consumerName, flags); @@ -639,9 +654,15 @@ public Task StreamReadGroupAsync(StreamPosition[] streamPositions public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => Inner.StreamReadGroupAsync(streamPositions, groupName, consumerName, countPerStream, noAck, flags); - public Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + public Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength, CommandFlags flags) => Inner.StreamTrimAsync(ToInner(key), maxLength, useApproximateMaxLength, flags); + public Task StreamTrimAsync(RedisKey key, long maxLength, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + Inner.StreamTrimAsync(ToInner(key), maxLength, useApproximateMaxLength, limit, mode, flags); + + public Task StreamTrimByMinIdAsync(RedisKey key, RedisValue minId, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + Inner.StreamTrimByMinIdAsync(ToInner(key), minId, useApproximateMaxLength, limit, mode, flags); + public Task StringAppendAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => Inner.StringAppendAsync(ToInner(key), value, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index a19dd0b7a..18406ba9f 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -546,12 +546,24 @@ public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue mes public long StreamAcknowledge(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => Inner.StreamAcknowledge(ToInner(key), groupName, messageIds, flags); - public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + public StreamTrimResult StreamAcknowledgeAndDelete(RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue messageId, CommandFlags flags = CommandFlags.None) => + Inner.StreamAcknowledgeAndDelete(ToInner(key), groupName, mode, messageId, flags); + + public StreamTrimResult[] StreamAcknowledgeAndDelete(RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => + Inner.StreamAcknowledgeAndDelete(ToInner(key), groupName, mode, messageIds, flags); + + public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, CommandFlags flags) => Inner.StreamAdd(ToInner(key), streamField, streamValue, messageId, maxLength, useApproximateMaxLength, flags); - public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, CommandFlags flags) => Inner.StreamAdd(ToInner(key), streamPairs, messageId, maxLength, useApproximateMaxLength, flags); + public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + Inner.StreamAdd(ToInner(key), streamField, streamValue, messageId, maxLength, useApproximateMaxLength, limit, mode, flags); + + public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + Inner.StreamAdd(ToInner(key), streamPairs, messageId, maxLength, useApproximateMaxLength, limit, mode, flags); + public StreamAutoClaimResult StreamAutoClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => Inner.StreamAutoClaim(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, startAtId, count, flags); @@ -588,6 +600,9 @@ public long StreamLength(RedisKey key, CommandFlags flags = CommandFlags.None) = public long StreamDelete(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) => Inner.StreamDelete(ToInner(key), messageIds, flags); + public StreamTrimResult[] StreamDelete(RedisKey key, RedisValue[] messageIds, StreamTrimMode mode, CommandFlags flags = CommandFlags.None) => + Inner.StreamDelete(ToInner(key), messageIds, mode, flags); + public long StreamDeleteConsumer(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None) => Inner.StreamDeleteConsumer(ToInner(key), groupName, consumerName, flags); @@ -621,9 +636,15 @@ public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValu public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => Inner.StreamReadGroup(streamPositions, groupName, consumerName, countPerStream, noAck, flags); - public long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) => + public long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength, CommandFlags flags) => Inner.StreamTrim(ToInner(key), maxLength, useApproximateMaxLength, flags); + public long StreamTrim(RedisKey key, long maxLength, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + Inner.StreamTrim(ToInner(key), maxLength, useApproximateMaxLength, limit, mode, flags); + + public long StreamTrimByMinId(RedisKey key, RedisValue minId, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + Inner.StreamTrimByMinId(ToInner(key), minId, useApproximateMaxLength, limit, mode, flags); + public long StringAppend(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) => Inner.StringAppend(ToInner(key), value, flags); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 00ae49025..43b35ba58 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -717,8 +717,12 @@ StackExchange.Redis.IDatabase.SortedSetUpdate(StackExchange.Redis.RedisKey key, StackExchange.Redis.IDatabase.SortedSetUpdate(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.SortedSetWhen when = StackExchange.Redis.SortedSetWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StreamAcknowledge(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue messageId, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StreamAcknowledge(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long -StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue -StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StreamAcknowledgeAndDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.StreamTrimMode mode, StackExchange.Redis.RedisValue messageId, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamTrimResult +StackExchange.Redis.IDatabase.StreamAcknowledgeAndDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.StreamTrimMode mode, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamTrimResult[]! +StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.StreamAutoClaim(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue startAtId, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamAutoClaimResult StackExchange.Redis.IDatabase.StreamAutoClaimIdsOnly(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue startAtId, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamAutoClaimIdsOnlyResult StackExchange.Redis.IDatabase.StreamClaim(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[]! @@ -728,6 +732,7 @@ StackExchange.Redis.IDatabase.StreamConsumerInfo(StackExchange.Redis.RedisKey ke StackExchange.Redis.IDatabase.StreamCreateConsumerGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue? position = null, bool createStream = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.StreamCreateConsumerGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue? position, StackExchange.Redis.CommandFlags flags) -> bool StackExchange.Redis.IDatabase.StreamDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StreamDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.StreamTrimMode mode, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamTrimResult[]! StackExchange.Redis.IDatabase.StreamDeleteConsumer(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StreamDeleteConsumerGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.StreamGroupInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamGroupInfo[]! @@ -742,7 +747,9 @@ StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position, int? count, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.StreamEntry[]! StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisStream[]! StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.RedisStream[]! -StackExchange.Redis.IDatabase.StreamTrim(StackExchange.Redis.RedisKey key, int maxLength, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StreamTrim(StackExchange.Redis.RedisKey key, int maxLength, bool useApproximateMaxLength, StackExchange.Redis.CommandFlags flags) -> long +StackExchange.Redis.IDatabase.StreamTrim(StackExchange.Redis.RedisKey key, long maxLength, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode mode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +StackExchange.Redis.IDatabase.StreamTrimByMinId(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue minId, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode mode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringAppend(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringBitCount(StackExchange.Redis.RedisKey key, long start, long end, StackExchange.Redis.CommandFlags flags) -> long StackExchange.Redis.IDatabase.StringBitCount(StackExchange.Redis.RedisKey key, long start = 0, long end = -1, StackExchange.Redis.StringIndexType indexType = StackExchange.Redis.StringIndexType.Byte, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long @@ -954,8 +961,12 @@ StackExchange.Redis.IDatabaseAsync.SortedSetUpdateAsync(StackExchange.Redis.Redi StackExchange.Redis.IDatabaseAsync.SortedSetUpdateAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.SortedSetEntry[]! values, StackExchange.Redis.SortedSetWhen when = StackExchange.Redis.SortedSetWhen.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue messageId, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAndDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.StreamTrimMode mode, StackExchange.Redis.RedisValue messageId, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamAcknowledgeAndDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.StreamTrimMode mode, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamAutoClaimAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue startAtId, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamAutoClaimIdsOnlyAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue startAtId, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamClaimAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue consumerGroup, StackExchange.Redis.RedisValue claimingConsumer, long minIdleTimeInMs, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -965,6 +976,7 @@ StackExchange.Redis.IDatabaseAsync.StreamConsumerInfoAsync(StackExchange.Redis.R StackExchange.Redis.IDatabaseAsync.StreamCreateConsumerGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue? position = null, bool createStream = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamCreateConsumerGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue? position, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! messageIds, StackExchange.Redis.StreamTrimMode mode, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamDeleteConsumerAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamDeleteConsumerGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamGroupInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -979,7 +991,9 @@ StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.Redi StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position, int? count, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.StreamTrimAsync(StackExchange.Redis.RedisKey key, int maxLength, bool useApproximateMaxLength = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamTrimAsync(StackExchange.Redis.RedisKey key, int maxLength, bool useApproximateMaxLength, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamTrimAsync(StackExchange.Redis.RedisKey key, long maxLength, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode mode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamTrimByMinIdAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue minId, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode mode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringAppendAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringBitCountAsync(StackExchange.Redis.RedisKey key, long start, long end, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringBitCountAsync(StackExchange.Redis.RedisKey key, long start = 0, long end = -1, StackExchange.Redis.StringIndexType indexType = StackExchange.Redis.StringIndexType.Byte, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -1902,4 +1916,12 @@ StackExchange.Redis.ConfigurationOptions.SetUserPfxCertificate(string! userCerti StackExchange.Redis.Bitwise.AndOr = 6 -> StackExchange.Redis.Bitwise StackExchange.Redis.Bitwise.Diff = 4 -> StackExchange.Redis.Bitwise StackExchange.Redis.Bitwise.Diff1 = 5 -> StackExchange.Redis.Bitwise -StackExchange.Redis.Bitwise.One = 7 -> StackExchange.Redis.Bitwise \ No newline at end of file +StackExchange.Redis.Bitwise.One = 7 -> StackExchange.Redis.Bitwise +StackExchange.Redis.StreamTrimMode +StackExchange.Redis.StreamTrimMode.Acknowledged = 2 -> StackExchange.Redis.StreamTrimMode +StackExchange.Redis.StreamTrimMode.DeleteReferences = 1 -> StackExchange.Redis.StreamTrimMode +StackExchange.Redis.StreamTrimMode.KeepReferences = 0 -> StackExchange.Redis.StreamTrimMode +StackExchange.Redis.StreamTrimResult +StackExchange.Redis.StreamTrimResult.Deleted = 1 -> StackExchange.Redis.StreamTrimResult +StackExchange.Redis.StreamTrimResult.NotDeleted = 2 -> StackExchange.Redis.StreamTrimResult +StackExchange.Redis.StreamTrimResult.NotFound = -1 -> StackExchange.Redis.StreamTrimResult \ No newline at end of file diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 716176662..5493f3ebd 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Net; using System.Threading.Tasks; using Pipelines.Sockets.Unofficial.Arenas; @@ -2423,7 +2424,34 @@ public Task StreamAcknowledgeAsync(RedisKey key, RedisValue groupName, Red return ExecuteAsync(msg, ResultProcessor.Int64); } - public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) + public StreamTrimResult StreamAcknowledgeAndDelete(RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue messageId, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamAcknowledgeAndDeleteMessage(key, groupName, mode, messageId, flags); + return ExecuteSync(msg, ResultProcessor.StreamTrimResult); + } + + public Task StreamAcknowledgeAndDeleteAsync(RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue messageId, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamAcknowledgeAndDeleteMessage(key, groupName, mode, messageId, flags); + return ExecuteAsync(msg, ResultProcessor.StreamTrimResult); + } + + public StreamTrimResult[] StreamAcknowledgeAndDelete(RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamAcknowledgeAndDeleteMessage(key, groupName, mode, messageIds, flags); + return ExecuteSync(msg, ResultProcessor.StreamTrimResultArray)!; + } + + public Task StreamAcknowledgeAndDeleteAsync(RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamAcknowledgeAndDeleteMessage(key, groupName, mode, messageIds, flags); + return ExecuteAsync(msg, ResultProcessor.StreamTrimResultArray)!; + } + + public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, CommandFlags flags) + => StreamAdd(key, streamField, streamValue, messageId, maxLength, useApproximateMaxLength, null, StreamTrimMode.KeepReferences, flags); + + public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) { var msg = GetStreamAddMessage( key, @@ -2431,12 +2459,17 @@ public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue str maxLength, useApproximateMaxLength, new NameValueEntry(streamField, streamValue), + limit, + mode, flags); return ExecuteSync(msg, ResultProcessor.RedisValue); } - public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) + public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, CommandFlags flags) + => StreamAddAsync(key, streamField, streamValue, messageId, maxLength, useApproximateMaxLength, null, StreamTrimMode.KeepReferences, flags); + + public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) { var msg = GetStreamAddMessage( key, @@ -2444,12 +2477,17 @@ public Task StreamAddAsync(RedisKey key, RedisValue streamField, Red maxLength, useApproximateMaxLength, new NameValueEntry(streamField, streamValue), + limit, + mode, flags); return ExecuteAsync(msg, ResultProcessor.RedisValue); } - public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) + public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, CommandFlags flags) + => StreamAdd(key, streamPairs, messageId, maxLength, useApproximateMaxLength, null, StreamTrimMode.KeepReferences, flags); + + public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) { var msg = GetStreamAddMessage( key, @@ -2457,12 +2495,17 @@ public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisVal maxLength, useApproximateMaxLength, streamPairs, + limit, + mode, flags); return ExecuteSync(msg, ResultProcessor.RedisValue); } - public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, int? maxLength = null, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) + public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId, int? maxLength, bool useApproximateMaxLength, CommandFlags flags) + => StreamAddAsync(key, streamPairs, messageId, maxLength, useApproximateMaxLength, null, StreamTrimMode.KeepReferences, flags); + + public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) { var msg = GetStreamAddMessage( key, @@ -2470,6 +2513,8 @@ public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPair maxLength, useApproximateMaxLength, streamPairs, + limit, + mode, flags); return ExecuteAsync(msg, ResultProcessor.RedisValue); @@ -2703,28 +2748,50 @@ public Task StreamLengthAsync(RedisKey key, CommandFlags flags = CommandFl public long StreamDelete(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create( - Database, - flags, - RedisCommand.XDEL, - key, - messageIds); - + var msg = Message.Create(Database, flags, RedisCommand.XDEL, key, messageIds); return ExecuteSync(msg, ResultProcessor.Int64); } - public Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) + public StreamTrimResult[] StreamDelete(RedisKey key, RedisValue[] messageIds, StreamTrimMode mode, CommandFlags flags) { - var msg = Message.Create( - Database, - flags, - RedisCommand.XDEL, - key, - messageIds); + var msg = GetStreamDeleteExMessage(key, messageIds, mode, flags); + return ExecuteSync(msg, ResultProcessor.StreamTrimResultArray)!; + } + private Message GetStreamDeleteExMessage(RedisKey key, RedisValue[] messageIds, StreamTrimMode mode, CommandFlags flags) + { + if (messageIds == null) throw new ArgumentNullException(nameof(messageIds)); + if (messageIds.Length == 0) throw new ArgumentOutOfRangeException(nameof(messageIds), "messageIds must contain at least one item."); + + // avoid array for single message case + if (messageIds.Length == 1) + { + return Message.Create(Database, flags, RedisCommand.XDELEX, key, StreamConstants.GetMode(mode), StreamConstants.Ids, 1, messageIds[0]); + } + + var values = new RedisValue[messageIds.Length + 3]; + + var offset = 0; + values[offset++] = StreamConstants.GetMode(mode); + values[offset++] = StreamConstants.Ids; + values[offset++] = messageIds.Length; + messageIds.AsSpan().CopyTo(values.AsSpan(offset)); + Debug.Assert(offset + messageIds.Length == values.Length); + return Message.Create(Database, flags, RedisCommand.XDELEX, key, values); + } + + public Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.XDEL, key, messageIds); return ExecuteAsync(msg, ResultProcessor.Int64); } + public Task StreamDeleteAsync(RedisKey key, RedisValue[] messageIds, StreamTrimMode mode, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamDeleteExMessage(key, messageIds, mode, flags); + return ExecuteAsync(msg, ResultProcessor.StreamTrimResultArray)!; + } + public long StreamDeleteConsumer(RedisKey key, RedisValue groupName, RedisValue consumerName, CommandFlags flags = CommandFlags.None) { var msg = Message.Create( @@ -2995,15 +3062,33 @@ public Task StreamReadGroupAsync(StreamPosition[] streamPositions return ExecuteAsync(msg, ResultProcessor.MultiStream, defaultValue: Array.Empty()); } - public long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) + public long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength, CommandFlags flags) + => StreamTrim(key, maxLength, useApproximateMaxLength, null, StreamTrimMode.KeepReferences, flags); + + public long StreamTrim(RedisKey key, long maxLength, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamTrimMessage(true, key, maxLength, useApproximateMaxLength, limit, mode, flags); + return ExecuteSync(msg, ResultProcessor.Int64); + } + + public Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength, CommandFlags flags) + => StreamTrimAsync(key, maxLength, useApproximateMaxLength, null, StreamTrimMode.KeepReferences, flags); + + public Task StreamTrimAsync(RedisKey key, long maxLength, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamTrimMessage(true, key, maxLength, useApproximateMaxLength, limit, mode, flags); + return ExecuteAsync(msg, ResultProcessor.Int64); + } + + public long StreamTrimByMinId(RedisKey key, RedisValue minId, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) { - var msg = GetStreamTrimMessage(key, maxLength, useApproximateMaxLength, flags); + var msg = GetStreamTrimMessage(false, key, minId, useApproximateMaxLength, limit, mode, flags); return ExecuteSync(msg, ResultProcessor.Int64); } - public Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength = false, CommandFlags flags = CommandFlags.None) + public Task StreamTrimByMinIdAsync(RedisKey key, RedisValue minId, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) { - var msg = GetStreamTrimMessage(key, maxLength, useApproximateMaxLength, flags); + var msg = GetStreamTrimMessage(false, key, minId, useApproximateMaxLength, limit, mode, flags); return ExecuteAsync(msg, ResultProcessor.Int64); } @@ -4109,13 +4194,7 @@ private Message GetSortedSetRemoveRangeByScoreMessage(RedisKey key, double start private Message GetStreamAcknowledgeMessage(RedisKey key, RedisValue groupName, RedisValue messageId, CommandFlags flags) { - var values = new RedisValue[] - { - groupName, - messageId, - }; - - return Message.Create(Database, flags, RedisCommand.XACK, key, values); + return Message.Create(Database, flags, RedisCommand.XACK, key, groupName, messageId); } private Message GetStreamAcknowledgeMessage(RedisKey key, RedisValue groupName, RedisValue[] messageIds, CommandFlags flags) @@ -4124,27 +4203,45 @@ private Message GetStreamAcknowledgeMessage(RedisKey key, RedisValue groupName, if (messageIds.Length == 0) throw new ArgumentOutOfRangeException(nameof(messageIds), "messageIds must contain at least one item."); var values = new RedisValue[messageIds.Length + 1]; + values[0] = groupName; + messageIds.AsSpan().CopyTo(values.AsSpan(1)); - var offset = 0; + return Message.Create(Database, flags, RedisCommand.XACK, key, values); + } - values[offset++] = groupName; + private Message GetStreamAcknowledgeAndDeleteMessage(RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue messageId, CommandFlags flags) + { + return Message.Create(Database, flags, RedisCommand.XACKDEL, key, groupName, StreamConstants.GetMode(mode), StreamConstants.Ids, 1, messageId); + } - for (var i = 0; i < messageIds.Length; i++) - { - values[offset++] = messageIds[i]; - } + private Message GetStreamAcknowledgeAndDeleteMessage(RedisKey key, RedisValue groupName, StreamTrimMode mode, RedisValue[] messageIds, CommandFlags flags) + { + if (messageIds == null) throw new ArgumentNullException(nameof(messageIds)); + if (messageIds.Length == 0) throw new ArgumentOutOfRangeException(nameof(messageIds), "messageIds must contain at least one item."); - return Message.Create(Database, flags, RedisCommand.XACK, key, values); + var values = new RedisValue[messageIds.Length + 4]; + + var offset = 0; + values[offset++] = groupName; + values[offset++] = StreamConstants.GetMode(mode); + values[offset++] = StreamConstants.Ids; + values[offset++] = messageIds.Length; + messageIds.AsSpan().CopyTo(values.AsSpan(offset)); + Debug.Assert(offset + messageIds.Length == values.Length); + + return Message.Create(Database, flags, RedisCommand.XACKDEL, key, values); } - private Message GetStreamAddMessage(RedisKey key, RedisValue messageId, int? maxLength, bool useApproximateMaxLength, NameValueEntry streamPair, CommandFlags flags) + private Message GetStreamAddMessage(RedisKey key, RedisValue messageId, long? maxLength, bool useApproximateMaxLength, NameValueEntry streamPair, long? limit, StreamTrimMode mode, CommandFlags flags) { // Calculate the correct number of arguments: // 3 array elements for Entry ID & NameValueEntry.Name & NameValueEntry.Value. // 2 elements if using MAXLEN (keyword & value), otherwise 0. // 1 element if using Approximate Length (~), otherwise 0. var totalLength = 3 + (maxLength.HasValue ? 2 : 0) - + (maxLength.HasValue && useApproximateMaxLength ? 1 : 0); + + (maxLength.HasValue && useApproximateMaxLength ? 1 : 0) + + (limit.HasValue ? 2 : 0) + + (mode != StreamTrimMode.KeepReferences ? 1 : 0); var values = new RedisValue[totalLength]; var offset = 0; @@ -4156,26 +4253,35 @@ private Message GetStreamAddMessage(RedisKey key, RedisValue messageId, int? max if (useApproximateMaxLength) { values[offset++] = StreamConstants.ApproximateMaxLen; - values[offset++] = maxLength.Value; - } - else - { - values[offset++] = maxLength.Value; } + + values[offset++] = maxLength.Value; + } + + if (limit.HasValue) + { + values[offset++] = RedisLiterals.LIMIT; + values[offset++] = limit.Value; + } + + if (mode != StreamTrimMode.KeepReferences) + { + values[offset++] = StreamConstants.GetMode(mode); } values[offset++] = messageId; values[offset++] = streamPair.Name; - values[offset] = streamPair.Value; + values[offset++] = streamPair.Value; + Debug.Assert(offset == totalLength); return Message.Create(Database, flags, RedisCommand.XADD, key, values); } /// /// Gets message for . /// - private Message GetStreamAddMessage(RedisKey key, RedisValue entryId, int? maxLength, bool useApproximateMaxLength, NameValueEntry[] streamPairs, CommandFlags flags) + private Message GetStreamAddMessage(RedisKey key, RedisValue entryId, long? maxLength, bool useApproximateMaxLength, NameValueEntry[] streamPairs, long? limit, StreamTrimMode mode, CommandFlags flags) { if (streamPairs == null) throw new ArgumentNullException(nameof(streamPairs)); if (streamPairs.Length == 0) throw new ArgumentOutOfRangeException(nameof(streamPairs), "streamPairs must contain at least one item."); @@ -4209,6 +4315,17 @@ private Message GetStreamAddMessage(RedisKey key, RedisValue entryId, int? maxLe values[offset++] = maxLength.Value; } + if (limit.HasValue) + { + values[offset++] = RedisLiterals.LIMIT; + values[offset++] = limit.Value; + } + + if (mode != StreamTrimMode.KeepReferences) + { + values[offset++] = StreamConstants.GetMode(mode); + } + values[offset++] = entryId; for (var i = 0; i < streamPairs.Length; i++) @@ -4217,6 +4334,7 @@ private Message GetStreamAddMessage(RedisKey key, RedisValue entryId, int? maxLe values[offset++] = streamPairs[i].Value; } + Debug.Assert(offset == totalLength); return Message.Create(Database, flags, RedisCommand.XADD, key, values); } @@ -4465,27 +4583,45 @@ protected override void WriteImpl(PhysicalConnection physical) public override int ArgCount => argCount; } - private Message GetStreamTrimMessage(RedisKey key, int maxLength, bool useApproximateMaxLength, CommandFlags flags) + private Message GetStreamTrimMessage(bool maxLen, RedisKey key, RedisValue threshold, bool useApproximateMaxLength, long? limit, StreamTrimMode mode, CommandFlags flags) { - if (maxLength < 0) + if (limit.HasValue && limit.GetValueOrDefault() <= 0) { - throw new ArgumentOutOfRangeException(nameof(maxLength), "maxLength must be equal to or greater than 0."); + throw new ArgumentOutOfRangeException(nameof(limit), "limit must be greater than 0 when specified."); } - var values = new RedisValue[2 + (useApproximateMaxLength ? 1 : 0)]; + if (limit is null && !useApproximateMaxLength && mode == StreamTrimMode.KeepReferences) + { + // avoid array alloc in simple case + return Message.Create(Database, flags, RedisCommand.XTRIM, key, maxLen ? StreamConstants.MaxLen : StreamConstants.MinId, threshold); + } - values[0] = StreamConstants.MaxLen; + var values = new RedisValue[2 + (useApproximateMaxLength ? 1 : 0) + (limit.HasValue ? 2 : 0) + (mode == StreamTrimMode.KeepReferences ? 0 : 1)]; + + var offset = 0; + + values[offset++] = maxLen ? StreamConstants.MaxLen : StreamConstants.MinId; if (useApproximateMaxLength) { - values[1] = StreamConstants.ApproximateMaxLen; - values[2] = maxLength; + values[offset++] = StreamConstants.ApproximateMaxLen; } - else + + values[offset++] = threshold; + + if (limit.HasValue) + { + values[offset++] = RedisLiterals.LIMIT; + values[offset++] = limit.GetValueOrDefault(); + } + + if (mode != StreamTrimMode.KeepReferences) // omit when not needed, for back-compat { - values[1] = maxLength; + values[offset++] = StreamConstants.GetMode(mode); } + Debug.Assert(offset == values.Length); + return Message.Create( Database, flags, diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 08f9a20ee..294d1f03b 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Net; +using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; @@ -1377,6 +1378,77 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + internal static ResultProcessor StreamTrimResult => + Int32EnumProcessor.Instance; + + internal static ResultProcessor StreamTrimResultArray => + Int32EnumArrayProcessor.Instance; + + private class Int32EnumProcessor : ResultProcessor where T : unmanaged, Enum + { + private Int32EnumProcessor() { } + public static readonly Int32EnumProcessor Instance = new(); + + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Resp2TypeBulkString) + { + case ResultType.Integer: + case ResultType.SimpleString: + case ResultType.BulkString: + if (result.TryGetInt64(out long i64)) + { + Debug.Assert(Unsafe.SizeOf() == sizeof(int)); + int i32 = (int)i64; + SetResult(message, Unsafe.As(ref i32)); + return true; + } + break; + case ResultType.Array when result.ItemsCount == 1: // pick a single element from a unit vector + if (result.GetItems()[0].TryGetInt64(out i64)) + { + Debug.Assert(Unsafe.SizeOf() == sizeof(int)); + int i32 = (int)i64; + SetResult(message, Unsafe.As(ref i32)); + return true; + } + break; + } + return false; + } + } + + private class Int32EnumArrayProcessor : ResultProcessor where T : unmanaged, Enum + { + private Int32EnumArrayProcessor() { } + public static readonly Int32EnumArrayProcessor Instance = new(); + + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Resp2TypeArray) + { + case ResultType.Array: + T[] arr; + if (result.IsNull) + { + arr = null!; + } + else + { + Debug.Assert(Unsafe.SizeOf() == sizeof(int)); + arr = result.ToArray(static (in RawResult x) => + { + int i32 = (int)x.AsRedisValue(); + return Unsafe.As(ref i32); + })!; + } + SetResult(message, arr); + return true; + } + return false; + } + } + private class PubSubNumSubProcessor : Int64Processor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) diff --git a/src/StackExchange.Redis/StreamConstants.cs b/src/StackExchange.Redis/StreamConstants.cs index 74650e010..929398e4b 100644 --- a/src/StackExchange.Redis/StreamConstants.cs +++ b/src/StackExchange.Redis/StreamConstants.cs @@ -1,4 +1,6 @@ -namespace StackExchange.Redis +using System; + +namespace StackExchange.Redis { /// /// Constants representing values used in Redis Stream commands. @@ -59,6 +61,7 @@ internal static class StreamConstants internal static readonly RedisValue SetId = "SETID"; internal static readonly RedisValue MaxLen = "MAXLEN"; + internal static readonly RedisValue MinId = "MINID"; internal static readonly RedisValue MkStream = "MKSTREAM"; @@ -67,5 +70,17 @@ internal static class StreamConstants internal static readonly RedisValue Stream = "STREAM"; internal static readonly RedisValue Streams = "STREAMS"; + + private static readonly RedisValue KeepRef = "KEEPREF", DelRef = "DELREF", Acked = "ACKED"; + + internal static readonly RedisValue Ids = "IDS"; + + internal static RedisValue GetMode(StreamTrimMode mode) => mode switch + { + StreamTrimMode.KeepReferences => KeepRef, + StreamTrimMode.DeleteReferences => DelRef, + StreamTrimMode.Acknowledged => Acked, + _ => throw new ArgumentOutOfRangeException(nameof(mode)), + }; } } diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs index 3c56f7605..612ca182b 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs @@ -1202,6 +1202,27 @@ public void StreamTrim() mock.Received().StreamTrim("prefix:key", 1000, true, CommandFlags.None); } + [Fact] + public void StreamTrimByMinId() + { + prefixed.StreamTrimByMinId("key", 1111111111); + mock.Received().StreamTrimByMinId("prefix:key", 1111111111); + } + + [Fact] + public void StreamTrimByMinIdWithApproximate() + { + prefixed.StreamTrimByMinId("key", 1111111111, useApproximateMaxLength: true); + mock.Received().StreamTrimByMinId("prefix:key", 1111111111, useApproximateMaxLength: true); + } + + [Fact] + public void StreamTrimByMinIdWithApproximateAndLimit() + { + prefixed.StreamTrimByMinId("key", 1111111111, useApproximateMaxLength: true, limit: 100); + mock.Received().StreamTrimByMinId("prefix:key", 1111111111, useApproximateMaxLength: true, limit: 100); + } + [Fact] public void StringAppend() { diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs index 70893e510..b8cf9a4b9 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs @@ -1118,6 +1118,27 @@ public async Task StreamTrimAsync() await mock.Received().StreamTrimAsync("prefix:key", 1000, true, CommandFlags.None); } + [Fact] + public async Task StreamTrimByMinIdAsync() + { + await prefixed.StreamTrimByMinIdAsync("key", 1111111111); + await mock.Received().StreamTrimByMinIdAsync("prefix:key", 1111111111); + } + + [Fact] + public async Task StreamTrimByMinIdAsyncWithApproximate() + { + await prefixed.StreamTrimByMinIdAsync("key", 1111111111, useApproximateMaxLength: true); + await mock.Received().StreamTrimByMinIdAsync("prefix:key", 1111111111, useApproximateMaxLength: true); + } + + [Fact] + public async Task StreamTrimByMinIdAsyncWithApproximateAndLimit() + { + await prefixed.StreamTrimByMinIdAsync("key", 1111111111, useApproximateMaxLength: true, limit: 100); + await mock.Received().StreamTrimByMinIdAsync("prefix:key", 1111111111, useApproximateMaxLength: true, limit: 100); + } + [Fact] public async Task StringAppendAsync() { diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index 05591db27..83d9fe8f8 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -698,31 +698,85 @@ public async Task StreamConsumerGroupAcknowledgeMessage() var id1 = db.StreamAdd(key, "field1", "value1"); var id2 = db.StreamAdd(key, "field2", "value2"); var id3 = db.StreamAdd(key, "field3", "value3"); + RedisValue notexist = "0-0"; var id4 = db.StreamAdd(key, "field4", "value4"); db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); // Read all 4 messages, they will be assigned to the consumer var entries = db.StreamReadGroup(key, groupName, consumer, StreamPosition.NewMessages); + Assert.Equal(4, entries.Length); // Send XACK for 3 of the messages // Single message Id overload. var oneAck = db.StreamAcknowledge(key, groupName, id1); + Assert.Equal(1, oneAck); + + var nack = db.StreamAcknowledge(key, groupName, notexist); + Assert.Equal(0, nack); // Multiple message Id overload. - var twoAck = db.StreamAcknowledge(key, groupName, [id3, id4]); + var twoAck = db.StreamAcknowledge(key, groupName, [id3, notexist, id4]); // Read the group again, it should only return the unacknowledged message. var notAcknowledged = db.StreamReadGroup(key, groupName, consumer, "0-0"); - Assert.Equal(4, entries.Length); - Assert.Equal(1, oneAck); Assert.Equal(2, twoAck); Assert.Single(notAcknowledged); Assert.Equal(id2, notAcknowledged[0].Id); } + [Theory] + [InlineData(StreamTrimMode.KeepReferences)] + [InlineData(StreamTrimMode.DeleteReferences)] + [InlineData(StreamTrimMode.Acknowledged)] + public void StreamConsumerGroupAcknowledgeAndDeleteMessage(StreamTrimMode mode) + { + using var conn = Create(require: RedisFeatures.v8_2_0_rc1); + + var db = conn.GetDatabase(); + var key = Me() + ":" + mode; + const string groupName = "test_group", + consumer = "test_consumer"; + + var id1 = db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); + var id3 = db.StreamAdd(key, "field3", "value3"); + RedisValue notexist = "0-0"; + var id4 = db.StreamAdd(key, "field4", "value4"); + + db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); + + // Read all 4 messages, they will be assigned to the consumer + var entries = db.StreamReadGroup(key, groupName, consumer, StreamPosition.NewMessages); + Assert.Equal(4, entries.Length); + + // Send XACK for 3 of the messages + + // Single message Id overload. + var oneAck = db.StreamAcknowledgeAndDelete(key, groupName, mode, id1); + Assert.Equal(StreamTrimResult.Deleted, oneAck); + + StreamTrimResult nack = db.StreamAcknowledgeAndDelete(key, groupName, mode, notexist); + Assert.Equal(StreamTrimResult.NotFound, nack); + + // Multiple message Id overload. + RedisValue[] ids = new[] { id3, notexist, id4 }; + var twoAck = db.StreamAcknowledgeAndDelete(key, groupName, mode, ids); + + // Read the group again, it should only return the unacknowledged message. + var notAcknowledged = db.StreamReadGroup(key, groupName, consumer, "0-0"); + + Assert.Equal(3, twoAck.Length); + Assert.Equal(StreamTrimResult.Deleted, twoAck[0]); + Assert.Equal(StreamTrimResult.NotFound, twoAck[1]); + Assert.Equal(StreamTrimResult.Deleted, twoAck[2]); + + Assert.Single(notAcknowledged); + Assert.Equal(id2, notAcknowledged[0].Id); + } + [Fact] public async Task StreamConsumerGroupClaimMessages() { @@ -1229,6 +1283,54 @@ public async Task StreamDeleteMessages() Assert.Equal(2, messages.Length); } + [Theory] + [InlineData(StreamTrimMode.KeepReferences)] + [InlineData(StreamTrimMode.DeleteReferences)] + [InlineData(StreamTrimMode.Acknowledged)] + public void StreamDeleteExMessage(StreamTrimMode mode) + { + using var conn = Create(require: RedisFeatures.v8_2_0_rc1); // XDELEX + + var db = conn.GetDatabase(); + var key = Me() + ":" + mode; + + db.StreamAdd(key, "field1", "value1"); + db.StreamAdd(key, "field2", "value2"); + var id3 = db.StreamAdd(key, "field3", "value3"); + db.StreamAdd(key, "field4", "value4"); + + var deleted = db.StreamDelete(key, new[] { id3 }, mode: mode); + var messages = db.StreamRange(key); + + Assert.Equal(StreamTrimResult.Deleted, Assert.Single(deleted)); + Assert.Equal(3, messages.Length); + } + + [Theory] + [InlineData(StreamTrimMode.KeepReferences)] + [InlineData(StreamTrimMode.DeleteReferences)] + [InlineData(StreamTrimMode.Acknowledged)] + public void StreamDeleteExMessages(StreamTrimMode mode) + { + using var conn = Create(require: RedisFeatures.v8_2_0_rc1); // XDELEX + + var db = conn.GetDatabase(); + var key = Me() + ":" + mode; + + db.StreamAdd(key, "field1", "value1"); + var id2 = db.StreamAdd(key, "field2", "value2"); + var id3 = db.StreamAdd(key, "field3", "value3"); + db.StreamAdd(key, "field4", "value4"); + + var deleted = db.StreamDelete(key, new[] { id2, id3 }, mode: mode); + var messages = db.StreamRange(key); + + Assert.Equal(2, deleted.Length); + Assert.Equal(StreamTrimResult.Deleted, deleted[0]); + Assert.Equal(StreamTrimResult.Deleted, deleted[1]); + Assert.Equal(2, messages.Length); + } + [Fact] public async Task StreamGroupInfoGet() { @@ -1891,6 +1993,8 @@ public async Task StreamReadWithAfterIdAndCount_2() Assert.Equal(id3, entries[1].Id); } + protected override string GetConfiguration() => "127.0.0.1:6379"; + [Fact] public async Task StreamTrimLength() { @@ -1912,6 +2016,70 @@ public async Task StreamTrimLength() Assert.Equal(1, len); } + private static Version ForMode(StreamTrimMode mode, Version? defaultVersion = null) => mode switch + { + StreamTrimMode.KeepReferences => defaultVersion ?? RedisFeatures.v5_0_0, + StreamTrimMode.Acknowledged => RedisFeatures.v8_2_0_rc1, + StreamTrimMode.DeleteReferences => RedisFeatures.v8_2_0_rc1, + _ => throw new ArgumentOutOfRangeException(nameof(mode)), + }; + + [Theory] + [InlineData(StreamTrimMode.KeepReferences)] + [InlineData(StreamTrimMode.DeleteReferences)] + [InlineData(StreamTrimMode.Acknowledged)] + public void StreamTrimByMinId(StreamTrimMode mode) + { + using var conn = Create(require: ForMode(mode, RedisFeatures.v6_2_0)); + + var db = conn.GetDatabase(); + var key = Me() + ":" + mode; + + // Add a couple items and check length. + db.StreamAdd(key, "field1", "value1", 1111111110); + db.StreamAdd(key, "field2", "value2", 1111111111); + db.StreamAdd(key, "field3", "value3", 1111111112); + + var numRemoved = db.StreamTrimByMinId(key, 1111111111, mode: mode); + var len = db.StreamLength(key); + + Assert.Equal(1, numRemoved); + Assert.Equal(2, len); + } + + [Theory] + [InlineData(StreamTrimMode.KeepReferences)] + [InlineData(StreamTrimMode.DeleteReferences)] + [InlineData(StreamTrimMode.Acknowledged)] + public void StreamTrimByMinIdWithApproximateAndLimit(StreamTrimMode mode) + { + using var conn = Create(require: ForMode(mode, RedisFeatures.v6_2_0)); + + var db = conn.GetDatabase(); + var key = Me() + ":" + mode; + + const int maxLength = 1000; + const int limit = 100; + + for (var i = 0; i < maxLength; i++) + { + db.StreamAdd(key, $"field", $"value", 1111111110 + i); + } + + var numRemoved = db.StreamTrimByMinId(key, 1111111110 + maxLength, useApproximateMaxLength: true, limit: limit, mode: mode); + var expectRemoved = mode switch + { + StreamTrimMode.KeepReferences => limit, + StreamTrimMode.DeleteReferences => 0, + StreamTrimMode.Acknowledged => 0, + _ => throw new ArgumentOutOfRangeException(nameof(mode)), + }; + var len = db.StreamLength(key); + + Assert.Equal(expectRemoved, numRemoved); + Assert.Equal(maxLength - expectRemoved, len); + } + [Fact] public async Task StreamVerifyLength() { @@ -1939,14 +2107,17 @@ public async Task AddWithApproxCountAsync() await db.StreamAddAsync(key, "field", "value", maxLength: 10, useApproximateMaxLength: true, flags: CommandFlags.None).ConfigureAwait(false); } - [Fact] - public async Task AddWithApproxCount() + [Theory] + [InlineData(StreamTrimMode.KeepReferences)] + [InlineData(StreamTrimMode.DeleteReferences)] + [InlineData(StreamTrimMode.Acknowledged)] + public async Task AddWithApproxCount(StreamTrimMode mode) { - await using var conn = Create(require: RedisFeatures.v5_0_0); + await using var conn = Create(require: ForMode(mode)); var db = conn.GetDatabase(); - var key = Me(); - db.StreamAdd(key, "field", "value", maxLength: 10, useApproximateMaxLength: true, flags: CommandFlags.None); + var key = Me() + ":" + mode; + db.StreamAdd(key, "field", "value", maxLength: 10, useApproximateMaxLength: true, trimMode: mode, flags: CommandFlags.None); } [Fact] From b4aaced4ab19eb8279ed10c93eadfb03b9bf5894 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Mon, 21 Jul 2025 12:06:56 +0300 Subject: [PATCH 342/435] Replaces inline logging with source-generated LoggerMessage (#2903) Adopts source-generated LoggerMessage for all logging calls, replacing direct string interpolation and formatting in log messages. Introduces strongly-typed helper structs for efficient log value formatting and factors out complex ToString() calls. Improves logging performance, ensures consistency across log entries, and reduces unnecessary allocations by deferring message formatting until required. Lays groundwork for easier log message maintenance and better structured logging. --- .../ConnectionMultiplexer.cs | 524 +++++++++++++++--- 1 file changed, 454 insertions(+), 70 deletions(-) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 7ac25e42b..627daaabf 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -170,10 +170,10 @@ private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions conf lock (log) // Keep the outer and any inner errors contiguous { var ex = a.Exception; - log?.LogError(ex, $"Connection failed: {Format.ToString(a.EndPoint)} ({a.ConnectionType}, {a.FailureType}): {ex?.Message ?? "(unknown)"}"); + LogErrorConnectionFailed(log, ex, new(a.EndPoint), a.ConnectionType, a.FailureType, ex?.Message ?? "(unknown)"); while ((ex = ex?.InnerException) != null) { - log?.LogError(ex, $"> {ex.Message}"); + LogErrorInnerException(log, ex, ex.Message); } } } @@ -219,14 +219,14 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt const CommandFlags flags = CommandFlags.NoRedirect; Message msg; - log?.LogInformation($"Checking {Format.ToString(srv.EndPoint)} is available..."); + LogInformationCheckingServerAvailable(log, new(srv.EndPoint)); try { await srv.PingAsync(flags).ForAwait(); // if it isn't happy, we're not happy } catch (Exception ex) { - log?.LogError(ex, $"Operation failed on {Format.ToString(srv.EndPoint)}, aborting: {ex.Message}"); + LogErrorOperationFailedOnServer(log, ex, new(srv.EndPoint), ex.Message); throw; } @@ -241,7 +241,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt foreach (var node in nodes) { if (!node.IsConnected || node.IsReplica) continue; - log?.LogInformation($"Attempting to set tie-breaker on {Format.ToString(node.EndPoint)}..."); + LogInformationAttemptingToSetTieBreaker(log, new(node.EndPoint)); msg = Message.Create(0, flags | CommandFlags.FireAndForget, RedisCommand.SET, tieBreakerKey, newPrimary); try { @@ -252,21 +252,21 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt } // stop replicating, promote to a standalone primary - log?.LogInformation($"Making {Format.ToString(srv.EndPoint)} a primary..."); + LogInformationMakingServerPrimary(log, new(srv.EndPoint)); try { await srv.ReplicaOfAsync(null, flags).ForAwait(); } catch (Exception ex) { - log?.LogError(ex, $"Operation failed on {Format.ToString(srv.EndPoint)}, aborting: {ex.Message}"); + LogErrorOperationFailedOnServer(log, ex, new(srv.EndPoint), ex.Message); throw; } // also, in case it was a replica a moment ago, and hasn't got the tie-breaker yet, we re-send the tie-breaker to this one if (!tieBreakerKey.IsNull && !server.IsReplica) { - log?.LogInformation($"Resending tie-breaker to {Format.ToString(server.EndPoint)}..."); + LogInformationResendingTieBreaker(log, new(server.EndPoint)); msg = Message.Create(0, flags | CommandFlags.FireAndForget, RedisCommand.SET, tieBreakerKey, newPrimary); try { @@ -297,7 +297,7 @@ async Task BroadcastAsync(ServerSnapshot serverNodes) foreach (var node in serverNodes) { if (!node.IsConnected) continue; - log?.LogInformation($"Broadcasting via {Format.ToString(node.EndPoint)}..."); + LogInformationBroadcastingViaNode(log, new(node.EndPoint)); msg = Message.Create(-1, flags | CommandFlags.FireAndForget, RedisCommand.PUBLISH, channel, newPrimary); await node.WriteDirectAsync(msg, ResultProcessor.Int64).ForAwait(); } @@ -313,7 +313,7 @@ async Task BroadcastAsync(ServerSnapshot serverNodes) { if (node == server || node.ServerType != ServerType.Standalone) continue; - log?.LogInformation($"Replicating to {Format.ToString(node.EndPoint)}..."); + LogInformationReplicatingToNode(log, new(node.EndPoint)); msg = RedisServer.CreateReplicaOfMessage(node, server.EndPoint, flags); await node.WriteDirectAsync(msg, ResultProcessor.DemandOK).ForAwait(); } @@ -325,7 +325,7 @@ async Task BroadcastAsync(ServerSnapshot serverNodes) await BroadcastAsync(nodes).ForAwait(); // and reconfigure the muxer - log?.LogInformation("Reconfiguring all endpoints..."); + LogInformationReconfiguringAllEndpoints(log); // Yes, there is a tiny latency race possible between this code and the next call, but it's far more minute than before. // The effective gap between 0 and > 0 (likely off-box) latency is something that may never get hit here by anyone. if (blockingReconfig) @@ -334,7 +334,7 @@ async Task BroadcastAsync(ServerSnapshot serverNodes) } if (!await ReconfigureAsync(first: false, reconfigureAll: true, log, srv.EndPoint, cause: nameof(MakePrimaryAsync)).ForAwait()) { - log?.LogInformation("Verifying the configuration was incomplete; please verify"); + LogInformationVerifyingConfigurationIncomplete(log); } } @@ -474,18 +474,23 @@ private static async Task WaitAllIgnoreErrorsAsync(string name, Task[] tas _ = tasks ?? throw new ArgumentNullException(nameof(tasks)); if (tasks.Length == 0) { - log?.LogInformation("No tasks to await"); + LogInformationNoTasksToAwait(log); return true; } if (AllComplete(tasks)) { - log?.LogInformation("All tasks are already complete"); + LogInformationAllTasksComplete(log); return true; } - static void LogWithThreadPoolStats(ILogger? log, string message, out int busyWorkerCount) + static void LogWithThreadPoolStats(ILogger? log, string message) { - busyWorkerCount = 0; + if (log?.IsEnabled(LogLevel.Information) != false) + { + return; + } + + var busyWorkerCount = 0; if (log is not null) { var sb = new StringBuilder(); @@ -496,25 +501,25 @@ static void LogWithThreadPoolStats(ILogger? log, string message, out int busyWor { sb.Append(", POOL: ").Append(workItems); } - log?.LogInformation(sb.ToString()); + LogInformationThreadPoolStats(log, sb.ToString()); } } var watch = ValueStopwatch.StartNew(); - LogWithThreadPoolStats(log, $"Awaiting {tasks.Length} {name} task completion(s) for {timeoutMilliseconds}ms", out _); + LogWithThreadPoolStats(log, $"Awaiting {tasks.Length} {name} task completion(s) for {timeoutMilliseconds}ms"); try { // if none error, great var remaining = timeoutMilliseconds - watch.ElapsedMilliseconds; if (remaining <= 0) { - LogWithThreadPoolStats(log, "Timeout before awaiting for tasks", out _); + LogWithThreadPoolStats(log, "Timeout before awaiting for tasks"); return false; } var allTasks = Task.WhenAll(tasks).ObserveErrors(); bool all = await allTasks.TimeoutAfter(timeoutMs: remaining).ObserveErrors().ForAwait(); - LogWithThreadPoolStats(log, all ? $"All {tasks.Length} {name} tasks completed cleanly" : $"Not all {name} tasks completed cleanly (from {caller}#{callerLineNumber}, timeout {timeoutMilliseconds}ms)", out _); + LogWithThreadPoolStats(log, all ? $"All {tasks.Length} {name} tasks completed cleanly" : $"Not all {name} tasks completed cleanly (from {caller}#{callerLineNumber}, timeout {timeoutMilliseconds}ms)"); return all; } catch @@ -530,7 +535,7 @@ static void LogWithThreadPoolStats(ILogger? log, string message, out int busyWor var remaining = timeoutMilliseconds - watch.ElapsedMilliseconds; if (remaining <= 0) { - LogWithThreadPoolStats(log, "Timeout awaiting tasks", out _); + LogWithThreadPoolStats(log, "Timeout awaiting tasks"); return false; } try @@ -541,7 +546,7 @@ static void LogWithThreadPoolStats(ILogger? log, string message, out int busyWor { } } } - LogWithThreadPoolStats(log, "Finished awaiting tasks", out _); + LogWithThreadPoolStats(log, "Finished awaiting tasks"); return false; } @@ -602,7 +607,7 @@ private static async Task ConnectImplAsync(ConfigurationO try { var sw = ValueStopwatch.StartNew(); - log?.LogInformation($"Connecting (async) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); + LogInformationConnectingAsync(log, RuntimeInformation.FrameworkDescription, Utils.GetLibVersion()); muxer = CreateMultiplexer(configuration, log, serverType, out connectHandler); killMe = muxer; @@ -621,9 +626,9 @@ private static async Task ConnectImplAsync(ConfigurationO muxer.InitializeSentinel(log); } - await configuration.AfterConnectAsync(muxer, s => log?.LogInformation(s)).ForAwait(); + await configuration.AfterConnectAsync(muxer, s => LogInformationAfterConnect(log, s)).ForAwait(); - log?.LogInformation($"Total connect time: {sw.ElapsedMilliseconds:n0} ms"); + LogInformationTotalConnectTime(log, sw.ElapsedMilliseconds); return muxer; } @@ -690,7 +695,7 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat try { var sw = ValueStopwatch.StartNew(); - log?.LogInformation($"Connecting (sync) on {RuntimeInformation.FrameworkDescription} (StackExchange.Redis: v{Utils.GetLibVersion()})"); + LogInformationConnectingSync(log, RuntimeInformation.FrameworkDescription, Utils.GetLibVersion()); muxer = CreateMultiplexer(configuration, log, serverType, out connectHandler, endpoints); killMe = muxer; @@ -709,7 +714,7 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat { var ex = ExceptionFactory.UnableToConnect(muxer, "ConnectTimeout"); muxer.LastException = ex; - muxer.Logger?.LogError(ex, ex.Message); + LogErrorSyncConnectTimeout(muxer.Logger, ex, ex.Message); } } @@ -723,9 +728,9 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat muxer.InitializeSentinel(log); } - configuration.AfterConnectAsync(muxer, s => log?.LogInformation(s)).Wait(muxer.SyncConnectTimeout(true)); + configuration.AfterConnectAsync(muxer, s => LogInformationAfterConnect(log, s)).Wait(muxer.SyncConnectTimeout(true)); - log?.LogInformation($"Total connect time: {sw.ElapsedMilliseconds:n0} ms"); + LogInformationTotalConnectTime(log, sw.ElapsedMilliseconds); return muxer; } @@ -1408,14 +1413,14 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog if (!ranThisCall) { - log?.LogInformation($"Reconfiguration was already in progress due to: {activeConfigCause}, attempted to run for: {cause}"); + LogInformationReconfigurationInProgress(log, activeConfigCause, cause); return false; } Trace("Starting reconfiguration..."); Trace(blame != null, "Blaming: " + Format.ToString(blame)); Interlocked.Exchange(ref lastReconfigiureTicks, Environment.TickCount); - log?.LogInformation(RawConfig.ToString(includePassword: false)); + LogInformationConfiguration(log, new(RawConfig)); if (first) { @@ -1445,7 +1450,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog int standaloneCount = 0, clusterCount = 0, sentinelCount = 0; var endpoints = EndPoints; bool useTieBreakers = RawConfig.TryGetTieBreaker(out var tieBreakerKey); - log?.LogInformation($"{endpoints.Count} unique nodes specified ({(useTieBreakers ? "with" : "without")} tiebreaker)"); + LogInformationUniqueNodesSpecified(log, endpoints.Count, useTieBreakers ? "with" : "without"); if (endpoints.Count == 0) { @@ -1487,7 +1492,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog watch ??= ValueStopwatch.StartNew(); var remaining = RawConfig.ConnectTimeout - watch.Value.ElapsedMilliseconds; - log?.LogInformation($"Allowing {available.Length} endpoint(s) {TimeSpan.FromMilliseconds(remaining)} to respond..."); + LogInformationAllowingEndpointsToRespond(log, available.Length, TimeSpan.FromMilliseconds(remaining)); Trace("Allowing endpoints " + TimeSpan.FromMilliseconds(remaining) + " to respond..."); var allConnected = await WaitAllIgnoreErrorsAsync("available", available, remaining, log).ForAwait(); @@ -1500,18 +1505,18 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog var task = available[i]; var bs = server.GetBridgeStatus(ConnectionType.Interactive); - log?.LogInformation($" Server[{i}] ({Format.ToString(server)}) Status: {task.Status} (inst: {bs.MessagesSinceLastHeartbeat}, qs: {bs.Connection.MessagesSentAwaitingResponse}, in: {bs.Connection.BytesAvailableOnSocket}, qu: {bs.MessagesSinceLastHeartbeat}, aw: {bs.IsWriterActive}, in-pipe: {bs.Connection.BytesInReadPipe}, out-pipe: {bs.Connection.BytesInWritePipe}, bw: {bs.BacklogStatus}, rs: {bs.Connection.ReadStatus}. ws: {bs.Connection.WriteStatus})"); + LogInformationServerStatus(log, i, new(server), task.Status, bs.MessagesSinceLastHeartbeat, bs.Connection.MessagesSentAwaitingResponse, bs.Connection.BytesAvailableOnSocket, bs.MessagesSinceLastHeartbeat, bs.IsWriterActive, bs.Connection.BytesInReadPipe, bs.Connection.BytesInWritePipe, bs.BacklogStatus, bs.Connection.ReadStatus, bs.Connection.WriteStatus); } } - log?.LogInformation("Endpoint summary:"); + LogInformationEndpointSummary(log); // Log current state after await foreach (var server in servers) { - log?.LogInformation($" {Format.ToString(server.EndPoint)}: Endpoint is (Interactive: {server.InteractiveConnectionState}, Subscription: {server.SubscriptionConnectionState})"); + LogInformationEndpointState(log, new(server.EndPoint), server.InteractiveConnectionState, server.SubscriptionConnectionState); } - log?.LogInformation("Task summary:"); + LogInformationTaskSummary(log); EndPointCollection? updatedClusterEndpointCollection = null; for (int i = 0; i < available.Length; i++) { @@ -1524,21 +1529,21 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog var aex = task.Exception!; foreach (var ex in aex.InnerExceptions) { - log?.LogError(ex, $" {Format.ToString(server)}: Faulted: {ex.Message}"); + LogErrorServerFaulted(log, ex, new(server), ex.Message); failureMessage = ex.Message; } } else if (task.IsCanceled) { server.SetUnselectable(UnselectableFlags.DidNotRespond); - log?.LogInformation($" {Format.ToString(server)}: Connect task canceled"); + LogInformationConnectTaskCanceled(log, new(server)); } else if (task.IsCompleted) { if (task.Result != "Disconnected") { server.ClearUnselectable(UnselectableFlags.DidNotRespond); - log?.LogInformation($" {Format.ToString(server)}: Returned with success as {server.ServerType} {(server.IsReplica ? "replica" : "primary")} (Source: {task.Result})"); + LogInformationServerReturnedSuccess(log, new(server), server.ServerType, server.IsReplica ? "replica" : "primary", task.Result); // Count the server types switch (server.ServerType) @@ -1591,13 +1596,13 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog else { server.SetUnselectable(UnselectableFlags.DidNotRespond); - log?.LogInformation($" {Format.ToString(server)}: Returned, but incorrectly"); + LogInformationServerReturnedIncorrectly(log, new(server)); } } else { server.SetUnselectable(UnselectableFlags.DidNotRespond); - log?.LogInformation($" {Format.ToString(server)}: Did not respond (Task.Status: {task.Status})"); + LogInformationServerDidNotRespond(log, new(server), task.Status); } } @@ -1641,12 +1646,11 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog { if (primary == preferred || primary.IsReplica) { - log?.LogInformation($"{Format.ToString(primary)}: Clearing as RedundantPrimary"); - primary.ClearUnselectable(UnselectableFlags.RedundantPrimary); + LogInformationClearingAsRedundantPrimary(log, new(primary)); } else { - log?.LogInformation($"{Format.ToString(primary)}: Setting as RedundantPrimary"); + LogInformationSettingAsRedundantPrimary(log, new(primary)); primary.SetUnselectable(UnselectableFlags.RedundantPrimary); } } @@ -1656,7 +1660,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog { ServerSelectionStrategy.ServerType = ServerType.Cluster; long coveredSlots = ServerSelectionStrategy.CountCoveredSlots(); - log?.LogInformation($"Cluster: {coveredSlots} of {ServerSelectionStrategy.TotalSlots} slots covered"); + LogInformationClusterSlotsCovered(log, coveredSlots, ServerSelectionStrategy.TotalSlots); } if (!first) { @@ -1664,11 +1668,11 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog long subscriptionChanges = EnsureSubscriptions(CommandFlags.FireAndForget); if (subscriptionChanges == 0) { - log?.LogInformation("No subscription changes necessary"); + LogInformationNoSubscriptionChanges(log); } else { - log?.LogInformation($"Subscriptions attempting reconnect: {subscriptionChanges}"); + LogInformationSubscriptionsAttemptingReconnect(log, subscriptionChanges); } } if (showStats) @@ -1679,14 +1683,14 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog string? stormLog = GetStormLog(); if (!string.IsNullOrWhiteSpace(stormLog)) { - log?.LogInformation(stormLog); + LogInformationStormLog(log, stormLog!); } healthy = standaloneCount != 0 || clusterCount != 0 || sentinelCount != 0; if (first && !healthy && attemptsLeft > 0) { - log?.LogInformation("Resetting failing connections to retry..."); + LogInformationResettingFailingConnections(log); ResetAllNonConnected(); - log?.LogInformation($" Retrying - attempts left: {attemptsLeft}..."); + LogInformationRetryingAttempts(log, attemptsLeft); } // WTF("?: " + attempts); } @@ -1698,14 +1702,14 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog } if (first) { - log?.LogInformation("Starting heartbeat..."); + LogInformationStartingHeartbeat(log); pulse = TimerToken.Create(this); } if (publishReconfigure) { try { - log?.LogInformation("Broadcasting reconfigure..."); + LogInformationBroadcastingReconfigure(log); PublishReconfigureImpl(publishReconfigureFlags); } catch @@ -1758,7 +1762,7 @@ public EndPoint[] GetEndPoints(bool configuredOnly = false) => } catch (Exception ex) { - log?.LogError(ex, $"Encountered error while updating cluster config: {ex.Message}"); + LogErrorEncounteredErrorWhileUpdatingClusterConfig(log, ex, ex.Message); return null; } } @@ -1774,7 +1778,7 @@ private void ResetAllNonConnected() private static ServerEndPoint? NominatePreferredPrimary(ILogger? log, ServerEndPoint[] servers, bool useTieBreakers, List primaries) { - log?.LogInformation("Election summary:"); + LogInformationElectionSummary(log); Dictionary? uniques = null; if (useTieBreakers) @@ -1788,11 +1792,11 @@ private void ResetAllNonConnected() if (serverResult.IsNullOrWhiteSpace()) { - log?.LogInformation($" Election: {Format.ToString(server)} had no tiebreaker set"); + LogInformationElectionNoTiebreaker(log, new(server)); } else { - log?.LogInformation($" Election: {Format.ToString(server)} nominates: {serverResult}"); + LogInformationElectionNominates(log, new(server), serverResult); if (!uniques.TryGetValue(serverResult, out int count)) count = 0; uniques[serverResult] = count + 1; } @@ -1802,37 +1806,37 @@ private void ResetAllNonConnected() switch (primaries.Count) { case 0: - log?.LogInformation(" Election: No primaries detected"); + LogInformationElectionNoPrimariesDetected(log); return null; case 1: - log?.LogInformation($" Election: Single primary detected: {Format.ToString(primaries[0].EndPoint)}"); + LogInformationElectionSinglePrimaryDetected(log, new(primaries[0].EndPoint)); return primaries[0]; default: - log?.LogInformation(" Election: Multiple primaries detected..."); + LogInformationElectionMultiplePrimariesDetected(log); if (useTieBreakers && uniques != null) { switch (uniques.Count) { case 0: - log?.LogInformation(" Election: No nominations by tie-breaker"); + LogInformationElectionNoNominationsByTieBreaker(log); break; case 1: string unanimous = uniques.Keys.Single(); - log?.LogInformation($" Election: Tie-breaker unanimous: {unanimous}"); + LogInformationElectionTieBreakerUnanimous(log, unanimous); var found = SelectServerByElection(servers, unanimous, log); if (found != null) { - log?.LogInformation($" Election: Elected: {Format.ToString(found.EndPoint)}"); + LogInformationElectionElected(log, new(found.EndPoint)); return found; } break; default: - log?.LogInformation(" Election is contested:"); + LogInformationElectionContested(log); ServerEndPoint? highest = null; bool arbitrary = false; foreach (var pair in uniques.OrderByDescending(x => x.Value)) { - log?.LogInformation($" Election: {pair.Key} has {pair.Value} votes"); + LogInformationElectionVotes(log, pair.Key, pair.Value); if (highest == null) { highest = SelectServerByElection(servers, pair.Key, log); @@ -1847,11 +1851,11 @@ private void ResetAllNonConnected() { if (arbitrary) { - log?.LogInformation($" Election: Choosing primary arbitrarily: {Format.ToString(highest.EndPoint)}"); + LogInformationElectionChoosingPrimaryArbitrarily(log, new(highest.EndPoint)); } else { - log?.LogInformation($" Election: Elected: {Format.ToString(highest.EndPoint)}"); + LogInformationElectionElected(log, new(highest.EndPoint)); } return highest; } @@ -1861,7 +1865,7 @@ private void ResetAllNonConnected() break; } - log?.LogInformation($" Election: Choosing primary arbitrarily: {Format.ToString(primaries[0].EndPoint)}"); + LogInformationElectionChoosingPrimaryArbitrarily(log, new(primaries[0].EndPoint)); return primaries[0]; } @@ -1873,13 +1877,13 @@ private void ResetAllNonConnected() if (string.Equals(Format.ToString(servers[i].EndPoint), endpoint, StringComparison.OrdinalIgnoreCase)) return servers[i]; } - log?.LogInformation("...but we couldn't find that"); + LogInformationCouldNotFindThatEndpoint(log); var deDottedEndpoint = DeDotifyHost(endpoint); for (int i = 0; i < servers.Length; i++) { if (string.Equals(DeDotifyHost(Format.ToString(servers[i].EndPoint)), deDottedEndpoint, StringComparison.OrdinalIgnoreCase)) { - log?.LogInformation($"...but we did find instead: {deDottedEndpoint}"); + LogInformationFoundAlternativeEndpoint(log, deDottedEndpoint); return servers[i]; } } @@ -2347,5 +2351,385 @@ private Task[] QuitAllServers() long? IInternalConnectionMultiplexer.GetConnectionId(EndPoint endpoint, ConnectionType type) => GetServerEndPoint(endpoint)?.GetBridge(type)?.ConnectionId; + + // Helper structs for complex ToString() calls + private readonly struct EndPointLogValue + { + private readonly EndPoint? endPoint; + + public EndPointLogValue(EndPoint? endPoint) + { + this.endPoint = endPoint; + } + + public override string ToString() => Format.ToString(endPoint); + } + + private readonly struct ServerEndPointLogValue + { + private readonly ServerEndPoint server; + + public ServerEndPointLogValue(ServerEndPoint server) + { + this.server = server; + } + + public override string ToString() => Format.ToString(server); + } + + // Generated LoggerMessage methods + [LoggerMessage( + Level = LogLevel.Error, + Message = "Connection failed: {EndPoint} ({ConnectionType}, {FailureType}): {ErrorMessage}")] + private static partial void LogErrorConnectionFailed(ILogger? logger, Exception? exception, EndPointLogValue endPoint, ConnectionType connectionType, ConnectionFailureType failureType, string errorMessage); + + [LoggerMessage( + Level = LogLevel.Error, + EventId = 1, + Message = "> {Message}")] + private static partial void LogErrorInnerException(ILogger? logger, Exception exception, string message); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 2, + Message = "Checking {EndPoint} is available...")] + private static partial void LogInformationCheckingServerAvailable(ILogger? logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Error, + EventId = 3, + Message = "Operation failed on {EndPoint}, aborting: {ErrorMessage}")] + private static partial void LogErrorOperationFailedOnServer(ILogger? logger, Exception exception, EndPointLogValue endPoint, string errorMessage); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 4, + Message = "Attempting to set tie-breaker on {EndPoint}...")] + private static partial void LogInformationAttemptingToSetTieBreaker(ILogger? logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 5, + Message = "Making {EndPoint} a primary...")] + private static partial void LogInformationMakingServerPrimary(ILogger? logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 6, + Message = "Resending tie-breaker to {EndPoint}...")] + private static partial void LogInformationResendingTieBreaker(ILogger? logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 7, + Message = "Broadcasting via {EndPoint}...")] + private static partial void LogInformationBroadcastingViaNode(ILogger? logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 8, + Message = "Replicating to {EndPoint}...")] + private static partial void LogInformationReplicatingToNode(ILogger? logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 9, + Message = "Reconfiguring all endpoints...")] + private static partial void LogInformationReconfiguringAllEndpoints(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 10, + Message = "Verifying the configuration was incomplete; please verify")] + private static partial void LogInformationVerifyingConfigurationIncomplete(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 11, + Message = "Connecting (async) on {Framework} (StackExchange.Redis: v{Version})")] + private static partial void LogInformationConnectingAsync(ILogger? logger, string framework, string version); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 12, + Message = "Connecting (sync) on {Framework} (StackExchange.Redis: v{Version})")] + private static partial void LogInformationConnectingSync(ILogger? logger, string framework, string version); + + [LoggerMessage( + Level = LogLevel.Error, + EventId = 13, + Message = "{ErrorMessage}")] + private static partial void LogErrorSyncConnectTimeout(ILogger? logger, Exception exception, string errorMessage); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 14, + Message = "{Message}")] + private static partial void LogInformationAfterConnect(ILogger? logger, string message); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 15, + Message = "Total connect time: {ElapsedMs:n0} ms")] + private static partial void LogInformationTotalConnectTime(ILogger? logger, long elapsedMs); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 16, + Message = "No tasks to await")] + private static partial void LogInformationNoTasksToAwait(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 17, + Message = "All tasks are already complete")] + private static partial void LogInformationAllTasksComplete(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 18, + Message = "{Message}", + SkipEnabledCheck = true)] + private static partial void LogInformationThreadPoolStats(ILogger? logger, string message); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 19, + Message = "Reconfiguration was already in progress due to: {ActiveCause}, attempted to run for: {NewCause}")] + private static partial void LogInformationReconfigurationInProgress(ILogger? logger, string? activeCause, string newCause); + + private readonly struct ConfigurationOptionsLogValue + { + private readonly ConfigurationOptions options; + + public ConfigurationOptionsLogValue(ConfigurationOptions options) + { + this.options = options; + } + + public override string ToString() => options.ToString(includePassword: false); + } + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 20, + Message = "{Configuration}")] + private static partial void LogInformationConfiguration(ILogger? logger, ConfigurationOptionsLogValue configuration); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 21, + Message = "{Count} unique nodes specified ({TieBreakerStatus} tiebreaker)")] + private static partial void LogInformationUniqueNodesSpecified(ILogger? logger, int count, string tieBreakerStatus); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 22, + Message = "Allowing {Count} endpoint(s) {TimeSpan} to respond...")] + private static partial void LogInformationAllowingEndpointsToRespond(ILogger? logger, int count, TimeSpan timeSpan); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 23, + Message = " Server[{Index}] ({Server}) Status: {Status} (inst: {MessagesSinceLastHeartbeat}, qs: {MessagesSentAwaitingResponse}, in: {BytesAvailableOnSocket}, qu: {MessagesSinceLastHeartbeat2}, aw: {IsWriterActive}, in-pipe: {BytesInReadPipe}, out-pipe: {BytesInWritePipe}, bw: {BacklogStatus}, rs: {ReadStatus}. ws: {WriteStatus})")] + private static partial void LogInformationServerStatus(ILogger? logger, int index, ServerEndPointLogValue server, TaskStatus status, long messagesSinceLastHeartbeat, long messagesSentAwaitingResponse, long bytesAvailableOnSocket, long messagesSinceLastHeartbeat2, bool isWriterActive, long bytesInReadPipe, long bytesInWritePipe, PhysicalBridge.BacklogStatus backlogStatus, PhysicalConnection.ReadStatus readStatus, PhysicalConnection.WriteStatus writeStatus); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 24, + Message = "Endpoint summary:")] + private static partial void LogInformationEndpointSummary(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 25, + Message = " {EndPoint}: Endpoint is (Interactive: {InteractiveState}, Subscription: {SubscriptionState})")] + private static partial void LogInformationEndpointState(ILogger? logger, EndPointLogValue endPoint, PhysicalBridge.State interactiveState, PhysicalBridge.State subscriptionState); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 26, + Message = "Task summary:")] + private static partial void LogInformationTaskSummary(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Error, + EventId = 27, + Message = " {Server}: Faulted: {ErrorMessage}")] + private static partial void LogErrorServerFaulted(ILogger? logger, Exception exception, ServerEndPointLogValue server, string errorMessage); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 28, + Message = " {Server}: Connect task canceled")] + private static partial void LogInformationConnectTaskCanceled(ILogger? logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 29, + Message = " {Server}: Returned with success as {ServerType} {Role} (Source: {Source})")] + private static partial void LogInformationServerReturnedSuccess(ILogger? logger, ServerEndPointLogValue server, ServerType serverType, string role, string source); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 30, + Message = " {Server}: Returned, but incorrectly")] + private static partial void LogInformationServerReturnedIncorrectly(ILogger? logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 31, + Message = " {Server}: Did not respond (Task.Status: {TaskStatus})")] + private static partial void LogInformationServerDidNotRespond(ILogger? logger, ServerEndPointLogValue server, TaskStatus taskStatus); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 32, + Message = "{EndPoint}: Clearing as RedundantPrimary")] + private static partial void LogInformationClearingAsRedundantPrimary(ILogger? logger, ServerEndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 33, + Message = "{EndPoint}: Setting as RedundantPrimary")] + private static partial void LogInformationSettingAsRedundantPrimary(ILogger? logger, ServerEndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 34, + Message = "Cluster: {CoveredSlots} of {TotalSlots} slots covered")] + private static partial void LogInformationClusterSlotsCovered(ILogger? logger, long coveredSlots, int totalSlots); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 35, + Message = "No subscription changes necessary")] + private static partial void LogInformationNoSubscriptionChanges(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 36, + Message = "Subscriptions attempting reconnect: {SubscriptionChanges}")] + private static partial void LogInformationSubscriptionsAttemptingReconnect(ILogger? logger, long subscriptionChanges); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 37, + Message = "{StormLog}")] + private static partial void LogInformationStormLog(ILogger? logger, string stormLog); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 38, + Message = "Resetting failing connections to retry...")] + private static partial void LogInformationResettingFailingConnections(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 39, + Message = " Retrying - attempts left: {AttemptsLeft}...")] + private static partial void LogInformationRetryingAttempts(ILogger? logger, int attemptsLeft); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 40, + Message = "Starting heartbeat...")] + private static partial void LogInformationStartingHeartbeat(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 41, + Message = "Broadcasting reconfigure...")] + private static partial void LogInformationBroadcastingReconfigure(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Error, + EventId = 42, + Message = "Encountered error while updating cluster config: {ErrorMessage}")] + private static partial void LogErrorEncounteredErrorWhileUpdatingClusterConfig(ILogger? logger, Exception exception, string errorMessage); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 43, + Message = "Election summary:")] + private static partial void LogInformationElectionSummary(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 44, + Message = " Election: {Server} had no tiebreaker set")] + private static partial void LogInformationElectionNoTiebreaker(ILogger? logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 45, + Message = " Election: {Server} nominates: {ServerResult}")] + private static partial void LogInformationElectionNominates(ILogger? logger, ServerEndPointLogValue server, string serverResult); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 46, + Message = " Election: No primaries detected")] + private static partial void LogInformationElectionNoPrimariesDetected(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 47, + Message = " Election: Single primary detected: {EndPoint}")] + private static partial void LogInformationElectionSinglePrimaryDetected(ILogger? logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 48, + Message = " Election: Multiple primaries detected...")] + private static partial void LogInformationElectionMultiplePrimariesDetected(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 49, + Message = " Election: No nominations by tie-breaker")] + private static partial void LogInformationElectionNoNominationsByTieBreaker(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 50, + Message = " Election: Tie-breaker unanimous: {Unanimous}")] + private static partial void LogInformationElectionTieBreakerUnanimous(ILogger? logger, string unanimous); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 51, + Message = " Election: Elected: {EndPoint}")] + private static partial void LogInformationElectionElected(ILogger? logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 52, + Message = " Election is contested:")] + private static partial void LogInformationElectionContested(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 53, + Message = " Election: {Key} has {Value} votes")] + private static partial void LogInformationElectionVotes(ILogger? logger, string key, int value); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 54, + Message = " Election: Choosing primary arbitrarily: {EndPoint}")] + private static partial void LogInformationElectionChoosingPrimaryArbitrarily(ILogger? logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 55, + Message = "...but we couldn't find that")] + private static partial void LogInformationCouldNotFindThatEndpoint(ILogger? logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 56, + Message = "...but we did find instead: {DeDottedEndpoint}")] + private static partial void LogInformationFoundAlternativeEndpoint(ILogger? logger, string deDottedEndpoint); } } From cf9f65989f15d4fabc1e023085107eb21c5258ad Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 21 Jul 2025 10:38:30 +0100 Subject: [PATCH 343/435] Implement internal cancellation for SCAN via WithCancellation (#2911) * - implement internal cancellation for SCAN via WithCancellation * release notes --- docs/AsyncTimeouts.md | 16 +++++++- docs/ReleaseNotes.md | 6 +-- src/StackExchange.Redis/CursorEnumerable.cs | 3 +- src/StackExchange.Redis/TaskExtensions.cs | 38 ++++++++++++++++++ .../CancellationTests.cs | 39 +++++++++++++++++++ 5 files changed, 97 insertions(+), 5 deletions(-) diff --git a/docs/AsyncTimeouts.md b/docs/AsyncTimeouts.md index 5ba4fd3f1..04892d59a 100644 --- a/docs/AsyncTimeouts.md +++ b/docs/AsyncTimeouts.md @@ -62,4 +62,18 @@ using var cts = CancellationTokenSource.CreateLinkedTokenSource(token); // or mu cts.CancelAfter(timeout); await database.StringSetAsync("key", "value").WaitAsync(cts.Token); var value = await database.StringGetAsync("key").WaitAsync(cts.Token); -`````` \ No newline at end of file +``` + +### Cancelling keys enumeration + +Keys being enumerated (via `SCAN`) can *also* be cancelled, using the inbuilt `.WithCancellation(...)` method: + +```csharp +CancellationToken token = ...; // for example, from HttpContext.RequestAborted +await foreach (var key in server.KeysAsync(pattern: "*foo*").WithCancellation(token)) +{ + ... +} +``` + +To use a timeout instead, you can use the `CancellationTokenSource` approach shown above. \ No newline at end of file diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b301cca8b..22dd36256 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,9 @@ Current package versions: ## Unreleased -- (none) +- Support async cancellation of `SCAN` enumeration ([#2911 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2911)) +- Add `XTRIM MINID` support ([#2842 by kijanawoodard](https://github.com/StackExchange/StackExchange.Redis/pull/2842)) +- Add new CE 8.2 stream support - `XDELEX`, `XACKDEL`, `{XADD|XTRIM} [KEEPREF|DELREF|ACKED]` ([#2912 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2912)) ## 2.8.47 @@ -16,8 +18,6 @@ Current package versions: - Package updates ([#2906 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2906)) - Docs: added [guidance on async timeouts](https://stackexchange.github.io/StackExchange.Redis/AsyncTimeouts) ([#2910 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2910)) - Fix handshake error with `CLIENT ID` ([#2909 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2909)) -- Add `XTRIM MINID` support ([#2842 by kijanawoodard](https://github.com/StackExchange/StackExchange.Redis/pull/2842)) -- Add new CE 8.2 stream support - `XDELEX`, `XACKDEL`, `{XADD|XTRIM} [KEEPREF|DELREF|ACKED]` ([#2912 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2912)) ## 2.8.41 diff --git a/src/StackExchange.Redis/CursorEnumerable.cs b/src/StackExchange.Redis/CursorEnumerable.cs index 55d93d6a6..e526eceaa 100644 --- a/src/StackExchange.Redis/CursorEnumerable.cs +++ b/src/StackExchange.Redis/CursorEnumerable.cs @@ -141,6 +141,7 @@ private bool SimpleNext() { if (_pageOffset + 1 < _pageCount) { + cancellationToken.ThrowIfCancellationRequested(); _pageOffset++; return true; } @@ -274,7 +275,7 @@ private async ValueTask AwaitedNextAsync(bool isInitial) ScanResult scanResult; try { - scanResult = await pending.ForAwait(); + scanResult = await pending.WaitAsync(cancellationToken).ForAwait(); } catch (Exception ex) { diff --git a/src/StackExchange.Redis/TaskExtensions.cs b/src/StackExchange.Redis/TaskExtensions.cs index ad4b41113..a0994a0b6 100644 --- a/src/StackExchange.Redis/TaskExtensions.cs +++ b/src/StackExchange.Redis/TaskExtensions.cs @@ -25,6 +25,44 @@ internal static Task ObserveErrors(this Task task) return task; } +#if !NET6_0_OR_GREATER + // suboptimal polyfill version of the .NET 6+ API, but reasonable for light use + internal static Task WaitAsync(this Task task, CancellationToken cancellationToken) + { + if (task.IsCompleted || !cancellationToken.CanBeCanceled) return task; + return Wrap(task, cancellationToken); + + static async Task Wrap(Task task, CancellationToken cancellationToken) + { + var tcs = new TaskSourceWithToken(cancellationToken); + using var reg = cancellationToken.Register( + static state => ((TaskSourceWithToken)state!).Cancel(), tcs); + _ = task.ContinueWith( + static (t, state) => + { + var tcs = (TaskSourceWithToken)state!; + if (t.IsCanceled) tcs.TrySetCanceled(); + else if (t.IsFaulted) tcs.TrySetException(t.Exception!); + else tcs.TrySetResult(t.Result); + }, + tcs); + return await tcs.Task; + } + } + + // the point of this type is to combine TCS and CT so that we can use a static + // registration via Register + private sealed class TaskSourceWithToken : TaskCompletionSource + { + public TaskSourceWithToken(CancellationToken cancellationToken) + => _cancellationToken = cancellationToken; + + private readonly CancellationToken _cancellationToken; + + public void Cancel() => TrySetCanceled(_cancellationToken); + } +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static ConfiguredTaskAwaitable ForAwait(this Task task) => task.ConfigureAwait(false); [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/tests/StackExchange.Redis.Tests/CancellationTests.cs b/tests/StackExchange.Redis.Tests/CancellationTests.cs index 71306e9dd..f9f955eb8 100644 --- a/tests/StackExchange.Redis.Tests/CancellationTests.cs +++ b/tests/StackExchange.Redis.Tests/CancellationTests.cs @@ -40,6 +40,11 @@ public async Task WithCancellation_ValidToken_OperationSucceeds() private static void Pause(IDatabase db) => db.Execute("client", ["pause", ConnectionPauseMilliseconds], CommandFlags.FireAndForget); + private void Pause(IServer server) + { + server.Execute("client", new object[] { "pause", ConnectionPauseMilliseconds }, CommandFlags.FireAndForget); + } + [Fact] public async Task WithTimeout_ShortTimeout_Async_ThrowsOperationCanceledException() { @@ -147,4 +152,38 @@ public async Task CancellationDuringOperation_Async_CancelsGracefully(CancelStra Assert.Equal(cts.Token, oce.CancellationToken); } } + + [Fact] + public async Task ScanCancellable() + { + using var conn = Create(); + var db = conn.GetDatabase(); + var server = conn.GetServer(conn.GetEndPoints()[0]); + + using var cts = new CancellationTokenSource(); + + var watch = Stopwatch.StartNew(); + Pause(server); + try + { + db.StringSet(Me(), "value", TimeSpan.FromMinutes(5), flags: CommandFlags.FireAndForget); + await using var iter = server.KeysAsync(pageSize: 1000).WithCancellation(cts.Token).GetAsyncEnumerator(); + var pending = iter.MoveNextAsync(); + Assert.False(cts.Token.IsCancellationRequested); + cts.CancelAfter(ShortDelayMilliseconds); // start this *after* we've got past the initial check + while (await pending) + { + pending = iter.MoveNextAsync(); + } + Assert.Fail($"{ExpectedCancel}: {watch.ElapsedMilliseconds}ms"); + } + catch (OperationCanceledException oce) + { + var taken = watch.ElapsedMilliseconds; + // Expected if cancellation happens during operation + Log($"Cancelled after {taken}ms"); + Assert.True(taken < ConnectionPauseMilliseconds / 2, "Should have cancelled much sooner"); + Assert.Equal(cts.Token, oce.CancellationToken); + } + } } From 7b1460a4bbe3a740cfce75c4f9e698b3d098d25f Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 21 Jul 2025 11:04:13 +0100 Subject: [PATCH 344/435] move logger extensions to LoggerExtensions.cs (extends 2903) (#2917) * move logger extensions to LoggerExtensions.cs * move LogWithThreadPoolStats * optimize LogWithThreadPoolStats --- .../ConnectionMultiplexer.cs | 534 +++--------------- src/StackExchange.Redis/LoggerExtensions.cs | 406 +++++++++++++ 2 files changed, 472 insertions(+), 468 deletions(-) create mode 100644 src/StackExchange.Redis/LoggerExtensions.cs diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 627daaabf..de3e8d92e 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -170,10 +170,10 @@ private static ConnectionMultiplexer CreateMultiplexer(ConfigurationOptions conf lock (log) // Keep the outer and any inner errors contiguous { var ex = a.Exception; - LogErrorConnectionFailed(log, ex, new(a.EndPoint), a.ConnectionType, a.FailureType, ex?.Message ?? "(unknown)"); + log?.LogErrorConnectionFailed(ex, new(a.EndPoint), a.ConnectionType, a.FailureType, ex?.Message ?? "(unknown)"); while ((ex = ex?.InnerException) != null) { - LogErrorInnerException(log, ex, ex.Message); + log?.LogErrorInnerException(ex, ex.Message); } } } @@ -219,14 +219,14 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt const CommandFlags flags = CommandFlags.NoRedirect; Message msg; - LogInformationCheckingServerAvailable(log, new(srv.EndPoint)); + log?.LogInformationCheckingServerAvailable(new(srv.EndPoint)); try { await srv.PingAsync(flags).ForAwait(); // if it isn't happy, we're not happy } catch (Exception ex) { - LogErrorOperationFailedOnServer(log, ex, new(srv.EndPoint), ex.Message); + log?.LogErrorOperationFailedOnServer(ex, new(srv.EndPoint), ex.Message); throw; } @@ -241,7 +241,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt foreach (var node in nodes) { if (!node.IsConnected || node.IsReplica) continue; - LogInformationAttemptingToSetTieBreaker(log, new(node.EndPoint)); + log?.LogInformationAttemptingToSetTieBreaker(new(node.EndPoint)); msg = Message.Create(0, flags | CommandFlags.FireAndForget, RedisCommand.SET, tieBreakerKey, newPrimary); try { @@ -252,21 +252,21 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt } // stop replicating, promote to a standalone primary - LogInformationMakingServerPrimary(log, new(srv.EndPoint)); + log?.LogInformationMakingServerPrimary(new(srv.EndPoint)); try { await srv.ReplicaOfAsync(null, flags).ForAwait(); } catch (Exception ex) { - LogErrorOperationFailedOnServer(log, ex, new(srv.EndPoint), ex.Message); + log?.LogErrorOperationFailedOnServer(ex, new(srv.EndPoint), ex.Message); throw; } // also, in case it was a replica a moment ago, and hasn't got the tie-breaker yet, we re-send the tie-breaker to this one if (!tieBreakerKey.IsNull && !server.IsReplica) { - LogInformationResendingTieBreaker(log, new(server.EndPoint)); + log?.LogInformationResendingTieBreaker(new(server.EndPoint)); msg = Message.Create(0, flags | CommandFlags.FireAndForget, RedisCommand.SET, tieBreakerKey, newPrimary); try { @@ -297,7 +297,7 @@ async Task BroadcastAsync(ServerSnapshot serverNodes) foreach (var node in serverNodes) { if (!node.IsConnected) continue; - LogInformationBroadcastingViaNode(log, new(node.EndPoint)); + log?.LogInformationBroadcastingViaNode(new(node.EndPoint)); msg = Message.Create(-1, flags | CommandFlags.FireAndForget, RedisCommand.PUBLISH, channel, newPrimary); await node.WriteDirectAsync(msg, ResultProcessor.Int64).ForAwait(); } @@ -313,7 +313,7 @@ async Task BroadcastAsync(ServerSnapshot serverNodes) { if (node == server || node.ServerType != ServerType.Standalone) continue; - LogInformationReplicatingToNode(log, new(node.EndPoint)); + log?.LogInformationReplicatingToNode(new(node.EndPoint)); msg = RedisServer.CreateReplicaOfMessage(node, server.EndPoint, flags); await node.WriteDirectAsync(msg, ResultProcessor.DemandOK).ForAwait(); } @@ -325,7 +325,7 @@ async Task BroadcastAsync(ServerSnapshot serverNodes) await BroadcastAsync(nodes).ForAwait(); // and reconfigure the muxer - LogInformationReconfiguringAllEndpoints(log); + log?.LogInformationReconfiguringAllEndpoints(); // Yes, there is a tiny latency race possible between this code and the next call, but it's far more minute than before. // The effective gap between 0 and > 0 (likely off-box) latency is something that may never get hit here by anyone. if (blockingReconfig) @@ -334,7 +334,7 @@ async Task BroadcastAsync(ServerSnapshot serverNodes) } if (!await ReconfigureAsync(first: false, reconfigureAll: true, log, srv.EndPoint, cause: nameof(MakePrimaryAsync)).ForAwait()) { - LogInformationVerifyingConfigurationIncomplete(log); + log?.LogInformationVerifyingConfigurationIncomplete(); } } @@ -474,52 +474,30 @@ private static async Task WaitAllIgnoreErrorsAsync(string name, Task[] tas _ = tasks ?? throw new ArgumentNullException(nameof(tasks)); if (tasks.Length == 0) { - LogInformationNoTasksToAwait(log); + log?.LogInformationNoTasksToAwait(); return true; } if (AllComplete(tasks)) { - LogInformationAllTasksComplete(log); + log?.LogInformationAllTasksComplete(); return true; } - static void LogWithThreadPoolStats(ILogger? log, string message) - { - if (log?.IsEnabled(LogLevel.Information) != false) - { - return; - } - - var busyWorkerCount = 0; - if (log is not null) - { - var sb = new StringBuilder(); - sb.Append(message); - busyWorkerCount = PerfCounterHelper.GetThreadPoolStats(out string iocp, out string worker, out string? workItems); - sb.Append(", IOCP: ").Append(iocp).Append(", WORKER: ").Append(worker); - if (workItems is not null) - { - sb.Append(", POOL: ").Append(workItems); - } - LogInformationThreadPoolStats(log, sb.ToString()); - } - } - var watch = ValueStopwatch.StartNew(); - LogWithThreadPoolStats(log, $"Awaiting {tasks.Length} {name} task completion(s) for {timeoutMilliseconds}ms"); + log?.LogWithThreadPoolStats($"Awaiting {tasks.Length} {name} task completion(s) for {timeoutMilliseconds}ms"); try { // if none error, great var remaining = timeoutMilliseconds - watch.ElapsedMilliseconds; if (remaining <= 0) { - LogWithThreadPoolStats(log, "Timeout before awaiting for tasks"); + log.LogWithThreadPoolStats("Timeout before awaiting for tasks"); return false; } var allTasks = Task.WhenAll(tasks).ObserveErrors(); bool all = await allTasks.TimeoutAfter(timeoutMs: remaining).ObserveErrors().ForAwait(); - LogWithThreadPoolStats(log, all ? $"All {tasks.Length} {name} tasks completed cleanly" : $"Not all {name} tasks completed cleanly (from {caller}#{callerLineNumber}, timeout {timeoutMilliseconds}ms)"); + log?.LogWithThreadPoolStats(all ? $"All {tasks.Length} {name} tasks completed cleanly" : $"Not all {name} tasks completed cleanly (from {caller}#{callerLineNumber}, timeout {timeoutMilliseconds}ms)"); return all; } catch @@ -535,7 +513,7 @@ static void LogWithThreadPoolStats(ILogger? log, string message) var remaining = timeoutMilliseconds - watch.ElapsedMilliseconds; if (remaining <= 0) { - LogWithThreadPoolStats(log, "Timeout awaiting tasks"); + log.LogWithThreadPoolStats("Timeout awaiting tasks"); return false; } try @@ -546,7 +524,7 @@ static void LogWithThreadPoolStats(ILogger? log, string message) { } } } - LogWithThreadPoolStats(log, "Finished awaiting tasks"); + log.LogWithThreadPoolStats("Finished awaiting tasks"); return false; } @@ -607,7 +585,7 @@ private static async Task ConnectImplAsync(ConfigurationO try { var sw = ValueStopwatch.StartNew(); - LogInformationConnectingAsync(log, RuntimeInformation.FrameworkDescription, Utils.GetLibVersion()); + log?.LogInformationConnectingAsync(RuntimeInformation.FrameworkDescription, Utils.GetLibVersion()); muxer = CreateMultiplexer(configuration, log, serverType, out connectHandler); killMe = muxer; @@ -626,9 +604,9 @@ private static async Task ConnectImplAsync(ConfigurationO muxer.InitializeSentinel(log); } - await configuration.AfterConnectAsync(muxer, s => LogInformationAfterConnect(log, s)).ForAwait(); + await configuration.AfterConnectAsync(muxer, s => log?.LogInformationAfterConnect(s)).ForAwait(); - LogInformationTotalConnectTime(log, sw.ElapsedMilliseconds); + log?.LogInformationTotalConnectTime(sw.ElapsedMilliseconds); return muxer; } @@ -695,7 +673,7 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat try { var sw = ValueStopwatch.StartNew(); - LogInformationConnectingSync(log, RuntimeInformation.FrameworkDescription, Utils.GetLibVersion()); + log?.LogInformationConnectingSync(RuntimeInformation.FrameworkDescription, Utils.GetLibVersion()); muxer = CreateMultiplexer(configuration, log, serverType, out connectHandler, endpoints); killMe = muxer; @@ -714,7 +692,7 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat { var ex = ExceptionFactory.UnableToConnect(muxer, "ConnectTimeout"); muxer.LastException = ex; - LogErrorSyncConnectTimeout(muxer.Logger, ex, ex.Message); + muxer.Logger?.LogErrorSyncConnectTimeout(ex, ex.Message); } } @@ -728,9 +706,9 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat muxer.InitializeSentinel(log); } - configuration.AfterConnectAsync(muxer, s => LogInformationAfterConnect(log, s)).Wait(muxer.SyncConnectTimeout(true)); + configuration.AfterConnectAsync(muxer, s => log?.LogInformationAfterConnect(s)).Wait(muxer.SyncConnectTimeout(true)); - LogInformationTotalConnectTime(log, sw.ElapsedMilliseconds); + log?.LogInformationTotalConnectTime(sw.ElapsedMilliseconds); return muxer; } @@ -1413,14 +1391,14 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog if (!ranThisCall) { - LogInformationReconfigurationInProgress(log, activeConfigCause, cause); + log?.LogInformationReconfigurationInProgress(activeConfigCause, cause); return false; } Trace("Starting reconfiguration..."); Trace(blame != null, "Blaming: " + Format.ToString(blame)); Interlocked.Exchange(ref lastReconfigiureTicks, Environment.TickCount); - LogInformationConfiguration(log, new(RawConfig)); + log?.LogInformationConfiguration(new(RawConfig)); if (first) { @@ -1450,7 +1428,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog int standaloneCount = 0, clusterCount = 0, sentinelCount = 0; var endpoints = EndPoints; bool useTieBreakers = RawConfig.TryGetTieBreaker(out var tieBreakerKey); - LogInformationUniqueNodesSpecified(log, endpoints.Count, useTieBreakers ? "with" : "without"); + log?.LogInformationUniqueNodesSpecified(endpoints.Count, useTieBreakers ? "with" : "without"); if (endpoints.Count == 0) { @@ -1492,7 +1470,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog watch ??= ValueStopwatch.StartNew(); var remaining = RawConfig.ConnectTimeout - watch.Value.ElapsedMilliseconds; - LogInformationAllowingEndpointsToRespond(log, available.Length, TimeSpan.FromMilliseconds(remaining)); + log?.LogInformationAllowingEndpointsToRespond(available.Length, TimeSpan.FromMilliseconds(remaining)); Trace("Allowing endpoints " + TimeSpan.FromMilliseconds(remaining) + " to respond..."); var allConnected = await WaitAllIgnoreErrorsAsync("available", available, remaining, log).ForAwait(); @@ -1505,18 +1483,18 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog var task = available[i]; var bs = server.GetBridgeStatus(ConnectionType.Interactive); - LogInformationServerStatus(log, i, new(server), task.Status, bs.MessagesSinceLastHeartbeat, bs.Connection.MessagesSentAwaitingResponse, bs.Connection.BytesAvailableOnSocket, bs.MessagesSinceLastHeartbeat, bs.IsWriterActive, bs.Connection.BytesInReadPipe, bs.Connection.BytesInWritePipe, bs.BacklogStatus, bs.Connection.ReadStatus, bs.Connection.WriteStatus); + log?.LogInformationServerStatus(i, new(server), task.Status, bs.MessagesSinceLastHeartbeat, bs.Connection.MessagesSentAwaitingResponse, bs.Connection.BytesAvailableOnSocket, bs.MessagesSinceLastHeartbeat, bs.IsWriterActive, bs.Connection.BytesInReadPipe, bs.Connection.BytesInWritePipe, bs.BacklogStatus, bs.Connection.ReadStatus, bs.Connection.WriteStatus); } } - LogInformationEndpointSummary(log); + log?.LogInformationEndpointSummary(); // Log current state after await foreach (var server in servers) { - LogInformationEndpointState(log, new(server.EndPoint), server.InteractiveConnectionState, server.SubscriptionConnectionState); + log?.LogInformationEndpointState(new(server.EndPoint), server.InteractiveConnectionState, server.SubscriptionConnectionState); } - LogInformationTaskSummary(log); + log?.LogInformationTaskSummary(); EndPointCollection? updatedClusterEndpointCollection = null; for (int i = 0; i < available.Length; i++) { @@ -1529,21 +1507,21 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog var aex = task.Exception!; foreach (var ex in aex.InnerExceptions) { - LogErrorServerFaulted(log, ex, new(server), ex.Message); + log?.LogErrorServerFaulted(ex, new(server), ex.Message); failureMessage = ex.Message; } } else if (task.IsCanceled) { server.SetUnselectable(UnselectableFlags.DidNotRespond); - LogInformationConnectTaskCanceled(log, new(server)); + log?.LogInformationConnectTaskCanceled(new(server)); } else if (task.IsCompleted) { if (task.Result != "Disconnected") { server.ClearUnselectable(UnselectableFlags.DidNotRespond); - LogInformationServerReturnedSuccess(log, new(server), server.ServerType, server.IsReplica ? "replica" : "primary", task.Result); + log?.LogInformationServerReturnedSuccess(new(server), server.ServerType, server.IsReplica ? "replica" : "primary", task.Result); // Count the server types switch (server.ServerType) @@ -1596,13 +1574,13 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog else { server.SetUnselectable(UnselectableFlags.DidNotRespond); - LogInformationServerReturnedIncorrectly(log, new(server)); + log?.LogInformationServerReturnedIncorrectly(new(server)); } } else { server.SetUnselectable(UnselectableFlags.DidNotRespond); - LogInformationServerDidNotRespond(log, new(server), task.Status); + log?.LogInformationServerDidNotRespond(new(server), task.Status); } } @@ -1646,11 +1624,11 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog { if (primary == preferred || primary.IsReplica) { - LogInformationClearingAsRedundantPrimary(log, new(primary)); + log?.LogInformationClearingAsRedundantPrimary(new(primary)); } else { - LogInformationSettingAsRedundantPrimary(log, new(primary)); + log?.LogInformationSettingAsRedundantPrimary(new(primary)); primary.SetUnselectable(UnselectableFlags.RedundantPrimary); } } @@ -1660,7 +1638,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog { ServerSelectionStrategy.ServerType = ServerType.Cluster; long coveredSlots = ServerSelectionStrategy.CountCoveredSlots(); - LogInformationClusterSlotsCovered(log, coveredSlots, ServerSelectionStrategy.TotalSlots); + log?.LogInformationClusterSlotsCovered(coveredSlots, ServerSelectionStrategy.TotalSlots); } if (!first) { @@ -1668,11 +1646,11 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog long subscriptionChanges = EnsureSubscriptions(CommandFlags.FireAndForget); if (subscriptionChanges == 0) { - LogInformationNoSubscriptionChanges(log); + log?.LogInformationNoSubscriptionChanges(); } else { - LogInformationSubscriptionsAttemptingReconnect(log, subscriptionChanges); + log?.LogInformationSubscriptionsAttemptingReconnect(subscriptionChanges); } } if (showStats) @@ -1683,14 +1661,14 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog string? stormLog = GetStormLog(); if (!string.IsNullOrWhiteSpace(stormLog)) { - LogInformationStormLog(log, stormLog!); + log?.LogInformationStormLog(stormLog!); } healthy = standaloneCount != 0 || clusterCount != 0 || sentinelCount != 0; if (first && !healthy && attemptsLeft > 0) { - LogInformationResettingFailingConnections(log); + log?.LogInformationResettingFailingConnections(); ResetAllNonConnected(); - LogInformationRetryingAttempts(log, attemptsLeft); + log?.LogInformationRetryingAttempts(attemptsLeft); } // WTF("?: " + attempts); } @@ -1702,14 +1680,14 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog } if (first) { - LogInformationStartingHeartbeat(log); + log?.LogInformationStartingHeartbeat(); pulse = TimerToken.Create(this); } if (publishReconfigure) { try { - LogInformationBroadcastingReconfigure(log); + log?.LogInformationBroadcastingReconfigure(); PublishReconfigureImpl(publishReconfigureFlags); } catch @@ -1762,7 +1740,7 @@ public EndPoint[] GetEndPoints(bool configuredOnly = false) => } catch (Exception ex) { - LogErrorEncounteredErrorWhileUpdatingClusterConfig(log, ex, ex.Message); + log?.LogErrorEncounteredErrorWhileUpdatingClusterConfig(ex, ex.Message); return null; } } @@ -1778,7 +1756,7 @@ private void ResetAllNonConnected() private static ServerEndPoint? NominatePreferredPrimary(ILogger? log, ServerEndPoint[] servers, bool useTieBreakers, List primaries) { - LogInformationElectionSummary(log); + log?.LogInformationElectionSummary(); Dictionary? uniques = null; if (useTieBreakers) @@ -1792,11 +1770,11 @@ private void ResetAllNonConnected() if (serverResult.IsNullOrWhiteSpace()) { - LogInformationElectionNoTiebreaker(log, new(server)); + log?.LogInformationElectionNoTiebreaker(new(server)); } else { - LogInformationElectionNominates(log, new(server), serverResult); + log?.LogInformationElectionNominates(new(server), serverResult); if (!uniques.TryGetValue(serverResult, out int count)) count = 0; uniques[serverResult] = count + 1; } @@ -1806,37 +1784,37 @@ private void ResetAllNonConnected() switch (primaries.Count) { case 0: - LogInformationElectionNoPrimariesDetected(log); + log?.LogInformationElectionNoPrimariesDetected(); return null; case 1: - LogInformationElectionSinglePrimaryDetected(log, new(primaries[0].EndPoint)); + log?.LogInformationElectionSinglePrimaryDetected(new(primaries[0].EndPoint)); return primaries[0]; default: - LogInformationElectionMultiplePrimariesDetected(log); + log?.LogInformationElectionMultiplePrimariesDetected(); if (useTieBreakers && uniques != null) { switch (uniques.Count) { case 0: - LogInformationElectionNoNominationsByTieBreaker(log); + log?.LogInformationElectionNoNominationsByTieBreaker(); break; case 1: string unanimous = uniques.Keys.Single(); - LogInformationElectionTieBreakerUnanimous(log, unanimous); + log?.LogInformationElectionTieBreakerUnanimous(unanimous); var found = SelectServerByElection(servers, unanimous, log); if (found != null) { - LogInformationElectionElected(log, new(found.EndPoint)); + log?.LogInformationElectionElected(new(found.EndPoint)); return found; } break; default: - LogInformationElectionContested(log); + log?.LogInformationElectionContested(); ServerEndPoint? highest = null; bool arbitrary = false; foreach (var pair in uniques.OrderByDescending(x => x.Value)) { - LogInformationElectionVotes(log, pair.Key, pair.Value); + log?.LogInformationElectionVotes(pair.Key, pair.Value); if (highest == null) { highest = SelectServerByElection(servers, pair.Key, log); @@ -1851,11 +1829,11 @@ private void ResetAllNonConnected() { if (arbitrary) { - LogInformationElectionChoosingPrimaryArbitrarily(log, new(highest.EndPoint)); + log?.LogInformationElectionChoosingPrimaryArbitrarily(new(highest.EndPoint)); } else { - LogInformationElectionElected(log, new(highest.EndPoint)); + log?.LogInformationElectionElected(new(highest.EndPoint)); } return highest; } @@ -1865,7 +1843,7 @@ private void ResetAllNonConnected() break; } - LogInformationElectionChoosingPrimaryArbitrarily(log, new(primaries[0].EndPoint)); + log?.LogInformationElectionChoosingPrimaryArbitrarily(new(primaries[0].EndPoint)); return primaries[0]; } @@ -1877,13 +1855,13 @@ private void ResetAllNonConnected() if (string.Equals(Format.ToString(servers[i].EndPoint), endpoint, StringComparison.OrdinalIgnoreCase)) return servers[i]; } - LogInformationCouldNotFindThatEndpoint(log); + log?.LogInformationCouldNotFindThatEndpoint(); var deDottedEndpoint = DeDotifyHost(endpoint); for (int i = 0; i < servers.Length; i++) { if (string.Equals(DeDotifyHost(Format.ToString(servers[i].EndPoint)), deDottedEndpoint, StringComparison.OrdinalIgnoreCase)) { - LogInformationFoundAlternativeEndpoint(log, deDottedEndpoint); + log?.LogInformationFoundAlternativeEndpoint(deDottedEndpoint); return servers[i]; } } @@ -2351,385 +2329,5 @@ private Task[] QuitAllServers() long? IInternalConnectionMultiplexer.GetConnectionId(EndPoint endpoint, ConnectionType type) => GetServerEndPoint(endpoint)?.GetBridge(type)?.ConnectionId; - - // Helper structs for complex ToString() calls - private readonly struct EndPointLogValue - { - private readonly EndPoint? endPoint; - - public EndPointLogValue(EndPoint? endPoint) - { - this.endPoint = endPoint; - } - - public override string ToString() => Format.ToString(endPoint); - } - - private readonly struct ServerEndPointLogValue - { - private readonly ServerEndPoint server; - - public ServerEndPointLogValue(ServerEndPoint server) - { - this.server = server; - } - - public override string ToString() => Format.ToString(server); - } - - // Generated LoggerMessage methods - [LoggerMessage( - Level = LogLevel.Error, - Message = "Connection failed: {EndPoint} ({ConnectionType}, {FailureType}): {ErrorMessage}")] - private static partial void LogErrorConnectionFailed(ILogger? logger, Exception? exception, EndPointLogValue endPoint, ConnectionType connectionType, ConnectionFailureType failureType, string errorMessage); - - [LoggerMessage( - Level = LogLevel.Error, - EventId = 1, - Message = "> {Message}")] - private static partial void LogErrorInnerException(ILogger? logger, Exception exception, string message); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 2, - Message = "Checking {EndPoint} is available...")] - private static partial void LogInformationCheckingServerAvailable(ILogger? logger, EndPointLogValue endPoint); - - [LoggerMessage( - Level = LogLevel.Error, - EventId = 3, - Message = "Operation failed on {EndPoint}, aborting: {ErrorMessage}")] - private static partial void LogErrorOperationFailedOnServer(ILogger? logger, Exception exception, EndPointLogValue endPoint, string errorMessage); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 4, - Message = "Attempting to set tie-breaker on {EndPoint}...")] - private static partial void LogInformationAttemptingToSetTieBreaker(ILogger? logger, EndPointLogValue endPoint); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 5, - Message = "Making {EndPoint} a primary...")] - private static partial void LogInformationMakingServerPrimary(ILogger? logger, EndPointLogValue endPoint); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 6, - Message = "Resending tie-breaker to {EndPoint}...")] - private static partial void LogInformationResendingTieBreaker(ILogger? logger, EndPointLogValue endPoint); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 7, - Message = "Broadcasting via {EndPoint}...")] - private static partial void LogInformationBroadcastingViaNode(ILogger? logger, EndPointLogValue endPoint); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 8, - Message = "Replicating to {EndPoint}...")] - private static partial void LogInformationReplicatingToNode(ILogger? logger, EndPointLogValue endPoint); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 9, - Message = "Reconfiguring all endpoints...")] - private static partial void LogInformationReconfiguringAllEndpoints(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 10, - Message = "Verifying the configuration was incomplete; please verify")] - private static partial void LogInformationVerifyingConfigurationIncomplete(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 11, - Message = "Connecting (async) on {Framework} (StackExchange.Redis: v{Version})")] - private static partial void LogInformationConnectingAsync(ILogger? logger, string framework, string version); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 12, - Message = "Connecting (sync) on {Framework} (StackExchange.Redis: v{Version})")] - private static partial void LogInformationConnectingSync(ILogger? logger, string framework, string version); - - [LoggerMessage( - Level = LogLevel.Error, - EventId = 13, - Message = "{ErrorMessage}")] - private static partial void LogErrorSyncConnectTimeout(ILogger? logger, Exception exception, string errorMessage); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 14, - Message = "{Message}")] - private static partial void LogInformationAfterConnect(ILogger? logger, string message); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 15, - Message = "Total connect time: {ElapsedMs:n0} ms")] - private static partial void LogInformationTotalConnectTime(ILogger? logger, long elapsedMs); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 16, - Message = "No tasks to await")] - private static partial void LogInformationNoTasksToAwait(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 17, - Message = "All tasks are already complete")] - private static partial void LogInformationAllTasksComplete(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 18, - Message = "{Message}", - SkipEnabledCheck = true)] - private static partial void LogInformationThreadPoolStats(ILogger? logger, string message); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 19, - Message = "Reconfiguration was already in progress due to: {ActiveCause}, attempted to run for: {NewCause}")] - private static partial void LogInformationReconfigurationInProgress(ILogger? logger, string? activeCause, string newCause); - - private readonly struct ConfigurationOptionsLogValue - { - private readonly ConfigurationOptions options; - - public ConfigurationOptionsLogValue(ConfigurationOptions options) - { - this.options = options; - } - - public override string ToString() => options.ToString(includePassword: false); - } - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 20, - Message = "{Configuration}")] - private static partial void LogInformationConfiguration(ILogger? logger, ConfigurationOptionsLogValue configuration); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 21, - Message = "{Count} unique nodes specified ({TieBreakerStatus} tiebreaker)")] - private static partial void LogInformationUniqueNodesSpecified(ILogger? logger, int count, string tieBreakerStatus); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 22, - Message = "Allowing {Count} endpoint(s) {TimeSpan} to respond...")] - private static partial void LogInformationAllowingEndpointsToRespond(ILogger? logger, int count, TimeSpan timeSpan); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 23, - Message = " Server[{Index}] ({Server}) Status: {Status} (inst: {MessagesSinceLastHeartbeat}, qs: {MessagesSentAwaitingResponse}, in: {BytesAvailableOnSocket}, qu: {MessagesSinceLastHeartbeat2}, aw: {IsWriterActive}, in-pipe: {BytesInReadPipe}, out-pipe: {BytesInWritePipe}, bw: {BacklogStatus}, rs: {ReadStatus}. ws: {WriteStatus})")] - private static partial void LogInformationServerStatus(ILogger? logger, int index, ServerEndPointLogValue server, TaskStatus status, long messagesSinceLastHeartbeat, long messagesSentAwaitingResponse, long bytesAvailableOnSocket, long messagesSinceLastHeartbeat2, bool isWriterActive, long bytesInReadPipe, long bytesInWritePipe, PhysicalBridge.BacklogStatus backlogStatus, PhysicalConnection.ReadStatus readStatus, PhysicalConnection.WriteStatus writeStatus); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 24, - Message = "Endpoint summary:")] - private static partial void LogInformationEndpointSummary(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 25, - Message = " {EndPoint}: Endpoint is (Interactive: {InteractiveState}, Subscription: {SubscriptionState})")] - private static partial void LogInformationEndpointState(ILogger? logger, EndPointLogValue endPoint, PhysicalBridge.State interactiveState, PhysicalBridge.State subscriptionState); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 26, - Message = "Task summary:")] - private static partial void LogInformationTaskSummary(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Error, - EventId = 27, - Message = " {Server}: Faulted: {ErrorMessage}")] - private static partial void LogErrorServerFaulted(ILogger? logger, Exception exception, ServerEndPointLogValue server, string errorMessage); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 28, - Message = " {Server}: Connect task canceled")] - private static partial void LogInformationConnectTaskCanceled(ILogger? logger, ServerEndPointLogValue server); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 29, - Message = " {Server}: Returned with success as {ServerType} {Role} (Source: {Source})")] - private static partial void LogInformationServerReturnedSuccess(ILogger? logger, ServerEndPointLogValue server, ServerType serverType, string role, string source); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 30, - Message = " {Server}: Returned, but incorrectly")] - private static partial void LogInformationServerReturnedIncorrectly(ILogger? logger, ServerEndPointLogValue server); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 31, - Message = " {Server}: Did not respond (Task.Status: {TaskStatus})")] - private static partial void LogInformationServerDidNotRespond(ILogger? logger, ServerEndPointLogValue server, TaskStatus taskStatus); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 32, - Message = "{EndPoint}: Clearing as RedundantPrimary")] - private static partial void LogInformationClearingAsRedundantPrimary(ILogger? logger, ServerEndPointLogValue endPoint); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 33, - Message = "{EndPoint}: Setting as RedundantPrimary")] - private static partial void LogInformationSettingAsRedundantPrimary(ILogger? logger, ServerEndPointLogValue endPoint); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 34, - Message = "Cluster: {CoveredSlots} of {TotalSlots} slots covered")] - private static partial void LogInformationClusterSlotsCovered(ILogger? logger, long coveredSlots, int totalSlots); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 35, - Message = "No subscription changes necessary")] - private static partial void LogInformationNoSubscriptionChanges(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 36, - Message = "Subscriptions attempting reconnect: {SubscriptionChanges}")] - private static partial void LogInformationSubscriptionsAttemptingReconnect(ILogger? logger, long subscriptionChanges); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 37, - Message = "{StormLog}")] - private static partial void LogInformationStormLog(ILogger? logger, string stormLog); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 38, - Message = "Resetting failing connections to retry...")] - private static partial void LogInformationResettingFailingConnections(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 39, - Message = " Retrying - attempts left: {AttemptsLeft}...")] - private static partial void LogInformationRetryingAttempts(ILogger? logger, int attemptsLeft); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 40, - Message = "Starting heartbeat...")] - private static partial void LogInformationStartingHeartbeat(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 41, - Message = "Broadcasting reconfigure...")] - private static partial void LogInformationBroadcastingReconfigure(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Error, - EventId = 42, - Message = "Encountered error while updating cluster config: {ErrorMessage}")] - private static partial void LogErrorEncounteredErrorWhileUpdatingClusterConfig(ILogger? logger, Exception exception, string errorMessage); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 43, - Message = "Election summary:")] - private static partial void LogInformationElectionSummary(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 44, - Message = " Election: {Server} had no tiebreaker set")] - private static partial void LogInformationElectionNoTiebreaker(ILogger? logger, ServerEndPointLogValue server); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 45, - Message = " Election: {Server} nominates: {ServerResult}")] - private static partial void LogInformationElectionNominates(ILogger? logger, ServerEndPointLogValue server, string serverResult); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 46, - Message = " Election: No primaries detected")] - private static partial void LogInformationElectionNoPrimariesDetected(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 47, - Message = " Election: Single primary detected: {EndPoint}")] - private static partial void LogInformationElectionSinglePrimaryDetected(ILogger? logger, EndPointLogValue endPoint); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 48, - Message = " Election: Multiple primaries detected...")] - private static partial void LogInformationElectionMultiplePrimariesDetected(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 49, - Message = " Election: No nominations by tie-breaker")] - private static partial void LogInformationElectionNoNominationsByTieBreaker(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 50, - Message = " Election: Tie-breaker unanimous: {Unanimous}")] - private static partial void LogInformationElectionTieBreakerUnanimous(ILogger? logger, string unanimous); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 51, - Message = " Election: Elected: {EndPoint}")] - private static partial void LogInformationElectionElected(ILogger? logger, EndPointLogValue endPoint); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 52, - Message = " Election is contested:")] - private static partial void LogInformationElectionContested(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 53, - Message = " Election: {Key} has {Value} votes")] - private static partial void LogInformationElectionVotes(ILogger? logger, string key, int value); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 54, - Message = " Election: Choosing primary arbitrarily: {EndPoint}")] - private static partial void LogInformationElectionChoosingPrimaryArbitrarily(ILogger? logger, EndPointLogValue endPoint); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 55, - Message = "...but we couldn't find that")] - private static partial void LogInformationCouldNotFindThatEndpoint(ILogger? logger); - - [LoggerMessage( - Level = LogLevel.Information, - EventId = 56, - Message = "...but we did find instead: {DeDottedEndpoint}")] - private static partial void LogInformationFoundAlternativeEndpoint(ILogger? logger, string deDottedEndpoint); } } diff --git a/src/StackExchange.Redis/LoggerExtensions.cs b/src/StackExchange.Redis/LoggerExtensions.cs new file mode 100644 index 000000000..af57264bb --- /dev/null +++ b/src/StackExchange.Redis/LoggerExtensions.cs @@ -0,0 +1,406 @@ +using System; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace StackExchange.Redis; + +internal static partial class LoggerExtensions +{ + // Helper structs for complex ToString() calls + internal readonly struct EndPointLogValue(EndPoint? endpoint) + { + public override string ToString() => Format.ToString(endpoint); + } + + internal readonly struct ServerEndPointLogValue(ServerEndPoint server) + { + public override string ToString() => Format.ToString(server); + } + + internal readonly struct ConfigurationOptionsLogValue(ConfigurationOptions options) + { + public override string ToString() => options.ToString(includePassword: false); + } + + // manual extensions + internal static void LogWithThreadPoolStats(this ILogger? log, string message) + { + if (log is null || !log.IsEnabled(LogLevel.Information)) + { + return; + } + + _ = PerfCounterHelper.GetThreadPoolStats(out string iocp, out string worker, out string? workItems); + +#if NET6_0_OR_GREATER + // use DISH when possible + // similar to: var composed = $"{message}, IOCP: {iocp}, WORKER: {worker}, ..."; on net6+ + var dish = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(26, 4); + dish.AppendFormatted(message); + dish.AppendLiteral(", IOCP: "); + dish.AppendFormatted(iocp); + dish.AppendLiteral(", WORKER: "); + dish.AppendFormatted(worker); + if (workItems is not null) + { + dish.AppendLiteral(", POOL: "); + dish.AppendFormatted(workItems); + } + var composed = dish.ToStringAndClear(); +#else + var sb = new StringBuilder(); + sb.Append(message).Append(", IOCP: ").Append(iocp).Append(", WORKER: ").Append(worker); + if (workItems is not null) + { + sb.Append(", POOL: ").Append(workItems); + } + var composed = sb.ToString(); +#endif + log.LogInformationThreadPoolStats(composed); + } + + // Generated LoggerMessage methods + [LoggerMessage( + Level = LogLevel.Error, + Message = "Connection failed: {EndPoint} ({ConnectionType}, {FailureType}): {ErrorMessage}")] + internal static partial void LogErrorConnectionFailed(this ILogger logger, Exception? exception, EndPointLogValue endPoint, ConnectionType connectionType, ConnectionFailureType failureType, string errorMessage); + + [LoggerMessage( + Level = LogLevel.Error, + EventId = 1, + Message = "> {Message}")] + internal static partial void LogErrorInnerException(this ILogger logger, Exception exception, string message); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 2, + Message = "Checking {EndPoint} is available...")] + internal static partial void LogInformationCheckingServerAvailable(this ILogger logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Error, + EventId = 3, + Message = "Operation failed on {EndPoint}, aborting: {ErrorMessage}")] + internal static partial void LogErrorOperationFailedOnServer(this ILogger logger, Exception exception, EndPointLogValue endPoint, string errorMessage); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 4, + Message = "Attempting to set tie-breaker on {EndPoint}...")] + internal static partial void LogInformationAttemptingToSetTieBreaker(this ILogger logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 5, + Message = "Making {EndPoint} a primary...")] + internal static partial void LogInformationMakingServerPrimary(this ILogger logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 6, + Message = "Resending tie-breaker to {EndPoint}...")] + internal static partial void LogInformationResendingTieBreaker(this ILogger logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 7, + Message = "Broadcasting via {EndPoint}...")] + internal static partial void LogInformationBroadcastingViaNode(this ILogger logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 8, + Message = "Replicating to {EndPoint}...")] + internal static partial void LogInformationReplicatingToNode(this ILogger logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 9, + Message = "Reconfiguring all endpoints...")] + internal static partial void LogInformationReconfiguringAllEndpoints(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 10, + Message = "Verifying the configuration was incomplete; please verify")] + internal static partial void LogInformationVerifyingConfigurationIncomplete(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 11, + Message = "Connecting (async) on {Framework} (StackExchange.Redis: v{Version})")] + internal static partial void LogInformationConnectingAsync(this ILogger logger, string framework, string version); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 12, + Message = "Connecting (sync) on {Framework} (StackExchange.Redis: v{Version})")] + internal static partial void LogInformationConnectingSync(this ILogger logger, string framework, string version); + + [LoggerMessage( + Level = LogLevel.Error, + EventId = 13, + Message = "{ErrorMessage}")] + internal static partial void LogErrorSyncConnectTimeout(this ILogger logger, Exception exception, string errorMessage); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 14, + Message = "{Message}")] + internal static partial void LogInformationAfterConnect(this ILogger logger, string message); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 15, + Message = "Total connect time: {ElapsedMs:n0} ms")] + internal static partial void LogInformationTotalConnectTime(this ILogger logger, long elapsedMs); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 16, + Message = "No tasks to await")] + internal static partial void LogInformationNoTasksToAwait(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 17, + Message = "All tasks are already complete")] + internal static partial void LogInformationAllTasksComplete(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 18, + Message = "{Message}", + SkipEnabledCheck = true)] + internal static partial void LogInformationThreadPoolStats(this ILogger logger, string message); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 19, + Message = "Reconfiguration was already in progress due to: {ActiveCause}, attempted to run for: {NewCause}")] + internal static partial void LogInformationReconfigurationInProgress(this ILogger logger, string? activeCause, string newCause); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 20, + Message = "{Configuration}")] + internal static partial void LogInformationConfiguration(this ILogger logger, ConfigurationOptionsLogValue configuration); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 21, + Message = "{Count} unique nodes specified ({TieBreakerStatus} tiebreaker)")] + internal static partial void LogInformationUniqueNodesSpecified(this ILogger logger, int count, string tieBreakerStatus); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 22, + Message = "Allowing {Count} endpoint(s) {TimeSpan} to respond...")] + internal static partial void LogInformationAllowingEndpointsToRespond(this ILogger logger, int count, TimeSpan timeSpan); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 23, + Message = " Server[{Index}] ({Server}) Status: {Status} (inst: {MessagesSinceLastHeartbeat}, qs: {MessagesSentAwaitingResponse}, in: {BytesAvailableOnSocket}, qu: {MessagesSinceLastHeartbeat2}, aw: {IsWriterActive}, in-pipe: {BytesInReadPipe}, out-pipe: {BytesInWritePipe}, bw: {BacklogStatus}, rs: {ReadStatus}. ws: {WriteStatus})")] + internal static partial void LogInformationServerStatus(this ILogger logger, int index, ServerEndPointLogValue server, TaskStatus status, long messagesSinceLastHeartbeat, long messagesSentAwaitingResponse, long bytesAvailableOnSocket, long messagesSinceLastHeartbeat2, bool isWriterActive, long bytesInReadPipe, long bytesInWritePipe, PhysicalBridge.BacklogStatus backlogStatus, PhysicalConnection.ReadStatus readStatus, PhysicalConnection.WriteStatus writeStatus); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 24, + Message = "Endpoint summary:")] + internal static partial void LogInformationEndpointSummary(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 25, + Message = " {EndPoint}: Endpoint is (Interactive: {InteractiveState}, Subscription: {SubscriptionState})")] + internal static partial void LogInformationEndpointState(this ILogger logger, EndPointLogValue endPoint, PhysicalBridge.State interactiveState, PhysicalBridge.State subscriptionState); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 26, + Message = "Task summary:")] + internal static partial void LogInformationTaskSummary(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Error, + EventId = 27, + Message = " {Server}: Faulted: {ErrorMessage}")] + internal static partial void LogErrorServerFaulted(this ILogger logger, Exception exception, ServerEndPointLogValue server, string errorMessage); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 28, + Message = " {Server}: Connect task canceled")] + internal static partial void LogInformationConnectTaskCanceled(this ILogger logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 29, + Message = " {Server}: Returned with success as {ServerType} {Role} (Source: {Source})")] + internal static partial void LogInformationServerReturnedSuccess(this ILogger logger, ServerEndPointLogValue server, ServerType serverType, string role, string source); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 30, + Message = " {Server}: Returned, but incorrectly")] + internal static partial void LogInformationServerReturnedIncorrectly(this ILogger logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 31, + Message = " {Server}: Did not respond (Task.Status: {TaskStatus})")] + internal static partial void LogInformationServerDidNotRespond(this ILogger logger, ServerEndPointLogValue server, TaskStatus taskStatus); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 32, + Message = "{EndPoint}: Clearing as RedundantPrimary")] + internal static partial void LogInformationClearingAsRedundantPrimary(this ILogger logger, ServerEndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 33, + Message = "{EndPoint}: Setting as RedundantPrimary")] + internal static partial void LogInformationSettingAsRedundantPrimary(this ILogger logger, ServerEndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 34, + Message = "Cluster: {CoveredSlots} of {TotalSlots} slots covered")] + internal static partial void LogInformationClusterSlotsCovered(this ILogger logger, long coveredSlots, int totalSlots); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 35, + Message = "No subscription changes necessary")] + internal static partial void LogInformationNoSubscriptionChanges(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 36, + Message = "Subscriptions attempting reconnect: {SubscriptionChanges}")] + internal static partial void LogInformationSubscriptionsAttemptingReconnect(this ILogger logger, long subscriptionChanges); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 37, + Message = "{StormLog}")] + internal static partial void LogInformationStormLog(this ILogger logger, string stormLog); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 38, + Message = "Resetting failing connections to retry...")] + internal static partial void LogInformationResettingFailingConnections(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 39, + Message = " Retrying - attempts left: {AttemptsLeft}...")] + internal static partial void LogInformationRetryingAttempts(this ILogger logger, int attemptsLeft); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 40, + Message = "Starting heartbeat...")] + internal static partial void LogInformationStartingHeartbeat(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 41, + Message = "Broadcasting reconfigure...")] + internal static partial void LogInformationBroadcastingReconfigure(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Error, + EventId = 42, + Message = "Encountered error while updating cluster config: {ErrorMessage}")] + internal static partial void LogErrorEncounteredErrorWhileUpdatingClusterConfig(this ILogger logger, Exception exception, string errorMessage); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 43, + Message = "Election summary:")] + internal static partial void LogInformationElectionSummary(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 44, + Message = " Election: {Server} had no tiebreaker set")] + internal static partial void LogInformationElectionNoTiebreaker(this ILogger logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 45, + Message = " Election: {Server} nominates: {ServerResult}")] + internal static partial void LogInformationElectionNominates(this ILogger logger, ServerEndPointLogValue server, string serverResult); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 46, + Message = " Election: No primaries detected")] + internal static partial void LogInformationElectionNoPrimariesDetected(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 47, + Message = " Election: Single primary detected: {EndPoint}")] + internal static partial void LogInformationElectionSinglePrimaryDetected(this ILogger logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 48, + Message = " Election: Multiple primaries detected...")] + internal static partial void LogInformationElectionMultiplePrimariesDetected(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 49, + Message = " Election: No nominations by tie-breaker")] + internal static partial void LogInformationElectionNoNominationsByTieBreaker(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 50, + Message = " Election: Tie-breaker unanimous: {Unanimous}")] + internal static partial void LogInformationElectionTieBreakerUnanimous(this ILogger logger, string unanimous); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 51, + Message = " Election: Elected: {EndPoint}")] + internal static partial void LogInformationElectionElected(this ILogger logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 52, + Message = " Election is contested:")] + internal static partial void LogInformationElectionContested(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 53, + Message = " Election: {Key} has {Value} votes")] + internal static partial void LogInformationElectionVotes(this ILogger logger, string key, int value); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 54, + Message = " Election: Choosing primary arbitrarily: {EndPoint}")] + internal static partial void LogInformationElectionChoosingPrimaryArbitrarily(this ILogger logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 55, + Message = "...but we couldn't find that")] + internal static partial void LogInformationCouldNotFindThatEndpoint(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 56, + Message = "...but we did find instead: {DeDottedEndpoint}")] + internal static partial void LogInformationFoundAlternativeEndpoint(this ILogger logger, string deDottedEndpoint); +} From 0b4fd453aab6f960a5ceee7cc0a61d05c6518f8e Mon Sep 17 00:00:00 2001 From: ArnoKoll <97118098+ArnoKoll@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:30:44 +0200 Subject: [PATCH 345/435] fix-zrevrangebylex (#2636) --- src/StackExchange.Redis/RedisDatabase.cs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 5493f3ebd..fb1f6cc65 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -3668,12 +3668,15 @@ private Message GetSortedSetMultiPopMessage(RedisKey[] keys, Order order, long c return tran; } - private static RedisValue GetLexRange(RedisValue value, Exclude exclude, bool isStart) + private static RedisValue GetLexRange(RedisValue value, Exclude exclude, bool isStart, Order order) { if (value.IsNull) { - return isStart ? RedisLiterals.MinusSymbol : RedisLiterals.PlusSymbol; + if (order == Order.Ascending) return isStart ? RedisLiterals.MinusSymbol : RedisLiterals.PlusSymbol; + + return isStart ? RedisLiterals.PlusSymbol : RedisLiterals.MinusSymbol; // 24.01.2024: when descending order: Plus and Minus have to be reversed } + byte[] orig = value!; byte[] result = new byte[orig.Length + 1]; @@ -4846,9 +4849,9 @@ private Message GetStringSetAndGetMessage( return new ScanEnumerable(this, server, key, pattern, pageSize, cursor, pageOffset, flags, command, processor, noValues); } - private Message GetLexMessage(RedisCommand command, RedisKey key, RedisValue min, RedisValue max, Exclude exclude, long skip, long take, CommandFlags flags) + private Message GetLexMessage(RedisCommand command, RedisKey key, RedisValue min, RedisValue max, Exclude exclude, long skip, long take, CommandFlags flags, Order order) { - RedisValue start = GetLexRange(min, exclude, true), stop = GetLexRange(max, exclude, false); + RedisValue start = GetLexRange(min, exclude, true, order), stop = GetLexRange(max, exclude, false, order); if (skip == 0 && take == -1) return Message.Create(Database, flags, command, key, start, stop); @@ -4858,7 +4861,7 @@ private Message GetLexMessage(RedisCommand command, RedisKey key, RedisValue min public long SortedSetLengthByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) { - var msg = GetLexMessage(RedisCommand.ZLEXCOUNT, key, min, max, exclude, 0, -1, flags); + var msg = GetLexMessage(RedisCommand.ZLEXCOUNT, key, min, max, exclude, 0, -1, flags, Order.Ascending); return ExecuteSync(msg, ResultProcessor.Int64); } @@ -4891,19 +4894,19 @@ public RedisValue[] SortedSetRangeByValue( CommandFlags flags = CommandFlags.None) { ReverseLimits(order, ref exclude, ref min, ref max); - var msg = GetLexMessage(order == Order.Ascending ? RedisCommand.ZRANGEBYLEX : RedisCommand.ZREVRANGEBYLEX, key, min, max, exclude, skip, take, flags); + var msg = GetLexMessage(order == Order.Ascending ? RedisCommand.ZRANGEBYLEX : RedisCommand.ZREVRANGEBYLEX, key, min, max, exclude, skip, take, flags, order); return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public long SortedSetRemoveRangeByValue(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) { - var msg = GetLexMessage(RedisCommand.ZREMRANGEBYLEX, key, min, max, exclude, 0, -1, flags); + var msg = GetLexMessage(RedisCommand.ZREMRANGEBYLEX, key, min, max, exclude, 0, -1, flags, Order.Ascending); return ExecuteSync(msg, ResultProcessor.Int64); } public Task SortedSetLengthByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) { - var msg = GetLexMessage(RedisCommand.ZLEXCOUNT, key, min, max, exclude, 0, -1, flags); + var msg = GetLexMessage(RedisCommand.ZLEXCOUNT, key, min, max, exclude, 0, -1, flags, Order.Ascending); return ExecuteAsync(msg, ResultProcessor.Int64); } @@ -4921,13 +4924,13 @@ public Task SortedSetRangeByValueAsync( CommandFlags flags = CommandFlags.None) { ReverseLimits(order, ref exclude, ref min, ref max); - var msg = GetLexMessage(order == Order.Ascending ? RedisCommand.ZRANGEBYLEX : RedisCommand.ZREVRANGEBYLEX, key, min, max, exclude, skip, take, flags); + var msg = GetLexMessage(order == Order.Ascending ? RedisCommand.ZRANGEBYLEX : RedisCommand.ZREVRANGEBYLEX, key, min, max, exclude, skip, take, flags, order); return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } public Task SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min, RedisValue max, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) { - var msg = GetLexMessage(RedisCommand.ZREMRANGEBYLEX, key, min, max, exclude, 0, -1, flags); + var msg = GetLexMessage(RedisCommand.ZREMRANGEBYLEX, key, min, max, exclude, 0, -1, flags, Order.Ascending); return ExecuteAsync(msg, ResultProcessor.Int64); } From dcae5ddd72c0c8fd1f68b0881577f12824bdd934 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 21 Jul 2025 11:34:57 +0100 Subject: [PATCH 346/435] Fix #2679: connect/config: only access task.Result if we know it completed (#2680) * connect/config: only access task.Result if we know it completed * cite PR * typo --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/ConnectionMultiplexer.cs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 22dd36256..eb176e6e7 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,7 @@ Current package versions: ## Unreleased +- Fix [#2679](https://github.com/StackExchange/StackExchange.Redis/issues/2679) - blocking call in long-running connects ([#2680 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2680)) - Support async cancellation of `SCAN` enumeration ([#2911 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2911)) - Add `XTRIM MINID` support ([#2842 by kijanawoodard](https://github.com/StackExchange/StackExchange.Redis/pull/2842)) - Add new CE 8.2 stream support - `XDELEX`, `XACKDEL`, `{XADD|XTRIM} [KEEPREF|DELREF|ACKED]` ([#2912 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2912)) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index de3e8d92e..146f7d2d3 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -681,8 +681,17 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat // note that task has timeouts internally, so it might take *just over* the regular timeout var task = muxer.ReconfigureAsync(first: true, reconfigureAll: false, log, null, "connect"); - if (!task.Wait(muxer.SyncConnectTimeout(true))) + if (task.Wait(muxer.SyncConnectTimeout(true))) { + // completed promptly - we can check the outcome; hard failures + // (such as password problems) should be reported promptly - it + // won't magically start working + if (!task.Result) throw ExceptionFactory.UnableToConnect(muxer, muxer.failureMessage); + } + else + { + // incomplete - most likely slow initial connection; optionally + // allow a soft failure mode task.ObserveErrors(); if (muxer.RawConfig.AbortOnConnectFail) { @@ -696,7 +705,6 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat } } - if (!task.Result) throw ExceptionFactory.UnableToConnect(muxer, muxer.failureMessage); killMe = null; Interlocked.Increment(ref muxer._connectCompletedCount); From 4c70460a0d2f609896865be884f3448aabe95e97 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 21 Jul 2025 11:46:01 +0100 Subject: [PATCH 347/435] update release notes --- docs/ReleaseNotes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index eb176e6e7..19a83f1c7 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -12,6 +12,12 @@ Current package versions: - Support async cancellation of `SCAN` enumeration ([#2911 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2911)) - Add `XTRIM MINID` support ([#2842 by kijanawoodard](https://github.com/StackExchange/StackExchange.Redis/pull/2842)) - Add new CE 8.2 stream support - `XDELEX`, `XACKDEL`, `{XADD|XTRIM} [KEEPREF|DELREF|ACKED]` ([#2912 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2912)) +- Fix `ZREVRANGEBYLEX` open-ended commands ([#2636 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2636)) +- Fix `StreamGroupInfo.Lag` when `null` ([#2902 by robhop](https://github.com/StackExchange/StackExchange.Redis/pull/2902)) +- Internals + - Logging improvements ([#2903 by Meir017](https://github.com/StackExchange/StackExchange.Redis/pull/2903) and [#2917 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2917)) + - Update tests to xUnit v3 ([#2907 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2907)) + - Avoid `CLIENT PAUSE` in CI tests ([#2916 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2916)) ## 2.8.47 From c364806f6eb8d21295abcf9c49b0da7881d4903b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 21 Jul 2025 13:24:06 +0100 Subject: [PATCH 348/435] update version in release notes --- docs/ReleaseNotes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 19a83f1c7..758e0a30f 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,10 @@ Current package versions: ## Unreleased +- nothing yet + +## 2.8.58 + - Fix [#2679](https://github.com/StackExchange/StackExchange.Redis/issues/2679) - blocking call in long-running connects ([#2680 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2680)) - Support async cancellation of `SCAN` enumeration ([#2911 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2911)) - Add `XTRIM MINID` support ([#2842 by kijanawoodard](https://github.com/StackExchange/StackExchange.Redis/pull/2842)) From 265cc12adb2351b46415778e1bbbf9ecae685697 Mon Sep 17 00:00:00 2001 From: Henrik Date: Wed, 23 Jul 2025 00:11:34 +0200 Subject: [PATCH 349/435] Make private & internal stuff sealed (#2915) --- .../APITypes/GeoSearchShape.cs | 8 +++---- src/StackExchange.Redis/ClientInfo.cs | 2 +- src/StackExchange.Redis/CommandTrace.cs | 2 +- src/StackExchange.Redis/Condition.cs | 24 +++++++++---------- .../Configuration/LoggingTunnel.cs | 2 +- src/StackExchange.Redis/CursorEnumerable.cs | 2 +- src/StackExchange.Redis/Message.cs | 2 +- src/StackExchange.Redis/RedisBatch.cs | 2 +- src/StackExchange.Redis/RedisDatabase.cs | 6 ++--- src/StackExchange.Redis/RedisServer.cs | 2 +- src/StackExchange.Redis/RedisTransaction.cs | 10 ++++---- src/StackExchange.Redis/ResultProcessor.cs | 14 +++++------ 12 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/StackExchange.Redis/APITypes/GeoSearchShape.cs b/src/StackExchange.Redis/APITypes/GeoSearchShape.cs index 7d85c3bfa..f2879ccc1 100644 --- a/src/StackExchange.Redis/APITypes/GeoSearchShape.cs +++ b/src/StackExchange.Redis/APITypes/GeoSearchShape.cs @@ -46,12 +46,12 @@ public GeoSearchCircle(double radius, GeoUnit unit = GeoUnit.Meters) : base(unit _radius = radius; } - internal override int ArgCount => 3; + internal sealed override int ArgCount => 3; /// /// Gets the s for this shape. /// - internal override void AddArgs(List args) + internal sealed override void AddArgs(List args) { args.Add(RedisLiterals.BYRADIUS); args.Add(_radius); @@ -80,9 +80,9 @@ public GeoSearchBox(double height, double width, GeoUnit unit = GeoUnit.Meters) _width = width; } - internal override int ArgCount => 4; + internal sealed override int ArgCount => 4; - internal override void AddArgs(List args) + internal sealed override void AddArgs(List args) { args.Add(RedisLiterals.BYBOX); args.Add(_width); diff --git a/src/StackExchange.Redis/ClientInfo.cs b/src/StackExchange.Redis/ClientInfo.cs index c5ce0d0bf..d743affff 100644 --- a/src/StackExchange.Redis/ClientInfo.cs +++ b/src/StackExchange.Redis/ClientInfo.cs @@ -287,7 +287,7 @@ private static void AddFlag(ref ClientFlags value, string raw, ClientFlags toAdd if (raw.IndexOf(token) >= 0) value |= toAdd; } - private class ClientInfoProcessor : ResultProcessor + private sealed class ClientInfoProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { diff --git a/src/StackExchange.Redis/CommandTrace.cs b/src/StackExchange.Redis/CommandTrace.cs index aedd05fea..a61499f0c 100644 --- a/src/StackExchange.Redis/CommandTrace.cs +++ b/src/StackExchange.Redis/CommandTrace.cs @@ -69,7 +69,7 @@ internal CommandTrace(long uniqueId, long time, long duration, RedisValue[] argu return BaseUrl + encoded0; } - private class CommandTraceProcessor : ResultProcessor + private sealed class CommandTraceProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs index 308c87c11..19e8b2863 100644 --- a/src/StackExchange.Redis/Condition.cs +++ b/src/StackExchange.Redis/Condition.cs @@ -384,7 +384,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return false; } - private class ConditionMessage : Message.CommandKeyBase + private sealed class ConditionMessage : Message.CommandKeyBase { public readonly Condition Condition; private readonly RedisValue value; @@ -425,7 +425,7 @@ protected override void WriteImpl(PhysicalConnection physical) } } - internal class ExistsCondition : Condition + internal sealed class ExistsCondition : Condition { private readonly bool expectedResult; private readonly RedisValue expectedValue; @@ -501,7 +501,7 @@ internal override bool TryValidate(in RawResult result, out bool value) } } - internal class EqualsCondition : Condition + internal sealed class EqualsCondition : Condition { internal override Condition MapKeys(Func map) => new EqualsCondition(map(key), type, memberName, expectedEqual, expectedValue); @@ -535,7 +535,7 @@ public override string ToString() => internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(cmd); - internal sealed override IEnumerable CreateMessages(int db, IResultBox? resultBox) + internal override IEnumerable CreateMessages(int db, IResultBox? resultBox) { yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key); @@ -580,7 +580,7 @@ internal override bool TryValidate(in RawResult result, out bool value) } } - internal class ListCondition : Condition + internal sealed class ListCondition : Condition { internal override Condition MapKeys(Func map) => new ListCondition(map(key), index, expectedResult, expectedValue); @@ -606,7 +606,7 @@ public override string ToString() => internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.LINDEX); - internal sealed override IEnumerable CreateMessages(int db, IResultBox? resultBox) + internal override IEnumerable CreateMessages(int db, IResultBox? resultBox) { yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key); @@ -643,7 +643,7 @@ internal override bool TryValidate(in RawResult result, out bool value) } } - internal class LengthCondition : Condition + internal sealed class LengthCondition : Condition { internal override Condition MapKeys(Func map) => new LengthCondition(map(key), type, compareToResult, expectedLength); @@ -679,7 +679,7 @@ public LengthCondition(in RedisKey key, RedisType type, int compareToResult, lon internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(cmd); - internal sealed override IEnumerable CreateMessages(int db, IResultBox? resultBox) + internal override IEnumerable CreateMessages(int db, IResultBox? resultBox) { yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key); @@ -708,7 +708,7 @@ internal override bool TryValidate(in RawResult result, out bool value) } } - internal class SortedSetRangeLengthCondition : Condition + internal sealed class SortedSetRangeLengthCondition : Condition { internal override Condition MapKeys(Func map) => new SortedSetRangeLengthCondition(map(key), min, max, compareToResult, expectedLength); @@ -736,7 +736,7 @@ public override string ToString() => internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.ZCOUNT); - internal sealed override IEnumerable CreateMessages(int db, IResultBox? resultBox) + internal override IEnumerable CreateMessages(int db, IResultBox? resultBox) { yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key); @@ -765,7 +765,7 @@ internal override bool TryValidate(in RawResult result, out bool value) } } - internal class SortedSetScoreCondition : Condition + internal sealed class SortedSetScoreCondition : Condition { internal override Condition MapKeys(Func map) => new SortedSetScoreCondition(map(key), sortedSetScore, expectedEqual, expectedValue); @@ -792,7 +792,7 @@ public override string ToString() => internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.ZCOUNT); - internal sealed override IEnumerable CreateMessages(int db, IResultBox? resultBox) + internal override IEnumerable CreateMessages(int db, IResultBox? resultBox) { yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key); diff --git a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs index d61442071..0eca972b8 100644 --- a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs +++ b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs @@ -277,7 +277,7 @@ public static void LogToDirectory(ConfigurationOptions options, string path) options.Tunnel = tunnel; } - private class DirectoryLoggingTunnel : LoggingTunnel + private sealed class DirectoryLoggingTunnel : LoggingTunnel { private readonly string path; private int _nextIndex = -1; diff --git a/src/StackExchange.Redis/CursorEnumerable.cs b/src/StackExchange.Redis/CursorEnumerable.cs index e526eceaa..921d83ce0 100644 --- a/src/StackExchange.Redis/CursorEnumerable.cs +++ b/src/StackExchange.Redis/CursorEnumerable.cs @@ -344,7 +344,7 @@ public void Reset() internal static CursorEnumerable From(RedisBase redis, ServerEndPoint? server, Task pending, int pageOffset) => new SingleBlockEnumerable(redis, server, pending, pageOffset); - private class SingleBlockEnumerable : CursorEnumerable + private sealed class SingleBlockEnumerable : CursorEnumerable { private readonly Task _pending; public SingleBlockEnumerable(RedisBase redis, ServerEndPoint? server, Task pending, int pageOffset) diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index fd75585a5..9472b6db0 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -1627,7 +1627,7 @@ protected override void WriteImpl(PhysicalConnection physical) public override int ArgCount => 0; } - private class CommandSlotValuesMessage : Message + private sealed class CommandSlotValuesMessage : Message { private readonly int slot; private readonly RedisValue[] values; diff --git a/src/StackExchange.Redis/RedisBatch.cs b/src/StackExchange.Redis/RedisBatch.cs index 0a4c888f2..0ef97f365 100644 --- a/src/StackExchange.Redis/RedisBatch.cs +++ b/src/StackExchange.Redis/RedisBatch.cs @@ -4,7 +4,7 @@ namespace StackExchange.Redis { - internal class RedisBatch : RedisDatabase, IBatch + internal sealed class RedisBatch : RedisDatabase, IBatch { private List? pending; diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index fb1f6cc65..651d1b4a3 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -4934,7 +4934,7 @@ public Task SortedSetRemoveRangeByValueAsync(RedisKey key, RedisValue min, return ExecuteAsync(msg, ResultProcessor.Int64); } - internal class ScanEnumerable : CursorEnumerable + internal sealed class ScanEnumerable : CursorEnumerable { private readonly RedisKey key; private readonly RedisValue pattern; @@ -5327,7 +5327,7 @@ private SortedSetScanResultProcessor() { } => SortedSetWithScores.TryParse(result, out SortedSetEntry[]? pairs, true, out count) ? pairs : null; } - private class StringGetWithExpiryMessage : Message.CommandKeyBase, IMultiMessage + private sealed class StringGetWithExpiryMessage : Message.CommandKeyBase, IMultiMessage { private readonly RedisCommand ttlCommand; private IResultBox? box; @@ -5371,7 +5371,7 @@ protected override void WriteImpl(PhysicalConnection physical) public override int ArgCount => 1; } - private class StringGetWithExpiryProcessor : ResultProcessor + private sealed class StringGetWithExpiryProcessor : ResultProcessor { public static readonly ResultProcessor Default = new StringGetWithExpiryProcessor(); private StringGetWithExpiryProcessor() { } diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 8810e1e2b..af734b0f5 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -889,7 +889,7 @@ private protected override Message CreateMessage(in RedisValue cursor) private protected override ResultProcessor Processor => processor; public static readonly ResultProcessor processor = new ScanResultProcessor(); - private class ScanResultProcessor : ResultProcessor + private sealed class ScanResultProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { diff --git a/src/StackExchange.Redis/RedisTransaction.cs b/src/StackExchange.Redis/RedisTransaction.cs index 04d7293ac..f0a9600fa 100644 --- a/src/StackExchange.Redis/RedisTransaction.cs +++ b/src/StackExchange.Redis/RedisTransaction.cs @@ -6,7 +6,7 @@ namespace StackExchange.Redis { - internal class RedisTransaction : RedisDatabase, ITransaction + internal sealed class RedisTransaction : RedisDatabase, ITransaction { private List? _conditions; private List? _pending; @@ -169,7 +169,7 @@ private void QueueMessage(Message message) return new TransactionMessage(Database, flags, cond, work); } - private class QueuedMessage : Message + private sealed class QueuedMessage : Message { public Message Wrapped { get; } private volatile bool wasQueued; @@ -197,7 +197,7 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => Wrapped.GetHashSlot(serverSelectionStrategy); } - private class QueuedProcessor : ResultProcessor + private sealed class QueuedProcessor : ResultProcessor { public static readonly ResultProcessor Default = new QueuedProcessor(); @@ -216,7 +216,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - private class TransactionMessage : Message, IMultiMessage + private sealed class TransactionMessage : Message, IMultiMessage { private readonly ConditionResult[] conditions; @@ -465,7 +465,7 @@ private bool AreAllConditionsSatisfied(ConnectionMultiplexer multiplexer) } } - private class TransactionProcessor : ResultProcessor + private sealed class TransactionProcessor : ResultProcessor { public static readonly TransactionProcessor Default = new(); diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 294d1f03b..67dd73173 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -1336,7 +1336,7 @@ private static string Normalize(string? category) => category.IsNullOrWhiteSpace() ? "miscellaneous" : category.Trim(); } - private class Int64DefaultValueProcessor : ResultProcessor + private sealed class Int64DefaultValueProcessor : ResultProcessor { private readonly long _defaultValue; @@ -1384,7 +1384,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes internal static ResultProcessor StreamTrimResultArray => Int32EnumArrayProcessor.Instance; - private class Int32EnumProcessor : ResultProcessor where T : unmanaged, Enum + private sealed class Int32EnumProcessor : ResultProcessor where T : unmanaged, Enum { private Int32EnumProcessor() { } public static readonly Int32EnumProcessor Instance = new(); @@ -1418,7 +1418,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - private class Int32EnumArrayProcessor : ResultProcessor where T : unmanaged, Enum + private sealed class Int32EnumArrayProcessor : ResultProcessor where T : unmanaged, Enum { private Int32EnumArrayProcessor() { } public static readonly Int32EnumArrayProcessor Instance = new(); @@ -1449,7 +1449,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - private class PubSubNumSubProcessor : Int64Processor + private sealed class PubSubNumSubProcessor : Int64Processor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { @@ -2049,7 +2049,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - private class ScriptResultProcessor : ResultProcessor + private sealed class ScriptResultProcessor : ResultProcessor { public override bool SetResult(PhysicalConnection connection, Message message, in RawResult result) { @@ -2632,7 +2632,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - internal class StreamNameValueEntryProcessor : ValuePairInterleavedProcessorBase + internal sealed class StreamNameValueEntryProcessor : ValuePairInterleavedProcessorBase { public static readonly StreamNameValueEntryProcessor Instance = new(); private StreamNameValueEntryProcessor() @@ -2747,7 +2747,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } - private class TracerProcessor : ResultProcessor + private sealed class TracerProcessor : ResultProcessor { private readonly bool establishConnection; From c07aec93d8ec2e58941ed890359a880c3a2157dc Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 23 Jul 2025 13:24:17 +0100 Subject: [PATCH 350/435] workaround Mono test failures: (#2921) - acknowledge enum tostring delta (skip check) - acknowledge limitation in cert handling (avoid fault, report as rejection) --- StackExchange.Redis.sln.DotSettings | 3 +- .../ConfigurationOptions.cs | 14 +++++- src/StackExchange.Redis/Runtime.cs | 9 ++++ .../Certificates/CertValidationTests.cs | 45 +++++++++++-------- .../StackExchange.Redis.Tests/FormatTests.cs | 5 ++- 5 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 src/StackExchange.Redis/Runtime.cs diff --git a/StackExchange.Redis.sln.DotSettings b/StackExchange.Redis.sln.DotSettings index 165f8337f..de893e54d 100644 --- a/StackExchange.Redis.sln.DotSettings +++ b/StackExchange.Redis.sln.DotSettings @@ -1,3 +1,4 @@  OK - PONG \ No newline at end of file + PONG + True \ No newline at end of file diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index dfdab5f4e..c0021f024 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -390,8 +390,18 @@ private static bool CheckTrustedIssuer(X509Certificate2 certificateToValidate, X try { // This only verifies that the chain is valid, but with AllowUnknownCertificateAuthority could trust - // self-signed or partial chained vertificates - var chainIsVerified = chain.Build(certificateToValidate); + // self-signed or partial chained certificates + bool chainIsVerified; + try + { + chainIsVerified = chain.Build(certificateToValidate); + } + catch (ArgumentException ex) when ((ex.ParamName ?? ex.Message) == "certificate" && Runtime.IsMono) + { + // work around Mono cert limitation; report as rejected rather than fault + // (note also the likely .ctor mixup re param-name vs message) + chainIsVerified = false; + } if (chainIsVerified) { // Our method is "TrustIssuer", which means any intermediate cert we're being told to trust diff --git a/src/StackExchange.Redis/Runtime.cs b/src/StackExchange.Redis/Runtime.cs new file mode 100644 index 000000000..879c9c325 --- /dev/null +++ b/src/StackExchange.Redis/Runtime.cs @@ -0,0 +1,9 @@ +using System; +using System.Runtime.InteropServices; + +namespace StackExchange.Redis; + +internal static class Runtime +{ + public static readonly bool IsMono = RuntimeInformation.FrameworkDescription.StartsWith("Mono ", StringComparison.OrdinalIgnoreCase); +} diff --git a/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs b/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs index 529b29a02..a0d9b5c88 100644 --- a/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs +++ b/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs @@ -16,30 +16,39 @@ public void CheckIssuerValidity() // Trusting CA explicitly var callback = ConfigurationOptions.TrustIssuerCallback(Path.Combine("Certificates", "ca.foo.com.pem")); - Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None)); - Assert.True(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable)); + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None), "subtest 1a"); + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors), "subtest 1b"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch), "subtest 1c"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable), "subtest 1d"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch), "subtest 1e"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable), "subtest 1f"); // Trusting the remote endpoint cert directly callback = ConfigurationOptions.TrustIssuerCallback(Path.Combine("Certificates", "device01.foo.com.pem")); - Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None)); - Assert.True(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable)); + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None), "subtest 2a"); + if (Runtime.IsMono) + { + // Mono doesn't support this cert usage, reports as rejection (happy for someone to work around this, but isn't high priority) + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors), "subtest 2b"); + } + else + { + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors), "subtest 2b"); + } + + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch), "subtest 2c"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable), "subtest 2d"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch), "subtest 2e"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable), "subtest 2f"); // Attempting to trust another CA (mismatch) callback = ConfigurationOptions.TrustIssuerCallback(Path.Combine("Certificates", "ca2.foo.com.pem")); - Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch)); - Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable)); + Assert.True(callback(this, endpointCert, null, SslPolicyErrors.None), "subtest 3a"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors), "subtest 3b"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNameMismatch), "subtest 3c"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateNotAvailable), "subtest 3d"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch), "subtest 3e"); + Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable), "subtest 3f"); } private static X509Certificate2 LoadCert(string certificatePath) => new X509Certificate2(File.ReadAllBytes(certificatePath)); diff --git a/tests/StackExchange.Redis.Tests/FormatTests.cs b/tests/StackExchange.Redis.Tests/FormatTests.cs index 451db8c20..0054ce11d 100644 --- a/tests/StackExchange.Redis.Tests/FormatTests.cs +++ b/tests/StackExchange.Redis.Tests/FormatTests.cs @@ -68,7 +68,10 @@ public void ParseEndPoint(string data, EndPoint expected, string? expectedFormat [InlineData(CommandFlags.DemandReplica | CommandFlags.FireAndForget, "PreferMaster, FireAndForget, DemandReplica")] // 2-bit flag is hit-and-miss #endif public void CommandFlagsFormatting(CommandFlags value, string expected) - => Assert.Equal(expected, value.ToString()); + { + Assert.SkipWhen(Runtime.IsMono, "Mono has different enum flag behavior"); + Assert.Equal(expected, value.ToString()); + } [Theory] [InlineData(ClientType.Normal, "Normal")] From bf972cc9ad1597b0018eb67644de9e6fd8c66f47 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Wed, 23 Jul 2025 18:02:45 +0300 Subject: [PATCH 351/435] chore: Replaces string interpolation with structured logging - ServerEndPoint (#2924) * chore: Replaces string interpolation with structured logging Migrates Redis server endpoint logging from string interpolation to LoggerMessage attributes for better performance and structured logging support. Adds 14 new LoggerMessage methods for server connection lifecycle events including handshake, authentication, configuration, and buffer operations. Uses ServerEndPointLogValue wrapper to ensure consistent server representation in logs while maintaining type safety. * Improves type safety for tie-breaker key logging Changes parameter type from string to RedisKey in logging method to maintain type consistency throughout the tie-breaker request flow. Simplifies object instantiation using target-typed new expression. --- src/StackExchange.Redis/LoggerExtensions.cs | 85 +++++++++++++++++++++ src/StackExchange.Redis/ServerEndPoint.cs | 28 +++---- 2 files changed, 99 insertions(+), 14 deletions(-) diff --git a/src/StackExchange.Redis/LoggerExtensions.cs b/src/StackExchange.Redis/LoggerExtensions.cs index af57264bb..877de6257 100644 --- a/src/StackExchange.Redis/LoggerExtensions.cs +++ b/src/StackExchange.Redis/LoggerExtensions.cs @@ -403,4 +403,89 @@ internal static void LogWithThreadPoolStats(this ILogger? log, string message) EventId = 56, Message = "...but we did find instead: {DeDottedEndpoint}")] internal static partial void LogInformationFoundAlternativeEndpoint(this ILogger logger, string deDottedEndpoint); + + // ServerEndPoint logging methods + [LoggerMessage( + Level = LogLevel.Information, + EventId = 57, + Message = "{Server}: OnConnectedAsync already connected start")] + internal static partial void LogInformationOnConnectedAsyncAlreadyConnectedStart(this ILogger logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 58, + Message = "{Server}: OnConnectedAsync already connected end")] + internal static partial void LogInformationOnConnectedAsyncAlreadyConnectedEnd(this ILogger logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 59, + Message = "{Server}: OnConnectedAsync init (State={ConnectionState})")] + internal static partial void LogInformationOnConnectedAsyncInit(this ILogger logger, ServerEndPointLogValue server, PhysicalBridge.State? connectionState); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 60, + Message = "{Server}: OnConnectedAsync completed ({Result})")] + internal static partial void LogInformationOnConnectedAsyncCompleted(this ILogger logger, ServerEndPointLogValue server, string result); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 61, + Message = "{Server}: Auto-configuring...")] + internal static partial void LogInformationAutoConfiguring(this ILogger logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 62, + Message = "{EndPoint}: Requesting tie-break (Key=\"{TieBreakerKey}\")...")] + internal static partial void LogInformationRequestingTieBreak(this ILogger logger, EndPointLogValue endPoint, RedisKey tieBreakerKey); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 63, + Message = "{Server}: Server handshake")] + internal static partial void LogInformationServerHandshake(this ILogger logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 64, + Message = "{Server}: Authenticating via HELLO")] + internal static partial void LogInformationAuthenticatingViaHello(this ILogger logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 65, + Message = "{Server}: Authenticating (user/password)")] + internal static partial void LogInformationAuthenticatingUserPassword(this ILogger logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 66, + Message = "{Server}: Authenticating (password)")] + internal static partial void LogInformationAuthenticatingPassword(this ILogger logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 67, + Message = "{Server}: Setting client name: {ClientName}")] + internal static partial void LogInformationSettingClientName(this ILogger logger, ServerEndPointLogValue server, string clientName); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 68, + Message = "{Server}: Setting client lib/ver")] + internal static partial void LogInformationSettingClientLibVer(this ILogger logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 69, + Message = "{Server}: Sending critical tracer (handshake): {CommandAndKey}")] + internal static partial void LogInformationSendingCriticalTracer(this ILogger logger, ServerEndPointLogValue server, string commandAndKey); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 70, + Message = "{Server}: Flushing outbound buffer")] + internal static partial void LogInformationFlushingOutboundBuffer(this ILogger logger, ServerEndPointLogValue server); } diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index 1e32275d6..e62dc9f43 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -116,7 +116,7 @@ public Task OnConnectedAsync(ILogger? log = null, bool sendTracerIfConne { async Task IfConnectedAsync(ILogger? log, bool sendTracerIfConnected, bool autoConfigureIfConnected) { - log?.LogInformation($"{Format.ToString(this)}: OnConnectedAsync already connected start"); + log?.LogInformationOnConnectedAsyncAlreadyConnectedStart(new(this)); if (autoConfigureIfConnected) { await AutoConfigureAsync(null, log).ForAwait(); @@ -125,15 +125,15 @@ async Task IfConnectedAsync(ILogger? log, bool sendTracerIfConnected, bo { await SendTracerAsync(log).ForAwait(); } - log?.LogInformation($"{Format.ToString(this)}: OnConnectedAsync already connected end"); + log?.LogInformationOnConnectedAsyncAlreadyConnectedEnd(new(this)); return "Already connected"; } if (!IsConnected) { - log?.LogInformation($"{Format.ToString(this)}: OnConnectedAsync init (State={interactive?.ConnectionState})"); + log?.LogInformationOnConnectedAsyncInit(new(this), interactive?.ConnectionState); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _ = tcs.Task.ContinueWith(t => log?.LogInformation($"{Format.ToString(this)}: OnConnectedAsync completed ({t.Result})")); + _ = tcs.Task.ContinueWith(t => log?.LogInformationOnConnectedAsyncCompleted(new(this), t.Result)); lock (_pendingConnectionMonitors) { _pendingConnectionMonitors.Add(tcs); @@ -383,7 +383,7 @@ internal async Task AutoConfigureAsync(PhysicalConnection? connection, ILogger? return; } - log?.LogInformation($"{Format.ToString(this)}: Auto-configuring..."); + log?.LogInformationAutoConfiguring(new(this)); var commandMap = Multiplexer.CommandMap; const CommandFlags flags = CommandFlags.FireAndForget | CommandFlags.NoRedirect; @@ -458,7 +458,7 @@ internal async Task AutoConfigureAsync(PhysicalConnection? connection, ILogger? // But if GETs are disabled on this, do not fail the connection - we just don't get tiebreaker benefits if (Multiplexer.RawConfig.TryGetTieBreaker(out var tieBreakerKey) && Multiplexer.CommandMap.IsAvailable(RedisCommand.GET)) { - log?.LogInformation($"{Format.ToString(EndPoint)}: Requesting tie-break (Key=\"{tieBreakerKey}\")..."); + log?.LogInformationRequestingTieBreak(new(EndPoint), tieBreakerKey); msg = Message.Create(0, flags, RedisCommand.GET, tieBreakerKey); msg.SetInternalCall(); msg = LoggingMessage.Create(log, msg); @@ -929,7 +929,7 @@ internal ValueTask WriteDirectOrQueueFireAndForgetAsync(PhysicalConnection? c private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) { - log?.LogInformation($"{Format.ToString(this)}: Server handshake"); + log?.LogInformationServerHandshake(new(this)); if (connection == null) { Multiplexer.Trace("No connection!?"); @@ -979,7 +979,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) ResultProcessor? autoConfig = null; if (Multiplexer.RawConfig.TryResp3()) // note this includes an availability check on HELLO { - log?.LogInformation($"{Format.ToString(this)}: Authenticating via HELLO"); + log?.LogInformationAuthenticatingViaHello(new(this)); var hello = Message.CreateHello(3, user, password, clientName, CommandFlags.FireAndForget); hello.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, hello, autoConfig ??= ResultProcessor.AutoConfigureProcessor.Create(log)).ForAwait(); @@ -997,14 +997,14 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) // and: we're pipelined here, so... meh if (!string.IsNullOrWhiteSpace(user) && Multiplexer.CommandMap.IsAvailable(RedisCommand.AUTH)) { - log?.LogInformation($"{Format.ToString(this)}: Authenticating (user/password)"); + log?.LogInformationAuthenticatingUserPassword(new(this)); msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.AUTH, (RedisValue)user, (RedisValue)password); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); } else if (!string.IsNullOrWhiteSpace(password) && Multiplexer.CommandMap.IsAvailable(RedisCommand.AUTH)) { - log?.LogInformation($"{Format.ToString(this)}: Authenticating (password)"); + log?.LogInformationAuthenticatingPassword(new(this)); msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.AUTH, (RedisValue)password); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); @@ -1014,7 +1014,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) { if (!string.IsNullOrWhiteSpace(clientName)) { - log?.LogInformation($"{Format.ToString(this)}: Setting client name: {clientName}"); + log?.LogInformationSettingClientName(new(this), clientName); msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT, RedisLiterals.SETNAME, (RedisValue)clientName); msg.SetInternalCall(); await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.DemandOK).ForAwait(); @@ -1024,7 +1024,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) { // note that this is a relatively new feature, but usually we won't know the // server version, so we will use this speculatively and hope for the best - log?.LogInformation($"{Format.ToString(this)}: Setting client lib/ver"); + log?.LogInformationSettingClientLibVer(new(this)); var libName = Multiplexer.GetFullLibraryName(); if (!string.IsNullOrWhiteSpace(libName)) @@ -1062,7 +1062,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) var tracer = GetTracerMessage(true); tracer = LoggingMessage.Create(log, tracer); - log?.LogInformation($"{Format.ToString(this)}: Sending critical tracer (handshake): {tracer.CommandAndKey}"); + log?.LogInformationSendingCriticalTracer(new(this), tracer.CommandAndKey); await WriteDirectOrQueueFireAndForgetAsync(connection, tracer, ResultProcessor.EstablishConnection).ForAwait(); // Note: this **must** be the last thing on the subscription handshake, because after this @@ -1077,7 +1077,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log) await WriteDirectOrQueueFireAndForgetAsync(connection, msg, ResultProcessor.TrackSubscriptions).ForAwait(); } } - log?.LogInformation($"{Format.ToString(this)}: Flushing outbound buffer"); + log?.LogInformationFlushingOutboundBuffer(new(this)); await connection.FlushAsync().ForAwait(); } From cf8b6fbdd1c8d5a73128cf252cf2e10edb732813 Mon Sep 17 00:00:00 2001 From: atakavci Date: Wed, 23 Jul 2025 18:27:34 +0300 Subject: [PATCH 352/435] Support for new HGETDEL, HGETEX and HSETEX commands (#2863) * Support for HFE API commands, HGETDEL, HGETEX, HSETEX * fix missing ToInner usage in key-prefixed wrapper * simplify and fix key prefix tests * add missing key-prefix tests * CheckCommandResult: disambiguate key to prevent CI race * fix brittle ScanCancellable test * skip WithCancellation_CancelledToken_ThrowsOperationCanceledException on netfx due to unpredictable impl * release notes and version bump --------- Co-authored-by: Marc Gravell --- docs/ReleaseNotes.md | 5 +- src/StackExchange.Redis/Enums/RedisCommand.cs | 6 + .../Interfaces/IDatabase.cs | 143 ++++++ .../Interfaces/IDatabaseAsync.cs | 39 ++ .../KeyspaceIsolation/KeyPrefixed.cs | 47 +- .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 47 +- src/StackExchange.Redis/Message.cs | 33 ++ .../PublicAPI/PublicAPI.Shipped.txt | 29 +- .../RedisDatabase.ExpiryToken.cs | 76 +++ src/StackExchange.Redis/RedisDatabase.cs | 306 +++++++++++- src/StackExchange.Redis/RedisFeatures.cs | 2 + src/StackExchange.Redis/RedisLiterals.cs | 2 + src/StackExchange.Redis/ResultProcessor.cs | 44 ++ .../CancellationTests.cs | 8 +- .../ExpiryTokenTests.cs | 117 +++++ .../HashFieldTests.cs | 271 +++++++++++ .../KeyPrefixedDatabaseTests.cs | 443 +++++++++++++++-- .../KeyPrefixedTests.cs | 458 ++++++++++++++++-- .../RespProtocolTests.cs | 25 +- version.json | 2 +- 20 files changed, 1984 insertions(+), 119 deletions(-) create mode 100644 src/StackExchange.Redis/RedisDatabase.ExpiryToken.cs create mode 100644 tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 758e0a30f..d912f0842 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,9 +6,10 @@ Current package versions: | ------------ | ----------------- | ----- | | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | -## Unreleased +## Unreleased (2.9.xxx) -- nothing yet +- Add `HGETDEL`, `HGETEX` and `HSETEX` support ([#2863 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2863)) +- Fix key-prefix omission in `SetIntersectionLength` and `SortedSet{Combine[WithScores]|IntersectionLength}` ([#2863 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2863)) ## 2.8.58 diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 34e1eb296..52f0b134d 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -70,6 +70,8 @@ internal enum RedisCommand HEXPIREAT, HEXPIRETIME, HGET, + HGETEX, + HGETDEL, HGETALL, HINCRBY, HINCRBYFLOAT, @@ -85,6 +87,7 @@ internal enum RedisCommand HRANDFIELD, HSCAN, HSET, + HSETEX, HSETNX, HSTRLEN, HVALS, @@ -294,6 +297,8 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.HDEL: case RedisCommand.HEXPIRE: case RedisCommand.HEXPIREAT: + case RedisCommand.HGETDEL: + case RedisCommand.HGETEX: case RedisCommand.HINCRBY: case RedisCommand.HINCRBYFLOAT: case RedisCommand.HMSET: @@ -301,6 +306,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.HPEXPIRE: case RedisCommand.HPEXPIREAT: case RedisCommand.HSET: + case RedisCommand.HSETEX: case RedisCommand.HSETNX: case RedisCommand.INCR: case RedisCommand.INCRBY: diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index c37d3ddb0..37fb6e32b 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -516,6 +516,149 @@ public interface IDatabase : IRedis, IDatabaseAsync /// RedisValue[] HashGet(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + /// + /// Returns the value associated with field in the hash stored at key. + /// + /// The key of the hash. + /// The field in the hash to get. + /// The flags to use for this operation. + /// The value associated with field, or when field is not present in the hash or key does not exist. + /// + RedisValue HashFieldGetAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the value associated with field in the hash stored at key. + /// + /// The key of the hash. + /// The field in the hash to get. + /// The flags to use for this operation. + /// The value associated with field, or when field is not present in the hash or key does not exist. + /// + Lease? HashFieldGetLeaseAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); + + /// + /// Returns the values associated with the specified fields in the hash stored at key. + /// For every field that does not exist in the hash, a value is returned. + /// Because non-existing keys are treated as empty hashes, running HMGET against a non-existing key will return a list of values. + /// + /// The key of the hash. + /// The fields in the hash to get. + /// The flags to use for this operation. + /// List of values associated with the given fields, in the same order as they are requested. + /// + RedisValue[] HashFieldGetAndDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + + /// + /// Gets the value of the specified hash field and sets its expiration time. + /// + /// The key of the hash. + /// The field in the hash to get and set the expiration for. + /// The expiration time to set. + /// If true, the expiration will be removed. And 'expiry' parameter is ignored. + /// The flags to use for this operation. + /// The value of the specified hash field. + RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None); + + /// + /// Gets the value of the specified hash field and sets its expiration time. + /// + /// The key of the hash. + /// The field in the hash to get and set the expiration for. + /// The exact date and time to set the expiration to. + /// The flags to use for this operation. + /// The value of the specified hash field. + RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None); + + /// + /// Gets the value of the specified hash field and sets its expiration time, returning a lease. + /// + /// The key of the hash. + /// The field in the hash to get and set the expiration for. + /// The expiration time to set. + /// If true, the expiration will be removed. And 'expiry' parameter is ignored. + /// The flags to use for this operation. + /// The value of the specified hash field as a lease. + Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None); + + /// + /// Gets the value of the specified hash field and sets its expiration time, returning a lease. + /// + /// The key of the hash. + /// The field in the hash to get and set the expiration for. + /// The exact date and time to set the expiration to. + /// The flags to use for this operation. + /// The value of the specified hash field as a lease. + Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None); + + /// + /// Gets the values of the specified hash fields and sets their expiration times. + /// + /// The key of the hash. + /// The fields in the hash to get and set the expiration for. + /// The expiration time to set. + /// If true, the expiration will be removed. And 'expiry' parameter is ignored. + /// The flags to use for this operation. + /// The values of the specified hash fields. + RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None); + + /// + /// Gets the values of the specified hash fields and sets their expiration times. + /// + /// The key of the hash. + /// The fields in the hash to get and set the expiration for. + /// The exact date and time to set the expiration to. + /// The flags to use for this operation. + /// The values of the specified hash fields. + RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None); + + /// + /// Sets the value of the specified hash field and sets its expiration time. + /// + /// The key of the hash. + /// The field in the hash to set and set the expiration for. + /// The value in the hash to set and set the expiration for. + /// The expiration time to set. + /// Whether to maintain the existing field's TTL (KEEPTTL flag). + /// Which conditions to set the value under (defaults to always). + /// The flags to use for this operation. + /// 0 if no fields were set, 1 if all the fields were set. + RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); + + /// + /// Sets the value of the specified hash field and sets its expiration time. + /// + /// The key of the hash. + /// The field in the hash to set and set the expiration for. + /// The value in the hash to set and set the expiration for. + /// The exact date and time to set the expiration to. + /// Which conditions to set the value under (defaults to always). + /// The flags to use for this operation. + /// 0 if no fields were set, 1 if all the fields were set. + RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None); + + /// + /// Sets the values of the specified hash fields and sets their expiration times. + /// + /// The key of the hash. + /// The fields in the hash to set and set the expiration for. + /// The expiration time to set. + /// Whether to maintain the existing fields' TTL (KEEPTTL flag). + /// Which conditions to set the values under (defaults to always). + /// The flags to use for this operation. + /// 0 if no fields were set, 1 if all the fields were set. + RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); + + /// + /// Sets the values of the specified hash fields and sets their expiration times. + /// + /// The key of the hash. + /// The fields in the hash to set and set the expiration for. + /// The exact date and time to set the expiration to. + /// Which conditions to set the values under (defaults to always). + /// The flags to use for this operation. + /// 0 if no fields were set, 1 if all the fields were set. + RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None); + /// /// Returns all fields and values of the hash stored at key. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 4873c1069..b88570790 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -84,6 +84,45 @@ public interface IDatabaseAsync : IRedisAsync /// Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); + /// + Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); + + /// + Task?> HashFieldGetLeaseAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None); + + /// + Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None); + + /// + Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); + + /// + Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None); + /// Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 331d23ea7..06c5359eb 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -84,6 +84,45 @@ public Task HashDeleteAsync(RedisKey key, RedisValue hashField, CommandFla public Task HashExistsAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => Inner.HashExistsAsync(ToInner(key), hashField, flags); + public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetAndDeleteAsync(ToInner(key), hashField, flags); + + public Task?> HashFieldGetLeaseAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetLeaseAndDeleteAsync(ToInner(key), hashField, flags); + + public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetAndDeleteAsync(ToInner(key), hashFields, flags); + + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetAndSetExpiryAsync(ToInner(key), hashField, expiry, persist, flags); + + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetAndSetExpiryAsync(ToInner(key), hashField, expiry, flags); + + public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetLeaseAndSetExpiryAsync(ToInner(key), hashField, expiry, persist, flags); + + public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetLeaseAndSetExpiryAsync(ToInner(key), hashField, expiry, flags); + + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetAndSetExpiryAsync(ToInner(key), hashFields, expiry, persist, flags); + + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetAndSetExpiryAsync(ToInner(key), hashFields, expiry, flags); + + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldSetAndSetExpiryAsync(ToInner(key), field, value, expiry, keepTtl, when, flags); + + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldSetAndSetExpiryAsync(ToInner(key), field, value, expiry, when, flags); + + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldSetAndSetExpiryAsync(ToInner(key), hashFields, expiry, keepTtl, when, flags); + + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldSetAndSetExpiryAsync(ToInner(key), hashFields, expiry, when, flags); + public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => Inner.HashFieldExpireAsync(ToInner(key), hashFields, expiry, when, flags); @@ -394,7 +433,7 @@ public Task SetContainsAsync(RedisKey key, RedisValue[] values, CommandF Inner.SetContainsAsync(ToInner(key), values, flags); public Task SetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => - Inner.SetIntersectionLengthAsync(keys, limit, flags); + Inner.SetIntersectionLengthAsync(ToInner(keys), limit, flags); public Task SetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.SetLengthAsync(ToInner(key), flags); @@ -450,10 +489,10 @@ public Task SortedSetAddAsync(RedisKey key, RedisValue member, double scor public Task SortedSetAddAsync(RedisKey key, RedisValue member, double score, SortedSetWhen updateWhen = SortedSetWhen.Always, CommandFlags flags = CommandFlags.None) => Inner.SortedSetAddAsync(ToInner(key), member, score, updateWhen, flags); public Task SortedSetCombineAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - Inner.SortedSetCombineAsync(operation, keys, weights, aggregate, flags); + Inner.SortedSetCombineAsync(operation, ToInner(keys), weights, aggregate, flags); public Task SortedSetCombineWithScoresAsync(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - Inner.SortedSetCombineWithScoresAsync(operation, keys, weights, aggregate, flags); + Inner.SortedSetCombineWithScoresAsync(operation, ToInner(keys), weights, aggregate, flags); public Task SortedSetCombineAndStoreAsync(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => Inner.SortedSetCombineAndStoreAsync(operation, ToInner(destination), ToInner(keys), weights, aggregate, flags); @@ -468,7 +507,7 @@ public Task SortedSetIncrementAsync(RedisKey key, RedisValue member, dou Inner.SortedSetIncrementAsync(ToInner(key), member, value, flags); public Task SortedSetIntersectionLengthAsync(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => - Inner.SortedSetIntersectionLengthAsync(keys, limit, flags); + Inner.SortedSetIntersectionLengthAsync(ToInner(keys), limit, flags); public Task SortedSetLengthAsync(RedisKey key, double min = -1.0 / 0.0, double max = 1.0 / 0.0, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => Inner.SortedSetLengthAsync(ToInner(key), min, max, exclude, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index 18406ba9f..48df1e4db 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -81,6 +81,45 @@ public bool HashDelete(RedisKey key, RedisValue hashField, CommandFlags flags = public bool HashExists(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => Inner.HashExists(ToInner(key), hashField, flags); + public RedisValue HashFieldGetAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetAndDelete(ToInner(key), hashField, flags); + + public Lease? HashFieldGetLeaseAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetLeaseAndDelete(ToInner(key), hashField, flags); + + public RedisValue[] HashFieldGetAndDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetAndDelete(ToInner(key), hashFields, flags); + + public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetAndSetExpiry(ToInner(key), hashField, expiry, persist, flags); + + public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetAndSetExpiry(ToInner(key), hashField, expiry, flags); + + public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetLeaseAndSetExpiry(ToInner(key), hashField, expiry, persist, flags); + + public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetLeaseAndSetExpiry(ToInner(key), hashField, expiry, flags); + + public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetAndSetExpiry(ToInner(key), hashFields, expiry, persist, flags); + + public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldGetAndSetExpiry(ToInner(key), hashFields, expiry, flags); + + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldSetAndSetExpiry(ToInner(key), field, value, expiry, keepTtl, when, flags); + + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldSetAndSetExpiry(ToInner(key), field, value, expiry, when, flags); + + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldSetAndSetExpiry(ToInner(key), hashFields, expiry, keepTtl, when, flags); + + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) => + Inner.HashFieldSetAndSetExpiry(ToInner(key), hashFields, expiry, when, flags); + public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) => Inner.HashFieldExpire(ToInner(key), hashFields, expiry, when, flags); @@ -381,7 +420,7 @@ public bool[] SetContains(RedisKey key, RedisValue[] values, CommandFlags flags Inner.SetContains(ToInner(key), values, flags); public long SetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => - Inner.SetIntersectionLength(keys, limit, flags); + Inner.SetIntersectionLength(ToInner(keys), limit, flags); public long SetLength(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.SetLength(ToInner(key), flags); @@ -435,10 +474,10 @@ public bool SortedSetAdd(RedisKey key, RedisValue member, double score, SortedSe Inner.SortedSetAdd(ToInner(key), member, score, when, flags); public RedisValue[] SortedSetCombine(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - Inner.SortedSetCombine(operation, keys, weights, aggregate, flags); + Inner.SortedSetCombine(operation, ToInner(keys), weights, aggregate, flags); public SortedSetEntry[] SortedSetCombineWithScores(SetOperation operation, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => - Inner.SortedSetCombineWithScores(operation, keys, weights, aggregate, flags); + Inner.SortedSetCombineWithScores(operation, ToInner(keys), weights, aggregate, flags); public long SortedSetCombineAndStore(SetOperation operation, RedisKey destination, RedisKey[] keys, double[]? weights = null, Aggregate aggregate = Aggregate.Sum, CommandFlags flags = CommandFlags.None) => Inner.SortedSetCombineAndStore(operation, ToInner(destination), ToInner(keys), weights, aggregate, flags); @@ -453,7 +492,7 @@ public double SortedSetIncrement(RedisKey key, RedisValue member, double value, Inner.SortedSetIncrement(ToInner(key), member, value, flags); public long SortedSetIntersectionLength(RedisKey[] keys, long limit = 0, CommandFlags flags = CommandFlags.None) => - Inner.SortedSetIntersectionLength(keys, limit, flags); + Inner.SortedSetIntersectionLength(ToInner(keys), limit, flags); public long SortedSetLength(RedisKey key, double min = -1.0 / 0.0, double max = 1.0 / 0.0, Exclude exclude = Exclude.None, CommandFlags flags = CommandFlags.None) => Inner.SortedSetLength(ToInner(key), min, max, exclude, flags); diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 9472b6db0..cd3d29947 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -310,6 +310,9 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) => new CommandValueValueValueValueValueMessage(db, flags, command, value0, value1, value2, value3, value4); + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue[] values) => + new CommandKeyValueValueValuesMessage(db, flags, command, key, value0, value1, values); + public static Message Create( int db, CommandFlags flags, @@ -1180,6 +1183,36 @@ protected override void WriteImpl(PhysicalConnection physical) public override int ArgCount => values.Length + 1; } + private sealed class CommandKeyValueValueValuesMessage : CommandKeyBase + { + private readonly RedisValue value0; + private readonly RedisValue value1; + private readonly RedisValue[] values; + public CommandKeyValueValueValuesMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, RedisValue[] values) : base(db, flags, command, key) + { + for (int i = 0; i < values.Length; i++) + { + values[i].AssertNotNull(); + } + + value0.AssertNotNull(); + value1.AssertNotNull(); + this.value0 = value0; + this.value1 = value1; + this.values = values; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, values.Length + 3); + physical.Write(Key); + physical.WriteBulkString(value0); + physical.WriteBulkString(value1); + for (int i = 0; i < values.Length; i++) physical.WriteBulkString(values[i]); + } + public override int ArgCount => values.Length + 3; + } + private sealed class CommandKeyValueValueMessage : CommandKeyBase { private readonly RedisValue value0, value1; diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 43b35ba58..4a77208aa 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1924,4 +1924,31 @@ StackExchange.Redis.StreamTrimMode.KeepReferences = 0 -> StackExchange.Redis.Str StackExchange.Redis.StreamTrimResult StackExchange.Redis.StreamTrimResult.Deleted = 1 -> StackExchange.Redis.StreamTrimResult StackExchange.Redis.StreamTrimResult.NotDeleted = 2 -> StackExchange.Redis.StreamTrimResult -StackExchange.Redis.StreamTrimResult.NotFound = -1 -> StackExchange.Redis.StreamTrimResult \ No newline at end of file +StackExchange.Redis.StreamTrimResult.NotFound = -1 -> StackExchange.Redis.StreamTrimResult +StackExchange.Redis.IDatabase.HashFieldGetAndDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.HashFieldGetAndDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.IDatabase.HashFieldGetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.HashFieldGetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.TimeSpan? expiry = null, bool persist = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.HashFieldGetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.IDatabase.HashFieldGetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.TimeSpan? expiry = null, bool persist = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +StackExchange.Redis.IDatabase.HashFieldGetLeaseAndDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +StackExchange.Redis.IDatabase.HashFieldGetLeaseAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +StackExchange.Redis.IDatabase.HashFieldGetLeaseAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.TimeSpan? expiry = null, bool persist = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +StackExchange.Redis.IDatabase.HashFieldSetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue field, StackExchange.Redis.RedisValue value, System.DateTime expiry, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.HashFieldSetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue field, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.HashFieldSetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, System.DateTime expiry, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabase.HashFieldSetAndSetExpiry(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +StackExchange.Redis.IDatabaseAsync.HashFieldGetAndDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldGetAndDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldGetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldGetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.TimeSpan? expiry = null, bool persist = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldGetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldGetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! hashFields, System.TimeSpan? expiry = null, bool persist = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldGetLeaseAndDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +StackExchange.Redis.IDatabaseAsync.HashFieldGetLeaseAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.DateTime expiry, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +StackExchange.Redis.IDatabaseAsync.HashFieldGetLeaseAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue hashField, System.TimeSpan? expiry = null, bool persist = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue field, StackExchange.Redis.RedisValue value, System.DateTime expiry, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue field, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, System.DateTime expiry, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! + diff --git a/src/StackExchange.Redis/RedisDatabase.ExpiryToken.cs b/src/StackExchange.Redis/RedisDatabase.ExpiryToken.cs new file mode 100644 index 000000000..42cfdcb18 --- /dev/null +++ b/src/StackExchange.Redis/RedisDatabase.ExpiryToken.cs @@ -0,0 +1,76 @@ +using System; + +namespace StackExchange.Redis; + +internal partial class RedisDatabase +{ + /// + /// Parses, validates and represents, for example: "EX 10", "KEEPTTL" or "". + /// + internal readonly struct ExpiryToken + { + private static readonly ExpiryToken s_Persist = new(RedisLiterals.PERSIST), s_KeepTtl = new(RedisLiterals.KEEPTTL), s_Null = new(RedisValue.Null); + + public RedisValue Operand { get; } + public long Value { get; } + public int Tokens => Value == long.MinValue ? (Operand.IsNull ? 0 : 1) : 2; + public bool HasValue => Value != long.MinValue; + public bool HasOperand => !Operand.IsNull; + + public static ExpiryToken Persist(TimeSpan? expiry, bool persist) + { + if (expiry.HasValue) + { + if (persist) throw new ArgumentException("Cannot specify both expiry and persist", nameof(persist)); + return new(expiry.GetValueOrDefault()); // EX 10 + } + + return persist ? s_Persist : s_Null; // PERSIST (or nothing) + } + + public static ExpiryToken KeepTtl(TimeSpan? expiry, bool keepTtl) + { + if (expiry.HasValue) + { + if (keepTtl) throw new ArgumentException("Cannot specify both expiry and keepTtl", nameof(keepTtl)); + return new(expiry.GetValueOrDefault()); // EX 10 + } + + return keepTtl ? s_KeepTtl : s_Null; // KEEPTTL (or nothing) + } + + private ExpiryToken(RedisValue operand, long value = long.MinValue) + { + Operand = operand; + Value = value; + } + + public ExpiryToken(TimeSpan expiry) + { + long milliseconds = expiry.Ticks / TimeSpan.TicksPerMillisecond; + var useSeconds = milliseconds % 1000 == 0; + + Operand = useSeconds ? RedisLiterals.EX : RedisLiterals.PX; + Value = useSeconds ? (milliseconds / 1000) : milliseconds; + } + + public ExpiryToken(DateTime expiry) + { + long milliseconds = GetUnixTimeMilliseconds(expiry); + var useSeconds = milliseconds % 1000 == 0; + + Operand = useSeconds ? RedisLiterals.EXAT : RedisLiterals.PXAT; + Value = useSeconds ? (milliseconds / 1000) : milliseconds; + } + + public override string ToString() => Tokens switch + { + 2 => $"{Operand} {Value}", + 1 => Operand.ToString(), + _ => "", + }; + + public override int GetHashCode() => throw new NotSupportedException(); + public override bool Equals(object? obj) => throw new NotSupportedException(); + } +} diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 651d1b4a3..7b23b0773 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -8,7 +8,7 @@ namespace StackExchange.Redis { - internal class RedisDatabase : RedisBase, IDatabase + internal partial class RedisDatabase : RedisBase, IDatabase { internal RedisDatabase(ConnectionMultiplexer multiplexer, int db, object? asyncState) : base(multiplexer, asyncState) @@ -396,7 +396,7 @@ public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, Tim public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) { - long milliseconds = GetMillisecondsUntil(expiry); + long milliseconds = GetUnixTimeMilliseconds(expiry); return HashFieldExpireExecute(key, milliseconds, when, PickExpireAtCommandByPrecision, SyncCustomArrExecutor>, ResultProcessor.ExpireResultArray, flags, hashFields); } @@ -408,7 +408,7 @@ public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hash public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) { - long milliseconds = GetMillisecondsUntil(expiry); + long milliseconds = GetUnixTimeMilliseconds(expiry); return HashFieldExpireExecute(key, milliseconds, when, PickExpireAtCommandByPrecision, AsyncCustomArrExecutor>, ResultProcessor.ExpireResultArray, flags, hashFields); } @@ -447,6 +447,300 @@ private T HashFieldExecute(RedisCommand cmd, RedisKey key, Custom private Task AsyncCustomArrExecutor(Message msg, TProcessor processor) where TProcessor : ResultProcessor => ExecuteAsync(msg, processor)!; + public RedisValue HashFieldGetAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.HGETDEL, key, RedisLiterals.FIELDS, 1, hashField); + return ExecuteSync(msg, ResultProcessor.RedisValueFromArray); + } + + public Lease? HashFieldGetLeaseAndDelete(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.HGETDEL, key, RedisLiterals.FIELDS, 1, hashField); + return ExecuteSync(msg, ResultProcessor.LeaseFromArray); + } + + public RedisValue[] HashFieldGetAndDelete(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + { + if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); + if (hashFields.Length == 0) return Array.Empty(); + var msg = Message.Create(Database, flags, RedisCommand.HGETDEL, key, RedisLiterals.FIELDS, hashFields.Length, hashFields); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.HGETDEL, key, RedisLiterals.FIELDS, 1, hashField); + return ExecuteAsync(msg, ResultProcessor.RedisValueFromArray); + } + + public Task?> HashFieldGetLeaseAndDeleteAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.HGETDEL, key, RedisLiterals.FIELDS, 1, hashField); + return ExecuteAsync(msg, ResultProcessor.LeaseFromArray); + } + + public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) + { + if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); + if (hashFields.Length == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState); + var msg = Message.Create(Database, flags, RedisCommand.HGETDEL, key, RedisLiterals.FIELDS, hashFields.Length, hashFields); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, in RedisValue hashField, ExpiryToken expiry, CommandFlags flags) => + expiry.Tokens switch + { + // expiry, for example EX 10 + 2 => Message.Create(Database, flags, RedisCommand.HGETEX, key, expiry.Operand, expiry.Value, RedisLiterals.FIELDS, 1, hashField), + // keyword only, for example PERSIST + 1 => Message.Create(Database, flags, RedisCommand.HGETEX, key, expiry.Operand, RedisLiterals.FIELDS, 1, hashField), + // default case when neither expiry nor persist are set + _ => Message.Create(Database, flags, RedisCommand.HGETEX, key, RedisLiterals.FIELDS, 1, hashField), + }; + + private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, RedisValue[] hashFields, ExpiryToken expiry, CommandFlags flags) + { + if (hashFields is null) throw new ArgumentNullException(nameof(hashFields)); + if (hashFields.Length == 1) + { + return HashFieldGetAndSetExpiryMessage(key, in hashFields[0], expiry, flags); + } + + // precision, time, FIELDS, hashFields.Length + int extraTokens = expiry.Tokens + 2; + + RedisValue[] values = new RedisValue[expiry.Tokens + 2 + hashFields.Length]; + + int index = 0; + // add PERSIST or expiry values + switch (expiry.Tokens) + { + case 2: + values[index++] = expiry.Operand; + values[index++] = expiry.Value; + break; + case 1: + values[index++] = expiry.Operand; + break; + } + // add the fields + values[index++] = RedisLiterals.FIELDS; + values[index++] = hashFields.Length; + // check we've added everything we expected to + Debug.Assert(index == extraTokens + hashFields.Length); + + // Add hash fields to the array + hashFields.AsSpan().CopyTo(values.AsSpan(index)); + + return Message.Create(Database, flags, RedisCommand.HGETEX, key, values); + } + + public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) + { + var msg = HashFieldGetAndSetExpiryMessage(key, hashField, ExpiryToken.Persist(expiry, persist), flags); + return ExecuteSync(msg, ResultProcessor.RedisValueFromArray); + } + + public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) + { + var msg = HashFieldGetAndSetExpiryMessage(key, hashField, new(expiry), flags); + return ExecuteSync(msg, ResultProcessor.RedisValueFromArray); + } + + public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) + { + var msg = HashFieldGetAndSetExpiryMessage(key, hashField, ExpiryToken.Persist(expiry, persist), flags); + return ExecuteSync(msg, ResultProcessor.LeaseFromArray); + } + + public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) + { + var msg = HashFieldGetAndSetExpiryMessage(key, hashField, new(expiry), flags); + return ExecuteSync(msg, ResultProcessor.LeaseFromArray); + } + + public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) + { + if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); + if (hashFields.Length == 0) return Array.Empty(); + var msg = HashFieldGetAndSetExpiryMessage(key, hashFields, ExpiryToken.Persist(expiry, persist), flags); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None) + { + if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); + if (hashFields.Length == 0) return Array.Empty(); + var msg = HashFieldGetAndSetExpiryMessage(key, hashFields, new(expiry), flags); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) + { + var msg = HashFieldGetAndSetExpiryMessage(key, hashField, ExpiryToken.Persist(expiry, persist), flags); + return ExecuteAsync(msg, ResultProcessor.RedisValueFromArray); + } + + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) + { + var msg = HashFieldGetAndSetExpiryMessage(key, hashField, new(expiry), flags); + return ExecuteAsync(msg, ResultProcessor.RedisValueFromArray); + } + + public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) + { + var msg = HashFieldGetAndSetExpiryMessage(key, hashField, ExpiryToken.Persist(expiry, persist), flags); + return ExecuteAsync(msg, ResultProcessor.LeaseFromArray); + } + + public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, DateTime expiry, CommandFlags flags = CommandFlags.None) + { + var msg = HashFieldGetAndSetExpiryMessage(key, hashField, new(expiry), flags); + return ExecuteAsync(msg, ResultProcessor.LeaseFromArray); + } + + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) + { + if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); + if (hashFields.Length == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState); + var msg = HashFieldGetAndSetExpiryMessage(key, hashFields, ExpiryToken.Persist(expiry, persist), flags); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, CommandFlags flags = CommandFlags.None) + { + if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); + if (hashFields.Length == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState); + var msg = HashFieldGetAndSetExpiryMessage(key, hashFields, new(expiry), flags); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, in RedisValue field, in RedisValue value, ExpiryToken expiry, When when, CommandFlags flags) + { + if (when == When.Always) + { + return expiry.Tokens switch + { + 2 => Message.Create(Database, flags, RedisCommand.HSETEX, key, expiry.Operand, expiry.Value, RedisLiterals.FIELDS, 1, field, value), + 1 => Message.Create(Database, flags, RedisCommand.HSETEX, key, expiry.Operand, RedisLiterals.FIELDS, 1, field, value), + _ => Message.Create(Database, flags, RedisCommand.HSETEX, key, RedisLiterals.FIELDS, 1, field, value), + }; + } + else + { + // we need an extra token + var existance = when switch + { + When.Exists => RedisLiterals.FXX, + When.NotExists => RedisLiterals.FNX, + _ => throw new ArgumentOutOfRangeException(nameof(when)), + }; + + return expiry.Tokens switch + { + 2 => Message.Create(Database, flags, RedisCommand.HSETEX, key, existance, expiry.Operand, expiry.Value, RedisLiterals.FIELDS, 1, field, value), + 1 => Message.Create(Database, flags, RedisCommand.HSETEX, key, existance, expiry.Operand, RedisLiterals.FIELDS, 1, field, value), + _ => Message.Create(Database, flags, RedisCommand.HSETEX, key, existance, RedisLiterals.FIELDS, 1, field, value), + }; + } + } + + private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, HashEntry[] hashFields, ExpiryToken expiry, When when, CommandFlags flags) + { + if (hashFields.Length == 1) + { + var field = hashFields[0]; + return HashFieldSetAndSetExpiryMessage(key, field.Name, field.Value, expiry, when, flags); + } + // Determine the base array size + var extraTokens = expiry.Tokens + (when == When.Always ? 2 : 3); // [FXX|FNX] {expiry} FIELDS {length} + RedisValue[] values = new RedisValue[(hashFields.Length * 2) + extraTokens]; + + int index = 0; + switch (when) + { + case When.Always: + break; + case When.Exists: + values[index++] = RedisLiterals.FXX; + break; + case When.NotExists: + values[index++] = RedisLiterals.FNX; + break; + default: + throw new ArgumentOutOfRangeException(nameof(when)); + } + switch (expiry.Tokens) + { + case 2: + values[index++] = expiry.Operand; + values[index++] = expiry.Value; + break; + case 1: + values[index++] = expiry.Operand; + break; + } + values[index++] = RedisLiterals.FIELDS; + values[index++] = hashFields.Length; + for (int i = 0; i < hashFields.Length; i++) + { + values[index++] = hashFields[i].name; + values[index++] = hashFields[i].value; + } + Debug.Assert(index == values.Length); + return Message.Create(Database, flags, RedisCommand.HSETEX, key, values); + } + + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + var msg = HashFieldSetAndSetExpiryMessage(key, field, value, ExpiryToken.KeepTtl(expiry, keepTtl), when, flags); + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + var msg = HashFieldSetAndSetExpiryMessage(key, field, value, new(expiry), when, flags); + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); + var msg = HashFieldSetAndSetExpiryMessage(key, hashFields, ExpiryToken.KeepTtl(expiry, keepTtl), when, flags); + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); + var msg = HashFieldSetAndSetExpiryMessage(key, hashFields, new(expiry), when, flags); + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + var msg = HashFieldSetAndSetExpiryMessage(key, field, value, ExpiryToken.KeepTtl(expiry, keepTtl), when, flags); + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + var msg = HashFieldSetAndSetExpiryMessage(key, field, value, new(expiry), when, flags); + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); + var msg = HashFieldSetAndSetExpiryMessage(key, hashFields, ExpiryToken.KeepTtl(expiry, keepTtl), when, flags); + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) + { + if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); + var msg = HashFieldSetAndSetExpiryMessage(key, hashFields, new(expiry), when, flags); + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + public long[] HashFieldGetExpireDateTime(RedisKey key, RedisValue[] hashFields, CommandFlags flags = CommandFlags.None) => HashFieldExecute(RedisCommand.HPEXPIRETIME, key, SyncCustomArrExecutor>, ResultProcessor.Int64Array, flags, hashFields); @@ -3474,7 +3768,7 @@ public Task StringSetRangeAsync(RedisKey key, long offset, RedisValu return ExecuteAsync(msg, ResultProcessor.RedisValue); } - private long GetMillisecondsUntil(DateTime when) => when.Kind switch + private static long GetUnixTimeMilliseconds(DateTime when) => when.Kind switch { DateTimeKind.Local or DateTimeKind.Utc => (when.ToUniversalTime() - RedisBase.UnixEpoch).Ticks / TimeSpan.TicksPerMillisecond, _ => throw new ArgumentException("Expiry time must be either Utc or Local", nameof(when)), @@ -3518,7 +3812,7 @@ private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, DateTime? }; } - long milliseconds = GetMillisecondsUntil(expiry.Value); + long milliseconds = GetUnixTimeMilliseconds(expiry.Value); return GetExpiryMessage(key, RedisCommand.PEXPIREAT, RedisCommand.EXPIREAT, milliseconds, when, flags, out server); } @@ -4677,7 +4971,7 @@ private Message GetStringBitOperationMessage(Bitwise operation, RedisKey destina private Message GetStringGetExMessage(in RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) => expiry == DateTime.MaxValue ? Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PERSIST) - : Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PXAT, GetMillisecondsUntil(expiry)); + : Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PXAT, GetUnixTimeMilliseconds(expiry)); private Message GetStringGetWithExpiryMessage(RedisKey key, CommandFlags flags, out ResultProcessor processor, out ServerEndPoint? server) { diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 06a44e643..87bcbf20c 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -45,7 +45,9 @@ namespace StackExchange.Redis v7_2_0_rc1 = new Version(7, 1, 240), // 7.2 RC1 is version 7.1.240 v7_4_0_rc1 = new Version(7, 3, 240), // 7.4 RC1 is version 7.3.240 v7_4_0_rc2 = new Version(7, 3, 241), // 7.4 RC2 is version 7.3.241 + v8_0_0_M04 = new Version(7, 9, 227), // 8.0 M04 is version 7.9.227 v8_2_0_rc1 = new Version(8, 1, 240); // 8.2 RC1 is version 8.1.240 + #pragma warning restore SA1310 // Field names should not contain underscore #pragma warning restore SA1311 // Static readonly fields should begin with upper-case letter diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 29937c0dd..46a64cc88 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -84,7 +84,9 @@ public static readonly RedisValue FIELDS = "FIELDS", FILTERBY = "FILTERBY", FLUSH = "FLUSH", + FNX = "FNX", FREQ = "FREQ", + FXX = "FXX", GET = "GET", GETKEYS = "GETKEYS", GETNAME = "GETNAME", diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 67dd73173..982ee565d 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -88,9 +88,15 @@ public static readonly ResultProcessor public static readonly ResultProcessor RedisValue = new RedisValueProcessor(); + public static readonly ResultProcessor + RedisValueFromArray = new RedisValueFromArrayProcessor(); + public static readonly ResultProcessor> Lease = new LeaseProcessor(); + public static readonly ResultProcessor> + LeaseFromArray = new LeaseFromArrayProcessor(); + public static readonly ResultProcessor RedisValueArray = new RedisValueArrayProcessor(); @@ -1904,6 +1910,25 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + private sealed class RedisValueFromArrayProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Resp2TypeBulkString) + { + case ResultType.Array: + var items = result.GetItems(); + if (items.Length == 1) + { // treat an array of 1 like a single reply + SetResult(message, items[0].AsRedisValue()); + return true; + } + break; + } + return false; + } + } + private sealed class RoleProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) @@ -2049,6 +2074,25 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + private sealed class LeaseFromArrayProcessor : ResultProcessor> + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Resp2TypeBulkString) + { + case ResultType.Array: + var items = result.GetItems(); + if (items.Length == 1) + { // treat an array of 1 like a single reply + SetResult(message, items[0].AsLease()!); + return true; + } + break; + } + return false; + } + } + private sealed class ScriptResultProcessor : ResultProcessor { public override bool SetResult(PhysicalConnection connection, Message message, in RawResult result) diff --git a/tests/StackExchange.Redis.Tests/CancellationTests.cs b/tests/StackExchange.Redis.Tests/CancellationTests.cs index f9f955eb8..a512743f9 100644 --- a/tests/StackExchange.Redis.Tests/CancellationTests.cs +++ b/tests/StackExchange.Redis.Tests/CancellationTests.cs @@ -12,6 +12,10 @@ public class CancellationTests(ITestOutputHelper output, SharedConnectionFixture [Fact] public async Task WithCancellation_CancelledToken_ThrowsOperationCanceledException() { +#if NETFRAMEWORK + Skip.UnlessLongRunning(); // unpredictable on netfx due to weak WaitAsync impl +#endif + await using var conn = Create(); var db = conn.GetDatabase(); @@ -156,6 +160,8 @@ public async Task CancellationDuringOperation_Async_CancelsGracefully(CancelStra [Fact] public async Task ScanCancellable() { + Skip.UnlessLongRunning(); // because of CLIENT PAUSE impact to unrelated tests + using var conn = Create(); var db = conn.GetDatabase(); var server = conn.GetServer(conn.GetEndPoints()[0]); @@ -182,7 +188,7 @@ public async Task ScanCancellable() var taken = watch.ElapsedMilliseconds; // Expected if cancellation happens during operation Log($"Cancelled after {taken}ms"); - Assert.True(taken < ConnectionPauseMilliseconds / 2, "Should have cancelled much sooner"); + Assert.True(taken < (ConnectionPauseMilliseconds * 3) / 4, $"Should have cancelled sooner; took {taken}ms"); Assert.Equal(cts.Token, oce.CancellationToken); } } diff --git a/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs b/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs new file mode 100644 index 000000000..3f0d39f28 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs @@ -0,0 +1,117 @@ +using System; +using Xunit; +using static StackExchange.Redis.RedisDatabase; +using static StackExchange.Redis.RedisDatabase.ExpiryToken; +namespace StackExchange.Redis.Tests; + +public class ExpiryTokenTests // pure tests, no DB +{ + [Fact] + public void Persist_Seconds() + { + TimeSpan? time = TimeSpan.FromMilliseconds(5000); + var ex = Persist(time, false); + Assert.Equal(2, ex.Tokens); + Assert.Equal("EX 5", ex.ToString()); + } + + [Fact] + public void Persist_Milliseconds() + { + TimeSpan? time = TimeSpan.FromMilliseconds(5001); + var ex = Persist(time, false); + Assert.Equal(2, ex.Tokens); + Assert.Equal("PX 5001", ex.ToString()); + } + + [Fact] + public void Persist_None_False() + { + TimeSpan? time = null; + var ex = Persist(time, false); + Assert.Equal(0, ex.Tokens); + Assert.Equal("", ex.ToString()); + } + + [Fact] + public void Persist_None_True() + { + TimeSpan? time = null; + var ex = Persist(time, true); + Assert.Equal(1, ex.Tokens); + Assert.Equal("PERSIST", ex.ToString()); + } + + [Fact] + public void Persist_Both() + { + TimeSpan? time = TimeSpan.FromMilliseconds(5000); + var ex = Assert.Throws(() => Persist(time, true)); + Assert.Equal("persist", ex.ParamName); + Assert.StartsWith("Cannot specify both expiry and persist", ex.Message); + } + + [Fact] + public void KeepTtl_Seconds() + { + TimeSpan? time = TimeSpan.FromMilliseconds(5000); + var ex = KeepTtl(time, false); + Assert.Equal(2, ex.Tokens); + Assert.Equal("EX 5", ex.ToString()); + } + + [Fact] + public void KeepTtl_Milliseconds() + { + TimeSpan? time = TimeSpan.FromMilliseconds(5001); + var ex = KeepTtl(time, false); + Assert.Equal(2, ex.Tokens); + Assert.Equal("PX 5001", ex.ToString()); + } + + [Fact] + public void KeepTtl_None_False() + { + TimeSpan? time = null; + var ex = KeepTtl(time, false); + Assert.Equal(0, ex.Tokens); + Assert.Equal("", ex.ToString()); + } + + [Fact] + public void KeepTtl_None_True() + { + TimeSpan? time = null; + var ex = KeepTtl(time, true); + Assert.Equal(1, ex.Tokens); + Assert.Equal("KEEPTTL", ex.ToString()); + } + + [Fact] + public void KeepTtl_Both() + { + TimeSpan? time = TimeSpan.FromMilliseconds(5000); + var ex = Assert.Throws(() => KeepTtl(time, true)); + Assert.Equal("keepTtl", ex.ParamName); + Assert.StartsWith("Cannot specify both expiry and keepTtl", ex.Message); + } + + [Fact] + public void DateTime_Seconds() + { + var when = new DateTime(2025, 7, 23, 10, 4, 14, DateTimeKind.Utc); + var ex = new ExpiryToken(when); + Assert.Equal(2, ex.Tokens); + Assert.Equal("EXAT 1753265054", ex.ToString()); + } + + [Fact] + public void DateTime_Milliseconds() + { + var when = new DateTime(2025, 7, 23, 10, 4, 14, DateTimeKind.Utc); + when = when.AddMilliseconds(14); + var ex = new ExpiryToken(when); + Assert.Equal(2, ex.Tokens); + Assert.Equal("PXAT 1753265054014", ex.ToString()); + } +} diff --git a/tests/StackExchange.Redis.Tests/HashFieldTests.cs b/tests/StackExchange.Redis.Tests/HashFieldTests.cs index 3d1cb0c6e..2bb98eb85 100644 --- a/tests/StackExchange.Redis.Tests/HashFieldTests.cs +++ b/tests/StackExchange.Redis.Tests/HashFieldTests.cs @@ -18,6 +18,8 @@ public class HashFieldTests(ITestOutputHelper output, SharedConnectionFixture fi private readonly RedisValue[] fields = ["f1", "f2"]; + private readonly RedisValue[] values = [1, 2]; + [Fact] public void HashFieldExpire() { @@ -297,4 +299,273 @@ public void HashFieldPersistNoField() var fieldsResult = db.HashFieldPersist(hashKey, ["notExistingField1", "notExistingField2"]); Assert.Equal([PersistResult.NoSuchField, PersistResult.NoSuchField], fieldsResult); } + + [Fact] + public void HashFieldGetAndSetExpiry() + { + using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var hashKey = Me(); + + // testing with timespan + db.HashSet(hashKey, entries); + var fieldResult = db.HashFieldGetAndSetExpiry(hashKey, "f1", TimeSpan.FromHours(1)); + Assert.Equal(1, fieldResult); + var fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + + // testing with datetime + db.HashSet(hashKey, entries); + fieldResult = db.HashFieldGetAndSetExpiry(hashKey, "f1", DateTime.Now.AddMinutes(120)); + Assert.Equal(1, fieldResult); + fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.InRange(fieldTtl, TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + + // testing persist + fieldResult = db.HashFieldGetAndSetExpiry(hashKey, "f1", persist: true); + Assert.Equal(1, fieldResult); + fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.Equal(-1, fieldTtl); + + // testing multiple fields with timespan + db.HashSet(hashKey, entries); + var fieldResults = db.HashFieldGetAndSetExpiry(hashKey, fields, TimeSpan.FromHours(1)); + Assert.Equal(values, fieldResults); + var fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + + // testing multiple fields with datetime + db.HashSet(hashKey, entries); + fieldResults = db.HashFieldGetAndSetExpiry(hashKey, fields, DateTime.Now.AddMinutes(120)); + Assert.Equal(values, fieldResults); + fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + + // testing multiple fields with persist + fieldResults = db.HashFieldGetAndSetExpiry(hashKey, fields, persist: true); + Assert.Equal(values, fieldResults); + fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.Equal(new long[] { -1, -1 }, fieldTtls); + } + + [Fact] + public async Task HashFieldGetAndSetExpiryAsync() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var hashKey = Me(); + + // testing with timespan + db.HashSet(hashKey, entries); + var fieldResult = await db.HashFieldGetAndSetExpiryAsync(hashKey, "f1", TimeSpan.FromHours(1)); + Assert.Equal(1, fieldResult); + var fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + + // testing with datetime + db.HashSet(hashKey, entries); + fieldResult = await db.HashFieldGetAndSetExpiryAsync(hashKey, "f1", DateTime.Now.AddMinutes(120)); + Assert.Equal(1, fieldResult); + fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.InRange(fieldTtl, TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + + // testing persist + fieldResult = await db.HashFieldGetAndSetExpiryAsync(hashKey, "f1", persist: true); + Assert.Equal(1, fieldResult); + fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.Equal(-1, fieldTtl); + + // testing multiple fields with timespan + db.HashSet(hashKey, entries); + var fieldResults = await db.HashFieldGetAndSetExpiryAsync(hashKey, fields, TimeSpan.FromHours(1)); + Assert.Equal(values, fieldResults); + var fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + + // testing multiple fields with datetime + db.HashSet(hashKey, entries); + fieldResults = await db.HashFieldGetAndSetExpiryAsync(hashKey, fields, DateTime.Now.AddMinutes(120)); + Assert.Equal(values, fieldResults); + fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + + // testing multiple fields with persist + fieldResults = await db.HashFieldGetAndSetExpiryAsync(hashKey, fields, persist: true); + Assert.Equal(values, fieldResults); + fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.Equal(new long[] { -1, -1 }, fieldTtls); + } + + [Fact] + public void HashFieldSetAndSetExpiry() + { + using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var hashKey = Me(); + + // testing with timespan + var result = db.HashFieldSetAndSetExpiry(hashKey, "f1", 1, TimeSpan.FromHours(1)); + Assert.Equal(1, result); + var fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + + // testing with datetime + result = db.HashFieldSetAndSetExpiry(hashKey, "f1", 1, DateTime.Now.AddMinutes(120)); + Assert.Equal(1, result); + fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.InRange(fieldTtl, TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + + // testing with keepttl + result = db.HashFieldSetAndSetExpiry(hashKey, "f1", 1, keepTtl: true); + Assert.Equal(1, result); + fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.InRange(fieldTtl, TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + + // testing multiple fields with timespan + result = db.HashFieldSetAndSetExpiry(hashKey, entries, TimeSpan.FromHours(1)); + Assert.Equal(1, result); + var fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + + // testing multiple fields with datetime + result = db.HashFieldSetAndSetExpiry(hashKey, entries, DateTime.Now.AddMinutes(120)); + Assert.Equal(1, result); + fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + + // testing multiple fields with keepttl + result = db.HashFieldSetAndSetExpiry(hashKey, entries, keepTtl: true); + Assert.Equal(1, result); + fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + + // testing with ExpireWhen.Exists + db.KeyDelete(hashKey); + result = db.HashFieldSetAndSetExpiry(hashKey, "f1", 1, TimeSpan.FromHours(1), when: When.Exists); + Assert.Equal(0, result); // should not set because it doesnt exist + + // testing with ExpireWhen.NotExists + result = db.HashFieldSetAndSetExpiry(hashKey, "f1", 1, TimeSpan.FromHours(1), when: When.NotExists); + Assert.Equal(1, result); // should set because it doesnt exist + fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + + // testing with ExpireWhen.GreaterThanCurrentExpiry + result = db.HashFieldSetAndSetExpiry(hashKey, "f1", -1, keepTtl: true, when: When.Exists); + Assert.Equal(1, result); // should set because it exists + fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + } + + [Fact] + public async Task HashFieldSetAndSetExpiryAsync() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var hashKey = Me(); + + // testing with timespan + var result = await db.HashFieldSetAndSetExpiryAsync(hashKey, "f1", 1, TimeSpan.FromHours(1)); + Assert.Equal(1, result); + var fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + + // testing with datetime + result = await db.HashFieldSetAndSetExpiryAsync(hashKey, "f1", 1, DateTime.Now.AddMinutes(120)); + Assert.Equal(1, result); + fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.InRange(fieldTtl, TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + + // testing with keepttl + result = await db.HashFieldSetAndSetExpiryAsync(hashKey, "f1", 1, keepTtl: true); + Assert.Equal(1, result); + fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.InRange(fieldTtl, TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + + // testing multiple fields with timespan + result = await db.HashFieldSetAndSetExpiryAsync(hashKey, entries, TimeSpan.FromHours(1)); + Assert.Equal(1, result); + var fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + + // testing multiple fields with datetime + result = await db.HashFieldSetAndSetExpiryAsync(hashKey, entries, DateTime.Now.AddMinutes(120)); + Assert.Equal(1, result); + fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + + // testing multiple fields with keepttl + result = await db.HashFieldSetAndSetExpiryAsync(hashKey, entries, keepTtl: true); + Assert.Equal(1, result); + fieldTtls = db.HashFieldGetTimeToLive(hashKey, fields); + Assert.InRange(fieldTtls[0], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + Assert.InRange(fieldTtls[1], TimeSpan.FromMinutes(119).TotalMilliseconds, TimeSpan.FromHours(2).TotalMilliseconds); + + // testing with ExpireWhen.Exists + db.KeyDelete(hashKey); + result = await db.HashFieldSetAndSetExpiryAsync(hashKey, "f1", 1, TimeSpan.FromHours(1), when: When.Exists); + Assert.Equal(0, result); // should not set because it doesnt exist + + // testing with ExpireWhen.NotExists + result = await db.HashFieldSetAndSetExpiryAsync(hashKey, "f1", 1, TimeSpan.FromHours(1), when: When.NotExists); + Assert.Equal(1, result); // should set because it doesnt exist + fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + + // testing with ExpireWhen.GreaterThanCurrentExpiry + result = await db.HashFieldSetAndSetExpiryAsync(hashKey, "f1", -1, keepTtl: true, when: When.Exists); + Assert.Equal(1, result); // should set because it exists + fieldTtl = db.HashFieldGetTimeToLive(hashKey, new RedisValue[] { "f1" })[0]; + Assert.InRange(fieldTtl, TimeSpan.FromMinutes(59).TotalMilliseconds, TimeSpan.FromHours(1).TotalMilliseconds); + } + [Fact] + public void HashFieldGetAndDelete() + { + using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var hashKey = Me(); + + // single field + db.HashSet(hashKey, entries); + var fieldResult = db.HashFieldGetAndDelete(hashKey, "f1"); + Assert.Equal(1, fieldResult); + Assert.False(db.HashExists(hashKey, "f1")); + + // multiple fields + db.HashSet(hashKey, entries); + var fieldResults = db.HashFieldGetAndDelete(hashKey, fields); + Assert.Equal(values, fieldResults); + Assert.False(db.HashExists(hashKey, "f1")); + Assert.False(db.HashExists(hashKey, "f2")); + } + + [Fact] + public async Task HashFieldGetAndDeleteAsync() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var hashKey = Me(); + + // single field + db.HashSet(hashKey, entries); + var fieldResult = await db.HashFieldGetAndDeleteAsync(hashKey, "f1"); + Assert.Equal(1, fieldResult); + Assert.False(db.HashExists(hashKey, "f1")); + + // multiple fields + db.HashSet(hashKey, entries); + var fieldResults = await db.HashFieldGetAndDeleteAsync(hashKey, fields); + Assert.Equal(values, fieldResults); + Assert.False(db.HashExists(hashKey, "f1")); + Assert.False(db.HashExists(hashKey, "f2")); + } } diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs index 612ca182b..571961eb0 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Net; using System.Text; @@ -18,6 +19,14 @@ public sealed class KeyPrefixedDatabaseTests private readonly IDatabase mock; private readonly IDatabase prefixed; + internal static RedisKey[] IsKeys(params RedisKey[] expected) => IsRaw(expected); + internal static RedisValue[] IsValues(params RedisValue[] expected) => IsRaw(expected); + private static T[] IsRaw(T[] expected) + { + Expression> lambda = actual => actual.Length == expected.Length && expected.SequenceEqual(actual); + return Arg.Is(lambda); + } + public KeyPrefixedDatabaseTests() { mock = Substitute.For(); @@ -237,10 +246,8 @@ public void HyperLogLogMerge_1() [Fact] public void HyperLogLogMerge_2() { - RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - prefixed.HyperLogLogMerge("destination", keys, CommandFlags.None); - mock.Received().HyperLogLogMerge("prefix:destination", Arg.Is(valid), CommandFlags.None); + prefixed.HyperLogLogMerge("destination", ["a", "b"], CommandFlags.None); + mock.Received().HyperLogLogMerge("prefix:destination", IsKeys(["prefix:a", "prefix:b"]), CommandFlags.None); } [Fact] @@ -267,10 +274,8 @@ public void KeyDelete_1() [Fact] public void KeyDelete_2() { - RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; - prefixed.KeyDelete(keys, CommandFlags.None); - mock.Received().KeyDelete(Arg.Is(valid), CommandFlags.None); + prefixed.KeyDelete(["a", "b"], CommandFlags.None); + mock.Received().KeyDelete(IsKeys(["prefix:a", "prefix:b"]), CommandFlags.None); } [Fact] @@ -594,9 +599,8 @@ public void ScriptEvaluate_1() byte[] hash = Array.Empty(); RedisValue[] values = Array.Empty(); RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.ScriptEvaluate(hash, keys, values, CommandFlags.None); - mock.Received().ScriptEvaluate(hash, Arg.Is(valid), values, CommandFlags.None); + mock.Received().ScriptEvaluate(hash, IsKeys(["prefix:a", "prefix:b"]), values, CommandFlags.None); } [Fact] @@ -604,9 +608,8 @@ public void ScriptEvaluate_2() { RedisValue[] values = Array.Empty(); RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.ScriptEvaluate(script: "script", keys: keys, values: values, flags: CommandFlags.None); - mock.Received().ScriptEvaluate(script: "script", keys: Arg.Is(valid), values: values, flags: CommandFlags.None); + mock.Received().ScriptEvaluate(script: "script", keys: IsKeys(["prefix:a", "prefix:b"]), values: values, flags: CommandFlags.None); } [Fact] @@ -635,9 +638,8 @@ public void SetCombine_1() public void SetCombine_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.SetCombine(SetOperation.Intersect, keys, CommandFlags.None); - mock.Received().SetCombine(SetOperation.Intersect, Arg.Is(valid), CommandFlags.None); + mock.Received().SetCombine(SetOperation.Intersect, IsKeys(["prefix:a", "prefix:b"]), CommandFlags.None); } [Fact] @@ -651,9 +653,8 @@ public void SetCombineAndStore_1() public void SetCombineAndStore_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None); - mock.Received().SetCombineAndStore(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None); + mock.Received().SetCombineAndStore(SetOperation.Intersect, "prefix:destination", IsKeys(["prefix:a", "prefix:b"]), CommandFlags.None); } [Fact] @@ -674,9 +675,8 @@ public void SetContains_2() [Fact] public void SetIntersectionLength() { - var keys = new RedisKey[] { "key1", "key2" }; - prefixed.SetIntersectionLength(keys); - mock.Received().SetIntersectionLength(keys, 0, CommandFlags.None); + prefixed.SetIntersectionLength(["key1", "key2"]); + mock.Received().SetIntersectionLength(IsKeys(["prefix:key1", "prefix:key2"]), 0, CommandFlags.None); } [Fact] @@ -764,26 +764,24 @@ public void SetScan_Full() public void Sort() { RedisValue[] get = ["a", "#"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; prefixed.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); prefixed.Sort("key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); - mock.Received().Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", Arg.Is(valid), CommandFlags.None); - mock.Received().Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", Arg.Is(valid), CommandFlags.None); + mock.Received().Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", IsValues(["prefix:a", "#"]), CommandFlags.None); + mock.Received().Sort("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", IsValues(["prefix:a", "#"]), CommandFlags.None); } [Fact] public void SortAndStore() { RedisValue[] get = ["a", "#"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; prefixed.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); prefixed.SortAndStore("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); - mock.Received().SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", Arg.Is(valid), CommandFlags.None); - mock.Received().SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", Arg.Is(valid), CommandFlags.None); + mock.Received().SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", IsValues(["prefix:a", "#"]), CommandFlags.None); + mock.Received().SortAndStore("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", IsValues(["prefix:a", "#"]), CommandFlags.None); } [Fact] @@ -813,16 +811,15 @@ public void SortedSetAdd_3() public void SortedSetCombine() { RedisKey[] keys = ["a", "b"]; - prefixed.SortedSetCombine(SetOperation.Intersect, keys); - mock.Received().SortedSetCombine(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None); + prefixed.SortedSetCombine(SetOperation.Intersect, ["a", "b"]); + mock.Received().SortedSetCombine(SetOperation.Intersect, IsKeys(["prefix:a", "prefix:b"]), null, Aggregate.Sum, CommandFlags.None); } [Fact] public void SortedSetCombineWithScores() { - RedisKey[] keys = ["a", "b"]; - prefixed.SortedSetCombineWithScores(SetOperation.Intersect, keys); - mock.Received().SortedSetCombineWithScores(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None); + prefixed.SortedSetCombineWithScores(SetOperation.Intersect, ["a", "b"]); + mock.Received().SortedSetCombineWithScores(SetOperation.Intersect, IsKeys("prefix:a", "prefix:b"), null, Aggregate.Sum, CommandFlags.None); } [Fact] @@ -836,9 +833,8 @@ public void SortedSetCombineAndStore_1() public void SortedSetCombineAndStore_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.SetCombineAndStore(SetOperation.Intersect, "destination", keys, CommandFlags.None); - mock.Received().SetCombineAndStore(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None); + mock.Received().SetCombineAndStore(SetOperation.Intersect, "prefix:destination", IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] @@ -858,9 +854,8 @@ public void SortedSetIncrement() [Fact] public void SortedSetIntersectionLength() { - RedisKey[] keys = ["a", "b"]; - prefixed.SortedSetIntersectionLength(keys, 1, CommandFlags.None); - mock.Received().SortedSetIntersectionLength(keys, 1, CommandFlags.None); + prefixed.SortedSetIntersectionLength(["a", "b"], 1, CommandFlags.None); + mock.Received().SortedSetIntersectionLength(IsKeys("prefix:a", "prefix:b"), 1, CommandFlags.None); } [Fact] @@ -1255,45 +1250,40 @@ public void StringBitOperation_1() public void StringBitOperation_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.StringBitOperation(Bitwise.Xor, "destination", keys, CommandFlags.None); - mock.Received().StringBitOperation(Bitwise.Xor, "prefix:destination", Arg.Is(valid), CommandFlags.None); + mock.Received().StringBitOperation(Bitwise.Xor, "prefix:destination", IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] public void StringBitOperation_Diff() { RedisKey[] keys = ["x", "y1", "y2"]; - Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; prefixed.StringBitOperation(Bitwise.Diff, "destination", keys, CommandFlags.None); - mock.Received().StringBitOperation(Bitwise.Diff, "prefix:destination", Arg.Is(valid), CommandFlags.None); + mock.Received().StringBitOperation(Bitwise.Diff, "prefix:destination", IsKeys("prefix:x", "prefix:y1", "prefix:y2"), CommandFlags.None); } [Fact] public void StringBitOperation_Diff1() { RedisKey[] keys = ["x", "y1", "y2"]; - Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; prefixed.StringBitOperation(Bitwise.Diff1, "destination", keys, CommandFlags.None); - mock.Received().StringBitOperation(Bitwise.Diff1, "prefix:destination", Arg.Is(valid), CommandFlags.None); + mock.Received().StringBitOperation(Bitwise.Diff1, "prefix:destination", IsKeys("prefix:x", "prefix:y1", "prefix:y2"), CommandFlags.None); } [Fact] public void StringBitOperation_AndOr() { RedisKey[] keys = ["x", "y1", "y2"]; - Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; prefixed.StringBitOperation(Bitwise.AndOr, "destination", keys, CommandFlags.None); - mock.Received().StringBitOperation(Bitwise.AndOr, "prefix:destination", Arg.Is(valid), CommandFlags.None); + mock.Received().StringBitOperation(Bitwise.AndOr, "prefix:destination", IsKeys("prefix:x", "prefix:y1", "prefix:y2"), CommandFlags.None); } [Fact] public void StringBitOperation_One() { RedisKey[] keys = ["a", "b", "c"]; - Expression> valid = _ => _.Length == 3 && _[0] == "prefix:a" && _[1] == "prefix:b" && _[2] == "prefix:c"; prefixed.StringBitOperation(Bitwise.One, "destination", keys, CommandFlags.None); - mock.Received().StringBitOperation(Bitwise.One, "prefix:destination", Arg.Is(valid), CommandFlags.None); + mock.Received().StringBitOperation(Bitwise.One, "prefix:destination", IsKeys("prefix:a", "prefix:b", "prefix:c"), CommandFlags.None); } [Fact] @@ -1335,9 +1325,8 @@ public void StringGet_1() public void StringGet_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; prefixed.StringGet(keys, CommandFlags.None); - mock.Received().StringGet(Arg.Is(valid), CommandFlags.None); + mock.Received().StringGet(IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] @@ -1442,4 +1431,364 @@ public void StringSetRange() prefixed.StringSetRange("key", 123, "value", CommandFlags.None); mock.Received().StringSetRange("prefix:key", 123, "value", CommandFlags.None); } + + [Fact] + public void Execute_1() + { + prefixed.Execute("CUSTOM", "arg1", (RedisKey)"arg2"); + mock.Received().Execute("CUSTOM", Arg.Is(args => args.Length == 2 && args[0].Equals("arg1") && args[1].Equals((RedisKey)"prefix:arg2")), CommandFlags.None); + } + + [Fact] + public void Execute_2() + { + var args = new List { "arg1", (RedisKey)"arg2" }; + prefixed.Execute("CUSTOM", args, CommandFlags.None); + mock.Received().Execute("CUSTOM", Arg.Is>(a => a.Count == 2 && a.ElementAt(0).Equals("arg1") && a.ElementAt(1).Equals((RedisKey)"prefix:arg2"))!, CommandFlags.None); + } + + [Fact] + public void GeoAdd_1() + { + prefixed.GeoAdd("key", 1.23, 4.56, "member", CommandFlags.None); + mock.Received().GeoAdd("prefix:key", 1.23, 4.56, "member", CommandFlags.None); + } + + [Fact] + public void GeoAdd_2() + { + var geoEntry = new GeoEntry(1.23, 4.56, "member"); + prefixed.GeoAdd("key", geoEntry, CommandFlags.None); + mock.Received().GeoAdd("prefix:key", geoEntry, CommandFlags.None); + } + + [Fact] + public void GeoAdd_3() + { + var geoEntries = new GeoEntry[] { new GeoEntry(1.23, 4.56, "member1") }; + prefixed.GeoAdd("key", geoEntries, CommandFlags.None); + mock.Received().GeoAdd("prefix:key", geoEntries, CommandFlags.None); + } + + [Fact] + public void GeoRemove() + { + prefixed.GeoRemove("key", "member", CommandFlags.None); + mock.Received().GeoRemove("prefix:key", "member", CommandFlags.None); + } + + [Fact] + public void GeoDistance() + { + prefixed.GeoDistance("key", "member1", "member2", GeoUnit.Meters, CommandFlags.None); + mock.Received().GeoDistance("prefix:key", "member1", "member2", GeoUnit.Meters, CommandFlags.None); + } + + [Fact] + public void GeoHash_1() + { + prefixed.GeoHash("key", "member", CommandFlags.None); + mock.Received().GeoHash("prefix:key", "member", CommandFlags.None); + } + + [Fact] + public void GeoHash_2() + { + var members = new RedisValue[] { "member1", "member2" }; + prefixed.GeoHash("key", members, CommandFlags.None); + mock.Received().GeoHash("prefix:key", members, CommandFlags.None); + } + + [Fact] + public void GeoPosition_1() + { + prefixed.GeoPosition("key", "member", CommandFlags.None); + mock.Received().GeoPosition("prefix:key", "member", CommandFlags.None); + } + + [Fact] + public void GeoPosition_2() + { + var members = new RedisValue[] { "member1", "member2" }; + prefixed.GeoPosition("key", members, CommandFlags.None); + mock.Received().GeoPosition("prefix:key", members, CommandFlags.None); + } + + [Fact] + public void GeoRadius_1() + { + prefixed.GeoRadius("key", "member", 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + mock.Received().GeoRadius("prefix:key", "member", 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public void GeoRadius_2() + { + prefixed.GeoRadius("key", 1.23, 4.56, 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + mock.Received().GeoRadius("prefix:key", 1.23, 4.56, 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public void GeoSearch_1() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + prefixed.GeoSearch("key", "member", shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + mock.Received().GeoSearch("prefix:key", "member", shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public void GeoSearch_2() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + prefixed.GeoSearch("key", 1.23, 4.56, shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + mock.Received().GeoSearch("prefix:key", 1.23, 4.56, shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public void GeoSearchAndStore_1() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + prefixed.GeoSearchAndStore("source", "destination", "member", shape, 10, true, Order.Ascending, false, CommandFlags.None); + mock.Received().GeoSearchAndStore("prefix:source", "prefix:destination", "member", shape, 10, true, Order.Ascending, false, CommandFlags.None); + } + + [Fact] + public void GeoSearchAndStore_2() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + prefixed.GeoSearchAndStore("source", "destination", 1.23, 4.56, shape, 10, true, Order.Ascending, false, CommandFlags.None); + mock.Received().GeoSearchAndStore("prefix:source", "prefix:destination", 1.23, 4.56, shape, 10, true, Order.Ascending, false, CommandFlags.None); + } + + [Fact] + public void HashFieldExpire_1() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + var expiry = TimeSpan.FromSeconds(60); + prefixed.HashFieldExpire("key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + mock.Received().HashFieldExpire("prefix:key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + } + + [Fact] + public void HashFieldExpire_2() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + var expiry = DateTime.Now.AddMinutes(1); + prefixed.HashFieldExpire("key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + mock.Received().HashFieldExpire("prefix:key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + } + + [Fact] + public void HashFieldGetExpireDateTime() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + prefixed.HashFieldGetExpireDateTime("key", hashFields, CommandFlags.None); + mock.Received().HashFieldGetExpireDateTime("prefix:key", hashFields, CommandFlags.None); + } + + [Fact] + public void HashFieldPersist() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + prefixed.HashFieldPersist("key", hashFields, CommandFlags.None); + mock.Received().HashFieldPersist("prefix:key", hashFields, CommandFlags.None); + } + + [Fact] + public void HashFieldGetTimeToLive() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + prefixed.HashFieldGetTimeToLive("key", hashFields, CommandFlags.None); + mock.Received().HashFieldGetTimeToLive("prefix:key", hashFields, CommandFlags.None); + } + + [Fact] + public void HashGetLease() + { + prefixed.HashGetLease("key", "field", CommandFlags.None); + mock.Received().HashGetLease("prefix:key", "field", CommandFlags.None); + } + + [Fact] + public void HashFieldGetAndDelete_1() + { + prefixed.HashFieldGetAndDelete("key", "field", CommandFlags.None); + mock.Received().HashFieldGetAndDelete("prefix:key", "field", CommandFlags.None); + } + + [Fact] + public void HashFieldGetAndDelete_2() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + prefixed.HashFieldGetAndDelete("key", hashFields, CommandFlags.None); + mock.Received().HashFieldGetAndDelete("prefix:key", hashFields, CommandFlags.None); + } + + [Fact] + public void HashFieldGetLeaseAndDelete() + { + prefixed.HashFieldGetLeaseAndDelete("key", "field", CommandFlags.None); + mock.Received().HashFieldGetLeaseAndDelete("prefix:key", "field", CommandFlags.None); + } + + [Fact] + public void HashFieldGetAndSetExpiry_1() + { + var expiry = TimeSpan.FromMinutes(5); + prefixed.HashFieldGetAndSetExpiry("key", "field", expiry, false, CommandFlags.None); + mock.Received().HashFieldGetAndSetExpiry("prefix:key", "field", expiry, false, CommandFlags.None); + } + + [Fact] + public void HashFieldGetAndSetExpiry_2() + { + var expiry = DateTime.Now.AddMinutes(5); + prefixed.HashFieldGetAndSetExpiry("key", "field", expiry, CommandFlags.None); + mock.Received().HashFieldGetAndSetExpiry("prefix:key", "field", expiry, CommandFlags.None); + } + + [Fact] + public void HashFieldGetLeaseAndSetExpiry_1() + { + var expiry = TimeSpan.FromMinutes(5); + prefixed.HashFieldGetLeaseAndSetExpiry("key", "field", expiry, false, CommandFlags.None); + mock.Received().HashFieldGetLeaseAndSetExpiry("prefix:key", "field", expiry, false, CommandFlags.None); + } + + [Fact] + public void HashFieldGetLeaseAndSetExpiry_2() + { + var expiry = DateTime.Now.AddMinutes(5); + prefixed.HashFieldGetLeaseAndSetExpiry("key", "field", expiry, CommandFlags.None); + mock.Received().HashFieldGetLeaseAndSetExpiry("prefix:key", "field", expiry, CommandFlags.None); + } + [Fact] + public void StringGetLease() + { + prefixed.StringGetLease("key", CommandFlags.None); + mock.Received().StringGetLease("prefix:key", CommandFlags.None); + } + + [Fact] + public void StringGetSetExpiry_1() + { + var expiry = TimeSpan.FromMinutes(5); + prefixed.StringGetSetExpiry("key", expiry, CommandFlags.None); + mock.Received().StringGetSetExpiry("prefix:key", expiry, CommandFlags.None); + } + + [Fact] + public void StringGetSetExpiry_2() + { + var expiry = DateTime.Now.AddMinutes(5); + prefixed.StringGetSetExpiry("key", expiry, CommandFlags.None); + mock.Received().StringGetSetExpiry("prefix:key", expiry, CommandFlags.None); + } + + [Fact] + public void StringSetAndGet_1() + { + var expiry = TimeSpan.FromMinutes(5); + prefixed.StringSetAndGet("key", "value", expiry, When.Always, CommandFlags.None); + mock.Received().StringSetAndGet("prefix:key", "value", expiry, When.Always, CommandFlags.None); + } + + [Fact] + public void StringSetAndGet_2() + { + var expiry = TimeSpan.FromMinutes(5); + prefixed.StringSetAndGet("key", "value", expiry, false, When.Always, CommandFlags.None); + mock.Received().StringSetAndGet("prefix:key", "value", expiry, false, When.Always, CommandFlags.None); + } + [Fact] + public void StringLongestCommonSubsequence() + { + prefixed.StringLongestCommonSubsequence("key1", "key2", CommandFlags.None); + mock.Received().StringLongestCommonSubsequence("prefix:key1", "prefix:key2", CommandFlags.None); + } + + [Fact] + public void StringLongestCommonSubsequenceLength() + { + prefixed.StringLongestCommonSubsequenceLength("key1", "key2", CommandFlags.None); + mock.Received().StringLongestCommonSubsequenceLength("prefix:key1", "prefix:key2", CommandFlags.None); + } + + [Fact] + public void StringLongestCommonSubsequenceWithMatches() + { + prefixed.StringLongestCommonSubsequenceWithMatches("key1", "key2", 5, CommandFlags.None); + mock.Received().StringLongestCommonSubsequenceWithMatches("prefix:key1", "prefix:key2", 5, CommandFlags.None); + } + [Fact] + public void IsConnected() + { + prefixed.IsConnected("key", CommandFlags.None); + mock.Received().IsConnected("prefix:key", CommandFlags.None); + } + [Fact] + public void StreamAdd_WithTrimMode_1() + { + prefixed.StreamAdd("key", "field", "value", "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + mock.Received().StreamAdd("prefix:key", "field", "value", "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public void StreamAdd_WithTrimMode_2() + { + var fields = new NameValueEntry[] { new NameValueEntry("field", "value") }; + prefixed.StreamAdd("key", fields, "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + mock.Received().StreamAdd("prefix:key", fields, "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public void StreamTrim_WithMode() + { + prefixed.StreamTrim("key", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + mock.Received().StreamTrim("prefix:key", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public void StreamTrimByMinId_WithMode() + { + prefixed.StreamTrimByMinId("key", "1111111111", false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + mock.Received().StreamTrimByMinId("prefix:key", "1111111111", false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public void StreamReadGroup_WithNoAck_1() + { + prefixed.StreamReadGroup("key", "group", "consumer", "0-0", 10, true, CommandFlags.None); + mock.Received().StreamReadGroup("prefix:key", "group", "consumer", "0-0", 10, true, CommandFlags.None); + } + + [Fact] + public void StreamReadGroup_WithNoAck_2() + { + var streamPositions = new StreamPosition[] { new StreamPosition("key", "0-0") }; + prefixed.StreamReadGroup(streamPositions, "group", "consumer", 10, true, CommandFlags.None); + mock.Received().StreamReadGroup(streamPositions, "group", "consumer", 10, true, CommandFlags.None); + } + + [Fact] + public void StreamTrim_Simple() + { + prefixed.StreamTrim("key", 1000, true, CommandFlags.None); + mock.Received().StreamTrim("prefix:key", 1000, true, CommandFlags.None); + } + + [Fact] + public void StreamReadGroup_Simple_1() + { + prefixed.StreamReadGroup("key", "group", "consumer", "0-0", 10, CommandFlags.None); + mock.Received().StreamReadGroup("prefix:key", "group", "consumer", "0-0", 10, CommandFlags.None); + } + + [Fact] + public void StreamReadGroup_Simple_2() + { + var streamPositions = new StreamPosition[] { new StreamPosition("key", "0-0") }; + prefixed.StreamReadGroup(streamPositions, "group", "consumer", 10, CommandFlags.None); + mock.Received().StreamReadGroup(streamPositions, "group", "consumer", 10, CommandFlags.None); + } } diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs index b8cf9a4b9..cbef3d9e7 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using System.Net; using System.Text; @@ -7,6 +8,7 @@ using NSubstitute; using StackExchange.Redis.KeyspaceIsolation; using Xunit; +using static StackExchange.Redis.Tests.KeyPrefixedDatabaseTests; // for IsKeys etc namespace StackExchange.Redis.Tests { @@ -177,9 +179,8 @@ public async Task HyperLogLogMergeAsync_1() public async Task HyperLogLogMergeAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.HyperLogLogMergeAsync("destination", keys, CommandFlags.None); - await mock.Received().HyperLogLogMergeAsync("prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().HyperLogLogMergeAsync("prefix:destination", IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] @@ -214,9 +215,8 @@ public async Task KeyDeleteAsync_1() public async Task KeyDeleteAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.KeyDeleteAsync(keys, CommandFlags.None); - await mock.Received().KeyDeleteAsync(Arg.Is(valid), CommandFlags.None); + await mock.Received().KeyDeleteAsync(IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] @@ -538,9 +538,8 @@ public async Task ScriptEvaluateAsync_1() byte[] hash = Array.Empty(); RedisValue[] values = Array.Empty(); RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.ScriptEvaluateAsync(hash, keys, values, CommandFlags.None); - await mock.Received().ScriptEvaluateAsync(hash, Arg.Is(valid), values, CommandFlags.None); + await mock.Received().ScriptEvaluateAsync(hash, IsKeys("prefix:a", "prefix:b"), values, CommandFlags.None); } [Fact] @@ -548,9 +547,8 @@ public async Task ScriptEvaluateAsync_2() { RedisValue[] values = Array.Empty(); RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.ScriptEvaluateAsync("script", keys, values, CommandFlags.None); - await mock.Received().ScriptEvaluateAsync(script: "script", keys: Arg.Is(valid), values: values, flags: CommandFlags.None); + await mock.Received().ScriptEvaluateAsync(script: "script", keys: IsKeys("prefix:a", "prefix:b"), values: values, flags: CommandFlags.None); } [Fact] @@ -579,9 +577,8 @@ public async Task SetCombineAndStoreAsync_1() public async Task SetCombineAndStoreAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", keys, CommandFlags.None); - await mock.Received().SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] @@ -595,9 +592,8 @@ public async Task SetCombineAsync_1() public async Task SetCombineAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.SetCombineAsync(SetOperation.Intersect, keys, CommandFlags.None); - await mock.Received().SetCombineAsync(SetOperation.Intersect, Arg.Is(valid), CommandFlags.None); + await mock.Received().SetCombineAsync(SetOperation.Intersect, IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] @@ -618,9 +614,8 @@ public async Task SetContainsAsync_2() [Fact] public async Task SetIntersectionLengthAsync() { - var keys = new RedisKey[] { "key1", "key2" }; - await prefixed.SetIntersectionLengthAsync(keys); - await mock.Received().SetIntersectionLengthAsync(keys, 0, CommandFlags.None); + await prefixed.SetIntersectionLengthAsync(["key1", "key2"]); + await mock.Received().SetIntersectionLengthAsync(IsKeys("prefix:key1", "prefix:key2"), 0, CommandFlags.None); } [Fact] @@ -694,26 +689,24 @@ public async Task SetRemoveAsync_2() public async Task SortAndStoreAsync() { RedisValue[] get = ["a", "#"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; await prefixed.SortAndStoreAsync("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); await prefixed.SortAndStoreAsync("destination", "key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); - await mock.Received().SortAndStoreAsync("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", Arg.Is(valid), CommandFlags.None); - await mock.Received().SortAndStoreAsync("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", Arg.Is(valid), CommandFlags.None); + await mock.Received().SortAndStoreAsync("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", IsValues("prefix:a", "#"), CommandFlags.None); + await mock.Received().SortAndStoreAsync("prefix:destination", "prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", IsValues("prefix:a", "#"), CommandFlags.None); } [Fact] public async Task SortAsync() { RedisValue[] get = ["a", "#"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "#"; await prefixed.SortAsync("key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", get, CommandFlags.None); await prefixed.SortAsync("key", 123, 456, Order.Descending, SortType.Alphabetic, "by", get, CommandFlags.None); - await mock.Received().SortAsync("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", Arg.Is(valid), CommandFlags.None); - await mock.Received().SortAsync("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", Arg.Is(valid), CommandFlags.None); + await mock.Received().SortAsync("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "nosort", IsValues("prefix:a", "#"), CommandFlags.None); + await mock.Received().SortAsync("prefix:key", 123, 456, Order.Descending, SortType.Alphabetic, "prefix:by", IsValues("prefix:a", "#"), CommandFlags.None); } [Fact] @@ -742,17 +735,15 @@ public async Task SortedSetAddAsync_3() [Fact] public async Task SortedSetCombineAsync() { - RedisKey[] keys = ["a", "b"]; - await prefixed.SortedSetCombineAsync(SetOperation.Intersect, keys); - await mock.Received().SortedSetCombineAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None); + await prefixed.SortedSetCombineAsync(SetOperation.Intersect, ["a", "b"]); + await mock.Received().SortedSetCombineAsync(SetOperation.Intersect, IsKeys("prefix:a", "prefix:b"), null, Aggregate.Sum, CommandFlags.None); } [Fact] public async Task SortedSetCombineWithScoresAsync() { - RedisKey[] keys = ["a", "b"]; - await prefixed.SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys); - await mock.Received().SortedSetCombineWithScoresAsync(SetOperation.Intersect, keys, null, Aggregate.Sum, CommandFlags.None); + await prefixed.SortedSetCombineWithScoresAsync(SetOperation.Intersect, ["a", "b"]); + await mock.Received().SortedSetCombineWithScoresAsync(SetOperation.Intersect, IsKeys("prefix:a", "prefix:b"), null, Aggregate.Sum, CommandFlags.None); } [Fact] @@ -766,9 +757,8 @@ public async Task SortedSetCombineAndStoreAsync_1() public async Task SortedSetCombineAndStoreAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.SetCombineAndStoreAsync(SetOperation.Intersect, "destination", keys, CommandFlags.None); - await mock.Received().SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().SetCombineAndStoreAsync(SetOperation.Intersect, "prefix:destination", IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] @@ -788,9 +778,8 @@ public async Task SortedSetIncrementAsync() [Fact] public async Task SortedSetIntersectionLengthAsync() { - RedisKey[] keys = ["a", "b"]; - await prefixed.SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None); - await mock.Received().SortedSetIntersectionLengthAsync(keys, 1, CommandFlags.None); + await prefixed.SortedSetIntersectionLengthAsync(["a", "b"], 1, CommandFlags.None); + await mock.Received().SortedSetIntersectionLengthAsync(IsKeys("prefix:a", "prefix:b"), 1, CommandFlags.None); } [Fact] @@ -1171,45 +1160,40 @@ public async Task StringBitOperationAsync_1() public async Task StringBitOperationAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.StringBitOperationAsync(Bitwise.Xor, "destination", keys, CommandFlags.None); - await mock.Received().StringBitOperationAsync(Bitwise.Xor, "prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.Xor, "prefix:destination", IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] public async Task StringBitOperationAsync_Diff() { RedisKey[] keys = ["x", "y1", "y2"]; - Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; await prefixed.StringBitOperationAsync(Bitwise.Diff, "destination", keys, CommandFlags.None); - await mock.Received().StringBitOperationAsync(Bitwise.Diff, "prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.Diff, "prefix:destination", IsKeys("prefix:x", "prefix:y1", "prefix:y2"), CommandFlags.None); } [Fact] public async Task StringBitOperationAsync_Diff1() { RedisKey[] keys = ["x", "y1", "y2"]; - Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; await prefixed.StringBitOperationAsync(Bitwise.Diff1, "destination", keys, CommandFlags.None); - await mock.Received().StringBitOperationAsync(Bitwise.Diff1, "prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.Diff1, "prefix:destination", IsKeys("prefix:x", "prefix:y1", "prefix:y2"), CommandFlags.None); } [Fact] public async Task StringBitOperationAsync_AndOr() { RedisKey[] keys = ["x", "y1", "y2"]; - Expression> valid = _ => _.Length == 3 && _[0] == "prefix:x" && _[1] == "prefix:y1" && _[2] == "prefix:y2"; await prefixed.StringBitOperationAsync(Bitwise.AndOr, "destination", keys, CommandFlags.None); - await mock.Received().StringBitOperationAsync(Bitwise.AndOr, "prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.AndOr, "prefix:destination", IsKeys("prefix:x", "prefix:y1", "prefix:y2"), CommandFlags.None); } [Fact] public async Task StringBitOperationAsync_One() { RedisKey[] keys = ["a", "b", "c"]; - Expression> valid = _ => _.Length == 3 && _[0] == "prefix:a" && _[1] == "prefix:b" && _[2] == "prefix:c"; await prefixed.StringBitOperationAsync(Bitwise.One, "destination", keys, CommandFlags.None); - await mock.Received().StringBitOperationAsync(Bitwise.One, "prefix:destination", Arg.Is(valid), CommandFlags.None); + await mock.Received().StringBitOperationAsync(Bitwise.One, "prefix:destination", IsKeys("prefix:a", "prefix:b", "prefix:c"), CommandFlags.None); } [Fact] @@ -1251,9 +1235,8 @@ public async Task StringGetAsync_1() public async Task StringGetAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.StringGetAsync(keys, CommandFlags.None); - await mock.Received().StringGetAsync(Arg.Is(valid), CommandFlags.None); + await mock.Received().StringGetAsync(IsKeys("prefix:a", "prefix:b"), CommandFlags.None); } [Fact] @@ -1370,9 +1353,392 @@ public async Task KeyTouchAsync_1() public async Task KeyTouchAsync_2() { RedisKey[] keys = ["a", "b"]; - Expression> valid = _ => _.Length == 2 && _[0] == "prefix:a" && _[1] == "prefix:b"; await prefixed.KeyTouchAsync(keys, CommandFlags.None); - await mock.Received().KeyTouchAsync(Arg.Is(valid), CommandFlags.None); + await mock.Received().KeyTouchAsync(IsKeys("prefix:a", "prefix:b"), CommandFlags.None); + } + [Fact] + public async Task ExecuteAsync_1() + { + await prefixed.ExecuteAsync("CUSTOM", "arg1", (RedisKey)"arg2"); + await mock.Received().ExecuteAsync("CUSTOM", Arg.Is(args => args.Length == 2 && args[0].Equals("arg1") && args[1].Equals((RedisKey)"prefix:arg2")), CommandFlags.None); + } + + [Fact] + public async Task ExecuteAsync_2() + { + var args = new List { "arg1", (RedisKey)"arg2" }; + await prefixed.ExecuteAsync("CUSTOM", args, CommandFlags.None); + await mock.Received().ExecuteAsync("CUSTOM", Arg.Is?>(a => a != null && a.Count == 2 && a.ElementAt(0).Equals("arg1") && a.ElementAt(1).Equals((RedisKey)"prefix:arg2")), CommandFlags.None); + } + [Fact] + public async Task GeoAddAsync_1() + { + await prefixed.GeoAddAsync("key", 1.23, 4.56, "member", CommandFlags.None); + await mock.Received().GeoAddAsync("prefix:key", 1.23, 4.56, "member", CommandFlags.None); + } + + [Fact] + public async Task GeoAddAsync_2() + { + var geoEntry = new GeoEntry(1.23, 4.56, "member"); + await prefixed.GeoAddAsync("key", geoEntry, CommandFlags.None); + await mock.Received().GeoAddAsync("prefix:key", geoEntry, CommandFlags.None); + } + + [Fact] + public async Task GeoAddAsync_3() + { + var geoEntries = new GeoEntry[] { new GeoEntry(1.23, 4.56, "member1") }; + await prefixed.GeoAddAsync("key", geoEntries, CommandFlags.None); + await mock.Received().GeoAddAsync("prefix:key", geoEntries, CommandFlags.None); + } + + [Fact] + public async Task GeoRemoveAsync() + { + await prefixed.GeoRemoveAsync("key", "member", CommandFlags.None); + await mock.Received().GeoRemoveAsync("prefix:key", "member", CommandFlags.None); + } + + [Fact] + public async Task GeoDistanceAsync() + { + await prefixed.GeoDistanceAsync("key", "member1", "member2", GeoUnit.Meters, CommandFlags.None); + await mock.Received().GeoDistanceAsync("prefix:key", "member1", "member2", GeoUnit.Meters, CommandFlags.None); + } + + [Fact] + public async Task GeoHashAsync_1() + { + await prefixed.GeoHashAsync("key", "member", CommandFlags.None); + await mock.Received().GeoHashAsync("prefix:key", "member", CommandFlags.None); + } + + [Fact] + public async Task GeoHashAsync_2() + { + var members = new RedisValue[] { "member1", "member2" }; + await prefixed.GeoHashAsync("key", members, CommandFlags.None); + await mock.Received().GeoHashAsync("prefix:key", members, CommandFlags.None); + } + + [Fact] + public async Task GeoPositionAsync_1() + { + await prefixed.GeoPositionAsync("key", "member", CommandFlags.None); + await mock.Received().GeoPositionAsync("prefix:key", "member", CommandFlags.None); + } + + [Fact] + public async Task GeoPositionAsync_2() + { + var members = new RedisValue[] { "member1", "member2" }; + await prefixed.GeoPositionAsync("key", members, CommandFlags.None); + await mock.Received().GeoPositionAsync("prefix:key", members, CommandFlags.None); + } + + [Fact] + public async Task GeoRadiusAsync_1() + { + await prefixed.GeoRadiusAsync("key", "member", 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + await mock.Received().GeoRadiusAsync("prefix:key", "member", 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public async Task GeoRadiusAsync_2() + { + await prefixed.GeoRadiusAsync("key", 1.23, 4.56, 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + await mock.Received().GeoRadiusAsync("prefix:key", 1.23, 4.56, 100, GeoUnit.Meters, 10, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public async Task GeoSearchAsync_1() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + await prefixed.GeoSearchAsync("key", "member", shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + await mock.Received().GeoSearchAsync("prefix:key", "member", shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public async Task GeoSearchAsync_2() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + await prefixed.GeoSearchAsync("key", 1.23, 4.56, shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + await mock.Received().GeoSearchAsync("prefix:key", 1.23, 4.56, shape, 10, true, Order.Ascending, GeoRadiusOptions.Default, CommandFlags.None); + } + + [Fact] + public async Task GeoSearchAndStoreAsync_1() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + await prefixed.GeoSearchAndStoreAsync("source", "destination", "member", shape, 10, true, Order.Ascending, false, CommandFlags.None); + await mock.Received().GeoSearchAndStoreAsync("prefix:source", "prefix:destination", "member", shape, 10, true, Order.Ascending, false, CommandFlags.None); + } + + [Fact] + public async Task GeoSearchAndStoreAsync_2() + { + var shape = new GeoSearchCircle(100, GeoUnit.Meters); + await prefixed.GeoSearchAndStoreAsync("source", "destination", 1.23, 4.56, shape, 10, true, Order.Ascending, false, CommandFlags.None); + await mock.Received().GeoSearchAndStoreAsync("prefix:source", "prefix:destination", 1.23, 4.56, shape, 10, true, Order.Ascending, false, CommandFlags.None); + } + [Fact] + public async Task HashFieldExpireAsync_1() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + var expiry = TimeSpan.FromSeconds(60); + await prefixed.HashFieldExpireAsync("key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + await mock.Received().HashFieldExpireAsync("prefix:key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + } + + [Fact] + public async Task HashFieldExpireAsync_2() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + var expiry = DateTime.Now.AddMinutes(1); + await prefixed.HashFieldExpireAsync("key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + await mock.Received().HashFieldExpireAsync("prefix:key", hashFields, expiry, ExpireWhen.Always, CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetExpireDateTimeAsync() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + await prefixed.HashFieldGetExpireDateTimeAsync("key", hashFields, CommandFlags.None); + await mock.Received().HashFieldGetExpireDateTimeAsync("prefix:key", hashFields, CommandFlags.None); + } + + [Fact] + public async Task HashFieldPersistAsync() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + await prefixed.HashFieldPersistAsync("key", hashFields, CommandFlags.None); + await mock.Received().HashFieldPersistAsync("prefix:key", hashFields, CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetTimeToLiveAsync() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + await prefixed.HashFieldGetTimeToLiveAsync("key", hashFields, CommandFlags.None); + await mock.Received().HashFieldGetTimeToLiveAsync("prefix:key", hashFields, CommandFlags.None); + } + [Fact] + public async Task HashGetLeaseAsync() + { + await prefixed.HashGetLeaseAsync("key", "field", CommandFlags.None); + await mock.Received().HashGetLeaseAsync("prefix:key", "field", CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetAndDeleteAsync_1() + { + await prefixed.HashFieldGetAndDeleteAsync("key", "field", CommandFlags.None); + await mock.Received().HashFieldGetAndDeleteAsync("prefix:key", "field", CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetAndDeleteAsync_2() + { + var hashFields = new RedisValue[] { "field1", "field2" }; + await prefixed.HashFieldGetAndDeleteAsync("key", hashFields, CommandFlags.None); + await mock.Received().HashFieldGetAndDeleteAsync("prefix:key", hashFields, CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetLeaseAndDeleteAsync() + { + await prefixed.HashFieldGetLeaseAndDeleteAsync("key", "field", CommandFlags.None); + await mock.Received().HashFieldGetLeaseAndDeleteAsync("prefix:key", "field", CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetAndSetExpiryAsync_1() + { + var expiry = TimeSpan.FromMinutes(5); + await prefixed.HashFieldGetAndSetExpiryAsync("key", "field", expiry, false, CommandFlags.None); + await mock.Received().HashFieldGetAndSetExpiryAsync("prefix:key", "field", expiry, false, CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetAndSetExpiryAsync_2() + { + var expiry = DateTime.Now.AddMinutes(5); + await prefixed.HashFieldGetAndSetExpiryAsync("key", "field", expiry, CommandFlags.None); + await mock.Received().HashFieldGetAndSetExpiryAsync("prefix:key", "field", expiry, CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetLeaseAndSetExpiryAsync_1() + { + var expiry = TimeSpan.FromMinutes(5); + await prefixed.HashFieldGetLeaseAndSetExpiryAsync("key", "field", expiry, false, CommandFlags.None); + await mock.Received().HashFieldGetLeaseAndSetExpiryAsync("prefix:key", "field", expiry, false, CommandFlags.None); + } + + [Fact] + public async Task HashFieldGetLeaseAndSetExpiryAsync_2() + { + var expiry = DateTime.Now.AddMinutes(5); + await prefixed.HashFieldGetLeaseAndSetExpiryAsync("key", "field", expiry, CommandFlags.None); + await mock.Received().HashFieldGetLeaseAndSetExpiryAsync("prefix:key", "field", expiry, CommandFlags.None); + } + [Fact] + public async Task StringGetLeaseAsync() + { + await prefixed.StringGetLeaseAsync("key", CommandFlags.None); + await mock.Received().StringGetLeaseAsync("prefix:key", CommandFlags.None); + } + + [Fact] + public async Task StringGetSetExpiryAsync_1() + { + var expiry = TimeSpan.FromMinutes(5); + await prefixed.StringGetSetExpiryAsync("key", expiry, CommandFlags.None); + await mock.Received().StringGetSetExpiryAsync("prefix:key", expiry, CommandFlags.None); + } + + [Fact] + public async Task StringGetSetExpiryAsync_2() + { + var expiry = DateTime.Now.AddMinutes(5); + await prefixed.StringGetSetExpiryAsync("key", expiry, CommandFlags.None); + await mock.Received().StringGetSetExpiryAsync("prefix:key", expiry, CommandFlags.None); + } + + [Fact] + public async Task StringSetAndGetAsync_1() + { + var expiry = TimeSpan.FromMinutes(5); + await prefixed.StringSetAndGetAsync("key", "value", expiry, When.Always, CommandFlags.None); + await mock.Received().StringSetAndGetAsync("prefix:key", "value", expiry, When.Always, CommandFlags.None); + } + + [Fact] + public async Task StringSetAndGetAsync_2() + { + var expiry = TimeSpan.FromMinutes(5); + await prefixed.StringSetAndGetAsync("key", "value", expiry, false, When.Always, CommandFlags.None); + await mock.Received().StringSetAndGetAsync("prefix:key", "value", expiry, false, When.Always, CommandFlags.None); + } + [Fact] + public async Task StringLongestCommonSubsequenceAsync() + { + await prefixed.StringLongestCommonSubsequenceAsync("key1", "key2", CommandFlags.None); + await mock.Received().StringLongestCommonSubsequenceAsync("prefix:key1", "prefix:key2", CommandFlags.None); + } + + [Fact] + public async Task StringLongestCommonSubsequenceLengthAsync() + { + await prefixed.StringLongestCommonSubsequenceLengthAsync("key1", "key2", CommandFlags.None); + await mock.Received().StringLongestCommonSubsequenceLengthAsync("prefix:key1", "prefix:key2", CommandFlags.None); + } + + [Fact] + public async Task StringLongestCommonSubsequenceWithMatchesAsync() + { + await prefixed.StringLongestCommonSubsequenceWithMatchesAsync("key1", "key2", 5, CommandFlags.None); + await mock.Received().StringLongestCommonSubsequenceWithMatchesAsync("prefix:key1", "prefix:key2", 5, CommandFlags.None); + } + [Fact] + public async Task KeyIdleTimeAsync() + { + await prefixed.KeyIdleTimeAsync("key", CommandFlags.None); + await mock.Received().KeyIdleTimeAsync("prefix:key", CommandFlags.None); + } + [Fact] + public async Task StreamAddAsync_WithTrimMode_1() + { + await prefixed.StreamAddAsync("key", "field", "value", "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + await mock.Received().StreamAddAsync("prefix:key", "field", "value", "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public async Task StreamAddAsync_WithTrimMode_2() + { + var fields = new NameValueEntry[] { new NameValueEntry("field", "value") }; + await prefixed.StreamAddAsync("key", fields, "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + await mock.Received().StreamAddAsync("prefix:key", fields, "*", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public async Task StreamTrimAsync_WithMode() + { + await prefixed.StreamTrimAsync("key", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + await mock.Received().StreamTrimAsync("prefix:key", 1000, false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public async Task StreamTrimByMinIdAsync_WithMode() + { + await prefixed.StreamTrimByMinIdAsync("key", "1111111111", false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + await mock.Received().StreamTrimByMinIdAsync("prefix:key", "1111111111", false, 100, StreamTrimMode.KeepReferences, CommandFlags.None); + } + + [Fact] + public async Task StreamReadGroupAsync_WithNoAck_1() + { + await prefixed.StreamReadGroupAsync("key", "group", "consumer", "0-0", 10, true, CommandFlags.None); + await mock.Received().StreamReadGroupAsync("prefix:key", "group", "consumer", "0-0", 10, true, CommandFlags.None); + } + + [Fact] + public async Task StreamReadGroupAsync_WithNoAck_2() + { + var streamPositions = new StreamPosition[] { new StreamPosition("key", "0-0") }; + await prefixed.StreamReadGroupAsync(streamPositions, "group", "consumer", 10, true, CommandFlags.None); + await mock.Received().StreamReadGroupAsync(streamPositions, "group", "consumer", 10, true, CommandFlags.None); + } + + [Fact] + public async Task StreamTrimAsync_Simple() + { + await prefixed.StreamTrimAsync("key", 1000, true, CommandFlags.None); + await mock.Received().StreamTrimAsync("prefix:key", 1000, true, CommandFlags.None); + } + + [Fact] + public async Task StreamReadGroupAsync_Simple_1() + { + await prefixed.StreamReadGroupAsync("key", "group", "consumer", "0-0", 10, CommandFlags.None); + await mock.Received().StreamReadGroupAsync("prefix:key", "group", "consumer", "0-0", 10, CommandFlags.None); + } + + [Fact] + public async Task StreamReadGroupAsync_Simple_2() + { + var streamPositions = new StreamPosition[] { new StreamPosition("key", "0-0") }; + await prefixed.StreamReadGroupAsync(streamPositions, "group", "consumer", 10, CommandFlags.None); + await mock.Received().StreamReadGroupAsync(streamPositions, "group", "consumer", 10, CommandFlags.None); + } + + [Fact] + public void HashScanAsync() + { + var result = prefixed.HashScanAsync("key", "pattern*", 10, 1, 2, CommandFlags.None); + _ = mock.Received().HashScanAsync("prefix:key", "pattern*", 10, 1, 2, CommandFlags.None); + } + + [Fact] + public void HashScanNoValuesAsync() + { + var result = prefixed.HashScanNoValuesAsync("key", "pattern*", 10, 1, 2, CommandFlags.None); + _ = mock.Received().HashScanNoValuesAsync("prefix:key", "pattern*", 10, 1, 2, CommandFlags.None); + } + + [Fact] + public void SetScanAsync() + { + var result = prefixed.SetScanAsync("key", "pattern*", 10, 1, 2, CommandFlags.None); + _ = mock.Received().SetScanAsync("prefix:key", "pattern*", 10, 1, 2, CommandFlags.None); + } + + [Fact] + public void SortedSetScanAsync() + { + var result = prefixed.SortedSetScanAsync("key", "pattern*", 10, 1, 2, CommandFlags.None); + _ = mock.Received().SortedSetScanAsync("prefix:key", "pattern*", 10, 1, 2, CommandFlags.None); } } } diff --git a/tests/StackExchange.Redis.Tests/RespProtocolTests.cs b/tests/StackExchange.Redis.Tests/RespProtocolTests.cs index 08e31b699..855ec96d1 100644 --- a/tests/StackExchange.Redis.Tests/RespProtocolTests.cs +++ b/tests/StackExchange.Redis.Tests/RespProtocolTests.cs @@ -324,17 +324,28 @@ public async Task CheckCommandResult(string command, RedisProtocol protocol, Res var db = muxer.GetDatabase(); if (args.Length > 0) { - await db.KeyDeleteAsync((string)args[0]); - switch (args[0]) + var origKey = (string)args[0]; + switch (origKey) { case "ikey": - await db.StringSetAsync("ikey", "40"); - break; case "skey": - await db.SetAddAsync("skey", ["a", "b", "c"]); - break; case "hkey": - await db.HashSetAsync("hkey", [new("a", 1), new("b", 2), new("c", 3)]); + case "nkey": + var newKey = Me() + "_" + origKey; // disambiguate + args[0] = newKey; + await db.KeyDeleteAsync(newKey); // remove + switch (origKey) // initialize + { + case "ikey": + await db.StringSetAsync(newKey, "40"); + break; + case "skey": + await db.SetAddAsync(newKey, ["a", "b", "c"]); + break; + case "hkey": + await db.HashSetAsync(newKey, [new("a", 1), new("b", 2), new("c", 3)]); + break; + } break; } } diff --git a/version.json b/version.json index be7077002..63f4a5346 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "2.8", + "version": "2.9", "versionHeightOffset": -1, "assemblyVersion": "2.0", "publicReleaseRefSpec": [ From 038f3de7549aeb1cb301a7f44a7a83603473718e Mon Sep 17 00:00:00 2001 From: ArnoKoll <97118098+ArnoKoll@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:10:07 +0200 Subject: [PATCH 353/435] Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions (#2638) * New-SortedSetStartsWith-Condition * fix formatting rules * clean up and optimize * tests for sorted-set-starts-with condition IDE noise cleanup * release notes / shipped --------- Co-authored-by: Marc Gravell --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Condition.cs | 102 +++++++++++- src/StackExchange.Redis/FrameworkShims.cs | 39 +++++ src/StackExchange.Redis/Hacks.cs | 13 -- .../PublicAPI/PublicAPI.Shipped.txt | 6 +- .../PublicAPI/PublicAPI.Unshipped.txt | 2 +- src/StackExchange.Redis/RedisDatabase.cs | 14 +- src/StackExchange.Redis/RedisValue.cs | 76 +++++++-- tests/StackExchange.Redis.Tests/LexTests.cs | 3 + .../TransactionTests.cs | 147 ++++++++++++++++-- 10 files changed, 352 insertions(+), 51 deletions(-) create mode 100644 src/StackExchange.Redis/FrameworkShims.cs delete mode 100644 src/StackExchange.Redis/Hacks.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index d912f0842..80d7f5f51 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: - Add `HGETDEL`, `HGETEX` and `HSETEX` support ([#2863 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2863)) - Fix key-prefix omission in `SetIntersectionLength` and `SortedSet{Combine[WithScores]|IntersectionLength}` ([#2863 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2863)) +- Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638)) ## 2.8.58 diff --git a/src/StackExchange.Redis/Condition.cs b/src/StackExchange.Redis/Condition.cs index 19e8b2863..ec7ee53b6 100644 --- a/src/StackExchange.Redis/Condition.cs +++ b/src/StackExchange.Redis/Condition.cs @@ -284,6 +284,20 @@ public static Condition StringNotEqual(RedisKey key, RedisValue value) /// The member the sorted set must not contain. public static Condition SortedSetNotContains(RedisKey key, RedisValue member) => new ExistsCondition(key, RedisType.SortedSet, member, false); + /// + /// Enforces that the given sorted set contains a member that starts with the specified prefix. + /// + /// The key of the sorted set to check. + /// The sorted set must contain at least one member that starts with the specified prefix. + public static Condition SortedSetContainsStarting(RedisKey key, RedisValue prefix) => new StartsWithCondition(key, prefix, true); + + /// + /// Enforces that the given sorted set does not contain a member that starts with the specified prefix. + /// + /// The key of the sorted set to check. + /// The sorted set must not contain at a member that starts with the specified prefix. + public static Condition SortedSetNotContainsStarting(RedisKey key, RedisValue prefix) => new StartsWithCondition(key, prefix, false); + /// /// Enforces that the given sorted set member must have the specified score. /// @@ -370,6 +384,9 @@ public static Message CreateMessage(Condition condition, int db, CommandFlags fl public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, in RedisValue value1) => new ConditionMessage(condition, db, flags, command, key, value, value1); + public static Message CreateMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) => + new ConditionMessage(condition, db, flags, command, key, value, value1, value2, value3, value4); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0071:Simplify interpolation", Justification = "Allocations (string.Concat vs. string.Format)")] protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) { @@ -389,6 +406,9 @@ private sealed class ConditionMessage : Message.CommandKeyBase public readonly Condition Condition; private readonly RedisValue value; private readonly RedisValue value1; + private readonly RedisValue value2; + private readonly RedisValue value3; + private readonly RedisValue value4; public ConditionMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value) : base(db, flags, command, key) @@ -403,6 +423,15 @@ public ConditionMessage(Condition condition, int db, CommandFlags flags, RedisCo this.value1 = value1; // note no assert here } + // Message with 3 or 4 values not used, therefore not implemented + public ConditionMessage(Condition condition, int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) + : this(condition, db, flags, command, key, value, value1) + { + this.value2 = value2; // note no assert here + this.value3 = value3; // note no assert here + this.value4 = value4; // note no assert here + } + protected override void WriteImpl(PhysicalConnection physical) { if (value.IsNull) @@ -412,16 +441,20 @@ protected override void WriteImpl(PhysicalConnection physical) } else { - physical.WriteHeader(command, value1.IsNull ? 2 : 3); + physical.WriteHeader(command, value1.IsNull ? 2 : value2.IsNull ? 3 : value3.IsNull ? 4 : value4.IsNull ? 5 : 6); physical.Write(Key); physical.WriteBulkString(value); if (!value1.IsNull) - { physical.WriteBulkString(value1); - } + if (!value2.IsNull) + physical.WriteBulkString(value2); + if (!value3.IsNull) + physical.WriteBulkString(value3); + if (!value4.IsNull) + physical.WriteBulkString(value4); } } - public override int ArgCount => value.IsNull ? 1 : value1.IsNull ? 2 : 3; + public override int ArgCount => value.IsNull ? 1 : value1.IsNull ? 2 : value2.IsNull ? 3 : value3.IsNull ? 4 : value4.IsNull ? 5 : 6; } } @@ -501,6 +534,67 @@ internal override bool TryValidate(in RawResult result, out bool value) } } + internal sealed class StartsWithCondition : Condition + { + /* only usable for RedisType.SortedSet, members of SortedSets are always byte-arrays, expectedStartValue therefore is a byte-array + any Encoding and Conversion for the search-sequence has to be executed in calling application + working with byte arrays should prevent any encoding within this class, that could distort the comparison */ + + private readonly bool expectedResult; + private readonly RedisValue prefix; + private readonly RedisKey key; + + internal override Condition MapKeys(Func map) => + new StartsWithCondition(map(key), prefix, expectedResult); + + public StartsWithCondition(in RedisKey key, in RedisValue prefix, bool expectedResult) + { + if (key.IsNull) throw new ArgumentNullException(nameof(key)); + if (prefix.IsNull) throw new ArgumentNullException(nameof(prefix)); + this.key = key; + this.prefix = prefix; + this.expectedResult = expectedResult; + } + + public override string ToString() => + $"{key} {nameof(RedisType.SortedSet)} > {(expectedResult ? " member starting " : " no member starting ")} {prefix} + prefix"; + + internal override void CheckCommands(CommandMap commandMap) => commandMap.AssertAvailable(RedisCommand.ZRANGEBYLEX); + + internal override IEnumerable CreateMessages(int db, IResultBox? resultBox) + { + yield return Message.Create(db, CommandFlags.None, RedisCommand.WATCH, key); + + // prepend '[' to prefix for inclusive search + var startValueWithToken = RedisDatabase.GetLexRange(prefix, Exclude.None, isStart: true, Order.Ascending); + + var message = ConditionProcessor.CreateMessage( + this, + db, + CommandFlags.None, + RedisCommand.ZRANGEBYLEX, + key, + startValueWithToken, + RedisLiterals.PlusSymbol, + RedisLiterals.LIMIT, + 0, + 1); + + message.SetSource(ConditionProcessor.Default, resultBox); + yield return message; + } + + internal override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(key); + + internal override bool TryValidate(in RawResult result, out bool value) + { + value = result.ItemsCount == 1 && result[0].AsRedisValue().StartsWith(prefix); + + if (!expectedResult) value = !value; + return true; + } + } + internal sealed class EqualsCondition : Condition { internal override Condition MapKeys(Func map) => diff --git a/src/StackExchange.Redis/FrameworkShims.cs b/src/StackExchange.Redis/FrameworkShims.cs new file mode 100644 index 000000000..9472df9ae --- /dev/null +++ b/src/StackExchange.Redis/FrameworkShims.cs @@ -0,0 +1,39 @@ +#pragma warning disable SA1403 // single namespace + +#if NET5_0_OR_GREATER +// context: https://github.com/StackExchange/StackExchange.Redis/issues/2619 +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))] +#else +// To support { get; init; } properties +using System.ComponentModel; +using System.Text; + +namespace System.Runtime.CompilerServices +{ + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit { } +} +#endif + +#if !(NETCOREAPP || NETSTANDARD2_1_OR_GREATER) + +namespace System.Text +{ + internal static class EncodingExtensions + { + public static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan source, Span destination) + { + fixed (byte* bPtr = destination) + { + fixed (char* cPtr = source) + { + return encoding.GetBytes(cPtr, source.Length, bPtr, destination.Length); + } + } + } + } +} +#endif + + +#pragma warning restore SA1403 diff --git a/src/StackExchange.Redis/Hacks.cs b/src/StackExchange.Redis/Hacks.cs deleted file mode 100644 index 8dda522a3..000000000 --- a/src/StackExchange.Redis/Hacks.cs +++ /dev/null @@ -1,13 +0,0 @@ -#if NET5_0_OR_GREATER -// context: https://github.com/StackExchange/StackExchange.Redis/issues/2619 -[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))] -#else -// To support { get; init; } properties -using System.ComponentModel; - -namespace System.Runtime.CompilerServices -{ - [EditorBrowsable(EditorBrowsableState.Never)] - internal static class IsExternalInit { } -} -#endif diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 4a77208aa..39188f81d 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1951,4 +1951,8 @@ StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.R StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue field, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, System.DateTime expiry, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.HashFieldSetAndSetExpiryAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.HashEntry[]! hashFields, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! - +StackExchange.Redis.RedisValue.CopyTo(System.Span destination) -> int +StackExchange.Redis.RedisValue.GetByteCount() -> int +StackExchange.Redis.RedisValue.GetLongByteCount() -> long +static StackExchange.Redis.Condition.SortedSetContainsStarting(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue prefix) -> StackExchange.Redis.Condition! +static StackExchange.Redis.Condition.SortedSetNotContainsStarting(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue prefix) -> StackExchange.Redis.Condition! \ No newline at end of file diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 91b0e1a43..ab058de62 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1 @@ -#nullable enable \ No newline at end of file +#nullable enable diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 7b23b0773..02d59104c 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -3962,21 +3962,23 @@ private Message GetSortedSetMultiPopMessage(RedisKey[] keys, Order order, long c return tran; } - private static RedisValue GetLexRange(RedisValue value, Exclude exclude, bool isStart, Order order) + internal static RedisValue GetLexRange(RedisValue value, Exclude exclude, bool isStart, Order order) { - if (value.IsNull) + if (value.IsNull) // open search { if (order == Order.Ascending) return isStart ? RedisLiterals.MinusSymbol : RedisLiterals.PlusSymbol; - return isStart ? RedisLiterals.PlusSymbol : RedisLiterals.MinusSymbol; // 24.01.2024: when descending order: Plus and Minus have to be reversed + return isStart ? RedisLiterals.PlusSymbol : RedisLiterals.MinusSymbol; // when descending order: Plus and Minus have to be reversed } - byte[] orig = value!; + var srcLength = value.GetByteCount(); + Debug.Assert(srcLength >= 0); - byte[] result = new byte[orig.Length + 1]; + byte[] result = new byte[srcLength + 1]; // no defaults here; must always explicitly specify [ / ( result[0] = (exclude & (isStart ? Exclude.Start : Exclude.Stop)) == 0 ? (byte)'[' : (byte)'('; - Buffer.BlockCopy(orig, 0, result, 1, orig.Length); + int written = value.CopyTo(result.AsSpan(1)); + Debug.Assert(written == srcLength, "predicted/actual length mismatch"); return result; } diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index 0eb1a5812..a670607de 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Buffers.Text; using System.ComponentModel; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -838,23 +839,78 @@ private static string ToHex(ReadOnlySpan src) return value._memory.ToArray(); case StorageType.Int64: - Span span = stackalloc byte[Format.MaxInt64TextLen + 2]; - int len = PhysicalConnection.WriteRaw(span, value.OverlappedValueInt64, false, 0); - arr = new byte[len - 2]; // don't need the CRLF - span.Slice(0, arr.Length).CopyTo(arr); - return arr; + Debug.Assert(Format.MaxInt64TextLen <= 24); + Span span = stackalloc byte[24]; + int len = Format.FormatInt64(value.OverlappedValueInt64, span); + return span.Slice(0, len).ToArray(); case StorageType.UInt64: - // we know it is a huge value - just jump straight to Utf8Formatter - span = stackalloc byte[Format.MaxInt64TextLen]; + Debug.Assert(Format.MaxInt64TextLen <= 24); + span = stackalloc byte[24]; len = Format.FormatUInt64(value.OverlappedValueUInt64, span); - arr = new byte[len]; - span.Slice(0, len).CopyTo(arr); - return arr; + return span.Slice(0, len).ToArray(); + case StorageType.Double: + span = stackalloc byte[128]; + len = Format.FormatDouble(value.OverlappedValueDouble, span); + return span.Slice(0, len).ToArray(); + case StorageType.String: + return Encoding.UTF8.GetBytes((string)value._objectOrSentinel!); } // fallback: stringify and encode return Encoding.UTF8.GetBytes((string)value!); } + /// + /// Gets the length of the value in bytes. + /// + public int GetByteCount() + { + switch (Type) + { + case StorageType.Null: return 0; + case StorageType.Raw: return _memory.Length; + case StorageType.String: return Encoding.UTF8.GetByteCount((string)_objectOrSentinel!); + case StorageType.Int64: return Format.MeasureInt64(OverlappedValueInt64); + case StorageType.UInt64: return Format.MeasureUInt64(OverlappedValueUInt64); + case StorageType.Double: return Format.MeasureDouble(OverlappedValueDouble); + default: return ThrowUnableToMeasure(); + } + } + + private int ThrowUnableToMeasure() => throw new InvalidOperationException("Unable to compute length of type: " + Type); + + /// + /// Gets the length of the value in bytes. + /// + /* right now, we only support int lengths, but adding this now so that + there are no surprises if/when we add support for discontiguous buffers */ + public long GetLongByteCount() => GetByteCount(); + + /// + /// Copy the value as bytes to the provided . + /// + public int CopyTo(Span destination) + { + switch (Type) + { + case StorageType.Null: + return 0; + case StorageType.Raw: + var srcBytes = _memory.Span; + srcBytes.CopyTo(destination); + return srcBytes.Length; + case StorageType.String: + return Encoding.UTF8.GetBytes(((string)_objectOrSentinel!).AsSpan(), destination); + case StorageType.Int64: + return Format.FormatInt64(OverlappedValueInt64, destination); + case StorageType.UInt64: + return Format.FormatUInt64(OverlappedValueUInt64, destination); + case StorageType.Double: + return Format.FormatDouble(OverlappedValueDouble, destination); + default: + return ThrowUnableToMeasure(); + } + } + /// /// Converts a to a . /// diff --git a/tests/StackExchange.Redis.Tests/LexTests.cs b/tests/StackExchange.Redis.Tests/LexTests.cs index e29255b24..b70fdda7e 100644 --- a/tests/StackExchange.Redis.Tests/LexTests.cs +++ b/tests/StackExchange.Redis.Tests/LexTests.cs @@ -51,6 +51,9 @@ public async Task QueryRangeAndLengthByLex() set = db.SortedSetRangeByValue(key, "e", default(RedisValue)); count = db.SortedSetLengthByValue(key, "e", default(RedisValue)); Equate(set, count, "e", "f", "g"); + + set = db.SortedSetRangeByValue(key, RedisValue.Null, RedisValue.Null, Exclude.None, Order.Descending, 0, 3); // added to test Null-min- and max-param + Equate(set, set.Length, "g", "f", "e"); } [Fact] diff --git a/tests/StackExchange.Redis.Tests/TransactionTests.cs b/tests/StackExchange.Redis.Tests/TransactionTests.cs index daee7ee00..3a0f1e40e 100644 --- a/tests/StackExchange.Redis.Tests/TransactionTests.cs +++ b/tests/StackExchange.Redis.Tests/TransactionTests.cs @@ -362,8 +362,8 @@ public async Task BasicTranWithStringLengthCondition(string? value, ComparisonTy db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key2, CommandFlags.FireAndForget); - var expectSuccess = false; - Condition? condition = null; + bool expectSuccess; + Condition? condition; var valueLength = value?.Length ?? 0; switch (type) { @@ -441,8 +441,8 @@ public async Task BasicTranWithHashLengthCondition(string value, ComparisonType db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key2, CommandFlags.FireAndForget); - var expectSuccess = false; - Condition? condition = null; + bool expectSuccess; + Condition? condition; var valueLength = value?.Length ?? 0; switch (type) { @@ -520,8 +520,8 @@ public async Task BasicTranWithSetCardinalityCondition(string value, ComparisonT db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key2, CommandFlags.FireAndForget); - var expectSuccess = false; - Condition? condition = null; + bool expectSuccess; + Condition? condition; var valueLength = value?.Length ?? 0; switch (type) { @@ -640,8 +640,8 @@ public async Task BasicTranWithSortedSetCardinalityCondition(string value, Compa db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key2, CommandFlags.FireAndForget); - var expectSuccess = false; - Condition? condition = null; + bool expectSuccess; + Condition? condition; var valueLength = value?.Length ?? 0; switch (type) { @@ -719,8 +719,8 @@ public async Task BasicTranWithSortedSetRangeCountCondition(double min, double m db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key2, CommandFlags.FireAndForget); - var expectSuccess = false; - Condition? condition = null; + bool expectSuccess; + Condition? condition; var valueLength = (int)(max - min) + 1; switch (type) { @@ -812,6 +812,120 @@ public async Task BasicTranWithSortedSetContainsCondition(bool demandKeyExists, } } + public enum SortedSetValue + { + None, + Exact, + Shorter, + Longer, + } + + [Theory] + [InlineData(false, SortedSetValue.None, true)] + [InlineData(false, SortedSetValue.Shorter, true)] + [InlineData(false, SortedSetValue.Exact, false)] + [InlineData(false, SortedSetValue.Longer, false)] + [InlineData(true, SortedSetValue.None, false)] + [InlineData(true, SortedSetValue.Shorter, false)] + [InlineData(true, SortedSetValue.Exact, true)] + [InlineData(true, SortedSetValue.Longer, true)] + public async Task BasicTranWithSortedSetStartsWithCondition_String(bool requestExists, SortedSetValue existingValue, bool expectTranResult) + { + using var conn = Create(); + + RedisKey key1 = Me() + "_1", key2 = Me() + "_2"; + var db = conn.GetDatabase(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key2, "unrelated", 0.0, flags: CommandFlags.FireAndForget); + switch (existingValue) + { + case SortedSetValue.Shorter: + db.SortedSetAdd(key2, "see", 0.0, flags: CommandFlags.FireAndForget); + break; + case SortedSetValue.Exact: + db.SortedSetAdd(key2, "seek", 0.0, flags: CommandFlags.FireAndForget); + break; + case SortedSetValue.Longer: + db.SortedSetAdd(key2, "seeks", 0.0, flags: CommandFlags.FireAndForget); + break; + } + + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(requestExists ? Condition.SortedSetContainsStarting(key2, "seek") : Condition.SortedSetNotContainsStarting(key2, "seek")); + var incr = tran.StringIncrementAsync(key1); + var exec = await tran.ExecuteAsync(); + var get = await db.StringGetAsync(key1); + + Assert.Equal(expectTranResult, exec); + Assert.Equal(expectTranResult, cond.WasSatisfied); + + if (expectTranResult) + { + Assert.Equal(1, await incr); // eq: incr + Assert.Equal(1, (long)get); // eq: get + } + else + { + Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr + Assert.Equal(0, (long)get); // neq: get + } + } + + [Theory] + [InlineData(false, SortedSetValue.None, true)] + [InlineData(false, SortedSetValue.Shorter, true)] + [InlineData(false, SortedSetValue.Exact, false)] + [InlineData(false, SortedSetValue.Longer, false)] + [InlineData(true, SortedSetValue.None, false)] + [InlineData(true, SortedSetValue.Shorter, false)] + [InlineData(true, SortedSetValue.Exact, true)] + [InlineData(true, SortedSetValue.Longer, true)] + public async Task BasicTranWithSortedSetStartsWithCondition_Integer(bool requestExists, SortedSetValue existingValue, bool expectTranResult) + { + using var conn = Create(); + + RedisKey key1 = Me() + "_1", key2 = Me() + "_2"; + var db = conn.GetDatabase(); + db.KeyDelete(key1, CommandFlags.FireAndForget); + db.KeyDelete(key2, CommandFlags.FireAndForget); + + db.SortedSetAdd(key2, 789, 0.0, flags: CommandFlags.FireAndForget); + switch (existingValue) + { + case SortedSetValue.Shorter: + db.SortedSetAdd(key2, 123, 0.0, flags: CommandFlags.FireAndForget); + break; + case SortedSetValue.Exact: + db.SortedSetAdd(key2, 1234, 0.0, flags: CommandFlags.FireAndForget); + break; + case SortedSetValue.Longer: + db.SortedSetAdd(key2, 12345, 0.0, flags: CommandFlags.FireAndForget); + break; + } + + var tran = db.CreateTransaction(); + var cond = tran.AddCondition(requestExists ? Condition.SortedSetContainsStarting(key2, 1234) : Condition.SortedSetNotContainsStarting(key2, 1234)); + var incr = tran.StringIncrementAsync(key1); + var exec = await tran.ExecuteAsync(); + var get = await db.StringGetAsync(key1); + + Assert.Equal(expectTranResult, exec); + Assert.Equal(expectTranResult, cond.WasSatisfied); + + if (expectTranResult) + { + Assert.Equal(1, await incr); // eq: incr + Assert.Equal(1, (long)get); // eq: get + } + else + { + Assert.Equal(TaskStatus.Canceled, SafeStatus(incr)); // neq: incr + Assert.Equal(0, (long)get); // neq: get + } + } + [Theory] [InlineData(4D, 4D, true, true)] [InlineData(4D, 5D, true, false)] @@ -893,8 +1007,8 @@ public async Task BasicTranWithSortedSetScoreExistsCondition(bool member1HasScor } Assert.False(db.KeyExists(key)); - Assert.Equal(member1HasScore ? (double?)Score : null, db.SortedSetScore(key2, member1)); - Assert.Equal(member2HasScore ? (double?)Score : null, db.SortedSetScore(key2, member2)); + Assert.Equal(member1HasScore ? Score : null, db.SortedSetScore(key2, member1)); + Assert.Equal(member2HasScore ? Score : null, db.SortedSetScore(key2, member2)); var tran = db.CreateTransaction(); var cond = tran.AddCondition(demandScoreExists ? Condition.SortedSetScoreExists(key2, Score) : Condition.SortedSetScoreNotExists(key2, Score)); @@ -1014,8 +1128,8 @@ public async Task BasicTranWithListLengthCondition(string value, ComparisonType db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key2, CommandFlags.FireAndForget); - var expectSuccess = false; - Condition? condition = null; + bool expectSuccess; + Condition? condition; var valueLength = value?.Length ?? 0; switch (type) { @@ -1093,8 +1207,8 @@ public async Task BasicTranWithStreamLengthCondition(string value, ComparisonTyp db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key2, CommandFlags.FireAndForget); - var expectSuccess = false; - Condition? condition = null; + bool expectSuccess; + Condition? condition; var valueLength = value?.Length ?? 0; switch (type) { @@ -1227,6 +1341,7 @@ public async Task TransactionWithAdHocCommandsAndSelectDisabled() var tran = db.CreateTransaction("state"); var a = tran.ExecuteAsync("SET", "foo", "bar"); Assert.True(await tran.ExecuteAsync()); + await a; var setting = db.StringGet("foo"); Assert.Equal("bar", setting); } From 2f52c952bafc9cc44d57c67986639360d28c5692 Mon Sep 17 00:00:00 2001 From: David Brink <43828739+david-brink-talogy@users.noreply.github.com> Date: Thu, 24 Jul 2025 07:39:03 -0400 Subject: [PATCH 354/435] Adds support for XPENDING IDLE parameter (#2822) * Adds support for XPENDING IDLE parameter fixes #2432 * Increase delay to ensure sufficient time for idle * PR feedback - Add release notes - Add overload to StreamPendingMessages/StreamPendingMessagesAsync to preserve back compat --------- Co-authored-by: Marc Gravell --- docs/ReleaseNotes.md | 3 +- .../Interfaces/IDatabase.cs | 20 ++++++++- .../Interfaces/IDatabaseAsync.cs | 5 ++- .../KeyspaceIsolation/KeyPrefixed.cs | 5 ++- .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 5 ++- .../PublicAPI/PublicAPI.Shipped.txt | 6 ++- src/StackExchange.Redis/RedisDatabase.cs | 45 ++++++++++++++----- .../KeyPrefixedDatabaseTests.cs | 4 +- .../KeyPrefixedTests.cs | 4 +- .../StackExchange.Redis.Tests/StreamTests.cs | 35 +++++++++++++++ 10 files changed, 110 insertions(+), 22 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 80d7f5f51..7f2ed4432 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -11,7 +11,8 @@ Current package versions: - Add `HGETDEL`, `HGETEX` and `HSETEX` support ([#2863 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2863)) - Fix key-prefix omission in `SetIntersectionLength` and `SortedSet{Combine[WithScores]|IntersectionLength}` ([#2863 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2863)) - Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638)) - +- Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822)) +- ## 2.8.58 - Fix [#2679](https://github.com/StackExchange/StackExchange.Redis/issues/2679) - blocking call in long-running connects ([#2680 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2680)) diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 37fb6e32b..b6caafabe 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -2874,6 +2874,21 @@ IEnumerable SortedSetScan( /// StreamPendingInfo StreamPending(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); + /// + /// View information about each pending message. + /// + /// The key of the stream. + /// The name of the consumer group. + /// The maximum number of pending messages to return. + /// The consumer name for the pending messages. Pass RedisValue.Null to include pending messages for all consumers. + /// The minimum ID from which to read the stream of pending messages. Pass null to read from the beginning of the stream. + /// The maximum ID to read to within the stream of pending messages. Pass null to read to the end of the stream. + /// The flags to use for this operation. + /// An instance of for each pending message. + /// Equivalent of calling XPENDING key group start-id end-id count consumer-name. + /// + StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId, RedisValue? maxId, CommandFlags flags); + /// /// View information about each pending message. /// @@ -2883,11 +2898,12 @@ IEnumerable SortedSetScan( /// The consumer name for the pending messages. Pass RedisValue.Null to include pending messages for all consumers. /// The minimum ID from which to read the stream of pending messages. The method will default to reading from the beginning of the stream. /// The maximum ID to read to within the stream of pending messages. The method will default to reading to the end of the stream. + /// The minimum idle time threshold for pending messages to be claimed. /// The flags to use for this operation. /// An instance of for each pending message. - /// Equivalent of calling XPENDING key group start-id end-id count consumer-name. + /// Equivalent of calling XPENDING key group IDLE min-idle-time start-id end-id count consumer-name. /// - StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None); + StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, long? minIdleTimeInMs = null, CommandFlags flags = CommandFlags.None); /// /// Read a stream using the given range of IDs. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index b88570790..51a15d7d5 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -706,7 +706,10 @@ IAsyncEnumerable SortedSetScanAsync( Task StreamPendingAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None); /// - Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None); + Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId, RedisValue? maxId, CommandFlags flags); + + /// + Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, long? minIdleTimeInMs = null, CommandFlags flags = CommandFlags.None); /// Task StreamRangeAsync(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 06c5359eb..32c76f4d2 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -669,9 +669,12 @@ public Task StreamDeleteConsumerGroupAsync(RedisKey key, RedisValue groupN public Task StreamPendingAsync(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => Inner.StreamPendingAsync(ToInner(key), groupName, flags); - public Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) => + public Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId, RedisValue? maxId, CommandFlags flags) => Inner.StreamPendingMessagesAsync(ToInner(key), groupName, count, consumerName, minId, maxId, flags); + public Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, long? minIdleTimeInMs = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamPendingMessagesAsync(ToInner(key), groupName, count, consumerName, minId, maxId, minIdleTimeInMs, flags); + public Task StreamRangeAsync(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None) => Inner.StreamRangeAsync(ToInner(key), minId, maxId, count, messageOrder, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index 48df1e4db..755bec64e 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -651,9 +651,12 @@ public bool StreamDeleteConsumerGroup(RedisKey key, RedisValue groupName, Comman public StreamPendingInfo StreamPending(RedisKey key, RedisValue groupName, CommandFlags flags = CommandFlags.None) => Inner.StreamPending(ToInner(key), groupName, flags); - public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) => + public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId, RedisValue? maxId, CommandFlags flags) => Inner.StreamPendingMessages(ToInner(key), groupName, count, consumerName, minId, maxId, flags); + public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, long? minIdleTimeInMs = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamPendingMessages(ToInner(key), groupName, count, consumerName, minId, maxId, minIdleTimeInMs, flags); + public StreamEntry[] StreamRange(RedisKey key, RedisValue? minId = null, RedisValue? maxId = null, int? count = null, Order messageOrder = Order.Ascending, CommandFlags flags = CommandFlags.None) => Inner.StreamRange(ToInner(key), minId, maxId, count, messageOrder, flags); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 39188f81d..66c49976e 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -739,7 +739,8 @@ StackExchange.Redis.IDatabase.StreamGroupInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.IDatabase.StreamInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamInfo StackExchange.Redis.IDatabase.StreamLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StreamPending(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamPendingInfo -StackExchange.Redis.IDatabase.StreamPendingMessages(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, int count, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamPendingMessageInfo[]! +StackExchange.Redis.IDatabase.StreamPendingMessages(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, int count, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? minId, StackExchange.Redis.RedisValue? maxId, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.StreamPendingMessageInfo[]! +StackExchange.Redis.IDatabase.StreamPendingMessages(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, int count, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, long? minIdleTimeInMs = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamPendingMessageInfo[]! StackExchange.Redis.IDatabase.StreamRange(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, int? count = null, StackExchange.Redis.Order messageOrder = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[]! StackExchange.Redis.IDatabase.StreamRead(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue position, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[]! StackExchange.Redis.IDatabase.StreamRead(StackExchange.Redis.StreamPosition[]! streamPositions, int? countPerStream = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisStream[]! @@ -983,7 +984,8 @@ StackExchange.Redis.IDatabaseAsync.StreamGroupInfoAsync(StackExchange.Redis.Redi StackExchange.Redis.IDatabaseAsync.StreamInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamPendingAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.StreamPendingMessagesAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, int count, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamPendingMessagesAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, int count, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? minId, StackExchange.Redis.RedisValue? maxId, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamPendingMessagesAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, int count, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, long? minIdleTimeInMs = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamRangeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, int? count = null, StackExchange.Redis.Order messageOrder = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamReadAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue position, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamReadAsync(StackExchange.Redis.StreamPosition[]! streamPositions, int? countPerStream = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 02d59104c..bf69f25f3 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -3164,7 +3164,10 @@ public Task StreamPendingAsync(RedisKey key, RedisValue group return ExecuteAsync(msg, ResultProcessor.StreamPendingInfo); } - public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) + public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) => + StreamPendingMessages(key, groupName, count, consumerName, minId, maxId, null, flags); + + public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, long? minIdleTimeInMs = null, CommandFlags flags = CommandFlags.None) { var msg = GetStreamPendingMessagesMessage( key, @@ -3173,12 +3176,16 @@ public StreamPendingMessageInfo[] StreamPendingMessages(RedisKey key, RedisValue maxId, count, consumerName, + minIdleTimeInMs, flags); return ExecuteSync(msg, ResultProcessor.StreamPendingMessages, defaultValue: Array.Empty()); } - public Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) + public Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, CommandFlags flags = CommandFlags.None) => + StreamPendingMessagesAsync(key, groupName, count, consumerName, minId, maxId, null, flags); + + public Task StreamPendingMessagesAsync(RedisKey key, RedisValue groupName, int count, RedisValue consumerName, RedisValue? minId = null, RedisValue? maxId = null, long? minIdleTimeInMs = null, CommandFlags flags = CommandFlags.None) { var msg = GetStreamPendingMessagesMessage( key, @@ -3187,6 +3194,7 @@ public Task StreamPendingMessagesAsync(RedisKey key, maxId, count, consumerName, + minIdleTimeInMs, flags); return ExecuteAsync(msg, ResultProcessor.StreamPendingMessages, defaultValue: Array.Empty()); @@ -4717,9 +4725,9 @@ private Message GetStreamCreateConsumerGroupMessage(RedisKey key, RedisValue gro /// Gets a message for . /// /// - private Message GetStreamPendingMessagesMessage(RedisKey key, RedisValue groupName, RedisValue? minId, RedisValue? maxId, int count, RedisValue consumerName, CommandFlags flags) + private Message GetStreamPendingMessagesMessage(RedisKey key, RedisValue groupName, RedisValue? minId, RedisValue? maxId, int count, RedisValue consumerName, long? minIdleTimeInMs, CommandFlags flags) { - // > XPENDING mystream mygroup - + 10 [consumer name] + // > XPENDING mystream mygroup [IDLE min-idle-time] - + 10 [consumer name] // 1) 1) 1526569498055 - 0 // 2) "Bob" // 3) (integer)74170458 @@ -4733,16 +4741,33 @@ private Message GetStreamPendingMessagesMessage(RedisKey key, RedisValue groupNa throw new ArgumentOutOfRangeException(nameof(count), "count must be greater than 0."); } - var values = new RedisValue[consumerName == RedisValue.Null ? 4 : 5]; + var valuesLength = 4; + if (consumerName != RedisValue.Null) + { + valuesLength++; + } - values[0] = groupName; - values[1] = minId ?? StreamConstants.ReadMinValue; - values[2] = maxId ?? StreamConstants.ReadMaxValue; - values[3] = count; + if (minIdleTimeInMs is not null) + { + valuesLength += 2; + } + var values = new RedisValue[valuesLength]; + + var offset = 0; + + values[offset++] = groupName; + if (minIdleTimeInMs is not null) + { + values[offset++] = "IDLE"; + values[offset++] = minIdleTimeInMs; + } + values[offset++] = minId ?? StreamConstants.ReadMinValue; + values[offset++] = maxId ?? StreamConstants.ReadMaxValue; + values[offset++] = count; if (consumerName != RedisValue.Null) { - values[4] = consumerName; + values[offset++] = consumerName; } return Message.Create( diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs index 571961eb0..0b781123c 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedDatabaseTests.cs @@ -1149,8 +1149,8 @@ public void StreamPendingInfoGet() [Fact] public void StreamPendingMessageInfoGet() { - prefixed.StreamPendingMessages("key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); - mock.Received().StreamPendingMessages("prefix:key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); + prefixed.StreamPendingMessages("key", "group", 10, RedisValue.Null, "-", "+", 1000, CommandFlags.None); + mock.Received().StreamPendingMessages("prefix:key", "group", 10, RedisValue.Null, "-", "+", 1000, CommandFlags.None); } [Fact] diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs index cbef3d9e7..94b54e112 100644 --- a/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedTests.cs @@ -1059,8 +1059,8 @@ public async Task StreamPendingInfoGetAsync() [Fact] public async Task StreamPendingMessageInfoGetAsync() { - await prefixed.StreamPendingMessagesAsync("key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); - await mock.Received().StreamPendingMessagesAsync("prefix:key", "group", 10, RedisValue.Null, "-", "+", CommandFlags.None); + await prefixed.StreamPendingMessagesAsync("key", "group", 10, RedisValue.Null, "-", "+", 1000, CommandFlags.None); + await mock.Received().StreamPendingMessagesAsync("prefix:key", "group", 10, RedisValue.Null, "-", "+", 1000, CommandFlags.None); } [Fact] diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index 83d9fe8f8..196913f40 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -1148,6 +1148,41 @@ public async Task StreamConsumerGroupViewPendingMessageInfo() Assert.Equal(id1, pendingMessageInfoList[0].MessageId); } + [Fact] + public async Task StreamConsumerGroupViewPendingMessageWithMinIdle() + { + await using var conn = Create(require: RedisFeatures.v6_2_0); + + var db = conn.GetDatabase(); + var key = Me(); + const string groupName = "test_group", + consumer1 = "test_consumer_1"; + const int minIdleTimeInMs = 100; + + var id1 = db.StreamAdd(key, "field1", "value1"); + + db.StreamCreateConsumerGroup(key, groupName, StreamPosition.Beginning); + + // Read a single message into the first consumer. + db.StreamReadGroup(key, groupName, consumer1, count: 1); + + var preDelayPendingMessages = + db.StreamPendingMessages(key, groupName, 10, RedisValue.Null, minId: id1, maxId: id1, minIdleTimeInMs: minIdleTimeInMs); + + await Task.Delay(minIdleTimeInMs * 2).ForAwait(); + + var postDelayPendingMessages = + db.StreamPendingMessages(key, groupName, 10, RedisValue.Null, minId: id1, maxId: id1, minIdleTimeInMs: minIdleTimeInMs); + + Assert.NotNull(preDelayPendingMessages); + Assert.Empty(preDelayPendingMessages); + Assert.NotNull(postDelayPendingMessages); + Assert.Single(postDelayPendingMessages); + Assert.Equal(1, postDelayPendingMessages[0].DeliveryCount); + Assert.True((int)postDelayPendingMessages[0].IdleTimeInMilliseconds > minIdleTimeInMs); + Assert.Equal(id1, postDelayPendingMessages[0].MessageId); + } + [Fact] public async Task StreamConsumerGroupViewPendingMessageInfoForConsumer() { From 3c236a408424506652cbfbff6ae226a056abf292 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Thu, 24 Jul 2025 14:45:01 +0300 Subject: [PATCH 355/435] chore: Replaces string interpolation with structured logging - ResultProcessor (#2925) * Migrates logging to structured LoggerMessage methods Replaces manual string interpolation with compile-time generated LoggerMessage methods for better performance and structured logging. Adds 16 new LoggerMessage extension methods covering response logging and auto-configuration scenarios for Redis server connections. Improves logging efficiency by eliminating runtime string formatting overhead and enables better log analysis through consistent message templates. * merge read-only-replica methods --------- Co-authored-by: Marc Gravell --- src/StackExchange.Redis/LoggerExtensions.cs | 91 +++++++++++++++++++++ src/StackExchange.Redis/ResultProcessor.cs | 34 ++++---- 2 files changed, 108 insertions(+), 17 deletions(-) diff --git a/src/StackExchange.Redis/LoggerExtensions.cs b/src/StackExchange.Redis/LoggerExtensions.cs index 877de6257..3cb2ec6fb 100644 --- a/src/StackExchange.Redis/LoggerExtensions.cs +++ b/src/StackExchange.Redis/LoggerExtensions.cs @@ -488,4 +488,95 @@ internal static void LogWithThreadPoolStats(this ILogger? log, string message) EventId = 70, Message = "{Server}: Flushing outbound buffer")] internal static partial void LogInformationFlushingOutboundBuffer(this ILogger logger, ServerEndPointLogValue server); + + // ResultProcessor logging methods + [LoggerMessage( + Level = LogLevel.Information, + EventId = 71, + Message = "Response from {BridgeName} / {CommandAndKey}: {Result}")] + internal static partial void LogInformationResponse(this ILogger logger, string? bridgeName, string commandAndKey, RawResult result); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 72, + Message = "{Server}: Auto-configured role: replica")] + internal static partial void LogInformationAutoConfiguredRoleReplica(this ILogger logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 73, + Message = "{Server}: Auto-configured (CLIENT) connection-id: {ConnectionId}")] + internal static partial void LogInformationAutoConfiguredClientConnectionId(this ILogger logger, ServerEndPointLogValue server, long connectionId); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 74, + Message = "{Server}: Auto-configured (INFO) role: {Role}")] + internal static partial void LogInformationAutoConfiguredInfoRole(this ILogger logger, ServerEndPointLogValue server, string role); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 75, + Message = "{Server}: Auto-configured (INFO) version: {Version}")] + internal static partial void LogInformationAutoConfiguredInfoVersion(this ILogger logger, ServerEndPointLogValue server, Version version); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 76, + Message = "{Server}: Auto-configured (INFO) server-type: {ServerType}")] + internal static partial void LogInformationAutoConfiguredInfoServerType(this ILogger logger, ServerEndPointLogValue server, ServerType serverType); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 77, + Message = "{Server}: Auto-configured (SENTINEL) server-type: sentinel")] + internal static partial void LogInformationAutoConfiguredSentinelServerType(this ILogger logger, ServerEndPointLogValue server); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 78, + Message = "{Server}: Auto-configured (CONFIG) timeout: {TimeoutSeconds}s")] + internal static partial void LogInformationAutoConfiguredConfigTimeout(this ILogger logger, ServerEndPointLogValue server, int timeoutSeconds); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 79, + Message = "{Server}: Auto-configured (CONFIG) databases: {DatabaseCount}")] + internal static partial void LogInformationAutoConfiguredConfigDatabases(this ILogger logger, ServerEndPointLogValue server, int databaseCount); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 81, + Message = "{Server}: Auto-configured (CONFIG) read-only replica: {ReadOnlyReplica}")] + internal static partial void LogInformationAutoConfiguredConfigReadOnlyReplica(this ILogger logger, ServerEndPointLogValue server, bool readOnlyReplica); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 82, + Message = "{Server}: Auto-configured (HELLO) server-version: {Version}")] + internal static partial void LogInformationAutoConfiguredHelloServerVersion(this ILogger logger, ServerEndPointLogValue server, Version version); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 83, + Message = "{Server}: Auto-configured (HELLO) protocol: {Protocol}")] + internal static partial void LogInformationAutoConfiguredHelloProtocol(this ILogger logger, ServerEndPointLogValue server, RedisProtocol protocol); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 84, + Message = "{Server}: Auto-configured (HELLO) connection-id: {ConnectionId}")] + internal static partial void LogInformationAutoConfiguredHelloConnectionId(this ILogger logger, ServerEndPointLogValue server, long connectionId); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 85, + Message = "{Server}: Auto-configured (HELLO) server-type: {ServerType}")] + internal static partial void LogInformationAutoConfiguredHelloServerType(this ILogger logger, ServerEndPointLogValue server, ServerType serverType); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 86, + Message = "{Server}: Auto-configured (HELLO) role: {Role}")] + internal static partial void LogInformationAutoConfiguredHelloRole(this ILogger logger, ServerEndPointLogValue server, string role); } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 982ee565d..627953941 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -233,7 +233,7 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in { try { - logging.Log?.LogInformation($"Response from {bridge?.Name} / {message.CommandAndKey}: {result}"); + logging.Log?.LogInformationResponse(bridge?.Name, message.CommandAndKey, result); } catch { } } @@ -816,7 +816,7 @@ public override bool SetResult(PhysicalConnection connection, Message message, i if (bridge != null) { var server = bridge.ServerEndPoint; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured role: replica"); + Log?.LogInformationAutoConfiguredRoleReplica(new(server)); server.IsReplica = true; } } @@ -837,7 +837,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (result.TryGetInt64(out long clientId)) { connection.ConnectionId = clientId; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (CLIENT) connection-id: {clientId}"); + Log?.LogInformationAutoConfiguredClientConnectionId(new(server), clientId); SetResult(message, true); return true; @@ -871,7 +871,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (TryParseRole(val, out bool isReplica)) { server.IsReplica = isReplica; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) role: {(isReplica ? "replica" : "primary")}"); + Log?.LogInformationAutoConfiguredInfoRole(new(server), isReplica ? "replica" : "primary"); } } else if ((val = Extract(line, "master_host:")) != null) @@ -887,7 +887,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (Format.TryParseVersion(val, out Version? version)) { server.Version = version; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) version: " + version); + Log?.LogInformationAutoConfiguredInfoVersion(new(server), version); } } else if ((val = Extract(line, "redis_mode:")) != null) @@ -895,7 +895,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (TryParseServerType(val, out var serverType)) { server.ServerType = serverType; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (INFO) server-type: {serverType}"); + Log?.LogInformationAutoConfiguredInfoServerType(new(server), serverType); } } else if ((val = Extract(line, "run_id:")) != null) @@ -913,7 +913,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes else if (message?.Command == RedisCommand.SENTINEL) { server.ServerType = ServerType.Sentinel; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (SENTINEL) server-type: sentinel"); + Log?.LogInformationAutoConfiguredSentinelServerType(new(server)); } SetResult(message, true); return true; @@ -941,14 +941,14 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { targetSeconds = (timeoutSeconds * 3) / 4; } - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (CONFIG) timeout: " + targetSeconds + "s"); + Log?.LogInformationAutoConfiguredConfigTimeout(new(server), targetSeconds); server.WriteEverySeconds = targetSeconds; } } else if (key.IsEqual(CommonReplies.databases) && val.TryGetInt64(out i64)) { int dbCount = checked((int)i64); - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (CONFIG) databases: " + dbCount); + Log?.LogInformationAutoConfiguredConfigDatabases(new(server), dbCount); server.Databases = dbCount; if (dbCount > 1) { @@ -960,12 +960,12 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (val.IsEqual(CommonReplies.yes)) { server.ReplicaReadOnly = true; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (CONFIG) read-only replica: true"); + Log?.LogInformationAutoConfiguredConfigReadOnlyReplica(new(server), true); } else if (val.IsEqual(CommonReplies.no)) { server.ReplicaReadOnly = false; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (CONFIG) read-only replica: false"); + Log?.LogInformationAutoConfiguredConfigReadOnlyReplica(new(server), false); } } } @@ -982,34 +982,34 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (key.IsEqual(CommonReplies.version) && Format.TryParseVersion(val.GetString(), out var version)) { server.Version = version; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (HELLO) server-version: {version}"); + Log?.LogInformationAutoConfiguredHelloServerVersion(new(server), version); } else if (key.IsEqual(CommonReplies.proto) && val.TryGetInt64(out var i64)) { connection.SetProtocol(i64 >= 3 ? RedisProtocol.Resp3 : RedisProtocol.Resp2); - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (HELLO) protocol: {connection.Protocol}"); + Log?.LogInformationAutoConfiguredHelloProtocol(new(server), connection.Protocol ?? RedisProtocol.Resp2); } else if (key.IsEqual(CommonReplies.id) && val.TryGetInt64(out i64)) { connection.ConnectionId = i64; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (HELLO) connection-id: {i64}"); + Log?.LogInformationAutoConfiguredHelloConnectionId(new(server), i64); } else if (key.IsEqual(CommonReplies.mode) && TryParseServerType(val.GetString(), out var serverType)) { server.ServerType = serverType; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (HELLO) server-type: {serverType}"); + Log?.LogInformationAutoConfiguredHelloServerType(new(server), serverType); } else if (key.IsEqual(CommonReplies.role) && TryParseRole(val.GetString(), out bool isReplica)) { server.IsReplica = isReplica; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (HELLO) role: {(isReplica ? "replica" : "primary")}"); + Log?.LogInformationAutoConfiguredHelloRole(new(server), isReplica ? "replica" : "primary"); } } } else if (message?.Command == RedisCommand.SENTINEL) { server.ServerType = ServerType.Sentinel; - Log?.LogInformation($"{Format.ToString(server)}: Auto-configured (SENTINEL) server-type: sentinel"); + Log?.LogInformationAutoConfiguredSentinelServerType(new(server)); } SetResult(message, true); return true; From b3aeb61ff3b87a72760c8e6c69631ee45733c2e8 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Thu, 24 Jul 2025 16:52:39 +0300 Subject: [PATCH 356/435] chore: Replaces string interpolation with structured logging - PhysicalConnection & PhysicalBridge (#2929) * chore: Replaces string interpolation with structured logging - PhysicalConnection & PhysicalBridge Replaces string interpolation with structured logging Migrates logging calls from string interpolation to structured logging using LoggerMessage attributes. This improves performance by avoiding string allocation and enables better log analysis through structured data. Adds new logging extension methods for PhysicalBridge operations including connection establishment, failures, dead socket detection, and resurrection attempts. * Replaces manual logging with structured logger methods Introduces dedicated LoggerMessage attributes for PhysicalConnection operations to improve logging performance and consistency. Replaces string interpolation and manual log calls with strongly-typed logging methods that provide better compile-time safety and runtime performance through source generation. Covers connection lifecycle events including endpoint validation, connection attempts, TLS configuration, socket operations, and connection establishment. --- src/StackExchange.Redis/LoggerExtensions.cs | 86 +++++++++++++++++++ src/StackExchange.Redis/PhysicalBridge.cs | 16 ++-- src/StackExchange.Redis/PhysicalConnection.cs | 16 ++-- 3 files changed, 102 insertions(+), 16 deletions(-) diff --git a/src/StackExchange.Redis/LoggerExtensions.cs b/src/StackExchange.Redis/LoggerExtensions.cs index 3cb2ec6fb..759c3949b 100644 --- a/src/StackExchange.Redis/LoggerExtensions.cs +++ b/src/StackExchange.Redis/LoggerExtensions.cs @@ -579,4 +579,90 @@ internal static void LogWithThreadPoolStats(this ILogger? log, string message) EventId = 86, Message = "{Server}: Auto-configured (HELLO) role: {Role}")] internal static partial void LogInformationAutoConfiguredHelloRole(this ILogger logger, ServerEndPointLogValue server, string role); + + // PhysicalBridge logging methods + [LoggerMessage( + Level = LogLevel.Information, + EventId = 87, + Message = "{EndPoint}: OnEstablishingAsync complete")] + internal static partial void LogInformationOnEstablishingComplete(this ILogger logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 88, + Message = "{ErrorMessage}")] + internal static partial void LogInformationConnectionFailureRequested(this ILogger logger, Exception exception, string errorMessage); + + [LoggerMessage( + Level = LogLevel.Error, + EventId = 89, + Message = "{ErrorMessage}")] + internal static partial void LogErrorConnectionIssue(this ILogger logger, Exception exception, string errorMessage); + + [LoggerMessage( + Level = LogLevel.Warning, + EventId = 90, + Message = "Dead socket detected, no reads in {LastReadSecondsAgo} seconds with {TimeoutCount} timeouts, issuing disconnect")] + internal static partial void LogWarningDeadSocketDetected(this ILogger logger, long lastReadSecondsAgo, long timeoutCount); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 91, + Message = "Resurrecting {Bridge} (retry: {RetryCount})")] + internal static partial void LogInformationResurrecting(this ILogger logger, PhysicalBridge bridge, long retryCount); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 92, + Message = "{BridgeName}: Connecting...")] + internal static partial void LogInformationConnecting(this ILogger logger, string bridgeName); + + [LoggerMessage( + Level = LogLevel.Error, + EventId = 93, + Message = "{BridgeName}: Connect failed: {ErrorMessage}")] + internal static partial void LogErrorConnectFailed(this ILogger logger, Exception exception, string bridgeName, string errorMessage); + + // PhysicalConnection logging methods + [LoggerMessage( + Level = LogLevel.Error, + EventId = 94, + Message = "No endpoint")] + internal static partial void LogErrorNoEndpoint(this ILogger logger, Exception exception); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 95, + Message = "{EndPoint}: BeginConnectAsync")] + internal static partial void LogInformationBeginConnectAsync(this ILogger logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 96, + Message = "{EndPoint}: Starting read")] + internal static partial void LogInformationStartingRead(this ILogger logger, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Error, + EventId = 97, + Message = "{EndPoint}: (socket shutdown)")] + internal static partial void LogErrorSocketShutdown(this ILogger logger, Exception exception, EndPointLogValue endPoint); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 98, + Message = "Configuring TLS")] + internal static partial void LogInformationConfiguringTLS(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 99, + Message = "TLS connection established successfully using protocol: {SslProtocol}")] + internal static partial void LogInformationTLSConnectionEstablished(this ILogger logger, System.Security.Authentication.SslProtocols sslProtocol); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 100, + Message = "{BridgeName}: Connected")] + internal static partial void LogInformationConnected(this ILogger logger, string bridgeName); } diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index f5d75c188..c430cf5af 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -431,7 +431,7 @@ internal async Task OnConnectedAsync(PhysicalConnection connection, ILogger? log { ConnectedAt ??= DateTime.UtcNow; await ServerEndPoint.OnEstablishingAsync(connection, log).ForAwait(); - log?.LogInformation($"{Format.ToString(ServerEndPoint)}: OnEstablishingAsync complete"); + log?.LogInformationOnEstablishingComplete(new(ServerEndPoint.EndPoint)); } else { @@ -457,11 +457,11 @@ internal void OnConnectionFailed(PhysicalConnection connection, ConnectionFailur { if (wasRequested) { - Multiplexer.Logger?.LogInformation(innerException, innerException.Message); + Multiplexer.Logger?.LogInformationConnectionFailureRequested(innerException, innerException.Message); } else { - Multiplexer.Logger?.LogError(innerException, innerException.Message); + Multiplexer.Logger?.LogErrorConnectionIssue(innerException, innerException.Message); } Trace($"OnConnectionFailed: {connection}"); // If we're configured to, fail all pending backlogged messages @@ -589,7 +589,7 @@ internal void OnHeartbeat(bool ifConnectedOnly) Interlocked.Increment(ref connectTimeoutRetryCount); var ex = ExceptionFactory.UnableToConnect(Multiplexer, "ConnectTimeout", Name); LastException = ex; - Multiplexer.Logger?.LogError(ex, ex.Message); + Multiplexer.Logger?.LogErrorConnectionIssue(ex, ex.Message); Trace("Aborting connect"); // abort and reconnect var snapshot = physical; @@ -671,7 +671,7 @@ internal void OnHeartbeat(bool ifConnectedOnly) // This is meant to address the scenario we see often in Linux configs where TCP retries will happen for 15 minutes. // To us as a client, we'll see the socket as green/open/fine when writing but we'll bet getting nothing back. // Since we can't depend on the pipe to fail in that case, we want to error here based on the criteria above so we reconnect broken clients much faster. - tmp.BridgeCouldBeNull?.Multiplexer.Logger?.LogWarning($"Dead socket detected, no reads in {tmp.LastReadSecondsAgo} seconds with {timedOutThisHeartbeat} timeouts, issuing disconnect"); + tmp.BridgeCouldBeNull?.Multiplexer.Logger?.LogWarningDeadSocketDetected(tmp.LastReadSecondsAgo, timedOutThisHeartbeat); OnDisconnected(ConnectionFailureType.SocketFailure, tmp, out _, out State oldState); tmp.Dispose(); // Cleanup the existing connection/socket if any, otherwise it will wait reading indefinitely } @@ -691,7 +691,7 @@ internal void OnHeartbeat(bool ifConnectedOnly) // Increment count here, so that we don't re-enter in Connecting case up top - we don't want to re-enter and log there. Interlocked.Increment(ref connectTimeoutRetryCount); - Multiplexer.Logger?.LogInformation($"Resurrecting {ToString()} (retry: {connectTimeoutRetryCount})"); + Multiplexer.Logger?.LogInformationResurrecting(this, connectTimeoutRetryCount); Multiplexer.OnResurrecting(ServerEndPoint.EndPoint, ConnectionType); TryConnect(null); } @@ -1453,7 +1453,7 @@ private bool ChangeState(State oldState, State newState) { if (!Multiplexer.IsDisposed) { - log?.LogInformation($"{Name}: Connecting..."); + log?.LogInformationConnecting(Name); Multiplexer.Trace("Connecting...", Name); if (ChangeState(State.Disconnected, State.Connecting)) { @@ -1470,7 +1470,7 @@ private bool ChangeState(State oldState, State newState) } catch (Exception ex) { - log?.LogError(ex, $"{Name}: Connect failed: {ex.Message}"); + log?.LogErrorConnectFailed(ex, Name, ex.Message); Multiplexer.Trace("Connect failed: " + ex.Message, Name); ChangeState(State.Disconnected); OnInternalError(ex); diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 8a0bad393..324b952fd 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -109,7 +109,7 @@ internal async Task BeginConnectAsync(ILogger? log) var endpoint = bridge?.ServerEndPoint?.EndPoint; if (bridge == null || endpoint == null) { - log?.LogError(new ArgumentNullException(nameof(endpoint)), "No endpoint"); + log?.LogErrorNoEndpoint(new ArgumentNullException(nameof(endpoint))); return; } @@ -135,7 +135,7 @@ internal async Task BeginConnectAsync(ILogger? log) } } bridge.Multiplexer.OnConnecting(endpoint, bridge.ConnectionType); - log?.LogInformation($"{Format.ToString(endpoint)}: BeginConnectAsync"); + log?.LogInformationBeginConnectAsync(new(endpoint)); CancellationTokenSource? timeoutSource = null; try @@ -184,7 +184,7 @@ internal async Task BeginConnectAsync(ILogger? log) } else if (await ConnectedAsync(x, log, bridge.Multiplexer.SocketManager!).ForAwait()) { - log?.LogInformation($"{Format.ToString(endpoint)}: Starting read"); + log?.LogInformationStartingRead(new(endpoint)); try { StartReading(); @@ -204,7 +204,7 @@ internal async Task BeginConnectAsync(ILogger? log) } catch (ObjectDisposedException ex) { - log?.LogError(ex, $"{Format.ToString(endpoint)}: (socket shutdown)"); + log?.LogErrorSocketShutdown(ex, new(endpoint)); try { RecordConnectionFailed(ConnectionFailureType.UnableToConnect, isInitialConnect: true); } catch (Exception inner) { @@ -1563,7 +1563,7 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock if (config.Ssl) { - log?.LogInformation("Configuring TLS"); + log?.LogInformationConfiguringTLS(); var host = config.SslHost; if (host.IsNullOrWhiteSpace()) { @@ -1599,10 +1599,10 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock { Debug.WriteLine(ex.Message); bridge.Multiplexer.SetAuthSuspect(ex); - bridge.Multiplexer.Logger?.LogError(ex, ex.Message); + bridge.Multiplexer.Logger?.LogErrorConnectionIssue(ex, ex.Message); throw; } - log?.LogInformation($"TLS connection established successfully using protocol: {ssl.SslProtocol}"); + log?.LogInformationTLSConnectionEstablished(ssl.SslProtocol); } catch (AuthenticationException authexception) { @@ -1625,7 +1625,7 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock _ioPipe = pipe; - log?.LogInformation($"{bridge.Name}: Connected "); + log?.LogInformationConnected(bridge.Name); await bridge.OnConnectedAsync(this, log).ForAwait(); return true; From 45f7af46846b43b7d85d6ea499279e4b476a3842 Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Fri, 25 Jul 2025 15:29:44 +0300 Subject: [PATCH 357/435] Replaces string interpolation with structured logging (#2930) Converts manual log message formatting to LoggerMessage source generator methods for better performance and structured logging support. Improves logging efficiency by eliminating string allocations when logging is disabled and provides consistent structured data for log analysis tools. --- .../ConnectionMultiplexer.Sentinel.cs | 2 +- .../ConnectionMultiplexer.cs | 12 ++--- src/StackExchange.Redis/EndPointCollection.cs | 6 +-- src/StackExchange.Redis/LoggerExtensions.cs | 44 +++++++++++++++++++ 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs index 88e42ed5e..b1bf7371c 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs @@ -396,7 +396,7 @@ internal void SwitchPrimary(EndPoint? switchBlame, ConnectionMultiplexer connect var logger = Logger.With(writer); if (connection.RawConfig.ServiceName is not string serviceName) { - logger?.LogInformation("Service name not defined."); + logger?.LogInformationServiceNameNotDefined(); return; } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 146f7d2d3..bf1b75e25 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1335,14 +1335,16 @@ internal void GetStatus(ILogger? log) if (log == null) return; var tmp = GetServerSnapshot(); - log?.LogInformation("Endpoint Summary:"); + log.LogInformationEndpointSummaryHeader(); foreach (var server in tmp) { - log?.LogInformation(" " + server.Summary()); - log?.LogInformation(" " + server.GetCounters().ToString()); - log?.LogInformation(" " + server.GetProfile()); + log.LogInformationServerSummary(server.Summary(), server.GetCounters(), server.GetProfile()); } - log?.LogInformation($"Sync timeouts: {Interlocked.Read(ref syncTimeouts)}; async timeouts: {Interlocked.Read(ref asyncTimeouts)}; fire and forget: {Interlocked.Read(ref fireAndForgets)}; last heartbeat: {LastHeartbeatSecondsAgo}s ago"); + log.LogInformationTimeoutsSummary( + Interlocked.Read(ref syncTimeouts), + Interlocked.Read(ref asyncTimeouts), + Interlocked.Read(ref fireAndForgets), + LastHeartbeatSecondsAgo); } private void ActivateAllServers(ILogger? log) diff --git a/src/StackExchange.Redis/EndPointCollection.cs b/src/StackExchange.Redis/EndPointCollection.cs index cf4c844c1..359f6d811 100644 --- a/src/StackExchange.Redis/EndPointCollection.cs +++ b/src/StackExchange.Redis/EndPointCollection.cs @@ -209,12 +209,12 @@ internal async Task ResolveEndPointsAsync(ConnectionMultiplexer multiplexer, ILo } else { - log?.LogInformation($"Using DNS to resolve '{dns.Host}'..."); + log?.LogInformationUsingDnsToResolve(dns.Host); var ips = await Dns.GetHostAddressesAsync(dns.Host).ObserveErrors().ForAwait(); if (ips.Length == 1) { ip = ips[0]; - log?.LogInformation($"'{dns.Host}' => {ip}"); + log?.LogInformationDnsResolutionResult(dns.Host, ip); cache[dns.Host] = ip; this[i] = new IPEndPoint(ip, dns.Port); } @@ -223,7 +223,7 @@ internal async Task ResolveEndPointsAsync(ConnectionMultiplexer multiplexer, ILo catch (Exception ex) { multiplexer.OnInternalError(ex); - log?.LogError(ex, ex.Message); + log?.LogErrorDnsResolution(ex, ex.Message); } } } diff --git a/src/StackExchange.Redis/LoggerExtensions.cs b/src/StackExchange.Redis/LoggerExtensions.cs index 759c3949b..be51733ce 100644 --- a/src/StackExchange.Redis/LoggerExtensions.cs +++ b/src/StackExchange.Redis/LoggerExtensions.cs @@ -665,4 +665,48 @@ internal static void LogWithThreadPoolStats(this ILogger? log, string message) EventId = 100, Message = "{BridgeName}: Connected")] internal static partial void LogInformationConnected(this ILogger logger, string bridgeName); + + // ConnectionMultiplexer GetStatus logging methods + [LoggerMessage( + Level = LogLevel.Information, + EventId = 101, + Message = "Endpoint Summary:")] + internal static partial void LogInformationEndpointSummaryHeader(this ILogger logger); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 102, + Message = "Server summary: {ServerSummary}, counters: {ServerCounters}, profile: {ServerProfile}")] + internal static partial void LogInformationServerSummary(this ILogger logger, string serverSummary, ServerCounters serverCounters, string serverProfile); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 105, + Message = "Sync timeouts: {SyncTimeouts}; async timeouts: {AsyncTimeouts}; fire and forget: {FireAndForgets}; last heartbeat: {LastHeartbeatSecondsAgo}s ago")] + internal static partial void LogInformationTimeoutsSummary(this ILogger logger, long syncTimeouts, long asyncTimeouts, long fireAndForgets, long lastHeartbeatSecondsAgo); + + // EndPointCollection logging methods + [LoggerMessage( + Level = LogLevel.Information, + EventId = 106, + Message = "Using DNS to resolve '{DnsHost}'...")] + internal static partial void LogInformationUsingDnsToResolve(this ILogger logger, string dnsHost); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 107, + Message = "'{DnsHost}' => {IpAddress}")] + internal static partial void LogInformationDnsResolutionResult(this ILogger logger, string dnsHost, IPAddress ipAddress); + + [LoggerMessage( + Level = LogLevel.Error, + EventId = 108, + Message = "{ErrorMessage}")] + internal static partial void LogErrorDnsResolution(this ILogger logger, Exception exception, string errorMessage); + + [LoggerMessage( + Level = LogLevel.Information, + EventId = 109, + Message = "Service name not defined.")] + internal static partial void LogInformationServiceNameNotDefined(this ILogger logger); } From b995ebccc661acd637c6d746fe47ee44b12c1479 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 25 Jul 2025 10:47:29 -0400 Subject: [PATCH 358/435] Tests: Add benchmark suite for easily measuring improvements (#2931) * Tests: Add benchmark suite for easily measuring improvements * - fix mono - add double parse - add double format * limit netfx to windows * tyop --------- Co-authored-by: Marc Gravell --- StackExchange.Redis.sln | 9 +++- .../StackExchange.Redis.csproj | 1 + .../CustomConfig.cs | 28 ++++++++++ .../FormatBenchmarks.cs | 53 +++++++++++++++++++ .../StackExchange.Redis.Benchmarks/Program.cs | 10 ++++ .../SlowConfig.cs | 12 +++++ .../StackExchange.Redis.Benchmarks.csproj | 15 ++++++ tests/StackExchange.Redis.Benchmarks/run.cmd | 1 + tests/StackExchange.Redis.Benchmarks/run.sh | 2 + 9 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 tests/StackExchange.Redis.Benchmarks/CustomConfig.cs create mode 100644 tests/StackExchange.Redis.Benchmarks/FormatBenchmarks.cs create mode 100644 tests/StackExchange.Redis.Benchmarks/Program.cs create mode 100644 tests/StackExchange.Redis.Benchmarks/SlowConfig.cs create mode 100644 tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj create mode 100644 tests/StackExchange.Redis.Benchmarks/run.cmd create mode 100755 tests/StackExchange.Redis.Benchmarks/run.sh diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index 20b5e2f01..8f772ae42 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -13,13 +13,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets Directory.Packages.props = Directory.Packages.props + tests\RedisConfigs\docker-compose.yml = tests\RedisConfigs\docker-compose.yml global.json = global.json NuGet.Config = NuGet.Config README.md = README.md docs\ReleaseNotes.md = docs\ReleaseNotes.md Shared.ruleset = Shared.ruleset version.json = version.json - tests\RedisConfigs\docker-compose.yml = tests\RedisConfigs\docker-compose.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RedisConfigs", "RedisConfigs", "{96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}" @@ -120,6 +120,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleTestBaseline", "test EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs", "docs\docs.csproj", "{1DC43E76-5372-4C7F-A433-0602273E87FC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Benchmarks", "tests\StackExchange.Redis.Benchmarks\StackExchange.Redis.Benchmarks.csproj", "{59889284-FFEE-82E7-94CB-3B43E87DA6CF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -174,6 +176,10 @@ Global {1DC43E76-5372-4C7F-A433-0602273E87FC}.Debug|Any CPU.Build.0 = Debug|Any CPU {1DC43E76-5372-4C7F-A433-0602273E87FC}.Release|Any CPU.ActiveCfg = Release|Any CPU {1DC43E76-5372-4C7F-A433-0602273E87FC}.Release|Any CPU.Build.0 = Release|Any CPU + {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -195,6 +201,7 @@ Global {A9F81DA3-DA82-423E-A5DD-B11C37548E06} = {96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05} {A0F89B8B-32A3-4C28-8F1B-ADE343F16137} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} {69A0ACF2-DF1F-4F49-B554-F732DCA938A3} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} + {59889284-FFEE-82E7-94CB-3B43E87DA6CF} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {193AA352-6748-47C1-A5FC-C9AA6B5F000B} diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index e1b428a36..44efe09be 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -43,6 +43,7 @@ + diff --git a/tests/StackExchange.Redis.Benchmarks/CustomConfig.cs b/tests/StackExchange.Redis.Benchmarks/CustomConfig.cs new file mode 100644 index 000000000..09f44cc31 --- /dev/null +++ b/tests/StackExchange.Redis.Benchmarks/CustomConfig.cs @@ -0,0 +1,28 @@ +using System.Runtime.InteropServices; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Validators; + +namespace StackExchange.Redis.Benchmarks +{ + internal class CustomConfig : ManualConfig + { + protected virtual Job Configure(Job j) => j; + + public CustomConfig() + { + AddDiagnoser(MemoryDiagnoser.Default); + AddColumn(StatisticColumn.OperationsPerSecond); + AddValidator(JitOptimizationsValidator.FailOnError); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + AddJob(Configure(Job.Default.WithRuntime(ClrRuntime.Net481))); + } + AddJob(Configure(Job.Default.WithRuntime(CoreRuntime.Core80))); + } + } +} diff --git a/tests/StackExchange.Redis.Benchmarks/FormatBenchmarks.cs b/tests/StackExchange.Redis.Benchmarks/FormatBenchmarks.cs new file mode 100644 index 000000000..77548b254 --- /dev/null +++ b/tests/StackExchange.Redis.Benchmarks/FormatBenchmarks.cs @@ -0,0 +1,53 @@ +using System; +using System.Net; +using BenchmarkDotNet.Attributes; + +namespace StackExchange.Redis.Benchmarks +{ + [Config(typeof(CustomConfig))] + public class FormatBenchmarks + { + [GlobalSetup] + public void Setup() { } + + [Benchmark] + [Arguments("64")] + [Arguments("-1")] + [Arguments("0")] + [Arguments("123442")] + public long ParseInt64(string s) => Format.ParseInt64(s); + + [Benchmark] + [Arguments("64")] + [Arguments("-1")] + [Arguments("0")] + [Arguments("123442")] + public long ParseInt32(string s) => Format.ParseInt32(s); + + [Benchmark] + [Arguments("64")] + [Arguments("-1")] + [Arguments("0")] + [Arguments("123442")] + [Arguments("-inf")] + [Arguments("nan")] + public double ParseDouble(string s) => Format.TryParseDouble(s, out var val) ? val : double.NaN; + + private byte[] buffer = new byte[128]; + + [Benchmark] + [Arguments(64D)] + [Arguments(-1D)] + [Arguments(0D)] + [Arguments(123442D)] + [Arguments(double.NegativeInfinity)] + [Arguments(double.NaN)] + public int FormatDouble(double value) => Format.FormatDouble(value, buffer.AsSpan()); + + [Benchmark] + [Arguments("host.com", -1)] + [Arguments("host.com", 0)] + [Arguments("host.com", 65345)] + public EndPoint ParseEndPoint(string host, int port) => Format.ParseEndPoint(host, port); + } +} diff --git a/tests/StackExchange.Redis.Benchmarks/Program.cs b/tests/StackExchange.Redis.Benchmarks/Program.cs new file mode 100644 index 000000000..622d7d593 --- /dev/null +++ b/tests/StackExchange.Redis.Benchmarks/Program.cs @@ -0,0 +1,10 @@ +using System.Reflection; +using BenchmarkDotNet.Running; + +namespace StackExchange.Redis.Benchmarks +{ + internal static class Program + { + private static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); + } +} diff --git a/tests/StackExchange.Redis.Benchmarks/SlowConfig.cs b/tests/StackExchange.Redis.Benchmarks/SlowConfig.cs new file mode 100644 index 000000000..0c3546006 --- /dev/null +++ b/tests/StackExchange.Redis.Benchmarks/SlowConfig.cs @@ -0,0 +1,12 @@ +using BenchmarkDotNet.Jobs; + +namespace StackExchange.Redis.Benchmarks +{ + internal class SlowConfig : CustomConfig + { + protected override Job Configure(Job j) + => j.WithLaunchCount(1) + .WithWarmupCount(1) + .WithIterationCount(5); + } +} diff --git a/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj b/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj new file mode 100644 index 000000000..be9a3081b --- /dev/null +++ b/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj @@ -0,0 +1,15 @@ + + + StackExchange.Redis MicroBenchmark Suite + net481;net8.0 + Release + Exe + true + + + + + + + + diff --git a/tests/StackExchange.Redis.Benchmarks/run.cmd b/tests/StackExchange.Redis.Benchmarks/run.cmd new file mode 100644 index 000000000..2b8844c56 --- /dev/null +++ b/tests/StackExchange.Redis.Benchmarks/run.cmd @@ -0,0 +1 @@ +dotnet run --framework net8.0 -c Release %* \ No newline at end of file diff --git a/tests/StackExchange.Redis.Benchmarks/run.sh b/tests/StackExchange.Redis.Benchmarks/run.sh new file mode 100755 index 000000000..1824c7161 --- /dev/null +++ b/tests/StackExchange.Redis.Benchmarks/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet run --framework net8.0 -c Release "$@" \ No newline at end of file From 0f4f4694f4f2ffbd314fb5597aecaead803488e0 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 25 Jul 2025 19:52:56 +0100 Subject: [PATCH 359/435] Improve `double` formatting performance on net8+ and fix equality incorrectness re special doubles (#2928) * Improve `double` formatting performance on net8+ * release notes * - set max len - use in physicalconnection * radical idea: let's actually copy the data * simplify inf/nan parsing - massively reduce ToLower overhead * NaN rules * avoid problem with literal "nan" / "inf" having inconsistent equality behaviour * update/clarify nan/inf rules in test --- docs/ReleaseNotes.md | 3 +- src/StackExchange.Redis/Format.cs | 133 +++++++++++------- src/StackExchange.Redis/PhysicalConnection.cs | 39 +++-- src/StackExchange.Redis/RedisValue.cs | 12 +- .../KeyAndValueTests.cs | 6 + .../RedisValueEquivalencyTests.cs | 124 +++++++++++++++- 6 files changed, 253 insertions(+), 64 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 7f2ed4432..3a50f1ef3 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -12,7 +12,8 @@ Current package versions: - Fix key-prefix omission in `SetIntersectionLength` and `SortedSet{Combine[WithScores]|IntersectionLength}` ([#2863 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2863)) - Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638)) - Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822)) -- +- Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2928)) + ## 2.8.58 - Fix [#2679](https://github.com/StackExchange/StackExchange.Redis/issues/2679) - blocking call in long-running connects ([#2680 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2680)) diff --git a/src/StackExchange.Redis/Format.cs b/src/StackExchange.Redis/Format.cs index 329da4363..86aa9910d 100644 --- a/src/StackExchange.Redis/Format.cs +++ b/src/StackExchange.Redis/Format.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Net; +using System.Runtime.CompilerServices; using System.Text; #if UNIX_SOCKET @@ -168,24 +169,41 @@ internal static bool TryParseDouble(string? s, out double value) value = s[0] - '0'; return true; // RESP3 spec demands inf/nan handling - case 3 when CaseInsensitiveASCIIEqual("inf", s): - value = double.PositiveInfinity; - return true; - case 3 when CaseInsensitiveASCIIEqual("nan", s): - value = double.NaN; - return true; - case 4 when CaseInsensitiveASCIIEqual("+inf", s): - value = double.PositiveInfinity; - return true; - case 4 when CaseInsensitiveASCIIEqual("-inf", s): - value = double.NegativeInfinity; - return true; - case 4 when CaseInsensitiveASCIIEqual("+nan", s): - case 4 when CaseInsensitiveASCIIEqual("-nan", s): - value = double.NaN; + case 3 when TryParseInfNaN(s.AsSpan(), true, out value): + case 4 when s[0] == '+' && TryParseInfNaN(s.AsSpan(1), true, out value): + case 4 when s[0] == '-' && TryParseInfNaN(s.AsSpan(1), false, out value): return true; } return double.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out value); + + static bool TryParseInfNaN(ReadOnlySpan s, bool positive, out double value) + { + switch (s[0]) + { + case 'i': + case 'I': + if (s[1] is 'n' or 'N' && s[2] is 'f' or 'F') + { + value = positive ? double.PositiveInfinity : double.NegativeInfinity; + return true; + } + break; + case 'n': + case 'N': + if (s[1] is 'a' or 'A' && s[2] is 'n' or 'N') + { + value = double.NaN; + return true; + } + break; + } +#if NET6_0_OR_GREATER + Unsafe.SkipInit(out value); +#else + value = 0; +#endif + return false; + } } internal static bool TryParseUInt64(string s, out ulong value) => @@ -235,37 +253,41 @@ internal static bool TryParseDouble(ReadOnlySpan s, out double value) value = s[0] - '0'; return true; // RESP3 spec demands inf/nan handling - case 3 when CaseInsensitiveASCIIEqual("inf", s): - value = double.PositiveInfinity; - return true; - case 3 when CaseInsensitiveASCIIEqual("nan", s): - value = double.NaN; - return true; - case 4 when CaseInsensitiveASCIIEqual("+inf", s): - value = double.PositiveInfinity; - return true; - case 4 when CaseInsensitiveASCIIEqual("-inf", s): - value = double.NegativeInfinity; - return true; - case 4 when CaseInsensitiveASCIIEqual("+nan", s): - case 4 when CaseInsensitiveASCIIEqual("-nan", s): - value = double.NaN; + case 3 when TryParseInfNaN(s, true, out value): + case 4 when s[0] == '+' && TryParseInfNaN(s.Slice(1), true, out value): + case 4 when s[0] == '-' && TryParseInfNaN(s.Slice(1), false, out value): return true; } return Utf8Parser.TryParse(s, out value, out int bytes) & bytes == s.Length; - } - - private static bool CaseInsensitiveASCIIEqual(string xLowerCase, string y) - => string.Equals(xLowerCase, y, StringComparison.OrdinalIgnoreCase); - private static bool CaseInsensitiveASCIIEqual(string xLowerCase, ReadOnlySpan y) - { - if (y.Length != xLowerCase.Length) return false; - for (int i = 0; i < y.Length; i++) + static bool TryParseInfNaN(ReadOnlySpan s, bool positive, out double value) { - if (char.ToLower((char)y[i]) != xLowerCase[i]) return false; + switch (s[0]) + { + case (byte)'i': + case (byte)'I': + if (s[1] is (byte)'n' or (byte)'N' && s[2] is (byte)'f' or (byte)'F') + { + value = positive ? double.PositiveInfinity : double.NegativeInfinity; + return true; + } + break; + case (byte)'n': + case (byte)'N': + if (s[1] is (byte)'a' or (byte)'A' && s[2] is (byte)'n' or (byte)'N') + { + value = double.NaN; + return true; + } + break; + } +#if NET6_0_OR_GREATER + Unsafe.SkipInit(out value); +#else + value = 0; +#endif + return false; } - return true; } /// @@ -399,11 +421,21 @@ internal static unsafe string GetString(ReadOnlySpan span) internal const int MaxInt32TextLen = 11, // -2,147,483,648 (not including the commas) - MaxInt64TextLen = 20; // -9,223,372,036,854,775,808 (not including the commas) + MaxInt64TextLen = 20, // -9,223,372,036,854,775,808 (not including the commas), + MaxDoubleTextLen = 40; // we use G17, allow for sign/E/and allow plenty of panic room internal static int MeasureDouble(double value) { if (double.IsInfinity(value)) return 4; // +inf / -inf + +#if NET8_0_OR_GREATER // can use IUtf8Formattable + Span buffer = stackalloc byte[MaxDoubleTextLen]; + if (value.TryFormat(buffer, out int len, "G17", NumberFormatInfo.InvariantInfo)) + { + return len; + } +#endif + // fallback (TFM or unexpected size) var s = value.ToString("G17", NumberFormatInfo.InvariantInfo); // this looks inefficient, but is how Utf8Formatter works too, just: more direct return s.Length; } @@ -412,16 +444,18 @@ internal static int FormatDouble(double value, Span destination) { if (double.IsInfinity(value)) { - if (double.IsPositiveInfinity(value)) - { - if (!"+inf"u8.TryCopyTo(destination)) ThrowFormatFailed(); - } - else - { - if (!"-inf"u8.TryCopyTo(destination)) ThrowFormatFailed(); - } + if (!(double.IsPositiveInfinity(value) ? "+inf"u8 : "-inf"u8).TryCopyTo(destination)) ThrowFormatFailed(); return 4; } + +#if NET8_0_OR_GREATER // can use IUtf8Formattable + if (!value.TryFormat(destination, out int len, "G17", NumberFormatInfo.InvariantInfo)) + { + ThrowFormatFailed(); + } + + return len; +#else var s = value.ToString("G17", NumberFormatInfo.InvariantInfo); // this looks inefficient, but is how Utf8Formatter works too, just: more direct if (s.Length > destination.Length) ThrowFormatFailed(); @@ -431,6 +465,7 @@ internal static int FormatDouble(double value, Span destination) destination[i] = (byte)chars[i]; } return chars.Length; +#endif } internal static int MeasureInt64(long value) diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 324b952fd..c587241a0 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -845,7 +845,9 @@ internal static void WriteBulkString(in RedisValue value, PipeWriter? maybeNullW case RedisValue.StorageType.UInt64: WriteUnifiedUInt64(writer, value.OverlappedValueUInt64); break; - case RedisValue.StorageType.Double: // use string + case RedisValue.StorageType.Double: + WriteUnifiedDouble(writer, value.OverlappedValueDouble); + break; case RedisValue.StorageType.String: WriteUnifiedPrefixedString(writer, null, (string?)value); break; @@ -1341,9 +1343,9 @@ private static void WriteUnifiedInt64(PipeWriter writer, long value) // note from specification: A client sends to the Redis server a RESP Array consisting of just Bulk Strings. // (i.e. we can't just send ":123\r\n", we need to send "$3\r\n123\r\n" - // ${asc-len}\r\n = 3 + MaxInt32TextLen + // ${asc-len}\r\n = 4/5 (asc-len at most 2 digits) // {asc}\r\n = MaxInt64TextLen + 2 - var span = writer.GetSpan(5 + Format.MaxInt32TextLen + Format.MaxInt64TextLen); + var span = writer.GetSpan(7 + Format.MaxInt64TextLen); span[0] = (byte)'$'; var bytes = WriteRaw(span, value, withLengthPrefix: true, offset: 1); @@ -1354,20 +1356,41 @@ private static void WriteUnifiedUInt64(PipeWriter writer, ulong value) { // note from specification: A client sends to the Redis server a RESP Array consisting of just Bulk Strings. // (i.e. we can't just send ":123\r\n", we need to send "$3\r\n123\r\n" - - // ${asc-len}\r\n = 3 + MaxInt32TextLen - // {asc}\r\n = MaxInt64TextLen + 2 - var span = writer.GetSpan(5 + Format.MaxInt32TextLen + Format.MaxInt64TextLen); - Span valueSpan = stackalloc byte[Format.MaxInt64TextLen]; + var len = Format.FormatUInt64(value, valueSpan); + // ${asc-len}\r\n = 4/5 (asc-len at most 2 digits) + // {asc}\r\n = {len} + 2 + var span = writer.GetSpan(7 + len); + span[0] = (byte)'$'; + int offset = WriteRaw(span, len, withLengthPrefix: false, offset: 1); + valueSpan.Slice(0, len).CopyTo(span.Slice(offset)); + offset += len; + offset = WriteCrlf(span, offset); + writer.Advance(offset); + } + + private static void WriteUnifiedDouble(PipeWriter writer, double value) + { +#if NET8_0_OR_GREATER + Span valueSpan = stackalloc byte[Format.MaxDoubleTextLen]; + var len = Format.FormatDouble(value, valueSpan); + + // ${asc-len}\r\n = 4/5 (asc-len at most 2 digits) + // {asc}\r\n = {len} + 2 + var span = writer.GetSpan(7 + len); span[0] = (byte)'$'; int offset = WriteRaw(span, len, withLengthPrefix: false, offset: 1); valueSpan.Slice(0, len).CopyTo(span.Slice(offset)); offset += len; offset = WriteCrlf(span, offset); writer.Advance(offset); +#else + // fallback: drop to string + WriteUnifiedPrefixedString(writer, null, Format.ToString(value)); +#endif } + internal static void WriteInteger(PipeWriter writer, long value) { // note: client should never write integer; only server does this diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index a670607de..f198f3d08 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -149,7 +149,7 @@ public bool IsNullOrEmpty /// The second to compare. public static bool operator !=(RedisValue x, RedisValue y) => !(x == y); - private double OverlappedValueDouble + internal double OverlappedValueDouble { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => BitConverter.Int64BitsToDouble(_overlappedBits64); @@ -849,7 +849,7 @@ private static string ToHex(ReadOnlySpan src) len = Format.FormatUInt64(value.OverlappedValueUInt64, span); return span.Slice(0, len).ToArray(); case StorageType.Double: - span = stackalloc byte[128]; + span = stackalloc byte[Format.MaxDoubleTextLen]; len = Format.FormatDouble(value.OverlappedValueDouble, span); return span.Slice(0, len).ToArray(); case StorageType.String: @@ -986,7 +986,8 @@ internal RedisValue Simplify() if (Format.TryParseInt64(s, out i64)) return i64; if (Format.TryParseUInt64(s, out u64)) return u64; } - if (Format.TryParseDouble(s, out var f64)) return f64; + // note: don't simplify inf/nan, as that causes equality semantic problems + if (Format.TryParseDouble(s, out var f64) && !IsSpecialDouble(f64)) return f64; break; case StorageType.Raw: var b = _memory.Span; @@ -995,7 +996,8 @@ internal RedisValue Simplify() if (Format.TryParseInt64(b, out i64)) return i64; if (Format.TryParseUInt64(b, out u64)) return u64; } - if (TryParseDouble(b, out f64)) return f64; + // note: don't simplify inf/nan, as that causes equality semantic problems + if (TryParseDouble(b, out f64) && !IsSpecialDouble(f64)) return f64; break; case StorageType.Double: // is the double actually an integer? @@ -1006,6 +1008,8 @@ internal RedisValue Simplify() return this; } + private static bool IsSpecialDouble(double d) => double.IsNaN(d) || double.IsInfinity(d); + /// /// Convert to a signed if possible. /// diff --git a/tests/StackExchange.Redis.Tests/KeyAndValueTests.cs b/tests/StackExchange.Redis.Tests/KeyAndValueTests.cs index 1f8864083..6d37fbe7a 100644 --- a/tests/StackExchange.Redis.Tests/KeyAndValueTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyAndValueTests.cs @@ -64,6 +64,12 @@ public void TestValues() internal static void CheckSame(RedisValue x, RedisValue y) { + if (x.TryParse(out double value) && double.IsNaN(value)) + { + // NaN has atypical equality rules + Assert.True(y.TryParse(out value) && double.IsNaN(value)); + return; + } Assert.True(Equals(x, y), "Equals(x, y)"); Assert.True(Equals(y, x), "Equals(y, x)"); Assert.True(EqualityComparer.Default.Equals(x, y), "EQ(x,y)"); diff --git a/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs b/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs index 7a56d16b0..7f6ad1561 100644 --- a/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs +++ b/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs @@ -136,12 +136,132 @@ static void Check(RedisValue known, RedisValue test) CheckString(-1099511627848.6001, "-1099511627848.6001"); Check(double.NegativeInfinity, double.NegativeInfinity); - Check(double.NegativeInfinity, "-inf"); CheckString(double.NegativeInfinity, "-inf"); Check(double.PositiveInfinity, double.PositiveInfinity); - Check(double.PositiveInfinity, "+inf"); CheckString(double.PositiveInfinity, "+inf"); + + Check(double.NaN, double.NaN); + CheckString(double.NaN, "NaN"); + } + + [Theory] + [InlineData("na")] + [InlineData("nan")] + [InlineData("nans")] + [InlineData("in")] + [InlineData("inf")] + [InlineData("info")] + public void SpecialCaseEqualityRules_String(string value) + { + RedisValue x = value, y = value; + Assert.Equal(x, y); + + Assert.True(x.Equals(y)); + Assert.True(y.Equals(x)); + Assert.True(x == y); + Assert.True(y == x); + Assert.False(x != y); + Assert.False(y != x); + Assert.Equal(x.GetHashCode(), y.GetHashCode()); + } + + [Theory] + [InlineData("na")] + [InlineData("nan")] + [InlineData("nans")] + [InlineData("in")] + [InlineData("inf")] + [InlineData("info")] + public void SpecialCaseEqualityRules_Bytes(string value) + { + byte[] bytes0 = Encoding.UTF8.GetBytes(value), + bytes1 = Encoding.UTF8.GetBytes(value); + Assert.NotSame(bytes0, bytes1); + RedisValue x = bytes0, y = bytes1; + + Assert.True(x.Equals(y)); + Assert.True(y.Equals(x)); + Assert.True(x == y); + Assert.True(y == x); + Assert.False(x != y); + Assert.False(y != x); + Assert.Equal(x.GetHashCode(), y.GetHashCode()); + } + + [Theory] + [InlineData("na")] + [InlineData("nan")] + [InlineData("nans")] + [InlineData("in")] + [InlineData("inf")] + [InlineData("info")] + public void SpecialCaseEqualityRules_Hybrid(string value) + { + byte[] bytes = Encoding.UTF8.GetBytes(value); + RedisValue x = bytes, y = value; + + Assert.True(x.Equals(y)); + Assert.True(y.Equals(x)); + Assert.True(x == y); + Assert.True(y == x); + Assert.False(x != y); + Assert.False(y != x); + Assert.Equal(x.GetHashCode(), y.GetHashCode()); + } + + [Theory] + [InlineData("na", "NA")] + [InlineData("nan", "NAN")] + [InlineData("nans", "NANS")] + [InlineData("in", "IN")] + [InlineData("inf", "INF")] + [InlineData("info", "INFO")] + public void SpecialCaseNonEqualityRules_String(string s, string t) + { + RedisValue x = s, y = t; + Assert.False(x.Equals(y)); + Assert.False(y.Equals(x)); + Assert.False(x == y); + Assert.False(y == x); + Assert.True(x != y); + Assert.True(y != x); + } + + [Theory] + [InlineData("na", "NA")] + [InlineData("nan", "NAN")] + [InlineData("nans", "NANS")] + [InlineData("in", "IN")] + [InlineData("inf", "INF")] + [InlineData("info", "INFO")] + public void SpecialCaseNonEqualityRules_Bytes(string s, string t) + { + RedisValue x = Encoding.UTF8.GetBytes(s), y = Encoding.UTF8.GetBytes(t); + Assert.False(x.Equals(y)); + Assert.False(y.Equals(x)); + Assert.False(x == y); + Assert.False(y == x); + Assert.True(x != y); + Assert.True(y != x); + } + + [Theory] + [InlineData("na", "NA")] + [InlineData("nan", "NAN")] + [InlineData("nans", "NANS")] + [InlineData("in", "IN")] + [InlineData("inf", "INF")] + [InlineData("info", "INFO")] + public void SpecialCaseNonEqualityRules_Hybrid(string s, string t) + { + RedisValue x = s, y = Encoding.UTF8.GetBytes(t); + Assert.False(x.Equals(y)); + Assert.False(y.Equals(x)); + Assert.False(x == y); + Assert.False(y == x); + Assert.True(x != y); + Assert.True(y != x); } private static void CheckString(RedisValue value, string expected) From f6e8b64c75521247c116c81ed5288e7b67638c14 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 29 Jul 2025 13:34:33 +0100 Subject: [PATCH 360/435] intellisense clarification for StreamTrimResult.NotDeleted (#2933) --- src/StackExchange.Redis/Enums/StreamTrimResult.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StackExchange.Redis/Enums/StreamTrimResult.cs b/src/StackExchange.Redis/Enums/StreamTrimResult.cs index aa157a8a0..e58c321ab 100644 --- a/src/StackExchange.Redis/Enums/StreamTrimResult.cs +++ b/src/StackExchange.Redis/Enums/StreamTrimResult.cs @@ -16,7 +16,8 @@ public enum StreamTrimResult Deleted = 1, /// - /// Entry was not deleted, but there are still dangling references. + /// Entry was not deleted because it has either not been delivered to any consumer, or + /// still has references in the consumer groups' Pending Entries List (PEL). /// /// This response relates to the mode. NotDeleted = 2, From 8fffdff0ed09db72ea375dcf25c76a5db5140383 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Sat, 2 Aug 2025 14:57:01 -0400 Subject: [PATCH 361/435] Tests: Improve stability for AbortOnConnectFailTests (#2934) * Tests: Improve stability for AbortOnConnectFailTests These had a lot timeout for runtime due to a shared method for faster test execution but in the day but the success case _should_ be fast but worst case it should not fail - we'd much rather it occasionally take a second than fail the test suite and add noise to the process. * Use Vampire/setup-wsl@v6 Fixes a few issues and transitions us to WSL2 underneath - let's see how this works * Revert "Use Vampire/setup-wsl@v6" This reverts commit 90c1c3e67cfd79361365949daa3683dd8bf288f8. --- .../StackExchange.Redis.Tests/AbortOnConnectFailTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs index b68e57c9e..25033fa1a 100644 --- a/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs +++ b/tests/StackExchange.Redis.Tests/AbortOnConnectFailTests.cs @@ -81,16 +81,16 @@ public async Task DisconnectAndNoReconnectThrowsConnectionExceptionAsync() } private ConnectionMultiplexer GetFailFastConn() => - ConnectionMultiplexer.Connect(GetOptions(BacklogPolicy.FailFast, 400).Apply(o => o.EndPoints.Add($"doesnot.exist.{Guid.NewGuid():N}:6379")), Writer); + ConnectionMultiplexer.Connect(GetOptions(BacklogPolicy.FailFast, duration: 400, connectTimeout: 500).Apply(o => o.EndPoints.Add($"doesnot.exist.{Guid.NewGuid():N}:6379")), Writer); private ConnectionMultiplexer GetWorkingBacklogConn() => - ConnectionMultiplexer.Connect(GetOptions(BacklogPolicy.Default, 1000).Apply(o => o.EndPoints.Add(GetConfiguration())), Writer); + ConnectionMultiplexer.Connect(GetOptions(BacklogPolicy.Default).Apply(o => o.EndPoints.Add(GetConfiguration())), Writer); - private static ConfigurationOptions GetOptions(BacklogPolicy policy, int duration) => new ConfigurationOptions() + private static ConfigurationOptions GetOptions(BacklogPolicy policy, int duration = 1000, int connectTimeout = 2000) => new ConfigurationOptions() { AbortOnConnectFail = false, BacklogPolicy = policy, - ConnectTimeout = 500, + ConnectTimeout = connectTimeout, SyncTimeout = duration, KeepAlive = duration, AllowAdmin = true, From b03e5f76a644bc69723b78b466a7a219c1b6f866 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 18 Aug 2025 19:55:54 +0100 Subject: [PATCH 362/435] fix error in XADD arg-count calculation (#2941) * fix error in XADD arg-count calculation fix #2940 * release notes * fix CI fail - key can now be embstr or raw * CI update to redis 8.2.0 --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/RedisDatabase.cs | 11 +++++----- tests/RedisConfigs/.docker/Redis/Dockerfile | 2 +- tests/StackExchange.Redis.Tests/KeyTests.cs | 4 ++-- .../StackExchange.Redis.Tests/StreamTests.cs | 22 +++++++++++++++++++ 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 3a50f1ef3..12b4f9ded 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -13,6 +13,7 @@ Current package versions: - Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638)) - Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822)) - Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2928)) +- Fix error constructing `StreamAdd` message ([#2941 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2941)) ## 2.8.58 diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index bf69f25f3..349864a1b 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -4598,13 +4598,12 @@ private Message GetStreamAddMessage(RedisKey key, RedisValue entryId, long? maxL throw new ArgumentOutOfRangeException(nameof(maxLength), "maxLength must be greater than 0."); } - var includeMaxLen = maxLength.HasValue ? 2 : 0; - var includeApproxLen = maxLength.HasValue && useApproximateMaxLength ? 1 : 0; - var totalLength = (streamPairs.Length * 2) // Room for the name/value pairs - + 1 // The stream entry ID - + includeMaxLen // 2 or 0 (MAXLEN keyword & the count) - + includeApproxLen; // 1 or 0 + + 1 // The stream entry ID + + (maxLength.HasValue ? 2 : 0) // MAXLEN N + + (maxLength.HasValue && useApproximateMaxLength ? 1 : 0) // ~ + + (mode == StreamTrimMode.KeepReferences ? 0 : 1) // relevant trim-mode keyword + + (limit.HasValue ? 2 : 0); // LIMIT N var values = new RedisValue[totalLength]; diff --git a/tests/RedisConfigs/.docker/Redis/Dockerfile b/tests/RedisConfigs/.docker/Redis/Dockerfile index 4de03f221..424abd1cd 100644 --- a/tests/RedisConfigs/.docker/Redis/Dockerfile +++ b/tests/RedisConfigs/.docker/Redis/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:7.4.2 +FROM redis:8.2.0 COPY --from=configs ./Basic /data/Basic/ COPY --from=configs ./Failover /data/Failover/ diff --git a/tests/StackExchange.Redis.Tests/KeyTests.cs b/tests/StackExchange.Redis.Tests/KeyTests.cs index 31cd87d79..e956af4ff 100644 --- a/tests/StackExchange.Redis.Tests/KeyTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyTests.cs @@ -182,8 +182,8 @@ public async Task KeyEncoding() db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, "new value", flags: CommandFlags.FireAndForget); - Assert.Equal("embstr", db.KeyEncoding(key)); - Assert.Equal("embstr", await db.KeyEncodingAsync(key)); + Assert.True(db.KeyEncoding(key) is "embstr" or "raw"); // server-version dependent + Assert.True(await db.KeyEncodingAsync(key) is "embstr" or "raw"); db.KeyDelete(key, CommandFlags.FireAndForget); db.ListLeftPush(key, "new value", flags: CommandFlags.FireAndForget); diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index 196913f40..58d2bb1fb 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -2155,6 +2155,28 @@ public async Task AddWithApproxCount(StreamTrimMode mode) db.StreamAdd(key, "field", "value", maxLength: 10, useApproximateMaxLength: true, trimMode: mode, flags: CommandFlags.None); } + [Theory] + [InlineData(StreamTrimMode.KeepReferences, 1)] + [InlineData(StreamTrimMode.DeleteReferences, 1)] + [InlineData(StreamTrimMode.Acknowledged, 1)] + [InlineData(StreamTrimMode.KeepReferences, 2)] + [InlineData(StreamTrimMode.DeleteReferences, 2)] + [InlineData(StreamTrimMode.Acknowledged, 2)] + public async Task AddWithMultipleApproxCount(StreamTrimMode mode, int count) + { + await using var conn = Create(require: ForMode(mode)); + + var db = conn.GetDatabase(); + var key = Me() + ":" + mode; + + var pairs = new NameValueEntry[count]; + for (var i = 0; i < count; i++) + { + pairs[i] = new NameValueEntry($"field{i}", $"value{i}"); + } + db.StreamAdd(key, maxLength: 10, useApproximateMaxLength: true, trimMode: mode, flags: CommandFlags.None, streamPairs: pairs); + } + [Fact] public async Task StreamReadGroupWithNoAckShowsNoPendingMessages() { From 4d2da7abdc0cbd166bc5670035c51629e1750454 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 19 Aug 2025 15:59:53 +0100 Subject: [PATCH 363/435] Implement GetServer(RedisKey, ...) (#2936) * - implement GetServer API from #2932, and tests - implement memoization on GetServer when asyncState is null - fixup NotImplementedException in dummy places * release notes and shipped * remove params overload * remove params overload from muxer impl * test stability: allow both embstr or raw for string encoding * remove pragma * Additional intellisense comments to clarify usage. --- docs/ReleaseNotes.md | 1 + .../ConnectionMultiplexer.cs | 20 +- .../Interfaces/IConnectionMultiplexer.cs | 600 +++++++++--------- src/StackExchange.Redis/Interfaces/IServer.cs | 22 +- .../PublicAPI/PublicAPI.Shipped.txt | 6 +- src/StackExchange.Redis/RedisServer.cs | 18 +- src/StackExchange.Redis/ServerEndPoint.cs | 6 + .../GetServerTests.cs | 150 +++++ .../Helpers/SharedConnectionFixture.cs | 2 +- .../StackExchange.Redis.Tests/LoggerTests.cs | 10 +- 10 files changed, 528 insertions(+), 307 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/GetServerTests.cs diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 12b4f9ded..c782d3f5a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -13,6 +13,7 @@ Current package versions: - Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638)) - Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822)) - Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2928)) +- Add `GetServer(RedisKey, ...)` API ([#2936 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2936)) - Fix error constructing `StreamAdd` message ([#2941 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2941)) ## 2.8.58 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index bf1b75e25..bf6b66674 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -210,7 +210,7 @@ internal async Task MakePrimaryAsync(ServerEndPoint server, ReplicationChangeOpt { throw ExceptionFactory.AdminModeNotEnabled(RawConfig.IncludeDetailInExceptions, cmd, null, server); } - var srv = new RedisServer(this, server, null); + var srv = server.GetRedisServer(null); if (!srv.IsConnected) { throw ExceptionFactory.NoConnectionAvailable(this, null, server, GetServerSnapshot(), command: cmd); @@ -1229,7 +1229,21 @@ public IServer GetServer(EndPoint? endpoint, object? asyncState = null) throw new NotSupportedException($"The server API is not available via {RawConfig.Proxy}"); } var server = servers[endpoint] as ServerEndPoint ?? throw new ArgumentException("The specified endpoint is not defined", nameof(endpoint)); - return new RedisServer(this, server, asyncState); + return server.GetRedisServer(asyncState); + } + + /// +#pragma warning disable RS0026 + public IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None) +#pragma warning restore RS0026 + { + // We'll spoof the GET command for this; we're not supporting ad-hoc access to the pub/sub channel, because: bad things. + // Any read-only-replica vs writable-primary concerns should be managed by the caller via "flags"; the default is PreferPrimary. + // Note that ServerSelectionStrategy treats "null" (default) keys as NoSlot, aka Any. + return (SelectServer(RedisCommand.GET, flags, key) ?? Throw()).GetRedisServer(asyncState); + + [DoesNotReturn] + static ServerEndPoint Throw() => throw new InvalidOperationException("It was not possible to resolve a connection to the server owning the specified key"); } /// @@ -1241,7 +1255,7 @@ public IServer[] GetServers() var result = new IServer[snapshot.Length]; for (var i = 0; i < snapshot.Length; i++) { - result[i] = new RedisServer(this, snapshot[i], null); + result[i] = snapshot[i].GetRedisServer(null); } return result; } diff --git a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs index b4bdb0950..96b4ce8f6 100644 --- a/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs @@ -8,299 +8,311 @@ using StackExchange.Redis.Profiling; using static StackExchange.Redis.ConnectionMultiplexer; -namespace StackExchange.Redis +namespace StackExchange.Redis; + +internal interface IInternalConnectionMultiplexer : IConnectionMultiplexer { - internal interface IInternalConnectionMultiplexer : IConnectionMultiplexer - { - bool AllowConnect { get; set; } - - bool IgnoreConnect { get; set; } - - ReadOnlySpan GetServerSnapshot(); - ServerEndPoint GetServerEndPoint(EndPoint endpoint); - - ConfigurationOptions RawConfig { get; } - - long? GetConnectionId(EndPoint endPoint, ConnectionType type); - - ServerSelectionStrategy ServerSelectionStrategy { get; } - - int GetSubscriptionsCount(); - ConcurrentDictionary GetSubscriptions(); - - ConnectionMultiplexer UnderlyingMultiplexer { get; } - } - - /// - /// Represents the abstract multiplexer API. - /// - public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable - { - /// - /// Gets the client-name that will be used on all new connections. - /// - string ClientName { get; } - - /// - /// Gets the configuration of the connection. - /// - string Configuration { get; } - - /// - /// Gets the timeout associated with the connections. - /// - int TimeoutMilliseconds { get; } - - /// - /// The number of operations that have been performed on all connections. - /// - long OperationCount { get; } - - /// - /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order. - /// - [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue), false)] - [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - bool PreserveAsyncOrder { get; set; } - - /// - /// Indicates whether any servers are connected. - /// - bool IsConnected { get; } - - /// - /// Indicates whether any servers are connecting. - /// - bool IsConnecting { get; } - - /// - /// Should exceptions include identifiable details? (key names, additional annotations). - /// - [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")] - [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - bool IncludeDetailInExceptions { get; set; } - - /// - /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time. - /// Set to a negative value to disable this feature). - /// - int StormLogThreshold { get; set; } - - /// - /// Register a callback to provide an on-demand ambient session provider based on the calling context. - /// The implementing code is responsible for reliably resolving the same provider - /// based on ambient context, or returning null to not profile. - /// - /// The profiling session provider. - void RegisterProfiler(Func profilingSessionProvider); - - /// - /// Get summary statistics associates with this server. - /// - ServerCounters GetCounters(); - - /// - /// A server replied with an error message. - /// - event EventHandler ErrorMessage; - - /// - /// Raised whenever a physical connection fails. - /// - event EventHandler ConnectionFailed; - - /// - /// Raised whenever an internal error occurs (this is primarily for debugging). - /// - event EventHandler InternalError; - - /// - /// Raised whenever a physical connection is established. - /// - event EventHandler ConnectionRestored; - - /// - /// Raised when configuration changes are detected. - /// - event EventHandler ConfigurationChanged; - - /// - /// Raised when nodes are explicitly requested to reconfigure via broadcast. - /// This usually means primary/replica changes. - /// - event EventHandler ConfigurationChangedBroadcast; - - /// - /// Raised when server indicates a maintenance event is going to happen. - /// - event EventHandler ServerMaintenanceEvent; - - /// - /// Gets all endpoints defined on the multiplexer. - /// - /// Whether to return only the explicitly configured endpoints. - EndPoint[] GetEndPoints(bool configuredOnly = false); - - /// - /// Wait for a given asynchronous operation to complete (or timeout). - /// - /// The task to wait on. - void Wait(Task task); - - /// - /// Wait for a given asynchronous operation to complete (or timeout). - /// - /// The type in . - /// The task to wait on. - T Wait(Task task); - - /// - /// Wait for the given asynchronous operations to complete (or timeout). - /// - /// The tasks to wait on. - void WaitAll(params Task[] tasks); - - /// - /// Raised when a hash-slot has been relocated. - /// - event EventHandler HashSlotMoved; - - /// - /// Compute the hash-slot of a specified key. - /// - /// The key to get a slot ID for. - int HashSlot(RedisKey key); - - /// - /// Obtain a pub/sub subscriber connection to the specified server. - /// - /// The async state to pass to the created . - ISubscriber GetSubscriber(object? asyncState = null); - - /// - /// Obtain an interactive connection to a database inside redis. - /// - /// The database ID to get. - /// The async state to pass to the created . - IDatabase GetDatabase(int db = -1, object? asyncState = null); - - /// - /// Obtain a configuration API for an individual server. - /// - /// The host to get a server for. - /// The specific port for to get a server for. - /// The async state to pass to the created . - IServer GetServer(string host, int port, object? asyncState = null); - - /// - /// Obtain a configuration API for an individual server. - /// - /// The "host:port" string to get a server for. - /// The async state to pass to the created . - IServer GetServer(string hostAndPort, object? asyncState = null); - - /// - /// Obtain a configuration API for an individual server. - /// - /// The host to get a server for. - /// The specific port for to get a server for. - IServer GetServer(IPAddress host, int port); - - /// - /// Obtain a configuration API for an individual server. - /// - /// The endpoint to get a server for. - /// The async state to pass to the created . - IServer GetServer(EndPoint endpoint, object? asyncState = null); - - /// - /// Obtain configuration APIs for all servers in this multiplexer. - /// - IServer[] GetServers(); - - /// - /// Reconfigure the current connections based on the existing configuration. - /// - /// The log to write output to. - Task ConfigureAsync(TextWriter? log = null); - - /// - /// Reconfigure the current connections based on the existing configuration. - /// - /// The log to write output to. - bool Configure(TextWriter? log = null); - - /// - /// Provides a text overview of the status of all connections. - /// - string GetStatus(); - - /// - /// Provides a text overview of the status of all connections. - /// - /// The log to write output to. - void GetStatus(TextWriter log); - - /// - /// See . - /// - string ToString(); - - /// - /// Close all connections and release all resources associated with this object. - /// - /// Whether to allow in-queue commands to complete first. - void Close(bool allowCommandsToComplete = true); - - /// - /// Close all connections and release all resources associated with this object. - /// - /// Whether to allow in-queue commands to complete first. - Task CloseAsync(bool allowCommandsToComplete = true); - - /// - /// Obtains the log of unusual busy patterns. - /// - string? GetStormLog(); - - /// - /// Resets the log of unusual busy patterns. - /// - void ResetStormLog(); - - /// - /// Request all compatible clients to reconfigure or reconnect. - /// - /// The command flags to use. - /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending). - long PublishReconfigure(CommandFlags flags = CommandFlags.None); - - /// - /// Request all compatible clients to reconfigure or reconnect. - /// - /// The command flags to use. - /// The number of instances known to have received the message (however, the actual number can be higher). - Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None); - - /// - /// Get the hash-slot associated with a given key, if applicable; this can be useful for grouping operations. - /// - /// The key to get a the slot for. - int GetHashSlot(RedisKey key); - - /// - /// Write the configuration of all servers to an output stream. - /// - /// The destination stream to write the export to. - /// The options to use for this export. - void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All); - - /// - /// Append a usage-specific modifier to the advertised library name; suffixes are de-duplicated - /// and sorted alphabetically (so adding 'a', 'b' and 'a' will result in suffix '-a-b'). - /// Connections will be updated as necessary (RESP2 subscription - /// connections will not show updates until those connections next connect). - /// - void AddLibraryNameSuffix(string suffix); - } + bool AllowConnect { get; set; } + + bool IgnoreConnect { get; set; } + + ReadOnlySpan GetServerSnapshot(); + ServerEndPoint GetServerEndPoint(EndPoint endpoint); + + ConfigurationOptions RawConfig { get; } + + long? GetConnectionId(EndPoint endPoint, ConnectionType type); + + ServerSelectionStrategy ServerSelectionStrategy { get; } + + int GetSubscriptionsCount(); + ConcurrentDictionary GetSubscriptions(); + + ConnectionMultiplexer UnderlyingMultiplexer { get; } +} + +/// +/// Represents the abstract multiplexer API. +/// +public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable +{ + /// + /// Gets the client-name that will be used on all new connections. + /// + string ClientName { get; } + + /// + /// Gets the configuration of the connection. + /// + string Configuration { get; } + + /// + /// Gets the timeout associated with the connections. + /// + int TimeoutMilliseconds { get; } + + /// + /// The number of operations that have been performed on all connections. + /// + long OperationCount { get; } + + /// + /// Gets or sets whether asynchronous operations should be invoked in a way that guarantees their original delivery order. + /// + [Obsolete("Not supported; if you require ordered pub/sub, please see " + nameof(ChannelMessageQueue), false)] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + bool PreserveAsyncOrder { get; set; } + + /// + /// Indicates whether any servers are connected. + /// + bool IsConnected { get; } + + /// + /// Indicates whether any servers are connecting. + /// + bool IsConnecting { get; } + + /// + /// Should exceptions include identifiable details? (key names, additional annotations). + /// + [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + bool IncludeDetailInExceptions { get; set; } + + /// + /// Limit at which to start recording unusual busy patterns (only one log will be retained at a time. + /// Set to a negative value to disable this feature). + /// + int StormLogThreshold { get; set; } + + /// + /// Register a callback to provide an on-demand ambient session provider based on the calling context. + /// The implementing code is responsible for reliably resolving the same provider + /// based on ambient context, or returning null to not profile. + /// + /// The profiling session provider. + void RegisterProfiler(Func profilingSessionProvider); + + /// + /// Get summary statistics associates with this server. + /// + ServerCounters GetCounters(); + + /// + /// A server replied with an error message. + /// + event EventHandler ErrorMessage; + + /// + /// Raised whenever a physical connection fails. + /// + event EventHandler ConnectionFailed; + + /// + /// Raised whenever an internal error occurs (this is primarily for debugging). + /// + event EventHandler InternalError; + + /// + /// Raised whenever a physical connection is established. + /// + event EventHandler ConnectionRestored; + + /// + /// Raised when configuration changes are detected. + /// + event EventHandler ConfigurationChanged; + + /// + /// Raised when nodes are explicitly requested to reconfigure via broadcast. + /// This usually means primary/replica changes. + /// + event EventHandler ConfigurationChangedBroadcast; + + /// + /// Raised when server indicates a maintenance event is going to happen. + /// + event EventHandler ServerMaintenanceEvent; + + /// + /// Gets all endpoints defined on the multiplexer. + /// + /// Whether to return only the explicitly configured endpoints. + EndPoint[] GetEndPoints(bool configuredOnly = false); + + /// + /// Wait for a given asynchronous operation to complete (or timeout). + /// + /// The task to wait on. + void Wait(Task task); + + /// + /// Wait for a given asynchronous operation to complete (or timeout). + /// + /// The type in . + /// The task to wait on. + T Wait(Task task); + + /// + /// Wait for the given asynchronous operations to complete (or timeout). + /// + /// The tasks to wait on. + void WaitAll(params Task[] tasks); + + /// + /// Raised when a hash-slot has been relocated. + /// + event EventHandler HashSlotMoved; + + /// + /// Compute the hash-slot of a specified key. + /// + /// The key to get a slot ID for. + int HashSlot(RedisKey key); + + /// + /// Obtain a pub/sub subscriber connection to the specified server. + /// + /// The async state to pass to the created . + ISubscriber GetSubscriber(object? asyncState = null); + + /// + /// Obtain an interactive connection to a database inside redis. + /// + /// The database ID to get. + /// The async state to pass to the created . + IDatabase GetDatabase(int db = -1, object? asyncState = null); + + /// + /// Obtain a configuration API for an individual server. + /// + /// The host to get a server for. + /// The specific port for to get a server for. + /// The async state to pass to the created . + IServer GetServer(string host, int port, object? asyncState = null); + + /// + /// Obtain a configuration API for an individual server. + /// + /// The "host:port" string to get a server for. + /// The async state to pass to the created . + IServer GetServer(string hostAndPort, object? asyncState = null); + + /// + /// Obtain a configuration API for an individual server. + /// + /// The host to get a server for. + /// The specific port for to get a server for. + IServer GetServer(IPAddress host, int port); + + /// + /// Obtain a configuration API for an individual server. + /// + /// The endpoint to get a server for. + /// The async state to pass to the created . + IServer GetServer(EndPoint endpoint, object? asyncState = null); + + /// + /// Gets a server that would be used for a given key and flags. + /// + /// The endpoint to get a server for. In a non-cluster environment, this parameter is ignored. A key may be specified + /// on cluster, which will return a connection to an arbitrary server matching the specified flags. + /// The async state to pass to the created . + /// The command flags to use. + /// This method is particularly useful when communicating with a cluster environment, to obtain a connection to the server that owns the specified key + /// and ad-hoc commands with unusual routing requirements. Note that provides a connection that automatically routes commands by + /// looking for parameters, so this method is only necessary when used with commands that do not take a parameter, + /// but require consistent routing using key-like semantics. + IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None); + + /// + /// Obtain configuration APIs for all servers in this multiplexer. + /// + IServer[] GetServers(); + + /// + /// Reconfigure the current connections based on the existing configuration. + /// + /// The log to write output to. + Task ConfigureAsync(TextWriter? log = null); + + /// + /// Reconfigure the current connections based on the existing configuration. + /// + /// The log to write output to. + bool Configure(TextWriter? log = null); + + /// + /// Provides a text overview of the status of all connections. + /// + string GetStatus(); + + /// + /// Provides a text overview of the status of all connections. + /// + /// The log to write output to. + void GetStatus(TextWriter log); + + /// + /// See . + /// + string ToString(); + + /// + /// Close all connections and release all resources associated with this object. + /// + /// Whether to allow in-queue commands to complete first. + void Close(bool allowCommandsToComplete = true); + + /// + /// Close all connections and release all resources associated with this object. + /// + /// Whether to allow in-queue commands to complete first. + Task CloseAsync(bool allowCommandsToComplete = true); + + /// + /// Obtains the log of unusual busy patterns. + /// + string? GetStormLog(); + + /// + /// Resets the log of unusual busy patterns. + /// + void ResetStormLog(); + + /// + /// Request all compatible clients to reconfigure or reconnect. + /// + /// The command flags to use. + /// The number of instances known to have received the message (however, the actual number can be higher; returns -1 if the operation is pending). + long PublishReconfigure(CommandFlags flags = CommandFlags.None); + + /// + /// Request all compatible clients to reconfigure or reconnect. + /// + /// The command flags to use. + /// The number of instances known to have received the message (however, the actual number can be higher). + Task PublishReconfigureAsync(CommandFlags flags = CommandFlags.None); + + /// + /// Get the hash-slot associated with a given key, if applicable; this can be useful for grouping operations. + /// + /// The key to get a the slot for. + int GetHashSlot(RedisKey key); + + /// + /// Write the configuration of all servers to an output stream. + /// + /// The destination stream to write the export to. + /// The options to use for this export. + void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All); + + /// + /// Append a usage-specific modifier to the advertised library name; suffixes are de-duplicated + /// and sorted alphabetically (so adding 'a', 'b' and 'a' will result in suffix '-a-b'). + /// Connections will be updated as necessary (RESP2 subscription + /// connections will not show updates until those connections next connect). + /// + void AddLibraryNameSuffix(string suffix); } diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 4971c7f18..8e4178fc9 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -266,10 +266,13 @@ public partial interface IServer : IRedis /// Task ExecuteAsync(string command, params object[] args); +#pragma warning disable RS0026, RS0027 // multiple overloads /// /// Execute an arbitrary command against the server; this is primarily intended for /// executing modules, but may also be used to provide access to new features that lack - /// a direct API. + /// a direct API. The command is assumed to be not database-specific. If this is not the case, + /// should be used to + /// specify the database (using null to use the configured default database). /// /// The command to run. /// The arguments to pass for the command. @@ -280,6 +283,23 @@ public partial interface IServer : IRedis /// Task ExecuteAsync(string command, ICollection args, CommandFlags flags = CommandFlags.None); +#pragma warning restore RS0026, RS0027 + + /// + /// Execute an arbitrary database-specific command against the server; this is primarily intended for + /// executing modules, but may also be used to provide access to new features that lack + /// a direct API. + /// + /// The database ID; if , the configured default database is used. + /// The command to run. + /// The arguments to pass for the command. + /// The flags to use for this operation. + /// A dynamic representation of the command's result. + /// This API should be considered an advanced feature; inappropriate use can be harmful. + RedisResult Execute(int? database, string command, ICollection args, CommandFlags flags = CommandFlags.None); + + /// + Task ExecuteAsync(int? database, string command, ICollection args, CommandFlags flags = CommandFlags.None); /// /// Delete all the keys of all databases on the server. diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 66c49976e..e82af2bee 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1957,4 +1957,8 @@ StackExchange.Redis.RedisValue.CopyTo(System.Span destination) -> int StackExchange.Redis.RedisValue.GetByteCount() -> int StackExchange.Redis.RedisValue.GetLongByteCount() -> long static StackExchange.Redis.Condition.SortedSetContainsStarting(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue prefix) -> StackExchange.Redis.Condition! -static StackExchange.Redis.Condition.SortedSetNotContainsStarting(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue prefix) -> StackExchange.Redis.Condition! \ No newline at end of file +static StackExchange.Redis.Condition.SortedSetNotContainsStarting(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue prefix) -> StackExchange.Redis.Condition! +StackExchange.Redis.ConnectionMultiplexer.GetServer(StackExchange.Redis.RedisKey key, object? asyncState = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.IServer! +StackExchange.Redis.IConnectionMultiplexer.GetServer(StackExchange.Redis.RedisKey key, object? asyncState = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.IServer! +StackExchange.Redis.IServer.Execute(int? database, string! command, System.Collections.Generic.ICollection! args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! +StackExchange.Redis.IServer.ExecuteAsync(int? database, string! command, System.Collections.Generic.ICollection! args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index af734b0f5..3bc306c69 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -16,9 +16,9 @@ internal sealed class RedisServer : RedisBase, IServer { private readonly ServerEndPoint server; - internal RedisServer(ConnectionMultiplexer multiplexer, ServerEndPoint server, object? asyncState) : base(multiplexer, asyncState) + internal RedisServer(ServerEndPoint server, object? asyncState) : base(server.Multiplexer, asyncState) { - this.server = server ?? throw new ArgumentNullException(nameof(server)); + this.server = server; // definitely can't be null because .Multiplexer in base call } int IServer.DatabaseCount => server.Databases; @@ -1045,6 +1045,20 @@ public Task ExecuteAsync(string command, ICollection args, return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); } + public RedisResult Execute(int? database, string command, ICollection args, CommandFlags flags = CommandFlags.None) + { + var db = multiplexer.ApplyDefaultDatabase(database ?? -1); + var msg = new RedisDatabase.ExecuteMessage(multiplexer?.CommandMap, db, flags, command, args); + return ExecuteSync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + + public Task ExecuteAsync(int? database, string command, ICollection args, CommandFlags flags = CommandFlags.None) + { + var db = multiplexer.ApplyDefaultDatabase(database ?? -1); + var msg = new RedisDatabase.ExecuteMessage(multiplexer?.CommandMap, db, flags, command, args); + return ExecuteAsync(msg, ResultProcessor.ScriptResult, defaultValue: RedisResult.NullSingle); + } + /// /// For testing only. /// diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index e62dc9f43..af98af0f7 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -71,6 +71,12 @@ public ServerEndPoint(ConnectionMultiplexer multiplexer, EndPoint endpoint) } } + private RedisServer? _defaultServer; + public RedisServer GetRedisServer(object? asyncState) + => asyncState is null + ? (_defaultServer ??= new RedisServer(this, null)) // reuse and memoize + : new RedisServer(this, asyncState); + public EndPoint EndPoint { get; } public ClusterConfiguration? ClusterConfiguration { get; private set; } diff --git a/tests/StackExchange.Redis.Tests/GetServerTests.cs b/tests/StackExchange.Redis.Tests/GetServerTests.cs new file mode 100644 index 000000000..50cb9e7ef --- /dev/null +++ b/tests/StackExchange.Redis.Tests/GetServerTests.cs @@ -0,0 +1,150 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace StackExchange.Redis.Tests; + +public abstract class GetServerTestsBase(ITestOutputHelper output, SharedConnectionFixture fixture) + : TestBase(output, fixture) +{ + protected abstract bool IsCluster { get; } + + [Fact] + public async Task GetServersMemoization() + { + await using var conn = Create(); + + var servers0 = conn.GetServers(); + var servers1 = conn.GetServers(); + + // different array, exact same contents + Assert.NotSame(servers0, servers1); + Assert.NotEmpty(servers0); + Assert.NotNull(servers0); + Assert.NotNull(servers1); + Assert.Equal(servers0.Length, servers1.Length); + for (int i = 0; i < servers0.Length; i++) + { + Assert.Same(servers0[i], servers1[i]); + } + } + + [Fact] + public async Task GetServerByEndpointMemoization() + { + await using var conn = Create(); + var ep = conn.GetEndPoints().First(); + + IServer x = conn.GetServer(ep), y = conn.GetServer(ep); + Assert.Same(x, y); + + object asyncState = "whatever"; + x = conn.GetServer(ep, asyncState); + y = conn.GetServer(ep, asyncState); + Assert.NotSame(x, y); + } + + [Fact] + public async Task GetServerByKeyMemoization() + { + await using var conn = Create(); + RedisKey key = Me(); + string value = $"{key}:value"; + await conn.GetDatabase().StringSetAsync(key, value); + + IServer x = conn.GetServer(key), y = conn.GetServer(key); + Assert.False(y.IsReplica, "IsReplica"); + Assert.Same(x, y); + + y = conn.GetServer(key, flags: CommandFlags.DemandMaster); + Assert.Same(x, y); + + // async state demands separate instance + y = conn.GetServer(key, "async state", flags: CommandFlags.DemandMaster); + Assert.NotSame(x, y); + + // primary and replica should be different + y = conn.GetServer(key, flags: CommandFlags.DemandReplica); + Assert.NotSame(x, y); + Assert.True(y.IsReplica, "IsReplica"); + + // replica again: same + var z = conn.GetServer(key, flags: CommandFlags.DemandReplica); + Assert.Same(y, z); + + // check routed correctly + var actual = (string?)await x.ExecuteAsync(null, "get", [key], CommandFlags.NoRedirect); + Assert.Equal(value, actual); // check value against primary + + // for replica, don't check the value, because of replication delay - just: no error + _ = y.ExecuteAsync(null, "get", [key], CommandFlags.NoRedirect); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GetServerWithDefaultKey(bool explicitNull) + { + await using var conn = Create(); + bool isCluster = conn.ServerSelectionStrategy.ServerType == ServerType.Cluster; + Assert.Equal(IsCluster, isCluster); // check our assumptions! + + // we expect explicit null and default to act the same, but: check + RedisKey key = explicitNull ? RedisKey.Null : default(RedisKey); + + IServer primary = conn.GetServer(key); + Assert.False(primary.IsReplica); + + IServer replica = conn.GetServer(key, flags: CommandFlags.DemandReplica); + Assert.True(replica.IsReplica); + + // check multiple calls + HashSet uniques = []; + for (int i = 0; i < 100; i++) + { + uniques.Add(conn.GetServer(key)); + } + + if (isCluster) + { + Assert.True(uniques.Count > 1); // should be able to get arbitrary servers + } + else + { + Assert.Single(uniques); + } + + uniques.Clear(); + for (int i = 0; i < 100; i++) + { + uniques.Add(conn.GetServer(key, flags: CommandFlags.DemandReplica)); + } + + if (isCluster) + { + Assert.True(uniques.Count > 1); // should be able to get arbitrary servers + } + else + { + Assert.Single(uniques); + } + } +} + +[RunPerProtocol] +public class GetServerTestsCluster(ITestOutputHelper output, SharedConnectionFixture fixture) : GetServerTestsBase(output, fixture) +{ + protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts; + + protected override bool IsCluster => true; +} + +[RunPerProtocol] +public class GetServerTestsStandalone(ITestOutputHelper output, SharedConnectionFixture fixture) : GetServerTestsBase(output, fixture) +{ + protected override string GetConfiguration() => // we want to test flags usage including replicas + TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort; + + protected override bool IsCluster => false; +} diff --git a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs index cf6c7d326..9656ee45b 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/SharedConnectionFixture.cs @@ -197,7 +197,7 @@ public event EventHandler ServerMaintenanceEvent public IServer GetServer(IPAddress host, int port) => _inner.GetServer(host, port); public IServer GetServer(EndPoint endpoint, object? asyncState = null) => _inner.GetServer(endpoint, asyncState); - + public IServer GetServer(RedisKey key, object? asyncState = null, CommandFlags flags = CommandFlags.None) => _inner.GetServer(key, asyncState, flags); public IServer[] GetServers() => _inner.GetServers(); public string GetStatus() => _inner.GetStatus(); diff --git a/tests/StackExchange.Redis.Tests/LoggerTests.cs b/tests/StackExchange.Redis.Tests/LoggerTests.cs index bc097d24c..e001250b0 100644 --- a/tests/StackExchange.Redis.Tests/LoggerTests.cs +++ b/tests/StackExchange.Redis.Tests/LoggerTests.cs @@ -52,7 +52,7 @@ public class TestWrapperLoggerFactory(ILogger logger) : ILoggerFactory { public TestWrapperLogger Logger { get; } = new TestWrapperLogger(logger); - public void AddProvider(ILoggerProvider provider) => throw new NotImplementedException(); + public void AddProvider(ILoggerProvider provider) { } public ILogger CreateLogger(string categoryName) => Logger; public void Dispose() { } } @@ -81,9 +81,9 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except private class TestMultiLogger(params ILogger[] loggers) : ILogger { #if NET8_0_OR_GREATER - public IDisposable? BeginScope(TState state) where TState : notnull => throw new NotImplementedException(); + public IDisposable? BeginScope(TState state) where TState : notnull => null; #else - public IDisposable BeginScope(TState state) => throw new NotImplementedException(); + public IDisposable BeginScope(TState state) => null!; #endif public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) @@ -105,9 +105,9 @@ public TestLogger(LogLevel logLevel, TextWriter output) => (_logLevel, _output) = (logLevel, output); #if NET8_0_OR_GREATER - public IDisposable? BeginScope(TState state) where TState : notnull => throw new NotImplementedException(); + public IDisposable? BeginScope(TState state) where TState : notnull => null; #else - public IDisposable BeginScope(TState state) => throw new NotImplementedException(); + public IDisposable BeginScope(TState state) => null!; #endif public bool IsEnabled(LogLevel logLevel) => logLevel >= _logLevel; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) From d48af3edcad1286b1997015881e00d771ba6be1b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 20 Aug 2025 10:31:42 +0100 Subject: [PATCH 364/435] Update ReleaseNotes.md release notes --- docs/ReleaseNotes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index c782d3f5a..4621190d2 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,7 +6,9 @@ Current package versions: | ------------ | ----------------- | ----- | | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | -## Unreleased (2.9.xxx) +## Unreleased + +## 2.9.11 - Add `HGETDEL`, `HGETEX` and `HSETEX` support ([#2863 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2863)) - Fix key-prefix omission in `SetIntersectionLength` and `SortedSet{Combine[WithScores]|IntersectionLength}` ([#2863 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2863)) From a1e485304192543e5a4ce260cf7dfaa4529d0d49 Mon Sep 17 00:00:00 2001 From: Henrik Date: Tue, 9 Sep 2025 16:48:54 +0200 Subject: [PATCH 365/435] Enable CA1852 and fix warnings for it (#2942) --- .editorconfig | 3 +++ tests/BasicTest/Program.cs | 2 +- tests/StackExchange.Redis.Benchmarks/SlowConfig.cs | 2 +- tests/StackExchange.Redis.Tests/ConfigTests.cs | 2 +- tests/StackExchange.Redis.Tests/LoggerTests.cs | 4 ++-- tests/StackExchange.Redis.Tests/ParseTests.cs | 2 +- tests/StackExchange.Redis.Tests/ProfilingTests.cs | 4 ++-- 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.editorconfig b/.editorconfig index a00642936..eb05866a0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -138,6 +138,9 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true +# Help Link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852 +# Tags : Telemetry, EnabledRuleInAggressiveMode, CompilationEnd +dotnet_diagnostic.CA1852.severity = warning # IDE preferences dotnet_diagnostic.IDE0090.severity = silent # IDE0090: Use 'new(...)' diff --git a/tests/BasicTest/Program.cs b/tests/BasicTest/Program.cs index faff1b7d7..2977c42c2 100644 --- a/tests/BasicTest/Program.cs +++ b/tests/BasicTest/Program.cs @@ -34,7 +34,7 @@ public CustomConfig() AddJob(Configure(Job.Default.WithRuntime(CoreRuntime.Core50))); } } - internal class SlowConfig : CustomConfig + internal sealed class SlowConfig : CustomConfig { protected override Job Configure(Job j) => j.WithLaunchCount(1) diff --git a/tests/StackExchange.Redis.Benchmarks/SlowConfig.cs b/tests/StackExchange.Redis.Benchmarks/SlowConfig.cs index 0c3546006..fc1ab6f71 100644 --- a/tests/StackExchange.Redis.Benchmarks/SlowConfig.cs +++ b/tests/StackExchange.Redis.Benchmarks/SlowConfig.cs @@ -2,7 +2,7 @@ namespace StackExchange.Redis.Benchmarks { - internal class SlowConfig : CustomConfig + internal sealed class SlowConfig : CustomConfig { protected override Job Configure(Job j) => j.WithLaunchCount(1) diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 995b66a5a..801565a83 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -713,7 +713,7 @@ public void HttpTunnelCanRoundtrip(string input, string expected) Assert.Equal($"127.0.0.1:6380,tunnel={expected}", cs); } - private class CustomTunnel : Tunnel { } + private sealed class CustomTunnel : Tunnel { } [Fact] public void CustomTunnelCanRoundtripMinusTunnel() diff --git a/tests/StackExchange.Redis.Tests/LoggerTests.cs b/tests/StackExchange.Redis.Tests/LoggerTests.cs index e001250b0..682856baa 100644 --- a/tests/StackExchange.Redis.Tests/LoggerTests.cs +++ b/tests/StackExchange.Redis.Tests/LoggerTests.cs @@ -78,7 +78,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except /// /// To save on test time, no reason to spin up n connections just to test n logging implementations... /// - private class TestMultiLogger(params ILogger[] loggers) : ILogger + private sealed class TestMultiLogger(params ILogger[] loggers) : ILogger { #if NET8_0_OR_GREATER public IDisposable? BeginScope(TState state) where TState : notnull => null; @@ -95,7 +95,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except } } - private class TestLogger : ILogger + private sealed class TestLogger : ILogger { private readonly StringBuilder sb = new StringBuilder(); private long _callCount; diff --git a/tests/StackExchange.Redis.Tests/ParseTests.cs b/tests/StackExchange.Redis.Tests/ParseTests.cs index 51c17fbf3..2621ddab9 100644 --- a/tests/StackExchange.Redis.Tests/ParseTests.cs +++ b/tests/StackExchange.Redis.Tests/ParseTests.cs @@ -79,7 +79,7 @@ private void ProcessMessages(Arena arena, ReadOnlySequence buff Assert.Equal(expected, found); } - private class FragmentedSegment : ReadOnlySequenceSegment + private sealed class FragmentedSegment : ReadOnlySequenceSegment { public FragmentedSegment(long runningIndex, ReadOnlyMemory memory) { diff --git a/tests/StackExchange.Redis.Tests/ProfilingTests.cs b/tests/StackExchange.Redis.Tests/ProfilingTests.cs index f374788db..366abd395 100644 --- a/tests/StackExchange.Redis.Tests/ProfilingTests.cs +++ b/tests/StackExchange.Redis.Tests/ProfilingTests.cs @@ -204,14 +204,14 @@ public async Task ManyContexts() } } - internal class PerThreadProfiler + internal sealed class PerThreadProfiler { private readonly ThreadLocal perThreadSession = new ThreadLocal(() => new ProfilingSession()); public ProfilingSession GetSession() => perThreadSession.Value!; } - internal class AsyncLocalProfiler + internal sealed class AsyncLocalProfiler { private readonly AsyncLocal perThreadSession = new AsyncLocal(); From 6fdbc88ee554ee3eb61fd3e6e40e3505dfda1aef Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 10 Sep 2025 10:26:10 +0100 Subject: [PATCH 366/435] fix: RedisValue/RedisResult: cast to double should respect special values (#2950) * RedisValue/RedisResult: cast to double should respect NaN/[+/-]Inf from raw/string * release notes --- docs/ReleaseNotes.md | 2 + src/StackExchange.Redis/RedisValue.cs | 4 ++ .../RedisResultTests.cs | 44 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 4621190d2..c0d061044 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,8 @@ Current package versions: ## Unreleased +- Fix `RedisValue` special-value (NaN, Inf, etc) handling when casting from raw/string values to `double` ([#2950 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2950)) + ## 2.9.11 - Add `HGETDEL`, `HGETEX` and `HSETEX` support ([#2863 by atakavci](https://github.com/StackExchange/StackExchange.Redis/pull/2863)) diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index f198f3d08..08d65519c 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -656,6 +656,10 @@ public static explicit operator double(RedisValue value) StorageType.Int64 => value.OverlappedValueInt64, StorageType.UInt64 => value.OverlappedValueUInt64, StorageType.Double => value.OverlappedValueDouble, + // special values like NaN/Inf are deliberately not handled by Simplify, but need to be considered for casting + StorageType.String when Format.TryParseDouble((string)value._objectOrSentinel!, out var d) => d, + StorageType.Raw when TryParseDouble(value._memory.Span, out var d) => d, + // anything else: fail _ => throw new InvalidCastException($"Unable to cast from {value.Type} to double: '{value}'"), }; } diff --git a/tests/StackExchange.Redis.Tests/RedisResultTests.cs b/tests/StackExchange.Redis.Tests/RedisResultTests.cs index 1f58ceecf..e63e00dc8 100644 --- a/tests/StackExchange.Redis.Tests/RedisResultTests.cs +++ b/tests/StackExchange.Redis.Tests/RedisResultTests.cs @@ -150,4 +150,48 @@ public void SingleResultConvertibleDirectViaChangeType_TypeCode() Assert.StrictEqual(123f, Convert.ChangeType(value, TypeCode.Single)); Assert.StrictEqual(123d, Convert.ChangeType(value, TypeCode.Double)); } + + [Theory] + [InlineData(ResultType.Double)] + [InlineData(ResultType.BulkString)] + [InlineData(ResultType.SimpleString)] + public void RedisResultParseNaN(ResultType resultType) + { + // https://github.com/redis/NRedisStack/issues/439 + var value = RedisResult.Create("NaN", resultType); + Assert.True(double.IsNaN(value.AsDouble())); + } + + [Theory] + [InlineData(ResultType.Double)] + [InlineData(ResultType.BulkString)] + [InlineData(ResultType.SimpleString)] + public void RedisResultParseInf(ResultType resultType) + { + // https://github.com/redis/NRedisStack/issues/439 + var value = RedisResult.Create("inf", resultType); + Assert.True(double.IsPositiveInfinity(value.AsDouble())); + } + + [Theory] + [InlineData(ResultType.Double)] + [InlineData(ResultType.BulkString)] + [InlineData(ResultType.SimpleString)] + public void RedisResultParsePlusInf(ResultType resultType) + { + // https://github.com/redis/NRedisStack/issues/439 + var value = RedisResult.Create("+inf", resultType); + Assert.True(double.IsPositiveInfinity(value.AsDouble())); + } + + [Theory] + [InlineData(ResultType.Double)] + [InlineData(ResultType.BulkString)] + [InlineData(ResultType.SimpleString)] + public void RedisResultParseMinusInf(ResultType resultType) + { + // https://github.com/redis/NRedisStack/issues/439 + var value = RedisResult.Create("-inf", resultType); + Assert.True(double.IsNegativeInfinity(value.AsDouble())); + } } From d5b1d50b895a84c5f3336cc77f7c420d463b47b2 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 10 Sep 2025 11:07:21 +0100 Subject: [PATCH 367/435] Add vector-set API (#2939) Adds support for [Redis vector sets](https://redis.io/docs/latest/develop/data-types/vector-sets/) --- Directory.Build.props | 2 +- Directory.Packages.props | 5 + StackExchange.Redis.sln | 9 + StackExchange.Redis.sln.DotSettings | 3 +- docs/ReleaseNotes.md | 1 + docs/exp/SER001.md | 22 + .../FastHashGenerator.cs | 215 ++++++ .../FastHashGenerator.md | 64 ++ .../StackExchange.Redis.Build.csproj | 20 + src/Directory.Build.props | 3 + src/StackExchange.Redis/Enums/RedisCommand.cs | 25 + src/StackExchange.Redis/Experiments.cs | 41 ++ src/StackExchange.Redis/FastHash.cs | 137 ++++ .../Interfaces/IDatabase.VectorSets.cs | 183 +++++ .../Interfaces/IDatabase.cs | 3 +- .../Interfaces/IDatabaseAsync.VectorSets.cs | 95 +++ .../Interfaces/IDatabaseAsync.cs | 3 +- .../KeyPrefixed.VectorSets.cs | 59 ++ .../KeyspaceIsolation/KeyPrefixed.cs | 2 +- .../KeyPrefixedDatabase.VectorSets.cs | 56 ++ .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 2 +- src/StackExchange.Redis/Message.cs | 1 + src/StackExchange.Redis/PhysicalConnection.cs | 8 + .../PublicAPI/PublicAPI.Shipped.txt | 89 +++ src/StackExchange.Redis/RawResult.cs | 8 +- .../RedisDatabase.VectorSets.cs | 191 +++++ .../ResultProcessor.Lease.cs | 218 ++++++ .../ResultProcessor.VectorSets.cs | 138 ++++ src/StackExchange.Redis/ResultProcessor.cs | 87 +-- .../StackExchange.Redis.csproj | 4 + .../VectorSetAddMessage.cs | 168 +++++ .../VectorSetAddRequest.cs | 80 +++ src/StackExchange.Redis/VectorSetInfo.cs | 54 ++ src/StackExchange.Redis/VectorSetLink.cs | 24 + .../VectorSetQuantization.cs | 30 + .../VectorSetSimilaritySearchMessage.cs | 263 +++++++ .../VectorSetSimilaritySearchRequest.cs | 219 ++++++ .../VectorSetSimilaritySearchResult.cs | 44 ++ .../FastHashBenchmarks.cs | 139 ++++ .../StackExchange.Redis.Benchmarks/Program.cs | 19 +- .../StackExchange.Redis.Benchmarks.csproj | 1 + .../StackExchange.Redis.Tests/ConfigTests.cs | 10 + .../FastHashTests.cs | 112 +++ .../KeyPrefixedVectorSetTests.cs | 214 ++++++ .../StackExchange.Redis.Tests/NamingTests.cs | 3 +- .../StackExchange.Redis.Tests.csproj | 1 + tests/StackExchange.Redis.Tests/TestBase.cs | 14 +- .../VectorSetIntegrationTests.cs | 675 ++++++++++++++++++ 48 files changed, 3699 insertions(+), 65 deletions(-) create mode 100644 docs/exp/SER001.md create mode 100644 eng/StackExchange.Redis.Build/FastHashGenerator.cs create mode 100644 eng/StackExchange.Redis.Build/FastHashGenerator.md create mode 100644 eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj create mode 100644 src/StackExchange.Redis/Experiments.cs create mode 100644 src/StackExchange.Redis/FastHash.cs create mode 100644 src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs create mode 100644 src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs create mode 100644 src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs create mode 100644 src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs create mode 100644 src/StackExchange.Redis/RedisDatabase.VectorSets.cs create mode 100644 src/StackExchange.Redis/ResultProcessor.Lease.cs create mode 100644 src/StackExchange.Redis/ResultProcessor.VectorSets.cs create mode 100644 src/StackExchange.Redis/VectorSetAddMessage.cs create mode 100644 src/StackExchange.Redis/VectorSetAddRequest.cs create mode 100644 src/StackExchange.Redis/VectorSetInfo.cs create mode 100644 src/StackExchange.Redis/VectorSetLink.cs create mode 100644 src/StackExchange.Redis/VectorSetQuantization.cs create mode 100644 src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs create mode 100644 src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs create mode 100644 src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs create mode 100644 tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs create mode 100644 tests/StackExchange.Redis.Tests/FastHashTests.cs create mode 100644 tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs create mode 100644 tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs diff --git a/Directory.Build.props b/Directory.Build.props index 9f512d5e9..42de5875c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,7 +10,7 @@ true $(MSBuildThisFileDirectory)Shared.ruleset NETSDK1069 - NU5105;NU1507 + $(NoWarn);NU5105;NU1507;SER001 https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes https://stackexchange.github.io/StackExchange.Redis/ MIT diff --git a/Directory.Packages.props b/Directory.Packages.props index 79c404dc2..df8c078a3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,6 +8,10 @@ + + + + @@ -23,6 +27,7 @@ + diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index 8f772ae42..2ed4ebfb3 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -122,6 +122,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs", "docs\docs.csproj", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Benchmarks", "tests\StackExchange.Redis.Benchmarks\StackExchange.Redis.Benchmarks.csproj", "{59889284-FFEE-82E7-94CB-3B43E87DA6CF}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{5FA0958E-6EBD-45F4-808E-3447A293F96F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Build", "eng\StackExchange.Redis.Build\StackExchange.Redis.Build.csproj", "{190742E1-FA50-4E36-A8C4-88AE87654340}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -180,6 +184,10 @@ Global {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Debug|Any CPU.Build.0 = Debug|Any CPU {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Release|Any CPU.ActiveCfg = Release|Any CPU {59889284-FFEE-82E7-94CB-3B43E87DA6CF}.Release|Any CPU.Build.0 = Release|Any CPU + {190742E1-FA50-4E36-A8C4-88AE87654340}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {190742E1-FA50-4E36-A8C4-88AE87654340}.Debug|Any CPU.Build.0 = Debug|Any CPU + {190742E1-FA50-4E36-A8C4-88AE87654340}.Release|Any CPU.ActiveCfg = Release|Any CPU + {190742E1-FA50-4E36-A8C4-88AE87654340}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -202,6 +210,7 @@ Global {A0F89B8B-32A3-4C28-8F1B-ADE343F16137} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} {69A0ACF2-DF1F-4F49-B554-F732DCA938A3} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} {59889284-FFEE-82E7-94CB-3B43E87DA6CF} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} + {190742E1-FA50-4E36-A8C4-88AE87654340} = {5FA0958E-6EBD-45F4-808E-3447A293F96F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {193AA352-6748-47C1-A5FC-C9AA6B5F000B} diff --git a/StackExchange.Redis.sln.DotSettings b/StackExchange.Redis.sln.DotSettings index de893e54d..b72a49d2c 100644 --- a/StackExchange.Redis.sln.DotSettings +++ b/StackExchange.Redis.sln.DotSettings @@ -1,4 +1,5 @@  OK PONG - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index c0d061044..8dc65c544 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -17,6 +17,7 @@ Current package versions: - Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638)) - Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822)) - Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2928)) +- Add vector-set support ([#2939 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2939)) - Add `GetServer(RedisKey, ...)` API ([#2936 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2936)) - Fix error constructing `StreamAdd` message ([#2941 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2941)) diff --git a/docs/exp/SER001.md b/docs/exp/SER001.md new file mode 100644 index 000000000..2def8be6e --- /dev/null +++ b/docs/exp/SER001.md @@ -0,0 +1,22 @@ +At the current time, [Redis documents that](https://redis.io/docs/latest/commands/vadd/): + +> Vector set is a new data type that is currently in preview and may be subject to change. + +As such, the corresponding library feature must also be considered subject to change: + +1. Existing bindings may cease working correctly if the underlying server API changes. +2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time + or run-time breaks. + +While this seems *unlikely*, it must be considered a possibility. If you acknowledge this, you can suppress +this warning by adding the following to your `csproj` file: + +```xml +$(NoWarn);SER001 +``` + +or more granularly / locally in C#: + +``` c# +#pragma warning disable SER001 +``` \ No newline at end of file diff --git a/eng/StackExchange.Redis.Build/FastHashGenerator.cs b/eng/StackExchange.Redis.Build/FastHashGenerator.cs new file mode 100644 index 000000000..cdbc94ebe --- /dev/null +++ b/eng/StackExchange.Redis.Build/FastHashGenerator.cs @@ -0,0 +1,215 @@ +using System.Buffers; +using System.Collections.Immutable; +using System.Reflection; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace StackExchange.Redis.Build; + +[Generator(LanguageNames.CSharp)] +public class FastHashGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var literals = context.SyntaxProvider + .CreateSyntaxProvider(Predicate, Transform) + .Where(pair => pair.Name is { Length: > 0 }) + .Collect(); + + context.RegisterSourceOutput(literals, Generate); + } + + private bool Predicate(SyntaxNode node, CancellationToken cancellationToken) + { + // looking for [FastHash] partial static class Foo { } + if (node is ClassDeclarationSyntax decl + && decl.Modifiers.Any(SyntaxKind.StaticKeyword) + && decl.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + foreach (var attribList in decl.AttributeLists) + { + foreach (var attrib in attribList.Attributes) + { + if (attrib.Name.ToString() is "FastHashAttribute" or "FastHash") return true; + } + } + } + + return false; + } + + private static string GetName(INamedTypeSymbol type) + { + if (type.ContainingType is null) return type.Name; + var stack = new Stack(); + while (true) + { + stack.Push(type.Name); + if (type.ContainingType is null) break; + type = type.ContainingType; + } + var sb = new StringBuilder(stack.Pop()); + while (stack.Count != 0) + { + sb.Append('.').Append(stack.Pop()); + } + return sb.ToString(); + } + + private (string Namespace, string ParentType, string Name, string Value) Transform( + GeneratorSyntaxContext ctx, + CancellationToken cancellationToken) + { + // extract the name and value (defaults to name, but can be overridden via attribute) and the location + if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not INamedTypeSymbol named) return default; + string ns = "", parentType = ""; + if (named.ContainingType is { } containingType) + { + parentType = GetName(containingType); + ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + } + else if (named.ContainingNamespace is { } containingNamespace) + { + ns = containingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + } + + string name = named.Name, value = ""; + foreach (var attrib in named.GetAttributes()) + { + if (attrib.AttributeClass?.Name == "FastHashAttribute") + { + if (attrib.ConstructorArguments.Length == 1) + { + if (attrib.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val) + { + value = val; + break; + } + } + } + } + + if (string.IsNullOrWhiteSpace(value)) + { + value = name.Replace("_", "-"); // if nothing explicit: infer from name + } + + return (ns, parentType, name, value); + } + + private string GetVersion() + { + var asm = GetType().Assembly; + if (asm.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false).FirstOrDefault() is + AssemblyFileVersionAttribute { Version: { Length: > 0 } } version) + { + return version.Version; + } + + return asm.GetName().Version?.ToString() ?? "??"; + } + + private void Generate( + SourceProductionContext ctx, + ImmutableArray<(string Namespace, string ParentType, string Name, string Value)> literals) + { + if (literals.IsDefaultOrEmpty) return; + + var sb = new StringBuilder("// ") + .AppendLine().Append("// ").Append(GetType().Name).Append(" v").Append(GetVersion()).AppendLine(); + + // lease a buffer that is big enough for the longest string + var buffer = ArrayPool.Shared.Rent( + Encoding.UTF8.GetMaxByteCount(literals.Max(l => l.Value.Length))); + int indent = 0; + + StringBuilder NewLine() => sb.AppendLine().Append(' ', indent * 4); + NewLine().Append("using System;"); + NewLine().Append("using StackExchange.Redis;"); + NewLine().Append("#pragma warning disable CS8981"); + foreach (var grp in literals.GroupBy(l => (l.Namespace, l.ParentType))) + { + NewLine(); + int braces = 0; + if (!string.IsNullOrWhiteSpace(grp.Key.Namespace)) + { + NewLine().Append("namespace ").Append(grp.Key.Namespace); + NewLine().Append("{"); + indent++; + braces++; + } + if (!string.IsNullOrWhiteSpace(grp.Key.ParentType)) + { + if (grp.Key.ParentType.Contains('.')) // nested types + { + foreach (var part in grp.Key.ParentType.Split('.')) + { + NewLine().Append("partial class ").Append(part); + NewLine().Append("{"); + indent++; + braces++; + } + } + else + { + NewLine().Append("partial class ").Append(grp.Key.ParentType); + NewLine().Append("{"); + indent++; + braces++; + } + } + + foreach (var literal in grp) + { + int len; + unsafe + { + fixed (byte* bPtr = buffer) // netstandard2.0 forces fallback API + { + fixed (char* cPtr = literal.Value) + { + len = Encoding.UTF8.GetBytes(cPtr, literal.Value.Length, bPtr, buffer.Length); + } + } + } + + // perform string escaping on the generated value (this includes the quotes, note) + var csValue = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(literal.Value)).ToFullString(); + + var hash = FastHash.Hash64(buffer.AsSpan(0, len)); + NewLine().Append("static partial class ").Append(literal.Name); + NewLine().Append("{"); + indent++; + NewLine().Append("public const int Length = ").Append(len).Append(';'); + NewLine().Append("public const long Hash = ").Append(hash).Append(';'); + NewLine().Append("public static ReadOnlySpan U8 => ").Append(csValue).Append("u8;"); + NewLine().Append("public const string Text = ").Append(csValue).Append(';'); + if (len <= 8) + { + // the hash enforces all the values + NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.Payload.Length == Length;"); + NewLine().Append("public static bool Is(long hash, ReadOnlySpan value) => hash == Hash & value.Length == Length;"); + } + else + { + NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8);"); + NewLine().Append("public static bool Is(long hash, ReadOnlySpan value) => hash == Hash && value.SequenceEqual(U8);"); + } + indent--; + NewLine().Append("}"); + } + + // handle any closing braces + while (braces-- > 0) + { + indent--; + NewLine().Append("}"); + } + } + + ArrayPool.Shared.Return(buffer); + ctx.AddSource("FastHash.generated.cs", sb.ToString()); + } +} diff --git a/eng/StackExchange.Redis.Build/FastHashGenerator.md b/eng/StackExchange.Redis.Build/FastHashGenerator.md new file mode 100644 index 000000000..7fc5103ae --- /dev/null +++ b/eng/StackExchange.Redis.Build/FastHashGenerator.md @@ -0,0 +1,64 @@ +# FastHashGenerator + +Efficient matching of well-known short string tokens is a high-volume scenario, for example when matching RESP literals. + +The purpose of this generator is to interpret inputs like: + +``` c# +[FastHash] public static partial class bin { } +[FastHash] public static partial class f32 { } +``` + +Usually the token is inferred from the name; `[FastHash("real value")]` can be used if the token is not a valid identifier. +Underscore is replaced with hyphen, so a field called `my_token` has the default value `"my-token"`. +The generator demands *all* of `[FastHash] public static partial class`, and note that any *containing* types must +*also* be declared `partial`. + +The output is of the form: + +``` c# +static partial class bin +{ + public const int Length = 3; + public const long Hash = 7235938; + public static ReadOnlySpan U8 => @"bin"u8; + public static string Text => @"bin"; + public static bool Is(long hash, in RawResult value) => ... + public static bool Is(long hash, in ReadOnlySpan value) => ... +} +static partial class f32 +{ + public const int Length = 3; + public const long Hash = 3289958; + public static ReadOnlySpan U8 => @"f32"u8; + public const string Text = @"f32"; + public static bool Is(long hash, in RawResult value) => ... + public static bool Is(long hash, in ReadOnlySpan value) => ... +} +``` + +(this API is strictly an internal implementation detail, and can change at any time) + +This generated code allows for fast, efficient, and safe matching of well-known tokens, for example: + +``` c# +var key = ... +var hash = key.Hash64(); +switch (key.Length) +{ + case bin.Length when bin.Is(hash, key): + // handle bin + break; + case f32.Length when f32.Is(hash, key): + // handle f32 + break; +} +``` + +The switch on the `Length` is optional, but recommended - these low values can often be implemented (by the compiler) +as a simple jump-table, which is very fast. However, switching on the hash itself is also valid. All hash matches +must also perform a sequence equality check - the `Is(hash, value)` convenience method validates both hash and equality. + +Note that `switch` requires `const` values, hence why we use generated *types* rather than partial-properties +that emit an instance with the known values. Also, the `"..."u8` syntax emits a span which is awkward to store, but +easy to return via a property. diff --git a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj new file mode 100644 index 000000000..f875133ba --- /dev/null +++ b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.0 + enable + enable + true + + + + + + + + + FastHash.cs + + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 29eadff61..06e403ebb 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -10,4 +10,7 @@ + + + diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 52f0b134d..7a0c2f08d 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -206,6 +206,19 @@ internal enum RedisCommand UNSUBSCRIBE, UNWATCH, + VADD, + VCARD, + VDIM, + VEMB, + VGETATTR, + VINFO, + VISMEMBER, + VLINKS, + VRANDMEMBER, + VREM, + VSETATTR, + VSIM, + WATCH, XACK, @@ -352,6 +365,9 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.SWAPDB: case RedisCommand.TOUCH: case RedisCommand.UNLINK: + case RedisCommand.VADD: + case RedisCommand.VREM: + case RedisCommand.VSETATTR: case RedisCommand.XAUTOCLAIM: case RedisCommand.ZADD: case RedisCommand.ZDIFFSTORE: @@ -499,6 +515,15 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.ZSCORE: case RedisCommand.ZUNION: case RedisCommand.UNKNOWN: + case RedisCommand.VCARD: + case RedisCommand.VDIM: + case RedisCommand.VEMB: + case RedisCommand.VGETATTR: + case RedisCommand.VINFO: + case RedisCommand.VISMEMBER: + case RedisCommand.VLINKS: + case RedisCommand.VRANDMEMBER: + case RedisCommand.VSIM: // Writable commands, but allowed for the writable-replicas scenario case RedisCommand.COPY: case RedisCommand.GEOADD: diff --git a/src/StackExchange.Redis/Experiments.cs b/src/StackExchange.Redis/Experiments.cs new file mode 100644 index 000000000..577c9f8c9 --- /dev/null +++ b/src/StackExchange.Redis/Experiments.cs @@ -0,0 +1,41 @@ +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis +{ + // example usage: + // [Experimental(Experiments.SomeFeature, UrlFormat = Experiments.UrlFormat)] + // where SomeFeature has the next label, for example "SER042", and /docs/exp/SER042.md exists + internal static class Experiments + { + public const string UrlFormat = "https://stackexchange.github.io/StackExchange.Redis/exp/"; + public const string VectorSets = "SER001"; + } +} + +#if !NET8_0_OR_GREATER +#pragma warning disable SA1403 +namespace System.Diagnostics.CodeAnalysis +#pragma warning restore SA1403 +{ + [AttributeUsage( + AttributeTargets.Assembly | + AttributeTargets.Module | + AttributeTargets.Class | + AttributeTargets.Struct | + AttributeTargets.Enum | + AttributeTargets.Constructor | + AttributeTargets.Method | + AttributeTargets.Property | + AttributeTargets.Field | + AttributeTargets.Event | + AttributeTargets.Interface | + AttributeTargets.Delegate, + Inherited = false)] + internal sealed class ExperimentalAttribute(string diagnosticId) : Attribute + { + public string DiagnosticId { get; } = diagnosticId; + public string? UrlFormat { get; set; } + public string? Message { get; set; } + } +} +#endif diff --git a/src/StackExchange.Redis/FastHash.cs b/src/StackExchange.Redis/FastHash.cs new file mode 100644 index 000000000..49eb01b31 --- /dev/null +++ b/src/StackExchange.Redis/FastHash.cs @@ -0,0 +1,137 @@ +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace StackExchange.Redis; + +/// +/// This type is intended to provide fast hashing functions for small strings, for example well-known +/// RESP literals that are usually identifiable by their length and initial bytes; it is not intended +/// for general purpose hashing. All matches must also perform a sequence equality check. +/// +/// See HastHashGenerator.md for more information and intended usage. +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +[Conditional("DEBUG")] // evaporate in release +internal sealed class FastHashAttribute(string token = "") : Attribute +{ + public string Token => token; +} + +internal static class FastHash +{ + /* not sure we need this, but: retain for reference + + // Perform case-insensitive hash by masking (X and x differ by only 1 bit); this halves + // our entropy, but is still useful when case doesn't matter. + private const long CaseMask = ~0x2020202020202020; + + public static long Hash64CI(this ReadOnlySequence value) + => value.Hash64() & CaseMask; + public static long Hash64CI(this scoped ReadOnlySpan value) + => value.Hash64() & CaseMask; +*/ + + public static long Hash64(this ReadOnlySequence value) + { +#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + var first = value.FirstSpan; +#else + var first = value.First.Span; +#endif + return first.Length >= sizeof(long) || value.IsSingleSegment + ? first.Hash64() : SlowHash64(value); + + static long SlowHash64(ReadOnlySequence value) + { + Span buffer = stackalloc byte[sizeof(long)]; + if (value.Length < sizeof(long)) + { + value.CopyTo(buffer); + buffer.Slice((int)value.Length).Clear(); + } + else + { + value.Slice(0, sizeof(long)).CopyTo(buffer); + } + return BitConverter.IsLittleEndian + ? Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(buffer)) + : BinaryPrimitives.ReadInt64LittleEndian(buffer); + } + } + + public static long Hash64(this scoped ReadOnlySpan value) + { + if (BitConverter.IsLittleEndian) + { + ref byte data = ref MemoryMarshal.GetReference(value); + return value.Length switch + { + 0 => 0, + 1 => data, // 0000000A + 2 => Unsafe.ReadUnaligned(ref data), // 000000BA + 3 => Unsafe.ReadUnaligned(ref data) | // 000000BA + (Unsafe.Add(ref data, 2) << 16), // 00000C00 + 4 => Unsafe.ReadUnaligned(ref data), // 0000DCBA + 5 => Unsafe.ReadUnaligned(ref data) | // 0000DCBA + ((long)Unsafe.Add(ref data, 4) << 32), // 000E0000 + 6 => Unsafe.ReadUnaligned(ref data) | // 0000DCBA + ((long)Unsafe.ReadUnaligned(ref Unsafe.Add(ref data, 4)) << 32), // 00FE0000 + 7 => Unsafe.ReadUnaligned(ref data) | // 0000DCBA + ((long)Unsafe.ReadUnaligned(ref Unsafe.Add(ref data, 4)) << 32) | // 00FE0000 + ((long)Unsafe.Add(ref data, 6) << 48), // 0G000000 + _ => Unsafe.ReadUnaligned(ref data), // HGFEDCBA + }; + } + +#pragma warning disable CS0618 // Type or member is obsolete + return Hash64Fallback(value); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Obsolete("Only exists for benchmarks (to show that we don't need to use it) and unit tests (for correctness)")] + internal static unsafe long Hash64Unsafe(scoped ReadOnlySpan value) + { + if (BitConverter.IsLittleEndian) + { + fixed (byte* ptr = &MemoryMarshal.GetReference(value)) + { + return value.Length switch + { + 0 => 0, + 1 => *ptr, // 0000000A + 2 => *(ushort*)ptr, // 000000BA + 3 => *(ushort*)ptr | // 000000BA + (ptr[2] << 16), // 00000C00 + 4 => *(int*)ptr, // 0000DCBA + 5 => (long)*(int*)ptr | // 0000DCBA + ((long)ptr[4] << 32), // 000E0000 + 6 => (long)*(int*)ptr | // 0000DCBA + ((long)*(ushort*)(ptr + 4) << 32), // 00FE0000 + 7 => (long)*(int*)ptr | // 0000DCBA + ((long)*(ushort*)(ptr + 4) << 32) | // 00FE0000 + ((long)ptr[6] << 48), // 0G000000 + _ => *(long*)ptr, // HGFEDCBA + }; + } + } + + return Hash64Fallback(value); + } + + [Obsolete("Only exists for unit tests and fallback")] + internal static long Hash64Fallback(scoped ReadOnlySpan value) + { + if (value.Length < sizeof(long)) + { + Span tmp = stackalloc byte[sizeof(long)]; + value.CopyTo(tmp); // ABC***** + tmp.Slice(value.Length).Clear(); // ABC00000 + return BinaryPrimitives.ReadInt64LittleEndian(tmp); // 00000CBA + } + + return BinaryPrimitives.ReadInt64LittleEndian(value); // HGFEDCBA + } +} diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs new file mode 100644 index 000000000..039075ec8 --- /dev/null +++ b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs @@ -0,0 +1,183 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis; + +/// +/// Describes functionality that is common to both standalone redis servers and redis clusters. +/// +public partial interface IDatabase +{ + // Vector Set operations + + /// + /// Add a vector to a vectorset. + /// + /// The key of the vectorset. + /// The data to add. + /// The flags to use for this operation. + /// if the element was added; if it already existed. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + bool VectorSetAdd( + RedisKey key, + VectorSetAddRequest request, + CommandFlags flags = CommandFlags.None); + + /// + /// Get the cardinality (number of elements) of a vectorset. + /// + /// The key of the vectorset. + /// The flags to use for this operation. + /// The cardinality of the vectorset. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Get the dimension of vectors in a vectorset. + /// + /// The key of the vectorset. + /// The flags to use for this operation. + /// The dimension of vectors in the vectorset. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Get the vector for a member. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// The vector as a pooled memory lease. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Lease? VectorSetGetApproximateVector( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None); + + /// + /// Get JSON attributes for a member in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// The attributes as a JSON string. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Get information about a vectorset. + /// + /// The key of the vectorset. + /// The flags to use for this operation. + /// Information about the vectorset. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Check if a member exists in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// True if the member exists, false otherwise. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Get links/connections for a member in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// The linked members. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Get links/connections with scores for a member in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The flags to use for this operation. + /// The linked members with their similarity scores. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Lease? VectorSetGetLinksWithScores( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None); + + /// + /// Get a random member from a vectorset. + /// + /// The key of the vectorset. + /// The flags to use for this operation. + /// A random member from the vectorset, or null if the vectorset is empty. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + /// Get random members from a vectorset. + /// + /// The key of the vectorset. + /// The number of random members to return. + /// The flags to use for this operation. + /// Random members from the vectorset. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + + /// + /// Remove a member from a vectorset. + /// + /// The key of the vectorset. + /// The member to remove. + /// The flags to use for this operation. + /// if the member was removed; if it was not found. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + /// Set JSON attributes for a member in a vectorset. + /// + /// The key of the vectorset. + /// The member name. + /// The attributes to set as a JSON string. + /// The flags to use for this operation. + /// True if successful. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + bool VectorSetSetAttributesJson( + RedisKey key, + RedisValue member, +#if NET7_0_OR_GREATER + [StringSyntax(StringSyntaxAttribute.Json)] +#endif + string attributesJson, + CommandFlags flags = CommandFlags.None); + + /// + /// Find similar vectors using vector similarity search. + /// + /// The key of the vectorset. + /// The query to execute. + /// The flags to use for this operation. + /// Similar vectors with their similarity scores. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Lease? VectorSetSimilaritySearch( + RedisKey key, + VectorSetSimilaritySearchRequest query, + CommandFlags flags = CommandFlags.None); +} diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index b6caafabe..6c52e89bd 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -3,12 +3,13 @@ using System.ComponentModel; using System.Net; +// ReSharper disable once CheckNamespace namespace StackExchange.Redis { /// /// Describes functionality that is common to both standalone redis servers and redis clusters. /// - public interface IDatabase : IRedis, IDatabaseAsync + public partial interface IDatabase : IRedis, IDatabaseAsync { /// /// The numeric identifier of this database. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs new file mode 100644 index 000000000..863095140 --- /dev/null +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs @@ -0,0 +1,95 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis; + +/// +/// Describes functionality that is common to both standalone redis servers and redis clusters. +/// +public partial interface IDatabaseAsync +{ + // Vector Set operations + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetAddAsync( + RedisKey key, + VectorSetAddRequest request, + CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task?> VectorSetGetApproximateVectorAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetGetAttributesJsonAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task?> VectorSetGetLinksAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task?> VectorSetGetLinksWithScoresAsync( + RedisKey key, + RedisValue member, + CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task VectorSetSetAttributesJsonAsync( + RedisKey key, + RedisValue member, +#if NET7_0_OR_GREATER + [StringSyntax(StringSyntaxAttribute.Json)] +#endif + string attributesJson, + CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task?> VectorSetSimilaritySearchAsync( + RedisKey key, + VectorSetSimilaritySearchRequest query, + CommandFlags flags = CommandFlags.None); +} diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 51a15d7d5..0bc7b4867 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -4,12 +4,13 @@ using System.Net; using System.Threading.Tasks; +// ReSharper disable once CheckNamespace namespace StackExchange.Redis { /// /// Describes functionality that is common to both standalone redis servers and redis clusters. /// - public interface IDatabaseAsync : IRedisAsync + public partial interface IDatabaseAsync : IRedisAsync { /// /// Indicates whether the instance can communicate with the server (resolved using the supplied key and optional flags). diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs new file mode 100644 index 000000000..809adad97 --- /dev/null +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs @@ -0,0 +1,59 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis.KeyspaceIsolation; + +internal partial class KeyPrefixed +{ + // Vector Set operations - async methods + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + public Task VectorSetAddAsync( + RedisKey key, + VectorSetAddRequest request, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetAddAsync(ToInner(key), request, flags); + + public Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetLengthAsync(ToInner(key), flags); + + public Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetDimensionAsync(ToInner(key), flags); + + public Task?> VectorSetGetApproximateVectorAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetApproximateVectorAsync(ToInner(key), member, flags); + + public Task VectorSetGetAttributesJsonAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetAttributesJsonAsync(ToInner(key), member, flags); + + public Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetInfoAsync(ToInner(key), flags); + + public Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetContainsAsync(ToInner(key), member, flags); + + public Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetLinksAsync(ToInner(key), member, flags); + + public Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetLinksWithScoresAsync(ToInner(key), member, flags); + + public Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRandomMemberAsync(ToInner(key), flags); + + public Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRandomMembersAsync(ToInner(key), count, flags); + + public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRemoveAsync(ToInner(key), member, flags); + + public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string attributesJson, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSetAttributesJsonAsync(ToInner(key), member, attributesJson, flags); + + public Task?> VectorSetSimilaritySearchAsync( + RedisKey key, + VectorSetSimilaritySearchRequest query, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSimilaritySearchAsync(ToInner(key), query, flags); +} diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 32c76f4d2..61a6f44c4 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -7,7 +7,7 @@ namespace StackExchange.Redis.KeyspaceIsolation { - internal class KeyPrefixed : IDatabaseAsync where TInner : IDatabaseAsync + internal partial class KeyPrefixed : IDatabaseAsync where TInner : IDatabaseAsync { internal KeyPrefixed(TInner inner, byte[] keyPrefix) { diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs new file mode 100644 index 000000000..62f4e9202 --- /dev/null +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs @@ -0,0 +1,56 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis.KeyspaceIsolation; + +internal sealed partial class KeyPrefixedDatabase +{ + // Vector Set operations + public bool VectorSetAdd( + RedisKey key, + VectorSetAddRequest request, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetAdd(ToInner(key), request, flags); + + public long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetLength(ToInner(key), flags); + + public int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetDimension(ToInner(key), flags); + + public Lease? VectorSetGetApproximateVector(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetApproximateVector(ToInner(key), member, flags); + + public string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetAttributesJson(ToInner(key), member, flags); + + public VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetInfo(ToInner(key), flags); + + public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetContains(ToInner(key), member, flags); + + public Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetLinks(ToInner(key), member, flags); + + public Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetGetLinksWithScores(ToInner(key), member, flags); + + public RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRandomMember(ToInner(key), flags); + + public RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRandomMembers(ToInner(key), count, flags); + + public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRemove(ToInner(key), member, flags); + + public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string attributesJson, CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSetAttributesJson(ToInner(key), member, attributesJson, flags); + + public Lease? VectorSetSimilaritySearch( + RedisKey key, + VectorSetSimilaritySearchRequest query, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetSimilaritySearch(ToInner(key), query, flags); +} diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index 755bec64e..2a139694e 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -4,7 +4,7 @@ namespace StackExchange.Redis.KeyspaceIsolation { - internal sealed class KeyPrefixedDatabase : KeyPrefixed, IDatabase + internal sealed partial class KeyPrefixedDatabase : KeyPrefixed, IDatabase { public KeyPrefixedDatabase(IDatabase inner, byte[] prefix) : base(inner, prefix) { diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index cd3d29947..5973bd55b 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -705,6 +705,7 @@ internal void SetWriteTime() _writeTickCount = Environment.TickCount; // note this might be reset if we resend a message, cluster-moved etc; I'm OK with that } private int _writeTickCount; + public int GetWriteTime() => Volatile.Read(ref _writeTickCount); /// diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index c587241a0..129fd9e07 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -859,6 +859,14 @@ internal static void WriteBulkString(in RedisValue value, PipeWriter? maybeNullW } } + internal void WriteBulkString(ReadOnlySpan value) + { + if (_ioPipe?.Output is { } writer) + { + WriteUnifiedSpan(writer, value); + } + } + internal const int REDIS_MAX_ARGS = 1024 * 1024; // there is a <= 1024*1024 max constraint inside redis itself: https://github.com/antirez/redis/blob/6c60526db91e23fb2d666fc52facc9a11780a2a3/src/networking.c#L1024 internal void WriteHeader(RedisCommand command, int arguments, CommandBytes commandBytes = default) diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index e82af2bee..10044dc9b 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -1962,3 +1962,92 @@ StackExchange.Redis.ConnectionMultiplexer.GetServer(StackExchange.Redis.RedisKey StackExchange.Redis.IConnectionMultiplexer.GetServer(StackExchange.Redis.RedisKey key, object? asyncState = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.IServer! StackExchange.Redis.IServer.Execute(int? database, string! command, System.Collections.Generic.ICollection! args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisResult! StackExchange.Redis.IServer.ExecuteAsync(int? database, string! command, System.Collections.Generic.ICollection! args, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]override StackExchange.Redis.VectorSetLink.ToString() -> string! +[SER001]override StackExchange.Redis.VectorSetSimilaritySearchResult.ToString() -> string! +[SER001]StackExchange.Redis.IDatabase.VectorSetAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetAddRequest! request, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetAddRequest! request, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.VectorSetAddRequest +[SER001]StackExchange.Redis.VectorSetAddRequest.BuildExplorationFactor.get -> int? +[SER001]StackExchange.Redis.VectorSetAddRequest.BuildExplorationFactor.set -> void +[SER001]StackExchange.Redis.VectorSetAddRequest.MaxConnections.get -> int? +[SER001]StackExchange.Redis.VectorSetAddRequest.MaxConnections.set -> void +[SER001]StackExchange.Redis.VectorSetAddRequest.Quantization.get -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetAddRequest.Quantization.set -> void +[SER001]StackExchange.Redis.VectorSetAddRequest.ReducedDimensions.get -> int? +[SER001]StackExchange.Redis.VectorSetAddRequest.ReducedDimensions.set -> void +[SER001]StackExchange.Redis.VectorSetAddRequest.UseCheckAndSet.get -> bool +[SER001]StackExchange.Redis.VectorSetAddRequest.UseCheckAndSet.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Count.get -> int? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Count.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.DisableThreading.get -> bool +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.DisableThreading.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Epsilon.get -> double? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.Epsilon.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.FilterExpression.get -> string? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.FilterExpression.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.MaxFilteringEffort.get -> int? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.MaxFilteringEffort.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.SearchExplorationFactor.get -> int? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.SearchExplorationFactor.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.UseExactSearch.get -> bool +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.UseExactSearch.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithAttributes.get -> bool +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithAttributes.set -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithScores.get -> bool +[SER001]StackExchange.Redis.VectorSetSimilaritySearchRequest.WithScores.set -> void +[SER001]StackExchange.Redis.IDatabase.VectorSetContains(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabase.VectorSetDimension(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> int +[SER001]StackExchange.Redis.IDatabase.VectorSetGetApproximateVector(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabase.VectorSetGetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? +[SER001]StackExchange.Redis.IDatabase.VectorSetGetLinks(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabase.VectorSetGetLinksWithScores(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabase.VectorSetInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.VectorSetInfo? +[SER001]StackExchange.Redis.IDatabase.VectorSetLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long +[SER001]StackExchange.Redis.IDatabase.VectorSetRandomMember(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +[SER001]StackExchange.Redis.IDatabase.VectorSetRandomMembers(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]! +[SER001]StackExchange.Redis.IDatabase.VectorSetRemove(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabase.VectorSetSetAttributesJson(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! attributesJson, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER001]StackExchange.Redis.IDatabase.VectorSetSimilaritySearch(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetSimilaritySearchRequest! query, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease? +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetContainsAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetDimensionAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetApproximateVectorAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetLinksAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetGetLinksWithScoresAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRandomMemberAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRandomMembersAsync(StackExchange.Redis.RedisKey key, long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRemoveAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSetAttributesJsonAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue member, string! attributesJson, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetSimilaritySearchAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.VectorSetSimilaritySearchRequest! query, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.VectorSetInfo +[SER001]StackExchange.Redis.VectorSetInfo.Dimension.get -> int +[SER001]StackExchange.Redis.VectorSetInfo.HnswMaxNodeUid.get -> long +[SER001]StackExchange.Redis.VectorSetInfo.Length.get -> long +[SER001]StackExchange.Redis.VectorSetInfo.MaxLevel.get -> int +[SER001]StackExchange.Redis.VectorSetInfo.Quantization.get -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetInfo.QuantizationRaw.get -> string? +[SER001]StackExchange.Redis.VectorSetInfo.VectorSetInfo() -> void +[SER001]StackExchange.Redis.VectorSetInfo.VectorSetInfo(StackExchange.Redis.VectorSetQuantization quantization, string? quantizationRaw, int dimension, long length, int maxLevel, long vectorSetUid, long hnswMaxNodeUid) -> void +[SER001]StackExchange.Redis.VectorSetInfo.VectorSetUid.get -> long +[SER001]StackExchange.Redis.VectorSetLink +[SER001]StackExchange.Redis.VectorSetLink.Member.get -> StackExchange.Redis.RedisValue +[SER001]StackExchange.Redis.VectorSetLink.Score.get -> double +[SER001]StackExchange.Redis.VectorSetLink.VectorSetLink() -> void +[SER001]StackExchange.Redis.VectorSetLink.VectorSetLink(StackExchange.Redis.RedisValue member, double score) -> void +[SER001]StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetQuantization.Binary = 3 -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetQuantization.Int8 = 2 -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetQuantization.None = 1 -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetQuantization.Unknown = 0 -> StackExchange.Redis.VectorSetQuantization +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.AttributesJson.get -> string? +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.Member.get -> StackExchange.Redis.RedisValue +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.Score.get -> double +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.VectorSetSimilaritySearchResult() -> void +[SER001]StackExchange.Redis.VectorSetSimilaritySearchResult.VectorSetSimilaritySearchResult(StackExchange.Redis.RedisValue member, double score = NaN, string? attributesJson = null) -> void +[SER001]static StackExchange.Redis.VectorSetAddRequest.Member(StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, string? attributesJson = null) -> StackExchange.Redis.VectorSetAddRequest! +[SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByMember(StackExchange.Redis.RedisValue member) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! +[SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByVector(System.ReadOnlyMemory vector) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 55c44652b..1ac9f081a 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -237,10 +237,14 @@ internal bool IsEqual(in CommandBytes expected) return new CommandBytes(Payload).Equals(expected); } - internal unsafe bool IsEqual(byte[]? expected) + internal bool IsEqual(byte[]? expected) { if (expected == null) throw new ArgumentNullException(nameof(expected)); + return IsEqual(new ReadOnlySpan(expected)); + } + internal bool IsEqual(ReadOnlySpan expected) + { var rangeToCheck = Payload; if (expected.Length != rangeToCheck.Length) return false; @@ -250,7 +254,7 @@ internal unsafe bool IsEqual(byte[]? expected) foreach (var segment in rangeToCheck) { var from = segment.Span; - var to = new Span(expected, offset, from.Length); + var to = expected.Slice(offset, from.Length); if (!from.SequenceEqual(to)) return false; offset += from.Length; diff --git a/src/StackExchange.Redis/RedisDatabase.VectorSets.cs b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs new file mode 100644 index 000000000..9b3f1b43b --- /dev/null +++ b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs @@ -0,0 +1,191 @@ +using System; +using System.Threading.Tasks; + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis; + +internal partial class RedisDatabase +{ + public bool VectorSetAdd( + RedisKey key, + VectorSetAddRequest request, + CommandFlags flags = CommandFlags.None) + { + var msg = request.ToMessage(key, Database, flags); + return ExecuteSync(msg, ResultProcessor.Boolean); + } + + public long VectorSetLength(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VCARD, key); + return ExecuteSync(msg, ResultProcessor.Int64); + } + + public int VectorSetDimension(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VDIM, key); + return ExecuteSync(msg, ResultProcessor.Int32); + } + + public Lease? VectorSetGetApproximateVector(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VEMB, key, member); + return ExecuteSync(msg, ResultProcessor.LeaseFloat32); + } + + public string? VectorSetGetAttributesJson(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VGETATTR, key, member); + return ExecuteSync(msg, ResultProcessor.String); + } + + public VectorSetInfo? VectorSetInfo(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VINFO, key); + return ExecuteSync(msg, ResultProcessor.VectorSetInfo); + } + + public bool VectorSetContains(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VISMEMBER, key, member); + return ExecuteSync(msg, ResultProcessor.Boolean); + } + + public Lease? VectorSetGetLinks(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member); + return ExecuteSync(msg, ResultProcessor.VectorSetLinks); + } + + public Lease? VectorSetGetLinksWithScores(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member, RedisLiterals.WITHSCORES); + return ExecuteSync(msg, ResultProcessor.VectorSetLinksWithScores); + } + + public RedisValue VectorSetRandomMember(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key); + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + + public RedisValue[] VectorSetRandomMembers(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key, count); + return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public bool VectorSetRemove(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VREM, key, member); + return ExecuteSync(msg, ResultProcessor.Boolean); + } + + public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string attributesJson, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, attributesJson); + return ExecuteSync(msg, ResultProcessor.Boolean); + } + + public Lease? VectorSetSimilaritySearch( + RedisKey key, + VectorSetSimilaritySearchRequest query, + CommandFlags flags = CommandFlags.None) + { + if (query == null) throw new ArgumentNullException(nameof(query)); + var msg = query.ToMessage(key, Database, flags); + return ExecuteSync(msg, msg.GetResultProcessor()); + } + + // Vector Set async operations + public Task VectorSetAddAsync( + RedisKey key, + VectorSetAddRequest request, + CommandFlags flags = CommandFlags.None) + { + var msg = request.ToMessage(key, Database, flags); + return ExecuteAsync(msg, ResultProcessor.Boolean); + } + + public Task VectorSetLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VCARD, key); + return ExecuteAsync(msg, ResultProcessor.Int64); + } + + public Task VectorSetDimensionAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VDIM, key); + return ExecuteAsync(msg, ResultProcessor.Int32); + } + + public Task?> VectorSetGetApproximateVectorAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VEMB, key, member); + return ExecuteAsync(msg, ResultProcessor.LeaseFloat32); + } + + public Task VectorSetGetAttributesJsonAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VGETATTR, key, member); + return ExecuteAsync(msg, ResultProcessor.String); + } + + public Task VectorSetInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VINFO, key); + return ExecuteAsync(msg, ResultProcessor.VectorSetInfo); + } + + public Task VectorSetContainsAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VISMEMBER, key, member); + return ExecuteAsync(msg, ResultProcessor.Boolean); + } + + public Task?> VectorSetGetLinksAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member); + return ExecuteAsync(msg, ResultProcessor.VectorSetLinks); + } + + public Task?> VectorSetGetLinksWithScoresAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VLINKS, key, member, RedisLiterals.WITHSCORES); + return ExecuteAsync(msg, ResultProcessor.VectorSetLinksWithScores); + } + + public Task VectorSetRandomMemberAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key); + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + + public Task VectorSetRandomMembersAsync(RedisKey key, long count, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VRANDMEMBER, key, count); + return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); + } + + public Task VectorSetRemoveAsync(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VREM, key, member); + return ExecuteAsync(msg, ResultProcessor.Boolean); + } + + public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue member, string attributesJson, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.VSETATTR, key, member, attributesJson); + return ExecuteAsync(msg, ResultProcessor.Boolean); + } + + public Task?> VectorSetSimilaritySearchAsync( + RedisKey key, + VectorSetSimilaritySearchRequest query, + CommandFlags flags = CommandFlags.None) + { + if (query == null) throw new ArgumentNullException(nameof(query)); + var msg = query.ToMessage(key, Database, flags); + return ExecuteAsync(msg, msg.GetResultProcessor()); + } +} diff --git a/src/StackExchange.Redis/ResultProcessor.Lease.cs b/src/StackExchange.Redis/ResultProcessor.Lease.cs new file mode 100644 index 000000000..c0f9e6d8e --- /dev/null +++ b/src/StackExchange.Redis/ResultProcessor.Lease.cs @@ -0,0 +1,218 @@ +using System.Diagnostics; +using Pipelines.Sockets.Unofficial.Arenas; + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis; + +internal abstract partial class ResultProcessor +{ + // Lease result processors + public static readonly ResultProcessor?> LeaseFloat32 = new LeaseFloat32Processor(); + + public static readonly ResultProcessor> + Lease = new LeaseProcessor(); + + public static readonly ResultProcessor> + LeaseFromArray = new LeaseFromArrayProcessor(); + + private abstract class LeaseProcessor : ResultProcessor?> + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray != ResultType.Array) + { + return false; // not an array + } + + // deal with null + if (result.IsNull) + { + SetResult(message, Lease.Empty); + return true; + } + + // lease and fill + var items = result.GetItems(); + var length = checked((int)items.Length); + var lease = Lease.Create(length, clear: false); // note this handles zero nicely + var target = lease.Span; + int index = 0; + foreach (ref RawResult item in items) + { + if (!TryParse(item, out target[index++])) + { + // something went wrong; recycle and quit + lease.Dispose(); + return false; + } + } + Debug.Assert(index == length, "length mismatch"); + SetResult(message, lease); + return true; + } + + protected abstract bool TryParse(in RawResult raw, out T parsed); + } + + private abstract class InterleavedLeaseProcessor : ResultProcessor?> + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray != ResultType.Array) + { + return false; // not an array + } + + // deal with null + if (result.IsNull) + { + SetResult(message, Lease.Empty); + return true; + } + + // lease and fill + var items = result.GetItems(); + var length = checked((int)items.Length) / 2; + var lease = Lease.Create(length, clear: false); // note this handles zero nicely + var target = lease.Span; + + var iter = items.GetEnumerator(); + for (int i = 0; i < target.Length; i++) + { + bool ok = iter.MoveNext(); + if (ok) + { + ref readonly RawResult first = ref iter.Current; + ok = iter.MoveNext() && TryParse(in first, in iter.Current, out target[i]); + } + if (!ok) + { + lease.Dispose(); + return false; + } + } + SetResult(message, lease); + return true; + } + + protected abstract bool TryParse(in RawResult first, in RawResult second, out T parsed); + } + + // takes a nested vector of the form [[A],[B,C],[D]] and exposes it as [A,B,C,D]; this is + // especially useful for VLINKS + private abstract class FlattenedLeaseProcessor : ResultProcessor?> + { + protected virtual long GetArrayLength(in RawResult array) => array.GetItems().Length; + + protected virtual bool TryReadOne(ref Sequence.Enumerator reader, out T value) + { + if (reader.MoveNext()) + { + return TryReadOne(in reader.Current, out value); + } + value = default!; + return false; + } + + protected virtual bool TryReadOne(in RawResult result, out T value) + { + value = default!; + return false; + } + + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray != ResultType.Array) + { + return false; // not an array + } + if (result.IsNull) + { + SetResult(message, Lease.Empty); + return true; + } + var items = result.GetItems(); + long length = 0; + foreach (ref RawResult item in items) + { + if (item.Resp2TypeArray == ResultType.Array && !item.IsNull) + { + length += GetArrayLength(in item); + } + } + + if (length == 0) + { + SetResult(message, Lease.Empty); + return true; + } + var lease = Lease.Create(checked((int)length), clear: false); + int index = 0; + var target = lease.Span; + foreach (ref RawResult item in items) + { + if (item.Resp2TypeArray == ResultType.Array && !item.IsNull) + { + var iter = item.GetItems().GetEnumerator(); + while (index < target.Length && TryReadOne(ref iter, out target[index])) + { + index++; + } + } + } + + if (index == length) + { + SetResult(message, lease); + return true; + } + lease.Dispose(); // failed to fill? + return false; + } + } + + private sealed class LeaseFloat32Processor : LeaseProcessor + { + protected override bool TryParse(in RawResult raw, out float parsed) + { + var result = raw.TryGetDouble(out double val); + parsed = (float)val; + return result; + } + } + + private sealed class LeaseProcessor : ResultProcessor> + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Resp2TypeBulkString) + { + case ResultType.Integer: + case ResultType.SimpleString: + case ResultType.BulkString: + SetResult(message, result.AsLease()!); + return true; + } + return false; + } + } + + private sealed class LeaseFromArrayProcessor : ResultProcessor> + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Resp2TypeBulkString) + { + case ResultType.Array: + var items = result.GetItems(); + if (items.Length == 1) + { // treat an array of 1 like a single reply + SetResult(message, items[0].AsLease()!); + return true; + } + break; + } + return false; + } + } +} diff --git a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs new file mode 100644 index 000000000..8743ebd0b --- /dev/null +++ b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs @@ -0,0 +1,138 @@ +using Pipelines.Sockets.Unofficial.Arenas; + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis; + +internal abstract partial class ResultProcessor +{ + // VectorSet result processors + public static readonly ResultProcessor?> VectorSetLinksWithScores = + new VectorSetLinksWithScoresProcessor(); + + public static readonly ResultProcessor?> VectorSetLinks = new VectorSetLinksProcessor(); + + public static ResultProcessor VectorSetInfo = new VectorSetInfoProcessor(); + + private sealed class VectorSetLinksWithScoresProcessor : FlattenedLeaseProcessor + { + protected override long GetArrayLength(in RawResult array) => array.GetItems().Length / 2; + + protected override bool TryReadOne(ref Sequence.Enumerator reader, out VectorSetLink value) + { + if (reader.MoveNext()) + { + ref readonly RawResult first = ref reader.Current; + if (reader.MoveNext() && reader.Current.TryGetDouble(out var score)) + { + value = new VectorSetLink(first.AsRedisValue(), score); + return true; + } + } + + value = default; + return false; + } + } + + private sealed class VectorSetLinksProcessor : FlattenedLeaseProcessor + { + protected override bool TryReadOne(in RawResult result, out RedisValue value) + { + value = result.AsRedisValue(); + return true; + } + } + + private sealed partial class VectorSetInfoProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray == ResultType.Array) + { + if (result.IsNull) + { + SetResult(message, null); + return true; + } + + var quantType = VectorSetQuantization.Unknown; + string? quantTypeRaw = null; + int vectorDim = 0, maxLevel = 0; + long resultSize = 0, vsetUid = 0, hnswMaxNodeUid = 0; + var iter = result.GetItems().GetEnumerator(); + while (iter.MoveNext()) + { + ref readonly RawResult key = ref iter.Current; + if (!iter.MoveNext()) break; + ref readonly RawResult value = ref iter.Current; + + var len = key.Payload.Length; + var keyHash = key.Payload.Hash64(); + switch (key.Payload.Length) + { + case size.Length when size.Is(keyHash, key) && value.TryGetInt64(out var i64): + resultSize = i64; + break; + case vset_uid.Length when vset_uid.Is(keyHash, key) && value.TryGetInt64(out var i64): + vsetUid = i64; + break; + case max_level.Length when max_level.Is(keyHash, key) && value.TryGetInt64(out var i64): + maxLevel = checked((int)i64); + break; + case vector_dim.Length + when vector_dim.Is(keyHash, key) && value.TryGetInt64(out var i64): + vectorDim = checked((int)i64); + break; + case quant_type.Length when quant_type.Is(keyHash, key): + var qHash = value.Payload.Hash64(); + switch (value.Payload.Length) + { + case bin.Length when bin.Is(qHash, value): + quantType = VectorSetQuantization.Binary; + break; + case f32.Length when f32.Is(qHash, value): + quantType = VectorSetQuantization.None; + break; + case int8.Length when int8.Is(qHash, value): + quantType = VectorSetQuantization.Int8; + break; + default: + quantTypeRaw = value.GetString(); + quantType = VectorSetQuantization.Unknown; + break; + } + + break; + case hnsw_max_node_uid.Length + when hnsw_max_node_uid.Is(keyHash, key) && value.TryGetInt64(out var i64): + hnswMaxNodeUid = i64; + break; + } + } + + SetResult( + message, + new VectorSetInfo(quantType, quantTypeRaw, vectorDim, resultSize, maxLevel, vsetUid, hnswMaxNodeUid)); + return true; + } + + return false; + } + +#pragma warning disable CS8981, SA1134, SA1300, SA1303, SA1502 + // ReSharper disable InconsistentNaming - to better represent expected literals + // ReSharper disable IdentifierTypo + [FastHash] private static partial class bin { } + [FastHash] private static partial class f32 { } + [FastHash] private static partial class int8 { } + [FastHash] private static partial class size { } + [FastHash] private static partial class vset_uid { } + [FastHash] private static partial class max_level { } + [FastHash] private static partial class quant_type { } + [FastHash] private static partial class vector_dim { } + [FastHash] private static partial class hnsw_max_node_uid { } + // ReSharper restore InconsistentNaming + // ReSharper restore IdentifierTypo +#pragma warning restore CS8981, SA1134, SA1300, SA1303, SA1502 + } +} diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 627953941..650cba603 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -14,7 +14,7 @@ namespace StackExchange.Redis { - internal abstract class ResultProcessor + internal abstract partial class ResultProcessor { public static readonly ResultProcessor Boolean = new BooleanProcessor(), @@ -60,6 +60,8 @@ public static readonly ResultProcessor PubSubNumSub = new PubSubNumSubProcessor(), Int64DefaultNegativeOne = new Int64DefaultValueProcessor(-1); + public static readonly ResultProcessor Int32 = new Int32Processor(); + public static readonly ResultProcessor NullableDouble = new NullableDoubleProcessor(); @@ -91,12 +93,6 @@ public static readonly ResultProcessor public static readonly ResultProcessor RedisValueFromArray = new RedisValueFromArrayProcessor(); - public static readonly ResultProcessor> - Lease = new LeaseProcessor(); - - public static readonly ResultProcessor> - LeaseFromArray = new LeaseFromArrayProcessor(); - public static readonly ResultProcessor RedisValueArray = new RedisValueArrayProcessor(); @@ -700,7 +696,7 @@ public bool TryParse(in RawResult result, out T[]? pairs) count = (int)arr.Length; if (count == 0) { - return Array.Empty(); + return []; } bool interleaved = !(result.IsResp3 && AllowJaggedPairs && IsAllJaggedPairs(arr)); @@ -1384,6 +1380,26 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes } } + private class Int32Processor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + switch (result.Resp2TypeBulkString) + { + case ResultType.Integer: + case ResultType.SimpleString: + case ResultType.BulkString: + if (result.TryGetInt64(out long i64)) + { + SetResult(message, checked((int)i64)); + return true; + } + break; + } + return false; + } + } + internal static ResultProcessor StreamTrimResult => Int32EnumProcessor.Instance; @@ -2058,41 +2074,6 @@ private static bool TryParsePrimaryReplica(in Sequence items, out Rol } } - private sealed class LeaseProcessor : ResultProcessor> - { - protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) - { - switch (result.Resp2TypeBulkString) - { - case ResultType.Integer: - case ResultType.SimpleString: - case ResultType.BulkString: - SetResult(message, result.AsLease()!); - return true; - } - return false; - } - } - - private sealed class LeaseFromArrayProcessor : ResultProcessor> - { - protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) - { - switch (result.Resp2TypeBulkString) - { - case ResultType.Array: - var items = result.GetItems(); - if (items.Length == 1) - { // treat an array of 1 like a single reply - SetResult(message, items[0].AsLease()!); - return true; - } - break; - } - return false; - } - } - private sealed class ScriptResultProcessor : ResultProcessor { public override bool SetResult(PhysicalConnection connection, Message message, in RawResult result) @@ -2136,7 +2117,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (result.IsNull) { // Server returns 'nil' if no entries are returned for the given stream. - SetResult(message, Array.Empty()); + SetResult(message, []); return true; } @@ -2241,7 +2222,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes if (result.IsNull) { // Nothing returned for any of the requested streams. The server returns 'nil'. - SetResult(message, Array.Empty()); + SetResult(message, []); return true; } @@ -2307,7 +2288,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var entries = ParseRedisStreamEntries(items[1]); // [2] The array of message IDs deleted from the stream that were in the PEL. // This is not available in 6.2 so we need to be defensive when reading this part of the response. - var deletedIds = (items.Length == 3 ? items[2].GetItemsAsValues() : null) ?? Array.Empty(); + var deletedIds = (items.Length == 3 ? items[2].GetItemsAsValues() : null) ?? []; SetResult(message, new StreamAutoClaimResult(nextStartId, entries, deletedIds)); return true; @@ -2333,10 +2314,10 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes // [0] The next start ID. var nextStartId = items[0].AsRedisValue(); // [1] The array of claimed message IDs. - var claimedIds = items[1].GetItemsAsValues() ?? Array.Empty(); + var claimedIds = items[1].GetItemsAsValues() ?? []; // [2] The array of message IDs deleted from the stream that were in the PEL. // This is not available in 6.2 so we need to be defensive when reading this part of the response. - var deletedIds = (items.Length == 3 ? items[2].GetItemsAsValues() : null) ?? Array.Empty(); + var deletedIds = (items.Length == 3 ? items[2].GetItemsAsValues() : null) ?? []; SetResult(message, new StreamAutoClaimIdsOnlyResult(nextStartId, claimedIds, deletedIds)); return true; @@ -2644,7 +2625,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes pendingMessageCount: (int)arr[0].AsRedisValue(), lowestId: arr[1].AsRedisValue(), highestId: arr[2].AsRedisValue(), - consumers: consumers ?? Array.Empty()); + consumers: consumers ?? []); SetResult(message, pendingInfo); return true; @@ -2729,7 +2710,7 @@ protected static NameValueEntry[] ParseStreamEntryValues(in RawResult result) // 4) "18.2" if (result.Resp2TypeArray != ResultType.Array || result.IsNull) { - return Array.Empty(); + return []; } return StreamNameValueEntryProcessor.Instance.ParseArray(result, false, out _, null)!; // ! because we checked null above } @@ -2917,7 +2898,7 @@ private sealed class SentinelGetSentinelAddressesProcessor : ResultProcessor endPoints = new List(); + List endPoints = []; switch (result.Resp2TypeArray) { @@ -2951,7 +2932,7 @@ private sealed class SentinelGetReplicaAddressesProcessor : ResultProcessor endPoints = new List(); + List endPoints = []; switch (result.Resp2TypeArray) { @@ -3045,7 +3026,7 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes T[] arr; if (items.IsEmpty) { - arr = Array.Empty(); + arr = []; } else { diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 44efe09be..b13a12423 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -47,4 +47,8 @@ + + + + \ No newline at end of file diff --git a/src/StackExchange.Redis/VectorSetAddMessage.cs b/src/StackExchange.Redis/VectorSetAddMessage.cs new file mode 100644 index 000000000..0beb65205 --- /dev/null +++ b/src/StackExchange.Redis/VectorSetAddMessage.cs @@ -0,0 +1,168 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; + +namespace StackExchange.Redis; + +internal abstract class VectorSetAddMessage( + int db, + CommandFlags flags, + RedisKey key, + int? reducedDimensions, + VectorSetQuantization quantization, + int? buildExplorationFactor, + int? maxConnections, + bool useCheckAndSet) : Message(db, flags, RedisCommand.VADD) +{ + public override int ArgCount => GetArgCount(UseFp32); + + private int GetArgCount(bool packed) + { + var count = 2 + GetElementArgCount(packed); // key, element and either "FP32 {vector}" or VALUES {num}" + if (reducedDimensions.HasValue) count += 2; // [REDUCE {dim}] + + if (useCheckAndSet) count++; // [CAS] + count += quantization switch + { + VectorSetQuantization.None or VectorSetQuantization.Binary => 1, // [NOQUANT] or [BIN] + VectorSetQuantization.Int8 => 0, // implicit + _ => throw new ArgumentOutOfRangeException(nameof(quantization)), + }; + + if (buildExplorationFactor.HasValue) count += 2; // [EF {build-exploration-factor}] + count += GetAttributeArgCount(); // [SETATTR {attributes}] + if (maxConnections.HasValue) count += 2; // [M {numlinks}] + return count; + } + + public abstract int GetElementArgCount(bool packed); + public abstract int GetAttributeArgCount(); + + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) + => serverSelectionStrategy.HashSlot(key); + + private static readonly bool CanUseFp32 = BitConverter.IsLittleEndian && CheckFp32(); + + private static bool CheckFp32() // check endianness with a known value + { + // ReSharper disable once CompareOfFloatsByEqualityOperator - expect exact + return MemoryMarshal.Cast("\0\0(B"u8)[0] == 42; + } + +#if DEBUG + private static int _fp32Disabled; + internal static bool UseFp32 => CanUseFp32 & Volatile.Read(ref _fp32Disabled) == 0; + internal static void SuppressFp32() => Interlocked.Increment(ref _fp32Disabled); + internal static void RestoreFp32() => Interlocked.Decrement(ref _fp32Disabled); +#else + internal static bool UseFp32 => CanUseFp32; + internal static void SuppressFp32() { } + internal static void RestoreFp32() { } +#endif + + protected abstract void WriteElement(bool packed, PhysicalConnection physical); + + protected override void WriteImpl(PhysicalConnection physical) + { + bool packed = UseFp32; // snapshot to avoid race in debug scenarios + physical.WriteHeader(Command, GetArgCount(packed)); + physical.Write(key); + if (reducedDimensions.HasValue) + { + physical.WriteBulkString("REDUCE"u8); + physical.WriteBulkString(reducedDimensions.GetValueOrDefault()); + } + + WriteElement(packed, physical); + if (useCheckAndSet) physical.WriteBulkString("CAS"u8); + + switch (quantization) + { + case VectorSetQuantization.Int8: + break; + case VectorSetQuantization.None: + physical.WriteBulkString("NOQUANT"u8); + break; + case VectorSetQuantization.Binary: + physical.WriteBulkString("BIN"u8); + break; + default: + throw new ArgumentOutOfRangeException(nameof(quantization)); + } + + if (buildExplorationFactor.HasValue) + { + physical.WriteBulkString("EF"u8); + physical.WriteBulkString(buildExplorationFactor.GetValueOrDefault()); + } + + WriteAttributes(physical); + + if (maxConnections.HasValue) + { + physical.WriteBulkString("M"u8); + physical.WriteBulkString(maxConnections.GetValueOrDefault()); + } + } + + protected abstract void WriteAttributes(PhysicalConnection physical); + + internal sealed class VectorSetAddMemberMessage( + int db, + CommandFlags flags, + RedisKey key, + int? reducedDimensions, + VectorSetQuantization quantization, + int? buildExplorationFactor, + int? maxConnections, + bool useCheckAndSet, + RedisValue element, + ReadOnlyMemory values, + string? attributesJson) : VectorSetAddMessage( + db, + flags, + key, + reducedDimensions, + quantization, + buildExplorationFactor, + maxConnections, + useCheckAndSet) + { + private readonly string? _attributesJson = string.IsNullOrWhiteSpace(attributesJson) ? null : attributesJson; + public override int GetElementArgCount(bool packed) + => 2 // "FP32 {vector}" or "VALUES {num}" + + (packed ? 0 : values.Length); // {vector...}" + + public override int GetAttributeArgCount() + => _attributesJson is null ? 0 : 2; // [SETATTR {attributes}] + + protected override void WriteElement(bool packed, PhysicalConnection physical) + { + if (packed) + { + physical.WriteBulkString("FP32"u8); + physical.WriteBulkString(MemoryMarshal.AsBytes(values.Span)); + } + else + { + physical.WriteBulkString("VALUES"u8); + physical.WriteBulkString(values.Length); + foreach (var val in values.Span) + { + physical.WriteBulkString(val); + } + } + + physical.WriteBulkString(element); + } + + protected override void WriteAttributes(PhysicalConnection physical) + { + if (_attributesJson is not null) + { + physical.WriteBulkString("SETATTR"u8); + physical.WriteBulkString(_attributesJson); + } + } + } +} diff --git a/src/StackExchange.Redis/VectorSetAddRequest.cs b/src/StackExchange.Redis/VectorSetAddRequest.cs new file mode 100644 index 000000000..987118c09 --- /dev/null +++ b/src/StackExchange.Redis/VectorSetAddRequest.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis; + +/// +/// Represents the request for a vectorset add operation. +/// +[Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] +public abstract class VectorSetAddRequest +{ + // polymorphism left open for future, but needs to be handled internally + internal VectorSetAddRequest() + { + } + + /// + /// Add a member to the vectorset. + /// + /// The element name. + /// The vector data. + /// Optional JSON attributes for the element (SETATTR parameter). + public static VectorSetAddRequest Member( + RedisValue element, + ReadOnlyMemory values, +#if NET7_0_OR_GREATER + [StringSyntax(StringSyntaxAttribute.Json)] +#endif + string? attributesJson = null) + => new VectorSetAddMemberRequest(element, values, attributesJson); + + /// + /// Optional check-and-set mode for partial threading (CAS parameter). + /// + public bool UseCheckAndSet { get; set; } + + /// + /// Optional dimension reduction using random projection (REDUCE parameter). + /// + public int? ReducedDimensions { get; set; } + + /// + /// Quantization type - Int8 (Q8), None (NOQUANT), or Binary (BIN). Default: Int8. + /// + public VectorSetQuantization Quantization { get; set; } = VectorSetQuantization.Int8; + + /// + /// Optional HNSW build exploration factor (EF parameter, default: 200). + /// + public int? BuildExplorationFactor { get; set; } + + /// + /// Optional maximum connections per HNSW node (M parameter, default: 16). + /// + public int? MaxConnections { get; set; } + + // snapshot the values; I don't trust people not to mutate the object behind my back + internal abstract VectorSetAddMessage ToMessage(RedisKey key, int db, CommandFlags flags); + + internal sealed class VectorSetAddMemberRequest( + RedisValue element, + ReadOnlyMemory values, + string? attributesJson) + : VectorSetAddRequest + { + internal override VectorSetAddMessage ToMessage(RedisKey key, int db, CommandFlags flags) + => new VectorSetAddMessage.VectorSetAddMemberMessage( + db, + flags, + key, + ReducedDimensions, + Quantization, + BuildExplorationFactor, + MaxConnections, + UseCheckAndSet, + element, + values, + attributesJson); + } +} diff --git a/src/StackExchange.Redis/VectorSetInfo.cs b/src/StackExchange.Redis/VectorSetInfo.cs new file mode 100644 index 000000000..c9277eae5 --- /dev/null +++ b/src/StackExchange.Redis/VectorSetInfo.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis; + +/// +/// Contains metadata information about a vectorset returned by VINFO command. +/// +[Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] +public readonly struct VectorSetInfo( + VectorSetQuantization quantization, + string? quantizationRaw, + int dimension, + long length, + int maxLevel, + long vectorSetUid, + long hnswMaxNodeUid) +{ + /// + /// The quantization type used for vectors in this vectorset. + /// + public VectorSetQuantization Quantization { get; } = quantization; + + /// + /// The raw representation of the quantization type used for vectors in this vectorset. This is only + /// populated if the is . + /// + public string? QuantizationRaw { get; } = quantizationRaw; + + /// + /// The number of dimensions in each vector. + /// + public int Dimension { get; } = dimension; + + /// + /// The number of elements (cardinality) in the vectorset. + /// + public long Length { get; } = length; + + /// + /// The maximum level in the HNSW graph structure. + /// + public int MaxLevel { get; } = maxLevel; + + /// + /// The unique identifier for this vectorset. + /// + public long VectorSetUid { get; } = vectorSetUid; + + /// + /// The maximum node unique identifier in the HNSW graph. + /// + public long HnswMaxNodeUid { get; } = hnswMaxNodeUid; +} diff --git a/src/StackExchange.Redis/VectorSetLink.cs b/src/StackExchange.Redis/VectorSetLink.cs new file mode 100644 index 000000000..c18e8a95f --- /dev/null +++ b/src/StackExchange.Redis/VectorSetLink.cs @@ -0,0 +1,24 @@ +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis; + +/// +/// Represents a link/connection between members in a vectorset with similarity score. +/// Used by VLINKS command with WITHSCORES option. +/// +[Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] +public readonly struct VectorSetLink(RedisValue member, double score) +{ + /// + /// The linked member name/identifier. + /// + public RedisValue Member { get; } = member; + + /// + /// The similarity score between the queried member and this linked member. + /// + public double Score { get; } = score; + + /// + public override string ToString() => $"{Member}: {Score}"; +} diff --git a/src/StackExchange.Redis/VectorSetQuantization.cs b/src/StackExchange.Redis/VectorSetQuantization.cs new file mode 100644 index 000000000..d78f4b34b --- /dev/null +++ b/src/StackExchange.Redis/VectorSetQuantization.cs @@ -0,0 +1,30 @@ +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis; + +/// +/// Specifies the quantization type for vectors in a vectorset. +/// +[Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] +public enum VectorSetQuantization +{ + /// + /// Unknown or unrecognized quantization type. + /// + Unknown = 0, + + /// + /// No quantization (full precision). This maps to "NOQUANT" or "f32". + /// + None = 1, + + /// + /// 8-bit integer quantization (default). This maps to "Q8" or "int8". + /// + Int8 = 2, + + /// + /// Binary quantization. This maps to "BIN" or "bin". + /// + Binary = 3, +} diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs new file mode 100644 index 000000000..1bbc418d5 --- /dev/null +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchMessage.cs @@ -0,0 +1,263 @@ +using System; + +namespace StackExchange.Redis; + +internal abstract class VectorSetSimilaritySearchMessage( + int db, + CommandFlags flags, + VectorSetSimilaritySearchMessage.VsimFlags vsimFlags, + RedisKey key, + int count, + double epsilon, + int searchExplorationFactor, + string? filterExpression, + int maxFilteringEffort) : Message(db, flags, RedisCommand.VSIM) +{ + // For "FP32" and "VALUES" scenarios; in the future we might want other vector sizes / encodings - for + // example, there could be some "FP16" or "FP8" transport that requires a ROM-short or ROM-sbyte from + // the calling code. Or, as a convenience, we might want to allow ROM-double input, but transcode that + // to FP32 on the way out. + internal sealed class VectorSetSimilaritySearchBySingleVectorMessage( + int db, + CommandFlags flags, + VsimFlags vsimFlags, + RedisKey key, + ReadOnlyMemory vector, + int count, + double epsilon, + int searchExplorationFactor, + string? filterExpression, + int maxFilteringEffort) : VectorSetSimilaritySearchMessage(db, flags, vsimFlags, key, count, epsilon, + searchExplorationFactor, filterExpression, maxFilteringEffort) + { + internal override int GetSearchTargetArgCount(bool packed) => + packed ? 2 : 2 + vector.Length; // FP32 {vector} or VALUES {num} {vector} + + internal override void WriteSearchTarget(bool packed, PhysicalConnection physical) + { + if (packed) + { + physical.WriteBulkString("FP32"u8); + physical.WriteBulkString(System.Runtime.InteropServices.MemoryMarshal.AsBytes(vector.Span)); + } + else + { + physical.WriteBulkString("VALUES"u8); + physical.WriteBulkString(vector.Length); + foreach (var val in vector.Span) + { + physical.WriteBulkString(val); + } + } + } + } + + // for "ELE" scenarios + internal sealed class VectorSetSimilaritySearchByMemberMessage( + int db, + CommandFlags flags, + VsimFlags vsimFlags, + RedisKey key, + RedisValue member, + int count, + double epsilon, + int searchExplorationFactor, + string? filterExpression, + int maxFilteringEffort) : VectorSetSimilaritySearchMessage(db, flags, vsimFlags, key, count, epsilon, + searchExplorationFactor, filterExpression, maxFilteringEffort) + { + internal override int GetSearchTargetArgCount(bool packed) => 2; // ELE {member} + + internal override void WriteSearchTarget(bool packed, PhysicalConnection physical) + { + physical.WriteBulkString("ELE"u8); + physical.WriteBulkString(member); + } + } + + internal abstract int GetSearchTargetArgCount(bool packed); + internal abstract void WriteSearchTarget(bool packed, PhysicalConnection physical); + + public ResultProcessor?> GetResultProcessor() => + VectorSetSimilaritySearchProcessor.Instance; + + private sealed class VectorSetSimilaritySearchProcessor : ResultProcessor?> + { + // keep local, since we need to know what flags were being sent + public static readonly VectorSetSimilaritySearchProcessor Instance = new(); + private VectorSetSimilaritySearchProcessor() { } + + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.Resp2TypeArray == ResultType.Array && message is VectorSetSimilaritySearchMessage vssm) + { + if (result.IsNull) + { + SetResult(message, null); + return true; + } + + bool withScores = vssm.HasFlag(VsimFlags.WithScores); + bool withAttribs = vssm.HasFlag(VsimFlags.WithAttributes); + + // in RESP3 mode (only), when both are requested, we get a sub-array per item; weird, but true + bool internalNesting = withScores && withAttribs && connection.Protocol is RedisProtocol.Resp3; + + int rowsPerItem = internalNesting + ? 2 + : 1 + ((withScores ? 1 : 0) + (withAttribs ? 1 : 0)); // each value is separate root element + + var items = result.GetItems(); + var length = checked((int)items.Length) / rowsPerItem; + var lease = Lease.Create(length, clear: false); + var target = lease.Span; + int count = 0; + var iter = items.GetEnumerator(); + for (int i = 0; i < target.Length && iter.MoveNext(); i++) + { + var member = iter.Current.AsRedisValue(); + double score = double.NaN; + string? attributesJson = null; + + if (internalNesting) + { + if (!iter.MoveNext() || iter.Current.Resp2TypeArray != ResultType.Array) break; + if (!iter.Current.IsNull) + { + var subArray = iter.Current.GetItems(); + if (subArray.Length >= 1 && !subArray[0].TryGetDouble(out score)) break; + if (subArray.Length >= 2) attributesJson = subArray[1].GetString(); + } + } + else + { + if (withScores) + { + if (!iter.MoveNext() || !iter.Current.TryGetDouble(out score)) break; + } + + if (withAttribs) + { + if (!iter.MoveNext()) break; + attributesJson = iter.Current.GetString(); + } + } + + target[i] = new VectorSetSimilaritySearchResult(member, score, attributesJson); + count++; + } + + if (count == target.Length) + { + SetResult(message, lease); + return true; + } + + lease.Dispose(); // failed to fill? + } + + return false; + } + } + + [Flags] + internal enum VsimFlags + { + None = 0, + Count = 1 << 0, + WithScores = 1 << 1, + WithAttributes = 1 << 2, + UseExactSearch = 1 << 3, + DisableThreading = 1 << 4, + Epsilon = 1 << 5, + SearchExplorationFactor = 1 << 6, + MaxFilteringEffort = 1 << 7, + FilterExpression = 1 << 8, + } + + private bool HasFlag(VsimFlags flag) => (vsimFlags & flag) != 0; + + public override int ArgCount => GetArgCount(VectorSetAddMessage.UseFp32); + + private int GetArgCount(bool packed) + { + int argCount = 1 + GetSearchTargetArgCount(packed); // {key} and whatever we need for the vector/element portion + if (HasFlag(VsimFlags.WithScores)) argCount++; // [WITHSCORES] + if (HasFlag(VsimFlags.WithAttributes)) argCount++; // [WITHATTRIBS] + if (HasFlag(VsimFlags.Count)) argCount += 2; // [COUNT {count}] + if (HasFlag(VsimFlags.Epsilon)) argCount += 2; // [EPSILON {epsilon}] + if (HasFlag(VsimFlags.SearchExplorationFactor)) argCount += 2; // [EF {search-exploration-factor}] + if (HasFlag(VsimFlags.FilterExpression)) argCount += 2; // [FILTER {filterExpression}] + if (HasFlag(VsimFlags.MaxFilteringEffort)) argCount += 2; // [FILTER-EF {max-filtering-effort}] + if (HasFlag(VsimFlags.UseExactSearch)) argCount++; // [TRUTH] + if (HasFlag(VsimFlags.DisableThreading)) argCount++; // [NOTHREAD] + return argCount; + } + + protected override void WriteImpl(PhysicalConnection physical) + { + // snapshot to avoid race in debug scenarios + bool packed = VectorSetAddMessage.UseFp32; + physical.WriteHeader(Command, GetArgCount(packed)); + + // Write key + physical.Write(key); + + // Write search target: either "ELE {member}" or vector data + WriteSearchTarget(packed, physical); + + if (HasFlag(VsimFlags.WithScores)) + { + physical.WriteBulkString("WITHSCORES"u8); + } + + if (HasFlag(VsimFlags.WithAttributes)) + { + physical.WriteBulkString("WITHATTRIBS"u8); + } + + // Write optional parameters + if (HasFlag(VsimFlags.Count)) + { + physical.WriteBulkString("COUNT"u8); + physical.WriteBulkString(count); + } + + if (HasFlag(VsimFlags.Epsilon)) + { + physical.WriteBulkString("EPSILON"u8); + physical.WriteBulkString(epsilon); + } + + if (HasFlag(VsimFlags.SearchExplorationFactor)) + { + physical.WriteBulkString("EF"u8); + physical.WriteBulkString(searchExplorationFactor); + } + + if (HasFlag(VsimFlags.FilterExpression)) + { + physical.WriteBulkString("FILTER"u8); + physical.WriteBulkString(filterExpression); + } + + if (HasFlag(VsimFlags.MaxFilteringEffort)) + { + physical.WriteBulkString("FILTER-EF"u8); + physical.WriteBulkString(maxFilteringEffort); + } + + if (HasFlag(VsimFlags.UseExactSearch)) + { + physical.WriteBulkString("TRUTH"u8); + } + + if (HasFlag(VsimFlags.DisableThreading)) + { + physical.WriteBulkString("NOTHREAD"u8); + } + } + + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) + => serverSelectionStrategy.HashSlot(key); +} diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs new file mode 100644 index 000000000..d0c0fd4cc --- /dev/null +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs @@ -0,0 +1,219 @@ +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using VsimFlags = StackExchange.Redis.VectorSetSimilaritySearchMessage.VsimFlags; + +namespace StackExchange.Redis; + +/// +/// Represents the request for a vector similarity search operation. +/// +[Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] +public abstract class VectorSetSimilaritySearchRequest +{ + internal VectorSetSimilaritySearchRequest() + { + } // polymorphism left open for future, but needs to be handled internally + + private sealed class VectorSetSimilarityByMemberSearchRequest(RedisValue member) : VectorSetSimilaritySearchRequest + { + internal override VectorSetSimilaritySearchMessage ToMessage(RedisKey key, int db, CommandFlags flags) + => new VectorSetSimilaritySearchMessage.VectorSetSimilaritySearchByMemberMessage( + db, + flags, + _vsimFlags, + key, + member, + _count, + _epsilon, + _searchExplorationFactor, + _filterExpression, + _maxFilteringEffort); + } + + private sealed class VectorSetSimilarityVectorSingleSearchRequest(ReadOnlyMemory vector) + : VectorSetSimilaritySearchRequest + { + internal override VectorSetSimilaritySearchMessage ToMessage(RedisKey key, int db, CommandFlags flags) + => new VectorSetSimilaritySearchMessage.VectorSetSimilaritySearchBySingleVectorMessage( + db, + flags, + _vsimFlags, + key, + vector, + _count, + _epsilon, + _searchExplorationFactor, + _filterExpression, + _maxFilteringEffort); + } + + // snapshot the values; I don't trust people not to mutate the object behind my back + internal abstract VectorSetSimilaritySearchMessage ToMessage(RedisKey key, int db, CommandFlags flags); + + /// + /// Create a request to search by an existing member in the index. + /// + /// The member to search for. + public static VectorSetSimilaritySearchRequest ByMember(RedisValue member) + => new VectorSetSimilarityByMemberSearchRequest(member); + + /// + /// Create a request to search by a vector value. + /// + /// The vector value to search for. + public static VectorSetSimilaritySearchRequest ByVector(ReadOnlyMemory vector) + => new VectorSetSimilarityVectorSingleSearchRequest(vector); + + private VsimFlags _vsimFlags; + + // use the flags to reduce storage from N*Nullable + private int _searchExplorationFactor, _maxFilteringEffort, _count; + private double _epsilon; + + private bool HasFlag(VsimFlags flag) => (_vsimFlags & flag) != 0; + + private void SetFlag(VsimFlags flag, bool value) + { + if (value) + { + _vsimFlags |= flag; + } + else + { + _vsimFlags &= ~flag; + } + } + + /// + /// The number of similar vectors to return (COUNT parameter). + /// + public int? Count + { + get => HasFlag(VsimFlags.Count) ? _count : null; + set + { + if (value.HasValue) + { + _count = value.GetValueOrDefault(); + SetFlag(VsimFlags.Count, true); + } + else + { + SetFlag(VsimFlags.Count, false); + } + } + } + + /// + /// Whether to include similarity scores in the results (WITHSCORES parameter). + /// + public bool WithScores + { + get => HasFlag(VsimFlags.WithScores); + set => SetFlag(VsimFlags.WithScores, value); + } + + /// + /// Whether to include JSON attributes in the results (WITHATTRIBS parameter). + /// + public bool WithAttributes + { + get => HasFlag(VsimFlags.WithAttributes); + set => SetFlag(VsimFlags.WithAttributes, value); + } + + /// + /// Optional similarity threshold - only return elements with similarity >= (1 - epsilon) (EPSILON parameter). + /// + public double? Epsilon + { + get => HasFlag(VsimFlags.Epsilon) ? _epsilon : null; + set + { + if (value.HasValue) + { + _epsilon = value.GetValueOrDefault(); + SetFlag(VsimFlags.Epsilon, true); + } + else + { + SetFlag(VsimFlags.Epsilon, false); + } + } + } + + /// + /// Optional search exploration factor for better recall (EF parameter). + /// + public int? SearchExplorationFactor + { + get => HasFlag(VsimFlags.SearchExplorationFactor) ? _searchExplorationFactor : null; + set + { + if (value.HasValue) + { + _searchExplorationFactor = value.GetValueOrDefault(); + SetFlag(VsimFlags.SearchExplorationFactor, true); + } + else + { + SetFlag(VsimFlags.SearchExplorationFactor, false); + } + } + } + + /// + /// Optional maximum filtering attempts (FILTER-EF parameter). + /// + public int? MaxFilteringEffort + { + get => HasFlag(VsimFlags.MaxFilteringEffort) ? _maxFilteringEffort : null; + set + { + if (value.HasValue) + { + _maxFilteringEffort = value.GetValueOrDefault(); + SetFlag(VsimFlags.MaxFilteringEffort, true); + } + else + { + SetFlag(VsimFlags.MaxFilteringEffort, false); + } + } + } + + private string? _filterExpression; + + /// + /// Optional filter expression to restrict results (FILTER parameter); . + /// + public string? FilterExpression + { + get => _filterExpression; + set + { + _filterExpression = value; + SetFlag(VsimFlags.FilterExpression, !string.IsNullOrWhiteSpace(value)); + } + } + + /// + /// Whether to use exact linear scan instead of HNSW (TRUTH parameter). + /// + public bool UseExactSearch + { + get => HasFlag(VsimFlags.UseExactSearch); + set => SetFlag(VsimFlags.UseExactSearch, value); + } + + /// + /// Whether to run search in main thread (NOTHREAD parameter). + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced)] + public bool DisableThreading + { + get => HasFlag(VsimFlags.DisableThreading); + set => SetFlag(VsimFlags.DisableThreading, value); + } +} diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs new file mode 100644 index 000000000..fd912898b --- /dev/null +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs @@ -0,0 +1,44 @@ +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis; + +/// +/// Represents a result from vector similarity search operations. +/// +[Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] +public readonly struct VectorSetSimilaritySearchResult(RedisValue member, double score = double.NaN, string? attributesJson = null) +{ + /// + /// The member name/identifier in the vectorset. + /// + public RedisValue Member { get; } = member; + + /// + /// The similarity score (0-1) when WITHSCORES is used, NaN otherwise. + /// A score of 1 means identical vectors, 0 means opposite vectors. + /// + public double Score { get; } = score; + + /// + /// The JSON attributes associated with the member when WITHATTRIBS is used, null otherwise. + /// +#if NET7_0_OR_GREATER + [StringSyntax(StringSyntaxAttribute.Json)] +#endif + public string? AttributesJson { get; } = attributesJson; + + /// + public override string ToString() + { + if (double.IsNaN(Score)) + { + return AttributesJson is null + ? Member.ToString() + : $"{Member}: {AttributesJson}"; + } + + return AttributesJson is null + ? $"{Member} ({Score})" + : $"{Member} ({Score}): {AttributesJson}"; + } +} diff --git a/tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs b/tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs new file mode 100644 index 000000000..78877f163 --- /dev/null +++ b/tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs @@ -0,0 +1,139 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Text; +using BenchmarkDotNet.Attributes; + +namespace StackExchange.Redis.Benchmarks; + +[Config(typeof(CustomConfig))] +public class FastHashBenchmarks +{ + private const string SharedString = "some-typical-data-for-comparisons"; + private static readonly byte[] SharedUtf8; + private static readonly ReadOnlySequence SharedMultiSegment; + + static FastHashBenchmarks() + { + SharedUtf8 = Encoding.UTF8.GetBytes(SharedString); + + var first = new Segment(SharedUtf8.AsMemory(0, 1), null); + var second = new Segment(SharedUtf8.AsMemory(1), first); + SharedMultiSegment = new ReadOnlySequence(first, 0, second, second.Memory.Length); + } + + private sealed class Segment : ReadOnlySequenceSegment + { + public Segment(ReadOnlyMemory memory, Segment? previous) + { + Memory = memory; + if (previous is { }) + { + RunningIndex = previous.RunningIndex + previous.Memory.Length; + previous.Next = this; + } + } + } + + private string _sourceString = SharedString; + private ReadOnlyMemory _sourceBytes = SharedUtf8; + private ReadOnlySequence _sourceMultiSegmentBytes = SharedMultiSegment; + private ReadOnlySequence SingleSegmentBytes => new(_sourceBytes); + + [GlobalSetup] + public void Setup() + { + _sourceString = SharedString.Substring(0, Size); + _sourceBytes = SharedUtf8.AsMemory(0, Size); + _sourceMultiSegmentBytes = SharedMultiSegment.Slice(0, Size); + +#pragma warning disable CS0618 // Type or member is obsolete + var bytes = _sourceBytes.Span; + var expected = FastHash.Hash64Fallback(bytes); + + Assert(bytes.Hash64(), nameof(FastHash.Hash64)); + Assert(FastHash.Hash64Unsafe(bytes), nameof(FastHash.Hash64Unsafe)); +#pragma warning restore CS0618 // Type or member is obsolete + Assert(SingleSegmentBytes.Hash64(), nameof(FastHash.Hash64) + " (single segment)"); + Assert(_sourceMultiSegmentBytes.Hash64(), nameof(FastHash.Hash64) + " (multi segment)"); + + void Assert(long actual, string name) + { + if (actual != expected) + { + throw new InvalidOperationException($"Hash mismatch for {name}, {expected} != {actual}"); + } + } + } + + [ParamsSource(nameof(Sizes))] + public int Size { get; set; } = 7; + + public IEnumerable Sizes => [0, 1, 2, 3, 4, 5, 6, 7, 8, 16]; + + private const int OperationsPerInvoke = 1024; + + [Benchmark(OperationsPerInvoke = OperationsPerInvoke, Baseline = true)] + public void String() + { + var val = _sourceString; + for (int i = 0; i < OperationsPerInvoke; i++) + { + _ = val.GetHashCode(); + } + } + + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public void Hash64() + { + var val = _sourceBytes.Span; + for (int i = 0; i < OperationsPerInvoke; i++) + { + _ = val.Hash64(); + } + } + + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public void Hash64Unsafe() + { + var val = _sourceBytes.Span; + for (int i = 0; i < OperationsPerInvoke; i++) + { +#pragma warning disable CS0618 // Type or member is obsolete + _ = FastHash.Hash64Unsafe(val); +#pragma warning restore CS0618 // Type or member is obsolete + } + } + + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public void Hash64Fallback() + { + var val = _sourceBytes.Span; + for (int i = 0; i < OperationsPerInvoke; i++) + { +#pragma warning disable CS0618 // Type or member is obsolete + _ = FastHash.Hash64Fallback(val); +#pragma warning restore CS0618 // Type or member is obsolete + } + } + + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public void Hash64_SingleSegment() + { + var val = SingleSegmentBytes; + for (int i = 0; i < OperationsPerInvoke; i++) + { + _ = val.Hash64(); + } + } + + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public void Hash64_MultiSegment() + { + var val = _sourceMultiSegmentBytes; + for (int i = 0; i < OperationsPerInvoke; i++) + { + _ = val.Hash64(); + } + } +} diff --git a/tests/StackExchange.Redis.Benchmarks/Program.cs b/tests/StackExchange.Redis.Benchmarks/Program.cs index 622d7d593..311202877 100644 --- a/tests/StackExchange.Redis.Benchmarks/Program.cs +++ b/tests/StackExchange.Redis.Benchmarks/Program.cs @@ -1,10 +1,25 @@ -using System.Reflection; +using System; +using System.Reflection; using BenchmarkDotNet.Running; namespace StackExchange.Redis.Benchmarks { internal static class Program { - private static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); + private static void Main(string[] args) + { +#if DEBUG + var obj = new FastHashBenchmarks(); + foreach (var size in obj.Sizes) + { + Console.WriteLine($"Size: {size}"); + obj.Size = size; + obj.Setup(); + obj.Hash64(); + } +#else + BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); +#endif + } } } diff --git a/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj b/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj index be9a3081b..8b335ab02 100644 --- a/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj +++ b/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj @@ -5,6 +5,7 @@ Release Exe true + enable diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 801565a83..0c9286e17 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -442,6 +442,16 @@ public async Task GetInfo() } var cpuCount = cpu.Count(); Assert.True(cpuCount > 2); + if (cpu.Key != "CPU") + { + // seem to be seeing this in logs; add lots of detail + var sb = new StringBuilder("Expected CPU, got ").AppendLine(cpu.Key); + foreach (var setting in cpu) + { + sb.Append(setting.Key).Append('=').AppendLine(setting.Value); + } + Assert.Fail(sb.ToString()); + } Assert.Equal("CPU", cpu.Key); Assert.Contains(cpu, x => x.Key == "used_cpu_sys"); Assert.Contains(cpu, x => x.Key == "used_cpu_user"); diff --git a/tests/StackExchange.Redis.Tests/FastHashTests.cs b/tests/StackExchange.Redis.Tests/FastHashTests.cs new file mode 100644 index 000000000..418198cfd --- /dev/null +++ b/tests/StackExchange.Redis.Tests/FastHashTests.cs @@ -0,0 +1,112 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using Xunit; + +#pragma warning disable CS8981, SA1134, SA1300, SA1303, SA1502 // names are weird in this test! +// ReSharper disable InconsistentNaming - to better represent expected literals +// ReSharper disable IdentifierTypo +namespace StackExchange.Redis.Tests; + +public partial class FastHashTests +{ + // note: if the hashing algorithm changes, we can update the last parameter freely; it doesn't matter + // what it *is* - what matters is that we can see that it has entropy between different values + [Theory] + [InlineData(1, a.Length, a.Text, a.Hash, 97)] + [InlineData(2, ab.Length, ab.Text, ab.Hash, 25185)] + [InlineData(3, abc.Length, abc.Text, abc.Hash, 6513249)] + [InlineData(4, abcd.Length, abcd.Text, abcd.Hash, 1684234849)] + [InlineData(5, abcde.Length, abcde.Text, abcde.Hash, 435475931745)] + [InlineData(6, abcdef.Length, abcdef.Text, abcdef.Hash, 112585661964897)] + [InlineData(7, abcdefg.Length, abcdefg.Text, abcdefg.Hash, 29104508263162465)] + [InlineData(8, abcdefgh.Length, abcdefgh.Text, abcdefgh.Hash, 7523094288207667809)] + + [InlineData(1, x.Length, x.Text, x.Hash, 120)] + [InlineData(2, xx.Length, xx.Text, xx.Hash, 30840)] + [InlineData(3, xxx.Length, xxx.Text, xxx.Hash, 7895160)] + [InlineData(4, xxxx.Length, xxxx.Text, xxxx.Hash, 2021161080)] + [InlineData(5, xxxxx.Length, xxxxx.Text, xxxxx.Hash, 517417236600)] + [InlineData(6, xxxxxx.Length, xxxxxx.Text, xxxxxx.Hash, 132458812569720)] + [InlineData(7, xxxxxxx.Length, xxxxxxx.Text, xxxxxxx.Hash, 33909456017848440)] + [InlineData(8, xxxxxxxx.Length, xxxxxxxx.Text, xxxxxxxx.Hash, 8680820740569200760)] + + [InlineData(3, 窓.Length, 窓.Text, 窓.Hash, 9677543, "窓")] + [InlineData(20, abcdefghijklmnopqrst.Length, abcdefghijklmnopqrst.Text, abcdefghijklmnopqrst.Hash, 7523094288207667809)] + + // show that foo_bar is interpreted as foo-bar + [InlineData(7, foo_bar.Length, foo_bar.Text, foo_bar.Hash, 32195221641981798, "foo-bar", nameof(foo_bar))] + [InlineData(7, foo_bar_hyphen.Length, foo_bar_hyphen.Text, foo_bar_hyphen.Hash, 32195221641981798, "foo-bar", nameof(foo_bar_hyphen))] + [InlineData(7, foo_bar_underscore.Length, foo_bar_underscore.Text, foo_bar_underscore.Hash, 32195222480842598, "foo_bar", nameof(foo_bar_underscore))] + public void Validate(int expectedLength, int actualLength, string actualValue, long actualHash, long expectedHash, string? expectedValue = null, string originForDisambiguation = "") + { + _ = originForDisambiguation; // to allow otherwise-identical test data to coexist + Assert.Equal(expectedLength, actualLength); + Assert.Equal(expectedHash, actualHash); + var bytes = Encoding.UTF8.GetBytes(actualValue); + Assert.Equal(expectedLength, bytes.Length); + Assert.Equal(expectedHash, FastHash.Hash64(bytes)); +#pragma warning disable CS0618 // Type or member is obsolete + Assert.Equal(expectedHash, FastHash.Hash64Fallback(bytes)); +#pragma warning restore CS0618 // Type or member is obsolete + if (expectedValue is not null) + { + Assert.Equal(expectedValue, actualValue); + } + } + + [Fact] + public void FastHashIs_Short() + { + ReadOnlySpan value = "abc"u8; + var hash = value.Hash64(); + Assert.Equal(abc.Hash, hash); + Assert.True(abc.Is(hash, value)); + + value = "abz"u8; + hash = value.Hash64(); + Assert.NotEqual(abc.Hash, hash); + Assert.False(abc.Is(hash, value)); + } + + [Fact] + public void FastHashIs_Long() + { + ReadOnlySpan value = "abcdefghijklmnopqrst"u8; + var hash = value.Hash64(); + Assert.Equal(abcdefghijklmnopqrst.Hash, hash); + Assert.True(abcdefghijklmnopqrst.Is(hash, value)); + + value = "abcdefghijklmnopqrsz"u8; + hash = value.Hash64(); + Assert.Equal(abcdefghijklmnopqrst.Hash, hash); // hash collision, fine + Assert.False(abcdefghijklmnopqrst.Is(hash, value)); + } + + [FastHash] private static partial class a { } + [FastHash] private static partial class ab { } + [FastHash] private static partial class abc { } + [FastHash] private static partial class abcd { } + [FastHash] private static partial class abcde { } + [FastHash] private static partial class abcdef { } + [FastHash] private static partial class abcdefg { } + [FastHash] private static partial class abcdefgh { } + + [FastHash] private static partial class abcdefghijklmnopqrst { } + + // show that foo_bar and foo-bar are different + [FastHash] private static partial class foo_bar { } + [FastHash("foo-bar")] private static partial class foo_bar_hyphen { } + [FastHash("foo_bar")] private static partial class foo_bar_underscore { } + + [FastHash] private static partial class 窓 { } + + [FastHash] private static partial class x { } + [FastHash] private static partial class xx { } + [FastHash] private static partial class xxx { } + [FastHash] private static partial class xxxx { } + [FastHash] private static partial class xxxxx { } + [FastHash] private static partial class xxxxxx { } + [FastHash] private static partial class xxxxxxx { } + [FastHash] private static partial class xxxxxxxx { } +} diff --git a/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs new file mode 100644 index 000000000..b4ff2091b --- /dev/null +++ b/tests/StackExchange.Redis.Tests/KeyPrefixedVectorSetTests.cs @@ -0,0 +1,214 @@ +using System; +using System.Text; +using NSubstitute; +using Xunit; + +namespace StackExchange.Redis.Tests +{ + [Collection(nameof(SubstituteDependentCollection))] + public sealed class KeyPrefixedVectorSetTests + { + private readonly IDatabase mock; + private readonly IDatabase prefixed; + + public KeyPrefixedVectorSetTests() + { + mock = Substitute.For(); + prefixed = new KeyspaceIsolation.KeyPrefixedDatabase(mock, Encoding.UTF8.GetBytes("prefix:")); + } + + [Fact] + public void VectorSetAdd_Fp32() + { + if (BitConverter.IsLittleEndian) + { + Assert.True(VectorSetAddMessage.UseFp32); +#if DEBUG // can be suppressed + VectorSetAddMessage.SuppressFp32(); + Assert.False(VectorSetAddMessage.UseFp32); + VectorSetAddMessage.RestoreFp32(); + Assert.True(VectorSetAddMessage.UseFp32); +#endif + } + else + { + Assert.False(VectorSetAddMessage.UseFp32); + } + } + + [Fact] + public void VectorSetAdd_BasicCall() + { + var vector = new[] { 1.0f, 2.0f, 3.0f }.AsMemory(); + + var request = VectorSetAddRequest.Member("element1", vector); + prefixed.VectorSetAdd("vectorset", request); + + mock.Received().VectorSetAdd( + "prefix:vectorset", + request); + } + + [Fact] + public void VectorSetAdd_WithAllParameters() + { + var vector = new[] { 1.0f, 2.0f, 3.0f }.AsMemory(); + var attributes = """{"category":"test"}"""; + + var request = VectorSetAddRequest.Member( + "element1", + vector, + attributes); + request.ReducedDimensions = 64; + request.Quantization = VectorSetQuantization.Binary; + request.BuildExplorationFactor = 300; + request.MaxConnections = 32; + request.UseCheckAndSet = true; + prefixed.VectorSetAdd( + "vectorset", + request, + flags: CommandFlags.FireAndForget); + + mock.Received().VectorSetAdd( + "prefix:vectorset", + request, + CommandFlags.FireAndForget); + } + + [Fact] + public void VectorSetLength() + { + prefixed.VectorSetLength("vectorset"); + mock.Received().VectorSetLength("prefix:vectorset"); + } + + [Fact] + public void VectorSetDimension() + { + prefixed.VectorSetDimension("vectorset"); + mock.Received().VectorSetDimension("prefix:vectorset"); + } + + [Fact] + public void VectorSetGetApproximateVector() + { + prefixed.VectorSetGetApproximateVector("vectorset", "member1"); + mock.Received().VectorSetGetApproximateVector("prefix:vectorset", "member1"); + } + + [Fact] + public void VectorSetGetAttributesJson() + { + prefixed.VectorSetGetAttributesJson("vectorset", "member1"); + mock.Received().VectorSetGetAttributesJson("prefix:vectorset", "member1"); + } + + [Fact] + public void VectorSetInfo() + { + prefixed.VectorSetInfo("vectorset"); + mock.Received().VectorSetInfo("prefix:vectorset"); + } + + [Fact] + public void VectorSetContains() + { + prefixed.VectorSetContains("vectorset", "member1"); + mock.Received().VectorSetContains("prefix:vectorset", "member1"); + } + + [Fact] + public void VectorSetGetLinks() + { + prefixed.VectorSetGetLinks("vectorset", "member1"); + mock.Received().VectorSetGetLinks("prefix:vectorset", "member1"); + } + + [Fact] + public void VectorSetGetLinksWithScores() + { + prefixed.VectorSetGetLinksWithScores("vectorset", "member1"); + mock.Received().VectorSetGetLinksWithScores("prefix:vectorset", "member1"); + } + + [Fact] + public void VectorSetRandomMember() + { + prefixed.VectorSetRandomMember("vectorset"); + mock.Received().VectorSetRandomMember("prefix:vectorset"); + } + + [Fact] + public void VectorSetRandomMembers() + { + prefixed.VectorSetRandomMembers("vectorset", 5); + mock.Received().VectorSetRandomMembers("prefix:vectorset", 5); + } + + [Fact] + public void VectorSetRemove() + { + prefixed.VectorSetRemove("vectorset", "member1"); + mock.Received().VectorSetRemove("prefix:vectorset", "member1"); + } + + [Fact] + public void VectorSetSetAttributesJson() + { + var attributes = """{"category":"test"}"""; + + prefixed.VectorSetSetAttributesJson("vectorset", "member1", attributes); + mock.Received().VectorSetSetAttributesJson("prefix:vectorset", "member1", attributes); + } + + [Fact] + public void VectorSetSimilaritySearchByVector() + { + var vector = new[] { 1.0f, 2.0f, 3.0f }.AsMemory(); + + var query = VectorSetSimilaritySearchRequest.ByVector(vector); + prefixed.VectorSetSimilaritySearch( + "vectorset", + query); + mock.Received().VectorSetSimilaritySearch( + "prefix:vectorset", + query); + } + + [Fact] + public void VectorSetSimilaritySearchByMember() + { + var query = VectorSetSimilaritySearchRequest.ByMember("member1"); + query.Count = 5; + query.WithScores = true; + query.WithAttributes = true; + query.Epsilon = 0.1; + query.SearchExplorationFactor = 400; + query.FilterExpression = "category='test'"; + query.MaxFilteringEffort = 1000; + query.UseExactSearch = true; + query.DisableThreading = true; + prefixed.VectorSetSimilaritySearch( + "vectorset", + query, + CommandFlags.FireAndForget); + mock.Received().VectorSetSimilaritySearch( + "prefix:vectorset", + query, + CommandFlags.FireAndForget); + } + + [Fact] + public void VectorSetSimilaritySearchByVector_DefaultParameters() + { + var vector = new[] { 1.0f, 2.0f }.AsMemory(); + + // Test that default parameters work correctly + var query = VectorSetSimilaritySearchRequest.ByVector(vector); + prefixed.VectorSetSimilaritySearch("vectorset", query); + mock.Received().VectorSetSimilaritySearch( + "prefix:vectorset", + query); + } + } +} diff --git a/tests/StackExchange.Redis.Tests/NamingTests.cs b/tests/StackExchange.Redis.Tests/NamingTests.cs index d0474f782..9d9e032ad 100644 --- a/tests/StackExchange.Redis.Tests/NamingTests.cs +++ b/tests/StackExchange.Redis.Tests/NamingTests.cs @@ -193,7 +193,8 @@ private void CheckMethod(MethodInfo method, bool isAsync) || shortName.StartsWith("Script") || shortName.StartsWith("SortedSet") || shortName.StartsWith("String") - || shortName.StartsWith("Stream"); + || shortName.StartsWith("Stream") + || shortName.StartsWith("VectorSet"); Log(fullName + ": " + (isValid ? "valid" : "invalid")); Assert.True(isValid, fullName + ":Prefix"); break; diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index 50d4ae3d1..f6e38236b 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -30,5 +30,6 @@ + diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 94a14ee32..68dbb6055 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -51,7 +51,19 @@ public static void Log(TextWriter output, string message) output?.WriteLine(Time() + ": " + message); } } - protected void Log(string? message, params object[] args) => Output.WriteLine(Time() + ": " + message, args); + + protected void Log(string? message, params object[] args) + { + if (args is { Length: > 0 }) + { + Output.WriteLine(Time() + ": " + message, args); + } + else + { + // avoid "not intended as a format specifier" scenarios + Output.WriteLine(Time() + ": " + message); + } + } protected ProfiledCommandEnumerable Log(ProfilingSession session) { diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs new file mode 100644 index 000000000..12eda7147 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -0,0 +1,675 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Xunit; + +namespace StackExchange.Redis.Tests; + +[RunPerProtocol] +public sealed class VectorSetIntegrationTests(ITestOutputHelper output) : TestBase(output) +{ + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task VectorSetAdd_BasicOperation(bool suppressFp32) + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + // Clean up any existing data + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f, 4.0f }; + + if (suppressFp32) VectorSetAddMessage.SuppressFp32(); + try + { + var request = VectorSetAddRequest.Member("element1", vector.AsMemory(), null); + var result = await db.VectorSetAddAsync(key, request); + + Assert.True(result); + } + finally + { + if (suppressFp32) VectorSetAddMessage.RestoreFp32(); + } + } + + [Fact] + public async Task VectorSetAdd_WithAttributes() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f, 4.0f }; + var attributes = """{"category":"test","id":123}"""; + + var request = VectorSetAddRequest.Member("element1", vector.AsMemory(), attributes); + var result = await db.VectorSetAddAsync(key, request); + + Assert.True(result); + + // Verify attributes were stored + var retrievedAttributes = await db.VectorSetGetAttributesJsonAsync(key, "element1"); + Assert.Equal(attributes, retrievedAttributes); + } + + [Theory] + [InlineData(VectorSetQuantization.Int8)] + [InlineData(VectorSetQuantization.None)] + [InlineData(VectorSetQuantization.Binary)] + public async Task VectorSetAdd_WithEverything(VectorSetQuantization quantization) + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f, 4.0f }; + var attributes = """{"category":"test","id":123}"""; + + var request = VectorSetAddRequest.Member( + "element1", + vector.AsMemory(), + attributes); + request.Quantization = quantization; + request.ReducedDimensions = 64; + request.BuildExplorationFactor = 300; + request.MaxConnections = 32; + request.UseCheckAndSet = true; + var result = await db.VectorSetAddAsync( + key, + request); + + Assert.True(result); + + // Verify attributes were stored + var retrievedAttributes = await db.VectorSetGetAttributesJsonAsync(key, "element1"); + Assert.Equal(attributes, retrievedAttributes); + } + + [Fact] + public async Task VectorSetLength_EmptySet() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var length = await db.VectorSetLengthAsync(key); + Assert.Equal(0, length); + } + + [Fact] + public async Task VectorSetLength_WithElements() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector1 = new[] { 1.0f, 2.0f, 3.0f }; + var vector2 = new[] { 4.0f, 5.0f, 6.0f }; + + var request = VectorSetAddRequest.Member("element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, request); + + var length = await db.VectorSetLengthAsync(key); + Assert.Equal(2, length); + } + + [Fact] + public async Task VectorSetDimension() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; + var request = VectorSetAddRequest.Member("element1", vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + + var dimension = await db.VectorSetDimensionAsync(key); + Assert.Equal(5, dimension); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task VectorSetContains(bool suppressFp32) + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + if (suppressFp32) VectorSetAddMessage.SuppressFp32(); + try + { + var request = VectorSetAddRequest.Member("element1", vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + + var exists = await db.VectorSetContainsAsync(key, "element1"); + var notExists = await db.VectorSetContainsAsync(key, "element2"); + + Assert.True(exists); + Assert.False(notExists); + } + finally + { + if (suppressFp32) VectorSetAddMessage.RestoreFp32(); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task VectorSetGetApproximateVector(bool suppressFp32) + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var originalVector = new[] { 1.0f, 2.0f, 3.0f, 4.0f }; + if (suppressFp32) VectorSetAddMessage.SuppressFp32(); + try + { + var request = VectorSetAddRequest.Member("element1", originalVector.AsMemory()); + await db.VectorSetAddAsync(key, request); + + using var retrievedLease = await db.VectorSetGetApproximateVectorAsync(key, "element1"); + + Assert.NotNull(retrievedLease); + var retrievedVector = retrievedLease.Span; + + Assert.Equal(originalVector.Length, retrievedVector.Length); + // Note: Due to quantization, values might not be exactly equal + for (int i = 0; i < originalVector.Length; i++) + { + Assert.True( + Math.Abs(originalVector[i] - retrievedVector[i]) < 0.1f, + $"Vector component {i} differs too much: expected {originalVector[i]}, got {retrievedVector[i]}"); + } + } + finally + { + if (suppressFp32) VectorSetAddMessage.RestoreFp32(); + } + } + + [Fact] + public async Task VectorSetRemove() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + var request = VectorSetAddRequest.Member("element1", vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + + var removed = await db.VectorSetRemoveAsync(key, "element1"); + Assert.True(removed); + + removed = await db.VectorSetRemoveAsync(key, "element1"); + Assert.False(removed); + + var exists = await db.VectorSetContainsAsync(key, "element1"); + Assert.False(exists); + } + + [Theory] + [InlineData(VectorSetQuantization.Int8)] + [InlineData(VectorSetQuantization.Binary)] + [InlineData(VectorSetQuantization.None)] + public async Task VectorSetInfo(VectorSetQuantization quantization) + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f }; + var request = VectorSetAddRequest.Member("element1", vector.AsMemory()); + request.Quantization = quantization; + await db.VectorSetAddAsync(key, request); + + var info = await db.VectorSetInfoAsync(key); + + Assert.NotNull(info); + var v = info.GetValueOrDefault(); + Assert.Equal(5, v.Dimension); + Assert.Equal(1, v.Length); + Assert.Equal(quantization, v.Quantization); + Assert.Null(v.QuantizationRaw); // Should be null for known quant types + + Assert.NotEqual(0, v.VectorSetUid); + Assert.NotEqual(0, v.HnswMaxNodeUid); + } + + [Fact] + public async Task VectorSetRandomMember() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector1 = new[] { 1.0f, 2.0f, 3.0f }; + var vector2 = new[] { 4.0f, 5.0f, 6.0f }; + + var request = VectorSetAddRequest.Member("element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, request); + + var randomMember = await db.VectorSetRandomMemberAsync(key); + Assert.True(randomMember == "element1" || randomMember == "element2"); + } + + [Fact] + public async Task VectorSetRandomMembers() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector1 = new[] { 1.0f, 2.0f, 3.0f }; + var vector2 = new[] { 4.0f, 5.0f, 6.0f }; + var vector3 = new[] { 7.0f, 8.0f, 9.0f }; + + var request = VectorSetAddRequest.Member("element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element3", vector3.AsMemory()); + await db.VectorSetAddAsync(key, request); + + var randomMembers = await db.VectorSetRandomMembersAsync(key, 2); + + Assert.Equal(2, randomMembers.Length); + Assert.All(randomMembers, member => + Assert.True(member == "element1" || member == "element2" || member == "element3")); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task VectorSetSimilaritySearch_ByVector(bool withScores, bool withAttributes) + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var disambiguator = (withScores ? 1 : 0) + (withAttributes ? 2 : 0); + var key = Me() + disambiguator; + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + // Add some test vectors + var vector1 = new[] { 1.0f, 0.0f, 0.0f }; + var vector2 = new[] { 0.0f, 1.0f, 0.0f }; + var vector3 = new[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 + + var request = + VectorSetAddRequest.Member("element1", vector1.AsMemory(), attributesJson: """{"category":"x"}"""); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element2", vector2.AsMemory(), attributesJson: """{"category":"y"}"""); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element3", vector3.AsMemory(), attributesJson: """{"category":"z"}"""); + await db.VectorSetAddAsync(key, request); + + // Search for vectors similar to vector1 + var query = VectorSetSimilaritySearchRequest.ByVector(vector1.AsMemory()); + query.Count = 2; + query.WithScores = withScores; + query.WithAttributes = withAttributes; + using var results = await db.VectorSetSimilaritySearchAsync(key, query); + + Assert.NotNull(results); + foreach (var result in results.Span) + { + Log(result.ToString()); + } + + var resultsArray = results.Span.ToArray(); + + Assert.True(resultsArray.Length <= 2); + Assert.Contains(resultsArray, r => r.Member == "element1"); + var found = resultsArray.First(r => r.Member == "element1"); + + if (withAttributes) + { + Assert.Equal("""{"category":"x"}""", found.AttributesJson); + } + else + { + Assert.Null(found.AttributesJson); + } + + Assert.NotEqual(withScores, double.IsNaN(found.Score)); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task VectorSetSimilaritySearch_ByMember(bool withScores, bool withAttributes) + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var disambiguator = (withScores ? 1 : 0) + (withAttributes ? 2 : 0); + var key = Me() + disambiguator; + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector1 = new[] { 1.0f, 0.0f, 0.0f }; + var vector2 = new[] { 0.0f, 1.0f, 0.0f }; + + var request = + VectorSetAddRequest.Member("element1", vector1.AsMemory(), attributesJson: """{"category":"x"}"""); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element2", vector2.AsMemory(), attributesJson: """{"category":"y"}"""); + await db.VectorSetAddAsync(key, request); + + var query = VectorSetSimilaritySearchRequest.ByMember("element1"); + query.Count = 1; + query.WithScores = withScores; + query.WithAttributes = withAttributes; + using var results = await db.VectorSetSimilaritySearchAsync(key, query); + + Assert.NotNull(results); + foreach (var result in results.Span) + { + Log(result.ToString()); + } + + var resultsArray = results.Span.ToArray(); + + Assert.Single(resultsArray); + Assert.Equal("element1", resultsArray[0].Member); + if (withAttributes) + { + Assert.Equal("""{"category":"x"}""", resultsArray[0].AttributesJson); + } + else + { + Assert.Null(resultsArray[0].AttributesJson); + } + + Assert.NotEqual(withScores, double.IsNaN(resultsArray[0].Score)); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task VectorSetSimilaritySearch_WithFilter(bool corruptPrefix, bool corruptSuffix) + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + Random rand = new Random(); + + float[] vector = new float[50]; + + void ScrambleVector() + { + var arr = vector; + for (int i = 0; i < arr.Length; i++) + { + arr[i] = (float)rand.NextDouble(); + } + } + + string[] regions = new[] { "us-west", "us-east", "eu-west", "eu-east", "ap-south", "ap-north" }; + for (int i = 0; i < 100; i++) + { + var region = regions[rand.Next(regions.Length)]; + var json = (corruptPrefix ? "oops" : "") + + JsonConvert.SerializeObject(new { id = i, region }) + + (corruptSuffix ? "oops" : ""); + ScrambleVector(); + var request = VectorSetAddRequest.Member($"element{i}", vector.AsMemory(), json); + await db.VectorSetAddAsync(key, request); + } + + ScrambleVector(); + var query = VectorSetSimilaritySearchRequest.ByVector(vector); + query.Count = 100; + query.WithScores = true; + query.WithAttributes = true; + query.FilterExpression = ".id >= 30"; + using var results = await db.VectorSetSimilaritySearchAsync(key, query); + + Assert.NotNull(results); + foreach (var result in results.Span) + { + Log(result.ToString()); + } + + Log($"Total matches: {results.Span.Length}"); + + var resultsArray = results.Span.ToArray(); + if (corruptPrefix) + { + // server short-circuits failure to be no match; we just want to assert + // what the observed behavior *is* + Assert.Empty(resultsArray); + } + else + { + Assert.Equal(70, resultsArray.Length); + Assert.All(resultsArray, r => Assert.True( + r.Score is > 0.0 and < 1.0 && GetId(r.Member!) >= 30)); + } + + static int GetId(string member) + { + if (member.StartsWith("element")) + { + return int.Parse(member.Substring(7), NumberStyles.Integer, CultureInfo.InvariantCulture); + } + + return -1; + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(".id >= 30")] + public async Task VectorSetSimilaritySearch_TestFilterValues(string? filterExpression) + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + Random rand = new Random(); + + float[] vector = new float[50]; + + void ScrambleVector() + { + var arr = vector; + for (int i = 0; i < arr.Length; i++) + { + arr[i] = (float)rand.NextDouble(); + } + } + + string[] regions = new[] { "us-west", "us-east", "eu-west", "eu-east", "ap-south", "ap-north" }; + for (int i = 0; i < 100; i++) + { + var region = regions[rand.Next(regions.Length)]; + var json = JsonConvert.SerializeObject(new { id = i, region }); + ScrambleVector(); + var request = VectorSetAddRequest.Member($"element{i}", vector.AsMemory(), json); + await db.VectorSetAddAsync(key, request); + } + + ScrambleVector(); + var query = VectorSetSimilaritySearchRequest.ByVector(vector); + query.Count = 100; + query.WithScores = true; + query.WithAttributes = true; + query.FilterExpression = filterExpression; + + using var results = await db.VectorSetSimilaritySearchAsync(key, query); + + Assert.NotNull(results); + foreach (var result in results.Span) + { + Log(result.ToString()); + } + + Log($"Total matches: {results.Span.Length}"); + // we're not interested in the specific results; we're just checking that the + // filter expression was added and parsed without exploding about arg mismatch + } + + [Fact] + public async Task VectorSetSetAttributesJson() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + var request = VectorSetAddRequest.Member("element1", vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + + // Set attributes for existing element + var attributes = """{"category":"updated","priority":"high","timestamp":"2024-01-01"}"""; + var result = await db.VectorSetSetAttributesJsonAsync(key, "element1", attributes); + + Assert.True(result); + + // Verify attributes were set + var retrievedAttributes = await db.VectorSetGetAttributesJsonAsync(key, "element1"); + Assert.Equal(attributes, retrievedAttributes); + + // Try setting attributes for non-existent element + var failResult = await db.VectorSetSetAttributesJsonAsync(key, "nonexistent", attributes); + Assert.False(failResult); + } + + [Fact] + public async Task VectorSetGetLinks() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + // Add some vectors that should be linked + var vector1 = new[] { 1.0f, 0.0f, 0.0f }; + var vector2 = new[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 + var vector3 = new[] { 0.0f, 1.0f, 0.0f }; // Different from vector1 + + var request = VectorSetAddRequest.Member("element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element3", vector3.AsMemory()); + await db.VectorSetAddAsync(key, request); + + // Get links for element1 (should include similar vectors) + using var links = await db.VectorSetGetLinksAsync(key, "element1"); + + Assert.NotNull(links); + foreach (var link in links.Span) + { + Log(link.ToString()); + } + + var linksArray = links.Span.ToArray(); + + // Should contain the other elements (note there can be transient duplicates, so: contains, not exact) + Assert.Contains("element2", linksArray); + Assert.Contains("element3", linksArray); + } + + [Fact] + public async Task VectorSetGetLinksWithScores() + { + await using var conn = Create(require: RedisFeatures.v8_0_0_M04); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + // Add some vectors with known relationships + var vector1 = new[] { 1.0f, 0.0f, 0.0f }; + var vector2 = new[] { 0.9f, 0.1f, 0.0f }; // Similar to vector1 + var vector3 = new[] { 0.0f, 1.0f, 0.0f }; // Different from vector1 + + var request = VectorSetAddRequest.Member("element1", vector1.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element2", vector2.AsMemory()); + await db.VectorSetAddAsync(key, request); + request = VectorSetAddRequest.Member("element3", vector3.AsMemory()); + await db.VectorSetAddAsync(key, request); + + // Get links with scores for element1 + using var linksWithScores = await db.VectorSetGetLinksWithScoresAsync(key, "element1"); + Assert.NotNull(linksWithScores); + foreach (var link in linksWithScores.Span) + { + Log(link.ToString()); + } + + var linksArray = linksWithScores.Span.ToArray(); + Assert.NotEmpty(linksArray); + + // Verify each link has a valid score + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + Assert.All(linksArray, static link => + { + Assert.False(link.Member.IsNull); + Assert.False(double.IsNaN(link.Score)); + Assert.True(link.Score >= 0.0); // Similarity scores should be non-negative + }); + + // Should contain the other elements (note there can be transient duplicates, so: contains, not exact) + Assert.Contains(linksArray, l => l.Member == "element2"); + Assert.Contains(linksArray, l => l.Member == "element3"); + + Assert.True(linksArray.First(l => l.Member == "element2").Score > 0.9); // similar + Assert.True(linksArray.First(l => l.Member == "element3").Score < 0.8); // less-so + } +} From 91bbd3ebe959fef0706384523c369c991e4c9c07 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 10 Sep 2025 11:08:17 +0100 Subject: [PATCH 368/435] release notes: move vectorsets to unshipped --- docs/ReleaseNotes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 8dc65c544..4ef264769 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Fix `RedisValue` special-value (NaN, Inf, etc) handling when casting from raw/string values to `double` ([#2950 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2950)) +- Add vector-set support ([#2939 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2939)) ## 2.9.11 @@ -17,7 +18,6 @@ Current package versions: - Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638)) - Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822)) - Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2928)) -- Add vector-set support ([#2939 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2939)) - Add `GetServer(RedisKey, ...)` API ([#2936 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2936)) - Fix error constructing `StreamAdd` message ([#2941 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2941)) From 229e9c568fabc477e1fd41d0d9ed02a7254d73b0 Mon Sep 17 00:00:00 2001 From: Henrik Date: Wed, 10 Sep 2025 12:10:33 +0200 Subject: [PATCH 369/435] Flush & write concurrently in LoggingTunnel and avoid double lookups in dictionaries (#2943) --- src/StackExchange.Redis/ClusterConfiguration.cs | 4 ++-- src/StackExchange.Redis/Configuration/LoggingTunnel.cs | 9 ++++++--- .../ConnectionMultiplexer.Sentinel.cs | 8 +++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/StackExchange.Redis/ClusterConfiguration.cs b/src/StackExchange.Redis/ClusterConfiguration.cs index 0ce256c95..99488ddff 100644 --- a/src/StackExchange.Redis/ClusterConfiguration.cs +++ b/src/StackExchange.Redis/ClusterConfiguration.cs @@ -180,7 +180,7 @@ internal ClusterConfiguration(ServerSelectionStrategy serverSelectionStrategy, s if (node.IsMyself) Origin = node.EndPoint; - if (nodeLookup.ContainsKey(node.EndPoint)) + if (nodeLookup.TryGetValue(node.EndPoint, out var lookedUpNode)) { // Deal with conflicting node entries for the same endpoint // This can happen in dynamic environments when a node goes down and a new one is created @@ -190,7 +190,7 @@ internal ClusterConfiguration(ServerSelectionStrategy serverSelectionStrategy, s // The node we're trying to add is probably about to become stale. Ignore it. continue; } - else if (!nodeLookup[node.EndPoint].IsConnected) + else if (!lookedUpNode.IsConnected) { // The node we registered previously is probably stale. Replace it with a known good node. nodeLookup[node.EndPoint] = node; diff --git a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs index 0eca972b8..ccfa4ee63 100644 --- a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs +++ b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs @@ -504,8 +504,9 @@ public override void Flush() public override async Task FlushAsync(CancellationToken cancellationToken) { - await _writes.FlushAsync().ForAwait(); + var writesTask = _writes.FlushAsync().ForAwait(); await _inner.FlushAsync().ForAwait(); + await writesTask; } protected override void Dispose(bool disposing) @@ -608,8 +609,9 @@ public override void Write(byte[] buffer, int offset, int count) } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - await _writes.WriteAsync(buffer, offset, count, cancellationToken).ForAwait(); + var writesTask = _writes.WriteAsync(buffer, offset, count, cancellationToken).ForAwait(); await _inner.WriteAsync(buffer, offset, count, cancellationToken).ForAwait(); + await writesTask; } #if NETCOREAPP3_0_OR_GREATER public override void Write(ReadOnlySpan buffer) @@ -619,8 +621,9 @@ public override void Write(ReadOnlySpan buffer) } public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) { - await _writes.WriteAsync(buffer, cancellationToken).ForAwait(); + var writesTask = _writes.WriteAsync(buffer, cancellationToken).ForAwait(); await _inner.WriteAsync(buffer, cancellationToken).ForAwait(); + await writesTask; } #endif } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs index b1bf7371c..61b36b014 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs @@ -14,7 +14,7 @@ public partial class ConnectionMultiplexer { internal EndPoint? currentSentinelPrimaryEndPoint; internal Timer? sentinelPrimaryReconnectTimer; - internal Dictionary sentinelConnectionChildren = new Dictionary(); + internal readonly Dictionary sentinelConnectionChildren = new(); internal ConnectionMultiplexer? sentinelConnection; /// @@ -44,10 +44,8 @@ internal void InitializeSentinel(ILogger? log) lock (sentinelConnectionChildren) { // Switch the primary if we have connections for that service - if (sentinelConnectionChildren.ContainsKey(messageParts[0])) + if (sentinelConnectionChildren.TryGetValue(messageParts[0], out ConnectionMultiplexer? child)) { - ConnectionMultiplexer child = sentinelConnectionChildren[messageParts[0]]; - // Is the connection still valid? if (child.IsDisposed) { @@ -57,7 +55,7 @@ internal void InitializeSentinel(ILogger? log) } else { - SwitchPrimary(switchBlame, sentinelConnectionChildren[messageParts[0]]); + SwitchPrimary(switchBlame, child); } } } From fec2cae3f37576e4a5a1b1101e3de139633bf844 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 10 Sep 2025 11:24:41 +0100 Subject: [PATCH 370/435] 2.9.17 release notes --- docs/ReleaseNotes.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 4ef264769..893ec2de0 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,8 +8,15 @@ Current package versions: ## Unreleased -- Fix `RedisValue` special-value (NaN, Inf, etc) handling when casting from raw/string values to `double` ([#2950 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2950)) +- (none) + +## 2.9.17 + - Add vector-set support ([#2939 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2939)) +- Fix `RedisValue` special-value (NaN, Inf, etc) handling when casting from raw/string values to `double` ([#2950 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2950)) +- Internals: + - Use `sealed` classes where possible ([#2942 by Henr1k80](https://github.com/StackExchange/StackExchange.Redis/pull/2942)) + - Add overlapped flushing in `LoggingTunnel` and avoid double-lookups ([#2943 by Henr1k80](https://github.com/StackExchange/StackExchange.Redis/pull/2943)) ## 2.9.11 From c5f26965cff1f6b1e768555aa1a270a407887c26 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 23 Sep 2025 13:25:08 +0100 Subject: [PATCH 371/435] Fix 2951 - Sentinel reconnect failure (#2956) * Fix erroneous deletion from https://github.com/StackExchange/StackExchange.Redis/commit/b4aaced4ab19eb8279ed10c93eadfb03b9bf5894#diff-b874d835cf9aa762b0e52adf8c37c2ed87b244f10dd36749ffdf735f0aea0e53L1645 * release notes * Update ReleaseNotes.md --- docs/ReleaseNotes.md | 2 +- src/StackExchange.Redis/ConnectionMultiplexer.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 893ec2de0..185a679f4 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -- (none) +- Fix [#2951](https://github.com/StackExchange/StackExchange.Redis/issues/2951) - sentinel reconnection failure ([#2956 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2956)) ## 2.9.17 diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index bf6b66674..4b06baf27 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1649,6 +1649,7 @@ internal async Task ReconfigureAsync(bool first, bool reconfigureAll, ILog if (primary == preferred || primary.IsReplica) { log?.LogInformationClearingAsRedundantPrimary(new(primary)); + primary.ClearUnselectable(UnselectableFlags.RedundantPrimary); } else { From 11a919afb2d7236118a3831c0aad0012309f396b Mon Sep 17 00:00:00 2001 From: Sergii Shumakov Date: Tue, 23 Sep 2025 22:27:05 -0700 Subject: [PATCH 372/435] Remove supported Envoyproxy commands from exclusions. (#2957) --- src/StackExchange.Redis/CommandMap.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index 31974cab9..1f31d3224 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -59,8 +59,6 @@ public sealed class CommandMap RedisCommand.PSUBSCRIBE, RedisCommand.PUBLISH, RedisCommand.PUNSUBSCRIBE, RedisCommand.SUBSCRIBE, RedisCommand.UNSUBSCRIBE, RedisCommand.SPUBLISH, RedisCommand.SSUBSCRIBE, RedisCommand.SUNSUBSCRIBE, - RedisCommand.DISCARD, RedisCommand.EXEC, RedisCommand.MULTI, RedisCommand.UNWATCH, RedisCommand.WATCH, - RedisCommand.SCRIPT, RedisCommand.SELECT, From 862a70eb5134e0e8eff0c0bd70d2e8cf75e27225 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Cura Date: Wed, 24 Sep 2025 10:38:45 +0200 Subject: [PATCH 373/435] Convert to Hex only on Encoding.UTF8.GetString possible exceptions (#2954) * Convert to Hex only on Encoding.UTF8.GetString possible exception Has OutOfMemory exceptions can occure in native Encoding.UTF8.GetString, we should not convert result to hex values * Fix other cases where we return an Hex string on all kind of exceptions --- src/StackExchange.Redis/RedisChannel.cs | 7 +++++-- src/StackExchange.Redis/RedisKey.cs | 7 +++++-- src/StackExchange.Redis/RedisValue.cs | 5 ++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/StackExchange.Redis/RedisChannel.cs b/src/StackExchange.Redis/RedisChannel.cs index f6debd1eb..2e0d7fc6c 100644 --- a/src/StackExchange.Redis/RedisChannel.cs +++ b/src/StackExchange.Redis/RedisChannel.cs @@ -302,9 +302,12 @@ public static implicit operator RedisChannel(byte[]? key) { return Encoding.UTF8.GetString(arr); } - catch + catch (Exception e) when // Only catch exception throwed by Encoding.UTF8.GetString + (e is DecoderFallbackException + || e is ArgumentException + || e is ArgumentNullException) { - return BitConverter.ToString(arr); + return BitConverter.ToString(arr); } } diff --git a/src/StackExchange.Redis/RedisKey.cs b/src/StackExchange.Redis/RedisKey.cs index 9bfc041b1..0ee83d560 100644 --- a/src/StackExchange.Redis/RedisKey.cs +++ b/src/StackExchange.Redis/RedisKey.cs @@ -299,9 +299,12 @@ public static implicit operator RedisKey(byte[]? key) { return Encoding.UTF8.GetString(arr, 0, length); } - catch + catch (Exception e) when // Only catch exception throwed by Encoding.UTF8.GetString + (e is DecoderFallbackException + || e is ArgumentException + || e is ArgumentNullException) { - return BitConverter.ToString(arr, 0, length); + return BitConverter.ToString(arr, 0, length); } } } diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index 08d65519c..da33c803e 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -789,7 +789,10 @@ private static bool TryParseDouble(ReadOnlySpan blob, out double value) { return Format.GetString(span); } - catch + catch (Exception e) when // Only catch exception throwed by Encoding.UTF8.GetString + (e is DecoderFallbackException + || e is ArgumentException + || e is ArgumentNullException) { return ToHex(span); } From f4f66bed29f637f93c52a6a3238260c0a4d259a2 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 26 Sep 2025 13:59:41 +0100 Subject: [PATCH 374/435] eng: prefer Volatile.Read over Thread.VolatileRead (#2960) --- .../ConnectionMultiplexer.Debug.cs | 2 +- .../ConnectionMultiplexer.cs | 6 +-- src/StackExchange.Redis/PhysicalBridge.cs | 2 +- src/StackExchange.Redis/PhysicalConnection.cs | 10 ++--- .../Profiling/ProfilingSession.cs | 2 +- src/StackExchange.Redis/ServerEndPoint.cs | 2 +- .../FailoverTests.cs | 6 +-- .../StackExchange.Redis.Tests/PubSubTests.cs | 40 +++++++++---------- .../SyncContextTests.cs | 2 +- 9 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs index da3f61be9..9b30ac141 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis; public partial class ConnectionMultiplexer { private static int _collectedWithoutDispose; - internal static int CollectedWithoutDispose => Thread.VolatileRead(ref _collectedWithoutDispose); + internal static int CollectedWithoutDispose => Volatile.Read(ref _collectedWithoutDispose); /// /// Invoked by the garbage collector. diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 4b06baf27..cc766338a 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -58,17 +58,17 @@ public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplex private int lastReconfigiureTicks = Environment.TickCount; internal long LastReconfigureSecondsAgo => - unchecked(Environment.TickCount - Thread.VolatileRead(ref lastReconfigiureTicks)) / 1000; + unchecked(Environment.TickCount - Volatile.Read(ref lastReconfigiureTicks)) / 1000; private int _activeHeartbeatErrors, lastHeartbeatTicks; internal long LastHeartbeatSecondsAgo => pulse is null ? -1 - : unchecked(Environment.TickCount - Thread.VolatileRead(ref lastHeartbeatTicks)) / 1000; + : unchecked(Environment.TickCount - Volatile.Read(ref lastHeartbeatTicks)) / 1000; private static int lastGlobalHeartbeatTicks = Environment.TickCount; internal static long LastGlobalHeartbeatSecondsAgo => - unchecked(Environment.TickCount - Thread.VolatileRead(ref lastGlobalHeartbeatTicks)) / 1000; + unchecked(Environment.TickCount - Volatile.Read(ref lastGlobalHeartbeatTicks)) / 1000; /// [Obsolete($"Please use {nameof(ConfigurationOptions)}.{nameof(ConfigurationOptions.IncludeDetailInExceptions)} instead - this will be removed in 3.0.")] diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index c430cf5af..1a38b7d89 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -555,7 +555,7 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) private bool DueForConnectRetry() { - int connectTimeMilliseconds = unchecked(Environment.TickCount - Thread.VolatileRead(ref connectStartTicks)); + int connectTimeMilliseconds = unchecked(Environment.TickCount - Volatile.Read(ref connectStartTicks)); return Multiplexer.RawConfig.ReconnectRetryPolicy.ShouldRetry(Interlocked.Read(ref connectTimeoutRetryCount), connectTimeMilliseconds); } diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 129fd9e07..c21bc07fc 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -261,8 +261,8 @@ private enum ReadMode : byte private readonly WeakReference _bridge; public PhysicalBridge? BridgeCouldBeNull => (PhysicalBridge?)_bridge.Target; - public long LastReadSecondsAgo => unchecked(Environment.TickCount - Thread.VolatileRead(ref lastReadTickCount)) / 1000; - public long LastWriteSecondsAgo => unchecked(Environment.TickCount - Thread.VolatileRead(ref lastWriteTickCount)) / 1000; + public long LastReadSecondsAgo => unchecked(Environment.TickCount - Volatile.Read(ref lastReadTickCount)) / 1000; + public long LastWriteSecondsAgo => unchecked(Environment.TickCount - Volatile.Read(ref lastWriteTickCount)) / 1000; private bool IncludeDetailInExceptions => BridgeCouldBeNull?.Multiplexer.RawConfig.IncludeDetailInExceptions ?? false; @@ -418,8 +418,8 @@ public void RecordConnectionFailed( if (isCurrent && Interlocked.CompareExchange(ref failureReported, 1, 0) == 0) { - int now = Environment.TickCount, lastRead = Thread.VolatileRead(ref lastReadTickCount), lastWrite = Thread.VolatileRead(ref lastWriteTickCount), - lastBeat = Thread.VolatileRead(ref lastBeatTickCount); + int now = Environment.TickCount, lastRead = Volatile.Read(ref lastReadTickCount), lastWrite = Volatile.Read(ref lastWriteTickCount), + lastBeat = Volatile.Read(ref lastBeatTickCount); int unansweredWriteTime = 0; lock (_writtenAwaitingResponse) @@ -434,7 +434,7 @@ public void RecordConnectionFailed( var exMessage = new StringBuilder(failureType.ToString()); // If the reason for the shutdown was we asked for the socket to die, don't log it as an error (only informational) - weAskedForThis = Thread.VolatileRead(ref clientSentQuit) != 0; + weAskedForThis = Volatile.Read(ref clientSentQuit) != 0; var pipe = connectingPipe ?? _ioPipe; if (pipe is SocketConnection sc) diff --git a/src/StackExchange.Redis/Profiling/ProfilingSession.cs b/src/StackExchange.Redis/Profiling/ProfilingSession.cs index f83a49c91..3bc3caf38 100644 --- a/src/StackExchange.Redis/Profiling/ProfilingSession.cs +++ b/src/StackExchange.Redis/Profiling/ProfilingSession.cs @@ -24,7 +24,7 @@ internal void Add(ProfiledCommand command) { if (command == null) return; - object? cur = Thread.VolatileRead(ref _untypedHead); + object? cur = Volatile.Read(ref _untypedHead); while (true) { command.NextElement = (ProfiledCommand?)cur; diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index af98af0f7..f856a5b21 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -719,7 +719,7 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) } internal int LastInfoReplicationCheckSecondsAgo => - unchecked(Environment.TickCount - Thread.VolatileRead(ref lastInfoReplicationCheckTicks)) / 1000; + unchecked(Environment.TickCount - Volatile.Read(ref lastInfoReplicationCheckTicks)) / 1000; private EndPoint? primaryEndPoint; public EndPoint? PrimaryEndPoint diff --git a/tests/StackExchange.Redis.Tests/FailoverTests.cs b/tests/StackExchange.Redis.Tests/FailoverTests.cs index 68f8f2266..1f33275b5 100644 --- a/tests/StackExchange.Redis.Tests/FailoverTests.cs +++ b/tests/StackExchange.Redis.Tests/FailoverTests.cs @@ -217,7 +217,7 @@ public async Task SubscriptionsSurviveConnectionFailureAsync() await sub.PingAsync(); await Task.Delay(200).ConfigureAwait(false); - var counter1 = Thread.VolatileRead(ref counter); + var counter1 = Volatile.Read(ref counter); Log($"Expecting 1 message, got {counter1}"); Assert.Equal(1, counter1); @@ -274,9 +274,9 @@ public async Task SubscriptionsSurviveConnectionFailureAsync() // Give it a few seconds to get our messages Log("Waiting for 2 messages"); - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => Thread.VolatileRead(ref counter) == 2); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => Volatile.Read(ref counter) == 2); - var counter2 = Thread.VolatileRead(ref counter); + var counter2 = Volatile.Read(ref counter); Log($"Expecting 2 messages, got {counter2}"); Assert.Equal(2, counter2); diff --git a/tests/StackExchange.Redis.Tests/PubSubTests.cs b/tests/StackExchange.Redis.Tests/PubSubTests.cs index 9418fe80f..a3dadb07e 100644 --- a/tests/StackExchange.Redis.Tests/PubSubTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubTests.cs @@ -30,19 +30,19 @@ public async Task ExplicitPublishMode() #pragma warning restore CS0618 await UntilConditionAsync( TimeSpan.FromSeconds(10), - () => Thread.VolatileRead(ref b) == 1 - && Thread.VolatileRead(ref c) == 1 - && Thread.VolatileRead(ref d) == 1); - Assert.Equal(0, Thread.VolatileRead(ref a)); - Assert.Equal(1, Thread.VolatileRead(ref b)); - Assert.Equal(1, Thread.VolatileRead(ref c)); - Assert.Equal(1, Thread.VolatileRead(ref d)); + () => Volatile.Read(ref b) == 1 + && Volatile.Read(ref c) == 1 + && Volatile.Read(ref d) == 1); + Assert.Equal(0, Volatile.Read(ref a)); + Assert.Equal(1, Volatile.Read(ref b)); + Assert.Equal(1, Volatile.Read(ref c)); + Assert.Equal(1, Volatile.Read(ref d)); #pragma warning disable CS0618 pub.Publish("*bcd", "efg"); #pragma warning restore CS0618 - await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Thread.VolatileRead(ref a) == 1); - Assert.Equal(1, Thread.VolatileRead(ref a)); + await UntilConditionAsync(TimeSpan.FromSeconds(10), () => Volatile.Read(ref a) == 1); + Assert.Equal(1, Volatile.Read(ref a)); } [Theory] @@ -86,7 +86,7 @@ public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string b { Assert.Empty(received); } - Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); + Assert.Equal(0, Volatile.Read(ref secondHandler)); #pragma warning disable CS0618 var count = sub.Publish(pubChannel, "def"); #pragma warning restore CS0618 @@ -99,8 +99,8 @@ public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string b Assert.Single(received); } // Give handler firing a moment - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); - Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref secondHandler) == 1); + Assert.Equal(1, Volatile.Read(ref secondHandler)); // unsubscribe from first; should still see second #pragma warning disable CS0618 @@ -113,9 +113,9 @@ public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string b Assert.Single(received); } - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 2); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref secondHandler) == 2); - var secondHandlerCount = Thread.VolatileRead(ref secondHandler); + var secondHandlerCount = Volatile.Read(ref secondHandler); Log("Expecting 2 from second handler, got: " + secondHandlerCount); Assert.Equal(2, secondHandlerCount); Assert.Equal(1, count); @@ -130,7 +130,7 @@ public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string b { Assert.Single(received); } - secondHandlerCount = Thread.VolatileRead(ref secondHandler); + secondHandlerCount = Volatile.Read(ref secondHandler); Log("Expecting 2 from second handler, got: " + secondHandlerCount); Assert.Equal(2, secondHandlerCount); Assert.Equal(0, count); @@ -170,7 +170,7 @@ public async Task TestBasicPubSubFireAndForget() { Assert.Empty(received); } - Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); + Assert.Equal(0, Volatile.Read(ref secondHandler)); await PingAsync(pub, sub).ForAwait(); var count = sub.Publish(key, "def", CommandFlags.FireAndForget); await PingAsync(pub, sub).ForAwait(); @@ -182,7 +182,7 @@ public async Task TestBasicPubSubFireAndForget() { Assert.Single(received); } - Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); + Assert.Equal(1, Volatile.Read(ref secondHandler)); sub.Unsubscribe(key); count = sub.Publish(key, "ghi", CommandFlags.FireAndForget); @@ -241,7 +241,7 @@ public async Task TestPatternPubSub() { Assert.Empty(received); } - Assert.Equal(0, Thread.VolatileRead(ref secondHandler)); + Assert.Equal(0, Volatile.Read(ref secondHandler)); await PingAsync(pub, sub).ForAwait(); var count = sub.Publish(RedisChannel.Literal("abc"), "def"); @@ -254,8 +254,8 @@ public async Task TestPatternPubSub() } // Give reception a bit, the handler could be delayed under load - await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Thread.VolatileRead(ref secondHandler) == 1); - Assert.Equal(1, Thread.VolatileRead(ref secondHandler)); + await UntilConditionAsync(TimeSpan.FromSeconds(2), () => Volatile.Read(ref secondHandler) == 1); + Assert.Equal(1, Volatile.Read(ref secondHandler)); #pragma warning disable CS0618 sub.Unsubscribe("a*c"); diff --git a/tests/StackExchange.Redis.Tests/SyncContextTests.cs b/tests/StackExchange.Redis.Tests/SyncContextTests.cs index 8eddc9f1d..b98caefeb 100644 --- a/tests/StackExchange.Redis.Tests/SyncContextTests.cs +++ b/tests/StackExchange.Redis.Tests/SyncContextTests.cs @@ -118,7 +118,7 @@ public MySyncContext(TextWriter log) _log = log; SetSynchronizationContext(this); } - public int OpCount => Thread.VolatileRead(ref _opCount); + public int OpCount => Volatile.Read(ref _opCount); private int _opCount; private void Incr() => Interlocked.Increment(ref _opCount); From d4b05ca58fa3d784ceeebc00ee708ab3cdc5db29 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 26 Sep 2025 14:55:51 +0100 Subject: [PATCH 375/435] Channel routing: revert non-S routing to random, with new API to opt into routed (#2958) * mitigate #2955 - by default: use round-robin (not channel-routing) for "non-sharded" pub/sub - add new API for channel-routed literals/wildcards - when publishing, if we're also subscribed: use that connection - randomize where the round-robin starts, to better randomize startup behaviour * release notes * prefer a single WithKeyRouting API --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Message.cs | 3 +- .../PublicAPI/PublicAPI.Shipped.txt | 1 + src/StackExchange.Redis/RedisChannel.cs | 107 +++++++++++++----- src/StackExchange.Redis/RedisDatabase.cs | 6 +- src/StackExchange.Redis/RedisSubscriber.cs | 14 +-- .../ServerSelectionStrategy.cs | 8 +- .../StackExchange.Redis.Tests/ClusterTests.cs | 52 +++++++-- 8 files changed, 146 insertions(+), 46 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 185a679f4..00c63d0ff 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -9,6 +9,7 @@ Current package versions: ## Unreleased - Fix [#2951](https://github.com/StackExchange/StackExchange.Redis/issues/2951) - sentinel reconnection failure ([#2956 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2956)) +- Mitigate [#2955](https://github.com/StackExchange/StackExchange.Redis/issues/2955) (unbalanced pub/sub routing) ([#2958 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2958)) ## 2.9.17 diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 5973bd55b..a3c19ab93 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -866,7 +866,8 @@ protected CommandChannelBase(int db, CommandFlags flags, RedisCommand command, i public override string CommandAndKey => Command + " " + Channel; - public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) => serverSelectionStrategy.HashSlot(Channel); + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) + => Channel.IsKeyRouted ? serverSelectionStrategy.HashSlot(Channel) : ServerSelectionStrategy.NoSlot; } internal abstract class CommandKeyBase : Message diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 10044dc9b..0abb20043 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -2051,3 +2051,4 @@ StackExchange.Redis.IServer.ExecuteAsync(int? database, string! command, System. [SER001]static StackExchange.Redis.VectorSetAddRequest.Member(StackExchange.Redis.RedisValue element, System.ReadOnlyMemory values, string? attributesJson = null) -> StackExchange.Redis.VectorSetAddRequest! [SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByMember(StackExchange.Redis.RedisValue member) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! [SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByVector(System.ReadOnlyMemory vector) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! +StackExchange.Redis.RedisChannel.WithKeyRouting() -> StackExchange.Redis.RedisChannel diff --git a/src/StackExchange.Redis/RedisChannel.cs b/src/StackExchange.Redis/RedisChannel.cs index 2e0d7fc6c..d4289f3c6 100644 --- a/src/StackExchange.Redis/RedisChannel.cs +++ b/src/StackExchange.Redis/RedisChannel.cs @@ -18,10 +18,20 @@ internal enum RedisChannelOptions None = 0, Pattern = 1 << 0, Sharded = 1 << 1, + KeyRouted = 1 << 2, } + // we don't consider Routed for equality - it's an implementation detail, not a fundamental feature + private const RedisChannelOptions EqualityMask = ~RedisChannelOptions.KeyRouted; + internal RedisCommand PublishCommand => IsSharded ? RedisCommand.SPUBLISH : RedisCommand.PUBLISH; + /// + /// Should we use cluster routing for this channel? This applies *either* to sharded (SPUBLISH) scenarios, + /// or to scenarios using . + /// + internal bool IsKeyRouted => (Options & RedisChannelOptions.KeyRouted) != 0; + /// /// Indicates whether the channel-name is either null or a zero-length value. /// @@ -51,24 +61,44 @@ public static bool UseImplicitAutoPattern private static PatternMode s_DefaultPatternMode = PatternMode.Auto; /// - /// Creates a new that does not act as a wildcard subscription. + /// Creates a new that does not act as a wildcard subscription. In cluster + /// environments, this channel will be freely routed to any applicable server - different client nodes + /// will generally connect to different servers; this is suitable for distributing pub/sub in scenarios with + /// very few channels. In non-cluster environments, routing is not a consideration. + /// + public static RedisChannel Literal(string value) => new(value, RedisChannelOptions.None); + + /// + /// Creates a new that does not act as a wildcard subscription. In cluster + /// environments, this channel will be freely routed to any applicable server - different client nodes + /// will generally connect to different servers; this is suitable for distributing pub/sub in scenarios with + /// very few channels. In non-cluster environments, routing is not a consideration. /// - public static RedisChannel Literal(string value) => new RedisChannel(value, PatternMode.Literal); + public static RedisChannel Literal(byte[] value) => new(value, RedisChannelOptions.None); /// - /// Creates a new that does not act as a wildcard subscription. + /// In cluster environments, this channel will be routed using similar rules to , which is suitable + /// for distributing pub/sub in scenarios with lots of channels. In non-cluster environments, routing is not + /// a consideration. /// - public static RedisChannel Literal(byte[] value) => new RedisChannel(value, PatternMode.Literal); + /// Note that channels from Sharded are always routed. + public RedisChannel WithKeyRouting() => new(Value, Options | RedisChannelOptions.KeyRouted); /// - /// Creates a new that acts as a wildcard subscription. + /// Creates a new that acts as a wildcard subscription. In cluster + /// environments, this channel will be freely routed to any applicable server - different client nodes + /// will generally connect to different servers; this is suitable for distributing pub/sub in scenarios with + /// very few channels. In non-cluster environments, routing is not a consideration. /// - public static RedisChannel Pattern(string value) => new RedisChannel(value, PatternMode.Pattern); + public static RedisChannel Pattern(string value) => new(value, RedisChannelOptions.Pattern); /// - /// Creates a new that acts as a wildcard subscription. + /// Creates a new that acts as a wildcard subscription. In cluster + /// environments, this channel will be freely routed to any applicable server - different client nodes + /// will generally connect to different servers; this is suitable for distributing pub/sub in scenarios with + /// very few channels. In non-cluster environments, routing is not a consideration. /// - public static RedisChannel Pattern(byte[] value) => new RedisChannel(value, PatternMode.Pattern); + public static RedisChannel Pattern(byte[] value) => new(value, RedisChannelOptions.Pattern); /// /// Create a new redis channel from a buffer, explicitly controlling the pattern mode. @@ -84,21 +114,32 @@ public RedisChannel(byte[]? value, PatternMode mode) : this(value, DeterminePatt /// /// The string name of the channel to create. /// The mode for name matching. + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract public RedisChannel(string value, PatternMode mode) : this(value is null ? null : Encoding.UTF8.GetBytes(value), mode) { } /// - /// Create a new redis channel from a buffer, representing a sharded channel. + /// Create a new redis channel from a buffer, representing a sharded channel. In cluster + /// environments, this channel will be routed using similar rules to , which is suitable + /// for distributing pub/sub in scenarios with lots of channels. In non-cluster environments, routing is not + /// a consideration. /// /// The name of the channel to create. - public static RedisChannel Sharded(byte[]? value) => new(value, RedisChannelOptions.Sharded); + /// Note that sharded subscriptions are completely separate to regular subscriptions; subscriptions + /// using sharded channels must also be published with sharded channels (and vice versa). + public static RedisChannel Sharded(byte[]? value) => new(value, RedisChannelOptions.Sharded | RedisChannelOptions.KeyRouted); /// - /// Create a new redis channel from a string, representing a sharded channel. + /// Create a new redis channel from a string, representing a sharded channel. In cluster + /// environments, this channel will be routed using similar rules to , which is suitable + /// for distributing pub/sub in scenarios with lots of channels. In non-cluster environments, routing is not + /// a consideration. /// /// The string name of the channel to create. - public static RedisChannel Sharded(string value) => new(value is null ? null : Encoding.UTF8.GetBytes(value), RedisChannelOptions.Sharded); + /// Note that sharded subscriptions are completely separate to regular subscriptions; subscriptions + /// using sharded channels must also be published with sharded channels (and vice versa). + public static RedisChannel Sharded(string value) => new(value, RedisChannelOptions.Sharded | RedisChannelOptions.KeyRouted); internal RedisChannel(byte[]? value, RedisChannelOptions options) { @@ -106,6 +147,12 @@ internal RedisChannel(byte[]? value, RedisChannelOptions options) Options = options; } + internal RedisChannel(string? value, RedisChannelOptions options) + { + Value = value is null ? null : Encoding.UTF8.GetBytes(value); + Options = options; + } + private static bool DeterminePatternBased(byte[]? value, PatternMode mode) => mode switch { PatternMode.Auto => value != null && Array.IndexOf(value, (byte)'*') >= 0, @@ -155,7 +202,8 @@ internal RedisChannel(byte[]? value, RedisChannelOptions options) /// The first to compare. /// The second to compare. public static bool operator ==(RedisChannel x, RedisChannel y) => - x.Options == y.Options && RedisValue.Equals(x.Value, y.Value); + (x.Options & EqualityMask) == (y.Options & EqualityMask) + && RedisValue.Equals(x.Value, y.Value); /// /// Indicate whether two channel names are equal. @@ -163,7 +211,8 @@ internal RedisChannel(byte[]? value, RedisChannelOptions options) /// The first to compare. /// The second to compare. public static bool operator ==(string x, RedisChannel y) => - RedisValue.Equals(x == null ? null : Encoding.UTF8.GetBytes(x), y.Value); + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + RedisValue.Equals(x is null ? null : Encoding.UTF8.GetBytes(x), y.Value); /// /// Indicate whether two channel names are equal. @@ -178,7 +227,8 @@ internal RedisChannel(byte[]? value, RedisChannelOptions options) /// The first to compare. /// The second to compare. public static bool operator ==(RedisChannel x, string y) => - RedisValue.Equals(x.Value, y == null ? null : Encoding.UTF8.GetBytes(y)); + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + RedisValue.Equals(x.Value, y is null ? null : Encoding.UTF8.GetBytes(y)); /// /// Indicate whether two channel names are equal. @@ -203,10 +253,11 @@ internal RedisChannel(byte[]? value, RedisChannelOptions options) /// Indicate whether two channel names are equal. /// /// The to compare to. - public bool Equals(RedisChannel other) => Options == other.Options && RedisValue.Equals(Value, other.Value); + public bool Equals(RedisChannel other) => (Options & EqualityMask) == (other.Options & EqualityMask) + && RedisValue.Equals(Value, other.Value); /// - public override int GetHashCode() => RedisValue.GetHashCode(Value) ^ (int)Options; + public override int GetHashCode() => RedisValue.GetHashCode(Value) ^ (int)(Options & EqualityMask); /// /// Obtains a string representation of the channel name. @@ -266,23 +317,21 @@ public enum PatternMode [Obsolete("It is preferable to explicitly specify a " + nameof(PatternMode) + ", or use the " + nameof(Literal) + "/" + nameof(Pattern) + " methods", error: false)] public static implicit operator RedisChannel(string key) { - if (key == null) return default; + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (key is null) return default; return new RedisChannel(Encoding.UTF8.GetBytes(key), s_DefaultPatternMode); } /// - /// Create a channel name from a . + /// Create a channel name from a byte[]. /// /// The byte array to get a channel from. [Obsolete("It is preferable to explicitly specify a " + nameof(PatternMode) + ", or use the " + nameof(Literal) + "/" + nameof(Pattern) + " methods", error: false)] public static implicit operator RedisChannel(byte[]? key) - { - if (key == null) return default; - return new RedisChannel(key, s_DefaultPatternMode); - } + => key is null ? default : new RedisChannel(key, s_DefaultPatternMode); /// - /// Obtain the channel name as a . + /// Obtain the channel name as a byte[]. /// /// The channel to get a byte[] from. public static implicit operator byte[]?(RedisChannel key) => key.Value; @@ -294,7 +343,7 @@ public static implicit operator RedisChannel(byte[]? key) public static implicit operator string?(RedisChannel key) { var arr = key.Value; - if (arr == null) + if (arr is null) { return null; } @@ -303,9 +352,7 @@ public static implicit operator RedisChannel(byte[]? key) return Encoding.UTF8.GetString(arr); } catch (Exception e) when // Only catch exception throwed by Encoding.UTF8.GetString - (e is DecoderFallbackException - || e is ArgumentException - || e is ArgumentNullException) + (e is DecoderFallbackException or ArgumentException or ArgumentNullException) { return BitConverter.ToString(arr); } @@ -316,8 +363,12 @@ public static implicit operator RedisChannel(byte[]? key) // giving due consideration to the default pattern mode (UseImplicitAutoPattern) // (since we don't ship them, we don't need them in release) [Obsolete("Watch for " + nameof(UseImplicitAutoPattern), error: true)] + // ReSharper disable once UnusedMember.Local + // ReSharper disable once UnusedParameter.Local private RedisChannel(string value) => throw new NotSupportedException(); [Obsolete("Watch for " + nameof(UseImplicitAutoPattern), error: true)] + // ReSharper disable once UnusedMember.Local + // ReSharper disable once UnusedParameter.Local private RedisChannel(byte[]? value) => throw new NotSupportedException(); #endif } diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 349864a1b..bcda4146b 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1871,14 +1871,16 @@ public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags { if (channel.IsNullOrEmpty) throw new ArgumentNullException(nameof(channel)); var msg = Message.Create(-1, flags, channel.PublishCommand, channel, message); - return ExecuteSync(msg, ResultProcessor.Int64); + // if we're actively subscribed: send via that connection (otherwise, follow normal rules) + return ExecuteSync(msg, ResultProcessor.Int64, server: multiplexer.GetSubscribedServer(channel)); } public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { if (channel.IsNullOrEmpty) throw new ArgumentNullException(nameof(channel)); var msg = Message.Create(-1, flags, channel.PublishCommand, channel, message); - return ExecuteAsync(msg, ResultProcessor.Int64); + // if we're actively subscribed: send via that connection (otherwise, follow normal rules) + return ExecuteAsync(msg, ResultProcessor.Int64, server: multiplexer.GetSubscribedServer(channel)); } public RedisResult Execute(string command, params object[] args) diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index b641baf05..8ff9610b0 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -182,18 +182,16 @@ public Subscription(CommandFlags flags) /// internal Message GetMessage(RedisChannel channel, SubscriptionAction action, CommandFlags flags, bool internalCall) { - var isPattern = channel.IsPattern; - var isSharded = channel.IsSharded; - var command = action switch + var command = action switch // note that the Routed flag doesn't impact the message here - just the routing { - SubscriptionAction.Subscribe => channel.Options switch + SubscriptionAction.Subscribe => (channel.Options & ~RedisChannel.RedisChannelOptions.KeyRouted) switch { RedisChannel.RedisChannelOptions.None => RedisCommand.SUBSCRIBE, RedisChannel.RedisChannelOptions.Pattern => RedisCommand.PSUBSCRIBE, RedisChannel.RedisChannelOptions.Sharded => RedisCommand.SSUBSCRIBE, _ => Unknown(action, channel.Options), }, - SubscriptionAction.Unsubscribe => channel.Options switch + SubscriptionAction.Unsubscribe => (channel.Options & ~RedisChannel.RedisChannelOptions.KeyRouted) switch { RedisChannel.RedisChannelOptions.None => RedisCommand.UNSUBSCRIBE, RedisChannel.RedisChannelOptions.Pattern => RedisCommand.PUNSUBSCRIBE, @@ -384,14 +382,16 @@ public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags { ThrowIfNull(channel); var msg = Message.Create(-1, flags, channel.PublishCommand, channel, message); - return ExecuteSync(msg, ResultProcessor.Int64); + // if we're actively subscribed: send via that connection (otherwise, follow normal rules) + return ExecuteSync(msg, ResultProcessor.Int64, server: multiplexer.GetSubscribedServer(channel)); } public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { ThrowIfNull(channel); var msg = Message.Create(-1, flags, channel.PublishCommand, channel, message); - return ExecuteAsync(msg, ResultProcessor.Int64); + // if we're actively subscribed: send via that connection (otherwise, follow normal rules) + return ExecuteAsync(msg, ResultProcessor.Int64, server: multiplexer.GetSubscribedServer(channel)); } void ISubscriber.Subscribe(RedisChannel channel, Action handler, CommandFlags flags) diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index 44241d373..4084b4c33 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -47,7 +47,13 @@ internal sealed class ServerSelectionStrategy }; private readonly ConnectionMultiplexer multiplexer; - private int anyStartOffset; + private int anyStartOffset = SharedRandom.Next(); // initialize to a random value so routing isn't uniform + + #if NET6_0_OR_GREATER + private static Random SharedRandom => Random.Shared; + #else + private static Random SharedRandom { get; } = new(); + #endif private ServerEndPoint[]? map; diff --git a/tests/StackExchange.Redis.Tests/ClusterTests.cs b/tests/StackExchange.Redis.Tests/ClusterTests.cs index 6fa963b8a..8146dc9be 100644 --- a/tests/StackExchange.Redis.Tests/ClusterTests.cs +++ b/tests/StackExchange.Redis.Tests/ClusterTests.cs @@ -743,16 +743,40 @@ public async Task ConnectIncludesSubscriber() } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ClusterPubSub(bool sharded) + [InlineData(true, false)] + [InlineData(true, true)] + [InlineData(false, false)] + [InlineData(false, true)] + public async Task ClusterPubSub(bool sharded, bool withKeyRouting) { var guid = Guid.NewGuid().ToString(); var channel = sharded ? RedisChannel.Sharded(guid) : RedisChannel.Literal(guid); + if (withKeyRouting) + { + channel = channel.WithKeyRouting(); + } await using var conn = Create(keepAlive: 1, connectTimeout: 3000, shared: false, require: sharded ? RedisFeatures.v7_0_0_rc1 : RedisFeatures.v2_0_0); Assert.True(conn.IsConnected); var pubsub = conn.GetSubscriber(); + HashSet eps = []; + for (int i = 0; i < 10; i++) + { + var ep = Format.ToString(await pubsub.IdentifyEndpointAsync(channel)); + Log($"Channel {channel} => {ep}"); + eps.Add(ep); + } + + if (sharded | withKeyRouting) + { + Assert.Single(eps); + } + else + { + // if not routed: we should have at least two different endpoints + Assert.True(eps.Count > 1); + } + List<(RedisChannel, RedisValue)> received = []; var queue = await pubsub.SubscribeAsync(channel); _ = Task.Run(async () => @@ -766,16 +790,28 @@ public async Task ClusterPubSub(bool sharded) } } }); - + var subscribedEp = Format.ToString(pubsub.SubscribedEndpoint(channel)); + Log($"Subscribed to {subscribedEp}"); + Assert.NotNull(subscribedEp); + if (sharded | withKeyRouting) + { + Assert.Equal(eps.Single(), subscribedEp); + } var db = conn.GetDatabase(); await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) await db.PingAsync(); for (int i = 0; i < 10; i++) { - // check we get a hit - Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); + // publish + var receivers = await db.PublishAsync(channel, i.ToString()); + + // check we get a hit (we are the only subscriber, and because we prefer to + // use our own subscribed connection: we can reliably expect to see this hit) + Log($"Published {i} to {receivers} receiver(s) against the receiving server."); + Assert.Equal(1, receivers); } - await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + + await Task.Delay(250); // let the sub settle (this isn't needed on RESP3, note) await db.PingAsync(); await pubsub.UnsubscribeAsync(channel); @@ -792,6 +828,8 @@ public async Task ClusterPubSub(bool sharded) var pair = snap[i]; Log("element {0}: {1}/{2}", i, pair.Channel, pair.Value); } + // even if not routed: we can expect the *order* to be correct, since there's + // only one publisher (us), and we prefer to publish via our own subscription for (int i = 0; i < 10; i++) { var pair = snap[i]; From 307129e40554fc76e6f2eafd6a7d1f82b8d4fcdb Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 26 Sep 2025 15:05:30 +0100 Subject: [PATCH 376/435] release notes --- docs/ReleaseNotes.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 00c63d0ff..661f2680d 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,8 +8,13 @@ Current package versions: ## Unreleased +## 2.9.24 + - Fix [#2951](https://github.com/StackExchange/StackExchange.Redis/issues/2951) - sentinel reconnection failure ([#2956 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2956)) -- Mitigate [#2955](https://github.com/StackExchange/StackExchange.Redis/issues/2955) (unbalanced pub/sub routing) ([#2958 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2958)) +- Mitigate [#2955](https://github.com/StackExchange/StackExchange.Redis/issues/2955) (unbalanced pub/sub routing) / add `RedisValue.WithKeyRouting()` ([#2958 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2958)) +- Fix envoyproxy command exclusions ([#2957 by sshumakov](https://github.com/StackExchange/StackExchange.Redis/pull/2957)) +- Restrict `RedisValue` hex fallback (`string` conversion) to encoding failures ([2954 by jcaspes](https://github.com/StackExchange/StackExchange.Redis/pull/2954)) +- (internals) prefer `Volatile.Read` over `Thread.VolatileRead` ([2960 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2960)) ## 2.9.17 From 0be1df7e6cbc9d30d3fa37fec1a2ed824552aef3 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 29 Sep 2025 15:11:04 +0100 Subject: [PATCH 377/435] eng: remove PublicSign windows-only restriction (#2963) * remove PublicSign windows-only restriction * release notes --- docs/ReleaseNotes.md | 2 ++ src/StackExchange.Redis/StackExchange.Redis.csproj | 1 - .../StackExchange.Redis.Server.csproj | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 661f2680d..f0a17082b 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,8 @@ Current package versions: ## Unreleased +- (build) Fix SNK on non-Windows builds ([#2963 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2963)) + ## 2.9.24 - Fix [#2951](https://github.com/StackExchange/StackExchange.Redis/issues/2951) - sentinel reconnection failure ([#2956 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2956)) diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index b13a12423..b03103656 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -9,7 +9,6 @@ StackExchange.Redis Async;Redis;Cache;PubSub;Messaging true - true $(DefineConstants);VECTOR_SAFE $(DefineConstants);UNIX_SOCKET README.md diff --git a/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj b/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj index 1fba90d7f..9908e9088 100644 --- a/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj +++ b/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj @@ -9,7 +9,6 @@ Server;Async;Redis;Cache;PubSub;Messaging Library true - true $(NoWarn);CS1591 From 515c47a782145f9a0a6c5874ee8cbef6df335d40 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 29 Sep 2025 15:42:45 +0100 Subject: [PATCH 378/435] release notes --- docs/ReleaseNotes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index f0a17082b..b3546e969 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,8 @@ Current package versions: ## Unreleased +## 2.9.25 + - (build) Fix SNK on non-Windows builds ([#2963 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2963)) ## 2.9.24 From a5e6e3d1283e8e78e64c79415ee443af62e36c34 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 30 Sep 2025 11:15:50 -0400 Subject: [PATCH 379/435] Actions: support publishing to MyGet off main branch (#2964) * Actions: support publishing to MyGet off main branch This is on the way to removing AppVeyor (currently broken) from our environment. Publishes to MyGet when building from a push to main, _not_ on pull requests. * Only package on main as well --- .github/workflows/CI.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1796710a7..37a419cc9 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -143,3 +143,10 @@ jobs: name: Tests Results - Windows Server 2022 path: 'test-results/*.trx' reporter: dotnet-trx + # Package and upload to MyGet only on pushes to main, not on PRs + - name: .NET Pack + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: dotnet pack Build.csproj --no-build -c Release /p:PackageOutputPath=${env:GITHUB_WORKSPACE}\.nupkgs /p:CI=true + - name: Upload to MyGet + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: dotnet nuget push ${env:GITHUB_WORKSPACE}\.nupkgs\*.nupkg -s https://www.myget.org/F/stackoverflow/api/v2/package -k ${{ secrets.MYGET_API_KEY }} \ No newline at end of file From 895a2c72d6a8ad0d1594c05c7917dfcf04db424c Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 30 Sep 2025 11:31:09 -0400 Subject: [PATCH 380/435] Adjust YAML for main branch builds --- .github/workflows/CI.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 37a419cc9..7f9373adb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -4,10 +4,10 @@ on: pull_request: push: branches: - - main + - 'main' paths: - - '*' - - '!/docs/*' # Don't run workflow when files are only in the /docs directory + - '*' + - '!/docs/*' # Don't run workflow when files are only in the /docs directory jobs: main: From 9abcef1fa2337f458052d8bb312e489302a368aa Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 30 Sep 2025 11:34:57 -0400 Subject: [PATCH 381/435] Adjust main branches --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7f9373adb..3a2f905f3 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,9 +2,9 @@ name: CI on: pull_request: + branches: [ 'main' ] push: - branches: - - 'main' + branches: [ 'main' ] paths: - '*' - '!/docs/*' # Don't run workflow when files are only in the /docs directory From 9ee5f6f921daecdf6d3a4816bc2d3a1e439836f7 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Tue, 30 Sep 2025 11:37:01 -0400 Subject: [PATCH 382/435] Adjust path globs on file paths to include subdirectories --- .github/workflows/CI.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3a2f905f3..51f96b88d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,11 +2,10 @@ name: CI on: pull_request: - branches: [ 'main' ] push: branches: [ 'main' ] paths: - - '*' + - '**' - '!/docs/*' # Don't run workflow when files are only in the /docs directory jobs: From 8a6ad4a7ef03984c8bf81d965764d5cb2b573ff4 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 7 Oct 2025 15:40:21 +0100 Subject: [PATCH 383/435] Update README.md (#2967) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e65e97260..076a128d7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ For all documentation, [see here](https://stackexchange.github.io/StackExchange. #### Build Status -[![Build status](https://ci.appveyor.com/api/projects/status/2o3frasprum8mbaj/branch/main?svg=true)](https://ci.appveyor.com/project/StackExchange/stackexchange-redis/branch/main) +[![CI](https://github.com/StackExchange/StackExchange.Redis/actions/workflows/CI.yml/badge.svg)](https://github.com/StackExchange/StackExchange.Redis/actions/workflows/CI.yml) #### Package Status From d5b445a9ee3e1e43cab6731a85aa526b13971d28 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 13 Oct 2025 14:07:15 +0100 Subject: [PATCH 384/435] Fix sharded pub/sub handling over slot migrations (#2969) * add failing test and mitigation for OSS sharded sunbscribe behavior * fix unsolicited SUNSUBSCRIBE * remove redundant code * ssh test * SUNSUBSCRIBE handling; if possible, use the active connection to find where we should be subscribing * PR nits * more PR nits --- docs/ReleaseNotes.md | 2 + src/StackExchange.Redis/Message.cs | 2 +- src/StackExchange.Redis/PhysicalConnection.cs | 270 +++++++++++++----- src/StackExchange.Redis/RedisSubscriber.cs | 53 +++- .../ServerSelectionStrategy.cs | 2 +- .../ClusterShardedTests.cs | 175 +++++++++++- 6 files changed, 426 insertions(+), 78 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index b3546e969..615f44497 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,8 @@ Current package versions: ## Unreleased +- Fix `SSUBSCRIBE` routing during slot migrations ([#2969 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2969)) + ## 2.9.25 - (build) Fix SNK on non-Windows builds ([#2963 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2963)) diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index a3c19ab93..0c9eb4c92 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -856,7 +856,7 @@ protected override void WriteImpl(PhysicalConnection physical) internal abstract class CommandChannelBase : Message { - protected readonly RedisChannel Channel; + internal readonly RedisChannel Channel; protected CommandChannelBase(int db, CommandFlags flags, RedisCommand command, in RedisChannel channel) : base(db, flags, command) { diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index c21bc07fc..57bcd608d 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -29,8 +29,6 @@ internal sealed partial class PhysicalConnection : IDisposable private const int DefaultRedisDatabaseCount = 16; - private static readonly CommandBytes message = "message", pmessage = "pmessage", smessage = "smessage"; - private static readonly Message[] ReusableChangeDatabaseCommands = Enumerable.Range(0, DefaultRedisDatabaseCount).Select( i => Message.Create(i, CommandFlags.FireAndForget, RedisCommand.SELECT)).ToArray(); @@ -1669,6 +1667,130 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock } } + private enum PushKind + { + None, + Message, + PMessage, + SMessage, + Subscribe, + PSubscribe, + SSubscribe, + Unsubscribe, + PUnsubscribe, + SUnsubscribe, + } + private PushKind GetPushKind(in Sequence result, out RedisChannel channel) + { + var len = result.Length; + if (len < 2) + { + // for supported cases, we demand at least the kind and the subscription channel + channel = default; + return PushKind.None; + } + + const int MAX_LEN = 16; + Debug.Assert(MAX_LEN >= Enumerable.Max( + [ + PushMessage.Length, PushPMessage.Length, PushSMessage.Length, + PushSubscribe.Length, PushPSubscribe.Length, PushSSubscribe.Length, + PushUnsubscribe.Length, PushPUnsubscribe.Length, PushSUnsubscribe.Length, + ])); + ref readonly RawResult pushKind = ref result[0]; + var multiSegmentPayload = pushKind.Payload; + if (multiSegmentPayload.Length <= MAX_LEN) + { + var span = multiSegmentPayload.IsSingleSegment + ? multiSegmentPayload.First.Span + : CopyTo(stackalloc byte[MAX_LEN], multiSegmentPayload); + + var hash = FastHash.Hash64(span); + RedisChannel.RedisChannelOptions channelOptions = RedisChannel.RedisChannelOptions.None; + PushKind kind; + switch (hash) + { + case PushMessage.Hash when PushMessage.Is(hash, span) & len >= 3: + kind = PushKind.Message; + break; + case PushPMessage.Hash when PushPMessage.Is(hash, span) & len >= 4: + channelOptions = RedisChannel.RedisChannelOptions.Pattern; + kind = PushKind.PMessage; + break; + case PushSMessage.Hash when PushSMessage.Is(hash, span) & len >= 3: + channelOptions = RedisChannel.RedisChannelOptions.Sharded; + kind = PushKind.SMessage; + break; + case PushSubscribe.Hash when PushSubscribe.Is(hash, span): + kind = PushKind.Subscribe; + break; + case PushPSubscribe.Hash when PushPSubscribe.Is(hash, span): + channelOptions = RedisChannel.RedisChannelOptions.Pattern; + kind = PushKind.PSubscribe; + break; + case PushSSubscribe.Hash when PushSSubscribe.Is(hash, span): + channelOptions = RedisChannel.RedisChannelOptions.Sharded; + kind = PushKind.SSubscribe; + break; + case PushUnsubscribe.Hash when PushUnsubscribe.Is(hash, span): + kind = PushKind.Unsubscribe; + break; + case PushPUnsubscribe.Hash when PushPUnsubscribe.Is(hash, span): + channelOptions = RedisChannel.RedisChannelOptions.Pattern; + kind = PushKind.PUnsubscribe; + break; + case PushSUnsubscribe.Hash when PushSUnsubscribe.Is(hash, span): + channelOptions = RedisChannel.RedisChannelOptions.Sharded; + kind = PushKind.SUnsubscribe; + break; + default: + kind = PushKind.None; + break; + } + if (kind != PushKind.None) + { + // the channel is always the second element + channel = result[1].AsRedisChannel(ChannelPrefix, channelOptions); + return kind; + } + } + channel = default; + return PushKind.None; + + static ReadOnlySpan CopyTo(Span target, in ReadOnlySequence source) + { + source.CopyTo(target); + return target.Slice(0, (int)source.Length); + } + } + + [FastHash("message")] + private static partial class PushMessage { } + + [FastHash("pmessage")] + private static partial class PushPMessage { } + + [FastHash("smessage")] + private static partial class PushSMessage { } + + [FastHash("subscribe")] + private static partial class PushSubscribe { } + + [FastHash("psubscribe")] + private static partial class PushPSubscribe { } + + [FastHash("ssubscribe")] + private static partial class PushSSubscribe { } + + [FastHash("unsubscribe")] + private static partial class PushUnsubscribe { } + + [FastHash("punsubscribe")] + private static partial class PushPUnsubscribe { } + + [FastHash("sunsubscribe")] + private static partial class PushSUnsubscribe { } + private void MatchResult(in RawResult result) { // check to see if it could be an out-of-band pubsub message @@ -1679,85 +1801,87 @@ private void MatchResult(in RawResult result) // out of band message does not match to a queued message var items = result.GetItems(); - if (items.Length >= 3 && (items[0].IsEqual(message) || items[0].IsEqual(smessage))) + var kind = GetPushKind(items, out var subscriptionChannel); + switch (kind) { - _readStatus = items[0].IsEqual(message) ? ReadStatus.PubSubMessage : ReadStatus.PubSubSMessage; + case PushKind.Message: + case PushKind.SMessage: + _readStatus = kind is PushKind.Message ? ReadStatus.PubSubMessage : ReadStatus.PubSubSMessage; - // special-case the configuration change broadcasts (we don't keep that in the usual pub/sub registry) - var configChanged = muxer.ConfigurationChangedChannel; - if (configChanged != null && items[1].IsEqual(configChanged)) - { - EndPoint? blame = null; - try + // special-case the configuration change broadcasts (we don't keep that in the usual pub/sub registry) + var configChanged = muxer.ConfigurationChangedChannel; + if (configChanged != null && items[1].IsEqual(configChanged)) { - if (!items[2].IsEqual(CommonReplies.wildcard)) + EndPoint? blame = null; + try { - // We don't want to fail here, just trying to identify - _ = Format.TryParseEndPoint(items[2].GetString(), out blame); + if (!items[2].IsEqual(CommonReplies.wildcard)) + { + // We don't want to fail here, just trying to identify + _ = Format.TryParseEndPoint(items[2].GetString(), out blame); + } + } + catch + { + /* no biggie */ } - } - catch { /* no biggie */ } - Trace("Configuration changed: " + Format.ToString(blame)); - _readStatus = ReadStatus.Reconfigure; - muxer.ReconfigureIfNeeded(blame, true, "broadcast"); - } - // invoke the handlers - RedisChannel channel; - if (items[0].IsEqual(message)) - { - channel = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.RedisChannelOptions.None); - Trace("MESSAGE: " + channel); - } - else // see check on outer-if that restricts to message / smessage - { - channel = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.RedisChannelOptions.Sharded); - Trace("SMESSAGE: " + channel); - } - if (!channel.IsNull) - { - if (TryGetPubSubPayload(items[2], out var payload)) - { - _readStatus = ReadStatus.InvokePubSub; - muxer.OnMessage(channel, channel, payload); + Trace("Configuration changed: " + Format.ToString(blame)); + _readStatus = ReadStatus.Reconfigure; + muxer.ReconfigureIfNeeded(blame, true, "broadcast"); } - // could be multi-message: https://github.com/StackExchange/StackExchange.Redis/issues/2507 - else if (TryGetMultiPubSubPayload(items[2], out var payloads)) + + // invoke the handlers + if (!subscriptionChannel.IsNull) { - _readStatus = ReadStatus.InvokePubSub; - muxer.OnMessage(channel, channel, payloads); + Trace($"{kind}: {subscriptionChannel}"); + if (TryGetPubSubPayload(items[2], out var payload)) + { + _readStatus = ReadStatus.InvokePubSub; + muxer.OnMessage(subscriptionChannel, subscriptionChannel, payload); + } + // could be multi-message: https://github.com/StackExchange/StackExchange.Redis/issues/2507 + else if (TryGetMultiPubSubPayload(items[2], out var payloads)) + { + _readStatus = ReadStatus.InvokePubSub; + muxer.OnMessage(subscriptionChannel, subscriptionChannel, payloads); + } } - } - return; // AND STOP PROCESSING! - } - else if (items.Length >= 4 && items[0].IsEqual(pmessage)) - { - _readStatus = ReadStatus.PubSubPMessage; + return; // and stop processing + case PushKind.PMessage: + _readStatus = ReadStatus.PubSubPMessage; - var channel = items[2].AsRedisChannel(ChannelPrefix, RedisChannel.RedisChannelOptions.Pattern); - - Trace("PMESSAGE: " + channel); - if (!channel.IsNull) - { - if (TryGetPubSubPayload(items[3], out var payload)) + var messageChannel = items[2].AsRedisChannel(ChannelPrefix, RedisChannel.RedisChannelOptions.None); + if (!messageChannel.IsNull) { - var sub = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.RedisChannelOptions.Pattern); - - _readStatus = ReadStatus.InvokePubSub; - muxer.OnMessage(sub, channel, payload); + Trace($"{kind}: {messageChannel} via {subscriptionChannel}"); + if (TryGetPubSubPayload(items[3], out var payload)) + { + _readStatus = ReadStatus.InvokePubSub; + muxer.OnMessage(subscriptionChannel, messageChannel, payload); + } + else if (TryGetMultiPubSubPayload(items[3], out var payloads)) + { + _readStatus = ReadStatus.InvokePubSub; + muxer.OnMessage(subscriptionChannel, messageChannel, payloads); + } } - else if (TryGetMultiPubSubPayload(items[3], out var payloads)) + return; // and stop processing + case PushKind.SUnsubscribe when !PeekChannelMessage(RedisCommand.SUNSUBSCRIBE, subscriptionChannel): + // then it was *unsolicited* - this probably means the slot was migrated + // (otherwise, we'll let the command-processor deal with it) + _readStatus = ReadStatus.PubSubUnsubscribe; + var server = BridgeCouldBeNull?.ServerEndPoint; + if (server is not null && muxer.TryGetSubscription(subscriptionChannel, out var subscription)) { - var sub = items[1].AsRedisChannel(ChannelPrefix, RedisChannel.RedisChannelOptions.Pattern); - - _readStatus = ReadStatus.InvokePubSub; - muxer.OnMessage(sub, channel, payloads); + // wipe and reconnect; but: to where? + // counter-intuitively, the only server we *know* already knows the new route is: + // the outgoing server, since it had to change to MIGRATING etc; the new INCOMING server + // knows, but *we don't know who that is*, and other nodes: aren't guaranteed to know (yet) + muxer.DefaultSubscriber.ResubscribeToServer(subscription, subscriptionChannel, server, cause: PushSUnsubscribe.Text); } - } - return; // AND STOP PROCESSING! + return; // and STOP PROCESSING; unsolicited } - - // if it didn't look like "[p|s]message", then we still need to process the pending queue } Trace("Matching result..."); @@ -1875,6 +1999,19 @@ static bool TryGetMultiPubSubPayload(in RawResult value, out Sequence } } + private bool PeekChannelMessage(RedisCommand command, RedisChannel channel) + { + Message? msg; + bool haveMsg; + lock (_writtenAwaitingResponse) + { + haveMsg = _writtenAwaitingResponse.TryPeek(out msg); + } + + return haveMsg && msg is CommandChannelBase typed + && typed.Command == command && typed.Channel == channel; + } + private volatile Message? _activeMessage; internal void GetHeadMessages(out Message? now, out Message? next) @@ -2168,6 +2305,7 @@ internal enum ReadStatus MatchResultComplete, ResetArena, ProcessBufferComplete, + PubSubUnsubscribe, NA = -1, } private volatile ReadStatus _readStatus; diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index 8ff9610b0..9ade78c2d 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.SymbolStore; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -13,7 +14,7 @@ namespace StackExchange.Redis public partial class ConnectionMultiplexer { private RedisSubscriber? _defaultSubscriber; - private RedisSubscriber DefaultSubscriber => _defaultSubscriber ??= new RedisSubscriber(this, null); + internal RedisSubscriber DefaultSubscriber => _defaultSubscriber ??= new RedisSubscriber(this, null); private readonly ConcurrentDictionary subscriptions = new(); @@ -282,6 +283,17 @@ internal void GetSubscriberCounts(out int handlers, out int queues) internal ServerEndPoint? GetCurrentServer() => Volatile.Read(ref CurrentServer); internal void SetCurrentServer(ServerEndPoint? server) => CurrentServer = server; + // conditional clear + internal bool ClearCurrentServer(ServerEndPoint expected) + { + if (CurrentServer == expected) + { + CurrentServer = null; + return true; + } + + return false; + } /// /// Evaluates state and if we're not currently connected, clears the server reference. @@ -404,7 +416,7 @@ public ChannelMessageQueue Subscribe(RedisChannel channel, CommandFlags flags = return queue; } - public bool Subscribe(RedisChannel channel, Action? handler, ChannelMessageQueue? queue, CommandFlags flags) + private bool Subscribe(RedisChannel channel, Action? handler, ChannelMessageQueue? queue, CommandFlags flags) { ThrowIfNull(channel); if (handler == null && queue == null) { return true; } @@ -425,35 +437,58 @@ internal bool EnsureSubscribedToServer(Subscription sub, RedisChannel channel, C return ExecuteSync(message, sub.Processor, selected); } + internal void ResubscribeToServer(Subscription sub, RedisChannel channel, ServerEndPoint serverEndPoint, string cause) + { + // conditional: only if that's the server we were connected to, or "none"; we don't want to end up duplicated + if (sub.ClearCurrentServer(serverEndPoint) || !sub.IsConnected) + { + if (serverEndPoint.IsSubscriberConnected) + { + // we'll *try* for a simple resubscribe, following any -MOVED etc, but if that fails: fall back + // to full reconfigure; importantly, note that we've already recorded the disconnect + var message = sub.GetMessage(channel, SubscriptionAction.Subscribe, CommandFlags.None, false); + _ = ExecuteAsync(message, sub.Processor, serverEndPoint).ContinueWith( + t => multiplexer.ReconfigureIfNeeded(serverEndPoint.EndPoint, false, cause: cause), + TaskContinuationOptions.OnlyOnFaulted); + } + else + { + multiplexer.ReconfigureIfNeeded(serverEndPoint.EndPoint, false, cause: cause); + } + } + } + Task ISubscriber.SubscribeAsync(RedisChannel channel, Action handler, CommandFlags flags) => SubscribeAsync(channel, handler, null, flags); - public async Task SubscribeAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None) + Task ISubscriber.SubscribeAsync(RedisChannel channel, CommandFlags flags) => SubscribeAsync(channel, flags); + + public async Task SubscribeAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None, ServerEndPoint? server = null) { var queue = new ChannelMessageQueue(channel, this); - await SubscribeAsync(channel, null, queue, flags).ForAwait(); + await SubscribeAsync(channel, null, queue, flags, server).ForAwait(); return queue; } - public Task SubscribeAsync(RedisChannel channel, Action? handler, ChannelMessageQueue? queue, CommandFlags flags) + private Task SubscribeAsync(RedisChannel channel, Action? handler, ChannelMessageQueue? queue, CommandFlags flags, ServerEndPoint? server = null) { ThrowIfNull(channel); if (handler == null && queue == null) { return CompletedTask.Default(null); } var sub = multiplexer.GetOrAddSubscription(channel, flags); sub.Add(handler, queue); - return EnsureSubscribedToServerAsync(sub, channel, flags, false); + return EnsureSubscribedToServerAsync(sub, channel, flags, false, server); } - public Task EnsureSubscribedToServerAsync(Subscription sub, RedisChannel channel, CommandFlags flags, bool internalCall) + public Task EnsureSubscribedToServerAsync(Subscription sub, RedisChannel channel, CommandFlags flags, bool internalCall, ServerEndPoint? server = null) { if (sub.IsConnected) { return CompletedTask.Default(null); } // TODO: Cleanup old hangers here? sub.SetCurrentServer(null); // we're not appropriately connected, so blank it out for eligible reconnection var message = sub.GetMessage(channel, SubscriptionAction.Subscribe, flags, internalCall); - var selected = multiplexer.SelectServer(message); - return ExecuteAsync(message, sub.Processor, selected); + server ??= multiplexer.SelectServer(message); + return ExecuteAsync(message, sub.Processor, server); } public EndPoint? SubscribedEndpoint(RedisChannel channel) => multiplexer.GetSubscribedServer(channel)?.EndPoint; diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index 4084b4c33..ca247c38b 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -328,7 +328,7 @@ private ServerEndPoint[] MapForMutation() return arr; } - private ServerEndPoint? Select(int slot, RedisCommand command, CommandFlags flags, bool allowDisconnected) + internal ServerEndPoint? Select(int slot, RedisCommand command, CommandFlags flags, bool allowDisconnected) { // Only interested in primary/replica preferences flags = Message.GetPrimaryReplicaFlags(flags); diff --git a/tests/StackExchange.Redis.Tests/ClusterShardedTests.cs b/tests/StackExchange.Redis.Tests/ClusterShardedTests.cs index dd57483b9..8af0a1c7b 100644 --- a/tests/StackExchange.Redis.Tests/ClusterShardedTests.cs +++ b/tests/StackExchange.Redis.Tests/ClusterShardedTests.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -174,4 +177,174 @@ private async Task MigrateSlotForTestShardChannelAsync(bool rollback) Log("Slot already migrated."); } } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SubscribeToWrongServerAsync(bool sharded) + { + // the purpose of this test is to simulate subscribing while a node move is happening, i.e. we send + // the SSUBSCRIBE to the wrong server, get a -MOVED, and redirect; in particular: do we end up *knowing* + // where we actually subscribed to? + // + // note: to check our thinking, we also do this for regular non-sharded channels too; the point here + // being that this should behave *differently*, since there will be no -MOVED + var name = $"{Me()}:{Guid.NewGuid()}"; + var channel = sharded ? RedisChannel.Sharded(name) : RedisChannel.Literal(name).WithKeyRouting(); + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1); + + var asKey = (RedisKey)(byte[])channel!; + Assert.False(asKey.IsEmpty); + var shouldBeServer = conn.GetServer(asKey); // this is where it *should* go + + // now intentionally choose *a different* server + var server = conn.GetServers().First(s => !Equals(s.EndPoint, shouldBeServer.EndPoint)); + Log($"Should be {Format.ToString(shouldBeServer.EndPoint)}; routing via {Format.ToString(server.EndPoint)}"); + + var subscriber = Assert.IsType(conn.GetSubscriber()); + var serverEndpoint = conn.GetServerEndPoint(server.EndPoint); + Assert.Equal(server.EndPoint, serverEndpoint.EndPoint); + var queue = await subscriber.SubscribeAsync(channel, server: serverEndpoint); + await Task.Delay(50); + var actual = subscriber.SubscribedEndpoint(channel); + + if (sharded) + { + // we should end up at the correct node, following the -MOVED + Assert.Equal(shouldBeServer.EndPoint, actual); + } + else + { + // we should end up where we *actually sent the message* - there is no -MOVED + Assert.Equal(serverEndpoint.EndPoint, actual); + } + + Log("Unsubscribing..."); + await queue.UnsubscribeAsync(); + Log("Unsubscribed."); + } + + [Fact] + public async Task KeepSubscribedThroughSlotMigrationAsync() + { + await using var conn = Create(require: RedisFeatures.v7_0_0_rc1, allowAdmin: true); + var name = $"{Me()}:{Guid.NewGuid()}"; + var channel = RedisChannel.Sharded(name); + var subscriber = conn.GetSubscriber(); + var queue = await subscriber.SubscribeAsync(channel); + await Task.Delay(50); + var actual = subscriber.SubscribedEndpoint(channel); + Assert.NotNull(actual); + + var asKey = (RedisKey)(byte[])channel!; + Assert.False(asKey.IsEmpty); + var slot = conn.GetHashSlot(asKey); + var viaMap = conn.ServerSelectionStrategy.Select(slot, RedisCommand.SSUBSCRIBE, CommandFlags.None, allowDisconnected: false); + + Log($"Slot {slot}, subscribed to {Format.ToString(actual)} (mapped to {Format.ToString(viaMap?.EndPoint)})"); + Assert.NotNull(viaMap); + Assert.Equal(actual, viaMap.EndPoint); + + var oldServer = conn.GetServer(asKey); // this is where it *should* go + + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5))) + { + // now publish... we *expect* things to have sorted themselves out + var msg = Guid.NewGuid().ToString(); + var count = await subscriber.PublishAsync(channel, msg); + Assert.Equal(1, count); + + Log("Waiting for message on original subscription..."); + var received = await queue.ReadAsync(timeout.Token); + Log($"Message received: {received.Message}"); + Assert.Equal(msg, (string)received.Message!); + } + + // now intentionally choose *a different* server + var newServer = conn.GetServers().First(s => !Equals(s.EndPoint, oldServer.EndPoint)); + + var nodes = await newServer.ClusterNodesAsync(); + Assert.NotNull(nodes); + var fromNode = nodes[oldServer.EndPoint]?.NodeId; + var toNode = nodes[newServer.EndPoint]?.NodeId; + Assert.NotNull(fromNode); + Assert.NotNull(toNode); + Assert.Equal(oldServer.EndPoint, nodes.GetBySlot(slot)?.EndPoint); + + var ep = subscriber.SubscribedEndpoint(channel); + Log($"Endpoint before migration: {Format.ToString(ep)}"); + Log($"Migrating slot {slot} to {Format.ToString(newServer.EndPoint)}; node {fromNode} -> {toNode}..."); + + // see https://redis.io/docs/latest/commands/cluster-setslot/#redis-cluster-live-resharding-explained + WriteLog("IMPORTING", await newServer.ExecuteAsync("CLUSTER", "SETSLOT", slot, "IMPORTING", fromNode)); + WriteLog("MIGRATING", await oldServer.ExecuteAsync("CLUSTER", "SETSLOT", slot, "MIGRATING", toNode)); + + while (true) + { + var keys = (await oldServer.ExecuteAsync("CLUSTER", "GETKEYSINSLOT", slot, 100)).AsRedisKeyArray()!; + Log($"Migrating {keys.Length} keys..."); + if (keys.Length == 0) break; + foreach (var key in keys) + { + await conn.GetDatabase().KeyMigrateAsync(key, newServer.EndPoint, migrateOptions: MigrateOptions.None); + } + } + + WriteLog("NODE (old)", await newServer.ExecuteAsync("CLUSTER", "SETSLOT", slot, "NODE", toNode)); + WriteLog("NODE (new)", await oldServer.ExecuteAsync("CLUSTER", "SETSLOT", slot, "NODE", toNode)); + + void WriteLog(string caption, RedisResult result) + { + if (result.IsNull) + { + Log($"{caption}: null"); + } + else if (result.Length >= 0) + { + var arr = result.AsRedisValueArray()!; + Log($"{caption}: {arr.Length} items"); + foreach (var item in arr) + { + Log($" {item}"); + } + } + else + { + Log($"{caption}: {result}"); + } + } + + Log("Migration initiated; checking node state..."); + await Task.Delay(100); + ep = subscriber.SubscribedEndpoint(channel); + Log($"Endpoint after migration: {Format.ToString(ep)}"); + Assert.True( + ep is null || ep == newServer.EndPoint, + "Target server after migration should be null or the new server"); + + nodes = await newServer.ClusterNodesAsync(); + Assert.NotNull(nodes); + Assert.Equal(newServer.EndPoint, nodes.GetBySlot(slot)?.EndPoint); + await conn.ConfigureAsync(); + Assert.Equal(newServer, conn.GetServer(asKey)); + + using (var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5))) + { + // now publish... we *expect* things to have sorted themselves out + var msg = Guid.NewGuid().ToString(); + var count = await subscriber.PublishAsync(channel, msg); + Assert.Equal(1, count); + + Log("Waiting for message on moved subscription..."); + var received = await queue.ReadAsync(timeout.Token); + Log($"Message received: {received.Message}"); + Assert.Equal(msg, (string)received.Message!); + ep = subscriber.SubscribedEndpoint(channel); + Log($"Endpoint after receiving message: {Format.ToString(ep)}"); + } + + Log("Unsubscribing..."); + await queue.UnsubscribeAsync(); + Log("Unsubscribed."); + } } From 9c6023fbd06efa9fbc61a96f821445477f3f891d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 13 Oct 2025 16:21:25 +0100 Subject: [PATCH 385/435] release notes 2.9.32 --- docs/ReleaseNotes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 615f44497..fa2773519 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,8 @@ Current package versions: ## Unreleased +## 2.9.32 + - Fix `SSUBSCRIBE` routing during slot migrations ([#2969 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2969)) ## 2.9.25 From e5120f7df045ea3671840b3193887319adc042e9 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 4 Nov 2025 17:00:18 +0000 Subject: [PATCH 386/435] docker image: use client-libs-test (#2976) * docker image: use client-libs-test (which has better preview support) * Update Dockerfile --- StackExchange.Redis.sln | 1 + tests/RedisConfigs/.docker/Redis/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index 2ed4ebfb3..adb1291de 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -20,6 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution docs\ReleaseNotes.md = docs\ReleaseNotes.md Shared.ruleset = Shared.ruleset version.json = version.json + tests\RedisConfigs\.docker\Redis\Dockerfile = tests\RedisConfigs\.docker\Redis\Dockerfile EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RedisConfigs", "RedisConfigs", "{96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}" diff --git a/tests/RedisConfigs/.docker/Redis/Dockerfile b/tests/RedisConfigs/.docker/Redis/Dockerfile index 424abd1cd..28bf78ebe 100644 --- a/tests/RedisConfigs/.docker/Redis/Dockerfile +++ b/tests/RedisConfigs/.docker/Redis/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:8.2.0 +FROM redislabs/client-libs-test:8.4-RC1-pre.2 COPY --from=configs ./Basic /data/Basic/ COPY --from=configs ./Failover /data/Failover/ From 24ed30c9936e613daaee04c2315646ad66cf8a09 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 5 Nov 2025 16:01:26 +0000 Subject: [PATCH 387/435] Support MSETEX (#2977) * Support MSETEX * release notes * prefer WriteBulkString("WHATEVER"u8) over WriteBulkString(RedisLiterals.WHATEVER) * create Expiration as top-level concept * The method is no longer ambiguous, yay (also: typos) * Mark the MSETEX API as [Experimental], with docs * actually, we can't make that [SER002] because of overload resolution --- Directory.Build.props | 2 +- StackExchange.Redis.sln.DotSettings | 23 +- docs/ReleaseNotes.md | 2 + docs/exp/SER002.md | 26 ++ src/StackExchange.Redis/CommandMap.cs | 4 +- src/StackExchange.Redis/Enums/RedisCommand.cs | 2 + src/StackExchange.Redis/Experiments.cs | 2 + src/StackExchange.Redis/Expiration.cs | 273 ++++++++++++++++++ .../Interfaces/IDatabase.cs | 72 +++-- .../Interfaces/IDatabaseAsync.cs | 5 +- .../KeyspaceIsolation/KeyPrefixed.cs | 3 + .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 3 + src/StackExchange.Redis/Message.cs | 56 +++- .../PublicAPI/PublicAPI.Shipped.txt | 18 +- .../RedisDatabase.ExpiryToken.cs | 76 ----- src/StackExchange.Redis/RedisDatabase.cs | 164 ++++++----- src/StackExchange.Redis/RedisFeatures.cs | 3 +- src/StackExchange.Redis/RedisLiterals.cs | 3 - .../ExpiryTokenTests.cs | 29 +- tests/StackExchange.Redis.Tests/MSetTests.cs | 166 +++++++++++ 20 files changed, 725 insertions(+), 207 deletions(-) create mode 100644 docs/exp/SER002.md create mode 100644 src/StackExchange.Redis/Expiration.cs delete mode 100644 src/StackExchange.Redis/RedisDatabase.ExpiryToken.cs create mode 100644 tests/StackExchange.Redis.Tests/MSetTests.cs diff --git a/Directory.Build.props b/Directory.Build.props index 42de5875c..9f10eddcd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,7 +10,7 @@ true $(MSBuildThisFileDirectory)Shared.ruleset NETSDK1069 - $(NoWarn);NU5105;NU1507;SER001 + $(NoWarn);NU5105;NU1507;SER001;SER002 https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes https://stackexchange.github.io/StackExchange.Redis/ MIT diff --git a/StackExchange.Redis.sln.DotSettings b/StackExchange.Redis.sln.DotSettings index b72a49d2c..216edbcca 100644 --- a/StackExchange.Redis.sln.DotSettings +++ b/StackExchange.Redis.sln.DotSettings @@ -1,5 +1,26 @@  OK PONG + True + True + True + True + True + True + True + True + True + True + True True - True \ No newline at end of file + True + True + True + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index fa2773519..d11ddb7fd 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,8 @@ Current package versions: ## Unreleased +- Support `MSETEX` (Redis 8.4.0) for multi-key operations with expiration ([#2977 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2977)) + ## 2.9.32 - Fix `SSUBSCRIBE` routing during slot migrations ([#2969 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2969)) diff --git a/docs/exp/SER002.md b/docs/exp/SER002.md new file mode 100644 index 000000000..21a2990c6 --- /dev/null +++ b/docs/exp/SER002.md @@ -0,0 +1,26 @@ +Redis 8.4 is currently in preview and may be subject to change. + +New features in Redis 8.4 include: + +- [`MSETEX`](https://github.com/redis/redis/pull/14434) for setting multiple strings with expiry +- [`XREADGROUP ... CLAIM`](https://github.com/redis/redis/pull/14402) for simplifed stream consumption +- [`SET ... {IFEQ|IFNE|IFDEQ|IFDNE}`, `DELEX` and `DIGEST`](https://github.com/redis/redis/pull/14434) for checked (CAS/CAD) string operations + +The corresponding library feature must also be considered subject to change: + +1. Existing bindings may cease working correctly if the underlying server API changes. +2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time + or run-time breaks. + +While this seems *unlikely*, it must be considered a possibility. If you acknowledge this, you can suppress +this warning by adding the following to your `csproj` file: + +```xml +$(NoWarn);SER002 +``` + +or more granularly / locally in C#: + +``` c# +#pragma warning disable SER002 +``` \ No newline at end of file diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index 1f31d3224..663c61b36 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -27,7 +27,7 @@ public sealed class CommandMap RedisCommand.KEYS, RedisCommand.MIGRATE, RedisCommand.MOVE, RedisCommand.OBJECT, RedisCommand.RANDOMKEY, RedisCommand.RENAME, RedisCommand.RENAMENX, RedisCommand.SCAN, - RedisCommand.BITOP, RedisCommand.MSETNX, + RedisCommand.BITOP, RedisCommand.MSETEX, RedisCommand.MSETNX, RedisCommand.BLPOP, RedisCommand.BRPOP, RedisCommand.BRPOPLPUSH, // yeah, me neither! @@ -53,7 +53,7 @@ public sealed class CommandMap RedisCommand.KEYS, RedisCommand.MIGRATE, RedisCommand.MOVE, RedisCommand.OBJECT, RedisCommand.RANDOMKEY, RedisCommand.RENAME, RedisCommand.RENAMENX, RedisCommand.SORT, RedisCommand.SCAN, - RedisCommand.BITOP, RedisCommand.MSETNX, + RedisCommand.BITOP, RedisCommand.MSETEX, RedisCommand.MSETNX, RedisCommand.BLPOP, RedisCommand.BRPOP, RedisCommand.BRPOPLPUSH, // yeah, me neither! diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 7a0c2f08d..6138d2609 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -122,6 +122,7 @@ internal enum RedisCommand MONITOR, MOVE, MSET, + MSETEX, MSETNX, MULTI, @@ -336,6 +337,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.MIGRATE: case RedisCommand.MOVE: case RedisCommand.MSET: + case RedisCommand.MSETEX: case RedisCommand.MSETNX: case RedisCommand.PERSIST: case RedisCommand.PEXPIRE: diff --git a/src/StackExchange.Redis/Experiments.cs b/src/StackExchange.Redis/Experiments.cs index 577c9f8c9..9234f9f4e 100644 --- a/src/StackExchange.Redis/Experiments.cs +++ b/src/StackExchange.Redis/Experiments.cs @@ -9,6 +9,8 @@ internal static class Experiments { public const string UrlFormat = "https://stackexchange.github.io/StackExchange.Redis/exp/"; public const string VectorSets = "SER001"; + // ReSharper disable once InconsistentNaming + public const string Server_8_4 = "SER002"; } } diff --git a/src/StackExchange.Redis/Expiration.cs b/src/StackExchange.Redis/Expiration.cs new file mode 100644 index 000000000..786cc928c --- /dev/null +++ b/src/StackExchange.Redis/Expiration.cs @@ -0,0 +1,273 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// Configures the expiration behaviour of a command. +/// +public readonly struct Expiration +{ + /* + Redis expiration supports different modes: + - (nothing) - do nothing; implicit wipe for writes, nothing for reads + - PERSIST - explicit wipe of expiry + - KEEPTTL - sets no expiry, but leaves any existing expiry alone + - EX {s} - relative expiry in seconds + - PX {ms} - relative expiry in milliseconds + - EXAT {s} - absolute expiry in seconds + - PXAT {ms} - absolute expiry in milliseconds + + We need to distinguish between these 6 scenarios, which we can logically do with 3 bits (8 options). + So; we'll use a ulong for the value, reserving the top 3 bits for the mode. + */ + + /// + /// Default expiration behaviour. For writes, this is typically no expiration. For reads, this is typically no action. + /// + public static Expiration Default => s_Default; + + /// + /// Explicitly retain the existing expiry, if one. This is valid in some (not all) write scenarios. + /// + public static Expiration KeepTtl => s_KeepTtl; + + /// + /// Explicitly remove the existing expiry, if one. This is valid in some (not all) read scenarios. + /// + public static Expiration Persist => s_Persist; + + /// + /// Expire at the specified absolute time. + /// + public Expiration(DateTime when) + { + if (when == DateTime.MaxValue) + { + _valueAndMode = s_Default._valueAndMode; + return; + } + + long millis = GetUnixTimeMilliseconds(when); + if ((millis % 1000) == 0) + { + Init(ExpirationMode.AbsoluteSeconds, millis / 1000, out _valueAndMode); + } + else + { + Init(ExpirationMode.AbsoluteMilliseconds, millis, out _valueAndMode); + } + } + + /// + /// Expire at the specified absolute time. + /// + public static implicit operator Expiration(DateTime when) => new(when); + + /// + /// Expire at the specified absolute time. + /// + public static implicit operator Expiration(TimeSpan ttl) => new(ttl); + + /// + /// Expire at the specified relative time. + /// + public Expiration(TimeSpan ttl) + { + if (ttl == TimeSpan.MaxValue) + { + _valueAndMode = s_Default._valueAndMode; + return; + } + + var millis = ttl.Ticks / TimeSpan.TicksPerMillisecond; + if ((millis % 1000) == 0) + { + Init(ExpirationMode.RelativeSeconds, millis / 1000, out _valueAndMode); + } + else + { + Init(ExpirationMode.RelativeMilliseconds, millis, out _valueAndMode); + } + } + + private readonly ulong _valueAndMode; + + private static void Init(ExpirationMode mode, long value, out ulong valueAndMode) + { + // check the caller isn't using the top 3 bits that we have reserved; this includes checking for -ve values + ulong uValue = (ulong)value; + if ((uValue & ~ValueMask) != 0) Throw(); + valueAndMode = (uValue & ValueMask) | ((ulong)mode << 61); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(value)); + } + + private Expiration(ExpirationMode mode, long value) => Init(mode, value, out _valueAndMode); + + private enum ExpirationMode : byte + { + Default = 0, + RelativeSeconds = 1, + RelativeMilliseconds = 2, + AbsoluteSeconds = 3, + AbsoluteMilliseconds = 4, + KeepTtl = 5, + Persist = 6, + NotUsed = 7, // just to ensure all 8 possible values are covered + } + + private const ulong ValueMask = (~0UL) >> 3; + internal long Value => unchecked((long)(_valueAndMode & ValueMask)); + private ExpirationMode Mode => (ExpirationMode)(_valueAndMode >> 61); // note unsigned, no need to mask + + internal bool IsKeepTtl => Mode is ExpirationMode.KeepTtl; + internal bool IsPersist => Mode is ExpirationMode.Persist; + internal bool IsNone => Mode is ExpirationMode.Default; + internal bool IsNoneOrKeepTtl => Mode is ExpirationMode.Default or ExpirationMode.KeepTtl; + internal bool IsAbsolute => Mode is ExpirationMode.AbsoluteSeconds or ExpirationMode.AbsoluteMilliseconds; + internal bool IsRelative => Mode is ExpirationMode.RelativeSeconds or ExpirationMode.RelativeMilliseconds; + + internal bool IsMilliseconds => + Mode is ExpirationMode.RelativeMilliseconds or ExpirationMode.AbsoluteMilliseconds; + + internal bool IsSeconds => Mode is ExpirationMode.RelativeSeconds or ExpirationMode.AbsoluteSeconds; + + private static readonly Expiration s_Default = new(ExpirationMode.Default, 0); + + private static readonly Expiration s_KeepTtl = new(ExpirationMode.KeepTtl, 0), + s_Persist = new(ExpirationMode.Persist, 0); + + private static void ThrowExpiryAndKeepTtl() => + // ReSharper disable once NotResolvedInText + throw new ArgumentException(message: "Cannot specify both expiry and keepTtl.", paramName: "keepTtl"); + + private static void ThrowExpiryAndPersist() => + // ReSharper disable once NotResolvedInText + throw new ArgumentException(message: "Cannot specify both expiry and persist.", paramName: "persist"); + + internal static Expiration CreateOrPersist(in TimeSpan? ttl, bool persist) + { + if (persist) + { + if (ttl.HasValue) ThrowExpiryAndPersist(); + return s_Persist; + } + + return ttl.HasValue ? new(ttl.GetValueOrDefault()) : s_Default; + } + + internal static Expiration CreateOrKeepTtl(in TimeSpan? ttl, bool keepTtl) + { + if (keepTtl) + { + if (ttl.HasValue) ThrowExpiryAndKeepTtl(); + return s_KeepTtl; + } + + return ttl.HasValue ? new(ttl.GetValueOrDefault()) : s_Default; + } + + internal static long GetUnixTimeMilliseconds(DateTime when) + { + return when.Kind switch + { + DateTimeKind.Local or DateTimeKind.Utc => (when.ToUniversalTime() - RedisBase.UnixEpoch).Ticks / + TimeSpan.TicksPerMillisecond, + _ => ThrowKind(), + }; + + static long ThrowKind() => + throw new ArgumentException("Expiry time must be either Utc or Local", nameof(when)); + } + + internal static Expiration CreateOrPersist(in DateTime? when, bool persist) + { + if (persist) + { + if (when.HasValue) ThrowExpiryAndPersist(); + return s_Persist; + } + + return when.HasValue ? new(when.GetValueOrDefault()) : s_Default; + } + + internal static Expiration CreateOrKeepTtl(in DateTime? ttl, bool keepTtl) + { + if (keepTtl) + { + if (ttl.HasValue) ThrowExpiryAndKeepTtl(); + return s_KeepTtl; + } + + return ttl.HasValue ? new(ttl.GetValueOrDefault()) : s_Default; + } + + internal RedisValue Operand => GetOperand(out _); + + internal RedisValue GetOperand(out long value) + { + value = Value; + var mode = Mode; + return mode switch + { + ExpirationMode.KeepTtl => RedisLiterals.KEEPTTL, + ExpirationMode.Persist => RedisLiterals.PERSIST, + ExpirationMode.RelativeSeconds => RedisLiterals.EX, + ExpirationMode.RelativeMilliseconds => RedisLiterals.PX, + ExpirationMode.AbsoluteSeconds => RedisLiterals.EXAT, + ExpirationMode.AbsoluteMilliseconds => RedisLiterals.PXAT, + _ => RedisValue.Null, + }; + } + + private static void ThrowMode(ExpirationMode mode) => + throw new InvalidOperationException("Unknown mode: " + mode); + + /// + public override string ToString() => Mode switch + { + ExpirationMode.Default or ExpirationMode.NotUsed => "", + ExpirationMode.KeepTtl => "KEEPTTL", + ExpirationMode.Persist => "PERSIST", + _ => $"{Operand} {Value}", + }; + + /// + public override int GetHashCode() => _valueAndMode.GetHashCode(); + + /// + public override bool Equals(object? obj) => obj is Expiration other && _valueAndMode == other._valueAndMode; + + internal int Tokens => Mode switch + { + ExpirationMode.Default or ExpirationMode.NotUsed => 0, + ExpirationMode.KeepTtl or ExpirationMode.Persist => 1, + _ => 2, + }; + + internal void WriteTo(PhysicalConnection physical) + { + var mode = Mode; + switch (Mode) + { + case ExpirationMode.Default or ExpirationMode.NotUsed: + break; + case ExpirationMode.KeepTtl: + physical.WriteBulkString("KEEPTTL"u8); + break; + case ExpirationMode.Persist: + physical.WriteBulkString("PERSIST"u8); + break; + default: + physical.WriteBulkString(mode switch + { + ExpirationMode.RelativeSeconds => "EX"u8, + ExpirationMode.RelativeMilliseconds => "PX"u8, + ExpirationMode.AbsoluteSeconds => "EXAT"u8, + ExpirationMode.AbsoluteMilliseconds => "PXAT"u8, + _ => default, + }); + physical.WriteBulkString(Value); + break; + } + } +} diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 6c52e89bd..fd4fb3e30 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -95,7 +95,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// /// Removes the specified member from the geo sorted set stored at key. - /// Non existing members are ignored. + /// Non-existing members are ignored. /// /// The key of the set. /// The geo value to remove. @@ -144,7 +144,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command. - /// Non existing elements are reported as NULL elements of the array. + /// Non-existing elements are reported as NULL elements of the array. /// /// GeoPosition?[] GeoPosition(RedisKey key, RedisValue[] members, CommandFlags flags = CommandFlags.None); @@ -157,7 +157,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// /// The command returns an array where each element is a two elements array representing longitude and latitude (x,y) of each member name passed as argument to the command. - /// Non existing elements are reported as NULL elements of the array. + /// Non-existing elements are reported as NULL elements of the array. /// /// GeoPosition? GeoPosition(RedisKey key, RedisValue member, CommandFlags flags = CommandFlags.None); @@ -203,7 +203,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// The set member to use as the center of the shape. /// The shape to use to bound the geo search. /// The maximum number of results to pull back. - /// Whether or not to terminate the search after finding results. Must be true of count is -1. + /// Whether to terminate the search after finding results. Must be true of count is -1. /// The order to sort by (defaults to unordered). /// The search options to use. /// The flags for this operation. @@ -220,7 +220,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// The latitude of the center point. /// The shape to use to bound the geo search. /// The maximum number of results to pull back. - /// Whether or not to terminate the search after finding results. Must be true of count is -1. + /// Whether to terminate the search after finding results. Must be true of count is -1. /// The order to sort by (defaults to unordered). /// The search options to use. /// The flags for this operation. @@ -237,7 +237,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// The set member to use as the center of the shape. /// The shape to use to bound the geo search. /// The maximum number of results to pull back. - /// Whether or not to terminate the search after finding results. Must be true of count is -1. + /// Whether to terminate the search after finding results. Must be true of count is -1. /// The order to sort by (defaults to unordered). /// If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set. /// The flags for this operation. @@ -255,7 +255,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// The latitude of the center point. /// The shape to use to bound the geo search. /// The maximum number of results to pull back. - /// Whether or not to terminate the search after finding results. Must be true of count is -1. + /// Whether to terminate the search after finding results. Must be true of count is -1. /// The order to sort by (defaults to unordered). /// If set to true, the resulting set will be a regular sorted-set containing only distances, rather than a geo-encoded sorted-set. /// The flags for this operation. @@ -274,13 +274,13 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The value at field after the decrement operation. /// - /// The range of values supported by HINCRBY is limited to 64 bit signed integers. + /// The range of values supported by HINCRBY is limited to 64-bit signed integers. /// /// long HashDecrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None); /// - /// Decrement the specified field of an hash stored at key, and representing a floating point number, by the specified decrement. + /// Decrement the specified field of a hash stored at key, and representing a floating point number, by the specified decrement. /// If the field does not exist, it is set to 0 before performing the operation. /// /// The key of the hash. @@ -336,7 +336,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// under which condition the expiration will be set using . /// The flags to use for this operation. /// - /// Empty array if the key does not exist. Otherwise returns an array where each item is the result of operation for given fields: + /// Empty array if the key does not exist. Otherwise, returns an array where each item is the result of operation for given fields: /// /// /// Result @@ -363,7 +363,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, TimeSpan expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None); /// - /// Set the time out on a field of the given set of fields of hash. + /// Set the time-out on a field of the given set of fields of hash. /// After the timeout has expired, the field of the hash will automatically be deleted. /// /// The key of the hash. @@ -372,7 +372,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// under which condition the expiration will be set using . /// The flags to use for this operation. /// - /// Empty array if the key does not exist. Otherwise returns an array where each item is the result of operation for given fields: + /// Empty array if the key does not exist. Otherwise, returns an array where each item is the result of operation for given fields: /// /// /// Result @@ -405,7 +405,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// The fields in the hash to get expire time. /// The flags to use for this operation. /// - /// Empty array if the key does not exist. Otherwise returns the result of operation for given fields: + /// Empty array if the key does not exist. Otherwise, returns the result of operation for given fields: /// /// /// Result @@ -434,7 +434,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// The fields in the hash to remove expire time. /// The flags to use for this operation. /// - /// Empty array if the key does not exist. Otherwise returns the result of operation for given fields: + /// Empty array if the key does not exist. Otherwise, returns the result of operation for given fields: /// /// /// Result @@ -463,7 +463,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// The fields in the hash to get expire time. /// The flags to use for this operation. /// - /// Empty array if the key does not exist. Otherwise returns the result of operation for given fields: + /// Empty array if the key does not exist. Otherwise, returns the result of operation for given fields: /// /// /// Result @@ -680,13 +680,13 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// The flags to use for this operation. /// The value at field after the increment operation. /// - /// The range of values supported by HINCRBY is limited to 64 bit signed integers. + /// The range of values supported by HINCRBY is limited to 64-bit signed integers. /// /// long HashIncrement(RedisKey key, RedisValue hashField, long value = 1, CommandFlags flags = CommandFlags.None); /// - /// Increment the specified field of an hash stored at key, and representing a floating point number, by the specified increment. + /// Increment the specified field of a hash stored at key, and representing a floating point number, by the specified increment. /// If the field does not exist, it is set to 0 before performing the operation. /// /// The key of the hash. @@ -810,7 +810,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// if field is a new field in the hash and value was set, if field already exists in the hash and the value was updated. /// /// See - /// , + /// and /// . /// bool HashSet(RedisKey key, RedisValue hashField, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None); @@ -873,7 +873,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync long HyperLogLogLength(RedisKey[] keys, CommandFlags flags = CommandFlags.None); /// - /// Merge multiple HyperLogLog values into an unique value that will approximate the cardinality of the union of the observed Sets of the source HyperLogLog structures. + /// Merge multiple HyperLogLog values into a unique value that will approximate the cardinality of the union of the observed Sets of the source HyperLogLog structures. /// /// The key of the merged hyperloglog. /// The key of the first hyperloglog to merge. @@ -883,7 +883,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync void HyperLogLogMerge(RedisKey destination, RedisKey first, RedisKey second, CommandFlags flags = CommandFlags.None); /// - /// Merge multiple HyperLogLog values into an unique value that will approximate the cardinality of the union of the observed Sets of the source HyperLogLog structures. + /// Merge multiple HyperLogLog values into a unique value that will approximate the cardinality of the union of the observed Sets of the source HyperLogLog structures. /// /// The key of the merged hyperloglog. /// The keys of the hyperloglogs to merge. @@ -920,7 +920,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// if the key was removed. /// /// See - /// , + /// and /// . /// bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -1045,8 +1045,8 @@ public partial interface IDatabase : IRedis, IDatabaseAsync /// /// /// See - /// , - /// , + /// or + /// or /// . /// /// @@ -1116,7 +1116,7 @@ public partial interface IDatabase : IRedis, IDatabaseAsync bool KeyMove(RedisKey key, int database, CommandFlags flags = CommandFlags.None); /// - /// Remove the existing timeout on key, turning the key from volatile (a key with an expire set) to persistent (a key that will never expire as no timeout is associated). + /// Remove the existing timeout on key, turning the key from volatile (a key with an expiry set) to persistent (a key that will never expire as no timeout is associated). /// /// The key to persist. /// The flags to use for this operation. @@ -3314,8 +3314,8 @@ IEnumerable SortedSetScan( /// /// Implements the longest common subsequence algorithm between the values at and , - /// returning the legnth of the common sequence. - /// Note that this is different than the longest common string algorithm, + /// returning the length of the common sequence. + /// Note that this is different to the longest common string algorithm, /// since matching characters in the string does not need to be contiguous. /// /// The key of the first string. @@ -3372,8 +3372,26 @@ IEnumerable SortedSetScan( /// See /// , /// . + /// . + /// + bool StringSet(KeyValuePair[] values, When when, CommandFlags flags); + + /// + /// Sets the given keys to their respective values, optionally including expiration. + /// If is specified, this will not perform any operation at all even if just a single key already exists. + /// + /// The keys and values to set. + /// Which condition to set the value under (defaults to always). + /// The expiry to set. + /// The flags to use for this operation. + /// if the keys were set, otherwise. + /// + /// See + /// , + /// . + /// . /// - bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); + bool StringSet(KeyValuePair[] values, When when = When.Always, Expiration expiry = default, CommandFlags flags = CommandFlags.None); /// /// Atomically sets key to value and returns the previous value (if any) stored at . diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 0bc7b4867..6515740af 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -831,7 +831,10 @@ IAsyncEnumerable SortedSetScanAsync( Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); /// - Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None); + Task StringSetAsync(KeyValuePair[] values, When when, CommandFlags flags); + + /// + Task StringSetAsync(KeyValuePair[] values, When when = When.Always, Expiration expiry = default, CommandFlags flags = CommandFlags.None); /// Task StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 61a6f44c4..1651d1069 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -774,6 +774,9 @@ public Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFl public Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.StringSetAsync(ToInner(values), when, flags); + public Task StringSetAsync(KeyValuePair[] values, When when, Expiration expiry, CommandFlags flags) => + Inner.StringSetAsync(ToInner(values), when, expiry, flags); + public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when) => Inner.StringSetAsync(ToInner(key), value, expiry, when); public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index 2a139694e..3965625f9 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -756,6 +756,9 @@ public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) = public bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.StringSet(ToInner(values), when, flags); + public bool StringSet(KeyValuePair[] values, When when, Expiration expiry, CommandFlags flags) => + Inner.StringSet(ToInner(values), when, expiry, flags); + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when) => Inner.StringSet(ToInner(key), value, expiry, when); public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) => diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 0c9eb4c92..c8d433d17 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -391,6 +391,9 @@ public static Message Create( public static Message CreateInSlot(int db, int slot, CommandFlags flags, RedisCommand command, RedisValue[] values) => new CommandSlotValuesMessage(db, slot, flags, command, values); + public static Message Create(int db, CommandFlags flags, RedisCommand command, KeyValuePair[] values, Expiration expiry, When when) + => new MultiSetMessage(db, flags, command, values, expiry, when); + /// Gets whether this is primary-only. /// /// Note that the constructor runs the switch statement above, so @@ -842,13 +845,13 @@ protected override void WriteImpl(PhysicalConnection physical) physical.WriteBulkString(_protocolVersion); if (!string.IsNullOrWhiteSpace(_password)) { - physical.WriteBulkString(RedisLiterals.AUTH); + physical.WriteBulkString("AUTH"u8); physical.WriteBulkString(string.IsNullOrWhiteSpace(_username) ? RedisLiterals.@default : _username); physical.WriteBulkString(_password); } if (!string.IsNullOrWhiteSpace(_clientName)) { - physical.WriteBulkString(RedisLiterals.SETNAME); + physical.WriteBulkString("SETNAME"u8); physical.WriteBulkString(_clientName); } } @@ -1691,6 +1694,55 @@ protected override void WriteImpl(PhysicalConnection physical) public override int ArgCount => values.Length; } + private sealed class MultiSetMessage(int db, CommandFlags flags, RedisCommand command, KeyValuePair[] values, Expiration expiry, When when) : Message(db, flags, command) + { + public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) + { + int slot = ServerSelectionStrategy.NoSlot; + for (int i = 0; i < values.Length; i++) + { + slot = serverSelectionStrategy.CombineSlot(slot, values[i].Key); + } + return slot; + } + + // we support: + // - MSET {key1} {value1} [{key2} {value2}...] + // - MSETNX {key1} {value1} [{key2} {value2}...] + // - MSETEX {count} {key1} {value1} [{key2} {value2}...] [standard-expiry-tokens] + public override int ArgCount => Command == RedisCommand.MSETEX + ? (1 + (2 * values.Length) + expiry.Tokens + (when is When.Exists or When.NotExists ? 1 : 0)) + : (2 * values.Length); // MSET/MSETNX only support simple syntax + + protected override void WriteImpl(PhysicalConnection physical) + { + var cmd = Command; + physical.WriteHeader(cmd, ArgCount); + if (cmd == RedisCommand.MSETEX) // need count prefix + { + physical.WriteBulkString(values.Length); + } + for (int i = 0; i < values.Length; i++) + { + physical.Write(values[i].Key); + physical.WriteBulkString(values[i].Value); + } + if (cmd == RedisCommand.MSETEX) // allow expiry/mode tokens + { + expiry.WriteTo(physical); + switch (when) + { + case When.Exists: + physical.WriteBulkString("XX"u8); + break; + case When.NotExists: + physical.WriteBulkString("NX"u8); + break; + } + } + } + } + private sealed class CommandValueChannelMessage : CommandChannelBase { private readonly RedisValue value; diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 0abb20043..ba60cdc8c 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -779,7 +779,7 @@ StackExchange.Redis.IDatabase.StringLongestCommonSubsequenceWithMatches(StackExc StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when) -> bool StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> bool -StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> bool StackExchange.Redis.IDatabase.StringSetAndGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.StringSetAndGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.RedisValue StackExchange.Redis.IDatabase.StringSetBit(StackExchange.Redis.RedisKey key, long offset, bool bit, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool @@ -1026,7 +1026,7 @@ StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.Redi StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.StringSetAsync(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringSetAsync(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetBitAsync(StackExchange.Redis.RedisKey key, long offset, bool bit, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetRangeAsync(StackExchange.Redis.RedisKey key, long offset, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.InternalErrorEventArgs @@ -2052,3 +2052,17 @@ StackExchange.Redis.IServer.ExecuteAsync(int? database, string! command, System. [SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByMember(StackExchange.Redis.RedisValue member) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! [SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByVector(System.ReadOnlyMemory vector) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! StackExchange.Redis.RedisChannel.WithKeyRouting() -> StackExchange.Redis.RedisChannel +StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.Expiration expiry = default(StackExchange.Redis.Expiration), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabaseAsync.StringSetAsync(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.Expiration expiry = default(StackExchange.Redis.Expiration), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.Expiration +StackExchange.Redis.Expiration.Expiration() -> void +StackExchange.Redis.Expiration.Expiration(System.DateTime when) -> void +StackExchange.Redis.Expiration.Expiration(System.TimeSpan ttl) -> void +override StackExchange.Redis.Expiration.Equals(object? obj) -> bool +override StackExchange.Redis.Expiration.GetHashCode() -> int +override StackExchange.Redis.Expiration.ToString() -> string! +static StackExchange.Redis.Expiration.Default.get -> StackExchange.Redis.Expiration +static StackExchange.Redis.Expiration.KeepTtl.get -> StackExchange.Redis.Expiration +static StackExchange.Redis.Expiration.Persist.get -> StackExchange.Redis.Expiration +static StackExchange.Redis.Expiration.implicit operator StackExchange.Redis.Expiration(System.DateTime when) -> StackExchange.Redis.Expiration +static StackExchange.Redis.Expiration.implicit operator StackExchange.Redis.Expiration(System.TimeSpan ttl) -> StackExchange.Redis.Expiration diff --git a/src/StackExchange.Redis/RedisDatabase.ExpiryToken.cs b/src/StackExchange.Redis/RedisDatabase.ExpiryToken.cs deleted file mode 100644 index 42cfdcb18..000000000 --- a/src/StackExchange.Redis/RedisDatabase.ExpiryToken.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; - -namespace StackExchange.Redis; - -internal partial class RedisDatabase -{ - /// - /// Parses, validates and represents, for example: "EX 10", "KEEPTTL" or "". - /// - internal readonly struct ExpiryToken - { - private static readonly ExpiryToken s_Persist = new(RedisLiterals.PERSIST), s_KeepTtl = new(RedisLiterals.KEEPTTL), s_Null = new(RedisValue.Null); - - public RedisValue Operand { get; } - public long Value { get; } - public int Tokens => Value == long.MinValue ? (Operand.IsNull ? 0 : 1) : 2; - public bool HasValue => Value != long.MinValue; - public bool HasOperand => !Operand.IsNull; - - public static ExpiryToken Persist(TimeSpan? expiry, bool persist) - { - if (expiry.HasValue) - { - if (persist) throw new ArgumentException("Cannot specify both expiry and persist", nameof(persist)); - return new(expiry.GetValueOrDefault()); // EX 10 - } - - return persist ? s_Persist : s_Null; // PERSIST (or nothing) - } - - public static ExpiryToken KeepTtl(TimeSpan? expiry, bool keepTtl) - { - if (expiry.HasValue) - { - if (keepTtl) throw new ArgumentException("Cannot specify both expiry and keepTtl", nameof(keepTtl)); - return new(expiry.GetValueOrDefault()); // EX 10 - } - - return keepTtl ? s_KeepTtl : s_Null; // KEEPTTL (or nothing) - } - - private ExpiryToken(RedisValue operand, long value = long.MinValue) - { - Operand = operand; - Value = value; - } - - public ExpiryToken(TimeSpan expiry) - { - long milliseconds = expiry.Ticks / TimeSpan.TicksPerMillisecond; - var useSeconds = milliseconds % 1000 == 0; - - Operand = useSeconds ? RedisLiterals.EX : RedisLiterals.PX; - Value = useSeconds ? (milliseconds / 1000) : milliseconds; - } - - public ExpiryToken(DateTime expiry) - { - long milliseconds = GetUnixTimeMilliseconds(expiry); - var useSeconds = milliseconds % 1000 == 0; - - Operand = useSeconds ? RedisLiterals.EXAT : RedisLiterals.PXAT; - Value = useSeconds ? (milliseconds / 1000) : milliseconds; - } - - public override string ToString() => Tokens switch - { - 2 => $"{Operand} {Value}", - 1 => Operand.ToString(), - _ => "", - }; - - public override int GetHashCode() => throw new NotSupportedException(); - public override bool Equals(object? obj) => throw new NotSupportedException(); - } -} diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index bcda4146b..f0a4ed39f 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -396,7 +396,7 @@ public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, Tim public ExpireResult[] HashFieldExpire(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) { - long milliseconds = GetUnixTimeMilliseconds(expiry); + long milliseconds = Expiration.GetUnixTimeMilliseconds(expiry); return HashFieldExpireExecute(key, milliseconds, when, PickExpireAtCommandByPrecision, SyncCustomArrExecutor>, ResultProcessor.ExpireResultArray, flags, hashFields); } @@ -408,7 +408,7 @@ public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hash public Task HashFieldExpireAsync(RedisKey key, RedisValue[] hashFields, DateTime expiry, ExpireWhen when = ExpireWhen.Always, CommandFlags flags = CommandFlags.None) { - long milliseconds = GetUnixTimeMilliseconds(expiry); + long milliseconds = Expiration.GetUnixTimeMilliseconds(expiry); return HashFieldExpireExecute(key, milliseconds, when, PickExpireAtCommandByPrecision, AsyncCustomArrExecutor>, ResultProcessor.ExpireResultArray, flags, hashFields); } @@ -487,7 +487,7 @@ public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue[] return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } - private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, in RedisValue hashField, ExpiryToken expiry, CommandFlags flags) => + private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, in RedisValue hashField, Expiration expiry, CommandFlags flags) => expiry.Tokens switch { // expiry, for example EX 10 @@ -498,7 +498,7 @@ private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, in RedisValue h _ => Message.Create(Database, flags, RedisCommand.HGETEX, key, RedisLiterals.FIELDS, 1, hashField), }; - private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, RedisValue[] hashFields, ExpiryToken expiry, CommandFlags flags) + private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, RedisValue[] hashFields, Expiration expiry, CommandFlags flags) { if (hashFields is null) throw new ArgumentNullException(nameof(hashFields)); if (hashFields.Length == 1) @@ -537,7 +537,7 @@ private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, RedisValue[] ha public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) { - var msg = HashFieldGetAndSetExpiryMessage(key, hashField, ExpiryToken.Persist(expiry, persist), flags); + var msg = HashFieldGetAndSetExpiryMessage(key, hashField, Expiration.CreateOrPersist(expiry, persist), flags); return ExecuteSync(msg, ResultProcessor.RedisValueFromArray); } @@ -549,7 +549,7 @@ public RedisValue HashFieldGetAndSetExpiry(RedisKey key, RedisValue hashField, D public Lease? HashFieldGetLeaseAndSetExpiry(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) { - var msg = HashFieldGetAndSetExpiryMessage(key, hashField, ExpiryToken.Persist(expiry, persist), flags); + var msg = HashFieldGetAndSetExpiryMessage(key, hashField, Expiration.CreateOrPersist(expiry, persist), flags); return ExecuteSync(msg, ResultProcessor.LeaseFromArray); } @@ -563,7 +563,7 @@ public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFiel { if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); if (hashFields.Length == 0) return Array.Empty(); - var msg = HashFieldGetAndSetExpiryMessage(key, hashFields, ExpiryToken.Persist(expiry, persist), flags); + var msg = HashFieldGetAndSetExpiryMessage(key, hashFields, Expiration.CreateOrPersist(expiry, persist), flags); return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } @@ -577,7 +577,7 @@ public RedisValue[] HashFieldGetAndSetExpiry(RedisKey key, RedisValue[] hashFiel public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) { - var msg = HashFieldGetAndSetExpiryMessage(key, hashField, ExpiryToken.Persist(expiry, persist), flags); + var msg = HashFieldGetAndSetExpiryMessage(key, hashField, Expiration.CreateOrPersist(expiry, persist), flags); return ExecuteAsync(msg, ResultProcessor.RedisValueFromArray); } @@ -589,7 +589,7 @@ public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue h public Task?> HashFieldGetLeaseAndSetExpiryAsync(RedisKey key, RedisValue hashField, TimeSpan? expiry = null, bool persist = false, CommandFlags flags = CommandFlags.None) { - var msg = HashFieldGetAndSetExpiryMessage(key, hashField, ExpiryToken.Persist(expiry, persist), flags); + var msg = HashFieldGetAndSetExpiryMessage(key, hashField, Expiration.CreateOrPersist(expiry, persist), flags); return ExecuteAsync(msg, ResultProcessor.LeaseFromArray); } @@ -603,7 +603,7 @@ public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue { if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); if (hashFields.Length == 0) return CompletedTask.FromDefault(Array.Empty(), asyncState); - var msg = HashFieldGetAndSetExpiryMessage(key, hashFields, ExpiryToken.Persist(expiry, persist), flags); + var msg = HashFieldGetAndSetExpiryMessage(key, hashFields, Expiration.CreateOrPersist(expiry, persist), flags); return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } @@ -615,7 +615,7 @@ public Task HashFieldGetAndSetExpiryAsync(RedisKey key, RedisValue return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } - private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, in RedisValue field, in RedisValue value, ExpiryToken expiry, When when, CommandFlags flags) + private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, in RedisValue field, in RedisValue value, Expiration expiry, When when, CommandFlags flags) { if (when == When.Always) { @@ -645,7 +645,7 @@ private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, in RedisValue f } } - private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, HashEntry[] hashFields, ExpiryToken expiry, When when, CommandFlags flags) + private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, HashEntry[] hashFields, Expiration expiry, When when, CommandFlags flags) { if (hashFields.Length == 1) { @@ -693,7 +693,7 @@ private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, HashEntry[] has public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) { - var msg = HashFieldSetAndSetExpiryMessage(key, field, value, ExpiryToken.KeepTtl(expiry, keepTtl), when, flags); + var msg = HashFieldSetAndSetExpiryMessage(key, field, value, Expiration.CreateOrKeepTtl(expiry, keepTtl), when, flags); return ExecuteSync(msg, ResultProcessor.RedisValue); } @@ -706,7 +706,7 @@ public RedisValue HashFieldSetAndSetExpiry(RedisKey key, RedisValue field, Redis public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) { if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); - var msg = HashFieldSetAndSetExpiryMessage(key, hashFields, ExpiryToken.KeepTtl(expiry, keepTtl), when, flags); + var msg = HashFieldSetAndSetExpiryMessage(key, hashFields, Expiration.CreateOrKeepTtl(expiry, keepTtl), when, flags); return ExecuteSync(msg, ResultProcessor.RedisValue); } public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) @@ -718,7 +718,7 @@ public RedisValue HashFieldSetAndSetExpiry(RedisKey key, HashEntry[] hashFields, public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue field, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) { - var msg = HashFieldSetAndSetExpiryMessage(key, field, value, ExpiryToken.KeepTtl(expiry, keepTtl), when, flags); + var msg = HashFieldSetAndSetExpiryMessage(key, field, value, Expiration.CreateOrKeepTtl(expiry, keepTtl), when, flags); return ExecuteAsync(msg, ResultProcessor.RedisValue); } @@ -731,7 +731,7 @@ public Task HashFieldSetAndSetExpiryAsync(RedisKey key, RedisValue f public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) { if (hashFields == null) throw new ArgumentNullException(nameof(hashFields)); - var msg = HashFieldSetAndSetExpiryMessage(key, hashFields, ExpiryToken.KeepTtl(expiry, keepTtl), when, flags); + var msg = HashFieldSetAndSetExpiryMessage(key, hashFields, Expiration.CreateOrKeepTtl(expiry, keepTtl), when, flags); return ExecuteAsync(msg, ResultProcessor.RedisValue); } public Task HashFieldSetAndSetExpiryAsync(RedisKey key, HashEntry[] hashFields, DateTime expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) @@ -1333,8 +1333,8 @@ protected override void WriteImpl(PhysicalConnection physical) physical.Write(Key); physical.WriteBulkString(toDatabase); physical.WriteBulkString(timeoutMilliseconds); - if (isCopy) physical.WriteBulkString(RedisLiterals.COPY); - if (isReplace) physical.WriteBulkString(RedisLiterals.REPLACE); + if (isCopy) physical.WriteBulkString("COPY"u8); + if (isReplace) physical.WriteBulkString("REPLACE"u8); } public override int ArgCount @@ -3512,25 +3512,25 @@ public RedisValue StringGet(RedisKey key, CommandFlags flags = CommandFlags.None public RedisValue StringGetSetExpiry(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) { - var msg = GetStringGetExMessage(key, expiry, flags); + var msg = GetStringGetExMessage(key, Expiration.CreateOrPersist(expiry, !expiry.HasValue), flags); return ExecuteSync(msg, ResultProcessor.RedisValue); } public RedisValue StringGetSetExpiry(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) { - var msg = GetStringGetExMessage(key, expiry, flags); + var msg = GetStringGetExMessage(key, new(expiry), flags); return ExecuteSync(msg, ResultProcessor.RedisValue); } public Task StringGetSetExpiryAsync(RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) { - var msg = GetStringGetExMessage(key, expiry, flags); + var msg = GetStringGetExMessage(key, Expiration.CreateOrPersist(expiry, !expiry.HasValue), flags); return ExecuteAsync(msg, ResultProcessor.RedisValue); } public Task StringGetSetExpiryAsync(RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) { - var msg = GetStringGetExMessage(key, expiry, flags); + var msg = GetStringGetExMessage(key, new(expiry), flags); return ExecuteAsync(msg, ResultProcessor.RedisValue); } @@ -3674,13 +3674,19 @@ public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When whe public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetStringSetMessage(key, value, expiry, keepTtl, when, flags); + var msg = GetStringSetMessage(key, value, Expiration.CreateOrKeepTtl(expiry, keepTtl), when, flags); return ExecuteSync(msg, ResultProcessor.Boolean); } public bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetStringSetMessage(values, when, flags); + var msg = GetStringSetMessage(values, when, Expiration.Default, flags); + return ExecuteSync(msg, ResultProcessor.Boolean); + } + + public bool StringSet(KeyValuePair[] values, When when, Expiration expiry, CommandFlags flags) + { + var msg = GetStringSetMessage(values, when, expiry, flags); return ExecuteSync(msg, ResultProcessor.Boolean); } @@ -3692,13 +3698,19 @@ public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expir public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetStringSetMessage(key, value, expiry, keepTtl, when, flags); + var msg = GetStringSetMessage(key, value, Expiration.CreateOrKeepTtl(expiry, keepTtl), when, flags); return ExecuteAsync(msg, ResultProcessor.Boolean); } public Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) { - var msg = GetStringSetMessage(values, when, flags); + var msg = GetStringSetMessage(values, when, Expiration.Default, flags); + return ExecuteAsync(msg, ResultProcessor.Boolean); + } + + public Task StringSetAsync(KeyValuePair[] values, When when, Expiration expiry, CommandFlags flags) + { + var msg = GetStringSetMessage(values, when, expiry, flags); return ExecuteAsync(msg, ResultProcessor.Boolean); } @@ -3778,12 +3790,6 @@ public Task StringSetRangeAsync(RedisKey key, long offset, RedisValu return ExecuteAsync(msg, ResultProcessor.RedisValue); } - private static long GetUnixTimeMilliseconds(DateTime when) => when.Kind switch - { - DateTimeKind.Local or DateTimeKind.Utc => (when.ToUniversalTime() - RedisBase.UnixEpoch).Ticks / TimeSpan.TicksPerMillisecond, - _ => throw new ArgumentException("Expiry time must be either Utc or Local", nameof(when)), - }; - private Message GetCopyMessage(in RedisKey sourceKey, RedisKey destinationKey, int destinationDatabase, bool replace, CommandFlags flags) => destinationDatabase switch { @@ -3822,7 +3828,7 @@ private Message GetExpiryMessage(in RedisKey key, CommandFlags flags, DateTime? }; } - long milliseconds = GetUnixTimeMilliseconds(expiry.Value); + long milliseconds = Expiration.GetUnixTimeMilliseconds(expiry.Value); return GetExpiryMessage(key, RedisCommand.PEXPIREAT, RedisCommand.EXPIREAT, milliseconds, when, flags, out server); } @@ -4991,15 +4997,15 @@ private Message GetStringBitOperationMessage(Bitwise operation, RedisKey destina return Message.CreateInSlot(Database, slot, flags, RedisCommand.BITOP, new[] { op, destination.AsRedisValue(), first.AsRedisValue(), second.AsRedisValue() }); } - private Message GetStringGetExMessage(in RedisKey key, TimeSpan? expiry, CommandFlags flags = CommandFlags.None) => expiry switch + private Message GetStringGetExMessage(in RedisKey key, Expiration expiry, CommandFlags flags = CommandFlags.None) { - null => Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PERSIST), - _ => Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PX, (long)expiry.Value.TotalMilliseconds), - }; - - private Message GetStringGetExMessage(in RedisKey key, DateTime expiry, CommandFlags flags = CommandFlags.None) => expiry == DateTime.MaxValue - ? Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PERSIST) - : Message.Create(Database, flags, RedisCommand.GETEX, key, RedisLiterals.PXAT, GetUnixTimeMilliseconds(expiry)); + return expiry.Tokens switch + { + 0 => Message.Create(Database, flags, RedisCommand.GETEX, key), + 1 => Message.Create(Database, flags, RedisCommand.GETEX, key, expiry.Operand), + _ => Message.Create(Database, flags, RedisCommand.GETEX, key, expiry.Operand, expiry.Value), + }; + } private Message GetStringGetWithExpiryMessage(RedisKey key, CommandFlags flags, out ResultProcessor processor, out ServerEndPoint? server) { @@ -5018,73 +5024,79 @@ private Message GetStringGetWithExpiryMessage(RedisKey key, CommandFlags flags, return new StringGetWithExpiryMessage(Database, flags, RedisCommand.TTL, key); } - private Message? GetStringSetMessage(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) + private Message? GetStringSetMessage(KeyValuePair[] values, When when, Expiration expiry, CommandFlags flags) { if (values == null) throw new ArgumentNullException(nameof(values)); switch (values.Length) { case 0: return null; - case 1: return GetStringSetMessage(values[0].Key, values[0].Value, null, false, when, flags); + case 1: return GetStringSetMessage(values[0].Key, values[0].Value, expiry, when, flags); default: - WhenAlwaysOrNotExists(when); - int slot = ServerSelectionStrategy.NoSlot, offset = 0; - var args = new RedisValue[values.Length * 2]; - var serverSelectionStrategy = multiplexer.ServerSelectionStrategy; - for (int i = 0; i < values.Length; i++) + // assume MSETEX in the general case, but look for scenarios where we can use the simpler + // MSET/MSETNX commands (which have wider applicability in terms of server versions) + // (note that when/expiry is ignored when not MSETEX; no need to explicitly wipe) + WhenAlwaysOrExistsOrNotExists(when); + var cmd = when switch { - args[offset++] = values[i].Key.AsRedisValue(); - args[offset++] = values[i].Value; - slot = serverSelectionStrategy.CombineSlot(slot, values[i].Key); - } - return Message.CreateInSlot(Database, slot, flags, when == When.NotExists ? RedisCommand.MSETNX : RedisCommand.MSET, args); + When.Always when expiry.IsNone => RedisCommand.MSET, + When.NotExists when expiry.IsNoneOrKeepTtl => RedisCommand.MSETNX, // "keepttl" with "not exists" is the same as "no expiry" + _ => RedisCommand.MSETEX, + }; + return Message.Create(Database, flags, cmd, values, expiry, when); } } private Message GetStringSetMessage( RedisKey key, RedisValue value, - TimeSpan? expiry = null, - bool keepTtl = false, + Expiration expiry, When when = When.Always, CommandFlags flags = CommandFlags.None) { WhenAlwaysOrExistsOrNotExists(when); + static Message ThrowWhen() => throw new ArgumentOutOfRangeException(nameof(when)); + if (value.IsNull) return Message.Create(Database, flags, RedisCommand.DEL, key); - if (expiry == null || expiry.Value == TimeSpan.MaxValue) + if (expiry.IsPersist) throw new NotSupportedException("SET+PERSIST is not supported"); // we don't expect to get here ever + + if (expiry.IsNone) { - // no expiry return when switch { - When.Always when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value), - When.Always when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.KEEPTTL), - When.NotExists when !keepTtl => Message.Create(Database, flags, RedisCommand.SETNX, key, value), - When.NotExists when keepTtl => Message.Create(Database, flags, RedisCommand.SETNX, key, value, RedisLiterals.KEEPTTL), - When.Exists when !keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX), - When.Exists when keepTtl => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.KEEPTTL), - _ => throw new ArgumentOutOfRangeException(nameof(when)), + When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value), + When.NotExists => Message.Create(Database, flags, RedisCommand.SETNX, key, value), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX), + _ => ThrowWhen(), }; } - long milliseconds = expiry.Value.Ticks / TimeSpan.TicksPerMillisecond; - if ((milliseconds % 1000) == 0) + if (expiry.IsKeepTtl) { - // a nice round number of seconds - long seconds = milliseconds / 1000; return when switch { - When.Always => Message.Create(Database, flags, RedisCommand.SETEX, key, seconds, value), - When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX), - When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.NX), - _ => throw new ArgumentOutOfRangeException(nameof(when)), + When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.KEEPTTL), + When.NotExists => Message.Create(Database, flags, RedisCommand.SETNX, key, value), // (there would be no existing TTL to keep) + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.KEEPTTL), + _ => ThrowWhen(), }; } + if (when is When.Always & expiry.IsRelative) + { + // special case to SETEX/PSETEX + return expiry.IsSeconds + ? Message.Create(Database, flags, RedisCommand.SETEX, key, expiry.Value, value) + : Message.Create(Database, flags, RedisCommand.PSETEX, key, expiry.Value, value); + } + + // use SET with EX/PX/EXAT/PXAT and possibly XX/NX + var expiryOperand = expiry.GetOperand(out var expiryValue); return when switch { - When.Always => Message.Create(Database, flags, RedisCommand.PSETEX, key, milliseconds, value), - When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX), - When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.NX), + When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, expiryOperand, expiryValue), + When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, expiryOperand, expiryValue, RedisLiterals.XX), + When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, expiryOperand, expiryValue, RedisLiterals.NX), _ => throw new ArgumentOutOfRangeException(nameof(when)), }; } @@ -5332,7 +5344,7 @@ public ScriptLoadMessage(CommandFlags flags, string script) protected override void WriteImpl(PhysicalConnection physical) { physical.WriteHeader(Command, 2); - physical.WriteBulkString(RedisLiterals.LOAD); + physical.WriteBulkString("LOAD"u8); physical.WriteBulkString((RedisValue)Script); } public override int ArgCount => 2; diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 87bcbf20c..9bc9af6d2 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -46,7 +46,8 @@ namespace StackExchange.Redis v7_4_0_rc1 = new Version(7, 3, 240), // 7.4 RC1 is version 7.3.240 v7_4_0_rc2 = new Version(7, 3, 241), // 7.4 RC2 is version 7.3.241 v8_0_0_M04 = new Version(7, 9, 227), // 8.0 M04 is version 7.9.227 - v8_2_0_rc1 = new Version(8, 1, 240); // 8.2 RC1 is version 8.1.240 + v8_2_0_rc1 = new Version(8, 1, 240), // 8.2 RC1 is version 8.1.240 + v8_4_0_rc1 = new Version(8, 3, 224); // 8.4 RC1 is version 8.3.224 #pragma warning restore SA1310 // Field names should not contain underscore #pragma warning restore SA1311 // Static readonly fields should begin with upper-case letter diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 46a64cc88..9a8c15613 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -60,7 +60,6 @@ public static readonly RedisValue ANDOR = "ANDOR", ANY = "ANY", ASC = "ASC", - AUTH = "AUTH", BEFORE = "BEFORE", BIT = "BIT", BY = "BY", @@ -69,7 +68,6 @@ public static readonly RedisValue BYTE = "BYTE", CH = "CH", CHANNELS = "CHANNELS", - COPY = "COPY", COUNT = "COUNT", DB = "DB", @default = "default", @@ -105,7 +103,6 @@ public static readonly RedisValue lib_ver = "lib-ver", LIMIT = "LIMIT", LIST = "LIST", - LOAD = "LOAD", LT = "LT", MATCH = "MATCH", MALLOC_STATS = "MALLOC-STATS", diff --git a/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs b/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs index 3f0d39f28..fea4d4885 100644 --- a/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs +++ b/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs @@ -1,16 +1,15 @@ using System; using Xunit; -using static StackExchange.Redis.RedisDatabase; -using static StackExchange.Redis.RedisDatabase.ExpiryToken; +using static StackExchange.Redis.Expiration; namespace StackExchange.Redis.Tests; -public class ExpiryTokenTests // pure tests, no DB +public class ExpirationTests // pure tests, no DB { [Fact] public void Persist_Seconds() { TimeSpan? time = TimeSpan.FromMilliseconds(5000); - var ex = Persist(time, false); + var ex = CreateOrPersist(time, false); Assert.Equal(2, ex.Tokens); Assert.Equal("EX 5", ex.ToString()); } @@ -19,7 +18,7 @@ public void Persist_Seconds() public void Persist_Milliseconds() { TimeSpan? time = TimeSpan.FromMilliseconds(5001); - var ex = Persist(time, false); + var ex = CreateOrPersist(time, false); Assert.Equal(2, ex.Tokens); Assert.Equal("PX 5001", ex.ToString()); } @@ -28,7 +27,7 @@ public void Persist_Milliseconds() public void Persist_None_False() { TimeSpan? time = null; - var ex = Persist(time, false); + var ex = CreateOrPersist(time, false); Assert.Equal(0, ex.Tokens); Assert.Equal("", ex.ToString()); } @@ -37,7 +36,7 @@ public void Persist_None_False() public void Persist_None_True() { TimeSpan? time = null; - var ex = Persist(time, true); + var ex = CreateOrPersist(time, true); Assert.Equal(1, ex.Tokens); Assert.Equal("PERSIST", ex.ToString()); } @@ -46,7 +45,7 @@ public void Persist_None_True() public void Persist_Both() { TimeSpan? time = TimeSpan.FromMilliseconds(5000); - var ex = Assert.Throws(() => Persist(time, true)); + var ex = Assert.Throws(() => CreateOrPersist(time, true)); Assert.Equal("persist", ex.ParamName); Assert.StartsWith("Cannot specify both expiry and persist", ex.Message); } @@ -55,7 +54,7 @@ public void Persist_Both() public void KeepTtl_Seconds() { TimeSpan? time = TimeSpan.FromMilliseconds(5000); - var ex = KeepTtl(time, false); + var ex = CreateOrKeepTtl(time, false); Assert.Equal(2, ex.Tokens); Assert.Equal("EX 5", ex.ToString()); } @@ -64,7 +63,7 @@ public void KeepTtl_Seconds() public void KeepTtl_Milliseconds() { TimeSpan? time = TimeSpan.FromMilliseconds(5001); - var ex = KeepTtl(time, false); + var ex = CreateOrKeepTtl(time, false); Assert.Equal(2, ex.Tokens); Assert.Equal("PX 5001", ex.ToString()); } @@ -73,7 +72,7 @@ public void KeepTtl_Milliseconds() public void KeepTtl_None_False() { TimeSpan? time = null; - var ex = KeepTtl(time, false); + var ex = CreateOrKeepTtl(time, false); Assert.Equal(0, ex.Tokens); Assert.Equal("", ex.ToString()); } @@ -82,7 +81,7 @@ public void KeepTtl_None_False() public void KeepTtl_None_True() { TimeSpan? time = null; - var ex = KeepTtl(time, true); + var ex = CreateOrKeepTtl(time, true); Assert.Equal(1, ex.Tokens); Assert.Equal("KEEPTTL", ex.ToString()); } @@ -91,7 +90,7 @@ public void KeepTtl_None_True() public void KeepTtl_Both() { TimeSpan? time = TimeSpan.FromMilliseconds(5000); - var ex = Assert.Throws(() => KeepTtl(time, true)); + var ex = Assert.Throws(() => CreateOrKeepTtl(time, true)); Assert.Equal("keepTtl", ex.ParamName); Assert.StartsWith("Cannot specify both expiry and keepTtl", ex.Message); } @@ -100,7 +99,7 @@ public void KeepTtl_Both() public void DateTime_Seconds() { var when = new DateTime(2025, 7, 23, 10, 4, 14, DateTimeKind.Utc); - var ex = new ExpiryToken(when); + var ex = new Expiration(when); Assert.Equal(2, ex.Tokens); Assert.Equal("EXAT 1753265054", ex.ToString()); } @@ -110,7 +109,7 @@ public void DateTime_Milliseconds() { var when = new DateTime(2025, 7, 23, 10, 4, 14, DateTimeKind.Utc); when = when.AddMilliseconds(14); - var ex = new ExpiryToken(when); + var ex = new Expiration(when); Assert.Equal(2, ex.Tokens); Assert.Equal("PXAT 1753265054014", ex.ToString()); } diff --git a/tests/StackExchange.Redis.Tests/MSetTests.cs b/tests/StackExchange.Redis.Tests/MSetTests.cs new file mode 100644 index 000000000..8657c9d5a --- /dev/null +++ b/tests/StackExchange.Redis.Tests/MSetTests.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace StackExchange.Redis.Tests; + +public class MSetTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) +{ + [Theory] + [InlineData(0, When.Always)] + [InlineData(1, When.Always)] + [InlineData(2, When.Always)] + [InlineData(10, When.Always)] + [InlineData(0, When.NotExists)] + [InlineData(1, When.NotExists)] + [InlineData(2, When.NotExists)] + [InlineData(10, When.NotExists)] + [InlineData(0, When.NotExists, true)] + [InlineData(1, When.NotExists, true)] + [InlineData(2, When.NotExists, true)] + [InlineData(10, When.NotExists, true)] + [InlineData(0, When.Exists)] + [InlineData(1, When.Exists)] + [InlineData(2, When.Exists)] + [InlineData(10, When.Exists)] + [InlineData(0, When.Exists, true)] + [InlineData(1, When.Exists, true)] + [InlineData(2, When.Exists, true)] + [InlineData(10, When.Exists, true)] + public async Task AddWithoutExpiration(int count, When when, bool precreate = false) + { + await using var conn = Create(require: (when == When.Exists && count > 1) ? RedisFeatures.v8_4_0_rc1 : null); + var pairs = new KeyValuePair[count]; + var key = Me(); + for (int i = 0; i < count; i++) + { + // note the unusual braces; this is to force (on cluster) a hash-slot based on key + pairs[i] = new KeyValuePair($"{{{key}}}_{i}", $"value {i}"); + } + + var keys = Array.ConvertAll(pairs, pair => pair.Key); + var db = conn.GetDatabase(); + // set initial state + await db.KeyDeleteAsync(keys, flags: CommandFlags.FireAndForget); + if (precreate) + { + foreach (var pair in pairs) + { + await db.StringSetAsync(pair.Key, "dummy value", flags: CommandFlags.FireAndForget); + } + } + + bool expected = count != 0 & when switch + { + When.Always => true, + When.Exists => precreate, + When.NotExists => !precreate, + _ => throw new ArgumentOutOfRangeException(nameof(when)), + }; + + // issue the test command + var actualPending = db.StringSetAsync(pairs, when); + var values = await db.StringGetAsync(keys); // pipelined + var actual = await actualPending; + + // check the state *after* the command + Assert.Equal(expected, actual); + Assert.Equal(count, values.Length); + for (int i = 0; i < count; i++) + { + if (expected) + { + Assert.Equal(pairs[i].Value, values[i]); + } + else + { + Assert.NotEqual(pairs[i].Value, values[i]); + } + } + } + + [Theory] + [InlineData(0, When.Always)] + [InlineData(1, When.Always)] + [InlineData(2, When.Always)] + [InlineData(10, When.Always)] + [InlineData(0, When.NotExists)] + [InlineData(1, When.NotExists)] + [InlineData(2, When.NotExists)] + [InlineData(10, When.NotExists)] + [InlineData(0, When.NotExists, true)] + [InlineData(1, When.NotExists, true)] + [InlineData(2, When.NotExists, true)] + [InlineData(10, When.NotExists, true)] + [InlineData(0, When.Exists)] + [InlineData(1, When.Exists)] + [InlineData(2, When.Exists)] + [InlineData(10, When.Exists)] + [InlineData(0, When.Exists, true)] + [InlineData(1, When.Exists, true)] + [InlineData(2, When.Exists, true)] + [InlineData(10, When.Exists, true)] + public async Task AddWithRelativeExpiration(int count, When when, bool precreate = false) + { + await using var conn = Create(require: count > 1 ? RedisFeatures.v8_4_0_rc1 : null); + var pairs = new KeyValuePair[count]; + var key = Me(); + for (int i = 0; i < count; i++) + { + // note the unusual braces; this is to force (on cluster) a hash-slot based on key + pairs[i] = new KeyValuePair($"{{{key}}}_{i}", $"value {i}"); + } + var expiry = TimeSpan.FromMinutes(10); + + var keys = Array.ConvertAll(pairs, pair => pair.Key); + var db = conn.GetDatabase(); + // set initial state + await db.KeyDeleteAsync(keys, flags: CommandFlags.FireAndForget); + if (precreate) + { + foreach (var pair in pairs) + { + await db.StringSetAsync(pair.Key, "dummy value", flags: CommandFlags.FireAndForget); + } + } + + bool expected = count != 0 & when switch + { + When.Always => true, + When.Exists => precreate, + When.NotExists => !precreate, + _ => throw new ArgumentOutOfRangeException(nameof(when)), + }; + + // issue the test command + var actualPending = db.StringSetAsync(pairs, when, expiry); + Task[] ttls = new Task[count]; + for (int i = 0; i < count; i++) + { + ttls[i] = db.KeyTimeToLiveAsync(keys[i]); + } + await Task.WhenAll(ttls); + var values = await db.StringGetAsync(keys); // pipelined + var actual = await actualPending; + + // check the state *after* the command + Assert.Equal(expected, actual); + Assert.Equal(count, values.Length); + for (int i = 0; i < count; i++) + { + var ttl = await ttls[i]; + if (expected) + { + Assert.Equal(pairs[i].Value, values[i]); + Assert.NotNull(ttl); + Assert.True(ttl > TimeSpan.Zero && ttl <= expiry); + } + else + { + Assert.NotEqual(pairs[i].Value, values[i]); + Assert.Null(ttl); + } + } + } +} From 416daff9e14ffcfcd59f92204eff9795b4ac52fd Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 5 Nov 2025 16:02:40 +0000 Subject: [PATCH 388/435] propose support for XREADGROUP CLAIM (#2972) * propose support for XREADGROUP CLAIM * release notes * comma nit * add integration tests for `claimMinIdleTime` * support claimMinIdleTime on single-stream read API * u8-ify the stream messages * Update src/StackExchange.Redis/APITypes/StreamEntry.cs Co-authored-by: Philo --------- Co-authored-by: Philo --- docs/ReleaseNotes.md | 1 + .../APITypes/StreamEntry.cs | 25 ++++ .../Interfaces/IDatabase.cs | 37 ++++- .../Interfaces/IDatabaseAsync.VectorSets.cs | 3 + .../Interfaces/IDatabaseAsync.cs | 7 +- .../KeyspaceIsolation/KeyPrefixed.cs | 6 + .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 6 + .../PublicAPI/PublicAPI.Shipped.txt | 15 +- src/StackExchange.Redis/RedisDatabase.cs | 134 +++++++++++++----- src/StackExchange.Redis/RedisFeatures.cs | 2 +- src/StackExchange.Redis/ResultProcessor.cs | 20 ++- src/StackExchange.Redis/StreamConstants.cs | 6 - .../StackExchange.Redis.Tests/StreamTests.cs | 99 ++++++++++++- 13 files changed, 304 insertions(+), 57 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index d11ddb7fd..e6c010116 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,7 @@ Current package versions: ## Unreleased +- Support `XREADGROUP CLAIM` ([#2972 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2972)) - Support `MSETEX` (Redis 8.4.0) for multi-key operations with expiration ([#2977 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2977)) ## 2.9.32 diff --git a/src/StackExchange.Redis/APITypes/StreamEntry.cs b/src/StackExchange.Redis/APITypes/StreamEntry.cs index 3f37b9430..25f4f690c 100644 --- a/src/StackExchange.Redis/APITypes/StreamEntry.cs +++ b/src/StackExchange.Redis/APITypes/StreamEntry.cs @@ -14,6 +14,19 @@ public StreamEntry(RedisValue id, NameValueEntry[] values) { Id = id; Values = values; + IdleTime = null; + DeliveryCount = 0; + } + + /// + /// Creates a stream entry. + /// + public StreamEntry(RedisValue id, NameValueEntry[] values, TimeSpan? idleTime, int deliveryCount) + { + Id = id; + Values = values; + IdleTime = idleTime; + DeliveryCount = deliveryCount; } /// @@ -51,6 +64,18 @@ public RedisValue this[RedisValue fieldName] } } + /// + /// Delivery count - the number of times this entry has been delivered: 0 for new messages that haven't been delivered before, + /// 1+ for claimed messages (previously unacknowledged entries). + /// + public int DeliveryCount { get; } + + /// + /// Idle time in milliseconds - the number of milliseconds elapsed since this entry was last delivered to a consumer. + /// + /// This member is populated when using XREADGROUP with CLAIM. + public TimeSpan? IdleTime { get; } + /// /// Indicates that the Redis Stream Entry is null. /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index fd4fb3e30..e15b4bbdb 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -2971,7 +2971,22 @@ IEnumerable SortedSetScan( /// The flags to use for this operation. /// Returns a value of for each message returned. /// - StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None); + StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, bool noAck, CommandFlags flags); + + /// + /// Read messages from a stream into an associated consumer group. + /// + /// The key of the stream. + /// The name of the consumer group. + /// The consumer name. + /// The position from which to read the stream. Defaults to when . + /// The maximum number of messages to return. + /// When true, the message will not be added to the pending message list. + /// Auto-claim messages that have been idle for at least this long. + /// The flags to use for this operation. + /// Returns a value of for each message returned. + /// + StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None); /// /// Read from multiple streams into the given consumer group. @@ -3004,7 +3019,25 @@ IEnumerable SortedSetScan( /// Equivalent of calling XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2. /// /// - RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None); + RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, bool noAck, CommandFlags flags); + + /// + /// Read from multiple streams into the given consumer group. + /// The consumer group with the given will need to have been created for each stream prior to calling this method. + /// + /// Array of streams and the positions from which to begin reading for each stream. + /// The name of the consumer group. + /// The name of the consumer. + /// The maximum number of messages to return from each stream. + /// When true, the message will not be added to the pending message list. + /// Auto-claim messages that have been idle for at least this long. + /// The flags to use for this operation. + /// A value of for each stream. + /// + /// Equivalent of calling XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2. + /// + /// + RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None); /// /// Trim the stream to a specified maximum length. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs index 863095140..3ac67d40f 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs @@ -92,4 +92,7 @@ Task VectorSetSetAttributesJsonAsync( RedisKey key, VectorSetSimilaritySearchRequest query, CommandFlags flags = CommandFlags.None); + + /// + Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None); } diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 6515740af..6e411cbd3 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -725,13 +725,16 @@ IAsyncEnumerable SortedSetScanAsync( Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, CommandFlags flags); /// - Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None); + Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, bool noAck, CommandFlags flags); + + /// + Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None); /// Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags); /// - Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None); + Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, bool noAck, CommandFlags flags); /// Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength, CommandFlags flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 1651d1069..378c90704 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -690,12 +690,18 @@ public Task StreamReadGroupAsync(RedisKey key, RedisValue groupNa public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => Inner.StreamReadGroupAsync(ToInner(key), groupName, consumerName, position, count, noAck, flags); + public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamReadGroupAsync(ToInner(key), groupName, consumerName, position, count, noAck, claimMinIdleTime, flags); + public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags) => Inner.StreamReadGroupAsync(streamPositions, groupName, consumerName, countPerStream, flags); public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => Inner.StreamReadGroupAsync(streamPositions, groupName, consumerName, countPerStream, noAck, flags); + public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamReadGroupAsync(streamPositions, groupName, consumerName, countPerStream, noAck, claimMinIdleTime, flags); + public Task StreamTrimAsync(RedisKey key, int maxLength, bool useApproximateMaxLength, CommandFlags flags) => Inner.StreamTrimAsync(ToInner(key), maxLength, useApproximateMaxLength, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index 3965625f9..dfb906f32 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -672,12 +672,18 @@ public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisVa public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => Inner.StreamReadGroup(ToInner(key), groupName, consumerName, position, count, noAck, flags); + public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamReadGroup(ToInner(key), groupName, consumerName, position, count, noAck, claimMinIdleTime, flags); + public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags) => Inner.StreamReadGroup(streamPositions, groupName, consumerName, countPerStream, flags); public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) => Inner.StreamReadGroup(streamPositions, groupName, consumerName, countPerStream, noAck, flags); + public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None) => + Inner.StreamReadGroup(streamPositions, groupName, consumerName, countPerStream, noAck, claimMinIdleTime, flags); + public long StreamTrim(RedisKey key, int maxLength, bool useApproximateMaxLength, CommandFlags flags) => Inner.StreamTrim(ToInner(key), maxLength, useApproximateMaxLength, flags); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index ba60cdc8c..dbb710243 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -744,9 +744,9 @@ StackExchange.Redis.IDatabase.StreamPendingMessages(StackExchange.Redis.RedisKey StackExchange.Redis.IDatabase.StreamRange(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, int? count = null, StackExchange.Redis.Order messageOrder = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[]! StackExchange.Redis.IDatabase.StreamRead(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue position, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[]! StackExchange.Redis.IDatabase.StreamRead(StackExchange.Redis.StreamPosition[]! streamPositions, int? countPerStream = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisStream[]! -StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position = null, int? count = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[]! +StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position, int? count, bool noAck, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.StreamEntry[]! +StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position = null, int? count = null, bool noAck = false, System.TimeSpan? claimMinIdleTime = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.StreamEntry[]! StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position, int? count, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.StreamEntry[]! -StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisStream[]! StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.RedisStream[]! StackExchange.Redis.IDatabase.StreamTrim(StackExchange.Redis.RedisKey key, int maxLength, bool useApproximateMaxLength, StackExchange.Redis.CommandFlags flags) -> long StackExchange.Redis.IDatabase.StreamTrim(StackExchange.Redis.RedisKey key, long maxLength, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode mode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long @@ -989,9 +989,9 @@ StackExchange.Redis.IDatabaseAsync.StreamPendingMessagesAsync(StackExchange.Redi StackExchange.Redis.IDatabaseAsync.StreamRangeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue? minId = null, StackExchange.Redis.RedisValue? maxId = null, int? count = null, StackExchange.Redis.Order messageOrder = StackExchange.Redis.Order.Ascending, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamReadAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue position, int? count = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamReadAsync(StackExchange.Redis.StreamPosition[]! streamPositions, int? countPerStream = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position = null, int? count = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position, int? count, bool noAck, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position = null, int? count = null, bool noAck = false, System.TimeSpan? claimMinIdleTime = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, StackExchange.Redis.RedisValue? position, int? count, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream = null, bool noAck = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamTrimAsync(StackExchange.Redis.RedisKey key, int maxLength, bool useApproximateMaxLength, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StreamTrimAsync(StackExchange.Redis.RedisKey key, long maxLength, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode mode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! @@ -2052,6 +2052,13 @@ StackExchange.Redis.IServer.ExecuteAsync(int? database, string! command, System. [SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByMember(StackExchange.Redis.RedisValue member) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! [SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByVector(System.ReadOnlyMemory vector) -> StackExchange.Redis.VectorSetSimilaritySearchRequest! StackExchange.Redis.RedisChannel.WithKeyRouting() -> StackExchange.Redis.RedisChannel +StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream = null, bool noAck = false, System.TimeSpan? claimMinIdleTime = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisStream[]! +StackExchange.Redis.IDatabase.StreamReadGroup(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream, bool noAck, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.RedisStream[]! +StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream = null, bool noAck = false, System.TimeSpan? claimMinIdleTime = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StreamReadGroupAsync(StackExchange.Redis.StreamPosition[]! streamPositions, StackExchange.Redis.RedisValue groupName, StackExchange.Redis.RedisValue consumerName, int? countPerStream, bool noAck, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! +StackExchange.Redis.StreamEntry.DeliveryCount.get -> int +StackExchange.Redis.StreamEntry.IdleTime.get -> System.TimeSpan? +StackExchange.Redis.StreamEntry.StreamEntry(StackExchange.Redis.RedisValue id, StackExchange.Redis.NameValueEntry[]! values, System.TimeSpan? idleTime, int deliveryCount) -> void StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.Expiration expiry = default(StackExchange.Redis.Expiration), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabaseAsync.StringSetAsync(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.Expiration expiry = default(StackExchange.Redis.Expiration), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.Expiration diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index f0a4ed39f..6f39bf5be 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -3262,29 +3262,38 @@ public Task StreamReadAsync(StreamPosition[] streamPositions, int return ExecuteAsync(msg, ResultProcessor.MultiStream, defaultValue: Array.Empty()); } - public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, CommandFlags flags) - { - return StreamReadGroup( + public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position, int? count, CommandFlags flags) => + StreamReadGroup( key, groupName, consumerName, position, count, false, + null, flags); - } public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None) - { - var actualPosition = position ?? StreamPosition.NewMessages; + => StreamReadGroup( + key, + groupName, + consumerName, + position, + count, + noAck, + null, + flags); + public StreamEntry[] StreamReadGroup(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None) + { var msg = GetStreamReadGroupMessage( key, groupName, consumerName, - StreamPosition.Resolve(actualPosition, RedisCommand.XREADGROUP), + StreamPosition.Resolve(position ?? StreamPosition.NewMessages, RedisCommand.XREADGROUP), count, noAck, + claimMinIdleTime, flags); return ExecuteSync(msg, ResultProcessor.SingleStreamWithNameSkip, defaultValue: Array.Empty()); @@ -3299,37 +3308,57 @@ public Task StreamReadGroupAsync(RedisKey key, RedisValue groupNa position, count, false, + null, flags); } public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, CommandFlags flags = CommandFlags.None) - { - var actualPosition = position ?? StreamPosition.NewMessages; + => StreamReadGroupAsync( + key, + groupName, + consumerName, + position, + count, + noAck, + null, + flags); + public Task StreamReadGroupAsync(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue? position = null, int? count = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None) + { var msg = GetStreamReadGroupMessage( key, groupName, consumerName, - StreamPosition.Resolve(actualPosition, RedisCommand.XREADGROUP), + StreamPosition.Resolve(position ?? StreamPosition.NewMessages, RedisCommand.XREADGROUP), count, noAck, + claimMinIdleTime, flags); return ExecuteAsync(msg, ResultProcessor.SingleStreamWithNameSkip, defaultValue: Array.Empty()); } public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags) - { - return StreamReadGroup( + => StreamReadGroup( streamPositions, groupName, consumerName, countPerStream, false, + null, + flags); + + public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, bool noAck, CommandFlags flags) + => StreamReadGroup( + streamPositions, + groupName, + consumerName, + countPerStream, + noAck, + null, flags); - } - public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) + public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None) { var msg = GetMultiStreamReadGroupMessage( streamPositions, @@ -3337,23 +3366,33 @@ public RedisStream[] StreamReadGroup(StreamPosition[] streamPositions, RedisValu consumerName, countPerStream, noAck, + claimMinIdleTime, flags); return ExecuteSync(msg, ResultProcessor.MultiStream, defaultValue: Array.Empty()); } public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, CommandFlags flags) - { - return StreamReadGroupAsync( + => StreamReadGroupAsync( streamPositions, groupName, consumerName, countPerStream, false, + null, flags); - } - public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, CommandFlags flags = CommandFlags.None) + public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, bool noAck, CommandFlags flags) + => StreamReadGroupAsync( + streamPositions, + groupName, + consumerName, + countPerStream, + noAck, + null, + flags); + + public Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None) { var msg = GetMultiStreamReadGroupMessage( streamPositions, @@ -3361,6 +3400,7 @@ public Task StreamReadGroupAsync(StreamPosition[] streamPositions consumerName, countPerStream, noAck, + claimMinIdleTime, flags); return ExecuteAsync(msg, ResultProcessor.MultiStream, defaultValue: Array.Empty()); @@ -3998,7 +4038,7 @@ internal static RedisValue GetLexRange(RedisValue value, Exclude exclude, bool i return result; } - private Message GetMultiStreamReadGroupMessage(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, bool noAck, CommandFlags flags) => + private Message GetMultiStreamReadGroupMessage(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, bool noAck, TimeSpan? claimMinIdleTime, CommandFlags flags) => new MultiStreamReadGroupCommandMessage( Database, flags, @@ -4006,7 +4046,8 @@ private Message GetMultiStreamReadGroupMessage(StreamPosition[] streamPositions, groupName, consumerName, countPerStream, - noAck); + noAck, + claimMinIdleTime); private sealed class MultiStreamReadGroupCommandMessage : Message // XREADGROUP with multiple stream. Example: XREADGROUP GROUP groupName consumerName COUNT countPerStream STREAMS stream1 stream2 id1 id2 { @@ -4016,8 +4057,9 @@ private sealed class MultiStreamReadGroupCommandMessage : Message // XREADGROUP private readonly int? countPerStream; private readonly bool noAck; private readonly int argCount; + private readonly TimeSpan? claimMinIdleTime; - public MultiStreamReadGroupCommandMessage(int db, CommandFlags flags, StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, bool noAck) + public MultiStreamReadGroupCommandMessage(int db, CommandFlags flags, StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream, bool noAck, TimeSpan? claimMinIdleTime) : base(db, flags, RedisCommand.XREADGROUP) { if (streamPositions == null) throw new ArgumentNullException(nameof(streamPositions)); @@ -4040,11 +4082,13 @@ public MultiStreamReadGroupCommandMessage(int db, CommandFlags flags, StreamPosi this.consumerName = consumerName; this.countPerStream = countPerStream; this.noAck = noAck; + this.claimMinIdleTime = claimMinIdleTime; argCount = 4 // Room for GROUP groupName consumerName & STREAMS + (streamPositions.Length * 2) // Enough room for the stream keys and associated IDs. + (countPerStream.HasValue ? 2 : 0) // Room for "COUNT num" or 0 if countPerStream is null. - + (noAck ? 1 : 0); // Allow for the NOACK subcommand. + + (noAck ? 1 : 0) // Allow for the NOACK subcommand. + + (claimMinIdleTime.HasValue ? 2 : 0); // Allow for the CLAIM {minIdleTime} subcommand. } public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) @@ -4060,22 +4104,28 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) protected override void WriteImpl(PhysicalConnection physical) { physical.WriteHeader(Command, argCount); - physical.WriteBulkString(StreamConstants.Group); + physical.WriteBulkString("GROUP"u8); physical.WriteBulkString(groupName); physical.WriteBulkString(consumerName); if (countPerStream.HasValue) { - physical.WriteBulkString(StreamConstants.Count); + physical.WriteBulkString("COUNT"u8); physical.WriteBulkString(countPerStream.Value); } if (noAck) { - physical.WriteBulkString(StreamConstants.NoAck); + physical.WriteBulkString("NOACK"u8); } - physical.WriteBulkString(StreamConstants.Streams); + if (claimMinIdleTime.HasValue) + { + physical.WriteBulkString("CLAIM"u8); + physical.WriteBulkString(claimMinIdleTime.Value.TotalMilliseconds); + } + + physical.WriteBulkString("STREAMS"u8); for (int i = 0; i < streamPositions.Length; i++) { physical.Write(streamPositions[i].Key); @@ -4137,11 +4187,11 @@ protected override void WriteImpl(PhysicalConnection physical) if (countPerStream.HasValue) { - physical.WriteBulkString(StreamConstants.Count); + physical.WriteBulkString("COUNT"u8); physical.WriteBulkString(countPerStream.Value); } - physical.WriteBulkString(StreamConstants.Streams); + physical.WriteBulkString("STREAMS"u8); for (int i = 0; i < streamPositions.Length; i++) { physical.Write(streamPositions[i].Key); @@ -4814,8 +4864,8 @@ private Message GetStreamRangeMessage(RedisKey key, RedisValue? minId, RedisValu values); } - private Message GetStreamReadGroupMessage(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue afterId, int? count, bool noAck, CommandFlags flags) => - new SingleStreamReadGroupCommandMessage(Database, flags, key, groupName, consumerName, afterId, count, noAck); + private Message GetStreamReadGroupMessage(RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue afterId, int? count, bool noAck, TimeSpan? claimMinIdleTime, CommandFlags flags) => + new SingleStreamReadGroupCommandMessage(Database, flags, key, groupName, consumerName, afterId, count, noAck, claimMinIdleTime); private sealed class SingleStreamReadGroupCommandMessage : Message.CommandKeyBase // XREADGROUP with single stream. eg XREADGROUP GROUP mygroup Alice COUNT 1 STREAMS mystream > { @@ -4825,8 +4875,9 @@ private sealed class SingleStreamReadGroupCommandMessage : Message.CommandKeyBas private readonly int? count; private readonly bool noAck; private readonly int argCount; + private readonly TimeSpan? claimMinIdleTime; - public SingleStreamReadGroupCommandMessage(int db, CommandFlags flags, RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue afterId, int? count, bool noAck) + public SingleStreamReadGroupCommandMessage(int db, CommandFlags flags, RedisKey key, RedisValue groupName, RedisValue consumerName, RedisValue afterId, int? count, bool noAck, TimeSpan? claimMinIdleTime) : base(db, flags, RedisCommand.XREADGROUP, key) { if (count.HasValue && count <= 0) @@ -4843,28 +4894,35 @@ public SingleStreamReadGroupCommandMessage(int db, CommandFlags flags, RedisKey this.afterId = afterId; this.count = count; this.noAck = noAck; - argCount = 6 + (count.HasValue ? 2 : 0) + (noAck ? 1 : 0); + this.claimMinIdleTime = claimMinIdleTime; + argCount = 6 + (count.HasValue ? 2 : 0) + (noAck ? 1 : 0) + (claimMinIdleTime.HasValue ? 2 : 0); } protected override void WriteImpl(PhysicalConnection physical) { physical.WriteHeader(Command, argCount); - physical.WriteBulkString(StreamConstants.Group); + physical.WriteBulkString("GROUP"u8); physical.WriteBulkString(groupName); physical.WriteBulkString(consumerName); if (count.HasValue) { - physical.WriteBulkString(StreamConstants.Count); + physical.WriteBulkString("COUNT"u8); physical.WriteBulkString(count.Value); } if (noAck) { - physical.WriteBulkString(StreamConstants.NoAck); + physical.WriteBulkString("NOACK"u8); + } + + if (claimMinIdleTime.HasValue) + { + physical.WriteBulkString("CLAIM"u8); + physical.WriteBulkString(claimMinIdleTime.Value.TotalMilliseconds); } - physical.WriteBulkString(StreamConstants.Streams); + physical.WriteBulkString("STREAMS"u8); physical.Write(Key); physical.WriteBulkString(afterId); } @@ -4902,11 +4960,11 @@ protected override void WriteImpl(PhysicalConnection physical) if (count.HasValue) { - physical.WriteBulkString(StreamConstants.Count); + physical.WriteBulkString("COUNT"u8); physical.WriteBulkString(count.Value); } - physical.WriteBulkString(StreamConstants.Streams); + physical.WriteBulkString("STREAMS"u8); physical.Write(Key); physical.WriteBulkString(afterId); } diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 9bc9af6d2..1218a9f80 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -47,7 +47,7 @@ namespace StackExchange.Redis v7_4_0_rc2 = new Version(7, 3, 241), // 7.4 RC2 is version 7.3.241 v8_0_0_M04 = new Version(7, 9, 227), // 8.0 M04 is version 7.9.227 v8_2_0_rc1 = new Version(8, 1, 240), // 8.2 RC1 is version 8.1.240 - v8_4_0_rc1 = new Version(8, 3, 224); // 8.4 RC1 is version 8.3.224 + v8_4_0_rc1 = new Version(8, 3, 224); // 8.2 RC1 is version 8.3.224 #pragma warning restore SA1310 // Field names should not contain underscore #pragma warning restore SA1311 // Static readonly fields should begin with upper-case letter diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 650cba603..196cabde5 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -2215,6 +2215,8 @@ Multibulk array. 2) "Jane" 3) "surname" 4) "Austen" + + (note that XREADGROUP may include additional interior elements; see ParseRedisStreamEntries) */ protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) @@ -2683,11 +2685,25 @@ protected static StreamEntry ParseRedisStreamEntry(in RawResult item) // Process the Multibulk array for each entry. The entry contains the following elements: // [0] = SimpleString (the ID of the stream entry) // [1] = Multibulk array of the name/value pairs of the stream entry's data + // optional (XREADGROUP with CLAIM): + // [2] = idle time (in milliseconds) + // [3] = delivery count var entryDetails = item.GetItems(); + var id = entryDetails[0].AsRedisValue(); + var values = ParseStreamEntryValues(entryDetails[1]); + // check for optional fields (XREADGROUP with CLAIM) + if (entryDetails.Length >= 4 && entryDetails[2].TryGetInt64(out var idleTimeInMs) && entryDetails[3].TryGetInt64(out var deliveryCount)) + { + return new StreamEntry( + id: id, + values: values, + idleTime: TimeSpan.FromMilliseconds(idleTimeInMs), + deliveryCount: checked((int)deliveryCount)); + } return new StreamEntry( - id: entryDetails[0].AsRedisValue(), - values: ParseStreamEntryValues(entryDetails[1])); + id: id, + values: values); } protected internal StreamEntry[] ParseRedisStreamEntries(in RawResult result) => result.GetItems().ToArray((in RawResult item, in StreamProcessorBase _) => ParseRedisStreamEntry(item), this); diff --git a/src/StackExchange.Redis/StreamConstants.cs b/src/StackExchange.Redis/StreamConstants.cs index 929398e4b..92c37222a 100644 --- a/src/StackExchange.Redis/StreamConstants.cs +++ b/src/StackExchange.Redis/StreamConstants.cs @@ -52,8 +52,6 @@ internal static class StreamConstants internal static readonly RedisValue Destroy = "DESTROY"; - internal static readonly RedisValue Group = "GROUP"; - internal static readonly RedisValue Groups = "GROUPS"; internal static readonly RedisValue JustId = "JUSTID"; @@ -65,12 +63,8 @@ internal static class StreamConstants internal static readonly RedisValue MkStream = "MKSTREAM"; - internal static readonly RedisValue NoAck = "NOACK"; - internal static readonly RedisValue Stream = "STREAM"; - internal static readonly RedisValue Streams = "STREAMS"; - private static readonly RedisValue KeepRef = "KEEPREF", DelRef = "DELREF", Acked = "ACKED"; internal static readonly RedisValue Ids = "IDS"; diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index 58d2bb1fb..2419f673a 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -495,8 +495,8 @@ public async Task StreamConsumerGroupSetId() var db = conn.GetDatabase(); var key = Me(); - const string groupName = "test_group", - consumer = "consumer"; + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + const string groupName = "test_group", consumer = "consumer"; // Create a stream db.StreamAdd(key, "field1", "value1"); @@ -519,6 +519,101 @@ public async Task StreamConsumerGroupSetId() Assert.Equal(2, secondRead.Length); } + [Fact] + public async Task StreamConsumerGroupAutoClaim_MultiStream() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + const string groupName = "test_group", consumer = "consumer"; + + // Create a group and set the position to deliver new messages only. + await db.StreamCreateConsumerGroupAsync(key, groupName, StreamPosition.NewMessages); + + // add some entries + await db.StreamAddAsync(key, "field1", "value1"); + await db.StreamAddAsync(key, "field2", "value2"); + + var idleTime = TimeSpan.FromMilliseconds(100); + // Read into the group, expect the two entries; we don't expect any data + // here, at least on a fast server, because it hasn't been idle long enough. + StreamPosition[] positions = [new(key, StreamPosition.NewMessages)]; + var groups = await db.StreamReadGroupAsync(positions, groupName, consumer, noAck: false, countPerStream: 10, claimMinIdleTime: idleTime); + var grp = Assert.Single(groups); + Assert.Equal(key, grp.Key); + Assert.Equal(2, grp.Entries.Length); + foreach (var entry in grp.Entries) + { + Assert.Equal(0, entry.DeliveryCount); // never delivered before + Assert.Equal(TimeSpan.Zero, entry.IdleTime); // never delivered before + } + + // now repeat immediately; we didn't "ack", so they're still pending, but not idle long enough + groups = await db.StreamReadGroupAsync(positions, groupName, consumer, noAck: false, countPerStream: 10, claimMinIdleTime: idleTime); + Assert.Empty(groups); // nothing available from any group + + // wait long enough for the messages to be considered idle + await Task.Delay(idleTime + idleTime); + + // repeat again; we should get the entries + groups = await db.StreamReadGroupAsync(positions, groupName, consumer, noAck: false, countPerStream: 10, claimMinIdleTime: idleTime); + grp = Assert.Single(groups); + Assert.Equal(key, grp.Key); + Assert.Equal(2, grp.Entries.Length); + foreach (var entry in grp.Entries) + { + Assert.Equal(1, entry.DeliveryCount); // this is a redelivery + Assert.True(entry.IdleTime > TimeSpan.Zero); // and is considered idle + } + } + + [Fact] + public async Task StreamConsumerGroupAutoClaim_SingleStream() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + + var db = conn.GetDatabase(); + var key = Me(); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + const string groupName = "test_group", consumer = "consumer"; + + // Create a group and set the position to deliver new messages only. + await db.StreamCreateConsumerGroupAsync(key, groupName, StreamPosition.NewMessages); + + // add some entries + await db.StreamAddAsync(key, "field1", "value1"); + await db.StreamAddAsync(key, "field2", "value2"); + + var idleTime = TimeSpan.FromMilliseconds(100); + // Read into the group, expect the two entries; we don't expect any data + // here, at least on a fast server, because it hasn't been idle long enough. + var entries = await db.StreamReadGroupAsync(key, groupName, consumer, noAck: false, count: 10, claimMinIdleTime: idleTime); + Assert.Equal(2, entries.Length); + foreach (var entry in entries) + { + Assert.Equal(0, entry.DeliveryCount); // never delivered before + Assert.Equal(TimeSpan.Zero, entry.IdleTime); // never delivered before + } + + // now repeat immediately; we didn't "ack", so they're still pending, but not idle long enough + entries = await db.StreamReadGroupAsync(key, groupName, consumer, null, noAck: false, count: 10, claimMinIdleTime: idleTime); + Assert.Empty(entries); // nothing available from any group + + // wait long enough for the messages to be considered idle + await Task.Delay(idleTime + idleTime); + + // repeat again; we should get the entries + entries = await db.StreamReadGroupAsync(key, groupName, consumer, null, noAck: false, count: 10, claimMinIdleTime: idleTime); + Assert.Equal(2, entries.Length); + foreach (var entry in entries) + { + Assert.Equal(1, entry.DeliveryCount); // this is a redelivery + Assert.True(entry.IdleTime > TimeSpan.Zero); // and is considered idle + } + } + [Fact] public async Task StreamConsumerGroupWithNoConsumers() { From 94e0090f2db03f6941547e839f1af40f5935ee21 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 5 Nov 2025 16:03:53 +0000 Subject: [PATCH 389/435] Support 8.4 CAS/CAD (IF*) operations (#2978) * Completely untested start for CAD/CAD * unit tests * propose the actual API * DIGEST integration tests * compensate for leading-zero oddness * apply fixes for server hash format changes * use CAS/CAD in locking operations * release notes * remove mitigations for receiving under-length digests from the server (now fixed at server) * add zero-length digest test * Update src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs Co-authored-by: Philo --------- Co-authored-by: Philo --- Directory.Packages.props | 1 + docs/ReleaseNotes.md | 2 + docs/exp/SER002.md | 4 +- src/Directory.Build.props | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 4 + src/StackExchange.Redis/Experiments.cs | 1 + .../Interfaces/IDatabase.cs | 34 ++ .../Interfaces/IDatabaseAsync.cs | 15 + .../KeyspaceIsolation/KeyPrefixed.cs | 9 + .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 9 + .../Message.ValueCondition.cs | 71 ++++ src/StackExchange.Redis/Message.cs | 2 +- .../PublicAPI/PublicAPI.Unshipped.txt | 27 ++ .../RedisDatabase.Strings.cs | 81 ++++ src/StackExchange.Redis/RedisDatabase.cs | 41 +- src/StackExchange.Redis/RedisFeatures.cs | 12 +- src/StackExchange.Redis/RedisValue.cs | 22 ++ .../ResultProcessor.Digest.cs | 42 ++ .../StackExchange.Redis.csproj | 2 + src/StackExchange.Redis/ValueCondition.cs | 361 ++++++++++++++++++ .../DigestIntegrationTests.cs | 157 ++++++++ .../DigestUnitTests.cs | 188 +++++++++ 22 files changed, 1077 insertions(+), 9 deletions(-) create mode 100644 src/StackExchange.Redis/Message.ValueCondition.cs create mode 100644 src/StackExchange.Redis/RedisDatabase.Strings.cs create mode 100644 src/StackExchange.Redis/ResultProcessor.Digest.cs create mode 100644 src/StackExchange.Redis/ValueCondition.cs create mode 100644 tests/StackExchange.Redis.Tests/DigestIntegrationTests.cs create mode 100644 tests/StackExchange.Redis.Tests/DigestUnitTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index df8c078a3..2088a054f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,6 +8,7 @@ + diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index e6c010116..58de2287b 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,8 @@ Current package versions: ## Unreleased +- Support Redis 8.4 CAS/CAD operations (`DIGEST`, and the `IFEQ`, `IFNE`, `IFDEQ`, `IFDNE` modifiers on `SET` / `DEL`) + via the new `ValueCondition` abstraction, and use CAS/CAD operations for `Lock*` APIs when possible ([#2978 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2978)) - Support `XREADGROUP CLAIM` ([#2972 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2972)) - Support `MSETEX` (Redis 8.4.0) for multi-key operations with expiration ([#2977 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2977)) diff --git a/docs/exp/SER002.md b/docs/exp/SER002.md index 21a2990c6..6e5100a6e 100644 --- a/docs/exp/SER002.md +++ b/docs/exp/SER002.md @@ -2,7 +2,7 @@ Redis 8.4 is currently in preview and may be subject to change. New features in Redis 8.4 include: -- [`MSETEX`](https://github.com/redis/redis/pull/14434) for setting multiple strings with expiry +- [`MSETEX`](https://github.com/redis/redis/pull/14434) for setting multiple strings with expiry - [`XREADGROUP ... CLAIM`](https://github.com/redis/redis/pull/14402) for simplifed stream consumption - [`SET ... {IFEQ|IFNE|IFDEQ|IFDNE}`, `DELEX` and `DIGEST`](https://github.com/redis/redis/pull/14434) for checked (CAS/CAD) string operations @@ -10,7 +10,7 @@ The corresponding library feature must also be considered subject to change: 1. Existing bindings may cease working correctly if the underlying server API changes. 2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time - or run-time breaks. + or run-time breaks. While this seems *unlikely*, it must be considered a possibility. If you acknowledge this, you can suppress this warning by adding the following to your `csproj` file: diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 06e403ebb..40f59348d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,6 +4,7 @@ true true false + true diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 6138d2609..14f304a35 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -30,6 +30,8 @@ internal enum RedisCommand DECR, DECRBY, DEL, + DELEX, + DIGEST, DISCARD, DUMP, @@ -300,6 +302,8 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.DECR: case RedisCommand.DECRBY: case RedisCommand.DEL: + case RedisCommand.DELEX: + case RedisCommand.DIGEST: case RedisCommand.EXPIRE: case RedisCommand.EXPIREAT: case RedisCommand.FLUSHALL: diff --git a/src/StackExchange.Redis/Experiments.cs b/src/StackExchange.Redis/Experiments.cs index 9234f9f4e..441b0ec54 100644 --- a/src/StackExchange.Redis/Experiments.cs +++ b/src/StackExchange.Redis/Experiments.cs @@ -8,6 +8,7 @@ namespace StackExchange.Redis internal static class Experiments { public const string UrlFormat = "https://stackexchange.github.io/StackExchange.Redis/exp/"; + public const string VectorSets = "SER001"; // ReSharper disable once InconsistentNaming public const string Server_8_4 = "SER002"; diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index e15b4bbdb..5556990df 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Net; // ReSharper disable once CheckNamespace @@ -3174,6 +3175,16 @@ IEnumerable SortedSetScan( /// long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); + /// + /// Deletes if it matches the given condition. + /// + /// The key of the string. + /// The condition to enforce. + /// The flags to use for this operation. + /// See . + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] + bool StringDelete(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None); + /// /// Decrements the string representing a floating point number stored at key by the specified decrement. /// If the key does not exist, it is set to 0 before performing the operation. @@ -3186,6 +3197,15 @@ IEnumerable SortedSetScan( /// double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None); + /// + /// Gets the digest (hash) value of the specified key, represented as a digest equality . + /// + /// The key of the string. + /// The flags to use for this operation. + /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] + ValueCondition? StringDigest(RedisKey key, CommandFlags flags = CommandFlags.None); + /// /// Get the value of key. If the key does not exist the special value is returned. /// An error is returned if the value stored at key is not a string, because GET only handles string values. @@ -3393,6 +3413,20 @@ IEnumerable SortedSetScan( /// bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); + /// + /// Set to hold the string , if it matches the given condition. + /// + /// The key of the string. + /// The value to set. + /// The expiry to set. + /// The condition to enforce. + /// The flags to use for this operation. + /// See . + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] +#pragma warning disable RS0027 + bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None); +#pragma warning restore RS0027 + /// /// Sets the given keys to their respective values. /// If is specified, this will not perform any operation at all even if just a single key already exists. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 6e411cbd3..b35a685a5 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Threading.Tasks; @@ -771,9 +772,17 @@ IAsyncEnumerable SortedSetScanAsync( /// Task StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); + /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] + Task StringDeleteAsync(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None); + /// Task StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None); + /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] + Task StringDigestAsync(RedisKey key, CommandFlags flags = CommandFlags.None); + /// Task StringGetAsync(RedisKey key, CommandFlags flags = CommandFlags.None); @@ -833,6 +842,12 @@ IAsyncEnumerable SortedSetScanAsync( /// Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); + /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] +#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads + Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None); +#pragma warning restore RS0027 + /// Task StringSetAsync(KeyValuePair[] values, When when, CommandFlags flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 378c90704..9119035a3 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -732,9 +732,15 @@ public Task StringBitPositionAsync(RedisKey key, bool bit, long start, lon public Task StringBitPositionAsync(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => Inner.StringBitPositionAsync(ToInner(key), bit, start, end, indexType, flags); + public Task StringDeleteAsync(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None) => + Inner.StringDeleteAsync(ToInner(key), when, flags); + public Task StringDecrementAsync(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => Inner.StringDecrementAsync(ToInner(key), value, flags); + public Task StringDigestAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StringDigestAsync(ToInner(key), flags); + public Task StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => Inner.StringDecrementAsync(ToInner(key), value, flags); @@ -777,6 +783,9 @@ public Task StringIncrementAsync(RedisKey key, long value = 1, CommandFlag public Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.StringLengthAsync(ToInner(key), flags); + public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) + => Inner.StringSetAsync(ToInner(key), value, expiry, when, flags); + public Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.StringSetAsync(ToInner(values), when, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index dfb906f32..3c7e34aa9 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -714,9 +714,15 @@ public long StringBitPosition(RedisKey key, bool bit, long start, long end, Comm public long StringBitPosition(RedisKey key, bool bit, long start = 0, long end = -1, StringIndexType indexType = StringIndexType.Byte, CommandFlags flags = CommandFlags.None) => Inner.StringBitPosition(ToInner(key), bit, start, end, indexType, flags); + public bool StringDelete(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None) => + Inner.StringDelete(ToInner(key), when, flags); + public double StringDecrement(RedisKey key, double value, CommandFlags flags = CommandFlags.None) => Inner.StringDecrement(ToInner(key), value, flags); + public ValueCondition? StringDigest(RedisKey key, CommandFlags flags = CommandFlags.None) => + Inner.StringDigest(ToInner(key), flags); + public long StringDecrement(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None) => Inner.StringDecrement(ToInner(key), value, flags); @@ -759,6 +765,9 @@ public long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = C public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.StringLength(ToInner(key), flags); + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) + => Inner.StringSet(ToInner(key), value, expiry, when, flags); + public bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => Inner.StringSet(ToInner(values), when, flags); diff --git a/src/StackExchange.Redis/Message.ValueCondition.cs b/src/StackExchange.Redis/Message.ValueCondition.cs new file mode 100644 index 000000000..c8b5febc4 --- /dev/null +++ b/src/StackExchange.Redis/Message.ValueCondition.cs @@ -0,0 +1,71 @@ +using System; + +namespace StackExchange.Redis; + +internal partial class Message +{ + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in ValueCondition when) + => new KeyConditionMessage(db, flags, command, key, when); + + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, TimeSpan? expiry, in ValueCondition when) + => new KeyValueExpiryConditionMessage(db, flags, command, key, value, expiry, when); + + private sealed class KeyConditionMessage( + int db, + CommandFlags flags, + RedisCommand command, + in RedisKey key, + in ValueCondition when) + : CommandKeyBase(db, flags, command, key) + { + private readonly ValueCondition _when = when; + + public override int ArgCount => 1 + _when.TokenCount; + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, ArgCount); + physical.Write(Key); + _when.WriteTo(physical); + } + } + + private sealed class KeyValueExpiryConditionMessage( + int db, + CommandFlags flags, + RedisCommand command, + in RedisKey key, + in RedisValue value, + TimeSpan? expiry, + in ValueCondition when) + : CommandKeyBase(db, flags, command, key) + { + private readonly RedisValue _value = value; + private readonly ValueCondition _when = when; + private readonly TimeSpan? _expiry = expiry == TimeSpan.MaxValue ? null : expiry; + + public override int ArgCount => 2 + _when.TokenCount + (_expiry is null ? 0 : 2); + + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, ArgCount); + physical.Write(Key); + physical.WriteBulkString(_value); + if (_expiry.HasValue) + { + var ms = (long)_expiry.GetValueOrDefault().TotalMilliseconds; + if ((ms % 1000) == 0) + { + physical.WriteBulkString("EX"u8); + physical.WriteBulkString(ms / 1000); + } + else + { + physical.WriteBulkString("PX"u8); + physical.WriteBulkString(ms); + } + } + _when.WriteTo(physical); + } + } +} diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index c8d433d17..0eff3ff8d 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -49,7 +49,7 @@ protected override void WriteImpl(PhysicalConnection physical) public ILogger Log => log; } - internal abstract class Message : ICompletable + internal abstract partial class Message : ICompletable { public readonly int Db; diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index ab058de62..1b8aba3b0 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,28 @@ #nullable enable +StackExchange.Redis.RedisFeatures.DeleteWithValueCheck.get -> bool +StackExchange.Redis.RedisFeatures.SetWithValueCheck.get -> bool +[SER002]override StackExchange.Redis.ValueCondition.Equals(object? obj) -> bool +[SER002]override StackExchange.Redis.ValueCondition.GetHashCode() -> int +[SER002]override StackExchange.Redis.ValueCondition.ToString() -> string! +[SER002]StackExchange.Redis.IDatabase.StringDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER002]StackExchange.Redis.IDatabase.StringDigest(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ValueCondition? +[SER002]StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER002]StackExchange.Redis.IDatabaseAsync.StringDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER002]StackExchange.Redis.IDatabaseAsync.StringDigestAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER002]StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER002]StackExchange.Redis.ValueCondition +[SER002]StackExchange.Redis.ValueCondition.AsDigest() -> StackExchange.Redis.ValueCondition +[SER002]StackExchange.Redis.ValueCondition.Value.get -> StackExchange.Redis.RedisValue +[SER002]StackExchange.Redis.ValueCondition.ValueCondition() -> void +[SER002]static StackExchange.Redis.ValueCondition.Always.get -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.CalculateDigest(System.ReadOnlySpan value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.DigestEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.DigestNotEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.Equal(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.Exists.get -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.implicit operator StackExchange.Redis.ValueCondition(StackExchange.Redis.When when) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.NotEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.NotExists.get -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.operator !(in StackExchange.Redis.ValueCondition value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.ParseDigest(System.ReadOnlySpan digest) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.ParseDigest(System.ReadOnlySpan digest) -> StackExchange.Redis.ValueCondition diff --git a/src/StackExchange.Redis/RedisDatabase.Strings.cs b/src/StackExchange.Redis/RedisDatabase.Strings.cs new file mode 100644 index 000000000..1323246f9 --- /dev/null +++ b/src/StackExchange.Redis/RedisDatabase.Strings.cs @@ -0,0 +1,81 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace StackExchange.Redis; + +internal partial class RedisDatabase +{ + public bool StringDelete(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None) + { + var msg = GetStringDeleteMessage(key, when, flags); + return ExecuteSync(msg, ResultProcessor.Boolean); + } + + public Task StringDeleteAsync(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None) + { + var msg = GetStringDeleteMessage(key, when, flags); + return ExecuteAsync(msg, ResultProcessor.Boolean); + } + + private Message GetStringDeleteMessage(in RedisKey key, in ValueCondition when, CommandFlags flags, [CallerMemberName] string? operation = null) + { + switch (when.Kind) + { + case ValueCondition.ConditionKind.Always: + case ValueCondition.ConditionKind.Exists: + return Message.Create(Database, flags, RedisCommand.DEL, key); + case ValueCondition.ConditionKind.ValueEquals: + case ValueCondition.ConditionKind.ValueNotEquals: + case ValueCondition.ConditionKind.DigestEquals: + case ValueCondition.ConditionKind.DigestNotEquals: + return Message.Create(Database, flags, RedisCommand.DELEX, key, when); + default: + when.ThrowInvalidOperation(operation); + goto case ValueCondition.ConditionKind.Always; // not reached + } + } + + public ValueCondition? StringDigest(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.DIGEST, key); + return ExecuteSync(msg, ResultProcessor.Digest); + } + + public Task StringDigestAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + { + var msg = Message.Create(Database, flags, RedisCommand.DIGEST, key); + return ExecuteAsync(msg, ResultProcessor.Digest); + } + + public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) + { + var msg = GetStringSetMessage(key, value, expiry, when, flags); + return ExecuteAsync(msg, ResultProcessor.Boolean); + } + + public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) + { + var msg = GetStringSetMessage(key, value, expiry, when, flags); + return ExecuteSync(msg, ResultProcessor.Boolean); + } + + private Message GetStringSetMessage(in RedisKey key, in RedisValue value, TimeSpan? expiry, in ValueCondition when, CommandFlags flags, [CallerMemberName] string? operation = null) + { + switch (when.Kind) + { + case ValueCondition.ConditionKind.Exists: + case ValueCondition.ConditionKind.NotExists: + case ValueCondition.ConditionKind.Always: + return GetStringSetMessage(key, value, expiry: expiry, when: when.AsWhen(), flags: flags); + case ValueCondition.ConditionKind.ValueEquals: + case ValueCondition.ConditionKind.ValueNotEquals: + case ValueCondition.ConditionKind.DigestEquals: + case ValueCondition.ConditionKind.DigestNotEquals: + return Message.Create(Database, flags, RedisCommand.SET, key, value, expiry, when); + default: + when.ThrowInvalidOperation(operation); + goto case ValueCondition.ConditionKind.Always; // not reached + } + } +} diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 6f39bf5be..948a9e894 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Net; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Pipelines.Sockets.Unofficial.Arenas; @@ -1770,18 +1771,33 @@ public Task ListTrimAsync(RedisKey key, long start, long stop, CommandFlags flag public bool LockExtend(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) { - if (value.IsNull) throw new ArgumentNullException(nameof(value)); - var tran = GetLockExtendTransaction(key, value, expiry); + var msg = TryGetLockExtendMessage(key, value, expiry, flags, out var server); + if (msg is not null) return ExecuteSync(msg, ResultProcessor.Boolean, server); + var tran = GetLockExtendTransaction(key, value, expiry); if (tran != null) return tran.Execute(flags); // without transactions (twemproxy etc), we can't enforce the "value" part return KeyExpire(key, expiry, flags); } - public Task LockExtendAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) + private Message? TryGetLockExtendMessage(in RedisKey key, in RedisValue value, TimeSpan expiry, CommandFlags flags, out ServerEndPoint? server, [CallerMemberName] string? caller = null) { if (value.IsNull) throw new ArgumentNullException(nameof(value)); + + // note that lock tokens are expected to be small, so: we'll use IFEQ rather than IFDEQ, for reliability + // note possible future extension:[P]EXPIRE ... IF* https://github.com/redis/redis/issues/14505 + var features = GetFeatures(key, flags, RedisCommand.SET, out server); + return features.SetWithValueCheck + ? GetStringSetMessage(key, value, expiry, ValueCondition.Equal(value), flags, caller) // use check-and-set + : null; + } + + public Task LockExtendAsync(RedisKey key, RedisValue value, TimeSpan expiry, CommandFlags flags = CommandFlags.None) + { + var msg = TryGetLockExtendMessage(key, value, expiry, flags, out var server); + if (msg is not null) return ExecuteAsync(msg, ResultProcessor.Boolean, server); + var tran = GetLockExtendTransaction(key, value, expiry); if (tran != null) return tran.ExecuteAsync(flags); @@ -1801,7 +1817,9 @@ public Task LockQueryAsync(RedisKey key, CommandFlags flags = Comman public bool LockRelease(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) { - if (value.IsNull) throw new ArgumentNullException(nameof(value)); + var msg = TryGetLockReleaseMessage(key, value, flags, out var server); + if (msg is not null) return ExecuteSync(msg, ResultProcessor.Boolean, server); + var tran = GetLockReleaseTransaction(key, value); if (tran != null) return tran.Execute(flags); @@ -1809,9 +1827,22 @@ public bool LockRelease(RedisKey key, RedisValue value, CommandFlags flags = Com return KeyDelete(key, flags); } - public Task LockReleaseAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) + private Message? TryGetLockReleaseMessage(in RedisKey key, in RedisValue value, CommandFlags flags, out ServerEndPoint? server, [CallerMemberName] string? caller = null) { if (value.IsNull) throw new ArgumentNullException(nameof(value)); + + // note that lock tokens are expected to be small, so: we'll use IFEQ rather than IFDEQ, for reliability + var features = GetFeatures(key, flags, RedisCommand.DELEX, out server); + return features.DeleteWithValueCheck + ? GetStringDeleteMessage(key, ValueCondition.Equal(value), flags, caller) // use check-and-delete + : null; + } + + public Task LockReleaseAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) + { + var msg = TryGetLockReleaseMessage(key, value, flags, out var server); + if (msg is not null) return ExecuteAsync(msg, ResultProcessor.Boolean, server); + var tran = GetLockReleaseTransaction(key, value); if (tran != null) return tran.ExecuteAsync(flags); diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 1218a9f80..0e6b410a9 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -47,7 +47,7 @@ namespace StackExchange.Redis v7_4_0_rc2 = new Version(7, 3, 241), // 7.4 RC2 is version 7.3.241 v8_0_0_M04 = new Version(7, 9, 227), // 8.0 M04 is version 7.9.227 v8_2_0_rc1 = new Version(8, 1, 240), // 8.2 RC1 is version 8.1.240 - v8_4_0_rc1 = new Version(8, 3, 224); // 8.2 RC1 is version 8.3.224 + v8_4_0_rc1 = new Version(8, 3, 224); // 8.4 RC1 is version 8.3.224 #pragma warning restore SA1310 // Field names should not contain underscore #pragma warning restore SA1311 // Static readonly fields should begin with upper-case letter @@ -285,6 +285,16 @@ public RedisFeatures(Version version) /// public bool Resp3 => Version.IsAtLeast(v6_0_0); + /// + /// Are the IF* modifiers on SET available? + /// + public bool SetWithValueCheck => Version.IsAtLeast(v8_4_0_rc1); + + /// + /// Are the IF* modifiers on DEL available? + /// + public bool DeleteWithValueCheck => Version.IsAtLeast(v8_4_0_rc1); + #pragma warning restore 1629 // Documentation text should end with a period. /// diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index da33c803e..d306ca0d0 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -1223,5 +1223,27 @@ private ReadOnlyMemory AsMemory(out byte[]? leased) leased = null; return default; } + + /// + /// Get the digest (hash used for check-and-set/check-and-delete operations) of this value. + /// + internal ValueCondition Digest() + { + switch (Type) + { + case StorageType.Raw: + return ValueCondition.CalculateDigest(_memory.Span); + case StorageType.Null: + return ValueCondition.NotExists; // interpret === null as "not exists" + default: + var len = GetByteCount(); + byte[]? oversized = null; + Span buffer = len <= 128 ? stackalloc byte[128] : (oversized = ArrayPool.Shared.Rent(len)); + CopyTo(buffer); + var digest = ValueCondition.CalculateDigest(buffer.Slice(0, len)); + if (oversized is not null) ArrayPool.Shared.Return(oversized); + return digest; + } + } } } diff --git a/src/StackExchange.Redis/ResultProcessor.Digest.cs b/src/StackExchange.Redis/ResultProcessor.Digest.cs new file mode 100644 index 000000000..757009ea5 --- /dev/null +++ b/src/StackExchange.Redis/ResultProcessor.Digest.cs @@ -0,0 +1,42 @@ +using System; +using System.Buffers; + +namespace StackExchange.Redis; + +internal abstract partial class ResultProcessor +{ + // VectorSet result processors + public static readonly ResultProcessor Digest = + new DigestProcessor(); + + private sealed class DigestProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.IsNull) // for example, key doesn't exist + { + SetResult(message, null); + return true; + } + + if (result.Resp2TypeBulkString == ResultType.BulkString + && result.Payload is { Length: 2 * ValueCondition.DigestBytes } payload) + { + ValueCondition digest; + if (payload.IsSingleSegment) // single chunk - fast path + { + digest = ValueCondition.ParseDigest(payload.First.Span); + } + else // linearize + { + Span buffer = stackalloc byte[2 * ValueCondition.DigestBytes]; + payload.CopyTo(buffer); + digest = ValueCondition.ParseDigest(buffer); + } + SetResult(message, digest); + return true; + } + return false; + } + } +} diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index b03103656..e66b3874c 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -12,6 +12,7 @@ $(DefineConstants);VECTOR_SAFE $(DefineConstants);UNIX_SOCKET README.md + $(NoWarn);SER002 @@ -19,6 +20,7 @@ + diff --git a/src/StackExchange.Redis/ValueCondition.cs b/src/StackExchange.Redis/ValueCondition.cs new file mode 100644 index 000000000..94e9850c4 --- /dev/null +++ b/src/StackExchange.Redis/ValueCondition.cs @@ -0,0 +1,361 @@ +using System; +using System.Buffers.Binary; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO.Hashing; +using System.Runtime.CompilerServices; + +namespace StackExchange.Redis; + +/// +/// Represents a check for an existing value, for use in conditional operations such as DELEX or SET ... IFEQ. +/// +[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] +public readonly struct ValueCondition +{ + internal enum ConditionKind : byte + { + Always, // default, importantly + Exists, + NotExists, + ValueEquals, + ValueNotEquals, + DigestEquals, + DigestNotEquals, + } + + // Supported: equality and non-equality checks for values and digests. Values are stored a RedisValue; + // digests are stored as a native (CPU-endian) Int64 (long) value, inside the same RedisValue (via the + // RedisValue.DirectOverlappedBits64 feature). This native Int64 value is an implementation detail that + // is not directly exposed to the consumer. + // + // The exchange format with Redis is hex of the bytes; for the purposes of interfacing this with our + // raw integer value, this should be considered big-endian, based on the behaviour of XxHash3. + internal const int DigestBytes = 8; // XXH3 is 64-bit + + private readonly ConditionKind _kind; + private readonly RedisValue _value; + + internal ConditionKind Kind => _kind; + + /// + /// Always perform the operation; equivalent to . + /// + public static ValueCondition Always { get; } = new(ConditionKind.Always, RedisValue.Null); + + /// + /// Only perform the operation if the value exists; equivalent to . + /// + public static ValueCondition Exists { get; } = new(ConditionKind.Exists, RedisValue.Null); + + /// + /// Only perform the operation if the value does not exist; equivalent to . + /// + public static ValueCondition NotExists { get; } = new(ConditionKind.NotExists, RedisValue.Null); + + /// + public override string ToString() + { + switch (_kind) + { + case ConditionKind.Exists: + return "XX"; + case ConditionKind.NotExists: + return "NX"; + case ConditionKind.ValueEquals: + return $"IFEQ {_value}"; + case ConditionKind.ValueNotEquals: + return $"IFNE {_value}"; + case ConditionKind.DigestEquals: + var written = WriteHex(_value.DirectOverlappedBits64, stackalloc char[2 * DigestBytes]); + return $"IFDEQ {written.ToString()}"; + case ConditionKind.DigestNotEquals: + written = WriteHex(_value.DirectOverlappedBits64, stackalloc char[2 * DigestBytes]); + return $"IFDNE {written.ToString()}"; + case ConditionKind.Always: + return ""; + default: + return ThrowInvalidOperation().ToString(); + } + } + + /// + public override bool Equals(object? obj) => obj is ValueCondition other && _kind == other._kind && _value == other._value; + + /// + public override int GetHashCode() => _kind.GetHashCode() ^ _value.GetHashCode(); + + /// + /// Indicates whether this instance represents a value comparison test. + /// + internal bool IsValueTest => _kind is ConditionKind.ValueEquals or ConditionKind.ValueNotEquals; + + /// + /// Indicates whether this instance represents a digest test. + /// + internal bool IsDigestTest => _kind is ConditionKind.DigestEquals or ConditionKind.DigestNotEquals; + + /// + /// Indicates whether this instance represents an existence test. + /// + internal bool IsExistenceTest => _kind is ConditionKind.Exists or ConditionKind.NotExists; + + /// + /// Indicates whether this instance represents a negative test (not-equals, not-exists, digest-not-equals). + /// + internal bool IsNegated => _kind is ConditionKind.ValueNotEquals or ConditionKind.DigestNotEquals or ConditionKind.NotExists; + + /// + /// Gets the underlying value for this condition. + /// + public RedisValue Value => _value; + + private ValueCondition(ConditionKind kind, in RedisValue value) + { + if (value.IsNull) + { + kind = kind switch + { + // interpret === null as "does not exist" + ConditionKind.DigestEquals or ConditionKind.ValueEquals => ConditionKind.NotExists, + + // interpret !== null as "exists" + ConditionKind.DigestNotEquals or ConditionKind.ValueNotEquals => ConditionKind.Exists, + + // otherwise: leave alone + _ => kind, + }; + } + _kind = kind; + _value = value; + // if it's a digest operation, the value must be an int64 + Debug.Assert(_kind is not (ConditionKind.DigestEquals or ConditionKind.DigestNotEquals) || + value.Type == RedisValue.StorageType.Int64); + } + + /// + /// Create a value equality condition with the supplied value. + /// + public static ValueCondition Equal(in RedisValue value) => new(ConditionKind.ValueEquals, value); + + /// + /// Create a value non-equality condition with the supplied value. + /// + public static ValueCondition NotEqual(in RedisValue value) => new(ConditionKind.ValueNotEquals, value); + + /// + /// Create a digest equality condition, computing the digest of the supplied value. + /// + public static ValueCondition DigestEqual(in RedisValue value) => value.Digest(); + + /// + /// Create a digest non-equality condition, computing the digest of the supplied value. + /// + public static ValueCondition DigestNotEqual(in RedisValue value) => !value.Digest(); + + /// + /// Calculate the digest of a payload, as an equality test. For a non-equality test, use on the result. + /// + public static ValueCondition CalculateDigest(ReadOnlySpan value) + { + // the internal impl of XxHash3 uses ulong (not Span), so: use + // that to avoid extra steps, and store the CPU-endian value + var digest = unchecked((long)XxHash3.HashToUInt64(value)); + return new ValueCondition(ConditionKind.DigestEquals, digest); + } + + /// + /// Creates an equality match based on the specified digest bytes. + /// + public static ValueCondition ParseDigest(ReadOnlySpan digest) + { + if (digest.Length != 2 * DigestBytes) ThrowDigestLength(); + + // we receive 16 hex characters, as bytes; parse that into a long, by + // first dealing with the nibbles + Span tmp = stackalloc byte[DigestBytes]; + int offset = 0; + for (int i = 0; i < tmp.Length; i++) + { + tmp[i] = (byte)( + (ParseNibble(digest[offset++]) << 4) // hi + | ParseNibble(digest[offset++])); // lo + } + // now interpret that as big-endian + var digestInt64 = BinaryPrimitives.ReadInt64BigEndian(tmp); + return new ValueCondition(ConditionKind.DigestEquals, digestInt64); + } + + private static byte ParseNibble(int b) + { + if (b >= '0' & b <= '9') return (byte)(b - '0'); + if (b >= 'a' & b <= 'f') return (byte)(b - 'a' + 10); + if (b >= 'A' & b <= 'F') return (byte)(b - 'A' + 10); + return ThrowInvalidBytes(); + + static byte ThrowInvalidBytes() => throw new ArgumentException("Invalid digest bytes"); + } + + private static void ThrowDigestLength() => throw new ArgumentException($"Invalid digest length; expected {2 * DigestBytes} bytes"); + + /// + /// Creates an equality match based on the specified digest bytes. + /// + public static ValueCondition ParseDigest(ReadOnlySpan digest) + { + if (digest.Length != 2 * DigestBytes) ThrowDigestLength(); + + // we receive 16 hex characters, as bytes; parse that into a long, by + // first dealing with the nibbles + Span tmp = stackalloc byte[DigestBytes]; + int offset = 0; + for (int i = 0; i < tmp.Length; i++) + { + tmp[i] = (byte)( + (ToNibble(digest[offset++]) << 4) // hi + | ToNibble(digest[offset++])); // lo + } + // now interpret that as big-endian + var digestInt64 = BinaryPrimitives.ReadInt64BigEndian(tmp); + return new ValueCondition(ConditionKind.DigestEquals, digestInt64); + + static byte ToNibble(int b) + { + if (b >= '0' & b <= '9') return (byte)(b - '0'); + if (b >= 'a' & b <= 'f') return (byte)(b - 'a' + 10); + if (b >= 'A' & b <= 'F') return (byte)(b - 'A' + 10); + return ThrowInvalidBytes(); + } + + static byte ThrowInvalidBytes() => throw new ArgumentException("Invalid digest bytes"); + } + + internal int TokenCount => _kind switch + { + ConditionKind.Exists or ConditionKind.NotExists => 1, + ConditionKind.ValueEquals or ConditionKind.ValueNotEquals or ConditionKind.DigestEquals or ConditionKind.DigestNotEquals => 2, + _ => 0, + }; + + internal void WriteTo(PhysicalConnection physical) + { + switch (_kind) + { + case ConditionKind.Exists: + physical.WriteBulkString("XX"u8); + break; + case ConditionKind.NotExists: + physical.WriteBulkString("NX"u8); + break; + case ConditionKind.ValueEquals: + physical.WriteBulkString("IFEQ"u8); + physical.WriteBulkString(_value); + break; + case ConditionKind.ValueNotEquals: + physical.WriteBulkString("IFNE"u8); + physical.WriteBulkString(_value); + break; + case ConditionKind.DigestEquals: + physical.WriteBulkString("IFDEQ"u8); + var written = WriteHex(_value.DirectOverlappedBits64, stackalloc byte[2 * DigestBytes]); + physical.WriteBulkString(written); + break; + case ConditionKind.DigestNotEquals: + physical.WriteBulkString("IFDNE"u8); + written = WriteHex(_value.DirectOverlappedBits64, stackalloc byte[2 * DigestBytes]); + physical.WriteBulkString(written); + break; + } + } + + internal static Span WriteHex(long value, Span target) + { + Debug.Assert(target.Length >= 2 * DigestBytes); + + // iterate over the bytes in big-endian order, writing the hi/lo nibbles, + // using pointer-like behaviour (rather than complex shifts and masks) + if (BitConverter.IsLittleEndian) + { + value = BinaryPrimitives.ReverseEndianness(value); + } + ref byte ptr = ref Unsafe.As(ref value); + int targetOffset = 0; + ReadOnlySpan hex = "0123456789abcdef"u8; + for (int sourceOffset = 0; sourceOffset < sizeof(long); sourceOffset++) + { + byte b = Unsafe.Add(ref ptr, sourceOffset); + target[targetOffset++] = hex[(b >> 4) & 0xF]; // hi nibble + target[targetOffset++] = hex[b & 0xF]; // lo + } + return target.Slice(0, 2 * DigestBytes); + } + + internal static Span WriteHex(long value, Span target) + { + Debug.Assert(target.Length >= 2 * DigestBytes); + + // iterate over the bytes in big-endian order, writing the hi/lo nibbles, + // using pointer-like behaviour (rather than complex shifts and masks) + if (BitConverter.IsLittleEndian) + { + value = BinaryPrimitives.ReverseEndianness(value); + } + ref byte ptr = ref Unsafe.As(ref value); + int targetOffset = 0; + const string hex = "0123456789abcdef"; + for (int sourceOffset = 0; sourceOffset < sizeof(long); sourceOffset++) + { + byte b = Unsafe.Add(ref ptr, sourceOffset); + target[targetOffset++] = hex[(b >> 4) & 0xF]; // hi nibble + target[targetOffset++] = hex[b & 0xF]; // lo + } + return target.Slice(0, 2 * DigestBytes); + } + + /// + /// Negate this condition. The nature of the condition is preserved. + /// + public static ValueCondition operator !(in ValueCondition value) => value._kind switch + { + ConditionKind.ValueEquals => new(ConditionKind.ValueNotEquals, value._value), + ConditionKind.ValueNotEquals => new(ConditionKind.ValueEquals, value._value), + ConditionKind.DigestEquals => new(ConditionKind.DigestNotEquals, value._value), + ConditionKind.DigestNotEquals => new(ConditionKind.DigestEquals, value._value), + ConditionKind.Exists => new(ConditionKind.NotExists, value._value), + ConditionKind.NotExists => new(ConditionKind.Exists, value._value), + // ReSharper disable once ExplicitCallerInfoArgument + _ => value.ThrowInvalidOperation("operator !"), + }; + + /// + /// Convert a to a . + /// + public static implicit operator ValueCondition(When when) => when switch + { + When.Always => Always, + When.Exists => Exists, + When.NotExists => NotExists, + _ => throw new ArgumentOutOfRangeException(nameof(when)), + }; + + /// + /// Convert a value condition to a digest condition. + /// + public ValueCondition AsDigest() => _kind switch + { + ConditionKind.ValueEquals => _value.Digest(), + ConditionKind.ValueNotEquals => !_value.Digest(), + _ => ThrowInvalidOperation(), + }; + + internal ValueCondition ThrowInvalidOperation([CallerMemberName] string? operation = null) + => throw new InvalidOperationException($"{operation} cannot be used with a {_kind} condition."); + + internal When AsWhen() => _kind switch + { + ConditionKind.Always => When.Always, + ConditionKind.Exists => When.Exists, + ConditionKind.NotExists => When.NotExists, + _ => ThrowInvalidOperation().AsWhen(), + }; +} diff --git a/tests/StackExchange.Redis.Tests/DigestIntegrationTests.cs b/tests/StackExchange.Redis.Tests/DigestIntegrationTests.cs new file mode 100644 index 000000000..a71e2f910 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/DigestIntegrationTests.cs @@ -0,0 +1,157 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace StackExchange.Redis.Tests; + +#pragma warning disable SER002 // 8.4 + +public class DigestIntegrationTests(ITestOutputHelper output, SharedConnectionFixture fixture) + : TestBase(output, fixture) +{ + [Fact] + public async Task ReadDigest() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + byte[] blob = new byte[1024]; + new Random().NextBytes(blob); + var local = ValueCondition.CalculateDigest(blob); + Assert.Equal(ValueCondition.ConditionKind.DigestEquals, local.Kind); + Assert.Equal(RedisValue.StorageType.Int64, local.Value.Type); + Log("Local digest: " + local); + + var key = Me(); + var db = conn.GetDatabase(); + await db.KeyDeleteAsync(key, flags: CommandFlags.FireAndForget); + + // test without a value + var digest = await db.StringDigestAsync(key); + Assert.Null(digest); + + // test with a value + await db.StringSetAsync(key, blob, flags: CommandFlags.FireAndForget); + digest = await db.StringDigestAsync(key); + Assert.NotNull(digest); + Assert.Equal(ValueCondition.ConditionKind.DigestEquals, digest.Value.Kind); + Assert.Equal(RedisValue.StorageType.Int64, digest.Value.Value.Type); + Log("Server digest: " + digest); + Assert.Equal(local, digest.Value); + } + + [Theory] + [InlineData(null, (int)ValueCondition.ConditionKind.NotExists)] + [InlineData("new value", (int)ValueCondition.ConditionKind.NotExists)] + [InlineData(null, (int)ValueCondition.ConditionKind.ValueEquals)] + [InlineData(null, (int)ValueCondition.ConditionKind.DigestEquals)] + public async Task InvalidConditionalDelete(string? testValue, int rawKind) + { + await using var conn = Create(); // no server requirement, since fails locally + var key = Me(); + var db = conn.GetDatabase(); + var condition = CreateCondition(testValue, rawKind); + + var ex = await Assert.ThrowsAsync(async () => + { + await db.StringDeleteAsync(key, when: condition); + }); + Assert.StartsWith("StringDeleteAsync cannot be used with a NotExists condition.", ex.Message); + } + + [Theory] + [InlineData(null, null, (int)ValueCondition.ConditionKind.Always)] + [InlineData(null, "new value", (int)ValueCondition.ConditionKind.Always)] + [InlineData("old value", "new value", (int)ValueCondition.ConditionKind.Always, true)] + [InlineData("new value", "new value", (int)ValueCondition.ConditionKind.Always, true)] + + [InlineData(null, null, (int)ValueCondition.ConditionKind.Exists)] + [InlineData(null, "new value", (int)ValueCondition.ConditionKind.Exists)] + [InlineData("old value", "new value", (int)ValueCondition.ConditionKind.Exists, true)] + [InlineData("new value", "new value", (int)ValueCondition.ConditionKind.Exists, true)] + + [InlineData(null, "new value", (int)ValueCondition.ConditionKind.DigestEquals)] + [InlineData("old value", "new value", (int)ValueCondition.ConditionKind.DigestEquals)] + [InlineData("new value", "new value", (int)ValueCondition.ConditionKind.DigestEquals, true)] + + [InlineData(null, "new value", (int)ValueCondition.ConditionKind.ValueEquals)] + [InlineData("old value", "new value", (int)ValueCondition.ConditionKind.ValueEquals)] + [InlineData("new value", "new value", (int)ValueCondition.ConditionKind.ValueEquals, true)] + + [InlineData(null, null, (int)ValueCondition.ConditionKind.DigestNotEquals)] + [InlineData(null, "new value", (int)ValueCondition.ConditionKind.DigestNotEquals)] + [InlineData("old value", "new value", (int)ValueCondition.ConditionKind.DigestNotEquals, true)] + [InlineData("new value", "new value", (int)ValueCondition.ConditionKind.DigestNotEquals)] + + [InlineData(null, null, (int)ValueCondition.ConditionKind.ValueNotEquals)] + [InlineData(null, "new value", (int)ValueCondition.ConditionKind.ValueNotEquals)] + [InlineData("old value", "new value", (int)ValueCondition.ConditionKind.ValueNotEquals, true)] + [InlineData("new value", "new value", (int)ValueCondition.ConditionKind.ValueNotEquals)] + public async Task ConditionalDelete(string? dbValue, string? testValue, int rawKind, bool expectDelete = false) + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var key = Me(); + var db = conn.GetDatabase(); + await db.KeyDeleteAsync(key, flags: CommandFlags.FireAndForget); + if (dbValue != null) await db.StringSetAsync(key, dbValue, flags: CommandFlags.FireAndForget); + + var condition = CreateCondition(testValue, rawKind); + + var pendingDelete = db.StringDeleteAsync(key, when: condition); + var exists = await db.KeyExistsAsync(key); + var deleted = await pendingDelete; + + if (dbValue is null) + { + // didn't exist to be deleted + Assert.False(expectDelete); + Assert.False(exists); + Assert.False(deleted); + } + else + { + Assert.Equal(expectDelete, deleted); + Assert.Equal(!expectDelete, exists); + } + } + + private ValueCondition CreateCondition(string? testValue, int rawKind) + { + var condition = (ValueCondition.ConditionKind)rawKind switch + { + ValueCondition.ConditionKind.Always => ValueCondition.Always, + ValueCondition.ConditionKind.Exists => ValueCondition.Exists, + ValueCondition.ConditionKind.NotExists => ValueCondition.NotExists, + ValueCondition.ConditionKind.ValueEquals => ValueCondition.Equal(testValue), + ValueCondition.ConditionKind.ValueNotEquals => ValueCondition.NotEqual(testValue), + ValueCondition.ConditionKind.DigestEquals => ValueCondition.DigestEqual(testValue), + ValueCondition.ConditionKind.DigestNotEquals => ValueCondition.DigestNotEqual(testValue), + _ => throw new ArgumentOutOfRangeException(nameof(rawKind)), + }; + Log($"Condition: {condition}"); + return condition; + } + + [Fact] + public async Task LeadingZeroFormatting() + { + // Example generated that hashes to 0x00006c38adf31777; see https://github.com/redis/redis/issues/14496 + var localDigest = + ValueCondition.CalculateDigest("v8lf0c11xh8ymlqztfd3eeq16kfn4sspw7fqmnuuq3k3t75em5wdizgcdw7uc26nnf961u2jkfzkjytls2kwlj7626sd"u8); + Log($"local: {localDigest}"); + Assert.Equal("IFDEQ 00006c38adf31777", localDigest.ToString()); + + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var key = Me(); + var db = conn.GetDatabase(); + await db.KeyDeleteAsync(key, flags: CommandFlags.FireAndForget); + await db.StringSetAsync(key, "v8lf0c11xh8ymlqztfd3eeq16kfn4sspw7fqmnuuq3k3t75em5wdizgcdw7uc26nnf961u2jkfzkjytls2kwlj7626sd", flags: CommandFlags.FireAndForget); + var pendingDigest = db.StringDigestAsync(key); + var pendingDeleted = db.StringDeleteAsync(key, when: localDigest); + var existsAfter = await db.KeyExistsAsync(key); + + var serverDigest = await pendingDigest; + Log($"server: {serverDigest}"); + Assert.Equal(localDigest, serverDigest); + Assert.True(await pendingDeleted); + Assert.False(existsAfter); + } +} diff --git a/tests/StackExchange.Redis.Tests/DigestUnitTests.cs b/tests/StackExchange.Redis.Tests/DigestUnitTests.cs new file mode 100644 index 000000000..9f04342d1 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/DigestUnitTests.cs @@ -0,0 +1,188 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO.Hashing; +using System.Text; +using Xunit; + +namespace StackExchange.Redis.Tests; + +#pragma warning disable SER002 // 8.4 + +public class DigestUnitTests(ITestOutputHelper output) : TestBase(output) +{ + [Theory] + [MemberData(nameof(SimpleDigestTestValues))] + public void RedisValue_Digest(string equivalentValue, RedisValue value) + { + // first, use pure XxHash3 to see what we expect + var hashHex = GetXxh3Hex(equivalentValue); + + var digest = value.Digest(); + Assert.Equal(ValueCondition.ConditionKind.DigestEquals, digest.Kind); + + Assert.Equal($"IFDEQ {hashHex}", digest.ToString()); + } + + public static IEnumerable SimpleDigestTestValues() + { + yield return ["Hello World", (RedisValue)"Hello World"]; + yield return ["42", (RedisValue)"42"]; + yield return ["42", (RedisValue)42]; + } + + [Theory] + [InlineData("Hello World", "e34615aade2e6333")] + [InlineData("42", "1217cb28c0ef2191")] + public void ValueCondition_CalculateDigest(string source, string expected) + { + var digest = ValueCondition.CalculateDigest(Encoding.UTF8.GetBytes(source)); + Assert.Equal($"IFDEQ {expected}", digest.ToString()); + } + + [Theory] + [InlineData("e34615aade2e6333")] + [InlineData("1217cb28c0ef2191")] + public void ValueCondition_ParseDigest(string value) + { + // parse from hex chars + var digest = ValueCondition.ParseDigest(value.AsSpan()); + Assert.Equal($"IFDEQ {value}", digest.ToString()); + + // and the same, from hex bytes + digest = ValueCondition.ParseDigest(Encoding.UTF8.GetBytes(value).AsSpan()); + Assert.Equal($"IFDEQ {value}", digest.ToString()); + } + + [Theory] + [InlineData("Hello World", "e34615aade2e6333")] + [InlineData("42", "1217cb28c0ef2191")] + [InlineData("", "2d06800538d394c2")] + [InlineData("a", "e6c632b61e964e1f")] + public void KnownXxh3Values(string source, string expected) + => Assert.Equal(expected, GetXxh3Hex(source)); + + private static string GetXxh3Hex(string source) + { + var len = Encoding.UTF8.GetMaxByteCount(source.Length); + var oversized = ArrayPool.Shared.Rent(len); + #if NET + var bytes = Encoding.UTF8.GetBytes(source, oversized); + #else + int bytes; + unsafe + { + fixed (byte* bPtr = oversized) + { + fixed (char* cPtr = source) + { + bytes = Encoding.UTF8.GetBytes(cPtr, source.Length, bPtr, len); + } + } + } + #endif + var result = GetXxh3Hex(oversized.AsSpan(0, bytes)); + ArrayPool.Shared.Return(oversized); + return result; + } + + private static string GetXxh3Hex(ReadOnlySpan source) + { + byte[] targetBytes = new byte[8]; + XxHash3.Hash(source, targetBytes); + return BitConverter.ToString(targetBytes).Replace("-", string.Empty).ToLowerInvariant(); + } + + [Fact] + public void ValueCondition_Mutations() + { + const string InputValue = + "Meantime we shall express our darker purpose.\nGive me the map there. Know we have divided\nIn three our kingdom; and 'tis our fast intent\nTo shake all cares and business from our age,\nConferring them on younger strengths while we\nUnburthen'd crawl toward death. Our son of Cornwall,\nAnd you, our no less loving son of Albany,\nWe have this hour a constant will to publish\nOur daughters' several dowers, that future strife\nMay be prevented now. The princes, France and Burgundy,\nGreat rivals in our youngest daughter's love,\nLong in our court have made their amorous sojourn,\nAnd here are to be answer'd."; + + var condition = ValueCondition.Equal(InputValue); + Assert.Equal($"IFEQ {InputValue}", condition.ToString()); + Assert.True(condition.IsValueTest); + Assert.False(condition.IsDigestTest); + Assert.False(condition.IsNegated); + Assert.False(condition.IsExistenceTest); + + var negCondition = !condition; + Assert.NotEqual(condition, negCondition); + Assert.Equal($"IFNE {InputValue}", negCondition.ToString()); + Assert.True(negCondition.IsValueTest); + Assert.False(negCondition.IsDigestTest); + Assert.True(negCondition.IsNegated); + Assert.False(negCondition.IsExistenceTest); + + var negNegCondition = !negCondition; + Assert.Equal(condition, negNegCondition); + + var digest = condition.AsDigest(); + Assert.NotEqual(condition, digest); + Assert.Equal($"IFDEQ {GetXxh3Hex(InputValue)}", digest.ToString()); + Assert.False(digest.IsValueTest); + Assert.True(digest.IsDigestTest); + Assert.False(digest.IsNegated); + Assert.False(digest.IsExistenceTest); + + var negDigest = !digest; + Assert.NotEqual(digest, negDigest); + Assert.Equal($"IFDNE {GetXxh3Hex(InputValue)}", negDigest.ToString()); + Assert.False(negDigest.IsValueTest); + Assert.True(negDigest.IsDigestTest); + Assert.True(negDigest.IsNegated); + Assert.False(negDigest.IsExistenceTest); + + var negNegDigest = !negDigest; + Assert.Equal(digest, negNegDigest); + + var @default = default(ValueCondition); + Assert.False(@default.IsValueTest); + Assert.False(@default.IsDigestTest); + Assert.False(@default.IsNegated); + Assert.False(@default.IsExistenceTest); + Assert.Equal("", @default.ToString()); + Assert.Equal(ValueCondition.Always, @default); + + var ex = Assert.Throws(() => !@default); + Assert.Equal("operator ! cannot be used with a Always condition.", ex.Message); + + var exists = ValueCondition.Exists; + Assert.False(exists.IsValueTest); + Assert.False(exists.IsDigestTest); + Assert.False(exists.IsNegated); + Assert.True(exists.IsExistenceTest); + Assert.Equal("XX", exists.ToString()); + + var notExists = ValueCondition.NotExists; + Assert.False(notExists.IsValueTest); + Assert.False(notExists.IsDigestTest); + Assert.True(notExists.IsNegated); + Assert.True(notExists.IsExistenceTest); + Assert.Equal("NX", notExists.ToString()); + + Assert.NotEqual(exists, notExists); + Assert.Equal(exists, !notExists); + Assert.Equal(notExists, !exists); + } + + [Fact] + public void RandomBytes() + { + byte[] buffer = ArrayPool.Shared.Rent(8000); + var rand = new Random(); + + for (int i = 0; i < 100; i++) + { + var len = rand.Next(1, buffer.Length); + var span = buffer.AsSpan(0, len); +#if NET + rand.NextBytes(span); +#else + rand.NextBytes(buffer); +#endif + var digest = ValueCondition.CalculateDigest(span); + Assert.Equal($"IFDEQ {GetXxh3Hex(span)}", digest.ToString()); + } + } +} From 2c3b3e0404c2e68722bc7f76cd5b44eb2b785e41 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 6 Nov 2025 09:57:39 +0000 Subject: [PATCH 390/435] Combine fixups from 8.4 changes (#2979) * limit [SER002] to the digest-related features, to prevent warnings on overload resolution * use `Expiration` (from MSETEX) in new `SET` API (combined with `ValueCondition`) * fix defaults to minimize build-time impact * chmod +x --- docs/ReleaseNotes.md | 1 + src/StackExchange.Redis/Expiration.cs | 2 +- .../Interfaces/IDatabase.cs | 8 ++--- .../Interfaces/IDatabaseAsync.cs | 8 ++--- .../KeyspaceIsolation/KeyPrefixed.cs | 2 +- .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 2 +- .../Message.ValueCondition.cs | 23 +++----------- src/StackExchange.Redis/Message.cs | 2 +- .../PublicAPI/PublicAPI.Shipped.txt | 31 +++++++++++++++++-- .../PublicAPI/PublicAPI.Unshipped.txt | 29 +---------------- .../RedisDatabase.Strings.cs | 6 ++-- src/StackExchange.Redis/RedisDatabase.cs | 18 +++++------ .../StackExchange.Redis.csproj | 1 - src/StackExchange.Redis/ValueCondition.cs | 18 +++++++++-- tests/RedisConfigs/start-all.sh | 0 tests/RedisConfigs/start-basic.sh | 0 .../DigestIntegrationTests.cs | 2 -- .../DigestUnitTests.cs | 2 -- .../ExpiryTokenTests.cs | 20 ++++++------ .../OverloadCompatTests.cs | 4 +-- .../StackExchange.Redis.Tests/StringTests.cs | 4 +-- 21 files changed, 87 insertions(+), 96 deletions(-) mode change 100644 => 100755 tests/RedisConfigs/start-all.sh mode change 100644 => 100755 tests/RedisConfigs/start-basic.sh diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 58de2287b..e5ddbe65d 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -10,6 +10,7 @@ Current package versions: - Support Redis 8.4 CAS/CAD operations (`DIGEST`, and the `IFEQ`, `IFNE`, `IFDEQ`, `IFDNE` modifiers on `SET` / `DEL`) via the new `ValueCondition` abstraction, and use CAS/CAD operations for `Lock*` APIs when possible ([#2978 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2978)) + - **note**: overload resolution for `StringSet[Async]` may be impacted in niche cases, requiring trivial build changes (there are no runtime-breaking changes such as missing methods) - Support `XREADGROUP CLAIM` ([#2972 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2972)) - Support `MSETEX` (Redis 8.4.0) for multi-key operations with expiration ([#2977 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2977)) diff --git a/src/StackExchange.Redis/Expiration.cs b/src/StackExchange.Redis/Expiration.cs index 786cc928c..e04094358 100644 --- a/src/StackExchange.Redis/Expiration.cs +++ b/src/StackExchange.Redis/Expiration.cs @@ -237,7 +237,7 @@ private static void ThrowMode(ExpirationMode mode) => /// public override bool Equals(object? obj) => obj is Expiration other && _valueAndMode == other._valueAndMode; - internal int Tokens => Mode switch + internal int TokenCount => Mode switch { ExpirationMode.Default or ExpirationMode.NotUsed => 0, ExpirationMode.KeepTtl or ExpirationMode.Persist => 1, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 5556990df..3df162682 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -3182,7 +3182,6 @@ IEnumerable SortedSetScan( /// The condition to enforce. /// The flags to use for this operation. /// See . - [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] bool StringDelete(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None); /// @@ -3411,7 +3410,7 @@ IEnumerable SortedSetScan( /// The flags to use for this operation. /// if the string was set, otherwise. /// - bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); + bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, bool keepTtl, When when = When.Always, CommandFlags flags = CommandFlags.None); /// /// Set to hold the string , if it matches the given condition. @@ -3422,9 +3421,8 @@ IEnumerable SortedSetScan( /// The condition to enforce. /// The flags to use for this operation. /// See . - [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] -#pragma warning disable RS0027 - bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None); +#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads + bool StringSet(RedisKey key, RedisValue value, Expiration expiry = default, ValueCondition when = default, CommandFlags flags = CommandFlags.None); #pragma warning restore RS0027 /// diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index b35a685a5..855ea6c8f 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -773,7 +773,6 @@ IAsyncEnumerable SortedSetScanAsync( Task StringDecrementAsync(RedisKey key, long value = 1, CommandFlags flags = CommandFlags.None); /// - [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] Task StringDeleteAsync(RedisKey key, ValueCondition when, CommandFlags flags = CommandFlags.None); /// @@ -840,12 +839,11 @@ IAsyncEnumerable SortedSetScanAsync( Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags); /// - Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None); + Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, bool keepTtl, When when = When.Always, CommandFlags flags = CommandFlags.None); - /// - [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] + /// #pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads - Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None); + Task StringSetAsync(RedisKey key, RedisValue value, Expiration expiry = default, ValueCondition when = default, CommandFlags flags = CommandFlags.None); #pragma warning restore RS0027 /// diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index 9119035a3..fe23b73c1 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -783,7 +783,7 @@ public Task StringIncrementAsync(RedisKey key, long value = 1, CommandFlag public Task StringLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.StringLengthAsync(ToInner(key), flags); - public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) + public Task StringSetAsync(RedisKey key, RedisValue value, Expiration expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) => Inner.StringSetAsync(ToInner(key), value, expiry, when, flags); public Task StringSetAsync(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index 3c7e34aa9..69775c15d 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -765,7 +765,7 @@ public long StringIncrement(RedisKey key, long value = 1, CommandFlags flags = C public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) => Inner.StringLength(ToInner(key), flags); - public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) + public bool StringSet(RedisKey key, RedisValue value, Expiration expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) => Inner.StringSet(ToInner(key), value, expiry, when, flags); public bool StringSet(KeyValuePair[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) => diff --git a/src/StackExchange.Redis/Message.ValueCondition.cs b/src/StackExchange.Redis/Message.ValueCondition.cs index c8b5febc4..53ddc651b 100644 --- a/src/StackExchange.Redis/Message.ValueCondition.cs +++ b/src/StackExchange.Redis/Message.ValueCondition.cs @@ -7,7 +7,7 @@ internal partial class Message public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in ValueCondition when) => new KeyConditionMessage(db, flags, command, key, when); - public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, TimeSpan? expiry, in ValueCondition when) + public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value, Expiration expiry, in ValueCondition when) => new KeyValueExpiryConditionMessage(db, flags, command, key, value, expiry, when); private sealed class KeyConditionMessage( @@ -36,35 +36,22 @@ private sealed class KeyValueExpiryConditionMessage( RedisCommand command, in RedisKey key, in RedisValue value, - TimeSpan? expiry, + Expiration expiry, in ValueCondition when) : CommandKeyBase(db, flags, command, key) { private readonly RedisValue _value = value; private readonly ValueCondition _when = when; - private readonly TimeSpan? _expiry = expiry == TimeSpan.MaxValue ? null : expiry; + private readonly Expiration _expiry = expiry; - public override int ArgCount => 2 + _when.TokenCount + (_expiry is null ? 0 : 2); + public override int ArgCount => 2 + _expiry.TokenCount + _when.TokenCount; protected override void WriteImpl(PhysicalConnection physical) { physical.WriteHeader(Command, ArgCount); physical.Write(Key); physical.WriteBulkString(_value); - if (_expiry.HasValue) - { - var ms = (long)_expiry.GetValueOrDefault().TotalMilliseconds; - if ((ms % 1000) == 0) - { - physical.WriteBulkString("EX"u8); - physical.WriteBulkString(ms / 1000); - } - else - { - physical.WriteBulkString("PX"u8); - physical.WriteBulkString(ms); - } - } + _expiry.WriteTo(physical); _when.WriteTo(physical); } } diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 0eff3ff8d..386d426d8 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -1711,7 +1711,7 @@ public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy) // - MSETNX {key1} {value1} [{key2} {value2}...] // - MSETEX {count} {key1} {value1} [{key2} {value2}...] [standard-expiry-tokens] public override int ArgCount => Command == RedisCommand.MSETEX - ? (1 + (2 * values.Length) + expiry.Tokens + (when is When.Exists or When.NotExists ? 1 : 0)) + ? (1 + (2 * values.Length) + expiry.TokenCount + (when is When.Exists or When.NotExists ? 1 : 0)) : (2 * values.Length); // MSET/MSETNX only support simple syntax protected override void WriteImpl(PhysicalConnection physical) diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index dbb710243..5eaa42b3f 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -776,7 +776,6 @@ StackExchange.Redis.IDatabase.StringLength(StackExchange.Redis.RedisKey key, Sta StackExchange.Redis.IDatabase.StringLongestCommonSubsequence(StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> string? StackExchange.Redis.IDatabase.StringLongestCommonSubsequenceLength(StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long StackExchange.Redis.IDatabase.StringLongestCommonSubsequenceWithMatches(StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, long minLength = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.LCSMatchResult -StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when) -> bool StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> bool StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> bool @@ -1023,7 +1022,6 @@ StackExchange.Redis.IDatabaseAsync.StringLongestCommonSubsequenceLengthAsync(Sta StackExchange.Redis.IDatabaseAsync.StringLongestCommonSubsequenceWithMatchesAsync(StackExchange.Redis.RedisKey first, StackExchange.Redis.RedisKey second, long minLength = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! -StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! StackExchange.Redis.IDatabaseAsync.StringSetAsync(System.Collections.Generic.KeyValuePair[]! values, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task! @@ -2073,3 +2071,32 @@ static StackExchange.Redis.Expiration.KeepTtl.get -> StackExchange.Redis.Expirat static StackExchange.Redis.Expiration.Persist.get -> StackExchange.Redis.Expiration static StackExchange.Redis.Expiration.implicit operator StackExchange.Redis.Expiration(System.DateTime when) -> StackExchange.Redis.Expiration static StackExchange.Redis.Expiration.implicit operator StackExchange.Redis.Expiration(System.TimeSpan ttl) -> StackExchange.Redis.Expiration +override StackExchange.Redis.ValueCondition.Equals(object? obj) -> bool +override StackExchange.Redis.ValueCondition.GetHashCode() -> int +override StackExchange.Redis.ValueCondition.ToString() -> string! +StackExchange.Redis.RedisFeatures.DeleteWithValueCheck.get -> bool +StackExchange.Redis.RedisFeatures.SetWithValueCheck.get -> bool +StackExchange.Redis.ValueCondition +StackExchange.Redis.ValueCondition.ValueCondition() -> void +static StackExchange.Redis.ValueCondition.Always.get -> StackExchange.Redis.ValueCondition +static StackExchange.Redis.ValueCondition.Exists.get -> StackExchange.Redis.ValueCondition +static StackExchange.Redis.ValueCondition.implicit operator StackExchange.Redis.ValueCondition(StackExchange.Redis.When when) -> StackExchange.Redis.ValueCondition +static StackExchange.Redis.ValueCondition.NotExists.get -> StackExchange.Redis.ValueCondition +static StackExchange.Redis.ValueCondition.operator !(in StackExchange.Redis.ValueCondition value) -> StackExchange.Redis.ValueCondition +StackExchange.Redis.IDatabase.StringDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabaseAsync.StringDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.Expiration expiry = default(StackExchange.Redis.Expiration), StackExchange.Redis.ValueCondition when = default(StackExchange.Redis.ValueCondition), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, bool keepTtl, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.Expiration expiry = default(StackExchange.Redis.Expiration), StackExchange.Redis.ValueCondition when = default(StackExchange.Redis.ValueCondition), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, bool keepTtl, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER002]StackExchange.Redis.IDatabase.StringDigest(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ValueCondition? +[SER002]StackExchange.Redis.IDatabaseAsync.StringDigestAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER002]StackExchange.Redis.ValueCondition.AsDigest() -> StackExchange.Redis.ValueCondition +[SER002]StackExchange.Redis.ValueCondition.Value.get -> StackExchange.Redis.RedisValue +[SER002]static StackExchange.Redis.ValueCondition.CalculateDigest(System.ReadOnlySpan value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.DigestEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.DigestNotEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.Equal(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.NotEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.ParseDigest(System.ReadOnlySpan digest) -> StackExchange.Redis.ValueCondition +[SER002]static StackExchange.Redis.ValueCondition.ParseDigest(System.ReadOnlySpan digest) -> StackExchange.Redis.ValueCondition diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 1b8aba3b0..91b0e1a43 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,28 +1 @@ -#nullable enable -StackExchange.Redis.RedisFeatures.DeleteWithValueCheck.get -> bool -StackExchange.Redis.RedisFeatures.SetWithValueCheck.get -> bool -[SER002]override StackExchange.Redis.ValueCondition.Equals(object? obj) -> bool -[SER002]override StackExchange.Redis.ValueCondition.GetHashCode() -> int -[SER002]override StackExchange.Redis.ValueCondition.ToString() -> string! -[SER002]StackExchange.Redis.IDatabase.StringDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -[SER002]StackExchange.Redis.IDatabase.StringDigest(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ValueCondition? -[SER002]StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -[SER002]StackExchange.Redis.IDatabaseAsync.StringDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER002]StackExchange.Redis.IDatabaseAsync.StringDigestAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER002]StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.ValueCondition when, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER002]StackExchange.Redis.ValueCondition -[SER002]StackExchange.Redis.ValueCondition.AsDigest() -> StackExchange.Redis.ValueCondition -[SER002]StackExchange.Redis.ValueCondition.Value.get -> StackExchange.Redis.RedisValue -[SER002]StackExchange.Redis.ValueCondition.ValueCondition() -> void -[SER002]static StackExchange.Redis.ValueCondition.Always.get -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.CalculateDigest(System.ReadOnlySpan value) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.DigestEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.DigestNotEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.Equal(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.Exists.get -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.implicit operator StackExchange.Redis.ValueCondition(StackExchange.Redis.When when) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.NotEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.NotExists.get -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.operator !(in StackExchange.Redis.ValueCondition value) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.ParseDigest(System.ReadOnlySpan digest) -> StackExchange.Redis.ValueCondition -[SER002]static StackExchange.Redis.ValueCondition.ParseDigest(System.ReadOnlySpan digest) -> StackExchange.Redis.ValueCondition +#nullable enable \ No newline at end of file diff --git a/src/StackExchange.Redis/RedisDatabase.Strings.cs b/src/StackExchange.Redis/RedisDatabase.Strings.cs index 1323246f9..6fcb7dd3b 100644 --- a/src/StackExchange.Redis/RedisDatabase.Strings.cs +++ b/src/StackExchange.Redis/RedisDatabase.Strings.cs @@ -48,19 +48,19 @@ private Message GetStringDeleteMessage(in RedisKey key, in ValueCondition when, return ExecuteAsync(msg, ResultProcessor.Digest); } - public Task StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) + public Task StringSetAsync(RedisKey key, RedisValue value, Expiration expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) { var msg = GetStringSetMessage(key, value, expiry, when, flags); return ExecuteAsync(msg, ResultProcessor.Boolean); } - public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) + public bool StringSet(RedisKey key, RedisValue value, Expiration expiry, ValueCondition when, CommandFlags flags = CommandFlags.None) { var msg = GetStringSetMessage(key, value, expiry, when, flags); return ExecuteSync(msg, ResultProcessor.Boolean); } - private Message GetStringSetMessage(in RedisKey key, in RedisValue value, TimeSpan? expiry, in ValueCondition when, CommandFlags flags, [CallerMemberName] string? operation = null) + private Message GetStringSetMessage(in RedisKey key, in RedisValue value, Expiration expiry, in ValueCondition when, CommandFlags flags, [CallerMemberName] string? operation = null) { switch (when.Kind) { diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 948a9e894..f13571c4e 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -489,7 +489,7 @@ public Task HashFieldGetAndDeleteAsync(RedisKey key, RedisValue[] } private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, in RedisValue hashField, Expiration expiry, CommandFlags flags) => - expiry.Tokens switch + expiry.TokenCount switch { // expiry, for example EX 10 2 => Message.Create(Database, flags, RedisCommand.HGETEX, key, expiry.Operand, expiry.Value, RedisLiterals.FIELDS, 1, hashField), @@ -508,13 +508,13 @@ private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, RedisValue[] ha } // precision, time, FIELDS, hashFields.Length - int extraTokens = expiry.Tokens + 2; + int extraTokens = expiry.TokenCount + 2; - RedisValue[] values = new RedisValue[expiry.Tokens + 2 + hashFields.Length]; + RedisValue[] values = new RedisValue[expiry.TokenCount + 2 + hashFields.Length]; int index = 0; // add PERSIST or expiry values - switch (expiry.Tokens) + switch (expiry.TokenCount) { case 2: values[index++] = expiry.Operand; @@ -620,7 +620,7 @@ private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, in RedisValue f { if (when == When.Always) { - return expiry.Tokens switch + return expiry.TokenCount switch { 2 => Message.Create(Database, flags, RedisCommand.HSETEX, key, expiry.Operand, expiry.Value, RedisLiterals.FIELDS, 1, field, value), 1 => Message.Create(Database, flags, RedisCommand.HSETEX, key, expiry.Operand, RedisLiterals.FIELDS, 1, field, value), @@ -637,7 +637,7 @@ private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, in RedisValue f _ => throw new ArgumentOutOfRangeException(nameof(when)), }; - return expiry.Tokens switch + return expiry.TokenCount switch { 2 => Message.Create(Database, flags, RedisCommand.HSETEX, key, existance, expiry.Operand, expiry.Value, RedisLiterals.FIELDS, 1, field, value), 1 => Message.Create(Database, flags, RedisCommand.HSETEX, key, existance, expiry.Operand, RedisLiterals.FIELDS, 1, field, value), @@ -654,7 +654,7 @@ private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, HashEntry[] has return HashFieldSetAndSetExpiryMessage(key, field.Name, field.Value, expiry, when, flags); } // Determine the base array size - var extraTokens = expiry.Tokens + (when == When.Always ? 2 : 3); // [FXX|FNX] {expiry} FIELDS {length} + var extraTokens = expiry.TokenCount + (when == When.Always ? 2 : 3); // [FXX|FNX] {expiry} FIELDS {length} RedisValue[] values = new RedisValue[(hashFields.Length * 2) + extraTokens]; int index = 0; @@ -671,7 +671,7 @@ private Message HashFieldSetAndSetExpiryMessage(in RedisKey key, HashEntry[] has default: throw new ArgumentOutOfRangeException(nameof(when)); } - switch (expiry.Tokens) + switch (expiry.TokenCount) { case 2: values[index++] = expiry.Operand; @@ -5088,7 +5088,7 @@ private Message GetStringBitOperationMessage(Bitwise operation, RedisKey destina private Message GetStringGetExMessage(in RedisKey key, Expiration expiry, CommandFlags flags = CommandFlags.None) { - return expiry.Tokens switch + return expiry.TokenCount switch { 0 => Message.Create(Database, flags, RedisCommand.GETEX, key), 1 => Message.Create(Database, flags, RedisCommand.GETEX, key, expiry.Operand), diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index e66b3874c..983624bc0 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -12,7 +12,6 @@ $(DefineConstants);VECTOR_SAFE $(DefineConstants);UNIX_SOCKET README.md - $(NoWarn);SER002 diff --git a/src/StackExchange.Redis/ValueCondition.cs b/src/StackExchange.Redis/ValueCondition.cs index 94e9850c4..d61a2f00e 100644 --- a/src/StackExchange.Redis/ValueCondition.cs +++ b/src/StackExchange.Redis/ValueCondition.cs @@ -8,9 +8,8 @@ namespace StackExchange.Redis; /// -/// Represents a check for an existing value, for use in conditional operations such as DELEX or SET ... IFEQ. +/// Represents a check for an existing value - this could be existence (NX/XX), equality (IFEQ/IFNE), or digest equality (IFDEQ/IFDNE). /// -[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public readonly struct ValueCondition { internal enum ConditionKind : byte @@ -108,7 +107,11 @@ public override string ToString() /// /// Gets the underlying value for this condition. /// - public RedisValue Value => _value; + public RedisValue Value + { + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] + get => _value; + } private ValueCondition(ConditionKind kind, in RedisValue value) { @@ -136,26 +139,32 @@ private ValueCondition(ConditionKind kind, in RedisValue value) /// /// Create a value equality condition with the supplied value. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public static ValueCondition Equal(in RedisValue value) => new(ConditionKind.ValueEquals, value); /// /// Create a value non-equality condition with the supplied value. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] + public static ValueCondition NotEqual(in RedisValue value) => new(ConditionKind.ValueNotEquals, value); /// /// Create a digest equality condition, computing the digest of the supplied value. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public static ValueCondition DigestEqual(in RedisValue value) => value.Digest(); /// /// Create a digest non-equality condition, computing the digest of the supplied value. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public static ValueCondition DigestNotEqual(in RedisValue value) => !value.Digest(); /// /// Calculate the digest of a payload, as an equality test. For a non-equality test, use on the result. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public static ValueCondition CalculateDigest(ReadOnlySpan value) { // the internal impl of XxHash3 uses ulong (not Span), so: use @@ -167,6 +176,7 @@ public static ValueCondition CalculateDigest(ReadOnlySpan value) /// /// Creates an equality match based on the specified digest bytes. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public static ValueCondition ParseDigest(ReadOnlySpan digest) { if (digest.Length != 2 * DigestBytes) ThrowDigestLength(); @@ -201,6 +211,7 @@ private static byte ParseNibble(int b) /// /// Creates an equality match based on the specified digest bytes. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public static ValueCondition ParseDigest(ReadOnlySpan digest) { if (digest.Length != 2 * DigestBytes) ThrowDigestLength(); @@ -341,6 +352,7 @@ internal static Span WriteHex(long value, Span target) /// /// Convert a value condition to a digest condition. /// + [Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)] public ValueCondition AsDigest() => _kind switch { ConditionKind.ValueEquals => _value.Digest(), diff --git a/tests/RedisConfigs/start-all.sh b/tests/RedisConfigs/start-all.sh old mode 100644 new mode 100755 diff --git a/tests/RedisConfigs/start-basic.sh b/tests/RedisConfigs/start-basic.sh old mode 100644 new mode 100755 diff --git a/tests/StackExchange.Redis.Tests/DigestIntegrationTests.cs b/tests/StackExchange.Redis.Tests/DigestIntegrationTests.cs index a71e2f910..ec5171075 100644 --- a/tests/StackExchange.Redis.Tests/DigestIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/DigestIntegrationTests.cs @@ -4,8 +4,6 @@ namespace StackExchange.Redis.Tests; -#pragma warning disable SER002 // 8.4 - public class DigestIntegrationTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) { diff --git a/tests/StackExchange.Redis.Tests/DigestUnitTests.cs b/tests/StackExchange.Redis.Tests/DigestUnitTests.cs index 9f04342d1..e1883c13b 100644 --- a/tests/StackExchange.Redis.Tests/DigestUnitTests.cs +++ b/tests/StackExchange.Redis.Tests/DigestUnitTests.cs @@ -7,8 +7,6 @@ namespace StackExchange.Redis.Tests; -#pragma warning disable SER002 // 8.4 - public class DigestUnitTests(ITestOutputHelper output) : TestBase(output) { [Theory] diff --git a/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs b/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs index fea4d4885..6012422ed 100644 --- a/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs +++ b/tests/StackExchange.Redis.Tests/ExpiryTokenTests.cs @@ -10,7 +10,7 @@ public void Persist_Seconds() { TimeSpan? time = TimeSpan.FromMilliseconds(5000); var ex = CreateOrPersist(time, false); - Assert.Equal(2, ex.Tokens); + Assert.Equal(2, ex.TokenCount); Assert.Equal("EX 5", ex.ToString()); } @@ -19,7 +19,7 @@ public void Persist_Milliseconds() { TimeSpan? time = TimeSpan.FromMilliseconds(5001); var ex = CreateOrPersist(time, false); - Assert.Equal(2, ex.Tokens); + Assert.Equal(2, ex.TokenCount); Assert.Equal("PX 5001", ex.ToString()); } @@ -28,7 +28,7 @@ public void Persist_None_False() { TimeSpan? time = null; var ex = CreateOrPersist(time, false); - Assert.Equal(0, ex.Tokens); + Assert.Equal(0, ex.TokenCount); Assert.Equal("", ex.ToString()); } @@ -37,7 +37,7 @@ public void Persist_None_True() { TimeSpan? time = null; var ex = CreateOrPersist(time, true); - Assert.Equal(1, ex.Tokens); + Assert.Equal(1, ex.TokenCount); Assert.Equal("PERSIST", ex.ToString()); } @@ -55,7 +55,7 @@ public void KeepTtl_Seconds() { TimeSpan? time = TimeSpan.FromMilliseconds(5000); var ex = CreateOrKeepTtl(time, false); - Assert.Equal(2, ex.Tokens); + Assert.Equal(2, ex.TokenCount); Assert.Equal("EX 5", ex.ToString()); } @@ -64,7 +64,7 @@ public void KeepTtl_Milliseconds() { TimeSpan? time = TimeSpan.FromMilliseconds(5001); var ex = CreateOrKeepTtl(time, false); - Assert.Equal(2, ex.Tokens); + Assert.Equal(2, ex.TokenCount); Assert.Equal("PX 5001", ex.ToString()); } @@ -73,7 +73,7 @@ public void KeepTtl_None_False() { TimeSpan? time = null; var ex = CreateOrKeepTtl(time, false); - Assert.Equal(0, ex.Tokens); + Assert.Equal(0, ex.TokenCount); Assert.Equal("", ex.ToString()); } @@ -82,7 +82,7 @@ public void KeepTtl_None_True() { TimeSpan? time = null; var ex = CreateOrKeepTtl(time, true); - Assert.Equal(1, ex.Tokens); + Assert.Equal(1, ex.TokenCount); Assert.Equal("KEEPTTL", ex.ToString()); } @@ -100,7 +100,7 @@ public void DateTime_Seconds() { var when = new DateTime(2025, 7, 23, 10, 4, 14, DateTimeKind.Utc); var ex = new Expiration(when); - Assert.Equal(2, ex.Tokens); + Assert.Equal(2, ex.TokenCount); Assert.Equal("EXAT 1753265054", ex.ToString()); } @@ -110,7 +110,7 @@ public void DateTime_Milliseconds() var when = new DateTime(2025, 7, 23, 10, 4, 14, DateTimeKind.Utc); when = when.AddMilliseconds(14); var ex = new Expiration(when); - Assert.Equal(2, ex.Tokens); + Assert.Equal(2, ex.TokenCount); Assert.Equal("PXAT 1753265054014", ex.ToString()); } } diff --git a/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs b/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs index c695488f2..0acadc74b 100644 --- a/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs +++ b/tests/StackExchange.Redis.Tests/OverloadCompatTests.cs @@ -226,7 +226,7 @@ public async Task StringSet() db.StringSet(key, val, expiresIn, When.NotExists); db.StringSet(key, val, expiresIn, When.NotExists, flags); - db.StringSet(key, val, null); + db.StringSet(key, val, expiry: default); db.StringSet(key, val, null, When.NotExists); db.StringSet(key, val, null, When.NotExists, flags); @@ -241,7 +241,7 @@ public async Task StringSet() await db.StringSetAsync(key, val, expiresIn, When.NotExists); await db.StringSetAsync(key, val, expiresIn, When.NotExists, flags); - await db.StringSetAsync(key, val, null); + await db.StringSetAsync(key, val, expiry: default); await db.StringSetAsync(key, val, null, When.NotExists); await db.StringSetAsync(key, val, null, When.NotExists, flags); } diff --git a/tests/StackExchange.Redis.Tests/StringTests.cs b/tests/StackExchange.Redis.Tests/StringTests.cs index 85bcc7dd5..1ade532d7 100644 --- a/tests/StackExchange.Redis.Tests/StringTests.cs +++ b/tests/StackExchange.Redis.Tests/StringTests.cs @@ -301,9 +301,9 @@ public async Task SetKeepTtl() Assert.True(await x2 > TimeSpan.FromMinutes(9), "Over 9"); Assert.True(await x2 <= TimeSpan.FromMinutes(10), "Under 10"); - db.StringSet(prefix + "1", "def", keepTtl: true, flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "1", "def", Expiration.KeepTtl, flags: CommandFlags.FireAndForget); db.StringSet(prefix + "2", "def", flags: CommandFlags.FireAndForget); - db.StringSet(prefix + "3", "def", keepTtl: true, flags: CommandFlags.FireAndForget); + db.StringSet(prefix + "3", "def", Expiration.KeepTtl, flags: CommandFlags.FireAndForget); var y0 = db.KeyTimeToLiveAsync(prefix + "1"); var y1 = db.KeyTimeToLiveAsync(prefix + "2"); From fe4d5f68184e2d73d77f05ffec1af4297b0ba9a5 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 6 Nov 2025 11:16:17 +0000 Subject: [PATCH 391/435] rev 2.10 --- docs/ReleaseNotes.md | 2 +- version.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index e5ddbe65d..7f6d1b77a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,7 +6,7 @@ Current package versions: | ------------ | ----------------- | ----- | | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | -## Unreleased +## 2.10 - Support Redis 8.4 CAS/CAD operations (`DIGEST`, and the `IFEQ`, `IFNE`, `IFDEQ`, `IFDNE` modifiers on `SET` / `DEL`) via the new `ValueCondition` abstraction, and use CAS/CAD operations for `Lock*` APIs when possible ([#2978 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2978)) diff --git a/version.json b/version.json index 63f4a5346..ff21ff51a 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { - "version": "2.9", - "versionHeightOffset": -1, + "version": "2.10", + "versionHeightOffset": 0, "assemblyVersion": "2.0", "publicReleaseRefSpec": [ "^refs/heads/main$", From 3167fdeb21f5c9e98105e5989507055354c20a9a Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 6 Nov 2025 12:11:57 +0000 Subject: [PATCH 392/435] 2.10.1 --- docs/ReleaseNotes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 7f6d1b77a..0ad00222b 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,7 +6,7 @@ Current package versions: | ------------ | ----------------- | ----- | | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | -## 2.10 +## 2.10.1 - Support Redis 8.4 CAS/CAD operations (`DIGEST`, and the `IFEQ`, `IFNE`, `IFDEQ`, `IFDNE` modifiers on `SET` / `DEL`) via the new `ValueCondition` abstraction, and use CAS/CAD operations for `Lock*` APIs when possible ([#2978 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2978)) From 24d8c1e8328c8101adad1aac45d6d5865546ff41 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 11 Nov 2025 10:49:03 +0000 Subject: [PATCH 393/435] fix docs link --- docs/exp/SER002.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/exp/SER002.md b/docs/exp/SER002.md index 6e5100a6e..d122038e2 100644 --- a/docs/exp/SER002.md +++ b/docs/exp/SER002.md @@ -4,7 +4,7 @@ New features in Redis 8.4 include: - [`MSETEX`](https://github.com/redis/redis/pull/14434) for setting multiple strings with expiry - [`XREADGROUP ... CLAIM`](https://github.com/redis/redis/pull/14402) for simplifed stream consumption -- [`SET ... {IFEQ|IFNE|IFDEQ|IFDNE}`, `DELEX` and `DIGEST`](https://github.com/redis/redis/pull/14434) for checked (CAS/CAD) string operations +- [`SET ... {IFEQ|IFNE|IFDEQ|IFDNE}`, `DELEX` and `DIGEST`](https://github.com/redis/redis/pull/14435) for checked (CAS/CAD) string operations The corresponding library feature must also be considered subject to change: @@ -23,4 +23,4 @@ or more granularly / locally in C#: ``` c# #pragma warning disable SER002 -``` \ No newline at end of file +``` From feb122ca7f933f0dedecf67fd35d9b205dd50789 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 18 Nov 2025 09:42:24 +0000 Subject: [PATCH 394/435] Update Dockerfile to 8.4 GA pre 3 (#2981) --- tests/RedisConfigs/.docker/Redis/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/RedisConfigs/.docker/Redis/Dockerfile b/tests/RedisConfigs/.docker/Redis/Dockerfile index 28bf78ebe..b26ab5d76 100644 --- a/tests/RedisConfigs/.docker/Redis/Dockerfile +++ b/tests/RedisConfigs/.docker/Redis/Dockerfile @@ -1,4 +1,4 @@ -FROM redislabs/client-libs-test:8.4-RC1-pre.2 +FROM redislabs/client-libs-test:8.4-GA-pre.3 COPY --from=configs ./Basic /data/Basic/ COPY --from=configs ./Failover /data/Failover/ From d80bcbff2c1d7c1a234232b6f9d6c5fa0cd326c2 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 23 Jan 2026 19:16:43 +0000 Subject: [PATCH 395/435] fix incorrect debug assertion in HGETEX; no impact to release build (#2999) * fix incorrect debug assertion in HGETEX; no impact to release build * add release note --- docs/ReleaseNotes.md | 4 ++++ src/StackExchange.Redis/RedisDatabase.cs | 7 +++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 0ad00222b..591d1ca25 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,6 +6,10 @@ Current package versions: | ------------ | ----------------- | ----- | | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | +## unreleased + +- Fix incorrect debug assertion in `HGETEX` (no impact to release library) ([#2999 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2999)) + ## 2.10.1 - Support Redis 8.4 CAS/CAD operations (`DIGEST`, and the `IFEQ`, `IFNE`, `IFDEQ`, `IFDNE` modifiers on `SET` / `DEL`) diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index f13571c4e..c1c3c5728 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -507,10 +507,9 @@ private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, RedisValue[] ha return HashFieldGetAndSetExpiryMessage(key, in hashFields[0], expiry, flags); } - // precision, time, FIELDS, hashFields.Length + // precision, time, FIELDS, hashFields.Length, {N x fields} int extraTokens = expiry.TokenCount + 2; - - RedisValue[] values = new RedisValue[expiry.TokenCount + 2 + hashFields.Length]; + RedisValue[] values = new RedisValue[extraTokens + hashFields.Length]; int index = 0; // add PERSIST or expiry values @@ -528,7 +527,7 @@ private Message HashFieldGetAndSetExpiryMessage(in RedisKey key, RedisValue[] ha values[index++] = RedisLiterals.FIELDS; values[index++] = hashFields.Length; // check we've added everything we expected to - Debug.Assert(index == extraTokens + hashFields.Length); + Debug.Assert(index == extraTokens, $"token mismatch: {index} vs {extraTokens}"); // Add hash fields to the array hashFields.AsSpan().CopyTo(values.AsSpan(index)); From 84b015e4196875444349b894b5ce51f50d8d33d0 Mon Sep 17 00:00:00 2001 From: Nathan <35500370+nathan-miller23@users.noreply.github.com> Date: Tue, 27 Jan 2026 03:29:33 -0800 Subject: [PATCH 396/435] Propagate PhysicalBridge backlog to ServerCounters (#2996) * propagate PhysicalBridge backlog to ServerCounters * style fix --------- Co-authored-by: Nathan Miller --- src/StackExchange.Redis/PhysicalBridge.cs | 1 + .../StackExchange.Redis.Tests/BacklogTests.cs | 72 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 1a38b7d89..0d839f4c7 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -287,6 +287,7 @@ internal void GetCounters(ConnectionCounters counters) counters.SocketCount = Interlocked.Read(ref socketCount); counters.WriterCount = Interlocked.CompareExchange(ref activeWriters, 0, 0); counters.NonPreferredEndpointCount = Interlocked.Read(ref nonPreferredEndpointCount); + counters.PendingUnsentItems = Volatile.Read(ref _backlogCurrentEnqueued); physical?.GetCounters(counters); } diff --git a/tests/StackExchange.Redis.Tests/BacklogTests.cs b/tests/StackExchange.Redis.Tests/BacklogTests.cs index f0c0d3d0c..e8ed1daf0 100644 --- a/tests/StackExchange.Redis.Tests/BacklogTests.cs +++ b/tests/StackExchange.Redis.Tests/BacklogTests.cs @@ -398,4 +398,76 @@ static Task PingAsync(ServerEndPoint server, CommandFlags flags = Comm ClearAmbientFailures(); } } + + [Fact] + public async Task TotalOutstandingIncludesBacklogQueue() + { + try + { + var options = new ConfigurationOptions() + { + BacklogPolicy = BacklogPolicy.Default, + AbortOnConnectFail = false, + ConnectTimeout = 1000, + ConnectRetry = 2, + SyncTimeout = 10000, + KeepAlive = 10000, + AsyncTimeout = 5000, + AllowAdmin = true, + SocketManager = SocketManager.ThreadPool, + }; + options.EndPoints.Add(TestConfig.Current.PrimaryServerAndPort); + + using var conn = await ConnectionMultiplexer.ConnectAsync(options, Writer); + var db = conn.GetDatabase(); + Log("Test: Initial (connected) ping"); + await db.PingAsync(); + + var server = conn.GetServerSnapshot()[0]; + + // Verify TotalOutstanding is 0 when connected and idle + Log("Test: asserting connected counters"); + var connectedServerCounters = server.GetCounters(); + var connectedConnCounters = conn.GetCounters(); + Assert.Equal(0, connectedServerCounters.Interactive.TotalOutstanding); + Assert.Equal(0, connectedConnCounters.TotalOutstanding); + + Log("Test: Simulating failure"); + conn.AllowConnect = false; + server.SimulateConnectionFailure(SimulatedFailureType.All); + + // Queue up some commands + Log("Test: Disconnected pings"); + _ = db.PingAsync(); + _ = db.PingAsync(); + var lastPing = db.PingAsync(); + + Log("Test: asserting disconnected counters"); + var disconnectedServerCounters = server.GetCounters(); + var disconnectedConnCounters = conn.GetCounters(); + Assert.True(disconnectedServerCounters.Interactive.PendingUnsentItems >= 3, $"Expected PendingUnsentItems >= 3, got {disconnectedServerCounters.Interactive.PendingUnsentItems}"); + Assert.True(disconnectedConnCounters.TotalOutstanding >= 3, $"Expected TotalOutstanding >= 3, got {disconnectedServerCounters.Interactive.TotalOutstanding}"); + + Log("Test: Awaiting reconnect"); + conn.AllowConnect = true; + await UntilConditionAsync(TimeSpan.FromSeconds(3), () => conn.IsConnected).ForAwait(); + + Log("Test: Awaiting lastPing"); + await lastPing; + + Log("Test: Checking reconnected"); + Assert.True(conn.IsConnected); + + Log("Test: asserting reconnected counters"); + var reconnectedServerCounters = server.GetCounters(); + var reconnectedConnCounters = conn.GetCounters(); + Assert.Equal(0, reconnectedServerCounters.Interactive.PendingUnsentItems); + Assert.Equal(0, reconnectedConnCounters.TotalOutstanding); + Log("Test: Done"); + } + finally + { + ClearAmbientFailures(); + } + } } From c8e3152efaa9414a1bcb62e52019cf941d675a59 Mon Sep 17 00:00:00 2001 From: Nathan <35500370+nathan-miller23@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:06:41 -0800 Subject: [PATCH 397/435] handle backlog process startup failures (#3002) Co-authored-by: Nathan Miller --- src/StackExchange.Redis/PhysicalBridge.cs | 46 ++++++++++++++++------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 0d839f4c7..b380c203e 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -934,20 +934,40 @@ private void StartBacklogProcessor() { if (Interlocked.CompareExchange(ref _backlogProcessorIsRunning, 1, 0) == 0) { - _backlogStatus = BacklogStatus.Activating; - - // Start the backlog processor; this is a bit unorthodox, as you would *expect* this to just - // be Task.Run; that would work fine when healthy, but when we're falling on our face, it is - // easy to get into a thread-pool-starvation "spiral of death" if we rely on the thread-pool - // to unblock the thread-pool when there could be sync-over-async callers. Note that in reality, - // the initial "enough" of the back-log processor is typically sync, which means that the thread - // we start is actually useful, despite thinking "but that will just go async and back to the pool" - var thread = new Thread(s => ((PhysicalBridge)s!).ProcessBacklog()) + var successfullyStarted = false; + try + { + _backlogStatus = BacklogStatus.Activating; + + // Start the backlog processor; this is a bit unorthodox, as you would *expect* this to just + // be Task.Run; that would work fine when healthy, but when we're falling on our face, it is + // easy to get into a thread-pool-starvation "spiral of death" if we rely on the thread-pool + // to unblock the thread-pool when there could be sync-over-async callers. Note that in reality, + // the initial "enough" of the back-log processor is typically sync, which means that the thread + // we start is actually useful, despite thinking "but that will just go async and back to the pool" + var thread = new Thread(s => ((PhysicalBridge)s!).ProcessBacklog()) + { + IsBackground = true, // don't keep process alive (also: act like the thread-pool used to) + Name = "StackExchange.Redis Backlog", // help anyone looking at thread-dumps + }; + + thread.Start(this); + successfullyStarted = true; + } + catch (Exception ex) { - IsBackground = true, // don't keep process alive (also: act like the thread-pool used to) - Name = "StackExchange.Redis Backlog", // help anyone looking at thread-dumps - }; - thread.Start(this); + OnInternalError(ex); + Trace("StartBacklogProcessor failed to start backlog processor thread: " + ex.Message); + } + finally + { + // If thread failed to start - reset flag to ensure next call doesn't erroneously think backlog process is running + if (!successfullyStarted) + { + _backlogStatus = BacklogStatus.Inactive; + Interlocked.Exchange(ref _backlogProcessorIsRunning, 0); + } + } } else { From f92f9f4c49903fae803aeb94a41840837e66465e Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 3 Feb 2026 16:15:56 +0000 Subject: [PATCH 398/435] release notes --- docs/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 591d1ca25..817213978 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,8 +8,11 @@ Current package versions: ## unreleased +- Fix bug with connection startup failing in low-memory scenarios ([#3002 by nathan-miller23](https://github.com/StackExchange/StackExchange.Redis/pull/3002)) +- Fix under-count of `TotalOutstanding` in server-counters ([#2996 by nathan-miller23](https://github.com/StackExchange/StackExchange.Redis/pull/2996)) - Fix incorrect debug assertion in `HGETEX` (no impact to release library) ([#2999 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2999)) + ## 2.10.1 - Support Redis 8.4 CAS/CAD operations (`DIGEST`, and the `IFEQ`, `IFNE`, `IFDEQ`, `IFDNE` modifiers on `SET` / `DEL`) From 56957709e47b116a55124b4c40239dbbbe1bac90 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 3 Feb 2026 16:20:28 +0000 Subject: [PATCH 399/435] trigger CI From 2da69754994f8311c47882a8fc11689d0e2ac568 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 3 Feb 2026 16:40:37 +0000 Subject: [PATCH 400/435] Migrate to codeql 4 (#3004) See https://github.blog/changelog/2025-10-28-upcoming-deprecation-of-codeql-action-v3/ --- .github/workflows/codeql.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 53f5701f2..2046fe0d3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -22,6 +22,7 @@ jobs: fail-fast: false matrix: language: [ 'csharp' ] + build-mode: manual # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Use only 'java' to analyze code written in Java, Kotlin or both # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both @@ -40,7 +41,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -55,6 +56,6 @@ jobs: run: dotnet build Build.csproj -c Release /p:CI=true - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: - category: "/language:${{matrix.language}}" \ No newline at end of file + category: "/language:${{matrix.language}}" From 37a1a5ddd9721927f6399b8190249b3ef3b27d9e Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 3 Feb 2026 16:45:50 +0000 Subject: [PATCH 401/435] Enable manual codeql run --- .github/workflows/codeql.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2046fe0d3..c0634ac28 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -6,6 +6,8 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [ 'main' ] + workflow_dispatch: + schedule: - cron: '8 9 * * 1' From f1b7fa75993b79fcc79a9436d234f41769d2d02c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 3 Feb 2026 16:47:07 +0000 Subject: [PATCH 402/435] Allow manual CI run --- .github/workflows/CI.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 51f96b88d..33819ea85 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -7,6 +7,7 @@ on: paths: - '**' - '!/docs/*' # Don't run workflow when files are only in the /docs directory + workflow_dispatch: jobs: main: @@ -148,4 +149,4 @@ jobs: run: dotnet pack Build.csproj --no-build -c Release /p:PackageOutputPath=${env:GITHUB_WORKSPACE}\.nupkgs /p:CI=true - name: Upload to MyGet if: github.event_name == 'push' && github.ref == 'refs/heads/main' - run: dotnet nuget push ${env:GITHUB_WORKSPACE}\.nupkgs\*.nupkg -s https://www.myget.org/F/stackoverflow/api/v2/package -k ${{ secrets.MYGET_API_KEY }} \ No newline at end of file + run: dotnet nuget push ${env:GITHUB_WORKSPACE}\.nupkgs\*.nupkg -s https://www.myget.org/F/stackoverflow/api/v2/package -k ${{ secrets.MYGET_API_KEY }} From 3ce6682af9cf8d2a930785ca2c11364cb8294f70 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 3 Feb 2026 17:01:25 +0000 Subject: [PATCH 403/435] CI: manual run should pack/deploy --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 33819ea85..a14352953 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -145,8 +145,8 @@ jobs: reporter: dotnet-trx # Package and upload to MyGet only on pushes to main, not on PRs - name: .NET Pack - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' run: dotnet pack Build.csproj --no-build -c Release /p:PackageOutputPath=${env:GITHUB_WORKSPACE}\.nupkgs /p:CI=true - name: Upload to MyGet - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' run: dotnet nuget push ${env:GITHUB_WORKSPACE}\.nupkgs\*.nupkg -s https://www.myget.org/F/stackoverflow/api/v2/package -k ${{ secrets.MYGET_API_KEY }} From fb487d6c98c6466d8a6810e3b95f2d6bab380fe9 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 4 Feb 2026 08:12:25 +0000 Subject: [PATCH 404/435] Release 2.10.14 --- docs/ReleaseNotes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 817213978..f77a1d10a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,7 +6,7 @@ Current package versions: | ------------ | ----------------- | ----- | | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | -## unreleased +## 2.10.14 - Fix bug with connection startup failing in low-memory scenarios ([#3002 by nathan-miller23](https://github.com/StackExchange/StackExchange.Redis/pull/3002)) - Fix under-count of `TotalOutstanding` in server-counters ([#2996 by nathan-miller23](https://github.com/StackExchange/StackExchange.Redis/pull/2996)) From 686f11811aaec53e5d0bc7e3b66c6794321a743c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 4 Feb 2026 10:15:03 +0000 Subject: [PATCH 405/435] fix codeql --- .github/workflows/codeql.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c0634ac28..fa4a444bf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,6 @@ jobs: fail-fast: false matrix: language: [ 'csharp' ] - build-mode: manual # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Use only 'java' to analyze code written in Java, Kotlin or both # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both From df1db72e480bf63bcb92d2128ca1f999dd95a29b Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 4 Feb 2026 16:03:59 -0500 Subject: [PATCH 406/435] Update README to include Azure Managed Redis (#3005) * Update README to include Azure Managed Redis * Removed ACR --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 076a128d7..5da32c6ce 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ StackExchange.Redis =================== -StackExchange.Redis is a .NET client for communicating with RESP servers such as [Redis](https://redis.io/), [Garnet](https://microsoft.github.io/garnet/), [Valkey](https://valkey.io/), [Azure Cache for Redis](https://azure.microsoft.com/products/cache), [AWS ElastiCache](https://aws.amazon.com/elasticache/), and a wide range of other Redis-like servers. We do not maintain a list of compatible servers, but if the server has a Redis-like API: it will *probably* work fine. If not: log an issue with details! +StackExchange.Redis is a .NET client for communicating with RESP servers such as [Redis](https://redis.io/), [Azure Managed Redis](https://azure.microsoft.com/products/managed-redis), [Garnet](https://microsoft.github.io/garnet/), [Valkey](https://valkey.io/), [AWS ElastiCache](https://aws.amazon.com/elasticache/), and a wide range of other Redis-like servers. We do not maintain a list of compatible servers, but if the server has a Redis-like API: it will *probably* work fine. If not: log an issue with details! For all documentation, [see here](https://stackexchange.github.io/StackExchange.Redis/). From c979c6b8714f19a823b5cd1b1cadbcafbc63e5d6 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 6 Feb 2026 16:38:40 +0000 Subject: [PATCH 407/435] keyspace notifications including cluster support (#2995) * add new KeyNotification API - KeyNotification wraps channel+value, exposes friendly parsed members (db, type, etc) - KeyNotificationType is new enum for known values - add TryParseKeyNotification help to ChannelMessage (and use explicit fields) * clarifications * RedisChannel creation API * assert non-sharded in tests * simplify database handling for null and zero * Add API for KeyEvent usage with unexpected event types * nits * optimize channel tests * nit * assertions for multi-node and key-routing logic * prevent publish on multi-node channels * naming * split Subscription into single/multi implementation and do the necessary * initial tests; requires CI changes to be applied * enable key notifications in CI * implement alt-lookup-friendly API * improve alt-lookup logic * Bump tests (and CI etc) to net10, to allow up-to-date bits * queue vs handler tests * docs; moar tests * docs * fix routing for single-key channels * Consider keyspace and channel isolation * Update KeyspaceNotifications.md * Much better API for handling keyspace prefixes in KeyNotification * clarify docs * docs are hard * words * Fix incorrect routing of pub/sub messages on cluster when using channel prefix * simplify channel-prefix passing * - reconnect RESP3 channel subscriptions - EnsureSubscribedToServer[Async] can now *remove* subscriptions in the multi-node case * runner note * make SubscriptionsSurviveConnectionFailureAsync more reliable; Ping is not routed to the channel, and can use primary or replica, so Ping is not a reliable test *unless* we demand master * rem net8.0 tests * - allow single-node subscriptions to follow relocations - prevent concurrency problem on TextWriterOutputHelper * improve subscription recovery logic when using key-routed subscriptions * docs typo * Update docs/KeyspaceNotifications.md Co-authored-by: Philo * Update docs/KeyspaceNotifications.md Co-authored-by: Philo * Update version.json --------- Co-authored-by: Philo --- .github/workflows/CI.yml | 6 +- Directory.Build.props | 2 +- Directory.Packages.props | 3 +- StackExchange.Redis.sln.DotSettings | 3 + docs/KeyspaceNotifications.md | 213 ++++++ docs/index.md | 1 + src/StackExchange.Redis/ChannelMessage.cs | 73 ++ .../ChannelMessageQueue.cs | 552 +++++++------- .../ConnectionMultiplexer.cs | 5 + src/StackExchange.Redis/Format.cs | 64 ++ src/StackExchange.Redis/FrameworkShims.cs | 39 + src/StackExchange.Redis/KeyNotification.cs | 497 +++++++++++++ .../KeyNotificationType.cs | 69 ++ .../KeyNotificationTypeFastHash.cs | 413 +++++++++++ src/StackExchange.Redis/Message.cs | 9 +- src/StackExchange.Redis/PhysicalConnection.cs | 6 +- .../PublicAPI/PublicAPI.Unshipped.txt | 86 ++- src/StackExchange.Redis/RawResult.cs | 19 +- src/StackExchange.Redis/RedisChannel.cs | 212 +++++- src/StackExchange.Redis/RedisDatabase.cs | 4 +- src/StackExchange.Redis/RedisKey.cs | 8 + src/StackExchange.Redis/RedisSubscriber.cs | 244 +----- src/StackExchange.Redis/RedisValue.cs | 146 +++- src/StackExchange.Redis/ResultProcessor.cs | 19 +- src/StackExchange.Redis/ServerEndPoint.cs | 5 +- .../ServerSelectionStrategy.cs | 48 +- .../StackExchange.Redis.csproj | 5 +- src/StackExchange.Redis/Subscription.cs | 520 +++++++++++++ .../3.0.503/redis.windows-service.conf | 2 +- tests/RedisConfigs/3.0.503/redis.windows.conf | 2 +- .../RedisConfigs/Basic/primary-6379-3.0.conf | 3 +- tests/RedisConfigs/Basic/primary-6379.conf | 3 +- tests/RedisConfigs/Basic/replica-6380.conf | 3 +- tests/RedisConfigs/Basic/secure-6381.conf | 3 +- .../RedisConfigs/Basic/tls-ciphers-6384.conf | 1 + tests/RedisConfigs/Cluster/cluster-7000.conf | 3 +- tests/RedisConfigs/Cluster/cluster-7001.conf | 3 +- tests/RedisConfigs/Cluster/cluster-7002.conf | 3 +- tests/RedisConfigs/Cluster/cluster-7003.conf | 3 +- tests/RedisConfigs/Cluster/cluster-7004.conf | 3 +- tests/RedisConfigs/Cluster/cluster-7005.conf | 3 +- tests/RedisConfigs/Failover/primary-6382.conf | 3 +- tests/RedisConfigs/Failover/replica-6383.conf | 3 +- tests/RedisConfigs/Sentinel/redis-7010.conf | 3 +- tests/RedisConfigs/Sentinel/redis-7011.conf | 3 +- .../Certificates/CertValidationTests.cs | 2 + .../ClusterShardedTests.cs | 90 ++- .../StackExchange.Redis.Tests/ClusterTests.cs | 23 +- .../FailoverTests.cs | 8 +- .../FastHashTests.cs | 43 +- tests/StackExchange.Redis.Tests/HashTests.cs | 2 +- .../Helpers/TextWriterOutputHelper.cs | 42 +- .../Issues/Issue2507.cs | 2 +- .../KeyNotificationTests.cs | 698 ++++++++++++++++++ .../PubSubKeyNotificationTests.cs | 419 +++++++++++ .../PubSubMultiserverTests.cs | 26 +- .../RedisValueEquivalencyTests.cs | 16 +- tests/StackExchange.Redis.Tests/SSLTests.cs | 2 + .../StackExchange.Redis.Tests.csproj | 3 +- .../SyncContextTests.cs | 2 +- version.json | 2 +- 61 files changed, 4039 insertions(+), 659 deletions(-) create mode 100644 docs/KeyspaceNotifications.md create mode 100644 src/StackExchange.Redis/ChannelMessage.cs create mode 100644 src/StackExchange.Redis/KeyNotification.cs create mode 100644 src/StackExchange.Redis/KeyNotificationType.cs create mode 100644 src/StackExchange.Redis/KeyNotificationTypeFastHash.cs create mode 100644 src/StackExchange.Redis/Subscription.cs create mode 100644 tests/StackExchange.Redis.Tests/KeyNotificationTests.cs create mode 100644 tests/StackExchange.Redis.Tests/PubSubKeyNotificationTests.cs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a14352953..a8ab53c74 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -23,14 +23,14 @@ jobs: fetch-depth: 0 # Fetch the full history - name: Start Redis Services (docker-compose) working-directory: ./tests/RedisConfigs - run: docker compose -f docker-compose.yml up -d --wait + run: docker compose -f docker-compose.yml up -d --wait - name: Install .NET SDK uses: actions/setup-dotnet@v3 with: - dotnet-version: | + dotnet-version: | 6.0.x 8.0.x - 9.0.x + 10.0.x - name: .NET Build run: dotnet build Build.csproj -c Release /p:CI=true - name: StackExchange.Redis.Tests diff --git a/Directory.Build.props b/Directory.Build.props index 9f10eddcd..06542aa32 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,7 +15,7 @@ https://stackexchange.github.io/StackExchange.Redis/ MIT - 13 + 14 git https://github.com/StackExchange/StackExchange.Redis/ diff --git a/Directory.Packages.props b/Directory.Packages.props index 2088a054f..3fa9e0e3d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,8 @@ - + + diff --git a/StackExchange.Redis.sln.DotSettings b/StackExchange.Redis.sln.DotSettings index 216edbcca..8dd9095d9 100644 --- a/StackExchange.Redis.sln.DotSettings +++ b/StackExchange.Redis.sln.DotSettings @@ -12,9 +12,12 @@ True True True + True True True + True True + True True True True diff --git a/docs/KeyspaceNotifications.md b/docs/KeyspaceNotifications.md new file mode 100644 index 000000000..d9c4f26a1 --- /dev/null +++ b/docs/KeyspaceNotifications.md @@ -0,0 +1,213 @@ +# Redis Keyspace Notifications + +Redis keyspace notifications let you monitor operations happening on your Redis keys in real-time. StackExchange.Redis provides a strongly-typed API for subscribing to and consuming these events. +This could be used for example to implement a cache invalidation strategy. + +## Prerequisites + +### Redis Configuration + +You must [enable keyspace notifications](https://redis.io/docs/latest/develop/pubsub/keyspace-notifications/#configuration) in your Redis server config, +for example: + +``` conf +notify-keyspace-events AKE +``` + +- **A** - All event types +- **K** - Keyspace notifications (`__keyspace@__:`) +- **E** - Keyevent notifications (`__keyevent@__:`) + +The two types of event (keyspace and keyevent) encode the same information, but in different formats. +To simplify consumption, StackExchange.Redis provides a unified API for both types of event, via the `KeyNotification` type. + +### Event Broadcasting in Redis Cluster + +Importantly, in Redis Cluster, keyspace notifications are **not** broadcast to all nodes - they are only received by clients connecting to the +individual node where the keyspace notification originated, i.e. where the key was modified. +This is different to how regular pub/sub events are handled, where a subscription to a channel on one node will receive events published on any node. +Clients must explicitly subscribe to the same channel on each node they wish to receive events from, which typically means: every primary node in the cluster. +To make this easier, StackExchange.Redis provides dedicated APIs for subscribing to keyspace and keyevent notifications that handle this for you. + +## Quick Start + +As an example, we'll subscribe to all keys with a specific prefix, and print out the key and event type for each notification. First, +we need to create a `RedisChannel`: + +```csharp +// this will subscribe to __keyspace@0__:user:*, including supporting Redis Cluster +var channel = RedisChannel.KeySpacePrefix(prefix: "user:"u8, database: 0); +``` + +Note that there are a range of other `KeySpace...` and `KeyEvent...` methods for different scenarios, including: + +- `KeySpaceSingleKey` - subscribe to notifications for a single key in a specific database +- `KeySpacePattern` - subscribe to notifications for a key pattern, optionally in a specific database +- `KeySpacePrefix` - subscribe to notifications for all keys with a specific prefix, optionally in a specific database +- `KeyEvent` - subscribe to notifications for a specific event type, optionally in a specific database + +The `KeySpace*` methods are similar, and are presented separately to make the intent clear. For example, `KeySpacePattern("foo*")` is equivalent to `KeySpacePrefix("foo")`, and will subscribe to all keys beginning with `"foo"`. + +Next, we subscribe to the channel and process the notifications using the normal pub/sub subscription API; there are two +main approaches: queue-based and callback-based. + +Queue-based: + +```csharp +var queue = await sub.SubscribeAsync(channel); +_ = Task.Run(async () => +{ + await foreach (var msg in queue) + { + if (msg.TryParseKeyNotification(out var notification)) + { + Console.WriteLine($"Key: {notification.GetKey()}"); + Console.WriteLine($"Type: {notification.Type}"); + Console.WriteLine($"Database: {notification.Database}"); + } + } +}); +``` + +Callback-based: + +```csharp +sub.Subscribe(channel, (recvChannel, recvValue) => +{ + if (KeyNotification.TryParse(recvChannel, recvValue, out var notification)) + { + Console.WriteLine($"Key: {notification.GetKey()}"); + Console.WriteLine($"Type: {notification.Type}"); + Console.WriteLine($"Database: {notification.Database}"); + } +}); +``` + +Note that the channels created by the `KeySpace...` and `KeyEvent...` methods cannot be used to manually *publish* events, +only to subscribe to them. The events are published automatically by the Redis server when keys are modified. If you +want to simulate keyspace notifications by publishing events manually, you should use regular pub/sub channels that avoid +the `__keyspace@` and `__keyevent@` prefixes. + +## Performance considerations for KeyNotification + +The `KeyNotification` struct provides parsed notification data, including (as already shown) the key, event type, +database, etc. Note that using `GetKey()` will allocate a copy of the key bytes; to avoid allocations, +you can use `TryCopyKey()` to copy the key bytes into a provided buffer (potentially with `GetKeyByteCount()`, +`GetKeyMaxCharCount()`, etc in order to size the buffer appropriately). Similarly, `KeyStartsWith()` can be used to +efficiently check the key prefix without allocating a string. This approach is designed to be efficient for high-volume +notification processing, and in particular: for use with the alt-lookup (span) APIs that are slowly being introduced +in various .NET APIs. + +For example, with a `ConcurrentDictionary` (for some `T`), you can use `GetAlternateLookup>()` +to get an alternate lookup API that takes a `ReadOnlySpan` instead of a `string`, and then use `TryCopyKey()` to copy +the key bytes into a buffer, and then use the alt-lookup API to find the value. This means that we avoid allocating a string +for the key entirely, and instead just copy the bytes into a buffer. If we consider that commonly a local cache will *not* +contain the key for the majority of notifications (since they are for cache invalidation), this can be a significant +performance win. + +## Considerations when using database isolation + +Database isolation is controlled either via the `ConfigurationOptions.DefaultDatabase` option when connecting to Redis, +or by using the `GetDatabase(int? db = null)` method to get a specific database instance. Note that the +`KeySpace...` and `KeyEvent...` APIs may optionally take a database. When a database is specified, subscription will only +respond to notifications for keys in that database. If a database is not specified, the subscription will respond to +notifications for keys in all databases. Often, you will want to pass `db.Database` from the `IDatabase` instance you are +using for your application logic, to ensure that you are monitoring the correct database. When using Redis Cluster, +this usually means database `0`, since Redis Cluster does not usually support multiple databases. + +For example: + +- `RedisChannel.KeySpaceSingleKey("foo", 0)` maps to `SUBSCRIBE __keyspace@0__:foo` +- `RedisChannel.KeySpacePrefix("foo", 0)` maps to `PSUBSCRIBE __keyspace@0__:foo*` +- `RedisChannel.KeySpacePrefix("foo")` maps to `PSUBSCRIBE __keyspace@*__:foo*` +- `RedisChannel.KeyEvent(KeyNotificationType.Set, 0)` maps to `SUBSCRIBE __keyevent@0__:set` +- `RedisChannel.KeyEvent(KeyNotificationType.Set)` maps to `PSUBSCRIBE __keyevent@*__:set` + +Additionally, note that while most of these examples require multi-node subscriptions on Redis Cluster, `KeySpaceSingleKey` +is an exception, and will only subscribe to the single node that owns the key `foo`. + +When subscribing without specifying a database (i.e. listening to changes in all database), the database relating +to the notification can be fetched via `KeyNotification.Database`: + +``` c# +var channel = RedisChannel.KeySpacePrefix("foo"); +sub.SubscribeAsync(channel, (recvChannel, recvValue) => +{ + if (KeyNotification.TryParse(recvChannel, recvValue, out var notification)) + { + var key = notification.GetKey(); + var db = notification.Database; + // ... + } +} +``` + +## Considerations when using keyspace or channel isolation + +StackExchange.Redis supports the concept of keyspace and channel (pub/sub) isolation. + +Channel isolation is controlled using the `ConfigurationOptions.ChannelPrefix` option when connecting to Redis. +Intentionally, this feature *is ignored* by the `KeySpace...` and `KeyEvent...` APIs, because they are designed to +subscribe to specific (server-defined) channels that are outside the control of the client. + +Keyspace isolation is controlled using the `WithKeyPrefix` extension method on `IDatabase`. This is *not* used +by the `KeySpace...` and `KeyEvent...` APIs. Since the database and pub/sub APIs are independent, keyspace isolation +*is not applied* (and cannot be; consuming code could have zero, one, or multiple databases with different prefixes). +The caller is responsible for ensuring that the prefix is applied appropriately when constructing the `RedisChannel`. + +By default, key-related features of `KeyNotification` will return the full key reported by the server, +including any prefix. However, the `TryParseKeyNotification` and `TryParse` methods can optionally be passed a +key prefix, which will be used both to filter unwanted notifications and strip the prefix from the key when reading. +It is *possible* to handle keyspace isolation manually by checking the key with `KeyNotification.KeyStartsWith` and +manually trimming the prefix, but it is *recommended* to do this via `TryParseKeyNotification` and `TryParse`. + +As an example, with a multi-tenant scenario using keyspace isolation, we might have in the database code: + +``` c# +// multi-tenant scenario using keyspace isolation +byte[] keyPrefix = Encoding.UTF8.GetBytes("client1234:"); +var db = conn.GetDatabase().WithKeyPrefix(keyPrefix); + +// we will later commit order data for example: +await db.StringSetAsync("order/123", "ISBN 9789123684434"); +``` + +To observe this, we could use: + +``` c# +var sub = conn.GetSubscriber(); + +// subscribe to the specific tenant as a prefix: +var channel = RedisChannel.KeySpacePrefix("client1234:order/", db.Database); + +sub.SubscribeAsync(channel, (recvChannel, recvValue) => +{ + // by including prefix in the TryParse, we filter out notifications that are not for this client + // *and* the key is sliced internally to remove this prefix when reading + if (KeyNotification.TryParse(keyPrefix, recvChannel, recvValue, out var notification)) + { + // if we get here, the key prefix was a match + var key = notification.GetKey(); // "order/123" - note no prefix + // ... + } + + /* + // for contrast only: this is *not* usually the recommended approach when using keyspace isolation + if (KeyNotification.TryParse(recvChannel, recvValue, out var notification) + && notification.KeyStartsWith(keyPrefix)) + { + var key = notification.GetKey(); // "client1234:order/123" - note prefix is included + // ... + } + */ +}); + +``` + +Alternatively, if we wanted a single handler that observed *all* tenants, we could use: + +``` c# +var channel = RedisChannel.KeySpacePattern("client*:order/*", db.Database); +``` + +with similar code, parsing the client from the key manually, using the full key length. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 0b4d9bb2e..b1498d878 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,6 +39,7 @@ Documentation - [Transactions](Transactions) - how atomic transactions work in redis - [Events](Events) - the events available for logging / information purposes - [Pub/Sub Message Order](PubSubOrder) - advice on sequential and concurrent processing +- [Pub/Sub Key Notifications](KeyspaceNotifications) - how to use keyspace and keyevent notifications - [Using RESP3](Resp3) - information on using RESP3 - [ServerMaintenanceEvent](ServerMaintenanceEvent) - how to listen and prepare for hosted server maintenance (e.g. Azure Cache for Redis) - [Streams](Streams) - how to use the Stream data type diff --git a/src/StackExchange.Redis/ChannelMessage.cs b/src/StackExchange.Redis/ChannelMessage.cs new file mode 100644 index 000000000..a29454f0c --- /dev/null +++ b/src/StackExchange.Redis/ChannelMessage.cs @@ -0,0 +1,73 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// Represents a message that is broadcast via publish/subscribe. +/// +public readonly struct ChannelMessage +{ + // this is *smaller* than storing a RedisChannel for the subscribed channel + private readonly ChannelMessageQueue _queue; + + /// + /// The Channel:Message string representation. + /// + public override string ToString() => ((string?)Channel) + ":" + ((string?)Message); + + /// + public override int GetHashCode() => Channel.GetHashCode() ^ Message.GetHashCode(); + + /// + public override bool Equals(object? obj) => obj is ChannelMessage cm + && cm.Channel == Channel && cm.Message == Message; + + internal ChannelMessage(ChannelMessageQueue queue, in RedisChannel channel, in RedisValue value) + { + _queue = queue; + _channel = channel; + _message = value; + } + + /// + /// The channel that the subscription was created from. + /// + public RedisChannel SubscriptionChannel => _queue.Channel; + + private readonly RedisChannel _channel; + + /// + /// The channel that the message was broadcast to. + /// + public RedisChannel Channel => _channel; + + private readonly RedisValue _message; + + /// + /// The value that was broadcast. + /// + public RedisValue Message => _message; + + /// + /// Checks if 2 messages are .Equal(). + /// + public static bool operator ==(ChannelMessage left, ChannelMessage right) => left.Equals(right); + + /// + /// Checks if 2 messages are not .Equal(). + /// + public static bool operator !=(ChannelMessage left, ChannelMessage right) => !left.Equals(right); + + /// + /// If the channel is either a keyspace or keyevent notification, resolve the key and event type. + /// + public bool TryParseKeyNotification(out KeyNotification notification) + => KeyNotification.TryParse(in _channel, in _message, out notification); + + /// + /// If the channel is either a keyspace or keyevent notification *with the requested prefix*, resolve the key and event type, + /// and remove the prefix when reading the key. + /// + public bool TryParseKeyNotification(ReadOnlySpan keyPrefix, out KeyNotification notification) + => KeyNotification.TryParse(keyPrefix, in _channel, in _message, out notification); +} diff --git a/src/StackExchange.Redis/ChannelMessageQueue.cs b/src/StackExchange.Redis/ChannelMessageQueue.cs index e58fb393b..9f962e52a 100644 --- a/src/StackExchange.Redis/ChannelMessageQueue.cs +++ b/src/StackExchange.Redis/ChannelMessageQueue.cs @@ -1,385 +1,353 @@ using System; +using System.Buffers.Text; using System.Collections.Generic; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; #if NETCOREAPP3_1 +using System.Diagnostics; using System.Reflection; #endif -namespace StackExchange.Redis +namespace StackExchange.Redis; + +/// +/// Represents a message queue of ordered pub/sub notifications. +/// +/// +/// To create a ChannelMessageQueue, use +/// or . +/// +public sealed class ChannelMessageQueue : IAsyncEnumerable { + private readonly Channel _queue; + /// - /// Represents a message that is broadcast via publish/subscribe. + /// The Channel that was subscribed for this queue. /// - public readonly struct ChannelMessage - { - // this is *smaller* than storing a RedisChannel for the subscribed channel - private readonly ChannelMessageQueue _queue; + public RedisChannel Channel { get; } - /// - /// The Channel:Message string representation. - /// - public override string ToString() => ((string?)Channel) + ":" + ((string?)Message); + private RedisSubscriber? _parent; - /// - public override int GetHashCode() => Channel.GetHashCode() ^ Message.GetHashCode(); - - /// - public override bool Equals(object? obj) => obj is ChannelMessage cm - && cm.Channel == Channel && cm.Message == Message; + /// + /// The string representation of this channel. + /// + public override string? ToString() => (string?)Channel; - internal ChannelMessage(ChannelMessageQueue queue, in RedisChannel channel, in RedisValue value) - { - _queue = queue; - Channel = channel; - Message = value; - } + /// + /// An awaitable task the indicates completion of the queue (including drain of data). + /// + public Task Completion => _queue.Reader.Completion; - /// - /// The channel that the subscription was created from. - /// - public RedisChannel SubscriptionChannel => _queue.Channel; - - /// - /// The channel that the message was broadcast to. - /// - public RedisChannel Channel { get; } - - /// - /// The value that was broadcast. - /// - public RedisValue Message { get; } - - /// - /// Checks if 2 messages are .Equal(). - /// - public static bool operator ==(ChannelMessage left, ChannelMessage right) => left.Equals(right); - - /// - /// Checks if 2 messages are not .Equal(). - /// - public static bool operator !=(ChannelMessage left, ChannelMessage right) => !left.Equals(right); + internal ChannelMessageQueue(in RedisChannel redisChannel, RedisSubscriber parent) + { + Channel = redisChannel; + _parent = parent; + _queue = System.Threading.Channels.Channel.CreateUnbounded(s_ChannelOptions); } - /// - /// Represents a message queue of ordered pub/sub notifications. - /// - /// - /// To create a ChannelMessageQueue, use - /// or . - /// - public sealed class ChannelMessageQueue : IAsyncEnumerable + private static readonly UnboundedChannelOptions s_ChannelOptions = new UnboundedChannelOptions { - private readonly Channel _queue; + SingleWriter = true, SingleReader = false, AllowSynchronousContinuations = false, + }; - /// - /// The Channel that was subscribed for this queue. - /// - public RedisChannel Channel { get; } - private RedisSubscriber? _parent; + private void Write(in RedisChannel channel, in RedisValue value) + { + var writer = _queue.Writer; + writer.TryWrite(new ChannelMessage(this, channel, value)); + } - /// - /// The string representation of this channel. - /// - public override string? ToString() => (string?)Channel; + /// + /// Consume a message from the channel. + /// + /// The to use. + public ValueTask ReadAsync(CancellationToken cancellationToken = default) + => _queue.Reader.ReadAsync(cancellationToken); - /// - /// An awaitable task the indicates completion of the queue (including drain of data). - /// - public Task Completion => _queue.Reader.Completion; + /// + /// Attempt to synchronously consume a message from the channel. + /// + /// The read from the Channel. + public bool TryRead(out ChannelMessage item) => _queue.Reader.TryRead(out item); - internal ChannelMessageQueue(in RedisChannel redisChannel, RedisSubscriber parent) + /// + /// Attempt to query the backlog length of the queue. + /// + /// The (approximate) count of items in the Channel. + public bool TryGetCount(out int count) + { + // This is specific to netcoreapp3.1, because full framework was out of band and the new prop is present +#if NETCOREAPP3_1 + // get this using the reflection + try { - Channel = redisChannel; - _parent = parent; - _queue = System.Threading.Channels.Channel.CreateUnbounded(s_ChannelOptions); + var prop = + _queue.GetType().GetProperty("ItemsCountForDebugger", BindingFlags.Instance | BindingFlags.NonPublic); + if (prop is not null) + { + count = (int)prop.GetValue(_queue)!; + return true; + } } - - private static readonly UnboundedChannelOptions s_ChannelOptions = new UnboundedChannelOptions + catch (Exception ex) { - SingleWriter = true, - SingleReader = false, - AllowSynchronousContinuations = false, - }; - - private void Write(in RedisChannel channel, in RedisValue value) + Debug.WriteLine(ex.Message); // but ignore + } +#else + var reader = _queue.Reader; + if (reader.CanCount) { - var writer = _queue.Writer; - writer.TryWrite(new ChannelMessage(this, channel, value)); + count = reader.Count; + return true; } +#endif - /// - /// Consume a message from the channel. - /// - /// The to use. - public ValueTask ReadAsync(CancellationToken cancellationToken = default) - => _queue.Reader.ReadAsync(cancellationToken); - - /// - /// Attempt to synchronously consume a message from the channel. - /// - /// The read from the Channel. - public bool TryRead(out ChannelMessage item) => _queue.Reader.TryRead(out item); - - /// - /// Attempt to query the backlog length of the queue. - /// - /// The (approximate) count of items in the Channel. - public bool TryGetCount(out int count) + count = 0; + return false; + } + + private Delegate? _onMessageHandler; + + private void AssertOnMessage(Delegate handler) + { + if (handler == null) throw new ArgumentNullException(nameof(handler)); + if (Interlocked.CompareExchange(ref _onMessageHandler, handler, null) != null) + throw new InvalidOperationException("Only a single " + nameof(OnMessage) + " is allowed"); + } + + /// + /// Create a message loop that processes messages sequentially. + /// + /// The handler to run when receiving a message. + public void OnMessage(Action handler) + { + AssertOnMessage(handler); + + ThreadPool.QueueUserWorkItem( + state => ((ChannelMessageQueue)state!).OnMessageSyncImpl().RedisFireAndForget(), this); + } + + private async Task OnMessageSyncImpl() + { + var handler = (Action?)_onMessageHandler; + while (!Completion.IsCompleted) { - // This is specific to netcoreapp3.1, because full framework was out of band and the new prop is present -#if NETCOREAPP3_1 - // get this using the reflection + ChannelMessage next; try { - var prop = _queue.GetType().GetProperty("ItemsCountForDebugger", BindingFlags.Instance | BindingFlags.NonPublic); - if (prop is not null) - { - count = (int)prop.GetValue(_queue)!; - return true; - } + if (!TryRead(out next)) next = await ReadAsync().ForAwait(); } - catch { } -#else - var reader = _queue.Reader; - if (reader.CanCount) + catch (ChannelClosedException) { break; } // expected + catch (Exception ex) { - count = reader.Count; - return true; + _parent?.multiplexer?.OnInternalError(ex); + break; } -#endif - count = default; - return false; + try { handler?.Invoke(next); } + catch { } // matches MessageCompletable } + } - private Delegate? _onMessageHandler; - private void AssertOnMessage(Delegate handler) + internal static void Combine(ref ChannelMessageQueue? head, ChannelMessageQueue queue) + { + if (queue != null) { - if (handler == null) throw new ArgumentNullException(nameof(handler)); - if (Interlocked.CompareExchange(ref _onMessageHandler, handler, null) != null) - throw new InvalidOperationException("Only a single " + nameof(OnMessage) + " is allowed"); + // insert at the start of the linked-list + ChannelMessageQueue? old; + do + { + old = Volatile.Read(ref head); + queue._next = old; + } + // format and validator disagree on newline... + while (Interlocked.CompareExchange(ref head, queue, old) != old); } + } - /// - /// Create a message loop that processes messages sequentially. - /// - /// The handler to run when receiving a message. - public void OnMessage(Action handler) - { - AssertOnMessage(handler); + /// + /// Create a message loop that processes messages sequentially. + /// + /// The handler to execute when receiving a message. + public void OnMessage(Func handler) + { + AssertOnMessage(handler); - ThreadPool.QueueUserWorkItem( - state => ((ChannelMessageQueue)state!).OnMessageSyncImpl().RedisFireAndForget(), this); - } + ThreadPool.QueueUserWorkItem( + state => ((ChannelMessageQueue)state!).OnMessageAsyncImpl().RedisFireAndForget(), this); + } - private async Task OnMessageSyncImpl() + internal static void Remove(ref ChannelMessageQueue? head, ChannelMessageQueue queue) + { + if (queue is null) { - var handler = (Action?)_onMessageHandler; - while (!Completion.IsCompleted) - { - ChannelMessage next; - try { if (!TryRead(out next)) next = await ReadAsync().ForAwait(); } - catch (ChannelClosedException) { break; } // expected - catch (Exception ex) - { - _parent?.multiplexer?.OnInternalError(ex); - break; - } - - try { handler?.Invoke(next); } - catch { } // matches MessageCompletable - } + return; } - internal static void Combine(ref ChannelMessageQueue? head, ChannelMessageQueue queue) + bool found; + // if we fail due to a conflict, re-do from start + do { - if (queue != null) + var current = Volatile.Read(ref head); + if (current == null) return; // no queue? nothing to do + if (current == queue) { - // insert at the start of the linked-list - ChannelMessageQueue? old; - do + found = true; + // found at the head - then we need to change the head + if (Interlocked.CompareExchange(ref head, Volatile.Read(ref current._next), current) == current) { - old = Volatile.Read(ref head); - queue._next = old; + return; // success } - while (Interlocked.CompareExchange(ref head, queue, old) != old); } - } - - /// - /// Create a message loop that processes messages sequentially. - /// - /// The handler to execute when receiving a message. - public void OnMessage(Func handler) - { - AssertOnMessage(handler); - - ThreadPool.QueueUserWorkItem( - state => ((ChannelMessageQueue)state!).OnMessageAsyncImpl().RedisFireAndForget(), this); - } - - internal static void Remove(ref ChannelMessageQueue? head, ChannelMessageQueue queue) - { - if (queue is null) + else { - return; - } - - bool found; - // if we fail due to a conflict, re-do from start - do - { - var current = Volatile.Read(ref head); - if (current == null) return; // no queue? nothing to do - if (current == queue) - { - found = true; - // found at the head - then we need to change the head - if (Interlocked.CompareExchange(ref head, Volatile.Read(ref current._next), current) == current) - { - return; // success - } - } - else + ChannelMessageQueue? previous = current; + current = Volatile.Read(ref previous._next); + found = false; + do { - ChannelMessageQueue? previous = current; - current = Volatile.Read(ref previous._next); - found = false; - do + if (current == queue) { - if (current == queue) + found = true; + // found it, not at the head; remove the node + if (Interlocked.CompareExchange( + ref previous._next, + Volatile.Read(ref current._next), + current) == current) { - found = true; - // found it, not at the head; remove the node - if (Interlocked.CompareExchange(ref previous._next, Volatile.Read(ref current._next), current) == current) - { - return; // success - } - else - { - break; // exit the inner loop, and repeat the outer loop - } + return; // success + } + else + { + break; // exit the inner loop, and repeat the outer loop } - previous = current; - current = Volatile.Read(ref previous!._next); } - while (current != null); + + previous = current; + current = Volatile.Read(ref previous!._next); } + // format and validator disagree on newline... + while (current != null); } - while (found); } + // format and validator disagree on newline... + while (found); + } - internal static int Count(ref ChannelMessageQueue? head) + internal static int Count(ref ChannelMessageQueue? head) + { + var current = Volatile.Read(ref head); + int count = 0; + while (current != null) { - var current = Volatile.Read(ref head); - int count = 0; - while (current != null) - { - count++; - current = Volatile.Read(ref current._next); - } - return count; + count++; + current = Volatile.Read(ref current._next); } - internal static void WriteAll(ref ChannelMessageQueue head, in RedisChannel channel, in RedisValue message) + return count; + } + + internal static void WriteAll(ref ChannelMessageQueue head, in RedisChannel channel, in RedisValue message) + { + var current = Volatile.Read(ref head); + while (current != null) { - var current = Volatile.Read(ref head); - while (current != null) - { - current.Write(channel, message); - current = Volatile.Read(ref current._next); - } + current.Write(channel, message); + current = Volatile.Read(ref current._next); } + } - private ChannelMessageQueue? _next; + private ChannelMessageQueue? _next; - private async Task OnMessageAsyncImpl() + private async Task OnMessageAsyncImpl() + { + var handler = (Func?)_onMessageHandler; + while (!Completion.IsCompleted) { - var handler = (Func?)_onMessageHandler; - while (!Completion.IsCompleted) + ChannelMessage next; + try { - ChannelMessage next; - try { if (!TryRead(out next)) next = await ReadAsync().ForAwait(); } - catch (ChannelClosedException) { break; } // expected - catch (Exception ex) - { - _parent?.multiplexer?.OnInternalError(ex); - break; - } - - try - { - var task = handler?.Invoke(next); - if (task != null && task.Status != TaskStatus.RanToCompletion) await task.ForAwait(); - } - catch { } // matches MessageCompletable + if (!TryRead(out next)) next = await ReadAsync().ForAwait(); + } + catch (ChannelClosedException) { break; } // expected + catch (Exception ex) + { + _parent?.multiplexer?.OnInternalError(ex); + break; } - } - internal static void MarkAllCompleted(ref ChannelMessageQueue? head) - { - var current = Interlocked.Exchange(ref head, null); - while (current != null) + try { - current.MarkCompleted(); - current = Volatile.Read(ref current._next); + var task = handler?.Invoke(next); + if (task != null && task.Status != TaskStatus.RanToCompletion) await task.ForAwait(); } + catch { } // matches MessageCompletable } + } - private void MarkCompleted(Exception? error = null) + internal static void MarkAllCompleted(ref ChannelMessageQueue? head) + { + var current = Interlocked.Exchange(ref head, null); + while (current != null) { - _parent = null; - _queue.Writer.TryComplete(error); + current.MarkCompleted(); + current = Volatile.Read(ref current._next); } + } - internal void UnsubscribeImpl(Exception? error = null, CommandFlags flags = CommandFlags.None) - { - var parent = _parent; - _parent = null; - parent?.UnsubscribeAsync(Channel, null, this, flags); - _queue.Writer.TryComplete(error); - } + private void MarkCompleted(Exception? error = null) + { + _parent = null; + _queue.Writer.TryComplete(error); + } - internal async Task UnsubscribeAsyncImpl(Exception? error = null, CommandFlags flags = CommandFlags.None) + internal void UnsubscribeImpl(Exception? error = null, CommandFlags flags = CommandFlags.None) + { + var parent = _parent; + _parent = null; + parent?.UnsubscribeAsync(Channel, null, this, flags); + _queue.Writer.TryComplete(error); + } + + internal async Task UnsubscribeAsyncImpl(Exception? error = null, CommandFlags flags = CommandFlags.None) + { + var parent = _parent; + _parent = null; + if (parent != null) { - var parent = _parent; - _parent = null; - if (parent != null) - { - await parent.UnsubscribeAsync(Channel, null, this, flags).ForAwait(); - } - _queue.Writer.TryComplete(error); + await parent.UnsubscribeAsync(Channel, null, this, flags).ForAwait(); } - /// - /// Stop receiving messages on this channel. - /// - /// The flags to use when unsubscribing. - public void Unsubscribe(CommandFlags flags = CommandFlags.None) => UnsubscribeImpl(null, flags); + _queue.Writer.TryComplete(error); + } - /// - /// Stop receiving messages on this channel. - /// - /// The flags to use when unsubscribing. - public Task UnsubscribeAsync(CommandFlags flags = CommandFlags.None) => UnsubscribeAsyncImpl(null, flags); + /// + /// Stop receiving messages on this channel. + /// + /// The flags to use when unsubscribing. + public void Unsubscribe(CommandFlags flags = CommandFlags.None) => UnsubscribeImpl(null, flags); - /// + /// + /// Stop receiving messages on this channel. + /// + /// The flags to use when unsubscribing. + public Task UnsubscribeAsync(CommandFlags flags = CommandFlags.None) => UnsubscribeAsyncImpl(null, flags); + + /// #if NETCOREAPP3_0_OR_GREATER - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) - => _queue.Reader.ReadAllAsync().GetAsyncEnumerator(cancellationToken); + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + // ReSharper disable once MethodSupportsCancellation - provided in GetAsyncEnumerator + => _queue.Reader.ReadAllAsync().GetAsyncEnumerator(cancellationToken); #else - public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + while (await _queue.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) { - while (await _queue.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) + while (_queue.Reader.TryRead(out var item)) { - while (_queue.Reader.TryRead(out var item)) - { - yield return item; - } + yield return item; } } -#endif } +#endif } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index cc766338a..0c6148923 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -730,6 +730,7 @@ private static ConnectionMultiplexer ConnectImpl(ConfigurationOptions configurat ReadOnlySpan IInternalConnectionMultiplexer.GetServerSnapshot() => _serverSnapshot.AsSpan(); internal ReadOnlySpan GetServerSnapshot() => _serverSnapshot.AsSpan(); + internal ReadOnlyMemory GetServerSnaphotMemory() => _serverSnapshot.AsMemory(); internal sealed class ServerSnapshot : IEnumerable { public static ServerSnapshot Empty { get; } = new ServerSnapshot(Array.Empty(), 0); @@ -1281,6 +1282,10 @@ public long OperationCount } } + // note that the RedisChannel->byte[] converter is always direct, so this is not an alloc + // (we deal with channels far less frequently, so pay the encoding cost up-front) + internal byte[] ChannelPrefix => ((byte[]?)RawConfig.ChannelPrefix) ?? []; + /// /// Reconfigure the current connections based on the existing configuration. /// diff --git a/src/StackExchange.Redis/Format.cs b/src/StackExchange.Redis/Format.cs index 86aa9910d..a76b77afc 100644 --- a/src/StackExchange.Redis/Format.cs +++ b/src/StackExchange.Redis/Format.cs @@ -468,6 +468,31 @@ internal static int FormatDouble(double value, Span destination) #endif } + internal static int FormatDouble(double value, Span destination) + { + string s; + if (double.IsInfinity(value)) + { + s = double.IsPositiveInfinity(value) ? "+inf" : "-inf"; + if (!s.AsSpan().TryCopyTo(destination)) ThrowFormatFailed(); + return 4; + } + +#if NET + if (!value.TryFormat(destination, out int len, "G17", NumberFormatInfo.InvariantInfo)) + { + ThrowFormatFailed(); + } + + return len; +#else + s = value.ToString("G17", NumberFormatInfo.InvariantInfo); // this looks inefficient, but is how Utf8Formatter works too, just: more direct + if (s.Length > destination.Length) ThrowFormatFailed(); + s.AsSpan().CopyTo(destination); + return s.Length; +#endif + } + internal static int MeasureInt64(long value) { Span valueSpan = stackalloc byte[MaxInt64TextLen]; @@ -481,12 +506,38 @@ internal static int FormatInt64(long value, Span destination) return len; } + internal static int FormatInt64(long value, Span destination) + { +#if NET + if (!value.TryFormat(destination, out var len)) + ThrowFormatFailed(); + return len; +#else + Span buffer = stackalloc byte[MaxInt64TextLen]; + var bytes = FormatInt64(value, buffer); + return Encoding.UTF8.GetChars(buffer.Slice(0, bytes), destination); +#endif + } + internal static int MeasureUInt64(ulong value) { Span valueSpan = stackalloc byte[MaxInt64TextLen]; return FormatUInt64(value, valueSpan); } + internal static int FormatUInt64(ulong value, Span destination) + { +#if NET + if (!value.TryFormat(destination, out var len)) + ThrowFormatFailed(); + return len; +#else + Span buffer = stackalloc byte[MaxInt64TextLen]; + var bytes = FormatUInt64(value, buffer); + return Encoding.UTF8.GetChars(buffer.Slice(0, bytes), destination); +#endif + } + internal static int FormatUInt64(ulong value, Span destination) { if (!Utf8Formatter.TryFormat(value, destination, out var len)) @@ -501,6 +552,19 @@ internal static int FormatInt32(int value, Span destination) return len; } + internal static int FormatInt32(int value, Span destination) + { +#if NET + if (!value.TryFormat(destination, out var len)) + ThrowFormatFailed(); + return len; +#else + Span buffer = stackalloc byte[MaxInt32TextLen]; + var bytes = FormatInt32(value, buffer); + return Encoding.UTF8.GetChars(buffer.Slice(0, bytes), destination); +#endif + } + internal static bool TryParseVersion(ReadOnlySpan input, [NotNullWhen(true)] out Version? version) { #if NETCOREAPP3_1_OR_GREATER diff --git a/src/StackExchange.Redis/FrameworkShims.cs b/src/StackExchange.Redis/FrameworkShims.cs index 9472df9ae..c0fe4cb1d 100644 --- a/src/StackExchange.Redis/FrameworkShims.cs +++ b/src/StackExchange.Redis/FrameworkShims.cs @@ -15,6 +15,18 @@ internal static class IsExternalInit { } } #endif +#if !NET9_0_OR_GREATER +namespace System.Runtime.CompilerServices +{ + // see https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.overloadresolutionpriorityattribute + [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property, Inherited = false)] + internal sealed class OverloadResolutionPriorityAttribute(int priority) : Attribute + { + public int Priority => priority; + } +} +#endif + #if !(NETCOREAPP || NETSTANDARD2_1_OR_GREATER) namespace System.Text @@ -31,6 +43,33 @@ public static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan sou } } } + + public static unsafe int GetChars(this Encoding encoding, ReadOnlySpan source, Span destination) + { + fixed (byte* bPtr = source) + { + fixed (char* cPtr = destination) + { + return encoding.GetChars(bPtr, source.Length, cPtr, destination.Length); + } + } + } + + public static unsafe int GetCharCount(this Encoding encoding, ReadOnlySpan source) + { + fixed (byte* bPtr = source) + { + return encoding.GetCharCount(bPtr, source.Length); + } + } + + public static unsafe string GetString(this Encoding encoding, ReadOnlySpan source) + { + fixed (byte* bPtr = source) + { + return encoding.GetString(bPtr, source.Length); + } + } } } #endif diff --git a/src/StackExchange.Redis/KeyNotification.cs b/src/StackExchange.Redis/KeyNotification.cs new file mode 100644 index 000000000..3427c4dce --- /dev/null +++ b/src/StackExchange.Redis/KeyNotification.cs @@ -0,0 +1,497 @@ +using System; +using System.Buffers; +using System.Buffers.Text; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; +using static StackExchange.Redis.KeyNotificationChannels; +namespace StackExchange.Redis; + +/// +/// Represents keyspace and keyevent notifications, with utility methods for accessing the component data. Additionally, +/// since notifications can be high volume, a range of utility APIs is provided for avoiding allocations, in particular +/// to assist in filtering and inspecting the key without performing string allocations and substring operations. +/// In particular, note that this allows use with the alt-lookup (span-based) APIs on dictionaries. +/// +public readonly ref struct KeyNotification +{ + // effectively we just wrap a channel, but: we've pre-validated that things make sense + private readonly RedisChannel _channel; + private readonly RedisValue _value; + private readonly int _keyOffset; // used to efficiently strip key prefixes + + // this type has been designed with the intent of being able to move the entire thing alloc-free in some future + // high-throughput callback, potentially with a ReadOnlySpan field for the key fragment; this is + // not implemented currently, but is why this is a ref struct + + /// + /// If the channel is either a keyspace or keyevent notification, resolve the key and event type. + /// + public static bool TryParse(scoped in RedisChannel channel, scoped in RedisValue value, out KeyNotification notification) + { + // validate that it looks reasonable + var span = channel.Span; + + // KeySpaceStart and KeyEventStart are the same size, see KeyEventPrefix_KeySpacePrefix_Length_Matches + if (span.Length >= KeySpacePrefix.Length + MinSuffixBytes) + { + // check that the prefix is valid, i.e. "__keyspace@" or "__keyevent@" + var prefix = span.Slice(0, KeySpacePrefix.Length); + var hash = prefix.Hash64(); + switch (hash) + { + case KeySpacePrefix.Hash when KeySpacePrefix.Is(hash, prefix): + case KeyEventPrefix.Hash when KeyEventPrefix.Is(hash, prefix): + // check that there is *something* non-empty after the prefix, with __: as the suffix (we don't verify *what*) + if (span.Slice(KeySpacePrefix.Length).IndexOf("__:"u8) > 0) + { + notification = new KeyNotification(in channel, in value); + return true; + } + + break; + } + } + + notification = default; + return false; + } + + /// + /// If the channel is either a keyspace or keyevent notification *with the requested prefix*, resolve the key and event type, + /// and remove the prefix when reading the key. + /// + public static bool TryParse(scoped in ReadOnlySpan keyPrefix, scoped in RedisChannel channel, scoped in RedisValue value, out KeyNotification notification) + { + if (TryParse(in channel, in value, out notification) && notification.KeyStartsWith(keyPrefix)) + { + notification = notification.WithKeySlice(keyPrefix.Length); + return true; + } + + notification = default; + return false; + } + + internal KeyNotification WithKeySlice(int keyPrefixLength) + { + KeyNotification result = this; + Unsafe.AsRef(in result._keyOffset) = keyPrefixLength; + return result; + } + + private const int MinSuffixBytes = 5; // need "0__:x" or similar after prefix + + /// + /// The channel associated with this notification. + /// + public RedisChannel GetChannel() => _channel; + + /// + /// The payload associated with this notification. + /// + public RedisValue GetValue() => _value; + + internal KeyNotification(scoped in RedisChannel channel, scoped in RedisValue value) + { + _channel = channel; + _value = value; + _keyOffset = 0; + } + + internal int KeyOffset => _keyOffset; + + /// + /// The database the key is in. If the database cannot be parsed, -1 is returned. + /// + public int Database + { + get + { + // prevalidated format, so we can just skip past the prefix (except for the default value) + if (_channel.IsNull) return -1; + var span = _channel.Span.Slice(KeySpacePrefix.Length); // also works for KeyEventPrefix + var end = span.IndexOf((byte)'_'); // expecting "__:foo" - we'll just stop at the underscore + if (end <= 0) return -1; + + span = span.Slice(0, end); + return Utf8Parser.TryParse(span, out int database, out var bytes) + && bytes == end ? database : -1; + } + } + + /// + /// The key associated with this event. + /// + /// Note that this will allocate a copy of the key bytes; to avoid allocations, + /// the , , and APIs can be used. + public RedisKey GetKey() + { + if (IsKeySpace) + { + // then the channel contains the key, and the payload contains the event-type + return ChannelSuffix.Slice(_keyOffset).ToArray(); // create an isolated copy + } + + if (IsKeyEvent) + { + // then the channel contains the event-type, and the payload contains the key + byte[]? blob = _value; + if (_keyOffset != 0 & blob is not null) + { + return blob.AsSpan(_keyOffset).ToArray(); + } + return blob; + } + + return RedisKey.Null; + } + + /// + /// Get the number of bytes in the key. + /// + /// If a scratch-buffer is required, it may be preferable to use , which is less expensive. + public int GetKeyByteCount() + { + if (IsKeySpace) + { + return ChannelSuffix.Length - _keyOffset; + } + + if (IsKeyEvent) + { + return _value.GetByteCount() - _keyOffset; + } + + return 0; + } + + /// + /// Get the maximum number of bytes in the key. + /// + public int GetKeyMaxByteCount() + { + if (IsKeySpace) + { + return ChannelSuffix.Length - _keyOffset; + } + + if (IsKeyEvent) + { + return _value.GetMaxByteCount() - _keyOffset; + } + + return 0; + } + + /// + /// Get the maximum number of characters in the key, interpreting as UTF8. + /// + public int GetKeyMaxCharCount() + { + if (IsKeySpace) + { + return Encoding.UTF8.GetMaxCharCount(ChannelSuffix.Length - _keyOffset); + } + + if (IsKeyEvent) + { + return _value.GetMaxCharCount() - _keyOffset; + } + + return 0; + } + + /// + /// Get the number of characters in the key, interpreting as UTF8. + /// + /// If a scratch-buffer is required, it may be preferable to use , which is less expensive. + public int GetKeyCharCount() + { + if (IsKeySpace) + { + return Encoding.UTF8.GetCharCount(ChannelSuffix.Slice(_keyOffset)); + } + + if (IsKeyEvent) + { + return _keyOffset == 0 ? _value.GetCharCount() : SlowMeasure(in this); + } + + return 0; + + static int SlowMeasure(in KeyNotification value) + { + var span = value.GetKeySpan(out var lease, stackalloc byte[128]); + var result = Encoding.UTF8.GetCharCount(span); + Return(lease); + return result; + } + } + + private ReadOnlySpan GetKeySpan(out byte[]? lease, Span buffer) // buffer typically stackalloc + { + lease = null; + if (_value.TryGetSpan(out var direct)) + { + return direct.Slice(_keyOffset); + } + var count = _value.GetMaxByteCount(); + if (count > buffer.Length) + { + buffer = lease = ArrayPool.Shared.Rent(count); + } + count = _value.CopyTo(buffer); + return buffer.Slice(_keyOffset, count - _keyOffset); + } + + private static void Return(byte[]? lease) + { + if (lease is not null) ArrayPool.Shared.Return(lease); + } + + /// + /// Attempt to copy the bytes from the key to a buffer, returning the number of bytes written. + /// + public bool TryCopyKey(Span destination, out int bytesWritten) + { + if (IsKeySpace) + { + var suffix = ChannelSuffix.Slice(_keyOffset); + bytesWritten = suffix.Length; // assume success + if (bytesWritten <= destination.Length) + { + suffix.CopyTo(destination); + return true; + } + } + + if (IsKeyEvent) + { + if (_value.TryGetSpan(out var direct)) + { + bytesWritten = direct.Length - _keyOffset; // assume success + if (bytesWritten <= destination.Length) + { + direct.Slice(_keyOffset).CopyTo(destination); + return true; + } + bytesWritten = 0; + return false; + } + + if (_keyOffset == 0) + { + // get the value to do the hard work + bytesWritten = _value.GetByteCount(); + if (bytesWritten <= destination.Length) + { + _value.CopyTo(destination); + return true; + } + bytesWritten = 0; + return false; + } + + return SlowCopy(in this, destination, out bytesWritten); + + static bool SlowCopy(in KeyNotification value, Span destination, out int bytesWritten) + { + var span = value.GetKeySpan(out var lease, stackalloc byte[128]); + bool result = span.TryCopyTo(destination); + bytesWritten = result ? span.Length : 0; + Return(lease); + return result; + } + } + + bytesWritten = 0; + return false; + } + + /// + /// Attempt to copy the bytes from the key to a buffer, returning the number of bytes written. + /// + public bool TryCopyKey(Span destination, out int charsWritten) + { + if (IsKeySpace) + { + var suffix = ChannelSuffix.Slice(_keyOffset); + if (Encoding.UTF8.GetMaxCharCount(suffix.Length) <= destination.Length || + Encoding.UTF8.GetCharCount(suffix) <= destination.Length) + { + charsWritten = Encoding.UTF8.GetChars(suffix, destination); + return true; + } + } + + if (IsKeyEvent) + { + if (_keyOffset == 0) // can use short-cut + { + if (_value.GetMaxCharCount() <= destination.Length || _value.GetCharCount() <= destination.Length) + { + charsWritten = _value.CopyTo(destination); + return true; + } + } + var span = GetKeySpan(out var lease, stackalloc byte[128]); + charsWritten = 0; + bool result = false; + if (Encoding.UTF8.GetMaxCharCount(span.Length) <= destination.Length || + Encoding.UTF8.GetCharCount(span) <= destination.Length) + { + charsWritten = Encoding.UTF8.GetChars(span, destination); + result = true; + } + Return(lease); + return result; + } + + charsWritten = 0; + return false; + } + + /// + /// Get the portion of the channel after the "__{keyspace|keyevent}@{db}__:". + /// + private ReadOnlySpan ChannelSuffix + { + get + { + var span = _channel.Span; + var index = span.IndexOf("__:"u8); + return index > 0 ? span.Slice(index + 3) : default; + } + } + + /// + /// Indicates whether this notification is of the given type, specified as raw bytes. + /// + /// This is especially useful for working with unknown event types, but repeated calls to this method will be more expensive than + /// a single successful call to . + public bool IsType(ReadOnlySpan type) + { + if (IsKeySpace) + { + if (_value.TryGetSpan(out var direct)) + { + return direct.SequenceEqual(type); + } + + const int MAX_STACK = 64; + byte[]? lease = null; + var maxCount = _value.GetMaxByteCount(); + Span localCopy = maxCount <= MAX_STACK + ? stackalloc byte[MAX_STACK] + : (lease = ArrayPool.Shared.Rent(maxCount)); + var count = _value.CopyTo(localCopy); + bool result = localCopy.Slice(0, count).SequenceEqual(type); + if (lease is not null) ArrayPool.Shared.Return(lease); + return result; + } + + if (IsKeyEvent) + { + return ChannelSuffix.SequenceEqual(type); + } + + return false; + } + + /// + /// The type of notification associated with this event, if it is well-known - otherwise . + /// + /// Unexpected values can be processed manually from the and . + public KeyNotificationType Type + { + get + { + if (IsKeySpace) + { + // then the channel contains the key, and the payload contains the event-type + var count = _value.GetByteCount(); + if (count >= KeyNotificationTypeFastHash.MinBytes & count <= KeyNotificationTypeFastHash.MaxBytes) + { + if (_value.TryGetSpan(out var direct)) + { + return KeyNotificationTypeFastHash.Parse(direct); + } + else + { + Span localCopy = stackalloc byte[KeyNotificationTypeFastHash.MaxBytes]; + return KeyNotificationTypeFastHash.Parse(localCopy.Slice(0, _value.CopyTo(localCopy))); + } + } + } + + if (IsKeyEvent) + { + // then the channel contains the event-type, and the payload contains the key + return KeyNotificationTypeFastHash.Parse(ChannelSuffix); + } + return KeyNotificationType.Unknown; + } + } + + /// + /// Indicates whether this notification originated from a keyspace notification, for example __keyspace@4__:mykey with payload set. + /// + public bool IsKeySpace + { + get + { + var span = _channel.Span; + return span.Length >= KeySpacePrefix.Length + MinSuffixBytes && KeySpacePrefix.Is(span.Hash64(), span.Slice(0, KeySpacePrefix.Length)); + } + } + + /// + /// Indicates whether this notification originated from a keyevent notification, for example __keyevent@4__:set with payload mykey. + /// + public bool IsKeyEvent + { + get + { + var span = _channel.Span; + return span.Length >= KeyEventPrefix.Length + MinSuffixBytes && KeyEventPrefix.Is(span.Hash64(), span.Slice(0, KeyEventPrefix.Length)); + } + } + + /// + /// Indicates whether the key associated with this notification starts with the specified prefix. + /// + /// This API is intended as a high-throughput filter API. + public bool KeyStartsWith(ReadOnlySpan prefix) // intentionally leading people to the BLOB API + { + if (IsKeySpace) + { + return ChannelSuffix.Slice(_keyOffset).StartsWith(prefix); + } + + if (IsKeyEvent) + { + if (_keyOffset == 0) return _value.StartsWith(prefix); + + var span = GetKeySpan(out var lease, stackalloc byte[128]); + bool result = span.StartsWith(prefix); + Return(lease); + return result; + } + + return false; + } +} + +internal static partial class KeyNotificationChannels +{ + [FastHash("__keyspace@")] + internal static partial class KeySpacePrefix + { + } + + [FastHash("__keyevent@")] + internal static partial class KeyEventPrefix + { + } +} diff --git a/src/StackExchange.Redis/KeyNotificationType.cs b/src/StackExchange.Redis/KeyNotificationType.cs new file mode 100644 index 000000000..cc4c74ef1 --- /dev/null +++ b/src/StackExchange.Redis/KeyNotificationType.cs @@ -0,0 +1,69 @@ +namespace StackExchange.Redis; + +/// +/// The type of keyspace or keyevent notification. +/// +public enum KeyNotificationType +{ + // note: initially presented alphabetically, but: new values *must* be appended, not inserted + // (to preserve values of existing elements) +#pragma warning disable CS1591 // docs, redundant + Unknown = 0, + Append = 1, + Copy = 2, + Del = 3, + Expire = 4, + HDel = 5, + HExpired = 6, + HIncrByFloat = 7, + HIncrBy = 8, + HPersist = 9, + HSet = 10, + IncrByFloat = 11, + IncrBy = 12, + LInsert = 13, + LPop = 14, + LPush = 15, + LRem = 16, + LSet = 17, + LTrim = 18, + MoveFrom = 19, + MoveTo = 20, + Persist = 21, + RenameFrom = 22, + RenameTo = 23, + Restore = 24, + RPop = 25, + RPush = 26, + SAdd = 27, + Set = 28, + SetRange = 29, + SortStore = 30, + SRem = 31, + SPop = 32, + XAdd = 33, + XDel = 34, + XGroupCreateConsumer = 35, + XGroupCreate = 36, + XGroupDelConsumer = 37, + XGroupDestroy = 38, + XGroupSetId = 39, + XSetId = 40, + XTrim = 41, + ZAdd = 42, + ZDiffStore = 43, + ZInterStore = 44, + ZUnionStore = 45, + ZIncr = 46, + ZRemByRank = 47, + ZRemByScore = 48, + ZRem = 49, + + // side-effect notifications + Expired = 1000, + Evicted = 1001, + New = 1002, + Overwritten = 1003, + TypeChanged = 1004, // type_changed +#pragma warning restore CS1591 // docs, redundant +} diff --git a/src/StackExchange.Redis/KeyNotificationTypeFastHash.cs b/src/StackExchange.Redis/KeyNotificationTypeFastHash.cs new file mode 100644 index 000000000..bcf08bad2 --- /dev/null +++ b/src/StackExchange.Redis/KeyNotificationTypeFastHash.cs @@ -0,0 +1,413 @@ +using System; + +namespace StackExchange.Redis; + +/// +/// Internal helper type for fast parsing of key notification types, using [FastHash]. +/// +internal static partial class KeyNotificationTypeFastHash +{ + // these are checked by KeyNotificationTypeFastHash_MinMaxBytes_ReflectsActualLengths + public const int MinBytes = 3, MaxBytes = 21; + + public static KeyNotificationType Parse(ReadOnlySpan value) + { + var hash = value.Hash64(); + return hash switch + { + append.Hash when append.Is(hash, value) => KeyNotificationType.Append, + copy.Hash when copy.Is(hash, value) => KeyNotificationType.Copy, + del.Hash when del.Is(hash, value) => KeyNotificationType.Del, + expire.Hash when expire.Is(hash, value) => KeyNotificationType.Expire, + hdel.Hash when hdel.Is(hash, value) => KeyNotificationType.HDel, + hexpired.Hash when hexpired.Is(hash, value) => KeyNotificationType.HExpired, + hincrbyfloat.Hash when hincrbyfloat.Is(hash, value) => KeyNotificationType.HIncrByFloat, + hincrby.Hash when hincrby.Is(hash, value) => KeyNotificationType.HIncrBy, + hpersist.Hash when hpersist.Is(hash, value) => KeyNotificationType.HPersist, + hset.Hash when hset.Is(hash, value) => KeyNotificationType.HSet, + incrbyfloat.Hash when incrbyfloat.Is(hash, value) => KeyNotificationType.IncrByFloat, + incrby.Hash when incrby.Is(hash, value) => KeyNotificationType.IncrBy, + linsert.Hash when linsert.Is(hash, value) => KeyNotificationType.LInsert, + lpop.Hash when lpop.Is(hash, value) => KeyNotificationType.LPop, + lpush.Hash when lpush.Is(hash, value) => KeyNotificationType.LPush, + lrem.Hash when lrem.Is(hash, value) => KeyNotificationType.LRem, + lset.Hash when lset.Is(hash, value) => KeyNotificationType.LSet, + ltrim.Hash when ltrim.Is(hash, value) => KeyNotificationType.LTrim, + move_from.Hash when move_from.Is(hash, value) => KeyNotificationType.MoveFrom, + move_to.Hash when move_to.Is(hash, value) => KeyNotificationType.MoveTo, + persist.Hash when persist.Is(hash, value) => KeyNotificationType.Persist, + rename_from.Hash when rename_from.Is(hash, value) => KeyNotificationType.RenameFrom, + rename_to.Hash when rename_to.Is(hash, value) => KeyNotificationType.RenameTo, + restore.Hash when restore.Is(hash, value) => KeyNotificationType.Restore, + rpop.Hash when rpop.Is(hash, value) => KeyNotificationType.RPop, + rpush.Hash when rpush.Is(hash, value) => KeyNotificationType.RPush, + sadd.Hash when sadd.Is(hash, value) => KeyNotificationType.SAdd, + set.Hash when set.Is(hash, value) => KeyNotificationType.Set, + setrange.Hash when setrange.Is(hash, value) => KeyNotificationType.SetRange, + sortstore.Hash when sortstore.Is(hash, value) => KeyNotificationType.SortStore, + srem.Hash when srem.Is(hash, value) => KeyNotificationType.SRem, + spop.Hash when spop.Is(hash, value) => KeyNotificationType.SPop, + xadd.Hash when xadd.Is(hash, value) => KeyNotificationType.XAdd, + xdel.Hash when xdel.Is(hash, value) => KeyNotificationType.XDel, + xgroup_createconsumer.Hash when xgroup_createconsumer.Is(hash, value) => KeyNotificationType.XGroupCreateConsumer, + xgroup_create.Hash when xgroup_create.Is(hash, value) => KeyNotificationType.XGroupCreate, + xgroup_delconsumer.Hash when xgroup_delconsumer.Is(hash, value) => KeyNotificationType.XGroupDelConsumer, + xgroup_destroy.Hash when xgroup_destroy.Is(hash, value) => KeyNotificationType.XGroupDestroy, + xgroup_setid.Hash when xgroup_setid.Is(hash, value) => KeyNotificationType.XGroupSetId, + xsetid.Hash when xsetid.Is(hash, value) => KeyNotificationType.XSetId, + xtrim.Hash when xtrim.Is(hash, value) => KeyNotificationType.XTrim, + zadd.Hash when zadd.Is(hash, value) => KeyNotificationType.ZAdd, + zdiffstore.Hash when zdiffstore.Is(hash, value) => KeyNotificationType.ZDiffStore, + zinterstore.Hash when zinterstore.Is(hash, value) => KeyNotificationType.ZInterStore, + zunionstore.Hash when zunionstore.Is(hash, value) => KeyNotificationType.ZUnionStore, + zincr.Hash when zincr.Is(hash, value) => KeyNotificationType.ZIncr, + zrembyrank.Hash when zrembyrank.Is(hash, value) => KeyNotificationType.ZRemByRank, + zrembyscore.Hash when zrembyscore.Is(hash, value) => KeyNotificationType.ZRemByScore, + zrem.Hash when zrem.Is(hash, value) => KeyNotificationType.ZRem, + expired.Hash when expired.Is(hash, value) => KeyNotificationType.Expired, + evicted.Hash when evicted.Is(hash, value) => KeyNotificationType.Evicted, + _new.Hash when _new.Is(hash, value) => KeyNotificationType.New, + overwritten.Hash when overwritten.Is(hash, value) => KeyNotificationType.Overwritten, + type_changed.Hash when type_changed.Is(hash, value) => KeyNotificationType.TypeChanged, + _ => KeyNotificationType.Unknown, + }; + } + + internal static ReadOnlySpan GetRawBytes(KeyNotificationType type) + { + return type switch + { + KeyNotificationType.Append => append.U8, + KeyNotificationType.Copy => copy.U8, + KeyNotificationType.Del => del.U8, + KeyNotificationType.Expire => expire.U8, + KeyNotificationType.HDel => hdel.U8, + KeyNotificationType.HExpired => hexpired.U8, + KeyNotificationType.HIncrByFloat => hincrbyfloat.U8, + KeyNotificationType.HIncrBy => hincrby.U8, + KeyNotificationType.HPersist => hpersist.U8, + KeyNotificationType.HSet => hset.U8, + KeyNotificationType.IncrByFloat => incrbyfloat.U8, + KeyNotificationType.IncrBy => incrby.U8, + KeyNotificationType.LInsert => linsert.U8, + KeyNotificationType.LPop => lpop.U8, + KeyNotificationType.LPush => lpush.U8, + KeyNotificationType.LRem => lrem.U8, + KeyNotificationType.LSet => lset.U8, + KeyNotificationType.LTrim => ltrim.U8, + KeyNotificationType.MoveFrom => move_from.U8, + KeyNotificationType.MoveTo => move_to.U8, + KeyNotificationType.Persist => persist.U8, + KeyNotificationType.RenameFrom => rename_from.U8, + KeyNotificationType.RenameTo => rename_to.U8, + KeyNotificationType.Restore => restore.U8, + KeyNotificationType.RPop => rpop.U8, + KeyNotificationType.RPush => rpush.U8, + KeyNotificationType.SAdd => sadd.U8, + KeyNotificationType.Set => set.U8, + KeyNotificationType.SetRange => setrange.U8, + KeyNotificationType.SortStore => sortstore.U8, + KeyNotificationType.SRem => srem.U8, + KeyNotificationType.SPop => spop.U8, + KeyNotificationType.XAdd => xadd.U8, + KeyNotificationType.XDel => xdel.U8, + KeyNotificationType.XGroupCreateConsumer => xgroup_createconsumer.U8, + KeyNotificationType.XGroupCreate => xgroup_create.U8, + KeyNotificationType.XGroupDelConsumer => xgroup_delconsumer.U8, + KeyNotificationType.XGroupDestroy => xgroup_destroy.U8, + KeyNotificationType.XGroupSetId => xgroup_setid.U8, + KeyNotificationType.XSetId => xsetid.U8, + KeyNotificationType.XTrim => xtrim.U8, + KeyNotificationType.ZAdd => zadd.U8, + KeyNotificationType.ZDiffStore => zdiffstore.U8, + KeyNotificationType.ZInterStore => zinterstore.U8, + KeyNotificationType.ZUnionStore => zunionstore.U8, + KeyNotificationType.ZIncr => zincr.U8, + KeyNotificationType.ZRemByRank => zrembyrank.U8, + KeyNotificationType.ZRemByScore => zrembyscore.U8, + KeyNotificationType.ZRem => zrem.U8, + KeyNotificationType.Expired => expired.U8, + KeyNotificationType.Evicted => evicted.U8, + KeyNotificationType.New => _new.U8, + KeyNotificationType.Overwritten => overwritten.U8, + KeyNotificationType.TypeChanged => type_changed.U8, + _ => Throw(), + }; + static ReadOnlySpan Throw() => throw new ArgumentOutOfRangeException(nameof(type)); + } + +#pragma warning disable SA1300, CS8981 + // ReSharper disable InconsistentNaming + [FastHash] + internal static partial class append + { + } + + [FastHash] + internal static partial class copy + { + } + + [FastHash] + internal static partial class del + { + } + + [FastHash] + internal static partial class expire + { + } + + [FastHash] + internal static partial class hdel + { + } + + [FastHash] + internal static partial class hexpired + { + } + + [FastHash] + internal static partial class hincrbyfloat + { + } + + [FastHash] + internal static partial class hincrby + { + } + + [FastHash] + internal static partial class hpersist + { + } + + [FastHash] + internal static partial class hset + { + } + + [FastHash] + internal static partial class incrbyfloat + { + } + + [FastHash] + internal static partial class incrby + { + } + + [FastHash] + internal static partial class linsert + { + } + + [FastHash] + internal static partial class lpop + { + } + + [FastHash] + internal static partial class lpush + { + } + + [FastHash] + internal static partial class lrem + { + } + + [FastHash] + internal static partial class lset + { + } + + [FastHash] + internal static partial class ltrim + { + } + + [FastHash("move_from")] // by default, the generator interprets underscore as hyphen + internal static partial class move_from + { + } + + [FastHash("move_to")] // by default, the generator interprets underscore as hyphen + internal static partial class move_to + { + } + + [FastHash] + internal static partial class persist + { + } + + [FastHash("rename_from")] // by default, the generator interprets underscore as hyphen + internal static partial class rename_from + { + } + + [FastHash("rename_to")] // by default, the generator interprets underscore as hyphen + internal static partial class rename_to + { + } + + [FastHash] + internal static partial class restore + { + } + + [FastHash] + internal static partial class rpop + { + } + + [FastHash] + internal static partial class rpush + { + } + + [FastHash] + internal static partial class sadd + { + } + + [FastHash] + internal static partial class set + { + } + + [FastHash] + internal static partial class setrange + { + } + + [FastHash] + internal static partial class sortstore + { + } + + [FastHash] + internal static partial class srem + { + } + + [FastHash] + internal static partial class spop + { + } + + [FastHash] + internal static partial class xadd + { + } + + [FastHash] + internal static partial class xdel + { + } + + [FastHash] // note: becomes hyphenated + internal static partial class xgroup_createconsumer + { + } + + [FastHash] // note: becomes hyphenated + internal static partial class xgroup_create + { + } + + [FastHash] // note: becomes hyphenated + internal static partial class xgroup_delconsumer + { + } + + [FastHash] // note: becomes hyphenated + internal static partial class xgroup_destroy + { + } + + [FastHash] // note: becomes hyphenated + internal static partial class xgroup_setid + { + } + + [FastHash] + internal static partial class xsetid + { + } + + [FastHash] + internal static partial class xtrim + { + } + + [FastHash] + internal static partial class zadd + { + } + + [FastHash] + internal static partial class zdiffstore + { + } + + [FastHash] + internal static partial class zinterstore + { + } + + [FastHash] + internal static partial class zunionstore + { + } + + [FastHash] + internal static partial class zincr + { + } + + [FastHash] + internal static partial class zrembyrank + { + } + + [FastHash] + internal static partial class zrembyscore + { + } + + [FastHash] + internal static partial class zrem + { + } + + [FastHash] + internal static partial class expired + { + } + + [FastHash] + internal static partial class evicted + { + } + + [FastHash("new")] + internal static partial class _new // it isn't worth making the code-gen keyword aware + { + } + + [FastHash] + internal static partial class overwritten + { + } + + [FastHash("type_changed")] // by default, the generator interprets underscore as hyphen + internal static partial class type_changed + { + } + + // ReSharper restore InconsistentNaming +#pragma warning restore SA1300, CS8981 +} diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 386d426d8..37472fd4c 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -890,7 +890,8 @@ protected CommandKeyBase(int db, CommandFlags flags, RedisCommand command, in Re private sealed class CommandChannelMessage : CommandChannelBase { - public CommandChannelMessage(int db, CommandFlags flags, RedisCommand command, in RedisChannel channel) : base(db, flags, command, channel) + public CommandChannelMessage(int db, CommandFlags flags, RedisCommand command, in RedisChannel channel) + : base(db, flags, command, channel) { } protected override void WriteImpl(PhysicalConnection physical) { @@ -903,7 +904,8 @@ protected override void WriteImpl(PhysicalConnection physical) private sealed class CommandChannelValueMessage : CommandChannelBase { private readonly RedisValue value; - public CommandChannelValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisChannel channel, in RedisValue value) : base(db, flags, command, channel) + public CommandChannelValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisChannel channel, in RedisValue value) + : base(db, flags, command, channel) { value.AssertNotNull(); this.value = value; @@ -1746,7 +1748,8 @@ protected override void WriteImpl(PhysicalConnection physical) private sealed class CommandValueChannelMessage : CommandChannelBase { private readonly RedisValue value; - public CommandValueChannelMessage(int db, CommandFlags flags, RedisCommand command, in RedisValue value, in RedisChannel channel) : base(db, flags, command, channel) + public CommandValueChannelMessage(int db, CommandFlags flags, RedisCommand command, in RedisValue value, in RedisChannel channel) + : base(db, flags, command, channel) { value.AssertNotNull(); this.value = value; diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 57bcd608d..857902f48 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -90,7 +90,7 @@ public PhysicalConnection(PhysicalBridge bridge) lastBeatTickCount = 0; connectionType = bridge.ConnectionType; _bridge = new WeakReference(bridge); - ChannelPrefix = bridge.Multiplexer.RawConfig.ChannelPrefix; + ChannelPrefix = bridge.Multiplexer.ChannelPrefix; if (ChannelPrefix?.Length == 0) ChannelPrefix = null; // null tests are easier than null+empty var endpoint = bridge.ServerEndPoint.EndPoint; _physicalName = connectionType + "#" + Interlocked.Increment(ref totalCount) + "@" + Format.ToString(endpoint); @@ -820,7 +820,7 @@ internal void Write(in RedisKey key) } internal void Write(in RedisChannel channel) - => WriteUnifiedPrefixedBlob(_ioPipe?.Output, ChannelPrefix, channel.Value); + => WriteUnifiedPrefixedBlob(_ioPipe?.Output, channel.IgnoreChannelPrefix ? null : ChannelPrefix, channel.Value); [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void WriteBulkString(in RedisValue value) @@ -1999,7 +1999,7 @@ static bool TryGetMultiPubSubPayload(in RawResult value, out Sequence } } - private bool PeekChannelMessage(RedisCommand command, RedisChannel channel) + private bool PeekChannelMessage(RedisCommand command, in RedisChannel channel) { Message? msg; bool haveMsg; diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 91b0e1a43..6e96ed550 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,85 @@ -#nullable enable \ No newline at end of file +#nullable enable +StackExchange.Redis.ChannelMessage.TryParseKeyNotification(System.ReadOnlySpan keyPrefix, out StackExchange.Redis.KeyNotification notification) -> bool +StackExchange.Redis.KeyNotification +StackExchange.Redis.KeyNotification.GetChannel() -> StackExchange.Redis.RedisChannel +StackExchange.Redis.KeyNotification.GetKeyByteCount() -> int +StackExchange.Redis.KeyNotification.GetKeyCharCount() -> int +StackExchange.Redis.KeyNotification.GetKeyMaxByteCount() -> int +StackExchange.Redis.KeyNotification.GetKeyMaxCharCount() -> int +StackExchange.Redis.KeyNotification.GetValue() -> StackExchange.Redis.RedisValue +StackExchange.Redis.KeyNotification.IsType(System.ReadOnlySpan type) -> bool +StackExchange.Redis.KeyNotification.KeyStartsWith(System.ReadOnlySpan prefix) -> bool +StackExchange.Redis.KeyNotification.TryCopyKey(System.Span destination, out int charsWritten) -> bool +StackExchange.Redis.KeyNotification.Database.get -> int +StackExchange.Redis.KeyNotification.GetKey() -> StackExchange.Redis.RedisKey +StackExchange.Redis.KeyNotification.IsKeyEvent.get -> bool +StackExchange.Redis.KeyNotification.IsKeySpace.get -> bool +StackExchange.Redis.KeyNotification.KeyNotification() -> void +StackExchange.Redis.KeyNotification.TryCopyKey(System.Span destination, out int bytesWritten) -> bool +StackExchange.Redis.KeyNotification.Type.get -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.RedisValue.StartsWith(System.ReadOnlySpan value) -> bool +static StackExchange.Redis.KeyNotification.TryParse(scoped in StackExchange.Redis.RedisChannel channel, scoped in StackExchange.Redis.RedisValue value, out StackExchange.Redis.KeyNotification notification) -> bool +StackExchange.Redis.ChannelMessage.TryParseKeyNotification(out StackExchange.Redis.KeyNotification notification) -> bool +static StackExchange.Redis.KeyNotification.TryParse(scoped in System.ReadOnlySpan keyPrefix, scoped in StackExchange.Redis.RedisChannel channel, scoped in StackExchange.Redis.RedisValue value, out StackExchange.Redis.KeyNotification notification) -> bool +static StackExchange.Redis.RedisChannel.KeyEvent(StackExchange.Redis.KeyNotificationType type, int? database = null) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.KeyEvent(System.ReadOnlySpan type, int? database) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.KeySpacePattern(in StackExchange.Redis.RedisKey pattern, int? database = null) -> StackExchange.Redis.RedisChannel +StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Append = 1 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Copy = 2 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Del = 3 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Evicted = 1001 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Expire = 4 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Expired = 1000 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.HDel = 5 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.HExpired = 6 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.HIncrBy = 8 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.HIncrByFloat = 7 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.HPersist = 9 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.HSet = 10 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.IncrBy = 12 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.IncrByFloat = 11 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.LInsert = 13 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.LPop = 14 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.LPush = 15 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.LRem = 16 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.LSet = 17 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.LTrim = 18 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.MoveFrom = 19 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.MoveTo = 20 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.New = 1002 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Overwritten = 1003 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Persist = 21 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.RenameFrom = 22 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.RenameTo = 23 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Restore = 24 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.RPop = 25 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.RPush = 26 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.SAdd = 27 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Set = 28 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.SetRange = 29 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.SortStore = 30 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.SPop = 32 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.SRem = 31 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.TypeChanged = 1004 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Unknown = 0 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XAdd = 33 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XDel = 34 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XGroupCreate = 36 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XGroupCreateConsumer = 35 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XGroupDelConsumer = 37 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XGroupDestroy = 38 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XGroupSetId = 39 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XSetId = 40 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XTrim = 41 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZAdd = 42 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZDiffStore = 43 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZIncr = 46 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZInterStore = 44 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZRem = 49 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZRemByRank = 47 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZRemByScore = 48 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZUnionStore = 45 -> StackExchange.Redis.KeyNotificationType +static StackExchange.Redis.RedisChannel.KeySpacePrefix(in StackExchange.Redis.RedisKey prefix, int? database = null) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.KeySpacePrefix(System.ReadOnlySpan prefix, int? database = null) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.KeySpaceSingleKey(in StackExchange.Redis.RedisKey key, int database) -> StackExchange.Redis.RedisChannel diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index 1ac9f081a..e1c91b74e 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -161,22 +161,34 @@ public bool MoveNext() } public ReadOnlySequence Current { get; private set; } } + internal RedisChannel AsRedisChannel(byte[]? channelPrefix, RedisChannel.RedisChannelOptions options) { switch (Resp2TypeBulkString) { case ResultType.SimpleString: case ResultType.BulkString: - if (channelPrefix == null) + if (channelPrefix is null) { + // no channel-prefix enabled, just use as-is return new RedisChannel(GetBlob(), options); } if (StartsWith(channelPrefix)) { + // we have a channel-prefix, and it matches; strip it byte[] copy = Payload.Slice(channelPrefix.Length).ToArray(); return new RedisChannel(copy, options); } + + // we shouldn't get unexpected events, so to get here: we've received a notification + // on a channel that doesn't match our prefix; this *should* be limited to + // key notifications (see: IgnoreChannelPrefix), but: we need to be sure + if (StartsWith("__keyspace@"u8) || StartsWith("__keyevent@"u8)) + { + // use as-is + return new RedisChannel(GetBlob(), options); + } return default; default: throw new InvalidCastException("Cannot convert to RedisChannel: " + Resp3Type); @@ -270,9 +282,8 @@ internal bool StartsWith(in CommandBytes expected) var rangeToCheck = Payload.Slice(0, len); return new CommandBytes(rangeToCheck).Equals(expected); } - internal bool StartsWith(byte[] expected) + internal bool StartsWith(ReadOnlySpan expected) { - if (expected == null) throw new ArgumentNullException(nameof(expected)); if (expected.Length > Payload.Length) return false; var rangeToCheck = Payload.Slice(0, expected.Length); @@ -282,7 +293,7 @@ internal bool StartsWith(byte[] expected) foreach (var segment in rangeToCheck) { var from = segment.Span; - var to = new Span(expected, offset, from.Length); + var to = expected.Slice(offset, from.Length); if (!from.SequenceEqual(to)) return false; offset += from.Length; diff --git a/src/StackExchange.Redis/RedisChannel.cs b/src/StackExchange.Redis/RedisChannel.cs index d4289f3c6..889525bd2 100644 --- a/src/StackExchange.Redis/RedisChannel.cs +++ b/src/StackExchange.Redis/RedisChannel.cs @@ -1,4 +1,7 @@ using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Text; namespace StackExchange.Redis @@ -10,6 +13,36 @@ namespace StackExchange.Redis { internal readonly byte[]? Value; + internal ReadOnlySpan Span => Value is null ? default : Value.AsSpan(); + + internal ReadOnlySpan RoutingSpan + { + get + { + var span = Span; + if ((Options & (RedisChannelOptions.KeyRouted | RedisChannelOptions.IgnoreChannelPrefix | + RedisChannelOptions.Sharded | RedisChannelOptions.MultiNode | RedisChannelOptions.Pattern)) + == (RedisChannelOptions.KeyRouted | RedisChannelOptions.IgnoreChannelPrefix)) + { + // this *could* be a single-key __keyspace@{db}__:{key} subscription, in which case we want to use the key + // part for routing, but to avoid overhead we'll only even look if the channel starts with an underscore + if (span.Length >= 16 && span[0] == (byte)'_') span = StripKeySpacePrefix(span); + } + return span; + } + } + + internal static ReadOnlySpan StripKeySpacePrefix(ReadOnlySpan span) + { + if (span.Length >= 16 && span.StartsWith("__keyspace@"u8)) + { + var subspan = span.Slice(12); + int end = subspan.IndexOf("__:"u8); + if (end >= 0) return subspan.Slice(end + 3); + } + return span; + } + internal readonly RedisChannelOptions Options; [Flags] @@ -19,19 +52,42 @@ internal enum RedisChannelOptions Pattern = 1 << 0, Sharded = 1 << 1, KeyRouted = 1 << 2, + MultiNode = 1 << 3, + IgnoreChannelPrefix = 1 << 4, } // we don't consider Routed for equality - it's an implementation detail, not a fundamental feature - private const RedisChannelOptions EqualityMask = ~RedisChannelOptions.KeyRouted; + private const RedisChannelOptions EqualityMask = + ~(RedisChannelOptions.KeyRouted | RedisChannelOptions.MultiNode | RedisChannelOptions.IgnoreChannelPrefix); - internal RedisCommand PublishCommand => IsSharded ? RedisCommand.SPUBLISH : RedisCommand.PUBLISH; + internal RedisCommand GetPublishCommand() + { + return (Options & (RedisChannelOptions.Sharded | RedisChannelOptions.MultiNode)) switch + { + RedisChannelOptions.None => RedisCommand.PUBLISH, + RedisChannelOptions.Sharded => RedisCommand.SPUBLISH, + _ => ThrowKeyRouted(), + }; + + static RedisCommand ThrowKeyRouted() => throw new InvalidOperationException("Publishing is not supported for multi-node channels"); + } /// - /// Should we use cluster routing for this channel? This applies *either* to sharded (SPUBLISH) scenarios, + /// Should we use cluster routing for this channel? This applies *either* to sharded (SPUBLISH) scenarios, /// or to scenarios using . /// internal bool IsKeyRouted => (Options & RedisChannelOptions.KeyRouted) != 0; + /// + /// Should this channel be subscribed to on all nodes? This is only relevant for cluster scenarios and keyspace notifications. + /// + internal bool IsMultiNode => (Options & RedisChannelOptions.MultiNode) != 0; + + /// + /// Should the channel prefix be ignored when writing this channel. + /// + internal bool IgnoreChannelPrefix => (Options & RedisChannelOptions.IgnoreChannelPrefix) != 0; + /// /// Indicates whether the channel-name is either null or a zero-length value. /// @@ -58,6 +114,7 @@ public static bool UseImplicitAutoPattern get => s_DefaultPatternMode == PatternMode.Auto; set => s_DefaultPatternMode = value ? PatternMode.Auto : PatternMode.Literal; } + private static PatternMode s_DefaultPatternMode = PatternMode.Auto; /// @@ -82,7 +139,13 @@ public static bool UseImplicitAutoPattern /// a consideration. /// /// Note that channels from Sharded are always routed. - public RedisChannel WithKeyRouting() => new(Value, Options | RedisChannelOptions.KeyRouted); + public RedisChannel WithKeyRouting() + { + if (IsMultiNode) Throw(); + return new(Value, Options | RedisChannelOptions.KeyRouted); + + static void Throw() => throw new InvalidOperationException("Key routing is not supported for multi-node channels"); + } /// /// Creates a new that acts as a wildcard subscription. In cluster @@ -105,7 +168,8 @@ public static bool UseImplicitAutoPattern /// /// The name of the channel to create. /// The mode for name matching. - public RedisChannel(byte[]? value, PatternMode mode) : this(value, DeterminePatternBased(value, mode) ? RedisChannelOptions.Pattern : RedisChannelOptions.None) + public RedisChannel(byte[]? value, PatternMode mode) : this( + value, DeterminePatternBased(value, mode) ? RedisChannelOptions.Pattern : RedisChannelOptions.None) { } @@ -115,7 +179,9 @@ public RedisChannel(byte[]? value, PatternMode mode) : this(value, DeterminePatt /// The string name of the channel to create. /// The mode for name matching. // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - public RedisChannel(string value, PatternMode mode) : this(value is null ? null : Encoding.UTF8.GetBytes(value), mode) + public RedisChannel(string value, PatternMode mode) : this( + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + value is null ? null : Encoding.UTF8.GetBytes(value), mode) { } @@ -128,7 +194,8 @@ public RedisChannel(string value, PatternMode mode) : this(value is null ? null /// The name of the channel to create. /// Note that sharded subscriptions are completely separate to regular subscriptions; subscriptions /// using sharded channels must also be published with sharded channels (and vice versa). - public static RedisChannel Sharded(byte[]? value) => new(value, RedisChannelOptions.Sharded | RedisChannelOptions.KeyRouted); + public static RedisChannel Sharded(byte[]? value) => + new(value, RedisChannelOptions.Sharded | RedisChannelOptions.KeyRouted); /// /// Create a new redis channel from a string, representing a sharded channel. In cluster @@ -139,7 +206,134 @@ public RedisChannel(string value, PatternMode mode) : this(value is null ? null /// The string name of the channel to create. /// Note that sharded subscriptions are completely separate to regular subscriptions; subscriptions /// using sharded channels must also be published with sharded channels (and vice versa). - public static RedisChannel Sharded(string value) => new(value, RedisChannelOptions.Sharded | RedisChannelOptions.KeyRouted); + public static RedisChannel Sharded(string value) => + new(value, RedisChannelOptions.Sharded | RedisChannelOptions.KeyRouted); + + /// + /// Create a key-notification channel for a single key in a single database. + /// + public static RedisChannel KeySpaceSingleKey(in RedisKey key, int database) + // note we can allow patterns, because we aren't using PSUBSCRIBE + => BuildKeySpaceChannel(key, database, RedisChannelOptions.KeyRouted, default, false, true); + + /// + /// Create a key-notification channel for a pattern, optionally in a specified database. + /// + public static RedisChannel KeySpacePattern(in RedisKey pattern, int? database = null) + => BuildKeySpaceChannel(pattern, database, RedisChannelOptions.Pattern | RedisChannelOptions.MultiNode, default, appendStar: pattern.IsNull, allowKeyPatterns: true); + +#pragma warning disable RS0026 // competing overloads - disambiguated via OverloadResolutionPriority + /// + /// Create a key-notification channel using a raw prefix, optionally in a specified database. + /// + public static RedisChannel KeySpacePrefix(in RedisKey prefix, int? database = null) + { + if (prefix.IsEmpty) Throw(); + return BuildKeySpaceChannel(prefix, database, RedisChannelOptions.Pattern | RedisChannelOptions.MultiNode, default, true, false); + static void Throw() => throw new ArgumentNullException(nameof(prefix)); + } + + /// + /// Create a key-notification channel using a raw prefix, optionally in a specified database. + /// + [OverloadResolutionPriority(1)] + public static RedisChannel KeySpacePrefix(ReadOnlySpan prefix, int? database = null) + { + if (prefix.IsEmpty) Throw(); + return BuildKeySpaceChannel(RedisKey.Null, database, RedisChannelOptions.Pattern | RedisChannelOptions.MultiNode, prefix, true, false); + static void Throw() => throw new ArgumentNullException(nameof(prefix)); + } +#pragma warning restore RS0026 // competing overloads - disambiguated via OverloadResolutionPriority + + private const int DatabaseScratchBufferSize = 16; // largest non-negative int32 is 10 digits + + private static ReadOnlySpan AppendDatabase(Span target, int? database, RedisChannelOptions options) + { + if (database is null) + { + if ((options & RedisChannelOptions.Pattern) == 0) throw new ArgumentNullException(nameof(database)); + return "*"u8; // don't worry about the inbound scratch buffer, this is fine + } + else + { + var db32 = database.GetValueOrDefault(); + if (db32 == 0) return "0"u8; // so common, we might as well special case + if (db32 < 0) throw new ArgumentOutOfRangeException(nameof(database)); + return target.Slice(0, Format.FormatInt32(db32, target)); + } + } + + /// + /// Create an event-notification channel for a given event type, optionally in a specified database. + /// +#pragma warning disable RS0027 + public static RedisChannel KeyEvent(KeyNotificationType type, int? database = null) +#pragma warning restore RS0027 + => KeyEvent(KeyNotificationTypeFastHash.GetRawBytes(type), database); + + /// + /// Create an event-notification channel for a given event type, optionally in a specified database. + /// + /// This API is intended for use with custom/unknown event types; for well-known types, use . + public static RedisChannel KeyEvent(ReadOnlySpan type, int? database) + { + if (type.IsEmpty) throw new ArgumentNullException(nameof(type)); + + RedisChannelOptions options = RedisChannelOptions.MultiNode; + if (database is null) options |= RedisChannelOptions.Pattern; + var db = AppendDatabase(stackalloc byte[DatabaseScratchBufferSize], database, options); + + // __keyevent@{db}__:{type} + var arr = new byte[14 + db.Length + type.Length]; + + var target = AppendAndAdvance(arr.AsSpan(), "__keyevent@"u8); + target = AppendAndAdvance(target, db); + target = AppendAndAdvance(target, "__:"u8); + target = AppendAndAdvance(target, type); + Debug.Assert(target.IsEmpty); // should have calculated length correctly + + return new RedisChannel(arr, options | RedisChannelOptions.IgnoreChannelPrefix); + } + + private static Span AppendAndAdvance(Span target, scoped ReadOnlySpan value) + { + value.CopyTo(target); + return target.Slice(value.Length); + } + + private static RedisChannel BuildKeySpaceChannel(in RedisKey key, int? database, RedisChannelOptions options, ReadOnlySpan suffix, bool appendStar, bool allowKeyPatterns) + { + int fullKeyLength = key.TotalLength() + suffix.Length + (appendStar ? 1 : 0); + if (appendStar & (options & RedisChannelOptions.Pattern) == 0) throw new ArgumentNullException(nameof(key)); + if (fullKeyLength == 0) throw new ArgumentOutOfRangeException(nameof(key)); + + var db = AppendDatabase(stackalloc byte[DatabaseScratchBufferSize], database, options); + + // __keyspace@{db}__:{key}[*] + var arr = new byte[14 + db.Length + fullKeyLength]; + + var target = AppendAndAdvance(arr.AsSpan(), "__keyspace@"u8); + target = AppendAndAdvance(target, db); + target = AppendAndAdvance(target, "__:"u8); + var keySpan = target; // remember this for if we need to check for patterns + var keyLen = key.CopyTo(target); + target = target.Slice(keyLen); + target = AppendAndAdvance(target, suffix); + if (!allowKeyPatterns) + { + keySpan = keySpan.Slice(0, keyLen + suffix.Length); + if (keySpan.IndexOfAny((byte)'*', (byte)'?', (byte)'[') >= 0) ThrowPattern(); + } + if (appendStar) + { + target[0] = (byte)'*'; + target = target.Slice(1); + } + Debug.Assert(target.IsEmpty, "length calculated incorrectly"); + return new RedisChannel(arr, options | RedisChannelOptions.IgnoreChannelPrefix); + + static void ThrowPattern() => throw new ArgumentException("The supplied key contains pattern characters, but patterns are not supported in this context."); + } internal RedisChannel(byte[]? value, RedisChannelOptions options) { @@ -351,7 +545,7 @@ public static implicit operator RedisChannel(byte[]? key) { return Encoding.UTF8.GetString(arr); } - catch (Exception e) when // Only catch exception throwed by Encoding.UTF8.GetString + catch (Exception e) when // Only catch exception thrown by Encoding.UTF8.GetString (e is DecoderFallbackException or ArgumentException or ArgumentNullException) { return BitConverter.ToString(arr); diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index c1c3c5728..056a5380a 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -1900,7 +1900,7 @@ public Task StringLongestCommonSubsequenceWithMatchesAsync(Redis public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { if (channel.IsNullOrEmpty) throw new ArgumentNullException(nameof(channel)); - var msg = Message.Create(-1, flags, channel.PublishCommand, channel, message); + var msg = Message.Create(-1, flags, channel.GetPublishCommand(), channel, message); // if we're actively subscribed: send via that connection (otherwise, follow normal rules) return ExecuteSync(msg, ResultProcessor.Int64, server: multiplexer.GetSubscribedServer(channel)); } @@ -1908,7 +1908,7 @@ public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { if (channel.IsNullOrEmpty) throw new ArgumentNullException(nameof(channel)); - var msg = Message.Create(-1, flags, channel.PublishCommand, channel, message); + var msg = Message.Create(-1, flags, channel.GetPublishCommand(), channel, message); // if we're actively subscribed: send via that connection (otherwise, follow normal rules) return ExecuteAsync(msg, ResultProcessor.Int64, server: multiplexer.GetSubscribedServer(channel)); } diff --git a/src/StackExchange.Redis/RedisKey.cs b/src/StackExchange.Redis/RedisKey.cs index 0ee83d560..e18e0fb7c 100644 --- a/src/StackExchange.Redis/RedisKey.cs +++ b/src/StackExchange.Redis/RedisKey.cs @@ -395,6 +395,14 @@ internal int TotalLength() => _ => ((byte[])KeyValue).Length, }; + internal int MaxByteCount() => + (KeyPrefix is null ? 0 : KeyPrefix.Length) + KeyValue switch + { + null => 0, + string s => Encoding.UTF8.GetMaxByteCount(s.Length), + _ => ((byte[])KeyValue).Length, + }; + internal int CopyTo(Span destination) { int written = 0; diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index 9ade78c2d..ca66e6113 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -1,11 +1,8 @@ using System; using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.SymbolStore; using System.Net; -using System.Threading; using System.Threading.Tasks; -using Pipelines.Sockets.Unofficial; using Pipelines.Sockets.Unofficial.Arenas; using static StackExchange.Redis.ConnectionMultiplexer; @@ -30,7 +27,7 @@ internal Subscription GetOrAddSubscription(in RedisChannel channel, CommandFlags { if (!subscriptions.TryGetValue(channel, out var sub)) { - sub = new Subscription(flags); + sub = channel.IsMultiNode ? new MultiNodeSubscription(flags) : new SingleNodeSubscription(flags); subscriptions.TryAdd(channel, sub); } return sub; @@ -71,7 +68,7 @@ internal bool GetSubscriberCounts(in RedisChannel channel, out int handlers, out { if (!channel.IsNullOrEmpty && subscriptions.TryGetValue(channel, out Subscription? sub)) { - return sub.GetCurrentServer(); + return sub.GetAnyCurrentServer(); } return null; } @@ -123,7 +120,7 @@ internal void UpdateSubscriptions() { foreach (var pair in subscriptions) { - pair.Value.UpdateServer(); + pair.Value.RemoveDisconnectedEndpoints(); } } @@ -135,13 +132,10 @@ internal long EnsureSubscriptions(CommandFlags flags = CommandFlags.None) { // TODO: Subscribe with variadic commands to reduce round trips long count = 0; + var subscriber = DefaultSubscriber; foreach (var pair in subscriptions) { - if (!pair.Value.IsConnected) - { - count++; - DefaultSubscriber.EnsureSubscribedToServer(pair.Value, pair.Key, flags, true); - } + count += pair.Value.EnsureSubscribedToServer(subscriber, pair.Key, flags, true); } return count; } @@ -151,161 +145,6 @@ internal enum SubscriptionAction Subscribe, Unsubscribe, } - - /// - /// This is the record of a single subscription to a redis server. - /// It's the singular channel (which may or may not be a pattern), to one or more handlers. - /// We subscriber to a redis server once (for all messages) and execute 1-many handlers when a message arrives. - /// - internal sealed class Subscription - { - private Action? _handlers; - private readonly object _handlersLock = new object(); - private ChannelMessageQueue? _queues; - private ServerEndPoint? CurrentServer; - public CommandFlags Flags { get; } - public ResultProcessor.TrackSubscriptionsProcessor Processor { get; } - - /// - /// Whether the we have is connected. - /// Since we clear on a disconnect, this should stay correct. - /// - internal bool IsConnected => CurrentServer?.IsSubscriberConnected == true; - - public Subscription(CommandFlags flags) - { - Flags = flags; - Processor = new ResultProcessor.TrackSubscriptionsProcessor(this); - } - - /// - /// Gets the configured (P)SUBSCRIBE or (P)UNSUBSCRIBE for an action. - /// - internal Message GetMessage(RedisChannel channel, SubscriptionAction action, CommandFlags flags, bool internalCall) - { - var command = action switch // note that the Routed flag doesn't impact the message here - just the routing - { - SubscriptionAction.Subscribe => (channel.Options & ~RedisChannel.RedisChannelOptions.KeyRouted) switch - { - RedisChannel.RedisChannelOptions.None => RedisCommand.SUBSCRIBE, - RedisChannel.RedisChannelOptions.Pattern => RedisCommand.PSUBSCRIBE, - RedisChannel.RedisChannelOptions.Sharded => RedisCommand.SSUBSCRIBE, - _ => Unknown(action, channel.Options), - }, - SubscriptionAction.Unsubscribe => (channel.Options & ~RedisChannel.RedisChannelOptions.KeyRouted) switch - { - RedisChannel.RedisChannelOptions.None => RedisCommand.UNSUBSCRIBE, - RedisChannel.RedisChannelOptions.Pattern => RedisCommand.PUNSUBSCRIBE, - RedisChannel.RedisChannelOptions.Sharded => RedisCommand.SUNSUBSCRIBE, - _ => Unknown(action, channel.Options), - }, - _ => Unknown(action, channel.Options), - }; - - // TODO: Consider flags here - we need to pass Fire and Forget, but don't want to intermingle Primary/Replica - var msg = Message.Create(-1, Flags | flags, command, channel); - msg.SetForSubscriptionBridge(); - if (internalCall) - { - msg.SetInternalCall(); - } - return msg; - } - - private RedisCommand Unknown(SubscriptionAction action, RedisChannel.RedisChannelOptions options) - => throw new ArgumentException($"Unable to determine pub/sub operation for '{action}' against '{options}'"); - - public void Add(Action? handler, ChannelMessageQueue? queue) - { - if (handler != null) - { - lock (_handlersLock) - { - _handlers += handler; - } - } - if (queue != null) - { - ChannelMessageQueue.Combine(ref _queues, queue); - } - } - - public bool Remove(Action? handler, ChannelMessageQueue? queue) - { - if (handler != null) - { - lock (_handlersLock) - { - _handlers -= handler; - } - } - if (queue != null) - { - ChannelMessageQueue.Remove(ref _queues, queue); - } - return _handlers == null & _queues == null; - } - - public ICompletable? ForInvoke(in RedisChannel channel, in RedisValue message, out ChannelMessageQueue? queues) - { - var handlers = _handlers; - queues = Volatile.Read(ref _queues); - return handlers == null ? null : new MessageCompletable(channel, message, handlers); - } - - internal void MarkCompleted() - { - lock (_handlersLock) - { - _handlers = null; - } - ChannelMessageQueue.MarkAllCompleted(ref _queues); - } - - internal void GetSubscriberCounts(out int handlers, out int queues) - { - queues = ChannelMessageQueue.Count(ref _queues); - var tmp = _handlers; - if (tmp == null) - { - handlers = 0; - } - else if (tmp.IsSingle()) - { - handlers = 1; - } - else - { - handlers = 0; - foreach (var sub in tmp.AsEnumerable()) { handlers++; } - } - } - - internal ServerEndPoint? GetCurrentServer() => Volatile.Read(ref CurrentServer); - internal void SetCurrentServer(ServerEndPoint? server) => CurrentServer = server; - // conditional clear - internal bool ClearCurrentServer(ServerEndPoint expected) - { - if (CurrentServer == expected) - { - CurrentServer = null; - return true; - } - - return false; - } - - /// - /// Evaluates state and if we're not currently connected, clears the server reference. - /// - internal void UpdateServer() - { - if (!IsConnected) - { - CurrentServer = null; - } - } - } } /// @@ -393,7 +232,7 @@ private static void ThrowIfNull(in RedisChannel channel) public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { ThrowIfNull(channel); - var msg = Message.Create(-1, flags, channel.PublishCommand, channel, message); + var msg = Message.Create(-1, flags, channel.GetPublishCommand(), channel, message); // if we're actively subscribed: send via that connection (otherwise, follow normal rules) return ExecuteSync(msg, ResultProcessor.Int64, server: multiplexer.GetSubscribedServer(channel)); } @@ -401,7 +240,7 @@ public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags public Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { ThrowIfNull(channel); - var msg = Message.Create(-1, flags, channel.PublishCommand, channel, message); + var msg = Message.Create(-1, flags, channel.GetPublishCommand(), channel, message); // if we're actively subscribed: send via that connection (otherwise, follow normal rules) return ExecuteAsync(msg, ResultProcessor.Int64, server: multiplexer.GetSubscribedServer(channel)); } @@ -416,37 +255,26 @@ public ChannelMessageQueue Subscribe(RedisChannel channel, CommandFlags flags = return queue; } - private bool Subscribe(RedisChannel channel, Action? handler, ChannelMessageQueue? queue, CommandFlags flags) + private int Subscribe(RedisChannel channel, Action? handler, ChannelMessageQueue? queue, CommandFlags flags) { ThrowIfNull(channel); - if (handler == null && queue == null) { return true; } + if (handler == null && queue == null) { return 0; } var sub = multiplexer.GetOrAddSubscription(channel, flags); sub.Add(handler, queue); - return EnsureSubscribedToServer(sub, channel, flags, false); + return sub.EnsureSubscribedToServer(this, channel, flags, false); } - internal bool EnsureSubscribedToServer(Subscription sub, RedisChannel channel, CommandFlags flags, bool internalCall) - { - if (sub.IsConnected) { return true; } - - // TODO: Cleanup old hangers here? - sub.SetCurrentServer(null); // we're not appropriately connected, so blank it out for eligible reconnection - var message = sub.GetMessage(channel, SubscriptionAction.Subscribe, flags, internalCall); - var selected = multiplexer.SelectServer(message); - return ExecuteSync(message, sub.Processor, selected); - } - - internal void ResubscribeToServer(Subscription sub, RedisChannel channel, ServerEndPoint serverEndPoint, string cause) + internal void ResubscribeToServer(Subscription sub, in RedisChannel channel, ServerEndPoint serverEndPoint, string cause) { // conditional: only if that's the server we were connected to, or "none"; we don't want to end up duplicated - if (sub.ClearCurrentServer(serverEndPoint) || !sub.IsConnected) + if (sub.TryRemoveEndpoint(serverEndPoint) || !sub.IsConnectedAny()) { if (serverEndPoint.IsSubscriberConnected) { // we'll *try* for a simple resubscribe, following any -MOVED etc, but if that fails: fall back // to full reconfigure; importantly, note that we've already recorded the disconnect - var message = sub.GetMessage(channel, SubscriptionAction.Subscribe, CommandFlags.None, false); + var message = sub.GetSubscriptionMessage(channel, SubscriptionAction.Subscribe, CommandFlags.None, false); _ = ExecuteAsync(message, sub.Processor, serverEndPoint).ContinueWith( t => multiplexer.ReconfigureIfNeeded(serverEndPoint.EndPoint, false, cause: cause), TaskContinuationOptions.OnlyOnFaulted); @@ -470,25 +298,14 @@ public async Task SubscribeAsync(RedisChannel channel, Comm return queue; } - private Task SubscribeAsync(RedisChannel channel, Action? handler, ChannelMessageQueue? queue, CommandFlags flags, ServerEndPoint? server = null) + private Task SubscribeAsync(RedisChannel channel, Action? handler, ChannelMessageQueue? queue, CommandFlags flags, ServerEndPoint? server = null) { ThrowIfNull(channel); - if (handler == null && queue == null) { return CompletedTask.Default(null); } + if (handler == null && queue == null) { return CompletedTask.Default(null); } var sub = multiplexer.GetOrAddSubscription(channel, flags); sub.Add(handler, queue); - return EnsureSubscribedToServerAsync(sub, channel, flags, false, server); - } - - public Task EnsureSubscribedToServerAsync(Subscription sub, RedisChannel channel, CommandFlags flags, bool internalCall, ServerEndPoint? server = null) - { - if (sub.IsConnected) { return CompletedTask.Default(null); } - - // TODO: Cleanup old hangers here? - sub.SetCurrentServer(null); // we're not appropriately connected, so blank it out for eligible reconnection - var message = sub.GetMessage(channel, SubscriptionAction.Subscribe, flags, internalCall); - server ??= multiplexer.SelectServer(message); - return ExecuteAsync(message, sub.Processor, server); + return sub.EnsureSubscribedToServerAsync(this, channel, flags, false, server); } public EndPoint? SubscribedEndpoint(RedisChannel channel) => multiplexer.GetSubscribedServer(channel)?.EndPoint; @@ -500,21 +317,12 @@ public bool Unsubscribe(in RedisChannel channel, Action? handler, CommandFlags flags) => UnsubscribeAsync(channel, handler, null, flags); @@ -523,20 +331,10 @@ public Task UnsubscribeAsync(in RedisChannel channel, Action.Default(asyncState); } - private Task UnsubscribeFromServerAsync(Subscription sub, RedisChannel channel, CommandFlags flags, object? asyncState, bool internalCall) - { - if (sub.GetCurrentServer() is ServerEndPoint oldOwner) - { - var message = sub.GetMessage(channel, SubscriptionAction.Unsubscribe, flags, internalCall); - return multiplexer.ExecuteAsyncImpl(message, sub.Processor, asyncState, oldOwner); - } - return CompletedTask.FromResult(true, asyncState); - } - /// /// Unregisters a handler or queue and returns if we should remove it from the server. /// @@ -573,7 +371,7 @@ public void UnsubscribeAll(CommandFlags flags = CommandFlags.None) if (subs.TryRemove(pair.Key, out var sub)) { sub.MarkCompleted(); - UnsubscribeFromServer(sub, pair.Key, flags, false); + sub.UnsubscribeFromServer(this, pair.Key, flags, false); } } } @@ -588,7 +386,7 @@ public Task UnsubscribeAllAsync(CommandFlags flags = CommandFlags.None) if (subs.TryRemove(pair.Key, out var sub)) { sub.MarkCompleted(); - last = UnsubscribeFromServerAsync(sub, pair.Key, flags, asyncState, false); + last = sub.UnsubscribeFromServerAsync(this, pair.Key, flags, asyncState, false); } } return last ?? CompletedTask.Default(asyncState); diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index d306ca0d0..46228a912 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -869,19 +869,58 @@ private static string ToHex(ReadOnlySpan src) /// /// Gets the length of the value in bytes. /// - public int GetByteCount() + public int GetByteCount() => Type switch { - switch (Type) - { - case StorageType.Null: return 0; - case StorageType.Raw: return _memory.Length; - case StorageType.String: return Encoding.UTF8.GetByteCount((string)_objectOrSentinel!); - case StorageType.Int64: return Format.MeasureInt64(OverlappedValueInt64); - case StorageType.UInt64: return Format.MeasureUInt64(OverlappedValueUInt64); - case StorageType.Double: return Format.MeasureDouble(OverlappedValueDouble); - default: return ThrowUnableToMeasure(); - } - } + StorageType.Null => 0, + StorageType.Raw => _memory.Length, + StorageType.String => Encoding.UTF8.GetByteCount((string)_objectOrSentinel!), + StorageType.Int64 => Format.MeasureInt64(OverlappedValueInt64), + StorageType.UInt64 => Format.MeasureUInt64(OverlappedValueUInt64), + StorageType.Double => Format.MeasureDouble(OverlappedValueDouble), + _ => ThrowUnableToMeasure(), + }; + + /// + /// Gets the maximum length of the value in bytes. + /// + internal int GetMaxByteCount() => Type switch + { + StorageType.Null => 0, + StorageType.Raw => _memory.Length, + StorageType.String => Encoding.UTF8.GetMaxByteCount(((string)_objectOrSentinel!).Length), + StorageType.Int64 => Format.MaxInt64TextLen, + StorageType.UInt64 => Format.MaxInt64TextLen, + StorageType.Double => Format.MaxDoubleTextLen, + _ => ThrowUnableToMeasure(), + }; + + /// + /// Gets the length of the value in characters, assuming UTF8 interpretation of BLOB payloads. + /// + internal int GetCharCount() => Type switch + { + StorageType.Null => 0, + StorageType.Raw => Encoding.UTF8.GetCharCount(_memory.Span), + StorageType.String => ((string)_objectOrSentinel!).Length, + StorageType.Int64 => Format.MeasureInt64(OverlappedValueInt64), + StorageType.UInt64 => Format.MeasureUInt64(OverlappedValueUInt64), + StorageType.Double => Format.MeasureDouble(OverlappedValueDouble), + _ => ThrowUnableToMeasure(), + }; + + /// + /// Gets the length of the value in characters, assuming UTF8 interpretation of BLOB payloads. + /// + internal int GetMaxCharCount() => Type switch + { + StorageType.Null => 0, + StorageType.Raw => Encoding.UTF8.GetMaxCharCount(_memory.Length), + StorageType.String => ((string)_objectOrSentinel!).Length, + StorageType.Int64 => Format.MaxInt64TextLen, + StorageType.UInt64 => Format.MaxInt64TextLen, + StorageType.Double => Format.MaxDoubleTextLen, + _ => ThrowUnableToMeasure(), + }; private int ThrowUnableToMeasure() => throw new InvalidOperationException("Unable to compute length of type: " + Type); @@ -918,6 +957,33 @@ public int CopyTo(Span destination) } } + /// + /// Copy the value as character data to the provided . + /// + internal int CopyTo(Span destination) + { + switch (Type) + { + case StorageType.Null: + return 0; + case StorageType.Raw: + var srcBytes = _memory.Span; + return Encoding.UTF8.GetChars(srcBytes, destination); + case StorageType.String: + var span = ((string)_objectOrSentinel!).AsSpan(); + span.CopyTo(destination); + return span.Length; + case StorageType.Int64: + return Format.FormatInt64(OverlappedValueInt64, destination); + case StorageType.UInt64: + return Format.FormatUInt64(OverlappedValueUInt64, destination); + case StorageType.Double: + return Format.FormatDouble(OverlappedValueDouble, destination); + default: + return ThrowUnableToMeasure(); + } + } + /// /// Converts a to a . /// @@ -1245,5 +1311,61 @@ internal ValueCondition Digest() return digest; } } + + internal bool TryGetSpan(out ReadOnlySpan span) + { + if (_objectOrSentinel == Sentinel_Raw) + { + span = _memory.Span; + return true; + } + span = default; + return false; + } + + /// + /// Indicates whether the current value has the supplied value as a prefix. + /// + /// The to check. + [OverloadResolutionPriority(1)] // prefer this when it is an option (vs casting a byte[] to RedisValue) + public bool StartsWith(ReadOnlySpan value) + { + if (IsNull) return false; + if (value.IsEmpty) return true; + if (IsNullOrEmpty) return false; + + int len; + switch (Type) + { + case StorageType.Raw: + return _memory.Span.StartsWith(value); + case StorageType.Int64: + Span buffer = stackalloc byte[Format.MaxInt64TextLen]; + len = Format.FormatInt64(OverlappedValueInt64, buffer); + return buffer.Slice(0, len).StartsWith(value); + case StorageType.UInt64: + buffer = stackalloc byte[Format.MaxInt64TextLen]; + len = Format.FormatUInt64(OverlappedValueUInt64, buffer); + return buffer.Slice(0, len).StartsWith(value); + case StorageType.Double: + buffer = stackalloc byte[Format.MaxDoubleTextLen]; + len = Format.FormatDouble(OverlappedValueDouble, buffer); + return buffer.Slice(0, len).StartsWith(value); + case StorageType.String: + var s = ((string)_objectOrSentinel!).AsSpan(); + if (s.Length < value.Length) return false; // not enough characters to match + if (s.Length > value.Length) s = s.Slice(0, value.Length); // only need to match the prefix + var maxBytes = Encoding.UTF8.GetMaxByteCount(s.Length); + byte[]? lease = null; + const int MAX_STACK = 128; + buffer = maxBytes <= MAX_STACK ? stackalloc byte[MAX_STACK] : (lease = ArrayPool.Shared.Rent(maxBytes)); + var bytes = Encoding.UTF8.GetBytes(s, buffer); + bool isMatch = buffer.Slice(0, bytes).StartsWith(value); + if (lease is not null) ArrayPool.Shared.Return(lease); + return isMatch; + default: + return false; + } + } } } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 196cabde5..f2c6deb8b 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -469,12 +469,21 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes connection.SubscriptionCount = count; SetResult(message, true); - var newServer = message.Command switch + var ep = connection.BridgeCouldBeNull?.ServerEndPoint; + if (ep is not null) { - RedisCommand.SUBSCRIBE or RedisCommand.SSUBSCRIBE or RedisCommand.PSUBSCRIBE => connection.BridgeCouldBeNull?.ServerEndPoint, - _ => null, - }; - Subscription?.SetCurrentServer(newServer); + switch (message.Command) + { + case RedisCommand.SUBSCRIBE: + case RedisCommand.SSUBSCRIBE: + case RedisCommand.PSUBSCRIBE: + Subscription?.AddEndpoint(ep); + break; + default: + Subscription?.TryRemoveEndpoint(ep); + break; + } + } return true; } } diff --git a/src/StackExchange.Redis/ServerEndPoint.cs b/src/StackExchange.Redis/ServerEndPoint.cs index f856a5b21..abe8d8afb 100644 --- a/src/StackExchange.Redis/ServerEndPoint.cs +++ b/src/StackExchange.Redis/ServerEndPoint.cs @@ -695,14 +695,15 @@ internal void OnFullyEstablished(PhysicalConnection connection, string source) // Clear the unselectable flag ASAP since we are open for business ClearUnselectable(UnselectableFlags.DidNotRespond); - if (bridge == subscription) + bool isResp3 = KnowOrAssumeResp3(); + if (bridge == subscription || isResp3) { // Note: this MUST be fire and forget, because we might be in the middle of a Sync processing // TracerProcessor which is executing this line inside a SetResultCore(). // Since we're issuing commands inside a SetResult path in a message, we'd create a deadlock by waiting. Multiplexer.EnsureSubscriptions(CommandFlags.FireAndForget); } - if (IsConnected && (IsSubscriberConnected || !SupportsSubscriptions || KnowOrAssumeResp3())) + if (IsConnected && (IsSubscriberConnected || !SupportsSubscriptions || isResp3)) { // Only connect on the second leg - we can accomplish this by checking both // Or the first leg, if we're only making 1 connection because subscriptions aren't supported diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index ca247c38b..db729ba26 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -101,9 +101,31 @@ public int HashSlot(in RedisKey key) /// /// The to determine a slot ID for. public int HashSlot(in RedisChannel channel) - // note that the RedisChannel->byte[] converter is always direct, so this is not an alloc - // (we deal with channels far less frequently, so pay the encoding cost up-front) - => ServerType == ServerType.Standalone || channel.IsNull ? NoSlot : GetClusterSlot((byte[])channel!); + { + if (ServerType == ServerType.Standalone || channel.IsNull) return NoSlot; + + ReadOnlySpan routingSpan = channel.RoutingSpan; + byte[] prefix; + return channel.IgnoreChannelPrefix || (prefix = multiplexer.ChannelPrefix).Length == 0 + ? GetClusterSlot(routingSpan) : GetClusterSlotWithPrefix(prefix, routingSpan); + + static int GetClusterSlotWithPrefix(byte[] prefixRaw, ReadOnlySpan routingSpan) + { + ReadOnlySpan prefixSpan = prefixRaw; + const int MAX_STACK = 128; + byte[]? lease = null; + var totalLength = prefixSpan.Length + routingSpan.Length; + var span = totalLength <= MAX_STACK + ? stackalloc byte[MAX_STACK] + : (lease = ArrayPool.Shared.Rent(totalLength)); + + prefixSpan.CopyTo(span); + routingSpan.CopyTo(span.Slice(prefixSpan.Length)); + var result = GetClusterSlot(span.Slice(0, totalLength)); + if (lease is not null) ArrayPool.Shared.Return(lease); + return result; + } + } /// /// Gets the hashslot for a given byte sequence. @@ -360,5 +382,25 @@ private ServerEndPoint[] MapForMutation() } return Any(command, flags, allowDisconnected); } + + internal bool CanServeSlot(ServerEndPoint server, in RedisChannel channel) + => CanServeSlot(server, HashSlot(in channel)); + + internal bool CanServeSlot(ServerEndPoint server, int slot) + { + if (slot == NoSlot) return true; + var arr = map; + if (arr is null) return true; // means "any" + + var primary = arr[slot]; + if (server == primary) return true; + + var replicas = primary.Replicas; + for (int i = 0; i < replicas.Length; i++) + { + if (server == replicas[i]) return true; + } + return false; + } } } diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 983624bc0..84e495f1a 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -19,7 +19,10 @@ - + + + + diff --git a/src/StackExchange.Redis/Subscription.cs b/src/StackExchange.Redis/Subscription.cs new file mode 100644 index 000000000..99f3d00cb --- /dev/null +++ b/src/StackExchange.Redis/Subscription.cs @@ -0,0 +1,520 @@ +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Pipelines.Sockets.Unofficial; + +namespace StackExchange.Redis; + +public partial class ConnectionMultiplexer +{ + /// + /// This is the record of a single subscription to a redis server. + /// It's the singular channel (which may or may not be a pattern), to one or more handlers. + /// We subscriber to a redis server once (for all messages) and execute 1-many handlers when a message arrives. + /// + internal abstract class Subscription + { + private Action? _handlers; + private readonly object _handlersLock = new(); + private ChannelMessageQueue? _queues; + public CommandFlags Flags { get; } + public ResultProcessor.TrackSubscriptionsProcessor Processor { get; } + + internal abstract bool IsConnectedAny(); + internal abstract bool IsConnectedTo(EndPoint endpoint); + + internal abstract void AddEndpoint(ServerEndPoint server); + + // conditional clear + internal abstract bool TryRemoveEndpoint(ServerEndPoint expected); + + internal abstract void RemoveDisconnectedEndpoints(); + + // returns the number of changes required + internal abstract int EnsureSubscribedToServer( + RedisSubscriber subscriber, + in RedisChannel channel, + CommandFlags flags, + bool internalCall); + + // returns the number of changes required + internal abstract Task EnsureSubscribedToServerAsync( + RedisSubscriber subscriber, + RedisChannel channel, + CommandFlags flags, + bool internalCall, + ServerEndPoint? server = null); + + internal abstract bool UnsubscribeFromServer( + RedisSubscriber subscriber, + in RedisChannel channel, + CommandFlags flags, + bool internalCall); + + internal abstract Task UnsubscribeFromServerAsync( + RedisSubscriber subscriber, + RedisChannel channel, + CommandFlags flags, + object? asyncState, + bool internalCall); + + internal abstract int GetConnectionCount(); + + internal abstract ServerEndPoint? GetAnyCurrentServer(); + + public Subscription(CommandFlags flags) + { + Flags = flags; + Processor = new ResultProcessor.TrackSubscriptionsProcessor(this); + } + + /// + /// Gets the configured (P)SUBSCRIBE or (P)UNSUBSCRIBE for an action. + /// + internal Message GetSubscriptionMessage( + in RedisChannel channel, + SubscriptionAction action, + CommandFlags flags, + bool internalCall) + { + const RedisChannel.RedisChannelOptions OPTIONS_MASK = ~( + RedisChannel.RedisChannelOptions.KeyRouted | RedisChannel.RedisChannelOptions.IgnoreChannelPrefix); + var command = + action switch // note that the Routed flag doesn't impact the message here - just the routing + { + SubscriptionAction.Subscribe => (channel.Options & OPTIONS_MASK) switch + { + RedisChannel.RedisChannelOptions.None => RedisCommand.SUBSCRIBE, + RedisChannel.RedisChannelOptions.MultiNode => RedisCommand.SUBSCRIBE, + RedisChannel.RedisChannelOptions.Pattern => RedisCommand.PSUBSCRIBE, + RedisChannel.RedisChannelOptions.Pattern | RedisChannel.RedisChannelOptions.MultiNode => + RedisCommand.PSUBSCRIBE, + RedisChannel.RedisChannelOptions.Sharded => RedisCommand.SSUBSCRIBE, + _ => Unknown(action, channel.Options), + }, + SubscriptionAction.Unsubscribe => (channel.Options & OPTIONS_MASK) switch + { + RedisChannel.RedisChannelOptions.None => RedisCommand.UNSUBSCRIBE, + RedisChannel.RedisChannelOptions.MultiNode => RedisCommand.UNSUBSCRIBE, + RedisChannel.RedisChannelOptions.Pattern => RedisCommand.PUNSUBSCRIBE, + RedisChannel.RedisChannelOptions.Pattern | RedisChannel.RedisChannelOptions.MultiNode => + RedisCommand.PUNSUBSCRIBE, + RedisChannel.RedisChannelOptions.Sharded => RedisCommand.SUNSUBSCRIBE, + _ => Unknown(action, channel.Options), + }, + _ => Unknown(action, channel.Options), + }; + + // TODO: Consider flags here - we need to pass Fire and Forget, but don't want to intermingle Primary/Replica + var msg = Message.Create(-1, Flags | flags, command, channel); + msg.SetForSubscriptionBridge(); + if (internalCall) + { + msg.SetInternalCall(); + } + + return msg; + } + + private RedisCommand Unknown(SubscriptionAction action, RedisChannel.RedisChannelOptions options) + => throw new ArgumentException( + $"Unable to determine pub/sub operation for '{action}' against '{options}'"); + + public void Add(Action? handler, ChannelMessageQueue? queue) + { + if (handler != null) + { + lock (_handlersLock) + { + _handlers += handler; + } + } + + if (queue != null) + { + ChannelMessageQueue.Combine(ref _queues, queue); + } + } + + public bool Remove(Action? handler, ChannelMessageQueue? queue) + { + if (handler != null) + { + lock (_handlersLock) + { + _handlers -= handler; + } + } + + if (queue != null) + { + ChannelMessageQueue.Remove(ref _queues, queue); + } + + return _handlers == null & _queues == null; + } + + public ICompletable? ForInvoke(in RedisChannel channel, in RedisValue message, out ChannelMessageQueue? queues) + { + var handlers = _handlers; + queues = Volatile.Read(ref _queues); + return handlers == null ? null : new MessageCompletable(channel, message, handlers); + } + + internal void MarkCompleted() + { + lock (_handlersLock) + { + _handlers = null; + } + + ChannelMessageQueue.MarkAllCompleted(ref _queues); + } + + internal void GetSubscriberCounts(out int handlers, out int queues) + { + queues = ChannelMessageQueue.Count(ref _queues); + var tmp = _handlers; + if (tmp == null) + { + handlers = 0; + } + else if (tmp.IsSingle()) + { + handlers = 1; + } + else + { + handlers = 0; + foreach (var sub in tmp.AsEnumerable()) { handlers++; } + } + } + } + + // used for most subscriptions; routed to a single node + internal sealed class SingleNodeSubscription(CommandFlags flags) : Subscription(flags) + { + internal override bool IsConnectedAny() => _currentServer is { IsSubscriberConnected: true }; + + internal override int GetConnectionCount() => IsConnectedAny() ? 1 : 0; + + internal override bool IsConnectedTo(EndPoint endpoint) + { + var server = _currentServer; + return server is { IsSubscriberConnected: true } && server.EndPoint == endpoint; + } + + internal override void AddEndpoint(ServerEndPoint server) => _currentServer = server; + + internal override bool TryRemoveEndpoint(ServerEndPoint expected) + { + if (_currentServer == expected) + { + _currentServer = null; + return true; + } + + return false; + } + + internal override bool UnsubscribeFromServer( + RedisSubscriber subscriber, + in RedisChannel channel, + CommandFlags flags, + bool internalCall) + { + var server = _currentServer; + if (server is not null) + { + var message = GetSubscriptionMessage(channel, SubscriptionAction.Unsubscribe, flags, internalCall); + return subscriber.multiplexer.ExecuteSyncImpl(message, Processor, server); + } + + return true; + } + + internal override Task UnsubscribeFromServerAsync( + RedisSubscriber subscriber, + RedisChannel channel, + CommandFlags flags, + object? asyncState, + bool internalCall) + { + var server = _currentServer; + if (server is not null) + { + var message = GetSubscriptionMessage(channel, SubscriptionAction.Unsubscribe, flags, internalCall); + return subscriber.multiplexer.ExecuteAsyncImpl(message, Processor, asyncState, server); + } + + return CompletedTask.FromResult(true, asyncState); + } + + private ServerEndPoint? _currentServer; + internal ServerEndPoint? GetCurrentServer() => Volatile.Read(ref _currentServer); + + internal override ServerEndPoint? GetAnyCurrentServer() => Volatile.Read(ref _currentServer); + + /// + /// Evaluates state and if we're not currently connected, clears the server reference. + /// + internal override void RemoveDisconnectedEndpoints() + { + var server = _currentServer; + if (server is { IsSubscriberConnected: false }) + { + _currentServer = null; + } + } + + internal override int EnsureSubscribedToServer( + RedisSubscriber subscriber, + in RedisChannel channel, + CommandFlags flags, + bool internalCall) + { + RemoveIncorrectRouting(subscriber, in channel, flags, internalCall); + if (IsConnectedAny()) return 0; + + // we're not appropriately connected, so blank it out for eligible reconnection + _currentServer = null; + var message = GetSubscriptionMessage(channel, SubscriptionAction.Subscribe, flags, internalCall); + var selected = subscriber.multiplexer.SelectServer(message); + _ = subscriber.ExecuteSync(message, Processor, selected); + return 1; + } + + private void RemoveIncorrectRouting(RedisSubscriber subscriber, in RedisChannel channel, CommandFlags flags, bool internalCall) + { + // only applies to cluster, when using key-routed channels (sharded, explicit key-routed, or + // a single-key keyspace notification); is the subscribed server still handling that channel? + if (channel.IsKeyRouted && _currentServer is { ServerType: ServerType.Cluster } current) + { + // if we consider replicas, there can be multiple valid target servers; we can't ask + // "is this the correct server?", but we can ask "is it suitable?", based on the slot + if (!subscriber.multiplexer.ServerSelectionStrategy.CanServeSlot(_currentServer, channel)) + { + var message = GetSubscriptionMessage(channel, SubscriptionAction.Unsubscribe, flags | CommandFlags.FireAndForget, internalCall); + subscriber.multiplexer.ExecuteSyncImpl(message, Processor, current); + _currentServer = null; // pre-emptively disconnect - F+F + } + } + } + + internal override async Task EnsureSubscribedToServerAsync( + RedisSubscriber subscriber, + RedisChannel channel, + CommandFlags flags, + bool internalCall, + ServerEndPoint? server = null) + { + RemoveIncorrectRouting(subscriber, in channel, flags, internalCall); + if (IsConnectedAny()) return 0; + + // we're not appropriately connected, so blank it out for eligible reconnection + _currentServer = null; + var message = GetSubscriptionMessage(channel, SubscriptionAction.Subscribe, flags, internalCall); + server ??= subscriber.multiplexer.SelectServer(message); + await subscriber.ExecuteAsync(message, Processor, server).ForAwait(); + return 1; + } + } + + // used for keyspace subscriptions, which are routed to multiple nodes + internal sealed class MultiNodeSubscription(CommandFlags flags) : Subscription(flags) + { + private readonly ConcurrentDictionary _servers = new(); + + internal override bool IsConnectedAny() + { + foreach (var server in _servers) + { + if (server.Value is { IsSubscriberConnected: true }) return true; + } + + return false; + } + + internal override int GetConnectionCount() + { + int count = 0; + foreach (var server in _servers) + { + if (server.Value is { IsSubscriberConnected: true }) count++; + } + + return count; + } + + internal override bool IsConnectedTo(EndPoint endpoint) + => _servers.TryGetValue(endpoint, out var server) + && server.IsSubscriberConnected; + + internal override void AddEndpoint(ServerEndPoint server) + { + var ep = server.EndPoint; + if (!_servers.TryAdd(ep, server)) + { + _servers[ep] = server; + } + } + + internal override bool TryRemoveEndpoint(ServerEndPoint expected) + { + return _servers.TryRemove(expected.EndPoint, out _); + } + + internal override ServerEndPoint? GetAnyCurrentServer() + { + ServerEndPoint? last = null; + // prefer actively connected servers, but settle for anything + foreach (var server in _servers) + { + last = server.Value; + if (last is { IsSubscriberConnected: true }) + { + break; + } + } + + return last; + } + + internal override void RemoveDisconnectedEndpoints() + { + // This looks more complicated than it is, because of avoiding mutating the collection + // while iterating; instead, buffer any removals in a scratch buffer, and remove them in a second pass. + EndPoint[] scratch = []; + int count = 0; + foreach (var server in _servers) + { + if (server.Value.IsSubscriberConnected) + { + // flag for removal + if (scratch.Length == count) // need to resize the scratch buffer, using the pool + { + // let the array pool worry about min-sizing etc + var newLease = ArrayPool.Shared.Rent(count + 1); + scratch.CopyTo(newLease, 0); + ArrayPool.Shared.Return(scratch); + scratch = newLease; + } + + scratch[count++] = server.Key; + } + } + + // did we find anything to remove? + if (count != 0) + { + foreach (var ep in scratch.AsSpan(0, count)) + { + _servers.TryRemove(ep, out _); + } + } + + ArrayPool.Shared.Return(scratch); + } + + internal override int EnsureSubscribedToServer( + RedisSubscriber subscriber, + in RedisChannel channel, + CommandFlags flags, + bool internalCall) + { + int delta = 0; + var muxer = subscriber.multiplexer; + foreach (var server in muxer.GetServerSnapshot()) + { + var change = GetSubscriptionChange(server, flags); + if (change is not null) + { + // make it so + var message = GetSubscriptionMessage(channel, change.GetValueOrDefault(), flags, internalCall); + subscriber.ExecuteSync(message, Processor, server); + delta++; + } + } + + return delta; + } + + private SubscriptionAction? GetSubscriptionChange(ServerEndPoint server, CommandFlags flags) + { + // exclude sentinel, and only use replicas if we're explicitly asking for them + bool useReplica = (Flags & CommandFlags.DemandReplica) != 0; + bool shouldBeConnected = server.ServerType != ServerType.Sentinel & server.IsReplica == useReplica; + if (shouldBeConnected == IsConnectedTo(server.EndPoint)) + { + return null; + } + return shouldBeConnected ? SubscriptionAction.Subscribe : SubscriptionAction.Unsubscribe; + } + + internal override async Task EnsureSubscribedToServerAsync( + RedisSubscriber subscriber, + RedisChannel channel, + CommandFlags flags, + bool internalCall, + ServerEndPoint? server = null) + { + int delta = 0; + var muxer = subscriber.multiplexer; + var snapshot = muxer.GetServerSnaphotMemory(); + var len = snapshot.Length; + for (int i = 0; i < len; i++) + { + var loopServer = snapshot.Span[i]; // spans and async do not mix well + if (server is null || server == loopServer) // either "all" or "just the one we passed in" + { + var change = GetSubscriptionChange(loopServer, flags); + if (change is not null) + { + // make it so + var message = GetSubscriptionMessage(channel, change.GetValueOrDefault(), flags, internalCall); + await subscriber.ExecuteAsync(message, Processor, loopServer).ForAwait(); + delta++; + } + } + } + + return delta; + } + + internal override bool UnsubscribeFromServer( + RedisSubscriber subscriber, + in RedisChannel channel, + CommandFlags flags, + bool internalCall) + { + bool any = false; + foreach (var server in _servers) + { + var message = GetSubscriptionMessage(channel, SubscriptionAction.Unsubscribe, flags, internalCall); + any |= subscriber.ExecuteSync(message, Processor, server.Value); + } + + return any; + } + + internal override async Task UnsubscribeFromServerAsync( + RedisSubscriber subscriber, + RedisChannel channel, + CommandFlags flags, + object? asyncState, + bool internalCall) + { + bool any = false; + foreach (var server in _servers) + { + var message = GetSubscriptionMessage(channel, SubscriptionAction.Unsubscribe, flags, internalCall); + any |= await subscriber.ExecuteAsync(message, Processor, server.Value).ForAwait(); + } + + return any; + } + } +} diff --git a/tests/RedisConfigs/3.0.503/redis.windows-service.conf b/tests/RedisConfigs/3.0.503/redis.windows-service.conf index ed44371a3..b374dad58 100644 --- a/tests/RedisConfigs/3.0.503/redis.windows-service.conf +++ b/tests/RedisConfigs/3.0.503/redis.windows-service.conf @@ -829,7 +829,7 @@ latency-monitor-threshold 0 # By default all notifications are disabled because most users don't need # this feature and the feature has some overhead. Note that if you don't # specify at least one of K or E, no events will be delivered. -notify-keyspace-events "" +notify-keyspace-events "AKE" ############################### ADVANCED CONFIG ############################### diff --git a/tests/RedisConfigs/3.0.503/redis.windows.conf b/tests/RedisConfigs/3.0.503/redis.windows.conf index c07a7e9ab..4a99b8fdb 100644 --- a/tests/RedisConfigs/3.0.503/redis.windows.conf +++ b/tests/RedisConfigs/3.0.503/redis.windows.conf @@ -829,7 +829,7 @@ latency-monitor-threshold 0 # By default all notifications are disabled because most users don't need # this feature and the feature has some overhead. Note that if you don't # specify at least one of K or E, no events will be delivered. -notify-keyspace-events "" +notify-keyspace-events "AKE" ############################### ADVANCED CONFIG ############################### diff --git a/tests/RedisConfigs/Basic/primary-6379-3.0.conf b/tests/RedisConfigs/Basic/primary-6379-3.0.conf index 1f4d96da5..889756fec 100644 --- a/tests/RedisConfigs/Basic/primary-6379-3.0.conf +++ b/tests/RedisConfigs/Basic/primary-6379-3.0.conf @@ -6,4 +6,5 @@ maxmemory 6gb dir "../Temp" appendonly no dbfilename "primary-6379.rdb" -save "" \ No newline at end of file +save "" +notify-keyspace-events AKE \ No newline at end of file diff --git a/tests/RedisConfigs/Basic/primary-6379.conf b/tests/RedisConfigs/Basic/primary-6379.conf index dee83828c..2da592601 100644 --- a/tests/RedisConfigs/Basic/primary-6379.conf +++ b/tests/RedisConfigs/Basic/primary-6379.conf @@ -7,4 +7,5 @@ dir "../Temp" appendonly no dbfilename "primary-6379.rdb" save "" -enable-debug-command yes \ No newline at end of file +enable-debug-command yes +notify-keyspace-events AKE \ No newline at end of file diff --git a/tests/RedisConfigs/Basic/replica-6380.conf b/tests/RedisConfigs/Basic/replica-6380.conf index 8d87e54c2..0c1650513 100644 --- a/tests/RedisConfigs/Basic/replica-6380.conf +++ b/tests/RedisConfigs/Basic/replica-6380.conf @@ -7,4 +7,5 @@ maxmemory 2gb appendonly no dir "../Temp" dbfilename "replica-6380.rdb" -save "" \ No newline at end of file +save "" +notify-keyspace-events AKE \ No newline at end of file diff --git a/tests/RedisConfigs/Basic/secure-6381.conf b/tests/RedisConfigs/Basic/secure-6381.conf index bd9359244..ad2e380ad 100644 --- a/tests/RedisConfigs/Basic/secure-6381.conf +++ b/tests/RedisConfigs/Basic/secure-6381.conf @@ -4,4 +4,5 @@ databases 2000 maxmemory 512mb dir "../Temp" dbfilename "secure-6381.rdb" -save "" \ No newline at end of file +save "" +notify-keyspace-events AKE \ No newline at end of file diff --git a/tests/RedisConfigs/Basic/tls-ciphers-6384.conf b/tests/RedisConfigs/Basic/tls-ciphers-6384.conf index 52fc7d7b1..857d5c741 100644 --- a/tests/RedisConfigs/Basic/tls-ciphers-6384.conf +++ b/tests/RedisConfigs/Basic/tls-ciphers-6384.conf @@ -9,3 +9,4 @@ tls-protocols "TLSv1.2 TLSv1.3" tls-cert-file /Certs/redis.crt tls-key-file /Certs/redis.key tls-ca-cert-file /Certs/ca.crt +notify-keyspace-events AKE diff --git a/tests/RedisConfigs/Cluster/cluster-7000.conf b/tests/RedisConfigs/Cluster/cluster-7000.conf index f250a3db3..ad11a23fd 100644 --- a/tests/RedisConfigs/Cluster/cluster-7000.conf +++ b/tests/RedisConfigs/Cluster/cluster-7000.conf @@ -6,4 +6,5 @@ cluster-node-timeout 5000 appendonly yes dbfilename "dump-7000.rdb" appendfilename "appendonly-7000.aof" -save "" \ No newline at end of file +save "" +notify-keyspace-events AKE \ No newline at end of file diff --git a/tests/RedisConfigs/Cluster/cluster-7001.conf b/tests/RedisConfigs/Cluster/cluster-7001.conf index 1ae0c6f83..589f9ea23 100644 --- a/tests/RedisConfigs/Cluster/cluster-7001.conf +++ b/tests/RedisConfigs/Cluster/cluster-7001.conf @@ -6,4 +6,5 @@ cluster-node-timeout 5000 appendonly yes dbfilename "dump-7001.rdb" appendfilename "appendonly-7001.aof" -save "" \ No newline at end of file +save "" +notify-keyspace-events AKE \ No newline at end of file diff --git a/tests/RedisConfigs/Cluster/cluster-7002.conf b/tests/RedisConfigs/Cluster/cluster-7002.conf index 897301f59..66a376865 100644 --- a/tests/RedisConfigs/Cluster/cluster-7002.conf +++ b/tests/RedisConfigs/Cluster/cluster-7002.conf @@ -6,4 +6,5 @@ cluster-node-timeout 5000 appendonly yes dbfilename "dump-7002.rdb" appendfilename "appendonly-7002.aof" -save "" \ No newline at end of file +save "" +notify-keyspace-events AKE \ No newline at end of file diff --git a/tests/RedisConfigs/Cluster/cluster-7003.conf b/tests/RedisConfigs/Cluster/cluster-7003.conf index 0b51677fd..1f4883023 100644 --- a/tests/RedisConfigs/Cluster/cluster-7003.conf +++ b/tests/RedisConfigs/Cluster/cluster-7003.conf @@ -6,4 +6,5 @@ cluster-node-timeout 5000 appendonly yes dbfilename "dump-7003.rdb" appendfilename "appendonly-7003.aof" -save "" \ No newline at end of file +save "" +notify-keyspace-events AKE \ No newline at end of file diff --git a/tests/RedisConfigs/Cluster/cluster-7004.conf b/tests/RedisConfigs/Cluster/cluster-7004.conf index 9a49d21f5..93d75f38a 100644 --- a/tests/RedisConfigs/Cluster/cluster-7004.conf +++ b/tests/RedisConfigs/Cluster/cluster-7004.conf @@ -6,4 +6,5 @@ cluster-node-timeout 5000 appendonly yes dbfilename "dump-7004.rdb" appendfilename "appendonly-7004.aof" -save "" \ No newline at end of file +save "" +notify-keyspace-events AKE \ No newline at end of file diff --git a/tests/RedisConfigs/Cluster/cluster-7005.conf b/tests/RedisConfigs/Cluster/cluster-7005.conf index b333a4b44..c9b5d55e2 100644 --- a/tests/RedisConfigs/Cluster/cluster-7005.conf +++ b/tests/RedisConfigs/Cluster/cluster-7005.conf @@ -6,4 +6,5 @@ cluster-node-timeout 5000 appendonly yes dbfilename "dump-7005.rdb" appendfilename "appendonly-7005.aof" -save "" \ No newline at end of file +save "" +notify-keyspace-events AKE \ No newline at end of file diff --git a/tests/RedisConfigs/Failover/primary-6382.conf b/tests/RedisConfigs/Failover/primary-6382.conf index c19e8c701..6055c0347 100644 --- a/tests/RedisConfigs/Failover/primary-6382.conf +++ b/tests/RedisConfigs/Failover/primary-6382.conf @@ -6,4 +6,5 @@ maxmemory 2gb dir "../Temp" appendonly no dbfilename "primary-6382.rdb" -save "" \ No newline at end of file +save "" +notify-keyspace-events AKE \ No newline at end of file diff --git a/tests/RedisConfigs/Failover/replica-6383.conf b/tests/RedisConfigs/Failover/replica-6383.conf index 6f1a0fc7d..e07f5a69d 100644 --- a/tests/RedisConfigs/Failover/replica-6383.conf +++ b/tests/RedisConfigs/Failover/replica-6383.conf @@ -7,4 +7,5 @@ maxmemory 2gb appendonly no dir "../Temp" dbfilename "replica-6383.rdb" -save "" \ No newline at end of file +save "" +notify-keyspace-events AKE \ No newline at end of file diff --git a/tests/RedisConfigs/Sentinel/redis-7010.conf b/tests/RedisConfigs/Sentinel/redis-7010.conf index 0e27680b2..878160632 100644 --- a/tests/RedisConfigs/Sentinel/redis-7010.conf +++ b/tests/RedisConfigs/Sentinel/redis-7010.conf @@ -5,4 +5,5 @@ maxmemory 100mb appendonly no dir "../Temp" dbfilename "sentinel-target-7010.rdb" -save "" \ No newline at end of file +save "" +notify-keyspace-events AKE \ No newline at end of file diff --git a/tests/RedisConfigs/Sentinel/redis-7011.conf b/tests/RedisConfigs/Sentinel/redis-7011.conf index 6d02eb150..08b8dad1a 100644 --- a/tests/RedisConfigs/Sentinel/redis-7011.conf +++ b/tests/RedisConfigs/Sentinel/redis-7011.conf @@ -6,4 +6,5 @@ maxmemory 100mb appendonly no dir "../Temp" dbfilename "sentinel-target-7011.rdb" -save "" \ No newline at end of file +save "" +notify-keyspace-events AKE \ No newline at end of file diff --git a/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs b/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs index a0d9b5c88..fa80114f8 100644 --- a/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs +++ b/tests/StackExchange.Redis.Tests/Certificates/CertValidationTests.cs @@ -51,7 +51,9 @@ public void CheckIssuerValidity() Assert.False(callback(this, endpointCert, null, SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNotAvailable), "subtest 3f"); } +#pragma warning disable SYSLIB0057 private static X509Certificate2 LoadCert(string certificatePath) => new X509Certificate2(File.ReadAllBytes(certificatePath)); +#pragma warning restore SYSLIB0057 [Fact] public void CheckIssuerArgs() diff --git a/tests/StackExchange.Redis.Tests/ClusterShardedTests.cs b/tests/StackExchange.Redis.Tests/ClusterShardedTests.cs index 8af0a1c7b..c9101fb08 100644 --- a/tests/StackExchange.Redis.Tests/ClusterShardedTests.cs +++ b/tests/StackExchange.Redis.Tests/ClusterShardedTests.cs @@ -72,7 +72,8 @@ public async Task TestShardedPubsubSubscriberAgainstReconnects() public async Task TestShardedPubsubSubscriberAgainsHashSlotMigration() { Skip.UnlessLongRunning(); - var channel = RedisChannel.Sharded(Me()); + var channel = RedisChannel.Sharded(Me()); // invent a channel that will use SSUBSCRIBE + var key = (RedisKey)(byte[])channel!; // use the same value as a key, to test keyspace notifications via a single-key API await using var conn = Create(allowAdmin: true, keepAlive: 1, connectTimeout: 3000, shared: false, require: RedisFeatures.v7_0_0_rc1); Assert.True(conn.IsConnected); var db = conn.GetDatabase(); @@ -80,46 +81,75 @@ public async Task TestShardedPubsubSubscriberAgainsHashSlotMigration() await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) var pubsub = conn.GetSubscriber(); - List<(RedisChannel, RedisValue)> received = []; - var queue = await pubsub.SubscribeAsync(channel); - _ = Task.Run(async () => + var keynotify = RedisChannel.KeySpaceSingleKey(key, db.Database); + Assert.False(keynotify.IsSharded); // keyspace notifications do not use SSUBSCRIBE; this matters, because it means we don't get nuked when the slot migrates + Assert.False(keynotify.IsMultiNode); // we specificially want this *not* to be multi-node; we want to test that it follows the key correctly + + int keynotificationCount = 0; + await pubsub.SubscribeAsync(keynotify, (_, _) => Interlocked.Increment(ref keynotificationCount)); + try { - // use queue API to have control over order - await foreach (var item in queue) + List<(RedisChannel, RedisValue)> received = []; + var queue = await pubsub.SubscribeAsync(channel); + _ = Task.Run(async () => { - lock (received) + // use queue API to have control over order + await foreach (var item in queue) { - if (item.Channel.IsSharded && item.Channel == channel) received.Add((item.Channel, item.Message)); + lock (received) + { + if (item.Channel.IsSharded && item.Channel == channel) + received.Add((item.Channel, item.Message)); + } } + }); + Assert.Equal(2, conn.GetSubscriptionsCount()); + + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + await db.PingAsync(); + + for (int i = 0; i < 5; i++) + { + // check we get a hit + Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); + await db.StringIncrementAsync(key); } - }); - Assert.Equal(1, conn.GetSubscriptionsCount()); - await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) - await db.PingAsync(); + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) - for (int i = 0; i < 5; i++) - { - // check we get a hit - Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); - } - await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) + // lets migrate the slot for "testShardChannel" to another node + await DoHashSlotMigrationAsync(); + + await Task.Delay(4000); + for (int i = 0; i < 5; i++) + { + // check we get a hit + Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); + await db.StringIncrementAsync(key); + } - // lets migrate the slot for "testShardChannel" to another node - await DoHashSlotMigrationAsync(); + await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) - await Task.Delay(4000); - for (int i = 0; i < 5; i++) + Assert.Equal(2, conn.GetSubscriptionsCount()); + Assert.Equal(10, received.Count); + Assert.Equal(10, Volatile.Read(ref keynotificationCount)); + await RollbackHashSlotMigrationAsync(); + ClearAmbientFailures(); + } + finally { - // check we get a hit - Assert.Equal(1, await db.PublishAsync(channel, i.ToString())); + try + { + // ReSharper disable once MethodHasAsyncOverload - F+F + await pubsub.UnsubscribeAsync(keynotify, flags: CommandFlags.FireAndForget); + await pubsub.UnsubscribeAsync(channel, flags: CommandFlags.FireAndForget); + Log("Channels unsubscribed."); + } + catch (Exception ex) + { + Log($"Error while unsubscribing: {ex.Message}"); + } } - await Task.Delay(50); // let the sub settle (this isn't needed on RESP3, note) - - Assert.Equal(1, conn.GetSubscriptionsCount()); - Assert.Equal(10, received.Count); - await RollbackHashSlotMigrationAsync(); - ClearAmbientFailures(); } private Task DoHashSlotMigrationAsync() => MigrateSlotForTestShardChannelAsync(false); diff --git a/tests/StackExchange.Redis.Tests/ClusterTests.cs b/tests/StackExchange.Redis.Tests/ClusterTests.cs index 8146dc9be..781b65fef 100644 --- a/tests/StackExchange.Redis.Tests/ClusterTests.cs +++ b/tests/StackExchange.Redis.Tests/ClusterTests.cs @@ -743,11 +743,15 @@ public async Task ConnectIncludesSubscriber() } [Theory] - [InlineData(true, false)] - [InlineData(true, true)] - [InlineData(false, false)] - [InlineData(false, true)] - public async Task ClusterPubSub(bool sharded, bool withKeyRouting) + [InlineData(true, false, false)] + [InlineData(true, true, false)] + [InlineData(false, false, false)] + [InlineData(false, true, false)] + [InlineData(true, false, true)] + [InlineData(true, true, true)] + [InlineData(false, false, true)] + [InlineData(false, true, true)] + public async Task ClusterPubSub(bool sharded, bool withKeyRouting, bool withKeyPrefix) { var guid = Guid.NewGuid().ToString(); var channel = sharded ? RedisChannel.Sharded(guid) : RedisChannel.Literal(guid); @@ -755,7 +759,12 @@ public async Task ClusterPubSub(bool sharded, bool withKeyRouting) { channel = channel.WithKeyRouting(); } - await using var conn = Create(keepAlive: 1, connectTimeout: 3000, shared: false, require: sharded ? RedisFeatures.v7_0_0_rc1 : RedisFeatures.v2_0_0); + await using var conn = Create( + keepAlive: 1, + connectTimeout: 3000, + shared: false, + require: sharded ? RedisFeatures.v7_0_0_rc1 : RedisFeatures.v2_0_0, + channelPrefix: withKeyPrefix ? "c_prefix:" : null); Assert.True(conn.IsConnected); var pubsub = conn.GetSubscriber(); @@ -778,7 +787,7 @@ public async Task ClusterPubSub(bool sharded, bool withKeyRouting) } List<(RedisChannel, RedisValue)> received = []; - var queue = await pubsub.SubscribeAsync(channel); + var queue = await pubsub.SubscribeAsync(channel, CommandFlags.NoRedirect); _ = Task.Run(async () => { // use queue API to have control over order diff --git a/tests/StackExchange.Redis.Tests/FailoverTests.cs b/tests/StackExchange.Redis.Tests/FailoverTests.cs index 1f33275b5..825c8efce 100644 --- a/tests/StackExchange.Redis.Tests/FailoverTests.cs +++ b/tests/StackExchange.Redis.Tests/FailoverTests.cs @@ -236,10 +236,12 @@ public async Task SubscriptionsSurviveConnectionFailureAsync() server.SimulateConnectionFailure(SimulatedFailureType.All); // Trigger failure (RedisTimeoutException or RedisConnectionException because // of backlog behavior) - var ex = Assert.ThrowsAny(() => sub.Ping()); - Assert.True(ex is RedisTimeoutException or RedisConnectionException); Assert.False(sub.IsConnected(channel)); + var ex = Assert.ThrowsAny(() => Log($"Ping: {sub.Ping(CommandFlags.DemandMaster)}ms")); + Assert.True(ex is RedisTimeoutException or RedisConnectionException); + Log($"Failed as expected: {ex.Message}"); + // Now reconnect... conn.AllowConnect = true; Log("Waiting on reconnect"); @@ -263,7 +265,7 @@ public async Task SubscriptionsSurviveConnectionFailureAsync() foreach (var pair in muxerSubs) { var muxerSub = pair.Value; - Log($" Muxer Sub: {pair.Key}: (EndPoint: {muxerSub.GetCurrentServer()}, Connected: {muxerSub.IsConnected})"); + Log($" Muxer Sub: {pair.Key}: (EndPoint: {muxerSub.GetAnyCurrentServer()}, Connected: {muxerSub.IsConnectedAny()})"); } Log("Publishing"); diff --git a/tests/StackExchange.Redis.Tests/FastHashTests.cs b/tests/StackExchange.Redis.Tests/FastHashTests.cs index 418198cfd..a032cfc80 100644 --- a/tests/StackExchange.Redis.Tests/FastHashTests.cs +++ b/tests/StackExchange.Redis.Tests/FastHashTests.cs @@ -2,13 +2,14 @@ using System.Runtime.InteropServices; using System.Text; using Xunit; +using Xunit.Sdk; #pragma warning disable CS8981, SA1134, SA1300, SA1303, SA1502 // names are weird in this test! // ReSharper disable InconsistentNaming - to better represent expected literals // ReSharper disable IdentifierTypo namespace StackExchange.Redis.Tests; -public partial class FastHashTests +public partial class FastHashTests(ITestOutputHelper log) { // note: if the hashing algorithm changes, we can update the last parameter freely; it doesn't matter // what it *is* - what matters is that we can see that it has entropy between different values @@ -83,6 +84,46 @@ public void FastHashIs_Long() Assert.False(abcdefghijklmnopqrst.Is(hash, value)); } + [Fact] + public void KeyNotificationTypeFastHash_MinMaxBytes_ReflectsActualLengths() + { + // Use reflection to find all nested types in KeyNotificationTypeFastHash + var fastHashType = typeof(KeyNotificationTypeFastHash); + var nestedTypes = fastHashType.GetNestedTypes(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + + int? minLength = null; + int? maxLength = null; + + foreach (var nestedType in nestedTypes) + { + // Look for the Length field (generated by FastHash source generator) + var lengthField = nestedType.GetField("Length", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + if (lengthField != null && lengthField.FieldType == typeof(int)) + { + var length = (int)lengthField.GetValue(null)!; + + if (minLength == null || length < minLength) + { + minLength = length; + } + + if (maxLength == null || length > maxLength) + { + maxLength = length; + } + } + } + + // Assert that we found at least some nested types with Length fields + Assert.NotNull(minLength); + Assert.NotNull(maxLength); + + // Assert that MinBytes and MaxBytes match the actual min/max lengths + log.WriteLine($"MinBytes: {KeyNotificationTypeFastHash.MinBytes}, MaxBytes: {KeyNotificationTypeFastHash.MaxBytes}"); + Assert.Equal(KeyNotificationTypeFastHash.MinBytes, minLength.Value); + Assert.Equal(KeyNotificationTypeFastHash.MaxBytes, maxLength.Value); + } + [FastHash] private static partial class a { } [FastHash] private static partial class ab { } [FastHash] private static partial class abc { } diff --git a/tests/StackExchange.Redis.Tests/HashTests.cs b/tests/StackExchange.Redis.Tests/HashTests.cs index af2fa11c8..9523ca102 100644 --- a/tests/StackExchange.Redis.Tests/HashTests.cs +++ b/tests/StackExchange.Redis.Tests/HashTests.cs @@ -265,7 +265,7 @@ public async Task TestGetAll() } var inRedis = (await db.HashGetAllAsync(key).ForAwait()).ToDictionary( - x => Guid.Parse(x.Name!), x => int.Parse(x.Value!)); + x => Guid.Parse((string)x.Name!), x => int.Parse(x.Value!)); Assert.Equal(shouldMatch.Count, inRedis.Count); diff --git a/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs b/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs index e41a46670..2a23f3246 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/TextWriterOutputHelper.cs @@ -7,7 +7,7 @@ namespace StackExchange.Redis.Tests.Helpers; public class TextWriterOutputHelper(ITestOutputHelper outputHelper) : TextWriter { - private StringBuilder Buffer { get; } = new StringBuilder(2048); + private readonly StringBuilder _buffer = new(2048); private StringBuilder? Echo { get; set; } public override Encoding Encoding => Encoding.UTF8; private readonly ITestOutputHelper Output = outputHelper; @@ -37,7 +37,10 @@ public override void WriteLine(string? value) try { - base.WriteLine(value); + lock (_buffer) // keep everything together + { + base.WriteLine(value); + } } catch (Exception ex) { @@ -49,32 +52,44 @@ public override void WriteLine(string? value) public override void Write(char value) { - if (value == '\n' || value == '\r') + lock (_buffer) { - // Ignore empty lines - if (Buffer.Length > 0) + if (value == '\n' || value == '\r') { - FlushBuffer(); + // Ignore empty lines + if (_buffer.Length > 0) + { + FlushBuffer(); + } + } + else + { + _buffer.Append(value); } - } - else - { - Buffer.Append(value); } } protected override void Dispose(bool disposing) { - if (Buffer.Length > 0) + lock (_buffer) { - FlushBuffer(); + if (_buffer.Length > 0) + { + FlushBuffer(); + } } + base.Dispose(disposing); } private void FlushBuffer() { - var text = Buffer.ToString(); + string text; + lock (_buffer) + { + text = _buffer.ToString(); + _buffer.Clear(); + } try { Output.WriteLine(text); @@ -84,6 +99,5 @@ private void FlushBuffer() // Thrown when writing from a handler after a test has ended - just bail in this case } Echo?.AppendLine(text); - Buffer.Clear(); } } diff --git a/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs b/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs index b548d7031..f77e43e29 100644 --- a/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs +++ b/tests/StackExchange.Redis.Tests/Issues/Issue2507.cs @@ -7,7 +7,7 @@ namespace StackExchange.Redis.Tests.Issues; [Collection(NonParallelCollection.Name)] public class Issue2507(ITestOutputHelper output, SharedConnectionFixture? fixture = null) : TestBase(output, fixture) { - [Fact(Explicit = true)] + [Fact(Explicit = true)] // note this may show as Inconclusive, depending on the runner public async Task Execute() { await using var conn = Create(shared: false); diff --git a/tests/StackExchange.Redis.Tests/KeyNotificationTests.cs b/tests/StackExchange.Redis.Tests/KeyNotificationTests.cs new file mode 100644 index 000000000..60469eb49 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/KeyNotificationTests.cs @@ -0,0 +1,698 @@ +using System; +using System.Buffers; +using System.Text; +using Xunit; +using Xunit.Sdk; + +namespace StackExchange.Redis.Tests; + +public class KeyNotificationTests(ITestOutputHelper log) +{ + [Theory] + [InlineData("foo", "foo")] + [InlineData("__foo__", "__foo__")] + [InlineData("__keyspace@4__:", "__keyspace@4__:")] // not long enough + [InlineData("__keyspace@4__:f", "f")] + [InlineData("__keyspace@4__:fo", "fo")] + [InlineData("__keyspace@4__:foo", "foo")] + [InlineData("__keyspace@42__:foo", "foo")] // check multi-char db + [InlineData("__keyevent@4__:foo", "__keyevent@4__:foo")] // key-event + [InlineData("__keyevent@42__:foo", "__keyevent@42__:foo")] // key-event + public void RoutingSpan_StripKeySpacePrefix(string raw, string routed) + { + ReadOnlySpan srcBytes = Encoding.UTF8.GetBytes(raw); + var strippedBytes = RedisChannel.StripKeySpacePrefix(srcBytes); + var result = Encoding.UTF8.GetString(strippedBytes); + Assert.Equal(routed, result); + } + + [Fact] + public void Keyspace_Del_ParsesCorrectly() + { + // __keyspace@1__:mykey with payload "del" + var channel = RedisChannel.Literal("__keyspace@1__:mykey"); + Assert.False(channel.IgnoreChannelPrefix); // because constructed manually + RedisValue value = "del"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeySpace); + Assert.False(notification.IsKeyEvent); + Assert.Equal(1, notification.Database); + Assert.Equal(KeyNotificationType.Del, notification.Type); + Assert.True(notification.IsType("del"u8)); + Assert.Equal("mykey", (string?)notification.GetKey()); + Assert.Equal(5, notification.GetKeyByteCount()); + Assert.Equal(5, notification.GetKeyMaxByteCount()); + Assert.Equal(5, notification.GetKeyCharCount()); + Assert.Equal(6, notification.GetKeyMaxCharCount()); + } + + [Fact] + public void Keyevent_Del_ParsesCorrectly() + { + // __keyevent@42__:del with value "mykey" + var channel = RedisChannel.Literal("__keyevent@42__:del"); + RedisValue value = "mykey"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.False(notification.IsKeySpace); + Assert.True(notification.IsKeyEvent); + Assert.Equal(42, notification.Database); + Assert.Equal(KeyNotificationType.Del, notification.Type); + Assert.True(notification.IsType("del"u8)); + Assert.Equal("mykey", (string?)notification.GetKey()); + Assert.Equal(5, notification.GetKeyByteCount()); + Assert.Equal(18, notification.GetKeyMaxByteCount()); + Assert.Equal(5, notification.GetKeyCharCount()); + Assert.Equal(5, notification.GetKeyMaxCharCount()); + } + + [Fact] + public void Keyspace_Set_ParsesCorrectly() + { + var channel = RedisChannel.Literal("__keyspace@0__:testkey"); + RedisValue value = "set"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeySpace); + Assert.Equal(0, notification.Database); + Assert.Equal(KeyNotificationType.Set, notification.Type); + Assert.True(notification.IsType("set"u8)); + Assert.Equal("testkey", (string?)notification.GetKey()); + Assert.Equal(7, notification.GetKeyByteCount()); + Assert.Equal(7, notification.GetKeyMaxByteCount()); + Assert.Equal(7, notification.GetKeyCharCount()); + Assert.Equal(8, notification.GetKeyMaxCharCount()); + } + + [Fact] + public void Keyevent_Expire_ParsesCorrectly() + { + var channel = RedisChannel.Literal("__keyevent@5__:expire"); + RedisValue value = "session:12345"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeyEvent); + Assert.Equal(5, notification.Database); + Assert.Equal(KeyNotificationType.Expire, notification.Type); + Assert.True(notification.IsType("expire"u8)); + Assert.Equal("session:12345", (string?)notification.GetKey()); + Assert.Equal(13, notification.GetKeyByteCount()); + Assert.Equal(42, notification.GetKeyMaxByteCount()); + Assert.Equal(13, notification.GetKeyCharCount()); + Assert.Equal(13, notification.GetKeyMaxCharCount()); + } + + [Fact] + public void Keyspace_Expired_ParsesCorrectly() + { + var channel = RedisChannel.Literal("__keyspace@3__:cache:item"); + RedisValue value = "expired"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeySpace); + Assert.Equal(3, notification.Database); + Assert.Equal(KeyNotificationType.Expired, notification.Type); + Assert.True(notification.IsType("expired"u8)); + Assert.Equal("cache:item", (string?)notification.GetKey()); + Assert.Equal(10, notification.GetKeyByteCount()); + Assert.Equal(10, notification.GetKeyMaxByteCount()); + Assert.Equal(10, notification.GetKeyCharCount()); + Assert.Equal(11, notification.GetKeyMaxCharCount()); + } + + [Fact] + public void Keyevent_LPush_ParsesCorrectly() + { + var channel = RedisChannel.Literal("__keyevent@0__:lpush"); + RedisValue value = "queue:tasks"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeyEvent); + Assert.Equal(0, notification.Database); + Assert.Equal(KeyNotificationType.LPush, notification.Type); + Assert.True(notification.IsType("lpush"u8)); + Assert.Equal("queue:tasks", (string?)notification.GetKey()); + Assert.Equal(11, notification.GetKeyByteCount()); + Assert.Equal(36, notification.GetKeyMaxByteCount()); + Assert.Equal(11, notification.GetKeyCharCount()); + Assert.Equal(11, notification.GetKeyMaxCharCount()); + } + + [Fact] + public void Keyspace_HSet_ParsesCorrectly() + { + var channel = RedisChannel.Literal("__keyspace@2__:user:1000"); + RedisValue value = "hset"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeySpace); + Assert.Equal(2, notification.Database); + Assert.Equal(KeyNotificationType.HSet, notification.Type); + Assert.True(notification.IsType("hset"u8)); + Assert.Equal("user:1000", (string?)notification.GetKey()); + Assert.Equal(9, notification.GetKeyByteCount()); + Assert.Equal(9, notification.GetKeyMaxByteCount()); + Assert.Equal(9, notification.GetKeyCharCount()); + Assert.Equal(10, notification.GetKeyMaxCharCount()); + } + + [Fact] + public void Keyevent_ZAdd_ParsesCorrectly() + { + var channel = RedisChannel.Literal("__keyevent@7__:zadd"); + RedisValue value = "leaderboard"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeyEvent); + Assert.Equal(7, notification.Database); + Assert.Equal(KeyNotificationType.ZAdd, notification.Type); + Assert.True(notification.IsType("zadd"u8)); + Assert.Equal("leaderboard", (string?)notification.GetKey()); + Assert.Equal(11, notification.GetKeyByteCount()); + Assert.Equal(36, notification.GetKeyMaxByteCount()); + Assert.Equal(11, notification.GetKeyCharCount()); + Assert.Equal(11, notification.GetKeyMaxCharCount()); + } + + [Fact] + public void CustomEventWithUnusualValue_Works() + { + var channel = RedisChannel.Literal("__keyevent@7__:flooble"); + RedisValue value = 17.5; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeyEvent); + Assert.Equal(7, notification.Database); + Assert.Equal(KeyNotificationType.Unknown, notification.Type); + Assert.False(notification.IsType("zadd"u8)); + Assert.True(notification.IsType("flooble"u8)); + Assert.Equal("17.5", (string?)notification.GetKey()); + Assert.Equal(4, notification.GetKeyByteCount()); + Assert.Equal(40, notification.GetKeyMaxByteCount()); + Assert.Equal(4, notification.GetKeyCharCount()); + Assert.Equal(40, notification.GetKeyMaxCharCount()); + } + + [Fact] + public void TryCopyKey_WorksCorrectly() + { + var channel = RedisChannel.Literal("__keyspace@0__:testkey"); + RedisValue value = "set"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + var lease = ArrayPool.Shared.Rent(20); + Span buffer = lease.AsSpan(0, 20); + Assert.True(notification.TryCopyKey(buffer, out var bytesWritten)); + Assert.Equal(7, bytesWritten); + Assert.Equal("testkey", Encoding.UTF8.GetString(lease, 0, bytesWritten)); + ArrayPool.Shared.Return(lease); + } + + [Fact] + public void TryCopyKey_FailsWithSmallBuffer() + { + var channel = RedisChannel.Literal("__keyspace@0__:testkey"); + RedisValue value = "set"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Span buffer = stackalloc byte[3]; // too small + Assert.False(notification.TryCopyKey(buffer, out var bytesWritten)); + Assert.Equal(0, bytesWritten); + } + + [Fact] + public void InvalidChannel_ReturnsFalse() + { + var channel = RedisChannel.Literal("regular:channel"); + RedisValue value = "data"; + + Assert.False(KeyNotification.TryParse(in channel, in value, out var notification)); + } + + [Fact] + public void InvalidKeyspaceChannel_MissingDelimiter_ReturnsFalse() + { + var channel = RedisChannel.Literal("__keyspace@0__"); // missing the key part + RedisValue value = "set"; + + Assert.False(KeyNotification.TryParse(in channel, in value, out var notification)); + } + + [Fact] + public void Keyspace_UnknownEventType_ReturnsUnknown() + { + var channel = RedisChannel.Literal("__keyspace@0__:mykey"); + RedisValue value = "unknownevent"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeySpace); + Assert.Equal(0, notification.Database); + Assert.Equal(KeyNotificationType.Unknown, notification.Type); + Assert.False(notification.IsType("del"u8)); + Assert.Equal("mykey", (string?)notification.GetKey()); + } + + [Fact] + public void Keyevent_UnknownEventType_ReturnsUnknown() + { + var channel = RedisChannel.Literal("__keyevent@0__:unknownevent"); + RedisValue value = "mykey"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeyEvent); + Assert.Equal(0, notification.Database); + Assert.Equal(KeyNotificationType.Unknown, notification.Type); + Assert.False(notification.IsType("del"u8)); + Assert.Equal("mykey", (string?)notification.GetKey()); + } + + [Fact] + public void Keyspace_WithColonInKey_ParsesCorrectly() + { + var channel = RedisChannel.Literal("__keyspace@0__:user:session:12345"); + RedisValue value = "del"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeySpace); + Assert.Equal(0, notification.Database); + Assert.Equal(KeyNotificationType.Del, notification.Type); + Assert.True(notification.IsType("del"u8)); + Assert.Equal("user:session:12345", (string?)notification.GetKey()); + } + + [Fact] + public void Keyevent_Evicted_ParsesCorrectly() + { + var channel = RedisChannel.Literal("__keyevent@1__:evicted"); + RedisValue value = "cache:old"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeyEvent); + Assert.Equal(1, notification.Database); + Assert.Equal(KeyNotificationType.Evicted, notification.Type); + Assert.True(notification.IsType("evicted"u8)); + Assert.Equal("cache:old", (string?)notification.GetKey()); + } + + [Fact] + public void Keyspace_New_ParsesCorrectly() + { + var channel = RedisChannel.Literal("__keyspace@0__:newkey"); + RedisValue value = "new"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeySpace); + Assert.Equal(0, notification.Database); + Assert.Equal(KeyNotificationType.New, notification.Type); + Assert.True(notification.IsType("new"u8)); + Assert.Equal("newkey", (string?)notification.GetKey()); + } + + [Fact] + public void Keyevent_XGroupCreate_ParsesCorrectly() + { + var channel = RedisChannel.Literal("__keyevent@0__:xgroup-create"); + RedisValue value = "mystream"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeyEvent); + Assert.Equal(0, notification.Database); + Assert.Equal(KeyNotificationType.XGroupCreate, notification.Type); + Assert.True(notification.IsType("xgroup-create"u8)); + Assert.Equal("mystream", (string?)notification.GetKey()); + } + + [Fact] + public void Keyspace_TypeChanged_ParsesCorrectly() + { + var channel = RedisChannel.Literal("__keyspace@0__:mykey"); + RedisValue value = "type_changed"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeySpace); + Assert.Equal(0, notification.Database); + Assert.Equal(KeyNotificationType.TypeChanged, notification.Type); + Assert.True(notification.IsType("type_changed"u8)); + Assert.Equal("mykey", (string?)notification.GetKey()); + } + + [Fact] + public void Keyevent_HighDatabaseNumber_ParsesCorrectly() + { + var channel = RedisChannel.Literal("__keyevent@999__:set"); + RedisValue value = "testkey"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeyEvent); + Assert.Equal(999, notification.Database); + Assert.Equal(KeyNotificationType.Set, notification.Type); + Assert.True(notification.IsType("set"u8)); + Assert.Equal("testkey", (string?)notification.GetKey()); + } + + [Fact] + public void Keyevent_NonIntegerDatabase_ParsesWellEnough() + { + var channel = RedisChannel.Literal("__keyevent@abc__:set"); + RedisValue value = "testkey"; + + Assert.True(KeyNotification.TryParse(in channel, in value, out var notification)); + + Assert.True(notification.IsKeyEvent); + Assert.Equal(-1, notification.Database); + Assert.Equal(KeyNotificationType.Set, notification.Type); + Assert.True(notification.IsType("set"u8)); + Assert.Equal("testkey", (string?)notification.GetKey()); + } + + [Fact] + public void DefaultKeyNotification_HasExpectedProperties() + { + var notification = default(KeyNotification); + + Assert.False(notification.IsKeySpace); + Assert.False(notification.IsKeyEvent); + Assert.Equal(-1, notification.Database); + Assert.Equal(KeyNotificationType.Unknown, notification.Type); + Assert.False(notification.IsType("del"u8)); + Assert.True(notification.GetKey().IsNull); + Assert.Equal(0, notification.GetKeyByteCount()); + Assert.Equal(0, notification.GetKeyMaxByteCount()); + Assert.Equal(0, notification.GetKeyCharCount()); + Assert.Equal(0, notification.GetKeyMaxCharCount()); + Assert.True(notification.GetChannel().IsNull); + Assert.True(notification.GetValue().IsNull); + + // TryCopyKey should return false and write 0 bytes + Span buffer = stackalloc byte[10]; + Assert.False(notification.TryCopyKey(buffer, out var bytesWritten)); + Assert.Equal(0, bytesWritten); + } + + [Theory] + [InlineData(KeyNotificationTypeFastHash.append.Text, KeyNotificationType.Append)] + [InlineData(KeyNotificationTypeFastHash.copy.Text, KeyNotificationType.Copy)] + [InlineData(KeyNotificationTypeFastHash.del.Text, KeyNotificationType.Del)] + [InlineData(KeyNotificationTypeFastHash.expire.Text, KeyNotificationType.Expire)] + [InlineData(KeyNotificationTypeFastHash.hdel.Text, KeyNotificationType.HDel)] + [InlineData(KeyNotificationTypeFastHash.hexpired.Text, KeyNotificationType.HExpired)] + [InlineData(KeyNotificationTypeFastHash.hincrbyfloat.Text, KeyNotificationType.HIncrByFloat)] + [InlineData(KeyNotificationTypeFastHash.hincrby.Text, KeyNotificationType.HIncrBy)] + [InlineData(KeyNotificationTypeFastHash.hpersist.Text, KeyNotificationType.HPersist)] + [InlineData(KeyNotificationTypeFastHash.hset.Text, KeyNotificationType.HSet)] + [InlineData(KeyNotificationTypeFastHash.incrbyfloat.Text, KeyNotificationType.IncrByFloat)] + [InlineData(KeyNotificationTypeFastHash.incrby.Text, KeyNotificationType.IncrBy)] + [InlineData(KeyNotificationTypeFastHash.linsert.Text, KeyNotificationType.LInsert)] + [InlineData(KeyNotificationTypeFastHash.lpop.Text, KeyNotificationType.LPop)] + [InlineData(KeyNotificationTypeFastHash.lpush.Text, KeyNotificationType.LPush)] + [InlineData(KeyNotificationTypeFastHash.lrem.Text, KeyNotificationType.LRem)] + [InlineData(KeyNotificationTypeFastHash.lset.Text, KeyNotificationType.LSet)] + [InlineData(KeyNotificationTypeFastHash.ltrim.Text, KeyNotificationType.LTrim)] + [InlineData(KeyNotificationTypeFastHash.move_from.Text, KeyNotificationType.MoveFrom)] + [InlineData(KeyNotificationTypeFastHash.move_to.Text, KeyNotificationType.MoveTo)] + [InlineData(KeyNotificationTypeFastHash.persist.Text, KeyNotificationType.Persist)] + [InlineData(KeyNotificationTypeFastHash.rename_from.Text, KeyNotificationType.RenameFrom)] + [InlineData(KeyNotificationTypeFastHash.rename_to.Text, KeyNotificationType.RenameTo)] + [InlineData(KeyNotificationTypeFastHash.restore.Text, KeyNotificationType.Restore)] + [InlineData(KeyNotificationTypeFastHash.rpop.Text, KeyNotificationType.RPop)] + [InlineData(KeyNotificationTypeFastHash.rpush.Text, KeyNotificationType.RPush)] + [InlineData(KeyNotificationTypeFastHash.sadd.Text, KeyNotificationType.SAdd)] + [InlineData(KeyNotificationTypeFastHash.set.Text, KeyNotificationType.Set)] + [InlineData(KeyNotificationTypeFastHash.setrange.Text, KeyNotificationType.SetRange)] + [InlineData(KeyNotificationTypeFastHash.sortstore.Text, KeyNotificationType.SortStore)] + [InlineData(KeyNotificationTypeFastHash.srem.Text, KeyNotificationType.SRem)] + [InlineData(KeyNotificationTypeFastHash.spop.Text, KeyNotificationType.SPop)] + [InlineData(KeyNotificationTypeFastHash.xadd.Text, KeyNotificationType.XAdd)] + [InlineData(KeyNotificationTypeFastHash.xdel.Text, KeyNotificationType.XDel)] + [InlineData(KeyNotificationTypeFastHash.xgroup_createconsumer.Text, KeyNotificationType.XGroupCreateConsumer)] + [InlineData(KeyNotificationTypeFastHash.xgroup_create.Text, KeyNotificationType.XGroupCreate)] + [InlineData(KeyNotificationTypeFastHash.xgroup_delconsumer.Text, KeyNotificationType.XGroupDelConsumer)] + [InlineData(KeyNotificationTypeFastHash.xgroup_destroy.Text, KeyNotificationType.XGroupDestroy)] + [InlineData(KeyNotificationTypeFastHash.xgroup_setid.Text, KeyNotificationType.XGroupSetId)] + [InlineData(KeyNotificationTypeFastHash.xsetid.Text, KeyNotificationType.XSetId)] + [InlineData(KeyNotificationTypeFastHash.xtrim.Text, KeyNotificationType.XTrim)] + [InlineData(KeyNotificationTypeFastHash.zadd.Text, KeyNotificationType.ZAdd)] + [InlineData(KeyNotificationTypeFastHash.zdiffstore.Text, KeyNotificationType.ZDiffStore)] + [InlineData(KeyNotificationTypeFastHash.zinterstore.Text, KeyNotificationType.ZInterStore)] + [InlineData(KeyNotificationTypeFastHash.zunionstore.Text, KeyNotificationType.ZUnionStore)] + [InlineData(KeyNotificationTypeFastHash.zincr.Text, KeyNotificationType.ZIncr)] + [InlineData(KeyNotificationTypeFastHash.zrembyrank.Text, KeyNotificationType.ZRemByRank)] + [InlineData(KeyNotificationTypeFastHash.zrembyscore.Text, KeyNotificationType.ZRemByScore)] + [InlineData(KeyNotificationTypeFastHash.zrem.Text, KeyNotificationType.ZRem)] + [InlineData(KeyNotificationTypeFastHash.expired.Text, KeyNotificationType.Expired)] + [InlineData(KeyNotificationTypeFastHash.evicted.Text, KeyNotificationType.Evicted)] + [InlineData(KeyNotificationTypeFastHash._new.Text, KeyNotificationType.New)] + [InlineData(KeyNotificationTypeFastHash.overwritten.Text, KeyNotificationType.Overwritten)] + [InlineData(KeyNotificationTypeFastHash.type_changed.Text, KeyNotificationType.TypeChanged)] + public unsafe void FastHashParse_AllKnownValues_ParseCorrectly(string raw, KeyNotificationType parsed) + { + var arr = ArrayPool.Shared.Rent(Encoding.UTF8.GetMaxByteCount(raw.Length)); + int bytes; + fixed (byte* bPtr = arr) // encode into the buffer + { + fixed (char* cPtr = raw) + { + bytes = Encoding.UTF8.GetBytes(cPtr, raw.Length, bPtr, arr.Length); + } + } + + var result = KeyNotificationTypeFastHash.Parse(arr.AsSpan(0, bytes)); + log.WriteLine($"Parsed '{raw}' as {result}"); + Assert.Equal(parsed, result); + + // and the other direction: + var fetchedBytes = KeyNotificationTypeFastHash.GetRawBytes(parsed); + string fetched; + fixed (byte* bPtr = fetchedBytes) + { + fetched = Encoding.UTF8.GetString(bPtr, fetchedBytes.Length); + } + + log.WriteLine($"Fetched '{raw}'"); + Assert.Equal(raw, fetched); + + ArrayPool.Shared.Return(arr); + } + + [Fact] + public void CreateKeySpaceNotification_Valid() + { + var channel = RedisChannel.KeySpaceSingleKey("abc", 42); + Assert.Equal("__keyspace@42__:abc", channel.ToString()); + Assert.False(channel.IsMultiNode); + Assert.True(channel.IsKeyRouted); + Assert.False(channel.IsSharded); + Assert.False(channel.IsPattern); + Assert.True(channel.IgnoreChannelPrefix); + } + + [Theory] + [InlineData(null, null, "__keyspace@*__:*")] + [InlineData("abc*", null, "__keyspace@*__:abc*")] + [InlineData(null, 42, "__keyspace@42__:*")] + [InlineData("abc*", 42, "__keyspace@42__:abc*")] + public void CreateKeySpaceNotificationPattern(string? pattern, int? database, string expected) + { + var channel = RedisChannel.KeySpacePattern(pattern, database); + Assert.Equal(expected, channel.ToString()); + Assert.True(channel.IsMultiNode); + Assert.False(channel.IsKeyRouted); + Assert.False(channel.IsSharded); + Assert.True(channel.IsPattern); + Assert.True(channel.IgnoreChannelPrefix); + } + + [Theory] + [InlineData("abc", null, "__keyspace@*__:abc*")] + [InlineData("abc", 42, "__keyspace@42__:abc*")] + public void CreateKeySpaceNotificationPrefix_Key(string prefix, int? database, string expected) + { + var channel = RedisChannel.KeySpacePrefix((RedisKey)prefix, database); + Assert.Equal(expected, channel.ToString()); + Assert.True(channel.IsMultiNode); + Assert.False(channel.IsKeyRouted); + Assert.False(channel.IsSharded); + Assert.True(channel.IsPattern); + Assert.True(channel.IgnoreChannelPrefix); + } + + [Theory] + [InlineData("abc", null, "__keyspace@*__:abc*")] + [InlineData("abc", 42, "__keyspace@42__:abc*")] + public void CreateKeySpaceNotificationPrefix_Span(string prefix, int? database, string expected) + { + var channel = RedisChannel.KeySpacePrefix((ReadOnlySpan)Encoding.UTF8.GetBytes(prefix), database); + Assert.Equal(expected, channel.ToString()); + Assert.True(channel.IsMultiNode); + Assert.False(channel.IsKeyRouted); + Assert.False(channel.IsSharded); + Assert.True(channel.IsPattern); + Assert.True(channel.IgnoreChannelPrefix); + } + + [Theory] + [InlineData("a?bc", null)] + [InlineData("a?bc", 42)] + [InlineData("a*bc", null)] + [InlineData("a*bc", 42)] + [InlineData("a[bc", null)] + [InlineData("a[bc", 42)] + public void CreateKeySpaceNotificationPrefix_DisallowGlob(string prefix, int? database) + { + var bytes = Encoding.UTF8.GetBytes(prefix); + var ex = Assert.Throws(() => + RedisChannel.KeySpacePrefix((RedisKey)bytes, database)); + Assert.StartsWith("The supplied key contains pattern characters, but patterns are not supported in this context.", ex.Message); + + ex = Assert.Throws(() => + RedisChannel.KeySpacePrefix((ReadOnlySpan)bytes, database)); + Assert.StartsWith("The supplied key contains pattern characters, but patterns are not supported in this context.", ex.Message); + } + + [Theory] + [InlineData(KeyNotificationType.Set, null, "__keyevent@*__:set", true)] + [InlineData(KeyNotificationType.XGroupCreate, null, "__keyevent@*__:xgroup-create", true)] + [InlineData(KeyNotificationType.Set, 42, "__keyevent@42__:set", false)] + [InlineData(KeyNotificationType.XGroupCreate, 42, "__keyevent@42__:xgroup-create", false)] + public void CreateKeyEventNotification(KeyNotificationType type, int? database, string expected, bool isPattern) + { + var channel = RedisChannel.KeyEvent(type, database); + Assert.Equal(expected, channel.ToString()); + Assert.True(channel.IsMultiNode); + Assert.False(channel.IsKeyRouted); + Assert.False(channel.IsSharded); + Assert.True(channel.IgnoreChannelPrefix); + if (isPattern) + { + Assert.True(channel.IsPattern); + } + else + { + Assert.False(channel.IsPattern); + } + } + + [Theory] + [InlineData("abc", "__keyspace@42__:abc")] + [InlineData("a*bc", "__keyspace@42__:a*bc")] // pattern-like is allowed, since not using PSUBSCRIBE + public void Cannot_KeyRoute_KeySpace_SingleKeyIsKeyRouted(string key, string pattern) + { + var channel = RedisChannel.KeySpaceSingleKey(key, 42); + Assert.Equal(pattern, channel.ToString()); + Assert.False(channel.IsMultiNode); + Assert.False(channel.IsPattern); + Assert.False(channel.IsSharded); + Assert.True(channel.IgnoreChannelPrefix); + Assert.True(channel.IsKeyRouted); + Assert.True(channel.WithKeyRouting().IsKeyRouted); // no change, still key-routed + Assert.Equal(RedisCommand.PUBLISH, channel.GetPublishCommand()); + } + + [Fact] + public void Cannot_KeyRoute_KeySpacePattern() + { + var channel = RedisChannel.KeySpacePattern("abc", 42); + Assert.True(channel.IsMultiNode); + Assert.False(channel.IsKeyRouted); + Assert.True(channel.IgnoreChannelPrefix); + Assert.StartsWith("Key routing is not supported for multi-node channels", Assert.Throws(() => channel.WithKeyRouting()).Message); + Assert.StartsWith("Publishing is not supported for multi-node channels", Assert.Throws(() => channel.GetPublishCommand()).Message); + } + + [Fact] + public void Cannot_KeyRoute_KeyEvent() + { + var channel = RedisChannel.KeyEvent(KeyNotificationType.Set, 42); + Assert.True(channel.IsMultiNode); + Assert.False(channel.IsKeyRouted); + Assert.True(channel.IgnoreChannelPrefix); + Assert.StartsWith("Key routing is not supported for multi-node channels", Assert.Throws(() => channel.WithKeyRouting()).Message); + Assert.StartsWith("Publishing is not supported for multi-node channels", Assert.Throws(() => channel.GetPublishCommand()).Message); + } + + [Fact] + public void Cannot_KeyRoute_KeyEvent_Custom() + { + var channel = RedisChannel.KeyEvent("foo"u8, 42); + Assert.True(channel.IsMultiNode); + Assert.False(channel.IsKeyRouted); + Assert.True(channel.IgnoreChannelPrefix); + Assert.StartsWith("Key routing is not supported for multi-node channels", Assert.Throws(() => channel.WithKeyRouting()).Message); + Assert.StartsWith("Publishing is not supported for multi-node channels", Assert.Throws(() => channel.GetPublishCommand()).Message); + } + + [Fact] + public void KeyEventPrefix_KeySpacePrefix_Length_Matches() + { + // this is a sanity check for the parsing step in KeyNotification.TryParse + Assert.Equal(KeyNotificationChannels.KeySpacePrefix.Length, KeyNotificationChannels.KeyEventPrefix.Length); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void KeyNotificationKeyStripping(bool asString) + { + Span blob = stackalloc byte[32]; + Span clob = stackalloc char[32]; + + RedisChannel channel = RedisChannel.Literal("__keyevent@0__:sadd"); + RedisValue value = asString ? "mykey:abc" : "mykey:abc"u8.ToArray(); + KeyNotification.TryParse(in channel, in value, out var notification); + Assert.Equal("mykey:abc", (string?)notification.GetKey()); + Assert.True(notification.KeyStartsWith("mykey:"u8)); + Assert.Equal(0, notification.KeyOffset); + + Assert.Equal(9, notification.GetKeyByteCount()); + Assert.Equal(asString ? 30 : 9, notification.GetKeyMaxByteCount()); + Assert.Equal(9, notification.GetKeyCharCount()); + Assert.Equal(asString ? 9 : 10, notification.GetKeyMaxCharCount()); + + Assert.True(notification.TryCopyKey(blob, out var bytesWritten)); + Assert.Equal(9, bytesWritten); + Assert.Equal("mykey:abc", Encoding.UTF8.GetString(blob.Slice(0, bytesWritten))); + + Assert.True(notification.TryCopyKey(clob, out var charsWritten)); + Assert.Equal(9, charsWritten); + Assert.Equal("mykey:abc", clob.Slice(0, charsWritten).ToString()); + + // now with a prefix + notification = notification.WithKeySlice("mykey:"u8.Length); + Assert.Equal("abc", (string?)notification.GetKey()); + Assert.False(notification.KeyStartsWith("mykey:"u8)); + Assert.Equal(6, notification.KeyOffset); + + Assert.Equal(3, notification.GetKeyByteCount()); + Assert.Equal(asString ? 24 : 3, notification.GetKeyMaxByteCount()); + Assert.Equal(3, notification.GetKeyCharCount()); + Assert.Equal(asString ? 3 : 4, notification.GetKeyMaxCharCount()); + + Assert.True(notification.TryCopyKey(blob, out bytesWritten)); + Assert.Equal(3, bytesWritten); + Assert.Equal("abc", Encoding.UTF8.GetString(blob.Slice(0, bytesWritten))); + + Assert.True(notification.TryCopyKey(clob, out charsWritten)); + Assert.Equal(3, charsWritten); + Assert.Equal("abc", clob.Slice(0, charsWritten).ToString()); + } +} diff --git a/tests/StackExchange.Redis.Tests/PubSubKeyNotificationTests.cs b/tests/StackExchange.Redis.Tests/PubSubKeyNotificationTests.cs new file mode 100644 index 000000000..723921d45 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/PubSubKeyNotificationTests.cs @@ -0,0 +1,419 @@ +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using StackExchange.Redis.KeyspaceIsolation; +using Xunit; + +namespace StackExchange.Redis.Tests; + +// ReSharper disable once UnusedMember.Global - used via test framework +public sealed class PubSubKeyNotificationTestsCluster(ITestOutputHelper output, ITestContextAccessor context, SharedConnectionFixture fixture) + : PubSubKeyNotificationTests(output, context, fixture) +{ + protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts; +} + +// ReSharper disable once UnusedMember.Global - used via test framework +public sealed class PubSubKeyNotificationTestsStandalone(ITestOutputHelper output, ITestContextAccessor context, SharedConnectionFixture fixture) + : PubSubKeyNotificationTests(output, context, fixture) +{ +} + +public abstract class PubSubKeyNotificationTests(ITestOutputHelper output, ITestContextAccessor context, SharedConnectionFixture? fixture = null) + : TestBase(output, fixture) +{ + private const int DefaultKeyCount = 10; + private const int DefaultEventCount = 512; + private CancellationToken CancellationToken => context.Current.CancellationToken; + + private RedisKey[] InventKeys(out byte[] prefix, int count = DefaultKeyCount) + { + RedisKey[] keys = new RedisKey[count]; + var prefixString = $"{Guid.NewGuid()}/"; + prefix = Encoding.UTF8.GetBytes(prefixString); + for (int i = 0; i < count; i++) + { + keys[i] = $"{prefixString}{Guid.NewGuid()}"; + } + return keys; + } + + [Obsolete("Use Create(withChannelPrefix: false) instead", error: true)] + private IInternalConnectionMultiplexer Create() => Create(withChannelPrefix: false); + private IInternalConnectionMultiplexer Create(bool withChannelPrefix) => + Create(channelPrefix: withChannelPrefix ? "prefix:" : null); + + private RedisKey SelectKey(RedisKey[] keys) => keys[SharedRandom.Next(0, keys.Length)]; + +#if NET6_0_OR_GREATER + private static Random SharedRandom => Random.Shared; +#else + private static Random SharedRandom { get; } = new(); +#endif + + [Fact] + public async Task KeySpace_Events_Enabled() + { + // see https://redis.io/docs/latest/develop/pubsub/keyspace-notifications/#configuration + await using var conn = Create(allowAdmin: true); + int failures = 0; + foreach (var ep in conn.GetEndPoints()) + { + var server = conn.GetServer(ep); + var config = (await server.ConfigGetAsync("notify-keyspace-events")).Single(); + Log($"[{Format.ToString(ep)}] notify-keyspace-events: '{config.Value}'"); + + // this is a very broad config, but it's what we use in CI (and probably a common basic config) + if (config.Value != "AKE") + { + failures++; + } + } + // for details, check the log output + Assert.Equal(0, failures); + } + + [Fact] + public async Task KeySpace_CanSubscribe_ManualPublish() + { + await using var conn = Create(withChannelPrefix: false); + var db = conn.GetDatabase(); + + var channel = RedisChannel.KeyEvent("nonesuch"u8, database: null); + Log($"Monitoring channel: {channel}"); + var sub = conn.GetSubscriber(); + await sub.UnsubscribeAsync(channel); + + int count = 0; + await sub.SubscribeAsync(channel, (_, _) => Interlocked.Increment(ref count)); + + // to publish, we need to remove the marker that this is a multi-node channel + var asLiteral = RedisChannel.Literal(channel.ToString()); + await sub.PublishAsync(asLiteral, Guid.NewGuid().ToString()); + + int expected = GetConnectedCount(conn, channel); + await Task.Delay(100).ForAwait(); + Assert.Equal(expected, count); + } + + // this looks past the horizon to see how many connections we actually have for a given channel, + // which could be more than 1 in a cluster scenario + private static int GetConnectedCount(IConnectionMultiplexer muxer, in RedisChannel channel) + => muxer is ConnectionMultiplexer typed && typed.TryGetSubscription(channel, out var sub) + ? sub.GetConnectionCount() : 1; + + private sealed class Counter + { + private int _count; + public int Count => Volatile.Read(ref _count); + public int Increment() => Interlocked.Increment(ref _count); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task KeyEvent_CanObserveSimple_ViaCallbackHandler(bool withChannelPrefix) + { + await using var conn = Create(withChannelPrefix); + var db = conn.GetDatabase(); + + var keys = InventKeys(out var prefix); + var channel = RedisChannel.KeyEvent(KeyNotificationType.SAdd, db.Database); + Assert.True(channel.IsMultiNode); + Assert.False(channel.IsPattern); + Log($"Monitoring channel: {channel}"); + var sub = conn.GetSubscriber(); + await sub.UnsubscribeAsync(channel); + Counter callbackCount = new(), matchingEventCount = new(); + TaskCompletionSource allDone = new(); + + ConcurrentDictionary observedCounts = new(); + foreach (var key in keys) + { + observedCounts[key.ToString()] = new(); + } + + await sub.SubscribeAsync(channel, (recvChannel, recvValue) => + { + callbackCount.Increment(); + if (KeyNotification.TryParse(in recvChannel, in recvValue, out var notification) + && notification is { IsKeyEvent: true, Type: KeyNotificationType.SAdd }) + { + OnNotification(notification, prefix, matchingEventCount, observedCounts, allDone); + } + }); + + await SendAndObserveAsync(keys, db, allDone, callbackCount, observedCounts); + await sub.UnsubscribeAsync(channel); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task KeyEvent_CanObserveSimple_ViaQueue(bool withChannelPrefix) + { + await using var conn = Create(withChannelPrefix); + var db = conn.GetDatabase(); + + var keys = InventKeys(out var prefix); + var channel = RedisChannel.KeyEvent(KeyNotificationType.SAdd, db.Database); + Assert.True(channel.IsMultiNode); + Assert.False(channel.IsPattern); + Log($"Monitoring channel: {channel}"); + var sub = conn.GetSubscriber(); + await sub.UnsubscribeAsync(channel); + Counter callbackCount = new(), matchingEventCount = new(); + TaskCompletionSource allDone = new(); + + ConcurrentDictionary observedCounts = new(); + foreach (var key in keys) + { + observedCounts[key.ToString()] = new(); + } + + var queue = await sub.SubscribeAsync(channel); + _ = Task.Run(async () => + { + await foreach (var msg in queue.WithCancellation(CancellationToken)) + { + callbackCount.Increment(); + if (msg.TryParseKeyNotification(out var notification) + && notification is { IsKeyEvent: true, Type: KeyNotificationType.SAdd }) + { + OnNotification(notification, prefix, matchingEventCount, observedCounts, allDone); + } + } + }); + + await SendAndObserveAsync(keys, db, allDone, callbackCount, observedCounts); + await queue.UnsubscribeAsync(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task KeyNotification_CanObserveSimple_ViaCallbackHandler(bool withChannelPrefix) + { + await using var conn = Create(withChannelPrefix); + var db = conn.GetDatabase(); + + var keys = InventKeys(out var prefix); + var channel = RedisChannel.KeySpacePrefix(prefix, db.Database); + Assert.True(channel.IsMultiNode); + Assert.True(channel.IsPattern); + Log($"Monitoring channel: {channel}"); + var sub = conn.GetSubscriber(); + await sub.UnsubscribeAsync(channel); + Counter callbackCount = new(), matchingEventCount = new(); + TaskCompletionSource allDone = new(); + + ConcurrentDictionary observedCounts = new(); + foreach (var key in keys) + { + observedCounts[key.ToString()] = new(); + } + + var queue = await sub.SubscribeAsync(channel); + _ = Task.Run(async () => + { + await foreach (var msg in queue.WithCancellation(CancellationToken)) + { + callbackCount.Increment(); + if (msg.TryParseKeyNotification(out var notification) + && notification is { IsKeySpace: true, Type: KeyNotificationType.SAdd }) + { + OnNotification(notification, prefix, matchingEventCount, observedCounts, allDone); + } + } + }); + + await SendAndObserveAsync(keys, db, allDone, callbackCount, observedCounts); + await sub.UnsubscribeAsync(channel); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task KeyNotification_CanObserveSimple_ViaQueue(bool withChannelPrefix) + { + await using var conn = Create(withChannelPrefix); + var db = conn.GetDatabase(); + + var keys = InventKeys(out var prefix); + var channel = RedisChannel.KeySpacePrefix(prefix, db.Database); + Assert.True(channel.IsMultiNode); + Assert.True(channel.IsPattern); + Log($"Monitoring channel: {channel}"); + var sub = conn.GetSubscriber(); + await sub.UnsubscribeAsync(channel); + Counter callbackCount = new(), matchingEventCount = new(); + TaskCompletionSource allDone = new(); + + ConcurrentDictionary observedCounts = new(); + foreach (var key in keys) + { + observedCounts[key.ToString()] = new(); + } + + await sub.SubscribeAsync(channel, (recvChannel, recvValue) => + { + callbackCount.Increment(); + if (KeyNotification.TryParse(in recvChannel, in recvValue, out var notification) + && notification is { IsKeySpace: true, Type: KeyNotificationType.SAdd }) + { + OnNotification(notification, prefix, matchingEventCount, observedCounts, allDone); + } + }); + + await SendAndObserveAsync(keys, db, allDone, callbackCount, observedCounts); + await sub.UnsubscribeAsync(channel); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + public async Task KeyNotification_CanObserveSingleKey_ViaQueue(bool withChannelPrefix, bool withKeyPrefix) + { + await using var conn = Create(withChannelPrefix); + string keyPrefix = withKeyPrefix ? "isolated:" : ""; + byte[] keyPrefixBytes = Encoding.UTF8.GetBytes(keyPrefix); + var db = conn.GetDatabase().WithKeyPrefix(keyPrefix); + + var keys = InventKeys(out var prefix, count: 1); + Log($"Using {Encoding.UTF8.GetString(prefix)} as filter prefix, sample key: {SelectKey(keys)}"); + var channel = RedisChannel.KeySpaceSingleKey(RedisKey.WithPrefix(keyPrefixBytes, keys.Single()), db.Database); + + Assert.False(channel.IsMultiNode); + Assert.False(channel.IsPattern); + Log($"Monitoring channel: {channel}, routing via {Encoding.UTF8.GetString(channel.RoutingSpan)}"); + + var sub = conn.GetSubscriber(); + await sub.UnsubscribeAsync(channel); + Counter callbackCount = new(), matchingEventCount = new(); + TaskCompletionSource allDone = new(); + + ConcurrentDictionary observedCounts = new(); + foreach (var key in keys) + { + observedCounts[key.ToString()] = new(); + } + + var queue = await sub.SubscribeAsync(channel); + _ = Task.Run(async () => + { + await foreach (var msg in queue.WithCancellation(CancellationToken)) + { + callbackCount.Increment(); + if (msg.TryParseKeyNotification(keyPrefixBytes, out var notification) + && notification is { IsKeySpace: true, Type: KeyNotificationType.SAdd }) + { + OnNotification(notification, prefix, matchingEventCount, observedCounts, allDone); + } + } + }); + + await SendAndObserveAsync(keys, db, allDone, callbackCount, observedCounts); + await sub.UnsubscribeAsync(channel); + } + + private void OnNotification( + in KeyNotification notification, + ReadOnlySpan prefix, + Counter matchingEventCount, + ConcurrentDictionary observedCounts, + TaskCompletionSource allDone) + { + if (notification.KeyStartsWith(prefix)) // avoid problems with parallel SADD tests + { + int currentCount = matchingEventCount.Increment(); + + // get the key and check that we expected it + var recvKey = notification.GetKey(); + Assert.True(observedCounts.TryGetValue(recvKey.ToString(), out var counter)); + +#if NET9_0_OR_GREATER + // it would be more efficient to stash the alt-lookup, but that would make our API here non-viable, + // since we need to support multiple frameworks + var viaAlt = FindViaAltLookup(notification, observedCounts.GetAlternateLookup>()); + Assert.Same(counter, viaAlt); +#endif + + // accounting... + if (counter.Increment() == 1) + { + Log($"Observed key: '{recvKey}' after {currentCount} events"); + } + + if (currentCount == DefaultEventCount) + { + allDone.TrySetResult(true); + } + } + } + + private async Task SendAndObserveAsync( + RedisKey[] keys, + IDatabase db, + TaskCompletionSource allDone, + Counter callbackCount, + ConcurrentDictionary observedCounts) + { + await Task.Delay(300).ForAwait(); // give it a moment to settle + + Dictionary sentCounts = new(keys.Length); + foreach (var key in keys) + { + sentCounts[key] = new(); + } + + for (int i = 0; i < DefaultEventCount; i++) + { + var key = SelectKey(keys); + sentCounts[key].Increment(); + await db.SetAddAsync(key, i); + } + + // Wait for all events to be observed + try + { + Assert.True(await allDone.Task.WithTimeout(5000)); + } + catch (TimeoutException) when (callbackCount.Count == 0) + { + Assert.Fail($"Timeout with zero events; are keyspace events enabled?"); + } + + foreach (var key in keys) + { + Assert.Equal(sentCounts[key].Count, observedCounts[key.ToString()].Count); + } + } + +#if NET9_0_OR_GREATER + // demonstrate that we can use the alt-lookup APIs to avoid string allocations + private static Counter? FindViaAltLookup( + in KeyNotification notification, + ConcurrentDictionary.AlternateLookup> lookup) + { + // Demonstrate typical alt-lookup usage; this is an advanced topic, so it + // isn't trivial to grok, but: this is typical of perf-focused APIs. + char[]? lease = null; + const int MAX_STACK = 128; + var maxLength = notification.GetKeyMaxCharCount(); + Span scratch = maxLength <= MAX_STACK + ? stackalloc char[MAX_STACK] + : (lease = ArrayPool.Shared.Rent(maxLength)); + Assert.True(notification.TryCopyKey(scratch, out var length)); + if (!lookup.TryGetValue(scratch.Slice(0, length), out var counter)) counter = null; + if (lease is not null) ArrayPool.Shared.Return(lease); + return counter; + } +#endif +} diff --git a/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs b/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs index 43bb4b2b8..691232218 100644 --- a/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubMultiserverTests.cs @@ -63,7 +63,7 @@ await sub.SubscribeAsync(channel, (_, val) => Assert.True(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); Assert.True(conn.GetSubscriptions().TryGetValue(channel, out var subscription)); - var initialServer = subscription.GetCurrentServer(); + var initialServer = subscription.GetAnyCurrentServer(); Assert.NotNull(initialServer); Assert.True(initialServer.IsConnected); Log("Connected to: " + initialServer); @@ -83,10 +83,10 @@ await sub.SubscribeAsync(channel, (_, val) => Assert.True(subscribedServerEndpoint.IsConnected, "subscribedServerEndpoint.IsConnected"); Assert.False(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); } - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); - Assert.True(subscription.IsConnected); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnectedAny()); + Assert.True(subscription.IsConnectedAny()); - var newServer = subscription.GetCurrentServer(); + var newServer = subscription.GetAnyCurrentServer(); Assert.NotNull(newServer); Assert.NotEqual(newServer, initialServer); Log("Now connected to: " + newServer); @@ -148,7 +148,7 @@ await sub.SubscribeAsync( Assert.True(subscribedServerEndpoint.IsSubscriberConnected, "subscribedServerEndpoint.IsSubscriberConnected"); Assert.True(conn.GetSubscriptions().TryGetValue(channel, out var subscription)); - var initialServer = subscription.GetCurrentServer(); + var initialServer = subscription.GetAnyCurrentServer(); Assert.NotNull(initialServer); Assert.True(initialServer.IsConnected); Log("Connected to: " + initialServer); @@ -169,10 +169,10 @@ await sub.SubscribeAsync( if (expectSuccess) { - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); - Assert.True(subscription.IsConnected); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnectedAny()); + Assert.True(subscription.IsConnectedAny()); - var newServer = subscription.GetCurrentServer(); + var newServer = subscription.GetAnyCurrentServer(); Assert.NotNull(newServer); Assert.NotEqual(newServer, initialServer); Log("Now connected to: " + newServer); @@ -180,16 +180,16 @@ await sub.SubscribeAsync( else { // This subscription shouldn't be able to reconnect by flags (demanding an unavailable server) - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); - Assert.False(subscription.IsConnected); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnectedAny()); + Assert.False(subscription.IsConnectedAny()); Log("Unable to reconnect (as expected)"); // Allow connecting back to the original conn.AllowConnect = true; - await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnected); - Assert.True(subscription.IsConnected); + await UntilConditionAsync(TimeSpan.FromSeconds(5), () => subscription.IsConnectedAny()); + Assert.True(subscription.IsConnectedAny()); - var newServer = subscription.GetCurrentServer(); + var newServer = subscription.GetAnyCurrentServer(); Assert.NotNull(newServer); Assert.Equal(newServer, initialServer); Log("Now connected to: " + newServer); diff --git a/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs b/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs index 7f6ad1561..391a0237a 100644 --- a/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs +++ b/tests/StackExchange.Redis.Tests/RedisValueEquivalencyTests.cs @@ -297,11 +297,17 @@ public void RedisValueStartsWith() Assert.False(x.StartsWith(123), LineNumber()); Assert.False(x.StartsWith(false), LineNumber()); - Assert.True(x.StartsWith(Encoding.ASCII.GetBytes("a")), LineNumber()); - Assert.True(x.StartsWith(Encoding.ASCII.GetBytes("ab")), LineNumber()); - Assert.True(x.StartsWith(Encoding.ASCII.GetBytes("abc")), LineNumber()); - Assert.False(x.StartsWith(Encoding.ASCII.GetBytes("abd")), LineNumber()); - Assert.False(x.StartsWith(Encoding.ASCII.GetBytes("abcd")), LineNumber()); + Assert.True(x.StartsWith((RedisValue)Encoding.ASCII.GetBytes("a")), LineNumber()); + Assert.True(x.StartsWith((RedisValue)Encoding.ASCII.GetBytes("ab")), LineNumber()); + Assert.True(x.StartsWith((RedisValue)Encoding.ASCII.GetBytes("abc")), LineNumber()); + Assert.False(x.StartsWith((RedisValue)Encoding.ASCII.GetBytes("abd")), LineNumber()); + Assert.False(x.StartsWith((RedisValue)Encoding.ASCII.GetBytes("abcd")), LineNumber()); + + Assert.True(x.StartsWith("a"u8), LineNumber()); + Assert.True(x.StartsWith("ab"u8), LineNumber()); + Assert.True(x.StartsWith("abc"u8), LineNumber()); + Assert.False(x.StartsWith("abd"u8), LineNumber()); + Assert.False(x.StartsWith("abcd"u8), LineNumber()); x = 10; // integers are effectively strings in this context Assert.True(x.StartsWith(1), LineNumber()); diff --git a/tests/StackExchange.Redis.Tests/SSLTests.cs b/tests/StackExchange.Redis.Tests/SSLTests.cs index 0dafe3f9b..c9c5cc2bb 100644 --- a/tests/StackExchange.Redis.Tests/SSLTests.cs +++ b/tests/StackExchange.Redis.Tests/SSLTests.cs @@ -240,7 +240,9 @@ public async Task RedisLabsSSL() Skip.IfNoConfig(nameof(TestConfig.Config.RedisLabsSslServer), TestConfig.Current.RedisLabsSslServer); Skip.IfNoConfig(nameof(TestConfig.Config.RedisLabsPfxPath), TestConfig.Current.RedisLabsPfxPath); +#pragma warning disable SYSLIB0057 var cert = new X509Certificate2(TestConfig.Current.RedisLabsPfxPath, ""); +#pragma warning restore SYSLIB0057 Assert.NotNull(cert); Log("Thumbprint: " + cert.Thumbprint); diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index f6e38236b..e02a6ac36 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -1,6 +1,7 @@  - net481;net8.0 + + net481;net10.0 Exe StackExchange.Redis.Tests true diff --git a/tests/StackExchange.Redis.Tests/SyncContextTests.cs b/tests/StackExchange.Redis.Tests/SyncContextTests.cs index b98caefeb..5feb37e3d 100644 --- a/tests/StackExchange.Redis.Tests/SyncContextTests.cs +++ b/tests/StackExchange.Redis.Tests/SyncContextTests.cs @@ -122,7 +122,7 @@ public MySyncContext(TextWriter log) private int _opCount; private void Incr() => Interlocked.Increment(ref _opCount); - public void Reset() => Thread.VolatileWrite(ref _opCount, 0); + public void Reset() => Volatile.Write(ref _opCount, 0); public override string ToString() => $"Sync context ({(IsCurrent ? "active" : "inactive")}): {OpCount}"; diff --git a/version.json b/version.json index ff21ff51a..fb6e5bc8a 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "2.10", + "version": "2.11", "versionHeightOffset": 0, "assemblyVersion": "2.0", "publicReleaseRefSpec": [ From 809055e3ea270da06366192a00861d9a9612b5f1 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 6 Feb 2026 17:00:15 +0000 Subject: [PATCH 408/435] update API 'shipped' files --- .../PublicAPI/PublicAPI.Shipped.txt | 84 +++++++++++++++++++ .../PublicAPI/PublicAPI.Unshipped.txt | 84 ------------------- 2 files changed, 84 insertions(+), 84 deletions(-) diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 5eaa42b3f..fce1aa454 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -2100,3 +2100,87 @@ StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey k [SER002]static StackExchange.Redis.ValueCondition.NotEqual(in StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ValueCondition [SER002]static StackExchange.Redis.ValueCondition.ParseDigest(System.ReadOnlySpan digest) -> StackExchange.Redis.ValueCondition [SER002]static StackExchange.Redis.ValueCondition.ParseDigest(System.ReadOnlySpan digest) -> StackExchange.Redis.ValueCondition +StackExchange.Redis.ChannelMessage.TryParseKeyNotification(System.ReadOnlySpan keyPrefix, out StackExchange.Redis.KeyNotification notification) -> bool +StackExchange.Redis.KeyNotification +StackExchange.Redis.KeyNotification.GetChannel() -> StackExchange.Redis.RedisChannel +StackExchange.Redis.KeyNotification.GetKeyByteCount() -> int +StackExchange.Redis.KeyNotification.GetKeyCharCount() -> int +StackExchange.Redis.KeyNotification.GetKeyMaxByteCount() -> int +StackExchange.Redis.KeyNotification.GetKeyMaxCharCount() -> int +StackExchange.Redis.KeyNotification.GetValue() -> StackExchange.Redis.RedisValue +StackExchange.Redis.KeyNotification.IsType(System.ReadOnlySpan type) -> bool +StackExchange.Redis.KeyNotification.KeyStartsWith(System.ReadOnlySpan prefix) -> bool +StackExchange.Redis.KeyNotification.TryCopyKey(System.Span destination, out int charsWritten) -> bool +StackExchange.Redis.KeyNotification.Database.get -> int +StackExchange.Redis.KeyNotification.GetKey() -> StackExchange.Redis.RedisKey +StackExchange.Redis.KeyNotification.IsKeyEvent.get -> bool +StackExchange.Redis.KeyNotification.IsKeySpace.get -> bool +StackExchange.Redis.KeyNotification.KeyNotification() -> void +StackExchange.Redis.KeyNotification.TryCopyKey(System.Span destination, out int bytesWritten) -> bool +StackExchange.Redis.KeyNotification.Type.get -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.RedisValue.StartsWith(System.ReadOnlySpan value) -> bool +static StackExchange.Redis.KeyNotification.TryParse(scoped in StackExchange.Redis.RedisChannel channel, scoped in StackExchange.Redis.RedisValue value, out StackExchange.Redis.KeyNotification notification) -> bool +StackExchange.Redis.ChannelMessage.TryParseKeyNotification(out StackExchange.Redis.KeyNotification notification) -> bool +static StackExchange.Redis.KeyNotification.TryParse(scoped in System.ReadOnlySpan keyPrefix, scoped in StackExchange.Redis.RedisChannel channel, scoped in StackExchange.Redis.RedisValue value, out StackExchange.Redis.KeyNotification notification) -> bool +static StackExchange.Redis.RedisChannel.KeyEvent(StackExchange.Redis.KeyNotificationType type, int? database = null) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.KeyEvent(System.ReadOnlySpan type, int? database) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.KeySpacePattern(in StackExchange.Redis.RedisKey pattern, int? database = null) -> StackExchange.Redis.RedisChannel +StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Append = 1 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Copy = 2 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Del = 3 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Evicted = 1001 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Expire = 4 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Expired = 1000 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.HDel = 5 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.HExpired = 6 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.HIncrBy = 8 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.HIncrByFloat = 7 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.HPersist = 9 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.HSet = 10 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.IncrBy = 12 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.IncrByFloat = 11 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.LInsert = 13 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.LPop = 14 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.LPush = 15 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.LRem = 16 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.LSet = 17 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.LTrim = 18 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.MoveFrom = 19 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.MoveTo = 20 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.New = 1002 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Overwritten = 1003 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Persist = 21 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.RenameFrom = 22 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.RenameTo = 23 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Restore = 24 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.RPop = 25 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.RPush = 26 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.SAdd = 27 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Set = 28 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.SetRange = 29 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.SortStore = 30 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.SPop = 32 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.SRem = 31 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.TypeChanged = 1004 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.Unknown = 0 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XAdd = 33 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XDel = 34 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XGroupCreate = 36 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XGroupCreateConsumer = 35 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XGroupDelConsumer = 37 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XGroupDestroy = 38 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XGroupSetId = 39 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XSetId = 40 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.XTrim = 41 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZAdd = 42 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZDiffStore = 43 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZIncr = 46 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZInterStore = 44 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZRem = 49 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZRemByRank = 47 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZRemByScore = 48 -> StackExchange.Redis.KeyNotificationType +StackExchange.Redis.KeyNotificationType.ZUnionStore = 45 -> StackExchange.Redis.KeyNotificationType +static StackExchange.Redis.RedisChannel.KeySpacePrefix(in StackExchange.Redis.RedisKey prefix, int? database = null) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.KeySpacePrefix(System.ReadOnlySpan prefix, int? database = null) -> StackExchange.Redis.RedisChannel +static StackExchange.Redis.RedisChannel.KeySpaceSingleKey(in StackExchange.Redis.RedisKey key, int database) -> StackExchange.Redis.RedisChannel diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 6e96ed550..ab058de62 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,85 +1 @@ #nullable enable -StackExchange.Redis.ChannelMessage.TryParseKeyNotification(System.ReadOnlySpan keyPrefix, out StackExchange.Redis.KeyNotification notification) -> bool -StackExchange.Redis.KeyNotification -StackExchange.Redis.KeyNotification.GetChannel() -> StackExchange.Redis.RedisChannel -StackExchange.Redis.KeyNotification.GetKeyByteCount() -> int -StackExchange.Redis.KeyNotification.GetKeyCharCount() -> int -StackExchange.Redis.KeyNotification.GetKeyMaxByteCount() -> int -StackExchange.Redis.KeyNotification.GetKeyMaxCharCount() -> int -StackExchange.Redis.KeyNotification.GetValue() -> StackExchange.Redis.RedisValue -StackExchange.Redis.KeyNotification.IsType(System.ReadOnlySpan type) -> bool -StackExchange.Redis.KeyNotification.KeyStartsWith(System.ReadOnlySpan prefix) -> bool -StackExchange.Redis.KeyNotification.TryCopyKey(System.Span destination, out int charsWritten) -> bool -StackExchange.Redis.KeyNotification.Database.get -> int -StackExchange.Redis.KeyNotification.GetKey() -> StackExchange.Redis.RedisKey -StackExchange.Redis.KeyNotification.IsKeyEvent.get -> bool -StackExchange.Redis.KeyNotification.IsKeySpace.get -> bool -StackExchange.Redis.KeyNotification.KeyNotification() -> void -StackExchange.Redis.KeyNotification.TryCopyKey(System.Span destination, out int bytesWritten) -> bool -StackExchange.Redis.KeyNotification.Type.get -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.RedisValue.StartsWith(System.ReadOnlySpan value) -> bool -static StackExchange.Redis.KeyNotification.TryParse(scoped in StackExchange.Redis.RedisChannel channel, scoped in StackExchange.Redis.RedisValue value, out StackExchange.Redis.KeyNotification notification) -> bool -StackExchange.Redis.ChannelMessage.TryParseKeyNotification(out StackExchange.Redis.KeyNotification notification) -> bool -static StackExchange.Redis.KeyNotification.TryParse(scoped in System.ReadOnlySpan keyPrefix, scoped in StackExchange.Redis.RedisChannel channel, scoped in StackExchange.Redis.RedisValue value, out StackExchange.Redis.KeyNotification notification) -> bool -static StackExchange.Redis.RedisChannel.KeyEvent(StackExchange.Redis.KeyNotificationType type, int? database = null) -> StackExchange.Redis.RedisChannel -static StackExchange.Redis.RedisChannel.KeyEvent(System.ReadOnlySpan type, int? database) -> StackExchange.Redis.RedisChannel -static StackExchange.Redis.RedisChannel.KeySpacePattern(in StackExchange.Redis.RedisKey pattern, int? database = null) -> StackExchange.Redis.RedisChannel -StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.Append = 1 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.Copy = 2 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.Del = 3 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.Evicted = 1001 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.Expire = 4 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.Expired = 1000 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.HDel = 5 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.HExpired = 6 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.HIncrBy = 8 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.HIncrByFloat = 7 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.HPersist = 9 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.HSet = 10 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.IncrBy = 12 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.IncrByFloat = 11 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.LInsert = 13 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.LPop = 14 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.LPush = 15 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.LRem = 16 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.LSet = 17 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.LTrim = 18 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.MoveFrom = 19 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.MoveTo = 20 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.New = 1002 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.Overwritten = 1003 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.Persist = 21 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.RenameFrom = 22 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.RenameTo = 23 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.Restore = 24 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.RPop = 25 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.RPush = 26 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.SAdd = 27 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.Set = 28 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.SetRange = 29 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.SortStore = 30 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.SPop = 32 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.SRem = 31 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.TypeChanged = 1004 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.Unknown = 0 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.XAdd = 33 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.XDel = 34 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.XGroupCreate = 36 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.XGroupCreateConsumer = 35 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.XGroupDelConsumer = 37 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.XGroupDestroy = 38 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.XGroupSetId = 39 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.XSetId = 40 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.XTrim = 41 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.ZAdd = 42 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.ZDiffStore = 43 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.ZIncr = 46 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.ZInterStore = 44 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.ZRem = 49 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.ZRemByRank = 47 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.ZRemByScore = 48 -> StackExchange.Redis.KeyNotificationType -StackExchange.Redis.KeyNotificationType.ZUnionStore = 45 -> StackExchange.Redis.KeyNotificationType -static StackExchange.Redis.RedisChannel.KeySpacePrefix(in StackExchange.Redis.RedisKey prefix, int? database = null) -> StackExchange.Redis.RedisChannel -static StackExchange.Redis.RedisChannel.KeySpacePrefix(System.ReadOnlySpan prefix, int? database = null) -> StackExchange.Redis.RedisChannel -static StackExchange.Redis.RedisChannel.KeySpaceSingleKey(in StackExchange.Redis.RedisKey key, int database) -> StackExchange.Redis.RedisChannel From e71373bda9588c704966fe6c3636438d84200c39 Mon Sep 17 00:00:00 2001 From: Nick Craver Date: Fri, 6 Feb 2026 12:02:21 -0500 Subject: [PATCH 409/435] Options: Split out AMR to its own options provider (#2986) * Options: Split out AMR to its own options provider This breaks out AMR into its own default options provider to specify default versions but also allow us to do things like specify default RESP later on. * Add AMR to BuildInProviders * Add new cloud endpoints for OSS Azure Redis * Fix/add DefaultOptions tests * Move Enterprise domain root from OSS to AMR --------- Co-authored-by: Philo --- .../AzureManagedRedisOptionsProvider.cs | 62 +++++++++++++++++++ .../Configuration/AzureOptionsProvider.cs | 21 ++----- .../Configuration/DefaultOptionsProvider.cs | 1 + .../PublicAPI/PublicAPI.Shipped.txt | 7 +++ src/StackExchange.Redis/RedisFeatures.cs | 1 + .../StackExchange.Redis.Tests/ConfigTests.cs | 18 +++++- .../DefaultOptionsTests.cs | 16 ++++- 7 files changed, 103 insertions(+), 23 deletions(-) create mode 100644 src/StackExchange.Redis/Configuration/AzureManagedRedisOptionsProvider.cs diff --git a/src/StackExchange.Redis/Configuration/AzureManagedRedisOptionsProvider.cs b/src/StackExchange.Redis/Configuration/AzureManagedRedisOptionsProvider.cs new file mode 100644 index 000000000..06656b608 --- /dev/null +++ b/src/StackExchange.Redis/Configuration/AzureManagedRedisOptionsProvider.cs @@ -0,0 +1,62 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using StackExchange.Redis.Maintenance; + +namespace StackExchange.Redis.Configuration +{ + /// + /// Options provider for Azure Managed Redis environments. + /// + public class AzureManagedRedisOptionsProvider : DefaultOptionsProvider + { + /// + /// Allow connecting after startup, in the cases where remote cache isn't ready or is overloaded. + /// + public override bool AbortOnConnectFail => false; + + /// + /// The minimum version of Redis in Azure Managed Redis is 7.4, so use the widest set of available commands when connecting. + /// + public override Version DefaultVersion => RedisFeatures.v7_4_0; + + private static readonly string[] azureManagedRedisDomains = + [ + ".redis.azure.net", + ".redis.chinacloudapi.cn", + ".redis.usgovcloudapi.net", + ".redisenterprise.cache.azure.net", + ]; + + /// + public override bool IsMatch(EndPoint endpoint) + { + if (endpoint is DnsEndPoint dnsEp && IsHostInDomains(dnsEp.Host, azureManagedRedisDomains)) + { + return true; + } + + return false; + } + + private bool IsHostInDomains(string hostName, string[] domains) + { + foreach (var domain in domains) + { + if (hostName.EndsWith(domain, StringComparison.InvariantCultureIgnoreCase)) + { + return true; + } + } + + return false; + } + + /// + public override Task AfterConnectAsync(ConnectionMultiplexer muxer, Action log) + => AzureMaintenanceEvent.AddListenerAsync(muxer, log); + + /// + public override bool GetDefaultSsl(EndPointCollection endPoints) => true; + } +} diff --git a/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs b/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs index fb01f0704..c02f8f760 100644 --- a/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/AzureOptionsProvider.cs @@ -29,25 +29,16 @@ public class AzureOptionsProvider : DefaultOptionsProvider ".redis.cache.windows.net", ".redis.cache.chinacloudapi.cn", ".redis.cache.usgovcloudapi.net", - ".redisenterprise.cache.azure.net", - }; - - private static readonly string[] azureManagedRedisDomains = new[] - { - ".redis.azure.net", - ".redis.chinacloudapi.cn", - ".redis.usgovcloudapi.net", + ".redis.cache.sovcloud-api.de", + ".redis.cache.sovcloud-api.fr", }; /// public override bool IsMatch(EndPoint endpoint) { - if (endpoint is DnsEndPoint dnsEp) + if (endpoint is DnsEndPoint dnsEp && IsHostInDomains(dnsEp.Host, azureRedisDomains)) { - if (IsHostInDomains(dnsEp.Host, azureRedisDomains) || IsHostInDomains(dnsEp.Host, azureManagedRedisDomains)) - { - return true; - } + return true; } return false; @@ -82,10 +73,6 @@ public override bool GetDefaultSsl(EndPointCollection endPoints) { return true; } - if (dns.Port == 10000 && IsHostInDomains(dns.Host, azureManagedRedisDomains)) - { - return true; // SSL is enabled by default on AMR caches - } break; case IPEndPoint ip: if (ip.Port == 6380) diff --git a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs index 703adbcac..e4fa25891 100644 --- a/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs +++ b/src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs @@ -26,6 +26,7 @@ public class DefaultOptionsProvider private static readonly List BuiltInProviders = new() { new AzureOptionsProvider(), + new AzureManagedRedisOptionsProvider(), }; /// diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index fce1aa454..d2996e2c2 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -14,6 +14,11 @@ override StackExchange.Redis.Configuration.AzureOptionsProvider.AfterConnectAsyn override StackExchange.Redis.Configuration.AzureOptionsProvider.DefaultVersion.get -> System.Version! override StackExchange.Redis.Configuration.AzureOptionsProvider.GetDefaultSsl(StackExchange.Redis.EndPointCollection! endPoints) -> bool override StackExchange.Redis.Configuration.AzureOptionsProvider.IsMatch(System.Net.EndPoint! endpoint) -> bool +override StackExchange.Redis.Configuration.AzureManagedRedisOptionsProvider.AbortOnConnectFail.get -> bool +override StackExchange.Redis.Configuration.AzureManagedRedisOptionsProvider.AfterConnectAsync(StackExchange.Redis.ConnectionMultiplexer! muxer, System.Action! log) -> System.Threading.Tasks.Task! +override StackExchange.Redis.Configuration.AzureManagedRedisOptionsProvider.DefaultVersion.get -> System.Version! +override StackExchange.Redis.Configuration.AzureManagedRedisOptionsProvider.GetDefaultSsl(StackExchange.Redis.EndPointCollection! endPoints) -> bool +override StackExchange.Redis.Configuration.AzureManagedRedisOptionsProvider.IsMatch(System.Net.EndPoint! endpoint) -> bool override StackExchange.Redis.ConfigurationOptions.ToString() -> string! override StackExchange.Redis.ConnectionCounters.ToString() -> string! override StackExchange.Redis.ConnectionFailedEventArgs.ToString() -> string! @@ -199,6 +204,8 @@ StackExchange.Redis.ConditionResult StackExchange.Redis.ConditionResult.WasSatisfied.get -> bool StackExchange.Redis.Configuration.AzureOptionsProvider StackExchange.Redis.Configuration.AzureOptionsProvider.AzureOptionsProvider() -> void +StackExchange.Redis.Configuration.AzureManagedRedisOptionsProvider +StackExchange.Redis.Configuration.AzureManagedRedisOptionsProvider.AzureManagedRedisOptionsProvider() -> void StackExchange.Redis.Configuration.DefaultOptionsProvider StackExchange.Redis.Configuration.DefaultOptionsProvider.ClientName.get -> string! StackExchange.Redis.Configuration.DefaultOptionsProvider.DefaultOptionsProvider() -> void diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index 0e6b410a9..d097e418c 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -45,6 +45,7 @@ namespace StackExchange.Redis v7_2_0_rc1 = new Version(7, 1, 240), // 7.2 RC1 is version 7.1.240 v7_4_0_rc1 = new Version(7, 3, 240), // 7.4 RC1 is version 7.3.240 v7_4_0_rc2 = new Version(7, 3, 241), // 7.4 RC2 is version 7.3.241 + v7_4_0 = new Version(7, 4, 0), v8_0_0_M04 = new Version(7, 9, 227), // 8.0 M04 is version 7.9.227 v8_2_0_rc1 = new Version(8, 1, 240), // 8.2 RC1 is version 8.1.240 v8_4_0_rc1 = new Version(8, 3, 224); // 8.4 RC1 is version 8.3.224 diff --git a/tests/StackExchange.Redis.Tests/ConfigTests.cs b/tests/StackExchange.Redis.Tests/ConfigTests.cs index 0c9286e17..3dfa4f99a 100644 --- a/tests/StackExchange.Redis.Tests/ConfigTests.cs +++ b/tests/StackExchange.Redis.Tests/ConfigTests.cs @@ -131,13 +131,25 @@ public void SslProtocols_InvalidValue() [InlineData("contoso.redis.cache.windows.net:6380", true)] [InlineData("contoso.REDIS.CACHE.chinacloudapi.cn:6380", true)] // added a few upper case chars to validate comparison [InlineData("contoso.redis.cache.usgovcloudapi.net:6380", true)] - [InlineData("contoso.redisenterprise.cache.azure.net:10000", false)] + [InlineData("contoso.redis.cache.sovcloud-api.de:6380", true)] + [InlineData("contoso.redis.cache.sovcloud-api.fr:6380", true)] + public void ConfigurationOptionsDefaultForAzure(string hostAndPort, bool sslShouldBeEnabled) + { + Version defaultAzureVersion = new(6, 0, 0); + var options = ConfigurationOptions.Parse(hostAndPort); + Assert.True(options.DefaultVersion.Equals(defaultAzureVersion)); + Assert.False(options.AbortOnConnectFail); + Assert.Equal(sslShouldBeEnabled, options.Ssl); + } + + [Theory] [InlineData("contoso.redis.azure.net:10000", true)] [InlineData("contoso.redis.chinacloudapi.cn:10000", true)] [InlineData("contoso.redis.usgovcloudapi.net:10000", true)] - public void ConfigurationOptionsDefaultForAzure(string hostAndPort, bool sslShouldBeEnabled) + [InlineData("contoso.redisenterprise.cache.azure.net:10000", true)] + public void ConfigurationOptionsDefaultForAzureManagedRedis(string hostAndPort, bool sslShouldBeEnabled) { - Version defaultAzureVersion = new(6, 0, 0); + Version defaultAzureVersion = new(7, 4, 0); var options = ConfigurationOptions.Parse(hostAndPort); Assert.True(options.DefaultVersion.Equals(defaultAzureVersion)); Assert.False(options.AbortOnConnectFail); diff --git a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs index be80fd9c5..a01e845da 100644 --- a/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs +++ b/tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs @@ -65,15 +65,25 @@ public void IsMatchOnDomain() [InlineData("contoso.redis.cache.windows.net")] [InlineData("contoso.REDIS.CACHE.chinacloudapi.cn")] // added a few upper case chars to validate comparison [InlineData("contoso.redis.cache.usgovcloudapi.net")] - [InlineData("contoso.redisenterprise.cache.azure.net")] + [InlineData("contoso.redis.cache.sovcloud-api.de")] + [InlineData("contoso.redis.cache.sovcloud-api.fr")] + public void IsMatchOnAzureDomain(string hostName) + { + var epc = new EndPointCollection(new List() { new DnsEndPoint(hostName, 0) }); + var provider = DefaultOptionsProvider.GetProvider(epc); + Assert.IsType(provider); + } + + [Theory] [InlineData("contoso.redis.azure.net")] [InlineData("contoso.redis.chinacloudapi.cn")] [InlineData("contoso.redis.usgovcloudapi.net")] - public void IsMatchOnAzureDomain(string hostName) + [InlineData("contoso.redisenterprise.cache.azure.net")] + public void IsMatchOnAzureManagedRedisDomain(string hostName) { var epc = new EndPointCollection(new List() { new DnsEndPoint(hostName, 0) }); var provider = DefaultOptionsProvider.GetProvider(epc); - Assert.IsType(provider); + Assert.IsType(provider); } [Fact] From 18fdcd7377f058d6e74115723f6b4078da0e91a3 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 9 Feb 2026 14:02:47 +0000 Subject: [PATCH 410/435] Support 8.6 idempotent streams (#3006) * - XADD [IDMP|IDMPAUTO] - XINFO new fields - fix delta in XTRIM in 8.6 * - XCFGSET * - add [Experimental] - add docs --- Directory.Build.props | 2 +- docs/ReleaseNotes.md | 4 + docs/Streams.md | 18 ++ docs/exp/SER003.md | 25 +++ .../APITypes/StreamInfo.cs | 109 ++++++++++- src/StackExchange.Redis/Enums/RedisCommand.cs | 2 + src/StackExchange.Redis/Experiments.cs | 2 + .../Interfaces/IDatabase.cs | 52 +++++- .../Interfaces/IDatabaseAsync.cs | 13 ++ .../KeyspaceIsolation/KeyPrefixed.cs | 9 + .../KeyspaceIsolation/KeyPrefixedDatabase.cs | 10 ++ .../PublicAPI/PublicAPI.Unshipped.txt | 30 ++++ src/StackExchange.Redis/RedisDatabase.cs | 134 +++++++++++++- src/StackExchange.Redis/RedisFeatures.cs | 3 +- src/StackExchange.Redis/RedisLiterals.cs | 44 +++-- src/StackExchange.Redis/ResultProcessor.cs | 99 ++++++---- .../StreamConfiguration.cs | 20 +++ src/StackExchange.Redis/StreamIdempotentId.cs | 82 +++++++++ .../StackExchange.Redis.Tests/StreamTests.cs | 169 ++++++++++++++++-- 19 files changed, 762 insertions(+), 65 deletions(-) create mode 100644 docs/exp/SER003.md create mode 100644 src/StackExchange.Redis/StreamConfiguration.cs create mode 100644 src/StackExchange.Redis/StreamIdempotentId.cs diff --git a/Directory.Build.props b/Directory.Build.props index 06542aa32..e36f0f7d1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,7 +10,7 @@ true $(MSBuildThisFileDirectory)Shared.ruleset NETSDK1069 - $(NoWarn);NU5105;NU1507;SER001;SER002 + $(NoWarn);NU5105;NU1507;SER001;SER002;SER003 https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes https://stackexchange.github.io/StackExchange.Redis/ MIT diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index f77a1d10a..2ccc6d2d0 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,6 +6,10 @@ Current package versions: | ------------ | ----------------- | ----- | | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | +## unreleased + +- Implement idempotent stream entry (IDMP) support ([#3006 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3006)) + ## 2.10.14 - Fix bug with connection startup failing in low-memory scenarios ([#3002 by nathan-miller23](https://github.com/StackExchange/StackExchange.Redis/pull/3002)) diff --git a/docs/Streams.md b/docs/Streams.md index 4378a528d..47e82c2b9 100644 --- a/docs/Streams.md +++ b/docs/Streams.md @@ -37,6 +37,24 @@ You also have the option to override the auto-generated message ID by passing yo db.StreamAdd("events_stream", "foo_name", "bar_value", messageId: "0-1", maxLength: 100); ``` +Idempotent write-at-most-once production +=== + +From Redis 8.6, streams support idempotent write-at-most-once production. This is achieved by passing a `StreamIdempotentId` to the `StreamAdd` method. Using idempotent ids avoids +duplicate entries in the stream, even in the event of a failure and retry. + +The `StreamIdempotentId` contains a producer id and an optional idempotent id. The producer id should be unique for a given data generator and should be stable and consistent between runs. +The optional idempotent id should be unique for a given data item. If the idempotent id is not provided, the server will generate it from the content of the data item. + +```csharp +// int someUniqueExternalSourceId = ... // optional +var idempotentId = new StreamIdempotentId("ticket_generator"); +// optionally, new StreamIdempotentId("ticket_generator", someUniqueExternalSourceId) +var messageId = db.StreamAdd("events_stream", "foo_name", "bar_value", idempotentId); +``` + +~~~~The `StreamConfigure` method can be used to configure the stream, in particular the IDMP map. The `StreamConfiguration` class has properties for the idempotent producer (IDMP) duration and max-size. + Reading from Streams === diff --git a/docs/exp/SER003.md b/docs/exp/SER003.md new file mode 100644 index 000000000..651434063 --- /dev/null +++ b/docs/exp/SER003.md @@ -0,0 +1,25 @@ +Redis 8.6 is currently in preview and may be subject to change. + +New features in Redis 8.6 include: + +- `HOTKEYS` for profiling CPU and network hot-spots by key +- `XADD IDMP[AUTP]` for idempotent (write-at-most-once) stream addition + +The corresponding library feature must also be considered subject to change: + +1. Existing bindings may cease working correctly if the underlying server API changes. +2. Changes to the server API may require changes to the library API, manifesting in either/both of build-time + or run-time breaks. + +While this seems *unlikely*, it must be considered a possibility. If you acknowledge this, you can suppress +this warning by adding the following to your `csproj` file: + +```xml +$(NoWarn);SER003 +``` + +or more granularly / locally in C#: + +``` c# +#pragma warning disable SER003 +``` diff --git a/src/StackExchange.Redis/APITypes/StreamInfo.cs b/src/StackExchange.Redis/APITypes/StreamInfo.cs index 230ea47fb..e37df5add 100644 --- a/src/StackExchange.Redis/APITypes/StreamInfo.cs +++ b/src/StackExchange.Redis/APITypes/StreamInfo.cs @@ -1,11 +1,31 @@ -namespace StackExchange.Redis; +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis; /// /// Describes stream information retrieved using the XINFO STREAM command. . /// public readonly struct StreamInfo { - internal StreamInfo(int length, int radixTreeKeys, int radixTreeNodes, int groups, StreamEntry firstEntry, StreamEntry lastEntry, RedisValue lastGeneratedId) + // OK, I accept that this parameter list / size is getting silly, but: it is too late + // to refactor this as a class. + internal StreamInfo( + int length, + int radixTreeKeys, + int radixTreeNodes, + int groups, + StreamEntry firstEntry, + StreamEntry lastEntry, + RedisValue lastGeneratedId, + RedisValue maxDeletedEntryId, + long entriesAdded, + RedisValue recordedFirstEntryId, + long idmpDuration, + long idmpMaxSize, + long pidsTracked, + long iidsTracked, + long iidsAdded, + long iidsDuplicates) { Length = length; RadixTreeKeys = radixTreeKeys; @@ -14,6 +34,19 @@ internal StreamInfo(int length, int radixTreeKeys, int radixTreeNodes, int group FirstEntry = firstEntry; LastEntry = lastEntry; LastGeneratedId = lastGeneratedId; + + // 7.0 + MaxDeletedEntryId = maxDeletedEntryId; + EntriesAdded = entriesAdded; + RecordedFirstEntryId = recordedFirstEntryId; + + // 8.6 + IdmpDuration = idmpDuration; + IdmpMaxSize = idmpMaxSize; + PidsTracked = pidsTracked; + IidsTracked = iidsTracked; + IidsAdded = iidsAdded; + IidsDuplicates = iidsDuplicates; } /// @@ -50,4 +83,76 @@ internal StreamInfo(int length, int radixTreeKeys, int radixTreeNodes, int group /// The last generated id. /// public RedisValue LastGeneratedId { get; } + + /// + /// The first id recorded for the stream. + /// + public RedisValue RecordedFirstEntryId { get; } + + /// + /// The count of all entries added to the stream during its lifetime. + /// + public long EntriesAdded { get; } + + /// + /// The maximal entry ID that was deleted from the stream. + /// + public RedisValue MaxDeletedEntryId { get; } + + /// + /// The duration value configured for the stream’s IDMP map (seconds), or -1 if unavailable. + /// + public long IdmpDuration + { + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + get; + } + + /// + /// The maxsize value configured for the stream’s IDMP map, or -1 if unavailable. + /// + public long IdmpMaxSize + { + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + get; + } + + /// + /// The number of idempotent pids currently tracked in the stream, or -1 if unavailable. + /// + public long PidsTracked + { + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + get; + } + + /// + /// The number of idempotent ids currently tracked in the stream, or -1 if unavailable. + /// This count reflects active iids that haven't expired or been evicted yet. + /// + public long IidsTracked + { + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + get; + } + + /// + /// The count of all entries with an idempotent iid added to the stream during its lifetime, or -1 if unavailable. + /// This is a cumulative counter that increases with each idempotent entry added. + /// + public long IidsAdded + { + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + get; + } + + /// + /// The count of all duplicate iids (for all pids) detected during the stream's lifetime, or -1 if unavailable. + /// This is a cumulative counter that increases with each duplicate iid. + /// + public long IidsDuplicates + { + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + get; + } } diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 14f304a35..f731a6676 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -229,6 +229,7 @@ internal enum RedisCommand XADD, XAUTOCLAIM, XCLAIM, + XCFGSET, XDEL, XDELEX, XGROUP, @@ -375,6 +376,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.VREM: case RedisCommand.VSETATTR: case RedisCommand.XAUTOCLAIM: + case RedisCommand.XCFGSET: case RedisCommand.ZADD: case RedisCommand.ZDIFFSTORE: case RedisCommand.ZINTERSTORE: diff --git a/src/StackExchange.Redis/Experiments.cs b/src/StackExchange.Redis/Experiments.cs index 441b0ec54..547838873 100644 --- a/src/StackExchange.Redis/Experiments.cs +++ b/src/StackExchange.Redis/Experiments.cs @@ -12,6 +12,8 @@ internal static class Experiments public const string VectorSets = "SER001"; // ReSharper disable once InconsistentNaming public const string Server_8_4 = "SER002"; + // ReSharper disable once InconsistentNaming + public const string Server_8_6 = "SER003"; } } diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index 3df162682..cf2ecafac 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -2662,7 +2662,27 @@ IEnumerable SortedSetScan( /// #pragma warning disable RS0026 // different shape RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); -#pragma warning restore RS0026 + + /// + /// Adds an entry using the specified values to the given stream key. + /// If key does not exist, a new key holding a stream is created. + /// The command returns the ID of the newly created stream entry, using + /// the idempotent id (pid/iid) mechanism to ensure at-most-once production. + /// See for more information of the idempotent API. + /// + /// The key of the stream. + /// The field name for the stream entry. + /// The value to set in the stream entry. + /// The idempotent producer (pid) and optionally id (iid) to use for this entry. + /// The maximum length of the stream. + /// If true, the "~" argument is used to allow the stream to exceed max length by a small number. This improves performance when removing messages. + /// Specifies the maximal count of entries that will be evicted. + /// Determines how stream trimming should be performed. + /// The flags to use for this operation. + /// The ID of the newly created message. + /// + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); /// /// Adds an entry using the specified values to the given stream key. @@ -2679,10 +2699,38 @@ IEnumerable SortedSetScan( /// The flags to use for this operation. /// The ID of the newly created message. /// -#pragma warning disable RS0026 // different shape RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); + + /// + /// Adds an entry using the specified values to the given stream key. + /// If key does not exist, a new key holding a stream is created. + /// The command returns the ID of the newly created stream entry, using + /// the idempotent id (pid/iid) mechanism to ensure at-most-once production. + /// See for more information of the idempotent API. + /// + /// The key of the stream. + /// The fields and their associated values to set in the stream entry. + /// The idempotent producer (pid) and optionally id (iid) to use for this entry. + /// The maximum length of the stream. + /// If true, the "~" argument is used to allow the stream to exceed max length by a small number. This improves performance when removing messages. + /// Specifies the maximal count of entries that will be evicted. + /// Determines how stream trimming should be performed. + /// The flags to use for this operation. + /// The ID of the newly created message. + /// + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); #pragma warning restore RS0026 + /// + /// Configures a stream, in particular the IDMP map. + /// + /// The key of the stream. + /// The configuration to apply. + /// The flags to use for this operation. + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + void StreamConfigure(RedisKey key, StreamConfiguration configuration, CommandFlags flags = CommandFlags.None); + /// /// Change ownership of messages consumed, but not yet acknowledged, by a different consumer. /// Messages that have been idle for more than will be claimed. diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 855ea6c8f..029c7975e 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Net; using System.Threading.Tasks; @@ -655,8 +656,20 @@ IAsyncEnumerable SortedSetScanAsync( /// Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode trimMode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None); #pragma warning restore RS0026 + /// + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + Task StreamConfigureAsync(RedisKey key, StreamConfiguration configuration, CommandFlags flags = CommandFlags.None); + /// Task StreamAutoClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs index fe23b73c1..c7831fdb8 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs @@ -621,6 +621,15 @@ public Task StreamAddAsync(RedisKey key, RedisValue streamField, Red public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => Inner.StreamAddAsync(ToInner(key), streamPairs, messageId, maxLength, useApproximateMaxLength, limit, mode, flags); + public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + Inner.StreamAddAsync(ToInner(key), streamField, streamValue, idempotentId, maxLength, useApproximateMaxLength, limit, mode, flags); + + public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + Inner.StreamAddAsync(ToInner(key), streamPairs, idempotentId, maxLength, useApproximateMaxLength, limit, mode, flags); + + public Task StreamConfigureAsync(RedisKey key, StreamConfiguration configuration, CommandFlags flags = CommandFlags.None) => + Inner.StreamConfigureAsync(ToInner(key), configuration, flags); + public Task StreamAutoClaimAsync(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => Inner.StreamAutoClaimAsync(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, startAtId, count, flags); diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs index 69775c15d..01fe28505 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Net; namespace StackExchange.Redis.KeyspaceIsolation @@ -603,6 +604,15 @@ public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue str public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisValue? messageId = null, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => Inner.StreamAdd(ToInner(key), streamPairs, messageId, maxLength, useApproximateMaxLength, limit, mode, flags); + public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + Inner.StreamAdd(ToInner(key), streamField, streamValue, idempotentId, maxLength, useApproximateMaxLength, limit, mode, flags); + + public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) => + Inner.StreamAdd(ToInner(key), streamPairs, idempotentId, maxLength, useApproximateMaxLength, limit, mode, flags); + + public void StreamConfigure(RedisKey key, StreamConfiguration configuration, CommandFlags flags = CommandFlags.None) => + Inner.StreamConfigure(ToInner(key), configuration, flags); + public StreamAutoClaimResult StreamAutoClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) => Inner.StreamAutoClaim(ToInner(key), consumerGroup, claimingConsumer, minIdleTimeInMs, startAtId, count, flags); diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index ab058de62..3a80ab570 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,31 @@ #nullable enable +[SER003]override StackExchange.Redis.StreamIdempotentId.Equals(object? obj) -> bool +[SER003]override StackExchange.Redis.StreamIdempotentId.GetHashCode() -> int +[SER003]override StackExchange.Redis.StreamIdempotentId.ToString() -> string! +[SER003]StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +[SER003]StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +[SER003]StackExchange.Redis.IDatabase.StreamConfigure(StackExchange.Redis.RedisKey key, StackExchange.Redis.StreamConfiguration! configuration, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +[SER003]StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER003]StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER003]StackExchange.Redis.IDatabaseAsync.StreamConfigureAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.StreamConfiguration! configuration, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER003]StackExchange.Redis.StreamConfiguration +[SER003]StackExchange.Redis.StreamConfiguration.IdmpDuration.get -> long? +[SER003]StackExchange.Redis.StreamConfiguration.IdmpDuration.set -> void +[SER003]StackExchange.Redis.StreamConfiguration.IdmpMaxSize.get -> long? +[SER003]StackExchange.Redis.StreamConfiguration.IdmpMaxSize.set -> void +[SER003]StackExchange.Redis.StreamConfiguration.StreamConfiguration() -> void +[SER003]StackExchange.Redis.StreamIdempotentId +[SER003]StackExchange.Redis.StreamIdempotentId.IdempotentId.get -> StackExchange.Redis.RedisValue +[SER003]StackExchange.Redis.StreamIdempotentId.ProducerId.get -> StackExchange.Redis.RedisValue +[SER003]StackExchange.Redis.StreamIdempotentId.StreamIdempotentId() -> void +[SER003]StackExchange.Redis.StreamIdempotentId.StreamIdempotentId(StackExchange.Redis.RedisValue producerId) -> void +[SER003]StackExchange.Redis.StreamIdempotentId.StreamIdempotentId(StackExchange.Redis.RedisValue producerId, StackExchange.Redis.RedisValue idempotentId) -> void +[SER003]StackExchange.Redis.StreamInfo.IdmpDuration.get -> long +[SER003]StackExchange.Redis.StreamInfo.IdmpMaxSize.get -> long +[SER003]StackExchange.Redis.StreamInfo.IidsAdded.get -> long +[SER003]StackExchange.Redis.StreamInfo.IidsDuplicates.get -> long +[SER003]StackExchange.Redis.StreamInfo.IidsTracked.get -> long +[SER003]StackExchange.Redis.StreamInfo.PidsTracked.get -> long +StackExchange.Redis.StreamInfo.EntriesAdded.get -> long +StackExchange.Redis.StreamInfo.MaxDeletedEntryId.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.StreamInfo.RecordedFirstEntryId.get -> StackExchange.Redis.RedisValue diff --git a/src/StackExchange.Redis/RedisDatabase.cs b/src/StackExchange.Redis/RedisDatabase.cs index 056a5380a..ac3c14bcc 100644 --- a/src/StackExchange.Redis/RedisDatabase.cs +++ b/src/StackExchange.Redis/RedisDatabase.cs @@ -2782,6 +2782,23 @@ public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue str var msg = GetStreamAddMessage( key, messageId ?? StreamConstants.AutoGeneratedId, + StreamIdempotentId.Empty, + maxLength, + useApproximateMaxLength, + new NameValueEntry(streamField, streamValue), + limit, + mode, + flags); + + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + + public RedisValue StreamAdd(RedisKey key, RedisValue streamField, RedisValue streamValue, StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamAddMessage( + key, + StreamConstants.AutoGeneratedId, + idempotentId, maxLength, useApproximateMaxLength, new NameValueEntry(streamField, streamValue), @@ -2800,6 +2817,23 @@ public Task StreamAddAsync(RedisKey key, RedisValue streamField, Red var msg = GetStreamAddMessage( key, messageId ?? StreamConstants.AutoGeneratedId, + StreamIdempotentId.Empty, + maxLength, + useApproximateMaxLength, + new NameValueEntry(streamField, streamValue), + limit, + mode, + flags); + + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + + public Task StreamAddAsync(RedisKey key, RedisValue streamField, RedisValue streamValue, StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamAddMessage( + key, + StreamConstants.AutoGeneratedId, + idempotentId, maxLength, useApproximateMaxLength, new NameValueEntry(streamField, streamValue), @@ -2818,6 +2852,23 @@ public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, RedisVal var msg = GetStreamAddMessage( key, messageId ?? StreamConstants.AutoGeneratedId, + StreamIdempotentId.Empty, + maxLength, + useApproximateMaxLength, + streamPairs, + limit, + mode, + flags); + + return ExecuteSync(msg, ResultProcessor.RedisValue); + } + + public RedisValue StreamAdd(RedisKey key, NameValueEntry[] streamPairs, StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamAddMessage( + key, + StreamConstants.AutoGeneratedId, + idempotentId, maxLength, useApproximateMaxLength, streamPairs, @@ -2836,6 +2887,7 @@ public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPair var msg = GetStreamAddMessage( key, messageId ?? StreamConstants.AutoGeneratedId, + StreamIdempotentId.Empty, maxLength, useApproximateMaxLength, streamPairs, @@ -2846,6 +2898,78 @@ public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPair return ExecuteAsync(msg, ResultProcessor.RedisValue); } + public Task StreamAddAsync(RedisKey key, NameValueEntry[] streamPairs, StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StreamTrimMode mode = StreamTrimMode.KeepReferences, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamAddMessage( + key, + StreamConstants.AutoGeneratedId, + idempotentId, + maxLength, + useApproximateMaxLength, + streamPairs, + limit, + mode, + flags); + + return ExecuteAsync(msg, ResultProcessor.RedisValue); + } + + public void StreamConfigure(RedisKey key, StreamConfiguration configuration, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamConfigureMessage(key, configuration, flags); + ExecuteSync(msg, ResultProcessor.DemandOK); + } + + public Task StreamConfigureAsync(RedisKey key, StreamConfiguration configuration, CommandFlags flags = CommandFlags.None) + { + var msg = GetStreamConfigureMessage(key, configuration, flags); + return ExecuteAsync(msg, ResultProcessor.DemandOK); + } + + private Message GetStreamConfigureMessage(RedisKey key, StreamConfiguration configuration, CommandFlags flags) + { + if (key.IsNull) throw new ArgumentNullException(nameof(key)); + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + if (configuration.IdmpMaxSize.HasValue) + { + if (configuration.IdmpDuration.HasValue) + { + // duration and maxsize + return Message.Create( + Database, + flags, + RedisCommand.XCFGSET, + key, + RedisLiterals.IDMP_DURATION, + configuration.IdmpDuration.Value, + RedisLiterals.IDMP_MAXSIZE, + configuration.IdmpMaxSize.Value); + } + // just maxsize + return Message.Create( + Database, + flags, + RedisCommand.XCFGSET, + key, + RedisLiterals.IDMP_MAXSIZE, + configuration.IdmpMaxSize.Value); + } + + if (configuration.IdmpDuration.HasValue) + { + // just duration + return Message.Create( + Database, + flags, + RedisCommand.XCFGSET, + key, + RedisLiterals.IDMP_DURATION, + configuration.IdmpDuration.Value); + } + + return Message.Create(Database, flags, RedisCommand.XCFGSET, key); // this will manifest a -ERR, but let's use the server's message + } + public StreamAutoClaimResult StreamAutoClaim(RedisKey key, RedisValue consumerGroup, RedisValue claimingConsumer, long minIdleTimeInMs, RedisValue startAtId, int? count = null, CommandFlags flags = CommandFlags.None) { var msg = GetStreamAutoClaimMessage(key, consumerGroup, claimingConsumer, minIdleTimeInMs, startAtId, count, idsOnly: false, flags); @@ -4627,13 +4751,14 @@ private Message GetStreamAcknowledgeAndDeleteMessage(RedisKey key, RedisValue gr return Message.Create(Database, flags, RedisCommand.XACKDEL, key, values); } - private Message GetStreamAddMessage(RedisKey key, RedisValue messageId, long? maxLength, bool useApproximateMaxLength, NameValueEntry streamPair, long? limit, StreamTrimMode mode, CommandFlags flags) + private Message GetStreamAddMessage(in RedisKey key, RedisValue messageId, in StreamIdempotentId idempotentId, long? maxLength, bool useApproximateMaxLength, NameValueEntry streamPair, long? limit, StreamTrimMode mode, CommandFlags flags) { // Calculate the correct number of arguments: // 3 array elements for Entry ID & NameValueEntry.Name & NameValueEntry.Value. // 2 elements if using MAXLEN (keyword & value), otherwise 0. // 1 element if using Approximate Length (~), otherwise 0. var totalLength = 3 + (maxLength.HasValue ? 2 : 0) + + idempotentId.ArgCount + (maxLength.HasValue && useApproximateMaxLength ? 1 : 0) + (limit.HasValue ? 2 : 0) + (mode != StreamTrimMode.KeepReferences ? 1 : 0); @@ -4664,6 +4789,8 @@ private Message GetStreamAddMessage(RedisKey key, RedisValue messageId, long? ma values[offset++] = StreamConstants.GetMode(mode); } + idempotentId.WriteTo(values, ref offset); + values[offset++] = messageId; values[offset++] = streamPair.Name; @@ -4676,7 +4803,7 @@ private Message GetStreamAddMessage(RedisKey key, RedisValue messageId, long? ma /// /// Gets message for . /// - private Message GetStreamAddMessage(RedisKey key, RedisValue entryId, long? maxLength, bool useApproximateMaxLength, NameValueEntry[] streamPairs, long? limit, StreamTrimMode mode, CommandFlags flags) + private Message GetStreamAddMessage(in RedisKey key, RedisValue entryId, in StreamIdempotentId idempotentId, long? maxLength, bool useApproximateMaxLength, NameValueEntry[] streamPairs, long? limit, StreamTrimMode mode, CommandFlags flags) { if (streamPairs == null) throw new ArgumentNullException(nameof(streamPairs)); if (streamPairs.Length == 0) throw new ArgumentOutOfRangeException(nameof(streamPairs), "streamPairs must contain at least one item."); @@ -4688,6 +4815,7 @@ private Message GetStreamAddMessage(RedisKey key, RedisValue entryId, long? maxL var totalLength = (streamPairs.Length * 2) // Room for the name/value pairs + 1 // The stream entry ID + + idempotentId.ArgCount + (maxLength.HasValue ? 2 : 0) // MAXLEN N + (maxLength.HasValue && useApproximateMaxLength ? 1 : 0) // ~ + (mode == StreamTrimMode.KeepReferences ? 0 : 1) // relevant trim-mode keyword @@ -4720,6 +4848,8 @@ private Message GetStreamAddMessage(RedisKey key, RedisValue entryId, long? maxL values[offset++] = StreamConstants.GetMode(mode); } + idempotentId.WriteTo(values, ref offset); + values[offset++] = entryId; for (var i = 0; i < streamPairs.Length; i++) diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index d097e418c..d0ea77707 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -48,7 +48,8 @@ namespace StackExchange.Redis v7_4_0 = new Version(7, 4, 0), v8_0_0_M04 = new Version(7, 9, 227), // 8.0 M04 is version 7.9.227 v8_2_0_rc1 = new Version(8, 1, 240), // 8.2 RC1 is version 8.1.240 - v8_4_0_rc1 = new Version(8, 3, 224); // 8.4 RC1 is version 8.3.224 + v8_4_0_rc1 = new Version(8, 3, 224), + v8_6_0 = new Version(8, 5, 999); // 8.4 RC1 is version 8.3.224 #pragma warning restore SA1310 // Field names should not contain underscore #pragma warning restore SA1311 // Static readonly fields should begin with upper-case letter diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 9a8c15613..dd1522d71 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -4,13 +4,14 @@ namespace StackExchange.Redis { #pragma warning disable SA1310 // Field names should not contain underscore #pragma warning disable SA1311 // Static readonly fields should begin with upper-case letter - internal static class CommonReplies + internal static partial class CommonReplies { public static readonly CommandBytes ASK = "ASK ", authFail_trimmed = CommandBytes.TrimToFit("ERR operation not permitted"), backgroundSavingStarted_trimmed = CommandBytes.TrimToFit("Background saving started"), - backgroundSavingAOFStarted_trimmed = CommandBytes.TrimToFit("Background append only file rewriting started"), + backgroundSavingAOFStarted_trimmed = + CommandBytes.TrimToFit("Background append only file rewriting started"), databases = "databases", loading = "LOADING ", MOVED = "MOVED ", @@ -30,15 +31,6 @@ public static readonly CommandBytes yes = "yes", zero = "0", - // streams - length = "length", - radixTreeKeys = "radix-tree-keys", - radixTreeNodes = "radix-tree-nodes", - groups = "groups", - lastGeneratedId = "last-generated-id", - firstEntry = "first-entry", - lastEntry = "last-entry", - // HELLO version = "version", proto = "proto", @@ -46,6 +38,32 @@ public static readonly CommandBytes mode = "mode", id = "id"; } + + internal static partial class CommonRepliesHash + { +#pragma warning disable CS8981, SA1300, SA1134 // forgive naming + // ReSharper disable InconsistentNaming + [FastHash] internal static partial class length { } + [FastHash] internal static partial class radix_tree_keys { } + [FastHash] internal static partial class radix_tree_nodes { } + [FastHash] internal static partial class last_generated_id { } + [FastHash] internal static partial class max_deleted_entry_id { } + [FastHash] internal static partial class entries_added { } + [FastHash] internal static partial class recorded_first_entry_id { } + [FastHash] internal static partial class idmp_duration { } + [FastHash] internal static partial class idmp_maxsize { } + [FastHash] internal static partial class pids_tracked { } + [FastHash] internal static partial class first_entry { } + [FastHash] internal static partial class last_entry { } + [FastHash] internal static partial class groups { } + [FastHash] internal static partial class iids_tracked { } + [FastHash] internal static partial class iids_added { } + [FastHash] internal static partial class iids_duplicates { } + + // ReSharper restore InconsistentNaming +#pragma warning restore CS8981, SA1300, SA1134 // forgive naming + } + internal static class RedisLiterals { // unlike primary commands, these do not get altered by the command-map; we may as @@ -93,6 +111,10 @@ public static readonly RedisValue ID = "ID", IDX = "IDX", IDLETIME = "IDLETIME", + IDMP = "IDMP", + IDMPAUTO = "IDMPAUTO", + IDMP_DURATION = "IDMP-DURATION", + IDMP_MAXSIZE = "IDMP-MAXSIZE", KEEPTTL = "KEEPTTL", KILL = "KILL", LADDR = "LADDR", diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index f2c6deb8b..926fe8950 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -2537,43 +2537,71 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var arr = result.GetItems(); var max = arr.Length / 2; - long length = -1, radixTreeKeys = -1, radixTreeNodes = -1, groups = -1; - var lastGeneratedId = Redis.RedisValue.Null; + long length = -1, radixTreeKeys = -1, radixTreeNodes = -1, groups = -1, + entriesAdded = -1, idmpDuration = -1, idmpMaxsize = -1, + pidsTracked = -1, iidsTracked = -1, iidsAdded = -1, iidsDuplicates = -1; + RedisValue lastGeneratedId = Redis.RedisValue.Null, + maxDeletedEntryId = Redis.RedisValue.Null, + recordedFirstEntryId = Redis.RedisValue.Null; StreamEntry firstEntry = StreamEntry.Null, lastEntry = StreamEntry.Null; var iter = arr.GetEnumerator(); for (int i = 0; i < max; i++) { ref RawResult key = ref iter.GetNext(), value = ref iter.GetNext(); if (key.Payload.Length > CommandBytes.MaxLength) continue; - - var keyBytes = new CommandBytes(key.Payload); - if (keyBytes.Equals(CommonReplies.length)) - { - if (!value.TryGetInt64(out length)) return false; - } - else if (keyBytes.Equals(CommonReplies.radixTreeKeys)) - { - if (!value.TryGetInt64(out radixTreeKeys)) return false; - } - else if (keyBytes.Equals(CommonReplies.radixTreeNodes)) - { - if (!value.TryGetInt64(out radixTreeNodes)) return false; - } - else if (keyBytes.Equals(CommonReplies.groups)) - { - if (!value.TryGetInt64(out groups)) return false; - } - else if (keyBytes.Equals(CommonReplies.lastGeneratedId)) - { - lastGeneratedId = value.AsRedisValue(); - } - else if (keyBytes.Equals(CommonReplies.firstEntry)) - { - firstEntry = ParseRedisStreamEntry(value); - } - else if (keyBytes.Equals(CommonReplies.lastEntry)) + var hash = key.Payload.Hash64(); + switch (hash) { - lastEntry = ParseRedisStreamEntry(value); + case CommonRepliesHash.length.Hash when CommonRepliesHash.length.Is(hash, key): + if (!value.TryGetInt64(out length)) return false; + break; + case CommonRepliesHash.radix_tree_keys.Hash when CommonRepliesHash.radix_tree_keys.Is(hash, key): + if (!value.TryGetInt64(out radixTreeKeys)) return false; + break; + case CommonRepliesHash.radix_tree_nodes.Hash when CommonRepliesHash.radix_tree_nodes.Is(hash, key): + if (!value.TryGetInt64(out radixTreeNodes)) return false; + break; + case CommonRepliesHash.groups.Hash when CommonRepliesHash.groups.Is(hash, key): + if (!value.TryGetInt64(out groups)) return false; + break; + case CommonRepliesHash.last_generated_id.Hash when CommonRepliesHash.last_generated_id.Is(hash, key): + lastGeneratedId = value.AsRedisValue(); + break; + case CommonRepliesHash.first_entry.Hash when CommonRepliesHash.first_entry.Is(hash, key): + firstEntry = ParseRedisStreamEntry(value); + break; + case CommonRepliesHash.last_entry.Hash when CommonRepliesHash.last_entry.Is(hash, key): + lastEntry = ParseRedisStreamEntry(value); + break; + // 7.0 + case CommonRepliesHash.max_deleted_entry_id.Hash when CommonRepliesHash.max_deleted_entry_id.Is(hash, key): + maxDeletedEntryId = value.AsRedisValue(); + break; + case CommonRepliesHash.recorded_first_entry_id.Hash when CommonRepliesHash.recorded_first_entry_id.Is(hash, key): + recordedFirstEntryId = value.AsRedisValue(); + break; + case CommonRepliesHash.entries_added.Hash when CommonRepliesHash.entries_added.Is(hash, key): + if (!value.TryGetInt64(out entriesAdded)) return false; + break; + // 8.6 + case CommonRepliesHash.idmp_duration.Hash when CommonRepliesHash.idmp_duration.Is(hash, key): + if (!value.TryGetInt64(out idmpDuration)) return false; + break; + case CommonRepliesHash.idmp_maxsize.Hash when CommonRepliesHash.idmp_maxsize.Is(hash, key): + if (!value.TryGetInt64(out idmpMaxsize)) return false; + break; + case CommonRepliesHash.pids_tracked.Hash when CommonRepliesHash.pids_tracked.Is(hash, key): + if (!value.TryGetInt64(out pidsTracked)) return false; + break; + case CommonRepliesHash.iids_tracked.Hash when CommonRepliesHash.iids_tracked.Is(hash, key): + if (!value.TryGetInt64(out iidsTracked)) return false; + break; + case CommonRepliesHash.iids_added.Hash when CommonRepliesHash.iids_added.Is(hash, key): + if (!value.TryGetInt64(out iidsAdded)) return false; + break; + case CommonRepliesHash.iids_duplicates.Hash when CommonRepliesHash.iids_duplicates.Is(hash, key): + if (!value.TryGetInt64(out iidsDuplicates)) return false; + break; } } @@ -2584,7 +2612,16 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes groups: checked((int)groups), firstEntry: firstEntry, lastEntry: lastEntry, - lastGeneratedId: lastGeneratedId); + lastGeneratedId: lastGeneratedId, + maxDeletedEntryId: maxDeletedEntryId, + entriesAdded: entriesAdded, + recordedFirstEntryId: recordedFirstEntryId, + idmpDuration: idmpDuration, + idmpMaxSize: idmpMaxsize, + pidsTracked: pidsTracked, + iidsTracked: iidsTracked, + iidsAdded: iidsAdded, + iidsDuplicates: iidsDuplicates); SetResult(message, streamInfo); return true; diff --git a/src/StackExchange.Redis/StreamConfiguration.cs b/src/StackExchange.Redis/StreamConfiguration.cs new file mode 100644 index 000000000..71bbe483e --- /dev/null +++ b/src/StackExchange.Redis/StreamConfiguration.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis; + +/// +/// Configuration parameters for a stream, for example idempotent producer (IDMP) duration and maxsize. +/// +[Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] +public sealed class StreamConfiguration +{ + /// + /// How long the server remembers each iid, in seconds. + /// + public long? IdmpDuration { get; set; } + + /// + /// Maximum number of iids the server remembers per pid. + /// + public long? IdmpMaxSize { get; set; } +} diff --git a/src/StackExchange.Redis/StreamIdempotentId.cs b/src/StackExchange.Redis/StreamIdempotentId.cs new file mode 100644 index 000000000..601890d1f --- /dev/null +++ b/src/StackExchange.Redis/StreamIdempotentId.cs @@ -0,0 +1,82 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace StackExchange.Redis; + +/// +/// The idempotent id for a stream entry, ensuring at-most-once production. Each producer should have a unique +/// that is stable and consistent between runs. When adding stream entries, the +/// caller can specify an that is unique and repeatable for a given data item, or omit it +/// and let the server generate it from the content of the data item. In either event: duplicates are rejected. +/// +[Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] +public readonly struct StreamIdempotentId +{ + // note: if exposing wider, maybe expose as a by-ref property rather than a readonly field + internal static readonly StreamIdempotentId Empty = default; + + /// + /// Create a new with the given producer id. + /// + public StreamIdempotentId(RedisValue producerId) + { + if (producerId.IsNull) throw new ArgumentNullException(nameof(producerId)); + ProducerId = producerId; + IdempotentId = RedisValue.Null; + } + + /// + /// The idempotent id for a stream entry, ensuring at-most-once production. + /// + public StreamIdempotentId(RedisValue producerId, RedisValue idempotentId) + { + if (!producerId.HasValue) throw new ArgumentNullException(nameof(producerId)); + ProducerId = producerId; + IdempotentId = idempotentId; // can be explicit null, fine + } + + /// + /// The producer of the idempotent id; this is fixed for a given data generator. + /// + public RedisValue ProducerId { get; } + + /// + /// The optional idempotent id; this should be unique for a given data item. If omitted / null, + /// the server will generate the idempotent id from the content of the data item. + /// + public RedisValue IdempotentId { get; } + + /// + public override string ToString() + { + if (IdempotentId.HasValue) return $"IDMP {ProducerId} {IdempotentId}"; + if (ProducerId.HasValue) return $"IDMPAUTO {ProducerId}"; + return ""; + } + + internal int ArgCount => IdempotentId.HasValue ? 3 : ProducerId.HasValue ? 2 : 0; + + internal void WriteTo(RedisValue[] args, ref int index) + { + if (IdempotentId.HasValue) + { + args[index++] = RedisLiterals.IDMP; + args[index++] = ProducerId; + args[index++] = IdempotentId; + } + else if (ProducerId.HasValue) + { + args[index++] = RedisLiterals.IDMPAUTO; + args[index++] = ProducerId; + } + } + + /// + public override int GetHashCode() => ProducerId.GetHashCode() ^ IdempotentId.GetHashCode(); + + /// + public override bool Equals(object? obj) => + obj is StreamIdempotentId other + && ProducerId == other.ProducerId + && IdempotentId == other.IdempotentId; +} diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index 2419f673a..22d2a0159 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -81,6 +81,102 @@ public async Task StreamAddWithManualId() Assert.Equal(id, messageId); } + [Theory] + [InlineData(false, false, false)] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(false, true, true)] + [InlineData(true, false, false)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + [InlineData(true, true, true)] + public async Task StreamAddIdempotentId(bool iid, bool pairs, bool async) + { + await using var conn = Create(require: RedisFeatures.v8_6_0); + var db = conn.GetDatabase(); + StreamIdempotentId id = iid ? new StreamIdempotentId("pid", "iid") : new StreamIdempotentId("pid"); + Log($"id: {id}"); + var key = Me(); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + async Task Add() + { + if (pairs) + { + NameValueEntry[] fields = [new("field1", "value1"), new("field2", "value2"), new("field3", "value3")]; + if (async) + { + return await db.StreamAddAsync(key, fields, idempotentId: id); + } + + return db.StreamAdd(key, fields, idempotentId: id); + } + + if (async) + { + return await db.StreamAddAsync(key, "field1", "value1", idempotentId: id); + } + + return db.StreamAdd(key, "field1", "value1", idempotentId: id); + } + + RedisValue first = await Add(); + Log($"Message ID: {first}"); + + RedisValue second = await Add(); + Assert.Equal(first, second); // idempotent id has avoided a duplicate + } + + [Theory] + [InlineData(null, null, false)] + [InlineData(null, 42, false)] + [InlineData(13, null, false)] + [InlineData(13, 42, false)] + [InlineData(null, null, true)] + [InlineData(null, 42, true)] + [InlineData(13, null, true)] + [InlineData(13, 42, true)] + public async Task StreamConfigure(int? duration, int? maxsize, bool async) + { + await using var conn = Create(require: RedisFeatures.v8_6_0); + var db = conn.GetDatabase(); + + var key = Me(); + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + var id = await db.StreamAddAsync(key, "field1", "value1"); + Log($"id: {id}"); + var settings = new StreamConfiguration { IdmpDuration = duration, IdmpMaxSize = maxsize }; + bool doomed = duration is null && maxsize is null; + if (async) + { + if (doomed) + { + var ex = await Assert.ThrowsAsync(async () => await db.StreamConfigureAsync(key, settings)); + Assert.StartsWith("ERR At least one parameter must be specified", ex.Message); + } + else + { + await db.StreamConfigureAsync(key, settings); + } + } + else + { + if (doomed) + { + var ex = Assert.Throws(() => db.StreamConfigure(key, settings)); + Assert.StartsWith("ERR At least one parameter must be specified", ex.Message); + } + else + { + db.StreamConfigure(key, settings); + } + } + var info = async ? await db.StreamInfoAsync(key) : db.StreamInfo(key); + const int SERVER_DEFAULT = 100; + Assert.Equal(duration ?? SERVER_DEFAULT, info.IdmpDuration); + Assert.Equal(maxsize ?? SERVER_DEFAULT, info.IdmpMaxSize); + } + [Fact] public async Task StreamAddMultipleValuePairsWithManualId() { @@ -1562,16 +1658,51 @@ public async Task StreamInfoGet() var id1 = db.StreamAdd(key, "field1", "value1"); db.StreamAdd(key, "field2", "value2"); - db.StreamAdd(key, "field3", "value3"); - var id4 = db.StreamAdd(key, "field4", "value4"); - + var id3 = db.StreamAdd(key, "field3", "value3"); + db.StreamAdd(key, "field4", "value4"); + var id5 = db.StreamAdd(key, "field5", "value5"); + db.StreamDelete(key, [id3]); var streamInfo = db.StreamInfo(key); Assert.Equal(4, streamInfo.Length); Assert.True(streamInfo.RadixTreeKeys > 0); Assert.True(streamInfo.RadixTreeNodes > 0); Assert.Equal(id1, streamInfo.FirstEntry.Id); - Assert.Equal(id4, streamInfo.LastEntry.Id); + Assert.Equal(id5, streamInfo.LastEntry.Id); + + var server = conn.GetServer(conn.GetEndPoints().First()); + Log($"server version: {server.Version}"); + if (server.Version.IsAtLeast(RedisFeatures.v7_0_0_rc1)) + { + Assert.Equal(id3, streamInfo.MaxDeletedEntryId); + Assert.Equal(5, streamInfo.EntriesAdded); + Assert.False(streamInfo.RecordedFirstEntryId.IsNull); + } + else + { + Assert.True(streamInfo.MaxDeletedEntryId.IsNull); + Assert.Equal(-1, streamInfo.EntriesAdded); + Assert.True(streamInfo.RecordedFirstEntryId.IsNull); + } + + if (server.Version.IsAtLeast(RedisFeatures.v8_6_0)) + { + Assert.True(streamInfo.IdmpDuration > 0); + Assert.True(streamInfo.IdmpMaxSize > 0); + Assert.Equal(0, streamInfo.PidsTracked); + Assert.Equal(0, streamInfo.IidsTracked); + Assert.Equal(0, streamInfo.IidsDuplicates); + Assert.Equal(0, streamInfo.IidsAdded); + } + else + { + Assert.Equal(-1, streamInfo.IdmpDuration); + Assert.Equal(-1, streamInfo.IdmpMaxSize); + Assert.Equal(-1, streamInfo.PidsTracked); + Assert.Equal(-1, streamInfo.IidsTracked); + Assert.Equal(-1, streamInfo.IidsDuplicates); + Assert.Equal(-1, streamInfo.IidsAdded); + } } [Fact] @@ -2188,26 +2319,34 @@ public void StreamTrimByMinIdWithApproximateAndLimit(StreamTrimMode mode) var db = conn.GetDatabase(); var key = Me() + ":" + mode; - const int maxLength = 1000; - const int limit = 100; + const int maxLength = 100; + const int limit = 10; + // The behavior of ACKED etc is undefined when there are no consumer groups; or rather, + // it *is* defined, but it is defined/implemented differently < and >= server 8.6 + // This *does* have the side-effect that the 3 modes behave the same in this test, + // but: we're trying to test the API, not the server. + const string groupName = "test_group", consumer = "consumer"; + db.StreamCreateConsumerGroup(key, groupName, StreamPosition.NewMessages); for (var i = 0; i < maxLength; i++) { db.StreamAdd(key, $"field", $"value", 1111111110 + i); } + var entries = db.StreamReadGroup( + key, + groupName, + consumer, + StreamPosition.NewMessages); + + Assert.Equal(maxLength, entries.Length); + var numRemoved = db.StreamTrimByMinId(key, 1111111110 + maxLength, useApproximateMaxLength: true, limit: limit, mode: mode); - var expectRemoved = mode switch - { - StreamTrimMode.KeepReferences => limit, - StreamTrimMode.DeleteReferences => 0, - StreamTrimMode.Acknowledged => 0, - _ => throw new ArgumentOutOfRangeException(nameof(mode)), - }; + const int EXPECT_REMOVED = 0; var len = db.StreamLength(key); - Assert.Equal(expectRemoved, numRemoved); - Assert.Equal(maxLength - expectRemoved, len); + Assert.Equal(EXPECT_REMOVED, numRemoved); + Assert.Equal(maxLength - EXPECT_REMOVED, len); } [Fact] From 494b26cf1ac3d46b03c50d1458e5f530762b19c0 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 9 Feb 2026 14:12:05 +0000 Subject: [PATCH 411/435] release notes --- docs/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 2ccc6d2d0..af7ecad45 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,9 @@ Current package versions: ## unreleased + +- Add support for keyspace notifications ([#2995 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2995)) +- (internals) split AMR out to a separate options provider ([#2986 by NickCraver and philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2986)) - Implement idempotent stream entry (IDMP) support ([#3006 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3006)) ## 2.10.14 From 0aa60054a3ddd6c836c23c6a3c41a9e1e037196b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 11 Feb 2026 09:24:08 +0000 Subject: [PATCH 412/435] Update CI to 8.6 (#3010) --- tests/RedisConfigs/.docker/Redis/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/RedisConfigs/.docker/Redis/Dockerfile b/tests/RedisConfigs/.docker/Redis/Dockerfile index b26ab5d76..e32d53161 100644 --- a/tests/RedisConfigs/.docker/Redis/Dockerfile +++ b/tests/RedisConfigs/.docker/Redis/Dockerfile @@ -1,5 +1,4 @@ -FROM redislabs/client-libs-test:8.4-GA-pre.3 - +FROM redislabs/client-libs-test:8.6.0 COPY --from=configs ./Basic /data/Basic/ COPY --from=configs ./Failover /data/Failover/ COPY --from=configs ./Cluster /data/Cluster/ From a018b871c54d69d32cfac7937101f6bbe76cb9ec Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 11 Feb 2026 16:14:56 +0000 Subject: [PATCH 413/435] 8.6 Support HOTKEYS (#3008) * propose API for HOTKEYS * stubs * untested stab at message impl * untested result processor * basic integration tests * more integration tests * release notes and [Experimental] * github link * sample ration default is 1, not zero * - RESP3 - don't expose raw arrays - expose API-shaped ms/us accessors - reuse shared all-slots array * validate/fix cluster slot filter * validate duration * docs; more tests and compensation * make SharedAllSlots lazy; explicitly track empty cpu/network/slots * More docs * "wow"? * more words * update meaning of count * expose a bunch of values that are conditionally present * tests on the sampled/slot-filtered metrics * - naming in HotKeysResult - prefer Nullable when not-always-present * pre-empt typo fix * CI: use internal 8.6 preview build * additional validation on conditional members * CI image update * stabilize CI for Windows Server * be explicit about per-protocol/collection on cluster --- docs/HotKeys.md | 71 ++++ docs/ReleaseNotes.md | 6 +- docs/index.md | 1 + .../ClusterConfiguration.cs | 5 + src/StackExchange.Redis/CommandMap.cs | 4 +- src/StackExchange.Redis/Enums/RedisCommand.cs | 2 + .../HotKeys.ResultProcessor.cs | 217 +++++++++++ src/StackExchange.Redis/HotKeys.Server.cs | 47 +++ .../HotKeys.StartMessage.cs | 80 +++++ src/StackExchange.Redis/HotKeys.cs | 336 ++++++++++++++++++ src/StackExchange.Redis/Message.cs | 2 + .../PublicAPI/PublicAPI.Unshipped.txt | 49 +++ src/StackExchange.Redis/RedisFeatures.cs | 4 +- src/StackExchange.Redis/RedisLiterals.cs | 1 + src/StackExchange.Redis/RedisServer.cs | 2 +- .../StackExchange.Redis.csproj | 2 + tests/RedisConfigs/.docker/Redis/Dockerfile | 1 + .../StackExchange.Redis.Tests/HotKeysTests.cs | 326 +++++++++++++++++ 18 files changed, 1148 insertions(+), 8 deletions(-) create mode 100644 docs/HotKeys.md create mode 100644 src/StackExchange.Redis/HotKeys.ResultProcessor.cs create mode 100644 src/StackExchange.Redis/HotKeys.Server.cs create mode 100644 src/StackExchange.Redis/HotKeys.StartMessage.cs create mode 100644 src/StackExchange.Redis/HotKeys.cs create mode 100644 tests/StackExchange.Redis.Tests/HotKeysTests.cs diff --git a/docs/HotKeys.md b/docs/HotKeys.md new file mode 100644 index 000000000..5ac7c86f9 --- /dev/null +++ b/docs/HotKeys.md @@ -0,0 +1,71 @@ +Hot Keys +=== + +The `HOTKEYS` command allows for server-side profiling of CPU and network usage by key. It is available in Redis 8.6 and later. + +This command is available via the `IServer.HotKeys*` methods: + +``` c# +// Get the server instance. +IConnectionMultiplexer muxer = ... // connect to Redis 8.6 or later +var server = muxer.GetServer(endpoint); // or muxer.GetServer(key) + +// Start the capture; you can specify a duration, or manually use the HotKeysStop[Async] method; specifying +// a duration is recommended, so that the profiler will not be left running in the case of failure. +// Optional parameters allow you to specify the metrics to capture, the sample ratio, and the key slots to include; +// by default, all metrics are captured, every command is sampled, and all key slots are included. +await server.HotKeysStartAsync(duration: TimeSpan.FromSeconds(30)); + +// Now either do some work ourselves, or await for some other activity to happen: +await Task.Delay(TimeSpan.FromSeconds(35)); // whatever happens: happens + +// Fetch the results; note that this does not stop the capture, and you can fetch the results multiple times +// either while it is running, or after it has completed - but only a single capture can be active at a time. +var result = await server.HotKeysGetAsync(); + +// ...investigate the results... + +// Optional: discard the active capture data at the server, if any. +await server.HotKeysResetAsync(); +``` + +The `HotKeysResult` class (our `result` value above) contains the following properties: + +- `Metrics`: The metrics captured during this profiling session. +- `TrackingActive`: Indicates whether the capture currently active. +- `SampleRatio`: Profiling frequency; effectively: measure every Nth command. (also: `IsSampled`) +- `SelectedSlots`: The key slots active for this profiling session. +- `CollectionStartTime`: The start time of the capture. +- `CollectionDuration`: The duration of the capture. +- `AllCommandsAllSlotsTime`: The total CPU time measured for all commands in all slots, without any sampling or filtering applied. +- `AllCommandsAllSlotsNetworkBytes`: The total network usage measured for all commands in all slots, without any sampling or filtering applied. + +When slot filtering is used, the following properties are also available: + +- `AllCommandsSelectedSlotsTime`: The total CPU time measured for all commands in the selected slots. +- `AllCommandsSelectedSlotsNetworkBytes`: The total network usage measured for all commands in the selected slots. + +When slot filtering *and* sampling is used, the following properties are also available: + +- `SampledCommandsSelectedSlotsTime`: The total CPU time measured for the sampled commands in the selected slots. +- `SampledCommandsSelectedSlotsNetworkBytes`: The total network usage measured for the sampled commands in the selected slots. + +If CPU metrics were captured, the following properties are also available: + +- `TotalCpuTimeUser`: The total user CPU time measured in the profiling session. +- `TotalCpuTimeSystem`: The total system CPU time measured in the profiling session. +- `TotalCpuTime`: The total CPU time measured in the profiling session. +- `CpuByKey`: Hot keys, as measured by CPU activity; for each: + - `Key`: The key observed. + - `Duration`: The time taken. + +If network metrics were captured, the following properties are also available: + +- `TotalNetworkBytes`: The total network data measured in the profiling session. +- `NetworkBytesByKey`: Hot keys, as measured by network activity; for each: + - `Key`: The key observed. + - `Bytes`: The network activity, in bytes. + +Note: to use slot-based filtering, you must be connected to a Redis Cluster instance. The +`IConnectionMultiplexer.HashSlot(RedisKey)` method can be used to determine the slot for a given key. The key +can also be used in place of an endpoint when using `GetServer(...)` to get the `IServer` instance for a given key. diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index af7ecad45..c038ab327 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,12 +6,12 @@ Current package versions: | ------------ | ----------------- | ----- | | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | -## unreleased - +## 2.11.unreleased +- Add support for `HOTKEYS` ([#3008 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3008)) - Add support for keyspace notifications ([#2995 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2995)) +- Add support for idempotent stream entry (`XADD IDMP[AUTO]`) support ([#3006 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3006)) - (internals) split AMR out to a separate options provider ([#2986 by NickCraver and philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/2986)) -- Implement idempotent stream entry (IDMP) support ([#3006 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3006)) ## 2.10.14 diff --git a/docs/index.md b/docs/index.md index b1498d878..9180d3423 100644 --- a/docs/index.md +++ b/docs/index.md @@ -40,6 +40,7 @@ Documentation - [Events](Events) - the events available for logging / information purposes - [Pub/Sub Message Order](PubSubOrder) - advice on sequential and concurrent processing - [Pub/Sub Key Notifications](KeyspaceNotifications) - how to use keyspace and keyevent notifications +- [Hot Keys](HotKeys) - how to use `HOTKEYS` profiling - [Using RESP3](Resp3) - information on using RESP3 - [ServerMaintenanceEvent](ServerMaintenanceEvent) - how to listen and prepare for hosted server maintenance (e.g. Azure Cache for Redis) - [Streams](Streams) - how to use the Stream data type diff --git a/src/StackExchange.Redis/ClusterConfiguration.cs b/src/StackExchange.Redis/ClusterConfiguration.cs index 99488ddff..60e606ce2 100644 --- a/src/StackExchange.Redis/ClusterConfiguration.cs +++ b/src/StackExchange.Redis/ClusterConfiguration.cs @@ -45,6 +45,11 @@ private SlotRange(short from, short to) /// public int To => to; + internal const int MinSlot = 0, MaxSlot = 16383; + + private static SlotRange[]? s_SharedAllSlots; + internal static SlotRange[] SharedAllSlots => s_SharedAllSlots ??= [new(MinSlot, MaxSlot)]; + /// /// Indicates whether two ranges are not equal. /// diff --git a/src/StackExchange.Redis/CommandMap.cs b/src/StackExchange.Redis/CommandMap.cs index 663c61b36..683e51219 100644 --- a/src/StackExchange.Redis/CommandMap.cs +++ b/src/StackExchange.Redis/CommandMap.cs @@ -41,7 +41,7 @@ public sealed class CommandMap RedisCommand.BGREWRITEAOF, RedisCommand.BGSAVE, RedisCommand.CLIENT, RedisCommand.CLUSTER, RedisCommand.CONFIG, RedisCommand.DBSIZE, RedisCommand.DEBUG, RedisCommand.FLUSHALL, RedisCommand.FLUSHDB, RedisCommand.INFO, RedisCommand.LASTSAVE, RedisCommand.MONITOR, RedisCommand.REPLICAOF, - RedisCommand.SAVE, RedisCommand.SHUTDOWN, RedisCommand.SLAVEOF, RedisCommand.SLOWLOG, RedisCommand.SYNC, RedisCommand.TIME, + RedisCommand.SAVE, RedisCommand.SHUTDOWN, RedisCommand.SLAVEOF, RedisCommand.SLOWLOG, RedisCommand.SYNC, RedisCommand.TIME, RedisCommand.HOTKEYS, }); /// @@ -65,7 +65,7 @@ public sealed class CommandMap RedisCommand.BGREWRITEAOF, RedisCommand.BGSAVE, RedisCommand.CLIENT, RedisCommand.CLUSTER, RedisCommand.CONFIG, RedisCommand.DBSIZE, RedisCommand.DEBUG, RedisCommand.FLUSHALL, RedisCommand.FLUSHDB, RedisCommand.INFO, RedisCommand.LASTSAVE, RedisCommand.MONITOR, RedisCommand.REPLICAOF, - RedisCommand.SAVE, RedisCommand.SHUTDOWN, RedisCommand.SLAVEOF, RedisCommand.SLOWLOG, RedisCommand.SYNC, RedisCommand.TIME, + RedisCommand.SAVE, RedisCommand.SHUTDOWN, RedisCommand.SLAVEOF, RedisCommand.SLOWLOG, RedisCommand.SYNC, RedisCommand.TIME, RedisCommand.HOTKEYS, // supported by envoy but not enabled by stack exchange // RedisCommand.BITFIELD, diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index f731a6676..c55a39d8a 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -81,6 +81,7 @@ internal enum RedisCommand HLEN, HMGET, HMSET, + HOTKEYS, HPERSIST, HPEXPIRE, HPEXPIREAT, @@ -432,6 +433,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.HKEYS: case RedisCommand.HLEN: case RedisCommand.HMGET: + case RedisCommand.HOTKEYS: case RedisCommand.HPEXPIRETIME: case RedisCommand.HPTTL: case RedisCommand.HRANDFIELD: diff --git a/src/StackExchange.Redis/HotKeys.ResultProcessor.cs b/src/StackExchange.Redis/HotKeys.ResultProcessor.cs new file mode 100644 index 000000000..a0f5b2892 --- /dev/null +++ b/src/StackExchange.Redis/HotKeys.ResultProcessor.cs @@ -0,0 +1,217 @@ +namespace StackExchange.Redis; + +public sealed partial class HotKeysResult +{ + internal static readonly ResultProcessor Processor = new HotKeysResultProcessor(); + + private sealed class HotKeysResultProcessor : ResultProcessor + { + protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) + { + if (result.IsNull) + { + SetResult(message, null); + return true; + } + + // an array with a single element that *is* an array/map that is the results + if (result is { Resp2TypeArray: ResultType.Array, ItemsCount: 1 }) + { + ref readonly RawResult inner = ref result[0]; + if (inner is { Resp2TypeArray: ResultType.Array, IsNull: false }) + { + var hotKeys = new HotKeysResult(in inner); + SetResult(message, hotKeys); + return true; + } + } + + return false; + } + } + + private HotKeysResult(in RawResult result) + { + var metrics = HotKeysMetrics.None; // we infer this from the keys present + var iter = result.GetItems().GetEnumerator(); + while (iter.MoveNext()) + { + ref readonly RawResult key = ref iter.Current; + if (!iter.MoveNext()) break; // lies about the length! + ref readonly RawResult value = ref iter.Current; + var hash = key.Payload.Hash64(); + long i64; + switch (hash) + { + case tracking_active.Hash when tracking_active.Is(hash, key): + TrackingActive = value.GetBoolean(); + break; + case sample_ratio.Hash when sample_ratio.Is(hash, key) && value.TryGetInt64(out i64): + SampleRatio = i64; + break; + case selected_slots.Hash when selected_slots.Is(hash, key) & value.Resp2TypeArray is ResultType.Array: + var len = value.ItemsCount; + if (len == 0) + { + _selectedSlots = []; + continue; + } + + var items = value.GetItems().GetEnumerator(); + var slots = len == 1 ? null : new SlotRange[len]; + for (int i = 0; i < len && items.MoveNext(); i++) + { + ref readonly RawResult pair = ref items.Current; + if (pair.Resp2TypeArray is ResultType.Array) + { + long from = -1, to = -1; + switch (pair.ItemsCount) + { + case 1 when pair[0].TryGetInt64(out from): + to = from; // single slot + break; + case 2 when pair[0].TryGetInt64(out from) && pair[1].TryGetInt64(out to): + break; + } + + if (from < SlotRange.MinSlot) + { + // skip invalid ranges + } + else if (len == 1 & from == SlotRange.MinSlot & to == SlotRange.MaxSlot) + { + // this is the "normal" case when no slot filter was applied + slots = SlotRange.SharedAllSlots; // avoid the alloc + } + else + { + slots ??= new SlotRange[len]; + slots[i] = new((int)from, (int)to); + } + } + } + _selectedSlots = slots; + break; + case all_commands_all_slots_us.Hash when all_commands_all_slots_us.Is(hash, key) && value.TryGetInt64(out i64): + AllCommandsAllSlotsMicroseconds = i64; + break; + case all_commands_selected_slots_us.Hash when all_commands_selected_slots_us.Is(hash, key) && value.TryGetInt64(out i64): + AllCommandSelectedSlotsMicroseconds = i64; + break; + case sampled_command_selected_slots_us.Hash when sampled_command_selected_slots_us.Is(hash, key) && value.TryGetInt64(out i64): + case sampled_commands_selected_slots_us.Hash when sampled_commands_selected_slots_us.Is(hash, key) && value.TryGetInt64(out i64): + SampledCommandsSelectedSlotsMicroseconds = i64; + break; + case net_bytes_all_commands_all_slots.Hash when net_bytes_all_commands_all_slots.Is(hash, key) && value.TryGetInt64(out i64): + AllCommandsAllSlotsNetworkBytes = i64; + break; + case net_bytes_all_commands_selected_slots.Hash when net_bytes_all_commands_selected_slots.Is(hash, key) && value.TryGetInt64(out i64): + NetworkBytesAllCommandsSelectedSlotsRaw = i64; + break; + case net_bytes_sampled_commands_selected_slots.Hash when net_bytes_sampled_commands_selected_slots.Is(hash, key) && value.TryGetInt64(out i64): + NetworkBytesSampledCommandsSelectedSlotsRaw = i64; + break; + case collection_start_time_unix_ms.Hash when collection_start_time_unix_ms.Is(hash, key) && value.TryGetInt64(out i64): + CollectionStartTimeUnixMilliseconds = i64; + break; + case collection_duration_ms.Hash when collection_duration_ms.Is(hash, key) && value.TryGetInt64(out i64): + CollectionDurationMicroseconds = i64 * 1000; // ms vs us is in question: support both, and abstract it from the caller + break; + case collection_duration_us.Hash when collection_duration_us.Is(hash, key) && value.TryGetInt64(out i64): + CollectionDurationMicroseconds = i64; + break; + case total_cpu_time_sys_ms.Hash when total_cpu_time_sys_ms.Is(hash, key) && value.TryGetInt64(out i64): + metrics |= HotKeysMetrics.Cpu; + TotalCpuTimeSystemMicroseconds = i64 * 1000; // ms vs us is in question: support both, and abstract it from the caller + break; + case total_cpu_time_sys_us.Hash when total_cpu_time_sys_us.Is(hash, key) && value.TryGetInt64(out i64): + metrics |= HotKeysMetrics.Cpu; + TotalCpuTimeSystemMicroseconds = i64; + break; + case total_cpu_time_user_ms.Hash when total_cpu_time_user_ms.Is(hash, key) && value.TryGetInt64(out i64): + metrics |= HotKeysMetrics.Cpu; + TotalCpuTimeUserMicroseconds = i64 * 1000; // ms vs us is in question: support both, and abstract it from the caller + break; + case total_cpu_time_user_us.Hash when total_cpu_time_user_us.Is(hash, key) && value.TryGetInt64(out i64): + metrics |= HotKeysMetrics.Cpu; + TotalCpuTimeUserMicroseconds = i64; + break; + case total_net_bytes.Hash when total_net_bytes.Is(hash, key) && value.TryGetInt64(out i64): + metrics |= HotKeysMetrics.Network; + TotalNetworkBytesRaw = i64; + break; + case by_cpu_time_us.Hash when by_cpu_time_us.Is(hash, key) & value.Resp2TypeArray is ResultType.Array: + metrics |= HotKeysMetrics.Cpu; + len = value.ItemsCount / 2; + if (len == 0) + { + _cpuByKey = []; + continue; + } + + var cpuTime = new MetricKeyCpu[len]; + items = value.GetItems().GetEnumerator(); + for (int i = 0; i < len && items.MoveNext(); i++) + { + var metricKey = items.Current.AsRedisKey(); + if (items.MoveNext() && items.Current.TryGetInt64(out var metricValue)) + { + cpuTime[i] = new(metricKey, metricValue); + } + } + + _cpuByKey = cpuTime; + break; + case by_net_bytes.Hash when by_net_bytes.Is(hash, key) & value.Resp2TypeArray is ResultType.Array: + metrics |= HotKeysMetrics.Network; + len = value.ItemsCount / 2; + if (len == 0) + { + _networkBytesByKey = []; + continue; + } + + var netBytes = new MetricKeyBytes[len]; + items = value.GetItems().GetEnumerator(); + for (int i = 0; i < len && items.MoveNext(); i++) + { + var metricKey = items.Current.AsRedisKey(); + if (items.MoveNext() && items.Current.TryGetInt64(out var metricValue)) + { + netBytes[i] = new(metricKey, metricValue); + } + } + + _networkBytesByKey = netBytes; + break; + } // switch + } // while + Metrics = metrics; + } + +#pragma warning disable SA1134, SA1300 + // ReSharper disable InconsistentNaming + [FastHash] internal static partial class tracking_active { } + [FastHash] internal static partial class sample_ratio { } + [FastHash] internal static partial class selected_slots { } + [FastHash] internal static partial class all_commands_all_slots_us { } + [FastHash] internal static partial class all_commands_selected_slots_us { } + [FastHash] internal static partial class sampled_command_selected_slots_us { } + [FastHash] internal static partial class sampled_commands_selected_slots_us { } + [FastHash] internal static partial class net_bytes_all_commands_all_slots { } + [FastHash] internal static partial class net_bytes_all_commands_selected_slots { } + [FastHash] internal static partial class net_bytes_sampled_commands_selected_slots { } + [FastHash] internal static partial class collection_start_time_unix_ms { } + [FastHash] internal static partial class collection_duration_ms { } + [FastHash] internal static partial class collection_duration_us { } + [FastHash] internal static partial class total_cpu_time_user_ms { } + [FastHash] internal static partial class total_cpu_time_user_us { } + [FastHash] internal static partial class total_cpu_time_sys_ms { } + [FastHash] internal static partial class total_cpu_time_sys_us { } + [FastHash] internal static partial class total_net_bytes { } + [FastHash] internal static partial class by_cpu_time_us { } + [FastHash] internal static partial class by_net_bytes { } + + // ReSharper restore InconsistentNaming +#pragma warning restore SA1134, SA1300 +} diff --git a/src/StackExchange.Redis/HotKeys.Server.cs b/src/StackExchange.Redis/HotKeys.Server.cs new file mode 100644 index 000000000..967a454e8 --- /dev/null +++ b/src/StackExchange.Redis/HotKeys.Server.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; + +namespace StackExchange.Redis; + +internal partial class RedisServer +{ + public void HotKeysStart( + HotKeysMetrics metrics = (HotKeysMetrics)~0, + long count = 0, + TimeSpan duration = default, + long sampleRatio = 1, + int[]? slots = null, + CommandFlags flags = CommandFlags.None) + => ExecuteSync( + new HotKeysStartMessage(flags, metrics, count, duration, sampleRatio, slots), + ResultProcessor.DemandOK); + + public Task HotKeysStartAsync( + HotKeysMetrics metrics = (HotKeysMetrics)~0, + long count = 0, + TimeSpan duration = default, + long sampleRatio = 1, + int[]? slots = null, + CommandFlags flags = CommandFlags.None) + => ExecuteAsync( + new HotKeysStartMessage(flags, metrics, count, duration, sampleRatio, slots), + ResultProcessor.DemandOK); + + public bool HotKeysStop(CommandFlags flags = CommandFlags.None) + => ExecuteSync(Message.Create(-1, flags, RedisCommand.HOTKEYS, RedisLiterals.STOP), ResultProcessor.Boolean, server); + + public Task HotKeysStopAsync(CommandFlags flags = CommandFlags.None) + => ExecuteAsync(Message.Create(-1, flags, RedisCommand.HOTKEYS, RedisLiterals.STOP), ResultProcessor.Boolean, server); + + public void HotKeysReset(CommandFlags flags = CommandFlags.None) + => ExecuteSync(Message.Create(-1, flags, RedisCommand.HOTKEYS, RedisLiterals.RESET), ResultProcessor.DemandOK, server); + + public Task HotKeysResetAsync(CommandFlags flags = CommandFlags.None) + => ExecuteAsync(Message.Create(-1, flags, RedisCommand.HOTKEYS, RedisLiterals.RESET), ResultProcessor.DemandOK, server); + + public HotKeysResult? HotKeysGet(CommandFlags flags = CommandFlags.None) + => ExecuteSync(Message.Create(-1, flags, RedisCommand.HOTKEYS, RedisLiterals.GET), HotKeysResult.Processor, server); + + public Task HotKeysGetAsync(CommandFlags flags = CommandFlags.None) + => ExecuteAsync(Message.Create(-1, flags, RedisCommand.HOTKEYS, RedisLiterals.GET), HotKeysResult.Processor, server); +} diff --git a/src/StackExchange.Redis/HotKeys.StartMessage.cs b/src/StackExchange.Redis/HotKeys.StartMessage.cs new file mode 100644 index 000000000..c9f0bc371 --- /dev/null +++ b/src/StackExchange.Redis/HotKeys.StartMessage.cs @@ -0,0 +1,80 @@ +using System; +using System.Threading.Tasks; + +namespace StackExchange.Redis; + +internal partial class RedisServer +{ + internal sealed class HotKeysStartMessage( + CommandFlags flags, + HotKeysMetrics metrics, + long count, + TimeSpan duration, + long sampleRatio, + int[]? slots) : Message(-1, flags, RedisCommand.HOTKEYS) + { + protected override void WriteImpl(PhysicalConnection physical) + { + /* + HOTKEYS START + + [COUNT k] + [DURATION duration] + [SAMPLE ratio] + [SLOTS count slot…] + */ + physical.WriteHeader(Command, ArgCount); + physical.WriteBulkString("START"u8); + physical.WriteBulkString("METRICS"u8); + var metricCount = 0; + if ((metrics & HotKeysMetrics.Cpu) != 0) metricCount++; + if ((metrics & HotKeysMetrics.Network) != 0) metricCount++; + physical.WriteBulkString(metricCount); + if ((metrics & HotKeysMetrics.Cpu) != 0) physical.WriteBulkString("CPU"u8); + if ((metrics & HotKeysMetrics.Network) != 0) physical.WriteBulkString("NET"u8); + + if (count != 0) + { + physical.WriteBulkString("COUNT"u8); + physical.WriteBulkString(count); + } + + if (duration != TimeSpan.Zero) + { + physical.WriteBulkString("DURATION"u8); + physical.WriteBulkString(Math.Ceiling(duration.TotalSeconds)); + } + + if (sampleRatio != 1) + { + physical.WriteBulkString("SAMPLE"u8); + physical.WriteBulkString(sampleRatio); + } + + if (slots is { Length: > 0 }) + { + physical.WriteBulkString("SLOTS"u8); + physical.WriteBulkString(slots.Length); + foreach (var slot in slots) + { + physical.WriteBulkString(slot); + } + } + } + + public override int ArgCount + { + get + { + int argCount = 3; + if ((metrics & HotKeysMetrics.Cpu) != 0) argCount++; + if ((metrics & HotKeysMetrics.Network) != 0) argCount++; + if (count != 0) argCount += 2; + if (duration != TimeSpan.Zero) argCount += 2; + if (sampleRatio != 1) argCount += 2; + if (slots is { Length: > 0 }) argCount += 2 + slots.Length; + return argCount; + } + } + } +} diff --git a/src/StackExchange.Redis/HotKeys.cs b/src/StackExchange.Redis/HotKeys.cs new file mode 100644 index 000000000..270bcf9f7 --- /dev/null +++ b/src/StackExchange.Redis/HotKeys.cs @@ -0,0 +1,336 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace StackExchange.Redis; + +public partial interface IServer +{ + /// + /// Start a new HOTKEYS profiling session. + /// + /// The metrics to record during this capture (defaults to "all"). + /// The number of keys to retain and report when is invoked. If zero, the server default is used (currently 10). + /// The duration of this profiling session. + /// Profiling frequency; effectively: measure every Nth command. + /// The key-slots to record during this capture (defaults to "all"). + /// The command flags to use. + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + void HotKeysStart( + HotKeysMetrics metrics = (HotKeysMetrics)~0, // everything by default + long count = 0, + TimeSpan duration = default, + long sampleRatio = 1, + int[]? slots = null, + CommandFlags flags = CommandFlags.None); + + /// + /// Start a new HOTKEYS profiling session. + /// + /// The metrics to record during this capture (defaults to "all"). + /// The number of keys to retain and report when is invoked. If zero, the server default is used (currently 10). + /// The duration of this profiling session. + /// Profiling frequency; effectively: measure every Nth command. + /// The key-slots to record during this capture (defaults to "all" / "all on this node"). + /// The command flags to use. + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + Task HotKeysStartAsync( + HotKeysMetrics metrics = (HotKeysMetrics)~0, // everything by default + long count = 0, + TimeSpan duration = default, + long sampleRatio = 1, + int[]? slots = null, + CommandFlags flags = CommandFlags.None); + + /// + /// Stop the current HOTKEYS capture, if any. + /// + /// The command flags to use. + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + bool HotKeysStop(CommandFlags flags = CommandFlags.None); + + /// + /// Stop the current HOTKEYS capture, if any. + /// + /// The command flags to use. + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + Task HotKeysStopAsync(CommandFlags flags = CommandFlags.None); + + /// + /// Discard the last HOTKEYS capture data, if any. + /// + /// The command flags to use. + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + void HotKeysReset(CommandFlags flags = CommandFlags.None); + + /// + /// Discard the last HOTKEYS capture data, if any. + /// + /// The command flags to use. + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + Task HotKeysResetAsync(CommandFlags flags = CommandFlags.None); + + /// + /// Fetch the most recent HOTKEYS profiling data. + /// + /// The command flags to use. + /// The data captured during HOTKEYS profiling. + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + HotKeysResult? HotKeysGet(CommandFlags flags = CommandFlags.None); + + /// + /// Fetch the most recent HOTKEYS profiling data. + /// + /// The command flags to use. + /// The data captured during HOTKEYS profiling. + [Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] + Task HotKeysGetAsync(CommandFlags flags = CommandFlags.None); +} + +/// +/// Metrics to record during HOTKEYS profiling. +/// +[Flags] +[Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] +public enum HotKeysMetrics +{ + /// + /// No metrics. + /// + None = 0, + + /// + /// Capture CPU time. + /// + Cpu = 1 << 0, + + /// + /// Capture network bytes. + /// + Network = 1 << 1, +} + +/// +/// Captured data from HOTKEYS profiling. +/// +[Experimental(Experiments.Server_8_6, UrlFormat = Experiments.UrlFormat)] +public sealed partial class HotKeysResult +{ + // Note: names are intentionally chosen to align reasonably well with the Redis command output; some + // liberties have been taken, for example "all-commands-all-slots-us" and "net-bytes-all-commands-all-slots" + // have been named "AllCommandsAllSlotsTime" and "AllCommandsAllSlotsNetworkBytes" for consistency + // with each-other. + + /// + /// The metrics captured during this profiling session. + /// + public HotKeysMetrics Metrics { get; } + + /// + /// Indicates whether the capture currently active. + /// + public bool TrackingActive { get; } + + /// + /// Profiling frequency; effectively: measure every Nth command. + /// + public long SampleRatio { get; } + + /// + /// Gets whether sampling is in use. + /// + public bool IsSampled => SampleRatio > 1; + + /// + /// The key slots active for this profiling session. + /// + public ReadOnlySpan SelectedSlots => _selectedSlots; + + private readonly SlotRange[]? _selectedSlots; + + /// + /// Gets whether slot filtering is in use. + /// + public bool IsSlotFiltered => + NetworkBytesAllCommandsSelectedSlotsRaw >= 0; // this key only present if slot-filtering active + + /// + /// The total CPU measured for all commands in all slots, without any sampling or filtering applied. + /// + public TimeSpan AllCommandsAllSlotsTime => NonNegativeMicroseconds(AllCommandsAllSlotsMicroseconds); + + internal long AllCommandsAllSlotsMicroseconds { get; } = -1; + + internal long AllCommandSelectedSlotsMicroseconds { get; } = -1; + internal long SampledCommandsSelectedSlotsMicroseconds { get; } = -1; + + /// + /// When slot filtering is used, this is the total CPU time measured for all commands in the selected slots. + /// + public TimeSpan? AllCommandsSelectedSlotsTime => AllCommandSelectedSlotsMicroseconds < 0 + ? null + : NonNegativeMicroseconds(AllCommandSelectedSlotsMicroseconds); + + /// + /// When sampling and slot filtering are used, this is the total CPU time measured for the sampled commands in the selected slots. + /// + public TimeSpan? SampledCommandsSelectedSlotsTime => SampledCommandsSelectedSlotsMicroseconds < 0 + ? null + : NonNegativeMicroseconds(SampledCommandsSelectedSlotsMicroseconds); + + private static TimeSpan NonNegativeMicroseconds(long us) + { + const long TICKS_PER_MICROSECOND = TimeSpan.TicksPerMillisecond / 1000; // 10, but: clearer + return TimeSpan.FromTicks(Math.Max(us, 0) / TICKS_PER_MICROSECOND); + } + + /// + /// The total network usage measured for all commands in all slots, without any sampling or filtering applied. + /// + public long AllCommandsAllSlotsNetworkBytes { get; } + + internal long NetworkBytesAllCommandsSelectedSlotsRaw { get; } = -1; + internal long NetworkBytesSampledCommandsSelectedSlotsRaw { get; } = -1; + + /// + /// When slot filtering is used, this is the total network usage measured for all commands in the selected slots. + /// + public long? AllCommandsSelectedSlotsNetworkBytes => NetworkBytesAllCommandsSelectedSlotsRaw < 0 + ? null + : NetworkBytesAllCommandsSelectedSlotsRaw; + + /// + /// When sampling and slot filtering are used, this is the total network usage measured for the sampled commands in the selected slots. + /// + public long? SampledCommandsSelectedSlotsNetworkBytes => NetworkBytesSampledCommandsSelectedSlotsRaw < 0 + ? null + : NetworkBytesSampledCommandsSelectedSlotsRaw; + + internal long CollectionStartTimeUnixMilliseconds { get; } = -1; + + /// + /// The start time of the capture. + /// + public DateTime CollectionStartTime => + RedisBase.UnixEpoch.AddMilliseconds(Math.Max(CollectionStartTimeUnixMilliseconds, 0)); + + internal long CollectionDurationMicroseconds { get; } + + /// + /// The duration of the capture. + /// + public TimeSpan CollectionDuration => NonNegativeMicroseconds(CollectionDurationMicroseconds); + + internal long TotalCpuTimeUserMicroseconds { get; } = -1; + + /// + /// The total user CPU time measured in the profiling session. + /// + public TimeSpan? TotalCpuTimeUser => TotalCpuTimeUserMicroseconds < 0 + ? null + : NonNegativeMicroseconds(TotalCpuTimeUserMicroseconds); + + internal long TotalCpuTimeSystemMicroseconds { get; } = -1; + + /// + /// The total system CPU measured in the profiling session. + /// + public TimeSpan? TotalCpuTimeSystem => TotalCpuTimeSystemMicroseconds < 0 + ? null + : NonNegativeMicroseconds(TotalCpuTimeSystemMicroseconds); + + /// + /// The total CPU time measured in the profiling session (this is just + ). + /// + public TimeSpan? TotalCpuTime => TotalCpuTimeUser + TotalCpuTimeSystem; + + internal long TotalNetworkBytesRaw { get; } = -1; + + /// + /// The total network data measured in the profiling session. + /// + public long? TotalNetworkBytes => TotalNetworkBytesRaw < 0 + ? null + : TotalNetworkBytesRaw; + + // Intentionally do construct a dictionary from the results; the caller is unlikely to be looking + // for a particular key (lookup), but rather: is likely to want to list them for display; this way, + // we'll preserve the server's display order. + + /// + /// Hot keys, as measured by CPU activity. + /// + public ReadOnlySpan CpuByKey => _cpuByKey; + + private readonly MetricKeyCpu[]? _cpuByKey; + + /// + /// Hot keys, as measured by network activity. + /// + public ReadOnlySpan NetworkBytesByKey => _networkBytesByKey; + + private readonly MetricKeyBytes[]? _networkBytesByKey; + + /// + /// A hot key, as measured by CPU activity. + /// + /// The key observed. + /// The time taken, in microseconds. + public readonly struct MetricKeyCpu(in RedisKey key, long durationMicroseconds) + { + private readonly RedisKey _key = key; + + /// + /// The key observed. + /// + public RedisKey Key => _key; + + internal long DurationMicroseconds => durationMicroseconds; + + /// + /// The time taken. + /// + public TimeSpan Duration => NonNegativeMicroseconds(durationMicroseconds); + + /// + public override string ToString() => $"{_key}: {Duration}"; + + /// + public override int GetHashCode() => _key.GetHashCode() ^ durationMicroseconds.GetHashCode(); + + /// + public override bool Equals(object? obj) + => obj is MetricKeyCpu other && _key.Equals(other.Key) && + durationMicroseconds == other.DurationMicroseconds; + } + + /// + /// A hot key, as measured by network activity. + /// + /// The key observed. + /// The network activity, in bytes. + public readonly struct MetricKeyBytes(in RedisKey key, long bytes) + { + private readonly RedisKey _key = key; + + /// + /// The key observed. + /// + public RedisKey Key => _key; + + /// + /// The network activity, in bytes. + /// + public long Bytes => bytes; + + /// + public override string ToString() => $"{_key}: {bytes}B"; + + /// + public override int GetHashCode() => _key.GetHashCode() ^ bytes.GetHashCode(); + + /// + public override bool Equals(object? obj) + => obj is MetricKeyBytes other && _key.Equals(other.Key) && Bytes == other.Bytes; + } +} diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index 37472fd4c..faf25ba44 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -182,6 +182,7 @@ public bool IsAdmin case RedisCommand.DEBUG: case RedisCommand.FLUSHALL: case RedisCommand.FLUSHDB: + case RedisCommand.HOTKEYS: case RedisCommand.INFO: case RedisCommand.KEYS: case RedisCommand.MONITOR: @@ -553,6 +554,7 @@ internal static bool RequiresDatabase(RedisCommand command) case RedisCommand.ECHO: case RedisCommand.FLUSHALL: case RedisCommand.HELLO: + case RedisCommand.HOTKEYS: case RedisCommand.INFO: case RedisCommand.LASTSAVE: case RedisCommand.LATENCY: diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 3a80ab570..1b2aa2a9e 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,4 +1,53 @@ #nullable enable +[SER003]StackExchange.Redis.HotKeysMetrics +[SER003]StackExchange.Redis.HotKeysMetrics.Cpu = 1 -> StackExchange.Redis.HotKeysMetrics +[SER003]StackExchange.Redis.HotKeysMetrics.Network = 2 -> StackExchange.Redis.HotKeysMetrics +[SER003]StackExchange.Redis.HotKeysMetrics.None = 0 -> StackExchange.Redis.HotKeysMetrics +[SER003]StackExchange.Redis.HotKeysResult +[SER003]StackExchange.Redis.HotKeysResult.AllCommandsAllSlotsNetworkBytes.get -> long +[SER003]StackExchange.Redis.HotKeysResult.AllCommandsAllSlotsTime.get -> System.TimeSpan +[SER003]StackExchange.Redis.HotKeysResult.AllCommandsSelectedSlotsNetworkBytes.get -> long? +[SER003]StackExchange.Redis.HotKeysResult.AllCommandsSelectedSlotsTime.get -> System.TimeSpan? +[SER003]StackExchange.Redis.HotKeysResult.CollectionDuration.get -> System.TimeSpan +[SER003]StackExchange.Redis.HotKeysResult.CollectionStartTime.get -> System.DateTime +[SER003]StackExchange.Redis.HotKeysResult.CpuByKey.get -> System.ReadOnlySpan +[SER003]StackExchange.Redis.HotKeysResult.IsSampled.get -> bool +[SER003]StackExchange.Redis.HotKeysResult.IsSlotFiltered.get -> bool +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.Bytes.get -> long +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.Key.get -> StackExchange.Redis.RedisKey +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.MetricKeyBytes() -> void +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.MetricKeyBytes(in StackExchange.Redis.RedisKey key, long bytes) -> void +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.Duration.get -> System.TimeSpan +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.Key.get -> StackExchange.Redis.RedisKey +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.MetricKeyCpu() -> void +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.MetricKeyCpu(in StackExchange.Redis.RedisKey key, long durationMicroseconds) -> void +[SER003]StackExchange.Redis.HotKeysResult.Metrics.get -> StackExchange.Redis.HotKeysMetrics +[SER003]StackExchange.Redis.HotKeysResult.NetworkBytesByKey.get -> System.ReadOnlySpan +[SER003]StackExchange.Redis.HotKeysResult.SampledCommandsSelectedSlotsNetworkBytes.get -> long? +[SER003]StackExchange.Redis.HotKeysResult.SampledCommandsSelectedSlotsTime.get -> System.TimeSpan? +[SER003]StackExchange.Redis.HotKeysResult.SampleRatio.get -> long +[SER003]StackExchange.Redis.HotKeysResult.SelectedSlots.get -> System.ReadOnlySpan +[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTime.get -> System.TimeSpan? +[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTimeSystem.get -> System.TimeSpan? +[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTimeUser.get -> System.TimeSpan? +[SER003]StackExchange.Redis.HotKeysResult.TotalNetworkBytes.get -> long? +[SER003]StackExchange.Redis.HotKeysResult.TrackingActive.get -> bool +[SER003]StackExchange.Redis.IServer.HotKeysGet(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.HotKeysResult? +[SER003]StackExchange.Redis.IServer.HotKeysGetAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER003]StackExchange.Redis.IServer.HotKeysReset(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +[SER003]StackExchange.Redis.IServer.HotKeysResetAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER003]StackExchange.Redis.IServer.HotKeysStart(StackExchange.Redis.HotKeysMetrics metrics = (StackExchange.Redis.HotKeysMetrics)-1, long count = 0, System.TimeSpan duration = default(System.TimeSpan), long sampleRatio = 1, int[]? slots = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +[SER003]StackExchange.Redis.IServer.HotKeysStartAsync(StackExchange.Redis.HotKeysMetrics metrics = (StackExchange.Redis.HotKeysMetrics)-1, long count = 0, System.TimeSpan duration = default(System.TimeSpan), long sampleRatio = 1, int[]? slots = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER003]StackExchange.Redis.IServer.HotKeysStop(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER003]StackExchange.Redis.IServer.HotKeysStopAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyBytes.Equals(object? obj) -> bool +[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyBytes.GetHashCode() -> int +[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyBytes.ToString() -> string! +[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyCpu.Equals(object? obj) -> bool +[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyCpu.GetHashCode() -> int +[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyCpu.ToString() -> string! [SER003]override StackExchange.Redis.StreamIdempotentId.Equals(object? obj) -> bool [SER003]override StackExchange.Redis.StreamIdempotentId.GetHashCode() -> int [SER003]override StackExchange.Redis.StreamIdempotentId.ToString() -> string! diff --git a/src/StackExchange.Redis/RedisFeatures.cs b/src/StackExchange.Redis/RedisFeatures.cs index d0ea77707..d185089e6 100644 --- a/src/StackExchange.Redis/RedisFeatures.cs +++ b/src/StackExchange.Redis/RedisFeatures.cs @@ -48,8 +48,8 @@ namespace StackExchange.Redis v7_4_0 = new Version(7, 4, 0), v8_0_0_M04 = new Version(7, 9, 227), // 8.0 M04 is version 7.9.227 v8_2_0_rc1 = new Version(8, 1, 240), // 8.2 RC1 is version 8.1.240 - v8_4_0_rc1 = new Version(8, 3, 224), - v8_6_0 = new Version(8, 5, 999); // 8.4 RC1 is version 8.3.224 + v8_4_0_rc1 = new Version(8, 3, 224), // 8.4 RC1 is version 8.3.224 + v8_6_0 = new Version(8, 6, 0); #pragma warning restore SA1310 // Field names should not contain underscore #pragma warning restore SA1311 // Static readonly fields should begin with upper-case letter diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index dd1522d71..be79b3267 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -166,6 +166,7 @@ public static readonly RedisValue SETNAME = "SETNAME", SKIPME = "SKIPME", STATS = "STATS", + STOP = "STOP", STORE = "STORE", TYPE = "TYPE", USERNAME = "USERNAME", diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs index 3bc306c69..2d7e184ad 100644 --- a/src/StackExchange.Redis/RedisServer.cs +++ b/src/StackExchange.Redis/RedisServer.cs @@ -12,7 +12,7 @@ namespace StackExchange.Redis { - internal sealed class RedisServer : RedisBase, IServer + internal sealed partial class RedisServer : RedisBase, IServer { private readonly ServerEndPoint server; diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 84e495f1a..2c2e7702a 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -35,6 +35,8 @@ + + diff --git a/tests/RedisConfigs/.docker/Redis/Dockerfile b/tests/RedisConfigs/.docker/Redis/Dockerfile index e32d53161..363edde51 100644 --- a/tests/RedisConfigs/.docker/Redis/Dockerfile +++ b/tests/RedisConfigs/.docker/Redis/Dockerfile @@ -1,4 +1,5 @@ FROM redislabs/client-libs-test:8.6.0 + COPY --from=configs ./Basic /data/Basic/ COPY --from=configs ./Failover /data/Failover/ COPY --from=configs ./Cluster /data/Cluster/ diff --git a/tests/StackExchange.Redis.Tests/HotKeysTests.cs b/tests/StackExchange.Redis.Tests/HotKeysTests.cs new file mode 100644 index 000000000..5e2daa6b3 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/HotKeysTests.cs @@ -0,0 +1,326 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace StackExchange.Redis.Tests; + +[RunPerProtocol] +[Collection(NonParallelCollection.Name)] +public class HotKeysClusterTests(ITestOutputHelper output, SharedConnectionFixture fixture) : HotKeysTests(output, fixture) +{ + protected override string GetConfiguration() => TestConfig.Current.ClusterServersAndPorts + ",connectTimeout=10000"; + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CanUseClusterFilter(bool sample) + { + var key = Me(); + using var muxer = GetServer(key, out var server); + Log($"server: {Format.ToString(server.EndPoint)}, key: '{key}'"); + + var slot = muxer.HashSlot(key); + server.HotKeysStart(slots: [(short)slot], sampleRatio: sample ? 3 : 1, duration: Duration); + + var db = muxer.GetDatabase(); + db.KeyDelete(key, flags: CommandFlags.FireAndForget); + for (int i = 0; i < 20; i++) + { + db.StringIncrement(key, flags: CommandFlags.FireAndForget); + } + + server.HotKeysStop(); + var result = server.HotKeysGet(); + Assert.NotNull(result); + Assert.True(result.IsSlotFiltered, nameof(result.IsSlotFiltered)); + var slots = result.SelectedSlots; + Assert.Equal(1, slots.Length); + Assert.Equal(slot, slots[0].From); + Assert.Equal(slot, slots[0].To); + + Assert.False(result.CpuByKey.IsEmpty, "Expected at least one CPU result"); + bool found = false; + foreach (var cpu in result.CpuByKey) + { + if (cpu.Key == key) found = true; + } + Assert.True(found, "key not found in CPU results"); + + Assert.False(result.NetworkBytesByKey.IsEmpty, "Expected at least one network result"); + found = false; + foreach (var net in result.NetworkBytesByKey) + { + if (net.Key == key) found = true; + } + Assert.True(found, "key not found in network results"); + + Assert.True(result.AllCommandSelectedSlotsMicroseconds >= 0, nameof(result.AllCommandSelectedSlotsMicroseconds)); + Assert.True(result.TotalCpuTimeUserMicroseconds >= 0, nameof(result.TotalCpuTimeUserMicroseconds)); + + Assert.Equal(sample, result.IsSampled); + if (sample) + { + Assert.Equal(3, result.SampleRatio); + Assert.True(result.SampledCommandsSelectedSlotsMicroseconds >= 0, nameof(result.SampledCommandsSelectedSlotsMicroseconds)); + Assert.True(result.NetworkBytesSampledCommandsSelectedSlotsRaw >= 0, nameof(result.NetworkBytesSampledCommandsSelectedSlotsRaw)); + Assert.True(result.SampledCommandsSelectedSlotsTime.HasValue); + Assert.True(result.SampledCommandsSelectedSlotsNetworkBytes.HasValue); + } + else + { + Assert.Equal(1, result.SampleRatio); + Assert.Equal(-1, result.SampledCommandsSelectedSlotsMicroseconds); + Assert.Equal(-1, result.NetworkBytesSampledCommandsSelectedSlotsRaw); + Assert.False(result.SampledCommandsSelectedSlotsTime.HasValue); + Assert.False(result.SampledCommandsSelectedSlotsNetworkBytes.HasValue); + } + + Assert.True(result.AllCommandsSelectedSlotsTime.HasValue); + Assert.True(result.AllCommandsSelectedSlotsNetworkBytes.HasValue); + } +} + +[RunPerProtocol] +[Collection(NonParallelCollection.Name)] +public class HotKeysTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) +{ + protected TimeSpan Duration => TimeSpan.FromMinutes(1); // ensure we don't leave profiling running + + private protected IConnectionMultiplexer GetServer(out IServer server) + => GetServer(RedisKey.Null, out server); + + private protected IConnectionMultiplexer GetServer(in RedisKey key, out IServer server) + { + var muxer = Create(require: RedisFeatures.v8_6_0, allowAdmin: true); + server = key.IsNull ? muxer.GetServer(muxer.GetEndPoints()[0]) : muxer.GetServer(key); + server.HotKeysStop(CommandFlags.FireAndForget); + server.HotKeysReset(CommandFlags.FireAndForget); + return muxer; + } + + [Fact] + public void GetWhenEmptyIsNull() + { + using var muxer = GetServer(out var server); + Assert.Null(server.HotKeysGet()); + } + + [Fact] + public async Task GetWhenEmptyIsNullAsync() + { + await using var muxer = GetServer(out var server); + Assert.Null(await server.HotKeysGetAsync()); + } + + [Fact] + public void StopWhenNotRunningIsFalse() + { + using var muxer = GetServer(out var server); + Assert.False(server.HotKeysStop()); + } + + [Fact] + public async Task StopWhenNotRunningIsFalseAsync() + { + await using var muxer = GetServer(out var server); + Assert.False(await server.HotKeysStopAsync()); + } + + [Fact] + public void CanStartStopReset() + { + RedisKey key = Me(); + using var muxer = GetServer(key, out var server); + server.HotKeysStart(duration: Duration); + var db = muxer.GetDatabase(); + db.KeyDelete(key, flags: CommandFlags.FireAndForget); + for (int i = 0; i < 20; i++) + { + db.StringIncrement(key, flags: CommandFlags.FireAndForget); + } + + var result = server.HotKeysGet(); + Assert.NotNull(result); + Assert.True(result.TrackingActive); + CheckSimpleWithKey(key, result, server); + + Assert.True(server.HotKeysStop()); + result = server.HotKeysGet(); + Assert.NotNull(result); + Assert.False(result.TrackingActive); + CheckSimpleWithKey(key, result, server); + + server.HotKeysReset(); + result = server.HotKeysGet(); + Assert.Null(result); + } + + private void CheckSimpleWithKey(RedisKey key, HotKeysResult hotKeys, IServer server) + { + Assert.Equal(HotKeysMetrics.Cpu | HotKeysMetrics.Network, hotKeys.Metrics); + Assert.True(hotKeys.CollectionDurationMicroseconds >= 0, nameof(hotKeys.CollectionDurationMicroseconds)); + Assert.True(hotKeys.CollectionStartTimeUnixMilliseconds >= 0, nameof(hotKeys.CollectionStartTimeUnixMilliseconds)); + + Assert.False(hotKeys.CpuByKey.IsEmpty, "Expected at least one CPU result"); + bool found = false; + foreach (var cpu in hotKeys.CpuByKey) + { + Assert.True(cpu.DurationMicroseconds >= 0, nameof(cpu.DurationMicroseconds)); + if (cpu.Key == key) found = true; + } + Assert.True(found, "key not found in CPU results"); + + Assert.False(hotKeys.NetworkBytesByKey.IsEmpty, "Expected at least one network result"); + found = false; + foreach (var net in hotKeys.NetworkBytesByKey) + { + Assert.True(net.Bytes > 0, nameof(net.Bytes)); + if (net.Key == key) found = true; + } + Assert.True(found, "key not found in network results"); + + Assert.Equal(1, hotKeys.SampleRatio); + Assert.False(hotKeys.IsSampled, nameof(hotKeys.IsSampled)); + Assert.False(hotKeys.IsSlotFiltered, nameof(hotKeys.IsSlotFiltered)); + + if (server.ServerType is ServerType.Cluster) + { + Assert.NotEqual(0, hotKeys.SelectedSlots.Length); + Log("Cluster mode detected; not enforcing slots, but:"); + foreach (var slot in hotKeys.SelectedSlots) + { + Log($" {slot}"); + } + } + else + { + Assert.Equal(1, hotKeys.SelectedSlots.Length); + var slots = hotKeys.SelectedSlots[0]; + Assert.Equal(SlotRange.MinSlot, slots.From); + Assert.Equal(SlotRange.MaxSlot, slots.To); + } + + Assert.True(hotKeys.AllCommandsAllSlotsMicroseconds >= 0, nameof(hotKeys.AllCommandsAllSlotsMicroseconds)); + Assert.True(hotKeys.TotalCpuTimeSystemMicroseconds >= 0, nameof(hotKeys.TotalCpuTimeSystemMicroseconds)); + Assert.True(hotKeys.TotalCpuTimeUserMicroseconds >= 0, nameof(hotKeys.TotalCpuTimeUserMicroseconds)); + Assert.True(hotKeys.AllCommandsAllSlotsNetworkBytes > 0, nameof(hotKeys.AllCommandsAllSlotsNetworkBytes)); + Assert.True(hotKeys.TotalNetworkBytes > 0, nameof(hotKeys.TotalNetworkBytes)); + + Assert.False(hotKeys.AllCommandsSelectedSlotsTime.HasValue); + Assert.False(hotKeys.AllCommandsSelectedSlotsNetworkBytes.HasValue); + Assert.False(hotKeys.SampledCommandsSelectedSlotsTime.HasValue); + Assert.False(hotKeys.SampledCommandsSelectedSlotsNetworkBytes.HasValue); + } + + [Fact] + public async Task CanStartStopResetAsync() + { + RedisKey key = Me(); + await using var muxer = GetServer(key, out var server); + await server.HotKeysStartAsync(duration: Duration); + var db = muxer.GetDatabase(); + await db.KeyDeleteAsync(key, flags: CommandFlags.FireAndForget); + for (int i = 0; i < 20; i++) + { + await db.StringIncrementAsync(key, flags: CommandFlags.FireAndForget); + } + + var result = await server.HotKeysGetAsync(); + Assert.NotNull(result); + Assert.True(result.TrackingActive); + CheckSimpleWithKey(key, result, server); + + Assert.True(await server.HotKeysStopAsync()); + result = await server.HotKeysGetAsync(); + Assert.NotNull(result); + Assert.False(result.TrackingActive); + CheckSimpleWithKey(key, result, server); + + await server.HotKeysResetAsync(); + result = await server.HotKeysGetAsync(); + Assert.Null(result); + } + + [Fact] + public async Task DurationFilterAsync() + { + Skip.UnlessLongRunning(); // time-based tests are horrible + + RedisKey key = Me(); + await using var muxer = GetServer(key, out var server); + await server.HotKeysStartAsync(duration: TimeSpan.FromSeconds(1)); + var db = muxer.GetDatabase(); + await db.KeyDeleteAsync(key, flags: CommandFlags.FireAndForget); + for (int i = 0; i < 20; i++) + { + await db.StringIncrementAsync(key, flags: CommandFlags.FireAndForget); + } + var before = await server.HotKeysGetAsync(); + await Task.Delay(TimeSpan.FromSeconds(2)); + var after = await server.HotKeysGetAsync(); + + Assert.NotNull(before); + Assert.True(before.TrackingActive); + + Assert.NotNull(after); + Assert.False(after.TrackingActive); + + var millis = after.CollectionDuration.TotalMilliseconds; + Log($"Duration: {millis}ms"); + Assert.True(millis > 900 && millis < 1100); + } + + [Theory] + [InlineData(HotKeysMetrics.Cpu)] + [InlineData(HotKeysMetrics.Network)] + [InlineData(HotKeysMetrics.Network | HotKeysMetrics.Cpu)] + public async Task MetricsChoiceAsync(HotKeysMetrics metrics) + { + RedisKey key = Me(); + await using var muxer = GetServer(key, out var server); + await server.HotKeysStartAsync(metrics, duration: Duration); + var db = muxer.GetDatabase(); + await db.KeyDeleteAsync(key, flags: CommandFlags.FireAndForget); + for (int i = 0; i < 20; i++) + { + await db.StringIncrementAsync(key, flags: CommandFlags.FireAndForget); + } + await server.HotKeysStopAsync(flags: CommandFlags.FireAndForget); + var result = await server.HotKeysGetAsync(); + Assert.NotNull(result); + Assert.Equal(metrics, result.Metrics); + + bool cpu = (metrics & HotKeysMetrics.Cpu) != 0; + bool net = (metrics & HotKeysMetrics.Network) != 0; + + Assert.NotEqual(cpu, result.CpuByKey.IsEmpty); + Assert.Equal(cpu, result.TotalCpuTimeSystem.HasValue); + Assert.Equal(cpu, result.TotalCpuTimeUser.HasValue); + Assert.Equal(cpu, result.TotalCpuTime.HasValue); + + Assert.NotEqual(net, result.NetworkBytesByKey.IsEmpty); + Assert.Equal(net, result.TotalNetworkBytes.HasValue); + } + + [Fact] + public async Task SampleRatioUsageAsync() + { + RedisKey key = Me(); + await using var muxer = GetServer(key, out var server); + await server.HotKeysStartAsync(sampleRatio: 3, duration: Duration); + var db = muxer.GetDatabase(); + await db.KeyDeleteAsync(key, flags: CommandFlags.FireAndForget); + for (int i = 0; i < 20; i++) + { + await db.StringIncrementAsync(key, flags: CommandFlags.FireAndForget); + } + + await server.HotKeysStopAsync(flags: CommandFlags.FireAndForget); + var result = await server.HotKeysGetAsync(); + Assert.NotNull(result); + Assert.True(result.IsSampled, nameof(result.IsSampled)); + Assert.Equal(3, result.SampleRatio); + Assert.True(result.TotalNetworkBytes.HasValue); + Assert.True(result.TotalCpuTime.HasValue); + } +} From 8defa5553dd0803918d5cf2e3848f2d80dd999a2 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 11 Feb 2026 16:19:49 +0000 Subject: [PATCH 414/435] shipped-files --- .../PublicAPI/PublicAPI.Shipped.txt | 79 +++++++++++++++++++ .../PublicAPI/PublicAPI.Unshipped.txt | 79 ------------------- 2 files changed, 79 insertions(+), 79 deletions(-) diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index d2996e2c2..e97d9800a 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -2191,3 +2191,82 @@ StackExchange.Redis.KeyNotificationType.ZUnionStore = 45 -> StackExchange.Redis. static StackExchange.Redis.RedisChannel.KeySpacePrefix(in StackExchange.Redis.RedisKey prefix, int? database = null) -> StackExchange.Redis.RedisChannel static StackExchange.Redis.RedisChannel.KeySpacePrefix(System.ReadOnlySpan prefix, int? database = null) -> StackExchange.Redis.RedisChannel static StackExchange.Redis.RedisChannel.KeySpaceSingleKey(in StackExchange.Redis.RedisKey key, int database) -> StackExchange.Redis.RedisChannel +[SER003]StackExchange.Redis.HotKeysMetrics +[SER003]StackExchange.Redis.HotKeysMetrics.Cpu = 1 -> StackExchange.Redis.HotKeysMetrics +[SER003]StackExchange.Redis.HotKeysMetrics.Network = 2 -> StackExchange.Redis.HotKeysMetrics +[SER003]StackExchange.Redis.HotKeysMetrics.None = 0 -> StackExchange.Redis.HotKeysMetrics +[SER003]StackExchange.Redis.HotKeysResult +[SER003]StackExchange.Redis.HotKeysResult.AllCommandsAllSlotsNetworkBytes.get -> long +[SER003]StackExchange.Redis.HotKeysResult.AllCommandsAllSlotsTime.get -> System.TimeSpan +[SER003]StackExchange.Redis.HotKeysResult.AllCommandsSelectedSlotsNetworkBytes.get -> long? +[SER003]StackExchange.Redis.HotKeysResult.AllCommandsSelectedSlotsTime.get -> System.TimeSpan? +[SER003]StackExchange.Redis.HotKeysResult.CollectionDuration.get -> System.TimeSpan +[SER003]StackExchange.Redis.HotKeysResult.CollectionStartTime.get -> System.DateTime +[SER003]StackExchange.Redis.HotKeysResult.CpuByKey.get -> System.ReadOnlySpan +[SER003]StackExchange.Redis.HotKeysResult.IsSampled.get -> bool +[SER003]StackExchange.Redis.HotKeysResult.IsSlotFiltered.get -> bool +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.Bytes.get -> long +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.Key.get -> StackExchange.Redis.RedisKey +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.MetricKeyBytes() -> void +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.MetricKeyBytes(in StackExchange.Redis.RedisKey key, long bytes) -> void +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.Duration.get -> System.TimeSpan +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.Key.get -> StackExchange.Redis.RedisKey +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.MetricKeyCpu() -> void +[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.MetricKeyCpu(in StackExchange.Redis.RedisKey key, long durationMicroseconds) -> void +[SER003]StackExchange.Redis.HotKeysResult.Metrics.get -> StackExchange.Redis.HotKeysMetrics +[SER003]StackExchange.Redis.HotKeysResult.NetworkBytesByKey.get -> System.ReadOnlySpan +[SER003]StackExchange.Redis.HotKeysResult.SampledCommandsSelectedSlotsNetworkBytes.get -> long? +[SER003]StackExchange.Redis.HotKeysResult.SampledCommandsSelectedSlotsTime.get -> System.TimeSpan? +[SER003]StackExchange.Redis.HotKeysResult.SampleRatio.get -> long +[SER003]StackExchange.Redis.HotKeysResult.SelectedSlots.get -> System.ReadOnlySpan +[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTime.get -> System.TimeSpan? +[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTimeSystem.get -> System.TimeSpan? +[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTimeUser.get -> System.TimeSpan? +[SER003]StackExchange.Redis.HotKeysResult.TotalNetworkBytes.get -> long? +[SER003]StackExchange.Redis.HotKeysResult.TrackingActive.get -> bool +[SER003]StackExchange.Redis.IServer.HotKeysGet(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.HotKeysResult? +[SER003]StackExchange.Redis.IServer.HotKeysGetAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER003]StackExchange.Redis.IServer.HotKeysReset(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +[SER003]StackExchange.Redis.IServer.HotKeysResetAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER003]StackExchange.Redis.IServer.HotKeysStart(StackExchange.Redis.HotKeysMetrics metrics = (StackExchange.Redis.HotKeysMetrics)-1, long count = 0, System.TimeSpan duration = default(System.TimeSpan), long sampleRatio = 1, int[]? slots = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +[SER003]StackExchange.Redis.IServer.HotKeysStartAsync(StackExchange.Redis.HotKeysMetrics metrics = (StackExchange.Redis.HotKeysMetrics)-1, long count = 0, System.TimeSpan duration = default(System.TimeSpan), long sampleRatio = 1, int[]? slots = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER003]StackExchange.Redis.IServer.HotKeysStop(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool +[SER003]StackExchange.Redis.IServer.HotKeysStopAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyBytes.Equals(object? obj) -> bool +[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyBytes.GetHashCode() -> int +[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyBytes.ToString() -> string! +[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyCpu.Equals(object? obj) -> bool +[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyCpu.GetHashCode() -> int +[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyCpu.ToString() -> string! +[SER003]override StackExchange.Redis.StreamIdempotentId.Equals(object? obj) -> bool +[SER003]override StackExchange.Redis.StreamIdempotentId.GetHashCode() -> int +[SER003]override StackExchange.Redis.StreamIdempotentId.ToString() -> string! +[SER003]StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +[SER003]StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue +[SER003]StackExchange.Redis.IDatabase.StreamConfigure(StackExchange.Redis.RedisKey key, StackExchange.Redis.StreamConfiguration! configuration, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void +[SER003]StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER003]StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER003]StackExchange.Redis.IDatabaseAsync.StreamConfigureAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.StreamConfiguration! configuration, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! +[SER003]StackExchange.Redis.StreamConfiguration +[SER003]StackExchange.Redis.StreamConfiguration.IdmpDuration.get -> long? +[SER003]StackExchange.Redis.StreamConfiguration.IdmpDuration.set -> void +[SER003]StackExchange.Redis.StreamConfiguration.IdmpMaxSize.get -> long? +[SER003]StackExchange.Redis.StreamConfiguration.IdmpMaxSize.set -> void +[SER003]StackExchange.Redis.StreamConfiguration.StreamConfiguration() -> void +[SER003]StackExchange.Redis.StreamIdempotentId +[SER003]StackExchange.Redis.StreamIdempotentId.IdempotentId.get -> StackExchange.Redis.RedisValue +[SER003]StackExchange.Redis.StreamIdempotentId.ProducerId.get -> StackExchange.Redis.RedisValue +[SER003]StackExchange.Redis.StreamIdempotentId.StreamIdempotentId() -> void +[SER003]StackExchange.Redis.StreamIdempotentId.StreamIdempotentId(StackExchange.Redis.RedisValue producerId) -> void +[SER003]StackExchange.Redis.StreamIdempotentId.StreamIdempotentId(StackExchange.Redis.RedisValue producerId, StackExchange.Redis.RedisValue idempotentId) -> void +[SER003]StackExchange.Redis.StreamInfo.IdmpDuration.get -> long +[SER003]StackExchange.Redis.StreamInfo.IdmpMaxSize.get -> long +[SER003]StackExchange.Redis.StreamInfo.IidsAdded.get -> long +[SER003]StackExchange.Redis.StreamInfo.IidsDuplicates.get -> long +[SER003]StackExchange.Redis.StreamInfo.IidsTracked.get -> long +[SER003]StackExchange.Redis.StreamInfo.PidsTracked.get -> long +StackExchange.Redis.StreamInfo.EntriesAdded.get -> long +StackExchange.Redis.StreamInfo.MaxDeletedEntryId.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.StreamInfo.RecordedFirstEntryId.get -> StackExchange.Redis.RedisValue diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 1b2aa2a9e..ab058de62 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,80 +1 @@ #nullable enable -[SER003]StackExchange.Redis.HotKeysMetrics -[SER003]StackExchange.Redis.HotKeysMetrics.Cpu = 1 -> StackExchange.Redis.HotKeysMetrics -[SER003]StackExchange.Redis.HotKeysMetrics.Network = 2 -> StackExchange.Redis.HotKeysMetrics -[SER003]StackExchange.Redis.HotKeysMetrics.None = 0 -> StackExchange.Redis.HotKeysMetrics -[SER003]StackExchange.Redis.HotKeysResult -[SER003]StackExchange.Redis.HotKeysResult.AllCommandsAllSlotsNetworkBytes.get -> long -[SER003]StackExchange.Redis.HotKeysResult.AllCommandsAllSlotsTime.get -> System.TimeSpan -[SER003]StackExchange.Redis.HotKeysResult.AllCommandsSelectedSlotsNetworkBytes.get -> long? -[SER003]StackExchange.Redis.HotKeysResult.AllCommandsSelectedSlotsTime.get -> System.TimeSpan? -[SER003]StackExchange.Redis.HotKeysResult.CollectionDuration.get -> System.TimeSpan -[SER003]StackExchange.Redis.HotKeysResult.CollectionStartTime.get -> System.DateTime -[SER003]StackExchange.Redis.HotKeysResult.CpuByKey.get -> System.ReadOnlySpan -[SER003]StackExchange.Redis.HotKeysResult.IsSampled.get -> bool -[SER003]StackExchange.Redis.HotKeysResult.IsSlotFiltered.get -> bool -[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes -[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.Bytes.get -> long -[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.Key.get -> StackExchange.Redis.RedisKey -[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.MetricKeyBytes() -> void -[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.MetricKeyBytes(in StackExchange.Redis.RedisKey key, long bytes) -> void -[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu -[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.Duration.get -> System.TimeSpan -[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.Key.get -> StackExchange.Redis.RedisKey -[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.MetricKeyCpu() -> void -[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.MetricKeyCpu(in StackExchange.Redis.RedisKey key, long durationMicroseconds) -> void -[SER003]StackExchange.Redis.HotKeysResult.Metrics.get -> StackExchange.Redis.HotKeysMetrics -[SER003]StackExchange.Redis.HotKeysResult.NetworkBytesByKey.get -> System.ReadOnlySpan -[SER003]StackExchange.Redis.HotKeysResult.SampledCommandsSelectedSlotsNetworkBytes.get -> long? -[SER003]StackExchange.Redis.HotKeysResult.SampledCommandsSelectedSlotsTime.get -> System.TimeSpan? -[SER003]StackExchange.Redis.HotKeysResult.SampleRatio.get -> long -[SER003]StackExchange.Redis.HotKeysResult.SelectedSlots.get -> System.ReadOnlySpan -[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTime.get -> System.TimeSpan? -[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTimeSystem.get -> System.TimeSpan? -[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTimeUser.get -> System.TimeSpan? -[SER003]StackExchange.Redis.HotKeysResult.TotalNetworkBytes.get -> long? -[SER003]StackExchange.Redis.HotKeysResult.TrackingActive.get -> bool -[SER003]StackExchange.Redis.IServer.HotKeysGet(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.HotKeysResult? -[SER003]StackExchange.Redis.IServer.HotKeysGetAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER003]StackExchange.Redis.IServer.HotKeysReset(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -[SER003]StackExchange.Redis.IServer.HotKeysResetAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER003]StackExchange.Redis.IServer.HotKeysStart(StackExchange.Redis.HotKeysMetrics metrics = (StackExchange.Redis.HotKeysMetrics)-1, long count = 0, System.TimeSpan duration = default(System.TimeSpan), long sampleRatio = 1, int[]? slots = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -[SER003]StackExchange.Redis.IServer.HotKeysStartAsync(StackExchange.Redis.HotKeysMetrics metrics = (StackExchange.Redis.HotKeysMetrics)-1, long count = 0, System.TimeSpan duration = default(System.TimeSpan), long sampleRatio = 1, int[]? slots = null, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER003]StackExchange.Redis.IServer.HotKeysStop(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool -[SER003]StackExchange.Redis.IServer.HotKeysStopAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyBytes.Equals(object? obj) -> bool -[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyBytes.GetHashCode() -> int -[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyBytes.ToString() -> string! -[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyCpu.Equals(object? obj) -> bool -[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyCpu.GetHashCode() -> int -[SER003]override StackExchange.Redis.HotKeysResult.MetricKeyCpu.ToString() -> string! -[SER003]override StackExchange.Redis.StreamIdempotentId.Equals(object? obj) -> bool -[SER003]override StackExchange.Redis.StreamIdempotentId.GetHashCode() -> int -[SER003]override StackExchange.Redis.StreamIdempotentId.ToString() -> string! -[SER003]StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue -[SER003]StackExchange.Redis.IDatabase.StreamAdd(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue -[SER003]StackExchange.Redis.IDatabase.StreamConfigure(StackExchange.Redis.RedisKey key, StackExchange.Redis.StreamConfiguration! configuration, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void -[SER003]StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.NameValueEntry[]! streamPairs, StackExchange.Redis.StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER003]StackExchange.Redis.IDatabaseAsync.StreamAddAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue streamField, StackExchange.Redis.RedisValue streamValue, StackExchange.Redis.StreamIdempotentId idempotentId, long? maxLength = null, bool useApproximateMaxLength = false, long? limit = null, StackExchange.Redis.StreamTrimMode trimMode = StackExchange.Redis.StreamTrimMode.KeepReferences, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER003]StackExchange.Redis.IDatabaseAsync.StreamConfigureAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.StreamConfiguration! configuration, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task! -[SER003]StackExchange.Redis.StreamConfiguration -[SER003]StackExchange.Redis.StreamConfiguration.IdmpDuration.get -> long? -[SER003]StackExchange.Redis.StreamConfiguration.IdmpDuration.set -> void -[SER003]StackExchange.Redis.StreamConfiguration.IdmpMaxSize.get -> long? -[SER003]StackExchange.Redis.StreamConfiguration.IdmpMaxSize.set -> void -[SER003]StackExchange.Redis.StreamConfiguration.StreamConfiguration() -> void -[SER003]StackExchange.Redis.StreamIdempotentId -[SER003]StackExchange.Redis.StreamIdempotentId.IdempotentId.get -> StackExchange.Redis.RedisValue -[SER003]StackExchange.Redis.StreamIdempotentId.ProducerId.get -> StackExchange.Redis.RedisValue -[SER003]StackExchange.Redis.StreamIdempotentId.StreamIdempotentId() -> void -[SER003]StackExchange.Redis.StreamIdempotentId.StreamIdempotentId(StackExchange.Redis.RedisValue producerId) -> void -[SER003]StackExchange.Redis.StreamIdempotentId.StreamIdempotentId(StackExchange.Redis.RedisValue producerId, StackExchange.Redis.RedisValue idempotentId) -> void -[SER003]StackExchange.Redis.StreamInfo.IdmpDuration.get -> long -[SER003]StackExchange.Redis.StreamInfo.IdmpMaxSize.get -> long -[SER003]StackExchange.Redis.StreamInfo.IidsAdded.get -> long -[SER003]StackExchange.Redis.StreamInfo.IidsDuplicates.get -> long -[SER003]StackExchange.Redis.StreamInfo.IidsTracked.get -> long -[SER003]StackExchange.Redis.StreamInfo.PidsTracked.get -> long -StackExchange.Redis.StreamInfo.EntriesAdded.get -> long -StackExchange.Redis.StreamInfo.MaxDeletedEntryId.get -> StackExchange.Redis.RedisValue -StackExchange.Redis.StreamInfo.RecordedFirstEntryId.get -> StackExchange.Redis.RedisValue From 58d2f9862bcaf7f40a635e0ed0259f731a771f9d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 11 Feb 2026 16:22:17 +0000 Subject: [PATCH 415/435] Add additional documentation about client authentication (#3009) * test * docs * Revert "test" This reverts commit ecd118950a14857723d9affdd2154194720c6fe8. * Update Authentication.md * Update Authentication.md * Update index.md * prefer async --- docs/Authentication.md | 124 +++++++++++++++++++++++++++++++++++++++++ docs/index.md | 1 + 2 files changed, 125 insertions(+) create mode 100644 docs/Authentication.md diff --git a/docs/Authentication.md b/docs/Authentication.md new file mode 100644 index 000000000..f5551559d --- /dev/null +++ b/docs/Authentication.md @@ -0,0 +1,124 @@ +Authentication +=== + +There are multiple ways of connecting to a Redis server, depending on the authentication model. The simplest +(but least secure) approach is to use the `default` user, with no authentication, and no transport security. +This is as simple as: + +``` csharp +var muxer = await ConnectionMultiplexer.ConnectAsync("myserver"); // or myserver:1241 to use a custom port +``` + +This approach is often used for local transient servers - it is simple, but insecure. But from there, +we can get more complex! + +TLS +=== + +If your server has TLS enabled, SE.Redis can be instructed to use it. In some cases (AMR, etc), the +library will recognize the endpoint address, meaning: *you do not need to do anything*. To +*manually* enable TLS, the `ssl` token can be used: + +``` csharp +var muxer = await ConnectionMultiplexer.ConnectAsync("myserver,ssl=true"); +``` + +This will work fine if the server is using a server-certificate that is already trusted by the local +machine. If this is *not* the case, we need to tell the library about the server. This requires +the `ConfigurationOptions` type: + +``` csharp +var options = ConfigurationOptions.Parse("myserver,ssl=true"); +// or: var options = new ConfigurationOptions { Endpoints = { "myserver" }, Ssl = true }; +// TODO configure +var muxer = await ConnectionMultiplexer.ConnectAsync(options); +``` + +If we have a local *issuer* public certificate (commonly `ca.crt`), we can use: + +``` csharp +options.TrustIssuer(caPath); +``` + +Alternatively, in advanced scenarios: to provide your own custom server validation, the `options.CertificateValidation` callback +can be used; this uses the normal [`RemoteCertificateValidationCallback`](https://learn.microsoft.com/dotnet/api/system.net.security.remotecertificatevalidationcallback) +API. + +Usernames and Passwords +=== + +Usernames and passwords can be specified with the `user` and `password` tokens, respectively: + +``` csharp +var muxer = await ConnectionMultiplexer.ConnectAsync("myserver,ssl=true,user=myuser,password=mypassword"); +``` + +If no `user` is provided, the `default` user is assumed. In some cases, an authentication-token can be +used in place of a classic password. + +Client certificates +=== + +If the server is configured to require a client certificate, this can be supplied in multiple ways. +If you have a local public / private key pair (such as `MyUser2.crt` and `MyUser2.key`), the +`options.SetUserPemCertificate(...)` method can be used: + +``` csharp +config.SetUserPemCertificate( + userCertificatePath: userCrtPath, + userKeyPath: userKeyPath +); +``` + +If you have a single `pfx` file that contains the public / private pair, the `options.SetUserPfxCertificate(...)` +method can be used: + +``` csharp +config.SetUserPfxCertificate( + userCertificatePath: userCrtPath, + password: filePassword // optional +); +``` + +Alternatively, in advanced scenarios: to provide your own custom client-certificate lookup, the `options.CertificateSelection` callback +can be used; this uses the normal +[`LocalCertificateSelectionCallback`](https://learn.microsoft.com/dotnet/api/system.net.security.remotecertificatevalidationcallback) +API. + +User certificates with implicit user authentication +=== + +Historically, the client certificate only provided access to the server, but as the `default` user. From 8.6, +the server can be configured to use client certificates to provide user identity. This replaces the +usage of passwords, and requires: + +- An 8.6+ server, configured to use TLS with client certificates mapped - typically using the `CN` of the certificate as the user. +- A matching `ACL` user account configured on the server, that is enabled (`on`) - i.e. the `ACL LIST` command should + display something like `user MyUser2 on sanitize-payload ~* &* +@all` (the details will vary depending on the user permissions). +- At the client: access to the client certificate pair. + +For example: + +``` csharp +string certRoot = // some path to a folder with ca.crt, MyUser2.crt and MyUser2.key + +var options = ConfigurationOptions.Parse("myserver:6380"); +options.SetUserPemCertificate(// automatically enables TLS + userCertificatePath: Path.Combine(certRoot, "MyUser2.crt"), + userKeyPath: Path.Combine(certRoot, "MyUser2.key")); +options.TrustIssuer(Path.Combine(certRoot, "ca.crt")); +await using var conn = await ConnectionMultiplexer.ConnectAsync(options); + +// prove we are connected as MyUser2 +var user = (string?)await conn.GetDatabase().ExecuteAsync("acl", "whoami"); +Console.WriteLine(user); // writes "MyUser2" +``` + +More info +=== + +For more information: + +- [Redis Security](https://redis.io/docs/latest/operate/oss_and_stack/management/security/) + - [ACL](https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/) + - [TLS](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/) diff --git a/docs/index.md b/docs/index.md index 9180d3423..25bdd942e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,6 +31,7 @@ Documentation --- - [Server](Server) - running a redis server +- [Authentication](Authentication) - connecting to a Redis server with user authentication - [Basic Usage](Basics) - getting started and basic usage - [Async Timeouts](AsyncTimeouts) - async timeouts and cancellation - [Configuration](Configuration) - options available when connecting to redis From 98eb6a28c9f2d1e2c6e2a656f5e491dc164861d8 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 11 Feb 2026 16:35:34 +0000 Subject: [PATCH 416/435] fixup release version --- version.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.json b/version.json index fb6e5bc8a..8d660cad3 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "version": "2.11", - "versionHeightOffset": 0, + "versionHeightOffset": -10, "assemblyVersion": "2.0", "publicReleaseRefSpec": [ "^refs/heads/main$", @@ -15,4 +15,4 @@ "setVersionVariables": true } } -} \ No newline at end of file +} From a2ac95672ecc5efd8c5021426627cdd088646c64 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 13 Feb 2026 09:42:33 +0000 Subject: [PATCH 417/435] Investigate #3007; defensive code in maintenance events, and resubscription; fix inappropriate async void (#3013) --- .../Maintenance/AzureMaintenanceEvent.cs | 28 ++++++++++++------- src/StackExchange.Redis/RedisSubscriber.cs | 9 +++++- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs index 4e32afa5a..f4c7d3e49 100644 --- a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs @@ -124,24 +124,32 @@ internal static async Task AddListenerAsync(ConnectionMultiplexer multiplexer, A try { var sub = multiplexer.GetSubscriber(); - if (sub == null) + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (sub is null) { log?.Invoke("Failed to GetSubscriber for AzureRedisEvents"); return; } - await sub.SubscribeAsync(RedisChannel.Literal(PubSubChannelName), async (_, message) => + await sub.SubscribeAsync(RedisChannel.Literal(PubSubChannelName), (_, message) => { - var newMessage = new AzureMaintenanceEvent(message!); - newMessage.NotifyMultiplexer(multiplexer); + try + { + var newMessage = new AzureMaintenanceEvent(message!); + newMessage.NotifyMultiplexer(multiplexer); - switch (newMessage.NotificationType) + switch (newMessage.NotificationType) + { + case AzureNotificationType.NodeMaintenanceEnded: + case AzureNotificationType.NodeMaintenanceFailoverComplete: + case AzureNotificationType.NodeMaintenanceScaleComplete: + multiplexer.ReconfigureAsync($"Azure Event: {newMessage.NotificationType.ToString()}").RedisFireAndForget(); + break; + } + } + catch (Exception e) { - case AzureNotificationType.NodeMaintenanceEnded: - case AzureNotificationType.NodeMaintenanceFailoverComplete: - case AzureNotificationType.NodeMaintenanceScaleComplete: - await multiplexer.ReconfigureAsync($"Azure Event: {newMessage.NotificationType}").ForAwait(); - break; + log?.Invoke($"Encountered exception: {e}"); } }).ForAwait(); } diff --git a/src/StackExchange.Redis/RedisSubscriber.cs b/src/StackExchange.Redis/RedisSubscriber.cs index ca66e6113..bd2434771 100644 --- a/src/StackExchange.Redis/RedisSubscriber.cs +++ b/src/StackExchange.Redis/RedisSubscriber.cs @@ -135,7 +135,14 @@ internal long EnsureSubscriptions(CommandFlags flags = CommandFlags.None) var subscriber = DefaultSubscriber; foreach (var pair in subscriptions) { - count += pair.Value.EnsureSubscribedToServer(subscriber, pair.Key, flags, true); + try + { + count += pair.Value.EnsureSubscribedToServer(subscriber, pair.Key, flags, true); + } + catch (Exception ex) + { + OnInternalError(ex); + } } return count; } From c0c384389de557176de457612f69d681e9dedeb8 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 13 Feb 2026 20:07:33 +0000 Subject: [PATCH 418/435] Propose CAS/CAD docs (#3012) * Propose CAS/CAD docs * internal link * clarify usage of LockExtendAsync * reuse local * Update docs/CompareAndSwap.md Co-authored-by: Philo --------- Co-authored-by: Philo --- docs/CompareAndSwap.md | 321 +++++++++++++++++++++++++++++++++++++++++ docs/index.md | 1 + 2 files changed, 322 insertions(+) create mode 100644 docs/CompareAndSwap.md diff --git a/docs/CompareAndSwap.md b/docs/CompareAndSwap.md new file mode 100644 index 000000000..7d79d42a0 --- /dev/null +++ b/docs/CompareAndSwap.md @@ -0,0 +1,321 @@ +# Compare-And-Swap / Compare-And-Delete (CAS/CAD) + +Redis 8.4 introduces atomic Compare-And-Swap (CAS) and Compare-And-Delete (CAD) operations, allowing you to conditionally modify +or delete values based on their current state. SE.Redis exposes these features through the `ValueCondition` abstraction. + +## Prerequisites + +- Redis 8.4.0 or later + +## Overview + +Traditional Redis operations like `SET NX` (set if not exists) and `SET XX` (set if exists) only check for key existence. +CAS/CAD operations go further by allowing you to verify the **actual value** before making changes, enabling true atomic +compare-and-swap semantics, without requiring Lua scripts or complex `MULTI`/`WATCH`/`EXEC` usage. + +The `ValueCondition` struct supports several condition types: + +- **Existence checks**: `Always`, `Exists`, `NotExists` (equivalent to the traditional `When` enum) +- **Value equality**: `Equal(value)`, `NotEqual(value)` - compare the full value (uses `IFEQ`/`IFNE`) +- **Digest equality**: `DigestEqual(value)`, `DigestNotEqual(value)` - compare XXH3 64-bit hash (uses `IFDEQ`/`IFDNE`) + +## Basic Value Equality Checks + +Use value equality when you need to verify the exact current value before updating or deleting: + +```csharp +var db = connection.GetDatabase(); +var key = "user:session:12345"; + +// Set a value only if it currently equals a specific value +var currentToken = "old-token-abc"; +var newToken = "new-token-xyz"; + +var wasSet = await db.StringSetAsync( + key, + newToken, + when: ValueCondition.Equal(currentToken) +); + +if (wasSet) +{ + Console.WriteLine("Token successfully rotated"); +} +else +{ + Console.WriteLine("Token mismatch - someone else updated it"); +} +``` + +### Conditional Delete + +Delete a key only if it contains a specific value: + +```csharp +var lockToken = "my-unique-lock-token"; + +// Only delete if the lock still has our token +var wasDeleted = await db.StringDeleteAsync( + "resource:lock", + when: ValueCondition.Equal(lockToken) +); + +if (wasDeleted) +{ + Console.WriteLine("Lock released successfully"); +} +else +{ + Console.WriteLine("Lock was already released or taken by someone else"); +} +``` + +(see also the [Lock Operations section](#lock-operations) below) + +## Digest-Based Checks + +For large values, comparing the full value can be inefficient. Digest-based checks use XXH3 64-bit hashing to compare values efficiently: + +```csharp +var key = "document:content"; +var largeDocument = GetLargeDocumentBytes(); // e.g., 10MB + +// Calculate digest locally +var expectedDigest = ValueCondition.CalculateDigest(largeDocument); + +// Update only if the document hasn't changed +var newDocument = GetUpdatedDocumentBytes(); +var wasSet = await db.StringSetAsync( + key, + newDocument, + when: expectedDigest +); +``` + +### Retrieving Server-Side Digests + +You can retrieve the digest of a value stored in Redis without fetching the entire value: + +```csharp +// Get the digest of the current value +var digest = await db.StringDigestAsync(key); + +if (digest.HasValue) +{ + Console.WriteLine($"Current digest: {digest.Value}"); + + // Later, use this digest for conditional operations + var wasDeleted = await db.StringDeleteAsync(key, when: digest.Value); +} +else +{ + Console.WriteLine("Key does not exist"); +} +``` + +## Negating Conditions + +Use the `!` operator to negate any condition: + +```csharp +var expectedValue = "old-value"; + +// Set only if the value is NOT equal to expectedValue +var wasSet = await db.StringSetAsync( + key, + "new-value", + when: !ValueCondition.Equal(expectedValue) +); + +// Equivalent to: +var wasSet2 = await db.StringSetAsync( + key, + "new-value", + when: ValueCondition.NotEqual(expectedValue) +); +``` + +## Converting Between Value and Digest Conditions + +Convert a value condition to a digest condition for efficiency: + +```csharp +var valueCondition = ValueCondition.Equal("some-value"); + +// Convert to digest-based check +var digestCondition = valueCondition.AsDigest(); + +// Now uses IFDEQ instead of IFEQ +var wasSet = await db.StringSetAsync(key, "new-value", when: digestCondition); +``` + +## Parsing Digests + +If you receive a XXH3 digest as a hex string (e.g., from external systems), you can parse it: + +```csharp +// Parse from hex string +var digestCondition = ValueCondition.ParseDigest("e34615aade2e6333"); + +// Use in conditional operations +var wasSet = await db.StringSetAsync(key, newValue, when: digestCondition); +``` + +## Lock Operations + +StackExchange.Redis automatically uses CAS/CAD for lock operations when Redis 8.4+ is available, providing better performance and atomicity: + +```csharp +var lockKey = "resource:lock"; +var lockToken = Guid.NewGuid().ToString(); +var lockExpiry = TimeSpan.FromSeconds(30); + +// Take a lock (uses NX internally) +if (await db.LockTakeAsync(lockKey, lockToken, lockExpiry)) +{ + try + { + // Do work while holding the lock + + // Extend the lock (uses CAS internally on Redis 8.4+) + if (!(await db.LockExtendAsync(lockKey, lockToken, lockExpiry))) + { + // Failed to extend the lock - it expired, or was forcibly taken against our will + throw new InvalidOperationException("Lock extension failed - check expiry duration is appropriate."); + } + + // Do more work... + } + finally + { + // Release the lock (uses CAD internally on Redis 8.4+) + await db.LockReleaseAsync(lockKey, lockToken); + } +} +``` + +On Redis 8.4+, `LockExtend` uses `SET` with `IFEQ` and `LockRelease` uses `DELEX` with `IFEQ`, eliminating +the need for transactions. + +## Common Patterns + +### Optimistic Locking + +Implement optimistic concurrency control for updating data: + +```csharp +async Task UpdateUserProfileAsync(string userId, Func updateFunc) +{ + var key = $"user:profile:{userId}"; + + // Read current value + var currentJson = await db.StringGetAsync(key); + if (currentJson.IsNull) + { + return false; // User doesn't exist + } + + var currentProfile = JsonSerializer.Deserialize(currentJson!); + var updatedProfile = updateFunc(currentProfile); + var updatedJson = JsonSerializer.Serialize(updatedProfile); + + // Attempt to update only if value hasn't changed + var wasSet = await db.StringSetAsync( + key, + updatedJson, + when: ValueCondition.Equal(currentJson) + ); + + return wasSet; // Returns false if someone else modified it +} + +// Usage with retry logic +int maxRetries = 10; +for (int i = 0; i < maxRetries; i++) +{ + if (await UpdateUserProfileAsync(userId, profile => + { + profile.LastLogin = DateTime.UtcNow; + return profile; + })) + { + break; // Success + } + + // Retry with exponential backoff + await Task.Delay(TimeSpan.FromMilliseconds(Math.Pow(2, i) * 10)); +} +``` + +### Session Token Rotation + +Safely rotate session tokens with atomic verification: + +```csharp +async Task RotateSessionTokenAsync(string sessionId, string expectedToken) +{ + var key = $"session:{sessionId}"; + var newToken = GenerateSecureToken(); + + // Only rotate if the current token matches + var wasRotated = await db.StringSetAsync( + key, + newToken, + expiry: TimeSpan.FromHours(24), + when: ValueCondition.Equal(expectedToken) + ); + + return wasRotated; +} +``` + +### Large Document Updates with Digest + +For large documents, use digests to avoid transferring the full value: + +```csharp +async Task UpdateLargeDocumentAsync(string docId, byte[] newContent) +{ + var key = $"document:{docId}"; + + // Get just the digest, not the full document + var currentDigest = await db.StringDigestAsync(key); + + if (!currentDigest.HasValue) + { + return false; // Document doesn't exist + } + + // Update only if digest matches (document unchanged) + var wasSet = await db.StringSetAsync( + key, + newContent, + when: currentDigest.Value + ); + + return wasSet; +} +``` + +## Performance Considerations + +### Value vs. Digest Checks + +- **Value equality** (`IFEQ`/`IFNE`): Best for small values (< 1KB). Sends the full value to Redis for comparison. +- **Digest equality** (`IFDEQ`/`IFDNE`): Best for large values. Only sends a 16-character hex digest (8 bytes). + +```csharp +// For small values (session tokens, IDs, etc.) +var condition = ValueCondition.Equal(smallValue); + +// For large values (documents, images, etc.) +var condition = ValueCondition.DigestEqual(largeValue); +// or +var condition = ValueCondition.CalculateDigest(largeValueBytes); +``` + +## See Also + +- [Transactions](Transactions.md) - For multi-key atomic operations +- [Keys and Values](KeysValues.md) - Understanding Redis data types +- [Redis CAS/CAD Documentation](https://redis.io/docs/latest/commands/set/) - Redis 8.4 SET command with IFEQ/IFNE/IFDEQ/IFDNE modifiers diff --git a/docs/index.md b/docs/index.md index 25bdd942e..4e4cfbc3f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -38,6 +38,7 @@ Documentation - [Pipelines and Multiplexers](PipelinesMultiplexers) - what is a multiplexer? - [Keys, Values and Channels](KeysValues) - discusses the data-types used on the API - [Transactions](Transactions) - how atomic transactions work in redis +- [Compare-And-Swap / Compare-And-Delete (CAS/CAD)](CompareAndSwap) - atomic conditional operations using value comparison - [Events](Events) - the events available for logging / information purposes - [Pub/Sub Message Order](PubSubOrder) - advice on sequential and concurrent processing - [Pub/Sub Key Notifications](KeyspaceNotifications) - how to use keyspace and keyevent notifications From 2a846a50f375efa3e7fca5453c8e84ccfaf3fe5a Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Sat, 14 Feb 2026 08:59:25 +0000 Subject: [PATCH 419/435] Add VRANGE support (#3011) * Implement VRANGE support (8.4) * docs * using lease for VRANGE * release notes * space * Update docs/VectorSets.md Co-authored-by: Philo * Update docs/VectorSets.md Co-authored-by: Philo --------- Co-authored-by: Philo --- docs/ReleaseNotes.md | 4 + docs/VectorSets.md | 394 ++++++++++++++ docs/index.md | 1 + src/StackExchange.Redis/Enums/RedisCommand.cs | 2 + .../Interfaces/IDatabase.VectorSets.cs | 40 ++ .../Interfaces/IDatabaseAsync.VectorSets.cs | 20 + .../KeyPrefixed.VectorSets.cs | 18 + .../KeyPrefixedDatabase.VectorSets.cs | 18 + src/StackExchange.Redis/Lease.cs | 5 + .../PublicAPI/PublicAPI.Unshipped.txt | 5 + .../RedisDatabase.VectorSets.cs | 106 ++++ .../ResultProcessor.VectorSets.cs | 11 + .../VectorSetIntegrationTests.cs | 485 ++++++++++++++++++ 13 files changed, 1109 insertions(+) create mode 100644 docs/VectorSets.md diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index c038ab327..e9c1e2e79 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,6 +8,10 @@ Current package versions: ## 2.11.unreleased +- Add support for `VRANGE` ([#3011 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3011)) + +## 2.11.0 + - Add support for `HOTKEYS` ([#3008 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3008)) - Add support for keyspace notifications ([#2995 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2995)) - Add support for idempotent stream entry (`XADD IDMP[AUTO]`) support ([#3006 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3006)) diff --git a/docs/VectorSets.md b/docs/VectorSets.md new file mode 100644 index 000000000..9a362ec15 --- /dev/null +++ b/docs/VectorSets.md @@ -0,0 +1,394 @@ +# Redis Vector Sets + +Redis Vector Sets provide efficient storage and similarity search for vector data. SE.Redis provides a strongly-typed API for working with vector sets. + +## Prerequisites + +### Redis Version + +Vector Sets require Redis 8.0 or later. + +## Quick Start + +Note that the vectors used in these examples are small for illustrative purposes. In practice, you would commonly use much +larger vectors. The API is designed to efficiently handle large vectors - in particular, the use of `ReadOnlyMemory` +rather than arrays allows you to work with vectors in "pooled" memory buffers (such as `ArrayPool`), which can be more +efficient than creating arrays - or even working with raw memory for example memory-mapped-files. + +### Adding Vectors + +Add vectors to a vector set using `VectorSetAddAsync`: + +```csharp +var db = conn.GetDatabase(); +var key = "product-embeddings"; + +// Create a vector (e.g., from an ML model) +var vector = new[] { 0.1f, 0.2f, 0.3f, 0.4f }; + +// Add a member with its vector +var request = VectorSetAddRequest.Member("product-123", vector.AsMemory()); +bool added = await db.VectorSetAddAsync(key, request); +``` + +### Adding Vectors with Attributes + +You can attach JSON metadata to vectors for filtering: + +```csharp +var vector = new[] { 0.1f, 0.2f, 0.3f, 0.4f }; +var request = VectorSetAddRequest.Member( + "product-123", + vector.AsMemory(), + attributesJson: """{"category":"electronics","price":299.99}""" +); +await db.VectorSetAddAsync(key, request); +``` + +### Similarity Search + +Find similar vectors using `VectorSetSimilaritySearchAsync`: + +```csharp +// Search by an existing member +var query = VectorSetSimilaritySearchRequest.ByMember("product-123"); +query.Count = 10; +query.WithScores = true; + +using var results = await db.VectorSetSimilaritySearchAsync(key, query); +if (results is not null) +{ + foreach (var result in results.Value.Results) + { + Console.WriteLine($"Member: {result.Member}, Score: {result.Score}"); + } +} +``` + +Or search by a vector directly: + +```csharp +var queryVector = new[] { 0.15f, 0.25f, 0.35f, 0.45f }; +var query = VectorSetSimilaritySearchRequest.ByVector(queryVector.AsMemory()); +query.Count = 10; +query.WithScores = true; + +using var results = await db.VectorSetSimilaritySearchAsync(key, query); +``` + +### Filtered Search + +Use JSON path expressions to filter results: + +```csharp +var query = VectorSetSimilaritySearchRequest.ByVector(queryVector.AsMemory()); +query.Count = 10; +query.FilterExpression = "$.category == 'electronics' && $.price < 500"; +query.WithAttributes = true; // Include attributes in results + +using var results = await db.VectorSetSimilaritySearchAsync(key, query); +``` + +See [Redis filtered search documentation](https://redis.io/docs/latest/develop/data-types/vector-sets/filtered-search/) for filter syntax. + +## Vector Set Operations + +### Getting Vector Set Information + +```csharp +var info = await db.VectorSetInfoAsync(key); +if (info is not null) +{ + Console.WriteLine($"Dimension: {info.Value.Dimension}"); + Console.WriteLine($"Length: {info.Value.Length}"); + Console.WriteLine($"Quantization: {info.Value.Quantization}"); +} +``` + +### Checking Membership + +```csharp +bool exists = await db.VectorSetContainsAsync(key, "product-123"); +``` + +### Removing Members + +```csharp +bool removed = await db.VectorSetRemoveAsync(key, "product-123"); +``` + +### Getting Random Members + +```csharp +// Get a single random member +var member = await db.VectorSetRandomMemberAsync(key); + +// Get multiple random members +var members = await db.VectorSetRandomMembersAsync(key, count: 5); +``` + +## Range Queries + +### Getting Members by Lexicographical Range + +Retrieve members in lexicographical order: + +```csharp +// Get all members +using var allMembers = await db.VectorSetRangeAsync(key); +// ... access allMembers.Span, etc + +// Get members in a specific range +using var rangeMembers = await db.VectorSetRangeAsync( + key, + start: "product-100", + end: "product-200", + count: 50 +); +// ... access rangeMembers.Span, etc + +// Exclude boundaries +using var members = await db.VectorSetRangeAsync( + key, + start: "product-100", + end: "product-200", + exclude: Exclude.Both +); +// ... access members.Span, etc +``` + +### Enumerating Large Result Sets + +For large vector sets, use enumeration to process results in batches: + +```csharp +await foreach (var member in db.VectorSetRangeEnumerateAsync(key, count: 100)) +{ + Console.WriteLine($"Processing: {member}"); +} +``` + +The enumeration of results is done in batches, so that the client does not need to buffer the entire result set in memory; +if you exit the loop early, the client and server will stop processing and sending results. This also supports async cancellation: + +```csharp +using var cts = new CancellationTokenSource(); // cancellation not shown + +await foreach (var member in db.VectorSetRangeEnumerateAsync(key, count: 100) + .WithCancellation(cts.Token)) +{ + // ... +} +``` + +## Advanced Configuration + +### Quantization + +Control vector compression: + +```csharp +var request = VectorSetAddRequest.Member("product-123", vector.AsMemory()); +request.Quantization = VectorSetQuantization.Int8; // Default +// or VectorSetQuantization.None +// or VectorSetQuantization.Binary +await db.VectorSetAddAsync(key, request); +``` + +### Dimension Reduction + +Use projection to reduce vector dimensions: + +```csharp +var request = VectorSetAddRequest.Member("product-123", vector.AsMemory()); +request.ReducedDimensions = 128; // Reduce from original dimension +await db.VectorSetAddAsync(key, request); +``` + +### HNSW Parameters + +Fine-tune the HNSW index: + +```csharp +var request = VectorSetAddRequest.Member("product-123", vector.AsMemory()); +request.MaxConnections = 32; // M parameter (default: 16) +request.BuildExplorationFactor = 400; // EF parameter (default: 200) +await db.VectorSetAddAsync(key, request); +``` + +### Search Parameters + +Control search behavior: + +```csharp +var query = VectorSetSimilaritySearchRequest.ByVector(queryVector.AsMemory()); +query.SearchExplorationFactor = 500; // Higher = more accurate, slower +query.Epsilon = 0.1; // Only return similarity >= 0.9 +query.UseExactSearch = true; // Use linear scan instead of HNSW +await db.VectorSetSimilaritySearchAsync(key, query); +``` + +## Working with Vector Data + +### Retrieving Vectors + +Get the approximate vector for a member: + +```csharp +using var vectorLease = await db.VectorSetGetApproximateVectorAsync(key, "product-123"); +if (vectorLease != null) +{ + ReadOnlySpan vector = vectorLease.Value.Span; + // Use the vector data +} +``` + +### Managing Attributes + +Get and set JSON attributes: + +```csharp +// Get attributes +var json = await db.VectorSetGetAttributesJsonAsync(key, "product-123"); + +// Set attributes +await db.VectorSetSetAttributesJsonAsync( + key, + "product-123", + """{"category":"electronics","updated":"2024-01-15"}""" +); +``` + +### Graph Links + +Inspect HNSW graph connections: + +```csharp +// Get linked members +using var links = await db.VectorSetGetLinksAsync(key, "product-123"); +if (links != null) +{ + foreach (var link in links.Value.Span) + { + Console.WriteLine($"Linked to: {link}"); + } +} + +// Get links with similarity scores +using var linksWithScores = await db.VectorSetGetLinksWithScoresAsync(key, "product-123"); +if (linksWithScores != null) +{ + foreach (var link in linksWithScores.Value.Span) + { + Console.WriteLine($"Linked to: {link.Member}, Score: {link.Score}"); + } +} +``` + +## Memory Management + +Vector operations return `Lease` for efficient memory pooling. Always dispose leases: + +```csharp +// Using statement (recommended) +using var results = await db.VectorSetSimilaritySearchAsync(key, query); + +// Or explicit disposal +var results = await db.VectorSetSimilaritySearchAsync(key, query); +try +{ + // Use results +} +finally +{ + results?.Dispose(); +} +``` + +## Performance Considerations + +### Batch Operations + +For bulk inserts, consider using pipelining: + +```csharp +var batch = db.CreateBatch(); +var tasks = new List>(); + +foreach (var (member, vector) in vectorData) +{ + var request = VectorSetAddRequest.Member(member, vector.AsMemory()); + tasks.Add(batch.VectorSetAddAsync(key, request)); +} + +batch.Execute(); +await Task.WhenAll(tasks); +``` + +### Search Optimization + +- Use **quantization** to reduce memory usage and improve search speed +- Tune **SearchExplorationFactor** based on accuracy vs. speed requirements +- Use **filters** to reduce the search space +- Consider **dimension reduction** for very high-dimensional vectors + +### Range Query Pagination + +Prefer enumeration for large result sets to avoid loading everything into memory: + +```csharp +// Good: loads results in batches, processes items individually +await foreach (var member in db.VectorSetRangeEnumerateAsync(key)) +{ + await ProcessMemberAsync(member); +} + +// Avoid: loads all results at once +using var allMembers1 = await db.VectorSetRangeAsync(key); + +// Avoid: loads results in batches, but still loads everything into memory at once +var allMembers2 = await db.VectorSetRangeEnumerateAsync(key).ToArrayAsync(); +``` + +## Common Patterns + +### Semantic Search + +```csharp +// 1. Store document embeddings +var embedding = await GetEmbeddingFromMLModel(document); +var request = VectorSetAddRequest.Member( + documentId, + embedding.AsMemory(), + attributesJson: $$"""{"title":"{{document.Title}}","date":"{{document.Date}}"}""" +); +await db.VectorSetAddAsync("documents", request); + +// 2. Search for similar documents +var queryEmbedding = await GetEmbeddingFromMLModel(searchQuery); +var query = VectorSetSimilaritySearchRequest.ByVector(queryEmbedding.AsMemory()); +query.Count = 10; +query.WithScores = true; +query.WithAttributes = true; + +using var results = await db.VectorSetSimilaritySearchAsync("documents", query); +``` + +### Recommendation System + +```csharp +// Find similar items based on an item the user liked +var query = VectorSetSimilaritySearchRequest.ByMember(userLikedItemId); +query.Count = 20; +query.FilterExpression = "$.inStock == true && $.price < 100"; +query.WithScores = true; + +using var recommendations = await db.VectorSetSimilaritySearchAsync("products", query); +``` + +## See Also + +- [Redis Vector Sets Documentation](https://redis.io/docs/latest/develop/data-types/vector-sets/) +- [HNSW Algorithm](https://arxiv.org/abs/1603.09320) +- [Filtered Search Syntax](https://redis.io/docs/latest/develop/data-types/vector-sets/filtered-search/) + diff --git a/docs/index.md b/docs/index.md index 4e4cfbc3f..0a2e6c721 100644 --- a/docs/index.md +++ b/docs/index.md @@ -46,6 +46,7 @@ Documentation - [Using RESP3](Resp3) - information on using RESP3 - [ServerMaintenanceEvent](ServerMaintenanceEvent) - how to listen and prepare for hosted server maintenance (e.g. Azure Cache for Redis) - [Streams](Streams) - how to use the Stream data type +- [Vector Sets](VectorSets) - how to use Vector Sets for similarity search with embeddings - [Where are `KEYS` / `SCAN` / `FLUSH*`?](KeysScan) - how to use server-based commands - [Profiling](Profiling) - profiling interfaces, as well as how to profile in an `async` world - [Scripting](Scripting) - running Lua scripts with convenient named parameter replacement diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index c55a39d8a..2a4d1180f 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -219,6 +219,7 @@ internal enum RedisCommand VISMEMBER, VLINKS, VRANDMEMBER, + VRANGE, VREM, VSETATTR, VSIM, @@ -533,6 +534,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command) case RedisCommand.VISMEMBER: case RedisCommand.VLINKS: case RedisCommand.VRANDMEMBER: + case RedisCommand.VRANGE: case RedisCommand.VSIM: // Writable commands, but allowed for the writable-replicas scenario case RedisCommand.COPY: diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs index 039075ec8..1c163f315 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs @@ -180,4 +180,44 @@ bool VectorSetSetAttributesJson( RedisKey key, VectorSetSimilaritySearchRequest query, CommandFlags flags = CommandFlags.None); + + /// + /// Get a range of members from a vectorset by lexicographical order. + /// + /// The key of the vectorset. + /// The minimum value to filter by (inclusive by default). + /// The maximum value to filter by (inclusive by default). + /// The maximum number of members to return (-1 for all). + /// Whether to exclude the start and/or end values. + /// The flags to use for this operation. + /// Members in the specified range as a pooled memory lease. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Lease VectorSetRange( + RedisKey key, + RedisValue start = default, + RedisValue end = default, + long count = -1, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None); + + /// + /// Enumerate members from a vectorset by lexicographical order in batches. + /// + /// The key of the vectorset. + /// The minimum value to filter by (inclusive by default). + /// The maximum value to filter by (inclusive by default). + /// The batch size for each iteration. + /// Whether to exclude the start and/or end values. + /// The flags to use for this operation. + /// An enumerable of members in the specified range. + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + System.Collections.Generic.IEnumerable VectorSetRangeEnumerate( + RedisKey key, + RedisValue start = default, + RedisValue end = default, + long count = 100, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None); } diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs index 3ac67d40f..7b8825e4c 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs @@ -93,6 +93,26 @@ Task VectorSetSetAttributesJsonAsync( VectorSetSimilaritySearchRequest query, CommandFlags flags = CommandFlags.None); + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + Task?> VectorSetRangeAsync( + RedisKey key, + RedisValue start = default, + RedisValue end = default, + long count = -1, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None); + + /// + [Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)] + System.Collections.Generic.IAsyncEnumerable VectorSetRangeEnumerateAsync( + RedisKey key, + RedisValue start = default, + RedisValue end = default, + long count = 100, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None); + /// Task StreamReadGroupAsync(StreamPosition[] streamPositions, RedisValue groupName, RedisValue consumerName, int? countPerStream = null, bool noAck = false, TimeSpan? claimMinIdleTime = null, CommandFlags flags = CommandFlags.None); } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs index 809adad97..ae7498401 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs @@ -56,4 +56,22 @@ public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue membe VectorSetSimilaritySearchRequest query, CommandFlags flags = CommandFlags.None) => Inner.VectorSetSimilaritySearchAsync(ToInner(key), query, flags); + + public Task?> VectorSetRangeAsync( + RedisKey key, + RedisValue start = default, + RedisValue end = default, + long count = -1, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRangeAsync(ToInner(key), start, end, count, exclude, flags); + + public System.Collections.Generic.IAsyncEnumerable VectorSetRangeEnumerateAsync( + RedisKey key, + RedisValue start = default, + RedisValue end = default, + long count = 100, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRangeEnumerateAsync(ToInner(key), start, end, count, exclude, flags); } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs index 62f4e9202..83fbb2f85 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs @@ -53,4 +53,22 @@ public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string a VectorSetSimilaritySearchRequest query, CommandFlags flags = CommandFlags.None) => Inner.VectorSetSimilaritySearch(ToInner(key), query, flags); + + public Lease VectorSetRange( + RedisKey key, + RedisValue start = default, + RedisValue end = default, + long count = -1, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRange(ToInner(key), start, end, count, exclude, flags); + + public System.Collections.Generic.IEnumerable VectorSetRangeEnumerate( + RedisKey key, + RedisValue start = default, + RedisValue end = default, + long count = 100, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) => + Inner.VectorSetRangeEnumerate(ToInner(key), start, end, count, exclude, flags); } diff --git a/src/StackExchange.Redis/Lease.cs b/src/StackExchange.Redis/Lease.cs index 91495dd08..a5a88e4eb 100644 --- a/src/StackExchange.Redis/Lease.cs +++ b/src/StackExchange.Redis/Lease.cs @@ -18,6 +18,11 @@ public sealed class Lease : IMemoryOwner private T[]? _arr; + /// + /// Gets whether this lease is empty. + /// + public bool IsEmpty => Length == 0; + /// /// The length of the lease. /// diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index ab058de62..b12b9f826 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ #nullable enable +StackExchange.Redis.Lease.IsEmpty.get -> bool +[SER001]StackExchange.Redis.IDatabase.VectorSetRange(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = -1, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease! +[SER001]StackExchange.Redis.IDatabase.VectorSetRangeEnumerate(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = 100, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRangeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = -1, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRangeEnumerateAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = 100, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! diff --git a/src/StackExchange.Redis/RedisDatabase.VectorSets.cs b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs index 9b3f1b43b..f10693dc5 100644 --- a/src/StackExchange.Redis/RedisDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/RedisDatabase.VectorSets.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; // ReSharper disable once CheckNamespace @@ -188,4 +191,107 @@ public Task VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue membe var msg = query.ToMessage(key, Database, flags); return ExecuteAsync(msg, msg.GetResultProcessor()); } + + private Message GetVectorSetRangeMessage( + in RedisKey key, + in RedisValue start, + in RedisValue end, + long count, + Exclude exclude, + CommandFlags flags) + { + static RedisValue GetTerminator(RedisValue value, Exclude exclude, bool isStart) + { + if (value.IsNull) return isStart ? RedisLiterals.MinusSymbol : RedisLiterals.PlusSymbol; + var mask = isStart ? Exclude.Start : Exclude.Stop; + var isExclusive = (exclude & mask) != 0; + return (isExclusive ? "(" : "[") + value; + } + + var from = GetTerminator(start, exclude, true); + var to = GetTerminator(end, exclude, false); + return count < 0 + ? Message.Create(Database, flags, RedisCommand.VRANGE, key, from, to) + : Message.Create(Database, flags, RedisCommand.VRANGE, key, from, to, count); + } + + public Lease VectorSetRange( + RedisKey key, + RedisValue start = default, + RedisValue end = default, + long count = -1, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) + { + var msg = GetVectorSetRangeMessage(key, start, end, count, exclude, flags); + return ExecuteSync(msg, ResultProcessor.LeaseRedisValue)!; + } + + public Task?> VectorSetRangeAsync( + RedisKey key, + RedisValue start = default, + RedisValue end = default, + long count = -1, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) + { + var msg = GetVectorSetRangeMessage(key, start, end, count, exclude, flags); + return ExecuteAsync(msg, ResultProcessor.LeaseRedisValue); + } + + public IEnumerable VectorSetRangeEnumerate( + RedisKey key, + RedisValue start = default, + RedisValue end = default, + long count = 100, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) + { + // intentionally not using "scan" naming in case a VSCAN command is added later + while (true) + { + using var batch = VectorSetRange(key, start, end, count, exclude, flags); + exclude |= Exclude.Start; // on subsequent iterations, exclude the start (we've already yielded it) + + if (batch is null || batch.IsEmpty) yield break; + var segment = batch.ArraySegment; + for (int i = 0; i < segment.Count; i++) + { + // note side effect: use the last value as the exclusive start of the next batch + yield return start = segment.Array![segment.Offset + i]; + } + if (batch.Length < count || (!end.IsNull && end == start)) yield break; // no need to issue a final query + } + } + + public IAsyncEnumerable VectorSetRangeEnumerateAsync( + RedisKey key, + RedisValue start = default, + RedisValue end = default, + long count = 100, + Exclude exclude = Exclude.None, + CommandFlags flags = CommandFlags.None) + { + // intentionally not using "scan" naming in case a VSCAN command is added later + return WithCancellationSupport(CancellationToken.None); + + async IAsyncEnumerable WithCancellationSupport([EnumeratorCancellation] CancellationToken cancellationToken) + { + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + using var batch = await VectorSetRangeAsync(key, start, end, count, exclude, flags); + exclude |= Exclude.Start; // on subsequent iterations, exclude the start (we've already yielded it) + + if (batch is null || batch.IsEmpty) yield break; + var segment = batch.ArraySegment; + for (int i = 0; i < segment.Count; i++) + { + // note side effect: use the last value as the exclusive start of the next batch + yield return start = segment.Array![segment.Offset + i]; + } + if (batch.Length < count || (!end.IsNull && end == start)) yield break; // no need to issue a final query + } + } + } } diff --git a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs index 8743ebd0b..b10e5fd93 100644 --- a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs +++ b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs @@ -11,6 +11,8 @@ internal abstract partial class ResultProcessor public static readonly ResultProcessor?> VectorSetLinks = new VectorSetLinksProcessor(); + public static readonly ResultProcessor?> LeaseRedisValue = new LeaseRedisValueProcessor(); + public static ResultProcessor VectorSetInfo = new VectorSetInfoProcessor(); private sealed class VectorSetLinksWithScoresProcessor : FlattenedLeaseProcessor @@ -43,6 +45,15 @@ protected override bool TryReadOne(in RawResult result, out RedisValue value) } } + private sealed class LeaseRedisValueProcessor : LeaseProcessor + { + protected override bool TryParse(in RawResult raw, out RedisValue parsed) + { + parsed = raw.AsRedisValue(); + return true; + } + } + private sealed partial class VectorSetInfoProcessor : ResultProcessor { protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result) diff --git a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs index 12eda7147..fb8e5d52a 100644 --- a/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs +++ b/tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using Xunit; @@ -672,4 +673,488 @@ public async Task VectorSetGetLinksWithScores() Assert.True(linksArray.First(l => l.Member == "element2").Score > 0.9); // similar Assert.True(linksArray.First(l => l.Member == "element3").Score < 0.8); // less-so } + + [Fact] + public async Task VectorSetRange_BasicOperation() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + // Add members with lexicographically ordered names + var vector = new[] { 1.0f, 2.0f, 3.0f }; + var members = new[] { "alpha", "beta", "delta", "gamma" }; // note: delta before gamma because lexicographical + + foreach (var member in members) + { + var request = VectorSetAddRequest.Member(member, vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Get all members - should be in lexicographical order + using var result = await db.VectorSetRangeAsync(key); + + Assert.NotNull(result); + Assert.Equal(4, result.Length); + // Lexicographical order: alpha, beta, delta, gamma + Assert.Equal(new[] { "alpha", "beta", "delta", "gamma" }, result.Span.ToArray().Select(r => (string?)r).ToArray()); + } + + [Fact] + public async Task VectorSetRange_WithStartAndEnd() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + var members = new[] { "apple", "banana", "cherry", "date", "elderberry" }; + + foreach (var member in members) + { + var request = VectorSetAddRequest.Member(member, vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Get range from "banana" to "date" (inclusive) + using var result = await db.VectorSetRangeAsync(key, start: "banana", end: "date"); + + Assert.NotNull(result); + Assert.Equal(3, result.Length); + Assert.Equal(new[] { "banana", "cherry", "date" }, result.Span.ToArray().Select(r => (string?)r).ToArray()); + } + + [Fact] + public async Task VectorSetRange_WithCount() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + + // Add 10 members + for (int i = 0; i < 10; i++) + { + var request = VectorSetAddRequest.Member($"member{i}", vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Get only 5 members + using var result = await db.VectorSetRangeAsync(key, count: 5); + + Assert.NotNull(result); + Assert.Equal(5, result.Length); + } + + [Fact] + public async Task VectorSetRange_WithExcludeStart() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + var members = new[] { "a", "b", "c", "d" }; + + foreach (var member in members) + { + var request = VectorSetAddRequest.Member(member, vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Get range excluding start + using var result = await db.VectorSetRangeAsync(key, start: "a", end: "d", exclude: Exclude.Start); + + Assert.NotNull(result); + Assert.Equal(3, result.Length); + Assert.Equal(new[] { "b", "c", "d" }, result.Span.ToArray().Select(r => (string?)r).ToArray()); + } + + [Fact] + public async Task VectorSetRange_WithExcludeEnd() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + var members = new[] { "a", "b", "c", "d" }; + + foreach (var member in members) + { + var request = VectorSetAddRequest.Member(member, vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Get range excluding end + using var result = await db.VectorSetRangeAsync(key, start: "a", end: "d", exclude: Exclude.Stop); + + Assert.NotNull(result); + Assert.Equal(3, result.Length); + Assert.Equal(new[] { "a", "b", "c" }, result.Span.ToArray().Select(r => (string?)r).ToArray()); + } + + [Fact] + public async Task VectorSetRange_WithExcludeBoth() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + var members = new[] { "a", "b", "c", "d", "e" }; + + foreach (var member in members) + { + var request = VectorSetAddRequest.Member(member, vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Get range excluding both boundaries + using var result = await db.VectorSetRangeAsync(key, start: "a", end: "e", exclude: Exclude.Both); + + Assert.NotNull(result); + Assert.Equal(3, result.Length); + Assert.Equal(new[] { "b", "c", "d" }, result.Span.ToArray().Select(r => (string?)r).ToArray()); + } + + [Fact] + public async Task VectorSetRange_EmptySet() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + // Don't add any members + using var result = await db.VectorSetRangeAsync(key); + + Assert.NotNull(result); + Assert.Empty(result.Span.ToArray()); + } + + [Fact] + public async Task VectorSetRange_NoMatches() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + var members = new[] { "a", "b", "c" }; + + foreach (var member in members) + { + var request = VectorSetAddRequest.Member(member, vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Query range with no matching members + using var result = await db.VectorSetRangeAsync(key, start: "x", end: "z"); + + Assert.NotNull(result); + Assert.Empty(result.Span.ToArray()); + } + + [Fact] + public async Task VectorSetRange_OpenStart() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + var members = new[] { "alpha", "beta", "gamma" }; + + foreach (var member in members) + { + var request = VectorSetAddRequest.Member(member, vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Get from beginning to "beta" + using var result = await db.VectorSetRangeAsync(key, end: "beta"); + + Assert.NotNull(result); + Assert.Equal(2, result.Length); + Assert.Equal(new[] { "alpha", "beta" }, result.Span.ToArray().Select(r => (string?)r).ToArray()); + } + + [Fact] + public async Task VectorSetRange_OpenEnd() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + var members = new[] { "alpha", "beta", "gamma" }; + + foreach (var member in members) + { + var request = VectorSetAddRequest.Member(member, vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Get from "beta" to end + using var result = await db.VectorSetRangeAsync(key, start: "beta"); + + Assert.NotNull(result); + Assert.Equal(2, result.Length); + Assert.Equal(new[] { "beta", "gamma" }, result.Span.ToArray().Select(r => (string?)r).ToArray()); + } + + [Fact] + public async Task VectorSetRange_SyncVsAsync() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + + // Add 20 members + for (int i = 0; i < 20; i++) + { + var request = VectorSetAddRequest.Member($"m{i:D2}", vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Call both sync and async + using var syncResult = db.VectorSetRange(key, start: "m05", end: "m15"); + using var asyncResult = await db.VectorSetRangeAsync(key, start: "m05", end: "m15"); + + Assert.NotNull(syncResult); + Assert.NotNull(asyncResult); + Assert.Equal(syncResult.Length, asyncResult.Length); + Assert.Equal(syncResult.Span.ToArray().Select(r => (string?)r), asyncResult.Span.ToArray().Select(r => (string?)r)); + } + + [Fact] + public async Task VectorSetRange_WithNumericLexOrder() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + var members = new[] { "1", "10", "2", "20", "3" }; + + foreach (var member in members) + { + var request = VectorSetAddRequest.Member(member, vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Get all - should be in lexicographical order, not numeric + using var result = await db.VectorSetRangeAsync(key); + + Assert.NotNull(result); + Assert.Equal(5, result.Length); + // Lexicographical order: "1", "10", "2", "20", "3" + Assert.Equal(new[] { "1", "10", "2", "20", "3" }, result.Span.ToArray().Select(r => (string?)r).ToArray()); + } + + [Fact] + public async Task VectorSetRangeEnumerate_BasicIteration() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + + // Add 50 members + for (int i = 0; i < 50; i++) + { + var request = VectorSetAddRequest.Member($"member{i:D3}", vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Enumerate with batch size of 10 + var allMembers = new System.Collections.Generic.List(); + foreach (var member in db.VectorSetRangeEnumerate(key, count: 10)) + { + allMembers.Add(member); + } + + Assert.Equal(50, allMembers.Count); + + // Verify lexicographical order + var sorted = allMembers.OrderBy(m => (string?)m, StringComparer.Ordinal).ToList(); + Assert.Equal(sorted, allMembers); + } + + [Fact] + public async Task VectorSetRangeEnumerate_WithRange() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + + // Add members "a" through "z" + for (char c = 'a'; c <= 'z'; c++) + { + var request = VectorSetAddRequest.Member(c.ToString(), vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Enumerate from "f" to "p" with batch size 5 + var allMembers = new System.Collections.Generic.List(); + foreach (var member in db.VectorSetRangeEnumerate(key, start: "f", end: "p", count: 5)) + { + allMembers.Add(member); + } + + // Should get "f" through "p" inclusive (11 members) + Assert.Equal(11, allMembers.Count); + Assert.Equal("f", (string?)allMembers.First()); + Assert.Equal("p", (string?)allMembers.Last()); + } + + [Fact] + public async Task VectorSetRangeEnumerate_EarlyBreak() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + + // Add 100 members + for (int i = 0; i < 100; i++) + { + var request = VectorSetAddRequest.Member($"member{i:D3}", vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Take only first 25 members + var limitedMembers = db.VectorSetRangeEnumerate(key, count: 10).Take(25).ToList(); + + Assert.Equal(25, limitedMembers.Count); + } + + [Fact] + public async Task VectorSetRangeEnumerate_EmptyBatches() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + // Don't add any members + var allMembers = new System.Collections.Generic.List(); + foreach (var member in db.VectorSetRangeEnumerate(key)) + { + allMembers.Add(member); + } + + Assert.Empty(allMembers); + } + + [Fact] + public async Task VectorSetRangeEnumerateAsync_BasicIteration() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + + // Add 50 members + for (int i = 0; i < 50; i++) + { + var request = VectorSetAddRequest.Member($"member{i:D3}", vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + // Enumerate with batch size of 10 + var allMembers = new System.Collections.Generic.List(); + await foreach (var member in db.VectorSetRangeEnumerateAsync(key, count: 10)) + { + allMembers.Add(member); + } + + Assert.Equal(50, allMembers.Count); + + // Verify lexicographical order + var sorted = allMembers.OrderBy(m => (string?)m, StringComparer.Ordinal).ToList(); + Assert.Equal(sorted, allMembers); + } + + [Fact] + public async Task VectorSetRangeEnumerateAsync_WithCancellation() + { + await using var conn = Create(require: RedisFeatures.v8_4_0_rc1); + var db = conn.GetDatabase(); + var key = Me(); + + await db.KeyDeleteAsync(key, CommandFlags.FireAndForget); + + var vector = new[] { 1.0f, 2.0f, 3.0f }; + + // Add 100 members + for (int i = 0; i < 100; i++) + { + var request = VectorSetAddRequest.Member($"member{i:D3}", vector.AsMemory()); + await db.VectorSetAddAsync(key, request); + } + + using var cts = new CancellationTokenSource(); + var allMembers = new System.Collections.Generic.List(); + + // Start enumeration and cancel after collecting some members + await Assert.ThrowsAnyAsync(async () => + { + await foreach (var member in db.VectorSetRangeEnumerateAsync(key, count: 10).WithCancellation(cts.Token)) + { + allMembers.Add(member); + + // Cancel after we've collected 25 members + if (allMembers.Count == 25) + { + cts.Cancel(); + } + } + }); + + // Should have stopped at or shortly after 25 members + Log($"Expected ~25 members, got {allMembers.Count}"); + Assert.True(allMembers.Count >= 25 && allMembers.Count <= 35, $"Expected ~25 members, got {allMembers.Count}"); + } } From 11af75bd39c3f77508de25b014abd827bab426e1 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 19 Feb 2026 15:08:06 +0000 Subject: [PATCH 420/435] release notes --- docs/ReleaseNotes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index e9c1e2e79..220f3cd0e 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,10 @@ Current package versions: ## 2.11.unreleased +## 2.11.3 + - Add support for `VRANGE` ([#3011 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3011)) +- Add defensive code in azure-maintenance-events handling ([#3013 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3013)) ## 2.11.0 From d4b2f398b7b7579557f6d650f527c4136a7d9cd6 Mon Sep 17 00:00:00 2001 From: Bar Shaul <88437685+barshaul@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:35:17 +0200 Subject: [PATCH 421/435] Handle MOVED error pointing to same endpoint. (#3003) * Handle MOVED error pointing to same endpoint by triggering reconnection before retrying the request. * Better stimulate proxy/LB * Increase timeout * Fixed key name to prevent collisions * Add NeedsReconnect flag to defer reconnection to reader loop for MOVED-to-same-endpoint * drop unstable test --------- Co-authored-by: Marc Gravell --- src/StackExchange.Redis/PhysicalBridge.cs | 17 +- src/StackExchange.Redis/PhysicalConnection.cs | 4 +- src/StackExchange.Redis/ResultProcessor.cs | 63 +++-- .../MovedTestServer.cs | 245 ++++++++++++++++++ .../MovedToSameEndpointTests.cs | 118 +++++++++ .../StackExchange.Redis.Tests.csproj | 1 + .../StackExchange.Redis.Tests/StreamTests.cs | 4 +- 7 files changed, 419 insertions(+), 33 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/MovedTestServer.cs create mode 100644 tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index b380c203e..9e5808009 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -48,6 +48,7 @@ internal sealed class PhysicalBridge : IDisposable private int failConnectCount = 0; private volatile bool isDisposed; private volatile bool shouldResetConnectionRetryCount; + private bool _needsReconnect; private long nonPreferredEndpointCount; // private volatile int missedHeartbeats; @@ -131,6 +132,16 @@ public enum State : byte private RedisProtocol _protocol; // note starts at zero, not RESP2 internal void SetProtocol(RedisProtocol protocol) => _protocol = protocol; + /// + /// Indicates whether the bridge needs to reconnect. + /// + internal bool NeedsReconnect => Volatile.Read(ref _needsReconnect); + + /// + /// Marks that the bridge needs to reconnect. + /// + internal void MarkNeedsReconnect() => Volatile.Write(ref _needsReconnect, true); + public void Dispose() { isDisposed = true; @@ -210,7 +221,7 @@ private WriteResult FailDueToNoConnection(Message message) public WriteResult TryWriteSync(Message message, bool isReplica) { if (isDisposed) throw new ObjectDisposedException(Name); - if (!IsConnected) return QueueOrFailMessage(message); + if (!IsConnected || NeedsReconnect) return QueueOrFailMessage(message); var physical = this.physical; if (physical == null) @@ -234,7 +245,7 @@ public WriteResult TryWriteSync(Message message, bool isReplica) public ValueTask TryWriteAsync(Message message, bool isReplica, bool bypassBacklog = false) { if (isDisposed) throw new ObjectDisposedException(Name); - if (!IsConnected && !bypassBacklog) return new ValueTask(QueueOrFailMessage(message)); + if ((!IsConnected || NeedsReconnect) && !bypassBacklog) return new ValueTask(QueueOrFailMessage(message)); var physical = this.physical; if (physical == null) @@ -1478,6 +1489,8 @@ private bool ChangeState(State oldState, State newState) Multiplexer.Trace("Connecting...", Name); if (ChangeState(State.Disconnected, State.Connecting)) { + // Clear the reconnect flag as we're starting a new connection + Volatile.Write(ref _needsReconnect, false); Interlocked.Increment(ref socketCount); Interlocked.Exchange(ref connectStartTicks, Environment.TickCount); // separate creation and connection for case when connection completes synchronously diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 857902f48..00c8e7541 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -2091,9 +2091,9 @@ private async Task ReadFromPipe() Trace($"Processed {handled} messages"); input.AdvanceTo(buffer.Start, buffer.End); - if (handled == 0 && readResult.IsCompleted) + if ((handled == 0 && readResult.IsCompleted) || BridgeCouldBeNull?.NeedsReconnect == true) { - break; // no more data, or trailing incomplete messages + break; // no more data, trailing incomplete messages, or reconnection required } } Trace("EOF"); diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 926fe8950..40c3bf8b6 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -259,43 +259,50 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in if (Format.TryParseInt32(parts[1], out int hashSlot) && Format.TryParseEndPoint(parts[2], out var endpoint)) { - // no point sending back to same server, and no point sending to a dead server - if (!Equals(server?.EndPoint, endpoint)) + // Check if MOVED points to same endpoint + bool isSameEndpoint = Equals(server?.EndPoint, endpoint); + if (isSameEndpoint && isMoved) { - if (bridge is null) - { - // already toast - } - else if (bridge.Multiplexer.TryResend(hashSlot, message, endpoint, isMoved)) + // MOVED to same endpoint detected. + // This occurs when Redis/Valkey servers are behind DNS records, load balancers, or proxies. + // The MOVED error signals that the client should reconnect to allow the DNS/proxy/load balancer + // to route the connection to a different underlying server host, then retry the command. + // Mark the bridge to reconnect - reader loop will handle disconnection and reconnection. + bridge?.MarkNeedsReconnect(); + } + if (bridge is null) + { + // already toast + } + else if (bridge.Multiplexer.TryResend(hashSlot, message, endpoint, isMoved)) + { + bridge.Multiplexer.Trace(message.Command + " re-issued to " + endpoint, isMoved ? "MOVED" : "ASK"); + return false; + } + else + { + if (isMoved && wasNoRedirect) { - bridge.Multiplexer.Trace(message.Command + " re-issued to " + endpoint, isMoved ? "MOVED" : "ASK"); - return false; + if (bridge.Multiplexer.RawConfig.IncludeDetailInExceptions) + { + err = $"Key has MOVED to Endpoint {endpoint} and hashslot {hashSlot} but CommandFlags.NoRedirect was specified - redirect not followed for {message.CommandAndKey}. "; + } + else + { + err = "Key has MOVED but CommandFlags.NoRedirect was specified - redirect not followed. "; + } } else { - if (isMoved && wasNoRedirect) + unableToConnectError = true; + if (bridge.Multiplexer.RawConfig.IncludeDetailInExceptions) { - if (bridge.Multiplexer.RawConfig.IncludeDetailInExceptions) - { - err = $"Key has MOVED to Endpoint {endpoint} and hashslot {hashSlot} but CommandFlags.NoRedirect was specified - redirect not followed for {message.CommandAndKey}. "; - } - else - { - err = "Key has MOVED but CommandFlags.NoRedirect was specified - redirect not followed. "; - } + err = $"Endpoint {endpoint} serving hashslot {hashSlot} is not reachable at this point of time. Please check connectTimeout value. If it is low, try increasing it to give the ConnectionMultiplexer a chance to recover from the network disconnect. " + + PerfCounterHelper.GetThreadPoolAndCPUSummary(); } else { - unableToConnectError = true; - if (bridge.Multiplexer.RawConfig.IncludeDetailInExceptions) - { - err = $"Endpoint {endpoint} serving hashslot {hashSlot} is not reachable at this point of time. Please check connectTimeout value. If it is low, try increasing it to give the ConnectionMultiplexer a chance to recover from the network disconnect. " - + PerfCounterHelper.GetThreadPoolAndCPUSummary(); - } - else - { - err = "Endpoint is not reachable at this point of time. Please check connectTimeout value. If it is low, try increasing it to give the ConnectionMultiplexer a chance to recover from the network disconnect. "; - } + err = "Endpoint is not reachable at this point of time. Please check connectTimeout value. If it is low, try increasing it to give the ConnectionMultiplexer a chance to recover from the network disconnect. "; } } } diff --git a/tests/StackExchange.Redis.Tests/MovedTestServer.cs b/tests/StackExchange.Redis.Tests/MovedTestServer.cs new file mode 100644 index 000000000..94202a1b2 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/MovedTestServer.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using StackExchange.Redis.Server; + +namespace StackExchange.Redis.Tests; + +/// +/// Test Redis/Valkey server that simulates MOVED errors pointing to the same endpoint. +/// Used to verify client reconnection behavior when the server is behind DNS/load balancers/proxies. +/// When a MOVED error points to the same endpoint, it signals the client to reconnect before retrying the command, +/// allowing the DNS record/proxy/load balancer to route the connection to a different underlying server host. +/// +public class MovedTestServer : MemoryCacheRedisServer +{ + /// + /// Represents the simulated server host state behind a proxy/load balancer. + /// + private enum SimulatedHost + { + /// + /// Old server that returns MOVED errors for the trigger key (pre-migration state). + /// + OldServer, + + /// + /// New server that handles requests normally (post-migration state). + /// + NewServer, + } + + private int _setCmdCount = 0; + private int _movedResponseCount = 0; + private int _connectionCount = 0; + private SimulatedHost _currentServerHost = SimulatedHost.OldServer; + private readonly ConcurrentDictionary _clientHostAssignments = new(); + private readonly Func _getEndpoint; + private readonly string _triggerKey; + private readonly int _hashSlot; + private EndPoint? _actualEndpoint; + + public MovedTestServer(Func getEndpoint, string triggerKey = "testkey", int hashSlot = 12345) + { + _getEndpoint = getEndpoint; + _triggerKey = triggerKey; + _hashSlot = hashSlot; + } + + /// + /// Called when a new client connection is established. + /// Assigns the client to the current server host state (simulating proxy/load balancer routing). + /// + public override RedisClient CreateClient() + { + var client = base.CreateClient(); + var assignedHost = _currentServerHost; + _clientHostAssignments[client] = assignedHost; + Interlocked.Increment(ref _connectionCount); + Log($"New client connection established (assigned to {assignedHost}, total connections: {_connectionCount}), endpoint: {_actualEndpoint}"); + return client; + } + + /// + /// Handles the INFO command, reporting cluster mode as enabled. + /// + protected override TypedRedisValue Info(RedisClient client, RedisRequest request) + { + // Override INFO to report cluster mode enabled + var section = request.Count >= 2 ? request.GetString(1) : null; + + // Return cluster-enabled info + var infoResponse = section?.Equals("CLUSTER", StringComparison.OrdinalIgnoreCase) == true + ? "# Cluster\r\ncluster_enabled:1\r\n" + : "# Server\r\nredis_version:7.0.0\r\n# Cluster\r\ncluster_enabled:1\r\n"; + + Log($"Returning INFO response (cluster_enabled:1), endpoint: {_actualEndpoint}"); + + return TypedRedisValue.BulkString(infoResponse); + } + + /// + /// Handles CLUSTER commands, supporting SLOTS and NODES subcommands for cluster mode simulation. + /// + protected override TypedRedisValue Cluster(RedisClient client, RedisRequest request) + { + if (request.Count < 2) + { + return TypedRedisValue.Error("ERR wrong number of arguments for 'cluster' command"); + } + + var subcommand = request.GetString(1); + + // Handle CLUSTER SLOTS command to support cluster mode + if (subcommand.Equals("SLOTS", StringComparison.OrdinalIgnoreCase)) + { + Log($"Returning CLUSTER SLOTS response, endpoint: {_actualEndpoint}"); + return GetClusterSlotsResponse(); + } + + // Handle CLUSTER NODES command + if (subcommand.Equals("NODES", StringComparison.OrdinalIgnoreCase)) + { + Log($"Returning CLUSTER NODES response, endpoint: {_actualEndpoint}"); + return GetClusterNodesResponse(); + } + + return TypedRedisValue.Error($"ERR Unknown CLUSTER subcommand '{subcommand}'"); + } + + /// + /// Handles SET commands. Returns MOVED error for the trigger key when requested by clients + /// connected to the old server, simulating a server migration behind a proxy/load balancer. + /// + protected override TypedRedisValue Set(RedisClient client, RedisRequest request) + { + var key = request.GetKey(1); + + // Increment SET command counter for every SET call + Interlocked.Increment(ref _setCmdCount); + + // Get the client's assigned server host + if (!_clientHostAssignments.TryGetValue(client, out var clientHost)) + { + throw new InvalidOperationException("Client host assignment not found - this indicates a test infrastructure error"); + } + + // Check if this is the trigger key from an old server client + if (key == _triggerKey && clientHost == SimulatedHost.OldServer) + { + // Transition server to new host (so future connections route to new server) + _currentServerHost = SimulatedHost.NewServer; + + Interlocked.Increment(ref _movedResponseCount); + var endpoint = _getEndpoint(); + Log($"Returning MOVED {_hashSlot} {endpoint} for key '{key}' from {clientHost} client, server transitioned to {SimulatedHost.NewServer}, actual endpoint: {_actualEndpoint}"); + + // Return MOVED error pointing to same endpoint + return TypedRedisValue.Error($"MOVED {_hashSlot} {endpoint}"); + } + + // Normal processing for new server clients or other keys + Log($"Processing SET normally for key '{key}' from {clientHost} client, endpoint: {_actualEndpoint}"); + return base.Set(client, request); + } + + /// + /// Returns a CLUSTER SLOTS response indicating this endpoint serves all slots (0-16383). + /// + private TypedRedisValue GetClusterSlotsResponse() + { + // Return a minimal CLUSTER SLOTS response indicating this endpoint serves all slots (0-16383) + // Format: Array of slot ranges, each containing: + // [start_slot, end_slot, [host, port, node_id]] + if (_actualEndpoint == null) + { + return TypedRedisValue.Error("ERR endpoint not set"); + } + + var endpoint = _getEndpoint(); + var parts = endpoint.Split(':'); + var host = parts.Length > 0 ? parts[0] : "127.0.0.1"; + var port = parts.Length > 1 ? parts[1] : "6379"; + + // Build response: [[0, 16383, [host, port, node-id]]] + // Inner array: [host, port, node-id] + var hostPortArray = TypedRedisValue.MultiBulk((ICollection)new[] + { + TypedRedisValue.BulkString(host), + TypedRedisValue.Integer(int.Parse(port)), + TypedRedisValue.BulkString("test-node-id"), + }); + // Slot range: [start_slot, end_slot, [host, port, node-id]] + var slotRange = TypedRedisValue.MultiBulk((ICollection)new[] + { + TypedRedisValue.Integer(0), // start slot + TypedRedisValue.Integer(16383), // end slot + hostPortArray, + }); + + // Outer array containing the single slot range + return TypedRedisValue.MultiBulk((ICollection)new[] { slotRange }); + } + + /// + /// Returns a CLUSTER NODES response. + /// + private TypedRedisValue GetClusterNodesResponse() + { + // Return CLUSTER NODES response + // Format: node-id host:port@cport flags master - ping-sent pong-recv config-epoch link-state slot-range + // Example: test-node-id 127.0.0.1:6379@16379 myself,master - 0 0 1 connected 0-16383 + if (_actualEndpoint == null) + { + return TypedRedisValue.Error("ERR endpoint not set"); + } + + var endpoint = _getEndpoint(); + var nodesInfo = $"test-node-id {endpoint}@1{endpoint.Split(':')[1]} myself,master - 0 0 1 connected 0-16383\r\n"; + + return TypedRedisValue.BulkString(nodesInfo); + } + + /// + /// Gets the number of SET commands executed. + /// + public int SetCmdCount => _setCmdCount; + + /// + /// Gets the number of times MOVED response was returned. + /// + public int MovedResponseCount => _movedResponseCount; + + /// + /// Gets the number of client connections established. + /// + public int ConnectionCount => _connectionCount; + + /// + /// Gets the actual endpoint the server is listening on. + /// + public EndPoint? ActualEndpoint => _actualEndpoint; + + /// + /// Sets the actual endpoint the server is listening on. + /// This should be called externally after the server starts. + /// + public void SetActualEndpoint(EndPoint endPoint) + { + _actualEndpoint = endPoint; + Log($"MovedTestServer endpoint set to {endPoint}"); + } + + /// + /// Resets all counters for test reusability. + /// + public void ResetCounters() + { + Interlocked.Exchange(ref _setCmdCount, 0); + Interlocked.Exchange(ref _movedResponseCount, 0); + Interlocked.Exchange(ref _connectionCount, 0); + } +} diff --git a/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs b/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs new file mode 100644 index 000000000..e047d2619 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs @@ -0,0 +1,118 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using StackExchange.Redis.Server; +using Xunit; + +namespace StackExchange.Redis.Tests; + +/// +/// Integration tests for MOVED-to-same-endpoint error handling. +/// When a MOVED error points to the same endpoint, the client should reconnect before retrying, +/// allowing the DNS record/proxy/load balancer to route to a different underlying server host. +/// +public class MovedToSameEndpointTests +{ + /// + /// Gets a free port by temporarily binding to port 0 and retrieving the OS-assigned port. + /// + private static int GetFreePort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + + /// + /// Integration test: Verifies that when a MOVED error points to the same endpoint, + /// the client reconnects and successfully retries the operation. + /// + /// Test scenario: + /// 1. Client connects to test server + /// 2. Client sends SET command for trigger key + /// 3. Server returns MOVED error pointing to same endpoint + /// 4. Client detects MOVED-to-same-endpoint and triggers reconnection + /// 5. Client retries SET command after reconnection + /// 6. Server processes SET normally on retry + /// + /// Expected behavior: + /// - SET command count should increase by 2 (initial attempt + retry) + /// - MOVED response count should increase by 1 (only on first attempt) + /// - Connection count should increase by 1 (reconnection after MOVED) + /// - Final SET operation should succeed with value stored + /// + [Fact] + public async Task MovedToSameEndpoint_TriggersReconnectAndRetry_CommandSucceeds() + { + var keyName = "MovedToSameEndpoint_TriggersReconnectAndRetry_CommandSucceeds"; + // Arrange: Get a free port to avoid conflicts when tests run in parallel + var port = GetFreePort(); + var listenEndpoint = new IPEndPoint(IPAddress.Loopback, port); + + var testServer = new MovedTestServer( + getEndpoint: () => Format.ToString(listenEndpoint), + triggerKey: keyName); + + var socketServer = new RespSocketServer(testServer); + + try + { + // Start listening on the free port + socketServer.Listen(listenEndpoint); + testServer.SetActualEndpoint(listenEndpoint); + + // Wait a moment for the server to fully start + await Task.Delay(100); + + // Act: Connect to the test server + var config = new ConfigurationOptions + { + EndPoints = { listenEndpoint }, + ConnectTimeout = 10000, + SyncTimeout = 5000, + AsyncTimeout = 5000, + }; + + await using var conn = await ConnectionMultiplexer.ConnectAsync(config); + // Ping the server to ensure it's responsive + var server = conn.GetServer(listenEndpoint); + await server.PingAsync(); + // Verify server is detected as cluster mode + Assert.Equal(ServerType.Cluster, server.ServerType); + var db = conn.GetDatabase(); + + // Record baseline counters after initial connection + var initialSetCmdCount = testServer.SetCmdCount; + var initialMovedResponseCount = testServer.MovedResponseCount; + var initialConnectionCount = testServer.ConnectionCount; + // Execute SET command: This should receive MOVED → reconnect → retry → succeed + var setResult = await db.StringSetAsync(keyName, "testvalue"); + + // Assert: Verify SET command succeeded + Assert.True(setResult, "SET command should return true (OK)"); + + // Verify the value was actually stored (proving retry succeeded) + var retrievedValue = await db.StringGetAsync(keyName); + Assert.Equal("testvalue", (string?)retrievedValue); + + // Verify SET command was executed twice: once with MOVED response, once successfully + var expectedSetCmdCount = initialSetCmdCount + 2; + Assert.Equal(expectedSetCmdCount, testServer.SetCmdCount); + + // Verify MOVED response was returned exactly once + var expectedMovedResponseCount = initialMovedResponseCount + 1; + Assert.Equal(expectedMovedResponseCount, testServer.MovedResponseCount); + + // Verify reconnection occurred: connection count should have increased by 1 + var expectedConnectionCount = initialConnectionCount + 1; + Assert.Equal(expectedConnectionCount, testServer.ConnectionCount); + } + finally + { + socketServer?.Dispose(); + } + } +} diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index e02a6ac36..4227fedc3 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -20,6 +20,7 @@ + diff --git a/tests/StackExchange.Redis.Tests/StreamTests.cs b/tests/StackExchange.Redis.Tests/StreamTests.cs index 22d2a0159..7e625d399 100644 --- a/tests/StackExchange.Redis.Tests/StreamTests.cs +++ b/tests/StackExchange.Redis.Tests/StreamTests.cs @@ -2308,7 +2308,9 @@ public void StreamTrimByMinId(StreamTrimMode mode) Assert.Equal(2, len); } - [Theory] +#pragma warning disable xUnit1004 + [Theory(Skip = "Flaky")] +#pragma warning restore xUnit1004 [InlineData(StreamTrimMode.KeepReferences)] [InlineData(StreamTrimMode.DeleteReferences)] [InlineData(StreamTrimMode.Acknowledged)] From aa25c4c076c89f4693b921df1bd8917a0bd60c86 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 24 Feb 2026 15:28:47 +0000 Subject: [PATCH 422/435] fix time conversion error in HOTKEYS (#3017) --- src/StackExchange.Redis/HotKeys.cs | 4 +- .../StackExchange.Redis.Tests/HotKeysTests.cs | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/StackExchange.Redis/HotKeys.cs b/src/StackExchange.Redis/HotKeys.cs index 270bcf9f7..28f3ddc56 100644 --- a/src/StackExchange.Redis/HotKeys.cs +++ b/src/StackExchange.Redis/HotKeys.cs @@ -178,10 +178,10 @@ public sealed partial class HotKeysResult ? null : NonNegativeMicroseconds(SampledCommandsSelectedSlotsMicroseconds); - private static TimeSpan NonNegativeMicroseconds(long us) + internal static TimeSpan NonNegativeMicroseconds(long us) { const long TICKS_PER_MICROSECOND = TimeSpan.TicksPerMillisecond / 1000; // 10, but: clearer - return TimeSpan.FromTicks(Math.Max(us, 0) / TICKS_PER_MICROSECOND); + return TimeSpan.FromTicks(Math.Max(us, 0) * TICKS_PER_MICROSECOND); } /// diff --git a/tests/StackExchange.Redis.Tests/HotKeysTests.cs b/tests/StackExchange.Redis.Tests/HotKeysTests.cs index 5e2daa6b3..b8c8c4847 100644 --- a/tests/StackExchange.Redis.Tests/HotKeysTests.cs +++ b/tests/StackExchange.Redis.Tests/HotKeysTests.cs @@ -323,4 +323,43 @@ public async Task SampleRatioUsageAsync() Assert.True(result.TotalNetworkBytes.HasValue); Assert.True(result.TotalCpuTime.HasValue); } + + [Fact] + public void NonNegativeMicroseconds_ConvertsCorrectly() + { + // Test case: 103 microseconds should convert to 103 microseconds in TimeSpan + // 103 microseconds = 103 * 10 ticks = 1030 ticks = 0.103 milliseconds + long inputMicroseconds = 103; + TimeSpan result = HotKeysResult.NonNegativeMicroseconds(inputMicroseconds); + + // Expected: 1030 ticks (103 microseconds = 0.103 milliseconds) + Assert.Equal(1030, result.Ticks); + Assert.Equal(0.103, result.TotalMilliseconds, precision: 10); + } + + [Fact] + public void NonNegativeMicroseconds_HandlesZero() + { + TimeSpan result = HotKeysResult.NonNegativeMicroseconds(0); + Assert.Equal(TimeSpan.Zero, result); + } + + [Fact] + public void NonNegativeMicroseconds_HandlesNegativeAsZero() + { + TimeSpan result = HotKeysResult.NonNegativeMicroseconds(-100); + Assert.Equal(TimeSpan.Zero, result); + } + + [Fact] + public void NonNegativeMicroseconds_HandlesLargeValues() + { + // 1 second = 1,000,000 microseconds = 10,000,000 ticks = 1000 milliseconds + long inputMicroseconds = 1_000_000; + TimeSpan result = HotKeysResult.NonNegativeMicroseconds(inputMicroseconds); + + Assert.Equal(10_000_000, result.Ticks); + Assert.Equal(1000.0, result.TotalMilliseconds, precision: 10); + Assert.Equal(1.0, result.TotalSeconds, precision: 10); + } } From 05c80ce86d84d8469344a6cebb8c2bfa82476cca Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 25 Feb 2026 10:26:54 +0000 Subject: [PATCH 423/435] doomed test, needs thought --- tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs b/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs index e047d2619..d1a10bf76 100644 --- a/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs +++ b/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs @@ -42,9 +42,9 @@ private static int GetFreePort() /// - SET command count should increase by 2 (initial attempt + retry) /// - MOVED response count should increase by 1 (only on first attempt) /// - Connection count should increase by 1 (reconnection after MOVED) - /// - Final SET operation should succeed with value stored + /// - Final SET operation should succeed with value stored. /// - [Fact] + [Fact(Skip = "dummy server is not a cluster!")] public async Task MovedToSameEndpoint_TriggersReconnectAndRetry_CommandSucceeds() { var keyName = "MovedToSameEndpoint_TriggersReconnectAndRetry_CommandSucceeds"; From 248ff150d2f10ca139e18f45a105032da13ea0f7 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 25 Feb 2026 11:08:44 +0000 Subject: [PATCH 424/435] stabilize -MOVED test (#3020) --- tests/StackExchange.Redis.Tests/MovedTestServer.cs | 2 +- .../StackExchange.Redis.Tests/MovedToSameEndpointTests.cs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/StackExchange.Redis.Tests/MovedTestServer.cs b/tests/StackExchange.Redis.Tests/MovedTestServer.cs index 94202a1b2..44df81f56 100644 --- a/tests/StackExchange.Redis.Tests/MovedTestServer.cs +++ b/tests/StackExchange.Redis.Tests/MovedTestServer.cs @@ -74,7 +74,7 @@ protected override TypedRedisValue Info(RedisClient client, RedisRequest request // Return cluster-enabled info var infoResponse = section?.Equals("CLUSTER", StringComparison.OrdinalIgnoreCase) == true ? "# Cluster\r\ncluster_enabled:1\r\n" - : "# Server\r\nredis_version:7.0.0\r\n# Cluster\r\ncluster_enabled:1\r\n"; + : "# Server\r\nredis_version:7.0.0\r\nredis_mode:cluster\r\n# Cluster\r\ncluster_enabled:1\r\n"; Log($"Returning INFO response (cluster_enabled:1), endpoint: {_actualEndpoint}"); diff --git a/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs b/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs index d1a10bf76..4d437fbae 100644 --- a/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs +++ b/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using StackExchange.Redis.Server; using Xunit; +using Xunit.Sdk; namespace StackExchange.Redis.Tests; @@ -12,7 +13,7 @@ namespace StackExchange.Redis.Tests; /// When a MOVED error points to the same endpoint, the client should reconnect before retrying, /// allowing the DNS record/proxy/load balancer to route to a different underlying server host. /// -public class MovedToSameEndpointTests +public class MovedToSameEndpointTests(ITestOutputHelper log) { /// /// Gets a free port by temporarily binding to port 0 and retrieving the OS-assigned port. @@ -44,7 +45,7 @@ private static int GetFreePort() /// - Connection count should increase by 1 (reconnection after MOVED) /// - Final SET operation should succeed with value stored. /// - [Fact(Skip = "dummy server is not a cluster!")] + [Fact] public async Task MovedToSameEndpoint_TriggersReconnectAndRetry_CommandSucceeds() { var keyName = "MovedToSameEndpoint_TriggersReconnectAndRetry_CommandSucceeds"; @@ -74,11 +75,13 @@ public async Task MovedToSameEndpoint_TriggersReconnectAndRetry_CommandSucceeds( ConnectTimeout = 10000, SyncTimeout = 5000, AsyncTimeout = 5000, + AllowAdmin = true, }; await using var conn = await ConnectionMultiplexer.ConnectAsync(config); // Ping the server to ensure it's responsive var server = conn.GetServer(listenEndpoint); + log?.WriteLine($"info: {await server.InfoRawAsync()}"); await server.PingAsync(); // Verify server is detected as cluster mode Assert.Equal(ServerType.Cluster, server.ServerType); From b8ebdf7d9c3bbde2299c5fa8dd94aa8cbaaa4f50 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 25 Feb 2026 11:43:34 +0000 Subject: [PATCH 425/435] release notes and ship-files for 2.8.11 --- docs/ReleaseNotes.md | 8 +++++++- src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt | 5 +++++ src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt | 5 ----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 220f3cd0e..69706f09a 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,7 +6,13 @@ Current package versions: | ------------ | ----------------- | ----- | | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | -## 2.11.unreleased +## 2.11.8 + +* Handle `-MOVED` error pointing to same endpoint. ([#3003 by @barshaul](https://github.com/StackExchange/StackExchange.Redis/pull/3003)) +* fix time conversion error in `HOTKEYS` ([#3017 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3017)) + +- Add support for `VRANGE` ([#3011 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3011)) +- Add defensive code in azure-maintenance-events handling ([#3013 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3013)) ## 2.11.3 diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index e97d9800a..b2f5cab2c 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -2270,3 +2270,8 @@ static StackExchange.Redis.RedisChannel.KeySpaceSingleKey(in StackExchange.Redis StackExchange.Redis.StreamInfo.EntriesAdded.get -> long StackExchange.Redis.StreamInfo.MaxDeletedEntryId.get -> StackExchange.Redis.RedisValue StackExchange.Redis.StreamInfo.RecordedFirstEntryId.get -> StackExchange.Redis.RedisValue +StackExchange.Redis.Lease.IsEmpty.get -> bool +[SER001]StackExchange.Redis.IDatabase.VectorSetRange(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = -1, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease! +[SER001]StackExchange.Redis.IDatabase.VectorSetRangeEnumerate(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = 100, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRangeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = -1, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! +[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRangeEnumerateAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = 100, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index b12b9f826..ab058de62 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,6 +1 @@ #nullable enable -StackExchange.Redis.Lease.IsEmpty.get -> bool -[SER001]StackExchange.Redis.IDatabase.VectorSetRange(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = -1, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease! -[SER001]StackExchange.Redis.IDatabase.VectorSetRangeEnumerate(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = 100, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRangeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = -1, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! -[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRangeEnumerateAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = 100, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! From bcf41c29c197ab319b5b1617038e8f3b8fc82156 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 25 Feb 2026 14:40:03 +0000 Subject: [PATCH 426/435] add test infrastructure for rich in-proc dummy servers (#3021) * - add test infrastructure for rich in-proc dummy servers, for simulating connection state - migrate the -MOVED/self test to the new infrastructure, and use better server/cluster/etc impl * log client id --- .../InProcessTestServer.cs | 87 +++++++++++++ .../MovedTestServer.cs | 57 +++------ .../MovedToSameEndpointTests.cs | 120 +++++++----------- .../StackExchange.Redis.Server/RedisClient.cs | 2 +- .../StackExchange.Redis.Server/RedisServer.cs | 32 ++++- toys/StackExchange.Redis.Server/RespServer.cs | 46 ++++--- 6 files changed, 209 insertions(+), 135 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/InProcessTestServer.cs diff --git a/tests/StackExchange.Redis.Tests/InProcessTestServer.cs b/tests/StackExchange.Redis.Tests/InProcessTestServer.cs new file mode 100644 index 000000000..29955e7a7 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/InProcessTestServer.cs @@ -0,0 +1,87 @@ +using System; +using System.IO; +using System.IO.Pipelines; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Pipelines.Sockets.Unofficial; +using StackExchange.Redis.Configuration; +using StackExchange.Redis.Server; +using Xunit; + +namespace StackExchange.Redis.Tests; + +public class InProcessTestServer : MemoryCacheRedisServer +{ + public Tunnel Tunnel { get; } + + private readonly ITestOutputHelper? _log; + public InProcessTestServer(ITestOutputHelper? log = null) + { + _log = log; + // ReSharper disable once VirtualMemberCallInConstructor + _log?.WriteLine($"Creating in-process server: {ToString()}"); + Tunnel = new InProcTunnel(this); + } + + private sealed class InProcTunnel( + InProcessTestServer server, + PipeOptions? pipeOptions = null) : Tunnel + { + public override ValueTask GetSocketConnectEndpointAsync( + EndPoint endpoint, + CancellationToken cancellationToken) + { + // server._log?.WriteLine($"Disabling client creation, requested endpoint: {Format.ToString(endpoint)}"); + return default; + } + + public override ValueTask BeforeAuthenticateAsync( + EndPoint endpoint, + ConnectionType connectionType, + Socket? socket, + CancellationToken cancellationToken) + { + server._log?.WriteLine($"Client intercepted, requested endpoint: {Format.ToString(endpoint)} for {connectionType} usage"); + var clientToServer = new Pipe(pipeOptions ?? PipeOptions.Default); + var serverToClient = new Pipe(pipeOptions ?? PipeOptions.Default); + var serverSide = new Duplex(clientToServer.Reader, serverToClient.Writer); + _ = Task.Run(async () => await server.RunClientAsync(serverSide), cancellationToken); + var clientSide = StreamConnection.GetDuplex(serverToClient.Reader, clientToServer.Writer); + return new(clientSide); + } + + private sealed class Duplex(PipeReader input, PipeWriter output) : IDuplexPipe + { + public PipeReader Input => input; + public PipeWriter Output => output; + + public ValueTask Dispose() + { + input.Complete(); + output.Complete(); + return default; + } + } + } + /* + + private readonly RespServer _server; + public RespSocketServer(RespServer server) + { + _server = server ?? throw new ArgumentNullException(nameof(server)); + server.Shutdown.ContinueWith((_, o) => ((SocketServer)o).Dispose(), this); + } + protected override void OnStarted(EndPoint endPoint) + => _server.Log("Server is listening on " + endPoint); + + protected override Task OnClientConnectedAsync(in ClientConnection client) + => _server.RunClientAsync(client.Transport); + + protected override void Dispose(bool disposing) + { + if (disposing) _server.Dispose(); + } + */ +} diff --git a/tests/StackExchange.Redis.Tests/MovedTestServer.cs b/tests/StackExchange.Redis.Tests/MovedTestServer.cs index 44df81f56..3b572c3d1 100644 --- a/tests/StackExchange.Redis.Tests/MovedTestServer.cs +++ b/tests/StackExchange.Redis.Tests/MovedTestServer.cs @@ -2,9 +2,12 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; +using System.Net.Sockets; +using System.Text; using System.Threading; using System.Threading.Tasks; using StackExchange.Redis.Server; +using Xunit; namespace StackExchange.Redis.Tests; @@ -14,7 +17,7 @@ namespace StackExchange.Redis.Tests; /// When a MOVED error points to the same endpoint, it signals the client to reconnect before retrying the command, /// allowing the DNS record/proxy/load balancer to route the connection to a different underlying server host. /// -public class MovedTestServer : MemoryCacheRedisServer +public class MovedTestServer : InProcessTestServer { /// /// Represents the simulated server host state behind a proxy/load balancer. @@ -34,19 +37,26 @@ private enum SimulatedHost private int _setCmdCount = 0; private int _movedResponseCount = 0; - private int _connectionCount = 0; + private SimulatedHost _currentServerHost = SimulatedHost.OldServer; - private readonly ConcurrentDictionary _clientHostAssignments = new(); + private readonly Func _getEndpoint; private readonly string _triggerKey; private readonly int _hashSlot; private EndPoint? _actualEndpoint; - public MovedTestServer(Func getEndpoint, string triggerKey = "testkey", int hashSlot = 12345) + public MovedTestServer(Func getEndpoint, string triggerKey = "testkey", int hashSlot = 12345, ITestOutputHelper? log = null) : base(log) { _getEndpoint = getEndpoint; _triggerKey = triggerKey; _hashSlot = hashSlot; + ServerType = ServerType.Cluster; + RedisVersion = RedisFeatures.v7_2_0_rc1; + } + + private sealed class MovedTestClient(SimulatedHost assignedHost) : RedisClient + { + public SimulatedHost AssignedHost => assignedHost; } /// @@ -55,32 +65,11 @@ public MovedTestServer(Func getEndpoint, string triggerKey = "testkey", /// public override RedisClient CreateClient() { - var client = base.CreateClient(); - var assignedHost = _currentServerHost; - _clientHostAssignments[client] = assignedHost; - Interlocked.Increment(ref _connectionCount); - Log($"New client connection established (assigned to {assignedHost}, total connections: {_connectionCount}), endpoint: {_actualEndpoint}"); + var client = new MovedTestClient(_currentServerHost); + Log($"New client connection established (assigned to {client.AssignedHost}, total connections: {TotalClientCount}), endpoint: {_actualEndpoint}"); return client; } - /// - /// Handles the INFO command, reporting cluster mode as enabled. - /// - protected override TypedRedisValue Info(RedisClient client, RedisRequest request) - { - // Override INFO to report cluster mode enabled - var section = request.Count >= 2 ? request.GetString(1) : null; - - // Return cluster-enabled info - var infoResponse = section?.Equals("CLUSTER", StringComparison.OrdinalIgnoreCase) == true - ? "# Cluster\r\ncluster_enabled:1\r\n" - : "# Server\r\nredis_version:7.0.0\r\nredis_mode:cluster\r\n# Cluster\r\ncluster_enabled:1\r\n"; - - Log($"Returning INFO response (cluster_enabled:1), endpoint: {_actualEndpoint}"); - - return TypedRedisValue.BulkString(infoResponse); - } - /// /// Handles CLUSTER commands, supporting SLOTS and NODES subcommands for cluster mode simulation. /// @@ -122,10 +111,11 @@ protected override TypedRedisValue Set(RedisClient client, RedisRequest request) Interlocked.Increment(ref _setCmdCount); // Get the client's assigned server host - if (!_clientHostAssignments.TryGetValue(client, out var clientHost)) + if (client is not MovedTestClient movedClient) { - throw new InvalidOperationException("Client host assignment not found - this indicates a test infrastructure error"); + throw new InvalidOperationException($"Client is not a {nameof(MovedTestClient)}"); } + var clientHost = movedClient.AssignedHost; // Check if this is the trigger key from an old server client if (key == _triggerKey && clientHost == SimulatedHost.OldServer) @@ -213,11 +203,6 @@ private TypedRedisValue GetClusterNodesResponse() /// public int MovedResponseCount => _movedResponseCount; - /// - /// Gets the number of client connections established. - /// - public int ConnectionCount => _connectionCount; - /// /// Gets the actual endpoint the server is listening on. /// @@ -236,10 +221,10 @@ public void SetActualEndpoint(EndPoint endPoint) /// /// Resets all counters for test reusability. /// - public void ResetCounters() + public override void ResetCounters() { Interlocked.Exchange(ref _setCmdCount, 0); Interlocked.Exchange(ref _movedResponseCount, 0); - Interlocked.Exchange(ref _connectionCount, 0); + base.ResetCounters(); } } diff --git a/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs b/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs index 4d437fbae..42f8f2dea 100644 --- a/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs +++ b/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs @@ -1,10 +1,6 @@ -using System; using System.Net; -using System.Net.Sockets; using System.Threading.Tasks; -using StackExchange.Redis.Server; using Xunit; -using Xunit.Sdk; namespace StackExchange.Redis.Tests; @@ -15,18 +11,6 @@ namespace StackExchange.Redis.Tests; /// public class MovedToSameEndpointTests(ITestOutputHelper log) { - /// - /// Gets a free port by temporarily binding to port 0 and retrieving the OS-assigned port. - /// - private static int GetFreePort() - { - var listener = new TcpListener(IPAddress.Loopback, 0); - listener.Start(); - var port = ((IPEndPoint)listener.LocalEndpoint).Port; - listener.Stop(); - return port; - } - /// /// Integration test: Verifies that when a MOVED error points to the same endpoint, /// the client reconnects and successfully retries the operation. @@ -49,73 +33,67 @@ private static int GetFreePort() public async Task MovedToSameEndpoint_TriggersReconnectAndRetry_CommandSucceeds() { var keyName = "MovedToSameEndpoint_TriggersReconnectAndRetry_CommandSucceeds"; - // Arrange: Get a free port to avoid conflicts when tests run in parallel - var port = GetFreePort(); - var listenEndpoint = new IPEndPoint(IPAddress.Loopback, port); - var testServer = new MovedTestServer( + var listenEndpoint = new IPEndPoint(IPAddress.Loopback, 6382); + using var testServer = new MovedTestServer( getEndpoint: () => Format.ToString(listenEndpoint), - triggerKey: keyName); + triggerKey: keyName, + log: log); - var socketServer = new RespSocketServer(testServer); + testServer.SetActualEndpoint(listenEndpoint); - try - { - // Start listening on the free port - socketServer.Listen(listenEndpoint); - testServer.SetActualEndpoint(listenEndpoint); + // Wait a moment for the server to fully start + await Task.Delay(100); - // Wait a moment for the server to fully start - await Task.Delay(100); + // Act: Connect to the test server + var config = new ConfigurationOptions + { + EndPoints = { listenEndpoint }, + ConnectTimeout = 10000, + SyncTimeout = 5000, + AsyncTimeout = 5000, + AllowAdmin = true, + Tunnel = testServer.Tunnel, + }; - // Act: Connect to the test server - var config = new ConfigurationOptions - { - EndPoints = { listenEndpoint }, - ConnectTimeout = 10000, - SyncTimeout = 5000, - AsyncTimeout = 5000, - AllowAdmin = true, - }; + await using var conn = await ConnectionMultiplexer.ConnectAsync(config); + // Ping the server to ensure it's responsive + var server = conn.GetServer(listenEndpoint); + log?.WriteLine((await server.InfoRawAsync()) ?? ""); + var id = await server.ExecuteAsync("client", "id"); + log?.WriteLine($"client id: {id}"); - await using var conn = await ConnectionMultiplexer.ConnectAsync(config); - // Ping the server to ensure it's responsive - var server = conn.GetServer(listenEndpoint); - log?.WriteLine($"info: {await server.InfoRawAsync()}"); - await server.PingAsync(); - // Verify server is detected as cluster mode - Assert.Equal(ServerType.Cluster, server.ServerType); - var db = conn.GetDatabase(); + await server.PingAsync(); + // Verify server is detected as cluster mode + Assert.Equal(ServerType.Cluster, server.ServerType); + var db = conn.GetDatabase(); - // Record baseline counters after initial connection - var initialSetCmdCount = testServer.SetCmdCount; - var initialMovedResponseCount = testServer.MovedResponseCount; - var initialConnectionCount = testServer.ConnectionCount; - // Execute SET command: This should receive MOVED → reconnect → retry → succeed - var setResult = await db.StringSetAsync(keyName, "testvalue"); + // Record baseline counters after initial connection + var initialSetCmdCount = testServer.SetCmdCount; + var initialMovedResponseCount = testServer.MovedResponseCount; + var initialConnectionCount = testServer.TotalClientCount; + // Execute SET command: This should receive MOVED → reconnect → retry → succeed + var setResult = await db.StringSetAsync(keyName, "testvalue"); - // Assert: Verify SET command succeeded - Assert.True(setResult, "SET command should return true (OK)"); + // Assert: Verify SET command succeeded + Assert.True(setResult, "SET command should return true (OK)"); - // Verify the value was actually stored (proving retry succeeded) - var retrievedValue = await db.StringGetAsync(keyName); - Assert.Equal("testvalue", (string?)retrievedValue); + // Verify the value was actually stored (proving retry succeeded) + var retrievedValue = await db.StringGetAsync(keyName); + Assert.Equal("testvalue", (string?)retrievedValue); - // Verify SET command was executed twice: once with MOVED response, once successfully - var expectedSetCmdCount = initialSetCmdCount + 2; - Assert.Equal(expectedSetCmdCount, testServer.SetCmdCount); + // Verify SET command was executed twice: once with MOVED response, once successfully + var expectedSetCmdCount = initialSetCmdCount + 2; + Assert.Equal(expectedSetCmdCount, testServer.SetCmdCount); - // Verify MOVED response was returned exactly once - var expectedMovedResponseCount = initialMovedResponseCount + 1; - Assert.Equal(expectedMovedResponseCount, testServer.MovedResponseCount); + // Verify MOVED response was returned exactly once + var expectedMovedResponseCount = initialMovedResponseCount + 1; + Assert.Equal(expectedMovedResponseCount, testServer.MovedResponseCount); - // Verify reconnection occurred: connection count should have increased by 1 - var expectedConnectionCount = initialConnectionCount + 1; - Assert.Equal(expectedConnectionCount, testServer.ConnectionCount); - } - finally - { - socketServer?.Dispose(); - } + // Verify reconnection occurred: connection count should have increased by 1 + var expectedConnectionCount = initialConnectionCount + 1; + Assert.Equal(expectedConnectionCount, testServer.TotalClientCount); + id = await server.ExecuteAsync("client", "id"); + log?.WriteLine($"client id: {id}"); } } diff --git a/toys/StackExchange.Redis.Server/RedisClient.cs b/toys/StackExchange.Redis.Server/RedisClient.cs index bfe27b042..f27699039 100644 --- a/toys/StackExchange.Redis.Server/RedisClient.cs +++ b/toys/StackExchange.Redis.Server/RedisClient.cs @@ -4,7 +4,7 @@ namespace StackExchange.Redis.Server { - public sealed class RedisClient : IDisposable + public class RedisClient : IDisposable { internal int SkipReplies { get; set; } internal bool ShouldSkipResponse() diff --git a/toys/StackExchange.Redis.Server/RedisServer.cs b/toys/StackExchange.Redis.Server/RedisServer.cs index 52728fd44..98c50c953 100644 --- a/toys/StackExchange.Redis.Server/RedisServer.cs +++ b/toys/StackExchange.Redis.Server/RedisServer.cs @@ -109,6 +109,10 @@ protected virtual TypedRedisValue ClientReply(RedisClient client, RedisRequest r return TypedRedisValue.OK; } + [RedisCommand(2, "client", "id", LockFree = true)] + protected virtual TypedRedisValue ClientId(RedisClient client, RedisRequest request) + => TypedRedisValue.Integer(client.Id); + [RedisCommand(-1)] protected virtual TypedRedisValue Cluster(RedisClient client, RedisRequest request) => request.CommandNotFound(); @@ -346,6 +350,7 @@ bool IsMatch(string section) => string.IsNullOrWhiteSpace(selected) if (IsMatch("Persistence")) Info(sb, "Persistence"); if (IsMatch("Stats")) Info(sb, "Stats"); if (IsMatch("Replication")) Info(sb, "Replication"); + if (IsMatch("Cluster")) Info(sb, "Cluster"); if (IsMatch("Keyspace")) Info(sb, "Keyspace"); return sb.ToString(); } @@ -364,6 +369,12 @@ protected virtual TypedRedisValue Keys(RedisClient client, RedisRequest request) } protected virtual IEnumerable Keys(int database, RedisKey pattern) => throw new NotSupportedException(); + private static readonly Version s_DefaultServerVersion = new(1, 0, 0); + + public Version RedisVersion { get; set; } = s_DefaultServerVersion; + + public DateTime StartTime { get; set; } = DateTime.UtcNow; + public ServerType ServerType { get; set; } = ServerType.Standalone; protected virtual void Info(StringBuilder sb, string section) { StringBuilder AddHeader() @@ -375,14 +386,25 @@ StringBuilder AddHeader() switch (section) { case "Server": - AddHeader().AppendLine("redis_version:1.0") - .AppendLine("redis_mode:standalone") + var v = RedisVersion; + AddHeader().Append("redis_version:").Append(v.Major).Append('.').Append(v.Minor); + if (v.Revision >= 0) sb.Append('.').Append(v.Revision); + if (v.Build >= 0) sb.Append('.').Append(v.Build); + sb.AppendLine(); + sb.Append("redis_mode:").Append(ServerType switch { + ServerType.Cluster => "cluster", + ServerType.Sentinel => "sentinel", + _ => "standalone", + }).AppendLine() .Append("os:").Append(Environment.OSVersion).AppendLine() .Append("arch_bits:x").Append(IntPtr.Size * 8).AppendLine(); using (var process = Process.GetCurrentProcess()) { - sb.Append("process:").Append(process.Id).AppendLine(); + sb.Append("process_id:").Append(process.Id).AppendLine(); } + var time = DateTime.UtcNow - StartTime; + sb.Append("uptime_in_seconds:").Append((int)time.TotalSeconds).AppendLine(); + sb.Append("uptime_in_days:").Append((int)time.TotalDays).AppendLine(); // var port = TcpPort(); // if (port >= 0) sb.Append("tcp_port:").Append(port).AppendLine(); break; @@ -401,10 +423,14 @@ StringBuilder AddHeader() case "Replication": AddHeader().AppendLine("role:master"); break; + case "Cluster": + AddHeader().Append("cluster_enabled:").Append(ServerType is ServerType.Cluster ? 1 : 0).AppendLine(); + break; case "Keyspace": break; } } + [RedisCommand(2, "memory", "purge")] protected virtual TypedRedisValue MemoryPurge(RedisClient client, RedisRequest request) { diff --git a/toys/StackExchange.Redis.Server/RespServer.cs b/toys/StackExchange.Redis.Server/RespServer.cs index 75a0273ea..4a55c61c5 100644 --- a/toys/StackExchange.Redis.Server/RespServer.cs +++ b/toys/StackExchange.Redis.Server/RespServer.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.Pipelines; @@ -21,7 +22,6 @@ public enum ShutdownReason ClientInitiated, } - private readonly List _clients = new List(); private readonly TextWriter _output; protected RespServer(TextWriter output = null) @@ -193,32 +193,28 @@ internal int NetArity() // to be used via ListenForConnections public virtual RedisClient CreateClient() => new RedisClient(); - public int ClientCount - { - get { lock (_clients) { return _clients.Count; } } - } - public int TotalClientCount { get; private set; } - private int _nextId; + public int ClientCount => _clientLookup.Count; + public int TotalClientCount => _totalClientCount; + private int _nextId, _totalClientCount; public RedisClient AddClient() { var client = CreateClient(); - lock (_clients) - { - ThrowIfShutdown(); - client.Id = ++_nextId; - _clients.Add(client); - TotalClientCount++; - } + client.Id = Interlocked.Increment(ref _nextId); + Interlocked.Increment(ref _totalClientCount); + ThrowIfShutdown(); + _clientLookup[client.Id] = client; return client; } + + public bool TryGetClient(int id, out RedisClient client) => _clientLookup.TryGetValue(id, out client); + + private readonly ConcurrentDictionary _clientLookup = new(); + public bool RemoveClient(RedisClient client) { if (client == null) return false; - lock (_clients) - { - client.Closed = true; - return _clients.Remove(client); - } + client.Closed = true; + return _clientLookup.TryRemove(client.Id, out _); } private readonly TaskCompletionSource _shutdown = TaskSource.Create(null, TaskCreationOptions.RunContinuationsAsynchronously); @@ -232,11 +228,8 @@ protected void DoShutdown(ShutdownReason reason) if (_isShutdown) return; Log("Server shutting down..."); _isShutdown = true; - lock (_clients) - { - foreach (var client in _clients) client.Dispose(); - _clients.Clear(); - } + foreach (var client in _clientLookup.Values) client.Dispose(); + _clientLookup.Clear(); _shutdown.TrySetResult(reason); } public Task Shutdown => _shutdown.Task; @@ -413,6 +406,11 @@ static async ValueTask Awaited(ValueTask wwrite, TypedRedisValue rresponse public long TotalCommandsProcesed => _totalCommandsProcesed; public long TotalErrorCount => _totalErrorCount; + public virtual void ResetCounters() + { + _totalCommandsProcesed = _totalErrorCount = _totalClientCount = 0; + } + public TypedRedisValue Execute(RedisClient client, RedisRequest request) { if (request.Count == 0) return default; // not a request From 939cc85930febfa66b04a1d5b5611da904172ff2 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 26 Feb 2026 07:50:19 +0000 Subject: [PATCH 427/435] auth/hello --- .../StackExchange.Redis.Server/RedisClient.cs | 3 + .../RedisRequest.cs | 14 ++ .../StackExchange.Redis.Server/RedisServer.cs | 141 ++++++++++++++++-- toys/StackExchange.Redis.Server/RespServer.cs | 2 +- 4 files changed, 149 insertions(+), 11 deletions(-) diff --git a/toys/StackExchange.Redis.Server/RedisClient.cs b/toys/StackExchange.Redis.Server/RedisClient.cs index f27699039..1a49acc64 100644 --- a/toys/StackExchange.Redis.Server/RedisClient.cs +++ b/toys/StackExchange.Redis.Server/RedisClient.cs @@ -35,6 +35,9 @@ internal int Unsubscribe(RedisChannel channel) internal IDuplexPipe LinkedPipe { get; set; } public bool Closed { get; internal set; } public int Id { get; internal set; } + public bool IsAuthenticated { get; internal set; } + public RedisProtocol Protocol { get; internal set; } = RedisProtocol.Resp2; + public long ProtocolVersion => Protocol is RedisProtocol.Resp2 ? 2 : 3; public void Dispose() { diff --git a/toys/StackExchange.Redis.Server/RedisRequest.cs b/toys/StackExchange.Redis.Server/RedisRequest.cs index 36d133bab..3878398d9 100644 --- a/toys/StackExchange.Redis.Server/RedisRequest.cs +++ b/toys/StackExchange.Redis.Server/RedisRequest.cs @@ -41,6 +41,20 @@ public RedisValue GetValue(int index) public int GetInt32(int index) => (int)_inner[index].AsRedisValue(); + public bool TryGetInt64(int index, out long value) + => _inner[index].TryGetInt64(out value); + public bool TryGetInt32(int index, out int value) + { + if (_inner[index].TryGetInt64(out var tmp)) + { + value = (int)tmp; + if (value == tmp) return true; + } + + value = 0; + return false; + } + public long GetInt64(int index) => (long)_inner[index].AsRedisValue(); public RedisKey GetKey(int index) => _inner[index].AsRedisKey(); diff --git a/toys/StackExchange.Redis.Server/RedisServer.cs b/toys/StackExchange.Redis.Server/RedisServer.cs index 98c50c953..e07ead48d 100644 --- a/toys/StackExchange.Redis.Server/RedisServer.cs +++ b/toys/StackExchange.Redis.Server/RedisServer.cs @@ -14,6 +14,7 @@ public static bool IsMatch(string pattern, string key) => protected RedisServer(int databases = 16, TextWriter output = null) : base(output) { + RedisVersion = s_DefaultServerVersion; if (databases < 1) throw new ArgumentOutOfRangeException(nameof(databases)); Databases = databases; var config = ServerConfiguration; @@ -42,6 +43,107 @@ protected override void AppendStats(StringBuilder sb) } public int Databases { get; } + public string Password { get; set; } = ""; + + public override TypedRedisValue Execute(RedisClient client, RedisRequest request) + { + var pw = Password; + if (pw.Length != 0 & !client.IsAuthenticated) + { + if (!Literals.IsAuthCommand(in request)) return TypedRedisValue.Error("NOAUTH Authentication required."); + } + return base.Execute(client, request); + } + + internal class Literals + { + public static readonly CommandBytes + AUTH = new("AUTH"u8), + HELLO = new("HELLO"u8), + SETNAME = new("SETNAME"u8); + + public static bool IsAuthCommand(in RedisRequest request) => + request.Count != 0 && request.TryGetCommandBytes(0, out var command) + && (command.Equals(AUTH) || command.Equals(HELLO)); + } + + [RedisCommand(2)] + protected virtual TypedRedisValue Auth(RedisClient client, RedisRequest request) + { + if (request.GetString(1) == Password) + { + client.IsAuthenticated = true; + return TypedRedisValue.OK; + } + return TypedRedisValue.Error("ERR invalid password"); + } + + [RedisCommand(-1)] + protected virtual TypedRedisValue Hello(RedisClient client, RedisRequest request) + { + var protocol = client.Protocol; + bool isAuthed = client.IsAuthenticated; + string name = client.Name; + if (request.Count >= 2) + { + if (!request.TryGetInt32(1, out var protover)) return TypedRedisValue.Error("ERR Protocol version is not an integer or out of range"); + switch (protover) + { + case 2: + case 3: + protocol = RedisProtocol.Resp2; + break; + /* case 3: // this client does not currently support RESP3 + protocol = RedisProtocol.Resp3; + break; */ + default: + return TypedRedisValue.Error("NOPROTO unsupported protocol version"); + } + + for (int i = 2; i < request.Count && request.TryGetCommandBytes(i, out var key); i++) + { + int remaining = request.Count - (i + 2); + TypedRedisValue ArgFail() => TypedRedisValue.Error($"ERR Syntax error in HELLO option '{key.ToString().ToLower()}'\""); + if (key.Equals(Literals.AUTH)) + { + if (remaining < 2) return ArgFail(); + // ignore username for now + var pw = request.GetString(i + 2); + if (pw != Password) return TypedRedisValue.Error("WRONGPASS invalid username-password pair or user is disabled."); + isAuthed = true; + i += 2; + } + else if (key.Equals(Literals.SETNAME)) + { + if (remaining < 1) return ArgFail(); + name = request.GetString(++i); + } + } + } + + // all good, update client + client.Protocol = protocol; + client.IsAuthenticated = isAuthed; + client.Name = name; + + var reply = TypedRedisValue.Rent(14, out var span); + span[0] = TypedRedisValue.BulkString("server"); + span[1] = TypedRedisValue.BulkString("redis"); + span[2] = TypedRedisValue.BulkString("version"); + span[3] = TypedRedisValue.BulkString(VersionString); + span[4] = TypedRedisValue.BulkString("proto"); + span[5] = TypedRedisValue.Integer(client.ProtocolVersion); + span[6] = TypedRedisValue.BulkString("id"); + span[7] = TypedRedisValue.Integer(client.Id); + span[8] = TypedRedisValue.BulkString("mode"); + span[9] = TypedRedisValue.BulkString(ModeString); + span[10] = TypedRedisValue.BulkString("role"); + span[11] = TypedRedisValue.BulkString("master"); + span[12] = TypedRedisValue.BulkString("modules"); + span[13] = TypedRedisValue.EmptyArray; + return reply; + } + [RedisCommand(-3)] protected virtual TypedRedisValue Sadd(RedisClient client, RedisRequest request) { @@ -371,10 +473,36 @@ protected virtual TypedRedisValue Keys(RedisClient client, RedisRequest request) private static readonly Version s_DefaultServerVersion = new(1, 0, 0); - public Version RedisVersion { get; set; } = s_DefaultServerVersion; + private string _versionString; + private string VersionString => _versionString; + private static string FormatVersion(Version v) + { + var sb = new StringBuilder().Append(v.Major).Append('.').Append(v.Minor); + if (v.Revision >= 0) sb.Append('.').Append(v.Revision); + if (v.Build >= 0) sb.Append('.').Append(v.Build); + return sb.ToString(); + } + + public Version RedisVersion + { + get; + set + { + if (field == value) return; + field = value; + _versionString = FormatVersion(value); + } + } public DateTime StartTime { get; set; } = DateTime.UtcNow; public ServerType ServerType { get; set; } = ServerType.Standalone; + + private string ModeString => ServerType switch + { + ServerType.Cluster => "cluster", + ServerType.Sentinel => "sentinel", + _ => "standalone", + }; protected virtual void Info(StringBuilder sb, string section) { StringBuilder AddHeader() @@ -387,15 +515,8 @@ StringBuilder AddHeader() { case "Server": var v = RedisVersion; - AddHeader().Append("redis_version:").Append(v.Major).Append('.').Append(v.Minor); - if (v.Revision >= 0) sb.Append('.').Append(v.Revision); - if (v.Build >= 0) sb.Append('.').Append(v.Build); - sb.AppendLine(); - sb.Append("redis_mode:").Append(ServerType switch { - ServerType.Cluster => "cluster", - ServerType.Sentinel => "sentinel", - _ => "standalone", - }).AppendLine() + AddHeader().Append("redis_version:").AppendLine(VersionString) + .Append("redis_mode:").Append(ModeString).AppendLine() .Append("os:").Append(Environment.OSVersion).AppendLine() .Append("arch_bits:x").Append(IntPtr.Size * 8).AppendLine(); using (var process = Process.GetCurrentProcess()) diff --git a/toys/StackExchange.Redis.Server/RespServer.cs b/toys/StackExchange.Redis.Server/RespServer.cs index 4a55c61c5..068154b74 100644 --- a/toys/StackExchange.Redis.Server/RespServer.cs +++ b/toys/StackExchange.Redis.Server/RespServer.cs @@ -411,7 +411,7 @@ public virtual void ResetCounters() _totalCommandsProcesed = _totalErrorCount = _totalClientCount = 0; } - public TypedRedisValue Execute(RedisClient client, RedisRequest request) + public virtual TypedRedisValue Execute(RedisClient client, RedisRequest request) { if (request.Count == 0) return default; // not a request From ac7ab7f655833de6c0e68d782c9dac2f2a37fa23 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 27 Feb 2026 13:08:26 +0000 Subject: [PATCH 428/435] More powerful virtual test server for unit tests (#3022) * wip * Multi-node test infrastructure * respect CROSSSLOT in the toy server * extensively use "in" in the toy server; lots of stack values * implement decrby (for desting from CI) * implement basic TTL to support basic-ops test in primary tests * basic resp3 * - simplify connecting to the test server - include all commands and endpoints when connecting to the test server - better RESP3 aggregate support * run basic tests on RESP2+3 * Fix remaining RESP3 handshake snafus * test fixture for in-proc; run with basic tests * implement everything in multi/exec except the serialize/deserialize (d'oh!) * also force attributes to be pairs (same semantics as maps) * don't buffer any of the transaction commands --- .../ClusterConfiguration.cs | 2 + .../ConnectionMultiplexer.cs | 4 +- src/StackExchange.Redis/PhysicalConnection.cs | 24 + src/StackExchange.Redis/ResultProcessor.cs | 2 +- .../ServerSelectionStrategy.cs | 28 +- .../StackExchange.Redis.Tests/BasicOpTests.cs | 19 +- .../Helpers/InProcServerFixture.cs | 27 + .../HighIntegrityBasicOpsTests.cs | 5 + .../InProcessTestServer.cs | 95 ++- .../MovedTestServer.cs | 159 +--- .../MovedToSameEndpointTests.cs | 99 --- .../MovedUnitTests.cs | 148 ++++ tests/StackExchange.Redis.Tests/TestBase.cs | 40 +- toys/KestrelRedisServer/Program.cs | 34 +- .../RedisConnectionHandler.cs | 17 +- .../MemoryCacheRedisServer.cs | 165 +++- .../StackExchange.Redis.Server/RedisClient.cs | 211 ++++- .../RedisRequest.cs | 23 +- .../StackExchange.Redis.Server/RedisServer.cs | 782 +++++++++++++++--- toys/StackExchange.Redis.Server/RespServer.cs | 254 ++++-- .../TypedRedisValue.cs | 37 +- 21 files changed, 1683 insertions(+), 492 deletions(-) create mode 100644 tests/StackExchange.Redis.Tests/Helpers/InProcServerFixture.cs delete mode 100644 tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs create mode 100644 tests/StackExchange.Redis.Tests/MovedUnitTests.cs diff --git a/src/StackExchange.Redis/ClusterConfiguration.cs b/src/StackExchange.Redis/ClusterConfiguration.cs index 60e606ce2..084f7c639 100644 --- a/src/StackExchange.Redis/ClusterConfiguration.cs +++ b/src/StackExchange.Redis/ClusterConfiguration.cs @@ -45,6 +45,8 @@ private SlotRange(short from, short to) /// public int To => to; + internal bool IsSingleSlot => From == To; + internal const int MinSlot = 0, MaxSlot = 16383; private static SlotRange[]? s_SharedAllSlots; diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 0c6148923..e19de6d52 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -356,7 +356,7 @@ internal void CheckMessage(Message message) } } - internal bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isMoved) + internal bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isMoved, bool isSelf) { // If we're being told to re-send something because the hash slot moved, that means our topology is out of date // ...and we should re-evaluate what's what. @@ -367,7 +367,7 @@ internal bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool i ReconfigureIfNeeded(endpoint, false, "MOVED encountered"); } - return ServerSelectionStrategy.TryResend(hashSlot, message, endpoint, isMoved); + return ServerSelectionStrategy.TryResend(hashSlot, message, endpoint, isMoved, isSelf); } /// diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 00c8e7541..1af5589cf 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -925,6 +925,30 @@ internal static void WriteMultiBulkHeader(PipeWriter output, long count) output.Advance(offset); } + internal static void WriteMultiBulkHeader(PipeWriter output, long count, ResultType type) + { + // *{count}\r\n = 3 + MaxInt32TextLen + var span = output.GetSpan(3 + Format.MaxInt32TextLen); + span[0] = type switch + { + ResultType.Push => (byte)'>', + ResultType.Attribute => (byte)'|', + ResultType.Map => (byte)'%', + ResultType.Set => (byte)'~', + _ => (byte)'*', + }; + if ((type is ResultType.Map or ResultType.Attribute) & count > 0) + { + if ((count & 1) != 0) Throw(type, count); + count >>= 1; + static void Throw(ResultType type, long count) => throw new ArgumentOutOfRangeException( + paramName: nameof(count), + message: $"{type} data must be in pairs; got {count}"); + } + int offset = WriteRaw(span, count, offset: 1); + output.Advance(offset); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int WriteCrlf(Span span, int offset) { diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 40c3bf8b6..0562d7a4c 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -274,7 +274,7 @@ public virtual bool SetResult(PhysicalConnection connection, Message message, in { // already toast } - else if (bridge.Multiplexer.TryResend(hashSlot, message, endpoint, isMoved)) + else if (bridge.Multiplexer.TryResend(hashSlot, message, endpoint, isMoved, isSameEndpoint)) { bridge.Multiplexer.Trace(message.Command + " re-issued to " + endpoint, isMoved ? "MOVED" : "ASK"); return false; diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index db729ba26..7d599724a 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -46,7 +46,7 @@ internal sealed class ServerSelectionStrategy 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, }; - private readonly ConnectionMultiplexer multiplexer; + private readonly ConnectionMultiplexer? multiplexer; private int anyStartOffset = SharedRandom.Next(); // initialize to a random value so routing isn't uniform #if NET6_0_OR_GREATER @@ -57,7 +57,7 @@ internal sealed class ServerSelectionStrategy private ServerEndPoint[]? map; - public ServerSelectionStrategy(ConnectionMultiplexer multiplexer) => this.multiplexer = multiplexer; + public ServerSelectionStrategy(ConnectionMultiplexer? multiplexer) => this.multiplexer = multiplexer; public ServerType ServerType { get; set; } = ServerType.Standalone; internal static int TotalSlots => RedisClusterSlotCount; @@ -96,6 +96,8 @@ public int HashSlot(in RedisKey key) } } + private byte[] ChannelPrefix => multiplexer?.ChannelPrefix ?? []; + /// /// Computes the hash-slot that would be used by the given channel. /// @@ -106,7 +108,7 @@ public int HashSlot(in RedisChannel channel) ReadOnlySpan routingSpan = channel.RoutingSpan; byte[] prefix; - return channel.IgnoreChannelPrefix || (prefix = multiplexer.ChannelPrefix).Length == 0 + return channel.IgnoreChannelPrefix || (prefix = ChannelPrefix).Length == 0 ? GetClusterSlot(routingSpan) : GetClusterSlotWithPrefix(prefix, routingSpan); static int GetClusterSlotWithPrefix(byte[] prefixRaw, ReadOnlySpan routingSpan) @@ -133,15 +135,15 @@ static int GetClusterSlotWithPrefix(byte[] prefixRaw, ReadOnlySpan routing /// /// HASH_SLOT = CRC16(key) mod 16384. /// - private static unsafe int GetClusterSlot(ReadOnlySpan blob) + internal static unsafe int GetClusterSlot(ReadOnlySpan key) { unchecked { - fixed (byte* ptr = blob) + fixed (byte* ptr = key) { fixed (ushort* crc16tab = ServerSelectionStrategy.Crc16tab) { - int offset = 0, count = blob.Length, start, end; + int offset = 0, count = key.Length, start, end; if ((start = IndexOf(ptr, (byte)'{', 0, count - 1)) >= 0 && (end = IndexOf(ptr, (byte)'}', start + 1, count)) >= 0 && --end != start) @@ -169,7 +171,7 @@ private static unsafe int GetClusterSlot(ReadOnlySpan blob) // the same, so this does a pretty good job of spotting illegal commands before sending them case ServerType.Twemproxy: slot = message.GetHashSlot(this); - if (slot == MultipleSlots) throw ExceptionFactory.MultiSlot(multiplexer.RawConfig.IncludeDetailInExceptions, message); + if (slot == MultipleSlots) throw ExceptionFactory.MultiSlot(multiplexer?.RawConfig?.IncludeDetailInExceptions ?? false, message); break; /* just shown for completeness case ServerType.Standalone: // don't use sharding @@ -193,13 +195,13 @@ private static unsafe int GetClusterSlot(ReadOnlySpan blob) return Select(slot, command, flags, allowDisconnected); } - public bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isMoved) + public bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isMoved, bool isSelf) { try { - if (ServerType == ServerType.Standalone || hashSlot < 0 || hashSlot >= RedisClusterSlotCount) return false; + if ((ServerType == ServerType.Standalone && !isSelf) || hashSlot < 0 || hashSlot >= RedisClusterSlotCount) return false; - ServerEndPoint server = multiplexer.GetServerEndPoint(endpoint); + ServerEndPoint? server = multiplexer?.GetServerEndPoint(endpoint); if (server != null) { bool retry = false; @@ -230,7 +232,7 @@ public bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isM } if (resendVia == null) { - multiplexer.Trace("Unable to resend to " + endpoint); + multiplexer?.Trace("Unable to resend to " + endpoint); } else { @@ -248,7 +250,7 @@ public bool TryResend(int hashSlot, Message message, EndPoint endpoint, bool isM arr[hashSlot] = server; if (oldServer != server) { - multiplexer.OnHashSlotMoved(hashSlot, oldServer?.EndPoint, endpoint); + multiplexer?.OnHashSlotMoved(hashSlot, oldServer?.EndPoint, endpoint); } } @@ -305,7 +307,7 @@ private static unsafe int IndexOf(byte* ptr, byte value, int start, int end) } private ServerEndPoint? Any(RedisCommand command, CommandFlags flags, bool allowDisconnected) => - multiplexer.AnyServer(ServerType, (uint)Interlocked.Increment(ref anyStartOffset), command, flags, allowDisconnected); + multiplexer?.AnyServer(ServerType, (uint)Interlocked.Increment(ref anyStartOffset), command, flags, allowDisconnected); private static ServerEndPoint? FindPrimary(ServerEndPoint endpoint, RedisCommand command) { diff --git a/tests/StackExchange.Redis.Tests/BasicOpTests.cs b/tests/StackExchange.Redis.Tests/BasicOpTests.cs index ad85f5a88..f7b0be324 100644 --- a/tests/StackExchange.Redis.Tests/BasicOpTests.cs +++ b/tests/StackExchange.Redis.Tests/BasicOpTests.cs @@ -6,7 +6,21 @@ namespace StackExchange.Redis.Tests; -public class BasicOpsTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) +[RunPerProtocol] +public class BasicOpsTests(ITestOutputHelper output, SharedConnectionFixture fixture) + : BasicOpsTestsBase(output, fixture, null) +{ +} + +[RunPerProtocol] +public class InProcBasicOpsTests(ITestOutputHelper output, InProcServerFixture fixture) + : BasicOpsTestsBase(output, null, fixture) +{ +} + +[RunPerProtocol] +public abstract class BasicOpsTestsBase(ITestOutputHelper output, SharedConnectionFixture? connection, InProcServerFixture? server) + : TestBase(output, connection, server) { [Fact] public async Task PingOnce() @@ -471,6 +485,7 @@ public async Task WrappedDatabasePrefixIntegration() public async Task TransactionSync() { await using var conn = Create(); + Assert.SkipUnless(conn.RawConfig.CommandMap.IsAvailable(RedisCommand.MULTI), "MULTI is not available"); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -490,6 +505,8 @@ public async Task TransactionSync() public async Task TransactionAsync() { await using var conn = Create(); + Assert.SkipUnless(conn.RawConfig.CommandMap.IsAvailable(RedisCommand.MULTI), "MULTI is not available"); + var db = conn.GetDatabase(); RedisKey key = Me(); diff --git a/tests/StackExchange.Redis.Tests/Helpers/InProcServerFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/InProcServerFixture.cs new file mode 100644 index 000000000..5e801e5ca --- /dev/null +++ b/tests/StackExchange.Redis.Tests/Helpers/InProcServerFixture.cs @@ -0,0 +1,27 @@ +using System; +using StackExchange.Redis.Configuration; +using Xunit; + +[assembly: AssemblyFixture(typeof(StackExchange.Redis.Tests.InProcServerFixture))] + +// ReSharper disable once CheckNamespace +namespace StackExchange.Redis.Tests; + +public class InProcServerFixture : IDisposable +{ + private readonly InProcessTestServer _server = new(); + private readonly ConfigurationOptions _config; + public InProcServerFixture() + { + _config = _server.GetClientConfig(); + Configuration = _config.ToString(); + } + + public ConfigurationOptions Config => _config; + + public string Configuration { get; } + + public Tunnel? Tunnel => _server.Tunnel; + + public void Dispose() => _server.Dispose(); +} diff --git a/tests/StackExchange.Redis.Tests/HighIntegrityBasicOpsTests.cs b/tests/StackExchange.Redis.Tests/HighIntegrityBasicOpsTests.cs index d7b85cd62..bedc1bf11 100644 --- a/tests/StackExchange.Redis.Tests/HighIntegrityBasicOpsTests.cs +++ b/tests/StackExchange.Redis.Tests/HighIntegrityBasicOpsTests.cs @@ -6,3 +6,8 @@ public class HighIntegrityBasicOpsTests(ITestOutputHelper output, SharedConnecti { internal override bool HighIntegrity => true; } + +public class InProcHighIntegrityBasicOpsTests(ITestOutputHelper output, InProcServerFixture fixture) : InProcBasicOpsTests(output, fixture) +{ + internal override bool HighIntegrity => true; +} diff --git a/tests/StackExchange.Redis.Tests/InProcessTestServer.cs b/tests/StackExchange.Redis.Tests/InProcessTestServer.cs index 29955e7a7..5ac222c0a 100644 --- a/tests/StackExchange.Redis.Tests/InProcessTestServer.cs +++ b/tests/StackExchange.Redis.Tests/InProcessTestServer.cs @@ -3,6 +3,7 @@ using System.IO.Pipelines; using System.Net; using System.Net.Sockets; +using System.Text; using System.Threading; using System.Threading.Tasks; using Pipelines.Sockets.Unofficial; @@ -14,17 +15,79 @@ namespace StackExchange.Redis.Tests; public class InProcessTestServer : MemoryCacheRedisServer { - public Tunnel Tunnel { get; } - private readonly ITestOutputHelper? _log; public InProcessTestServer(ITestOutputHelper? log = null) { + RedisVersion = RedisFeatures.v6_0_0; // for client to expect RESP3 _log = log; // ReSharper disable once VirtualMemberCallInConstructor _log?.WriteLine($"Creating in-process server: {ToString()}"); Tunnel = new InProcTunnel(this); } + public Task ConnectAsync(bool withPubSub = false, TextWriter? log = null) + => ConnectionMultiplexer.ConnectAsync(GetClientConfig(withPubSub), log); + + public ConfigurationOptions GetClientConfig(bool withPubSub = false) + { + var commands = GetCommands(); + if (!withPubSub) + { + commands.Remove(nameof(RedisCommand.SUBSCRIBE)); + commands.Remove(nameof(RedisCommand.PSUBSCRIBE)); + commands.Remove(nameof(RedisCommand.SSUBSCRIBE)); + commands.Remove(nameof(RedisCommand.UNSUBSCRIBE)); + commands.Remove(nameof(RedisCommand.PUNSUBSCRIBE)); + commands.Remove(nameof(RedisCommand.SUNSUBSCRIBE)); + commands.Remove(nameof(RedisCommand.PUBLISH)); + commands.Remove(nameof(RedisCommand.SPUBLISH)); + } + // transactions don't work yet + commands.Remove(nameof(RedisCommand.MULTI)); + commands.Remove(nameof(RedisCommand.EXEC)); + commands.Remove(nameof(RedisCommand.DISCARD)); + commands.Remove(nameof(RedisCommand.WATCH)); + commands.Remove(nameof(RedisCommand.UNWATCH)); + + var config = new ConfigurationOptions + { + CommandMap = CommandMap.Create(commands), + ConfigurationChannel = "", + TieBreaker = "", + DefaultVersion = RedisVersion, + ConnectTimeout = 10000, + SyncTimeout = 5000, + AsyncTimeout = 5000, + AllowAdmin = true, + Tunnel = Tunnel, + }; + foreach (var endpoint in GetEndPoints()) + { + config.EndPoints.Add(endpoint); + } + return config; + } + + public Tunnel Tunnel { get; } + + public override void Log(string message) + { + _log?.WriteLine(message); + base.Log(message); + } + + protected override void OnMoved(RedisClient client, int hashSlot, Node node) + { + _log?.WriteLine($"Client {client.Id} being redirected: {hashSlot} to {node}"); + base.OnMoved(client, hashSlot, node); + } + + public override TypedRedisValue OnUnknownCommand(in RedisClient client, in RedisRequest request, ReadOnlySpan command) + { + _log?.WriteLine($"[{client.Id}] unknown command: {Encoding.ASCII.GetString(command)}"); + return base.OnUnknownCommand(in client, in request, command); + } + private sealed class InProcTunnel( InProcessTestServer server, PipeOptions? pipeOptions = null) : Tunnel @@ -33,8 +96,12 @@ private sealed class InProcTunnel( EndPoint endpoint, CancellationToken cancellationToken) { - // server._log?.WriteLine($"Disabling client creation, requested endpoint: {Format.ToString(endpoint)}"); - return default; + if (server.TryGetNode(endpoint, out _)) + { + // server._log?.WriteLine($"Disabling client creation, requested endpoint: {Format.ToString(endpoint)}"); + return default; + } + return base.GetSocketConnectEndpointAsync(endpoint, cancellationToken); } public override ValueTask BeforeAuthenticateAsync( @@ -43,13 +110,18 @@ private sealed class InProcTunnel( Socket? socket, CancellationToken cancellationToken) { - server._log?.WriteLine($"Client intercepted, requested endpoint: {Format.ToString(endpoint)} for {connectionType} usage"); - var clientToServer = new Pipe(pipeOptions ?? PipeOptions.Default); - var serverToClient = new Pipe(pipeOptions ?? PipeOptions.Default); - var serverSide = new Duplex(clientToServer.Reader, serverToClient.Writer); - _ = Task.Run(async () => await server.RunClientAsync(serverSide), cancellationToken); - var clientSide = StreamConnection.GetDuplex(serverToClient.Reader, clientToServer.Writer); - return new(clientSide); + if (server.TryGetNode(endpoint, out var node)) + { + server._log?.WriteLine( + $"Client intercepted, endpoint {Format.ToString(endpoint)} ({connectionType}) mapped to {server.ServerType} node {node}"); + var clientToServer = new Pipe(pipeOptions ?? PipeOptions.Default); + var serverToClient = new Pipe(pipeOptions ?? PipeOptions.Default); + var serverSide = new Duplex(clientToServer.Reader, serverToClient.Writer); + _ = Task.Run(async () => await server.RunClientAsync(serverSide, node: node), cancellationToken); + var clientSide = StreamConnection.GetDuplex(serverToClient.Reader, clientToServer.Writer); + return new(clientSide); + } + return base.BeforeAuthenticateAsync(endpoint, connectionType, socket, cancellationToken); } private sealed class Duplex(PipeReader input, PipeWriter output) : IDuplexPipe @@ -65,6 +137,7 @@ public ValueTask Dispose() } } } + /* private readonly RespServer _server; diff --git a/tests/StackExchange.Redis.Tests/MovedTestServer.cs b/tests/StackExchange.Redis.Tests/MovedTestServer.cs index 3b572c3d1..17ed92c35 100644 --- a/tests/StackExchange.Redis.Tests/MovedTestServer.cs +++ b/tests/StackExchange.Redis.Tests/MovedTestServer.cs @@ -40,157 +40,61 @@ private enum SimulatedHost private SimulatedHost _currentServerHost = SimulatedHost.OldServer; - private readonly Func _getEndpoint; - private readonly string _triggerKey; - private readonly int _hashSlot; - private EndPoint? _actualEndpoint; + private readonly RedisKey _triggerKey; - public MovedTestServer(Func getEndpoint, string triggerKey = "testkey", int hashSlot = 12345, ITestOutputHelper? log = null) : base(log) + public MovedTestServer(in RedisKey triggerKey, ITestOutputHelper? log = null) : base(log) { - _getEndpoint = getEndpoint; _triggerKey = triggerKey; - _hashSlot = hashSlot; - ServerType = ServerType.Cluster; - RedisVersion = RedisFeatures.v7_2_0_rc1; } - private sealed class MovedTestClient(SimulatedHost assignedHost) : RedisClient + private sealed class MovedTestClient(MovedTestServer server, Node node, SimulatedHost assignedHost) : RedisClient(node) { public SimulatedHost AssignedHost => assignedHost; + + public override void OnKey(in RedisKey key, KeyFlags flags) + { + if (AssignedHost == SimulatedHost.OldServer && key == server._triggerKey) + { + server.OnTrigger(Id, key, assignedHost); + } + base.OnKey(in key, flags); + } } /// /// Called when a new client connection is established. /// Assigns the client to the current server host state (simulating proxy/load balancer routing). /// - public override RedisClient CreateClient() - { - var client = new MovedTestClient(_currentServerHost); - Log($"New client connection established (assigned to {client.AssignedHost}, total connections: {TotalClientCount}), endpoint: {_actualEndpoint}"); - return client; - } + public override RedisClient CreateClient(Node node) => new MovedTestClient(this, node, _currentServerHost); - /// - /// Handles CLUSTER commands, supporting SLOTS and NODES subcommands for cluster mode simulation. - /// - protected override TypedRedisValue Cluster(RedisClient client, RedisRequest request) + public override void OnClientConnected(RedisClient client, object state) { - if (request.Count < 2) + if (client is MovedTestClient movedClient) { - return TypedRedisValue.Error("ERR wrong number of arguments for 'cluster' command"); + Log($"Client {client.Id} connected (assigned to {movedClient.AssignedHost}), total connections: {TotalClientCount}"); } - - var subcommand = request.GetString(1); - - // Handle CLUSTER SLOTS command to support cluster mode - if (subcommand.Equals("SLOTS", StringComparison.OrdinalIgnoreCase)) - { - Log($"Returning CLUSTER SLOTS response, endpoint: {_actualEndpoint}"); - return GetClusterSlotsResponse(); - } - - // Handle CLUSTER NODES command - if (subcommand.Equals("NODES", StringComparison.OrdinalIgnoreCase)) - { - Log($"Returning CLUSTER NODES response, endpoint: {_actualEndpoint}"); - return GetClusterNodesResponse(); - } - - return TypedRedisValue.Error($"ERR Unknown CLUSTER subcommand '{subcommand}'"); + base.OnClientConnected(client, state); } /// /// Handles SET commands. Returns MOVED error for the trigger key when requested by clients /// connected to the old server, simulating a server migration behind a proxy/load balancer. /// - protected override TypedRedisValue Set(RedisClient client, RedisRequest request) + protected override TypedRedisValue Set(RedisClient client, in RedisRequest request) { - var key = request.GetKey(1); - - // Increment SET command counter for every SET call Interlocked.Increment(ref _setCmdCount); - - // Get the client's assigned server host - if (client is not MovedTestClient movedClient) - { - throw new InvalidOperationException($"Client is not a {nameof(MovedTestClient)}"); - } - var clientHost = movedClient.AssignedHost; - - // Check if this is the trigger key from an old server client - if (key == _triggerKey && clientHost == SimulatedHost.OldServer) - { - // Transition server to new host (so future connections route to new server) - _currentServerHost = SimulatedHost.NewServer; - - Interlocked.Increment(ref _movedResponseCount); - var endpoint = _getEndpoint(); - Log($"Returning MOVED {_hashSlot} {endpoint} for key '{key}' from {clientHost} client, server transitioned to {SimulatedHost.NewServer}, actual endpoint: {_actualEndpoint}"); - - // Return MOVED error pointing to same endpoint - return TypedRedisValue.Error($"MOVED {_hashSlot} {endpoint}"); - } - - // Normal processing for new server clients or other keys - Log($"Processing SET normally for key '{key}' from {clientHost} client, endpoint: {_actualEndpoint}"); return base.Set(client, request); } - /// - /// Returns a CLUSTER SLOTS response indicating this endpoint serves all slots (0-16383). - /// - private TypedRedisValue GetClusterSlotsResponse() - { - // Return a minimal CLUSTER SLOTS response indicating this endpoint serves all slots (0-16383) - // Format: Array of slot ranges, each containing: - // [start_slot, end_slot, [host, port, node_id]] - if (_actualEndpoint == null) - { - return TypedRedisValue.Error("ERR endpoint not set"); - } - - var endpoint = _getEndpoint(); - var parts = endpoint.Split(':'); - var host = parts.Length > 0 ? parts[0] : "127.0.0.1"; - var port = parts.Length > 1 ? parts[1] : "6379"; - - // Build response: [[0, 16383, [host, port, node-id]]] - // Inner array: [host, port, node-id] - var hostPortArray = TypedRedisValue.MultiBulk((ICollection)new[] - { - TypedRedisValue.BulkString(host), - TypedRedisValue.Integer(int.Parse(port)), - TypedRedisValue.BulkString("test-node-id"), - }); - // Slot range: [start_slot, end_slot, [host, port, node-id]] - var slotRange = TypedRedisValue.MultiBulk((ICollection)new[] - { - TypedRedisValue.Integer(0), // start slot - TypedRedisValue.Integer(16383), // end slot - hostPortArray, - }); - - // Outer array containing the single slot range - return TypedRedisValue.MultiBulk((ICollection)new[] { slotRange }); - } - - /// - /// Returns a CLUSTER NODES response. - /// - private TypedRedisValue GetClusterNodesResponse() + private void OnTrigger(int clientId, in RedisKey key, SimulatedHost assignedHost) { - // Return CLUSTER NODES response - // Format: node-id host:port@cport flags master - ping-sent pong-recv config-epoch link-state slot-range - // Example: test-node-id 127.0.0.1:6379@16379 myself,master - 0 0 1 connected 0-16383 - if (_actualEndpoint == null) - { - return TypedRedisValue.Error("ERR endpoint not set"); - } + // Transition server to new host (so future connections know they're on the new server) + _currentServerHost = SimulatedHost.NewServer; - var endpoint = _getEndpoint(); - var nodesInfo = $"test-node-id {endpoint}@1{endpoint.Split(':')[1]} myself,master - 0 0 1 connected 0-16383\r\n"; + Interlocked.Increment(ref _movedResponseCount); - return TypedRedisValue.BulkString(nodesInfo); + Log($"Triggering MOVED on Client {clientId} ({assignedHost}) with key: {key}"); + KeyMovedException.Throw(key); } /// @@ -203,21 +107,6 @@ private TypedRedisValue GetClusterNodesResponse() /// public int MovedResponseCount => _movedResponseCount; - /// - /// Gets the actual endpoint the server is listening on. - /// - public EndPoint? ActualEndpoint => _actualEndpoint; - - /// - /// Sets the actual endpoint the server is listening on. - /// This should be called externally after the server starts. - /// - public void SetActualEndpoint(EndPoint endPoint) - { - _actualEndpoint = endPoint; - Log($"MovedTestServer endpoint set to {endPoint}"); - } - /// /// Resets all counters for test reusability. /// diff --git a/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs b/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs deleted file mode 100644 index 42f8f2dea..000000000 --- a/tests/StackExchange.Redis.Tests/MovedToSameEndpointTests.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using Xunit; - -namespace StackExchange.Redis.Tests; - -/// -/// Integration tests for MOVED-to-same-endpoint error handling. -/// When a MOVED error points to the same endpoint, the client should reconnect before retrying, -/// allowing the DNS record/proxy/load balancer to route to a different underlying server host. -/// -public class MovedToSameEndpointTests(ITestOutputHelper log) -{ - /// - /// Integration test: Verifies that when a MOVED error points to the same endpoint, - /// the client reconnects and successfully retries the operation. - /// - /// Test scenario: - /// 1. Client connects to test server - /// 2. Client sends SET command for trigger key - /// 3. Server returns MOVED error pointing to same endpoint - /// 4. Client detects MOVED-to-same-endpoint and triggers reconnection - /// 5. Client retries SET command after reconnection - /// 6. Server processes SET normally on retry - /// - /// Expected behavior: - /// - SET command count should increase by 2 (initial attempt + retry) - /// - MOVED response count should increase by 1 (only on first attempt) - /// - Connection count should increase by 1 (reconnection after MOVED) - /// - Final SET operation should succeed with value stored. - /// - [Fact] - public async Task MovedToSameEndpoint_TriggersReconnectAndRetry_CommandSucceeds() - { - var keyName = "MovedToSameEndpoint_TriggersReconnectAndRetry_CommandSucceeds"; - - var listenEndpoint = new IPEndPoint(IPAddress.Loopback, 6382); - using var testServer = new MovedTestServer( - getEndpoint: () => Format.ToString(listenEndpoint), - triggerKey: keyName, - log: log); - - testServer.SetActualEndpoint(listenEndpoint); - - // Wait a moment for the server to fully start - await Task.Delay(100); - - // Act: Connect to the test server - var config = new ConfigurationOptions - { - EndPoints = { listenEndpoint }, - ConnectTimeout = 10000, - SyncTimeout = 5000, - AsyncTimeout = 5000, - AllowAdmin = true, - Tunnel = testServer.Tunnel, - }; - - await using var conn = await ConnectionMultiplexer.ConnectAsync(config); - // Ping the server to ensure it's responsive - var server = conn.GetServer(listenEndpoint); - log?.WriteLine((await server.InfoRawAsync()) ?? ""); - var id = await server.ExecuteAsync("client", "id"); - log?.WriteLine($"client id: {id}"); - - await server.PingAsync(); - // Verify server is detected as cluster mode - Assert.Equal(ServerType.Cluster, server.ServerType); - var db = conn.GetDatabase(); - - // Record baseline counters after initial connection - var initialSetCmdCount = testServer.SetCmdCount; - var initialMovedResponseCount = testServer.MovedResponseCount; - var initialConnectionCount = testServer.TotalClientCount; - // Execute SET command: This should receive MOVED → reconnect → retry → succeed - var setResult = await db.StringSetAsync(keyName, "testvalue"); - - // Assert: Verify SET command succeeded - Assert.True(setResult, "SET command should return true (OK)"); - - // Verify the value was actually stored (proving retry succeeded) - var retrievedValue = await db.StringGetAsync(keyName); - Assert.Equal("testvalue", (string?)retrievedValue); - - // Verify SET command was executed twice: once with MOVED response, once successfully - var expectedSetCmdCount = initialSetCmdCount + 2; - Assert.Equal(expectedSetCmdCount, testServer.SetCmdCount); - - // Verify MOVED response was returned exactly once - var expectedMovedResponseCount = initialMovedResponseCount + 1; - Assert.Equal(expectedMovedResponseCount, testServer.MovedResponseCount); - - // Verify reconnection occurred: connection count should have increased by 1 - var expectedConnectionCount = initialConnectionCount + 1; - Assert.Equal(expectedConnectionCount, testServer.TotalClientCount); - id = await server.ExecuteAsync("client", "id"); - log?.WriteLine($"client id: {id}"); - } -} diff --git a/tests/StackExchange.Redis.Tests/MovedUnitTests.cs b/tests/StackExchange.Redis.Tests/MovedUnitTests.cs new file mode 100644 index 000000000..1671d6cc3 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/MovedUnitTests.cs @@ -0,0 +1,148 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using StackExchange.Redis.Configuration; +using Xunit; + +namespace StackExchange.Redis.Tests; + +/// +/// Integration tests for MOVED-to-same-endpoint error handling. +/// When a MOVED error points to the same endpoint, the client should reconnect before retrying, +/// allowing the DNS record/proxy/load balancer to route to a different underlying server host. +/// +public class MovedUnitTests(ITestOutputHelper log) +{ + private RedisKey Me([CallerMemberName] string callerName = "") => callerName; + + [Theory] + [InlineData(ServerType.Cluster)] + [InlineData(ServerType.Standalone)] + public async Task CrossSlotDisallowed(ServerType serverType) + { + // intentionally sending as strings (not keys) via execute to prevent the + // client library from getting in our way + string keyA = "abc", keyB = "def"; // known to be on different slots + + using var server = new InProcessTestServer(log) { ServerType = serverType }; + await using var muxer = await server.ConnectAsync(); + + var db = muxer.GetDatabase(); + await db.StringSetAsync(keyA, "value", flags: CommandFlags.FireAndForget); + + var pending = db.ExecuteAsync("rename", keyA, keyB); + if (serverType == ServerType.Cluster) + { + var ex = await Assert.ThrowsAsync(() => pending); + Assert.Contains("CROSSSLOT", ex.Message); + + Assert.Equal("value", await db.StringGetAsync(keyA)); + Assert.False(await db.KeyExistsAsync(keyB)); + } + else + { + await pending; + Assert.False(await db.KeyExistsAsync(keyA)); + Assert.Equal("value", await db.StringGetAsync(keyB)); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task KeyMigrationFollowed(bool allowFollowRedirects) + { + RedisKey key = Me(); + using var server = new InProcessTestServer(log) { ServerType = ServerType.Cluster }; + var secondNode = server.AddEmptyNode(); + + await using var muxer = await server.ConnectAsync(); + var db = muxer.GetDatabase(); + + await db.StringSetAsync(key, "value"); + var value = await db.StringGetAsync(key); + Assert.Equal("value", (string?)value); + + server.Migrate(key, secondNode); + + if (allowFollowRedirects) + { + value = await db.StringGetAsync(key, flags: CommandFlags.None); + Assert.Equal("value", (string?)value); + } + else + { + var ex = await Assert.ThrowsAsync(() => db.StringGetAsync(key, flags: CommandFlags.NoRedirect)); + Assert.Contains("MOVED", ex.Message); + } + } + + /// + /// Integration test: Verifies that when a MOVED error points to the same endpoint, + /// the client reconnects and successfully retries the operation. + /// + /// Test scenario: + /// 1. Client connects to test server + /// 2. Client sends SET command for trigger key + /// 3. Server returns MOVED error pointing to same endpoint + /// 4. Client detects MOVED-to-same-endpoint and triggers reconnection + /// 5. Client retries SET command after reconnection + /// 6. Server processes SET normally on retry + /// + /// Expected behavior: + /// - SET command count should increase by 2 (initial attempt + retry) + /// - MOVED response count should increase by 1 (only on first attempt) + /// - Connection count should increase by 1 (reconnection after MOVED) + /// - Final SET operation should succeed with value stored. + /// + [Theory] + [InlineData(ServerType.Cluster)] + [InlineData(ServerType.Standalone)] + public async Task MovedToSameEndpoint_TriggersReconnectAndRetry_CommandSucceeds(ServerType serverType) + { + RedisKey key = Me(); + + using var testServer = new MovedTestServer( + triggerKey: key, + log: log) { ServerType = serverType, }; + + // Act: Connect to the test server + await using var conn = await testServer.ConnectAsync(); + // Ping the server to ensure it's responsive + var server = conn.GetServer(testServer.DefaultEndPoint); + + var id = await server.ExecuteAsync("client", "id"); + log?.WriteLine($"Client id before: {id}"); + + await server.PingAsync(); // init everything + // Verify server is detected as per test config + Assert.Equal(serverType, server.ServerType); + var db = conn.GetDatabase(); + + // Record baseline counters after initial connection + Assert.Equal(0, testServer.SetCmdCount); + Assert.Equal(0, testServer.MovedResponseCount); + var initialConnectionCount = testServer.TotalClientCount; + + // Execute SET command: This should receive MOVED → reconnect → retry → succeed + var setResult = await db.StringSetAsync(key, "testvalue"); + + // Assert: Verify SET command succeeded + Assert.True(setResult, "SET command should return true (OK)"); + + // Verify the value was actually stored (proving retry succeeded) + var retrievedValue = await db.StringGetAsync(key); + Assert.Equal("testvalue", (string?)retrievedValue); + + // Verify SET command was executed twice: once with MOVED response, once successfully + Assert.Equal(2, testServer.SetCmdCount); + + // Verify MOVED response was returned exactly once + Assert.Equal(1, testServer.MovedResponseCount); + + // Verify reconnection occurred: connection count should have increased by 1 + Assert.Equal(initialConnectionCount + 1, testServer.TotalClientCount); + id = await server.ExecuteAsync("client", "id"); + log?.WriteLine($"Client id after: {id}"); + } +} diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 68dbb6055..0e622c20c 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using StackExchange.Redis.Configuration; using StackExchange.Redis.Profiling; using StackExchange.Redis.Tests.Helpers; using Xunit; @@ -17,22 +18,35 @@ public abstract class TestBase : IDisposable { private ITestOutputHelper Output { get; } protected TextWriterOutputHelper Writer { get; } - protected virtual string GetConfiguration() => GetDefaultConfiguration(); + protected virtual string GetConfiguration() + { + if (_inProcServerFixture != null) + { + return _inProcServerFixture.Configuration; + } + return GetDefaultConfiguration(); + } internal static string GetDefaultConfiguration() => TestConfig.Current.PrimaryServerAndPort; - private readonly SharedConnectionFixture? _fixture; + private readonly SharedConnectionFixture? _sharedConnectionFixture; + private readonly InProcServerFixture? _inProcServerFixture; - protected bool SharedFixtureAvailable => _fixture != null && _fixture.IsEnabled && !HighIntegrity; + protected bool SharedFixtureAvailable => _sharedConnectionFixture != null && _sharedConnectionFixture.IsEnabled && !HighIntegrity; - protected TestBase(ITestOutputHelper output, SharedConnectionFixture? fixture = null) + protected TestBase(ITestOutputHelper output, SharedConnectionFixture? connection = null, InProcServerFixture? server = null) { Output = output; Output.WriteFrameworkVersion(); Writer = new TextWriterOutputHelper(output); - _fixture = fixture; + _sharedConnectionFixture = connection; + _inProcServerFixture = server; ClearAmbientFailures(); } + protected TestBase(ITestOutputHelper output, InProcServerFixture fixture) : this(output, null, fixture) + { + } + /// /// Useful to temporarily get extra worker threads for an otherwise synchronous test case which will 'block' the thread, /// on a synchronous API like or . @@ -85,7 +99,7 @@ protected static void CollectGarbage() [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Trust me yo")] public void Dispose() { - _fixture?.Teardown(Writer); + _sharedConnectionFixture?.Teardown(Writer); Teardown(); Writer.Dispose(); GC.SuppressFinalize(this); @@ -226,6 +240,8 @@ protected static IServer GetAnyPrimary(IConnectionMultiplexer muxer) internal virtual bool HighIntegrity => false; + internal virtual Tunnel? Tunnel => _inProcServerFixture?.Tunnel; + internal virtual IInternalConnectionMultiplexer Create( string? clientName = null, int? syncTimeout = null, @@ -262,17 +278,18 @@ internal virtual IInternalConnectionMultiplexer Create( // Share a connection if instructed to and we can - many specifics mean no sharing bool highIntegrity = HighIntegrity; - if (shared && expectedFailCount == 0 - && _fixture != null && _fixture.IsEnabled + var tunnel = Tunnel; + if (tunnel is null && shared && expectedFailCount == 0 + && _sharedConnectionFixture != null && _sharedConnectionFixture.IsEnabled && GetConfiguration() == GetDefaultConfiguration() && CanShare(allowAdmin, password, tieBreaker, fail, disabledCommands, enabledCommands, channelPrefix, proxy, configuration, defaultDatabase, backlogPolicy, highIntegrity)) { configuration = GetConfiguration(); - var fixtureConn = _fixture.GetConnection(this, protocol.Value, caller: caller); + var fixtureConn = _sharedConnectionFixture.GetConnection(this, protocol.Value, caller: caller); // Only return if we match TestBase.ThrowIfIncorrectProtocol(fixtureConn, protocol); - if (configuration == _fixture.Configuration) + if (configuration == _sharedConnectionFixture.Configuration) { TestBase.ThrowIfBelowMinVersion(fixtureConn, require); return fixtureConn; @@ -303,6 +320,7 @@ internal virtual IInternalConnectionMultiplexer Create( backlogPolicy, protocol, highIntegrity, + tunnel, caller); TestBase.ThrowIfIncorrectProtocol(conn, protocol); @@ -392,6 +410,7 @@ public static ConnectionMultiplexer CreateDefault( BacklogPolicy? backlogPolicy = null, RedisProtocol? protocol = null, bool highIntegrity = false, + Tunnel? tunnel = null, [CallerMemberName] string caller = "") { StringWriter? localLog = null; @@ -413,6 +432,7 @@ public static ConnectionMultiplexer CreateDefault( syncTimeout = int.MaxValue; } + config.Tunnel = tunnel; if (channelPrefix is not null) config.ChannelPrefix = RedisChannel.Literal(channelPrefix); if (tieBreaker is not null) config.TieBreaker = tieBreaker; if (password is not null) config.Password = string.IsNullOrEmpty(password) ? null : password; diff --git a/toys/KestrelRedisServer/Program.cs b/toys/KestrelRedisServer/Program.cs index 6cabf95d1..fb77c2f14 100644 --- a/toys/KestrelRedisServer/Program.cs +++ b/toys/KestrelRedisServer/Program.cs @@ -1,11 +1,26 @@ -using KestrelRedisServer; +using System.Net; +using KestrelRedisServer; using Microsoft.AspNetCore.Connections; +using StackExchange.Redis; using StackExchange.Redis.Server; -var server = new MemoryCacheRedisServer(); +var server = new MemoryCacheRedisServer +{ + // note: we don't support many v6 features, but some clients + // want this before they'll try RESP3 + RedisVersion = new(6, 0), + // Password = "letmein", +}; + +/* +// demonstrate cluster spoofing +server.ServerType = ServerType.Cluster; +var ep = server.AddEmptyNode(); +server.Migrate("key", ep); +*/ var builder = WebApplication.CreateBuilder(args); -builder.Services.AddSingleton(server); +builder.Services.AddSingleton(server); builder.WebHost.ConfigureKestrel(options => { // HTTP 5000 (test/debug API only) @@ -13,7 +28,18 @@ // this is the core of using Kestrel to create a TCP server // TCP 6379 - options.ListenLocalhost(6379, builder => builder.UseConnectionHandler()); + Action builder = builder => builder.UseConnectionHandler(); + foreach (var ep in server.GetEndPoints()) + { + if (ep is IPEndPoint ip && ip.Address.Equals(IPAddress.Loopback)) + { + options.ListenLocalhost(ip.Port, builder); + } + else + { + options.Listen(ep, builder); + } + } }); var app = builder.Build(); diff --git a/toys/KestrelRedisServer/RedisConnectionHandler.cs b/toys/KestrelRedisServer/RedisConnectionHandler.cs index a447440d9..58511b1fc 100644 --- a/toys/KestrelRedisServer/RedisConnectionHandler.cs +++ b/toys/KestrelRedisServer/RedisConnectionHandler.cs @@ -1,15 +1,18 @@ -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections; using StackExchange.Redis.Server; namespace KestrelRedisServer { - public class RedisConnectionHandler : ConnectionHandler + public class RedisConnectionHandler(RedisServer server) : ConnectionHandler { - private readonly RespServer _server; - public RedisConnectionHandler(RespServer server) => _server = server; public override Task OnConnectedAsync(ConnectionContext connection) - => _server.RunClientAsync(connection.Transport); + { + RedisServer.Node? node; + if (!(connection.LocalEndPoint is { } ep && server.TryGetNode(ep, out node))) + { + node = null; + } + return server.RunClientAsync(connection.Transport, node: node); + } } } diff --git a/toys/StackExchange.Redis.Server/MemoryCacheRedisServer.cs b/toys/StackExchange.Redis.Server/MemoryCacheRedisServer.cs index b57ec4aea..e9bcb5a5f 100644 --- a/toys/StackExchange.Redis.Server/MemoryCacheRedisServer.cs +++ b/toys/StackExchange.Redis.Server/MemoryCacheRedisServer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Runtime.Caching; using System.Runtime.CompilerServices; @@ -9,108 +10,206 @@ namespace StackExchange.Redis.Server { public class MemoryCacheRedisServer : RedisServer { - public MemoryCacheRedisServer(TextWriter output = null) : base(1, output) + public MemoryCacheRedisServer(EndPoint endpoint = null, TextWriter output = null) : base(endpoint, 1, output) => CreateNewCache(); - private MemoryCache _cache; + private MemoryCache _cache2; private void CreateNewCache() { - var old = _cache; - _cache = new MemoryCache(GetType().Name); + var old = _cache2; + _cache2 = new MemoryCache(GetType().Name); old?.Dispose(); } protected override void Dispose(bool disposing) { - if (disposing) _cache.Dispose(); + if (disposing) _cache2.Dispose(); base.Dispose(disposing); } - protected override long Dbsize(int database) => _cache.GetCount(); - protected override RedisValue Get(int database, RedisKey key) - => RedisValue.Unbox(_cache[key]); - protected override void Set(int database, RedisKey key, RedisValue value) - => _cache[key] = value.Box(); - protected override bool Del(int database, RedisKey key) - => _cache.Remove(key) != null; + protected override long Dbsize(int database) => _cache2.GetCount(); + + private readonly struct ExpiringValue(object value, DateTime absoluteExpiration) + { + public readonly object Value = value; + public readonly DateTime AbsoluteExpiration = absoluteExpiration; + } + + private enum ExpectedType + { + Any = 0, + Stack, + Set, + List, + } + private object Get(in RedisKey key, ExpectedType expectedType) + { + var val = _cache2[key]; + switch (val) + { + case null: + return null; + case ExpiringValue ev: + if (ev.AbsoluteExpiration <= Time()) + { + _cache2.Remove(key); + return null; + } + return Validate(ev.Value, expectedType); + default: + return Validate(val, expectedType); + } + static object Validate(object value, ExpectedType expectedType) + { + return value switch + { + null => value, + HashSet set when expectedType is ExpectedType.Set or ExpectedType.Any => value, + HashSet => Throw(), + Stack stack when expectedType is ExpectedType.List or ExpectedType.Any => value, + Stack => Throw(), + _ when expectedType is ExpectedType.Stack or ExpectedType.Any => value, + _ => Throw(), + }; + + static object Throw() => throw new WrongTypeException(); + } + } + protected override TimeSpan? Ttl(int database, in RedisKey key) + { + var val = _cache2[key]; + switch (val) + { + case null: + return null; + case ExpiringValue ev: + var delta = ev.AbsoluteExpiration - Time(); + if (delta <= TimeSpan.Zero) + { + _cache2.Remove(key); + return null; + } + return delta; + default: + return TimeSpan.MaxValue; + } + } + + protected override bool Expire(int database, in RedisKey key, TimeSpan timeout) + { + if (timeout <= TimeSpan.Zero) return Del(database, key); + var val = Get(key, ExpectedType.Any); + if (val is not null) + { + _cache2[key] = new ExpiringValue(val, Time() + timeout); + return true; + } + + return false; + } + + protected override RedisValue Get(int database, in RedisKey key) + { + var val = Get(key, ExpectedType.Stack); + return RedisValue.Unbox(val); + } + + protected override void Set(int database, in RedisKey key, in RedisValue value) + => _cache2[key] = value.Box(); + + protected override void SetEx(int database, in RedisKey key, TimeSpan expiration, in RedisValue value) + { + var now = Time(); + var absolute = now + expiration; + if (absolute <= now) _cache2.Remove(key); + else _cache2[key] = new ExpiringValue(value.Box(), absolute); + } + + protected override bool Del(int database, in RedisKey key) + => _cache2.Remove(key) != null; protected override void Flushdb(int database) => CreateNewCache(); - protected override bool Exists(int database, RedisKey key) - => _cache.Contains(key); + protected override bool Exists(int database, in RedisKey key) + { + var val = Get(key, ExpectedType.Any); + return val != null && !(val is ExpiringValue ev && ev.AbsoluteExpiration <= Time()); + } - protected override IEnumerable Keys(int database, RedisKey pattern) + protected override IEnumerable Keys(int database, in RedisKey pattern) => GetKeysCore(pattern); + private IEnumerable GetKeysCore(RedisKey pattern) { - foreach (var pair in _cache) + foreach (var pair in _cache2) { + if (pair.Value is ExpiringValue ev && ev.AbsoluteExpiration <= Time()) continue; if (IsMatch(pattern, pair.Key)) yield return pair.Key; } } - protected override bool Sadd(int database, RedisKey key, RedisValue value) + protected override bool Sadd(int database, in RedisKey key, in RedisValue value) => GetSet(key, true).Add(value); - protected override bool Sismember(int database, RedisKey key, RedisValue value) + protected override bool Sismember(int database, in RedisKey key, in RedisValue value) => GetSet(key, false)?.Contains(value) ?? false; - protected override bool Srem(int database, RedisKey key, RedisValue value) + protected override bool Srem(int database, in RedisKey key, in RedisValue value) { var set = GetSet(key, false); if (set != null && set.Remove(value)) { - if (set.Count == 0) _cache.Remove(key); + if (set.Count == 0) _cache2.Remove(key); return true; } return false; } - protected override long Scard(int database, RedisKey key) + protected override long Scard(int database, in RedisKey key) => GetSet(key, false)?.Count ?? 0; private HashSet GetSet(RedisKey key, bool create) { - var set = (HashSet)_cache[key]; + var set = (HashSet)Get(key, ExpectedType.Set); if (set == null && create) { set = new HashSet(); - _cache[key] = set; + _cache2[key] = set; } return set; } - protected override RedisValue Spop(int database, RedisKey key) + protected override RedisValue Spop(int database, in RedisKey key) { var set = GetSet(key, false); if (set == null) return RedisValue.Null; var result = set.First(); set.Remove(result); - if (set.Count == 0) _cache.Remove(key); + if (set.Count == 0) _cache2.Remove(key); return result; } - protected override long Lpush(int database, RedisKey key, RedisValue value) + protected override long Lpush(int database, in RedisKey key, in RedisValue value) { var stack = GetStack(key, true); stack.Push(value); return stack.Count; } - protected override RedisValue Lpop(int database, RedisKey key) + protected override RedisValue Lpop(int database, in RedisKey key) { var stack = GetStack(key, false); if (stack == null) return RedisValue.Null; var val = stack.Pop(); - if (stack.Count == 0) _cache.Remove(key); + if (stack.Count == 0) _cache2.Remove(key); return val; } - protected override long Llen(int database, RedisKey key) + protected override long Llen(int database, in RedisKey key) => GetStack(key, false)?.Count ?? 0; [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowArgumentOutOfRangeException() => throw new ArgumentOutOfRangeException(); - protected override void LRange(int database, RedisKey key, long start, Span arr) + protected override void LRange(int database, in RedisKey key, long start, Span arr) { var stack = GetStack(key, false); @@ -128,13 +227,13 @@ protected override void LRange(int database, RedisKey key, long start, Span GetStack(RedisKey key, bool create) + private Stack GetStack(in RedisKey key, bool create) { - var stack = (Stack)_cache[key]; + var stack = (Stack)Get(key, ExpectedType.Stack); if (stack == null && create) { stack = new Stack(); - _cache[key] = stack; + _cache2[key] = stack; } return stack; } diff --git a/toys/StackExchange.Redis.Server/RedisClient.cs b/toys/StackExchange.Redis.Server/RedisClient.cs index 1a49acc64..cf3116500 100644 --- a/toys/StackExchange.Redis.Server/RedisClient.cs +++ b/toys/StackExchange.Redis.Server/RedisClient.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.IO.Pipelines; +using System.Text; namespace StackExchange.Redis.Server { - public class RedisClient : IDisposable + public class RedisClient(RedisServer.Node node) : IDisposable { + public RedisServer.Node Node => node; internal int SkipReplies { get; set; } internal bool ShouldSkipResponse() { @@ -53,5 +55,212 @@ public void Dispose() if (pipe is IDisposable d) try { d.Dispose(); } catch { } } } + + private int _activeSlot = ServerSelectionStrategy.NoSlot; + internal void ResetAfterRequest() => _activeSlot = ServerSelectionStrategy.NoSlot; + public virtual void OnKey(in RedisKey key, KeyFlags flags) + { + if ((flags & KeyFlags.NoSlotCheck) == 0 & node.CheckCrossSlot) + { + var slot = RespServer.GetHashSlot(key); + if (_activeSlot is ServerSelectionStrategy.NoSlot) + { + _activeSlot = slot; + } + else if (_activeSlot != slot) + { + CrossSlotException.Throw(); + } + } + // ASKING here? + node.AssertKey(key); + + if ((flags & KeyFlags.ReadOnly) == 0) node.Touch(Database, key); + } + + public void Touch(int database, in RedisKey key) + { + TransactionState failureState = TransactionState.WatchDoomed; + switch (_transactionState) + { + case TransactionState.WatchHopeful: + if (_watching.Contains(new(database, key))) + { + _transactionState = failureState; + _watching.Clear(); + } + break; + case TransactionState.MultiHopeful: + failureState = TransactionState.MultiDoomedByTouch; + _transaction?.Clear(); + goto case TransactionState.WatchHopeful; + } + } + + public bool Watch(in RedisKey key) + { + switch (_transactionState) + { + case TransactionState.None: + _transactionState = TransactionState.WatchHopeful; + goto case TransactionState.WatchHopeful; + case TransactionState.WatchHopeful: + _watching.Add(new(Database, key)); + return true; + case TransactionState.WatchDoomed: + case TransactionState.MultiDoomedByTouch: + // no point tracking, just pretend + return true; + default: + // can't watch inside multi + return false; + } + } + + public bool Unwatch() + { + switch (_transactionState) + { + case TransactionState.MultiHopeful: + case TransactionState.MultiDoomedByTouch: + case TransactionState.MultiAbortByError: + return false; + default: + _watching.Clear(); + _transactionState = TransactionState.None; + return true; + } + } + + private TransactionState _transactionState; + + private enum TransactionState + { + None, + WatchHopeful, + WatchDoomed, + MultiHopeful, + MultiDoomedByTouch, + MultiAbortByError, + } + + private readonly struct DatabaseKey(int db, in RedisKey key) : IEquatable + { + public readonly int Db = db; + public readonly RedisKey Key = key; + public override int GetHashCode() => unchecked((Db * 397) ^ Key.GetHashCode()); + public override bool Equals(object obj) => obj is DatabaseKey other && Equals(other); + public bool Equals(DatabaseKey other) => Db == other.Db && Key.Equals(other.Key); + } + private readonly HashSet _watching = []; + + public bool Multi() + { + switch (_transactionState) + { + case TransactionState.None: + case TransactionState.WatchHopeful: + _transactionState = TransactionState.MultiHopeful; + return true; + case TransactionState.WatchDoomed: + _transactionState = TransactionState.MultiDoomedByTouch; + return true; + default: + return false; + } + } + + public bool Discard() + { + switch (_transactionState) + { + case TransactionState.MultiHopeful: + case TransactionState.MultiDoomedByTouch: + _transactionState = TransactionState.None; + _watching.Clear(); + _transaction?.Clear(); + return true; + case TransactionState.MultiAbortByError: + return true; + default: + return false; + } + } + + public void ExecAbort() + { + switch (_transactionState) + { + case TransactionState.MultiHopeful: + case TransactionState.MultiDoomedByTouch: + _transactionState = TransactionState.MultiAbortByError; + _watching.Clear(); + _transaction?.Clear(); + break; + } + } + + public enum ExecResult + { + NotInTransaction, + WatchConflict, + AbortedByError, + CommandsReturned, + } + + public ExecResult FlushMulti(out byte[][] commands) + { + commands = []; + switch (_transactionState) + { + case TransactionState.MultiHopeful: + _transactionState = TransactionState.None; + _watching.Clear(); + commands = _transaction?.ToArray() ?? []; + _transaction?.Clear(); + return ExecResult.CommandsReturned; + case TransactionState.MultiDoomedByTouch: + _transactionState = TransactionState.None; + return ExecResult.WatchConflict; + case TransactionState.MultiAbortByError: + _transactionState = TransactionState.None; + return ExecResult.AbortedByError; + default: + return ExecResult.NotInTransaction; + } + } + + // completely unoptimized for now; this is fine + private List _transaction; // null until needed + + internal bool BufferMulti(in RedisRequest request, in CommandBytes command) + { + switch (_transactionState) + { + case TransactionState.MultiHopeful when !AllowInTransaction(command): + // TODO we also can't do this bit! just store the command name for now + (_transaction ??= []).Add(Encoding.ASCII.GetBytes(request.GetString(0))); + return true; + case TransactionState.MultiAbortByError when !AllowInTransaction(command): + case TransactionState.MultiDoomedByTouch when !AllowInTransaction(command): + // don't buffer anything, just pretend + return true; + default: + return false; + } + + static bool AllowInTransaction(in CommandBytes cmd) + => cmd.Equals(EXEC) || cmd.Equals(DISCARD) || cmd.Equals(MULTI) + || cmd.Equals(WATCH) || cmd.Equals(UNWATCH); + } + + private static readonly CommandBytes + EXEC = new("EXEC"u8), DISCARD = new("DISCARD"u8), MULTI = new("MULTI"u8), + WATCH = new("WATCH"u8), UNWATCH = new("UNWATCH"u8); + } + + internal sealed class CrossSlotException : Exception + { + public static void Throw() => throw new CrossSlotException(); } } diff --git a/toys/StackExchange.Redis.Server/RedisRequest.cs b/toys/StackExchange.Redis.Server/RedisRequest.cs index 3878398d9..d8ea13b86 100644 --- a/toys/StackExchange.Redis.Server/RedisRequest.cs +++ b/toys/StackExchange.Redis.Server/RedisRequest.cs @@ -9,7 +9,15 @@ public readonly ref struct RedisRequest // so: using "ref" makes it clear that you can't expect to store these and have // them keep working private readonly RawResult _inner; + private readonly RedisClient _client; + public RedisRequest WithClient(RedisClient client) => new(in this, client); + + private RedisRequest(scoped in RedisRequest original, RedisClient client) + { + this = original; + _client = client; + } public int Count { get; } public override string ToString() => Count == 0 ? "(n/a)" : GetString(0); @@ -57,7 +65,12 @@ public bool TryGetInt32(int index, out int value) public long GetInt64(int index) => (long)_inner[index].AsRedisValue(); - public RedisKey GetKey(int index) => _inner[index].AsRedisKey(); + public RedisKey GetKey(int index, KeyFlags flags = KeyFlags.None) + { + var key = _inner[index].AsRedisKey(); + _client?.OnKey(key, flags); + return key; + } internal RedisChannel GetChannel(int index, RedisChannel.RedisChannelOptions options) => _inner[index].AsRedisChannel(null, options); @@ -75,4 +88,12 @@ internal bool TryGetCommandBytes(int i, out CommandBytes command) return true; } } + + [Flags] + public enum KeyFlags + { + None = 0, + ReadOnly = 1 << 0, + NoSlotCheck = 1 << 1, + } } diff --git a/toys/StackExchange.Redis.Server/RedisServer.cs b/toys/StackExchange.Redis.Server/RedisServer.cs index e07ead48d..2830cc35a 100644 --- a/toys/StackExchange.Redis.Server/RedisServer.cs +++ b/toys/StackExchange.Redis.Server/RedisServer.cs @@ -1,8 +1,12 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; +using System.Net; using System.Text; +using System.Threading; namespace StackExchange.Redis.Server { @@ -12,8 +16,112 @@ public abstract class RedisServer : RespServer public static bool IsMatch(string pattern, string key) => pattern == "*" || string.Equals(pattern, key, StringComparison.OrdinalIgnoreCase); - protected RedisServer(int databases = 16, TextWriter output = null) : base(output) + private ConcurrentDictionary _nodes = new(); + + public bool TryGetNode(EndPoint endpoint, out Node node) => _nodes.TryGetValue(endpoint, out node); + + public EndPoint DefaultEndPoint + { + get + { + foreach (var pair in _nodes) + { + return pair.Key; + } + throw new InvalidOperationException("No endpoints"); + } + } + + public override Node DefaultNode + { + get + { + foreach (var pair in _nodes) + { + return pair.Value; + } + return null; + } + } + + public IEnumerable GetEndPoints() + { + foreach (var pair in _nodes) + { + yield return pair.Key; + } + } + + public bool Migrate(int hashSlot, EndPoint to) + { + if (ServerType != ServerType.Cluster) throw new InvalidOperationException($"Server mode is {ServerType}"); + if (!TryGetNode(to, out var target)) throw new KeyNotFoundException($"Target node not found: {Format.ToString(to)}"); + foreach (var pair in _nodes) + { + if (pair.Value.HasSlot(hashSlot)) + { + if (pair.Value == target) return false; // nothing to do + + if (!pair.Value.RemoveSlot(hashSlot)) + { + throw new KeyNotFoundException($"Unable to remove slot {hashSlot} from old owner"); + } + target.AddSlot(hashSlot); + return true; + } + } + throw new KeyNotFoundException($"Source node not found for slot {hashSlot}"); + } + public bool Migrate(Span key, EndPoint to) => Migrate(ServerSelectionStrategy.GetClusterSlot(key), to); + public bool Migrate(in RedisKey key, EndPoint to) => Migrate(GetHashSlot(key), to); + + public EndPoint AddEmptyNode() { + EndPoint endpoint; + Node node; + do + { + endpoint = null; + int maxPort = 0; + foreach (var pair in _nodes) + { + endpoint ??= pair.Key; + switch (pair.Key) + { + case IPEndPoint ip: + if (ip.Port > maxPort) maxPort = ip.Port; + break; + case DnsEndPoint dns: + if (dns.Port > maxPort) maxPort = dns.Port; + break; + } + } + + switch (endpoint) + { + case null: + endpoint = new IPEndPoint(IPAddress.Loopback, 6379); + break; + case IPEndPoint ip: + endpoint = new IPEndPoint(ip.Address, maxPort + 1); + break; + case DnsEndPoint dns: + endpoint = new DnsEndPoint(dns.Host, maxPort + 1); + break; + } + + node = new(this, endpoint); + node.UpdateSlots([]); // explicit empty range (rather than implicit "all nodes") + } + // defensive loop for concurrency + while (!_nodes.TryAdd(endpoint, node)); + return endpoint; + } + + protected RedisServer(EndPoint endpoint = null, int databases = 16, TextWriter output = null) : base(output) + { + endpoint ??= new IPEndPoint(IPAddress.Loopback, 6379); + _nodes.TryAdd(endpoint, new Node(this, endpoint)); RedisVersion = s_DefaultServerVersion; if (databases < 1) throw new ArgumentOutOfRangeException(nameof(databases)); Databases = databases; @@ -45,7 +153,7 @@ protected override void AppendStats(StringBuilder sb) public string Password { get; set; } = ""; - public override TypedRedisValue Execute(RedisClient client, RedisRequest request) + public override TypedRedisValue Execute(RedisClient client, in RedisRequest request) { var pw = Password; if (pw.Length != 0 & !client.IsAuthenticated) @@ -68,7 +176,7 @@ public static bool IsAuthCommand(in RedisRequest request) => } [RedisCommand(2)] - protected virtual TypedRedisValue Auth(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Auth(RedisClient client, in RedisRequest request) { if (request.GetString(1) == Password) { @@ -79,7 +187,7 @@ protected virtual TypedRedisValue Auth(RedisClient client, RedisRequest request) } [RedisCommand(-1)] - protected virtual TypedRedisValue Hello(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Hello(RedisClient client, in RedisRequest request) { var protocol = client.Protocol; bool isAuthed = client.IsAuthenticated; @@ -90,19 +198,18 @@ protected virtual TypedRedisValue Hello(RedisClient client, RedisRequest request switch (protover) { case 2: - case 3: protocol = RedisProtocol.Resp2; break; - /* case 3: // this client does not currently support RESP3 + case 3: // this client does not currently support RESP3 protocol = RedisProtocol.Resp3; - break; */ + break; default: return TypedRedisValue.Error("NOPROTO unsupported protocol version"); } for (int i = 2; i < request.Count && request.TryGetCommandBytes(i, out var key); i++) { - int remaining = request.Count - (i + 2); + int remaining = request.Count - (i + 1); TypedRedisValue ArgFail() => TypedRedisValue.Error($"ERR Syntax error in HELLO option '{key.ToString().ToLower()}'\""); if (key.Equals(Literals.AUTH)) { @@ -118,6 +225,10 @@ protected virtual TypedRedisValue Hello(RedisClient client, RedisRequest request if (remaining < 1) return ArgFail(); name = request.GetString(++i); } + else + { + return ArgFail(); + } } } @@ -126,7 +237,7 @@ protected virtual TypedRedisValue Hello(RedisClient client, RedisRequest request client.IsAuthenticated = isAuthed; client.Name = name; - var reply = TypedRedisValue.Rent(14, out var span); + var reply = TypedRedisValue.Rent(14, out var span, ResultType.Map); span[0] = TypedRedisValue.BulkString("server"); span[1] = TypedRedisValue.BulkString("redis"); span[2] = TypedRedisValue.BulkString("version"); @@ -140,12 +251,12 @@ protected virtual TypedRedisValue Hello(RedisClient client, RedisRequest request span[10] = TypedRedisValue.BulkString("role"); span[11] = TypedRedisValue.BulkString("master"); span[12] = TypedRedisValue.BulkString("modules"); - span[13] = TypedRedisValue.EmptyArray; + span[13] = TypedRedisValue.EmptyArray(ResultType.Array); return reply; } [RedisCommand(-3)] - protected virtual TypedRedisValue Sadd(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Sadd(RedisClient client, in RedisRequest request) { int added = 0; var key = request.GetKey(1); @@ -156,10 +267,10 @@ protected virtual TypedRedisValue Sadd(RedisClient client, RedisRequest request) } return TypedRedisValue.Integer(added); } - protected virtual bool Sadd(int database, RedisKey key, RedisValue value) => throw new NotSupportedException(); + protected virtual bool Sadd(int database, in RedisKey key, in RedisValue value) => throw new NotSupportedException(); [RedisCommand(-3)] - protected virtual TypedRedisValue Srem(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Srem(RedisClient client, in RedisRequest request) { int removed = 0; var key = request.GetKey(1); @@ -170,39 +281,142 @@ protected virtual TypedRedisValue Srem(RedisClient client, RedisRequest request) } return TypedRedisValue.Integer(removed); } - protected virtual bool Srem(int database, RedisKey key, RedisValue value) => throw new NotSupportedException(); + protected virtual bool Srem(int database, in RedisKey key, in RedisValue value) => throw new NotSupportedException(); [RedisCommand(2)] - protected virtual TypedRedisValue Spop(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Spop(RedisClient client, in RedisRequest request) => TypedRedisValue.BulkString(Spop(client.Database, request.GetKey(1))); - protected virtual RedisValue Spop(int database, RedisKey key) => throw new NotSupportedException(); + protected virtual RedisValue Spop(int database, in RedisKey key) => throw new NotSupportedException(); [RedisCommand(2)] - protected virtual TypedRedisValue Scard(RedisClient client, RedisRequest request) - => TypedRedisValue.Integer(Scard(client.Database, request.GetKey(1))); + protected virtual TypedRedisValue Scard(RedisClient client, in RedisRequest request) + => TypedRedisValue.Integer(Scard(client.Database, request.GetKey(1, KeyFlags.ReadOnly))); + + protected virtual long Scard(int database, in RedisKey key) => throw new NotSupportedException(); - protected virtual long Scard(int database, RedisKey key) => throw new NotSupportedException(); + [RedisCommand(3)] + protected virtual TypedRedisValue Sismember(RedisClient client, in RedisRequest request) + => Sismember(client.Database, request.GetKey(1, KeyFlags.ReadOnly), request.GetValue(2)) ? TypedRedisValue.One : TypedRedisValue.Zero; + + protected virtual bool Sismember(int database, in RedisKey key, in RedisValue value) => throw new NotSupportedException(); [RedisCommand(3)] - protected virtual TypedRedisValue Sismember(RedisClient client, RedisRequest request) - => Sismember(client.Database, request.GetKey(1), request.GetValue(2)) ? TypedRedisValue.One : TypedRedisValue.Zero; + protected virtual TypedRedisValue Rename(RedisClient client, in RedisRequest request) + { + RedisKey oldKey = request.GetKey(1), newKey = request.GetKey(2); + return oldKey == newKey || Rename(client.Database, oldKey, newKey) ? TypedRedisValue.OK : TypedRedisValue.Error("ERR no such key"); + } + + protected virtual bool Rename(int database, in RedisKey oldKey, in RedisKey newKey) + { + // can implement with Exists/Del/Set + if (!Exists(database, oldKey)) return false; + Del(database, newKey); + Set(database, newKey, Get(database, oldKey)); + Del(database, oldKey); + return true; + } + + [RedisCommand(4)] + protected virtual TypedRedisValue SetEx(RedisClient client, in RedisRequest request) + { + RedisKey key = request.GetKey(1); + int seconds = request.GetInt32(2); + var value = request.GetValue(3); + SetEx(client.Database, key, TimeSpan.FromSeconds(seconds), value); + return TypedRedisValue.OK; + } + + [RedisCommand(-2)] + protected virtual TypedRedisValue Touch(RedisClient client, in RedisRequest request) + { + for (int i = 1; i < request.Count; i++) + { + Touch(client.Database, request.GetKey(i)); + } + + return TypedRedisValue.OK; + } + + [RedisCommand(-2)] + protected virtual TypedRedisValue Watch(RedisClient client, in RedisRequest request) + { + for (int i = 1; i < request.Count; i++) + { + var key = request.GetKey(i, KeyFlags.ReadOnly); + if (!client.Watch(key)) + return TypedRedisValue.Error("WATCH inside MULTI is not allowed"); + } + + return TypedRedisValue.OK; + } + + [RedisCommand(1)] + protected virtual TypedRedisValue Unwatch(RedisClient client, in RedisRequest request) + { + return client.Unwatch() ? TypedRedisValue.OK : TypedRedisValue.Error("UNWATCH inside MULTI is not allowed"); + } + + [RedisCommand(1)] + protected virtual TypedRedisValue Multi(RedisClient client, in RedisRequest request) + { + return client.Multi() ? TypedRedisValue.OK : TypedRedisValue.Error("MULTI calls can not be nested"); + } + + [RedisCommand(1)] + protected virtual TypedRedisValue Discard(RedisClient client, in RedisRequest request) + { + return client.Discard() ? TypedRedisValue.OK : TypedRedisValue.Error("DISCARD without MULTI"); + } - protected virtual bool Sismember(int database, RedisKey key, RedisValue value) => throw new NotSupportedException(); + [RedisCommand(1)] + protected virtual TypedRedisValue Exec(RedisClient client, in RedisRequest request) + { + var exec = client.FlushMulti(out var commands); + switch (exec) + { + case RedisClient.ExecResult.NotInTransaction: + return TypedRedisValue.Error("EXEC without MULTI"); + case RedisClient.ExecResult.WatchConflict: + return TypedRedisValue.NullArray(ResultType.Array); + case RedisClient.ExecResult.AbortedByError: + return TypedRedisValue.Error("EXECABORT Transaction discarded because of previous errors."); + } + Debug.Assert(exec is RedisClient.ExecResult.CommandsReturned); + + var results = TypedRedisValue.Rent(commands.Length, out var span, ResultType.Array); + int index = 0; + foreach (var cmd in commands) + { + // TODO:this is the bit we can't do just yet, until we can freely parse results + // RedisRequest inner = // ... + // inner = inner.WithClient(client); + // results[index++] = Execute(client, cmd); + span[index++] = TypedRedisValue.Error($"ERR transactions not yet implemented, sorry; ignoring {Encoding.ASCII.GetString(cmd)}"); + } + return results; + } + + protected virtual void SetEx(int database, in RedisKey key, TimeSpan timeout, in RedisValue value) + { + Set(database, key, value); + Expire(database, key, timeout); + } [RedisCommand(3, "client", "setname", LockFree = true)] - protected virtual TypedRedisValue ClientSetname(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue ClientSetname(RedisClient client, in RedisRequest request) { client.Name = request.GetString(2); return TypedRedisValue.OK; } [RedisCommand(2, "client", "getname", LockFree = true)] - protected virtual TypedRedisValue ClientGetname(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue ClientGetname(RedisClient client, in RedisRequest request) => TypedRedisValue.BulkString(client.Name); [RedisCommand(3, "client", "reply", LockFree = true)] - protected virtual TypedRedisValue ClientReply(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue ClientReply(RedisClient client, in RedisRequest request) { if (request.IsString(2, "on")) client.SkipReplies = -1; // reply to nothing else if (request.IsString(2, "off")) client.SkipReplies = 0; // reply to everything @@ -212,15 +426,341 @@ protected virtual TypedRedisValue ClientReply(RedisClient client, RedisRequest r } [RedisCommand(2, "client", "id", LockFree = true)] - protected virtual TypedRedisValue ClientId(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue ClientId(RedisClient client, in RedisRequest request) => TypedRedisValue.Integer(client.Id); + private bool IsClusterEnabled(out TypedRedisValue fault) + { + if (ServerType == ServerType.Cluster) + { + fault = default; + return true; + } + fault = TypedRedisValue.Error("ERR This instance has cluster support disabled"); + return false; + } + + [RedisCommand(2, nameof(RedisCommand.CLUSTER), subcommand: "nodes", LockFree = true)] + protected virtual TypedRedisValue ClusterNodes(RedisClient client, in RedisRequest request) + { + if (!IsClusterEnabled(out TypedRedisValue fault)) return fault; + + var sb = new StringBuilder(); + foreach (var pair in _nodes.OrderBy(x => x.Key, EndPointComparer.Instance)) + { + var node = pair.Value; + sb.Append(node.Id).Append(" ").Append(node.Host).Append(":").Append(node.Port).Append("@1").Append(node.Port).Append(" "); + if (node == client.Node) + { + sb.Append("myself,"); + } + sb.Append("master - 0 0 1 connected"); + foreach (var range in node.Slots) + { + sb.Append(" ").Append(range.ToString()); + } + sb.AppendLine(); + } + return TypedRedisValue.BulkString(sb.ToString()); + } + + [RedisCommand(2, nameof(RedisCommand.CLUSTER), subcommand: "slots", LockFree = true)] + protected virtual TypedRedisValue ClusterSlots(RedisClient client, in RedisRequest request) + { + if (!IsClusterEnabled(out TypedRedisValue fault)) return fault; + + int count = 0, index = 0; + foreach (var pair in _nodes) + { + count += pair.Value.Slots.Length; + } + var slots = TypedRedisValue.Rent(count, out var slotsSpan, ResultType.Array); + foreach (var pair in _nodes.OrderBy(x => x.Key, EndPointComparer.Instance)) + { + string host = GetHost(pair.Key, out int port); + foreach (var range in pair.Value.Slots) + { + if (index >= count) break; // someone changed things while we were working + slotsSpan[index++] = TypedRedisValue.Rent(3, out var slotSpan, ResultType.Array); + slotSpan[0] = TypedRedisValue.Integer(range.From); + slotSpan[1] = TypedRedisValue.Integer(range.To); + slotSpan[2] = TypedRedisValue.Rent(4, out var nodeSpan, ResultType.Array); + nodeSpan[0] = TypedRedisValue.BulkString(host); + nodeSpan[1] = TypedRedisValue.Integer(port); + nodeSpan[2] = TypedRedisValue.BulkString(pair.Value.Id); + nodeSpan[3] = TypedRedisValue.EmptyArray(ResultType.Array); + } + } + return slots; + } + + private sealed class EndPointComparer : IComparer + { + private EndPointComparer() { } + public static readonly EndPointComparer Instance = new(); + + public int Compare(EndPoint x, EndPoint y) + { + if (x is null) return y is null ? 0 : -1; + if (y is null) return 1; + if (x is IPEndPoint ipX && y is IPEndPoint ipY) + { + // ignore the address, go by port alone + return ipX.Port.CompareTo(ipY.Port); + } + if (x is DnsEndPoint dnsX && y is DnsEndPoint dnsY) + { + var delta = dnsX.Host.CompareTo(dnsY.Host, StringComparison.Ordinal); + if (delta != 0) return delta; + return dnsX.Port.CompareTo(dnsY.Port); + } + + return 0; // whatever + } + } + + public static string GetHost(EndPoint endpoint, out int port) + { + if (endpoint is IPEndPoint ip) + { + port = ip.Port; + return ip.Address.ToString(); + } + if (endpoint is DnsEndPoint dns) + { + port = dns.Port; + return dns.Host; + } + throw new NotSupportedException("Unknown endpoint type: " + endpoint.GetType().Name); + } + + public sealed class Node + { + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(Host).Append(":").Append(Port).Append(" ("); + var slots = _slots; + if (slots is null) + { + sb.Append("all keys"); + } + else + { + bool first = true; + foreach (var slot in Slots) + { + if (!first) sb.Append(","); + sb.Append(slot); + first = false; + } + + if (first) sb.Append("empty"); + } + sb.Append(")"); + return sb.ToString(); + } + + public string Host { get; } + + public int Port { get; } + public string Id { get; } = NewId(); + + private SlotRange[] _slots; + + private readonly RedisServer _server; + public Node(RedisServer server, EndPoint endpoint) + { + Host = GetHost(endpoint, out var port); + Port = port; + _server = server; + } + + public void UpdateSlots(SlotRange[] slots) => _slots = slots; + public ReadOnlySpan Slots => _slots ?? SlotRange.SharedAllSlots; + public bool CheckCrossSlot => _server.CheckCrossSlot; + + public bool HasSlot(int hashSlot) + { + var slots = _slots; + if (slots is null) return true; // all nodes + foreach (var slot in slots) + { + if (slot.Includes(hashSlot)) return true; + } + return false; + } + + public bool HasSlot(in RedisKey key) + { + var slots = _slots; + if (slots is null) return true; // all nodes + var hashSlot = GetHashSlot(key); + foreach (var slot in slots) + { + if (slot.Includes(hashSlot)) return true; + } + return false; + } + + public bool HasSlot(ReadOnlySpan key) + { + var slots = _slots; + if (slots is null) return true; // all nodes + var hashSlot = ServerSelectionStrategy.GetClusterSlot(key); + foreach (var slot in slots) + { + if (slot.Includes(hashSlot)) return true; + } + return false; + } + + private static string NewId() + { + Span data = stackalloc char[40]; +#if NET + var rand = Random.Shared; +#else + var rand = new Random(); +#endif + ReadOnlySpan alphabet = "0123456789abcdef"; + for (int i = 0; i < data.Length; i++) + { + data[i] = alphabet[rand.Next(alphabet.Length)]; + } + return data.ToString(); + } + + public void AddSlot(int hashSlot) + { + SlotRange[] oldSlots, newSlots; + do + { + oldSlots = _slots; + newSlots = oldSlots; + if (oldSlots is null) + { + newSlots = [new SlotRange(hashSlot, hashSlot)]; + } + else + { + bool found = false; + int index = 0; + foreach (var slot in oldSlots) + { + if (slot.Includes(hashSlot)) return; // already covered + if (slot.To == hashSlot - 1) + { + // extend the range + newSlots = new SlotRange[oldSlots.Length]; + oldSlots.AsSpan().CopyTo(newSlots); + newSlots[index] = new SlotRange(slot.From, hashSlot); + found = true; + break; + } + + index++; + } + + if (!found) + { + newSlots = [..oldSlots, new SlotRange(hashSlot, hashSlot)]; + Array.Sort(newSlots); + } + } + } + while (Interlocked.CompareExchange(ref _slots, newSlots, oldSlots) != oldSlots); + } + + public bool RemoveSlot(int hashSlot) + { + SlotRange[] oldSlotsRaw, newSlots; + do + { + oldSlotsRaw = _slots; + newSlots = oldSlotsRaw; + // avoid the implicit null "all slots" usage + var oldSlots = oldSlotsRaw ?? SlotRange.SharedAllSlots; + bool found = false; + int index = 0; + foreach (var s in oldSlots) + { + if (s.Includes(hashSlot)) + { + found = true; + var oldSpan = oldSlots.AsSpan(); + if (s.IsSingleSlot) + { + // remove it + newSlots = new SlotRange[oldSlots.Length - 1]; + if (index > 0) oldSpan.Slice(0, index).CopyTo(newSlots); + if (index < oldSlots.Length - 1) oldSpan.Slice(index + 1).CopyTo(newSlots.AsSpan(index)); + } + else if (s.From == hashSlot) + { + // truncate the start + newSlots = new SlotRange[oldSlots.Length]; + oldSpan.CopyTo(newSlots); + newSlots[index] = new SlotRange(s.From + 1, s.To); + } + else if (s.To == hashSlot) + { + // truncate the end + newSlots = new SlotRange[oldSlots.Length]; + oldSpan.CopyTo(newSlots); + newSlots[index] = new SlotRange(s.From, s.To - 1); + } + else + { + // split it + newSlots = new SlotRange[oldSlots.Length + 1]; + if (index > 0) oldSpan.Slice(0, index).CopyTo(newSlots); + newSlots[index] = new SlotRange(s.From, hashSlot - 1); + newSlots[index + 1] = new SlotRange(hashSlot + 1, s.To); + if (index < oldSlots.Length - 1) oldSpan.Slice(index + 1).CopyTo(newSlots.AsSpan(index + 2)); + } + break; + } + index++; + } + + if (!found) return false; + } + while (Interlocked.CompareExchange(ref _slots, newSlots, oldSlotsRaw) != oldSlotsRaw); + + return true; + } + + public void AssertKey(in RedisKey key) + { + var slots = _slots; + if (slots is not null) + { + var hashSlot = GetHashSlot(key); + if (!HasSlot(hashSlot)) KeyMovedException.Throw(hashSlot); + } + } + + public void Touch(int db, in RedisKey key) => _server.Touch(db, key); + } + + public virtual bool CheckCrossSlot => ServerType == ServerType.Cluster; + + protected override Node GetNode(int hashSlot) + { + foreach (var pair in _nodes) + { + if (pair.Value.HasSlot(hashSlot)) return pair.Value; + } + return base.GetNode(hashSlot); + } + [RedisCommand(-1)] - protected virtual TypedRedisValue Cluster(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Sentinel(RedisClient client, in RedisRequest request) => request.CommandNotFound(); [RedisCommand(-3)] - protected virtual TypedRedisValue Lpush(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Lpush(RedisClient client, in RedisRequest request) { var key = request.GetKey(1); long length = -1; @@ -232,7 +772,7 @@ protected virtual TypedRedisValue Lpush(RedisClient client, RedisRequest request } [RedisCommand(-3)] - protected virtual TypedRedisValue Rpush(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Rpush(RedisClient client, in RedisRequest request) { var key = request.GetKey(1); long length = -1; @@ -244,36 +784,36 @@ protected virtual TypedRedisValue Rpush(RedisClient client, RedisRequest request } [RedisCommand(2)] - protected virtual TypedRedisValue Lpop(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Lpop(RedisClient client, in RedisRequest request) => TypedRedisValue.BulkString(Lpop(client.Database, request.GetKey(1))); [RedisCommand(2)] - protected virtual TypedRedisValue Rpop(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Rpop(RedisClient client, in RedisRequest request) => TypedRedisValue.BulkString(Rpop(client.Database, request.GetKey(1))); [RedisCommand(2)] - protected virtual TypedRedisValue Llen(RedisClient client, RedisRequest request) - => TypedRedisValue.Integer(Llen(client.Database, request.GetKey(1))); + protected virtual TypedRedisValue Llen(RedisClient client, in RedisRequest request) + => TypedRedisValue.Integer(Llen(client.Database, request.GetKey(1, KeyFlags.ReadOnly))); - protected virtual long Lpush(int database, RedisKey key, RedisValue value) => throw new NotSupportedException(); - protected virtual long Rpush(int database, RedisKey key, RedisValue value) => throw new NotSupportedException(); - protected virtual long Llen(int database, RedisKey key) => throw new NotSupportedException(); - protected virtual RedisValue Rpop(int database, RedisKey key) => throw new NotSupportedException(); - protected virtual RedisValue Lpop(int database, RedisKey key) => throw new NotSupportedException(); + protected virtual long Lpush(int database, in RedisKey key, in RedisValue value) => throw new NotSupportedException(); + protected virtual long Rpush(int database, in RedisKey key, in RedisValue value) => throw new NotSupportedException(); + protected virtual long Llen(int database, in RedisKey key) => throw new NotSupportedException(); + protected virtual RedisValue Rpop(int database, in RedisKey key) => throw new NotSupportedException(); + protected virtual RedisValue Lpop(int database, in RedisKey key) => throw new NotSupportedException(); [RedisCommand(4)] - protected virtual TypedRedisValue LRange(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue LRange(RedisClient client, in RedisRequest request) { - var key = request.GetKey(1); + var key = request.GetKey(1, KeyFlags.ReadOnly); long start = request.GetInt64(2), stop = request.GetInt64(3); var len = Llen(client.Database, key); - if (len == 0) return TypedRedisValue.EmptyArray; + if (len == 0) return TypedRedisValue.EmptyArray(ResultType.Array); if (start < 0) start = len + start; if (stop < 0) stop = len + stop; - if (stop < 0 || start >= len || stop < start) return TypedRedisValue.EmptyArray; + if (stop < 0 || start >= len || stop < start) return TypedRedisValue.EmptyArray(ResultType.Array); if (start < 0) start = 0; else if (start >= len) start = len - 1; @@ -281,11 +821,11 @@ protected virtual TypedRedisValue LRange(RedisClient client, RedisRequest reques if (stop < 0) stop = 0; else if (stop >= len) stop = len - 1; - var arr = TypedRedisValue.Rent(checked((int)((stop - start) + 1)), out var span); + var arr = TypedRedisValue.Rent(checked((int)((stop - start) + 1)), out var span, ResultType.Array); LRange(client.Database, key, start, span); return arr; } - protected virtual void LRange(int database, RedisKey key, long start, Span arr) => throw new NotSupportedException(); + protected virtual void LRange(int database, in RedisKey key, long start, Span arr) => throw new NotSupportedException(); protected virtual void OnUpdateServerConfiguration() { } protected RedisConfig ServerConfiguration { get; } = RedisConfig.Create(); @@ -319,16 +859,16 @@ internal int CountMatch(string pattern) } } [RedisCommand(3, "config", "get", LockFree = true)] - protected virtual TypedRedisValue Config(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Config(RedisClient client, in RedisRequest request) { var pattern = request.GetString(2); OnUpdateServerConfiguration(); var config = ServerConfiguration; var matches = config.CountMatch(pattern); - if (matches == 0) return TypedRedisValue.EmptyArray; + if (matches == 0) return TypedRedisValue.EmptyArray(ResultType.Map); - var arr = TypedRedisValue.Rent(2 * matches, out var span); + var arr = TypedRedisValue.Rent(2 * matches, out var span, ResultType.Map); int index = 0; foreach (var pair in config.Wrapped) { @@ -347,23 +887,23 @@ protected virtual TypedRedisValue Config(RedisClient client, RedisRequest reques } [RedisCommand(2, LockFree = true)] - protected virtual TypedRedisValue Echo(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Echo(RedisClient client, in RedisRequest request) => TypedRedisValue.BulkString(request.GetValue(1)); [RedisCommand(2)] - protected virtual TypedRedisValue Exists(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Exists(RedisClient client, in RedisRequest request) { int count = 0; var db = client.Database; for (int i = 1; i < request.Count; i++) { - if (Exists(db, request.GetKey(i))) + if (Exists(db, request.GetKey(i, KeyFlags.ReadOnly))) count++; } return TypedRedisValue.Integer(count); } - protected virtual bool Exists(int database, RedisKey key) + protected virtual bool Exists(int database, in RedisKey key) { try { @@ -373,32 +913,32 @@ protected virtual bool Exists(int database, RedisKey key) } [RedisCommand(2)] - protected virtual TypedRedisValue Get(RedisClient client, RedisRequest request) - => TypedRedisValue.BulkString(Get(client.Database, request.GetKey(1))); + protected virtual TypedRedisValue Get(RedisClient client, in RedisRequest request) + => TypedRedisValue.BulkString(Get(client.Database, request.GetKey(1, KeyFlags.ReadOnly))); - protected virtual RedisValue Get(int database, RedisKey key) => throw new NotSupportedException(); + protected virtual RedisValue Get(int database, in RedisKey key) => throw new NotSupportedException(); [RedisCommand(3)] - protected virtual TypedRedisValue Set(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Set(RedisClient client, in RedisRequest request) { Set(client.Database, request.GetKey(1), request.GetValue(2)); return TypedRedisValue.OK; } - protected virtual void Set(int database, RedisKey key, RedisValue value) => throw new NotSupportedException(); + protected virtual void Set(int database, in RedisKey key, in RedisValue value) => throw new NotSupportedException(); [RedisCommand(1)] - protected new virtual TypedRedisValue Shutdown(RedisClient client, RedisRequest request) + protected new virtual TypedRedisValue Shutdown(RedisClient client, in RedisRequest request) { DoShutdown(ShutdownReason.ClientInitiated); return TypedRedisValue.OK; } [RedisCommand(2)] - protected virtual TypedRedisValue Strlen(RedisClient client, RedisRequest request) - => TypedRedisValue.Integer(Strlen(client.Database, request.GetKey(1))); + protected virtual TypedRedisValue Strlen(RedisClient client, in RedisRequest request) + => TypedRedisValue.Integer(Strlen(client.Database, request.GetKey(1, KeyFlags.ReadOnly))); - protected virtual long Strlen(int database, RedisKey key) => Get(database, key).Length(); + protected virtual long Strlen(int database, in RedisKey key) => Get(database, key).Length(); [RedisCommand(-2)] - protected virtual TypedRedisValue Del(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Del(RedisClient client, in RedisRequest request) { int count = 0; for (int i = 1; i < request.Count; i++) @@ -408,16 +948,65 @@ protected virtual TypedRedisValue Del(RedisClient client, RedisRequest request) } return TypedRedisValue.Integer(count); } - protected virtual bool Del(int database, RedisKey key) => throw new NotSupportedException(); + protected virtual bool Del(int database, in RedisKey key) => throw new NotSupportedException(); + + [RedisCommand(2)] + protected virtual TypedRedisValue GetDel(RedisClient client, in RedisRequest request) + { + var key = request.GetKey(1); + var value = Get(client.Database, key); + if (!value.IsNull) Del(client.Database, key); + return TypedRedisValue.BulkString(value); + } [RedisCommand(1)] - protected virtual TypedRedisValue Dbsize(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Dbsize(RedisClient client, in RedisRequest request) => TypedRedisValue.Integer(Dbsize(client.Database)); protected virtual long Dbsize(int database) => throw new NotSupportedException(); + [RedisCommand(3)] + protected virtual TypedRedisValue Expire(RedisClient client, in RedisRequest request) + { + var key = request.GetKey(1); + var seconds = request.GetInt32(2); + return TypedRedisValue.Integer(Expire(client.Database, key, TimeSpan.FromSeconds(seconds)) ? 1 : 0); + } + + [RedisCommand(3)] + protected virtual TypedRedisValue PExpire(RedisClient client, in RedisRequest request) + { + var key = request.GetKey(1); + var millis = request.GetInt64(2); + return TypedRedisValue.Integer(Expire(client.Database, key, TimeSpan.FromMilliseconds(millis)) ? 1 : 0); + } + + [RedisCommand(2)] + protected virtual TypedRedisValue Ttl(RedisClient client, in RedisRequest request) + { + var key = request.GetKey(1, KeyFlags.ReadOnly); + var ttl = Ttl(client.Database, key); + if (ttl == null || ttl <= TimeSpan.Zero) return TypedRedisValue.Integer(-2); + if (ttl == TimeSpan.MaxValue) return TypedRedisValue.Integer(-1); + return TypedRedisValue.Integer((int)ttl.Value.TotalSeconds); + } + + protected virtual TimeSpan? Ttl(int database, in RedisKey key) => throw new NotSupportedException(); + + [RedisCommand(2)] + protected virtual TypedRedisValue Pttl(RedisClient client, in RedisRequest request) + { + var key = request.GetKey(1, KeyFlags.ReadOnly); + var ttl = Ttl(client.Database, key); + if (ttl == null || ttl <= TimeSpan.Zero) return TypedRedisValue.Integer(-2); + if (ttl == TimeSpan.MaxValue) return TypedRedisValue.Integer(-1); + return TypedRedisValue.Integer((long)ttl.Value.TotalMilliseconds); + } + + protected virtual bool Expire(int database, in RedisKey key, TimeSpan timeout) => throw new NotSupportedException(); + [RedisCommand(1)] - protected virtual TypedRedisValue Flushall(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Flushall(RedisClient client, in RedisRequest request) { var count = Databases; for (int i = 0; i < count; i++) @@ -428,7 +1017,7 @@ protected virtual TypedRedisValue Flushall(RedisClient client, RedisRequest requ } [RedisCommand(1)] - protected virtual TypedRedisValue Flushdb(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Flushdb(RedisClient client, in RedisRequest request) { Flushdb(client.Database); return TypedRedisValue.OK; @@ -436,7 +1025,7 @@ protected virtual TypedRedisValue Flushdb(RedisClient client, RedisRequest reque protected virtual void Flushdb(int database) => throw new NotSupportedException(); [RedisCommand(-1, LockFree = true, MaxArgs = 2)] - protected virtual TypedRedisValue Info(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Info(RedisClient client, in RedisRequest request) { var info = Info(request.Count == 1 ? null : request.GetString(1)); return TypedRedisValue.BulkString(info); @@ -458,18 +1047,21 @@ bool IsMatch(string section) => string.IsNullOrWhiteSpace(selected) } [RedisCommand(2)] - protected virtual TypedRedisValue Keys(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Keys(RedisClient client, in RedisRequest request) { List found = null; - foreach (var key in Keys(client.Database, request.GetKey(1))) + bool checkSlot = ServerType is ServerType.Cluster; + var node = client.Node ?? DefaultNode; + foreach (var key in Keys(client.Database, request.GetKey(1, flags: KeyFlags.NoSlotCheck | KeyFlags.ReadOnly))) { + if (checkSlot && !node.HasSlot(key)) continue; if (found == null) found = new List(); found.Add(TypedRedisValue.BulkString(key.AsRedisValue())); } - if (found == null) return TypedRedisValue.EmptyArray; - return TypedRedisValue.MultiBulk(found); + if (found == null) return TypedRedisValue.EmptyArray(ResultType.Array); + return TypedRedisValue.MultiBulk(found, ResultType.Array); } - protected virtual IEnumerable Keys(int database, RedisKey pattern) => throw new NotSupportedException(); + protected virtual IEnumerable Keys(int database, in RedisKey pattern) => throw new NotSupportedException(); private static readonly Version s_DefaultServerVersion = new(1, 0, 0); @@ -553,25 +1145,25 @@ StringBuilder AddHeader() } [RedisCommand(2, "memory", "purge")] - protected virtual TypedRedisValue MemoryPurge(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue MemoryPurge(RedisClient client, in RedisRequest request) { GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); return TypedRedisValue.OK; } [RedisCommand(-2)] - protected virtual TypedRedisValue Mget(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Mget(RedisClient client, in RedisRequest request) { int argCount = request.Count; - var arr = TypedRedisValue.Rent(argCount - 1, out var span); + var arr = TypedRedisValue.Rent(argCount - 1, out var span, ResultType.Map); var db = client.Database; for (int i = 1; i < argCount; i++) { - span[i - 1] = TypedRedisValue.BulkString(Get(db, request.GetKey(i))); + span[i - 1] = TypedRedisValue.BulkString(Get(db, request.GetKey(i, KeyFlags.ReadOnly))); } return arr; } [RedisCommand(-3)] - protected virtual TypedRedisValue Mset(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Mset(RedisClient client, in RedisRequest request) { int argCount = request.Count; var db = client.Database; @@ -582,28 +1174,28 @@ protected virtual TypedRedisValue Mset(RedisClient client, RedisRequest request) return TypedRedisValue.OK; } [RedisCommand(-1, LockFree = true, MaxArgs = 2)] - protected virtual TypedRedisValue Ping(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Ping(RedisClient client, in RedisRequest request) => TypedRedisValue.SimpleString(request.Count == 1 ? "PONG" : request.GetString(1)); [RedisCommand(1, LockFree = true)] - protected virtual TypedRedisValue Quit(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Quit(RedisClient client, in RedisRequest request) { RemoveClient(client); return TypedRedisValue.OK; } [RedisCommand(1, LockFree = true)] - protected virtual TypedRedisValue Role(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Role(RedisClient client, in RedisRequest request) { - var arr = TypedRedisValue.Rent(3, out var span); + var arr = TypedRedisValue.Rent(3, out var span, ResultType.Array); span[0] = TypedRedisValue.BulkString("master"); span[1] = TypedRedisValue.Integer(0); - span[2] = TypedRedisValue.EmptyArray; + span[2] = TypedRedisValue.EmptyArray(ResultType.Array); return arr; } [RedisCommand(2, LockFree = true)] - protected virtual TypedRedisValue Select(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Select(RedisClient client, in RedisRequest request) { var raw = request.GetValue(1); if (!raw.IsInteger) return TypedRedisValue.Error("ERR invalid DB index"); @@ -614,15 +1206,15 @@ protected virtual TypedRedisValue Select(RedisClient client, RedisRequest reques } [RedisCommand(-2)] - protected virtual TypedRedisValue Subscribe(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Subscribe(RedisClient client, in RedisRequest request) => SubscribeImpl(client, request); [RedisCommand(-2)] - protected virtual TypedRedisValue Unsubscribe(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Unsubscribe(RedisClient client, in RedisRequest request) => SubscribeImpl(client, request); - private TypedRedisValue SubscribeImpl(RedisClient client, RedisRequest request) + private TypedRedisValue SubscribeImpl(RedisClient client, in RedisRequest request) { - var reply = TypedRedisValue.Rent(3 * (request.Count - 1), out var span); + var reply = TypedRedisValue.Rent(3 * (request.Count - 1), out var span, ResultType.Array); int index = 0; request.TryGetCommandBytes(0, out var cmd); var cmdString = TypedRedisValue.BulkString(cmd.ToArray()); @@ -656,13 +1248,13 @@ private static readonly CommandBytes private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); [RedisCommand(1, LockFree = true)] - protected virtual TypedRedisValue Time(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Time(RedisClient client, in RedisRequest request) { var delta = Time() - UnixEpoch; var ticks = delta.Ticks; var seconds = ticks / TimeSpan.TicksPerSecond; var micros = (ticks % TimeSpan.TicksPerSecond) / (TimeSpan.TicksPerMillisecond / 1000); - var reply = TypedRedisValue.Rent(2, out var span); + var reply = TypedRedisValue.Rent(2, out var span, ResultType.Array); span[0] = TypedRedisValue.BulkString(seconds); span[1] = TypedRedisValue.BulkString(micros); return reply; @@ -670,21 +1262,25 @@ protected virtual TypedRedisValue Time(RedisClient client, RedisRequest request) protected virtual DateTime Time() => DateTime.UtcNow; [RedisCommand(-2)] - protected virtual TypedRedisValue Unlink(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Unlink(RedisClient client, in RedisRequest request) => Del(client, request); [RedisCommand(2)] - protected virtual TypedRedisValue Incr(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Incr(RedisClient client, in RedisRequest request) => TypedRedisValue.Integer(IncrBy(client.Database, request.GetKey(1), 1)); [RedisCommand(2)] - protected virtual TypedRedisValue Decr(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Decr(RedisClient client, in RedisRequest request) => TypedRedisValue.Integer(IncrBy(client.Database, request.GetKey(1), -1)); [RedisCommand(3)] - protected virtual TypedRedisValue IncrBy(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue DecrBy(RedisClient client, in RedisRequest request) + => TypedRedisValue.Integer(IncrBy(client.Database, request.GetKey(1), -request.GetInt64(2))); + + [RedisCommand(3)] + protected virtual TypedRedisValue IncrBy(RedisClient client, in RedisRequest request) => TypedRedisValue.Integer(IncrBy(client.Database, request.GetKey(1), request.GetInt64(2))); - protected virtual long IncrBy(int database, RedisKey key, long delta) + protected virtual long IncrBy(int database, in RedisKey key, long delta) { var value = ((long)Get(database, key)) + delta; Set(database, key, value); diff --git a/toys/StackExchange.Redis.Server/RespServer.cs b/toys/StackExchange.Redis.Server/RespServer.cs index 068154b74..b1611b087 100644 --- a/toys/StackExchange.Redis.Server/RespServer.cs +++ b/toys/StackExchange.Redis.Server/RespServer.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.IO.Pipelines; using System.Linq; @@ -30,13 +31,23 @@ protected RespServer(TextWriter output = null) _commands = BuildCommands(this); } + public HashSet GetCommands() + { + var set = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in _commands) + { + set.Add(kvp.Key.ToString()); + } + return set; + } + private static Dictionary BuildCommands(RespServer server) { static RedisCommandAttribute CheckSignatureAndGetAttribute(MethodInfo method) { if (method.ReturnType != typeof(TypedRedisValue)) return null; var p = method.GetParameters(); - if (p.Length != 2 || p[0].ParameterType != typeof(RedisClient) || p[1].ParameterType != typeof(RedisRequest)) + if (p.Length != 2 || p[0].ParameterType != typeof(RedisClient) || p[1].ParameterType != typeof(RedisRequest).MakeByRefType()) return null; return (RedisCommandAttribute)Attribute.GetCustomAttribute(method, typeof(RedisCommandAttribute)); } @@ -59,7 +70,10 @@ static RedisCommandAttribute CheckSignatureAndGetAttribute(MethodInfo method) { parent = grp.Single(); } - result.Add(new CommandBytes(grp.Key), parent); + + var cmd = new CommandBytes(grp.Key); + Debug.WriteLine($"Registering: {cmd}"); + result.Add(cmd, parent); } return result; } @@ -140,6 +154,7 @@ private RespCommand(in RespCommand parent, RespCommand[] subs) _subcommands = subs; } public bool IsUnknown => _operation == null; + public RespCommand Resolve(in RedisRequest request) { if (request.Count >= 2) @@ -187,22 +202,26 @@ internal int NetArity() } } - private delegate TypedRedisValue RespOperation(RedisClient client, RedisRequest request); + private delegate TypedRedisValue RespOperation(RedisClient client, in RedisRequest request); // for extensibility, so that a subclass can get their own client type // to be used via ListenForConnections - public virtual RedisClient CreateClient() => new RedisClient(); + public virtual RedisClient CreateClient(RedisServer.Node node) => new(node); + + public virtual void OnClientConnected(RedisClient client, object state) { } public int ClientCount => _clientLookup.Count; public int TotalClientCount => _totalClientCount; private int _nextId, _totalClientCount; - public RedisClient AddClient() + + public RedisClient AddClient(RedisServer.Node node, object state) { - var client = CreateClient(); + var client = CreateClient(node); client.Id = Interlocked.Increment(ref _nextId); Interlocked.Increment(ref _totalClientCount); ThrowIfShutdown(); _clientLookup[client.Id] = client; + OnClientConnected(client, state); return client; } @@ -217,6 +236,14 @@ public bool RemoveClient(RedisClient client) return _clientLookup.TryRemove(client.Id, out _); } + protected virtual void Touch(int database, in RedisKey key) + { + foreach (var client in _clientLookup.Values) + { + client.Touch(database, key); + } + } + private readonly TaskCompletionSource _shutdown = TaskSource.Create(null, TaskCreationOptions.RunContinuationsAsynchronously); private bool _isShutdown; protected void ThrowIfShutdown() @@ -240,13 +267,16 @@ protected virtual void Dispose(bool disposing) DoShutdown(ShutdownReason.ServerDisposed); } - public async Task RunClientAsync(IDuplexPipe pipe) + public virtual RedisServer.Node DefaultNode => null; + + public async Task RunClientAsync(IDuplexPipe pipe, RedisServer.Node node = null, object state = null) { Exception fault = null; RedisClient client = null; try { - client = AddClient(); + node ??= DefaultNode; + client = AddClient(node, state); while (!client.Closed) { var readResult = await pipe.Input.ReadAsync().ConfigureAwait(false); @@ -288,7 +318,7 @@ public async Task RunClientAsync(IDuplexPipe pipe) } } } - public void Log(string message) + public virtual void Log(string message) { var output = _output; if (output != null) @@ -300,47 +330,74 @@ public void Log(string message) } } - public static async ValueTask WriteResponseAsync(RedisClient client, PipeWriter output, TypedRedisValue value) + public static async ValueTask WriteResponseAsync(RedisClient client, PipeWriter output, TypedRedisValue value, RedisProtocol protocol) { - static void WritePrefix(PipeWriter ooutput, char pprefix) + static void WritePrefix(PipeWriter output, char prefix) { - var span = ooutput.GetSpan(1); - span[0] = (byte)pprefix; - ooutput.Advance(1); + var span = output.GetSpan(1); + span[0] = (byte)prefix; + output.Advance(1); } if (value.IsNil) return; // not actually a request (i.e. empty/whitespace request) if (client != null && client.ShouldSkipResponse()) return; // intentionally skipping the result - char prefix; - switch (value.Type.ToResp2()) + + var type = value.Type; + if (protocol is RedisProtocol.Resp2 & type is not ResultType.Null) { - case ResultType.Integer: - PhysicalConnection.WriteInteger(output, (long)value.AsRedisValue()); - break; - case ResultType.Error: - prefix = '-'; - goto BasicMessage; - case ResultType.SimpleString: - prefix = '+'; - BasicMessage: - WritePrefix(output, prefix); - var val = (string)value.AsRedisValue(); - var expectedLength = Encoding.UTF8.GetByteCount(val); - PhysicalConnection.WriteRaw(output, val, expectedLength); - PhysicalConnection.WriteCrlf(output); - break; - case ResultType.BulkString: - PhysicalConnection.WriteBulkString(value.AsRedisValue(), output); - break; - case ResultType.Array: - if (value.IsNullArray) - { - PhysicalConnection.WriteMultiBulkHeader(output, -1); - } - else - { + if (type is ResultType.VerbatimString) + { + var s = (string)value.AsRedisValue(); + if (s is { Length: >= 4 } && s[3] == ':') + value = TypedRedisValue.BulkString(s.Substring(4)); + } + type = type.ToResp2(); + } +RetryResp2: + if (protocol is RedisProtocol.Resp3 && value.IsNullValueOrArray) + { + output.Write("_\r\n"u8); + } + else + { + char prefix; + switch (type) + { + case ResultType.Integer: + PhysicalConnection.WriteInteger(output, (long)value.AsRedisValue()); + break; + case ResultType.Error: + prefix = '-'; + goto BasicMessage; + case ResultType.SimpleString: + prefix = '+'; + BasicMessage: + WritePrefix(output, prefix); + var val = (string)value.AsRedisValue(); + var expectedLength = Encoding.UTF8.GetByteCount(val); + PhysicalConnection.WriteRaw(output, val, expectedLength); + PhysicalConnection.WriteCrlf(output); + break; + case ResultType.BulkString: + PhysicalConnection.WriteBulkString(value.AsRedisValue(), output); + break; + case ResultType.Null: + case ResultType.Push when value.IsNullArray: + case ResultType.Map when value.IsNullArray: + case ResultType.Set when value.IsNullArray: + case ResultType.Attribute when value.IsNullArray: + output.Write("_\r\n"u8); + break; + case ResultType.Array when value.IsNullArray: + PhysicalConnection.WriteMultiBulkHeader(output, -1, type); + break; + case ResultType.Push: + case ResultType.Map: + case ResultType.Array: + case ResultType.Set: + case ResultType.Attribute: var segment = value.Segment; - PhysicalConnection.WriteMultiBulkHeader(output, segment.Count); + PhysicalConnection.WriteMultiBulkHeader(output, segment.Count, type); var arr = segment.Array; int offset = segment.Offset; for (int i = 0; i < segment.Count; i++) @@ -350,14 +407,23 @@ static void WritePrefix(PipeWriter ooutput, char pprefix) throw new InvalidOperationException("Array element cannot be nil, index " + i); // note: don't pass client down; this would impact SkipReplies - await WriteResponseAsync(null, output, item); + await WriteResponseAsync(null, output, item, protocol); } - } - break; - default: - throw new InvalidOperationException( - "Unexpected result type: " + value.Type); + break; + default: + // retry with RESP2 + var r2 = type.ToResp2(); + if (r2 != type) + { + Debug.WriteLine($"{type} not handled in RESP3; using {r2} instead"); + goto RetryResp2; + } + + throw new InvalidOperationException( + "Unexpected result type: " + value.Type); + } } + await output.FlushAsync().ConfigureAwait(false); } @@ -380,19 +446,24 @@ private static bool TryParseRequest(Arena arena, ref ReadOnlySequence public ValueTask TryProcessRequestAsync(ref ReadOnlySequence buffer, RedisClient client, PipeWriter output) { - static async ValueTask Awaited(ValueTask wwrite, TypedRedisValue rresponse) + static async ValueTask Awaited(ValueTask write, TypedRedisValue response) { - await wwrite; - rresponse.Recycle(); + await write; + response.Recycle(); return true; } if (!buffer.IsEmpty && TryParseRequest(_arena, ref buffer, out var request)) { + request = request.WithClient(client); TypedRedisValue response; try { response = Execute(client, request); } - finally { _arena.Reset(); } + finally + { + _arena.Reset(); + client.ResetAfterRequest(); + } - var write = WriteResponseAsync(client, output, response); + var write = WriteResponseAsync(client, output, response, client.Protocol); if (!write.IsCompletedSuccessfully) return Awaited(write, response); response.Recycle(); return new ValueTask(true); @@ -411,11 +482,21 @@ public virtual void ResetCounters() _totalCommandsProcesed = _totalErrorCount = _totalClientCount = 0; } - public virtual TypedRedisValue Execute(RedisClient client, RedisRequest request) + public virtual TypedRedisValue OnUnknownCommand(in RedisClient client, in RedisRequest request, ReadOnlySpan command) + { + return TypedRedisValue.Nil; + } + + public virtual TypedRedisValue Execute(RedisClient client, in RedisRequest request) { if (request.Count == 0) return default; // not a request - if (!request.TryGetCommandBytes(0, out var cmdBytes)) return request.CommandNotFound(); + if (!request.TryGetCommandBytes(0, out var cmdBytes)) + { + client.ExecAbort(); + return request.CommandNotFound(); + } + if (cmdBytes.Length == 0) return default; // not a request Interlocked.Increment(ref _totalCommandsProcesed); try @@ -426,8 +507,15 @@ public virtual TypedRedisValue Execute(RedisClient client, RedisRequest request) if (cmd.HasSubCommands) { cmd = cmd.Resolve(request); - if (cmd.IsUnknown) return request.UnknownSubcommandOrArgumentCount(); + if (cmd.IsUnknown) + { + client.ExecAbort(); + return request.UnknownSubcommandOrArgumentCount(); + } } + + if (client.BufferMulti(request, cmdBytes)) return TypedRedisValue.SimpleString("QUEUED"); + if (cmd.LockFree) { result = cmd.Execute(client, request); @@ -442,7 +530,10 @@ public virtual TypedRedisValue Execute(RedisClient client, RedisRequest request) } else { - result = TypedRedisValue.Nil; + client.ExecAbort(); + Span span = stackalloc byte[CommandBytes.MaxLength]; + cmdBytes.CopyTo(span); + result = OnUnknownCommand(client, request, span.Slice(0, cmdBytes.Length)); } if (result.IsNil) @@ -450,9 +541,19 @@ public virtual TypedRedisValue Execute(RedisClient client, RedisRequest request) Log($"missing command: '{request.GetString(0)}'"); return request.CommandNotFound(); } + if (result.Type == ResultType.Error) Interlocked.Increment(ref _totalErrorCount); return result; } + catch (KeyMovedException moved) when (GetNode(moved.HashSlot) is { } node) + { + OnMoved(client, moved.HashSlot, node); + return TypedRedisValue.Error($"MOVED {moved.HashSlot} {node.Host}:{node.Port}"); + } + catch (CrossSlotException) + { + return TypedRedisValue.Error("CROSSSLOT Keys in request don't hash to the same slot"); + } catch (NotSupportedException) { Log($"missing command: '{request.GetString(0)}'"); @@ -463,6 +564,10 @@ public virtual TypedRedisValue Execute(RedisClient client, RedisRequest request) Log($"missing command: '{request.GetString(0)}'"); return request.CommandNotFound(); } + catch (WrongTypeException) + { + return TypedRedisValue.Error("WRONGTYPE Operation against a key holding the wrong kind of value"); + } catch (InvalidCastException) { return TypedRedisValue.Error("WRONGTYPE Operation against a key holding the wrong kind of value"); @@ -474,6 +579,27 @@ public virtual TypedRedisValue Execute(RedisClient client, RedisRequest request) } } + protected virtual void OnMoved(RedisClient client, int hashSlot, RedisServer.Node node) + { + } + + protected virtual RedisServer.Node GetNode(int hashSlot) => null; + + public sealed class KeyMovedException : Exception + { + private KeyMovedException(int hashSlot) => HashSlot = hashSlot; + public int HashSlot { get; } + public static void Throw(int hashSlot) => throw new KeyMovedException(hashSlot); + public static void Throw(in RedisKey key) => throw new KeyMovedException(GetHashSlot(key)); + } + + public sealed class WrongTypeException : Exception + { + } + + protected internal static int GetHashSlot(in RedisKey key) => s_ClusterSelectionStrategy.HashSlot(key); + private static readonly ServerSelectionStrategy s_ClusterSelectionStrategy = new(null) { ServerType = ServerType.Cluster }; + internal static string ToLower(in RawResult value) { var val = value.GetString(); @@ -482,9 +608,9 @@ internal static string ToLower(in RawResult value) } [RedisCommand(1, LockFree = true)] - protected virtual TypedRedisValue Command(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue Command(RedisClient client, in RedisRequest request) { - var results = TypedRedisValue.Rent(_commands.Count, out var span); + var results = TypedRedisValue.Rent(_commands.Count, out var span, ResultType.Array); int index = 0; foreach (var pair in _commands) span[index++] = CommandInfo(pair.Value); @@ -492,24 +618,24 @@ protected virtual TypedRedisValue Command(RedisClient client, RedisRequest reque } [RedisCommand(-2, "command", "info", LockFree = true)] - protected virtual TypedRedisValue CommandInfo(RedisClient client, RedisRequest request) + protected virtual TypedRedisValue CommandInfo(RedisClient client, in RedisRequest request) { - var results = TypedRedisValue.Rent(request.Count - 2, out var span); + var results = TypedRedisValue.Rent(request.Count - 2, out var span, ResultType.Array); for (int i = 2; i < request.Count; i++) { span[i - 2] = request.TryGetCommandBytes(i, out var cmdBytes) && _commands.TryGetValue(cmdBytes, out var cmdInfo) - ? CommandInfo(cmdInfo) : TypedRedisValue.NullArray; + ? CommandInfo(cmdInfo) : TypedRedisValue.NullArray(ResultType.Array); } return results; } private TypedRedisValue CommandInfo(RespCommand command) { - var arr = TypedRedisValue.Rent(6, out var span); + var arr = TypedRedisValue.Rent(6, out var span, ResultType.Array); span[0] = TypedRedisValue.BulkString(command.Command); span[1] = TypedRedisValue.Integer(command.NetArity()); - span[2] = TypedRedisValue.EmptyArray; + span[2] = TypedRedisValue.EmptyArray(ResultType.Array); span[3] = TypedRedisValue.Zero; span[4] = TypedRedisValue.Zero; span[5] = TypedRedisValue.Zero; diff --git a/toys/StackExchange.Redis.Server/TypedRedisValue.cs b/toys/StackExchange.Redis.Server/TypedRedisValue.cs index a67ab8d5a..054cf1fa8 100644 --- a/toys/StackExchange.Redis.Server/TypedRedisValue.cs +++ b/toys/StackExchange.Redis.Server/TypedRedisValue.cs @@ -11,16 +11,16 @@ public readonly struct TypedRedisValue { // note: if this ever becomes exposed on the public API, it should be made so that it clears; // can't trust external callers to clear the space, and using recycle without that is dangerous - internal static TypedRedisValue Rent(int count, out Span span) + internal static TypedRedisValue Rent(int count, out Span span, ResultType type) { if (count == 0) { span = default; - return EmptyArray; + return EmptyArray(type); } var arr = ArrayPool.Shared.Rent(count); span = new Span(arr, 0, count); - return new TypedRedisValue(arr, count); + return new TypedRedisValue(arr, count, type); } /// @@ -36,7 +36,7 @@ internal static TypedRedisValue Rent(int count, out Span span) /// /// Returns whether this value represents a null array. /// - public bool IsNullArray => Type == ResultType.Array && _value.DirectObject == null; + public bool IsNullArray => IsAggregate && _value.DirectObject == null; private readonly RedisValue _value; @@ -76,8 +76,8 @@ public static TypedRedisValue SimpleString(string value) public static TypedRedisValue OK { get; } = SimpleString("OK"); internal static TypedRedisValue Zero { get; } = Integer(0); internal static TypedRedisValue One { get; } = Integer(1); - internal static TypedRedisValue NullArray { get; } = new TypedRedisValue((TypedRedisValue[])null, 0); - internal static TypedRedisValue EmptyArray { get; } = new TypedRedisValue(Array.Empty(), 0); + internal static TypedRedisValue NullArray(ResultType type) => new TypedRedisValue((TypedRedisValue[])null, 0, type); + internal static TypedRedisValue EmptyArray(ResultType type) => new TypedRedisValue([], 0, type); /// /// Gets the array elements as a span. @@ -86,7 +86,7 @@ public ReadOnlySpan Span { get { - if (Type != ResultType.Array) return default; + if (!IsAggregate) return default; var arr = (TypedRedisValue[])_value.DirectObject; if (arr == null) return default; var length = (int)_value.DirectOverlappedBits64; @@ -97,7 +97,7 @@ public ArraySegment Segment { get { - if (Type != ResultType.Array) return default; + if (!IsAggregate) return default; var arr = (TypedRedisValue[])_value.DirectObject; if (arr == null) return default; var length = (int)_value.DirectOverlappedBits64; @@ -105,6 +105,9 @@ public ArraySegment Segment } } + public bool IsAggregate => Type.ToResp2() is ResultType.Array; + public bool IsNullValueOrArray => IsAggregate ? IsNullArray : _value.IsNull; + /// /// Initialize a that represents an integer. /// @@ -116,10 +119,10 @@ public static TypedRedisValue Integer(long value) /// Initialize a from a . /// /// The items to intialize a value from. - public static TypedRedisValue MultiBulk(ReadOnlySpan items) + public static TypedRedisValue MultiBulk(ReadOnlySpan items, ResultType type) { - if (items.IsEmpty) return EmptyArray; - var result = Rent(items.Length, out var span); + if (items.IsEmpty) return EmptyArray(type); + var result = Rent(items.Length, out var span, type); items.CopyTo(span); return result; } @@ -128,14 +131,14 @@ public static TypedRedisValue MultiBulk(ReadOnlySpan items) /// Initialize a from a collection. /// /// The items to intialize a value from. - public static TypedRedisValue MultiBulk(ICollection items) + public static TypedRedisValue MultiBulk(ICollection items, ResultType type) { - if (items == null) return NullArray; + if (items == null) return NullArray(type); int count = items.Count; - if (count == 0) return EmptyArray; + if (count == 0) return EmptyArray(type); var arr = ArrayPool.Shared.Rent(count); items.CopyTo(arr, 0); - return new TypedRedisValue(arr, count); + return new TypedRedisValue(arr, count, type); } /// @@ -145,7 +148,7 @@ public static TypedRedisValue MultiBulk(ICollection items) public static TypedRedisValue BulkString(RedisValue value) => new TypedRedisValue(value, ResultType.BulkString); - private TypedRedisValue(TypedRedisValue[] oversizedItems, int count) + private TypedRedisValue(TypedRedisValue[] oversizedItems, int count, ResultType type) { if (oversizedItems == null) { @@ -157,7 +160,7 @@ private TypedRedisValue(TypedRedisValue[] oversizedItems, int count) if (count == 0) oversizedItems = Array.Empty(); } _value = new RedisValue(oversizedItems, count); - Type = ResultType.Array; + Type = type; } internal void Recycle(int limit = -1) From a0984263869a548800c230d10632603d7b9e9274 Mon Sep 17 00:00:00 2001 From: Philo Date: Mon, 2 Mar 2026 23:01:52 -0800 Subject: [PATCH 429/435] Add Entra ID Auth to Authentication doc (#3023) --- docs/Authentication.md | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/docs/Authentication.md b/docs/Authentication.md index f5551559d..15a673d19 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -1,5 +1,4 @@ -Authentication -=== +# Authentication There are multiple ways of connecting to a Redis server, depending on the authentication model. The simplest (but least secure) approach is to use the `default` user, with no authentication, and no transport security. @@ -12,10 +11,9 @@ var muxer = await ConnectionMultiplexer.ConnectAsync("myserver"); // or myserver This approach is often used for local transient servers - it is simple, but insecure. But from there, we can get more complex! -TLS -=== +## TLS -If your server has TLS enabled, SE.Redis can be instructed to use it. In some cases (AMR, etc), the +If your server has TLS enabled, SE.Redis can be instructed to use it. In some cases (Azure Managed Redis, etc), the library will recognize the endpoint address, meaning: *you do not need to do anything*. To *manually* enable TLS, the `ssl` token can be used: @@ -44,8 +42,7 @@ Alternatively, in advanced scenarios: to provide your own custom server validati can be used; this uses the normal [`RemoteCertificateValidationCallback`](https://learn.microsoft.com/dotnet/api/system.net.security.remotecertificatevalidationcallback) API. -Usernames and Passwords -=== +## Usernames and Passwords Usernames and passwords can be specified with the `user` and `password` tokens, respectively: @@ -56,15 +53,25 @@ var muxer = await ConnectionMultiplexer.ConnectAsync("myserver,ssl=true,user=myu If no `user` is provided, the `default` user is assumed. In some cases, an authentication-token can be used in place of a classic password. -Client certificates -=== +## Managed identities + +If the server is an Azure Managed Redis resource, connections can be secured using Microsoft Entra ID authentication. Use the [Microsoft.Azure.StackExchangeRedis](https://github.com/Azure/Microsoft.Azure.StackExchangeRedis) extension package to handle the authentication using tokens retrieved from Microsoft Entra. The package integrates via the ConfigurationOptions class, and can use various types of identities for token retrieval. For example with a user-assigned managed identity: + +```csharp +var options = ConfigurationOptions.Parse("mycache.region.redis.azure.net:10000"); +await options.ConfigureForAzureWithUserAssignedManagedIdentityAsync(managedIdentityClientId); +``` + +For details and samples see [https://github.com/Azure/Microsoft.Azure.StackExchangeRedis](https://github.com/Azure/Microsoft.Azure.StackExchangeRedis) + +## Client certificates If the server is configured to require a client certificate, this can be supplied in multiple ways. If you have a local public / private key pair (such as `MyUser2.crt` and `MyUser2.key`), the `options.SetUserPemCertificate(...)` method can be used: ``` csharp -config.SetUserPemCertificate( +options.SetUserPemCertificate( userCertificatePath: userCrtPath, userKeyPath: userKeyPath ); @@ -74,7 +81,7 @@ If you have a single `pfx` file that contains the public / private pair, the `op method can be used: ``` csharp -config.SetUserPfxCertificate( +options.SetUserPfxCertificate( userCertificatePath: userCrtPath, password: filePassword // optional ); @@ -85,8 +92,7 @@ can be used; this uses the normal [`LocalCertificateSelectionCallback`](https://learn.microsoft.com/dotnet/api/system.net.security.remotecertificatevalidationcallback) API. -User certificates with implicit user authentication -=== +## User certificates with implicit user authentication Historically, the client certificate only provided access to the server, but as the `default` user. From 8.6, the server can be configured to use client certificates to provide user identity. This replaces the @@ -114,8 +120,7 @@ var user = (string?)await conn.GetDatabase().ExecuteAsync("acl", "whoami"); Console.WriteLine(user); // writes "MyUser2" ``` -More info -=== +## More info For more information: From bc086f3ac14ca91037fca45f3ede2250bbf21b05 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Mon, 9 Mar 2026 09:55:51 +0000 Subject: [PATCH 430/435] Implement pub/sub in the toy server (#3027) * likely looking pub/sub * pub/sub test fixes * cleanup on outbound write * fix publish return val * ping/subs * fix pub/sub command check * disable InProcPubSubTests for now, server is brittle --- .../InProcessTestServer.cs | 36 +- .../StackExchange.Redis.Tests/PubSubTests.cs | 73 ++-- tests/StackExchange.Redis.Tests/TestBase.cs | 72 +++- .../RedisClient.Output.cs | 105 ++++++ .../StackExchange.Redis.Server/RedisClient.cs | 18 +- .../RedisServer.PubSub.cs | 339 ++++++++++++++++++ .../StackExchange.Redis.Server/RedisServer.cs | 101 +++--- toys/StackExchange.Redis.Server/RespServer.cs | 37 +- .../TypedRedisValue.cs | 13 +- 9 files changed, 673 insertions(+), 121 deletions(-) create mode 100644 toys/StackExchange.Redis.Server/RedisClient.Output.cs create mode 100644 toys/StackExchange.Redis.Server/RedisServer.PubSub.cs diff --git a/tests/StackExchange.Redis.Tests/InProcessTestServer.cs b/tests/StackExchange.Redis.Tests/InProcessTestServer.cs index 5ac222c0a..0e7d77ec2 100644 --- a/tests/StackExchange.Redis.Tests/InProcessTestServer.cs +++ b/tests/StackExchange.Redis.Tests/InProcessTestServer.cs @@ -25,24 +25,14 @@ public InProcessTestServer(ITestOutputHelper? log = null) Tunnel = new InProcTunnel(this); } - public Task ConnectAsync(bool withPubSub = false, TextWriter? log = null) - => ConnectionMultiplexer.ConnectAsync(GetClientConfig(withPubSub), log); + public Task ConnectAsync(TextWriter? log = null) + => ConnectionMultiplexer.ConnectAsync(GetClientConfig(), log); - public ConfigurationOptions GetClientConfig(bool withPubSub = false) + public ConfigurationOptions GetClientConfig() { var commands = GetCommands(); - if (!withPubSub) - { - commands.Remove(nameof(RedisCommand.SUBSCRIBE)); - commands.Remove(nameof(RedisCommand.PSUBSCRIBE)); - commands.Remove(nameof(RedisCommand.SSUBSCRIBE)); - commands.Remove(nameof(RedisCommand.UNSUBSCRIBE)); - commands.Remove(nameof(RedisCommand.PUNSUBSCRIBE)); - commands.Remove(nameof(RedisCommand.SUNSUBSCRIBE)); - commands.Remove(nameof(RedisCommand.PUBLISH)); - commands.Remove(nameof(RedisCommand.SPUBLISH)); - } - // transactions don't work yet + + // transactions don't work yet (needs v3 buffer features) commands.Remove(nameof(RedisCommand.MULTI)); commands.Remove(nameof(RedisCommand.EXEC)); commands.Remove(nameof(RedisCommand.DISCARD)); @@ -82,6 +72,22 @@ protected override void OnMoved(RedisClient client, int hashSlot, Node node) base.OnMoved(client, hashSlot, node); } + protected override void OnOutOfBand(RedisClient client, TypedRedisValue message) + { + if (message.IsAggregate + && message.Span is { IsEmpty: false } span + && !span[0].IsAggregate) + { + _log?.WriteLine($"Client {client.Id}: {span[0].AsRedisValue()} {message} "); + } + else + { + _log?.WriteLine($"Client {client.Id}: {message}"); + } + + base.OnOutOfBand(client, message); + } + public override TypedRedisValue OnUnknownCommand(in RedisClient client, in RedisRequest request, ReadOnlySpan command) { _log?.WriteLine($"[{client.Id}] unknown command: {Encoding.ASCII.GetString(command)}"); diff --git a/tests/StackExchange.Redis.Tests/PubSubTests.cs b/tests/StackExchange.Redis.Tests/PubSubTests.cs index a3dadb07e..7e3db5292 100644 --- a/tests/StackExchange.Redis.Tests/PubSubTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubTests.cs @@ -11,12 +11,31 @@ namespace StackExchange.Redis.Tests; [RunPerProtocol] -public class PubSubTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture) +public class PubSubTests(ITestOutputHelper output, SharedConnectionFixture fixture) + : PubSubTestBase(output, fixture, null) +{ +} + +/* +[RunPerProtocol] +public class InProcPubSubTests(ITestOutputHelper output, InProcServerFixture fixture) + : PubSubTestBase(output, null, fixture) +{ + protected override bool UseDedicatedInProcessServer => false; +} +*/ + +[RunPerProtocol] +public abstract class PubSubTestBase( + ITestOutputHelper output, + SharedConnectionFixture? connection, + InProcServerFixture? server) + : TestBase(output, connection, server) { [Fact] public async Task ExplicitPublishMode() { - await using var conn = Create(channelPrefix: "foo:", log: Writer); + await using var conn = ConnectFactory(channelPrefix: "foo:"); var pub = conn.GetSubscriber(); int a = 0, b = 0, c = 0, d = 0; @@ -54,9 +73,9 @@ await UntilConditionAsync( [InlineData("Foo:", true, "f")] public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string breaker) { - await using var conn = Create(channelPrefix: channelPrefix, shared: false, log: Writer); + await using var conn = ConnectFactory(channelPrefix: channelPrefix, shared: false); - var pub = GetAnyPrimary(conn); + var pub = GetAnyPrimary(conn.DefaultClient); var sub = conn.GetSubscriber(); await PingAsync(pub, sub).ForAwait(); HashSet received = []; @@ -139,10 +158,10 @@ public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string b [Fact] public async Task TestBasicPubSubFireAndForget() { - await using var conn = Create(shared: false, log: Writer); + await using var conn = ConnectFactory(shared: false); - var profiler = conn.AddProfiler(); - var pub = GetAnyPrimary(conn); + var profiler = conn.DefaultClient.AddProfiler(); + var pub = GetAnyPrimary(conn.DefaultClient); var sub = conn.GetSubscriber(); RedisChannel key = RedisChannel.Literal(Me() + Guid.NewGuid()); @@ -214,9 +233,9 @@ private async Task PingAsync(IServer pub, ISubscriber sub, int times = 1) [Fact] public async Task TestPatternPubSub() { - await using var conn = Create(shared: false, log: Writer); + await using var conn = ConnectFactory(shared: false); - var pub = GetAnyPrimary(conn); + var pub = GetAnyPrimary(conn.DefaultClient); var sub = conn.GetSubscriber(); HashSet received = []; @@ -273,7 +292,7 @@ public async Task TestPatternPubSub() [Fact] public async Task TestPublishWithNoSubscribers() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var sub = conn.GetSubscriber(); #pragma warning disable CS0618 @@ -285,7 +304,7 @@ public async Task TestPublishWithNoSubscribers() public async Task TestMassivePublishWithWithoutFlush_Local() { Skip.UnlessLongRunning(); - await using var conn = Create(); + await using var conn = ConnectFactory(); var sub = conn.GetSubscriber(); TestMassivePublish(sub, Me(), "local"); @@ -335,7 +354,7 @@ private void TestMassivePublish(ISubscriber sub, string channel, string caption) [Fact] public async Task SubscribeAsyncEnumerable() { - await using var conn = Create(syncTimeout: 20000, shared: false, log: Writer); + await using var conn = ConnectFactory(shared: false); var sub = conn.GetSubscriber(); RedisChannel channel = RedisChannel.Literal(Me()); @@ -370,7 +389,7 @@ public async Task SubscribeAsyncEnumerable() [Fact] public async Task PubSubGetAllAnyOrder() { - await using var conn = Create(syncTimeout: 20000, shared: false, log: Writer); + await using var conn = ConnectFactory(shared: false); var sub = conn.GetSubscriber(); RedisChannel channel = RedisChannel.Literal(Me()); @@ -625,9 +644,10 @@ public async Task PubSubGetAllCorrectOrder_OnMessage_Async() [Fact] public async Task TestPublishWithSubscribers() { - await using var connA = Create(shared: false, log: Writer); - await using var connB = Create(shared: false, log: Writer); - await using var connPub = Create(); + await using var pair = ConnectFactory(shared: false); + await using var connA = pair.DefaultClient; + await using var connB = pair.CreateClient(); + await using var connPub = pair.CreateClient(); var channel = Me(); var listenA = connA.GetSubscriber(); @@ -652,9 +672,10 @@ public async Task TestPublishWithSubscribers() [Fact] public async Task TestMultipleSubscribersGetMessage() { - await using var connA = Create(shared: false, log: Writer); - await using var connB = Create(shared: false, log: Writer); - await using var connPub = Create(); + await using var pair = ConnectFactory(shared: false); + await using var connA = pair.DefaultClient; + await using var connB = pair.CreateClient(); + await using var connPub = pair.CreateClient(); var channel = RedisChannel.Literal(Me()); var listenA = connA.GetSubscriber(); @@ -682,7 +703,7 @@ public async Task TestMultipleSubscribersGetMessage() [Fact] public async Task Issue38() { - await using var conn = Create(log: Writer); + await using var conn = ConnectFactory(); var sub = conn.GetSubscriber(); int count = 0; @@ -717,9 +738,10 @@ public async Task Issue38() [Fact] public async Task TestPartialSubscriberGetMessage() { - await using var connA = Create(); - await using var connB = Create(); - await using var connPub = Create(); + await using var pair = ConnectFactory(); + await using var connA = pair.DefaultClient; + await using var connB = pair.CreateClient(); + await using var connPub = pair.CreateClient(); int gotA = 0, gotB = 0; var listenA = connA.GetSubscriber(); @@ -750,8 +772,9 @@ public async Task TestPartialSubscriberGetMessage() [Fact] public async Task TestSubscribeUnsubscribeAndSubscribeAgain() { - await using var connPub = Create(); - await using var connSub = Create(); + await using var pair = ConnectFactory(); + await using var connPub = pair.DefaultClient; + await using var connSub = pair.CreateClient(); var prefix = Me(); var pub = connPub.GetSubscriber(); diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 0e622c20c..1eb0e8aab 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -16,7 +16,7 @@ namespace StackExchange.Redis.Tests; public abstract class TestBase : IDisposable { - private ITestOutputHelper Output { get; } + protected ITestOutputHelper Output { get; } protected TextWriterOutputHelper Writer { get; } protected virtual string GetConfiguration() { @@ -585,4 +585,74 @@ protected static async Task UntilConditionAsync(TimeSpan maxWaitTime, Func spent += wait; } } + + // simplified usage to get an interchangeable dedicated vs shared in-process server, useful for debugging + protected virtual bool UseDedicatedInProcessServer => false; // use the shared server by default + internal ClientFactory ConnectFactory(bool allowAdmin = false, string? channelPrefix = null, bool shared = true) + { + if (UseDedicatedInProcessServer) + { + var server = new InProcessTestServer(Output); + return new ClientFactory(this, allowAdmin, channelPrefix, shared, server); + } + return new ClientFactory(this, allowAdmin, channelPrefix, shared, null); + } + + internal sealed class ClientFactory : IDisposable, IAsyncDisposable + { + private readonly TestBase _testBase; + private readonly bool _allowAdmin; + private readonly string? _channelPrefix; + private readonly bool _shared; + private readonly InProcessTestServer? _server; + private IInternalConnectionMultiplexer? _defaultClient; + + internal ClientFactory(TestBase testBase, bool allowAdmin, string? channelPrefix, bool shared, InProcessTestServer? server) + { + _testBase = testBase; + _allowAdmin = allowAdmin; + _channelPrefix = channelPrefix; + _shared = shared; + _server = server; + } + + public IInternalConnectionMultiplexer DefaultClient => _defaultClient ??= CreateClient(); + + public InProcessTestServer? Server => _server; + + public IInternalConnectionMultiplexer CreateClient() + { + if (_server is not null) + { + var config = _server.GetClientConfig(); + config.AllowAdmin = _allowAdmin; + if (_channelPrefix is not null) + { + config.ChannelPrefix = RedisChannel.Literal(_channelPrefix); + } + return ConnectionMultiplexer.ConnectAsync(config).Result; + } + return _testBase.Create(allowAdmin: _allowAdmin, channelPrefix: _channelPrefix, shared: _shared); + } + + public IDatabase GetDatabase(int db = -1) => DefaultClient.GetDatabase(db); + + public ISubscriber GetSubscriber() => DefaultClient.GetSubscriber(); + + public void Dispose() + { + _server?.Dispose(); + _defaultClient?.Dispose(); + } + + public ValueTask DisposeAsync() + { + _server?.Dispose(); + if (_defaultClient is not null) + { + return _defaultClient.DisposeAsync(); + } + return default; + } + } } diff --git a/toys/StackExchange.Redis.Server/RedisClient.Output.cs b/toys/StackExchange.Redis.Server/RedisClient.Output.cs new file mode 100644 index 000000000..525dcc4e1 --- /dev/null +++ b/toys/StackExchange.Redis.Server/RedisClient.Output.cs @@ -0,0 +1,105 @@ +using System; +using System.Buffers; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace StackExchange.Redis.Server; + +public partial class RedisClient +{ + private static readonly UnboundedChannelOptions s_replyChannelOptions = new() + { + SingleReader = true, + SingleWriter = false, + AllowSynchronousContinuations = false, + }; + + private readonly Channel _replies = Channel.CreateUnbounded(s_replyChannelOptions); + + public void AddOutbound(in TypedRedisValue message) + { + if (message.IsNil) + { + message.Recycle(); + return; + } + + try + { + if (!_replies.Writer.TryWrite(message)) + { + // sorry, we're going to need it, but in reality: we're using + // unbounded channels, so this isn't an issue + _replies.Writer.WriteAsync(message).AsTask().Wait(); + } + } + catch + { + message.Recycle(); + } + } + + public ValueTask AddOutboundAsync(in TypedRedisValue message, CancellationToken cancellationToken = default) + { + if (message.IsNil) + { + message.Recycle(); + return default; + } + + try + { + var pending = _replies.Writer.WriteAsync(message, cancellationToken); + if (!pending.IsCompleted) return Awaited(message, pending); + pending.GetAwaiter().GetResult(); + // if we succeed, the writer owns it for recycling + } + catch + { + message.Recycle(); + } + return default; + + static async ValueTask Awaited(TypedRedisValue message, ValueTask pending) + { + try + { + await pending; + // if we succeed, the writer owns it for recycling + } + catch + { + message.Recycle(); + } + } + } + + public void Complete(Exception ex = null) => _replies.Writer.TryComplete(ex); + + public async Task WriteOutputAsync(PipeWriter writer, CancellationToken cancellationToken = default) + { + try + { + var reader = _replies.Reader; + do + { + while (reader.TryRead(out var message)) + { + await RespServer.WriteResponseAsync(this, writer, message, Protocol); + message.Recycle(); + } + + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + } + // await more data + while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)); + await writer.CompleteAsync(); + } + catch (Exception ex) + { + await writer.CompleteAsync(ex); + } + } +} diff --git a/toys/StackExchange.Redis.Server/RedisClient.cs b/toys/StackExchange.Redis.Server/RedisClient.cs index cf3116500..56ecd0dbb 100644 --- a/toys/StackExchange.Redis.Server/RedisClient.cs +++ b/toys/StackExchange.Redis.Server/RedisClient.cs @@ -5,7 +5,7 @@ namespace StackExchange.Redis.Server { - public class RedisClient(RedisServer.Node node) : IDisposable + public partial class RedisClient(RedisServer.Node node) : IDisposable { public RedisServer.Node Node => node; internal int SkipReplies { get; set; } @@ -18,20 +18,7 @@ internal bool ShouldSkipResponse() } return false; } - private HashSet _subscripions; - public int SubscriptionCount => _subscripions?.Count ?? 0; - internal int Subscribe(RedisChannel channel) - { - if (_subscripions == null) _subscripions = new HashSet(); - _subscripions.Add(channel); - return _subscripions.Count; - } - internal int Unsubscribe(RedisChannel channel) - { - if (_subscripions == null) return 0; - _subscripions.Remove(channel); - return _subscripions.Count; - } + public int Database { get; set; } public string Name { get; set; } internal IDuplexPipe LinkedPipe { get; set; } @@ -261,6 +248,7 @@ private static readonly CommandBytes internal sealed class CrossSlotException : Exception { + private CrossSlotException() { } public static void Throw() => throw new CrossSlotException(); } } diff --git a/toys/StackExchange.Redis.Server/RedisServer.PubSub.cs b/toys/StackExchange.Redis.Server/RedisServer.PubSub.cs new file mode 100644 index 000000000..140dd3caa --- /dev/null +++ b/toys/StackExchange.Redis.Server/RedisServer.PubSub.cs @@ -0,0 +1,339 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading; + +namespace StackExchange.Redis.Server; + +public partial class RedisServer +{ + protected virtual void OnOutOfBand(RedisClient client, TypedRedisValue message) + => client.AddOutbound(message); + + [RedisCommand(-2)] + protected virtual TypedRedisValue Subscribe(RedisClient client, in RedisRequest request) + => SubscribeImpl(client, request, RedisCommand.SUBSCRIBE); + + [RedisCommand(-1)] + protected virtual TypedRedisValue Unsubscribe(RedisClient client, in RedisRequest request) + => SubscribeImpl(client, request, RedisCommand.UNSUBSCRIBE); + + [RedisCommand(-2)] + protected virtual TypedRedisValue PSubscribe(RedisClient client, in RedisRequest request) + => SubscribeImpl(client, request, RedisCommand.PSUBSCRIBE); + + [RedisCommand(-1)] + protected virtual TypedRedisValue PUnsubscribe(RedisClient client, in RedisRequest request) + => SubscribeImpl(client, request, RedisCommand.PUNSUBSCRIBE); + + [RedisCommand(-2)] + protected virtual TypedRedisValue SSubscribe(RedisClient client, in RedisRequest request) + => SubscribeImpl(client, request, RedisCommand.SSUBSCRIBE); + + [RedisCommand(-1)] + protected virtual TypedRedisValue SUnsubscribe(RedisClient client, in RedisRequest request) + => SubscribeImpl(client, request, RedisCommand.SUNSUBSCRIBE); + + [RedisCommand(3)] + protected virtual TypedRedisValue Publish(RedisClient client, in RedisRequest request) + { + PublishPair pair = new( + request.GetChannel(1, RedisChannel.RedisChannelOptions.None), + request.GetValue(2)); + // note: docs say "the number of clients that the message was sent to.", but this is a lie; it + // is the number of *subscriptions* - if a client has two matching: delta is two + int count = ForAllClients(pair, static (client, pair) => client.Publish(pair.Channel, pair.Value)); + return TypedRedisValue.Integer(count); + } + + private readonly struct PublishPair(RedisChannel channel, RedisValue value, Node node = null) + { + public readonly RedisChannel Channel = channel; + public readonly RedisValue Value = value; + public readonly Node Node = node; + } + [RedisCommand(3)] + protected virtual TypedRedisValue SPublish(RedisClient client, in RedisRequest request) + { + var channel = request.GetChannel(1, RedisChannel.RedisChannelOptions.Sharded); + var node = client.Node; // filter to clients on the same node + var slot = ServerSelectionStrategy.GetClusterSlot((byte[])channel); + if (!node.HasSlot(slot)) KeyMovedException.Throw(slot); + + PublishPair pair = new(channel, request.GetValue(2)); + int count = ForAllClients(pair, static (client, pair) => + ReferenceEquals(client.Node, pair.Node) ? client.Publish(pair.Channel, pair.Value) : 0); + return TypedRedisValue.Integer(count); + } + + private TypedRedisValue SubscribeImpl(RedisClient client, in RedisRequest request, RedisCommand cmd) + { + bool add = cmd is RedisCommand.SUBSCRIBE or RedisCommand.PSUBSCRIBE or RedisCommand.SSUBSCRIBE; + var options = cmd switch + { + RedisCommand.SUBSCRIBE or RedisCommand.UNSUBSCRIBE => RedisChannel.RedisChannelOptions.None, + RedisCommand.PSUBSCRIBE or RedisCommand.PUNSUBSCRIBE => RedisChannel.RedisChannelOptions.Pattern, + RedisCommand.SSUBSCRIBE or RedisCommand.SUNSUBSCRIBE => RedisChannel.RedisChannelOptions.Sharded, + _ => throw new ArgumentOutOfRangeException(nameof(cmd)), + }; + + // buffer the slots while checking validity + var subCount = request.Count - 1; + if (subCount == 0 & !add) + { + client.UnsubscribeAll(cmd); + } + else + { + var lease = ArrayPool.Shared.Rent(subCount); + try + { + var channel = lease[0] = request.GetChannel(1, options); + int slot = channel.IsSharded + ? ServerSelectionStrategy.GetClusterSlot(channel) + : ServerSelectionStrategy.NoSlot; + if (!client.Node.HasSlot(slot)) KeyMovedException.Throw(slot); + for (int i = 2; i <= subCount; i++) + { + channel = lease[i - 1] = request.GetChannel(i, options); + if (slot != ServerSelectionStrategy.NoSlot && + slot != ServerSelectionStrategy.GetClusterSlot(channel)) + { + CrossSlotException.Throw(); + } + } + + for (int i = 0; i < subCount; i++) + { + if (add) client.Subscribe(lease[i]); + else client.Unsubscribe(lease[i]); + } + } + finally + { + ArrayPool.Shared.Return(lease); + } + } + + return TypedRedisValue.Nil; + } +} + +public partial class RedisClient +{ + private bool HasSubscriptions + { + get + { + var subs = _subscriptions; + if (subs is null) return false; + lock (subs) + { + return subs.Count != 0; + } + } + } + + private Dictionary SubscriptionsIfAny + { + get + { + var subs = _subscriptions; + if (subs is not null) + { + lock (subs) + { + if (subs.Count == 0) return null; + } + } + return subs; + } + } + private Dictionary Subscriptions + { + get + { + return _subscriptions ?? InitSubs(); + + Dictionary InitSubs() + { + var newSubs = new Dictionary(); + return Interlocked.CompareExchange(ref _subscriptions, newSubs, null) ?? newSubs; + } + } + } + + private int simpleCount, shardedCount, patternCount; + private Dictionary _subscriptions; + public int SubscriptionCount => simpleCount; + public int ShardedSubscriptionCount => shardedCount; + public int PatternSubscriptionCount => patternCount; + public bool IsSubscriber => (SubscriptionCount + ShardedSubscriptionCount + PatternSubscriptionCount) != 0; + + public int Publish(in RedisChannel channel, in RedisValue value) + { + var node = Node; + if (node is null) return 0; + int count = 0; + var subs = Subscriptions; + lock (subs) + { + // we can do simple and sharded equality lookups directly + if ((simpleCount + shardedCount) != 0 && subs.TryGetValue(channel, out _)) + { + var msg = TypedRedisValue.Rent(3, out var span, ResultType.Push); + span[0] = TypedRedisValue.BulkString(channel.IsSharded ? "smessage" : "message"); + span[1] = TypedRedisValue.BulkString(channel); + span[2] = TypedRedisValue.BulkString(value); + node.OnOutOfBand(this, msg); + count++; + } + + if (patternCount != 0 && !channel.IsSharded) + { + // need to loop for patterns + var channelName = channel.ToString(); + foreach (var pair in subs) + { + if (pair.Key.IsPattern && pair.Value is { } glob && glob.IsMatch(channelName)) + { + var msg = TypedRedisValue.Rent(4, out var span, ResultType.Push); + span[0] = TypedRedisValue.BulkString("pmessage"); + span[1] = TypedRedisValue.BulkString(pair.Key); + span[2] = TypedRedisValue.BulkString(channel); + span[3] = TypedRedisValue.BulkString(value); + node.OnOutOfBand(this, msg); + count++; + } + } + } + } + + return count; + } + + private void SendMessage(string kind, RedisChannel channel, int count) + { + if (Node is { } node) + { + var reply = TypedRedisValue.Rent(3, out var span, ResultType.Push); + span[0] = TypedRedisValue.BulkString(kind); + span[1] = TypedRedisValue.BulkString((byte[])channel); + span[2] = TypedRedisValue.Integer(count); + // go via node to allow logging etc + node.OnOutOfBand(this, reply); + } + } + + internal void Subscribe(RedisChannel channel) + { + Regex glob = channel.IsPattern ? BuildGlob(channel) : null; + var subs = Subscriptions; + int count; + lock (subs) + { + if (subs.ContainsKey(channel)) return; + subs.Add(channel, glob); + count = channel.IsSharded ? ++shardedCount + : channel.IsPattern ? ++patternCount + : ++simpleCount; + } + SendMessage( + channel.IsSharded ? "ssubscribe" + : channel.IsPattern ? "psubscribe" + : "subscribe", + channel, + count); + } + + private Regex BuildGlob(RedisChannel channel) + { + /* supported patterns: + h?llo subscribes to hello, hallo and hxllo + h*llo subscribes to hllo and heeeello + h[ae]llo subscribes to hello and hallo, but not hillo + */ + // firstly, escape *everything*, then we'll go back and fixup + var re = Regex.Escape(channel.ToString()); + re = re.Replace(@"\?", ".").Replace(@"\*", ".*") + .Replace(@"\[", "[").Replace(@"\]", "]"); // not perfect, but good enough for now + return new Regex(re, RegexOptions.CultureInvariant); + } + + internal void Unsubscribe(RedisChannel channel) + { + var subs = SubscriptionsIfAny; + if (subs is null) return; + int count; + lock (subs) + { + if (!subs.Remove(channel)) return; + count = channel.IsSharded ? --shardedCount + : channel.IsPattern ? --patternCount + : --simpleCount; + } + SendMessage( + channel.IsSharded ? "sunsubscribe" + : channel.IsPattern ? "punsubscribe" + : "unsubscribe", + channel, + count); + } + + internal void UnsubscribeAll(RedisCommand cmd) + { + var subs = Subscriptions; + if (subs is null) return; + RedisChannel[] remove; + int count = 0; + string msg; + lock (subs) + { + remove = ArrayPool.Shared.Rent(count); + foreach (var pair in subs) + { + var key = pair.Key; + if (cmd switch + { + RedisCommand.UNSUBSCRIBE when !(pair.Key.IsPattern | pair.Key.IsSharded) => true, + RedisCommand.PUNSUBSCRIBE when pair.Key.IsPattern => true, + RedisCommand.SUNSUBSCRIBE when pair.Key.IsSharded => true, + _ => false, + }) + { + remove[count++] = key; + } + } + + foreach (var key in remove.AsSpan(0, count)) + { + _subscriptions.Remove(key); + } + + switch (cmd) + { + case RedisCommand.SUNSUBSCRIBE: + msg = "sunsubscribe"; + shardedCount = 0; + break; + case RedisCommand.PUNSUBSCRIBE: + msg = "punsubscribe"; + patternCount = 0; + break; + case RedisCommand.UNSUBSCRIBE: + msg = "unsubscribe"; + simpleCount = 0; + break; + default: + msg = ""; + break; + } + } + foreach (var key in remove.AsSpan(0, count)) + { + SendMessage(msg, key, 0); + } + ArrayPool.Shared.Return(remove); + } +} diff --git a/toys/StackExchange.Redis.Server/RedisServer.cs b/toys/StackExchange.Redis.Server/RedisServer.cs index 2830cc35a..1e92aa8ec 100644 --- a/toys/StackExchange.Redis.Server/RedisServer.cs +++ b/toys/StackExchange.Redis.Server/RedisServer.cs @@ -10,7 +10,7 @@ namespace StackExchange.Redis.Server { - public abstract class RedisServer : RespServer + public abstract partial class RedisServer : RespServer { // non-trivial wildcards not implemented yet! public static bool IsMatch(string pattern, string key) => @@ -155,10 +155,20 @@ protected override void AppendStats(StringBuilder sb) public override TypedRedisValue Execute(RedisClient client, in RedisRequest request) { - var pw = Password; - if (pw.Length != 0 & !client.IsAuthenticated) + if (request.Count != 0) { - if (!Literals.IsAuthCommand(in request)) return TypedRedisValue.Error("NOAUTH Authentication required."); + var pw = Password; + if (pw.Length != 0 & !client.IsAuthenticated) + { + if (!Literals.IsAuthCommand(in request)) + return TypedRedisValue.Error("NOAUTH Authentication required."); + } + else if (client.Protocol is RedisProtocol.Resp2 && client.IsSubscriber && + !Literals.IsPubSubCommand(in request, out var cmd)) + { + return TypedRedisValue.Error( + $"ERR only (P|S)SUBSCRIBE / (P|S)UNSUBSCRIBE / PING / QUIT allowed in this context (got: '{cmd}')"); + } } return base.Execute(client, request); } @@ -168,11 +178,34 @@ internal class Literals public static readonly CommandBytes AUTH = new("AUTH"u8), HELLO = new("HELLO"u8), - SETNAME = new("SETNAME"u8); + SETNAME = new("SETNAME"u8), + QUIT = new("SETNAME"u8), + PING = new("PING"u8), + SUBSCRIBE = new("SUBSCRIBE"u8), + PSUBSCRIBE = new("PSUBSCRIBE"u8), + SSUBSCRIBE = new("SSUBSCRIBE"u8), + UNSUBSCRIBE = new("UNSUBSCRIBE"u8), + PUNSUBSCRIBE = new("PUNSUBSCRIBE"u8), + SUNSUBSCRIBE = new("SUNSUBSCRIBE"u8); public static bool IsAuthCommand(in RedisRequest request) => request.Count != 0 && request.TryGetCommandBytes(0, out var command) && (command.Equals(AUTH) || command.Equals(HELLO)); + + public static bool IsPubSubCommand(in RedisRequest request, out string badCommand) + { + badCommand = ""; + if (request.Count == 0 || !request.TryGetCommandBytes(0, out var command)) + { + if (request.Count != 0) badCommand = request.GetString(0); + return false; + } + + return command.Equals(SUBSCRIBE) || command.Equals(UNSUBSCRIBE) + || command.Equals(SSUBSCRIBE) || command.Equals(SUNSUBSCRIBE) + || command.Equals(PSUBSCRIBE) || command.Equals(PUNSUBSCRIBE) + || command.Equals(PING) || command.Equals(QUIT); + } } [RedisCommand(2)] @@ -582,6 +615,7 @@ public Node(RedisServer server, EndPoint endpoint) public bool HasSlot(int hashSlot) { + if (hashSlot == ServerSelectionStrategy.NoSlot) return true; var slots = _slots; if (slots is null) return true; // all nodes foreach (var slot in slots) @@ -742,6 +776,9 @@ public void AssertKey(in RedisKey key) } public void Touch(int db, in RedisKey key) => _server.Touch(db, key); + + public void OnOutOfBand(RedisClient client, TypedRedisValue message) + => _server.OnOutOfBand(client, message); } public virtual bool CheckCrossSlot => ServerType == ServerType.Cluster; @@ -1173,13 +1210,25 @@ protected virtual TypedRedisValue Mset(RedisClient client, in RedisRequest reque } return TypedRedisValue.OK; } + [RedisCommand(-1, LockFree = true, MaxArgs = 2)] protected virtual TypedRedisValue Ping(RedisClient client, in RedisRequest request) - => TypedRedisValue.SimpleString(request.Count == 1 ? "PONG" : request.GetString(1)); + { + if (client.IsSubscriber) + { + var reply = TypedRedisValue.Rent(2, out var span, ResultType.Array); + span[0] = TypedRedisValue.BulkString("pong"); + RedisValue value = request.Count == 1 ? RedisValue.Null : request.GetValue(1); + span[1] = TypedRedisValue.BulkString(value); + return reply; + } + return TypedRedisValue.SimpleString(request.Count == 1 ? "PONG" : request.GetString(1)); + } [RedisCommand(1, LockFree = true)] protected virtual TypedRedisValue Quit(RedisClient client, in RedisRequest request) { + client.Complete(); RemoveClient(client); return TypedRedisValue.OK; } @@ -1205,46 +1254,6 @@ protected virtual TypedRedisValue Select(RedisClient client, in RedisRequest req return TypedRedisValue.OK; } - [RedisCommand(-2)] - protected virtual TypedRedisValue Subscribe(RedisClient client, in RedisRequest request) - => SubscribeImpl(client, request); - [RedisCommand(-2)] - protected virtual TypedRedisValue Unsubscribe(RedisClient client, in RedisRequest request) - => SubscribeImpl(client, request); - - private TypedRedisValue SubscribeImpl(RedisClient client, in RedisRequest request) - { - var reply = TypedRedisValue.Rent(3 * (request.Count - 1), out var span, ResultType.Array); - int index = 0; - request.TryGetCommandBytes(0, out var cmd); - var cmdString = TypedRedisValue.BulkString(cmd.ToArray()); - var mode = cmd[0] == (byte)'p' ? RedisChannel.RedisChannelOptions.Pattern : RedisChannel.RedisChannelOptions.None; - for (int i = 1; i < request.Count; i++) - { - var channel = request.GetChannel(i, mode); - int count; - if (s_Subscribe.Equals(cmd)) - { - count = client.Subscribe(channel); - } - else if (s_Unsubscribe.Equals(cmd)) - { - count = client.Unsubscribe(channel); - } - else - { - reply.Recycle(index); - return TypedRedisValue.Nil; - } - span[index++] = cmdString; - span[index++] = TypedRedisValue.BulkString((byte[])channel); - span[index++] = TypedRedisValue.Integer(count); - } - return reply; - } - private static readonly CommandBytes - s_Subscribe = new CommandBytes("subscribe"), - s_Unsubscribe = new CommandBytes("unsubscribe"); private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); [RedisCommand(1, LockFree = true)] diff --git a/toys/StackExchange.Redis.Server/RespServer.cs b/toys/StackExchange.Redis.Server/RespServer.cs index b1611b087..fd592e3d8 100644 --- a/toys/StackExchange.Redis.Server/RespServer.cs +++ b/toys/StackExchange.Redis.Server/RespServer.cs @@ -225,6 +225,16 @@ public RedisClient AddClient(RedisServer.Node node, object state) return client; } + protected int ForAllClients(TState state, Func func) + { + int count = 0; + foreach (var client in _clientLookup.Values) + { + count += func(client, state); + } + return count; + } + public bool TryGetClient(int id, out RedisClient client) => _clientLookup.TryGetValue(id, out client); private readonly ConcurrentDictionary _clientLookup = new(); @@ -277,13 +287,14 @@ public async Task RunClientAsync(IDuplexPipe pipe, RedisServer.Node node = null, { node ??= DefaultNode; client = AddClient(node, state); + var incompleteOutput = client.WriteOutputAsync(pipe.Output); while (!client.Closed) { var readResult = await pipe.Input.ReadAsync().ConfigureAwait(false); var buffer = readResult.Buffer; bool makingProgress = false; - while (!client.Closed && await TryProcessRequestAsync(ref buffer, client, pipe.Output).ConfigureAwait(false)) + while (!client.Closed && await TryProcessRequestAsync(ref buffer, client).ConfigureAwait(false)) { makingProgress = true; } @@ -294,6 +305,8 @@ public async Task RunClientAsync(IDuplexPipe pipe, RedisServer.Node node = null, break; } } + client.Complete(); + await incompleteOutput; } catch (ConnectionResetException) { } catch (ObjectDisposedException) { } @@ -308,6 +321,7 @@ public async Task RunClientAsync(IDuplexPipe pipe, RedisServer.Node node = null, } finally { + client?.Complete(fault); RemoveClient(client); try { pipe.Input.Complete(fault); } catch { } try { pipe.Output.Complete(fault); } catch { } @@ -444,12 +458,11 @@ private static bool TryParseRequest(Arena arena, ref ReadOnlySequence private readonly Arena _arena = new Arena(); - public ValueTask TryProcessRequestAsync(ref ReadOnlySequence buffer, RedisClient client, PipeWriter output) + public ValueTask TryProcessRequestAsync(ref ReadOnlySequence buffer, RedisClient client) { - static async ValueTask Awaited(ValueTask write, TypedRedisValue response) + static async ValueTask Awaited(ValueTask write) { - await write; - response.Recycle(); + await write.ConfigureAwait(false); return true; } if (!buffer.IsEmpty && TryParseRequest(_arena, ref buffer, out var request)) @@ -463,9 +476,9 @@ static async ValueTask Awaited(ValueTask write, TypedRedisValue response) client.ResetAfterRequest(); } - var write = WriteResponseAsync(client, output, response, client.Protocol); - if (!write.IsCompletedSuccessfully) return Awaited(write, response); - response.Recycle(); + var write = client.AddOutboundAsync(response); + if (!write.IsCompletedSuccessfully) return Awaited(write); + write.GetAwaiter().GetResult(); return new ValueTask(true); } return new ValueTask(false); @@ -484,7 +497,7 @@ public virtual void ResetCounters() public virtual TypedRedisValue OnUnknownCommand(in RedisClient client, in RedisRequest request, ReadOnlySpan command) { - return TypedRedisValue.Nil; + return request.CommandNotFound(); } public virtual TypedRedisValue Execute(RedisClient client, in RedisRequest request) @@ -536,12 +549,6 @@ public virtual TypedRedisValue Execute(RedisClient client, in RedisRequest reque result = OnUnknownCommand(client, request, span.Slice(0, cmdBytes.Length)); } - if (result.IsNil) - { - Log($"missing command: '{request.GetString(0)}'"); - return request.CommandNotFound(); - } - if (result.Type == ResultType.Error) Interlocked.Increment(ref _totalErrorCount); return result; } diff --git a/toys/StackExchange.Redis.Server/TypedRedisValue.cs b/toys/StackExchange.Redis.Server/TypedRedisValue.cs index 054cf1fa8..d7fa7e4b6 100644 --- a/toys/StackExchange.Redis.Server/TypedRedisValue.cs +++ b/toys/StackExchange.Redis.Server/TypedRedisValue.cs @@ -148,6 +148,13 @@ public static TypedRedisValue MultiBulk(ICollection items, Resu public static TypedRedisValue BulkString(RedisValue value) => new TypedRedisValue(value, ResultType.BulkString); + /// + /// Initialize a that represents a bulk string. + /// + /// The value to initialize from. + public static TypedRedisValue BulkString(in RedisChannel value) + => new TypedRedisValue((byte[])value, ResultType.BulkString); + private TypedRedisValue(TypedRedisValue[] oversizedItems, int count, ResultType type) { if (oversizedItems == null) @@ -179,7 +186,7 @@ internal void Recycle(int limit = -1) /// /// Get the underlying assuming that it is a valid type with a meaningful value. /// - internal RedisValue AsRedisValue() => Type == ResultType.Array ? default : _value; + public RedisValue AsRedisValue() => IsAggregate ? default : _value; /// /// Obtain the value as a string. @@ -193,10 +200,8 @@ public override string ToString() case ResultType.Integer: case ResultType.Error: return $"{Type}:{_value}"; - case ResultType.Array: - return $"{Type}:[{Span.Length}]"; default: - return Type.ToString(); + return IsAggregate ? $"{Type}:[{Span.Length}]" : Type.ToString(); } } From ed0e35388031138105ffbebb1b4df708aabe6098 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 11 Mar 2026 15:12:20 +0000 Subject: [PATCH 431/435] Track the number of live multiplexers (#3030) * Track the number of live multiplexers, and report it in the error dump if non-trivial * fight the CI on a CS9336 mixup * simplify counting --- .../ConnectionMultiplexer.Debug.cs | 11 ++++++++++- src/StackExchange.Redis/ConnectionMultiplexer.cs | 8 ++++++-- src/StackExchange.Redis/ExceptionFactory.cs | 6 ++++++ src/StackExchange.Redis/PhysicalBridge.cs | 7 ++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs index 9b30ac141..bd77ec2f7 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Debug.cs @@ -4,9 +4,18 @@ namespace StackExchange.Redis; public partial class ConnectionMultiplexer { - private static int _collectedWithoutDispose; + private static int _collectedWithoutDispose, s_DisposedCount, s_MuxerCreateCount; internal static int CollectedWithoutDispose => Volatile.Read(ref _collectedWithoutDispose); + internal static int GetLiveObjectCount(out int created, out int disposed, out int finalized) + { + // read destroy first, to prevent negative numbers in race conditions + disposed = Volatile.Read(ref s_DisposedCount); + created = Volatile.Read(ref s_MuxerCreateCount); + finalized = Volatile.Read(ref _collectedWithoutDispose); + return created - (disposed + finalized); + } + /// /// Invoked by the garbage collector. /// diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index e19de6d52..f3cefa6e2 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -128,6 +128,8 @@ static ConnectionMultiplexer() private ConnectionMultiplexer(ConfigurationOptions configuration, ServerType? serverType = null, EndPointCollection? endpoints = null) { + Interlocked.Increment(ref s_MuxerCreateCount); + RawConfig = configuration ?? throw new ArgumentNullException(nameof(configuration)); EndPoints = endpoints ?? RawConfig.EndPoints.Clone(); EndPoints.SetDefaultPorts(serverType, ssl: RawConfig.Ssl); @@ -2258,7 +2260,8 @@ ConfigurationChangedChannel is byte[] channel public void Dispose() { GC.SuppressFinalize(this); - Close(!_isDisposed); + if (!_isDisposed) Interlocked.Increment(ref s_DisposedCount); + Close(!_isDisposed); // marks disposed sentinelConnection?.Dispose(); var oldTimer = Interlocked.Exchange(ref sentinelPrimaryReconnectTimer, null); oldTimer?.Dispose(); @@ -2270,7 +2273,8 @@ public void Dispose() public async ValueTask DisposeAsync() { GC.SuppressFinalize(this); - await CloseAsync(!_isDisposed).ForAwait(); + if (!_isDisposed) Interlocked.Increment(ref s_DisposedCount); + await CloseAsync(!_isDisposed).ForAwait(); // marks disposed if (sentinelConnection is ConnectionMultiplexer sentinel) { await sentinel.DisposeAsync().ForAwait(); diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 7e4eca49a..43959edb1 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -347,6 +347,12 @@ private static void AddCommonDetail( Add(data, sb, "Last-Result-Bytes", "last-in", bs.Connection.BytesLastResult.ToString()); Add(data, sb, "Inbound-Buffer-Bytes", "cur-in", bs.Connection.BytesInBuffer.ToString()); + var liveMuxers = ConnectionMultiplexer.GetLiveObjectCount(out var created, out var disposed, out var finalized); + if (created > 1) + { + Add(data, sb, "Live-Multiplexers", "lm", $"{liveMuxers}/{created}/{disposed}/{finalized}"); + } + Add(data, sb, "Sync-Ops", "sync-ops", multiplexer.syncOps.ToString()); Add(data, sb, "Async-Ops", "async-ops", multiplexer.asyncOps.ToString()); diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index 9e5808009..a9b5ee140 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -1622,8 +1622,13 @@ private WriteResult WriteMessageToServerInsideWriteLock(PhysicalConnection conne if (_nextHighIntegrityToken is not 0 && !connection.TransactionActive // validated in the UNWATCH/EXEC/DISCARD - && message.Command is not RedisCommand.AUTH or RedisCommand.HELLO) // if auth fails, ECHO may also fail; avoid confusion + && message.Command is not RedisCommand.AUTH // if auth fails, later commands may also fail; avoid confusion + && message.Command is not RedisCommand.HELLO) { + // note on the Command match above: curiously, .NET 10 and .NET 11 SDKs emit *opposite* errors here + // re "CS9336: The pattern is redundant." ("fixing" one "breaks" the other); possibly a fixed bool inversion + // in the analyzer? to avoid pain, we'll just use the most obviously correct form + // make sure this value exists early to avoid a race condition // if the response comes back super quickly message.WithHighIntegrity(NextHighIntegrityTokenInsideLock()); From 6f1dab4ed8d1dd8baa50d4592f3f1f18df4c22a9 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 11 Mar 2026 15:41:11 +0000 Subject: [PATCH 432/435] fix main CI; v2 in-proc server is catastrophically unstable: `Catastrophic failure: System.InvalidOperationException : End position was not reached during enumeration.` --- tests/StackExchange.Redis.Tests/BasicOpTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/StackExchange.Redis.Tests/BasicOpTests.cs b/tests/StackExchange.Redis.Tests/BasicOpTests.cs index f7b0be324..40fb42947 100644 --- a/tests/StackExchange.Redis.Tests/BasicOpTests.cs +++ b/tests/StackExchange.Redis.Tests/BasicOpTests.cs @@ -12,11 +12,13 @@ public class BasicOpsTests(ITestOutputHelper output, SharedConnectionFixture fix { } +/* [RunPerProtocol] public class InProcBasicOpsTests(ITestOutputHelper output, InProcServerFixture fixture) : BasicOpsTestsBase(output, null, fixture) { } +*/ [RunPerProtocol] public abstract class BasicOpsTestsBase(ITestOutputHelper output, SharedConnectionFixture? connection, InProcServerFixture? server) From 583f393f07a93c8a845f801cbbe79fad63a0fdd3 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 11 Mar 2026 15:55:31 +0000 Subject: [PATCH 433/435] duplicated --- tests/StackExchange.Redis.Tests/HighIntegrityBasicOpsTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/StackExchange.Redis.Tests/HighIntegrityBasicOpsTests.cs b/tests/StackExchange.Redis.Tests/HighIntegrityBasicOpsTests.cs index bedc1bf11..b03f7332c 100644 --- a/tests/StackExchange.Redis.Tests/HighIntegrityBasicOpsTests.cs +++ b/tests/StackExchange.Redis.Tests/HighIntegrityBasicOpsTests.cs @@ -7,7 +7,9 @@ public class HighIntegrityBasicOpsTests(ITestOutputHelper output, SharedConnecti internal override bool HighIntegrity => true; } +/* public class InProcHighIntegrityBasicOpsTests(ITestOutputHelper output, InProcServerFixture fixture) : InProcBasicOpsTests(output, fixture) { internal override bool HighIntegrity => true; } +*/ From 6eeb04d223ef68f2400db11067cd6b917b5ee1c3 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 13 Mar 2026 06:51:45 +0000 Subject: [PATCH 434/435] V3 pre-merge; all the early bits, for V2 (#3028) * minimal pre-v3 consolidation: - add RESPite to the solution, but *DO NOT* reference from SE.Redis - update eng tooling and migrate field parsing from FastHash to AsciiHash - update the toy server *completely* to the v3 specification * - backport LCS and RedisType.VectorSet changes from v3 * CI * tidy TFMs * nix TestHarness * deal with TFM-specific warnings from .NET 10 * in-proc test enhancements * fix Select * fix "fromBytes" logic to be more formal, and more importantly: to keep Nick happy * make DebugAssertValid logic #if DEBUG * cleanup a great many #if * join NET7 => NET8 * join net9 with net10 * fix high integrity token check on AUTH/HELLO * borked a #endif * Use a better way of refing the build project * clarify public API files * exclude Build.csproj from refing the analyzer project! * - improve "moved" test for "did the client already know the new node?" - fix where RESP3 default is specified for the toy server * make test server output more terse * toy server: don't pretend SENTINEL exists * rev minor * bump * Include endpoint data in log, for example `[127.0.0.1:6379 #1] CLIENT => +OK` --- .github/workflows/CI.yml | 244 +- .github/workflows/codeql.yml | 63 +- Build.csproj | 1 + Directory.Build.props | 8 +- Directory.Packages.props | 3 + StackExchange.Redis.sln | 15 + eng/StackExchange.Redis.Build/AsciiHash.md | 173 ++ .../AsciiHashGenerator.cs | 774 +++++++ eng/StackExchange.Redis.Build/BasicArray.cs | 85 + .../FastHashGenerator.cs | 215 -- .../FastHashGenerator.md | 64 - .../StackExchange.Redis.Build.csproj | 7 +- src/Directory.Build.props | 3 - src/RESPite/Buffers/CycleBuffer.cs | 753 ++++++ src/RESPite/Buffers/ICycleBufferCallback.cs | 14 + src/RESPite/Buffers/MemoryTrackedPool.cs | 63 + src/RESPite/Internal/BlockBuffer.cs | 341 +++ src/RESPite/Internal/BlockBufferSerializer.cs | 96 + src/RESPite/Internal/DebugCounters.cs | 163 ++ src/RESPite/Internal/Raw.cs | 138 ++ src/RESPite/Internal/RespConstants.cs | 53 + .../Internal/RespOperationExtensions.cs | 57 + .../SynchronizedBlockBufferSerializer.cs | 122 + .../ThreadLocalBlockBufferSerializer.cs | 21 + src/RESPite/Messages/RespAttributeReader.cs | 71 + src/RESPite/Messages/RespFrameScanner.cs | 203 ++ src/RESPite/Messages/RespPrefix.cs | 100 + .../RespReader.AggregateEnumerator.cs | 279 +++ src/RESPite/Messages/RespReader.Debug.cs | 59 + .../Messages/RespReader.ScalarEnumerator.cs | 105 + src/RESPite/Messages/RespReader.Span.cs | 86 + src/RESPite/Messages/RespReader.Utils.cs | 341 +++ src/RESPite/Messages/RespReader.cs | 2037 +++++++++++++++++ src/RESPite/Messages/RespScanState.cs | 163 ++ src/RESPite/PublicAPI/PublicAPI.Shipped.txt | 1 + src/RESPite/PublicAPI/PublicAPI.Unshipped.txt | 214 ++ .../PublicAPI/net8.0/PublicAPI.Shipped.txt | 1 + .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 3 + src/RESPite/RESPite.csproj | 51 + src/RESPite/RespException.cs | 11 + src/RESPite/Shared/AsciiHash.Comparers.cs | 37 + src/RESPite/Shared/AsciiHash.Instance.cs | 73 + src/RESPite/Shared/AsciiHash.Public.cs | 10 + src/RESPite/Shared/AsciiHash.cs | 294 +++ .../Shared}/Experiments.cs | 10 +- src/RESPite/Shared/FrameworkShims.Encoding.cs | 50 + src/RESPite/Shared/FrameworkShims.Stream.cs | 107 + src/RESPite/Shared/FrameworkShims.cs | 15 + src/RESPite/Shared/NullableHacks.cs | 146 ++ src/RESPite/readme.md | 6 + .../APITypes/LCSMatchResult.cs | 121 +- .../APITypes/StreamInfo.cs | 1 + .../ChannelMessageQueue.cs | 25 +- .../Configuration/LoggingTunnel.cs | 8 +- .../ConfigurationOptions.cs | 12 +- .../ConnectionMultiplexer.cs | 2 +- src/StackExchange.Redis/Enums/RedisCommand.cs | 12 + src/StackExchange.Redis/Enums/RedisType.cs | 6 + src/StackExchange.Redis/ExceptionFactory.cs | 2 +- .../ExtensionMethods.Internal.cs | 2 +- src/StackExchange.Redis/FastHash.cs | 137 -- src/StackExchange.Redis/Format.cs | 10 +- src/StackExchange.Redis/FrameworkShims.cs | 6 +- .../HotKeys.ResultProcessor.cs | 74 +- src/StackExchange.Redis/HotKeys.cs | 1 + src/StackExchange.Redis/HotKeysField.cs | 145 ++ .../Interfaces/IDatabase.VectorSets.cs | 3 +- .../Interfaces/IDatabase.cs | 1 + .../Interfaces/IDatabaseAsync.VectorSets.cs | 3 +- .../Interfaces/IDatabaseAsync.cs | 1 + src/StackExchange.Redis/Interfaces/IServer.cs | 3 + src/StackExchange.Redis/KeyNotification.cs | 42 +- .../KeyNotificationType.cs | 62 +- .../KeyNotificationTypeFastHash.cs | 413 ---- .../KeyNotificationTypeMetadata.cs | 77 + .../KeyPrefixed.VectorSets.cs | 1 + src/StackExchange.Redis/LoggerExtensions.cs | 2 +- .../Maintenance/AzureMaintenanceEvent.cs | 5 +- src/StackExchange.Redis/Message.cs | 8 +- src/StackExchange.Redis/NullableHacks.cs | 4 +- src/StackExchange.Redis/PerfCounterHelper.cs | 2 +- src/StackExchange.Redis/PhysicalBridge.cs | 40 +- src/StackExchange.Redis/PhysicalConnection.cs | 136 +- .../PublicAPI/PublicAPI.Unshipped.txt | 16 + .../PublicAPI/net8.0/PublicAPI.Shipped.txt | 4 - .../netcoreapp3.1/PublicAPI.Shipped.txt | 2 - src/StackExchange.Redis/RawResult.cs | 27 +- src/StackExchange.Redis/RedisChannel.cs | 2 +- src/StackExchange.Redis/RedisKey.cs | 2 +- src/StackExchange.Redis/RedisLiterals.cs | 25 - src/StackExchange.Redis/RedisValue.cs | 27 + .../ResultProcessor.VectorSets.cs | 63 +- src/StackExchange.Redis/ResultProcessor.cs | 78 +- .../ServerSelectionStrategy.cs | 2 +- src/StackExchange.Redis/SkipLocalsInit.cs | 2 +- .../StackExchange.Redis.csproj | 15 +- .../StreamConfiguration.cs | 1 + src/StackExchange.Redis/StreamIdempotentId.cs | 1 + src/StackExchange.Redis/StreamInfoField.cs | 121 + src/StackExchange.Redis/TaskExtensions.cs | 2 +- src/StackExchange.Redis/ValueCondition.cs | 1 + .../VectorSetAddRequest.cs | 3 +- src/StackExchange.Redis/VectorSetInfo.cs | 1 + src/StackExchange.Redis/VectorSetInfoField.cs | 61 + src/StackExchange.Redis/VectorSetLink.cs | 1 + .../VectorSetQuantization.cs | 15 + .../VectorSetSimilaritySearchRequest.cs | 1 + .../VectorSetSimilaritySearchResult.cs | 3 +- tests/RESPite.Tests/CycleBufferTests.cs | 87 + tests/RESPite.Tests/RESPite.Tests.csproj | 22 + tests/RESPite.Tests/RespReaderTests.cs | 1077 +++++++++ tests/RESPite.Tests/RespScannerTests.cs | 18 + tests/RESPite.Tests/TestDuplexStream.cs | 229 ++ ...shBenchmarks.cs => AsciiHashBenchmarks.cs} | 84 +- .../AsciiHashSwitch.cs | 517 +++++ .../CustomConfig.cs | 2 +- .../EnumParseBenchmarks.cs | 690 ++++++ .../FormatBenchmarks.cs | 4 +- .../StackExchange.Redis.Benchmarks/Program.cs | 5 +- .../StackExchange.Redis.Benchmarks.csproj | 5 +- tests/StackExchange.Redis.Tests/App.config | 2 +- .../AsciiHashUnitTests.cs | 460 ++++ .../StackExchange.Redis.Tests/BasicOpTests.cs | 86 +- .../StackExchange.Redis.Tests/DuplexStream.cs | 131 ++ .../ExceptionFactoryTests.cs | 2 +- .../FailoverTests.cs | 2 +- .../FastHashTests.cs | 153 -- .../StackExchange.Redis.Tests/GlobalUsings.cs | 3 + .../Helpers/InProcServerFixture.cs | 5 +- .../InProcessTestServer.cs | 126 +- .../KeyNotificationTests.cs | 112 +- .../MovedTestServer.cs | 2 +- .../MovedUnitTests.cs | 17 +- .../PubSubKeyNotificationTests.cs | 6 +- .../StackExchange.Redis.Tests/PubSubTests.cs | 44 +- tests/StackExchange.Redis.Tests/SSLTests.cs | 2 +- .../StackExchange.Redis.Tests.csproj | 2 +- .../StackExchange.Redis.Tests/StringTests.cs | 8 +- tests/StackExchange.Redis.Tests/TestBase.cs | 15 +- .../KestrelRedisServer.csproj | 2 +- .../RedisConnectionHandler.cs | 18 +- .../GlobalUsings.cs | 22 + .../RedisClient.Output.cs | 173 +- .../StackExchange.Redis.Server/RedisClient.cs | 105 +- .../RedisRequest.cs | 130 +- .../RedisServer.PubSub.cs | 49 +- .../StackExchange.Redis.Server/RedisServer.cs | 217 +- .../RespReaderExtensions.cs | 213 ++ toys/StackExchange.Redis.Server/RespServer.cs | 284 +-- .../StackExchange.Redis.Server.csproj | 5 +- .../TypedRedisValue.cs | 102 +- version.json | 4 +- 152 files changed, 12912 insertions(+), 2122 deletions(-) create mode 100644 eng/StackExchange.Redis.Build/AsciiHash.md create mode 100644 eng/StackExchange.Redis.Build/AsciiHashGenerator.cs create mode 100644 eng/StackExchange.Redis.Build/BasicArray.cs delete mode 100644 eng/StackExchange.Redis.Build/FastHashGenerator.cs delete mode 100644 eng/StackExchange.Redis.Build/FastHashGenerator.md create mode 100644 src/RESPite/Buffers/CycleBuffer.cs create mode 100644 src/RESPite/Buffers/ICycleBufferCallback.cs create mode 100644 src/RESPite/Buffers/MemoryTrackedPool.cs create mode 100644 src/RESPite/Internal/BlockBuffer.cs create mode 100644 src/RESPite/Internal/BlockBufferSerializer.cs create mode 100644 src/RESPite/Internal/DebugCounters.cs create mode 100644 src/RESPite/Internal/Raw.cs create mode 100644 src/RESPite/Internal/RespConstants.cs create mode 100644 src/RESPite/Internal/RespOperationExtensions.cs create mode 100644 src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs create mode 100644 src/RESPite/Internal/ThreadLocalBlockBufferSerializer.cs create mode 100644 src/RESPite/Messages/RespAttributeReader.cs create mode 100644 src/RESPite/Messages/RespFrameScanner.cs create mode 100644 src/RESPite/Messages/RespPrefix.cs create mode 100644 src/RESPite/Messages/RespReader.AggregateEnumerator.cs create mode 100644 src/RESPite/Messages/RespReader.Debug.cs create mode 100644 src/RESPite/Messages/RespReader.ScalarEnumerator.cs create mode 100644 src/RESPite/Messages/RespReader.Span.cs create mode 100644 src/RESPite/Messages/RespReader.Utils.cs create mode 100644 src/RESPite/Messages/RespReader.cs create mode 100644 src/RESPite/Messages/RespScanState.cs create mode 100644 src/RESPite/PublicAPI/PublicAPI.Shipped.txt create mode 100644 src/RESPite/PublicAPI/PublicAPI.Unshipped.txt create mode 100644 src/RESPite/PublicAPI/net8.0/PublicAPI.Shipped.txt create mode 100644 src/RESPite/PublicAPI/net8.0/PublicAPI.Unshipped.txt create mode 100644 src/RESPite/RESPite.csproj create mode 100644 src/RESPite/RespException.cs create mode 100644 src/RESPite/Shared/AsciiHash.Comparers.cs create mode 100644 src/RESPite/Shared/AsciiHash.Instance.cs create mode 100644 src/RESPite/Shared/AsciiHash.Public.cs create mode 100644 src/RESPite/Shared/AsciiHash.cs rename src/{StackExchange.Redis => RESPite/Shared}/Experiments.cs (86%) create mode 100644 src/RESPite/Shared/FrameworkShims.Encoding.cs create mode 100644 src/RESPite/Shared/FrameworkShims.Stream.cs create mode 100644 src/RESPite/Shared/FrameworkShims.cs create mode 100644 src/RESPite/Shared/NullableHacks.cs create mode 100644 src/RESPite/readme.md delete mode 100644 src/StackExchange.Redis/FastHash.cs create mode 100644 src/StackExchange.Redis/HotKeysField.cs delete mode 100644 src/StackExchange.Redis/KeyNotificationTypeFastHash.cs create mode 100644 src/StackExchange.Redis/KeyNotificationTypeMetadata.cs delete mode 100644 src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt delete mode 100644 src/StackExchange.Redis/PublicAPI/netcoreapp3.1/PublicAPI.Shipped.txt create mode 100644 src/StackExchange.Redis/StreamInfoField.cs create mode 100644 src/StackExchange.Redis/VectorSetInfoField.cs create mode 100644 tests/RESPite.Tests/CycleBufferTests.cs create mode 100644 tests/RESPite.Tests/RESPite.Tests.csproj create mode 100644 tests/RESPite.Tests/RespReaderTests.cs create mode 100644 tests/RESPite.Tests/RespScannerTests.cs create mode 100644 tests/RESPite.Tests/TestDuplexStream.cs rename tests/StackExchange.Redis.Benchmarks/{FastHashBenchmarks.cs => AsciiHashBenchmarks.cs} (64%) create mode 100644 tests/StackExchange.Redis.Benchmarks/AsciiHashSwitch.cs create mode 100644 tests/StackExchange.Redis.Benchmarks/EnumParseBenchmarks.cs create mode 100644 tests/StackExchange.Redis.Tests/AsciiHashUnitTests.cs create mode 100644 tests/StackExchange.Redis.Tests/DuplexStream.cs delete mode 100644 tests/StackExchange.Redis.Tests/FastHashTests.cs create mode 100644 tests/StackExchange.Redis.Tests/GlobalUsings.cs create mode 100644 toys/StackExchange.Redis.Server/GlobalUsings.cs create mode 100644 toys/StackExchange.Redis.Server/RespReaderExtensions.cs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a8ab53c74..0b62bc917 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -17,33 +17,33 @@ jobs: DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: "1" # Enable color output, even though the console output is redirected in Actions TERM: xterm # Enable color output in GitHub Actions steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetch the full history - - name: Start Redis Services (docker-compose) - working-directory: ./tests/RedisConfigs - run: docker compose -f docker-compose.yml up -d --wait - - name: Install .NET SDK - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 6.0.x - 8.0.x - 10.0.x - - name: .NET Build - run: dotnet build Build.csproj -c Release /p:CI=true - - name: StackExchange.Redis.Tests - run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true - - uses: dorny/test-reporter@v1 - continue-on-error: true - if: success() || failure() - with: - name: Test Results - Ubuntu - path: 'test-results/*.trx' - reporter: dotnet-trx - - name: .NET Lib Pack - run: dotnet pack src/StackExchange.Redis/StackExchange.Redis.csproj --no-build -c Release /p:Packing=true /p:PackageOutputPath=%CD%\.nupkgs /p:CI=true + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch the full history + - name: Start Redis Services (docker-compose) + working-directory: ./tests/RedisConfigs + run: docker compose -f docker-compose.yml up -d --wait + - name: Install .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 8.0.x + 10.0.x + - name: .NET Build + run: dotnet build Build.csproj -c Release /p:CI=true + - name: StackExchange.Redis.Tests + run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true + - uses: dorny/test-reporter@v1 + continue-on-error: true + if: success() || failure() + with: + name: Test Results - Ubuntu + path: 'test-results/*.trx' + reporter: dotnet-trx + - name: .NET Lib Pack + run: dotnet pack src/StackExchange.Redis/StackExchange.Redis.csproj --no-build -c Release /p:Packing=true /p:PackageOutputPath=%CD%\.nupkgs /p:CI=true windows: name: StackExchange.Redis (Windows Server 2022) @@ -54,99 +54,99 @@ jobs: TERM: xterm DOCKER_BUILDKIT: 1 steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetch the full history - - uses: Vampire/setup-wsl@v2 - with: - distribution: Ubuntu-22.04 - - name: Install Redis - shell: wsl-bash {0} - working-directory: ./tests/RedisConfigs - run: | - apt-get update - apt-get install curl gpg lsb-release libgomp1 jq -y - curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg - chmod 644 /usr/share/keyrings/redis-archive-keyring.gpg - echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/redis.list - apt-get update - apt-get install -y redis - mkdir redis - - name: Run redis-server - shell: wsl-bash {0} - working-directory: ./tests/RedisConfigs/redis - run: | - pwd - ls . - # Run each server instance in order - redis-server ../Basic/primary-6379.conf & - redis-server ../Basic/replica-6380.conf & - redis-server ../Basic/secure-6381.conf & - redis-server ../Failover/primary-6382.conf & - redis-server ../Failover/replica-6383.conf & - redis-server ../Cluster/cluster-7000.conf --dir ../Cluster & - redis-server ../Cluster/cluster-7001.conf --dir ../Cluster & - redis-server ../Cluster/cluster-7002.conf --dir ../Cluster & - redis-server ../Cluster/cluster-7003.conf --dir ../Cluster & - redis-server ../Cluster/cluster-7004.conf --dir ../Cluster & - redis-server ../Cluster/cluster-7005.conf --dir ../Cluster & - redis-server ../Sentinel/redis-7010.conf & - redis-server ../Sentinel/redis-7011.conf & - redis-server ../Sentinel/sentinel-26379.conf --sentinel & - redis-server ../Sentinel/sentinel-26380.conf --sentinel & - redis-server ../Sentinel/sentinel-26381.conf --sentinel & - # Wait for server instances to get ready - sleep 5 - echo "Checking redis-server version with port 6379" - redis-cli -p 6379 INFO SERVER | grep redis_version || echo "Failed to get version for port 6379" - echo "Checking redis-server version with port 6380" - redis-cli -p 6380 INFO SERVER | grep redis_version || echo "Failed to get version for port 6380" - echo "Checking redis-server version with port 6381" - redis-cli -p 6381 INFO SERVER | grep redis_version || echo "Failed to get version for port 6381" - echo "Checking redis-server version with port 6382" - redis-cli -p 6382 INFO SERVER | grep redis_version || echo "Failed to get version for port 6382" - echo "Checking redis-server version with port 6383" - redis-cli -p 6383 INFO SERVER | grep redis_version || echo "Failed to get version for port 6383" - echo "Checking redis-server version with port 7000" - redis-cli -p 7000 INFO SERVER | grep redis_version || echo "Failed to get version for port 7000" - echo "Checking redis-server version with port 7001" - redis-cli -p 7001 INFO SERVER | grep redis_version || echo "Failed to get version for port 7001" - echo "Checking redis-server version with port 7002" - redis-cli -p 7002 INFO SERVER | grep redis_version || echo "Failed to get version for port 7002" - echo "Checking redis-server version with port 7003" - redis-cli -p 7003 INFO SERVER | grep redis_version || echo "Failed to get version for port 7003" - echo "Checking redis-server version with port 7004" - redis-cli -p 7004 INFO SERVER | grep redis_version || echo "Failed to get version for port 7004" - echo "Checking redis-server version with port 7005" - redis-cli -p 7005 INFO SERVER | grep redis_version || echo "Failed to get version for port 7005" - echo "Checking redis-server version with port 7010" - redis-cli -p 7010 INFO SERVER | grep redis_version || echo "Failed to get version for port 7010" - echo "Checking redis-server version with port 7011" - redis-cli -p 7011 INFO SERVER | grep redis_version || echo "Failed to get version for port 7011" - echo "Checking redis-server version with port 26379" - redis-cli -p 26379 INFO SERVER | grep redis_version || echo "Failed to get version for port 26379" - echo "Checking redis-server version with port 26380" - redis-cli -p 26380 INFO SERVER | grep redis_version || echo "Failed to get version for port 26380" - echo "Checking redis-server version with port 26381" - redis-cli -p 26381 INFO SERVER | grep redis_version || echo "Failed to get version for port 26381" - continue-on-error: true + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch the full history + - uses: Vampire/setup-wsl@v2 + with: + distribution: Ubuntu-22.04 + - name: Install Redis + shell: wsl-bash {0} + working-directory: ./tests/RedisConfigs + run: | + apt-get update + apt-get install curl gpg lsb-release libgomp1 jq -y + curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg + chmod 644 /usr/share/keyrings/redis-archive-keyring.gpg + echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/redis.list + apt-get update + apt-get install -y redis + mkdir redis + - name: Run redis-server + shell: wsl-bash {0} + working-directory: ./tests/RedisConfigs/redis + run: | + pwd + ls . + # Run each server instance in order + redis-server ../Basic/primary-6379.conf & + redis-server ../Basic/replica-6380.conf & + redis-server ../Basic/secure-6381.conf & + redis-server ../Failover/primary-6382.conf & + redis-server ../Failover/replica-6383.conf & + redis-server ../Cluster/cluster-7000.conf --dir ../Cluster & + redis-server ../Cluster/cluster-7001.conf --dir ../Cluster & + redis-server ../Cluster/cluster-7002.conf --dir ../Cluster & + redis-server ../Cluster/cluster-7003.conf --dir ../Cluster & + redis-server ../Cluster/cluster-7004.conf --dir ../Cluster & + redis-server ../Cluster/cluster-7005.conf --dir ../Cluster & + redis-server ../Sentinel/redis-7010.conf & + redis-server ../Sentinel/redis-7011.conf & + redis-server ../Sentinel/sentinel-26379.conf --sentinel & + redis-server ../Sentinel/sentinel-26380.conf --sentinel & + redis-server ../Sentinel/sentinel-26381.conf --sentinel & + # Wait for server instances to get ready + sleep 5 + echo "Checking redis-server version with port 6379" + redis-cli -p 6379 INFO SERVER | grep redis_version || echo "Failed to get version for port 6379" + echo "Checking redis-server version with port 6380" + redis-cli -p 6380 INFO SERVER | grep redis_version || echo "Failed to get version for port 6380" + echo "Checking redis-server version with port 6381" + redis-cli -p 6381 INFO SERVER | grep redis_version || echo "Failed to get version for port 6381" + echo "Checking redis-server version with port 6382" + redis-cli -p 6382 INFO SERVER | grep redis_version || echo "Failed to get version for port 6382" + echo "Checking redis-server version with port 6383" + redis-cli -p 6383 INFO SERVER | grep redis_version || echo "Failed to get version for port 6383" + echo "Checking redis-server version with port 7000" + redis-cli -p 7000 INFO SERVER | grep redis_version || echo "Failed to get version for port 7000" + echo "Checking redis-server version with port 7001" + redis-cli -p 7001 INFO SERVER | grep redis_version || echo "Failed to get version for port 7001" + echo "Checking redis-server version with port 7002" + redis-cli -p 7002 INFO SERVER | grep redis_version || echo "Failed to get version for port 7002" + echo "Checking redis-server version with port 7003" + redis-cli -p 7003 INFO SERVER | grep redis_version || echo "Failed to get version for port 7003" + echo "Checking redis-server version with port 7004" + redis-cli -p 7004 INFO SERVER | grep redis_version || echo "Failed to get version for port 7004" + echo "Checking redis-server version with port 7005" + redis-cli -p 7005 INFO SERVER | grep redis_version || echo "Failed to get version for port 7005" + echo "Checking redis-server version with port 7010" + redis-cli -p 7010 INFO SERVER | grep redis_version || echo "Failed to get version for port 7010" + echo "Checking redis-server version with port 7011" + redis-cli -p 7011 INFO SERVER | grep redis_version || echo "Failed to get version for port 7011" + echo "Checking redis-server version with port 26379" + redis-cli -p 26379 INFO SERVER | grep redis_version || echo "Failed to get version for port 26379" + echo "Checking redis-server version with port 26380" + redis-cli -p 26380 INFO SERVER | grep redis_version || echo "Failed to get version for port 26380" + echo "Checking redis-server version with port 26381" + redis-cli -p 26381 INFO SERVER | grep redis_version || echo "Failed to get version for port 26381" + continue-on-error: true - - name: .NET Build - run: dotnet build Build.csproj -c Release /p:CI=true - - name: StackExchange.Redis.Tests - run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true - - uses: dorny/test-reporter@v1 - continue-on-error: true - if: success() || failure() - with: - name: Tests Results - Windows Server 2022 - path: 'test-results/*.trx' - reporter: dotnet-trx - # Package and upload to MyGet only on pushes to main, not on PRs - - name: .NET Pack - if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' - run: dotnet pack Build.csproj --no-build -c Release /p:PackageOutputPath=${env:GITHUB_WORKSPACE}\.nupkgs /p:CI=true - - name: Upload to MyGet - if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' - run: dotnet nuget push ${env:GITHUB_WORKSPACE}\.nupkgs\*.nupkg -s https://www.myget.org/F/stackoverflow/api/v2/package -k ${{ secrets.MYGET_API_KEY }} + - name: .NET Build + run: dotnet build Build.csproj -c Release /p:CI=true + - name: StackExchange.Redis.Tests + run: dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -c Release --logger trx --logger GitHubActions --results-directory ./test-results/ /p:CI=true + - uses: dorny/test-reporter@v1 + continue-on-error: true + if: success() || failure() + with: + name: Tests Results - Windows Server 2022 + path: 'test-results/*.trx' + reporter: dotnet-trx + # Package and upload to MyGet only on pushes to main, not on PRs + - name: .NET Pack + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' + run: dotnet pack Build.csproj --no-build -c Release /p:PackageOutputPath=${env:GITHUB_WORKSPACE}\.nupkgs /p:CI=true + - name: Upload to MyGet + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' + run: dotnet nuget push ${env:GITHUB_WORKSPACE}\.nupkgs\*.nupkg -s https://www.myget.org/F/stackoverflow/api/v2/package -k ${{ secrets.MYGET_API_KEY }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fa4a444bf..a03767211 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -7,7 +7,7 @@ on: # The branches below must be a subset of the branches above branches: [ 'main' ] workflow_dispatch: - + schedule: - cron: '8 9 * * 1' @@ -30,33 +30,34 @@ jobs: # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.0.x' - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - if: matrix.language == 'csharp' - name: .NET Build - run: dotnet build Build.csproj -c Release /p:CI=true - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 - with: - category: "/language:${{matrix.language}}" + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 10.0.x + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + - if: matrix.language == 'csharp' + name: .NET Build + run: dotnet build Build.csproj -c Release /p:CI=true + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/Build.csproj b/Build.csproj index 3e16e801c..41fb15b0c 100644 --- a/Build.csproj +++ b/Build.csproj @@ -1,5 +1,6 @@ + diff --git a/Directory.Build.props b/Directory.Build.props index e36f0f7d1..273acae25 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,7 +10,7 @@ true $(MSBuildThisFileDirectory)Shared.ruleset NETSDK1069 - $(NoWarn);NU5105;NU1507;SER001;SER002;SER003 + $(NoWarn);NU5105;NU1507;SER001;SER002;SER003;SER004;SER005 https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes https://stackexchange.github.io/StackExchange.Redis/ MIT @@ -42,4 +42,10 @@ + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 3fa9e0e3d..9767a0ab1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,6 +10,9 @@ + + + diff --git a/StackExchange.Redis.sln b/StackExchange.Redis.sln index adb1291de..4d275ad4f 100644 --- a/StackExchange.Redis.sln +++ b/StackExchange.Redis.sln @@ -21,6 +21,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Shared.ruleset = Shared.ruleset version.json = version.json tests\RedisConfigs\.docker\Redis\Dockerfile = tests\RedisConfigs\.docker\Redis\Dockerfile + .github\workflows\codeql.yml = .github\workflows\codeql.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RedisConfigs", "RedisConfigs", "{96E891CD-2ED7-4293-A7AB-4C6F5D8D2B05}" @@ -127,6 +128,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{5FA0958E-6EB EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Build", "eng\StackExchange.Redis.Build\StackExchange.Redis.Build.csproj", "{190742E1-FA50-4E36-A8C4-88AE87654340}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESPite", "src\RESPite\RESPite.csproj", "{05761CF5-CC46-43A6-814B-6BD2ECC1F0ED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RESPite.Tests", "tests\RESPite.Tests\RESPite.Tests.csproj", "{CA67D8CA-6CC9-40E2-8CAC-F0B1401BEF7B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -189,6 +194,14 @@ Global {190742E1-FA50-4E36-A8C4-88AE87654340}.Debug|Any CPU.Build.0 = Debug|Any CPU {190742E1-FA50-4E36-A8C4-88AE87654340}.Release|Any CPU.ActiveCfg = Release|Any CPU {190742E1-FA50-4E36-A8C4-88AE87654340}.Release|Any CPU.Build.0 = Release|Any CPU + {05761CF5-CC46-43A6-814B-6BD2ECC1F0ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05761CF5-CC46-43A6-814B-6BD2ECC1F0ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05761CF5-CC46-43A6-814B-6BD2ECC1F0ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05761CF5-CC46-43A6-814B-6BD2ECC1F0ED}.Release|Any CPU.Build.0 = Release|Any CPU + {CA67D8CA-6CC9-40E2-8CAC-F0B1401BEF7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA67D8CA-6CC9-40E2-8CAC-F0B1401BEF7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA67D8CA-6CC9-40E2-8CAC-F0B1401BEF7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA67D8CA-6CC9-40E2-8CAC-F0B1401BEF7B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -212,6 +225,8 @@ Global {69A0ACF2-DF1F-4F49-B554-F732DCA938A3} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} {59889284-FFEE-82E7-94CB-3B43E87DA6CF} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} {190742E1-FA50-4E36-A8C4-88AE87654340} = {5FA0958E-6EBD-45F4-808E-3447A293F96F} + {05761CF5-CC46-43A6-814B-6BD2ECC1F0ED} = {00CA0876-DA9F-44E8-B0DC-A88716BF347A} + {CA67D8CA-6CC9-40E2-8CAC-F0B1401BEF7B} = {73A5C363-CA1F-44C4-9A9B-EF791A76BA6A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {193AA352-6748-47C1-A5FC-C9AA6B5F000B} diff --git a/eng/StackExchange.Redis.Build/AsciiHash.md b/eng/StackExchange.Redis.Build/AsciiHash.md new file mode 100644 index 000000000..4a76ded62 --- /dev/null +++ b/eng/StackExchange.Redis.Build/AsciiHash.md @@ -0,0 +1,173 @@ +# AsciiHash + +Efficient matching of well-known short string tokens is a high-volume scenario, for example when matching RESP literals. + +The purpose of this generator is to efficiently interpret input tokens like `bin`, `f32`, etc - whether as byte or character data. + +There are multiple ways of using this tool, with the main distinction being whether you are confirming a single +token, or choosing between multiple tokens (in which case an `enum` is more appropriate): + +## Isolated literals (part 1) + +When using individual tokens, a `static partial class` can be used to generate helpers: + +``` c# +[AsciiHash] public static partial class bin { } +[AsciiHash] public static partial class f32 { } +``` + +Usually the token is inferred from the name; `[AsciiHash("real value")]` can be used if the token is not a valid identifier. +Underscores are replaced with hyphens, so a field called `my_token` has the default value `"my-token"`. +The generator demands *all* of `[AsciiHash] public static partial class`, and note that any *containing* types must +*also* be declared `partial`. + +The output is of the form: + +``` c# +static partial class bin +{ + public const int Length = 3; + public const long HashCS = ... + public const long HashUC = ... + public static ReadOnlySpan U8 => @"bin"u8; + public static string Text => @"bin"; + public static bool IsCS(in ReadOnlySpan value, long cs) => ... + public static bool IsCI(in RawResult value, long uc) => ... + +} +``` +The `CS` and `UC` are case-sensitive and case-insensitive (using upper-case) tools, respectively. + +(this API is strictly an internal implementation detail, and can change at any time) + +This generated code allows for fast, efficient, and safe matching of well-known tokens, for example: + +``` c# +var key = ... +var hash = key.HashCS(); +switch (key.Length) +{ + case bin.Length when bin.Is(key, hash): + // handle bin + break; + case f32.Length when f32.Is(key, hash): + // handle f32 + break; +} +``` + +The switch on the `Length` is optional, but recommended - these low values can often be implemented (by the compiler) +as a simple jump-table, which is very fast. However, switching on the hash itself is also valid. All hash matches +must also perform a sequence equality check - the `Is(value, hash)` convenience method validates both hash and equality. + +Note that `switch` requires `const` values, hence why we use generated *types* rather than partial-properties +that emit an instance with the known values. Also, the `"..."u8` syntax emits a span which is awkward to store, but +easy to return via a property. + +## Isolated literals (part 2) + +In some cases, you want to be able to say "match this value, only known at runtime". For this, note that `AsciiHash` +is also a `struct` that you can create an instance of and supply to code; the best way to do this is *inside* your +`partial class`: + +``` c# +[AsciiHash] +static partial class bin +{ + public static readonly AsciiHash Hash = new(U8); +} +``` + +Now, `bin.Hash` can be supplied to a caller that takes an `AsciiHash` instance (commonly with `in` semantics), +which then has *instance* methods for case-sensitive and case-insensitive matching; the instance already knows +the target hash and payload values. + +The `AsciiHash` returned implements `IEquatable` implementing case-sensitive equality; there are +also independent case-sensitive and case-insensitive comparers available via the static +`CaseSensitiveEqualityComparer` and `CaseInsensitiveEqualityComparer` properties respectively. + +Comparison values can be constructed on the fly on top of transient buffers using the constructors **that take +arrays**. Note that the other constructors may allocate on a per-usage basis. + +## Enum parsing (part 1) + +When identifying multiple values, an `enum` may be more convenient. Consider: + +``` c# +[AsciiHash] +public static partial bool TryParse(ReadOnlySpan value, out SomeEnum value); +``` + +This generates an efficient parser; inputs can be common `byte` or `char` types. Case sensitivity +is controlled by the optional `CaseSensitive` property on the attribute, or via a 3rd (`bool`) parameter +bbon the method, i.e. + +``` c# +[AsciiHash(CaseSensitive = false)] +public static partial bool TryParse(ReadOnlySpan value, out SomeEnum value); +``` + +or + +``` c# +[AsciiHash] +public static partial bool TryParse(ReadOnlySpan value, out SomeEnum value, bool caseSensitive = true); +``` + +Individual enum members can also be marked with `[AsciiHash("token value")]` to override the token payload. If +an enum member declares an empty explicit value (i.e. `[AsciiHash("")]`), then that member is ignored by the +tool; this is useful for marking "unknown" or "invalid" enum values (commonly the first enum, which by +convention has the value `0`): + +``` c# +public enum SomeEnum +{ + [AsciiHash("")] + Unknown, + SomeRealValue, + [AsciiHash("another-real-value")] + AnotherRealValue, + // ... +} +``` + +## Enum parsing (part 2) + +The tool has an *additional* facility when it comes to enums; you generally don't want to have to hard-code +things like buffer-lengths into your code, but when parsing an enum, you need to know how many bytes to read. + +The tool can generate a `static partial class` that contains the maximum length of any token in the enum, as well +as the maximum length of any token in bytes (when encoded as UTF-8). For example: + +``` c# +[AsciiHash("SomeTypeName")] +public enum SomeEnum +{ + // ... +} +``` + +This generates a class like the following: + +``` c# +static partial class SomeTypeName +{ + public const int EnumCount = 48; + public const int MaxChars = 11; + public const int MaxBytes = 11; // as UTF8 + public const int BufferBytes = 16; +} +``` + +The last of these is probably the most useful - it allows an additional byte (to rule out false-positives), +and rounds up to word-sizes, allowing for convenient stack-allocation - for example: + +``` c# +var span = reader.TryGetSpan(out var tmp) ? tmp : reader.Buffer(stackalloc byte[SomeTypeName.BufferBytes]); +if (TryParse(span, out var value)) +{ + // got a value +} +``` + +which allows for very efficient parsing of well-known tokens. \ No newline at end of file diff --git a/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs b/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs new file mode 100644 index 000000000..4fb411454 --- /dev/null +++ b/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs @@ -0,0 +1,774 @@ +using System.Buffers; +using System.Collections.Immutable; +using System.Reflection; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using RESPite; + +namespace StackExchange.Redis.Build; + +[Generator(LanguageNames.CSharp)] +public class AsciiHashGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // looking for [AsciiHash] partial static class Foo { } + var types = context.SyntaxProvider + .CreateSyntaxProvider( + static (node, _) => node is ClassDeclarationSyntax decl && IsStaticPartial(decl.Modifiers) && + HasAsciiHash(decl.AttributeLists), + TransformTypes) + .Where(pair => pair.Name is { Length: > 0 }) + .Collect(); + + // looking for [AsciiHash] partial static bool TryParse(input, out output) { } + var methods = context.SyntaxProvider + .CreateSyntaxProvider( + static (node, _) => node is MethodDeclarationSyntax decl && IsStaticPartial(decl.Modifiers) && + HasAsciiHash(decl.AttributeLists), + TransformMethods) + .Where(pair => pair.Name is { Length: > 0 }) + .Collect(); + + // looking for [AsciiHash("some type")] enum Foo { } + var enums = context.SyntaxProvider + .CreateSyntaxProvider( + static (node, _) => node is EnumDeclarationSyntax decl && HasAsciiHash(decl.AttributeLists), + TransformEnums) + .Where(pair => pair.Name is { Length: > 0 }) + .Collect(); + + context.RegisterSourceOutput( + types.Combine(methods).Combine(enums), + (ctx, content) => + Generate(ctx, content.Left.Left, content.Left.Right, content.Right)); + + static bool IsStaticPartial(SyntaxTokenList tokens) + => tokens.Any(SyntaxKind.StaticKeyword) && tokens.Any(SyntaxKind.PartialKeyword); + + static bool HasAsciiHash(SyntaxList attributeLists) + { + foreach (var attribList in attributeLists) + { + foreach (var attrib in attribList.Attributes) + { + if (attrib.Name.ToString() is nameof(AsciiHashAttribute) or nameof(AsciiHash)) return true; + } + } + + return false; + } + } + + private static string GetName(INamedTypeSymbol type) + { + if (type.ContainingType is null) return type.Name; + var stack = new Stack(); + while (true) + { + stack.Push(type.Name); + if (type.ContainingType is null) break; + type = type.ContainingType; + } + + var sb = new StringBuilder(stack.Pop()); + while (stack.Count != 0) + { + sb.Append('.').Append(stack.Pop()); + } + + return sb.ToString(); + } + + private static AttributeData? TryGetAsciiHashAttribute(ImmutableArray attributes) + { + foreach (var attrib in attributes) + { + if (attrib.AttributeClass is + { + Name: nameof(AsciiHashAttribute), + ContainingType: null, + ContainingNamespace: + { + Name: "RESPite", + ContainingNamespace.IsGlobalNamespace: true, + } + }) + { + return attrib; + } + } + + return null; + } + + private (string Namespace, string ParentType, string Name, int Count, int MaxChars, int MaxBytes) TransformEnums( + GeneratorSyntaxContext ctx, CancellationToken cancellationToken) + { + // extract the name and value (defaults to name, but can be overridden via attribute) and the location + if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not INamedTypeSymbol { TypeKind: TypeKind.Enum } named) return default; + if (TryGetAsciiHashAttribute(named.GetAttributes()) is not { } attrib) return default; + var innerName = GetRawValue("", attrib); + if (string.IsNullOrWhiteSpace(innerName)) return default; + + string ns = "", parentType = ""; + if (named.ContainingType is { } containingType) + { + parentType = GetName(containingType); + ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + } + else if (named.ContainingNamespace is { } containingNamespace) + { + ns = containingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + } + + int maxChars = 0, maxBytes = 0, count = 0; + foreach (var member in named.GetMembers()) + { + if (member.Kind is SymbolKind.Field) + { + var rawValue = GetRawValue(member.Name, TryGetAsciiHashAttribute(member.GetAttributes())); + if (string.IsNullOrWhiteSpace(rawValue)) continue; + + count++; + maxChars = Math.Max(maxChars, rawValue.Length); + maxBytes = Math.Max(maxBytes, Encoding.UTF8.GetByteCount(rawValue)); + } + } + return (ns, parentType, innerName, count, maxChars, maxBytes); + } + + private (string Namespace, string ParentType, string Name, string Value) TransformTypes( + GeneratorSyntaxContext ctx, + CancellationToken cancellationToken) + { + // extract the name and value (defaults to name, but can be overridden via attribute) and the location + if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not INamedTypeSymbol { TypeKind: TypeKind.Class } named) return default; + if (TryGetAsciiHashAttribute(named.GetAttributes()) is not { } attrib) return default; + + string ns = "", parentType = ""; + if (named.ContainingType is { } containingType) + { + parentType = GetName(containingType); + ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + } + else if (named.ContainingNamespace is { } containingNamespace) + { + ns = containingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + } + + string name = named.Name, value = GetRawValue(name, attrib); + if (string.IsNullOrWhiteSpace(value)) return default; + return (ns, parentType, name, value); + } + + private static string GetRawValue(string name, AttributeData? asciiHashAttribute) + { + var value = ""; + if (asciiHashAttribute is { ConstructorArguments.Length: 1 } + && asciiHashAttribute.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val) + { + value = val; + } + if (string.IsNullOrWhiteSpace(value)) + { + value = InferPayload(name); // if nothing explicit: infer from name + } + + return value; + } + + private static string InferPayload(string name) => name.Replace("_", "-"); + + private (string Namespace, string ParentType, Accessibility Accessibility, string Name, + (string Type, string Name, bool IsBytes, RefKind RefKind) From, (string Type, string Name, RefKind RefKind) To, + (string Name, bool Value, RefKind RefKind) CaseSensitive, + BasicArray<(string EnumMember, string ParseText)> Members, int DefaultValue) TransformMethods( + GeneratorSyntaxContext ctx, + CancellationToken cancellationToken) + { + if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not IMethodSymbol + { + IsStatic: true, + IsPartialDefinition: true, + PartialImplementationPart: null, + Arity: 0, + ReturnType.SpecialType: SpecialType.System_Boolean, + Parameters: + { + IsDefaultOrEmpty: false, + Length: 2 or 3, + }, + } method) return default; + + if (TryGetAsciiHashAttribute(method.GetAttributes()) is not { } attrib) return default; + + if (method.ContainingType is not { } containingType) return default; + var parentType = GetName(containingType); + var ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + + var arg = method.Parameters[0]; + if (arg is not { IsOptional: false, RefKind: RefKind.None or RefKind.In or RefKind.Ref or RefKind.RefReadOnlyParameter }) return default; + + static bool IsBytes(ITypeSymbol type) + { + // byte[] + if (type is IArrayTypeSymbol { ElementType: { SpecialType: SpecialType.System_Byte } }) + return true; + + // Span or ReadOnlySpan + if (type is INamedTypeSymbol { TypeKind: TypeKind.Struct, Arity: 1, Name: "Span" or "ReadOnlySpan", + ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true }, + TypeArguments: { Length: 1 } ta } + && ta[0].SpecialType == SpecialType.System_Byte) + { + return true; + } + return false; + } + + var fromType = arg.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + bool fromBytes = IsBytes(arg.Type); + var from = (fromType, arg.Name, fromBytes, arg.RefKind); + + arg = method.Parameters[1]; + if (arg is not + { + IsOptional: false, RefKind: RefKind.Out or RefKind.Ref, Type: INamedTypeSymbol { TypeKind: TypeKind.Enum } + }) return default; + var to = (arg.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), arg.Name, arg.RefKind); + + var members = arg.Type.GetMembers(); + var builder = new BasicArray<(string EnumMember, string ParseText)>.Builder(members.Length); + HashSet values = new(); + foreach (var member in members) + { + if (member is IFieldSymbol { IsStatic: true, IsConst: true } field) + { + var rawValue = GetRawValue(field.Name, TryGetAsciiHashAttribute(member.GetAttributes())); + if (string.IsNullOrWhiteSpace(rawValue)) continue; + builder.Add((field.Name, rawValue)); + int value = field.ConstantValue switch + { + sbyte i8 => i8, + short i16 => i16, + int i32 => i32, + long i64 => (int)i64, + byte u8 => u8, + ushort u16 => u16, + uint u32 => (int)u32, + ulong u64 => (int)u64, + char c16 => c16, + _ => 0, + }; + values.Add(value); + } + } + + (string, bool, RefKind) caseSensitive; + bool cs = IsCaseSensitive(attrib); + if (method.Parameters.Length > 2) + { + arg = method.Parameters[2]; + if (arg is not + { + RefKind: RefKind.None or RefKind.In or RefKind.Ref or RefKind.RefReadOnlyParameter, + Type.SpecialType: SpecialType.System_Boolean, + }) + { + return default; + } + + if (arg.IsOptional) + { + if (arg.ExplicitDefaultValue is not bool dv) return default; + cs = dv; + } + caseSensitive = (arg.Name, cs, arg.RefKind); + } + else + { + caseSensitive = ("", cs, RefKind.None); + } + + int defaultValue = 0; + if (values.Contains(0)) + { + int len = values.Count; + for (int i = 1; i <= len; i++) + { + if (!values.Contains(i)) + { + defaultValue = i; + break; + } + } + } + return (ns, parentType, method.DeclaredAccessibility, method.Name, from, to, caseSensitive, builder.Build(), defaultValue); + } + + private bool IsCaseSensitive(AttributeData attrib) + { + foreach (var member in attrib.NamedArguments) + { + if (member.Key == nameof(AsciiHashAttribute.CaseSensitive) + && member.Value.Kind is TypedConstantKind.Primitive + && member.Value.Value is bool caseSensitive) + { + return caseSensitive; + } + } + + return true; + } + + private string GetVersion() + { + var asm = GetType().Assembly; + if (asm.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false).FirstOrDefault() is + AssemblyFileVersionAttribute { Version: { Length: > 0 } } version) + { + return version.Version; + } + + return asm.GetName().Version?.ToString() ?? "??"; + } + + private void Generate( + SourceProductionContext ctx, + ImmutableArray<(string Namespace, string ParentType, string Name, string Value)> types, + ImmutableArray<(string Namespace, string ParentType, Accessibility Accessibility, string Name, + (string Type, string Name, bool IsBytes, RefKind RefKind) From, (string Type, string Name, RefKind RefKind) To, + (string Name, bool Value, RefKind RefKind) CaseSensitive, + BasicArray<(string EnumMember, string ParseText)> Members, int DefaultValue)> parseMethods, + ImmutableArray<(string Namespace, string ParentType, string Name, int Count, int MaxChars, int MaxBytes)> enums) + { + if (types.IsDefaultOrEmpty & parseMethods.IsDefaultOrEmpty & enums.IsDefaultOrEmpty) return; // nothing to do + + var sb = new StringBuilder("// ") + .AppendLine().Append("// ").Append(GetType().Name).Append(" v").Append(GetVersion()).AppendLine(); + + sb.AppendLine("using System;"); + sb.AppendLine("using StackExchange.Redis;"); + sb.AppendLine("#pragma warning disable CS8981, SER004"); + + BuildTypeImplementations(sb, types); + BuildEnumParsers(sb, parseMethods); + BuildEnumLengths(sb, enums); + ctx.AddSource(nameof(AsciiHash) + ".generated.cs", sb.ToString()); + } + + private void BuildEnumLengths(StringBuilder sb, ImmutableArray<(string Namespace, string ParentType, string Name, int Count, int MaxChars, int MaxBytes)> enums) + { + if (enums.IsDefaultOrEmpty) return; // nope + + int indent = 0; + StringBuilder NewLine() => sb.AppendLine().Append(' ', indent * 4); + + foreach (var grp in enums.GroupBy(l => (l.Namespace, l.ParentType))) + { + NewLine(); + int braces = 0; + if (!string.IsNullOrWhiteSpace(grp.Key.Namespace)) + { + NewLine().Append("namespace ").Append(grp.Key.Namespace); + NewLine().Append("{"); + indent++; + braces++; + } + + if (!string.IsNullOrWhiteSpace(grp.Key.ParentType)) + { + if (grp.Key.ParentType.Contains('.')) // nested types + { + foreach (var part in grp.Key.ParentType.Split('.')) + { + NewLine().Append("partial class ").Append(part); + NewLine().Append("{"); + indent++; + braces++; + } + } + else + { + NewLine().Append("partial class ").Append(grp.Key.ParentType); + NewLine().Append("{"); + indent++; + braces++; + } + } + + foreach (var @enum in grp) + { + NewLine().Append("internal static partial class ").Append(@enum.Name); + NewLine().Append("{"); + indent++; + NewLine().Append("public const int EnumCount = ").Append(@enum.Count).Append(";"); + NewLine().Append("public const int MaxChars = ").Append(@enum.MaxChars).Append(";"); + NewLine().Append("public const int MaxBytes = ").Append(@enum.MaxBytes).Append("; // as UTF8"); + // for buffer bytes: we want to allow 1 extra byte (to check for false-positive over-long values), + // and then round up to the nearest multiple of 8 (for stackalloc performance, etc) + int bufferBytes = (@enum.MaxBytes + 1 + 7) & ~7; + NewLine().Append("public const int BufferBytes = ").Append(bufferBytes).Append(";"); + indent--; + NewLine().Append("}"); + } + + // handle any closing braces + while (braces-- > 0) + { + indent--; + NewLine().Append("}"); + } + } + } + + private void BuildEnumParsers( + StringBuilder sb, + in ImmutableArray<(string Namespace, string ParentType, Accessibility Accessibility, string Name, + (string Type, string Name, bool IsBytes, RefKind RefKind) From, + (string Type, string Name, RefKind RefKind) To, + (string Name, bool Value, RefKind RefKind) CaseSensitive, + BasicArray<(string EnumMember, string ParseText)> Members, int DefaultValue)> enums) + { + if (enums.IsDefaultOrEmpty) return; // nope + + int indent = 0; + StringBuilder NewLine() => sb.AppendLine().Append(' ', indent * 4); + + foreach (var grp in enums.GroupBy(l => (l.Namespace, l.ParentType))) + { + NewLine(); + int braces = 0; + if (!string.IsNullOrWhiteSpace(grp.Key.Namespace)) + { + NewLine().Append("namespace ").Append(grp.Key.Namespace); + NewLine().Append("{"); + indent++; + braces++; + } + + if (!string.IsNullOrWhiteSpace(grp.Key.ParentType)) + { + if (grp.Key.ParentType.Contains('.')) // nested types + { + foreach (var part in grp.Key.ParentType.Split('.')) + { + NewLine().Append("partial class ").Append(part); + NewLine().Append("{"); + indent++; + braces++; + } + } + else + { + NewLine().Append("partial class ").Append(grp.Key.ParentType); + NewLine().Append("{"); + indent++; + braces++; + } + } + + foreach (var method in grp) + { + var line = NewLine().Append(Format(method.Accessibility)).Append(" static partial bool ") + .Append(method.Name).Append("(") + .Append(Format(method.From.RefKind)) + .Append(method.From.Type).Append(" ").Append(method.From.Name).Append(", ") + .Append(Format(method.To.RefKind)) + .Append(method.To.Type).Append(" ").Append(method.To.Name); + if (!string.IsNullOrEmpty(method.CaseSensitive.Name)) + { + line.Append(", ").Append(Format(method.CaseSensitive.RefKind)).Append("bool ") + .Append(method.CaseSensitive.Name); + } + line.Append(")"); + NewLine().Append("{"); + indent++; + NewLine().Append("// ").Append(method.To.Type).Append(" has ").Append(method.Members.Length).Append(" members"); + string valueTarget = method.To.Name; + if (method.To.RefKind != RefKind.Out) + { + valueTarget = "__tmp"; + NewLine().Append(method.To.Type).Append(" ").Append(valueTarget).Append(";"); + } + + bool alwaysCaseSensitive = + string.IsNullOrEmpty(method.CaseSensitive.Name) && method.CaseSensitive.Value; + if (!alwaysCaseSensitive && !HasCaseSensitiveCharacters(method.Members)) + { + alwaysCaseSensitive = true; + } + + bool twoPart = method.Members.Max(x => x.ParseText.Length) > AsciiHash.MaxBytesHashed; + if (alwaysCaseSensitive) + { + if (twoPart) + { + NewLine().Append("global::RESPite.AsciiHash.HashCS(").Append(method.From.Name).Append(", out var cs0, out var cs1);"); + } + else + { + NewLine().Append("var cs0 = global::RESPite.AsciiHash.HashCS(").Append(method.From.Name).Append(");"); + } + } + else + { + if (twoPart) + { + NewLine().Append("global::RESPite.AsciiHash.Hash(").Append(method.From.Name) + .Append(", out var cs0, out var uc0, out var cs1, out var uc1);"); + } + else + { + NewLine().Append("global::RESPite.AsciiHash.Hash(").Append(method.From.Name) + .Append(", out var cs0, out var uc0);"); + } + } + + if (string.IsNullOrEmpty(method.CaseSensitive.Name)) + { + Write(method.CaseSensitive.Value); + } + else + { + NewLine().Append("if (").Append(method.CaseSensitive.Name).Append(")"); + NewLine().Append("{"); + indent++; + Write(true); + indent--; + NewLine().Append("}"); + NewLine().Append("else"); + NewLine().Append("{"); + indent++; + Write(false); + indent--; + NewLine().Append("}"); + } + + if (method.To.RefKind == RefKind.Out) + { + NewLine().Append("if (").Append(valueTarget).Append(" == (") + .Append(method.To.Type).Append(")").Append(method.DefaultValue).Append(")"); + NewLine().Append("{"); + indent++; + NewLine().Append("// by convention, init to zero on miss"); + NewLine().Append(valueTarget).Append(" = default;"); + NewLine().Append("return false;"); + indent--; + NewLine().Append("}"); + NewLine().Append("return true;"); + } + else + { + NewLine().Append("// do not update parameter on miss"); + NewLine().Append("if (").Append(valueTarget).Append(" == (") + .Append(method.To.Type).Append(")").Append(method.DefaultValue).Append(") return false;"); + NewLine().Append(method.To.Name).Append(" = ").Append(valueTarget).Append(";"); + NewLine().Append("return true;"); + } + + void Write(bool caseSensitive) + { + NewLine().Append(valueTarget).Append(" = ").Append(method.From.Name).Append(".Length switch {"); + indent++; + foreach (var member in method.Members + .OrderBy(x => x.ParseText.Length) + .ThenBy(x => x.ParseText)) + { + var len = member.ParseText.Length; + AsciiHash.Hash(member.ParseText, out var cs0, out var uc0, out var cs1, out var uc1); + + bool valueCaseSensitive = caseSensitive || !HasCaseSensitiveCharacters(member.ParseText); + + line = NewLine().Append(len).Append(" when "); + if (twoPart) line.Append("("); + if (valueCaseSensitive) + { + line.Append("cs0 is ").Append(cs0); + } + else + { + line.Append("uc0 is ").Append(uc0); + } + + if (len > AsciiHash.MaxBytesHashed) + { + if (valueCaseSensitive) + { + line.Append(" & cs1 is ").Append(cs1); + } + else + { + line.Append(" & uc1 is ").Append(uc1); + } + } + if (twoPart) line.Append(")"); + if (len > 2 * AsciiHash.MaxBytesHashed) + { + line.Append(" && "); + var csValue = SyntaxFactory + .LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(member.ParseText.Substring(2 * AsciiHash.MaxBytesHashed))) + .ToFullString(); + + line.Append("global::RESPite.AsciiHash.") + .Append(valueCaseSensitive ? nameof(AsciiHash.SequenceEqualsCS) : nameof(AsciiHash.SequenceEqualsCI)) + .Append("(").Append(method.From.Name).Append(".Slice(").Append(2 * AsciiHash.MaxBytesHashed).Append("), ").Append(csValue); + if (method.From.IsBytes) line.Append("u8"); + line.Append(")"); + } + + line.Append(" => ").Append(method.To.Type).Append(".").Append(member.EnumMember).Append(","); + } + + NewLine().Append("_ => (").Append(method.To.Type).Append(")").Append(method.DefaultValue) + .Append(","); + indent--; + NewLine().Append("};"); + } + + indent--; + NewLine().Append("}"); + } + + // handle any closing braces + while (braces-- > 0) + { + indent--; + NewLine().Append("}"); + } + } + } + + private static bool HasCaseSensitiveCharacters(string value) + { + foreach (char c in value ?? "") + { + if (char.IsLetter(c)) return true; + } + + return false; + } + + private static bool HasCaseSensitiveCharacters(BasicArray<(string EnumMember, string ParseText)> members) + { + // do we have alphabet characters? case sensitivity doesn't apply if not + foreach (var member in members) + { + if (HasCaseSensitiveCharacters(member.ParseText)) return true; + } + + return false; + } + + private static string Format(RefKind refKind) => refKind switch + { + RefKind.None => "", + RefKind.In => "in ", + RefKind.Out => "out ", + RefKind.Ref => "ref ", + RefKind.RefReadOnlyParameter or RefKind.RefReadOnly => "ref readonly ", + _ => throw new NotSupportedException($"RefKind {refKind} is not yet supported."), + }; + private static string Format(Accessibility accessibility) => accessibility switch + { + Accessibility.Public => "public", + Accessibility.Private => "private", + Accessibility.Internal => "internal", + Accessibility.Protected => "protected", + Accessibility.ProtectedAndInternal => "private protected", + Accessibility.ProtectedOrInternal => "protected internal", + _ => throw new NotSupportedException($"Accessibility {accessibility} is not yet supported."), + }; + + private static void BuildTypeImplementations( + StringBuilder sb, + in ImmutableArray<(string Namespace, string ParentType, string Name, string Value)> types) + { + if (types.IsDefaultOrEmpty) return; // nope + + int indent = 0; + StringBuilder NewLine() => sb.AppendLine().Append(' ', indent * 4); + + foreach (var grp in types.GroupBy(l => (l.Namespace, l.ParentType))) + { + NewLine(); + int braces = 0; + if (!string.IsNullOrWhiteSpace(grp.Key.Namespace)) + { + NewLine().Append("namespace ").Append(grp.Key.Namespace); + NewLine().Append("{"); + indent++; + braces++; + } + + if (!string.IsNullOrWhiteSpace(grp.Key.ParentType)) + { + if (grp.Key.ParentType.Contains('.')) // nested types + { + foreach (var part in grp.Key.ParentType.Split('.')) + { + NewLine().Append("partial class ").Append(part); + NewLine().Append("{"); + indent++; + braces++; + } + } + else + { + NewLine().Append("partial class ").Append(grp.Key.ParentType); + NewLine().Append("{"); + indent++; + braces++; + } + } + + foreach (var literal in grp) + { + // perform string escaping on the generated value (this includes the quotes, note) + var csValue = SyntaxFactory + .LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(literal.Value)) + .ToFullString(); + + AsciiHash.Hash(literal.Value, out var hashCS, out var hashUC); + NewLine().Append("static partial class ").Append(literal.Name); + NewLine().Append("{"); + indent++; + NewLine().Append("public const int Length = ").Append(literal.Value.Length).Append(';'); + NewLine().Append("public const long HashCS = ").Append(hashCS).Append(';'); + NewLine().Append("public const long HashUC = ").Append(hashUC).Append(';'); + NewLine().Append("public static ReadOnlySpan U8 => ").Append(csValue).Append("u8;"); + NewLine().Append("public const string Text = ").Append(csValue).Append(';'); + if (literal.Value.Length <= AsciiHash.MaxBytesHashed) + { + // the case-sensitive hash enforces all the values + NewLine().Append( + "public static bool IsCS(ReadOnlySpan value, long cs) => cs == HashCS & value.Length == Length;"); + NewLine().Append( + "public static bool IsCI(ReadOnlySpan value, long uc) => uc == HashUC & value.Length == Length;"); + } + else + { + NewLine().Append( + "public static bool IsCS(ReadOnlySpan value, long cs) => cs == HashCS && value.SequenceEqual(U8);"); + NewLine().Append( + "public static bool IsCI(ReadOnlySpan value, long uc) => uc == HashUC && global::RESPite.AsciiHash.SequenceEqualsCI(value, U8);"); + } + + indent--; + NewLine().Append("}"); + } + + // handle any closing braces + while (braces-- > 0) + { + indent--; + NewLine().Append("}"); + } + } + } +} diff --git a/eng/StackExchange.Redis.Build/BasicArray.cs b/eng/StackExchange.Redis.Build/BasicArray.cs new file mode 100644 index 000000000..dc7984c75 --- /dev/null +++ b/eng/StackExchange.Redis.Build/BasicArray.cs @@ -0,0 +1,85 @@ +using System.Collections; + +namespace StackExchange.Redis.Build; + +// like ImmutableArray, but with decent equality semantics +public readonly struct BasicArray : IEquatable>, IReadOnlyList +{ + private readonly T[] _elements; + + private BasicArray(T[] elements, int length) + { + _elements = elements; + Length = length; + } + + private static readonly EqualityComparer _comparer = EqualityComparer.Default; + + public int Length { get; } + public bool IsEmpty => Length == 0; + + public ref readonly T this[int index] + { + get + { + if (index < 0 | index >= Length) Throw(); + return ref _elements[index]; + + static void Throw() => throw new IndexOutOfRangeException(); + } + } + + public ReadOnlySpan Span => _elements.AsSpan(0, Length); + + public bool Equals(BasicArray other) + { + if (Length != other.Length) return false; + var y = other.Span; + int i = 0; + foreach (ref readonly T el in this.Span) + { + if (!_comparer.Equals(el, y[i])) return false; + } + + return true; + } + + public ReadOnlySpan.Enumerator GetEnumerator() => Span.GetEnumerator(); + + private IEnumerator EnumeratorCore() + { + for (int i = 0; i < Length; i++) yield return this[i]; + } + + public override bool Equals(object? obj) => obj is BasicArray other && Equals(other); + + public override int GetHashCode() + { + var hash = Length; + foreach (ref readonly T el in this.Span) + { + _ = (hash * -37) + _comparer.GetHashCode(el); + } + + return hash; + } + IEnumerator IEnumerable.GetEnumerator() => EnumeratorCore(); + IEnumerator IEnumerable.GetEnumerator() => EnumeratorCore(); + + int IReadOnlyCollection.Count => Length; + T IReadOnlyList.this[int index] => this[index]; + + public struct Builder(int maxLength) + { + public int Count { get; private set; } + private readonly T[] elements = maxLength == 0 ? [] : new T[maxLength]; + + public void Add(in T value) + { + elements[Count] = value; + Count++; + } + + public BasicArray Build() => new(elements, Count); + } +} diff --git a/eng/StackExchange.Redis.Build/FastHashGenerator.cs b/eng/StackExchange.Redis.Build/FastHashGenerator.cs deleted file mode 100644 index cdbc94ebe..000000000 --- a/eng/StackExchange.Redis.Build/FastHashGenerator.cs +++ /dev/null @@ -1,215 +0,0 @@ -using System.Buffers; -using System.Collections.Immutable; -using System.Reflection; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace StackExchange.Redis.Build; - -[Generator(LanguageNames.CSharp)] -public class FastHashGenerator : IIncrementalGenerator -{ - public void Initialize(IncrementalGeneratorInitializationContext context) - { - var literals = context.SyntaxProvider - .CreateSyntaxProvider(Predicate, Transform) - .Where(pair => pair.Name is { Length: > 0 }) - .Collect(); - - context.RegisterSourceOutput(literals, Generate); - } - - private bool Predicate(SyntaxNode node, CancellationToken cancellationToken) - { - // looking for [FastHash] partial static class Foo { } - if (node is ClassDeclarationSyntax decl - && decl.Modifiers.Any(SyntaxKind.StaticKeyword) - && decl.Modifiers.Any(SyntaxKind.PartialKeyword)) - { - foreach (var attribList in decl.AttributeLists) - { - foreach (var attrib in attribList.Attributes) - { - if (attrib.Name.ToString() is "FastHashAttribute" or "FastHash") return true; - } - } - } - - return false; - } - - private static string GetName(INamedTypeSymbol type) - { - if (type.ContainingType is null) return type.Name; - var stack = new Stack(); - while (true) - { - stack.Push(type.Name); - if (type.ContainingType is null) break; - type = type.ContainingType; - } - var sb = new StringBuilder(stack.Pop()); - while (stack.Count != 0) - { - sb.Append('.').Append(stack.Pop()); - } - return sb.ToString(); - } - - private (string Namespace, string ParentType, string Name, string Value) Transform( - GeneratorSyntaxContext ctx, - CancellationToken cancellationToken) - { - // extract the name and value (defaults to name, but can be overridden via attribute) and the location - if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not INamedTypeSymbol named) return default; - string ns = "", parentType = ""; - if (named.ContainingType is { } containingType) - { - parentType = GetName(containingType); - ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); - } - else if (named.ContainingNamespace is { } containingNamespace) - { - ns = containingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); - } - - string name = named.Name, value = ""; - foreach (var attrib in named.GetAttributes()) - { - if (attrib.AttributeClass?.Name == "FastHashAttribute") - { - if (attrib.ConstructorArguments.Length == 1) - { - if (attrib.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val) - { - value = val; - break; - } - } - } - } - - if (string.IsNullOrWhiteSpace(value)) - { - value = name.Replace("_", "-"); // if nothing explicit: infer from name - } - - return (ns, parentType, name, value); - } - - private string GetVersion() - { - var asm = GetType().Assembly; - if (asm.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false).FirstOrDefault() is - AssemblyFileVersionAttribute { Version: { Length: > 0 } } version) - { - return version.Version; - } - - return asm.GetName().Version?.ToString() ?? "??"; - } - - private void Generate( - SourceProductionContext ctx, - ImmutableArray<(string Namespace, string ParentType, string Name, string Value)> literals) - { - if (literals.IsDefaultOrEmpty) return; - - var sb = new StringBuilder("// ") - .AppendLine().Append("// ").Append(GetType().Name).Append(" v").Append(GetVersion()).AppendLine(); - - // lease a buffer that is big enough for the longest string - var buffer = ArrayPool.Shared.Rent( - Encoding.UTF8.GetMaxByteCount(literals.Max(l => l.Value.Length))); - int indent = 0; - - StringBuilder NewLine() => sb.AppendLine().Append(' ', indent * 4); - NewLine().Append("using System;"); - NewLine().Append("using StackExchange.Redis;"); - NewLine().Append("#pragma warning disable CS8981"); - foreach (var grp in literals.GroupBy(l => (l.Namespace, l.ParentType))) - { - NewLine(); - int braces = 0; - if (!string.IsNullOrWhiteSpace(grp.Key.Namespace)) - { - NewLine().Append("namespace ").Append(grp.Key.Namespace); - NewLine().Append("{"); - indent++; - braces++; - } - if (!string.IsNullOrWhiteSpace(grp.Key.ParentType)) - { - if (grp.Key.ParentType.Contains('.')) // nested types - { - foreach (var part in grp.Key.ParentType.Split('.')) - { - NewLine().Append("partial class ").Append(part); - NewLine().Append("{"); - indent++; - braces++; - } - } - else - { - NewLine().Append("partial class ").Append(grp.Key.ParentType); - NewLine().Append("{"); - indent++; - braces++; - } - } - - foreach (var literal in grp) - { - int len; - unsafe - { - fixed (byte* bPtr = buffer) // netstandard2.0 forces fallback API - { - fixed (char* cPtr = literal.Value) - { - len = Encoding.UTF8.GetBytes(cPtr, literal.Value.Length, bPtr, buffer.Length); - } - } - } - - // perform string escaping on the generated value (this includes the quotes, note) - var csValue = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(literal.Value)).ToFullString(); - - var hash = FastHash.Hash64(buffer.AsSpan(0, len)); - NewLine().Append("static partial class ").Append(literal.Name); - NewLine().Append("{"); - indent++; - NewLine().Append("public const int Length = ").Append(len).Append(';'); - NewLine().Append("public const long Hash = ").Append(hash).Append(';'); - NewLine().Append("public static ReadOnlySpan U8 => ").Append(csValue).Append("u8;"); - NewLine().Append("public const string Text = ").Append(csValue).Append(';'); - if (len <= 8) - { - // the hash enforces all the values - NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.Payload.Length == Length;"); - NewLine().Append("public static bool Is(long hash, ReadOnlySpan value) => hash == Hash & value.Length == Length;"); - } - else - { - NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8);"); - NewLine().Append("public static bool Is(long hash, ReadOnlySpan value) => hash == Hash && value.SequenceEqual(U8);"); - } - indent--; - NewLine().Append("}"); - } - - // handle any closing braces - while (braces-- > 0) - { - indent--; - NewLine().Append("}"); - } - } - - ArrayPool.Shared.Return(buffer); - ctx.AddSource("FastHash.generated.cs", sb.ToString()); - } -} diff --git a/eng/StackExchange.Redis.Build/FastHashGenerator.md b/eng/StackExchange.Redis.Build/FastHashGenerator.md deleted file mode 100644 index 7fc5103ae..000000000 --- a/eng/StackExchange.Redis.Build/FastHashGenerator.md +++ /dev/null @@ -1,64 +0,0 @@ -# FastHashGenerator - -Efficient matching of well-known short string tokens is a high-volume scenario, for example when matching RESP literals. - -The purpose of this generator is to interpret inputs like: - -``` c# -[FastHash] public static partial class bin { } -[FastHash] public static partial class f32 { } -``` - -Usually the token is inferred from the name; `[FastHash("real value")]` can be used if the token is not a valid identifier. -Underscore is replaced with hyphen, so a field called `my_token` has the default value `"my-token"`. -The generator demands *all* of `[FastHash] public static partial class`, and note that any *containing* types must -*also* be declared `partial`. - -The output is of the form: - -``` c# -static partial class bin -{ - public const int Length = 3; - public const long Hash = 7235938; - public static ReadOnlySpan U8 => @"bin"u8; - public static string Text => @"bin"; - public static bool Is(long hash, in RawResult value) => ... - public static bool Is(long hash, in ReadOnlySpan value) => ... -} -static partial class f32 -{ - public const int Length = 3; - public const long Hash = 3289958; - public static ReadOnlySpan U8 => @"f32"u8; - public const string Text = @"f32"; - public static bool Is(long hash, in RawResult value) => ... - public static bool Is(long hash, in ReadOnlySpan value) => ... -} -``` - -(this API is strictly an internal implementation detail, and can change at any time) - -This generated code allows for fast, efficient, and safe matching of well-known tokens, for example: - -``` c# -var key = ... -var hash = key.Hash64(); -switch (key.Length) -{ - case bin.Length when bin.Is(hash, key): - // handle bin - break; - case f32.Length when f32.Is(hash, key): - // handle f32 - break; -} -``` - -The switch on the `Length` is optional, but recommended - these low values can often be implemented (by the compiler) -as a simple jump-table, which is very fast. However, switching on the hash itself is also valid. All hash matches -must also perform a sequence equality check - the `Is(hash, value)` convenience method validates both hash and equality. - -Note that `switch` requires `const` values, hence why we use generated *types* rather than partial-properties -that emit an instance with the known values. Also, the `"..."u8` syntax emits a span which is awkward to store, but -easy to return via a property. diff --git a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj index f875133ba..3cde6f5f6 100644 --- a/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj +++ b/eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj @@ -12,8 +12,11 @@ - - FastHash.cs + + Shared/AsciiHash.cs + + + Shared/Experiments.cs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 40f59348d..27366ae98 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -11,7 +11,4 @@ - - - diff --git a/src/RESPite/Buffers/CycleBuffer.cs b/src/RESPite/Buffers/CycleBuffer.cs new file mode 100644 index 000000000..14774b357 --- /dev/null +++ b/src/RESPite/Buffers/CycleBuffer.cs @@ -0,0 +1,753 @@ +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using RESPite.Internal; + +namespace RESPite.Buffers; + +/// +/// Manages the state for a based IO buffer. Unlike Pipe, +/// it is not intended for a separate producer-consumer - there is no thread-safety, and no +/// activation; it just handles the buffers. It is intended to be used as a mutable (non-readonly) +/// field in a type that performs IO; the internal state mutates - it should not be passed around. +/// +/// Notionally, there is an uncommitted area (write) and a committed area (read). Process: +/// - producer loop (*note no concurrency**) +/// - call to get a new scratch +/// - (write to that span) +/// - call to mark complete portions +/// - consumer loop (*note no concurrency**) +/// - call to see if there is a single-span chunk; otherwise +/// - call to get the multi-span chunk +/// - (process none, some, or all of that data) +/// - call to indicate how much data is no longer needed +/// Emphasis: no concurrency! This is intended for a single worker acting as both producer and consumer. +/// +/// There is a *lot* of validation in debug mode; we want to be super sure that we don't corrupt buffer state. +/// +[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] +public partial struct CycleBuffer +{ + #if TRACK_MEMORY + private static MemoryPool DefaultPool => MemoryTrackedPool.Shared; + #else + private static MemoryPool DefaultPool => MemoryPool.Shared; + #endif + + // note: if someone uses an uninitialized CycleBuffer (via default): that's a skills issue; git gud + public static CycleBuffer Create( + MemoryPool? pool = null, + int pageSize = DefaultPageSize, + ICycleBufferCallback? callback = null) + { + pool ??= DefaultPool; + if (pageSize <= 0) pageSize = DefaultPageSize; + if (pageSize > pool.MaxBufferSize) pageSize = pool.MaxBufferSize; + return new CycleBuffer(pool, pageSize, callback); + } + + private CycleBuffer(MemoryPool pool, int pageSize, ICycleBufferCallback? callback) + { + Pool = pool; + PageSize = pageSize; + _callback = callback; + leasedStart = -1; + } + + private const int DefaultPageSize = 8 * 1024; + + public int PageSize { get; } + public MemoryPool Pool { get; } + private readonly ICycleBufferCallback? _callback; + + private Segment? startSegment, endSegment; + + private int endSegmentCommitted, endSegmentLength; + private int leasedStart; + + public bool TryGetCommitted(out ReadOnlySpan span) + { + DebugAssertValid(); + if (!ReferenceEquals(startSegment, endSegment)) + { + span = default; + return false; + } + + span = startSegment is null ? default : startSegment.Memory.Span.Slice(start: 0, length: endSegmentCommitted); + return true; + } + + /// + /// Commits data written to buffers from , making it available for consumption + /// via . This compares to . + /// + public void Commit(int count) + { + DebugAssertValid(); + if (leasedStart < 0) + { + ThrowNoLease(); + } + + if (count <= 0) + { + if (count < 0) ThrowCount(); + return; + } + + var available = endSegmentLength - endSegmentCommitted; + if (count > available) ThrowCount(); + + var afterLeasedStart = endSegment!.StartTrimCount + endSegmentCommitted; + + if (leasedStart != afterLeasedStart) CopyDueToDiscardDuringWrite(count); + endSegmentCommitted += count; + DebugAssertValid(); + + static void ThrowCount() => throw new ArgumentOutOfRangeException(nameof(count)); + static void ThrowNoLease() => throw new InvalidOperationException("No open lease"); + } + + private void CopyDueToDiscardDuringWrite(int count) + { + var targetOffset = endSegment!.StartTrimCount + endSegmentCommitted; + if (targetOffset != leasedStart) + { + var full = endSegment.UntrimmedMemory.Span; + full.Slice(leasedStart, count) + .CopyTo(full.Slice(targetOffset, count)); + } + } + public bool CommittedIsEmpty => ReferenceEquals(startSegment, endSegment) & endSegmentCommitted == 0; + + /// + /// Marks committed data as fully consumed; it will no longer appear in later calls to . + /// + public void DiscardCommitted(int count) + { + DebugAssertValid(); + if (count == 0) return; + + // optimize for most common case, where we consume everything + if (ReferenceEquals(startSegment, endSegment) + & count == endSegmentCommitted + & count > 0) + { + /* + we are consuming all the data in the single segment; we can + just reset that segment back to full size and re-use as-is; + note that we also know that there must *be* a segment + for the count check to pass + */ + endSegmentCommitted = 0; + endSegmentLength = endSegment!.Untrim(expandBackwards: true); + DebugAssertValid(0); + DebugCounters.OnDiscardFull(count); + } + else + { + DiscardCommittedSlow(count); + } + } + + public void DiscardCommitted(long count) + { + DebugAssertValid(); + if (count == 0) return; + + // optimize for most common case, where we consume everything + if (ReferenceEquals(startSegment, endSegment) + & count == endSegmentCommitted + & count > 0) // checks sign *and* non-trimmed + { + // see for logic + endSegmentCommitted = 0; + endSegmentLength = endSegment!.Untrim(expandBackwards: true); + DebugAssertValid(0); + DebugCounters.OnDiscardFull(count); + } + else + { + DiscardCommittedSlow(count); + } + } + + private void DiscardCommittedSlow(long count) + { + DebugCounters.OnDiscardPartial(count); + DebugAssertValid(); +#if DEBUG + var originalLength = GetCommittedLength(); + var originalCount = count; + var expectedLength = originalLength - originalCount; + string blame = nameof(DiscardCommittedSlow); +#endif + while (count > 0) + { + DebugAssertValid(); + var segment = startSegment; + if (segment is null) break; + if (ReferenceEquals(segment, endSegment)) + { + // first==final==only segment + if (count == endSegmentCommitted) + { + endSegmentLength = startSegment!.Untrim(); + endSegmentCommitted = 0; // = untrimmed and unused +#if DEBUG + blame += ",full-final (t)"; +#endif + } + else + { + // discard from the start (note: don't need to compensate with writingCopyOffset until we untrim) + int count32 = checked((int)count); + segment.TrimStart(count32); + endSegmentLength -= count32; + endSegmentCommitted -= count32; +#if DEBUG + blame += ",partial-final"; +#endif + } + + count = 0; + break; + } + else if (count < segment.Length) + { + // multiple, but can take some (not all) of the first buffer +#if DEBUG + var len = segment.Length; +#endif + segment.TrimStart((int)count); + Debug.Assert(segment.Length > 0, "parial trim should have left non-empty segment"); +#if DEBUG + Debug.Assert(segment.Length == len - count, "trim failure"); + blame += ",partial-first"; +#endif + count = 0; + break; + } + else + { + // multiple; discard the entire first segment + count -= segment.Length; + startSegment = + segment.ResetAndGetNext(); // we already did a ref-check, so we know this isn't going past endSegment + endSegment!.AppendOrRecycle(segment, maxDepth: 2); + DebugAssertValid(); +#if DEBUG + blame += ",full-first"; +#endif + } + } + + if (count != 0) ThrowCount(); +#if DEBUG + DebugAssertValid(expectedLength, blame); + _ = originalLength; + _ = originalCount; +#endif + + [DoesNotReturn] + static void ThrowCount() => throw new ArgumentOutOfRangeException(nameof(count)); + } + + [Conditional("DEBUG")] + private void DebugAssertValid(long expectedCommittedLength, [CallerMemberName] string caller = "") + { + DebugAssertValid(); + var actual = GetCommittedLength(); + Debug.Assert( + expectedCommittedLength >= 0, + $"Expected committed length is just... wrong: {expectedCommittedLength} (from {caller})"); + Debug.Assert( + expectedCommittedLength == actual, + $"Committed length mismatch: expected {expectedCommittedLength}, got {actual} (from {caller})"); + } + + [Conditional("DEBUG")] + private void DebugAssertValid() + { +#if DEBUG + if (startSegment is null) + { + Debug.Assert( + endSegmentLength == 0 & endSegmentCommitted == 0, + "un-init state should be zero"); + return; + } + + Debug.Assert(endSegment is not null, "end segment must not be null if start segment exists"); + Debug.Assert( + endSegmentLength == endSegment!.Length, + $"end segment length is incorrect - expected {endSegmentLength}, got {endSegment.Length}"); + Debug.Assert(endSegmentCommitted <= endSegmentLength, $"end segment is over-committed - {endSegmentCommitted} of {endSegmentLength}"); + + // check running indices + startSegment?.DebugAssertValidChain(); +#endif + } + + public long GetCommittedLength() + { + if (ReferenceEquals(startSegment, endSegment)) + { + return endSegmentCommitted; + } + + // note that the start-segment is pre-trimmed; we don't need to account for an offset on the left + return (endSegment!.RunningIndex + endSegmentCommitted) - startSegment!.RunningIndex; + } + + /// + /// When used with , this means "any non-empty buffer". + /// + public const int GetAnything = 0; + + /// + /// When used with , this means "any full buffer". + /// + public const int GetFullPagesOnly = -1; + + public bool TryGetFirstCommittedSpan(int minBytes, out ReadOnlySpan span) + { + DebugAssertValid(); + if (TryGetFirstCommittedMemory(minBytes, out var memory)) + { + span = memory.Span; + return true; + } + + span = default; + return false; + } + + /// + /// The minLength arg: -ve means "full segments only" (useful when buffering outbound network data to avoid + /// packet fragmentation); otherwise, it is the minimum length we want. + /// + public bool TryGetFirstCommittedMemory(int minBytes, out ReadOnlyMemory memory) + { + if (minBytes == 0) minBytes = 1; // success always means "at least something" + DebugAssertValid(); + if (ReferenceEquals(startSegment, endSegment)) + { + // single page + var available = endSegmentCommitted; + if (available == 0) + { + // empty (includes uninitialized) + memory = default; + return false; + } + + memory = startSegment!.Memory; + var memLength = memory.Length; + if (available == memLength) + { + // full segment; is it enough to make the caller happy? + return available >= minBytes; + } + + // partial segment (and we know it isn't empty) + memory = memory.Slice(start: 0, length: available); + return available >= minBytes & minBytes > 0; // last check here applies the -ve logic + } + + // multi-page; hand out the first page (which is, by definition: full) + memory = startSegment!.Memory; + return memory.Length >= minBytes; + } + + /// + /// Note that this chain is invalidated by any other operations; no concurrency. + /// + public ReadOnlySequence GetAllCommitted() + { + if (ReferenceEquals(startSegment, endSegment)) + { + // single segment, fine + return startSegment is null + ? default + : new ReadOnlySequence(startSegment.Memory.Slice(start: 0, length: endSegmentCommitted)); + } + +#if PARSE_DETAIL + long length = GetCommittedLength(); +#endif + ReadOnlySequence ros = new(startSegment!, 0, endSegment!, endSegmentCommitted); +#if PARSE_DETAIL + Debug.Assert(ros.Length == length, $"length mismatch: calculated {length}, actual {ros.Length}"); +#endif + return ros; + } + + private Segment GetNextSegment() + { + DebugAssertValid(); + if (endSegment is not null) + { + endSegment.TrimEnd(endSegmentCommitted); + Debug.Assert(endSegment.Length == endSegmentCommitted, "trim failure"); + endSegmentLength = endSegmentCommitted; + DebugAssertValid(); + + // advertise the old page as available + _callback?.PageComplete(); + + var spare = endSegment.Next; + if (spare is not null) + { + // we already have a dangling segment; just update state + endSegment.DebugAssertValidChain(); + endSegment = spare; + endSegmentCommitted = 0; + endSegmentLength = spare.Length; + DebugAssertValid(); + return spare; + } + } + + Segment newSegment = Segment.Create(Pool.Rent(PageSize)); + if (endSegment is null) + { + // tabula rasa + endSegmentLength = newSegment.Length; + endSegment = startSegment = newSegment; + DebugAssertValid(); + return newSegment; + } + + endSegment.Append(newSegment); + endSegmentCommitted = 0; + endSegmentLength = newSegment.Length; + endSegment = newSegment; + DebugAssertValid(); + return newSegment; + } + + /// + /// Gets a scratch area for new data; this compares to . + /// + public Span GetUncommittedSpan(int hint = 0) + => GetUncommittedMemory(hint).Span; + + /// + /// Gets a scratch area for new data; this compares to . + /// + public Memory GetUncommittedMemory(int hint = 0) + { + DebugAssertValid(); + var segment = endSegment; + if (segment is not null) + { + leasedStart = segment.StartTrimCount + endSegmentCommitted; + var memory = segment.Memory; + if (endSegmentCommitted != 0) memory = memory.Slice(start: endSegmentCommitted); + if (hint <= 0) // allow anything non-empty + { + if (!memory.IsEmpty) return MemoryMarshal.AsMemory(memory); + } + else if (memory.Length >= Math.Min(hint, PageSize >> 2)) // respect the hint up to 1/4 of the page size + { + return MemoryMarshal.AsMemory(memory); + } + } + + // new segment, will always be entire + segment = GetNextSegment(); + leasedStart = segment.StartTrimCount + endSegmentCommitted; + Debug.Assert(leasedStart == 0, "should be zero for a new segment"); + return MemoryMarshal.AsMemory(segment.Memory); + } + + /// + /// This is the available unused buffer space, commonly used as the IO read-buffer to avoid + /// additional buffer-copy operations. + /// + public int UncommittedAvailable + { + get + { + DebugAssertValid(); + return endSegmentLength - endSegmentCommitted; + } + } + + private sealed class Segment : ReadOnlySequenceSegment + { + private Segment() { } + private IMemoryOwner _lease = NullLease.Instance; + private static Segment? _spare; + private Flags _flags; + + [Flags] + private enum Flags + { + None = 0, + StartTrim = 1 << 0, + EndTrim = 1 << 2, + } + + public static Segment Create(IMemoryOwner lease) + { + Debug.Assert(lease is not null, "null lease"); + var memory = lease!.Memory; + if (memory.IsEmpty) ThrowEmpty(); + + var obj = Interlocked.Exchange(ref _spare, null) ?? new(); + return obj.Init(lease, memory); + static void ThrowEmpty() => throw new InvalidOperationException("leased segment is empty"); + } + + private Segment Init(IMemoryOwner lease, Memory memory) + { + _lease = lease; + Memory = memory; + return this; + } + + public int Length => Memory.Length; + + public void Append(Segment next) + { + Debug.Assert(Next is null, "current segment already has a next"); + Debug.Assert(next.Next is null && next.RunningIndex == 0, "inbound next segment is already in a chain"); + next.RunningIndex = RunningIndex + Length; + Next = next; + DebugAssertValidChain(); + } + + private void ApplyChainDelta(int delta) + { + if (delta != 0) + { + var node = Next; + while (node is not null) + { + node.RunningIndex += delta; + node = node.Next; + } + } + } + + public void TrimEnd(int newLength) + { + var delta = Length - newLength; + if (delta != 0) + { + // buffer wasn't fully used; trim + _flags |= Flags.EndTrim; + Memory = Memory.Slice(0, newLength); + ApplyChainDelta(-delta); + DebugAssertValidChain(); + } + } + + public void TrimStart(int remove) + { + if (remove != 0) + { + _flags |= Flags.StartTrim; + Memory = Memory.Slice(start: remove); + RunningIndex += remove; // so that ROS length keeps working; note we *don't* need to adjust the chain + DebugAssertValidChain(); + StartTrimCount += remove; + } + } + + public new Segment? Next + { + get => (Segment?)base.Next; + private set => base.Next = value; + } + + public Segment? ResetAndGetNext() + { + var next = Next; + Next = null; + RunningIndex = 0; + _flags = Flags.None; + Memory = UntrimmedMemory; // reset, in case we trimmed it + DebugAssertValidChain(); + return next; + } + + public void Recycle() + { + var lease = _lease; + _lease = NullLease.Instance; + lease.Dispose(); + Next = null; + Memory = default; + RunningIndex = 0; + _flags = Flags.None; + Interlocked.Exchange(ref _spare, this); + DebugAssertValidChain(); + } + + private sealed class NullLease : IMemoryOwner + { + private NullLease() { } + public static readonly NullLease Instance = new NullLease(); + public void Dispose() { } + + public Memory Memory => default; + } + + public int StartTrimCount { get; private set; } + + /// + /// Get the full memory of the lease, before any trimming. + /// + public Memory UntrimmedMemory => _lease.Memory; + + /// + /// Undo any trimming, returning the new full capacity. + /// + public int Untrim(bool expandBackwards = false) + { + var fullMemory = UntrimmedMemory; + var fullLength = fullMemory.Length; + var delta = fullLength - Length; + if (delta != 0) + { + _flags &= ~(Flags.StartTrim | Flags.EndTrim); + Memory = fullMemory; + if (expandBackwards & RunningIndex >= delta) + { + // push our origin earlier; only valid if + // we're the first segment, otherwise + // we break someone-else's chain + RunningIndex -= delta; + } + else + { + // push everyone else later + ApplyChainDelta(delta); + } + + DebugAssertValidChain(); + } + + StartTrimCount = 0; + return fullLength; + } + + public bool StartTrimmed => (_flags & Flags.StartTrim) != 0; + public bool EndTrimmed => (_flags & Flags.EndTrim) != 0; + + [Conditional("DEBUG")] + public void DebugAssertValidChain([CallerMemberName] string blame = "") + { + var node = this; + var runningIndex = RunningIndex; + int index = 0; + while (node.Next is { } next) + { + index++; + var nextRunningIndex = runningIndex + node.Length; + if (nextRunningIndex != next.RunningIndex) ThrowRunningIndex(blame, index); + node = next; + runningIndex = nextRunningIndex; + static void ThrowRunningIndex(string blame, int index) => throw new InvalidOperationException( + $"Critical running index corruption in dangling chain, from '{blame}', segment {index}"); + } + } + + public void AppendOrRecycle(Segment segment, int maxDepth) + { + segment.Memory.DebugScramble(); + var node = this; + while (maxDepth-- > 0 && node is not null) + { + if (node.Next is null) // found somewhere to attach it + { + if (segment.Untrim() == 0) break; // turned out to be useless + segment.RunningIndex = node.RunningIndex + node.Length; + node.Next = segment; + return; + } + + node = node.Next; + } + + segment.Recycle(); + } + } + + /// + /// Discard all data and buffers. + /// + public void Release() + { + var node = startSegment; + startSegment = endSegment = null; + endSegmentCommitted = endSegmentLength = 0; + while (node is not null) + { + var next = node.Next; + node.Recycle(); + node = next; + } + } + + /// + /// Writes a value to the buffer; comparable to . + /// + public void Write(ReadOnlySpan value) + { + int srcLength = value.Length; + while (srcLength != 0) + { + var target = GetUncommittedSpan(hint: srcLength); + var tgtLength = target.Length; + if (tgtLength >= srcLength) + { + value.CopyTo(target); + Commit(srcLength); + return; + } + + value.Slice(0, tgtLength).CopyTo(target); + Commit(tgtLength); + value = value.Slice(tgtLength); + srcLength -= tgtLength; + } + } + + /// + /// Writes a value to the buffer; comparable to . + /// + public void Write(in ReadOnlySequence value) + { + if (value.IsSingleSegment) + { +#if NET + Write(value.FirstSpan); +#else + Write(value.First.Span); +#endif + } + else + { + WriteMultiSegment(ref this, in value); + } + + static void WriteMultiSegment(ref CycleBuffer @this, in ReadOnlySequence value) + { + foreach (var segment in value) + { +#if NET + @this.Write(value.FirstSpan); +#else + @this.Write(value.First.Span); +#endif + } + } + } +} diff --git a/src/RESPite/Buffers/ICycleBufferCallback.cs b/src/RESPite/Buffers/ICycleBufferCallback.cs new file mode 100644 index 000000000..9dcf1baa4 --- /dev/null +++ b/src/RESPite/Buffers/ICycleBufferCallback.cs @@ -0,0 +1,14 @@ +using System.Diagnostics.CodeAnalysis; + +namespace RESPite.Buffers; + +[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] +public interface ICycleBufferCallback +{ + /// + /// Notify that a page is available; this means that a consumer that wants + /// unflushed data can activate when pages are rotated, allowing large + /// payloads to be written concurrent with write. + /// + void PageComplete(); +} diff --git a/src/RESPite/Buffers/MemoryTrackedPool.cs b/src/RESPite/Buffers/MemoryTrackedPool.cs new file mode 100644 index 000000000..862910488 --- /dev/null +++ b/src/RESPite/Buffers/MemoryTrackedPool.cs @@ -0,0 +1,63 @@ +#if TRACK_MEMORY +using System.Buffers; +using System.Diagnostics.CodeAnalysis; + +namespace RESPite.Buffers; + +internal sealed class MemoryTrackedPool : MemoryPool +{ + // like MemoryPool, but tracks and reports double disposal via a custom memory manager, which + // allows all future use of a Memory to be tracked; contrast ArrayMemoryPool, which only tracks + // the initial fetch of .Memory from the lease + public override IMemoryOwner Rent(int minBufferSize = -1) => MemoryManager.Rent(minBufferSize); + + protected override void Dispose(bool disposing) + { + } + + // ReSharper disable once ArrangeModifiersOrder - you're wrong + public static new MemoryTrackedPool Shared { get; } = new(); + + public override int MaxBufferSize => MemoryPool.Shared.MaxBufferSize; + + private MemoryTrackedPool() + { + } + + private sealed class MemoryManager : MemoryManager + { + public static IMemoryOwner Rent(int minBufferSize = -1) => new MemoryManager(minBufferSize); + + private T[]? array; + private MemoryManager(int minBufferSize) + { + array = ArrayPool.Shared.Rent(Math.Max(64, minBufferSize)); + } + + private T[] CheckDisposed() + { + return array ?? Throw(); + [DoesNotReturn] + static T[] Throw() => throw new ObjectDisposedException("Use-after-free of Memory-" + typeof(T).Name); + } + + public override MemoryHandle Pin(int elementIndex = 0) => throw new NotSupportedException(nameof(Pin)); + + public override void Unpin() => throw new NotSupportedException(nameof(Unpin)); + + public override Span GetSpan() => CheckDisposed(); + + protected override bool TryGetArray(out ArraySegment segment) + { + segment = new ArraySegment(CheckDisposed()); + return true; + } + + protected override void Dispose(bool disposing) + { + var arr = Interlocked.Exchange(ref array, null); + if (arr is not null) ArrayPool.Shared.Return(arr); + } + } +} +#endif diff --git a/src/RESPite/Internal/BlockBuffer.cs b/src/RESPite/Internal/BlockBuffer.cs new file mode 100644 index 000000000..752d74c8d --- /dev/null +++ b/src/RESPite/Internal/BlockBuffer.cs @@ -0,0 +1,341 @@ +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace RESPite.Internal; + +internal abstract partial class BlockBufferSerializer +{ + internal sealed class BlockBuffer : MemoryManager + { + private BlockBuffer(BlockBufferSerializer parent, int minCapacity) + { + _arrayPool = parent._arrayPool; + _array = _arrayPool.Rent(minCapacity); + DebugCounters.OnBufferCapacity(_array.Length); +#if DEBUG + _parent = parent; + parent.DebugBufferCreated(); +#endif + } + + private int _refCount = 1; + private int _finalizedOffset, _writeOffset; + private readonly ArrayPool _arrayPool; + private byte[] _array; +#if DEBUG + private int _finalizedCount; + private BlockBufferSerializer _parent; +#endif + + public override string ToString() => +#if DEBUG + $"{_finalizedCount} messages; " + +#endif + $"{_finalizedOffset} finalized bytes; writing: {NonFinalizedData.Length} bytes, {Available} available; observers: {_refCount}"; + + // only used when filling; _buffer should be non-null + private int Available => _array.Length - _writeOffset; + public Memory UncommittedMemory => _array.AsMemory(_writeOffset); + public Span UncommittedSpan => _array.AsSpan(_writeOffset); + + // decrease ref-count; dispose if necessary + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Release() + { + if (Interlocked.Decrement(ref _refCount) <= 0) Recycle(); + } + + public void AddRef() + { + if (!TryAddRef()) Throw(); + static void Throw() => throw new ObjectDisposedException(nameof(BlockBuffer)); + } + + public bool TryAddRef() + { + int count; + do + { + count = Volatile.Read(ref _refCount); + if (count <= 0) return false; + } + // repeat until we can successfully swap/incr + while (Interlocked.CompareExchange(ref _refCount, count + 1, count) != count); + + return true; + } + + [MethodImpl(MethodImplOptions.NoInlining)] // called rarely vs Dispose + private void Recycle() + { + var count = Volatile.Read(ref _refCount); + if (count == 0) + { + _array.DebugScramble(); +#if DEBUG + GC.SuppressFinalize(this); // only have a finalizer in debug + _parent.DebugBufferRecycled(_array.Length); +#endif + _arrayPool.Return(_array); + _array = []; + } + + Debug.Assert(count == 0, $"over-disposal? count={count}"); + } + +#if DEBUG +#pragma warning disable CA2015 // Adding a finalizer to a type derived from MemoryManager may permit memory to be freed while it is still in use by a Span + // (the above is fine because we don't actually release anything - just a counter) + ~BlockBuffer() + { + _parent.DebugBufferLeaked(); + DebugCounters.OnBufferLeaked(); + } +#pragma warning restore CA2015 +#endif + + public static BlockBuffer GetBuffer(BlockBufferSerializer parent, int sizeHint) + { + // note this isn't an actual "max", just a max of what we guarantee; we give the caller + // whatever is left in the buffer; the clamped hint just decides whether we need a *new* buffer + const int MinSize = 16, MaxSize = 128; + sizeHint = Math.Min(Math.Max(sizeHint, MinSize), MaxSize); + + var buffer = parent.Buffer; // most common path is "exists, with enough data" + return buffer is not null && buffer.AvailableWithResetIfUseful() >= sizeHint + ? buffer + : GetBufferSlow(parent, sizeHint); + } + + // would it be useful and possible to reset? i.e. if all finalized chunks have been returned, + private int AvailableWithResetIfUseful() + { + if (_finalizedOffset != 0 // at least some chunks have been finalized + && Volatile.Read(ref _refCount) == 1 // all finalized chunks returned + & _writeOffset == _finalizedOffset) // we're not in the middle of serializing something new + { + _writeOffset = _finalizedOffset = 0; // swipe left + } + + return _array.Length - _writeOffset; + } + + private static BlockBuffer GetBufferSlow(BlockBufferSerializer parent, int minBytes) + { + // note clamp on size hint has already been applied + const int DefaultBufferSize = 2048; + var buffer = parent.Buffer; + if (buffer is null) + { + // first buffer + return parent.Buffer = new BlockBuffer(parent, DefaultBufferSize); + } + + Debug.Assert(minBytes > buffer.Available, "existing buffer has capacity - why are we here?"); + + if (buffer.TryResizeFor(minBytes)) + { + Debug.Assert(buffer.Available >= minBytes); + return buffer; + } + + // We've tried reset and resize - no more tricks; we need to move to a new buffer, starting with a + // capacity for any existing data in this message, plus the new chunk we're adding. + var nonFinalizedBytes = buffer.NonFinalizedData; + var newBuffer = new BlockBuffer(parent, Math.Max(nonFinalizedBytes.Length + minBytes, DefaultBufferSize)); + + // copy the existing message data, if any (the previous message might have finished near the + // boundary, in which case we might not have written anything yet) + newBuffer.CopyFrom(nonFinalizedBytes); + Debug.Assert(newBuffer.Available >= minBytes, "should have requested extra capacity"); + + // the ~emperor~ buffer is dead; long live the ~emperor~ buffer + parent.Buffer = newBuffer; + buffer.MarkComplete(parent); + return newBuffer; + } + + // used for elective reset (rather than "because we ran out of space") + public static void Clear(BlockBufferSerializer parent) + { + if (parent.Buffer is { } buffer) + { + parent.Buffer = null; + buffer.MarkComplete(parent); + } + } + + public static ReadOnlyMemory RetainCurrent(BlockBufferSerializer parent) + { + if (parent.Buffer is { } buffer && buffer._finalizedOffset != 0) + { + parent.Buffer = null; + buffer.AddRef(); + return buffer.CreateMemory(0, buffer._finalizedOffset); + } + // nothing useful to detach! + return default; + } + + private void MarkComplete(BlockBufferSerializer parent) + { + // record that the old buffer no longer logically has any non-committed bytes (mostly just for ToString()) + _writeOffset = _finalizedOffset; + Debug.Assert(IsNonCommittedEmpty); + + // see if the caller wants to take ownership of the segment + if (_finalizedOffset != 0 && !parent.ClaimSegment(CreateMemory(0, _finalizedOffset))) + { + Release(); // decrement the observer + } +#if DEBUG + DebugCounters.OnBufferCompleted(_finalizedCount, _finalizedOffset); +#endif + } + + private void CopyFrom(Span source) + { + source.CopyTo(UncommittedSpan); + _writeOffset += source.Length; + } + + private Span NonFinalizedData => _array.AsSpan( + _finalizedOffset, _writeOffset - _finalizedOffset); + + private bool TryResizeFor(int extraBytes) + { + if (_finalizedOffset == 0 & // we can only do this if there are no other messages in the buffer + Volatile.Read(ref _refCount) == 1) // and no-one else is looking (we already tried reset) + { + // we're already on the boundary - don't scrimp; just do the math from the end of the buffer + byte[] newArray = _arrayPool.Rent(_array.Length + extraBytes); + DebugCounters.OnBufferCapacity(newArray.Length - _array.Length); // account for extra only + + // copy the existing data (we always expect some, since we've clamped extraBytes to be + // much smaller than the default buffer size) + NonFinalizedData.CopyTo(newArray); + _array.DebugScramble(); + _arrayPool.Return(_array); + _array = newArray; + return true; + } + + return false; + } + + public static void Advance(BlockBufferSerializer parent, int count) + { + if (count == 0) return; + if (count < 0) ThrowOutOfRange(); + var buffer = parent.Buffer; + if (buffer is null || buffer.Available < count) ThrowOutOfRange(); + buffer._writeOffset += count; + + [DoesNotReturn] + static void ThrowOutOfRange() => throw new ArgumentOutOfRangeException(nameof(count)); + } + + public void RevertUnfinalized(BlockBufferSerializer parent) + { + // undo any writes (something went wrong during serialize) + _finalizedOffset = _writeOffset; + } + + private ReadOnlyMemory FinalizeBlock() + { + var length = _writeOffset - _finalizedOffset; + Debug.Assert(length > 0, "already checked this in FinalizeMessage!"); + var chunk = CreateMemory(_finalizedOffset, length); + _finalizedOffset = _writeOffset; // move the write head +#if DEBUG + _finalizedCount++; + _parent.DebugMessageFinalized(length); +#endif + Interlocked.Increment(ref _refCount); // add an observer + return chunk; + } + + private bool IsNonCommittedEmpty => _finalizedOffset == _writeOffset; + + public static ReadOnlyMemory FinalizeMessage(BlockBufferSerializer parent) + { + var buffer = parent.Buffer; + if (buffer is null || buffer.IsNonCommittedEmpty) + { +#if DEBUG // still count it for logging purposes + if (buffer is not null) buffer._finalizedCount++; + parent.DebugMessageFinalized(0); +#endif + return default; + } + + return buffer.FinalizeBlock(); + } + + // MemoryManager pieces + protected override void Dispose(bool disposing) + { + if (disposing) Release(); + } + + public override Span GetSpan() => _array; + public int Length => _array.Length; + + // base version is CreateMemory(GetSpan().Length); avoid that GetSpan() + public override Memory Memory => CreateMemory(_array.Length); + + public override unsafe MemoryHandle Pin(int elementIndex = 0) + { + // We *could* be cute and use a shared pin - but that's a *lot* + // of work (synchronization), requires extra storage, and for an + // API that is very unlikely; hence: we'll use per-call GC pins. + GCHandle handle = GCHandle.Alloc(_array, GCHandleType.Pinned); + DebugCounters.OnBufferPinned(); // prove how unlikely this is + byte* ptr = (byte*)handle.AddrOfPinnedObject(); + // note no IPinnable in the MemoryHandle; + return new MemoryHandle(ptr + elementIndex, handle); + } + + // This would only be called if we passed out a MemoryHandle with ourselves + // as IPinnable (in Pin), which: we don't. + public override void Unpin() => throw new NotSupportedException(); + + protected override bool TryGetArray(out ArraySegment segment) + { + segment = new ArraySegment(_array); + return true; + } + + internal static void Release(in ReadOnlySequence request) + { + if (request.IsSingleSegment) + { + if (MemoryMarshal.TryGetMemoryManager( + request.First, out var block)) + { + block.Release(); + } + } + else + { + ReleaseMultiBlock(in request); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ReleaseMultiBlock(in ReadOnlySequence request) + { + foreach (var segment in request) + { + if (MemoryMarshal.TryGetMemoryManager( + segment, out var block)) + { + block.Release(); + } + } + } + } + } +} diff --git a/src/RESPite/Internal/BlockBufferSerializer.cs b/src/RESPite/Internal/BlockBufferSerializer.cs new file mode 100644 index 000000000..5f90f66cb --- /dev/null +++ b/src/RESPite/Internal/BlockBufferSerializer.cs @@ -0,0 +1,96 @@ +using System.Buffers; +using System.Diagnostics; + +namespace RESPite.Internal; + +/// +/// Provides abstracted access to a buffer-writing API. Conveniently, we only give the caller +/// RespWriter - which they cannot export (ref-type), thus we never actually give the +/// public caller our IBufferWriter{byte}. Likewise, note that serialization is synchronous, +/// i.e. never switches thread during an operation. This gives us quite a bit of flexibility. +/// There are two main uses of BlockBufferSerializer: +/// 1. thread-local: ambient, used for random messages so that each thread is quietly packing +/// a thread-specific buffer; zero concurrency because of [ThreadStatic] hackery. +/// 2. batching: RespBatch hosts a serializer that reflects the batch we're building; successive +/// commands in the same batch are written adjacently in a shared buffer - we explicitly +/// detect and reject concurrency attempts in a batch (which is fair: a batch has order). +/// +internal abstract partial class BlockBufferSerializer(ArrayPool? arrayPool = null) : IBufferWriter +{ + private readonly ArrayPool _arrayPool = arrayPool ?? ArrayPool.Shared; + private protected abstract BlockBuffer? Buffer { get; set; } + + Memory IBufferWriter.GetMemory(int sizeHint) => BlockBuffer.GetBuffer(this, sizeHint).UncommittedMemory; + + Span IBufferWriter.GetSpan(int sizeHint) => BlockBuffer.GetBuffer(this, sizeHint).UncommittedSpan; + + void IBufferWriter.Advance(int count) => BlockBuffer.Advance(this, count); + + public virtual void Clear() => BlockBuffer.Clear(this); + + internal virtual ReadOnlySequence Flush() => throw new NotSupportedException(); + + /* + public virtual ReadOnlyMemory Serialize( + RespCommandMap? commandMap, + ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter) +#if NET10_0_OR_GREATER + where TRequest : allows ref struct +#endif + { + try + { + var writer = new RespWriter(this); + writer.CommandMap = commandMap; + formatter.Format(command, ref writer, request); + writer.Flush(); + return BlockBuffer.FinalizeMessage(this); + } + catch + { + Buffer?.RevertUnfinalized(this); + throw; + } + } + */ + + internal void Revert() => Buffer?.RevertUnfinalized(this); + + protected virtual bool ClaimSegment(ReadOnlyMemory segment) => false; + +#if DEBUG + private int _countAdded, _countRecycled, _countLeaked, _countMessages; + private long _countMessageBytes; + public int CountLeaked => Volatile.Read(ref _countLeaked); + public int CountRecycled => Volatile.Read(ref _countRecycled); + public int CountAdded => Volatile.Read(ref _countAdded); + public int CountMessages => Volatile.Read(ref _countMessages); + public long CountMessageBytes => Volatile.Read(ref _countMessageBytes); + + [Conditional("DEBUG")] + private void DebugBufferLeaked() => Interlocked.Increment(ref _countLeaked); + + [Conditional("DEBUG")] + private void DebugBufferRecycled(int length) + { + Interlocked.Increment(ref _countRecycled); + DebugCounters.OnBufferRecycled(length); + } + + [Conditional("DEBUG")] + private void DebugBufferCreated() + { + Interlocked.Increment(ref _countAdded); + DebugCounters.OnBufferCreated(); + } + + [Conditional("DEBUG")] + private void DebugMessageFinalized(int bytes) + { + Interlocked.Increment(ref _countMessages); + Interlocked.Add(ref _countMessageBytes, bytes); + } +#endif +} diff --git a/src/RESPite/Internal/DebugCounters.cs b/src/RESPite/Internal/DebugCounters.cs new file mode 100644 index 000000000..2ed742a84 --- /dev/null +++ b/src/RESPite/Internal/DebugCounters.cs @@ -0,0 +1,163 @@ +using System.Diagnostics; + +namespace RESPite.Internal; + +internal partial class DebugCounters +{ +#if DEBUG + private static int + _tallySyncReadCount, + _tallyAsyncReadCount, + _tallyAsyncReadInlineCount, + _tallyDiscardFullCount, + _tallyDiscardPartialCount, + _tallyBufferCreatedCount, + _tallyBufferRecycledCount, + _tallyBufferMessageCount, + _tallyBufferPinCount, + _tallyBufferLeakCount; + + private static long + _tallyReadBytes, + _tallyDiscardAverage, + _tallyBufferMessageBytes, + _tallyBufferRecycledBytes, + _tallyBufferMaxOutstandingBytes, + _tallyBufferTotalBytes; +#endif + + [Conditional("DEBUG")] + public static void OnDiscardFull(long count) + { +#if DEBUG + if (count > 0) + { + Interlocked.Increment(ref _tallyDiscardFullCount); + EstimatedMovingRangeAverage(ref _tallyDiscardAverage, count); + } +#endif + } + + [Conditional("DEBUG")] + public static void OnDiscardPartial(long count) + { +#if DEBUG + if (count > 0) + { + Interlocked.Increment(ref _tallyDiscardPartialCount); + EstimatedMovingRangeAverage(ref _tallyDiscardAverage, count); + } +#endif + } + + [Conditional("DEBUG")] + internal static void OnAsyncRead(int bytes, bool inline) + { +#if DEBUG + Interlocked.Increment(ref inline ? ref _tallyAsyncReadInlineCount : ref _tallyAsyncReadCount); + if (bytes > 0) Interlocked.Add(ref _tallyReadBytes, bytes); +#endif + } + + [Conditional("DEBUG")] + internal static void OnSyncRead(int bytes) + { +#if DEBUG + Interlocked.Increment(ref _tallySyncReadCount); + if (bytes > 0) Interlocked.Add(ref _tallyReadBytes, bytes); +#endif + } + + [Conditional("DEBUG")] + public static void OnBufferCreated() + { +#if DEBUG + Interlocked.Increment(ref _tallyBufferCreatedCount); +#endif + } + + [Conditional("DEBUG")] + public static void OnBufferRecycled(int messageBytes) + { +#if DEBUG + Interlocked.Increment(ref _tallyBufferRecycledCount); + var now = Interlocked.Add(ref _tallyBufferRecycledBytes, messageBytes); + var outstanding = Volatile.Read(ref _tallyBufferMessageBytes) - now; + + while (true) + { + var oldOutstanding = Volatile.Read(ref _tallyBufferMaxOutstandingBytes); + // loop until either it isn't an increase, or we successfully perform + // the swap + if (outstanding <= oldOutstanding + || Interlocked.CompareExchange( + ref _tallyBufferMaxOutstandingBytes, + outstanding, + oldOutstanding) == oldOutstanding) break; + } +#endif + } + + [Conditional("DEBUG")] + public static void OnBufferCompleted(int messageCount, int messageBytes) + { +#if DEBUG + Interlocked.Add(ref _tallyBufferMessageCount, messageCount); + Interlocked.Add(ref _tallyBufferMessageBytes, messageBytes); +#endif + } + + [Conditional("DEBUG")] + public static void OnBufferCapacity(int bytes) + { +#if DEBUG + Interlocked.Add(ref _tallyBufferTotalBytes, bytes); +#endif + } + + [Conditional("DEBUG")] + public static void OnBufferPinned() + { +#if DEBUG + Interlocked.Increment(ref _tallyBufferPinCount); +#endif + } + + [Conditional("DEBUG")] + public static void OnBufferLeaked() + { +#if DEBUG + Interlocked.Increment(ref _tallyBufferLeakCount); +#endif + } + +#if DEBUG + private static void EstimatedMovingRangeAverage(ref long field, long value) + { + var oldValue = Volatile.Read(ref field); + var delta = (value - oldValue) >> 3; // is is a 7:1 old:new EMRA, using integer/bit math (alplha=0.125) + if (delta != 0) Interlocked.Add(ref field, delta); + // note: strictly conflicting concurrent calls can skew the value incorrectly; this is, however, + // preferable to getting into a CEX squabble or requiring a lock - it is debug-only and just useful data + } + + public int SyncReadCount { get; } = Interlocked.Exchange(ref _tallySyncReadCount, 0); + public int AsyncReadCount { get; } = Interlocked.Exchange(ref _tallyAsyncReadCount, 0); + public int AsyncReadInlineCount { get; } = Interlocked.Exchange(ref _tallyAsyncReadInlineCount, 0); + public long ReadBytes { get; } = Interlocked.Exchange(ref _tallyReadBytes, 0); + + public long DiscardAverage { get; } = Interlocked.Exchange(ref _tallyDiscardAverage, 32); + public int DiscardFullCount { get; } = Interlocked.Exchange(ref _tallyDiscardFullCount, 0); + public int DiscardPartialCount { get; } = Interlocked.Exchange(ref _tallyDiscardPartialCount, 0); + + public int BufferCreatedCount { get; } = Interlocked.Exchange(ref _tallyBufferCreatedCount, 0); + public int BufferRecycledCount { get; } = Interlocked.Exchange(ref _tallyBufferRecycledCount, 0); + public long BufferRecycledBytes { get; } = Interlocked.Exchange(ref _tallyBufferRecycledBytes, 0); + public long BufferMaxOutstandingBytes { get; } = Interlocked.Exchange(ref _tallyBufferMaxOutstandingBytes, 0); + public int BufferMessageCount { get; } = Interlocked.Exchange(ref _tallyBufferMessageCount, 0); + public long BufferMessageBytes { get; } = Interlocked.Exchange(ref _tallyBufferMessageBytes, 0); + public long BufferTotalBytes { get; } = Interlocked.Exchange(ref _tallyBufferTotalBytes, 0); + public int BufferPinCount { get; } = Interlocked.Exchange(ref _tallyBufferPinCount, 0); + public int BufferLeakCount { get; } = Interlocked.Exchange(ref _tallyBufferLeakCount, 0); +#endif +} diff --git a/src/RESPite/Internal/Raw.cs b/src/RESPite/Internal/Raw.cs new file mode 100644 index 000000000..9df318630 --- /dev/null +++ b/src/RESPite/Internal/Raw.cs @@ -0,0 +1,138 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +#if NET +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + +namespace RESPite.Internal; + +/// +/// Pre-computed payload fragments, for high-volume scenarios / common values. +/// +/// +/// CPU-endianness applies here; we can't just use "const" - however, modern JITs treat "static readonly" *almost* the same as "const", so: meh. +/// +internal static class Raw +{ + public static ulong Create64(ReadOnlySpan bytes, int length) + { + if (length != bytes.Length) + { + throw new ArgumentException($"Length check failed: {length} vs {bytes.Length}, value: {RespConstants.UTF8.GetString(bytes)}", nameof(length)); + } + if (length < 0 || length > sizeof(ulong)) + { + throw new ArgumentOutOfRangeException(nameof(length), $"Invalid length {length} - must be 0-{sizeof(ulong)}"); + } + + // this *will* be aligned; this approach intentionally chosen for parity with write + Span scratch = stackalloc byte[sizeof(ulong)]; + if (length != sizeof(ulong)) scratch.Slice(length).Clear(); + bytes.CopyTo(scratch); + return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(scratch)); + } + + public static uint Create32(ReadOnlySpan bytes, int length) + { + if (length != bytes.Length) + { + throw new ArgumentException($"Length check failed: {length} vs {bytes.Length}, value: {RespConstants.UTF8.GetString(bytes)}", nameof(length)); + } + if (length < 0 || length > sizeof(uint)) + { + throw new ArgumentOutOfRangeException(nameof(length), $"Invalid length {length} - must be 0-{sizeof(uint)}"); + } + + // this *will* be aligned; this approach intentionally chosen for parity with write + Span scratch = stackalloc byte[sizeof(uint)]; + if (length != sizeof(uint)) scratch.Slice(length).Clear(); + bytes.CopyTo(scratch); + return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(scratch)); + } + + public static ulong BulkStringEmpty_6 = Create64("$0\r\n\r\n"u8, 6); + + public static ulong BulkStringInt32_M1_8 = Create64("$2\r\n-1\r\n"u8, 8); + public static ulong BulkStringInt32_0_7 = Create64("$1\r\n0\r\n"u8, 7); + public static ulong BulkStringInt32_1_7 = Create64("$1\r\n1\r\n"u8, 7); + public static ulong BulkStringInt32_2_7 = Create64("$1\r\n2\r\n"u8, 7); + public static ulong BulkStringInt32_3_7 = Create64("$1\r\n3\r\n"u8, 7); + public static ulong BulkStringInt32_4_7 = Create64("$1\r\n4\r\n"u8, 7); + public static ulong BulkStringInt32_5_7 = Create64("$1\r\n5\r\n"u8, 7); + public static ulong BulkStringInt32_6_7 = Create64("$1\r\n6\r\n"u8, 7); + public static ulong BulkStringInt32_7_7 = Create64("$1\r\n7\r\n"u8, 7); + public static ulong BulkStringInt32_8_7 = Create64("$1\r\n8\r\n"u8, 7); + public static ulong BulkStringInt32_9_7 = Create64("$1\r\n9\r\n"u8, 7); + public static ulong BulkStringInt32_10_8 = Create64("$2\r\n10\r\n"u8, 8); + + public static ulong BulkStringPrefix_M1_5 = Create64("$-1\r\n"u8, 5); + public static uint BulkStringPrefix_0_4 = Create32("$0\r\n"u8, 4); + public static uint BulkStringPrefix_1_4 = Create32("$1\r\n"u8, 4); + public static uint BulkStringPrefix_2_4 = Create32("$2\r\n"u8, 4); + public static uint BulkStringPrefix_3_4 = Create32("$3\r\n"u8, 4); + public static uint BulkStringPrefix_4_4 = Create32("$4\r\n"u8, 4); + public static uint BulkStringPrefix_5_4 = Create32("$5\r\n"u8, 4); + public static uint BulkStringPrefix_6_4 = Create32("$6\r\n"u8, 4); + public static uint BulkStringPrefix_7_4 = Create32("$7\r\n"u8, 4); + public static uint BulkStringPrefix_8_4 = Create32("$8\r\n"u8, 4); + public static uint BulkStringPrefix_9_4 = Create32("$9\r\n"u8, 4); + public static ulong BulkStringPrefix_10_5 = Create64("$10\r\n"u8, 5); + + public static ulong ArrayPrefix_M1_5 = Create64("*-1\r\n"u8, 5); + public static uint ArrayPrefix_0_4 = Create32("*0\r\n"u8, 4); + public static uint ArrayPrefix_1_4 = Create32("*1\r\n"u8, 4); + public static uint ArrayPrefix_2_4 = Create32("*2\r\n"u8, 4); + public static uint ArrayPrefix_3_4 = Create32("*3\r\n"u8, 4); + public static uint ArrayPrefix_4_4 = Create32("*4\r\n"u8, 4); + public static uint ArrayPrefix_5_4 = Create32("*5\r\n"u8, 4); + public static uint ArrayPrefix_6_4 = Create32("*6\r\n"u8, 4); + public static uint ArrayPrefix_7_4 = Create32("*7\r\n"u8, 4); + public static uint ArrayPrefix_8_4 = Create32("*8\r\n"u8, 4); + public static uint ArrayPrefix_9_4 = Create32("*9\r\n"u8, 4); + public static ulong ArrayPrefix_10_5 = Create64("*10\r\n"u8, 5); + +#if NET + private static uint FirstAndLast(char first, char last) + { + Debug.Assert(first < 128 && last < 128, "ASCII please"); + Span scratch = [(byte)first, 0, 0, (byte)last]; + // this *will* be aligned; this approach intentionally chosen for how we read + return Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(scratch)); + } + + public const int CommonRespIndex_Success = 0; + public const int CommonRespIndex_SingleDigitInteger = 1; + public const int CommonRespIndex_DoubleDigitInteger = 2; + public const int CommonRespIndex_SingleDigitString = 3; + public const int CommonRespIndex_DoubleDigitString = 4; + public const int CommonRespIndex_SingleDigitArray = 5; + public const int CommonRespIndex_DoubleDigitArray = 6; + public const int CommonRespIndex_Error = 7; + + public static readonly Vector256 CommonRespPrefixes = Vector256.Create( + FirstAndLast('+', '\r'), // success +OK\r\n + FirstAndLast(':', '\n'), // single-digit integer :4\r\n + FirstAndLast(':', '\r'), // double-digit integer :42\r\n + FirstAndLast('$', '\n'), // 0-9 char string $0\r\n\r\n + FirstAndLast('$', '\r'), // null/10-99 char string $-1\r\n or $10\r\nABCDEFGHIJ\r\n + FirstAndLast('*', '\n'), // 0-9 length array *0\r\n + FirstAndLast('*', '\r'), // null/10-99 length array *-1\r\n or *10\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n:0\r\n + FirstAndLast('-', 'R')); // common errors -ERR something bad happened + + public static readonly Vector256 FirstLastMask = CreateUInt32(0xFF0000FF); + + private static Vector256 CreateUInt32(uint value) + { +#if NET8_0_OR_GREATER + return Vector256.Create(value); +#else + return Vector256.Create(value, value, value, value, value, value, value, value); +#endif + } + +#endif +} diff --git a/src/RESPite/Internal/RespConstants.cs b/src/RESPite/Internal/RespConstants.cs new file mode 100644 index 000000000..accb8400b --- /dev/null +++ b/src/RESPite/Internal/RespConstants.cs @@ -0,0 +1,53 @@ +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +// ReSharper disable InconsistentNaming +namespace RESPite.Internal; + +internal static class RespConstants +{ + public static readonly UTF8Encoding UTF8 = new(false); + + public static ReadOnlySpan CrlfBytes => "\r\n"u8; + + public static readonly ushort CrLfUInt16 = UnsafeCpuUInt16(CrlfBytes); + + public static ReadOnlySpan OKBytes_LC => "ok"u8; + public static ReadOnlySpan OKBytes => "OK"u8; + public static readonly ushort OKUInt16 = UnsafeCpuUInt16(OKBytes); + public static readonly ushort OKUInt16_LC = UnsafeCpuUInt16(OKBytes_LC); + + public static readonly uint BulkStringStreaming = UnsafeCpuUInt32("$?\r\n"u8); + public static readonly uint BulkStringNull = UnsafeCpuUInt32("$-1\r"u8); + + public static readonly uint ArrayStreaming = UnsafeCpuUInt32("*?\r\n"u8); + public static readonly uint ArrayNull = UnsafeCpuUInt32("*-1\r"u8); + + public static ushort UnsafeCpuUInt16(ReadOnlySpan bytes) + => Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(bytes)); + public static ushort UnsafeCpuUInt16(ReadOnlySpan bytes, int offset) + => Unsafe.ReadUnaligned(ref Unsafe.Add(ref MemoryMarshal.GetReference(bytes), offset)); + public static byte UnsafeCpuByte(ReadOnlySpan bytes, int offset) + => Unsafe.Add(ref MemoryMarshal.GetReference(bytes), offset); + public static uint UnsafeCpuUInt32(ReadOnlySpan bytes) + => Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(bytes)); + public static uint UnsafeCpuUInt32(ReadOnlySpan bytes, int offset) + => Unsafe.ReadUnaligned(ref Unsafe.Add(ref MemoryMarshal.GetReference(bytes), offset)); + public static ulong UnsafeCpuUInt64(ReadOnlySpan bytes) + => Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(bytes)); + public static ushort CpuUInt16(ushort bigEndian) + => BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(bigEndian) : bigEndian; + public static uint CpuUInt32(uint bigEndian) + => BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(bigEndian) : bigEndian; + public static ulong CpuUInt64(ulong bigEndian) + => BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(bigEndian) : bigEndian; + + public const int MaxRawBytesInt32 = 11, // "-2147483648" + MaxRawBytesInt64 = 20, // "-9223372036854775808", + MaxProtocolBytesIntegerInt32 = MaxRawBytesInt32 + 3, // ?X10X\r\n where ? could be $, *, etc - usually a length prefix + MaxProtocolBytesBulkStringIntegerInt32 = MaxRawBytesInt32 + 7, // $NN\r\nX11X\r\n for NN (length) 1-11 + MaxProtocolBytesBulkStringIntegerInt64 = MaxRawBytesInt64 + 7, // $NN\r\nX20X\r\n for NN (length) 1-20 + MaxRawBytesNumber = 20, // note G17 format, allow 20 for payload + MaxProtocolBytesBytesNumber = MaxRawBytesNumber + 7; // $NN\r\nX...X\r\n for NN (length) 1-20 +} diff --git a/src/RESPite/Internal/RespOperationExtensions.cs b/src/RESPite/Internal/RespOperationExtensions.cs new file mode 100644 index 000000000..78ecd6d53 --- /dev/null +++ b/src/RESPite/Internal/RespOperationExtensions.cs @@ -0,0 +1,57 @@ +using System.Buffers; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace RESPite.Internal; + +internal static class RespOperationExtensions +{ +#if PREVIEW_LANGVER + extension(in RespOperation operation) + { + // since this is valid... + public ref readonly RespOperation Self => ref operation; + + // so is this (the types are layout-identical) + public ref readonly RespOperation Untyped => ref Unsafe.As, RespOperation>( + ref Unsafe.AsRef(in operation)); + } +#endif + + // if we're recycling a buffer, we need to consider it trashable by other threads; for + // debug purposes, force this by overwriting with *****, aka the meaning of life + [Conditional("DEBUG")] + internal static void DebugScramble(this Span value) + => value.Fill(42); + + [Conditional("DEBUG")] + internal static void DebugScramble(this Memory value) + => value.Span.Fill(42); + + [Conditional("DEBUG")] + internal static void DebugScramble(this ReadOnlyMemory value) + => MemoryMarshal.AsMemory(value).Span.Fill(42); + + [Conditional("DEBUG")] + internal static void DebugScramble(this ReadOnlySequence value) + { + if (value.IsSingleSegment) + { + value.First.DebugScramble(); + } + else + { + foreach (var segment in value) + { + segment.DebugScramble(); + } + } + } + + [Conditional("DEBUG")] + internal static void DebugScramble(this byte[]? value) + { + if (value is not null) + value.AsSpan().Fill(42); + } +} diff --git a/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs b/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs new file mode 100644 index 000000000..4f00e5194 --- /dev/null +++ b/src/RESPite/Internal/SynchronizedBlockBufferSerializer.cs @@ -0,0 +1,122 @@ +using System.Buffers; + +namespace RESPite.Internal; + +internal partial class BlockBufferSerializer +{ + internal static BlockBufferSerializer Create(bool retainChain = false) => + new SynchronizedBlockBufferSerializer(retainChain); + + /// + /// Used for things like . + /// + private sealed class SynchronizedBlockBufferSerializer(bool retainChain) : BlockBufferSerializer + { + private bool _discardDuringClear; + + private protected override BlockBuffer? Buffer { get; set; } // simple per-instance auto-prop + + /* + // use lock-based synchronization + public override ReadOnlyMemory Serialize( + RespCommandMap? commandMap, + ReadOnlySpan command, + in TRequest request, + IRespFormatter formatter) + { + bool haveLock = false; + try // note that "lock" unrolls to something very similar; we're not adding anything unusual here + { + // in reality, we *expect* people to not attempt to use batches concurrently, *and* + // we expect serialization to be very fast, but: out of an abundance of caution, + // add a timeout - just to avoid surprises (since people can write their own formatters) + Monitor.TryEnter(this, LockTimeout, ref haveLock); + if (!haveLock) ThrowTimeout(); + return base.Serialize(commandMap, command, in request, formatter); + } + finally + { + if (haveLock) Monitor.Exit(this); + } + + static void ThrowTimeout() => throw new TimeoutException( + "It took a long time to get access to the serialization-buffer. This is very odd - please " + + "ask on GitHub, but *as a guess*, you have a custom RESP formatter that is really slow *and* " + + "you are using concurrent access to a RESP batch / transaction."); + } + */ + + private static readonly TimeSpan LockTimeout = TimeSpan.FromSeconds(5); + + private Segment? _head, _tail; + + protected override bool ClaimSegment(ReadOnlyMemory segment) + { + if (retainChain & !_discardDuringClear) + { + if (_head is null) + { + _head = _tail = new Segment(segment); + } + else + { + _tail = new Segment(segment, _tail); + } + + // note we don't need to increment the ref-count; because of this "true" + return true; + } + + return false; + } + + internal override ReadOnlySequence Flush() + { + if (_head is null) + { + // at worst, single-segment - we can skip the alloc + return new(BlockBuffer.RetainCurrent(this)); + } + + // otherwise, flush everything *keeping the chain* + ClearWithDiscard(discard: false); + ReadOnlySequence seq = new(_head, 0, _tail!, _tail!.Length); + _head = _tail = null; + return seq; + } + + public override void Clear() + { + ClearWithDiscard(discard: true); + _head = _tail = null; + } + + private void ClearWithDiscard(bool discard) + { + try + { + _discardDuringClear = discard; + base.Clear(); + } + finally + { + _discardDuringClear = false; + } + } + + private sealed class Segment : ReadOnlySequenceSegment + { + public Segment(ReadOnlyMemory memory, Segment? previous = null) + { + Memory = memory; + if (previous is not null) + { + previous.Next = this; + RunningIndex = previous.RunningIndex + previous.Length; + } + } + + public int Length => Memory.Length; + } + } +} diff --git a/src/RESPite/Internal/ThreadLocalBlockBufferSerializer.cs b/src/RESPite/Internal/ThreadLocalBlockBufferSerializer.cs new file mode 100644 index 000000000..1c1895ff4 --- /dev/null +++ b/src/RESPite/Internal/ThreadLocalBlockBufferSerializer.cs @@ -0,0 +1,21 @@ +namespace RESPite.Internal; + +internal partial class BlockBufferSerializer +{ + internal static BlockBufferSerializer Shared => ThreadLocalBlockBufferSerializer.Instance; + private sealed class ThreadLocalBlockBufferSerializer : BlockBufferSerializer + { + private ThreadLocalBlockBufferSerializer() { } + public static readonly ThreadLocalBlockBufferSerializer Instance = new(); + + [ThreadStatic] + // side-step concurrency using per-thread semantics + private static BlockBuffer? _perTreadBuffer; + + private protected override BlockBuffer? Buffer + { + get => _perTreadBuffer; + set => _perTreadBuffer = value; + } + } +} diff --git a/src/RESPite/Messages/RespAttributeReader.cs b/src/RESPite/Messages/RespAttributeReader.cs new file mode 100644 index 000000000..9d61802c0 --- /dev/null +++ b/src/RESPite/Messages/RespAttributeReader.cs @@ -0,0 +1,71 @@ +using System.Diagnostics.CodeAnalysis; + +namespace RESPite.Messages; + +/// +/// Allows attribute data to be parsed conveniently. +/// +/// The type of data represented by this reader. +[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] +public abstract class RespAttributeReader +{ + /// + /// Parse a group of attributes. + /// + public virtual void Read(ref RespReader reader, ref T value) + { + reader.Demand(RespPrefix.Attribute); + _ = ReadKeyValuePairs(ref reader, ref value); + } + + /// + /// Parse an aggregate as a set of key/value pairs. + /// + /// The number of pairs successfully processed. + protected virtual int ReadKeyValuePairs(ref RespReader reader, ref T value) + { + var iterator = reader.AggregateChildren(); + + byte[] pooledBuffer = []; + Span localBuffer = stackalloc byte[128]; + int count = 0; + while (iterator.MoveNext()) + { + if (iterator.Value.IsScalar) + { + var key = iterator.Value.Buffer(ref pooledBuffer, localBuffer); + + if (iterator.MoveNext()) + { + if (ReadKeyValuePair(key, ref iterator.Value, ref value)) + { + count++; + } + } + else + { + break; // no matching value for this key + } + } + else + { + if (iterator.MoveNext()) + { + // we won't try to handle aggregate keys; skip the value + } + else + { + break; // no matching value for this key + } + } + } + iterator.MovePast(out reader); + return count; + } + + /// + /// Parse an individual key/value pair. + /// + /// True if the pair was successfully processed. + public virtual bool ReadKeyValuePair(scoped ReadOnlySpan key, ref RespReader reader, ref T value) => false; +} diff --git a/src/RESPite/Messages/RespFrameScanner.cs b/src/RESPite/Messages/RespFrameScanner.cs new file mode 100644 index 000000000..35650ca1d --- /dev/null +++ b/src/RESPite/Messages/RespFrameScanner.cs @@ -0,0 +1,203 @@ +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static RESPite.Internal.RespConstants; +namespace RESPite.Messages; + +/// +/// Scans RESP frames. +/// . +[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] +public sealed class RespFrameScanner // : IFrameSacanner, IFrameValidator +{ + /// + /// Gets a frame scanner for RESP2 request/response connections, or RESP3 connections. + /// + public static RespFrameScanner Default { get; } = new(false); + + /// + /// Gets a frame scanner that identifies RESP2 pub/sub messages. + /// + public static RespFrameScanner Subscription { get; } = new(true); + private RespFrameScanner(bool pubsub) => _pubsub = pubsub; + private readonly bool _pubsub; + + private static readonly uint FastNull = UnsafeCpuUInt32("_\r\n\0"u8), + SingleCharScalarMask = CpuUInt32(0xFF00FFFF), + SingleDigitInteger = UnsafeCpuUInt32(":\0\r\n"u8), + EitherBoolean = UnsafeCpuUInt32("#\0\r\n"u8), + FirstThree = CpuUInt32(0xFFFFFF00); + private static readonly ulong OK = UnsafeCpuUInt64("+OK\r\n\0\0\0"u8), + PONG = UnsafeCpuUInt64("+PONG\r\n\0"u8), + DoubleCharScalarMask = CpuUInt64(0xFF0000FFFF000000), + DoubleDigitInteger = UnsafeCpuUInt64(":\0\0\r\n"u8), + FirstFive = CpuUInt64(0xFFFFFFFFFF000000), + FirstSeven = CpuUInt64(0xFFFFFFFFFFFFFF00); + + private const OperationStatus UseReader = (OperationStatus)(-1); + private static OperationStatus TryFastRead(ReadOnlySpan data, ref RespScanState info) + { + // use silly math to detect the most common short patterns without needing + // to access a reader, or use indexof etc; handles: + // +OK\r\n + // +PONG\r\n + // :N\r\n for any single-digit N (integer) + // :NN\r\n for any double-digit N (integer) + // #N\r\n for any single-digit N (boolean) + // _\r\n (null) + uint hi, lo; + switch (data.Length) + { + case 0: + case 1: + case 2: + return OperationStatus.NeedMoreData; + case 3: + // assume we're reading as little-endian, so: first byte is low + hi = data[0] | ((uint)data[1] << 8) | ((uint)data[2] << 16); + if (!BitConverter.IsLittleEndian) + { + // compensate if necessary (which: it won't be) + hi = BinaryPrimitives.ReverseEndianness(hi); + } + break; + default: + hi = UnsafeCpuUInt32(data); + break; + } + if ((hi & FirstThree) == FastNull) + { + info.SetComplete(3, RespPrefix.Null); + return OperationStatus.Done; + } + + var masked = hi & SingleCharScalarMask; + if (masked == SingleDigitInteger) + { + info.SetComplete(4, RespPrefix.Integer); + return OperationStatus.Done; + } + else if (masked == EitherBoolean) + { + info.SetComplete(4, RespPrefix.Boolean); + return OperationStatus.Done; + } + + switch (data.Length) + { + case 3: + return OperationStatus.NeedMoreData; + case 4: + return UseReader; + case 5: + lo = ((uint)data[4]) << 24; + break; + case 6: + lo = ((uint)UnsafeCpuUInt16(data, 4)) << 16; + break; + case 7: + lo = ((uint)UnsafeCpuUInt16(data, 4)) << 16 | ((uint)UnsafeCpuByte(data, 6)) << 8; + break; + default: + lo = UnsafeCpuUInt32(data, 4); + break; + } + var u64 = BitConverter.IsLittleEndian ? ((((ulong)lo) << 32) | hi) : ((((ulong)hi) << 32) | lo); + if (((u64 & FirstFive) == OK) | ((u64 & DoubleCharScalarMask) == DoubleDigitInteger)) + { + info.SetComplete(5, RespPrefix.SimpleString); + return OperationStatus.Done; + } + if ((u64 & FirstSeven) == PONG) + { + info.SetComplete(7, RespPrefix.SimpleString); + return OperationStatus.Done; + } + return UseReader; + } + + /// + /// Attempt to read more data as part of the current frame. + /// + public OperationStatus TryRead(ref RespScanState state, in ReadOnlySequence data) + { + if (!_pubsub & state.TotalBytes == 0 & data.IsSingleSegment) + { +#if NET + var status = TryFastRead(data.FirstSpan, ref state); +#else + var status = TryFastRead(data.First.Span, ref state); +#endif + if (status != UseReader) return status; + } + + return TryReadViaReader(ref state, in data); + + static OperationStatus TryReadViaReader(ref RespScanState state, in ReadOnlySequence data) + { + var reader = new RespReader(in data); + var complete = state.TryRead(ref reader, out var consumed); + if (complete) + { + return OperationStatus.Done; + } + return OperationStatus.NeedMoreData; + } + } + + /// + /// Attempt to read more data as part of the current frame. + /// + public OperationStatus TryRead(ref RespScanState state, ReadOnlySpan data) + { + if (!_pubsub & state.TotalBytes == 0) + { +#if NET + var status = TryFastRead(data, ref state); +#else + var status = TryFastRead(data, ref state); +#endif + if (status != UseReader) return status; + } + + return TryReadViaReader(ref state, data); + + static OperationStatus TryReadViaReader(ref RespScanState state, ReadOnlySpan data) + { + var reader = new RespReader(data); + var complete = state.TryRead(ref reader, out var consumed); + if (complete) + { + return OperationStatus.Done; + } + return OperationStatus.NeedMoreData; + } + } + + /// + /// Validate that the supplied message is a valid RESP request, specifically: that it contains a single + /// top-level array payload with bulk-string elements, the first of which is non-empty (the command). + /// + public void ValidateRequest(in ReadOnlySequence message) + { + if (message.IsEmpty) Throw("Empty RESP frame"); + RespReader reader = new(in message); + reader.MoveNext(RespPrefix.Array); + reader.DemandNotNull(); + if (reader.IsStreaming) Throw("Streaming is not supported in this context"); + var count = reader.AggregateLength(); + for (int i = 0; i < count; i++) + { + reader.MoveNext(RespPrefix.BulkString); + reader.DemandNotNull(); + if (reader.IsStreaming) Throw("Streaming is not supported in this context"); + + if (i == 0 && reader.ScalarIsEmpty()) Throw("command must be non-empty"); + } + reader.DemandEnd(); + + static void Throw(string message) => throw new InvalidOperationException(message); + } +} diff --git a/src/RESPite/Messages/RespPrefix.cs b/src/RESPite/Messages/RespPrefix.cs new file mode 100644 index 000000000..d58749120 --- /dev/null +++ b/src/RESPite/Messages/RespPrefix.cs @@ -0,0 +1,100 @@ +using System.Diagnostics.CodeAnalysis; + +namespace RESPite.Messages; + +/// +/// RESP protocol prefix. +/// +[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] +public enum RespPrefix : byte +{ + /// + /// Invalid. + /// + None = 0, + + /// + /// Simple strings: +OK\r\n. + /// + SimpleString = (byte)'+', + + /// + /// Simple errors: -ERR message\r\n. + /// + SimpleError = (byte)'-', + + /// + /// Integers: :123\r\n. + /// + Integer = (byte)':', + + /// + /// String with support for binary data: $7\r\nmessage\r\n. + /// + BulkString = (byte)'$', + + /// + /// Multiple inner messages: *1\r\n+message\r\n. + /// + Array = (byte)'*', + + /// + /// Null strings/arrays: _\r\n. + /// + Null = (byte)'_', + + /// + /// Boolean values: #T\r\n. + /// + Boolean = (byte)'#', + + /// + /// Floating-point number: ,123.45\r\n. + /// + Double = (byte)',', + + /// + /// Large integer number: (12...89\r\n. + /// + BigInteger = (byte)'(', + + /// + /// Error with support for binary data: !7\r\nmessage\r\n. + /// + BulkError = (byte)'!', + + /// + /// String that should be interpreted verbatim: =11\r\ntxt:message\r\n. + /// + VerbatimString = (byte)'=', + + /// + /// Multiple sub-items that represent a map. + /// + Map = (byte)'%', + + /// + /// Multiple sub-items that represent a set. + /// + Set = (byte)'~', + + /// + /// Out-of band messages. + /// + Push = (byte)'>', + + /// + /// Continuation of streaming scalar values. + /// + StreamContinuation = (byte)';', + + /// + /// End sentinel for streaming aggregate values. + /// + StreamTerminator = (byte)'.', + + /// + /// Metadata about the next element. + /// + Attribute = (byte)'|', +} diff --git a/src/RESPite/Messages/RespReader.AggregateEnumerator.cs b/src/RESPite/Messages/RespReader.AggregateEnumerator.cs new file mode 100644 index 000000000..412ef6ab5 --- /dev/null +++ b/src/RESPite/Messages/RespReader.AggregateEnumerator.cs @@ -0,0 +1,279 @@ +using System.Collections; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace RESPite.Messages; + +public ref partial struct RespReader +{ + /// + /// Reads the sub-elements associated with an aggregate value. For convenience, when + /// using foreach () the reader + /// is advanced into the child element ready for reading, which bypasses attributes. If attributes + /// are required from child elements, the iterator can be advanced manually (not via + /// foreach using an optional attribute-reader in the call. + /// + public readonly AggregateEnumerator AggregateChildren() => new(in this); + + /// + /// Reads the sub-elements associated with an aggregate value. + /// + public ref struct AggregateEnumerator + { + // Note that _reader is the overall reader that can see outside this aggregate, as opposed + // to Current which is the sub-tree of the current element *only* + private RespReader _reader; + private int _remaining; + + /// + /// Create a new enumerator for the specified . + /// + /// The reader containing the data for this operation. + public AggregateEnumerator(scoped in RespReader reader) + { + reader.DemandAggregate(); + _remaining = reader.IsStreaming ? -1 : reader._length; + _reader = reader; + Value = default; + } + + /// + public readonly AggregateEnumerator GetEnumerator() => this; + + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] +#if DEBUG +#if NET8_0 // strictly net8; net10 and our polyfill have .Message + [Experimental("SERDBG")] +#else + [Experimental("SERDBG", Message = $"Prefer {nameof(Value)}")] +#endif +#endif + public RespReader Current => Value; + + /// + /// Gets the current element associated with this reader. + /// + public RespReader Value; // intentionally a field, because of ref-semantics + + /// + /// Move to the next child if possible, and move the child element into the next node. + /// + public bool MoveNext(RespPrefix prefix) + { + bool result = MoveNextRaw(); + if (result) + { + Value.MoveNext(prefix); + } + return result; + } + + /// + /// Move to the next child if possible, and move the child element into the next node. + /// + /// The type of data represented by this reader. + public bool MoveNext(RespPrefix prefix, RespAttributeReader respAttributeReader, ref T attributes) + { + bool result = MoveNextRaw(respAttributeReader, ref attributes); + if (result) + { + Value.MoveNext(prefix); + } + return result; + } + + /// + /// Move to the next child and leave the reader *ahead of* the first element, + /// allowing us to read attribute data. + /// + /// If you are not consuming attribute data, is preferred. + public bool MoveNextRaw() + { + object? attributes = null; + return MoveNextCore(null, ref attributes); + } + + /// + /// Move to the next child and move into the first element (skipping attributes etc), leaving it ready to consume. + /// + public bool MoveNext() + { + object? attributes = null; + if (MoveNextCore(null, ref attributes)) + { + Value.MoveNext(); + return true; + } + return false; + } + + /// + /// Move to the next child (capturing attribute data) and leave the reader *ahead of* the first element, + /// allowing us to also read attribute data of the child. + /// + /// The type of attribute data represented by this reader. + /// If you are not consuming attribute data, is preferred. + public bool MoveNextRaw(RespAttributeReader respAttributeReader, ref T attributes) + => MoveNextCore(respAttributeReader, ref attributes); + + /// > + private bool MoveNextCore(RespAttributeReader? attributeReader, ref T attributes) + { + if (_remaining == 0) + { + Value = default; + return false; + } + + // in order to provide access to attributes etc, we want Current to be positioned + // *before* the next element; for that, we'll take a snapshot before we read + _reader.MovePastCurrent(); + var snapshot = _reader.Clone(); + + if (!(attributeReader is null + ? _reader.TryReadNextSkipAttributes(skipStreamTerminator: false) + : _reader.TryReadNextProcessAttributes(attributeReader, ref attributes, false))) + { + if (_remaining != 0) ThrowEof(); // incomplete aggregate, simple or streaming + _remaining = 0; + Value = default; + return false; + } + + if (_remaining > 0) + { + // non-streaming, decrement + _remaining--; + } + else if (_reader.Prefix == RespPrefix.StreamTerminator) + { + // end of streaming aggregate + _remaining = 0; + Value = default; + return false; + } + + // move past that sub-tree and trim the "snapshot" state, giving + // us a scoped reader that is *just* that sub-tree + _reader.SkipChildren(); + snapshot.TrimToTotal(_reader.BytesConsumed); + + Value = snapshot; + return true; + } + + /// + /// Move to the end of this aggregate and export the state of the . + /// + /// The reader positioned at the end of the data; this is commonly + /// used to update a tree reader, to get to the next data after the aggregate. + public void MovePast(out RespReader reader) + { + while (MoveNextRaw()) { } + reader = _reader; + } + + /// + /// Moves to the next element, and moves into that element (skipping attributes etc), leaving it ready to consume. + /// + public void DemandNext() + { + if (!MoveNext()) ThrowEof(); + } + + public T ReadOne(Projection projection) + { + DemandNext(); + return projection(ref Value); + } + + public void FillAll(scoped Span target, Projection projection) + { + FillAll(target, ref projection, static (ref projection, ref reader) => projection(ref reader)); + } + + public void FillAll(scoped Span target, ref TState state, Projection projection) +#if NET10_0_OR_GREATER + where TState : allows ref struct +#endif + { + for (int i = 0; i < target.Length; i++) + { + DemandNext(); + target[i] = projection(ref state, ref Value); + } + } + + public void FillAll( + scoped Span target, + Projection first, + Projection second, + Func combine) + { + for (int i = 0; i < target.Length; i++) + { + DemandNext(); + + var x = first(ref Value); + + DemandNext(); + + var y = second(ref Value); + target[i] = combine(x, y); + } + } + + public void FillAll( + scoped Span target, + ref TState state, + Projection first, + Projection second, + Func combine) +#if NET10_0_OR_GREATER + where TState : allows ref struct +#endif + { + for (int i = 0; i < target.Length; i++) + { + DemandNext(); + + var x = first(ref state, ref Value); + + DemandNext(); + + var y = second(ref state, ref Value); + target[i] = combine(state, x, y); + } + } + } + + internal void TrimToTotal(long length) => TrimToRemaining(length - BytesConsumed); + + internal void TrimToRemaining(long bytes) + { + if (_prefix != RespPrefix.None || bytes < 0) Throw(); + + var current = CurrentAvailable; + if (bytes <= current) + { + UnsafeTrimCurrentBy(current - (int)bytes); + _remainingTailLength = 0; + return; + } + + bytes -= current; + if (bytes <= _remainingTailLength) + { + _remainingTailLength = bytes; + return; + } + + Throw(); + static void Throw() => throw new ArgumentOutOfRangeException(nameof(bytes)); + } +} diff --git a/src/RESPite/Messages/RespReader.Debug.cs b/src/RESPite/Messages/RespReader.Debug.cs new file mode 100644 index 000000000..71d5a44af --- /dev/null +++ b/src/RESPite/Messages/RespReader.Debug.cs @@ -0,0 +1,59 @@ +using System.Buffers; +using System.Diagnostics; +using System.Text; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace RESPite.Messages; + +[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] +public ref partial struct RespReader +{ + internal bool DebugEquals(in RespReader other) + => _prefix == other._prefix + && _length == other._length + && _flags == other._flags + && _bufferIndex == other._bufferIndex + && _positionBase == other._positionBase + && _remainingTailLength == other._remainingTailLength; + + internal new string ToString() => $"{Prefix} ({_flags}); length {_length}, {TotalAvailable} remaining"; + + internal void DebugReset() + { + _bufferIndex = 0; + _length = 0; + _flags = 0; + _prefix = RespPrefix.None; + } + +#if DEBUG + internal bool VectorizeDisabled { get; set; } +#endif + + private partial ReadOnlySpan ActiveBuffer { get; } + + internal readonly string BufferUtf8() + { + var clone = Clone(); + var active = clone.ActiveBuffer; + var totalLen = checked((int)(active.Length + clone._remainingTailLength)); + var oversized = ArrayPool.Shared.Rent(totalLen); + Span target = oversized.AsSpan(0, totalLen); + + while (!target.IsEmpty) + { + active.CopyTo(target); + target = target.Slice(active.Length); + if (!clone.TryMoveToNextSegment()) break; + active = clone.ActiveBuffer; + } + if (!target.IsEmpty) throw new EndOfStreamException(); + + var s = Encoding.UTF8.GetString(oversized, 0, totalLen); + ArrayPool.Shared.Return(oversized); + return s; + } +} diff --git a/src/RESPite/Messages/RespReader.ScalarEnumerator.cs b/src/RESPite/Messages/RespReader.ScalarEnumerator.cs new file mode 100644 index 000000000..9e8ffbe70 --- /dev/null +++ b/src/RESPite/Messages/RespReader.ScalarEnumerator.cs @@ -0,0 +1,105 @@ +using System.Buffers; +using System.Collections; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace RESPite.Messages; + +public ref partial struct RespReader +{ + /// + /// Gets the chunks associated with a scalar value. + /// + public readonly ScalarEnumerator ScalarChunks() => new(in this); + + /// + /// Allows enumeration of chunks in a scalar value; this includes simple values + /// that span multiple segments, and streaming + /// scalar RESP values. + /// + public ref struct ScalarEnumerator + { + /// + public readonly ScalarEnumerator GetEnumerator() => this; + + private RespReader _reader; + + private ReadOnlySpan _current; + private ReadOnlySequenceSegment? _tail; + private int _offset, _remaining; + + /// + /// Create a new enumerator for the specified . + /// + /// The reader containing the data for this operation. + public ScalarEnumerator(scoped in RespReader reader) + { + reader.DemandScalar(); + _reader = reader; + InitSegment(); + } + + private void InitSegment() + { + _current = _reader.CurrentSpan(); + _tail = _reader._tail; + _offset = CurrentLength = 0; + _remaining = _reader._length; + if (_reader.TotalAvailable < _remaining) ThrowEof(); + } + + /// + public bool MoveNext() + { + while (true) // for each streaming element + { + _offset += CurrentLength; + while (_remaining > 0) // for each span in the current element + { + // look in the active span + var take = Math.Min(_remaining, _current.Length - _offset); + if (take > 0) // more in the current chunk + { + _remaining -= take; + CurrentLength = take; + return true; + } + + // otherwise, we expect more tail data + if (_tail is null) ThrowEof(); + + _current = _tail.Memory.Span; + _offset = 0; + _tail = _tail.Next; + } + + if (!_reader.MoveNextStreamingScalar()) break; + InitSegment(); + } + + CurrentLength = 0; + return false; + } + + /// + public readonly ReadOnlySpan Current => _current.Slice(_offset, CurrentLength); + + /// + /// Gets the or . + /// + public int CurrentLength { readonly get; private set; } + + /// + /// Move to the end of this aggregate and export the state of the . + /// + /// The reader positioned at the end of the data; this is commonly + /// used to update a tree reader, to get to the next data after the aggregate. + public void MovePast(out RespReader reader) + { + while (MoveNext()) { } + reader = _reader; + } + } +} diff --git a/src/RESPite/Messages/RespReader.Span.cs b/src/RESPite/Messages/RespReader.Span.cs new file mode 100644 index 000000000..cfea585c4 --- /dev/null +++ b/src/RESPite/Messages/RespReader.Span.cs @@ -0,0 +1,86 @@ +#define USE_UNSAFE_SPAN + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace RESPite.Messages; + +/* + How we actually implement the underlying buffer depends on the capabilities of the runtime. + */ + +#if NET8_0_OR_GREATER && USE_UNSAFE_SPAN + +public ref partial struct RespReader +{ + // intent: avoid lots of slicing by dealing with everything manually, and accepting the "don't get it wrong" rule + private ref byte _bufferRoot; + private int _bufferLength; + + private partial void UnsafeTrimCurrentBy(int count) + { + Debug.Assert(count >= 0 && count <= _bufferLength, "Unsafe trim length"); + _bufferLength -= count; + } + + private readonly partial ref byte UnsafeCurrent + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.Add(ref _bufferRoot, _bufferIndex); + } + + private readonly partial int CurrentLength + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _bufferLength; + } + + private readonly partial ReadOnlySpan CurrentSpan() => MemoryMarshal.CreateReadOnlySpan( + ref UnsafeCurrent, CurrentAvailable); + + private readonly partial ReadOnlySpan UnsafePastPrefix() => MemoryMarshal.CreateReadOnlySpan( + ref Unsafe.Add(ref _bufferRoot, _bufferIndex + 1), + _bufferLength - (_bufferIndex + 1)); + + private partial void SetCurrent(ReadOnlySpan value) + { + _bufferRoot = ref MemoryMarshal.GetReference(value); + _bufferLength = value.Length; + } + private partial ReadOnlySpan ActiveBuffer => MemoryMarshal.CreateReadOnlySpan(ref _bufferRoot, _bufferLength); +} +#else +public ref partial struct RespReader // much more conservative - uses slices etc +{ + private ReadOnlySpan _buffer; + + private partial void UnsafeTrimCurrentBy(int count) + { + _buffer = _buffer.Slice(0, _buffer.Length - count); + } + + private readonly partial ref byte UnsafeCurrent + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.AsRef(in _buffer[_bufferIndex]); // hack around CS8333 + } + + private readonly partial int CurrentLength + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _buffer.Length; + } + + private readonly partial ReadOnlySpan UnsafePastPrefix() => _buffer.Slice(_bufferIndex + 1); + + private readonly partial ReadOnlySpan CurrentSpan() => _buffer.Slice(_bufferIndex); + + private partial void SetCurrent(ReadOnlySpan value) => _buffer = value; + private partial ReadOnlySpan ActiveBuffer => _buffer; +} +#endif diff --git a/src/RESPite/Messages/RespReader.Utils.cs b/src/RESPite/Messages/RespReader.Utils.cs new file mode 100644 index 000000000..9aca671fb --- /dev/null +++ b/src/RESPite/Messages/RespReader.Utils.cs @@ -0,0 +1,341 @@ +using System.Buffers.Text; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using RESPite.Internal; + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace RESPite.Messages; + +public ref partial struct RespReader +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UnsafeAssertClLf(int offset) => UnsafeAssertClLf(ref UnsafeCurrent, offset); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly void UnsafeAssertClLf(scoped ref byte source, int offset) + { + if (Unsafe.ReadUnaligned(ref Unsafe.Add(ref source, offset)) != RespConstants.CrLfUInt16) + { + ThrowProtocolFailure($"Expected CR/LF ({offset}={(char)Unsafe.Add(ref source, offset)})"); + } + } + + private enum LengthPrefixResult + { + NeedMoreData, + Length, + Null, + Streaming, + } + + /// + /// Asserts that the current element is a scalar type. + /// + public readonly void DemandScalar() + { + if (!IsScalar) Throw(Prefix); + static void Throw(RespPrefix prefix) => throw new InvalidOperationException($"This operation requires a scalar element, got {prefix}"); + } + + /// + /// Asserts that the current element is a scalar type. + /// + public readonly void DemandAggregate() + { + if (!IsAggregate) Throw(Prefix); + static void Throw(RespPrefix prefix) => throw new InvalidOperationException($"This operation requires an aggregate element, got {prefix}"); + } + + private readonly LengthPrefixResult TryReadLengthPrefix(ReadOnlySpan bytes, out int value, out int byteCount) + { + var end = bytes.IndexOf(RespConstants.CrlfBytes); + if (end < 0) + { + byteCount = value = 0; + if (bytes.Length >= RespConstants.MaxRawBytesInt32 + 2) + { + ThrowProtocolFailure("Unterminated or over-length integer"); // should have failed; report failure to prevent infinite loop + } + return LengthPrefixResult.NeedMoreData; + } + byteCount = end + 2; + switch (end) + { + case 0: + ThrowProtocolFailure("Length prefix expected"); + goto case default; // not reached, just satisfying definite assignment + case 1 when bytes[0] == (byte)'?': + value = 0; + return LengthPrefixResult.Streaming; + default: + if (end > RespConstants.MaxRawBytesInt32 || !(Utf8Parser.TryParse(bytes, out value, out var consumed) && consumed == end)) + { + ThrowProtocolFailure("Unable to parse integer"); + value = 0; + } + if (value < 0) + { + if (value == -1) + { + value = 0; + return LengthPrefixResult.Null; + } + ThrowProtocolFailure("Invalid negative length prefix"); + } + return LengthPrefixResult.Length; + } + } + + /// + /// Create an isolated copy of this reader, which can be advanced independently. + /// + public readonly RespReader Clone() => this; // useful for performing streaming operations without moving the primary + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + private readonly void ThrowProtocolFailure(string message) + => throw new InvalidOperationException($"RESP protocol failure around offset {_positionBase}-{BytesConsumed}: {message}"); // protocol exception? + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + internal static void ThrowEof() => throw new EndOfStreamException(); + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + private static void ThrowFormatException() => throw new FormatException(); + + private int RawTryReadByte() + { + if (_bufferIndex < CurrentLength || TryMoveToNextSegment()) + { + var result = UnsafeCurrent; + _bufferIndex++; + return result; + } + return -1; + } + + private int RawPeekByte() + { + return (CurrentLength < _bufferIndex || TryMoveToNextSegment()) ? UnsafeCurrent : -1; + } + + private bool RawAssertCrLf() + { + if (CurrentAvailable >= 2) + { + UnsafeAssertClLf(0); + _bufferIndex += 2; + return true; + } + else + { + int next = RawTryReadByte(); + if (next < 0) return false; + if (next == '\r') + { + next = RawTryReadByte(); + if (next < 0) return false; + if (next == '\n') return true; + } + ThrowProtocolFailure("Expected CR/LF"); + return false; + } + } + + private LengthPrefixResult RawTryReadLengthPrefix() + { + _length = 0; + if (!RawTryFindCrLf(out int end)) + { + if (TotalAvailable >= RespConstants.MaxRawBytesInt32 + 2) + { + ThrowProtocolFailure("Unterminated or over-length integer"); // should have failed; report failure to prevent infinite loop + } + return LengthPrefixResult.NeedMoreData; + } + + switch (end) + { + case 0: + ThrowProtocolFailure("Length prefix expected"); + goto case default; // not reached, just satisfying definite assignment + case 1: + var b = (byte)RawTryReadByte(); + RawAssertCrLf(); + if (b == '?') + { + return LengthPrefixResult.Streaming; + } + else + { + _length = ParseSingleDigit(b); + return LengthPrefixResult.Length; + } + default: + if (end > RespConstants.MaxRawBytesInt32) + { + ThrowProtocolFailure("Unable to parse integer"); + } + Span bytes = stackalloc byte[end]; + RawFillBytes(bytes); + RawAssertCrLf(); + if (!(Utf8Parser.TryParse(bytes, out _length, out var consumed) && consumed == end)) + { + ThrowProtocolFailure("Unable to parse integer"); + } + + if (_length < 0) + { + if (_length == -1) + { + _length = 0; + return LengthPrefixResult.Null; + } + ThrowProtocolFailure("Invalid negative length prefix"); + } + + return LengthPrefixResult.Length; + } + } + + private void RawFillBytes(scoped Span target) + { + do + { + var current = CurrentSpan(); + if (current.Length >= target.Length) + { + // more than enough, need to trim + current.Slice(0, target.Length).CopyTo(target); + _bufferIndex += target.Length; + return; // we're done + } + else + { + // take what we can + current.CopyTo(target); + target = target.Slice(current.Length); + // we could move _bufferIndex here, but we're about to trash that in TryMoveToNextSegment + } + } + while (TryMoveToNextSegment()); + ThrowEof(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ParseSingleDigit(byte value) + { + return value switch + { + (byte)'0' or (byte)'1' or (byte)'2' or (byte)'3' or (byte)'4' or (byte)'5' or (byte)'6' or (byte)'7' or (byte)'8' or (byte)'9' => value - (byte)'0', + _ => Invalid(value), + }; + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + static int Invalid(byte value) => throw new FormatException($"Unable to parse integer: '{(char)value}'"); + } + + private readonly bool RawTryAssertInlineScalarPayloadCrLf() + { + Debug.Assert(IsInlineScalar, "should be inline scalar"); + + var reader = Clone(); + var len = reader._length; + if (len == 0) return reader.RawAssertCrLf(); + + do + { + var current = reader.CurrentSpan(); + if (current.Length >= len) + { + reader._bufferIndex += len; + return reader.RawAssertCrLf(); // we're done + } + else + { + // take what we can + len -= current.Length; + // we could move _bufferIndex here, but we're about to trash that in TryMoveToNextSegment + } + } + while (reader.TryMoveToNextSegment()); + return false; // EOF + } + + private readonly bool RawTryFindCrLf(out int length) + { + length = 0; + RespReader reader = Clone(); + do + { + var span = reader.CurrentSpan(); + var index = span.IndexOf((byte)'\r'); + if (index >= 0) + { + checked + { + length += index; + } + // move past the CR and assert the LF + reader._bufferIndex += index + 1; + var next = reader.RawTryReadByte(); + if (next < 0) break; // we don't know + if (next != '\n') ThrowProtocolFailure("CR/LF expected"); + + return true; + } + checked + { + length += span.Length; + } + } + while (reader.TryMoveToNextSegment()); + length = 0; + return false; + } + + private string GetDebuggerDisplay() + { + return ToString(); + } + + internal readonly int GetInitialScanCount(out ushort streamingAggregateDepth) + { + // this is *similar* to GetDelta, but: without any discount for attributes + switch (_flags & (RespFlags.IsAggregate | RespFlags.IsStreaming)) + { + case RespFlags.IsAggregate: + streamingAggregateDepth = 0; + return _length - 1; + case RespFlags.IsAggregate | RespFlags.IsStreaming: + streamingAggregateDepth = 1; + return 0; + default: + streamingAggregateDepth = 0; + return -1; + } + } + + /// + /// Get the raw RESP payload. + /// + public readonly byte[] Serialize() + { + var reader = Clone(); + int remaining = checked((int)reader.TotalAvailable); + var arr = new byte[remaining]; + Span target = arr; + while (remaining > 0) + { + var span = reader.CurrentSpan(); + span.CopyTo(arr); + remaining -= span.Length; + target = target.Slice(span.Length); + if (!reader.TryMoveToNextSegment()) break; + } + if (remaining != 0 | !target.IsEmpty) ThrowEof(); + return arr; + } +} diff --git a/src/RESPite/Messages/RespReader.cs b/src/RESPite/Messages/RespReader.cs new file mode 100644 index 000000000..b2288b574 --- /dev/null +++ b/src/RESPite/Messages/RespReader.cs @@ -0,0 +1,2037 @@ +using System.Buffers; +using System.Buffers.Text; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; +using RESPite.Internal; + +#if NET +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CS0282 // There is no defined ordering between fields in multiple declarations of partial struct +#pragma warning restore IDE0079 // Remove unnecessary suppression + +namespace RESPite.Messages; + +/// +/// Provides low level RESP parsing functionality. +/// +[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] +public ref partial struct RespReader +{ + [Flags] + private enum RespFlags : byte + { + None = 0, + IsScalar = 1 << 0, // simple strings, bulk strings, etc + IsAggregate = 1 << 1, // arrays, maps, sets, etc + IsNull = 1 << 2, // explicit null RESP types, or bulk-strings/aggregates with length -1 + IsInlineScalar = 1 << 3, // a non-null scalar, i.e. with payload+CrLf + IsAttribute = 1 << 4, // is metadata for following elements + IsStreaming = 1 << 5, // unknown length + IsError = 1 << 6, // an explicit error reported inside the protocol + } + + // relates to the element we're currently reading + private RespFlags _flags; + private RespPrefix _prefix; + + private int _length; // for null: 0; for scalars: the length of the payload; for aggregates: the child count + + // the current buffer that we're observing + private int _bufferIndex; // after TryRead, this should be positioned immediately before the actual data + + // the position in a multi-segment payload + private long _positionBase; // total data we've already moved past in *previous* buffers + private ReadOnlySequenceSegment? _tail; // the next tail node + private long _remainingTailLength; // how much more can we consume from the tail? + + public long ProtocolBytesRemaining => TotalAvailable; + + private readonly int CurrentAvailable => CurrentLength - _bufferIndex; + + private readonly long TotalAvailable => CurrentAvailable + _remainingTailLength; + private partial void UnsafeTrimCurrentBy(int count); + private readonly partial ref byte UnsafeCurrent { get; } + private readonly partial int CurrentLength { get; } + private partial void SetCurrent(ReadOnlySpan value); + private RespPrefix UnsafePeekPrefix() => (RespPrefix)UnsafeCurrent; + private readonly partial ReadOnlySpan UnsafePastPrefix(); + private readonly partial ReadOnlySpan CurrentSpan(); + + /// + /// Get the scalar value as a single-segment span. + /// + /// True if this is a non-streaming scalar element that covers a single span only, otherwise False. + /// If a scalar reports False, can be used to iterate the entire payload. + /// When True, the contents of the scalar value. + public readonly bool TryGetSpan(out ReadOnlySpan value) + { + if (IsInlineScalar && CurrentAvailable >= _length) + { + value = CurrentSpan().Slice(0, _length); + return true; + } + + value = default; + return IsNullScalar; + } + + /// + /// Returns the position after the end of the current element. + /// + public readonly long BytesConsumed => _positionBase + _bufferIndex + TrailingLength; + + /// + /// Body length of scalar values, plus any terminating sentinels. + /// + private readonly int TrailingLength => (_flags & RespFlags.IsInlineScalar) == 0 ? 0 : (_length + 2); + + /// + /// Gets the RESP kind of the current element. + /// + public readonly RespPrefix Prefix => _prefix; + + /// + /// The payload length of this scalar element (includes combined length for streaming scalars). + /// + public readonly int ScalarLength() => + IsInlineScalar ? _length : IsNullScalar ? 0 : checked((int)ScalarLengthSlow()); + + /// + /// Indicates whether this scalar value is zero-length. + /// + public readonly bool ScalarIsEmpty() => + IsInlineScalar ? _length == 0 : (IsNullScalar || !ScalarChunks().MoveNext()); + + /// + /// Indicates whether this aggregate value is zero-length. + /// + public readonly bool AggregateIsEmpty() => AggregateLengthIs(0); + + /// + /// The payload length of this scalar element (includes combined length for streaming scalars). + /// + public readonly long ScalarLongLength() => IsInlineScalar ? _length : IsNullScalar ? 0 : ScalarLengthSlow(); + + /// + /// Indicates whether the payload length of this scalar element is exactly the specified value. + /// + public readonly bool ScalarLengthIs(int count) + => IsInlineScalar ? _length == count : (IsNullScalar ? count == 0 : ScalarLengthIsSlow(count)); + + private readonly long ScalarLengthSlow() + { + DemandScalar(); + long length = 0; + var iterator = ScalarChunks(); + while (iterator.MoveNext()) + { + length += iterator.CurrentLength; + } + + return length; + } + + private readonly bool ScalarLengthIsSlow(int expected) + { + DemandScalar(); + int length = 0; + var iterator = ScalarChunks(); + while (length <= expected && iterator.MoveNext()) // short-circuit if we've read enough to know + { + length += iterator.CurrentLength; + } + + return length == expected; + } + + /// + /// The number of child elements associated with an aggregate. + /// + /// For + /// and aggregates, this is twice the value reported in the RESP protocol, + /// i.e. a map of the form %2\r\n... will report 4 as the length. + /// Note that if the data could be streaming (), it may be preferable to use + /// the API, using the API to update the outer reader. + public readonly int AggregateLength() => + (_flags & (RespFlags.IsAggregate | RespFlags.IsStreaming)) == RespFlags.IsAggregate + ? _length : AggregateLengthSlow(); + + /// + /// Indicates whether the number of child elements associated with an aggregate is exactly the specified value. + /// + /// For + /// and aggregates, this is twice the value reported in the RESP protocol, + /// i.e. a map of the form %2\r\n... will report 4 as the length. + public readonly bool AggregateLengthIs(int count) + => (_flags & (RespFlags.IsAggregate | RespFlags.IsStreaming)) == RespFlags.IsAggregate + ? _length == count : AggregateLengthIsSlow(count); + + public delegate T Projection(ref RespReader value); + + public delegate TResult Projection(ref TState state, ref RespReader value) +#if NET10_0_OR_GREATER + where TState : allows ref struct +#endif + ; + + public void FillAll(scoped Span target, Projection projection) + { + DemandNotNull(); + AggregateChildren().FillAll(target, projection); + } + + public void FillAll(scoped Span target, ref TState state, Projection projection) + { + DemandNotNull(); + AggregateChildren().FillAll(target, ref state, projection); + } + + private readonly int AggregateLengthSlow() + { + switch (_flags & (RespFlags.IsAggregate | RespFlags.IsStreaming)) + { + case RespFlags.IsAggregate: + return _length; + case RespFlags.IsAggregate | RespFlags.IsStreaming: + break; + default: + DemandAggregate(); // we expect this to throw + break; + } + + int count = 0; + var reader = Clone(); + while (true) + { + if (!reader.TryReadNextSkipAttributes(skipStreamTerminator: false)) ThrowEof(); + if (reader.Prefix == RespPrefix.StreamTerminator) + { + return count; + } + + reader.SkipChildren(); + count++; + } + } + + private readonly bool AggregateLengthIsSlow(int expected) + { + switch (_flags & (RespFlags.IsAggregate | RespFlags.IsStreaming)) + { + case RespFlags.IsAggregate: + return _length == expected; + case RespFlags.IsAggregate | RespFlags.IsStreaming: + break; + default: + DemandAggregate(); // we expect this to throw + break; + } + + int count = 0; + var reader = Clone(); + while (count <= expected) // short-circuit if we've read enough to know + { + if (!reader.TryReadNextSkipAttributes(skipStreamTerminator: false)) ThrowEof(); + if (reader.Prefix == RespPrefix.StreamTerminator) + { + break; + } + + reader.SkipChildren(); + count++; + } + return count == expected; + } + + /// + /// Indicates whether this is a scalar value, i.e. with a potential payload body. + /// + public readonly bool IsScalar => (_flags & RespFlags.IsScalar) != 0; + + internal readonly bool IsInlineScalar => (_flags & RespFlags.IsInlineScalar) != 0; + + internal readonly bool IsNullScalar => + (_flags & (RespFlags.IsScalar | RespFlags.IsNull)) == (RespFlags.IsScalar | RespFlags.IsNull); + + /// + /// Indicates whether this is an aggregate value, i.e. represents a collection of sub-values. + /// + public readonly bool IsAggregate => (_flags & RespFlags.IsAggregate) != 0; + + internal readonly bool IsNonNullAggregate + => (_flags & (RespFlags.IsAggregate | RespFlags.IsNull)) == RespFlags.IsAggregate; + + /// + /// Indicates whether this is a null value; this could be an explicit , + /// or a scalar or aggregate a negative reported length. + /// + public readonly bool IsNull => (_flags & RespFlags.IsNull) != 0; + + /// + /// Indicates whether this is an attribute value, i.e. metadata relating to later element data. + /// + public readonly bool IsAttribute => (_flags & RespFlags.IsAttribute) != 0; + + /// + /// Indicates whether this represents streaming content, where the or is not known in advance. + /// + public readonly bool IsStreaming => (_flags & RespFlags.IsStreaming) != 0; + + /// + /// Equivalent to both and . + /// + internal readonly bool IsStreamingScalar => (_flags & (RespFlags.IsScalar | RespFlags.IsStreaming)) == + (RespFlags.IsScalar | RespFlags.IsStreaming); + + /// + /// Indicates errors reported inside the protocol. + /// + public readonly bool IsError => (_flags & RespFlags.IsError) != 0; + + /// + /// Gets the effective change (in terms of how many RESP nodes we expect to see) from consuming this element. + /// For simple scalars, this is -1 because we have one less node to read; for simple aggregates, this is + /// AggregateLength-1 because we will have consumed one element, but now need to read the additional + /// child elements. Attributes report 0, since they supplement data + /// we still need to consume. The final terminator for streaming data reports a delta of -1, otherwise: 0. + /// + /// This does not account for being nested inside a streaming aggregate; the caller must deal with that manually. + internal int Delta() => + (_flags & (RespFlags.IsScalar | RespFlags.IsAggregate | RespFlags.IsStreaming | RespFlags.IsAttribute)) switch + { + RespFlags.IsScalar | RespFlags.IsAggregate=> -1, // null has this + RespFlags.IsScalar => -1, + RespFlags.IsAggregate => _length - 1, + RespFlags.IsAggregate | RespFlags.IsAttribute => _length, + _ => 0, + }; + + /// + /// Assert that this is the final element in the current payload. + /// + /// If additional elements are available. + public void DemandEnd() + { +#pragma warning disable CS0618 // avoid TryReadNext unless you know what you're doing + while (IsStreamingScalar) + { + if (!TryReadNext()) ThrowEof(); + } + + if (TryReadNext()) + { + Throw(Prefix); + } +#pragma warning restore CS0618 + + static void Throw(RespPrefix prefix) => + throw new InvalidOperationException($"Expected end of payload, but found {prefix}"); + } + + private bool TryReadNextSkipAttributes(bool skipStreamTerminator) + { +#pragma warning disable CS0618 // avoid TryReadNext unless you know what you're doing + while (TryReadNext()) + { + if (IsAttribute) + { + SkipChildren(); + } + else if (skipStreamTerminator & Prefix is RespPrefix.StreamTerminator) + { + // skip terminator + } + else + { + return true; + } + } +#pragma warning restore CS0618 + return false; + } + + private bool TryReadNextProcessAttributes(RespAttributeReader respAttributeReader, ref T attributes, bool skipStreamTerminator) + { +#pragma warning disable CS0618 // avoid TryReadNext unless you know what you're doing + while (TryReadNext()) +#pragma warning restore CS0618 + { + if (IsAttribute) + { + respAttributeReader.Read(ref this, ref attributes); + } + else if (skipStreamTerminator & Prefix is RespPrefix.StreamTerminator) + { + // skip terminator + } + else + { + return true; + } + } + + return false; + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// If the data is exhausted before a streaming scalar is exhausted. + /// If the data contains an explicit error element. + public bool TryMoveNext() + { + while (IsStreamingScalar) // close out the current streaming scalar + { + if (!TryReadNextSkipAttributes(false)) ThrowEof(); + } + + if (TryReadNextSkipAttributes(true)) + { + if (IsError) ThrowError(); + return true; + } + + return false; + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// Whether to check and throw for error messages. + /// If the data is exhausted before a streaming scalar is exhausted. + /// If the data contains an explicit error element. + public bool TryMoveNext(bool checkError) + { + while (IsStreamingScalar) // close out the current streaming scalar + { + if (!TryReadNextSkipAttributes(false)) ThrowEof(); + } + + if (TryReadNextSkipAttributes(true)) + { + if (checkError && IsError) ThrowError(); + return true; + } + + return false; + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// Parser for attribute data preceding the data. + /// The state for attributes encountered. + /// If the data is exhausted before a streaming scalar is exhausted. + /// If the data contains an explicit error element. + /// The type of data represented by this reader. + public bool TryMoveNext(RespAttributeReader respAttributeReader, ref T attributes) + { + while (IsStreamingScalar) // close out the current streaming scalar + { + if (!TryReadNextSkipAttributes(false)) ThrowEof(); + } + + if (TryReadNextProcessAttributes(respAttributeReader, ref attributes, true)) + { + if (IsError) ThrowError(); + return true; + } + + return false; + } + + /// + /// Move to the next content element, asserting that it is of the expected type; this skips attribute metadata, checking for RESP error messages by default. + /// + /// The expected data type. + /// If the data is exhausted before a streaming scalar is exhausted. + /// If the data contains an explicit error element. + /// If the data is not of the expected type. + public bool TryMoveNext(RespPrefix prefix) + { + bool result = TryMoveNext(); + if (result) Demand(prefix); + return result; + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + public void MoveNext() + { + if (!TryMoveNext()) ThrowEof(); + } + + /// + /// Move to the next content element; this skips attribute metadata, checking for RESP error messages by default. + /// + /// Parser for attribute data preceding the data. + /// The state for attributes encountered. + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// The type of data represented by this reader. + public void MoveNext(RespAttributeReader respAttributeReader, ref T attributes) + { + if (!TryMoveNext(respAttributeReader, ref attributes)) ThrowEof(); + } + + private bool MoveNextStreamingScalar() + { + if (IsStreamingScalar) + { +#pragma warning disable CS0618 // avoid TryReadNext unless you know what you're doing + while (TryReadNext()) +#pragma warning restore CS0618 + { + if (IsAttribute) + { + SkipChildren(); + } + else + { + if (Prefix != RespPrefix.StreamContinuation) + ThrowProtocolFailure("Streaming continuation expected"); + return _length > 0; + } + } + + ThrowEof(); // we should have found something! + } + + return false; + } + + /// + /// Move to the next content element () and assert that it is a scalar (). + /// + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// If the data is not a scalar type. + public void MoveNextScalar() + { + MoveNext(); + DemandScalar(); + } + + /// + /// Move to the next content element () and assert that it is an aggregate (). + /// + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// If the data is not an aggregate type. + public void MoveNextAggregate() + { + MoveNext(); + DemandAggregate(); + } + + /// + /// Move to the next content element () and assert that it of type specified + /// in . + /// + /// The expected data type. + /// Parser for attribute data preceding the data. + /// The state for attributes encountered. + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// If the data is not of the expected type. + /// The type of data represented by this reader. + public void MoveNext(RespPrefix prefix, RespAttributeReader respAttributeReader, ref T attributes) + { + MoveNext(respAttributeReader, ref attributes); + Demand(prefix); + } + + /// + /// Move to the next content element () and assert that it of type specified + /// in . + /// + /// The expected data type. + /// If the data is exhausted before content is found. + /// If the data contains an explicit error element. + /// If the data is not of the expected type. + public void MoveNext(RespPrefix prefix) + { + MoveNext(); + Demand(prefix); + } + + internal void Demand(RespPrefix prefix) + { + if (Prefix != prefix) Throw(prefix, Prefix); + + static void Throw(RespPrefix expected, RespPrefix actual) => + throw new InvalidOperationException($"Expected {expected} element, but found {actual}."); + } + + private readonly void ThrowError() => throw new RespException(ReadString()!); + + /// + /// Skip all sub elements of the current node; this includes both aggregate children and scalar streaming elements. + /// + public void SkipChildren() + { + // if this is a simple non-streaming scalar, then: there's nothing complex to do; otherwise, re-use the + // frame scanner logic to seek past the noise (this way, we avoid recursion etc) + switch (_flags & (RespFlags.IsScalar | RespFlags.IsAggregate | RespFlags.IsStreaming)) + { + case RespFlags.None: + // no current element + break; + case RespFlags.IsScalar: + // simple scalar + MovePastCurrent(); + break; + default: + // something more complex + RespScanState state = new(in this); + if (!state.TryRead(ref this, out _)) ThrowEof(); + break; + } + } + + /// + /// Reads the current element as a string value. + /// + public readonly string? ReadString() => ReadString(out _); + + /// + /// Reads the current element as a string value. + /// + public readonly string? ReadString(out string prefix) + { + byte[] pooled = []; + try + { + var span = Buffer(ref pooled, stackalloc byte[256]); + prefix = ""; + if (span.IsEmpty) + { + return IsNull ? null : ""; + } + + if (Prefix == RespPrefix.VerbatimString + && span.Length >= 4 && span[3] == ':') + { + // "the first three bytes provide information about the format of the following string, + // which can be txt for plain text, or mkd for markdown. The fourth byte is always :. + // Then the real string follows." + var prefixValue = RespConstants.UnsafeCpuUInt32(span); + if (prefixValue == PrefixTxt) + { + prefix = "txt"; + } + else if (prefixValue == PrefixMkd) + { + prefix = "mkd"; + } + else + { + prefix = RespConstants.UTF8.GetString(span.Slice(0, 3)); + } + + span = span.Slice(4); + } + + return RespConstants.UTF8.GetString(span); + } + finally + { + ArrayPool.Shared.Return(pooled); + } + } + + private static readonly uint + PrefixTxt = RespConstants.UnsafeCpuUInt32("txt:"u8), + PrefixMkd = RespConstants.UnsafeCpuUInt32("mkd:"u8); + + /// + /// Reads the current element as a string value. + /// + public readonly byte[]? ReadByteArray() + { + byte[] pooled = []; + try + { + var span = Buffer(ref pooled, stackalloc byte[256]); + if (span.IsEmpty) + { + return IsNull ? null : []; + } + + return span.ToArray(); + } + finally + { + ArrayPool.Shared.Return(pooled); + } + } + + /// + /// Reads the current element using a general purpose text parser. + /// + /// The type of data being parsed. + internal readonly T ParseBytes(Parser parser) + { + byte[] pooled = []; + var span = Buffer(ref pooled, stackalloc byte[256]); + try + { + return parser(span); + } + finally + { + ArrayPool.Shared.Return(pooled); + } + } + + /// + /// Reads the current element using a general purpose text parser. + /// + /// The type of data being parsed. + /// State required by the parser. + internal readonly T ParseBytes(Parser parser, TState? state) + { + byte[] pooled = []; + var span = Buffer(ref pooled, stackalloc byte[256]); + try + { + return parser(span, default); + } + finally + { + ArrayPool.Shared.Return(pooled); + } + } + + public readonly unsafe bool TryParseScalar( + delegate* managed, out T, bool> parser, out T value) + { + // Fast path: try to get the span directly + return TryGetSpan(out var span) ? parser(span, out value) : TryParseSlow(parser, out value); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private readonly unsafe bool TryParseSlow( + delegate* managed, out T, bool> parser, + out T value) + { + byte[] pooled = []; + try + { + var span = Buffer(ref pooled, stackalloc byte[256]); + return parser(span, out value); + } + finally + { + ArrayPool.Shared.Return(pooled); + } + } + + /// + /// Tries to read the current scalar element using a parser callback. + /// + /// The type of data being parsed. + /// The parser callback. + /// The parsed value if successful. + /// true if parsing succeeded; otherwise, false. +#pragma warning disable RS0016, RS0027 // public API + [Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if DEBUG + [Obsolete("Please prefer the function-pointer API for library-internal use.")] +#endif + public readonly bool TryParseScalar(ScalarParser parser, out T value) +#pragma warning restore RS0016, RS0027 // public API + { + // Fast path: try to get the span directly + return TryGetSpan(out var span) ? parser(span, out value) : TryParseSlow(parser, out value); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private readonly bool TryParseSlow(ScalarParser parser, out T value) + { + byte[] pooled = []; + try + { + var span = Buffer(ref pooled, stackalloc byte[256]); + return parser(span, out value); + } + finally + { + ArrayPool.Shared.Return(pooled); + } + } + + /// + /// Buffers the current scalar value into the provided target span. + /// + /// The target span to buffer data into. + /// + /// A span containing the buffered data. If the scalar data fits entirely within , + /// returns a slice of containing all the data. If the scalar data is larger than + /// , returns filled with the first target.Length bytes + /// of the scalar data (the remaining data is not buffered). + /// + /// + /// This method first attempts to use to avoid copying. If the data is non-contiguous + /// (e.g., streaming scalars or data spanning multiple buffer segments), it will copy data into . + /// When the source data exceeds 's capacity, only the first target.Length bytes + /// are copied and returned. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly ReadOnlySpan Buffer(Span target) + { + if (TryGetSpan(out var simple)) + { + return simple; + } + +#if NET + return BufferSlow(ref Unsafe.NullRef(), target, usePool: false); +#else + byte[] pooled = []; + return BufferSlow(ref pooled, target, usePool: false); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly ReadOnlySpan Buffer(scoped ref byte[] pooled, Span target = default) + => TryGetSpan(out var simple) ? simple : BufferSlow(ref pooled, target, true); + + [MethodImpl(MethodImplOptions.NoInlining)] + private readonly ReadOnlySpan BufferSlow(scoped ref byte[] pooled, Span target, bool usePool) + { + DemandScalar(); + + if (IsInlineScalar && usePool) + { + // grow to the correct size in advance, if needed + var length = ScalarLength(); + if (length > target.Length) + { + var bigger = ArrayPool.Shared.Rent(length); + ArrayPool.Shared.Return(pooled); + target = pooled = bigger; + } + } + + var iterator = ScalarChunks(); + ReadOnlySpan current; + int offset = 0; + while (iterator.MoveNext()) + { + // will the current chunk fit? + current = iterator.Current; + if (current.TryCopyTo(target.Slice(offset))) + { + // fits into the current buffer + offset += current.Length; + } + else if (!usePool) + { + // rent disallowed; fill what we can + var available = target.Slice(offset); + current.Slice(0, available.Length).CopyTo(available); + return target; // we filled it + } + else + { + // rent a bigger buffer, copy and recycle + var bigger = ArrayPool.Shared.Rent(offset + current.Length); + if (offset != 0) + { + target.Slice(0, offset).CopyTo(bigger); + } + + ArrayPool.Shared.Return(pooled); + target = pooled = bigger; + current.CopyTo(target.Slice(offset)); + } + } + + return target.Slice(0, offset); + } + + /// + /// Reads the current element using a general purpose byte parser. + /// + /// The type of data being parsed. + internal readonly T ParseChars(Parser parser) + { + byte[] bArr = []; + char[] cArr = []; + try + { + var bSpan = Buffer(ref bArr, stackalloc byte[128]); + var maxChars = RespConstants.UTF8.GetMaxCharCount(bSpan.Length); + Span cSpan = maxChars <= 128 ? stackalloc char[128] : (cArr = ArrayPool.Shared.Rent(maxChars)); + int chars = RespConstants.UTF8.GetChars(bSpan, cSpan); + return parser(cSpan.Slice(0, chars)); + } + finally + { + ArrayPool.Shared.Return(bArr); + ArrayPool.Shared.Return(cArr); + } + } + + /// + /// Reads the current element using a general purpose byte parser. + /// + /// The type of data being parsed. + /// State required by the parser. + internal readonly T ParseChars(Parser parser, TState? state) + { + byte[] bArr = []; + char[] cArr = []; + try + { + var bSpan = Buffer(ref bArr, stackalloc byte[128]); + var maxChars = RespConstants.UTF8.GetMaxCharCount(bSpan.Length); + Span cSpan = maxChars <= 128 ? stackalloc char[128] : (cArr = ArrayPool.Shared.Rent(maxChars)); + int chars = RespConstants.UTF8.GetChars(bSpan, cSpan); + return parser(cSpan.Slice(0, chars), state); + } + finally + { + ArrayPool.Shared.Return(bArr); + ArrayPool.Shared.Return(cArr); + } + } + +#if NET8_0_OR_GREATER + /// + /// Reads the current element using . + /// + /// The type of data being parsed. +#pragma warning disable RS0016, RS0027 // back-compat overload + public readonly T ParseChars(IFormatProvider? formatProvider = null) where T : ISpanParsable +#pragma warning restore RS0016, RS0027 // back-compat overload + { + byte[] bArr = []; + char[] cArr = []; + try + { + var bSpan = Buffer(ref bArr, stackalloc byte[128]); + var maxChars = RespConstants.UTF8.GetMaxCharCount(bSpan.Length); + Span cSpan = maxChars <= 128 ? stackalloc char[128] : (cArr = ArrayPool.Shared.Rent(maxChars)); + int chars = RespConstants.UTF8.GetChars(bSpan, cSpan); + return T.Parse(cSpan.Slice(0, chars), formatProvider ?? CultureInfo.InvariantCulture); + } + finally + { + ArrayPool.Shared.Return(bArr); + ArrayPool.Shared.Return(cArr); + } + } +#endif + +#if NET8_0_OR_GREATER + /// + /// Reads the current element using . + /// + /// The type of data being parsed. +#pragma warning disable RS0016, RS0027 // back-compat overload + public readonly T ParseBytes(IFormatProvider? formatProvider = null) where T : IUtf8SpanParsable +#pragma warning restore RS0016, RS0027 // back-compat overload + { + byte[] bArr = []; + try + { + var bSpan = Buffer(ref bArr, stackalloc byte[128]); + return T.Parse(bSpan, formatProvider ?? CultureInfo.InvariantCulture); + } + finally + { + ArrayPool.Shared.Return(bArr); + } + } +#endif + + /// + /// General purpose parsing callback. + /// + /// The type of source data being parsed. + /// State required by the parser. + /// The output type of data being parsed. + // is this needed? + internal delegate TValue Parser(scoped ReadOnlySpan value, TState? state); + + /// + /// General purpose parsing callback. + /// + /// The type of source data being parsed. + /// The output type of data being parsed. + // is this needed? + internal delegate TValue Parser(scoped ReadOnlySpan value); + + /// + /// Scalar parsing callback that returns a boolean indicating success. + /// + /// The type of source data being parsed. + /// The output type of data being parsed. + public delegate bool ScalarParser(scoped ReadOnlySpan value, out TValue result); + + /// + /// Initializes a new instance of the struct. + /// + /// The raw contents to parse with this instance. + public RespReader(ReadOnlySpan value) + { + _length = 0; + _flags = RespFlags.None; + _prefix = RespPrefix.None; + SetCurrent(value); + + _remainingTailLength = _positionBase = 0; + _tail = null; + } + + private void MovePastCurrent() + { + // skip past the trailing portion of a value, if any + var skip = TrailingLength; + if (_bufferIndex + skip <= CurrentLength) + { + _bufferIndex += skip; // available in the current buffer + } + else + { + AdvanceSlow(skip); + } + + // reset the current state + _length = 0; + _flags = 0; + _prefix = RespPrefix.None; + } + + /// + public RespReader(scoped in ReadOnlySequence value) +#if NET + : this(value.FirstSpan) +#else + : this(value.First.Span) +#endif + { + if (!value.IsSingleSegment) + { + _remainingTailLength = value.Length - CurrentLength; + _tail = (value.Start.GetObject() as ReadOnlySequenceSegment)?.Next ?? MissingNext(); + } + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + static ReadOnlySequenceSegment MissingNext() => + throw new ArgumentException("Unable to extract tail segment", nameof(value)); + } + + /// + /// Attempt to move to the next RESP element. + /// + /// Unless you are intentionally handling errors, attributes and streaming data, should be preferred. + [EditorBrowsable(EditorBrowsableState.Never), Browsable(false)] + [Obsolete("Unless you are manually handling errors, attributes and streaming data, TryMoveNext() should be preferred.", false)] + public unsafe bool TryReadNext() + { + MovePastCurrent(); + +#if NET + // check what we have available; don't worry about zero/fetching the next segment; this is only + // for SIMD lookup, and zero would only apply when data ends exactly on segment boundaries, which + // is incredible niche + var available = CurrentAvailable; + + if (Avx2.IsSupported && Bmi1.IsSupported && available >= sizeof(uint)) + { + // read the first 4 bytes + ref byte origin = ref UnsafeCurrent; + var comparand = Unsafe.ReadUnaligned(ref origin); + + // broadcast those 4 bytes into a vector, mask to get just the first and last byte, and apply a SIMD equality test with our known cases + var eqs = + Avx2.CompareEqual(Avx2.And(Avx2.BroadcastScalarToVector256(&comparand), Raw.FirstLastMask), Raw.CommonRespPrefixes); + + // reinterpret that as floats, and pick out the sign bits (which will be 1 for "equal", 0 for "not equal"); since the + // test cases are mutually exclusive, we expect zero or one matches, so: lzcount tells us which matched + var index = + Bmi1.TrailingZeroCount((uint)Avx.MoveMask(Unsafe.As, Vector256>(ref eqs))); + int len; +#if DEBUG + if (VectorizeDisabled) index = uint.MaxValue; // just to break the switch +#endif + switch (index) + { + case Raw.CommonRespIndex_Success when available >= 5 && Unsafe.Add(ref origin, 4) == (byte)'\n': + _prefix = RespPrefix.SimpleString; + _length = 2; + _bufferIndex++; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + return true; + case Raw.CommonRespIndex_SingleDigitInteger when Unsafe.Add(ref origin, 2) == (byte)'\r': + _prefix = RespPrefix.Integer; + _length = 1; + _bufferIndex++; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + return true; + case Raw.CommonRespIndex_DoubleDigitInteger when available >= 5 && Unsafe.Add(ref origin, 4) == (byte)'\n': + _prefix = RespPrefix.Integer; + _length = 2; + _bufferIndex++; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + return true; + case Raw.CommonRespIndex_SingleDigitString when Unsafe.Add(ref origin, 2) == (byte)'\r': + if (comparand == RespConstants.BulkStringStreaming) + { + _flags = RespFlags.IsScalar | RespFlags.IsStreaming; + } + else + { + len = ParseSingleDigit(Unsafe.Add(ref origin, 1)); + if (available < len + 6) break; // need more data + + UnsafeAssertClLf(4 + len); + _length = len; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + } + _prefix = RespPrefix.BulkString; + _bufferIndex += 4; + return true; + case Raw.CommonRespIndex_DoubleDigitString when available >= 5 && Unsafe.Add(ref origin, 4) == (byte)'\n': + if (comparand == RespConstants.BulkStringNull) + { + _length = 0; + _flags = RespFlags.IsScalar | RespFlags.IsNull; + } + else + { + len = ParseDoubleDigitsNonNegative(ref Unsafe.Add(ref origin, 1)); + if (available < len + 7) break; // need more data + + UnsafeAssertClLf(5 + len); + _length = len; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + } + _prefix = RespPrefix.BulkString; + _bufferIndex += 5; + return true; + case Raw.CommonRespIndex_SingleDigitArray when Unsafe.Add(ref origin, 2) == (byte)'\r': + if (comparand == RespConstants.ArrayStreaming) + { + _flags = RespFlags.IsAggregate | RespFlags.IsStreaming; + } + else + { + _flags = RespFlags.IsAggregate; + _length = ParseSingleDigit(Unsafe.Add(ref origin, 1)); + } + _prefix = RespPrefix.Array; + _bufferIndex += 4; + return true; + case Raw.CommonRespIndex_DoubleDigitArray when available >= 5 && Unsafe.Add(ref origin, 4) == (byte)'\n': + if (comparand == RespConstants.ArrayNull) + { + _flags = RespFlags.IsAggregate | RespFlags.IsNull; + } + else + { + _length = ParseDoubleDigitsNonNegative(ref Unsafe.Add(ref origin, 1)); + _flags = RespFlags.IsAggregate; + } + _prefix = RespPrefix.Array; + _bufferIndex += 5; + return true; + case Raw.CommonRespIndex_Error: + len = UnsafePastPrefix().IndexOf(RespConstants.CrlfBytes); + if (len < 0) break; // need more data + + _prefix = RespPrefix.SimpleError; + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar | RespFlags.IsError; + _length = len; + _bufferIndex++; + return true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static int ParseDoubleDigitsNonNegative(ref byte value) => (10 * ParseSingleDigit(value)) + ParseSingleDigit(Unsafe.Add(ref value, 1)); +#endif + + // no fancy vectorization, but: we can still try to find the payload the fast way in a single segment + if (_bufferIndex + 3 <= CurrentLength) // shortest possible RESP fragment is length 3 + { + var remaining = UnsafePastPrefix(); + switch (_prefix = UnsafePeekPrefix()) + { + case RespPrefix.SimpleString: + case RespPrefix.SimpleError: + case RespPrefix.Integer: + case RespPrefix.Boolean: + case RespPrefix.Double: + case RespPrefix.BigInteger: + // CRLF-terminated + _length = remaining.IndexOf(RespConstants.CrlfBytes); + if (_length < 0) break; // can't find, need more data + _bufferIndex++; // payload follows prefix directly + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + if (_prefix == RespPrefix.SimpleError) _flags |= RespFlags.IsError; + return true; + case RespPrefix.BulkError: + case RespPrefix.BulkString: + case RespPrefix.VerbatimString: + // length prefix with value payload; first, the length + switch (TryReadLengthPrefix(remaining, out _length, out int consumed)) + { + case LengthPrefixResult.Length: + // still need to valid terminating CRLF + if (remaining.Length < consumed + _length + 2) break; // need more data + UnsafeAssertClLf(1 + consumed + _length); + + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + break; + case LengthPrefixResult.Null: + _flags = RespFlags.IsScalar | RespFlags.IsNull; + break; + case LengthPrefixResult.Streaming: + _flags = RespFlags.IsScalar | RespFlags.IsStreaming; + break; + } + + if (_flags == 0) break; // will need more data to know + if (_prefix == RespPrefix.BulkError) _flags |= RespFlags.IsError; + _bufferIndex += 1 + consumed; + return true; + case RespPrefix.StreamContinuation: + // length prefix, possibly with value payload; first, the length + switch (TryReadLengthPrefix(remaining, out _length, out consumed)) + { + case LengthPrefixResult.Length when _length == 0: + // EOF, no payload + _flags = RespFlags + .IsScalar; // don't claim as streaming, we want this to count towards delta-decrement + break; + case LengthPrefixResult.Length: + // still need to valid terminating CRLF + if (remaining.Length < consumed + _length + 2) break; // need more data + UnsafeAssertClLf(1 + consumed + _length); + + _flags = RespFlags.IsScalar | RespFlags.IsInlineScalar | RespFlags.IsStreaming; + break; + case LengthPrefixResult.Null: + case LengthPrefixResult.Streaming: + ThrowProtocolFailure("Invalid streaming scalar length prefix"); + break; + } + + if (_flags == 0) break; // will need more data to know + _bufferIndex += 1 + consumed; + return true; + case RespPrefix.Array: + case RespPrefix.Set: + case RespPrefix.Map: + case RespPrefix.Push: + case RespPrefix.Attribute: + // length prefix without value payload (child values follow) + switch (TryReadLengthPrefix(remaining, out _length, out consumed)) + { + case LengthPrefixResult.Length: + _flags = RespFlags.IsAggregate; + if (AggregateLengthNeedsDoubling()) _length *= 2; + break; + case LengthPrefixResult.Null: + _flags = RespFlags.IsAggregate | RespFlags.IsNull; + break; + case LengthPrefixResult.Streaming: + _flags = RespFlags.IsAggregate | RespFlags.IsStreaming; + break; + } + + if (_flags == 0) break; // will need more data to know + if (_prefix is RespPrefix.Attribute) _flags |= RespFlags.IsAttribute; + _bufferIndex += consumed + 1; + return true; + case RespPrefix.Null: // null + // note we already checked we had 3 bytes + UnsafeAssertClLf(1); + // treat as both scalar and aggregate; this might seem weird, but makes + // sense when considering how .IsScalar and .IsAggregate are typically used, + // and that a pure null can apply to either + _flags = RespFlags.IsScalar | RespFlags.IsAggregate | RespFlags.IsNull; + _bufferIndex += 3; // skip prefix+terminator + return true; + case RespPrefix.StreamTerminator: + // note we already checked we had 3 bytes + UnsafeAssertClLf(1); + _flags = RespFlags.IsAggregate; // don't claim as streaming - this counts towards delta + _bufferIndex += 3; // skip prefix+terminator + return true; + default: + ThrowProtocolFailure("Unexpected protocol prefix: " + _prefix); + return false; + } + } + + return TryReadNextSlow(ref this); + } + + private static bool TryReadNextSlow(ref RespReader live) + { + // in the case of failure, we don't want to apply any changes, + // so we work against an isolated copy until we're happy + live.MovePastCurrent(); + RespReader isolated = live; + + int next = isolated.RawTryReadByte(); + if (next < 0) return false; + + switch (isolated._prefix = (RespPrefix)next) + { + case RespPrefix.SimpleString: + case RespPrefix.SimpleError: + case RespPrefix.Integer: + case RespPrefix.Boolean: + case RespPrefix.Double: + case RespPrefix.BigInteger: + // CRLF-terminated + if (!isolated.RawTryFindCrLf(out isolated._length)) return false; + isolated._flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + if (isolated._prefix == RespPrefix.SimpleError) isolated._flags |= RespFlags.IsError; + break; + case RespPrefix.BulkError: + case RespPrefix.BulkString: + case RespPrefix.VerbatimString: + // length prefix with value payload + switch (isolated.RawTryReadLengthPrefix()) + { + case LengthPrefixResult.Length: + // still need to valid terminating CRLF + isolated._flags = RespFlags.IsScalar | RespFlags.IsInlineScalar; + if (!isolated.RawTryAssertInlineScalarPayloadCrLf()) return false; + break; + case LengthPrefixResult.Null: + isolated._flags = RespFlags.IsScalar | RespFlags.IsNull; + break; + case LengthPrefixResult.Streaming: + isolated._flags = RespFlags.IsScalar | RespFlags.IsStreaming; + break; + case LengthPrefixResult.NeedMoreData: + return false; + default: + live.ThrowProtocolFailure("Unexpected length prefix"); + return false; + } + + if (isolated._prefix == RespPrefix.BulkError) isolated._flags |= RespFlags.IsError; + break; + case RespPrefix.Array: + case RespPrefix.Set: + case RespPrefix.Map: + case RespPrefix.Push: + case RespPrefix.Attribute: + // length prefix without value payload (child values follow) + switch (isolated.RawTryReadLengthPrefix()) + { + case LengthPrefixResult.Length: + isolated._flags = RespFlags.IsAggregate; + if (isolated.AggregateLengthNeedsDoubling()) isolated._length *= 2; + break; + case LengthPrefixResult.Null: + isolated._flags = RespFlags.IsAggregate | RespFlags.IsNull; + break; + case LengthPrefixResult.Streaming: + isolated._flags = RespFlags.IsAggregate | RespFlags.IsStreaming; + break; + case LengthPrefixResult.NeedMoreData: + return false; + default: + isolated.ThrowProtocolFailure("Unexpected length prefix"); + return false; + } + + if (isolated._prefix is RespPrefix.Attribute) isolated._flags |= RespFlags.IsAttribute; + break; + case RespPrefix.Null: // null + if (!isolated.RawAssertCrLf()) return false; + isolated._flags = RespFlags.IsScalar | RespFlags.IsNull; + break; + case RespPrefix.StreamTerminator: + if (!isolated.RawAssertCrLf()) return false; + isolated._flags = RespFlags.IsAggregate; // don't claim as streaming - this counts towards delta + break; + case RespPrefix.StreamContinuation: + // length prefix, possibly with value payload; first, the length + switch (isolated.RawTryReadLengthPrefix()) + { + case LengthPrefixResult.Length when isolated._length == 0: + // EOF, no payload + isolated._flags = + RespFlags + .IsScalar; // don't claim as streaming, we want this to count towards delta-decrement + break; + case LengthPrefixResult.Length: + // still need to valid terminating CRLF + isolated._flags = RespFlags.IsScalar | RespFlags.IsInlineScalar | RespFlags.IsStreaming; + if (!isolated.RawTryAssertInlineScalarPayloadCrLf()) return false; // need more data + break; + case LengthPrefixResult.Null: + case LengthPrefixResult.Streaming: + isolated.ThrowProtocolFailure("Invalid streaming scalar length prefix"); + break; + case LengthPrefixResult.NeedMoreData: + default: + return false; + } + + break; + default: + isolated.ThrowProtocolFailure("Unexpected protocol prefix: " + isolated._prefix); + return false; + } + + // commit the speculative changes back, and accept + live = isolated; + return true; + } + + private void AdvanceSlow(long bytes) + { + while (bytes > 0) + { + var available = CurrentLength - _bufferIndex; + if (bytes <= available) + { + _bufferIndex += (int)bytes; + return; + } + + bytes -= available; + + if (!TryMoveToNextSegment()) Throw(); + } + + [DoesNotReturn] + static void Throw() => throw new EndOfStreamException( + "Unexpected end of payload; this is unexpected because we already validated that it was available!"); + } + + private bool AggregateLengthNeedsDoubling() => _prefix is RespPrefix.Map or RespPrefix.Attribute; + + private bool TryMoveToNextSegment() + { + while (_tail is not null && _remainingTailLength > 0) + { + var memory = _tail.Memory; + _tail = _tail.Next; + if (!memory.IsEmpty) + { + var span = memory.Span; // check we can get this before mutating anything + _positionBase += CurrentLength; + if (span.Length > _remainingTailLength) + { + span = span.Slice(0, (int)_remainingTailLength); + _remainingTailLength = 0; + } + else + { + _remainingTailLength -= span.Length; + } + + SetCurrent(span); + _bufferIndex = 0; + return true; + } + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly bool IsOK() // go mad with this, because it is used so often + { + if (TryGetSpan(out var span) && span.Length == 2) + { + var u16 = Unsafe.ReadUnaligned(ref UnsafeCurrent); + return u16 == RespConstants.OKUInt16 | u16 == RespConstants.OKUInt16_LC; + } + + return IsSlow(RespConstants.OKBytes, RespConstants.OKBytes_LC); + } + + /// + /// Indicates whether the current element is a scalar with a value that matches the provided . + /// + /// The payload value to verify. + public readonly bool Is(ReadOnlySpan value) + => TryGetSpan(out var span) ? span.SequenceEqual(value) : IsSlow(value); + + /// + /// Indicates whether the current element is a scalar with a value that starts with the provided . + /// + /// The payload value to verify. + public readonly bool StartsWith(ReadOnlySpan value) + => TryGetSpan(out var span) ? span.StartsWith(value) : StartsWithSlow(value); + + /// + /// Indicates whether the current element is a scalar with a value that matches the provided . + /// + /// The payload value to verify. + public readonly bool Is(ReadOnlySpan value) + { + var bytes = RespConstants.UTF8.GetMaxByteCount(value.Length); + byte[]? oversized = null; + Span buffer = bytes <= 128 ? stackalloc byte[128] : (oversized = ArrayPool.Shared.Rent(bytes)); + bytes = RespConstants.UTF8.GetBytes(value, buffer); + bool result = Is(buffer.Slice(0, bytes)); + if (oversized is not null) ArrayPool.Shared.Return(oversized); + return result; + } + + internal readonly bool IsInlneCpuUInt32(uint value) + { + if (IsInlineScalar && _length == sizeof(uint)) + { + return CurrentAvailable >= sizeof(uint) + ? Unsafe.ReadUnaligned(ref UnsafeCurrent) == value + : SlowIsInlneCpuUInt32(value); + } + + return false; + } + + private readonly bool SlowIsInlneCpuUInt32(uint value) + { + Debug.Assert(IsInlineScalar && _length == sizeof(uint), "should be inline scalar of length 4"); + Span buffer = stackalloc byte[sizeof(uint)]; + var copy = this; + copy.RawFillBytes(buffer); + return RespConstants.UnsafeCpuUInt32(buffer) == value; + } + + /// + /// Indicates whether the current element is a scalar with a value that matches the provided . + /// + /// The payload value to verify. + public readonly bool Is(byte value) + { + if (IsInlineScalar && _length == 1 && CurrentAvailable >= 1) + { + return UnsafeCurrent == value; + } + + ReadOnlySpan span = [value]; + return IsSlow(span); + } + + private readonly bool IsSlow(ReadOnlySpan testValue0, ReadOnlySpan testValue2) + => IsSlow(testValue0) || IsSlow(testValue2); + + private readonly bool IsSlow(ReadOnlySpan testValue) + { + DemandScalar(); + if (IsNull) return false; // nothing equals null + if (TotalAvailable < testValue.Length) return false; + + if (!IsStreaming && testValue.Length != ScalarLength()) return false; + + var iterator = ScalarChunks(); + while (true) + { + if (testValue.IsEmpty) + { + // nothing left to test; if also nothing left to read, great! + return !iterator.MoveNext(); + } + + if (!iterator.MoveNext()) + { + return false; // test is longer + } + + var current = iterator.Current; + if (testValue.Length < current.Length) return false; // payload is longer + + if (!current.SequenceEqual(testValue.Slice(0, current.Length))) return false; // payload is different + + testValue = testValue.Slice(current.Length); // validated; continue + } + } + + private readonly bool StartsWithSlow(ReadOnlySpan testValue) + { + DemandScalar(); + if (IsNull) return false; // nothing equals null + if (testValue.IsEmpty) return true; // every non-null scalar starts-with empty + if (TotalAvailable < testValue.Length) return false; + + if (!IsStreaming && testValue.Length < ScalarLength()) return false; + + var iterator = ScalarChunks(); + while (true) + { + if (testValue.IsEmpty) + { + return true; + } + + if (!iterator.MoveNext()) + { + return false; // test is longer + } + + var current = iterator.Current; + if (testValue.Length <= current.Length) + { + // current fragment exhausts the test data; check it with StartsWith + return testValue.StartsWith(current); + } + + // current fragment is longer than the test data; the overlap must match exactly + if (!current.SequenceEqual(testValue.Slice(0, current.Length))) return false; // payload is different + + testValue = testValue.Slice(current.Length); // validated; continue + } + } + + /// + /// Copy the current scalar value out into the supplied , or as much as can be copied. + /// + /// The destination for the copy operation. + /// The number of bytes successfully copied. + public readonly int CopyTo(scoped Span target) + { + if (TryGetSpan(out var value)) + { + if (target.Length < value.Length) value = value.Slice(0, target.Length); + + value.CopyTo(target); + return value.Length; + } + + int totalBytes = 0; + var iterator = ScalarChunks(); + while (iterator.MoveNext()) + { + value = iterator.Current; + if (target.Length <= value.Length) + { + value.Slice(0, target.Length).CopyTo(target); + return totalBytes + target.Length; + } + + value.CopyTo(target); + target = target.Slice(value.Length); + totalBytes += value.Length; + } + + return totalBytes; + } + + /// + /// Copy the current scalar value out into the supplied , or as much as can be copied. + /// + /// The destination for the copy operation. + /// The number of bytes successfully copied. + public readonly int CopyTo(IBufferWriter target) + { + if (TryGetSpan(out var value)) + { + target.Write(value); + return value.Length; + } + + int totalBytes = 0; + var iterator = ScalarChunks(); + while (iterator.MoveNext()) + { + value = iterator.Current; + target.Write(value); + totalBytes += value.Length; + } + + return totalBytes; + } + + /// + /// Asserts that the current element is not null. + /// + public void DemandNotNull() + { + if (IsNull) Throw(); + static void Throw() => throw new InvalidOperationException("A non-null element was expected"); + } + + /// + /// Read the current element as a value. + /// + [SuppressMessage("Style", "IDE0018:Inline variable declaration", Justification = "No it can't - conditional")] + public readonly long ReadInt64() + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesInt64 + 1]); + long value; + if (!(span.Length <= RespConstants.MaxRawBytesInt64 + && Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length)) + { + ThrowFormatException(); + value = 0; + } + + return value; + } + + /// + /// Try to read the current element as a value. + /// + public readonly bool TryReadInt64(out long value) + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesInt64 + 1]); + if (span.Length <= RespConstants.MaxRawBytesInt64) + { + return Utf8Parser.TryParse(span, out value, out int bytes) & bytes == span.Length; + } + + value = 0; + return false; + } + + /// + /// Read the current element as a value. + /// + [SuppressMessage("Style", "IDE0018:Inline variable declaration", Justification = "No it can't - conditional")] + public readonly int ReadInt32() + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesInt32 + 1]); + int value; + if (!(span.Length <= RespConstants.MaxRawBytesInt32 + && Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length)) + { + ThrowFormatException(); + value = 0; + } + + return value; + } + + /// + /// Try to read the current element as a value. + /// + public readonly bool TryReadInt32(out int value) + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesInt32 + 1]); + if (span.Length <= RespConstants.MaxRawBytesInt32) + { + return Utf8Parser.TryParse(span, out value, out int bytes) & bytes == span.Length; + } + + value = 0; + return false; + } + + /// + /// Read the current element as a value. + /// + public readonly double ReadDouble() + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesNumber + 1]); + + if (span.Length <= RespConstants.MaxRawBytesNumber + && Utf8Parser.TryParse(span, out double value, out int bytes) + && bytes == span.Length) + { + return value; + } + + switch (span.Length) + { + case 3 when "inf"u8.SequenceEqual(span): + return double.PositiveInfinity; + case 3 when "nan"u8.SequenceEqual(span): + return double.NaN; + case 4 when "+inf"u8.SequenceEqual(span): // not actually mentioned in spec, but: we'll allow it + return double.PositiveInfinity; + case 4 when "-inf"u8.SequenceEqual(span): + return double.NegativeInfinity; + } + + ThrowFormatException(); + return 0; + } + + /// + /// Try to read the current element as a value. + /// + public readonly bool TryReadDouble(out double value, bool allowTokens = true) + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesNumber + 1]); + + if (Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length) + { + return true; + } + + if (allowTokens) + { + switch (span.Length) + { + case 3 when "inf"u8.SequenceEqual(span): + value = double.PositiveInfinity; + return true; + case 3 when "nan"u8.SequenceEqual(span): + value = double.NaN; + return true; + case 4 when "+inf"u8.SequenceEqual(span): // not actually mentioned in spec, but: we'll allow it + value = double.PositiveInfinity; + return true; + case 4 when "-inf"u8.SequenceEqual(span): + value = double.NegativeInfinity; + return true; + } + } + + value = 0; + return false; + } + + /// + /// Note this uses a stackalloc buffer; requesting too much may overflow the stack. + /// + internal readonly bool UnsafeTryReadShortAscii(out string value, int maxLength = 127) + { + var span = Buffer(stackalloc byte[maxLength + 1]); + value = ""; + if (span.IsEmpty) return true; + + if (span.Length <= maxLength) + { + // check for anything that looks binary or unicode + foreach (var b in span) + { + // allow [SPACE]-thru-[DEL], plus CR/LF + if (!(b < 127 & (b >= 32 | (b is 12 or 13)))) + { + return false; + } + } + + value = Encoding.UTF8.GetString(span); + return true; + } + + return false; + } + + /// + /// Read the current element as a value. + /// + [SuppressMessage("Style", "IDE0018:Inline variable declaration", Justification = "No it can't - conditional")] + public readonly decimal ReadDecimal() + { + var span = Buffer(stackalloc byte[RespConstants.MaxRawBytesNumber + 1]); + decimal value; + if (!(span.Length <= RespConstants.MaxRawBytesNumber + && Utf8Parser.TryParse(span, out value, out int bytes) + && bytes == span.Length)) + { + ThrowFormatException(); + value = 0; + } + + return value; + } + + /// + /// Read the current element as a value. + /// + public readonly bool ReadBoolean() + { + var span = Buffer(stackalloc byte[2]); + switch (span.Length) + { + case 1: + switch (span[0]) + { + case (byte)'0' when Prefix == RespPrefix.Integer: return false; + case (byte)'1' when Prefix == RespPrefix.Integer: return true; + case (byte)'f' when Prefix == RespPrefix.Boolean: return false; + case (byte)'t' when Prefix == RespPrefix.Boolean: return true; + } + + break; + case 2 when Prefix == RespPrefix.SimpleString && IsOK(): return true; + } + + ThrowFormatException(); + return false; + } + + /// + /// Parse a scalar value as an enum of type . + /// + /// The value to report if the value is not recognized. + /// The type of enum being parsed. + public readonly T ReadEnum(T unknownValue = default) where T : struct, Enum + { +#if NET + return ParseChars(static (chars, state) => Enum.TryParse(chars, true, out T value) ? value : state, unknownValue); +#else + return Enum.TryParse(ReadString(), true, out T value) ? value : unknownValue; +#endif + } + +#pragma warning disable RS0026 // unambiguous due to signature + /// + /// Reads an aggregate as an array of elements without changing the position. + /// + /// The type of data to be projected. + public TResult[]? ReadArray(Projection projection, bool scalar = false) + { + var copy = this; + return copy.ReadPastArray(projection, scalar); + } + + /// + /// Reads an aggregate as an array of elements without changing the position. + /// + /// Additional state required by the projection. + /// The type of data to be projected. + public TResult[]? ReadArray(ref TState state, Projection projection, bool scalar = false) +#if NET10_0_OR_GREATER + where TState : allows ref struct +#endif + { + var copy = this; + return copy.ReadPastArray(ref state, projection, scalar); + } + + /// + /// Reads an aggregate as an array of elements, moving past the data as a side effect. + /// + /// The type of data to be projected. + public TResult[]? ReadPastArray(Projection projection, bool scalar = false) + => ReadPastArray(ref projection, static (ref projection, ref reader) => projection(ref reader), scalar); + + /// + /// Reads an aggregate as an array of elements, moving past the data as a side effect. + /// + /// Additional state required by the projection. + /// The type of data to be projected. + public TResult[]? ReadPastArray(ref TState state, Projection projection, bool scalar = false) +#if NET10_0_OR_GREATER + where TState : allows ref struct +#endif +#pragma warning restore RS0026 + { + DemandAggregate(); + if (IsNull) return null; + var len = AggregateLength(); + if (len == 0) return []; + var result = new TResult[len]; + if (scalar) + { + // if the data to be consumed is simple (scalar), we can use + // a simpler path that doesn't need to worry about RESP subtrees + for (int i = 0; i < result.Length; i++) + { + MoveNextScalar(); + result[i] = projection(ref state, ref this); + } + } + else + { + var agg = AggregateChildren(); + agg.FillAll(result, ref state, projection); + agg.MovePast(out this); + } + + return result; + } + + public TResult[]? ReadPairArray( + Projection first, + Projection second, + Func combine, + bool scalar = true) + { + DemandAggregate(); + if (IsNull) return null; + int sourceLength = AggregateLength(); + if (sourceLength is 0 or 1) return []; + var result = new TResult[sourceLength >> 1]; + if (scalar) + { + // if the data to be consumed is simple (scalar), we can use + // a simpler path that doesn't need to worry about RESP subtrees + for (int i = 0; i < result.Length; i++) + { + MoveNextScalar(); + var x = first(ref this); + MoveNextScalar(); + var y = second(ref this); + result[i] = combine(x, y); + } + // if we have an odd number of source elements, skip the last one + if ((sourceLength & 1) != 0) MoveNextScalar(); + } + else + { + var agg = AggregateChildren(); + agg.FillAll(result, first, second, combine); + agg.MovePast(out this); + } + return result; + } + internal TResult[]? ReadLeasedPairArray( + Projection first, + Projection second, + Func combine, + out int count, + bool scalar = true) + { + DemandAggregate(); + if (IsNull) + { + count = 0; + return null; + } + int sourceLength = AggregateLength(); + count = sourceLength >> 1; + if (count is 0) return []; + + var oversized = ArrayPool.Shared.Rent(count); + var result = oversized.AsSpan(0, count); + if (scalar) + { + // if the data to be consumed is simple (scalar), we can use + // a simpler path that doesn't need to worry about RESP subtrees + for (int i = 0; i < result.Length; i++) + { + MoveNextScalar(); + var x = first(ref this); + MoveNextScalar(); + var y = second(ref this); + result[i] = combine(x, y); + } + // if we have an odd number of source elements, skip the last one + if ((sourceLength & 1) != 0) MoveNextScalar(); + } + else + { + var agg = AggregateChildren(); + agg.FillAll(result, first, second, combine); + agg.MovePast(out this); + } + return oversized; + } +} diff --git a/src/RESPite/Messages/RespScanState.cs b/src/RESPite/Messages/RespScanState.cs new file mode 100644 index 000000000..37cd3f8b6 --- /dev/null +++ b/src/RESPite/Messages/RespScanState.cs @@ -0,0 +1,163 @@ +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace RESPite.Messages; + +/// +/// Holds state used for RESP frame parsing, i.e. detecting the RESP for an entire top-level message. +/// +[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] +public struct RespScanState +{ + /* + The key point of ScanState is to skim over a RESP stream with minimal frame processing, to find the + end of a single top-level RESP message. We start by expecting 1 message, and then just read, with the + rules that the end of a message subtracts one, and aggregates add N. Streaming scalars apply zero offset + until the scalar stream terminator. Attributes also apply zero offset. + Note that streaming aggregates change the rules - when at least one streaming aggregate is in effect, + no offsets are applied until we get back out of the outermost streaming aggregate - we achieve this + by simply counting the streaming aggregate depth, which is usually zero. + Note that in reality streaming (scalar and aggregates) and attributes are non-existent; in addition + to being specific to RESP3, no known server currently implements these parts of the RESP3 specification, + so everything here is theoretical, but: works according to the spec. + */ + private int _delta; // when this becomes -1, we have fully read a top-level message; + private ushort _streamingAggregateDepth; + private RespPrefix _prefix; + + public RespPrefix Prefix => _prefix; + + private long _totalBytes; +#if DEBUG + private int _elementCount; + + /// + public override string ToString() => $"{_prefix}, consumed: {_totalBytes} bytes, {_elementCount} nodes, complete: {IsComplete} ({_delta + 1} outstanding)"; +#else + /// + public override string ToString() => _prefix.ToString(); +#endif + + /// + public override bool Equals([NotNullWhen(true)] object? obj) => throw new NotSupportedException(); + + /// + public override int GetHashCode() => throw new NotSupportedException(); + + /// + /// Gets whether an entire top-level RESP message has been consumed. + /// + public bool IsComplete => _delta == -1; + + /// + /// Gets the total length of the payload read (or read so far, if it is not yet complete); this combines payloads from multiple + /// TryRead operations. + /// + public long TotalBytes => _totalBytes; + + // used when spotting common replies - we entirely bypass the usual reader/delta mechanism + internal void SetComplete(int totalBytes, RespPrefix prefix) + { + _totalBytes = totalBytes; + _delta = -1; + _prefix = prefix; +#if DEBUG + _elementCount = 1; +#endif + } + + /// + /// The amount of data, in bytes, to read before attempting to read the next frame. + /// + public const int MinBytes = 3; // minimum legal RESP frame is: _\r\n + + /// + /// Create a new value that can parse the supplied node (and subtree). + /// + internal RespScanState(in RespReader reader) + { + Debug.Assert(reader.Prefix != RespPrefix.None, "missing RESP prefix"); + _totalBytes = 0; + _delta = reader.GetInitialScanCount(out _streamingAggregateDepth); + } + + /// + /// Scan as far as possible, stopping when an entire top-level RESP message has been consumed or the data is exhausted. + /// + /// True if a top-level RESP message has been consumed. + public bool TryRead(ref RespReader reader, out long bytesRead) + { + bytesRead = ReadCore(ref reader, reader.BytesConsumed); + return IsComplete; + } + + /// + /// Scan as far as possible, stopping when an entire top-level RESP message has been consumed or the data is exhausted. + /// + /// True if a top-level RESP message has been consumed. + public bool TryRead(ReadOnlySpan value, out int bytesRead) + { + var reader = new RespReader(value); + bytesRead = (int)ReadCore(ref reader); + return IsComplete; + } + + /// + /// Scan as far as possible, stopping when an entire top-level RESP message has been consumed or the data is exhausted. + /// + /// True if a top-level RESP message has been consumed. + public bool TryRead(in ReadOnlySequence value, out long bytesRead) + { + var reader = new RespReader(in value); + bytesRead = ReadCore(ref reader); + return IsComplete; + } + + /// + /// Scan as far as possible, stopping when an entire top-level RESP message has been consumed or the data is exhausted. + /// + /// The number of bytes consumed in this operation. + private long ReadCore(ref RespReader reader, long startOffset = 0) + { +#pragma warning disable CS0618 // avoid TryReadNext unless you know what you're doing + while (_delta >= 0 && reader.TryReadNext()) +#pragma warning restore CS0618 + { +#if DEBUG + _elementCount++; +#endif + if (!reader.IsAttribute & _prefix == RespPrefix.None) + { + _prefix = reader.Prefix; + } + + if (reader.IsNonNullAggregate) ApplyAggregateRules(ref reader); + + if (_streamingAggregateDepth == 0) _delta += reader.Delta(); + } + + var bytesRead = reader.BytesConsumed - startOffset; + _totalBytes += bytesRead; + return bytesRead; + } + + private void ApplyAggregateRules(ref RespReader reader) + { + Debug.Assert(reader.IsAggregate, "RESP aggregate expected"); + if (reader.IsStreaming) + { + // entering an aggregate stream + if (_streamingAggregateDepth == ushort.MaxValue) ThrowTooDeep(); + _streamingAggregateDepth++; + } + else if (reader.Prefix == RespPrefix.StreamTerminator) + { + // exiting an aggregate stream + if (_streamingAggregateDepth == 0) ThrowUnexpectedTerminator(); + _streamingAggregateDepth--; + } + static void ThrowTooDeep() => throw new InvalidOperationException("Maximum streaming aggregate depth exceeded."); + static void ThrowUnexpectedTerminator() => throw new InvalidOperationException("Unexpected streaming aggregate terminator."); + } +} diff --git a/src/RESPite/PublicAPI/PublicAPI.Shipped.txt b/src/RESPite/PublicAPI/PublicAPI.Shipped.txt new file mode 100644 index 000000000..ab058de62 --- /dev/null +++ b/src/RESPite/PublicAPI/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..9ce6685bc --- /dev/null +++ b/src/RESPite/PublicAPI/PublicAPI.Unshipped.txt @@ -0,0 +1,214 @@ +#nullable enable +[SER004]const RESPite.Buffers.CycleBuffer.GetAnything = 0 -> int +[SER004]const RESPite.Buffers.CycleBuffer.GetFullPagesOnly = -1 -> int +[SER004]override RESPite.AsciiHash.Equals(object? other) -> bool +[SER004]override RESPite.AsciiHash.GetHashCode() -> int +[SER004]override RESPite.AsciiHash.ToString() -> string! +[SER004]RESPite.AsciiHash +[SER004]RESPite.AsciiHash.AsciiHash() -> void +[SER004]RESPite.AsciiHash.AsciiHash(byte[]! arr) -> void +[SER004]RESPite.AsciiHash.AsciiHash(byte[]! arr, int index, int length) -> void +[SER004]RESPite.AsciiHash.AsciiHash(System.ReadOnlySpan value) -> void +[SER004]RESPite.AsciiHash.BufferLength.get -> int +[SER004]RESPite.AsciiHash.Equals(in RESPite.AsciiHash other) -> bool +[SER004]RESPite.AsciiHash.IsCI(System.ReadOnlySpan value) -> bool +[SER004]RESPite.AsciiHash.IsCS(System.ReadOnlySpan value) -> bool +[SER004]RESPite.AsciiHash.Length.get -> int +[SER004]RESPite.AsciiHash.Span.get -> System.ReadOnlySpan +[SER004]RESPite.AsciiHashAttribute +[SER004]RESPite.AsciiHashAttribute.AsciiHashAttribute(string! token = "") -> void +[SER004]RESPite.AsciiHashAttribute.CaseSensitive.get -> bool +[SER004]RESPite.AsciiHashAttribute.CaseSensitive.set -> void +[SER004]RESPite.AsciiHashAttribute.Token.get -> string! +[SER004]RESPite.AsciiHash.AsciiHash(string? value) -> void +[SER004]RESPite.AsciiHash.IsEmpty.get -> bool +[SER004]RESPite.Buffers.CycleBuffer +[SER004]RESPite.Buffers.CycleBuffer.Commit(int count) -> void +[SER004]RESPite.Buffers.CycleBuffer.CommittedIsEmpty.get -> bool +[SER004]RESPite.Buffers.CycleBuffer.CycleBuffer() -> void +[SER004]RESPite.Buffers.CycleBuffer.DiscardCommitted(int count) -> void +[SER004]RESPite.Buffers.CycleBuffer.DiscardCommitted(long count) -> void +[SER004]RESPite.Buffers.CycleBuffer.GetAllCommitted() -> System.Buffers.ReadOnlySequence +[SER004]RESPite.Buffers.CycleBuffer.GetCommittedLength() -> long +[SER004]RESPite.Buffers.CycleBuffer.GetUncommittedMemory(int hint = 0) -> System.Memory +[SER004]RESPite.Buffers.CycleBuffer.GetUncommittedSpan(int hint = 0) -> System.Span +[SER004]RESPite.Buffers.CycleBuffer.PageSize.get -> int +[SER004]RESPite.Buffers.CycleBuffer.Pool.get -> System.Buffers.MemoryPool! +[SER004]RESPite.Buffers.CycleBuffer.Release() -> void +[SER004]RESPite.Buffers.CycleBuffer.TryGetCommitted(out System.ReadOnlySpan span) -> bool +[SER004]RESPite.Buffers.CycleBuffer.TryGetFirstCommittedMemory(int minBytes, out System.ReadOnlyMemory memory) -> bool +[SER004]RESPite.Buffers.CycleBuffer.TryGetFirstCommittedSpan(int minBytes, out System.ReadOnlySpan span) -> bool +[SER004]RESPite.Buffers.CycleBuffer.UncommittedAvailable.get -> int +[SER004]RESPite.Buffers.CycleBuffer.Write(in System.Buffers.ReadOnlySequence value) -> void +[SER004]RESPite.Buffers.CycleBuffer.Write(System.ReadOnlySpan value) -> void +[SER004]RESPite.Buffers.ICycleBufferCallback +[SER004]RESPite.Buffers.ICycleBufferCallback.PageComplete() -> void +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.FillAll(scoped System.Span target, RESPite.Messages.RespReader.Projection! projection) -> void +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.FillAll(scoped System.Span target, ref TState state, RESPite.Messages.RespReader.Projection! first, RESPite.Messages.RespReader.Projection! second, System.Func! combine) -> void +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.FillAll(scoped System.Span target, ref TState state, RESPite.Messages.RespReader.Projection! projection) -> void +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.MoveNextRaw() -> bool +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.MoveNextRaw(RESPite.Messages.RespAttributeReader! respAttributeReader, ref T attributes) -> bool +[SER004]RESPite.Messages.RespReader.AggregateIsEmpty() -> bool +[SER004]RESPite.Messages.RespReader.AggregateLengthIs(int count) -> bool +[SER004]RESPite.Messages.RespReader.Clone() -> RESPite.Messages.RespReader +[SER004]RESPite.Messages.RespReader.FillAll(scoped System.Span target, ref TState state, RESPite.Messages.RespReader.Projection! projection) -> void +[SER004]RESPite.Messages.RespReader.Projection +[SER004]RESPite.Messages.RespReader.ReadArray(ref TState state, RESPite.Messages.RespReader.Projection! projection, bool scalar = false) -> TResult[]? +[SER004]RESPite.Messages.RespReader.ReadPastArray(ref TState state, RESPite.Messages.RespReader.Projection! projection, bool scalar = false) -> TResult[]? +[SER004]RESPite.Messages.RespReader.ScalarParser +[SER004]RESPite.Messages.RespReader.TryParseScalar(delegate*, out T, bool> parser, out T value) -> bool +[SER004]RESPite.Messages.RespReader.TryParseScalar(RESPite.Messages.RespReader.ScalarParser! parser, out T value) -> bool +[SER004]static RESPite.AsciiHash.CaseInsensitiveEqualityComparer.get -> System.Collections.Generic.IEqualityComparer! +[SER004]static RESPite.AsciiHash.CaseSensitiveEqualityComparer.get -> System.Collections.Generic.IEqualityComparer! +[SER004]static RESPite.AsciiHash.EqualsCI(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool +[SER004]static RESPite.AsciiHash.EqualsCI(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool +[SER004]static RESPite.AsciiHash.EqualsCS(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool +[SER004]static RESPite.AsciiHash.EqualsCS(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool +[SER004]static RESPite.AsciiHash.Hash(scoped System.ReadOnlySpan value, out long cs, out long uc) -> void +[SER004]static RESPite.AsciiHash.Hash(scoped System.ReadOnlySpan value, out long cs0, out long uc0, out long cs1, out long uc1) -> void +[SER004]static RESPite.AsciiHash.Hash(scoped System.ReadOnlySpan value, out long cs, out long uc) -> void +[SER004]static RESPite.AsciiHash.Hash(scoped System.ReadOnlySpan value, out long cs0, out long uc0, out long cs1, out long uc1) -> void +[SER004]static RESPite.AsciiHash.HashCS(scoped System.ReadOnlySpan value) -> long +[SER004]static RESPite.AsciiHash.HashCS(scoped System.ReadOnlySpan value, out long cs0, out long cs1) -> void +[SER004]static RESPite.AsciiHash.HashCS(scoped System.ReadOnlySpan value) -> long +[SER004]static RESPite.AsciiHash.HashCS(scoped System.ReadOnlySpan value, out long cs0, out long cs1) -> void +[SER004]static RESPite.AsciiHash.HashUC(scoped System.ReadOnlySpan value) -> long +[SER004]static RESPite.AsciiHash.HashUC(scoped System.ReadOnlySpan value, out long cs0, out long cs1) -> void +[SER004]static RESPite.AsciiHash.HashUC(scoped System.ReadOnlySpan value) -> long +[SER004]static RESPite.AsciiHash.HashUC(scoped System.ReadOnlySpan value, out long cs0, out long cs1) -> void +[SER004]static RESPite.AsciiHash.SequenceEqualsCI(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool +[SER004]static RESPite.AsciiHash.SequenceEqualsCI(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool +[SER004]static RESPite.AsciiHash.SequenceEqualsCS(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool +[SER004]static RESPite.AsciiHash.SequenceEqualsCS(System.ReadOnlySpan first, System.ReadOnlySpan second) -> bool +[SER004]static RESPite.AsciiHash.ToLower(System.Span span) -> void +[SER004]static RESPite.AsciiHash.ToUpper(System.Span span) -> void +[SER004]const RESPite.Messages.RespScanState.MinBytes = 3 -> int +[SER004]override RESPite.Messages.RespScanState.Equals(object? obj) -> bool +[SER004]override RESPite.Messages.RespScanState.GetHashCode() -> int +[SER004]override RESPite.Messages.RespScanState.ToString() -> string! +[SER004]RESPite.Messages.RespAttributeReader +[SER004]RESPite.Messages.RespAttributeReader.RespAttributeReader() -> void +[SER004]RESPite.Messages.RespFrameScanner +[SER004]RESPite.Messages.RespFrameScanner.TryRead(ref RESPite.Messages.RespScanState state, in System.Buffers.ReadOnlySequence data) -> System.Buffers.OperationStatus +[SER004]RESPite.Messages.RespFrameScanner.TryRead(ref RESPite.Messages.RespScanState state, System.ReadOnlySpan data) -> System.Buffers.OperationStatus +[SER004]RESPite.Messages.RespFrameScanner.ValidateRequest(in System.Buffers.ReadOnlySequence message) -> void +[SER004]RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.Array = 42 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.Attribute = 124 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.BigInteger = 40 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.Boolean = 35 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.BulkError = 33 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.BulkString = 36 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.Double = 44 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.Integer = 58 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.Map = 37 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.None = 0 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.Null = 95 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.Push = 62 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.Set = 126 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.SimpleError = 45 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.SimpleString = 43 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.StreamContinuation = 59 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.StreamTerminator = 46 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespPrefix.VerbatimString = 61 -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespReader +[SER004]RESPite.Messages.RespReader.AggregateChildren() -> RESPite.Messages.RespReader.AggregateEnumerator +[SER004]RESPite.Messages.RespReader.AggregateEnumerator +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.AggregateEnumerator() -> void +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.AggregateEnumerator(scoped in RESPite.Messages.RespReader reader) -> void +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.Current.get -> RESPite.Messages.RespReader +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.DemandNext() -> void +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.FillAll(scoped System.Span target, RESPite.Messages.RespReader.Projection! first, RESPite.Messages.RespReader.Projection! second, System.Func! combine) -> void +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.GetEnumerator() -> RESPite.Messages.RespReader.AggregateEnumerator +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.MoveNext() -> bool +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.MoveNext(RESPite.Messages.RespPrefix prefix) -> bool +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.MoveNext(RESPite.Messages.RespPrefix prefix, RESPite.Messages.RespAttributeReader! respAttributeReader, ref T attributes) -> bool +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.MovePast(out RESPite.Messages.RespReader reader) -> void +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.ReadOne(RESPite.Messages.RespReader.Projection! projection) -> T +[SER004]RESPite.Messages.RespReader.AggregateEnumerator.Value -> RESPite.Messages.RespReader +[SER004]RESPite.Messages.RespReader.AggregateLength() -> int +[SER004]RESPite.Messages.RespReader.BytesConsumed.get -> long +[SER004]RESPite.Messages.RespReader.CopyTo(scoped System.Span target) -> int +[SER004]RESPite.Messages.RespReader.CopyTo(System.Buffers.IBufferWriter! target) -> int +[SER004]RESPite.Messages.RespReader.DemandAggregate() -> void +[SER004]RESPite.Messages.RespReader.DemandEnd() -> void +[SER004]RESPite.Messages.RespReader.DemandNotNull() -> void +[SER004]RESPite.Messages.RespReader.DemandScalar() -> void +[SER004]RESPite.Messages.RespReader.FillAll(scoped System.Span target, RESPite.Messages.RespReader.Projection! projection) -> void +[SER004]RESPite.Messages.RespReader.Is(byte value) -> bool +[SER004]RESPite.Messages.RespReader.Is(System.ReadOnlySpan value) -> bool +[SER004]RESPite.Messages.RespReader.Is(System.ReadOnlySpan value) -> bool +[SER004]RESPite.Messages.RespReader.IsAggregate.get -> bool +[SER004]RESPite.Messages.RespReader.IsAttribute.get -> bool +[SER004]RESPite.Messages.RespReader.IsError.get -> bool +[SER004]RESPite.Messages.RespReader.IsNull.get -> bool +[SER004]RESPite.Messages.RespReader.IsScalar.get -> bool +[SER004]RESPite.Messages.RespReader.IsStreaming.get -> bool +[SER004]RESPite.Messages.RespReader.MoveNext() -> void +[SER004]RESPite.Messages.RespReader.MoveNext(RESPite.Messages.RespPrefix prefix) -> void +[SER004]RESPite.Messages.RespReader.MoveNext(RESPite.Messages.RespAttributeReader! respAttributeReader, ref T attributes) -> void +[SER004]RESPite.Messages.RespReader.MoveNext(RESPite.Messages.RespPrefix prefix, RESPite.Messages.RespAttributeReader! respAttributeReader, ref T attributes) -> void +[SER004]RESPite.Messages.RespReader.MoveNextAggregate() -> void +[SER004]RESPite.Messages.RespReader.MoveNextScalar() -> void +[SER004]RESPite.Messages.RespReader.Prefix.get -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespReader.Projection +[SER004]RESPite.Messages.RespReader.ProtocolBytesRemaining.get -> long +[SER004]RESPite.Messages.RespReader.ReadArray(RESPite.Messages.RespReader.Projection! projection, bool scalar = false) -> TResult[]? +[SER004]RESPite.Messages.RespReader.ReadBoolean() -> bool +[SER004]RESPite.Messages.RespReader.ReadByteArray() -> byte[]? +[SER004]RESPite.Messages.RespReader.ReadDecimal() -> decimal +[SER004]RESPite.Messages.RespReader.ReadDouble() -> double +[SER004]RESPite.Messages.RespReader.ReadEnum(T unknownValue = default(T)) -> T +[SER004]RESPite.Messages.RespReader.ReadInt32() -> int +[SER004]RESPite.Messages.RespReader.ReadInt64() -> long +[SER004]RESPite.Messages.RespReader.ReadPairArray(RESPite.Messages.RespReader.Projection! first, RESPite.Messages.RespReader.Projection! second, System.Func! combine, bool scalar = true) -> TResult[]? +[SER004]RESPite.Messages.RespReader.ReadPastArray(RESPite.Messages.RespReader.Projection! projection, bool scalar = false) -> TResult[]? +[SER004]RESPite.Messages.RespReader.ReadString() -> string? +[SER004]RESPite.Messages.RespReader.ReadString(out string! prefix) -> string? +[SER004]RESPite.Messages.RespReader.RespReader() -> void +[SER004]RESPite.Messages.RespReader.RespReader(scoped in System.Buffers.ReadOnlySequence value) -> void +[SER004]RESPite.Messages.RespReader.RespReader(System.ReadOnlySpan value) -> void +[SER004]RESPite.Messages.RespReader.ScalarChunks() -> RESPite.Messages.RespReader.ScalarEnumerator +[SER004]RESPite.Messages.RespReader.ScalarEnumerator +[SER004]RESPite.Messages.RespReader.ScalarEnumerator.Current.get -> System.ReadOnlySpan +[SER004]RESPite.Messages.RespReader.ScalarEnumerator.CurrentLength.get -> int +[SER004]RESPite.Messages.RespReader.ScalarEnumerator.GetEnumerator() -> RESPite.Messages.RespReader.ScalarEnumerator +[SER004]RESPite.Messages.RespReader.ScalarEnumerator.MoveNext() -> bool +[SER004]RESPite.Messages.RespReader.ScalarEnumerator.MovePast(out RESPite.Messages.RespReader reader) -> void +[SER004]RESPite.Messages.RespReader.ScalarEnumerator.ScalarEnumerator() -> void +[SER004]RESPite.Messages.RespReader.ScalarEnumerator.ScalarEnumerator(scoped in RESPite.Messages.RespReader reader) -> void +[SER004]RESPite.Messages.RespReader.ScalarIsEmpty() -> bool +[SER004]RESPite.Messages.RespReader.ScalarLength() -> int +[SER004]RESPite.Messages.RespReader.ScalarLengthIs(int count) -> bool +[SER004]RESPite.Messages.RespReader.ScalarLongLength() -> long +[SER004]RESPite.Messages.RespReader.SkipChildren() -> void +[SER004]RESPite.Messages.RespReader.StartsWith(System.ReadOnlySpan value) -> bool +[SER004]RESPite.Messages.RespReader.TryGetSpan(out System.ReadOnlySpan value) -> bool +[SER004]RESPite.Messages.RespReader.TryMoveNext() -> bool +[SER004]RESPite.Messages.RespReader.TryMoveNext(bool checkError) -> bool +[SER004]RESPite.Messages.RespReader.TryMoveNext(RESPite.Messages.RespPrefix prefix) -> bool +[SER004]RESPite.Messages.RespReader.TryMoveNext(RESPite.Messages.RespAttributeReader! respAttributeReader, ref T attributes) -> bool +[SER004]RESPite.Messages.RespReader.TryReadDouble(out double value, bool allowTokens = true) -> bool +[SER004]RESPite.Messages.RespReader.TryReadInt32(out int value) -> bool +[SER004]RESPite.Messages.RespReader.TryReadInt64(out long value) -> bool +[SER004]RESPite.Messages.RespReader.TryReadNext() -> bool +[SER004]RESPite.Messages.RespScanState +[SER004]RESPite.Messages.RespScanState.IsComplete.get -> bool +[SER004]RESPite.Messages.RespScanState.Prefix.get -> RESPite.Messages.RespPrefix +[SER004]RESPite.Messages.RespScanState.RespScanState() -> void +[SER004]RESPite.Messages.RespScanState.TotalBytes.get -> long +[SER004]RESPite.Messages.RespScanState.TryRead(in System.Buffers.ReadOnlySequence value, out long bytesRead) -> bool +[SER004]RESPite.Messages.RespScanState.TryRead(ref RESPite.Messages.RespReader reader, out long bytesRead) -> bool +[SER004]RESPite.Messages.RespScanState.TryRead(System.ReadOnlySpan value, out int bytesRead) -> bool +[SER004]RESPite.RespException +[SER004]RESPite.RespException.RespException(string! message) -> void +[SER004]static RESPite.Buffers.CycleBuffer.Create(System.Buffers.MemoryPool? pool = null, int pageSize = 8192, RESPite.Buffers.ICycleBufferCallback? callback = null) -> RESPite.Buffers.CycleBuffer +[SER004]static RESPite.Messages.RespFrameScanner.Default.get -> RESPite.Messages.RespFrameScanner! +[SER004]static RESPite.Messages.RespFrameScanner.Subscription.get -> RESPite.Messages.RespFrameScanner! +[SER004]virtual RESPite.Messages.RespAttributeReader.Read(ref RESPite.Messages.RespReader reader, ref T value) -> void +[SER004]virtual RESPite.Messages.RespAttributeReader.ReadKeyValuePair(scoped System.ReadOnlySpan key, ref RESPite.Messages.RespReader reader, ref T value) -> bool +[SER004]virtual RESPite.Messages.RespAttributeReader.ReadKeyValuePairs(ref RESPite.Messages.RespReader reader, ref T value) -> int +[SER004]virtual RESPite.Messages.RespReader.Projection.Invoke(ref RESPite.Messages.RespReader value) -> T +[SER004]virtual RESPite.Messages.RespReader.Projection.Invoke(ref TState state, ref RESPite.Messages.RespReader value) -> TResult +[SER004]virtual RESPite.Messages.RespReader.ScalarParser.Invoke(scoped System.ReadOnlySpan value, out TValue result) -> bool +[SER004]RESPite.Messages.RespReader.Serialize() -> byte[]! \ No newline at end of file diff --git a/src/RESPite/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/RESPite/PublicAPI/net8.0/PublicAPI.Shipped.txt new file mode 100644 index 000000000..ab058de62 --- /dev/null +++ b/src/RESPite/PublicAPI/net8.0/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/RESPite/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/RESPite/PublicAPI/net8.0/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..c43af2e5e --- /dev/null +++ b/src/RESPite/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +#nullable enable +[SER004]RESPite.Messages.RespReader.ParseBytes(System.IFormatProvider? formatProvider = null) -> T +[SER004]RESPite.Messages.RespReader.ParseChars(System.IFormatProvider? formatProvider = null) -> T \ No newline at end of file diff --git a/src/RESPite/RESPite.csproj b/src/RESPite/RESPite.csproj new file mode 100644 index 000000000..fef03625b --- /dev/null +++ b/src/RESPite/RESPite.csproj @@ -0,0 +1,51 @@ + + + + true + net461;netstandard2.0;net472;net6.0;net8.0;net10.0 + enable + enable + false + 2025 - $([System.DateTime]::Now.Year) Marc Gravell + readme.md + + + + + + + + + + + + + + + + RespReader.cs + + + BlockBufferSerializer.cs + + + BlockBufferSerializer.cs + + + BlockBufferSerializer.cs + + + + + + + + + + + + + + + + diff --git a/src/RESPite/RespException.cs b/src/RESPite/RespException.cs new file mode 100644 index 000000000..6b5fd7c72 --- /dev/null +++ b/src/RESPite/RespException.cs @@ -0,0 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + +namespace RESPite; + +/// +/// Represents a RESP error message. +/// +[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] +public sealed class RespException(string message) : Exception(message) +{ +} diff --git a/src/RESPite/Shared/AsciiHash.Comparers.cs b/src/RESPite/Shared/AsciiHash.Comparers.cs new file mode 100644 index 000000000..7b69a15a4 --- /dev/null +++ b/src/RESPite/Shared/AsciiHash.Comparers.cs @@ -0,0 +1,37 @@ +namespace RESPite; + +public readonly partial struct AsciiHash +{ + public static IEqualityComparer CaseSensitiveEqualityComparer => CaseSensitiveComparer.Instance; + public static IEqualityComparer CaseInsensitiveEqualityComparer => CaseInsensitiveComparer.Instance; + + private sealed class CaseSensitiveComparer : IEqualityComparer + { + private CaseSensitiveComparer() { } + public static readonly CaseSensitiveComparer Instance = new(); + + public bool Equals(AsciiHash x, AsciiHash y) + { + var len = x.Length; + return (len == y.Length & x._hashCS == y._hashCS) + && (len <= MaxBytesHashed || x.Span.SequenceEqual(y.Span)); + } + + public int GetHashCode(AsciiHash obj) => obj._hashCS.GetHashCode(); + } + + private sealed class CaseInsensitiveComparer : IEqualityComparer + { + private CaseInsensitiveComparer() { } + public static readonly CaseInsensitiveComparer Instance = new(); + + public bool Equals(AsciiHash x, AsciiHash y) + { + var len = x.Length; + return (len == y.Length & x._hashUC == y._hashUC) + && (len <= MaxBytesHashed || SequenceEqualsCI(x.Span, y.Span)); + } + + public int GetHashCode(AsciiHash obj) => obj._hashUC.GetHashCode(); + } +} diff --git a/src/RESPite/Shared/AsciiHash.Instance.cs b/src/RESPite/Shared/AsciiHash.Instance.cs new file mode 100644 index 000000000..53db4ff27 --- /dev/null +++ b/src/RESPite/Shared/AsciiHash.Instance.cs @@ -0,0 +1,73 @@ +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace RESPite; + +public readonly partial struct AsciiHash : IEquatable +{ + // ReSharper disable InconsistentNaming + private readonly long _hashCS, _hashUC; + // ReSharper restore InconsistentNaming + private readonly int _index, _length; + private readonly byte[] _arr; + + public int Length => _length; + + /// + /// The optimal buffer length (with padding) to use for this value. + /// + public int BufferLength => (Length + 1 + 7) & ~7; // an extra byte, then round up to word-size + + public ReadOnlySpan Span => new(_arr ?? [], _index, _length); + public bool IsEmpty => Length == 0; + + public AsciiHash(ReadOnlySpan value) : this(value.ToArray(), 0, value.Length) { } + public AsciiHash(string? value) : this(value is null ? [] : Encoding.ASCII.GetBytes(value)) { } + + /// + public override int GetHashCode() => _hashCS.GetHashCode(); + + /// + public override string ToString() => _length == 0 ? "" : Encoding.ASCII.GetString(_arr, _index, _length); + + /// + public override bool Equals(object? other) => other is AsciiHash hash && Equals(hash); + + /// + public bool Equals(in AsciiHash other) + { + return (_length == other.Length & _hashCS == other._hashCS) + && (_length <= MaxBytesHashed || Span.SequenceEqual(other.Span)); + } + + bool IEquatable.Equals(AsciiHash other) => Equals(other); + + public AsciiHash(byte[] arr) : this(arr, 0, -1) { } + + public AsciiHash(byte[] arr, int index, int length) + { + _arr = arr ?? []; + _index = index; + _length = length < 0 ? (_arr.Length - index) : length; + + var span = new ReadOnlySpan(_arr, _index, _length); + Hash(span, out _hashCS, out _hashUC); + } + + public bool IsCS(ReadOnlySpan value) + { + var cs = HashCS(value); + var len = _length; + if (cs != _hashCS | value.Length != len) return false; + return len <= MaxBytesHashed || Span.SequenceEqual(value); + } + + public bool IsCI(ReadOnlySpan value) + { + var uc = HashUC(value); + var len = _length; + if (uc != _hashUC | value.Length != len) return false; + return len <= MaxBytesHashed || SequenceEqualsCI(Span, value); + } +} diff --git a/src/RESPite/Shared/AsciiHash.Public.cs b/src/RESPite/Shared/AsciiHash.Public.cs new file mode 100644 index 000000000..dd31cb415 --- /dev/null +++ b/src/RESPite/Shared/AsciiHash.Public.cs @@ -0,0 +1,10 @@ +namespace RESPite; + +// in the shared file, these are declared without accessibility modifiers +public sealed partial class AsciiHashAttribute +{ +} + +public readonly partial struct AsciiHash +{ +} diff --git a/src/RESPite/Shared/AsciiHash.cs b/src/RESPite/Shared/AsciiHash.cs new file mode 100644 index 000000000..37b3c5734 --- /dev/null +++ b/src/RESPite/Shared/AsciiHash.cs @@ -0,0 +1,294 @@ +using System; +using System.Buffers.Binary; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace RESPite; + +#pragma warning disable SA1205 // deliberately omit accessibility - see AsciiHash.Public.cs + +/// +/// This type is intended to provide fast hashing functions for small ASCII strings, for example well-known +/// RESP literals that are usually identifiable by their length and initial bytes; it is not intended +/// for general purpose hashing, and the behavior is undefined for non-ASCII literals. +/// All matches must also perform a sequence equality check. +/// +/// See HastHashGenerator.md for more information and intended usage. +[AttributeUsage( + AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Enum, + AllowMultiple = false, + Inherited = false)] +[Conditional("DEBUG")] // evaporate in release +[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] +sealed partial class AsciiHashAttribute(string token = "") : Attribute +{ + /// + /// The token expected when parsing data, if different from the implied value. The implied + /// value is the name, replacing underscores for hyphens, so: 'a_b' becomes 'a-b'. + /// + public string Token => token; + + /// + /// Indicates whether a parse operation is case-sensitive. Not used in other contexts. + /// + public bool CaseSensitive { get; set; } = true; +} + +// note: instance members are in AsciiHash.Instance.cs. +[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)] +readonly partial struct AsciiHash +{ + /// + /// In-place ASCII upper-case conversion. + /// + public static void ToUpper(Span span) + { + foreach (ref var b in span) + { + if (b >= 'a' && b <= 'z') + b = (byte)(b & ~0x20); + } + } + + /// + /// In-place ASCII lower-case conversion. + /// + public static void ToLower(Span span) + { + foreach (ref var b in span) + { + if (b >= 'a' && b <= 'z') + b |= (byte)(b & ~0x20); + } + } + + internal const int MaxBytesHashed = sizeof(long); + + public static bool EqualsCS(ReadOnlySpan first, ReadOnlySpan second) + { + var len = first.Length; + if (len != second.Length) return false; + // for very short values, the CS hash performs CS equality + return len <= MaxBytesHashed ? HashCS(first) == HashCS(second) : first.SequenceEqual(second); + } + + public static bool SequenceEqualsCS(ReadOnlySpan first, ReadOnlySpan second) + => first.SequenceEqual(second); + + public static bool EqualsCI(ReadOnlySpan first, ReadOnlySpan second) + { + var len = first.Length; + if (len != second.Length) return false; + // for very short values, the UC hash performs CI equality + return len <= MaxBytesHashed ? HashUC(first) == HashUC(second) : SequenceEqualsCI(first, second); + } + + public static unsafe bool SequenceEqualsCI(ReadOnlySpan first, ReadOnlySpan second) + { + var len = first.Length; + if (len != second.Length) return false; + + // OK, don't be clever (SIMD, etc); the purpose of FashHash is to compare RESP key tokens, which are + // typically relatively short, think 3-20 bytes. That wouldn't even touch a SIMD vector, so: + // just loop (the exact thing we'd need to do *anyway* in a SIMD implementation, to mop up the non-SIMD + // trailing bytes). + fixed (byte* firstPtr = &MemoryMarshal.GetReference(first)) + { + fixed (byte* secondPtr = &MemoryMarshal.GetReference(second)) + { + const int CS_MASK = 0b0101_1111; + for (int i = 0; i < len; i++) + { + byte x = firstPtr[i]; + var xCI = x & CS_MASK; + if (xCI >= 'A' & xCI <= 'Z') + { + // alpha mismatch + if (xCI != (secondPtr[i] & CS_MASK)) return false; + } + else if (x != secondPtr[i]) + { + // non-alpha mismatch + return false; + } + } + + return true; + } + } + } + + public static bool EqualsCS(ReadOnlySpan first, ReadOnlySpan second) + { + var len = first.Length; + if (len != second.Length) return false; + // for very short values, the CS hash performs CS equality + return len <= MaxBytesHashed ? HashCS(first) == HashCS(second) : first.SequenceEqual(second); + } + + public static bool SequenceEqualsCS(ReadOnlySpan first, ReadOnlySpan second) + => first.SequenceEqual(second); + + public static bool EqualsCI(ReadOnlySpan first, ReadOnlySpan second) + { + var len = first.Length; + if (len != second.Length) return false; + // for very short values, the CS hash performs CS equality; check that first + return len <= MaxBytesHashed ? HashUC(first) == HashUC(second) : SequenceEqualsCI(first, second); + } + + public static unsafe bool SequenceEqualsCI(ReadOnlySpan first, ReadOnlySpan second) + { + var len = first.Length; + if (len != second.Length) return false; + + // OK, don't be clever (SIMD, etc); the purpose of FashHash is to compare RESP key tokens, which are + // typically relatively short, think 3-20 bytes. That wouldn't even touch a SIMD vector, so: + // just loop (the exact thing we'd need to do *anyway* in a SIMD implementation, to mop up the non-SIMD + // trailing bytes). + fixed (char* firstPtr = &MemoryMarshal.GetReference(first)) + { + fixed (char* secondPtr = &MemoryMarshal.GetReference(second)) + { + const int CS_MASK = 0b0101_1111; + for (int i = 0; i < len; i++) + { + int x = (byte)firstPtr[i]; + var xCI = x & CS_MASK; + if (xCI >= 'A' & xCI <= 'Z') + { + // alpha mismatch + if (xCI != (secondPtr[i] & CS_MASK)) return false; + } + else if (x != (byte)secondPtr[i]) + { + // non-alpha mismatch + return false; + } + } + + return true; + } + } + } + + public static void Hash(scoped ReadOnlySpan value, out long cs, out long uc) + { + cs = HashCS(value); + uc = ToUC(cs); + } + + public static void Hash(scoped ReadOnlySpan value, out long cs, out long uc) + { + cs = HashCS(value); + uc = ToUC(cs); + } + + public static long HashUC(scoped ReadOnlySpan value) => ToUC(HashCS(value)); + + public static long HashUC(scoped ReadOnlySpan value) => ToUC(HashCS(value)); + + internal static long ToUC(long hashCS) + { + const long LC_MASK = 0x2020_2020_2020_2020; + // check whether there are any possible lower-case letters; + // this would be anything with the 0x20 bit set + if ((hashCS & LC_MASK) == 0) return hashCS; + + // Something looks possibly lower-case; we can't just mask it off, + // because there are other non-alpha characters in that range. +#if NET + ToUpper(MemoryMarshal.CreateSpan(ref Unsafe.As(ref hashCS), sizeof(long))); + return hashCS; +#else + Span buffer = stackalloc byte[8]; + BinaryPrimitives.WriteInt64LittleEndian(buffer, hashCS); + ToUpper(buffer); + return BinaryPrimitives.ReadInt64LittleEndian(buffer); +#endif + } + + public static long HashCS(scoped ReadOnlySpan value) + { + // at least 8? we can blit + if ((value.Length >> 3) != 0) return BinaryPrimitives.ReadInt64LittleEndian(value); + + // small (<7); manual loop + // note: profiling with unsafe code to pick out elements: much slower + // note: profiling with overstamping a local: 3x slower + ulong tally = 0; + for (int i = 0; i < value.Length; i++) + { + tally |= ((ulong)value[i]) << (i << 3); + } + return (long)tally; + } + + public static long HashCS(scoped ReadOnlySpan value) + { + // note: BDN profiling with Vector64.Narrow showed no benefit + if ((value.Length >> 3) != 0) + { + // slice if necessary, so we can use bounds-elided foreach + if (value.Length != 8) value = value.Slice(0, 8); + } + ulong tally = 0; + for (int i = 0; i < value.Length; i++) + { + tally |= ((ulong)value[i]) << (i << 3); + } + return (long)tally; + } + + public static void HashCS(scoped ReadOnlySpan value, out long cs0, out long cs1) + { + cs0 = HashCS(value); + cs1 = value.Length > MaxBytesHashed ? HashCS(value.Slice(start: MaxBytesHashed)) : 0; + } + + public static void HashCS(scoped ReadOnlySpan value, out long cs0, out long cs1) + { + cs0 = HashCS(value); + cs1 = value.Length > MaxBytesHashed ? HashCS(value.Slice(start: MaxBytesHashed)) : 0; + } + + public static void HashUC(scoped ReadOnlySpan value, out long cs0, out long cs1) + { + cs0 = HashUC(value); + cs1 = value.Length > MaxBytesHashed ? HashUC(value.Slice(start: MaxBytesHashed)) : 0; + } + + public static void HashUC(scoped ReadOnlySpan value, out long cs0, out long cs1) + { + cs0 = HashUC(value); + cs1 = value.Length > MaxBytesHashed ? HashUC(value.Slice(start: MaxBytesHashed)) : 0; + } + + public static void Hash(scoped ReadOnlySpan value, out long cs0, out long uc0, out long cs1, out long uc1) + { + Hash(value, out cs0, out uc0); + if (value.Length > MaxBytesHashed) + { + Hash(value.Slice(start: MaxBytesHashed), out cs1, out uc1); + } + else + { + cs1 = uc1 = 0; + } + } + + public static void Hash(scoped ReadOnlySpan value, out long cs0, out long uc0, out long cs1, out long uc1) + { + Hash(value, out cs0, out uc0); + if (value.Length > MaxBytesHashed) + { + Hash(value.Slice(start: MaxBytesHashed), out cs1, out uc1); + } + else + { + cs1 = uc1 = 0; + } + } +} diff --git a/src/StackExchange.Redis/Experiments.cs b/src/RESPite/Shared/Experiments.cs similarity index 86% rename from src/StackExchange.Redis/Experiments.cs rename to src/RESPite/Shared/Experiments.cs index 547838873..b4b9fcee1 100644 --- a/src/StackExchange.Redis/Experiments.cs +++ b/src/RESPite/Shared/Experiments.cs @@ -1,6 +1,4 @@ -using System.Diagnostics.CodeAnalysis; - -namespace StackExchange.Redis +namespace RESPite { // example usage: // [Experimental(Experiments.SomeFeature, UrlFormat = Experiments.UrlFormat)] @@ -9,11 +7,13 @@ internal static class Experiments { public const string UrlFormat = "https://stackexchange.github.io/StackExchange.Redis/exp/"; + // ReSharper disable InconsistentNaming public const string VectorSets = "SER001"; - // ReSharper disable once InconsistentNaming public const string Server_8_4 = "SER002"; - // ReSharper disable once InconsistentNaming public const string Server_8_6 = "SER003"; + public const string Respite = "SER004"; + public const string UnitTesting = "SER005"; + // ReSharper restore InconsistentNaming } } diff --git a/src/RESPite/Shared/FrameworkShims.Encoding.cs b/src/RESPite/Shared/FrameworkShims.Encoding.cs new file mode 100644 index 000000000..2f2c2e89d --- /dev/null +++ b/src/RESPite/Shared/FrameworkShims.Encoding.cs @@ -0,0 +1,50 @@ +#if !NET +// ReSharper disable once CheckNamespace +namespace System.Text +{ + internal static class EncodingExtensions + { + public static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan source, Span destination) + { + if (source.IsEmpty) return 0; + fixed (byte* bPtr = destination) + { + fixed (char* cPtr = source) + { + return encoding.GetBytes(cPtr, source.Length, bPtr, destination.Length); + } + } + } + + public static unsafe int GetChars(this Encoding encoding, ReadOnlySpan source, Span destination) + { + if (source.IsEmpty) return 0; + fixed (byte* bPtr = source) + { + fixed (char* cPtr = destination) + { + return encoding.GetChars(bPtr, source.Length, cPtr, destination.Length); + } + } + } + + public static unsafe int GetCharCount(this Encoding encoding, ReadOnlySpan source) + { + if (source.IsEmpty) return 0; + fixed (byte* bPtr = source) + { + return encoding.GetCharCount(bPtr, source.Length); + } + } + + public static unsafe string GetString(this Encoding encoding, ReadOnlySpan source) + { + if (source.IsEmpty) return ""; + fixed (byte* bPtr = source) + { + return encoding.GetString(bPtr, source.Length); + } + } + } +} +#endif diff --git a/src/RESPite/Shared/FrameworkShims.Stream.cs b/src/RESPite/Shared/FrameworkShims.Stream.cs new file mode 100644 index 000000000..3a42e5990 --- /dev/null +++ b/src/RESPite/Shared/FrameworkShims.Stream.cs @@ -0,0 +1,107 @@ +using System.Buffers; +using System.Runtime.InteropServices; + +#if !NET +// ReSharper disable once CheckNamespace +namespace System.IO +{ + internal static class StreamExtensions + { + public static void Write(this Stream stream, ReadOnlyMemory value) + { + if (MemoryMarshal.TryGetArray(value, out var segment)) + { + stream.Write(segment.Array!, segment.Offset, segment.Count); + } + else + { + var leased = ArrayPool.Shared.Rent(value.Length); + value.CopyTo(leased); + stream.Write(leased, 0, value.Length); + ArrayPool.Shared.Return(leased); // on success only + } + } + + public static int Read(this Stream stream, Memory value) + { + if (MemoryMarshal.TryGetArray(value, out var segment)) + { + return stream.Read(segment.Array!, segment.Offset, segment.Count); + } + else + { + var leased = ArrayPool.Shared.Rent(value.Length); + int bytes = stream.Read(leased, 0, value.Length); + if (bytes > 0) + { + leased.AsSpan(0, bytes).CopyTo(value.Span); + } + ArrayPool.Shared.Return(leased); // on success only + return bytes; + } + } + + public static ValueTask ReadAsync(this Stream stream, Memory value, CancellationToken cancellationToken) + { + if (MemoryMarshal.TryGetArray(value, out var segment)) + { + return new(stream.ReadAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken)); + } + else + { + var leased = ArrayPool.Shared.Rent(value.Length); + var pending = stream.ReadAsync(leased, 0, value.Length, cancellationToken); + if (!pending.IsCompleted) + { + return Awaited(pending, value, leased); + } + + var bytes = pending.GetAwaiter().GetResult(); + if (bytes > 0) + { + leased.AsSpan(0, bytes).CopyTo(value.Span); + } + ArrayPool.Shared.Return(leased); // on success only + return new(bytes); + + static async ValueTask Awaited(Task pending, Memory value, byte[] leased) + { + var bytes = await pending.ConfigureAwait(false); + if (bytes > 0) + { + leased.AsSpan(0, bytes).CopyTo(value.Span); + } + ArrayPool.Shared.Return(leased); // on success only + return bytes; + } + } + } + + public static ValueTask WriteAsync(this Stream stream, ReadOnlyMemory value, CancellationToken cancellationToken) + { + if (MemoryMarshal.TryGetArray(value, out var segment)) + { + return new(stream.WriteAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken)); + } + else + { + var leased = ArrayPool.Shared.Rent(value.Length); + value.CopyTo(leased); + var pending = stream.WriteAsync(leased, 0, value.Length, cancellationToken); + if (!pending.IsCompleted) + { + return Awaited(pending, leased); + } + pending.GetAwaiter().GetResult(); + ArrayPool.Shared.Return(leased); // on success only + return default; + } + static async ValueTask Awaited(Task pending, byte[] leased) + { + await pending.ConfigureAwait(false); + ArrayPool.Shared.Return(leased); // on success only + } + } + } +} +#endif diff --git a/src/RESPite/Shared/FrameworkShims.cs b/src/RESPite/Shared/FrameworkShims.cs new file mode 100644 index 000000000..ceb344b9e --- /dev/null +++ b/src/RESPite/Shared/FrameworkShims.cs @@ -0,0 +1,15 @@ +#pragma warning disable SA1403 // single namespace + +#if !NET10_0_OR_GREATER +namespace System.Runtime.CompilerServices +{ + // see https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.overloadresolutionpriorityattribute + [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property, Inherited = false)] + internal sealed class OverloadResolutionPriorityAttribute(int priority) : Attribute + { + public int Priority => priority; + } +} +#endif + +#pragma warning restore SA1403 diff --git a/src/RESPite/Shared/NullableHacks.cs b/src/RESPite/Shared/NullableHacks.cs new file mode 100644 index 000000000..704437442 --- /dev/null +++ b/src/RESPite/Shared/NullableHacks.cs @@ -0,0 +1,146 @@ +// https://github.com/dotnet/runtime/blob/527f9ae88a0ee216b44d556f9bdc84037fe0ebda/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs + +#pragma warning disable +#define INTERNAL_NULLABLE_ATTRIBUTES + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ +#if !NET + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class AllowNullAttribute : Attribute { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class DisallowNullAttribute : Attribute { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class MaybeNullAttribute : Attribute { } + + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + internal sealed class DoesNotReturnAttribute : Attribute { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute + { + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = new[] { member }; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } + } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + } +#endif +} diff --git a/src/RESPite/readme.md b/src/RESPite/readme.md new file mode 100644 index 000000000..034cae8d3 --- /dev/null +++ b/src/RESPite/readme.md @@ -0,0 +1,6 @@ +# RESPite + +RESPite is a high-performance low-level RESP (Redis, etc) library, used as the IO core for +StackExchange.Redis v3+. It is also available for direct use from other places! + +For now: you probably shouldn't be using this. \ No newline at end of file diff --git a/src/StackExchange.Redis/APITypes/LCSMatchResult.cs b/src/StackExchange.Redis/APITypes/LCSMatchResult.cs index fdeea89ff..3aca6357b 100644 --- a/src/StackExchange.Redis/APITypes/LCSMatchResult.cs +++ b/src/StackExchange.Redis/APITypes/LCSMatchResult.cs @@ -1,11 +1,14 @@ using System; +using System.ComponentModel; +// ReSharper disable once CheckNamespace namespace StackExchange.Redis; /// /// The result of a LongestCommonSubsequence command with IDX feature. /// Returns a list of the positions of each sub-match. /// +// ReSharper disable once InconsistentNaming public readonly struct LCSMatchResult { internal static LCSMatchResult Null { get; } = new LCSMatchResult(Array.Empty(), 0); @@ -36,20 +39,92 @@ internal LCSMatchResult(LCSMatch[] matches, long matchLength) LongestMatchLength = matchLength; } + /// + /// Represents a position range in a string. + /// + // ReSharper disable once InconsistentNaming + public readonly struct LCSPosition : IEquatable + { + /// + /// The start index of the position. + /// + public long Start { get; } + + /// + /// The end index of the position. + /// + public long End { get; } + + /// + /// Returns a new Position. + /// + /// The start index. + /// The end index. + public LCSPosition(long start, long end) + { + Start = start; + End = end; + } + + /// + public override string ToString() => $"[{Start}..{End}]"; + + /// + public override int GetHashCode() + { + unchecked + { + return ((int)Start * 31) + (int)End; + } + } + + /// + public override bool Equals(object? obj) => obj is LCSPosition other && Equals(in other); + + /// + /// Compares this position to another for equality. + /// + [CLSCompliant(false)] + public bool Equals(in LCSPosition other) => Start == other.Start && End == other.End; + + /// + /// Compares this position to another for equality. + /// + bool IEquatable.Equals(LCSPosition other) => Equals(in other); + } + /// /// Represents a sub-match of the longest match. i.e first indexes the matched substring in each string. /// - public readonly struct LCSMatch + // ReSharper disable once InconsistentNaming + public readonly struct LCSMatch : IEquatable { + private readonly LCSPosition _first; + private readonly LCSPosition _second; + + /// + /// The position of the matched substring in the first string. + /// + public LCSPosition First => _first; + + /// + /// The position of the matched substring in the second string. + /// + public LCSPosition Second => _second; + /// /// The first index of the matched substring in the first string. /// - public long FirstStringIndex { get; } + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public long FirstStringIndex => _first.Start; /// /// The first index of the matched substring in the second string. /// - public long SecondStringIndex { get; } + [EditorBrowsable(EditorBrowsableState.Never)] + [Browsable(false)] + public long SecondStringIndex => _second.Start; /// /// The length of the match. @@ -59,14 +134,44 @@ public readonly struct LCSMatch /// /// Returns a new Match. /// - /// The first index of the matched substring in the first string. - /// The first index of the matched substring in the second string. + /// The position of the matched substring in the first string. + /// The position of the matched substring in the second string. /// The length of the match. - internal LCSMatch(long firstStringIndex, long secondStringIndex, long length) + internal LCSMatch(in LCSPosition first, in LCSPosition second, long length) { - FirstStringIndex = firstStringIndex; - SecondStringIndex = secondStringIndex; + _first = first; + _second = second; Length = length; } + + /// + public override string ToString() => $"First: {_first}, Second: {_second}, Length: {Length}"; + + /// + public override int GetHashCode() + { + unchecked + { + int hash = 17; + hash = (hash * 31) + _first.GetHashCode(); + hash = (hash * 31) + _second.GetHashCode(); + hash = (hash * 31) + Length.GetHashCode(); + return hash; + } + } + + /// + public override bool Equals(object? obj) => obj is LCSMatch other && Equals(in other); + + /// + /// Compares this match to another for equality. + /// + [CLSCompliant(false)] + public bool Equals(in LCSMatch other) => _first.Equals(in other._first) && _second.Equals(in other._second) && Length == other.Length; + + /// + /// Compares this match to another for equality. + /// + bool IEquatable.Equals(LCSMatch other) => Equals(in other); } } diff --git a/src/StackExchange.Redis/APITypes/StreamInfo.cs b/src/StackExchange.Redis/APITypes/StreamInfo.cs index e37df5add..1de0526ec 100644 --- a/src/StackExchange.Redis/APITypes/StreamInfo.cs +++ b/src/StackExchange.Redis/APITypes/StreamInfo.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using RESPite; namespace StackExchange.Redis; diff --git a/src/StackExchange.Redis/ChannelMessageQueue.cs b/src/StackExchange.Redis/ChannelMessageQueue.cs index 9f962e52a..f7bd9a4a2 100644 --- a/src/StackExchange.Redis/ChannelMessageQueue.cs +++ b/src/StackExchange.Redis/ChannelMessageQueue.cs @@ -4,10 +4,6 @@ using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; -#if NETCOREAPP3_1 -using System.Diagnostics; -using System.Reflection; -#endif namespace StackExchange.Redis; @@ -76,31 +72,12 @@ public ValueTask ReadAsync(CancellationToken cancellationToken = /// The (approximate) count of items in the Channel. public bool TryGetCount(out int count) { - // This is specific to netcoreapp3.1, because full framework was out of band and the new prop is present -#if NETCOREAPP3_1 - // get this using the reflection - try - { - var prop = - _queue.GetType().GetProperty("ItemsCountForDebugger", BindingFlags.Instance | BindingFlags.NonPublic); - if (prop is not null) - { - count = (int)prop.GetValue(_queue)!; - return true; - } - } - catch (Exception ex) - { - Debug.WriteLine(ex.Message); // but ignore - } -#else var reader = _queue.Reader; if (reader.CanCount) { count = reader.Count; return true; } -#endif count = 0; return false; @@ -334,7 +311,7 @@ internal async Task UnsubscribeAsyncImpl(Exception? error = null, CommandFlags f public Task UnsubscribeAsync(CommandFlags flags = CommandFlags.None) => UnsubscribeAsyncImpl(null, flags); /// -#if NETCOREAPP3_0_OR_GREATER +#if NET public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) // ReSharper disable once MethodSupportsCancellation - provided in GetAsyncEnumerator => _queue.Reader.ReadAllAsync().GetAsyncEnumerator(cancellationToken); diff --git a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs index ccfa4ee63..18216c1f2 100644 --- a/src/StackExchange.Redis/Configuration/LoggingTunnel.cs +++ b/src/StackExchange.Redis/Configuration/LoggingTunnel.cs @@ -359,7 +359,7 @@ private async Task TlsHandshakeAsync(Stream stream, EndPoint endpoint) userCertificateSelectionCallback: _options.CertificateSelectionCallback ?? PhysicalConnection.GetAmbientClientCertificateCallback(), encryptionPolicy: EncryptionPolicy.RequireEncryption); -#if NETCOREAPP3_1_OR_GREATER +#if NET var configOptions = _options.SslClientAuthenticationOptions?.Invoke(host); if (configOptions is not null) { @@ -532,7 +532,7 @@ public override void Close() base.Close(); } -#if NETCOREAPP3_0_OR_GREATER +#if NET public override async ValueTask DisposeAsync() { await _inner.DisposeAsync().ForAwait(); @@ -574,7 +574,7 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, } return len; } -#if NETCOREAPP3_0_OR_GREATER +#if NET public override int Read(Span buffer) { var len = _inner.Read(buffer); @@ -613,7 +613,7 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc await _inner.WriteAsync(buffer, offset, count, cancellationToken).ForAwait(); await writesTask; } -#if NETCOREAPP3_0_OR_GREATER +#if NET public override void Write(ReadOnlySpan buffer) { _writes.Write(buffer); diff --git a/src/StackExchange.Redis/ConfigurationOptions.cs b/src/StackExchange.Redis/ConfigurationOptions.cs index c0021f024..641fccc95 100644 --- a/src/StackExchange.Redis/ConfigurationOptions.cs +++ b/src/StackExchange.Redis/ConfigurationOptions.cs @@ -301,7 +301,7 @@ public bool HighIntegrity /// The file system path to find the certificate at. public void TrustIssuer(string issuerCertificatePath) => CertificateValidationCallback = TrustIssuerCallback(issuerCertificatePath); -#if NET5_0_OR_GREATER +#if NET /// /// Supply a user certificate from a PEM file pair and enable TLS. /// @@ -325,7 +325,7 @@ public void SetUserPfxCertificate(string userCertificatePath, string? password = Ssl = true; } -#if NET5_0_OR_GREATER +#if NET internal static LocalCertificateSelectionCallback CreatePemUserCertificateCallback(string userCertificatePath, string? userKeyPath) { // PEM handshakes not universally supported and causes a runtime error about ephemeral certificates; to avoid, export as PFX @@ -340,7 +340,9 @@ internal static LocalCertificateSelectionCallback CreatePemUserCertificateCallba internal static LocalCertificateSelectionCallback CreatePfxUserCertificateCallback(string userCertificatePath, string? password, X509KeyStorageFlags storageFlags = X509KeyStorageFlags.DefaultKeySet) { +#pragma warning disable SYSLIB0057 var pfx = new X509Certificate2(userCertificatePath, password ?? "", storageFlags); +#pragma warning restore SYSLIB0057 return (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => pfx; } @@ -351,7 +353,9 @@ internal static LocalCertificateSelectionCallback CreatePfxUserCertificateCallba public void TrustIssuer(X509Certificate2 issuer) => CertificateValidationCallback = TrustIssuerCallback(issuer); internal static RemoteCertificateValidationCallback TrustIssuerCallback(string issuerCertificatePath) +#pragma warning disable SYSLIB0057 => TrustIssuerCallback(new X509Certificate2(issuerCertificatePath)); +#pragma warning restore SYSLIB0057 private static RemoteCertificateValidationCallback TrustIssuerCallback(X509Certificate2 issuer) { if (issuer == null) throw new ArgumentNullException(nameof(issuer)); @@ -694,7 +698,7 @@ public int ResponseTimeout /// public SocketManager? SocketManager { get; set; } -#if NETCOREAPP3_1_OR_GREATER +#if NET /// /// A provider for a given host, for custom TLS connection options. /// Note: this overrides *all* other TLS and certificate settings, only for advanced use cases. @@ -833,7 +837,7 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow BeforeSocketConnect = BeforeSocketConnect, EndPoints = EndPoints.Clone(), LoggerFactory = LoggerFactory, -#if NETCOREAPP3_1_OR_GREATER +#if NET SslClientAuthenticationOptions = SslClientAuthenticationOptions, #endif Tunnel = Tunnel, diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index f3cefa6e2..a5995046e 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -1913,7 +1913,7 @@ private static string DeDotifyHost(string input) if (colonPosition > 0) { // Has a port specifier -#if NETCOREAPP +#if NET return string.Concat(input.AsSpan(0, periodPosition), input.AsSpan(colonPosition)); #else return input.Substring(0, periodPosition) + input.Substring(colonPosition); diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs index 2a4d1180f..2a1c7695e 100644 --- a/src/StackExchange.Redis/Enums/RedisCommand.cs +++ b/src/StackExchange.Redis/Enums/RedisCommand.cs @@ -1,7 +1,9 @@ using System; +using RESPite; namespace StackExchange.Redis; +// ReSharper disable InconsistentNaming internal enum RedisCommand { NONE, // must be first for "zero reasons" @@ -280,6 +282,16 @@ internal enum RedisCommand UNKNOWN, } +internal static partial class RedisCommandMetadata +{ + [AsciiHash(CaseSensitive = false)] + public static partial bool TryParseCI(ReadOnlySpan command, out RedisCommand value); + + [AsciiHash(CaseSensitive = false)] + public static partial bool TryParseCI(ReadOnlySpan command, out RedisCommand value); +} + +// ReSharper restore InconsistentNaming internal static class RedisCommandExtensions { /// diff --git a/src/StackExchange.Redis/Enums/RedisType.cs b/src/StackExchange.Redis/Enums/RedisType.cs index f1da87505..90a41165b 100644 --- a/src/StackExchange.Redis/Enums/RedisType.cs +++ b/src/StackExchange.Redis/Enums/RedisType.cs @@ -65,5 +65,11 @@ public enum RedisType /// The data-type was not recognised by the client library. /// Unknown, + + /// + /// Vector sets are a data type similar to sorted sets, but instead of a score, + /// vector set elements have a string representation of a vector. + /// + VectorSet, } } diff --git a/src/StackExchange.Redis/ExceptionFactory.cs b/src/StackExchange.Redis/ExceptionFactory.cs index 43959edb1..434abce17 100644 --- a/src/StackExchange.Redis/ExceptionFactory.cs +++ b/src/StackExchange.Redis/ExceptionFactory.cs @@ -107,7 +107,7 @@ internal static Exception NoConnectionAvailable( serverSnapshot = new ServerEndPoint[] { server }; } - var innerException = PopulateInnerExceptions(serverSnapshot == default ? multiplexer.GetServerSnapshot() : serverSnapshot); + var innerException = PopulateInnerExceptions(serverSnapshot.IsEmpty ? multiplexer.GetServerSnapshot() : serverSnapshot); // Try to get a useful error message for the user. long attempts = multiplexer._connectAttemptCount, completions = multiplexer._connectCompletedCount; diff --git a/src/StackExchange.Redis/ExtensionMethods.Internal.cs b/src/StackExchange.Redis/ExtensionMethods.Internal.cs index de5a9f2a6..446f6ff88 100644 --- a/src/StackExchange.Redis/ExtensionMethods.Internal.cs +++ b/src/StackExchange.Redis/ExtensionMethods.Internal.cs @@ -11,7 +11,7 @@ internal static bool IsNullOrEmpty([NotNullWhen(false)] this string? s) => internal static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? s) => string.IsNullOrWhiteSpace(s); -#if !NETCOREAPP3_1_OR_GREATER +#if !NET internal static bool TryDequeue(this Queue queue, [NotNullWhen(true)] out T? result) { if (queue.Count == 0) diff --git a/src/StackExchange.Redis/FastHash.cs b/src/StackExchange.Redis/FastHash.cs deleted file mode 100644 index 49eb01b31..000000000 --- a/src/StackExchange.Redis/FastHash.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Buffers; -using System.Buffers.Binary; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace StackExchange.Redis; - -/// -/// This type is intended to provide fast hashing functions for small strings, for example well-known -/// RESP literals that are usually identifiable by their length and initial bytes; it is not intended -/// for general purpose hashing. All matches must also perform a sequence equality check. -/// -/// See HastHashGenerator.md for more information and intended usage. -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -[Conditional("DEBUG")] // evaporate in release -internal sealed class FastHashAttribute(string token = "") : Attribute -{ - public string Token => token; -} - -internal static class FastHash -{ - /* not sure we need this, but: retain for reference - - // Perform case-insensitive hash by masking (X and x differ by only 1 bit); this halves - // our entropy, but is still useful when case doesn't matter. - private const long CaseMask = ~0x2020202020202020; - - public static long Hash64CI(this ReadOnlySequence value) - => value.Hash64() & CaseMask; - public static long Hash64CI(this scoped ReadOnlySpan value) - => value.Hash64() & CaseMask; -*/ - - public static long Hash64(this ReadOnlySequence value) - { -#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - var first = value.FirstSpan; -#else - var first = value.First.Span; -#endif - return first.Length >= sizeof(long) || value.IsSingleSegment - ? first.Hash64() : SlowHash64(value); - - static long SlowHash64(ReadOnlySequence value) - { - Span buffer = stackalloc byte[sizeof(long)]; - if (value.Length < sizeof(long)) - { - value.CopyTo(buffer); - buffer.Slice((int)value.Length).Clear(); - } - else - { - value.Slice(0, sizeof(long)).CopyTo(buffer); - } - return BitConverter.IsLittleEndian - ? Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(buffer)) - : BinaryPrimitives.ReadInt64LittleEndian(buffer); - } - } - - public static long Hash64(this scoped ReadOnlySpan value) - { - if (BitConverter.IsLittleEndian) - { - ref byte data = ref MemoryMarshal.GetReference(value); - return value.Length switch - { - 0 => 0, - 1 => data, // 0000000A - 2 => Unsafe.ReadUnaligned(ref data), // 000000BA - 3 => Unsafe.ReadUnaligned(ref data) | // 000000BA - (Unsafe.Add(ref data, 2) << 16), // 00000C00 - 4 => Unsafe.ReadUnaligned(ref data), // 0000DCBA - 5 => Unsafe.ReadUnaligned(ref data) | // 0000DCBA - ((long)Unsafe.Add(ref data, 4) << 32), // 000E0000 - 6 => Unsafe.ReadUnaligned(ref data) | // 0000DCBA - ((long)Unsafe.ReadUnaligned(ref Unsafe.Add(ref data, 4)) << 32), // 00FE0000 - 7 => Unsafe.ReadUnaligned(ref data) | // 0000DCBA - ((long)Unsafe.ReadUnaligned(ref Unsafe.Add(ref data, 4)) << 32) | // 00FE0000 - ((long)Unsafe.Add(ref data, 6) << 48), // 0G000000 - _ => Unsafe.ReadUnaligned(ref data), // HGFEDCBA - }; - } - -#pragma warning disable CS0618 // Type or member is obsolete - return Hash64Fallback(value); -#pragma warning restore CS0618 // Type or member is obsolete - } - - [Obsolete("Only exists for benchmarks (to show that we don't need to use it) and unit tests (for correctness)")] - internal static unsafe long Hash64Unsafe(scoped ReadOnlySpan value) - { - if (BitConverter.IsLittleEndian) - { - fixed (byte* ptr = &MemoryMarshal.GetReference(value)) - { - return value.Length switch - { - 0 => 0, - 1 => *ptr, // 0000000A - 2 => *(ushort*)ptr, // 000000BA - 3 => *(ushort*)ptr | // 000000BA - (ptr[2] << 16), // 00000C00 - 4 => *(int*)ptr, // 0000DCBA - 5 => (long)*(int*)ptr | // 0000DCBA - ((long)ptr[4] << 32), // 000E0000 - 6 => (long)*(int*)ptr | // 0000DCBA - ((long)*(ushort*)(ptr + 4) << 32), // 00FE0000 - 7 => (long)*(int*)ptr | // 0000DCBA - ((long)*(ushort*)(ptr + 4) << 32) | // 00FE0000 - ((long)ptr[6] << 48), // 0G000000 - _ => *(long*)ptr, // HGFEDCBA - }; - } - } - - return Hash64Fallback(value); - } - - [Obsolete("Only exists for unit tests and fallback")] - internal static long Hash64Fallback(scoped ReadOnlySpan value) - { - if (value.Length < sizeof(long)) - { - Span tmp = stackalloc byte[sizeof(long)]; - value.CopyTo(tmp); // ABC***** - tmp.Slice(value.Length).Clear(); // ABC00000 - return BinaryPrimitives.ReadInt64LittleEndian(tmp); // 00000CBA - } - - return BinaryPrimitives.ReadInt64LittleEndian(value); // HGFEDCBA - } -} diff --git a/src/StackExchange.Redis/Format.cs b/src/StackExchange.Redis/Format.cs index a76b77afc..9279bb0f5 100644 --- a/src/StackExchange.Redis/Format.cs +++ b/src/StackExchange.Redis/Format.cs @@ -15,7 +15,7 @@ namespace StackExchange.Redis { internal static class Format { -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER +#if NET public static int ParseInt32(ReadOnlySpan s) => int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo); public static bool TryParseInt32(ReadOnlySpan s, out int value) => int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out value); #endif @@ -197,7 +197,7 @@ static bool TryParseInfNaN(ReadOnlySpan s, bool positive, out double value } break; } -#if NET6_0_OR_GREATER +#if NET Unsafe.SkipInit(out value); #else value = 0; @@ -281,7 +281,7 @@ static bool TryParseInfNaN(ReadOnlySpan s, bool positive, out double value } break; } -#if NET6_0_OR_GREATER +#if NET Unsafe.SkipInit(out value); #else value = 0; @@ -406,7 +406,7 @@ internal static string GetString(ReadOnlySequence buffer) internal static unsafe string GetString(ReadOnlySpan span) { if (span.IsEmpty) return ""; -#if NETCOREAPP3_1_OR_GREATER +#if NET return Encoding.UTF8.GetString(span); #else fixed (byte* ptr = span) @@ -567,7 +567,7 @@ internal static int FormatInt32(int value, Span destination) internal static bool TryParseVersion(ReadOnlySpan input, [NotNullWhen(true)] out Version? version) { -#if NETCOREAPP3_1_OR_GREATER +#if NET if (Version.TryParse(input, out version)) return true; // allow major-only (Version doesn't do this, because... reasons?) if (TryParseInt32(input, out int i32)) diff --git a/src/StackExchange.Redis/FrameworkShims.cs b/src/StackExchange.Redis/FrameworkShims.cs index c0fe4cb1d..ce954406d 100644 --- a/src/StackExchange.Redis/FrameworkShims.cs +++ b/src/StackExchange.Redis/FrameworkShims.cs @@ -1,6 +1,6 @@ #pragma warning disable SA1403 // single namespace -#if NET5_0_OR_GREATER +#if NET // context: https://github.com/StackExchange/StackExchange.Redis/issues/2619 [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))] #else @@ -15,7 +15,7 @@ internal static class IsExternalInit { } } #endif -#if !NET9_0_OR_GREATER +#if !NET10_0_OR_GREATER namespace System.Runtime.CompilerServices { // see https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.overloadresolutionpriorityattribute @@ -27,7 +27,7 @@ internal sealed class OverloadResolutionPriorityAttribute(int priority) : Attrib } #endif -#if !(NETCOREAPP || NETSTANDARD2_1_OR_GREATER) +#if !NET namespace System.Text { diff --git a/src/StackExchange.Redis/HotKeys.ResultProcessor.cs b/src/StackExchange.Redis/HotKeys.ResultProcessor.cs index a0f5b2892..71644c010 100644 --- a/src/StackExchange.Redis/HotKeys.ResultProcessor.cs +++ b/src/StackExchange.Redis/HotKeys.ResultProcessor.cs @@ -36,20 +36,22 @@ private HotKeysResult(in RawResult result) var iter = result.GetItems().GetEnumerator(); while (iter.MoveNext()) { - ref readonly RawResult key = ref iter.Current; + if (!iter.Current.TryParse(HotKeysFieldMetadata.TryParse, out HotKeysField field)) + field = HotKeysField.Unknown; + if (!iter.MoveNext()) break; // lies about the length! ref readonly RawResult value = ref iter.Current; - var hash = key.Payload.Hash64(); + long i64; - switch (hash) + switch (field) { - case tracking_active.Hash when tracking_active.Is(hash, key): + case HotKeysField.TrackingActive: TrackingActive = value.GetBoolean(); break; - case sample_ratio.Hash when sample_ratio.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.SampleRatio when value.TryGetInt64(out i64): SampleRatio = i64; break; - case selected_slots.Hash when selected_slots.Is(hash, key) & value.Resp2TypeArray is ResultType.Array: + case HotKeysField.SelectedSlots when value.Resp2TypeArray is ResultType.Array: var len = value.ItemsCount; if (len == 0) { @@ -92,55 +94,55 @@ private HotKeysResult(in RawResult result) } _selectedSlots = slots; break; - case all_commands_all_slots_us.Hash when all_commands_all_slots_us.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.AllCommandsAllSlotsUs when value.TryGetInt64(out i64): AllCommandsAllSlotsMicroseconds = i64; break; - case all_commands_selected_slots_us.Hash when all_commands_selected_slots_us.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.AllCommandsSelectedSlotsUs when value.TryGetInt64(out i64): AllCommandSelectedSlotsMicroseconds = i64; break; - case sampled_command_selected_slots_us.Hash when sampled_command_selected_slots_us.Is(hash, key) && value.TryGetInt64(out i64): - case sampled_commands_selected_slots_us.Hash when sampled_commands_selected_slots_us.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.SampledCommandSelectedSlotsUs when value.TryGetInt64(out i64): + case HotKeysField.SampledCommandsSelectedSlotsUs when value.TryGetInt64(out i64): SampledCommandsSelectedSlotsMicroseconds = i64; break; - case net_bytes_all_commands_all_slots.Hash when net_bytes_all_commands_all_slots.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.NetBytesAllCommandsAllSlots when value.TryGetInt64(out i64): AllCommandsAllSlotsNetworkBytes = i64; break; - case net_bytes_all_commands_selected_slots.Hash when net_bytes_all_commands_selected_slots.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.NetBytesAllCommandsSelectedSlots when value.TryGetInt64(out i64): NetworkBytesAllCommandsSelectedSlotsRaw = i64; break; - case net_bytes_sampled_commands_selected_slots.Hash when net_bytes_sampled_commands_selected_slots.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.NetBytesSampledCommandsSelectedSlots when value.TryGetInt64(out i64): NetworkBytesSampledCommandsSelectedSlotsRaw = i64; break; - case collection_start_time_unix_ms.Hash when collection_start_time_unix_ms.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.CollectionStartTimeUnixMs when value.TryGetInt64(out i64): CollectionStartTimeUnixMilliseconds = i64; break; - case collection_duration_ms.Hash when collection_duration_ms.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.CollectionDurationMs when value.TryGetInt64(out i64): CollectionDurationMicroseconds = i64 * 1000; // ms vs us is in question: support both, and abstract it from the caller break; - case collection_duration_us.Hash when collection_duration_us.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.CollectionDurationUs when value.TryGetInt64(out i64): CollectionDurationMicroseconds = i64; break; - case total_cpu_time_sys_ms.Hash when total_cpu_time_sys_ms.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.TotalCpuTimeSysMs when value.TryGetInt64(out i64): metrics |= HotKeysMetrics.Cpu; TotalCpuTimeSystemMicroseconds = i64 * 1000; // ms vs us is in question: support both, and abstract it from the caller break; - case total_cpu_time_sys_us.Hash when total_cpu_time_sys_us.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.TotalCpuTimeSysUs when value.TryGetInt64(out i64): metrics |= HotKeysMetrics.Cpu; TotalCpuTimeSystemMicroseconds = i64; break; - case total_cpu_time_user_ms.Hash when total_cpu_time_user_ms.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.TotalCpuTimeUserMs when value.TryGetInt64(out i64): metrics |= HotKeysMetrics.Cpu; TotalCpuTimeUserMicroseconds = i64 * 1000; // ms vs us is in question: support both, and abstract it from the caller break; - case total_cpu_time_user_us.Hash when total_cpu_time_user_us.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.TotalCpuTimeUserUs when value.TryGetInt64(out i64): metrics |= HotKeysMetrics.Cpu; TotalCpuTimeUserMicroseconds = i64; break; - case total_net_bytes.Hash when total_net_bytes.Is(hash, key) && value.TryGetInt64(out i64): + case HotKeysField.TotalNetBytes when value.TryGetInt64(out i64): metrics |= HotKeysMetrics.Network; TotalNetworkBytesRaw = i64; break; - case by_cpu_time_us.Hash when by_cpu_time_us.Is(hash, key) & value.Resp2TypeArray is ResultType.Array: + case HotKeysField.ByCpuTimeUs when value.Resp2TypeArray is ResultType.Array: metrics |= HotKeysMetrics.Cpu; len = value.ItemsCount / 2; if (len == 0) @@ -162,7 +164,7 @@ private HotKeysResult(in RawResult result) _cpuByKey = cpuTime; break; - case by_net_bytes.Hash when by_net_bytes.Is(hash, key) & value.Resp2TypeArray is ResultType.Array: + case HotKeysField.ByNetBytes when value.Resp2TypeArray is ResultType.Array: metrics |= HotKeysMetrics.Network; len = value.ItemsCount / 2; if (len == 0) @@ -188,30 +190,4 @@ private HotKeysResult(in RawResult result) } // while Metrics = metrics; } - -#pragma warning disable SA1134, SA1300 - // ReSharper disable InconsistentNaming - [FastHash] internal static partial class tracking_active { } - [FastHash] internal static partial class sample_ratio { } - [FastHash] internal static partial class selected_slots { } - [FastHash] internal static partial class all_commands_all_slots_us { } - [FastHash] internal static partial class all_commands_selected_slots_us { } - [FastHash] internal static partial class sampled_command_selected_slots_us { } - [FastHash] internal static partial class sampled_commands_selected_slots_us { } - [FastHash] internal static partial class net_bytes_all_commands_all_slots { } - [FastHash] internal static partial class net_bytes_all_commands_selected_slots { } - [FastHash] internal static partial class net_bytes_sampled_commands_selected_slots { } - [FastHash] internal static partial class collection_start_time_unix_ms { } - [FastHash] internal static partial class collection_duration_ms { } - [FastHash] internal static partial class collection_duration_us { } - [FastHash] internal static partial class total_cpu_time_user_ms { } - [FastHash] internal static partial class total_cpu_time_user_us { } - [FastHash] internal static partial class total_cpu_time_sys_ms { } - [FastHash] internal static partial class total_cpu_time_sys_us { } - [FastHash] internal static partial class total_net_bytes { } - [FastHash] internal static partial class by_cpu_time_us { } - [FastHash] internal static partial class by_net_bytes { } - - // ReSharper restore InconsistentNaming -#pragma warning restore SA1134, SA1300 } diff --git a/src/StackExchange.Redis/HotKeys.cs b/src/StackExchange.Redis/HotKeys.cs index 28f3ddc56..c3d71fe17 100644 --- a/src/StackExchange.Redis/HotKeys.cs +++ b/src/StackExchange.Redis/HotKeys.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using RESPite; namespace StackExchange.Redis; diff --git a/src/StackExchange.Redis/HotKeysField.cs b/src/StackExchange.Redis/HotKeysField.cs new file mode 100644 index 000000000..0c514c6fd --- /dev/null +++ b/src/StackExchange.Redis/HotKeysField.cs @@ -0,0 +1,145 @@ +using System; +using RESPite; + +namespace StackExchange.Redis; + +/// +/// Fields that can appear in a HOTKEYS response. +/// +internal enum HotKeysField +{ + /// + /// Unknown or unrecognized field. + /// + [AsciiHash("")] + Unknown = 0, + + /// + /// Whether tracking is active. + /// + [AsciiHash("tracking-active")] + TrackingActive, + + /// + /// Sample ratio. + /// + [AsciiHash("sample-ratio")] + SampleRatio, + + /// + /// Selected slots. + /// + [AsciiHash("selected-slots")] + SelectedSlots, + + /// + /// All commands all slots microseconds. + /// + [AsciiHash("all-commands-all-slots-us")] + AllCommandsAllSlotsUs, + + /// + /// All commands selected slots microseconds. + /// + [AsciiHash("all-commands-selected-slots-us")] + AllCommandsSelectedSlotsUs, + + /// + /// Sampled command selected slots microseconds (singular). + /// + [AsciiHash("sampled-command-selected-slots-us")] + SampledCommandSelectedSlotsUs, + + /// + /// Sampled commands selected slots microseconds (plural). + /// + [AsciiHash("sampled-commands-selected-slots-us")] + SampledCommandsSelectedSlotsUs, + + /// + /// Network bytes all commands all slots. + /// + [AsciiHash("net-bytes-all-commands-all-slots")] + NetBytesAllCommandsAllSlots, + + /// + /// Network bytes all commands selected slots. + /// + [AsciiHash("net-bytes-all-commands-selected-slots")] + NetBytesAllCommandsSelectedSlots, + + /// + /// Network bytes sampled commands selected slots. + /// + [AsciiHash("net-bytes-sampled-commands-selected-slots")] + NetBytesSampledCommandsSelectedSlots, + + /// + /// Collection start time in Unix milliseconds. + /// + [AsciiHash("collection-start-time-unix-ms")] + CollectionStartTimeUnixMs, + + /// + /// Collection duration in milliseconds. + /// + [AsciiHash("collection-duration-ms")] + CollectionDurationMs, + + /// + /// Collection duration in microseconds. + /// + [AsciiHash("collection-duration-us")] + CollectionDurationUs, + + /// + /// Total CPU time user in milliseconds. + /// + [AsciiHash("total-cpu-time-user-ms")] + TotalCpuTimeUserMs, + + /// + /// Total CPU time user in microseconds. + /// + [AsciiHash("total-cpu-time-user-us")] + TotalCpuTimeUserUs, + + /// + /// Total CPU time system in milliseconds. + /// + [AsciiHash("total-cpu-time-sys-ms")] + TotalCpuTimeSysMs, + + /// + /// Total CPU time system in microseconds. + /// + [AsciiHash("total-cpu-time-sys-us")] + TotalCpuTimeSysUs, + + /// + /// Total network bytes. + /// + [AsciiHash("total-net-bytes")] + TotalNetBytes, + + /// + /// By CPU time in microseconds. + /// + [AsciiHash("by-cpu-time-us")] + ByCpuTimeUs, + + /// + /// By network bytes. + /// + [AsciiHash("by-net-bytes")] + ByNetBytes, +} + +/// +/// Metadata and parsing methods for HotKeysField. +/// +internal static partial class HotKeysFieldMetadata +{ + [AsciiHash] + internal static partial bool TryParse(ReadOnlySpan value, out HotKeysField field); +} diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs index 1c163f315..8e6444ea8 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using RESPite; // ReSharper disable once CheckNamespace namespace StackExchange.Redis; @@ -161,7 +162,7 @@ bool VectorSetAdd( bool VectorSetSetAttributesJson( RedisKey key, RedisValue member, -#if NET7_0_OR_GREATER +#if NET8_0_OR_GREATER [StringSyntax(StringSyntaxAttribute.Json)] #endif string attributesJson, diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.cs b/src/StackExchange.Redis/Interfaces/IDatabase.cs index cf2ecafac..e26154652 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Net; +using RESPite; // ReSharper disable once CheckNamespace namespace StackExchange.Redis diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs index 7b8825e4c..a2d9b4058 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using RESPite; // ReSharper disable once CheckNamespace namespace StackExchange.Redis; @@ -80,7 +81,7 @@ Task VectorSetAddAsync( Task VectorSetSetAttributesJsonAsync( RedisKey key, RedisValue member, -#if NET7_0_OR_GREATER +#if NET8_0_OR_GREATER [StringSyntax(StringSyntaxAttribute.Json)] #endif string attributesJson, diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs index 029c7975e..c581470ca 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs @@ -5,6 +5,7 @@ using System.IO; using System.Net; using System.Threading.Tasks; +using RESPite; // ReSharper disable once CheckNamespace namespace StackExchange.Redis diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs index 8e4178fc9..fdd3d6872 100644 --- a/src/StackExchange.Redis/Interfaces/IServer.cs +++ b/src/StackExchange.Redis/Interfaces/IServer.cs @@ -814,5 +814,8 @@ internal static class IServerExtensions /// The server to simulate failure on. /// The type of failure(s) to simulate. internal static void SimulateConnectionFailure(this IServer server, SimulatedFailureType failureType) => (server as RedisServer)?.SimulateConnectionFailure(failureType); + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + internal static bool CanSimulateConnectionFailure(this IServer server) => server is not null; // this changes in v3 } } diff --git a/src/StackExchange.Redis/KeyNotification.cs b/src/StackExchange.Redis/KeyNotification.cs index 3427c4dce..08c157bc6 100644 --- a/src/StackExchange.Redis/KeyNotification.cs +++ b/src/StackExchange.Redis/KeyNotification.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; +using RESPite; using static StackExchange.Redis.KeyNotificationChannels; namespace StackExchange.Redis; @@ -37,11 +38,11 @@ public static bool TryParse(scoped in RedisChannel channel, scoped in RedisValue { // check that the prefix is valid, i.e. "__keyspace@" or "__keyevent@" var prefix = span.Slice(0, KeySpacePrefix.Length); - var hash = prefix.Hash64(); - switch (hash) + var hashCS = AsciiHash.HashCS(prefix); + switch (hashCS) { - case KeySpacePrefix.Hash when KeySpacePrefix.Is(hash, prefix): - case KeyEventPrefix.Hash when KeyEventPrefix.Is(hash, prefix): + case KeyEventPrefix.HashCS when KeyEventPrefix.IsCS(prefix, hashCS): + case KeySpacePrefix.HashCS when KeySpacePrefix.IsCS(prefix, hashCS): // check that there is *something* non-empty after the prefix, with __: as the suffix (we don't verify *what*) if (span.Slice(KeySpacePrefix.Length).IndexOf("__:"u8) > 0) { @@ -410,25 +411,22 @@ public KeyNotificationType Type if (IsKeySpace) { // then the channel contains the key, and the payload contains the event-type - var count = _value.GetByteCount(); - if (count >= KeyNotificationTypeFastHash.MinBytes & count <= KeyNotificationTypeFastHash.MaxBytes) + if (_value.TryGetSpan(out var direct)) { - if (_value.TryGetSpan(out var direct)) - { - return KeyNotificationTypeFastHash.Parse(direct); - } - else - { - Span localCopy = stackalloc byte[KeyNotificationTypeFastHash.MaxBytes]; - return KeyNotificationTypeFastHash.Parse(localCopy.Slice(0, _value.CopyTo(localCopy))); - } + return KeyNotificationTypeMetadata.Parse(direct); } - } - if (IsKeyEvent) + if (_value.GetByteCount() <= KeyNotificationTypeMetadata.BufferBytes) + { + Span localCopy = stackalloc byte[KeyNotificationTypeMetadata.BufferBytes]; + var len = _value.CopyTo(localCopy); + return KeyNotificationTypeMetadata.Parse(localCopy.Slice(0, len)); + } + } + else if (IsKeyEvent) { // then the channel contains the event-type, and the payload contains the key - return KeyNotificationTypeFastHash.Parse(ChannelSuffix); + return KeyNotificationTypeMetadata.Parse(ChannelSuffix); } return KeyNotificationType.Unknown; } @@ -442,7 +440,7 @@ public bool IsKeySpace get { var span = _channel.Span; - return span.Length >= KeySpacePrefix.Length + MinSuffixBytes && KeySpacePrefix.Is(span.Hash64(), span.Slice(0, KeySpacePrefix.Length)); + return span.Length >= KeySpacePrefix.Length + MinSuffixBytes && KeySpacePrefix.IsCS(span.Slice(0, KeySpacePrefix.Length), AsciiHash.HashCS(span)); } } @@ -454,7 +452,7 @@ public bool IsKeyEvent get { var span = _channel.Span; - return span.Length >= KeyEventPrefix.Length + MinSuffixBytes && KeyEventPrefix.Is(span.Hash64(), span.Slice(0, KeyEventPrefix.Length)); + return span.Length >= KeyEventPrefix.Length + MinSuffixBytes && KeyEventPrefix.IsCS(span.Slice(0, KeyEventPrefix.Length), AsciiHash.HashCS(span)); } } @@ -485,12 +483,12 @@ public bool KeyStartsWith(ReadOnlySpan prefix) // intentionally leading pe internal static partial class KeyNotificationChannels { - [FastHash("__keyspace@")] + [AsciiHash("__keyspace@")] internal static partial class KeySpacePrefix { } - [FastHash("__keyevent@")] + [AsciiHash("__keyevent@")] internal static partial class KeyEventPrefix { } diff --git a/src/StackExchange.Redis/KeyNotificationType.cs b/src/StackExchange.Redis/KeyNotificationType.cs index cc4c74ef1..d45d11e47 100644 --- a/src/StackExchange.Redis/KeyNotificationType.cs +++ b/src/StackExchange.Redis/KeyNotificationType.cs @@ -1,69 +1,127 @@ -namespace StackExchange.Redis; +using RESPite; + +namespace StackExchange.Redis; /// /// The type of keyspace or keyevent notification. /// +[AsciiHash(nameof(KeyNotificationTypeMetadata))] public enum KeyNotificationType { // note: initially presented alphabetically, but: new values *must* be appended, not inserted // (to preserve values of existing elements) #pragma warning disable CS1591 // docs, redundant + [AsciiHash("")] Unknown = 0, + [AsciiHash("append")] Append = 1, + [AsciiHash("copy")] Copy = 2, + [AsciiHash("del")] Del = 3, + [AsciiHash("expire")] Expire = 4, + [AsciiHash("hdel")] HDel = 5, + [AsciiHash("hexpired")] HExpired = 6, + [AsciiHash("hincrbyfloat")] HIncrByFloat = 7, + [AsciiHash("hincrby")] HIncrBy = 8, + [AsciiHash("hpersist")] HPersist = 9, + [AsciiHash("hset")] HSet = 10, + [AsciiHash("incrbyfloat")] IncrByFloat = 11, + [AsciiHash("incrby")] IncrBy = 12, + [AsciiHash("linsert")] LInsert = 13, + [AsciiHash("lpop")] LPop = 14, + [AsciiHash("lpush")] LPush = 15, + [AsciiHash("lrem")] LRem = 16, + [AsciiHash("lset")] LSet = 17, + [AsciiHash("ltrim")] LTrim = 18, + [AsciiHash("move_from")] MoveFrom = 19, + [AsciiHash("move_to")] MoveTo = 20, + [AsciiHash("persist")] Persist = 21, + [AsciiHash("rename_from")] RenameFrom = 22, + [AsciiHash("rename_to")] RenameTo = 23, + [AsciiHash("restore")] Restore = 24, + [AsciiHash("rpop")] RPop = 25, + [AsciiHash("rpush")] RPush = 26, + [AsciiHash("sadd")] SAdd = 27, + [AsciiHash("set")] Set = 28, + [AsciiHash("setrange")] SetRange = 29, + [AsciiHash("sortstore")] SortStore = 30, + [AsciiHash("srem")] SRem = 31, + [AsciiHash("spop")] SPop = 32, + [AsciiHash("xadd")] XAdd = 33, + [AsciiHash("xdel")] XDel = 34, + [AsciiHash("xgroup-createconsumer")] XGroupCreateConsumer = 35, + [AsciiHash("xgroup-create")] XGroupCreate = 36, + [AsciiHash("xgroup-delconsumer")] XGroupDelConsumer = 37, + [AsciiHash("xgroup-destroy")] XGroupDestroy = 38, + [AsciiHash("xgroup-setid")] XGroupSetId = 39, + [AsciiHash("xsetid")] XSetId = 40, + [AsciiHash("xtrim")] XTrim = 41, + [AsciiHash("zadd")] ZAdd = 42, + [AsciiHash("zdiffstore")] ZDiffStore = 43, + [AsciiHash("zinterstore")] ZInterStore = 44, + [AsciiHash("zunionstore")] ZUnionStore = 45, + [AsciiHash("zincr")] ZIncr = 46, + [AsciiHash("zrembyrank")] ZRemByRank = 47, + [AsciiHash("zrembyscore")] ZRemByScore = 48, + [AsciiHash("zrem")] ZRem = 49, // side-effect notifications + [AsciiHash("expired")] Expired = 1000, + [AsciiHash("evicted")] Evicted = 1001, + [AsciiHash("new")] New = 1002, + [AsciiHash("overwritten")] Overwritten = 1003, - TypeChanged = 1004, // type_changed + [AsciiHash("type_changed")] + TypeChanged = 1004, #pragma warning restore CS1591 // docs, redundant } diff --git a/src/StackExchange.Redis/KeyNotificationTypeFastHash.cs b/src/StackExchange.Redis/KeyNotificationTypeFastHash.cs deleted file mode 100644 index bcf08bad2..000000000 --- a/src/StackExchange.Redis/KeyNotificationTypeFastHash.cs +++ /dev/null @@ -1,413 +0,0 @@ -using System; - -namespace StackExchange.Redis; - -/// -/// Internal helper type for fast parsing of key notification types, using [FastHash]. -/// -internal static partial class KeyNotificationTypeFastHash -{ - // these are checked by KeyNotificationTypeFastHash_MinMaxBytes_ReflectsActualLengths - public const int MinBytes = 3, MaxBytes = 21; - - public static KeyNotificationType Parse(ReadOnlySpan value) - { - var hash = value.Hash64(); - return hash switch - { - append.Hash when append.Is(hash, value) => KeyNotificationType.Append, - copy.Hash when copy.Is(hash, value) => KeyNotificationType.Copy, - del.Hash when del.Is(hash, value) => KeyNotificationType.Del, - expire.Hash when expire.Is(hash, value) => KeyNotificationType.Expire, - hdel.Hash when hdel.Is(hash, value) => KeyNotificationType.HDel, - hexpired.Hash when hexpired.Is(hash, value) => KeyNotificationType.HExpired, - hincrbyfloat.Hash when hincrbyfloat.Is(hash, value) => KeyNotificationType.HIncrByFloat, - hincrby.Hash when hincrby.Is(hash, value) => KeyNotificationType.HIncrBy, - hpersist.Hash when hpersist.Is(hash, value) => KeyNotificationType.HPersist, - hset.Hash when hset.Is(hash, value) => KeyNotificationType.HSet, - incrbyfloat.Hash when incrbyfloat.Is(hash, value) => KeyNotificationType.IncrByFloat, - incrby.Hash when incrby.Is(hash, value) => KeyNotificationType.IncrBy, - linsert.Hash when linsert.Is(hash, value) => KeyNotificationType.LInsert, - lpop.Hash when lpop.Is(hash, value) => KeyNotificationType.LPop, - lpush.Hash when lpush.Is(hash, value) => KeyNotificationType.LPush, - lrem.Hash when lrem.Is(hash, value) => KeyNotificationType.LRem, - lset.Hash when lset.Is(hash, value) => KeyNotificationType.LSet, - ltrim.Hash when ltrim.Is(hash, value) => KeyNotificationType.LTrim, - move_from.Hash when move_from.Is(hash, value) => KeyNotificationType.MoveFrom, - move_to.Hash when move_to.Is(hash, value) => KeyNotificationType.MoveTo, - persist.Hash when persist.Is(hash, value) => KeyNotificationType.Persist, - rename_from.Hash when rename_from.Is(hash, value) => KeyNotificationType.RenameFrom, - rename_to.Hash when rename_to.Is(hash, value) => KeyNotificationType.RenameTo, - restore.Hash when restore.Is(hash, value) => KeyNotificationType.Restore, - rpop.Hash when rpop.Is(hash, value) => KeyNotificationType.RPop, - rpush.Hash when rpush.Is(hash, value) => KeyNotificationType.RPush, - sadd.Hash when sadd.Is(hash, value) => KeyNotificationType.SAdd, - set.Hash when set.Is(hash, value) => KeyNotificationType.Set, - setrange.Hash when setrange.Is(hash, value) => KeyNotificationType.SetRange, - sortstore.Hash when sortstore.Is(hash, value) => KeyNotificationType.SortStore, - srem.Hash when srem.Is(hash, value) => KeyNotificationType.SRem, - spop.Hash when spop.Is(hash, value) => KeyNotificationType.SPop, - xadd.Hash when xadd.Is(hash, value) => KeyNotificationType.XAdd, - xdel.Hash when xdel.Is(hash, value) => KeyNotificationType.XDel, - xgroup_createconsumer.Hash when xgroup_createconsumer.Is(hash, value) => KeyNotificationType.XGroupCreateConsumer, - xgroup_create.Hash when xgroup_create.Is(hash, value) => KeyNotificationType.XGroupCreate, - xgroup_delconsumer.Hash when xgroup_delconsumer.Is(hash, value) => KeyNotificationType.XGroupDelConsumer, - xgroup_destroy.Hash when xgroup_destroy.Is(hash, value) => KeyNotificationType.XGroupDestroy, - xgroup_setid.Hash when xgroup_setid.Is(hash, value) => KeyNotificationType.XGroupSetId, - xsetid.Hash when xsetid.Is(hash, value) => KeyNotificationType.XSetId, - xtrim.Hash when xtrim.Is(hash, value) => KeyNotificationType.XTrim, - zadd.Hash when zadd.Is(hash, value) => KeyNotificationType.ZAdd, - zdiffstore.Hash when zdiffstore.Is(hash, value) => KeyNotificationType.ZDiffStore, - zinterstore.Hash when zinterstore.Is(hash, value) => KeyNotificationType.ZInterStore, - zunionstore.Hash when zunionstore.Is(hash, value) => KeyNotificationType.ZUnionStore, - zincr.Hash when zincr.Is(hash, value) => KeyNotificationType.ZIncr, - zrembyrank.Hash when zrembyrank.Is(hash, value) => KeyNotificationType.ZRemByRank, - zrembyscore.Hash when zrembyscore.Is(hash, value) => KeyNotificationType.ZRemByScore, - zrem.Hash when zrem.Is(hash, value) => KeyNotificationType.ZRem, - expired.Hash when expired.Is(hash, value) => KeyNotificationType.Expired, - evicted.Hash when evicted.Is(hash, value) => KeyNotificationType.Evicted, - _new.Hash when _new.Is(hash, value) => KeyNotificationType.New, - overwritten.Hash when overwritten.Is(hash, value) => KeyNotificationType.Overwritten, - type_changed.Hash when type_changed.Is(hash, value) => KeyNotificationType.TypeChanged, - _ => KeyNotificationType.Unknown, - }; - } - - internal static ReadOnlySpan GetRawBytes(KeyNotificationType type) - { - return type switch - { - KeyNotificationType.Append => append.U8, - KeyNotificationType.Copy => copy.U8, - KeyNotificationType.Del => del.U8, - KeyNotificationType.Expire => expire.U8, - KeyNotificationType.HDel => hdel.U8, - KeyNotificationType.HExpired => hexpired.U8, - KeyNotificationType.HIncrByFloat => hincrbyfloat.U8, - KeyNotificationType.HIncrBy => hincrby.U8, - KeyNotificationType.HPersist => hpersist.U8, - KeyNotificationType.HSet => hset.U8, - KeyNotificationType.IncrByFloat => incrbyfloat.U8, - KeyNotificationType.IncrBy => incrby.U8, - KeyNotificationType.LInsert => linsert.U8, - KeyNotificationType.LPop => lpop.U8, - KeyNotificationType.LPush => lpush.U8, - KeyNotificationType.LRem => lrem.U8, - KeyNotificationType.LSet => lset.U8, - KeyNotificationType.LTrim => ltrim.U8, - KeyNotificationType.MoveFrom => move_from.U8, - KeyNotificationType.MoveTo => move_to.U8, - KeyNotificationType.Persist => persist.U8, - KeyNotificationType.RenameFrom => rename_from.U8, - KeyNotificationType.RenameTo => rename_to.U8, - KeyNotificationType.Restore => restore.U8, - KeyNotificationType.RPop => rpop.U8, - KeyNotificationType.RPush => rpush.U8, - KeyNotificationType.SAdd => sadd.U8, - KeyNotificationType.Set => set.U8, - KeyNotificationType.SetRange => setrange.U8, - KeyNotificationType.SortStore => sortstore.U8, - KeyNotificationType.SRem => srem.U8, - KeyNotificationType.SPop => spop.U8, - KeyNotificationType.XAdd => xadd.U8, - KeyNotificationType.XDel => xdel.U8, - KeyNotificationType.XGroupCreateConsumer => xgroup_createconsumer.U8, - KeyNotificationType.XGroupCreate => xgroup_create.U8, - KeyNotificationType.XGroupDelConsumer => xgroup_delconsumer.U8, - KeyNotificationType.XGroupDestroy => xgroup_destroy.U8, - KeyNotificationType.XGroupSetId => xgroup_setid.U8, - KeyNotificationType.XSetId => xsetid.U8, - KeyNotificationType.XTrim => xtrim.U8, - KeyNotificationType.ZAdd => zadd.U8, - KeyNotificationType.ZDiffStore => zdiffstore.U8, - KeyNotificationType.ZInterStore => zinterstore.U8, - KeyNotificationType.ZUnionStore => zunionstore.U8, - KeyNotificationType.ZIncr => zincr.U8, - KeyNotificationType.ZRemByRank => zrembyrank.U8, - KeyNotificationType.ZRemByScore => zrembyscore.U8, - KeyNotificationType.ZRem => zrem.U8, - KeyNotificationType.Expired => expired.U8, - KeyNotificationType.Evicted => evicted.U8, - KeyNotificationType.New => _new.U8, - KeyNotificationType.Overwritten => overwritten.U8, - KeyNotificationType.TypeChanged => type_changed.U8, - _ => Throw(), - }; - static ReadOnlySpan Throw() => throw new ArgumentOutOfRangeException(nameof(type)); - } - -#pragma warning disable SA1300, CS8981 - // ReSharper disable InconsistentNaming - [FastHash] - internal static partial class append - { - } - - [FastHash] - internal static partial class copy - { - } - - [FastHash] - internal static partial class del - { - } - - [FastHash] - internal static partial class expire - { - } - - [FastHash] - internal static partial class hdel - { - } - - [FastHash] - internal static partial class hexpired - { - } - - [FastHash] - internal static partial class hincrbyfloat - { - } - - [FastHash] - internal static partial class hincrby - { - } - - [FastHash] - internal static partial class hpersist - { - } - - [FastHash] - internal static partial class hset - { - } - - [FastHash] - internal static partial class incrbyfloat - { - } - - [FastHash] - internal static partial class incrby - { - } - - [FastHash] - internal static partial class linsert - { - } - - [FastHash] - internal static partial class lpop - { - } - - [FastHash] - internal static partial class lpush - { - } - - [FastHash] - internal static partial class lrem - { - } - - [FastHash] - internal static partial class lset - { - } - - [FastHash] - internal static partial class ltrim - { - } - - [FastHash("move_from")] // by default, the generator interprets underscore as hyphen - internal static partial class move_from - { - } - - [FastHash("move_to")] // by default, the generator interprets underscore as hyphen - internal static partial class move_to - { - } - - [FastHash] - internal static partial class persist - { - } - - [FastHash("rename_from")] // by default, the generator interprets underscore as hyphen - internal static partial class rename_from - { - } - - [FastHash("rename_to")] // by default, the generator interprets underscore as hyphen - internal static partial class rename_to - { - } - - [FastHash] - internal static partial class restore - { - } - - [FastHash] - internal static partial class rpop - { - } - - [FastHash] - internal static partial class rpush - { - } - - [FastHash] - internal static partial class sadd - { - } - - [FastHash] - internal static partial class set - { - } - - [FastHash] - internal static partial class setrange - { - } - - [FastHash] - internal static partial class sortstore - { - } - - [FastHash] - internal static partial class srem - { - } - - [FastHash] - internal static partial class spop - { - } - - [FastHash] - internal static partial class xadd - { - } - - [FastHash] - internal static partial class xdel - { - } - - [FastHash] // note: becomes hyphenated - internal static partial class xgroup_createconsumer - { - } - - [FastHash] // note: becomes hyphenated - internal static partial class xgroup_create - { - } - - [FastHash] // note: becomes hyphenated - internal static partial class xgroup_delconsumer - { - } - - [FastHash] // note: becomes hyphenated - internal static partial class xgroup_destroy - { - } - - [FastHash] // note: becomes hyphenated - internal static partial class xgroup_setid - { - } - - [FastHash] - internal static partial class xsetid - { - } - - [FastHash] - internal static partial class xtrim - { - } - - [FastHash] - internal static partial class zadd - { - } - - [FastHash] - internal static partial class zdiffstore - { - } - - [FastHash] - internal static partial class zinterstore - { - } - - [FastHash] - internal static partial class zunionstore - { - } - - [FastHash] - internal static partial class zincr - { - } - - [FastHash] - internal static partial class zrembyrank - { - } - - [FastHash] - internal static partial class zrembyscore - { - } - - [FastHash] - internal static partial class zrem - { - } - - [FastHash] - internal static partial class expired - { - } - - [FastHash] - internal static partial class evicted - { - } - - [FastHash("new")] - internal static partial class _new // it isn't worth making the code-gen keyword aware - { - } - - [FastHash] - internal static partial class overwritten - { - } - - [FastHash("type_changed")] // by default, the generator interprets underscore as hyphen - internal static partial class type_changed - { - } - - // ReSharper restore InconsistentNaming -#pragma warning restore SA1300, CS8981 -} diff --git a/src/StackExchange.Redis/KeyNotificationTypeMetadata.cs b/src/StackExchange.Redis/KeyNotificationTypeMetadata.cs new file mode 100644 index 000000000..594fd29c2 --- /dev/null +++ b/src/StackExchange.Redis/KeyNotificationTypeMetadata.cs @@ -0,0 +1,77 @@ +using System; +using RESPite; + +namespace StackExchange.Redis; + +/// +/// Metadata and parsing methods for KeyNotificationType. +/// +internal static partial class KeyNotificationTypeMetadata +{ + [AsciiHash] + internal static partial bool TryParse(ReadOnlySpan value, out KeyNotificationType keyNotificationType); + + public static KeyNotificationType Parse(ReadOnlySpan value) + { + return TryParse(value, out var result) ? result : KeyNotificationType.Unknown; + } + + internal static ReadOnlySpan GetRawBytes(KeyNotificationType type) => type switch + { + KeyNotificationType.Append => "append"u8, + KeyNotificationType.Copy => "copy"u8, + KeyNotificationType.Del => "del"u8, + KeyNotificationType.Expire => "expire"u8, + KeyNotificationType.HDel => "hdel"u8, + KeyNotificationType.HExpired => "hexpired"u8, + KeyNotificationType.HIncrByFloat => "hincrbyfloat"u8, + KeyNotificationType.HIncrBy => "hincrby"u8, + KeyNotificationType.HPersist => "hpersist"u8, + KeyNotificationType.HSet => "hset"u8, + KeyNotificationType.IncrByFloat => "incrbyfloat"u8, + KeyNotificationType.IncrBy => "incrby"u8, + KeyNotificationType.LInsert => "linsert"u8, + KeyNotificationType.LPop => "lpop"u8, + KeyNotificationType.LPush => "lpush"u8, + KeyNotificationType.LRem => "lrem"u8, + KeyNotificationType.LSet => "lset"u8, + KeyNotificationType.LTrim => "ltrim"u8, + KeyNotificationType.MoveFrom => "move_from"u8, + KeyNotificationType.MoveTo => "move_to"u8, + KeyNotificationType.Persist => "persist"u8, + KeyNotificationType.RenameFrom => "rename_from"u8, + KeyNotificationType.RenameTo => "rename_to"u8, + KeyNotificationType.Restore => "restore"u8, + KeyNotificationType.RPop => "rpop"u8, + KeyNotificationType.RPush => "rpush"u8, + KeyNotificationType.SAdd => "sadd"u8, + KeyNotificationType.Set => "set"u8, + KeyNotificationType.SetRange => "setrange"u8, + KeyNotificationType.SortStore => "sortstore"u8, + KeyNotificationType.SRem => "srem"u8, + KeyNotificationType.SPop => "spop"u8, + KeyNotificationType.XAdd => "xadd"u8, + KeyNotificationType.XDel => "xdel"u8, + KeyNotificationType.XGroupCreateConsumer => "xgroup-createconsumer"u8, + KeyNotificationType.XGroupCreate => "xgroup-create"u8, + KeyNotificationType.XGroupDelConsumer => "xgroup-delconsumer"u8, + KeyNotificationType.XGroupDestroy => "xgroup-destroy"u8, + KeyNotificationType.XGroupSetId => "xgroup-setid"u8, + KeyNotificationType.XSetId => "xsetid"u8, + KeyNotificationType.XTrim => "xtrim"u8, + KeyNotificationType.ZAdd => "zadd"u8, + KeyNotificationType.ZDiffStore => "zdiffstore"u8, + KeyNotificationType.ZInterStore => "zinterstore"u8, + KeyNotificationType.ZUnionStore => "zunionstore"u8, + KeyNotificationType.ZIncr => "zincr"u8, + KeyNotificationType.ZRemByRank => "zrembyrank"u8, + KeyNotificationType.ZRemByScore => "zrembyscore"u8, + KeyNotificationType.ZRem => "zrem"u8, + KeyNotificationType.Expired => "expired"u8, + KeyNotificationType.Evicted => "evicted"u8, + KeyNotificationType.New => "new"u8, + KeyNotificationType.Overwritten => "overwritten"u8, + KeyNotificationType.TypeChanged => "type_changed"u8, + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; +} diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs index ae7498401..ad4efe916 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; +using RESPite; // ReSharper disable once CheckNamespace namespace StackExchange.Redis.KeyspaceIsolation; diff --git a/src/StackExchange.Redis/LoggerExtensions.cs b/src/StackExchange.Redis/LoggerExtensions.cs index be51733ce..4a43514e4 100644 --- a/src/StackExchange.Redis/LoggerExtensions.cs +++ b/src/StackExchange.Redis/LoggerExtensions.cs @@ -34,7 +34,7 @@ internal static void LogWithThreadPoolStats(this ILogger? log, string message) _ = PerfCounterHelper.GetThreadPoolStats(out string iocp, out string worker, out string? workItems); -#if NET6_0_OR_GREATER +#if NET // use DISH when possible // similar to: var composed = $"{message}, IOCP: {iocp}, WORKER: {worker}, ..."; on net6+ var dish = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(26, 4); diff --git a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs index f4c7d3e49..0a5874c29 100644 --- a/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs +++ b/src/StackExchange.Redis/Maintenance/AzureMaintenanceEvent.cs @@ -2,9 +2,6 @@ using System.Globalization; using System.Net; using System.Threading.Tasks; -#if NETCOREAPP -using System.Buffers.Text; -#endif namespace StackExchange.Redis.Maintenance { @@ -58,7 +55,7 @@ internal AzureMaintenanceEvent(string? azureEvent) if (key.Length > 0 && value.Length > 0) { -#if NETCOREAPP +#if NET switch (key) { case var _ when key.SequenceEqual(nameof(NotificationType).AsSpan()): diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs index faf25ba44..0ffcf4256 100644 --- a/src/StackExchange.Redis/Message.cs +++ b/src/StackExchange.Redis/Message.cs @@ -262,7 +262,7 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, GeoEntry[] values) { -#if NET6_0_OR_GREATER +#if NET ArgumentNullException.ThrowIfNull(values); #else if (values == null) throw new ArgumentNullException(nameof(values)); @@ -485,7 +485,7 @@ public void Complete() internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, RedisValue[] values) { -#if NET6_0_OR_GREATER +#if NET ArgumentNullException.ThrowIfNull(values); #else if (values == null) throw new ArgumentNullException(nameof(values)); @@ -503,7 +503,7 @@ internal static Message Create(int db, CommandFlags flags, RedisCommand command, internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, in RedisKey key1, RedisValue[] values) { -#if NET6_0_OR_GREATER +#if NET ArgumentNullException.ThrowIfNull(values); #else if (values == null) throw new ArgumentNullException(nameof(values)); @@ -524,7 +524,7 @@ internal static Message Create(int db, CommandFlags flags, RedisCommand command, internal static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key0, RedisValue[] values, in RedisKey key1) { -#if NET6_0_OR_GREATER +#if NET ArgumentNullException.ThrowIfNull(values); #else if (values == null) throw new ArgumentNullException(nameof(values)); diff --git a/src/StackExchange.Redis/NullableHacks.cs b/src/StackExchange.Redis/NullableHacks.cs index 5f8969c73..4ebebf73b 100644 --- a/src/StackExchange.Redis/NullableHacks.cs +++ b/src/StackExchange.Redis/NullableHacks.cs @@ -8,7 +8,7 @@ namespace System.Diagnostics.CodeAnalysis { -#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 +#if !NET /// Specifies that null is allowed as an input even if the corresponding type disallows it. [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] internal sealed class AllowNullAttribute : Attribute { } @@ -87,7 +87,7 @@ internal sealed class DoesNotReturnIfAttribute : Attribute } #endif -#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 +#if !NET /// Specifies that the method or property will ensure that the listed field and property members have not-null values. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] internal sealed class MemberNotNullAttribute : Attribute diff --git a/src/StackExchange.Redis/PerfCounterHelper.cs b/src/StackExchange.Redis/PerfCounterHelper.cs index 8d8b6fbb0..763c86f04 100644 --- a/src/StackExchange.Redis/PerfCounterHelper.cs +++ b/src/StackExchange.Redis/PerfCounterHelper.cs @@ -22,7 +22,7 @@ internal static int GetThreadPoolStats(out string iocp, out string worker, out s iocp = $"(Busy={busyIoThreads},Free={freeIoThreads},Min={minIoThreads},Max={maxIoThreads})"; worker = $"(Busy={busyWorkerThreads},Free={freeWorkerThreads},Min={minWorkerThreads},Max={maxWorkerThreads})"; -#if NETCOREAPP +#if NET workItems = $"(Threads={ThreadPool.ThreadCount},QueuedItems={ThreadPool.PendingWorkItemCount},CompletedItems={ThreadPool.CompletedWorkItemCount},Timers={Timer.ActiveCount})"; #else workItems = null; diff --git a/src/StackExchange.Redis/PhysicalBridge.cs b/src/StackExchange.Redis/PhysicalBridge.cs index a9b5ee140..36d8268bf 100644 --- a/src/StackExchange.Redis/PhysicalBridge.cs +++ b/src/StackExchange.Redis/PhysicalBridge.cs @@ -64,7 +64,7 @@ internal sealed class PhysicalBridge : IDisposable internal long? ConnectionId => physical?.ConnectionId; -#if NETCOREAPP +#if NET private readonly SemaphoreSlim _singleWriterMutex = new(1, 1); #else private readonly MutexSlim _singleWriterMutex; @@ -357,7 +357,7 @@ public override string ToString() => { MessagesSinceLastHeartbeat = (int)(Interlocked.Read(ref operationCount) - Interlocked.Read(ref profileLastLog)), ConnectedAt = ConnectedAt, -#if NETCOREAPP +#if NET IsWriterActive = _singleWriterMutex.CurrentCount == 0, #else IsWriterActive = !_singleWriterMutex.IsAvailable, @@ -819,14 +819,14 @@ internal WriteResult WriteMessageTakingWriteLockSync(PhysicalConnection physical return WriteResult.Success; // queued counts as success } -#if NETCOREAPP +#if NET bool gotLock = false; #else LockToken token = default; #endif try { -#if NETCOREAPP +#if NET gotLock = _singleWriterMutex.Wait(0); if (!gotLock) #else @@ -843,7 +843,7 @@ internal WriteResult WriteMessageTakingWriteLockSync(PhysicalConnection physical // no backlog... try to wait with the timeout; // if we *still* can't get it: that counts as // an actual timeout -#if NETCOREAPP +#if NET gotLock = _singleWriterMutex.Wait(TimeoutMilliseconds); if (!gotLock) return TimedOutBeforeWrite(message); #else @@ -866,7 +866,7 @@ internal WriteResult WriteMessageTakingWriteLockSync(PhysicalConnection physical finally { UnmarkActiveMessage(message); -#if NETCOREAPP +#if NET if (gotLock) { _singleWriterMutex.Release(); @@ -1144,7 +1144,7 @@ private void ProcessBridgeBacklog() { // Importantly: don't assume we have a physical connection here // We are very likely to hit a state where it's not re-established or even referenced here -#if NETCOREAPP +#if NET bool gotLock = false; #else LockToken token = default; @@ -1166,7 +1166,7 @@ private void ProcessBridgeBacklog() if (_backlog.IsEmpty) return; // nothing to do // try and get the lock; if unsuccessful, retry -#if NETCOREAPP +#if NET gotLock = _singleWriterMutex.Wait(TimeoutMilliseconds); if (gotLock) break; // got the lock; now go do something with it #else @@ -1231,7 +1231,7 @@ private void ProcessBridgeBacklog() } finally { -#if NETCOREAPP +#if NET if (gotLock) { _singleWriterMutex.Release(); @@ -1282,7 +1282,7 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect } bool releaseLock = true; // fine to default to true, as it doesn't matter until token is a "success" -#if NETCOREAPP +#if NET bool gotLock = false; #else LockToken token = default; @@ -1290,7 +1290,7 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect try { // try to acquire it synchronously -#if NETCOREAPP +#if NET gotLock = _singleWriterMutex.Wait(0); if (!gotLock) #else @@ -1308,7 +1308,7 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect // no backlog... try to wait with the timeout; // if we *still* can't get it: that counts as // an actual timeout -#if NETCOREAPP +#if NET var pending = _singleWriterMutex.WaitAsync(TimeoutMilliseconds); if (pending.Status != TaskStatus.RanToCompletion) return WriteMessageTakingWriteLockAsync_Awaited(pending, physical, message); @@ -1329,7 +1329,7 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect if (!flush.IsCompletedSuccessfully) { releaseLock = false; // so we don't release prematurely -#if NETCOREAPP +#if NET return CompleteWriteAndReleaseLockAsync(flush, message); #else return CompleteWriteAndReleaseLockAsync(token, flush, message); @@ -1349,7 +1349,7 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect } finally { -#if NETCOREAPP +#if NET if (gotLock) #else if (token.Success) @@ -1359,7 +1359,7 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect if (releaseLock) { -#if NETCOREAPP +#if NET _singleWriterMutex.Release(); #else token.Dispose(); @@ -1370,7 +1370,7 @@ internal ValueTask WriteMessageTakingWriteLockAsync(PhysicalConnect } private async ValueTask WriteMessageTakingWriteLockAsync_Awaited( -#if NETCOREAPP +#if NET Task pending, #else ValueTask pending, @@ -1378,13 +1378,13 @@ private async ValueTask WriteMessageTakingWriteLockAsync_Awaited( PhysicalConnection physical, Message message) { -#if NETCOREAPP +#if NET bool gotLock = false; #endif try { -#if NETCOREAPP +#if NET gotLock = await pending.ForAwait(); if (!gotLock) return TimedOutBeforeWrite(message); #else @@ -1408,7 +1408,7 @@ private async ValueTask WriteMessageTakingWriteLockAsync_Awaited( finally { UnmarkActiveMessage(message); -#if NETCOREAPP +#if NET if (gotLock) { _singleWriterMutex.Release(); @@ -1440,7 +1440,7 @@ private async ValueTask CompleteWriteAndReleaseLockAsync( } finally { -#if NETCOREAPP +#if NET _singleWriterMutex.Release(); #endif } diff --git a/src/StackExchange.Redis/PhysicalConnection.cs b/src/StackExchange.Redis/PhysicalConnection.cs index 1af5589cf..6ba8b4cde 100644 --- a/src/StackExchange.Redis/PhysicalConnection.cs +++ b/src/StackExchange.Redis/PhysicalConnection.cs @@ -19,6 +19,7 @@ using Microsoft.Extensions.Logging; using Pipelines.Sockets.Unofficial; using Pipelines.Sockets.Unofficial.Arenas; +using RESPite; using static StackExchange.Redis.Message; namespace StackExchange.Redis @@ -825,9 +826,9 @@ internal void Write(in RedisChannel channel) [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void WriteBulkString(in RedisValue value) => WriteBulkString(value, _ioPipe?.Output); - internal static void WriteBulkString(in RedisValue value, PipeWriter? maybeNullWriter) + internal static void WriteBulkString(in RedisValue value, IBufferWriter? maybeNullWriter) { - if (maybeNullWriter is not PipeWriter writer) + if (maybeNullWriter is not { } writer) { return; // Prevent null refs during disposal } @@ -912,11 +913,11 @@ internal void WriteHeader(RedisCommand command, int arguments, CommandBytes comm internal void RecordQuit() { // don't blame redis if we fired the first shot - Thread.VolatileWrite(ref clientSentQuit, 1); + Volatile.Write(ref clientSentQuit, 1); (_ioPipe as SocketConnection)?.TrySetProtocolShutdown(PipeShutdownKind.ProtocolExitClient); } - internal static void WriteMultiBulkHeader(PipeWriter output, long count) + internal static void WriteMultiBulkHeader(IBufferWriter output, long count) { // *{count}\r\n = 3 + MaxInt32TextLen var span = output.GetSpan(3 + Format.MaxInt32TextLen); @@ -925,7 +926,7 @@ internal static void WriteMultiBulkHeader(PipeWriter output, long count) output.Advance(offset); } - internal static void WriteMultiBulkHeader(PipeWriter output, long count, ResultType type) + internal static void WriteMultiBulkHeader(IBufferWriter output, long count, ResultType type) { // *{count}\r\n = 3 + MaxInt32TextLen var span = output.GetSpan(3 + Format.MaxInt32TextLen); @@ -958,7 +959,7 @@ internal static int WriteCrlf(Span span, int offset) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static void WriteCrlf(PipeWriter writer) + internal static void WriteCrlf(IBufferWriter writer) { var span = writer.GetSpan(2); span[0] = (byte)'\r'; @@ -1122,7 +1123,7 @@ internal ValueTask FlushAsync(bool throwOnFailure, CancellationToke private static readonly ReadOnlyMemory NullBulkString = Encoding.ASCII.GetBytes("$-1\r\n"), EmptyBulkString = Encoding.ASCII.GetBytes("$0\r\n\r\n"); - private static void WriteUnifiedBlob(PipeWriter writer, byte[]? value) + private static void WriteUnifiedBlob(IBufferWriter writer, byte[]? value) { if (value == null) { @@ -1135,7 +1136,7 @@ private static void WriteUnifiedBlob(PipeWriter writer, byte[]? value) } } - private static void WriteUnifiedSpan(PipeWriter writer, ReadOnlySpan value) + private static void WriteUnifiedSpan(IBufferWriter writer, ReadOnlySpan value) { // ${len}\r\n = 3 + MaxInt32TextLen // {value}\r\n = 2 + value.Length @@ -1229,9 +1230,9 @@ internal static byte ToHexNibble(int value) return value < 10 ? (byte)('0' + value) : (byte)('a' - 10 + value); } - internal static void WriteUnifiedPrefixedString(PipeWriter? maybeNullWriter, byte[]? prefix, string? value) + internal static void WriteUnifiedPrefixedString(IBufferWriter? maybeNullWriter, byte[]? prefix, string? value) { - if (maybeNullWriter is not PipeWriter writer) + if (maybeNullWriter is not { } writer) { return; // Prevent null refs during disposal } @@ -1284,7 +1285,7 @@ internal static Encoder GetPerThreadEncoder() return encoder; } - internal static unsafe void WriteRaw(PipeWriter writer, string value, int expectedLength) + internal static unsafe void WriteRaw(IBufferWriter writer, string value, int expectedLength) { const int MaxQuickEncodeSize = 512; @@ -1368,7 +1369,7 @@ private static void WriteUnifiedPrefixedBlob(PipeWriter? maybeNullWriter, byte[] } } - private static void WriteUnifiedInt64(PipeWriter writer, long value) + private static void WriteUnifiedInt64(IBufferWriter writer, long value) { // note from specification: A client sends to the Redis server a RESP Array consisting of just Bulk Strings. // (i.e. we can't just send ":123\r\n", we need to send "$3\r\n123\r\n" @@ -1382,7 +1383,7 @@ private static void WriteUnifiedInt64(PipeWriter writer, long value) writer.Advance(bytes); } - private static void WriteUnifiedUInt64(PipeWriter writer, ulong value) + private static void WriteUnifiedUInt64(IBufferWriter writer, ulong value) { // note from specification: A client sends to the Redis server a RESP Array consisting of just Bulk Strings. // (i.e. we can't just send ":123\r\n", we need to send "$3\r\n123\r\n" @@ -1400,7 +1401,7 @@ private static void WriteUnifiedUInt64(PipeWriter writer, ulong value) writer.Advance(offset); } - private static void WriteUnifiedDouble(PipeWriter writer, double value) + private static void WriteUnifiedDouble(IBufferWriter writer, double value) { #if NET8_0_OR_GREATER Span valueSpan = stackalloc byte[Format.MaxDoubleTextLen]; @@ -1421,7 +1422,7 @@ private static void WriteUnifiedDouble(PipeWriter writer, double value) #endif } - internal static void WriteInteger(PipeWriter writer, long value) + internal static void WriteInteger(IBufferWriter writer, long value) { // note: client should never write integer; only server does this // :{asc}\r\n = MaxInt64TextLen + 3 @@ -1575,7 +1576,7 @@ public ConnectionStatus GetStatus() return ConfigurationOptions.CreatePfxUserCertificateCallback(certificatePath, password, storageFlags); } -#if NET5_0_OR_GREATER +#if NET certificatePath = Environment.GetEnvironmentVariable("SERedis_ClientCertPemPath"); if (!string.IsNullOrEmpty(certificatePath) && File.Exists(certificatePath)) { @@ -1634,7 +1635,7 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock { try { -#if NETCOREAPP3_1_OR_GREATER +#if NET var configOptions = config.SslClientAuthenticationOptions?.Invoke(host); if (configOptions is not null) { @@ -1691,19 +1692,36 @@ internal async ValueTask ConnectedAsync(Socket? socket, ILogger? log, Sock } } - private enum PushKind + internal enum PushKind { + [AsciiHash("")] None, + [AsciiHash("message")] Message, + [AsciiHash("pmessage")] PMessage, + [AsciiHash("smessage")] SMessage, + [AsciiHash("subscribe")] Subscribe, + [AsciiHash("psubscribe")] PSubscribe, + [AsciiHash("ssubscribe")] SSubscribe, + [AsciiHash("unsubscribe")] Unsubscribe, + [AsciiHash("punsubscribe")] PUnsubscribe, + [AsciiHash("sunsubscribe")] SUnsubscribe, } + + internal static partial class PushKindMetadata + { + [AsciiHash] + internal static partial bool TryParse(ReadOnlySpan value, out PushKind result); + } + private PushKind GetPushKind(in Sequence result, out RedisChannel channel) { var len = result.Length; @@ -1714,63 +1732,40 @@ private PushKind GetPushKind(in Sequence result, out RedisChannel cha return PushKind.None; } - const int MAX_LEN = 16; - Debug.Assert(MAX_LEN >= Enumerable.Max( - [ - PushMessage.Length, PushPMessage.Length, PushSMessage.Length, - PushSubscribe.Length, PushPSubscribe.Length, PushSSubscribe.Length, - PushUnsubscribe.Length, PushPUnsubscribe.Length, PushSUnsubscribe.Length, - ])); - ref readonly RawResult pushKind = ref result[0]; - var multiSegmentPayload = pushKind.Payload; - if (multiSegmentPayload.Length <= MAX_LEN) + if (result[0].TryParse(PushKindMetadata.TryParse, out PushKind kind) && kind is not PushKind.None) { - var span = multiSegmentPayload.IsSingleSegment - ? multiSegmentPayload.First.Span - : CopyTo(stackalloc byte[MAX_LEN], multiSegmentPayload); - - var hash = FastHash.Hash64(span); RedisChannel.RedisChannelOptions channelOptions = RedisChannel.RedisChannelOptions.None; - PushKind kind; - switch (hash) + switch (kind) { - case PushMessage.Hash when PushMessage.Is(hash, span) & len >= 3: - kind = PushKind.Message; + case PushKind.Message when len >= 3: break; - case PushPMessage.Hash when PushPMessage.Is(hash, span) & len >= 4: + case PushKind.PMessage when len >= 4: channelOptions = RedisChannel.RedisChannelOptions.Pattern; - kind = PushKind.PMessage; break; - case PushSMessage.Hash when PushSMessage.Is(hash, span) & len >= 3: + case PushKind.SMessage when len >= 3: channelOptions = RedisChannel.RedisChannelOptions.Sharded; - kind = PushKind.SMessage; break; - case PushSubscribe.Hash when PushSubscribe.Is(hash, span): - kind = PushKind.Subscribe; + case PushKind.Subscribe: break; - case PushPSubscribe.Hash when PushPSubscribe.Is(hash, span): + case PushKind.PSubscribe: channelOptions = RedisChannel.RedisChannelOptions.Pattern; - kind = PushKind.PSubscribe; break; - case PushSSubscribe.Hash when PushSSubscribe.Is(hash, span): + case PushKind.SSubscribe: channelOptions = RedisChannel.RedisChannelOptions.Sharded; - kind = PushKind.SSubscribe; break; - case PushUnsubscribe.Hash when PushUnsubscribe.Is(hash, span): - kind = PushKind.Unsubscribe; + case PushKind.Unsubscribe: break; - case PushPUnsubscribe.Hash when PushPUnsubscribe.Is(hash, span): + case PushKind.PUnsubscribe: channelOptions = RedisChannel.RedisChannelOptions.Pattern; - kind = PushKind.PUnsubscribe; break; - case PushSUnsubscribe.Hash when PushSUnsubscribe.Is(hash, span): + case PushKind.SUnsubscribe: channelOptions = RedisChannel.RedisChannelOptions.Sharded; - kind = PushKind.SUnsubscribe; break; default: kind = PushKind.None; break; } + if (kind != PushKind.None) { // the channel is always the second element @@ -1780,41 +1775,8 @@ private PushKind GetPushKind(in Sequence result, out RedisChannel cha } channel = default; return PushKind.None; - - static ReadOnlySpan CopyTo(Span target, in ReadOnlySequence source) - { - source.CopyTo(target); - return target.Slice(0, (int)source.Length); - } } - [FastHash("message")] - private static partial class PushMessage { } - - [FastHash("pmessage")] - private static partial class PushPMessage { } - - [FastHash("smessage")] - private static partial class PushSMessage { } - - [FastHash("subscribe")] - private static partial class PushSubscribe { } - - [FastHash("psubscribe")] - private static partial class PushPSubscribe { } - - [FastHash("ssubscribe")] - private static partial class PushSSubscribe { } - - [FastHash("unsubscribe")] - private static partial class PushUnsubscribe { } - - [FastHash("punsubscribe")] - private static partial class PushPUnsubscribe { } - - [FastHash("sunsubscribe")] - private static partial class PushSUnsubscribe { } - private void MatchResult(in RawResult result) { // check to see if it could be an out-of-band pubsub message @@ -1902,7 +1864,7 @@ private void MatchResult(in RawResult result) // counter-intuitively, the only server we *know* already knows the new route is: // the outgoing server, since it had to change to MIGRATING etc; the new INCOMING server // knows, but *we don't know who that is*, and other nodes: aren't guaranteed to know (yet) - muxer.DefaultSubscriber.ResubscribeToServer(subscription, subscriptionChannel, server, cause: PushSUnsubscribe.Text); + muxer.DefaultSubscriber.ResubscribeToServer(subscription, subscriptionChannel, server, cause: "sunsubscribe"); } return; // and STOP PROCESSING; unsolicited } diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index ab058de62..797632e41 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,17 @@ #nullable enable +override StackExchange.Redis.LCSMatchResult.LCSMatch.Equals(object? obj) -> bool +override StackExchange.Redis.LCSMatchResult.LCSMatch.GetHashCode() -> int +override StackExchange.Redis.LCSMatchResult.LCSMatch.ToString() -> string! +override StackExchange.Redis.LCSMatchResult.LCSPosition.Equals(object? obj) -> bool +override StackExchange.Redis.LCSMatchResult.LCSPosition.GetHashCode() -> int +override StackExchange.Redis.LCSMatchResult.LCSPosition.ToString() -> string! +StackExchange.Redis.LCSMatchResult.LCSMatch.Equals(in StackExchange.Redis.LCSMatchResult.LCSMatch other) -> bool +StackExchange.Redis.LCSMatchResult.LCSMatch.First.get -> StackExchange.Redis.LCSMatchResult.LCSPosition +StackExchange.Redis.LCSMatchResult.LCSMatch.Second.get -> StackExchange.Redis.LCSMatchResult.LCSPosition +StackExchange.Redis.LCSMatchResult.LCSPosition +StackExchange.Redis.LCSMatchResult.LCSPosition.End.get -> long +StackExchange.Redis.LCSMatchResult.LCSPosition.Equals(in StackExchange.Redis.LCSMatchResult.LCSPosition other) -> bool +StackExchange.Redis.LCSMatchResult.LCSPosition.LCSPosition() -> void +StackExchange.Redis.LCSMatchResult.LCSPosition.LCSPosition(long start, long end) -> void +StackExchange.Redis.LCSMatchResult.LCSPosition.Start.get -> long +StackExchange.Redis.RedisType.VectorSet = 8 -> StackExchange.Redis.RedisType diff --git a/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt deleted file mode 100644 index fae4f65ce..000000000 --- a/src/StackExchange.Redis/PublicAPI/net8.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1,4 +0,0 @@ -StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.get -> System.Func? -StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.set -> void -System.Runtime.CompilerServices.IsExternalInit (forwarded, contained in System.Runtime) -StackExchange.Redis.ConfigurationOptions.SetUserPemCertificate(string! userCertificatePath, string? userKeyPath = null) -> void \ No newline at end of file diff --git a/src/StackExchange.Redis/PublicAPI/netcoreapp3.1/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/netcoreapp3.1/PublicAPI.Shipped.txt deleted file mode 100644 index 194e1b51b..000000000 --- a/src/StackExchange.Redis/PublicAPI/netcoreapp3.1/PublicAPI.Shipped.txt +++ /dev/null @@ -1,2 +0,0 @@ -StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.get -> System.Func? -StackExchange.Redis.ConfigurationOptions.SslClientAuthenticationOptions.set -> void \ No newline at end of file diff --git a/src/StackExchange.Redis/RawResult.cs b/src/StackExchange.Redis/RawResult.cs index e1c91b74e..2b2b3989a 100644 --- a/src/StackExchange.Redis/RawResult.cs +++ b/src/StackExchange.Redis/RawResult.cs @@ -12,6 +12,31 @@ internal readonly struct RawResult internal int ItemsCount => (int)_items.Length; + public delegate bool ScalarParser(scoped ReadOnlySpan span, out T value); + + internal bool TryParse(ScalarParser parser, out T value) + => _payload.IsSingleSegment ? parser(_payload.First.Span, out value) : TryParseSlow(parser, out value); + + private bool TryParseSlow(ScalarParser parser, out T value) + { + // linearize a multi-segment payload into a single span for parsing + const int MAX_STACK = 64; + var len = checked((int)_payload.Length); + byte[]? lease = null; + try + { + Span span = + (len <= MAX_STACK ? stackalloc byte[MAX_STACK] : (lease = ArrayPool.Shared.Rent(len))) + .Slice(0, len); + _payload.CopyTo(span); + return parser(span, out value); + } + finally + { + if (lease is not null) ArrayPool.Shared.Return(lease); + } + } + private readonly ReadOnlySequence _payload; internal ReadOnlySequence Payload => _payload; @@ -416,7 +441,7 @@ private static GeoPosition AsGeoPosition(in Sequence coords) s = Format.GetString(Payload.First.Span); return Resp3Type == ResultType.VerbatimString ? GetVerbatimString(s, out verbatimPrefix) : s; } -#if NET6_0_OR_GREATER +#if NET // use system-provided sequence decoder return Encoding.UTF8.GetString(in _payload); #else diff --git a/src/StackExchange.Redis/RedisChannel.cs b/src/StackExchange.Redis/RedisChannel.cs index 889525bd2..c3acf1493 100644 --- a/src/StackExchange.Redis/RedisChannel.cs +++ b/src/StackExchange.Redis/RedisChannel.cs @@ -269,7 +269,7 @@ private static ReadOnlySpan AppendDatabase(Span target, int? databas #pragma warning disable RS0027 public static RedisChannel KeyEvent(KeyNotificationType type, int? database = null) #pragma warning restore RS0027 - => KeyEvent(KeyNotificationTypeFastHash.GetRawBytes(type), database); + => KeyEvent(KeyNotificationTypeMetadata.GetRawBytes(type), database); /// /// Create an event-notification channel for a given event type, optionally in a specified database. diff --git a/src/StackExchange.Redis/RedisKey.cs b/src/StackExchange.Redis/RedisKey.cs index e18e0fb7c..2d192c244 100644 --- a/src/StackExchange.Redis/RedisKey.cs +++ b/src/StackExchange.Redis/RedisKey.cs @@ -419,7 +419,7 @@ internal int CopyTo(Span destination) case string s: if (s.Length != 0) { -#if NETCOREAPP +#if NET written += Encoding.UTF8.GetBytes(s, destination); #else unsafe diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index be79b3267..9a8f54971 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -39,31 +39,6 @@ public static readonly CommandBytes id = "id"; } - internal static partial class CommonRepliesHash - { -#pragma warning disable CS8981, SA1300, SA1134 // forgive naming - // ReSharper disable InconsistentNaming - [FastHash] internal static partial class length { } - [FastHash] internal static partial class radix_tree_keys { } - [FastHash] internal static partial class radix_tree_nodes { } - [FastHash] internal static partial class last_generated_id { } - [FastHash] internal static partial class max_deleted_entry_id { } - [FastHash] internal static partial class entries_added { } - [FastHash] internal static partial class recorded_first_entry_id { } - [FastHash] internal static partial class idmp_duration { } - [FastHash] internal static partial class idmp_maxsize { } - [FastHash] internal static partial class pids_tracked { } - [FastHash] internal static partial class first_entry { } - [FastHash] internal static partial class last_entry { } - [FastHash] internal static partial class groups { } - [FastHash] internal static partial class iids_tracked { } - [FastHash] internal static partial class iids_added { } - [FastHash] internal static partial class iids_duplicates { } - - // ReSharper restore InconsistentNaming -#pragma warning restore CS8981, SA1300, SA1134 // forgive naming - } - internal static class RedisLiterals { // unlike primary commands, these do not get altered by the command-map; we may as diff --git a/src/StackExchange.Redis/RedisValue.cs b/src/StackExchange.Redis/RedisValue.cs index 46228a912..5b8bfe58f 100644 --- a/src/StackExchange.Redis/RedisValue.cs +++ b/src/StackExchange.Redis/RedisValue.cs @@ -3,6 +3,7 @@ using System.Buffers.Text; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -1367,5 +1368,31 @@ public bool StartsWith(ReadOnlySpan value) return false; } } + + // used by the toy server to smuggle weird vectors; on their own heads... not used by SE.Redis itself + // (these additions just formalize the usage in the older server code) + internal bool TryGetForeign([NotNullWhen(true)] out T? value, out int index, out int length) + where T : class + { + if (typeof(T) != typeof(string) && typeof(T) != typeof(byte[]) && DirectObject is T found) + { + index = 0; + length = checked((int)DirectOverlappedBits64); + value = found; + return true; + } + value = null; + index = 0; + length = 0; + return false; + } + + internal static RedisValue CreateForeign(T obj, int offset, int count) where T : class + { + // non-zero offset isn't supported until v3, left here for API parity + if (typeof(T) == typeof(string) || typeof(T) == typeof(byte[]) || offset != 0) Throw(); + return new RedisValue(obj, count); + static void Throw() => throw new InvalidOperationException(); + } } } diff --git a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs index b10e5fd93..f8f3bed72 100644 --- a/src/StackExchange.Redis/ResultProcessor.VectorSets.cs +++ b/src/StackExchange.Redis/ResultProcessor.VectorSets.cs @@ -73,49 +73,36 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var iter = result.GetItems().GetEnumerator(); while (iter.MoveNext()) { - ref readonly RawResult key = ref iter.Current; + if (!iter.Current.TryParse(VectorSetInfoFieldMetadata.TryParse, out VectorSetInfoField field)) + field = VectorSetInfoField.Unknown; + if (!iter.MoveNext()) break; ref readonly RawResult value = ref iter.Current; - var len = key.Payload.Length; - var keyHash = key.Payload.Hash64(); - switch (key.Payload.Length) + switch (field) { - case size.Length when size.Is(keyHash, key) && value.TryGetInt64(out var i64): + case VectorSetInfoField.Size when value.TryGetInt64(out var i64): resultSize = i64; break; - case vset_uid.Length when vset_uid.Is(keyHash, key) && value.TryGetInt64(out var i64): + case VectorSetInfoField.VsetUid when value.TryGetInt64(out var i64): vsetUid = i64; break; - case max_level.Length when max_level.Is(keyHash, key) && value.TryGetInt64(out var i64): + case VectorSetInfoField.MaxLevel when value.TryGetInt64(out var i64): maxLevel = checked((int)i64); break; - case vector_dim.Length - when vector_dim.Is(keyHash, key) && value.TryGetInt64(out var i64): + case VectorSetInfoField.VectorDim when value.TryGetInt64(out var i64): vectorDim = checked((int)i64); break; - case quant_type.Length when quant_type.Is(keyHash, key): - var qHash = value.Payload.Hash64(); - switch (value.Payload.Length) - { - case bin.Length when bin.Is(qHash, value): - quantType = VectorSetQuantization.Binary; - break; - case f32.Length when f32.Is(qHash, value): - quantType = VectorSetQuantization.None; - break; - case int8.Length when int8.Is(qHash, value): - quantType = VectorSetQuantization.Int8; - break; - default: - quantTypeRaw = value.GetString(); - quantType = VectorSetQuantization.Unknown; - break; - } - + case VectorSetInfoField.QuantType + when value.TryParse(VectorSetQuantizationMetadata.TryParse, out VectorSetQuantization quantTypeValue) + && quantTypeValue is not VectorSetQuantization.Unknown: + quantType = quantTypeValue; break; - case hnsw_max_node_uid.Length - when hnsw_max_node_uid.Is(keyHash, key) && value.TryGetInt64(out var i64): + case VectorSetInfoField.QuantType: + quantTypeRaw = value.GetString(); + quantType = VectorSetQuantization.Unknown; + break; + case VectorSetInfoField.HnswMaxNodeUid when value.TryGetInt64(out var i64): hnswMaxNodeUid = i64; break; } @@ -129,21 +116,5 @@ when hnsw_max_node_uid.Is(keyHash, key) && value.TryGetInt64(out var i64): return false; } - -#pragma warning disable CS8981, SA1134, SA1300, SA1303, SA1502 - // ReSharper disable InconsistentNaming - to better represent expected literals - // ReSharper disable IdentifierTypo - [FastHash] private static partial class bin { } - [FastHash] private static partial class f32 { } - [FastHash] private static partial class int8 { } - [FastHash] private static partial class size { } - [FastHash] private static partial class vset_uid { } - [FastHash] private static partial class max_level { } - [FastHash] private static partial class quant_type { } - [FastHash] private static partial class vector_dim { } - [FastHash] private static partial class hnsw_max_node_uid { } - // ReSharper restore InconsistentNaming - // ReSharper restore IdentifierTypo -#pragma warning restore CS8981, SA1134, SA1300, SA1303, SA1502 } } diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs index 0562d7a4c..fc5c3d5b4 100644 --- a/src/StackExchange.Redis/ResultProcessor.cs +++ b/src/StackExchange.Redis/ResultProcessor.cs @@ -1898,14 +1898,14 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes { switch (result.Resp2TypeArray) { - case ResultType.Array: - SetResult(message, Parse(result)); + case ResultType.Array when TryParse(result, out var value): + SetResult(message, value); return true; } return false; } - private static LCSMatchResult Parse(in RawResult result) + private static bool TryParse(in RawResult result, out LCSMatchResult value) { var topItems = result.GetItems(); var matches = new LCSMatchResult.LCSMatch[topItems[1].GetItems().Length]; @@ -1915,14 +1915,35 @@ private static LCSMatchResult Parse(in RawResult result) { var matchItems = match.GetItems(); - matches[i++] = new LCSMatchResult.LCSMatch( - firstStringIndex: (long)matchItems[0].GetItems()[0].AsRedisValue(), - secondStringIndex: (long)matchItems[1].GetItems()[0].AsRedisValue(), - length: (long)matchItems[2].AsRedisValue()); + if (TryReadPosition(matchItems[0], out var first) + && TryReadPosition(matchItems[1], out var second) + && matchItems[2].TryGetInt64(out var length)) + { + matches[i++] = new LCSMatchResult.LCSMatch(first, second, length); + } + else + { + value = default; + return false; + } } var len = (long)topItems[3].AsRedisValue(); - return new LCSMatchResult(matches, len); + value = new LCSMatchResult(matches, len); + return true; + } + + private static bool TryReadPosition(in RawResult raw, out LCSMatchResult.LCSPosition position) + { + // Expecting a 2-element array: [start, end] + if (raw.Resp2TypeArray is ResultType.Array && raw.ItemsCount >= 2 + && raw[0].TryGetInt64(out var start) && raw[1].TryGetInt64(out var end)) + { + position = new LCSMatchResult.LCSPosition(start, end); + return true; + } + position = default; + return false; } } @@ -2554,59 +2575,60 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes var iter = arr.GetEnumerator(); for (int i = 0; i < max; i++) { - ref RawResult key = ref iter.GetNext(), value = ref iter.GetNext(); - if (key.Payload.Length > CommandBytes.MaxLength) continue; - var hash = key.Payload.Hash64(); - switch (hash) + if (!iter.GetNext().TryParse(StreamInfoFieldMetadata.TryParse, out StreamInfoField field)) + field = StreamInfoField.Unknown; + ref RawResult value = ref iter.GetNext(); + + switch (field) { - case CommonRepliesHash.length.Hash when CommonRepliesHash.length.Is(hash, key): + case StreamInfoField.Length: if (!value.TryGetInt64(out length)) return false; break; - case CommonRepliesHash.radix_tree_keys.Hash when CommonRepliesHash.radix_tree_keys.Is(hash, key): + case StreamInfoField.RadixTreeKeys: if (!value.TryGetInt64(out radixTreeKeys)) return false; break; - case CommonRepliesHash.radix_tree_nodes.Hash when CommonRepliesHash.radix_tree_nodes.Is(hash, key): + case StreamInfoField.RadixTreeNodes: if (!value.TryGetInt64(out radixTreeNodes)) return false; break; - case CommonRepliesHash.groups.Hash when CommonRepliesHash.groups.Is(hash, key): + case StreamInfoField.Groups: if (!value.TryGetInt64(out groups)) return false; break; - case CommonRepliesHash.last_generated_id.Hash when CommonRepliesHash.last_generated_id.Is(hash, key): + case StreamInfoField.LastGeneratedId: lastGeneratedId = value.AsRedisValue(); break; - case CommonRepliesHash.first_entry.Hash when CommonRepliesHash.first_entry.Is(hash, key): + case StreamInfoField.FirstEntry: firstEntry = ParseRedisStreamEntry(value); break; - case CommonRepliesHash.last_entry.Hash when CommonRepliesHash.last_entry.Is(hash, key): + case StreamInfoField.LastEntry: lastEntry = ParseRedisStreamEntry(value); break; // 7.0 - case CommonRepliesHash.max_deleted_entry_id.Hash when CommonRepliesHash.max_deleted_entry_id.Is(hash, key): + case StreamInfoField.MaxDeletedEntryId: maxDeletedEntryId = value.AsRedisValue(); break; - case CommonRepliesHash.recorded_first_entry_id.Hash when CommonRepliesHash.recorded_first_entry_id.Is(hash, key): + case StreamInfoField.RecordedFirstEntryId: recordedFirstEntryId = value.AsRedisValue(); break; - case CommonRepliesHash.entries_added.Hash when CommonRepliesHash.entries_added.Is(hash, key): + case StreamInfoField.EntriesAdded: if (!value.TryGetInt64(out entriesAdded)) return false; break; // 8.6 - case CommonRepliesHash.idmp_duration.Hash when CommonRepliesHash.idmp_duration.Is(hash, key): + case StreamInfoField.IdmpDuration: if (!value.TryGetInt64(out idmpDuration)) return false; break; - case CommonRepliesHash.idmp_maxsize.Hash when CommonRepliesHash.idmp_maxsize.Is(hash, key): + case StreamInfoField.IdmpMaxsize: if (!value.TryGetInt64(out idmpMaxsize)) return false; break; - case CommonRepliesHash.pids_tracked.Hash when CommonRepliesHash.pids_tracked.Is(hash, key): + case StreamInfoField.PidsTracked: if (!value.TryGetInt64(out pidsTracked)) return false; break; - case CommonRepliesHash.iids_tracked.Hash when CommonRepliesHash.iids_tracked.Is(hash, key): + case StreamInfoField.IidsTracked: if (!value.TryGetInt64(out iidsTracked)) return false; break; - case CommonRepliesHash.iids_added.Hash when CommonRepliesHash.iids_added.Is(hash, key): + case StreamInfoField.IidsAdded: if (!value.TryGetInt64(out iidsAdded)) return false; break; - case CommonRepliesHash.iids_duplicates.Hash when CommonRepliesHash.iids_duplicates.Is(hash, key): + case StreamInfoField.IidsDuplicates: if (!value.TryGetInt64(out iidsDuplicates)) return false; break; } diff --git a/src/StackExchange.Redis/ServerSelectionStrategy.cs b/src/StackExchange.Redis/ServerSelectionStrategy.cs index 7d599724a..48ba32a77 100644 --- a/src/StackExchange.Redis/ServerSelectionStrategy.cs +++ b/src/StackExchange.Redis/ServerSelectionStrategy.cs @@ -49,7 +49,7 @@ internal sealed class ServerSelectionStrategy private readonly ConnectionMultiplexer? multiplexer; private int anyStartOffset = SharedRandom.Next(); // initialize to a random value so routing isn't uniform - #if NET6_0_OR_GREATER + #if NET private static Random SharedRandom => Random.Shared; #else private static Random SharedRandom { get; } = new(); diff --git a/src/StackExchange.Redis/SkipLocalsInit.cs b/src/StackExchange.Redis/SkipLocalsInit.cs index 353b00142..494a37a57 100644 --- a/src/StackExchange.Redis/SkipLocalsInit.cs +++ b/src/StackExchange.Redis/SkipLocalsInit.cs @@ -4,7 +4,7 @@ // the most relevant to us, so we have audited that no "stackalloc" use expects the buffers to be zero'd initially [module:System.Runtime.CompilerServices.SkipLocalsInit] -#if !NET5_0_OR_GREATER +#if !NET // when not available, we can spoof it in a private type namespace System.Runtime.CompilerServices { diff --git a/src/StackExchange.Redis/StackExchange.Redis.csproj b/src/StackExchange.Redis/StackExchange.Redis.csproj index 2c2e7702a..4bff8ce53 100644 --- a/src/StackExchange.Redis/StackExchange.Redis.csproj +++ b/src/StackExchange.Redis/StackExchange.Redis.csproj @@ -2,7 +2,7 @@ enable - net461;netstandard2.0;net472;netcoreapp3.1;net6.0;net8.0 + net461;netstandard2.0;net472;net6.0;net8.0;net10.0 High performance Redis client, incorporating both synchronous and asynchronous usage. StackExchange.Redis StackExchange.Redis @@ -41,10 +41,11 @@ - - - - + + + + + @@ -54,6 +55,8 @@ - + + + \ No newline at end of file diff --git a/src/StackExchange.Redis/StreamConfiguration.cs b/src/StackExchange.Redis/StreamConfiguration.cs index 71bbe483e..46e5d0ba3 100644 --- a/src/StackExchange.Redis/StreamConfiguration.cs +++ b/src/StackExchange.Redis/StreamConfiguration.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using RESPite; namespace StackExchange.Redis; diff --git a/src/StackExchange.Redis/StreamIdempotentId.cs b/src/StackExchange.Redis/StreamIdempotentId.cs index 601890d1f..1ad331eda 100644 --- a/src/StackExchange.Redis/StreamIdempotentId.cs +++ b/src/StackExchange.Redis/StreamIdempotentId.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using RESPite; namespace StackExchange.Redis; diff --git a/src/StackExchange.Redis/StreamInfoField.cs b/src/StackExchange.Redis/StreamInfoField.cs new file mode 100644 index 000000000..5429dec5e --- /dev/null +++ b/src/StackExchange.Redis/StreamInfoField.cs @@ -0,0 +1,121 @@ +using System; +using RESPite; + +namespace StackExchange.Redis; + +/// +/// Fields that can appear in a XINFO STREAM response. +/// +internal enum StreamInfoField +{ + /// + /// Unknown or unrecognized field. + /// + [AsciiHash("")] + Unknown = 0, + + /// + /// The number of entries in the stream. + /// + [AsciiHash("length")] + Length, + + /// + /// The number of radix tree keys. + /// + [AsciiHash("radix-tree-keys")] + RadixTreeKeys, + + /// + /// The number of radix tree nodes. + /// + [AsciiHash("radix-tree-nodes")] + RadixTreeNodes, + + /// + /// The number of consumer groups. + /// + [AsciiHash("groups")] + Groups, + + /// + /// The last generated ID. + /// + [AsciiHash("last-generated-id")] + LastGeneratedId, + + /// + /// The first entry in the stream. + /// + [AsciiHash("first-entry")] + FirstEntry, + + /// + /// The last entry in the stream. + /// + [AsciiHash("last-entry")] + LastEntry, + + /// + /// The maximum deleted entry ID (Redis 7.0+). + /// + [AsciiHash("max-deleted-entry-id")] + MaxDeletedEntryId, + + /// + /// The recorded first entry ID (Redis 7.0+). + /// + [AsciiHash("recorded-first-entry-id")] + RecordedFirstEntryId, + + /// + /// The total number of entries added (Redis 7.0+). + /// + [AsciiHash("entries-added")] + EntriesAdded, + + /// + /// IDMP duration in seconds (Redis 8.6+). + /// + [AsciiHash("idmp-duration")] + IdmpDuration, + + /// + /// IDMP max size (Redis 8.6+). + /// + [AsciiHash("idmp-maxsize")] + IdmpMaxsize, + + /// + /// Number of PIDs tracked (Redis 8.6+). + /// + [AsciiHash("pids-tracked")] + PidsTracked, + + /// + /// Number of IIDs tracked (Redis 8.6+). + /// + [AsciiHash("iids-tracked")] + IidsTracked, + + /// + /// Number of IIDs added (Redis 8.6+). + /// + [AsciiHash("iids-added")] + IidsAdded, + + /// + /// Number of duplicate IIDs (Redis 8.6+). + /// + [AsciiHash("iids-duplicates")] + IidsDuplicates, +} + +/// +/// Metadata and parsing methods for StreamInfoField. +/// +internal static partial class StreamInfoFieldMetadata +{ + [AsciiHash] + internal static partial bool TryParse(ReadOnlySpan value, out StreamInfoField field); +} diff --git a/src/StackExchange.Redis/TaskExtensions.cs b/src/StackExchange.Redis/TaskExtensions.cs index a0994a0b6..5b5684da1 100644 --- a/src/StackExchange.Redis/TaskExtensions.cs +++ b/src/StackExchange.Redis/TaskExtensions.cs @@ -25,7 +25,7 @@ internal static Task ObserveErrors(this Task task) return task; } -#if !NET6_0_OR_GREATER +#if !NET // suboptimal polyfill version of the .NET 6+ API, but reasonable for light use internal static Task WaitAsync(this Task task, CancellationToken cancellationToken) { diff --git a/src/StackExchange.Redis/ValueCondition.cs b/src/StackExchange.Redis/ValueCondition.cs index d61a2f00e..c5cf4bd5a 100644 --- a/src/StackExchange.Redis/ValueCondition.cs +++ b/src/StackExchange.Redis/ValueCondition.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO.Hashing; using System.Runtime.CompilerServices; +using RESPite; namespace StackExchange.Redis; diff --git a/src/StackExchange.Redis/VectorSetAddRequest.cs b/src/StackExchange.Redis/VectorSetAddRequest.cs index 987118c09..8262d4750 100644 --- a/src/StackExchange.Redis/VectorSetAddRequest.cs +++ b/src/StackExchange.Redis/VectorSetAddRequest.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using RESPite; namespace StackExchange.Redis; @@ -23,7 +24,7 @@ internal VectorSetAddRequest() public static VectorSetAddRequest Member( RedisValue element, ReadOnlyMemory values, -#if NET7_0_OR_GREATER +#if NET8_0_OR_GREATER [StringSyntax(StringSyntaxAttribute.Json)] #endif string? attributesJson = null) diff --git a/src/StackExchange.Redis/VectorSetInfo.cs b/src/StackExchange.Redis/VectorSetInfo.cs index c9277eae5..afbc3fece 100644 --- a/src/StackExchange.Redis/VectorSetInfo.cs +++ b/src/StackExchange.Redis/VectorSetInfo.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using RESPite; namespace StackExchange.Redis; diff --git a/src/StackExchange.Redis/VectorSetInfoField.cs b/src/StackExchange.Redis/VectorSetInfoField.cs new file mode 100644 index 000000000..1ed9266be --- /dev/null +++ b/src/StackExchange.Redis/VectorSetInfoField.cs @@ -0,0 +1,61 @@ +using System; +using RESPite; + +namespace StackExchange.Redis; + +/// +/// Represents fields in a VSET.INFO response. +/// +internal enum VectorSetInfoField +{ + /// + /// Unknown or unrecognized field. + /// + [AsciiHash("")] + Unknown = 0, + + /// + /// The size field. + /// + [AsciiHash("size")] + Size, + + /// + /// The vset-uid field. + /// + [AsciiHash("vset-uid")] + VsetUid, + + /// + /// The max-level field. + /// + [AsciiHash("max-level")] + MaxLevel, + + /// + /// The vector-dim field. + /// + [AsciiHash("vector-dim")] + VectorDim, + + /// + /// The quant-type field. + /// + [AsciiHash("quant-type")] + QuantType, + + /// + /// The hnsw-max-node-uid field. + /// + [AsciiHash("hnsw-max-node-uid")] + HnswMaxNodeUid, +} + +/// +/// Metadata and parsing methods for VectorSetInfoField. +/// +internal static partial class VectorSetInfoFieldMetadata +{ + [AsciiHash] + internal static partial bool TryParse(ReadOnlySpan value, out VectorSetInfoField field); +} diff --git a/src/StackExchange.Redis/VectorSetLink.cs b/src/StackExchange.Redis/VectorSetLink.cs index c18e8a95f..5d58a8d7f 100644 --- a/src/StackExchange.Redis/VectorSetLink.cs +++ b/src/StackExchange.Redis/VectorSetLink.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using RESPite; namespace StackExchange.Redis; diff --git a/src/StackExchange.Redis/VectorSetQuantization.cs b/src/StackExchange.Redis/VectorSetQuantization.cs index d78f4b34b..c7c5bf2e7 100644 --- a/src/StackExchange.Redis/VectorSetQuantization.cs +++ b/src/StackExchange.Redis/VectorSetQuantization.cs @@ -1,4 +1,6 @@ +using System; using System.Diagnostics.CodeAnalysis; +using RESPite; namespace StackExchange.Redis; @@ -11,20 +13,33 @@ public enum VectorSetQuantization /// /// Unknown or unrecognized quantization type. /// + [AsciiHash("")] Unknown = 0, /// /// No quantization (full precision). This maps to "NOQUANT" or "f32". /// + [AsciiHash("f32")] None = 1, /// /// 8-bit integer quantization (default). This maps to "Q8" or "int8". /// + [AsciiHash("int8")] Int8 = 2, /// /// Binary quantization. This maps to "BIN" or "bin". /// + [AsciiHash("bin")] Binary = 3, } + +/// +/// Metadata and parsing methods for VectorSetQuantization. +/// +internal static partial class VectorSetQuantizationMetadata +{ + [AsciiHash] + internal static partial bool TryParse(ReadOnlySpan value, out VectorSetQuantization quantization); +} diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs index d0c0fd4cc..1343fd3f1 100644 --- a/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchRequest.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using RESPite; using VsimFlags = StackExchange.Redis.VectorSetSimilaritySearchMessage.VsimFlags; namespace StackExchange.Redis; diff --git a/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs b/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs index fd912898b..c87e04bc1 100644 --- a/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs +++ b/src/StackExchange.Redis/VectorSetSimilaritySearchResult.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using RESPite; namespace StackExchange.Redis; @@ -22,7 +23,7 @@ public readonly struct VectorSetSimilaritySearchResult(RedisValue member, double /// /// The JSON attributes associated with the member when WITHATTRIBS is used, null otherwise. /// -#if NET7_0_OR_GREATER +#if NET8_0_OR_GREATER [StringSyntax(StringSyntaxAttribute.Json)] #endif public string? AttributesJson { get; } = attributesJson; diff --git a/tests/RESPite.Tests/CycleBufferTests.cs b/tests/RESPite.Tests/CycleBufferTests.cs new file mode 100644 index 000000000..14dcd6f13 --- /dev/null +++ b/tests/RESPite.Tests/CycleBufferTests.cs @@ -0,0 +1,87 @@ +using System; +using RESPite.Buffers; +using Xunit; + +namespace RESPite.Tests; + +public class CycleBufferTests() +{ + public enum Timing + { + CommitEverythingBeforeDiscard, + CommitAfterFirstDiscard, + } + + [Theory] + [InlineData(Timing.CommitEverythingBeforeDiscard)] + [InlineData(Timing.CommitAfterFirstDiscard)] + public void CanDiscardSafely(Timing timing) + { + var buffer = CycleBuffer.Create(); + buffer.GetUncommittedSpan(10).Slice(0, 10).Fill(1); + Assert.Equal(0, buffer.GetCommittedLength()); + buffer.Commit(10); + Assert.Equal(10, buffer.GetCommittedLength()); + buffer.GetUncommittedSpan(15).Slice(0, 15).Fill(2); + + if (timing is Timing.CommitEverythingBeforeDiscard) buffer.Commit(15); + + Assert.True(buffer.TryGetFirstCommittedSpan(1, out var committed)); + switch (timing) + { + case Timing.CommitEverythingBeforeDiscard: + Assert.Equal(25, committed.Length); + for (int i = 0; i < 10; i++) + { + if (1 != committed[i]) + { + Assert.Fail($"committed[{i}]={committed[i]}"); + } + } + for (int i = 10; i < 25; i++) + { + if (2 != committed[i]) + { + Assert.Fail($"committed[{i}]={committed[i]}"); + } + } + break; + case Timing.CommitAfterFirstDiscard: + Assert.Equal(10, committed.Length); + for (int i = 0; i < committed.Length; i++) + { + if (1 != committed[i]) + { + Assert.Fail($"committed[{i}]={committed[i]}"); + } + } + break; + } + + buffer.DiscardCommitted(committed.Length); + Assert.Equal(0, buffer.GetCommittedLength()); + + // now (simulating concurrent) we commit the second span + if (timing is Timing.CommitAfterFirstDiscard) + { + buffer.Commit(15); + + Assert.Equal(15, buffer.GetCommittedLength()); + + // and we should be able to read those bytes + Assert.True(buffer.TryGetFirstCommittedSpan(1, out committed)); + Assert.Equal(15, committed.Length); + for (int i = 0; i < committed.Length; i++) + { + if (2 != committed[i]) + { + Assert.Fail($"committed[{i}]={committed[i]}"); + } + } + + buffer.DiscardCommitted(committed.Length); + } + + Assert.Equal(0, buffer.GetCommittedLength()); + } +} diff --git a/tests/RESPite.Tests/RESPite.Tests.csproj b/tests/RESPite.Tests/RESPite.Tests.csproj new file mode 100644 index 000000000..eb40683a7 --- /dev/null +++ b/tests/RESPite.Tests/RESPite.Tests.csproj @@ -0,0 +1,22 @@ + + + + net481;net8.0;net10.0 + enable + false + true + Exe + + + + + + + + + + + + + + diff --git a/tests/RESPite.Tests/RespReaderTests.cs b/tests/RESPite.Tests/RespReaderTests.cs new file mode 100644 index 000000000..b80c840f4 --- /dev/null +++ b/tests/RESPite.Tests/RespReaderTests.cs @@ -0,0 +1,1077 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using RESPite.Internal; +using RESPite.Messages; +using Xunit; +using Xunit.Sdk; +using Xunit.v3; + +namespace RESPite.Tests; + +public class RespReaderTests(ITestOutputHelper logger) +{ + public readonly struct RespPayload(string label, ReadOnlySequence payload, byte[] expected, bool? outOfBand, int count) + { + public override string ToString() => Label; + public string Label { get; } = label; + public ReadOnlySequence PayloadRaw { get; } = payload; + public int Length { get; } = CheckPayload(payload, expected, outOfBand, count); + private static int CheckPayload(scoped in ReadOnlySequence actual, byte[] expected, bool? outOfBand, int count) + { + Assert.Equal(expected.LongLength, actual.Length); + var pool = ArrayPool.Shared.Rent(expected.Length); + actual.CopyTo(pool); + bool isSame = pool.AsSpan(0, expected.Length).SequenceEqual(expected); + ArrayPool.Shared.Return(pool); + Assert.True(isSame, "Data mismatch"); + + // verify that the data exactly passes frame-scanning + long totalBytes = 0; + RespReader reader = new(actual); + while (count > 0) + { + RespScanState state = default; + Assert.True(state.TryRead(ref reader, out long bytesRead)); + totalBytes += bytesRead; + Assert.True(state.IsComplete, nameof(state.IsComplete)); + if (outOfBand.HasValue) + { + if (outOfBand.Value) + { + Assert.Equal(RespPrefix.Push, state.Prefix); + } + else + { + Assert.NotEqual(RespPrefix.Push, state.Prefix); + } + } + count--; + } + Assert.Equal(expected.Length, totalBytes); + reader.DemandEnd(); + return expected.Length; + } + + public RespReader Reader() => new(PayloadRaw); + } + + public sealed class RespAttribute : DataAttribute + { + public override bool SupportsDiscoveryEnumeration() => true; + + private readonly object _value; + public bool OutOfBand { get; set; } = false; + + private bool? EffectiveOutOfBand => Count == 1 ? OutOfBand : default(bool?); + public int Count { get; set; } = 1; + + public RespAttribute(string value) => _value = value; + public RespAttribute(params string[] values) => _value = values; + + public override ValueTask> GetData(MethodInfo testMethod, DisposalTracker disposalTracker) + => new(GetData(testMethod).ToArray()); + + public IEnumerable GetData(MethodInfo testMethod) + { + switch (_value) + { + case string s: + foreach (var item in GetVariants(s, EffectiveOutOfBand, Count)) + { + yield return new TheoryDataRow(item); + } + break; + case string[] arr: + foreach (string s in arr) + { + foreach (var item in GetVariants(s, EffectiveOutOfBand, Count)) + { + yield return new TheoryDataRow(item); + } + } + break; + } + } + + private static IEnumerable GetVariants(string value, bool? outOfBand, int count) + { + var bytes = Encoding.UTF8.GetBytes(value); + + // all in one + yield return new("Right-sized", new(bytes), bytes, outOfBand, count); + + var bigger = new byte[bytes.Length + 4]; + bytes.CopyTo(bigger.AsSpan(2, bytes.Length)); + bigger.AsSpan(0, 2).Fill(0xFF); + bigger.AsSpan(bytes.Length + 2, 2).Fill(0xFF); + + // all in one, oversized + yield return new("Oversized", new(bigger, 2, bytes.Length), bytes, outOfBand, count); + + // two-chunks + for (int i = 0; i <= bytes.Length; i++) + { + int offset = 2 + i; + var left = new Segment(new ReadOnlyMemory(bigger, 0, offset), null); + var right = new Segment(new ReadOnlyMemory(bigger, offset, bigger.Length - offset), left); + yield return new($"Split:{i}", new ReadOnlySequence(left, 2, right, right.Length - 2), bytes, outOfBand, count); + } + + // N-chunks + Segment head = new(new(bytes, 0, 1), null), tail = head; + for (int i = 1; i < bytes.Length; i++) + { + tail = new(new(bytes, i, 1), tail); + } + yield return new("Chunk-per-byte", new(head, 0, tail, 1), bytes, outOfBand, count); + } + } + + [Theory, Resp("$3\r\n128\r\n")] + public void HandleSplitTokens(RespPayload payload) + { + RespReader reader = payload.Reader(); + RespScanState scan = default; + bool readResult = scan.TryRead(ref reader, out _); + logger.WriteLine(scan.ToString()); + Assert.Equal(payload.Length, reader.BytesConsumed); + Assert.True(readResult); + } + + // the examples from https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md + [Theory, Resp("$11\r\nhello world\r\n", "$?\r\n;6\r\nhello \r\n;5\r\nworld\r\n;0\r\n")] + public void BlobString(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.BulkString); + Assert.True(reader.Is("hello world"u8)); + Assert.Equal("hello world", reader.ReadString()); + Assert.Equal("hello world", reader.ReadString(out var prefix)); + Assert.Equal("", prefix); +#if NET8_0_OR_GREATER + Assert.Equal("hello world", reader.ParseChars()); + /* interestingly, string does not implement IUtf8SpanParsable + Assert.Equal("hello world", reader.ParseBytes()); + */ +#endif + reader.DemandEnd(); + } + + [Theory, Resp("$0\r\n\r\n", "$?\r\n;0\r\n")] + public void EmptyBlobString(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.BulkString); + Assert.True(reader.Is(""u8)); + Assert.Equal("", reader.ReadString()); + reader.DemandEnd(); + } + + [Theory, Resp("+hello world\r\n")] + public void SimpleString(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.SimpleString); + Assert.True(reader.Is("hello world"u8)); + Assert.Equal("hello world", reader.ReadString()); + Assert.Equal("hello world", reader.ReadString(out var prefix)); + Assert.Equal("", prefix); + reader.DemandEnd(); + } + + [Theory, Resp("-ERR this is the error description\r\n")] + public void SimpleError_ImplicitErrors(RespPayload payload) + { + var ex = Assert.Throws(() => + { + var reader = payload.Reader(); + reader.MoveNext(); + }); + Assert.Equal("ERR this is the error description", ex.Message); + } + + [Theory, Resp("-ERR this is the error description\r\n")] + public void SimpleError_Careful(RespPayload payload) + { + var reader = payload.Reader(); + Assert.True(reader.TryMoveNext(checkError: false)); + Assert.Equal(RespPrefix.SimpleError, reader.Prefix); + Assert.True(reader.Is("ERR this is the error description"u8)); + Assert.Equal("ERR this is the error description", reader.ReadString()); + reader.DemandEnd(); + } + + [Theory, Resp(":1234\r\n")] + public void Number(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Integer); + Assert.True(reader.Is("1234"u8)); + Assert.Equal("1234", reader.ReadString()); + Assert.Equal(1234, reader.ReadInt32()); + Assert.Equal(1234D, reader.ReadDouble()); + Assert.Equal(1234M, reader.ReadDecimal()); +#if NET8_0_OR_GREATER + Assert.Equal(1234, reader.ParseChars()); + Assert.Equal(1234D, reader.ParseChars()); + Assert.Equal(1234M, reader.ParseChars()); + Assert.Equal(1234, reader.ParseBytes()); + Assert.Equal(1234D, reader.ParseBytes()); + Assert.Equal(1234M, reader.ParseBytes()); +#endif + reader.DemandEnd(); + } + + [Theory, Resp("_\r\n")] + public void Null(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Null); + Assert.True(reader.Is(""u8)); + Assert.Null(reader.ReadString()); + reader.DemandEnd(); + } + + [Theory, Resp("$-1\r\n")] + public void NullString(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.BulkString); + Assert.True(reader.IsNull); + Assert.Null(reader.ReadString()); + Assert.Equal(0, reader.ScalarLength()); + Assert.True(reader.Is(""u8)); + Assert.True(reader.ScalarIsEmpty()); + + var iterator = reader.ScalarChunks(); + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + reader.DemandEnd(); + } + + [Theory, Resp(",1.23\r\n")] + public void Double(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Double); + Assert.True(reader.Is("1.23"u8)); + Assert.Equal("1.23", reader.ReadString()); + Assert.Equal(1.23D, reader.ReadDouble()); + Assert.Equal(1.23M, reader.ReadDecimal()); + reader.DemandEnd(); + } + + [Theory, Resp(":10\r\n")] + public void Integer_Simple(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Integer); + Assert.True(reader.Is("10"u8)); + Assert.Equal("10", reader.ReadString()); + Assert.Equal(10, reader.ReadInt32()); + Assert.Equal(10D, reader.ReadDouble()); + Assert.Equal(10M, reader.ReadDecimal()); + reader.DemandEnd(); + } + + [Theory, Resp(",10\r\n")] + public void Double_Simple(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Double); + Assert.True(reader.Is("10"u8)); + Assert.Equal("10", reader.ReadString()); + Assert.Equal(10, reader.ReadInt32()); + Assert.Equal(10D, reader.ReadDouble()); + Assert.Equal(10M, reader.ReadDecimal()); + reader.DemandEnd(); + } + + [Theory, Resp(",inf\r\n")] + public void Double_Infinity(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Double); + Assert.True(reader.Is("inf"u8)); + Assert.Equal("inf", reader.ReadString()); + var val = reader.ReadDouble(); + Assert.True(double.IsInfinity(val)); + Assert.True(double.IsPositiveInfinity(val)); + reader.DemandEnd(); + } + + [Theory, Resp(",+inf\r\n")] + public void Double_PosInfinity(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Double); + Assert.True(reader.Is("+inf"u8)); + Assert.Equal("+inf", reader.ReadString()); + var val = reader.ReadDouble(); + Assert.True(double.IsInfinity(val)); + Assert.True(double.IsPositiveInfinity(val)); + reader.DemandEnd(); + } + + [Theory, Resp(",-inf\r\n")] + public void Double_NegInfinity(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Double); + Assert.True(reader.Is("-inf"u8)); + Assert.Equal("-inf", reader.ReadString()); + var val = reader.ReadDouble(); + Assert.True(double.IsInfinity(val)); + Assert.True(double.IsNegativeInfinity(val)); + reader.DemandEnd(); + } + + [Theory, Resp(",nan\r\n")] + public void Double_NaN(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Double); + Assert.True(reader.Is("nan"u8)); + Assert.Equal("nan", reader.ReadString()); + var val = reader.ReadDouble(); + Assert.True(double.IsNaN(val)); + reader.DemandEnd(); + } + + [Theory, Resp("#t\r\n")] + public void Boolean_T(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Boolean); + Assert.True(reader.ReadBoolean()); + reader.DemandEnd(); + } + + [Theory, Resp("#f\r\n")] + public void Boolean_F(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Boolean); + Assert.False(reader.ReadBoolean()); + reader.DemandEnd(); + } + + [Theory, Resp(":1\r\n")] + public void Boolean_1(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Integer); + Assert.True(reader.ReadBoolean()); + reader.DemandEnd(); + } + + [Theory, Resp(":0\r\n")] + public void Boolean_0(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Integer); + Assert.False(reader.ReadBoolean()); + reader.DemandEnd(); + } + + [Theory, Resp("!21\r\nSYNTAX invalid syntax\r\n", "!?\r\n;6\r\nSYNTAX\r\n;15\r\n invalid syntax\r\n;0\r\n")] + public void BlobError_ImplicitErrors(RespPayload payload) + { + var ex = Assert.Throws(() => + { + var reader = payload.Reader(); + reader.MoveNext(); + }); + Assert.Equal("SYNTAX invalid syntax", ex.Message); + } + + [Theory, Resp("!21\r\nSYNTAX invalid syntax\r\n", "!?\r\n;6\r\nSYNTAX\r\n;15\r\n invalid syntax\r\n;0\r\n")] + public void BlobError_Careful(RespPayload payload) + { + var reader = payload.Reader(); + Assert.True(reader.TryMoveNext(checkError: false)); + Assert.Equal(RespPrefix.BulkError, reader.Prefix); + Assert.True(reader.Is("SYNTAX invalid syntax"u8)); + Assert.Equal("SYNTAX invalid syntax", reader.ReadString()); + reader.DemandEnd(); + } + + [Theory, Resp("=15\r\ntxt:Some string\r\n", "=?\r\n;4\r\ntxt:\r\n;11\r\nSome string\r\n;0\r\n")] + public void VerbatimString(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.VerbatimString); + Assert.Equal("Some string", reader.ReadString()); + Assert.Equal("Some string", reader.ReadString(out var prefix)); + Assert.Equal("txt", prefix); + + Assert.Equal("Some string", reader.ReadString(out var prefix2)); + Assert.Same(prefix, prefix2); // check prefix recognized and reuse literal + reader.DemandEnd(); + } + + [Theory, Resp("(3492890328409238509324850943850943825024385\r\n")] + public void BigIntegers(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.BigInteger); + Assert.Equal("3492890328409238509324850943850943825024385", reader.ReadString()); +#if NET8_0_OR_GREATER + var actual = reader.ParseChars(chars => BigInteger.Parse(chars, CultureInfo.InvariantCulture)); + + var expected = BigInteger.Parse("3492890328409238509324850943850943825024385"); + Assert.Equal(expected, actual); +#endif + } + + [Theory, Resp("*3\r\n:1\r\n:2\r\n:3\r\n", "*?\r\n:1\r\n:2\r\n:3\r\n.\r\n")] + public void Array(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + Assert.Equal(3, reader.AggregateLength()); + var iterator = reader.AggregateChildren(); + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(1, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(2, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(3, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext(RespPrefix.Integer)); + iterator.MovePast(out reader); + reader.DemandEnd(); + + reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + int[] arr = new int[reader.AggregateLength()]; + int i = 0; +#pragma warning disable SERDBG // warning about .Current vs .Value + foreach (var sub in reader.AggregateChildren()) +#pragma warning restore SERDBG + { + sub.Demand(RespPrefix.Integer); + arr[i++] = sub.ReadInt32(); + sub.DemandEnd(); + } + iterator.MovePast(out reader); + reader.DemandEnd(); + + Assert.Equal([1, 2, 3], arr); + } + + [Theory, Resp("*-1\r\n")] + public void NullArray(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + Assert.True(reader.IsNull); + Assert.Equal(0, reader.AggregateLength()); + var iterator = reader.AggregateChildren(); + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + reader.DemandEnd(); + } + + [Theory, Resp("*2\r\n*3\r\n:1\r\n$5\r\nhello\r\n:2\r\n#f\r\n", "*?\r\n*?\r\n:1\r\n$5\r\nhello\r\n:2\r\n.\r\n#f\r\n.\r\n")] + public void NestedArray(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + + Assert.Equal(2, reader.AggregateLength()); + + var iterator = reader.AggregateChildren(); + Assert.True(iterator.MoveNext(RespPrefix.Array)); + + Assert.Equal(3, iterator.Value.AggregateLength()); + var subIterator = iterator.Value.AggregateChildren(); + Assert.True(subIterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(1, subIterator.Value.ReadInt64()); + subIterator.Value.DemandEnd(); + + Assert.True(subIterator.MoveNext(RespPrefix.BulkString)); + Assert.True(subIterator.Value.Is("hello"u8)); + subIterator.Value.DemandEnd(); + + Assert.True(subIterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(2, subIterator.Value.ReadInt64()); + subIterator.Value.DemandEnd(); + + Assert.False(subIterator.MoveNext()); + + Assert.True(iterator.MoveNext(RespPrefix.Boolean)); + Assert.False(iterator.Value.ReadBoolean()); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + + reader.DemandEnd(); + } + + [Theory, Resp("%2\r\n+first\r\n:1\r\n+second\r\n:2\r\n", "%?\r\n+first\r\n:1\r\n+second\r\n:2\r\n.\r\n")] + public void Map(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Map); + + Assert.Equal(4, reader.AggregateLength()); + + var iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("first".AsSpan())); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(1, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("second"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(2, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + + iterator.MovePast(out reader); + reader.DemandEnd(); + } + + [Theory, Resp("~5\r\n+orange\r\n+apple\r\n#t\r\n:100\r\n:999\r\n", "~?\r\n+orange\r\n+apple\r\n#t\r\n:100\r\n:999\r\n.\r\n")] + public void Set(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Set); + + Assert.Equal(5, reader.AggregateLength()); + + var iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("orange".AsSpan())); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("apple"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Boolean)); + Assert.True(iterator.Value.ReadBoolean()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(100, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(999, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + + iterator.MovePast(out reader); + reader.DemandEnd(); + } + + private sealed class TestAttributeReader : RespAttributeReader<(int Count, int Ttl, decimal A, decimal B)> + { + public override void Read(ref RespReader reader, ref (int Count, int Ttl, decimal A, decimal B) value) + { + value.Count += ReadKeyValuePairs(ref reader, ref value); + } + private TestAttributeReader() { } + public static readonly TestAttributeReader Instance = new(); + public static (int Count, int Ttl, decimal A, decimal B) Zero = (0, 0, 0, 0); + public override bool ReadKeyValuePair(scoped ReadOnlySpan key, ref RespReader reader, ref (int Count, int Ttl, decimal A, decimal B) value) + { + if (key.SequenceEqual("ttl"u8) && reader.IsScalar) + { + value.Ttl = reader.ReadInt32(); + } + else if (key.SequenceEqual("key-popularity"u8) && reader.IsAggregate) + { + ReadKeyValuePairs(ref reader, ref value); // recurse to process a/b below + } + else if (key.SequenceEqual("a"u8) && reader.IsScalar) + { + value.A = reader.ReadDecimal(); + } + else if (key.SequenceEqual("b"u8) && reader.IsScalar) + { + value.B = reader.ReadDecimal(); + } + else + { + return false; // not recognized + } + return true; // recognized + } + } + + [Theory, Resp( + "|1\r\n+key-popularity\r\n%2\r\n$1\r\na\r\n,0.1923\r\n$1\r\nb\r\n,0.0012\r\n*2\r\n:2039123\r\n:9543892\r\n", + "|1\r\n+key-popularity\r\n%2\r\n$1\r\na\r\n,0.1923\r\n$1\r\nb\r\n,0.0012\r\n*?\r\n:2039123\r\n:9543892\r\n.\r\n")] + public void AttributeRoot(RespPayload payload) + { + // ignore the attribute data + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + Assert.Equal(2, reader.AggregateLength()); + var iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(2039123, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(9543892, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + reader.DemandEnd(); + + // process the attribute data + var state = TestAttributeReader.Zero; + reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array, TestAttributeReader.Instance, ref state); + Assert.Equal(1, state.Count); + Assert.Equal(0.1923M, state.A); + Assert.Equal(0.0012M, state.B); + state = TestAttributeReader.Zero; + + Assert.Equal(2, reader.AggregateLength()); + iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer, TestAttributeReader.Instance, ref state)); + Assert.Equal(2039123, iterator.Value.ReadInt32()); + Assert.Equal(0, state.Count); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer, TestAttributeReader.Instance, ref state)); + Assert.Equal(9543892, iterator.Value.ReadInt32()); + Assert.Equal(0, state.Count); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + reader.DemandEnd(); + } + + [Theory, Resp("*3\r\n:1\r\n:2\r\n|1\r\n+ttl\r\n:3600\r\n:3\r\n", "*?\r\n:1\r\n:2\r\n|1\r\n+ttl\r\n:3600\r\n:3\r\n.\r\n")] + public void AttributeInner(RespPayload payload) + { + // ignore the attribute data + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + Assert.Equal(3, reader.AggregateLength()); + var iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(1, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(2, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer)); + Assert.Equal(3, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + reader.DemandEnd(); + + // process the attribute data + var state = TestAttributeReader.Zero; + reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array, TestAttributeReader.Instance, ref state); + Assert.Equal(0, state.Count); + Assert.Equal(3, reader.AggregateLength()); + iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer, TestAttributeReader.Instance, ref state)); + Assert.Equal(0, state.Count); + Assert.Equal(1, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer, TestAttributeReader.Instance, ref state)); + Assert.Equal(0, state.Count); + Assert.Equal(2, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.Integer, TestAttributeReader.Instance, ref state)); + Assert.Equal(1, state.Count); + Assert.Equal(3600, state.Ttl); + state = TestAttributeReader.Zero; // reset + Assert.Equal(3, iterator.Value.ReadInt32()); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNextRaw(TestAttributeReader.Instance, ref state)); + Assert.Equal(0, state.Count); + iterator.MovePast(out reader); + reader.DemandEnd(); + } + + [Theory, Resp(">3\r\n+message\r\n+somechannel\r\n+this is the message\r\n", OutOfBand = true)] + public void Push(RespPayload payload) + { + // ignore the attribute data + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Push); + Assert.Equal(3, reader.AggregateLength()); + var iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("message"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("somechannel"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("this is the message"u8)); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + reader.DemandEnd(); + } + + [Theory, Resp(">3\r\n+message\r\n+somechannel\r\n+this is the message\r\n$9\r\nGet-Reply\r\n", Count = 2)] + public void PushThenGetReply(RespPayload payload) + { + // ignore the attribute data + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Push); + Assert.Equal(3, reader.AggregateLength()); + var iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("message"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("somechannel"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("this is the message"u8)); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + + reader.MoveNext(RespPrefix.BulkString); + Assert.True(reader.Is("Get-Reply"u8)); + reader.DemandEnd(); + } + + [Theory, Resp("$9\r\nGet-Reply\r\n>3\r\n+message\r\n+somechannel\r\n+this is the message\r\n", Count = 2)] + public void GetReplyThenPush(RespPayload payload) + { + // ignore the attribute data + var reader = payload.Reader(); + + reader.MoveNext(RespPrefix.BulkString); + Assert.True(reader.Is("Get-Reply"u8)); + + reader.MoveNext(RespPrefix.Push); + Assert.Equal(3, reader.AggregateLength()); + var iterator = reader.AggregateChildren(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("message"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("somechannel"u8)); + iterator.Value.DemandEnd(); + + Assert.True(iterator.MoveNext(RespPrefix.SimpleString)); + Assert.True(iterator.Value.Is("this is the message"u8)); + iterator.Value.DemandEnd(); + + Assert.False(iterator.MoveNext()); + iterator.MovePast(out reader); + + reader.DemandEnd(); + } + + [Theory, Resp("*0\r\n$4\r\npass\r\n", "*1\r\n+ok\r\n$4\r\npass\r\n", "*-1\r\n$4\r\npass\r\n", "*?\r\n.\r\n$4\r\npass\r\n", Count = 2)] + public void ArrayThenString(RespPayload payload) + { + var reader = payload.Reader(); + Assert.True(reader.TryMoveNext(RespPrefix.Array)); + reader.SkipChildren(); + + Assert.True(reader.TryMoveNext(RespPrefix.BulkString)); + Assert.True(reader.Is("pass"u8)); + + reader.DemandEnd(); + + // and the same using child iterator + reader = payload.Reader(); + Assert.True(reader.TryMoveNext(RespPrefix.Array)); + var iterator = reader.AggregateChildren(); + iterator.MovePast(out reader); + + Assert.True(reader.TryMoveNext(RespPrefix.BulkString)); + Assert.True(reader.Is("pass"u8)); + + reader.DemandEnd(); + } + + // Tests for ScalarLengthIs + [Theory, Resp("$-1\r\n")] // null bulk string + public void ScalarLengthIs_NullBulkString(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.BulkString); + Assert.True(reader.ScalarLengthIs(0)); + Assert.False(reader.ScalarLengthIs(1)); + Assert.False(reader.ScalarLengthIs(5)); + reader.DemandEnd(); + } + + // Note: Null prefix (_\r\n) is tested in the existing Null() test above + [Theory, Resp("$0\r\n\r\n", "$?\r\n;0\r\n")] // empty scalar (simple and streaming) + public void ScalarLengthIs_Empty(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.BulkString); + Assert.True(reader.ScalarLengthIs(0)); + Assert.False(reader.ScalarLengthIs(1)); + Assert.False(reader.ScalarLengthIs(5)); + reader.DemandEnd(); + } + + [Theory, Resp("$5\r\nhello\r\n")] // simple scalar + public void ScalarLengthIs_Simple(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.BulkString); + Assert.True(reader.ScalarLengthIs(5)); + Assert.False(reader.ScalarLengthIs(0)); + Assert.False(reader.ScalarLengthIs(4)); + Assert.False(reader.ScalarLengthIs(6)); + Assert.False(reader.ScalarLengthIs(10)); + reader.DemandEnd(); + } + + [Theory, Resp("$?\r\n;2\r\nhe\r\n;3\r\nllo\r\n;0\r\n")] // streaming scalar + public void ScalarLengthIs_Streaming(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.BulkString); + Assert.True(reader.ScalarLengthIs(5)); + Assert.False(reader.ScalarLengthIs(0)); + Assert.False(reader.ScalarLengthIs(2)); // short-circuit: stops early + Assert.False(reader.ScalarLengthIs(3)); // short-circuit: stops early + Assert.False(reader.ScalarLengthIs(6)); // short-circuit: stops early + Assert.False(reader.ScalarLengthIs(10)); // short-circuit: stops early + reader.DemandEnd(); + } + + [Fact] // streaming scalar - verify short-circuiting stops before reading malformed data + public void ScalarLengthIs_Streaming_ShortCircuits() + { + // Streaming scalar: 2 bytes "he", then 3 bytes "llo", then 1 byte "X", then MALFORMED + // To check if length == N, we need to read N+1 bytes to verify there isn't more + // So malformed data must come AFTER the N+1 threshold + var data = "$?\r\n;2\r\nhe\r\n;3\r\nllo\r\n;1\r\nX\r\nMALFORMED"u8.ToArray(); + var reader = new RespReader(new ReadOnlySequence(data)); + reader.MoveNext(RespPrefix.BulkString); + + // When checking length < 6, we read up to 6 bytes (he+llo+X), see 6 > expected, stop + Assert.False(reader.ScalarLengthIs(0)); // reads "he" (2), 2 > 0, stops before "llo" + Assert.False(reader.ScalarLengthIs(2)); // reads "he" (2), "llo" (5 total), 5 > 2, stops before "X" + Assert.False(reader.ScalarLengthIs(4)); // reads "he" (2), "llo" (5 total), 5 > 4, stops before "X" + Assert.False(reader.ScalarLengthIs(5)); // reads "he" (2), "llo" (5), "X" (6 total), 6 > 5, stops before MALFORMED + + // All of the above should succeed without hitting MALFORMED because we short-circuit + } + + [Fact] // streaming scalar - verify TryGetSpan fails and Buffer works correctly + public void StreamingScalar_BufferPartial() + { + // 32 bytes total: "abcdefgh" (8) + "ijklmnop" (8) + "qrstuvwx" (8) + "yz012345" (8) + "6789" (4) + var data = "$?\r\n;8\r\nabcdefgh\r\n;8\r\nijklmnop\r\n;8\r\nqrstuvwx\r\n;8\r\nyz012345\r\n;4\r\n6789\r\n;0\r\n"u8.ToArray(); + var reader = new RespReader(new ReadOnlySequence(data)); + reader.MoveNext(RespPrefix.BulkString); + + Assert.True(reader.IsScalar); + Assert.False(reader.TryGetSpan(out _)); // Should fail - data is non-contiguous + + // Buffer should fetch just the first 16 bytes + Span buffer = stackalloc byte[16]; + var buffered = reader.Buffer(buffer); + Assert.Equal(16, buffered.Length); + Assert.True(buffered.SequenceEqual("abcdefghijklmnop"u8)); + } + + [Theory, Resp("+hello\r\n")] // simple string + public void ScalarLengthIs_SimpleString(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.SimpleString); + Assert.True(reader.ScalarLengthIs(5)); + Assert.False(reader.ScalarLengthIs(0)); + Assert.False(reader.ScalarLengthIs(4)); + reader.DemandEnd(); + } + + // Tests for AggregateLengthIs + [Theory, Resp("*-1\r\n")] // null array + public void AggregateLengthIs_NullArray(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + Assert.True(reader.IsNull); + // Note: AggregateLength() would throw on null, but AggregateLengthIs should handle it + reader.DemandEnd(); + } + + [Theory, Resp("*0\r\n", "*?\r\n.\r\n")] // empty array (simple and streaming) + public void AggregateLengthIs_Empty(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + Assert.True(reader.AggregateLengthIs(0)); + Assert.False(reader.AggregateLengthIs(1)); + Assert.False(reader.AggregateLengthIs(3)); + reader.SkipChildren(); + reader.DemandEnd(); + } + + [Theory, Resp("*3\r\n:1\r\n:2\r\n:3\r\n")] // simple array + public void AggregateLengthIs_Simple(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + Assert.True(reader.AggregateLengthIs(3)); + Assert.False(reader.AggregateLengthIs(0)); + Assert.False(reader.AggregateLengthIs(2)); + Assert.False(reader.AggregateLengthIs(4)); + Assert.False(reader.AggregateLengthIs(10)); + reader.SkipChildren(); + reader.DemandEnd(); + } + + [Theory, Resp("*?\r\n:1\r\n:2\r\n:3\r\n.\r\n")] // streaming array + public void AggregateLengthIs_Streaming(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Array); + Assert.True(reader.AggregateLengthIs(3)); + Assert.False(reader.AggregateLengthIs(0)); + Assert.False(reader.AggregateLengthIs(2)); // short-circuit: stops early + Assert.False(reader.AggregateLengthIs(4)); // short-circuit: stops early + Assert.False(reader.AggregateLengthIs(10)); // short-circuit: stops early + reader.SkipChildren(); + reader.DemandEnd(); + } + + [Fact] // streaming array - verify short-circuiting works even with extra data present + public void AggregateLengthIs_Streaming_ShortCircuits() + { + // Streaming array: 3 elements (:1, :2, :3), then extra elements + // Short-circuiting means we can return false without reading all elements + var data = "*?\r\n:1\r\n:2\r\n:3\r\n:999\r\n:888\r\n.\r\n"u8.ToArray(); + var reader = new RespReader(new ReadOnlySequence(data)); + reader.MoveNext(RespPrefix.Array); + + // These should all return false via short-circuiting + // (we know the answer before reading all elements) + Assert.False(reader.AggregateLengthIs(0)); // can tell after 1 element + Assert.False(reader.AggregateLengthIs(2)); // can tell after 3 elements + Assert.False(reader.AggregateLengthIs(4)); // can tell after 4 elements (count > expected) + Assert.False(reader.AggregateLengthIs(10)); // can tell after 4 elements (count > expected) + + // The actual length is 5 (:1, :2, :3, :999, :888) + Assert.True(reader.AggregateLengthIs(5)); + } + + [Fact] // streaming array - verify short-circuiting stops before reading malformed data + public void AggregateLengthIs_Streaming_MalformedAfterShortCircuit() + { + // Streaming array: 3 elements (:1, :2, :3), then :4, then MALFORMED + // To check if length == N, we need to read N+1 elements to verify there isn't more + // So malformed data must come AFTER the N+1 threshold + var data = "*?\r\n:1\r\n:2\r\n:3\r\n:4\r\nGARBAGE_NOT_A_VALID_ELEMENT"u8.ToArray(); + var reader = new RespReader(new ReadOnlySequence(data)); + reader.MoveNext(RespPrefix.Array); + + // When checking length < 4, we read up to 4 elements, see 4 > expected, stop + Assert.False(reader.AggregateLengthIs(0)); // reads :1 (1 element), 1 > 0, stops before :2 + Assert.False(reader.AggregateLengthIs(2)); // reads :1, :2, :3 (3 elements), 3 > 2, stops before :4 + Assert.False(reader.AggregateLengthIs(3)); // reads :1, :2, :3, :4 (4 elements), 4 > 3, stops before MALFORMED + + // All of the above should succeed without hitting MALFORMED because we short-circuit + } + + [Theory, Resp("%2\r\n+first\r\n:1\r\n+second\r\n:2\r\n", "%?\r\n+first\r\n:1\r\n+second\r\n:2\r\n.\r\n")] // map (simple and streaming) + public void AggregateLengthIs_Map(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Map); + // Map length is doubled (2 pairs = 4 elements) + Assert.True(reader.AggregateLengthIs(4)); + Assert.False(reader.AggregateLengthIs(0)); + Assert.False(reader.AggregateLengthIs(2)); + Assert.False(reader.AggregateLengthIs(3)); + Assert.False(reader.AggregateLengthIs(5)); + reader.SkipChildren(); + reader.DemandEnd(); + } + + [Theory, Resp("~5\r\n+orange\r\n+apple\r\n#t\r\n:100\r\n:999\r\n", "~?\r\n+orange\r\n+apple\r\n#t\r\n:100\r\n:999\r\n.\r\n")] // set (simple and streaming) + public void AggregateLengthIs_Set(RespPayload payload) + { + var reader = payload.Reader(); + reader.MoveNext(RespPrefix.Set); + Assert.True(reader.AggregateLengthIs(5)); + Assert.False(reader.AggregateLengthIs(0)); + Assert.False(reader.AggregateLengthIs(4)); + Assert.False(reader.AggregateLengthIs(6)); + reader.SkipChildren(); + reader.DemandEnd(); + } + + private sealed class Segment : ReadOnlySequenceSegment + { + public override string ToString() => RespConstants.UTF8.GetString(Memory.Span) + .Replace("\r", "\\r").Replace("\n", "\\n"); + + public Segment(ReadOnlyMemory value, Segment? head) + { + Memory = value; + if (head is not null) + { + RunningIndex = head.RunningIndex + head.Memory.Length; + head.Next = this; + } + } + public bool IsEmpty => Memory.IsEmpty; + public int Length => Memory.Length; + } +} diff --git a/tests/RESPite.Tests/RespScannerTests.cs b/tests/RESPite.Tests/RespScannerTests.cs new file mode 100644 index 000000000..0028f0b3a --- /dev/null +++ b/tests/RESPite.Tests/RespScannerTests.cs @@ -0,0 +1,18 @@ +using RESPite.Messages; +using Xunit; + +namespace RESPite.Tests; + +public class RespScannerTests +{ + [Fact] + public void ScanNull() + { + RespScanState scanner = default; + Assert.True(scanner.TryRead("_\r\n"u8, out var consumed)); + + Assert.Equal(3, consumed); + Assert.Equal(3, scanner.TotalBytes); + Assert.Equal(RespPrefix.Null, scanner.Prefix); + } +} diff --git a/tests/RESPite.Tests/TestDuplexStream.cs b/tests/RESPite.Tests/TestDuplexStream.cs new file mode 100644 index 000000000..3456f0cb7 --- /dev/null +++ b/tests/RESPite.Tests/TestDuplexStream.cs @@ -0,0 +1,229 @@ +using System; +using System.Buffers; +using System.IO; +using System.IO.Pipelines; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace RESPite.Tests; + +/// +/// A controllable duplex stream for testing Redis protocol interactions. +/// Captures outbound data (client-to-redis) and allows controlled inbound data (redis-to-client). +/// +public sealed class TestDuplexStream : Stream +{ + private static readonly PipeOptions s_pipeOptions = new(useSynchronizationContext: false); + + private readonly MemoryStream _outbound; + private readonly Pipe _inbound; + private readonly Stream _inboundStream; + + public TestDuplexStream() + { + _outbound = new MemoryStream(); + _inbound = new Pipe(s_pipeOptions); + _inboundStream = _inbound.Reader.AsStream(); + } + + /// + /// Gets the data that has been written to the stream (client-to-redis). + /// + public ReadOnlySpan GetOutboundData() + { + if (_outbound.TryGetBuffer(out var buffer)) + { + return buffer.AsSpan(); + } + return _outbound.GetBuffer().AsSpan(0, (int)_outbound.Length); + } + + /// + /// Clears the outbound data buffer. + /// + public void FlushOutboundData() + { + _outbound.Position = 0; + _outbound.SetLength(0); + } + + /// + /// Adds data to the inbound buffer (redis-to-client) that will be available for reading. + /// + public async ValueTask AddInboundAsync(ReadOnlyMemory data, CancellationToken cancellationToken = default) + { + await _inbound.Writer.WriteAsync(data, cancellationToken).ConfigureAwait(false); + await _inbound.Writer.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// Adds data to the inbound buffer (redis-to-client) that will be available for reading. + /// Supports the "return pending.IsCompletedSynchronously ? default : AwaitAsync(pending)" pattern. + /// + public ValueTask AddInboundAsync(ReadOnlySpan data, CancellationToken cancellationToken = default) + { + // Use the Write extension method to write the span synchronously + _inbound.Writer.Write(data); + + // Flush and return based on completion status + var flushPending = _inbound.Writer.FlushAsync(cancellationToken); + return flushPending.IsCompletedSuccessfully ? default : AwaitFlushAsync(flushPending); + + static async ValueTask AwaitFlushAsync(ValueTask flushPending) + { + await flushPending.ConfigureAwait(false); + } + } + + /// + /// Adds UTF8-encoded string data to the inbound buffer (redis-to-client) that will be available for reading. + /// Uses stack allocation for small strings (≤256 bytes) and ArrayPool for larger strings. + /// Supports the "return pending.IsCompletedSynchronously ? default : AwaitAsync(pending)" pattern. + /// + public ValueTask AddInboundAsync(string data, CancellationToken cancellationToken = default) + { + const int StackAllocThreshold = 256; + + // Get the max byte count for UTF8 encoding + var maxByteCount = Encoding.UTF8.GetMaxByteCount(data.Length); + + if (maxByteCount <= StackAllocThreshold) + { + // Use stack allocation for small strings + Span buffer = stackalloc byte[maxByteCount]; + var actualByteCount = Encoding.UTF8.GetBytes(data, buffer); + _inbound.Writer.Write(buffer.Slice(0, actualByteCount)); + } + else + { + // Use ArrayPool for larger strings + var buffer = ArrayPool.Shared.Rent(maxByteCount); + try + { + var actualByteCount = Encoding.UTF8.GetBytes(data, buffer); + _inbound.Writer.Write(buffer.AsSpan(0, actualByteCount)); + } + finally + { + ArrayPool.Shared.Return(buffer); // can't have been captured during write, because span + } + } + + // Flush and return based on completion status + var flushPending = _inbound.Writer.FlushAsync(cancellationToken); + return flushPending.IsCompletedSuccessfully ? default : AwaitFlushAsync(flushPending); + + static async ValueTask AwaitFlushAsync(ValueTask flushPending) + { + await flushPending.ConfigureAwait(false); + } + } + + /// + /// Completes the inbound stream, signaling no more data will be written. + /// + public void CompleteInbound() + { + _inbound.Writer.Complete(); + } + + // Stream implementation - Read operations proxy to the inbound stream + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _inboundStream.Read(buffer, offset, count); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inboundStream.ReadAsync(buffer, offset, count, cancellationToken); + } + +#if NET + public override int Read(Span buffer) + { + return _inboundStream.Read(buffer); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + return _inboundStream.ReadAsync(buffer, cancellationToken); + } +#endif + + // Stream implementation - Write operations capture to the outbound stream + public override void Write(byte[] buffer, int offset, int count) + { + _outbound.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _outbound.WriteAsync(buffer, offset, count, cancellationToken); + } + +#if NET + public override void Write(ReadOnlySpan buffer) + { + _outbound.Write(buffer); + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + return _outbound.WriteAsync(buffer, cancellationToken); + } +#endif + + public override void Flush() + { + _outbound.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _outbound.FlushAsync(cancellationToken); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _inbound.Writer.Complete(); + _inbound.Reader.Complete(); + _inboundStream.Dispose(); + _outbound.Dispose(); + } + base.Dispose(disposing); + } + +#if NET + public override async ValueTask DisposeAsync() + { + _inbound.Writer.Complete(); + _inbound.Reader.Complete(); + await _inboundStream.DisposeAsync().ConfigureAwait(false); + await _outbound.DisposeAsync().ConfigureAwait(false); + await base.DisposeAsync().ConfigureAwait(false); + } +#endif +} diff --git a/tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs b/tests/StackExchange.Redis.Benchmarks/AsciiHashBenchmarks.cs similarity index 64% rename from tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs rename to tests/StackExchange.Redis.Benchmarks/AsciiHashBenchmarks.cs index 78877f163..57677f705 100644 --- a/tests/StackExchange.Redis.Benchmarks/FastHashBenchmarks.cs +++ b/tests/StackExchange.Redis.Benchmarks/AsciiHashBenchmarks.cs @@ -3,17 +3,19 @@ using System.Collections.Generic; using System.Text; using BenchmarkDotNet.Attributes; +using RESPite; namespace StackExchange.Redis.Benchmarks; -[Config(typeof(CustomConfig))] -public class FastHashBenchmarks +// [Config(typeof(CustomConfig))] +[ShortRunJob, MemoryDiagnoser] +public class AsciiHashBenchmarks { - private const string SharedString = "some-typical-data-for-comparisons"; + private const string SharedString = "some-typical-data-for-comparisons-that-needs-to-be-at-least-64-characters"; private static readonly byte[] SharedUtf8; private static readonly ReadOnlySequence SharedMultiSegment; - static FastHashBenchmarks() + static AsciiHashBenchmarks() { SharedUtf8 = Encoding.UTF8.GetBytes(SharedString); @@ -47,15 +49,16 @@ public void Setup() _sourceBytes = SharedUtf8.AsMemory(0, Size); _sourceMultiSegmentBytes = SharedMultiSegment.Slice(0, Size); -#pragma warning disable CS0618 // Type or member is obsolete var bytes = _sourceBytes.Span; - var expected = FastHash.Hash64Fallback(bytes); + var expected = AsciiHash.HashCS(bytes); - Assert(bytes.Hash64(), nameof(FastHash.Hash64)); - Assert(FastHash.Hash64Unsafe(bytes), nameof(FastHash.Hash64Unsafe)); -#pragma warning restore CS0618 // Type or member is obsolete - Assert(SingleSegmentBytes.Hash64(), nameof(FastHash.Hash64) + " (single segment)"); - Assert(_sourceMultiSegmentBytes.Hash64(), nameof(FastHash.Hash64) + " (multi segment)"); + Assert(AsciiHash.HashCS(bytes), nameof(AsciiHash.HashCS) + ":byte"); + Assert(AsciiHash.HashCS(_sourceString.AsSpan()), nameof(AsciiHash.HashCS) + ":char"); + + /* + Assert(AsciiHash.HashCS(SingleSegmentBytes), nameof(AsciiHash.HashCS) + " (single segment)"); + Assert(AsciiHash.HashCS(_sourceMultiSegmentBytes), nameof(AsciiHash.HashCS) + " (multi segment)"); + */ void Assert(long actual, string name) { @@ -69,71 +72,78 @@ void Assert(long actual, string name) [ParamsSource(nameof(Sizes))] public int Size { get; set; } = 7; - public IEnumerable Sizes => [0, 1, 2, 3, 4, 5, 6, 7, 8, 16]; + public IEnumerable Sizes => [0, 1, 2, 3, 4, 5, 6, 7, 8, 16, 64]; private const int OperationsPerInvoke = 1024; - [Benchmark(OperationsPerInvoke = OperationsPerInvoke, Baseline = true)] - public void String() + // [Benchmark(OperationsPerInvoke = OperationsPerInvoke, Baseline = true)] + public int StringGetHashCode() { + int hash = 0; var val = _sourceString; for (int i = 0; i < OperationsPerInvoke; i++) { - _ = val.GetHashCode(); + hash = val.GetHashCode(); } - } - [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] - public void Hash64() - { - var val = _sourceBytes.Span; - for (int i = 0; i < OperationsPerInvoke; i++) - { - _ = val.Hash64(); - } + return hash; } + [BenchmarkCategory("byte")] [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] - public void Hash64Unsafe() + public long HashCS_B() { + long hash = 0; var val = _sourceBytes.Span; for (int i = 0; i < OperationsPerInvoke; i++) { -#pragma warning disable CS0618 // Type or member is obsolete - _ = FastHash.Hash64Unsafe(val); -#pragma warning restore CS0618 // Type or member is obsolete + hash = AsciiHash.HashCS(val); } + + return hash; } + [BenchmarkCategory("char")] [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] - public void Hash64Fallback() + public long HashCS_C() { - var val = _sourceBytes.Span; + long hash = 0; + var val = _sourceString.AsSpan(); for (int i = 0; i < OperationsPerInvoke; i++) { #pragma warning disable CS0618 // Type or member is obsolete - _ = FastHash.Hash64Fallback(val); + hash = AsciiHash.HashCS(val); #pragma warning restore CS0618 // Type or member is obsolete } + + return hash; } - [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] - public void Hash64_SingleSegment() + /* + // [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public long Hash64_SingleSegment() { + long hash = 0; var val = SingleSegmentBytes; for (int i = 0; i < OperationsPerInvoke; i++) { - _ = val.Hash64(); + hash = AsciiHash.HashCS(val); } + + return hash; } - [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] - public void Hash64_MultiSegment() + // [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public long Hash64_MultiSegment() { + long hash = 0; var val = _sourceMultiSegmentBytes; for (int i = 0; i < OperationsPerInvoke; i++) { - _ = val.Hash64(); + hash = AsciiHash.HashCS(val); } + + return hash; } + */ } diff --git a/tests/StackExchange.Redis.Benchmarks/AsciiHashSwitch.cs b/tests/StackExchange.Redis.Benchmarks/AsciiHashSwitch.cs new file mode 100644 index 000000000..2409362ce --- /dev/null +++ b/tests/StackExchange.Redis.Benchmarks/AsciiHashSwitch.cs @@ -0,0 +1,517 @@ +using System; +using System.Text; +using BenchmarkDotNet.Attributes; +using RESPite; +// ReSharper disable InconsistentNaming +// ReSharper disable ArrangeTypeMemberModifiers +// ReSharper disable MemberCanBePrivate.Local +#pragma warning disable SA1300, SA1134, CS8981, SA1400 +namespace StackExchange.Redis.Benchmarks; + +[ShortRunJob, MemoryDiagnoser] +public class AsciiHashSwitch +{ + // conclusion: it doesn't matter; switch on the hash or length is fine, just: remember to do the Is check + // CS vs CI: CI misses are cheap, because of the hash fail; CI hits of values <= 8 characters are cheap if + // it turns out to be a CS match, because of the CS hash check which can cheaply test CS equality; CI inequality + // and CI equality over 8 characters has a bit more overhead, but still fine + public enum Field + { + key, + abc, + port, + test, + tracking_active, + sample_ratio, + selected_slots, + all_commands_all_slots_us, + all_commands_selected_slots_us, + sampled_command_selected_slots_us, + sampled_commands_selected_slots_us, + net_bytes_all_commands_all_slots, + net_bytes_all_commands_selected_slots, + net_bytes_sampled_commands_selected_slots, + collection_start_time_unix_ms, + collection_duration_ms, + collection_duration_us, + total_cpu_time_user_ms, + total_cpu_time_user_us, + total_cpu_time_sys_ms, + total_cpu_time_sys_us, + total_net_bytes, + by_cpu_time_us, + by_net_bytes, + + Unknown = -1, + } + + private byte[] _bytes = []; + [GlobalSetup] + public void Init() => _bytes = Encoding.UTF8.GetBytes(Value); + + public static string[] GetValues() => + [ + key.Text, + abc.Text, + port.Text, + test.Text, + tracking_active.Text, + sample_ratio.Text, + selected_slots.Text, + all_commands_all_slots_us.Text, + net_bytes_sampled_commands_selected_slots.Text, + total_cpu_time_sys_us.Text, + total_net_bytes.Text, + by_cpu_time_us.Text, + by_net_bytes.Text, + "miss", + "PORT", + "much longer miss", + ]; + + [ParamsSource(nameof(GetValues))] + public string Value { get; set; } = ""; + + [Benchmark] + public Field SwitchOnHash() + { + ReadOnlySpan span = _bytes; + var hash = AsciiHash.HashCS(span); + return hash switch + { + key.HashCS when key.IsCS(hash, span) => Field.key, + abc.HashCS when abc.IsCS(hash, span) => Field.abc, + port.HashCS when port.IsCS(hash, span) => Field.port, + test.HashCS when test.IsCS(hash, span) => Field.test, + tracking_active.HashCS when tracking_active.IsCS(hash, span) => Field.tracking_active, + sample_ratio.HashCS when sample_ratio.IsCS(hash, span) => Field.sample_ratio, + selected_slots.HashCS when selected_slots.IsCS(hash, span) => Field.selected_slots, + all_commands_all_slots_us.HashCS when all_commands_all_slots_us.IsCS(hash, span) => Field.all_commands_all_slots_us, + all_commands_selected_slots_us.HashCS when all_commands_selected_slots_us.IsCS(hash, span) => Field.all_commands_selected_slots_us, + sampled_command_selected_slots_us.HashCS when sampled_command_selected_slots_us.IsCS(hash, span) => Field.sampled_command_selected_slots_us, + sampled_commands_selected_slots_us.HashCS when sampled_commands_selected_slots_us.IsCS(hash, span) => Field.sampled_commands_selected_slots_us, + net_bytes_all_commands_all_slots.HashCS when net_bytes_all_commands_all_slots.IsCS(hash, span) => Field.net_bytes_all_commands_all_slots, + net_bytes_all_commands_selected_slots.HashCS when net_bytes_all_commands_selected_slots.IsCS(hash, span) => Field.net_bytes_all_commands_selected_slots, + net_bytes_sampled_commands_selected_slots.HashCS when net_bytes_sampled_commands_selected_slots.IsCS(hash, span) => Field.net_bytes_sampled_commands_selected_slots, + collection_start_time_unix_ms.HashCS when collection_start_time_unix_ms.IsCS(hash, span) => Field.collection_start_time_unix_ms, + collection_duration_ms.HashCS when collection_duration_ms.IsCS(hash, span) => Field.collection_duration_ms, + collection_duration_us.HashCS when collection_duration_us.IsCS(hash, span) => Field.collection_duration_us, + total_cpu_time_user_ms.HashCS when total_cpu_time_user_ms.IsCS(hash, span) => Field.total_cpu_time_user_ms, + total_cpu_time_user_us.HashCS when total_cpu_time_user_us.IsCS(hash, span) => Field.total_cpu_time_user_us, + total_cpu_time_sys_ms.HashCS when total_cpu_time_sys_ms.IsCS(hash, span) => Field.total_cpu_time_sys_ms, + total_cpu_time_sys_us.HashCS when total_cpu_time_sys_us.IsCS(hash, span) => Field.total_cpu_time_sys_us, + total_net_bytes.HashCS when total_net_bytes.IsCS(hash, span) => Field.total_net_bytes, + by_cpu_time_us.HashCS when by_cpu_time_us.IsCS(hash, span) => Field.by_cpu_time_us, + by_net_bytes.HashCS when by_net_bytes.IsCS(hash, span) => Field.by_net_bytes, + _ => Field.Unknown, + }; + } + + [Benchmark] + public Field SequenceEqual() + { + ReadOnlySpan span = _bytes; + if (span.SequenceEqual(key.U8)) return Field.key; + if (span.SequenceEqual(abc.U8)) return Field.abc; + if (span.SequenceEqual(port.U8)) return Field.port; + if (span.SequenceEqual(test.U8)) return Field.test; + if (span.SequenceEqual(tracking_active.U8)) return Field.tracking_active; + if (span.SequenceEqual(sample_ratio.U8)) return Field.sample_ratio; + if (span.SequenceEqual(selected_slots.U8)) return Field.selected_slots; + if (span.SequenceEqual(all_commands_all_slots_us.U8)) return Field.all_commands_all_slots_us; + if (span.SequenceEqual(all_commands_selected_slots_us.U8)) return Field.all_commands_selected_slots_us; + if (span.SequenceEqual(sampled_command_selected_slots_us.U8)) return Field.sampled_command_selected_slots_us; + if (span.SequenceEqual(sampled_commands_selected_slots_us.U8)) return Field.sampled_commands_selected_slots_us; + if (span.SequenceEqual(net_bytes_all_commands_all_slots.U8)) return Field.net_bytes_all_commands_all_slots; + if (span.SequenceEqual(net_bytes_all_commands_selected_slots.U8)) return Field.net_bytes_all_commands_selected_slots; + if (span.SequenceEqual(net_bytes_sampled_commands_selected_slots.U8)) return Field.net_bytes_sampled_commands_selected_slots; + if (span.SequenceEqual(collection_start_time_unix_ms.U8)) return Field.collection_start_time_unix_ms; + if (span.SequenceEqual(collection_duration_ms.U8)) return Field.collection_duration_ms; + if (span.SequenceEqual(collection_duration_us.U8)) return Field.collection_duration_us; + if (span.SequenceEqual(total_cpu_time_user_ms.U8)) return Field.total_cpu_time_user_ms; + if (span.SequenceEqual(total_cpu_time_user_us.U8)) return Field.total_cpu_time_user_us; + if (span.SequenceEqual(total_cpu_time_sys_ms.U8)) return Field.total_cpu_time_sys_ms; + if (span.SequenceEqual(total_cpu_time_sys_us.U8)) return Field.total_cpu_time_sys_us; + if (span.SequenceEqual(total_net_bytes.U8)) return Field.total_net_bytes; + if (span.SequenceEqual(by_cpu_time_us.U8)) return Field.by_cpu_time_us; + if (span.SequenceEqual(by_net_bytes.U8)) return Field.by_net_bytes; + + return Field.Unknown; + } + + [Benchmark] + public Field SwitchOnLength() + { + ReadOnlySpan span = _bytes; + var hash = AsciiHash.HashCS(span); + return span.Length switch + { + key.Length when key.IsCS(hash, span) => Field.key, + abc.Length when abc.IsCS(hash, span) => Field.abc, + port.Length when port.IsCS(hash, span) => Field.port, + test.Length when test.IsCS(hash, span) => Field.test, + tracking_active.Length when tracking_active.IsCS(hash, span) => Field.tracking_active, + sample_ratio.Length when sample_ratio.IsCS(hash, span) => Field.sample_ratio, + selected_slots.Length when selected_slots.IsCS(hash, span) => Field.selected_slots, + all_commands_all_slots_us.Length when all_commands_all_slots_us.IsCS(hash, span) => Field.all_commands_all_slots_us, + all_commands_selected_slots_us.Length when all_commands_selected_slots_us.IsCS(hash, span) => Field.all_commands_selected_slots_us, + sampled_command_selected_slots_us.Length when sampled_command_selected_slots_us.IsCS(hash, span) => Field.sampled_command_selected_slots_us, + sampled_commands_selected_slots_us.Length when sampled_commands_selected_slots_us.IsCS(hash, span) => Field.sampled_commands_selected_slots_us, + net_bytes_all_commands_all_slots.Length when net_bytes_all_commands_all_slots.IsCS(hash, span) => Field.net_bytes_all_commands_all_slots, + net_bytes_all_commands_selected_slots.Length when net_bytes_all_commands_selected_slots.IsCS(hash, span) => Field.net_bytes_all_commands_selected_slots, + net_bytes_sampled_commands_selected_slots.Length when net_bytes_sampled_commands_selected_slots.IsCS(hash, span) => Field.net_bytes_sampled_commands_selected_slots, + collection_start_time_unix_ms.Length when collection_start_time_unix_ms.IsCS(hash, span) => Field.collection_start_time_unix_ms, + collection_duration_ms.Length when collection_duration_ms.IsCS(hash, span) => Field.collection_duration_ms, + collection_duration_us.Length when collection_duration_us.IsCS(hash, span) => Field.collection_duration_us, + total_cpu_time_user_ms.Length when total_cpu_time_user_ms.IsCS(hash, span) => Field.total_cpu_time_user_ms, + total_cpu_time_user_us.Length when total_cpu_time_user_us.IsCS(hash, span) => Field.total_cpu_time_user_us, + total_cpu_time_sys_ms.Length when total_cpu_time_sys_ms.IsCS(hash, span) => Field.total_cpu_time_sys_ms, + total_cpu_time_sys_us.Length when total_cpu_time_sys_us.IsCS(hash, span) => Field.total_cpu_time_sys_us, + total_net_bytes.Length when total_net_bytes.IsCS(hash, span) => Field.total_net_bytes, + by_cpu_time_us.Length when by_cpu_time_us.IsCS(hash, span) => Field.by_cpu_time_us, + by_net_bytes.Length when by_net_bytes.IsCS(hash, span) => Field.by_net_bytes, + _ => Field.Unknown, + }; + } + + [Benchmark] + public Field SwitchOnHash_CI() + { + ReadOnlySpan span = _bytes; + var hash = AsciiHash.HashUC(span); + return hash switch + { + key.HashCI when key.IsCI(hash, span) => Field.key, + abc.HashCI when abc.IsCI(hash, span) => Field.abc, + port.HashCI when port.IsCI(hash, span) => Field.port, + test.HashCI when test.IsCI(hash, span) => Field.test, + tracking_active.HashCI when tracking_active.IsCI(hash, span) => Field.tracking_active, + sample_ratio.HashCI when sample_ratio.IsCI(hash, span) => Field.sample_ratio, + selected_slots.HashCI when selected_slots.IsCI(hash, span) => Field.selected_slots, + all_commands_all_slots_us.HashCI when all_commands_all_slots_us.IsCI(hash, span) => Field.all_commands_all_slots_us, + all_commands_selected_slots_us.HashCI when all_commands_selected_slots_us.IsCI(hash, span) => Field.all_commands_selected_slots_us, + sampled_command_selected_slots_us.HashCI when sampled_command_selected_slots_us.IsCI(hash, span) => Field.sampled_command_selected_slots_us, + sampled_commands_selected_slots_us.HashCI when sampled_commands_selected_slots_us.IsCI(hash, span) => Field.sampled_commands_selected_slots_us, + net_bytes_all_commands_all_slots.HashCI when net_bytes_all_commands_all_slots.IsCI(hash, span) => Field.net_bytes_all_commands_all_slots, + net_bytes_all_commands_selected_slots.HashCI when net_bytes_all_commands_selected_slots.IsCI(hash, span) => Field.net_bytes_all_commands_selected_slots, + net_bytes_sampled_commands_selected_slots.HashCI when net_bytes_sampled_commands_selected_slots.IsCI(hash, span) => Field.net_bytes_sampled_commands_selected_slots, + collection_start_time_unix_ms.HashCI when collection_start_time_unix_ms.IsCI(hash, span) => Field.collection_start_time_unix_ms, + collection_duration_ms.HashCI when collection_duration_ms.IsCI(hash, span) => Field.collection_duration_ms, + collection_duration_us.HashCI when collection_duration_us.IsCI(hash, span) => Field.collection_duration_us, + total_cpu_time_user_ms.HashCI when total_cpu_time_user_ms.IsCI(hash, span) => Field.total_cpu_time_user_ms, + total_cpu_time_user_us.HashCI when total_cpu_time_user_us.IsCI(hash, span) => Field.total_cpu_time_user_us, + total_cpu_time_sys_ms.HashCI when total_cpu_time_sys_ms.IsCI(hash, span) => Field.total_cpu_time_sys_ms, + total_cpu_time_sys_us.HashCI when total_cpu_time_sys_us.IsCI(hash, span) => Field.total_cpu_time_sys_us, + total_net_bytes.HashCI when total_net_bytes.IsCI(hash, span) => Field.total_net_bytes, + by_cpu_time_us.HashCI when by_cpu_time_us.IsCI(hash, span) => Field.by_cpu_time_us, + by_net_bytes.HashCI when by_net_bytes.IsCI(hash, span) => Field.by_net_bytes, + _ => Field.Unknown, + }; + } + + [Benchmark] + public Field SwitchOnLength_CI() + { + ReadOnlySpan span = _bytes; + var hash = AsciiHash.HashUC(span); + return span.Length switch + { + key.Length when key.IsCI(hash, span) => Field.key, + abc.Length when abc.IsCI(hash, span) => Field.abc, + port.Length when port.IsCI(hash, span) => Field.port, + test.Length when test.IsCI(hash, span) => Field.test, + tracking_active.Length when tracking_active.IsCI(hash, span) => Field.tracking_active, + sample_ratio.Length when sample_ratio.IsCI(hash, span) => Field.sample_ratio, + selected_slots.Length when selected_slots.IsCI(hash, span) => Field.selected_slots, + all_commands_all_slots_us.Length when all_commands_all_slots_us.IsCI(hash, span) => Field.all_commands_all_slots_us, + all_commands_selected_slots_us.Length when all_commands_selected_slots_us.IsCI(hash, span) => Field.all_commands_selected_slots_us, + sampled_command_selected_slots_us.Length when sampled_command_selected_slots_us.IsCI(hash, span) => Field.sampled_command_selected_slots_us, + sampled_commands_selected_slots_us.Length when sampled_commands_selected_slots_us.IsCI(hash, span) => Field.sampled_commands_selected_slots_us, + net_bytes_all_commands_all_slots.Length when net_bytes_all_commands_all_slots.IsCI(hash, span) => Field.net_bytes_all_commands_all_slots, + net_bytes_all_commands_selected_slots.Length when net_bytes_all_commands_selected_slots.IsCI(hash, span) => Field.net_bytes_all_commands_selected_slots, + net_bytes_sampled_commands_selected_slots.Length when net_bytes_sampled_commands_selected_slots.IsCI(hash, span) => Field.net_bytes_sampled_commands_selected_slots, + collection_start_time_unix_ms.Length when collection_start_time_unix_ms.IsCI(hash, span) => Field.collection_start_time_unix_ms, + collection_duration_ms.Length when collection_duration_ms.IsCI(hash, span) => Field.collection_duration_ms, + collection_duration_us.Length when collection_duration_us.IsCI(hash, span) => Field.collection_duration_us, + total_cpu_time_user_ms.Length when total_cpu_time_user_ms.IsCI(hash, span) => Field.total_cpu_time_user_ms, + total_cpu_time_user_us.Length when total_cpu_time_user_us.IsCI(hash, span) => Field.total_cpu_time_user_us, + total_cpu_time_sys_ms.Length when total_cpu_time_sys_ms.IsCI(hash, span) => Field.total_cpu_time_sys_ms, + total_cpu_time_sys_us.Length when total_cpu_time_sys_us.IsCI(hash, span) => Field.total_cpu_time_sys_us, + total_net_bytes.Length when total_net_bytes.IsCI(hash, span) => Field.total_net_bytes, + by_cpu_time_us.Length when by_cpu_time_us.IsCI(hash, span) => Field.by_cpu_time_us, + by_net_bytes.Length when by_net_bytes.IsCI(hash, span) => Field.by_net_bytes, + _ => Field.Unknown, + }; + } + + /* + we're using raw output from the code-gen, because BDN kinda hates the tooling, because + of the complex build pipe; this is left for reference only + + [AsciiHash] internal static partial class key { } + [AsciiHash] internal static partial class abc { } + [AsciiHash] internal static partial class port { } + [AsciiHash] internal static partial class test { } + [AsciiHash] internal static partial class tracking_active { } + [AsciiHash] internal static partial class sample_ratio { } + [AsciiHash] internal static partial class selected_slots { } + [AsciiHash] internal static partial class all_commands_all_slots_us { } + [AsciiHash] internal static partial class all_commands_selected_slots_us { } + [AsciiHash] internal static partial class sampled_command_selected_slots_us { } + [AsciiHash] internal static partial class sampled_commands_selected_slots_us { } + [AsciiHash] internal static partial class net_bytes_all_commands_all_slots { } + [AsciiHash] internal static partial class net_bytes_all_commands_selected_slots { } + [AsciiHash] internal static partial class net_bytes_sampled_commands_selected_slots { } + [AsciiHash] internal static partial class collection_start_time_unix_ms { } + [AsciiHash] internal static partial class collection_duration_ms { } + [AsciiHash] internal static partial class collection_duration_us { } + [AsciiHash] internal static partial class total_cpu_time_user_ms { } + [AsciiHash] internal static partial class total_cpu_time_user_us { } + [AsciiHash] internal static partial class total_cpu_time_sys_ms { } + [AsciiHash] internal static partial class total_cpu_time_sys_us { } + [AsciiHash] internal static partial class total_net_bytes { } + [AsciiHash] internal static partial class by_cpu_time_us { } + [AsciiHash] internal static partial class by_net_bytes { } + */ + + static class key + { + public const int Length = 3; + public const long HashCS = 7955819; + public const long HashCI = 5850443; + public static ReadOnlySpan U8 => "key"u8; + public const string Text = "key"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS & value.Length == Length; + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && (global::RESPite.AsciiHash.HashCS(value) == HashCS || global::RESPite.AsciiHash.EqualsCI(value, U8)); + } + static class abc + { + public const int Length = 3; + public const long HashCS = 6513249; + public const long HashCI = 4407873; + public static ReadOnlySpan U8 => "abc"u8; + public const string Text = "abc"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS & value.Length == Length; + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && (global::RESPite.AsciiHash.HashCS(value) == HashCS || global::RESPite.AsciiHash.EqualsCI(value, U8)); + } + static class port + { + public const int Length = 4; + public const long HashCS = 1953656688; + public const long HashCI = 1414680400; + public static ReadOnlySpan U8 => "port"u8; + public const string Text = "port"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS & value.Length == Length; + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && (global::RESPite.AsciiHash.HashCS(value) == HashCS || global::RESPite.AsciiHash.EqualsCI(value, U8)); + } + static class test + { + public const int Length = 4; + public const long HashCS = 1953719668; + public const long HashCI = 1414743380; + public static ReadOnlySpan U8 => "test"u8; + public const string Text = "test"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS & value.Length == Length; + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && (global::RESPite.AsciiHash.HashCS(value) == HashCS || global::RESPite.AsciiHash.EqualsCI(value, U8)); + } + static class tracking_active + { + public const int Length = 15; + public const long HashCS = 7453010343294497396; + public const long HashCI = 5138124812476043860; + public static ReadOnlySpan U8 => "tracking-active"u8; + public const string Text = "tracking-active"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class sample_ratio + { + public const int Length = 12; + public const long HashCS = 8227343610692854131; + public const long HashCI = 5912458079874400595; + public static ReadOnlySpan U8 => "sample-ratio"u8; + public const string Text = "sample-ratio"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class selected_slots + { + public const int Length = 14; + public const long HashCS = 7234316346692756851; + public const long HashCI = 4919430815874303315; + public static ReadOnlySpan U8 => "selected-slots"u8; + public const string Text = "selected-slots"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class all_commands_all_slots_us + { + public const int Length = 25; + public const long HashCS = 7885080994350132321; + public const long HashCI = 5570195463531678785; + public static ReadOnlySpan U8 => "all-commands-all-slots-us"u8; + public const string Text = "all-commands-all-slots-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class all_commands_selected_slots_us + { + public const int Length = 30; + public const long HashCS = 7885080994350132321; + public const long HashCI = 5570195463531678785; + public static ReadOnlySpan U8 => "all-commands-selected-slots-us"u8; + public const string Text = "all-commands-selected-slots-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class sampled_command_selected_slots_us + { + public const int Length = 33; + public const long HashCS = 3270850745794912627; + public const long HashCI = 955965214976459091; + public static ReadOnlySpan U8 => "sampled-command-selected-slots-us"u8; + public const string Text = "sampled-command-selected-slots-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class sampled_commands_selected_slots_us + { + public const int Length = 34; + public const long HashCS = 3270850745794912627; + public const long HashCI = 955965214976459091; + public static ReadOnlySpan U8 => "sampled-commands-selected-slots-us"u8; + public const string Text = "sampled-commands-selected-slots-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class net_bytes_all_commands_all_slots + { + public const int Length = 32; + public const long HashCS = 7310601557705516398; + public const long HashCI = 4995716026887062862; + public static ReadOnlySpan U8 => "net-bytes-all-commands-all-slots"u8; + public const string Text = "net-bytes-all-commands-all-slots"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class net_bytes_all_commands_selected_slots + { + public const int Length = 37; + public const long HashCS = 7310601557705516398; + public const long HashCI = 4995716026887062862; + public static ReadOnlySpan U8 => "net-bytes-all-commands-selected-slots"u8; + public const string Text = "net-bytes-all-commands-selected-slots"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class net_bytes_sampled_commands_selected_slots + { + public const int Length = 41; + public const long HashCS = 7310601557705516398; + public const long HashCI = 4995716026887062862; + public static ReadOnlySpan U8 => "net-bytes-sampled-commands-selected-slots"u8; + public const string Text = "net-bytes-sampled-commands-selected-slots"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class collection_start_time_unix_ms + { + public const int Length = 29; + public const long HashCS = 7598807758542761827; + public const long HashCI = 5283922227724308291; + public static ReadOnlySpan U8 => "collection-start-time-unix-ms"u8; + public const string Text = "collection-start-time-unix-ms"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class collection_duration_ms + { + public const int Length = 22; + public const long HashCS = 7598807758542761827; + public const long HashCI = 5283922227724308291; + public static ReadOnlySpan U8 => "collection-duration-ms"u8; + public const string Text = "collection-duration-ms"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class collection_duration_us + { + public const int Length = 22; + public const long HashCS = 7598807758542761827; + public const long HashCI = 5283922227724308291; + public static ReadOnlySpan U8 => "collection-duration-us"u8; + public const string Text = "collection-duration-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class total_cpu_time_user_ms + { + public const int Length = 22; + public const long HashCS = 8098366498457022324; + public const long HashCI = 5783480967638568788; + public static ReadOnlySpan U8 => "total-cpu-time-user-ms"u8; + public const string Text = "total-cpu-time-user-ms"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class total_cpu_time_user_us + { + public const int Length = 22; + public const long HashCS = 8098366498457022324; + public const long HashCI = 5783480967638568788; + public static ReadOnlySpan U8 => "total-cpu-time-user-us"u8; + public const string Text = "total-cpu-time-user-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class total_cpu_time_sys_ms + { + public const int Length = 21; + public const long HashCS = 8098366498457022324; + public const long HashCI = 5783480967638568788; + public static ReadOnlySpan U8 => "total-cpu-time-sys-ms"u8; + public const string Text = "total-cpu-time-sys-ms"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class total_cpu_time_sys_us + { + public const int Length = 21; + public const long HashCS = 8098366498457022324; + public const long HashCI = 5783480967638568788; + public static ReadOnlySpan U8 => "total-cpu-time-sys-us"u8; + public const string Text = "total-cpu-time-sys-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class total_net_bytes + { + public const int Length = 15; + public const long HashCS = 7308829188783632244; + public const long HashCI = 4993943657965178708; + public static ReadOnlySpan U8 => "total-net-bytes"u8; + public const string Text = "total-net-bytes"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class by_cpu_time_us + { + public const int Length = 14; + public const long HashCS = 8371476407912331618; + public const long HashCI = 6056590877093878082; + public static ReadOnlySpan U8 => "by-cpu-time-us"u8; + public const string Text = "by-cpu-time-us"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } + static class by_net_bytes + { + public const int Length = 12; + public const long HashCS = 7074438568657910114; + public const long HashCI = 4759553037839456578; + public static ReadOnlySpan U8 => "by-net-bytes"u8; + public const string Text = "by-net-bytes"; + public static bool IsCS(long hash, ReadOnlySpan value) => hash == HashCS && value.SequenceEqual(U8); + public static bool IsCI(long hash, ReadOnlySpan value) => (hash == HashCI & value.Length == Length) && global::RESPite.AsciiHash.EqualsCI(value, U8); + } +} diff --git a/tests/StackExchange.Redis.Benchmarks/CustomConfig.cs b/tests/StackExchange.Redis.Benchmarks/CustomConfig.cs index 09f44cc31..7013d4386 100644 --- a/tests/StackExchange.Redis.Benchmarks/CustomConfig.cs +++ b/tests/StackExchange.Redis.Benchmarks/CustomConfig.cs @@ -22,7 +22,7 @@ public CustomConfig() { AddJob(Configure(Job.Default.WithRuntime(ClrRuntime.Net481))); } - AddJob(Configure(Job.Default.WithRuntime(CoreRuntime.Core80))); + AddJob(Configure(Job.Default.WithRuntime(CoreRuntime.Core10_0))); } } } diff --git a/tests/StackExchange.Redis.Benchmarks/EnumParseBenchmarks.cs b/tests/StackExchange.Redis.Benchmarks/EnumParseBenchmarks.cs new file mode 100644 index 000000000..de6ae174e --- /dev/null +++ b/tests/StackExchange.Redis.Benchmarks/EnumParseBenchmarks.cs @@ -0,0 +1,690 @@ +using System; +using System.Text; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using RESPite; + +namespace StackExchange.Redis.Benchmarks; + +[ShortRunJob, MemoryDiagnoser, GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +public partial class EnumParseBenchmarks +{ + private const int OperationsPerInvoke = 1000; + + public string[] Values() => + [ + nameof(RedisCommand.GET), + nameof(RedisCommand.EXPIREAT), + nameof(RedisCommand.ZREMRANGEBYSCORE), + "~~~~", + "get", + "expireat", + "zremrangebyscore", + "GeoRadiusByMember", + ]; + + private byte[] _bytes = []; + private string _value = ""; + + [ParamsSource(nameof(Values))] + public string Value + { + get => _value; + set + { + value ??= ""; + _bytes = Encoding.UTF8.GetBytes(value); + _value = value; + } + } + + [BenchmarkCategory("Case sensitive")] + [Benchmark(OperationsPerInvoke = OperationsPerInvoke, Baseline = true)] + public RedisCommand EnumParse_CS() + { + var value = Value; + RedisCommand r = default; + for (int i = 0; i < OperationsPerInvoke; i++) + { + Enum.TryParse(value, false, out r); + } + + return r; + } + + [BenchmarkCategory("Case insensitive")] + [Benchmark(OperationsPerInvoke = OperationsPerInvoke, Baseline = true)] + public RedisCommand EnumParse_CI() + { + var value = Value; + RedisCommand r = default; + for (int i = 0; i < OperationsPerInvoke; i++) + { + Enum.TryParse(value, true, out r); + } + + return r; + } + + [BenchmarkCategory("Case sensitive")] + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public RedisCommand Ascii_C_CS() + { + ReadOnlySpan value = Value; + RedisCommand r = default; + for (int i = 0; i < OperationsPerInvoke; i++) + { + TryParse_CS(value, out r); + } + + return r; + } + + [BenchmarkCategory("Case insensitive")] + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public RedisCommand Ascii_C_CI() + { + ReadOnlySpan value = Value; + RedisCommand r = default; + for (int i = 0; i < OperationsPerInvoke; i++) + { + TryParse_CI(value, out r); + } + + return r; + } + + [BenchmarkCategory("Case sensitive")] + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public RedisCommand Ascii_B_CS() + { + ReadOnlySpan value = _bytes; + RedisCommand r = default; + for (int i = 0; i < OperationsPerInvoke; i++) + { + TryParse_CS(value, out r); + } + + return r; + } + + [BenchmarkCategory("Case insensitive")] + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public RedisCommand Ascii_B_CI() + { + ReadOnlySpan value = _bytes; + RedisCommand r = default; + for (int i = 0; i < OperationsPerInvoke; i++) + { + TryParse_CI(value, out r); + } + + return r; + } + + [BenchmarkCategory("Case sensitive")] + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public RedisCommand Switch_CS() + { + var value = Value; + RedisCommand r = default; + for (int i = 0; i < OperationsPerInvoke; i++) + { + TryParseSwitch(value, out r); + } + + return r; + } + + private static bool TryParseSwitch(string s, out RedisCommand r) + { + r = s switch + { + "NONE" => RedisCommand.NONE, + "APPEND" => RedisCommand.APPEND, + "ASKING" => RedisCommand.ASKING, + "AUTH" => RedisCommand.AUTH, + "BGREWRITEAOF" => RedisCommand.BGREWRITEAOF, + "BGSAVE" => RedisCommand.BGSAVE, + "BITCOUNT" => RedisCommand.BITCOUNT, + "BITOP" => RedisCommand.BITOP, + "BITPOS" => RedisCommand.BITPOS, + "BLPOP" => RedisCommand.BLPOP, + "BRPOP" => RedisCommand.BRPOP, + "BRPOPLPUSH" => RedisCommand.BRPOPLPUSH, + "CLIENT" => RedisCommand.CLIENT, + "CLUSTER" => RedisCommand.CLUSTER, + "CONFIG" => RedisCommand.CONFIG, + "COPY" => RedisCommand.COPY, + "COMMAND" => RedisCommand.COMMAND, + "DBSIZE" => RedisCommand.DBSIZE, + "DEBUG" => RedisCommand.DEBUG, + "DECR" => RedisCommand.DECR, + "DECRBY" => RedisCommand.DECRBY, + "DEL" => RedisCommand.DEL, + "DELEX" => RedisCommand.DELEX, + "DIGEST" => RedisCommand.DIGEST, + "DISCARD" => RedisCommand.DISCARD, + "DUMP" => RedisCommand.DUMP, + "ECHO" => RedisCommand.ECHO, + "EVAL" => RedisCommand.EVAL, + "EVALSHA" => RedisCommand.EVALSHA, + "EVAL_RO" => RedisCommand.EVAL_RO, + "EVALSHA_RO" => RedisCommand.EVALSHA_RO, + "EXEC" => RedisCommand.EXEC, + "EXISTS" => RedisCommand.EXISTS, + "EXPIRE" => RedisCommand.EXPIRE, + "EXPIREAT" => RedisCommand.EXPIREAT, + "EXPIRETIME" => RedisCommand.EXPIRETIME, + "FLUSHALL" => RedisCommand.FLUSHALL, + "FLUSHDB" => RedisCommand.FLUSHDB, + "GEOADD" => RedisCommand.GEOADD, + "GEODIST" => RedisCommand.GEODIST, + "GEOHASH" => RedisCommand.GEOHASH, + "GEOPOS" => RedisCommand.GEOPOS, + "GEORADIUS" => RedisCommand.GEORADIUS, + "GEORADIUSBYMEMBER" => RedisCommand.GEORADIUSBYMEMBER, + "GEOSEARCH" => RedisCommand.GEOSEARCH, + "GEOSEARCHSTORE" => RedisCommand.GEOSEARCHSTORE, + "GET" => RedisCommand.GET, + "GETBIT" => RedisCommand.GETBIT, + "GETDEL" => RedisCommand.GETDEL, + "GETEX" => RedisCommand.GETEX, + "GETRANGE" => RedisCommand.GETRANGE, + "GETSET" => RedisCommand.GETSET, + "HDEL" => RedisCommand.HDEL, + "HELLO" => RedisCommand.HELLO, + "HEXISTS" => RedisCommand.HEXISTS, + "HEXPIRE" => RedisCommand.HEXPIRE, + "HEXPIREAT" => RedisCommand.HEXPIREAT, + "HEXPIRETIME" => RedisCommand.HEXPIRETIME, + "HGET" => RedisCommand.HGET, + "HGETEX" => RedisCommand.HGETEX, + "HGETDEL" => RedisCommand.HGETDEL, + "HGETALL" => RedisCommand.HGETALL, + "HINCRBY" => RedisCommand.HINCRBY, + "HINCRBYFLOAT" => RedisCommand.HINCRBYFLOAT, + "HKEYS" => RedisCommand.HKEYS, + "HLEN" => RedisCommand.HLEN, + "HMGET" => RedisCommand.HMGET, + "HMSET" => RedisCommand.HMSET, + "HOTKEYS" => RedisCommand.HOTKEYS, + "HPERSIST" => RedisCommand.HPERSIST, + "HPEXPIRE" => RedisCommand.HPEXPIRE, + "HPEXPIREAT" => RedisCommand.HPEXPIREAT, + "HPEXPIRETIME" => RedisCommand.HPEXPIRETIME, + "HPTTL" => RedisCommand.HPTTL, + "HRANDFIELD" => RedisCommand.HRANDFIELD, + "HSCAN" => RedisCommand.HSCAN, + "HSET" => RedisCommand.HSET, + "HSETEX" => RedisCommand.HSETEX, + "HSETNX" => RedisCommand.HSETNX, + "HSTRLEN" => RedisCommand.HSTRLEN, + "HVALS" => RedisCommand.HVALS, + "INCR" => RedisCommand.INCR, + "INCRBY" => RedisCommand.INCRBY, + "INCRBYFLOAT" => RedisCommand.INCRBYFLOAT, + "INFO" => RedisCommand.INFO, + "KEYS" => RedisCommand.KEYS, + "LASTSAVE" => RedisCommand.LASTSAVE, + "LATENCY" => RedisCommand.LATENCY, + "LCS" => RedisCommand.LCS, + "LINDEX" => RedisCommand.LINDEX, + "LINSERT" => RedisCommand.LINSERT, + "LLEN" => RedisCommand.LLEN, + "LMOVE" => RedisCommand.LMOVE, + "LMPOP" => RedisCommand.LMPOP, + "LPOP" => RedisCommand.LPOP, + "LPOS" => RedisCommand.LPOS, + "LPUSH" => RedisCommand.LPUSH, + "LPUSHX" => RedisCommand.LPUSHX, + "LRANGE" => RedisCommand.LRANGE, + "LREM" => RedisCommand.LREM, + "LSET" => RedisCommand.LSET, + "LTRIM" => RedisCommand.LTRIM, + "MEMORY" => RedisCommand.MEMORY, + "MGET" => RedisCommand.MGET, + "MIGRATE" => RedisCommand.MIGRATE, + "MONITOR" => RedisCommand.MONITOR, + "MOVE" => RedisCommand.MOVE, + "MSET" => RedisCommand.MSET, + "MSETEX" => RedisCommand.MSETEX, + "MSETNX" => RedisCommand.MSETNX, + "MULTI" => RedisCommand.MULTI, + "OBJECT" => RedisCommand.OBJECT, + "PERSIST" => RedisCommand.PERSIST, + "PEXPIRE" => RedisCommand.PEXPIRE, + "PEXPIREAT" => RedisCommand.PEXPIREAT, + "PEXPIRETIME" => RedisCommand.PEXPIRETIME, + "PFADD" => RedisCommand.PFADD, + "PFCOUNT" => RedisCommand.PFCOUNT, + "PFMERGE" => RedisCommand.PFMERGE, + "PING" => RedisCommand.PING, + "PSETEX" => RedisCommand.PSETEX, + "PSUBSCRIBE" => RedisCommand.PSUBSCRIBE, + "PTTL" => RedisCommand.PTTL, + "PUBLISH" => RedisCommand.PUBLISH, + "PUBSUB" => RedisCommand.PUBSUB, + "PUNSUBSCRIBE" => RedisCommand.PUNSUBSCRIBE, + "QUIT" => RedisCommand.QUIT, + "RANDOMKEY" => RedisCommand.RANDOMKEY, + "READONLY" => RedisCommand.READONLY, + "READWRITE" => RedisCommand.READWRITE, + "RENAME" => RedisCommand.RENAME, + "RENAMENX" => RedisCommand.RENAMENX, + "REPLICAOF" => RedisCommand.REPLICAOF, + "RESTORE" => RedisCommand.RESTORE, + "ROLE" => RedisCommand.ROLE, + "RPOP" => RedisCommand.RPOP, + "RPOPLPUSH" => RedisCommand.RPOPLPUSH, + "RPUSH" => RedisCommand.RPUSH, + "RPUSHX" => RedisCommand.RPUSHX, + "SADD" => RedisCommand.SADD, + "SAVE" => RedisCommand.SAVE, + "SCAN" => RedisCommand.SCAN, + "SCARD" => RedisCommand.SCARD, + "SCRIPT" => RedisCommand.SCRIPT, + "SDIFF" => RedisCommand.SDIFF, + "SDIFFSTORE" => RedisCommand.SDIFFSTORE, + "SELECT" => RedisCommand.SELECT, + "SENTINEL" => RedisCommand.SENTINEL, + "SET" => RedisCommand.SET, + "SETBIT" => RedisCommand.SETBIT, + "SETEX" => RedisCommand.SETEX, + "SETNX" => RedisCommand.SETNX, + "SETRANGE" => RedisCommand.SETRANGE, + "SHUTDOWN" => RedisCommand.SHUTDOWN, + "SINTER" => RedisCommand.SINTER, + "SINTERCARD" => RedisCommand.SINTERCARD, + "SINTERSTORE" => RedisCommand.SINTERSTORE, + "SISMEMBER" => RedisCommand.SISMEMBER, + "SLAVEOF" => RedisCommand.SLAVEOF, + "SLOWLOG" => RedisCommand.SLOWLOG, + "SMEMBERS" => RedisCommand.SMEMBERS, + "SMISMEMBER" => RedisCommand.SMISMEMBER, + "SMOVE" => RedisCommand.SMOVE, + "SORT" => RedisCommand.SORT, + "SORT_RO" => RedisCommand.SORT_RO, + "SPOP" => RedisCommand.SPOP, + "SPUBLISH" => RedisCommand.SPUBLISH, + "SRANDMEMBER" => RedisCommand.SRANDMEMBER, + "SREM" => RedisCommand.SREM, + "STRLEN" => RedisCommand.STRLEN, + "SUBSCRIBE" => RedisCommand.SUBSCRIBE, + "SUNION" => RedisCommand.SUNION, + "SUNIONSTORE" => RedisCommand.SUNIONSTORE, + "SSCAN" => RedisCommand.SSCAN, + "SSUBSCRIBE" => RedisCommand.SSUBSCRIBE, + "SUNSUBSCRIBE" => RedisCommand.SUNSUBSCRIBE, + "SWAPDB" => RedisCommand.SWAPDB, + "SYNC" => RedisCommand.SYNC, + "TIME" => RedisCommand.TIME, + "TOUCH" => RedisCommand.TOUCH, + "TTL" => RedisCommand.TTL, + "TYPE" => RedisCommand.TYPE, + "UNLINK" => RedisCommand.UNLINK, + "UNSUBSCRIBE" => RedisCommand.UNSUBSCRIBE, + "UNWATCH" => RedisCommand.UNWATCH, + "VADD" => RedisCommand.VADD, + "VCARD" => RedisCommand.VCARD, + "VDIM" => RedisCommand.VDIM, + "VEMB" => RedisCommand.VEMB, + "VGETATTR" => RedisCommand.VGETATTR, + "VINFO" => RedisCommand.VINFO, + "VISMEMBER" => RedisCommand.VISMEMBER, + "VLINKS" => RedisCommand.VLINKS, + "VRANDMEMBER" => RedisCommand.VRANDMEMBER, + "VREM" => RedisCommand.VREM, + "VSETATTR" => RedisCommand.VSETATTR, + "VSIM" => RedisCommand.VSIM, + "WATCH" => RedisCommand.WATCH, + "XACK" => RedisCommand.XACK, + "XACKDEL" => RedisCommand.XACKDEL, + "XADD" => RedisCommand.XADD, + "XAUTOCLAIM" => RedisCommand.XAUTOCLAIM, + "XCLAIM" => RedisCommand.XCLAIM, + "XCFGSET" => RedisCommand.XCFGSET, + "XDEL" => RedisCommand.XDEL, + "XDELEX" => RedisCommand.XDELEX, + "XGROUP" => RedisCommand.XGROUP, + "XINFO" => RedisCommand.XINFO, + "XLEN" => RedisCommand.XLEN, + "XPENDING" => RedisCommand.XPENDING, + "XRANGE" => RedisCommand.XRANGE, + "XREAD" => RedisCommand.XREAD, + "XREADGROUP" => RedisCommand.XREADGROUP, + "XREVRANGE" => RedisCommand.XREVRANGE, + "XTRIM" => RedisCommand.XTRIM, + "ZADD" => RedisCommand.ZADD, + "ZCARD" => RedisCommand.ZCARD, + "ZCOUNT" => RedisCommand.ZCOUNT, + "ZDIFF" => RedisCommand.ZDIFF, + "ZDIFFSTORE" => RedisCommand.ZDIFFSTORE, + "ZINCRBY" => RedisCommand.ZINCRBY, + "ZINTER" => RedisCommand.ZINTER, + "ZINTERCARD" => RedisCommand.ZINTERCARD, + "ZINTERSTORE" => RedisCommand.ZINTERSTORE, + "ZLEXCOUNT" => RedisCommand.ZLEXCOUNT, + "ZMPOP" => RedisCommand.ZMPOP, + "ZMSCORE" => RedisCommand.ZMSCORE, + "ZPOPMAX" => RedisCommand.ZPOPMAX, + "ZPOPMIN" => RedisCommand.ZPOPMIN, + "ZRANDMEMBER" => RedisCommand.ZRANDMEMBER, + "ZRANGE" => RedisCommand.ZRANGE, + "ZRANGEBYLEX" => RedisCommand.ZRANGEBYLEX, + "ZRANGEBYSCORE" => RedisCommand.ZRANGEBYSCORE, + "ZRANGESTORE" => RedisCommand.ZRANGESTORE, + "ZRANK" => RedisCommand.ZRANK, + "ZREM" => RedisCommand.ZREM, + "ZREMRANGEBYLEX" => RedisCommand.ZREMRANGEBYLEX, + "ZREMRANGEBYRANK" => RedisCommand.ZREMRANGEBYRANK, + "ZREMRANGEBYSCORE" => RedisCommand.ZREMRANGEBYSCORE, + "ZREVRANGE" => RedisCommand.ZREVRANGE, + "ZREVRANGEBYLEX" => RedisCommand.ZREVRANGEBYLEX, + "ZREVRANGEBYSCORE" => RedisCommand.ZREVRANGEBYSCORE, + "ZREVRANK" => RedisCommand.ZREVRANK, + "ZSCAN" => RedisCommand.ZSCAN, + "ZSCORE" => RedisCommand.ZSCORE, + "ZUNION" => RedisCommand.ZUNION, + "ZUNIONSTORE" => RedisCommand.ZUNIONSTORE, + "UNKNOWN" => RedisCommand.UNKNOWN, + _ => (RedisCommand)(-1), + }; + if (r == (RedisCommand)(-1)) + { + r = default; + return false; + } + + return true; + } + + [AsciiHash] + internal static partial bool TryParse_CS(ReadOnlySpan value, out RedisCommand command); + + [AsciiHash] + internal static partial bool TryParse_CS(ReadOnlySpan value, out RedisCommand command); + + [AsciiHash(CaseSensitive = false)] + internal static partial bool TryParse_CI(ReadOnlySpan value, out RedisCommand command); + + [AsciiHash(CaseSensitive = false)] + internal static partial bool TryParse_CI(ReadOnlySpan value, out RedisCommand command); + + public enum RedisCommand + { + NONE, // must be first for "zero reasons" + + APPEND, + ASKING, + AUTH, + + BGREWRITEAOF, + BGSAVE, + BITCOUNT, + BITOP, + BITPOS, + BLPOP, + BRPOP, + BRPOPLPUSH, + + CLIENT, + CLUSTER, + CONFIG, + COPY, + COMMAND, + + DBSIZE, + DEBUG, + DECR, + DECRBY, + DEL, + DELEX, + DIGEST, + DISCARD, + DUMP, + + ECHO, + EVAL, + EVALSHA, + EVAL_RO, + EVALSHA_RO, + EXEC, + EXISTS, + EXPIRE, + EXPIREAT, + EXPIRETIME, + + FLUSHALL, + FLUSHDB, + + GEOADD, + GEODIST, + GEOHASH, + GEOPOS, + GEORADIUS, + GEORADIUSBYMEMBER, + GEOSEARCH, + GEOSEARCHSTORE, + + GET, + GETBIT, + GETDEL, + GETEX, + GETRANGE, + GETSET, + + HDEL, + HELLO, + HEXISTS, + HEXPIRE, + HEXPIREAT, + HEXPIRETIME, + HGET, + HGETEX, + HGETDEL, + HGETALL, + HINCRBY, + HINCRBYFLOAT, + HKEYS, + HLEN, + HMGET, + HMSET, + HOTKEYS, + HPERSIST, + HPEXPIRE, + HPEXPIREAT, + HPEXPIRETIME, + HPTTL, + HRANDFIELD, + HSCAN, + HSET, + HSETEX, + HSETNX, + HSTRLEN, + HVALS, + + INCR, + INCRBY, + INCRBYFLOAT, + INFO, + + KEYS, + + LASTSAVE, + LATENCY, + LCS, + LINDEX, + LINSERT, + LLEN, + LMOVE, + LMPOP, + LPOP, + LPOS, + LPUSH, + LPUSHX, + LRANGE, + LREM, + LSET, + LTRIM, + + MEMORY, + MGET, + MIGRATE, + MONITOR, + MOVE, + MSET, + MSETEX, + MSETNX, + MULTI, + + OBJECT, + + PERSIST, + PEXPIRE, + PEXPIREAT, + PEXPIRETIME, + PFADD, + PFCOUNT, + PFMERGE, + PING, + PSETEX, + PSUBSCRIBE, + PTTL, + PUBLISH, + PUBSUB, + PUNSUBSCRIBE, + + QUIT, + + RANDOMKEY, + READONLY, + READWRITE, + RENAME, + RENAMENX, + REPLICAOF, + RESTORE, + ROLE, + RPOP, + RPOPLPUSH, + RPUSH, + RPUSHX, + + SADD, + SAVE, + SCAN, + SCARD, + SCRIPT, + SDIFF, + SDIFFSTORE, + SELECT, + SENTINEL, + SET, + SETBIT, + SETEX, + SETNX, + SETRANGE, + SHUTDOWN, + SINTER, + SINTERCARD, + SINTERSTORE, + SISMEMBER, + SLAVEOF, + SLOWLOG, + SMEMBERS, + SMISMEMBER, + SMOVE, + SORT, + SORT_RO, + SPOP, + SPUBLISH, + SRANDMEMBER, + SREM, + STRLEN, + SUBSCRIBE, + SUNION, + SUNIONSTORE, + SSCAN, + SSUBSCRIBE, + SUNSUBSCRIBE, + SWAPDB, + SYNC, + + TIME, + TOUCH, + TTL, + TYPE, + + UNLINK, + UNSUBSCRIBE, + UNWATCH, + + VADD, + VCARD, + VDIM, + VEMB, + VGETATTR, + VINFO, + VISMEMBER, + VLINKS, + VRANDMEMBER, + VREM, + VSETATTR, + VSIM, + + WATCH, + + XACK, + XACKDEL, + XADD, + XAUTOCLAIM, + XCLAIM, + XCFGSET, + XDEL, + XDELEX, + XGROUP, + XINFO, + XLEN, + XPENDING, + XRANGE, + XREAD, + XREADGROUP, + XREVRANGE, + XTRIM, + + ZADD, + ZCARD, + ZCOUNT, + ZDIFF, + ZDIFFSTORE, + ZINCRBY, + ZINTER, + ZINTERCARD, + ZINTERSTORE, + ZLEXCOUNT, + ZMPOP, + ZMSCORE, + ZPOPMAX, + ZPOPMIN, + ZRANDMEMBER, + ZRANGE, + ZRANGEBYLEX, + ZRANGEBYSCORE, + ZRANGESTORE, + ZRANK, + ZREM, + ZREMRANGEBYLEX, + ZREMRANGEBYRANK, + ZREMRANGEBYSCORE, + ZREVRANGE, + ZREVRANGEBYLEX, + ZREVRANGEBYSCORE, + ZREVRANK, + ZSCAN, + ZSCORE, + ZUNION, + ZUNIONSTORE, + + UNKNOWN, + } +} diff --git a/tests/StackExchange.Redis.Benchmarks/FormatBenchmarks.cs b/tests/StackExchange.Redis.Benchmarks/FormatBenchmarks.cs index 77548b254..714e1724a 100644 --- a/tests/StackExchange.Redis.Benchmarks/FormatBenchmarks.cs +++ b/tests/StackExchange.Redis.Benchmarks/FormatBenchmarks.cs @@ -1,4 +1,5 @@ -using System; +/* +using System; using System.Net; using BenchmarkDotNet.Attributes; @@ -51,3 +52,4 @@ public void Setup() { } public EndPoint ParseEndPoint(string host, int port) => Format.ParseEndPoint(host, port); } } +*/ diff --git a/tests/StackExchange.Redis.Benchmarks/Program.cs b/tests/StackExchange.Redis.Benchmarks/Program.cs index 311202877..3999a61b4 100644 --- a/tests/StackExchange.Redis.Benchmarks/Program.cs +++ b/tests/StackExchange.Redis.Benchmarks/Program.cs @@ -9,13 +9,14 @@ internal static class Program private static void Main(string[] args) { #if DEBUG - var obj = new FastHashBenchmarks(); + var obj = new AsciiHashBenchmarks(); foreach (var size in obj.Sizes) { Console.WriteLine($"Size: {size}"); obj.Size = size; obj.Setup(); - obj.Hash64(); + obj.HashCS_C(); + obj.HashCS_B(); } #else BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); diff --git a/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj b/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj index 8b335ab02..47359ac85 100644 --- a/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj +++ b/tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj @@ -10,7 +10,8 @@ - - + + + diff --git a/tests/StackExchange.Redis.Tests/App.config b/tests/StackExchange.Redis.Tests/App.config index c7c0b6d7a..295bdd49d 100644 --- a/tests/StackExchange.Redis.Tests/App.config +++ b/tests/StackExchange.Redis.Tests/App.config @@ -4,7 +4,7 @@ - + diff --git a/tests/StackExchange.Redis.Tests/AsciiHashUnitTests.cs b/tests/StackExchange.Redis.Tests/AsciiHashUnitTests.cs new file mode 100644 index 000000000..5e2b9571f --- /dev/null +++ b/tests/StackExchange.Redis.Tests/AsciiHashUnitTests.cs @@ -0,0 +1,460 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using RESPite; +using Xunit; +using Xunit.Sdk; + +#pragma warning disable CS8981, SA1134, SA1300, SA1303, SA1502 // names are weird in this test! +// ReSharper disable InconsistentNaming - to better represent expected literals +// ReSharper disable IdentifierTypo +namespace StackExchange.Redis.Tests; + +public partial class AsciiHashUnitTests +{ + // note: if the hashing algorithm changes, we can update the last parameter freely; it doesn't matter + // what it *is* - what matters is that we can see that it has entropy between different values + [Theory] + [InlineData(1, a.Length, a.Text, a.HashCS, 97)] + [InlineData(2, ab.Length, ab.Text, ab.HashCS, 25185)] + [InlineData(3, abc.Length, abc.Text, abc.HashCS, 6513249)] + [InlineData(4, abcd.Length, abcd.Text, abcd.HashCS, 1684234849)] + [InlineData(5, abcde.Length, abcde.Text, abcde.HashCS, 435475931745)] + [InlineData(6, abcdef.Length, abcdef.Text, abcdef.HashCS, 112585661964897)] + [InlineData(7, abcdefg.Length, abcdefg.Text, abcdefg.HashCS, 29104508263162465)] + [InlineData(8, abcdefgh.Length, abcdefgh.Text, abcdefgh.HashCS, 7523094288207667809)] + + [InlineData(1, x.Length, x.Text, x.HashCS, 120)] + [InlineData(2, xx.Length, xx.Text, xx.HashCS, 30840)] + [InlineData(3, xxx.Length, xxx.Text, xxx.HashCS, 7895160)] + [InlineData(4, xxxx.Length, xxxx.Text, xxxx.HashCS, 2021161080)] + [InlineData(5, xxxxx.Length, xxxxx.Text, xxxxx.HashCS, 517417236600)] + [InlineData(6, xxxxxx.Length, xxxxxx.Text, xxxxxx.HashCS, 132458812569720)] + [InlineData(7, xxxxxxx.Length, xxxxxxx.Text, xxxxxxx.HashCS, 33909456017848440)] + [InlineData(8, xxxxxxxx.Length, xxxxxxxx.Text, xxxxxxxx.HashCS, 8680820740569200760)] + + [InlineData(20, abcdefghijklmnopqrst.Length, abcdefghijklmnopqrst.Text, abcdefghijklmnopqrst.HashCS, 7523094288207667809)] + + // show that foo_bar is interpreted as foo-bar + [InlineData(7, foo_bar.Length, foo_bar.Text, foo_bar.HashCS, 32195221641981798, "foo-bar", nameof(foo_bar))] + [InlineData(7, foo_bar_hyphen.Length, foo_bar_hyphen.Text, foo_bar_hyphen.HashCS, 32195221641981798, "foo-bar", nameof(foo_bar_hyphen))] + [InlineData(7, foo_bar_underscore.Length, foo_bar_underscore.Text, foo_bar_underscore.HashCS, 32195222480842598, "foo_bar", nameof(foo_bar_underscore))] + public void Validate(int expectedLength, int actualLength, string actualValue, long actualHash, long expectedHash, string? expectedValue = null, string originForDisambiguation = "") + { + _ = originForDisambiguation; // to allow otherwise-identical test data to coexist + Assert.Equal(expectedLength, actualLength); + Assert.Equal(expectedHash, actualHash); + var bytes = Encoding.UTF8.GetBytes(actualValue); + Assert.Equal(expectedLength, bytes.Length); + Assert.Equal(expectedHash, AsciiHash.HashCS(bytes)); + Assert.Equal(expectedHash, AsciiHash.HashCS(actualValue.AsSpan())); + + if (expectedValue is not null) + { + Assert.Equal(expectedValue, actualValue); + } + } + + [Fact] + public void AsciiHashIs_Short() + { + ReadOnlySpan value = "abc"u8; + var hash = AsciiHash.HashCS(value); + Assert.Equal(abc.HashCS, hash); + Assert.True(abc.IsCS(value, hash)); + + value = "abz"u8; + hash = AsciiHash.HashCS(value); + Assert.NotEqual(abc.HashCS, hash); + Assert.False(abc.IsCS(value, hash)); + } + + [Fact] + public void AsciiHashIs_Long() + { + ReadOnlySpan value = "abcdefghijklmnopqrst"u8; + var hash = AsciiHash.HashCS(value); + Assert.Equal(abcdefghijklmnopqrst.HashCS, hash); + Assert.True(abcdefghijklmnopqrst.IsCS(value, hash)); + + value = "abcdefghijklmnopqrsz"u8; + hash = AsciiHash.HashCS(value); + Assert.Equal(abcdefghijklmnopqrst.HashCS, hash); // hash collision, fine + Assert.False(abcdefghijklmnopqrst.IsCS(value, hash)); + } + + // Test case-sensitive and case-insensitive equality for various lengths + [Theory] + [InlineData("a")] // length 1 + [InlineData("ab")] // length 2 + [InlineData("abc")] // length 3 + [InlineData("abcd")] // length 4 + [InlineData("abcde")] // length 5 + [InlineData("abcdef")] // length 6 + [InlineData("abcdefg")] // length 7 + [InlineData("abcdefgh")] // length 8 + [InlineData("abcdefghi")] // length 9 + [InlineData("abcdefghij")] // length 10 + [InlineData("abcdefghijklmnop")] // length 16 + [InlineData("abcdefghijklmnopqrst")] // length 20 + public void CaseSensitiveEquality(string text) + { + var lower = Encoding.UTF8.GetBytes(text); + var upper = Encoding.UTF8.GetBytes(text.ToUpperInvariant()); + + var hashLowerCS = AsciiHash.HashCS(lower); + var hashUpperCS = AsciiHash.HashCS(upper); + + // Case-sensitive: same case should match + Assert.True(AsciiHash.EqualsCS(lower, lower), "CS: lower == lower"); + Assert.True(AsciiHash.EqualsCS(upper, upper), "CS: upper == upper"); + + // Case-sensitive: different case should NOT match + Assert.False(AsciiHash.EqualsCS(lower, upper), "CS: lower != upper"); + Assert.False(AsciiHash.EqualsCS(upper, lower), "CS: upper != lower"); + + // Hashes should be different for different cases + Assert.NotEqual(hashLowerCS, hashUpperCS); + } + + [Theory] + [InlineData("a")] // length 1 + [InlineData("ab")] // length 2 + [InlineData("abc")] // length 3 + [InlineData("abcd")] // length 4 + [InlineData("abcde")] // length 5 + [InlineData("abcdef")] // length 6 + [InlineData("abcdefg")] // length 7 + [InlineData("abcdefgh")] // length 8 + [InlineData("abcdefghi")] // length 9 + [InlineData("abcdefghij")] // length 10 + [InlineData("abcdefghijklmnop")] // length 16 + [InlineData("abcdefghijklmnopqrst")] // length 20 + public void CaseInsensitiveEquality(string text) + { + var lower = Encoding.UTF8.GetBytes(text); + var upper = Encoding.UTF8.GetBytes(text.ToUpperInvariant()); + + var hashLowerUC = AsciiHash.HashUC(lower); + var hashUpperUC = AsciiHash.HashUC(upper); + + // Case-insensitive: same case should match + Assert.True(AsciiHash.EqualsCI(lower, lower), "CI: lower == lower"); + Assert.True(AsciiHash.EqualsCI(upper, upper), "CI: upper == upper"); + + // Case-insensitive: different case SHOULD match + Assert.True(AsciiHash.EqualsCI(lower, upper), "CI: lower == upper"); + Assert.True(AsciiHash.EqualsCI(upper, lower), "CI: upper == lower"); + + // CI hashes should be the same for different cases + Assert.Equal(hashLowerUC, hashUpperUC); + } + + [Theory] + [InlineData("a")] // length 1 + [InlineData("ab")] // length 2 + [InlineData("abc")] // length 3 + [InlineData("abcd")] // length 4 + [InlineData("abcde")] // length 5 + [InlineData("abcdef")] // length 6 + [InlineData("abcdefg")] // length 7 + [InlineData("abcdefgh")] // length 8 + [InlineData("abcdefghi")] // length 9 + [InlineData("abcdefghij")] // length 10 + [InlineData("abcdefghijklmnop")] // length 16 + [InlineData("abcdefghijklmnopqrst")] // length 20 + [InlineData("foo-bar")] // foo_bar_hyphen + [InlineData("foo_bar")] // foo_bar_underscore + public void GeneratedTypes_CaseSensitive(string text) + { + var lower = Encoding.UTF8.GetBytes(text); + var upper = Encoding.UTF8.GetBytes(text.ToUpperInvariant()); + + var hashLowerCS = AsciiHash.HashCS(lower); + var hashUpperCS = AsciiHash.HashCS(upper); + + // Use the generated types to verify CS behavior + switch (text) + { + case "a": + Assert.True(a.IsCS(lower, hashLowerCS)); + Assert.False(a.IsCS(lower, hashUpperCS)); + break; + case "ab": + Assert.True(ab.IsCS(lower, hashLowerCS)); + Assert.False(ab.IsCS(lower, hashUpperCS)); + break; + case "abc": + Assert.True(abc.IsCS(lower, hashLowerCS)); + Assert.False(abc.IsCS(lower, hashUpperCS)); + break; + case "abcd": + Assert.True(abcd.IsCS(lower, hashLowerCS)); + Assert.False(abcd.IsCS(lower, hashUpperCS)); + break; + case "abcde": + Assert.True(abcde.IsCS(lower, hashLowerCS)); + Assert.False(abcde.IsCS(lower, hashUpperCS)); + break; + case "abcdef": + Assert.True(abcdef.IsCS(lower, hashLowerCS)); + Assert.False(abcdef.IsCS(lower, hashUpperCS)); + break; + case "abcdefg": + Assert.True(abcdefg.IsCS(lower, hashLowerCS)); + Assert.False(abcdefg.IsCS(lower, hashUpperCS)); + break; + case "abcdefgh": + Assert.True(abcdefgh.IsCS(lower, hashLowerCS)); + Assert.False(abcdefgh.IsCS(lower, hashUpperCS)); + break; + case "abcdefghijklmnopqrst": + Assert.True(abcdefghijklmnopqrst.IsCS(lower, hashLowerCS)); + Assert.False(abcdefghijklmnopqrst.IsCS(lower, hashUpperCS)); + break; + case "foo-bar": + Assert.True(foo_bar_hyphen.IsCS(lower, hashLowerCS)); + Assert.False(foo_bar_hyphen.IsCS(lower, hashUpperCS)); + break; + case "foo_bar": + Assert.True(foo_bar_underscore.IsCS(lower, hashLowerCS)); + Assert.False(foo_bar_underscore.IsCS(lower, hashUpperCS)); + break; + } + } + + [Theory] + [InlineData("a")] // length 1 + [InlineData("ab")] // length 2 + [InlineData("abc")] // length 3 + [InlineData("abcd")] // length 4 + [InlineData("abcde")] // length 5 + [InlineData("abcdef")] // length 6 + [InlineData("abcdefg")] // length 7 + [InlineData("abcdefgh")] // length 8 + [InlineData("abcdefghi")] // length 9 + [InlineData("abcdefghij")] // length 10 + [InlineData("abcdefghijklmnop")] // length 16 + [InlineData("abcdefghijklmnopqrst")] // length 20 + [InlineData("foo-bar")] // foo_bar_hyphen + [InlineData("foo_bar")] // foo_bar_underscore + public void GeneratedTypes_CaseInsensitive(string text) + { + var lower = Encoding.UTF8.GetBytes(text); + var upper = Encoding.UTF8.GetBytes(text.ToUpperInvariant()); + + var hashLowerUC = AsciiHash.HashUC(lower); + var hashUpperUC = AsciiHash.HashUC(upper); + + // Use the generated types to verify CI behavior + switch (text) + { + case "a": + Assert.True(a.IsCI(lower, hashLowerUC)); + Assert.True(a.IsCI(upper, hashUpperUC)); + break; + case "ab": + Assert.True(ab.IsCI(lower, hashLowerUC)); + Assert.True(ab.IsCI(upper, hashUpperUC)); + break; + case "abc": + Assert.True(abc.IsCI(lower, hashLowerUC)); + Assert.True(abc.IsCI(upper, hashUpperUC)); + break; + case "abcd": + Assert.True(abcd.IsCI(lower, hashLowerUC)); + Assert.True(abcd.IsCI(upper, hashUpperUC)); + break; + case "abcde": + Assert.True(abcde.IsCI(lower, hashLowerUC)); + Assert.True(abcde.IsCI(upper, hashUpperUC)); + break; + case "abcdef": + Assert.True(abcdef.IsCI(lower, hashLowerUC)); + Assert.True(abcdef.IsCI(upper, hashUpperUC)); + break; + case "abcdefg": + Assert.True(abcdefg.IsCI(lower, hashLowerUC)); + Assert.True(abcdefg.IsCI(upper, hashUpperUC)); + break; + case "abcdefgh": + Assert.True(abcdefgh.IsCI(lower, hashLowerUC)); + Assert.True(abcdefgh.IsCI(upper, hashUpperUC)); + break; + case "abcdefghijklmnopqrst": + Assert.True(abcdefghijklmnopqrst.IsCI(lower, hashLowerUC)); + Assert.True(abcdefghijklmnopqrst.IsCI(upper, hashUpperUC)); + break; + case "foo-bar": + Assert.True(foo_bar_hyphen.IsCI(lower, hashLowerUC)); + Assert.True(foo_bar_hyphen.IsCI(upper, hashUpperUC)); + break; + case "foo_bar": + Assert.True(foo_bar_underscore.IsCI(lower, hashLowerUC)); + Assert.True(foo_bar_underscore.IsCI(upper, hashUpperUC)); + break; + } + } + + // Test each generated AsciiHash type individually for case sensitivity + [Fact] + public void GeneratedType_a_CaseSensitivity() + { + ReadOnlySpan lower = "a"u8; + ReadOnlySpan upper = "A"u8; + + Assert.True(a.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(a.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(a.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(a.IsCI(upper, AsciiHash.HashUC(upper))); + } + + [Fact] + public void GeneratedType_ab_CaseSensitivity() + { + ReadOnlySpan lower = "ab"u8; + ReadOnlySpan upper = "AB"u8; + + Assert.True(ab.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(ab.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(ab.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(ab.IsCI(upper, AsciiHash.HashUC(upper))); + } + + [Fact] + public void GeneratedType_abc_CaseSensitivity() + { + ReadOnlySpan lower = "abc"u8; + ReadOnlySpan upper = "ABC"u8; + + Assert.True(abc.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(abc.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(abc.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(abc.IsCI(upper, AsciiHash.HashUC(upper))); + } + + [Fact] + public void GeneratedType_abcd_CaseSensitivity() + { + ReadOnlySpan lower = "abcd"u8; + ReadOnlySpan upper = "ABCD"u8; + + Assert.True(abcd.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(abcd.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(abcd.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(abcd.IsCI(upper, AsciiHash.HashUC(upper))); + } + + [Fact] + public void GeneratedType_abcde_CaseSensitivity() + { + ReadOnlySpan lower = "abcde"u8; + ReadOnlySpan upper = "ABCDE"u8; + + Assert.True(abcde.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(abcde.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(abcde.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(abcde.IsCI(upper, AsciiHash.HashUC(upper))); + } + + [Fact] + public void GeneratedType_abcdef_CaseSensitivity() + { + ReadOnlySpan lower = "abcdef"u8; + ReadOnlySpan upper = "ABCDEF"u8; + + Assert.True(abcdef.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(abcdef.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(abcdef.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(abcdef.IsCI(upper, AsciiHash.HashUC(upper))); + } + + [Fact] + public void GeneratedType_abcdefg_CaseSensitivity() + { + ReadOnlySpan lower = "abcdefg"u8; + ReadOnlySpan upper = "ABCDEFG"u8; + + Assert.True(abcdefg.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(abcdefg.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(abcdefg.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(abcdefg.IsCI(upper, AsciiHash.HashUC(upper))); + } + + [Fact] + public void GeneratedType_abcdefgh_CaseSensitivity() + { + ReadOnlySpan lower = "abcdefgh"u8; + ReadOnlySpan upper = "ABCDEFGH"u8; + + Assert.True(abcdefgh.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(abcdefgh.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(abcdefgh.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(abcdefgh.IsCI(upper, AsciiHash.HashUC(upper))); + } + + [Fact] + public void GeneratedType_abcdefghijklmnopqrst_CaseSensitivity() + { + ReadOnlySpan lower = "abcdefghijklmnopqrst"u8; + ReadOnlySpan upper = "ABCDEFGHIJKLMNOPQRST"u8; + + Assert.True(abcdefghijklmnopqrst.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(abcdefghijklmnopqrst.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(abcdefghijklmnopqrst.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(abcdefghijklmnopqrst.IsCI(upper, AsciiHash.HashUC(upper))); + } + + [Fact] + public void GeneratedType_foo_bar_CaseSensitivity() + { + // foo_bar is interpreted as foo-bar + ReadOnlySpan lower = "foo-bar"u8; + ReadOnlySpan upper = "FOO-BAR"u8; + + Assert.True(foo_bar.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(foo_bar.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(foo_bar.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(foo_bar.IsCI(upper, AsciiHash.HashUC(upper))); + } + + [Fact] + public void GeneratedType_foo_bar_hyphen_CaseSensitivity() + { + // foo_bar_hyphen is explicitly "foo-bar" + ReadOnlySpan lower = "foo-bar"u8; + ReadOnlySpan upper = "FOO-BAR"u8; + + Assert.True(foo_bar_hyphen.IsCS(lower, AsciiHash.HashCS(lower))); + Assert.False(foo_bar_hyphen.IsCS(upper, AsciiHash.HashCS(upper))); + Assert.True(foo_bar_hyphen.IsCI(lower, AsciiHash.HashUC(lower))); + Assert.True(foo_bar_hyphen.IsCI(upper, AsciiHash.HashUC(upper))); + } + + [AsciiHash] private static partial class a { } + [AsciiHash] private static partial class ab { } + [AsciiHash] private static partial class abc { } + [AsciiHash] private static partial class abcd { } + [AsciiHash] private static partial class abcde { } + [AsciiHash] private static partial class abcdef { } + [AsciiHash] private static partial class abcdefg { } + [AsciiHash] private static partial class abcdefgh { } + + [AsciiHash] private static partial class abcdefghijklmnopqrst { } + + // show that foo_bar and foo-bar are different + [AsciiHash] private static partial class foo_bar { } + [AsciiHash("foo-bar")] private static partial class foo_bar_hyphen { } + [AsciiHash("foo_bar")] private static partial class foo_bar_underscore { } + + [AsciiHash] private static partial class 窓 { } + + [AsciiHash] private static partial class x { } + [AsciiHash] private static partial class xx { } + [AsciiHash] private static partial class xxx { } + [AsciiHash] private static partial class xxxx { } + [AsciiHash] private static partial class xxxxx { } + [AsciiHash] private static partial class xxxxxx { } + [AsciiHash] private static partial class xxxxxxx { } + [AsciiHash] private static partial class xxxxxxxx { } +} diff --git a/tests/StackExchange.Redis.Tests/BasicOpTests.cs b/tests/StackExchange.Redis.Tests/BasicOpTests.cs index 40fb42947..2b045823d 100644 --- a/tests/StackExchange.Redis.Tests/BasicOpTests.cs +++ b/tests/StackExchange.Redis.Tests/BasicOpTests.cs @@ -17,6 +17,7 @@ public class BasicOpsTests(ITestOutputHelper output, SharedConnectionFixture fix public class InProcBasicOpsTests(ITestOutputHelper output, InProcServerFixture fixture) : BasicOpsTestsBase(output, null, fixture) { + protected override bool UseDedicatedInProcessServer => true; } */ @@ -27,7 +28,7 @@ public abstract class BasicOpsTestsBase(ITestOutputHelper output, SharedConnecti [Fact] public async Task PingOnce() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); var duration = await db.PingAsync().ForAwait(); @@ -35,17 +36,18 @@ public async Task PingOnce() Assert.True(duration.TotalMilliseconds > 0); } - [Fact(Skip = "This needs some CI love, it's not a scenario we care about too much but noisy atm.")] + [Fact] public async Task RapidDispose() { - await using var primary = Create(); + SkipIfWouldUseRealServer("This needs some CI love, it's not a scenario we care about too much but noisy atm."); + await using var primary = ConnectFactory(); var db = primary.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); for (int i = 0; i < 10; i++) { - await using var secondary = Create(fail: true, shared: false); + await using var secondary = primary.CreateClient(); secondary.GetDatabase().StringIncrement(key, flags: CommandFlags.FireAndForget); } // Give it a moment to get through the pipe...they were fire and forget @@ -56,7 +58,7 @@ public async Task RapidDispose() [Fact] public async Task PingMany() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); var tasks = new Task[100]; for (int i = 0; i < tasks.Length; i++) @@ -71,7 +73,7 @@ public async Task PingMany() [Fact] public async Task GetWithNullKey() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); const string? key = null; var ex = Assert.Throws(() => db.StringGet(key)); @@ -81,7 +83,7 @@ public async Task GetWithNullKey() [Fact] public async Task SetWithNullKey() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); const string? key = null, value = "abc"; var ex = Assert.Throws(() => db.StringSet(key!, value)); @@ -91,7 +93,7 @@ public async Task SetWithNullKey() [Fact] public async Task SetWithNullValue() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); string key = Me(); const string? value = null; @@ -109,7 +111,7 @@ public async Task SetWithNullValue() [Fact] public async Task SetWithDefaultValue() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); string key = Me(); var value = default(RedisValue); // this is kinda 0... ish @@ -127,7 +129,7 @@ public async Task SetWithDefaultValue() [Fact] public async Task SetWithZeroValue() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); string key = Me(); const long value = 0; @@ -145,7 +147,7 @@ public async Task SetWithZeroValue() [Fact] public async Task GetSetAsync() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -170,7 +172,7 @@ public async Task GetSetAsync() [Fact] public async Task GetSetSync() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -197,7 +199,7 @@ public async Task GetSetSync() [InlineData(true, false)] public async Task GetWithExpiry(bool exists, bool hasExpiry) { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -233,7 +235,7 @@ public async Task GetWithExpiry(bool exists, bool hasExpiry) [Fact] public async Task GetWithExpiryWrongTypeAsync() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); RedisKey key = Me(); _ = db.KeyDeleteAsync(key); @@ -256,11 +258,11 @@ public async Task GetWithExpiryWrongTypeAsync() [Fact] public async Task GetWithExpiryWrongTypeSync() { + await using var conn = ConnectFactory(); + var db = conn.GetDatabase(); RedisKey key = Me(); var ex = await Assert.ThrowsAsync(async () => { - await using var conn = Create(); - var db = conn.GetDatabase(); db.KeyDelete(key, CommandFlags.FireAndForget); db.SetAdd(key, "abc", CommandFlags.FireAndForget); db.StringGetWithExpiry(key); @@ -272,13 +274,15 @@ public async Task GetWithExpiryWrongTypeSync() [Fact] public async Task TestSevered() { - SetExpectedAmbientFailureCount(2); - await using var conn = Create(allowAdmin: true, shared: false); + await using var conn = ConnectFactory(allowAdmin: true, shared: false); var db = conn.GetDatabase(); string key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); db.StringSet(key, key, flags: CommandFlags.FireAndForget); - var server = GetServer(conn); + var server = GetServer(conn.DefaultClient); + Assert.SkipUnless(server.CanSimulateConnectionFailure(), "Skipping because server cannot simulate connection failure"); + + SetExpectedAmbientFailureCount(2); server.SimulateConnectionFailure(SimulatedFailureType.All); var watch = Stopwatch.StartNew(); await UntilConditionAsync(TimeSpan.FromSeconds(10), () => server.IsConnected); @@ -293,7 +297,7 @@ public async Task TestSevered() [Fact] public async Task IncrAsync() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -321,7 +325,7 @@ public async Task IncrAsync() [Fact] public async Task IncrSync() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); RedisKey key = Me(); Log(key); @@ -350,7 +354,7 @@ public async Task IncrSync() [Fact] public async Task IncrDifferentSizes() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); RedisKey key = Me(); db.KeyDelete(key, CommandFlags.FireAndForget); @@ -380,30 +384,10 @@ private static void Incr(IDatabase database, RedisKey key, int delta, ref int to total += delta; } - [Fact] - public async Task ShouldUseSharedMuxer() - { - Log($"Shared: {SharedFixtureAvailable}"); - if (SharedFixtureAvailable) - { - await using var a = Create(); - Assert.IsNotType(a); - await using var b = Create(); - Assert.Same(a, b); - } - else - { - await using var a = Create(); - Assert.IsType(a); - await using var b = Create(); - Assert.NotSame(a, b); - } - } - [Fact] public async Task Delete() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); var key = Me(); _ = db.StringSetAsync(key, "Heyyyyy"); @@ -418,7 +402,7 @@ public async Task Delete() [Fact] public async Task DeleteAsync() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); var key = Me(); _ = db.StringSetAsync(key, "Heyyyyy"); @@ -433,7 +417,7 @@ public async Task DeleteAsync() [Fact] public async Task DeleteMany() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); var key1 = Me(); var key2 = Me() + "2"; @@ -452,7 +436,7 @@ public async Task DeleteMany() [Fact] public async Task DeleteManyAsync() { - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase(); var key1 = Me(); var key2 = Me() + "2"; @@ -472,7 +456,7 @@ public async Task DeleteManyAsync() public async Task WrappedDatabasePrefixIntegration() { var key = Me(); - await using var conn = Create(); + await using var conn = ConnectFactory(); var db = conn.GetDatabase().WithKeyPrefix("abc"); db.KeyDelete(key, CommandFlags.FireAndForget); db.StringIncrement(key, flags: CommandFlags.FireAndForget); @@ -486,8 +470,8 @@ public async Task WrappedDatabasePrefixIntegration() [Fact] public async Task TransactionSync() { - await using var conn = Create(); - Assert.SkipUnless(conn.RawConfig.CommandMap.IsAvailable(RedisCommand.MULTI), "MULTI is not available"); + await using var conn = ConnectFactory(); + Assert.SkipUnless(conn.DefaultClient.RawConfig.CommandMap.IsAvailable(RedisCommand.MULTI), "MULTI is not available"); var db = conn.GetDatabase(); RedisKey key = Me(); @@ -506,8 +490,8 @@ public async Task TransactionSync() [Fact] public async Task TransactionAsync() { - await using var conn = Create(); - Assert.SkipUnless(conn.RawConfig.CommandMap.IsAvailable(RedisCommand.MULTI), "MULTI is not available"); + await using var conn = ConnectFactory(); + Assert.SkipUnless(conn.DefaultClient.RawConfig.CommandMap.IsAvailable(RedisCommand.MULTI), "MULTI is not available"); var db = conn.GetDatabase(); diff --git a/tests/StackExchange.Redis.Tests/DuplexStream.cs b/tests/StackExchange.Redis.Tests/DuplexStream.cs new file mode 100644 index 000000000..8a9b8c737 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/DuplexStream.cs @@ -0,0 +1,131 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace StackExchange.Redis.Tests; + +/// +/// Combines separate input and output streams into a single duplex stream. +/// +internal sealed class DuplexStream(Stream inputStream, Stream outputStream) : Stream +{ + private readonly Stream _inputStream = inputStream ?? throw new ArgumentNullException(nameof(inputStream)); + private readonly Stream _outputStream = outputStream ?? throw new ArgumentNullException(nameof(outputStream)); + + public override bool CanRead => _inputStream.CanRead; + public override bool CanWrite => _outputStream.CanWrite; + public override bool CanSeek => false; + public override bool CanTimeout => _inputStream.CanTimeout || _outputStream.CanTimeout; + + public override int ReadTimeout + { + get => _inputStream.ReadTimeout; + set => _inputStream.ReadTimeout = value; + } + + public override int WriteTimeout + { + get => _outputStream.WriteTimeout; + set => _outputStream.WriteTimeout = value; + } + + public override long Length => throw new NotSupportedException($"{nameof(DuplexStream)} does not support seeking."); + public override long Position + { + get => throw new NotSupportedException($"{nameof(DuplexStream)} does not support seeking."); + set => throw new NotSupportedException($"{nameof(DuplexStream)} does not support seeking."); + } + + public override int Read(byte[] buffer, int offset, int count) + => _inputStream.Read(buffer, offset, count); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => _inputStream.ReadAsync(buffer, offset, count, cancellationToken); + +#if NET + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + => _inputStream.ReadAsync(buffer, cancellationToken); + + public override int Read(Span buffer) + => _inputStream.Read(buffer); +#endif + + public override int ReadByte() + => _inputStream.ReadByte(); + + public override void Write(byte[] buffer, int offset, int count) + => _outputStream.Write(buffer, offset, count); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => _outputStream.WriteAsync(buffer, offset, count, cancellationToken); + +#if NET + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + => _outputStream.WriteAsync(buffer, cancellationToken); + + public override void Write(ReadOnlySpan buffer) + => _outputStream.Write(buffer); +#endif + + public override void WriteByte(byte value) + => _outputStream.WriteByte(value); + + public override void Flush() + => _outputStream.Flush(); + + public override Task FlushAsync(CancellationToken cancellationToken) + => _outputStream.FlushAsync(cancellationToken); + + public override long Seek(long offset, SeekOrigin origin) + => throw new NotSupportedException($"{nameof(DuplexStream)} does not support seeking."); + + public override void SetLength(long value) + => throw new NotSupportedException($"{nameof(DuplexStream)} does not support seeking."); + + public override void Close() + { + _inputStream.Close(); + _outputStream.Close(); + base.Close(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _inputStream.Dispose(); + _outputStream.Dispose(); + } + base.Dispose(disposing); + } + +#if NET + public override async ValueTask DisposeAsync() + { + await _inputStream.DisposeAsync().ConfigureAwait(false); + await _outputStream.DisposeAsync().ConfigureAwait(false); + await base.DisposeAsync().ConfigureAwait(false); + } +#endif + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => _inputStream.BeginRead(buffer, offset, count, callback, state); + + public override int EndRead(IAsyncResult asyncResult) + => _inputStream.EndRead(asyncResult); + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => _outputStream.BeginWrite(buffer, offset, count, callback, state); + + public override void EndWrite(IAsyncResult asyncResult) + => _outputStream.EndWrite(asyncResult); + +#if NET + public override void CopyTo(Stream destination, int bufferSize) + => _inputStream.CopyTo(destination, bufferSize); +#endif + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + => _inputStream.CopyToAsync(destination, bufferSize, cancellationToken); +} diff --git a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs index 53f28f163..65d6946dc 100644 --- a/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs +++ b/tests/StackExchange.Redis.Tests/ExceptionFactoryTests.cs @@ -123,7 +123,7 @@ public async Task TimeoutException() Assert.Contains("async-ops: ", ex.Message); Assert.Contains("conn-sec: n/a", ex.Message); Assert.Contains("aoc: 1", ex.Message); -#if NETCOREAPP +#if NET // ...POOL: (Threads=33,QueuedItems=0,CompletedItems=5547,Timers=60)... Assert.Contains("POOL: ", ex.Message); Assert.Contains("Threads=", ex.Message); diff --git a/tests/StackExchange.Redis.Tests/FailoverTests.cs b/tests/StackExchange.Redis.Tests/FailoverTests.cs index 825c8efce..9c330a3f3 100644 --- a/tests/StackExchange.Redis.Tests/FailoverTests.cs +++ b/tests/StackExchange.Redis.Tests/FailoverTests.cs @@ -1,4 +1,4 @@ -#if NET6_0_OR_GREATER +#if NET using System; using System.IO; using System.Threading; diff --git a/tests/StackExchange.Redis.Tests/FastHashTests.cs b/tests/StackExchange.Redis.Tests/FastHashTests.cs deleted file mode 100644 index a032cfc80..000000000 --- a/tests/StackExchange.Redis.Tests/FastHashTests.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Text; -using Xunit; -using Xunit.Sdk; - -#pragma warning disable CS8981, SA1134, SA1300, SA1303, SA1502 // names are weird in this test! -// ReSharper disable InconsistentNaming - to better represent expected literals -// ReSharper disable IdentifierTypo -namespace StackExchange.Redis.Tests; - -public partial class FastHashTests(ITestOutputHelper log) -{ - // note: if the hashing algorithm changes, we can update the last parameter freely; it doesn't matter - // what it *is* - what matters is that we can see that it has entropy between different values - [Theory] - [InlineData(1, a.Length, a.Text, a.Hash, 97)] - [InlineData(2, ab.Length, ab.Text, ab.Hash, 25185)] - [InlineData(3, abc.Length, abc.Text, abc.Hash, 6513249)] - [InlineData(4, abcd.Length, abcd.Text, abcd.Hash, 1684234849)] - [InlineData(5, abcde.Length, abcde.Text, abcde.Hash, 435475931745)] - [InlineData(6, abcdef.Length, abcdef.Text, abcdef.Hash, 112585661964897)] - [InlineData(7, abcdefg.Length, abcdefg.Text, abcdefg.Hash, 29104508263162465)] - [InlineData(8, abcdefgh.Length, abcdefgh.Text, abcdefgh.Hash, 7523094288207667809)] - - [InlineData(1, x.Length, x.Text, x.Hash, 120)] - [InlineData(2, xx.Length, xx.Text, xx.Hash, 30840)] - [InlineData(3, xxx.Length, xxx.Text, xxx.Hash, 7895160)] - [InlineData(4, xxxx.Length, xxxx.Text, xxxx.Hash, 2021161080)] - [InlineData(5, xxxxx.Length, xxxxx.Text, xxxxx.Hash, 517417236600)] - [InlineData(6, xxxxxx.Length, xxxxxx.Text, xxxxxx.Hash, 132458812569720)] - [InlineData(7, xxxxxxx.Length, xxxxxxx.Text, xxxxxxx.Hash, 33909456017848440)] - [InlineData(8, xxxxxxxx.Length, xxxxxxxx.Text, xxxxxxxx.Hash, 8680820740569200760)] - - [InlineData(3, 窓.Length, 窓.Text, 窓.Hash, 9677543, "窓")] - [InlineData(20, abcdefghijklmnopqrst.Length, abcdefghijklmnopqrst.Text, abcdefghijklmnopqrst.Hash, 7523094288207667809)] - - // show that foo_bar is interpreted as foo-bar - [InlineData(7, foo_bar.Length, foo_bar.Text, foo_bar.Hash, 32195221641981798, "foo-bar", nameof(foo_bar))] - [InlineData(7, foo_bar_hyphen.Length, foo_bar_hyphen.Text, foo_bar_hyphen.Hash, 32195221641981798, "foo-bar", nameof(foo_bar_hyphen))] - [InlineData(7, foo_bar_underscore.Length, foo_bar_underscore.Text, foo_bar_underscore.Hash, 32195222480842598, "foo_bar", nameof(foo_bar_underscore))] - public void Validate(int expectedLength, int actualLength, string actualValue, long actualHash, long expectedHash, string? expectedValue = null, string originForDisambiguation = "") - { - _ = originForDisambiguation; // to allow otherwise-identical test data to coexist - Assert.Equal(expectedLength, actualLength); - Assert.Equal(expectedHash, actualHash); - var bytes = Encoding.UTF8.GetBytes(actualValue); - Assert.Equal(expectedLength, bytes.Length); - Assert.Equal(expectedHash, FastHash.Hash64(bytes)); -#pragma warning disable CS0618 // Type or member is obsolete - Assert.Equal(expectedHash, FastHash.Hash64Fallback(bytes)); -#pragma warning restore CS0618 // Type or member is obsolete - if (expectedValue is not null) - { - Assert.Equal(expectedValue, actualValue); - } - } - - [Fact] - public void FastHashIs_Short() - { - ReadOnlySpan value = "abc"u8; - var hash = value.Hash64(); - Assert.Equal(abc.Hash, hash); - Assert.True(abc.Is(hash, value)); - - value = "abz"u8; - hash = value.Hash64(); - Assert.NotEqual(abc.Hash, hash); - Assert.False(abc.Is(hash, value)); - } - - [Fact] - public void FastHashIs_Long() - { - ReadOnlySpan value = "abcdefghijklmnopqrst"u8; - var hash = value.Hash64(); - Assert.Equal(abcdefghijklmnopqrst.Hash, hash); - Assert.True(abcdefghijklmnopqrst.Is(hash, value)); - - value = "abcdefghijklmnopqrsz"u8; - hash = value.Hash64(); - Assert.Equal(abcdefghijklmnopqrst.Hash, hash); // hash collision, fine - Assert.False(abcdefghijklmnopqrst.Is(hash, value)); - } - - [Fact] - public void KeyNotificationTypeFastHash_MinMaxBytes_ReflectsActualLengths() - { - // Use reflection to find all nested types in KeyNotificationTypeFastHash - var fastHashType = typeof(KeyNotificationTypeFastHash); - var nestedTypes = fastHashType.GetNestedTypes(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); - - int? minLength = null; - int? maxLength = null; - - foreach (var nestedType in nestedTypes) - { - // Look for the Length field (generated by FastHash source generator) - var lengthField = nestedType.GetField("Length", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); - if (lengthField != null && lengthField.FieldType == typeof(int)) - { - var length = (int)lengthField.GetValue(null)!; - - if (minLength == null || length < minLength) - { - minLength = length; - } - - if (maxLength == null || length > maxLength) - { - maxLength = length; - } - } - } - - // Assert that we found at least some nested types with Length fields - Assert.NotNull(minLength); - Assert.NotNull(maxLength); - - // Assert that MinBytes and MaxBytes match the actual min/max lengths - log.WriteLine($"MinBytes: {KeyNotificationTypeFastHash.MinBytes}, MaxBytes: {KeyNotificationTypeFastHash.MaxBytes}"); - Assert.Equal(KeyNotificationTypeFastHash.MinBytes, minLength.Value); - Assert.Equal(KeyNotificationTypeFastHash.MaxBytes, maxLength.Value); - } - - [FastHash] private static partial class a { } - [FastHash] private static partial class ab { } - [FastHash] private static partial class abc { } - [FastHash] private static partial class abcd { } - [FastHash] private static partial class abcde { } - [FastHash] private static partial class abcdef { } - [FastHash] private static partial class abcdefg { } - [FastHash] private static partial class abcdefgh { } - - [FastHash] private static partial class abcdefghijklmnopqrst { } - - // show that foo_bar and foo-bar are different - [FastHash] private static partial class foo_bar { } - [FastHash("foo-bar")] private static partial class foo_bar_hyphen { } - [FastHash("foo_bar")] private static partial class foo_bar_underscore { } - - [FastHash] private static partial class 窓 { } - - [FastHash] private static partial class x { } - [FastHash] private static partial class xx { } - [FastHash] private static partial class xxx { } - [FastHash] private static partial class xxxx { } - [FastHash] private static partial class xxxxx { } - [FastHash] private static partial class xxxxxx { } - [FastHash] private static partial class xxxxxxx { } - [FastHash] private static partial class xxxxxxxx { } -} diff --git a/tests/StackExchange.Redis.Tests/GlobalUsings.cs b/tests/StackExchange.Redis.Tests/GlobalUsings.cs new file mode 100644 index 000000000..ca9c34d74 --- /dev/null +++ b/tests/StackExchange.Redis.Tests/GlobalUsings.cs @@ -0,0 +1,3 @@ +extern alias respite; +global using AsciiHash = respite::RESPite.AsciiHash; +global using AsciiHashAttribute = respite::RESPite.AsciiHashAttribute; diff --git a/tests/StackExchange.Redis.Tests/Helpers/InProcServerFixture.cs b/tests/StackExchange.Redis.Tests/Helpers/InProcServerFixture.cs index 5e801e5ca..9f5a5a59b 100644 --- a/tests/StackExchange.Redis.Tests/Helpers/InProcServerFixture.cs +++ b/tests/StackExchange.Redis.Tests/Helpers/InProcServerFixture.cs @@ -23,5 +23,8 @@ public InProcServerFixture() public Tunnel? Tunnel => _server.Tunnel; - public void Dispose() => _server.Dispose(); + public void Dispose() + { + try { _server.Dispose(); } catch { } + } } diff --git a/tests/StackExchange.Redis.Tests/InProcessTestServer.cs b/tests/StackExchange.Redis.Tests/InProcessTestServer.cs index 0e7d77ec2..6f80215dd 100644 --- a/tests/StackExchange.Redis.Tests/InProcessTestServer.cs +++ b/tests/StackExchange.Redis.Tests/InProcessTestServer.cs @@ -1,4 +1,5 @@ -using System; +extern alias respite; +using System; using System.IO; using System.IO.Pipelines; using System.Net; @@ -6,7 +7,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Pipelines.Sockets.Unofficial; +using respite::RESPite.Messages; using StackExchange.Redis.Configuration; using StackExchange.Redis.Server; using Xunit; @@ -25,19 +26,53 @@ public InProcessTestServer(ITestOutputHelper? log = null) Tunnel = new InProcTunnel(this); } - public Task ConnectAsync(TextWriter? log = null) - => ConnectionMultiplexer.ConnectAsync(GetClientConfig(), log); + public Task ConnectAsync(bool withPubSub = true /*, WriteMode writeMode = WriteMode.Default */, TextWriter? log = null) + => ConnectionMultiplexer.ConnectAsync(GetClientConfig(withPubSub /*, writeMode */), log); - public ConfigurationOptions GetClientConfig() + // view request/response highlights in the log + public override TypedRedisValue Execute(RedisClient client, in RedisRequest request) { - var commands = GetCommands(); + var result = base.Execute(client, in request); + var type = client.ApplyProtocol(result.Type); + if (result.IsNil) + { + Log($"[{client}] {request.Command} (no reply)"); + } + else if (result.IsAggregate) + { + Log($"[{client}] {request.Command} => {(char)type}{result.Span.Length}"); + } + else + { + try + { + var s = result.AsRedisValue().ToString() ?? "(null)"; + const int MAX_CHARS = 32; + s = s.Length <= MAX_CHARS ? s : s.Substring(0, MAX_CHARS) + "..."; + Log($"[{client}] {request.Command} => {(char)type}{s}"); + } + catch + { + Log($"[{client}] {request.Command} => {(char)type}"); + } + } + return result; + } - // transactions don't work yet (needs v3 buffer features) - commands.Remove(nameof(RedisCommand.MULTI)); - commands.Remove(nameof(RedisCommand.EXEC)); - commands.Remove(nameof(RedisCommand.DISCARD)); - commands.Remove(nameof(RedisCommand.WATCH)); - commands.Remove(nameof(RedisCommand.UNWATCH)); + public ConfigurationOptions GetClientConfig(bool withPubSub = true /*, WriteMode writeMode = WriteMode.Default */) + { + var commands = GetCommands(); + if (!withPubSub) + { + commands.Remove(nameof(RedisCommand.SUBSCRIBE)); + commands.Remove(nameof(RedisCommand.PSUBSCRIBE)); + commands.Remove(nameof(RedisCommand.SSUBSCRIBE)); + commands.Remove(nameof(RedisCommand.UNSUBSCRIBE)); + commands.Remove(nameof(RedisCommand.PUNSUBSCRIBE)); + commands.Remove(nameof(RedisCommand.SUNSUBSCRIBE)); + commands.Remove(nameof(RedisCommand.PUBLISH)); + commands.Remove(nameof(RedisCommand.SPUBLISH)); + } var config = new ConfigurationOptions { @@ -50,7 +85,26 @@ public ConfigurationOptions GetClientConfig() AsyncTimeout = 5000, AllowAdmin = true, Tunnel = Tunnel, + Protocol = TestContext.Current.GetProtocol(), + // WriteMode = (BufferedStreamWriter.WriteMode)writeMode, }; + if (!string.IsNullOrEmpty(Password)) config.Password = Password; + + /* useful for viewing *outbound* data in the log +#if DEBUG + if (_log is not null) + { + config.OutputLog = msg => + { + lock (_log) + { + _log.WriteLine(msg); + } + }; + } +#endif + */ + foreach (var endpoint in GetEndPoints()) { config.EndPoints.Add(endpoint); @@ -68,32 +122,57 @@ public override void Log(string message) protected override void OnMoved(RedisClient client, int hashSlot, Node node) { - _log?.WriteLine($"Client {client.Id} being redirected: {hashSlot} to {node}"); + _log?.WriteLine($"[{client}] being redirected: slot {hashSlot} to {node}"); base.OnMoved(client, hashSlot, node); } protected override void OnOutOfBand(RedisClient client, TypedRedisValue message) { + var type = client.ApplyProtocol(message.Type); if (message.IsAggregate && message.Span is { IsEmpty: false } span && !span[0].IsAggregate) { - _log?.WriteLine($"Client {client.Id}: {span[0].AsRedisValue()} {message} "); + _log?.WriteLine($"[{client}] => {(char)type}{message.Span.Length} {span[0].AsRedisValue()}"); } else { - _log?.WriteLine($"Client {client.Id}: {message}"); + _log?.WriteLine($"[{client}] => {(char)type}"); } base.OnOutOfBand(client, message); } + /* + public override void OnFlush(RedisClient client, int messages, long bytes) + { + if (bytes >= 0) + { + _log?.WriteLine($"[{client}] flushed {messages} messages, {bytes} bytes"); + } + else + { + _log?.WriteLine($"[{client}] flushed {messages} messages"); // bytes not available + } + base.OnFlush(client, messages, bytes); + } + */ + public override TypedRedisValue OnUnknownCommand(in RedisClient client, in RedisRequest request, ReadOnlySpan command) { - _log?.WriteLine($"[{client.Id}] unknown command: {Encoding.ASCII.GetString(command)}"); + _log?.WriteLine($"[{client}] unknown command: {Encoding.ASCII.GetString(command)}"); return base.OnUnknownCommand(in client, in request, command); } + public override void OnClientConnected(RedisClient client, object state) + { + if (state is TaskCompletionSource pending) + { + pending.TrySetResult(client); + } + base.OnClientConnected(client, state); + } + private sealed class InProcTunnel( InProcessTestServer server, PipeOptions? pipeOptions = null) : Tunnel @@ -118,13 +197,20 @@ private sealed class InProcTunnel( { if (server.TryGetNode(endpoint, out var node)) { - server._log?.WriteLine( - $"Client intercepted, endpoint {Format.ToString(endpoint)} ({connectionType}) mapped to {server.ServerType} node {node}"); var clientToServer = new Pipe(pipeOptions ?? PipeOptions.Default); var serverToClient = new Pipe(pipeOptions ?? PipeOptions.Default); var serverSide = new Duplex(clientToServer.Reader, serverToClient.Writer); - _ = Task.Run(async () => await server.RunClientAsync(serverSide, node: node), cancellationToken); - var clientSide = StreamConnection.GetDuplex(serverToClient.Reader, clientToServer.Writer); + + TaskCompletionSource clientTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + Task.Run(async () => await server.RunClientAsync(serverSide, node: node, state: clientTcs), cancellationToken).RedisFireAndForget(); + if (!clientTcs.Task.Wait(1000)) throw new TimeoutException("Client not connected"); + var client = clientTcs.Task.Result; + server._log?.WriteLine( + $"[{client}] connected ({connectionType} mapped to {server.ServerType} node {node})"); + + var readStream = serverToClient.Reader.AsStream(); + var writeStream = clientToServer.Writer.AsStream(); + var clientSide = new DuplexStream(readStream, writeStream); return new(clientSide); } return base.BeforeAuthenticateAsync(endpoint, connectionType, socket, cancellationToken); diff --git a/tests/StackExchange.Redis.Tests/KeyNotificationTests.cs b/tests/StackExchange.Redis.Tests/KeyNotificationTests.cs index 60469eb49..0a70aa739 100644 --- a/tests/StackExchange.Redis.Tests/KeyNotificationTests.cs +++ b/tests/StackExchange.Redis.Tests/KeyNotificationTests.cs @@ -410,60 +410,60 @@ public void DefaultKeyNotification_HasExpectedProperties() } [Theory] - [InlineData(KeyNotificationTypeFastHash.append.Text, KeyNotificationType.Append)] - [InlineData(KeyNotificationTypeFastHash.copy.Text, KeyNotificationType.Copy)] - [InlineData(KeyNotificationTypeFastHash.del.Text, KeyNotificationType.Del)] - [InlineData(KeyNotificationTypeFastHash.expire.Text, KeyNotificationType.Expire)] - [InlineData(KeyNotificationTypeFastHash.hdel.Text, KeyNotificationType.HDel)] - [InlineData(KeyNotificationTypeFastHash.hexpired.Text, KeyNotificationType.HExpired)] - [InlineData(KeyNotificationTypeFastHash.hincrbyfloat.Text, KeyNotificationType.HIncrByFloat)] - [InlineData(KeyNotificationTypeFastHash.hincrby.Text, KeyNotificationType.HIncrBy)] - [InlineData(KeyNotificationTypeFastHash.hpersist.Text, KeyNotificationType.HPersist)] - [InlineData(KeyNotificationTypeFastHash.hset.Text, KeyNotificationType.HSet)] - [InlineData(KeyNotificationTypeFastHash.incrbyfloat.Text, KeyNotificationType.IncrByFloat)] - [InlineData(KeyNotificationTypeFastHash.incrby.Text, KeyNotificationType.IncrBy)] - [InlineData(KeyNotificationTypeFastHash.linsert.Text, KeyNotificationType.LInsert)] - [InlineData(KeyNotificationTypeFastHash.lpop.Text, KeyNotificationType.LPop)] - [InlineData(KeyNotificationTypeFastHash.lpush.Text, KeyNotificationType.LPush)] - [InlineData(KeyNotificationTypeFastHash.lrem.Text, KeyNotificationType.LRem)] - [InlineData(KeyNotificationTypeFastHash.lset.Text, KeyNotificationType.LSet)] - [InlineData(KeyNotificationTypeFastHash.ltrim.Text, KeyNotificationType.LTrim)] - [InlineData(KeyNotificationTypeFastHash.move_from.Text, KeyNotificationType.MoveFrom)] - [InlineData(KeyNotificationTypeFastHash.move_to.Text, KeyNotificationType.MoveTo)] - [InlineData(KeyNotificationTypeFastHash.persist.Text, KeyNotificationType.Persist)] - [InlineData(KeyNotificationTypeFastHash.rename_from.Text, KeyNotificationType.RenameFrom)] - [InlineData(KeyNotificationTypeFastHash.rename_to.Text, KeyNotificationType.RenameTo)] - [InlineData(KeyNotificationTypeFastHash.restore.Text, KeyNotificationType.Restore)] - [InlineData(KeyNotificationTypeFastHash.rpop.Text, KeyNotificationType.RPop)] - [InlineData(KeyNotificationTypeFastHash.rpush.Text, KeyNotificationType.RPush)] - [InlineData(KeyNotificationTypeFastHash.sadd.Text, KeyNotificationType.SAdd)] - [InlineData(KeyNotificationTypeFastHash.set.Text, KeyNotificationType.Set)] - [InlineData(KeyNotificationTypeFastHash.setrange.Text, KeyNotificationType.SetRange)] - [InlineData(KeyNotificationTypeFastHash.sortstore.Text, KeyNotificationType.SortStore)] - [InlineData(KeyNotificationTypeFastHash.srem.Text, KeyNotificationType.SRem)] - [InlineData(KeyNotificationTypeFastHash.spop.Text, KeyNotificationType.SPop)] - [InlineData(KeyNotificationTypeFastHash.xadd.Text, KeyNotificationType.XAdd)] - [InlineData(KeyNotificationTypeFastHash.xdel.Text, KeyNotificationType.XDel)] - [InlineData(KeyNotificationTypeFastHash.xgroup_createconsumer.Text, KeyNotificationType.XGroupCreateConsumer)] - [InlineData(KeyNotificationTypeFastHash.xgroup_create.Text, KeyNotificationType.XGroupCreate)] - [InlineData(KeyNotificationTypeFastHash.xgroup_delconsumer.Text, KeyNotificationType.XGroupDelConsumer)] - [InlineData(KeyNotificationTypeFastHash.xgroup_destroy.Text, KeyNotificationType.XGroupDestroy)] - [InlineData(KeyNotificationTypeFastHash.xgroup_setid.Text, KeyNotificationType.XGroupSetId)] - [InlineData(KeyNotificationTypeFastHash.xsetid.Text, KeyNotificationType.XSetId)] - [InlineData(KeyNotificationTypeFastHash.xtrim.Text, KeyNotificationType.XTrim)] - [InlineData(KeyNotificationTypeFastHash.zadd.Text, KeyNotificationType.ZAdd)] - [InlineData(KeyNotificationTypeFastHash.zdiffstore.Text, KeyNotificationType.ZDiffStore)] - [InlineData(KeyNotificationTypeFastHash.zinterstore.Text, KeyNotificationType.ZInterStore)] - [InlineData(KeyNotificationTypeFastHash.zunionstore.Text, KeyNotificationType.ZUnionStore)] - [InlineData(KeyNotificationTypeFastHash.zincr.Text, KeyNotificationType.ZIncr)] - [InlineData(KeyNotificationTypeFastHash.zrembyrank.Text, KeyNotificationType.ZRemByRank)] - [InlineData(KeyNotificationTypeFastHash.zrembyscore.Text, KeyNotificationType.ZRemByScore)] - [InlineData(KeyNotificationTypeFastHash.zrem.Text, KeyNotificationType.ZRem)] - [InlineData(KeyNotificationTypeFastHash.expired.Text, KeyNotificationType.Expired)] - [InlineData(KeyNotificationTypeFastHash.evicted.Text, KeyNotificationType.Evicted)] - [InlineData(KeyNotificationTypeFastHash._new.Text, KeyNotificationType.New)] - [InlineData(KeyNotificationTypeFastHash.overwritten.Text, KeyNotificationType.Overwritten)] - [InlineData(KeyNotificationTypeFastHash.type_changed.Text, KeyNotificationType.TypeChanged)] + [InlineData("append", KeyNotificationType.Append)] + [InlineData("copy", KeyNotificationType.Copy)] + [InlineData("del", KeyNotificationType.Del)] + [InlineData("expire", KeyNotificationType.Expire)] + [InlineData("hdel", KeyNotificationType.HDel)] + [InlineData("hexpired", KeyNotificationType.HExpired)] + [InlineData("hincrbyfloat", KeyNotificationType.HIncrByFloat)] + [InlineData("hincrby", KeyNotificationType.HIncrBy)] + [InlineData("hpersist", KeyNotificationType.HPersist)] + [InlineData("hset", KeyNotificationType.HSet)] + [InlineData("incrbyfloat", KeyNotificationType.IncrByFloat)] + [InlineData("incrby", KeyNotificationType.IncrBy)] + [InlineData("linsert", KeyNotificationType.LInsert)] + [InlineData("lpop", KeyNotificationType.LPop)] + [InlineData("lpush", KeyNotificationType.LPush)] + [InlineData("lrem", KeyNotificationType.LRem)] + [InlineData("lset", KeyNotificationType.LSet)] + [InlineData("ltrim", KeyNotificationType.LTrim)] + [InlineData("move_from", KeyNotificationType.MoveFrom)] + [InlineData("move_to", KeyNotificationType.MoveTo)] + [InlineData("persist", KeyNotificationType.Persist)] + [InlineData("rename_from", KeyNotificationType.RenameFrom)] + [InlineData("rename_to", KeyNotificationType.RenameTo)] + [InlineData("restore", KeyNotificationType.Restore)] + [InlineData("rpop", KeyNotificationType.RPop)] + [InlineData("rpush", KeyNotificationType.RPush)] + [InlineData("sadd", KeyNotificationType.SAdd)] + [InlineData("set", KeyNotificationType.Set)] + [InlineData("setrange", KeyNotificationType.SetRange)] + [InlineData("sortstore", KeyNotificationType.SortStore)] + [InlineData("srem", KeyNotificationType.SRem)] + [InlineData("spop", KeyNotificationType.SPop)] + [InlineData("xadd", KeyNotificationType.XAdd)] + [InlineData("xdel", KeyNotificationType.XDel)] + [InlineData("xgroup-createconsumer", KeyNotificationType.XGroupCreateConsumer)] + [InlineData("xgroup-create", KeyNotificationType.XGroupCreate)] + [InlineData("xgroup-delconsumer", KeyNotificationType.XGroupDelConsumer)] + [InlineData("xgroup-destroy", KeyNotificationType.XGroupDestroy)] + [InlineData("xgroup-setid", KeyNotificationType.XGroupSetId)] + [InlineData("xsetid", KeyNotificationType.XSetId)] + [InlineData("xtrim", KeyNotificationType.XTrim)] + [InlineData("zadd", KeyNotificationType.ZAdd)] + [InlineData("zdiffstore", KeyNotificationType.ZDiffStore)] + [InlineData("zinterstore", KeyNotificationType.ZInterStore)] + [InlineData("zunionstore", KeyNotificationType.ZUnionStore)] + [InlineData("zincr", KeyNotificationType.ZIncr)] + [InlineData("zrembyrank", KeyNotificationType.ZRemByRank)] + [InlineData("zrembyscore", KeyNotificationType.ZRemByScore)] + [InlineData("zrem", KeyNotificationType.ZRem)] + [InlineData("expired", KeyNotificationType.Expired)] + [InlineData("evicted", KeyNotificationType.Evicted)] + [InlineData("new", KeyNotificationType.New)] + [InlineData("overwritten", KeyNotificationType.Overwritten)] + [InlineData("type_changed", KeyNotificationType.TypeChanged)] public unsafe void FastHashParse_AllKnownValues_ParseCorrectly(string raw, KeyNotificationType parsed) { var arr = ArrayPool.Shared.Rent(Encoding.UTF8.GetMaxByteCount(raw.Length)); @@ -476,12 +476,12 @@ public unsafe void FastHashParse_AllKnownValues_ParseCorrectly(string raw, KeyNo } } - var result = KeyNotificationTypeFastHash.Parse(arr.AsSpan(0, bytes)); + var result = KeyNotificationTypeMetadata.Parse(arr.AsSpan(0, bytes)); log.WriteLine($"Parsed '{raw}' as {result}"); Assert.Equal(parsed, result); // and the other direction: - var fetchedBytes = KeyNotificationTypeFastHash.GetRawBytes(parsed); + var fetchedBytes = KeyNotificationTypeMetadata.GetRawBytes(parsed); string fetched; fixed (byte* bPtr = fetchedBytes) { diff --git a/tests/StackExchange.Redis.Tests/MovedTestServer.cs b/tests/StackExchange.Redis.Tests/MovedTestServer.cs index 17ed92c35..89a8567d0 100644 --- a/tests/StackExchange.Redis.Tests/MovedTestServer.cs +++ b/tests/StackExchange.Redis.Tests/MovedTestServer.cs @@ -71,7 +71,7 @@ public override void OnClientConnected(RedisClient client, object state) { if (client is MovedTestClient movedClient) { - Log($"Client {client.Id} connected (assigned to {movedClient.AssignedHost}), total connections: {TotalClientCount}"); + Log($"[{client}] connected (assigned to {movedClient.AssignedHost}), total connections: {TotalClientCount}"); } base.OnClientConnected(client, state); } diff --git a/tests/StackExchange.Redis.Tests/MovedUnitTests.cs b/tests/StackExchange.Redis.Tests/MovedUnitTests.cs index 1671d6cc3..5618adf27 100644 --- a/tests/StackExchange.Redis.Tests/MovedUnitTests.cs +++ b/tests/StackExchange.Redis.Tests/MovedUnitTests.cs @@ -11,6 +11,7 @@ namespace StackExchange.Redis.Tests; /// When a MOVED error points to the same endpoint, the client should reconnect before retrying, /// allowing the DNS record/proxy/load balancer to route to a different underlying server host. /// +[RunPerProtocol] public class MovedUnitTests(ITestOutputHelper log) { private RedisKey Me([CallerMemberName] string callerName = "") => callerName; @@ -48,13 +49,16 @@ public async Task CrossSlotDisallowed(ServerType serverType) } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task KeyMigrationFollowed(bool allowFollowRedirects) + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + public async Task KeyMigrationFollowed(bool allowFollowRedirects, bool toNewUnknownNode) { RedisKey key = Me(); using var server = new InProcessTestServer(log) { ServerType = ServerType.Cluster }; - var secondNode = server.AddEmptyNode(); + // depending on the test, we might not want the client to know about the second node yet + var secondNode = toNewUnknownNode ? null : server.AddEmptyNode(); await using var muxer = await server.ConnectAsync(); var db = muxer.GetDatabase(); @@ -63,6 +67,11 @@ public async Task KeyMigrationFollowed(bool allowFollowRedirects) var value = await db.StringGetAsync(key); Assert.Equal("value", (string?)value); + if (toNewUnknownNode) // if deferred, the client doesn't know about this yet + { + secondNode = server.AddEmptyNode(); + } + server.Migrate(key, secondNode); if (allowFollowRedirects) diff --git a/tests/StackExchange.Redis.Tests/PubSubKeyNotificationTests.cs b/tests/StackExchange.Redis.Tests/PubSubKeyNotificationTests.cs index 723921d45..a65d0c631 100644 --- a/tests/StackExchange.Redis.Tests/PubSubKeyNotificationTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubKeyNotificationTests.cs @@ -50,7 +50,7 @@ private IInternalConnectionMultiplexer Create(bool withChannelPrefix) => private RedisKey SelectKey(RedisKey[] keys) => keys[SharedRandom.Next(0, keys.Length)]; -#if NET6_0_OR_GREATER +#if NET private static Random SharedRandom => Random.Shared; #else private static Random SharedRandom { get; } = new(); @@ -338,7 +338,7 @@ private void OnNotification( var recvKey = notification.GetKey(); Assert.True(observedCounts.TryGetValue(recvKey.ToString(), out var counter)); -#if NET9_0_OR_GREATER +#if NET10_0_OR_GREATER // it would be more efficient to stash the alt-lookup, but that would make our API here non-viable, // since we need to support multiple frameworks var viaAlt = FindViaAltLookup(notification, observedCounts.GetAlternateLookup>()); @@ -396,7 +396,7 @@ private async Task SendAndObserveAsync( } } -#if NET9_0_OR_GREATER +#if NET10_0_OR_GREATER // demonstrate that we can use the alt-lookup APIs to avoid string allocations private static Counter? FindViaAltLookup( in KeyNotification notification, diff --git a/tests/StackExchange.Redis.Tests/PubSubTests.cs b/tests/StackExchange.Redis.Tests/PubSubTests.cs index 7e3db5292..65e1ee574 100644 --- a/tests/StackExchange.Redis.Tests/PubSubTests.cs +++ b/tests/StackExchange.Redis.Tests/PubSubTests.cs @@ -16,14 +16,12 @@ public class PubSubTests(ITestOutputHelper output, SharedConnectionFixture fixtu { } -/* [RunPerProtocol] public class InProcPubSubTests(ITestOutputHelper output, InProcServerFixture fixture) : PubSubTestBase(output, null, fixture) { - protected override bool UseDedicatedInProcessServer => false; + protected override bool UseDedicatedInProcessServer => true; } -*/ [RunPerProtocol] public abstract class PubSubTestBase( @@ -155,6 +153,18 @@ public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string b Assert.Equal(0, count); } + [Fact] + public async Task Ping() + { + await using var conn = ConnectFactory(shared: false); + var pub = GetAnyPrimary(conn.DefaultClient); + var sub = conn.GetSubscriber(); + + await PingAsync(pub, sub, 5).ForAwait(); + await sub.SubscribeAsync(RedisChannel.Literal(Me()), (_, __) => { }); // to ensure we're in subscriber mode + await PingAsync(pub, sub, 5).ForAwait(); + } + [Fact] public async Task TestBasicPubSubFireAndForget() { @@ -223,10 +233,28 @@ private async Task PingAsync(IServer pub, ISubscriber sub, int times = 1) // way to prove that is to use TPL objects var subTask = sub.PingAsync(); var pubTask = pub.PingAsync(); - await Task.WhenAll(subTask, pubTask).ForAwait(); + try + { + await Task.WhenAll(subTask, pubTask).ForAwait(); + } + catch (TimeoutException ex) + { + throw new TimeoutException($"Timeout; sub: {GetState(subTask)}, pub: {GetState(pubTask)}", ex); + } - Log($"Sub PING time: {subTask.Result.TotalMilliseconds} ms"); - Log($"Pub PING time: {pubTask.Result.TotalMilliseconds} ms"); + Log($"sub: {GetState(subTask)}, pub: {GetState(pubTask)}"); + + static string GetState(Task pending) + { + var status = pending.Status; + return status switch + { + TaskStatus.RanToCompletion => $"{status} in {pending.Result.TotalMilliseconds:###,##0.0}ms)", + TaskStatus.Faulted when pending.Exception is { InnerExceptions.Count:1 } ae => $"{status}: {ae.InnerExceptions[0].Message}", + TaskStatus.Faulted => $"{status}: {pending.Exception?.Message}", + _ => status.ToString(), + }; + } } } @@ -314,6 +342,7 @@ public async Task TestMassivePublishWithWithoutFlush_Local() public async Task TestMassivePublishWithWithoutFlush_Remote() { Skip.UnlessLongRunning(); + SkipIfWouldUseInProcessServer(); await using var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort); var sub = conn.GetSubscriber(); @@ -437,6 +466,7 @@ await sub.SubscribeAsync(channel, (_, val) => [Fact] public async Task PubSubGetAllCorrectOrder() { + SkipIfWouldUseInProcessServer(); await using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { var sub = conn.GetSubscriber(); @@ -507,6 +537,7 @@ async Task RunLoop() [Fact] public async Task PubSubGetAllCorrectOrder_OnMessage_Sync() { + SkipIfWouldUseInProcessServer(); await using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { var sub = conn.GetSubscriber(); @@ -573,6 +604,7 @@ public async Task PubSubGetAllCorrectOrder_OnMessage_Sync() [Fact] public async Task PubSubGetAllCorrectOrder_OnMessage_Async() { + SkipIfWouldUseInProcessServer(); await using (var conn = Create(configuration: TestConfig.Current.RemoteServerAndPort, syncTimeout: 20000, log: Writer)) { var sub = conn.GetSubscriber(); diff --git a/tests/StackExchange.Redis.Tests/SSLTests.cs b/tests/StackExchange.Redis.Tests/SSLTests.cs index c9c5cc2bb..96d964b23 100644 --- a/tests/StackExchange.Redis.Tests/SSLTests.cs +++ b/tests/StackExchange.Redis.Tests/SSLTests.cs @@ -163,7 +163,7 @@ public async Task ConnectToSSLServer(bool useSsl, bool specifyHost) } } -#if NETCOREAPP3_1_OR_GREATER +#if NET #pragma warning disable CS0618 // Type or member is obsolete // Docker configured with only TLS_AES_256_GCM_SHA384 for testing [Theory] diff --git a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj index 4227fedc3..4c312d448 100644 --- a/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj +++ b/tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj @@ -20,6 +20,7 @@ + @@ -32,6 +33,5 @@ - diff --git a/tests/StackExchange.Redis.Tests/StringTests.cs b/tests/StackExchange.Redis.Tests/StringTests.cs index 1ade532d7..2dcf8f6fb 100644 --- a/tests/StackExchange.Redis.Tests/StringTests.cs +++ b/tests/StackExchange.Redis.Tests/StringTests.cs @@ -967,8 +967,8 @@ public async Task LongestCommonSubsequence() var stringMatchResult = db.StringLongestCommonSubsequenceWithMatches(key1, key2); Assert.Equal(2, stringMatchResult.Matches.Length); // "my" and "text" are the two matches of the result - Assert.Equivalent(new LCSMatchResult.LCSMatch(4, 5, length: 4), stringMatchResult.Matches[0]); // the string "text" starts at index 4 in the first string and at index 5 in the second string - Assert.Equivalent(new LCSMatchResult.LCSMatch(2, 0, length: 2), stringMatchResult.Matches[1]); // the string "my" starts at index 2 in the first string and at index 0 in the second string + Assert.Equivalent(new LCSMatchResult.LCSMatch(new(4, 7), new(5, 8), length: 4), stringMatchResult.Matches[0]); // the string "text" starts at index 4 in the first string and at index 5 in the second string + Assert.Equivalent(new LCSMatchResult.LCSMatch(new(2, 3), new(0, 1), length: 2), stringMatchResult.Matches[1]); // the string "my" starts at index 2 in the first string and at index 0 in the second string stringMatchResult = db.StringLongestCommonSubsequenceWithMatches(key1, key2, 5); Assert.Empty(stringMatchResult.Matches); // no matches longer than 5 characters @@ -1007,8 +1007,8 @@ public async Task LongestCommonSubsequenceAsync() var stringMatchResult = await db.StringLongestCommonSubsequenceWithMatchesAsync(key1, key2); Assert.Equal(2, stringMatchResult.Matches.Length); // "my" and "text" are the two matches of the result - Assert.Equivalent(new LCSMatchResult.LCSMatch(4, 5, length: 4), stringMatchResult.Matches[0]); // the string "text" starts at index 4 in the first string and at index 5 in the second string - Assert.Equivalent(new LCSMatchResult.LCSMatch(2, 0, length: 2), stringMatchResult.Matches[1]); // the string "my" starts at index 2 in the first string and at index 0 in the second string + Assert.Equivalent(new LCSMatchResult.LCSMatch(new(4, 7), new(5, 8), length: 4), stringMatchResult.Matches[0]); // the string "text" starts at index 4 in the first string and at index 5 in the second string + Assert.Equivalent(new LCSMatchResult.LCSMatch(new(2, 3), new(0, 1), length: 2), stringMatchResult.Matches[1]); // the string "my" starts at index 2 in the first string and at index 0 in the second string stringMatchResult = await db.StringLongestCommonSubsequenceWithMatchesAsync(key1, key2, 5); Assert.Empty(stringMatchResult.Matches); // no matches longer than 5 characters diff --git a/tests/StackExchange.Redis.Tests/TestBase.cs b/tests/StackExchange.Redis.Tests/TestBase.cs index 1eb0e8aab..62b841f08 100644 --- a/tests/StackExchange.Redis.Tests/TestBase.cs +++ b/tests/StackExchange.Redis.Tests/TestBase.cs @@ -211,8 +211,6 @@ public void Teardown() } Assert.Skip($"There were {privateFailCount} private and {sharedFailCount.Value} ambient exceptions; expected {expectedFailCount}."); } - var pool = SocketManager.Shared?.SchedulerPool; - Log($"Service Counts: (Scheduler) Queue: {pool?.TotalServicedByQueue.ToString()}, Pool: {pool?.TotalServicedByPool.ToString()}, Workers: {pool?.WorkerCount.ToString()}, Available: {pool?.AvailableCount.ToString()}"); } protected static IServer GetServer(IConnectionMultiplexer muxer) @@ -564,7 +562,7 @@ void Callback() for (int i = 0; i < threads; i++) { var thd = threadArr[i]; -#if !NET6_0_OR_GREATER +#if !NET if (thd.IsAlive) thd.Abort(); #endif } @@ -588,6 +586,7 @@ protected static async Task UntilConditionAsync(TimeSpan maxWaitTime, Func // simplified usage to get an interchangeable dedicated vs shared in-process server, useful for debugging protected virtual bool UseDedicatedInProcessServer => false; // use the shared server by default + internal ClientFactory ConnectFactory(bool allowAdmin = false, string? channelPrefix = null, bool shared = true) { if (UseDedicatedInProcessServer) @@ -598,6 +597,16 @@ internal ClientFactory ConnectFactory(bool allowAdmin = false, string? channelPr return new ClientFactory(this, allowAdmin, channelPrefix, shared, null); } + protected void SkipIfWouldUseInProcessServer(string? reason = null) + { + Assert.SkipWhen(_inProcServerFixture != null || UseDedicatedInProcessServer, reason ?? "In-process server is in use."); + } + + protected void SkipIfWouldUseRealServer(string? reason = null) + { + Assert.SkipUnless(_inProcServerFixture != null || UseDedicatedInProcessServer, reason ?? "Real server is in use."); + } + internal sealed class ClientFactory : IDisposable, IAsyncDisposable { private readonly TestBase _testBase; diff --git a/toys/KestrelRedisServer/KestrelRedisServer.csproj b/toys/KestrelRedisServer/KestrelRedisServer.csproj index 8854d6ac8..f7955c42e 100644 --- a/toys/KestrelRedisServer/KestrelRedisServer.csproj +++ b/toys/KestrelRedisServer/KestrelRedisServer.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 $(NoWarn);CS1591 enable enable diff --git a/toys/KestrelRedisServer/RedisConnectionHandler.cs b/toys/KestrelRedisServer/RedisConnectionHandler.cs index 58511b1fc..415da54b9 100644 --- a/toys/KestrelRedisServer/RedisConnectionHandler.cs +++ b/toys/KestrelRedisServer/RedisConnectionHandler.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Connections; +using System.Diagnostics; +using Microsoft.AspNetCore.Connections; using StackExchange.Redis.Server; namespace KestrelRedisServer @@ -12,7 +13,20 @@ public override Task OnConnectedAsync(ConnectionContext connection) { node = null; } - return server.RunClientAsync(connection.Transport, node: node); + + return server.RunClientAsync(connection.Transport, node: node) + .ContinueWith( + t => + { + // ensure any exceptions are observed + var ex = t.Exception; + if (ex != null) + { + Debug.WriteLine(ex.Message); + GC.KeepAlive(ex); + } + }, + TaskContinuationOptions.OnlyOnFaulted); } } } diff --git a/toys/StackExchange.Redis.Server/GlobalUsings.cs b/toys/StackExchange.Redis.Server/GlobalUsings.cs new file mode 100644 index 000000000..aa3ae0946 --- /dev/null +++ b/toys/StackExchange.Redis.Server/GlobalUsings.cs @@ -0,0 +1,22 @@ +extern alias seredis; +global using Format = seredis::StackExchange.Redis.Format; +global using PhysicalConnection = seredis::StackExchange.Redis.PhysicalConnection; +/* +During the v2/v3 transition, SE.Redis doesn't have RESPite, which +means it needs to merge in a few types like AsciiHash; this causes +conflicts; this file is a place to resolve them. Since the server +is now *mostly* RESPite, it turns out that the most efficient way +to do this is to shunt all of SE.Redis off into an alias, and bring +back just the types we need. +*/ +global using RedisChannel = seredis::StackExchange.Redis.RedisChannel; +global using RedisCommand = seredis::StackExchange.Redis.RedisCommand; +global using RedisCommandMetadata = seredis::StackExchange.Redis.RedisCommandMetadata; +global using RedisKey = seredis::StackExchange.Redis.RedisKey; +global using RedisProtocol = seredis::StackExchange.Redis.RedisProtocol; +global using RedisValue = seredis::StackExchange.Redis.RedisValue; +global using ResultType = seredis::StackExchange.Redis.ResultType; +global using ServerSelectionStrategy = seredis::StackExchange.Redis.ServerSelectionStrategy; +global using ServerType = seredis::StackExchange.Redis.ServerType; +global using SlotRange = seredis::StackExchange.Redis.SlotRange; +global using TaskSource = seredis::StackExchange.Redis.TaskSource; diff --git a/toys/StackExchange.Redis.Server/RedisClient.Output.cs b/toys/StackExchange.Redis.Server/RedisClient.Output.cs index 525dcc4e1..e27d693be 100644 --- a/toys/StackExchange.Redis.Server/RedisClient.Output.cs +++ b/toys/StackExchange.Redis.Server/RedisClient.Output.cs @@ -1,9 +1,12 @@ using System; using System.Buffers; +using System.Diagnostics; using System.IO.Pipelines; +using System.Text; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; +using RESPite.Messages; namespace StackExchange.Redis.Server; @@ -16,7 +19,13 @@ public partial class RedisClient AllowSynchronousContinuations = false, }; - private readonly Channel _replies = Channel.CreateUnbounded(s_replyChannelOptions); + private readonly struct VersionedResponse(TypedRedisValue value, RedisProtocol protocol) + { + public readonly TypedRedisValue Value = value; + public readonly RedisProtocol Protocol = protocol; + } + + private readonly Channel _replies = Channel.CreateUnbounded(s_replyChannelOptions); public void AddOutbound(in TypedRedisValue message) { @@ -28,11 +37,12 @@ public void AddOutbound(in TypedRedisValue message) try { - if (!_replies.Writer.TryWrite(message)) + var versioned = new VersionedResponse(message, Protocol); + if (!_replies.Writer.TryWrite(versioned)) { // sorry, we're going to need it, but in reality: we're using // unbounded channels, so this isn't an issue - _replies.Writer.WriteAsync(message).AsTask().Wait(); + _replies.Writer.WriteAsync(versioned).AsTask().Wait(); } } catch @@ -51,7 +61,8 @@ public ValueTask AddOutboundAsync(in TypedRedisValue message, CancellationToken try { - var pending = _replies.Writer.WriteAsync(message, cancellationToken); + var versioned = new VersionedResponse(message, Protocol); + var pending = _replies.Writer.WriteAsync(versioned, cancellationToken); if (!pending.IsCompleted) return Awaited(message, pending); pending.GetAwaiter().GetResult(); // if we succeed, the writer owns it for recycling @@ -85,13 +96,23 @@ public async Task WriteOutputAsync(PipeWriter writer, CancellationToken cancella var reader = _replies.Reader; do { - while (reader.TryRead(out var message)) + int count = 0; + while (reader.TryRead(out var versioned)) { - await RespServer.WriteResponseAsync(this, writer, message, Protocol); - message.Recycle(); + WriteResponse(writer, versioned.Value, versioned.Protocol); + versioned.Value.Recycle(); + count++; } - await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + if (count != 0) + { +#if NET10_0_OR_GREATER + Node?.Server?.OnFlush(this, count, writer.CanGetUnflushedBytes ? writer.UnflushedBytes : -1); +#else + Node?.Server?.OnFlush(this, count, -1); +#endif + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + } } // await more data while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)); @@ -101,5 +122,141 @@ public async Task WriteOutputAsync(PipeWriter writer, CancellationToken cancella { await writer.CompleteAsync(ex); } + + static void WriteResponse(IBufferWriter output, TypedRedisValue value, RedisProtocol protocol) + { + static void WritePrefix(IBufferWriter output, char prefix) + { + var span = output.GetSpan(1); + span[0] = (byte)prefix; + output.Advance(1); + } + + if (value.IsNil) return; // not actually a request (i.e. empty/whitespace request) + + var type = value.Type; + if (protocol is RedisProtocol.Resp2 & type is not RespPrefix.Null) + { + if (type is RespPrefix.VerbatimString) + { + var s = (string)value.AsRedisValue(); + if (s is { Length: >= 4 } && s[3] == ':') + value = TypedRedisValue.BulkString(s.Substring(4)); + } + type = ToResp2(type); + } + RetryResp2: + if (protocol is RedisProtocol.Resp3 && value.IsNullValueOrArray) + { + output.Write("_\r\n"u8); + } + else + { + char prefix; + switch (type) + { + case RespPrefix.Integer: + PhysicalConnection.WriteInteger(output, (long)value.AsRedisValue()); + break; + case RespPrefix.SimpleError: + prefix = '-'; + goto BasicMessage; + case RespPrefix.SimpleString: + prefix = '+'; + BasicMessage: + WritePrefix(output, prefix); + var val = (string)value.AsRedisValue() ?? ""; + var expectedLength = Encoding.UTF8.GetByteCount(val); + PhysicalConnection.WriteRaw(output, val, expectedLength); + PhysicalConnection.WriteCrlf(output); + break; + case RespPrefix.BulkString: + PhysicalConnection.WriteBulkString(value.AsRedisValue(), output); + break; + case RespPrefix.Null: + case RespPrefix.Push when value.IsNullArray: + case RespPrefix.Map when value.IsNullArray: + case RespPrefix.Set when value.IsNullArray: + case RespPrefix.Attribute when value.IsNullArray: + output.Write("_\r\n"u8); + break; + case RespPrefix.Array when value.IsNullArray: + PhysicalConnection.WriteMultiBulkHeader(output, -1); + break; + case RespPrefix.Push: + case RespPrefix.Map: + case RespPrefix.Array: + case RespPrefix.Set: + case RespPrefix.Attribute: + var segment = value.Span; + PhysicalConnection.WriteMultiBulkHeader(output, segment.Length, ToResultType(type)); + foreach (var item in segment) + { + if (item.IsNil) throw new InvalidOperationException("Array element cannot be nil"); + WriteResponse(output, item, protocol); + } + break; + default: + // retry with RESP2 + var r2 = ToResp2(type); + if (r2 != type) + { + Debug.WriteLine($"{type} not handled in RESP3; using {r2} instead"); + goto RetryResp2; + } + + throw new InvalidOperationException( + "Unexpected result type: " + value.Type); + } + } + + static ResultType ToResultType(RespPrefix type) => + type switch + { + RespPrefix.None => ResultType.None, + RespPrefix.SimpleString => ResultType.SimpleString, + RespPrefix.SimpleError => ResultType.Error, + RespPrefix.Integer => ResultType.Integer, + RespPrefix.BulkString => ResultType.BulkString, + RespPrefix.Array => ResultType.Array, + RespPrefix.Null => ResultType.Null, + RespPrefix.Boolean => ResultType.Boolean, + RespPrefix.Double => ResultType.Double, + RespPrefix.BigInteger => ResultType.BigInteger, + RespPrefix.BulkError => ResultType.BlobError, + RespPrefix.VerbatimString => ResultType.VerbatimString, + RespPrefix.Map => ResultType.Map, + RespPrefix.Set => ResultType.Set, + RespPrefix.Push => ResultType.Push, + RespPrefix.Attribute => ResultType.Attribute, + // StreamContinuation and StreamTerminator don't have direct ResultType equivalents + // These are protocol-level markers, not result types + _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unexpected RespPrefix value"), + }; + } + } + + public RespPrefix ApplyProtocol(RespPrefix type) => IsResp2 ? ToResp2(type) : type; + + private static RespPrefix ToResp2(RespPrefix type) + { + switch (type) + { + case RespPrefix.Boolean: + return RespPrefix.Integer; + case RespPrefix.Double: + case RespPrefix.BigInteger: + return RespPrefix.SimpleString; + case RespPrefix.BulkError: + return RespPrefix.SimpleError; + case RespPrefix.VerbatimString: + return RespPrefix.BulkString; + case RespPrefix.Map: + case RespPrefix.Set: + case RespPrefix.Push: + case RespPrefix.Attribute: + return RespPrefix.Array; + default: return type; + } } } diff --git a/toys/StackExchange.Redis.Server/RedisClient.cs b/toys/StackExchange.Redis.Server/RedisClient.cs index 56ecd0dbb..1f09b3916 100644 --- a/toys/StackExchange.Redis.Server/RedisClient.cs +++ b/toys/StackExchange.Redis.Server/RedisClient.cs @@ -1,22 +1,112 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.IO.Pipelines; using System.Text; +using RESPite; +using RESPite.Messages; namespace StackExchange.Redis.Server { public partial class RedisClient(RedisServer.Node node) : IDisposable +#pragma warning disable SA1001 + #if NET + , ISpanFormattable +#else + , IFormattable + #endif +#pragma warning restore SA1001 { + private RespScanState _readState; + + public override string ToString() + { + if (Protocol is RedisProtocol.Resp2) + { + return IsSubscriber ? $"{node.Host}:{node.Port} #{Id}:sub" : $"{node.Host}:{node.Port} #{Id}"; + } + return $"{node.Host}:{node.Port} #{Id}:r3"; + } + + string IFormattable.ToString(string format, IFormatProvider formatProvider) => ToString(); +#if NET + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) + { + charsWritten = 0; + if (!(TryWrite(ref destination, node.Host.AsSpan(), ref charsWritten) + && TryWrite(ref destination, ":".AsSpan(), ref charsWritten) + && TryWriteInt32(ref destination, node.Port, ref charsWritten) + && TryWrite(ref destination, " #".AsSpan(), ref charsWritten) + && TryWriteInt32(ref destination, Id, ref charsWritten))) + { + return false; + } + if (Protocol is RedisProtocol.Resp2) + { + if (IsSubscriber) + { + if (!TryWrite(ref destination, ":sub".AsSpan(), ref charsWritten)) return false; + } + } + else + { + if (!TryWrite(ref destination, ":r3".AsSpan(), ref charsWritten)) return false; + } + return true; + + static bool TryWrite(ref Span destination, ReadOnlySpan value, ref int charsWritten) + { + if (value.Length > destination.Length) + { + return false; + } + value.CopyTo(destination); + destination = destination.Slice(value.Length); + charsWritten += value.Length; + return true; + } + static bool TryWriteInt32(ref Span destination, int value, ref int charsWritten) + { + if (!value.TryFormat(destination, out var len)) + { + return false; + } + destination = destination.Slice(len); + charsWritten += len; + return true; + } + } +#endif + + public bool TryReadRequest(ReadOnlySequence data, out long consumed) + { + // skip past data we've already read + data = data.Slice(_readState.TotalBytes); + var status = RespFrameScanner.Default.TryRead(ref _readState, data); + consumed = _readState.TotalBytes; + switch (status) + { + case OperationStatus.Done: + _readState = default; // reset ready for the next frame + return true; + case OperationStatus.NeedMoreData: + consumed = 0; + return false; + default: + throw new InvalidOperationException($"Unexpected status: {status}"); + } + } + public RedisServer.Node Node => node; internal int SkipReplies { get; set; } internal bool ShouldSkipResponse() { - if (SkipReplies > 0) + if (SkipReplies > 0) // skips N { SkipReplies--; return true; } - return false; + return SkipReplies < 0; // skips forever } public int Database { get; set; } @@ -41,6 +131,8 @@ public void Dispose() try { pipe.Output.Complete(); } catch { } if (pipe is IDisposable d) try { d.Dispose(); } catch { } } + + _readState = default; } private int _activeSlot = ServerSelectionStrategy.NoSlot; @@ -220,13 +312,12 @@ public ExecResult FlushMulti(out byte[][] commands) // completely unoptimized for now; this is fine private List _transaction; // null until needed - internal bool BufferMulti(in RedisRequest request, in CommandBytes command) + internal bool BufferMulti(in RedisRequest request, in AsciiHash command) { switch (_transactionState) { case TransactionState.MultiHopeful when !AllowInTransaction(command): - // TODO we also can't do this bit! just store the command name for now - (_transaction ??= []).Add(Encoding.ASCII.GetBytes(request.GetString(0))); + (_transaction ??= []).Add(request.Serialize()); return true; case TransactionState.MultiAbortByError when !AllowInTransaction(command): case TransactionState.MultiDoomedByTouch when !AllowInTransaction(command): @@ -236,12 +327,12 @@ internal bool BufferMulti(in RedisRequest request, in CommandBytes command) return false; } - static bool AllowInTransaction(in CommandBytes cmd) + static bool AllowInTransaction(in AsciiHash cmd) => cmd.Equals(EXEC) || cmd.Equals(DISCARD) || cmd.Equals(MULTI) || cmd.Equals(WATCH) || cmd.Equals(UNWATCH); } - private static readonly CommandBytes + private static readonly AsciiHash EXEC = new("EXEC"u8), DISCARD = new("DISCARD"u8), MULTI = new("MULTI"u8), WATCH = new("WATCH"u8), UNWATCH = new("UNWATCH"u8); } diff --git a/toys/StackExchange.Redis.Server/RedisRequest.cs b/toys/StackExchange.Redis.Server/RedisRequest.cs index d8ea13b86..269e31d9a 100644 --- a/toys/StackExchange.Redis.Server/RedisRequest.cs +++ b/toys/StackExchange.Redis.Server/RedisRequest.cs @@ -1,14 +1,14 @@ using System; +using System.Buffers; +using System.Diagnostics; +using RESPite; +using RESPite.Messages; namespace StackExchange.Redis.Server { public readonly ref struct RedisRequest { - // why ref? don't *really* need it, but: these things are "in flight" - // based on an open RawResult (which is just the detokenized ReadOnlySequence) - // so: using "ref" makes it clear that you can't expect to store these and have - // them keep working - private readonly RawResult _inner; + private readonly RespReader _rootReader; private readonly RedisClient _client; public RedisRequest WithClient(RedisClient client) => new(in this, client); @@ -30,63 +30,113 @@ public TypedRedisValue CommandNotFound() public TypedRedisValue UnknownSubcommandOrArgumentCount() => TypedRedisValue.Error($"ERR Unknown subcommand or wrong number of arguments for '{ToString()}'."); - public string GetString(int index) - => _inner[index].GetString(); + public string GetString(int index) => GetReader(index).ReadString(); - public bool IsString(int index, string value) // TODO: optimize - => string.Equals(value, _inner[index].GetString(), StringComparison.OrdinalIgnoreCase); + [Obsolete("Use IsString(int, ReadOnlySpan{byte}) instead.")] + public bool IsString(int index, string value) + => GetReader(index).Is(value); + + public bool IsString(int index, ReadOnlySpan value) + => GetReader(index).Is(value); public override int GetHashCode() => throw new NotSupportedException(); - internal RedisRequest(scoped in RawResult result) - { - _inner = result; - Count = result.ItemsCount; - } - public RedisValue GetValue(int index) - => _inner[index].AsRedisValue(); + /// + /// Get a reader initialized at the start of the payload. + /// + public RespReader GetRootReader() => _rootReader; - public int GetInt32(int index) - => (int)_inner[index].AsRedisValue(); + /// + /// Get a reader initialized at the start of the payload. + /// + public RespReader GetReader(int childIndex) + { + if (childIndex < 0 || childIndex >= Count) Throw(); + var reader = GetRootReader(); + reader.MoveNextAggregate(); + for (int i = 0; i < childIndex; i++) + { + reader.MoveNextScalar(); + } + reader.MoveNextScalar(); + return reader; - public bool TryGetInt64(int index, out long value) - => _inner[index].TryGetInt64(out value); - public bool TryGetInt32(int index, out int value) + static void Throw() => throw new ArgumentOutOfRangeException(nameof(childIndex)); + } + + internal RedisRequest(scoped in RespReader reader, ref byte[] commandLease) { - if (_inner[index].TryGetInt64(out var tmp)) + _rootReader = reader; + var local = reader; + if (local.TryMoveNext(checkError: false) & local.IsAggregate) + { + Count = local.AggregateLength(); + } + + if (Count == 0) + { + Command = s_EmptyCommand; + KnownCommand = RedisCommand.UNKNOWN; + } + else { - value = (int)tmp; - if (value == tmp) return true; + local.MoveNextScalar(); + unsafe + { + KnownCommand = local.TryParseScalar(&RedisCommandMetadata.TryParseCI, out RedisCommand cmd) + ? cmd : RedisCommand.UNKNOWN; + } + var len = local.ScalarLength(); + if (len > commandLease.Length) + { + ArrayPool.Shared.Return(commandLease); + commandLease = ArrayPool.Shared.Rent(len); + } + var readBytes = local.CopyTo(commandLease); + Debug.Assert(readBytes == len); + AsciiHash.ToUpper(commandLease.AsSpan(0, readBytes)); + // note we retain the lease array in the Command, this is intentional + Command = new(commandLease, 0, readBytes); } + } + + internal RedisCommand KnownCommand { get; } - value = 0; - return false; + internal static byte[] GetLease() => ArrayPool.Shared.Rent(16); + internal static void ReleaseLease(ref byte[] commandLease) + { + ArrayPool.Shared.Return(commandLease); + commandLease = []; } - public long GetInt64(int index) => (long)_inner[index].AsRedisValue(); + private static readonly AsciiHash s_EmptyCommand = new(Array.Empty()); + + public readonly AsciiHash Command; + + public RedisValue GetValue(int index) => GetReader(index).ReadRedisValue(); + + public bool TryGetInt64(int index, out long value) => GetReader(index).TryReadInt64(out value); + + public bool TryGetInt32(int index, out int value) => GetReader(index).TryReadInt32(out value); + + public int GetInt32(int index) => GetReader(index).ReadInt32(); + + public long GetInt64(int index) => GetReader(index).ReadInt64(); public RedisKey GetKey(int index, KeyFlags flags = KeyFlags.None) { - var key = _inner[index].AsRedisKey(); + var key = GetReader(index).ReadRedisKey(); _client?.OnKey(key, flags); return key; } internal RedisChannel GetChannel(int index, RedisChannel.RedisChannelOptions options) - => _inner[index].AsRedisChannel(null, options); + => GetReader(index).ReadRedisChannel(options); - internal bool TryGetCommandBytes(int i, out CommandBytes command) - { - var payload = _inner[i].Payload; - if (payload.Length > CommandBytes.MaxLength) - { - command = default; - return false; - } + internal RedisRequest(ReadOnlySpan payload, ref byte[] commandLease) : this(new RespReader(payload), ref commandLease) { } + internal RedisRequest(in ReadOnlySequence payload, ref byte[] commandLease) : this(new RespReader(payload), ref commandLease) { } - command = payload.IsEmpty ? default : new CommandBytes(payload); - return true; - } + public byte[] Serialize() => _rootReader.Serialize(); } [Flags] diff --git a/toys/StackExchange.Redis.Server/RedisServer.PubSub.cs b/toys/StackExchange.Redis.Server/RedisServer.PubSub.cs index 140dd3caa..7778ed63b 100644 --- a/toys/StackExchange.Redis.Server/RedisServer.PubSub.cs +++ b/toys/StackExchange.Redis.Server/RedisServer.PubSub.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading; +using RESPite.Messages; namespace StackExchange.Redis.Server; @@ -182,7 +183,7 @@ public int Publish(in RedisChannel channel, in RedisValue value) // we can do simple and sharded equality lookups directly if ((simpleCount + shardedCount) != 0 && subs.TryGetValue(channel, out _)) { - var msg = TypedRedisValue.Rent(3, out var span, ResultType.Push); + var msg = TypedRedisValue.Rent(3, out var span, PushKind); span[0] = TypedRedisValue.BulkString(channel.IsSharded ? "smessage" : "message"); span[1] = TypedRedisValue.BulkString(channel); span[2] = TypedRedisValue.BulkString(value); @@ -198,7 +199,7 @@ public int Publish(in RedisChannel channel, in RedisValue value) { if (pair.Key.IsPattern && pair.Value is { } glob && glob.IsMatch(channelName)) { - var msg = TypedRedisValue.Rent(4, out var span, ResultType.Push); + var msg = TypedRedisValue.Rent(4, out var span, PushKind); span[0] = TypedRedisValue.BulkString("pmessage"); span[1] = TypedRedisValue.BulkString(pair.Key); span[2] = TypedRedisValue.BulkString(channel); @@ -213,11 +214,15 @@ public int Publish(in RedisChannel channel, in RedisValue value) return count; } - private void SendMessage(string kind, RedisChannel channel, int count) + public bool IsResp2 => Protocol is RedisProtocol.Resp2; + + public RespPrefix PushKind => IsResp2 ? RespPrefix.Array : RespPrefix.Push; + + private void SendSubUnsubMessage(string kind, RedisChannel channel, int count) { if (Node is { } node) { - var reply = TypedRedisValue.Rent(3, out var span, ResultType.Push); + var reply = TypedRedisValue.Rent(3, out var span, PushKind); span[0] = TypedRedisValue.BulkString(kind); span[1] = TypedRedisValue.BulkString((byte[])channel); span[2] = TypedRedisValue.Integer(count); @@ -226,20 +231,34 @@ private void SendMessage(string kind, RedisChannel channel, int count) } } + private ref int GetCountField(RedisChannel channel) + => ref channel.IsSharded ? ref shardedCount + : ref channel.IsPattern ? ref patternCount + : ref simpleCount; + internal void Subscribe(RedisChannel channel) { Regex glob = channel.IsPattern ? BuildGlob(channel) : null; var subs = Subscriptions; int count; + ref int field = ref GetCountField(channel); lock (subs) { - if (subs.ContainsKey(channel)) return; - subs.Add(channel, glob); - count = channel.IsSharded ? ++shardedCount - : channel.IsPattern ? ++patternCount - : ++simpleCount; + #if NET + count = subs.TryAdd(channel, glob) ? ++field : field; + #else + if (subs.ContainsKey(channel)) + { + count = field; + } + else + { + subs.Add(channel, glob); + count = ++field; + } + #endif } - SendMessage( + SendSubUnsubMessage( channel.IsSharded ? "ssubscribe" : channel.IsPattern ? "psubscribe" : "subscribe", @@ -266,14 +285,12 @@ internal void Unsubscribe(RedisChannel channel) var subs = SubscriptionsIfAny; if (subs is null) return; int count; + ref int field = ref GetCountField(channel); lock (subs) { - if (!subs.Remove(channel)) return; - count = channel.IsSharded ? --shardedCount - : channel.IsPattern ? --patternCount - : --simpleCount; + count = subs.Remove(channel) ? --field : field; } - SendMessage( + SendSubUnsubMessage( channel.IsSharded ? "sunsubscribe" : channel.IsPattern ? "punsubscribe" : "unsubscribe", @@ -332,7 +349,7 @@ internal void UnsubscribeAll(RedisCommand cmd) } foreach (var key in remove.AsSpan(0, count)) { - SendMessage(msg, key, 0); + SendSubUnsubMessage(msg, key, 0); } ArrayPool.Shared.Return(remove); } diff --git a/toys/StackExchange.Redis.Server/RedisServer.cs b/toys/StackExchange.Redis.Server/RedisServer.cs index 1e92aa8ec..aa26e34a6 100644 --- a/toys/StackExchange.Redis.Server/RedisServer.cs +++ b/toys/StackExchange.Redis.Server/RedisServer.cs @@ -7,6 +7,8 @@ using System.Net; using System.Text; using System.Threading; +using RESPite; +using RESPite.Messages; namespace StackExchange.Redis.Server { @@ -155,57 +157,26 @@ protected override void AppendStats(StringBuilder sb) public override TypedRedisValue Execute(RedisClient client, in RedisRequest request) { - if (request.Count != 0) + var pw = Password; + if (pw.Length != 0 & !client.IsAuthenticated) { - var pw = Password; - if (pw.Length != 0 & !client.IsAuthenticated) - { - if (!Literals.IsAuthCommand(in request)) - return TypedRedisValue.Error("NOAUTH Authentication required."); - } - else if (client.Protocol is RedisProtocol.Resp2 && client.IsSubscriber && - !Literals.IsPubSubCommand(in request, out var cmd)) - { - return TypedRedisValue.Error( - $"ERR only (P|S)SUBSCRIBE / (P|S)UNSUBSCRIBE / PING / QUIT allowed in this context (got: '{cmd}')"); - } + if (!IsAuthCommand(request.KnownCommand)) + return TypedRedisValue.Error("NOAUTH Authentication required."); } - return base.Execute(client, request); - } - - internal class Literals - { - public static readonly CommandBytes - AUTH = new("AUTH"u8), - HELLO = new("HELLO"u8), - SETNAME = new("SETNAME"u8), - QUIT = new("SETNAME"u8), - PING = new("PING"u8), - SUBSCRIBE = new("SUBSCRIBE"u8), - PSUBSCRIBE = new("PSUBSCRIBE"u8), - SSUBSCRIBE = new("SSUBSCRIBE"u8), - UNSUBSCRIBE = new("UNSUBSCRIBE"u8), - PUNSUBSCRIBE = new("PUNSUBSCRIBE"u8), - SUNSUBSCRIBE = new("SUNSUBSCRIBE"u8); - - public static bool IsAuthCommand(in RedisRequest request) => - request.Count != 0 && request.TryGetCommandBytes(0, out var command) - && (command.Equals(AUTH) || command.Equals(HELLO)); - - public static bool IsPubSubCommand(in RedisRequest request, out string badCommand) + else if (client.Protocol is RedisProtocol.Resp2 && client.IsSubscriber && + !IsPubSubCommand(request.KnownCommand)) { - badCommand = ""; - if (request.Count == 0 || !request.TryGetCommandBytes(0, out var command)) - { - if (request.Count != 0) badCommand = request.GetString(0); - return false; - } - - return command.Equals(SUBSCRIBE) || command.Equals(UNSUBSCRIBE) - || command.Equals(SSUBSCRIBE) || command.Equals(SUNSUBSCRIBE) - || command.Equals(PSUBSCRIBE) || command.Equals(PUNSUBSCRIBE) - || command.Equals(PING) || command.Equals(QUIT); + return TypedRedisValue.Error( + $"ERR only [P|S][UN]SUBSCRIBE / PING / QUIT allowed in this context (got: '{request.Command}')"); } + return base.Execute(client, request); + + static bool IsAuthCommand(RedisCommand cmd) => cmd is RedisCommand.AUTH or RedisCommand.HELLO; + static bool IsPubSubCommand(RedisCommand cmd) + => cmd is RedisCommand.SUBSCRIBE or RedisCommand.UNSUBSCRIBE + or RedisCommand.SSUBSCRIBE or RedisCommand.SUNSUBSCRIBE + or RedisCommand.PSUBSCRIBE or RedisCommand.PUNSUBSCRIBE + or RedisCommand.PING or RedisCommand.QUIT; } [RedisCommand(2)] @@ -239,28 +210,37 @@ protected virtual TypedRedisValue Hello(RedisClient client, in RedisRequest requ default: return TypedRedisValue.Error("NOPROTO unsupported protocol version"); } + static TypedRedisValue ArgFail(in RespReader reader) => TypedRedisValue.Error($"ERR Syntax error in HELLO option '{reader.ReadString()}'\""); - for (int i = 2; i < request.Count && request.TryGetCommandBytes(i, out var key); i++) + for (int i = 2; i < request.Count; i++) { int remaining = request.Count - (i + 1); - TypedRedisValue ArgFail() => TypedRedisValue.Error($"ERR Syntax error in HELLO option '{key.ToString().ToLower()}'\""); - if (key.Equals(Literals.AUTH)) - { - if (remaining < 2) return ArgFail(); - // ignore username for now - var pw = request.GetString(i + 2); - if (pw != Password) return TypedRedisValue.Error("WRONGPASS invalid username-password pair or user is disabled."); - isAuthed = true; - i += 2; - } - else if (key.Equals(Literals.SETNAME)) + var fieldReader = request.GetReader(i); + HelloSubFields field; + unsafe { - if (remaining < 1) return ArgFail(); - name = request.GetString(++i); + if (!fieldReader.TryParseScalar(&HelloSubFieldsMetadata.TryParseCI, out field)) + { + return ArgFail(fieldReader); + } } - else + + switch (field) { - return ArgFail(); + case HelloSubFields.Auth: + if (remaining < 2) return ArgFail(fieldReader); + // ignore username for now + var pw = request.GetString(i + 2); + if (pw != Password) return TypedRedisValue.Error("WRONGPASS invalid username-password pair or user is disabled."); + isAuthed = true; + i += 2; + break; + case HelloSubFields.SetName: + if (remaining < 1) return ArgFail(fieldReader); + name = request.GetString(++i); + break; + default: + return ArgFail(fieldReader); } } } @@ -270,7 +250,7 @@ protected virtual TypedRedisValue Hello(RedisClient client, in RedisRequest requ client.IsAuthenticated = isAuthed; client.Name = name; - var reply = TypedRedisValue.Rent(14, out var span, ResultType.Map); + var reply = TypedRedisValue.Rent(14, out var span, RespPrefix.Map); span[0] = TypedRedisValue.BulkString("server"); span[1] = TypedRedisValue.BulkString("redis"); span[2] = TypedRedisValue.BulkString("version"); @@ -284,7 +264,7 @@ protected virtual TypedRedisValue Hello(RedisClient client, in RedisRequest requ span[10] = TypedRedisValue.BulkString("role"); span[11] = TypedRedisValue.BulkString("master"); span[12] = TypedRedisValue.BulkString("modules"); - span[13] = TypedRedisValue.EmptyArray(ResultType.Array); + span[13] = TypedRedisValue.EmptyArray(RespPrefix.Array); return reply; } @@ -381,7 +361,6 @@ protected virtual TypedRedisValue Watch(RedisClient client, in RedisRequest requ if (!client.Watch(key)) return TypedRedisValue.Error("WATCH inside MULTI is not allowed"); } - return TypedRedisValue.OK; } @@ -412,21 +391,27 @@ protected virtual TypedRedisValue Exec(RedisClient client, in RedisRequest reque case RedisClient.ExecResult.NotInTransaction: return TypedRedisValue.Error("EXEC without MULTI"); case RedisClient.ExecResult.WatchConflict: - return TypedRedisValue.NullArray(ResultType.Array); + return TypedRedisValue.NullArray(RespPrefix.Array); case RedisClient.ExecResult.AbortedByError: return TypedRedisValue.Error("EXECABORT Transaction discarded because of previous errors."); } Debug.Assert(exec is RedisClient.ExecResult.CommandsReturned); - var results = TypedRedisValue.Rent(commands.Length, out var span, ResultType.Array); + var results = TypedRedisValue.Rent(commands.Length, out var span, RespPrefix.Array); int index = 0; - foreach (var cmd in commands) + var lease = RedisRequest.GetLease(); + try + { + foreach (var cmd in commands) + { + RedisRequest inner = new(cmd, ref lease); + inner = inner.WithClient(client); + span[index++] = Execute(client, inner); + } + } + finally { - // TODO:this is the bit we can't do just yet, until we can freely parse results - // RedisRequest inner = // ... - // inner = inner.WithClient(client); - // results[index++] = Execute(client, cmd); - span[index++] = TypedRedisValue.Error($"ERR transactions not yet implemented, sorry; ignoring {Encoding.ASCII.GetString(cmd)}"); + RedisRequest.ReleaseLease(ref lease); } return results; } @@ -437,31 +422,35 @@ protected virtual void SetEx(int database, in RedisKey key, TimeSpan timeout, in Expire(database, key, timeout); } - [RedisCommand(3, "client", "setname", LockFree = true)] + [RedisCommand(3, nameof(RedisCommand.CLIENT), "setname", LockFree = true)] protected virtual TypedRedisValue ClientSetname(RedisClient client, in RedisRequest request) { client.Name = request.GetString(2); return TypedRedisValue.OK; } - [RedisCommand(2, "client", "getname", LockFree = true)] + [RedisCommand(2, nameof(RedisCommand.CLIENT), "getname", LockFree = true)] protected virtual TypedRedisValue ClientGetname(RedisClient client, in RedisRequest request) => TypedRedisValue.BulkString(client.Name); - [RedisCommand(3, "client", "reply", LockFree = true)] + [RedisCommand(3, nameof(RedisCommand.CLIENT), "reply", LockFree = true)] protected virtual TypedRedisValue ClientReply(RedisClient client, in RedisRequest request) { - if (request.IsString(2, "on")) client.SkipReplies = -1; // reply to nothing - else if (request.IsString(2, "off")) client.SkipReplies = 0; // reply to everything - else if (request.IsString(2, "skip")) client.SkipReplies = 2; // this one, and the next one + if (request.IsString(2, "on"u8)) client.SkipReplies = -1; // reply to nothing + else if (request.IsString(2, "off"u8)) client.SkipReplies = 0; // reply to everything + else if (request.IsString(2, "skip"u8)) client.SkipReplies = 2; // this one, and the next one else return TypedRedisValue.Error("ERR syntax error"); return TypedRedisValue.OK; } - [RedisCommand(2, "client", "id", LockFree = true)] + [RedisCommand(2, nameof(RedisCommand.CLIENT), "id", LockFree = true)] protected virtual TypedRedisValue ClientId(RedisClient client, in RedisRequest request) => TypedRedisValue.Integer(client.Id); + [RedisCommand(4, nameof(RedisCommand.CLIENT), "setinfo", LockFree = true)] + protected virtual TypedRedisValue ClientSetInfo(RedisClient client, in RedisRequest request) + => TypedRedisValue.OK; // only exists to keep logs clean + private bool IsClusterEnabled(out TypedRedisValue fault) { if (ServerType == ServerType.Cluster) @@ -507,21 +496,21 @@ protected virtual TypedRedisValue ClusterSlots(RedisClient client, in RedisReque { count += pair.Value.Slots.Length; } - var slots = TypedRedisValue.Rent(count, out var slotsSpan, ResultType.Array); + var slots = TypedRedisValue.Rent(count, out var slotsSpan, RespPrefix.Array); foreach (var pair in _nodes.OrderBy(x => x.Key, EndPointComparer.Instance)) { string host = GetHost(pair.Key, out int port); foreach (var range in pair.Value.Slots) { if (index >= count) break; // someone changed things while we were working - slotsSpan[index++] = TypedRedisValue.Rent(3, out var slotSpan, ResultType.Array); + slotsSpan[index++] = TypedRedisValue.Rent(3, out var slotSpan, RespPrefix.Array); slotSpan[0] = TypedRedisValue.Integer(range.From); slotSpan[1] = TypedRedisValue.Integer(range.To); - slotSpan[2] = TypedRedisValue.Rent(4, out var nodeSpan, ResultType.Array); + slotSpan[2] = TypedRedisValue.Rent(4, out var nodeSpan, RespPrefix.Array); nodeSpan[0] = TypedRedisValue.BulkString(host); nodeSpan[1] = TypedRedisValue.Integer(port); nodeSpan[2] = TypedRedisValue.BulkString(pair.Value.Id); - nodeSpan[3] = TypedRedisValue.EmptyArray(ResultType.Array); + nodeSpan[3] = TypedRedisValue.EmptyArray(RespPrefix.Array); } } return slots; @@ -602,6 +591,7 @@ public override string ToString() private SlotRange[] _slots; private readonly RedisServer _server; + public RedisServer Server => _server; public Node(RedisServer server, EndPoint endpoint) { Host = GetHost(endpoint, out var port); @@ -792,10 +782,6 @@ protected override Node GetNode(int hashSlot) return base.GetNode(hashSlot); } - [RedisCommand(-1)] - protected virtual TypedRedisValue Sentinel(RedisClient client, in RedisRequest request) - => request.CommandNotFound(); - [RedisCommand(-3)] protected virtual TypedRedisValue Lpush(RedisClient client, in RedisRequest request) { @@ -845,12 +831,12 @@ protected virtual TypedRedisValue LRange(RedisClient client, in RedisRequest req long start = request.GetInt64(2), stop = request.GetInt64(3); var len = Llen(client.Database, key); - if (len == 0) return TypedRedisValue.EmptyArray(ResultType.Array); + if (len == 0) return TypedRedisValue.EmptyArray(RespPrefix.Array); if (start < 0) start = len + start; if (stop < 0) stop = len + stop; - if (stop < 0 || start >= len || stop < start) return TypedRedisValue.EmptyArray(ResultType.Array); + if (stop < 0 || start >= len || stop < start) return TypedRedisValue.EmptyArray(RespPrefix.Array); if (start < 0) start = 0; else if (start >= len) start = len - 1; @@ -858,7 +844,7 @@ protected virtual TypedRedisValue LRange(RedisClient client, in RedisRequest req if (stop < 0) stop = 0; else if (stop >= len) stop = len - 1; - var arr = TypedRedisValue.Rent(checked((int)((stop - start) + 1)), out var span, ResultType.Array); + var arr = TypedRedisValue.Rent(checked((int)((stop - start) + 1)), out var span, RespPrefix.Array); LRange(client.Database, key, start, span); return arr; } @@ -895,7 +881,7 @@ internal int CountMatch(string pattern) return count; } } - [RedisCommand(3, "config", "get", LockFree = true)] + [RedisCommand(3, nameof(RedisCommand.CONFIG), "get", LockFree = true)] protected virtual TypedRedisValue Config(RedisClient client, in RedisRequest request) { var pattern = request.GetString(2); @@ -903,9 +889,9 @@ protected virtual TypedRedisValue Config(RedisClient client, in RedisRequest req OnUpdateServerConfiguration(); var config = ServerConfiguration; var matches = config.CountMatch(pattern); - if (matches == 0) return TypedRedisValue.EmptyArray(ResultType.Map); + if (matches == 0) return TypedRedisValue.EmptyArray(RespPrefix.Map); - var arr = TypedRedisValue.Rent(2 * matches, out var span, ResultType.Map); + var arr = TypedRedisValue.Rent(2 * matches, out var span, RespPrefix.Map); int index = 0; foreach (var pair in config.Wrapped) { @@ -1095,8 +1081,8 @@ protected virtual TypedRedisValue Keys(RedisClient client, in RedisRequest reque if (found == null) found = new List(); found.Add(TypedRedisValue.BulkString(key.AsRedisValue())); } - if (found == null) return TypedRedisValue.EmptyArray(ResultType.Array); - return TypedRedisValue.MultiBulk(found, ResultType.Array); + if (found == null) return TypedRedisValue.EmptyArray(RespPrefix.Array); + return TypedRedisValue.MultiBulk(found, RespPrefix.Array); } protected virtual IEnumerable Keys(int database, in RedisKey pattern) => throw new NotSupportedException(); @@ -1181,7 +1167,7 @@ StringBuilder AddHeader() } } - [RedisCommand(2, "memory", "purge")] + [RedisCommand(2, nameof(RedisCommand.MEMORY), "purge")] protected virtual TypedRedisValue MemoryPurge(RedisClient client, in RedisRequest request) { GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); @@ -1191,7 +1177,7 @@ protected virtual TypedRedisValue MemoryPurge(RedisClient client, in RedisReques protected virtual TypedRedisValue Mget(RedisClient client, in RedisRequest request) { int argCount = request.Count; - var arr = TypedRedisValue.Rent(argCount - 1, out var span, ResultType.Map); + var arr = TypedRedisValue.Rent(argCount - 1, out var span, RespPrefix.Map); var db = client.Database; for (int i = 1; i < argCount; i++) { @@ -1214,9 +1200,9 @@ protected virtual TypedRedisValue Mset(RedisClient client, in RedisRequest reque [RedisCommand(-1, LockFree = true, MaxArgs = 2)] protected virtual TypedRedisValue Ping(RedisClient client, in RedisRequest request) { - if (client.IsSubscriber) + if (client.IsResp2 & client.IsSubscriber) { - var reply = TypedRedisValue.Rent(2, out var span, ResultType.Array); + var reply = TypedRedisValue.Rent(2, out var span, RespPrefix.Array); span[0] = TypedRedisValue.BulkString("pong"); RedisValue value = request.Count == 1 ? RedisValue.Null : request.GetValue(1); span[1] = TypedRedisValue.BulkString(value); @@ -1236,10 +1222,10 @@ protected virtual TypedRedisValue Quit(RedisClient client, in RedisRequest reque [RedisCommand(1, LockFree = true)] protected virtual TypedRedisValue Role(RedisClient client, in RedisRequest request) { - var arr = TypedRedisValue.Rent(3, out var span, ResultType.Array); + var arr = TypedRedisValue.Rent(3, out var span, RespPrefix.Array); span[0] = TypedRedisValue.BulkString("master"); span[1] = TypedRedisValue.Integer(0); - span[2] = TypedRedisValue.EmptyArray(ResultType.Array); + span[2] = TypedRedisValue.EmptyArray(RespPrefix.Array); return arr; } @@ -1247,8 +1233,7 @@ protected virtual TypedRedisValue Role(RedisClient client, in RedisRequest reque protected virtual TypedRedisValue Select(RedisClient client, in RedisRequest request) { var raw = request.GetValue(1); - if (!raw.IsInteger) return TypedRedisValue.Error("ERR invalid DB index"); - int db = (int)raw; + if (!raw.TryParse(out int db)) return TypedRedisValue.Error("ERR invalid DB index"); if (db < 0 || db >= Databases) return TypedRedisValue.Error("ERR DB index is out of range"); client.Database = db; return TypedRedisValue.OK; @@ -1263,7 +1248,7 @@ protected virtual TypedRedisValue Time(RedisClient client, in RedisRequest reque var ticks = delta.Ticks; var seconds = ticks / TimeSpan.TicksPerSecond; var micros = (ticks % TimeSpan.TicksPerSecond) / (TimeSpan.TicksPerMillisecond / 1000); - var reply = TypedRedisValue.Rent(2, out var span, ResultType.Array); + var reply = TypedRedisValue.Rent(2, out var span, RespPrefix.Array); span[0] = TypedRedisValue.BulkString(seconds); span[1] = TypedRedisValue.BulkString(micros); return reply; @@ -1295,5 +1280,25 @@ protected virtual long IncrBy(int database, in RedisKey key, long delta) Set(database, key, value); return value; } + + public virtual void OnFlush(RedisClient client, int messages, long bytes) + { + } + } + + internal static partial class HelloSubFieldsMetadata + { + [AsciiHash(CaseSensitive = false)] + public static partial bool TryParseCI(ReadOnlySpan command, out HelloSubFields value); + } + + internal enum HelloSubFields + { + [AsciiHash("")] + None = 0, + [AsciiHash("AUTH")] + Auth, + [AsciiHash("SETNAME")] + SetName, } } diff --git a/toys/StackExchange.Redis.Server/RespReaderExtensions.cs b/toys/StackExchange.Redis.Server/RespReaderExtensions.cs new file mode 100644 index 000000000..8ee5f921c --- /dev/null +++ b/toys/StackExchange.Redis.Server/RespReaderExtensions.cs @@ -0,0 +1,213 @@ +#nullable enable +extern alias seredis; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using RESPite.Messages; + +namespace StackExchange.Redis; // this really belongs in SE.Redis, will be moved in v3 + +internal static class RespReaderExtensions +{ + extension(in RespReader reader) + { + public RedisValue ReadRedisValue() + { + reader.DemandScalar(); + if (reader.IsNull) return RedisValue.Null; + + return reader.Prefix switch + { + RespPrefix.Boolean => reader.ReadBoolean(), + RespPrefix.Integer => reader.ReadInt64(), + _ when reader.TryReadInt64(out var i64) => i64, + _ when reader.TryReadDouble(out var fp64) => fp64, + _ => reader.ReadByteArray(), + }; + } + + public string DebugReadTruncatedString(int maxChars) + { + if (!reader.IsScalar) return ""; + try + { + var s = reader.ReadString() ?? ""; + return s.Length <= maxChars ? s : s.Substring(0, maxChars) + "..."; + } + catch + { + return ""; + } + } + + public RedisKey ReadRedisKey() => (RedisKey)reader.ReadByteArray(); + + public RedisChannel ReadRedisChannel(RedisChannel.RedisChannelOptions options) + => new(reader.ReadByteArray(), options); + + private bool TryGetFirst(out string first) + { + if (reader.IsNonNullAggregate && !reader.AggregateIsEmpty()) + { + var clone = reader.Clone(); + if (clone.TryMoveNext()) + { + unsafe + { + if (clone.IsScalar && + clone.TryParseScalar(&PhysicalConnection.PushKindMetadata.TryParse, out PhysicalConnection.PushKind kind)) + { + first = kind.ToString(); + return true; + } + } + + first = clone.GetOverview(); + return true; + } + } + first = ""; + return false; + } + + public string GetOverview() + { + // return reader.BufferUtf8(); // <== for when you really can't grok what is happening + if (reader.Prefix is RespPrefix.None) + { + var copy = reader; + copy.MovePastBof(); + return copy.Prefix is RespPrefix.None ? "(empty)" : copy.GetOverview(); + } + if (reader.IsNull) return "(null)"; + + return reader.Prefix switch + { + RespPrefix.SimpleString or RespPrefix.Integer or RespPrefix.SimpleError or RespPrefix.Double => $"{reader.Prefix}: {reader.ReadString()}", + RespPrefix.Push when reader.TryGetFirst(out var first) => $"{reader.Prefix} ({first}): {reader.AggregateLength()} items", + _ when reader.IsScalar => $"{reader.Prefix}: {reader.ScalarLength()} bytes, '{reader.DebugReadTruncatedString(16)}'", + _ when reader.IsAggregate => $"{reader.Prefix}: {reader.AggregateLength()} items", + _ => $"(unknown: {reader.Prefix})", + }; + } + + public RespPrefix GetFirstPrefix() + { + var prefix = reader.Prefix; + if (prefix is RespPrefix.None) + { + var mutable = reader; + mutable.MovePastBof(); + prefix = mutable.Prefix; + } + return prefix; + } + + /* + public bool AggregateHasAtLeast(int count) + { + reader.DemandAggregate(); + if (reader.IsNull) return false; + if (reader.IsStreaming) return CheckStreamingAggregateAtLeast(in reader, count); + return reader.AggregateLength() >= count; + + static bool CheckStreamingAggregateAtLeast(in RespReader reader, int count) + { + var iter = reader.AggregateChildren(); + object? attributes = null; + while (count > 0 && iter.MoveNextRaw(null!, ref attributes)) + { + count--; + } + + return count == 0; + } + } + */ + } + + extension(ref RespReader reader) + { + public bool SafeTryMoveNext() => reader.TryMoveNext(checkError: false) & !reader.IsError; + + public void MovePastBof() + { + // if we're at BOF, read the first element, ignoring errors + if (reader.Prefix is RespPrefix.None) reader.SafeTryMoveNext(); + } + + public RedisValue[]? ReadPastRedisValues() + => reader.ReadPastArray(static (ref r) => r.ReadRedisValue(), scalar: true); + + public seredis::StackExchange.Redis.Lease? AsLease() + { + if (!reader.IsScalar) throw new InvalidCastException("Cannot convert to Lease: " + reader.Prefix); + if (reader.IsNull) return null; + + var length = reader.ScalarLength(); + if (length == 0) return seredis::StackExchange.Redis.Lease.Empty; + + var lease = seredis::StackExchange.Redis.Lease.Create(length, clear: false); + if (reader.TryGetSpan(out var span)) + { + span.CopyTo(lease.Span); + } + else + { + var buffer = reader.Buffer(lease.Span); + Debug.Assert(buffer.Length == length, "buffer length mismatch"); + } + return lease; + } + } + + public static RespPrefix GetRespPrefix(ReadOnlySpan frame) + { + var reader = new RespReader(frame); + reader.SafeTryMoveNext(); + return reader.Prefix; + } + + extension(RespPrefix prefix) + { + public ResultType ToResultType() => prefix switch + { + RespPrefix.Array => ResultType.Array, + RespPrefix.Attribute => ResultType.Attribute, + RespPrefix.BigInteger => ResultType.BigInteger, + RespPrefix.Boolean => ResultType.Boolean, + RespPrefix.BulkError => ResultType.BlobError, + RespPrefix.BulkString => ResultType.BulkString, + RespPrefix.SimpleString => ResultType.SimpleString, + RespPrefix.Map => ResultType.Map, + RespPrefix.Set => ResultType.Set, + RespPrefix.Double => ResultType.Double, + RespPrefix.Integer => ResultType.Integer, + RespPrefix.SimpleError => ResultType.Error, + RespPrefix.Null => ResultType.Null, + RespPrefix.VerbatimString => ResultType.VerbatimString, + RespPrefix.Push=> ResultType.Push, + _ => throw new ArgumentOutOfRangeException(nameof(prefix), prefix, null), + }; + } + + extension(T?[] array) where T : class + { + internal bool AnyNull() + { + foreach (var el in array) + { + if (el is null) return true; + } + + return false; + } + } + +#if !NET + extension(Task task) + { + public bool IsCompletedSuccessfully => task.Status is TaskStatus.RanToCompletion; + } +#endif +} diff --git a/toys/StackExchange.Redis.Server/RespServer.cs b/toys/StackExchange.Redis.Server/RespServer.cs index fd592e3d8..5826d97f1 100644 --- a/toys/StackExchange.Redis.Server/RespServer.cs +++ b/toys/StackExchange.Redis.Server/RespServer.cs @@ -12,6 +12,9 @@ using System.Threading.Tasks; using Pipelines.Sockets.Unofficial; using Pipelines.Sockets.Unofficial.Arenas; +using RESPite; +using RESPite.Buffers; +using RESPite.Messages; namespace StackExchange.Redis.Server { @@ -41,7 +44,7 @@ public HashSet GetCommands() return set; } - private static Dictionary BuildCommands(RespServer server) + private static Dictionary BuildCommands(RespServer server) { static RedisCommandAttribute CheckSignatureAndGetAttribute(MethodInfo method) { @@ -51,13 +54,16 @@ static RedisCommandAttribute CheckSignatureAndGetAttribute(MethodInfo method) return null; return (RedisCommandAttribute)Attribute.GetCustomAttribute(method, typeof(RedisCommandAttribute)); } - var grouped = from method in server.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - let attrib = CheckSignatureAndGetAttribute(method) - where attrib != null - select new RespCommand(attrib, method, server) into cmd - group cmd by cmd.Command; - var result = new Dictionary(); + var grouped = ( + from method in server.GetType() + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + let attrib = CheckSignatureAndGetAttribute(method) + where attrib != null + select new RespCommand(attrib, method, server)) + .GroupBy(x => new AsciiHash(x.Command.ToUpperInvariant()), AsciiHash.CaseSensitiveEqualityComparer); + + var result = new Dictionary(AsciiHash.CaseSensitiveEqualityComparer); foreach (var grp in grouped) { RespCommand parent; @@ -71,9 +77,8 @@ static RedisCommandAttribute CheckSignatureAndGetAttribute(MethodInfo method) parent = grp.Single(); } - var cmd = new CommandBytes(grp.Key); - Debug.WriteLine($"Registering: {cmd}"); - result.Add(cmd, parent); + Debug.WriteLine($"Registering: {grp.Key}"); + result.Add(grp.Key, parent); } return result; } @@ -110,22 +115,24 @@ public RedisCommandAttribute( public int Arity { get; } public bool LockFree { get; set; } } - private readonly Dictionary _commands; + private readonly Dictionary _commands; private readonly struct RespCommand { public RespCommand(RedisCommandAttribute attrib, MethodInfo method, RespServer server) { _operation = (RespOperation)Delegate.CreateDelegate(typeof(RespOperation), server, method); - Command = (string.IsNullOrWhiteSpace(attrib.Command) ? method.Name : attrib.Command).Trim().ToLowerInvariant(); - CommandBytes = new CommandBytes(Command); + + var command = attrib.Command; + if (string.IsNullOrEmpty(command)) command = method.Name; + + Command = command; SubCommand = attrib.SubCommand?.Trim()?.ToLowerInvariant(); Arity = attrib.Arity; MaxArgs = attrib.MaxArgs; LockFree = attrib.LockFree; _subcommands = null; } - private CommandBytes CommandBytes { get; } public string Command { get; } public string SubCommand { get; } public bool IsSubCommand => !string.IsNullOrEmpty(SubCommand); @@ -145,7 +152,6 @@ private RespCommand(in RespCommand parent, RespCommand[] subs) if (subs == null || subs.Length == 0) throw new InvalidOperationException("Cannot add empty sub-commands"); Command = parent.Command; - CommandBytes = parent.CommandBytes; SubCommand = parent.SubCommand; Arity = parent.Arity; MaxArgs = parent.MaxArgs; @@ -273,40 +279,61 @@ protected void DoShutdown(ShutdownReason reason) public void Dispose() => Dispose(true); protected virtual void Dispose(bool disposing) { - _arena.Dispose(); DoShutdown(ShutdownReason.ServerDisposed); } + private readonly Arena _arena = new(); + public virtual RedisServer.Node DefaultNode => null; public async Task RunClientAsync(IDuplexPipe pipe, RedisServer.Node node = null, object state = null) { Exception fault = null; RedisClient client = null; + byte[] commandLease = RedisRequest.GetLease(); + ReadOnlySequence buffer = default; + bool wasReading = false; try { node ??= DefaultNode; client = AddClient(node, state); - var incompleteOutput = client.WriteOutputAsync(pipe.Output); + OnClientConnected(client, state); + Task output = client.WriteOutputAsync(pipe.Output); while (!client.Closed) { var readResult = await pipe.Input.ReadAsync().ConfigureAwait(false); - var buffer = readResult.Buffer; + buffer = readResult.Buffer; - bool makingProgress = false; - while (!client.Closed && await TryProcessRequestAsync(ref buffer, client).ConfigureAwait(false)) + wasReading = true; + while (!client.Closed && client.TryReadRequest(buffer, out long consumed)) { - makingProgress = true; - } - pipe.Input.AdvanceTo(buffer.Start, buffer.End); + wasReading = false; + // process a completed request + RedisRequest request = new(buffer.Slice(0, consumed), ref commandLease); + request = request.WithClient(client); + var response = Execute(client, request); - if (!makingProgress && readResult.IsCompleted) - { // nothing to do, and nothing more will be arriving - break; + if (client.ShouldSkipResponse() || response.IsNil) // elective or no-result + { + response.Recycle(); + } + else + { + await client.AddOutboundAsync(response); + } + client.ResetAfterRequest(); + + // advance the buffer to account for the message we just read + buffer = buffer.Slice(consumed); + wasReading = true; } + wasReading = false; + + pipe.Input.AdvanceTo(buffer.Start, buffer.End); + if (readResult.IsCompleted) break; // EOF } client.Complete(); - await incompleteOutput; + await output; } catch (ConnectionResetException) { } catch (ObjectDisposedException) { } @@ -321,6 +348,7 @@ public async Task RunClientAsync(IDuplexPipe pipe, RedisServer.Node node = null, } finally { + RedisRequest.ReleaseLease(ref commandLease); client?.Complete(fault); RemoveClient(client); try { pipe.Input.Complete(fault); } catch { } @@ -329,159 +357,49 @@ public async Task RunClientAsync(IDuplexPipe pipe, RedisServer.Node node = null, if (fault != null && !_isShutdown) { Log("Connection faulted (" + fault.GetType().Name + "): " + fault.Message); - } - } - } - public virtual void Log(string message) - { - var output = _output; - if (output != null) - { - lock (output) - { - output.WriteLine(message); + if (wasReading) + { + Log("Read fault, buffer: " + GetUtf8String(buffer)); + } } } } - public static async ValueTask WriteResponseAsync(RedisClient client, PipeWriter output, TypedRedisValue value, RedisProtocol protocol) + internal static string GetUtf8String(in ReadOnlySequence buffer) { - static void WritePrefix(PipeWriter output, char prefix) - { - var span = output.GetSpan(1); - span[0] = (byte)prefix; - output.Advance(1); - } - - if (value.IsNil) return; // not actually a request (i.e. empty/whitespace request) - if (client != null && client.ShouldSkipResponse()) return; // intentionally skipping the result - - var type = value.Type; - if (protocol is RedisProtocol.Resp2 & type is not ResultType.Null) - { - if (type is ResultType.VerbatimString) - { - var s = (string)value.AsRedisValue(); - if (s is { Length: >= 4 } && s[3] == ':') - value = TypedRedisValue.BulkString(s.Substring(4)); - } - type = type.ToResp2(); - } -RetryResp2: - if (protocol is RedisProtocol.Resp3 && value.IsNullValueOrArray) + if (buffer.IsEmpty) return "(empty)"; + char[] lease = null; + var maxLen = Encoding.UTF8.GetMaxCharCount(checked((int)buffer.Length)); + Span target = maxLen <= 128 ? stackalloc char[128] : (lease = ArrayPool.Shared.Rent(maxLen)); + int charCount = 0; + if (buffer.IsSingleSegment) { - output.Write("_\r\n"u8); + charCount = Encoding.UTF8.GetChars(buffer.First.Span, target); } else { - char prefix; - switch (type) + foreach (var segment in buffer) { - case ResultType.Integer: - PhysicalConnection.WriteInteger(output, (long)value.AsRedisValue()); - break; - case ResultType.Error: - prefix = '-'; - goto BasicMessage; - case ResultType.SimpleString: - prefix = '+'; - BasicMessage: - WritePrefix(output, prefix); - var val = (string)value.AsRedisValue(); - var expectedLength = Encoding.UTF8.GetByteCount(val); - PhysicalConnection.WriteRaw(output, val, expectedLength); - PhysicalConnection.WriteCrlf(output); - break; - case ResultType.BulkString: - PhysicalConnection.WriteBulkString(value.AsRedisValue(), output); - break; - case ResultType.Null: - case ResultType.Push when value.IsNullArray: - case ResultType.Map when value.IsNullArray: - case ResultType.Set when value.IsNullArray: - case ResultType.Attribute when value.IsNullArray: - output.Write("_\r\n"u8); - break; - case ResultType.Array when value.IsNullArray: - PhysicalConnection.WriteMultiBulkHeader(output, -1, type); - break; - case ResultType.Push: - case ResultType.Map: - case ResultType.Array: - case ResultType.Set: - case ResultType.Attribute: - var segment = value.Segment; - PhysicalConnection.WriteMultiBulkHeader(output, segment.Count, type); - var arr = segment.Array; - int offset = segment.Offset; - for (int i = 0; i < segment.Count; i++) - { - var item = arr[offset++]; - if (item.IsNil) - throw new InvalidOperationException("Array element cannot be nil, index " + i); - - // note: don't pass client down; this would impact SkipReplies - await WriteResponseAsync(null, output, item, protocol); - } - break; - default: - // retry with RESP2 - var r2 = type.ToResp2(); - if (r2 != type) - { - Debug.WriteLine($"{type} not handled in RESP3; using {r2} instead"); - goto RetryResp2; - } - - throw new InvalidOperationException( - "Unexpected result type: " + value.Type); + charCount += Encoding.UTF8.GetChars(segment.Span, target.Slice(charCount)); } } - - await output.FlushAsync().ConfigureAwait(false); - } - - private static bool TryParseRequest(Arena arena, ref ReadOnlySequence buffer, out RedisRequest request) - { - var reader = new BufferReader(buffer); - var raw = PhysicalConnection.TryParseResult(false, arena, in buffer, ref reader, false, null, true); - if (raw.HasValue) - { - buffer = reader.SliceFromCurrent(); - request = new RedisRequest(raw); - return true; - } - request = default; - - return false; + const string CR = "\u240D", LF = "\u240A", CRLF = CR + LF; + string s = target.Slice(0, charCount).ToString() + .Replace("\r\n", CRLF).Replace("\r", CR).Replace("\n", LF); + if (lease is not null) ArrayPool.Shared.Return(lease); + return s; } - private readonly Arena _arena = new Arena(); - - public ValueTask TryProcessRequestAsync(ref ReadOnlySequence buffer, RedisClient client) + public virtual void Log(string message) { - static async ValueTask Awaited(ValueTask write) - { - await write.ConfigureAwait(false); - return true; - } - if (!buffer.IsEmpty && TryParseRequest(_arena, ref buffer, out var request)) + var output = _output; + if (output != null) { - request = request.WithClient(client); - TypedRedisValue response; - try { response = Execute(client, request); } - finally + lock (output) { - _arena.Reset(); - client.ResetAfterRequest(); + output.WriteLine(message); } - - var write = client.AddOutboundAsync(response); - if (!write.IsCompletedSuccessfully) return Awaited(write); - write.GetAwaiter().GetResult(); - return new ValueTask(true); } - return new ValueTask(false); } protected object ServerSyncLock => this; @@ -502,20 +420,17 @@ public virtual TypedRedisValue OnUnknownCommand(in RedisClient client, in RedisR public virtual TypedRedisValue Execute(RedisClient client, in RedisRequest request) { - if (request.Count == 0) return default; // not a request - - if (!request.TryGetCommandBytes(0, out var cmdBytes)) + if (request.Count == 0 || request.Command.Length == 0) // not a request { client.ExecAbort(); return request.CommandNotFound(); } - if (cmdBytes.Length == 0) return default; // not a request Interlocked.Increment(ref _totalCommandsProcesed); try { TypedRedisValue result; - if (_commands.TryGetValue(cmdBytes, out var cmd)) + if (_commands.TryGetValue(request.Command, out var cmd)) { if (cmd.HasSubCommands) { @@ -527,7 +442,7 @@ public virtual TypedRedisValue Execute(RedisClient client, in RedisRequest reque } } - if (client.BufferMulti(request, cmdBytes)) return TypedRedisValue.SimpleString("QUEUED"); + if (client.BufferMulti(request, request.Command)) return TypedRedisValue.SimpleString("QUEUED"); if (cmd.LockFree) { @@ -544,18 +459,20 @@ public virtual TypedRedisValue Execute(RedisClient client, in RedisRequest reque else { client.ExecAbort(); - Span span = stackalloc byte[CommandBytes.MaxLength]; - cmdBytes.CopyTo(span); - result = OnUnknownCommand(client, request, span.Slice(0, cmdBytes.Length)); + result = OnUnknownCommand(client, request, request.Command.Span); } - if (result.Type == ResultType.Error) Interlocked.Increment(ref _totalErrorCount); + if (result.IsError) Interlocked.Increment(ref _totalErrorCount); return result; } - catch (KeyMovedException moved) when (GetNode(moved.HashSlot) is { } node) + catch (KeyMovedException moved) { - OnMoved(client, moved.HashSlot, node); - return TypedRedisValue.Error($"MOVED {moved.HashSlot} {node.Host}:{node.Port}"); + if (GetNode(moved.HashSlot) is { } node) + { + OnMoved(client, moved.HashSlot, node); + return TypedRedisValue.Error($"MOVED {moved.HashSlot} {node.Host}:{node.Port}"); + } + return TypedRedisValue.Error($"ERR key has been migrated from slot {moved.HashSlot}, but the new owner is unknown"); } catch (CrossSlotException) { @@ -607,42 +524,43 @@ public sealed class WrongTypeException : Exception protected internal static int GetHashSlot(in RedisKey key) => s_ClusterSelectionStrategy.HashSlot(key); private static readonly ServerSelectionStrategy s_ClusterSelectionStrategy = new(null) { ServerType = ServerType.Cluster }; + /* internal static string ToLower(in RawResult value) { var val = value.GetString(); if (string.IsNullOrWhiteSpace(val)) return val; return val.ToLowerInvariant(); } + */ [RedisCommand(1, LockFree = true)] protected virtual TypedRedisValue Command(RedisClient client, in RedisRequest request) { - var results = TypedRedisValue.Rent(_commands.Count, out var span, ResultType.Array); + var results = TypedRedisValue.Rent(_commands.Count, out var span, RespPrefix.Array); int index = 0; foreach (var pair in _commands) span[index++] = CommandInfo(pair.Value); return results; } - [RedisCommand(-2, "command", "info", LockFree = true)] + [RedisCommand(-2, nameof(RedisCommand.COMMAND), "info", LockFree = true)] protected virtual TypedRedisValue CommandInfo(RedisClient client, in RedisRequest request) { - var results = TypedRedisValue.Rent(request.Count - 2, out var span, ResultType.Array); + var results = TypedRedisValue.Rent(request.Count - 2, out var span, RespPrefix.Array); for (int i = 2; i < request.Count; i++) { - span[i - 2] = request.TryGetCommandBytes(i, out var cmdBytes) - && _commands.TryGetValue(cmdBytes, out var cmdInfo) - ? CommandInfo(cmdInfo) : TypedRedisValue.NullArray(ResultType.Array); + span[i - 2] = _commands.TryGetValue(request.Command, out var cmdInfo) + ? CommandInfo(cmdInfo) : TypedRedisValue.NullArray(RespPrefix.Array); } return results; } - private TypedRedisValue CommandInfo(RespCommand command) + private TypedRedisValue CommandInfo(in RespCommand command) { - var arr = TypedRedisValue.Rent(6, out var span, ResultType.Array); + var arr = TypedRedisValue.Rent(6, out var span, RespPrefix.Array); span[0] = TypedRedisValue.BulkString(command.Command); span[1] = TypedRedisValue.Integer(command.NetArity()); - span[2] = TypedRedisValue.EmptyArray(ResultType.Array); + span[2] = TypedRedisValue.EmptyArray(RespPrefix.Array); span[3] = TypedRedisValue.Zero; span[4] = TypedRedisValue.Zero; span[5] = TypedRedisValue.Zero; diff --git a/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj b/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj index 9908e9088..68d690497 100644 --- a/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj +++ b/toys/StackExchange.Redis.Server/StackExchange.Redis.Server.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + netstandard2.0;net10.0 Basic redis server based on StackExchange.Redis StackExchange.Redis StackExchange.Redis.Server @@ -12,7 +12,8 @@ $(NoWarn);CS1591 - + + diff --git a/toys/StackExchange.Redis.Server/TypedRedisValue.cs b/toys/StackExchange.Redis.Server/TypedRedisValue.cs index d7fa7e4b6..0493399ac 100644 --- a/toys/StackExchange.Redis.Server/TypedRedisValue.cs +++ b/toys/StackExchange.Redis.Server/TypedRedisValue.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using RESPite.Messages; namespace StackExchange.Redis { @@ -11,14 +12,15 @@ public readonly struct TypedRedisValue { // note: if this ever becomes exposed on the public API, it should be made so that it clears; // can't trust external callers to clear the space, and using recycle without that is dangerous - internal static TypedRedisValue Rent(int count, out Span span, ResultType type) + internal static TypedRedisValue Rent(int count, out Span span, RespPrefix type) { if (count == 0) { span = default; return EmptyArray(type); } - var arr = ArrayPool.Shared.Rent(count); + + var arr = ArrayPool.Shared.Rent(count); // new TypedRedisValue[count]; span = new Span(arr, 0, count); return new TypedRedisValue(arr, count, type); } @@ -31,28 +33,28 @@ internal static TypedRedisValue Rent(int count, out Span span, /// /// Returns whether this value is an invalid empty value. /// - public bool IsNil => Type == ResultType.None; + public bool IsNil => Type == RespPrefix.None; /// /// Returns whether this value represents a null array. /// - public bool IsNullArray => IsAggregate && _value.DirectObject == null; + public bool IsNullArray => IsAggregate && _value.IsNull; private readonly RedisValue _value; /// /// The type of value being represented. /// - public ResultType Type { get; } + public RespPrefix Type { get; } /// /// Initialize a TypedRedisValue from a value and optionally a type. /// /// The value to initialize. /// The type of . - private TypedRedisValue(RedisValue value, ResultType? type = null) + private TypedRedisValue(RedisValue value, RespPrefix? type = null) { - Type = type ?? (value.IsInteger ? ResultType.Integer : ResultType.BulkString); + Type = type ?? (value.IsInteger ? RespPrefix.Integer : RespPrefix.BulkString); _value = value; } @@ -61,23 +63,24 @@ private TypedRedisValue(RedisValue value, ResultType? type = null) /// /// The error message. public static TypedRedisValue Error(string value) - => new TypedRedisValue(value, ResultType.Error); + => new TypedRedisValue(value, RespPrefix.SimpleError); /// /// Initialize a TypedRedisValue that represents a simple string. /// /// The string value. public static TypedRedisValue SimpleString(string value) - => new TypedRedisValue(value, ResultType.SimpleString); + => new TypedRedisValue(value, RespPrefix.SimpleString); /// /// The simple string OK. /// public static TypedRedisValue OK { get; } = SimpleString("OK"); + internal static TypedRedisValue Zero { get; } = Integer(0); internal static TypedRedisValue One { get; } = Integer(1); - internal static TypedRedisValue NullArray(ResultType type) => new TypedRedisValue((TypedRedisValue[])null, 0, type); - internal static TypedRedisValue EmptyArray(ResultType type) => new TypedRedisValue([], 0, type); + internal static TypedRedisValue NullArray(RespPrefix type) => new TypedRedisValue((TypedRedisValue[])null, 0, type); + internal static TypedRedisValue EmptyArray(RespPrefix type) => new TypedRedisValue([], 0, type); /// /// Gets the array elements as a span. @@ -86,40 +89,32 @@ public ReadOnlySpan Span { get { - if (!IsAggregate) return default; - var arr = (TypedRedisValue[])_value.DirectObject; - if (arr == null) return default; - var length = (int)_value.DirectOverlappedBits64; - return new ReadOnlySpan(arr, 0, length); - } - } - public ArraySegment Segment - { - get - { - if (!IsAggregate) return default; - var arr = (TypedRedisValue[])_value.DirectObject; - if (arr == null) return default; - var length = (int)_value.DirectOverlappedBits64; - return new ArraySegment(arr, 0, length); + if (_value.TryGetForeign(out var arr, out int index, out var length)) + { + return arr.AsSpan(index, length); + } + + return default; } } - public bool IsAggregate => Type.ToResp2() is ResultType.Array; + public bool IsAggregate => Type is RespPrefix.Array or RespPrefix.Set or RespPrefix.Map or RespPrefix.Push or RespPrefix.Attribute; + public bool IsNullValueOrArray => IsAggregate ? IsNullArray : _value.IsNull; + public bool IsError => Type is RespPrefix.SimpleError or RespPrefix.BulkError; /// /// Initialize a that represents an integer. /// /// The value to initialize from. public static TypedRedisValue Integer(long value) - => new TypedRedisValue(value, ResultType.Integer); + => new TypedRedisValue(value, RespPrefix.Integer); /// /// Initialize a from a . /// /// The items to intialize a value from. - public static TypedRedisValue MultiBulk(ReadOnlySpan items, ResultType type) + public static TypedRedisValue MultiBulk(ReadOnlySpan items, RespPrefix type) { if (items.IsEmpty) return EmptyArray(type); var result = Rent(items.Length, out var span, type); @@ -131,14 +126,19 @@ public static TypedRedisValue MultiBulk(ReadOnlySpan items, Res /// Initialize a from a collection. /// /// The items to intialize a value from. - public static TypedRedisValue MultiBulk(ICollection items, ResultType type) + public static TypedRedisValue MultiBulk(ICollection items, RespPrefix type) { if (items == null) return NullArray(type); int count = items.Count; if (count == 0) return EmptyArray(type); - var arr = ArrayPool.Shared.Rent(count); - items.CopyTo(arr, 0); - return new TypedRedisValue(arr, count, type); + var result = Rent(count, out var span, type); + int i = 0; + foreach (var item in items) + { + span[i++] = item; + } + + return result; } /// @@ -146,40 +146,44 @@ public static TypedRedisValue MultiBulk(ICollection items, Resu /// /// The value to initialize from. public static TypedRedisValue BulkString(RedisValue value) - => new TypedRedisValue(value, ResultType.BulkString); + => new TypedRedisValue(value, RespPrefix.BulkString); /// /// Initialize a that represents a bulk string. /// /// The value to initialize from. public static TypedRedisValue BulkString(in RedisChannel value) - => new TypedRedisValue((byte[])value, ResultType.BulkString); + => new TypedRedisValue((byte[])value, RespPrefix.BulkString); - private TypedRedisValue(TypedRedisValue[] oversizedItems, int count, ResultType type) + private TypedRedisValue(TypedRedisValue[] oversizedItems, int count, RespPrefix type) { if (oversizedItems == null) { if (count != 0) throw new ArgumentOutOfRangeException(nameof(count)); + oversizedItems = []; } else { if (count < 0 || count > oversizedItems.Length) throw new ArgumentOutOfRangeException(nameof(count)); - if (count == 0) oversizedItems = Array.Empty(); + if (count == 0) oversizedItems = []; } - _value = new RedisValue(oversizedItems, count); + + _value = RedisValue.CreateForeign(oversizedItems, 0, count); Type = type; } internal void Recycle(int limit = -1) { - if (_value.DirectObject is TypedRedisValue[] arr) + if (_value.TryGetForeign(out var arr, out var index, out var length)) { - if (limit < 0) limit = (int)_value.DirectOverlappedBits64; - for (int i = 0; i < limit; i++) + if (limit < 0) limit = length; + var span = arr.AsSpan(index, limit); + foreach (ref readonly TypedRedisValue el in span) { - arr[i].Recycle(); + el.Recycle(); } - ArrayPool.Shared.Return(arr, clearArray: false); + span.Clear(); + ArrayPool.Shared.Return(arr, clearArray: false); // we did it ourselves } } @@ -193,12 +197,14 @@ internal void Recycle(int limit = -1) /// public override string ToString() { + if (IsAggregate) return $"{Type}:[{Span.Length}]"; + switch (Type) { - case ResultType.BulkString: - case ResultType.SimpleString: - case ResultType.Integer: - case ResultType.Error: + case RespPrefix.BulkString: + case RespPrefix.SimpleString: + case RespPrefix.Integer: + case RespPrefix.SimpleError: return $"{Type}:{_value}"; default: return IsAggregate ? $"{Type}:[{Span.Length}]" : Type.ToString(); diff --git a/version.json b/version.json index 8d660cad3..c2ded472b 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { - "version": "2.11", - "versionHeightOffset": -10, + "version": "2.12", + "versionHeightOffset": 0, "assemblyVersion": "2.0", "publicReleaseRefSpec": [ "^refs/heads/main$", From bf7f84628fa863bad2770abe64456de718343ea5 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Fri, 13 Mar 2026 09:05:05 +0000 Subject: [PATCH 435/435] 2.12.1 release notes --- docs/ReleaseNotes.md | 12 ++++++++++++ .../PublicAPI/PublicAPI.Shipped.txt | 16 ++++++++++++++++ .../PublicAPI/PublicAPI.Unshipped.txt | 16 ---------------- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 69706f09a..c4cf50c32 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -6,6 +6,18 @@ Current package versions: | ------------ | ----------------- | ----- | | [![StackExchange.Redis](https://img.shields.io/nuget/v/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis](https://img.shields.io/nuget/vpre/StackExchange.Redis.svg)](https://www.nuget.org/packages/StackExchange.Redis/) | [![StackExchange.Redis MyGet](https://img.shields.io/myget/stackoverflow/vpre/StackExchange.Redis.svg)](https://www.myget.org/feed/stackoverflow/package/nuget/StackExchange.Redis) | +## Unreleased + +- (none) + +## 2.12.1 + +- Add missing `LCS` outputs and missing `RedisType.VectorSet` ([#3028 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3028)) +- Track and report multiplexer count ([#3030 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3030)) +- (docs) Add Entra ID authentication docs ([#3023 by @philon-msft](https://github.com/StackExchange/StackExchange.Redis/pull/3023)) +- (eng) Improve test infrastructure (toy-server) ([#3021 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3021), [#3022 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3022), [#3027 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3027), [#3028 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3028)) +- (eng) Pre-V2 work: bring RESPite down, toy-server, migrate to `AsciiHash` ([#3028 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3028)) + ## 2.11.8 * Handle `-MOVED` error pointing to same endpoint. ([#3003 by @barshaul](https://github.com/StackExchange/StackExchange.Redis/pull/3003)) diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index b2f5cab2c..d474fc98d 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -2275,3 +2275,19 @@ StackExchange.Redis.Lease.IsEmpty.get -> bool [SER001]StackExchange.Redis.IDatabase.VectorSetRangeEnumerate(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = 100, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRangeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = -1, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task?>! [SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRangeEnumerateAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = 100, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable! +override StackExchange.Redis.LCSMatchResult.LCSMatch.Equals(object? obj) -> bool +override StackExchange.Redis.LCSMatchResult.LCSMatch.GetHashCode() -> int +override StackExchange.Redis.LCSMatchResult.LCSMatch.ToString() -> string! +override StackExchange.Redis.LCSMatchResult.LCSPosition.Equals(object? obj) -> bool +override StackExchange.Redis.LCSMatchResult.LCSPosition.GetHashCode() -> int +override StackExchange.Redis.LCSMatchResult.LCSPosition.ToString() -> string! +StackExchange.Redis.LCSMatchResult.LCSMatch.Equals(in StackExchange.Redis.LCSMatchResult.LCSMatch other) -> bool +StackExchange.Redis.LCSMatchResult.LCSMatch.First.get -> StackExchange.Redis.LCSMatchResult.LCSPosition +StackExchange.Redis.LCSMatchResult.LCSMatch.Second.get -> StackExchange.Redis.LCSMatchResult.LCSPosition +StackExchange.Redis.LCSMatchResult.LCSPosition +StackExchange.Redis.LCSMatchResult.LCSPosition.End.get -> long +StackExchange.Redis.LCSMatchResult.LCSPosition.Equals(in StackExchange.Redis.LCSMatchResult.LCSPosition other) -> bool +StackExchange.Redis.LCSMatchResult.LCSPosition.LCSPosition() -> void +StackExchange.Redis.LCSMatchResult.LCSPosition.LCSPosition(long start, long end) -> void +StackExchange.Redis.LCSMatchResult.LCSPosition.Start.get -> long +StackExchange.Redis.RedisType.VectorSet = 8 -> StackExchange.Redis.RedisType diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index 797632e41..ab058de62 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1,17 +1 @@ #nullable enable -override StackExchange.Redis.LCSMatchResult.LCSMatch.Equals(object? obj) -> bool -override StackExchange.Redis.LCSMatchResult.LCSMatch.GetHashCode() -> int -override StackExchange.Redis.LCSMatchResult.LCSMatch.ToString() -> string! -override StackExchange.Redis.LCSMatchResult.LCSPosition.Equals(object? obj) -> bool -override StackExchange.Redis.LCSMatchResult.LCSPosition.GetHashCode() -> int -override StackExchange.Redis.LCSMatchResult.LCSPosition.ToString() -> string! -StackExchange.Redis.LCSMatchResult.LCSMatch.Equals(in StackExchange.Redis.LCSMatchResult.LCSMatch other) -> bool -StackExchange.Redis.LCSMatchResult.LCSMatch.First.get -> StackExchange.Redis.LCSMatchResult.LCSPosition -StackExchange.Redis.LCSMatchResult.LCSMatch.Second.get -> StackExchange.Redis.LCSMatchResult.LCSPosition -StackExchange.Redis.LCSMatchResult.LCSPosition -StackExchange.Redis.LCSMatchResult.LCSPosition.End.get -> long -StackExchange.Redis.LCSMatchResult.LCSPosition.Equals(in StackExchange.Redis.LCSMatchResult.LCSPosition other) -> bool -StackExchange.Redis.LCSMatchResult.LCSPosition.LCSPosition() -> void -StackExchange.Redis.LCSMatchResult.LCSPosition.LCSPosition(long start, long end) -> void -StackExchange.Redis.LCSMatchResult.LCSPosition.Start.get -> long -StackExchange.Redis.RedisType.VectorSet = 8 -> StackExchange.Redis.RedisType